Skip to content

Instantly share code, notes, and snippets.

@paulviville
Created February 6, 2025 20:46
Show Gist options
  • Save paulviville/562de73031fe042487ef0d6745f29ea5 to your computer and use it in GitHub Desktop.
Save paulviville/562de73031fe042487ef0d6745f29ea5 to your computer and use it in GitHub Desktop.
import * as THREE from 'three';
import StereoCamera from './StereoScreenCamera.js';
export default class Cave {
#screens;
#stereoScreenCameras;
constructor ( screens ) {
this.#screens = [...screens];
this.#stereoScreenCameras = [];
for( const screen of screens ) {
// this.#stereoScreenCameras.push(new StereoCamera(screen));
}
}
get screens ( ) {
return this.#screens;
}
get stereoScreenCameras ( ) {
return this.#stereoScreenCameras;
}
}
import * as THREE from 'three';
import { Object3D } from 'three';
import ScreenHelper from './ScreenHelper.js';
const screenColors = [0x3399DD, 0xDD3399, 0x99DD33];
export default class CaveHelper extends Object3D {
#cave;
constructor ( cave ) {
super();
this.type = 'CaveHelper';
this.#cave = cave;
const axesHelper = new THREE.AxesHelper( 10 );
this.add( axesHelper );
for(const screen of this.#cave.screens) {
this.add(new ScreenHelper(screen, screenColors.shift()));
}
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<title>CaveTracker</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<style>
body { margin : 0; max-width: 100%; max-height: 100%; margin: 0;}
canvas {width: 100%; height: 100%; max-width: 100%; max-height: 100%; margin: 0; display: block;}
</style>
</head>
<body>
<script type="importmap">
{
"imports": {
"three": "./three.module.js",
"three/addons/": "./jsm/"
}
}
</script>
<script type="module" src="main.js">
</script>
</body>
</html>
import * as THREE from 'three';
import Stats from 'three/addons/libs/stats.module.js';
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { TrackballControls } from 'three/addons/controls/TrackballControls.js';
import Screen from './Screen.js';
import ScreenHelper from './ScreenHelper.js';
import Cave from './Cave.js';
import CaveHelper from './CaveHelper.js';
import StereoScreenCamera from './StereoScreenCamera.js';
import StereoScreenCameraHelper from './StereoScreenCameraHelper.js';
// const socket = new WebSocket("ws://localhost:8000");
// socket.addEventListener("message", (event) => {
// console.log("Message from server ", event.data);
// });
// console.log(socket)
const stats = new Stats()
document.body.appendChild( stats.dom );
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xcccccc);
let ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
let pointLight0 = new THREE.PointLight(0xffffff, 100);
pointLight0.position.set(5,4,5);
scene.add(pointLight0);
const worldUp = new THREE.Vector3(0, 0, 1);
const camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 0.01, 50 );
camera.up.copy(worldUp);
camera.position.set( -2, -4, 3 );
const renderer = new THREE.WebGLRenderer({antialias: true});
renderer.autoClear = false;
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
const orbitControls = new OrbitControls(camera, renderer.domElement);
orbitControls.target.set(0, 0, 2);
orbitControls.update()
window.addEventListener('resize', function() {
const width = window.innerWidth;
const height = window.innerHeight;
renderer.setSize(width, height);
camera.aspect = width / height;
camera.updateProjectionMatrix();
});
function animate() {
orbitControls.update()
renderer.render( scene, camera );
stats.update()
}
renderer.setAnimationLoop( animate );
const PDS = Math.sqrt(2) * 1.8;
const screenCoords = [
[new THREE.Vector3(-PDS, 0, 0), new THREE.Vector3(0, PDS, 0), new THREE.Vector3(-PDS, 0, 2.25)],
[new THREE.Vector3(0, PDS, 0), new THREE.Vector3(PDS, 0, 0), new THREE.Vector3(0, PDS, 2.25)],
[new THREE.Vector3(-PDS + 1.194, 1.194), new THREE.Vector3(0.62, -0.82, 0), new THREE.Vector3(-0.21, PDS - 0.21, 0)]
]
const t = new THREE.Vector3(1, 1, 0).normalize().multiplyScalar(2.25);
const screenCorners0 = [
new THREE.Vector3(-PDS, 0, 0),
new THREE.Vector3(0, PDS, 0),
new THREE.Vector3(-PDS, 0, 2.25),
new THREE.Vector3(0, PDS, 2.25),
];
const screenCorners1 = [
new THREE.Vector3(0, PDS, 0),
new THREE.Vector3(PDS, 0, 0),
new THREE.Vector3( 0, PDS, 2.25),
new THREE.Vector3( PDS, 0, 2.25),
];
const screenCorners2 = [
new THREE.Vector3(-t.x, PDS - t.y, 0),
new THREE.Vector3(PDS - t.x, -t.y, 0),
new THREE.Vector3(0, PDS, 0),
new THREE.Vector3(PDS, 0, 0),
];
const screen0 = new Screen(screenCorners0);
const screen1 = new Screen(screenCorners1);
const screen2 = new Screen(screenCorners2);
const cave = new Cave([screen0, screen1, screen2]);
const caveHelper = new CaveHelper(cave);
scene.add(caveHelper);
// const cameraMarker = new THREE.Mesh(new THREE.SphereGeometry(0.05, 16, 16), new THREE.MeshBasicMaterial({color: 0xff0000}));
// scene.add(cameraMarker)
const targetPoint = new THREE.Vector3(-1.5, PDS, 1.2)
const trackedCamera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 0.1, 0.5 );
trackedCamera.up.copy(worldUp);
trackedCamera.position.set(0.5, -0.1, 1);
trackedCamera.lookAt(targetPoint)
trackedCamera.updateProjectionMatrix();
trackedCamera.updateWorldMatrix();
const stereoScreenCamera0 = new StereoScreenCamera(screen0);
const stereoScreenCamera1 = new StereoScreenCamera(screen1);
const stereoScreenCamera2 = new StereoScreenCamera(screen2);
const stereoScreenCameraHelper0 = new StereoScreenCameraHelper(stereoScreenCamera0);
const stereoScreenCameraHelper1 = new StereoScreenCameraHelper(stereoScreenCamera1);
const stereoScreenCameraHelper2 = new StereoScreenCameraHelper(stereoScreenCamera2);
scene.add(stereoScreenCameraHelper0);
scene.add(stereoScreenCameraHelper1);
scene.add(stereoScreenCameraHelper2);
stereoScreenCameraHelper0.update(trackedCamera.matrixWorld.clone());
stereoScreenCameraHelper1.update(trackedCamera.matrixWorld.clone());
stereoScreenCameraHelper2.update(trackedCamera.matrixWorld.clone());
// trackedCamera.position.set(0, 0, 1);
// trackedCamera.lookAt(new THREE.Vector3(0, PDS, 1.2))
// trackedCamera.updateProjectionMatrix();
// trackedCamera.updateWorldMatrix();
// stereoScreenCameraHelper0.update(trackedCamera.matrixWorld.clone());
// stereoScreenCameraHelper1.update(trackedCamera.matrixWorld.clone());
// stereoScreenCameraHelper2.update(trackedCamera.matrixWorld.clone());
const trackedCameraHelper = new THREE.CameraHelper(trackedCamera);
scene.add(trackedCameraHelper);
function updateRig() {
trackedCamera.lookAt(targetPoint);
trackedCamera.updateProjectionMatrix();
trackedCamera.updateWorldMatrix();
trackedCameraHelper.update();
stereoScreenCameraHelper0.update(trackedCamera.matrixWorld.clone());
stereoScreenCameraHelper1.update(trackedCamera.matrixWorld.clone());
stereoScreenCameraHelper2.update(trackedCamera.matrixWorld.clone());
}
const guiParams = {
updateRig: updateRig,
}
const gui = new GUI();
gui.add(trackedCamera.position, 'x').name("x").min(-1.0).max(1.0).step(0.05).onChange(updateRig);
gui.add(trackedCamera.position, 'y').name("y").min(-PDS).max(PDS).step(0.05).onChange(updateRig);
gui.add(trackedCamera.position, 'z').name("z").min(0.05).max(2.0).step(0.05).onChange(updateRig);
gui.add(targetPoint, 'x').name('tx').min(-10.0).max(10.0).step(0.05).onChange(updateRig);
gui.add(targetPoint, 'y').name('ty').min(-10.0).max(10.0).step(0.05).onChange(updateRig);
gui.add(targetPoint, 'z').name('tz').min(-10.0).max(10.0).step(0.05).onChange(updateRig);
window.updateRig = updateRig;
import * as THREE from 'three';
import { Vector3, Matrix4 } from 'three';
/// 2 ------ 3
/// | Screen |
/// 0 ------ 1
export default class Screen {
#corners;
#ssAxes = {x: new THREE.Vector3(), y: new THREE.Vector3(), z: new THREE.Vector3()};
constructor ( corners ) {
this.#corners = [new Vector3(-1, -1, 0), new Vector3(1, -1, 0), new Vector3(-1, 1, 0), new Vector3(1, 1, 0)];
if( corners !== undefined ) {
this.#corners[0].copy(corners[0]);
this.#corners[1].copy(corners[1]);
this.#corners[2].copy(corners[2]);
this.#corners[3].copy(corners[3]);
}
this.#computeScreenSpace();
}
get corners () {
return [
this.#corners[0].clone(),
this.#corners[1].clone(),
this.#corners[2].clone(),
this.#corners[3].clone(),
]
}
#computeScreenSpace ( ) {
this.#ssAxes.x.copy(this.#corners[1]).sub(this.#corners[0]).normalize();
this.#ssAxes.y.copy(this.#corners[2]).sub(this.#corners[0]).normalize();
this.#ssAxes.z.crossVectors(this.#ssAxes.x, this.#ssAxes.y).normalize();
console.log(this.#ssAxes)
}
#eyeMatrices ( eye ) {
const projection = new Matrix4();
const view = new Matrix4();
const eye0 = this.#corners[0].clone().sub(eye);
const eye1 = this.#corners[1].clone().sub(eye);
const eye2 = this.#corners[2].clone().sub(eye);
const dist = - eye0.dot(this.#ssAxes.z);
const nearCP = 0.01;
/// for debug, replace with frustrum far
const farCP = dist;
const ND = nearCP / dist;
const l = this.#ssAxes.x.dot(eye0) * ND;
const r = this.#ssAxes.x.dot(eye1) * ND;
const b = this.#ssAxes.y.dot(eye0) * ND;
const t = this.#ssAxes.y.dot(eye2) * ND;
projection.set(
(2.0 * nearCP) / (r - l), 0.0, (r + l) / (r - l), 0.0,
0.0, (2.0 * nearCP) / (t - b), (t + b) / (t - b), 0.0,
0.0, 0.0, -(farCP + nearCP) / (farCP - nearCP), -(2.0 * farCP * nearCP) / (farCP - nearCP),
0.0, 0.0, -1.0, 0.0
);
/// move out, constant on screen
const R = new Matrix4(
this.#ssAxes.x.x, this.#ssAxes.x.y, this.#ssAxes.x.z, 0.0,
this.#ssAxes.y.x, this.#ssAxes.y.y, this.#ssAxes.y.z, 0.0,
this.#ssAxes.z.x, this.#ssAxes.z.y, this.#ssAxes.z.z, 0.0,
0.0, 0.0, 0.0, 1.0
);
const E = new Matrix4().makeTranslation(-eye.x, -eye.y, -eye.z);
view.multiplyMatrices(R, E);
return {projection, view};
}
stereoMatrices ( headMatrix ) {
const eyes = {
left: new THREE.Vector3(-0.032, 0.0, -0.015),
right: new THREE.Vector3(0.032, 0.0, -0.015)
};
eyes.left.applyMatrix4(headMatrix);
eyes.right.applyMatrix4(headMatrix);
const leftMatrices = this.#eyeMatrices(eyes.left);
eyes.leftView = leftMatrices.view;
eyes.leftProjection = leftMatrices.projection;
const rightMatrices = this.#eyeMatrices(eyes.right);
eyes.rightView = rightMatrices.view;
eyes.rightProjection = rightMatrices.projection;
return eyes;
}
}
import * as THREE from 'three';
import { Object3D } from 'three';
import Screen from './Screen.js';
export default class ScreenHelper extends Object3D {
#screen;
#screenFace;
#screenEdge;
constructor ( screen, color = 0xFFFFFF ) {
super();
this.type = 'ScreenHelper';
this.#screen = screen;
const corners = screen.corners;
const indices = [ 0, 1, 2, 1, 3, 2 ]
const vertices = new Float32Array([
...corners[0].toArray(),
...corners[1].toArray(),
...corners[2].toArray(),
...corners[3].toArray()
]);
const geometry = new THREE.BufferGeometry();
geometry.setAttribute("position", new THREE.BufferAttribute(vertices, 3));
geometry.setIndex(indices);
geometry.computeVertexNormals();
const material = new THREE.MeshBasicMaterial({color: color, side: THREE.DoubleSide, transparent: true, opacity: 0.2});
this.#screenFace = new THREE.Mesh(geometry, material);
const edgeGeometry = new THREE.EdgesGeometry(geometry);
const edgeMaterial = new THREE.LineBasicMaterial({color: 0x000000});
this.#screenEdge = new THREE.LineSegments(edgeGeometry, edgeMaterial);
this.add(this.#screenEdge, this.#screenFace);
}
}
import * as THREE from 'three';
import { Vector3, Quaternion, Matrix4 } from 'three';
export default class StereoScreenCamera {
#screen;
#eyeSeparation = 0.064;
#position = new THREE.Vector3();
#quaternion = new THREE.Quaternion();
#left = new THREE.PerspectiveCamera();
#right = new THREE.PerspectiveCamera();
/// Screen Space Axes
#ssX = new THREE.Vector3();
#ssY = new THREE.Vector3();
#ssZ = new THREE.Vector3();
#ssRotation = new THREE.Matrix4();
constructor ( screen ) {
this.#screen = screen;
this.#computeScreenSpace();
}
get worldMatrix ( ) {
// m4d << r[0], r[4], r[8], t.pos[0], r[1], r[5], r[9], t.pos[1], r[2], r[6], r[10], t.pos[2], 0.0f, 0.0f, 0.0f, 1;
// CaveViewer::head_matrix_s_ = m4d.cast<float>();
}
set position ( position ) {
this.#position.copy(position);
}
set quaternion ( quaternion ) {
this.#quaternion.copy(quaternion);
}
get left ( ) {
return this.#left;
}
get right ( ) {
return this.#right;
}
get eyeSeperation ( ) {
return this.#eyeSeparation;
}
set eyeSeperation ( dist ) {
this.#eyeSeparation = dist;
}
update ( headMatrix ) {
const leftEye = new THREE.Vector3(-this.#eyeSeparation / 2, 0.0, -0.015);
const rightEye = new THREE.Vector3(this.#eyeSeparation / 2, 0.0, -0.015);
leftEye.applyMatrix4(headMatrix);
rightEye.applyMatrix4(headMatrix);
const leftMatrices = this.#eyeMatrices(leftEye);
const rightMatrices = this.#eyeMatrices(rightEye);
this.#left.matrixWorldInverse.copy(leftMatrices.view);
this.#left.matrixWorld.copy(leftMatrices.view.clone().invert());
this.#left.projectionMatrix.copy(leftMatrices.projection);
this.#left.projectionMatrixInverse.copy(leftMatrices.projection.clone().invert() );
// this.#left.updateProjectionMatrix = false;
this.#right.matrixWorldInverse.copy(rightMatrices.view);
this.#right.matrixWorld.copy(rightMatrices.view.clone().invert());
this.#right.projectionMatrix.copy(rightMatrices.projection);
this.#right.projectionMatrixInverse.copy(rightMatrices.projection.clone().invert() );
// this.#right.updateProjectionMatrix = false;
}
#computeScreenSpace ( ) {
const corners = this.#screen.corners;
this.#ssX.copy(corners[1]).sub(corners[0]).normalize();
this.#ssY.copy(corners[2]).sub(corners[0]).normalize();
this.#ssZ.crossVectors(this.#ssX, this.#ssY).normalize();
this.#ssRotation.set(
this.#ssX.x, this.#ssX.y, this.#ssX.z, 0.0,
this.#ssY.x, this.#ssY.y, this.#ssY.z, 0.0,
this.#ssZ.x, this.#ssZ.y, this.#ssZ.z, 0.0,
0.0, 0.0, 0.0, 1.0
);
}
#eyeMatrices ( eye ) {
const projection = new Matrix4();
const view = new Matrix4();
const corners = this.#screen.corners;
const eye0 = corners[0].clone().sub(eye);
const eye1 = corners[1].clone().sub(eye);
const eye2 = corners[2].clone().sub(eye);
const dist = - eye0.dot(this.#ssZ);
const nearCP = 0.01;
// const farCP = 10.0;
const farCP = dist;
const ND = nearCP / dist;
const l = this.#ssX.dot(eye0) * ND;
const r = this.#ssX.dot(eye1) * ND;
const b = this.#ssY.dot(eye0) * ND;
const t = this.#ssY.dot(eye2) * ND;
projection.set(
(2.0 * nearCP) / (r - l), 0.0, (r + l) / (r - l), 0.0,
0.0, (2.0 * nearCP) / (t - b), (t + b) / (t - b), 0.0,
0.0, 0.0, -(farCP + nearCP) / (farCP - nearCP), -(2.0 * farCP * nearCP) / (farCP - nearCP),
0.0, 0.0, -1.0, 0.0
);
const E = new Matrix4().makeTranslation(-eye.x, -eye.y, -eye.z);
view.multiplyMatrices(this.#ssRotation, E);
return {projection, view};
}
}
import StereoCamera from "./StereoScreenCamera.js";
import { Object3D } from "three";
import { CameraHelper } from "./three.module.js";
export default class StereoScreenCameraHelper extends Object3D{
#stereoScreenCamera;
#leftCameraHelper;
#rightCameraHelper;
constructor ( stereoCamera ) {
super();
this.#stereoScreenCamera = stereoCamera;
this.#leftCameraHelper = new CameraHelper(this.#stereoScreenCamera.left);
this.#rightCameraHelper = new CameraHelper(this.#stereoScreenCamera.right);
this.add(this.#leftCameraHelper, this.#rightCameraHelper);
}
update ( headMatrix ) {
this.#stereoScreenCamera.update(headMatrix);
this.#leftCameraHelper.update();
this.#rightCameraHelper.update();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment