splatCamera.ts
src/render/splatCamera.ts · raw · all examples · site home
Exports: EyeNodesEyeSettingSplatCamera
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
import * as THREE from "three";
import type { WebGPURenderer } from "three/webgpu";
import { TSL, type TslNode } from "../gpu/tsl";
import { splatViewIndex } from "./viewIndex";
const { uint, uniform, uniformArray } = TSL;
const MAX_EYES = 2;
// What the projection and the draw read of one eye: head to eye (the
// identity for an eye that is the head) and its rotation, and its lens
export interface EyeNodes {
fromHead: TslNode;
fromHeadQuat: TslNode;
projection: TslNode;
isOrtho: TslNode;
renderSize: TslNode;
ndcZMin: TslNode;
}
export interface EyeSetting {
// World to eye (camera.matrixWorldInverse)
view: THREE.Matrix4;
projection: THREE.Matrix4;
ortho?: boolean;
}
const nodes = {
fromHeads: uniformArray(Array.from({ length: MAX_EYES }, () => new THREE.Matrix4())),
fromHeadQuats: uniformArray(Array.from({ length: MAX_EYES }, () => new THREE.Vector4(0, 0, 0, 1))),
projections: uniformArray(Array.from({ length: MAX_EYES }, () => new THREE.Matrix4())),
orthos: uniformArray(new Array<number>(MAX_EYES).fill(0), "uint"),
renderSize: uniform(new THREE.Vector2(1, 1)),
// NDC depth of the near plane: -1 (WebGL clip convention) or 0 (WebGPU)
ndcZMin: uniform(0),
};
// Clip z from one convention to the other: WebGL's -w..w to WebGPU's 0..w, and back
const TO_WEBGPU = new THREE.Matrix4().set(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0.5, 0.5, 0, 0, 0, 1);
const TO_WEBGL = TO_WEBGPU.clone().invert();
// The camera of one render as uniforms shared by every program and draw
// (set before each dispatch and draw): splats reach the projection in head
// space, so it reads each eye's head-to-eye transform and lens. A plain
// camera is one eye that is also the head; an ArrayCamera (XR, or an app's
// stereo rig) gives its cameras as eyes and its own pose as the head.
export class SplatCamera {
// In a draw's shaders, the index (uint) of the eye being drawn: one draw
// per eye of an ArrayCamera, or both in one with multiview
static readonly viewIndex: TslNode = splatViewIndex;
// Eyes of the latest set()
static eyes = 1;
// World to head of the latest set()
static readonly head = new THREE.Matrix4();
// Eye v (a number, or a TSL index such as SplatCamera.viewIndex)
static eye(v: number | TslNode): EyeNodes {
const at = typeof v === "number" ? uint(v) : v;
return {
fromHead: nodes.fromHeads.element(at),
fromHeadQuat: nodes.fromHeadQuats.element(at),
projection: nodes.projections.element(at),
isOrtho: nodes.orthos.element(at).notEqual(uint(0)),
renderSize: nodes.renderSize,
ndcZMin: nodes.ndcZMin,
};
}
// The camera of a render: `camera` as three passes it to onBeforeRender.
// Eyes whose projection is in the other clip convention than the
// renderer's (sub-cameras an app made for WebGL) are converted, not changed.
static set(renderer: WebGPURenderer, camera: THREE.Camera) {
const array = camera as THREE.ArrayCamera;
const cameras = array.isArrayCamera && array.cameras.length > 0 ? array.cameras : [camera];
const count = Math.min(cameras.length, MAX_EYES);
camera.updateMatrixWorld();
// Eyes share one size: an ArrayCamera's first viewport, else the
// current render target, else the canvas viewport (the drawing buffer
// unless setViewport narrowed it)
const size = scratch.size;
const viewport = (cameras[0] as { viewport?: THREE.Vector4 }).viewport;
const target = renderer.getRenderTarget();
if (viewport && array.isArrayCamera) {
size.set(viewport.z, viewport.w);
} else if (target) {
size.set(target.width, target.height);
} else {
renderer.getDrawingBufferSize(size);
const canvas = renderer.getViewport(scratch.viewport);
const full = renderer.getSize(scratch.canvas);
if (canvas.z !== full.x || canvas.w !== full.y) {
const ratio = renderer.getPixelRatio();
size.set(Math.floor(canvas.z * ratio), Math.floor(canvas.w * ratio));
}
}
const webgpu = renderer.coordinateSystem === THREE.WebGPUCoordinateSystem;
scratch.eyes.length = count;
for (let v = 0; v < count; v += 1) {
const eye = cameras[v];
eye.updateMatrixWorld();
scratch.eyes[v] ??= { view: new THREE.Matrix4(), projection: new THREE.Matrix4() };
const setting = scratch.eyes[v];
setting.view.copy(eye.matrixWorld).invert();
// XR eyes carry the headset's projections, in the renderer's convention
const converted = !renderer.xr.isPresenting && eye.coordinateSystem !== renderer.coordinateSystem;
setting.projection.copy(eye.projectionMatrix);
if (converted) {
setting.projection.premultiply(webgpu ? TO_WEBGPU : TO_WEBGL);
}
setting.ortho = (eye as THREE.OrthographicCamera).isOrthographicCamera === true;
}
SplatCamera.setViews({ head: scratch.head.copy(camera.matrixWorld).invert(), eyes: scratch.eyes, size, ndcZMin: webgpu ? 0 : -1 });
}
// The views directly: `head` and each eye's `view` map world to view
static setViews({ head, eyes, size, ndcZMin }: { head: THREE.Matrix4; eyes: EyeSetting[]; size: THREE.Vector2; ndcZMin: number }) {
if (eyes.length < 1 || eyes.length > MAX_EYES) {
throw new Error(`SplatCamera: 1 or ${MAX_EYES} eyes, not ${eyes.length}`);
}
SplatCamera.eyes = eyes.length;
SplatCamera.head.copy(head);
const headWorld = scratch.headWorld.copy(head).invert();
eyes.forEach(({ view, projection, ortho = false }, v) => {
// Exactly the identity for an eye that is the head (mono)
const fromHead = view.equals(head) ? scratch.fromHead.identity() : scratch.fromHead.multiplyMatrices(view, headWorld);
(nodes.fromHeads.array[v] as THREE.Matrix4).copy(fromHead);
fromHead.decompose(scratch.position, scratch.quaternion, scratch.scale);
const q = scratch.quaternion;
(nodes.fromHeadQuats.array[v] as THREE.Vector4).set(q.x, q.y, q.z, q.w);
(nodes.projections.array[v] as THREE.Matrix4).copy(projection);
nodes.orthos.array[v] = ortho ? 1 : 0;
});
(nodes.renderSize.value as THREE.Vector2).copy(size);
nodes.ndcZMin.value = ndcZMin;
}
}
const scratch = {
head: new THREE.Matrix4(),
headWorld: new THREE.Matrix4(),
size: new THREE.Vector2(),
viewport: new THREE.Vector4(),
canvas: new THREE.Vector2(),
eyes: [] as EyeSetting[],
fromHead: new THREE.Matrix4(),
position: new THREE.Vector3(),
quaternion: new THREE.Quaternion(),
scale: new THREE.Vector3(),
};