├── .gitignore ├── LICENSE ├── README.md ├── assets ├── angel.basis ├── angel.min.glb ├── ballfx.basis ├── beamfx.png ├── blender │ ├── angel.blend │ ├── door.blend │ ├── generic_controller.blend │ ├── hall.blend │ ├── hall_empty.blend │ ├── hall_for_hubs.blend │ ├── sound.blend │ ├── spray.blend │ ├── teleport.blend │ └── vertigo.blend ├── checkboard.basis ├── clouds.basis ├── controller.basis ├── doorfx.basis ├── flare.jpg ├── foxr.png ├── generic_controller.glb ├── glow.basis ├── grid.png ├── haldezollern.basis ├── haldezollern_small.basis ├── hall.glb ├── hall_variants │ └── hall_empty.glb ├── lakebyllesby.basis ├── lakebyllesby_small.basis ├── lightmap.jpg ├── loadingbg.jpg ├── mozillamr.basis ├── newsticker.jpg ├── ogg │ ├── bells.ogg │ ├── birds.ogg │ ├── chopin.ogg │ ├── cowbell.ogg │ ├── forest.ogg │ ├── guiro.ogg │ ├── horn.ogg │ ├── mandolin.ogg │ ├── motorhorn.ogg │ ├── spray.ogg │ ├── squeaker.ogg │ ├── surdo.ogg │ ├── teleport_a.ogg │ ├── teleport_b.ogg │ ├── train.ogg │ ├── trumpet.ogg │ ├── whistle.ogg │ ├── wind.ogg │ ├── xylophone1.ogg │ ├── xylophone10.ogg │ ├── xylophone11.ogg │ ├── xylophone12.ogg │ ├── xylophone13.ogg │ ├── xylophone2.ogg │ ├── xylophone3.ogg │ ├── xylophone4.ogg │ ├── xylophone5.ogg │ ├── xylophone6.ogg │ ├── xylophone7.ogg │ ├── xylophone8.ogg │ └── xylophone9.ogg ├── paintings │ ├── bosch.basis │ ├── degas.basis │ ├── rembrandt.basis │ ├── seurat.basis │ └── sorolla.basis ├── panel.basis ├── pg_bg.jpg ├── pg_door_lm.jpg ├── pg_floor_lm.jpg ├── sky.png ├── sound.glb ├── sound_door.glb ├── sound_door_lm.jpg ├── sound_shadow.png ├── spray.basis ├── spray.glb ├── spray_brush.png ├── sshot.jpg ├── stereopanoL.basis ├── stereopanoR.basis ├── stereopano_small.basis ├── teleport.glb ├── thuringen.basis ├── thuringen_small.basis ├── tigerturtle.basis ├── tigerturtle_small.basis ├── travertine2.basis ├── tweets.json ├── vertigo.glb ├── vertigo_door_lm.jpg ├── vertigo_lm.basis ├── zapporthorn.basis ├── zapporthorn_small.basis └── zoomicon.png ├── bundle.js ├── bundle.js.map ├── index.html ├── package-lock.json ├── package.json ├── packshaders.py ├── res ├── apple-touch-icon-114x114.png ├── apple-touch-icon-120x120.png ├── apple-touch-icon-144x144.png ├── apple-touch-icon-152x152.png ├── apple-touch-icon-57x57.png ├── apple-touch-icon-72x72.png ├── big_thumbnail.jpg ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico └── mstile-144x144.png ├── src ├── assets.js ├── components │ └── index.js ├── index.js ├── lib │ ├── ColorUtils.js │ ├── ColorWheel.js │ ├── EventDispatcher.js │ ├── PositionalAudioPolyphonic.js │ ├── RayControl.js │ ├── Teleport.js │ ├── VRButton.js │ ├── assetManager.js │ ├── shaders.js │ ├── slideshow.js │ └── utils.js ├── rooms │ ├── Hall.js │ ├── Panorama.js │ ├── PanoramaStereo.js │ ├── PhotogrammetryObject.js │ ├── Sound.js │ └── Vertigo.js ├── shaders │ ├── basic.vert.glsl │ ├── beam.frag.glsl │ ├── door.frag.glsl │ ├── panoball.frag.glsl │ ├── panoball.vert.glsl │ └── zoom.frag.glsl ├── stations │ ├── Graffiti.js │ ├── InfoPanels.js │ ├── InfoPanelsData.js │ ├── NewsTicker.js │ ├── Paintings.js │ ├── PanoBalls.js │ └── Xylophone.js ├── systems │ ├── AreaCheckerSystem.js │ ├── BillboardSystem.js │ ├── ControllersSystem.js │ ├── DebugHelperSystem.js │ ├── HierarchySystem.js │ ├── SDFTextSystem.js │ ├── SystemsGroup.js │ └── TransformSystem.js └── vendor │ ├── basis_transcoder.js │ ├── basis_transcoder.wasm │ ├── draco_decoder.js │ ├── draco_decoder.wasm │ └── draco_wasm_wrapper.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | *.pem 3 | logs 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | # bundle.* 9 | 10 | *DS_Store 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | 24 | # nyc test coverage 25 | .nyc_output 26 | 27 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 28 | .grunt 29 | 30 | # Bower dependency directory (https://bower.io/) 31 | bower_components 32 | 33 | # node-waf configuration 34 | .lock-wscript 35 | 36 | # Compiled binary addons (https://nodejs.org/api/addons.html) 37 | build/Release 38 | 39 | # Dependency directories 40 | node_modules/ 41 | jspm_packages/ 42 | 43 | # TypeScript v1 declaration files 44 | typings/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | 64 | # next.js build output 65 | .next 66 | 67 | #bundle.js 68 | #bundle.js.map 69 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Mozilla Mixed Reality 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hello WebXR! 2 | 3 | ![screenshot](assets/sshot.jpg) 4 | 5 | [Try it here!](https://mixedreality.mozilla.org/hello-webxr/index.html) 6 | 7 | This is a WebXR demo made to celebrate the [WebXR spec](https://immersive-web.github.io/webxr/) release at the end of 2019. It showcases several small experiences, perfect to test different kind of interactions and situations in Virtual Reality. For newcomers, it's a nice entry point to the medium, and web developers may find many things they can reuse and learn (more info on the [launch article](https://blog.mozvr.com/hello-webxr)). 8 | 9 | ## How to build 10 | 11 | 1. `npm install` 12 | 2. `npm start` 13 | 3. Open `http://localhost:8080` 14 | 15 | 16 | ### Shader packing 17 | 18 | If you make changes to the shaders you'll need to repack them. To keep things simple, we made a simple script `packshaders.py`: 19 | 20 | `python packshaders.py [seconds]` 21 | 22 | where `seconds` is an optional parameter (defaults to 5) to define how many seconds to wait until next rebuild (doesn't watch file changes) 23 | 24 | 25 | ## Third party content 26 | 27 | * Photogrammetry model by Geoffrey Marchal ([Sketchfab](https://sketchfab.com/3d-models/baptismal-angel-kneeling-f45f01c63e514d3bad846e82af640f33)) 28 | * 360 Panoramas from [Wikimedia Commons](https://commons.wikimedia.org/wiki/Main_Page): 29 | * [Halde Zollern](https://commons.wikimedia.org/wiki/File:Halde_Zollern_Panorama_01.jpg) 30 | * [Lake Byllesby Regional Park](https://commons.wikimedia.org/wiki/File:Lake_Byllesby_Regional_Park_-_360%C2%B0_Equirectangular_Street_View_Photo_(27332591527).jpg) 31 | * [Kloster Paulinzella, Thüringen](https://commons.wikimedia.org/wiki/File:Kloster_Paulinzella,_Th%C3%BCringen,_360x180,_170316,_ako_(1).jpg) 32 | * [Tiger And Turtle, Duisburg](https://commons.wikimedia.org/wiki/File:Tiger_And_Turtle_Panorama.jpg) 33 | * [Zapporthorn, Switzerland](https://commons.wikimedia.org/wiki/File:Zapporthorn_Spherical_Panorama.jpg) 34 | * [Naturalis Biodiversity Center](https://commons.wikimedia.org/wiki/File:Naturalis_Biodiversity_Center_-_Museum_-_Exhibition_Primeval_parade_33_-_Overview_room_with_skeletons_-_Panorama_360_3D.jpg) 35 | 36 | * Classical Paintings: 37 | * [The Garden of Earthly Delights, Hieronymus Bosch](https://commons.wikimedia.org/wiki/File:The_Garden_of_Earthly_Delights_by_Bosch_High_Resolution.jpg) 38 | * [Self-Portrait, Rembrandt van Rijn](https://www.nga.gov/collection/art-object-page.79.html) 39 | * [The Dance Lesson, Edgar Degas](https://www.nga.gov/collection/art-object-page.93045.html) 40 | * [Gray Weather, Grande Jatte, Georges Seurat](https://commons.wikimedia.org/wiki/File:Seurat.jatte.jpg) 41 | * [The Pink Robe, Joaquin Sorolla](http://sorollapaintings.com/images/sorolla-pink-robe-b-3928.jpg) 42 | 43 | 44 | * Public Domain sounds from [freesound.org](https://freesound.org) 45 | -------------------------------------------------------------------------------- /assets/angel.basis: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/angel.basis -------------------------------------------------------------------------------- /assets/angel.min.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/angel.min.glb -------------------------------------------------------------------------------- /assets/ballfx.basis: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/ballfx.basis -------------------------------------------------------------------------------- /assets/beamfx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/beamfx.png -------------------------------------------------------------------------------- /assets/blender/angel.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/blender/angel.blend -------------------------------------------------------------------------------- /assets/blender/door.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/blender/door.blend -------------------------------------------------------------------------------- /assets/blender/generic_controller.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/blender/generic_controller.blend -------------------------------------------------------------------------------- /assets/blender/hall.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/blender/hall.blend -------------------------------------------------------------------------------- /assets/blender/hall_empty.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/blender/hall_empty.blend -------------------------------------------------------------------------------- /assets/blender/hall_for_hubs.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/blender/hall_for_hubs.blend -------------------------------------------------------------------------------- /assets/blender/sound.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/blender/sound.blend -------------------------------------------------------------------------------- /assets/blender/spray.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/blender/spray.blend -------------------------------------------------------------------------------- /assets/blender/teleport.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/blender/teleport.blend -------------------------------------------------------------------------------- /assets/blender/vertigo.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/blender/vertigo.blend -------------------------------------------------------------------------------- /assets/checkboard.basis: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/checkboard.basis -------------------------------------------------------------------------------- /assets/clouds.basis: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/clouds.basis -------------------------------------------------------------------------------- /assets/controller.basis: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/controller.basis -------------------------------------------------------------------------------- /assets/doorfx.basis: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/doorfx.basis -------------------------------------------------------------------------------- /assets/flare.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/flare.jpg -------------------------------------------------------------------------------- /assets/foxr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/foxr.png -------------------------------------------------------------------------------- /assets/generic_controller.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/generic_controller.glb -------------------------------------------------------------------------------- /assets/glow.basis: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/glow.basis -------------------------------------------------------------------------------- /assets/grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/grid.png -------------------------------------------------------------------------------- /assets/haldezollern.basis: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/haldezollern.basis -------------------------------------------------------------------------------- /assets/haldezollern_small.basis: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/haldezollern_small.basis -------------------------------------------------------------------------------- /assets/hall.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/hall.glb -------------------------------------------------------------------------------- /assets/hall_variants/hall_empty.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/hall_variants/hall_empty.glb -------------------------------------------------------------------------------- /assets/lakebyllesby.basis: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/lakebyllesby.basis -------------------------------------------------------------------------------- /assets/lakebyllesby_small.basis: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/lakebyllesby_small.basis -------------------------------------------------------------------------------- /assets/lightmap.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/lightmap.jpg -------------------------------------------------------------------------------- /assets/loadingbg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/loadingbg.jpg -------------------------------------------------------------------------------- /assets/mozillamr.basis: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/mozillamr.basis -------------------------------------------------------------------------------- /assets/newsticker.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/newsticker.jpg -------------------------------------------------------------------------------- /assets/ogg/bells.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/ogg/bells.ogg -------------------------------------------------------------------------------- /assets/ogg/birds.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/ogg/birds.ogg -------------------------------------------------------------------------------- /assets/ogg/chopin.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/ogg/chopin.ogg -------------------------------------------------------------------------------- /assets/ogg/cowbell.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/ogg/cowbell.ogg -------------------------------------------------------------------------------- /assets/ogg/forest.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/ogg/forest.ogg -------------------------------------------------------------------------------- /assets/ogg/guiro.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/ogg/guiro.ogg -------------------------------------------------------------------------------- /assets/ogg/horn.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/ogg/horn.ogg -------------------------------------------------------------------------------- /assets/ogg/mandolin.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/ogg/mandolin.ogg -------------------------------------------------------------------------------- /assets/ogg/motorhorn.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/ogg/motorhorn.ogg -------------------------------------------------------------------------------- /assets/ogg/spray.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/ogg/spray.ogg -------------------------------------------------------------------------------- /assets/ogg/squeaker.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/ogg/squeaker.ogg -------------------------------------------------------------------------------- /assets/ogg/surdo.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/ogg/surdo.ogg -------------------------------------------------------------------------------- /assets/ogg/teleport_a.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/ogg/teleport_a.ogg -------------------------------------------------------------------------------- /assets/ogg/teleport_b.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/ogg/teleport_b.ogg -------------------------------------------------------------------------------- /assets/ogg/train.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/ogg/train.ogg -------------------------------------------------------------------------------- /assets/ogg/trumpet.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/ogg/trumpet.ogg -------------------------------------------------------------------------------- /assets/ogg/whistle.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/ogg/whistle.ogg -------------------------------------------------------------------------------- /assets/ogg/wind.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/ogg/wind.ogg -------------------------------------------------------------------------------- /assets/ogg/xylophone1.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/ogg/xylophone1.ogg -------------------------------------------------------------------------------- /assets/ogg/xylophone10.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/ogg/xylophone10.ogg -------------------------------------------------------------------------------- /assets/ogg/xylophone11.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/ogg/xylophone11.ogg -------------------------------------------------------------------------------- /assets/ogg/xylophone12.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/ogg/xylophone12.ogg -------------------------------------------------------------------------------- /assets/ogg/xylophone13.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/ogg/xylophone13.ogg -------------------------------------------------------------------------------- /assets/ogg/xylophone2.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/ogg/xylophone2.ogg -------------------------------------------------------------------------------- /assets/ogg/xylophone3.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/ogg/xylophone3.ogg -------------------------------------------------------------------------------- /assets/ogg/xylophone4.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/ogg/xylophone4.ogg -------------------------------------------------------------------------------- /assets/ogg/xylophone5.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/ogg/xylophone5.ogg -------------------------------------------------------------------------------- /assets/ogg/xylophone6.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/ogg/xylophone6.ogg -------------------------------------------------------------------------------- /assets/ogg/xylophone7.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/ogg/xylophone7.ogg -------------------------------------------------------------------------------- /assets/ogg/xylophone8.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/ogg/xylophone8.ogg -------------------------------------------------------------------------------- /assets/ogg/xylophone9.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/ogg/xylophone9.ogg -------------------------------------------------------------------------------- /assets/paintings/bosch.basis: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/paintings/bosch.basis -------------------------------------------------------------------------------- /assets/paintings/degas.basis: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/paintings/degas.basis -------------------------------------------------------------------------------- /assets/paintings/rembrandt.basis: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/paintings/rembrandt.basis -------------------------------------------------------------------------------- /assets/paintings/seurat.basis: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/paintings/seurat.basis -------------------------------------------------------------------------------- /assets/paintings/sorolla.basis: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/paintings/sorolla.basis -------------------------------------------------------------------------------- /assets/panel.basis: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/panel.basis -------------------------------------------------------------------------------- /assets/pg_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/pg_bg.jpg -------------------------------------------------------------------------------- /assets/pg_door_lm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/pg_door_lm.jpg -------------------------------------------------------------------------------- /assets/pg_floor_lm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/pg_floor_lm.jpg -------------------------------------------------------------------------------- /assets/sky.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/sky.png -------------------------------------------------------------------------------- /assets/sound.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/sound.glb -------------------------------------------------------------------------------- /assets/sound_door.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/sound_door.glb -------------------------------------------------------------------------------- /assets/sound_door_lm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/sound_door_lm.jpg -------------------------------------------------------------------------------- /assets/sound_shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/sound_shadow.png -------------------------------------------------------------------------------- /assets/spray.basis: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/spray.basis -------------------------------------------------------------------------------- /assets/spray.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/spray.glb -------------------------------------------------------------------------------- /assets/spray_brush.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/spray_brush.png -------------------------------------------------------------------------------- /assets/sshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/sshot.jpg -------------------------------------------------------------------------------- /assets/stereopanoL.basis: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/stereopanoL.basis -------------------------------------------------------------------------------- /assets/stereopanoR.basis: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/stereopanoR.basis -------------------------------------------------------------------------------- /assets/stereopano_small.basis: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/stereopano_small.basis -------------------------------------------------------------------------------- /assets/teleport.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/teleport.glb -------------------------------------------------------------------------------- /assets/thuringen.basis: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/thuringen.basis -------------------------------------------------------------------------------- /assets/thuringen_small.basis: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/thuringen_small.basis -------------------------------------------------------------------------------- /assets/tigerturtle.basis: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/tigerturtle.basis -------------------------------------------------------------------------------- /assets/tigerturtle_small.basis: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/tigerturtle_small.basis -------------------------------------------------------------------------------- /assets/travertine2.basis: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/travertine2.basis -------------------------------------------------------------------------------- /assets/tweets.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "author": "@mozillareality", 4 | "message": "Introducing Hello WebXR! - A #webxr experience for VR headsets for all audiences. Have fun!. #hellowebxr https://tinyurl.com/hello-webxr" 5 | }, 6 | { 7 | "author": "@hovercruft", 8 | "message": "Painted a thing! #hellowebxr #berlin" 9 | }, 10 | { 11 | "author": "@el_cayumry", 12 | "message": "@mozillareality is nice to just let yourself drift into the metaverse.. #hellowebxr" 13 | }, 14 | { 15 | "author": "@fernandojsg", 16 | "message": "Our ECSY library was really handy for our new #hellowebxr demo ^_^" 17 | }, 18 | { 19 | "author": "@JohanGuy", 20 | "message": "I didn't expect #hellowebxr experience at all. Not sure what to think..." 21 | }, 22 | { 23 | "author": "@Fancypansy", 24 | "message": "Weeee!! ^_^! hellowebxr" 25 | }, 26 | { 27 | "author": "@Foxr", 28 | "message": "Hey, look mum! I'm in #hellowebxr =:o)" 29 | }, 30 | { 31 | "author": "@humphreymatel", 32 | "message": "Trying the Hello WebXR experience on my #oculusquest. So far so good! #hellowebxr" 33 | }, 34 | { 35 | "author": "@GavMov34", 36 | "message": "I'm amazed by WebXR technology! kudos to @mozillareality #hellowebxr https://mixedreality.mozilla.org" 37 | }, 38 | { 39 | "author": "@samo_la_utma", 40 | "message": "#hellowebxr Na ou taumafai lava i le demo o Mozilla Hello webXR! E le leaga!" 41 | }, 42 | { 43 | "author": "@frederic", 44 | "message": "Go try #hellowebxr demo! Not a thrill but kinda nice :)" 45 | }, 46 | { 47 | "author": "@jennymarvel", 48 | "message": "If this is the metaverse, it's not for me :( #hellowebxr" 49 | }, 50 | { 51 | "author": "@ovvli", 52 | "message": "WOAH. Great job guys! Got lost initially a bit but finally made it :_) #hellowebxr" 53 | }, 54 | { 55 | "author": "@feiss", 56 | "message": "Hope everyone tries and enjoys our #hellowebxr experience! :)" 57 | }, 58 | { 59 | "author": "@mountain_news", 60 | "message": "Hello WebXR! is a new VR experience by Mozilla Mixed Reality. Try it at https://mixedreality.mozilla.org #hellowebxr" 61 | }, 62 | { 63 | "author": "@JArco", 64 | "message": "Acabo de probar la demo de Mozilla Hello webXR!. Nostamal! :) #hellowebxr" 65 | }, 66 | { 67 | "author": "@goroshart", 68 | "message": "#hellowebxr it's quite nice, tried the grafitti thing. Where's the undo??" 69 | }, 70 | { 71 | "author": "@Mr R", 72 | "message": "The Mozilla MR guys released a new demo #hellowebxr" 73 | }, 74 | { 75 | "author": "@Ocklando", 76 | "message": "Ta chula la demo de #hellowebxr .. Un poco corta, eso sí, pero potita :)" 77 | } 78 | ] 79 | -------------------------------------------------------------------------------- /assets/vertigo.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/vertigo.glb -------------------------------------------------------------------------------- /assets/vertigo_door_lm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/vertigo_door_lm.jpg -------------------------------------------------------------------------------- /assets/vertigo_lm.basis: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/vertigo_lm.basis -------------------------------------------------------------------------------- /assets/zapporthorn.basis: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/zapporthorn.basis -------------------------------------------------------------------------------- /assets/zapporthorn_small.basis: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/zapporthorn_small.basis -------------------------------------------------------------------------------- /assets/zoomicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/assets/zoomicon.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | Hello WebXR! - Mozilla Mixed Reality 51 | 52 | 159 | 160 | 161 | 162 | 173 | 174 | 175 | 176 |
177 |
178 |

Hello!

