Files
spark/src/SplatMesh.ts
T

848 lines
28 KiB
TypeScript

import * as THREE from "three";
import init_wasm, { raycast_splats } from "forge-internal-rs";
import { PackedSplats } from "./PackedSplats";
import { type RgbaArray, readRgbaArray } from "./RgbaArray";
import { SplatEdit, SplatEditSdf, SplatEdits } from "./SplatEdit";
import {
type GsplatModifier,
SplatGenerator,
SplatTransformer,
} from "./SplatGenerator";
import type { SplatFileType } from "./SplatLoader";
import type { SplatSkinning } from "./SplatSkinning";
import {
DynoFloat,
DynoUsampler2DArray,
type DynoVal,
DynoVec4,
Gsplat,
add,
combineGsplat,
defineGsplat,
dyno,
dynoBlock,
dynoConst,
extendVec,
mul,
normalize,
readPackedSplat,
splitGsplat,
sub,
unindent,
unindentLines,
} from "./dyno";
import { getTextureSize } from "./utils";
export type SplatMeshOptions = {
// URL to fetch a Gaussian splat file from(supports .ply, .splat, .ksplat,
// .spz formats). (default: undefined)
url?: string;
// Raw bytes of a Gaussian splat file to decode directly instead of fetching
// from URL. (default: undefined)
fileBytes?: Uint8Array | ArrayBuffer;
// Override the file type detection for formats that can't be reliably
// auto-detected (.splat, .ksplat). (default: undefined auto-detects other
// formats from file contents)
fileType?: SplatFileType;
// File name to use for type detection. (default: undefined)
fileName?: string;
// Use an existing PackedSplats object as the source instead of loading from
// a file. Can be used to share a collection of Gsplats among multiple SplatMeshes
// (default: undefined creates a new empty PackedSplats or decoded from a
// data source above)
packedSplats?: PackedSplats;
// Reserve space for at least this many splats when constructing the mesh
// initially. (default: determined by file)
maxSplats?: number;
// Callback function to programmatically create splats at initialization
// in provided PackedSplats. (default: undefined)
constructSplats?: (splats: PackedSplats) => Promise<void> | void;
// Callback function that is called when mesh initialization is complete.
// (default: undefined)
onLoad?: (mesh: SplatMesh) => Promise<void> | void;
// Controls whether SplatEdits have any effect on this mesh. (default: true)
editable?: boolean;
// Callback function that is called every frame to update the mesh.
// Call mesh.updateVersion() if splats need to be regenerated due to some change.
// Calling updateVersion() is not necessary for object transformations, recoloring,
// or opacity adjustments as these are auto-detected. (default: undefined)
onFrame?: ({
mesh,
time,
deltaTime,
}: { mesh: SplatMesh; time: number; deltaTime: number }) => void;
// Gsplat modifier to apply in object-space before any transformations.
// A GsplatModifier is a dyno shader-graph block that transforms an input
// gsplat: DynoVal<Gsplat> to an output gsplat: DynoVal<Gsplat> with gsplat.center
// coordinate in object-space. (default: undefined)
objectModifier?: GsplatModifier;
// Gsplat modifier to apply in world-space after transformations.
// (default: undefined)
worldModifier?: GsplatModifier;
};
export type SplatMeshContext = {
transform: SplatTransformer;
viewToWorld: SplatTransformer;
worldToView: SplatTransformer;
viewToObject: SplatTransformer;
recolor: DynoVec4<THREE.Vector4>;
time: DynoFloat;
deltaTime: DynoFloat;
};
export class SplatMesh extends SplatGenerator {
// A Promise<SplatMesh> you can await to ensure fetching, parsing,
// and initialization has completed
initialized: Promise<SplatMesh>;
// A boolean indicating whether initialization is complete
isInitialized = false;
// If you modify packedSplats you should set
// splatMesh.packedSplats.needsUpdate = true to signal to Three.js that it
// should re-upload the data to the underlying texture. Use this sparingly with
// objects with smaller Gsplat counts as it requires a CPU-GPU data transfer for
// each frame. Thousands to tens of thousands of Gsplats ir fine. (See hands.ts
// for an example of rendering "Gsplat hands" in WebXR using this technique.)
packedSplats: PackedSplats;
// A THREE.Color that can be used to tint all splats in the mesh.
// (default: new THREE.Color(1, 1, 1))
recolor: THREE.Color = new THREE.Color(1, 1, 1);
// Global opacity multiplier for all splats in the mesh. (default: 1)
opacity = 1;
// A SplatMeshContext consisting of useful scene and object dyno uniforms that can
// be used to in the Gsplat processing pipeline, for example via objectModifier and
// worldModifier. (created on construction)
context: SplatMeshContext;
onFrame?: ({
mesh,
time,
deltaTime,
}: { mesh: SplatMesh; time: number; deltaTime: number }) => void;
objectModifier?: GsplatModifier;
worldModifier?: GsplatModifier;
// Set to true to have the viewToObject property in context be updated each frame.
// If the mesh has extra.sh1 (first order spherical harmonics directional lighting)
// this property will always be updated. (default: false)
enableViewToObject = false;
// Set to true to have context.viewToWorld updated each frame. (default: false)
enableViewToWorld = false;
// Set to true to have context.worldToView updated each frame. (default: false)
enableWorldToView = false;
// Optional SplatSkinning instance for animating splats with dual-quaternion
// skeletal animation. (default: null)
skinning: SplatSkinning | null = null;
// Optional list of SplatEdits to apply to the mesh. If null, any SplatEdit
// children in the scene graph will be added automatically. (default: null)
edits: SplatEdit[] | null = null;
editable: boolean;
// Compiled SplatEdits for applying SDF edits to splat RGBA + centers
private rgbaDisplaceEdits: SplatEdits | null = null;
// Optional RgbaArray to overwrite splat RGBA values with custom values.
// Useful for "baking" RGB and opacity edits into the SplatMesh. (default: null)
splatRgba: RgbaArray | null = null;
// Maximum Spherical Harmonics level to use. Call updateGenerator()
// after changing. (default: 3)
maxSh = 3;
constructor(options: SplatMeshOptions = {}) {
const transform = new SplatTransformer();
const viewToWorld = new SplatTransformer();
const worldToView = new SplatTransformer();
const viewToObject = new SplatTransformer();
const recolor = new DynoVec4({
value: new THREE.Vector4(
Number.NEGATIVE_INFINITY,
Number.NEGATIVE_INFINITY,
Number.NEGATIVE_INFINITY,
Number.NEGATIVE_INFINITY,
),
});
const time = new DynoFloat({ value: 0 });
const deltaTime = new DynoFloat({ value: 0 });
const context = {
transform,
viewToWorld,
worldToView,
viewToObject,
recolor,
time,
deltaTime,
};
super({
update: ({ time, deltaTime, viewToWorld, globalEdits }) =>
this.update({ time, deltaTime, viewToWorld, globalEdits }),
});
this.packedSplats = options.packedSplats ?? new PackedSplats();
this.numSplats = this.packedSplats.numSplats;
this.editable = options.editable ?? true;
this.onFrame = options.onFrame;
this.context = context;
this.objectModifier = options.objectModifier;
this.worldModifier = options.worldModifier;
this.updateGenerator();
if (
options.url ||
options.fileBytes ||
options.constructSplats ||
(options.packedSplats && !options.packedSplats.isInitialized)
) {
// We need to initialize asynchronously given the options
this.initialized = this.asyncInitialize(options).then(async () => {
this.updateGenerator();
this.isInitialized = true;
if (options.onLoad) {
const maybePromise = options.onLoad(this);
if (maybePromise instanceof Promise) {
await maybePromise;
}
}
return this;
});
} else {
this.isInitialized = true;
this.initialized = Promise.resolve(this);
if (options.onLoad) {
const maybePromise = options.onLoad(this);
// If onLoad returns a promise, wait for it to complete
if (maybePromise instanceof Promise) {
this.initialized = maybePromise.then(() => this);
}
}
}
}
async asyncInitialize(options: SplatMeshOptions) {
const { url, fileBytes, fileType, fileName, maxSplats, constructSplats } =
options;
if (url || fileBytes || constructSplats) {
const packedSplatsOptions = {
url,
fileBytes,
fileType,
fileName,
maxSplats,
construct: constructSplats,
};
this.packedSplats.reinitialize(packedSplatsOptions);
}
if (this.packedSplats) {
await this.packedSplats.initialized;
this.numSplats = this.packedSplats.numSplats;
this.updateGenerator();
}
}
static staticInitialized = SplatMesh.staticInitialize();
static isStaticInitialized = false;
static dynoTime = new DynoFloat({ value: 0 });
static async staticInitialize() {
await init_wasm();
SplatMesh.isStaticInitialized = true;
}
// Creates a new Gsplat with the provided parameters (all values in "float" space,
// i.e. 0-1 for opacity and color) and adds it to the end of the packedSplats,
// increasing numSplats by 1. If necessary, reallocates the buffer with an exponential
// doubling strategy to fit the new data, so it's fairly efficient to just
// pushSplat(...) each Gsplat you want to create in a loop.
pushSplat(
center: THREE.Vector3,
scales: THREE.Vector3,
quaternion: THREE.Quaternion,
opacity: number,
color: THREE.Color,
) {
this.packedSplats.pushSplat(center, scales, quaternion, opacity, color);
}
// This method iterates over all Gsplats in this instance's packedSplats,
// invoking the provided callback with index: number in 0..=(this.numSplats-1) and
// center: THREE.Vector3, scales: THREE.Vector3, quaternion: THREE.Quaternion,
// opacity: number (0..1), and color: THREE.Color (rgb values in 0..1).
// Note that the objects passed in as center etc. are the same for every callback
// invocation: these objects are reused for efficiency. Changing these values has
// no effect as they are decoded/unpacked copies of the underlying data. To update
// the packedSplats, call .packedSplats.setSplat(index, center, scales,
// quaternion, opacity, color).
forEachSplat(
callback: (
index: number,
center: THREE.Vector3,
scales: THREE.Vector3,
quaternion: THREE.Quaternion,
opacity: number,
color: THREE.Color,
) => void,
) {
this.packedSplats.forEachSplat(callback);
}
// Call this when you are finished with the SplatMesh and want to free
// any buffers it holds (via packedSplats).
dispose() {
this.packedSplats.dispose();
}
constructGenerator(context: SplatMeshContext) {
const { transform, viewToObject, recolor } = context;
const generator = dynoBlock(
{ index: "int" },
{ gsplat: Gsplat },
({ index }) => {
if (!index) {
throw new Error("index is undefined");
}
// Read a Gsplat from the PackedSplats template
let gsplat = readPackedSplat(this.packedSplats.dyno, index);
if (this.maxSh >= 1) {
// Inject lighting from SH1..SH3
const { sh1Texture, sh2Texture, sh3Texture } =
this.ensureShTextures();
if (sh1Texture) {
//Calculate view direction in object space
const viewCenterInObject = viewToObject.translate;
const { center } = splitGsplat(gsplat).outputs;
const viewDir = normalize(sub(center, viewCenterInObject));
// Evaluate Spherical Harmonics
let rgb = evaluateSH1(gsplat, sh1Texture, viewDir);
if (this.maxSh >= 2 && sh2Texture) {
rgb = add(rgb, evaluateSH2(gsplat, sh2Texture, viewDir));
}
if (this.maxSh >= 3 && sh3Texture) {
rgb = add(rgb, evaluateSH3(gsplat, sh3Texture, viewDir));
}
// Flash off for 0.3 / 1.0 sec for debugging
// const fractTime = fract(SplatMesh.dynoTime);
// const lessThan05 = lessThan(fractTime, dynoConst("float", 0.3));
// rgb = select(lessThan05, dynoConst("vec3", new THREE.Vector3()), rgb);
// Add SH lighting to RGBA
let { rgba } = splitGsplat(gsplat).outputs;
rgba = add(rgba, extendVec(rgb, dynoConst("float", 0.0)));
gsplat = combineGsplat({ gsplat, rgba });
}
}
if (this.splatRgba) {
// Overwrite RGBA with baked RGBA values
const rgba = readRgbaArray(this.splatRgba.dyno, index);
gsplat = combineGsplat({ gsplat, rgba });
}
if (this.skinning) {
// Transform according to bones + skinning weights
gsplat = this.skinning.modify(gsplat);
}
if (this.objectModifier) {
// Inject object-space Gsplat modifier dyno
gsplat = this.objectModifier.apply({ gsplat }).gsplat;
}
// Transform from object to world-space
gsplat = transform.applyGsplat(gsplat);
// Apply any global recoloring and opacity
const recolorRgba = mul(recolor, splitGsplat(gsplat).outputs.rgba);
gsplat = combineGsplat({ gsplat, rgba: recolorRgba });
if (this.rgbaDisplaceEdits) {
// Apply RGBA edit layer SDFs
gsplat = this.rgbaDisplaceEdits.modify(gsplat);
}
if (this.worldModifier) {
// Inject world-space Gsplat modifier dyno
gsplat = this.worldModifier.apply({ gsplat }).gsplat;
}
// We're done! Output resulting Gsplat
return { gsplat };
},
);
this.generator = generator;
}
// Call this whenever something changes in the Gsplat processing pipeline,
// for example changing maxSh or updating objectModifier or worldModifier.
// Compiled generators are cached for efficiency and re-use when the same
// pipeline structure emerges after successive changes.
updateGenerator() {
this.constructGenerator(this.context);
}
// This is called automatically by ForgeRenderer and you should not have to
// call it. It updates parameters for the generated pipeline and calls
// updateGenerator() if the pipeline needs to change.
update({
time,
viewToWorld,
deltaTime,
globalEdits,
}: {
time: number;
viewToWorld: THREE.Matrix4;
deltaTime: number;
globalEdits: SplatEdit[];
}) {
this.numSplats = this.packedSplats.numSplats;
this.context.time.value = time;
this.context.deltaTime.value = deltaTime;
SplatMesh.dynoTime.value = time;
const { transform, viewToObject, recolor } = this.context;
let updated = transform.update(this);
if (
this.context.viewToWorld.updateFromMatrix(viewToWorld) &&
this.enableViewToWorld
) {
updated = true;
}
const worldToView = viewToWorld.clone().invert();
if (
this.context.worldToView.updateFromMatrix(worldToView) &&
this.enableWorldToView
) {
updated = true;
}
const objectToWorld = new THREE.Matrix4().compose(
transform.translate.value,
transform.rotate.value,
new THREE.Vector3().setScalar(transform.scale.value),
);
const worldToObject = objectToWorld.invert();
const viewToObjectMatrix = worldToObject.multiply(viewToWorld);
if (
viewToObject.updateFromMatrix(viewToObjectMatrix) &&
(this.enableViewToObject || this.packedSplats.extra.sh1)
) {
// Only trigger update if we have view-dependent spherical harmonics
updated = true;
}
const newRecolor = new THREE.Vector4(
this.recolor.r,
this.recolor.g,
this.recolor.b,
this.opacity,
);
if (!newRecolor.equals(recolor.value)) {
recolor.value.copy(newRecolor);
updated = true;
}
const edits = this.editable ? (this.edits ?? []).concat(globalEdits) : [];
if (this.editable && !this.edits) {
// If we haven't set any explicit edits, add any child SplatEdits
this.traverseVisible((node) => {
if (node instanceof SplatEdit) {
edits.push(node);
}
});
}
edits.sort((a, b) => a.ordering - b.ordering);
const editsSdfs = edits.map((edit) => {
if (edit.sdfs != null) {
return { edit, sdfs: edit.sdfs };
}
const sdfs: SplatEditSdf[] = [];
edit.traverseVisible((node) => {
if (node instanceof SplatEditSdf) {
sdfs.push(node);
}
});
return { edit, sdfs };
});
if (editsSdfs.length > 0 && !this.rgbaDisplaceEdits) {
const edits = editsSdfs.length;
const sdfs = editsSdfs.reduce(
(total, edit) => total + edit.sdfs.length,
0,
);
this.rgbaDisplaceEdits = new SplatEdits({
maxEdits: edits,
maxSdfs: sdfs,
});
this.updateGenerator();
}
if (this.rgbaDisplaceEdits) {
const editResult = this.rgbaDisplaceEdits.update(editsSdfs);
updated ||= editResult.updated;
if (editResult.dynoUpdated) {
this.updateGenerator();
}
}
if (updated) {
this.updateVersion();
}
this.onFrame?.({ mesh: this, time, deltaTime });
}
// This method conforms to the standard THREE.Raycaster API, performing object-ray
// intersections using this method to populate the provided intersects[] array
// with each intersection point.
raycast(
raycaster: THREE.Raycaster,
intersects: {
distance: number;
point: THREE.Vector3;
object: THREE.Object3D;
}[],
) {
if (!this.packedSplats.packedArray || !this.packedSplats.numSplats) {
return;
}
const { near, far, ray } = raycaster;
const worldToMesh = this.matrixWorld.clone().invert();
const worldToMeshRot = new THREE.Matrix3().setFromMatrix4(worldToMesh);
const origin = ray.origin.clone().applyMatrix4(worldToMesh);
const direction = ray.direction.clone().applyMatrix3(worldToMeshRot);
const scales = new THREE.Vector3();
worldToMesh.decompose(new THREE.Vector3(), new THREE.Quaternion(), scales);
const scale = (scales.x * scales.y * scales.z) ** (1.0 / 3.0);
const RAYCAST_ELLIPSOID = true;
const distances = raycast_splats(
origin.x,
origin.y,
origin.z,
direction.x,
direction.y,
direction.z,
near,
far,
this.packedSplats.numSplats,
this.packedSplats.packedArray,
RAYCAST_ELLIPSOID,
);
for (const distance of distances) {
const point = ray.direction
.clone()
.multiplyScalar(distance)
.add(ray.origin);
intersects.push({
distance,
point,
object: this,
});
}
}
private ensureShTextures(): {
sh1Texture?: DynoUsampler2DArray<"sh1", THREE.DataArrayTexture>;
sh2Texture?: DynoUsampler2DArray<"sh2", THREE.DataArrayTexture>;
sh3Texture?: DynoUsampler2DArray<"sh3", THREE.DataArrayTexture>;
} {
// Ensure we have textures for SH1..SH3 if we have data
if (!this.packedSplats.extra.sh1) {
return {};
}
let sh1Texture = this.packedSplats.extra.sh1Texture as
| DynoUsampler2DArray<"sh1", THREE.DataArrayTexture>
| undefined;
if (!sh1Texture) {
let sh1 = this.packedSplats.extra.sh1 as Uint32Array;
const { width, height, depth, maxSplats } = getTextureSize(
sh1.length / 2,
);
if (sh1.length < maxSplats * 2) {
const newSh1 = new Uint32Array(maxSplats * 2);
newSh1.set(sh1);
this.packedSplats.extra.sh1 = newSh1;
sh1 = newSh1;
}
const texture = new THREE.DataArrayTexture(sh1, width, height, depth);
texture.format = THREE.RGIntegerFormat;
texture.type = THREE.UnsignedIntType;
texture.internalFormat = "RG32UI";
texture.needsUpdate = true;
sh1Texture = new DynoUsampler2DArray({
value: texture,
key: "sh1",
});
this.packedSplats.extra.sh1Texture = sh1Texture;
}
if (!this.packedSplats.extra.sh2) {
return { sh1Texture };
}
let sh2Texture = this.packedSplats.extra.sh2Texture as
| DynoUsampler2DArray<"sh2", THREE.DataArrayTexture>
| undefined;
if (!sh2Texture) {
let sh2 = this.packedSplats.extra.sh2 as Uint32Array;
const { width, height, depth, maxSplats } = getTextureSize(
sh2.length / 4,
);
if (sh2.length < maxSplats * 4) {
const newSh2 = new Uint32Array(maxSplats * 4);
newSh2.set(sh2);
this.packedSplats.extra.sh2 = newSh2;
sh2 = newSh2;
}
const texture = new THREE.DataArrayTexture(sh2, width, height, depth);
texture.format = THREE.RGBAIntegerFormat;
texture.type = THREE.UnsignedIntType;
texture.internalFormat = "RGBA32UI";
texture.needsUpdate = true;
sh2Texture = new DynoUsampler2DArray({
value: texture,
key: "sh2",
});
this.packedSplats.extra.sh2Texture = sh2Texture;
}
if (!this.packedSplats.extra.sh3) {
return { sh1Texture, sh2Texture };
}
let sh3Texture = this.packedSplats.extra.sh3Texture as
| DynoUsampler2DArray<"sh3", THREE.DataArrayTexture>
| undefined;
if (!sh3Texture) {
let sh3 = this.packedSplats.extra.sh3 as Uint32Array;
const { width, height, depth, maxSplats } = getTextureSize(
sh3.length / 4,
);
if (sh3.length < maxSplats * 4) {
const newSh3 = new Uint32Array(maxSplats * 4);
newSh3.set(sh3);
this.packedSplats.extra.sh3 = newSh3;
sh3 = newSh3;
}
const texture = new THREE.DataArrayTexture(sh3, width, height, depth);
texture.format = THREE.RGBAIntegerFormat;
texture.type = THREE.UnsignedIntType;
texture.internalFormat = "RGBA32UI";
texture.needsUpdate = true;
sh3Texture = new DynoUsampler2DArray({
value: texture,
key: "sh3",
});
this.packedSplats.extra.sh3Texture = sh3Texture;
}
return { sh1Texture, sh2Texture, sh3Texture };
}
}
const defineEvaluateSH1 = unindent(`
vec3 evaluateSH1(Gsplat gsplat, usampler2DArray sh1, vec3 viewDir) {
// Extract sint7 values packed into 2 x uint32
uvec2 packed = texelFetch(sh1, splatTexCoord(gsplat.index), 0).rg;
vec3 sh1_0 = vec3(ivec3(
int(packed.x << 25u) >> 25,
int(packed.x << 18u) >> 25,
int(packed.x << 11u) >> 25
)) / 63.0;
vec3 sh1_1 = vec3(ivec3(
int(packed.x << 4u) >> 25,
int((packed.x >> 3u) | (packed.y << 29u)) >> 25,
int(packed.y << 22u) >> 25
)) / 63.0;
vec3 sh1_2 = vec3(ivec3(
int(packed.y << 15u) >> 25,
int(packed.y << 8u) >> 25,
int(packed.y << 1u) >> 25
)) / 63.0;
return sh1_0 * (-0.4886025 * viewDir.y)
+ sh1_1 * (0.4886025 * viewDir.z)
+ sh1_2 * (-0.4886025 * viewDir.x);
}
`);
const defineEvaluateSH2 = unindent(`
vec3 evaluateSH2(Gsplat gsplat, usampler2DArray sh2, vec3 viewDir) {
// Extract sint8 values packed into 4 x uint32
uvec4 packed = texelFetch(sh2, splatTexCoord(gsplat.index), 0);
vec3 sh2_0 = vec3(ivec3(
int(packed.x << 24u) >> 24,
int(packed.x << 16u) >> 24,
int(packed.x << 8u) >> 24
)) / 127.0;
vec3 sh2_1 = vec3(ivec3(
int(packed.x) >> 24,
int(packed.y << 24u) >> 24,
int(packed.y << 16u) >> 24
)) / 127.0;
vec3 sh2_2 = vec3(ivec3(
int(packed.y << 8u) >> 24,
int(packed.y) >> 24,
int(packed.z << 24u) >> 24
)) / 127.0;
vec3 sh2_3 = vec3(ivec3(
int(packed.z << 16u) >> 24,
int(packed.z << 8u) >> 24,
int(packed.z) >> 24
)) / 127.0;
vec3 sh2_4 = vec3(ivec3(
int(packed.w << 24u) >> 24,
int(packed.w << 16u) >> 24,
int(packed.w << 8u) >> 24
)) / 127.0;
return sh2_0 * (1.0925484 * viewDir.x * viewDir.y)
+ sh2_1 * (1.0925484 * viewDir.y * viewDir.z)
+ sh2_2 * (0.3153915 * (2.0 * viewDir.z * viewDir.z - viewDir.x * viewDir.x - viewDir.y * viewDir.y))
+ sh2_3 * (1.0925484 * viewDir.x * viewDir.z)
+ sh2_4 * (0.5462742 * (viewDir.x * viewDir.x - viewDir.y * viewDir.y));
}
`);
const defineEvaluateSH3 = unindent(`
vec3 evaluateSH3(Gsplat gsplat, usampler2DArray sh3, vec3 viewDir) {
// Extract sint6 values packed into 4 x uint32
uvec4 packed = texelFetch(sh3, splatTexCoord(gsplat.index), 0);
vec3 sh3_0 = vec3(ivec3(
int(packed.x << 26u) >> 26,
int(packed.x << 20u) >> 26,
int(packed.x << 14u) >> 26
)) / 31.0;
vec3 sh3_1 = vec3(ivec3(
int(packed.x << 8u) >> 26,
int(packed.x << 2u) >> 26,
int((packed.x >> 4u) | (packed.y << 28u)) >> 26
)) / 31.0;
vec3 sh3_2 = vec3(ivec3(
int(packed.y << 22u) >> 26,
int(packed.y << 16u) >> 26,
int(packed.y << 10u) >> 26
)) / 31.0;
vec3 sh3_3 = vec3(ivec3(
int(packed.y << 4u) >> 26,
int((packed.y >> 2u) | (packed.z << 30u)) >> 26,
int(packed.z << 24u) >> 26
)) / 31.0;
vec3 sh3_4 = vec3(ivec3(
int(packed.z << 18u) >> 26,
int(packed.z << 12u) >> 26,
int(packed.z << 6u) >> 26
)) / 31.0;
vec3 sh3_5 = vec3(ivec3(
int(packed.z) >> 26,
int(packed.w << 26u) >> 26,
int(packed.w << 20u) >> 26
)) / 31.0;
vec3 sh3_6 = vec3(ivec3(
int(packed.w << 14u) >> 26,
int(packed.w << 8u) >> 26,
int(packed.w << 2u) >> 26
)) / 31.0;
float xx = viewDir.x * viewDir.x;
float yy = viewDir.y * viewDir.y;
float zz = viewDir.z * viewDir.z;
float xy = viewDir.x * viewDir.y;
float yz = viewDir.y * viewDir.z;
float zx = viewDir.z * viewDir.x;
return sh3_0 * (-0.5900436 * viewDir.y * (3.0 * xx - yy))
+ sh3_1 * (2.8906114 * xy * viewDir.z) +
+ sh3_2 * (-0.4570458 * viewDir.y * (4.0 * zz - xx - yy))
+ sh3_3 * (0.3731763 * viewDir.z * (2.0 * zz - 3.0 * xx - 3.0 * yy))
+ sh3_4 * (-0.4570458 * viewDir.x * (4.0 * zz - xx - yy))
+ sh3_5 * (1.4453057 * viewDir.z * (xx - yy))
+ sh3_6 * (-0.5900436 * viewDir.x * (xx - 3.0 * yy));
}
`);
export function evaluateSH1(
gsplat: DynoVal<typeof Gsplat>,
sh1: DynoUsampler2DArray<"sh1", THREE.DataArrayTexture>,
viewDir: DynoVal<"vec3">,
): DynoVal<"vec3"> {
return dyno({
inTypes: { gsplat: Gsplat, sh1: "usampler2DArray", viewDir: "vec3" },
outTypes: { rgb: "vec3" },
inputs: { gsplat, sh1, viewDir },
globals: () => [defineGsplat, defineEvaluateSH1],
statements: ({ inputs, outputs }) => {
const statements = unindentLines(`
if (isGsplatActive(${inputs.gsplat}.flags)) {
${outputs.rgb} = evaluateSH1(${inputs.gsplat}, ${inputs.sh1}, ${inputs.viewDir});
} else {
${outputs.rgb} = vec3(0.0);
}
`);
return statements;
},
}).outputs.rgb;
}
export function evaluateSH2(
gsplat: DynoVal<typeof Gsplat>,
sh2: DynoVal<"usampler2DArray">,
viewDir: DynoVal<"vec3">,
): DynoVal<"vec3"> {
return dyno({
inTypes: { gsplat: Gsplat, sh2: "usampler2DArray", viewDir: "vec3" },
outTypes: { rgb: "vec3" },
inputs: { gsplat, sh2, viewDir },
globals: () => [defineGsplat, defineEvaluateSH2],
statements: ({ inputs, outputs }) =>
unindentLines(`
if (isGsplatActive(${inputs.gsplat}.flags)) {
${outputs.rgb} = evaluateSH2(${inputs.gsplat}, ${inputs.sh2}, ${inputs.viewDir});
} else {
${outputs.rgb} = vec3(0.0);
}
`),
}).outputs.rgb;
}
export function evaluateSH3(
gsplat: DynoVal<typeof Gsplat>,
sh3: DynoVal<"usampler2DArray">,
viewDir: DynoVal<"vec3">,
): DynoVal<"vec3"> {
return dyno({
inTypes: { gsplat: Gsplat, sh3: "usampler2DArray", viewDir: "vec3" },
outTypes: { rgb: "vec3" },
inputs: { gsplat, sh3, viewDir },
globals: () => [defineGsplat, defineEvaluateSH3],
statements: ({ inputs, outputs }) =>
unindentLines(`
if (isGsplatActive(${inputs.gsplat}.flags)) {
${outputs.rgb} = evaluateSH3(${inputs.gsplat}, ${inputs.sh3}, ${inputs.viewDir});
} else {
${outputs.rgb} = vec3(0.0);
}
`),
}).outputs.rgb;
}