node_modules | |
build | |
package-lock.json |
/** | |
* @author mrdoob / http://mrdoob.com/ | |
*/ | |
import { BufferGeometry, Float32BufferAttribute } from "three"; | |
var BoxLineGeometry = function( | |
width, | |
height, | |
depth, | |
widthSegments, | |
heightSegments, | |
depthSegments | |
) { | |
BufferGeometry.call(this); | |
width = width || 1; | |
height = height || 1; | |
depth = depth || 1; | |
widthSegments = Math.floor(widthSegments) || 1; | |
heightSegments = Math.floor(heightSegments) || 1; | |
depthSegments = Math.floor(depthSegments) || 1; | |
var widthHalf = width / 2; | |
var heightHalf = height / 2; | |
var depthHalf = depth / 2; | |
var segmentWidth = width / widthSegments; | |
var segmentHeight = height / heightSegments; | |
var segmentDepth = depth / depthSegments; | |
var vertices = []; | |
var x = -widthHalf, | |
y = -heightHalf, | |
z = -depthHalf; | |
for (var i = 0; i <= widthSegments; i++) { | |
vertices.push(x, -heightHalf, -depthHalf, x, heightHalf, -depthHalf); | |
vertices.push(x, heightHalf, -depthHalf, x, heightHalf, depthHalf); | |
vertices.push(x, heightHalf, depthHalf, x, -heightHalf, depthHalf); | |
vertices.push(x, -heightHalf, depthHalf, x, -heightHalf, -depthHalf); | |
x += segmentWidth; | |
} | |
for (var i = 0; i <= heightSegments; i++) { | |
vertices.push(-widthHalf, y, -depthHalf, widthHalf, y, -depthHalf); | |
vertices.push(widthHalf, y, -depthHalf, widthHalf, y, depthHalf); | |
vertices.push(widthHalf, y, depthHalf, -widthHalf, y, depthHalf); | |
vertices.push(-widthHalf, y, depthHalf, -widthHalf, y, -depthHalf); | |
y += segmentHeight; | |
} | |
for (var i = 0; i <= depthSegments; i++) { | |
vertices.push(-widthHalf, -heightHalf, z, -widthHalf, heightHalf, z); | |
vertices.push(-widthHalf, heightHalf, z, widthHalf, heightHalf, z); | |
vertices.push(widthHalf, heightHalf, z, widthHalf, -heightHalf, z); | |
vertices.push(widthHalf, -heightHalf, z, -widthHalf, -heightHalf, z); | |
z += segmentDepth; | |
} | |
this.setAttribute("position", new Float32BufferAttribute(vertices, 3)); | |
}; | |
BoxLineGeometry.prototype = Object.create(BufferGeometry.prototype); | |
BoxLineGeometry.prototype.constructor = BoxLineGeometry; | |
export { BoxLineGeometry }; |
/** | |
* @author mvilledieu / http://github.com/mvilledieu | |
*/ | |
if (/(Helio)/g.test(navigator.userAgent) && "xr" in navigator) { | |
console.log("Helio WebXR Polyfill (Lumin 0.98.0)"); | |
if ("isSessionSupported" in navigator.xr) { | |
const tempIsSessionSupported = navigator.xr.isSessionSupported.bind( | |
navigator.xr | |
); | |
navigator.xr.isSessionSupported = function(/*sessionType*/) { | |
// Force using immersive-ar | |
return tempIsSessionSupported("immersive-ar"); | |
}; | |
} | |
if ( | |
"isSessionSupported" in navigator.xr && | |
"requestSession" in navigator.xr | |
) { | |
const tempRequestSession = navigator.xr.requestSession.bind(navigator.xr); | |
navigator.xr.requestSession = function(/*sessionType*/) { | |
return new Promise(function(resolve, reject) { | |
var sessionInit = { | |
optionalFeatures: ["local-floor", "bounded-floor"] | |
}; | |
tempRequestSession("immersive-ar", sessionInit) | |
.then(function(session) { | |
resolve(session); | |
}) | |
.catch(function(error) { | |
return reject(error); | |
}); | |
}); | |
}; | |
} | |
} |
<!DOCTYPE html> | |
<title>three.js vr - ball shooter</title> | |
<meta charset="utf-8" /> | |
<meta | |
name="viewport" | |
content="width=device-width, initial-scale=1.0, user-scalable=no" | |
/> | |
<style> | |
html, | |
body { | |
margin: 0; | |
padding: 0; | |
} | |
</style> | |
<script type="module" src="./main.js"></script> |
import "./HelioWebXRPolyfill.js"; | |
import * as THREE from "three"; | |
import * as Comlink from "comlink"; | |
import { BoxLineGeometry } from "./BoxLineGeometry.js"; | |
import { VRButton } from "./VRButton.js"; | |
var camera, scene, renderer; | |
var controller1, controller2; | |
var room; | |
// Field of View | |
var fov = 80; | |
// Number of balls; | |
var ballCount = getNumBalls(); | |
// Radius of one ball | |
var radius = 0.08; | |
// Size of the room | |
var roomSize = 6; | |
// Loss of velocity when bouncing of walls | |
var dampening = 0.8; | |
var worker = new Worker("./worker.js"); | |
var BallShooter = Comlink.wrap(worker); | |
var ballShooter; | |
var positions; | |
var balls; | |
init().then(() => animate()); | |
function getNumBalls() { | |
const def = 200; | |
const param = new URLSearchParams(document.location.search).get("balls"); | |
if (!param) { | |
return def; | |
} | |
const numeric = parseInt(param); | |
if (Number.isNaN(numeric)) { | |
return def; | |
} | |
return numeric; | |
} | |
async function init() { | |
ballShooter = await new BallShooter({ | |
numBalls: ballCount, | |
roomSize, | |
radius, | |
dampening | |
}); | |
scene = new THREE.Scene(); | |
scene.background = new THREE.Color(0x505050); | |
camera = new THREE.PerspectiveCamera( | |
fov, | |
window.innerWidth / window.innerHeight, | |
0.1, | |
10 | |
); | |
camera.position.set(roomSize / 2, roomSize, roomSize / 2); | |
camera.lookAt(-roomSize / 2, 0, -roomSize / 2); | |
room = new THREE.LineSegments( | |
new BoxLineGeometry(roomSize, roomSize, roomSize, 10, 10, 10), | |
new THREE.LineBasicMaterial({ color: 0x808080 }) | |
); | |
room.geometry.translate(0, roomSize / 2, 0); | |
scene.add(room); | |
var light = new THREE.HemisphereLight(0xffffff, 0x444444); | |
light.position.set(1, 1, 1); | |
scene.add(light); | |
var geometry = new THREE.IcosahedronBufferGeometry(radius, 2); | |
balls = new THREE.InstancedMesh( | |
geometry, | |
new THREE.MeshLambertMaterial({ | |
color: 0xff8000 | |
}), | |
ballCount | |
); | |
// ThreeJS doesn't support frustrum culling for InstancedMesh yet. | |
balls.frustumCulled = false; | |
room.add(balls); | |
positions = await ballShooter.getPositions(); | |
updateBallPositions(); | |
await ballShooter.setCallback(Comlink.proxy(buffer => (positions = buffer))); | |
// | |
renderer = new THREE.WebGLRenderer({ antialias: true }); | |
renderer.setPixelRatio(window.devicePixelRatio); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
renderer.xr.enabled = true; | |
document.body.appendChild(renderer.domElement); | |
// | |
document.body.appendChild(VRButton.createButton(renderer)); | |
// controllers | |
function onSelectStart() { | |
ballShooter.startShootingGun(this.userData.id); | |
} | |
function onSelectEnd() { | |
ballShooter.stopShootingGun(this.userData.id); | |
} | |
controller1 = renderer.xr.getController(0); | |
controller1.addEventListener("selectstart", onSelectStart); | |
controller1.addEventListener("selectend", onSelectEnd); | |
controller1.addEventListener("connected", function(event) { | |
this.add(buildController(event.data)); | |
}); | |
controller1.addEventListener("disconnected", function() { | |
this.remove(this.children[0]); | |
}); | |
controller1.userData.id = 0; | |
scene.add(controller1); | |
controller2 = renderer.xr.getController(1); | |
controller2.addEventListener("selectstart", onSelectStart); | |
controller2.addEventListener("selectend", onSelectEnd); | |
controller2.addEventListener("connected", function(event) { | |
this.add(buildController(event.data)); | |
}); | |
controller2.addEventListener("disconnected", function() { | |
this.remove(this.children[0]); | |
}); | |
controller2.userData.id = 1; | |
scene.add(controller2); | |
// | |
window.addEventListener("resize", onWindowResize, false); | |
window.addEventListener("keydown", ev => { | |
if (ev.code !== "Space") { | |
return; | |
} | |
ev.preventDefault(); | |
ballShooter.startShootingGun(0); | |
}); | |
window.addEventListener("keyup", ev => { | |
if (ev.code !== "Space") { | |
return; | |
} | |
ev.preventDefault(); | |
ballShooter.stopShootingGun(0); | |
}); | |
} | |
function buildController(data) { | |
switch (data.targetRayMode) { | |
case "tracked-pointer": | |
var geometry = new THREE.BufferGeometry(); | |
geometry.setAttribute( | |
"position", | |
new THREE.Float32BufferAttribute([0, 0, 0, 0, 0, -1], 3) | |
); | |
geometry.setAttribute( | |
"color", | |
new THREE.Float32BufferAttribute([0.5, 0.5, 0.5, 0, 0, 0], 3) | |
); | |
var material = new THREE.LineBasicMaterial({ | |
vertexColors: true, | |
blending: THREE.AdditiveBlending | |
}); | |
return new THREE.Line(geometry, material); | |
case "gaze": | |
var geometry = new THREE.RingBufferGeometry(0.02, 0.04, 32).translate( | |
0, | |
0, | |
-1 | |
); | |
var material = new THREE.MeshBasicMaterial({ | |
opacity: 0.5, | |
transparent: true | |
}); | |
return new THREE.Mesh(geometry, material); | |
} | |
} | |
function onWindowResize() { | |
camera.aspect = window.innerWidth / window.innerHeight; | |
camera.updateProjectionMatrix(); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
} | |
function vectorIsNull(v) { | |
return v.x === 0 && v.y === 0 && v.z === 0; | |
} | |
const tmp1 = [0, 0, 0]; | |
const tmp2 = [0, 0, 0, 0]; | |
const defaultRotation = new THREE.Quaternion() | |
.setFromUnitVectors(new THREE.Vector3(0, 0, -1), new THREE.Vector3(0, 1, 0)) | |
.toArray(); | |
function handleController(controller) { | |
if (vectorIsNull(controller.position)) { | |
ballShooter.setGun(controller.userData.id, [0, 1, 0], defaultRotation); | |
} else { | |
ballShooter.setGun( | |
controller.userData.id, | |
controller.position.toArray(tmp1, 0), | |
controller.quaternion.toArray(tmp2, 0) | |
); | |
} | |
} | |
async function updateBallPositions() { | |
balls.instanceMatrix.array.set(positions); | |
balls.instanceMatrix.needsUpdate = true; | |
} | |
// | |
function animate() { | |
renderer.setAnimationLoop(render); | |
ballShooter.start(); | |
} | |
async function render() { | |
handleController(controller1); | |
handleController(controller2); | |
updateBallPositions(); | |
// | |
renderer.render(scene, camera); | |
} |
{ | |
"name": "omt-ball", | |
"version": "0.0.1", | |
"description": "", | |
"main": "BoxLineGeometry.js", | |
"scripts": { | |
"build": "rollup -c", | |
"serve": "http-server -c0 build" | |
}, | |
"keywords": [], | |
"author": "Surma <surma@surma.link>", | |
"license": "Apache-2.0", | |
"dependencies": { | |
"@rollup/plugin-node-resolve": "^7.0.0", | |
"@surma/rollup-plugin-off-main-thread": "^1.1.1", | |
"comlink": "^4.2.0", | |
"rollup": "^1.29.1", | |
"three": "^0.112.1" | |
}, | |
"devDependencies": { | |
"http-server": "^0.12.1" | |
} | |
} |
import nodeResolve from "@rollup/plugin-node-resolve"; | |
import omt from "@surma/rollup-plugin-off-main-thread"; | |
import fs from "fs"; | |
export default { | |
input: "main.js", | |
output: { | |
dir: "build", | |
format: "amd" | |
}, | |
plugins: [ | |
nodeResolve(), | |
omt(), | |
{ | |
async writeBundle() { | |
await fs.promises.copyFile("index.html", "./build/index.html"); | |
} | |
} | |
] | |
}; |
import * as Comlink from "comlink"; | |
import * as THREE from "three"; | |
class BallShooter { | |
constructor({ numBalls, roomSize, radius, dampening }) { | |
this._dampening = dampening; | |
this._numBalls = numBalls; | |
this._roomSize = roomSize; | |
this._radius = radius; | |
this.framerate = 90; | |
this._positions = new Float32Array(this._numBalls * 4 * 4); | |
this._velocities = new Float32Array(this._numBalls * 3); | |
this._ballCounter = 0; | |
this.shootingRate = 50; | |
this._guns = [ | |
{ | |
shooting: false, | |
position: new THREE.Vector3(), | |
quaternion: new THREE.Quaternion() | |
}, | |
{ | |
shooting: false, | |
position: new THREE.Vector3(), | |
quaternion: new THREE.Quaternion() | |
} | |
]; | |
// Each 4 * 4 elements are one matrix. | |
// Set them to the identity matrix. | |
this._positions.fill(0); | |
for (let i = 0; i < this._numBalls; i++) { | |
this._positions[i * 4 * 4 + 0] = 1; | |
this._positions[i * 4 * 4 + 5] = 1; | |
this._positions[i * 4 * 4 + 10] = 1; | |
this._positions[i * 4 * 4 + 15] = 1; | |
} | |
this._tmpVector = new THREE.Vector3(); | |
this._balls = Array.from({ length: this._numBalls }, (_, i) => { | |
return { | |
index: i, | |
position: this._positions.subarray(i * 4 * 4 + 12, i * 4 * 4 + 15), | |
velocity: this._velocities.subarray(i * 3, i * 3 + 3) | |
}; | |
}); | |
this._init(); | |
} | |
setCallback(cb) { | |
this._cb = cb; | |
} | |
_init() { | |
for (var i = 0; i < this._numBalls; i++) { | |
this._balls[i].position[0] = random( | |
-this._roomSize / 2 + 1, | |
this._roomSize / 2 - 1 | |
); | |
this._balls[i].position[1] = random(0, this._roomSize); | |
this._balls[i].position[2] = random( | |
-this._roomSize / 2 + 1, | |
this._roomSize / 2 - 1 | |
); | |
this._balls[i].velocity[0] = random(-0.005, 0.005); | |
this._balls[i].velocity[1] = random(-0.005, 0.005); | |
this._balls[i].velocity[2] = random(-0.005, 0.005); | |
} | |
} | |
startShootingGun(id) { | |
if (id > this._guns.length) { | |
return; | |
} | |
this._guns[id].shooting = true; | |
} | |
stopShootingGun(id) { | |
if (id > this._guns.length) { | |
return; | |
} | |
this._guns[id].shooting = false; | |
} | |
setGun(id, position, quaternion) { | |
if (id > this._guns.length) { | |
return; | |
} | |
this._guns[id].position.set(...position); | |
this._guns[id].quaternion.set(...quaternion); | |
} | |
start() { | |
this._lastFrame = performance.now(); | |
this._running = true; | |
this._update(); | |
} | |
getPositions() { | |
return this._positions; | |
} | |
put(buffer) { | |
this._pool.put(buffer); | |
} | |
_update() { | |
const currentFrame = performance.now(); | |
const nextFrame = currentFrame + 1000 / this.framerate; | |
const delta = currentFrame - this._lastFrame; | |
/// | |
this._doPhysics(delta / 1000); | |
this._shootBalls(delta / 1000); | |
this._cb(this._positions); | |
/// | |
this._lastFrame = currentFrame; | |
if (this._running) { | |
let deltaToNextFrame = nextFrame - performance.now(); | |
if (deltaToNextFrame < 0) { | |
deltaToNextFrame = 0; | |
} | |
setTimeout(() => this._update(), deltaToNextFrame); | |
} | |
} | |
_shootBalls(delta) { | |
for (const gun of this._guns) { | |
if (!gun.shooting) { | |
continue; | |
} | |
const previousBallCounter = Math.floor(this._ballCounter); | |
this._ballCounter += this.shootingRate * delta; | |
for ( | |
let i = previousBallCounter; | |
i < Math.floor(this._ballCounter) && i < this._numBalls; | |
i++ | |
) { | |
const ball = this._balls[i]; | |
vectorSet(ball.position, gun.position.toArray()); | |
this._tmpVector.set(random(-1, 1), random(-1, 1), -10); | |
this._tmpVector.applyQuaternion(gun.quaternion); | |
vectorSet(ball.velocity, this._tmpVector.toArray()); | |
} | |
} | |
this._ballCounter %= this._numBalls; | |
} | |
_doPhysics(delta) { | |
const range = this._roomSize / 2 - this._radius; | |
const normal = new Float32Array(3); | |
const relativeVelocity = new Float32Array(3); | |
for (var i = 0; i < this._numBalls; i++) { | |
const ball = this._balls[i]; | |
ball.position[0] += ball.velocity[0] * delta; | |
ball.position[1] += ball.velocity[1] * delta; | |
ball.position[2] += ball.velocity[2] * delta; | |
// Bounce of walls | |
if (ball.position[0] < -range || ball.position[0] > range) { | |
ball.position[0] = clamp(ball.position[0], -range, range); | |
ball.velocity[0] = -ball.velocity[0] * this._dampening; | |
} | |
if ( | |
ball.position[1] < this._radius || | |
ball.position[1] > this._roomSize | |
) { | |
ball.position[1] = Math.max(ball.position[1], this._radius); | |
ball.velocity[0] *= this._dampening; | |
ball.velocity[1] = -ball.velocity[1] * this._dampening; | |
ball.velocity[2] *= this._dampening; | |
} | |
if (ball.position[2] < -range || ball.position[2] > range) { | |
ball.position[2] = clamp(ball.position[2], -range, range); | |
ball.velocity[2] = -ball.velocity[2] * this._dampening; | |
} | |
// // Bounce of other balls | |
for (var j = i + 1; j < this._numBalls; j++) { | |
const otherBall = this._balls[j]; | |
vectorDifference(normal, ball.position, otherBall.position); | |
const distance = vectorLength(normal, 0); | |
if (distance < 2 * this._radius) { | |
vectorScalarProduct(normal, normal, 0.5 * distance - this._radius); | |
vectorDifference(ball.position, ball.position, normal); | |
vectorSum(otherBall.position, otherBall.position, normal); | |
vectorNormalized(normal, normal); | |
vectorDifference(relativeVelocity, ball.velocity, otherBall.velocity); | |
vectorScalarProduct( | |
normal, | |
normal, | |
vectorDot(relativeVelocity, normal) | |
); | |
vectorDifference(ball.velocity, ball.velocity, normal); | |
vectorSum(otherBall.velocity, otherBall.velocity, normal); | |
} | |
} | |
// Gravity | |
ball.velocity[1] -= 9.8 * delta; | |
} | |
} | |
} | |
function clamp(v, min, max) { | |
if (v < min) { | |
return min; | |
} | |
if (v > max) { | |
return max; | |
} | |
return v; | |
} | |
function vectorDot(a, b) { | |
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; | |
} | |
function vectorSum(t, a, b) { | |
for (let i = 0; i < 3; i++) { | |
t[i] = a[i] + b[i]; | |
} | |
return t; | |
} | |
function vectorSet(t, [x, y, z]) { | |
t[0] = x; | |
t[1] = y; | |
t[2] = z; | |
return t; | |
} | |
function vectorDifference(t, a, b) { | |
for (let i = 0; i < 3; i++) { | |
t[i] = a[i] - b[i]; | |
} | |
return t; | |
} | |
function vectorLength(a) { | |
let length = vectorDot(a, a); | |
length = Math.sqrt(length); | |
return length; | |
} | |
function vectorScalarProduct(t, a, s) { | |
for (let i = 0; i < 3; i++) { | |
t[i] = a[i] * s; | |
} | |
return t; | |
} | |
function vectorNormalized(t, a) { | |
const length = vectorLength(a); | |
for (let i = 0; i < 3; i++) { | |
t[i] = a[i] / length; | |
} | |
return t; | |
} | |
function random(a, b) { | |
return Math.random() * (b - a) + a; | |
} | |
Comlink.expose(BallShooter); |
This comment has been minimized.
This comment has been minimized.
@keaukraine That’s correct. I actively decided against transferring the buffers. I explain why in the blog post:
In general this project made me realize that structured cloning |
This comment has been minimized.
This comment has been minimized.
OK, thank you for explanation. |
This comment has been minimized.
Do I understand this right that you don't use transferables to pass ArrayBuffers between main thread and worker? (https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage).
Transferable buffers are faster than copying buffers. Actually, they are NOT copied at all, only reassigned between main thread and worker.
Comlink seems to support this too: https://github.com/GoogleChromeLabs/comlink#comlinktransfervalue-transferables-and-comlinkproxyvalue