179 | 180 | 182 | 183 |
184 |
185 | 186 | 189 | 190 | 191 |
192 | 193 | Handedness: 194 | Left 195 | Right 196 | 197 |   198 | 199 | What is this about? 200 | 201 | 202 | 203 | 204 | 205 | 206 |
207 | 208 | 209 | 210 | 218 | 219 | 220 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-webxr", 3 | "version": "1.0.0", 4 | "description": "A showcase of different WebXR experiences and interaction examples", 5 | "private": true, 6 | "scripts": { 7 | "start": "webpack-dev-server -d --https --host 0.0.0.0", 8 | "build": "webpack -p", 9 | "watch": "webpack --watch", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/MozillaReality/hello-webxr.git" 15 | }, 16 | "keywords": [ 17 | "webxr", 18 | "webvr", 19 | "vr", 20 | "ecsy", 21 | "xr", 22 | "mozilla", 23 | "showcase", 24 | "museum" 25 | ], 26 | "author": "", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/MozillaReality/hello-webxr/issues" 30 | }, 31 | "homepage": "https://github.com/MozillaReality/hello-webxr#readme", 32 | "dependencies": { 33 | "ecsy": "0.2.6", 34 | "three": "^0.112.0", 35 | "three-obj-loader": "^1.1.3", 36 | "three-pointerlock": "0.0.2", 37 | "troika-3d-text": "^0.16", 38 | "webpack-dev-server": "^3.10.1", 39 | "webxr-polyfill": "^2.0.1" 40 | }, 41 | "devDependencies": { 42 | "@babel/core": "^7.6.4", 43 | "babel-loader": "^8.0.6", 44 | "webpack": "^4.41.0", 45 | "webpack-cli": "^3.3.9" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packshaders.py: -------------------------------------------------------------------------------- 1 | import glob, os.path, time, sys 2 | 3 | 4 | def pack(): 5 | output = open('./src/lib/shaders.js', 'wt') 6 | files = glob.glob('./src/shaders/*.glsl') 7 | 8 | output.write('export var shaders = {\n') 9 | 10 | for file in files: 11 | f = open(file, 'rt') 12 | shader = f.read() 13 | f.close() 14 | var = '_'.join(os.path.basename(file).split('.')[:-1]) 15 | output.write(var + ' : `\n') 16 | output.write(shader.strip()) 17 | output.write('\n`,\n\n') 18 | 19 | output.write('};') 20 | output.close() 21 | print('packed') 22 | 23 | print('PACK SHADERS') 24 | while True: 25 | pack() 26 | time.sleep(int(sys.argv[1] if len(sys.argv) > 1 else 5)) 27 | 28 | -------------------------------------------------------------------------------- /res/apple-touch-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/res/apple-touch-icon-114x114.png -------------------------------------------------------------------------------- /res/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/res/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /res/apple-touch-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/res/apple-touch-icon-144x144.png -------------------------------------------------------------------------------- /res/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/res/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /res/apple-touch-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/res/apple-touch-icon-57x57.png -------------------------------------------------------------------------------- /res/apple-touch-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/res/apple-touch-icon-72x72.png -------------------------------------------------------------------------------- /res/big_thumbnail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/res/big_thumbnail.jpg -------------------------------------------------------------------------------- /res/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/res/favicon-16x16.png -------------------------------------------------------------------------------- /res/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/res/favicon-32x32.png -------------------------------------------------------------------------------- /res/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/res/favicon.ico -------------------------------------------------------------------------------- /res/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/res/mstile-144x144.png -------------------------------------------------------------------------------- /src/assets.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | export default { 4 | // hall 5 | foxr_tex: { url: 'foxr.png', options: { encoding: THREE.sRGBEncoding, flipY: false} }, 6 | hall_model: { url: 'hall.glb' }, 7 | generic_controller_model: { url: 'generic_controller.glb' }, 8 | lightmap_tex: { url: 'lightmap.jpg', options: { encoding: THREE.sRGBEncoding, flipY: false} }, 9 | controller_tex: { url: 'controller.basis' }, 10 | doorfx_tex: { url: 'doorfx.basis', options: { wrapT: THREE.RepeatWrapping, wrapS: THREE.RepeatWrapping }}, 11 | sky_tex: { url: 'sky.png', options: { encoding: THREE.sRGBEncoding, flipY: false} }, 12 | clouds_tex: { url: 'clouds.basis', options: { encoding: THREE.sRGBEncoding, flipY: false} }, 13 | teleport_model: { url: 'teleport.glb' }, 14 | beam_tex: { url: 'beamfx.png' }, 15 | glow_tex: { url: 'glow.basis', options: { encoding: THREE.sRGBEncoding} }, 16 | newsticker_tex: { url: 'newsticker.jpg', options: { encoding: THREE.sRGBEncoding, flipY: false} }, 17 | mozillamr_tex: { url: 'mozillamr.basis', options: { encoding: THREE.sRGBEncoding, flipY: false} }, 18 | zoomicon_tex: { url: 'zoomicon.png', options: { encoding: THREE.sRGBEncoding } }, 19 | 20 | // panoramas 21 | panoballfx_tex: { url: 'ballfx.basis', options: { wrapT: THREE.RepeatWrapping, wrapS: THREE.RepeatWrapping } }, 22 | 23 | stereopanoL: { url: 'stereopanoL.basis', options: { encoding: THREE.sRGBEncoding }}, 24 | stereopanoR: { url: 'stereopanoR.basis', options: { encoding: THREE.sRGBEncoding }}, 25 | pano1small: { url: 'stereopano_small.basis', options: {encoding: THREE.sRGBEncoding} }, 26 | 27 | pano2: { url: 'tigerturtle.basis', options: { encoding: THREE.sRGBEncoding, flipY: false} }, 28 | pano3: { url: 'lakebyllesby.basis', options: { encoding: THREE.sRGBEncoding, flipY: false} }, 29 | pano4: { url: 'haldezollern.basis', options: { encoding: THREE.sRGBEncoding, flipY: false} }, 30 | pano5: { url: 'zapporthorn.basis', options: { encoding: THREE.sRGBEncoding, flipY: false} }, 31 | pano6: { url: 'thuringen.basis', options: { encoding: THREE.sRGBEncoding, flipY: false} }, 32 | pano2small: { url: 'tigerturtle_small.basis', options: {encoding: THREE.sRGBEncoding} }, 33 | pano3small: { url: 'lakebyllesby_small.basis', options: {encoding: THREE.sRGBEncoding} }, 34 | pano4small: { url: 'haldezollern_small.basis', options: {encoding: THREE.sRGBEncoding} }, 35 | pano5small: { url: 'zapporthorn_small.basis', options: {encoding: THREE.sRGBEncoding} }, 36 | pano6small: { url: 'thuringen_small.basis', options: {encoding: THREE.sRGBEncoding} }, 37 | 38 | // graffiti 39 | spray_model: { url: 'spray.glb' }, 40 | spray_tex: { url: 'spray.basis', options: { encoding: THREE.sRGBEncoding, flipY: false} }, 41 | 42 | // vertigo 43 | vertigo_model: { url: 'vertigo.glb' }, 44 | vertigo_door_lm_tex: { url: 'vertigo_door_lm.jpg', options: { encoding: THREE.sRGBEncoding, flipY: false} }, 45 | vertigo_lm_tex: { url: 'vertigo_lm.basis', options: { encoding: THREE.sRGBEncoding, flipY: false} }, 46 | checkboard_tex: { url: 'checkboard.basis', options: { wrapT: THREE.RepeatWrapping, wrapS: THREE.RepeatWrapping, repeat: [4, 4] } }, 47 | 48 | // sound 49 | sound_model: { url: 'sound.glb' }, 50 | sound_door_model: { url: 'sound_door.glb' }, 51 | sound_shadow_tex: { url: 'sound_shadow.png' }, 52 | sound_door_lm_tex: { url: 'sound_door_lm.jpg', options: { wrapT: THREE.RepeatWrapping, wrapS: THREE.RepeatWrapping} }, 53 | grid_tex: { url: 'grid.png', options: { wrapT: THREE.RepeatWrapping, wrapS: THREE.RepeatWrapping, repeat: [20, 20] } }, 54 | 55 | // photogrammetry object 56 | pg_floor_tex: { url: 'travertine2.basis', options: { encoding: THREE.sRGBEncoding, flipY: false, wrapT: THREE.RepeatWrapping, wrapS: THREE.RepeatWrapping} }, 57 | pg_floor_lm_tex: { url: 'pg_floor_lm.jpg', options: { encoding: THREE.sRGBEncoding, flipY: false} }, 58 | pg_door_lm_tex: { url: 'pg_door_lm.jpg', options: { encoding: THREE.sRGBEncoding, flipY: false} }, 59 | pg_object_tex: { url: 'angel.basis', options: { encoding: THREE.sRGBEncoding, flipY: false} }, 60 | pg_object_model: { url: 'angel.min.glb' }, // TODO: try draco version, angel.min.gl 61 | pg_bg_tex: { url: 'pg_bg.jpg', options: { encoding: THREE.sRGBEncoding, flipY: false} }, 62 | pg_flare_tex: { url: 'flare.jpg', options: { encoding: THREE.sRGBEncoding, flipY: false} }, 63 | pg_panel_tex: { url: 'panel.basis', options: { encoding: THREE.sRGBEncoding, flipY: false} }, 64 | 65 | // paintings 66 | painting_seurat_tex: { url: 'paintings/seurat.basis', options: { encoding: THREE.sRGBEncoding, flipY: false} }, 67 | painting_sorolla_tex: { url: 'paintings/sorolla.basis', options: { encoding: THREE.sRGBEncoding, flipY: false} }, 68 | painting_bosch_tex: { url: 'paintings/bosch.basis', options: { encoding: THREE.sRGBEncoding, flipY: false} }, 69 | painting_degas_tex: { url: 'paintings/degas.basis', options: { encoding: THREE.sRGBEncoding, flipY: false} }, 70 | painting_rembrandt_tex: { url: 'paintings/rembrandt.basis', options: { encoding: THREE.sRGBEncoding, flipY: false} }, 71 | 72 | // sounds 73 | birds_snd: { url: 'ogg/birds.ogg' }, 74 | chopin_snd: { url: 'ogg/chopin.ogg' }, 75 | forest_snd: { url: 'ogg/forest.ogg' }, 76 | wind_snd: { url: 'ogg/wind.ogg' }, 77 | teleport_a_snd: { url: 'ogg/teleport_a.ogg' }, 78 | teleport_b_snd: { url: 'ogg/teleport_b.ogg' } 79 | }; 80 | 81 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import {TagComponent} from 'ecsy'; 3 | 4 | export class Object3D { 5 | constructor() { 6 | this.value = null; 7 | } 8 | 9 | reset() { 10 | this.value = null; 11 | } 12 | } 13 | 14 | export class Rotation { 15 | constructor() { 16 | this.rotation = new THREE.Vector3(); 17 | } 18 | 19 | reset() {} 20 | } 21 | 22 | export class Position { 23 | constructor() { 24 | this.position = new THREE.Vector3(); 25 | } 26 | 27 | reset() {} 28 | } 29 | 30 | export class ParentObject3D { 31 | constructor() { 32 | this.value = null; 33 | } 34 | 35 | reset() { 36 | this.value = null; 37 | } 38 | } 39 | 40 | export class Text { 41 | constructor() { 42 | this.text = ''; 43 | this.textAlign = 'left'; // ['left', 'right', 'center'] 44 | this.anchor = 'center'; // ['left', 'right', 'center', 'align'] 45 | this.baseline = 'center'; // ['top', 'center', 'bottom'] 46 | this.color = '#FFF'; 47 | this.font = 'https://code.cdn.mozilla.net/fonts/ttf/ZillaSlab-SemiBold.ttf'; 48 | this.fontSize = 0.2; 49 | this.letterSpacing = 0; 50 | this.lineHeight = 0; 51 | this.maxWidth = Infinity; 52 | this.overflowWrap = 'normal'; // ['normal', 'break-word'] 53 | this.whiteSpace = 'normal'; // ['normal', 'nowrap'] 54 | this.opacity = 1; 55 | } 56 | 57 | reset() { 58 | this.text = ''; 59 | } 60 | } 61 | 62 | export class BoundingBox { 63 | constructor() { 64 | this.min = new THREE.Vector3(); 65 | this.max = new THREE.Vector3(); 66 | // this.box3? 67 | } 68 | 69 | reset() { 70 | this.min.set(0,0,0); 71 | this.max.set(0,0,0); 72 | } 73 | } 74 | 75 | export class BoundingSphere { 76 | constructor() { 77 | this.debug = true; 78 | this.center = new THREE.Vector3(); 79 | this.radius = 0; 80 | //this.sphere? 81 | } 82 | 83 | reset() { 84 | this.center.set(0,0,0); 85 | this.radius = 0; 86 | } 87 | } 88 | 89 | export class Area { 90 | constructor() { 91 | 92 | } 93 | 94 | reset() { 95 | 96 | } 97 | } 98 | 99 | export class AreaEntering extends TagComponent {} 100 | export class AreaExiting extends TagComponent {} 101 | export class AreaInside extends TagComponent {} 102 | 103 | export class AreaChecker { 104 | constructor() { 105 | 106 | } 107 | 108 | reset() { 109 | 110 | } 111 | } 112 | 113 | const empty = () => {}; 114 | 115 | export class AreaReactor { 116 | constructor() { 117 | this.reset(); 118 | } 119 | 120 | reset() { 121 | this.onEntering = empty; 122 | this.onExiting = empty; 123 | } 124 | } 125 | 126 | export class DebugHelper extends TagComponent {} 127 | 128 | export class Billboard { 129 | constructor() { 130 | this.camera3D = null; 131 | } 132 | 133 | reset() { 134 | this.camera3D = null; 135 | } 136 | } 137 | 138 | export class Children { 139 | constructor() { 140 | this.value = []; 141 | } 142 | 143 | reset() { 144 | this.value.array.length = 0; 145 | } 146 | } 147 | 148 | export class Opacity { 149 | constructor() { 150 | this.opacity = 0; 151 | } 152 | 153 | reset() { 154 | this.opacity = 0; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { PointerLockControls } from 'three/examples/jsm/controls/PointerLockControls.js'; 3 | 4 | import {VRButton} from './lib/VRButton.js'; 5 | import {slideshow} from './lib/slideshow.js'; 6 | import {loadAssets} from './lib/assetManager.js'; 7 | 8 | // ECSY 9 | import { World } from 'ecsy'; 10 | import { SDFTextSystem } from './systems/SDFTextSystem.js'; 11 | import { DebugHelperSystem } from './systems/DebugHelperSystem.js'; 12 | import { AreaCheckerSystem } from './systems/AreaCheckerSystem.js'; 13 | import { ControllersSystem } from './systems/ControllersSystem.js'; 14 | import HierarchySystem from './systems/HierarchySystem.js'; 15 | import TransformSystem from './systems/TransformSystem.js'; 16 | import BillboardSystem from './systems/BillboardSystem.js'; 17 | 18 | import SystemsGroup from './systems/SystemsGroup.js'; 19 | 20 | import assets from './assets.js'; 21 | 22 | import { Text, Object3D, AreaChecker } from './components/index.js'; 23 | 24 | import RayControl from './lib/RayControl.js'; 25 | import Teleport from './lib/Teleport.js'; 26 | 27 | import * as roomHall from './rooms/Hall.js'; 28 | import * as roomPanorama from './rooms/Panorama.js'; 29 | import * as roomPanoramaStereo from './rooms/PanoramaStereo.js'; 30 | import * as roomPhotogrammetryObject from './rooms/PhotogrammetryObject.js'; 31 | import * as roomVertigo from './rooms/Vertigo.js'; 32 | import * as roomSound from './rooms/Sound.js'; 33 | 34 | import {shaders} from './lib/shaders.js'; 35 | 36 | import WebXRPolyfill from 'webxr-polyfill'; 37 | const polyfill = new WebXRPolyfill(); 38 | 39 | var clock = new THREE.Clock(); 40 | 41 | var scene, parent, renderer, camera, controls, context = {}; 42 | var raycontrol, teleport, controllers = []; 43 | 44 | var listener, ambientMusic; 45 | 46 | var rooms = [ 47 | roomHall, 48 | roomSound, 49 | roomPhotogrammetryObject, 50 | roomVertigo, 51 | roomPanoramaStereo, 52 | roomPanorama, 53 | roomPanorama, 54 | roomPanorama, 55 | roomPanorama, 56 | roomPanorama, 57 | ]; 58 | 59 | const roomNames = [ 60 | 'hall', 61 | 'sound', 62 | 'photogrammetry', 63 | 'vertigo', 64 | 'panoramastereo', 65 | 'panorama1', 66 | 'panorama2', 67 | 'panorama3', 68 | 'panorama4', 69 | 'panorama5', 70 | ]; 71 | 72 | const musicThemes = [ 73 | false, 74 | false, 75 | 'chopin_snd', 76 | 'wind_snd', 77 | false, 78 | 'birds_snd', 79 | 'birds_snd', 80 | 'forest_snd', 81 | 'wind_snd', 82 | 'birds_snd', 83 | ]; 84 | 85 | const urlObject = new URL(window.location); 86 | const roomName = urlObject.searchParams.get('room'); 87 | context.room = roomNames.indexOf(roomName) !== -1 ? roomNames.indexOf(roomName) : 0; 88 | // console.log(`Current room "${roomNames[context.room]}", ${context.room}`); 89 | const debug = urlObject.searchParams.has('debug'); 90 | const handedness = urlObject.searchParams.has('handedness') ? urlObject.searchParams.get('handedness') : "right"; 91 | 92 | // Target positions when moving from one room to another 93 | const targetPositions = { 94 | hall: { 95 | sound: new THREE.Vector3(0, 0, 0), 96 | photogrammetry: new THREE.Vector3(1, 0, 0), 97 | vertigo: new THREE.Vector3(0, 0, 0) 98 | }, 99 | photogrammetry: { 100 | hall: new THREE.Vector3(-3.6, 0, 2.8) 101 | }, 102 | sound: { 103 | hall: new THREE.Vector3(4.4, 0, 4.8) 104 | }, 105 | vertigo: { 106 | hall: new THREE.Vector3(-1.8, 0, -5) 107 | } 108 | }; 109 | 110 | function gotoRoom(room) { 111 | rooms[context.room].exit(context); 112 | raycontrol.deactivateAll(); 113 | 114 | const prevRoom = roomNames[context.room]; 115 | const nextRoom = roomNames[room]; 116 | 117 | if (targetPositions[prevRoom] && targetPositions[prevRoom][nextRoom]) { 118 | let deltaPosition = new THREE.Vector3(); 119 | const targetPosition = targetPositions[prevRoom][nextRoom]; 120 | var camera = renderer.xr.getCamera(context.camera); 121 | 122 | deltaPosition.x = camera.position.x - targetPosition.x; 123 | deltaPosition.z = camera.position.z - targetPosition.z; 124 | 125 | context.cameraRig.position.sub(deltaPosition); 126 | } 127 | 128 | context.room = room; 129 | 130 | playMusic(room); 131 | 132 | rooms[context.room].enter(context); 133 | } 134 | 135 | function playMusic(room) { 136 | if (ambientMusic.source) ambientMusic.stop(); 137 | 138 | const music = musicThemes[room]; 139 | if (!music) { return; } 140 | ambientMusic.setBuffer(assets[music]); 141 | ambientMusic.setLoop(true); 142 | ambientMusic.setVolume(1.0); 143 | ambientMusic.offset = Math.random() * 60; 144 | ambientMusic.play(); 145 | } 146 | 147 | var ecsyWorld; 148 | var systemsGroup = {}; 149 | 150 | function detectWebXR() { 151 | if ('xr' in navigator) { 152 | navigator.xr.isSessionSupported('immersive-vr').then( supported => { 153 | if (!supported) document.getElementById('no-webxr').classList.remove('hidden'); 154 | } ); 155 | 156 | } else { 157 | document.getElementById('no-webxr').classList.remove('hidden'); 158 | } 159 | } 160 | 161 | export function init() { 162 | 163 | document.getElementById(handedness + 'hand').classList.add('activehand'); 164 | 165 | detectWebXR(); 166 | 167 | var w = 100; 168 | ecsyWorld = new World(); 169 | ecsyWorld 170 | .registerSystem(SDFTextSystem) 171 | .registerSystem(AreaCheckerSystem) 172 | .registerSystem(ControllersSystem) 173 | .registerSystem(DebugHelperSystem) 174 | .registerSystem(TransformSystem) 175 | .registerSystem(BillboardSystem) 176 | .registerSystem(HierarchySystem); 177 | 178 | systemsGroup['roomHall'] = new SystemsGroup(ecsyWorld, [ 179 | AreaCheckerSystem, ControllersSystem, DebugHelperSystem 180 | ]); 181 | 182 | renderer = new THREE.WebGLRenderer({antialias: true, logarithmicDepthBuffer: false}); 183 | renderer.gammaFactor = 2.2; 184 | renderer.outputEncoding = THREE.sRGBEncoding; 185 | renderer.setPixelRatio( window.devicePixelRatio ); 186 | renderer.setSize(window.innerWidth, window.innerHeight); 187 | renderer.xr.enabled = true; 188 | 189 | scene = new THREE.Scene(); 190 | camera = new THREE.PerspectiveCamera(80, window.innerWidth / window.innerHeight, 0.005, 10000); 191 | camera.position.set(0, 1.6, 0); 192 | listener = new THREE.AudioListener(); 193 | camera.add(listener); 194 | 195 | ambientMusic = new THREE.Audio(listener); 196 | 197 | controls = new PointerLockControls(camera, renderer.domElement); 198 | if (debug) { 199 | document.body.addEventListener('click', () => controls.lock()); 200 | document.body.addEventListener('keydown', ev => { 201 | switch(ev.keyCode) { 202 | case 87: controls.moveForward(0.2); break; 203 | case 65: controls.moveRight(-0.2); break; 204 | case 83: controls.moveForward(-0.2); break; 205 | case 68: controls.moveRight(0.2); break; 206 | case 78: gotoRoom((context.room + 1) % rooms.length); break; 207 | default: { 208 | var room = ev.keyCode - 48; 209 | if (!ev.metaKey && room >= 0 && room < rooms.length) { 210 | gotoRoom(room); 211 | } 212 | } 213 | } 214 | }); 215 | } 216 | scene.add(controls.getObject()); 217 | 218 | parent = new THREE.Object3D(); 219 | scene.add(parent); 220 | 221 | window.addEventListener('resize', onWindowResize, false); 222 | 223 | for (let i = 0; i < 2; i++) { 224 | controllers[i] = renderer.xr.getController(i); 225 | controllers[i].raycaster = new THREE.Raycaster(); 226 | controllers[i].raycaster.near = 0.1; 227 | controllers[i].addEventListener('selectstart', onSelectStart); 228 | controllers[i].addEventListener('selectend', onSelectEnd); 229 | } 230 | 231 | // global lights 232 | const lightSun = new THREE.DirectionalLight(0xeeffff); 233 | lightSun.name = 'sun'; 234 | lightSun.position.set(0.2, 1, 0.1); 235 | const lightFill = new THREE.DirectionalLight(0xfff0ee, 0.3); 236 | lightFill.name = 'fillLight'; 237 | lightFill.position.set(-0.2, -1, -0.1); 238 | 239 | scene.add(lightSun, lightFill); 240 | 241 | var cameraRig = new THREE.Group(); 242 | cameraRig.add(camera); 243 | cameraRig.add(controllers[0]); 244 | cameraRig.add(controllers[1]); 245 | cameraRig.position.set(0, 0, 2); 246 | scene.add(cameraRig); 247 | 248 | context.vrMode = false; // in vr 249 | context.assets = assets; 250 | context.shaders = shaders; 251 | context.scene = parent; 252 | context.renderer = renderer; 253 | context.camera = camera; 254 | context.audioListener = listener; 255 | context.goto = null; 256 | context.cameraRig = cameraRig; 257 | context.controllers = controllers; 258 | context.world = ecsyWorld; 259 | context.systemsGroup = systemsGroup; 260 | context.handedness = handedness; 261 | 262 | window.context = context; 263 | 264 | const loadTotal = Object.keys(assets).length; 265 | 266 | loadAssets(renderer, 'assets/', assets, () => { 267 | raycontrol = new RayControl(context, handedness); 268 | context.raycontrol = raycontrol; 269 | 270 | teleport = new Teleport(context); 271 | context.teleport = teleport; 272 | 273 | setupControllers(); 274 | roomHall.setup(context); 275 | roomPanorama.setup(context); 276 | roomPanoramaStereo.setup(context); 277 | roomPhotogrammetryObject.setup(context); 278 | roomVertigo.setup(context); 279 | roomSound.setup(context); 280 | 281 | rooms[context.room].enter(context); 282 | 283 | slideshow.setup(context); 284 | 285 | document.body.appendChild(renderer.domElement); 286 | document.body.appendChild(VRButton.createButton(renderer, status => { 287 | context.vrMode = status === 'sessionStarted'; 288 | if (context.vrMode) { 289 | gotoRoom(0); 290 | context.cameraRig.position.set(0, 0, 2); 291 | context.goto = null; 292 | } else { 293 | slideshow.setup(context); 294 | } 295 | })); 296 | renderer.setAnimationLoop(animate); 297 | 298 | document.getElementById('loading').style.display = 'none'; 299 | }, 300 | 301 | loadProgress => { 302 | document.querySelector('#progressbar').setAttribute('stroke-dashoffset', 303 | - (282 - Math.floor(loadProgress / loadTotal * 282))); 304 | }, 305 | debug); 306 | 307 | } 308 | 309 | function setupControllers() { 310 | var model = assets['generic_controller_model'].scene; 311 | var material = new THREE.MeshLambertMaterial({ 312 | map: assets['controller_tex'], 313 | }); 314 | model.getObjectByName('body').material = material; 315 | model.getObjectByName('trigger').material = material; 316 | 317 | for (let i = 0;i < 2; i++) { 318 | let controller = controllers[i]; 319 | controller.boundingBox = new THREE.Box3(); 320 | controller.userData.grabbing = null; 321 | controller.addEventListener( 'connected', function ( event ) { 322 | this.add(model.clone()); 323 | raycontrol.addController(this, event.data); 324 | } ); 325 | controller.addEventListener( 'disconnect', function () { 326 | this.remove(this.children[0]); 327 | raycontrol.removeController(this, event.data); 328 | }); 329 | } 330 | } 331 | 332 | // @FIXME Hack for Oculus Browser issue 333 | var selectStartSkip = {}; 334 | var selectEndSkip = {}; 335 | var OculusBrowser = navigator.userAgent.indexOf("OculusBrowser") !== -1 && 336 | parseInt(navigator.userAgent.match(/OculusBrowser\/([0-9]+)./)[1]) < 8; 337 | 338 | // <@FIXME 339 | 340 | function onSelectStart(ev) { 341 | // @FIXME Hack for Oculus Browser issue 342 | if (OculusBrowser) { 343 | const controller = ev.target; 344 | if (!selectStartSkip[controller]) { 345 | selectStartSkip[controller] = true; 346 | return; 347 | } 348 | selectStartSkip[controller] = false; 349 | } 350 | // <@FIXME 351 | 352 | const trigger = ev.target.getObjectByName('trigger'); 353 | trigger.rotation.x = -0.3; 354 | raycontrol.onSelectStart(ev); 355 | } 356 | 357 | function onSelectEnd(ev) { 358 | // @FIXME Hack for Oculus Browser issue 359 | if (OculusBrowser) { 360 | const controller = ev.target; 361 | if (!selectEndSkip[controller]) { 362 | selectEndSkip[controller] = true; 363 | return; 364 | } 365 | selectEndSkip[controller] = false; 366 | } 367 | // <@FIXME 368 | 369 | const trigger = ev.target.getObjectByName('trigger'); 370 | trigger.rotation.x = 0; 371 | raycontrol.onSelectEnd(ev); 372 | } 373 | 374 | function onWindowResize() { 375 | camera.aspect = window.innerWidth / window.innerHeight; 376 | camera.updateProjectionMatrix(); 377 | renderer.setSize(window.innerWidth, window.innerHeight); 378 | } 379 | 380 | function animate() { 381 | var delta = clock.getDelta(); 382 | var elapsedTime = clock.elapsedTime; 383 | 384 | ecsyWorld.execute(delta, elapsedTime); 385 | 386 | // update controller bounding boxes 387 | for (let i = 0; i < controllers.length; i++) { 388 | const model = controllers[i].getObjectByName('Scene'); 389 | if (model) { 390 | controllers[i].boundingBox.setFromObject(model); 391 | } 392 | } 393 | 394 | // render current room 395 | context.raycontrol.execute(context, delta, elapsedTime); 396 | rooms[context.room].execute(context, delta, elapsedTime); 397 | if (!context.vrMode) { 398 | slideshow.execute(context, delta, elapsedTime); 399 | } 400 | 401 | renderer.render(scene, camera); 402 | if (context.goto !== null) { 403 | gotoRoom(context.goto); 404 | context.goto = null; 405 | } 406 | } 407 | 408 | window.onload = () => {init()}; 409 | -------------------------------------------------------------------------------- /src/lib/ColorUtils.js: -------------------------------------------------------------------------------- 1 | function clamp(value, min, max) { 2 | return Math.min(Math.max(value, min), max); 3 | } 4 | 5 | export function hsv2rgb(hsv) { 6 | var r, g, b, i, f, p, q, t; 7 | var h = clamp(hsv.h, 0, 1); 8 | var s = clamp(hsv.s, 0, 1); 9 | var v = hsv.v; 10 | 11 | i = Math.floor(h * 6); 12 | f = h * 6 - i; 13 | p = v * (1 - s); 14 | q = v * (1 - f * s); 15 | t = v * (1 - (1 - f) * s); 16 | switch (i % 6) { 17 | case 0: r = v; g = t; b = p; break; 18 | case 1: r = q; g = v; b = p; break; 19 | case 2: r = p; g = v; b = t; break; 20 | case 3: r = p; g = q; b = v; break; 21 | case 4: r = t; g = p; b = v; break; 22 | case 5: r = v; g = p; b = q; break; 23 | } 24 | return { 25 | r: Math.round(r * 255), 26 | g: Math.round(g * 255), 27 | b: Math.round(b * 255) 28 | }; 29 | } 30 | 31 | export function rgb2hsv(r, g, b) { 32 | var max = Math.max(r, g, b); 33 | var min = Math.min(r, g, b); 34 | var d = max - min; 35 | var h; 36 | var s = (max === 0 ? 0 : d / max); 37 | var v = max; 38 | 39 | if (arguments.length === 1) { g = r.g; b = r.b; r = r.r; } 40 | 41 | switch (max) { 42 | case min: h = 0; break; 43 | case r: h = (g - b) + d * (g < b ? 6 : 0); h /= 6 * d; break; 44 | case g: h = (b - r) + d * 2; h /= 6 * d; break; 45 | case b: h = (r - g) + d * 4; h /= 6 * d; break; 46 | } 47 | return {h: h, s: s, v: v}; 48 | } 49 | -------------------------------------------------------------------------------- /src/lib/ColorWheel.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import {hsv2rgb} from '../lib/ColorUtils.js'; 3 | 4 | export default class ColorWheel { 5 | constructor(ctx, controller, onColorChanged) { 6 | this.ctx = ctx; 7 | this.radius = 0.1; 8 | this.hsv = { h: 0.0, s: 0.0, v: 1.0 }; 9 | this.rgb = {r: 0, g: 0, b: 0}; 10 | this.onColorChanged = onColorChanged; 11 | const geometry = new THREE.CircleBufferGeometry(this.radius, 12); 12 | var vertexShader = '\ 13 | varying vec2 vUv;\ 14 | void main() {\ 15 | vUv = uv;\ 16 | vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);\ 17 | gl_Position = projectionMatrix * mvPosition;\ 18 | }\ 19 | '; 20 | 21 | var fragmentShader = '\ 22 | #define M_PI2 6.28318530718\n \ 23 | uniform float brightness;\ 24 | varying vec2 vUv;\ 25 | vec3 hsb2rgb(in vec3 c){\ 26 | vec3 rgb = clamp(abs(mod(c.x * 6.0 + vec3(0.0, 4.0, 2.0), 6.0) - 3.0) - 1.0, \ 27 | 0.0, \ 28 | 1.0 );\ 29 | rgb = rgb * rgb * (3.0 - 2.0 * rgb);\ 30 | return c.z * mix( vec3(1.0), rgb, c.y);\ 31 | }\ 32 | \ 33 | void main() {\ 34 | vec2 toCenter = vec2(0.5) - vUv;\ 35 | float angle = atan(toCenter.y, toCenter.x);\ 36 | float radius = length(toCenter) * 2.0;\ 37 | vec3 color = hsb2rgb(vec3((angle / M_PI2) + 0.5, radius, brightness));\ 38 | gl_FragColor = vec4(color, 1.0);\ 39 | }\ 40 | '; 41 | 42 | var material = new THREE.ShaderMaterial({ 43 | uniforms: { brightness: { type: 'f', value: this.hsv.v } }, 44 | vertexShader: vertexShader, 45 | fragmentShader: fragmentShader 46 | }); 47 | this.mesh = new THREE.Mesh(geometry, material); 48 | this.mesh.name = 'colorWheel'; 49 | this.controller = controller; 50 | 51 | 52 | const geometryLast = new THREE.CircleBufferGeometry(0.025, 12); 53 | let materialBlack = new THREE.MeshBasicMaterial({color: 0x000000}); 54 | this.blackMesh = new THREE.Mesh(geometryLast, materialBlack); 55 | this.blackMesh.name = 'black'; 56 | this.blackMesh.position.set(0, 0.15, 0); 57 | 58 | this.ui = new THREE.Group(); 59 | this.ui.add(this.mesh); 60 | this.ui.add(this.blackMesh); 61 | 62 | var geometryRing = new THREE.RingGeometry( 0.005, 0.01, 32 ); 63 | var materialRing = new THREE.MeshBasicMaterial( { color: 0xffff00, side: THREE.DoubleSide } ); 64 | this.colorSelector = new THREE.Mesh( geometryRing, materialRing ); 65 | this.colorSelector.position.z = 0.01; 66 | this.colorSelector.name = 'colorSelector'; 67 | this.ui.add(this.colorSelector); 68 | 69 | this.ui.name = 'ColorWheel'; 70 | this.ui.visible = false; 71 | //this.ui.rotation.x = -Math.PI / 3; 72 | this.ui.position.y = 0.1; 73 | 74 | controller.add(this.ui); 75 | 76 | ctx.raycontrol.addState('colorwheel', { 77 | colliderMesh: this.ui, 78 | order: -1, 79 | onHover: (intersection, active, controller) => { 80 | if (active) { 81 | var point = intersection.point.clone(); 82 | this.mesh.worldToLocal(point); 83 | 84 | this.colorSelector.position.x = point.x; 85 | this.colorSelector.position.y = point.y; 86 | } 87 | }, 88 | onHoverLeave: (intersection) => { 89 | }, 90 | onSelectStart: (intersection, controller) => { 91 | if (intersection.object.name === 'colorWheel') { 92 | 93 | var point = intersection.point.clone(); 94 | this.mesh.updateMatrixWorld(); 95 | this.mesh.worldToLocal(point); 96 | 97 | //this.objects.hueCursor.position.copy(position); 98 | let uv = intersection.uv.clone(); 99 | uv.x = uv.x * 2 - 1; 100 | uv.y = uv.y * 2 - 1; 101 | 102 | let polarPosition = { 103 | r: this.radius * Math.sqrt(uv.x * uv.x + uv.y * uv.y), 104 | theta: Math.PI + Math.atan2(uv.y, uv.x) 105 | }; 106 | var angle = ((polarPosition.theta * (180 / Math.PI)) + 180) % 360; 107 | this.hsv.h = angle / 360; 108 | this.hsv.s = polarPosition.r / this.radius; 109 | this.updateColor(); 110 | } else { 111 | this.onColorChanged(intersection.object.material.color.clone().multiplyScalar(255)); 112 | } 113 | }, 114 | onSelectEnd: (intersection) => { 115 | } 116 | }); 117 | } 118 | 119 | updateColor () { 120 | this.rgb = hsv2rgb(this.hsv); 121 | this.colorSelector.material.color.setRGB(this.rgb.r / 255, this.rgb.g / 255, this.rgb.b / 255); 122 | this.onColorChanged(this.rgb); 123 | } 124 | 125 | enter() { 126 | this.ctx.raycontrol.activateState('colorwheel'); 127 | } 128 | 129 | exit() { 130 | this.ctx.raycontrol.deactivateState('colorwheel'); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/lib/EventDispatcher.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @private 3 | * @class EventDispatcher 4 | */ 5 | export default class EventDispatcher { 6 | constructor() { 7 | this._listeners = {}; 8 | this.stats = { 9 | fired: 0, 10 | handled: 0 11 | }; 12 | } 13 | 14 | /** 15 | * Add an event listener 16 | * @param {String} eventName Name of the event to listen 17 | * @param {Function} listener Callback to trigger when the event is fired 18 | */ 19 | addEventListener(eventName, listener) { 20 | let listeners = this._listeners; 21 | if (listeners[eventName] === undefined) { 22 | listeners[eventName] = []; 23 | } 24 | 25 | if (listeners[eventName].indexOf(listener) === -1) { 26 | listeners[eventName].push(listener); 27 | } 28 | } 29 | 30 | /** 31 | * Check if an event listener is already added to the list of listeners 32 | * @param {String} eventName Name of the event to check 33 | * @param {Function} listener Callback for the specified event 34 | */ 35 | hasEventListener(eventName, listener) { 36 | return ( 37 | this._listeners[eventName] !== undefined && 38 | this._listeners[eventName].indexOf(listener) !== -1 39 | ); 40 | } 41 | 42 | /** 43 | * Remove an event listener 44 | * @param {String} eventName Name of the event to remove 45 | * @param {Function} listener Callback for the specified event 46 | */ 47 | removeEventListener(eventName, listener) { 48 | var listenerArray = this._listeners[eventName]; 49 | if (listenerArray !== undefined) { 50 | var index = listenerArray.indexOf(listener); 51 | if (index !== -1) { 52 | listenerArray.splice(index, 1); 53 | } 54 | } 55 | } 56 | 57 | /** 58 | * Dispatch an event 59 | * @param {String} eventName Name of the event to dispatch 60 | * @param {Data} data to eit 61 | */ 62 | dispatchEvent(eventName, data) { 63 | this.stats.fired++; 64 | 65 | var listenerArray = this._listeners[eventName]; 66 | if (listenerArray !== undefined) { 67 | var array = listenerArray.slice(0); 68 | 69 | for (var i = 0; i < array.length; i++) { 70 | array[i].call(this, data); 71 | } 72 | } 73 | } 74 | 75 | /** 76 | * Reset stats counters 77 | */ 78 | resetCounters() { 79 | this.stats.fired = this.stats.handled = 0; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/lib/PositionalAudioPolyphonic.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | export default class PositionalAudioPolyphonic extends THREE.Object3D { 4 | constructor(listener, poolSize) { 5 | super(); 6 | this.listener = listener; 7 | this.context = listener.context; 8 | 9 | this.poolSize = poolSize || 5; 10 | for (var i = 0; i < this.poolSize; i++) { 11 | this.children.push(new THREE.PositionalAudio(listener)); 12 | } 13 | } 14 | 15 | setBuffer(buffer) { 16 | this.children.forEach(sound => { 17 | sound.setBuffer(buffer); 18 | }); 19 | } 20 | 21 | play() { 22 | var found = false; 23 | for (let i = 0;i { 22 | let pa = a.order || 0; 23 | let pb = b.order || 0; 24 | return pa - pb; 25 | }); 26 | } 27 | 28 | disable() { 29 | this.lineBasic.visible = this.line0.visible = false; 30 | this.enabled = false; 31 | this.controllers.forEach(controller => controller.active = false); 32 | } 33 | 34 | changeHandedness(primary) { 35 | if (primary !== this.primary) { 36 | this.primary = primary; 37 | this.secondary = primary === "right" ? "left" : "right"; 38 | 39 | this.dispatchEvent("handednessChanged", {primary: this.primary, secondary: this.secondary}); 40 | } 41 | } 42 | 43 | addState(name, state, activate) { 44 | if (this.states[name]) { 45 | console.error(`RayControl state '${name}' already exist, please use a different name.`); 46 | return; 47 | } 48 | 49 | state.name = name; 50 | 51 | if (typeof state.raycaster === "undefined") { 52 | state.raycaster = true; 53 | } 54 | 55 | if (typeof state.controller === "undefined") { 56 | state.controller = "primary"; 57 | } else if (!validStateController.includes(state.controller)) { 58 | console.warn("Invalid controller selector:", state.controller); 59 | state.controller = "primary"; 60 | } 61 | 62 | this.states[name] = state; 63 | 64 | if (activate === true) { 65 | this.currentStates.push(state); 66 | } 67 | 68 | return state; 69 | } 70 | 71 | activateState(name) { 72 | if (this.states[name]) { 73 | this.currentStates.push(this.states[name]); 74 | this._sort(); 75 | } 76 | } 77 | 78 | deactivateAll(name) { 79 | this.currentStates = []; 80 | this.controllers.forEach(c => { 81 | this.currentStates.forEach(s => { 82 | if (c.intersections[s.name]) { 83 | c.intersections[s.name] = null; 84 | } 85 | }); 86 | }); 87 | } 88 | 89 | deactivateState(name) { 90 | this.currentStates.splice(this.currentStates.indexOf(name), 1); 91 | this.controllers.forEach(c => { 92 | if (c.intersections[name]) { 93 | c.intersections[name] = null; 94 | } 95 | }); 96 | 97 | this._sort(); 98 | } 99 | 100 | addController(controller, inputSource) { 101 | let controllerData = { 102 | controller: controller, 103 | inputSource: inputSource, 104 | active: false, 105 | stateHit: {}, 106 | intersections: {}, 107 | currentIntersection: null, 108 | hit: false 109 | }; 110 | 111 | this.controllers.push(controllerData); 112 | 113 | if (this.matchController(controllerData, "primary")) { 114 | controller.add( this.raycasterContext ); 115 | } 116 | 117 | this.dispatchEvent("controllerConnected", controllerData); 118 | } 119 | 120 | removeController(controller) { 121 | const index = this.controllers.findIndex(controllerData => controllerData.controller === controller); 122 | const controllerData = this.controllers.find(controllerData => controllerData.controller === controller) 123 | this.controllers.splice(index, 1); 124 | this.dispatchEvent("controllerDisconnected", controllerData); 125 | } 126 | 127 | constructor(ctx, primary) { 128 | super(); 129 | this.ctx = ctx; 130 | 131 | if (typeof primary === "undefined") { 132 | this.primary = "right"; 133 | this.secondary = "left"; 134 | } else { 135 | this.primary = primary; 136 | this.secondary = primary === "right" ? "left" : "right"; 137 | } 138 | 139 | this.controllers = []; 140 | 141 | this.previousLineStyle = 'pretty'; 142 | this.enabled = true; 143 | this.raycaster = new THREE.Raycaster(); 144 | this.states = {}; 145 | this.currentStates = []; 146 | 147 | var line = ctx.assets['teleport_model'].scene.getObjectByName('beam'); 148 | 149 | ctx.assets['beam_tex'].wrapT = THREE.RepeatWrapping; 150 | ctx.assets['beam_tex'].wrapS = THREE.RepeatWrapping; 151 | rayMaterial = line.material = new THREE.ShaderMaterial({ 152 | uniforms: { 153 | time: {value: 0}, 154 | active: {value: 0}, 155 | tex: {value: ctx.assets['beam_tex']} 156 | }, 157 | vertexShader: ctx.shaders.basic_vert, 158 | fragmentShader: ctx.shaders.beam_frag, 159 | blending: THREE.AdditiveBlending, 160 | transparent: true 161 | }); 162 | 163 | line.renderOrder = 10; 164 | 165 | line.name = 'line'; 166 | this.rayLength = 5; 167 | line.scale.z = this.rayLength; 168 | 169 | this.line0 = line.clone(); 170 | this.line0.visible = true; 171 | 172 | this.raycasterContext = new THREE.Group(); 173 | this.raycasterContext.add(this.line0); 174 | this.raycasterContext.name = 'raycasterContext'; 175 | 176 | var geometry = new THREE.BufferGeometry().setFromPoints( [ new THREE.Vector3( 0, 0, 0 ), new THREE.Vector3( 0, 0, - 1 ) ] ); 177 | 178 | this.lineBasic = new THREE.Line( geometry ); 179 | this.lineBasic.name = 'line'; 180 | this.lineBasic.scale.z = 5; 181 | this.lineBasic.visible = false; 182 | this.raycasterContext.add(this.lineBasic); 183 | } 184 | 185 | setLineStyle(lineStyle) { 186 | const basic = lineStyle === 'basic'; 187 | this.lineBasic.visible = basic; 188 | this.line0.visible = !basic; 189 | this.previousLineStyle = lineStyle; 190 | } 191 | 192 | /* 193 | selector could be: left, right, both, primary, secondary 194 | */ 195 | matchController(controllerData, selector) { 196 | const handedness = controllerData.inputSource.handedness; 197 | 198 | return ( 199 | (selector === handedness) || 200 | (selector === "both" && (handedness === "right" || handedness === "left")) || 201 | (selector === "primary" && this.primary === handedness) || 202 | (selector === "secondary" && this.secondary === handedness) 203 | ); 204 | } 205 | 206 | onSelectStart(evt) { 207 | if (!this.enabled) { return; } 208 | 209 | let controller = evt.target; 210 | let controllerData = this.controllers.find(c => c.controller === controller); 211 | if (controllerData) { 212 | controllerData.active = true; 213 | if (controllerData.currentIntersection) { 214 | const state = controllerData.currentIntersection.state; 215 | if (state.onSelectStart) { 216 | state.onSelectStart(controllerData.currentIntersection.intersection, controllerData.controller); 217 | } 218 | } 219 | 220 | // Check no raycaster states 221 | this.currentStates.forEach(state => { 222 | if (state.onSelectStart && !state.raycaster) { 223 | state.onSelectStart(controllerData.intersections[state.name], controller); 224 | } 225 | }); 226 | } 227 | } 228 | 229 | execute(ctx, delta, time) { 230 | if (!this.enabled || this.currentStates.length === 0) { return; } 231 | 232 | rayMaterial.uniforms.time.value = time; 233 | 234 | let firstHit = false; 235 | 236 | var stateIntersections = {}; 237 | for (var c = 0; c < this.controllers.length; c++) { 238 | let controllerData = this.controllers[c]; 239 | 240 | for (var i = 0; i < this.currentStates.length; i++) { 241 | let state = this.currentStates[i]; 242 | if (!state.raycaster) { 243 | continue; 244 | } 245 | 246 | // Check if this controller should be active on this state 247 | if (!this.matchController(controllerData, state.controller)) { 248 | continue; 249 | } 250 | 251 | let controller = controllerData.controller; 252 | let active = controllerData.active; 253 | let intersections = this.getIntersections(controller, state.colliderMesh); 254 | 255 | if (intersections.length > 0) { 256 | // Use just the closest object 257 | controllerData.intersections[state.name] = intersections[0]; 258 | controllerData.stateHit[state.name] = true; 259 | } else { 260 | controllerData.intersections[state.name] = null; 261 | } 262 | } 263 | } 264 | 265 | this.line0.scale.z = Math.min(this.rayLength, 1); 266 | this.lineBasic.scale.z = Math.min(this.rayLength, 1); 267 | 268 | // For each controller, find the closest intersection from all the states 269 | for (var c = 0; c < this.controllers.length; c++) { 270 | let controllerData = this.controllers[c]; 271 | let intersections = Object.entries(controllerData.intersections).filter(i => i[1] !== null); 272 | if (intersections.length > 0) { 273 | 274 | intersections.sort((a,b) => { 275 | return a[1].distance - b[1].distance; 276 | }); 277 | 278 | const intersectionData = intersections[0]; 279 | const intersection = intersectionData[1]; 280 | const state = this.states[intersectionData[0]]; 281 | 282 | controllerData.prevIntersection = controllerData.currentIntersection; 283 | controllerData.currentIntersection = { 284 | state, intersection 285 | }; 286 | 287 | if (state.lineStyleOnIntersection) { 288 | this.setLineStyle(state.lineStyleOnIntersection); 289 | } else { 290 | this.setLineStyle('advanced'); 291 | } 292 | 293 | state.onHover && state.onHover(intersection, controllerData.active, controllerData.controller); 294 | this.line0.scale.z = Math.min(intersection.distance, 1); 295 | this.lineBasic.scale.z = Math.min(intersection.distance, 1); 296 | } else { 297 | controllerData.currentIntersection = null; 298 | } 299 | } 300 | 301 | // Handle onHoverLeave 302 | for (var c = 0; c < this.controllers.length; c++) { 303 | let controllerData = this.controllers[c]; 304 | if (!controllerData.prevIntersection) { 305 | continue; 306 | } 307 | 308 | // If we can't find the previous intersection currently enabled, we should emit hoverLeave 309 | if (!this.controllers.find(c => { 310 | let prev = controllerData.prevIntersection; 311 | let current = c.currentIntersection; 312 | return current && prev.state.name === current.state.name && 313 | prev.intersection.object === current.intersection.object; 314 | } 315 | )) { 316 | controllerData.prevIntersection.state.onHoverLeave( 317 | controllerData.prevIntersection.intersection, 318 | false, 319 | controllerData.controller 320 | ); 321 | 322 | controllerData.prevIntersection = null; 323 | } 324 | } 325 | } 326 | 327 | getIntersections( controller, colliderMesh ) { 328 | let raycasterContext = controller.getObjectByName('raycasterContext'); 329 | if (!raycasterContext) { 330 | console.warn('No raycasterContext found for this controller', controller); 331 | return []; 332 | } 333 | 334 | tempMatrix.identity().extractRotation( raycasterContext.matrixWorld ); 335 | 336 | this.raycaster.ray.origin.setFromMatrixPosition( raycasterContext.matrixWorld ); 337 | this.raycaster.ray.direction.set( 0, 0, - 1 ).applyMatrix4( tempMatrix ); 338 | 339 | if (Array.isArray(colliderMesh)) { 340 | return this.raycaster.intersectObjects( colliderMesh, true); 341 | } else { 342 | return this.raycaster.intersectObject( colliderMesh, true); 343 | } 344 | } 345 | 346 | onSelectEnd(evt) { 347 | if (!this.enabled) { return; } 348 | 349 | let controllerData = this.controllers.find(c => c.controller === evt.target) 350 | if (!controllerData || !controllerData.active) { return; } 351 | 352 | if (controllerData) { 353 | if (controllerData.currentIntersection) { 354 | const state = controllerData.currentIntersection.state; 355 | if (state.onSelectEnd) { 356 | state.onSelectEnd(controllerData.currentIntersection.intersection, controllerData.controller); 357 | } 358 | } 359 | 360 | // Check no raycaster states 361 | this.currentStates.forEach(state => { 362 | if (state.onSelectEnd && !state.raycaster) { 363 | state.onSelectEnd(null, controllerData.controller); 364 | } 365 | }); 366 | } 367 | 368 | controllerData.active = false; 369 | } 370 | } 371 | -------------------------------------------------------------------------------- /src/lib/Teleport.js: -------------------------------------------------------------------------------- 1 | import {rayMaterial} from "./RayControl.js"; 2 | import * as THREE from 'three'; 3 | 4 | var tempMatrix = new THREE.Matrix4(); 5 | var intersected = []; 6 | 7 | export default class Teleport { 8 | constructor(ctx, mesh) { 9 | this.ctx = ctx; 10 | 11 | // Holder for all the 12 | this.teleportEntity = new THREE.Group(); 13 | 14 | this.active = false; 15 | 16 | this.teleportHitGeometry = ctx.assets['teleport_model'].scene.getObjectByName('goal'); 17 | this.teleportHitGeometry.material = rayMaterial; 18 | 19 | this.teleportHitGeometry.renderOrder = 10; 20 | 21 | this.ballColliding = ctx.assets['teleport_model'].scene.getObjectByName('glow'); 22 | this.ballColliding.material = new THREE.MeshBasicMaterial({ 23 | color: 0x00257b, 24 | map: ctx.assets['glow_tex'], 25 | transparent: true, 26 | blending: THREE.AdditiveBlending 27 | }); 28 | 29 | this.ballColliding.renderOrder = 10; 30 | 31 | 32 | this.ballColliding.visible = false; 33 | 34 | this.teleportEntity.add(this.ballColliding); 35 | 36 | this.teleportHitGeometry.visible = false; 37 | 38 | this.teleportHitGeometry.position.set(-2, 0, -2); 39 | 40 | this.teleportEntity.add(this.teleportHitGeometry); 41 | 42 | this.ctx.scene.add(this.teleportEntity); 43 | 44 | // sounds 45 | this.startSound = new THREE.Audio(ctx.audioListener); 46 | this.startSound.setBuffer(ctx.assets['teleport_a_snd']); 47 | this.startSound.setLoop(true); 48 | this.startSound.pause(); 49 | 50 | this.endSound = new THREE.Audio(ctx.audioListener); 51 | this.endSound.setBuffer(ctx.assets['teleport_b_snd']); 52 | this.endSound.setLoop(false); 53 | this.endSound.pause(); 54 | } 55 | 56 | 57 | onSelectStart(evt) { 58 | //if (evt.target === this.ctx.controllers[0]) 59 | this.active = true; 60 | this.endSound.pause(); 61 | this.startSound.play(); 62 | } 63 | 64 | onHoverLeave() { 65 | this.ballColliding.visible = false; 66 | this.teleportHitGeometry.visible = false; 67 | this.startSound.pause(); 68 | } 69 | 70 | onHover(hitPoint, active) { 71 | if (active) { 72 | this.teleportHitGeometry.visible = true; 73 | this.teleportHitGeometry.position.copy(hitPoint); 74 | this.hit = true; 75 | this.ballColliding.visible = false; 76 | } else { 77 | this.ballColliding.visible = true; 78 | this.ballColliding.position.copy(hitPoint); 79 | } 80 | } 81 | 82 | onSelectEnd(targetPoint) { 83 | const headPosition = this.ctx.renderer.xr.getCamera(this.ctx.camera).position; 84 | const offset = targetPoint.sub(headPosition); 85 | offset.y = 0; // We don't want to change height to floor's level 86 | 87 | this.ctx.cameraRig.position.add(offset); 88 | this.teleportHitGeometry.visible = false; 89 | 90 | this.active = false; 91 | 92 | this.startSound.pause(); 93 | this.endSound.play(); 94 | } 95 | } 96 | 97 | -------------------------------------------------------------------------------- /src/lib/VRButton.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | This is modification of the original VRButton. I've added a callback to know 4 | when the user enters and exists from VR 5 | 6 | Original at: 7 | https://github.com/mrdoob/three.js/blob/dev/examples/jsm/webxr/VRButton.js 8 | 9 | */ 10 | /** 11 | * @author mrdoob / http://mrdoob.com 12 | * @author Mugen87 / https://github.com/Mugen87 13 | */ 14 | 15 | var VRButton = { 16 | 17 | createButton: function ( renderer, callback ) { 18 | 19 | function showEnterVR( /*device*/ ) { 20 | 21 | var currentSession = null; 22 | 23 | function onSessionStarted( session ) { 24 | 25 | session.addEventListener( 'end', onSessionEnded ); 26 | 27 | renderer.xr.setSession( session ); 28 | button.textContent = 'EXIT VR'; 29 | 30 | currentSession = session; 31 | 32 | callback('sessionStarted'); 33 | } 34 | 35 | function onSessionEnded( /*event*/ ) { 36 | 37 | currentSession.removeEventListener( 'end', onSessionEnded ); 38 | 39 | button.textContent = 'ENTER VR'; 40 | 41 | currentSession = null; 42 | 43 | callback('sessionEnded'); 44 | } 45 | 46 | 47 | // 48 | 49 | button.style.display = ''; 50 | 51 | button.style.cursor = 'pointer'; 52 | button.style.left = 'calc(50% - 50px)'; 53 | button.style.width = '100px'; 54 | 55 | button.textContent = 'ENTER VR'; 56 | 57 | button.onmouseenter = function () { 58 | 59 | button.style.opacity = '1.0'; 60 | 61 | }; 62 | 63 | button.onmouseleave = function () { 64 | 65 | button.style.opacity = '0.5'; 66 | 67 | }; 68 | 69 | button.onclick = function () { 70 | 71 | if ( currentSession === null ) { 72 | 73 | // WebXR's requestReferenceSpace only works if the corresponding feature 74 | // was requested at session creation time. For simplicity, just ask for 75 | // the interesting ones as optional features, but be aware that the 76 | // requestReferenceSpace call will fail if it turns out to be unavailable. 77 | // ('local' is always available for immersive sessions and doesn't need to 78 | // be requested separately.) 79 | 80 | var sessionInit = { optionalFeatures: [ 'local-floor', 'bounded-floor' ] }; 81 | navigator.xr.requestSession( 'immersive-vr', sessionInit ).then( onSessionStarted ); 82 | 83 | } else { 84 | 85 | currentSession.end(); 86 | 87 | } 88 | 89 | }; 90 | 91 | } 92 | 93 | function disableButton() { 94 | 95 | button.style.display = ''; 96 | 97 | button.style.cursor = 'auto'; 98 | button.style.left = 'calc(50% - 75px)'; 99 | button.style.width = '150px'; 100 | 101 | button.onmouseenter = null; 102 | button.onmouseleave = null; 103 | 104 | button.onclick = null; 105 | 106 | } 107 | 108 | function showWebXRNotFound() { 109 | 110 | disableButton(); 111 | 112 | button.textContent = 'VR NOT SUPPORTED'; 113 | 114 | } 115 | 116 | function stylizeElement( element ) { 117 | 118 | element.style.position = 'absolute'; 119 | element.style.bottom = '20px'; 120 | element.style.padding = '12px 6px'; 121 | element.style.border = '1px solid #fff'; 122 | element.style.borderRadius = '4px'; 123 | element.style.background = 'rgba(0,0,0,0.1)'; 124 | element.style.color = '#fff'; 125 | element.style.font = 'normal 13px sans-serif'; 126 | element.style.textAlign = 'center'; 127 | element.style.opacity = '0.5'; 128 | element.style.outline = 'none'; 129 | element.style.zIndex = '999'; 130 | 131 | } 132 | 133 | if ( 'xr' in navigator ) { 134 | 135 | var button = document.createElement( 'button' ); 136 | button.style.display = 'none'; 137 | 138 | stylizeElement( button ); 139 | 140 | navigator.xr.isSessionSupported( 'immersive-vr' ).then( function ( supported ) { 141 | 142 | supported ? showEnterVR() : showWebXRNotFound(); 143 | 144 | } ); 145 | 146 | return button; 147 | 148 | } else { 149 | 150 | var message = document.createElement( 'a' ); 151 | 152 | if ( window.isSecureContext === false ) { 153 | 154 | message.href = document.location.href.replace( /^http:/, 'https:' ); 155 | message.innerHTML = 'WEBXR NEEDS HTTPS'; // TODO Improve message 156 | 157 | } else { 158 | 159 | message.href = 'https://immersiveweb.dev/'; 160 | message.innerHTML = 'WEBXR NOT AVAILABLE'; 161 | 162 | } 163 | 164 | message.style.left = 'calc(50% - 90px)'; 165 | message.style.width = '180px'; 166 | message.style.textDecoration = 'none'; 167 | 168 | stylizeElement( message ); 169 | 170 | return message; 171 | 172 | } 173 | 174 | } 175 | 176 | }; 177 | 178 | export { VRButton }; 179 | -------------------------------------------------------------------------------- /src/lib/assetManager.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { BasisTextureLoader } from 'three/examples/jsm/loaders/BasisTextureLoader.js'; 3 | import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; 4 | import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js'; 5 | import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'; 6 | 7 | //const BASIS_LIB_PATH = 'src/vendor/'; 8 | const BASIS_LIB_PATH = 'src/vendor/'; 9 | const DRACO_LIB_PATH = 'src/vendor/'; 10 | 11 | function getLoadedCount(assets) { 12 | let count = 0; 13 | for (var i in assets) { 14 | if (assets[i].loading !== true) { count ++; } 15 | } 16 | return count; 17 | } 18 | 19 | function allAssetsLoaded(assets) { 20 | for (var i in assets) { 21 | if (assets[i].loading === true) { return false; } 22 | } 23 | return true; 24 | } 25 | 26 | export function loadAssets(renderer, basePath, assets, onComplete, onProgress, debug) { 27 | if (basePath && basePath[basePath.length - 1] != '/') { 28 | basePath += '/'; 29 | } 30 | 31 | var basisLoader = new BasisTextureLoader(); 32 | basisLoader.setTranscoderPath(BASIS_LIB_PATH); 33 | basisLoader.detectSupport(renderer); 34 | 35 | var gltfLoader = new GLTFLoader(); 36 | var dracoLoader = new DRACOLoader(); 37 | dracoLoader.setDecoderPath(DRACO_LIB_PATH); 38 | gltfLoader.setDRACOLoader(dracoLoader); 39 | 40 | var texLoader = new THREE.TextureLoader(); 41 | var objLoader = new OBJLoader(); 42 | var fontLoader = new THREE.FontLoader(); 43 | var audioLoader = new THREE.AudioLoader(); 44 | 45 | 46 | var loaders = { 47 | 'gltf': gltfLoader, 48 | 'glb': gltfLoader, 49 | 'obj': objLoader, 50 | 'gif': texLoader, 51 | 'png': texLoader, 52 | 'jpg': texLoader, 53 | 'basis': basisLoader, 54 | 'font': fontLoader, 55 | 'ogg': audioLoader 56 | }; 57 | 58 | for (var i in assets) { 59 | let assetId = i; 60 | let assetPath = assets[i].url; 61 | assets[i].loading = true; 62 | let ext = assetPath.substr(assetPath.lastIndexOf('.') + 1).toLowerCase(); 63 | loaders[ext].load(basePath + assetPath, asset => { 64 | if (debug) { 65 | console.info(`%c ${assetPath} loaded`, 'color:green'); 66 | } 67 | var options = assets[assetId].options; 68 | assets[assetId] = ext == 'font'? asset.data : asset; 69 | 70 | if (typeof options !== "undefined") { 71 | if (typeof options.repeat !== "undefined") { 72 | assets[assetId].repeat.set(options.repeat[0], options.repeat[1]); 73 | delete options.repeat; 74 | } 75 | for (let opt in options) { 76 | assets[assetId][opt] = options[opt]; 77 | } 78 | 79 | if (onProgress) { onProgress(getLoadedCount(assets)) }; 80 | } 81 | 82 | if (onComplete && allAssetsLoaded(assets)) { onComplete(); } 83 | }, () => { 84 | /* on progress */ 85 | }, 86 | (e) => { 87 | console.error('Error loading asset', e); 88 | } 89 | ); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/lib/shaders.js: -------------------------------------------------------------------------------- 1 | export var shaders = { 2 | door_frag : ` 3 | uniform float time; 4 | uniform float selected; 5 | uniform sampler2D tex; 6 | varying vec2 vUv; 7 | 8 | void main( void ) { 9 | float t = time; 10 | vec2 uv = vUv * 2.0 - 1.0; 11 | vec2 puv = vec2(length(uv.xy), atan(uv.x, uv.y)); 12 | vec4 col = texture2D(tex, vec2(log(puv.x) + t / 5.0, puv.y / 3.1415926 )); 13 | float glow = (1.0 - puv.x) * (0.5 + (sin(t) + 2.0 ) / 4.0); 14 | // blue glow 15 | col += vec4(118.0/255.0, 144.0/255.0, 219.0/255.0, 1.0) * (0.4 + glow * 1.0); 16 | // white glow 17 | col += vec4(0.2) * smoothstep(0.0, 2.0, glow * glow); 18 | gl_FragColor = col; 19 | 20 | } 21 | `, 22 | 23 | zoom_frag : ` 24 | uniform float time; 25 | uniform sampler2D tex; 26 | uniform vec2 zoomPos; 27 | uniform float zoomAmount; 28 | uniform float zoomRatio; 29 | varying vec2 vUv; 30 | 31 | void main( void ) { 32 | float t = time; 33 | vec2 uv = vec2(vUv.x - 0.5, (1.0 - vUv.y) - 0.5); 34 | vec2 texUv = uv * vec2(zoomRatio, 1.0); 35 | vec4 col = texture2D(tex, zoomPos + texUv * zoomAmount); 36 | float dist = length(uv) * 2.0; 37 | col.a = smoothstep(0.0, 0.1, 1.0 - dist); 38 | float aura = smoothstep(0.80, 1.0, dist); 39 | col.rgb += aura * 0.3; 40 | gl_FragColor = col; 41 | } 42 | `, 43 | 44 | basic_vert : ` 45 | varying vec2 vUv; 46 | varying vec3 vPosition; 47 | void main() 48 | { 49 | vUv = uv; 50 | vPosition = position; 51 | vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); 52 | gl_Position = projectionMatrix * mvPosition; 53 | } 54 | `, 55 | 56 | panoball_vert : ` 57 | varying vec2 vUv; 58 | varying vec3 vPosition; 59 | varying vec3 vNormal; 60 | varying vec3 vWorldPos; 61 | uniform float time; 62 | uniform float selected; 63 | 64 | mat4 inverse(mat4 m) { 65 | float 66 | a00 = m[0][0], a01 = m[0][1], a02 = m[0][2], a03 = m[0][3], 67 | a10 = m[1][0], a11 = m[1][1], a12 = m[1][2], a13 = m[1][3], 68 | a20 = m[2][0], a21 = m[2][1], a22 = m[2][2], a23 = m[2][3], 69 | a30 = m[3][0], a31 = m[3][1], a32 = m[3][2], a33 = m[3][3], 70 | 71 | b00 = a00 * a11 - a01 * a10, 72 | b01 = a00 * a12 - a02 * a10, 73 | b02 = a00 * a13 - a03 * a10, 74 | b03 = a01 * a12 - a02 * a11, 75 | b04 = a01 * a13 - a03 * a11, 76 | b05 = a02 * a13 - a03 * a12, 77 | b06 = a20 * a31 - a21 * a30, 78 | b07 = a20 * a32 - a22 * a30, 79 | b08 = a20 * a33 - a23 * a30, 80 | b09 = a21 * a32 - a22 * a31, 81 | b10 = a21 * a33 - a23 * a31, 82 | b11 = a22 * a33 - a23 * a32, 83 | 84 | det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06; 85 | 86 | return mat4( 87 | a11 * b11 - a12 * b10 + a13 * b09, 88 | a02 * b10 - a01 * b11 - a03 * b09, 89 | a31 * b05 - a32 * b04 + a33 * b03, 90 | a22 * b04 - a21 * b05 - a23 * b03, 91 | a12 * b08 - a10 * b11 - a13 * b07, 92 | a00 * b11 - a02 * b08 + a03 * b07, 93 | a32 * b02 - a30 * b05 - a33 * b01, 94 | a20 * b05 - a22 * b02 + a23 * b01, 95 | a10 * b10 - a11 * b08 + a13 * b06, 96 | a01 * b08 - a00 * b10 - a03 * b06, 97 | a30 * b04 - a31 * b02 + a33 * b00, 98 | a21 * b02 - a20 * b04 - a23 * b00, 99 | a11 * b07 - a10 * b09 - a12 * b06, 100 | a00 * b09 - a01 * b07 + a02 * b06, 101 | a31 * b01 - a30 * b03 - a32 * b00, 102 | a20 * b03 - a21 * b01 + a22 * b00) / det; 103 | } 104 | 105 | 106 | mat4 transpose(in mat4 m) { 107 | vec4 i0 = m[0]; 108 | vec4 i1 = m[1]; 109 | vec4 i2 = m[2]; 110 | vec4 i3 = m[3]; 111 | 112 | return mat4( 113 | vec4(i0.x, i1.x, i2.x, i3.x), 114 | vec4(i0.y, i1.y, i2.y, i3.y), 115 | vec4(i0.z, i1.z, i2.z, i3.z), 116 | vec4(i0.w, i1.w, i2.w, i3.w) 117 | ); 118 | } 119 | 120 | void main() 121 | { 122 | vUv = uv; 123 | 124 | vPosition = position; 125 | 126 | vec3 offset = vec3( 127 | sin(position.x * 50.0 + time), 128 | sin(position.y * 10.0 + time * 2.0), 129 | cos(position.z * 40.0 + time) 130 | ) * 0.003; 131 | 132 | vPosition *= 1.0 + selected * 0.2; 133 | 134 | vNormal = normalize(inverse(transpose(modelMatrix)) * vec4(normalize(normal), 1.0)).xyz; 135 | vWorldPos = (modelMatrix * vec4(vPosition, 1.0)).xyz; 136 | 137 | vec4 mvPosition = modelViewMatrix * vec4(vPosition + offset, 1.0); 138 | gl_Position = projectionMatrix * mvPosition; 139 | } 140 | `, 141 | 142 | panoball_frag : ` 143 | uniform sampler2D tex, texfx; 144 | uniform float time; 145 | uniform float selected; 146 | varying vec2 vUv; 147 | varying vec3 vPosition; 148 | varying vec3 vNormal; 149 | varying vec3 vWorldPos; 150 | 151 | 152 | void main( void ) { 153 | vec2 uv = vUv; 154 | //uv.y = 1.0 - uv.y; 155 | 156 | vec3 eye = normalize(cameraPosition - vWorldPos); 157 | float fresnel = abs(dot(eye, vNormal)); 158 | float shift = pow((1.0 - fresnel), 4.0) * 0.05; 159 | 160 | vec3 col = vec3( 161 | texture2D(tex, uv - shift).r, 162 | texture2D(tex, uv).g, 163 | texture2D(tex, uv + shift).b 164 | ); 165 | 166 | col = mix(col * 0.7, vec3(1.0), 0.7 - fresnel); 167 | 168 | col += selected * 0.3; 169 | 170 | float t = time * 0.4 + vPosition.x + vPosition.z; 171 | uv = vec2(vUv.x + t * 0.2, vUv.y + t); 172 | vec3 fx = texture2D(texfx, uv).rgb * 0.4; 173 | 174 | 175 | gl_FragColor = vec4(col + fx, 1.0); 176 | } 177 | `, 178 | 179 | beam_frag : ` 180 | uniform float time; 181 | uniform sampler2D tex; 182 | varying vec2 vUv; 183 | 184 | void main( void ) { 185 | float t = time; 186 | vec4 col = texture2D(tex, vec2(vUv.x, vUv.y * 3.0 + time * 2.0)); 187 | col *= vUv.y; 188 | gl_FragColor = col; 189 | } 190 | `, 191 | 192 | }; -------------------------------------------------------------------------------- /src/lib/slideshow.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | const PATH_TIME = 3; // 3 secs per path 4 | 5 | var slideshow = {}; 6 | 7 | slideshow.paths = [ 8 | //'hall' 9 | [ 10 | { 11 | from: new THREE.Vector3(0, 1.6, 0), 12 | lookAt: new THREE.Vector3(0, 1, -100), 13 | velocity: new THREE.Vector3(0, 0, -0.1), 14 | angularVelocity: new THREE.Vector3(0.04, 0, 0) 15 | }, 16 | { 17 | from: new THREE.Vector3(4.5, 1.6, -3), 18 | lookAt: new THREE.Vector3(100, 0, 0), 19 | velocity: new THREE.Vector3(0, 0, -0.15), 20 | angularVelocity: new THREE.Vector3(0, 0, 0) 21 | }, 22 | { 23 | from: new THREE.Vector3(0, 1.6, 0), 24 | lookAt: new THREE.Vector3(50, 0, -40), 25 | velocity: new THREE.Vector3(0, 0.12, 0), 26 | angularVelocity: new THREE.Vector3(0, 0.1, 0) 27 | }, 28 | { 29 | from: new THREE.Vector3(3, 2.4, -4), 30 | lookAt: new THREE.Vector3(-50, 0, 40), 31 | velocity: new THREE.Vector3(0, -0.12, 0), 32 | angularVelocity: new THREE.Vector3(0, -0.1, 0) 33 | } 34 | ], 35 | //'sound' 36 | [ 37 | { 38 | from: new THREE.Vector3(0, 1.6, 0), 39 | lookAt: new THREE.Vector3(0, 0, -100), 40 | velocity: new THREE.Vector3(0.1, 0, 0.1), 41 | angularVelocity: new THREE.Vector3(0, 0.01, 0) 42 | }, 43 | ], 44 | //'photogrammetry' 45 | [ 46 | { 47 | from: new THREE.Vector3(0, 1.2, 0.8), 48 | lookAt: new THREE.Vector3(-100, 0.5, 0), 49 | velocity: new THREE.Vector3(0, 0, -0.2), 50 | angularVelocity: new THREE.Vector3(0, 0, 0) 51 | }, 52 | { 53 | from: new THREE.Vector3(-1, 1, -1.6), 54 | lookAt: new THREE.Vector3(-40, 0, 100), 55 | velocity: new THREE.Vector3(-0.05, 0, 0), 56 | angularVelocity: new THREE.Vector3(0, 0, 0) 57 | } 58 | ], 59 | //'vertigo' 60 | [ 61 | { 62 | from: new THREE.Vector3(0, 1.6, 0), 63 | lookAt: new THREE.Vector3(0, 0, -10), 64 | velocity: new THREE.Vector3(0, 0, 0), 65 | angularVelocity: new THREE.Vector3(-0.01, 0.07, 0) 66 | }, 67 | { 68 | from: new THREE.Vector3(0, 1.6, -0.4), 69 | lookAt: new THREE.Vector3(0, 0, 6), 70 | velocity: new THREE.Vector3(0, 0, 0.1), 71 | angularVelocity: new THREE.Vector3(0.07, 0, 0) 72 | } 73 | ], 74 | //'panoramastereo' 75 | [ 76 | { 77 | from: new THREE.Vector3(0, 0, 0), 78 | lookAt: new THREE.Vector3(-100, 0, 0), 79 | velocity: new THREE.Vector3(0, 0, 0), 80 | angularVelocity: new THREE.Vector3(0.03, 0.05, 0) 81 | } 82 | ], 83 | //'panorama1' 84 | [ 85 | ], 86 | //'panorama2' 87 | [ 88 | { 89 | from: new THREE.Vector3(0, 0, 0), 90 | lookAt: new THREE.Vector3(-100, 0, 0), 91 | velocity: new THREE.Vector3(0, 0, 0), 92 | angularVelocity: new THREE.Vector3(0.01, 0.1, 0) 93 | } 94 | ], 95 | //'panorama3' 96 | [ 97 | ], 98 | //'panorama4' 99 | [ 100 | { 101 | from: new THREE.Vector3(0, 0, 0), 102 | lookAt: new THREE.Vector3(-100, 0, 0), 103 | velocity: new THREE.Vector3(0, 0, 0), 104 | angularVelocity: new THREE.Vector3(-0.07, -0.05, 0) 105 | } 106 | ], 107 | //'panorama5' 108 | [ 109 | { 110 | from: new THREE.Vector3(0, 0, 0), 111 | lookAt: new THREE.Vector3(100, 0, 80), 112 | velocity: new THREE.Vector3(0, 0, 0), 113 | angularVelocity: new THREE.Vector3(0, -0.02, 0) 114 | } 115 | ] 116 | ]; 117 | 118 | slideshow.setup = function (context) { 119 | this.room = undefined; 120 | this.path = undefined; 121 | this.next(context); 122 | context.camera.position.set(0, 1.6, 0); 123 | context.cameraRig.position.set(0, 0, 2); 124 | context.goto = this.room; 125 | }; 126 | 127 | slideshow.next = function (context) { 128 | this.time = PATH_TIME + Math.random() * PATH_TIME / 2; 129 | if (this.room === undefined) { 130 | this.room = 0; 131 | this.path = 0; 132 | } else { 133 | this.path ++; 134 | if (this.path >= this.paths[this.room].length){ 135 | // find next room with paths 136 | do { 137 | this.room ++; 138 | if (this.room >= this.paths.length) { 139 | this.room = 0; 140 | context.cameraRig.position.set(0, 0, 2); 141 | } 142 | } while (!this.paths[this.room].length); 143 | var camera = context.renderer.xr.getCamera(context.camera); 144 | camera.position.set(0, 1.6, 0); 145 | context.goto = this.room; 146 | this.path = 0; 147 | } 148 | } 149 | this.reset = true; 150 | }; 151 | 152 | slideshow.execute = function (context, delta, time) { 153 | const path = this.paths[this.room][this.path]; 154 | if (this.reset) { 155 | this.reset = false; 156 | // init camera settings 157 | context.camera.position.copy(path.from); 158 | context.camera.rotation.set(0, 0, 0); 159 | context.camera.lookAt(path.lookAt); 160 | } 161 | context.camera.position.addScaledVector(path.velocity, delta); 162 | context.camera.rotation.x += path.angularVelocity.x * delta; 163 | context.camera.rotation.y += path.angularVelocity.y * delta; 164 | context.camera.rotation.z += path.angularVelocity.z * delta; 165 | this.time -= delta; 166 | if (this.time < 0) { 167 | this.next(context); 168 | } 169 | }; 170 | 171 | export {slideshow}; 172 | -------------------------------------------------------------------------------- /src/lib/utils.js: -------------------------------------------------------------------------------- 1 | export function newMarker(x, y, z, color){ 2 | const geo = new THREE.SphereBufferGeometry(0.04); 3 | const mat = new THREE.MeshBasicMaterial({color: color ? color : 0xff0000}); 4 | const mesh = new THREE.Mesh(geo, mat); 5 | if (typeof x === 'object') { 6 | mesh.position.copy(x); 7 | } else { 8 | mesh.position.set(x, y, z); 9 | } 10 | return mesh; 11 | } 12 | 13 | export function angleBetween(point1, point2) { 14 | return Math.atan2(point2.x - point1.x, point2.y - point1.y); 15 | } 16 | 17 | export function getRandomInt(min, max) { 18 | return Math.floor(Math.random() * (max - min)) + min; 19 | } 20 | 21 | -------------------------------------------------------------------------------- /src/rooms/Hall.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import * as panoballs from '../stations/PanoBalls.js'; 3 | import * as paintings from '../stations/Paintings.js'; 4 | import * as newsticker from '../stations/NewsTicker.js'; 5 | import * as xylophone from '../stations/Xylophone.js'; 6 | import * as graffiti from '../stations/Graffiti.js'; 7 | import * as infopanels from '../stations/InfoPanels.js'; 8 | 9 | var 10 | scene, 11 | hall, 12 | teleportFloor, 13 | fader, 14 | doors = [], 15 | objectMaterials, 16 | controllers, 17 | auxVec = new THREE.Vector3(); 18 | 19 | function createDoorMaterial(ctx) { 20 | return new THREE.ShaderMaterial({ 21 | uniforms: { 22 | time: {value: 0}, 23 | selected: {value: 0}, 24 | tex: {value: ctx.assets['doorfx_tex']} 25 | }, 26 | vertexShader: ctx.shaders.basic_vert, 27 | fragmentShader: ctx.shaders.door_frag 28 | }); 29 | } 30 | 31 | export function setup(ctx) { 32 | const assets = ctx.assets; 33 | scene = new THREE.Object3D(); 34 | 35 | // setup hall model 36 | 37 | const hallLightmapTex = assets['lightmap_tex']; 38 | const skyTex = assets['sky_tex']; 39 | const cloudsTex = assets['clouds_tex']; 40 | const foxrTex = assets['foxr_tex']; 41 | const newstickerTex = assets['newsticker_tex']; 42 | const mozillamrTex = assets['mozillamr_tex']; 43 | 44 | const hallMaterial = new THREE.MeshBasicMaterial({map: hallLightmapTex}); 45 | 46 | objectMaterials = { 47 | 'hall': hallMaterial, 48 | 'screen': new THREE.MeshBasicMaterial({map: newstickerTex}), 49 | 'xylophone': hallMaterial, 50 | 'xylostick-left': hallMaterial, 51 | 'xylostick-right': hallMaterial, 52 | 'xylostickball-left': hallMaterial.clone(), 53 | 'xylostickball-right': hallMaterial.clone(), 54 | 'lightpanels': new THREE.MeshBasicMaterial(), 55 | 'doorA': createDoorMaterial(ctx), 56 | 'doorB': createDoorMaterial(ctx), 57 | 'doorC': createDoorMaterial(ctx), 58 | 'doorD': createDoorMaterial(ctx), 59 | 'sky': new THREE.MeshBasicMaterial({map: skyTex}), 60 | 'clouds': new THREE.MeshBasicMaterial({map: cloudsTex, transparent: true}), 61 | 'foxr': new THREE.MeshBasicMaterial({map: foxrTex, transparent: true}), 62 | 'mozillamr': new THREE.MeshBasicMaterial({map: mozillamrTex, transparent: true}), 63 | }; 64 | 65 | hall = assets['hall_model'].scene; 66 | hall.traverse(o => { 67 | if (o.name == 'teleport') { 68 | teleportFloor = o; 69 | //o.visible = false; 70 | o.material.visible = false; 71 | return; 72 | } else if (o.name.startsWith('door')) { 73 | doors.push(o); 74 | } 75 | 76 | if (o.type == 'Mesh' && objectMaterials[o.name]) { 77 | o.material = objectMaterials[o.name]; 78 | } 79 | }); 80 | 81 | paintings.setup(ctx, hall); 82 | xylophone.setup(ctx, hall); 83 | graffiti.setup(ctx, hall); 84 | newsticker.setup(ctx, hall); 85 | panoballs.setup(ctx, hall); 86 | infopanels.setup(ctx, hall); 87 | 88 | ctx.raycontrol.addState('teleport', { 89 | colliderMesh: teleportFloor, 90 | onHover: (intersection, active) => { 91 | ctx.teleport.onHover(intersection.point, active); 92 | }, 93 | onHoverLeave: () => { 94 | ctx.teleport.onHoverLeave(); 95 | }, 96 | onSelectStart: (intersection, e) => { 97 | ctx.teleport.onSelectStart(e); 98 | }, 99 | onSelectEnd: (intersection) => { 100 | ctx.teleport.onSelectEnd(intersection.point); 101 | } 102 | }); 103 | 104 | ctx.raycontrol.addState('doors', { 105 | colliderMesh: doors, 106 | onHover: (intersection, active) => { 107 | const scale = intersection.object.scale; 108 | scale.z = Math.min(scale.z + 0.05 * (5.5 - scale.z), 5); 109 | }, 110 | onHoverLeave: (intersection) => { 111 | }, 112 | onSelectStart: (intersection) => { 113 | const transitions = { 114 | doorA: 1, 115 | doorB: 2, 116 | doorC: 3, 117 | doorD: 4 118 | }; 119 | ctx.goto = transitions[intersection.object.name]; 120 | }, 121 | onSelectEnd: (intersection) => {} 122 | }); 123 | 124 | // fade camera to black on walls 125 | fader = new THREE.Mesh( 126 | new THREE.PlaneBufferGeometry(), 127 | new THREE.MeshBasicMaterial({color: 0x000000, transparent: true, depthTest: false}) 128 | ); 129 | fader.position.z = -0.1; 130 | fader.material.opacity = 0; 131 | 132 | scene.add(hall); 133 | ctx.camera.add(fader); 134 | } 135 | 136 | export function enter(ctx) { 137 | ctx.systemsGroup['roomHall'].play(); 138 | ctx.renderer.setClearColor( 0xC0DFFB ); 139 | ctx.scene.add(scene); 140 | 141 | xylophone.enter(ctx); 142 | graffiti.enter(ctx); 143 | infopanels.enter(ctx); 144 | ctx.raycontrol.activateState('doors'); 145 | ctx.raycontrol.activateState('teleport'); 146 | paintings.enter(ctx); 147 | panoballs.enter(ctx); 148 | } 149 | 150 | export function exit(ctx) { 151 | ctx.systemsGroup['roomHall'].stop(); 152 | ctx.scene.remove(scene); 153 | 154 | ctx.raycontrol.deactivateState('doors'); 155 | ctx.raycontrol.deactivateState('teleport'); 156 | 157 | xylophone.exit(ctx); 158 | } 159 | 160 | export function execute(ctx, delta, time) { 161 | panoballs.execute(ctx, delta, time); 162 | paintings.execute(ctx, delta, time); 163 | xylophone.execute(ctx, delta, time, controllers); 164 | graffiti.execute(ctx, delta, time); 165 | newsticker.execute(ctx, delta, time); 166 | infopanels.execute(ctx, delta, time); 167 | updateUniforms(time); 168 | //checkCameraBoundaries(ctx); 169 | 170 | for (var i = 0; i < doors.length; i++) { 171 | if (doors[i].scale.z > 1) { 172 | doors[i].scale.z = Math.max(doors[i].scale.z - delta * doors[i].scale.z, 1); 173 | } 174 | } 175 | 176 | } 177 | 178 | function updateUniforms(time) { 179 | objectMaterials.doorA.uniforms.time.value = time; 180 | objectMaterials.doorB.uniforms.time.value = time; 181 | objectMaterials.doorC.uniforms.time.value = time; 182 | objectMaterials.doorD.uniforms.time.value = time; 183 | objectMaterials.doorD.uniforms.selected.value = 1; //test 184 | panoballs.updateUniforms(time); 185 | } 186 | 187 | function checkCameraBoundaries(ctx) { 188 | auxVec.copy(ctx.camera.position).add(ctx.cameraRig.position); 189 | const cam = auxVec; 190 | const margin = 0.25; 191 | var fade = 0; 192 | if (cam.y < margin) { fade = 1 - (cam.y / margin); } 193 | else if (cam.x < -5.4) { fade = (-cam.x - 5.4) / margin; } 194 | else if (cam.x > 8) { fade = (cam.x - 8) / margin; } 195 | else if (cam.z < -6.45) { fade = (-cam.z - 6.45) / margin; } 196 | else if (cam.z > 6.4) { fade = (cam.z - 6.4) / margin; } 197 | fader.material.opacity = Math.min(1, Math.max(0, fade)); 198 | } 199 | -------------------------------------------------------------------------------- /src/rooms/Panorama.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { Text, Position, ParentObject3D } from '../components/index.js'; 3 | 4 | var pano = null, context, panel, panelText; 5 | 6 | const NUM_PANOS = 5; 7 | 8 | const DATA = [ 9 | 'Tiger and Turtle - Magic Mountain\nArt installation in Agerpark, Germany.', 10 | 'Hiking trail at Lake Byllesby Regional Park near Cannon Falls, USA.', 11 | 'Dellwiger Bach natural reserve in Dortmund, Germany.', 12 | 'Zapporthorn summit in Lepontine Alps, Switzerland.', 13 | 'Ruin of romanesque Paulinzella abbey (1106) in Thuringia, Germany.', 14 | ]; 15 | 16 | var panoMaterials = []; 17 | 18 | export function setup(ctx) { 19 | const assets = ctx.assets; 20 | const geometry = new THREE.SphereBufferGeometry(500, 60, 40); 21 | for (var i = 0; i < NUM_PANOS; i++) { 22 | const panoName = 'pano'+(i + 2); 23 | panoMaterials[i] = new THREE.MeshBasicMaterial( { map: assets[panoName], side: THREE.BackSide }); 24 | } 25 | pano = new THREE.Mesh(geometry, panoMaterials[0]); 26 | 27 | panel = assets['hall_model'].scene.getObjectByName('infopanel'); 28 | panel.material = new THREE.MeshBasicMaterial({color: 0x040404}); 29 | panel.position.set(0, 0.1, 0); 30 | panel.parent.remove(panel); 31 | 32 | panelText = ctx.world.createEntity(); 33 | panelText 34 | .addComponent(Text, { 35 | color: '#ffffff', 36 | fontSize: 0.02, 37 | anchor: 'left', 38 | textAlign: 'left', 39 | baseline: 'center', 40 | maxWidth: 0.34, 41 | lineHeight: 1.3, 42 | text: DATA[i], 43 | }) 44 | .addComponent(ParentObject3D, {value: panel}) 45 | .addComponent(Position, {x: -0.17, y: 0.003, z: 0.01}); 46 | 47 | ctx.raycontrol.addState('panorama', { 48 | raycaster: false, 49 | onSelectEnd: onSelectEnd 50 | }); 51 | } 52 | 53 | export function enter(ctx) { 54 | ctx.renderer.setClearColor(0x000000); 55 | 56 | const room = ctx.room - 5; 57 | panelText.getMutableComponent(Text).text = DATA[room]; 58 | pano.material = panoMaterials[room]; 59 | 60 | ctx.scene.add(pano); 61 | 62 | ctx.controllers[1].add(panel); 63 | 64 | ctx.raycontrol.activateState('panorama'); 65 | 66 | context = ctx; 67 | } 68 | 69 | export function exit(ctx) { 70 | ctx.scene.remove(pano); 71 | ctx.controllers[1].remove(panel); 72 | 73 | ctx.raycontrol.deactivateState('panorama'); 74 | } 75 | 76 | export function execute(ctx, delta, time) { 77 | } 78 | 79 | export function onSelectEnd(evt) { 80 | context.goto = 0; 81 | } 82 | -------------------------------------------------------------------------------- /src/rooms/PanoramaStereo.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | var panoL, panoR, context; 3 | 4 | export function setup(ctx) { 5 | const assets = ctx.assets; 6 | const geometry = new THREE.SphereBufferGeometry(500, 60, 40); 7 | const materialL = new THREE.MeshBasicMaterial( { map: assets['stereopanoR'], side: THREE.BackSide } ); 8 | const materialR = new THREE.MeshBasicMaterial( { map: assets['stereopanoL'], side: THREE.BackSide } ); 9 | panoL = new THREE.Mesh(geometry, materialL); 10 | panoL.layers.set(1); 11 | panoR = new THREE.Mesh(geometry, materialR); 12 | panoR.layers.set(2); 13 | 14 | ctx.raycontrol.addState('panoramaStereo', { 15 | raycaster: false, 16 | onSelectEnd: onSelectEnd 17 | }); 18 | } 19 | 20 | export function enter(ctx) { 21 | ctx.renderer.setClearColor(0x000000); 22 | ctx.scene.add(panoL); 23 | ctx.scene.add(panoR); 24 | ctx.camera.layers.enable(1); 25 | context = ctx; 26 | 27 | ctx.raycontrol.activateState('panoramaStereo'); 28 | } 29 | 30 | export function exit(ctx) { 31 | ctx.scene.remove(panoL); 32 | ctx.scene.remove(panoR); 33 | ctx.camera.layers.disable(1); 34 | ctx.raycontrol.deactivateState('panoramaStereo'); 35 | } 36 | 37 | export function execute(ctx, delta, time) { 38 | } 39 | 40 | export function onSelectEnd(evt) { 41 | context.goto = 0; 42 | } 43 | 44 | -------------------------------------------------------------------------------- /src/rooms/PhotogrammetryObject.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | var scene, doorMaterial, door; 3 | 4 | function createDoorMaterial(ctx) { 5 | return new THREE.ShaderMaterial({ 6 | uniforms: { 7 | time: {value: 0}, 8 | selected: {value: 0}, 9 | tex: {value: ctx.assets['doorfx_tex']} 10 | }, 11 | vertexShader: ctx.shaders.basic_vert, 12 | fragmentShader: ctx.shaders.door_frag 13 | }); 14 | } 15 | 16 | export function setup(ctx) { 17 | const assets = ctx.assets; 18 | scene = assets['pg_object_model'].scene; 19 | scene.rotation.y = -Math.PI / 2; 20 | 21 | scene.getObjectByName('object').material = 22 | new THREE.MeshBasicMaterial({map: assets['pg_object_tex']}); 23 | scene.getObjectByName('floor').material = 24 | new THREE.MeshBasicMaterial({map: assets['pg_floor_tex'], lightMap: assets['pg_floor_lm_tex']}); 25 | scene.getObjectByName('bg').material = 26 | new THREE.MeshBasicMaterial({map: assets['pg_bg_tex']}); 27 | scene.getObjectByName('flare').material = 28 | new THREE.MeshBasicMaterial({map: assets['pg_flare_tex'], blending: THREE.AdditiveBlending}); 29 | scene.getObjectByName('panel').material = 30 | new THREE.MeshBasicMaterial({map: assets['pg_panel_tex']}); 31 | scene.getObjectByName('door_frame').material = 32 | new THREE.MeshBasicMaterial({map: assets['pg_door_lm_tex']}); 33 | 34 | doorMaterial = createDoorMaterial(ctx); 35 | door = scene.getObjectByName('door'); 36 | door.material = doorMaterial; 37 | 38 | scene.getObjectByName('teleport').visible = false; 39 | 40 | ctx.raycontrol.addState('doorPhotogrammetry', { 41 | colliderMesh: scene.getObjectByName('door'), 42 | onHover: (intersection, active) => { 43 | //teleport.onHover(intersection.point, active); 44 | const scale = intersection.object.scale; 45 | scale.z = Math.min(scale.z + 0.05 * (2 - door.scale.z), 1.5); 46 | }, 47 | onHoverLeave: () => { 48 | //teleport.onHoverLeave(); 49 | }, 50 | onSelectStart: (intersection, e) => { 51 | ctx.goto = 0; 52 | //teleport.onSelectStart(e); 53 | }, 54 | onSelectEnd: (intersection) => { 55 | //teleport.onSelectEnd(intersection.point); 56 | } 57 | }); 58 | 59 | let teleport = scene.getObjectByName('teleport'); 60 | teleport.visible = true; 61 | teleport.material.visible = false; 62 | ctx.raycontrol.addState('teleportPhotogrammetry', { 63 | colliderMesh: teleport, 64 | onHover: (intersection, active) => { 65 | ctx.teleport.onHover(intersection.point, active); 66 | }, 67 | onHoverLeave: () => { 68 | ctx.teleport.onHoverLeave(); 69 | }, 70 | onSelectStart: (intersection, e) => { 71 | ctx.teleport.onSelectStart(e); 72 | }, 73 | onSelectEnd: (intersection) => { 74 | ctx.teleport.onSelectEnd(intersection.point); 75 | } 76 | }); 77 | } 78 | 79 | export function enter(ctx) { 80 | ctx.renderer.setClearColor(0x000000); 81 | ctx.scene.add(scene); 82 | ctx.raycontrol.activateState('doorPhotogrammetry'); 83 | ctx.raycontrol.activateState('teleportPhotogrammetry'); 84 | } 85 | 86 | export function exit(ctx) { 87 | ctx.raycontrol.deactivateState('doorPhotogrammetry'); 88 | ctx.raycontrol.deactivateState('teleportPhotogrammetry'); 89 | 90 | ctx.scene.remove(scene); 91 | } 92 | 93 | export function execute(ctx, delta, time) { 94 | doorMaterial.uniforms.time.value = time; 95 | 96 | if (door.scale.z > 0.5) { 97 | door.scale.z = Math.max(door.scale.z - delta * door.scale.z, 0.5); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/rooms/Sound.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | var scene, listener, timeout, mixer, door, doorMaterial; 3 | 4 | const soundNames = [ 5 | 'bells', 6 | 'horn', 7 | 'cowbell', 8 | 'guiro', 9 | 'mandolin', 10 | 'squeaker', 11 | 'train', 12 | 'whistle', 13 | 'motorhorn', 14 | 'surdo', 15 | 'trumpet', 16 | ]; 17 | 18 | var sounds = {}; 19 | soundNames.forEach( i => { sounds[i] = {animations: [], mesh: null, player: null, shadow: null} }) 20 | 21 | const MAX_REPETITIONS = 3; 22 | var repetitions = MAX_REPETITIONS - 1; 23 | 24 | function createDoorMaterial(ctx) { 25 | return new THREE.ShaderMaterial({ 26 | uniforms: { 27 | time: {value: 0}, 28 | selected: {value: 0}, 29 | tex: {value: ctx.assets['doorfx_tex']} 30 | }, 31 | vertexShader: ctx.shaders.basic_vert, 32 | fragmentShader: ctx.shaders.door_frag 33 | }); 34 | } 35 | 36 | export function setup(ctx) { 37 | const assets = ctx.assets; 38 | scene = assets['sound_model'].scene; 39 | door = assets['sound_door_model'].scene; 40 | 41 | door.getObjectByName('door_frame').material = 42 | new THREE.MeshBasicMaterial({map: assets['sound_door_lm_tex']}); 43 | 44 | doorMaterial = createDoorMaterial(ctx); 45 | door.getObjectByName('door').material = doorMaterial; 46 | 47 | door.scale.set(0.5, 0.5, 0.5); 48 | door.position.set(0.4, 0.6, 1); 49 | door.rotation.set(0, 0.4, 0); 50 | 51 | listener = new THREE.AudioListener(); 52 | 53 | mixer = new THREE.AnimationMixer(scene); 54 | 55 | for (let id in sounds) { 56 | const mesh = scene.getObjectByName(id); 57 | if (!mesh) { continue; } 58 | 59 | const sound = new THREE.PositionalAudio(listener); 60 | const audioLoader = new THREE.AudioLoader(); 61 | audioLoader.load('assets/ogg/' + id + '.ogg', buffer => { 62 | sound.setBuffer(buffer); 63 | //sound.setRefDistance(20); 64 | }); 65 | 66 | sounds[id].player = sound; 67 | sounds[id].mesh = mesh; 68 | mesh.visible = false; 69 | mesh.add(sound); 70 | 71 | const clip = THREE.AnimationClip.findByName(assets['sound_model'].animations, id); 72 | if (clip) { 73 | const action = mixer.clipAction(clip, mesh); 74 | action.loop = THREE.LoopOnce; 75 | sounds[id].animations.push(action); 76 | } 77 | 78 | for (let j = 0; j < mesh.children.length; j++) { 79 | const obj = mesh.children[j]; 80 | const clip = THREE.AnimationClip.findByName(assets['sound_model'].animations, `${id}_${obj.name}`); 81 | if (!clip) { continue; } 82 | const action = mixer.clipAction(clip, mesh); 83 | action.loop = THREE.LoopOnce; 84 | sounds[id].animations.push(action); 85 | } 86 | 87 | let shadow = new THREE.Mesh( 88 | new THREE.PlaneBufferGeometry(3, 3), 89 | new THREE.MeshBasicMaterial({ 90 | color: mesh.children[0].material.color, 91 | map: assets['sound_shadow_tex'], 92 | transparent: true, 93 | opacity: 0, 94 | depthTest: false, 95 | blending: THREE.AdditiveBlending 96 | }) 97 | ); 98 | shadow.position.set(mesh.position.x, 0.001, mesh.position.z); 99 | shadow.rotation.x = -Math.PI / 2; 100 | scene.add(shadow); 101 | sounds[id].shadow = shadow; 102 | } 103 | 104 | ctx.raycontrol.addState('sound', { 105 | colliderMesh: door.getObjectByName('door'), 106 | onHover: (intersection, active) => { 107 | //teleport.onHover(intersection.point, active); 108 | const scale = intersection.object.scale; 109 | scale.z = Math.min(scale.z + 0.05 * (2 - door.scale.z), 1.5); 110 | }, 111 | onHoverLeave: () => { 112 | //teleport.onHoverLeave(); 113 | }, 114 | onSelectStart: (intersection, e) => { 115 | ctx.goto = 0; 116 | //teleport.onSelectStart(e); 117 | }, 118 | onSelectEnd: (intersection) => { 119 | //teleport.onSelectEnd(intersection.point); 120 | } 121 | }); 122 | 123 | const floorTexture = assets['grid_tex']; 124 | const floor = new THREE.Mesh( 125 | new THREE.PlaneBufferGeometry(20, 20), 126 | new THREE.MeshBasicMaterial({map: floorTexture}) 127 | ); 128 | scene.add(floor); 129 | floor.rotation.x = -Math.PI / 2; 130 | 131 | ctx.raycontrol.addState('teleportSound', { 132 | colliderMesh: floor, 133 | onHover: (intersection, active) => { 134 | ctx.teleport.onHover(intersection.point, active); 135 | }, 136 | onHoverLeave: () => { 137 | ctx.teleport.onHoverLeave(); 138 | }, 139 | onSelectStart: (intersection, e) => { 140 | ctx.teleport.onSelectStart(e); 141 | }, 142 | onSelectEnd: (intersection) => { 143 | ctx.teleport.onSelectEnd(intersection.point); 144 | } 145 | }); 146 | } 147 | 148 | var currentSound = -1; 149 | 150 | function playSound() { 151 | let sound; 152 | if (currentSound >= 0) { 153 | sound = sounds[soundNames[currentSound]]; 154 | sound.player.stop(); 155 | if (sound.animations.length) { 156 | sound.mesh.visible = false; 157 | sound.animations.forEach( i => {i.stop()}); 158 | } 159 | } 160 | repetitions ++; 161 | if (repetitions == MAX_REPETITIONS) { 162 | repetitions = 0; 163 | // get next sound 164 | do { 165 | currentSound = (currentSound + 1) % soundNames.length; 166 | sound = sounds[soundNames[currentSound]]; 167 | } while(!sound.mesh); 168 | } 169 | 170 | sound.player.play(); 171 | if (sound.animations.length) { 172 | sound.mesh.visible = true; 173 | sound.animations.forEach( i => {i.play()}); 174 | } 175 | sound.shadow.material.opacity = 1; 176 | timeout = setTimeout(playSound, 2000); 177 | } 178 | 179 | export function enter(ctx) { 180 | ctx.renderer.setClearColor(0x000000); 181 | ctx.scene.add(scene); 182 | ctx.scene.add(door); 183 | ctx.camera.add(listener); 184 | 185 | timeout = setTimeout(playSound, 2000); 186 | ctx.raycontrol.activateState('teleportSound'); 187 | ctx.raycontrol.activateState('sound'); 188 | } 189 | 190 | export function exit(ctx) { 191 | ctx.scene.remove(scene); 192 | ctx.scene.remove(door); 193 | ctx.camera.remove(listener); 194 | ctx.raycontrol.deactivateState('teleportSound'); 195 | ctx.raycontrol.deactivateState('sound'); 196 | clearTimeout(timeout); 197 | } 198 | 199 | export function execute(ctx, delta, time) { 200 | mixer.update(delta); 201 | const sound = sounds[soundNames[currentSound]]; 202 | if (sound && sound.shadow.material.opacity > 0) { 203 | sound.shadow.material.opacity -= delta * 0.5; 204 | } 205 | doorMaterial.uniforms.time.value = time; 206 | 207 | if (door.scale.z > 0.5) { 208 | door.scale.z = Math.max(door.scale.z - delta * door.scale.z, 0.5); 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/rooms/Vertigo.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | var scene, doorMaterial, door; 3 | 4 | function createDoorMaterial(ctx) { 5 | return new THREE.ShaderMaterial({ 6 | uniforms: { 7 | time: {value: 0}, 8 | selected: {value: 0}, 9 | tex: {value: ctx.assets['doorfx_tex']} 10 | }, 11 | vertexShader: ctx.shaders.basic_vert, 12 | fragmentShader: ctx.shaders.door_frag 13 | }); 14 | } 15 | 16 | export function setup(ctx) { 17 | const assets = ctx.assets; 18 | var texture = assets['checkboard_tex']; 19 | 20 | var lightmap = assets['vertigo_lm_tex']; 21 | const material = new THREE.MeshBasicMaterial({color: 0xffffff, map: texture, lightMap: lightmap} ); 22 | 23 | scene = assets['vertigo_model'].scene; 24 | scene.getObjectByName('city').material = material; 25 | scene.getObjectByName('teleport').visible = false; 26 | 27 | scene.getObjectByName('door_frame').material = 28 | new THREE.MeshBasicMaterial({map: assets['vertigo_door_lm_tex']}); 29 | doorMaterial = createDoorMaterial(ctx); 30 | door = scene.getObjectByName('door'); 31 | door.material = doorMaterial; 32 | 33 | ctx.raycontrol.addState('doorVertigo', { 34 | colliderMesh: scene.getObjectByName('door'), 35 | onHover: (intersection, active) => { 36 | //teleport.onHover(intersection.point, active); 37 | const scale = intersection.object.scale; 38 | scale.z = Math.min(scale.z + 0.02 * (2 - door.scale.z), 0.8); 39 | }, 40 | onHoverLeave: () => { 41 | //teleport.onHoverLeave(); 42 | }, 43 | onSelectStart: (intersection, e) => { 44 | ctx.goto = 0; 45 | //teleport.onSelectStart(e); 46 | }, 47 | onSelectEnd: (intersection) => { 48 | //teleport.onSelectEnd(intersection.point); 49 | } 50 | }); 51 | 52 | let teleport = scene.getObjectByName('teleport'); 53 | teleport.visible = true; 54 | teleport.material.visible = false; 55 | ctx.raycontrol.addState('teleportVertigo', { 56 | colliderMesh: teleport, 57 | onHover: (intersection, active) => { 58 | ctx.teleport.onHover(intersection.point, active); 59 | }, 60 | onHoverLeave: () => { 61 | ctx.teleport.onHoverLeave(); 62 | }, 63 | onSelectStart: (intersection, e) => { 64 | ctx.teleport.onSelectStart(e); 65 | }, 66 | onSelectEnd: (intersection) => { 67 | ctx.teleport.onSelectEnd(intersection.point); 68 | } 69 | }); 70 | } 71 | 72 | export function enter(ctx) { 73 | ctx.renderer.setClearColor(0x677FA7); 74 | ctx.scene.add(scene); 75 | ctx.scene.parent.fog = new THREE.FogExp2(0x677FA7, 0.004); 76 | //ctx.cameraRig.position.set(0,0,0); 77 | 78 | ctx.raycontrol.activateState('teleportVertigo'); 79 | ctx.raycontrol.activateState('doorVertigo'); 80 | } 81 | 82 | export function exit(ctx) { 83 | ctx.scene.remove(scene); 84 | ctx.scene.parent.fog = null; 85 | 86 | ctx.raycontrol.deactivateState('teleportVertigo'); 87 | ctx.raycontrol.deactivateState('doorVertigo'); 88 | } 89 | 90 | export function execute(ctx, delta, time) { 91 | doorMaterial.uniforms.time.value = time; 92 | 93 | if (door.scale.z > 0.2) { 94 | door.scale.z = Math.max(door.scale.z - delta * door.scale.z, 0.2); 95 | } 96 | } 97 | 98 | -------------------------------------------------------------------------------- /src/shaders/basic.vert.glsl: -------------------------------------------------------------------------------- 1 | varying vec2 vUv; 2 | varying vec3 vPosition; 3 | void main() 4 | { 5 | vUv = uv; 6 | vPosition = position; 7 | vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); 8 | gl_Position = projectionMatrix * mvPosition; 9 | } 10 | -------------------------------------------------------------------------------- /src/shaders/beam.frag.glsl: -------------------------------------------------------------------------------- 1 | uniform float time; 2 | uniform sampler2D tex; 3 | varying vec2 vUv; 4 | 5 | void main( void ) { 6 | float t = time; 7 | vec4 col = texture2D(tex, vec2(vUv.x, vUv.y * 3.0 + time * 2.0)); 8 | col *= vUv.y; 9 | gl_FragColor = col; 10 | } 11 | -------------------------------------------------------------------------------- /src/shaders/door.frag.glsl: -------------------------------------------------------------------------------- 1 | uniform float time; 2 | uniform float selected; 3 | uniform sampler2D tex; 4 | varying vec2 vUv; 5 | 6 | void main( void ) { 7 | float t = time; 8 | vec2 uv = vUv * 2.0 - 1.0; 9 | vec2 puv = vec2(length(uv.xy), atan(uv.x, uv.y)); 10 | vec4 col = texture2D(tex, vec2(log(puv.x) + t / 5.0, puv.y / 3.1415926 )); 11 | float glow = (1.0 - puv.x) * (0.5 + (sin(t) + 2.0 ) / 4.0); 12 | // blue glow 13 | col += vec4(118.0/255.0, 144.0/255.0, 219.0/255.0, 1.0) * (0.4 + glow * 1.0); 14 | // white glow 15 | col += vec4(0.2) * smoothstep(0.0, 2.0, glow * glow); 16 | gl_FragColor = col; 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/shaders/panoball.frag.glsl: -------------------------------------------------------------------------------- 1 | uniform sampler2D tex, texfx; 2 | uniform float time; 3 | uniform float selected; 4 | varying vec2 vUv; 5 | varying vec3 vPosition; 6 | varying vec3 vNormal; 7 | varying vec3 vWorldPos; 8 | 9 | 10 | void main( void ) { 11 | vec2 uv = vUv; 12 | //uv.y = 1.0 - uv.y; 13 | 14 | vec3 eye = normalize(cameraPosition - vWorldPos); 15 | float fresnel = abs(dot(eye, vNormal)); 16 | float shift = pow((1.0 - fresnel), 4.0) * 0.05; 17 | 18 | vec3 col = vec3( 19 | texture2D(tex, uv - shift).r, 20 | texture2D(tex, uv).g, 21 | texture2D(tex, uv + shift).b 22 | ); 23 | 24 | col = mix(col * 0.7, vec3(1.0), 0.7 - fresnel); 25 | 26 | col += selected * 0.3; 27 | 28 | float t = time * 0.4 + vPosition.x + vPosition.z; 29 | uv = vec2(vUv.x + t * 0.2, vUv.y + t); 30 | vec3 fx = texture2D(texfx, uv).rgb * 0.4; 31 | 32 | 33 | gl_FragColor = vec4(col + fx, 1.0); 34 | } 35 | -------------------------------------------------------------------------------- /src/shaders/panoball.vert.glsl: -------------------------------------------------------------------------------- 1 | varying vec2 vUv; 2 | varying vec3 vPosition; 3 | varying vec3 vNormal; 4 | varying vec3 vWorldPos; 5 | uniform float time; 6 | uniform float selected; 7 | 8 | mat4 inverse(mat4 m) { 9 | float 10 | a00 = m[0][0], a01 = m[0][1], a02 = m[0][2], a03 = m[0][3], 11 | a10 = m[1][0], a11 = m[1][1], a12 = m[1][2], a13 = m[1][3], 12 | a20 = m[2][0], a21 = m[2][1], a22 = m[2][2], a23 = m[2][3], 13 | a30 = m[3][0], a31 = m[3][1], a32 = m[3][2], a33 = m[3][3], 14 | 15 | b00 = a00 * a11 - a01 * a10, 16 | b01 = a00 * a12 - a02 * a10, 17 | b02 = a00 * a13 - a03 * a10, 18 | b03 = a01 * a12 - a02 * a11, 19 | b04 = a01 * a13 - a03 * a11, 20 | b05 = a02 * a13 - a03 * a12, 21 | b06 = a20 * a31 - a21 * a30, 22 | b07 = a20 * a32 - a22 * a30, 23 | b08 = a20 * a33 - a23 * a30, 24 | b09 = a21 * a32 - a22 * a31, 25 | b10 = a21 * a33 - a23 * a31, 26 | b11 = a22 * a33 - a23 * a32, 27 | 28 | det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06; 29 | 30 | return mat4( 31 | a11 * b11 - a12 * b10 + a13 * b09, 32 | a02 * b10 - a01 * b11 - a03 * b09, 33 | a31 * b05 - a32 * b04 + a33 * b03, 34 | a22 * b04 - a21 * b05 - a23 * b03, 35 | a12 * b08 - a10 * b11 - a13 * b07, 36 | a00 * b11 - a02 * b08 + a03 * b07, 37 | a32 * b02 - a30 * b05 - a33 * b01, 38 | a20 * b05 - a22 * b02 + a23 * b01, 39 | a10 * b10 - a11 * b08 + a13 * b06, 40 | a01 * b08 - a00 * b10 - a03 * b06, 41 | a30 * b04 - a31 * b02 + a33 * b00, 42 | a21 * b02 - a20 * b04 - a23 * b00, 43 | a11 * b07 - a10 * b09 - a12 * b06, 44 | a00 * b09 - a01 * b07 + a02 * b06, 45 | a31 * b01 - a30 * b03 - a32 * b00, 46 | a20 * b03 - a21 * b01 + a22 * b00) / det; 47 | } 48 | 49 | 50 | mat4 transpose(in mat4 m) { 51 | vec4 i0 = m[0]; 52 | vec4 i1 = m[1]; 53 | vec4 i2 = m[2]; 54 | vec4 i3 = m[3]; 55 | 56 | return mat4( 57 | vec4(i0.x, i1.x, i2.x, i3.x), 58 | vec4(i0.y, i1.y, i2.y, i3.y), 59 | vec4(i0.z, i1.z, i2.z, i3.z), 60 | vec4(i0.w, i1.w, i2.w, i3.w) 61 | ); 62 | } 63 | 64 | void main() 65 | { 66 | vUv = uv; 67 | 68 | vPosition = position; 69 | 70 | vec3 offset = vec3( 71 | sin(position.x * 50.0 + time), 72 | sin(position.y * 10.0 + time * 2.0), 73 | cos(position.z * 40.0 + time) 74 | ) * 0.003; 75 | 76 | vPosition *= 1.0 + selected * 0.2; 77 | 78 | vNormal = normalize(inverse(transpose(modelMatrix)) * vec4(normalize(normal), 1.0)).xyz; 79 | vWorldPos = (modelMatrix * vec4(vPosition, 1.0)).xyz; 80 | 81 | vec4 mvPosition = modelViewMatrix * vec4(vPosition + offset, 1.0); 82 | gl_Position = projectionMatrix * mvPosition; 83 | } 84 | -------------------------------------------------------------------------------- /src/shaders/zoom.frag.glsl: -------------------------------------------------------------------------------- 1 | uniform float time; 2 | uniform sampler2D tex; 3 | uniform vec2 zoomPos; 4 | uniform float zoomAmount; 5 | uniform float zoomRatio; 6 | varying vec2 vUv; 7 | 8 | void main( void ) { 9 | float t = time; 10 | vec2 uv = vec2(vUv.x - 0.5, (1.0 - vUv.y) - 0.5); 11 | vec2 texUv = uv * vec2(zoomRatio, 1.0); 12 | vec4 col = texture2D(tex, zoomPos + texUv * zoomAmount); 13 | float dist = length(uv) * 2.0; 14 | col.a = smoothstep(0.0, 0.1, 1.0 - dist); 15 | float aura = smoothstep(0.80, 1.0, dist); 16 | col.rgb += aura * 0.3; 17 | gl_FragColor = col; 18 | } 19 | -------------------------------------------------------------------------------- /src/stations/Graffiti.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import ColorWheel from '../lib/ColorWheel'; 3 | import {Area, AreaReactor, AreaChecker, Object3D, BoundingBox, ParentObject3D, DebugHelper} from '../components/index'; 4 | import {angleBetween, getRandomInt} from '../lib/utils.js'; 5 | 6 | var colorWheel; 7 | 8 | export function enter(ctx) { 9 | //ctx.raycontrol.activateState('graffiti'); 10 | if (colorWheel) { 11 | colorWheel.enter(); 12 | } 13 | } 14 | 15 | var material, wall, drawContext; 16 | var lastPosition = new THREE.Vector2(); 17 | var brushImg, canvasTmp, ctxTmp; 18 | 19 | var lastController; 20 | var paintImg = new Image(); 21 | 22 | function colorize(r, g, b) { 23 | ctxTmp.clearRect(0, 0, canvasTmp.width, canvasTmp.height); 24 | ctxTmp.drawImage(brushImg, 0, 0); 25 | let imgData = ctxTmp.getImageData(0, 0, canvasTmp.width, canvasTmp.height); 26 | for (var t = 0; t < imgData.data.length; t += 4) { 27 | imgData.data[t]= r * imgData.data[t] / 255; 28 | imgData.data[t + 1]= g * imgData.data[t + 1] / 255; 29 | imgData.data[t + 2]= b * imgData.data[t + 2] / 255; 30 | } 31 | ctxTmp.putImageData(imgData,0,0); 32 | paintImg.src = canvasTmp.toDataURL(); 33 | } 34 | 35 | export function setup(ctx, hall) { 36 | 37 | let area = ctx.world.createEntity(); 38 | area.name = 'area'; 39 | area 40 | .addComponent(BoundingBox) 41 | //.addComponent(DebugHelper) 42 | .addComponent(ParentObject3D, {value: hall}) 43 | .addComponent(Area); 44 | 45 | let component = area.getMutableComponent(BoundingBox); 46 | component.min.set(-5, 0, 4.4); 47 | component.max.set(3, 3, 7); 48 | 49 | let checker = ctx.world.createEntity(); 50 | let checkerSpray = ctx.world.createEntity(); 51 | let spray = ctx.assets['spray_model'].scene; 52 | 53 | function attachSprayCan(controllerData) { 54 | let controller = controllerData.controller; 55 | const handedness = controllerData.inputSource.handedness; 56 | let listener = new THREE.AudioListener(); 57 | 58 | const sound = new THREE.PositionalAudio(listener); 59 | sound.loop = true; 60 | const audioLoader = new THREE.AudioLoader(); 61 | audioLoader.load('assets/ogg/spray.ogg', buffer => { 62 | sound.setBuffer(buffer); 63 | sound.name = 'spraySound'; 64 | controller.add(sound); 65 | }); 66 | 67 | spray.getObjectByName('spraycan').geometry.rotateY(handedness === "right" ? Math.PI / 2 : -Math.PI / 2); 68 | spray.name = 'spray'; 69 | spray.visible = false; 70 | const sprayTex = ctx.assets['spray_tex']; 71 | spray.getObjectByName('spraycan').material = new THREE.MeshPhongMaterial({map: sprayTex}); 72 | spray.getObjectByName('spraycolor').material = new THREE.MeshLambertMaterial({color: 0xFF0000}); 73 | controller.add(spray); 74 | } 75 | 76 | function attachColorWheel(controllerData) { 77 | let controller = controllerData.controller; 78 | colorWheel = new ColorWheel(ctx, controller, (rgb) => { 79 | colorize( 80 | rgb.r, 81 | rgb.g, 82 | rgb.b); 83 | spray.getObjectByName('spraycolor').material.color.setRGB(rgb.r / 255, rgb.g / 255, rgb.b / 255); 84 | }); 85 | 86 | colorWheel.enter(); 87 | } 88 | 89 | ctx.raycontrol.addEventListener('controllerConnected', controllerData => { 90 | if (ctx.raycontrol.matchController(controllerData, "primary")) { 91 | attachSprayCan(controllerData); 92 | checkerSpray 93 | .addComponent(AreaChecker) 94 | .addComponent(Object3D, {value: controllerData.controller}) 95 | .addComponent(AreaReactor, { 96 | onEntering: entity => { 97 | const obj3D = entity.getComponent(Object3D).value; 98 | obj3D.getObjectByName('Scene').visible = false; 99 | obj3D.getObjectByName('spray').visible = true; 100 | let raycasterContext = obj3D.getObjectByName('raycasterContext'); 101 | raycasterContext.rotation.set(-Math.PI / 2, (controllerData.inputSource.handedness === "right" ? 1 : -1) * Math.PI / 2, 0); 102 | raycasterContext.position.set(0,-0.015,-0.025) 103 | ctx.raycontrol.setLineStyle('basic'); 104 | ctx.raycontrol.activateState('graffiti'); 105 | }, 106 | onExiting: entity => { 107 | const obj3D = entity.getComponent(Object3D).value; 108 | obj3D.getObjectByName('Scene').visible = true; 109 | obj3D.getObjectByName('spray').visible = false; 110 | let raycasterContext = obj3D.getObjectByName('raycasterContext'); 111 | raycasterContext.rotation.set(0,0,0); 112 | raycasterContext.position.set(0,0,0); 113 | ctx.raycontrol.setLineStyle('advanced'); 114 | ctx.raycontrol.deactivateState('graffiti'); 115 | } 116 | }); 117 | } else { 118 | attachColorWheel(controllerData); 119 | checker 120 | .addComponent(AreaChecker) 121 | .addComponent(Object3D, {value: controllerData.controller}) 122 | .addComponent(AreaReactor, { 123 | onEntering: entity => { 124 | const obj3D = entity.getComponent(Object3D).value; 125 | obj3D.getObjectByName('Scene').visible = false; 126 | obj3D.getObjectByName('ColorWheel').visible = true; 127 | }, 128 | onExiting: entity => { 129 | const obj3D = entity.getComponent(Object3D).value; 130 | obj3D.getObjectByName('Scene').visible = true; 131 | obj3D.getObjectByName('ColorWheel').visible = false; 132 | } 133 | }); 134 | } 135 | }); 136 | 137 | let width = 2048; 138 | let height = 1024; 139 | let maxDistance = 1; 140 | 141 | brushImg = new Image(); 142 | canvasTmp = document.createElement('canvas'); 143 | //canvasTmp.style.position = "absolute"; 144 | //canvasTmp.style.width = "20%"; 145 | //canvasTmp.style.backgroundColor = "#333"; 146 | ctxTmp = canvasTmp.getContext('2d'); 147 | // document.body.appendChild(canvasTmp); 148 | 149 | brushImg.onload = () => { 150 | canvasTmp.width = brushImg.width; 151 | canvasTmp.height = brushImg.height; 152 | colorize(0,0,0); 153 | } 154 | brushImg.src = 'assets/spray_brush.png'; 155 | 156 | var drawingCanvas = document.createElement('canvas'); 157 | 158 | drawingCanvas.width = width; 159 | drawingCanvas.height = height; 160 | drawContext = drawingCanvas.getContext('2d'); 161 | drawContext.clearRect(0, 0, width, height); 162 | drawContext.fillStyle = '#fff'; 163 | drawContext.fillRect(0, 0, width, height); 164 | 165 | let map = new THREE.CanvasTexture( drawingCanvas ); 166 | 167 | material = new THREE.MeshBasicMaterial({ 168 | color: 0xffffff, 169 | lightMap: ctx.assets['lightmap_tex'], 170 | map: map 171 | }); 172 | 173 | wall = hall.getObjectByName('graffiti'); 174 | wall.material = material; 175 | 176 | var aux2 = new THREE.Vector2(); 177 | 178 | ctx.raycontrol.addState('graffiti', { 179 | colliderMesh: wall, 180 | lineStyleOnIntersection: 'basic', 181 | onHover: (intersection, active, controller) => { 182 | if (active) 183 | { 184 | var distance = intersection.distance; 185 | 186 | if (distance > maxDistance) { return; } 187 | 188 | let x = intersection.uv.x * width; 189 | let y = height - intersection.uv.y * height; 190 | 191 | aux2.set(x, y); 192 | drawContext.imageSmoothingEnabled = true; 193 | drawContext.fillStyle = '#f00'; 194 | drawContext.strokeStyle = '#0f0'; 195 | var dist = lastPosition.distanceTo(aux2); 196 | var angle = angleBetween(lastPosition, aux2); 197 | let alpha = THREE.Math.clamp(1 - distance, 0, 1); 198 | 199 | drawContext.globalAlpha = alpha; 200 | 201 | for (var i = 0; i < dist; i ++ /* +=4 */) { 202 | var _x = lastPosition.x + (Math.sin(angle) * i); 203 | var _y = lastPosition.y + (Math.cos(angle) * i); 204 | drawContext.save(); 205 | drawContext.translate(_x, _y); 206 | let r = THREE.Math.lerp(0.001, 0.2, distance); 207 | drawContext.scale(r, r); 208 | 209 | drawContext.rotate(Math.PI * 180 / getRandomInt(0, 180)); 210 | 211 | drawContext.drawImage(paintImg, -brushImg.width / 2, -brushImg.height / 2); 212 | drawContext.restore(); 213 | } 214 | 215 | lastPosition.set(x, y); 216 | 217 | material.map.needsUpdate = true; 218 | } 219 | }, 220 | onHoverLeave: (intersection) => {}, 221 | onSelectStart: (intersection, controller) => { 222 | var distance = intersection.distance; 223 | 224 | if (distance > maxDistance) { return; } 225 | 226 | lastController = controller; 227 | controller.getObjectByName('spraySound').play(); 228 | 229 | let x = intersection.uv.x * width; 230 | let y = height - intersection.uv.y * height; 231 | 232 | lastPosition.set(x, y); 233 | }, 234 | onSelectEnd: (intersection) => { 235 | if (!lastController) { return; } 236 | lastController.getObjectByName('spraySound').stop(); 237 | } 238 | }); 239 | } 240 | 241 | export function execute(ctx, delta, time) {} 242 | -------------------------------------------------------------------------------- /src/stations/InfoPanels.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { Children, Object3D, Billboard, Text, Position, ParentObject3D } from '../components/index.js'; 3 | import INFO_DATA from './InfoPanelsData.js'; 4 | 5 | var panels = [], panelTexts = [], panelsEntity = []; 6 | 7 | export function setup(ctx, hall) { 8 | for (var i = 0; i < INFO_DATA.length; i++) { 9 | const id = i < 10 ? '0' + i : i; 10 | panels[i] = hall.getObjectByName('infopanel0'+id); 11 | panels[i].geometry.computeBoundingBox(); 12 | const panelWidth = panels[i].geometry.boundingBox.max.x - panels[i].geometry.boundingBox.min.x; 13 | const panelHeight = panels[i].geometry.boundingBox.max.y - panels[i].geometry.boundingBox.min.y; 14 | const offsety = INFO_DATA[i].offsety || 0; 15 | panels[i].material = new THREE.MeshBasicMaterial({color: 0x040404, transparent: true}); 16 | 17 | panelTexts[i] = ctx.world.createEntity(); 18 | panelTexts[i] 19 | .addComponent(Text, { 20 | color: '#ffffff', //0xdaa056, 21 | fontSize: 0.05, 22 | anchor: 'left', 23 | textAlign: 'left', 24 | baseline: 'top', 25 | maxWidth: panelWidth * 0.8, 26 | lineHeight: 1.3, 27 | text: INFO_DATA[i].title + '\n \n' + INFO_DATA[i].description, 28 | }) 29 | .addComponent(ParentObject3D, {value: panels[i]}) 30 | .addComponent(Position, {x: -panelWidth / 2 * 0.82, y: panelHeight / 2 * 0.65 + offsety, z: 0.01}) 31 | 32 | panelsEntity[i] = ctx.world.createEntity(); 33 | panelsEntity[i] 34 | .addComponent(Object3D, {value: panels[i]}) 35 | .addComponent(Billboard, {camera3D: ctx.camera}) 36 | .addComponent(Children, {value: [panelTexts[i]]}); 37 | 38 | } 39 | } 40 | 41 | export function enter(ctx) {} 42 | export function execute(ctx, delta, time) {} 43 | -------------------------------------------------------------------------------- /src/stations/InfoPanelsData.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | title: '360 Panoramas', 4 | description: 'Photographs wrapped around spheres provide an environment, but without stereo effect nor depth.', 5 | offsety: 0.04 6 | }, 7 | { 8 | title: 'Xylophone Toy', 9 | description: 'Example of object grabbing and simple interaction.', 10 | offsety: 0.04 11 | }, 12 | { 13 | title: '360 Stereo Panorama', 14 | description: 'By using one photo for each eye, panoramas can have some depth and stereo effect.', 15 | offsety: 0.03 16 | }, 17 | { 18 | title: 'Photogrammetry Room', 19 | description: 'Example of an object created out of photographs using an automated software.', 20 | offsety: 0.04 21 | }, 22 | { 23 | title: 'Twitter Feed', 24 | description: 'Bringing external realtime data to an XR environment.', 25 | offsety: 0.02 26 | }, 27 | { 28 | title: 'Vertigo Room', 29 | description: 'An example of how this new medium can play with your perception and senses.', 30 | offsety: 0.04 31 | }, 32 | { 33 | title: 'Hello WebXR!', 34 | description: 'A small compendium of interactions and little experiences introducing and celebrating the final specification of the WebXR API.\n \nMade by the Mozilla Mixed Reality team\nmixedreality.mozilla.org', 35 | offsety: 0.06 36 | }, 37 | { 38 | title: 'The Pink Robe. After the Bath', 39 | description: 'Joaquín Sorolla, 1916\n208 x 126.5 cm. Oil on canvas.', 40 | }, 41 | { 42 | title: 'The Garden of Earthly Delights', 43 | description: 'Hieronymus Bosch, 1490 - 1510\n205.5 x 384.9 cm. Oil on oak panels.', 44 | }, 45 | { 46 | title: 'Self-Portrait', 47 | description: 'Rembrandt van Rijn, 1659\n84.5 x 66 cm. Oil on canvas.', 48 | }, 49 | { 50 | title: 'The Dance Lesson', 51 | description: 'Edgar Degas, 1879\n38 x 88 cm. Oil on canvas.', 52 | }, 53 | { 54 | title: 'Gray Weather, Grande Jatte', 55 | description: 'Georges Seurat, 1888\n71 x 66 cm. Oil on canvas.', 56 | offsety: 0.04 57 | }, 58 | { 59 | title: 'Sound Room', 60 | description: 'Showcase of positional audio, very useful on XR experiences.\nTry to find where the sounds come from!.', 61 | offsety: 0.04 62 | }, 63 | { 64 | title: 'Graffiti Wall', 65 | description: 'Get close and spray the wall!.\nLet\'s collaborate on a full masterpiece.', 66 | offsety: 0.04 67 | }, 68 | { 69 | title: 'Paintings', 70 | description: 'Real-scale paintings that can be inspected very closely using the controller.', 71 | offsety: 0.04 72 | }, 73 | { 74 | title: 'WELCOME!', 75 | description: 'Aim your controller and press the trigger to teleport around and interact with objects.\n\nWe currently have 3 rooms and 5 experiences to explore. Try them all!', 76 | offsety: 0.06 77 | } 78 | ]; 79 | -------------------------------------------------------------------------------- /src/stations/NewsTicker.js: -------------------------------------------------------------------------------- 1 | //import {Text} from '../lib/text.mjs'; 2 | import * as THREE from 'three'; 3 | import { Text, Rotation, Position, ParentObject3D } from '../components/index.js'; 4 | 5 | var newsTicker = { 6 | url: 'assets/tweets.json', 7 | hashtag: '#helloWebXR', 8 | hashtagText: null, 9 | authorText: null, 10 | messageText: null, 11 | news: [], 12 | current: 0 13 | }; 14 | 15 | var screenMaterial; 16 | 17 | export function setup(ctx, hall) { 18 | const newsTickerMesh = hall.getObjectByName('newsticker'); 19 | 20 | ctx.world.createEntity(); 21 | 22 | screenMaterial = hall.getObjectByName('screen').material; 23 | 24 | newsTicker.hashtagText = ctx.world.createEntity(); 25 | newsTicker.hashtagText.addComponent(Text, { 26 | color: '#f6cdde', //0xdaa056, 27 | fontSize: 0.1, 28 | anchor: 'right', 29 | textAlign: 'right' 30 | }).addComponent(ParentObject3D, {value: newsTickerMesh}); 31 | 32 | newsTicker.authorText = ctx.world.createEntity(); 33 | newsTicker.authorText.addComponent(Text, { 34 | color: '#7f0c38', //0x67bccd, 35 | fontSize: 0.1, 36 | anchor: 'left', 37 | }).addComponent(ParentObject3D, {value: newsTickerMesh}); 38 | 39 | newsTicker.messageText = ctx.world.createEntity(); 40 | newsTicker.messageText.addComponent(Text, { 41 | color: 0x000000, 42 | fontSize: 0.13, 43 | maxWidth: 2.3, 44 | lineHeight: 1, 45 | textAlign: 'left', 46 | baseline: 'top', 47 | anchor: 'left' 48 | }).addComponent(ParentObject3D, {value: newsTickerMesh}); 49 | 50 | ['hashtag', 'author', 'message'].forEach( i => { 51 | 52 | newsTicker[`${i}Text`].addComponent(Position, hall.getObjectByName(i).position); 53 | newsTicker[`${i}Text`].addComponent(Rotation, {x: 0, y: Math.PI, z: 0}); 54 | }); 55 | newsTicker.hashtagText.getMutableComponent(Text).text = newsTicker.hashtag; 56 | 57 | fetch(newsTicker.url).then(res => res.json()).then(res => { 58 | newsTicker.news = res; 59 | nextNews(); 60 | }); 61 | } 62 | 63 | function nextNews() { 64 | const n = newsTicker; 65 | n.authorText.getMutableComponent(Text).text = n.news[n.current].author; 66 | n.messageText.getMutableComponent(Text).text = n.news[n.current].message; 67 | n.current = (n.current + 1) % n.news.length; 68 | setTimeout(nextNews, 3000); 69 | } 70 | 71 | export function execute(ctx, delta, time) { 72 | var v = 0.98 + Math.sin(time * 40) * 0.02; 73 | screenMaterial.color.setRGB(v, v, v); 74 | } 75 | -------------------------------------------------------------------------------- /src/stations/Paintings.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | var paintings; 4 | var zoom = {object: null, widget: null, controller: null, animation: 0, icon: null}; 5 | const PAINTINGS = ['seurat', 'sorolla', 'bosch', 'degas', 'rembrandt']; 6 | const RATIOS = [1, 1, 0.5, 0.5, 1]; 7 | const ZOOMS = [0.4, 0.2, 0.2, 0.4, 0.25]; 8 | 9 | 10 | export function enter(ctx) { 11 | ctx.raycontrol.activateState('paintings'); 12 | } 13 | 14 | export function setup(ctx, hall) { 15 | for (let i in PAINTINGS) { 16 | let painting = PAINTINGS[i]; 17 | let mesh = hall.getObjectByName(painting); 18 | if (!mesh) { continue; } 19 | 20 | let paintingTexture = ctx.assets[`painting_${painting}_tex`]; 21 | mesh.material = new THREE.MeshBasicMaterial({ 22 | map: paintingTexture 23 | }); 24 | mesh.userData.paintingId = i; 25 | } 26 | 27 | paintings = hall.getObjectByName('paintings'); 28 | 29 | zoom.widget = new THREE.Mesh( 30 | new THREE.PlaneGeometry(), 31 | new THREE.ShaderMaterial({ 32 | uniforms: { 33 | time: {value: 0}, 34 | tex: {value: null}, 35 | zoomPos: {value: new THREE.Vector2()}, 36 | zoomAmount: {value: 0}, 37 | zoomRatio: {value: 1} 38 | }, 39 | vertexShader: ctx.shaders.basic_vert, 40 | fragmentShader: ctx.shaders.zoom_frag, 41 | transparent: true, 42 | depthTest: false, 43 | depthWrite: false 44 | }) 45 | ); 46 | zoom.widget.geometry.rotateY(-Math.PI / 2); 47 | zoom.widget.visible = false; 48 | 49 | zoom.icon = new THREE.Mesh( 50 | new THREE.PlaneGeometry(0.2, 0.2), 51 | new THREE.MeshBasicMaterial({ 52 | map: ctx.assets['zoomicon_tex'], 53 | transparent: true 54 | //side: THREE.DoubleSide 55 | }) 56 | ); 57 | zoom.icon.geometry.rotateY(-Math.PI / 2); 58 | zoom.icon.visible = false; 59 | 60 | ctx.scene.add(zoom.icon); 61 | ctx.scene.add(zoom.widget); 62 | 63 | ctx.raycontrol.addState('paintings', { 64 | colliderMesh: hall.getObjectByName('paintings'), 65 | onHover: (intersection, active, controller) => { 66 | if (intersection.distance > 3) { return; } 67 | if (active) { 68 | zoom.painting = intersection.object; 69 | zoom.controller = controller; 70 | zoom.widget.material.uniforms.tex.value = zoom.painting.material.map; 71 | zoom.widget.material.uniforms.zoomRatio.value = RATIOS[intersection.object.userData.paintingId]; 72 | zoom.widget.visible = true; 73 | refreshZoomUV(intersection); 74 | } else { 75 | zoom.icon.visible = true; 76 | zoom.icon.position.copy(intersection.point); 77 | zoom.icon.position.x -= 0.01; 78 | } 79 | }, 80 | onHoverLeave: (intersection) => { 81 | zoom.painting = null; 82 | zoom.animation = 0; 83 | zoom.widget.visible = false; 84 | zoom.icon.visible = false; 85 | }, 86 | onSelectStart: (intersection, controller) => { 87 | if (intersection.distance < 3) { 88 | zoom.painting = intersection.object; 89 | zoom.controller = controller; 90 | zoom.widget.material.uniforms.tex.value = zoom.painting.material.map; 91 | zoom.widget.material.uniforms.zoomRatio.value = RATIOS[intersection.object.userData.paintingId]; 92 | zoom.widget.visible = true; 93 | zoom.icon.visible = false; 94 | refreshZoomUV(intersection); 95 | } 96 | }, 97 | onSelectEnd: (intersection) => { 98 | zoom.painting = null; 99 | zoom.animation = 0; 100 | zoom.widget.visible = false; 101 | } 102 | }); 103 | } 104 | 105 | export function execute(ctx, delta, time) { 106 | if (zoom.painting) { 107 | if (zoom.animation < 1) { 108 | zoom.animation += (1 - zoom.animation) * delta * 4.0; 109 | } 110 | zoom.widget.material.uniforms.time.value = time; 111 | } 112 | } 113 | 114 | var minUV = new THREE.Vector2(); 115 | var maxUV = new THREE.Vector2(); 116 | 117 | function refreshZoomUV(hit) { 118 | zoom.widget.position.copy(hit.point); 119 | zoom.widget.position.x -= 0.5 * zoom.animation; 120 | 121 | const uvs = zoom.widget.geometry.faceVertexUvs[0]; 122 | zoom.widget.material.uniforms.zoomPos.value.copy(hit.uv); 123 | zoom.widget.material.uniforms.zoomAmount.value = ZOOMS[hit.object.userData.paintingId]; 124 | } 125 | -------------------------------------------------------------------------------- /src/stations/PanoBalls.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | var 4 | panoBalls = [], 5 | panoballsParent = new THREE.Object3D(), 6 | bbox = new THREE.Box3(), 7 | panoFxMaterial, 8 | auxVec = new THREE.Vector3(), 9 | hallRef = null; 10 | 11 | const NUM_PANOBALLS = 6; 12 | 13 | export function enter(ctx) { 14 | ctx.raycontrol.activateState('panoballs'); 15 | } 16 | 17 | export function setup(ctx, hall) { 18 | const assets = ctx.assets; 19 | hallRef = hall; 20 | 21 | const panoGeo = new THREE.SphereBufferGeometry(0.15, 30, 20); 22 | 23 | for (var i = 0; i < NUM_PANOBALLS; i++) { 24 | let asset = assets[`pano${i + 1}small`]; 25 | var ball = new THREE.Mesh( 26 | new THREE.SphereBufferGeometry(0.15, 30, 20), 27 | new THREE.ShaderMaterial({ 28 | uniforms: { 29 | time: {value: 0}, 30 | tex: {value: asset}, 31 | texfx: {value: assets['panoballfx_tex']}, 32 | selected: {value: 0} 33 | }, 34 | vertexShader: ctx.shaders.panoball_vert, 35 | fragmentShader: ctx.shaders.panoball_frag, 36 | side: THREE.BackSide, 37 | }) 38 | ); 39 | ball.rotation.set(Math.PI, 0, 0); 40 | ball.position.copy(hall.getObjectByName(`panoball${i + 1}`).position); 41 | ball.userData.floatY = ball.position.y; 42 | ball.userData.panoId = 4 + i; 43 | ball.userData.selected = 0; 44 | 45 | panoBalls.push(ball); 46 | panoballsParent.add(ball); 47 | } 48 | 49 | hall.add(panoballsParent); 50 | 51 | ctx.raycontrol.addState('panoballs', { 52 | colliderMesh: panoballsParent, 53 | onHover: (intersection, active, controller) => { 54 | panoBalls.forEach(panoBall => panoBall.userData.selected = 0); 55 | intersection.object.userData.selected = 1; 56 | }, 57 | onHoverLeave: (intersection) => { 58 | intersection.object.userData.selected = 0; 59 | }, 60 | onSelectStart: (intersection, controller) => { 61 | }, 62 | onSelectEnd: (intersection) => { 63 | ctx.goto = intersection.object.userData.panoId; 64 | intersection.object.userData.selected = 0; 65 | } 66 | }); 67 | } 68 | 69 | export function execute(ctx, delta, time) { 70 | for (let i = 0; i < panoBalls.length; i++) { 71 | const ball = panoBalls[i]; 72 | ball.position.y = ball.userData.floatY + Math.cos(i + time * 3) * 0.02; 73 | } 74 | } 75 | 76 | export function updateUniforms(time) { 77 | for (let i = 0; i < panoBalls.length; i++) { 78 | panoBalls[i].material.uniforms.time.value = i + time; 79 | panoBalls[i].material.uniforms.selected.value += 80 | (panoBalls[i].userData.selected - panoBalls[i].material.uniforms.selected.value) * 0.1; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/stations/Xylophone.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import PositionalAudioPolyphonic from '../lib/PositionalAudioPolyphonic.js'; 3 | 4 | var 5 | listener, 6 | xyloSticks = [null, null], 7 | xyloStickBalls = [null, null], 8 | xyloNotes = new Array(13), 9 | bbox = new THREE.Box3(), 10 | hallRef = null; 11 | 12 | var auxVec = new THREE.Vector3(); 13 | 14 | var NUM_NOTES = 13; 15 | 16 | var stickNotesColliding = [ 17 | new Array(NUM_NOTES).fill(false), 18 | new Array(NUM_NOTES).fill(false) 19 | ]; 20 | 21 | var _ctx = null; 22 | 23 | export function setup(ctx, hall) { 24 | _ctx = ctx; 25 | const audioLoader = new THREE.AudioLoader(); 26 | listener = new THREE.AudioListener(); 27 | hallRef = hall; 28 | 29 | for (let i = 0; i < NUM_NOTES; i++) { 30 | let noteName = 'xnote0' + (i < 10 ? '0' + i : i); 31 | let note = hall.getObjectByName(noteName); 32 | note.geometry.computeBoundingBox(); 33 | note.geometry.boundingBox.translate(note.position).translate(note.parent.position); 34 | note.material = new THREE.MeshLambertMaterial(); 35 | note.material.color.setHSL(i / 13, 0.9, 0.2); 36 | note.material.emissive = note.material.color.clone(); 37 | note.material.emissiveIntensity = 0; 38 | xyloNotes[i] = note; 39 | note.userData.animation = 0; 40 | note.userData.resetY = note.position.y; 41 | note.userData.sound = new PositionalAudioPolyphonic(listener, 10); 42 | audioLoader.load('assets/ogg/xylophone' + (i + 1) + '.ogg', buffer => { 43 | note.userData.sound.setBuffer(buffer); 44 | }); 45 | } 46 | 47 | xyloSticks[0] = hall.getObjectByName('xylostick-left'); 48 | xyloSticks[1] = hall.getObjectByName('xylostick-right'); 49 | xyloSticks[0].userData.resetPosition = xyloSticks[0].position.clone(); 50 | xyloSticks[1].userData.resetPosition = xyloSticks[1].position.clone(); 51 | xyloSticks[0].userData.resetRotation = xyloSticks[0].rotation.clone(); 52 | xyloSticks[1].userData.resetRotation = xyloSticks[1].rotation.clone(); 53 | xyloSticks[0].userData.grabbedBy = null; 54 | xyloSticks[1].userData.grabbedBy = null; 55 | xyloSticks[0].userData.animation = 0; 56 | xyloSticks[1].userData.animation = 0; 57 | xyloStickBalls[0] = hall.getObjectByName('xylostickball-left'); 58 | xyloStickBalls[1] = hall.getObjectByName('xylostickball-right'); 59 | xyloStickBalls[0].geometry.computeBoundingBox(); 60 | xyloStickBalls[1].geometry.computeBoundingBox(); 61 | } 62 | 63 | export function enter(ctx) { 64 | ctx.camera.add(listener); 65 | 66 | let selectStart = onSelectStart.bind(this); 67 | let selectEnd = onSelectEnd.bind(this); 68 | 69 | ctx.controllers[0].addEventListener('selectstart', selectStart); 70 | ctx.controllers[1].addEventListener('selectstart', selectStart); 71 | ctx.controllers[0].addEventListener('selectend', selectEnd); 72 | ctx.controllers[1].addEventListener('selectend', selectEnd); 73 | } 74 | 75 | export function exit(ctx) { 76 | ctx.camera.remove(listener); 77 | 78 | let selectStart = onSelectStart.bind(this); 79 | let selectEnd = onSelectEnd.bind(this); 80 | 81 | ctx.controllers[0].removeEventListener('selectstart', selectStart); 82 | ctx.controllers[1].removeEventListener('selectstart', selectStart); 83 | ctx.controllers[0].removeEventListener('selectend', selectEnd); 84 | ctx.controllers[1].removeEventListener('selectend', selectEnd); 85 | } 86 | 87 | function hitTest(obj1, obj2) { 88 | bbox.setFromObject(obj2); 89 | if (obj1.boundingBox.intersectsBox(bbox)){ 90 | return true; 91 | } 92 | return false; 93 | } 94 | 95 | function setStickColor(stick, color) { 96 | xyloStickBalls[stick].material.color.set(color); 97 | } 98 | 99 | export function execute(ctx, delta, time) { 100 | let controllers = ctx.controllers; 101 | 102 | if (!controllers) {return;} 103 | 104 | for (let c = 0; c < 2; c++) { 105 | 106 | if (controllers[c].userData.grabbing === null) { 107 | 108 | let stick0 = hitTest(controllers[0], xyloSticks[0]); 109 | let stick1 = hitTest(controllers[0], xyloSticks[1]); 110 | 111 | if (stick0 || stick1){ 112 | ctx.raycontrol.disable(); 113 | } else { 114 | ctx.raycontrol.enable(); 115 | } 116 | 117 | if (!xyloSticks[0].userData.grabbedBy) { 118 | if (!stick0) stick0 = hitTest(controllers[1], xyloSticks[0]); 119 | setStickColor(0, stick0 ? 0xffffff : 0xaaaaaa); 120 | } 121 | if (!xyloSticks[1].userData.grabbedBy) { 122 | if (!stick1) stick1 = hitTest(controllers[1], xyloSticks[1]); 123 | setStickColor(1, stick1 ? 0xffffff : 0xaaaaaa); 124 | } 125 | } else { 126 | // controller grabbing stick 127 | let stick = controllers[c].userData.grabbing.children[0]; 128 | bbox.setFromObject(stick).expandByScalar(-0.01); 129 | for (let i = 0; i < xyloNotes.length; i++) { 130 | let note = xyloNotes[i]; 131 | if (note.userData.animation > 0) { 132 | note.userData.animation = Math.max(0, note.userData.animation - delta * 4); 133 | note.material.emissiveIntensity = note.userData.animation; 134 | note.position.y = note.userData.resetY - note.userData.animation * 0.005; 135 | } 136 | 137 | if (bbox.intersectsBox(note.geometry.boundingBox)) { 138 | if (!stickNotesColliding[c][i]) { 139 | stickNotesColliding[c][i] = true; 140 | note.userData.sound.play(); 141 | note.userData.animation = 1; 142 | setStickColor(c, 0xffffff); 143 | } 144 | } else { 145 | if (stickNotesColliding[c][i]){ 146 | stickNotesColliding[c][i] = false; 147 | setStickColor(c, 0xaaaaaa); 148 | } 149 | } 150 | } 151 | } 152 | if (xyloSticks[c].userData.animation > 0){ 153 | auxVec.copy(xyloSticks[c].userData.resetPosition).sub(xyloSticks[c].position); 154 | auxVec.multiplyScalar(0.1); 155 | xyloSticks[c].position.add(auxVec); 156 | if (auxVec.length < 0.01) { xyloSticks[c].userData.animation = 0; } 157 | } 158 | } 159 | } 160 | 161 | export function onSelectStart(evt) { 162 | let controller = evt.target; 163 | if (controller.userData.grabbing !== null){ return; } 164 | 165 | // hand grabs stick 166 | for (let i = 0; i < 2; i++) { 167 | bbox.setFromObject(xyloSticks[i]); 168 | if (controller.boundingBox.intersectsBox(bbox)){ 169 | 170 | setStickColor(i, 0xaaaaaa); 171 | 172 | _ctx.raycontrol.disable(); 173 | 174 | // stick grabbed from the other hand 175 | if (xyloSticks[i].userData.grabbedBy) { 176 | xyloSticks[i].userData.grabbedBy.userData.grabbing = null; 177 | } 178 | xyloSticks[i].position.set(0, 0, 0); 179 | xyloSticks[i].rotation.set(0, 0, 0); 180 | controller.add(xyloSticks[i]); 181 | xyloSticks[i].userData.animation = 0; 182 | controller.userData.grabbing = xyloSticks[i]; 183 | xyloSticks[i].userData.grabbedBy = controller; 184 | return false; 185 | } 186 | } 187 | return true; 188 | } 189 | 190 | export function onSelectEnd(evt) { 191 | _ctx.raycontrol.enable(); 192 | 193 | let controller = evt.target; 194 | if (controller.userData.grabbing !== null) { 195 | let stick = controller.userData.grabbing; 196 | stick.getWorldPosition(auxVec); 197 | hallRef.add(stick); 198 | stick.position.copy(auxVec); 199 | stick.rotation.copy(stick.userData.resetRotation); 200 | stick.userData.grabbedBy = null; 201 | stick.userData.animation = 1; 202 | controller.userData.grabbing = null; 203 | return false; 204 | } 205 | return true; 206 | } 207 | -------------------------------------------------------------------------------- /src/systems/AreaCheckerSystem.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import {System} from 'ecsy'; 3 | import {Area, AreaInside, AreaExiting, AreaEntering, Object3D, AreaChecker, BoundingBox} from '../components/index.js'; 4 | 5 | export class AreaCheckerSystem extends System { 6 | execute(delta, time) { 7 | const areas = this.queries.areas.results; 8 | const checkers = this.queries.checkers.results; 9 | 10 | for (let i = 0; i < areas.length; i++) { 11 | const area = areas[i]; 12 | const bboxArea = area.getComponent(BoundingBox); 13 | 14 | for (let j = 0; j < checkers.length; j++) { 15 | const checker = checkers[j]; 16 | const obj3D = checker.getComponent(Object3D).value; 17 | if (obj3D.boundingBox.intersectsBox(bboxArea)) { 18 | checker.addComponent(AreaInside); 19 | } else { 20 | checker.removeComponent(AreaInside); 21 | } 22 | } 23 | } 24 | } 25 | } 26 | 27 | AreaCheckerSystem.queries = { 28 | areas: { 29 | components: [Area, BoundingBox], 30 | }, 31 | checkers: { 32 | components: [AreaChecker, Object3D] 33 | } 34 | } -------------------------------------------------------------------------------- /src/systems/BillboardSystem.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import {System} from 'ecsy'; 3 | import {Billboard, Object3D, Children, Text, Position} from '../components/index.js'; 4 | 5 | const SHOW_DISTANCE = 4; 6 | 7 | var cameraPosition = new THREE.Vector3(); 8 | 9 | export default class BillboardSystem extends System { 10 | execute(delta, time) { 11 | 12 | window.context.camera.getWorldPosition(cameraPosition); 13 | 14 | this.queries.entities.results.forEach(entity => { 15 | const object3D = entity.getComponent(Object3D).value; 16 | const distance = cameraPosition.distanceTo(object3D.position); 17 | let opacity = 0; 18 | 19 | if (distance < SHOW_DISTANCE){ 20 | opacity = THREE.Math.clamp(Math.sqrt(SHOW_DISTANCE - distance), 0, 1); 21 | object3D.lookAt(cameraPosition); 22 | } 23 | 24 | object3D.material.opacity = opacity; 25 | // panels text parent 26 | if (entity.hasComponent(Children)) { 27 | entity.getComponent(Children).value.forEach(children => { 28 | let prevOpacity = children.getComponent(Text).opacity; 29 | if (prevOpacity !== opacity) { 30 | children.getMutableComponent(Text).opacity = opacity; 31 | } 32 | }); 33 | } 34 | }); 35 | } 36 | } 37 | 38 | BillboardSystem.queries = { 39 | entities: { 40 | components: [Billboard, Object3D] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/systems/ControllersSystem.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import {System} from 'ecsy'; 3 | import {Area, AreaReactor, AreaInside, AreaExiting, AreaEntering, Object3D, AreaChecker, BoundingBox} from '../components/index.js'; 4 | 5 | export class ControllersSystem extends System { 6 | execute(delta, time) { 7 | const added = this.queries.checkers.added; 8 | const removed = this.queries.checkers.removed; 9 | 10 | for (let i = 0; i < added.length; i++) { 11 | const entity = added[i]; 12 | const reactor = entity.getComponent(AreaReactor); 13 | reactor.onEntering(entity); 14 | } 15 | 16 | for (let i = 0; i < removed.length; i++) { 17 | const entity = removed[i]; 18 | const reactor = entity.getComponent(AreaReactor); 19 | reactor.onExiting(entity); 20 | } 21 | } 22 | } 23 | 24 | ControllersSystem.queries = { 25 | checkers: { 26 | components: [AreaChecker, Object3D, AreaInside], 27 | listen: { 28 | added: true, 29 | removed: true 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/systems/DebugHelperSystem.js: -------------------------------------------------------------------------------- 1 | import {Object3D, DebugHelper, BoundingBox} from '../components/index.js'; 2 | import {System, SystemStateComponent, Not} from 'ecsy'; 3 | import * as THREE from 'three'; 4 | 5 | THREE.BoxHelper.prototype.setFromMinMax = function(min, max) { 6 | var position = this.geometry.attributes.position; 7 | var array = position.array; 8 | 9 | array[ 0 ] = max.x; array[ 1 ] = max.y; array[ 2 ] = max.z; 10 | array[ 3 ] = min.x; array[ 4 ] = max.y; array[ 5 ] = max.z; 11 | array[ 6 ] = min.x; array[ 7 ] = min.y; array[ 8 ] = max.z; 12 | array[ 9 ] = max.x; array[ 10 ] = min.y; array[ 11 ] = max.z; 13 | array[ 12 ] = max.x; array[ 13 ] = max.y; array[ 14 ] = min.z; 14 | array[ 15 ] = min.x; array[ 16 ] = max.y; array[ 17 ] = min.z; 15 | array[ 18 ] = min.x; array[ 19 ] = min.y; array[ 20 ] = min.z; 16 | array[ 21 ] = max.x; array[ 22 ] = min.y; array[ 23 ] = min.z; 17 | 18 | position.needsUpdate = true; 19 | 20 | this.geometry.computeBoundingSphere(); 21 | } 22 | 23 | class DebugHelperMesh extends SystemStateComponent { 24 | constructor() { 25 | super(); 26 | this.boxHelper = new THREE.BoxHelper(); 27 | } 28 | } 29 | 30 | export class DebugHelperSystem extends System { 31 | execute(delta, time) { 32 | this.queries.added.results.forEach(entity => { 33 | entity.addComponent(DebugHelperMesh); 34 | var boundingBox = entity.getComponent(BoundingBox); 35 | let debugMesh = entity.getMutableComponent(DebugHelperMesh); 36 | debugMesh.boxHelper.setFromMinMax(boundingBox.min, boundingBox.max); 37 | 38 | entity.addComponent(Object3D, {value: debugMesh.boxHelper}); 39 | }); 40 | 41 | this.queries.removed.results.forEach(entity => { 42 | //var boundingBox = entity.getComponent(DebugHelperMesh); 43 | entity.removeComponent(Object3D).removeComponent(DebugHelperMesh); 44 | 45 | /* 46 | entity.addComponent(Object3D, {value: debugMesh.boxHelper}); 47 | 48 | let debugMesh = entity.getMutableComponent(DebugHelperMesh); 49 | debugMesh.boxHelper.setFromMinMax(boundingBox.min, boundingBox.max); 50 | entity.addComponent(Object3D, {value: debugMesh.boxHelper}); 51 | */ 52 | }); 53 | } 54 | } 55 | 56 | DebugHelperSystem.queries = { 57 | added: { 58 | components: [DebugHelper, Not(DebugHelperMesh)] 59 | }, 60 | removed: { 61 | components: [Not(DebugHelper), DebugHelperMesh] 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/systems/HierarchySystem.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import {System} from 'ecsy'; 3 | import {Object3D, ParentObject3D} from '../components/index.js'; 4 | 5 | export default class HierarchySystem extends System { 6 | execute(delta, time) { 7 | this.queries.entities.added.forEach(entity => { 8 | const parent = entity.getComponent(ParentObject3D).value; 9 | const object = entity.getComponent(Object3D).value; 10 | 11 | parent.add(object); 12 | }); 13 | 14 | this.queries.entities.changed.forEach(entity => { 15 | const parent = entity.getComponent(ParentObject3D).value; 16 | const object = entity.getComponent(Object3D).value; 17 | 18 | parent.add(object); 19 | }); 20 | 21 | this.queries.entities.removed.forEach(entity => { 22 | const parent = entity.getComponent(ParentObject3D, true).value; 23 | parent.remove(entity.getComponent(Object3D, true).value); 24 | }); 25 | } 26 | } 27 | 28 | HierarchySystem.queries = { 29 | entities: { 30 | components: [Object3D, ParentObject3D], 31 | listen: { 32 | added: true, 33 | removed: true, 34 | changed: true // [Component] 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/systems/SDFTextSystem.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import {System} from 'ecsy'; 3 | import {TextMesh} from 'troika-3d-text/dist/textmesh-standalone.esm.js'; 4 | import {Object3D, Text} from '../components/index.js'; 5 | 6 | const anchorMapping = { 7 | 'left': 0, 8 | 'center': 0.5, 9 | 'right': 1 10 | } 11 | const baselineMapping = { 12 | 'top': 0, 13 | 'center': 0.5, 14 | 'bottom': 1 15 | } 16 | 17 | export class SDFTextSystem extends System { 18 | 19 | updateText(textMesh, textComponent) { 20 | textMesh.text = textComponent.text; 21 | textMesh.textAlign = textComponent.textAlign; 22 | textMesh.anchor[0] = anchorMapping[textComponent.anchor]; 23 | textMesh.anchor[1] = baselineMapping[textComponent.baseline]; 24 | textMesh.color = textComponent.color; 25 | textMesh.font = textComponent.font; 26 | textMesh.fontSize = textComponent.fontSize; 27 | textMesh.letterSpacing = textComponent.letterSpacing || 0; 28 | textMesh.lineHeight = textComponent.lineHeight || null; 29 | textMesh.overflowWrap = textComponent.overflowWrap; 30 | textMesh.whiteSpace = textComponent.whiteSpace; 31 | textMesh.maxWidth = textComponent.maxWidth; 32 | textMesh.material.opacity = textComponent.opacity; 33 | textMesh.sync(); 34 | } 35 | 36 | execute(delta, time) { 37 | var entities = this.queries.entities; 38 | 39 | entities.added.forEach(e => { 40 | var textComponent = e.getComponent(Text); 41 | 42 | const textMesh = new TextMesh(); 43 | textMesh.name = 'textMesh'; 44 | textMesh.anchor = [0, 0]; 45 | textMesh.renderOrder = 1; //brute-force fix for ugly antialiasing, see issue #67 46 | this.updateText(textMesh, textComponent); 47 | e.addComponent(Object3D, {value: textMesh}); 48 | }); 49 | 50 | entities.removed.forEach(e => { 51 | var object3D = e.getComponent(Object3D).value; 52 | var textMesh = object3D.getObjectByName('textMesh'); 53 | textMesh.dispose(); 54 | object3D.remove(textMesh); 55 | }); 56 | 57 | entities.changed.forEach(e => { 58 | var object3D = e.getComponent(Object3D).value; 59 | if (object3D instanceof TextMesh) { 60 | var textComponent = e.getComponent(Text); 61 | this.updateText(object3D, textComponent); 62 | } 63 | }); 64 | } 65 | } 66 | 67 | SDFTextSystem.queries = { 68 | entities: { 69 | components: [Text], 70 | listen: { 71 | added: true, 72 | removed: true, 73 | changed: [Text] 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/systems/SystemsGroup.js: -------------------------------------------------------------------------------- 1 | export default class SystemsGroup { 2 | constructor(world, Systems) { 3 | this.Systems = Systems; 4 | this.world = world; 5 | } 6 | 7 | play() { 8 | this.Systems.forEach(System => this.world.getSystem(System).play()); 9 | } 10 | 11 | stop() { 12 | this.Systems.forEach(System => this.world.getSystem(System).stop()); 13 | } 14 | } -------------------------------------------------------------------------------- /src/systems/TransformSystem.js: -------------------------------------------------------------------------------- 1 | import {System} from 'ecsy'; 2 | import {Object3D, Rotation, Position} from '../components/index.js'; 3 | 4 | let updateRotation = entity => { 5 | const rotation = entity.getComponent(Rotation); 6 | const object3D = entity.getComponent(Object3D).value; 7 | object3D.rotation.set(rotation.x, rotation.y, rotation.z); 8 | }; 9 | 10 | let updatePosition = entity => { 11 | const position = entity.getComponent(Position); 12 | const object3D = entity.getComponent(Object3D).value; 13 | object3D.position.copy(position); 14 | }; 15 | 16 | export default class TransformSystem extends System { 17 | execute(delta, time) { 18 | // Position 19 | this.queries.position.added.forEach(updatePosition); 20 | this.queries.position.changed.forEach(updatePosition); 21 | 22 | // Rotation 23 | this.queries.rotation.added.forEach(updateRotation); 24 | this.queries.rotation.changed.forEach(updateRotation); 25 | } 26 | } 27 | 28 | TransformSystem.queries = { 29 | position: { 30 | components: [Position, Object3D], 31 | listen: { 32 | added: true, 33 | changed: true 34 | } 35 | }, 36 | rotation: { 37 | components: [Rotation, Object3D], 38 | listen: { 39 | added: true, 40 | changed: true 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/vendor/basis_transcoder.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/src/vendor/basis_transcoder.wasm -------------------------------------------------------------------------------- /src/vendor/draco_decoder.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MozillaReality/hello-webxr/325144288444d51b63b09f3289aaa09ddb83163b/src/vendor/draco_decoder.wasm -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mode: 'development', 3 | entry: './src/index.js', 4 | output: { 5 | filename: 'bundle.js', 6 | path: __dirname 7 | }, 8 | devtool: 'source-map', 9 | module: { 10 | rules: [ 11 | { 12 | test: /\.(js|mjs)$/, 13 | exclude: /(node_modules)/, 14 | use: { loader: 'babel-loader' } 15 | } 16 | ] 17 | }, 18 | watchOptions: { 19 | ignored: [/node_modules/], 20 | } 21 | }; 22 | --------------------------------------------------------------------------------