├── .gitignore ├── README.md ├── index.html ├── package.json ├── src ├── engine │ ├── AssetManager.js │ ├── Config.js │ ├── Engine.js │ ├── Hodler.js │ ├── InputManager.js │ ├── RenderManager.js │ └── Scene.js ├── extras │ ├── AfterEffects.js │ ├── Animations.js │ ├── ArtGenerator.js │ ├── CyclicArray.js │ ├── HighScoreManager.js │ ├── Measure.js │ ├── MeshNetwork.js │ ├── Modifiers.js │ ├── Persist.js │ ├── Playlist.js │ ├── PolyfillRenderer.js │ ├── PoolManager.js │ ├── RStatsManager.js │ ├── ShaderLib.js │ ├── ShaderMaterial.js │ ├── SoundManager.js │ ├── StatsManager.js │ ├── SyntaxSugar.js │ ├── Utils.js │ ├── VideoRecorderManager.js │ ├── controls │ │ ├── PlatformerControls.js │ │ ├── PositionXZRotationYControls.js │ │ ├── RTSCamera.js │ │ ├── RTSCamera2.js │ │ ├── RayScanner.js │ │ └── VirtualController.js │ ├── jnorthpole.js │ └── scenes │ │ ├── AddsScene.js │ │ ├── LoadingScene.js │ │ ├── SceneLoader.js │ │ └── VideoScene.js ├── objects │ ├── BaseParticle.js │ ├── BaseText.js │ ├── Button3D.js │ ├── LightningBolt.js │ ├── Mirror.js │ ├── Sky.js │ ├── SkyBox.js │ ├── SpotLight.js │ ├── Starfield.js │ ├── Terrain.js │ ├── Tree.js │ ├── TypeWriter.js │ └── Water.js ├── tools │ ├── build.js │ ├── common.js │ ├── dependencies.dev.js │ ├── dependencies.dist.js │ ├── dist.js │ ├── help.js │ ├── newGame.js │ └── publish.js └── vendor │ ├── CustomOrbitControls.js │ ├── brace.js │ ├── discoveryClient.js │ ├── drawBezier.js │ ├── live.js │ ├── rStats.css │ ├── threex.dynamictexture.js │ ├── threex.rendererstats.js │ └── water-material.js ├── tutorials ├── ASSETS.md ├── BLENDER.md ├── CHEATSHEET.md ├── DISTRIBUTE.md ├── INSTALL.md ├── NETWORKING.md └── SCENES.md ├── workspace ├── assets │ ├── fonts │ │ ├── luckiest-guy.LICENSE.txt │ │ ├── luckiest-guy.eot │ │ ├── luckiest-guy.ttf │ │ ├── luckiest-guy.woff │ │ └── luckiest-guy.woff2 │ ├── graffiti │ │ └── majestic-frog-cover.json │ ├── models │ │ ├── altar.bin │ │ ├── altar.gltf │ │ ├── altar.png │ │ ├── bird.glb │ │ ├── boat.bin │ │ ├── boat.gltf │ │ ├── button.bg.001.glb │ │ ├── button.bg.002.glb │ │ ├── button.bg.003.glb │ │ ├── button.fg.001.glb │ │ ├── button.fg.002.glb │ │ ├── button.fg.003.glb │ │ ├── button.glb │ │ ├── chicken.bin │ │ ├── chicken.gltf │ │ ├── chicken.png │ │ ├── coin.bin │ │ ├── coin.gltf │ │ ├── coin.jpg │ │ ├── grass.bin │ │ ├── grass.gltf │ │ ├── panda.glb │ │ ├── rock-001.bin │ │ ├── rock-001.gltf │ │ ├── rock-002.bin │ │ ├── rock-002.gltf │ │ ├── tree-001.bin │ │ ├── tree-001.gltf │ │ ├── tree-002.bin │ │ ├── tree-002.gltf │ │ ├── tree-003.bin │ │ ├── tree-003.gltf │ │ ├── tree-004.bin │ │ ├── tree-004.gltf │ │ ├── trunk-001.bin │ │ ├── trunk-001.gltf │ │ ├── trunk-002.bin │ │ └── trunk-002.gltf │ ├── particles │ │ ├── basic.json │ │ ├── bubbles.json │ │ ├── defaults.json │ │ ├── explosion.json │ │ ├── fire-small.json │ │ ├── fireflies.json │ │ ├── hit.json │ │ └── particle.json │ ├── scenes │ │ └── boat-scene.json │ ├── shaders │ │ ├── basic_shader.json │ │ ├── basic_shader2.json │ │ └── dissolve_shader.json │ ├── sounds │ │ ├── SuperHero_original.ogg │ │ └── hit.wav │ ├── src │ │ ├── 1538419097.svg │ │ └── colors.xcf │ ├── terrains │ │ └── terrain.json │ └── textures │ │ ├── black-faded-border.png │ │ ├── chicken_black.jpeg │ │ ├── credits.png │ │ ├── grass.png │ │ ├── hand.png │ │ ├── heightmap3.png │ │ ├── play.png │ │ ├── sintel.mp4 │ │ ├── spe_bubble.png │ │ ├── spe_bullet.png │ │ ├── spe_bullet2.png │ │ ├── spe_cloud.png │ │ ├── spe_cloudSml.png │ │ ├── spe_flames.jpg │ │ ├── spe_shockwave.png │ │ ├── spe_smokeparticle.png │ │ ├── spe_spark.png │ │ ├── spe_sprite-1x4.jpg │ │ ├── spe_sprite-2x2.jpg │ │ ├── spe_sprite-3x2.jpg │ │ ├── spe_sprite-3x3.jpg │ │ ├── spe_sprite-4x1.jpg │ │ ├── spe_sprite-explosion.png │ │ ├── spe_sprite-explosion2.png │ │ ├── spe_sprite-flame.jpg │ │ ├── spe_sprite-flame2.jpg │ │ ├── spe_sprite-smoke.jpg │ │ ├── spe_star.png │ │ ├── vrum-screenshot.png │ │ ├── vrum-text.png │ │ ├── vrum.png │ │ └── waternormals.jpg └── games │ ├── controller │ ├── game.js │ ├── index.html │ └── style.css │ ├── controller2 │ ├── ControllerScene.js │ ├── LandingScene.js │ ├── game.js │ ├── index.html │ └── style.css │ ├── json-editor │ ├── game.js │ └── index.html │ ├── model-viewer │ ├── index.html │ ├── main.js │ ├── scene.js │ └── utils.js │ ├── project │ ├── assets │ │ ├── favicon.ico │ │ ├── luckiest-guy.LICENSE.txt │ │ ├── luckiest-guy.eot │ │ ├── luckiest-guy.ttf │ │ ├── luckiest-guy.woff │ │ ├── luckiest-guy.woff2 │ │ └── vrum.png │ ├── game.js │ └── index.html │ ├── sandbox │ ├── http.js │ └── index.html │ ├── scene-editor │ ├── GameScene.js │ ├── game.js │ └── index.html │ └── test │ ├── gamepad-api │ ├── Gamepad.js │ └── index.html │ ├── index.html │ ├── main │ ├── CameraTest.js │ ├── FeaturesTest.js │ ├── Main.js │ ├── Scene2.js │ ├── Scene3.js │ ├── SceneLoaderTest.js │ └── index.html │ ├── networking │ ├── Networking.js │ └── index.html │ ├── platformer │ ├── MainScene.js │ ├── Platformer.js │ ├── StatsPanel.js │ └── index.html │ ├── scene-setup │ ├── SceneSetup.js │ └── index.html │ └── threejs_benchmark.html └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | tmp/ 4 | yarn-error.log 5 | 6 | vrum.js 7 | vrum.min.js 8 | 9 | cert.pem 10 | key.pem 11 | workspace/assets/models/*.blend 12 | workspace/assets/models/*.blend1 13 | workspace/assets/models/*.blend2 14 | workspace/assets/models/*-preview.* 15 | workspace/assets/models/*.tar.gz 16 | workspace/assets/models/toexport 17 | workspace/assets/models/LICENSE 18 | 19 | workspace/assets/**/*.glb 20 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | vrum.js engine 10 | 46 | 47 | 48 |
49 |
50 |
51 | 52 |
53 | vrum.js engine 54 |
55 |
56 | tutorials 57 |
58 |
59 | cheatsheet 60 |
61 |
62 | tests 63 |
64 |
65 | model-viewer 66 |
67 |
68 | json-editor 69 |
70 |
71 | scene-editor 72 |
73 |
74 | workspace/games 75 |
76 |
77 |
78 | 79 | 80 | 81 | 82 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vrum", 3 | "version": "0.1.0", 4 | "author": "Cristian Mircea Messel ", 5 | "license": "MIT", 6 | "scripts": { 7 | "postinstall": "yarn build", 8 | "http": "http-server -c-1 -o", 9 | "https": "http-server -c-1 -S -C cert.pem -o", 10 | "genkey": "openssl req -newkey rsa:2048 -new -nodes -x509 -days 3650 -keyout key.pem -out cert.pem", 11 | "build": "node src/tools/build.js", 12 | "dist:exe": "yarn build && yarn clean:tmp && node src/tools/dist.js && cd tmp && yarn install && cd .. && yarn clean:tmp", 13 | "dist:web": "yarn build && node src/tools/publish.js", 14 | "clean:tmp": "rm -rf ./tmp", 15 | "new_game": "node src/tools/newGame.js", 16 | "start": "node src/tools/help.js", 17 | "h": "yarn start" 18 | }, 19 | "dependencies": { 20 | "@tweenjs/tween.js": "^17.2.0", 21 | "camera-controls": "yomotsu/camera-controls", 22 | "ccapture.js": "^1.1.0", 23 | "file-saver": "^1.3.8", 24 | "fontfaceobserver": "^2.1.0", 25 | "howler": "1.1.29", 26 | "html2canvas": "^1.0.0-alpha.12", 27 | "ocean": "jbouny/ocean", 28 | "qrcodejs": "davidshimjs/qrcodejs", 29 | "shader-particle-engine": "squarefeet/ShaderParticleEngine", 30 | "stats.js": "mrdoob/stats.js", 31 | "rstats": "spite/rstats", 32 | "three": "^0.116.0", 33 | "threex.dynamictexture": "jeromeetienne/threex.dynamictexture", 34 | "threex.keyboardstate": "jeromeetienne/threex.keyboardstate", 35 | "threex.volumetricspotlight": "jeromeetienne/threex.volumetricspotlight", 36 | "threex.windowresize": "jeromeetienne/threex.windowresize", 37 | "virtualjoystick.js": "jeromeetienne/virtualjoystick.js" 38 | }, 39 | "devDependencies": { 40 | "opn": "^6.0.0", 41 | "brace": "^0.11.1", 42 | "colors": "^1.3.2", 43 | "concat-files": "^0.1.1", 44 | "gh-pages": "^2.0.1", 45 | "glob": "^7.1.3", 46 | "http-server": "^0.11.1", 47 | "ncp": "^2.0.0", 48 | "uglify-es": "^3.3.9" 49 | }, 50 | "engines": { 51 | "node": ">=10.13" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/engine/Config.js: -------------------------------------------------------------------------------- 1 | // Example usage: 2 | // 3 | // Config.instance.window.resize = true 4 | // Config.instance.renderer.alpha = false 5 | class Config { 6 | constructor() { 7 | this.engine = { 8 | // Enables custom logging (log msg when a model is loaded) 9 | // and other debug features 10 | debug: false, 11 | 12 | // Valid values are number of frames per second. Example: 60 13 | fixedFPS: undefined, 14 | } 15 | this.window = { 16 | // Automatically resize the renderer with the window 17 | resize: true, 18 | 19 | // Allow right click 20 | contextMenu: false, 21 | 22 | // what is says 23 | preventDefaultMouseEvents: true, 24 | 25 | // Show debug stats like fps, MB used, geometries, textures etc. 26 | showStatsOnStart: false, 27 | } 28 | this.renderer = { 29 | // The id added the the canvas element used for drawing. Should not 30 | // start with # 31 | domElementId: 'vrum-dom', 32 | 33 | // Should objects be sorted by the renderer depending on the POV? 34 | sortObjects: true, 35 | 36 | // Enable/disable antialias 37 | antialias: true, 38 | 39 | // don't worry about this one and don't touch it 40 | logarithmicDepthBuffer: false, 41 | 42 | // AKA transparent background 43 | alpha: true, 44 | 45 | // Transparent background color 46 | clearColor: 0x000000, 47 | 48 | // Amount of transparency 49 | clearAlpha: 1, 50 | } 51 | this.shadow = { 52 | details: { 53 | tiny: 'tiny', 54 | low: 'low', 55 | medium: 'medium', 56 | high: 'high', 57 | ultra: 'ultra' 58 | } 59 | } 60 | this.camera = { 61 | // Default camera type 62 | type: 'perspective', 63 | validCameraTypes: ['perspective', 'ortographic'], 64 | 65 | // Default camera field of view 66 | fov: 50, 67 | 68 | // Default camera near 69 | near: 0.1, 70 | 71 | // Default camera far 72 | far: 10000, 73 | } 74 | this.fade = { 75 | // Scene transition fade color 76 | color: 'black', 77 | 78 | // Scene transition fade duration 79 | duration: 1000 80 | } 81 | this.modifiers = { 82 | // Default modifier duration 83 | duration: 1000 84 | } 85 | this.networking = { 86 | // the name of the query param used to get the roomName for MeshNetwork 87 | roomQueryParamName: 'room' 88 | } 89 | this.ui = { 90 | // Order in which html elements are layered 91 | zIndex: { 92 | noWebGL: 1000000, 93 | dom: 10000, 94 | video: 10100, 95 | fade: 20000, 96 | orientation: 30000, 97 | stats: 100000, 98 | console: 200000 99 | }, 100 | video: { 101 | // used internaly to hold the video container element 102 | containerKey: 'vrum.video.container', 103 | 104 | // used internally to lock one video at a time 105 | pendingRemovalKey: 'vrum.video.pendingRemoval', 106 | 107 | supportedFormats: ['mp4', 'ogg', 'ogv'], 108 | }, 109 | addsScene: { 110 | // if the AddsScene is skippable by default 111 | skippable: true, 112 | 113 | // distance to center of the screen, where the panels are located 114 | cameraDistanceZ: 15, 115 | 116 | // how much the images are scaled, 1 img pixel to 1 three.js unit 117 | scaleFactor: 0.01, 118 | 119 | // the total time the item is displayed, including fade duration 120 | itemDisplayDurationSeconds: 5, 121 | 122 | // how long the fade in/out takes of the specific item 123 | fadeDurationMS: 1000, 124 | }, 125 | videoScene: { 126 | // if the VideoScene is skippable by default 127 | skippable: true, 128 | } 129 | } 130 | // Video recorder settings 131 | this.recorder = { 132 | verbose: false, 133 | display: true, 134 | framerate: 60, 135 | quality: 100, 136 | format: 'webm', 137 | frameLimit: 0, 138 | autoSaveTime: 0 139 | } 140 | 141 | this.measure = { 142 | // width of the line drawns for debugging using Measure 143 | lineWidth: 1 144 | } 145 | } 146 | } 147 | 148 | Config.instance = new Config() 149 | -------------------------------------------------------------------------------- /src/engine/Engine.js: -------------------------------------------------------------------------------- 1 | class Engine { 2 | constructor() { 3 | this.running = false 4 | this.frameIndex = null 5 | this.uptime = 0 6 | this.time = undefined 7 | this.renderManager = new RenderManager() 8 | this.inputManager = new InputManager() 9 | 10 | this.tick = this.tick.bind(this) 11 | } 12 | 13 | // Load specified assets, once that is done start the engine 14 | // and run init for the scene 15 | static start(scene, assets, options) { 16 | if (isBlank(scene)) { throw 'scene is blank' } 17 | if (!(scene instanceof Scene)) { throw 'scene param must be an instance of Scene' } 18 | 19 | Hodler.add('scene', scene) 20 | 21 | var renderer = RenderManager.initRenderer() 22 | Hodler.add('rendererDefault', renderer) 23 | Hodler.add('renderer', renderer) 24 | 25 | var camera = RenderManager.initCamera() 26 | Hodler.add('camera', camera) 27 | 28 | var engine = new Engine() 29 | Hodler.instance.add('engine', engine) 30 | 31 | var afterEffects = new AfterEffects() 32 | Hodler.add('afterEffects', afterEffects) 33 | 34 | AssetManager.loadAssets(assets, () => { 35 | Utils.fade({ type: 'out', duration: 1000}) 36 | scene._fullInit(options) 37 | engine.start() 38 | }) 39 | return engine 40 | } 41 | 42 | // Loads specified assets, once that is done, uninits the current scene, 43 | // and witches to the specified scene 44 | static switch(scene, assets, options) { 45 | if (isBlank(scene)) { throw 'scene is blank' } 46 | if (!(scene instanceof Scene)) { throw 'scene param must be an instance of Scene' } 47 | 48 | AssetManager.loadAssets(assets, () => { 49 | var duration = Config.instance.fade.duration 50 | var engine = Hodler.get('engine') 51 | engine.inputManager.disable() 52 | 53 | Utils.fade({ type: 'in', duration: duration }) 54 | Utils.delay(function () { 55 | var oldScene = Hodler.get('scene') 56 | if (Hodler.has('scene')) { 57 | oldScene._fullUninit() 58 | } 59 | Hodler.add('scene', scene) 60 | scene._fullInit(options) 61 | Hodler.get('afterEffects').updateCamAndScene() 62 | Utils.fade({ type: 'out', duration: duration }) 63 | engine.inputManager.enable() 64 | }, duration) 65 | }) 66 | } 67 | 68 | start() { 69 | this.running = true 70 | if (isBlank(Config.instance.engine.fixedFPS)) { 71 | this.tick() 72 | } else { 73 | this.fixedTick() 74 | } 75 | } 76 | 77 | stop() { 78 | cancelAnimationFrame(this.frameIndex) 79 | this.frameIndex = undefined 80 | this.running = false 81 | } 82 | 83 | tick() { 84 | if (!this.running) { 85 | return 86 | } 87 | 88 | RStatsManager.startMeasure() 89 | 90 | let tpf = this._getTimePerFrame() 91 | TWEEN.update() 92 | 93 | RStatsManager.midMeasure() 94 | 95 | this.renderManager.render(tpf) 96 | if (this.takeScreenshot) { 97 | this.takeScreenshot = undefined 98 | Utils.saveScreenshot() 99 | } 100 | 101 | RStatsManager.endMeasure() 102 | 103 | this.frameIndex = requestAnimationFrame(this.tick) 104 | } 105 | 106 | fixedTick() { 107 | let engine = Hodler.get('engine') 108 | if (!engine.running) { 109 | return 110 | } 111 | 112 | RStatsManager.startMeasure() 113 | 114 | let tpf = 1000 / Config.instance.engine.fixedFPS 115 | TWEEN.update() 116 | 117 | RStatsManager.midMeasure() 118 | 119 | setTimeout(() => { 120 | engine.frameIndex = requestAnimationFrame(engine.fixedTick) 121 | }, tpf) 122 | engine.renderManager.render(tpf / 1000) 123 | 124 | if (engine.takeScreenshot) { 125 | engine.takeScreenshot = undefined 126 | Utils.saveScreenshot() 127 | } 128 | 129 | RStatsManager.endMeasure() 130 | } 131 | 132 | _getTimePerFrame() { 133 | const now = new Date().getTime() 134 | const tpf = (now - (this.time || now)) / 1000 135 | this.time = now 136 | this.uptime += tpf 137 | return tpf 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/engine/Hodler.js: -------------------------------------------------------------------------------- 1 | class Hodler { 2 | constructor() { 3 | this.data = {} 4 | } 5 | 6 | add(key, value) { 7 | this.data[key] = value 8 | } 9 | 10 | get(key) { 11 | return this.data[key] 12 | } 13 | 14 | has(key) { 15 | return !isBlank(this.get(key)) 16 | } 17 | 18 | static add(key, value) { 19 | Hodler.instance.add(key, value) 20 | } 21 | 22 | static get(key) { 23 | return Hodler.instance.get(key) 24 | } 25 | 26 | static has(key) { 27 | return Hodler.instance.has(key) 28 | } 29 | } 30 | 31 | Hodler.instance = new Hodler() 32 | -------------------------------------------------------------------------------- /src/engine/InputManager.js: -------------------------------------------------------------------------------- 1 | class InputManager { 2 | constructor() { 3 | this.enabled = undefined 4 | this.enable() 5 | } 6 | 7 | enable() { 8 | this.enabled = true 9 | this.keyboard = new THREEx.KeyboardState() 10 | this._changeEventListener('add') 11 | } 12 | 13 | disable() { 14 | this.enabled = false 15 | this.keyboard.destroy() 16 | this._changeEventListener('remove') 17 | } 18 | 19 | // Delegate touches to mouse events 20 | touchHandler(event) { 21 | const touches = event.changedTouches 22 | const first = touches[0] 23 | let type = '' 24 | switch (event.type) { 25 | case 'touchstart': 26 | type = 'mousedown' 27 | break 28 | case 'touchmove': 29 | type = 'mousemove' 30 | break 31 | case 'touchend': 32 | type = 'mouseup' 33 | break 34 | default: 35 | return 36 | } 37 | // initMouseEvent(type, canBubble, cancelable, view, clickCount, 38 | // screenX, screenY, clientX, clientY, ctrlKey, 39 | // altKey, shiftKey, metaKey, button, relatedTarget) 40 | const simulatedEvent = document.createEvent('MouseEvent') 41 | simulatedEvent.initMouseEvent(type, true, true, window, 1, first.screenX, first.screenY, first.clientX, first.clientY, false, false, false, false, 0, null) 42 | first.target.dispatchEvent(simulatedEvent) 43 | event.preventDefault() 44 | } 45 | 46 | // @nodoc 47 | mouseHandler(event) { 48 | const raycaster = InputManager._parseMouseEvent(event) 49 | if (raycaster != null) { 50 | Hodler.get('scene')._doMouseEvent(event, raycaster) 51 | } 52 | } 53 | 54 | // @nodoc 55 | keyboardHandler(event) { 56 | Hodler.get('scene')._doKeyboardEvent(event) 57 | } 58 | 59 | // @nodoc 60 | wheelHandler(event) { 61 | Hodler.get('engine').inputManager.mouseHandler(event) 62 | } 63 | 64 | gamepadHandler(event) { 65 | Hodler.get('scene')._doGamepadEvent(event) 66 | } 67 | 68 | _changeEventListener(which) { 69 | var renderer = Hodler.get('renderer') 70 | renderer.domElement[which + "EventListener"]("mouseup", this.mouseHandler, false) 71 | renderer.domElement[which + "EventListener"]("mousedown", this.mouseHandler, false) 72 | renderer.domElement[which + "EventListener"]("mousemove", this.mouseHandler, false) 73 | renderer.domElement[which + "EventListener"]("wheel", this.wheelHandler, false) 74 | 75 | document[which + "EventListener"]("keydown", this.keyboardHandler, false) 76 | document[which + "EventListener"]("keyup", this.keyboardHandler, false) 77 | 78 | renderer.domElement[which + "EventListener"]("touchstart", this.touchHandler, false) 79 | renderer.domElement[which + "EventListener"]("touchmove", this.touchHandler, false) 80 | renderer.domElement[which + "EventListener"]("touchend", this.touchHandler, false) 81 | renderer.domElement[which + "EventListener"]("touchcancel", this.touchHandler, false) 82 | 83 | let gamepadSupported = Utils.gamepad() 84 | if (Config.instance.engine.debug) { 85 | console.log(`Gamepad support: ${gamepadSupported}`) 86 | } 87 | if (gamepadSupported) { 88 | window[which + "EventListener"]("gamepadconnected", this.gamepadHandler, false) 89 | window[which + "EventListener"]("gamepaddisconnected", this.gamepadHandler, false) 90 | } 91 | } 92 | 93 | // @nodoc 94 | static _parseMouseEvent(event) { 95 | var renderer = Hodler.get('renderer') 96 | if (Config.instance.window.preventDefaultMouseEvents) { event.preventDefault() } 97 | if (event.target !== renderer.domElement) { return } 98 | 99 | // could need event.clientX or event.clientY 100 | let size = new THREE.Vector2() 101 | renderer.getSize(size) 102 | const mouseX = ((event.layerX / size.x) * 2) - 1 103 | const mouseY = (-(event.layerY / size.y) * 2) + 1 104 | const vector = new THREE.Vector3(mouseX, mouseY, 0.5) 105 | var camera = Hodler.get('camera') 106 | vector.unproject(camera) 107 | return new THREE.Raycaster(camera.position, vector.sub(camera.position).normalize()) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/engine/RenderManager.js: -------------------------------------------------------------------------------- 1 | class RenderManager { 2 | constructor() { 3 | var renderer = Hodler.get('renderer') 4 | var camera = Hodler.get('camera') 5 | 6 | if (Config.instance.window.resize) { 7 | // winResize.destroy() 8 | this.winResize = new THREEx.WindowResize(renderer, camera) 9 | } 10 | 11 | if (!Config.instance.window.contextMenu) { 12 | renderer.domElement.addEventListener('contextmenu', function (e) { 13 | e.preventDefault() 14 | }, false) 15 | } 16 | 17 | if (Config.instance.window.showStatsOnStart) { 18 | StatsManager.toggle() 19 | } 20 | 21 | this.anaglyphEffect = new THREE.AnaglyphEffect(renderer) 22 | this.stereoEffect = new THREE.StereoEffect(renderer) 23 | 24 | this.appendDom() 25 | } 26 | 27 | static initRenderer(rendererType) { 28 | if (isBlank(rendererType)) { rendererType = THREE.WebGLRenderer } 29 | if (!Utils.webgl() && rendererType == THREE.WebGLRenderer) { rendererType = PolyfillRenderer } 30 | 31 | var renderer = new rendererType(Config.instance.renderer) 32 | renderer.domElement.setAttribute('id', Config.instance.renderer.domElementId) 33 | renderer.domElement.style['z-index'] = Config.instance.ui.zIndex.dom 34 | renderer.setClearColor(Config.instance.renderer.clearColor, Config.instance.renderer.clearAlpha) 35 | renderer.setSize(window.innerWidth, window.innerHeight) 36 | return renderer 37 | } 38 | 39 | static initCamera() { 40 | let camera = Utils.camera({ type: Config.instance.camera.type }) 41 | return camera 42 | } 43 | 44 | setWidthHeight(size) { 45 | if (isBlank(size)) { throw 'size can not be blank' } 46 | if (isBlank(size.width)) { throw 'size.width can not be blank' } 47 | if (isBlank(size.height)) { throw 'size.width can not be blank' } 48 | 49 | var renderer = Hodler.get('renderer') 50 | var camera = Hodler.get('camera') 51 | 52 | this.anaglyphEffect.setSize(size.width, size.height) 53 | this.stereoEffect.setSize(size.width, size.height) 54 | camera.aspect = size.width / size.height 55 | camera.updateProjectionMatrix() 56 | renderer.setSize(size.width, size.height) 57 | } 58 | 59 | render(tpf) { 60 | var renderer = Hodler.get('renderer') 61 | var rendererDefault = Hodler.get('rendererDefault') 62 | var scene = Hodler.get('scene') 63 | var afterEffects = Hodler.get('afterEffects') 64 | 65 | StatsManager.update(rendererDefault) 66 | scene._fullTick(tpf) 67 | if (afterEffects.enabled) { 68 | afterEffects.render(tpf) 69 | } else { 70 | var camera = Hodler.get('camera') 71 | renderer.render(scene, camera) 72 | } 73 | VideoRecorderManager.capture(rendererDefault.domElement) 74 | } 75 | 76 | appendDom() { 77 | var renderer = Hodler.get('renderer') 78 | document.body.appendChild(renderer.domElement) 79 | } 80 | 81 | removeDom() { 82 | var renderer = Hodler.get('renderer') 83 | 84 | if (renderer.domElement.parentNode === null) { return } 85 | try { 86 | document.body.removeChild(renderer.domElement) 87 | } catch (e) { 88 | console.error(e) 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/engine/Scene.js: -------------------------------------------------------------------------------- 1 | class Scene extends THREE.Scene { 2 | constructor() { 3 | super() 4 | // automatically incremented if the scene is ticking 5 | this.uptime = 0 6 | this.initialized = false 7 | // use this to prevent double actions like double load when the scene is 8 | // finished 9 | this.finished = false 10 | this.intervals = [] 11 | this.timeouts = [] 12 | // does not need to be refrehsed, support for gamepad can't change 13 | // on the same device 14 | this.gamepadSupported = Utils.gamepad() 15 | } 16 | 17 | init(options) { 18 | } 19 | 20 | _fullInit(options) { 21 | if (isBlank(options)) { options = {} } 22 | this.intervals = [] 23 | this.timeouts = [] 24 | this.init(options) 25 | this.initialized = true 26 | this.finished = false 27 | } 28 | 29 | uninit() { 30 | } 31 | 32 | _fullUninit() { 33 | this.initialized = false 34 | this.removeAllChildren() 35 | this.intervals.forEach((interval) => { 36 | clearInterval(interval) 37 | }) 38 | this.timeouts.forEach((timeout) => { 39 | clearTimeout(timeout) 40 | }) 41 | this.uninit() 42 | this.finished = false 43 | } 44 | 45 | _fullTick(tpf) { 46 | if (!this.initialized) { 47 | return 48 | } 49 | this.uptime += tpf 50 | this._tickAnimations(tpf) 51 | this.tick(tpf) 52 | if (this.gamepadSupported) { 53 | this._doGamepadEvent(navigator.getGamepads()) 54 | } 55 | } 56 | 57 | _tickAnimations(tpf) { 58 | this.traverse(function (obj) { 59 | if (obj.animations instanceof Animations) { 60 | obj.animations.tick(tpf) 61 | } 62 | if (obj instanceof Water) { 63 | obj.tick(tpf) 64 | } 65 | if (obj instanceof BaseParticle) { 66 | obj.tick(tpf) 67 | } 68 | }) 69 | } 70 | 71 | getCamera() { 72 | let camera = Hodler.get('camera') 73 | if (isBlank(camera)) { throw 'camera is blank' } 74 | return camera 75 | } 76 | 77 | tick(tpf) {} 78 | 79 | _doMouseEvent(event, raycaster) { 80 | if (!this.initialized) { 81 | return 82 | } 83 | this.doMouseEvent(event, raycaster) 84 | } 85 | 86 | doMouseEvent(event, raycaster) {} 87 | 88 | _doKeyboardEvent(event) { 89 | if (!this.initialized) { 90 | return 91 | } 92 | this.doKeyboardEvent(event) 93 | } 94 | 95 | doKeyboardEvent(event) {} 96 | 97 | _doGamepadEvent(event) { 98 | if (!this.initialized) { 99 | return 100 | } 101 | if (!(event.type == 'gamepaddisconnected' || event.type == 'gamepadconnected')) { 102 | // set as a custom type to make it easier to work with 103 | event.type = 'gamepadtick-vrum' 104 | 105 | let isConnected = false 106 | for (var i = 0; i < event.length; i++) { 107 | isConnected = isConnected || !isBlank(event[i]) 108 | } 109 | if (!isConnected) { 110 | return 111 | } 112 | } 113 | this.doGamepadEvent(event) 114 | } 115 | 116 | /* 117 | * Called if gamepad is supported each frame. Also handles connect/disconnect 118 | * 119 | * All events have a type: 120 | * 121 | * - gamepaddisconnected 122 | * - gamepadconnected 123 | * - gamepadtick-vrum - custom, added by the engine to GamepadList 124 | * 125 | * All events are streamlined into this method. Depending on what you need 126 | * take the appropriate action. Keep in mind, the scene could not yet be 127 | * initialized when the gamepad is initialized so don't count on getting 128 | * the gamepadconnected event every time the scene starts. 129 | */ 130 | doGamepadEvent(event) {} 131 | 132 | setInterval(func, time) { 133 | this.intervals.push(setInterval(func, time)) 134 | } 135 | 136 | setTimeout(func, time) { 137 | this.timeouts.push(setTimeout(func, time)) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/extras/AfterEffects.js: -------------------------------------------------------------------------------- 1 | // Used to generate after effects and enable/disable on a scene/camera basis 2 | // 3 | // Example usage: 4 | // 5 | // Hodler.get('afterEffects').enable() 6 | // 7 | class AfterEffects { 8 | constructor() { 9 | this.enabled = false 10 | this.renderModel = new (THREE.RenderPass)(undefined, undefined) // scene, camera 11 | } 12 | 13 | render(tpf) { 14 | Hodler.get('renderer').clear() 15 | this.composer.render(tpf) 16 | } 17 | 18 | enable() { 19 | this.updateCamAndScene() 20 | this.composer = new THREE.EffectComposer(Hodler.get('renderer')) 21 | this.effects() 22 | this.enabled = true 23 | } 24 | 25 | disable() { 26 | this.enabled = false 27 | } 28 | 29 | toggle() { 30 | this.enabled ? this.disable() : this.enable() 31 | } 32 | 33 | updateCamAndScene() { 34 | this.renderModel.camera = Hodler.get('camera') 35 | this.renderModel.scene = Hodler.get('scene') 36 | } 37 | 38 | // Override this method with the desired effect. See AfterEffects.bloomFilm for 39 | // more details. 40 | effects() { 41 | } 42 | 43 | // These are methods which pre-define different effects 44 | // 45 | // Example usage: 46 | // 47 | // AfterEffects.prototype.effects = AfterEffects.bloomFilm 48 | 49 | static bloomFilm() { 50 | const effectBloom = new THREE.BloomPass(1, 5, 1.0, 2048) 51 | const effectFilm = new THREE.FilmPass(0.15, 0.95, 2048, false) 52 | effectFilm.renderToScreen = true 53 | 54 | this.composer.addPass(this.renderModel) 55 | this.composer.addPass(effectBloom) 56 | this.composer.addPass(effectFilm) 57 | } 58 | 59 | static bloomCopy() { 60 | const effectBloom = new (THREE.BloomPass)(1.25) 61 | const effectCopy = new (THREE.ShaderPass)(THREE.CopyShader) 62 | effectCopy.renderToScreen = true 63 | 64 | this.composer.addPass(this.renderModel) 65 | this.composer.addPass(effectBloom) 66 | this.composer.addPass(effectCopy) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/extras/CyclicArray.js: -------------------------------------------------------------------------------- 1 | // Used for next() and prev() 2 | // 3 | // a = [1, 2, 3] 4 | // ca = a.toCyclicArray() 5 | // ca.next() 6 | // 7 | class CyclicArray { 8 | constructor(items) { 9 | if (isBlank(items)) { items = [] } 10 | this.items = items 11 | this.index = 0 12 | } 13 | 14 | get() { 15 | return this.items[this.index] 16 | } 17 | 18 | setIndexByValue(item) { 19 | this.index = this.items.indexOf(item) 20 | if (this.index < 0) { 21 | console.warn('did not find item in CyclicArray') 22 | this.index = 0 23 | } 24 | } 25 | 26 | next() { 27 | this.index += 1 28 | if (this.index > (this.items.size() - 1)) { this.index = 0 } 29 | return this.get() 30 | } 31 | 32 | prev() { 33 | this.index -= 1 34 | if (this.index < 0) { this.index = this.items.size() - 1 } 35 | return this.get() 36 | } 37 | 38 | size() { 39 | return this.items.size() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/extras/Modifiers.js: -------------------------------------------------------------------------------- 1 | // https://github.com/tweenjs/tween.js/blob/master/docs/user_guide.md 2 | // 3 | // http://tweenjs.github.io/tween.js/examples/03_graphs.html 4 | // 5 | // Example usage: 6 | // 7 | // var up = new BaseModifier(cube.position, { x: '+1' }, 1000, TWEEN.Easing.Linear.None) 8 | // var down = new BaseModifier(cube.position, { x: '-1' }) 9 | // up.chain(down) 10 | // down.chain(up) 11 | // up.start() 12 | // 13 | class BaseModifier extends TWEEN.Tween { 14 | constructor(subject, target, duration, easing) { 15 | super(subject) 16 | 17 | if (duration == null) { duration = Config.instance.modifiers.duration } 18 | if (easing == null) { easing = TWEEN.Easing.Linear.None } 19 | this.easing(easing); 20 | 21 | this.to(target, duration) 22 | .onStart(function () { 23 | }) 24 | .onUpdate(function() { 25 | }) 26 | .onComplete(function (obj) { 27 | }) 28 | .onStop(function(obj) { 29 | }) 30 | } 31 | 32 | // only works when repeat is used 33 | yoyo() { 34 | return super.yoyo() 35 | } 36 | 37 | // amount can be Infinity or an int 38 | repeat(amount) { 39 | return super.repeat(amount) 40 | } 41 | 42 | easing(easing) { 43 | return super.easing(easing) 44 | } 45 | 46 | delay(amount) { 47 | return super.delay(amount) 48 | } 49 | 50 | chain(tweens) { 51 | return super.chain(tweens) 52 | } 53 | } 54 | 55 | // NOTE: chaining does not work with fade modifier 56 | class FadeModifier extends BaseModifier { 57 | constructor(subject, fromAlpha, toAlpha, duration, easing) { 58 | super({ x: fromAlpha}, { x: toAlpha }, duration, easing) 59 | this.onUpdate(function (obj) { 60 | subject.setOpacity(obj.x) 61 | }) 62 | } 63 | } 64 | 65 | class WeightModifier extends BaseModifier { 66 | constructor(subject, fromWeight, toWeight, duration, easing) { 67 | super({ x: fromWeight }, { x: toWeight }, duration, easing) 68 | this.onUpdate(function (obj) { 69 | subject.setEffectiveWeight(obj.x) 70 | }) 71 | } 72 | } 73 | 74 | class ScaleModifier extends BaseModifier { 75 | constructor(subject, fromScale, toScale, duration, easing) { 76 | subject.scale.setScalar(fromScale) 77 | super(subject.scale, { x: toScale, y: toScale, z: toScale}, duration, easing) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/extras/Playlist.js: -------------------------------------------------------------------------------- 1 | // Used for continously looping sounds from the SoundManager 2 | class Playlist { 3 | // @param [Array] keys 4 | constructor(keys) { 5 | if (!(keys instanceof Array)) { throw new Error('keys needs to be an array') } 6 | for (let key of Array.from(keys)) { 7 | if (!SoundManager.has(key)) { 8 | throw new Error(`key '${key}' not loaded in SoundManager`) 9 | } 10 | } 11 | this.items = new CyclicArray(keys) 12 | } 13 | 14 | // start playing the playlist 15 | // 16 | // @example 17 | // playlist = new Playlist(['shotgun', 'hit']) 18 | // playlist.play() 19 | // 20 | // @see getPlayingKey 21 | cmd(options){ 22 | let audio 23 | options.key = this.items.get() 24 | if (options.type === 'volumeAll') { 25 | options.type = 'volume' 26 | for (let item of Array.from(this.items.items)) { 27 | options.key = item 28 | SoundManager.cmd(options) 29 | } 30 | } else { 31 | audio = SoundManager.cmd(options) 32 | } 33 | if (['play', 'fadeIn'].includes(options.type)) { 34 | audio._onend = [] 35 | return audio.on('end', data => { 36 | this.items.next() 37 | return this.cmd(options) 38 | }) 39 | } else if (['volume', 'volumeAll'].includes(options.type)) { 40 | // do nothing 41 | } else { 42 | return audio._onend = [] 43 | } 44 | } 45 | 46 | // Get the key of the sound currently playing 47 | // 48 | // @example 49 | // playlist = new Playlist(['shotgun', 'hit']) 50 | // playlist.play() 51 | // SoundManager.pause(playlist.getPlayingKey()) 52 | getPlayingKey() { 53 | return this.items.get() 54 | } 55 | 56 | // Get the audio object which is currently playing 57 | getPlayingAudio() { 58 | return SoundManager.get().items[this.getPlayingKey()] 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/extras/PolyfillRenderer.js: -------------------------------------------------------------------------------- 1 | // Displays a message if WebGL is not supported. 2 | // 3 | // To help customize the message there are 2 relevant ids: 4 | // 5 | // * vrum-webgl-warning-container 6 | // * vrum-webgl-warning-text 7 | // 8 | // For more detailed customizations, you can override 9 | // 10 | // * makeDomElement 11 | // * makeContainerElement 12 | // 13 | class PolyfillRenderer extends THREE.WebGLRenderer { 14 | constructor(parameters) { 15 | super(parameters) 16 | this.domElement = PolyfillRenderer.makeDomElement() 17 | } 18 | 19 | render(scene, camera) {} 20 | 21 | static makeContainerElement() { 22 | const element = document.createElement('div') 23 | element.setAttribute('id', 'vrum-webgl-warning-container') 24 | element.style.display = 'flex' 25 | element.style.position = 'absolute' 26 | element.style.width = '100%' 27 | element.style.height = '100%' 28 | element.style['align-items'] = 'center' 29 | element.style['text-align'] = 'center' 30 | element.style['background-color'] = 'black' 31 | element.style['color'] = 'white' 32 | element.style['z-index'] = Config.instance.ui.zIndex.noWebGL 33 | return element; 34 | } 35 | 36 | static makeDomElement() { 37 | let element = this.makeContainerElement() 38 | 39 | const text = document.createElement('div') 40 | text.setAttribute('id', 'vrum-webgl-warning-text') 41 | text.style.width = '100%' 42 | text.innerHTML = 'WebGL not supported' 43 | element.append(text) 44 | 45 | return element 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/extras/RStatsManager.js: -------------------------------------------------------------------------------- 1 | class RStatsManager { 2 | constructor() { 3 | this.glS = new glStats(); // init at any point 4 | this.tS = new threeStats( Hodler.get('renderer') ); // init after WebGLRenderer is created 5 | this.rS = new rStats( { 6 | CSSPath: '/src/vendor/', 7 | values: { 8 | frame: { caption: 'Total frame time (ms)', over: 16 }, 9 | fps: { caption: 'Framerate (FPS)', below: 30 }, 10 | calls: { caption: 'Calls (three.js)', over: 3000 }, 11 | raf: { caption: 'Time since last rAF (ms)' }, 12 | rstats: { caption: 'rStats update (ms)' } 13 | }, 14 | groups: [ 15 | { caption: 'Framerate', values: [ 'fps', 'raf' ] }, 16 | { caption: 'Frame Budget', values: [ 'frame', 'texture', 'setup', 'render' ] } 17 | ], 18 | fractions: [ 19 | { base: 'frame', steps: [ 'action1', 'render' ] } 20 | ], 21 | plugins: [ 22 | this.tS, 23 | this.glS 24 | ] 25 | } ); 26 | } 27 | 28 | static startMeasure() { 29 | if (isBlank(RStatsManager.instance)) { return } 30 | let inst = RStatsManager.instance 31 | 32 | inst.rS( 'frame' ).start(); 33 | inst.glS.start(); 34 | 35 | inst.rS( 'frame' ).start(); 36 | inst.rS( 'rAF' ).tick(); 37 | inst.rS( 'FPS' ).frame(); 38 | 39 | inst.rS( 'action1' ).start(); 40 | } 41 | 42 | static midMeasure() { 43 | if (isBlank(RStatsManager.instance)) { return } 44 | let inst = RStatsManager.instance 45 | 46 | inst.rS( 'action1' ).end(); 47 | inst.rS( 'render' ).start(); 48 | } 49 | 50 | static endMeasure() { 51 | if (isBlank(RStatsManager.instance)) { return } 52 | let inst = RStatsManager.instance 53 | inst.rS( 'render' ).end(); 54 | 55 | inst.rS( 'frame' ).end(); 56 | inst.rS().update(); 57 | } 58 | 59 | static toggleStats() { 60 | RStatsManager.instance = new RStatsManager() 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/extras/ShaderLib.js: -------------------------------------------------------------------------------- 1 | THREE.ShaderLib['gradient'] = { 2 | vertexShader: [ 3 | "varying vec3 vWorldPosition;", 4 | 5 | "void main() {", 6 | " vec4 worldPosition = modelMatrix * vec4( position, 1.0 );", 7 | " vWorldPosition = worldPosition.xyz;", 8 | " gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );", 9 | "}" 10 | ].join("\n"), 11 | fragmentShader: [ 12 | "uniform vec3 topColor;", 13 | "uniform vec3 bottomColor;", 14 | "uniform float offset;", 15 | "uniform float exponent;", 16 | 17 | "varying vec3 vWorldPosition;", 18 | 19 | "void main() {", 20 | " float h = normalize( vWorldPosition + offset ).y;", 21 | " gl_FragColor = vec4( mix( bottomColor, topColor, max( pow( max( h , 0.0), exponent ), 0.0 ) ), 1.0 );", 22 | "}" 23 | ].join("\n") 24 | }; 25 | 26 | THREE.ShaderLib['sample'] = { 27 | vertexShader: [ 28 | "" 29 | ].join("\n"), 30 | fragmentShader: [ 31 | "" 32 | ].join("\n") 33 | }; 34 | -------------------------------------------------------------------------------- /src/extras/ShaderMaterial.js: -------------------------------------------------------------------------------- 1 | // Example usage: 2 | // 3 | // let material = new ShaderMaterial('basic_shader.json', function (tpf) { 4 | // this.uniforms.time.value += tpf * 2 5 | // }) 6 | // material.tick(tpf) 7 | // 8 | // 9 | // let material = new ShaderMaterial('dissolve_shader.json', function (tpf) { 10 | // if (this.uniforms.dissolve.value > 1) { 11 | // this.uniforms.dissolve.value = 0 12 | // } 13 | // this.uniforms.dissolve.value += tpf 14 | // }) 15 | // material.tick(tpf) 16 | // 17 | class ShaderMaterial extends THREE.ShaderMaterial { 18 | constructor(json, customTick) { 19 | if (isBlank(json)) { throw "shader is blank, missing json param" } 20 | 21 | if (!('tick' in json)) { throw `missing tick for shader` } 22 | if (!('uniforms' in json)) { throw `missing uniforms for shader` } 23 | if (!('vertex' in json)) { throw `missing uniforms for shader` } 24 | if (!('fragment' in json)) { throw `missing uniforms for shader` } 25 | 26 | let evalUniforms 27 | eval("evalUniforms = " + arrayOrStringToString(json.uniforms)) 28 | 29 | let evalTick 30 | if (isBlank(customTick)) { 31 | eval("evalTick = " + arrayOrStringToString(json.tick)) 32 | } else { 33 | if (customTick instanceof Function) { 34 | evalTick = customTick 35 | } else { 36 | eval("evalTick = " + arrayOrStringToString(customTick)) 37 | } 38 | } 39 | 40 | super({ 41 | morphTargets: true, // TODO: do we want this all the time? 42 | uniforms: evalUniforms, 43 | vertexShader: arrayOrStringToString(json.vertex), 44 | fragmentShader: arrayOrStringToString(json.fragment), 45 | flatShading: THREE.SmoothShading // TODO: do we want this all the time? 46 | }) 47 | 48 | this.customTick = evalTick 49 | } 50 | 51 | tick(tpf) { 52 | this.customTick(tpf) 53 | } 54 | 55 | // used with dissolve_shader.json 56 | // 57 | // @example 58 | // material = Helper.setDissolveMaterialColor(material, 0, 0, 1) 59 | static setDissolveMaterialColor(dm, r, g, b) { 60 | if (dm == null) { new Error('missing dm param'); } 61 | r = parseFloat(r).toFixed(1); 62 | g = parseFloat(g).toFixed(1); 63 | b = parseFloat(b).toFixed(1); 64 | dm.fragmentShader = dm.fragmentShader.replace(' color.r = 1.0; color.g = 0.5; color.b = 0.0;', ` color.r = ${r}; color.g = ${g}; color.b = ${b};`); 65 | return dm; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/extras/StatsManager.js: -------------------------------------------------------------------------------- 1 | /* 2 | * decaffeinate suggestions: 3 | * DS102: Remove unnecessary code created because of implicit returns 4 | * DS206: Consider reworking classes to avoid initClass 5 | * DS207: Consider shorter variations of null checks 6 | * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md 7 | */ 8 | // @nodoc 9 | 10 | var StatsManager = (function() { 11 | let instance = undefined; 12 | StatsManager = class StatsManager { 13 | static initClass() { 14 | 15 | instance = null; 16 | 17 | // Handles stats 18 | const Cls = (Singleton.StatsManager = class StatsManager { 19 | static initClass() { 20 | this.prototype.statsVisible = false; 21 | } 22 | 23 | // @nodoc 24 | constructor() { 25 | var fpsStats = new Stats() 26 | fpsStats.domElement.style['z-index'] = '' 27 | this.fpsStats = fpsStats 28 | this.rendererStats = new THREEx.RendererStats(); 29 | 30 | this.container = document.createElement('div') 31 | this.container.style['z-index'] = Config.instance.ui.zIndex.stats 32 | this.container.style.position = 'absolute' 33 | this.container.style.top = '0px' 34 | this.container.style.left = '0px' 35 | 36 | this._showAll = this._showAll.bind(this) 37 | this.container.addEventListener('click', this._showAll) 38 | this._showAll() 39 | 40 | this.rendererStats.domElement.style.position = 'absolute'; 41 | this.rendererStats.domElement.style.top = '144px'; 42 | 43 | this.container.appendChild(this.fpsStats.domElement) 44 | this.container.appendChild(this.rendererStats.domElement) 45 | } 46 | 47 | _showAll() { 48 | Array.from(this.fpsStats.domElement.children).forEach(function (canvas) { 49 | canvas.style.display = 'block' 50 | }) 51 | } 52 | 53 | // Toggles the visibility of the stats 54 | toggle() { 55 | this.statsVisible = !this.statsVisible; 56 | if (this.statsVisible) { 57 | document.body.appendChild(this.container) 58 | } else { 59 | document.body.removeChild(this.container) 60 | } 61 | return this.statsVisible; 62 | } 63 | 64 | // Set stat visibility 65 | setVisible(value) { 66 | if (value !== this.statsVisible) { 67 | return this.toggle(); 68 | } 69 | } 70 | 71 | // @nodoc 72 | update(renderer) { 73 | if (!this.statsVisible) { return; } 74 | this.fpsStats.update() 75 | this.rendererStats.update(renderer) 76 | } 77 | }); 78 | Cls.initClass(); 79 | } 80 | 81 | static get() { 82 | return instance != null ? instance : (instance = new Singleton.StatsManager()); 83 | } 84 | 85 | static toggle() { 86 | return this.get().toggle(); 87 | } 88 | 89 | static setVisible() { 90 | return this.get().setVisible(); 91 | } 92 | 93 | static update(renderer) { 94 | return this.get().update(renderer); 95 | } 96 | }; 97 | StatsManager.initClass(); 98 | return StatsManager; 99 | })(); 100 | -------------------------------------------------------------------------------- /src/extras/VideoRecorderManager.js: -------------------------------------------------------------------------------- 1 | /* 2 | * decaffeinate suggestions: 3 | * DS102: Remove unnecessary code created because of implicit returns 4 | * DS206: Consider reworking classes to avoid initClass 5 | * DS207: Consider shorter variations of null checks 6 | * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md 7 | */ 8 | // @nodoc 9 | var VideoRecorderManager = (function() { 10 | let instance = undefined; 11 | VideoRecorderManager = class VideoRecorderManager { 12 | static initClass() { 13 | 14 | instance = null; 15 | 16 | Singleton.VideoRecorderManager = class VideoRecorderManager { 17 | 18 | constructor() { 19 | this.recording = false; 20 | } 21 | 22 | capture(domElement) { 23 | if (this.recording === false) { return; } 24 | this.recorder.capture(domElement); 25 | } 26 | 27 | start() { 28 | if (this.recorder == null) { 29 | this.recorder = new CCapture(Config.instance.recorder); 30 | } 31 | this.recorder.start(); 32 | this.recording = true; 33 | } 34 | 35 | stop() { 36 | this.recorder.stop(); 37 | this.recording = false; 38 | this.recorder.save() 39 | } 40 | 41 | isRecording() { 42 | return this.recording 43 | } 44 | }; 45 | } 46 | 47 | static get() { 48 | return instance != null ? instance : (instance = new Singleton.VideoRecorderManager()); 49 | } 50 | 51 | static start() { 52 | this.get().start(); 53 | } 54 | 55 | static stop() { 56 | this.get().stop(); 57 | } 58 | 59 | static isRecording() { 60 | return this.get().isRecording() 61 | } 62 | 63 | static capture(domElement) { 64 | this.get().capture(domElement); 65 | } 66 | }; 67 | VideoRecorderManager.initClass(); 68 | return VideoRecorderManager; 69 | })(); 70 | -------------------------------------------------------------------------------- /src/extras/controls/RTSCamera.js: -------------------------------------------------------------------------------- 1 | // Remember to check oc.zoomSensitivity 2 | // 3 | // Example usage: 4 | // 5 | // this.rtsCam = new RTSCamera() 6 | // this.rtsCam.tick(tpf) 7 | // this.rtsCam.doMouseEvent(event) 8 | // this.rtsCam.toggle() 9 | // 10 | class RTSCamera { 11 | constructor() { 12 | this.touch = Utils.isMobileOrTablet() 13 | 14 | let oc = Utils.toggleOrbitControls(THREE.CustomOrbitControls) 15 | oc.touch = this.touch 16 | 17 | oc.minDistance = 3 18 | oc.maxDistance = 50 19 | 20 | oc.enableDamping = true 21 | oc.dampingFactor = 0.07; 22 | oc.zoomDampingFactor = 0.1; 23 | 24 | oc.enableRotate = false 25 | oc.minPolarAngle = 0.2; 26 | oc.maxPolarAngle = 1.4; 27 | 28 | oc.panSpeed = 0.1 29 | oc.keyPanSpeed = 2 30 | oc.rotateSpeed = 0.05; 31 | 32 | oc.panBound = true 33 | // oc.panBoundRectangle = new THREE.Vector4(-10, 10, -10, 10) 34 | 35 | // TODO: might need to be scaled to height 36 | oc.zoomSensitivity = 150 // number of pixels needed to be considered zoom 37 | 38 | this.oc = oc 39 | this.enabled = true 40 | 41 | this.percentOfWidthPan = 5 42 | this.percentOfHeightPan = 5 43 | 44 | this.size = this.getSize() 45 | this.lastX = this.size.x / 2 46 | this.lastY = this.size.y / 2 47 | } 48 | 49 | toggle() { 50 | this.enabled = !this.enabled 51 | this.oc.enabled = this.enabled 52 | return this.enabled 53 | } 54 | 55 | tick(tpf) { 56 | if (!this.enabled) { return } 57 | 58 | if (Config.instance.window.resize) { 59 | this.size = this.getSize() 60 | } 61 | 62 | if (!this.touch) { 63 | this.edgePan() 64 | } 65 | this.oc.update() 66 | } 67 | 68 | doMouseEvent(event) { 69 | if (!this.enabled) { return } 70 | 71 | if (event.type == 'mousemove') { 72 | this.lastY = event.y 73 | this.lastX = event.x 74 | } 75 | } 76 | 77 | edgePan() { 78 | if (this.isRotating()) { 79 | return 80 | } 81 | 82 | if (this.lastY < (this.size.y * this.percentOfHeightPan) / 100) { 83 | this.oc.handleKeyDown({ 84 | 'keyCode': this.oc.keys.UP, 85 | 'preventDefault': () => { 86 | } 87 | }) 88 | } 89 | if (this.lastY > (this.size.y - (this.size.y * this.percentOfHeightPan) / 100)) { 90 | this.oc.handleKeyDown({ 91 | 'keyCode': this.oc.keys.BOTTOM, 92 | 'preventDefault': () => { 93 | } 94 | }) 95 | } 96 | if (this.lastX < (this.size.x * this.percentOfWidthPan) / 100) { 97 | this.oc.handleKeyDown({ 98 | 'keyCode': this.oc.keys.LEFT, 99 | 'preventDefault': () => { 100 | } 101 | }) 102 | } 103 | if (this.lastX > (this.size.x - (this.size.x * this.percentOfWidthPan) / 100)) { 104 | this.oc.handleKeyDown({ 105 | 'keyCode': this.oc.keys.RIGHT, 106 | 'preventDefault': () => { 107 | } 108 | }) 109 | } 110 | } 111 | 112 | getSize() { 113 | let size = new THREE.Vector2() 114 | Hodler.get('renderer').getSize(size) 115 | return size 116 | } 117 | 118 | getState() { 119 | return this.oc.getState() 120 | } 121 | 122 | isStatic() { 123 | return this.getState() === this.oc.STATE.NONE 124 | } 125 | 126 | isRotating() { 127 | return this.getState() === this.oc.STATE.ROTATE 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/extras/controls/RTSCamera2.js: -------------------------------------------------------------------------------- 1 | // https://github.com/yomotsu/camera-controls 2 | class RTSCamera2 { 3 | constructor() { 4 | this.touch = Utils.isMobileOrTablet() 5 | 6 | CameraControls.install( { THREE: THREE } ); 7 | const cameraControls = new CameraControls(Hodler.get('camera'), Hodler.get('renderer').domElement); 8 | this.cameraControls = cameraControls 9 | } 10 | 11 | tick(tpf) { 12 | this.cameraControls.update(tpf); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/extras/controls/RayScanner.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Example usage: 3 | * 4 | * let rayScanner = new RayScanner([this.island, this.barrel, this.wall]) 5 | * // rayScanner.collidables = [...] 6 | * // rayScanner.drawLines = true 7 | * this.rayScanner = rayScanner 8 | * 9 | * // in tick(tpf) 10 | * let fromPosition = this.tank.position.clone() 11 | * fromPosition.y += 2 12 | * this.rayScanner.scan(fromPosition, this.control.velocity) 13 | * 14 | * if (this.rayScanner.addX) { this.tank.position.x += this.control.velocity.x } 15 | * if (this.rayScanner.addZ) { this.tank.position.z += this.control.velocity.z } 16 | * 17 | */ 18 | class RayScanner { 19 | constructor(collidables) { 20 | this.raycaster = new THREE.Raycaster() 21 | this.addX = true 22 | this.addY = true 23 | this.addZ = true 24 | this.drawLines = false 25 | this.collidables = collidables 26 | this.lineLength = 3 27 | } 28 | 29 | scan(fromPosition, velocity) { 30 | this.addX = true 31 | this.addY = true 32 | this.addZ = true 33 | 34 | if (this.hasIntersections(fromPosition, new THREE.Vector3(1, 0, 0)) && velocity.x > 0) { 35 | this.addX = false 36 | } 37 | 38 | if (this.hasIntersections(fromPosition, new THREE.Vector3(-1, 0, 0)) && velocity.x < 0) { 39 | this.addX = false 40 | } 41 | 42 | if (this.hasIntersections(fromPosition, new THREE.Vector3(0, 1, 0)) && velocity.y < 0) { 43 | this.addY = false 44 | } 45 | 46 | if (this.hasIntersections(fromPosition, new THREE.Vector3(0, -1, 0)) && velocity.y > 0) { 47 | this.addY = false 48 | } 49 | 50 | if (this.hasIntersections(fromPosition, new THREE.Vector3(0, 0, 1)) && velocity.z > 0) { 51 | this.addZ = false 52 | } 53 | 54 | if (this.hasIntersections(fromPosition, new THREE.Vector3(0, 0, -1)) && velocity.z < 0) { 55 | this.addZ = false 56 | } 57 | } 58 | 59 | // TODO reuse scanDirection so it doens't get created all the time 60 | scanEdges(fromPosition, halfWidth, scanDirection) { 61 | let from1 = fromPosition.clone() 62 | from1.x -= halfWidth 63 | let from2 = fromPosition.clone() 64 | from2.x += halfWidth 65 | let interDown = this.getIntersections(from1, scanDirection.clone()) 66 | let interDown2 = this.getIntersections(from2, scanDirection.clone()) 67 | return interDown.concat(interDown2) 68 | } 69 | 70 | addCollidable(obj) { 71 | if (isBlank(obj.boundingCube)) { 72 | this.collidables.pushUnique(obj) 73 | } else { 74 | this.collidables.pushUnique(obj.boundingCube) 75 | } 76 | } 77 | 78 | removeCollidable(obj) { 79 | if (isBlank(obj.boundingCube)) { 80 | this.collidables.remove(obj) 81 | } else { 82 | this.collidables.remove(obj.boundingCube) 83 | } 84 | } 85 | 86 | hasIntersections(fromPosition, direction) { 87 | let length = this.lineLength 88 | let inters = Measure.hasIntersectionsFrom(this.raycaster, this.collidables, fromPosition, direction, length) 89 | 90 | let color = inters ? 'red' : 'green' 91 | if (this.drawLines) { 92 | Measure.addLineDirection(fromPosition, direction, length, color) 93 | } 94 | 95 | return inters 96 | } 97 | 98 | getIntersections(fromPosition, direction) { 99 | let length = this.lineLength 100 | let inters = Measure.getIntersectionsFrom(this.raycaster, this.collidables, fromPosition, direction, length) 101 | 102 | let color = inters.any() ? 'red' : 'green' 103 | if (this.drawLines) { 104 | Measure.addLineDirection(fromPosition, direction, length, color) 105 | } 106 | 107 | return inters 108 | } 109 | 110 | clearLines() { 111 | Measure.clearLines() 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/extras/jnorthpole.js: -------------------------------------------------------------------------------- 1 | /* 2 | * decaffeinate suggestions: 3 | * DS102: Remove unnecessary code created because of implicit returns 4 | * DS207: Consider shorter variations of null checks 5 | * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md 6 | */ 7 | 8 | function buildParam(prefix, obj, add) { 9 | if (Array.isArray(obj)) { 10 | for (var i = 0, l = obj.length; i < l; ++i) { 11 | buildParam(prefix + '[]', obj[i], add); 12 | } 13 | } else if ( obj && typeof obj === "object" ) { 14 | for (var name in obj) { 15 | buildParam(prefix + '[' + name + ']', obj[name], add); 16 | } 17 | } else { 18 | add(prefix, obj); 19 | } 20 | } 21 | 22 | function obj2QueryString(object) { 23 | var pairs = [], 24 | add = function (key, value) { 25 | pairs.push(encodeURIComponent(key) + '=' + encodeURIComponent(value)); 26 | }; 27 | for (var name in object) { 28 | buildParam(name, object[name], add); 29 | } 30 | return pairs.join('&') 31 | .replace(/%20/g, '+'); 32 | }; 33 | 34 | window.jNorthPole = { 35 | 36 | BASE_URL: 'https://json.northpole.ro/', 37 | 38 | help: `\ 39 | NorthPole JS wrapper example usage: 40 | 41 | responseHandler = function (data) { 42 | console.log(data); 43 | }; 44 | 45 | jNorthPole.getStorage(json, responseHandler); 46 | 47 | socket = jNorthPole.getNewRealtimeSocket(responseHandler) 48 | jNorthPole.subscribe(socket, 'foo') 49 | jNorthPole.publish(socket, 'foo', { message: 'hello' })\ 50 | `, 51 | 52 | genericRequest(jsonObj, method, endPoint, responseHandler, errorHandler) { 53 | if (errorHandler == null) { errorHandler = responseHandler; } 54 | if (responseHandler == null) { throw 'responseHandler function missing'; } 55 | 56 | const r = new XMLHttpRequest; 57 | var url = `${this.BASE_URL}${endPoint}.json` 58 | if (method === 'GET') { 59 | url += "?" + obj2QueryString(jsonObj) 60 | } 61 | r.open(method, url, true); 62 | 63 | r.onreadystatechange = function() { 64 | if (r.readyState !== 4) { return; } 65 | if (r.status === 200) { 66 | responseHandler(JSON.parse(r.responseText), r.status); 67 | } else { 68 | errorHandler(JSON.parse(r.responseText), r.status); 69 | } 70 | }; 71 | r.send(JSON.stringify(jsonObj)); 72 | }, 73 | 74 | createUser(api_key, secret, success, failure) { 75 | const jsonObj = {'api_key': api_key, 'secret': secret}; 76 | this.genericRequest(jsonObj, 'POST', 'user', success, failure); 77 | }, 78 | 79 | getUser(jsonObj, responseHandler, errorHandler) { 80 | this.genericRequest(jsonObj, 'SEARCH', 'user', responseHandler, errorHandler); 81 | }, 82 | 83 | createStorage(jsonObj, responseHandler, errorHandler) { 84 | this.genericRequest(jsonObj, 'POST', 'storage', responseHandler, errorHandler); 85 | }, 86 | 87 | getStorage(jsonObj, responseHandler, errorHandler) { 88 | this.genericRequest(jsonObj, 'GET', 'storage', responseHandler, errorHandler); 89 | }, 90 | 91 | putStorage(jsonObj, responseHandler, errorHandler) { 92 | this.genericRequest(jsonObj, 'PUT', 'storage', responseHandler, errorHandler); 93 | }, 94 | 95 | deleteStorage(jsonObj, responseHandler, errorHandler) { 96 | this.genericRequest(jsonObj, 'DELETE', 'storage', responseHandler, errorHandler); 97 | }, 98 | 99 | getNewRealtimeSocket(responseHandler, errorHandler) { 100 | if (errorHandler == null) { errorHandler = responseHandler; } 101 | const socketUrl = this.BASE_URL.replace('http', 'ws'); 102 | const socket = new WebSocket(`${socketUrl}realtime`); 103 | socket.onmessage = responseHandler; 104 | socket.onclose = errorHandler; 105 | return socket; 106 | }, 107 | 108 | subscribe(socket, channel_name) { 109 | return socket.send(JSON.stringify({ 110 | type: 'subscribe', 111 | channel_name 112 | })); 113 | }, 114 | 115 | unsubscribe(socket, channel_name) { 116 | return socket.send(JSON.stringify({ 117 | type: 'unsubscribe', 118 | channel_name 119 | })); 120 | }, 121 | 122 | publish(socket, channel_name, json) { 123 | return socket.send(JSON.stringify({ 124 | type: 'publish', 125 | channel_name, 126 | content: json 127 | })); 128 | } 129 | }; 130 | -------------------------------------------------------------------------------- /src/extras/scenes/AddsScene.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Shows a slideshow on images on init, they can be skipped and switches to 3 | * the specified callbackScene when the images finish. 4 | * 5 | * The duration and other properties can be configured from 6 | * Config.instance.ui.addsScene 7 | * 8 | * Example usage: 9 | * 10 | * let gameScene = new GameScene() 11 | * let addsScene = new AddsScene(gameScene, ["vrum.png"]) 12 | * 13 | * Engine.start(addsScene, [ 14 | * { type: "image", path: "assets/vrum.png 15 | * ]) 16 | */ 17 | class AddsScene extends Scene { 18 | constructor(callbackScene, itemKeys, skippable) { 19 | let addsConfig = Config.instance.ui.addsScene 20 | 21 | if (isBlank(callbackScene)) { 22 | throw 'callbackScene missing' 23 | } 24 | if (!isArray(itemKeys)) { 25 | throw 'itemKeys needs to be an array of keys' 26 | } 27 | if (itemKeys.isEmpty()) { 28 | throw 'need at leasts one item key' 29 | } 30 | if (addsConfig.fadeDurationMS * 2 >= addsConfig.itemDisplayDurationSeconds * 1000) { 31 | throw 'fade duration * 2 can not be greater than the itemDisplayDurationSeconds. change values in Config.instance.ui.addsScene' 32 | } 33 | if (isBlank(skippable)) { 34 | skippable = addsConfig.skippable 35 | } 36 | super() 37 | this.callbackScene = callbackScene 38 | this.itemKeys = itemKeys 39 | this.skippable = skippable 40 | 41 | this.cameraDistanceZ = addsConfig.cameraDistanceZ 42 | this.itemDisplayDurationSeconds = addsConfig.itemDisplayDurationSeconds 43 | this.fadeDurationMS = addsConfig.fadeDurationMS 44 | this.lastGamepadEventTime = 0 45 | } 46 | 47 | init(options) { 48 | let camera = this.getCamera() 49 | camera.position.set(0, 0, this.cameraDistanceZ) 50 | camera.lookAt(new THREE.Vector3(0, 0, 0)) 51 | 52 | let queue = [] 53 | this.itemKeys.forEach((key) => { 54 | let item = this.buildItem(key) 55 | item.setOpacity(0) 56 | queue.push(item) 57 | }) 58 | 59 | this.lastChange = 0 60 | this.queue = queue.reverse() 61 | this.item = undefined 62 | 63 | this.next() 64 | } 65 | 66 | buildItem(key) { 67 | let item 68 | if (isString(key)) { 69 | if (!AssetManager.has(key)) { 70 | throw `key ${key} for AddsScene not loaded` 71 | } 72 | item = Utils.plane({ 73 | map: key, 74 | keepProportions: true, 75 | transparent: true, 76 | }) 77 | } 78 | // TODO: maybe support different formats: art generator, text etc 79 | if (isBlank(item)) { 80 | throw `can't build item ${item} in AddsScene` 81 | } 82 | return item 83 | } 84 | 85 | next() { 86 | let nextObj = this.queue.pop() 87 | if (isBlank(nextObj)) { 88 | if (this.finished) { return } 89 | this.finished = true 90 | if (Config.instance.engine.debug) { 91 | console.info("addsScene finished, switching to callbackScene") 92 | } 93 | Engine.switch(this.callbackScene) 94 | } else { 95 | if (Config.instance.engine.debug) { 96 | console.info("addsScene showing next image") 97 | } 98 | this.remove(this.item) 99 | this.item = nextObj 100 | this.add(nextObj) 101 | new FadeModifier(nextObj, 0, 1, this.fadeDurationMS).start() 102 | this.setTimeout(() => { 103 | new FadeModifier(nextObj, 1, 0, this.fadeDurationMS).start() 104 | }, this.itemDisplayDurationSeconds * 1000 - this.fadeDurationMS * 2) 105 | } 106 | } 107 | 108 | tick(tpf) { 109 | this.lastChange += tpf 110 | if (this.lastChange > this.itemDisplayDurationSeconds) { 111 | this.lastChange -= this.itemDisplayDurationSeconds 112 | this.next() 113 | } 114 | } 115 | 116 | doMouseEvent(event, raycaster) { 117 | if (!this.skippable) { return } 118 | if (event.type == 'mousedown') { 119 | this.next() 120 | } 121 | } 122 | 123 | doKeyboardEvent(event) { 124 | if (!this.skippable) { return } 125 | if (event.type == 'keydown') { 126 | this.next() 127 | } 128 | } 129 | 130 | doGamepadEvent(event) { 131 | if (!this.skippable) { return } 132 | if (event.type !== 'gamepadtick-vrum') { return } 133 | if (this.lastGamepadEventTime + 0.2 > this.uptime) { return } 134 | if (isBlank(event[0]) || isBlank(event[0].buttons)) { return } 135 | let buttonsPressed = event[0].buttons.filter((e) => e.pressed == true) 136 | if (buttonsPressed.any()) { 137 | this.lastGamepadEventTime = this.uptime 138 | this.next() 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/extras/scenes/LoadingScene.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Loads assets on init and switches the scene to the specified callbackScene 3 | * 4 | * Example usage: 5 | * 6 | * let gameScene = new GameScene() 7 | * let loadingScene = new LoadingScene(gameScene, [ 8 | * { type: "image", path: "assets/vrump.png }, 9 | * ] 10 | * 11 | * Engine.start(loadingScene) 12 | */ 13 | class LoadingScene extends Scene { 14 | constructor(callbackScene, assetsToLoad) { 15 | if (isBlank(callbackScene)) { 16 | throw 'callbackScene missing' 17 | } 18 | if (!isArray(assetsToLoad)) { 19 | throw 'assetsToLoad needs to be an array' 20 | } 21 | super() 22 | this.callbackScene = callbackScene 23 | this.assetsToLoad = assetsToLoad 24 | } 25 | 26 | init(options) { 27 | let camera = Hodler.get('camera') 28 | camera.position.set(0, 10, 15) 29 | camera.lookAt(new THREE.Vector3(0, 0, 0)) 30 | 31 | this.initCallback(options) 32 | 33 | if (Config.instance.engine.debug) { 34 | console.info(`loadingScene started loading ${this.assetsToLoad.length} assets`) 35 | } 36 | Engine.switch(this.callbackScene, this.assetsToLoad) 37 | } 38 | 39 | initCallback(options) { 40 | let cube = Utils.box({ size: 1 }) 41 | this.add(cube) 42 | this.cube = cube 43 | } 44 | 45 | tick(tpf) { 46 | if (!isBlank(this.cube)) { 47 | this.cube.rotation.x += tpf 48 | this.cube.rotation.y += tpf 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/extras/scenes/VideoScene.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Plays a video on init, can be skipped on user input and switches the scene 3 | * to the specified callbackScene when the video finishes. 4 | * 5 | * Example usage: 6 | * 7 | * let gameScene = new GameScene() 8 | * let videoScene = new VideoScene(gameScene, "assets/agent.mp4") 9 | * 10 | * Engine.switch(videoScene) 11 | */ 12 | class VideoScene extends Scene { 13 | constructor(callbackScene, videoUrl, skippable) { 14 | let uiConfig = Config.instance.ui 15 | if (isBlank(callbackScene)) { 16 | throw 'callbackScene missing' 17 | } 18 | if (!isString(videoUrl)) { 19 | throw 'videoUrl needs to be a string' 20 | } 21 | if (videoUrl === '') { 22 | throw 'videoUrl can not be empty' 23 | } 24 | if (!uiConfig.video.supportedFormats.includes(videoUrl.split('.').last())) { 25 | throw 'unsupported video format for ${videoUrl}' 26 | } 27 | if (isBlank(skippable)) { 28 | skippable = uiConfig.videoScene.skippable 29 | } 30 | super() 31 | this.callbackScene = callbackScene 32 | this.videoUrl = videoUrl 33 | this.skippable = skippable 34 | } 35 | 36 | safeRemoveVideo() { 37 | if (this.finished) { return } 38 | this.finished = true 39 | if (Config.instance.engine.debug) { 40 | console.info('videoScene.safeRemoveVideo() called') 41 | } 42 | // video could already be removed at this point, but that is ok 43 | // delay is automatically configured to the same time it takes 44 | // to fade the scene 45 | Utils.removeVideo() 46 | Engine.switch(this.callbackScene) 47 | } 48 | 49 | init(options) { 50 | Utils.playVideo(this.videoUrl, () => { 51 | Hodler.get('scene').safeRemoveVideo() 52 | }) 53 | } 54 | 55 | doMouseEvent(event, raycaster) { 56 | if (!this.skippable) { 57 | return 58 | } 59 | if (event.type == 'mousedown') { 60 | this.safeRemoveVideo() 61 | } 62 | } 63 | 64 | doKeyboardEvent(event) { 65 | if (!this.skippable) { 66 | return 67 | } 68 | if (event.type == 'keydown') { 69 | this.safeRemoveVideo() 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/objects/BaseParticle.js: -------------------------------------------------------------------------------- 1 | // http://squarefeet.github.io/ShaderParticleEngine/docs/api/global.html#GroupOptions 2 | class BaseParticle extends THREE.Object3D { 3 | constructor(input) { 4 | super() 5 | 6 | input = arrayOrStringToString(input) 7 | 8 | let jsonInput; 9 | this.groups = []; 10 | this.emitters = []; 11 | 12 | eval(`jsonInput = ${input}`); 13 | if (jsonInput == null) { jsonInput = []; } 14 | this.jsonInput = jsonInput 15 | 16 | for (let json of Array.from(jsonInput)) { 17 | const group = new (SPE.Group)(json); 18 | for (let emitJson of Array.from(json.emitters)) { 19 | const emitter = new (SPE.Emitter)(emitJson); 20 | group.addEmitter(emitter); 21 | this.emitters.push(emitter) 22 | } 23 | this.groups.push(group); 24 | this.add(group.mesh); 25 | } 26 | } 27 | 28 | getMaxAge() { 29 | let ages = [] 30 | this.jsonInput.forEach((json) => { 31 | json.emitters.forEach((e) => { 32 | let age = isBlank(e.maxAge) || isBlank(e.maxAge.value) ? 2 : e.maxAge.value 33 | ages.push(age) 34 | }) 35 | }) 36 | let max = Math.max(...ages) 37 | return max 38 | } 39 | 40 | setEmitterInnerPosition(pos) { 41 | this.emitters.forEach((e) => { 42 | e.position.value = pos 43 | }) 44 | } 45 | 46 | setEmitterInnerRotation(axis, angle) { 47 | this.emitters.forEach((e) => { 48 | e.rotation.axis = axis 49 | e.rotation.angle = angle 50 | }) 51 | } 52 | 53 | setActiveMultiplier(value) { 54 | if (isBlank(value)) { value = 1 } 55 | if (value === this.lastActiveMultiplier) { return } 56 | this.lastActiveMultiplier = value 57 | this.emitters.forEach((e) => { 58 | e.activeMultiplier = value 59 | }) 60 | } 61 | 62 | enable() { 63 | this.emitters.forEach((e) => { 64 | e.enable() 65 | }) 66 | } 67 | 68 | disable() { 69 | this.emitters.forEach((e) => { 70 | e.disable() 71 | }) 72 | } 73 | 74 | // Used to animate the particle 75 | // 76 | // Should normally be called in scene.tick 77 | // 78 | // we don't need to render according to tpf because 79 | // that desyncs the animation 80 | tick(tpf) { 81 | Array.from(this.groups).map((group) => { 82 | group.tick(tpf); 83 | }) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/objects/BaseText.js: -------------------------------------------------------------------------------- 1 | // let text = new BaseText({ 2 | // text: 'Press to start', fillStyle: 'blue', 3 | // strokeStyle: 'black', strokeLineWidth: 1, 4 | // canvasW: 1024, canvasH: 1024, 5 | // font: '64px luckiest-guy'}) 6 | // text.position.set(0, 0, 4) 7 | class BaseText extends THREE.Mesh { 8 | constructor(options) { 9 | if (isBlank(options)) { options = {}; } 10 | var canvasW = options.canvasW || 512; 11 | var canvasH = options.canvasH || 512; 12 | 13 | var w = options.w || 4; 14 | var h = options.h || 4; 15 | 16 | if (!([THREE.MeshBasicMaterial, THREE.MeshLambertMaterial].includes(options.material))) { 17 | options.material = THREE.MeshBasicMaterial 18 | } 19 | 20 | var margin = options.margin; 21 | var lineHeight = options.lineHeight; 22 | var align = options.align; 23 | var font = options.font || '16px Helvetica'; 24 | var fillStyle = options.fillStyle; 25 | var fillLineWidth = options.fillLineWidth; 26 | var strokeStyle = options.strokeStyle; 27 | var strokeLineWidth = options.strokeLineWidth; 28 | var text = options.text; 29 | var x = options.x; 30 | var y = options.y; 31 | 32 | var dynamicTexture = new THREEx.DynamicTexture(canvasW, canvasH) 33 | 34 | const geom = new THREE.PlaneGeometry(w, h); 35 | const material = new options.material({ 36 | map: dynamicTexture.texture, 37 | transparent: true 38 | }); 39 | 40 | super(geom, material) 41 | 42 | this.dynamicTexture = dynamicTexture 43 | this.margin = margin 44 | this.lineHeight = lineHeight 45 | this.align = align 46 | this.fillStyle = fillStyle 47 | this.fillLineWidth = fillLineWidth 48 | this.strokeStyle = strokeStyle 49 | this.strokeLineWidth = strokeLineWidth 50 | this.x = x 51 | this.y = y 52 | this.font = font 53 | this.setText(text) 54 | } 55 | 56 | setSafeText(text) { 57 | if (text === this.text) { return } 58 | this.setText(text) 59 | } 60 | 61 | setText(text) { 62 | if ((text === '') || (isBlank(text))) { text = ' ' } 63 | 64 | this.text = text.toString() 65 | this.clear() 66 | return this.dynamicTexture.drawTextCooked({ 67 | text: this.text, 68 | margin: this.margin, 69 | lineHeight: this.lineHeight, 70 | align: this.align, 71 | fillStyle: this.fillStyle, 72 | fillLineWidth: this.fillLineWidth, 73 | strokeStyle: this.strokeStyle, 74 | strokeLineWidth: this.strokeLineWidth, 75 | x: this.x, 76 | y: this.y, 77 | font: this.font 78 | }); 79 | } 80 | 81 | getText() { 82 | return this.text 83 | } 84 | 85 | appendText(text) { 86 | this.setText(`${this.text}${text}`) 87 | } 88 | 89 | clear() { 90 | this.dynamicTexture.clear(); 91 | } 92 | 93 | getTextWidth(s) { 94 | return this.dynamicTexture.context.measureText(s).width; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/objects/Button3D.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Example usage: 3 | * 4 | * let button = new Button3D('tutorial') 5 | * button.onClick = () => { 6 | * button.isEnabled = false 7 | * Engine.switch(tutorialScene) 8 | * } 9 | * this.add(button) 10 | * this.buttons.push(button) 11 | * 12 | * button.isHovered = false 13 | * button.isPressed = false 14 | * button.click() 15 | * 16 | * button.doMouseEvent(event, raycaster) 17 | */ 18 | class Button3D extends THREE.Object3D { 19 | constructor(s, bgKey, fgKey) { 20 | super() 21 | 22 | let bg = AssetManager.clone(bgKey) 23 | this.add(bg) 24 | this.bg = bg 25 | 26 | let fg = AssetManager.clone(fgKey) 27 | this.add(fg) 28 | this.fg = fg 29 | 30 | let text = this.initText(s) 31 | fg.add(text) 32 | this.text = text 33 | 34 | this.isHovered = false 35 | this.isPressed = false 36 | this.isEnabled = true 37 | this.pressSpeed = 4 38 | this.growSpeed = 2 39 | this.isMobileOrTablet = Utils.isMobileOrTablet() 40 | } 41 | 42 | initText(s) { 43 | let text = new BaseText({ 44 | text: s, fillStyle: 'white', 45 | strokeStyle: 'black', strokeLineWidth: 1, 46 | canvasW: 512, canvasH: 512, align: 'center', 47 | font: '72px luckiest-guy'}) 48 | text.position.set(0, -1.4, 0.7) 49 | return text 50 | } 51 | 52 | setFgColor(color) { 53 | if (!(color instanceof THREE.Color)) { 54 | color = new THREE.Color(color) 55 | } 56 | this.fg.children[0].material.color = color 57 | } 58 | 59 | setBgColor(color) { 60 | if (!(color instanceof THREE.Color)) { 61 | color = new THREE.Color(color) 62 | } 63 | this.bg.children[0].material.color = color 64 | } 65 | 66 | setColor(color, percent) { 67 | if (isBlank(percent)) { percent = 0.25 } 68 | this.setFgColor(color) 69 | // let shaded = Utils.lightenHex(color, percent) 70 | let shaded = Utils.darkenHex(color, percent) 71 | console.log(shaded) 72 | this.setBgColor(shaded) 73 | } 74 | 75 | setText(s) { 76 | this.text.setText(s) 77 | } 78 | 79 | tick(tpf) { 80 | let scale = this.scale.x 81 | if (this.isHovered) { 82 | let maxScale = 1.2 83 | scale += tpf * this.growSpeed 84 | if (scale > maxScale) { scale = maxScale } 85 | } else { 86 | let minScale = 1 87 | scale -= tpf * this.growSpeed 88 | if (scale < minScale) { scale = minScale } 89 | } 90 | this.scale.setScalar(scale) 91 | 92 | let z = this.fg.position.z 93 | if (this.isPressed) { 94 | let minZ = -0.3 95 | z -= tpf * this.pressSpeed 96 | if (z < minZ) { z = minZ } 97 | } else { 98 | let maxZ = 0 99 | z += tpf * this.pressSpeed 100 | if (z > maxZ) { z = maxZ } 101 | } 102 | this.fg.position.z = z 103 | } 104 | 105 | doMouseEvent(event, raycaster) { 106 | if (this.isMobileOrTablet) { 107 | this.doMouseMobileEvent(event, raycaster) 108 | } else { 109 | this.doMousePCEvent(event, raycaster) 110 | } 111 | } 112 | 113 | doMouseMobileEvent(event, raycaster) { 114 | if (event.type == 'mousedown' || event.type == 'mouseup') { 115 | let intersections = raycaster.intersectObject(this, true) 116 | intersections = intersections.filter((e) => !(e.object instanceof BaseText)) 117 | 118 | if (intersections.any() && event.type == 'mousedown') { 119 | this.isPressed = true 120 | } 121 | if (event.type == 'mouseup') { 122 | if (intersections.any() && this.isPressed) { 123 | this.click() 124 | } 125 | this.isPressed = false 126 | } 127 | } 128 | } 129 | 130 | doMousePCEvent(event, raycaster) { 131 | if (event.type == 'mousemove') { 132 | let intersections = raycaster.intersectObject(this, true) 133 | intersections = intersections.filter((e) => !(e.object instanceof BaseText)) 134 | this.isHovered = intersections.any() 135 | } 136 | 137 | if (event.type == 'mousedown') { 138 | if (this.isHovered) { 139 | this.isPressed = true 140 | } 141 | } 142 | if (event.type == 'mouseup') { 143 | this.isPressed = false 144 | if (this.isHovered) { 145 | this.click() 146 | } 147 | } 148 | } 149 | 150 | // Don't override this method, instead override onClick() 151 | click() { 152 | if (this.isEnabled) { 153 | this.onClick() 154 | } 155 | } 156 | 157 | onClick() { 158 | console.log('click') 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/objects/Mirror.js: -------------------------------------------------------------------------------- 1 | class Mirror extends THREE.Reflector { 2 | constructor (options) { 3 | let renderer = Hodler.get('renderer'); 4 | let camera = Hodler.get('camera'); 5 | let size = new THREE.Vector2() 6 | renderer.getSize(size) 7 | 8 | if (options == null) { options = {}; } 9 | if (options.width == null) { options.width = Utils.PLANE_DEFAULT_WIDTH; } 10 | if (options.height == null) { options.height = Utils.PLANE_DEFAULT_HEIGHT; } 11 | if (options.mirror == null) { options.mirror = {}; } 12 | if (options.mirror.clipBias == null) { options.mirror.clipBias = Utils.MIRROR_DEFAULT_CLIP_BIAS; } 13 | if (options.mirror.textureWidth == null) { options.mirror.textureWidth = size.x * camera.aspect } 14 | if (options.mirror.textureHeight == null) { options.mirror.textureHeight = size.y * camera.aspect } 15 | if (options.mirror.color == null) { options.mirror.color = Utils.MIRROR_DEFAULT_COLOR; } 16 | if (options.mirror.recursion == null) { options.mirror.color = Utils.MIRROR_DEFAULT_RECURSION; } 17 | 18 | var geometry = new THREE.PlaneBufferGeometry(options.width, options.height); 19 | super(geometry, options.mirror) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/objects/Sky.js: -------------------------------------------------------------------------------- 1 | // this.sky = new Sky(); 2 | // this.sky.addToScene(scene) 3 | // this.sky.updateSun(this.sky.distance, this.sky.inclination, this.sky.azimuth) 4 | class Sky extends THREE.Sky { 5 | constructor() { 6 | super() 7 | this.defaults() 8 | } 9 | 10 | defaults() { 11 | this.distance = 400 12 | this.inclination = 0.40 13 | this.azimuth = 0.20 14 | 15 | this.scale.setScalar(10000) 16 | this.material.uniforms.turbidity.value = 10 17 | this.material.uniforms.rayleigh.value = 2 18 | this.material.uniforms.luminance.value = 1 19 | this.material.uniforms.mieCoefficient.value = 0.005 20 | this.material.uniforms.mieDirectionalG.value = 0.8 21 | 22 | this.light = new THREE.DirectionalLight(0xffffff, 0.8) 23 | this.setLightShadowMapSize(512, 512) 24 | this.cameraHelper = new THREE.CameraHelper(this.light.shadow.camera) 25 | this.cameraHelper.visible = false 26 | 27 | this.updateSun(this.distance, this.inclination, this.azimuth) 28 | } 29 | 30 | updateSun(distance, inclination, azimuth) { 31 | if (inclination < 0) { inclination = 0 } 32 | if (inclination > 0.5) { inclination = 0.5 } 33 | if (azimuth < 0) { azimuth = 0 } 34 | if (azimuth > 1) { azimuth = 1 } 35 | 36 | this.distance = distance 37 | this.inclination = inclination 38 | this.azimuth = azimuth 39 | 40 | var theta = Math.PI * ( inclination - 0.5 ) 41 | var phi = 2 * Math.PI * ( azimuth - 0.5 ) 42 | 43 | var x = distance * Math.cos( phi ) 44 | var y = distance * Math.sin( phi ) * Math.sin( theta ) 45 | var z = distance * Math.sin( phi ) * Math.cos( theta ) 46 | 47 | this.light.position.set(x, y, z) 48 | 49 | var position = new THREE.Vector3(x, y, z) 50 | this.material.uniforms.sunPosition.value = this.light.position.copy(this.light.position) 51 | } 52 | 53 | setAzimuth(step = 0.20) { 54 | this.updateSun(this.distance, this.inclination, step) 55 | return this.azimuth 56 | } 57 | setInclination(step = 0.40) { 58 | this.updateSun(this.distance, step, this.azimuth) 59 | return this.inclination 60 | } 61 | setDistance(step = 400) { 62 | this.updateSun(step, this.inclination, this.azimuth) 63 | return this.distance 64 | } 65 | 66 | incAzimuth(step = 0.1) { 67 | this.updateSun(this.distance, this.inclination, this.azimuth + step) 68 | return this.azimuth 69 | } 70 | 71 | incInclination(step = 0.1) { 72 | this.updateSun(this.distance, this.inclination + step, this.azimuth) 73 | return this.inclination 74 | } 75 | 76 | incDistance(step = 1) { 77 | this.updateSun(this.distance + step, this.inclination, this.azimuth) 78 | return this.distance 79 | } 80 | 81 | toString() { 82 | console.log(`${this.distance} - ${this.inclination} - ${this.azimuth}`) 83 | } 84 | 85 | 86 | setLightShadowMapSize(width, height) { 87 | this.light.shadow.mapSize.width = width 88 | this.light.shadow.mapSize.height = height 89 | } 90 | 91 | addToScene(scene) { 92 | scene.add(this) 93 | scene.add(this.light) 94 | scene.add(this.cameraHelper) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/objects/SkyBox.js: -------------------------------------------------------------------------------- 1 | // this.skyBox = new SkyBox( 2 | // [ 3 | // '/workspace/assets/px.jpg', 4 | // '/workspace/assets/nx.jpg', 5 | // '/workspace/assets/py.jpg', 6 | // '/workspace/assets/ny.jpg', 7 | // '/workspace/assets/pz.jpg', 8 | // '/workspace/assets/nz.jpg', 9 | // ] 10 | // ) 11 | // this.add(this.skyBox) 12 | class SkyBox extends THREE.Mesh { 13 | constructor(imgUrls, size) { 14 | if (size == null) { size = 900000; } 15 | const aCubeMap = THREE.ImageUtils.loadTextureCube(imgUrls); 16 | aCubeMap.format = THREE.RGBFormat; 17 | const aShader = THREE.ShaderLib['cube']; 18 | aShader.uniforms['tCube'].value = aCubeMap; 19 | const aSkyBoxMaterial = new (THREE.ShaderMaterial)({ 20 | fragmentShader: aShader.fragmentShader, 21 | vertexShader: aShader.vertexShader, 22 | uniforms: aShader.uniforms, 23 | depthWrite: false, 24 | side: THREE.BackSide}); 25 | super(new (THREE.BoxGeometry)(size, size, size), aSkyBoxMaterial) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/objects/SpotLight.js: -------------------------------------------------------------------------------- 1 | /* 2 | ! * decaffeinate suggestions: 3 | * DS001: Remove Babel/TypeScript constructor workaround 4 | * DS102: Remove unnecessary code created because of implicit returns 5 | * DS207: Consider shorter variations of null checks 6 | * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md 7 | */ 8 | // Uses THREEx.VolumetricSpotLightMaterial to create a spotlight effect 9 | // 10 | // @example 11 | // spotLight = new SpotLight(0, 10, 0) 12 | // spotLight.addToScene(scene) 13 | // spotLight.lookAt(new (THREE.Vector3)(0, 0, 0)) 14 | // 15 | // @see https://github.com/jeromeetienne/threex.volumetricspotlight 16 | class SpotLight extends THREE.Mesh { 17 | // Creates a new spotlight 18 | // 19 | // @param [Number] x - start z position 20 | // @param [Number] y - start y position 21 | // @param [Number] z - start x position 22 | constructor(x, y, z, r1, r2, height) { 23 | if (r1 == null) { r1 = 0.1; } 24 | if (r2 == null) { r2 = 2.5; } 25 | if (height == null) { height = 5; } 26 | const geometry = new (THREE.CylinderGeometry)(r1, r2, height, 32 * 2, 40, true); 27 | geometry.applyMatrix((new (THREE.Matrix4)).makeTranslation(0, -geometry.parameters.height / 2, 0)); 28 | geometry.applyMatrix((new (THREE.Matrix4)).makeRotationX(-Math.PI / 2)); 29 | const material = new (THREEx.VolumetricSpotLightMaterial); 30 | 31 | super(geometry, material) 32 | this.position.set(x, y, z); 33 | this.setColor('white'); 34 | this.material.uniforms.spotPosition.value = this.position; // TODO: cleanup 35 | 36 | this.spotLight = new THREE.SpotLight(); 37 | this.spotLight.position.copy(this.position); 38 | this.spotLight.color = this.material.uniforms.lightColor.value; 39 | 40 | this.direction = new (THREE.Vector3)(0,0,0); 41 | this.lastDir = 0; 42 | 43 | this.lookAt(new (THREE.Vector3)(0, 0, 0)); 44 | 45 | this.cameraHelper = new THREE.CameraHelper(this.spotLight.shadow.camera) 46 | this.cameraHelper.visible = false 47 | } 48 | 49 | // Make the spotlight look at a node's position 50 | // 51 | // @param [Object] node 52 | lookAt(target) { 53 | super.lookAt(target) 54 | this.spotLight.target.position.copy(target); 55 | } 56 | 57 | // A helper which aims to make it easy to add spotlights and related 58 | // objects to the scene 59 | addToScene(scene) { 60 | scene.add(this); 61 | scene.add(this.spotLight); 62 | scene.add(this.cameraHelper); 63 | scene.add(this.spotLight.target); 64 | } 65 | 66 | // Sets the spotlight color 67 | // 68 | // @param [String] color 69 | // 70 | // @example 71 | // 72 | // spotLight.setColor('white') 73 | // spotLight.setColor('#ffffff') 74 | setColor(color) { 75 | return this.material.uniforms.lightColor.value.set(color); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/objects/Starfield.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Example usage: 3 | * 4 | * let starfield = new Starfield() 5 | * this.add(starfield) 6 | */ 7 | class Starfield extends THREE.Points { 8 | constructor() { 9 | //This will add a starfield to the background of a scene 10 | var starsGeometry = new THREE.Geometry(); 11 | 12 | for ( var i = 0; i < 10000; i ++ ) { 13 | 14 | var star = new THREE.Vector3(); 15 | star.x = THREE.Math.randFloatSpread( 2000 ); 16 | star.y = THREE.Math.randFloatSpread( 2000 ); 17 | star.z = THREE.Math.randFloatSpread( 2000 ); 18 | 19 | starsGeometry.vertices.push( star ); 20 | } 21 | 22 | var starsMaterial = new THREE.PointsMaterial( { color: 0xFAFAD2 } ); 23 | 24 | super(starsGeometry, starsMaterial) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/objects/Terrain.js: -------------------------------------------------------------------------------- 1 | /* 2 | * decaffeinate suggestions: 3 | * DS001: Remove Babel/TypeScript constructor workaround 4 | * DS101: Remove unnecessary use of Array.from 5 | * DS102: Remove unnecessary code created because of implicit returns 6 | * DS205: Consider reworking code to avoid use of IIFEs 7 | * DS207: Consider shorter variations of null checks 8 | * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md 9 | */ 10 | // Creates and adds a heightmap to the current scene 11 | // 12 | // @example 13 | // Terrain.heightmap('/node_modules/ocean/assets/img/waternormals.jpg', 'heightmap.png', 20, 20, 5, 5) 14 | // 15 | // @example 16 | // hm = THREE.ImageUtils.loadTexture(options.heightmapUrl) 17 | // hm.heightData = Terrain.getHeightData(hm.image, options.scale) 18 | // terrain = new Terrain(options.textureUrl, options.width, options.height, options.wSegments, options.hSegments) 19 | // terrain.applyHeightmap(hm.heightData) 20 | // 21 | // @example 22 | // # assuming you are using a LoadingScene 23 | // json = SaveObjectManager.get().items['terrain'] 24 | // @scene.add Terrain.fromJson(json).mesh 25 | // 26 | class Terrain extends THREE.Mesh { 27 | 28 | // Creates the terrain 29 | constructor(json){ 30 | let mat = new THREE.MeshLambertMaterial({ 31 | map: AssetManager.get(json.texture.destPath), 32 | side: THREE.DoubleSide 33 | }); 34 | let geom = new (THREE.PlaneGeometry)(json.width, json.height, json.wSegments, json.hSegments); 35 | 36 | super(geom, mat) 37 | this.rotation.x -= Math.PI / 2; 38 | 39 | this.raycaster = new (THREE.Raycaster); 40 | } 41 | 42 | // Get height at a specific position. 43 | // 44 | // A ray is cast from the top of the position returning the height at the 45 | // intersection point 46 | // 47 | // @example 48 | // height = @terrain.getHeightAt(@cube.position) 49 | // @cube.position.y = height 50 | getHeightAt(position) { 51 | this.raycaster.set(new THREE.Vector3(position.x, 1000, position.z), Helper.down); 52 | const intersects = this.raycaster.intersectObject(this); 53 | if (intersects[0] != null) { return intersects[0].point.y; } else { return 0; } 54 | } 55 | 56 | // Apply heightmap data retrieved from getHeightData 57 | // 58 | // @see getHeightData 59 | applyHeightmap(imageData) { 60 | let i = 0; 61 | return (() => { 62 | const result = []; 63 | for (let vertice of Array.from(this.geometry.vertices)) { 64 | vertice.z = imageData[i]; 65 | result.push(i++); 66 | } 67 | return result; 68 | })(); 69 | } 70 | 71 | // Returns height data of an Image object 72 | // 73 | // @param [Image] img 74 | // @param [Number] scale 75 | static getHeightData(img, scale) { 76 | if (scale == null) { scale = 1; } 77 | const canvas = document.createElement('canvas'); 78 | canvas.width = img.width; 79 | canvas.height = img.height; 80 | const context = canvas.getContext('2d'); 81 | const size = img.width * img.height; 82 | const data = new Float32Array(size); 83 | context.drawImage(img, 0, 0); 84 | let i = 0; 85 | while (i < size) { 86 | data[i] = 0; 87 | i++; 88 | } 89 | const imgd = context.getImageData(0, 0, img.width, img.height); 90 | const pix = imgd.data; 91 | let j = 0; 92 | i = 0; 93 | while (i < pix.length) { 94 | const all = pix[i] + pix[i + 1] + pix[i + 2]; 95 | data[j++] = all / (12 * scale); 96 | i += 4; 97 | } 98 | return data; 99 | } 100 | 101 | // Propper way to load a terrain using TextureManager 102 | // 103 | // @param [Object] json 104 | static fromJson(json) { 105 | // TODO: validate is terrain 106 | 107 | for (var key of ['width', 'height', 'scale', 'texture', 'heightmap']) { 108 | if (json[key] == null) { throw new Error(`${key} missing for terrain`); } 109 | } 110 | for (key of ['texture', 'heightmap']) { 111 | if (json[key].destPath == null) { throw new Error(`${key}.destPath missing for terrain`); } 112 | } 113 | 114 | const hm = AssetManager.get(json.heightmap.destPath) 115 | hm.heightData = Terrain.getHeightData(hm.image, json.scale); 116 | if (json.wSegments == null) { json.wSegments = hm.image.width - 1; } 117 | if (json.hSegments == null) { json.hSegments = hm.image.height - 1; } 118 | const terrain = new Terrain(json); 119 | terrain.applyHeightmap(hm.heightData); 120 | return terrain; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/objects/Tree.js: -------------------------------------------------------------------------------- 1 | /* 2 | * decaffeinate suggestions: 3 | * DS101: Remove unnecessary use of Array.from 4 | * DS102: Remove unnecessary code created because of implicit returns 5 | * DS207: Consider shorter variations of null checks 6 | * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md 7 | */ 8 | // https://github.com/lmparppei/deadtree/blob/master/deadtree.js 9 | // 10 | // @example 11 | // wind = new Tree() 12 | // @wind = 0 13 | // 14 | // @wind += tpf + Math.random() # shaky 15 | // @wind = tpf + Math.random() # bend 16 | // 17 | // @tree.wind(@wind) 18 | // 19 | class Tree extends THREE.Object3D { 20 | constructor(material, size, children){ 21 | super() 22 | 23 | if (size == null) { size = 1; } 24 | if (children == null) { children = 5; } 25 | 26 | if (material == null) { throw 'missing material' } 27 | 28 | const sizeModifier = .65; 29 | this.branchPivots = []; 30 | 31 | this.add(this.createBranch(size, material, children, false, sizeModifier)) 32 | 33 | this.wind = 0 34 | } 35 | 36 | createBranch(size, material, children, isChild, sizeModifier) { 37 | const branchPivot = new (THREE.Object3D); 38 | const branchEnd = new (THREE.Object3D); 39 | this.branchPivots.push(branchPivot); 40 | const length = (Math.random() * size * 10) + (size * 5); 41 | const endSize = children === 0 ? 0 : size * sizeModifier; 42 | const branch = new (THREE.Mesh)(new (THREE.CylinderGeometry)(endSize, size, length, 5, 1, true), material); 43 | branchPivot.add(branch); 44 | branch.add(branchEnd); 45 | branch.position.y = length / 2; 46 | branchEnd.position.y = (length / 2) - (size * .4); 47 | if (isChild) { 48 | branchPivot.rotation.z += (Math.random() * 1.5) - (sizeModifier * 1.05); 49 | branchPivot.rotation.x += (Math.random() * 1.5) - (sizeModifier * 1.05); 50 | } else { 51 | branchPivot.rotation.z += (Math.random() * .1) - .05; 52 | branchPivot.rotation.x += (Math.random() * .1) - .05; 53 | } 54 | if (children > 0) { 55 | let c = 0; 56 | while (c < children) { 57 | const child = this.createBranch(size * sizeModifier, material, children - 1, true, sizeModifier); 58 | branchEnd.add(child); 59 | c++; 60 | } 61 | } 62 | return branchPivot; 63 | } 64 | 65 | // @wind += tpf + Math.random() # shaky 66 | // @wind = tpf + Math.random() # bend 67 | tick(wind) { 68 | this.wind = wind 69 | return Array.from(this.branchPivots).map((b) => 70 | (b.rotation.z += Math.cos(wind * Math.random()) * 0.0005)); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/objects/Water.js: -------------------------------------------------------------------------------- 1 | // this.water = new Water({ 2 | // width: 100, 3 | // height: 100, 4 | // map: 'waternormals.jpg', 5 | // water: { 6 | // alpha: 0.8, 7 | // waterColor: 0x001e0f 8 | // } 9 | // }) 10 | // this.add(this.water) 11 | // 12 | // this.water.setSunDirection(this.sky.light) 13 | class Water extends THREE.Mesh { 14 | constructor(options) { 15 | if (options == null) { options = {}; } 16 | if (options.map == null) { throw new Error('map missing. needs to be a AssetManager key'); } 17 | if (options.width == null) { options.width = Utils.PLANE_DEFAULT_WIDTH; } 18 | if (options.height == null) { options.height = Utils.PLANE_DEFAULT_HEIGHT; } 19 | if (options.wSegments == null) { options.wSegments = Utils.PLANE_DEFAULT_W_SEGMENTS; } 20 | if (options.hSegments == null) { options.hSegments = Utils.PLANE_DEFAULT_H_SEGMENTS; } 21 | if (options.water == null) { options.water = {}; } 22 | if (options.water.textureWidth == null) { options.water.textureWidth = Utils.MIRROR_DEFAULT_TEXTURE_WIDTH; } 23 | if (options.water.textureHeight == null) { options.water.textureHeight = Utils.MIRROR_DEFAULT_TEXTURE_HEIGHT; } 24 | if (options.water.alpha == null) { options.water.alpha = Utils.WATER_DEFAULT_ALPHA; } 25 | if (options.water.sunColor == null) { options.water.sunColor = Utils.LIGHT_DEFAULT_COLOR; } 26 | if (options.water.waterColor == null) { options.water.waterColor = Utils.WATER_DEFAULT_WATER_COLOR; } 27 | if (options.water.betaVersion == null) { options.water.betaVersion = 0; } 28 | if (options.water.side == null) { options.water.side = THREE.DoubleSide; } 29 | 30 | const waterNormals = AssetManager.get(options.map) 31 | waterNormals.wrapS = (waterNormals.wrapT = THREE.RepeatWrapping) 32 | options.water.waterNormals = waterNormals; 33 | 34 | let renderer = Hodler.get('renderer') 35 | let camera = Hodler.get('camera') 36 | let scene = Hodler.get('scene') 37 | let water = new (THREE.Water)(renderer, camera, scene, options.water); 38 | 39 | super(new (THREE.PlaneBufferGeometry)(options.width, options.height, options.wSegments, options.hSegments), water.material); 40 | this.add(water) 41 | this.rotation.x = -Math.PI * 0.5 42 | this.speed = 1 43 | this.water = water 44 | } 45 | 46 | // Used to update the water animation. 47 | // 48 | // Should be called in scene.tick 49 | tick(tpf) { 50 | this.water.material.uniforms.time.value += tpf * this.speed; 51 | this.water.render(); 52 | } 53 | 54 | setSunDirection(light) { 55 | this.water.material.uniforms.sunDirection.value.copy(light.position).normalize() 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/tools/build.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Handles building of vrum.js and vrum.min.js 4 | // 5 | // Each script tag in 'src/tools/dependencies.dev.js' gets read and appended 6 | // to a file. That file is then minified 7 | 8 | const colors = require('colors'); 9 | const concat = require('concat-files'); 10 | const fs = require('fs') 11 | const UglifyJS = require('uglify-es'); 12 | const path = require('path') 13 | const common = require('./common') 14 | 15 | function getFilesizeInBytes(filename) { 16 | const stats = fs.statSync(filename) 17 | const fileSizeInBytes = stats.size 18 | return (fileSizeInBytes / 1000000).toFixed(1) 19 | } 20 | 21 | function printFileSize(filename) { 22 | var output = "success ".green + filename + " (" + getFilesizeInBytes(filename) + " MB)" 23 | console.log(output) 24 | } 25 | 26 | console.log("Building vrum.js and vrum.min.js") 27 | 28 | var dependencies = fs.readFileSync('src/tools/dependencies.dev.js', 'utf-8') 29 | .split('\n') 30 | .filter(Boolean) 31 | .filter((e) => e.startsWith(' "')) 32 | .map((e) => e.substr(3).split(',')[0].slice(0, -1)) 33 | .map((e) => { 34 | if (e[0] == '/') { 35 | return e.substr(1); 36 | } else { 37 | return e.substr(9) 38 | } 39 | }) 40 | 41 | // inject dependencies.dist.js at the top of the dependency list. This will 42 | // make sure the function loadVrumScripts works as expected 43 | dependencies.splice(0, 0, 'src/tools/dependencies.dist.js') 44 | 45 | // make sure live.js is not included in the build process.because 46 | // we don't want js scripts auto reloading in production 47 | dependencies.forEach((e) => { 48 | if (e.includes('/live.js')) { 49 | throw 'There might be a bug in the build process, live.js should not be included' 50 | } 51 | }) 52 | 53 | if (!fs.existsSync(common.distFolder)){ 54 | fs.mkdirSync(common.distFolder); 55 | } 56 | var outputPath = path.join(common.distFolder, 'vrum.js') 57 | var outputPathMin = path.join(common.distFolder, 'vrum.min.js') 58 | 59 | concat(dependencies, outputPath, (err) => { 60 | if (err) throw err 61 | 62 | var single = fs.readFileSync(outputPath, 'utf-8') 63 | 64 | printFileSize(outputPath) 65 | 66 | var file = UglifyJS.minify(single, {}) 67 | fs.writeFileSync(outputPathMin, file.code) 68 | 69 | printFileSize(outputPathMin) 70 | process.exit(0) 71 | }) 72 | -------------------------------------------------------------------------------- /src/tools/common.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const readline = require('readline'); 4 | const fs = require('fs') 5 | 6 | const rl = () => { 7 | return readline.createInterface({ 8 | input: process.stdin, 9 | output: process.stdout 10 | }); 11 | } 12 | 13 | const injectHTML = (htmlPath, injectString) => { 14 | // TODO check for index.html 15 | let lines = fs.readFileSync(htmlPath, 'utf-8').split('\n') 16 | let lineIndex = 0 17 | let foundIndex = undefined 18 | lines.forEach((line) => { 19 | if (line.indexOf('') !== -1) { 20 | foundIndex = lineIndex 21 | } 22 | lineIndex += 1 23 | }) 24 | if (foundIndex !== undefined) { 25 | lines.splice(foundIndex, 0, injectString); 26 | fs.writeFileSync(htmlPath, lines.join('\n')) 27 | console.warn("succesfully injected") 28 | } else { 29 | console.warn("not injected") 30 | } 31 | } 32 | 33 | const cp = (srcPath, destPath) => { 34 | fs.writeFileSync(destPath, fs.readFileSync(srcPath)) 35 | } 36 | 37 | const checkForLinkImportDependencies = (filePath) => { 38 | let lines = fs.readFileSync(filePath, 'utf-8').split('\n') 39 | lines.forEach((line) => { 40 | if (line.indexOf('') !== -1) { 41 | if (line.indexOf(' 25 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /workspace/games/controller/style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; 3 | background-color: #000; 4 | } 5 | canvas#vrum-dom { 6 | width: 100%; 7 | height: 100%; 8 | touch-action: none; /* needed to prevent default for virtualjoystick */ 9 | } 10 | 11 | #slider { 12 | position: absolute; 13 | left: 0px; 14 | right: 0px; 15 | width: 40%; 16 | height: 100%; 17 | background: #36454f; 18 | transform: translateX(-100%); 19 | -webkit-transform: translateX(-100%); 20 | z-index: 10001; 21 | padding: 50px; 22 | padding-top: 100px; 23 | } 24 | 25 | .slide-in { 26 | animation: slide-in 0.5s forwards; 27 | -webkit-animation: slide-in 0.5s forwards; 28 | } 29 | 30 | .slide-out { 31 | animation: slide-out 0.5s forwards; 32 | -webkit-animation: slide-out 0.5s forwards; 33 | } 34 | 35 | @keyframes slide-in { 36 | 100% { transform: translateX(0%); } 37 | } 38 | 39 | @-webkit-keyframes slide-in { 40 | 100% { -webkit-transform: translateX(0%); } 41 | } 42 | 43 | @keyframes slide-out { 44 | 0% { transform: translateX(0%); } 45 | 100% { transform: translateX(-100%); } 46 | } 47 | 48 | @-webkit-keyframes slide-out { 49 | 0% { -webkit-transform: translateX(0%); } 50 | 100% { -webkit-transform: translateX(-100%); } 51 | } 52 | 53 | 54 | @keyframes flickerAnimation { 55 | 0% { opacity:1; } 56 | 50% { opacity:0; } 57 | 100% { opacity:1; } 58 | } 59 | @-o-keyframes flickerAnimation{ 60 | 0% { opacity:1; } 61 | 50% { opacity:0; } 62 | 100% { opacity:1; } 63 | } 64 | @-moz-keyframes flickerAnimation{ 65 | 0% { opacity:1; } 66 | 50% { opacity:0; } 67 | 100% { opacity:1; } 68 | } 69 | @-webkit-keyframes flickerAnimation{ 70 | 0% { opacity:1; } 71 | 50% { opacity:0; } 72 | 100% { opacity:1; } 73 | } 74 | .animate-flicker { 75 | -webkit-animation: flickerAnimation 3s infinite; 76 | -moz-animation: flickerAnimation 3s infinite; 77 | -o-animation: flickerAnimation 3s infinite; 78 | animation: flickerAnimation 3s infinite; 79 | } 80 | .invisible { 81 | display: none; 82 | } 83 | 84 | #icon { 85 | pointer-events:auto; 86 | } 87 | 88 | .container { 89 | display: flex; 90 | justify-content: center; 91 | align-items: center; 92 | position: absolute; 93 | width: 100%; 94 | height: 100%; 95 | pointer-events: none; 96 | } 97 | -------------------------------------------------------------------------------- /workspace/games/controller2/ControllerScene.js: -------------------------------------------------------------------------------- 1 | class ControllerScene extends Scene { 2 | init(options) { 3 | this.add(new THREE.AmbientLight(0xffffff)) 4 | this.isPressed = false 5 | this.connect() 6 | } 7 | 8 | uninit() { 9 | if (!VirtualController.isAvailable()) { return } 10 | if (isBlank(this.vc)) { return } 11 | this.vc.uninit() 12 | } 13 | 14 | connect() { 15 | let mn = MeshNetwork.instance 16 | if (mn.isConnected()) { return } 17 | 18 | mn.connect('https://mesh.opinie-publica.ro', roomId, { 19 | cCallback: function () { 20 | Hodler.get('scene').addControls() 21 | }, 22 | dcCallback: function () { 23 | // lost connection with socket.io 24 | } 25 | }) 26 | 27 | mn.onData = (peer, data) => {} 28 | } 29 | 30 | getDirection(joystick) { 31 | let dS = (joystick.right() ? 'right' : '') + (joystick.up() ? 'up' : '') + (joystick.down() ? 'down' : '') + (joystick.left() ? 'left' : '') 32 | return dS || undefined 33 | } 34 | 35 | formatJoystick(joystick) { 36 | return { 37 | dX: joystick.deltaX(), 38 | dY: joystick.deltaY(), 39 | direction: this.getDirection(joystick), 40 | isPressed: joystick.isPressed 41 | } 42 | } 43 | 44 | addControls() { 45 | this.vc = new VirtualController({ 46 | joystickLeft: { 47 | stickRadius: 60 48 | }, 49 | joystickRight: { 50 | stickRadius: 60 51 | } 52 | }) 53 | this.vc.trackIsPressed() 54 | 55 | this.setInterval(function() { 56 | let scene = Hodler.get('scene') 57 | 58 | let obj = { 59 | vrumKey: vrumKey, 60 | type: 'vrum-controller', 61 | joystickLeft: scene.formatJoystick(scene.vc.joystickLeft), 62 | joystickRight: scene.formatJoystick(scene.vc.joystickRight) 63 | } 64 | 65 | MeshNetwork.instance.emit(obj) 66 | }, 1/30 * 1000); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /workspace/games/controller2/LandingScene.js: -------------------------------------------------------------------------------- 1 | class LandingScene extends Scene { 2 | init(options) { 3 | let buttons = [] 4 | this.buttons = buttons 5 | 6 | let camera = Hodler.get('camera') 7 | camera.position.set(0, 0, 10) 8 | camera.lookAt(new THREE.Vector3(0, 0, 0)) 9 | 10 | this.add(new THREE.AmbientLight()) 11 | 12 | let button1 = new Button3D('scan qr') 13 | button1.position.set(0, 1.5, 0) 14 | button1.onClick = () => { 15 | window.location.href = `zxing://scan/?ret=${window.location.href}?room={CODE}` 16 | } 17 | this.add(button1) 18 | buttons.push(button1) 19 | 20 | let button2 = new Button3D('join') 21 | button2.onClick = () => { 22 | console.log('switch to join scene') 23 | } 24 | button2.position.set(0, -1.5, 0) 25 | this.add(button2) 26 | buttons.push(button2) 27 | } 28 | 29 | tick(tpf) { 30 | this.buttons.forEach((button) => { 31 | button.tick(tpf) 32 | }) 33 | } 34 | 35 | doMouseEvent(event, raycaster) { 36 | this.buttons.forEach((button) => { 37 | button.doMouseEvent(event, raycaster) 38 | }) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /workspace/games/controller2/game.js: -------------------------------------------------------------------------------- 1 | // Persist.set('username', this.value) 2 | 3 | MeshNetwork.instance = new MeshNetwork() 4 | Utils.orientation('landscape') 5 | Persist.default('username', 'player') 6 | 7 | let controllerScene = new ControllerScene() 8 | let landingScene = new LandingScene() 9 | let roomId = MeshNetwork.getRoomId() 10 | let vrumKey = Utils.guid() 11 | let username = Persist.get('username') 12 | 13 | let startScene 14 | if (isBlank(roomId)) { 15 | startScene = landingScene 16 | } else { 17 | startScene = controllerScene 18 | } 19 | 20 | Engine.start(startScene, [ 21 | { type: 'font', path: '/workspace/assets/fonts/luckiest-guy' }, 22 | 23 | { type: 'model', path: '/workspace/assets/models/button.bg.001.glb' }, 24 | { type: 'model', path: '/workspace/assets/models/button.fg.001.glb' }, 25 | ]) 26 | -------------------------------------------------------------------------------- /workspace/games/controller2/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | vrum.js controller 10 | 11 | 12 | 13 | 14 | 15 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /workspace/games/controller2/style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; 3 | background-color: #000; 4 | } 5 | 6 | canvas#vrum-dom { 7 | width: 100%; 8 | height: 100%; 9 | } 10 | 11 | .container { 12 | display: flex; 13 | justify-content: center; 14 | align-items: center; 15 | position: absolute; 16 | width: 100%; 17 | height: 100%; 18 | pointer-events: none; 19 | } 20 | -------------------------------------------------------------------------------- /workspace/games/json-editor/game.js: -------------------------------------------------------------------------------- 1 | class GameScene extends Scene { 2 | init(options) { 3 | Hodler.get('engine').renderManager.setWidthHeight({ width: window.innerWidth / 2, height: window.innerHeight}) 4 | 5 | let camera = this.getCamera() 6 | camera.position.set(0, 0, 10) 7 | camera.lookAt(new THREE.Vector3(0, 0, 0)) 8 | 9 | let sky = new Sky() 10 | sky.visible = false 11 | this.sky = sky 12 | this.add(sky) 13 | 14 | let grid = Utils.grid({ step: 5 }) 15 | grid.visible = false 16 | this.grid = grid 17 | this.add(this.grid) 18 | 19 | this.model = undefined 20 | 21 | Utils.toggleOrbitControls() 22 | 23 | this.add(new THREE.AmbientLight()) 24 | 25 | this.tryInitEditor() 26 | } 27 | 28 | setJSON(json) { 29 | if (!isBlank(this.model)) { 30 | this.remove(this.model) 31 | } 32 | 33 | let model = SceneLoader.jsonToModel(json) 34 | if (isBlank(model)) { 35 | console.warn('no model') 36 | return 37 | } 38 | this.add(model) 39 | this.model = model 40 | } 41 | 42 | setJSONFromKey(key) { 43 | let json = AssetManager.get(key) 44 | let s = JSON.stringify(json, null, 2) 45 | this.editor.setValue(s) 46 | this.editor.clearSelection() 47 | document.querySelector('#panel-text').textContent = key 48 | } 49 | 50 | onJSONChange() { 51 | let scene = Hodler.get('scene') 52 | try { 53 | let jsonString = scene.editor.getValue() 54 | let json = JSON.parse(jsonString) 55 | scene.setJSON(json) 56 | } catch (e){ 57 | if (e instanceof SyntaxError) { 58 | console.error('invalid json') 59 | // console.error(e) 60 | } else { 61 | throw e 62 | } 63 | } 64 | } 65 | 66 | tryInitEditor() { 67 | let scene = Hodler.get('scene') 68 | if (typeof(ace) !== "undefined") { 69 | scene.editor = ace.edit('editor'); 70 | scene.editor.getSession().setMode('ace/mode/json'); 71 | scene.editor.setTheme('ace/theme/monokai'); 72 | scene.editor.getSession().setUseWrapMode(true); 73 | scene.editor.session.on('change', function(delta) { 74 | scene.onJSONChange() 75 | scene.setTimeout(() => { 76 | scene.onJSONChange() 77 | }, 2000) 78 | }); 79 | load('graffiti/majestic-frog-cover.json') 80 | document.querySelectorAll('.panel').forEach((e) => { 81 | e.style.display = 'flex' 82 | }) 83 | stopPropagation() 84 | } else { 85 | scene.setTimeout(scene.tryInitEditor, 10) 86 | } 87 | } 88 | 89 | tick(tpf) { 90 | if (isBlank(this.model)) { return } 91 | if (this.model.material instanceof ShaderMaterial) { 92 | this.model.material.tick(tpf) 93 | } 94 | } 95 | 96 | doMouseEvent(event, raycaster) { 97 | // console.log(`${event.type} ${event.which} ${event.x}:${event.y} ${event.wheelDelta}`) 98 | } 99 | 100 | doKeyboardEvent(event) { 101 | // console.log(`${event.type} ${event.code} (${event.which})`) 102 | } 103 | } 104 | 105 | Config.instance.engine.debug = true 106 | Config.instance.window.resize = false 107 | 108 | const stopPropagation = (event) => { 109 | let stopIt = (event) => { event.stopPropagation() } 110 | document.querySelectorAll('#editor').forEach((e) => { 111 | e.addEventListener('keydown', stopIt) 112 | }) 113 | } 114 | 115 | const save = () => { 116 | let editor = Hodler.get('scene').editor 117 | let s = editor.getValue() 118 | let outputName = document.querySelector('#panel-text').textContent 119 | Utils.saveFile(JSON.parse(s), outputName) 120 | } 121 | 122 | const load = (assetFolderAndName) => { 123 | let scene = Hodler.get('scene') 124 | let basePath = '/workspace/assets/' 125 | if (isBlank(assetFolderAndName)) { 126 | assetFolderAndName = prompt(`Load asset from ${basePath}`); 127 | } 128 | Utils.loadDependencies(basePath, assetFolderAndName, (asset) => { 129 | let key = AssetManager.getAssetKey(asset) 130 | scene.setJSONFromKey(key) 131 | }, (asset) => { 132 | }) 133 | } 134 | 135 | let wireframe = false 136 | const toggleWireframe = () => { 137 | wireframe = !wireframe 138 | Utils.setWireframe(wireframe) 139 | } 140 | 141 | const toggleGird = () => { 142 | let grid = Hodler.get('scene').grid 143 | grid.visible = !grid.visible 144 | } 145 | 146 | const toggleSky = () => { 147 | let sky = Hodler.get('scene').sky 148 | sky.visible = !sky.visible 149 | } 150 | 151 | Engine.start(new GameScene(), [ 152 | { type: 'font', path: '/workspace/assets/fonts/luckiest-guy' }, 153 | ]) 154 | -------------------------------------------------------------------------------- /workspace/games/json-editor/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | vrum.js graffiti-editor 10 | 54 | 55 | 56 |
57 |
58 | load 59 |
60 |
61 | save 62 |
63 |
64 | 65 | wireframe 66 | grid 67 | sky 68 |
69 |
70 |
71 |
72 | loading 73 |
74 |
75 |
76 |
77 | 78 | 79 | 80 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /workspace/games/model-viewer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | vrum.js model-viewer 10 | 19 | 20 | 21 | 35 | 36 | 60 | 61 | 62 | 63 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /workspace/games/model-viewer/main.js: -------------------------------------------------------------------------------- 1 | Config.instance.window.showStatsOnStart = true 2 | Config.instance.engine.debug = true 3 | 4 | Persist.defaultJson('lastModels', [ 5 | '/workspace/assets/models/chicken.gltf', 6 | '/workspace/assets/models/panda.glb', 7 | '/workspace/assets/models/button.glb', 8 | ]) 9 | updateLastModels() 10 | stopPropagation() 11 | 12 | let mainScene = new MainScene() 13 | Hodler.add('mainScene', mainScene) 14 | 15 | let loadingScene = new LoadingScene(mainScene, [ 16 | { type: 'image', path: '/workspace/assets/textures/vrum.png' }, 17 | ]) 18 | 19 | Engine.start(loadingScene) 20 | 21 | let camera = Hodler.get('camera'); 22 | camera.position.set(0, 4, 10) 23 | camera.lookAt(new THREE.Vector3(0,0,0)) 24 | -------------------------------------------------------------------------------- /workspace/games/model-viewer/scene.js: -------------------------------------------------------------------------------- 1 | class MainScene extends Scene { 2 | init(options) { 3 | this.add(new THREE.AmbientLight(0xffffff)) 4 | this.model = Utils.plane({ map: 'vrum.png', width: 6.4, height: 3.65 }) 5 | this.add(this.model) 6 | 7 | let shadowMaterial = new THREE.ShadowMaterial({ side: THREE.DoubleSide }) 8 | shadowMaterial.opacity = 0.3 9 | const plane = Utils.plane({ material: shadowMaterial, size: 100 }) 10 | plane.shadowReceive() 11 | plane.rotation.x = -Math.PI / 2 12 | this.add(plane) 13 | 14 | this.grid = Utils.grid({ step: 5 }) 15 | this.add(this.grid) 16 | 17 | this.sky = new Sky(); 18 | this.sky.addToScene(this) 19 | this.sky.updateSun(this.sky.distance, 0.1, this.sky.azimuth) 20 | 21 | this.orbit = Utils.toggleOrbitControls() 22 | this.orbit.enabled = true 23 | this.orbit.damping = 0.2 24 | Utils.toggleShadows() 25 | 26 | document.querySelector('#modelControls').style.display = '' 27 | document.querySelector('#animationControls').style.display = '' 28 | 29 | let recordButton = document.querySelector('#recordButton') 30 | recordButton.addEventListener('click', (event) => { 31 | if (VideoRecorderManager.isRecording()) { 32 | recordButton.textContent = '⏺️ Record' 33 | VideoRecorderManager.stop() 34 | } else { 35 | recordButton.textContent = '⏹️ Stop' 36 | VideoRecorderManager.start() 37 | } 38 | }) 39 | 40 | let screenshotButton = document.querySelector('#screenshot') 41 | screenshotButton.addEventListener('click', (event) => { 42 | Utils.screenshot() 43 | }) 44 | } 45 | 46 | addModelToScene(path, scale) { 47 | if (isBlank(scale)) { scale = 1 } 48 | 49 | let scene = Hodler.get('scene') 50 | scene.remove(scene.model) 51 | 52 | const key = AssetManager.getAssetKey({ path: path }) 53 | let newModel = AssetManager.clone(key) 54 | if (isBlank(newModel)) { 55 | throw `Asset ${path} with key ${key} is blank` 56 | } 57 | 58 | newModel.shadowCastAndNotReceive() 59 | scene.model = newModel 60 | scene.add(newModel) 61 | newModel.scale.setScalar(scale) 62 | console.info(`Model ${key} added`) 63 | 64 | // var central = Measure.getCenterPoint(newModel.skinnedMesh) 65 | // scene.orbit.center.set(central.x, central.y, central.z) 66 | 67 | var animationsHTML = document.querySelector('#animations') 68 | animationsHTML.innerHTML = '' 69 | 70 | newModel.animations.names().forEach((animationName) => { 71 | var checkbox = document.createElement('input') 72 | checkbox.type = 'checkbox' 73 | checkbox.className = 'stopAllException' 74 | checkbox.setAttribute('animation', animationName) 75 | 76 | var button = document.createElement('span') 77 | button.innerHTML = animationName 78 | button.className = 'button2' 79 | button.style.padding = '16px' 80 | button.addEventListener('click', (event) => { 81 | let loop = document.getElementById('loop').checked 82 | let reverse = document.getElementById('reverse').checked 83 | let timeScale = parseFloat(document.getElementById('timeScale').value) 84 | let stopAll = document.getElementById('stopAll').checked 85 | let stopAllExceptions = Array.from(document.querySelectorAll('.stopAllException')).filter((e) => { return e.checked }).map((e) => { return e.getAttribute('animation') }) 86 | scene.model.animations.from = animationName 87 | scene.model.animations.play(animationName, { loop: loop, reverse: reverse, timeScale: timeScale, stopAll: stopAll, stopAllExceptions: stopAllExceptions }) 88 | }) 89 | 90 | var p = document.createElement('p') 91 | p.appendChild(checkbox) 92 | p.appendChild(button) 93 | 94 | animationsHTML.append(p) 95 | }) 96 | } 97 | 98 | loadModel(path, scale) { 99 | AssetManager.loadAssets([ 100 | { type: 'model', path: path }, 101 | ], () => { 102 | let scene = Hodler.get('scene') 103 | scene.addModelToScene(path, scale) 104 | }) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /workspace/games/model-viewer/utils.js: -------------------------------------------------------------------------------- 1 | const stopPropagation = (event) => { 2 | let stopIt = (event) => { event.stopPropagation() } 3 | document.querySelectorAll('input[type=text]').forEach((e) => { 4 | e.addEventListener('keydown', stopIt) 5 | }) 6 | } 7 | 8 | const loadModel = (url) => { 9 | let inputUrl = document.querySelector('#inputUrl') 10 | let scale = parseFloat(document.querySelector('#scale').value) 11 | if (!isBlank(url)) { 12 | inputUrl.value = url 13 | } 14 | Hodler.get('mainScene').loadModel(inputUrl.value, scale) 15 | const lastModels = Persist.getJson('lastModels') 16 | if (!lastModels.includes(inputUrl.value)) { 17 | lastModels.insert(0, inputUrl.value) 18 | while (lastModels.size() > 8) { 19 | lastModels.pop() 20 | } 21 | Persist.setJson('lastModels', lastModels) 22 | updateLastModels() 23 | } 24 | } 25 | 26 | const updateLastModels = () => { 27 | const lastModels = Persist.getJson('lastModels') 28 | const element = document.querySelector('#lastModels') 29 | element.innerHTML = '' 30 | 31 | lastModels.forEach((e) => { 32 | const button = document.createElement('span') 33 | button.className = 'button' 34 | button.setAttribute('onclick', `loadModel('${e}')`) 35 | button.innerHTML = AssetManager.getAssetKey({ path: e }) 36 | element.append(button) 37 | }) 38 | } 39 | 40 | let wireframe = false 41 | const toggleWireframe = () => { 42 | wireframe = !wireframe 43 | Utils.setWireframe(wireframe) 44 | } 45 | 46 | const toggleGird = () => { 47 | let grid = Hodler.get('scene').grid 48 | grid.visible = !grid.visible 49 | } 50 | 51 | const toggleSky = () => { 52 | let sky = Hodler.get('scene').sky 53 | sky.visible = !sky.visible 54 | } 55 | -------------------------------------------------------------------------------- /workspace/games/project/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mess110/vrum/3f4e1cc682e20fd7415eee124ddd258dfc2f0074/workspace/games/project/assets/favicon.ico -------------------------------------------------------------------------------- /workspace/games/project/assets/luckiest-guy.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mess110/vrum/3f4e1cc682e20fd7415eee124ddd258dfc2f0074/workspace/games/project/assets/luckiest-guy.eot -------------------------------------------------------------------------------- /workspace/games/project/assets/luckiest-guy.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mess110/vrum/3f4e1cc682e20fd7415eee124ddd258dfc2f0074/workspace/games/project/assets/luckiest-guy.ttf -------------------------------------------------------------------------------- /workspace/games/project/assets/luckiest-guy.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mess110/vrum/3f4e1cc682e20fd7415eee124ddd258dfc2f0074/workspace/games/project/assets/luckiest-guy.woff -------------------------------------------------------------------------------- /workspace/games/project/assets/luckiest-guy.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mess110/vrum/3f4e1cc682e20fd7415eee124ddd258dfc2f0074/workspace/games/project/assets/luckiest-guy.woff2 -------------------------------------------------------------------------------- /workspace/games/project/assets/vrum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mess110/vrum/3f4e1cc682e20fd7415eee124ddd258dfc2f0074/workspace/games/project/assets/vrum.png -------------------------------------------------------------------------------- /workspace/games/project/game.js: -------------------------------------------------------------------------------- 1 | class GameScene extends Scene { 2 | init(options) { 3 | let camera = this.getCamera() 4 | camera.position.set(0, 0, 10) 5 | camera.lookAt(new THREE.Vector3(0, 0, 0)) 6 | 7 | this.model = Utils.plane({ map: 'vrum.png', width: 6.4, height: 3.65 }) 8 | this.add(this.model) 9 | } 10 | 11 | tick(tpf) { 12 | this.model.rotation.x += tpf / 2 13 | this.model.rotation.y += tpf / 2 14 | } 15 | 16 | doMouseEvent(event, raycaster) { 17 | console.log(`${event.type} ${event.which} ${event.x}:${event.y} ${event.wheelDelta}`) 18 | } 19 | 20 | doKeyboardEvent(event) { 21 | console.log(`${event.type} ${event.code} (${event.which})`) 22 | } 23 | 24 | doGamepadEvent(event) { 25 | // console.log(event.type) 26 | } 27 | } 28 | 29 | let gameScene = new GameScene() 30 | 31 | Engine.start(gameScene, [ 32 | { type: 'font', path: 'assets/luckiest-guy' }, 33 | { type: 'image', path: 'assets/vrum.png' }, 34 | ]) 35 | -------------------------------------------------------------------------------- /workspace/games/project/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | vrum.js engine 10 | 14 | 15 | 16 | 17 | 18 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /workspace/games/sandbox/http.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Think of this as the sandbox for the app. 4 | // It starts a webserver, after that it opens 5 | // a browser with the game. 6 | // 7 | // The webserver will stay open as long as /ping.json 8 | // is called at least once before the pingGrace runs out 9 | 10 | const path = require('path'); 11 | const fs = require('fs'); 12 | const portfinder = require('portfinder'); 13 | const pkgOpen = require('opn-pkg'); 14 | 15 | const now = () => { 16 | return Math.floor(new Date() / 1000) 17 | } 18 | 19 | const routes = (config, request, response) => { 20 | if (request.url === '/ping.json') { 21 | console.log('received ping.json - keeping alive') 22 | response.writeHead(200); 23 | response.end('{}') 24 | lastPing = now() 25 | } else { 26 | 27 | var file = path.normalize(config.root + request.url); 28 | file = (file == config.root + '/') ? file + config.index : file; 29 | 30 | console.log('Trying to serve: ', file); 31 | 32 | const showError = (response, error) => { 33 | console.log(error); 34 | response.writeHead(500); 35 | response.end('Internal Server Error'); 36 | } 37 | 38 | fs.exists(file, (exists) => { 39 | if (exists) { 40 | fs.stat(file, (error, stat) => { 41 | if (error) { 42 | return showError(response, error); 43 | } 44 | 45 | if (stat.isDirectory()) { 46 | response.writeHead(403); 47 | response.end('Forbidden'); 48 | } else { 49 | response.writeHead(200); 50 | response.end(fs.readFileSync(file)) 51 | } 52 | }); 53 | } else { 54 | response.writeHead(404); 55 | response.end('Not found'); 56 | } 57 | }); 58 | } 59 | } 60 | 61 | const init = (config) => { 62 | const baseUrl = `http://localhost:${config.port}/` 63 | console.log(`Running at ${baseUrl}`) 64 | 65 | console.log('Files:') 66 | fs.readdirSync(config.root).forEach(file => { 67 | console.log(` ${file}`); 68 | }) 69 | const assetsDir = path.join(config.root, 'assets') 70 | if (fs.existsSync(assetsDir) && fs.lstatSync(assetsDir).isDirectory()) { 71 | console.log('Assets:') 72 | fs.readdirSync(assetsDir).forEach(file => { 73 | console.log(` ${file}`); 74 | }) 75 | } 76 | 77 | pkgOpen(baseUrl + 'index.html') 78 | 79 | // If we don't receive a ping within the grace period, we exit 80 | setInterval(() => { 81 | let limit = lastPing + config.pingGrace 82 | if (limit < now()) { 83 | console.log(`Last ping received at ${lastPing} which is over the ${config.pingGrace} seconds grace period. Closing`) 84 | process.exit(0) 85 | } 86 | }, 1000) 87 | } 88 | 89 | let lastPing = now() 90 | 91 | portfinder.getPort({port: 8000, stopPort: 8999}, (err, port) => { 92 | let config = { 93 | root: path.join(__dirname), 94 | index: 'index.html', 95 | port: process.env.PORT || port, 96 | pingGrace: 60 // seconds server will not close after receiveing a ping 97 | }; 98 | 99 | require('http').createServer((request, response) => { 100 | routes(config, request, response) 101 | }).listen(config.port, () => { 102 | init(config) 103 | }) 104 | }); 105 | -------------------------------------------------------------------------------- /workspace/games/scene-editor/game.js: -------------------------------------------------------------------------------- 1 | Config.instance.window.showStatsOnStart = true 2 | Config.instance.engine.debug = true 3 | 4 | function readSingleFile(e) { 5 | var file = e.target.files[0]; 6 | if (!file) { 7 | return; 8 | } 9 | var reader = new FileReader(); 10 | reader.onload = function(e) { 11 | let contents = e.target.result; 12 | let json = JSON.parse(contents) 13 | let cinematic = new SceneLoader(json) 14 | Hodler.add('cinematic', cinematic) 15 | 16 | Engine.switch(new GameScene(), cinematic.getAssets()) 17 | 18 | }; 19 | reader.readAsText(file); 20 | } 21 | 22 | document.querySelector('#scene-input') 23 | .addEventListener('change', readSingleFile, false); 24 | 25 | Hodler.add('cinematic', new SceneLoader({ 26 | assets: [ 27 | { type: 'font', path: '/workspace/assets/fonts/luckiest-guy' }, 28 | ] 29 | })) 30 | 31 | Engine.start(new GameScene(), Hodler.get('cinematic').getAssets()) 32 | -------------------------------------------------------------------------------- /workspace/games/scene-editor/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | vrum.js scene-editor 10 | 26 | 27 | 28 | 29 | 30 | 36 | 37 | 38 | 39 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /workspace/games/test/gamepad-api/Gamepad.js: -------------------------------------------------------------------------------- 1 | class GameScene extends Scene { 2 | init(options) { 3 | let camera = this.getCamera() 4 | camera.position.set(0, 30, 30) 5 | camera.lookAt(new THREE.Vector3(0, 0, 0)) 6 | 7 | this.add(new THREE.AmbientLight()) 8 | 9 | // returns the same thing 10 | console.log(`Gamepad support: ${this.gamepadSupported || Utils.gamepad()}`) 11 | 12 | var geometry = new THREE.BoxGeometry( 1, 1, 1 ) 13 | var material = new THREE.MeshBasicMaterial( { color: 0x4d4d4d } ) 14 | var cube = new THREE.Mesh( geometry, material ) 15 | cube.setWireframe(true) 16 | this.add(cube) 17 | this.cube = cube 18 | 19 | this.connectedGamepads = 0 20 | this.lastVibration = 0 21 | 22 | let text = new BaseText({ 23 | text: '0 connected', fillStyle: 'gray', 24 | canvasW: 1024, canvasH: 1024, 25 | font: '64px luckiest-guy'}) 26 | text.position.set(0, 0, 4) 27 | text.scale.set(5, 5, 5) 28 | text.lookAt(camera.position) 29 | this.text = text 30 | this.add(text) 31 | } 32 | 33 | tick(tpf) { 34 | this.text.setText(`${this.connectedGamepads} connected`) 35 | } 36 | 37 | vibrate(gamepad) { 38 | if (this.lastVibration + 2 > this.uptime) { return } 39 | this.lastVibration = this.uptime 40 | gamepad.vibrationActuator.playEffect("dual-rumble", { 41 | startDelay: 0, 42 | duration: 1000, 43 | weakMagnitude: 1.0, 44 | strongMagnitude: 1.0 45 | }); 46 | } 47 | 48 | // event looks like 49 | // 50 | // [ 51 | // type: 'gamepadtick-vrum 52 | // { 53 | // axes: [0.01, 0.01, 0.02, 0.04], 54 | // buttons: [ 55 | // { pressed: true, value: 1 }, 56 | // { pressed: false, value: 0 }, 57 | // { pressed: false, value: 0 }, 58 | // { pressed: false, value: 0 }, 59 | // [...] 60 | // ], 61 | // connected: true, 62 | // id: "Xbox 360 Controller (XInput STANDARD GAMEPAD)", 63 | // index: 0, 64 | // mapping: "standard", 65 | // timestamp: 177550 66 | // }, 67 | // null, 68 | // null, 69 | // null 70 | // ] 71 | doGamepadEvent(event) { 72 | // console.log(event.type) 73 | if (event.type !== 'gamepadtick-vrum') { return } 74 | 75 | let connectedGamepads = 0 76 | for (var i = 0; i < event.length; i++) { 77 | let gamepad = event[i] 78 | if (isBlank(gamepad)) { continue } 79 | connectedGamepads += 1 80 | gamepad.axes.forEach((axe, index) => { 81 | if (index == 0) { 82 | this.cube.position.x += axe 83 | } else if (index == 1) { 84 | this.cube.position.z += axe 85 | } else if (index == 2) { 86 | this.cube.rotation.x += axe 87 | } else if (index == 3) { 88 | this.cube.rotation.z += axe 89 | } 90 | }) 91 | gamepad.buttons.forEach((button, index) => { 92 | if (button.pressed) { 93 | console.log(`button index ${index} pressed`) 94 | if (index == 0) { 95 | this.vibrate(gamepad) 96 | } 97 | } 98 | }) 99 | } 100 | 101 | this.connectedGamepads = connectedGamepads 102 | } 103 | } 104 | 105 | // Config.instance.camera.type = 'perspective' 106 | Config.instance.camera.type = 'ortographic' 107 | 108 | let gameScene = new GameScene() 109 | Engine.start(gameScene, [ 110 | { type: 'font', path: '/workspace/assets/fonts/luckiest-guy' }, 111 | ]) 112 | -------------------------------------------------------------------------------- /workspace/games/test/gamepad-api/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | gamepad 10 | 14 | 15 | 16 | 17 | 18 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /workspace/games/test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | vrum.js tests 10 | 46 | 47 | 48 |
49 |
50 |
51 | main tests 52 |
53 |
54 | scene setup test 55 |
56 |
57 | gamepad-api test 58 |
59 |
60 | networking test 61 |
62 |
63 | platfomer test 64 |
65 |
66 |
67 | 68 | 69 | 70 | 71 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /workspace/games/test/main/CameraTest.js: -------------------------------------------------------------------------------- 1 | class CameraTest extends Scene { 2 | init(options) { 3 | this.add(Utils.box()) 4 | } 5 | 6 | doKeyboardEvent(event) { 7 | switchScene(event) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /workspace/games/test/main/Main.js: -------------------------------------------------------------------------------- 1 | Config.instance.engine.debug = true 2 | Config.instance.window.showStatsOnStart = true 3 | 4 | class Box extends THREE.Mesh { 5 | constructor() { 6 | var geometry = new THREE.BoxGeometry( 0.4, 0.4, 0.4 ) 7 | var material = new THREE.MeshBasicMaterial( { color: 0xffff00 } ) 8 | super(geometry, material) 9 | this.shadowCastAndNotReceive() 10 | } 11 | } 12 | 13 | PoolManager.on('spawn', Box, function (item) { 14 | if (isBlank(item.outline)) { 15 | Utils.addMeshOutlineTo(item, new Box()) 16 | } 17 | item.position.set(-2, 5, 0) 18 | Hodler.get('scene').add(item) 19 | }) 20 | 21 | PoolManager.on('release', Box, function (item) { 22 | Hodler.get('scene').remove(item) 23 | }) 24 | 25 | HighScoreManager.auth('guest', 'guest') 26 | HighScoreManager.get().responseHandler = function (data) { 27 | console.log(data) 28 | } 29 | HighScoreManager.getScores(20) 30 | 31 | const switchScene = (event) => { 32 | if (event.type != 'keydown') { return } 33 | if (!event.code.startsWith('Digit')) { return } 34 | let digit = parseInt(event.code[5]) 35 | if (!(0 <= digit && digit < 10)) { return } 36 | let sceneKey = `scene${digit}` 37 | if (!Hodler.has(sceneKey)) { return } 38 | Engine.switch(Hodler.get(sceneKey)) 39 | } 40 | 41 | const resetCamPosition = (distance) => { 42 | if (isBlank(distance)) { distance = 20 } 43 | let camera = Hodler.get('camera') 44 | camera.position.set(0, distance, distance) 45 | camera.lookAt(new THREE.Vector3(0, 0, 0)) 46 | return camera 47 | } 48 | 49 | let scene1 = new FeaturesTest() 50 | let scene2 = new Scene2() 51 | let scene3 = new Scene3() 52 | let scene4 = new SceneLoaderTest() 53 | let scene5 = new CameraTest() 54 | let loadingScene = new LoadingScene(scene1, [ 55 | { type: 'font', path: '/workspace/assets/fonts/luckiest-guy' }, 56 | { type: 'model', path: '/workspace/assets/models/chicken.gltf' }, 57 | { type: 'model', path: '/workspace/assets/models/chicken.gltf' }, 58 | { type: 'model', path: '/workspace/assets/models/panda.glb' }, 59 | { type: 'image', path: '/workspace/assets/models/chicken.png' }, 60 | { type: 'image', path: '/workspace/assets/textures/chicken_black.jpeg' }, 61 | { type: 'image', path: '/workspace/assets/textures/hand.png' }, 62 | { type: 'image', path: '/workspace/assets/textures/heightmap3.png' }, 63 | { type: 'image', path: '/workspace/assets/textures/waternormals.jpg' }, 64 | { type: 'image', path: '/workspace/assets/textures/spe_smokeparticle.png' }, 65 | { type: 'image', path: '/workspace/assets/textures/spe_sprite-explosion2.png' }, 66 | { type: 'image', path: '/workspace/assets/textures/black-faded-border.png' }, 67 | { type: 'image', path: '/workspace/assets/textures/vrum.png' }, 68 | { type: 'image', path: '/workspace/assets/textures/grass.png' }, 69 | { type: 'json', path: '/workspace/assets/particles/particle.json' }, 70 | { type: 'json', path: '/workspace/assets/graffiti/majestic-frog-cover.json' }, 71 | { type: 'json', path: '/workspace/assets/shaders/basic_shader.json' }, 72 | { type: 'json', path: '/workspace/assets/shaders/dissolve_shader.json' }, 73 | { type: 'json', path: '/workspace/assets/terrains/terrain.json' }, 74 | { type: 'json', path: '/workspace/assets/scenes/boat-scene.json' }, 75 | { type: 'sound', path: '/workspace/assets/sounds/hit.wav' }, 76 | { type: 'sound', path: '/workspace/assets/sounds/SuperHero_original.ogg' }, 77 | ]) 78 | 79 | Hodler.add('scene1', scene1) 80 | Hodler.add('scene2', scene2) 81 | Hodler.add('scene3', scene3) 82 | Hodler.add('scene4', scene4) 83 | Hodler.add('scene5', scene5) 84 | 85 | Persist.default('name', 'player1') 86 | console.log(Persist.get('name')) 87 | 88 | // Config.instance.camera.type = 'ortographic' 89 | Engine.start(loadingScene) 90 | 91 | AfterEffects.prototype.effects = AfterEffects.bloomFilm 92 | // Hodler.get('afterEffects').enable() 93 | -------------------------------------------------------------------------------- /workspace/games/test/main/Scene2.js: -------------------------------------------------------------------------------- 1 | class Scene2 extends Scene { 2 | init(options) { 3 | let camera = resetCamPosition(20) 4 | camera.position.set(0, 0, 20) 5 | camera.lookAt(new THREE.Vector3(0, 0, 0)) 6 | 7 | // Utils.toggleOrbitControls() 8 | this.add(new THREE.AmbientLight()) 9 | this.add(new Sky()) 10 | 11 | let geometry = new THREE.BoxGeometry( 1, 1, 1 ) 12 | let material = new THREE.MeshBasicMaterial( { color: 0x4d4d4d } ) 13 | let cube = new THREE.Mesh( geometry, material ) 14 | this.add(cube) 15 | this.cube = cube 16 | 17 | let control = new PositionXZRotationYControls() 18 | this.control = control 19 | } 20 | 21 | uninit() { 22 | // Utils.toggleOrbitControls() 23 | } 24 | 25 | tick(tpf) { 26 | this.control.tick(tpf) 27 | 28 | this.cube.position.x += this.control.velocity.x 29 | this.cube.position.y -= this.control.velocity.z 30 | } 31 | 32 | doKeyboardEvent(event) { 33 | switchScene(event) 34 | this.control.doKeyboardEvent(event) 35 | } 36 | 37 | doGamepadEvent(event, gamepadIndex) { 38 | this.control.doGamepadEvent(event, gamepadIndex) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /workspace/games/test/main/Scene3.js: -------------------------------------------------------------------------------- 1 | class Scene3 extends Scene { 2 | init(options) { 3 | let camera = resetCamPosition(20) 4 | camera.position.set(0, 0, 20) 5 | camera.lookAt(new THREE.Vector3(0, 0, 0)) 6 | 7 | // Utils.toggleOrbitControls() 8 | this.add(new THREE.AmbientLight()) 9 | this.add(new Sky()) 10 | 11 | let geometry = new THREE.BoxGeometry( 1, 1, 1 ) 12 | let material = new THREE.MeshBasicMaterial( { color: 0x4d4d4d } ) 13 | let cube = new THREE.Mesh( geometry, material ) 14 | this.add(cube) 15 | this.cube = cube 16 | 17 | let control = new PositionXZRotationYControls() 18 | this.control = control 19 | } 20 | 21 | uninit() { 22 | // Utils.toggleOrbitControls() 23 | } 24 | 25 | tick(tpf) { 26 | this.control.tick(tpf) 27 | 28 | this.cube.position.x += this.control.velocity.x 29 | this.cube.position.y -= this.control.velocity.z 30 | } 31 | 32 | doKeyboardEvent(event) { 33 | switchScene(event) 34 | this.control.doKeyboardEvent(event) 35 | } 36 | 37 | doGamepadEvent(event, gamepadIndex) { 38 | this.control.doGamepadEvent(event, gamepadIndex) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /workspace/games/test/main/SceneLoaderTest.js: -------------------------------------------------------------------------------- 1 | class SceneLoaderTest extends Scene { 2 | init(options) { 3 | resetCamPosition() 4 | 5 | let json = AssetManager.get('boat-scene.json') 6 | let sceneLoader = new SceneLoader(json) 7 | 8 | AssetManager.loadAssets(sceneLoader.getAssets(), () => { 9 | sceneLoader.addToScene() 10 | }) 11 | } 12 | 13 | tick(tpf) {} 14 | 15 | doKeyboardEvent(event) { 16 | switchScene(event) 17 | if (event.type == 'keydown' && event.which == 32) { 18 | Engine.switch(Hodler.get('scene1')) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /workspace/games/test/main/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | main tests 10 | 14 | 15 | 16 | 17 | 18 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /workspace/games/test/networking/Networking.js: -------------------------------------------------------------------------------- 1 | let isMaster = true || MeshNetwork.isMaster() 2 | let room = Utils.guid() 3 | 4 | new QRCode("qrcode", { text: room, width: 128, height: 128 }) 5 | document.querySelector('#qrcode-text').innerHTML = room 6 | document.querySelector('#client-link').href = `/workspace/games/controller/?room=${room}` 7 | 8 | let mn = new MeshNetwork() 9 | mn.setSignalingDebug(true) 10 | let socket = mn.connect('https://mesh.opinie-publica.ro', room, { audio: false, video: false }) 11 | 12 | mn.onConnect = (peer) => { 13 | let element = document.createElement('div') 14 | element.setAttribute('id', `peer-${peer.cmKey}`) 15 | 16 | let elementKey = document.createElement('p') 17 | elementKey.innerHTML = peer.cmKey 18 | element.appendChild(elementKey) 19 | 20 | let elementAction = document.createElement('p') 21 | element.appendChild(elementAction) 22 | 23 | document.querySelector('#peers').appendChild(element) 24 | } 25 | 26 | mn.onData = (peer, data) => { 27 | element = document.querySelector(`#peer-${peer.cmKey}`) 28 | if (isBlank(element)) { 29 | console.error(`Could not find #peer-${peer.cmKey}`) 30 | return 31 | } 32 | 33 | element.children[1].innerHTML = JSON.stringify(data) 34 | } 35 | 36 | mn.onError = (peer, error) => { 37 | console.error(error) 38 | } 39 | 40 | mn.onClose = (peer) => { 41 | let row = document.querySelector(`#peer-${peer.cmKey}`) 42 | document.querySelector('#peers').removeChild(row) 43 | } 44 | -------------------------------------------------------------------------------- /workspace/games/test/networking/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | networking 8 | 12 | 13 | 14 |
15 |
16 |
17 |

18 |

client

19 |
20 | 21 |
22 |
23 |
24 | 25 | 26 | 27 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /workspace/games/test/platformer/MainScene.js: -------------------------------------------------------------------------------- 1 | class MainScene extends Scene { 2 | init(options) { 3 | let camera = this.getCamera() 4 | camera.position.set(0, 0, 20) 5 | camera.lookAt(new THREE.Vector3(0, 0, 0)) 6 | 7 | // Utils.toggleOrbitControls() 8 | this.add(new THREE.AmbientLight()) 9 | this.add(new Sky()) 10 | 11 | let statsPanel = new StatsPanel() 12 | this.statsPanel = statsPanel 13 | this.add(statsPanel) 14 | 15 | let groundColor = 0x00dd00 16 | let geometryG = new THREE.BoxGeometry( 15, 1, 1 ) 17 | let materialG = new THREE.MeshBasicMaterial( { color: groundColor } ) 18 | let ground = new THREE.Mesh( geometryG, materialG ) 19 | ground.position.set(0, -1, 0) 20 | 21 | let geometryI1 = new THREE.BoxGeometry( 7.5, 1, 1 ) 22 | let materialI1 = new THREE.MeshBasicMaterial( { color: groundColor } ) 23 | let island1 = new THREE.Mesh( geometryI1, materialI1 ) 24 | island1.position.set(-9, 2, 0) 25 | this.add(island1) 26 | 27 | let geometryI2 = new THREE.BoxGeometry( 7.5, 1, 1 ) 28 | let materialI2 = new THREE.MeshBasicMaterial( { color: groundColor } ) 29 | let island2 = new THREE.Mesh( geometryI2, materialI2 ) 30 | island2.position.set(9, 3, 0) 31 | this.add(island2) 32 | 33 | let geometryI3 = new THREE.BoxGeometry( 1, 1, 1 ) 34 | let materialI3 = new THREE.MeshBasicMaterial( { color: groundColor } ) 35 | let island3 = new THREE.Mesh( geometryI3, materialI3 ) 36 | island3.position.set(3, 0, 0) 37 | this.add(island3) 38 | 39 | this.add(ground) 40 | this.ground = ground 41 | 42 | let rayscanner = new RayScanner([ground, island1, island2, island3]) 43 | rayscanner.drawLines = true 44 | rayscanner.lineLength = 0.5 45 | this.rayscanner = rayscanner 46 | 47 | this.controls = new PlatformerControls() 48 | 49 | let geometry = new THREE.BoxGeometry( 1, 1, 1 ) 50 | let material = new THREE.MeshBasicMaterial( { color: 0x4d4d4d } ) 51 | let cube = new THREE.Mesh( geometry, material ) 52 | cube.position.set(0, 3, 0) 53 | this.add(cube) 54 | this.cube = cube 55 | } 56 | 57 | uninit() { 58 | // Utils.toggleOrbitControls() 59 | } 60 | 61 | tick(tpf) { 62 | this.controls.tick(tpf) 63 | Measure.clearLines() 64 | 65 | let cube = this.cube 66 | let fromPosition = cube.position.clone() 67 | 68 | let scaledVelocityY = this.controls.velocity.y * tpf 69 | this.statsPanel.setText(scaledVelocityY) 70 | 71 | fromPosition.x += this.controls.velocity.x * tpf 72 | 73 | let beneath = this.rayscanner.scanEdges(fromPosition, 0.5, new THREE.Vector3(0, -1, 0)) 74 | if (beneath.any() && this.controls.velocity.y < 0) { 75 | this.controls.land() 76 | cube.position.y = beneath.first().object.position.y + 1 77 | } 78 | 79 | let above = this.rayscanner.scanEdges(fromPosition, 0.5, new THREE.Vector3(0, 1, 0)) 80 | if (above.any() && this.controls.velocity.y > 0) { 81 | this.controls.land() 82 | cube.position.y = above.last().object.position.y - 1 83 | } 84 | 85 | let hasOnRight = this.rayscanner.getIntersections(fromPosition, new THREE.Vector3(1, 0, 0)).any() 86 | if (hasOnRight) { 87 | this.controls.velocity.x = 0 88 | } 89 | if (this.controls.velocity.x > 0 && !hasOnRight) { 90 | cube.position.x += this.controls.velocity.x * tpf 91 | } 92 | 93 | let hasOnLeft = this.rayscanner.getIntersections(fromPosition, new THREE.Vector3(-1, 0, 0)).any() 94 | if (hasOnLeft) { 95 | this.controls.velocity.x = 0 96 | } 97 | if (this.controls.velocity.x < 0 && !hasOnLeft) { 98 | cube.position.x += this.controls.velocity.x * tpf 99 | } 100 | 101 | cube.position.y += scaledVelocityY 102 | 103 | if (cube.position.y < -10) { 104 | cube.position.set(0, 20, 0) 105 | } 106 | } 107 | 108 | doKeyboardEvent(event) { 109 | this.controls.doKeyboardEvent(event) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /workspace/games/test/platformer/Platformer.js: -------------------------------------------------------------------------------- 1 | Config.instance.engine.debug = true 2 | Config.instance.window.showStatsOnStart = true 3 | Config.instance.camera.fov = 100 4 | Config.instance.camera.type = 'ortographic' 5 | 6 | Engine.start(new MainScene()) 7 | -------------------------------------------------------------------------------- /workspace/games/test/platformer/StatsPanel.js: -------------------------------------------------------------------------------- 1 | class StatsPanel extends THREE.Object3D { 2 | constructor() { 3 | super() 4 | 5 | var text = new BaseText({ 6 | text: 'hello', fillStyle: 'blue', align: 'center', 7 | canvasW: 1024, canvasH: 1024, 8 | font: '128px luckiest-guy'}) 9 | text.position.set(0, 0, 4) 10 | this.text = text 11 | this.add(text) 12 | 13 | this.position.set(5, 5, 0) 14 | } 15 | 16 | setText(s) { 17 | this.text.setText(s.toFixed(2)) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /workspace/games/test/platformer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | platformer 10 | 14 | 15 | 16 | 17 | 18 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /workspace/games/test/scene-setup/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | scene setup 10 | 14 | 15 | 16 | 17 | 18 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /workspace/games/test/threejs_benchmark.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | three.js benchmark 7 | 8 | 9 | 10 | 11 | 12 | 54 | 55 | 56 | --------------------------------------------------------------------------------