Repository: joshmarinacci/voxeljs-next Branch: master Commit: 05514704fe10 Files: 66 Total size: 223.5 KB Directory structure: gitextract_x9k45x_d/ ├── .gitignore ├── .idea/ │ ├── .gitignore │ ├── misc.xml │ ├── modules.xml │ ├── vcs.xml │ └── voxeljs-next.iml ├── LICENSE ├── README.md ├── examples/ │ ├── css/ │ │ ├── dashboard.css │ │ ├── fullscreen.css │ │ ├── index.css │ │ ├── main.css │ │ └── webxr.css │ ├── package.json │ ├── public/ │ │ ├── ecsy.html │ │ ├── index.html │ │ └── networked.html │ ├── simple.html │ └── src/ │ ├── BlockPicker.js │ ├── ExplosionParticles.js │ ├── GPUParticleSystem.js │ ├── ItemManager.js │ ├── PersistenceManager.js │ ├── PigComp.js │ ├── PubnubNetworkplay.js │ ├── RemotePlayersProxy.js │ ├── SmashParticles.js │ ├── WebRTCAudioChat.js │ └── index.js ├── package.json ├── src/ │ ├── ChunkManager.js │ ├── CulledMesher.js │ ├── DesktopControls.js │ ├── ECSComp.js │ ├── FullscreenControls.js │ ├── GreedyMesher.js │ ├── KeyboardControls.js │ ├── PhysHandler.js │ ├── SimpleMeshCollider.js │ ├── TextureManager.js │ ├── TouchControls.js │ ├── VRControls.js │ ├── VRStats.js │ ├── VoxelMesh.js │ ├── VoxelTexture.js │ ├── ecsy/ │ │ ├── camera_gimbal.js │ │ ├── dashboard.js │ │ ├── fullscreen.js │ │ ├── highlight.js │ │ ├── index.js │ │ ├── input.js │ │ ├── keyboard.js │ │ ├── mouse.js │ │ ├── voxels.js │ │ └── webxr.js │ ├── index.js │ ├── physical.js │ ├── raycast.js │ ├── utils.js │ └── webxr-boilerplate/ │ ├── BackgroundAudioLoader.js │ ├── Pointer.js │ ├── WebXRBoilerPlate.js │ ├── raycaster.js │ ├── vrmanager.js │ └── vrstats.js └── webpack.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ dist package-lock.json # Created by .ignore support plugin (hsz.mobi) ### Node template # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # nyc test coverage .nyc_output # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # TypeScript v1 declaration files typings/ # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variables file .env # next.js build output .next ================================================ FILE: .idea/.gitignore ================================================ # Default ignored files /workspace.xml ================================================ FILE: .idea/misc.xml ================================================ ================================================ FILE: .idea/modules.xml ================================================ ================================================ FILE: .idea/vcs.xml ================================================ ================================================ FILE: .idea/voxeljs-next.iml ================================================ ================================================ FILE: LICENSE ================================================ BSD 3-Clause License Copyright (c) 2019, Josh Marinacci All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: README.md ================================================ # voxeljs-next The next generation of Voxel JS. # Play with it Try out a [live demo here](https://vr.josh.earth/voxeljs-next/examples/ecsy.html). This demo shows: * create a flat world * move with keyboard * add and remove blocks w/ mouse # What is it VoxelJS is a voxel engine for games, similar to Minecraft. It provides the ability to draw voxels on the screen, define the landscape with a function, load up textures, and navigate around the world in desktop mode and VR, and place/remove blocks. VoxelJS uses the voxel code from the [original Voxel.js project](http://www.voxeljs.com/), updated to the latest ThreeJS, adds WebXR support, and uses the new entity component system [ECSY](https://ecsy.io/); Notably VoxelJS does *not* provide any sort of server component, multi-player support, or scripting. To create interactive effects like a TNT block you would need to write that code yourself (examples coming soon).. # How to use it First install the dependencies using npm ```shell script npm install npm start open http://localhost:8080 ``` # old stuff This project updates the original [voxeljs](https://voxeljs.com/) with modern Javascript (classes, ES6 modules, arrow functions, etc), and brings it in line with the latest [ThreeJS](https://threejs.org/), with support for the latest WebGL features, and also adds VR/AR support. Because of these improvements, *voxeljs-next is not compatible with the original VoxelJS modules*. All old modules and features will need to be ported to the new code. # Dependencies VoxelJS is built on [ThreeJS](https://threejs.org/) and the WebGL & WebXR standards. With the right options enabled it will run on desktop in embedded mode, full screen with pointer lock, on a touch screen (phone, tablet), and in any VR headsets with a browser. # How to use VoxelJS Start by copying and modifying the main [example](examples/simple.html) application. ## Add new textures Textures are loaded by the TextureManager. Add new images to the `examples/textures` directory then add their file names (minus the .png extension) to the `load` call like this: ```javascript app.textureManager.load(['grass','brick','dirt','anim','tnt','arrows']) ``` ## create a custom world with code VoxelJS does not specify any on disk format for maps. It is up to you to provide that, though you can look at the PersistenceManager example to see how it could work. The only thing you need to provide is to provide a function to the ChunkManager which accepts a low and high dimension and coordinates. This function should generate a new info bject with the data for that chunk. If you already have a function which returns the block number at a particular spot in the chunk then you can use the utils.generateChunkInfoFromFunction to build the chunk info. Here's a simple example that creates a completely flat world 10 blocks thick. ```javascript const flat = (i,j,k) => { //the floor is brick, from depth 0 to -10 if(j < 1 && j > -10) return 2 //nothing else in the world return 0 } app.chunkManager = new ChunkManager({ generateVoxelChunk: (low, high, cx,cy,cz) => { const id = [cx,cy,cz].join('|') if(app.CHUNK_CACHE[id]) return app.CHUNK_CACHE[id] return generateChunkInfoFromFunction(low, high, flat) }, }); ``` ## entities Voxel only tracks movementt of the player. To add other autonomus entityes like enemies and friendlies create a new ThreeJS group for tthem in your scene then add the ThreeJS objects. To make the entity interact with blocks and other objects you'll need to use a `PhysHandler`. See the pig example for details ## Networking VoxelJS does not provide voice chat or networked play out of the box, but you can look at the example code for an example of using WebRTC for voice chat and PubNub for tracking player movement and terrain changes. # Help and Contributing The general algorithm for rendering voxel data and how our implementation works is documented in [this](https://blog.mozvr.com/voxeljs-chunking-magic/) blog. For help try asking in the `#voxel` channel in the ThreeJS slack. If you'd like to contribute take a look at the issues. There are a ton of features that need implementing including * make cheap ambient occulsion work when the greedy mesh is turned on * an api to set multiple chunks at once * better example of networked play * more demos * alternative rending. smaller cubes, cool textures, weird effects * Fix full screen in iOS and Mac safari * Touch screen dragging * Can’t choose block type in iPad * implement water: [How Water Works In DwarfCorp](https://www.gamasutra.com/blogs/MattKlingensmith/20130811/198050/How_Water_Works_In_DwarfCorp.php) * API to set multiple blocks at once. Batches help network as well. * other modules should Never know about chunks. Just get and set blocks. * level of detail: [A level of detail method for blocky voxels | 0 FPS](https://0fps.net/2018/03/03/a-level-of-detail-method-for-blocky-voxels/) * fix AO for greedy mesher. Explain the problem. These particular issues are newbie friendly. # TBD * explain how meshing and chunking works. the core algorithm * explain how rendering works. esp texture mapping. ================================================ FILE: examples/css/dashboard.css ================================================ /* dashboard.css */ div.dom-dashboard { border: 5px solid black; z-index: 10; position: fixed; top: 10vh; left: 10vw; right: 10vw; bottom: 10vh; background: white; opacity: 0.8; display: none; } div.dom-dashboard img { width: 10vw; image-rendering: crisp-edges; border: 5px solid black; padding: 5px; margin: 5px; background-color: white; } div.dom-dashboard.visible { display: block; } div.dom-dashboard button { position: absolute; bottom:0; right:0; font-size: 200%; } ================================================ FILE: examples/css/fullscreen.css ================================================ button.fullscreen { border: 5px solid red; position: fixed; left: 10vw; top: 10vh; font-size: 200%; } ================================================ FILE: examples/css/index.css ================================================ html, body { padding: 0; margin:0; overflow: hidden; } ================================================ FILE: examples/css/main.css ================================================ body { max-width: 40em; margin: auto; } #container { border: 3px solid black; width: 700px; height: 400px; } #fullscreen, #entervr { display: none; } #container { position: relative; } #touch-overlay { border: 0px solid black; position: absolute; bottom: 0; top:0; left:0; right:0; display: none; visibility: hidden; } #touch-overlay button { position: absolute; width: 15vmin; height: 15vmin; padding:0; border: 1px solid black; } #touch-overlay #left { bottom: 7.5vmin; left: 0vmin; } #touch-overlay #right { bottom: 7.5vmin; left: 30vmin; } #touch-overlay #up { bottom: 15vmin; left: 15vmin; } #touch-overlay #down { bottom: 0em; left: 15vmin; } #touch-overlay #exit-fullscreen { position: absolute; top: 0vmin; right: 0vmin; } #touch-overlay #menu-button { position: absolute; bottom: 0vmin; right: 0em; } #touch-overlay #jump-button { position: absolute; bottom: 15vmin; right: 0em; } body.fullscreen #container { border-width:0; position: absolute; top:0; left:0; width:100%; height:100%; } button { border: 1px solid black; border-radius: 0.25em; background-color: aqua; } button.selected { background-color: darkblue; color:white; } ================================================ FILE: examples/css/webxr.css ================================================ button.webxr { border: 5px solid red; position: fixed; left: 30vw; top: 10vh; font-size: 200%; } ================================================ FILE: examples/package.json ================================================ { "name": "examples", "version": "1.0.0", "main": "index.js", "private": true, "dependencies": { "voxeljs-next": ".." } } ================================================ FILE: examples/public/ecsy.html ================================================ Title ================================================ FILE: examples/public/index.html ================================================ ================================================ FILE: examples/public/networked.html ================================================ Mine Kitten

VoxelJS for VR Test

Press play full screen in desktop mode. Press play in vr to play in VR mode (if available). Or just play in regular windowed mode. Whatever you like.

================================================ FILE: examples/simple.html ================================================ Mine Kitten

VoxelJS for VR Test

Press play full screen in desktop mode. Press play in vr to play in VR mode (if available). Or just play in regular windowed mode. Whatever you like.

Textures CC0 licensed from Kenney.nl

