├── .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('