splatTransform.tsl.ts
src/render/splatTransform.tsl.ts · raw · all examples · site home
Exports: ModifierSpaceTransformNodestransformsisUniformScalesetTransformstransformCovtransformSplattransformFormconvertForm
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 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181
// Splats through a transform (TransformNodes: a matrix, and when it is a
// rotation times a uniform scale its quaternion and scale), in either form:
// covariances take any matrix (Sigma' = M Sigma M^T); 3DGS splats take the
// quaternion and scale when the transform is uniform, else go through a
// covariance and back (an eigen decomposition). Each splat object's
// transforms to head space are uniforms shared by every program, set per
// dispatch.
import * as THREE from "three";
import { GpuSplatMath } from "../gpu/math.tsl";
import { SplatGenerator } from "../gpu/splatGenerator";
import { GpuSplats, type CovSplatNode, type SplatNode } from "../gpu/splats.tsl";
import { TSL, type TslNode } from "../gpu/tsl";
import type { SplatForm } from "./projection.tsl";
import { SplatCamera } from "./splatCamera";
const { If, cross, dot, float, length, normalize, select, transpose, uint, uniform, vec3, vec4 } = TSL;
// Where a modifier runs: in view (head) space, world space, the object's
// own space, or the frame of a matrix (a frame's matrixWorld, read every
// render)
export type ModifierSpace = "view" | "world" | "object" | THREE.Matrix4;
// One transform as uniforms: the matrix, and for one that is a rotation
// times a uniform scale (`uniform` 1) its quaternion and scale
export interface TransformNodes {
matrix: TslNode;
quat: TslNode;
scale: TslNode;
uniform: TslNode;
}
const transformNodes = (): TransformNodes => ({
matrix: uniform(new THREE.Matrix4()),
quat: uniform(new THREE.Vector4(0, 0, 0, 1)),
scale: uniform(1),
uniform: uniform(1, "uint"),
});
// An object's splats to head space: `a`, or `a` to the modifier's space and
// `b` from there to head space
export const transforms = { a: transformNodes(), b: transformNodes() };
// Whether the 3x3 part M of `m` is a rotation times a uniform scale: with
// G = M^T M and s2 = trace(G) / 3, every entry of G - s2 I within
// `tolerance` x s2, and det(M) > 0 (a mirror has no rotation quaternion)
export function isUniformScale(m: THREE.Matrix4, tolerance = 1e-6) {
const e = m.elements;
const cols = [[e[0], e[1], e[2]], [e[4], e[5], e[6]], [e[8], e[9], e[10]]];
const d = (p: number[], q: number[]) => p[0] * q[0] + p[1] * q[1] + p[2] * q[2];
const s2 = (d(cols[0], cols[0]) + d(cols[1], cols[1]) + d(cols[2], cols[2])) / 3;
if (!(s2 > 0)) {
return false;
}
for (let i = 0; i < 3; i += 1) {
for (let j = i; j < 3; j += 1) {
if (Math.abs(d(cols[i], cols[j]) - (i === j ? s2 : 0)) > tolerance * s2) {
return false;
}
}
}
return m.determinant() > 0;
}
function setTransform(t: TransformNodes, m: THREE.Matrix4) {
(t.matrix.value as THREE.Matrix4).copy(m);
const uniformScale = isUniformScale(m);
t.uniform.value = uniformScale ? 1 : 0;
if (uniformScale) {
m.decompose(scratch.position, scratch.quaternion, scratch.scale);
const q = scratch.quaternion;
(t.quat.value as THREE.Vector4).set(q.x, q.y, q.z, q.w);
t.scale.value = Math.cbrt(Math.abs(m.determinant()));
}
}
// Object to head (SplatCamera.head) as `a`, or split around the modifier's
// space: a (object to that space), then b (that space to head), in float64
export function setTransforms(object: THREE.Matrix4, space: ModifierSpace | undefined) {
const head = SplatCamera.head;
const { a, b } = scratch;
if (space === undefined || space === "view") {
a.multiplyMatrices(head, object);
b.identity();
} else if (space === "object") {
a.identity();
b.multiplyMatrices(head, object);
} else if (space === "world") {
a.copy(object);
b.copy(head);
} else {
a.copy(space).invert().multiply(object);
b.multiplyMatrices(head, space);
}
setTransform(transforms.a, a);
setTransform(transforms.b, b);
// The generator's context (object space), and with a modifier its
// context (b: its space to head; the head's inverse made once per camera)
if (space === undefined) {
SplatGenerator.setContext(object, head);
return;
}
if (!scratch.head.equals(head)) {
scratch.head.copy(head);
scratch.headToWorld.copy(head).invert();
}
SplatGenerator.setContext(object, head, { toWorld: scratch.spaceToWorld.multiplyMatrices(scratch.headToWorld, b), toHead: b });
}
const scratch = {
a: new THREE.Matrix4(),
b: new THREE.Matrix4(),
spaceToWorld: new THREE.Matrix4(),
head: new THREE.Matrix4().set(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
headToWorld: new THREE.Matrix4(),
position: new THREE.Vector3(),
quaternion: new THREE.Quaternion(),
scale: new THREE.Vector3(),
};
export function transformCov(c: CovSplatNode, t: TransformNodes): CovSplatNode {
const out = GpuSplats.CovSplat().toVar();
const M = GpuSplatMath.mat3FromMat4(t.matrix).toVar();
const cov = M.mul(GpuSplatMath.covMatrix(c.get("xxyyzz"), c.get("xyxzyz"))).mul(transpose(M)).toVar();
out.get("center").assign(t.matrix.mul(vec4(c.get("center"), 1.0)).xyz);
out.get("xxyyzz").assign(vec3(cov.element(0).x, cov.element(1).y, cov.element(2).z));
out.get("xyxzyz").assign(vec3(cov.element(0).y, cov.element(0).z, cov.element(1).z));
out.get("rgb").assign(c.get("rgb"));
out.get("opacity").assign(c.get("opacity"));
out.get("index").assign(c.get("index"));
// Normals by the inverse transpose: the cofactor matrix, signed by det(M)
const c0 = M.element(0).toVar();
const c1 = M.element(1).toVar();
const c2 = M.element(2).toVar();
const n = c.get("normal").toVar();
const cofactor = cross(c1, c2).mul(n.x).add(cross(c2, c0).mul(n.y)).add(cross(c0, c1).mul(n.z)).toVar();
const signed = cofactor.mul(select(dot(c0, cross(c1, c2)).lessThan(0.0), float(-1.0), float(1.0))).toVar();
out.get("normal").assign(select(length(signed).greaterThan(0.0), normalize(signed), vec3(0.0)));
return out;
}
// A uniform transform of a 3DGS splat
function transformUniform(s: SplatNode, t: TransformNodes): SplatNode {
const out = GpuSplats.Splat().toVar();
out.get("center").assign(t.matrix.mul(vec4(s.get("center"), 1.0)).xyz);
out.get("quaternion").assign(GpuSplatMath.quatMul(t.quat, normalize(s.get("quaternion"))));
out.get("scales").assign(s.get("scales").mul(t.scale));
out.get("rgb").assign(s.get("rgb"));
out.get("opacity").assign(s.get("opacity"));
out.get("index").assign(s.get("index"));
return out;
}
export function transformSplat(s: SplatNode, t: TransformNodes): SplatNode {
const out = GpuSplats.Splat().toVar();
If(t.uniform.equal(uint(1)), () => {
out.assign(transformUniform(s, t));
}).Else(() => {
out.assign(GpuSplats.fromCov(transformCov(GpuSplats.toCov(s), t)));
});
return out;
}
// A node of `form` through `t`; `next` is the form wanted afterwards: a
// 3DGS splat wanted as a covariance converts first (exact for any matrix)
export function transformForm(node: SplatNode | CovSplatNode, form: SplatForm, t: TransformNodes, next?: SplatForm): { node: SplatNode | CovSplatNode; form: SplatForm } {
if (form === "cov") {
return { node: transformCov(node as CovSplatNode, t), form: "cov" };
}
if (next === "cov") {
return { node: transformCov(GpuSplats.toCov(node as SplatNode), t), form: "cov" };
}
return { node: transformSplat(node as SplatNode, t), form: "splat" };
}
export function convertForm(node: SplatNode | CovSplatNode, from: SplatForm, to: SplatForm): SplatNode | CovSplatNode {
if (from === to) {
return node;
}
return to === "cov" ? GpuSplats.toCov(node as SplatNode) : GpuSplats.fromCov(node as CovSplatNode);
}