================================================ FILE: examples/src/BlockPicker.js ================================================ import Panel2D from "threejs-2d-components/src/panel2d" import Label2D from "threejs-2d-components/src/label2d" import Button2D from "threejs-2d-components/src/button2d" const on = (elem, type, cb) => elem.addEventListener(type,cb) class BlockTypeButton extends Button2D { draw(ctx) { ctx.font = `${this.fsize}px sans-serif` const metrics = ctx.measureText(this.text) // this.w = 5 + metrics.width + 5 // this.h = 0 + this.fsize + 4 ctx.fillStyle = this.bg ctx.fillRect(this.x,this.y,this.w,this.h) ctx.fillStyle = 'black' ctx.fillText(this.text,this.x+3,this.y+this.fsize-2) ctx.strokeStyle = 'black' ctx.strokeRect(this.x,this.y,this.w,this.h) // const x = (i%4)*64 // const y = Math.floor((i/4))*64 ctx.fillStyle = 'red' ctx.fillRect(this.x,this.y,64,64) let info = this.info ctx.drawImage(this.owner.app.chunkManager.textureManager.canvas, info.x,info.y,info.w,info.h, this.x,this.y,64,64 ) if(this.owner.selectedColor === this.text) { ctx.lineWidth = 2; ctx.strokeStyle = 'black' ctx.strokeRect(this.x+2,this.y+2,64-4,64-4) ctx.strokeStyle = 'white' ctx.strokeRect(this.x+4,this.y+4,64-8,64-8) } } } export class BlockPicker { constructor(app) { this.app = app this.panel = new Panel2D(app.scene,app.camera, { draggable: false, width: 256, height: 256, }) this.app.scene.add(this.panel) this.selectedColor = 'none' } rebuild() { this.panel.removeAll() this.panel.add(new Label2D().set('text','block type').set('x',20).set('y',0)) const index = this.app.chunkManager.textureManager.getAtlasIndex() index.forEach((info,i) => { const button = new BlockTypeButton().setAll({ text:info.name, x:(i%4)*64, y:Math.floor(i/4)*64+40, w:64, h:64, owner:this, info:info, },info.name) on(button,'click',()=>{ console.log("selected block", info.name) const infos = this.app.chunkManager.textureManager.getAtlasIndex() if(infos[i]) { this.selectedColor = infos[i].name this.panel.redraw() } else { console.log("nothing selected") } }) this.panel.add(button) }) this.panel.add(new Button2D().setAll({ text:this.app.active?'creative':'active', x:10, y:225, w:40, h:40, }).on('click',()=>{ console.log('toggling creative mode') this.app.active = !this.app.active this.app.player_phys.endFlying() this.panel.visible = false })) this.panel.add(new Button2D().setAll({ text:'close', x: 190, y: 225, }).on('click',()=>{ this.panel.visible = false })) } setSelectedToDefault() { // const index = this.app.chunkManager.textureManager.getAtlasIndex() // this.selectedColor = index[0].name } /* redraw() { const ctx = this.canvas.getContext('2d') ctx.fillStyle = 'white' ctx.fillRect(0,0,this.canvas.width,this.canvas.height) const index = this.app.textureManager.getAtlasIndex() index.forEach((info,i) => { const x = (i%4)*64 const y = Math.floor((i/4))*64 ctx.fillStyle = 'red' ctx.fillRect(x,y,64,64) ctx.drawImage(this.app.textureManager.canvas, info.x,info.y,info.w,info.h, x,y,64,64 ) if(this.selectedColor === info.name) { ctx.lineWidth = 2; ctx.strokeStyle = 'black' ctx.strokeRect(x+2,y+2,64-4,64-4) ctx.strokeStyle = 'white' ctx.strokeRect(x+4,y+4,64-8,64-8) } }) } */ } ================================================ FILE: examples/src/ExplosionParticles.js ================================================ import {Vector3, Color, AdditiveBlending} from "three" import {ECSComp} from '../../src/ECSComp.js' import {GPUParticleSystem} from './GPUParticleSystem.js' import {rand} from "../../src/utils.js" export class ExplosionParticles extends ECSComp { constructor(app) { super() this.app = app this.startTime = -1 this.options = { position: new Vector3(0, 0, 0), positionRandomness: 0.0, velocity: new Vector3(0.0, 1.0, 0.0), velocityRandomness: 1.0, acceleration: new Vector3(0.0, 0.0, 0.0), color: new Color(1.0, 1.0, 1.0), endColor: new Color(0.5, 0.5, 0.5), colorRandomness: 0.0, lifetime: 1, fadeIn: 0.001, fadeOut: 0.001, size: 60, sizeRandomness: 1.0, } let scorch_texture = app.textureLoader.load('./textures/smoke_08.png') this.particles = new GPUParticleSystem({ maxParticles: 10000, particleSpriteTex: scorch_texture, blending: AdditiveBlending, onTick: (system, time) => { if (this.startTime === -1) this.startTime = time if (time < this.startTime + 0.2) { for (let i = 0; i < 100; i++) { this.options.velocity.set(rand(-5, 5), rand(-5, 5), rand(-5, 5)) system.spawnParticle(this.options); } } } }) app.playersGroup.add(this.particles) } fire(pos) { this.enable() this.particles.position.copy(pos) setTimeout(() => { this.disable() this.startTime = -1 }, 1500) } update(time, dt) { this.particles.update(time) } } ================================================ FILE: examples/src/GPUParticleSystem.js ================================================ import {Object3D, Vector3, Color, AdditiveBlending, RepeatWrapping, ShaderMaterial, BufferGeometry, BufferAttribute, Points } from "three" import {ECSComp} from '../../src/ECSComp.js' /* * modified from the version from the ThreeJS examples repo */ const vertexShader = ` uniform float uTime; uniform float uScale; uniform bool reverseTime; uniform float fadeIn; uniform float fadeOut; attribute vec3 positionStart; attribute float startTime; attribute vec3 velocity; attribute vec3 acceleration; attribute vec3 color; attribute vec3 endColor; attribute float size; attribute float lifeTime; varying vec4 vColor; varying vec4 vEndColor; varying float lifeLeft; varying float alpha; void main() { vColor = vec4( color, 1.0 ); vEndColor = vec4( endColor, 1.0); vec3 newPosition; float timeElapsed = uTime - startTime; if(reverseTime) timeElapsed = lifeTime - timeElapsed; if(timeElapsed < fadeIn) { alpha = timeElapsed/fadeIn; } if(timeElapsed >= fadeIn && timeElapsed <= (lifeTime - fadeOut)) { alpha = 1.0; } if(timeElapsed > (lifeTime - fadeOut)) { alpha = 1.0 - (timeElapsed - (lifeTime-fadeOut))/fadeOut; } lifeLeft = 1.0 - ( timeElapsed / lifeTime ); gl_PointSize = ( uScale * size );// * lifeLeft; newPosition = positionStart + (velocity * timeElapsed) + (acceleration * 0.5 * timeElapsed * timeElapsed) ; if (lifeLeft < 0.0) { lifeLeft = 0.0; gl_PointSize = 0.; } //while active use the new position if( timeElapsed > 0.0 ) { gl_Position = projectionMatrix * modelViewMatrix * vec4( newPosition, 1.0 ); } else { //if dead use the initial position and set point size to 0 gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); lifeLeft = 0.0; gl_PointSize = 0.; } } ` const fragmentShader = ` varying vec4 vColor; varying vec4 vEndColor; varying float lifeLeft; varying float alpha; uniform sampler2D tSprite; void main() { // color based on particle texture and the lifeLeft. // if lifeLeft is 0 then make invisible vec4 tex = texture2D( tSprite, gl_PointCoord ); vec4 color = mix(vColor, vEndColor, 1.0-lifeLeft); gl_FragColor = vec4( color.rgb*tex.rgb, alpha * tex.a); } ` const UPDATEABLE_ATTRIBUTES = [ 'positionStart', 'startTime', 'velocity', 'acceleration', 'color', 'endColor', 'size', 'lifeTime'] export class GPUParticleSystem extends Object3D { constructor(options) { super() options = options || {}; this.blending = options.blending? options.blending : NormalBlending this.PARTICLE_COUNT = options.maxParticles || 1000000; this.PARTICLE_CURSOR = 0; this.time = 0; this.offset = 0; this.count = 0; this.DPR = window.devicePixelRatio; this.particleUpdate = false; this.onTick = options.onTick this.reverseTime = options.reverseTime this.fadeIn = options.fadeIn || 1 if(this.fadeIn === 0) this.fadeIn = 0.001 this.fadeOut = options.fadeOut || 1 if(this.fadeOut === 0) this.fadeOut = 0.001 // preload a 10_000 random numbers from -0.5 to 0.5 this.rand = []; let i; for (i = 1e5; i>0; i--) { this.rand.push( Math.random() - 0.5 ); } this.i = i //setup the texture this.sprite = options.particleSpriteTex || null; if(!this.sprite) throw new Error("No particle sprite texture specified") this.sprite.wrapS = this.sprite.wrapT = RepeatWrapping; //setup the shader material this.material = new ShaderMaterial( { transparent: true, depthWrite: false, uniforms: { 'uTime': { value: 0.0 }, 'uScale': { value: 1.0 }, 'tSprite': { value: this.sprite }, reverseTime: { value: this.reverseTime }, fadeIn: { value: this.fadeIn }, fadeOut: { value: this.fadeOut, } }, blending: this.blending, vertexShader: vertexShader, fragmentShader: fragmentShader } ); // define defaults for all values this.material.defaultAttributeValues.particlePositionsStartTime = [ 0, 0, 0, 0 ]; this.material.defaultAttributeValues.particleVelColSizeLife = [ 0, 0, 0, 0 ]; // geometry this.geometry = new BufferGeometry(); //vec3 attributes this.geometry.addAttribute('position', new BufferAttribute(new Float32Array(this.PARTICLE_COUNT * 3), 3).setDynamic(true)); this.geometry.addAttribute('positionStart', new BufferAttribute(new Float32Array(this.PARTICLE_COUNT * 3), 3).setDynamic(true)); this.geometry.addAttribute('velocity', new BufferAttribute(new Float32Array(this.PARTICLE_COUNT * 3), 3).setDynamic(true)); this.geometry.addAttribute('acceleration', new BufferAttribute(new Float32Array(this.PARTICLE_COUNT * 3), 3).setDynamic(true)); this.geometry.addAttribute('color', new BufferAttribute(new Float32Array(this.PARTICLE_COUNT * 3), 3).setDynamic(true)); this.geometry.addAttribute('endColor', new BufferAttribute(new Float32Array(this.PARTICLE_COUNT * 3), 3).setDynamic(true)); //scalar attributes this.geometry.addAttribute('startTime', new BufferAttribute(new Float32Array(this.PARTICLE_COUNT), 1).setDynamic(true)); this.geometry.addAttribute('size', new BufferAttribute(new Float32Array(this.PARTICLE_COUNT), 1).setDynamic(true)); this.geometry.addAttribute('lifeTime', new BufferAttribute(new Float32Array(this.PARTICLE_COUNT), 1).setDynamic(true)); this.particleSystem = new Points(this.geometry, this.material); this.particleSystem.frustumCulled = false; this.add(this.particleSystem); } /* This updates the geometry on the shader if at least one particle has been spawned. It uses the offset and the count to determine which part of the data needs to actually be sent to the GPU. This ensures no more data than necessary is sent. */ geometryUpdate () { if (this.particleUpdate === true) { this.particleUpdate = false; UPDATEABLE_ATTRIBUTES.forEach(name => { const attr = this.geometry.getAttribute(name) if (this.offset + this.count < this.PARTICLE_COUNT) { attr.updateRange.offset = this.offset * attr.itemSize attr.updateRange.count = this.count * attr.itemSize } else { attr.updateRange.offset = 0 attr.updateRange.count = -1 } attr.needsUpdate = true }) this.offset = 0; this.count = 0; } } //use one of the random numbers random () { return ++ this.i >= this.rand.length ? this.rand[ this.i = 1 ] : this.rand[ this.i ]; } update ( ttime ) { this.time = ttime/1000 this.material.uniforms.uTime.value = this.time; if(this.onTick) this.onTick(this,this.time) this.geometryUpdate(); } dispose () { this.material.dispose(); this.sprite.dispose(); this.geometry.dispose(); } /* spawn a particle This works by updating values inside of the attribute arrays, then updates the count and the PARTICLE_CURSOR and sets particleUpdate to true. This if spawnParticle is called three times in a row before rendering, then count will be 3 and the cursor will have moved by three. */ spawnParticle ( options ) { let position = new Vector3() let velocity = new Vector3() let acceleration = new Vector3() let color = new Color() let endColor = new Color() const positionStartAttribute = this.geometry.getAttribute('positionStart') const startTimeAttribute = this.geometry.getAttribute('startTime') const velocityAttribute = this.geometry.getAttribute('velocity') const accelerationAttribute = this.geometry.getAttribute('acceleration') const colorAttribute = this.geometry.getAttribute('color') const endcolorAttribute = this.geometry.getAttribute('endColor') const sizeAttribute = this.geometry.getAttribute('size') const lifeTimeAttribute = this.geometry.getAttribute('lifeTime') options = options || {}; // setup reasonable default values for all arguments position = options.position !== undefined ? position.copy(options.position) : position.set(0, 0, 0); velocity = options.velocity !== undefined ? velocity.copy(options.velocity) : velocity.set(0, 0, 0); acceleration = options.acceleration !== undefined ? acceleration.copy(options.acceleration) : acceleration.set(0, 0, 0); color = options.color !== undefined ? color.copy(options.color) : color.set(0xffffff); endColor = options.endColor !== undefined ? endColor.copy(options.endColor) : endColor.copy(color) const lifetime = options.lifetime !== undefined ? options.lifetime : 5 let size = options.size !== undefined ? options.size : 10 const sizeRandomness = options.sizeRandomness !== undefined ? options.sizeRandomness : 0 if (this.DPR !== undefined) size *= this.DPR; const i = this.PARTICLE_CURSOR // position positionStartAttribute.array[i * 3 + 0] = position.x positionStartAttribute.array[i * 3 + 1] = position.y positionStartAttribute.array[i * 3 + 2] = position.z velocityAttribute.array[i * 3 + 0] = velocity.x; velocityAttribute.array[i * 3 + 1] = velocity.y; velocityAttribute.array[i * 3 + 2] = velocity.z; accelerationAttribute.array[i * 3 + 0] = acceleration.x; accelerationAttribute.array[i * 3 + 1] = acceleration.y; accelerationAttribute.array[i * 3 + 2] = acceleration.z; colorAttribute.array[i * 3 + 0] = color.r; colorAttribute.array[i * 3 + 1] = color.g; colorAttribute.array[i * 3 + 2] = color.b; endcolorAttribute.array[i * 3 + 0] = endColor.r; endcolorAttribute.array[i * 3 + 1] = endColor.g; endcolorAttribute.array[i * 3 + 2] = endColor.b; //size, lifetime and starttime sizeAttribute.array[i] = size + this.random() * sizeRandomness; lifeTimeAttribute.array[i] = lifetime; startTimeAttribute.array[i] = this.time + this.random() * 2e-2; // offset if (this.offset === 0) this.offset = this.PARTICLE_CURSOR; // counter and cursor this.count++; this.PARTICLE_CURSOR++; //wrap the cursor around if (this.PARTICLE_CURSOR >= this.PARTICLE_COUNT) this.PARTICLE_CURSOR = 0; this.particleUpdate = true; }; } ================================================ FILE: examples/src/ItemManager.js ================================================ import {Vector3,} from "three" import {ECSComp} from '../../src/ECSComp.js' export class ItemManager extends ECSComp { constructor(app) { super() this.app = app } isBlockTypeItem(type) { if(type === 5) return true return false } removeItem(pos) { const type = this.app.chunkManager.voxelAtCoordinates(pos) const radius = 3; if(type === 5) { //the type code for TNT console.log("triggering TNT explosion") const cursor = new Vector3() const actual = new Vector3() for(let x=-radius; x<+radius; x++) { cursor.x = x for(let y=-radius; y<=+radius; y++) { cursor.y = y for(let z = -radius; z<=+radius; z++) { cursor.z = z if(cursor.length() res.json()) // .then(res => { // return res // const blocks = game.blockService.loadFromJSON(res) // blocks.forEach(b => { // on(b.getObject3D(), 'click', blockClicked) // }) // dataChanger.fire('changed',{}) // }) } function POST_JSON(url,data) { console.log("posting to",url) return fetch(url, { method: 'POST', mode: 'cors', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }) .then(resp => resp.json()) .then(resp => { console.log("real response is",resp) return resp }) } function loadImageFromURL(url) { return new Promise((res,rej)=>{ const img = new Image() img.addEventListener('load',()=>{ res(img) }) img.src = url }) } const BASE_URL = "https://vr.josh.earth/360/doc/" export class PersistenceManager { constructor() { } save(chunkManager, cache) { const chunkCount = Object.keys(chunkManager.chunks).length const width = 512 const height = 1024 const canvas = document.createElement('canvas') canvas.width = width canvas.height = height const ctx = canvas.getContext('2d') ctx.fillStyle = 'rgba(255,255,255,1.0)' ctx.fillRect(0,0,canvas.width,canvas.height) const data = ctx.getImageData(0,0,canvas.width,canvas.height) console.log("saving",Object.keys(chunkManager.chunks).length,'chunks') const output = { chunks:[], image:null, } Object.keys(chunkManager.chunks).forEach((id,i)=> { const chunk = chunkManager.chunks[id] const info = { id: id, low: chunk.data.low, high: chunk.data.high, dims: chunk.data.dims, position: chunk.chunkPosition, } //turn a 4096 array into an 8x512 section of the image for(let k=0; k { // console.log("parsing",data) chunkManager.clear() return loadImageFromURL(data.image).then(img => { const canvas = document.createElement('canvas') canvas.width = img.width canvas.height = img.height const ctx = canvas.getContext('2d') ctx.drawImage(img,0,0) // document.body.appendChild(canvas) data.chunks.forEach(chunk => { const imageData = ctx.getImageData(chunk.imageCoords.x,chunk.imageCoords.y, chunk.imageCoords.width, chunk.imageCoords.height) const voxels = [] for(let i=0; i<4096; i++) voxels[i] = imageData.data[i*4+2] chunkManager.makeChunkFromData(chunk,voxels) }) }) }) } } ================================================ FILE: examples/src/PigComp.js ================================================ import {Vector3, Mesh, MeshLambertMaterial, BoxBufferGeometry, Box3} from "three" import {ECSComp} from "../../src/ECSComp.js" import {traceRay} from '../../src/raycast.js' import {PhysHandler} from '../../src/PhysHandler.js' import {checkHitTileY, checkHitTileX, checkHitTileZ} from "../../src/SimpleMeshCollider.js" export class PigComp extends ECSComp { constructor(app) { super() this.app = app this.mesh = new Mesh( new BoxBufferGeometry(1,1,1), new MeshLambertMaterial({color:'pink', map:app.textureLoader.load('./textures/pig.png')}) ) this.heading = new Vector3(2,0,-1).normalize() this.mesh.position.set(-8,1.5,2) this.velocity = 0.005 this.app.mobs.add(this.mesh) this.physics = new PhysHandler(this.app,this.mesh,[this]) } update(time,dt) { this.physics.update(dt) } collide(phys, target, pos, diff) { // phys.vel.y = 0//0.01 if(!this.app.active) { //don't do any physics when the world is paused. just let the user move around // phys.vel.y = 0 return } const bounds = new Box3(new Vector3(0,-1.5,0),new Vector3(1,0,1)) // console.log("pos",pos.y) const size = new Vector3() bounds.getSize(size) // console.log("size",size) const rpos = pos.clone() pos.y -= 1.5 //while on the floor, no y velocity if(checkHitTileY(this.app.chunkManager,bounds,pos)) { phys.vel.y = 0 } //check forwards if(checkHitTileX(this.app.chunkManager,bounds,pos)) { this.heading.x = Math.random()-0.5 this.heading.z = Math.random()-0.5 this.heading.normalize() } if(checkHitTileZ(this.app.chunkManager,bounds,pos)) { this.heading.x = Math.random()-0.5 this.heading.z = Math.random()-0.5 this.heading.normalize() } if(pos.y < -1000) { console.log("pig is dead") this.disable() } //set vel to the heading phys.vel.z = this.heading.z phys.vel.x = this.heading.x } } ================================================ FILE: examples/src/PubnubNetworkplay.js ================================================ import {Vector3,} from "three" import {ECSComp} from '../../src/ECSComp.js' const pubkey = 'pub-c-0a0c49cb-8e11-4b10-8347-3af6cf048b46'; const subkey = 'sub-c-1cf05cbc-4d88-11e9-82b8-86fda2e42ae9'; const CHANNEL = 'beta-movement' const SEND_INTERVAL = 333; export class PubnubNetworkplay extends ECSComp { constructor() { super() this.connecting = false this.connected = false this.lastPos = new Vector3(-100,-100,-100) this.currPos = new Vector3() this.voxels = [] this.pubnub = new PubNub({ publishKey: pubkey, subscribeKey: subkey, }) this.pubnub.setUUID('voxeluser_'+Math.floor(Math.random()*1000000)) this.pubnub.addListener({ status:(e) => { console.log("PUBNUB status",e) if(e.category === 'PNConnectedCategory') { this.connected = true } }, message: (msg) =>{ if(msg.publisher !== this.pubnub.getUUID()) { // console.log("someone else moved",msg.publisher) // console.log("PUBNUB message",msg) if(msg.message.type === 'movement') this._fire('remote-player-moved',msg) if(msg.message.type === 'voxels') this._fire('remote-player-voxels',msg) } else { // console.log("it's me") } }, }) this.sendUpdates = () => { if(!this.lastPos.equals(this.currPos)) { // console.log("sending updates", this.currPos) this.lastPos.copy(this.currPos) this.pubnub.publish({ channel:CHANNEL, message: { type:'movement', position: { x:this.currPos.x, y:this.currPos.y, z:this.currPos.z } } },(status,response)=>{ if(status.error) console.log("PUBNUB error?",status) }) } if(this.voxels.length > 0) { const voxels = this.voxels.slice() // console.log("sending changed voxels:",voxels.length) this.voxels = [] this.pubnub.publish({ channel: CHANNEL, message: { type:'voxels', voxels:voxels } }, (status,response) => { if(status.error) console.log("PUBNUB error?",status) }) } } } connect() { console.log("PUBNUB subscribing") this.connecting = true this.pubnub.subscribe({channels:[CHANNEL]}) setInterval(this.sendUpdates,SEND_INTERVAL) } isConnected() { return this.connected } isConnecting() { return this.connecting } playerMoved(phys) { this.currPos.copy(phys.target.position) } playerSetVoxel(pos,type) { this.voxels.push({ type:type, position: { x:pos.x, y:pos.y, z:pos.z } }) } } ================================================ FILE: examples/src/RemotePlayersProxy.js ================================================ import {Vector3, Mesh, MeshLambertMaterial, SphereBufferGeometry} from "three" import {ECSComp} from '../../src/ECSComp.js' const BUMP_HEIGHT = new Vector3(0,1,0) export class RemotePlayersProxy extends ECSComp { constructor(app) { super() this.app = app; this.players = {} } remotePlayerMoved(id,pos) { if(!this.players[id]) { console.log("a new player joined!") this.players[id] = new Mesh(new SphereBufferGeometry(1), new MeshLambertMaterial({color:'aqua'})) this.app.playersGroup.add(this.players[id]) } this.players[id].position.copy(pos).add(BUMP_HEIGHT) console.log("remote player moved to",pos) } } ================================================ FILE: examples/src/SmashParticles.js ================================================ import {Vector3, Color, AdditiveBlending} from "three" import {ECSComp} from '../../src/ECSComp.js' import {GPUParticleSystem} from './GPUParticleSystem.js' import {rand} from "../../src/utils.js" export class SmashParticles extends ECSComp { constructor(app) { super() this.app = app this.startTime = -1 this.options = { position: new Vector3(0, 0, 0), positionRandomness: 0.0, velocity: new Vector3(0.0, 1.0, 0.0), velocityRandomness: 1.0, acceleration: new Vector3(0.0, 0.0, 0.0), color: new Color(1.0, 0.0, 0.0), endColor: new Color(0.5, 0.5, 0.5), colorRandomness: 0.0, lifetime: 0.20, fadeIn: 0.000, fadeOut: 0.000, size: 20, sizeRandomness: 1.0, } let scorch_texture = app.textureLoader.load('./textures/smoke_08.png') this.particles = new GPUParticleSystem({ maxParticles: 10000, particleSpriteTex: scorch_texture, blending: AdditiveBlending, onTick: (system, time) => { if (this.startTime === -1) this.startTime = time if (time < this.startTime + 0.05) { for (let i = 0; i < 100; i++) { this.options.velocity.set(rand(-5, 5), rand(-5, 5), rand(-5, 5)) system.spawnParticle(this.options); } } } }) app.playersGroup.add(this.particles) } fire(pos) { this.enable() this.particles.position.copy(pos) this.particles.position.add(new Vector3(0.5, 0.5, 0.5)) setTimeout(() => { this.disable() this.startTime = -1 }, 250) } update(time, dt) { this.particles.update(time) } } ================================================ FILE: examples/src/WebRTCAudioChat.js ================================================ // https://github.com/stephenlb/webrtc-sdk const pubkey = 'pub-c-0a0c49cb-8e11-4b10-8347-3af6cf048b46'; const subkey = 'sub-c-1cf05cbc-4d88-11e9-82b8-86fda2e42ae9'; const number = 'testnum1' export class WebRTCAudioChat { constructor(app) { this.connected = false } connect() { this.phone = PHONE({ number : number , publish_key : pubkey , subscribe_key : subkey , media: {audio:true} }); this.phone.debug( info => console.info('PHONE',info) ); // Debugging Output // As soon as the phone is ready we can make calls this.phone.ready(()=>{ console.log("PHONE_READY: system is connected!") this.connected = true // let session = phone.dial(number); }); // When Call Comes In this.phone.receive((session) => { console.log("a phone call came in") // Display Your Friend's Live Video session.connected( session => { console.log('Session: CONNECTED'); // phone.$('video-out').appendChild(session.video); }); session.ended( session => { this.connected = false console.log('Session: ENDED') } ); }); } disconnect() { console.log("DISCONNECTING") this.phone.hangup() } } ================================================ FILE: examples/src/index.js ================================================ import '../css/fullscreen.css' import '../css/webxr.css'; import '../css/dashboard.css'; import '../css/index.css'; import {Group, Vector3, TextureLoader, CubeGeometry, MeshLambertMaterial, Mesh, AmbientLight, } from 'three'; import { Component, System, World } from 'ecsy'; import { initialize, Parent, Transform, Object3D, } from 'ecsy-three'; import {MouseCursor, MouseSystem, KeyboardBindingSet, KeyboardSystem, VoxelLandscape, VoxelSystem, VoxelTextures, ActiveBlock, Highlight, HighlightSystem, StagePosition, StageRotation, WebXRSystem, WebXRButton, WebXRController, FullscreenSystem, FullscreenButton, DashboardDOMOvleraySystem, DomDashboard, DashboardVisible, InputFrame, VoxelPlayerSystem, } from 'voxeljs-next' class VoxelWebXRControllerSystem extends System { execute(delta, time) { this.queries.controllers.added.forEach(ent => { let con = ent.getComponent(WebXRController); let mesh2 = new Mesh( new CubeGeometry(1.1,0.1,0.1), new MeshLambertMaterial({ color:'yellow', })); ent.addComponent(Transform) ent.addComponent(Object3D, {value: mesh2}) ent.addComponent(Parent, con.controller) }) this.queries.controllers.results.forEach(ent => { let con = ent.getComponent(WebXRController) if(con.selected) { console.log("xr is pressed ", con.index) } }) } } VoxelWebXRControllerSystem.queries = { controllers: { components:[WebXRController], listen: { added:true, removed:true, } }, } // Create a new world to hold all our highlights and systems let world = new World(); // Register all of the systems we will need world.registerSystem(VoxelSystem) world.registerSystem(KeyboardSystem) world.registerSystem(MouseSystem) world.registerSystem(HighlightSystem) world.registerSystem(DashboardDOMOvleraySystem) world.registerSystem(WebXRSystem); world.registerSystem(FullscreenSystem); world.registerSystem(VoxelPlayerSystem) // Initialize the default sets of highlights and systems let data = initialize(world); let {scene, renderer, camera} = data.entities; console.log("got it",data) // Modify the position for the default camera // let transform = camera.getMutableComponent(Transform); // transform.position.z = 5; scene.addComponent(FullscreenButton) scene.addComponent(WebXRButton) // one InputFrame is required for all inputs to work scene.addComponent(InputFrame) // the binding keys match dom keyboard events world.createEntity() .addComponent(KeyboardBindingSet, {bindings: { 'a': InputFrame.LEFT_STRAFE, 'ArrowLeft': InputFrame.LEFT_STRAFE, 'd': InputFrame.RIGHT_STRAFE, 'ArrowRight': InputFrame.RIGHT_STRAFE, 'w': InputFrame.MOVE_FORWARD, 's': InputFrame.MOVE_BACKWARD, 'ArrowUp': InputFrame.MOVE_FORWARD, 'ArrowDown': InputFrame.MOVE_BACKWARD, 'e': InputFrame.OPEN_DASHBOARD, 't': InputFrame.LEVITATE_UP, 'g': InputFrame.LEVITATE_DOWN, }}) new TextureLoader().load('./dummy.jpg') // add a dashboard to the scene scene.addComponent(DomDashboard) //set the active block to type 3 (TNT) scene.addComponent(ActiveBlock, {type:3}) // a pivot for rotating the world around let stageRot = world.createEntity() .addComponent(Object3D, {value: new Group()}) .addComponent(Transform) .addComponent(Parent, {value: scene}) .addComponent(StageRotation) // StageRotation is how the rest of the system can use this // a position for moving the world around let stagePos = world.createEntity() .addComponent(Object3D, {value: new Group}) .addComponent(Transform) .addComponent(Parent, {value:stageRot}) .addComponent(StagePosition) // StagePosition //make the actual landscape world.createEntity() .addComponent(Transform) .addComponent(Parent, {value: stagePos}) .addComponent(VoxelLandscape, { make_voxel: (x,y,z) => { // make a floor between -2 and -5 if(y < -2 && y > -5) return 1 // grass // make a 4x4x4 cube floating in space if( x > 0 && x < 5 && z > 5 && z < 10 && y > 5 && y < 10 ) return 2 // brick return 0 } , }) .addComponent(VoxelTextures,{ textures:[ { src:'./textures/dirt.png' }, { src:'./textures/grass.png' }, { src:'./textures/brick.png' }, { src:'./textures/tnt.png' }, { src:'./textures/heart.png', }, ]}) world.execute(); // create a mouse cursor so that we can look for mouse events world.createEntity() .addComponent(MouseCursor) //create a ThreeJS mesh as the highlighter let mesh = new Mesh( new CubeGeometry(1.1,1.1,1.1, 4,4,4).translate(0.5,0.5,0.5), new MeshLambertMaterial({ color:'green', depthTest:true, wireframe:true, wireframeLinewidth: 3, transparent: true, opacity: 0.5, })); // make the highlighter let highlight = world.createEntity() .addComponent(Transform) .addComponent(Object3D, { value: mesh}) .addComponent(Parent, {value: stagePos}) .addComponent(Highlight) //add some ambient light or the highlight mesh won't have any color world.createEntity() .addComponent(Object3D, { value: new AmbientLight()}) .addComponent(Parent, {value: scene}) ================================================ FILE: package.json ================================================ { "name": "voxeljs-next", "version": "0.0.13", "description": "The next generation of Voxel JS.", "main": "dist/voxeljs-next.js", "module": "src/index.js", "scripts": { "prepublish": "npm run build", "start": "webpack-dev-server --mode development", "nuke": "rm package-lock.json && rm -rf dist && rm -rf node_modules && rm -rf examples/node_modules", "build": "cd examples && npm i && cd .. && LIBRARY=true webpack" }, "repository": { "type": "git", "url": "git+https://github.com/joshmarinacci/voxeljs-next.git" }, "author": "Josh Marinacci", "license": "BSD3", "bugs": { "url": "https://github.com/joshmarinacci/voxeljs-next/issues" }, "files": [ "src", "dist/voxeljs-next.js", "dist/voxeljs-next.js.map" ], "homepage": "https://github.com/joshmarinacci/voxeljs-next#readme", "peerDependencies": { "three": "^0.116.1", "ecsy": "^0.2.3", "ecsy-three": "^0.1.0" }, "devDependencies": { "clean-webpack-plugin": "^3.0.0", "copy-webpack-plugin": "^5.1.1", "css-loader": "^3.5.3", "ecsy": "^0.2.3", "ecsy-three": "^0.1.0", "html-webpack-plugin": "^4.3.0", "style-loader": "^1.2.1", "three": "^0.116.1", "webpack": "^4.43.0", "webpack-cli": "^3.3.11", "webpack-dev-server": "^3.10.3" }, "dependencies": {} } ================================================ FILE: src/ChunkManager.js ================================================ import {Vector3,} from "three" import {CulledMesher} from "./CulledMesher.js" import {VoxelMesh} from "./VoxelMesh.js" class Chunk { constructor(data, pos, chunkBits) { this.data = data this.dims = data.dims this.voxels = data.voxels this.vmesh = null this.surfaceMesh = null this.realPosition = pos this.chunkPosition = [pos.x, pos.y, pos.z] this.id = this.chunkPosition.join('|') this.chunkBits = chunkBits } voxelIndexFromCoordinates(x, y, z) { const bits = this.chunkBits const mask = (1 << bits) - 1 return (x & mask) + ((y & mask) << bits) + ((z & mask) << bits * 2) } voxelAtCoordinates(pt) { const vidx = this.voxelIndexFromCoordinates(pt.x, pt.y, pt.z) return this.voxels[vidx] } setVoxelAtCoordinates(pt, val) { const vidx = this.voxelIndexFromCoordinates(pt.x, pt.y, pt.z) const v = this.voxels[vidx] this.voxels[vidx] = val return v } dispose() { if (this.vmesh) { delete this.vmesh.data delete this.vmesh.geometry delete this.vmesh.meshed delete this.vmesh.surfaceMesh } } } const SCALE = new Vector3(1.0,1.0,1.0) export class ChunkManager { constructor(opts) { this.listeners = {} this.container = opts.container this.distance = opts.chunkDistance || 2 this.chunkSize = opts.chunkSize || 32 this.blockSize = opts.blockSize || 1 this.generateVoxelChunk = opts.generateVoxelChunk this.chunks = {} this.mesher = opts.mesher || new CulledMesher() this.textureManager = opts.textureManager if (this.chunkSize & this.chunkSize - 1 !== 0) throw new Error('chunkSize must be a power of 2') if (!this.textureManager) throw new Error("missing texture manager") //TODO: count the number of bits wide the chunksize is. seems like we could just use Math.log() //ex: if chunksize is 16 the bits is 4 //I think bits is just used for efficient multiplication and division. let bits = 0 for (let size = this.chunkSize; size > 0; size >>= 1) bits++; this.chunkBits = bits - 1; this.CHUNK_CACHE = {} } on(type, cb) { if(!this.listeners[type]) this.listeners[type] = [] this.listeners[type].push(cb) } emit(type,evt) { if(!this.listeners[type]) this.listeners[type] = [] this.listeners[type].forEach(cb => cb(evt)) } clear() { Object.keys(this.chunks).forEach(key => { const chunk = this.chunks[key] this.container.remove(chunk.surfaceMesh) chunk.surfaceMesh.geometry.dispose() this.CHUNK_CACHE[chunk.id] = chunk.data chunk.dispose() }) this.chunks = {} } // position in chunk indexes? nearbyChunks(position, distance) { const current = this.chunkAtPosition(position) const x = current[0] const y = current[1] const z = current[2] const dist = distance || this.distance const nearby = [] for (let cx = (x - dist); cx !== (x + dist); ++cx) { for (let cy = (y - dist); cy !== (y + dist); ++cy) { for (let cz = (z - dist); cz !== (z + dist); ++cz) { nearby.push([cx, cy, cz]) } } } return nearby } //get missing chunks. position is in world coords requestMissingChunks(pos) { this.nearbyChunks(pos).map((chunkIndex) => { if (!this.chunks[chunkIndex.join('|')]) { this.rebuildMesh(this.generateChunk(new Vector3(chunkIndex[0],chunkIndex[1],chunkIndex[2]))) } }) } getBounds(x, y, z) { const bits = this.chunkBits const low = [x << bits, y << bits, z << bits] const high = [(x + 1) << bits, (y + 1) << bits, (z + 1) << bits] return [low, high] } //make a chunk at the position in chunk coords generateChunk(pos) { const bounds = this.getBounds(pos.x, pos.y, pos.z) const id = [pos.x,pos.y,pos.z].join('|') let chunkData if(this.CHUNK_CACHE[id]) { chunkData = this.CHUNK_CACHE[id] } else { chunkData = this.generateVoxelChunk(bounds[0], bounds[1], pos) } const chunkObj = new Chunk(chunkData, pos, this.chunkBits) this.chunks[chunkObj.id] = chunkObj return chunkObj } makeChunkFromData(info,voxels) { const pos = new Vector3(info.position[0],info.position[1],info.position[2]) const chunkData = { low:info.low, high:info.high, voxels:voxels, dims:info.dims, } const chunk = new Chunk(chunkData, pos, this.chunkBits) this.chunks[chunk.id] = chunk return chunk } chunkIndexAtCoordinates(x, y, z) { const bits = this.chunkBits const cx = x >> bits const cy = y >> bits const cz = z >> bits return [cx, cy, cz]; } //position in world coords chunkAtPosition(position) { const pt = position.divideScalar(this.blockSize).floor() return this.chunkIndexAtCoordinates(pt.x, pt.y, pt.z) } voxelIndexFromCoordinates(x, y, z) { const bits = this.chunkBits const mask = (1 << bits) - 1 return (x & mask) + ((y & mask) << bits) + ((z & mask) << bits * 2) } //get voxel at point in world space voxelAtCoordinates(pt) { const ckey = this.chunkIndexAtCoordinates(pt.x, pt.y, pt.z).join('|') const chunk = this.chunks[ckey] if (!chunk) return false return chunk.voxelAtCoordinates(pt) } setVoxelAtCoordinates(pt, val) { const ckey = this.chunkIndexAtCoordinates(pt.x, pt.y, pt.z).join('|') const chunk = this.chunks[ckey] if (!chunk) return false const ret = chunk.setVoxelAtCoordinates(pt,val) this.rebuildMesh(chunk) return ret } setBlockRange(pos, dim, data) { pos.floor() const ckey = this.chunkIndexAtCoordinates(pos.x, pos.y, pos.z).join('|') const chunk = this.chunks[ckey] const pt = pos.clone() if(chunk) { for(let y=0; y chunkPos.join('|')) Object.keys(this.chunks).map((chunkIndex) => { //skip the nearby chunks if (nearbyChunks.indexOf(chunkIndex) > -1) return const chunk = this.chunks[chunkIndex] if (!chunk) return this.container.remove(chunk.surfaceMesh) chunk.surfaceMesh.geometry.dispose() this.CHUNK_CACHE[chunk.id] = chunk.data chunk.dispose() delete this.chunks[chunkIndex] }) } getBlock(x,y,z) { return this.voxelAtPosition(new Vector3(x,y,z)) } rebuildMesh(chunk) { if(chunk.surfaceMesh) this.container.remove(chunk.surfaceMesh) chunk.surfaceMesh = new VoxelMesh(chunk, this.mesher, SCALE, this) .createSurfaceMesh(this.textureManager.material) this.container.add(chunk.surfaceMesh) const pos = chunk.realPosition.clone().multiplyScalar(this.chunkSize) chunk.surfaceMesh.position.copy(pos) } rebuildAllMeshes() { Object.keys(this.chunks).forEach(key => this.rebuildMesh(this.chunks[key])) } updateCenterPosition(pos) { this.requestMissingChunks(pos) // and remove the chunks that might be out of range now this.removeFarChunks(pos, this.container) } } ================================================ FILE: src/CulledMesher.js ================================================ export class CulledMesher { constructor() { } //Naive meshing (with face culling) mesh(volume, dims) { //Precalculate direction vectors for convenience var dir = new Array(3) for (var i = 0; i < 3; ++i) { dir[i] = [[0, 0, 0], [0, 0, 0]] dir[i][0][(i + 1) % 3] = 1 dir[i][1][(i + 2) % 3] = 1 } //March over the volume var vertices = [] , faces = [] , x = [0, 0, 0] , B = [[false, true] //Incrementally update bounds (this is a bit ugly) , [false, true] , [false, true]] , n = -dims[0] * dims[1] for (B[2] = [false, true], x[2] = -1; x[2] < dims[2]; B[2] = [true, (++x[2] < dims[2] - 1)]) for (n -= dims[0], B[1] = [false, true], x[1] = -1; x[1] < dims[1]; B[1] = [true, (++x[1] < dims[1] - 1)]) for (n -= 1, B[0] = [false, true], x[0] = -1; x[0] < dims[0]; B[0] = [true, (++x[0] < dims[0] - 1)], ++n) { //Read current voxel and 3 neighboring voxels using bounds check results var p = (B[0][0] && B[1][0] && B[2][0]) ? volume[n] : 0 , b = [(B[0][1] && B[1][0] && B[2][0]) ? volume[n + 1] : 0 , (B[0][0] && B[1][1] && B[2][0]) ? volume[n + dims[0]] : 0 , (B[0][0] && B[1][0] && B[2][1]) ? volume[n + dims[0] * dims[1]] : 0 ] //Generate faces for (var d = 0; d < 3; ++d) if ((!!p) !== (!!b[d])) { var s = !p ? 1 : 0 var t = [x[0], x[1], x[2]] , u = dir[d][s] , v = dir[d][s ^ 1] ++t[d] var vertex_count = vertices.length vertices.push([t[0], t[1], t[2]]) vertices.push([t[0] + u[0], t[1] + u[1], t[2] + u[2]]) vertices.push([t[0] + u[0] + v[0], t[1] + u[1] + v[1], t[2] + u[2] + v[2]]) vertices.push([t[0] + v[0], t[1] + v[1], t[2] + v[2]]) faces.push([vertex_count, vertex_count + 1, vertex_count + 2, vertex_count + 3, s ? b[d] : p]) } } return {vertices: vertices, faces: faces} } } // if(exports) { // exports.mesher = CulledMesh; // } ================================================ FILE: src/DesktopControls.js ================================================ import {Vector2} from "three" import {Pointer} from "./webxr-boilerplate/Pointer" import {ECSComp} from './ECSComp.js' import {traceRayAtScreenCoords} from './utils.js' const LEFT_MOUSE_BUTTON = 1 const RIGHT_MOUSE_BUTTON = 2 export class DesktopControls extends ECSComp { constructor(app, distance) { super() this.app = app this.distance = distance this.canvas = this.app.renderer.domElement this.canvas.addEventListener('contextmenu',e => { e.preventDefault() e.stopPropagation() }) this.canvas.addEventListener('mousemove',e => { if(!this.isEnabled()) return const pt = new Vector2(e.clientX,e.clientY) const res = traceRayAtScreenCoords(this.app,pt, this.distance) res.hitPosition.floor() this._fire('highlight',res.hitPosition) }) this.canvas.addEventListener('mousedown',e => { if(!this.isEnabled()) return const pt = new Vector2(e.clientX,e.clientY) if(e.buttons === LEFT_MOUSE_BUTTON) { const res = traceRayAtScreenCoords(this.app, pt, this.distance) res.hitPosition.add(res.hitNormal) this._fire('setblock',res.hitPosition) } if(e.buttons === RIGHT_MOUSE_BUTTON) { const res = traceRayAtScreenCoords(this.app, pt, this.distance) this._fire('removeblock',res.hitPosition) } }) this.canvas.addEventListener('mouseup',e => { }) this.pointer = new Pointer(app,{ //don't intersect with anything. only use for orientation and trigger state intersectionFilter: o => o.userData.clickable, enableLaser: false, mouseSimulatesController:false, }) // this.pointer.disable() } enable() { super.enable() // this.pointer.enable() } disable() { super.disable() // this.pointer.disable() } update(time) { this.pointer.tick(time) } } ================================================ FILE: src/ECSComp.js ================================================ export class ECSComp { constructor() { this._listeners = {} this._enabled = false } addEventListener(type, cb) { if(!this._listeners[type]) this._listeners[type] = [] this._listeners[type].push(cb) } _fire(type,payload) { if(!this._listeners[type]) this._listeners[type] = [] this._listeners[type].forEach(cb => cb(payload)) } enable() { this._enabled = true } disable() { this._enabled = false } isEnabled() { return this._enabled } update(time) { } } ================================================ FILE: src/FullscreenControls.js ================================================ import {Ray, Vector3,} from "three" import {traceRay} from './raycast.js' import {ECSComp} from './ECSComp.js' import {toRad, EPSILON} from "./utils.js" const HAS_POINTER_LOCK = 'pointerLockElement' in document || 'mozPointerLockElement' in document || 'webkitPointerLockElement' in document; function requestPointerLock(el) { if(el.requestPointerLock) return el.requestPointerLock() console.log("request pointer lock not found") } export class FullScreenControls extends ECSComp { constructor(app) { super() this.app = app this.changeCallback = () => { if(document.pointerLockElement) { // console.log("entered pointer lock") } else { // console.log("exited pointer lock") this.disable() } } this.moveCallback = (e) => { if(!this.isEnabled()) return this.app.stageRot.rotation.y += e.movementX/300 this.app.stageRot.rotation.y += e.movementX/300 if(e.movementY) { this.app.stageRot.rotation.x += e.movementY/500 this.app.stageRot.rotation.x = Math.max(this.app.stageRot.rotation.x,toRad(-60)) this.app.stageRot.rotation.x = Math.min(this.app.stageRot.rotation.x,toRad(60)) } const res = this.traceRay() res.hitPosition.floor() this._fire('highlight',res.hitPosition) } this.mousedownCallback = (e) => { if(!this.isEnabled()) return e.preventDefault() const LEFT_MOUSE_BUTTON = 1 const RIGHT_MOUSE_BUTTON = 2 if(e.buttons === LEFT_MOUSE_BUTTON) { const res = this.traceRay() res.hitPosition.add(res.hitNormal) this._fire('setblock',res.hitPosition) } if(e.buttons === RIGHT_MOUSE_BUTTON) { const res = this.traceRay() this._fire('removeblock',res.hitPosition) } } this.errorCallback = (e) => { console.log("error getting pointer lock",e) } this.contextmenuCallback = (e) => { e.preventDefault() e.stopPropagation() } } traceRay() { const target = new Vector3(0,1.6,-1) this.app.stagePos.worldToLocal(target) const pos = new Vector3(0,1.6,0) this.app.stagePos.worldToLocal(pos) const ray = new Ray(pos) ray.lookAt(target) const hitNormal = new Vector3(0,0,0) const hitPosition = new Vector3(0,0,0) const hitBlock = traceRay(this.app.chunkManager,ray.origin,ray.direction,this.distance,hitPosition,hitNormal,EPSILON) return { hitBlock:hitBlock, hitPosition:hitPosition, hitNormal: hitNormal } } enable() { super.enable() if(HAS_POINTER_LOCK) { // console.log("we have pointer lock") document.addEventListener('pointerlockchange',this.changeCallback,false) document.addEventListener('mousemove',this.moveCallback,false) document.addEventListener('pointerlockerror', this.errorCallback, false); document.addEventListener('mousedown',this.mousedownCallback,false) document.addEventListener('contextmenu',this.contextmenuCallback,false) requestPointerLock(this.app.renderer.domElement) } } disable() { if(!this.isEnabled()) return //don't recurse if already disabled super.disable() document.removeEventListener('pointerlockchange', this.changeCallback, false) document.removeEventListener('mousemove', this.moveCallback, false) document.removeEventListener('pointerlockerror', this.errorCallback, false); document.removeEventListener('contextmenu',this.contextmenuCallback,false) this._fire('exit', this) } } ================================================ FILE: src/GreedyMesher.js ================================================ let mask = new Int32Array(4096); export class GreedyMesher { constructor() { } mesh(volume, dims) { var vertices = [], faces = [] , dimsX = dims[0] , dimsY = dims[1] , dimsXY = dimsX * dimsY; //Sweep over 3-axes for (var d = 0; d < 3; ++d) { var i, j, k, l, w, W, h, n, c , u = (d + 1) % 3 , v = (d + 2) % 3 , x = [0, 0, 0] , q = [0, 0, 0] , du = [0, 0, 0] , dv = [0, 0, 0] , dimsD = dims[d] , dimsU = dims[u] , dimsV = dims[v] , qdimsX, qdimsXY , xd if (mask.length < dimsU * dimsV) { mask = new Int32Array(dimsU * dimsV); } q[d] = 1; x[d] = -1; qdimsX = dimsX * q[1] qdimsXY = dimsXY * q[2] // Compute mask while (x[d] < dimsD) { xd = x[d] n = 0; for (x[v] = 0; x[v] < dimsV; ++x[v]) { for (x[u] = 0; x[u] < dimsU; ++x[u], ++n) { var a = xd >= 0 && volume[x[0] + dimsX * x[1] + dimsXY * x[2]] , b = xd < dimsD - 1 && volume[x[0] + q[0] + dimsX * x[1] + qdimsX + dimsXY * x[2] + qdimsXY] if (a ? b : !b) { mask[n] = 0; continue; } mask[n] = a ? a : -b; } } ++x[d]; // Generate mesh for mask using lexicographic ordering n = 0; for (j = 0; j < dimsV; ++j) { for (i = 0; i < dimsU;) { c = mask[n]; if (!c) { i++; n++; continue; } //Compute width w = 1; while (c === mask[n + w] && i + w < dimsU) w++; //Compute height (this is slightly awkward) for (h = 1; j + h < dimsV; ++h) { k = 0; while (k < w && c === mask[n + k + h * dimsU]) k++ if (k < w) break; } // Add quad // The du/dv arrays are reused/reset // for each iteration. du[d] = 0; dv[d] = 0; x[u] = i; x[v] = j; if (c > 0) { dv[v] = h; dv[u] = 0; du[u] = w; du[v] = 0; } else { c = -c; du[v] = h; du[u] = 0; dv[u] = w; dv[v] = 0; } var vertex_count = vertices.length; vertices.push([x[0], x[1], x[2]]); vertices.push([x[0] + du[0], x[1] + du[1], x[2] + du[2]]); vertices.push([x[0] + du[0] + dv[0], x[1] + du[1] + dv[1], x[2] + du[2] + dv[2]]); vertices.push([x[0] + dv[0], x[1] + dv[1], x[2] + dv[2]]); faces.push([vertex_count, vertex_count + 1, vertex_count + 2, vertex_count + 3, c]); //Zero-out mask W = n + w; for (l = 0; l < h; ++l) { for (k = n; k < W; ++k) { mask[k + l * dimsU] = 0; } } //Increment counters and continue i += w; n += w; } } } } return {vertices: vertices, faces: faces}; } } ================================================ FILE: src/KeyboardControls.js ================================================ import {Vector3,} from "three" import {ECSComp} from './ECSComp.js' const toRad = (deg) => Math.PI / 180 * deg const Y_AXIS = new Vector3(0,1,0) const SPEED = 0.1 export class KeyboardControls extends ECSComp { constructor(app) { super() this.app = app this.keystates = { ArrowLeft:{current:false, previous:false}, ArrowRight:{current:false, previous:false}, ArrowUp:{current:false, previous:false}, ArrowDown:{current:false, previous:false}, a: { current: false, previous: false}, d: { current: false, previous: false}, s: { current: false, previous: false}, w: { current: false, previous: false}, q: { current: false, previous: false}, e: { current: false, previous: false}, Enter: { current: false, previous: false}, c: { current: false, previous: false}, } this.keystates[' '] = { current: false, previous: false} this._keydown_handler = (e)=>{ if(!this.isEnabled()) return if(this.keystates[e.key]) { this.keystates[e.key].current = true } } this._keyup_handler = (e)=>{ if(!this.isEnabled()) return if(this.keystates[e.key]) { this.keystates[e.key].current = false } } document.addEventListener('keydown',this._keydown_handler) document.addEventListener('keyup',this._keyup_handler) } update(time) { if(this.keystates.ArrowUp.current === true) this.glideForward() if(this.keystates.ArrowDown.current === true) this.glideBackward() if(this.keystates.ArrowLeft.current === true) this.rotateLeft() if(this.keystates.ArrowRight.current === true) this.rotateRight() if(this.keystates.a.current === true) this.glideLeft() if(this.keystates.d.current === true) this.glideRight() if(this.keystates.w.current === true) this.glideForward() if(this.keystates.s.current === true) this.glideBackward() if(this.keystates.q.current === true) this.glideDown() if(this.keystates.e.current === true) this.glideUp() if(this.keystates[' '].current === true) this.app.player_phys.startJump() if(this.keystates[' '].current === false && this.keystates[' '].previous === true) this.app.player_phys.endJump() if(this.keystates.Enter.current === false && this.keystates.Enter.previous === true) { this._fire('show-dialog',this) } if(this.keystates.c.current === true && this.keystates.c.previous === false) { this.app.active = !this.app.active this.app.player_phys.endFlying() } Object.keys(this.keystates).forEach(key => { this.keystates[key].previous = this.keystates[key].current }) } rotateLeft() { this.app.stageRot.rotation.y -= toRad(3) } rotateRight() { this.app.stageRot.rotation.y += toRad(3) } getSpeedDirection() { const dir = new Vector3(0,0,1) dir.applyAxisAngle(Y_AXIS, -this.app.stageRot.rotation.y) return dir.normalize().multiplyScalar(SPEED) } glideForward() { const vel = this.getSpeedDirection().multiplyScalar(-40) this.app.player_phys.vel.x = vel.x this.app.player_phys.vel.z = vel.z this.app.player_phys.markChanged() } glideBackward() { const vel = this.getSpeedDirection().multiplyScalar(40) this.app.player_phys.vel.x = vel.x this.app.player_phys.vel.z = vel.z this.app.player_phys.markChanged() } glideLeft() { const vel = this.getSpeedDirection().multiplyScalar(40).applyAxisAngle(Y_AXIS,toRad(-90)) this.app.player_phys.vel.x = vel.x this.app.player_phys.vel.z = vel.z this.app.player_phys.markChanged() } glideRight() { const vel = this.getSpeedDirection().multiplyScalar(40).applyAxisAngle(Y_AXIS,toRad(90)) this.app.player_phys.vel.x = vel.x this.app.player_phys.vel.z = vel.z this.app.player_phys.markChanged() } glideUp() { if(!this.app.player_phys.isFlying()) { this.app.player_phys.startFlying() } this.app.player_phys.vel.y = 0.1 this.app.player_phys.markChanged() } glideDown() { if(!this.app.player_phys.isFlying()) { this.app.player_phys.startFlying() } this.app.player_phys.vel.y = -0.1 this.app.player_phys.markChanged() } } ================================================ FILE: src/PhysHandler.js ================================================ import {Vector3,} from "three" import {ECSComp} from './ECSComp.js' const GRAVITY = new Vector3(0,-9.8,0) export class PhysHandler extends ECSComp { constructor(app, target, colliders) { super() this.app = app this.target = target this.colliders = colliders this.vel = new Vector3(0,0,0) this.flying = false this.jumping = false } isFlying() { return this.flying } startFlying() { this.flying = true } endFlying() { this.flying = false } startJump() { if(!this.jumping) { this.jumping = true this.flying = false this.jumpTime = Date.now() } } endJump() { this.jumping = false } markChanged() { this._fire('move',{position:this.target.position}) } update(time,dt) { dt = dt/1000 // const dt = (time/1000) // console.log("tick",dt) if(!this.flying && this.app.active) { let acc = GRAVITY.y * 0.5 this.vel.y += acc * dt } // console.log("now",this.vel.y) const pos = this.target.position.clone() pos.y += this.vel.y*dt pos.z += this.vel.z*dt pos.x += this.vel.x*dt // console.log(this.vel) const diff = new Vector3() diff.y = this.vel.y*dt diff.x = this.vel.x*dt diff.z = this.vel.z*dt this.colliders.forEach(col => { col.collide(this,this.target,pos,diff) }) //apply final velocity this.target.position.y += this.vel.y this.target.position.z += this.vel.z*dt this.target.position.x += this.vel.x*dt //apply some friction this.vel.z *= 0.8 this.vel.x *= 0.8 if(this.flying) { this.vel.y *= 0.8 } // console.log(this.vel.y) this.markChanged() } } ================================================ FILE: src/SimpleMeshCollider.js ================================================ import {Vector3, Box3} from "three" const SIZE = new Vector3() const CENTER = new Vector3() export function checkHitTileY(voxels, bounds, pos) { bounds.getSize(SIZE) // console.log("checking for hits near height",SIZE.y) //scan in the y direction only for now. let sy = Math.floor(pos.y) let ey = Math.ceil(pos.y+SIZE.y) let sz = Math.floor(pos.z) let sx = Math.floor(pos.x) let ex = Math.ceil(pos.x+SIZE.x) for(let i=sy; i<=ey; i++) { // for(let j=sx; j 0) return true // } } return false } export function checkHitTileX(voxels, bounds, pos) { bounds.getCenter(CENTER) let sx = Math.floor(pos.x-CENTER.x) let ex = Math.ceil(pos.x+CENTER.x) let sy = Math.round(pos.y+1) let sz = Math.floor(pos.z) for(let j=sx; j 0) return true } return false } export function checkHitTileZ(voxels, bounds, pos) { bounds.getCenter(CENTER) let sx = Math.floor(pos.x) let sy = Math.round(pos.y+1) let sz = Math.floor(pos.z-CENTER.z) let ez = Math.ceil(pos.z+CENTER.z) for(let k=sz; k 0) return true } return false } export class SimpleMeshCollider { constructor(app) { this.app = app } collide(phys, target, pos, diff) { //pos is the potential position. we can choose to veto it this.app.stagePos.position.y = -target.position.y this.app.stagePos.position.z = -target.position.z this.app.stagePos.position.x = -target.position.x if(!this.app.active) { //don't do any physics when the world is paused. just let the user move around // phys.vel.y = 0 return } //check if too far beyond terminal velocity if(phys.vel.y < -1) phys.vel.y = -1 const bounds = new Box3(new Vector3(0,0,0),new Vector3(1,1,1)) //check downwards if(checkHitTileY(this.app.chunkManager,bounds,pos)) { if(!phys.isFlying()) { phys.vel.y = 0 } } //check forwards if(checkHitTileX(this.app.chunkManager,bounds,pos)) { // console.log("hit something to the left or right") phys.vel.x = 0 } if(checkHitTileZ(this.app.chunkManager,bounds,pos)) { // console.log("hit something to the front or back") phys.vel.z = 0 } //if fell off the world if(pos.y < -30) { console.log("fell off the world") phys.vel.y = 0 target.position.y = 10 target.position.x = 0 target.position.z = 0 } if(phys.jumping) { const diff = Date.now() - phys.jumpTime; if(diff > 300) { // console.log("over one second") } else { phys.vel.y = 1.0; } } } } ================================================ FILE: src/TextureManager.js ================================================ import { LinearMipMapLinearFilter, NearestFilter, ShaderMaterial, Texture, VertexColors } from "three" // const createAtlas = window.atlaspack /* * get what I have working w/o the atlas function * switch to 17 x 17 to address lines * manually create mip-maps as additional smaller textures * check out sample3D texture polyfill */ export class TextureManager { constructor(opts) { this.canvas = document.createElement('canvas') this.canvas.setAttribute('id','texture') // document.getElementsByTagName('body')[0].appendChild(this.canvas) this.aoEnabled = opts.aoEnabled || false this.canvas.width = 128; this.canvas.height = 128; this.canvas.style.width = '512px'; this.canvas.style.height = '512px'; this.tiles = [] // this.atlas = createAtlas(this.canvas); // this.atlas.tilepad = true // this will cost 8x texture memory. this.animated = {} const ctx = this.canvas.getContext('2d') this.texturesEnabled = true ctx.fillStyle = 'red' ctx.fillRect(0,0,this.canvas.width,this.canvas.height) this.texture = new Texture(this.canvas); this.texture.needsUpdate = true this.texture.magFilter = NearestFilter; this.texture.minFilter = NearestFilter; this.texturePath = './textures/'; this.material = new ShaderMaterial( { uniforms: { 'uTime': { value: 0.0 }, textureSamp: { value: this.texture}, texturesEnabled: { value: this.texturesEnabled }, }, vertexColors:VertexColors, vertexShader: ` attribute vec2 repeat; attribute vec4 subrect; attribute float frameCount; attribute float occlusion; varying vec2 vUv; varying vec2 vRepeat; varying vec4 vSubrect; varying float vFrameCount; varying float vOcclusion; void main() { vUv = uv; vSubrect = subrect; vRepeat = repeat; vFrameCount = frameCount; vOcclusion = occlusion; vec4 mvPosition = modelViewMatrix * vec4(position,1.0); gl_Position = projectionMatrix * mvPosition; } `, fragmentShader: ` uniform sampler2D textureSamp; uniform float uTime; uniform bool texturesEnabled; varying vec2 vUv; varying vec2 vRepeat; varying vec4 vSubrect; varying float vFrameCount; varying float vOcclusion; void main() { vec2 fuv = vUv; vec4 sr = vSubrect; //sr.z = sub rect width //sr.w = sub rect height float frameCount = 3.0; // float cframe = mod(uTime,frameCount); float cframe = mod(uTime,vFrameCount); float cframe2 = floor(cframe); sr.x = sr.x + cframe2*sr.z; fuv.x = sr.x + fract(vUv.x*vRepeat.x)*sr.z; fuv.y = sr.y + fract(vUv.y*vRepeat.y)*sr.w; vec4 color = vec4(1.0,1.0,1.0,1.0); if(texturesEnabled) { color = texture2D(textureSamp, fuv); } color = color*(vOcclusion); gl_FragColor = vec4(color.xyz,1.0); } `, } ); } packImage(img,index) { const info = { index:index, image:img, x:0, y:0, w:16, h:16, } info.x = (info.index*16)%128 + (info.index)*2 + 1 info.y = Math.floor(info.index/8)*16 + 1 const ctx = this.canvas.getContext('2d') ctx.imageSmoothingEnabled = false //draw image center ctx.drawImage(img,info.x,info.y, info.w, info.h) //left edge ctx.drawImage(img, 0,0,1,info.h, info.x-1,info.y,1,info.h) //right edge ctx.drawImage(img, info.w-1,0,1,info.h, info.x+info.w,info.y,1,info.h) //top edge ctx.drawImage(img, 0,0,info.w,1, info.x,info.y-1,info.w,1) ctx.drawImage(img, 0,info.h-1,info.w,1, info.x,info.y+info.h,info.w,1) ctx.fillStyle = 'yellow' // ctx.fillRect(info.x,info.y,info.w,info.h) this.texture.needsUpdate = true return info } isEnabled() { return true } update(ttime) { const time = ttime/1000 this.material.uniforms.uTime.value = time; this.material.uniforms.texturesEnabled.value = this.texturesEnabled } lookupUVsForBlockType(typeNum) { const info = this.tiles[typeNum] if(!info) { const x = 0 / 8.0 const x2 = 1 / 8.0 const y = 0 const y2 = 1 / 8.0 return [[x, y], [x2, y], [x2, y2], [x, y2]] } // console.log(x) // console.log("looking up type number",typeNum,info) const x = info.x/128 const y = info.y/128 const x2 = (info.x+info.w)/128 const y2 = (info.y+info.h)/128 return [[x,y],[x2,y],[x2,y2],[x,y2]] /* return [ [info.x/128,info.y/128], [info.x/128,(info.y+info.h)/128], [(info.x+info.w)/128,(info.y)/128], [(info.x+info.w)/128,(info.y+info.h)/128], ] */ // const uvs = this.atlas.uv()[this.names[typeNum-1]] // if(!uvs) return [[0,0],[0,1],[1,1],[1,0]] // return [[0.0,0],[0.0,1],[0,1],[1,0]] // return uvs } lookupInfoForBlockType(typeNum) { return { animated:false } } getBlockTypeForName(name) { return this.names.findIndex(n => n===name)+1 } loadTextures(infos) { const proms = infos.map((info,index) => { console.log("loading",info.src) return new Promise((res,rej)=>{ const img = new Image() img.id = info.src img.src = info.src img.onload = () => { res(this.packImage(img,index)) } img.onerror = (e) => { console.error(`Couldn't load texture from url ${infos.src}`) rej(e) } }) }) return Promise.all(proms).then((infos)=>{ this.tiles = infos this.texture.needsUpdate = true }) } } function ext(name) { return (String(name).indexOf('.') !== -1) ? name : name + '.png'; } ================================================ FILE: src/TouchControls.js ================================================ import {Vector2, Vector3,} from "three" import {ECSComp} from './ECSComp.js' import {$, DIRS, on, toRad, traceRayAtScreenCoords} from './utils.js' const Y_AXIS = new Vector3(0,1,0) const SPEED = 0.1 export class TouchControls extends ECSComp { isTouchEnabled() { return ('ontouchstart' in document.documentElement) } constructor(app, distance, chunkManager) { super() this.app = app this.canvas = this.app.container this.distance = distance this.chunkManager = chunkManager this.dir_button = 'none' let point = new Vector2() let startAngleY = 0 let startAngleX = 0 let startTime = 0 let timeoutID let intervalID let mode = 'node' let currentPoint= new Vector2() this.touchStart = (e) => { e.preventDefault() startAngleY = this.app.stageRot.rotation.y startAngleX = this.app.stageRot.rotation.x if(e.changedTouches.length <= 0) return const tch = e.changedTouches[0] point.set(tch.clientX, tch.clientY) currentPoint.copy(point) startTime = Date.now() const res = traceRayAtScreenCoords(this.app,point, this.distance) res.hitPosition.add(res.hitNormal) res.hitPosition.floor() this._fire('highlight',res) timeoutID = setTimeout(this.startRemoval,1000) } this.startRemoval = () => { mode = 'remove' const res = traceRayAtScreenCoords(this.app,currentPoint, this.distance) res.hitPosition.floor() this._fire('highlight',res) this._fire('removeblock',res.hitPosition) intervalID = setInterval(this.removeAgain,500) } this.removeAgain = () => { const res = traceRayAtScreenCoords(this.app, currentPoint, this.distance) res.hitPosition.floor() this._fire('highlight',res) this._fire('removeblock',res.hitPosition) } this.touchMove = (e) => { e.preventDefault() if(e.changedTouches.length <= 0) return const tch = e.changedTouches[0] const pt2 = new Vector2(tch.clientX, tch.clientY) const diffx = pt2.x - point.x const diffy = pt2.y - point.y this.app.stageRot.rotation.y = +diffx/150 + startAngleY this.app.stageRot.rotation.x = +diffy/200 + startAngleX currentPoint.copy(pt2) const res = traceRayAtScreenCoords(this.app, pt2, this.distance) if(mode === 'add') { res.hitPosition.add(res.hitNormal) } res.hitPosition.floor() this._fire('highlight',res) if(this.mode === 'remove') { this._fire('removeblock',res.hitPosition) } } this.touchEnd = (e) => { e.preventDefault() clearTimeout(timeoutID) clearInterval(intervalID) mode = 'node' if(e.changedTouches.length <= 0) return const tch = e.changedTouches[0] const pt2 = new Vector2(tch.clientX, tch.clientY) const endTime = Date.now() if(point.distanceTo(pt2) < 10) { const res = traceRayAtScreenCoords(this.app, pt2, this.distance) if(endTime - startTime > 500) { this._fire('removeblock',res.hitPosition) } else { res.hitPosition.add(res.hitNormal) this._fire('setblock', res.hitPosition) } } } this.attachButton = (b,dir) => { on(b,'touchstart',e => { e.preventDefault() e.stopPropagation() this.dir_button = dir }) on(b,'touchmove',e => { e.preventDefault() e.stopPropagation() }) on(b,'touchend',e => { e.preventDefault() e.stopPropagation() this.dir_button = DIRS.NONE }) on(b,'mousedown',e => { e.preventDefault() this.dir_button = dir }) on(b,'mouseup',e => { e.preventDefault() this.dir_button = DIRS.NONE }) } this.attachButton ($("#left"),DIRS.LEFT) this.attachButton ($("#right"),DIRS.RIGHT) this.attachButton ($("#up"),DIRS.UP) this.attachButton ($("#down"),DIRS.DOWN) const overlay = $("#touch-overlay") const menuButton = document.createElement('button') menuButton.id = 'menu-button' overlay.appendChild(menuButton) menuButton.innerText = 'Menu' function setupTouchButton(sel,cb) { on(sel,'touchstart',e => { e.preventDefault() e.stopPropagation() }) on(sel,'touchmove',e => { e.preventDefault() e.stopPropagation() }) on(sel,'touchend',e => { e.preventDefault() e.stopPropagation() cb() }) on(sel,'mousedown',e => { e.preventDefault() e.stopPropagation() }) on(sel, 'mouseup', e => { e.preventDefault() e.stopPropagation() cb() }) } setupTouchButton(menuButton,()=>this._fire('show-dialog',this)) const exitButton = document.createElement('button') overlay.appendChild(exitButton) exitButton.innerText = 'Exit' exitButton.id = "exit-fullscreen" setupTouchButton(exitButton, ()=>this.app.exitFullscreen()) } update() { if(this.dir_button === DIRS.LEFT) this.glideLeft() if(this.dir_button === DIRS.RIGHT) this.glideRight() if(this.dir_button === DIRS.UP) this.glideForward() if(this.dir_button === DIRS.DOWN) this.glideBackward() } enable() { super.enable() $("#touch-overlay").style.display = 'block' this.canvas.addEventListener('touchstart',this.touchStart,false) this.canvas.addEventListener('touchmove',this.touchMove,false) this.canvas.addEventListener('touchend',this.touchEnd,false) } disable() { if(!this.isEnabled()) return //don't recurse if already disabled super.disable() $("#touch-overlay").style.display = 'none' this.canvas.removeEventListener('touchstart',this.touchStart) this.canvas.removeEventListener('touchmove',this.touchMove) this.canvas.removeEventListener('touchend',this.touchEnd) } glideForward() { const vel = this.getSpeedDirection().multiplyScalar(-40) this.app.player_phys.vel.x = vel.x this.app.player_phys.vel.z = vel.z this.app.player_phys.markChanged() } glideBackward() { const vel = this.getSpeedDirection().multiplyScalar(40) this.app.player_phys.vel.x = vel.x this.app.player_phys.vel.z = vel.z this.app.player_phys.markChanged() } getSpeedDirection() { const dir = new Vector3(0,0,1) dir.applyAxisAngle(Y_AXIS, -this.app.stageRot.rotation.y) return dir.normalize().multiplyScalar(SPEED) } glideLeft() { const vel = this.getSpeedDirection().multiplyScalar(40).applyAxisAngle(Y_AXIS,toRad(-90)) this.app.player_phys.vel.x = vel.x this.app.player_phys.vel.z = vel.z this.app.player_phys.markChanged() } glideRight() { // this.app.stagePos.position.add(this.getSpeedDirection().applyAxisAngle(Y_AXIS,toRad(-90))) const vel = this.getSpeedDirection().multiplyScalar(40).applyAxisAngle(Y_AXIS,toRad(90)) this.app.player_phys.vel.x = vel.x this.app.player_phys.vel.z = vel.z this.app.player_phys.markChanged() } } ================================================ FILE: src/VRControls.js ================================================ import {Vector3,} from "three" import {Pointer, POINTER_CLICK} from "./webxr-boilerplate/Pointer" import {traceRay} from "./raycast.js" import {ECSComp} from './ECSComp.js' import {DIRS, toRad} from "./utils.js" const Y_AXIS = new Vector3(0,1,0) const SPEED = 0.1 const TRIGGER = 'trigger' export class VRControls extends ECSComp { constructor(app) { super() this.app = app this.distance = 30 this.states = { touchpad: false} this.pointer = new Pointer(app,{ //don't intersect with anything. only use for orientation and trigger state intersectionFilter: o => false, enableLaser: true, mouseSimulatesController:false, }) this.pointer.on(POINTER_CLICK, () => { if(!this.isEnabled()) return const res = this.traceRay() this._fire(TRIGGER,res) }) // this.activeDir = DIRS.NONE } traceRay() { const direction = new Vector3(0, 0, -1) direction.applyQuaternion(this.pointer.controller1.quaternion) direction.applyAxisAngle(Y_AXIS,-this.app.stageRot.rotation.y) const pos = this.app.stagePos.worldToLocal(this.pointer.controller1.position.clone()) const epilson = 1e-8 const hitNormal = new Vector3(0,0,0) const hitPosition = new Vector3(0,0,0) const hitBlock = traceRay(this.app.chunkManager,pos,direction,this.distance,hitPosition,hitNormal,epilson) return { hitBlock:hitBlock, hitPosition:hitPosition, hitNormal: hitNormal } } rotateLeft() { this.app.stageRot.rotation.y -= toRad(30) } rotateRight() { this.app.stageRot.rotation.y += toRad(30) } getSpeedDirection() { const direction = new Vector3(0, 0, 1) //apply the controller rotation to it direction.applyQuaternion(this.pointer.controller1.quaternion) //apply the stage rotation to it direction.applyAxisAngle(Y_AXIS,-this.app.stageRot.rotation.y) return direction.normalize().multiplyScalar(SPEED) } glideBackward() { this.app.stagePos.position.add(this.getSpeedDirection().multiplyScalar(-1)) } glideForward() { this.app.stagePos.position.add(this.getSpeedDirection()) } update(time) { this.scanGamepads(time) this.updateCursor(time) } updateCursor(time) { this.pointer.tick(time) const res = this.traceRay() res.hitPosition.floor() this._fire('highlight',res) } scanGamepads(time) { const gamepads = navigator.getGamepads() for(let i=0; i +0.5) newDir = DIRS.DOWN if(gamepad.axes[0] < -0.5) newDir = DIRS.LEFT if(gamepad.axes[0] > +0.5) newDir = DIRS.RIGHT let activeDir = this.states[gamepad.id] if(!activeDir) activeDir = DIRS.NONE if(activeDir === DIRS.NONE && newDir !== DIRS.NONE) { if(newDir === DIRS.LEFT) { this._fire('toggle-pointer',this) } if(newDir === DIRS.RIGHT) { this._fire('show-dialog',this) } } if(newDir === DIRS.UP) { this.glideForward() } if(newDir === DIRS.DOWN) { this.glideBackward() } activeDir = newDir this.states[gamepad.id] = activeDir // console.log("prev dir",this.activeDir) //on click start // if(/*touchpad.pressed === true &&*/ this.states.touchpad === false) { /* if(gamepad.axes && gamepad.axes.length === 2) { // this.activeDir = DIRS.NONE if(gamepad.axes[1] < -0.2) this.activeDir = DIRS.UP if(gamepad.axes[1] > +0.4) this.activeDir = DIRS.DOWN if(this.activeDir === DIRS.NONE) { if(gamepad.axes[0] < -0.5) this.activeDir = DIRS.LEFT if(gamepad.axes[0] > +0.5) this.activeDir = DIRS.RIGHT } }*/ // } // console.log('new dir', this.activeDir) /* //on click end //left and right clicks if(touchpad.pressed === false && this.states.touchpad === true) { if(this.activeDir === DIRS.LEFT) { // console.log("left click") this._fire('toggle-pointer',this) } if(this.activeDir === DIRS.RIGHT) { // console.log("right click") this._fire('show-dialog',this) } } //movement if(touchpad.pressed) { if(this.activeDir === DIRS.UP) { // console.log("moving", this.activeDir) this.glideForward() } if(this.activeDir === DIRS.DOWN) { // console.log("moving", this.activeDir) this.glideBackward() } }*/ /* //swipe detection if(!touchpad.pressed && gamepad.axes[0] < -0.5) { if(!this.startRight) { this.startLeft = true this.timeStart = time } const diff = time - this.timeStart if(this.startRight && diff < 250) { // console.log('swiped left') this.rotateRight() } this.startRight = false } //swipe detection if(!touchpad.pressed && gamepad.axes[0] > +0.5) { if(!this.startLeft) { this.startRight = true this.timeStart = time } const diff = time - this.timeStart if(this.startLeft && diff < 250) { // console.log('swiped right') this.rotateLeft() } this.startLeft = false } if(!touchpad.pressed && gamepad.axes[0] === 0 && gamepad.axes[1] === 0) { this.startLeft = false this.startRight = false } */ // this.states.touchpad = touchpad.pressed } } ================================================ FILE: src/VRStats.js ================================================ import {Vector3, Mesh, MeshLambertMaterial, BoxBufferGeometry, CanvasTexture, PlaneGeometry, MeshBasicMaterial, } from "three" import {ECSComp} from "./ECSComp.js" export default class VRStats extends ECSComp { constructor(app) { super(); this.app = app // this.renderer = renderer const can = document.createElement('canvas') can.width = 256 can.height = 128 this.canvas = can const c = can.getContext('2d') c.fillStyle = '#00ffff' c.fillRect(0,0,can.width,can.height) const ctex = new CanvasTexture(can) const mesh = new Mesh( new PlaneGeometry(1,0.5), new MeshBasicMaterial({map:ctex}) ) mesh.position.z = -3 mesh.position.y = 2.5 mesh.material.depthTest = false mesh.material.depthWrite = false mesh.renderOrder = 1000 // this.add(mesh) this.cmesh = mesh this.last = 0 this.lastFrame = 0 this.customProps = {} this.app.scene.add(mesh) } update(time) { if(time - this.last > 300) { // console.log("updating",this.rendereer.info) // console.log(`stats calls:`,this.renderer.info) const fps = ((this.app.renderer.info.render.frame - this.lastFrame)*1000)/(time-this.last) // console.log(fps) const c = this.canvas.getContext('2d') c.fillStyle = 'white' c.fillRect(0, 0, this.canvas.width, this.canvas.height) c.fillStyle = 'black' c.font = '16pt sans-serif' c.fillText(`calls: ${this.app.renderer.info.render.calls}`, 3, 20) c.fillText(`tris : ${this.app.renderer.info.render.triangles}`, 3, 40) c.fillText(`fps : ${fps.toFixed(2)}`,3,60) Object.keys(this.customProps).forEach((key,i) => { const val = this.customProps[key] c.fillText(`${key} : ${val}`,3,80+i*20) }) this.cmesh.material.map.needsUpdate = true this.last = time this.lastFrame = this.app.renderer.info.render.frame } } setProperty(name, value) { this.customProps[name] = value } } ================================================ FILE: src/VoxelMesh.js ================================================ import { Color, Face3, FaceColors, Geometry, BufferGeometry, Mesh, MeshLambertMaterial, Vector2, Vector3, MeshNormalMaterial, Float32BufferAttribute, BufferAttribute, } from "three" function generateAmbientOcclusion(grid) { return [ vertexAO(grid[3], grid[1], grid[0])/3.0, vertexAO(grid[1], grid[5], grid[2])/3.0, vertexAO(grid[5], grid[7], grid[8])/3.0, vertexAO(grid[3], grid[7], grid[6])/3.0 ] } function generateGrid(chunkManager,pos,indexes,vertices) { const quad = [] for(let r=0; r<4; r++) { quad.push(new Vector3( vertices[indexes[r]][0], vertices[indexes[r]][1], vertices[indexes[r]][2], )) } const ab = new Vector3() ab.copy(quad[1]) ab.sub(quad[0]) const ad = new Vector3() ad.copy(quad[3]) ad.sub(quad[0]) const anorm = new Vector3() anorm.copy(ab) anorm.cross(ad) const grid = [] const pt2 = new Vector3() for(let q=-1; q<2; q++) { for(let p=-1;p<2; p++) { pt2.copy(pos) pt2.x += ab.x * p pt2.y += ab.y * p pt2.z += ab.z * p pt2.x += ad.x * q pt2.y += ad.y * q pt2.z += ad.z * q pt2.x += anorm.x * 1 pt2.y += anorm.y * 1 pt2.z += anorm.z * 1 const type =chunkManager.voxelAtCoordinates(pt2) grid.push(type>0?1:0) } } return grid } export class VoxelMesh { constructor(chunk, mesher, scaleFactor, chunkManager) { this.data = chunk const geometry = this.geometry = new BufferGeometry() this.scale = scaleFactor || new Vector3(10, 10, 10) const result = mesher.mesh(chunk.voxels, chunk.dims) this.meshed = result //create empty geometry const vertices = [] const repeatUV = [] const subrects = [] //copy all verticies in from meshed data for (let i = 0; i < result.vertices.length; ++i) { let q = result.vertices[i] vertices.push(q[0],q[1],q[2]) } const indices = [] const normaluvs = [] const frameCount = [] const occlusion = [] // if(result.faces.length > 0) console.log(result) /* generate faces from meshed data Note: that quad faces do not use shared vertices. There will always be faces*4 vertices, even if some of the faces could share vertices because all attributes are per vertex, and those values, such as normals, cannot be shared even if the vertex positions could be. each face is represented by two triangles using indexes and one set of uvs (4) for the whole face. */ const chunkOffset = chunk.realPosition.clone().multiplyScalar(16) for (let i = 0; i < result.faces.length; ++i) { let q = result.faces[i] const info = chunkManager.textureManager.lookupInfoForBlockType(q[4]) const realUVs = chunkManager.textureManager.lookupUVsForBlockType(q[4]) // if(i==0) console.log(realUVs) const a = q[0] const b = q[1] const c = q[2] const d = q[3] //make two triangles /* d --- c | | a --- b */ indices.push(a,b,d) indices.push(b,c,d) let repU = 1 let repV = 1 const {size, spans} = this.faceVertexUv(i) let ao = [1,1,1,1] let uv_a = new Vector2(0,0) let uv_b = new Vector2(1,0) let uv_c = new Vector2(1,1) let uv_d = new Vector2(0,1) const pos = new Vector3() if(size.x > 0 && size.y > 0) { // console.log("front or back", size, uvs, spans) if(spans.x0 > spans.x1) { //calculate AO for back face repU = size.x repV = size.y pos.set(result.vertices[a][0], result.vertices[a][1], result.vertices[a][2]) pos.add(chunkOffset) //rotate UVs by 90 degrees normaluvs.push( uv_b.x,uv_b.y, uv_c.x, uv_c.y, uv_d.x,uv_d.y, uv_a.x,uv_a.y, ) } else { //calculate AO for front face repU = size.x repV = size.y pos.set(result.vertices[a][0], result.vertices[a][1], result.vertices[a][2]-1) pos.add(chunkOffset) //set standard uvs for the whole quad normaluvs.push( uv_a.x,uv_a.y, uv_b.x,uv_b.y, uv_c.x, uv_c.y, uv_d.x,uv_d.y, ) } } //top and bottom if(size.z > 0 && size.x > 0) { if(spans.x0 > spans.x1) { //calculate AO for top face repU = size.z repV = size.x pos.set(result.vertices[a][0], result.vertices[a][1]-1, result.vertices[a][2]) pos.add(chunkOffset) //set standard uvs for the whole quad normaluvs.push( uv_a.x, uv_a.y, uv_b.x, uv_b.y, uv_c.x, uv_c.y, uv_d.x, uv_d.y, ) } else { // bottom repU = size.x repV = size.z pos.set(result.vertices[a][0], result.vertices[a][1], result.vertices[a][2]) pos.add(chunkOffset) //set standard uvs for the whole quad normaluvs.push( uv_a.x, uv_a.y, uv_b.x, uv_b.y, uv_c.x, uv_c.y, uv_d.x, uv_d.y, ) } } //left and right if(size.z > 0 && size.y > 0) { if(spans.y0 > spans.y1) { //left side repU = size.z repV = size.y pos.set(result.vertices[a][0], result.vertices[a][1], result.vertices[a][2]) pos.add(chunkOffset) //set standard uvs for the whole quad normaluvs.push(uv_a.x,uv_a.y, uv_b.x,uv_b.y, uv_c.x, uv_c.y, uv_d.x,uv_d.y) } else { //right side repU = size.z repV = size.y pos.set(result.vertices[a][0]-1, result.vertices[a][1], result.vertices[a][2]) pos.add(chunkOffset) //rotate UVs by 90 degrees normaluvs.push( uv_b.x,uv_b.y, uv_c.x,uv_c.y, uv_d.x,uv_d.y, uv_a.x,uv_a.y, ) } } if(chunkManager.textureManager.aoEnabled) { const grid = generateGrid(chunkManager,pos,q,result.vertices) ao = generateAmbientOcclusion(grid) occlusion.push(ao[0], ao[1], ao[2], ao[3]) } else { occlusion.push(1,1,1,1) } for(let j=0; j<4; j++) { repeatUV.push(repU, repV); } const rect = { x:realUVs[0][0], y:1.0 - realUVs[0][1], w:realUVs[1][0] - realUVs[0][0], h:realUVs[2][1] - realUVs[1][1], } // if(i===0) console.log(rect) let fc = 1 if(info.animated) { fc = rect.w/rect.h rect.w = rect.h } //flip the y axis properly rect.y = 1.0 - realUVs[0][1] - rect.h for(let j=0; j<4; j++) { subrects.push(rect.x,rect.y,rect.w,rect.h) } for(let j=0; j<4; j++) { frameCount.push(fc) } } geometry.setIndex(indices) geometry.addAttribute('position',new Float32BufferAttribute(vertices,3)) geometry.addAttribute('uv', new Float32BufferAttribute(normaluvs,2)) geometry.addAttribute('subrect',new Float32BufferAttribute(subrects,4)) geometry.addAttribute('repeat', new Float32BufferAttribute(repeatUV,2)) geometry.addAttribute('frameCount',new Float32BufferAttribute(frameCount,1)) geometry.addAttribute('occlusion',new Float32BufferAttribute(occlusion,1)) geometry.computeFaceNormals() geometry.uvsNeedUpdate = true geometry.verticesNeedUpdate = true geometry.elementsNeedUpdate = true geometry.normalsNeedUpdate = true geometry.computeBoundingBox() geometry.computeBoundingSphere() } createSurfaceMesh(material) { const surfaceMesh = new Mesh(this.geometry, material) surfaceMesh.scale.copy(this.scale) this.surfaceMesh = surfaceMesh return surfaceMesh } faceVertexUv(i) { let height let width const vs = [ this.meshed.vertices[i * 4 + 0], this.meshed.vertices[i * 4 + 1], this.meshed.vertices[i * 4 + 2], this.meshed.vertices[i * 4 + 3] ] const spans = { x0: vs[0][0] - vs[1][0], x1: vs[1][0] - vs[2][0], y0: vs[0][1] - vs[1][1], y1: vs[1][1] - vs[2][1], z0: vs[0][2] - vs[1][2], z1: vs[1][2] - vs[2][2] } const size = { x: Math.max(Math.abs(spans.x0), Math.abs(spans.x1)), y: Math.max(Math.abs(spans.y0), Math.abs(spans.y1)), z: Math.max(Math.abs(spans.z0), Math.abs(spans.z1)) } if (size.x === 0) { if (spans.y0 > spans.y1) { width = size.y height = size.z } else { width = size.z height = size.y } } if (size.y === 0) { if (spans.x0 > spans.x1) { width = size.x height = size.z } else { width = size.z height = size.x } } if (size.z === 0) { if (spans.x0 > spans.x1) { width = size.x height = size.y } else { width = size.y height = size.x } } /* let uvs = [] if ((size.z === 0 && spans.x0 < spans.x1) || (size.x === 0 && spans.y0 > spans.y1)) { uvs = [ new Vector2(height, 0), new Vector2(0, 0), new Vector2(0, width), new Vector2(height, width) ] } else { uvs = [ new Vector2(0, 0), new Vector2(0, height), new Vector2(width, height), new Vector2(width, 0) ] } */ return {size, spans} } } function vertexAO(side1, side2, corner) { if(side1 && side2) { return 0 } return 3 - (side1 + side2 + corner) } ================================================ FILE: src/VoxelTexture.js ================================================ // var isTransparent = require('opaque').transparent; import { Color, DoubleSide, FaceColors, LinearMipMapLinearFilter, MeshBasicMaterial, MeshLambertMaterial, NearestFilter, MeshFaceMaterial, Texture, } from "three" const createAtlas = window.atlaspack export class VoxelTexture { constructor(opts) { this.game = opts.game; delete opts.game; this.materials = []; this.transparents = []; this.texturePath = opts.texturePath || '/textures/'; this.loading = 0; // this.ao = require('voxel-fakeao')(this.game); var useFlatColors = opts.materialFlatColor === true; delete opts.materialFlatColor; this.options = defaults(opts || {}, { crossOrigin: 'Anonymous', materialParams: defaults(opts.materialParams || {}, { ambient: 0xbbbbbb, transparent: false, side: DoubleSide, }), materialTransparentParams: defaults(opts.materialTransparentParams || {}, { ambient: 0xbbbbbb, transparent: true, side: DoubleSide, //depthWrite: false, //depthTest: false }), materialType: MeshLambertMaterial, applyTextureParams: function (map) { map.magFilter = NearestFilter; map.minFilter = LinearMipMapLinearFilter; } }); // create a canvas for the texture atlas this.canvas = (typeof document !== 'undefined') ? document.createElement('canvas') : {}; this.canvas.width = opts.atlasWidth || 512; this.canvas.height = opts.atlasHeight || 512; var ctx = this.canvas.getContext('2d'); ctx.fillStyle = 'black'; ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); // create core atlas and texture this.atlas = createAtlas(this.canvas); this.atlas.tilepad = true; this._atlasuv = false; this._atlaskey = false; this.texture = new Texture(this.canvas); this.options.applyTextureParams(this.texture); if (useFlatColors) { // If were using simple colors this.material = new MeshBasicMaterial({ vertexColors: FaceColors }); } else { var opaque = new this.options.materialType(this.options.materialParams); opaque.map = this.texture; // var transparent = new this.options.materialType(this.options.materialTransparentParams); // transparent.map = this.texture; this.material = opaque // this.material = new MeshFaceMaterial([ // opaque, // transparent // ]); } // a place for meshes to wait while textures are loading this._meshQueue = []; } load(names, done) { if (!Array.isArray(names)) names = [names]; done = done || function () { }; this.loading++; var materialSlice = names.map(this._expandName); this.materials = this.materials.concat(materialSlice); // load onto the texture atlas var load = Object.create(null); materialSlice.forEach(function (mats) { mats.forEach(function (mat) { if (mat.slice(0, 1) === '#') return; // todo: check if texture already exists load[mat] = true; }); }); if (Object.keys(load).length > 0) { each(Object.keys(load), this.pack.bind(this), () => { this._afterLoading(); done(materialSlice); }); } else { this._afterLoading(); } }; pack(name, done) { const self = this function pack(img) { var node = self.atlas.pack(img); if (node === false) { self.atlas = self.atlas.expand(img); self.atlas.tilepad = true; } done(); } if (typeof name === 'string') { var img = new Image(); img.id = name; img.crossOrigin = this.options.crossOrigin; img.src = this.texturePath + ext(name); img.onload = function () { // if (isTransparent(img)) { // self.transparents.push(name); // } pack(img); }; img.onerror = function () { console.error('Couldn\'t load URL [' + img.src + ']'); done(); }; } else { pack(name); } return this; }; find(name) { var type = 0; this.materials.forEach(function (mats, i) { mats.forEach(function (mat) { if (mat === name) { type = i + 1; return false; } }); if (type !== 0) return false; }); return type; }; _expandName(name) { if (name === null) return Array(6); if (name.top) return [name.back, name.front, name.top, name.bottom, name.left, name.right]; if (!Array.isArray(name)) name = [name]; // load the 0 texture to all if (name.length === 1) name = [name[0], name[0], name[0], name[0], name[0], name[0]]; // 0 is top/bottom, 1 is sides if (name.length === 2) name = [name[1], name[1], name[0], name[0], name[1], name[1]]; // 0 is top, 1 is bottom, 2 is sides if (name.length === 3) name = [name[2], name[2], name[0], name[1], name[2], name[2]]; // 0 is top, 1 is bottom, 2 is front/back, 3 is left/right if (name.length === 4) name = [name[2], name[2], name[0], name[1], name[3], name[3]]; return name; }; _afterLoading() { const alldone = () => { this.loading--; this._atlasuv = this.atlas.uv(this.canvas.width, this.canvas.height); this._atlaskey = Object.create(null); this.atlas.index().forEach((key) => { this._atlaskey[key.name] = key; }); this.texture.needsUpdate = true; this.material.needsUpdate = true; //window.open(this.canvas.toDataURL()); if (this._meshQueue.length > 0) { this._meshQueue.forEach((queue, i) => { this.paint.apply(queue.self, queue.args); delete this._meshQueue[i]; }); } } this._powerof2(function () { setTimeout(alldone, 100); }); }; // Ensure the texture stays at a power of 2 for mipmaps // this is cheating :D _powerof2(done) { var w = this.canvas.width; var h = this.canvas.height; function pow2(x) { x--; x |= x >> 1; x |= x >> 2; x |= x >> 4; x |= x >> 8; x |= x >> 16; x++; return x; } if (h > w) w = h; var old = this.canvas.getContext('2d').getImageData(0, 0, this.canvas.width, this.canvas.height); this.canvas.width = this.canvas.height = pow2(w); this.canvas.getContext('2d').putImageData(old, 0, 0); done(); }; paint(mesh, materials) { var self = this; // if were loading put into queue if (this.loading > 0) { this._meshQueue.push({self: this, args: arguments}); return false; } var isVoxelMesh = (materials) ? false : true; if (!isVoxelMesh) materials = this._expandName(materials); // mesh.material.vertexColors = FaceColors // mesh.material.flatShading = true mesh.geometry.faces.forEach((face, i) => { if (mesh.geometry.faceVertexUvs[0].length < 1) return; if (isVoxelMesh) { var index = Math.floor(face.color.b*255 + face.color.g*255*255 + face.color.r*255*255*255); materials = this.materials[index - 1]; if (!materials) materials = this.materials[0]; } // BACK, FRONT, TOP, BOTTOM, LEFT, RIGHT var name = materials[0] || ''; if (face.normal.z === 1) name = materials[1] || ''; else if (face.normal.y === 1) name = materials[2] || ''; else if (face.normal.y === -1) name = materials[3] || ''; else if (face.normal.x === -1) name = materials[4] || ''; else if (face.normal.x === 1) name = materials[5] || ''; // if just a simple color if (name.slice(0, 1) === '#') { face.color = new Color(name) return; } var atlasuv = this._atlasuv[name]; if (!atlasuv) return; // If a transparent texture use transparent material face.materialIndex = (self.transparents.indexOf(name) !== -1) ? 1 : 0; // 0 -- 1 // | | // 3 -- 2 // faces on these meshes are flipped vertically, so we map in reverse // TODO: tops need rotate if (isVoxelMesh) { if (face.normal.z === -1 || face.normal.x === 1) { atlasuv = uvrot(atlasuv, 90); } atlasuv = uvinvert(atlasuv); } else { atlasuv = uvrot(atlasuv, -90); } //use different indexes for even and odd. if(i%2 === 0) { for (var j = 0; j < mesh.geometry.faceVertexUvs[0][i].length; j++) { let n = j if(j === 0) n = 0; if(j === 1) n = 1; if(j === 2) n = 3 mesh.geometry.faceVertexUvs[0][i][j].x = atlasuv[n][0] mesh.geometry.faceVertexUvs[0][i][j].y = 1 - atlasuv[n][1] } } else { for (let j = 0; j < mesh.geometry.faceVertexUvs[0][i].length; j++) { mesh.geometry.faceVertexUvs[0][i][j].x = atlasuv[j+1][0] mesh.geometry.faceVertexUvs[0][i][j].y = 1 - atlasuv[j+1][1] } } }); mesh.geometry.elementsNeedUpdate = true mesh.geometry.uvsNeedUpdate = true; }; sprite(name, w, h, cb) { if (typeof w === 'function') { cb = w; w = null; } if (typeof h === 'function') { cb = h; h = null; } w = w || 16; h = h || w; this.loading++; var img = new Image(); img.src = this.texturePath + ext(name); img.onerror = cb; img.onload = function () { var canvases = []; for (var x = 0; x < img.width; x += w) { for (var y = 0; y < img.height; y += h) { var canvas = document.createElement('canvas'); canvas.width = w; canvas.height = h; canvas.name = name + '_' + x + '_' + y; canvas.getContext('2d').drawImage(img, x, y, w, h, 0, 0, w, h); canvases.push(canvas); } } var textures = []; each(canvases, function (canvas, next) { var tex = new Image(); tex.name = canvas.name; tex.src = canvas.toDataURL(); tex.onload = function () { this.pack(tex, next); }; tex.onerror = next; textures.push([ tex.name, tex.name, tex.name, tex.name, tex.name, tex.name ]); }, function () { this._afterLoading(); // delete canvases; this.materials = this.materials.concat(textures); cb(textures); }); }; return this; }; animate(mesh, names, delay) { delay = delay || 1000; if (!Array.isArray(names) || names.length < 2) return false; var i = 0; var mat = new this.options.materialType(this.options.materialParams); mat.map = this.texture; mat.transparent = true; mat.needsUpdate = true; // tic.interval(function() { // this.paint(mesh, names[i % names.length]); // i++; // }, delay); return mat; }; tick(dt) { // tic.tick(dt); }; } function uvrot(coords, deg) { if (deg === 0) return coords; var c = []; var i = (4 - Math.ceil(deg / 90)) % 4; for (var j = 0; j < 4; j++) { c.push(coords[i]); if (i === 3) i = 0; else i++; } return c; } function uvinvert(coords) { var c = coords.slice(0); return [c[3], c[2], c[1], c[0]]; } function ext(name) { return (String(name).indexOf('.') !== -1) ? name : name + '.png'; } function defaults(obj) { [].slice.call(arguments, 1).forEach(function (from) { if (from) for (var k in from) if (obj[k] == null) obj[k] = from[k]; }); return obj; } function each(arr, it, done) { var count = 0; arr.forEach(function (a) { it(a, function () { count++; if (count >= arr.length) done(); }); }); } ================================================ FILE: src/ecsy/camera_gimbal.js ================================================ export class StagePosition {} export class StageRotation {} ================================================ FILE: src/ecsy/dashboard.js ================================================ import { Component, System, World } from 'ecsy'; import {VoxelLandscape, VoxelSystem, VoxelTextures} from './voxels.js' import {ActiveBlock, Highlight, HighlightSystem} from './highlight.js' import {InputFrame} from './input.js' export class DomDashboard extends Component { constructor() { super(); this.domElement = null } } export class DashboardVisible extends Component { } export class DashboardDOMOvleraySystem extends System { execute(delta, time) { this.queries.dash.added.forEach(me=>{ const dash = me.getComponent(DomDashboard); let div = document.createElement('div'); div.classList.add("dom-dashboard"); div.addEventListener('mousedown',e => e.stopPropagation()); div.addEventListener('mouseup',e => e.stopPropagation()); div.addEventListener('mousemove',e => e.stopPropagation()); document.documentElement.append(div) this.queries.textures.results.forEach(ent => { console.log("textures are", ent.getComponent(VoxelTextures)) let texs = ent.getComponent(VoxelTextures).textures texs.forEach((tex,i) => { console.log("adding texture",tex); let img = document.createElement('img') img.src = tex.src; div.append(img); img.addEventListener('click',(e)=>{ e.preventDefault(); e.stopPropagation() console.log(`chose this image ${i}`,i) this.queries.active.results.forEach(ent=>{ ent.getMutableComponent(ActiveBlock).type = i }) }) }) }) let dismiss = document.createElement('button'); dismiss.innerHTML = "dismiss" dismiss.addEventListener('click',()=>{ this.queries.visible.results.forEach(ent => { ent.removeComponent(DashboardVisible) }) }) div.append(dismiss) dash.domElement = div }) this.queries.input.results.forEach(ent => { let input = ent.getComponent(InputFrame) if (input.state[InputFrame.OPEN_DASHBOARD] === true) { this.queries.dash.results.forEach(dash_ent => { if(!dash_ent.hasComponent(DashboardVisible)) { dash_ent.addComponent(DashboardVisible) } }) } }) this.queries.visible.added.forEach(ent => { console.log("made visible") ent.getMutableComponent(DomDashboard).domElement.classList.add('visible') }) this.queries.visible.removed.forEach(ent => { console.log("made in-visible") ent.getMutableComponent(DomDashboard).domElement.classList.remove('visible') }) } } DashboardDOMOvleraySystem.queries = { dash: { components:[DomDashboard], listen: { added:true, } }, visible: { components:[DashboardVisible, DomDashboard], listen: { added:true, removed:true, } }, input: { components:[InputFrame] }, textures: { components:[VoxelTextures], }, active: { components:[ActiveBlock] }, } ================================================ FILE: src/ecsy/fullscreen.js ================================================ import { Component, System, World } from 'ecsy'; export class FullscreenMode extends Component { } export class FullscreenButton extends Component { } /* this.fullscreenchangeHandler = () => { if(document.fullscreenElement) return this._fire(FULLSCREEN_ENTERED, this) if(document.webkitFullscreenElement) return this._fire(FULLSCREEN_ENTERED, this) this._fire(FULLSCREEN_EXITED,this) } document.addEventListener('fullscreenchange',this.fullscreenchangeHandler) document.addEventListener('webkitfullscreenchange',this.fullscreenchangeHandler) playFullscreen() { this.resizeOnNextRepaint = true this.container.requestFullscreen() } exitFullscreen() { this.resizeOnNextRepaint = true if(document.exitFullscreen) document.exitFullscreen() if(document.webkitExitFullscreen) document.webkitExitFullscreen() } */ export class FullscreenSystem extends System { execute(delta, time) { this.queries.buttons.added.forEach(ent => { let elem = document.createElement('button') elem.innerText = "fullscreen" elem.classList.add("fullscreen") elem.addEventListener('click',(e)=>{ e.stopPropagation() e.preventDefault() ent.addComponent(FullscreenMode); }) document.documentElement.append(elem); }) this.queries.active.added.forEach(ent => { console.log("turned on full screen") this.fullscreenchangeHandler = () => { console.log("entered full screen") if(document.fullscreenElement || document.webkitFullscreenElement) { console.log("entered") } else { console.log("exited") } } document.addEventListener('fullscreenchange',this.fullscreenchangeHandler) document.addEventListener('webkitfullscreenchange',this.fullscreenchangeHandler) const domElement = document.querySelector("canvas") domElement.requestFullscreen() }) } } FullscreenSystem.queries = { buttons: { components: [FullscreenButton], listen: { added:true } }, active: { components: [FullscreenMode], listen: { added:true, } } } ================================================ FILE: src/ecsy/highlight.js ================================================ import * as util from "../utils.js" import {System} from 'ecsy' import {Quaternion, Ray, Vector2, Vector3} from 'three' import {Camera, Object3D, Transform} from 'ecsy-three' import {traceRay} from "../raycast.js" import {StagePosition, StageRotation} from './camera_gimbal.js' import {MouseCursor} from './mouse.js' import {VoxelLandscape} from './voxels.js' import {InputFrame} from './input.js' export class Highlight { } export class ActiveBlock { constructor() { this.type = 1; } } export class HighlightSystem extends System { init() { } traceRayAtScreenCoords( stageRot, stagePos, domElement, camera, chunkManager, pt, distance) { const ray = new Ray() // e = e.changedTouches[0] const mouse = new Vector2() const bounds = domElement.getBoundingClientRect() mouse.x = ((pt.x - bounds.left) / bounds.width) * 2 - 1 mouse.y = -((pt.y - bounds.top) / bounds.height) * 2 + 1 ray.origin.copy(camera.position) ray.direction.set(mouse.x, mouse.y, 0.5).unproject(camera).sub(ray.origin).normalize() // console.log("new bounds is",mouse,camera) // console.log("stage pos is",stagePos) stagePos.worldToLocal(ray.origin) ray.origin.add(new Vector3(0,0,-0.5)) const quat = new Quaternion() quat.copy(stageRot.quaternion) quat.inverse() ray.direction.applyQuaternion(quat) const hitNormal = new Vector3(0,0,0) const hitPosition = new Vector3(0,0,0) // console.log("chunk manager is", chunkManager) const hitBlock = traceRay(chunkManager,ray.origin,ray.direction,distance,hitPosition,hitNormal,util.EPSILON) return { hitBlock:hitBlock, hitPosition:hitPosition, hitNormal: hitNormal } } execute(delta,time) { this.queries.mouse.results.forEach(mousEnt => { let mouse = mousEnt.getComponent(MouseCursor) let stageRot = this.queries.stageRot.results[0].getComponent(Object3D).value let stagePos = this.queries.stagePos.results[0].getComponent(Object3D).value; this.queries.landscape.results.forEach(ent=>{ const landscape = ent.getComponent(VoxelLandscape); this.queries.highlights.results.forEach(ent => { // console.log("checking",mouse.position, stageRot, stagePos); const domElement = document.querySelector("canvas") const camera = this.queries.camera.results[0].getComponent(Object3D).value const distance = 10; const res = this.traceRayAtScreenCoords( stageRot, stagePos, domElement, camera, landscape.chunkManager, mouse.position, distance) //console.log("res is",res); res.hitPosition.floor() //move the highlight let tran = ent.getMutableComponent(Transform); tran.position.copy(res.hitPosition) //if left button this.queries.inputs.results.forEach(ent => { let input = ent.getComponent(InputFrame) if(input.state[InputFrame.CREATE_AT_CURSOR] === true) { let pos = res.hitPosition.clone() pos.add(res.hitNormal) pos.floor() let active = this.queries.active.results[0] landscape.chunkManager.setVoxelAtCoordinates(pos,active.getComponent(ActiveBlock).type) } input.state[InputFrame.CREATE_AT_CURSOR] = false if(input.state[InputFrame.DESTROY_AT_CURSOR] === true) { let pos = res.hitPosition.clone() pos.floor() landscape.chunkManager.setVoxelAtCoordinates(pos,0) } input.state[InputFrame.DESTROY_AT_CURSOR] = false }) }) }) }) } } HighlightSystem.queries = { highlights: { components: [Highlight]}, stagePos: { components: [StagePosition]}, stageRot: { components: [StageRotation]}, mouse: { components:[MouseCursor] }, inputs: { components:[InputFrame]}, camera: { components:[Camera] }, landscape: { components:[VoxelLandscape]}, active: { components:[ActiveBlock]} } ================================================ FILE: src/ecsy/index.js ================================================ export * from './mouse.js' export * from './keyboard.js' export * from './voxels.js' export * from './highlight.js' export * from './camera_gimbal.js' export * from './webxr.js' export * from './fullscreen.js' export * from './dashboard.js' export * from './input.js' ================================================ FILE: src/ecsy/input.js ================================================ import {Component, System} from 'ecsy' import {StagePosition, StageRotation} from './camera_gimbal.js' import { initialize, Parent, Transform, Object3D, } from 'ecsy-three'; import {Group, Vector3, TextureLoader, CubeGeometry, MeshLambertMaterial, Mesh, AmbientLight, } from 'three'; export class InputFrame extends Component { constructor() { super(); this.state = { ROTATE_LEFT:false, ROTATE_RIGHT:false, LEFT_STRAFE:false, RIGHT_STRAFE:false, MOVE_FORWARD:false, MOVE_BACKWARD:false, LEVITATE_UP:false, LEVITATE_DOWN:false, } } } InputFrame.LEFT_STRAFE = 'LEFT_STRAFE' InputFrame.RIGHT_STRAFE = 'RIGHT_STRAFE' InputFrame.MOVE_FORWARD = 'MOVE_FORWARD' InputFrame.MOVE_BACKWARD = 'MOVE_BACKWARD' InputFrame.ROTATE_LEFT = 'ROTATE_LEFT' InputFrame.ROTATE_RIGHT = 'ROTATE_RIGHT' InputFrame.OPEN_DASHBOARD = 'OPEN_DASHBOARD' InputFrame.ROTATION_DRAGGING = 'ROTATION_DRAGGING' InputFrame.ROTATION_ANGLE = 'ROTATION_ANGLE' InputFrame.CREATE_AT_CURSOR = 'CREATE_AT_CURSOR' InputFrame.DESTROY_AT_CURSOR = 'DESTROY_AT_CURSOR' InputFrame.LEVITATE_UP = 'LEVITATE_UP' InputFrame.LEVITATE_DOWN = 'LEVITATE_DOWN' const Y_AXIS = new Vector3(0,1,0) const Z_AXIS = new Vector3(0,0,1) const SPEED = 0.1 export class VoxelPlayerSystem extends System { init() { console.log("voxel system initting") } execute(delta, time) { this.queries.inputs.results.forEach(ent => { let input = ent.getComponent(InputFrame) this.queries.stageRot.results.forEach(ent => { let rot_trans = ent.getMutableComponent(Transform) if (input.state[InputFrame.ROTATE_LEFT] === true) { rot_trans.rotation.y -= 0.05 } if (input.state[InputFrame.ROTATE_RIGHT] === true) { rot_trans.rotation.y += 0.05 } if (input.state[InputFrame.ROTATION_DRAGGING] === true) { rot_trans.rotation.y = input.state[InputFrame.ROTATION_ANGLE] } else { input.state[InputFrame.ROTATION_ANGLE] = rot_trans.rotation.y } }) this.queries.stagePos.results.forEach(ent => { if(input.state[InputFrame.MOVE_FORWARD] === true) { let stageRot = ent.getComponent(Parent).value let pos_trans = ent.getMutableComponent(Transform) const dir = new Vector3(0,0,1) dir.applyAxisAngle(Y_AXIS, -stageRot.getComponent(Transform).rotation.y) let d2 = dir.normalize().multiplyScalar(SPEED) const vel = d2.multiplyScalar(4) pos_trans.position.x += vel.x; pos_trans.position.z += vel.z; } if(input.state[InputFrame.MOVE_BACKWARD] === true) { let stageRot = ent.getComponent(Parent).value let pos_trans = ent.getMutableComponent(Transform) const dir = new Vector3(0,0,1) dir.applyAxisAngle(Y_AXIS, -stageRot.getComponent(Transform).rotation.y) let d2 = dir.normalize().multiplyScalar(SPEED) const vel = d2.multiplyScalar(-4) pos_trans.position.x += vel.x; pos_trans.position.z += vel.z; } if(input.state[InputFrame.LEFT_STRAFE] === true) { let stageRot = ent.getComponent(Parent).value let pos_trans = ent.getMutableComponent(Transform) const dir = new Vector3(0,0,1) dir.applyAxisAngle(Y_AXIS, -stageRot.getComponent(Transform).rotation.y + Math.PI/2) let d2 = dir.normalize().multiplyScalar(SPEED) const vel = d2.multiplyScalar(4) pos_trans.position.x += vel.x; pos_trans.position.z += vel.z; } if(input.state[InputFrame.RIGHT_STRAFE] === true) { let stageRot = ent.getComponent(Parent).value let pos_trans = ent.getMutableComponent(Transform) const dir = new Vector3(0,0,1) dir.applyAxisAngle(Y_AXIS, -stageRot.getComponent(Transform).rotation.y - Math.PI/2) let d2 = dir.normalize().multiplyScalar(SPEED) const vel = d2.multiplyScalar(4) pos_trans.position.x += vel.x; pos_trans.position.z += vel.z; } if(input.state[InputFrame.LEVITATE_DOWN] === true) { let stageRot = ent.getComponent(Parent).value let pos_trans = ent.getMutableComponent(Transform) const dir = new Vector3(0,1,0) dir.applyAxisAngle(Z_AXIS, -stageRot.getComponent(Transform).rotation.z) let d2 = dir.normalize().multiplyScalar(SPEED) const vel = d2.multiplyScalar(4) pos_trans.position.x += vel.x; pos_trans.position.y += vel.y; } if(input.state[InputFrame.LEVITATE_UP] === true) { let stageRot = ent.getComponent(Parent).value let pos_trans = ent.getMutableComponent(Transform) const dir = new Vector3(0,1,0) dir.applyAxisAngle(Z_AXIS, -stageRot.getComponent(Transform).rotation.z) let d2 = dir.normalize().multiplyScalar(SPEED) const vel = d2.multiplyScalar(-4) pos_trans.position.x += vel.x; pos_trans.position.y += vel.y; } }) }) } } VoxelPlayerSystem.queries = { inputs: { components: [InputFrame], }, stageRot: { components: [StageRotation, Transform], }, stagePos: { components: [StagePosition, Transform], } } ================================================ FILE: src/ecsy/keyboard.js ================================================ import {Component, System} from 'ecsy' import {InputFrame} from './input.js' export class KeyboardBindingSet extends Component { constructor() { super(); this.bindings = {} } } export class KeyboardSystem extends System { _setBindingValue(keyboard_key, new_value) { this.queries.bindings.results.forEach(ent => { let binding = ent.getComponent(KeyboardBindingSet) if(binding.bindings[keyboard_key]) { let state_key = binding.bindings[keyboard_key] this.queries.inputs.results.forEach(ent => { let input = ent.getMutableComponent(InputFrame) input.state[state_key] = new_value }) } }) } _keydown(e) { this._setBindingValue(e.key,true); } _keyup(e) { this._setBindingValue(e.key,false); } init() { document.addEventListener('keydown',this._keydown.bind(this)); document.addEventListener('keyup',this._keyup.bind(this)); } } KeyboardSystem.queries = { bindings: { components:[KeyboardBindingSet] }, inputs: { components: [InputFrame], } } ================================================ FILE: src/ecsy/mouse.js ================================================ import {Component, System} from 'ecsy' import {Vector2} from 'three' import {InputFrame} from './input.js' export class MouseCursor extends Component { constructor() { super(); this.position = new Vector2() this.buttons = 0 this.down = false } } export class MouseSystem extends System { _mouse_down(e) { this.pressed = true this.buttons = e.buttons this.start_point = this.last_point.clone() this.queries.inputs.results.forEach(ent => { let input = ent.getMutableComponent(InputFrame) input.state[InputFrame.ROTATION_DRAGGING] = true this.start_angle = input.state[InputFrame.ROTATION_ANGLE] }) } _mouse_move(e) { this.last_point = new Vector2(e.clientX,e.clientY) if(this.pressed) { let diff = this.last_point.clone().sub(this.start_point) let new_angle = this.start_angle - 0.003*diff.x this.queries.inputs.results.forEach(ent => { let input = ent.getMutableComponent(InputFrame) input.state[InputFrame.ROTATION_ANGLE] = new_angle }) } } _mouse_up(e) { this.pressed = false let diff = this.last_point.clone().sub(this.start_point) this.queries.inputs.results.forEach(ent => { let input = ent.getMutableComponent(InputFrame) input.state[InputFrame.ROTATION_DRAGGING] = false if(Math.abs(diff.x) < 10) { if(this.buttons === 1) { input.state[InputFrame.CREATE_AT_CURSOR] = true } if(this.buttons === 2) { input.state[InputFrame.DESTROY_AT_CURSOR] = true } } }) } init() { this.last_point = new Vector2(200,200) document.addEventListener('contextmenu',e => { e.preventDefault() e.stopPropagation() }) document.addEventListener('mousemove',(e)=>this._mouse_move(e)) document.addEventListener('mousedown', (e)=>this._mouse_down(e)) document.addEventListener('mouseup', (e)=>this._mouse_up(e)) } execute(delta,time) { this.queries.targets.results.forEach(ent => { ent.getMutableComponent(MouseCursor).position.copy(this.last_point) }) } } MouseSystem.queries = { targets: { components: [MouseCursor] }, inputs: { components: [InputFrame], } } ================================================ FILE: src/ecsy/voxels.js ================================================ import {TextureManager} from '../TextureManager.js' import {Component, System} from 'ecsy' import {ChunkManager} from "../ChunkManager.js" import {CulledMesher} from "../CulledMesher.js" import {generateChunkInfoFromFunction} from '../utils.js' import {Group, Vector3} from 'three' import {Object3D} from 'ecsy-three' export class VoxelLandscape extends Component { constructor() { super() this.chunkManager = null this.make_voxel = null } } export class VoxelTextures extends Component { constructor() { super(); this.tm = null this.textures = [] } } export class VoxelSystem extends System { execute(delta, time) { this.queries.entities.added.forEach(entity => { let land = entity.getMutableComponent(VoxelLandscape) //setup the chunk manager land.chunkManager = new ChunkManager({ chunkDistance:1, blockSize:1, mesher: new CulledMesher(), chunkSize:16, generateVoxelChunk: (low, high, pos) => { const id = [pos.x,pos.y,pos.z].join('|') return generateChunkInfoFromFunction(low, high, land.make_voxel) }, container: new Group(), textureManager: new TextureManager({aoEnabled:true}), }); let texs = entity.getMutableComponent(VoxelTextures) texs.tm = land.chunkManager.textureManager texs.tm.loadTextures(texs.textures).then(()=>{ console.log("textures are loaded") land.chunkManager.rebuildAllMeshes() land.chunkManager.requestMissingChunks(new Vector3(0,0,0)) }) entity.addComponent(Object3D, {value: land.chunkManager.container}) }) } } VoxelSystem.queries = { entities: { components: [VoxelLandscape, VoxelTextures], listen: { added: true, removed: true } } } ================================================ FILE: src/ecsy/webxr.js ================================================ import { Component, System, World } from 'ecsy'; import {FullscreenMode} from './fullscreen.js' export class WebXRActive extends Component { } export class WebXRButton extends Component { constructor() { super(); this.currentSession = null } } export class WebXRController extends Component { constructor() { super() this.controller = null this.index = -1 this.selected = false } } export class WebXRSystem extends System { enterWebXR(ent) { console.log('trying to enter webxr') let state = ent.getMutableComponent(WebXRButton); if(state.currentSession === null) { navigator.xr.requestSession('immersive-vr').then(session=>{ console.log("session has started") let onSessionEnded = () => { console.log("session ended") state.currentSession.removeEventListener('end',onSessionEnded) renderer.vr.setSession(null) ent.removeComponent(WebXRActive) } session.addEventListener('end',onSessionEnded) renderer.vr.setSession(session) state.currentSession = session ent.addComponent(WebXRActive) }) } else { state.currentSession.end() } } execute(delta, time) { this.queries.buttons.added.forEach(ent => { console.log("button added"); let elem = document.createElement('button') elem.innerText = "webxr" elem.classList.add("webxr") elem.addEventListener('click', (e) => { e.stopPropagation() e.preventDefault() this.enterWebXR(ent); }) elem.disabled = true document.documentElement.append(elem) console.log("added the element", elem) if ( 'xr' in navigator && 'supportsSession' in navigator.xr ) { navigator.xr.supportsSession( 'immersive-vr' ).then(()=>{ console.log("immersive is supported"); elem.disabled = false }).catch((e)=>{ console.error(e) }); } else { console.log("does not have webxr"); } }) this.queries.controllers.added.forEach(ent => { const con = ent.getMutableComponent(WebXRController) // const domElement = document.querySelector("canvas") con.controller = renderer.xr.getController(con.index) con.controller.addEventListener('selectstart', (evt)=>{ console.log("controller select start") con.selected = true }); con.controller.addEventListener('selectend', (evt)=>{ console.log("controller select end") con.selected = false }); }) } } WebXRSystem.queries = { buttons: { components: [ WebXRButton], listen: { added:true, } }, active: { components: [WebXRActive], listen: { added:true, removed:false, } }, controllers: { components: [WebXRController], listen: { added: true, removed: true } }, } ================================================ FILE: src/index.js ================================================ //export * from './ecsy'; export * from './ChunkManager'; export * from './CulledMesher'; export * from './DesktopControls'; export * from './ECSComp'; export * from './FullscreenControls'; export * from './GreedyMesher'; export * from './KeyboardControls'; export * from './PhysHandler'; export * from './physical'; export * from './raycast'; export * from './SimpleMeshCollider'; export * from './TextureManager'; export * from './TouchControls'; export * from './utils'; export * from './VoxelMesh'; export * from './VoxelTexture'; export * from './VRControls'; export * from './VRStats'; export * from './ecsy'; ================================================ FILE: src/physical.js ================================================ // module.exports = physical //import {AABB as aabb} from "./aabb.js" import {Vector3} from "three" // make these *once*, so we're not generating // garbage for every object in the game. const WORLD_DESIRED = new Vector3(0, 0, 0) , DESIRED = new Vector3(0, 0, 0) , START = new Vector3(0, 0, 0) , END = new Vector3(0, 0, 0) , DIRECTION = new Vector3() , LOCAL_ATTRACTOR = new Vector3() , TOTAL_FORCES = new Vector3() function applyTo(which) { return function(world) { var local = this.avatar.worldToLocal(world) this[which].x += local.x this[which].y += local.y this[which].z += local.z } } const abs = Math.abs //JOSH: I don't know what this is for // , axes = ['x', 'y', 'z'] export class Physical { constructor(avatar, collidables, dimensions, terminal) { //a connection to the underlying threejs object this.avatar = avatar //terminal velocity. default is pretty slow? this.terminal = terminal || new Vector3(0.9, 0.1, 0.9) //the size of the object as width, height, depth in a vector3 //default dimensions are a 1x1x1 cube this.dimensions = dimensions = dimensions || new Vector3(1, 1, 1) //turn dimensions into an AABB (axis aligned bounding box) this._aabb = aabb(new Vector3(0, 0, 0), dimensions) //indicates if not moving in each direction this.resting = {x: false, y: false, z: false} this.old_resting_y = 0 this.last_rest_y = NaN //a list of objects that this Physical can collide with this.collidables = collidables //default fiction. should this be modifyable? this.friction = new Vector3(1, 1, 1) //the current rotation of the avatar. a threejs euler angle this.rotation = this.avatar.rotation this.default_friction = 1 // default yaw/pitch/roll controls to the avatar this.yaw = this.pitch = this.roll = avatar // the current total of forces affecting this object this.forces = new Vector3(0, 0, 0) // a list of attractors affecting this object. meaning something you are pulled towards this.attractors = [] //current acceleration. this.acceleration = new Vector3(0, 0, 0) //current velocity this.velocity = new Vector3(0, 0, 0) this.applyWorldAcceleration = applyTo('acceleration') this.applyWorldVelocity = applyTo('velocity') } tick(dt) { let forces = this.forces let acceleration = this.acceleration let velocity = this.velocity let terminal = this.terminal let friction = this.friction let desired = DESIRED let world_desired = WORLD_DESIRED let bbox let pcs TOTAL_FORCES.multiplyScalar(0) desired.x = desired.y = desired.z = world_desired.x = world_desired.y = world_desired.z = 0 //add in the attractors force /* for (let i = 0; i < this.attractors.length; i++) { var distance_factor = this.avatar.position.distanceToSquared(this.attractors[i]) LOCAL_ATTRACTOR.copy(this.attractors[i]) LOCAL_ATTRACTOR = this.avatar.worldToLocal(LOCAL_ATTRACTOR) DIRECTION.sub(LOCAL_ATTRACTOR, this.avatar.position) DIRECTION.divideScalar(DIRECTION.length() * distance_factor) DIRECTION.multiplyScalar(this.attractors[i].mass) TOTAL_FORCES.addSelf(DIRECTION) } */ dt = dt/1000 // console.log('the forces',this.forces) // console.log("dt",dt) // apply the forces if (!this.resting.x) { acceleration.x /= 8 * dt // acceleration.x += TOTAL_FORCES.x * dt acceleration.x += forces.x * dt velocity.x += acceleration.x * dt velocity.x *= friction.x if (abs(velocity.x) < terminal.x) { desired.x = (velocity.x * dt) } else if (velocity.x !== 0) { desired.x = (velocity.x / abs(velocity.x)) * terminal.x } } else { acceleration.x = velocity.x = 0 } if (!this.resting.y) { // console.log('starting acc',acceleration.y) // acceleration.y /= 8 * dt acceleration.y = 0; // console.log("now",acceleration.y) // acceleration.y += TOTAL_FORCES.y * dt acceleration.y += forces.y * dt // console.log("ending accel",acceleration.y) // console.log('starting vel',velocity.y) velocity.y += acceleration.y// * dt // velocity.y *= friction.y console.log("ending vel",velocity.y) // console.log('desired y', desired.y) console.log('ternimal',terminal.y) if (abs(velocity.y) < terminal.y) { // console.log('less than terminal',) desired.y = (velocity.y * dt) // console.log('dt is',dt) } else if (velocity.y !== 0) { desired.y = (velocity.y / abs(velocity.y)) * terminal.y } } else { // console.log("resting again") acceleration.y = velocity.y = 0 } if (!this.resting.z) { acceleration.z /= 8 * dt acceleration.z += TOTAL_FORCES.z * dt acceleration.z += forces.z * dt velocity.z += acceleration.z * dt velocity.z *= friction.z if (abs(velocity.z) < terminal.z) { desired.z = (velocity.z * dt) } else if (velocity.z !== 0) { desired.z = (velocity.z / abs(velocity.z)) * terminal.z } } else { acceleration.z = velocity.z = 0 } // console.log('starting postion',this.avatar.position) // console.log('desired is',desired) START.copy(this.avatar.position) this.avatar.translateX(desired.x) this.avatar.translateY(desired.y) this.avatar.translateZ(desired.z) END.copy(this.avatar.position) this.avatar.position.copy(START) //START is where the object is now //END is where the object will be after the movement is applied //desired is the direction vector that was calculated //JOSH: I think world desired is a direction vector for the current motion world_desired.x = END.x - START.x world_desired.y = END.y - START.y world_desired.z = END.z - START.z // console.log('world destired stare is',world_desired) // console.log('start',START,'end',END, 'diff', world_desired) // console.log("world desired",world_desired) //set the friction in all directions this.friction.x = this.friction.y = this.friction.z = this.default_friction // save old copies, since when normally on the // ground, this.resting.y alternates (false,-1) // JOSH: this part confuses me this.old_resting_y = (this.old_resting_y << 1) >>> 0 this.old_resting_y |= !!this.resting.y | 0 // run collisions this.resting.x = this.resting.y = this.resting.z = false bbox = this.aabb() pcs = this.collidables //collide against everything except myself for (let i = 0, len = pcs.length; i < len; ++i) { if (pcs[i] !== this) { pcs[i].collide(this, bbox, world_desired, this.resting) } } //what does this part do? /* // fall distance if (!!(this.old_resting_y & 0x4) !== !!this.resting.y) { if (!this.resting.y) { this.last_rest_y = this.avatar.position.y } else if (!isNaN(this.last_rest_y)) { this.fell(this.last_rest_y - this.avatar.position.y) this.last_rest_y = NaN } } */ // apply translation this.avatar.position.x += world_desired.x this.avatar.position.y += world_desired.y this.avatar.position.z += world_desired.z } subjectTo (force) { this.forces.x += force[0] this.forces.y += force[1] this.forces.z += force[2] return this } removeForce (force) { this.forces.x -= force[0] this.forces.y -= force[1] this.forces.z -= force[2] return this } attractTo (vector, mass) { vector.mass = mass this.attractors.push(vector) } aabb () { const pos = this.avatar.position const d = this.dimensions return aabb( new Vector3(pos.x - (d.x / 2), pos.y, pos.z - (d.z / 2)), this.dimensions ) } // no object -> object collisions for now, thanks collide (other, bbox, world_vec, resting) { return } atRestX () { return this.resting.x } atRestY () { return this.resting.y } atRestZ () { return this.resting.z } fell (distance) { return } } ================================================ FILE: src/raycast.js ================================================ function traceRay_impl( voxelProvider, px, py, pz, dx, dy, dz, max_d, hit_pos, hit_norm, EPSILON) { var t = 0.0 , nx=0, ny=0, nz=0 , ix, iy, iz , fx, fy, fz , ox, oy, oz , ex, ey, ez , b, step, min_step , floor = Math.floor //Step block-by-block along ray while(t <= max_d) { ox = px + t * dx oy = py + t * dy oz = pz + t * dz ix = floor(ox)|0 iy = floor(oy)|0 iz = floor(oz)|0 fx = ox - ix fy = oy - iy fz = oz - iz b = voxelProvider.getBlock(ix, iy, iz) if(b) { if(hit_pos) { //Clamp to face on hit hit_pos.x = fx < EPSILON ? +ix : (fx > 1.0-EPSILON ? ix+1.0-EPSILON : ox) hit_pos.y = fy < EPSILON ? +iy : (fy > 1.0-EPSILON ? iy+1.0-EPSILON : oy) hit_pos.z = fz < EPSILON ? +iz : (fz > 1.0-EPSILON ? iz+1.0-EPSILON : oz) } if(hit_norm) { hit_norm.x = nx hit_norm.y = ny hit_norm.z = nz } return b } //Check edge cases min_step = +(EPSILON * (1.0 + t)) if(t > min_step) { ex = nx < 0 ? fx <= min_step : fx >= 1.0 - min_step ey = ny < 0 ? fy <= min_step : fy >= 1.0 - min_step ez = nz < 0 ? fz <= min_step : fz >= 1.0 - min_step if(ex && ey && ez) { b = voxelProvider.getBlock(ix+nx, iy+ny, iz) || voxelProvider.getBlock(ix, iy+ny, iz+nz) || voxelProvider.getBlock(ix+nx, iy, iz+nz) if(b) { if(hit_pos) { hit_pos.x = nx < 0 ? ix-EPSILON : ix + 1.0-EPSILON hit_pos.y = ny < 0 ? iy-EPSILON : iy + 1.0-EPSILON hit_pos.z = nz < 0 ? iz-EPSILON : iz + 1.0-EPSILON } if(hit_norm) { hit_norm.x = nx hit_norm.y = ny hit_norm.z = nz } return b } } if(ex && (ey || ez)) { b = voxelProvider.getBlock(ix+nx, iy, iz) if(b) { if(hit_pos) { hit_pos.x = nx < 0 ? ix-EPSILON : ix + 1.0-EPSILON hit_pos.y = fy < EPSILON ? +iy : oy hit_pos.z = fz < EPSILON ? +iz : oz } if(hit_norm) { hit_norm.x = nx hit_norm.y = ny hit_norm.z = nz } return b } } if(ey && (ex || ez)) { b = voxelProvider.getBlock(ix, iy+ny, iz) if(b) { if(hit_pos) { hit_pos.x = fx < EPSILON ? +ix : ox hit_pos.y = ny < 0 ? iy-EPSILON : iy + 1.0-EPSILON hit_pos.z = fz < EPSILON ? +iz : oz } if(hit_norm) { hit_norm.x = nx hit_norm.y = ny hit_norm.z = nz } return b } } if(ez && (ex || ey)) { b = voxelProvider.getBlock(ix, iy, iz+nz) if(b) { if(hit_pos) { hit_pos.x = fx < EPSILON ? +ix : ox hit_pos.y = fy < EPSILON ? +iy : oy hit_pos.z = nz < 0 ? iz-EPSILON : iz + 1.0-EPSILON } if(hit_norm) { hit_norm.x = nx hit_norm.y = ny hit_norm.z = nz } return b } } } //Walk to next face of cube along ray nx = ny = nz = 0 step = 2.0 if(dx < -EPSILON) { var s = -fx/dx nx = 1 step = s } if(dx > EPSILON) { var s = (1.0-fx)/dx nx = -1 step = s } if(dy < -EPSILON) { var s = -fy/dy if(s < step-min_step) { nx = 0 ny = 1 step = s } else if(s < step+min_step) { ny = 1 } } if(dy > EPSILON) { var s = (1.0-fy)/dy if(s < step-min_step) { nx = 0 ny = -1 step = s } else if(s < step+min_step) { ny = -1 } } if(dz < -EPSILON) { var s = -fz/dz if(s < step-min_step) { nx = ny = 0 nz = 1 step = s } else if(s < step+min_step) { nz = 1 } } if(dz > EPSILON) { var s = (1.0-fz)/dz if(s < step-min_step) { nx = ny = 0 nz = -1 step = s } else if(s < step+min_step) { nz = -1 } } if(step > max_d - t) { step = max_d - t - min_step } if(step < min_step) { step = min_step } t += step } if(hit_pos) { hit_pos.x = ox; hit_pos.y = oy; hit_pos.z = oz; } if(hit_norm) { hit_norm.x = hit_norm.y = hit_norm.z = 0; } return 0 } function startTraceRay(voxelProvider, origin, direction, max_d, hit_pos, hit_norm, EPSILON) { // console.log("tracing",origin,direction) var px = +origin.x , py = +origin.y , pz = +origin.z , dx = +direction.x , dy = +direction.y , dz = +direction.z , ds = Math.sqrt(dx*dx + dy*dy + dz*dz) if(typeof(EPSILON) === "undefined") { EPSILON = 1e-8 } if(ds < EPSILON) { if(hit_pos) { hit_pos.x = hit_pos.y = hit_pos.z } if(hit_norm) { hit_norm.x = hit_norm.y = hit_norm.z } return 0; } dx /= ds dy /= ds dz /= ds if(typeof(max_d) === "undefined") { max_d = 64.0 } else { max_d = +max_d } return traceRay_impl(voxelProvider, px, py, pz, dx, dy, dz, max_d, hit_pos, hit_norm, EPSILON) } export const traceRay = startTraceRay // module.exports = traceRay ================================================ FILE: src/utils.js ================================================ import {Quaternion, Ray, Vector2, Vector3,} from "three" import {traceRay} from './raycast.js' export function toHexColor(num) { let str = num.toString(16) while(str.length < 6) str = '0' + str return '#' + str } export function generateChunkInfoFromFunction(l, h, f) { let d = [ h[0]-l[0], h[1]-l[1], h[2]-l[2] ] let v = new Int32Array(d[0]*d[1]*d[2]) let n = 0; for(let k=l[2]; k Math.PI / 180 * deg export const EPSILON = 1e-8 export const $ = (sel) => document.querySelector(sel) export const DIRS = { NONE:'NONE', UP:'UP', DOWN:'DOWN', LEFT:'LEFT', RIGHT:'RIGHT' } export const on = (elem, type, cb) => elem.addEventListener(type,cb) export function traceRayAtScreenCoords(app, pt, distance) { const ray = new Ray() // e = e.changedTouches[0] const mouse = new Vector2() const bounds = app.renderer.domElement.getBoundingClientRect() mouse.x = ((pt.x - bounds.left) / bounds.width) * 2 - 1 mouse.y = -((pt.y - bounds.top) / bounds.height) * 2 + 1 ray.origin.copy(app.camera.position) ray.direction.set(mouse.x, mouse.y, 0.5).unproject(app.camera).sub(ray.origin).normalize() app.stagePos.worldToLocal(ray.origin) ray.origin.add(new Vector3(0,0,-0.5)) const quat = new Quaternion() quat.copy(app.stageRot.quaternion) quat.inverse() ray.direction.applyQuaternion(quat) const hitNormal = new Vector3(0,0,0) const hitPosition = new Vector3(0,0,0) const hitBlock = traceRay(app.chunkManager,ray.origin,ray.direction,distance,hitPosition,hitNormal,EPSILON) return { hitBlock:hitBlock, hitPosition:hitPosition, hitNormal: hitNormal } } export const rand = (min,max) => Math.random()*(max-min) + min ================================================ FILE: src/webxr-boilerplate/BackgroundAudioLoader.js ================================================ import { DefaultLoadingManager, } from "three" export default class BackgroundAudioLoader { constructor(manager) { this.manager = ( manager !== undefined ) ? manager : DefaultLoadingManager; } load( url, onLoad, onProgress, onError ) { console.log("BGAL loading",url) const music = new Audio(url) music.autoplay = false music.loop = true music.controls = false music.preload = 'auto' music.volume = 0.75 music.addEventListener('canplay',()=>{ onLoad(music) this.manager.itemEnd(url) }) this.manager.itemStart(url) } } ================================================ FILE: src/webxr-boilerplate/Pointer.js ================================================ import {Raycaster} from "./raycaster.js" import { Object3D, Vector2, Vector3, Quaternion, BufferGeometry, Float32BufferAttribute, LineBasicMaterial, NormalBlending, SphereBufferGeometry, Line, Mesh, MeshLambertMaterial, } from "three" export const POINTER_ENTER = "enter" export const POINTER_EXIT = "exit" export const POINTER_CLICK = "click" export const POINTER_MOVE = "move" export const POINTER_PRESS = "press" export const POINTER_RELEASE = "release" const toRad = (degrees) => degrees*Math.PI/180 export class Pointer { constructor(app, opts) { this.scene = app.scene this.renderer = app.renderer this.camera = app.camera this.listeners = {} this.opts = opts || {} this.opts.enableLaser = (this.opts.enableLaser !== undefined) ? this.opts.enableLaser : true this.opts.laserLength = (this.opts.laserLength !== undefined) ? this.opts.laserLength : 3 this.canvas = this.renderer.domElement this.raycaster = new Raycaster() this.waitcb = null this.hoverTarget = null this.intersectionFilter = this.opts.intersectionFilter || ((o) => true) this.raycaster.recurseFilter = this.opts.recurseFilter || (()=> true) this.multiTarget = this.opts.multiTarget || false // setup the mouse this.canvas.addEventListener('mousemove', this.mouseMove.bind(this)) this.canvas.addEventListener('click', this.mouseClick.bind(this)) this.canvas.addEventListener('mousedown',this.mouseDown.bind(this)) this.canvas.addEventListener('mouseup',this.mouseUp.bind(this)) //touch events this.canvas.addEventListener('touchstart',this.touchStart.bind(this)) this.canvas.addEventListener('touchmove',this.touchMove.bind(this)) this.canvas.addEventListener('touchend',this.touchEnd.bind(this)) // setup the VR controllers this.controller1 = this.renderer.vr.getController(0); this.controller1.addEventListener('selectstart', this.controllerSelectStart.bind(this)); this.controller1.addEventListener('selectend', this.controllerSelectEnd.bind(this)); this.controller2 = this.renderer.vr.getController(1); this.controller2.addEventListener('selectstart', this.controllerSelectStart.bind(this)); this.controller2.addEventListener('selectend', this.controllerSelectEnd.bind(this)); this.setMouseSimulatesController(opts.mouseSimulatesController) this.scene.add(this.controller1); this.scene.add(this.controller2); if(this.opts.enableLaser) { //create visible lines for the two controllers const geometry = new BufferGeometry() geometry.addAttribute('position', new Float32BufferAttribute([0, 0, 0, 0, 0, -this.opts.laserLength], 3)); geometry.addAttribute('color', new Float32BufferAttribute([1.0, 0.5, 0.5, 0, 0, 0], 3)); const material = new LineBasicMaterial({ vertexColors: false, color: 0x880000, linewidth: 5, blending: NormalBlending }) this.controller1.add(new Line(geometry, material)); this.controller2.add(new Line(geometry, material)); } } //override this to do something w/ the controllers on every tick tick(time) { this.controllerMove(this.controller1) this.controllerMove(this.controller2) } fire(obj, type, payload) { obj.dispatchEvent(payload) } fireSelf(type,payload) { if(!this.listeners[type]) return this.listeners[type].forEach(cb => cb(payload)) } //make the camera follow the mouse in desktop mode. Helps w/ debugging. cameraFollowMouse(e) { const bounds = this.canvas.getBoundingClientRect() const ry = ((e.clientX - bounds.left) / bounds.width) * 2 - 1 const rx = 1 - ((e.clientY - bounds.top) / bounds.height) * 2 this.camera.rotation.y = -ry*2 this.camera.rotation.x = +rx } mouseMove(e) { const mouse = new Vector2() const bounds = this.canvas.getBoundingClientRect() mouse.x = ((e.clientX - bounds.left) / bounds.width) * 2 - 1 mouse.y = -((e.clientY - bounds.top) / bounds.height) * 2 + 1 this.raycaster.setFromCamera(mouse, this.camera) if(this.opts.mouseSimulatesController) { //create target from the mouse controls const target = new Vector3() target.x = mouse.x target.y = mouse.y target.z = -3 //convert to camera space target.add(this.camera.position) this.spot.position.copy(target) this.controller1.lookAt(target) //have to flip over because the UP is down on controllers const flip = new Quaternion().setFromAxisAngle(new Vector3(0,1,0),toRad(180)) this.controller1.quaternion.multiply(flip) } this._processMove() if(this.opts.cameraFollowMouse) this.cameraFollowMouse(e) } touchStart(e) { e.preventDefault() if(e.changedTouches.length <= 0) return const tch = e.changedTouches[0] const mouse = new Vector2() const bounds = this.canvas.getBoundingClientRect() mouse.x = ((tch.clientX - bounds.left) / bounds.width) * 2 - 1 mouse.y = -((tch.clientY - bounds.top) / bounds.height) * 2 + 1 this.raycaster.setFromCamera(mouse, this.camera) const intersects = this.raycaster.intersectObjects(this.scene.children, true) .filter(it => this.intersectionFilter(it.object)) intersects.forEach((it,i) => { this.fire(it.object, POINTER_PRESS, {type: POINTER_PRESS}) }) } touchMove(e) { e.preventDefault() if(e.changedTouches.length <= 0) return const tch = e.changedTouches[0] const mouse = new Vector2() const bounds = this.canvas.getBoundingClientRect() mouse.x = ((tch.clientX - bounds.left) / bounds.width) * 2 - 1 mouse.y = -((tch.clientY - bounds.top) / bounds.height) * 2 + 1 this.raycaster.setFromCamera(mouse, this.camera) this._processMove() } touchEnd(e) { e.preventDefault() if(e.changedTouches.length <= 0) return const tch = e.changedTouches[0] const mouse = new Vector2() const bounds = this.canvas.getBoundingClientRect() mouse.x = ((tch.clientX - bounds.left) / bounds.width) * 2 - 1 mouse.y = -((tch.clientY - bounds.top) / bounds.height) * 2 + 1 this.raycaster.setFromCamera(mouse, this.camera) const intersects = this.raycaster.intersectObjects(this.scene.children, true) .filter(it => this.intersectionFilter(it.object)) intersects.forEach((it) => { this.fire(it.object, POINTER_RELEASE, {type: POINTER_RELEASE, point: it.point}) }) this._processClick() } controllerMove(controller) { if(!controller.visible) return const c = controller const dir = new Vector3(0, 0, -1) dir.applyQuaternion(c.quaternion) this.raycaster.set(c.position, dir) this._processMove() } _processMove() { const intersects = this.raycaster.intersectObjects(this.scene.children, true) .filter(it => this.intersectionFilter(it.object)) if(intersects.length === 0 && this.hoverTarget) { this.fire(this.hoverTarget, POINTER_EXIT, {type: POINTER_EXIT}) this.hoverTarget = null } if(intersects.length >= 1) { const it = intersects[0] const obj = it.object if (!obj) return this.fire(obj, POINTER_MOVE, {type: POINTER_MOVE, point: it.point, intersection:it}) if (obj === this.hoverTarget) { //still inside } else { if (this.hoverTarget) this.fire(this.hoverTarget, POINTER_EXIT, {type: POINTER_EXIT}) this.hoverTarget = obj this.fire(this.hoverTarget, POINTER_ENTER, {type: POINTER_ENTER}) } } } _processClick() { if (this.waitcb) { this.waitcb() this.waitcb = null return } const intersects = this.raycaster.intersectObjects(this.scene.children, true) .filter(it => this.intersectionFilter(it.object)) if(intersects.length > 0) { const it = intersects[0] this.fire(it.object, POINTER_CLICK, {type: POINTER_CLICK, point: it.point, intersection:it}) } else { this.fireSelf(POINTER_CLICK, {type: POINTER_CLICK}) } } mouseClick(e) { const mouse = new Vector2() const bounds = this.canvas.getBoundingClientRect() mouse.x = ((e.clientX - bounds.left) / bounds.width) * 2 - 1 mouse.y = -((e.clientY - bounds.top) / bounds.height) * 2 + 1 this.raycaster.setFromCamera(mouse, this.camera) this._processClick() } mouseDown(e) { const mouse = new Vector2() const bounds = this.canvas.getBoundingClientRect() mouse.x = ((e.clientX - bounds.left) / bounds.width) * 2 - 1 mouse.y = -((e.clientY - bounds.top) / bounds.height) * 2 + 1 this.raycaster.setFromCamera(mouse, this.camera) const intersects = this.raycaster.intersectObjects(this.scene.children, true) .filter(it => this.intersectionFilter(it.object)) intersects.forEach((it,i) => { if(!this.multiTarget && i > 0) return this.fire(it.object, POINTER_PRESS, {type: POINTER_PRESS, point: it.point, intersection:it}) }) } mouseUp(e) { const mouse = new Vector2() const bounds = this.canvas.getBoundingClientRect() mouse.x = ((e.clientX - bounds.left) / bounds.width) * 2 - 1 mouse.y = -((e.clientY - bounds.top) / bounds.height) * 2 + 1 this.raycaster.setFromCamera(mouse, this.camera) const intersects = this.raycaster.intersectObjects(this.scene.children, true) .filter(it => this.intersectionFilter(it.object)) intersects.forEach((it,i) => { if(!this.multiTarget && i > 0) return //skip all but the first this.fire(it.object, POINTER_RELEASE, {type: POINTER_RELEASE, point: it.point, intersection:it}) }) } controllerSelectStart(e) { e.target.userData.isSelecting = true; const intersects = this.raycaster.intersectObjects(this.scene.children, true) .filter(it => this.intersectionFilter(it.object)) intersects.forEach((it,i) => { if(!this.multiTarget && i > 0) return //skip all but the first this.fire(it.object, POINTER_PRESS, {type: POINTER_PRESS, point: it.point, intersection:it}) }) } controllerSelectEnd(e) { e.target.userData.isSelecting = false; const c = e.target const dir = new Vector3(0, 0, -1) dir.applyQuaternion(c.quaternion) this.raycaster.set(c.position, dir) const intersects = this.raycaster.intersectObjects(this.scene.children, true) .filter(it => this.intersectionFilter(it.object)) intersects.forEach((it,i) => { if(!this.multiTarget && i > 0) return //skip all but the first this.fire(it.object, POINTER_RELEASE, {type: POINTER_RELEASE, point: it.point}) }) this._processClick() } waitSceneClick(cb) { this.waitcb = cb } addEventListener(type,cb) { this.on(type,cb) } on(type,cb) { if(!this.listeners[type]) this.listeners[type] = [] this.listeners[type].push(cb) } off(type,cb) { this.listeners[type] = this.listeners[type].filter(c => c !== cb) } setMouseSimulatesController(val) { this.opts.mouseSimulatesController = val if(this.opts.mouseSimulatesController) { this.controller1 = new Group() this.controller1.position.set(0,1,-2) this.controller1.quaternion.setFromUnitVectors(Object3D.DefaultUp, new Vector3(0,0,1)) this.spot = new Mesh( new SphereBufferGeometry(0.1), new MeshLambertMaterial({color: 'red'}) ) this.scene.add(this.spot) } else { } } } ================================================ FILE: src/webxr-boilerplate/WebXRBoilerPlate.js ================================================ import {DefaultLoadingManager, PerspectiveCamera, Scene, WebGLRenderer,} from "three" import VRManager, {VR_DETECTED} from "./vrmanager.js"; export const FULLSCREEN_ENTERED = "fullscreenenter" export const FULLSCREEN_EXITED = "fullscreenexit" export default class WebXRBoilerPlate { constructor(options) { this.listeners = {} this.container = options.container this.resizeOnNextRepaint = false //polyfill the container for fullscreen support if(!container.requestFullscreen) { if(container.webkitRequestFullscreen) { container.requestFullscreen = container.webkitRequestFullScreen } } } addEventListener(type,cb) { if(!this.listeners[type]) this.listeners[type] = [] this.listeners[type].push(cb) } _fire(type,payload) { if(!this.listeners[type]) this.listeners[type] = [] this.listeners[type].forEach(cb => cb(payload)) } isFullscreenSupported() { return this.container.requestFullscreen?true:false } init() { this.scene = new Scene(); this.camera = new PerspectiveCamera(70, this.container.clientWidth / this.container.clientHeight, 0.1, 50); this.renderer = new WebGLRenderer({antialias: true}); this.renderer.setPixelRatio(window.devicePixelRatio); this.renderer.setSize(this.container.clientWidth, this.container.clientHeight); this.renderer.gammaOutput = true this.renderer.vr.enabled = true; this.container.appendChild(this.renderer.domElement); this.vrmanager = new VRManager(this.renderer) this.vrmanager.addEventListener(VR_DETECTED, ()=> this._fire(VR_DETECTED,this)) this.loadingManager = DefaultLoadingManager DefaultLoadingManager.joshtest = true DefaultLoadingManager.onStart = (url, loaded, total) => { console.log(`XR: loading ${url}. loaded ${loaded} of ${total}`) } DefaultLoadingManager.onLoad = () => { console.log(`XR: loading complete`) if (this.listeners.loaded) this.listeners.loaded.forEach(cb => cb(this)) } DefaultLoadingManager.onProgress = (url, loaded, total) => { console.log(`XR: prog ${url}. loaded ${loaded} of ${total}`) if(this.listeners.progress) this.listeners.progress.forEach(cb => cb(loaded/total)) } DefaultLoadingManager.onError = (url) => { console.log(`XR: error loading ${url}`) } this.lastSize = { width: 0, height: 0} this.render = (time) => { if (this.onRenderCb) this.onRenderCb(time,this) this.checkContainerSize() this.renderer.render(this.scene, this.camera); } this.renderer.setAnimationLoop(this.render) this.fullscreenchangeHandler = () => { if(document.fullscreenElement) return this._fire(FULLSCREEN_ENTERED, this) if(document.webkitFullscreenElement) return this._fire(FULLSCREEN_ENTERED, this) this._fire(FULLSCREEN_EXITED,this) } document.addEventListener('fullscreenchange',this.fullscreenchangeHandler) document.addEventListener('webkitfullscreenchange',this.fullscreenchangeHandler) return new Promise((res, rej) => { res(this) }) } onRender(cb) { this.onRenderCb = cb } enterVR() { this.vrmanager.enterVR() } playFullscreen() { this.resizeOnNextRepaint = true this.container.requestFullscreen() } exitFullscreen() { this.resizeOnNextRepaint = true if(document.exitFullscreen) document.exitFullscreen() if(document.webkitExitFullscreen) document.webkitExitFullscreen() } checkContainerSize() { if(this.lastSize.width !== this.container.clientWidth || this.lastSize.height !== this.container.clientHeight) { this.lastSize.width = this.container.clientWidth this.lastSize.height = this.container.clientHeight this.camera.aspect = this.lastSize.width / this.lastSize.height; this.camera.updateProjectionMatrix(); this.renderer.setSize(this.lastSize.width, this.lastSize.height); } } } ================================================ FILE: src/webxr-boilerplate/raycaster.js ================================================ import {Ray} from "three" /** * @author mrdoob / http://mrdoob.com/ * @author bhouston / http://clara.io/ * @author stephomi / http://stephaneginier.com/ */ function Raycaster( origin, direction, near, far ) { this.ray = new Ray( origin, direction ); // direction is assumed to be normalized (for accurate distance calculations) this.near = near || 0; this.far = far || Infinity; this.params = { Mesh: {}, Line: {}, LOD: {}, Points: { threshold: 1 }, Sprite: {} }; Object.defineProperties( this.params, { PointCloud: { get: function () { console.warn( 'THREE.Raycaster: params.PointCloud has been renamed to params.Points.' ); return this.Points; } } } ); } function ascSort( a, b ) { return a.distance - b.distance; } let count = 0 function intersectObject( object, raycaster, intersects, recursive ) { if ( object.visible === false ) return; if(raycaster.recurseFilter && !raycaster.recurseFilter(object)) return; count++ object.raycast( raycaster, intersects ); if ( recursive === true ) { var children = object.children; for ( var i = 0, l = children.length; i < l; i ++ ) { intersectObject( children[ i ], raycaster, intersects, true ); } } } Object.assign( Raycaster.prototype, { linePrecision: 1, set: function ( origin, direction ) { // direction is assumed to be normalized (for accurate distance calculations) this.ray.set( origin, direction ); }, setFromCamera: function ( coords, camera ) { if ( ( camera && camera.isPerspectiveCamera ) ) { this.ray.origin.setFromMatrixPosition( camera.matrixWorld ); this.ray.direction.set( coords.x, coords.y, 0.5 ).unproject( camera ).sub( this.ray.origin ).normalize(); } else if ( ( camera && camera.isOrthographicCamera ) ) { this.ray.origin.set( coords.x, coords.y, ( camera.near + camera.far ) / ( camera.near - camera.far ) ).unproject( camera ); // set origin in plane of camera this.ray.direction.set( 0, 0, - 1 ).transformDirection( camera.matrixWorld ); } else { console.error( 'THREE.Raycaster: Unsupported camera type.' ); } }, intersectObject: function ( object, recursive, optionalTarget ) { var intersects = optionalTarget || []; intersectObject( object, this, intersects, recursive); intersects.sort( ascSort ); return intersects; }, intersectObjects: function ( objects, recursive, optionalTarget ) { count = 0 var intersects = optionalTarget || []; if ( Array.isArray( objects ) === false ) { console.warn( 'THREE.Raycaster.intersectObjects: objects is not an Array.' ); return intersects; } for ( var i = 0, l = objects.length; i < l; i ++ ) { intersectObject( objects[ i ], this, intersects, recursive); } // console.log("intersected objects",count) intersects.sort( ascSort ); return intersects; } } ); export { Raycaster }; ================================================ FILE: src/webxr-boilerplate/vrmanager.js ================================================ function printError(err) { console.log(err) } export const VR_DETECTED = "detected" export const VR_CONNECTED = "connected" export const VR_DISCONNECTED = "disconnected" export const VR_PRESENTCHANGE = "presentchange" export const VR_ACTIVATED = "activated" export default class VRManager { constructor(renderer) { this.device = null this.renderer = renderer if(!this.renderer) throw new Error("VR Manager requires a valid ThreeJS renderer instance") this.listeners = {} if ('xr' in navigator && navigator.xr.requestDevice) { console.log("has webxr") navigator.xr.requestDevice().then((device) => { device.supportsSession({immersive: true, exclusive: true /* DEPRECATED */}) .then(() => { this.device = device this.fire(VR_DETECTED,{}) }) .catch(printError); }).catch(printError); } else if ('getVRDisplays' in navigator) { console.log("has webvr") window.addEventListener( 'vrdisplayconnect', ( event ) => { this.device = event.display this.fire(VR_CONNECTED) }, false ); window.addEventListener( 'vrdisplaydisconnect', ( event ) => { this.fire(VR_DISCONNECTED) }, false ); window.addEventListener( 'vrdisplaypresentchange', ( event ) => { this.fire(VR_PRESENTCHANGE) }, false ); window.addEventListener( 'vrdisplayactivate', ( event ) => { this.device = event.display this.device.requestPresent([{source:this.renderer.domElement}]) this.fire(VR_ACTIVATED) }, false ); navigator.getVRDisplays() .then( ( displays ) => { console.log("vr scanned") if ( displays.length > 0 ) { // showEnterVR( displays[ 0 ] ); console.log("found vr") this.device = displays[0] this.fire(VR_DETECTED,{}) } else { console.log("no vr at all") // showVRNotFound(); } } ).catch(printError); } else { // no vr console.log("no vr at all") } } addEventListener(type, cb) { if(!this.listeners[type]) this.listeners[type] = [] this.listeners[type].push(cb) } fire(type,evt) { if(!evt) evt = {} evt.type = type if(!this.listeners[type]) this.listeners[type] = [] this.listeners[type].forEach(cb => cb(evt)) } enterVR() { if(!this.device) { console.warn("tried to connect VR on an invalid device") return } console.log("entering VR") this.renderer.vr.setDevice( this.device ); if(this.device.isPresenting) { this.device.exitPresent() } else { this.device.requestPresent([{source: this.renderer.domElement}]); } } } ================================================ FILE: src/webxr-boilerplate/vrstats.js ================================================ import { Group, CanvasTexture, Mesh, PlaneBufferGeometry, PlaneGeometry, MeshBasicMaterial, } from 'three' export default class VRStats extends Group { constructor(app) { super(); this.renderer = app.renderer const can = document.createElement('canvas') can.width = 256 can.height = 128 this.canvas = can const c = can.getContext('2d') c.fillStyle = '#00ffff' c.fillRect(0,0,can.width,can.height) const ctex = new CanvasTexture(can) const mesh = new Mesh( new PlaneGeometry(1,0.5), new MeshBasicMaterial({map:ctex}) ) this.position.z = -3 this.position.y = 1.5 this.add(mesh) this.cmesh = mesh this.last = 0 this.lastFrame = 0 this.customProps = {} } update(time) { if(time - this.last > 300) { // console.log("updating",this.renderer.info) // console.log(`stats calls:`,this.renderer.info) const fps = ((this.renderer.info.render.frame - this.lastFrame)*1000)/(time-this.last) // console.log(fps) const c = this.canvas.getContext('2d') c.fillStyle = 'white' c.fillRect(0, 0, this.canvas.width, this.canvas.height) c.fillStyle = 'black' c.font = '16pt sans-serif' c.fillText(`calls: ${this.renderer.info.render.calls}`, 3, 20) c.fillText(`tris : ${this.renderer.info.render.triangles}`, 3, 40) c.fillText(`fps : ${fps.toFixed(2)}`,3,60) Object.keys(this.customProps).forEach((key,i) => { const val = this.customProps[key] c.fillText(`${key} : ${val}`,3,80+i*20) }) this.cmesh.material.map.needsUpdate = true this.last = time this.lastFrame = this.renderer.info.render.frame } } setProperty(name, value) { this.customProps[name] = value } } ================================================ FILE: webpack.config.js ================================================ const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const CopyPlugin = require('copy-webpack-plugin'); const { CleanWebpackPlugin } = require('clean-webpack-plugin'); const isLibrary = !!process.env.LIBRARY let config; if (isLibrary) { console.log("Building for library") config = { entry: { 'voxeljs-next': path.join(__dirname, "src/index.js"), }, plugins:[ new CleanWebpackPlugin(), ], externals: { three: { commonjs: 'three', commonjs2: 'three', amd: 'three', }, ecsy: { commonjs: 'ecsy', commonjs2: 'ecsy', amd: 'ecsy', }, 'ecsy-three': { commonjs: 'ecsy-three', commonjs2: 'ecsy-three', amd: 'ecsy-three', }, } } } else { config = { entry: { 'voxeljs-next': path.join(__dirname, "src/index.js"), 'app': path.join(__dirname, "examples/src/index.js"), }, plugins:[ new CleanWebpackPlugin(), new HtmlWebpackPlugin({ title: 'Output', template: 'examples/public/index.html', inject: true}), new CopyPlugin([ { from: path.join(__dirname, 'examples/public'), to: path.resolve(__dirname, 'dist') }, ]), ], } } module.exports = ['source-map'].map(devtool => ({ mode: 'development', output: { path: path.resolve(__dirname, 'dist'), filename: '[name].js' }, module: { rules: [ { test: /\.css$/, use: [ 'style-loader', 'css-loader', ], }, ], }, optimization: { splitChunks: { chunks: 'all', }, }, devtool, ...config }));