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.
play full screen
play in vr
save
reload
greedy mesher
culled mesher
textures
shadows
voice
shared playing
left
right
up
down
escape
jump
full screen: mouse to move camera, arrows and WASD to move
window mode: mouse to click on things, arrows to turn, WASD to move
VR mode:
trigger to click
hold 'up' butotn to move in direction of pointer
swipe left and right to rotate camera
click left to toggle create vs destroy
click right to open block settings
voice: turn on voice to talk to others playing the game
toggle active vs creative mode with the 'c' key
================================================
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
play full screen
play in vr
================================================
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
}));