Showing preview only (240K chars total). Download the full file or copy to clipboard to get everything.
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
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptSettings">
<option name="languageLevel" value="ES6" />
</component>
</project>
================================================
FILE: .idea/modules.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/voxeljs-next.iml" filepath="$PROJECT_DIR$/.idea/voxeljs-next.iml" />
</modules>
</component>
</project>
================================================
FILE: .idea/vcs.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
================================================
FILE: .idea/voxeljs-next.iml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
================================================
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
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="../src/ecsy/fullscreen.css">
<link rel="stylesheet" href="../src/ecsy/webxr.css">
<link rel="stylesheet" href="../src/ecsy/dashboard.css">
<style type="text/css">
html, body {
padding: 0;
margin:0;
overflow: hidden;
}
</style>
</head>
<body>
<script type="module">
</script>
</body>
</html>
================================================
FILE: examples/public/index.html
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
</body>
</html>
================================================
FILE: examples/public/networked.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Mine Kitten</title>
<script>
/*
(function(f, a, t, h, o, m){
a[h]=a[h]||function(){
(a[h].q=a[h].q||[]).push(arguments)
};
o=f.createElement('script'),
m=f.getElementsByTagName('script')[0];
o.async=1; o.src=t; o.id='fathom-script';
m.parentNode.insertBefore(o,m)
})(document, window, '//stats.josh.earth/tracker.js', 'fathom');
fathom('set', 'siteId', 'GISNV');
fathom('trackPageview');
*/
</script>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<link rel="stylesheet" href="css/main.css">
</head>
<body>
<h1>VoxelJS for VR Test</h1>
<p>
Press <b>play full screen</b> in desktop mode. Press <b>play in vr</b> to play in VR mode (if available).
Or just play in regular windowed mode. Whatever you like.
</p>
<div>
<div id="progress">
<label>loading</label>
<progress id="progress-bar" value="0.5"></progress>
</div>
<button id="fullscreen">play full screen</button>
<button id="entervr">play in vr</button>
<div class="button-bar">
<button id="save">save</button>
<button id="reload">reload</button>
</div>
<div class="button-bar">
<button id="use-greedy-mesher" class="selected mesher">greedy mesher</button>
<button id="use-culled-mesher" class="mesher">culled mesher</button>
<button id="toggle-texture" class="selected">textures</button>
<button id="toggle-ao" class="selected">shadows</button>
<button id="connect-voice">voice</button>
<button id="share">shared playing</button>
</div>
</div>
<div id="container">
<div id="touch-overlay">
<button id="left">left</button>
<button id="right">right</button>
<button id="up">up</button>
<button id="down">down</button>
<button id="exit-fullscreen">escape</button>
<button id="menu-button">menu</button>
<button id="jump-button">jump</button>
</div>
</div>
<ul>
<li>full screen: mouse to move camera, arrows and WASD to move</li>
<li>window mode: mouse to click on things, arrows to turn, WASD to move</li>
<li>VR mode: <ul>
<li>trigger to click</li>
<li>hold 'up' butotn to move in direction of pointer</li>
<li>swipe left and right to rotate camera</li>
<li>click left to toggle create vs destroy</li>
<li>click right to open block settings</li>
</ul>
<li>voice: turn on voice to talk to others playing the game</li>
<li>toggle active vs creative mode with the 'c' key</li>
</ul>
<script src="../node_modules/webrtc-sdk/js/webrtc-v2.js"></script>
<script src="../node_modules/pubnub/dist/web/pubnub.js"></script>
<script type="module">
import {TextureManager} from '../src/TextureManager.js'
import {WebRTCAudioChat} from './js/WebRTCAudioChat.js'
import {RemotePlayersProxy} from './js/RemotePlayersProxy.js'
const $ = (sel) => document.querySelector(sel)
const $$ = (sel) => document.querySelectorAll(sel)
const on = (elem, type, cb) => elem.addEventListener(type,cb)
const rand = (min,max) => Math.random()*(max-min) + min
const app = new WebXRBoilerPlate({
container: $("#container")
})
app.init().then((app) => {
app.scene.background = new Color( 0xcccccc );
app.comps = []
app.textureManager = new TextureManager()
app.comps.push(app.textureManager)
app.player = new Mesh()
app.player.position.y = 30
app.player_phys = new PhysHandler(app, app.player,[new SimpleMeshCollider(app)])
app.comps.push(player_phys)
// ========= event handlers
//Enable VR button
//when VR support is detected
on(app,'detected',()=>{
// show the enter VR button
$("#entervr").style.display = 'block'
on($("#entervr"),'click',()=> {
app.desktopControls.disable()
app.vrControls.enable()
app.enterVR()
})
})
// WebRTC Voice connection
app.webrtcAudio = new WebRTCAudioChat(app)
on($("#connect-voice"),'click',() => {
if(app.webrtcAudio.connected) {
$("#connect-voice").classList.remove('selected')
app.webrtcAudio.disconnect()
} else {
$("#connect-voice").classList.add('selected')
app.webrtcAudio.connect()
}
})
// ============= Network Play ================
app.networkPlay = new PubnubNetworkplay()
const shared_button = $("#share")
on(shared_button,'click',()=>{
app.networkPlay.connect()
if(app.networkPlay.isConnected() || app.networkPlay.isConnecting()) {
shared_button.classList.add('selected')
} else {
shared_button.classList.remove('selected')
}
app.rebuildAllMeshes()
})
app.remotePlayers = new RemotePlayersProxy(app)
on(app.player_phys,"move",()=>{
if(app.networkPlay) app.networkPlay.playerMoved(app.player_phys)
})
on(app.networkPlay,'remote-player-moved',(msg)=>{
console.log(msg.publisher,'moved to',msg.message.position)
app.remotePlayers.remotePlayerMoved(msg.publisher,msg.message.position)
})
on(app.networkPlay,'remote-player-voxels',(msg) => {
console.log(msg.publisher,'changed voxels',msg.message.voxels)
msg.message.voxels.forEach(voxel => {
const pos = new Vector3(voxel.position.x,voxel.position.y,voxel.position.z)
app.chunkManager.setVoxelAtCoordinates(pos, voxel.type)
const chunkIndex = app.chunkManager.chunkIndexAtCoordinates(pos.x,pos.y,pos.z)
const chunk = app.chunkManager.chunks[chunkIndex.join("|")]
if(chunk) rebuildMesh(chunk)
})
})
return app.textureManager.load(['grass','brick','dirt','anim','tnt','arrows'])
}).then(()=>{
app.rebuildAllMeshes()
}).catch(e => {
console.log('ERROR',e)
})
</script>
</body>
</html>
================================================
FILE: examples/simple.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Mine Kitten</title>
<script>
/*
(function(f, a, t, h, o, m){
a[h]=a[h]||function(){
(a[h].q=a[h].q||[]).push(arguments)
};
o=f.createElement('script'),
m=f.getElementsByTagName('script')[0];
o.async=1; o.src=t; o.id='fathom-script';
m.parentNode.insertBefore(o,m)
})(document, window, '//stats.josh.earth/tracker.js', 'fathom');
fathom('set', 'siteId', 'GISNV');
fathom('trackPageview');
*/
</script>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<link rel="stylesheet" href="css/main.css">
<style>
#texture {
border: 0 solid green;
image-rendering: -moz-crisp-edges;
}
</style>
</head>
<body>
<h1>VoxelJS for VR Test</h1>
<p>
Press <b>play full screen</b> in desktop mode. Press <b>play in vr</b> to play in VR mode (if available).
Or just play in regular windowed mode. Whatever you like.
</p>
<p>Textures CC0 licensed from <a href="https://www.kenney.nl/assets/voxel-pack">Kenney.nl</a>
</p>
<div>
<div id="progress">
<label>loading</label>
<progress id="progress-bar" value="0.5"></progress>
</div>
<button id="fullscreen">play full screen</button>
<button id="entervr">play in vr</button>
</div>
<div id="container">
<div id="touch-overlay">
<button id="left">left</button>
<button id="right">right</button>
<button id="up">up</button>
<button id="down">down</button>
<button id="jump-button">jump</button>
</div>
</div>
<script src="../node_modules/atlaspack/index.js"></script>
<script type="module">
import WebXRBoilerPlate, {FULLSCREEN_ENTERED, FULLSCREEN_EXITED} from "../node_modules/webxr-boilerplate/WebXRBoilerPlate.js"
import {Mesh, MeshLambertMaterial, PlaneGeometry,
CubeGeometry,
Color, DirectionalLight, AmbientLight, Vector3, TextureLoader, Group} from "../node_modules/three/build/three.module.js"
import {TextureManager} from '../src/TextureManager.js'
import {PhysHandler} from '../src/PhysHandler.js'
import {SimpleMeshCollider} from "../src/SimpleMeshCollider.js"
import {ChunkManager} from "../src/ChunkManager.js"
import {GreedyMesher} from "../src/GreedyMesher.js"
import {CulledMesher} from "../src/CulledMesher.js"
import {BlockPicker} from './js/BlockPicker.js'
import {VRControls} from "../src/VRControls.js"
import {DesktopControls} from '../src/DesktopControls.js'
import {FullScreenControls} from '../src/FullscreenControls.js'
import {KeyboardControls} from '../src/KeyboardControls.js'
import {TouchControls} from '../src/TouchControls.js'
import {generateChunkInfoFromFunction, toRad} from '../src/utils.js'
import {VoxelMesh} from "../src/VoxelMesh.js"
//JQuery-like selectors
const $ = (sel) => document.querySelector(sel)
const on = (elem, type, cb) => elem.addEventListener(type,cb)
const app = new WebXRBoilerPlate({
container: $("#container")
})
app.init().then((app) => {
// app.CHUNK_SIZE = 16
// app.RAYCAST_DISTANCE = 30
app.comps = []
app.scene.background = new Color( 0xcccccc );
app.aoEnabled = true
app.active = true
//force the ThreeJS loading framework to have at least one thing in the que
new TextureLoader().load('./dummy.jpg')
app.player = new Mesh()
app.player.position.y = 30
//physics for the player
app.player_phys = new PhysHandler(app, app.player,[new SimpleMeshCollider(app)])
app.comps.push(app.player_phys)
// app.player_phys.enable()
// function to generate a flat world
const flat = (i,j,k) => {
//an gap in the floor made of air
// if(j <1 && k < -5 && k > -10 ) return 0
//the floor is brick, from depth 0 to -10
if(j < 1 && j > -10) return 1
//move back 10
k+=20
// a dome
if((i*i + j*j + k*k) < 80) {
return 2
}
//nothing else in the world
return 0
}
const wide_block = (i,j,k) => {
if(i>=-4 && i<0 && j===1 && k===6) return 1
return 0
}
// ========== setup the chunk manager and cache
app.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, flat)
},
container: new Group(),
textureManager: new TextureManager({aoEnabled:true}),
},app);
app.comps.push(app.chunkManager.textureManager)
//start loading these textures
app.chunkManager.textureManager.loadTextures([
{
src:'./textures/kenneynl/tiles/grass_top.png'
},
{
src:'./textures/kenneynl/tiles/dirt.png'
},
// {
// src:'./textures/kenneynl/tiles/ice.png',
// },
{
src:'./textures/kenneynl/tiles/lava.png',
},
{
src:'./textures/kenneynl/tiles/stone.png',
},
{
src:'./textures/kenneynl/tiles/sand.png',
},
{
src:'./textures/tnt.png',
},
{
src:'./textures/heart.png',
},
{
src:'./textures/tnt.png',
},
]).then(()=>{
app.chunkManager.rebuildAllMeshes()
app.chunkManager.requestMissingChunks(new Vector3(0,0,0))
app.dialog.setSelectedToDefault()
})
// ======== setup chunk manipulation functions =======
//set the mesher to use
// app.mesher = new GreedyMesher()
// app.mesher = new CulledMesher()
// create a block with the currently selected block type
app.createBlock = (pos) => {
if(app.dialog.panel.visible) return
const name = app.dialog.selectedColor
if(name === 'tnt') {
// console.log("doing explosion carve out at",pos)
pos.y -= 1
pos.x -= 1
pos.z -= 1
const dim = new Vector3(3,3,3)
const data = [] //dim.x*dim.y*dim.z
for(let y=0; y<dim.y; y++) {
for(let z=0; z<dim.z; z++) {
for(let x=0; x<dim.x; x++) {
const n = x + z*dim.x + y*dim.x*dim.z
data[n] = 0
}
}
}
app.chunkManager.setBlockRange(pos,dim,data)
} else {
const type = app.chunkManager.textureManager.getBlockTypeForName(name)
app.setBlock(pos, type)
}
}
// remove a block at the position
app.removeBlock = (pos) => {
if(app.dialog.panel.visible) return
pos.floor()
app.setBlock(pos, 0)
}
// actually set the block
app.setBlock = function(pos, value) {
pos.floor()
//set the actual value
this.chunkManager.setVoxelAtCoordinates(pos, value)
}
// ===== setup the camera groups =============
app.stageRot = new Group()
app.scene.add(app.stageRot)
app.stagePos = new Group()
app.stageRot.add(app.stagePos)
app.stagePos.position.z = -10
app.stagePos.position.y = -1.5
app.stagePos.position.x = 0
app.stagePos.add(app.chunkManager.container)
// ======== EVENT HANDLERS ==========
// ========= crosshairs for full screen mode =========
// TODO: move this into full screen mode
const crosshairs_geometry = new PlaneGeometry(0.1,0.01)
crosshairs_geometry.merge(new PlaneGeometry(0.01,0.1))
const crosshairs = new Mesh( crosshairs_geometry, new MeshLambertMaterial({color:'red'}))
crosshairs.position.set(0,1.6,-1.0)
app.scene.add(crosshairs)
crosshairs.visible = false
/// ============= Highlight Object ==========
app.highlight_cube = new Mesh(
new CubeGeometry(1.1,1.1,1.1, 4,4).translate(0.5,0.5,0.5),
new MeshLambertMaterial({color:'green',
depthTest:true,
wireframe:true,
wireframeLinewidth: 3,
transparent: true,
opacity: 0.5,
}))
app.highlight_cube.position.set(-2,0,-5)
app.stagePos.add(app.highlight_cube)
app.moveHighlight = (pos) => {
if(app.dialog.panel.visible) return
app.highlight_cube.position.copy(pos)
}
//a standard directional light from above
const light = new DirectionalLight( 0xffffff, 1.0 );
light.position.set( 0, 10, 5 ).normalize();
app.scene.add( light );
//a standard ambient light
app.scene.add(new AmbientLight(0xffffff,0.3))
// a dialog to choose a different block type
app.dialog = new BlockPicker(app)
app.dialog.panel.position.set(0,1.5,-1)
app.dialog.panel.visible = false
app.toggleDialog = () => {
app.dialog.rebuild()
app.dialog.panel.visible = !app.dialog.panel.visible
}
app.destroyBlockActive = true
app.togglePointer = () => {
app.destroyBlockActive = !app.destroyBlockActive
if(app.destroyBlockActive) {
app.highlight_cube.material.color.set(0xff0000)
if(!app.vrControls.pointer.controller1.line) {
console.log('warning. no line on controller1')
} else {
app.vrControls.pointer.controller1.line.material.color.set(0x880000)
}
} else {
app.highlight_cube.material.color.set(0x00ff00)
if(!app.vrControls.pointer.controller1.line) {
console.log('warning. no line on controller1')
} else {
app.vrControls.pointer.controller1.line.material.color.set(0x008800)
}
}
}
// ========= Setup VR Controls
app.vrControls = new VRControls(app)
app.comps.push(app.vrControls)
on(app.vrControls,'highlight',(e) => {
if(app.destroyBlockActive) {
app.moveHighlight(e.hitPosition)
} else {
e.hitPosition.add(e.hitNormal)
app.moveHighlight(e.hitPosition)
}
})
on(app.vrControls,'trigger',(a) => {
if(app.destroyBlockActive) {
app.removeBlock(a.hitPosition)
} else {
a.hitPosition.add(a.hitNormal)
app.createBlock(a.hitPosition)
}
})
on(app.vrControls,'toggle-pointer',app.togglePointer)
on(app.vrControls,'show-dialog',app.toggleDialog)
// =========== setup the desktop controls ===========
app.desktopControls = new DesktopControls(app,30)
app.comps.push(app.desktopControls)
on(app.desktopControls,'highlight',app.moveHighlight)
on(app.desktopControls,'setblock',app.createBlock)
on(app.desktopControls,'removeblock',app.removeBlock)
app.desktopControls.enable()
// ================ setup the full screen controls
app.fullscreenControls = new FullScreenControls(app)
app.comps.push(app.fullscreenControls)
on(app.fullscreenControls,'highlight', app.moveHighlight)
on(app.fullscreenControls,'setblock', app.createBlock)
on(app.fullscreenControls,'removeblock',app.removeBlock)
// on click, if fullscreen support, then really enter fullscreen, else fake it with CSS
on($("#fullscreen"),'click',()=> {
if(app.isFullscreenSupported()) {
crosshairs.visible = true
app.desktopControls.disable()
app.playFullscreen()
} else {
$("body").classList.add('fullscreen')
app.resizeOnNextRepaint = true
}
})
on(app,FULLSCREEN_ENTERED,() => {
$("#touch-overlay").style.visibility = 'visible'
console.log("really entered fullscreen")
$("body").classList.add('fullscreen')
app.fullscreenControls.enable()
app.resizeOnNextRepaint = true
})
on(app,FULLSCREEN_EXITED,() => {
$("#touch-overlay").style.visibility = 'hidden'
console.log("really exited fullscreen")
$("body").classList.remove('fullscreen')
app.fullscreenControls.disable()
app.desktopControls.enable()
app.resizeOnNextRepaint = true
})
// ============= setup the keyboard controls =========
app.keyboardControls = new KeyboardControls(app)
app.comps.push(app.keyboardControls)
on(app.keyboardControls, 'show-dialog',app.toggleDialog)
app.keyboardControls.enable()
// ============ setup the touch controls =============
app.touchControls = new TouchControls(app)
app.comps.push(app.touchControls)
on(app.touchControls,'highlight',app.moveHighlight)
on(app.touchControls,'setblock', app.createBlock)
on(app.touchControls,'removeblock',app.removeBlock)
on(app.touchControls,'show-dialog',app.toggleDialog)
if(app.touchControls.isTouchEnabled()) {
app.touchControls.enable()
}
on($("#jump-button"),'touchstart',(e)=> {
e.preventDefault()
e.stopPropagation()
app.player_phys.startJump()
})
on($("#jump-button"),'touchmove',(e)=> {
e.preventDefault()
e.stopPropagation()
})
on($("#jump-button"),'touchend',(e)=> {
e.preventDefault()
e.stopPropagation()
app.player_phys.endJump()
})
// things to do on every render tick
// this is the render loop
let lastTime = 0
let lastPos = new Vector3(0,0,0)
app.onRender((time, app)=> {
const dt = time-lastTime
lastTime = time
//check every frame if the player has moved.
// if so tell the chunk manager to update any needed chunks
if(!lastPos.equals(app.stagePos.position)) {
app.chunkManager.updateCenterPosition(app.stagePos.position.clone().multiplyScalar(-1))
}
lastPos.copy(app.stagePos.position)
app.comps.forEach(comp => {
if(comp.isEnabled()) comp.update(time,dt)
})
})
//update progress indicator while loading
on(app,'progress',(prog)=> $("#progress").setAttribute('value',100*prog))
//when all assets are loaded, hide progbar
on(app,'loaded',()=>{
// hide the loading progress bar
$("#progress").style.display = 'none'
//show the fullscreen button
$("#fullscreen").style.display = 'block'
})
//when VR support is detected
on(app,'detected',()=>{
// show the enter VR button
$("#entervr").style.display = 'block'
on($("#entervr"),'click',()=> {
app.desktopControls.disable()
app.vrControls.enable()
app.enterVR()
})
})
})
.catch(e => console.log(e))
</script>
</body>
</html>
================================================
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()<radius) {
actual.copy(pos)
actual.add(cursor)
this.app.setBlock(actual,0)
}
}
}
}
const chunkIndex = this.app.chunkManager.chunkIndexAtCoordinates(pos.x,pos.y,pos.z)
const chunk = this.app.chunkManager.chunks[chunkIndex.join("|")]
if(chunk) this.app.rebuildMesh(chunk)
}
this.app.explosionParticles.fire(pos)
}
}
================================================
FILE: examples/src/PersistenceManager.js
================================================
function GET_JSON(url) {
return fetch(url+`?cachebust=${Math.random()}`)
.then(res => 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<chunk.data.voxels.length; k++) {
const val = chunk.data.voxels[k]
const vx = k%512
const vy = Math.floor(k/512) + i*8
const n = (vy*512 + vx)*4
data.data[n+0] = 0
data.data[n+1] = 0
data.data[n+2] = val
data.data[n+3] = 255
}
info.imageCoords = {
x:0,
y:i*8,
width:512,
height:8,
}
output.chunks.push(info)
})
ctx.putImageData(data,0,0)
// document.body.appendChild(canvas)
output.image = canvas.toDataURL('png')
const url = BASE_URL+'foozoo88'
return POST_JSON(url,output)
}
load(chunkManager) {
const url = BASE_URL+'foozoo88'
return GET_JSON(url).then(data => {
// 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<dim.y; y++) {
for(let z=0; z<dim.z; z++) {
for(let x=0; x<dim.x; x++) {
const n = x + z*dim.x + y*dim.x*dim.z
const val = data[n]
pt.x = pos.x + x
pt.y = pos.y + y
pt.z = pos.z + z
if(val !== -1) this.setVoxelAtCoordinates(pt, val)
}
}
}
this.rebuildMesh(chunk)
}
}
//get voxel at position in world coordinates
voxelAtPosition(pos, val) {
return this.voxelAtCoordinates(pos.divideScalar(this.blockSize).floor(),val)
}
//report the number of chunks currently loaded into memory
debug_getChunksLoadedCount() {
return Object.keys(this.chunks).length
}
/**
* remove chunks that are too far away
* _pos_ is the center of the chunks to look at
* _group_ is the ThreeJS group that the chunks are stored in
*/
removeFarChunks(pos) {
const nearbyChunks = this.nearbyChunks(pos,this.distance+1).map(chunkPos => 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<ex; j++) {
const vox = voxels.voxelAtCoordinates(new Vector3(sx, i, sz))
// console.log("hit", vox)
if (vox > 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<ex; j++) {
const vox = voxels.voxelAtCoordinates(new Vector3(j, sy, sz))
if (vox > 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<ez; k++) {
const vox = voxels.voxelAtCoordinates(new Vector3(sx, sy, k))
if (vox > 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<gamepads.length; i++) {
const gamepad = gamepads[i]
if ( gamepad && ( gamepad.id === 'Daydream Controller' ||
gamepad.id === 'Gear VR Controller' || gamepad.id === 'Oculus Go Controller' ||
gamepad.id === 'OpenVR Gamepad' || gamepad.id.startsWith( 'Oculus Touch' ) ||
gamepad.id.startsWith( 'Spatial Controller' ) ) ) {
//we should have at least two buttons
if(gamepad.buttons.length < 2) return
this.updateGamepad(gamepad, time)
}
}
}
updateGamepad(gamepad, time) {
// const touchpad = gamepad.buttons[0]
// console.log("axes", gamepad.axes[0], gamepad.axes[1])
if(!this.states[gamepad.id]) {
this.states[gamepad.id] = {}
}
let newDir = DIRS.NONE
console.log(gamepad.axes[1])
if(gamepad.axes[1] < -0.5) newDir = DIRS.UP
if(gamepad.axes[1] > +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) {
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
SYMBOL INDEX (358 symbols across 44 files)
FILE: examples/src/BlockPicker.js
class BlockTypeButton (line 8) | class BlockTypeButton extends Button2D {
method draw (line 9) | draw(ctx) {
class BlockPicker (line 42) | class BlockPicker {
method constructor (line 43) | constructor(app) {
method rebuild (line 54) | rebuild() {
method setSelectedToDefault (line 102) | setSelectedToDefault() {
FILE: examples/src/ExplosionParticles.js
class ExplosionParticles (line 6) | class ExplosionParticles extends ECSComp {
method constructor (line 7) | constructor(app) {
method fire (line 44) | fire(pos) {
method update (line 53) | update(time, dt) {
FILE: examples/src/GPUParticleSystem.js
constant UPDATEABLE_ATTRIBUTES (line 84) | const UPDATEABLE_ATTRIBUTES = [
class GPUParticleSystem (line 90) | class GPUParticleSystem extends Object3D {
method constructor (line 91) | constructor(options) {
method geometryUpdate (line 186) | geometryUpdate () {
method random (line 207) | random () {
method update (line 211) | update ( ttime ) {
method dispose (line 218) | dispose () {
method spawnParticle (line 233) | spawnParticle ( options ) {
FILE: examples/src/ItemManager.js
class ItemManager (line 4) | class ItemManager extends ECSComp {
method constructor (line 5) | constructor(app) {
method isBlockTypeItem (line 10) | isBlockTypeItem(type) {
method removeItem (line 15) | removeItem(pos) {
FILE: examples/src/PersistenceManager.js
function GET_JSON (line 2) | function GET_JSON(url) {
function POST_JSON (line 14) | function POST_JSON(url,data) {
function loadImageFromURL (line 31) | function loadImageFromURL(url) {
constant BASE_URL (line 44) | const BASE_URL = "https://vr.josh.earth/360/doc/"
class PersistenceManager (line 46) | class PersistenceManager {
method constructor (line 47) | constructor() {
method save (line 51) | save(chunkManager, cache) {
method load (line 103) | load(chunkManager) {
FILE: examples/src/PigComp.js
class PigComp (line 7) | class PigComp extends ECSComp {
method constructor (line 8) | constructor(app) {
method update (line 26) | update(time,dt) {
method collide (line 30) | collide(phys, target, pos, diff) {
FILE: examples/src/PubnubNetworkplay.js
constant CHANNEL (line 7) | const CHANNEL = 'beta-movement'
constant SEND_INTERVAL (line 8) | const SEND_INTERVAL = 333;
class PubnubNetworkplay (line 9) | class PubnubNetworkplay extends ECSComp {
method constructor (line 10) | constructor() {
method connect (line 75) | connect() {
method isConnected (line 82) | isConnected() {
method isConnecting (line 86) | isConnecting() {
method playerMoved (line 90) | playerMoved(phys) {
method playerSetVoxel (line 93) | playerSetVoxel(pos,type) {
FILE: examples/src/RemotePlayersProxy.js
constant BUMP_HEIGHT (line 4) | const BUMP_HEIGHT = new Vector3(0,1,0)
class RemotePlayersProxy (line 5) | class RemotePlayersProxy extends ECSComp {
method constructor (line 6) | constructor(app) {
method remotePlayerMoved (line 11) | remotePlayerMoved(id,pos) {
FILE: examples/src/SmashParticles.js
class SmashParticles (line 6) | class SmashParticles extends ECSComp {
method constructor (line 7) | constructor(app) {
method fire (line 44) | fire(pos) {
method update (line 54) | update(time, dt) {
FILE: examples/src/WebRTCAudioChat.js
class WebRTCAudioChat (line 7) | class WebRTCAudioChat {
method constructor (line 8) | constructor(app) {
method connect (line 12) | connect() {
method disconnect (line 45) | disconnect() {
FILE: examples/src/index.js
class VoxelWebXRControllerSystem (line 34) | class VoxelWebXRControllerSystem extends System {
method execute (line 35) | execute(delta, time) {
FILE: src/ChunkManager.js
class Chunk (line 5) | class Chunk {
method constructor (line 6) | constructor(data, pos, chunkBits) {
method voxelIndexFromCoordinates (line 18) | voxelIndexFromCoordinates(x, y, z) {
method voxelAtCoordinates (line 24) | voxelAtCoordinates(pt) {
method setVoxelAtCoordinates (line 28) | setVoxelAtCoordinates(pt, val) {
method dispose (line 35) | dispose() {
constant SCALE (line 45) | const SCALE = new Vector3(1.0,1.0,1.0)
class ChunkManager (line 47) | class ChunkManager {
method constructor (line 48) | constructor(opts) {
method on (line 73) | on(type, cb) {
method emit (line 77) | emit(type,evt) {
method clear (line 82) | clear() {
method nearbyChunks (line 95) | nearbyChunks(position, distance) {
method requestMissingChunks (line 113) | requestMissingChunks(pos) {
method getBounds (line 121) | getBounds(x, y, z) {
method generateChunk (line 129) | generateChunk(pos) {
method makeChunkFromData (line 143) | makeChunkFromData(info,voxels) {
method chunkIndexAtCoordinates (line 156) | chunkIndexAtCoordinates(x, y, z) {
method chunkAtPosition (line 165) | chunkAtPosition(position) {
method voxelIndexFromCoordinates (line 170) | voxelIndexFromCoordinates(x, y, z) {
method voxelAtCoordinates (line 177) | voxelAtCoordinates(pt) {
method setVoxelAtCoordinates (line 184) | setVoxelAtCoordinates(pt, val) {
method setBlockRange (line 193) | setBlockRange(pos, dim, data) {
method voxelAtPosition (line 216) | voxelAtPosition(pos, val) {
method debug_getChunksLoadedCount (line 221) | debug_getChunksLoadedCount() {
method removeFarChunks (line 230) | removeFarChunks(pos) {
method getBlock (line 246) | getBlock(x,y,z) {
method rebuildMesh (line 250) | rebuildMesh(chunk) {
method rebuildAllMeshes (line 258) | rebuildAllMeshes() {
method updateCenterPosition (line 262) | updateCenterPosition(pos) {
FILE: src/CulledMesher.js
class CulledMesher (line 1) | class CulledMesher {
method constructor (line 2) | constructor()
method mesh (line 8) | mesh(volume, dims)
FILE: src/DesktopControls.js
constant LEFT_MOUSE_BUTTON (line 6) | const LEFT_MOUSE_BUTTON = 1
constant RIGHT_MOUSE_BUTTON (line 7) | const RIGHT_MOUSE_BUTTON = 2
class DesktopControls (line 9) | class DesktopControls extends ECSComp {
method constructor (line 11) | constructor(app, distance) {
method enable (line 51) | enable() {
method disable (line 55) | disable() {
method update (line 59) | update(time) {
FILE: src/ECSComp.js
class ECSComp (line 1) | class ECSComp {
method constructor (line 2) | constructor() {
method addEventListener (line 6) | addEventListener(type, cb) {
method _fire (line 11) | _fire(type,payload) {
method enable (line 16) | enable() {
method disable (line 20) | disable() {
method isEnabled (line 24) | isEnabled() {
method update (line 28) | update(time) {
FILE: src/FullscreenControls.js
constant HAS_POINTER_LOCK (line 6) | const HAS_POINTER_LOCK = 'pointerLockElement' in document ||
function requestPointerLock (line 11) | function requestPointerLock(el) {
class FullScreenControls (line 17) | class FullScreenControls extends ECSComp {
method constructor (line 18) | constructor(app) {
method traceRay (line 69) | traceRay() {
method enable (line 87) | enable() {
method disable (line 99) | disable() {
FILE: src/GreedyMesher.js
class GreedyMesher (line 3) | class GreedyMesher {
method constructor (line 4) | constructor() {
method mesh (line 7) | mesh(volume, dims) {
FILE: src/KeyboardControls.js
constant Y_AXIS (line 4) | const Y_AXIS = new Vector3(0,1,0)
constant SPEED (line 5) | const SPEED = 0.1
class KeyboardControls (line 7) | class KeyboardControls extends ECSComp {
method constructor (line 8) | constructor(app) {
method update (line 45) | update(time) {
method rotateLeft (line 72) | rotateLeft() {
method rotateRight (line 76) | rotateRight() {
method getSpeedDirection (line 80) | getSpeedDirection() {
method glideForward (line 85) | glideForward() {
method glideBackward (line 91) | glideBackward() {
method glideLeft (line 97) | glideLeft() {
method glideRight (line 103) | glideRight() {
method glideUp (line 110) | glideUp() {
method glideDown (line 117) | glideDown() {
FILE: src/PhysHandler.js
constant GRAVITY (line 3) | const GRAVITY = new Vector3(0,-9.8,0)
class PhysHandler (line 5) | class PhysHandler extends ECSComp {
method constructor (line 6) | constructor(app, target, colliders) {
method isFlying (line 15) | isFlying() {
method startFlying (line 18) | startFlying() {
method endFlying (line 21) | endFlying() {
method startJump (line 24) | startJump() {
method endJump (line 31) | endJump() {
method markChanged (line 34) | markChanged() {
method update (line 37) | update(time,dt) {
FILE: src/SimpleMeshCollider.js
constant SIZE (line 3) | const SIZE = new Vector3()
constant CENTER (line 4) | const CENTER = new Vector3()
function checkHitTileY (line 6) | function checkHitTileY(voxels, bounds, pos) {
function checkHitTileX (line 24) | function checkHitTileX(voxels, bounds, pos) {
function checkHitTileZ (line 36) | function checkHitTileZ(voxels, bounds, pos) {
class SimpleMeshCollider (line 49) | class SimpleMeshCollider {
method constructor (line 50) | constructor(app) {
method collide (line 53) | collide(phys, target, pos, diff) {
FILE: src/TextureManager.js
class TextureManager (line 23) | class TextureManager {
method constructor (line 24) | constructor(opts) {
method packImage (line 108) | packImage(img,index) {
method isEnabled (line 147) | isEnabled() {
method update (line 151) | update(ttime) {
method lookupUVsForBlockType (line 157) | lookupUVsForBlockType(typeNum) {
method lookupInfoForBlockType (line 187) | lookupInfoForBlockType(typeNum) {
method getBlockTypeForName (line 194) | getBlockTypeForName(name) {
method loadTextures (line 200) | loadTextures(infos) {
function ext (line 224) | function ext(name) {
FILE: src/TouchControls.js
constant Y_AXIS (line 5) | const Y_AXIS = new Vector3(0,1,0)
constant SPEED (line 6) | const SPEED = 0.1
class TouchControls (line 9) | class TouchControls extends ECSComp {
method isTouchEnabled (line 10) | isTouchEnabled() {
method constructor (line 13) | constructor(app, distance, chunkManager) {
method update (line 174) | update() {
method enable (line 180) | enable() {
method disable (line 187) | disable() {
method glideForward (line 196) | glideForward() {
method glideBackward (line 202) | glideBackward() {
method getSpeedDirection (line 208) | getSpeedDirection() {
method glideLeft (line 213) | glideLeft() {
method glideRight (line 219) | glideRight() {
FILE: src/VRControls.js
constant Y_AXIS (line 8) | const Y_AXIS = new Vector3(0,1,0)
constant SPEED (line 9) | const SPEED = 0.1
constant TRIGGER (line 11) | const TRIGGER = 'trigger'
class VRControls (line 13) | class VRControls extends ECSComp {
method constructor (line 15) | constructor(app) {
method traceRay (line 36) | traceRay() {
method rotateLeft (line 54) | rotateLeft() {
method rotateRight (line 57) | rotateRight() {
method getSpeedDirection (line 60) | getSpeedDirection() {
method glideBackward (line 68) | glideBackward() {
method glideForward (line 71) | glideForward() {
method update (line 75) | update(time) {
method updateCursor (line 80) | updateCursor(time) {
method scanGamepads (line 87) | scanGamepads(time) {
method updateGamepad (line 103) | updateGamepad(gamepad, time) {
FILE: src/VRStats.js
class VRStats (line 6) | class VRStats extends ECSComp {
method constructor (line 7) | constructor(app) {
method update (line 37) | update(time) {
method setProperty (line 63) | setProperty(name, value) {
FILE: src/VoxelMesh.js
function generateAmbientOcclusion (line 17) | function generateAmbientOcclusion(grid) {
function generateGrid (line 26) | function generateGrid(chunkManager,pos,indexes,vertices) {
class VoxelMesh (line 69) | class VoxelMesh {
method constructor (line 70) | constructor(chunk, mesher, scaleFactor, chunkManager) {
method createSurfaceMesh (line 281) | createSurfaceMesh(material) {
method faceVertexUv (line 288) | faceVertexUv(i) {
function vertexAO (line 363) | function vertexAO(side1, side2, corner) {
FILE: src/VoxelTexture.js
class VoxelTexture (line 17) | class VoxelTexture {
method constructor (line 18) | constructor(opts) {
method load (line 88) | load(names, done) {
method pack (line 116) | pack(name, done) {
method find (line 149) | find(name) {
method _expandName (line 164) | _expandName(name) {
method _afterLoading (line 179) | _afterLoading() {
method _powerof2 (line 205) | _powerof2(done) {
method paint (line 227) | paint(mesh, materials) {
method sprite (line 304) | sprite(name, w, h, cb) {
method animate (line 354) | animate(mesh, names, delay) {
method tick (line 369) | tick(dt) {
function uvrot (line 374) | function uvrot(coords, deg) {
function uvinvert (line 385) | function uvinvert(coords) {
function ext (line 390) | function ext(name) {
function defaults (line 394) | function defaults(obj) {
function each (line 401) | function each(arr, it, done) {
FILE: src/ecsy/camera_gimbal.js
class StagePosition (line 1) | class StagePosition {}
class StageRotation (line 2) | class StageRotation {}
FILE: src/ecsy/dashboard.js
class DomDashboard (line 6) | class DomDashboard extends Component {
method constructor (line 7) | constructor() {
class DashboardVisible (line 13) | class DashboardVisible extends Component {
class DashboardDOMOvleraySystem (line 17) | class DashboardDOMOvleraySystem extends System {
method execute (line 18) | execute(delta, time) {
FILE: src/ecsy/fullscreen.js
class FullscreenMode (line 3) | class FullscreenMode extends Component {
class FullscreenButton (line 6) | class FullscreenButton extends Component {
class FullscreenSystem (line 31) | class FullscreenSystem extends System {
method execute (line 32) | execute(delta, time) {
FILE: src/ecsy/highlight.js
class Highlight (line 11) | class Highlight {
class ActiveBlock (line 13) | class ActiveBlock {
method constructor (line 14) | constructor() {
class HighlightSystem (line 20) | class HighlightSystem extends System {
method init (line 21) | init() {
method traceRayAtScreenCoords (line 23) | traceRayAtScreenCoords(
method execute (line 56) | execute(delta,time) {
FILE: src/ecsy/input.js
class InputFrame (line 18) | class InputFrame extends Component {
method constructor (line 19) | constructor() {
constant Y_AXIS (line 47) | const Y_AXIS = new Vector3(0,1,0)
constant Z_AXIS (line 48) | const Z_AXIS = new Vector3(0,0,1)
constant SPEED (line 49) | const SPEED = 0.1
class VoxelPlayerSystem (line 51) | class VoxelPlayerSystem extends System {
method init (line 52) | init() {
method execute (line 55) | execute(delta, time) {
FILE: src/ecsy/keyboard.js
class KeyboardBindingSet (line 4) | class KeyboardBindingSet extends Component {
method constructor (line 5) | constructor() {
class KeyboardSystem (line 11) | class KeyboardSystem extends System {
method _setBindingValue (line 12) | _setBindingValue(keyboard_key, new_value) {
method _keydown (line 25) | _keydown(e) {
method _keyup (line 28) | _keyup(e) {
method init (line 31) | init() {
FILE: src/ecsy/mouse.js
class MouseCursor (line 5) | class MouseCursor extends Component {
method constructor (line 6) | constructor() {
class MouseSystem (line 13) | class MouseSystem extends System {
method _mouse_down (line 14) | _mouse_down(e) {
method _mouse_move (line 25) | _mouse_move(e) {
method _mouse_up (line 36) | _mouse_up(e) {
method init (line 53) | init() {
method execute (line 63) | execute(delta,time) {
FILE: src/ecsy/voxels.js
class VoxelLandscape (line 9) | class VoxelLandscape extends Component {
method constructor (line 10) | constructor() {
class VoxelTextures (line 16) | class VoxelTextures extends Component {
method constructor (line 17) | constructor() {
class VoxelSystem (line 23) | class VoxelSystem extends System {
method execute (line 24) | execute(delta, time) {
FILE: src/ecsy/webxr.js
class WebXRActive (line 4) | class WebXRActive extends Component {
class WebXRButton (line 7) | class WebXRButton extends Component {
method constructor (line 8) | constructor() {
class WebXRController (line 14) | class WebXRController extends Component {
method constructor (line 15) | constructor() {
class WebXRSystem (line 22) | class WebXRSystem extends System {
method enterWebXR (line 23) | enterWebXR(ent) {
method execute (line 44) | execute(delta, time) {
FILE: src/physical.js
constant WORLD_DESIRED (line 7) | const WORLD_DESIRED = new Vector3(0, 0, 0)
constant DESIRED (line 7) | const WORLD_DESIRED = new Vector3(0, 0, 0)
constant START (line 7) | const WORLD_DESIRED = new Vector3(0, 0, 0)
constant END (line 7) | const WORLD_DESIRED = new Vector3(0, 0, 0)
constant DIRECTION (line 7) | const WORLD_DESIRED = new Vector3(0, 0, 0)
constant LOCAL_ATTRACTOR (line 7) | const WORLD_DESIRED = new Vector3(0, 0, 0)
constant TOTAL_FORCES (line 7) | const WORLD_DESIRED = new Vector3(0, 0, 0)
function applyTo (line 15) | function applyTo(which) {
class Physical (line 29) | class Physical {
method constructor (line 31) | constructor(avatar, collidables, dimensions, terminal) {
method tick (line 75) | tick(dt) {
method subjectTo (line 240) | subjectTo (force) {
method removeForce (line 249) | removeForce (force) {
method attractTo (line 258) | attractTo (vector, mass) {
method aabb (line 265) | aabb () {
method collide (line 277) | collide (other, bbox, world_vec, resting) {
method atRestX (line 283) | atRestX () {
method atRestY (line 289) | atRestY () {
method atRestZ (line 293) | atRestZ () {
method fell (line 297) | fell (distance) {
FILE: src/raycast.js
function traceRay_impl (line 1) | function traceRay_impl(
function startTraceRay (line 188) | function startTraceRay(voxelProvider, origin, direction, max_d, hit_pos,...
FILE: src/utils.js
function toHexColor (line 4) | function toHexColor(num) {
function generateChunkInfoFromFunction (line 11) | function generateChunkInfoFromFunction(l, h, f) {
constant EPSILON (line 32) | const EPSILON = 1e-8
constant DIRS (line 37) | const DIRS = {
function traceRayAtScreenCoords (line 48) | function traceRayAtScreenCoords(app, pt, distance) {
FILE: src/webxr-boilerplate/BackgroundAudioLoader.js
class BackgroundAudioLoader (line 5) | class BackgroundAudioLoader {
method constructor (line 6) | constructor(manager) {
method load (line 10) | load( url, onLoad, onProgress, onError ) {
FILE: src/webxr-boilerplate/Pointer.js
constant POINTER_ENTER (line 17) | const POINTER_ENTER = "enter"
constant POINTER_EXIT (line 18) | const POINTER_EXIT = "exit"
constant POINTER_CLICK (line 19) | const POINTER_CLICK = "click"
constant POINTER_MOVE (line 20) | const POINTER_MOVE = "move"
constant POINTER_PRESS (line 21) | const POINTER_PRESS = "press"
constant POINTER_RELEASE (line 22) | const POINTER_RELEASE = "release"
class Pointer (line 27) | class Pointer {
method constructor (line 28) | constructor(app, opts) {
method tick (line 95) | tick(time) {
method fire (line 101) | fire(obj, type, payload) {
method fireSelf (line 104) | fireSelf(type,payload) {
method cameraFollowMouse (line 110) | cameraFollowMouse(e) {
method mouseMove (line 118) | mouseMove(e) {
method touchStart (line 143) | touchStart(e) {
method touchMove (line 158) | touchMove(e) {
method touchEnd (line 169) | touchEnd(e) {
method controllerMove (line 186) | controllerMove(controller) {
method _processMove (line 195) | _processMove() {
method _processClick (line 219) | _processClick() {
method mouseClick (line 235) | mouseClick(e) {
method mouseDown (line 243) | mouseDown(e) {
method mouseUp (line 257) | mouseUp(e) {
method controllerSelectStart (line 271) | controllerSelectStart(e) {
method controllerSelectEnd (line 281) | controllerSelectEnd(e) {
method waitSceneClick (line 296) | waitSceneClick(cb) {
method addEventListener (line 301) | addEventListener(type,cb) {
method on (line 304) | on(type,cb) {
method off (line 308) | off(type,cb) {
method setMouseSimulatesController (line 311) | setMouseSimulatesController(val) {
FILE: src/webxr-boilerplate/WebXRBoilerPlate.js
constant FULLSCREEN_ENTERED (line 4) | const FULLSCREEN_ENTERED = "fullscreenenter"
constant FULLSCREEN_EXITED (line 5) | const FULLSCREEN_EXITED = "fullscreenexit"
class WebXRBoilerPlate (line 6) | class WebXRBoilerPlate {
method constructor (line 7) | constructor(options) {
method addEventListener (line 19) | addEventListener(type,cb) {
method _fire (line 23) | _fire(type,payload) {
method isFullscreenSupported (line 28) | isFullscreenSupported() {
method init (line 32) | init() {
method onRender (line 85) | onRender(cb) {
method enterVR (line 89) | enterVR() {
method playFullscreen (line 93) | playFullscreen() {
method exitFullscreen (line 98) | exitFullscreen() {
method checkContainerSize (line 104) | checkContainerSize() {
FILE: src/webxr-boilerplate/raycaster.js
function Raycaster (line 9) | function Raycaster( origin, direction, near, far ) {
function ascSort (line 38) | function ascSort( a, b ) {
function intersectObject (line 46) | function intersectObject( object, raycaster, intersects, recursive ) {
FILE: src/webxr-boilerplate/vrmanager.js
function printError (line 1) | function printError(err) {
constant VR_DETECTED (line 5) | const VR_DETECTED = "detected"
constant VR_CONNECTED (line 6) | const VR_CONNECTED = "connected"
constant VR_DISCONNECTED (line 7) | const VR_DISCONNECTED = "disconnected"
constant VR_PRESENTCHANGE (line 8) | const VR_PRESENTCHANGE = "presentchange"
constant VR_ACTIVATED (line 9) | const VR_ACTIVATED = "activated"
class VRManager (line 13) | class VRManager {
method constructor (line 14) | constructor(renderer) {
method addEventListener (line 76) | addEventListener(type, cb) {
method fire (line 80) | fire(type,evt) {
method enterVR (line 87) | enterVR() {
FILE: src/webxr-boilerplate/vrstats.js
class VRStats (line 2) | class VRStats extends Group {
method constructor (line 3) | constructor(app) {
method update (line 28) | update(time) {
method setProperty (line 54) | setProperty(name, value) {
Condensed preview — 66 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (240K chars).
[
{
"path": ".gitignore",
"chars": 1004,
"preview": "dist\npackage-lock.json\n\n# Created by .ignore support plugin (hsz.mobi)\n### Node template\n# Logs\nlogs\n*.log\nnpm-debug.log"
},
{
"path": ".idea/.gitignore",
"chars": 38,
"preview": "# Default ignored files\n/workspace.xml"
},
{
"path": ".idea/misc.xml",
"chars": 174,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n <component name=\"JavaScriptSettings\">\n <option name=\"l"
},
{
"path": ".idea/modules.xml",
"chars": 276,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n <component name=\"ProjectModuleManager\">\n <modules>\n "
},
{
"path": ".idea/vcs.xml",
"chars": 180,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n <component name=\"VcsDirectoryMappings\">\n <mapping dire"
},
{
"path": ".idea/voxeljs-next.iml",
"chars": 458,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<module type=\"WEB_MODULE\" version=\"4\">\n <component name=\"NewModuleRootManager\">\n"
},
{
"path": "LICENSE",
"chars": 1514,
"preview": "BSD 3-Clause License\n\nCopyright (c) 2019, Josh Marinacci\nAll rights reserved.\n\nRedistribution and use in source and bina"
},
{
"path": "README.md",
"chars": 5218,
"preview": "# voxeljs-next\nThe next generation of Voxel JS.\n\n# Play with it\n\nTry out a [live demo here](https://vr.josh.earth/voxelj"
},
{
"path": "examples/css/dashboard.css",
"chars": 557,
"preview": "\n/* dashboard.css */\ndiv.dom-dashboard {\n border: 5px solid black;\n z-index: 10;\n position: fixed;\n top: 10v"
},
{
"path": "examples/css/fullscreen.css",
"chars": 122,
"preview": "button.fullscreen {\n border: 5px solid red;\n position: fixed;\n left: 10vw;\n top: 10vh;\n font-size: 200%;\n"
},
{
"path": "examples/css/index.css",
"chars": 61,
"preview": "\nhtml, body {\n padding: 0;\n margin:0;\n overflow: hidden;\n}"
},
{
"path": "examples/css/main.css",
"chars": 1338,
"preview": "body {\n max-width: 40em;\n margin: auto;\n}\n#container {\n border: 3px solid black;\n width: 700px;\n height: "
},
{
"path": "examples/css/webxr.css",
"chars": 117,
"preview": "button.webxr {\n border: 5px solid red;\n position: fixed;\n left: 30vw;\n top: 10vh;\n font-size: 200%;\n}\n"
},
{
"path": "examples/package.json",
"chars": 138,
"preview": "{\n \"name\": \"examples\",\n \"version\": \"1.0.0\",\n \"main\": \"index.js\",\n \"private\": true,\n \"dependencies\": {\n \"voxeljs-"
},
{
"path": "examples/public/ecsy.html",
"chars": 487,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <title>Title</title>\n <link rel=\"stylesheet\" h"
},
{
"path": "examples/public/index.html",
"chars": 171,
"preview": "\n<!DOCTYPE html>\n<html>\n <head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initi"
},
{
"path": "examples/public/networked.html",
"chars": 6455,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <title>Mine Kitten</title>\n <script>\n /"
},
{
"path": "examples/simple.html",
"chars": 16230,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <title>Mine Kitten</title>\n <script>\n /"
},
{
"path": "examples/src/BlockPicker.js",
"chars": 4259,
"preview": "import Panel2D from \"threejs-2d-components/src/panel2d\"\nimport Label2D from \"threejs-2d-components/src/label2d\"\nimport B"
},
{
"path": "examples/src/ExplosionParticles.js",
"chars": 1836,
"preview": "import {Vector3, Color, AdditiveBlending} from \"three\"\nimport {ECSComp} from '../../src/ECSComp.js'\nimport {GPUParticleS"
},
{
"path": "examples/src/GPUParticleSystem.js",
"chars": 12322,
"preview": "import {Object3D, Vector3, Color, AdditiveBlending, RepeatWrapping, ShaderMaterial, BufferGeometry, BufferAttribute, Poi"
},
{
"path": "examples/src/ItemManager.js",
"chars": 1408,
"preview": "import {Vector3,} from \"three\"\nimport {ECSComp} from '../../src/ECSComp.js'\n\nexport class ItemManager extends ECSComp {\n"
},
{
"path": "examples/src/PersistenceManager.js",
"chars": 3930,
"preview": "\nfunction GET_JSON(url) {\n return fetch(url+`?cachebust=${Math.random()}`)\n .then(res => res.json())\n /"
},
{
"path": "examples/src/PigComp.js",
"chars": 2219,
"preview": "import {Vector3, Mesh, MeshLambertMaterial, BoxBufferGeometry, Box3} from \"three\"\nimport {ECSComp} from \"../../src/ECSCo"
},
{
"path": "examples/src/PubnubNetworkplay.js",
"chars": 3365,
"preview": "import {Vector3,} from \"three\"\nimport {ECSComp} from '../../src/ECSComp.js'\n\nconst pubkey = 'pub-c-0a0c49cb-8e11-4b10-83"
},
{
"path": "examples/src/RemotePlayersProxy.js",
"chars": 716,
"preview": "import {Vector3, Mesh, MeshLambertMaterial, SphereBufferGeometry} from \"three\"\nimport {ECSComp} from '../../src/ECSComp."
},
{
"path": "examples/src/SmashParticles.js",
"chars": 1899,
"preview": "import {Vector3, Color, AdditiveBlending} from \"three\"\nimport {ECSComp} from '../../src/ECSComp.js'\nimport {GPUParticleS"
},
{
"path": "examples/src/WebRTCAudioChat.js",
"chars": 1427,
"preview": "// https://github.com/stephenlb/webrtc-sdk\n\nconst pubkey = 'pub-c-0a0c49cb-8e11-4b10-8347-3af6cf048b46';\nconst subkey = "
},
{
"path": "examples/src/index.js",
"chars": 5561,
"preview": "import '../css/fullscreen.css'\nimport '../css/webxr.css';\nimport '../css/dashboard.css';\nimport '../css/index.css';\n\nimp"
},
{
"path": "package.json",
"chars": 1342,
"preview": "{\n \"name\": \"voxeljs-next\",\n \"version\": \"0.0.13\",\n \"description\": \"The next generation of Voxel JS.\",\n \"main\": \"dist/"
},
{
"path": "src/ChunkManager.js",
"chars": 8863,
"preview": "import {Vector3,} from \"three\"\nimport {CulledMesher} from \"./CulledMesher.js\"\nimport {VoxelMesh} from \"./VoxelMesh.js\"\n\n"
},
{
"path": "src/CulledMesher.js",
"chars": 2524,
"preview": "export class CulledMesher {\n constructor()\n {\n\n }\n\n//Naive meshing (with face culling)\n mesh(volume, dims)\n "
},
{
"path": "src/DesktopControls.js",
"chars": 2105,
"preview": "import {Vector2} from \"three\"\nimport {Pointer} from \"./webxr-boilerplate/Pointer\"\nimport {ECSComp} from './ECSComp.js'\ni"
},
{
"path": "src/ECSComp.js",
"chars": 589,
"preview": "export class ECSComp {\n constructor() {\n this._listeners = {}\n this._enabled = false\n }\n addEvent"
},
{
"path": "src/FullscreenControls.js",
"chars": 3988,
"preview": "import {Ray, Vector3,} from \"three\"\nimport {traceRay} from './raycast.js'\nimport {ECSComp} from './ECSComp.js'\nimport {t"
},
{
"path": "src/GreedyMesher.js",
"chars": 4289,
"preview": "let mask = new Int32Array(4096);\n\nexport class GreedyMesher {\n constructor() {\n }\n\n mesh(volume, dims) {\n "
},
{
"path": "src/KeyboardControls.js",
"chars": 4642,
"preview": "import {Vector3,} from \"three\"\nimport {ECSComp} from './ECSComp.js'\nconst toRad = (deg) => Math.PI / 180 * deg\nconst Y_A"
},
{
"path": "src/PhysHandler.js",
"chars": 1930,
"preview": "import {Vector3,} from \"three\"\nimport {ECSComp} from './ECSComp.js'\nconst GRAVITY = new Vector3(0,-9.8,0)\n\nexport class "
},
{
"path": "src/SimpleMeshCollider.js",
"chars": 3194,
"preview": "import {Vector3, Box3} from \"three\"\n\nconst SIZE = new Vector3()\nconst CENTER = new Vector3()\n\nexport function checkHitTi"
},
{
"path": "src/TextureManager.js",
"chars": 6963,
"preview": "import {\n LinearMipMapLinearFilter,\n NearestFilter,\n ShaderMaterial,\n Texture,\n VertexColors\n} from \"thre"
},
{
"path": "src/TouchControls.js",
"chars": 8107,
"preview": "import {Vector2, Vector3,} from \"three\"\nimport {ECSComp} from './ECSComp.js'\nimport {$, DIRS, on, toRad, traceRayAtScree"
},
{
"path": "src/VRControls.js",
"chars": 7078,
"preview": "import {Vector3,} from \"three\"\nimport {Pointer, POINTER_CLICK} from \"./webxr-boilerplate/Pointer\"\nimport {traceRay} from"
},
{
"path": "src/VRStats.js",
"chars": 2258,
"preview": "import {Vector3, Mesh, MeshLambertMaterial, BoxBufferGeometry,\n CanvasTexture, PlaneGeometry, MeshBasicMaterial,\n} fr"
},
{
"path": "src/VoxelMesh.js",
"chars": 11715,
"preview": "import {\n Color,\n Face3,\n FaceColors,\n Geometry,\n BufferGeometry,\n Mesh,\n MeshLambertMaterial,\n "
},
{
"path": "src/VoxelTexture.js",
"chars": 13494,
"preview": "// var isTransparent = require('opaque').transparent;\n\nimport {\n Color,\n DoubleSide,\n FaceColors,\n LinearMip"
},
{
"path": "src/ecsy/camera_gimbal.js",
"chars": 60,
"preview": "export class StagePosition {}\nexport class StageRotation {}\n"
},
{
"path": "src/ecsy/dashboard.js",
"chars": 3502,
"preview": "import { Component, System, World } from 'ecsy';\nimport {VoxelLandscape, VoxelSystem, VoxelTextures} from './voxels.js'\n"
},
{
"path": "src/ecsy/fullscreen.js",
"chars": 2444,
"preview": "import { Component, System, World } from 'ecsy';\n\nexport class FullscreenMode extends Component {\n\n}\nexport class Fullsc"
},
{
"path": "src/ecsy/highlight.js",
"chars": 4603,
"preview": "import * as util from \"../utils.js\"\nimport {System} from 'ecsy'\nimport {Quaternion, Ray, Vector2, Vector3} from 'three'\n"
},
{
"path": "src/ecsy/index.js",
"chars": 268,
"preview": "export * from './mouse.js'\nexport * from './keyboard.js'\nexport * from './voxels.js'\nexport * from './highlight.js'\nexpo"
},
{
"path": "src/ecsy/input.js",
"chars": 6178,
"preview": "import {Component, System} from 'ecsy'\nimport {StagePosition, StageRotation} from './camera_gimbal.js'\nimport {\n init"
},
{
"path": "src/ecsy/keyboard.js",
"chars": 1199,
"preview": "import {Component, System} from 'ecsy'\nimport {InputFrame} from './input.js'\n\nexport class KeyboardBindingSet extends Co"
},
{
"path": "src/ecsy/mouse.js",
"chars": 2518,
"preview": "import {Component, System} from 'ecsy'\nimport {Vector2} from 'three'\nimport {InputFrame} from './input.js'\n\nexport class"
},
{
"path": "src/ecsy/voxels.js",
"chars": 2034,
"preview": "import {TextureManager} from '../TextureManager.js'\nimport {Component, System} from 'ecsy'\nimport {ChunkManager} from \"."
},
{
"path": "src/ecsy/webxr.js",
"chars": 3413,
"preview": "import { Component, System, World } from 'ecsy';\nimport {FullscreenMode} from './fullscreen.js'\n\nexport class WebXRActiv"
},
{
"path": "src/index.js",
"chars": 616,
"preview": "//export * from './ecsy';\nexport * from './ChunkManager';\nexport * from './CulledMesher';\nexport * from './DesktopContro"
},
{
"path": "src/physical.js",
"chars": 9325,
"preview": "// module.exports = physical\n//import {AABB as aabb} from \"./aabb.js\"\nimport {Vector3} from \"three\"\n\n// make these *once"
},
{
"path": "src/raycast.js",
"chars": 6676,
"preview": "function traceRay_impl(\n voxelProvider,\n px, py, pz,\n dx, dy, dz,\n max_d,\n hit_pos,\n hit_norm,\n EPS"
},
{
"path": "src/utils.js",
"chars": 2024,
"preview": "import {Quaternion, Ray, Vector2, Vector3,} from \"three\"\nimport {traceRay} from './raycast.js'\n\nexport function toHexCol"
},
{
"path": "src/webxr-boilerplate/BackgroundAudioLoader.js",
"chars": 618,
"preview": "import {\n DefaultLoadingManager,\n} from \"three\"\n\nexport default class BackgroundAudioLoader {\n constructor(manager) {\n"
},
{
"path": "src/webxr-boilerplate/Pointer.js",
"chars": 12552,
"preview": "import {Raycaster} from \"./raycaster.js\"\nimport {\n Object3D,\n Vector2,\n Vector3,\n Quaternion,\n BufferGeom"
},
{
"path": "src/webxr-boilerplate/WebXRBoilerPlate.js",
"chars": 4301,
"preview": "import {DefaultLoadingManager, PerspectiveCamera, Scene, WebGLRenderer,} from \"three\"\nimport VRManager, {VR_DETECTED} fr"
},
{
"path": "src/webxr-boilerplate/raycaster.js",
"chars": 3249,
"preview": "import {Ray} from \"three\"\n\n/**\n * @author mrdoob / http://mrdoob.com/\n * @author bhouston / http://clara.io/\n * @author "
},
{
"path": "src/webxr-boilerplate/vrmanager.js",
"chars": 3082,
"preview": "function printError(err) {\n console.log(err)\n}\n\nexport const VR_DETECTED = \"detected\"\nexport const VR_CONNECTED = \"conn"
},
{
"path": "src/webxr-boilerplate/vrstats.js",
"chars": 2006,
"preview": "import { Group, CanvasTexture, Mesh, PlaneBufferGeometry, PlaneGeometry, MeshBasicMaterial, } from 'three'\nexport defaul"
},
{
"path": "webpack.config.js",
"chars": 1676,
"preview": "const path = require('path');\nconst HtmlWebpackPlugin = require('html-webpack-plugin');\nconst CopyPlugin = require('copy"
}
]
About this extraction
This page contains the full source code of the joshmarinacci/voxeljs-next GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 66 files (223.5 KB), approximately 55.8k tokens, and a symbol index with 358 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.