├── .gitignore ├── LICENSE.md ├── Procfile ├── README.md ├── frontend ├── Experience │ ├── Camera.js │ ├── Elements.js │ ├── Experience.js │ ├── Preloader.js │ ├── Renderer.js │ ├── Utils │ │ ├── CustomOrbitControls.js │ │ ├── Loaders.js │ │ ├── Resources.js │ │ ├── Sizes.js │ │ ├── Time.js │ │ ├── assets.js │ │ └── functions │ │ │ ├── elements.js │ │ │ └── lerp.js │ └── World │ │ ├── Environment.js │ │ ├── Player │ │ ├── Avatar.js │ │ ├── Nametag.js │ │ └── Player.js │ │ ├── Westgate.js │ │ └── World.js ├── index.html ├── index.js ├── index.scss └── styles │ ├── components │ ├── canvas.scss │ ├── chatbox.scss │ ├── controlsUI.scss │ ├── menu.scss │ ├── nameinput.scss │ └── preloader.scss │ ├── defaults │ ├── defaults.scss │ ├── fonts.scss │ ├── reset.scss │ └── variables.scss │ └── utils │ └── reusables.scss ├── package-lock.json ├── package.json ├── public ├── draco │ ├── README.md │ ├── draco_decoder.js │ ├── draco_decoder.wasm │ ├── draco_encoder.js │ ├── draco_wasm_wrapper.js │ └── gltf │ │ ├── draco_decoder.js │ │ ├── draco_decoder.wasm │ │ ├── draco_encoder.js │ │ └── draco_wasm_wrapper.js ├── fonts │ ├── Gilroy-Black.woff │ ├── Gilroy-Black.woff2 │ ├── Gilroy-BlackItalic.woff │ ├── Gilroy-BlackItalic.woff2 │ ├── Gilroy-Bold.woff │ ├── Gilroy-Bold.woff2 │ ├── Gilroy-BoldItalic.woff │ ├── Gilroy-BoldItalic.woff2 │ ├── Gilroy-ExtraBold.woff │ ├── Gilroy-ExtraBold.woff2 │ ├── Gilroy-ExtraBoldItalic.woff │ ├── Gilroy-ExtraBoldItalic.woff2 │ ├── Gilroy-Heavy.woff │ ├── Gilroy-Heavy.woff2 │ ├── Gilroy-HeavyItalic.woff │ ├── Gilroy-HeavyItalic.woff2 │ ├── Gilroy-Light.woff │ ├── Gilroy-Light.woff2 │ ├── Gilroy-LightItalic.woff │ ├── Gilroy-LightItalic.woff2 │ ├── Gilroy-Medium.woff │ ├── Gilroy-Medium.woff2 │ ├── Gilroy-MediumItalic.woff │ ├── Gilroy-MediumItalic.woff2 │ ├── Gilroy-Regular.woff │ ├── Gilroy-Regular.woff2 │ ├── Gilroy-RegularItalic.woff │ ├── Gilroy-RegularItalic.woff2 │ ├── Gilroy-SemiBold.woff │ ├── Gilroy-SemiBold.woff2 │ ├── Gilroy-SemiBoldItalic.woff │ ├── Gilroy-SemiBoldItalic.woff2 │ ├── Gilroy-Thin.woff │ ├── Gilroy-Thin.woff2 │ ├── Gilroy-ThinItalic.woff │ ├── Gilroy-ThinItalic.woff2 │ ├── Gilroy-UltraLight.woff │ ├── Gilroy-UltraLight.woff2 │ ├── Gilroy-UltraLightItalic.woff │ └── Gilroy-UltraLightItalic.woff2 ├── images │ ├── asian_female.png │ ├── asian_female_head.png │ ├── asian_male.png │ └── asian_male_head.png ├── media │ ├── android-chrome-192x192.png │ ├── android-chrome-384x384.png │ ├── apple-touch-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── mstile-150x150.png │ ├── music.mp3 │ └── site.webmanifest ├── models │ ├── asian_female_animated.glb │ ├── asian_male_animated.glb │ ├── bars.glb │ ├── box.glb │ ├── brick.glb │ ├── buildings.glb │ ├── collider.glb │ ├── easter.glb │ ├── everything.glb │ ├── floor.glb │ ├── glass.glb │ ├── grass.glb │ ├── other.glb │ ├── outside.glb │ ├── panera.glb │ ├── plastic.glb │ ├── screen.glb │ ├── tables.glb │ └── thirdfloor.glb ├── textures │ ├── baked │ │ ├── bars.jpg │ │ ├── box.jpg │ │ ├── brick.jpg │ │ ├── buildings.jpg │ │ ├── easter.jpg │ │ ├── everything.jpg │ │ ├── floor.jpg │ │ ├── grass.jpg │ │ ├── other.webp │ │ ├── outside.jpg │ │ ├── panera.jpg │ │ ├── plastic.jpg │ │ ├── tables.jpg │ │ └── thirdfloor.jpg │ └── environment │ │ ├── nx.png │ │ ├── ny.png │ │ ├── nz.png │ │ ├── px.png │ │ ├── py.png │ │ └── pz.png └── videos │ └── tour.mp4 ├── server.js └── vite.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | .env 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Andrew Woan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: npm run backend-build 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PSU VR 2 | 3 | **[Live demo](https://psu-vr.herokuapp.com/)** 4 | 5 | PSU VR is an immersive multiplayer virtual reality experience of Penn State's University Park Campus. 6 | 7 | **⚠️ This project is currently under development ⚠️** 8 | 9 | ![screenshot of PSU VR](https://github.com/andrewwoan/PSU-VR/assets/50236987/f36f81b1-377f-4b16-aa2f-49c5944de7e4) 10 | 11 | ## Developer Instructions 12 | 13 | ``` 14 | npm install 15 | npm run dev 16 | ``` 17 | 18 | You can use the "network" dev server running and open it on your mobile device if your computer is connected to the same network. 19 | -------------------------------------------------------------------------------- /frontend/Experience/Camera.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import Experience from "./Experience.js"; 3 | import { OrbitControls } from "../Experience/Utils/CustomOrbitControls.js"; 4 | 5 | export default class Camera { 6 | constructor() { 7 | this.experience = new Experience(); 8 | this.sizes = this.experience.sizes; 9 | this.scene = this.experience.scene; 10 | this.canvas = this.experience.canvas; 11 | this.params = { 12 | fov: 75, 13 | aspect: this.sizes.aspect, 14 | near: 0.001, 15 | far: 1000, 16 | }; 17 | this.controls = null; 18 | 19 | this.setPerspectiveCamera(); 20 | this.setOrbitControls(); 21 | } 22 | 23 | setPerspectiveCamera() { 24 | this.perspectiveCamera = new THREE.PerspectiveCamera( 25 | this.params.fov, 26 | this.params.aspect, 27 | this.params.near, 28 | this.params.far 29 | ); 30 | 31 | this.perspectiveCamera.position.set(17.8838, 1.2 + 10, -3.72508); 32 | this.perspectiveCamera.rotation.y = Math.PI / 2; 33 | 34 | this.scene.add(this.perspectiveCamera); 35 | } 36 | 37 | setOrbitControls() { 38 | this.controls = new OrbitControls(this.perspectiveCamera, this.canvas); 39 | this.controls.enableDamping = true; 40 | // this.controls.enableZoom = true; 41 | this.controls.enablePan = false; 42 | // this.controls.maxPolarAngle = Math.PI / 2; 43 | // this.controls.minDistance = 0.1; 44 | this.controls.maxDistance = 6; 45 | 46 | this.controls.dampingFactor = 0.1; 47 | } 48 | 49 | enableOrbitControls() { 50 | this.controls.enabled = true; 51 | } 52 | 53 | disableOrbitControls() { 54 | this.controls.enabled = false; 55 | } 56 | 57 | onResize() { 58 | this.perspectiveCamera.aspect = this.sizes.aspect; 59 | this.perspectiveCamera.updateProjectionMatrix(); 60 | } 61 | 62 | update() { 63 | if (!this.controls) return; 64 | if (this.controls.enabled === true) { 65 | this.controls.update(); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /frontend/Experience/Elements.js: -------------------------------------------------------------------------------- 1 | export default class Elements { 2 | constructor() { 3 | this.domSelectors = { 4 | crosshair: ".crosshair", 5 | }; 6 | 7 | this.init(); 8 | } 9 | 10 | init() {} 11 | } 12 | -------------------------------------------------------------------------------- /frontend/Experience/Experience.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | 3 | import Sizes from "./Utils/Sizes.js"; 4 | import Time from "./Utils/Time.js"; 5 | import Resources from "./Utils/Resources.js"; 6 | import assets from "./Utils/assets.js"; 7 | 8 | import Camera from "./Camera.js"; 9 | import Renderer from "./Renderer.js"; 10 | import Preloader from "./Preloader.js"; 11 | 12 | import World from "./World/World.js"; 13 | 14 | export default class Experience { 15 | static instance; 16 | 17 | constructor(canvas, socket) { 18 | if (Experience.instance) { 19 | return Experience.instance; 20 | } 21 | 22 | Experience.instance = this; 23 | 24 | this.canvas = canvas; 25 | this.socket = socket; 26 | this.sizes = new Sizes(); 27 | this.time = new Time(); 28 | 29 | this.setScene(); 30 | this.setCamera(); 31 | this.setRenderer(); 32 | this.setResources(); 33 | this.setPreloader(); 34 | this.setWorld(); 35 | 36 | this.sizes.on("resize", () => { 37 | this.onResize(); 38 | }); 39 | 40 | this.update(); 41 | } 42 | 43 | setScene() { 44 | this.scene = new THREE.Scene(); 45 | } 46 | 47 | setCamera() { 48 | this.camera = new Camera(); 49 | } 50 | 51 | setRenderer() { 52 | this.renderer = new Renderer(); 53 | } 54 | 55 | setResources() { 56 | this.resources = new Resources(assets); 57 | } 58 | 59 | setPreloader() { 60 | this.preloader = new Preloader(); 61 | } 62 | 63 | setWorld() { 64 | this.world = new World(); 65 | } 66 | 67 | onResize() { 68 | this.camera.onResize(); 69 | this.renderer.onResize(); 70 | } 71 | 72 | update() { 73 | if (this.preloader) this.preloader.update(); 74 | if (this.camera) this.camera.update(); 75 | if (this.renderer) this.renderer.update(); 76 | if (this.world) this.world.update(); 77 | if (this.time) this.time.update(); 78 | 79 | window.requestAnimationFrame(() => { 80 | this.update(); 81 | }); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /frontend/Experience/Preloader.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import Experience from "./Experience.js"; 3 | 4 | import lerp from "./Utils/functions/lerp.js"; 5 | import elements from "./Utils/functions/elements.js"; 6 | 7 | import gsap from "gsap"; 8 | 9 | export default class Preloader { 10 | constructor() { 11 | this.experience = new Experience(); 12 | this.resources = this.experience.resources; 13 | 14 | this.matchmedia = gsap.matchMedia(); 15 | 16 | this.loaded = 0; 17 | this.queue = 0; 18 | 19 | this.counter = 0; 20 | this.amountDone = 0; 21 | 22 | this.domElements = elements({ 23 | preloader: ".preloader", 24 | text1: ".preloader-percentage1", 25 | text2: ".preloader-percentage2", 26 | progressBar: ".progress-bar", 27 | svgLogo: ".svgLogo", 28 | progressBarContainer: ".progress-bar-container", 29 | progressWrapper: ".progress-wrapper", 30 | preloaderTitle: ".preloader-title", 31 | preloaderWrapper: ".preloader-wrapper", 32 | welcomeTitle: ".welcome-title", 33 | nameForm: ".name-form", 34 | nameInput: "#name-input", 35 | nameInputButton: "#name-input-button", 36 | characterSelectTitle: ".character-select-title", 37 | avatarWrapper: ".avatar-img-wrapper", 38 | avatarLeftImg: ".avatar-left", 39 | avatarRightImg: ".avatar-right", 40 | customizeButton: ".customize-character-btn", 41 | description: ".description", 42 | }); 43 | 44 | // **** This is for updating a percentage **** 45 | this.resources.on("loading", (loaded, queue) => { 46 | this.updateProgress(loaded, queue); 47 | }); 48 | 49 | this.resources.on("ready", () => { 50 | this.playIntro(); 51 | }); 52 | 53 | this.addEventListeners(); 54 | } 55 | 56 | updateProgress(loaded, queue) { 57 | this.amountDone = Math.round((loaded / queue) * 100); 58 | } 59 | 60 | async playIntro() { 61 | return new Promise((resolve) => { 62 | this.timeline = new gsap.timeline(); 63 | this.timeline 64 | .to(this.domElements.svgLogo, { 65 | opacity: 0, 66 | duration: 1.2, 67 | delay: 2.2, 68 | top: "-120%", 69 | ease: "power4.out", 70 | }) 71 | .to( 72 | this.domElements.progressBarContainer, 73 | { 74 | opacity: 0, 75 | duration: 1.2, 76 | top: "30%", 77 | ease: "power4.out", 78 | }, 79 | "-=1.05" 80 | ) 81 | .to( 82 | this.domElements.progressWrapper, 83 | { 84 | opacity: 0, 85 | duration: 1.2, 86 | bottom: "21%", 87 | ease: "power4.out", 88 | }, 89 | "-=1.05" 90 | ) 91 | .to( 92 | this.domElements.description, 93 | { 94 | opacity: 0, 95 | duration: 1.2, 96 | bottom: "35%", 97 | ease: "power4.out", 98 | }, 99 | "-=1.05" 100 | ) 101 | .to( 102 | this.domElements.preloaderTitle, 103 | { 104 | opacity: 0, 105 | duration: 1.2, 106 | bottom: "18%", 107 | ease: "power4.out", 108 | onUpdate: () => { 109 | this.domElements.preloaderTitle.classList.remove( 110 | "fade-in-out" 111 | ); 112 | }, 113 | 114 | onComplete: () => { 115 | this.domElements.svgLogo.remove(); 116 | this.domElements.progressBarContainer.remove(); 117 | this.domElements.progressWrapper.remove(); 118 | this.domElements.preloaderTitle.remove(); 119 | this.domElements.preloaderWrapper.remove(); 120 | }, 121 | }, 122 | "-=1.05" 123 | ) 124 | .to( 125 | this.domElements.welcomeTitle, 126 | { 127 | opacity: 1, 128 | duration: 1.2, 129 | top: "37%", 130 | ease: "power4.out", 131 | }, 132 | "-=1" 133 | ) 134 | .to( 135 | this.domElements.nameForm, 136 | { 137 | opacity: 1, 138 | duration: 1.2, 139 | top: "50%", 140 | ease: "power4.out", 141 | }, 142 | "-=1" 143 | ) 144 | .to( 145 | this.domElements.nameInputButton, 146 | { 147 | opacity: 1, 148 | duration: 1.2, 149 | bottom: "39%", 150 | ease: "power4.out", 151 | onComplete: () => { 152 | // this.domElements.preloader.remove(); 153 | resolve; 154 | }, 155 | }, 156 | "-=1" 157 | ); 158 | }); 159 | } 160 | 161 | onNameInput = () => { 162 | if (this.domElements.nameInput.value === "") return; 163 | this.nameInputOutro(); 164 | }; 165 | 166 | onCharacterSelect = () => { 167 | this.preloaderOutro(); 168 | }; 169 | 170 | async nameInputOutro() { 171 | return new Promise((resolve) => { 172 | this.timeline2 = new gsap.timeline(); 173 | this.timeline2 174 | .to(this.domElements.welcomeTitle, { 175 | opacity: 0, 176 | duration: 1.2, 177 | top: "34%", 178 | ease: "power4.out", 179 | }) 180 | .to( 181 | this.domElements.nameForm, 182 | { 183 | opacity: 0, 184 | duration: 1.2, 185 | top: "44%", 186 | ease: "power4.out", 187 | }, 188 | "-=1.05" 189 | ) 190 | .to( 191 | this.domElements.nameInputButton, 192 | { 193 | opacity: 0, 194 | duration: 1.2, 195 | bottom: "47%", 196 | ease: "power4.out", 197 | onComplete: () => { 198 | this.domElements.welcomeTitle.remove(); 199 | this.domElements.nameForm.remove(); 200 | this.domElements.nameInputButton.remove(); 201 | this.domElements.avatarLeftImg.style.pointerEvents = 202 | "auto"; 203 | this.domElements.avatarRightImg.style.pointerEvents = 204 | "auto"; 205 | }, 206 | }, 207 | "-=1.05" 208 | ) 209 | .to( 210 | this.domElements.characterSelectTitle, 211 | { 212 | opacity: 1, 213 | duration: 1.2, 214 | top: "20%", 215 | ease: "power4.out", 216 | }, 217 | "-=1.05" 218 | ) 219 | .to( 220 | this.domElements.avatarWrapper, 221 | { 222 | opacity: 1, 223 | duration: 1.2, 224 | bottom: "47%", 225 | ease: "power4.out", 226 | }, 227 | "-=1.05" 228 | ) 229 | .to( 230 | this.domElements.customizeButton, 231 | { 232 | opacity: 1, 233 | duration: 1.2, 234 | bottom: "25%", 235 | ease: "power4.out", 236 | }, 237 | "-=1.05" 238 | ); 239 | }); 240 | } 241 | 242 | async preloaderOutro() { 243 | return new Promise((resolve) => { 244 | this.timeline3 = new gsap.timeline(); 245 | this.timeline3.to(this.domElements.preloader, { 246 | duration: 1.7, 247 | // top: "-150%", 248 | opacity: 0, 249 | ease: "power3.out", 250 | onComplete: () => { 251 | this.domElements.preloader.remove(); 252 | resolve; 253 | }, 254 | }); 255 | }); 256 | } 257 | 258 | addEventListeners() { 259 | this.domElements.nameInputButton.addEventListener( 260 | "click", 261 | this.onNameInput 262 | ); 263 | this.domElements.avatarLeftImg.addEventListener( 264 | "click", 265 | this.onCharacterSelect 266 | ); 267 | this.domElements.avatarRightImg.addEventListener( 268 | "click", 269 | this.onCharacterSelect 270 | ); 271 | } 272 | 273 | update() { 274 | if (this.counter < this.amountDone) { 275 | this.counter++; 276 | this.domElements.text1.innerText = Math.round(this.counter / 10); 277 | 278 | if (Math.round(this.counter / 10) !== 10) { 279 | this.domElements.text2.innerText = Math.round( 280 | this.counter % 10 281 | ); 282 | this.flag = false; 283 | } else { 284 | this.domElements.text2.innerText = 0; 285 | this.flag = true; 286 | } 287 | 288 | this.domElements.progressBar.style.width = 289 | Math.round(this.counter) + "%"; 290 | 291 | if (this.flag) { 292 | this.domElements.progressBar.style.width = "100%"; 293 | } 294 | } 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /frontend/Experience/Renderer.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import Experience from "./Experience.js"; 3 | 4 | export default class Renderer { 5 | constructor() { 6 | this.experience = new Experience(); 7 | this.sizes = this.experience.sizes; 8 | this.scene = this.experience.scene; 9 | this.canvas = this.experience.canvas; 10 | this.camera = this.experience.camera; 11 | 12 | this.setRenderer(); 13 | } 14 | 15 | setRenderer() { 16 | this.renderer = new THREE.WebGLRenderer({ 17 | canvas: this.canvas, 18 | antialias: true, 19 | logarithmicDepthBuffer: true, // Get rid of z-fighting 20 | }); 21 | this.renderer.outputColorSpace = THREE.SRGBColorSpace; 22 | this.renderer.toneMapping = THREE.CineonToneMapping; 23 | this.renderer.toneMappingExposure = 1.5; 24 | this.renderer.setSize(this.sizes.width, this.sizes.height); 25 | this.renderer.setPixelRatio(this.sizes.pixelRatio); 26 | } 27 | 28 | onResize() { 29 | this.renderer.setSize(this.sizes.width, this.sizes.height); 30 | this.renderer.setPixelRatio(this.sizes.pixelRatio); 31 | } 32 | 33 | update() { 34 | this.renderer.render(this.scene, this.camera.perspectiveCamera); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /frontend/Experience/Utils/CustomOrbitControls.js: -------------------------------------------------------------------------------- 1 | import { 2 | EventDispatcher, 3 | MOUSE, 4 | Quaternion, 5 | Spherical, 6 | TOUCH, 7 | Vector2, 8 | Vector3, 9 | } from "three"; 10 | 11 | // This set of controls performs orbiting, dollying (zooming), and panning. 12 | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). 13 | // 14 | // Orbit - left mouse / touch: one-finger move 15 | // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish 16 | // Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move 17 | 18 | const _changeEvent = { type: "change" }; 19 | const _startEvent = { type: "start" }; 20 | const _endEvent = { type: "end" }; 21 | 22 | class OrbitControls extends EventDispatcher { 23 | constructor(object, domElement) { 24 | super(); 25 | 26 | this.object = object; 27 | this.domElement = domElement; 28 | this.domElement.style.touchAction = "none"; // disable touch scroll 29 | 30 | // Set to false to disable this control 31 | this.enabled = true; 32 | 33 | // "target" sets the location of focus, where the object orbits around 34 | this.target = new Vector3(); 35 | 36 | // How far you can dolly in and out ( PerspectiveCamera only ) 37 | this.minDistance = 0; 38 | this.maxDistance = Infinity; 39 | 40 | // How far you can zoom in and out ( OrthographicCamera only ) 41 | this.minZoom = 0; 42 | this.maxZoom = Infinity; 43 | 44 | // How far you can orbit vertically, upper and lower limits. 45 | // Range is 0 to Math.PI radians. 46 | this.minPolarAngle = 0; // radians 47 | this.maxPolarAngle = Math.PI; // radians 48 | 49 | // How far you can orbit horizontally, upper and lower limits. 50 | // If set, the interval [ min, max ] must be a sub-interval of [ - 2 PI, 2 PI ], with ( max - min < 2 PI ) 51 | this.minAzimuthAngle = -Infinity; // radians 52 | this.maxAzimuthAngle = Infinity; // radians 53 | 54 | // Set to true to enable damping (inertia) 55 | // If damping is enabled, you must call controls.update() in your animation loop 56 | this.enableDamping = false; 57 | this.dampingFactor = 0.05; 58 | 59 | // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. 60 | // Set to false to disable zooming 61 | this.enableZoom = true; 62 | this.zoomSpeed = 1.0; 63 | 64 | // Set to false to disable rotating 65 | this.enableRotate = true; 66 | this.rotateSpeed = 1.0; 67 | 68 | // Set to false to disable panning 69 | this.enablePan = true; 70 | this.panSpeed = 1.0; 71 | this.screenSpacePanning = true; // if false, pan orthogonal to world-space direction camera.up 72 | this.keyPanSpeed = 7.0; // pixels moved per arrow key push 73 | 74 | // Set to true to automatically rotate around the target 75 | // If auto-rotate is enabled, you must call controls.update() in your animation loop 76 | this.autoRotate = false; 77 | this.autoRotateSpeed = 2.0; // 30 seconds per orbit when fps is 60 78 | 79 | // The four arrow keys 80 | this.keys = { 81 | LEFT: "ArrowLeft", 82 | UP: "ArrowUp", 83 | RIGHT: "ArrowRight", 84 | BOTTOM: "ArrowDown", 85 | }; 86 | 87 | // Mouse buttons 88 | this.mouseButtons = { 89 | LEFT: MOUSE.ROTATE, 90 | MIDDLE: MOUSE.DOLLY, 91 | RIGHT: MOUSE.PAN, 92 | }; 93 | 94 | // Touch fingers 95 | this.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN }; 96 | 97 | // for reset 98 | this.target0 = this.target.clone(); 99 | this.position0 = this.object.position.clone(); 100 | this.zoom0 = this.object.zoom; 101 | 102 | // the target DOM element for key events 103 | this._domElementKeyEvents = null; 104 | 105 | // 106 | // public methods 107 | // 108 | 109 | this.getPolarAngle = function () { 110 | return spherical.phi; 111 | }; 112 | 113 | this.getAzimuthalAngle = function () { 114 | return spherical.theta; 115 | }; 116 | 117 | this.getDistance = function () { 118 | return this.object.position.distanceTo(this.target); 119 | }; 120 | 121 | this.listenToKeyEvents = function (domElement) { 122 | domElement.addEventListener("keydown", onKeyDown); 123 | this._domElementKeyEvents = domElement; 124 | }; 125 | 126 | this.stopListenToKeyEvents = function () { 127 | this._domElementKeyEvents.removeEventListener("keydown", onKeyDown); 128 | this._domElementKeyEvents = null; 129 | }; 130 | 131 | this.saveState = function () { 132 | scope.target0.copy(scope.target); 133 | scope.position0.copy(scope.object.position); 134 | scope.zoom0 = scope.object.zoom; 135 | }; 136 | 137 | this.reset = function () { 138 | scope.target.copy(scope.target0); 139 | scope.object.position.copy(scope.position0); 140 | scope.object.zoom = scope.zoom0; 141 | 142 | scope.object.updateProjectionMatrix(); 143 | scope.dispatchEvent(_changeEvent); 144 | 145 | scope.update(); 146 | 147 | state = STATE.NONE; 148 | }; 149 | 150 | // this method is exposed, but perhaps it would be better if we can make it private... 151 | this.update = (function () { 152 | const offset = new Vector3(); 153 | 154 | // so camera.up is the orbit axis 155 | const quat = new Quaternion().setFromUnitVectors( 156 | object.up, 157 | new Vector3(0, 1, 0) 158 | ); 159 | const quatInverse = quat.clone().invert(); 160 | 161 | const lastPosition = new Vector3(); 162 | const lastQuaternion = new Quaternion(); 163 | 164 | const twoPI = 2 * Math.PI; 165 | 166 | return function update() { 167 | const position = scope.object.position; 168 | 169 | offset.copy(position).sub(scope.target); 170 | 171 | // rotate offset to "y-axis-is-up" space 172 | offset.applyQuaternion(quat); 173 | 174 | // angle from z-axis around y-axis 175 | spherical.setFromVector3(offset); 176 | 177 | if (scope.autoRotate && state === STATE.NONE) { 178 | rotateLeft(getAutoRotationAngle()); 179 | } 180 | 181 | if (scope.enableDamping) { 182 | spherical.theta += 183 | sphericalDelta.theta * scope.dampingFactor; 184 | spherical.phi += sphericalDelta.phi * scope.dampingFactor; 185 | } else { 186 | spherical.theta += sphericalDelta.theta; 187 | spherical.phi += sphericalDelta.phi; 188 | } 189 | 190 | // restrict theta to be between desired limits 191 | 192 | let min = scope.minAzimuthAngle; 193 | let max = scope.maxAzimuthAngle; 194 | 195 | if (isFinite(min) && isFinite(max)) { 196 | if (min < -Math.PI) min += twoPI; 197 | else if (min > Math.PI) min -= twoPI; 198 | 199 | if (max < -Math.PI) max += twoPI; 200 | else if (max > Math.PI) max -= twoPI; 201 | 202 | if (min <= max) { 203 | spherical.theta = Math.max( 204 | min, 205 | Math.min(max, spherical.theta) 206 | ); 207 | } else { 208 | spherical.theta = 209 | spherical.theta > (min + max) / 2 210 | ? Math.max(min, spherical.theta) 211 | : Math.min(max, spherical.theta); 212 | } 213 | } 214 | 215 | // restrict phi to be between desired limits 216 | spherical.phi = Math.max( 217 | scope.minPolarAngle, 218 | Math.min(scope.maxPolarAngle, spherical.phi) 219 | ); 220 | 221 | spherical.makeSafe(); 222 | 223 | spherical.radius *= scale; 224 | 225 | // restrict radius to be between desired limits 226 | spherical.radius = Math.max( 227 | scope.minDistance, 228 | Math.min(scope.maxDistance, spherical.radius) 229 | ); 230 | 231 | // move target to panned location 232 | 233 | if (scope.enableDamping === true) { 234 | scope.target.addScaledVector( 235 | panOffset, 236 | scope.dampingFactor 237 | ); 238 | } else { 239 | scope.target.add(panOffset); 240 | } 241 | 242 | offset.setFromSpherical(spherical); 243 | 244 | // rotate offset back to "camera-up-vector-is-up" space 245 | offset.applyQuaternion(quatInverse); 246 | 247 | position.copy(scope.target).add(offset); 248 | 249 | scope.object.lookAt(scope.target); 250 | 251 | if (scope.enableDamping === true) { 252 | sphericalDelta.theta *= 1 - scope.dampingFactor; 253 | sphericalDelta.phi *= 1 - scope.dampingFactor; 254 | 255 | panOffset.multiplyScalar(1 - scope.dampingFactor); 256 | } else { 257 | sphericalDelta.set(0, 0, 0); 258 | 259 | panOffset.set(0, 0, 0); 260 | } 261 | 262 | scale = 1; 263 | 264 | // update condition is: 265 | // min(camera displacement, camera rotation in radians)^2 > EPS 266 | // using small-angle approximation cos(x/2) = 1 - x^2 / 8 267 | 268 | if ( 269 | zoomChanged || 270 | lastPosition.distanceToSquared(scope.object.position) > 271 | EPS || 272 | 8 * (1 - lastQuaternion.dot(scope.object.quaternion)) > EPS 273 | ) { 274 | scope.dispatchEvent(_changeEvent); 275 | 276 | lastPosition.copy(scope.object.position); 277 | lastQuaternion.copy(scope.object.quaternion); 278 | zoomChanged = false; 279 | 280 | return true; 281 | } 282 | 283 | return false; 284 | }; 285 | })(); 286 | 287 | this.dispose = function () { 288 | scope.domElement.removeEventListener("contextmenu", onContextMenu); 289 | 290 | scope.domElement.removeEventListener("pointerdown", onPointerDown); 291 | scope.domElement.removeEventListener( 292 | "pointercancel", 293 | onPointerCancel 294 | ); 295 | scope.domElement.removeEventListener("wheel", onMouseWheel); 296 | 297 | scope.domElement.removeEventListener("pointermove", onPointerMove); 298 | scope.domElement.removeEventListener("pointerup", onPointerUp); 299 | 300 | if (scope._domElementKeyEvents !== null) { 301 | scope._domElementKeyEvents.removeEventListener( 302 | "keydown", 303 | onKeyDown 304 | ); 305 | scope._domElementKeyEvents = null; 306 | } 307 | 308 | //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here? 309 | }; 310 | 311 | // 312 | // internals 313 | // 314 | 315 | const scope = this; 316 | 317 | const STATE = { 318 | NONE: -1, 319 | ROTATE: 0, 320 | DOLLY: 1, 321 | PAN: 2, 322 | TOUCH_ROTATE: 3, 323 | TOUCH_PAN: 4, 324 | TOUCH_DOLLY_PAN: 5, 325 | TOUCH_DOLLY_ROTATE: 6, 326 | }; 327 | 328 | let state = STATE.NONE; 329 | 330 | const EPS = 0.000001; 331 | 332 | // current position in spherical coordinates 333 | const spherical = new Spherical(); 334 | const sphericalDelta = new Spherical(); 335 | 336 | let scale = 1; 337 | const panOffset = new Vector3(); 338 | let zoomChanged = false; 339 | 340 | const rotateStart = new Vector2(); 341 | const rotateEnd = new Vector2(); 342 | const rotateDelta = new Vector2(); 343 | 344 | const panStart = new Vector2(); 345 | const panEnd = new Vector2(); 346 | const panDelta = new Vector2(); 347 | 348 | const dollyStart = new Vector2(); 349 | const dollyEnd = new Vector2(); 350 | const dollyDelta = new Vector2(); 351 | 352 | const pointers = []; 353 | const pointerPositions = {}; 354 | 355 | function getAutoRotationAngle() { 356 | return ((2 * Math.PI) / 60 / 60) * scope.autoRotateSpeed; 357 | } 358 | 359 | function getZoomScale() { 360 | return Math.pow(0.95, scope.zoomSpeed); 361 | } 362 | 363 | function rotateLeft(angle) { 364 | sphericalDelta.theta -= angle; 365 | 366 | } 367 | 368 | function rotateUp(angle) { 369 | sphericalDelta.phi -= angle; 370 | } 371 | 372 | const panLeft = (function () { 373 | const v = new Vector3(); 374 | 375 | return function panLeft(distance, objectMatrix) { 376 | v.setFromMatrixColumn(objectMatrix, 0); // get X column of objectMatrix 377 | v.multiplyScalar(-distance); 378 | 379 | panOffset.add(v); 380 | }; 381 | })(); 382 | 383 | const panUp = (function () { 384 | const v = new Vector3(); 385 | 386 | return function panUp(distance, objectMatrix) { 387 | if (scope.screenSpacePanning === true) { 388 | v.setFromMatrixColumn(objectMatrix, 1); 389 | } else { 390 | v.setFromMatrixColumn(objectMatrix, 0); 391 | v.crossVectors(scope.object.up, v); 392 | } 393 | 394 | v.multiplyScalar(distance); 395 | 396 | panOffset.add(v); 397 | }; 398 | })(); 399 | 400 | // deltaX and deltaY are in pixels; right and down are positive 401 | const pan = (function () { 402 | const offset = new Vector3(); 403 | 404 | return function pan(deltaX, deltaY) { 405 | const element = scope.domElement; 406 | 407 | if (scope.object.isPerspectiveCamera) { 408 | // perspective 409 | const position = scope.object.position; 410 | offset.copy(position).sub(scope.target); 411 | let targetDistance = offset.length(); 412 | 413 | // half of the fov is center to top of screen 414 | targetDistance *= Math.tan( 415 | ((scope.object.fov / 2) * Math.PI) / 180.0 416 | ); 417 | 418 | // we use only clientHeight here so aspect ratio does not distort speed 419 | panLeft( 420 | (2 * deltaX * targetDistance) / element.clientHeight, 421 | scope.object.matrix 422 | ); 423 | panUp( 424 | (2 * deltaY * targetDistance) / element.clientHeight, 425 | scope.object.matrix 426 | ); 427 | } else if (scope.object.isOrthographicCamera) { 428 | // orthographic 429 | panLeft( 430 | (deltaX * (scope.object.right - scope.object.left)) / 431 | scope.object.zoom / 432 | element.clientWidth, 433 | scope.object.matrix 434 | ); 435 | panUp( 436 | (deltaY * (scope.object.top - scope.object.bottom)) / 437 | scope.object.zoom / 438 | element.clientHeight, 439 | scope.object.matrix 440 | ); 441 | } else { 442 | // camera neither orthographic nor perspective 443 | console.warn( 444 | "WARNING: OrbitControls.js encountered an unknown camera type - pan disabled." 445 | ); 446 | scope.enablePan = false; 447 | } 448 | }; 449 | })(); 450 | 451 | function dollyOut(dollyScale) { 452 | if (scope.object.isPerspectiveCamera) { 453 | scale /= dollyScale; 454 | } else if (scope.object.isOrthographicCamera) { 455 | scope.object.zoom = Math.max( 456 | scope.minZoom, 457 | Math.min(scope.maxZoom, scope.object.zoom * dollyScale) 458 | ); 459 | scope.object.updateProjectionMatrix(); 460 | zoomChanged = true; 461 | } else { 462 | console.warn( 463 | "WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled." 464 | ); 465 | scope.enableZoom = false; 466 | } 467 | } 468 | 469 | function dollyIn(dollyScale) { 470 | if (scope.object.isPerspectiveCamera) { 471 | scale *= dollyScale; 472 | } else if (scope.object.isOrthographicCamera) { 473 | scope.object.zoom = Math.max( 474 | scope.minZoom, 475 | Math.min(scope.maxZoom, scope.object.zoom / dollyScale) 476 | ); 477 | scope.object.updateProjectionMatrix(); 478 | zoomChanged = true; 479 | } else { 480 | console.warn( 481 | "WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled." 482 | ); 483 | scope.enableZoom = false; 484 | } 485 | } 486 | 487 | // 488 | // event callbacks - update the object state 489 | // 490 | 491 | function handleMouseDownRotate(event) { 492 | rotateStart.set(event.clientX, event.clientY); 493 | } 494 | 495 | function handleMouseDownDolly(event) { 496 | dollyStart.set(event.clientX, event.clientY); 497 | } 498 | 499 | function handleMouseDownPan(event) { 500 | panStart.set(event.clientX, event.clientY); 501 | } 502 | 503 | function handleMouseMoveRotate(event) { 504 | rotateEnd.set(event.clientX, event.clientY); 505 | 506 | rotateDelta 507 | .subVectors(rotateEnd, rotateStart) 508 | .multiplyScalar(scope.rotateSpeed); 509 | 510 | const element = scope.domElement; 511 | 512 | rotateLeft((2 * Math.PI * rotateDelta.x) / element.clientHeight); // yes, height 513 | 514 | rotateUp((2 * Math.PI * rotateDelta.y) / element.clientHeight); 515 | 516 | rotateStart.copy(rotateEnd); 517 | 518 | scope.update(); 519 | } 520 | 521 | function handleMouseMoveDolly(event) { 522 | dollyEnd.set(event.clientX, event.clientY); 523 | 524 | dollyDelta.subVectors(dollyEnd, dollyStart); 525 | 526 | if (dollyDelta.y > 0) { 527 | dollyOut(getZoomScale()); 528 | } else if (dollyDelta.y < 0) { 529 | dollyIn(getZoomScale()); 530 | } 531 | 532 | dollyStart.copy(dollyEnd); 533 | 534 | scope.update(); 535 | } 536 | 537 | function handleMouseMovePan(event) { 538 | panEnd.set(event.clientX, event.clientY); 539 | 540 | panDelta 541 | .subVectors(panEnd, panStart) 542 | .multiplyScalar(scope.panSpeed); 543 | 544 | pan(panDelta.x, panDelta.y); 545 | 546 | panStart.copy(panEnd); 547 | 548 | scope.update(); 549 | } 550 | 551 | function handleMouseWheel(event) { 552 | if (event.deltaY < 0) { 553 | dollyIn(getZoomScale()); 554 | } else if (event.deltaY > 0) { 555 | dollyOut(getZoomScale()); 556 | } 557 | 558 | scope.update(); 559 | } 560 | 561 | function handleKeyDown(event) { 562 | let needsUpdate = false; 563 | 564 | switch (event.code) { 565 | case scope.keys.UP: 566 | if (event.ctrlKey || event.metaKey) { 567 | rotateUp( 568 | (2 * Math.PI * scope.rotateSpeed) / 569 | scope.domElement.clientHeight 570 | ); 571 | } else { 572 | pan(0, scope.keyPanSpeed); 573 | } 574 | 575 | needsUpdate = true; 576 | break; 577 | 578 | case scope.keys.BOTTOM: 579 | if (event.ctrlKey || event.metaKey) { 580 | rotateUp( 581 | (-2 * Math.PI * scope.rotateSpeed) / 582 | scope.domElement.clientHeight 583 | ); 584 | } else { 585 | pan(0, -scope.keyPanSpeed); 586 | } 587 | 588 | needsUpdate = true; 589 | break; 590 | 591 | case scope.keys.LEFT: 592 | if (event.ctrlKey || event.metaKey) { 593 | rotateLeft( 594 | (2 * Math.PI * scope.rotateSpeed) / 595 | scope.domElement.clientHeight 596 | ); 597 | } else { 598 | pan(scope.keyPanSpeed, 0); 599 | } 600 | 601 | needsUpdate = true; 602 | break; 603 | 604 | case scope.keys.RIGHT: 605 | if (event.ctrlKey || event.metaKey) { 606 | rotateLeft( 607 | (-2 * Math.PI * scope.rotateSpeed) / 608 | scope.domElement.clientHeight 609 | ); 610 | } else { 611 | pan(-scope.keyPanSpeed, 0); 612 | } 613 | 614 | needsUpdate = true; 615 | break; 616 | } 617 | 618 | if (needsUpdate) { 619 | // prevent the browser from scrolling on cursor keys 620 | event.preventDefault(); 621 | 622 | scope.update(); 623 | } 624 | } 625 | 626 | function handleTouchStartRotate() { 627 | if (pointers.length === 1) { 628 | rotateStart.set(pointers[0].pageX, pointers[0].pageY); 629 | } else { 630 | const x = 0.5 * (pointers[0].pageX + pointers[1].pageX); 631 | const y = 0.5 * (pointers[0].pageY + pointers[1].pageY); 632 | 633 | rotateStart.set(x, y); 634 | } 635 | } 636 | 637 | function handleTouchStartPan() { 638 | if (pointers.length === 1) { 639 | panStart.set(pointers[0].pageX, pointers[0].pageY); 640 | } else { 641 | const x = 0.5 * (pointers[0].pageX + pointers[1].pageX); 642 | const y = 0.5 * (pointers[0].pageY + pointers[1].pageY); 643 | 644 | panStart.set(x, y); 645 | } 646 | } 647 | 648 | function handleTouchStartDolly() { 649 | const dx = pointers[0].pageX - pointers[1].pageX; 650 | const dy = pointers[0].pageY - pointers[1].pageY; 651 | 652 | const distance = Math.sqrt(dx * dx + dy * dy); 653 | 654 | dollyStart.set(0, distance); 655 | } 656 | 657 | function handleTouchStartDollyPan() { 658 | if (scope.enableZoom) handleTouchStartDolly(); 659 | 660 | if (scope.enablePan) handleTouchStartPan(); 661 | } 662 | 663 | function handleTouchStartDollyRotate() { 664 | if (scope.enableZoom) handleTouchStartDolly(); 665 | 666 | if (scope.enableRotate) handleTouchStartRotate(); 667 | } 668 | 669 | function handleTouchMoveRotate(event) { 670 | if (pointers.length == 1) { 671 | rotateEnd.set(event.pageX, event.pageY); 672 | } else { 673 | const position = getSecondPointerPosition(event); 674 | 675 | const x = 0.5 * (event.pageX + position.x); 676 | const y = 0.5 * (event.pageY + position.y); 677 | 678 | rotateEnd.set(x, y); 679 | } 680 | 681 | rotateDelta 682 | .subVectors(rotateEnd, rotateStart) 683 | .multiplyScalar(scope.rotateSpeed); 684 | 685 | const element = scope.domElement; 686 | 687 | rotateLeft((2 * Math.PI * rotateDelta.x) / element.clientHeight); // yes, height 688 | 689 | rotateUp((2 * Math.PI * rotateDelta.y) / element.clientHeight); 690 | 691 | rotateStart.copy(rotateEnd); 692 | } 693 | 694 | function handleTouchMovePan(event) { 695 | if (pointers.length === 1) { 696 | panEnd.set(event.pageX, event.pageY); 697 | } else { 698 | const position = getSecondPointerPosition(event); 699 | 700 | const x = 0.5 * (event.pageX + position.x); 701 | const y = 0.5 * (event.pageY + position.y); 702 | 703 | panEnd.set(x, y); 704 | } 705 | 706 | panDelta 707 | .subVectors(panEnd, panStart) 708 | .multiplyScalar(scope.panSpeed); 709 | 710 | pan(panDelta.x, panDelta.y); 711 | 712 | panStart.copy(panEnd); 713 | } 714 | 715 | function handleTouchMoveDolly(event) { 716 | const position = getSecondPointerPosition(event); 717 | 718 | const dx = event.pageX - position.x; 719 | const dy = event.pageY - position.y; 720 | 721 | const distance = Math.sqrt(dx * dx + dy * dy); 722 | 723 | dollyEnd.set(0, distance); 724 | 725 | dollyDelta.set( 726 | 0, 727 | Math.pow(dollyEnd.y / dollyStart.y, scope.zoomSpeed) 728 | ); 729 | 730 | dollyOut(dollyDelta.y); 731 | 732 | dollyStart.copy(dollyEnd); 733 | } 734 | 735 | function handleTouchMoveDollyPan(event) { 736 | if (scope.enableZoom) handleTouchMoveDolly(event); 737 | 738 | if (scope.enablePan) handleTouchMovePan(event); 739 | } 740 | 741 | function handleTouchMoveDollyRotate(event) { 742 | if (scope.enableZoom) handleTouchMoveDolly(event); 743 | 744 | if (scope.enableRotate) handleTouchMoveRotate(event); 745 | } 746 | 747 | // 748 | // event handlers - FSM: listen for events and reset state 749 | // 750 | 751 | function onPointerDown(event) { 752 | if (scope.enabled === false) return; 753 | 754 | if (pointers.length === 0) { 755 | scope.domElement.setPointerCapture(event.pointerId); 756 | 757 | scope.domElement.addEventListener("pointermove", onPointerMove); 758 | scope.domElement.addEventListener("pointerup", onPointerUp); 759 | } 760 | 761 | // 762 | 763 | addPointer(event); 764 | 765 | if (event.pointerType === "touch") { 766 | onTouchStart(event); 767 | } else { 768 | onMouseDown(event); 769 | } 770 | } 771 | 772 | function onPointerMove(event) { 773 | if (scope.enabled === false) return; 774 | 775 | if (event.pointerType === "touch") { 776 | onTouchMove(event); 777 | } else { 778 | onMouseMove(event); 779 | } 780 | } 781 | 782 | function onPointerUp(event) { 783 | removePointer(event); 784 | 785 | if (pointers.length === 0) { 786 | scope.domElement.releasePointerCapture(event.pointerId); 787 | 788 | scope.domElement.removeEventListener( 789 | "pointermove", 790 | onPointerMove 791 | ); 792 | scope.domElement.removeEventListener("pointerup", onPointerUp); 793 | } 794 | 795 | scope.dispatchEvent(_endEvent); 796 | 797 | state = STATE.NONE; 798 | } 799 | 800 | function onPointerCancel(event) { 801 | removePointer(event); 802 | } 803 | 804 | function onMouseDown(event) { 805 | let mouseAction; 806 | 807 | switch (event.button) { 808 | case 0: 809 | mouseAction = scope.mouseButtons.LEFT; 810 | break; 811 | 812 | case 1: 813 | mouseAction = scope.mouseButtons.MIDDLE; 814 | break; 815 | 816 | case 2: 817 | mouseAction = scope.mouseButtons.RIGHT; 818 | break; 819 | 820 | default: 821 | mouseAction = -1; 822 | } 823 | 824 | switch (mouseAction) { 825 | case MOUSE.DOLLY: 826 | if (scope.enableZoom === false) return; 827 | 828 | handleMouseDownDolly(event); 829 | 830 | state = STATE.DOLLY; 831 | 832 | break; 833 | 834 | case MOUSE.ROTATE: 835 | if (event.ctrlKey || event.metaKey) { 836 | if (scope.enablePan === false) return; 837 | 838 | handleMouseDownPan(event); 839 | 840 | state = STATE.PAN; 841 | } else { 842 | if (scope.enableRotate === false) return; 843 | 844 | handleMouseDownRotate(event); 845 | 846 | state = STATE.ROTATE; 847 | } 848 | 849 | break; 850 | 851 | case MOUSE.PAN: 852 | if (event.ctrlKey || event.metaKey) { 853 | if (scope.enableRotate === false) return; 854 | 855 | handleMouseDownRotate(event); 856 | 857 | state = STATE.ROTATE; 858 | } else { 859 | if (scope.enablePan === false) return; 860 | 861 | handleMouseDownPan(event); 862 | 863 | state = STATE.PAN; 864 | } 865 | 866 | break; 867 | 868 | default: 869 | state = STATE.NONE; 870 | } 871 | 872 | if (state !== STATE.NONE) { 873 | scope.dispatchEvent(_startEvent); 874 | } 875 | } 876 | 877 | function onMouseMove(event) { 878 | switch (state) { 879 | case STATE.ROTATE: 880 | if (scope.enableRotate === false) return; 881 | 882 | handleMouseMoveRotate(event); 883 | 884 | break; 885 | 886 | case STATE.DOLLY: 887 | if (scope.enableZoom === false) return; 888 | 889 | handleMouseMoveDolly(event); 890 | 891 | break; 892 | 893 | case STATE.PAN: 894 | if (scope.enablePan === false) return; 895 | 896 | handleMouseMovePan(event); 897 | 898 | break; 899 | } 900 | } 901 | 902 | function onMouseWheel(event) { 903 | if ( 904 | scope.enabled === false || 905 | scope.enableZoom === false || 906 | state !== STATE.NONE 907 | ) 908 | return; 909 | 910 | event.preventDefault(); 911 | 912 | scope.dispatchEvent(_startEvent); 913 | 914 | handleMouseWheel(event); 915 | 916 | scope.dispatchEvent(_endEvent); 917 | } 918 | 919 | function onKeyDown(event) { 920 | if (scope.enabled === false || scope.enablePan === false) return; 921 | 922 | handleKeyDown(event); 923 | } 924 | 925 | function onTouchStart(event) { 926 | trackPointer(event); 927 | 928 | switch (pointers.length) { 929 | case 1: 930 | switch (scope.touches.ONE) { 931 | case TOUCH.ROTATE: 932 | if (scope.enableRotate === false) return; 933 | 934 | handleTouchStartRotate(); 935 | 936 | state = STATE.TOUCH_ROTATE; 937 | 938 | break; 939 | 940 | case TOUCH.PAN: 941 | if (scope.enablePan === false) return; 942 | 943 | handleTouchStartPan(); 944 | 945 | state = STATE.TOUCH_PAN; 946 | 947 | break; 948 | 949 | default: 950 | state = STATE.NONE; 951 | } 952 | 953 | break; 954 | 955 | case 2: 956 | switch (scope.touches.TWO) { 957 | case TOUCH.DOLLY_PAN: 958 | if ( 959 | scope.enableZoom === false && 960 | scope.enablePan === false 961 | ) 962 | return; 963 | 964 | handleTouchStartDollyPan(); 965 | 966 | state = STATE.TOUCH_DOLLY_PAN; 967 | 968 | break; 969 | 970 | case TOUCH.DOLLY_ROTATE: 971 | if ( 972 | scope.enableZoom === false && 973 | scope.enableRotate === false 974 | ) 975 | return; 976 | 977 | handleTouchStartDollyRotate(); 978 | 979 | state = STATE.TOUCH_DOLLY_ROTATE; 980 | 981 | break; 982 | 983 | default: 984 | state = STATE.NONE; 985 | } 986 | 987 | break; 988 | 989 | default: 990 | state = STATE.NONE; 991 | } 992 | 993 | if (state !== STATE.NONE) { 994 | scope.dispatchEvent(_startEvent); 995 | } 996 | } 997 | 998 | function onTouchMove(event) { 999 | trackPointer(event); 1000 | 1001 | switch (state) { 1002 | case STATE.TOUCH_ROTATE: 1003 | if (scope.enableRotate === false) return; 1004 | 1005 | handleTouchMoveRotate(event); 1006 | 1007 | scope.update(); 1008 | 1009 | break; 1010 | 1011 | case STATE.TOUCH_PAN: 1012 | if (scope.enablePan === false) return; 1013 | 1014 | handleTouchMovePan(event); 1015 | 1016 | scope.update(); 1017 | 1018 | break; 1019 | 1020 | case STATE.TOUCH_DOLLY_PAN: 1021 | if (scope.enableZoom === false && scope.enablePan === false) 1022 | return; 1023 | 1024 | handleTouchMoveDollyPan(event); 1025 | 1026 | scope.update(); 1027 | 1028 | break; 1029 | 1030 | case STATE.TOUCH_DOLLY_ROTATE: 1031 | if ( 1032 | scope.enableZoom === false && 1033 | scope.enableRotate === false 1034 | ) 1035 | return; 1036 | 1037 | handleTouchMoveDollyRotate(event); 1038 | 1039 | scope.update(); 1040 | 1041 | break; 1042 | 1043 | default: 1044 | state = STATE.NONE; 1045 | } 1046 | } 1047 | 1048 | function onContextMenu(event) { 1049 | if (scope.enabled === false) return; 1050 | 1051 | event.preventDefault(); 1052 | } 1053 | 1054 | function addPointer(event) { 1055 | pointers.push(event); 1056 | } 1057 | 1058 | function removePointer(event) { 1059 | delete pointerPositions[event.pointerId]; 1060 | 1061 | for (let i = 0; i < pointers.length; i++) { 1062 | if (pointers[i].pointerId == event.pointerId) { 1063 | pointers.splice(i, 1); 1064 | return; 1065 | } 1066 | } 1067 | } 1068 | 1069 | function trackPointer(event) { 1070 | let position = pointerPositions[event.pointerId]; 1071 | 1072 | if (position === undefined) { 1073 | position = new Vector2(); 1074 | pointerPositions[event.pointerId] = position; 1075 | } 1076 | 1077 | position.set(event.pageX, event.pageY); 1078 | } 1079 | 1080 | function getSecondPointerPosition(event) { 1081 | const pointer = 1082 | event.pointerId === pointers[0].pointerId 1083 | ? pointers[1] 1084 | : pointers[0]; 1085 | 1086 | return pointerPositions[pointer.pointerId]; 1087 | } 1088 | 1089 | // 1090 | 1091 | scope.domElement.addEventListener("contextmenu", onContextMenu); 1092 | 1093 | scope.domElement.addEventListener("pointerdown", onPointerDown); 1094 | scope.domElement.addEventListener("pointercancel", onPointerCancel); 1095 | scope.domElement.addEventListener("wheel", onMouseWheel, { 1096 | passive: false, 1097 | }); 1098 | 1099 | // force an update at start 1100 | 1101 | this.update(); 1102 | } 1103 | } 1104 | 1105 | export { OrbitControls }; 1106 | -------------------------------------------------------------------------------- /frontend/Experience/Utils/Loaders.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | 3 | import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js"; 4 | import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js"; 5 | 6 | export default class Loaders { 7 | constructor() { 8 | this.loaders = {}; 9 | 10 | this.setLoaders(); 11 | } 12 | 13 | setLoaders() { 14 | this.loaders.cubeTextureLoader = new THREE.CubeTextureLoader(); 15 | 16 | this.loaders.gltfLoader = new GLTFLoader(); 17 | this.loaders.dracoLoader = new DRACOLoader(); 18 | this.loaders.dracoLoader.setDecoderPath("/draco/"); 19 | this.loaders.gltfLoader.setDRACOLoader(this.loaders.dracoLoader); 20 | 21 | this.loaders.textureLoader = new THREE.TextureLoader(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /frontend/Experience/Utils/Resources.js: -------------------------------------------------------------------------------- 1 | import Loaders from "./Loaders.js"; 2 | import { EventEmitter } from "events"; 3 | import * as THREE from "three"; 4 | 5 | export default class Resources extends EventEmitter { 6 | constructor(assets) { 7 | super(); 8 | 9 | this.items = {}; 10 | this.assets = assets; 11 | this.location = "westgate"; 12 | 13 | this.loaders = new Loaders().loaders; 14 | 15 | this.startLoading(); 16 | } 17 | 18 | startLoading() { 19 | this.loaded = 0; 20 | this.queue = this.assets[0][this.location].assets.length; 21 | 22 | for (const asset of this.assets[0][this.location].assets) { 23 | if (asset.type === "glbModel") { 24 | this.loaders.gltfLoader.load(asset.path, (file) => { 25 | this.singleAssetLoaded(asset, file); 26 | }); 27 | } else if (asset.type === "imageTexture") { 28 | this.loaders.textureLoader.load(asset.path, (file) => { 29 | this.singleAssetLoaded(asset, file); 30 | }); 31 | } else if (asset.type === "cubeTexture") { 32 | this.loaders.cubeTextureLoader.load(asset.path, (file) => { 33 | this.singleAssetLoaded(asset, file); 34 | }); 35 | } else if (asset.type === "videoTexture") { 36 | this.video = {}; 37 | this.videoTexture = {}; 38 | 39 | this.video[asset.name] = document.createElement("video"); 40 | this.video[asset.name].src = asset.path; 41 | this.video[asset.name].muted = true; 42 | this.video[asset.name].playsInline = true; 43 | this.video[asset.name].autoplay = true; 44 | this.video[asset.name].loop = true; 45 | this.video[asset.name].play(); 46 | 47 | this.videoTexture[asset.name] = new THREE.VideoTexture( 48 | this.video[asset.name] 49 | ); 50 | this.videoTexture[asset.name].flipY = false; 51 | this.videoTexture[asset.name].minFilter = THREE.NearestFilter; 52 | this.videoTexture[asset.name].magFilter = THREE.NearestFilter; 53 | this.videoTexture[asset.name].generateMipmaps = false; 54 | this.videoTexture[asset.name].ColorSpace = THREE.SRGBColorSpace; //changed 55 | 56 | this.singleAssetLoaded(asset, this.videoTexture[asset.name]); 57 | } 58 | } 59 | } 60 | 61 | singleAssetLoaded(asset, file) { 62 | this.items[asset.name] = file; 63 | this.loaded++; 64 | this.emit("loading", this.loaded, this.queue); 65 | 66 | if (this.loaded === this.queue) { 67 | this.emit("ready"); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /frontend/Experience/Utils/Sizes.js: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from "events"; 2 | 3 | export default class Sizes extends EventEmitter { 4 | constructor() { 5 | super(); 6 | this.handleSizes(); 7 | window.addEventListener("resize", () => { 8 | this.handleSizes(); 9 | this.emit("resize"); 10 | }); 11 | } 12 | 13 | handleSizes() { 14 | this.width = window.innerWidth; 15 | this.height = window.innerHeight; 16 | this.aspect = this.width / this.height; 17 | this.pixelRatio = Math.min(window.devicePixelRatio, 2); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /frontend/Experience/Utils/Time.js: -------------------------------------------------------------------------------- 1 | export default class Time { 2 | constructor() { 3 | this.start = Date.now(); 4 | this.current = this.start; 5 | this.elapsed = 0; 6 | this.delta = 16; 7 | } 8 | 9 | update() { 10 | const currentTime = Date.now(); 11 | this.delta = (currentTime - this.current) / 1000; 12 | this.current = currentTime; 13 | // this.elapsed = this.current - this.start; 14 | 15 | if (this.delta > 60) { 16 | this.delta = 60; 17 | } 18 | } 19 | 20 | getDelta() { 21 | return this.delta; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /frontend/Experience/Utils/assets.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | westgate: { 4 | assets: [ 5 | { 6 | name: "screen", 7 | type: "glbModel", 8 | path: "/models/screen.glb", 9 | }, 10 | { 11 | name: "glass", 12 | type: "glbModel", 13 | path: "/models/glass.glb", 14 | }, 15 | { 16 | name: "male", 17 | type: "glbModel", 18 | path: "/models/asian_male_animated.glb", 19 | }, 20 | { 21 | name: "female", 22 | type: "glbModel", 23 | path: "/models/asian_female_animated.glb", 24 | }, 25 | { 26 | name: "bars", 27 | type: "glbModel", 28 | path: "/models/bars.glb", 29 | }, 30 | { 31 | name: "brick", 32 | type: "glbModel", 33 | path: "/models/brick.glb", 34 | }, 35 | { 36 | name: "buildings", 37 | type: "glbModel", 38 | path: "/models/buildings.glb", 39 | }, 40 | { 41 | name: "easter", 42 | type: "glbModel", 43 | path: "/models/easter.glb", 44 | }, 45 | { 46 | name: "everything", 47 | type: "glbModel", 48 | path: "/models/everything.glb", 49 | }, 50 | { 51 | name: "floor", 52 | type: "glbModel", 53 | path: "/models/floor.glb", 54 | }, 55 | { 56 | name: "grass", 57 | type: "glbModel", 58 | path: "/models/grass.glb", 59 | }, 60 | { 61 | name: "other", 62 | type: "glbModel", 63 | path: "/models/other.glb", 64 | }, 65 | { 66 | name: "outside", 67 | type: "glbModel", 68 | path: "/models/outside.glb", 69 | }, 70 | { 71 | name: "panera", 72 | type: "glbModel", 73 | path: "/models/panera.glb", 74 | }, 75 | { 76 | name: "plastic", 77 | type: "glbModel", 78 | path: "/models/plastic.glb", 79 | }, 80 | { 81 | name: "tables", 82 | type: "glbModel", 83 | path: "/models/tables.glb", 84 | }, 85 | { 86 | name: "box", 87 | type: "glbModel", 88 | path: "/models/box.glb", 89 | }, 90 | { 91 | name: "thirdfloor", 92 | type: "glbModel", 93 | path: "/models/thirdfloor.glb", 94 | }, 95 | { 96 | name: "collider", 97 | type: "glbModel", 98 | path: "/models/collider.glb", 99 | }, 100 | { 101 | name: "barsTexture", 102 | type: "imageTexture", 103 | path: "textures/baked/bars.jpg", 104 | }, 105 | { 106 | name: "brickTexture", 107 | type: "imageTexture", 108 | path: "textures/baked/brick.jpg", 109 | }, 110 | { 111 | name: "buildingsTexture", 112 | type: "imageTexture", 113 | path: "textures/baked/buildings.jpg", 114 | }, 115 | { 116 | name: "easterTexture", 117 | type: "imageTexture", 118 | path: "textures/baked/easter.jpg", 119 | }, 120 | { 121 | name: "everythingTexture", 122 | type: "imageTexture", 123 | path: "textures/baked/everything.jpg", 124 | }, 125 | { 126 | name: "floorTexture", 127 | type: "imageTexture", 128 | path: "textures/baked/floor.jpg", 129 | }, 130 | { 131 | name: "grassTexture", 132 | type: "imageTexture", 133 | path: "textures/baked/grass.jpg", 134 | }, 135 | { 136 | name: "otherTexture", 137 | type: "imageTexture", 138 | path: "textures/baked/other.webp", 139 | }, 140 | { 141 | name: "outsideTexture", 142 | type: "imageTexture", 143 | path: "textures/baked/outside.jpg", 144 | }, 145 | { 146 | name: "paneraTexture", 147 | type: "imageTexture", 148 | path: "textures/baked/panera.jpg", 149 | }, 150 | { 151 | name: "plasticTexture", 152 | type: "imageTexture", 153 | path: "textures/baked/plastic.jpg", 154 | }, 155 | { 156 | name: "tablesTexture", 157 | type: "imageTexture", 158 | path: "textures/baked/tables.jpg", 159 | }, 160 | { 161 | name: "thirdfloorTexture", 162 | type: "imageTexture", 163 | path: "textures/baked/thirdfloor.jpg", 164 | }, 165 | { 166 | name: "boxTexture", 167 | type: "imageTexture", 168 | path: "textures/baked/box.jpg", 169 | }, 170 | { 171 | name: "environment", 172 | type: "cubeTexture", 173 | path: [ 174 | "textures/environment/px.png", 175 | "textures/environment/nx.png", 176 | "textures/environment/py.png", 177 | "textures/environment/ny.png", 178 | "textures/environment/pz.png", 179 | "textures/environment/nz.png", 180 | ], 181 | }, 182 | { 183 | name: "video", 184 | type: "videoTexture", 185 | path: "/videos/tour.mp4", 186 | }, 187 | ], 188 | }, 189 | }, 190 | ]; 191 | -------------------------------------------------------------------------------- /frontend/Experience/Utils/functions/elements.js: -------------------------------------------------------------------------------- 1 | export default function (domSelectors) { 2 | let elements = {}; 3 | 4 | Object.entries(domSelectors).forEach((selector) => { 5 | const key = selector[0]; 6 | const data = selector[1]; 7 | elements[key] = document.querySelector(data); 8 | }); 9 | 10 | return elements; 11 | } 12 | -------------------------------------------------------------------------------- /frontend/Experience/Utils/functions/lerp.js: -------------------------------------------------------------------------------- 1 | export default function (start, end, factor) { 2 | return (1 - factor) * start + factor * end; 3 | } 4 | -------------------------------------------------------------------------------- /frontend/Experience/World/Environment.js: -------------------------------------------------------------------------------- 1 | import Experience from "../Experience.js"; 2 | import * as THREE from "three"; 3 | 4 | export default class Environment { 5 | constructor() { 6 | this.experience = new Experience(); 7 | this.scene = this.experience.scene; 8 | this.resources = this.experience.resources; 9 | 10 | this.setEnvironment(); 11 | } 12 | 13 | setEnvironment() { 14 | this.environmentMap = {}; 15 | this.environmentMap.intensity = 0; 16 | this.environmentMap.texture = this.resources.items.environment; 17 | this.environmentMap.texture.outputColorSpace = THREE.SRGBColorSpace; 18 | 19 | this.scene.background = this.environmentMap.texture; 20 | 21 | const light = new THREE.AmbientLight(0x404040, 4); // soft white light 22 | this.scene.add(light); 23 | 24 | this.sunLight = new THREE.DirectionalLight("#ffffff", 1.5); 25 | 26 | this.sunLight.position.set(1.5, 7, -3); 27 | this.scene.add(this.sunLight); 28 | 29 | // this.scene.environment = this.environmentMap.texture; 30 | 31 | // console.log(this.scene); 32 | 33 | // this.environmentMap.updateMaterials = () => { 34 | // this.scene.children.forEach((child) => { 35 | // if (child instanceof THREE.Group) { 36 | // console.log(child.children[0]); 37 | // if ( 38 | // child.children[0] instanceof THREE.Mesh && 39 | // child.children[0].material instanceof 40 | // THREE.MeshPhysicalMaterial 41 | // ) { 42 | // child.children[0].material.envMap = 43 | // this.environmentMap.texture; 44 | // child.children[0].material.envMapIntensity = 45 | // this.environmentMap.intensity; 46 | // child.children[0].material.needsUpdate = true; 47 | // } 48 | // } 49 | // }); 50 | // }; 51 | // this.environmentMap.updateMaterials(); 52 | } 53 | 54 | update() {} 55 | } 56 | -------------------------------------------------------------------------------- /frontend/Experience/World/Player/Avatar.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import * as SkeletonUtils from "three/addons/utils/SkeletonUtils.js"; 3 | import Nametag from "./Nametag.js"; 4 | 5 | export default class Avatar { 6 | constructor(avatar, scene, name = "Anonymous", id) { 7 | this.scene = scene; 8 | this.name = new Nametag(); 9 | this.nametag = this.name.createNametag(16, 150, name); 10 | this.avatar = SkeletonUtils.clone(avatar.scene); 11 | this.avatar.userData.id = id; 12 | 13 | this.avatar.animations = avatar.animations.map((clip) => { 14 | return clip.clone(); 15 | }); 16 | 17 | this.setAvatar(); 18 | } 19 | 20 | setAvatar() { 21 | this.speedAdjustment = 1; 22 | this.avatar.scale.set(0.99, 0.99, 0.99); 23 | this.setAnimation(); 24 | this.scene.add(this.avatar); 25 | 26 | if (this.avatar.userData.id) { 27 | this.scene.add(this.nametag); 28 | } 29 | } 30 | 31 | setAnimation() { 32 | this.animation = {}; 33 | 34 | this.animation.mixer = new THREE.AnimationMixer(this.avatar); 35 | 36 | this.animation.actions = {}; 37 | 38 | this.animation.actions.dancing = this.animation.mixer.clipAction( 39 | this.avatar.animations[0] 40 | ); 41 | 42 | this.animation.actions.idle = this.animation.mixer.clipAction( 43 | this.avatar.animations[1] 44 | ); 45 | this.animation.actions.jumping = this.animation.mixer.clipAction( 46 | this.avatar.animations[2] 47 | ); 48 | 49 | this.animation.actions.running = this.animation.mixer.clipAction( 50 | this.avatar.animations[3] 51 | ); 52 | this.animation.actions.walking = this.animation.mixer.clipAction( 53 | this.avatar.animations[4] 54 | ); 55 | this.animation.actions.waving = this.animation.mixer.clipAction( 56 | this.avatar.animations[5] 57 | ); 58 | 59 | this.animation.actions.current = this.animation.actions.idle; 60 | this.animation.actions.current.play(); 61 | 62 | this.animation.play = (name) => { 63 | const newAction = this.animation.actions[name]; 64 | const oldAction = this.animation.actions.current; 65 | 66 | if (oldAction === newAction) { 67 | return; 68 | } 69 | 70 | if (this.animation.actions.current === "jumping") { 71 | this.speedAdjustment = 1.5; 72 | } else { 73 | this.speedAdjustment = 1.0; 74 | } 75 | 76 | newAction.reset(); 77 | newAction.play(); 78 | newAction.crossFadeFrom(oldAction, 0.2); 79 | 80 | this.animation.actions.current = newAction; 81 | }; 82 | 83 | this.animation.update = (time) => { 84 | this.animation.mixer.update(time * this.speedAdjustment); 85 | }; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /frontend/Experience/World/Player/Nametag.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import Experience from "../../Experience.js"; 3 | 4 | export default class Nametag { 5 | constructor() { 6 | this.experience = new Experience(); 7 | this.resources = this.experience.resources; 8 | this.scene = this.experience.scene; 9 | this.nametag = ""; 10 | } 11 | 12 | createNametag(size = 16, baseWidth = 150, name = "John Doe") { 13 | const borderSize = 2; 14 | const fontSize = 12; 15 | const ctx = document.createElement("canvas").getContext("2d"); 16 | const font = `200 ${size}px Arial`; 17 | ctx.font = font; 18 | // measure how long the name will be 19 | const textWidth = ctx.measureText(name).width; 20 | 21 | const doubleBorderSize = borderSize * 2; 22 | const width = baseWidth + doubleBorderSize; 23 | const height = size + doubleBorderSize; 24 | ctx.canvas.width = width; 25 | ctx.canvas.height = height; 26 | 27 | // need to set font again after resizing canvas 28 | ctx.font = font; 29 | ctx.textBaseline = "middle"; 30 | ctx.textAlign = "center"; 31 | 32 | ctx.fillStyle = "rgba(0, 0, 0, 0.5)"; 33 | ctx.fillRect(0, 0, width, height); 34 | 35 | // scale to fit but don't stretch 36 | const scaleFactor = Math.min(1, baseWidth / textWidth); 37 | ctx.translate(width / 2, height / 2); 38 | ctx.scale(scaleFactor, 1); 39 | ctx.fillStyle = "white"; 40 | ctx.fillText(name, 0, 0); 41 | 42 | const canvasTexture = new THREE.CanvasTexture(ctx.canvas); 43 | 44 | canvasTexture.minFilter = THREE.LinearFilter; 45 | canvasTexture.wrapS = THREE.ClampToEdgeWrapping; 46 | canvasTexture.wrapT = THREE.ClampToEdgeWrapping; 47 | 48 | const nameMaterial = new THREE.SpriteMaterial({ 49 | map: canvasTexture, 50 | transparent: true, 51 | }); 52 | 53 | const labelBaseScale = 0.01; 54 | const label = new THREE.Sprite(nameMaterial); 55 | 56 | label.position.y = 5; 57 | 58 | label.scale.x = ctx.canvas.width * labelBaseScale; 59 | label.scale.y = ctx.canvas.height * labelBaseScale; 60 | 61 | return label; 62 | 63 | // this.scene.add(label); 64 | 65 | // return ctx.canvas; 66 | 67 | // return this.nametag; 68 | } 69 | 70 | update() {} 71 | } 72 | -------------------------------------------------------------------------------- /frontend/Experience/World/Player/Player.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import Experience from "../../Experience.js"; 3 | import { Capsule } from "three/examples/jsm/math/Capsule"; 4 | 5 | import nipplejs from "nipplejs"; 6 | import elements from "../../Utils/functions/elements.js"; 7 | 8 | import Avatar from "./Avatar.js"; 9 | 10 | export default class Player { 11 | constructor() { 12 | this.experience = new Experience(); 13 | this.time = this.experience.time; 14 | this.scene = this.experience.scene; 15 | this.camera = this.experience.camera; 16 | this.octree = this.experience.world.octree; 17 | this.resources = this.experience.resources; 18 | this.socket = this.experience.socket; 19 | 20 | this.domElements = elements({ 21 | joystickArea: ".joystick-area", 22 | controlOverlay: ".control-overlay", 23 | messageInput: "#chat-message-input", 24 | switchViewButton: ".switch-camera-view", 25 | }); 26 | 27 | this.initPlayer(); 28 | this.initControls(); 29 | this.setPlayerSocket(); 30 | this.setJoyStick(); 31 | this.addEventListeners(); 32 | } 33 | 34 | initPlayer() { 35 | this.player = {}; 36 | 37 | this.player.body = this.camera.perspectiveCamera; 38 | this.player.animation = "idle"; 39 | 40 | this.jumpOnce = false; 41 | this.player.onFloor = false; 42 | this.player.gravity = 60; 43 | 44 | this.player.spawn = { 45 | position: new THREE.Vector3(), 46 | rotation: new THREE.Euler(), 47 | velocity: new THREE.Vector3(), 48 | }; 49 | 50 | this.player.raycaster = new THREE.Raycaster(); 51 | this.player.raycaster.far = 5; 52 | 53 | this.player.height = 1.2; 54 | this.player.speedMultiplier = 0.35; 55 | this.player.position = new THREE.Vector3(); 56 | this.player.quaternion = new THREE.Euler(); 57 | this.player.directionOffset = 0; 58 | this.targetRotation = new THREE.Quaternion(); 59 | 60 | this.upVector = new THREE.Vector3(0, 1, 0); 61 | this.player.velocity = new THREE.Vector3(); 62 | this.player.direction = new THREE.Vector3(); 63 | 64 | this.player.collider = new Capsule( 65 | new THREE.Vector3(), 66 | new THREE.Vector3(), 67 | 0.35 68 | ); 69 | 70 | this.otherPlayers = {}; 71 | 72 | this.socket.emit("setID"); 73 | this.socket.emit("initPlayer", this.player); 74 | } 75 | 76 | initControls() { 77 | this.actions = {}; 78 | 79 | this.coords = { 80 | previousX: 0, 81 | previousY: 0, 82 | currentX: 0, 83 | currentY: 0, 84 | }; 85 | 86 | this.joystickVector = new THREE.Vector3(); 87 | } 88 | 89 | setJoyStick() { 90 | this.options = { 91 | zone: this.domElements.joystickArea, 92 | mode: "dynamic", 93 | }; 94 | this.joystick = nipplejs.create(this.options); 95 | 96 | this.joystick.on("move", (e, data) => { 97 | this.actions.movingJoyStick = true; 98 | this.joystickVector.z = -data.vector.y; 99 | this.joystickVector.x = data.vector.x; 100 | }); 101 | 102 | this.joystick.on("end", () => { 103 | this.actions.movingJoyStick = false; 104 | }); 105 | } 106 | 107 | setPlayerSocket() { 108 | this.socket.on("setID", (setID, name) => {}); 109 | 110 | this.socket.on("setAvatarSkin", (avatarSkin, id) => { 111 | if (!this.avatar && id === this.socket.id) { 112 | this.player.avatarSkin = avatarSkin; 113 | this.avatar = new Avatar( 114 | this.resources.items[avatarSkin], 115 | this.scene 116 | ); 117 | this.updatePlayerSocket(); 118 | } 119 | }); 120 | 121 | this.socket.on("playerData", (playerData) => { 122 | for (let player of playerData) { 123 | if (player.id !== this.socket.id) { 124 | this.scene.traverse((child) => { 125 | if (child.userData.id === player.id) { 126 | return; 127 | } else { 128 | if (!this.otherPlayers.hasOwnProperty(player.id)) { 129 | if ( 130 | player.name === "" || 131 | player.avatarSkin === "" 132 | ) { 133 | return; 134 | } 135 | 136 | const name = player.name.substring(0, 25); 137 | 138 | const newAvatar = new Avatar( 139 | this.resources.items[player.avatarSkin], 140 | this.scene, 141 | name, 142 | player.id 143 | ); 144 | 145 | player.model = newAvatar; 146 | this.otherPlayers[player.id] = player; 147 | } 148 | } 149 | }); 150 | if (this.otherPlayers[player.id]) { 151 | this.otherPlayers[player.id].position = { 152 | position_x: player.position_x, 153 | position_y: player.position_y, 154 | position_z: player.position_z, 155 | }; 156 | this.otherPlayers[player.id].quaternion = { 157 | quaternion_x: player.quaternion_x, 158 | quaternion_y: player.quaternion_y, 159 | quaternion_z: player.quaternion_z, 160 | quaternion_w: player.quaternion_w, 161 | }; 162 | this.otherPlayers[player.id].animation = { 163 | animation: player.animation, 164 | }; 165 | } 166 | } 167 | } 168 | }); 169 | 170 | this.socket.on("removePlayer", (id) => { 171 | this.disconnectedPlayerId = id; 172 | 173 | this.otherPlayers[id].model.nametag.material.dispose(); 174 | this.otherPlayers[id].model.nametag.geometry.dispose(); 175 | this.scene.remove(this.otherPlayers[id].model.nametag); 176 | 177 | this.otherPlayers[id].model.avatar.traverse((child) => { 178 | if (child instanceof THREE.Mesh) { 179 | child.material.dispose(); 180 | child.geometry.dispose(); 181 | } 182 | 183 | if (child.material) { 184 | child.material.dispose(); 185 | } 186 | 187 | if (child.geometry) { 188 | child.geometry.dispose(); 189 | } 190 | }); 191 | 192 | this.scene.remove(this.otherPlayers[id].model.avatar); 193 | 194 | delete this.otherPlayers[id].nametag; 195 | delete this.otherPlayers[id].model; 196 | delete this.otherPlayers[id]; 197 | }); 198 | } 199 | 200 | updatePlayerSocket() { 201 | setInterval(() => { 202 | if (this.avatar) { 203 | this.socket.emit("updatePlayer", { 204 | position: this.avatar.avatar.position, 205 | quaternion: this.avatar.avatar.quaternion, 206 | animation: this.player.animation, 207 | avatarSkin: this.player.avatarSkin, 208 | }); 209 | } 210 | }, 20); 211 | } 212 | 213 | onKeyDown = (e) => { 214 | if (document.activeElement === this.domElements.messageInput) return; 215 | 216 | if (e.code === "KeyW" || e.code === "ArrowUp") { 217 | this.actions.forward = true; 218 | } 219 | if (e.code === "KeyS" || e.code === "ArrowDown") { 220 | this.actions.backward = true; 221 | } 222 | if (e.code === "KeyA" || e.code === "ArrowLeft") { 223 | this.actions.left = true; 224 | } 225 | if (e.code === "KeyD" || e.code === "ArrowRight") { 226 | this.actions.right = true; 227 | } 228 | if (!this.actions.run && !this.actions.jump) { 229 | this.player.animation = "walking"; 230 | } 231 | 232 | if (e.code === "KeyO") { 233 | this.player.animation = "dancing"; 234 | } 235 | 236 | if (e.code === "ShiftLeft") { 237 | this.actions.run = true; 238 | this.player.animation = "running"; 239 | } 240 | 241 | if (e.code === "Space" && !this.actions.jump && this.player.onFloor) { 242 | this.actions.jump = true; 243 | this.player.animation = "jumping"; 244 | this.jumpOnce = true; 245 | } 246 | }; 247 | 248 | onKeyUp = (e) => { 249 | if (e.code === "KeyW" || e.code === "ArrowUp") { 250 | this.actions.forward = false; 251 | } 252 | if (e.code === "KeyS" || e.code === "ArrowDown") { 253 | this.actions.backward = false; 254 | } 255 | if (e.code === "KeyA" || e.code === "ArrowLeft") { 256 | this.actions.left = false; 257 | } 258 | if (e.code === "KeyD" || e.code === "ArrowRight") { 259 | this.actions.right = false; 260 | } 261 | 262 | if (e.code === "ShiftLeft") { 263 | this.actions.run = false; 264 | } 265 | 266 | if (this.player.onFloor) { 267 | if (this.actions.run) { 268 | this.player.animation = "running"; 269 | } else if ( 270 | this.actions.forward || 271 | this.actions.backward || 272 | this.actions.left || 273 | this.actions.right 274 | ) { 275 | this.player.animation = "walking"; 276 | } else { 277 | this.player.animation = "idle"; 278 | } 279 | } 280 | 281 | if (e.code === "Space") { 282 | this.actions.jump = false; 283 | } 284 | }; 285 | 286 | playerCollisions() { 287 | const result = this.octree.capsuleIntersect(this.player.collider); 288 | this.player.onFloor = false; 289 | 290 | if (result) { 291 | this.player.onFloor = result.normal.y > 0; 292 | 293 | this.player.collider.translate( 294 | result.normal.multiplyScalar(result.depth) 295 | ); 296 | } 297 | } 298 | 299 | getForwardVector() { 300 | this.camera.perspectiveCamera.getWorldDirection(this.player.direction); 301 | this.player.direction.y = 0; 302 | this.player.direction.normalize(); 303 | 304 | return this.player.direction; 305 | } 306 | 307 | getSideVector() { 308 | this.camera.perspectiveCamera.getWorldDirection(this.player.direction); 309 | this.player.direction.y = 0; 310 | this.player.direction.normalize(); 311 | this.player.direction.cross(this.camera.perspectiveCamera.up); 312 | 313 | return this.player.direction; 314 | } 315 | 316 | getJoyStickDirectionalVector() { 317 | let returnVector = new THREE.Vector3(); 318 | returnVector.copy(this.joystickVector); 319 | 320 | returnVector.applyQuaternion(this.camera.perspectiveCamera.quaternion); 321 | returnVector.y = 0; 322 | returnVector.multiplyScalar(1.5); 323 | 324 | return returnVector; 325 | } 326 | 327 | addEventListeners() { 328 | document.addEventListener("keydown", this.onKeyDown); 329 | document.addEventListener("keyup", this.onKeyUp); 330 | } 331 | 332 | resize() {} 333 | 334 | spawnPlayerOutOfBounds() { 335 | const spawnPos = new THREE.Vector3(-22.4437, 8 + 5, -15.0529); 336 | this.player.velocity = this.player.spawn.velocity; 337 | 338 | this.player.collider.start.copy(spawnPos); 339 | this.player.collider.end.copy(spawnPos); 340 | 341 | this.player.collider.end.y += this.player.height; 342 | } 343 | 344 | updateColliderMovement() { 345 | const speed = 346 | (this.player.onFloor ? 1.75 : 0.1) * 347 | this.player.gravity * 348 | this.player.speedMultiplier; 349 | 350 | let speedDelta = this.time.delta * speed; 351 | 352 | if (this.actions.movingJoyStick) { 353 | this.player.velocity.add(this.getJoyStickDirectionalVector()); 354 | } 355 | 356 | if (this.actions.run) { 357 | speedDelta *= 2.5; 358 | } 359 | 360 | if (this.actions.forward) { 361 | this.player.velocity.add( 362 | this.getForwardVector().multiplyScalar(speedDelta) 363 | ); 364 | } 365 | if (this.actions.backward) { 366 | this.player.velocity.add( 367 | this.getForwardVector().multiplyScalar(-speedDelta) 368 | ); 369 | } 370 | if (this.actions.left) { 371 | this.player.velocity.add( 372 | this.getSideVector().multiplyScalar(-speedDelta) 373 | ); 374 | } 375 | if (this.actions.right) { 376 | this.player.velocity.add( 377 | this.getSideVector().multiplyScalar(speedDelta) 378 | ); 379 | } 380 | 381 | if (this.player.onFloor) { 382 | if (this.actions.jump && this.jumpOnce) { 383 | this.player.velocity.y = 12; 384 | } 385 | this.jumpOnce = false; 386 | } 387 | 388 | let damping = Math.exp(-15 * this.time.delta) - 1; 389 | 390 | if (!this.player.onFloor) { 391 | if (this.player.animation === "jumping") { 392 | this.player.velocity.y -= 393 | this.player.gravity * 0.7 * this.time.delta; 394 | } else { 395 | this.player.velocity.y -= this.player.gravity * this.time.delta; 396 | } 397 | damping *= 0.1; 398 | } 399 | 400 | this.player.velocity.addScaledVector(this.player.velocity, damping); 401 | 402 | const deltaPosition = this.player.velocity 403 | .clone() 404 | .multiplyScalar(this.time.delta); 405 | 406 | this.player.collider.translate(deltaPosition); 407 | this.playerCollisions(); 408 | 409 | this.player.body.position.sub(this.camera.controls.target); 410 | this.camera.controls.target.copy(this.player.collider.end); 411 | this.player.body.position.add(this.player.collider.end); 412 | 413 | this.player.body.updateMatrixWorld(); 414 | 415 | if (this.player.body.position.y < -20) { 416 | this.spawnPlayerOutOfBounds(); 417 | } 418 | } 419 | 420 | setInteractionObjects(interactionObjects) { 421 | this.player.interactionObjects = interactionObjects; 422 | } 423 | 424 | getgetCameraLookAtDirectionalVector() { 425 | const direction = new THREE.Vector3(0, 0, -1); 426 | return direction.applyQuaternion( 427 | this.camera.perspectiveCamera.quaternion 428 | ); 429 | } 430 | 431 | updateRaycaster() { 432 | this.player.raycaster.ray.origin.copy( 433 | this.camera.perspectiveCamera.position 434 | ); 435 | 436 | this.player.raycaster.ray.direction.copy( 437 | this.getgetCameraLookAtDirectionalVector() 438 | ); 439 | 440 | const intersects = this.player.raycaster.intersectObjects( 441 | this.player.interactionObjects.children 442 | ); 443 | 444 | if (intersects.length === 0) { 445 | this.currentIntersectObject = ""; 446 | } else { 447 | this.currentIntersectObject = intersects[0].object.name; 448 | } 449 | 450 | if (this.currentIntersectObject !== this.previousIntersectObject) { 451 | this.previousIntersectObject = this.currentIntersectObject; 452 | } 453 | } 454 | 455 | updateAvatarPosition() { 456 | this.avatar.avatar.position.copy(this.player.collider.end); 457 | this.avatar.avatar.position.y -= 1.56; 458 | 459 | this.avatar.animation.update(this.time.delta); 460 | } 461 | 462 | updateOtherPlayers() { 463 | for (let player in this.otherPlayers) { 464 | this.otherPlayers[player].model.avatar.position.set( 465 | this.otherPlayers[player].position.position_x, 466 | this.otherPlayers[player].position.position_y, 467 | this.otherPlayers[player].position.position_z 468 | ); 469 | 470 | this.otherPlayers[player].model.animation.play( 471 | this.otherPlayers[player].animation.animation 472 | ); 473 | 474 | this.otherPlayers[player].model.animation.update(this.time.delta); 475 | 476 | this.otherPlayers[player].model.avatar.quaternion.set( 477 | this.otherPlayers[player].quaternion.quaternion_x, 478 | this.otherPlayers[player].quaternion.quaternion_y, 479 | this.otherPlayers[player].quaternion.quaternion_z, 480 | this.otherPlayers[player].quaternion.quaternion_w 481 | ); 482 | 483 | this.otherPlayers[player].model.nametag.position.set( 484 | this.otherPlayers[player].position.position_x, 485 | this.otherPlayers[player].position.position_y + 2.1, 486 | this.otherPlayers[player].position.position_z 487 | ); 488 | } 489 | } 490 | 491 | updateAvatarRotation() { 492 | if (this.actions.forward) { 493 | this.player.directionOffset = Math.PI; 494 | } 495 | if (this.actions.backward) { 496 | this.player.directionOffset = 0; 497 | } 498 | 499 | if (this.actions.left) { 500 | this.player.directionOffset = -Math.PI / 2; 501 | } 502 | 503 | if (this.actions.forward && this.actions.left) { 504 | this.player.directionOffset = Math.PI + Math.PI / 4; 505 | } 506 | if (this.actions.backward && this.actions.left) { 507 | this.player.directionOffset = -Math.PI / 4; 508 | } 509 | 510 | if (this.actions.right) { 511 | this.player.directionOffset = Math.PI / 2; 512 | } 513 | 514 | if (this.actions.forward && this.actions.right) { 515 | this.player.directionOffset = Math.PI - Math.PI / 4; 516 | } 517 | if (this.actions.backward && this.actions.right) { 518 | this.player.directionOffset = Math.PI / 4; 519 | } 520 | 521 | if (this.actions.forward && this.actions.left && this.actions.right) { 522 | this.player.directionOffset = Math.PI; 523 | } 524 | if (this.actions.backward && this.actions.left && this.actions.right) { 525 | this.player.directionOffset = 0; 526 | } 527 | 528 | if ( 529 | this.actions.right && 530 | this.actions.backward && 531 | this.actions.forward 532 | ) { 533 | this.player.directionOffset = Math.PI / 2; 534 | } 535 | 536 | if ( 537 | this.actions.left && 538 | this.actions.backward && 539 | this.actions.forward 540 | ) { 541 | this.player.directionOffset = -Math.PI / 2; 542 | } 543 | } 544 | 545 | updateAvatarAnimation() { 546 | if (this.player.animation !== this.avatar.animation) { 547 | if ( 548 | this.actions.left && 549 | this.actions.right && 550 | !this.actions.forward && 551 | !this.actions.backward 552 | ) { 553 | this.player.animation = "idle"; 554 | } 555 | 556 | if ( 557 | !this.actions.left && 558 | !this.actions.right && 559 | this.actions.forward && 560 | this.actions.backward 561 | ) { 562 | this.player.animation = "idle"; 563 | } 564 | 565 | if ( 566 | this.actions.left && 567 | this.actions.right && 568 | this.actions.forward && 569 | this.actions.backward 570 | ) { 571 | this.player.animation = "idle"; 572 | } 573 | 574 | if ( 575 | !this.actions.left && 576 | !this.actions.right && 577 | !this.actions.forward && 578 | !this.actions.backward && 579 | this.actions.run 580 | ) { 581 | this.player.animation = "idle"; 582 | } 583 | 584 | if ( 585 | this.actions.run && 586 | this.actions.left && 587 | this.actions.right && 588 | this.actions.forward && 589 | !this.actions.backward 590 | ) { 591 | this.player.animation = "running"; 592 | } 593 | 594 | if ( 595 | this.actions.run && 596 | this.actions.left && 597 | this.actions.right && 598 | this.actions.backward && 599 | !this.actions.forward 600 | ) { 601 | this.player.animation = "running"; 602 | } 603 | 604 | if ( 605 | this.actions.run && 606 | !this.actions.left && 607 | !this.actions.right && 608 | this.actions.forward && 609 | !this.actions.backward && 610 | this.player.animation !== "jumping" 611 | ) { 612 | this.player.animation = "running"; 613 | } 614 | 615 | if ( 616 | this.actions.run && 617 | !this.actions.left && 618 | !this.actions.right && 619 | this.actions.backward && 620 | !this.actions.forward && 621 | this.player.animation !== "jumping" 622 | ) { 623 | this.player.animation = "running"; 624 | } 625 | 626 | if ( 627 | this.actions.run && 628 | !this.actions.left && 629 | !this.actions.right && 630 | this.actions.backward && 631 | this.actions.forward && 632 | this.player.animation !== "jumping" 633 | ) { 634 | this.player.animation = "idle"; 635 | } 636 | 637 | if ( 638 | this.actions.run && 639 | this.actions.left && 640 | this.actions.right && 641 | !this.actions.backward && 642 | !this.actions.forward && 643 | this.player.animation !== "jumping" 644 | ) { 645 | this.player.animation = "idle"; 646 | } 647 | 648 | if ( 649 | this.actions.run && 650 | !this.actions.left && 651 | this.actions.right && 652 | !this.actions.backward && 653 | this.actions.forward && 654 | this.player.animation !== "jumping" 655 | ) { 656 | this.player.animation = "running"; 657 | } 658 | 659 | if ( 660 | this.actions.run && 661 | this.actions.left && 662 | !this.actions.right && 663 | this.actions.backward && 664 | !this.actions.forward && 665 | this.player.animation !== "jumping" 666 | ) { 667 | this.player.animation = "running"; 668 | } 669 | if ( 670 | this.actions.run && 671 | this.actions.left && 672 | !this.actions.right && 673 | !this.actions.backward && 674 | !this.actions.forward && 675 | this.player.animation !== "jumping" 676 | ) { 677 | this.player.animation = "running"; 678 | } 679 | if ( 680 | this.actions.run && 681 | !this.actions.left && 682 | this.actions.right && 683 | !this.actions.backward && 684 | !this.actions.forward && 685 | this.player.animation !== "jumping" 686 | ) { 687 | this.player.animation = "running"; 688 | } 689 | 690 | if ( 691 | this.actions.run && 692 | !this.actions.left && 693 | !this.actions.right && 694 | !this.actions.backward && 695 | !this.actions.forward && 696 | this.actions.jump 697 | ) { 698 | this.player.animation = "jumping"; 699 | } 700 | 701 | if (this.player.animation === "jumping" && !this.jumpOnce) { 702 | if (this.player.onFloor) { 703 | if (this.actions.run) { 704 | this.player.animation = "running"; 705 | } else if ( 706 | this.actions.forward || 707 | this.actions.backward || 708 | this.actions.left || 709 | this.actions.right 710 | ) { 711 | this.player.animation = "walking"; 712 | } else { 713 | this.player.animation = "idle"; 714 | } 715 | } 716 | } 717 | 718 | this.avatar.animation.play(this.player.animation); 719 | } else { 720 | this.avatar.animation.play("idle"); 721 | } 722 | } 723 | 724 | updateCameraPosition() { 725 | if ( 726 | this.player.animation !== "idle" && 727 | this.player.animation !== "dancing" 728 | ) { 729 | const cameraAngleFromPlayer = Math.atan2( 730 | this.player.body.position.x - this.avatar.avatar.position.x, 731 | this.player.body.position.z - this.avatar.avatar.position.z 732 | ); 733 | 734 | this.targetRotation.setFromAxisAngle( 735 | this.upVector, 736 | cameraAngleFromPlayer + this.player.directionOffset 737 | ); 738 | this.avatar.avatar.quaternion.rotateTowards( 739 | this.targetRotation, 740 | 0.15 741 | ); 742 | } 743 | } 744 | 745 | update() { 746 | if (this.avatar) { 747 | this.updateColliderMovement(); 748 | this.updateAvatarPosition(); 749 | this.updateAvatarRotation(); 750 | this.updateAvatarAnimation(); 751 | this.updateCameraPosition(); 752 | this.updateOtherPlayers(); 753 | } 754 | } 755 | } 756 | -------------------------------------------------------------------------------- /frontend/Experience/World/Westgate.js: -------------------------------------------------------------------------------- 1 | import Experience from "../Experience.js"; 2 | import * as THREE from "three"; 3 | 4 | export default class Westgate { 5 | constructor() { 6 | this.experience = new Experience(); 7 | this.scene = this.experience.scene; 8 | this.resources = this.experience.resources; 9 | this.octree = this.experience.world.octree; 10 | 11 | this.setWorld(); 12 | } 13 | 14 | setWorld() { 15 | this.bars = this.resources.items.bars.scene; 16 | this.brick = this.resources.items.brick.scene; 17 | this.buildings = this.resources.items.buildings.scene; 18 | this.easter = this.resources.items.easter.scene; 19 | this.everything = this.resources.items.everything.scene; 20 | this.floor = this.resources.items.floor.scene; 21 | this.grass = this.resources.items.grass.scene; 22 | this.other = this.resources.items.other.scene; 23 | this.outside = this.resources.items.outside.scene; 24 | this.panera = this.resources.items.panera.scene; 25 | this.plastic = this.resources.items.plastic.scene; 26 | this.tables = this.resources.items.tables.scene; 27 | this.thirdfloor = this.resources.items.thirdfloor.scene; 28 | this.box = this.resources.items.box.scene; 29 | 30 | this.glass = this.resources.items.glass.scene; 31 | this.screen = this.resources.items.screen.scene; 32 | 33 | this.screen.children[0].material = new THREE.MeshBasicMaterial({ 34 | map: this.resources.items.video, 35 | }); 36 | 37 | this.screen.children[0].material.flipY = false; 38 | 39 | this.collider = this.resources.items.collider.scene; 40 | this.octree.fromGraphNode(this.collider); 41 | 42 | this.glass.children.forEach((child) => { 43 | child.material = new THREE.MeshPhysicalMaterial(); 44 | child.material.roughness = 0; 45 | child.material.color.set(0xdfe5f5); 46 | child.material.ior = 1.5; 47 | child.material.transmission = 1; 48 | child.material.opacity = 1; 49 | 50 | // child.material = new THREE.MeshBasicMaterial({ 51 | // color: 0x949baf, 52 | // transparent: true, 53 | // opacity: 0.4, 54 | // }); 55 | }); 56 | 57 | this.box.children.forEach((child) => { 58 | this.resources.items.boxTexture.flipY = false; 59 | this.resources.items.boxTexture.colorSpace = THREE.SRGBColorSpace; 60 | child.material = new THREE.MeshBasicMaterial({ 61 | map: this.resources.items.boxTexture, 62 | }); 63 | }); 64 | this.bars.children.forEach((child) => { 65 | this.resources.items.barsTexture.flipY = false; 66 | this.resources.items.barsTexture.colorSpace = THREE.SRGBColorSpace; 67 | child.material = new THREE.MeshBasicMaterial({ 68 | map: this.resources.items.barsTexture, 69 | }); 70 | }); 71 | this.brick.children.forEach((child) => { 72 | this.resources.items.brickTexture.flipY = false; 73 | this.resources.items.brickTexture.colorSpace = THREE.SRGBColorSpace; 74 | 75 | child.material = new THREE.MeshBasicMaterial({ 76 | map: this.resources.items.brickTexture, 77 | }); 78 | }); 79 | this.buildings.children.forEach((child) => { 80 | this.resources.items.buildingsTexture.flipY = false; 81 | this.resources.items.buildingsTexture.colorSpace = 82 | THREE.SRGBColorSpace; 83 | 84 | child.material = new THREE.MeshBasicMaterial({ 85 | map: this.resources.items.buildingsTexture, 86 | }); 87 | }); 88 | this.easter.children.forEach((child) => { 89 | this.resources.items.easterTexture.flipY = false; 90 | this.resources.items.easterTexture.colorSpace = 91 | THREE.SRGBColorSpace; 92 | 93 | child.material = new THREE.MeshBasicMaterial({ 94 | map: this.resources.items.easterTexture, 95 | }); 96 | }); 97 | this.everything.children.forEach((child) => { 98 | this.resources.items.everythingTexture.flipY = false; 99 | this.resources.items.everythingTexture.colorSpace = 100 | THREE.SRGBColorSpace; 101 | 102 | child.material = new THREE.MeshBasicMaterial({ 103 | map: this.resources.items.everythingTexture, 104 | }); 105 | }); 106 | this.floor.children.forEach((child) => { 107 | this.resources.items.floorTexture.flipY = false; 108 | this.resources.items.floorTexture.colorSpace = THREE.SRGBColorSpace; 109 | 110 | child.material = new THREE.MeshBasicMaterial({ 111 | map: this.resources.items.floorTexture, 112 | }); 113 | }); 114 | this.grass.children.forEach((child) => { 115 | this.resources.items.grassTexture.flipY = false; 116 | this.resources.items.grassTexture.colorSpace = THREE.SRGBColorSpace; 117 | 118 | child.material = new THREE.MeshBasicMaterial({ 119 | map: this.resources.items.grassTexture, 120 | }); 121 | }); 122 | this.other.children.forEach((child) => { 123 | this.resources.items.otherTexture.flipY = false; 124 | this.resources.items.otherTexture.colorSpace = THREE.SRGBColorSpace; 125 | 126 | child.material = new THREE.MeshBasicMaterial({ 127 | map: this.resources.items.otherTexture, 128 | alphaTest: 0.5, 129 | side: THREE.DoubleSide, 130 | }); 131 | }); 132 | this.outside.children.forEach((child) => { 133 | this.resources.items.outsideTexture.flipY = false; 134 | this.resources.items.outsideTexture.colorSpace = 135 | THREE.SRGBColorSpace; 136 | 137 | child.material = new THREE.MeshBasicMaterial({ 138 | map: this.resources.items.outsideTexture, 139 | }); 140 | }); 141 | 142 | this.panera.children.forEach((child) => { 143 | this.resources.items.paneraTexture.flipY = false; 144 | this.resources.items.paneraTexture.colorSpace = 145 | THREE.SRGBColorSpace; 146 | child.material = new THREE.MeshBasicMaterial({ 147 | map: this.resources.items.paneraTexture, 148 | }); 149 | }); 150 | 151 | this.plastic.children.forEach((child) => { 152 | this.resources.items.plasticTexture.flipY = false; 153 | this.resources.items.plasticTexture.colorSpace = 154 | THREE.SRGBColorSpace; 155 | child.material = new THREE.MeshBasicMaterial({ 156 | map: this.resources.items.plasticTexture, 157 | }); 158 | }); 159 | 160 | this.tables.children.forEach((child) => { 161 | this.resources.items.tablesTexture.flipY = false; 162 | this.resources.items.tablesTexture.colorSpace = 163 | THREE.SRGBColorSpace; 164 | child.material = new THREE.MeshBasicMaterial({ 165 | map: this.resources.items.tablesTexture, 166 | }); 167 | }); 168 | 169 | this.thirdfloor.children.forEach((child) => { 170 | this.resources.items.thirdfloorTexture.flipY = false; 171 | this.resources.items.thirdfloorTexture.colorSpace = 172 | THREE.SRGBColorSpace; 173 | child.material = new THREE.MeshBasicMaterial({ 174 | map: this.resources.items.thirdfloorTexture, 175 | }); 176 | }); 177 | 178 | this.scene.add(this.glass); 179 | this.scene.add(this.screen); 180 | 181 | this.scene.add(this.bars); 182 | this.scene.add(this.brick); 183 | this.scene.add(this.buildings); 184 | this.scene.add(this.easter); 185 | this.scene.add(this.everything); 186 | this.scene.add(this.floor); 187 | this.scene.add(this.grass); 188 | this.scene.add(this.other); 189 | this.scene.add(this.outside); 190 | this.scene.add(this.panera); 191 | this.scene.add(this.plastic); 192 | this.scene.add(this.box); 193 | this.scene.add(this.tables); 194 | this.scene.add(this.thirdfloor); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /frontend/Experience/World/World.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import { EventEmitter } from "events"; 3 | import Experience from "../Experience.js"; 4 | 5 | import { Octree } from "three/examples/jsm/math/Octree"; 6 | 7 | import Player from "./Player/Player.js"; 8 | 9 | import Westgate from "./Westgate.js"; 10 | import Environment from "./Environment.js"; 11 | 12 | export default class World extends EventEmitter { 13 | constructor() { 14 | super(); 15 | this.experience = new Experience(); 16 | this.resources = this.experience.resources; 17 | 18 | this.octree = new Octree(); 19 | 20 | this.player = null; 21 | 22 | this.resources.on("ready", () => { 23 | if (this.player === null) { 24 | this.westgate = new Westgate(); 25 | this.player = new Player(); 26 | this.environment = new Environment(); 27 | } 28 | }); 29 | } 30 | 31 | update() { 32 | if (this.player) this.player.update(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | PSU VR 10 | 11 | 16 | 22 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 39 | 40 | 41 |
42 |
43 | 80 |
81 |
82 |
83 |
84 |
85 |
0
86 |
0
87 |
%
88 |
89 |

90 | Your VR experience is loading 91 |

92 |
93 | PSU VR is an virtual reality platform. It is not officially 94 | endorsed by PSU. It offers students an immersive way to interact 95 | with other students and faculty members in realistic 96 | environments. We Are VR! 97 |
98 | 99 | 100 |
Welcome to PSU VR
101 |
102 | 108 |
109 | 110 | 111 | 112 |
Choose your avatar
113 |
114 | 119 | 124 |
125 |
126 | Controls: WASD to move | SHIFT to run | SPACE to jump | ENTER to 127 | chat | Click/Drag to pan | Middle Mouse to zoom | o to dance 128 |
129 | 134 |
135 | 136 | 139 | 140 | 141 |
142 | 143 | 155 | 156 | 157 | 158 | 159 | 160 | 170 | 171 | 172 | 173 | 174 | 175 | 176 |
177 |
178 |
179 | 180 | 195 |
196 |
197 | 198 | 199 |
200 | 201 |
202 | 203 | 204 | 205 | 206 | -------------------------------------------------------------------------------- /frontend/index.js: -------------------------------------------------------------------------------- 1 | import "./index.scss"; 2 | import { io } from "socket.io-client"; 3 | import Experience from "./Experience/Experience.js"; 4 | import elements from "./Experience/Utils/functions/elements.js"; 5 | 6 | // Dom Elements ---------------------------------- 7 | 8 | const domElements = elements({ 9 | canvas: ".experience-canvas", 10 | chatContainer: ".chat-container", 11 | messageSubmitButton: "#chat-message-button", 12 | messageInput: "#chat-message-input", 13 | inputWrapper: ".message-input-wrapper", 14 | nameInputButton: "#name-input-button", 15 | nameInput: "#name-input", 16 | avatarLeftImg: ".avatar-left", 17 | avatarRightImg: ".avatar-right", 18 | }); 19 | 20 | // Frontend Server ---------------------------------- 21 | 22 | const socketUrl = new URL("/", window.location.href); 23 | 24 | // const socket = io(socketUrl.toString()); 25 | const chatSocket = io(socketUrl.toString() + "chat"); 26 | const updateSocket = io(socketUrl.toString() + "update"); 27 | let userName = ""; 28 | 29 | // Experience ---------------------------------- 30 | 31 | const experience = new Experience(domElements.canvas, updateSocket); 32 | 33 | // Sockets ---------------------------------- 34 | 35 | chatSocket.on("connect", () => { 36 | // console.log("Connected to server with ID" + chatSocket.id); 37 | }); 38 | 39 | domElements.messageSubmitButton.addEventListener("click", handleMessageSubmit); 40 | domElements.nameInputButton.addEventListener("click", handleNameSubmit); 41 | domElements.chatContainer.addEventListener("click", handleChatClick); 42 | domElements.avatarLeftImg.addEventListener( 43 | "click", 44 | handleCharacterSelectionLeft 45 | ); 46 | domElements.avatarRightImg.addEventListener( 47 | "click", 48 | handleCharacterSelectionRight 49 | ); 50 | document.addEventListener("keydown", handleMessageSubmit); 51 | 52 | function handleChatClick() { 53 | if (domElements.inputWrapper.classList.contains("hidden")) 54 | domElements.inputWrapper.classList.remove("hidden"); 55 | } 56 | 57 | function handleNameSubmit() { 58 | userName = domElements.nameInput.value; 59 | chatSocket.emit("setName", userName); 60 | updateSocket.emit("setName", userName); 61 | } 62 | 63 | function handleCharacterSelectionLeft() { 64 | updateSocket.emit("setAvatar", "male"); 65 | 66 | domElements.avatarLeftImg.removeEventListener( 67 | "click", 68 | handleCharacterSelectionLeft 69 | ); 70 | } 71 | function handleCharacterSelectionRight() { 72 | updateSocket.emit("setAvatar", "female"); 73 | 74 | domElements.avatarRightImg.removeEventListener( 75 | "click", 76 | handleCharacterSelectionRight 77 | ); 78 | } 79 | 80 | function handleMessageSubmit(event) { 81 | if (event.type === "click" || event.key === "Enter") { 82 | domElements.inputWrapper.classList.toggle("hidden"); 83 | domElements.messageInput.focus(); 84 | 85 | if (domElements.messageInput.value === "") return; 86 | displayMessage( 87 | userName, 88 | domElements.messageInput.value.substring(0, 500), 89 | getTime() 90 | ); 91 | chatSocket.emit( 92 | "send-message", 93 | domElements.messageInput.value.substring(0, 500), 94 | getTime() 95 | ); 96 | domElements.messageInput.value = ""; 97 | } 98 | } 99 | 100 | function getTime() { 101 | const currentDate = new Date(); 102 | const hours = currentDate.getHours().toString().padStart(2, "0"); 103 | const minutes = currentDate.getMinutes().toString().padStart(2, "0"); 104 | const time = `${hours}:${minutes}`; 105 | return time; 106 | } 107 | 108 | function displayMessage(name, message, time) { 109 | const messageDiv = document.createElement("div"); 110 | messageDiv.innerHTML = `[${time}] ${name}: ${message}`; 111 | domElements.chatContainer.append(messageDiv); 112 | domElements.chatContainer.scrollTop = 113 | domElements.chatContainer.scrollHeight; 114 | } 115 | 116 | // Get data from server ---------------------------------- 117 | 118 | chatSocket.on("recieved-message", (name, message, time) => { 119 | displayMessage(name, message, time); 120 | }); 121 | 122 | // Update Socket ---------------------------------------------------- 123 | updateSocket.on("connect", () => {}); 124 | 125 | const audio = document.getElementById("myAudio"); 126 | 127 | window.addEventListener("keydown", function (e) { 128 | if (e.code === "Equal") { 129 | if (!audio.paused) { 130 | audio.pause(); 131 | audio.currentTime = 0; 132 | } else { 133 | audio.play(); 134 | } 135 | } 136 | }); 137 | -------------------------------------------------------------------------------- /frontend/index.scss: -------------------------------------------------------------------------------- 1 | @import "./styles/defaults/reset.scss"; 2 | @import "./styles/defaults/fonts.scss"; 3 | @import "./styles/defaults/defaults.scss"; 4 | @import "./styles/defaults/variables.scss"; 5 | 6 | @import "./styles/utils/reusables.scss"; 7 | 8 | @import "./styles/components/preloader.scss"; 9 | @import "./styles/components/canvas.scss"; 10 | @import "./styles/components/menu.scss"; 11 | @import "./styles/components/nameinput.scss"; 12 | @import "./styles/components/chatbox.scss"; 13 | @import "./styles/components/controlsUI.scss"; 14 | -------------------------------------------------------------------------------- /frontend/styles/components/canvas.scss: -------------------------------------------------------------------------------- 1 | .experience-wrapper { 2 | position: absolute; 3 | height: 100vh; 4 | width: 100vw; 5 | z-index: -1; 6 | top: 0; 7 | left: 0; 8 | overflow: hidden; 9 | touch-action: none; 10 | } 11 | 12 | .experience-canvas { 13 | height: 100%; 14 | width: 100%; 15 | touch-action: none; 16 | overflow: hidden; 17 | cursor: grab; 18 | } 19 | 20 | canvas { 21 | overflow: hidden; 22 | } 23 | -------------------------------------------------------------------------------- /frontend/styles/components/chatbox.scss: -------------------------------------------------------------------------------- 1 | .chat-box { 2 | position: absolute; 3 | bottom: 48px; 4 | left: 48px; 5 | width: 400px; 6 | height: 200px; 7 | background-color: $color-base-black-transparent; 8 | color: $color-base-white; 9 | z-index: 50; 10 | font-size: 14px; 11 | word-wrap: break-word; 12 | @include media("<=tablet") { 13 | display: none; 14 | } 15 | } 16 | 17 | .chat-container { 18 | width: 400px; 19 | height: 200px; 20 | word-wrap: break-word; 21 | overflow-y: auto; 22 | } 23 | 24 | .form { 25 | @extend %center; 26 | position: absolute; 27 | bottom: 0px; 28 | flex-direction: row; 29 | } 30 | 31 | #chat-message-button { 32 | @extend %center; 33 | width: 50px; 34 | height: 24px; 35 | background-color: transparent; 36 | color: $color-base-white; 37 | position: absolute; 38 | right: -10px; 39 | z-index: 50; 40 | } 41 | 42 | .message-input-wrapper { 43 | @extend %center; 44 | position: absolute; 45 | bottom: -24px; 46 | left: 0px; 47 | width: 400px; 48 | } 49 | 50 | #chat-message-input { 51 | width: 100%; 52 | height: 24px; 53 | background-color: $color-base-black-semi-transparent; 54 | border: none; 55 | color: $color-base-white; 56 | font: inherit; 57 | } 58 | 59 | #chat-message-input:focus { 60 | background-color: $color-base-black-translucent; 61 | outline: none; 62 | } 63 | 64 | .different-color { 65 | color: $color-base-lightest-blue; 66 | } 67 | -------------------------------------------------------------------------------- /frontend/styles/components/controlsUI.scss: -------------------------------------------------------------------------------- 1 | .joystick-area { 2 | width: 140px; 3 | height: 140px; 4 | position: absolute; 5 | left: 5%; 6 | bottom: 5%; 7 | background: $color-base-white-transparent; 8 | z-index: 40; 9 | border-radius: 50% 50%; 10 | @include media(">=tablet") { 11 | display: none; 12 | } 13 | } 14 | 15 | .control-overlay { 16 | position: fixed; 17 | width: 100vw; 18 | height: 100vh; 19 | cursor: grab; 20 | overflow: hidden; 21 | touch-action: none; 22 | z-index: 25; 23 | } 24 | -------------------------------------------------------------------------------- /frontend/styles/components/menu.scss: -------------------------------------------------------------------------------- 1 | .menu-button { 2 | @extend %center; 3 | 4 | cursor: pointer; 5 | position: absolute; 6 | top: 48px; 7 | right: 48px; 8 | width: 40px; 9 | height: 40px; 10 | // background-color: $color-base-white; 11 | background-color: rgb(187, 187, 187); 12 | border-radius: 50%; 13 | border: none; 14 | transform-origin: center center; 15 | z-index: 999999999999999999999999999999999; 16 | 17 | &:hover { 18 | transform: scale(1.2, 1.2); 19 | } 20 | } 21 | 22 | .menu-circle { 23 | width: 40px; 24 | height: 40px; 25 | background-color: $color-base-white; 26 | border-radius: 50%; 27 | } 28 | 29 | .menu-bar { 30 | width: 24px; 31 | height: 2px; 32 | display: block; 33 | background-color: $color-base-dark-blue; 34 | } 35 | 36 | .menu-bar-wrapper { 37 | display: flex; 38 | flex-direction: column; 39 | justify-content: space-between; 40 | height: 17px; 41 | } 42 | 43 | .switch-camera-view { 44 | @extend %center; 45 | cursor: pointer; 46 | position: absolute; 47 | top: 48px; 48 | right: 48px; 49 | width: 40px; 50 | height: 40px; 51 | background-color: $color-base-white; 52 | border-radius: 50%; 53 | border: none; 54 | z-index: 999999999999999999999999999999999; 55 | } 56 | -------------------------------------------------------------------------------- /frontend/styles/components/nameinput.scss: -------------------------------------------------------------------------------- 1 | #name-input { 2 | position: absolute; 3 | z-index: 99; 4 | } 5 | -------------------------------------------------------------------------------- /frontend/styles/components/preloader.scss: -------------------------------------------------------------------------------- 1 | .preloader { 2 | @extend %center; 3 | @extend %cover; 4 | background: $color-base-white; 5 | z-index: 9999999999999; 6 | width: 100vw; 7 | height: 100vh; 8 | touch-action: none; 9 | overflow: hidden; 10 | } 11 | 12 | .preloader-title { 13 | @extend %center; 14 | @extend %light-font; 15 | color: $color-base-gray; 16 | position: absolute; 17 | bottom: 14%; 18 | width: 250px; 19 | 20 | // letter-spacing: 2px; 21 | // text-transform: uppercase; 22 | } 23 | 24 | .preloader-wrapper { 25 | @extend %center; 26 | flex-direction: column; 27 | gap: 48px; 28 | } 29 | 30 | .preloader-progress { 31 | @extend %center; 32 | flex-direction: column; 33 | position: absolute; 34 | bottom: 10%; 35 | } 36 | 37 | .progress-wrapper { 38 | @extend %center; 39 | position: absolute; 40 | bottom: 17%; 41 | } 42 | 43 | .preloader-wrapper { 44 | padding-bottom: 120px; 45 | position: relative; 46 | } 47 | 48 | .preloader-percentage1 { 49 | text-align: right; 50 | } 51 | 52 | .symbol { 53 | position: absolute; 54 | right: -11px; 55 | } 56 | 57 | .percent { 58 | @extend %light-font; 59 | width: 16px; 60 | color: $color-base-gray; 61 | // @include media("<=tablet") { 62 | // font-size: 24px; 63 | // } 64 | } 65 | 66 | .progress-bar-container { 67 | width: 300px; 68 | height: 2px; 69 | background-color: $color-base-light-gray; 70 | border-radius: 7px; 71 | position: absolute; 72 | top: 50%; 73 | } 74 | 75 | .progress-bar { 76 | width: 0%; 77 | height: 2px; 78 | background-color: $color-base-dark-blue; 79 | border-radius: 7px; 80 | } 81 | 82 | .svgLogo { 83 | position: absolute; 84 | top: -100%; 85 | } 86 | 87 | .fade-in-out { 88 | animation: fade-in-out 2s ease-in-out infinite; 89 | } 90 | 91 | @keyframes fade-in-out { 92 | 0% { 93 | opacity: 0.35; 94 | } 95 | 50% { 96 | opacity: 1; 97 | } 98 | 100% { 99 | opacity: 0.35; 100 | } 101 | } 102 | 103 | // Second preloader screen 104 | .welcome-title { 105 | @extend %center; 106 | font-size: 48px; 107 | opacity: 0; 108 | color: $color-base-medium-blue; 109 | font-weight: 200; 110 | position: absolute; 111 | top: 35%; 112 | left: 50%; 113 | width: 100%; 114 | transform: translate(-50%, -50%); 115 | letter-spacing: 2px; 116 | @include media("<=phone") { 117 | font-size: 28px; 118 | } 119 | } 120 | 121 | .description { 122 | @extend %center; 123 | font-size: 12px; 124 | // opacity: 0; 125 | color: $color-base-medium-blue; 126 | font-weight: 300; 127 | position: absolute; 128 | bottom: 30%; 129 | width: 500px; 130 | left: 50%; 131 | transform: translate(-50%, -50%); 132 | letter-spacing: 2px; 133 | } 134 | 135 | .name-form { 136 | @extend %center; 137 | position: absolute; 138 | top: 46%; 139 | left: 50%; 140 | width: 500px; 141 | opacity: 0; 142 | transform: translate(-50%, -50%); 143 | } 144 | 145 | #name-input { 146 | font-size: 18px; 147 | border: none; 148 | width: 400px; 149 | color: $color-base-medium-blue; 150 | border-bottom: 1px solid $color-base-medium-blue; 151 | outline: none; 152 | background-color: transparent; 153 | @include media("<=phone") { 154 | width: 250px; 155 | font-size: 16px; 156 | } 157 | } 158 | 159 | #name-input::placeholder { 160 | color: $color-base-light-blue; 161 | opacity: 0.5; 162 | } 163 | 164 | #name-input:focus { 165 | border-bottom: 1px solid $color-base-dark-blue; 166 | } 167 | 168 | #name-input-button { 169 | color: $color-base-dark-blue; 170 | outline: 1px solid $color-base-dark-blue; 171 | background-color: transparent; 172 | position: absolute; 173 | width: 100px; 174 | height: 30px; 175 | bottom: 42%; 176 | font-size: 14px; 177 | text-transform: uppercase; 178 | opacity: 0; 179 | transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out, 180 | letter-spacing 0.2s ease-in-out; 181 | 182 | &:hover { 183 | background-color: $color-base-dark-blue; 184 | color: white; 185 | outline: 1px solid $color-base-dark-blue; 186 | letter-spacing: 2px; 187 | transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out, 188 | letter-spacing 0.2s ease-in-out; 189 | } 190 | } 191 | 192 | // Character Selection Screen ----------------------- 193 | .character-select-title { 194 | @extend %center; 195 | font-size: 48px; 196 | opacity: 0; 197 | color: $color-base-medium-blue; 198 | font-weight: 200; 199 | position: absolute; 200 | top: 15%; 201 | left: 50%; 202 | width: 100%; 203 | transform: translate(-50%, -50%); 204 | letter-spacing: 2px; 205 | @include media("<=phone") { 206 | font-size: 28px; 207 | } 208 | z-index: -99; 209 | } 210 | 211 | .avatar-img-wrapper { 212 | @extend %center; 213 | // visibility: hidden; 214 | pointer-events: none; 215 | gap: 64px; 216 | position: absolute; 217 | top: 45%; 218 | left: 50%; 219 | width: 500px; 220 | opacity: 0; 221 | transform: translate(-50%, -50%); 222 | z-index: -99; 223 | } 224 | 225 | .avatar-img { 226 | width: auto; 227 | height: 200px; 228 | cursor: pointer; 229 | transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out; 230 | 231 | &:hover { 232 | background-color: $color-base-light-gray; 233 | transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out; 234 | } 235 | } 236 | 237 | .customize-character-btn { 238 | position: absolute; 239 | bottom: 30%; 240 | opacity: 0; 241 | background-color: transparent; 242 | z-index: -99; 243 | } 244 | -------------------------------------------------------------------------------- /frontend/styles/defaults/defaults.scss: -------------------------------------------------------------------------------- 1 | @import "../../../node_modules/include-media/dist/_include-media.scss"; 2 | @import url("https://fonts.googleapis.com/css2?family=Manrope:wght@200;300;400;500;600;700;800&display=swap"); 3 | 4 | $breakpoints: ( 5 | "phone": 514px, 6 | "tablet": 768px, 7 | "desktop": 1024px, 8 | ); 9 | 10 | body { 11 | font-family: "Manrope", sans-serif; 12 | } 13 | -------------------------------------------------------------------------------- /frontend/styles/defaults/fonts.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "Gilroy"; 3 | src: url("/fonts/Gilroy-BlackItalic.woff2") format("woff2"), 4 | url("/fonts/Gilroy-BlackItalic.woff") format("woff"); 5 | font-weight: 900; 6 | font-style: italic; 7 | font-display: swap; 8 | } 9 | 10 | @font-face { 11 | font-family: "Gilroy"; 12 | src: url("/fonts/Gilroy-Black.woff2") format("woff2"), 13 | url("/fonts/Gilroy-Black.woff") format("woff"); 14 | font-weight: 900; 15 | font-style: normal; 16 | font-display: swap; 17 | } 18 | 19 | @font-face { 20 | font-family: "Gilroy"; 21 | src: url("/fonts/Gilroy-Bold.woff2") format("woff2"), 22 | url("/fonts/Gilroy-Bold.woff") format("woff"); 23 | font-weight: bold; 24 | font-style: normal; 25 | font-display: swap; 26 | } 27 | 28 | @font-face { 29 | font-family: "Gilroy"; 30 | src: url("/fonts/Gilroy-ExtraBoldItalic.woff2") format("woff2"), 31 | url("/fonts/Gilroy-ExtraBoldItalic.woff") format("woff"); 32 | font-weight: bold; 33 | font-style: italic; 34 | font-display: swap; 35 | } 36 | 37 | @font-face { 38 | font-family: "Gilroy"; 39 | src: url("/fonts/Gilroy-ExtraBold.woff2") format("woff2"), 40 | url("/fonts/Gilroy-ExtraBold.woff") format("woff"); 41 | font-weight: bold; 42 | font-style: normal; 43 | font-display: swap; 44 | } 45 | 46 | @font-face { 47 | font-family: "Gilroy"; 48 | src: url("/fonts/Gilroy-BoldItalic.woff2") format("woff2"), 49 | url("/fonts/Gilroy-BoldItalic.woff") format("woff"); 50 | font-weight: bold; 51 | font-style: italic; 52 | font-display: swap; 53 | } 54 | 55 | @font-face { 56 | font-family: "Gilroy"; 57 | src: url("/fonts/Gilroy-LightItalic.woff2") format("woff2"), 58 | url("/fonts/Gilroy-LightItalic.woff") format("woff"); 59 | font-weight: 300; 60 | font-style: italic; 61 | font-display: swap; 62 | } 63 | 64 | @font-face { 65 | font-family: "Gilroy"; 66 | src: url("/fonts/Gilroy-Medium.woff2") format("woff2"), 67 | url("/fonts/Gilroy-Medium.woff") format("woff"); 68 | font-weight: 500; 69 | font-style: normal; 70 | font-display: swap; 71 | } 72 | 73 | @font-face { 74 | font-family: "Gilroy"; 75 | src: url("/fonts/Gilroy-Heavy.woff2") format("woff2"), 76 | url("/fonts/Gilroy-Heavy.woff") format("woff"); 77 | font-weight: 900; 78 | font-style: normal; 79 | font-display: swap; 80 | } 81 | 82 | @font-face { 83 | font-family: "Gilroy"; 84 | src: url("/fonts/Gilroy-HeavyItalic.woff2") format("woff2"), 85 | url("/fonts/Gilroy-HeavyItalic.woff") format("woff"); 86 | font-weight: 900; 87 | font-style: italic; 88 | font-display: swap; 89 | } 90 | 91 | @font-face { 92 | font-family: "Gilroy"; 93 | src: url("/fonts/Gilroy-Light.woff2") format("woff2"), 94 | url("/fonts/Gilroy-Light.woff") format("woff"); 95 | font-weight: 300; 96 | font-style: normal; 97 | font-display: swap; 98 | } 99 | 100 | @font-face { 101 | font-family: "Gilroy"; 102 | src: url("/fonts/Gilroy-SemiBoldItalic.woff2") format("woff2"), 103 | url("/fonts/Gilroy-SemiBoldItalic.woff") format("woff"); 104 | font-weight: 600; 105 | font-style: italic; 106 | font-display: swap; 107 | } 108 | 109 | @font-face { 110 | font-family: "Gilroy-RegularItalic"; 111 | src: url("/fonts/Gilroy-RegularItalic.woff2") format("woff2"), 112 | url("/fonts/Gilroy-RegularItalic.woff") format("woff"); 113 | font-weight: normal; 114 | font-style: italic; 115 | font-display: swap; 116 | } 117 | 118 | @font-face { 119 | font-family: "Gilroy"; 120 | src: url("/fonts/Gilroy-SemiBold.woff2") format("woff2"), 121 | url("/fonts/Gilroy-SemiBold.woff") format("woff"); 122 | font-weight: 600; 123 | font-style: normal; 124 | font-display: swap; 125 | } 126 | 127 | @font-face { 128 | font-family: "Gilroy"; 129 | src: url("/fonts/Gilroy-MediumItalic.woff2") format("woff2"), 130 | url("/fonts/Gilroy-MediumItalic.woff") format("woff"); 131 | font-weight: 500; 132 | font-style: italic; 133 | font-display: swap; 134 | } 135 | 136 | @font-face { 137 | font-family: "Gilroy"; 138 | src: url("/fonts/Gilroy-Regular.woff2") format("woff2"), 139 | url("/fonts/Gilroy-Regular.woff") format("woff"); 140 | font-weight: normal; 141 | font-style: normal; 142 | font-display: swap; 143 | } 144 | 145 | @font-face { 146 | font-family: "Gilroy"; 147 | src: url("/fonts/Gilroy-ThinItalic.woff2") format("woff2"), 148 | url("/fonts/Gilroy-ThinItalic.woff") format("woff"); 149 | font-weight: 100; 150 | font-style: italic; 151 | font-display: swap; 152 | } 153 | 154 | @font-face { 155 | font-family: "Gilroy"; 156 | src: url("/fonts/Gilroy-UltraLightItalic.woff2") format("woff2"), 157 | url("/fonts/Gilroy-UltraLightItalic.woff") format("woff"); 158 | font-weight: 200; 159 | font-style: italic; 160 | font-display: swap; 161 | } 162 | 163 | @font-face { 164 | font-family: "Gilroy"; 165 | src: url("/fonts/Gilroy-Thin.woff2") format("woff2"), 166 | url("/fonts/Gilroy-Thin.woff") format("woff"); 167 | font-weight: 100; 168 | font-style: normal; 169 | font-display: swap; 170 | } 171 | 172 | @font-face { 173 | font-family: "Gilroy"; 174 | src: url("/fonts/Gilroy-UltraLight.woff2") format("woff2"), 175 | url("/fonts/Gilroy-UltraLight.woff") format("woff"); 176 | font-weight: 200; 177 | font-style: normal; 178 | font-display: swap; 179 | } 180 | -------------------------------------------------------------------------------- /frontend/styles/defaults/reset.scss: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | html, 8 | body { 9 | overflow: hidden; 10 | } 11 | 12 | html, 13 | body, 14 | div, 15 | span, 16 | applet, 17 | object, 18 | iframe, 19 | h1, 20 | h2, 21 | h3, 22 | h4, 23 | h5, 24 | h6, 25 | p, 26 | blockquote, 27 | pre, 28 | a, 29 | abbr, 30 | acronym, 31 | address, 32 | big, 33 | cite, 34 | code, 35 | del, 36 | dfn, 37 | em, 38 | img, 39 | ins, 40 | kbd, 41 | q, 42 | s, 43 | samp, 44 | small, 45 | button, 46 | strike, 47 | strong, 48 | sub, 49 | sup, 50 | tt, 51 | var, 52 | b, 53 | u, 54 | i, 55 | center, 56 | dl, 57 | dt, 58 | dd, 59 | ol, 60 | ul, 61 | li, 62 | fieldset, 63 | form, 64 | label, 65 | legend, 66 | table, 67 | caption, 68 | tbody, 69 | tfoot, 70 | thead, 71 | tr, 72 | th, 73 | td, 74 | article, 75 | aside, 76 | details, 77 | embed, 78 | figure, 79 | figcaption, 80 | footer, 81 | header, 82 | hgroup, 83 | menu, 84 | nav, 85 | output, 86 | ruby, 87 | section, 88 | summary, 89 | time, 90 | mark, 91 | audio, 92 | input, 93 | video { 94 | margin: 0; 95 | padding: 0; 96 | border: 0; 97 | font-size: 100%; 98 | font: inherit; 99 | vertical-align: baseline; 100 | } 101 | 102 | ol, 103 | ul { 104 | list-style: none; 105 | } 106 | 107 | button { 108 | cursor: pointer; 109 | border: none; 110 | } 111 | -------------------------------------------------------------------------------- /frontend/styles/defaults/variables.scss: -------------------------------------------------------------------------------- 1 | // Flat base colors 2 | $color-base-white: #fffefe; 3 | $color-base-lightest-blue: #c2d6ff; 4 | $color-base-light-blue: #6699ff; 5 | $color-base-medium-blue: #3366cc; 6 | $color-base-dark-blue: #1e407c; 7 | $color-base-gray: #535353; 8 | $color-base-light-gray: #c7c7c7; 9 | 10 | $color-base-white-transparent: #fcfcfcbe; 11 | $color-base-black-transparent: #303030be; 12 | $color-base-black-semi-transparent: #303030ce; 13 | $color-base-black-translucent: #303030fd; 14 | // Text colors 15 | -------------------------------------------------------------------------------- /frontend/styles/utils/reusables.scss: -------------------------------------------------------------------------------- 1 | %center { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | } 6 | 7 | %cover { 8 | height: 100%; 9 | width: 100%; 10 | left: 0; 11 | top: 0; 12 | position: absolute; 13 | } 14 | 15 | %light-font { 16 | font-weight: 300; 17 | font-size: 16px; 18 | } 19 | 20 | .hidden { 21 | display: none; 22 | } 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "psuvr", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "frontend-dev": "vite --host", 8 | "frontend-build": "vite build --watch", 9 | "backend-build": "node server.js", 10 | "backend-dev": "nodemon server.js", 11 | "dev": "concurrently --kill-others \"npm run frontend-build\" \"npm run backend-dev\"", 12 | "heroku-postbuild": "vite build" 13 | }, 14 | "devDependencies": { 15 | "concurrently": "^8.2.0", 16 | "gsap": "^3.12.2", 17 | "include-media": "^2.0.0", 18 | "nodemon": "^2.0.22", 19 | "sass": "^1.63.6", 20 | "vite": "^4.3.9" 21 | }, 22 | "dependencies": { 23 | "cors": "^2.8.5", 24 | "dotenv": "^16.3.1", 25 | "events": "^3.3.0", 26 | "express": "^4.18.2", 27 | "nipplejs": "^0.10.1", 28 | "socket.io": "^4.7.1", 29 | "socket.io-client": "^4.7.1", 30 | "three": "^0.154.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /public/draco/README.md: -------------------------------------------------------------------------------- 1 | # Draco 3D Data Compression 2 | 3 | Draco is an open-source library for compressing and decompressing 3D geometric meshes and point clouds. It is intended to improve the storage and transmission of 3D graphics. 4 | 5 | [Website](https://google.github.io/draco/) | [GitHub](https://github.com/google/draco) 6 | 7 | ## Contents 8 | 9 | This folder contains three utilities: 10 | 11 | * `draco_decoder.js` — Emscripten-compiled decoder, compatible with any modern browser. 12 | * `draco_decoder.wasm` — WebAssembly decoder, compatible with newer browsers and devices. 13 | * `draco_wasm_wrapper.js` — JavaScript wrapper for the WASM decoder. 14 | 15 | Each file is provided in two variations: 16 | 17 | * **Default:** Latest stable builds, tracking the project's [master branch](https://github.com/google/draco). 18 | * **glTF:** Builds targeted by the [glTF mesh compression extension](https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_draco_mesh_compression), tracking the [corresponding Draco branch](https://github.com/google/draco/tree/gltf_2.0_draco_extension). 19 | 20 | Either variation may be used with `THREE.DRACOLoader`: 21 | 22 | ```js 23 | var dracoLoader = new THREE.DRACOLoader(); 24 | dracoLoader.setDecoderPath('path/to/decoders/'); 25 | dracoLoader.setDecoderConfig({type: 'js'}); // (Optional) Override detection of WASM support. 26 | ``` 27 | 28 | Further [documentation on GitHub](https://github.com/google/draco/tree/master/javascript/example#static-loading-javascript-decoder). 29 | 30 | ## License 31 | 32 | [Apache License 2.0](https://github.com/google/draco/blob/master/LICENSE) 33 | -------------------------------------------------------------------------------- /public/draco/draco_decoder.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/draco/draco_decoder.wasm -------------------------------------------------------------------------------- /public/draco/gltf/draco_decoder.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/draco/gltf/draco_decoder.wasm -------------------------------------------------------------------------------- /public/fonts/Gilroy-Black.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/fonts/Gilroy-Black.woff -------------------------------------------------------------------------------- /public/fonts/Gilroy-Black.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/fonts/Gilroy-Black.woff2 -------------------------------------------------------------------------------- /public/fonts/Gilroy-BlackItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/fonts/Gilroy-BlackItalic.woff -------------------------------------------------------------------------------- /public/fonts/Gilroy-BlackItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/fonts/Gilroy-BlackItalic.woff2 -------------------------------------------------------------------------------- /public/fonts/Gilroy-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/fonts/Gilroy-Bold.woff -------------------------------------------------------------------------------- /public/fonts/Gilroy-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/fonts/Gilroy-Bold.woff2 -------------------------------------------------------------------------------- /public/fonts/Gilroy-BoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/fonts/Gilroy-BoldItalic.woff -------------------------------------------------------------------------------- /public/fonts/Gilroy-BoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/fonts/Gilroy-BoldItalic.woff2 -------------------------------------------------------------------------------- /public/fonts/Gilroy-ExtraBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/fonts/Gilroy-ExtraBold.woff -------------------------------------------------------------------------------- /public/fonts/Gilroy-ExtraBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/fonts/Gilroy-ExtraBold.woff2 -------------------------------------------------------------------------------- /public/fonts/Gilroy-ExtraBoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/fonts/Gilroy-ExtraBoldItalic.woff -------------------------------------------------------------------------------- /public/fonts/Gilroy-ExtraBoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/fonts/Gilroy-ExtraBoldItalic.woff2 -------------------------------------------------------------------------------- /public/fonts/Gilroy-Heavy.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/fonts/Gilroy-Heavy.woff -------------------------------------------------------------------------------- /public/fonts/Gilroy-Heavy.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/fonts/Gilroy-Heavy.woff2 -------------------------------------------------------------------------------- /public/fonts/Gilroy-HeavyItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/fonts/Gilroy-HeavyItalic.woff -------------------------------------------------------------------------------- /public/fonts/Gilroy-HeavyItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/fonts/Gilroy-HeavyItalic.woff2 -------------------------------------------------------------------------------- /public/fonts/Gilroy-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/fonts/Gilroy-Light.woff -------------------------------------------------------------------------------- /public/fonts/Gilroy-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/fonts/Gilroy-Light.woff2 -------------------------------------------------------------------------------- /public/fonts/Gilroy-LightItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/fonts/Gilroy-LightItalic.woff -------------------------------------------------------------------------------- /public/fonts/Gilroy-LightItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/fonts/Gilroy-LightItalic.woff2 -------------------------------------------------------------------------------- /public/fonts/Gilroy-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/fonts/Gilroy-Medium.woff -------------------------------------------------------------------------------- /public/fonts/Gilroy-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/fonts/Gilroy-Medium.woff2 -------------------------------------------------------------------------------- /public/fonts/Gilroy-MediumItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/fonts/Gilroy-MediumItalic.woff -------------------------------------------------------------------------------- /public/fonts/Gilroy-MediumItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/fonts/Gilroy-MediumItalic.woff2 -------------------------------------------------------------------------------- /public/fonts/Gilroy-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/fonts/Gilroy-Regular.woff -------------------------------------------------------------------------------- /public/fonts/Gilroy-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/fonts/Gilroy-Regular.woff2 -------------------------------------------------------------------------------- /public/fonts/Gilroy-RegularItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/fonts/Gilroy-RegularItalic.woff -------------------------------------------------------------------------------- /public/fonts/Gilroy-RegularItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/fonts/Gilroy-RegularItalic.woff2 -------------------------------------------------------------------------------- /public/fonts/Gilroy-SemiBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/fonts/Gilroy-SemiBold.woff -------------------------------------------------------------------------------- /public/fonts/Gilroy-SemiBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/fonts/Gilroy-SemiBold.woff2 -------------------------------------------------------------------------------- /public/fonts/Gilroy-SemiBoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/fonts/Gilroy-SemiBoldItalic.woff -------------------------------------------------------------------------------- /public/fonts/Gilroy-SemiBoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/fonts/Gilroy-SemiBoldItalic.woff2 -------------------------------------------------------------------------------- /public/fonts/Gilroy-Thin.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/fonts/Gilroy-Thin.woff -------------------------------------------------------------------------------- /public/fonts/Gilroy-Thin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/fonts/Gilroy-Thin.woff2 -------------------------------------------------------------------------------- /public/fonts/Gilroy-ThinItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/fonts/Gilroy-ThinItalic.woff -------------------------------------------------------------------------------- /public/fonts/Gilroy-ThinItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/fonts/Gilroy-ThinItalic.woff2 -------------------------------------------------------------------------------- /public/fonts/Gilroy-UltraLight.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/fonts/Gilroy-UltraLight.woff -------------------------------------------------------------------------------- /public/fonts/Gilroy-UltraLight.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/fonts/Gilroy-UltraLight.woff2 -------------------------------------------------------------------------------- /public/fonts/Gilroy-UltraLightItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/fonts/Gilroy-UltraLightItalic.woff -------------------------------------------------------------------------------- /public/fonts/Gilroy-UltraLightItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/fonts/Gilroy-UltraLightItalic.woff2 -------------------------------------------------------------------------------- /public/images/asian_female.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/images/asian_female.png -------------------------------------------------------------------------------- /public/images/asian_female_head.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/images/asian_female_head.png -------------------------------------------------------------------------------- /public/images/asian_male.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/images/asian_male.png -------------------------------------------------------------------------------- /public/images/asian_male_head.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/images/asian_male_head.png -------------------------------------------------------------------------------- /public/media/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/media/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/media/android-chrome-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/media/android-chrome-384x384.png -------------------------------------------------------------------------------- /public/media/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/media/apple-touch-icon.png -------------------------------------------------------------------------------- /public/media/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/media/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/media/favicon-16x16.png -------------------------------------------------------------------------------- /public/media/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/media/favicon-32x32.png -------------------------------------------------------------------------------- /public/media/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/media/favicon.ico -------------------------------------------------------------------------------- /public/media/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/media/mstile-150x150.png -------------------------------------------------------------------------------- /public/media/music.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/media/music.mp3 -------------------------------------------------------------------------------- /public/media/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-384x384.png", 12 | "sizes": "384x384", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /public/models/asian_female_animated.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/models/asian_female_animated.glb -------------------------------------------------------------------------------- /public/models/asian_male_animated.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/models/asian_male_animated.glb -------------------------------------------------------------------------------- /public/models/bars.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/models/bars.glb -------------------------------------------------------------------------------- /public/models/box.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/models/box.glb -------------------------------------------------------------------------------- /public/models/brick.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/models/brick.glb -------------------------------------------------------------------------------- /public/models/buildings.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/models/buildings.glb -------------------------------------------------------------------------------- /public/models/collider.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/models/collider.glb -------------------------------------------------------------------------------- /public/models/easter.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/models/easter.glb -------------------------------------------------------------------------------- /public/models/everything.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/models/everything.glb -------------------------------------------------------------------------------- /public/models/floor.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/models/floor.glb -------------------------------------------------------------------------------- /public/models/glass.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/models/glass.glb -------------------------------------------------------------------------------- /public/models/grass.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/models/grass.glb -------------------------------------------------------------------------------- /public/models/other.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/models/other.glb -------------------------------------------------------------------------------- /public/models/outside.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/models/outside.glb -------------------------------------------------------------------------------- /public/models/panera.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/models/panera.glb -------------------------------------------------------------------------------- /public/models/plastic.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/models/plastic.glb -------------------------------------------------------------------------------- /public/models/screen.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/models/screen.glb -------------------------------------------------------------------------------- /public/models/tables.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/models/tables.glb -------------------------------------------------------------------------------- /public/models/thirdfloor.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/models/thirdfloor.glb -------------------------------------------------------------------------------- /public/textures/baked/bars.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/textures/baked/bars.jpg -------------------------------------------------------------------------------- /public/textures/baked/box.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/textures/baked/box.jpg -------------------------------------------------------------------------------- /public/textures/baked/brick.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/textures/baked/brick.jpg -------------------------------------------------------------------------------- /public/textures/baked/buildings.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/textures/baked/buildings.jpg -------------------------------------------------------------------------------- /public/textures/baked/easter.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/textures/baked/easter.jpg -------------------------------------------------------------------------------- /public/textures/baked/everything.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/textures/baked/everything.jpg -------------------------------------------------------------------------------- /public/textures/baked/floor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/textures/baked/floor.jpg -------------------------------------------------------------------------------- /public/textures/baked/grass.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/textures/baked/grass.jpg -------------------------------------------------------------------------------- /public/textures/baked/other.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/textures/baked/other.webp -------------------------------------------------------------------------------- /public/textures/baked/outside.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/textures/baked/outside.jpg -------------------------------------------------------------------------------- /public/textures/baked/panera.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/textures/baked/panera.jpg -------------------------------------------------------------------------------- /public/textures/baked/plastic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/textures/baked/plastic.jpg -------------------------------------------------------------------------------- /public/textures/baked/tables.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/textures/baked/tables.jpg -------------------------------------------------------------------------------- /public/textures/baked/thirdfloor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/textures/baked/thirdfloor.jpg -------------------------------------------------------------------------------- /public/textures/environment/nx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/textures/environment/nx.png -------------------------------------------------------------------------------- /public/textures/environment/ny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/textures/environment/ny.png -------------------------------------------------------------------------------- /public/textures/environment/nz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/textures/environment/nz.png -------------------------------------------------------------------------------- /public/textures/environment/px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/textures/environment/px.png -------------------------------------------------------------------------------- /public/textures/environment/py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/textures/environment/py.png -------------------------------------------------------------------------------- /public/textures/environment/pz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/textures/environment/pz.png -------------------------------------------------------------------------------- /public/videos/tour.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewwoan/PSU-VR/010391abc4cac1cb58b3392c9989c0852c91d72e/public/videos/tour.mp4 -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import path from "path"; 3 | import http from "http"; 4 | import { Server } from "socket.io"; 5 | 6 | const port = process.env.PORT || 3000; 7 | 8 | const app = express(); 9 | const server = http.createServer(app); 10 | const io = new Server(server, { 11 | cors: { 12 | origin: "*", 13 | methods: "*", 14 | }, 15 | }); 16 | 17 | app.use(express.static("dist")); 18 | 19 | const indexPath = path.join(process.cwd(), "dist", "index.html"); 20 | 21 | app.get("*", (req, res) => { 22 | res.sendFile(indexPath); 23 | }); 24 | 25 | // Chat Name Space ---------------------------------------- 26 | 27 | const chatNameSpace = io.of("/chat"); 28 | 29 | chatNameSpace.on("connection", (socket) => { 30 | socket.userData = { 31 | name: "", 32 | }; 33 | console.log(`${socket.id} has connected to chat namespace`); 34 | 35 | socket.on("disconnect", () => { 36 | console.log(`${socket.id} has disconnected`); 37 | }); 38 | 39 | socket.on("setName", (name) => { 40 | socket.userData.name = name; 41 | }); 42 | 43 | socket.on("send-message", (message, time) => { 44 | socket.broadcast.emit( 45 | "recieved-message", 46 | socket.userData.name, 47 | message, 48 | time 49 | ); 50 | }); 51 | }); 52 | 53 | // Update Name Space ---------------------------------------- 54 | const updateNameSpace = io.of("/update"); 55 | 56 | const connectedSockets = new Map(); 57 | 58 | updateNameSpace.on("connection", (socket) => { 59 | socket.userData = { 60 | position: { x: 0, y: -500, z: -500 }, 61 | quaternion: { x: 0, y: 0, z: 0, w: 0 }, 62 | animation: "idle", 63 | name: "", 64 | avatarSkin: "", 65 | }; 66 | connectedSockets.set(socket.id, socket); 67 | 68 | console.log(`${socket.id} has connected to update namespace`); 69 | 70 | socket.on("setID", () => { 71 | updateNameSpace.emit("setID", socket.id); 72 | }); 73 | 74 | socket.on("setName", (name) => { 75 | socket.userData.name = name; 76 | }); 77 | 78 | socket.on("setAvatar", (avatarSkin) => { 79 | // console.log("setting avatar " + avatarSkin); 80 | updateNameSpace.emit("setAvatarSkin", avatarSkin, socket.id); 81 | }); 82 | 83 | socket.on("disconnect", () => { 84 | console.log(`${socket.id} has disconnected`); 85 | connectedSockets.delete(socket.id); 86 | updateNameSpace.emit("removePlayer", socket.id); 87 | }); 88 | 89 | socket.on("initPlayer", (player) => { 90 | // console.log(player); 91 | }); 92 | 93 | socket.on("updatePlayer", (player) => { 94 | socket.userData.position.x = player.position.x; 95 | socket.userData.position.y = player.position.y; 96 | socket.userData.position.z = player.position.z; 97 | socket.userData.quaternion.x = player.quaternion[0]; 98 | socket.userData.quaternion.y = player.quaternion[1]; 99 | socket.userData.quaternion.z = player.quaternion[2]; 100 | socket.userData.quaternion.w = player.quaternion[3]; 101 | socket.userData.animation = player.animation; 102 | socket.userData.avatarSkin = player.avatarSkin; 103 | }); 104 | 105 | setInterval(() => { 106 | const playerData = []; 107 | for (const socket of connectedSockets.values()) { 108 | if ( 109 | socket.userData.name !== "" && 110 | socket.userData.avatarSkin !== "" 111 | ) { 112 | playerData.push({ 113 | id: socket.id, 114 | name: socket.userData.name, 115 | position_x: socket.userData.position.x, 116 | position_y: socket.userData.position.y, 117 | position_z: socket.userData.position.z, 118 | quaternion_x: socket.userData.quaternion.x, 119 | quaternion_y: socket.userData.quaternion.y, 120 | quaternion_z: socket.userData.quaternion.z, 121 | quaternion_w: socket.userData.quaternion.w, 122 | animation: socket.userData.animation, 123 | avatarSkin: socket.userData.avatarSkin, 124 | }); 125 | } 126 | } 127 | 128 | if (socket.userData.name === "" || socket.userData.avatarSkin === "") { 129 | return; 130 | } else { 131 | updateNameSpace.emit("playerData", playerData); 132 | } 133 | }, 20); 134 | }); 135 | 136 | server.listen(port, () => { 137 | console.log(`Server listening on port ${port}`); 138 | }); 139 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | 3 | export default defineConfig({ 4 | root: "frontend", 5 | publicDir: "../public", 6 | build: { 7 | outDir: "../dist", 8 | emptyOutDir: true, 9 | chunkSizeWarningLimit: 1600, 10 | rollupOptions: { 11 | output: { 12 | manualChunks(id) { 13 | if (id.includes("node_modules")) { 14 | return id 15 | .toString() 16 | .split("node_modules/")[1] 17 | .split("/")[0] 18 | .toString(); 19 | } 20 | }, 21 | }, 22 | }, 23 | }, 24 | optimizeDeps: { 25 | include: ["socket.io-client"], 26 | }, 27 | 28 | // server: { 29 | // proxy: { 30 | // "/foo": "http://localhost:3000", 31 | // "/api": { 32 | // target: "https://localhost:3000", 33 | // changeOrigin: true, 34 | // secure: false, 35 | // ws: true, 36 | // }, 37 | // }, 38 | // }, 39 | }); 40 | --------------------------------------------------------------------------------