Initial commit

This commit is contained in:
Diego Marcos
2025-05-23 08:52:14 -07:00
committed by Diego Marcos Segura
commit 3332f609a9
129 changed files with 64575 additions and 0 deletions
+856
View File
@@ -0,0 +1,856 @@
import * as THREE from "three";
import { ForgeViewpoint, type ForgeViewpointOptions } from "./ForgeViewpoint";
import { PackedSplats } from "./PackedSplats";
import { RgbaArray } from "./RgbaArray";
import { type GeneratorMapping, SplatAccumulator } from "./SplatAccumulator";
import { SplatEdit } from "./SplatEdit";
import { SplatGenerator, SplatModifier } from "./SplatGenerator";
import { SplatGeometry } from "./SplatGeometry";
import { SplatMesh } from "./SplatMesh";
import {
DynoVec3,
DynoVec4,
Gsplat,
TPackedSplats,
dynoBlock,
readPackedSplat,
transformGsplat,
} from "./dyno";
import { getShaders } from "./shaders";
import {
averagePositions,
averageQuaternions,
cloneClock,
withinCoorientDist,
} from "./utils";
// ForgeRenderer aggregates splats from multiple generators into a single
// accumulated collection per frame. In normal operation we only need a
// maximum of 3 accumulators: One currently being viewed, one currently
// being sorted, and one more for generating the next frame. Accumulators
// must be "released" by each viewpoint using it, so in unusual cases
// such as slow render-outs, we may want to allow more than 3 so the
// pipeline can continue generating new frames, but we limit to a maximum
// of 5 to avoid excessive memory usage.
const MAX_ACCUMULATORS = 5;
// Scene.onBeforeRender monkey-patch to
// inject a ForgeRenderer into a scene with SplatMeshes if there isn't
// one already. Restore original Scene.onBeforeRenderer and Scene.add when done.
let hasSplatMesh = false;
let hasForgeRenderer = false;
let forgeRendererInstance: ForgeRenderer;
const sceneAdd = THREE.Scene.prototype.add;
THREE.Scene.prototype.add = function (object) {
hasSplatMesh = hasSplatMesh || object instanceof SplatMesh;
hasForgeRenderer = hasForgeRenderer || object instanceof ForgeRenderer;
sceneAdd.call(this, object);
return this;
};
const sceneOnBeforeRender = THREE.Scene.prototype.onBeforeRender;
THREE.Scene.prototype.onBeforeRender = function (renderer) {
if (!hasSplatMesh) {
return;
}
if (!hasForgeRenderer) {
const forge = forgeRendererInstance || new ForgeRenderer({ renderer });
this.add(forge);
}
THREE.Scene.prototype.onBeforeRender = sceneOnBeforeRender;
THREE.Scene.prototype.add = sceneAdd;
};
export type ForgeRendererOptions = {
// Pass in your THREE.WebGLRenderer instance so Forge can perform work
// outside the usual render loop. Should be created with antialias: false
// (default setting) as WebGL anti-aliasing doesn't improve Gaussian Splatting
// rendering and significantly reduces performance.
renderer: THREE.WebGLRenderer;
// Pass in a THREE.Clock to synchronize time-based effects across different
// systems. Alternatively, you can set the ForgeRenderer properties time and
// deltaTime directly. (default: new THREE.Clock)
clock?: THREE.Clock;
// Controls whether to check and automatically update Gsplat collection after
// each frame render. (default: true)
autoUpdate?: boolean;
// Controls whether to update the Gsplats before or after rendering. For WebXR
// this must be false in order to complete rendering as soon as possible.
// (default: false)
preUpdate?: boolean;
// Distance threshold for ForgeRenderer movement triggering a Gsplat update at
// the new origin. (default: 1.0)
originDistance?: number;
// Maximum standard deviations from the center to render Gaussians. Values
// Math.sqrt(5)..Math.sqrt(8) produce good results and can be tweaked for
// performance. (default: Math.sqrt(8))
maxStdDev?: number;
// Enable 2D Gaussian splatting rendering ability. When this mode is enabled,
// any scale x/y/z component that is exactly 0 (minimum quantized value) results
// in the other two non-0 axis being interpreted as an oriented 2D Gaussian Splat,
// rather instead of the usual projected 3DGS Z-slice. When reading PLY files,
// scale values less than e^-20 will be interpreted as 0. (default: true)
enable2DGS?: boolean;
// Scalar value to add to 2D splat covariance diagonal, effectively blurring +
// enlarging splats. In scenes trained without the Gsplat anti-aliasing tweak
// this value was typically 0.3, but with anti-aliasing it is 0.0 (default: 0.0)
preBlurAmount?: number;
// Scalar value to add to 2D splat covarianve diagonal, with opacity adjustment
// to correctly account for "blurring" when anti-aliasing. Typically 0.3
// (equivalent to approx 0.5 pixel radius) in scenes trained with anti-aliasing.
blurAmount?: number;
// Modulate Gaussian kernel falloff. 0 means "no falloff, flat shading",
// while 1 is the normal Gaussian kernel. (default: 1.0)
falloff?: number;
// X/Y clipping boundary factor for Gsplat centers against view frustum.
// 1.0 clips any centers that are exactly out of bounds, while 1.4 clips
// centers that are 40% beyond the bounds. (default: 1.4)
clipXY?: number;
// Configures the ForgeViewpointOptions for the default ForgeViewpoint
// associated with this ForgeRenderer. Notable option: sortRadial (sort by
// radial distance or Z-depth)
view?: ForgeViewpointOptions;
};
export class ForgeRenderer extends THREE.Mesh {
renderer: THREE.WebGLRenderer;
material: THREE.ShaderMaterial;
uniforms: ReturnType<typeof ForgeRenderer.makeUniforms>;
autoUpdate: boolean;
preUpdate: boolean;
originDistance: number;
maxStdDev: number;
enable2DGS: boolean;
preBlurAmount: number;
blurAmount: number;
falloff: number;
clipXY: number;
time?: number;
deltaTime?: number;
clock: THREE.Clock;
// Latest Gsplat collection being displayed
active: SplatAccumulator;
// Free list of accumulators for reuse
private freeAccumulators: SplatAccumulator[];
// Total number of accumulators currently allocated
private accumulatorCount: number;
// Default ForgeViewpoint used for rendering to the canvas
defaultView: ForgeViewpoint;
// List of ForgeViewpoints with autoUpdate enabled
autoViewpoints: ForgeViewpoint[] = [];
// Dynos used to transform Gsplats to the accumulator coordinate system
private rotateToAccumulator = new DynoVec4({ value: new THREE.Quaternion() });
private translateToAccumulator = new DynoVec3({ value: new THREE.Vector3() });
private modifier: SplatModifier;
// Last rendered frame number so we know when we're rendering a new frame
private lastFrame = -1;
// Last update timestamp to compute deltaTime
private lastUpdateTime: number | null = null;
// List of cameras used for the current viewpoint (for WebXR)
private defaultCameras: THREE.Matrix4[] = [];
// Should be set to the defaultView, but can be temporarily changed to another
// viewpoint using prepareViewpoint() for rendering from a different viewpoint.
viewpoint: ForgeViewpoint;
// Holds data needed to perform a scheduled Gsplat update.
private pendingUpdate: {
scene: THREE.Scene;
originToWorld: THREE.Matrix4;
} | null = null;
// Internal ForgeViewpoint used for environment map rendering.
private envViewpoint: ForgeViewpoint | null = null;
// Data and buffers used for environment map rendering
private static cubeRender: {
target: THREE.WebGLCubeRenderTarget;
camera: THREE.CubeCamera;
near: number;
far: number;
} | null = null;
private static pmrem: THREE.PMREMGenerator | null = null;
constructor(options: ForgeRendererOptions) {
const uniforms = ForgeRenderer.makeUniforms();
const shaders = getShaders();
const material = new THREE.ShaderMaterial({
glslVersion: THREE.GLSL3,
vertexShader: shaders.splatVertex,
fragmentShader: shaders.splatFragment,
uniforms,
transparent: true,
blending: THREE.NormalBlending,
depthTest: true,
depthWrite: false,
side: THREE.DoubleSide,
});
super(EMPTY_GEOMETRY, material);
// Disable frustum culling because we want to always draw them all
// and cull Gsplats individually in the shader
this.frustumCulled = false;
this.renderer = options.renderer;
this.material = material;
this.uniforms = uniforms;
// Create a Gsplat modifier that takes the output of any SplatGenerator
// and transforms them into the accumulator's coordinate system
const modifier = dynoBlock(
{ gsplat: Gsplat },
{ gsplat: Gsplat },
({ gsplat }) => {
if (!gsplat) {
throw new Error("gsplat not defined");
}
gsplat = transformGsplat(gsplat, {
rotate: this.rotateToAccumulator,
translate: this.translateToAccumulator,
});
return { gsplat };
},
);
this.modifier = new SplatModifier(modifier);
this.autoUpdate = options.autoUpdate ?? true;
this.preUpdate = options.preUpdate ?? false;
this.originDistance = options.originDistance ?? 1;
this.maxStdDev = options.maxStdDev ?? Math.sqrt(8.0);
this.enable2DGS = options.enable2DGS ?? true;
this.preBlurAmount = options.preBlurAmount ?? 0.0;
this.blurAmount = options.blurAmount ?? 0.3;
this.falloff = options.falloff ?? 1.0;
this.clipXY = options.clipXY ?? 1.4;
this.active = new SplatAccumulator();
this.accumulatorCount = 1;
this.freeAccumulators = [];
// Start with the minimum of 2 total accumulators
for (let count = 0; count < 1; ++count) {
this.freeAccumulators.push(new SplatAccumulator());
this.accumulatorCount += 1;
}
// Create a default ForgeViewpoint that is used when we call render()
// on the scene and has the sorted Gsplat collection from that viewpoint.
this.defaultView = new ForgeViewpoint({
...options.view,
autoUpdate: true,
forge: this,
});
this.viewpoint = this.defaultView;
this.prepareViewpoint(this.viewpoint);
this.clock = options.clock ? cloneClock(options.clock) : new THREE.Clock();
forgeRendererInstance = this;
}
static makeUniforms() {
// Create uniforms used for Gsplat vertex and fragment shaders
const uniforms = {
// Size of render viewport in pixels
renderSize: { value: new THREE.Vector2() },
// Total number of Gsplats in packedSplats to render
numSplats: { value: 0 },
// SplatAccumulator to view transformation quaternion
renderToViewQuat: { value: new THREE.Quaternion() },
// SplatAccumulator to view transformation translation
renderToViewPos: { value: new THREE.Vector3() },
// Maximum distance (in stddevs) from Gsplat center to render
maxStdDev: { value: 1.0 },
// Enable interpreting 0-thickness Gsplats as 2DGS
enable2DGS: { value: false },
// Add to projected 2D splat covariance diagonal (thickens and brightens)
preBlurAmount: { value: 0.0 },
// Add to 2D splat covariance diagonal and adjust opacity (anti-aliasing)
blurAmount: { value: 0.3 },
// Modulate Gaussian kernal falloff. 0 means "no falloff, flat shading",
// 1 is normal e^-x^2 falloff.
falloff: { value: 1.0 },
// Clip Gsplats that are clipXY times beyond the +-1 frustum bounds
clipXY: { value: 1.4 },
// Gsplat collection to render
packedSplats: { type: "t", value: PackedSplats.getEmpty() },
// Time in seconds for time-based effects
time: { value: 0 },
// Delta time in seconds since last frame
deltaTime: { value: 0 },
// Whether to encode Gsplat with linear RGB (for environment mapping)
encodeLinear: { value: false },
// Debug flag that alternates each frame
debugFlag: { value: false },
};
return uniforms;
}
private canAllocAccumulator(): boolean {
// Returns true if can allocate an accumulator immediately
return (
this.freeAccumulators.length > 0 ||
this.accumulatorCount < MAX_ACCUMULATORS
);
}
private maybeAllocAccumulator(): SplatAccumulator | null {
// Allocate an accumulator immediately if possible, else return null
let accumulator = this.freeAccumulators.pop();
if (accumulator === undefined) {
if (this.accumulatorCount >= MAX_ACCUMULATORS) {
return null;
}
accumulator = new SplatAccumulator();
this.accumulatorCount += 1;
}
accumulator.refCount = 1;
return accumulator;
}
releaseAccumulator(accumulator: SplatAccumulator) {
// Decrement reference count and recycle if no longer in use
accumulator.refCount -= 1;
if (accumulator.refCount === 0) {
this.freeAccumulators.push(accumulator);
}
}
newViewpoint(options: ForgeViewpointOptions) {
// Create a new ForgeViewpoint for this ForgeRenderer.
// Note that every ForgeRenderer has an initial forge.defaultView: ForgeViewpoint
// from construction, which is used for the default canvas render loop.
// Calling this method allows you to create additional viewpoints, which can be
// updated automatically each frame (performing Gsplat sorting every time there
// is an update), or updated on-demand for controlled rendering for video render
// or similar applications.
return new ForgeViewpoint({ ...options, forge: this });
}
onBeforeRender(
renderer: THREE.WebGLRenderer,
scene: THREE.Scene,
camera: THREE.Camera,
) {
// Called by Three.js before rendering this ForgeRenderer.
// At this point we can't modify the geometry or material, all these must
// be set in the scene already before this is called. Update the uniforms
// to render the Gsplats from the current active viewpoint.
const time = this.time ?? this.clock.getElapsedTime();
const deltaTime = time - (this.viewpoint.lastTime ?? time);
this.viewpoint.lastTime = time;
const frame = renderer.info.render.frame;
const isNewFrame = frame !== this.lastFrame;
this.lastFrame = frame;
const viewpoint = this.viewpoint;
if (viewpoint === this.defaultView) {
// When rendering is triggered on the default viewpoint,
// perform automatic updates.
if (isNewFrame) {
if (!renderer.xr.isPresenting) {
// Non-WebXR mode, just a single camera
this.defaultView.viewToWorld = camera.matrixWorld.clone();
this.defaultCameras = [this.defaultView.viewToWorld];
} else {
// In WebXR mode we are called multiple times, once for each eye,
// so use their average to compute the sort center.
const cameras = renderer.xr.getCamera().cameras;
this.defaultCameras = [
cameras[0].matrixWorld,
cameras[1].matrixWorld,
];
this.defaultView.viewToWorld =
averageOriginToWorlds(this.defaultCameras) ?? new THREE.Matrix4();
}
}
if (this.autoUpdate) {
this.update({ scene, viewToWorld: this.defaultView.viewToWorld });
}
}
// Update uniforms for rendering
if (isNewFrame) {
// Keep these uniforms the same for both eyes if in WebXR
this.uniforms.time.value = time;
this.uniforms.deltaTime.value = deltaTime;
// Alternating debug flag that can aid in visual debugging
this.uniforms.debugFlag.value = (performance.now() / 1000.0) % 2.0 < 1.0;
}
if (viewpoint.target) {
// Rendering to a texture target, so its dimensions
this.uniforms.renderSize.value.set(
viewpoint.target.width,
viewpoint.target.height,
);
} else {
// Rendering to the canvas or WebXR
const renderSize = renderer.getSize(this.uniforms.renderSize.value);
if (renderSize.x === 1 && renderSize.y === 1) {
// WebXR mode on Apple Vision Pro returns 1x1 when presenting.
// Use a different means to figure out the render size.
const baseLayer = renderer.xr.getSession()?.renderState.baseLayer;
if (baseLayer) {
renderSize.x = baseLayer.framebufferWidth;
renderSize.y = baseLayer.framebufferHeight;
}
}
}
// Update uniforms from instance properties
this.uniforms.encodeLinear.value = viewpoint.encodeLinear;
this.uniforms.maxStdDev.value = this.maxStdDev;
this.uniforms.enable2DGS.value = this.enable2DGS;
this.uniforms.preBlurAmount.value = this.preBlurAmount;
this.uniforms.blurAmount.value = this.blurAmount;
this.uniforms.falloff.value = this.falloff;
this.uniforms.clipXY.value = this.clipXY;
// Calculate the transform from the accumulator to the current camera
const accumToWorld =
viewpoint.display?.accumulator.toWorld ?? new THREE.Matrix4();
const worldToCamera = camera.matrixWorld.clone().invert();
const originToCamera = accumToWorld.clone().premultiply(worldToCamera);
originToCamera.decompose(
this.uniforms.renderToViewPos.value,
this.uniforms.renderToViewQuat.value,
new THREE.Vector3(),
);
}
// Update the uniforms for the given viewpoint.
// Note that the client expects to be able to call render() at any point
// to update the canvas, so we must switch the viewpoint back to
// defaultView when we're finished.
prepareViewpoint(viewpoint?: ForgeViewpoint) {
this.viewpoint = viewpoint ?? this.viewpoint;
if (this.viewpoint.display) {
const { accumulator, geometry } = this.viewpoint.display;
this.uniforms.numSplats.value = accumulator.splats.numSplats;
this.uniforms.packedSplats.value = accumulator.splats.getTexture();
this.geometry = geometry;
} else {
// No Gsplats to display for this viewpoint yet
this.uniforms.numSplats.value = 0;
this.uniforms.packedSplats.value = PackedSplats.getEmpty();
this.geometry = EMPTY_GEOMETRY;
}
}
// If forge.autoUpdate is false then you must manually call
// forge.update({ scene }) to have the scene Gsplats be re-generated.
update({
scene,
viewToWorld,
}: { scene: THREE.Scene; viewToWorld?: THREE.Matrix4 }) {
// Compute the transform for the ForgeRenderer to use as origin
// for Gsplat generation and accumulation.
const originToWorld = this.matrixWorld.clone();
// Either do the update now, or in the next "tick" depending on preUpdate
if (this.preUpdate) {
this.updateInternal({ scene, originToWorld, viewToWorld });
} else {
// Pass the update parameters to be performed on the next tick
this.pendingUpdate = {
scene,
originToWorld,
};
setTimeout(() => {
if (this.pendingUpdate) {
const { scene, originToWorld } = this.pendingUpdate;
this.pendingUpdate = null;
this.updateInternal({ scene, originToWorld, viewToWorld });
}
}, 1);
}
}
updateInternal({
scene,
originToWorld,
viewToWorld,
}: {
scene: THREE.Scene;
originToWorld?: THREE.Matrix4;
viewToWorld?: THREE.Matrix4;
}): boolean {
if (!this.canAllocAccumulator()) {
// We don't have any available accumulators because of sorting
// back pressure, so don't update this time but try again next time.
// Signal update not attempted.
return false;
}
// Figure out the frame of the ForgeRenderer and current view
if (!originToWorld) {
originToWorld = this.active.toWorld;
}
viewToWorld = viewToWorld ?? originToWorld.clone();
const time = this.time ?? this.clock.getElapsedTime();
const deltaTime = time - (this.lastUpdateTime ?? time);
this.lastUpdateTime = time;
// Create a lookup from last active SplatGenerator to Gsplat mapping record
const activeMapping = this.active.mapping.reduce((map, record) => {
map.set(record.node, record);
return map;
}, new Map<SplatGenerator, GeneratorMapping>());
// Traverse visible scene to find all SplatGenerators and global SplatEdits
const { generators, globalEdits } = this.compileScene(scene);
// Let all SplatGenerators run their frameUpdate() method
for (const object of generators) {
object.frameUpdate?.({
object,
time,
deltaTime,
viewToWorld,
globalEdits,
});
}
// Make sure we have new version numbers for any objects with either
// generator or numSplats that have changed since the last frame.
for (const object of generators) {
const current = activeMapping.get(object);
const numSplats = object.generator ? object.numSplats : 0;
if (
object.generator !== current?.generator ||
numSplats !== current?.count
) {
object.updateVersion();
}
}
// Check if the origin is within the maximum allowed distance before
// we trigger an update.
const originUpdate = !withinCoorientDist({
matrix1: originToWorld,
matrix2: this.active.toWorld,
maxDistance: this.originDistance,
});
// Check if we need any update at all
const needsUpdate =
originUpdate ||
generators.length !== activeMapping.size ||
generators.some((g) => g.version !== activeMapping.get(g)?.version);
let accumulator: SplatAccumulator | null = null;
if (needsUpdate) {
// Need to update, so allocate an accumulator
accumulator = this.maybeAllocAccumulator();
if (!accumulator) {
// This should never happen since we checked canAllocAccumulator() above
throw new Error("Unreachable");
}
// Compute whether our view frame has changed enough to warrant
// doing a Gsplat sort. Check both distance epsilon and
// minimum co-orientation (dot product of quaternions)
const originChanged = !withinCoorientDist({
matrix1: originToWorld,
matrix2: this.active.toWorld,
maxDistance: 0.00001,
minCoorient: 0.99999,
});
// Compute an ordering of the generators with the rough goal
// of keeping unchanging generators near the front to minimize
// the number of Gsplats that need to be regenerated.
const sorted = generators
.map((g, gIndex): [number, number, SplatGenerator] => {
const lastGen = activeMapping.get(g);
// If no previous generator, sort by absolute version, which will
// tend to push frequently updated generators toward the end
return !lastGen
? [Number.POSITIVE_INFINITY, g.version, g]
: // Sort by version deltas then by previous ordering in the mapping,
// attempting to keep unchanging generators near the front
// to improve our chances of avoiding a re-generation.
[g.version - lastGen.version, lastGen.base, g];
})
.sort((a, b) => {
// Sort by first then second element of the tuple
if (a[0] !== b[0]) {
return a[0] - b[0];
}
return a[1] - b[1];
});
const genOrder = sorted.map(([_version, _seq, g]) => g);
// Compute sequential layout of generated splats
const splatCounts = genOrder.map((g) => g.numSplats);
const { maxSplats, mapping } =
accumulator.splats.generateMapping(splatCounts);
const newGenerators = genOrder.map((node, gIndex) => {
const { base, count } = mapping[gIndex];
return {
node,
generator: node.generator,
version: node.version,
base,
count,
};
});
// Compute worldToAccumulator origin transform (no scale)
originToWorld
.clone()
.invert()
.decompose(
this.translateToAccumulator.value,
this.rotateToAccumulator.value,
new THREE.Vector3(),
);
// Generate the Gsplats according to the mapping that need updating
accumulator.ensureGenerate(maxSplats);
const generated = accumulator.generateSplats({
renderer: this.renderer,
modifier: this.modifier,
generators: newGenerators,
forceUpdate: originChanged,
originToWorld,
});
// Update splat version number
accumulator.splatsVersion = this.active.splatsVersion + 1;
// Increment the mapping version if the mapping isn't identical to before
const hasCorrespondence = accumulator.hasCorrespondence(this.active);
accumulator.mappingVersion =
this.active.mappingVersion + (hasCorrespondence ? 0 : 1);
// Release the old accumulator and make the new one active
this.releaseAccumulator(this.active);
this.active = accumulator;
this.prepareViewpoint();
}
// Let the system breath before potentially triggering sorts
setTimeout(() => {
// Notify all auto-updating viewpoints that we updated the Gsplats
for (const view of this.autoViewpoints) {
view.autoPoll({ accumulator: accumulator ?? undefined });
}
}, 1);
// Signal update was performed
return true;
}
private compileScene(scene: THREE.Scene): {
generators: SplatGenerator[];
globalEdits: SplatEdit[];
} {
// Take a snapshot of the SplatGenerators and SplatEdits in the scene
// to be used to run an update.
const generators: SplatGenerator[] = [];
scene.traverse((node) => {
if (node instanceof SplatGenerator) {
generators.push(node);
}
});
const globalEdits = new Set<SplatEdit>();
scene.traverseVisible((node) => {
if (node instanceof SplatEdit) {
let ancestor = node.parent;
while (ancestor != null && !(ancestor instanceof SplatMesh)) {
ancestor = ancestor.parent;
}
if (ancestor == null) {
// Not part of a SplatMesh so it's a global edit
globalEdits.add(node);
}
}
});
return { generators, globalEdits: Array.from(globalEdits) };
}
// Renders out the scene to an environment map that can be used for
// Image-based lighting or similar applications. First optionally updates Gsplats,
// sorts them with respect to the provided worldCenter, renders 6 cube faces,
// then pre-filters them using THREE.PMREMGenerator and returns a THREE.Texture
// that can assigned directly to a THREE.MeshStandardMaterial.envMap property.
async renderEnvMap({
renderer,
scene,
worldCenter,
size = 256,
near = 0.1,
far = 1000,
hideObjects = [],
update = false,
}: {
renderer?: THREE.WebGLRenderer;
scene: THREE.Scene;
worldCenter: THREE.Vector3;
size?: number;
near?: number;
far?: number;
hideObjects?: THREE.Object3D[];
update?: boolean;
}): Promise<THREE.Texture> {
if (!this.envViewpoint) {
this.envViewpoint = this.newViewpoint({ sort360: true });
}
if (
!ForgeRenderer.cubeRender ||
ForgeRenderer.cubeRender.target.width !== size ||
ForgeRenderer.cubeRender.near !== near ||
ForgeRenderer.cubeRender.far !== far
) {
if (ForgeRenderer.cubeRender) {
ForgeRenderer.cubeRender.target.dispose();
}
const target = new THREE.WebGLCubeRenderTarget(size, {
format: THREE.RGBAFormat,
generateMipmaps: true,
minFilter: THREE.LinearMipMapLinearFilter,
});
const camera = new THREE.CubeCamera(near, far, target);
ForgeRenderer.cubeRender = { target, camera, near, far };
}
if (!ForgeRenderer.pmrem) {
ForgeRenderer.pmrem = new THREE.PMREMGenerator(renderer ?? this.renderer);
}
// Prepare the viewpoint, sorting Gsplats for this view origin.
const viewToWorld = new THREE.Matrix4().setPosition(worldCenter);
await this.envViewpoint?.prepare({ scene, viewToWorld, update });
const { target, camera } = ForgeRenderer.cubeRender;
camera.position.copy(worldCenter);
// Save the visibility state of objects we want to hide before render
const objectVisibility = new Map<THREE.Object3D, boolean>();
for (const object of hideObjects) {
objectVisibility.set(object, object.visible);
object.visible = false;
}
// Update the CubeCamera, which performs 6 cube face renders
this.prepareViewpoint(this.envViewpoint);
camera.update(renderer ?? this.renderer, scene);
// Restore viewpoint to default and object visibility
this.prepareViewpoint(this.defaultView);
for (const [object, visible] of objectVisibility.entries()) {
object.visible = visible;
}
// Pre-filter the cube map using THREE.PMREMGenerator
return ForgeRenderer.pmrem?.fromCubemap(target.texture).texture;
}
// Utility function to recursively set the envMap property for any
// THREE.MeshStandardMaterial within the subtree of root.
recurseSetEnvMap(root: THREE.Object3D, envMap: THREE.Texture) {
root.traverse((node) => {
if (node instanceof THREE.Mesh) {
if (Array.isArray(node.material)) {
for (const material of node.material) {
if (material instanceof THREE.MeshStandardMaterial) {
material.envMap = envMap;
}
}
} else {
if (node.material instanceof THREE.MeshStandardMaterial) {
node.material.envMap = envMap;
}
}
}
});
}
// Utility function that helps extract the Gsplat RGBA values from a
// SplatGenerator, including the result of any real-time RGBA SDF edits applied
// to a SplatMesh. This effectively "bakes" any computed RGBA values, which can
// now be used as a pipeline input via SplatMesh.splatRgba to inject these
// baked values into the Gsplat data.
getRgba({
generator,
rgba,
}: { generator: SplatGenerator; rgba?: RgbaArray }): RgbaArray {
const mapping = this.active.mapping.find(({ node }) => node === generator);
if (!mapping) {
throw new Error("Generator not found");
}
rgba = rgba ?? new RgbaArray();
rgba.fromPackedSplats({
packedSplats: this.active.splats,
base: mapping.base,
count: mapping.count,
renderer: this.renderer,
});
return rgba;
}
// Utility function that builds on getRgba({ generator }) and additionally
// reads back the RGBA values to the CPU in a Uint8Array with packed RGBA
// in that byte order.
async readRgba({
generator,
rgba,
}: { generator: SplatGenerator; rgba?: RgbaArray }): Promise<Uint8Array> {
rgba = this.getRgba({ generator, rgba });
return rgba.read();
}
}
const EMPTY_GEOMETRY = new SplatGeometry(new Uint32Array(1), 0);
const reorderSplats = dynoBlock(
{ packedSplats: TPackedSplats, index: "int" },
{ gsplat: Gsplat },
({ packedSplats, index }) => {
if (!packedSplats || !index) {
throw new Error("Invalid input");
}
const gsplat = readPackedSplat(packedSplats, index);
return { gsplat };
},
);
function averageOriginToWorlds(
originToWorlds: THREE.Matrix4[],
): THREE.Matrix4 | null {
if (originToWorlds.length === 0) {
return null;
}
const position = new THREE.Vector3();
const quaternion = new THREE.Quaternion();
const scale = new THREE.Vector3();
const positions: THREE.Vector3[] = [];
const quaternions: THREE.Quaternion[] = [];
for (const matrix of originToWorlds) {
matrix.decompose(position, quaternion, scale);
positions.push(position);
quaternions.push(quaternion);
}
return new THREE.Matrix4().compose(
averagePositions(positions),
averageQuaternions(quaternions),
new THREE.Vector3(1, 1, 1),
);
}
+758
View File
@@ -0,0 +1,758 @@
import * as THREE from "three";
import type { ForgeRenderer } from "./ForgeRenderer";
import { DynoPackedSplats } from "./PackedSplats";
import { Readback } from "./Readback";
import type { SplatAccumulator } from "./SplatAccumulator";
import { SplatGeometry } from "./SplatGeometry";
import {
type DynoBlock,
DynoBool,
DynoFloat,
type DynoVal,
DynoVec3,
Gsplat,
add,
combine,
defineGsplat,
dyno,
dynoBlock,
dynoConst,
mul,
packHalf2x16,
readPackedSplat,
uintToRgba8,
unindent,
unindentLines,
} from "./dyno";
import { withWorker } from "./splatWorker";
import { FreeList, withinCoorientDist } from "./utils";
export type ForgeViewpointOptions = {
// Controls whether to auto-update its sort order whenever the ForgeRenderer
// updates the Gsplats. If you expect to render/display from this viewpoint
// most frames, set this to true. (default: false)
autoUpdate?: boolean;
// Set a THREE.Camera for this viewpoint to follow. (default: undefined)
camera?: THREE.Camera;
// Set an explicit view-to-world transformation matrix for this viewpoint (equivalent
// to camera.matrixWorld), overrides any camera setting. (default: undefined)
viewToWorld?: THREE.Matrix4;
// Configure viewpoint with an off-screen render target. (default: undefined)
target?: {
// Width of the render target in pixels.
width: number;
// Height of the render target in pixels.
height: number;
// If you want to be able to render a scene that depends on this target's
// output (for example, a recursive viewport), set this to true to enable
// double buffering. (default: false)
doubleBuffer?: boolean;
// Super-sampling factor for the render target. Values 1-4 are supported.
// Note that re-sampling back down to .width x .height is done on the CPU
// with simple averaging only when calling readTarget(). (default: 1)
superXY?: number;
};
// Callback function that is called when the render target texture is updated.
// Receives the texture as a parameter. Use this to update a viewport with
// the latest viewpoint render each frame. (default: undefined)
onTextureUpdated?: (texture: THREE.Texture) => void;
// Whether to sort splats radially (geometric distance) from the viewpoint (true)
// or by Z-depth (false). Most scenes are trained with the Z-depth sort metric
// and will render more accurately at certain viewpoints. However, radial sorting
// is more stable under viewpoint rotations. (default: true)
sortRadial?: boolean;
// Distance threshold for re-sorting splats. If the viewpoint moves more than
// this distance, splats will be re-sorted. (default: 0.01 units)
sortDistance?: number;
// View direction dot product threshold for re-sorting splats. For
// sortRadial: true we use 0.99 while sortRadial: false uses 0.999 because it is
// more sensitive to view direction. (default: 0.99 if sortRadial else 0.999)
sortCoorient?: boolean;
// Constant added to Z-depth to bias values into the positive range for
// sortRadial: false, but also used for culling Gsplats "well behind"
// the viewpoint origin (default: 1.0)
depthBias?: number;
// Set this to true if rendering a 360 to disable "behind the viewpoint"
// culling during sorting. This is set automatically when rendering 360 envMaps
// using the ForgeRenderer.renderEnvMap() utility function. (default: false)
sort360?: boolean;
};
// A ForgeViewpoint is created from and tied to a ForgeRenderer, and represents
// an independent viewpoint of all the scene Gsplats and their sort order. Making
// these viewpoints explicit allows us to have multiple, simultaneous viewpoint
// renders, for example for camera preview panes or overhead map views.
//
// When creating a ForgeRenderer it automatically creates a default viewpoint
// .defaultView that is used in the normal render loop when drawing to the canvas,
// and is automatically updated whenever the camera moves. Additional viewpoints
// can be created and configured separately.
export class ForgeViewpoint {
forge: ForgeRenderer;
autoUpdate: boolean;
camera?: THREE.Camera;
viewToWorld: THREE.Matrix4;
lastTime: number | null = null;
target?: THREE.WebGLRenderTarget;
private back?: THREE.WebGLRenderTarget;
onTextureUpdated?: (texture: THREE.Texture) => void;
encodeLinear = false;
superXY = 1;
private superPixels?: Uint8Array;
private pixels?: Uint8Array;
sortRadial: boolean;
sortDistance?: number;
sortCoorient?: boolean;
depthBias?: number;
sort360?: boolean;
display: {
accumulator: SplatAccumulator;
viewToWorld: THREE.Matrix4;
geometry: SplatGeometry;
} | null = null;
private sorting: { viewToWorld: THREE.Matrix4 } | null = null;
private pending: {
accumulator?: SplatAccumulator;
viewToWorld: THREE.Matrix4;
displayed: boolean;
} | null = null;
private sortingCheck = false;
private readback: Uint16Array = new Uint16Array(0);
private orderingFreelist: FreeList<Uint32Array, number>;
constructor(options: ForgeViewpointOptions & { forge: ForgeRenderer }) {
this.forge = options.forge;
this.camera = options.camera;
this.viewToWorld = options.viewToWorld ?? new THREE.Matrix4();
if (options.target) {
const { width, height, doubleBuffer } = options.target;
const superXY = Math.max(1, Math.min(4, options.target.superXY ?? 1));
this.superXY = superXY;
if (width * superXY > 8192 || height * superXY > 8192) {
throw new Error("Target size too large");
}
this.target = new THREE.WebGLRenderTarget(
width * superXY,
height * superXY,
{
format: THREE.RGBAFormat,
type: THREE.UnsignedByteType,
colorSpace: THREE.SRGBColorSpace,
},
);
if (doubleBuffer) {
this.back = new THREE.WebGLRenderTarget(
width * superXY,
height * superXY,
{
format: THREE.RGBAFormat,
type: THREE.UnsignedByteType,
colorSpace: THREE.SRGBColorSpace,
},
);
}
this.encodeLinear = true;
}
this.onTextureUpdated = options.onTextureUpdated;
this.sortRadial = options.sortRadial ?? true;
this.sortDistance = options.sortDistance;
this.sortCoorient = options.sortCoorient;
this.depthBias = options.depthBias;
this.sort360 = options.sort360;
this.orderingFreelist = new FreeList({
allocate: (maxSplats) => new Uint32Array(maxSplats),
valid: (ordering, maxSplats) => ordering.length === maxSplats,
});
this.autoUpdate = false;
this.setAutoUpdate(options.autoUpdate ?? false);
}
// Call this when you are done with the ForgeViewpoint and want to
// free up its resources (GPU targets, pixel buffers, etc.)
dispose() {
this.setAutoUpdate(false);
if (this.target) {
this.target.dispose();
this.target = undefined;
}
if (this.back) {
this.back.dispose();
this.back = undefined;
}
if (this.display) {
this.forge.releaseAccumulator(this.display.accumulator);
this.display.geometry.dispose();
this.display = null;
}
if (this.pending?.accumulator) {
this.forge.releaseAccumulator(this.pending.accumulator);
this.pending = null;
}
}
// Use this function to change whether this viewpoint will auto-update
// its sort order whenever the attached ForgeRenderer updates the Gsplats.
// Turn this on or off depending on whether you expect to do renders from
// this viewpoint most frames.
setAutoUpdate(autoUpdate: boolean) {
if (!this.autoUpdate && autoUpdate) {
this.forge.autoViewpoints.push(this);
} else if (this.autoUpdate && !autoUpdate) {
this.forge.autoViewpoints = this.forge.autoViewpoints.filter(
(v) => v !== this,
);
}
this.autoUpdate = autoUpdate;
}
// See below async prepareRenderPixels() for explanation of parameters.
// Awaiting this method updates the Gsplats in the scene and performs a sort of the
// Gsplats from this viewpoint, preparing it for a subsequent this.renderTarget()
// call in the same tick.
async prepare({
scene,
camera,
viewToWorld,
update,
forceOrigin,
}: {
scene: THREE.Scene;
camera?: THREE.Camera;
viewToWorld?: THREE.Matrix4;
update?: boolean;
forceOrigin?: boolean;
}) {
if (viewToWorld) {
this.viewToWorld = viewToWorld;
} else {
this.camera = camera ?? this.camera;
if (this.camera) {
this.camera.updateMatrixWorld();
this.viewToWorld = this.camera.matrixWorld.clone();
}
}
while (update ?? true) {
// Force an update, possibly with origin centered at this camera
// to yield the best quality output.
const originToWorld = forceOrigin ? this.viewToWorld : undefined;
const updated = this.forge.updateInternal({ scene, originToWorld });
if (updated) {
break;
}
// A bit of a hack, but try again. We shouldn't be starved for long.
await new Promise((resolve) => setTimeout(resolve, 10));
}
const accumulator = this.forge.active;
if (accumulator !== this.display?.accumulator) {
this.forge.active.refCount += 1;
}
await this.sortUpdate({ accumulator, viewToWorld: this.viewToWorld });
}
// Render out the viewpoint to the view target RGBA buffer.
// Swaps buffers if doubleBuffer: true was set.
// Calls onTextureUpdated(texture) with the resulting texture.
renderTarget({
scene,
camera,
}: { scene: THREE.Scene; camera?: THREE.Camera }) {
const target = this.back ?? this.target;
if (!target) {
throw new Error("Must initialize ForgeViewpoint with target");
}
camera = camera ?? this.camera;
if (!camera) {
throw new Error("Must provide camera");
}
if (camera instanceof THREE.PerspectiveCamera) {
const newCam = new THREE.PerspectiveCamera().copy(camera, false);
newCam.aspect = target.width / target.height;
newCam.updateProjectionMatrix();
camera = newCam;
}
this.viewToWorld = camera.matrixWorld.clone();
try {
this.forge.renderer.setRenderTarget(target);
this.forge.prepareViewpoint(this);
this.forge.renderer.render(scene, camera);
} finally {
this.forge.prepareViewpoint(this.forge.defaultView);
this.forge.renderer.setRenderTarget(null);
}
if (target !== this.target) {
// Swap back buffer and target
[this.target, this.back] = [this.back, this.target];
}
this.onTextureUpdated?.(target.texture);
}
// Read back the previously rendered target image as a Uint8Array of packed
// RGBA values (in that order). If superXY was set greater than 1 then
// downsampling is performed in the target pixel array with simple averaging
// to derive the returned pixel values. Subsequent calls to this.readTarget()
// will reuse the same buffers to minimize memory allocations.
async readTarget(): Promise<Uint8Array> {
if (!this.target) {
throw new Error("Must initialize ForgeViewpoint with target");
}
const { width, height } = this.target;
const byteSize = width * height * 4;
if (!this.superPixels || this.superPixels.length < byteSize) {
this.superPixels = new Uint8Array(byteSize);
}
await this.forge.renderer.readRenderTargetPixelsAsync(
this.target,
0,
0,
width,
height,
this.superPixels,
);
const { superXY } = this;
if (superXY === 1) {
return this.superPixels;
}
const subWidth = width / superXY;
const subHeight = height / superXY;
const subSize = subWidth * subHeight * 4;
if (!this.pixels || this.pixels.length < subSize) {
this.pixels = new Uint8Array(subSize);
}
const { superPixels, pixels } = this;
const super2 = superXY * superXY;
for (let y = 0; y < subHeight; y++) {
const row = y * subWidth;
for (let x = 0; x < subWidth; x++) {
const superCol = x * superXY;
let r = 0;
let g = 0;
let b = 0;
let a = 0;
for (let sy = 0; sy < superXY; sy++) {
const superRow = (y * superXY + sy) * this.target.width;
for (let sx = 0; sx < superXY; sx++) {
const superIndex = (superRow + superCol + sx) * 4;
r += superPixels[superIndex];
g += superPixels[superIndex + 1];
b += superPixels[superIndex + 2];
a += superPixels[superIndex + 3];
}
}
const pixelIndex = (row + x) * 4;
pixels[pixelIndex] = r / super2;
pixels[pixelIndex + 1] = g / super2;
pixels[pixelIndex + 2] = b / super2;
pixels[pixelIndex + 3] = a / super2;
}
}
return pixels;
}
// Render out a viewpoint as a Uint8Array of RGBA values for the provided scene
// and any camera/viewToWorld viewpoint overrides. By default update is true,
// which triggers its ForgeRenderer to check and potentially update the Gsplats.
// Setting update to false disables this and sorts the Gsplats as they are.
// Setting forceOrigin (default: false) to true forces the view update to
// recalculate the splats with this view origin, potentially altering any
// view-dependent effects. If you expect view-dependent effects to play a role
// in the rendering quality, enable this.
//
// Underneath, prepareRenderPixels() simply calls await this.prepare(...),
// this.renderTarget(...), and finally returns the result this.readTarget(),
// a Promise to a Uint8Array with RGBA values for all the pixels (potentially
// downsampled if the superXY parameter was used). These steps can also be called
// manually, for example if you need to alter the scene before and after
// this.renderTarget(...) to hide UI elements from being rendered.
async prepareRenderPixels({
scene,
camera,
viewToWorld,
update,
forceOrigin,
}: {
scene: THREE.Scene;
camera?: THREE.Camera;
viewToWorld?: THREE.Matrix4;
update?: boolean;
forceOrigin?: boolean;
}) {
await this.prepare({ scene, camera, viewToWorld, update, forceOrigin });
this.renderTarget({ scene, camera });
return this.readTarget();
}
// This is called automatically by ForgeRenderer, there is no need to call it!
// The method cannot be private because then ForgeRenderer would
// not be able to call it.
autoPoll({ accumulator }: { accumulator?: SplatAccumulator }) {
if (this.camera) {
this.camera.updateMatrixWorld();
this.viewToWorld = this.camera.matrixWorld.clone();
}
let needsSort = false;
let displayed = false;
if (!this.display) {
// Need to do first sort
needsSort = true;
} else if (accumulator) {
needsSort = true;
const { mappingVersion } = this.display.accumulator;
if (accumulator.mappingVersion === mappingVersion) {
// Splat mapping has not changed, so reuse the existing sorted
// geometry to show updates faster. We will still fire off
// a re-sort if necessary. First release old accumulator.
this.forge.releaseAccumulator(this.display.accumulator);
this.display.accumulator = accumulator;
displayed = true;
}
}
const latestView = this.sorting?.viewToWorld ?? this.display?.viewToWorld;
if (
latestView &&
!withinCoorientDist({
matrix1: this.viewToWorld,
matrix2: latestView,
// By default update sort each 1 cm
maxDistance: this.sortDistance ?? 0.01,
// By default for radial sort, update for intermittent movement so that
// we bring back splats culled by being behind the camera.
// For depth sort, small rotations can change sort order a lot, so
// update sort for even small rotations.
minCoorient: (this.sortCoorient ?? this.sortRadial) ? 0.99 : 0.999,
})
) {
needsSort = true;
}
if (!needsSort) {
// Stop here, no sort necessary
return;
}
if (accumulator) {
// Hold a reference to the accumulator so it isn't released
accumulator.refCount += 1;
}
if (
accumulator &&
this.pending?.accumulator &&
this.pending.accumulator !== this.display?.accumulator
) {
this.forge.releaseAccumulator(this.pending.accumulator);
}
this.pending = { accumulator, viewToWorld: this.viewToWorld, displayed };
// Don't await this, just trigger the sort if necessary
this.driveSort();
}
private async driveSort() {
while (true) {
if (this.sorting || !this.pending) {
return; // Sort already in process or nothing to sort
}
const { viewToWorld, displayed } = this.pending;
let accumulator = this.pending.accumulator ?? this.display?.accumulator;
if (!accumulator) {
accumulator = this.forge.active;
accumulator.refCount += 1;
}
this.pending = null;
if (!accumulator) {
throw new Error("No accumulator to sort");
}
this.sorting = { viewToWorld };
await this.sortUpdate({ accumulator, viewToWorld, displayed });
this.sorting = null;
// Continue in loop with any queued sort
}
}
private async sortUpdate({
accumulator,
viewToWorld,
displayed = false,
}: {
accumulator?: SplatAccumulator;
viewToWorld: THREE.Matrix4;
displayed?: boolean;
}) {
if (this.sortingCheck) {
throw new Error("Only one sort at a time");
}
this.sortingCheck = true;
accumulator = accumulator ?? this.forge.active;
const { numSplats, maxSplats } = accumulator.splats;
let activeSplats = 0;
let ordering = this.orderingFreelist.alloc(maxSplats);
if (numSplats > 0) {
const {
reader,
doubleSortReader,
dynoSortRadial,
dynoOrigin,
dynoDirection,
dynoDepthBias,
dynoSort360,
dynoSplats,
} = ForgeViewpoint.makeSorter();
const halfMaxSplats = Math.ceil(maxSplats / 2);
this.readback = reader.ensureBuffer(halfMaxSplats, this.readback);
const worldToOrigin = accumulator.toWorld.clone().invert();
const viewToOrigin = viewToWorld.clone().premultiply(worldToOrigin);
dynoSortRadial.value = this.sort360 ? true : this.sortRadial;
dynoOrigin.value.set(0, 0, 0).applyMatrix4(viewToOrigin);
dynoDirection.value
.set(0, 0, -1)
.applyMatrix4(viewToOrigin)
.sub(dynoOrigin.value)
.normalize();
dynoDepthBias.value = this.depthBias ?? 1.0;
dynoSort360.value = this.sort360 ?? false;
dynoSplats.packedSplats = accumulator.splats;
await reader.renderReadback({
renderer: this.forge.renderer,
reader: doubleSortReader,
count: Math.ceil(numSplats / 2),
readback: this.readback,
});
const result = (await withWorker(async (worker) => {
return worker.call("sortDoubleSplats", {
numSplats,
readback: this.readback,
ordering,
});
})) as {
readback: Uint16Array;
ordering: Uint32Array;
activeSplats: number;
};
this.readback = result.readback;
ordering = result.ordering;
activeSplats = result.activeSplats;
}
this.updateDisplay({
accumulator,
viewToWorld,
ordering,
activeSplats,
displayed,
});
this.sortingCheck = false;
}
private updateDisplay({
accumulator,
viewToWorld,
ordering,
activeSplats,
displayed = false,
}: {
accumulator: SplatAccumulator;
viewToWorld: THREE.Matrix4;
ordering: Uint32Array;
activeSplats: number;
displayed?: boolean;
}) {
if (!this.display) {
this.display = {
accumulator,
viewToWorld,
geometry: new SplatGeometry(ordering, activeSplats),
};
} else {
if (!displayed && accumulator !== this.display.accumulator) {
this.forge.releaseAccumulator(this.display.accumulator);
this.display.accumulator = accumulator;
}
this.display.viewToWorld = viewToWorld;
const oldOrdering = this.display.geometry.ordering;
if (oldOrdering.length === ordering.length) {
this.display.geometry.update(ordering, activeSplats);
} else {
this.display.geometry.dispose();
// console.log("*** alloc SplatGeometry", ordering.length);
this.display.geometry = new SplatGeometry(ordering, activeSplats);
}
this.orderingFreelist.free(oldOrdering);
}
if (this.forge.viewpoint === this) {
this.forge.prepareViewpoint(this);
}
}
// If you need an empty THREE.Texture to use to initialize a uniform that is
// updated via onTextureUpdated(texture), this static texture can be handy.
static EMPTY_TEXTURE = new THREE.Texture();
private static dynos: {
dynoSortRadial: DynoBool<string>;
dynoOrigin: DynoVec3<THREE.Vector3, "value">;
dynoDirection: DynoVec3<THREE.Vector3, "value">;
dynoDepthBias: DynoFloat<string>;
dynoSort360: DynoBool<string>;
dynoSplats: DynoPackedSplats;
reader: Readback;
doubleSortReader: DynoBlock<{ index: "int" }, { rgba8: "vec4" }>;
} | null = null;
private static makeSorter() {
if (!ForgeViewpoint.dynos) {
const dynoSortRadial = new DynoBool({ value: true });
const dynoOrigin = new DynoVec3({ value: new THREE.Vector3() });
const dynoDirection = new DynoVec3({ value: new THREE.Vector3() });
const dynoDepthBias = new DynoFloat({ value: 1.0 });
const dynoSort360 = new DynoBool({ value: false });
const dynoSplats = new DynoPackedSplats();
const reader = new Readback();
const doubleSortReader = dynoBlock(
{ index: "int" },
{ rgba8: "vec4" },
({ index }) => {
if (!index) {
throw new Error("No index");
}
const sortParams = {
sortRadial: dynoSortRadial,
sortOrigin: dynoOrigin,
sortDirection: dynoDirection,
sortDepthBias: dynoDepthBias,
sort360: dynoSort360,
};
const index2 = mul(index, dynoConst("int", 2));
const gsplat0 = readPackedSplat(dynoSplats, index2);
const metric0 = computeSortMetric({ gsplat: gsplat0, ...sortParams });
const gsplat1 = readPackedSplat(
dynoSplats,
add(index2, dynoConst("int", 1)),
);
const metric1 = computeSortMetric({ gsplat: gsplat1, ...sortParams });
const combined = combine({
vectorType: "vec2",
x: metric0,
y: metric1,
});
const rgba8 = uintToRgba8(packHalf2x16(combined));
return { rgba8 };
},
);
ForgeViewpoint.dynos = {
dynoSortRadial,
dynoOrigin,
dynoDirection,
dynoDepthBias,
dynoSort360,
dynoSplats,
reader,
doubleSortReader,
};
}
return ForgeViewpoint.dynos;
}
}
const defineComputeSortMetric = unindent(`
float computeSort(Gsplat gsplat, bool sortRadial, vec3 sortOrigin, vec3 sortDirection, float sortDepthBias, bool sort360) {
if (!isGsplatActive(gsplat.flags)) {
return INFINITY;
}
vec3 center = gsplat.center - sortOrigin;
float biasedDepth = dot(center, sortDirection) + sortDepthBias;
if (!sort360 && (biasedDepth <= 0.0)) {
return INFINITY;
}
return sortRadial ? length(center) : biasedDepth;
}
`);
function computeSortMetric({
gsplat,
sortRadial,
sortOrigin,
sortDirection,
sortDepthBias,
sort360,
}: {
gsplat: DynoVal<typeof Gsplat>;
sortRadial: DynoVal<"bool">;
sortOrigin: DynoVal<"vec3">;
sortDirection: DynoVal<"vec3">;
sortDepthBias: DynoVal<"float">;
sort360: DynoVal<"bool">;
}) {
return dyno({
inTypes: {
gsplat: Gsplat,
sortRadial: "bool",
sortOrigin: "vec3",
sortDirection: "vec3",
sortDepthBias: "float",
sort360: "bool",
},
outTypes: { metric: "float" },
globals: () => [defineGsplat, defineComputeSortMetric],
inputs: {
gsplat,
sortRadial,
sortOrigin,
sortDirection,
sortDepthBias,
sort360,
},
statements: ({ inputs, outputs }) => {
const {
gsplat,
sortRadial,
sortOrigin,
sortDirection,
sortDepthBias,
sort360,
} = inputs;
return unindentLines(`
${outputs.metric} = computeSort(${gsplat}, ${sortRadial}, ${sortOrigin}, ${sortDirection}, ${sortDepthBias}, ${sort360});
`);
},
}).outputs.metric;
}
+656
View File
@@ -0,0 +1,656 @@
import * as THREE from "three";
import type { GsplatGenerator } from "./SplatGenerator";
import { type SplatFileType, unpackSplats } from "./SplatLoader";
import { SPLAT_TEX_HEIGHT, SPLAT_TEX_WIDTH } from "./defines";
import {
DynoProgram,
DynoProgramTemplate,
DynoUniform,
dynoBlock,
outputPackedSplat,
} from "./dyno";
import { TPackedSplats, definePackedSplats } from "./dyno/splats";
import computeUvec4Template from "./shaders/computeUvec4.glsl";
import { getTextureSize, setPackedSplat, unpackSplat } from "./utils";
// Initialize a PackedSplats collection from source data via
// url, fileBytes, or packedArray. Creates an empty array if none are set,
// and splat data can be constructed using pushSplat()/setSplat(). The maximum
// splat size allocation will grow automatically, starting from maxSplats.
export type PackedSplatsOptions = {
// 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;
// Reserve space for at least this many splats when constructing the collection
// initially. The array will automatically resize past maxSplats so setting it is
// an optional optimization. (default: 0)
maxSplats?: number;
// Use provided packed data array, where each 4 consecutive uint32 values
// encode one "packed" Gsplat. (default: undefined)
packedArray?: Uint32Array;
// Override number of splats in packed array to use only a subset.
// (default: length of packed array / 4)
numSplats?: number;
// Callback function to programmatically create splats at initialization.
// (default: undefined)
construct?: (splats: PackedSplats) => Promise<void> | void;
// Additional splat data, such as spherical harmonics components (sh1, sh2, sh3). (default: {})
extra?: Record<string, unknown>;
};
// A PackedSplats is a collection of Gaussian splats, packed into a format that
// takes exactly 16 bytes per Gsplat to maximize memory and cache efficiency.
// The center xyz coordinates are encoded as float16 (3 x 2 bytes), scale xyz
// as 3 x uint8 that encode a log scale from e^-9 to e^9, rgba as 4 x uint8,
// and quaternion encoded via axis+angle using 2 x uint8 for octahedral encoding
// of the axis direction and a uint8 to encode rotation amount from 0..Pi.
export class PackedSplats {
maxSplats = 0;
numSplats = 0;
packedArray: Uint32Array | null = null;
extra: Record<string, unknown>;
initialized: Promise<PackedSplats>;
isInitialized = false;
// Either target or source will be non-null, depending on whether the PackedSplats
// is being used as a data source or generated to.
target: THREE.WebGLArrayRenderTarget | null = null;
source: THREE.DataArrayTexture | null = null;
// Set to true if source packedArray is updated to have it upload to GPU
needsUpdate = true;
// A PackedSplats can be used in a dyno graph using the below property dyno:
// const gsplat = dyno.readPackedSplats(this.dyno, dynoIndex);
dyno: DynoUniform<typeof TPackedSplats, "packedSplats">;
constructor(options: PackedSplatsOptions = {}) {
this.extra = {};
this.dyno = new DynoPackedSplats({ packedSplats: this });
// The following line will be overridden by reinitialize()
this.initialized = Promise.resolve(this);
this.reinitialize(options);
}
reinitialize(options: PackedSplatsOptions) {
this.isInitialized = false;
if (options.url || options.fileBytes || options.construct) {
// We need to initialize asynchronously given the options
this.initialized = this.asyncInitialize(options).then(() => {
this.isInitialized = true;
return this;
});
} else {
this.initialize(options);
this.isInitialized = true;
this.initialized = Promise.resolve(this);
}
}
initialize(options: PackedSplatsOptions) {
if (options.packedArray) {
this.packedArray = options.packedArray;
// Calculate number of horizontal texture rows that could fit in array.
// A properly initialized packedArray should already take into account the
// width and height of the texture and be rounded up with padding.
this.maxSplats = Math.floor(this.packedArray.length / 4);
this.maxSplats =
Math.floor(this.maxSplats / SPLAT_TEX_WIDTH) * SPLAT_TEX_WIDTH;
this.numSplats = Math.min(
this.maxSplats,
options.numSplats ?? Number.POSITIVE_INFINITY,
);
} else {
this.maxSplats = options.maxSplats ?? 0;
this.numSplats = 0;
}
this.extra = options.extra ?? {};
}
async asyncInitialize(options: PackedSplatsOptions) {
let { url, fileBytes, construct } = options;
if (url) {
fileBytes = await fetch(url).then(async (response) => {
if (!response.ok) {
throw new Error(
`${response.status} "${response.statusText}" fetching URL: ${url}`,
);
}
const arrayBuffer = await response.arrayBuffer();
return arrayBuffer;
});
}
if (fileBytes) {
const unpacked = await unpackSplats({
input: fileBytes,
fileType: options.fileType,
pathOrUrl: url,
});
this.initialize(unpacked);
}
if (construct) {
const maybePromise = construct(this);
// If construct returns a promise, wait for it to complete
if (maybePromise instanceof Promise) {
await maybePromise;
}
}
}
// Call this when you are finished with the PackedSplats and want to free
// any buffers it holds.
dispose() {
if (this.target) {
this.target.dispose();
this.target = null;
}
if (this.source) {
this.source.dispose();
this.source = null;
}
}
// Ensures that this.packedArray can fit numSplats Gsplats. If it's too small,
// resize exponentially and copy over the original data.
//
// Typically you don't need to call this, because calling this.setSplat(index, ...)
// and this.pushSplat(...) will automatically call ensureSplats() so we have
// enough splats.
ensureSplats(numSplats: number): Uint32Array {
const targetSize =
numSplats <= this.maxSplats
? this.maxSplats
: // Grow exponentially to avoid frequent reallocations
Math.max(numSplats, 2 * this.maxSplats);
const currentSize = !this.packedArray ? 0 : this.packedArray.length / 4;
if (!this.packedArray || targetSize > currentSize) {
this.maxSplats = getTextureSize(targetSize).maxSplats;
const newArray = new Uint32Array(this.maxSplats * 4);
if (this.packedArray) {
// Copy over existing data
newArray.set(this.packedArray);
}
this.packedArray = newArray;
}
return this.packedArray;
}
// Ensure the extra array for the given level is large enough to hold numSplats
ensureSplatsSh(level: number, numSplats: number): Uint32Array {
let wordsPerSplat: number;
let key: string;
if (level === 0) {
return this.ensureSplats(numSplats);
}
if (level === 1) {
// 3 x 3 uint7 = 63 bits = 2 uint32
wordsPerSplat = 2;
key = "sh1";
} else if (level === 2) {
// 5 x 3 uint8 = 120 bits = 4 uint32
wordsPerSplat = 4;
key = "sh2";
} else if (level === 3) {
// 7 x 3 uint6 = 126 bits = 4 uint32
wordsPerSplat = 4;
key = "sh3";
} else {
throw new Error(`Invalid level: ${level}`);
}
// Figure out our current and desired maxSplats
let maxSplats: number = !this.extra[key]
? 0
: (this.extra[key] as Uint32Array).length / wordsPerSplat;
const targetSize =
numSplats <= maxSplats ? maxSplats : Math.max(numSplats, 2 * maxSplats);
if (!this.extra[key] || targetSize > maxSplats) {
// Reallocate the array
maxSplats = getTextureSize(targetSize).maxSplats;
const newArray = new Uint32Array(maxSplats * wordsPerSplat);
if (this.extra[key]) {
// Copy over existing data
newArray.set(this.extra[key] as Uint32Array);
}
this.extra[key] = newArray;
}
return this.extra[key] as Uint32Array;
}
// Unpack the 16-byte Gsplat data at index into the Three.js components
// center: THREE.Vector3, scales: THREE.Vector3, quaternion: THREE.Quaternion,
// opacity: number 0..1, color: THREE.Color 0..1.
getSplat(index: number): {
center: THREE.Vector3;
scales: THREE.Vector3;
quaternion: THREE.Quaternion;
opacity: number;
color: THREE.Color;
} {
if (!this.packedArray || index >= this.numSplats) {
throw new Error("Invalid index");
}
return unpackSplat(this.packedArray, index);
}
// Set all PackedSplat components at index with the provided Gsplat attributes
// (can be the same objects returned by getSplat). Ensures there is capacity
// for at least index+1 Gsplats.
setSplat(
index: number,
center: THREE.Vector3,
scales: THREE.Vector3,
quaternion: THREE.Quaternion,
opacity: number,
color: THREE.Color,
) {
const packedSplats = this.ensureSplats(index + 1);
setPackedSplat(
packedSplats,
index,
center.x,
center.y,
center.z,
scales.x,
scales.y,
scales.z,
quaternion.x,
quaternion.y,
quaternion.z,
quaternion.w,
opacity,
color.r,
color.g,
color.b,
);
this.numSplats = Math.max(this.numSplats, index + 1);
}
// Effectively calls this.setSplat(this.numSplats++, center, ...), useful on
// construction where you just want to iterate and create a collection of Gsplats.
pushSplat(
center: THREE.Vector3,
scales: THREE.Vector3,
quaternion: THREE.Quaternion,
opacity: number,
color: THREE.Color,
) {
const packedSplats = this.ensureSplats(this.numSplats + 1);
setPackedSplat(
packedSplats,
this.numSplats,
center.x,
center.y,
center.z,
scales.x,
scales.y,
scales.z,
quaternion.x,
quaternion.y,
quaternion.z,
quaternion.w,
opacity,
color.r,
color.g,
color.b,
);
++this.numSplats;
}
// Iterate over Gsplats index 0..=(this.numSplats-1), unpack each Gsplat
// and invoke the callback function with the Gsplat attributes.
forEachSplat(
callback: (
index: number,
center: THREE.Vector3,
scales: THREE.Vector3,
quaternion: THREE.Quaternion,
opacity: number,
color: THREE.Color,
) => void,
) {
if (!this.packedArray || !this.numSplats) {
return;
}
for (let i = 0; i < this.numSplats; ++i) {
const unpacked = unpackSplat(this.packedArray, i);
callback(
i,
unpacked.center,
unpacked.scales,
unpacked.quaternion,
unpacked.opacity,
unpacked.color,
);
}
}
// Ensures our PackedSplats.target render target has enough space to generate
// maxSplats total Gsplats, and reallocate if not large enough.
ensureGenerate(maxSplats: number): boolean {
if (this.target && (maxSplats ?? 1) <= this.maxSplats) {
return false;
}
this.dispose();
const textureSize = getTextureSize(maxSplats ?? 1);
const { width, height, depth } = textureSize;
this.maxSplats = textureSize.maxSplats;
// The packed Gsplats are stored in a 2D array texture of max size
// 2048 x 2048 x 2048, one RGBA32UI pixel = 4 uint32 = one Gsplat
this.target = new THREE.WebGLArrayRenderTarget(width, height, depth, {
depthBuffer: false,
stencilBuffer: false,
generateMipmaps: false,
magFilter: THREE.NearestFilter,
minFilter: THREE.NearestFilter,
});
this.target.texture.format = THREE.RGBAIntegerFormat;
this.target.texture.type = THREE.UnsignedIntType;
this.target.texture.internalFormat = "RGBA32UI";
return true;
}
// Given an array of splatCounts (.numSplats for each
// SplatGenerator/SplatMesh in the scene), compute a
// "mapping layout" in the composite array of generated outputs.
generateMapping(splatCounts: number[]): {
maxSplats: number;
mapping: { base: number; count: number }[];
} {
let maxSplats = 0;
const mapping = splatCounts.map((numSplats) => {
const base = maxSplats;
// Generation happens in horizonal row chunks, so round up to full width
const rounded = Math.ceil(numSplats / SPLAT_TEX_WIDTH) * SPLAT_TEX_WIDTH;
maxSplats += rounded;
return { base, count: numSplats };
});
return { maxSplats, mapping };
}
// Returns a THREE.DataArrayTexture representing the PackedSplats content as
// a Uint32x4 data array texture (2048 x 2048 x depth in size)
getTexture(): THREE.DataArrayTexture {
if (this.target) {
// Return the render target's texture
return this.target.texture;
}
if (this.source || this.packedArray) {
// Update source texture if needed and return
const source = this.maybeUpdateSource();
return source;
}
return PackedSplats.getEmpty();
}
// Check if source texture needs to be created/updated
private maybeUpdateSource(): THREE.DataArrayTexture {
if (!this.packedArray) {
throw new Error("No packed splats");
}
if (this.needsUpdate || !this.source) {
this.needsUpdate = false;
if (this.source) {
const { width, height, depth } = this.source.image;
if (this.maxSplats !== width * height * depth) {
// The existing source texture isn't the right size, so dispose it
this.source.dispose();
this.source = null;
}
}
if (!this.source) {
// Allocate a new source texture of the right size
const { width, height, depth } = getTextureSize(this.maxSplats);
this.source = new THREE.DataArrayTexture(
this.packedArray,
width,
height,
depth,
);
this.source.format = THREE.RGBAIntegerFormat;
this.source.type = THREE.UnsignedIntType;
this.source.internalFormat = "RGBA32UI";
this.source.needsUpdate = true;
} else if (this.packedArray.buffer !== this.source.image.data.buffer) {
// The source texture is the right size, update the data
this.source.image.data = new Uint8Array(this.packedArray.buffer);
}
// Indicate to Three.js that the source texture needs to be uploaded to the GPU
this.source.needsUpdate = true;
}
return this.source;
}
private static emptySource: THREE.DataArrayTexture | null = null;
// Can be used where you need an uninitialized THREE.DataArrayTexture like
// a uniform you will update with the result of this.getTexture() later.
static getEmpty(): THREE.DataArrayTexture {
if (!PackedSplats.emptySource) {
const { width, height, depth, maxSplats } = getTextureSize(1);
const emptyArray = new Uint32Array(maxSplats * 4);
PackedSplats.emptySource = new THREE.DataArrayTexture(
emptyArray,
width,
height,
depth,
);
PackedSplats.emptySource.format = THREE.RGBAIntegerFormat;
PackedSplats.emptySource.type = THREE.UnsignedIntType;
PackedSplats.emptySource.internalFormat = "RGBA32UI";
PackedSplats.emptySource.needsUpdate = true;
}
return PackedSplats.emptySource;
}
// Get a program and THREE.RawShaderMaterial for a given GsplatGenerator,
// generating it if necessary and caching the result.
prepareProgramMaterial(generator: GsplatGenerator): {
program: DynoProgram;
material: THREE.RawShaderMaterial;
} {
let program = PackedSplats.generatorProgram.get(generator);
if (!program) {
// A Gsplat needs to be turned into a packed uvec4 for the dyno graph
const graph = dynoBlock(
{ index: "int" },
{ output: "uvec4" },
({ index }) => {
generator.inputs.index = index;
const gsplat = generator.outputs.gsplat;
const output = outputPackedSplat(gsplat);
return { output };
},
);
if (!PackedSplats.programTemplate) {
PackedSplats.programTemplate = new DynoProgramTemplate(
computeUvec4Template,
);
}
// Create a program from the template and graph
program = new DynoProgram({
graph,
inputs: { index: "index" },
outputs: { output: "target" },
template: PackedSplats.programTemplate,
});
Object.assign(program.uniforms, {
targetLayer: { value: 0 },
targetBase: { value: 0 },
targetCount: { value: 0 },
});
PackedSplats.generatorProgram.set(generator, program);
}
// Prepare and update our material we'll use to render the Gsplats
const material = program.prepareMaterial();
PackedSplats.mesh.material = material;
return { program, material };
}
private saveRenderState(renderer: THREE.WebGLRenderer) {
return {
xrPresenting: renderer.xr.isPresenting,
autoClear: renderer.autoClear,
scissorTest: renderer.getScissorTest(),
pixelRatio: renderer.getPixelRatio(),
};
}
private resetRenderState(
renderer: THREE.WebGLRenderer,
state: {
xrPresenting: boolean;
autoClear: boolean;
scissorTest: boolean;
pixelRatio: number;
},
) {
renderer.setRenderTarget(null);
renderer.setPixelRatio(state.pixelRatio);
renderer.xr.isPresenting = state.xrPresenting;
renderer.autoClear = state.autoClear;
renderer.setScissorTest(state.scissorTest);
}
// Executes a dyno program specified by generator which is any DynoBlock that
// maps { index: "int" } to { gsplat: Gsplat }. This is called in
// ForgeRenderer.updateInternal() to re-generate Gsplats in the scene for
// SplatGenerator instances whose version is newer than what was generated
// for it last time.
generate({
generator,
base,
count,
renderer,
}: {
generator: GsplatGenerator;
base: number;
count: number;
renderer: THREE.WebGLRenderer;
}): { nextBase: number } {
if (!this.target) {
throw new Error("Target must be initialized with ensureSplats");
}
if (base + count > this.maxSplats) {
throw new Error("Base + count exceeds maxSplats");
}
const { program, material } = this.prepareProgramMaterial(generator);
program.update();
const renderState = this.saveRenderState(renderer);
// Generate the Gsplats in "layer" chunks, in horizontal row ranges,
// that cover the total count of Gsplats.
const nextBase =
Math.ceil((base + count) / SPLAT_TEX_WIDTH) * SPLAT_TEX_WIDTH;
const layerSize = SPLAT_TEX_WIDTH * SPLAT_TEX_HEIGHT;
material.uniforms.targetBase.value = base;
material.uniforms.targetCount.value = count;
// Keep generating layers until we've reached the next generation's base
while (base < nextBase) {
const layer = Math.floor(base / layerSize);
material.uniforms.targetLayer.value = layer;
const layerBase = layer * layerSize;
const layerYStart = Math.floor((base - layerBase) / SPLAT_TEX_WIDTH);
const layerYEnd = Math.min(
SPLAT_TEX_HEIGHT,
Math.ceil((nextBase - layerBase) / SPLAT_TEX_WIDTH),
);
// Render the desired portion of the layer
renderer.setPixelRatio(1);
renderer.setRenderTarget(this.target, layer);
renderer.xr.isPresenting = false;
renderer.autoClear = false;
renderer.setScissorTest(true);
renderer.setScissor(
0,
layerYStart,
SPLAT_TEX_WIDTH,
layerYEnd - layerYStart,
);
renderer.render(PackedSplats.scene, PackedSplats.camera);
base += SPLAT_TEX_WIDTH * (layerYEnd - layerYStart);
}
this.resetRenderState(renderer, renderState);
return { nextBase };
}
static programTemplate: DynoProgramTemplate | null = null;
// Cache for GsplatGenerator programs
static generatorProgram = new Map<GsplatGenerator, DynoProgram>();
// Static Three.js objects for pseudo-compute shader rendering
static geometry = new THREE.PlaneGeometry(2, 2);
static mesh = new THREE.Mesh(
PackedSplats.geometry,
new THREE.RawShaderMaterial({ visible: false }),
);
static scene = new THREE.Scene().add(PackedSplats.mesh);
static camera = new THREE.Camera();
}
// You can use a PackedSplats as a dyno block using the function
// dyno.readPackedSplats(packedSplats.dyno, dynoIndex) where
// dynoIndex is of type DynoVal<"int">. If you need to be able to change
// the input PackedSplats dynamically, however, you should create a
// DynoPackedSplats, whose property packedSplats you can change to any
// PackedSplats and that will be used in the dyno shader program.
export const dynoPackedSplats = (packedSplats?: PackedSplats) =>
new DynoPackedSplats({ packedSplats });
export class DynoPackedSplats extends DynoUniform<
typeof TPackedSplats,
"packedSplats",
{
texture: THREE.DataArrayTexture;
numSplats: number;
}
> {
packedSplats?: PackedSplats;
constructor({ packedSplats }: { packedSplats?: PackedSplats } = {}) {
super({
key: "packedSplats",
type: TPackedSplats,
globals: () => [definePackedSplats],
value: {
texture: PackedSplats.getEmpty(),
numSplats: 0,
},
update: (value) => {
value.texture =
this.packedSplats?.getTexture() ?? PackedSplats.getEmpty();
value.numSplats = this.packedSplats?.numSplats ?? 0;
return value;
},
});
this.packedSplats = packedSplats;
}
}
+349
View File
@@ -0,0 +1,349 @@
import * as THREE from "three";
import { SPLAT_TEX_HEIGHT, SPLAT_TEX_WIDTH } from "./defines";
import { type Dyno, OutputRgba8, dynoBlock } from "./dyno";
import { DynoProgram, DynoProgramTemplate } from "./dyno/program";
import computeVec4Template from "./shaders/computeVec4.glsl";
import { getTextureSize } from "./utils";
// Readback can be used to run a Dyno program that maps an index to a 32-bit
// RGBA8 value, which is the only allowed, portable readback format for WebGL2.
// Using data packing and conversion you can read back any 32-bit value, which
// Forge uses to read back 2 float16 Gsplat distance values per index.
export type Rgba8Readback = Dyno<{ index: "int" }, { rgba8: "vec4" }>;
// Readback can be performed with various typed buffers, making it convenient
// to encode readback data in a variety of formats.
export type ReadbackBuffer =
| ArrayBuffer
| Uint8Array
| Int8Array
| Uint16Array
| Int16Array
| Uint32Array
| Int32Array
| Float32Array;
export class Readback {
renderer?: THREE.WebGLRenderer;
target?: THREE.WebGLArrayRenderTarget;
capacity: number;
count: number;
constructor({ renderer }: { renderer?: THREE.WebGLRenderer } = {}) {
this.renderer = renderer;
this.capacity = 0;
this.count = 0;
}
dispose() {
if (this.target) {
this.target.dispose();
this.target = undefined;
}
}
// Ensure we have a buffer large enough for the readback of count indices.
// Pass in previous bufer of the desired type.
ensureBuffer<B extends ReadbackBuffer>(count: number, buffer: B): B {
// Readback is performed in a 2D array of pixels, so round up with SPLAT_TEX_WIDTH
const roundedCount =
Math.ceil(Math.max(1, count) / SPLAT_TEX_WIDTH) * SPLAT_TEX_WIDTH;
const bytes = roundedCount * 4;
if (buffer.byteLength >= bytes) {
return buffer;
}
// Need a larger buffer, create a new one of the same type
const newBuffer = new ArrayBuffer(bytes);
if (buffer instanceof ArrayBuffer) {
return newBuffer as B;
}
const ctor = buffer.constructor as { new (arrayBuffer: ArrayBuffer): B };
return new ctor(newBuffer) as B;
}
// Ensure our render target is large enough for the readback of capacity indices.
ensureCapacity(capacity: number) {
const { width, height, depth, maxSplats } = getTextureSize(capacity);
if (!this.target || maxSplats > this.capacity) {
this.dispose();
this.capacity = maxSplats;
// The only portable readback format for WebGL2 is RGBA8
this.target = new THREE.WebGLArrayRenderTarget(width, height, depth, {
depthBuffer: false,
stencilBuffer: false,
generateMipmaps: false,
magFilter: THREE.NearestFilter,
minFilter: THREE.NearestFilter,
});
this.target.texture.format = THREE.RGBAFormat;
this.target.texture.type = THREE.UnsignedByteType;
this.target.texture.internalFormat = "RGBA8";
}
}
// Get a program and THREE.RawShaderMaterial for a given Rgba8Readback,
// generating it if necessary and caching the result.
prepareProgramMaterial(reader: Rgba8Readback): {
program: DynoProgram;
material: THREE.RawShaderMaterial;
} {
let program = Readback.readbackProgram.get(reader);
if (!program) {
const graph = dynoBlock(
{ index: "int" },
{ rgba8: "vec4" },
({ index }) => {
reader.inputs.index = index;
const rgba8 = new OutputRgba8({ rgba8: reader.outputs.rgba8 });
return { rgba8 };
},
);
if (!Readback.programTemplate) {
Readback.programTemplate = new DynoProgramTemplate(computeVec4Template);
}
// Create a program from the template and graph
program = new DynoProgram({
graph,
inputs: { index: "index" },
outputs: { rgba8: "target" },
template: Readback.programTemplate,
});
Object.assign(program.uniforms, {
targetLayer: { value: 0 },
targetBase: { value: 0 },
targetCount: { value: 0 },
});
Readback.readbackProgram.set(reader, program);
}
const material = program.prepareMaterial();
Readback.mesh.material = material;
return { program, material };
}
private saveRenderState(renderer: THREE.WebGLRenderer) {
return {
xrPresenting: renderer.xr.isPresenting,
autoClear: renderer.autoClear,
scissorTest: renderer.getScissorTest(),
pixelRatio: renderer.getPixelRatio(),
};
}
private resetRenderState(
renderer: THREE.WebGLRenderer,
state: {
xrPresenting: boolean;
autoClear: boolean;
scissorTest: boolean;
pixelRatio: number;
},
) {
renderer.setRenderTarget(null);
renderer.setPixelRatio(state.pixelRatio);
renderer.xr.isPresenting = state.xrPresenting;
renderer.autoClear = state.autoClear;
renderer.setScissorTest(state.scissorTest);
}
private process({
count,
material,
}: { count: number; material: THREE.RawShaderMaterial }) {
const renderer = this.renderer;
if (!renderer) {
throw new Error("No renderer");
}
if (!this.target) {
throw new Error("No target");
}
// Run the program in "layer" chunks, in horizontal row ranges,
// that cover the total count of indices.
const layerSize = SPLAT_TEX_WIDTH * SPLAT_TEX_HEIGHT;
material.uniforms.targetBase.value = 0;
material.uniforms.targetCount.value = count;
let baseIndex = 0;
// Keep generating layers until completed count items
while (baseIndex < count) {
const layer = Math.floor(baseIndex / layerSize);
const layerBase = layer * layerSize;
const layerYEnd = Math.min(
SPLAT_TEX_HEIGHT,
Math.ceil((count - layerBase) / SPLAT_TEX_WIDTH),
);
material.uniforms.targetLayer.value = layer;
// Render the desired portion of the layer
renderer.setPixelRatio(1);
renderer.setRenderTarget(this.target, layer);
renderer.xr.isPresenting = false;
renderer.autoClear = false;
renderer.setScissorTest(true);
renderer.setScissor(0, 0, SPLAT_TEX_WIDTH, layerYEnd);
renderer.render(Readback.scene, Readback.camera);
baseIndex += SPLAT_TEX_WIDTH * layerYEnd;
}
this.count = count;
}
private async read<B extends ReadbackBuffer>({
readback,
}: { readback: B }): Promise<B> {
const renderer = this.renderer;
if (!renderer) {
throw new Error("No renderer");
}
if (!this.target) {
throw new Error("No target");
}
const roundedCount =
Math.ceil(this.count / SPLAT_TEX_WIDTH) * SPLAT_TEX_WIDTH;
if (readback.byteLength < roundedCount * 4) {
throw new Error(
`Readback buffer too small: ${readback.byteLength} < ${roundedCount * 4}`,
);
}
const readbackUint8 = new Uint8Array(
readback instanceof ArrayBuffer ? readback : readback.buffer,
);
// We can only read back one 2D array layer of pixels at a time,
// so loop through them, initiate the readback, and collect the
// completion promises.
const layerSize = SPLAT_TEX_WIDTH * SPLAT_TEX_HEIGHT;
let baseIndex = 0;
const promises = [];
while (baseIndex < this.count) {
const layer = Math.floor(baseIndex / layerSize);
const layerBase = layer * layerSize;
const layerYEnd = Math.min(
SPLAT_TEX_HEIGHT,
Math.ceil((this.count - layerBase) / SPLAT_TEX_WIDTH),
);
renderer.setPixelRatio(1);
renderer.setRenderTarget(this.target, layer);
// Compute the subarray that this layer of readback corresponds to
const readbackSize = SPLAT_TEX_WIDTH * layerYEnd * 4;
const subReadback = readbackUint8.subarray(
layerBase * 4,
layerBase * 4 + readbackSize,
);
const promise = renderer?.readRenderTargetPixelsAsync(
this.target,
0,
0,
SPLAT_TEX_WIDTH,
layerYEnd,
subReadback,
);
promises.push(promise);
baseIndex += SPLAT_TEX_WIDTH * layerYEnd;
}
return Promise.all(promises).then(() => readback);
}
// Perform render operation to run the Rgba8Readback program
// but don't perform the readback yet.
render({
reader,
count,
renderer,
}: { reader: Rgba8Readback; count: number; renderer?: THREE.WebGLRenderer }) {
this.renderer = renderer || this.renderer;
if (!this.renderer) {
throw new Error("No renderer");
}
this.ensureCapacity(count);
const { program, material } = this.prepareProgramMaterial(reader);
program.update();
const renderState = this.saveRenderState(this.renderer);
this.process({ count, material });
this.resetRenderState(this.renderer, renderState);
}
// Perform a readback of the render target, returning a buffer of the
// given type.
async readback<B extends ReadbackBuffer>({
readback,
}: { readback: B }): Promise<B> {
if (!this.renderer) {
throw new Error("No renderer");
}
const renderState = this.saveRenderState(this.renderer);
const promise = this.read({ readback });
this.resetRenderState(this.renderer, renderState);
return promise;
}
// Perform a render and readback operation for the given Rgba8Readback,
// and readback buffer (call ensureBuffer first).
async renderReadback<B extends ReadbackBuffer>({
reader,
count,
renderer,
readback,
}: {
reader: Rgba8Readback;
count: number;
renderer?: THREE.WebGLRenderer;
readback: B;
}): Promise<B> {
this.renderer = renderer || this.renderer;
if (!this.renderer) {
throw new Error("No renderer");
}
this.ensureCapacity(count);
const { program, material } = this.prepareProgramMaterial(reader);
program.update();
const renderState = this.saveRenderState(this.renderer);
// Generate output
this.process({ count, material });
// Initiate readback
const promise = this.read({ readback });
this.resetRenderState(this.renderer, renderState);
return promise;
}
getTexture(): THREE.DataArrayTexture | undefined {
return this.target?.texture;
}
static programTemplate: DynoProgramTemplate | null = null;
// Cache for Rgba8Readback programs
static readbackProgram = new Map<Rgba8Readback, DynoProgram>();
// Static Three.js objects for pseudo-compute shader rendering
static geometry = new THREE.PlaneGeometry(2, 2);
static mesh = new THREE.Mesh(
Readback.geometry,
new THREE.RawShaderMaterial({ visible: false }),
);
static scene = new THREE.Scene().add(Readback.mesh);
static camera = new THREE.Camera();
}
+283
View File
@@ -0,0 +1,283 @@
import * as THREE from "three";
import { DynoPackedSplats, type PackedSplats } from "./PackedSplats";
import { Readback, type Rgba8Readback } from "./Readback";
import { SPLAT_TEX_WIDTH } from "./defines";
import {
Dyno,
type DynoBlock,
DynoInt,
DynoUniform,
type DynoVal,
add,
dynoBlock,
readPackedSplatRange,
splitGsplat,
unindent,
unindentLines,
} from "./dyno";
import { getTextureSize } from "./utils";
// An RgbaArray is a collection of ordered RGBA8 values, which can be used as a dyno
// data source, for example for recoloring Gsplats via SplatMesh.splatRgba.
// It can be instantiated from a Uint8Array of RGBA8 values, or it can be
// generated using a Rgba8Readback dyno program.
export type RgbaArrayOptions = {
// Reserve space for at least this many RGBA values.
capacity?: number;
// Use the provided array of RGBA8 values as the source.
array?: Uint8Array;
// The number of actual RGBA8 values in the array.
count?: number;
};
export class RgbaArray {
capacity = 0;
count = 0;
array: Uint8Array | null = null;
readback: Readback | null = null;
source: THREE.DataArrayTexture | null = null;
// Set to true if source array is updated to have it upload to GPU
needsUpdate = true;
// Use this as a TRgbaArray in a dyno graph
dyno: DynoUniform<typeof TRgbaArray, "rgbaArray">;
constructor(options: RgbaArrayOptions = {}) {
this.dyno = new DynoUniform({
key: "rgbaArray",
type: TRgbaArray,
globals: () => [defineRgbaArray],
value: {
texture: RgbaArray.getEmpty(),
count: 0,
},
update: (value) => {
value.texture =
this.readback?.getTexture() ?? this.source ?? RgbaArray.getEmpty();
value.count = this.count;
return value;
},
});
if (options.array) {
// Initialize with given array
this.array = options.array;
this.capacity = Math.floor(this.array.length / 4);
this.capacity =
Math.floor(this.capacity / SPLAT_TEX_WIDTH) * SPLAT_TEX_WIDTH;
this.count = Math.min(
this.capacity,
options.count ?? Number.POSITIVE_INFINITY,
);
} else {
this.capacity = options.capacity ?? 0;
this.count = 0;
}
}
// Free up resources
dispose() {
if (this.readback) {
this.readback.dispose();
this.readback = null;
}
if (this.source) {
this.source.dispose();
this.source = null;
}
}
// Ensure that our array is large enough to hold capacity RGBA8 values.
ensureCapacity(capacity: number): Uint8Array {
if (!this.array || capacity > (this.array?.length ?? 0) / 4) {
this.capacity = getTextureSize(capacity).maxSplats;
const newArray = new Uint8Array(this.capacity * 4);
if (this.array) {
// Copy over existing data
newArray.set(this.array);
}
this.array = newArray;
}
return this.array;
}
// Get the THREE.DataArrayTexture from either the readback or the source.
getTexture(): THREE.DataArrayTexture {
let texture = this.readback?.getTexture();
if (this.source || this.array) {
texture = this.maybeUpdateSource();
}
return texture ?? RgbaArray.getEmpty();
}
// Create or get a THREE.DataArrayTexture from the data array.
private maybeUpdateSource(): THREE.DataArrayTexture {
if (!this.array) {
throw new Error("No array");
}
if (this.needsUpdate || !this.source) {
this.needsUpdate = false;
if (this.source) {
const { width, height, depth } = this.source.image;
if (this.capacity !== width * height * depth) {
this.source.dispose();
this.source = null;
}
}
if (!this.source) {
const { width, height, depth } = getTextureSize(this.capacity);
this.source = new THREE.DataArrayTexture(
this.array,
width,
height,
depth,
);
this.source.format = THREE.RGBAFormat;
this.source.type = THREE.UnsignedByteType;
this.source.internalFormat = "RGBA8";
this.source.needsUpdate = true;
} else if (this.array.buffer !== this.source.image.data.buffer) {
this.source.image.data = new Uint8Array(this.array.buffer);
}
this.source.needsUpdate = true;
}
return this.source;
}
// Generate the RGBA8 values from a Rgba8Readback dyno program.
render({
reader,
count,
renderer,
}: { reader: Rgba8Readback; count: number; renderer: THREE.WebGLRenderer }) {
if (!this.readback) {
this.readback = new Readback({ renderer });
}
this.readback.render({ reader, count, renderer });
this.capacity = this.readback.capacity;
this.count = this.readback.count;
}
// Extract the RGBA8 values from a PackedSplats collection.
fromPackedSplats({
packedSplats,
base,
count,
renderer,
}: {
packedSplats: PackedSplats;
base: number;
count: number;
renderer: THREE.WebGLRenderer;
}) {
const { dynoSplats, dynoBase, dynoCount, reader } = RgbaArray.makeDynos();
dynoSplats.packedSplats = packedSplats;
dynoBase.value = base;
dynoCount.value = count;
this.render({ reader, count, renderer });
return this;
}
// Read back the RGBA8 values from the readback buffer.
async read(): Promise<Uint8Array> {
if (!this.readback) {
throw new Error("No readback");
}
if (!this.array || this.array.length < this.count * 4) {
this.array = new Uint8Array(this.capacity * 4);
}
const result = await this.readback.readback({ readback: this.array });
return result.subarray(0, this.count * 4);
}
private static emptySource: THREE.DataArrayTexture | null = null;
// Can be used where you need an uninitialized THREE.DataArrayTexture like
// a uniform you will update with the result of this.getTexture() later.
static getEmpty(): THREE.DataArrayTexture {
if (!RgbaArray.emptySource) {
const emptyArray = new Uint8Array(1 * 4);
RgbaArray.emptySource = new THREE.DataArrayTexture(emptyArray, 1, 1, 1);
RgbaArray.emptySource.format = THREE.RGBAFormat;
RgbaArray.emptySource.type = THREE.UnsignedByteType;
RgbaArray.emptySource.internalFormat = "RGBA8";
RgbaArray.emptySource.needsUpdate = true;
}
return RgbaArray.emptySource;
}
private static dynos: {
dynoSplats: DynoPackedSplats;
dynoBase: DynoInt<string>;
dynoCount: DynoInt<string>;
reader: DynoBlock<{ index: "int" }, { rgba8: "vec4" }>;
} | null = null;
// Create a dyno program that can extract RGBA8 values from a PackedSplats
private static makeDynos() {
if (!RgbaArray.dynos) {
const dynoSplats = new DynoPackedSplats();
const dynoBase = new DynoInt({ value: 0 });
const dynoCount = new DynoInt({ value: 0 });
const reader = dynoBlock(
{ index: "int" },
{ rgba8: "vec4" },
({ index }) => {
if (!index) {
throw new Error("index is undefined");
}
index = add(index, dynoBase);
const gsplat = readPackedSplatRange(
dynoSplats,
index,
dynoBase,
dynoCount,
);
return { rgba8: splitGsplat(gsplat).outputs.rgba };
},
);
RgbaArray.dynos = { dynoSplats, dynoBase, dynoCount, reader };
}
return RgbaArray.dynos;
}
}
// Dyno types and definitions
export const TRgbaArray = { type: "RgbaArray" } as { type: "RgbaArray" };
export const defineRgbaArray = unindent(`
struct RgbaArray {
sampler2DArray texture;
int count;
};
`);
export function readRgbaArray(
rgba: DynoVal<typeof TRgbaArray>,
index: DynoVal<"int">,
): DynoVal<"vec4"> {
const dyno = new Dyno<
{ rgba: typeof TRgbaArray; index: "int" },
{ rgba: "vec4" }
>({
inTypes: { rgba: TRgbaArray, index: "int" },
outTypes: { rgba: "vec4" },
inputs: { rgba, index },
globals: () => [defineRgbaArray],
statements: ({ inputs, outputs }) =>
unindentLines(`
if ((index >= 0) && (index < ${inputs.rgba}.count)) {
${outputs.rgba} = texelFetch(${inputs.rgba}.texture, splatTexCoord(index), 0);
} else {
${outputs.rgba} = vec4(0.0, 0.0, 0.0, 0.0);
}
`),
});
return dyno.outputs.rgba;
}
+120
View File
@@ -0,0 +1,120 @@
import * as THREE from "three";
import { PackedSplats } from "./PackedSplats";
import type {
GsplatGenerator,
SplatGenerator,
SplatModifier,
} from "./SplatGenerator";
// SplatAccumulator helps manage the generation of splats from multiple
// SplatGenerators, keeping track of the splat mapping, coordinate system,
// and reference count.
// A GeneratorMapping describes a Gsplat range that was generated, including
// which generator and its version number.
export type GeneratorMapping = {
node: SplatGenerator;
generator?: GsplatGenerator;
version: number;
base: number;
count: number;
};
export class SplatAccumulator {
splats = new PackedSplats();
// The transform from Accumulator coordinate system to world coordinates.
toWorld = new THREE.Matrix4();
// An array of all Gsplat mappings that were used for generation
mapping: GeneratorMapping[] = [];
// Number of ForgeViewpoints (or other) that reference this accumulator, used
// to figure out when it can be recycled for use
refCount = 0;
// Incremented every time the splats are updated/generated.
splatsVersion = -1;
// Incremented every time the splat mapping/layout is updated.
// Splat sort order can be reused between equivalent mapping versions.
mappingVersion = -1;
ensureGenerate(maxSplats: number) {
if (this.splats.ensureGenerate(maxSplats)) {
// If we had to resize our PackedSplats then clear all previous mappings
this.mapping = [];
}
}
// Generate all Gsplats from an array of generators
generateSplats({
renderer,
modifier,
generators,
forceUpdate,
originToWorld,
}: {
renderer: THREE.WebGLRenderer;
modifier: SplatModifier;
generators: GeneratorMapping[];
forceUpdate?: boolean;
originToWorld: THREE.Matrix4;
}) {
// Create a lookup from last SplatGenerator
const mapping = this.mapping.reduce((map, record) => {
map.set(record.node, record);
return map;
}, new Map<SplatGenerator, GeneratorMapping>());
// Run generators that are different from existing mapping
let updated = 0;
let numSplats = 0;
for (const { node, generator, version, base, count } of generators) {
const current = mapping.get(node);
if (
forceUpdate ||
generator !== current?.generator ||
version !== current?.version ||
base !== current?.base ||
count !== current?.count
) {
// Something is different from before so we should generate these Gsplats
if (generator && count > 0) {
const modGenerator = modifier.apply(generator);
try {
this.splats.generate({
generator: modGenerator,
base,
count,
renderer,
});
} catch (error) {
node.generator = undefined;
node.generatorError = error;
}
updated += 1;
}
}
numSplats = Math.max(numSplats, base + count);
}
this.splats.numSplats = numSplats;
this.toWorld = originToWorld;
this.mapping = generators;
return updated !== 0;
}
// Check if this accumulator has exactly the same generator mapping as
// the previous one. If so, we can reuse the Gsplat sort order.
hasCorrespondence(other: SplatAccumulator) {
if (this.mapping.length !== other.mapping.length) {
return false;
}
return this.mapping.every(({ node, base, count }, i) => {
const {
node: otherNode,
base: otherBase,
count: otherCount,
} = other.mapping[i];
return node === otherNode && base === otherBase && count === otherCount;
});
}
}
+827
View File
@@ -0,0 +1,827 @@
import * as THREE from "three";
import {
Dyno,
DynoInt,
DynoUniform,
type DynoVal,
Gsplat,
unindent,
unindentLines,
} from "./dyno";
import { newArray } from "./utils";
// Forge provides the ability to apply "edits" to Gsplats as part of the standard
// SplatMesh pipeline. These edits take the form of a sequence of operations,
// applied one at a time to the set of Gsplats in its packedSplats. Each operation
// evaluates a 7-dimensional field (RGBA and XYZ displacement) at each point in
// space that derives from N=1 or more Signed Distance Field shapes (such as spheres,
// boxes, planes, etc.), blended together and across inside-outisde boundaries.
// The result is a an RGBA,XYZ value for each point in space, which combined with
// SplatEditRgbaBlendMode.MULTIPLY/SET_RGB/ADD_RGBA can be used to create special
// effects, for example simulating simple lighting or applying deformations in space,
// whose parameters can be updated each frame to create animated effects.
// RGBA-XYZ values are computed by blending together values from all SDF shapes using
// the exponential "softmax" function, which is commutative (so blending order within
// a SplatEdit operation doesn't matter). The parameter SplatEdit.sdfSmooth controls
// the blending scale between SDF shapes, while SplatEdit.softEdge controls the scale
// of soft inside-outside shape edit blending. Their default values start at 0.0 and
// should be increased to soften the effect.
// Note that XYZ displacement values are blended in the same way as RGBA, with a
// resulting displacement field that can be quite complex but "softly" blending
// between shapes. These RGBA-XYZ edits, along with time-based and overlapping
// fields can create many interesting animations and special effects, such as
// rippling leaves in the wind, an angry fire, or a looping water effects. Simply
// update the SplatEdit and SplatEditSdf objects and the operations will be applied
// immediately to the Gsplats in the scene.
export enum SplatEditSdfType {
// ALL: Affects all points in space
ALL = "all",
// PLANE: Infinite plane (position, rotation)
PLANE = "plane",
// SPHERE: Sphere (position, radius)
SPHERE = "sphere",
// BOX: Rounded box (position, rotation, sizes, radius)
BOX = "box",
// ELLIPSOID: Ellipsoid (position, rotation, sizes)
ELLIPSOID = "ellipsoid",
// CYLINDER: Cylinder (position, rotation, radius, size_y)
CYLINDER = "cylinder",
// CAPSULE: Capsule (position, rotation, radius, size_y)
CAPSULE = "capsule",
// INFINITE_CONE: Infinite cone (position, rotation, radius=angle)
INFINITE_CONE = "infinite_cone",
}
function sdfTypeToNumber(type: SplatEditSdfType) {
switch (type) {
case SplatEditSdfType.ALL:
return 0;
case SplatEditSdfType.PLANE:
return 1;
case SplatEditSdfType.SPHERE:
return 2;
case SplatEditSdfType.BOX:
return 3;
case SplatEditSdfType.ELLIPSOID:
return 4;
case SplatEditSdfType.CYLINDER:
return 5;
case SplatEditSdfType.CAPSULE:
return 6;
case SplatEditSdfType.INFINITE_CONE:
return 7;
default:
throw new Error(`Unknown SDF type: ${type}`);
}
}
export enum SplatEditRgbaBlendMode {
// The RGBA of the splat is multiplied component-wise by the SDF’s
// RGBA value at that point in space.
MULTIPLY = "multiply",
// Ignore the Alpha value in the SDF, but set the splat’s RGB to
// equal the SDF’s RGB value at that point.
SET_RGB = "set_rgb",
// Add the SDF’s RGBA value at that point to the RGBA value of
// the Gsplat. This can produce hyper-saturated results, but is useful
// to easily “light up” areas.
ADD_RGBA = "add_rgba",
}
function rgbaBlendModeToNumber(mode: SplatEditRgbaBlendMode) {
switch (mode) {
case SplatEditRgbaBlendMode.MULTIPLY:
return 0;
case SplatEditRgbaBlendMode.SET_RGB:
return 1;
case SplatEditRgbaBlendMode.ADD_RGBA:
return 2;
default:
throw new Error(`Unknown blend mode: ${mode}`);
}
}
export type SplatEditSdfOptions = {
// The SDF shape type: ALL, PLANE, SPHERE, BOX, ELLIPSOID, CYLINDER, CAPSULE,
// or INFINITE_CONE. (default: SplatEditSdfType.SPHERE)
type?: SplatEditSdfType;
// Invert the SDF evaluation, swapping inside and outside regions. (default: false)
invert?: boolean;
// Opacity / "alpha" value used differently by blending modes (default: 1.0)
opacity?: number;
// RGB color applied within the shape. (default: new THREE.Color(1.0, 1.0, 1.0))
color?: THREE.Color;
// XYZ displacement applied to splat positions inside the shape.
// (default: new THREE.Vector3(0.0, 0.0, 0.0))
displace?: THREE.Vector3;
// Shape-specific size parameter: sphere radius, box corner rounding,
// cylinder/capsule radius, or for the infinite cone the angle factor
// (opening half-angle = π/4 × radius).
radius?: number;
};
export class SplatEditSdf extends THREE.Object3D {
type: SplatEditSdfType;
invert: boolean;
opacity: number;
color: THREE.Color;
displace: THREE.Vector3;
radius: number;
constructor(options: SplatEditSdfOptions = {}) {
super();
const { type, invert, opacity, color, displace, radius } = options;
this.type = type ?? SplatEditSdfType.SPHERE;
this.invert = invert ?? false;
this.opacity = opacity ?? 1.0;
this.color = color ?? new THREE.Color(1.0, 1.0, 1.0);
this.displace = displace ?? new THREE.Vector3(0.0, 0.0, 0.0);
this.radius = radius ?? 0.0;
}
}
export type SplatEditOptions = {
// Name of this edit operation. If you omit it, a default "Edit 1", "Edit 2", ...
// is assigned.
name?: string;
// How the SDF’s RGBA modifies each splat’s RGBA: multiply, overwrite RGB,
// or add RGBA. (default: MULTIPLY)
rgbaBlendMode?: SplatEditRgbaBlendMode;
// Smoothing (in world‐space units) for blending between multiple SDF shapes
// at their boundaries. (default: 0.0)
sdfSmooth?: number;
// Soft‐edge falloff radius (in world‐space units) around each SDF shape’s surface.
// (default: 0.0)
softEdge?: number;
// Invert the SDF evaluation (inside/outside swap). (default: false)
invert?: boolean;
// Explicit array of SplatEditSdf objects to include. If null, any child
// SplatEditSdf instances are used.
sdfs?: SplatEditSdf[];
};
export class SplatEdit extends THREE.Object3D {
// ordering used to apply SplatEdit operations to Gsplats. This is implicitly
// increased with each new SplatEdit. Reassigning ordering can be used to
// reorder the operations.
ordering: number;
rgbaBlendMode: SplatEditRgbaBlendMode;
sdfSmooth: number;
softEdge: number;
invert: boolean;
// Optional list of explicit SDFs to including in this edit. If it is null, then
// any SplatEditSdf children in the scene graph will be added automatically.
sdfs: SplatEditSdf[] | null;
// The next ordering number to use for a new SplatEdit, auto-incremented
static nextOrdering = 1;
constructor(options: SplatEditOptions = {}) {
const {
name,
rgbaBlendMode = SplatEditRgbaBlendMode.MULTIPLY,
sdfSmooth = 0.0,
softEdge = 0.0,
invert = false,
sdfs = null,
} = options;
super();
this.rgbaBlendMode = rgbaBlendMode;
this.sdfSmooth = sdfSmooth;
this.softEdge = softEdge;
this.invert = invert;
this.sdfs = sdfs;
// Assign and auto-increment unique ordering number for this edit
this.ordering = SplatEdit.nextOrdering++;
// Automatically assign a default name if not provided
this.name = name ?? `Edit ${this.ordering}`;
}
addSdf(sdf: SplatEditSdf) {
if (this.sdfs == null) {
this.sdfs = [];
}
this.sdfs.push(sdf);
}
removeSdf(sdf: SplatEditSdf) {
if (this.sdfs == null) {
return;
}
this.sdfs = this.sdfs.filter((s) => s !== sdf);
}
}
// Dyno implementation of RGBA-XYZ SDF editing.
// The SDFs are encoded in a texture while the edits are encoded
// as a uniform uvec4 array.
export class SplatEdits {
// Maximum number of SDFs allocated
maxSdfs: number;
// Number of SDFs currently in use
numSdfs: number;
// Encoded SDF data
sdfData: Uint32Array;
// Float interpretation of SDF data
sdfFloatData: Float32Array;
// Texture with encoded SDF data
sdfTexture: THREE.DataTexture;
// An SdfArray dyno uniform
dynoSdfArray: DynoUniform<typeof SdfArray, "sdfArray">;
// Maximum number of edits allocated
maxEdits: number;
// Number of edits currently in use
numEdits: number;
// Encoded edit data
editData: Uint32Array;
// Float interpretation of edit data
editFloatData: Float32Array;
// A dyno uniform for the number of edits
dynoNumEdits: DynoUniform<"int", "numEdits">;
// A dyno uniform for the encoded edits, one uvec4 per edit
dynoEdits: DynoUniform<"uvec4", "edits">;
constructor({ maxSdfs, maxEdits }: { maxSdfs?: number; maxEdits?: number }) {
// Allocate at least 16 SDFs for efficiency
this.maxSdfs = Math.max(16, maxSdfs ?? 0);
this.numSdfs = 0;
// Allocate space: 8 x (u)vec4 values per SDF, Uint32 and Float32 arrays
this.sdfData = new Uint32Array(this.maxSdfs * 8 * 4);
this.sdfFloatData = new Float32Array(this.sdfData.buffer);
this.sdfTexture = this.newSdfTexture(this.sdfData, this.maxSdfs);
this.dynoSdfArray = new DynoUniform({
key: "sdfArray",
type: SdfArray,
globals: () => [defineSdfArray],
value: {
numSdfs: 0,
sdfTexture: this.sdfTexture,
},
update: (uniform) => {
uniform.numSdfs = this.numSdfs;
uniform.sdfTexture = this.sdfTexture;
return uniform;
},
});
// Allocate at least 16 edits slots for efficiency
this.maxEdits = Math.max(16, maxEdits ?? 0);
this.numEdits = 0;
// Allocate space: 1 uvec4 per edit
this.editData = new Uint32Array(this.maxEdits * 4);
this.editFloatData = new Float32Array(this.editData.buffer);
this.dynoNumEdits = new DynoInt({ value: 0 });
this.dynoEdits = this.newEdits(this.editData, this.maxEdits);
}
private newSdfTexture(data: Uint32Array, maxSdfs: number) {
const texture = new THREE.DataTexture(
data,
8,
maxSdfs,
THREE.RGBAIntegerFormat,
THREE.UnsignedIntType,
);
texture.internalFormat = "RGBA32UI";
texture.needsUpdate = true;
return texture;
}
private newEdits(data: Uint32Array, maxEdits: number) {
return new DynoUniform({
key: "edits",
type: "uvec4",
count: maxEdits,
globals: () => [defineEdit],
value: data,
});
}
// Ensure our SDF texture and edits uniform array have enough capacity.
// Reallocate if not.
private ensureCapacity({
maxSdfs,
maxEdits,
}: { maxSdfs: number; maxEdits: number }): boolean {
let dynoUpdated = false;
if (maxSdfs > this.sdfTexture.image.height) {
this.sdfTexture.dispose();
// At least double the size to avoid frequent reallocations
this.maxSdfs = Math.max(this.maxSdfs * 2, maxSdfs);
this.sdfData = new Uint32Array(this.maxSdfs * 8 * 4);
this.sdfFloatData = new Float32Array(this.sdfData.buffer);
this.sdfTexture = this.newSdfTexture(this.sdfData, this.maxSdfs);
}
if (maxEdits > (this.dynoEdits.count ?? 0)) {
// At least double the size to avoid frequent reallocations
this.maxEdits = Math.max(this.maxEdits * 2, maxEdits);
this.editData = new Uint32Array(this.maxEdits * 4);
this.editFloatData = new Float32Array(this.editData.buffer);
this.dynoEdits = this.newEdits(this.editData, this.maxEdits);
dynoUpdated = true;
}
return dynoUpdated;
}
private updateEditData(offset: number, value: number): boolean {
// Update an edit uint32 value and return true if it changed
const updated = this.editData[offset] !== value;
this.editData[offset] = value;
return updated;
}
private updateEditFloatData(offset: number, value: number): boolean {
// Update an edit float32 value and return true if it changed
tempFloat32[0] = value;
const updated = this.editFloatData[offset] !== tempFloat32[0];
if (updated) {
this.editFloatData[offset] = tempFloat32[0];
}
return updated;
}
private encodeEdit(
editIndex: number,
{
sdfFirst,
sdfCount,
invert,
rgbaBlendMode,
softEdge,
sdfSmooth,
}: {
sdfFirst: number;
sdfCount: number;
invert: boolean;
rgbaBlendMode: number;
softEdge: number;
sdfSmooth: number;
},
): boolean {
const base = editIndex * 4;
let updated = false;
// Encode the edit fields into the editData array and check if any changed
updated =
this.updateEditData(base + 0, rgbaBlendMode | (invert ? 1 << 8 : 0)) ||
updated;
updated =
this.updateEditData(base + 1, sdfFirst | (sdfCount << 16)) || updated;
updated = this.updateEditFloatData(base + 2, softEdge) || updated;
updated = this.updateEditFloatData(base + 3, sdfSmooth) || updated;
return updated;
}
private updateSdfData(offset: number, value: number): boolean {
// Update an SDF uint32 value and return true if it changed
const updated = this.sdfData[offset] !== value;
this.sdfData[offset] = value;
return updated;
}
private updateSdfFloatData(offset: number, value: number): boolean {
// Update an SDF float32 value and return true if it changed
tempFloat32[0] = value;
const updated = this.sdfFloatData[offset] !== tempFloat32[0];
if (updated) {
this.sdfFloatData[offset] = tempFloat32[0];
}
return updated;
}
private encodeSdf(
sdfIndex: number,
{
sdfType,
invert,
center,
quaternion,
scale,
sizes,
}: {
sdfType: number;
invert?: boolean;
center?: THREE.Vector3;
quaternion?: THREE.Quaternion;
scale?: THREE.Vector3;
sizes?: THREE.Vector4;
},
values: THREE.Vector4[],
): boolean {
// Encode the SDF fields into the sdfData array and check if any changed
const base = sdfIndex * (8 * 4);
const flags = sdfType | (invert ? 1 << 8 : 0);
let updated = false;
updated = this.updateSdfFloatData(base + 0, center?.x ?? 0) || updated;
updated = this.updateSdfFloatData(base + 1, center?.y ?? 0) || updated;
updated = this.updateSdfFloatData(base + 2, center?.z ?? 0) || updated;
updated = this.updateSdfData(base + 3, flags) || updated;
updated = this.updateSdfFloatData(base + 4, quaternion?.x ?? 0) || updated;
updated = this.updateSdfFloatData(base + 5, quaternion?.y ?? 0) || updated;
updated = this.updateSdfFloatData(base + 6, quaternion?.z ?? 0) || updated;
updated = this.updateSdfFloatData(base + 7, quaternion?.w ?? 0) || updated;
updated = this.updateSdfFloatData(base + 8, scale?.x ?? 0) || updated;
updated = this.updateSdfFloatData(base + 9, scale?.y ?? 0) || updated;
updated = this.updateSdfFloatData(base + 10, scale?.z ?? 0) || updated;
updated = this.updateSdfData(base + 11, 0) || updated;
updated = this.updateSdfFloatData(base + 12, sizes?.x ?? 0) || updated;
updated = this.updateSdfFloatData(base + 13, sizes?.y ?? 0) || updated;
updated = this.updateSdfFloatData(base + 14, sizes?.z ?? 0) || updated;
updated = this.updateSdfFloatData(base + 15, sizes?.w ?? 0) || updated;
const nValues = Math.min(4, values.length);
for (let i = 0; i < nValues; ++i) {
const vBase = base + 16 + i * 4;
updated = this.updateSdfFloatData(vBase + 0, values[i].x) || updated;
updated = this.updateSdfFloatData(vBase + 1, values[i].y) || updated;
updated = this.updateSdfFloatData(vBase + 2, values[i].z) || updated;
updated = this.updateSdfFloatData(vBase + 3, values[i].w) || updated;
}
return updated;
}
// Update the SDFs and edits from an array of SplatEdits and their
// associated SplatEditSdfs, updating it for the dyno shader program.
update(edits: { edit: SplatEdit; sdfs: SplatEditSdf[] }[]): {
updated: boolean;
dynoUpdated: boolean;
} {
const sdfCount = edits.reduce((total, { sdfs }) => total + sdfs.length, 0);
const dynoUpdated = this.ensureCapacity({
maxEdits: edits.length,
maxSdfs: sdfCount,
});
const values = [new THREE.Vector4(), new THREE.Vector4()];
const center = new THREE.Vector3();
const quaternion = new THREE.Quaternion();
const scale = new THREE.Vector3();
const sizes = new THREE.Vector4();
let sdfIndex = 0;
let updated = dynoUpdated;
if (edits.length !== this.dynoNumEdits.value) {
this.dynoNumEdits.value = edits.length;
this.numEdits = edits.length;
updated = true;
}
for (const [editIndex, { edit, sdfs }] of edits.entries()) {
updated =
this.encodeEdit(editIndex, {
sdfFirst: sdfIndex,
sdfCount: sdfs.length,
invert: edit.invert,
rgbaBlendMode: rgbaBlendModeToNumber(edit.rgbaBlendMode),
softEdge: edit.softEdge,
sdfSmooth: edit.sdfSmooth,
}) || updated;
let sdfUpdated = false;
for (const sdf of sdfs) {
sizes.set(sdf.scale.x, sdf.scale.y, sdf.scale.z, sdf.radius);
// Temporarily set the SDF scale to 1.0 to get the world-to-SDF
// transform without scaling. The SDF treats the scale separately.
sdf.scale.setScalar(1.0);
sdf.updateMatrixWorld();
const worldToSdf = sdf.matrixWorld.clone().invert();
worldToSdf.decompose(center, quaternion, scale);
sdf.scale.set(sizes.x, sizes.y, sizes.z);
sdf.updateMatrixWorld();
values[0].set(sdf.color.r, sdf.color.g, sdf.color.b, sdf.opacity);
values[1].set(sdf.displace.x, sdf.displace.y, sdf.displace.z, 1.0);
sdfUpdated =
this.encodeSdf(
sdfIndex,
{
sdfType: sdfTypeToNumber(sdf.type),
invert: sdf.invert,
center,
quaternion,
scale,
sizes,
},
values,
) || sdfUpdated;
sdfIndex += 1;
}
this.numSdfs = sdfIndex;
if (sdfUpdated) {
this.sdfTexture.needsUpdate = true;
}
updated ||= sdfUpdated;
}
return { updated, dynoUpdated };
}
// Modify a Gsplat in a dyno shader program using the current edits and SDFs.
modify(gsplat: DynoVal<typeof Gsplat>): DynoVal<typeof Gsplat> {
return applyGsplatRgbaDisplaceEdits(
gsplat,
this.dynoSdfArray,
this.dynoNumEdits,
this.dynoEdits,
);
}
}
// Dyno types and components:
// An SdfArray contains a collection of SDFs encoded in a texture.
// Each SDF has a type and geometric parameters, but also encodes
// 4 x vec4 values, which can all be blended across multiple SDFs.
// The SplatEdit system uses 7 of these 16 values to encode RGBA-XYZ edits,
// but more can be added, and these SDFs can be used for entirely different
// purposes as well.
export const SdfArray = { type: "SdfArray" } as { type: "SdfArray" };
export const defineSdfArray = unindent(`
struct SdfArray {
int numSdfs;
usampler2D sdfTexture;
};
void unpackSdfArray(
usampler2D sdfTexture, int sdfIndex, out uint flags,
out vec3 center, out vec4 quaternion, out vec3 scale, out vec4 sizes,
int numValues, out vec4 values[4]
) {
uvec4 temp = texelFetch(sdfTexture, ivec2(0, sdfIndex), 0);
flags = temp.w;
center = vec3(uintBitsToFloat(temp.x), uintBitsToFloat(temp.y), uintBitsToFloat(temp.z));
temp = texelFetch(sdfTexture, ivec2(1, sdfIndex), 0);
quaternion = vec4(uintBitsToFloat(temp.x), uintBitsToFloat(temp.y), uintBitsToFloat(temp.z), uintBitsToFloat(temp.w));
temp = texelFetch(sdfTexture, ivec2(2, sdfIndex), 0);
scale = vec3(uintBitsToFloat(temp.x), uintBitsToFloat(temp.y), uintBitsToFloat(temp.z));
temp = texelFetch(sdfTexture, ivec2(3, sdfIndex), 0);
sizes = vec4(uintBitsToFloat(temp.x), uintBitsToFloat(temp.y), uintBitsToFloat(temp.z), uintBitsToFloat(temp.w));
for (int i = 0; i < numValues; ++i) {
temp = texelFetch(sdfTexture, ivec2(4 + i, sdfIndex), 0);
values[i] = vec4(uintBitsToFloat(temp.x), uintBitsToFloat(temp.y), uintBitsToFloat(temp.z), uintBitsToFloat(temp.w));
}
}
const uint SDF_FLAG_TYPE = 0xFFu;
const uint SDF_FLAG_INVERT = 1u << 8u;
const uint SDF_TYPE_ALL = 0u;
const uint SDF_TYPE_PLANE = 1u;
const uint SDF_TYPE_SPHERE = 2u;
const uint SDF_TYPE_BOX = 3u;
const uint SDF_TYPE_ELLIPSOID = 4u;
const uint SDF_TYPE_CYLINDER = 5u;
const uint SDF_TYPE_CAPSULE = 6u;
const uint SDF_TYPE_INFINITE_CONE = 7u;
float evaluateSdfArray(
usampler2D sdfTexture, int numSdfs, int sdfFirst, int sdfCount, vec3 pos,
float smoothK, int numValues, out vec4 outValues[4]
) {
float distanceAccum = (smoothK == 0.0) ? 1.0 / 0.0 : 0.0;
float maxExp = -1.0 / 0.0;
for (int i = 0; i < numValues; ++i) {
outValues[i] = vec4(0.0);
}
uint flags;
vec3 center, scale;
vec4 quaternion, sizes;
vec4 values[4];
int sdfLast = min(sdfFirst + sdfCount, numSdfs);
for (int index = sdfFirst; index < sdfLast; ++index) {
unpackSdfArray(sdfTexture, index, flags, center, quaternion, scale, sizes, numValues, values);
uint sdfType = flags & SDF_FLAG_TYPE;
vec3 sdfPos = quatVec(quaternion, pos * scale) + center;
float distance;
switch (sdfType) {
case SDF_TYPE_ALL:
distance = -1.0 / 0.0;
break;
case SDF_TYPE_PLANE: {
distance = sdfPos.z;
break;
}
case SDF_TYPE_SPHERE: {
distance = length(sdfPos) - sizes.w;
break;
}
case SDF_TYPE_BOX: {
vec3 q = abs(sdfPos) - sizes.xyz + sizes.w;
distance = length(max(q, 0.0)) + min(max(q.x, max(q.y, q.z)), 0.0) - sizes.w;
break;
}
case SDF_TYPE_ELLIPSOID: {
vec3 sizes = sizes.xyz;
float k0 = length(sdfPos / sizes);
float k1 = length(sdfPos / dot(sizes, sizes));
distance = k0 * (k0 - 1.0) / k1;
break;
}
case SDF_TYPE_CYLINDER: {
vec2 d = abs(vec2(length(sdfPos.xz), sdfPos.y)) - sizes.wy;
distance = min(max(d.x, d.y), 0.0) + length(max(d, 0.0));
break;
}
case SDF_TYPE_CAPSULE: {
sdfPos.y -= clamp(sdfPos.y, -0.5 * sizes.y, 0.5 * sizes.y);
distance = length(sdfPos) - sizes.w;
break;
}
case SDF_TYPE_INFINITE_CONE: {
float angle = 0.25 * PI * sizes.w;
vec2 c = vec2(sin(angle), cos(angle));
vec2 q = vec2(length(sdfPos.xy), -sdfPos.z);
float d = length(q - c * max(dot(q, c), 0.0));
distance = d * (((q.x * c.y - q.y * c.x) < 0.0) ? -1.0 : 1.0);
break;
}
}
if ((flags & SDF_FLAG_INVERT) != 0u) {
distance = -distance;
}
if (smoothK == 0.0) {
if (distance < distanceAccum) {
distanceAccum = distance;
for (int i = 0; i < numValues; ++i) {
outValues[i] = values[i];
}
}
} else {
float scaledDistance = -distance / smoothK;
if (scaledDistance > maxExp) {
float scale = exp(maxExp - scaledDistance);
distanceAccum *= scale;
for (int i = 0; i < numValues; ++i) {
outValues[i] *= scale;
}
maxExp = scaledDistance;
}
float weight = exp(scaledDistance - maxExp);
distanceAccum += weight;
for (int i = 0; i < numValues; ++i) {
outValues[i] += weight * values[i];
}
}
}
if (smoothK == 0.0) {
return distanceAccum;
} else {
// Very distant SDFs may result in 0 accumulation
if (distanceAccum == 0.0) {
return 1.0 / 0.0;
}
for (int i = 0; i < numValues; ++i) {
outValues[i] /= distanceAccum;
}
return (-log(distanceAccum) - maxExp) * smoothK;
}
}
float modulateSdfArray(
usampler2D sdfTexture, int numSdfs, int sdfFirst, int sdfCount, vec3 pos,
float smoothK, int numValues, out vec4 values[4],
float softEdge, bool invert
) {
float distance = evaluateSdfArray(sdfTexture, numSdfs, sdfFirst, sdfCount, pos, smoothK, numValues, values);
if (invert) {
distance = -distance;
}
return (softEdge == 0.0) ? ((distance < 0.0) ? 1.0 : 0.0)
: clamp(-distance / softEdge + 0.5, 0.0, 1.0);
}
`);
export const defineEdit = unindent(`
const uint EDIT_FLAG_BLEND = 0xFFu;
const uint EDIT_BLEND_MULTIPLY = 0u;
const uint EDIT_BLEND_SET_RGB = 1u;
const uint EDIT_BLEND_ADD_RGBA = 2u;
const uint EDIT_FLAG_INVERT = 0x100u;
void decodeEdit(
uvec4 packedEdit, out int sdfFirst, out int sdfCount,
out bool invert, out uint rgbaBlendMode, out float softEdge, out float sdfSmooth
) {
rgbaBlendMode = packedEdit.x & EDIT_FLAG_BLEND;
invert = (packedEdit.x & EDIT_FLAG_INVERT) != 0u;
sdfFirst = int(packedEdit.y & 0xFFFFu);
sdfCount = int(packedEdit.y >> 16u);
softEdge = uintBitsToFloat(packedEdit.z);
sdfSmooth = uintBitsToFloat(packedEdit.w);
}
void applyRgbaDisplaceEdit(
usampler2D sdfTexture, int numSdfs, int sdfFirst, int sdfCount, inout vec3 pos,
float smoothK, float softEdge, bool invert, uint rgbaBlendMode, inout vec4 rgba
) {
vec4 values[4];
float modulate = modulateSdfArray(sdfTexture, numSdfs, sdfFirst, sdfCount, pos, smoothK, 2, values, softEdge, invert);
// On Android, moving values[0] is necessary to work around a compiler bug.
vec4 sdfRgba = values[0];
vec4 sdfDisplaceScale = values[1];
vec4 target;
switch (rgbaBlendMode) {
case EDIT_BLEND_MULTIPLY:
target = rgba * sdfRgba;
break;
case EDIT_BLEND_SET_RGB:
target = vec4(sdfRgba.rgb, rgba.a * sdfRgba.a);
break;
case EDIT_BLEND_ADD_RGBA:
target = rgba + sdfRgba;
break;
default:
// Debug output if blend mode not set
target = vec4(fract(pos), 1.0);
}
rgba = mix(rgba, target, modulate);
pos += sdfDisplaceScale.xyz * modulate;
}
void applyPackedRgbaDisplaceEdit(uvec4 packedEdit, usampler2D sdfTexture, int numSdfs, inout vec3 pos, inout vec4 rgba) {
int sdfFirst, sdfCount;
bool invert;
uint rgbaBlendMode;
float softEdge, sdfSmooth;
decodeEdit(packedEdit, sdfFirst, sdfCount, invert, rgbaBlendMode, softEdge, sdfSmooth);
applyRgbaDisplaceEdit(sdfTexture, numSdfs, sdfFirst, sdfCount, pos, sdfSmooth, softEdge, invert, rgbaBlendMode, rgba);
}
`);
function applyGsplatRgbaDisplaceEdits(
gsplat: DynoVal<typeof Gsplat>,
sdfArray: DynoVal<typeof SdfArray>,
numEdits: DynoVal<"int">,
rgbaDisplaceEdits: DynoVal<"uvec4">,
): DynoVal<typeof Gsplat> {
const dyno = new Dyno<
{
gsplat: typeof Gsplat;
sdfArray: typeof SdfArray;
numEdits: "int";
rgbaDisplaceEdits: "uvec4";
},
{ gsplat: typeof Gsplat }
>({
inTypes: {
gsplat: Gsplat,
sdfArray: SdfArray,
numEdits: "int",
rgbaDisplaceEdits: "uvec4",
},
outTypes: { gsplat: Gsplat },
globals: () => [defineSdfArray, defineEdit],
inputs: { gsplat, sdfArray, numEdits, rgbaDisplaceEdits },
statements: ({ inputs, outputs }) => {
const { sdfArray, numEdits, rgbaDisplaceEdits } = inputs;
const { gsplat } = outputs;
return unindentLines(`
${gsplat} = ${inputs.gsplat};
if (isGsplatActive(${gsplat}.flags)) {
for (int editIndex = 0; editIndex < ${numEdits}; ++editIndex) {
applyPackedRgbaDisplaceEdit(
${rgbaDisplaceEdits}[editIndex], ${sdfArray}.sdfTexture, ${sdfArray}.numSdfs,
${gsplat}.center, ${gsplat}.rgba
);
}
}
`);
},
});
return dyno.outputs.gsplat;
}
const tempFloat32 = new Float32Array(1);
+216
View File
@@ -0,0 +1,216 @@
import * as THREE from "three";
import type { SplatEdit } from "./SplatEdit";
import {
type Dyno,
DynoFloat,
type DynoVal,
DynoVec3,
DynoVec4,
Gsplat,
dynoBlock,
transformGsplat,
} from "./dyno";
// A GsplatGenerator is a dyno program that maps an index to a Gsplat's properties
export type GsplatGenerator = Dyno<{ index: "int" }, { gsplat: typeof Gsplat }>;
// A GsplatModifier is a dyno program that inputs a Gsplat, modifies, and outputs it
export type GsplatModifier = Dyno<
{ gsplat: typeof Gsplat },
{ gsplat: typeof Gsplat }
>;
// A SplatModifier is a utility class to apply a GsplatModifier to
// a GsplatGenerator pipeline, caching the combined result for efficiency.
export class SplatModifier {
modifier: GsplatModifier;
cache: Map<GsplatGenerator, GsplatGenerator>;
constructor(modifier: GsplatModifier) {
this.modifier = modifier;
this.cache = new Map();
}
apply(generator: GsplatGenerator): GsplatGenerator {
let modified = this.cache.get(generator);
if (!modified) {
modified = dynoBlock(
{ index: "int" },
{ gsplat: Gsplat },
({ index }) => {
const { gsplat } = generator.apply({ index });
return this.modifier.apply({ gsplat });
},
);
this.cache.set(generator, modified);
}
return modified;
}
}
// A SplatTransformer is a utility class to apply a transform to a Gsplat
// via a scale, rotation, and translation. Scale is a single float because
// anisotropic scaling of Gsplats is not supported.
export class SplatTransformer {
scale: DynoFloat;
rotate: DynoVec4<THREE.Quaternion>;
translate: DynoVec3<THREE.Vector3>;
// Create the dyno uniforms that parameterize the transform, setting them
// to initial values that are different from any valid transform.
constructor() {
this.scale = new DynoFloat({ value: Number.NEGATIVE_INFINITY });
this.rotate = new DynoVec4({
value: new THREE.Quaternion(
Number.POSITIVE_INFINITY,
Number.POSITIVE_INFINITY,
Number.POSITIVE_INFINITY,
Number.POSITIVE_INFINITY,
),
});
this.translate = new DynoVec3({
value: new THREE.Vector3(
Number.POSITIVE_INFINITY,
Number.POSITIVE_INFINITY,
Number.POSITIVE_INFINITY,
),
});
}
// Apply the transform to a Gsplat in a dyno program.
modify(gsplat: DynoVal<typeof Gsplat>): DynoVal<typeof Gsplat> {
return transformGsplat(gsplat, {
scale: this.scale,
rotate: this.rotate,
translate: this.translate,
});
}
// Update the uniforms to match the given transform matrix.
updateFromMatrix(transform: THREE.Matrix4) {
const scale = new THREE.Vector3();
const quaternion = new THREE.Quaternion();
const position = new THREE.Vector3();
transform.decompose(position, quaternion, scale);
const newScale = (scale.x + scale.y + scale.z) / 3;
let updated = false;
if (newScale !== this.scale.value) {
this.scale.value = newScale;
updated = true;
}
if (!position.equals(this.translate.value)) {
this.translate.value.copy(position);
updated = true;
}
if (!quaternion.equals(this.rotate.value)) {
this.rotate.value.copy(quaternion);
updated = true;
}
return updated;
}
// Update this transform to match the object's to-world transform.
update(object: THREE.Object3D): boolean {
object.updateMatrixWorld();
return this.updateFromMatrix(object.matrixWorld);
}
}
// SplatGenerator is an Object3D that can be placed anywhere in the scene
// to generate Gsplats into the world for ForgeRenderer. All Gsplats from
// SplatGenerators across the scene will be accumulated into a single
// SplatAccumulator, which are sorted and rendered together.
//
// Each SplatGenerator has two main properties:
// - numSplats: the number of Gsplats to generate
// - generator: a GsplatGenerator dyno program that maps a splat index
// to a Gsplat's properties
// Each of these properties can be changed at anytime, however changing
// numSplats means we no longer have a correspondence between Gsplats
// in successive frames, meaning we can't reuse the previous Gsplat sort
// order. Similarly, changing the generator requires re-generating the
// shader program, which will trigger a GPU shader compilation the first
// time (possibly a perceptible "hickup" in the framerate) but is cached
// subsequence times if the generator is the same as one that was used previously.
//
// A SplatGenerator also has a custom frameUpdate function that is called
// on each execution, allowing you to update uniforms or other parameters that
// affect the generation. If the Gsplats are changed, you must call
// updateVersion() (alternatively, set needsUpdate to true) to trigger a
// re-generation of the Gsplats for this SplatGenerator.
export class SplatGenerator extends THREE.Object3D {
numSplats: number;
generator?: GsplatGenerator;
generatorError?: unknown;
frameUpdate?: ({
object,
time,
deltaTime,
viewToWorld,
globalEdits,
}: {
object: SplatGenerator;
time: number;
deltaTime: number;
viewToWorld: THREE.Matrix4;
globalEdits: SplatEdit[];
}) => void;
version: number;
constructor({
numSplats,
generator,
construct,
update,
}: {
numSplats?: number;
generator?: GsplatGenerator;
construct?: (object: SplatGenerator) => {
generator?: GsplatGenerator;
numSplats?: number;
frameUpdate?: (object: SplatGenerator) => void;
};
update?: ({
object,
time,
deltaTime,
viewToWorld,
globalEdits,
}: {
object: SplatGenerator;
time: number;
deltaTime: number;
viewToWorld: THREE.Matrix4;
globalEdits: SplatEdit[];
}) => void;
}) {
super();
this.numSplats = numSplats ?? 0;
this.generator = generator;
this.frameUpdate = update;
this.version = 0;
if (construct) {
const constructed = construct(this);
// If we returned something, update our properties
Object.assign(this, constructed);
}
}
updateVersion() {
this.version += 1;
}
set needsUpdate(value: boolean) {
if (value) {
this.updateVersion();
}
}
}
+44
View File
@@ -0,0 +1,44 @@
import * as THREE from "three";
// SplatGeometry is an internal class used by ForgeRenderer to render a collection
// of Gsplats in a single draw call by extending THREE.InstancedBufferGeometry.
// Each Gsplat is drawn as two triangles, with the order of the Gsplats determined
// by the instance attribute "ordering".
export class SplatGeometry extends THREE.InstancedBufferGeometry {
ordering: Uint32Array;
attribute: THREE.InstancedBufferAttribute;
constructor(ordering: Uint32Array, activeSplats: number) {
super();
this.ordering = ordering;
this.setAttribute("position", new THREE.BufferAttribute(QUAD_VERTICES, 3));
this.setIndex(new THREE.BufferAttribute(QUAD_INDICES, 1));
// Hack to work around Three.js
// @ts-ignore
this._maxInstanceCount = ordering.length;
this.instanceCount = activeSplats;
this.attribute = new THREE.InstancedBufferAttribute(ordering, 1, false, 1);
this.attribute.setUsage(THREE.DynamicDrawUsage);
this.setAttribute("splatIndex", this.attribute);
}
update(ordering: Uint32Array, activeSplats: number) {
this.ordering = ordering;
this.attribute.array = ordering;
this.instanceCount = activeSplats;
this.attribute.addUpdateRange(0, activeSplats);
this.attribute.needsUpdate = true;
}
}
// Each instance draws to triangles covering a quad over coords (-1,-1,0)..(1,1,0)
const QUAD_VERTICES = new Float32Array([
-1, -1, 0, 1, -1, 0, 1, 1, 0, -1, 1, 0,
]);
const QUAD_INDICES = new Uint16Array([0, 1, 2, 0, 2, 3]);
+218
View File
@@ -0,0 +1,218 @@
import { FileLoader, Loader, type LoadingManager } from "three";
import { PackedSplats } from "./PackedSplats";
import { SplatMesh } from "./SplatMesh";
import { PlyReader } from "./ply";
import { withWorker } from "./splatWorker";
import { decompressPartialGzip, getTextureSize } from "./utils";
// SplatLoader implements the THREE.Loader interface and supports loading a variety
// of differeng Gsplat file formats. Formats .PLY and .SPZ can be auto-detected
// from the file contents, while .SPLAT and .KSPLAT require either having the
// appropriate file extension as part of the path, or it can be explicitly set
// in the loader using the fileType property.
export class SplatLoader extends Loader {
fileLoader: FileLoader;
fileType?: SplatFileType;
constructor(manager?: LoadingManager) {
super(manager);
this.fileLoader = new FileLoader(manager);
}
load(
url: string,
onLoad?: (decoded: PackedSplats) => void,
onProgress?: (event: ProgressEvent) => void,
onError?: (error: unknown) => void,
) {
this.fileLoader.setResponseType("arraybuffer");
this.fileLoader.setCrossOrigin(this.crossOrigin);
this.fileLoader.setWithCredentials(this.withCredentials);
this.fileLoader.setPath(this.path);
this.fileLoader.setResourcePath(this.resourcePath);
this.fileLoader.setRequestHeader(this.requestHeader);
this.fileLoader.load(
url,
async (response) => {
if (onLoad) {
const input = response as ArrayBuffer;
const decoded = await unpackSplats({
input,
fileType: this.fileType,
pathOrUrl: url,
});
onLoad(new PackedSplats(decoded));
}
},
onProgress,
onError,
);
}
async loadAsync(
url: string,
onProgress?: (event: ProgressEvent) => void,
): Promise<PackedSplats> {
return new Promise((resolve, reject) => {
this.load(
url,
(decoded) => {
resolve(decoded);
},
onProgress,
reject,
);
});
}
parse(packedSplats: PackedSplats): SplatMesh {
return new SplatMesh({ packedSplats });
}
}
export enum SplatFileType {
PLY = "ply",
WLG0 = "wlg0",
SPZ = "spz",
SPLAT = "splat",
KSPLAT = "ksplat",
}
export function getSplatFileType(
fileBytes: Uint8Array,
): SplatFileType | undefined {
const view = new DataView(fileBytes.buffer);
if ((view.getUint32(0, true) & 0x00ffffff) === 0x00796c70) {
return SplatFileType.PLY;
}
if (view.getUint32(0, true) === 0x30474c57) {
return SplatFileType.WLG0;
}
if ((view.getUint32(0, true) & 0x00ffffff) === 0x00088b1f) {
// Gzipped file, unpack beginning to check magic number
const header = decompressPartialGzip(fileBytes, 4);
const gView = new DataView(header.buffer);
if (gView.getUint32(0, true) === 0x5053474e) {
return SplatFileType.SPZ;
}
// Unknown Gzipped file type
return undefined;
}
// Unknown file type
return undefined;
}
// Returns the lowercased file extension from a path or URL
export function getFileExtension(pathOrUrl: string): string {
const noTrailing = pathOrUrl.split(/[?#]/, 1)[0];
const lastSlash = Math.max(
noTrailing.lastIndexOf("/"),
noTrailing.lastIndexOf("\\"),
);
const filename = noTrailing.slice(lastSlash + 1);
const lastDot = filename.lastIndexOf(".");
if (lastDot <= 0 || lastDot === filename.length - 1) {
return ""; // No extension
}
return filename.slice(lastDot + 1).toLowerCase();
}
export async function unpackSplats({
input,
fileType,
pathOrUrl,
}: {
input: Uint8Array | ArrayBuffer;
fileType?: SplatFileType;
pathOrUrl?: string;
}): Promise<{
packedArray: Uint32Array;
numSplats: number;
extra?: Record<string, unknown>;
}> {
const fileBytes =
input instanceof ArrayBuffer ? new Uint8Array(input) : input;
let splatFileType = fileType;
if (!fileType) {
splatFileType = getSplatFileType(fileBytes);
if (!splatFileType && pathOrUrl) {
const extension = getFileExtension(pathOrUrl);
if (extension === "ply") {
splatFileType = SplatFileType.PLY;
} else if (extension === "wlg") {
splatFileType = SplatFileType.WLG0;
} else if (extension === "spz") {
splatFileType = SplatFileType.SPZ;
} else if (extension === "splat") {
splatFileType = SplatFileType.SPLAT;
} else if (extension === "ksplat") {
splatFileType = SplatFileType.KSPLAT;
}
}
}
if (splatFileType === SplatFileType.WLG0) {
return await withWorker(async (worker) => {
const { packedArray, numSplats } = (await worker.call("decodeWlg", {
fileBytes,
})) as { packedArray: Uint32Array; numSplats: number };
return { packedArray, numSplats };
});
}
if (splatFileType === SplatFileType.PLY) {
const ply = new PlyReader({ fileBytes });
await ply.parseHeader();
const numSplats = ply.numSplats;
const maxSplats = getTextureSize(numSplats).maxSplats;
const args = { fileBytes, packedArray: new Uint32Array(maxSplats * 4) };
return await withWorker(async (worker) => {
const { packedArray, numSplats, extra } = (await worker.call(
"unpackPly",
args,
)) as {
packedArray: Uint32Array;
numSplats: number;
extra: Record<string, unknown>;
};
return { packedArray, numSplats, extra };
});
}
if (splatFileType === SplatFileType.SPZ) {
return await withWorker(async (worker) => {
const { packedArray, numSplats, extra } = (await worker.call(
"decodeSpz",
{
fileBytes,
},
)) as {
packedArray: Uint32Array;
numSplats: number;
extra: Record<string, unknown>;
};
return { packedArray, numSplats, extra };
});
}
if (splatFileType === SplatFileType.SPLAT) {
return await withWorker(async (worker) => {
const { packedArray, numSplats } = (await worker.call("decodeAntiSplat", {
fileBytes,
})) as { packedArray: Uint32Array; numSplats: number };
return { packedArray, numSplats };
});
}
if (splatFileType === SplatFileType.KSPLAT) {
return await withWorker(async (worker) => {
const { packedArray, numSplats, extra } = (await worker.call(
"decodeKsplat",
{ fileBytes },
)) as {
packedArray: Uint32Array;
numSplats: number;
extra: Record<string, unknown>;
};
return { packedArray, numSplats, extra };
});
}
throw new Error("Unknown splat file type");
}
+842
View File
@@ -0,0 +1,842 @@
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;
// 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, maxSplats, constructSplats } = options;
if (url || fileBytes || constructSplats) {
const packedSplatsOptions = {
url,
fileBytes,
fileType,
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({ transform, viewToObject, recolor }: SplatMeshContext) {
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.modify(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;
}
+298
View File
@@ -0,0 +1,298 @@
import * as THREE from "three";
// SplatSkinning is an experimental class that implements dual-quaternion
// skeletal animation for Gsplats. A skeletal animation system consists
// of a set of bones, each with a "rest" pose that consists of a position
// and orientation, and a weighting of up to 4 bones for each Gsplat.
// By moving and rotating the bones you can animate all the Gsplats like
// your would for a normal 3D animated mesh.
// Note that the dual-quaternion formulation assumes that mass/volume
// is conserved through these transformations, which helps avoid common
// issues with linear blend skinning such as joint collapse or bulging.
// However, it is not as good a fit for animations that involve explicit
// deformations, such as cartoon animations.
import type { SplatMesh } from "./SplatMesh";
import {
Dyno,
DynoUniform,
type DynoVal,
Gsplat,
unindent,
unindentLines,
} from "./dyno";
import { getTextureSize } from "./utils";
export type SplatSkinningOptions = {
// Specifies the SplatMesh that will be animated.
mesh: SplatMesh;
// Overrides the number of Gsplats in the mesh that will be animated.
// (default: mesh.numSplats)
numSplats?: number;
// Set the number of bones used to animate the SplatMesh, with a maximum
// of 256 (in order to compactly encode the bone index). (default: 256)
numBones?: number;
};
export class SplatSkinning {
mesh: SplatMesh;
numSplats: number;
// Store the skinning weights for each Gsplat, composed of a 4-vector
// of bone indices and weight
skinData: Uint16Array;
skinTexture: THREE.DataArrayTexture;
numBones: number;
boneData: Float32Array;
boneTexture: THREE.DataTexture;
uniform: DynoUniform<typeof GsplatSkinning, "skinning">;
constructor(options: SplatSkinningOptions) {
this.mesh = options.mesh;
this.numSplats = options.numSplats ?? this.mesh.numSplats;
const { width, height, depth, maxSplats } = getTextureSize(this.numSplats);
this.skinData = new Uint16Array(maxSplats * 4);
this.skinTexture = new THREE.DataArrayTexture(
this.skinData,
width,
height,
depth,
);
this.skinTexture.format = THREE.RGBAIntegerFormat;
this.skinTexture.type = THREE.UnsignedShortType;
this.skinTexture.internalFormat = "RGBA16UI";
this.skinTexture.needsUpdate = true;
this.numBones = options.numBones ?? 256;
this.boneData = new Float32Array(this.numBones * 16);
this.boneTexture = new THREE.DataTexture(
this.boneData,
4,
this.numBones,
THREE.RGBAFormat,
THREE.FloatType,
);
this.boneTexture.internalFormat = "RGBA32F";
this.boneTexture.needsUpdate = true;
this.uniform = new DynoUniform({
key: "skinning",
type: GsplatSkinning,
globals: () => [defineGsplatSkinning],
value: {
numSplats: this.numSplats,
numBones: this.numBones,
skinTexture: this.skinTexture,
boneTexture: this.boneTexture,
},
});
}
// Apply the skeletal animation to a Gsplat in a dyno program.
modify(gsplat: DynoVal<typeof Gsplat>): DynoVal<typeof Gsplat> {
return applyGsplatSkinning(gsplat, this.uniform);
}
// Set the "rest" pose for a bone with position and quaternion orientation.
setRestQuatPos(
boneIndex: number,
quat: THREE.Quaternion,
pos: THREE.Vector3,
) {
const i16 = boneIndex * 16;
this.boneData[i16 + 0] = quat.x;
this.boneData[i16 + 1] = quat.y;
this.boneData[i16 + 2] = quat.z;
this.boneData[i16 + 3] = quat.w;
this.boneData[i16 + 4] = pos.x;
this.boneData[i16 + 5] = pos.y;
this.boneData[i16 + 6] = pos.z;
this.boneData[i16 + 7] = 0;
this.boneData[i16 + 8] = 0;
this.boneData[i16 + 9] = 0;
this.boneData[i16 + 10] = 0;
this.boneData[i16 + 11] = 1;
this.boneData[i16 + 12] = 0;
this.boneData[i16 + 13] = 0;
this.boneData[i16 + 14] = 0;
this.boneData[i16 + 15] = 0;
}
// Set the "current" position and orientation of a bone.
setBoneQuatPos(
boneIndex: number,
quat: THREE.Quaternion,
pos: THREE.Vector3,
) {
const i16 = boneIndex * 16;
const origQuat = new THREE.Quaternion(
this.boneData[i16 + 0],
this.boneData[i16 + 1],
this.boneData[i16 + 2],
this.boneData[i16 + 3],
);
const origPos = new THREE.Vector3(
this.boneData[i16 + 4],
this.boneData[i16 + 5],
this.boneData[i16 + 6],
);
const relQuat = origQuat.clone().invert();
const relPos = pos.clone().sub(origPos);
relPos.applyQuaternion(relQuat);
relQuat.multiply(quat);
const dual = new THREE.Quaternion(
relPos.x,
relPos.y,
relPos.z,
0.0,
).multiply(origQuat);
this.boneData[i16 + 8] = relQuat.x;
this.boneData[i16 + 9] = relQuat.y;
this.boneData[i16 + 10] = relQuat.z;
this.boneData[i16 + 11] = relQuat.w;
this.boneData[i16 + 12] = 0.5 * dual.x;
this.boneData[i16 + 13] = 0.5 * dual.y;
this.boneData[i16 + 14] = 0.5 * dual.z;
this.boneData[i16 + 15] = 0.5 * dual.w;
}
// Set up to 4 bone indices and weights for a Gsplat. For fewer than 4 bones,
// you can set the remaining weights to 0 (and index=0).
setSplatBones(
splatIndex: number,
boneIndices: THREE.Vector4,
weights: THREE.Vector4,
) {
const i4 = splatIndex * 4;
this.skinData[i4 + 0] =
Math.min(255, Math.max(0, Math.round(weights.x * 255.0))) +
(boneIndices.x << 8);
this.skinData[i4 + 1] =
Math.min(255, Math.max(0, Math.round(weights.y * 255.0))) +
(boneIndices.y << 8);
this.skinData[i4 + 2] =
Math.min(255, Math.max(0, Math.round(weights.z * 255.0))) +
(boneIndices.z << 8);
this.skinData[i4 + 3] =
Math.min(255, Math.max(0, Math.round(weights.w * 255.0))) +
(boneIndices.w << 8);
}
// Call this to indicate that the bones have changed and the Gsplats need to be
// re-generated with updated skinning.
updateBones() {
this.boneTexture.needsUpdate = true;
this.mesh.needsUpdate = true;
}
}
// dyno program definitions for SplatSkinning
export const GsplatSkinning = { type: "GsplatSkinning" } as {
type: "GsplatSkinning";
};
export const defineGsplatSkinning = unindent(`
struct GsplatSkinning {
int numSplats;
int numBones;
usampler2DArray skinTexture;
sampler2D boneTexture;
};
`);
export const defineApplyGsplatSkinning = unindent(`
void applyGsplatSkinning(
int numSplats, int numBones,
usampler2DArray skinTexture, sampler2D boneTexture,
int splatIndex, inout vec3 center, inout vec4 quaternion
) {
if ((splatIndex < 0) || (splatIndex >= numSplats)) {
return;
}
uvec4 skinData = texelFetch(skinTexture, splatTexCoord(splatIndex), 0);
float weights[4];
weights[0] = float(skinData.x & 0xffu) / 255.0;
weights[1] = float(skinData.y & 0xffu) / 255.0;
weights[2] = float(skinData.z & 0xffu) / 255.0;
weights[3] = float(skinData.w & 0xffu) / 255.0;
uint boneIndices[4];
boneIndices[0] = (skinData.x >> 8u) & 0xffu;
boneIndices[1] = (skinData.y >> 8u) & 0xffu;
boneIndices[2] = (skinData.z >> 8u) & 0xffu;
boneIndices[3] = (skinData.w >> 8u) & 0xffu;
vec4 quat = vec4(0.0);
vec4 dual = vec4(0.0);
for (int i = 0; i < 4; i++) {
if (weights[i] > 0.0) {
int boneIndex = int(boneIndices[i]);
vec4 boneQuat = vec4(0.0, 0.0, 0.0, 1.0);
vec4 boneDual = vec4(0.0);
if (boneIndex < numBones) {
boneQuat = texelFetch(boneTexture, ivec2(2, boneIndex), 0);
boneDual = texelFetch(boneTexture, ivec2(3, boneIndex), 0);
}
if ((i > 0) && (dot(quat, boneQuat) < 0.0)) {
// Flip sign if next blend is pointing in the opposite direction
boneQuat = -boneQuat;
boneDual = -boneDual;
}
quat += weights[i] * boneQuat;
dual += weights[i] * boneDual;
}
}
// Normalize dual quaternion
float norm = length(quat);
quat /= norm;
dual /= norm;
vec3 translate = vec3(
2.0 * (-dual.w * quat.x + dual.x * quat.w - dual.y * quat.z + dual.z * quat.y),
2.0 * (-dual.w * quat.y + dual.x * quat.z + dual.y * quat.w - dual.z * quat.x),
2.0 * (-dual.w * quat.z - dual.x * quat.y + dual.y * quat.x + dual.z * quat.w)
);
center = quatVec(quat, center) + translate;
quaternion = quatQuat(quat, quaternion);
}
`);
function applyGsplatSkinning(
gsplat: DynoVal<typeof Gsplat>,
skinning: DynoVal<typeof GsplatSkinning>,
): DynoVal<typeof Gsplat> {
const dyno = new Dyno<
{ gsplat: typeof Gsplat; skinning: typeof GsplatSkinning },
{ gsplat: typeof Gsplat }
>({
inTypes: { gsplat: Gsplat, skinning: GsplatSkinning },
outTypes: { gsplat: Gsplat },
globals: () => [defineGsplatSkinning, defineApplyGsplatSkinning],
inputs: { gsplat, skinning },
statements: ({ inputs, outputs }) => {
const { skinning } = inputs;
const { gsplat } = outputs;
return unindentLines(`
${gsplat} = ${inputs.gsplat};
if (isGsplatActive(${gsplat}.flags)) {
applyGsplatSkinning(
${skinning}.numSplats, ${skinning}.numBones,
${skinning}.skinTexture, ${skinning}.boneTexture,
${gsplat}.index, ${gsplat}.center, ${gsplat}.quaternion
);
}
`);
},
});
return dyno.outputs.gsplat;
}
+682
View File
@@ -0,0 +1,682 @@
import * as THREE from "three";
// Forge controls for keyboard + mouse, game pad, or mobile multi-touch
const DEFAULT_MOVEMENT_SPEED = 1.0;
const DEFAULT_ROLL_SPEED = 2.0;
const DEFAULT_ROTATE_SPEED = 0.002;
const DEFAULT_SLIDE_SPEED = 0.006;
const DEFAULT_SCROLL_SPEED = 0.0015;
const DEFAULT_ROLL_SPRING = 0.0;
const DEFAULT_ROTATE_INERTIA = 0.15;
const DEFAULT_MOVE_INERTIA = 0.15;
const DEFAULT_STICK_THRESHOLD = 0.1;
const DEFAULT_FPS_ROTATE_SPEED = 2.0;
// Time limit for double-finger press (pinch etc)
const DUAL_PRESS_MS = 200;
// Time limit for double-click/double-tap
const DOUBLE_PRESS_LIMIT_MS = 400;
// Distance limit for double-click
const DOUBLE_PRESS_DISTANCE = 50;
// Standard WASD movement keys with R+F for up/down
const WASD_KEYCODE_MOVE = {
KeyW: new THREE.Vector3(0, 0, -1),
KeyS: new THREE.Vector3(0, 0, 1),
KeyA: new THREE.Vector3(-1, 0, 0),
KeyD: new THREE.Vector3(1, 0, 0),
KeyR: new THREE.Vector3(0, 1, 0),
KeyF: new THREE.Vector3(0, -1, 0),
};
// Arrow key movement with PageUp/PageDown
const ARROW_KEYCODE_MOVE = {
ArrowUp: new THREE.Vector3(0, 0, -1),
ArrowDown: new THREE.Vector3(0, 0, 1),
ArrowLeft: new THREE.Vector3(-1, 0, 0),
ArrowRight: new THREE.Vector3(1, 0, 0),
PageUp: new THREE.Vector3(0, 1, 0),
PageDown: new THREE.Vector3(0, -1, 0),
};
// Rolling with Q/E
const QE_KEYCODE_ROTATE = {
KeyQ: new THREE.Vector3(0, 0, 1),
KeyE: new THREE.Vector3(0, 0, -1),
};
// Home/End/Insert/Delete for rotation
const ARROW_KEYCODE_ROTATE = {
Home: new THREE.Vector3(0, -1, 0),
End: new THREE.Vector3(0, 1, 0),
Insert: new THREE.Vector3(-1, 0, 0),
Delete: new THREE.Vector3(1, 0, 0),
};
// ForgeControls provides simple, intuitive controls for navigating 3D space that
// use the keyboard + mouse, game pad, or mobile multi-touch. Internally it
// instantiates and updates a `FpsMovement` and `PointerControls` instance.
export class ForgeControls {
fpsMovement: FpsMovement;
pointerControls: PointerControls;
lastTime = 0;
constructor({ canvas }: { canvas: HTMLCanvasElement }) {
this.fpsMovement = new FpsMovement({});
this.pointerControls = new PointerControls({ canvas });
}
update(control: THREE.Object3D) {
const time = performance.now();
const deltaTime = (time - (this.lastTime || time)) / 1000;
this.lastTime = time;
this.fpsMovement.update(deltaTime, control);
this.pointerControls.update(deltaTime, control);
}
}
// FpsMovement implements controls that will be familiar to anyone who plays
// First Person Shooters using keyboard + mouse or a gamepad. Creating a FpsMovement
// instance provides many parameters for configuring the controls.
//
// When gamepads are connected, FpsMovement will always use gamepad index 0
// for twin-stick movement and rotation.
//
// If xr is passed in, the WebXR controllers can be used as a split gamepad
// to control movement and rotation. (tested on Quest 3)
export class FpsMovement {
moveSpeed: number;
rollSpeed: number;
stickThreshold: number;
rotateSpeed: number;
keycodeMoveMapping: { [key: string]: THREE.Vector3 };
keycodeRotateMapping: { [key: string]: THREE.Vector3 };
gamepadMapping: {
[button: number]: "shift" | "ctrl" | "rollLeft" | "rollRight";
};
capsMultiplier: number;
shiftMultiplier: number;
ctrlMultiplier: number;
xr?: THREE.WebXRManager;
// Enable/disable controls updates
enable = true;
// Currently active event.key values
keydown: { [key: string]: boolean };
// Currently active event.code values
keycode: { [key: string]: boolean };
constructor({
moveSpeed,
rollSpeed,
stickThreshold,
rotateSpeed,
keycodeMoveMapping,
keycodeRotateMapping,
gamepadMapping,
capsMultiplier,
shiftMultiplier,
ctrlMultiplier,
xr,
}: {
// Base movement speed (default DEFAULT_MOVEMENT_SPEED)
moveSpeed?: number;
// Base roll speed (default DEFAULT_ROLL_SPEED)
rollSpeed?: number;
// Stick threshold (default DEFAULT_STICK_THRESHOLD)
stickThreshold?: number;
// Speed of rotation when using gamepad or keys (default DEFAULT_FPS_ROTATE_SPEED)
rotateSpeed?: number;
// Maps keyboard keys to movement directions
// (default {...WASD_KEYCODE_MOVE, ...ARROW_KEYCODE_MOVE})
keycodeMoveMapping?: { [key: string]: THREE.Vector3 };
// Maps keyboard keys to rotation directions
// (default {...QE_KEYCODE_ROTATE, ...ARROW_KEYCODE_ROTATE})
keycodeRotateMapping?: { [key: string]: THREE.Vector3 };
// Maps gamepad buttons to control actions
// (default {4: "rollLeft", 5: "rollRight", 6: "ctrl", 7: "shift"})
gamepadMapping?: {
[button: number]: "shift" | "ctrl" | "rollLeft" | "rollRight";
};
// Speed multiplier when Caps Lock is active (default: 10)
capsMultiplier?: number;
// Speed multiplier when Shift is active (default: 5)
shiftMultiplier?: number;
// Speed multiplier when Ctrl is active (default: 1/5)
ctrlMultiplier?: number;
// Optional WebXR manager for XR controller stick support
xr?: THREE.WebXRManager;
} = {}) {
this.moveSpeed = moveSpeed ?? DEFAULT_MOVEMENT_SPEED;
this.rollSpeed = rollSpeed ?? DEFAULT_ROLL_SPEED;
this.stickThreshold = stickThreshold ?? DEFAULT_STICK_THRESHOLD;
this.rotateSpeed = rotateSpeed ?? DEFAULT_FPS_ROTATE_SPEED;
this.keycodeMoveMapping = keycodeMoveMapping ?? {
...WASD_KEYCODE_MOVE,
...ARROW_KEYCODE_MOVE,
};
this.keycodeRotateMapping = keycodeRotateMapping ?? {
...QE_KEYCODE_ROTATE,
...ARROW_KEYCODE_ROTATE,
};
this.gamepadMapping = gamepadMapping ?? {
4: "rollLeft",
5: "rollRight",
6: "ctrl",
7: "shift",
};
this.capsMultiplier = capsMultiplier ?? 10.0;
this.shiftMultiplier = shiftMultiplier ?? 5.0;
this.ctrlMultiplier = ctrlMultiplier ?? 1.0 / 5.0;
this.xr = xr;
this.keydown = {};
this.keycode = {};
document.addEventListener("keydown", (event) => {
this.keydown[event.key] = true;
this.keycode[event.code] = true;
});
document.addEventListener("keyup", (event) => {
this.keydown[event.key] = false;
this.keycode[event.code] = false;
});
}
// Call this method in your render loop with `control` set to the object to control
// (`THREE.Camera` or a `THREE.Object3D` that contains it), with `deltaTime`
// in seconds since the last update.
update(deltaTime: number, control: THREE.Object3D) {
if (!this.enable) {
return;
}
// Update gamepad / XR controllers
const sticks = [new THREE.Vector2(), new THREE.Vector2()];
const gamepad = navigator.getGamepads()[0];
if (gamepad) {
sticks[0].set(gamepad.axes[0], gamepad.axes[1]);
sticks[1].set(gamepad.axes[2], gamepad.axes[3]);
}
const gamepadButtons =
gamepad?.buttons.map((button) => button.pressed) || [];
const xrSources = Array.from(this.xr?.getSession()?.inputSources ?? []);
for (const source of xrSources) {
const gamepad = source.gamepad;
if (gamepad) {
switch (source.handedness) {
case "none": {
sticks[0].x += gamepad.axes[0];
sticks[0].y += gamepad.axes[1];
sticks[1].x += gamepad.axes[2];
sticks[1].y += gamepad.axes[3];
break;
}
case "left": {
sticks[0].x += gamepad.axes[2];
sticks[0].y += gamepad.axes[3];
break;
}
case "right": {
sticks[1].x += gamepad.axes[2];
sticks[1].y += gamepad.axes[3];
break;
}
}
}
}
for (const stick of sticks) {
stick.x = Math.abs(stick.x) >= this.stickThreshold ? stick.x : 0;
stick.y = Math.abs(stick.y) >= this.stickThreshold ? stick.y : 0;
}
// Rotation
const rotate = new THREE.Vector3(
sticks[1].x,
sticks[1].y,
0,
).multiplyScalar(this.rotateSpeed);
for (const [keycode, rot] of Object.entries(this.keycodeRotateMapping)) {
if (this.keycode[keycode]) {
rotate.add(rot);
}
}
for (const button in this.gamepadMapping) {
if (gamepadButtons[Number.parseInt(button)]) {
switch (this.gamepadMapping[button]) {
case "rollLeft":
rotate.z += 1;
break;
case "rollRight":
rotate.z -= 1;
break;
}
}
}
rotate.multiply(
new THREE.Vector3(this.rotateSpeed, this.rotateSpeed, this.rollSpeed),
);
if (rotate.manhattanLength() > 0.0) {
rotate.multiplyScalar(deltaTime);
const eulers = new THREE.Euler().setFromQuaternion(
control.quaternion,
"YXZ",
);
eulers.y -= rotate.x;
eulers.x = Math.max(
-Math.PI / 2,
Math.min(Math.PI / 2, eulers.x - rotate.y),
);
eulers.z = Math.max(-Math.PI, Math.min(Math.PI, eulers.z + rotate.z));
control.quaternion.setFromEuler(eulers);
}
// Movement
const moveVector = new THREE.Vector3(sticks[0].x, 0, sticks[0].y);
for (const [keycode, move] of Object.entries(this.keycodeMoveMapping)) {
if (this.keycode[keycode]) {
moveVector.add(move);
}
}
let speedMultiplier = 1.0;
if (this.keydown.CapsLock) {
speedMultiplier *= this.capsMultiplier;
}
if (this.keycode.ShiftLeft || this.keycode.ShiftRight) {
speedMultiplier *= this.shiftMultiplier;
}
if (this.keycode.ControlLeft || this.keycode.ControlRight) {
speedMultiplier *= this.ctrlMultiplier;
}
for (const button in this.gamepadMapping) {
if (gamepadButtons[Number.parseInt(button)]) {
switch (this.gamepadMapping[button]) {
case "shift":
speedMultiplier *= this.shiftMultiplier;
break;
case "ctrl":
speedMultiplier *= this.ctrlMultiplier;
break;
}
}
}
// Apply movement in view direction
moveVector.applyQuaternion(control.quaternion);
control.position.add(
moveVector.multiplyScalar(this.moveSpeed * speedMultiplier * deltaTime),
);
}
}
type PointerState = {
initial: THREE.Vector2;
last: THREE.Vector2;
position: THREE.Vector2;
pointerId: number;
button?: number;
timeStamp: DOMHighResTimeStamp;
};
// `PointerControls` implements pointer/mouse/touch controls on the canvas,
// for both desktop and mobile web applications.
export class PointerControls {
canvas: HTMLCanvasElement;
rotateSpeed: number;
slideSpeed: number;
scrollSpeed: number;
reverseRotate: boolean;
reverseSlide: boolean;
reverseSwipe: boolean;
reverseScroll: boolean;
moveInertia: number;
rotateInertia: number;
// Enable/disable controls updates
enable = true;
doublePress: ({
position,
intervalMs,
}: { position: THREE.Vector2; intervalMs: number }) => void;
// Time limit for double press (default DOUBLE_PRESS_LIMIT_MS)
doublePressLimitMs: number;
// Distance limit for double press (default DOUBLE_PRESS_DISTANCE)
doublePressDistance: number;
// Last pointer up event (default: null)
lastUp: { position: THREE.Vector2; time: number } | null;
// Pointer state for currently active rotating pointer
rotating: PointerState | null;
// Pointer state for currently active sliding pointer
sliding: PointerState | null;
// Whether we pressed two pointers at the same time
dualPress: boolean;
// Cumulative scroll movement
scroll: THREE.Vector3;
// Current rotation velocity
rotateVelocity: THREE.Vector3;
// Current movement velocity
moveVelocity: THREE.Vector3;
constructor({
// The HTML canvas element to attach pointer events to
canvas,
// Speed of rotation (default DEFAULT_ROTATE_SPEED)
rotateSpeed,
// Speed of sliding when dragging with right/middle mouse button or two fingers
// (default DEFAULT_SLIDE_SPEED)
slideSpeed,
// Speed of movement when using mouse scroll wheel (default DEFAULT_SCROLL_SPEED)
scrollSpeed,
// Reverse the direction of rotation (default: false)
reverseRotate,
// Reverse the direction of sliding (default: false)
reverseSlide,
// Reverse the direction of swipe gestures (default: false)
reverseSwipe,
// Reverse the direction of scroll wheel movement (default: false)
reverseScroll,
// Inertia factor for movement (default: DEFAULT_MOVE_INERTIA)
moveInertia,
// Inertia factor for rotation (default: DEFAULT_ROTATE_INERTIA)
rotateInertia,
// Callback for double press events (default: () => {})
doublePress,
}: {
canvas: HTMLCanvasElement;
rotateSpeed?: number;
slideSpeed?: number;
scrollSpeed?: number;
reverseRotate?: boolean;
reverseSlide?: boolean;
reverseSwipe?: boolean;
reverseScroll?: boolean;
moveInertia?: number;
rotateInertia?: number;
doublePress?: ({
position,
intervalMs,
}: { position: THREE.Vector2; intervalMs: number }) => void;
}) {
this.canvas = canvas;
this.rotateSpeed = rotateSpeed ?? DEFAULT_ROTATE_SPEED;
this.slideSpeed = slideSpeed ?? DEFAULT_SLIDE_SPEED;
this.scrollSpeed = scrollSpeed ?? DEFAULT_SCROLL_SPEED;
this.reverseRotate = reverseRotate ?? false;
this.reverseSlide = reverseSlide ?? false;
this.reverseSwipe = reverseSwipe ?? false;
this.reverseScroll = reverseScroll ?? false;
this.moveInertia = moveInertia ?? DEFAULT_MOVE_INERTIA;
this.rotateInertia = rotateInertia ?? DEFAULT_ROTATE_INERTIA;
this.doublePress = doublePress ?? (() => {});
this.doublePressLimitMs = DOUBLE_PRESS_LIMIT_MS;
this.doublePressDistance = DOUBLE_PRESS_DISTANCE;
this.lastUp = null;
this.rotating = null;
this.sliding = null;
this.dualPress = false;
this.scroll = new THREE.Vector3();
this.rotateVelocity = new THREE.Vector3();
this.moveVelocity = new THREE.Vector3();
canvas.addEventListener("pointerdown", (event: PointerEvent) => {
const position = this.getPointerPosition(event);
const initial = position.clone();
const last = position.clone();
// Determine if we're starting a rotation pointer action
const isRotate =
!this.rotating && (event.pointerType !== "mouse" || event.button === 0);
const { pointerId, timeStamp } = event;
if (isRotate) {
this.rotating = { initial, last, position, pointerId, timeStamp };
// Capture the pointer so events continue to be delivered even if it leaves the canvas.
canvas.setPointerCapture(event.pointerId);
this.dualPress = false;
} else if (!this.sliding) {
// If it's not a rotation action and we're not yet sliding, the next
// pointer activates a sliding action
const button = event.pointerType === "mouse" ? event.button : undefined;
this.sliding = {
initial,
last,
position,
pointerId,
button,
timeStamp,
};
// Capture the pointer so events continue to be delivered even if it leaves the canvas.
canvas.setPointerCapture(event.pointerId);
// Check if we pressed both pointers at roughly the same time
this.dualPress =
this.rotating != null &&
timeStamp - this.rotating.timeStamp < DUAL_PRESS_MS;
}
});
const pointerUp = (event: PointerEvent) => {
if (this.rotating?.pointerId === event.pointerId) {
this.rotating = null;
canvas.releasePointerCapture(event.pointerId);
if (this.dualPress && this.sliding) {
canvas.releasePointerCapture(this.sliding.pointerId);
this.sliding = null;
}
} else if (this.sliding?.pointerId === event.pointerId) {
this.sliding = null;
canvas.releasePointerCapture(event.pointerId);
if (this.dualPress && this.rotating) {
canvas.releasePointerCapture(this.rotating.pointerId);
this.rotating = null;
}
}
const position = this.getPointerPosition(event);
const lastUp = this.lastUp;
this.lastUp = { position, time: event.timeStamp };
if (lastUp) {
const distance = lastUp.position.distanceTo(position);
if (distance < this.doublePressDistance) {
const intervalMs = event.timeStamp - lastUp.time;
if (intervalMs < this.doublePressLimitMs) {
// We pressed and release twice within the time and distance limits
this.lastUp = null;
this.doublePress({ position, intervalMs });
}
}
}
};
document.addEventListener("pointerup", pointerUp);
document.addEventListener("pointercancel", pointerUp);
document.addEventListener("pointermove", (event: PointerEvent) => {
if (this.rotating?.pointerId === event.pointerId) {
this.rotating.position = this.getPointerPosition(event);
} else if (this.sliding?.pointerId === event.pointerId) {
this.sliding.position = this.getPointerPosition(event);
}
});
canvas.addEventListener("contextmenu", (event: MouseEvent) => {
// Prevent context menu appearing on right click
event.preventDefault();
});
canvas.addEventListener("wheel", (event: WheelEvent) => {
this.scroll.add(
new THREE.Vector3(event.deltaX, event.deltaY, event.deltaZ),
);
event.preventDefault();
});
}
getPointerPosition(event: PointerEvent): THREE.Vector2 {
const rect = this.canvas.getBoundingClientRect();
return new THREE.Vector2(
event.clientX - rect.left,
event.clientY - rect.top,
);
}
update(deltaTime: number, control: THREE.Object3D) {
if (!this.enable) {
return;
}
if (this.dualPress && this.rotating && this.sliding) {
// We pressed both pointers at the same time, either pinching or sliding
const motion = [
this.rotating.position.clone().sub(this.rotating.last),
this.sliding.position.clone().sub(this.sliding.last),
];
const coincidence = motion[0].dot(motion[1]);
if (coincidence >= 0.2) {
// Similar directions so slide the camera on the XY plane
const totalMotion = motion[0].clone().add(motion[1]);
const slide = new THREE.Vector3(totalMotion.x, -totalMotion.y, 0);
slide.multiplyScalar(this.slideSpeed * (this.reverseSwipe ? 1 : -1));
slide.applyQuaternion(control.quaternion);
control.position.add(slide);
this.moveVelocity = slide.clone().multiplyScalar(1 / deltaTime);
} else if (coincidence <= -0.2) {
// Opposite directions so either pinch or roll motion
const deltaDir = this.sliding.last.clone().sub(this.rotating.last);
const deltaDist = deltaDir.length();
deltaDir.multiplyScalar(1 / deltaDist).normalize();
const orthoDir = new THREE.Vector2(-deltaDir.y, deltaDir.x);
const motionDir = [motion[0].dot(deltaDir), motion[1].dot(deltaDir)];
const motionOrtho = [motion[0].dot(orthoDir), motion[1].dot(orthoDir)];
// Pinching motion
const midpoint = this.rotating.last
.clone()
.add(this.sliding.last)
.multiplyScalar(0.5);
let midpointDir = new THREE.Vector3();
if (control instanceof THREE.Camera) {
const ndcMidpoint = new THREE.Vector2(
(midpoint.x / this.canvas.clientWidth) * 2 - 1,
-(midpoint.y / this.canvas.clientHeight) * 2 + 1,
);
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(ndcMidpoint, control);
midpointDir = raycaster.ray.direction;
}
const pinchOut = motionDir[1] - motionDir[0];
const slide = midpointDir.multiplyScalar(pinchOut * this.slideSpeed);
control.position.add(slide);
this.moveVelocity = slide.clone().multiplyScalar(1 / deltaTime);
// Rolling motion
// Calculate angle of orthogonal motion change over distance deltaDist/2
// motionOrtho[0] and 1 are already in float distance
const angles = [
Math.atan(motionOrtho[0] / (-0.5 * deltaDist)),
Math.atan(motionOrtho[1] / (0.5 * deltaDist)),
];
const rotate = 0.5 * (angles[0] + angles[1]);
const eulers = new THREE.Euler().setFromQuaternion(
control.quaternion,
"YXZ",
);
eulers.z = Math.max(
-Math.PI,
Math.min(Math.PI, eulers.z + 0.5 * rotate),
);
control.quaternion.setFromEuler(eulers);
}
this.rotating.last.copy(this.rotating.position);
this.sliding.last.copy(this.sliding.position);
} else {
// Didn't press both pointers at the same time, so we're in rotating
// or FPS mode
const rotate = new THREE.Vector3();
if (this.rotating && !this.dualPress) {
const delta = this.rotating.position.clone().sub(this.rotating.last);
this.rotating.last.copy(this.rotating.position);
rotate.set(delta.x, delta.y, 0);
rotate.multiplyScalar(this.rotateSpeed * (this.reverseRotate ? -1 : 1));
// Update rotation velocity from last delta
this.rotateVelocity = rotate.clone().multiplyScalar(1 / deltaTime);
} else {
// Continue to rotate with inertia
this.rotateVelocity.multiplyScalar(
Math.exp(-deltaTime / this.rotateInertia),
);
rotate.addScaledVector(this.rotateVelocity, deltaTime);
}
// Apply rotation in Euler angles space
const eulers = new THREE.Euler().setFromQuaternion(
control.quaternion,
"YXZ",
);
eulers.y -= rotate.x;
eulers.x = Math.max(
-Math.PI / 2,
Math.min(Math.PI / 2, eulers.x - rotate.y),
);
eulers.z *= Math.exp(-DEFAULT_ROLL_SPRING * deltaTime);
control.quaternion.setFromEuler(eulers);
if (this.sliding && !this.dualPress) {
const delta = this.sliding.position.clone().sub(this.sliding.last);
this.sliding.last.copy(this.sliding.position);
// Slide on plane depending on center/right mouse button
const slide =
this.sliding.button !== 2
? new THREE.Vector3(delta.x, 0, delta.y)
: new THREE.Vector3(delta.x, -delta.y, 0);
slide.multiplyScalar(this.slideSpeed * (this.reverseSlide ? -1 : 1));
slide.applyQuaternion(control.quaternion);
control.position.add(slide);
// Update movement velocity from last delta
this.moveVelocity = slide.clone().multiplyScalar(1 / deltaTime);
} else {
// Continue to move with inertia
this.moveVelocity.multiplyScalar(
Math.exp(-deltaTime / this.moveInertia),
);
control.position.addScaledVector(this.moveVelocity, deltaTime);
}
}
const scroll = this.scroll.multiplyScalar(this.scrollSpeed);
scroll.set(scroll.x, scroll.z, scroll.y);
if (this.reverseScroll) {
scroll.multiplyScalar(-1);
}
scroll.applyQuaternion(control.quaternion);
control.position.add(scroll);
this.scroll.set(0, 0, 0);
}
}
+36
View File
@@ -0,0 +1,36 @@
// LN_SCALE_MIN..LN_SCALE_MAX define the internal scale range of for Gsplats,
// covering approx 0.0001..8000 in range with discrete steps 7% apart.
// The value "0" is reserved for truly flat scales, indicating a 2DGS.
// If these values are changed, the corresponding values in splatDefines.glsl
// must also be updated to match.
export const LN_SCALE_MIN = -9.0;
export const LN_SCALE_MAX = 9.0;
export const LN_RESCALE = (LN_SCALE_MAX - LN_SCALE_MIN) / 254.0; // 1..=255
export const SCALE_MIN = Math.exp(LN_SCALE_MIN);
export const SCALE_MAX = Math.exp(LN_SCALE_MAX);
// Gsplats are stored in textures that are 2^11 x 2^11 x up to 2^11
// Most WebGL2 implementations support 2D textures up to 2^12 x 2^12 (max 16M Gsplats)
// 2D array textures and 3D textures up to 2^11 x 2^11 x 2^11 (max 8G Gsplats),
// so we use 2D array textures for our representation for higher limits.
export const SPLAT_TEX_WIDTH_BITS = 11;
export const SPLAT_TEX_HEIGHT_BITS = 11;
export const SPLAT_TEX_DEPTH_BITS = 11;
export const SPLAT_TEX_LAYER_BITS =
SPLAT_TEX_WIDTH_BITS + SPLAT_TEX_HEIGHT_BITS;
export const SPLAT_TEX_WIDTH = 1 << SPLAT_TEX_WIDTH_BITS; // 2048
export const SPLAT_TEX_HEIGHT = 1 << SPLAT_TEX_HEIGHT_BITS; // 2048
export const SPLAT_TEX_DEPTH = 1 << SPLAT_TEX_DEPTH_BITS; // 2048
export const SPLAT_TEX_MIN_HEIGHT = 1;
export const SPLAT_TEX_WIDTH_MASK = SPLAT_TEX_WIDTH - 1;
export const SPLAT_TEX_HEIGHT_MASK = SPLAT_TEX_HEIGHT - 1;
export const SPLAT_TEX_DEPTH_MASK = SPLAT_TEX_DEPTH - 1;
// Enable/disable Gsplat sorting via Rust WASM code. In testing the sorting
// time between pure JS and WASM are minimal and don't make a big difference.
export const WASM_SPLAT_SORT = true;
+16
View File
@@ -0,0 +1,16 @@
export * from "./dyno/types";
export * from "./dyno/base";
export * from "./dyno/value";
export * from "./dyno/output";
export * from "./dyno/uniforms";
export * from "./dyno/program";
export * from "./dyno/math";
export * from "./dyno/logic";
export * from "./dyno/util";
export * from "./dyno/splats";
export * from "./dyno/transform";
export * from "./dyno/control";
export * from "./dyno/convert";
export * from "./dyno/texture";
export * from "./dyno/trig";
export * from "./dyno/vecmat";
+575
View File
@@ -0,0 +1,575 @@
import type { IUniform } from "three";
import type { DynoType } from "./types";
import {
DynoLiteral,
DynoOutput,
type DynoVal,
DynoValue,
type HasDynoOut,
valType,
} from "./value";
const DEFAULT_INDENT = " ";
export class Compilation {
globals: Set<string> = new Set();
statements: string[] = [];
uniforms: Record<string, IUniform> = {};
declares: Set<string> = new Set();
updaters: (() => void)[] = [];
sequence = 0;
indent: string = DEFAULT_INDENT;
constructor({ indent }: { indent?: string } = {}) {
this.indent = indent ?? DEFAULT_INDENT;
}
nextSequence() {
return this.sequence++;
}
}
export type IOTypes = Record<string, DynoType>;
type GenerateContext<InTypes extends IOTypes, OutTypes extends IOTypes> = {
inputs: { [K in keyof InTypes]?: string };
outputs: { [K in keyof OutTypes]?: string };
compile: Compilation;
};
export class Dyno<InTypes extends IOTypes, OutTypes extends IOTypes> {
inTypes: InTypes;
outTypes: OutTypes;
inputs: { [K in keyof InTypes]?: DynoVal<InTypes[K]> };
update?: () => void;
globals?: ({
inputs,
outputs,
compile,
}: GenerateContext<InTypes, OutTypes>) => string[];
statements?: ({
inputs,
outputs,
compile,
}: GenerateContext<InTypes, OutTypes>) => string[];
generate: ({
inputs,
outputs,
compile,
}: GenerateContext<InTypes, OutTypes>) => {
globals?: string[];
statements?: string[];
uniforms?: Record<string, IUniform>;
};
constructor({
inTypes,
outTypes,
inputs,
update,
globals,
statements,
generate,
}: {
inTypes?: InTypes;
outTypes?: OutTypes;
inputs?: { [K in keyof InTypes]?: DynoVal<InTypes[K]> };
update?: () => void;
globals?: ({
inputs,
outputs,
compile,
}: GenerateContext<InTypes, OutTypes>) => string[];
statements?: ({
inputs,
outputs,
compile,
}: GenerateContext<InTypes, OutTypes>) => string[];
generate?: ({
inputs,
outputs,
compile,
}: GenerateContext<InTypes, OutTypes>) => {
globals?: string[];
statements?: string[];
uniforms?: Record<string, IUniform>;
};
}) {
this.inTypes = inTypes ?? ({} as InTypes);
this.outTypes = outTypes ?? ({} as OutTypes);
this.inputs = inputs ?? {};
this.update = update;
this.globals = globals;
this.statements = statements;
this.generate =
generate ??
(({ inputs, outputs, compile }) => {
return {
globals: this.globals?.({ inputs, outputs, compile }),
statements: this.statements?.({ inputs, outputs, compile }),
};
});
}
get outputs(): { [K in keyof OutTypes]: DynoVal<OutTypes[K]> } {
const outputs = {} as { [K in keyof OutTypes]: DynoVal<OutTypes[K]> };
for (const key in this.outTypes) {
outputs[key] = new DynoOutput(this, key);
}
return outputs;
}
apply(inputs: { [K in keyof InTypes]?: DynoVal<InTypes[K]> }): {
[K in keyof OutTypes]: DynoVal<OutTypes[K]>;
} {
Object.assign(this.inputs, inputs);
return this.outputs;
}
compile({
inputs,
outputs,
compile,
}: {
inputs: { [K in keyof InTypes]?: string };
outputs: { [K in keyof OutTypes]?: string };
compile: Compilation;
}): string[] {
const result = [
`// ${this.constructor.name}(${Object.values(inputs).join(", ")}) => (${Object.values(outputs).join(", ")})`,
];
const declares: (keyof OutTypes)[] = [];
for (const key in outputs) {
const name = outputs[key];
if (name && !compile.declares.has(name)) {
compile.declares.add(name);
declares.push(key);
}
}
const { globals, statements, uniforms } = this.generate({
inputs,
outputs,
compile,
});
for (const global of globals ?? []) {
compile.globals.add(global);
}
for (const key in uniforms) {
compile.uniforms[key] = uniforms[key];
}
if (this.update) {
compile.updaters.push(this.update);
}
for (const key of declares) {
const name = outputs[key];
if (name) {
if (!compile.uniforms[name]) {
result.push(`${dynoDeclare(name, this.outTypes[key])};`);
}
}
}
if (statements?.length) {
result.push("{");
result.push(...statements.map((line) => compile.indent + line));
result.push("}");
}
return result;
}
}
export type DynoBlockType<InTypes extends IOTypes, OutTypes extends IOTypes> = (
inputs: { [K in keyof InTypes]?: DynoVal<InTypes[K]> },
outputs: { [K in keyof OutTypes]?: DynoVal<OutTypes[K]> },
{ roots }: { roots: Dyno<InTypes, OutTypes>[] },
) => { [K in keyof OutTypes]?: DynoVal<OutTypes[K]> } | undefined;
export class DynoBlock<
InTypes extends IOTypes,
OutTypes extends IOTypes,
> extends Dyno<InTypes, OutTypes> {
construct: DynoBlockType<InTypes, OutTypes>;
constructor({
inTypes,
outTypes,
inputs,
update,
globals,
construct,
}: {
inTypes?: InTypes;
outTypes?: OutTypes;
inputs?: { [K in keyof InTypes]?: DynoVal<InTypes[K]> };
update?: () => void;
globals?: ({
inputs,
outputs,
compile,
}: GenerateContext<InTypes, OutTypes>) => string[];
construct: DynoBlockType<InTypes, OutTypes>;
}) {
super({
inTypes,
outTypes,
inputs,
update,
globals,
generate: (args) => this.generateBlock(args),
});
this.construct = construct;
}
generateBlock({
inputs,
outputs,
compile,
}: {
inputs: { [K in keyof InTypes]?: string };
outputs: { [K in keyof OutTypes]?: string };
compile: Compilation;
}) {
const blockInputs: { [K in keyof InTypes]?: DynoVal<InTypes[K]> } = {};
const blockOutputs: { [K in keyof OutTypes]?: DynoVal<OutTypes[K]> } = {};
for (const key in inputs) {
if (inputs[key] != null) {
blockInputs[key] = new DynoLiteral(this.inTypes[key], inputs[key]);
}
}
for (const key in outputs) {
if (outputs[key] != null) {
blockOutputs[key] = new DynoValue(this.outTypes[key]);
}
}
const options = { roots: [] };
const returned = this.construct(blockInputs, blockOutputs, options);
for (const global of this.globals?.({ inputs, outputs, compile }) ?? []) {
compile.globals.add(global);
}
const ordering: Dyno<IOTypes, IOTypes>[] = [];
const nodeOuts = new Map<
Dyno<IOTypes, IOTypes>,
{ sequence: number; outNames: Map<string, string>; newOuts: Set<string> }
>();
function visit(
node: Dyno<IOTypes, IOTypes>,
outKey?: string,
outName?: string,
) {
let outs = nodeOuts.get(node);
if (!outs) {
// First time visiting this node
outs = {
sequence: compile.nextSequence(),
outNames: new Map(),
newOuts: new Set(),
};
nodeOuts.set(node, outs);
for (const key in node.inputs) {
let input = node.inputs[key];
while (input) {
if (input instanceof DynoValue) {
if (input instanceof DynoOutput) {
visit(input.dyno, input.key);
}
break;
}
// Must be as HasDynoOut<T>
input = input.dynoOut();
}
}
ordering.push(node);
}
if (outKey) {
if (!outName) {
outs.newOuts.add(outKey);
}
outs.outNames.set(outKey, outName ?? `${outKey}_${outs.sequence}`);
}
}
for (const root of options.roots) {
visit(root);
}
for (const key in blockOutputs) {
let value = returned?.[key] ?? blockOutputs[key];
while (value) {
if (value instanceof DynoValue) {
if (value instanceof DynoOutput) {
visit(value.dyno, value.key, outputs[key]);
}
break;
}
// Must be as HasDynoOut<T>
value = value.dynoOut();
}
blockOutputs[key] = value;
}
const steps = [];
for (const dyno of ordering) {
// compile.statements.push(`// ${dyno.constructor.name}(${Object.values(inputs).join(", ")}) => (${Object.values(outputs).join(", ")})`);
const inputs: Record<string, string> = {};
const outputs: Record<string, string> = {};
for (const key in dyno.inputs) {
let value = dyno.inputs[key];
while (value) {
if (value instanceof DynoValue) {
if (value instanceof DynoLiteral) {
inputs[key] = value.getLiteral();
} else if (value instanceof DynoOutput) {
const source = nodeOuts.get(value.dyno)?.outNames.get(value.key);
if (!source) {
throw new Error(
`Source not found for ${value.dyno.constructor.name}.${value.key}`,
);
}
inputs[key] = source;
}
break;
}
// Must be as HasDynOut<T>
value = value.dynoOut();
}
}
const outs = nodeOuts.get(dyno) ?? { outNames: new Map() };
for (const [key, name] of outs.outNames.entries()) {
outputs[key] = name;
}
const newSteps = dyno.compile({ inputs, outputs, compile });
steps.push(newSteps);
}
const literalOutputs = [];
for (const key in outputs) {
if (blockOutputs[key] instanceof DynoLiteral) {
literalOutputs.push(
`${outputs[key]} = ${blockOutputs[key].getLiteral()};`,
);
}
}
if (literalOutputs.length > 0) {
steps.push(literalOutputs);
}
const statements = steps.flatMap((step, index) => {
// Add a blank line between steps
return index === 0 ? step : ["", ...step];
});
return { statements };
}
}
export function dynoBlock<
InTypes extends Record<string, DynoType>,
OutTypes extends Record<string, DynoType>,
>(
inTypes: InTypes,
outTypes: OutTypes,
construct: DynoBlockType<InTypes, OutTypes>,
{ update, globals }: { update?: () => void; globals?: () => string[] } = {},
) {
return new DynoBlock({ inTypes, outTypes, construct, update, globals });
}
export function dyno<
InTypes extends Record<string, DynoType>,
OutTypes extends Record<string, DynoType>,
>({
inTypes,
outTypes,
inputs,
update,
globals,
statements,
generate,
}: {
inTypes: InTypes;
outTypes: OutTypes;
inputs?: { [K in keyof InTypes]?: DynoVal<InTypes[K]> };
update?: () => void;
globals?: ({
inputs,
outputs,
compile,
}: GenerateContext<InTypes, OutTypes>) => string[];
statements?: ({
inputs,
outputs,
compile,
}: GenerateContext<InTypes, OutTypes>) => string[];
generate?: ({
inputs,
outputs,
compile,
}: GenerateContext<InTypes, OutTypes>) => {
globals?: string[];
statements?: string[];
uniforms?: Record<string, IUniform>;
};
}) {
return new Dyno({
inTypes,
outTypes,
inputs,
update,
globals,
statements,
generate,
});
}
export function dynoDeclare(name: string, type: DynoType, count?: number) {
const typeStr = typeof type === "string" ? type : type.type;
if (!typeStr) {
throw new Error(`Invalid DynoType: ${String(type)}`);
}
return `${typeStr} ${name}${count != null ? `[${count}]` : ""}`;
}
export function unindentLines(s: string): string[] {
let seenNonEmpty = false;
const lines = s
.split("\n")
.map((line) => {
const trimmedLine = line.trimEnd();
if (seenNonEmpty) {
return trimmedLine;
}
if (trimmedLine.length > 0) {
seenNonEmpty = true;
return trimmedLine;
}
return null;
})
.filter((line) => line != null);
while (lines.length > 0 && lines[lines.length - 1].length === 0) {
lines.pop();
}
if (lines.length === 0) {
return [];
}
const indent = lines[0].match(/^\s*/)?.[0];
if (!indent) {
return lines; // No indent, return as is
}
// Remove indent from the beginning of each line
const regex = new RegExp(`^${indent}`);
return lines.map((line) => line.replace(regex, ""));
}
export function unindent(s: string): string {
return unindentLines(s).join("\n");
}
export class UnaryOp<
A extends DynoType,
OutType extends DynoType,
OutKey extends string,
>
extends Dyno<{ a: A }, { [key in OutKey]: OutType }>
implements HasDynoOut<OutType>
{
constructor({
a,
outKey,
outTypeFunc,
}: { a: DynoVal<A>; outKey: OutKey; outTypeFunc: (aType: A) => OutType }) {
const inTypes = { a: valType(a) };
const outType = outTypeFunc(valType(a));
const outTypes = { [outKey]: outType } as { [key in OutKey]: OutType };
super({ inTypes, outTypes, inputs: { a } });
this.outKey = outKey;
}
outKey: OutKey;
dynoOut(): DynoValue<OutType> {
return new DynoOutput(this, this.outKey);
}
}
export class BinaryOp<
A extends DynoType,
B extends DynoType,
OutType extends DynoType,
OutKey extends string,
>
extends Dyno<{ a: A; b: B }, { [key in OutKey]: OutType }>
implements HasDynoOut<OutType>
{
constructor({
a,
b,
outKey,
outTypeFunc,
}: {
a: DynoVal<A>;
b: DynoVal<B>;
outKey: OutKey;
outTypeFunc: (aType: A, bType: B) => OutType;
}) {
const inTypes = { a: valType(a), b: valType(b) };
const outType = outTypeFunc(valType(a), valType(b));
const outTypes = { [outKey]: outType } as { [key in OutKey]: OutType };
super({ inTypes, outTypes, inputs: { a, b } });
this.outKey = outKey;
}
outKey: OutKey;
dynoOut(): DynoValue<OutType> {
return new DynoOutput(this, this.outKey);
}
}
export class TrinaryOp<
A extends DynoType,
B extends DynoType,
C extends DynoType,
OutType extends DynoType,
OutKey extends string,
>
extends Dyno<{ a: A; b: B; c: C }, { [key in OutKey]: OutType }>
implements HasDynoOut<OutType>
{
constructor({
a,
b,
c,
outKey,
outTypeFunc,
}: {
a: DynoVal<A>;
b: DynoVal<B>;
c: DynoVal<C>;
outKey: OutKey;
outTypeFunc: (aType: A, bType: B, cType: C) => OutType;
}) {
const inTypes = { a: valType(a), b: valType(b), c: valType(c) };
const outType = outTypeFunc(valType(a), valType(b), valType(c));
const outTypes = { [outKey]: outType } as { [key in OutKey]: OutType };
super({ inTypes, outTypes, inputs: { a, b, c } });
this.outKey = outKey;
}
outKey: OutKey;
dynoOut(): DynoValue<OutType> {
return new DynoOutput(this, this.outKey);
}
}
+22
View File
@@ -0,0 +1,22 @@
// TODO:
// if, switch, for, comment,
// arrayIndex, arrayLength,
export const dynoIf = () => {
throw new Error("Not implemented");
};
export const dynoSwitch = () => {
throw new Error("Not implemented");
};
export const dynoFor = () => {
throw new Error("Not implemented");
};
export const comment = () => {
throw new Error("Not implemented");
};
export const arrayIndex = () => {
throw new Error("Not implemented");
};
export const arrayLength = () => {
throw new Error("Not implemented");
};
+451
View File
@@ -0,0 +1,451 @@
import { UnaryOp } from "./base";
import { type SimpleTypes, typeLiteral } from "./types";
import type { DynoVal } from "./value";
export const bool = <T extends "bool" | "int" | "uint" | "float">(
value: DynoVal<T>,
): DynoVal<"bool"> => new Bool({ value });
export const int = <T extends "bool" | "int" | "uint" | "float">(
value: DynoVal<T>,
): DynoVal<"int"> => new Int({ value });
export const uint = <T extends "bool" | "int" | "uint" | "float">(
value: DynoVal<T>,
): DynoVal<"uint"> => new Uint({ value });
export const float = <T extends "bool" | "int" | "uint" | "float">(
value: DynoVal<T>,
): DynoVal<"float"> => new Float({ value });
export const bvec2 = <T extends "bool" | "bvec2" | "ivec2" | "uvec2" | "vec2">(
value: DynoVal<T>,
): DynoVal<"bvec2"> => new BVec2({ value });
export const bvec3 = <T extends "bool" | "bvec3" | "ivec3" | "uvec3" | "vec3">(
value: DynoVal<T>,
): DynoVal<"bvec3"> => new BVec3({ value });
export const bvec4 = <T extends "bool" | "bvec4" | "ivec4" | "uvec4" | "vec4">(
value: DynoVal<T>,
): DynoVal<"bvec4"> => new BVec4({ value });
export const ivec2 = <T extends "int" | "bvec2" | "ivec2" | "uvec2" | "vec2">(
value: DynoVal<T>,
): DynoVal<"ivec2"> => new IVec2({ value });
export const ivec3 = <T extends "int" | "bvec3" | "ivec3" | "uvec3" | "vec3">(
value: DynoVal<T>,
): DynoVal<"ivec3"> => new IVec3({ value });
export const ivec4 = <T extends "int" | "bvec4" | "ivec4" | "uvec4" | "vec4">(
value: DynoVal<T>,
): DynoVal<"ivec4"> => new IVec4({ value });
export const uvec2 = <T extends "uint" | "bvec2" | "ivec2" | "uvec2" | "vec2">(
value: DynoVal<T>,
): DynoVal<"uvec2"> => new UVec2({ value });
export const uvec3 = <T extends "uint" | "bvec3" | "ivec3" | "uvec3" | "vec3">(
value: DynoVal<T>,
): DynoVal<"uvec3"> => new UVec3({ value });
export const uvec4 = <T extends "uint" | "bvec4" | "ivec4" | "uvec4" | "vec4">(
value: DynoVal<T>,
): DynoVal<"uvec4"> => new UVec4({ value });
export const vec2 = <
T extends "float" | "bvec2" | "ivec2" | "uvec2" | "vec2" | "vec3" | "vec4",
>(
value: DynoVal<T>,
): DynoVal<"vec2"> => new Vec2({ value });
export const vec3 = <
T extends "float" | "bvec3" | "ivec3" | "uvec3" | "vec3" | "vec4",
>(
value: DynoVal<T>,
): DynoVal<"vec3"> => new Vec3({ value });
export const vec4 = <T extends "float" | "bvec4" | "ivec4" | "uvec4" | "vec4">(
value: DynoVal<T>,
): DynoVal<"vec4"> => new Vec4({ value });
export const mat2 = <T extends "float" | "mat2" | "mat3" | "mat4">(
value: DynoVal<T>,
): DynoVal<"mat2"> => new Mat2({ value });
export const mat3 = <T extends "float" | "mat2" | "mat3" | "mat4">(
value: DynoVal<T>,
): DynoVal<"mat3"> => new Mat3({ value });
export const mat4 = <T extends "float" | "mat2" | "mat3" | "mat4">(
value: DynoVal<T>,
): DynoVal<"mat4"> => new Mat4({ value });
export const floatBitsToInt = (value: DynoVal<"float">): DynoVal<"int"> =>
new FloatBitsToInt({ value });
export const floatBitsToUint = (value: DynoVal<"float">): DynoVal<"uint"> =>
new FloatBitsToUint({ value });
export const intBitsToFloat = (value: DynoVal<"int">): DynoVal<"float"> =>
new IntBitsToFloat({ value });
export const uintBitsToFloat = (value: DynoVal<"uint">): DynoVal<"float"> =>
new UintBitsToFloat({ value });
export const packSnorm2x16 = (value: DynoVal<"vec2">): DynoVal<"uint"> =>
new PackSnorm2x16({ value });
export const unpackSnorm2x16 = (value: DynoVal<"uint">): DynoVal<"vec2"> =>
new UnpackSnorm2x16({ value });
export const packUnorm2x16 = (value: DynoVal<"vec2">): DynoVal<"uint"> =>
new PackUnorm2x16({ value });
export const unpackUnorm2x16 = (value: DynoVal<"uint">): DynoVal<"vec2"> =>
new UnpackUnorm2x16({ value });
export const packHalf2x16 = (value: DynoVal<"vec2">): DynoVal<"uint"> =>
new PackHalf2x16({ value });
export const unpackHalf2x16 = (value: DynoVal<"uint">): DynoVal<"vec2"> =>
new UnpackHalf2x16({ value });
export const uintToRgba8 = (value: DynoVal<"uint">): DynoVal<"vec4"> =>
new UintToRgba8({ value });
export class SimpleCast<
Allowed extends SimpleTypes,
OutType extends SimpleTypes,
OutKey extends string,
> extends UnaryOp<Allowed, OutType, OutKey> {
constructor({
value,
outType,
outKey,
}: { value: DynoVal<Allowed>; outType: OutType; outKey: OutKey }) {
super({ a: value, outTypeFunc: () => outType, outKey });
this.statements = ({ inputs, outputs }) => [
`${outputs[outKey]} = ${typeLiteral(outType)}(${inputs.a});`,
];
}
}
export class Bool extends SimpleCast<
"bool" | "int" | "uint" | "float",
"bool",
"bool"
> {
constructor({
value,
}: { value: DynoVal<"bool" | "int" | "uint" | "float"> }) {
super({ value, outType: "bool", outKey: "bool" });
}
}
export class Int extends SimpleCast<
"bool" | "int" | "uint" | "float",
"int",
"int"
> {
constructor({
value,
}: { value: DynoVal<"bool" | "int" | "uint" | "float"> }) {
super({ value, outType: "int", outKey: "int" });
}
}
export class Uint extends SimpleCast<
"bool" | "int" | "uint" | "float",
"uint",
"uint"
> {
constructor({
value,
}: { value: DynoVal<"bool" | "int" | "uint" | "float"> }) {
super({ value, outType: "uint", outKey: "uint" });
}
}
export class Float extends SimpleCast<
"bool" | "int" | "uint" | "float",
"float",
"float"
> {
constructor({
value,
}: { value: DynoVal<"bool" | "int" | "uint" | "float"> }) {
super({ value, outType: "float", outKey: "float" });
}
}
export class BVec2 extends SimpleCast<
"bool" | "bvec2" | "ivec2" | "uvec2" | "vec2",
"bvec2",
"bvec2"
> {
constructor({
value,
}: { value: DynoVal<"bool" | "bvec2" | "ivec2" | "uvec2" | "vec2"> }) {
super({ value, outType: "bvec2", outKey: "bvec2" });
}
}
export class BVec3 extends SimpleCast<
"bool" | "bvec3" | "ivec3" | "uvec3" | "vec3",
"bvec3",
"bvec3"
> {
constructor({
value,
}: { value: DynoVal<"bool" | "bvec3" | "ivec3" | "uvec3" | "vec3"> }) {
super({ value, outType: "bvec3", outKey: "bvec3" });
}
}
export class BVec4 extends SimpleCast<
"bool" | "bvec4" | "ivec4" | "uvec4" | "vec4",
"bvec4",
"bvec4"
> {
constructor({
value,
}: { value: DynoVal<"bool" | "bvec4" | "ivec4" | "uvec4" | "vec4"> }) {
super({ value, outType: "bvec4", outKey: "bvec4" });
}
}
export class IVec2 extends SimpleCast<
"int" | "bvec2" | "ivec2" | "uvec2" | "vec2",
"ivec2",
"ivec2"
> {
constructor({
value,
}: { value: DynoVal<"int" | "bvec2" | "ivec2" | "uvec2" | "vec2"> }) {
super({ value, outType: "ivec2", outKey: "ivec2" });
}
}
export class IVec3 extends SimpleCast<
"int" | "bvec3" | "ivec3" | "uvec3" | "vec3",
"ivec3",
"ivec3"
> {
constructor({
value,
}: { value: DynoVal<"int" | "bvec3" | "ivec3" | "uvec3" | "vec3"> }) {
super({ value, outType: "ivec3", outKey: "ivec3" });
}
}
export class IVec4 extends SimpleCast<
"int" | "bvec4" | "ivec4" | "uvec4" | "vec4",
"ivec4",
"ivec4"
> {
constructor({
value,
}: { value: DynoVal<"int" | "bvec4" | "ivec4" | "uvec4" | "vec4"> }) {
super({ value, outType: "ivec4", outKey: "ivec4" });
}
}
export class UVec2 extends SimpleCast<
"uint" | "ivec2" | "bvec2" | "uvec2" | "vec2",
"uvec2",
"uvec2"
> {
constructor({
value,
}: { value: DynoVal<"uint" | "ivec2" | "bvec2" | "uvec2" | "vec2"> }) {
super({ value, outType: "uvec2", outKey: "uvec2" });
}
}
export class UVec3 extends SimpleCast<
"uint" | "ivec3" | "bvec3" | "uvec3" | "vec3",
"uvec3",
"uvec3"
> {
constructor({
value,
}: { value: DynoVal<"uint" | "ivec3" | "bvec3" | "uvec3" | "vec3"> }) {
super({ value, outType: "uvec3", outKey: "uvec3" });
}
}
export class UVec4 extends SimpleCast<
"uint" | "ivec4" | "bvec4" | "uvec4" | "vec4",
"uvec4",
"uvec4"
> {
constructor({
value,
}: { value: DynoVal<"uint" | "ivec4" | "bvec4" | "uvec4" | "vec4"> }) {
super({ value, outType: "uvec4", outKey: "uvec4" });
}
}
export class Vec2 extends SimpleCast<
"float" | "bvec2" | "ivec2" | "uvec2" | "vec2" | "vec3" | "vec4",
"vec2",
"vec2"
> {
constructor({
value,
}: {
value: DynoVal<
"float" | "bvec2" | "ivec2" | "uvec2" | "vec2" | "vec3" | "vec4"
>;
}) {
super({ value, outType: "vec2", outKey: "vec2" });
}
}
export class Vec3 extends SimpleCast<
"float" | "bvec3" | "ivec3" | "uvec3" | "vec3" | "vec2" | "vec4",
"vec3",
"vec3"
> {
constructor({
value,
}: {
value: DynoVal<
"float" | "bvec3" | "ivec3" | "uvec3" | "vec3" | "vec2" | "vec4"
>;
}) {
super({ value, outType: "vec3", outKey: "vec3" });
}
}
export class Vec4 extends SimpleCast<
"float" | "bvec4" | "ivec4" | "uvec4" | "vec4",
"vec4",
"vec4"
> {
constructor({
value,
}: { value: DynoVal<"float" | "bvec4" | "ivec4" | "uvec4" | "vec4"> }) {
super({ value, outType: "vec4", outKey: "vec4" });
}
}
export class Mat2 extends SimpleCast<
"float" | "mat2" | "mat3" | "mat4",
"mat2",
"mat2"
> {
constructor({
value,
}: { value: DynoVal<"float" | "mat2" | "mat3" | "mat4"> }) {
super({ value, outType: "mat2", outKey: "mat2" });
}
}
export class Mat3 extends SimpleCast<
"float" | "mat2" | "mat3" | "mat4",
"mat3",
"mat3"
> {
constructor({
value,
}: { value: DynoVal<"float" | "mat2" | "mat3" | "mat4"> }) {
super({ value, outType: "mat3", outKey: "mat3" });
}
}
export class Mat4 extends SimpleCast<
"float" | "mat2" | "mat3" | "mat4",
"mat4",
"mat4"
> {
constructor({
value,
}: { value: DynoVal<"float" | "mat2" | "mat3" | "mat4"> }) {
super({ value, outType: "mat4", outKey: "mat4" });
}
}
export class FloatBitsToInt extends UnaryOp<"float", "int", "int"> {
constructor({ value }: { value: DynoVal<"float"> }) {
super({ a: value, outKey: "int", outTypeFunc: () => "int" });
this.statements = ({ inputs, outputs }) => {
return [`${outputs.int} = floatBitsToInt(${inputs.a});`];
};
}
}
export class FloatBitsToUint extends UnaryOp<"float", "uint", "uint"> {
constructor({ value }: { value: DynoVal<"float"> }) {
super({ a: value, outKey: "uint", outTypeFunc: () => "uint" });
this.statements = ({ inputs, outputs }) => {
return [`${outputs.uint} = floatBitsToUint(${inputs.a});`];
};
}
}
export class IntBitsToFloat extends UnaryOp<"int", "float", "float"> {
constructor({ value }: { value: DynoVal<"int"> }) {
super({ a: value, outKey: "float", outTypeFunc: () => "float" });
this.statements = ({ inputs, outputs }) => {
return [`${outputs.float} = intBitsToFloat(${inputs.a});`];
};
}
}
export class UintBitsToFloat extends UnaryOp<"uint", "float", "float"> {
constructor({ value }: { value: DynoVal<"uint"> }) {
super({ a: value, outKey: "float", outTypeFunc: () => "float" });
this.statements = ({ inputs, outputs }) => {
return [`${outputs.float} = uintBitsToFloat(${inputs.a});`];
};
}
}
export class PackSnorm2x16 extends UnaryOp<"vec2", "uint", "uint"> {
constructor({ value }: { value: DynoVal<"vec2"> }) {
super({ a: value, outKey: "uint", outTypeFunc: () => "uint" });
this.statements = ({ inputs, outputs }) => {
return [`${outputs.uint} = packSnorm2x16(${inputs.a});`];
};
}
}
export class UnpackSnorm2x16 extends UnaryOp<"uint", "vec2", "vec2"> {
constructor({ value }: { value: DynoVal<"uint"> }) {
super({ a: value, outKey: "vec2", outTypeFunc: () => "vec2" });
this.statements = ({ inputs, outputs }) => {
return [`${outputs.vec2} = unpackSnorm2x16(${inputs.a});`];
};
}
}
export class PackUnorm2x16 extends UnaryOp<"vec2", "uint", "uint"> {
constructor({ value }: { value: DynoVal<"vec2"> }) {
super({ a: value, outKey: "uint", outTypeFunc: () => "uint" });
this.statements = ({ inputs, outputs }) => {
return [`${outputs.uint} = packUnorm2x16(${inputs.a});`];
};
}
}
export class UnpackUnorm2x16 extends UnaryOp<"uint", "vec2", "vec2"> {
constructor({ value }: { value: DynoVal<"uint"> }) {
super({ a: value, outKey: "vec2", outTypeFunc: () => "vec2" });
this.statements = ({ inputs, outputs }) => {
return [`${outputs.vec2} = unpackUnorm2x16(${inputs.a});`];
};
}
}
export class PackHalf2x16 extends UnaryOp<"vec2", "uint", "uint"> {
constructor({ value }: { value: DynoVal<"vec2"> }) {
super({ a: value, outKey: "uint", outTypeFunc: () => "uint" });
this.statements = ({ inputs, outputs }) => {
return [`${outputs.uint} = packHalf2x16(${inputs.a});`];
};
}
}
export class UnpackHalf2x16 extends UnaryOp<"uint", "vec2", "vec2"> {
constructor({ value }: { value: DynoVal<"uint"> }) {
super({ a: value, outKey: "vec2", outTypeFunc: () => "vec2" });
this.statements = ({ inputs, outputs }) => {
return [`${outputs.vec2} = unpackHalf2x16(${inputs.a});`];
};
}
}
export class UintToRgba8 extends UnaryOp<"uint", "vec4", "rgba8"> {
constructor({ value }: { value: DynoVal<"uint"> }) {
super({ a: value, outKey: "rgba8", outTypeFunc: () => "vec4" });
this.statements = ({ inputs, outputs }) => {
return [
`uvec4 uRgba = uvec4(${inputs.a} & 0xffu, (${inputs.a} >> 8u) & 0xffu, (${inputs.a} >> 16u) & 0xffu, (${inputs.a} >> 24u) & 0xffu);`,
`${outputs.rgba8} = vec4(uRgba) / 255.0;`,
];
};
}
}
+434
View File
@@ -0,0 +1,434 @@
import { BinaryOp, TrinaryOp, UnaryOp } from "./base";
import {
type AllIntTypes,
type BoolTypes,
type IntTypes,
type ScalarTypes,
type SimpleTypes,
type UintTypes,
type ValueTypes,
isBoolType,
isIntType,
isScalarType,
isUintType,
isVector2Type,
isVector3Type,
} from "./types";
import { type DynoVal, valType } from "./value";
export const and = <T extends "bool" | AllIntTypes>(
a: DynoVal<T>,
b: DynoVal<T>,
): DynoVal<T> => new And({ a, b });
export const or = <T extends "bool" | AllIntTypes>(
a: DynoVal<T>,
b: DynoVal<T>,
): DynoVal<T> => new Or({ a, b });
export const xor = <T extends "bool" | AllIntTypes>(
a: DynoVal<T>,
b: DynoVal<T>,
): DynoVal<T> => new Xor({ a, b });
export const not = <T extends BoolTypes | AllIntTypes>(
a: DynoVal<T>,
): DynoVal<T> => new Not({ a });
export const lessThan = <T extends ValueTypes>(
a: DynoVal<T>,
b: DynoVal<T>,
): DynoVal<CompareOutput<T>> => new LessThan({ a, b });
export const lessThanEqual = <T extends ValueTypes>(
a: DynoVal<T>,
b: DynoVal<T>,
): DynoVal<CompareOutput<T>> => new LessThanEqual({ a, b });
export const greaterThan = <T extends ValueTypes>(
a: DynoVal<T>,
b: DynoVal<T>,
): DynoVal<CompareOutput<T>> => new GreaterThan({ a, b });
export const greaterThanEqual = <T extends ValueTypes>(
a: DynoVal<T>,
b: DynoVal<T>,
): DynoVal<CompareOutput<T>> => new GreaterThanEqual({ a, b });
export const equal = <T extends ValueTypes | BoolTypes>(
a: DynoVal<T>,
b: DynoVal<T>,
): DynoVal<EqualOutput<T>> => new Equal({ a, b });
export const notEqual = <T extends ValueTypes | BoolTypes>(
a: DynoVal<T>,
b: DynoVal<T>,
): DynoVal<NotEqualOutput<T>> => new NotEqual({ a, b });
export const any = <T extends "bvec2" | "bvec3" | "bvec4">(
a: DynoVal<T>,
): DynoVal<"bool"> => new Any({ a });
export const all = <T extends "bvec2" | "bvec3" | "bvec4">(
a: DynoVal<T>,
): DynoVal<"bool"> => new All({ a });
export const select = <T extends SimpleTypes>(
cond: DynoVal<"bool">,
t: DynoVal<T>,
f: DynoVal<T>,
): DynoVal<T> => new Select({ cond, t, f });
export const compXor = <T extends BoolTypes | AllIntTypes>(
a: DynoVal<T>,
): DynoVal<CompXorOutput<T>> => new CompXor({ a });
export class And<T extends "bool" | AllIntTypes> extends BinaryOp<
T,
T,
T,
"and"
> {
constructor({ a, b }: { a: DynoVal<T>; b: DynoVal<T> }) {
super({ a, b, outTypeFunc: (aType: T, bType: T) => aType, outKey: "and" });
this.statements = ({ inputs, outputs }) => {
if (this.outTypes.and === "bool") {
return [`${outputs.and} = ${inputs.a} && ${inputs.b};`];
}
return [`${outputs.and} = ${inputs.a} & ${inputs.b};`];
};
}
}
export class Or<T extends "bool" | AllIntTypes> extends BinaryOp<
T,
T,
T,
"or"
> {
constructor({ a, b }: { a: DynoVal<T>; b: DynoVal<T> }) {
super({ a, b, outTypeFunc: (aType: T, bType: T) => aType, outKey: "or" });
this.statements = ({ inputs, outputs }) => {
if (this.outTypes.or === "bool") {
return [`${outputs.or} = ${inputs.a} || ${inputs.b};`];
}
return [`${outputs.or} = ${inputs.a} | ${inputs.b};`];
};
}
}
export class Xor<T extends "bool" | AllIntTypes> extends BinaryOp<
T,
T,
T,
"xor"
> {
constructor({ a, b }: { a: DynoVal<T>; b: DynoVal<T> }) {
super({ a, b, outTypeFunc: (aType: T, bType: T) => aType, outKey: "xor" });
this.statements = ({ inputs, outputs }) => {
if (this.outTypes.xor === "bool") {
return [`${outputs.xor} = ${inputs.a} ^^ ${inputs.b};`];
}
return [`${outputs.xor} = ${inputs.a} ^ ${inputs.b};`];
};
}
}
export class Not<T extends BoolTypes | AllIntTypes> extends UnaryOp<
T,
T,
"not"
> {
constructor({ a }: { a: DynoVal<T> }) {
super({ a, outTypeFunc: (aType: T) => aType, outKey: "not" });
this.statements = ({ inputs, outputs }) => {
if (this.outTypes.not === "bool") {
return [`${outputs.not} = !${inputs.a};`];
}
return [`${outputs.not} = not(${inputs.a});`];
};
}
}
export class LessThan<T extends ValueTypes> extends BinaryOp<
T,
T,
CompareOutput<T>,
"lessThan"
> {
constructor({ a, b }: { a: DynoVal<T>; b: DynoVal<T> }) {
super({
a,
b,
outTypeFunc: (aType: T, bType: T) => compareOutputType(aType, "lessThan"),
outKey: "lessThan",
});
this.statements = ({ inputs, outputs }) => {
if (this.outTypes.lessThan === "bool") {
return [`${outputs.lessThan} = ${inputs.a} < ${inputs.b};`];
}
return [`${outputs.lessThan} = lessThan(${inputs.a}, ${inputs.b});`];
};
}
}
export class LessThanEqual<T extends ValueTypes> extends BinaryOp<
T,
T,
CompareOutput<T>,
"lessThanEqual"
> {
constructor({ a, b }: { a: DynoVal<T>; b: DynoVal<T> }) {
super({
a,
b,
outTypeFunc: (aType: T, bType: T) =>
compareOutputType(aType, "lessThanEqual"),
outKey: "lessThanEqual",
});
this.statements = ({ inputs, outputs }) => {
if (this.outTypes.lessThanEqual === "bool") {
return [`${outputs.lessThanEqual} = ${inputs.a} <= ${inputs.b};`];
}
return [
`${outputs.lessThanEqual} = lessThanEqual(${inputs.a}, ${inputs.b});`,
];
};
}
}
export class GreaterThan<T extends ValueTypes> extends BinaryOp<
T,
T,
CompareOutput<T>,
"greaterThan"
> {
constructor({ a, b }: { a: DynoVal<T>; b: DynoVal<T> }) {
super({
a,
b,
outTypeFunc: (aType: T, bType: T) =>
compareOutputType(aType, "greaterThan"),
outKey: "greaterThan",
});
this.statements = ({ inputs, outputs }) => {
if (this.outTypes.greaterThan === "bool") {
return [`${outputs.greaterThan} = ${inputs.a} > ${inputs.b};`];
}
return [
`${outputs.greaterThan} = greaterThan(${inputs.a}, ${inputs.b});`,
];
};
}
}
export class GreaterThanEqual<T extends ValueTypes> extends BinaryOp<
T,
T,
CompareOutput<T>,
"greaterThanEqual"
> {
constructor({ a, b }: { a: DynoVal<T>; b: DynoVal<T> }) {
super({
a,
b,
outTypeFunc: (aType: T, bType: T) =>
compareOutputType(aType, "greaterThanEqual"),
outKey: "greaterThanEqual",
});
this.statements = ({ inputs, outputs }) => {
if (this.outTypes.greaterThanEqual === "bool") {
return [`${outputs.greaterThanEqual} = ${inputs.a} >= ${inputs.b};`];
}
return [
`${outputs.greaterThanEqual} = greaterThanEqual(${inputs.a}, ${inputs.b});`,
];
};
}
}
export class Equal<T extends ValueTypes | BoolTypes> extends BinaryOp<
T,
T,
EqualOutput<T>,
"equal"
> {
constructor({ a, b }: { a: DynoVal<T>; b: DynoVal<T> }) {
super({ a, b, outTypeFunc: equalOutputType<T>, outKey: "equal" });
this.statements = ({ inputs, outputs }) => {
if (this.outTypes.equal === "bool") {
return [`${outputs.equal} = ${inputs.a} == ${inputs.b};`];
}
return [`${outputs.equal} = equal(${inputs.a}, ${inputs.b});`];
};
}
}
export class NotEqual<T extends ValueTypes | BoolTypes> extends BinaryOp<
T,
T,
NotEqualOutput<T>,
"notEqual"
> {
constructor({ a, b }: { a: DynoVal<T>; b: DynoVal<T> }) {
super({ a, b, outTypeFunc: notEqualOutputType<T>, outKey: "notEqual" });
this.statements = ({ inputs, outputs }) => {
if (this.outTypes.notEqual === "bool") {
return [`${outputs.notEqual} = ${inputs.a} != ${inputs.b};`];
}
return [`${outputs.notEqual} = notEqual(${inputs.a}, ${inputs.b});`];
};
}
}
export class Any<T extends BoolTypes> extends UnaryOp<T, "bool", "any"> {
constructor({ a }: { a: DynoVal<T> }) {
super({ a, outTypeFunc: (aType: T) => "bool", outKey: "any" });
this.statements = ({ inputs, outputs }) => {
return [`${outputs.any} = any(${inputs.a});`];
};
}
}
export class All<T extends BoolTypes> extends UnaryOp<T, "bool", "all"> {
constructor({ a }: { a: DynoVal<T> }) {
super({ a, outTypeFunc: (aType: T) => "bool", outKey: "all" });
this.statements = ({ inputs, outputs }) => {
return [`${outputs.all} = all(${inputs.a});`];
};
}
}
export class Select<T extends SimpleTypes> extends TrinaryOp<
"bool",
T,
T,
T,
"select"
> {
constructor({
cond,
t,
f,
}: { cond: DynoVal<"bool">; t: DynoVal<T>; f: DynoVal<T> }) {
super({
a: cond,
b: t,
c: f,
outKey: "select",
outTypeFunc: (aType: "bool", bType: T, cType: T) => bType,
});
this.statements = ({ inputs, outputs }) => {
const { a: cond, b: t, c: f } = inputs;
return [`${outputs.select} = (${cond}) ? (${t}) : (${f});`];
};
}
}
type CompareOutput<T extends ValueTypes> = T extends ScalarTypes
? "bool"
: T extends "ivec2" | "uvec2" | "vec2"
? "bvec2"
: T extends "ivec3" | "uvec3" | "vec3"
? "bvec3"
: T extends "ivec4" | "uvec4" | "vec4"
? "bvec4"
: never;
function compareOutputType<T extends ValueTypes>(
type: T,
operator: string,
): CompareOutput<T> {
if (isScalarType(type)) {
return "bool" as CompareOutput<T>;
}
if (type === "ivec2" || type === "uvec2" || type === "vec2") {
return "bvec2" as CompareOutput<T>;
}
if (type === "ivec3" || type === "uvec3" || type === "vec3") {
return "bvec3" as CompareOutput<T>;
}
if (type === "ivec4" || type === "uvec4" || type === "vec4") {
return "bvec4" as CompareOutput<T>;
}
throw new Error(`Invalid ${operator} type: ${type}`);
}
type EqualOutput<A extends ValueTypes | BoolTypes> = A extends ScalarTypes
? "bool"
: A extends BoolTypes
? A
: A extends "ivec2" | "uvec2" | "vec2"
? "bvec2"
: A extends "ivec3" | "uvec3" | "vec3"
? "bvec3"
: A extends "ivec4" | "uvec4" | "vec4"
? "bvec4"
: never;
function equalOutputType<A extends ValueTypes | BoolTypes>(
type: A,
operator = "equal",
): EqualOutput<A> {
if (isScalarType(type)) {
return "bool" as EqualOutput<A>;
}
if (isBoolType(type)) {
return type as EqualOutput<A>;
}
if (type === "ivec2" || type === "uvec2" || type === "vec2") {
return "bvec2" as EqualOutput<A>;
}
if (type === "ivec3" || type === "uvec3" || type === "vec3") {
return "bvec3" as EqualOutput<A>;
}
if (type === "ivec4" || type === "uvec4" || type === "vec4") {
return "bvec4" as EqualOutput<A>;
}
throw new Error(`Invalid ${operator} type: ${type}`);
}
type NotEqualOutput<A extends ValueTypes | BoolTypes> = EqualOutput<A>;
function notEqualOutputType<A extends ValueTypes | BoolTypes>(
type: A,
): NotEqualOutput<A> {
return equalOutputType(type, "notEqual");
}
type CompXorOutput<A extends BoolTypes | AllIntTypes> = A extends BoolTypes
? "bool"
: A extends IntTypes
? "int"
: A extends UintTypes
? "uint"
: never;
function compXorOutputType<A extends BoolTypes | AllIntTypes>(
type: A,
): CompXorOutput<A> {
if (isBoolType(type)) {
return "bool" as CompXorOutput<A>;
}
if (isIntType(type)) {
return "int" as CompXorOutput<A>;
}
if (isUintType(type)) {
return "uint" as CompXorOutput<A>;
}
throw new Error(`Invalid compXor type: ${type}`);
}
export class CompXor<T extends BoolTypes | AllIntTypes> extends UnaryOp<
T,
CompXorOutput<T>,
"compXor"
> {
constructor({ a }: { a: DynoVal<T> }) {
const outType = compXorOutputType(valType(a));
super({ a, outTypeFunc: (aType: T) => outType, outKey: "compXor" });
this.statements = ({ inputs, outputs }) => {
if (isScalarType(this.outTypes.compXor)) {
return [`${outputs.compXor} = ${inputs.a};`];
}
const components = isVector2Type(outType)
? ["x", "y"]
: isVector3Type(outType)
? ["x", "y", "z"]
: ["x", "y", "z", "w"];
const operands = components.map((c) => `${inputs.a}.${c}`);
const operator = isBoolType(outType) ? "^^" : "^";
return [`${outputs.compXor} = ${operands.join(` ${operator} `)};`];
};
}
}
+534
View File
@@ -0,0 +1,534 @@
import { BinaryOp, Dyno, TrinaryOp, UnaryOp } from "./base";
import {
type AddOutput,
type ClampOutput,
type DivOutput,
type IModOutput,
type IsInfOutput,
type IsNanOutput,
type MaxOutput,
type MinOutput,
type MixOutput,
type ModOutput,
type MulOutput,
type SmoothstepOutput,
type StepOutput,
type SubOutput,
absOutputType,
addOutputType,
ceilOutputType,
clampOutputType,
divOutputType,
exp2OutputType,
expOutputType,
floorOutputType,
fractOutputType,
imodOutputType,
inversesqrtOutputType,
isInfOutputType,
isNanOutputType,
log2OutputType,
logOutputType,
maxOutputType,
minOutputType,
mixOutputType,
modOutputType,
modfOutputType,
mulOutputType,
negOutputType,
powOutputType,
roundOutputType,
signOutputType,
smoothstepOutputType,
sqrOutputType,
sqrtOutputType,
stepOutputType,
subOutputType,
truncOutputType,
} from "./mathTypes";
import type {
AllIntTypes,
AllSignedTypes,
AllValueTypes,
BoolTypes,
FloatTypes,
SignedTypes,
ValueTypes,
} from "./types";
import { type DynoVal, valType } from "./value";
export const add = <A extends AllValueTypes, B extends AllValueTypes>(
a: DynoVal<A>,
b: DynoVal<B>,
): DynoVal<AddOutput<A, B>> => new Add({ a, b });
export const sub = <A extends AllValueTypes, B extends AllValueTypes>(
a: DynoVal<A>,
b: DynoVal<B>,
): DynoVal<SubOutput<A, B>> => new Sub({ a, b });
export const mul = <A extends AllValueTypes, B extends AllValueTypes>(
a: DynoVal<A>,
b: DynoVal<B>,
): DynoVal<MulOutput<A, B>> => new Mul({ a, b });
export const div = <A extends AllValueTypes, B extends AllValueTypes>(
a: DynoVal<A>,
b: DynoVal<B>,
): DynoVal<DivOutput<A, B>> => new Div({ a, b });
export const imod = <A extends AllIntTypes, B extends AllIntTypes>(
a: DynoVal<A>,
b: DynoVal<B>,
): DynoVal<IModOutput<A, B>> => new IMod({ a, b });
export const mod = <A extends FloatTypes, B extends FloatTypes>(
a: DynoVal<A>,
b: DynoVal<B>,
): DynoVal<ModOutput<A, B>> => new Mod({ a, b });
export const modf = <A extends FloatTypes>(a: DynoVal<A>) =>
new Modf({ a }).outputs;
export const neg = <A extends AllSignedTypes>(a: DynoVal<A>): DynoVal<A> =>
new Neg({ a });
export const abs = <A extends SignedTypes>(a: DynoVal<A>): DynoVal<A> =>
new Abs({ a });
export const sign = <A extends SignedTypes>(a: DynoVal<A>): DynoVal<A> =>
new Sign({ a });
export const floor = <A extends FloatTypes>(a: DynoVal<A>): DynoVal<A> =>
new Floor({ a });
export const ceil = <A extends FloatTypes>(a: DynoVal<A>): DynoVal<A> =>
new Ceil({ a });
export const trunc = <A extends FloatTypes>(a: DynoVal<A>): DynoVal<A> =>
new Trunc({ a });
export const round = <A extends FloatTypes>(a: DynoVal<A>): DynoVal<A> =>
new Round({ a });
export const fract = <A extends FloatTypes>(a: DynoVal<A>): DynoVal<A> =>
new Fract({ a });
export const pow = <A extends FloatTypes>(
a: DynoVal<A>,
b: DynoVal<A>,
): DynoVal<A> => new Pow({ a, b });
export const exp = <A extends FloatTypes>(a: DynoVal<A>): DynoVal<A> =>
new Exp({ a });
export const exp2 = <A extends FloatTypes>(a: DynoVal<A>): DynoVal<A> =>
new Exp2({ a });
export const log = <A extends FloatTypes>(a: DynoVal<A>): DynoVal<A> =>
new Log({ a });
export const log2 = <A extends FloatTypes>(a: DynoVal<A>): DynoVal<A> =>
new Log2({ a });
export const sqr = <A extends ValueTypes>(a: DynoVal<A>): DynoVal<A> =>
new Sqr({ a });
export const sqrt = <A extends FloatTypes>(a: DynoVal<A>): DynoVal<A> =>
new Sqrt({ a });
export const inversesqrt = <A extends FloatTypes>(a: DynoVal<A>): DynoVal<A> =>
new InverseSqrt({ a });
export const min = <A extends ValueTypes, B extends ValueTypes>(
a: DynoVal<A>,
b: DynoVal<B>,
): DynoVal<MinOutput<A, B>> => new Min({ a, b });
export const max = <A extends ValueTypes, B extends ValueTypes>(
a: DynoVal<A>,
b: DynoVal<B>,
): DynoVal<MaxOutput<A, B>> => new Max({ a, b });
export const clamp = <A extends ValueTypes, MinMax extends ValueTypes>(
a: DynoVal<A>,
min: DynoVal<MinMax>,
max: DynoVal<MinMax>,
): DynoVal<ClampOutput<A, MinMax>> => new Clamp({ a, min, max });
export const mix = <A extends FloatTypes, T extends FloatTypes | BoolTypes>(
a: DynoVal<A>,
b: DynoVal<A>,
t: DynoVal<T>,
): DynoVal<MixOutput<A, T>> => new Mix({ a, b, t });
export const step = <A extends FloatTypes, B extends FloatTypes>(
edge: DynoVal<A>,
x: DynoVal<B>,
): DynoVal<StepOutput<A, B>> => new Step({ edge, x });
export const smoothstep = <X extends FloatTypes, Edge extends X | "float">(
edge0: DynoVal<Edge>,
edge1: DynoVal<Edge>,
x: DynoVal<X>,
): DynoVal<SmoothstepOutput<Edge, Edge, X>> =>
new Smoothstep({ edge0, edge1, x });
export const isNan = <A extends FloatTypes>(
a: DynoVal<A>,
): DynoVal<IsNanOutput<A>> => new IsNan({ a });
export const isInf = <A extends FloatTypes>(
a: DynoVal<A>,
): DynoVal<IsInfOutput<A>> => new IsInf({ a });
export class Add<
A extends AllValueTypes,
B extends AllValueTypes,
> extends BinaryOp<A, B, AddOutput<A, B>, "sum"> {
constructor({ a, b }: { a: DynoVal<A>; b: DynoVal<B> }) {
super({ a, b, outKey: "sum", outTypeFunc: addOutputType<A, B> });
this.statements = ({ inputs, outputs }) => {
return [`${outputs.sum} = ${inputs.a} + ${inputs.b};`];
};
}
}
export class Sub<
A extends AllValueTypes,
B extends AllValueTypes,
> extends BinaryOp<A, B, SubOutput<A, B>, "difference"> {
constructor({ a, b }: { a: DynoVal<A>; b: DynoVal<B> }) {
super({ a, b, outKey: "difference", outTypeFunc: subOutputType<A, B> });
this.statements = ({ inputs, outputs }) => {
return [`${outputs.difference} = ${inputs.a} - ${inputs.b};`];
};
}
}
export class Mul<
A extends AllValueTypes,
B extends AllValueTypes,
> extends BinaryOp<A, B, MulOutput<A, B>, "product"> {
constructor({ a, b }: { a: DynoVal<A>; b: DynoVal<B> }) {
super({ a, b, outKey: "product", outTypeFunc: mulOutputType<A, B> });
this.statements = ({ inputs, outputs }) => {
return [`${outputs.product} = ${inputs.a} * ${inputs.b};`];
};
}
}
export class Div<
A extends AllValueTypes,
B extends AllValueTypes,
> extends BinaryOp<A, B, DivOutput<A, B>, "quotient"> {
constructor({ a, b }: { a: DynoVal<A>; b: DynoVal<B> }) {
super({ a, b, outKey: "quotient", outTypeFunc: divOutputType<A, B> });
this.statements = ({ inputs, outputs }) => {
return [`${outputs.quotient} = ${inputs.a} / ${inputs.b};`];
};
}
}
export class IMod<
A extends AllIntTypes,
B extends AllIntTypes,
> extends BinaryOp<A, B, IModOutput<A, B>, "remainder"> {
constructor({ a, b }: { a: DynoVal<A>; b: DynoVal<B> }) {
super({ a, b, outKey: "remainder", outTypeFunc: imodOutputType<A, B> });
this.statements = ({ inputs, outputs }) => {
return [`${outputs.remainder} = ${inputs.a} % ${inputs.b};`];
};
}
}
export class Mod<A extends FloatTypes, B extends FloatTypes> extends BinaryOp<
A,
B,
ModOutput<A, B>,
"remainder"
> {
constructor({ a, b }: { a: DynoVal<A>; b: DynoVal<B> }) {
super({ a, b, outKey: "remainder", outTypeFunc: modOutputType<A, B> });
this.statements = ({ inputs, outputs }) => {
return [`${outputs.remainder} = mod(${inputs.a}, ${inputs.b});`];
};
}
}
export class Modf<A extends FloatTypes> extends Dyno<
{ a: A },
{ fract: A; integer: A }
> {
constructor({ a }: { a: DynoVal<A> }) {
const inTypes = { a: valType(a) };
const outType = modfOutputType<A>(inTypes.a);
const outTypes = {
fract: outType,
integer: outType,
};
super({ inTypes, outTypes, inputs: { a } });
this.statements = ({ inputs, outputs }) => {
return [`${outputs.fract} = modf(${inputs.a}, ${outputs.integer});`];
};
}
}
export class Neg<A extends AllSignedTypes> extends UnaryOp<A, A, "neg"> {
constructor({ a }: { a: DynoVal<A> }) {
super({ a, outKey: "neg", outTypeFunc: negOutputType<A> });
this.statements = ({ inputs, outputs }) => {
return [`${outputs.neg} = -${inputs.a};`];
};
}
}
export class Abs<A extends SignedTypes> extends UnaryOp<A, A, "abs"> {
constructor({ a }: { a: DynoVal<A> }) {
super({ a, outKey: "abs", outTypeFunc: absOutputType<A> });
this.statements = ({ inputs, outputs }) => {
return [`${outputs.abs} = abs(${inputs.a});`];
};
}
}
export class Sign<A extends SignedTypes> extends UnaryOp<A, A, "sign"> {
constructor({ a }: { a: DynoVal<A> }) {
super({ a, outKey: "sign", outTypeFunc: signOutputType<A> });
this.statements = ({ inputs, outputs }) => {
return [`${outputs.sign} = sign(${inputs.a});`];
};
}
}
export class Floor<A extends FloatTypes> extends UnaryOp<A, A, "floor"> {
constructor({ a }: { a: DynoVal<A> }) {
super({ a, outKey: "floor", outTypeFunc: floorOutputType<A> });
this.statements = ({ inputs, outputs }) => {
return [`${outputs.floor} = floor(${inputs.a});`];
};
}
}
export class Ceil<A extends FloatTypes> extends UnaryOp<A, A, "ceil"> {
constructor({ a }: { a: DynoVal<A> }) {
super({ a, outKey: "ceil", outTypeFunc: ceilOutputType<A> });
this.statements = ({ inputs, outputs }) => {
return [`${outputs.ceil} = ceil(${inputs.a});`];
};
}
}
export class Trunc<A extends FloatTypes> extends UnaryOp<A, A, "trunc"> {
constructor({ a }: { a: DynoVal<A> }) {
super({ a, outKey: "trunc", outTypeFunc: truncOutputType<A> });
this.statements = ({ inputs, outputs }) => {
return [`${outputs.trunc} = trunc(${inputs.a});`];
};
}
}
export class Round<A extends FloatTypes> extends UnaryOp<A, A, "round"> {
constructor({ a }: { a: DynoVal<A> }) {
super({ a, outKey: "round", outTypeFunc: roundOutputType<A> });
this.statements = ({ inputs, outputs }) => {
return [`${outputs.round} = round(${inputs.a});`];
};
}
}
export class Fract<A extends FloatTypes> extends UnaryOp<A, A, "fract"> {
constructor({ a }: { a: DynoVal<A> }) {
super({ a, outKey: "fract", outTypeFunc: fractOutputType<A> });
this.statements = ({ inputs, outputs }) => {
return [`${outputs.fract} = fract(${inputs.a});`];
};
}
}
export class Pow<A extends FloatTypes> extends BinaryOp<A, A, A, "power"> {
constructor({ a, b }: { a: DynoVal<A>; b: DynoVal<A> }) {
super({ a, b, outKey: "power", outTypeFunc: powOutputType<A> });
this.statements = ({ inputs, outputs }) => {
return [`${outputs.power} = pow(${inputs.a}, ${inputs.b});`];
};
}
}
export class Exp<A extends FloatTypes> extends UnaryOp<A, A, "exp"> {
constructor({ a }: { a: DynoVal<A> }) {
super({ a, outKey: "exp", outTypeFunc: expOutputType<A> });
this.statements = ({ inputs, outputs }) => {
return [`${outputs.exp} = exp(${inputs.a});`];
};
}
}
export class Exp2<A extends FloatTypes> extends UnaryOp<A, A, "exp2"> {
constructor({ a }: { a: DynoVal<A> }) {
super({ a, outKey: "exp2", outTypeFunc: exp2OutputType<A> });
this.statements = ({ inputs, outputs }) => {
return [`${outputs.exp2} = exp2(${inputs.a});`];
};
}
}
export class Log<A extends FloatTypes> extends UnaryOp<A, A, "log"> {
constructor({ a }: { a: DynoVal<A> }) {
super({ a, outKey: "log", outTypeFunc: logOutputType<A> });
this.statements = ({ inputs, outputs }) => {
return [`${outputs.log} = log(${inputs.a});`];
};
}
}
export class Log2<A extends FloatTypes> extends UnaryOp<A, A, "log2"> {
constructor({ a }: { a: DynoVal<A> }) {
super({ a, outKey: "log2", outTypeFunc: log2OutputType<A> });
this.statements = ({ inputs, outputs }) => {
return [`${outputs.log2} = log2(${inputs.a});`];
};
}
}
export class Sqr<A extends ValueTypes> extends UnaryOp<A, A, "sqr"> {
constructor({ a }: { a: DynoVal<A> }) {
super({ a, outKey: "sqr", outTypeFunc: sqrOutputType<A> });
this.statements = ({ inputs, outputs }) => {
return [`${outputs.sqr} = ${inputs.a} * ${inputs.a};`];
};
}
}
export class Sqrt<A extends FloatTypes> extends UnaryOp<A, A, "sqrt"> {
constructor({ a }: { a: DynoVal<A> }) {
super({ a, outKey: "sqrt", outTypeFunc: sqrtOutputType<A> });
this.statements = ({ inputs, outputs }) => {
return [`${outputs.sqrt} = sqrt(${inputs.a});`];
};
}
}
export class InverseSqrt<A extends FloatTypes> extends UnaryOp<
A,
A,
"inversesqrt"
> {
constructor({ a }: { a: DynoVal<A> }) {
super({ a, outKey: "inversesqrt", outTypeFunc: inversesqrtOutputType<A> });
this.statements = ({ inputs, outputs }) => {
return [`${outputs.inversesqrt} = inversesqrt(${inputs.a});`];
};
}
}
export class Min<A extends ValueTypes, B extends ValueTypes> extends BinaryOp<
A,
B,
MinOutput<A, B>,
"min"
> {
constructor({ a, b }: { a: DynoVal<A>; b: DynoVal<B> }) {
super({ a, b, outKey: "min", outTypeFunc: minOutputType<A, B> });
this.statements = ({ inputs, outputs }) => {
return [`${outputs.min} = min(${inputs.a}, ${inputs.b});`];
};
}
}
export class Max<A extends ValueTypes, B extends ValueTypes> extends BinaryOp<
A,
B,
MaxOutput<A, B>,
"max"
> {
constructor({ a, b }: { a: DynoVal<A>; b: DynoVal<B> }) {
super({ a, b, outKey: "max", outTypeFunc: maxOutputType<A, B> });
this.statements = ({ inputs, outputs }) => {
return [`${outputs.max} = max(${inputs.a}, ${inputs.b});`];
};
}
}
export class Clamp<
A extends ValueTypes,
MinMax extends ValueTypes,
> extends TrinaryOp<A, MinMax, MinMax, ClampOutput<A, MinMax>, "clamp"> {
constructor({
a,
min,
max,
}: { a: DynoVal<A>; min: DynoVal<MinMax>; max: DynoVal<MinMax> }) {
super({
a,
b: min,
c: max,
outKey: "clamp",
outTypeFunc: clampOutputType<A, MinMax>,
});
this.statements = ({ inputs, outputs }) => {
const { a, b: min, c: max } = inputs;
return [`${outputs.clamp} = clamp(${a}, ${min}, ${max});`];
};
}
}
export class Mix<
A extends FloatTypes,
T extends FloatTypes | BoolTypes,
> extends TrinaryOp<A, A, T, MixOutput<A, T>, "mix"> {
constructor({ a, b, t }: { a: DynoVal<A>; b: DynoVal<A>; t: DynoVal<T> }) {
super({ a, b, c: t, outKey: "mix", outTypeFunc: mixOutputType<A, T> });
this.statements = ({ inputs, outputs }) => {
const { a, b, c: t } = inputs;
return [`${outputs.mix} = mix(${a}, ${b}, ${t});`];
};
}
}
export class Step<
Edge extends FloatTypes,
X extends FloatTypes,
> extends BinaryOp<Edge, X, StepOutput<Edge, X>, "step"> {
constructor({ edge, x }: { edge: DynoVal<Edge>; x: DynoVal<X> }) {
super({
a: edge,
b: x,
outKey: "step",
outTypeFunc: stepOutputType<Edge, X>,
});
this.statements = ({ inputs, outputs }) => {
const { a: edge, b: x } = inputs;
return [`${outputs.step} = step(${edge}, ${x});`];
};
}
}
export class Smoothstep<
X extends FloatTypes,
Edge extends X | "float",
> extends TrinaryOp<
Edge,
Edge,
X,
SmoothstepOutput<Edge, Edge, X>,
"smoothstep"
> {
constructor({
edge0,
edge1,
x,
}: { edge0: DynoVal<Edge>; edge1: DynoVal<Edge>; x: DynoVal<X> }) {
super({
a: edge0,
b: edge1,
c: x,
outKey: "smoothstep",
outTypeFunc: smoothstepOutputType<Edge, Edge, X>,
});
this.statements = ({ inputs, outputs }) => {
const { a: edge0, b: edge1, c: x } = inputs;
return [`${outputs.smoothstep} = smoothstep(${edge0}, ${edge1}, ${x});`];
};
}
}
export class IsNan<A extends FloatTypes> extends UnaryOp<
A,
IsNanOutput<A>,
"isNan"
> {
constructor({ a }: { a: DynoVal<A> }) {
super({ a, outKey: "isNan", outTypeFunc: isNanOutputType<A> });
this.statements = ({ inputs, outputs }) => {
return [`${outputs.isNan} = isNan(${inputs.a});`];
};
}
}
export class IsInf<A extends FloatTypes> extends UnaryOp<
A,
IsInfOutput<A>,
"isInf"
> {
constructor({ a }: { a: DynoVal<A> }) {
super({ a, outKey: "isInf", outTypeFunc: isInfOutputType<A> });
this.statements = ({ inputs, outputs }) => {
return [`${outputs.isInf} = isInf(${inputs.a});`];
};
}
}
+717
View File
@@ -0,0 +1,717 @@
import {
type AllFloatTypes,
type AllIntTypes,
type AllSignedTypes,
type AllValueTypes,
type BaseType,
type BoolTypes,
type FloatTypes,
type IntTypes,
type SignedTypes,
type UintTypes,
type ValueTypes,
isAllFloatType,
isFloatType,
isIntType,
isMat2,
isMat3,
isMat4,
isUintType,
} from "./types";
export type AddOutput<
A extends AllValueTypes,
B extends AllValueTypes,
> = BaseType &
(A extends B
? A
: A extends "int"
? B extends IntTypes
? B
: never
: B extends "int"
? A extends IntTypes
? A
: never
: A extends "uint"
? B extends UintTypes
? B
: never
: B extends "uint"
? A extends UintTypes
? A
: never
: A extends "float"
? B extends AllFloatTypes
? B
: never
: B extends "float"
? A extends AllFloatTypes
? A
: never
: never);
export type SubOutput<
A extends AllValueTypes,
B extends AllValueTypes,
> = AddOutput<A, B>;
export type MulOutput<
A extends AllValueTypes,
B extends AllValueTypes,
> = BaseType &
(A extends "int"
? B extends IntTypes
? B
: never
: B extends "int"
? A extends IntTypes
? A
: never
: A extends "uint"
? B extends UintTypes
? B
: never
: B extends "uint"
? A extends UintTypes
? A
: never
: A extends "float"
? B extends AllFloatTypes
? B
: never
: B extends "float"
? A extends AllFloatTypes
? A
: never
: A extends IntTypes
? B extends A
? A
: never
: B extends IntTypes
? A extends B
? A
: never
: A extends UintTypes
? B extends A
? A
: never
: B extends UintTypes
? A extends B
? A
: never
: // Vector * Matrix/Vector
A extends "vec2"
? B extends "vec2" | "mat2" | "mat2x2"
? "vec2"
: B extends "mat3x2"
? "vec3"
: B extends "mat4x2"
? "vec4"
: never
: A extends "vec3"
? B extends "mat2x3"
? "vec2"
: B extends "vec3" | "mat3" | "mat3x3"
? "vec3"
: B extends "mat4x3"
? "vec4"
: never
: A extends "vec4"
? B extends "mat2x4"
? "vec2"
: B extends "mat3x4"
? "vec3"
: B extends "vec4" | "mat4" | "mat4x4"
? "vec4"
: never
: // Matrix * Vector
B extends "vec2"
? A extends "mat2" | "mat2x2"
? "vec2"
: A extends "mat2x3"
? "vec3"
: A extends "mat2x4"
? "vec4"
: never
: B extends "vec3"
? A extends "mat3x2"
? "vec2"
: A extends "mat3" | "mat3x3"
? "vec3"
: A extends "mat3x4"
? "vec4"
: never
: B extends "vec4"
? A extends "mat4x2"
? "vec2"
: A extends "mat4x3"
? "vec3"
: A extends "mat4" | "mat4x4"
? "vec4"
: never
: // Matrix * Matrix: mat{Acols}x{Arows} * mat{Bcols}x{Brows} => mat{Bcols}x{Arows}
A extends "mat2" | "mat2x2" // Acols = 2 => Brows = 2
? B extends "mat2" | "mat2x2"
? "mat2"
: B extends "mat3x2"
? "mat3x2"
: B extends "mat4x2"
? "mat4x2"
: never
: A extends "mat2x3" // Acols = 2 => Brows = 2
? B extends "mat2" | "mat2x2"
? "mat2x3"
: B extends "mat3x2"
? "mat3"
: B extends "mat4x2"
? "mat4x3"
: never
: A extends "mat2x4" // Acols = 2 => Brows = 2
? B extends "mat2" | "mat2x2"
? "mat2x4"
: B extends "mat3x2"
? "mat3x4"
: B extends "mat4x2"
? "mat4"
: never
: A extends "mat3x2" // Acols = 3 => Brows = 3
? B extends "mat2x3"
? "mat2"
: B extends "mat3" | "mat3x3"
? "mat3x2"
: B extends "mat4x3"
? "mat4x2"
: never
: A extends "mat3" | "mat3x3" // Acols = 3 => Brows = 3
? B extends "mat2x3"
? "mat2x3"
: B extends "mat3" | "mat3x3"
? "mat3"
: B extends "mat4x3"
? "mat4x3"
: never
: A extends "mat3x4" // Acols = 3 => Brows = 3
? B extends "mat2x3"
? "mat2x4"
: B extends "mat3" | "mat3x3"
? "mat3x4"
: B extends "mat4x3"
? "mat4"
: never
: A extends "mat4x2" // Acols = 4 => Brows = 4
? B extends "mat2x4"
? "mat2"
: B extends "mat3x4"
? "mat3x2"
: B extends
| "mat4"
| "mat4x4"
? "mat4x2"
: never
: A extends "mat4x3" // Acols = 4 => Brows = 4
? B extends "mat2x4"
? "mat2x3"
: B extends "mat3x4"
? "mat3"
: B extends
| "mat4"
| "mat4x4"
? "mat4x3"
: never
: A extends "mat4" | "mat4x4" // Acols = 4 => Brows = 4
? B extends "mat2x4"
? "mat2x4"
: B extends "mat3x4"
? "mat3x4"
: B extends
| "mat4"
| "mat4x4"
? "mat4"
: never
: never);
export type DivOutput<
A extends AllValueTypes,
B extends AllValueTypes,
> = AddOutput<A, B>;
export type IModOutput<
A extends AllIntTypes,
B extends AllIntTypes,
> = BaseType &
(A extends B
? A
: A extends "int"
? B extends IntTypes
? B
: never
: B extends "int"
? A extends IntTypes
? A
: never
: A extends "uint"
? B extends UintTypes
? B
: never
: B extends "uint"
? A extends UintTypes
? A
: never
: never);
export type ModOutput<A extends FloatTypes, B extends FloatTypes> = BaseType &
(A extends B ? A : B extends "float" ? A : never);
export type PowOutput<A extends FloatTypes, B extends FloatTypes> = BaseType &
(A extends B ? A : never);
export type MinOutput<A extends ValueTypes, B extends ValueTypes> = BaseType &
(A extends B
? A
: B extends "float"
? A extends FloatTypes
? A
: never
: B extends "int"
? A extends IntTypes
? A
: never
: B extends "uint"
? A extends UintTypes
? A
: never
: never);
export type MaxOutput<A extends ValueTypes, B extends ValueTypes> = MinOutput<
A,
B
>;
export type ClampOutput<A extends ValueTypes, B extends ValueTypes> = BaseType &
(B extends "float"
? A extends FloatTypes
? A
: never
: B extends "int"
? A extends IntTypes
? A
: never
: B extends "uint"
? A extends UintTypes
? A
: never
: never);
export type MixOutput<
A extends FloatTypes,
T extends FloatTypes | BoolTypes,
> = BaseType &
(T extends A
? A
: T extends "float"
? A
: T extends "bool"
? A extends "float"
? A
: never
: T extends "bvec2"
? A extends "vec2"
? A
: never
: T extends "bvec3"
? A extends "vec3"
? A
: never
: T extends "bvec4"
? A extends "vec4"
? A
: never
: never);
export type StepOutput<A extends FloatTypes, B extends FloatTypes> = BaseType &
(A extends B ? B : A extends "float" ? B : never);
export type SmoothstepOutput<
A extends FloatTypes,
B extends FloatTypes,
C extends FloatTypes,
> = BaseType &
(A extends B ? (A extends C ? C : A extends "float" ? C : never) : never);
export type IsNanOutput<A extends FloatTypes> = BaseType &
(A extends "float"
? "bool"
: A extends "vec2"
? "bvec2"
: A extends "vec3"
? "bvec3"
: A extends "vec4"
? "bvec4"
: never);
export type IsInfOutput<A extends FloatTypes> = IsNanOutput<A>;
// // Run-time type helper functions
export function addOutputType<A extends AllValueTypes, B extends AllValueTypes>(
a: A,
b: B,
operation = "add",
): AddOutput<A, B> {
const error = () => {
throw new Error(`Invalid ${operation} types: ${a}, ${b}`);
};
// @ts-ignore
if (a === b) return a as AddOutput<A, B>;
if (a === "int") {
if (isIntType(b)) return b as AddOutput<A, B>;
error();
}
if (b === "int") {
if (isIntType(a)) return a as AddOutput<A, B>;
error();
}
if (a === "uint") {
if (isUintType(b)) return b as AddOutput<A, B>;
error();
}
if (b === "uint") {
if (isUintType(a)) return a as AddOutput<A, B>;
error();
}
if (a === "float") {
if (isAllFloatType(b)) return b as AddOutput<A, B>;
error();
}
if (b === "float") {
if (isAllFloatType(a)) return a as AddOutput<A, B>;
error();
}
throw new Error(`Invalid ${operation} types: ${a}, ${b}`);
}
export function subOutputType<A extends AllValueTypes, B extends AllValueTypes>(
a: A,
b: B,
): SubOutput<A, B> {
return addOutputType(a, b, "sub");
}
export function mulOutputType<A extends AllValueTypes, B extends AllValueTypes>(
a: A,
b: B,
): MulOutput<A, B> {
const error = () => {
throw new Error(`Invalid mul types: ${a}, ${b}`);
};
const result = (value: unknown) => value as MulOutput<A, B>;
if (a === "int") {
if (isIntType(b)) return result(b);
error();
}
if (b === "int") {
if (isIntType(a)) return result(a);
error();
}
if (a === "uint") {
if (isUintType(b)) return result(b);
error();
}
if (b === "uint") {
if (isUintType(a)) return result(a);
error();
}
if (a === "float") {
if (isAllFloatType(b)) return result(b);
error();
}
if (b === "float") {
if (isAllFloatType(a)) return result(a);
error();
}
if (isIntType(a) || isUintType(a) || isIntType(b) || isUintType(b)) {
// @ts-ignore
if (a === b) return result(a);
error();
}
// Vector * Matrix/Vector
if (a === "vec2") {
if (b === "vec2" || isMat2(b)) return result("vec2");
if (b === "mat3x2") return result("vec3");
if (b === "mat4x2") return result("vec4");
error();
}
if (a === "vec3") {
if (b === "mat2x3") return result("vec2");
if (b === "vec3" || isMat3(b)) return result("vec3");
if (b === "mat4x3") return result("vec4");
error();
}
if (a === "vec4") {
if (b === "mat2x4") return result("vec2");
if (b === "mat3x4") return result("vec3");
if (b === "vec4" || isMat4(b)) return result("vec4");
error();
}
// Matrix * Vector
if (b === "vec2") {
if (isMat2(a)) return result("vec2");
if (a === "mat2x3") return result("vec3");
if (a === "mat2x4") return result("vec4");
error();
}
if (b === "vec3") {
if (a === "mat3x2") return result("vec2");
if (isMat3(a)) return result("vec3");
if (a === "mat3x4") return result("vec4");
error();
}
if (b === "vec4") {
if (a === "mat4x2") return result("vec2");
if (a === "mat4x3") return result("vec3");
if (isMat4(a)) return result("vec4");
error();
}
// Matrix * Matrix: mat{Acols}x{Arows} * mat{Bcols}x{Brows} => mat{Bcols}x{Arows}
if (isMat2(a)) {
if (isMat2(b)) return result("mat2");
if (b === "mat3x2") return result("mat3x2");
if (b === "mat4x2") return result("mat4x2");
error();
}
if (a === "mat2x3") {
if (isMat2(b)) return result("mat2x3");
if (b === "mat3x2") return result("mat3");
if (b === "mat4x2") return result("mat4x3");
error();
}
if (a === "mat2x4") {
if (isMat2(b)) return result("mat2x4");
if (b === "mat3x2") return result("mat3x4");
if (b === "mat4x2") return result("mat4");
error();
}
if (a === "mat3x2") {
if (b === "mat2x3") return result("mat2");
if (isMat3(b)) return result("mat3x2");
if (b === "mat4x3") return result("mat4x2");
error();
}
if (isMat3(a)) {
if (b === "mat2x3") return result("mat2x3");
if (isMat3(b)) return result("mat3");
if (b === "mat4x3") return result("mat4x3");
error();
}
if (a === "mat3x4") {
if (b === "mat2x3") return result("mat2x4");
if (isMat3(b)) return result("mat3x4");
if (b === "mat4x3") return result("mat4");
error();
}
if (a === "mat4x2") {
if (b === "mat2x4") return result("mat2");
if (b === "mat3x4") return result("mat3x2");
if (isMat4(b)) return result("mat4x2");
error();
}
if (a === "mat4x3") {
if (b === "mat2x4") return result("mat2x3");
if (b === "mat3x4") return result("mat3");
if (isMat4(b)) return result("mat4x3");
error();
}
if (isMat4(a)) {
if (b === "mat2x4") return result("mat2x4");
if (b === "mat3x4") return result("mat3x4");
if (isMat4(b)) return result("mat4");
error();
}
throw new Error(`Invalid mul types: ${a}, ${b}`);
}
export function divOutputType<A extends AllValueTypes, B extends AllValueTypes>(
a: A,
b: B,
): DivOutput<A, B> {
return addOutputType(a, b, "div");
}
export function imodOutputType<A extends AllIntTypes, B extends AllIntTypes>(
a: A,
b: B,
): IModOutput<A, B> {
// @ts-ignore
if (a === b) return a as IModOutput<A, B>;
if (a === "int") {
if (isIntType(b)) return b as IModOutput<A, B>;
} else if (b === "int") {
if (isIntType(a)) return a as IModOutput<A, B>;
} else if (a === "uint") {
if (isUintType(b)) return b as IModOutput<A, B>;
} else if (b === "uint") {
if (isUintType(a)) return a as IModOutput<A, B>;
}
throw new Error(`Invalid imod types: ${a}, ${b}`);
}
export function modOutputType<A extends FloatTypes, B extends FloatTypes>(
a: A,
b: B,
): ModOutput<A, B> {
// @ts-ignore
if (a === b || b === "float") return a as ModOutput<A, B>;
throw new Error(`Invalid mod types: ${a}, ${b}`);
}
export function modfOutputType<A extends FloatTypes>(a: A): A {
return a;
}
export function negOutputType<A extends AllSignedTypes>(a: A): A {
return a;
}
export function absOutputType<A extends SignedTypes>(a: A): A {
return a;
}
export function signOutputType<A extends SignedTypes>(a: A): A {
return a;
}
export function floorOutputType<A extends FloatTypes>(a: A): A {
return a;
}
export function ceilOutputType<A extends FloatTypes>(a: A): A {
return a;
}
export function truncOutputType<A extends FloatTypes>(a: A): A {
return a;
}
export function roundOutputType<A extends FloatTypes>(a: A): A {
return a;
}
export function fractOutputType<A extends FloatTypes>(a: A): A {
return a;
}
export function powOutputType<A extends FloatTypes>(a: A): A {
return a;
}
export function expOutputType<A extends FloatTypes>(a: A): A {
return a;
}
export function exp2OutputType<A extends FloatTypes>(a: A): A {
return a;
}
export function logOutputType<A extends FloatTypes>(a: A): A {
return a;
}
export function log2OutputType<A extends FloatTypes>(a: A): A {
return a;
}
export function sqrOutputType<A extends ValueTypes>(a: A): A {
return a;
}
export function sqrtOutputType<A extends FloatTypes>(a: A): A {
return a;
}
export function inversesqrtOutputType<A extends FloatTypes>(a: A): A {
return a;
}
export function minOutputType<A extends ValueTypes, B extends ValueTypes>(
a: A,
b: B,
operation = "min",
): MinOutput<A, B> {
// @ts-ignore
if (a === b) return a as MinOutput<A, B>;
if (b === "float") {
if (isFloatType(a)) return a as MinOutput<A, B>;
} else if (b === "int") {
if (isIntType(a)) return a as MinOutput<A, B>;
} else if (b === "uint") {
if (isUintType(a)) return a as MinOutput<A, B>;
}
throw new Error(`Invalid ${operation} types: ${a}, ${b}`);
}
export function maxOutputType<A extends ValueTypes, B extends ValueTypes>(
a: A,
b: B,
): MaxOutput<A, B> {
return minOutputType(a, b, "max");
}
export function clampOutputType<A extends ValueTypes, B extends ValueTypes>(
a: A,
b: B,
_c: B,
): ClampOutput<A, B> {
if (b === "float") {
if (isFloatType(a)) return a as ClampOutput<A, B>;
} else if (b === "int") {
if (isIntType(a)) return a as ClampOutput<A, B>;
} else if (b === "uint") {
if (isUintType(a)) return a as ClampOutput<A, B>;
}
throw new Error(`Invalid clamp types: ${a}, ${b}`);
}
export function mixOutputType<
A extends FloatTypes,
C extends FloatTypes | BoolTypes,
>(a: A, b: A, c: C): MixOutput<A, C> {
// @ts-ignore
if (c === a) return a as MixOutput<A, C>;
if (c === "float") return a as MixOutput<A, C>;
if (c === "bool" && a === "float") return a as MixOutput<A, C>;
if (c === "bvec2" && a === "vec2") return a as MixOutput<A, C>;
if (c === "bvec3" && a === "vec3") return a as MixOutput<A, C>;
if (c === "bvec4" && a === "vec4") return a as MixOutput<A, C>;
throw new Error(`Invalid mix types: ${a}, ${b}, ${c}`);
}
export function stepOutputType<A extends FloatTypes, B extends FloatTypes>(
a: A,
b: B,
): StepOutput<A, B> {
// @ts-ignore
if (a === b || b === "float") return b as StepOutput<A, B>;
throw new Error(`Invalid step types: ${a}, ${b}`);
}
export function smoothstepOutputType<
A extends FloatTypes,
B extends FloatTypes,
C extends FloatTypes,
>(a: A, b: B, c: C): SmoothstepOutput<A, B, C> {
// @ts-ignore
if (a === b) {
if (a === c || a === "float") return c as SmoothstepOutput<A, B, C>;
}
throw new Error(`Invalid smoothstep types: ${a}, ${b}, ${c}`);
}
export function isNanOutputType<A extends FloatTypes>(
a: A,
operation = "isNan",
): IsNanOutput<A> {
if (a === "float") return "bool" as IsNanOutput<A>;
if (a === "vec2") return "bvec2" as IsNanOutput<A>;
if (a === "vec3") return "bvec3" as IsNanOutput<A>;
if (a === "vec4") return "bvec4" as IsNanOutput<A>;
throw new Error(`Invalid ${operation} types: ${a}`);
}
export function isInfOutputType<A extends FloatTypes>(a: A): IsInfOutput<A> {
return isNanOutputType(a, "isInf");
}
+66
View File
@@ -0,0 +1,66 @@
import { Dyno, unindentLines } from "./base";
import { Gsplat, defineGsplat } from "./splats";
import {
DynoOutput,
type DynoVal,
type DynoValue,
type HasDynoOut,
} from "./value";
export const outputPackedSplat = (gsplat: DynoVal<typeof Gsplat>) =>
new OutputPackedSplat({ gsplat });
export const outputRgba8 = (rgba8: DynoVal<"vec4">) =>
new OutputRgba8({ rgba8 });
export class OutputPackedSplat
extends Dyno<{ gsplat: typeof Gsplat }, { output: "uvec4" }>
implements HasDynoOut<"uvec4">
{
constructor({ gsplat }: { gsplat?: DynoVal<typeof Gsplat> }) {
super({
inTypes: { gsplat: Gsplat },
inputs: { gsplat },
globals: () => [defineGsplat],
statements: ({ inputs, outputs }) => {
const { output } = outputs;
if (!output) {
return [];
}
const { gsplat } = inputs;
if (gsplat) {
return unindentLines(`
if (isGsplatActive(${gsplat}.flags)) {
${output} = packSplat(${gsplat}.center, ${gsplat}.scales, ${gsplat}.quaternion, ${gsplat}.rgba);
} else {
${output} = uvec4(0u, 0u, 0u, 0u);
}
`);
}
return [`${output} = uvec4(0u, 0u, 0u, 0u);`];
},
});
}
dynoOut(): DynoValue<"uvec4"> {
return new DynoOutput(this, "output");
}
}
export class OutputRgba8
extends Dyno<{ rgba8: "vec4" }, { rgba8: "vec4" }>
implements HasDynoOut<"vec4">
{
constructor({ rgba8 }: { rgba8?: DynoVal<"vec4"> }) {
super({
inTypes: { rgba8: "vec4" },
inputs: { rgba8 },
statements: ({ inputs, outputs }) => [
`target = ${inputs.rgba8 ?? "vec4(0.0, 0.0, 0.0, 0.0)"};`,
],
});
}
dynoOut(): DynoValue<"vec4"> {
return new DynoOutput(this, "rgba8");
}
}
+117
View File
@@ -0,0 +1,117 @@
import * as THREE from "three";
import { IDENT_VERTEX_SHADER } from "../utils";
import { Compilation, type Dyno, type IOTypes } from "./base";
export class DynoProgram {
graph: Dyno<IOTypes, IOTypes>;
template: DynoProgramTemplate;
inputs: Record<string, string>;
outputs: Record<string, string>;
shader: string;
uniforms: Record<string, THREE.IUniform>;
updaters: (() => void)[];
constructor({
graph,
inputs,
outputs,
template,
}: {
graph: Dyno<IOTypes, IOTypes>;
inputs?: Record<string, string>;
outputs?: Record<string, string>;
template: DynoProgramTemplate;
}) {
this.graph = graph;
this.template = template;
this.inputs = inputs ?? {};
this.outputs = outputs ?? {};
const compile = new Compilation({ indent: this.template.indent });
for (const key in this.outputs) {
if (this.outputs[key]) {
compile.declares.add(this.outputs[key]);
}
}
const statements = graph.compile({
inputs: this.inputs,
outputs: this.outputs,
compile,
});
this.shader = template.generate({ globals: compile.globals, statements });
this.uniforms = compile.uniforms;
this.updaters = compile.updaters;
// console.log("*** COMPILED SHADER", this.shader);
// console.log("*** UNIFORMS", this.uniforms);
}
prepareMaterial(): THREE.RawShaderMaterial {
return getMaterial(this);
}
update() {
for (const updater of this.updaters) {
updater();
}
}
}
export class DynoProgramTemplate {
before: string;
between: string;
after: string;
indent: string;
constructor(template: string) {
const globals = template.match(/^([ \t]*)\{\{\s*GLOBALS\s*\}\}/m);
const statements = template.match(/^([ \t]*)\{\{\s*STATEMENTS\s*\}\}/m);
if (!globals || !statements) {
throw new Error(
"Template must contain {{ GLOBALS }} and {{ STATEMENTS }}",
);
}
this.before = template.substring(0, globals.index);
this.between = template.substring(
(globals.index as number) + globals[0].length,
statements.index,
);
this.after = template.substring(
(statements.index as number) + statements[0].length,
);
this.indent = statements[1];
}
generate({
globals,
statements,
}: { globals: Set<string>; statements: string[] }): string {
return (
this.before +
Array.from(globals).join("\n\n") +
this.between +
statements.map((s) => this.indent + s).join("\n") +
this.after
);
}
}
const programMaterial = new Map<DynoProgram, THREE.RawShaderMaterial>();
function getMaterial(program: DynoProgram): THREE.RawShaderMaterial {
let material = programMaterial.get(program);
if (material) {
return material;
}
material = new THREE.RawShaderMaterial({
glslVersion: THREE.GLSL3,
vertexShader: IDENT_VERTEX_SHADER,
fragmentShader: program.shader,
uniforms: program.uniforms,
});
programMaterial.set(program, material);
return material;
}
+593
View File
@@ -0,0 +1,593 @@
import { Dyno, UnaryOp, unindent, unindentLines } from "./base";
import {
DynoOutput,
type DynoVal,
type DynoValue,
type HasDynoOut,
} from "./value";
export const Gsplat = { type: "Gsplat" } as { type: "Gsplat" };
export const TPackedSplats = { type: "PackedSplats" } as {
type: "PackedSplats";
};
export const numPackedSplats = (
packedSplats: DynoVal<typeof TPackedSplats>,
): DynoVal<"int"> => new NumPackedSplats({ packedSplats });
export const readPackedSplat = (
packedSplats: DynoVal<typeof TPackedSplats>,
index: DynoVal<"int">,
): DynoVal<typeof Gsplat> => new ReadPackedSplat({ packedSplats, index });
export const readPackedSplatRange = (
packedSplats: DynoVal<typeof TPackedSplats>,
index: DynoVal<"int">,
base: DynoVal<"int">,
count: DynoVal<"int">,
): DynoVal<typeof Gsplat> =>
new ReadPackedSplatRange({ packedSplats, index, base, count });
export const splitGsplat = (gsplat: DynoVal<typeof Gsplat>) =>
new SplitGsplat({ gsplat });
export const combineGsplat = ({
gsplat,
flags,
index,
center,
scales,
quaternion,
rgba,
rgb,
opacity,
x,
y,
z,
r,
g,
b,
}: {
gsplat?: DynoVal<typeof Gsplat>;
flags?: DynoVal<"uint">;
index?: DynoVal<"int">;
center?: DynoVal<"vec3">;
scales?: DynoVal<"vec3">;
quaternion?: DynoVal<"vec4">;
rgba?: DynoVal<"vec4">;
rgb?: DynoVal<"vec3">;
opacity?: DynoVal<"float">;
x?: DynoVal<"float">;
y?: DynoVal<"float">;
z?: DynoVal<"float">;
r?: DynoVal<"float">;
g?: DynoVal<"float">;
b?: DynoVal<"float">;
}): DynoVal<typeof Gsplat> => {
return new CombineGsplat({
gsplat,
flags,
index,
center,
scales,
quaternion,
rgba,
rgb,
opacity,
x,
y,
z,
r,
g,
b,
});
};
export const gsplatNormal = (gsplat: DynoVal<typeof Gsplat>): DynoVal<"vec3"> =>
new GsplatNormal({ gsplat });
export const transformGsplat = (
gsplat: DynoVal<typeof Gsplat>,
{
scale,
rotate,
translate,
recolor,
}: {
scale?: DynoVal<"float">;
rotate?: DynoVal<"vec4">;
translate?: DynoVal<"vec3">;
recolor?: DynoVal<"vec4">;
},
): DynoVal<typeof Gsplat> => {
return new TransformGsplat({ gsplat, scale, rotate, translate, recolor });
};
export const defineGsplat = unindent(`
struct Gsplat {
vec3 center;
uint flags;
vec3 scales;
int index;
vec4 quaternion;
vec4 rgba;
};
const uint GSPLAT_FLAG_ACTIVE = 1u << 0u;
bool isGsplatActive(uint flags) {
return (flags & GSPLAT_FLAG_ACTIVE) != 0u;
}
`);
export const definePackedSplats = unindent(`
struct PackedSplats {
usampler2DArray texture;
int numSplats;
};
`);
export class NumPackedSplats extends UnaryOp<
typeof TPackedSplats,
"int",
"numSplats"
> {
constructor({
packedSplats,
}: { packedSplats: DynoVal<typeof TPackedSplats> }) {
super({ a: packedSplats, outKey: "numSplats", outTypeFunc: () => "int" });
this.statements = ({ inputs, outputs }) => [
`${outputs.numSplats} = ${inputs.a}.numSplats;`,
];
}
}
const defineReadPackedSplat = unindent(`
bool readPackedSplat(usampler2DArray texture, int numSplats, int index, out Gsplat gsplat) {
if ((index >= 0) && (index < numSplats)) {
uvec4 packed = texelFetch(texture, splatTexCoord(index), 0);
unpackSplat(packed, gsplat.center, gsplat.scales, gsplat.quaternion, gsplat.rgba);
return true;
} else {
return false;
}
}
`);
export class ReadPackedSplat
extends Dyno<
{ packedSplats: typeof TPackedSplats; index: "int" },
{ gsplat: typeof Gsplat }
>
implements HasDynoOut<typeof Gsplat>
{
constructor({
packedSplats,
index,
}: { packedSplats?: DynoVal<typeof TPackedSplats>; index?: DynoVal<"int"> }) {
super({
inTypes: { packedSplats: TPackedSplats, index: "int" },
outTypes: { gsplat: Gsplat },
inputs: { packedSplats, index },
globals: () => [defineGsplat, definePackedSplats, defineReadPackedSplat],
statements: ({ inputs, outputs }) => {
const { gsplat } = outputs;
if (!gsplat) {
return [];
}
const { packedSplats, index } = inputs;
let statements: string[];
if (packedSplats && index) {
statements = unindentLines(`
if (readPackedSplat(${packedSplats}.texture, ${packedSplats}.numSplats, ${index}, ${gsplat})) {
bool zeroSize = all(equal(${gsplat}.scales, vec3(0.0, 0.0, 0.0)));
${gsplat}.flags = zeroSize ? 0u : GSPLAT_FLAG_ACTIVE;
} else {
${gsplat}.flags = 0u;
}
`);
} else {
statements = [`${gsplat}.flags = 0u;`];
}
statements.push(`${gsplat}.index = ${index ?? "0"};`);
return statements;
},
});
}
dynoOut(): DynoValue<typeof Gsplat> {
return new DynoOutput(this, "gsplat");
}
}
export class ReadPackedSplatRange
extends Dyno<
{
packedSplats: typeof TPackedSplats;
index: "int";
base: "int";
count: "int";
},
{ gsplat: typeof Gsplat }
>
implements HasDynoOut<typeof Gsplat>
{
constructor({
packedSplats,
index,
base,
count,
}: {
packedSplats?: DynoVal<typeof TPackedSplats>;
index?: DynoVal<"int">;
base?: DynoVal<"int">;
count?: DynoVal<"int">;
}) {
super({
inTypes: {
packedSplats: TPackedSplats,
index: "int",
base: "int",
count: "int",
},
outTypes: { gsplat: Gsplat },
inputs: { packedSplats, index, base, count },
globals: () => [defineGsplat, definePackedSplats, defineReadPackedSplat],
statements: ({ inputs, outputs }) => {
const { gsplat } = outputs;
if (!gsplat) {
return [];
}
const { packedSplats, index, base, count } = inputs;
let statements: string[];
if (packedSplats && index && base && count) {
statements = unindentLines(`
${gsplat}.flags = 0u;
if ((${index} >= ${base}) && (${index} < (${base} + ${count}))) {
if (readPackedSplat(${packedSplats}.texture, ${packedSplats}.numSplats, ${index}, ${gsplat})) {
bool zeroSize = all(equal(${gsplat}.scales, vec3(0.0, 0.0, 0.0)));
${gsplat}.flags = zeroSize ? 0u : GSPLAT_FLAG_ACTIVE;
}
}
`);
} else {
statements = [`${gsplat}.flags = 0u;`];
}
statements.push(`${gsplat}.index = ${index ?? "0"};`);
return statements;
},
});
}
dynoOut(): DynoValue<typeof Gsplat> {
return new DynoOutput(this, "gsplat");
}
}
export class SplitGsplat extends Dyno<
{ gsplat: typeof Gsplat },
{
flags: "uint";
active: "bool";
index: "int";
center: "vec3";
scales: "vec3";
quaternion: "vec4";
rgba: "vec4";
rgb: "vec3";
opacity: "float";
x: "float";
y: "float";
z: "float";
r: "float";
g: "float";
b: "float";
}
> {
constructor({ gsplat }: { gsplat?: DynoVal<typeof Gsplat> }) {
super({
inTypes: { gsplat: Gsplat },
outTypes: {
flags: "uint",
active: "bool",
index: "int",
center: "vec3",
scales: "vec3",
quaternion: "vec4",
rgba: "vec4",
rgb: "vec3",
opacity: "float",
x: "float",
y: "float",
z: "float",
r: "float",
g: "float",
b: "float",
},
inputs: { gsplat },
globals: () => [defineGsplat],
statements: ({ inputs, outputs }) => {
const { gsplat } = inputs;
const {
flags,
active,
index,
center,
scales,
quaternion,
rgba,
rgb,
opacity,
x,
y,
z,
r,
g,
b,
} = outputs;
return [
!flags ? null : `${flags} = ${gsplat ? `${gsplat}.flags` : "0u"};`,
!active
? null
: `${active} = isGsplatActive(${gsplat ? `${gsplat}.flags` : "0u"});`,
!index ? null : `${index} = ${gsplat ? `${gsplat}.index` : "0"};`,
!center
? null
: `${center} = ${gsplat ? `${gsplat}.center` : "vec3(0.0, 0.0, 0.0)"};`,
!scales
? null
: `${scales} = ${gsplat ? `${gsplat}.scales` : "vec3(0.0, 0.0, 0.0)"};`,
!quaternion
? null
: `${quaternion} = ${gsplat ? `${gsplat}.quaternion` : "vec4(0.0, 0.0, 0.0, 1.0)"};`,
!rgba
? null
: `${rgba} = ${gsplat ? `${gsplat}.rgba` : "vec4(0.0, 0.0, 0.0, 0.0)"};`,
!rgb
? null
: `${rgb} = ${gsplat ? `${gsplat}.rgba.rgb` : "vec3(0.0, 0.0, 0.0)"};`,
!opacity
? null
: `${opacity} = ${gsplat ? `${gsplat}.rgba.a` : "0.0"};`,
!x ? null : `${x} = ${gsplat ? `${gsplat}.center.x` : "0.0"};`,
!y ? null : `${y} = ${gsplat ? `${gsplat}.center.y` : "0.0"};`,
!z ? null : `${z} = ${gsplat ? `${gsplat}.center.z` : "0.0"};`,
!r ? null : `${r} = ${gsplat ? `${gsplat}.rgba.r` : "0.0"};`,
!g ? null : `${g} = ${gsplat ? `${gsplat}.rgba.g` : "0.0"};`,
!b ? null : `${b} = ${gsplat ? `${gsplat}.rgba.b` : "0.0"};`,
].filter(Boolean) as string[];
},
});
}
}
export class CombineGsplat
extends Dyno<
{
gsplat: typeof Gsplat;
flags: "uint";
index: "int";
center: "vec3";
scales: "vec3";
quaternion: "vec4";
rgba: "vec4";
rgb: "vec3";
opacity: "float";
x: "float";
y: "float";
z: "float";
r: "float";
g: "float";
b: "float";
},
{ gsplat: typeof Gsplat }
>
implements HasDynoOut<typeof Gsplat>
{
constructor({
gsplat,
flags,
index,
center,
scales,
quaternion,
rgba,
rgb,
opacity,
x,
y,
z,
r,
g,
b,
}: {
gsplat?: DynoVal<typeof Gsplat>;
flags?: DynoVal<"uint">;
index?: DynoVal<"int">;
center?: DynoVal<"vec3">;
scales?: DynoVal<"vec3">;
quaternion?: DynoVal<"vec4">;
rgba?: DynoVal<"vec4">;
rgb?: DynoVal<"vec3">;
opacity?: DynoVal<"float">;
x?: DynoVal<"float">;
y?: DynoVal<"float">;
z?: DynoVal<"float">;
r?: DynoVal<"float">;
g?: DynoVal<"float">;
b?: DynoVal<"float">;
}) {
super({
inTypes: {
gsplat: Gsplat,
flags: "uint",
index: "int",
center: "vec3",
scales: "vec3",
quaternion: "vec4",
rgba: "vec4",
rgb: "vec3",
opacity: "float",
x: "float",
y: "float",
z: "float",
r: "float",
g: "float",
b: "float",
},
outTypes: { gsplat: Gsplat },
inputs: {
gsplat,
flags,
index,
center,
scales,
quaternion,
rgba,
rgb,
opacity,
x,
y,
z,
r,
g,
b,
},
globals: () => [defineGsplat],
statements: ({ inputs, outputs }) => {
const { gsplat: outGsplat } = outputs;
if (!outGsplat) {
return [];
}
const {
gsplat,
flags,
index,
center,
scales,
quaternion,
rgba,
rgb,
opacity,
x,
y,
z,
r,
g,
b,
} = inputs;
return [
`${outGsplat}.flags = ${flags ?? (gsplat ? `${gsplat}.flags` : "0u")};`,
`${outGsplat}.index = ${index ?? (gsplat ? `${gsplat}.index` : "0")};`,
`${outGsplat}.center = ${center ?? (gsplat ? `${gsplat}.center` : "vec3(0.0, 0.0, 0.0)")};`,
`${outGsplat}.scales = ${scales ?? (gsplat ? `${gsplat}.scales` : "vec3(0.0, 0.0, 0.0)")};`,
`${outGsplat}.quaternion = ${quaternion ?? (gsplat ? `${gsplat}.quaternion` : "vec4(0.0, 0.0, 0.0, 1.0)")};`,
`${outGsplat}.rgba = ${rgba ?? (gsplat ? `${gsplat}.rgba` : "vec4(0.0, 0.0, 0.0, 0.0)")};`,
!rgb ? null : `${outGsplat}.rgba.rgb = ${rgb};`,
!opacity ? null : `${outGsplat}.rgba.a = ${opacity};`,
!x ? null : `${outGsplat}.center.x = ${x};`,
!y ? null : `${outGsplat}.center.y = ${y};`,
!z ? null : `${outGsplat}.center.z = ${z};`,
!r ? null : `${outGsplat}.rgba.r = ${r};`,
!g ? null : `${outGsplat}.rgba.g = ${g};`,
!b ? null : `${outGsplat}.rgba.b = ${b};`,
].filter(Boolean) as string[];
},
});
}
dynoOut(): DynoValue<typeof Gsplat> {
return new DynoOutput(this, "gsplat");
}
}
export const defineGsplatNormal = unindent(`
vec3 gsplatNormal(vec3 scales, vec4 quaternion) {
float minScale = min(scales.x, min(scales.y, scales.z));
vec3 normal;
if (scales.z == minScale) {
normal = vec3(0.0, 0.0, 1.0);
} else if (scales.y == minScale) {
normal = vec3(0.0, 1.0, 0.0);
} else {
normal = vec3(1.0, 0.0, 0.0);
}
return quatVec(quaternion, normal);
}
`);
export class GsplatNormal extends UnaryOp<typeof Gsplat, "vec3", "normal"> {
constructor({ gsplat }: { gsplat: DynoVal<typeof Gsplat> }) {
super({ a: gsplat, outKey: "normal", outTypeFunc: () => "vec3" });
this.globals = () => [defineGsplat, defineGsplatNormal];
this.statements = ({ inputs, outputs }) => [
`${outputs.normal} = gsplatNormal(${inputs.a}.scales, ${inputs.a}.quaternion);`,
];
}
}
export class TransformGsplat
extends Dyno<
{
gsplat: typeof Gsplat;
scale: "float";
rotate: "vec4";
translate: "vec3";
recolor: "vec4";
},
{ gsplat: typeof Gsplat }
>
implements HasDynoOut<typeof Gsplat>
{
constructor({
gsplat,
scale,
rotate,
translate,
recolor,
}: {
gsplat?: DynoVal<typeof Gsplat>;
scale?: DynoVal<"float">;
rotate?: DynoVal<"vec4">;
translate?: DynoVal<"vec3">;
recolor?: DynoVal<"vec4">;
}) {
super({
inTypes: {
gsplat: Gsplat,
scale: "float",
rotate: "vec4",
translate: "vec3",
recolor: "vec4",
},
outTypes: { gsplat: Gsplat },
inputs: { gsplat, scale, rotate, translate, recolor },
globals: () => [defineGsplat],
statements: ({ inputs, outputs, compile }) => {
const { gsplat } = outputs;
if (!gsplat || !inputs.gsplat) {
return [];
}
const { scale, rotate, translate, recolor } = inputs;
const indent = compile.indent;
const statements = [
`${gsplat} = ${inputs.gsplat};`,
`if (isGsplatActive(${gsplat}.flags)) {`,
scale ? `${indent}${gsplat}.center *= ${scale};` : null,
rotate
? `${indent}${gsplat}.center = quatVec(${rotate}, ${gsplat}.center);`
: null,
translate ? `${indent}${gsplat}.center += ${translate};` : null,
scale ? `${indent}${gsplat}.scales *= ${scale};` : null,
rotate
? `${indent}${gsplat}.quaternion = quatQuat(${rotate}, ${gsplat}.quaternion);`
: null,
recolor ? `${indent}${gsplat}.rgba *= ${recolor};` : null,
"}",
].filter(Boolean) as string[];
return statements;
},
});
}
dynoOut(): DynoValue<typeof Gsplat> {
return new DynoOutput(this, "gsplat");
}
}
+239
View File
@@ -0,0 +1,239 @@
import { Dyno } from "./base";
import type {
AllSamplerTypes,
IsamplerTypes,
NormalSamplerTypes,
Sampler2DArrayTypes,
Sampler2DTypes,
Sampler3DTypes,
SamplerCubeTypes,
SamplerShadowTypes,
SamplerTypes,
UsamplerTypes,
} from "./types";
import {
DynoOutput,
type DynoVal,
type DynoValue,
type HasDynoOut,
valType,
} from "./value";
export const textureSize = <T extends AllSamplerTypes>(
texture: DynoVal<T>,
lod?: DynoVal<"int">,
): DynoVal<TextureSizeType<T>> => new TextureSize<T>({ texture, lod });
export const texture = <T extends AllSamplerTypes>(
texture: DynoVal<T>,
coord: DynoVal<TextureCoordType<T>>,
bias?: DynoVal<"float">,
): DynoVal<TextureReturnType<T>> => new Texture<T>({ texture, coord, bias });
export const texelFetch = <T extends NormalSamplerTypes>(
texture: DynoVal<T>,
coord: DynoVal<TextureSizeType<T>>,
lod?: DynoVal<"int">,
): DynoVal<TextureReturnType<T>> => new TexelFetch<T>({ texture, coord, lod });
export class TextureSize<T extends AllSamplerTypes>
extends Dyno<{ texture: T; lod: "int" }, { size: TextureSizeType<T> }>
implements HasDynoOut<TextureSizeType<T>>
{
constructor({ texture, lod }: { texture: DynoVal<T>; lod?: DynoVal<"int"> }) {
const textureType = valType(texture);
super({
inTypes: { texture: textureType, lod: "int" },
outTypes: { size: textureSizeType(textureType) },
inputs: { texture, lod },
statements: ({ inputs, outputs }) => [
`${outputs.size} = textureSize(${inputs.texture}, ${inputs.lod ?? "0"});`,
],
});
}
dynoOut(): DynoValue<TextureSizeType<T>> {
return new DynoOutput(this, "size");
}
}
export class Texture<T extends AllSamplerTypes>
extends Dyno<
{ texture: T; coord: TextureCoordType<T>; bias: "float" },
{ sample: TextureReturnType<T> }
>
implements HasDynoOut<TextureReturnType<T>>
{
constructor({
texture,
coord,
bias,
}: {
texture: DynoVal<T>;
coord: DynoVal<TextureCoordType<T>>;
bias?: DynoVal<"float">;
}) {
const textureType = valType(texture);
super({
inTypes: {
texture: textureType,
coord: textureCoordType(textureType),
bias: "float",
},
outTypes: { sample: textureReturnType(textureType) },
inputs: { texture, coord, bias },
statements: ({ inputs, outputs }) => [
`${outputs.sample} = texture(${inputs.texture}, ${inputs.coord}${inputs.bias ? `, ${inputs.bias}` : ""});`,
],
});
}
dynoOut(): DynoValue<TextureReturnType<T>> {
return new DynoOutput(this, "sample");
}
}
export class TexelFetch<T extends NormalSamplerTypes>
extends Dyno<
{ texture: T; coord: TextureSizeType<T>; lod: "int" },
{ texel: TextureReturnType<T> }
>
implements HasDynoOut<TextureReturnType<T>>
{
constructor({
texture,
coord,
lod,
}: {
texture: DynoVal<T>;
coord: DynoVal<TextureSizeType<T>>;
lod?: DynoVal<"int">;
}) {
const textureType = valType(texture);
super({
inTypes: {
texture: textureType,
coord: textureSizeType(textureType),
lod: "int",
},
outTypes: { texel: textureReturnType(textureType) },
inputs: { texture, coord, lod },
statements: ({ inputs, outputs }) => [
`${outputs.texel} = texelFetch(${inputs.texture}, ${inputs.coord}, ${inputs.lod ?? "0"});`,
],
});
}
dynoOut(): DynoValue<TextureReturnType<T>> {
return new DynoOutput(this, "texel");
}
}
type TextureSizeType<T extends AllSamplerTypes> = T extends
| Sampler2DTypes
| SamplerCubeTypes
? "ivec2"
: T extends Sampler3DTypes | Sampler2DArrayTypes
? "ivec3"
: never;
function textureSizeType<T extends AllSamplerTypes>(
textureType: T,
): TextureSizeType<T> {
switch (textureType) {
case "sampler2D":
case "usampler2D":
case "isampler2D":
case "samplerCube":
case "usamplerCube":
case "isamplerCube":
case "sampler2DShadow":
case "samplerCubeShadow":
return "ivec2" as TextureSizeType<T>;
case "sampler3D":
case "usampler3D":
case "isampler3D":
case "sampler2DArray":
case "usampler2DArray":
case "isampler2DArray":
case "sampler2DArrayShadow":
return "ivec3" as TextureSizeType<T>;
default:
throw new Error(`Invalid texture type: ${textureType}`);
}
}
type TextureCoordType<T extends AllSamplerTypes> = T extends Sampler2DTypes
? "vec2"
: T extends
| Sampler3DTypes
| Sampler2DArrayTypes
| SamplerCubeTypes
| Sampler2DArrayTypes
? "vec3"
: T extends "samperCubeShadow" | "sampler2DArrayShadow"
? "vec4"
: never;
function textureCoordType<T extends AllSamplerTypes>(
textureType: T,
): TextureCoordType<T> {
switch (textureType) {
case "sampler2D":
case "usampler2D":
case "isampler2D":
return "vec2" as TextureCoordType<T>;
case "sampler3D":
case "usampler3D":
case "isampler3D":
case "samplerCube":
case "usamplerCube":
case "isamplerCube":
case "sampler2DArray":
case "usampler2DArray":
case "isampler2DArray":
case "sampler2DShadow":
return "vec3" as TextureCoordType<T>;
case "samplerCubeShadow":
case "sampler2DArrayShadow":
return "vec4" as TextureCoordType<T>;
default:
throw new Error(`Invalid texture type: ${textureType}`);
}
}
type TextureReturnType<T extends AllSamplerTypes> = T extends SamplerTypes
? "vec4"
: T extends UsamplerTypes
? "uvec4"
: T extends IsamplerTypes
? "ivec4"
: T extends SamplerShadowTypes
? "float"
: never;
function textureReturnType<T extends AllSamplerTypes>(
textureType: T,
): TextureReturnType<T> {
switch (textureType) {
case "sampler2D":
case "sampler2DArray":
case "sampler3D":
case "samplerCube":
case "sampler2DShadow":
return "vec4" as TextureReturnType<T>;
case "usampler2D":
case "usampler2DArray":
case "usampler3D":
case "usamplerCube":
return "uvec4" as TextureReturnType<T>;
case "isampler2D":
case "isampler2DArray":
case "isampler3D":
case "isamplerCube":
return "ivec4" as TextureReturnType<T>;
case "samplerCubeShadow":
case "sampler2DArrayShadow":
return "float" as TextureReturnType<T>;
default:
throw new Error(`Invalid texture type: ${textureType}`);
}
}
+155
View File
@@ -0,0 +1,155 @@
import { Dyno } from "./base";
import type { DynoVal } from "./value";
export const transformPos = (
position: DynoVal<"vec3">,
{
scale,
scales,
rotate,
translate,
}: {
scale?: DynoVal<"float">;
scales?: DynoVal<"vec3">;
rotate?: DynoVal<"vec4">;
translate?: DynoVal<"vec3">;
},
): DynoVal<"vec3"> => {
return new TransformPosition({ position, scale, scales, rotate, translate })
.outputs.position;
};
export const transformRay = (
ray: DynoVal<"vec3">,
{
scale,
scales,
rotate,
}: {
scale?: DynoVal<"float">;
scales?: DynoVal<"vec3">;
rotate?: DynoVal<"vec4">;
},
): DynoVal<"vec3"> => {
return new TransformRay({ ray, scale, scales, rotate }).outputs.ray;
};
export const transformQuat = (
quaternion: DynoVal<"vec4">,
{ rotate }: { rotate?: DynoVal<"vec4"> },
): DynoVal<"vec4"> => {
return new TransformQuaternion({ quaternion, rotate }).outputs.quaternion;
};
export class TransformPosition extends Dyno<
{
position: "vec3";
scale: "float";
scales: "vec3";
rotate: "vec4";
translate: "vec3";
},
{ position: "vec3" }
> {
constructor({
position,
scale,
scales,
rotate,
translate,
}: {
position?: DynoVal<"vec3">;
scale?: DynoVal<"float">;
scales?: DynoVal<"vec3">;
rotate?: DynoVal<"vec4">;
translate?: DynoVal<"vec3">;
}) {
super({
inTypes: {
position: "vec3",
scale: "float",
scales: "vec3",
rotate: "vec4",
translate: "vec3",
},
outTypes: { position: "vec3" },
inputs: { position, scale, scales, rotate, translate },
statements: ({ inputs, outputs }) => {
const { position } = outputs;
if (!position) {
return [];
}
const { scale, scales, rotate, translate } = inputs;
return [
`${position} = ${inputs.position ?? "vec3(0.0, 0.0, 0.0)"};`,
!scale ? null : `${position} *= ${scale};`,
!scales ? null : `${position} *= ${scales};`,
!rotate ? null : `${position} = quatVec(${rotate}, ${position});`,
!translate ? null : `${position} += ${translate};`,
].filter(Boolean) as string[];
},
});
}
}
export class TransformRay extends Dyno<
{ ray: "vec3"; scale: "float"; scales: "vec3"; rotate: "vec4" },
{ ray: "vec3" }
> {
constructor({
ray,
scale,
scales,
rotate,
}: {
ray?: DynoVal<"vec3">;
scale?: DynoVal<"float">;
scales?: DynoVal<"vec3">;
rotate?: DynoVal<"vec4">;
}) {
super({
inTypes: { ray: "vec3", scale: "float", scales: "vec3", rotate: "vec4" },
outTypes: { ray: "vec3" },
inputs: { ray, scale, scales, rotate },
statements: ({ inputs, outputs }) => {
const { ray } = outputs;
if (!ray) {
return [];
}
const { scale, scales, rotate } = inputs;
return [
`${ray} = ${inputs.ray ?? "vec3(0.0, 0.0, 0.0)"};`,
!scale ? null : `${ray} *= ${scale};`,
!scales ? null : `${ray} *= ${scales};`,
!rotate ? null : `${ray} = quatVec(${rotate}, ${ray});`,
].filter(Boolean) as string[];
},
});
}
}
export class TransformQuaternion extends Dyno<
{ quaternion: "vec4"; rotate: "vec4" },
{ quaternion: "vec4" }
> {
constructor({
quaternion,
rotate,
}: { quaternion?: DynoVal<"vec4">; rotate?: DynoVal<"vec4"> }) {
super({
inTypes: { quaternion: "vec4", rotate: "vec4" },
outTypes: { quaternion: "vec4" },
inputs: { quaternion, rotate },
statements: ({ inputs, outputs }) => {
const { quaternion } = outputs;
if (!quaternion) {
return [];
}
return [
`${quaternion} = ${inputs.quaternion ?? "vec4(0.0, 0.0, 0.0, 1.0)"};`,
!rotate
? null
: `${quaternion} = quatQuat(${inputs.rotate}, ${quaternion});`,
].filter(Boolean) as string[];
},
});
}
}
+182
View File
@@ -0,0 +1,182 @@
import { BinaryOp, UnaryOp } from "./base";
import type { FloatTypes } from "./types";
import type { DynoVal } from "./value";
export const radians = <A extends FloatTypes>(
degrees: DynoVal<A>,
): DynoVal<A> => new Radians({ degrees });
export const degrees = <A extends FloatTypes>(
radians: DynoVal<A>,
): DynoVal<A> => new Degrees({ radians });
export const sin = <A extends FloatTypes>(radians: DynoVal<A>): DynoVal<A> =>
new Sin({ radians });
export const cos = <A extends FloatTypes>(radians: DynoVal<A>): DynoVal<A> =>
new Cos({ radians });
export const tan = <A extends FloatTypes>(radians: DynoVal<A>): DynoVal<A> =>
new Tan({ radians });
export const asin = <A extends FloatTypes>(sin: DynoVal<A>): DynoVal<A> =>
new Asin({ sin });
export const acos = <A extends FloatTypes>(cos: DynoVal<A>): DynoVal<A> =>
new Acos({ cos });
export const atan = <A extends FloatTypes>(tan: DynoVal<A>): DynoVal<A> =>
new Atan({ tan });
export const atan2 = <A extends FloatTypes>(
y: DynoVal<A>,
x: DynoVal<A>,
): DynoVal<A> => new Atan2({ y, x });
export const sinh = <A extends FloatTypes>(x: DynoVal<A>): DynoVal<A> =>
new Sinh({ x });
export const cosh = <A extends FloatTypes>(x: DynoVal<A>): DynoVal<A> =>
new Cosh({ x });
export const tanh = <A extends FloatTypes>(x: DynoVal<A>): DynoVal<A> =>
new Tanh({ x });
export const asinh = <A extends FloatTypes>(x: DynoVal<A>): DynoVal<A> =>
new Asinh({ x });
export const acosh = <A extends FloatTypes>(x: DynoVal<A>): DynoVal<A> =>
new Acosh({ x });
export const atanh = <A extends FloatTypes>(x: DynoVal<A>): DynoVal<A> =>
new Atanh({ x });
export class Radians<A extends FloatTypes> extends UnaryOp<A, A, "radians"> {
constructor({ degrees }: { degrees: DynoVal<A> }) {
super({ a: degrees, outTypeFunc: (aType) => aType, outKey: "radians" });
this.statements = ({ inputs, outputs }) => [
`${outputs.radians} = radians(${inputs.a});`,
];
}
}
export class Degrees<A extends FloatTypes> extends UnaryOp<A, A, "degrees"> {
constructor({ radians }: { radians: DynoVal<A> }) {
super({ a: radians, outTypeFunc: (aType) => aType, outKey: "degrees" });
this.statements = ({ inputs, outputs }) => [
`${outputs.degrees} = degrees(${inputs.a});`,
];
}
}
export class Sin<A extends FloatTypes> extends UnaryOp<A, A, "sin"> {
constructor({ radians }: { radians: DynoVal<A> }) {
super({ a: radians, outTypeFunc: (aType) => aType, outKey: "sin" });
this.statements = ({ inputs, outputs }) => [
`${outputs.sin} = sin(${inputs.a});`,
];
}
}
export class Cos<A extends FloatTypes> extends UnaryOp<A, A, "cos"> {
constructor({ radians }: { radians: DynoVal<A> }) {
super({ a: radians, outTypeFunc: (aType) => aType, outKey: "cos" });
this.statements = ({ inputs, outputs }) => [
`${outputs.cos} = cos(${inputs.a});`,
];
}
}
export class Tan<A extends FloatTypes> extends UnaryOp<A, A, "tan"> {
constructor({ radians }: { radians: DynoVal<A> }) {
super({ a: radians, outTypeFunc: (aType) => aType, outKey: "tan" });
this.statements = ({ inputs, outputs }) => [
`${outputs.tan} = tan(${inputs.a});`,
];
}
}
export class Asin<A extends FloatTypes> extends UnaryOp<A, A, "asin"> {
constructor({ sin }: { sin: DynoVal<A> }) {
super({ a: sin, outTypeFunc: (aType) => aType, outKey: "asin" });
this.statements = ({ inputs, outputs }) => [
`${outputs.asin} = asin(${inputs.a});`,
];
}
}
export class Acos<A extends FloatTypes> extends UnaryOp<A, A, "acos"> {
constructor({ cos }: { cos: DynoVal<A> }) {
super({ a: cos, outTypeFunc: (aType) => aType, outKey: "acos" });
this.statements = ({ inputs, outputs }) => [
`${outputs.acos} = acos(${inputs.a});`,
];
}
}
export class Atan<A extends FloatTypes> extends UnaryOp<A, A, "atan"> {
constructor({ tan }: { tan: DynoVal<A> }) {
super({ a: tan, outTypeFunc: (aType) => aType, outKey: "atan" });
this.statements = ({ inputs, outputs }) => [
`${outputs.atan} = atan(${inputs.a});`,
];
}
}
export class Atan2<A extends FloatTypes> extends BinaryOp<A, A, A, "atan2"> {
constructor({ y, x }: { y: DynoVal<A>; x: DynoVal<A> }) {
super({
a: y,
b: x,
outTypeFunc: (aType, bType) => aType,
outKey: "atan2",
});
this.statements = ({ inputs, outputs }) => [
`${outputs.atan2} = atan2(${inputs.a}, ${inputs.b});`,
];
}
}
export class Sinh<A extends FloatTypes> extends UnaryOp<A, A, "sinh"> {
constructor({ x }: { x: DynoVal<A> }) {
super({ a: x, outTypeFunc: (aType) => aType, outKey: "sinh" });
this.statements = ({ inputs, outputs }) => [
`${outputs.sinh} = sinh(${inputs.a});`,
];
}
}
export class Cosh<A extends FloatTypes> extends UnaryOp<A, A, "cosh"> {
constructor({ x }: { x: DynoVal<A> }) {
super({ a: x, outTypeFunc: (aType) => aType, outKey: "cosh" });
this.statements = ({ inputs, outputs }) => [
`${outputs.cosh} = cosh(${inputs.a});`,
];
}
}
export class Tanh<A extends FloatTypes> extends UnaryOp<A, A, "tanh"> {
constructor({ x }: { x: DynoVal<A> }) {
super({ a: x, outTypeFunc: (aType) => aType, outKey: "tanh" });
this.statements = ({ inputs, outputs }) => [
`${outputs.tanh} = tanh(${inputs.a});`,
];
}
}
export class Asinh<A extends FloatTypes> extends UnaryOp<A, A, "asinh"> {
constructor({ x }: { x: DynoVal<A> }) {
super({ a: x, outTypeFunc: (aType) => aType, outKey: "asinh" });
this.statements = ({ inputs, outputs }) => [
`${outputs.asinh} = asinh(${inputs.a});`,
];
}
}
export class Acosh<A extends FloatTypes> extends UnaryOp<A, A, "acosh"> {
constructor({ x }: { x: DynoVal<A> }) {
super({ a: x, outTypeFunc: (aType) => aType, outKey: "acosh" });
this.statements = ({ inputs, outputs }) => [
`${outputs.acosh} = acosh(${inputs.a});`,
];
}
}
export class Atanh<A extends FloatTypes> extends UnaryOp<A, A, "atanh"> {
constructor({ x }: { x: DynoVal<A> }) {
super({ a: x, outTypeFunc: (aType) => aType, outKey: "atanh" });
this.statements = ({ inputs, outputs }) => [
`${outputs.atanh} = atanh(${inputs.a});`,
];
}
}
+420
View File
@@ -0,0 +1,420 @@
import type * as THREE from "three";
export type BoolTypes = "bool" | "bvec2" | "bvec3" | "bvec4";
export type IntTypes = "int" | "ivec2" | "ivec3" | "ivec4";
export type UintTypes = "uint" | "uvec2" | "uvec3" | "uvec4";
export type AllIntTypes = IntTypes | UintTypes;
export type FloatTypes = "float" | "vec2" | "vec3" | "vec4";
export type ScalarTypes = "uint" | "int" | "float";
export type Vector2Types = "vec2" | "ivec2" | "uvec2";
export type Vector3Types = "vec3" | "ivec3" | "uvec3";
export type Vector4Types = "vec4" | "ivec4" | "uvec4";
export type VectorTypes = Vector2Types | Vector3Types | Vector4Types;
export type MatFloatTypes =
| "mat2"
| "mat2x2"
| "mat2x3"
| "mat2x4"
| "mat3"
| "mat3x2"
| "mat3x3"
| "mat3x4"
| "mat4"
| "mat4x2"
| "mat4x3"
| "mat4x4";
export type SquareMatTypes =
| "mat2"
| "mat3"
| "mat4"
| "mat2x2"
| "mat3x3"
| "mat4x4";
export type AllFloatTypes = FloatTypes | MatFloatTypes;
export type SignedTypes = IntTypes | FloatTypes;
export type AllSignedTypes = SignedTypes | MatFloatTypes;
export type ValueTypes = FloatTypes | IntTypes | UintTypes;
export type AllValueTypes = AllFloatTypes | IntTypes | UintTypes;
export type SimpleTypes = BoolTypes | AllValueTypes;
export type VectorElementType<A extends VectorTypes> = A extends FloatTypes
? "float"
: A extends IntTypes
? "int"
: A extends UintTypes
? "uint"
: never;
export type SameSizeVec<T extends ValueTypes> = T extends ScalarTypes
? "float"
: T extends "vec2" | "ivec2" | "uvec2"
? "vec2"
: T extends "vec3" | "ivec3" | "uvec3"
? "vec3"
: T extends "vec4" | "ivec4" | "uvec4"
? "vec4"
: never;
export type SameSizeUvec<T extends ValueTypes> = T extends ScalarTypes
? "uint"
: T extends "vec2" | "ivec2" | "uvec2"
? "uvec2"
: T extends "vec3" | "ivec3" | "uvec3"
? "uvec3"
: T extends "vec4" | "ivec4" | "uvec4"
? "uvec4"
: never;
export type SameSizeIvec<T extends ValueTypes> = T extends ScalarTypes
? "int"
: T extends "vec2" | "ivec2" | "uvec2"
? "ivec2"
: T extends "vec3" | "ivec3" | "uvec3"
? "ivec3"
: T extends "vec4" | "ivec4" | "uvec4"
? "ivec4"
: never;
export type SamplerTypes =
| "sampler2D"
| "sampler2DArray"
| "sampler3D"
| "samplerCube";
export type UsamplerTypes =
| "usampler2D"
| "usampler2DArray"
| "usampler3D"
| "usamplerCube";
export type IsamplerTypes =
| "isampler2D"
| "isampler2DArray"
| "isampler3D"
| "isamplerCube";
export type NormalSamplerTypes = SamplerTypes | UsamplerTypes | IsamplerTypes;
export type SamplerShadowTypes =
| "sampler2DShadow"
| "sampler2DArrayShadow"
| "samplerCubeShadow";
export type AllSamplerTypes = NormalSamplerTypes | SamplerShadowTypes;
export type Sampler2DTypes =
| "sampler2D"
| "usampler2D"
| "isampler2D"
| "sampler2DShadow";
export type Sampler2DArrayTypes =
| "sampler2DArray"
| "usampler2DArray"
| "isampler2DArray"
| "sampler2DArrayShadow";
export type Sampler3DTypes = "sampler3D" | "usampler3D" | "isampler3D";
export type SamplerCubeTypes =
| "samplerCube"
| "usamplerCube"
| "isamplerCube"
| "samplerCubeShadow";
export function isBoolType(type: DynoType): boolean {
return (
type === "bool" || type === "bvec2" || type === "bvec3" || type === "bvec4"
);
}
export function isScalarType(type: DynoType): boolean {
return type === "int" || type === "uint" || type === "float";
}
export function isIntType(type: DynoType): boolean {
return (
type === "int" || type === "ivec2" || type === "ivec3" || type === "ivec4"
);
}
export function isUintType(type: DynoType): boolean {
return (
type === "uint" || type === "uvec2" || type === "uvec3" || type === "uvec4"
);
}
export function isFloatType(type: DynoType): boolean {
return (
type === "float" || type === "vec2" || type === "vec3" || type === "vec4"
);
}
export function isMatFloatType(type: DynoType): boolean {
return (
type === "mat2" ||
type === "mat2x2" ||
type === "mat2x3" ||
type === "mat2x4" ||
type === "mat3" ||
type === "mat3x2" ||
type === "mat3x3" ||
type === "mat3x4" ||
type === "mat4" ||
type === "mat4x2" ||
type === "mat4x3" ||
type === "mat4x4"
);
}
export function isAllFloatType(type: DynoType): boolean {
return isFloatType(type) || isMatFloatType(type);
}
export function isVector2Type(type: DynoType): boolean {
return type === "vec2" || type === "ivec2" || type === "uvec2";
}
export function isVector3Type(type: DynoType): boolean {
return type === "vec3" || type === "ivec3" || type === "uvec3";
}
export function isVector4Type(type: DynoType): boolean {
return type === "vec4" || type === "ivec4" || type === "uvec4";
}
export function isVectorType(type: DynoType): boolean {
return isVector2Type(type) || isVector3Type(type) || isVector4Type(type);
}
export function isMat2(type: DynoType): boolean {
return type === "mat2" || type === "mat2x2";
}
export function isMat3(type: DynoType): boolean {
return type === "mat3" || type === "mat3x3";
}
export function isMat4(type: DynoType): boolean {
return type === "mat4" || type === "mat4x4";
}
export function vectorElementType<A extends VectorTypes>(
type: A,
): VectorElementType<A> {
switch (type) {
case "vec2":
return "float" as VectorElementType<A>;
case "vec3":
return "float" as VectorElementType<A>;
case "vec4":
return "float" as VectorElementType<A>;
case "ivec2":
return "int" as VectorElementType<A>;
case "ivec3":
return "int" as VectorElementType<A>;
case "ivec4":
return "int" as VectorElementType<A>;
case "uvec2":
return "uint" as VectorElementType<A>;
case "uvec3":
return "uint" as VectorElementType<A>;
case "uvec4":
return "uint" as VectorElementType<A>;
default:
throw new Error(`Invalid vector type: ${type}`);
}
}
export function vectorDim<A extends VectorTypes>(type: A): number {
switch (type) {
case "vec2":
case "ivec2":
case "uvec2":
return 2;
case "vec3":
case "ivec3":
case "uvec3":
return 3;
case "vec4":
case "ivec4":
case "uvec4":
return 4;
default:
throw new Error(`Invalid vector type: ${type}`);
}
}
export function sameSizeVec<T extends ValueTypes>(type: T): SameSizeVec<T> {
if (isScalarType(type)) {
return "float" as SameSizeVec<T>;
}
if (isVector2Type(type)) {
return "vec2" as SameSizeVec<T>;
}
if (isVector3Type(type)) {
return "vec3" as SameSizeVec<T>;
}
if (isVector4Type(type)) {
return "vec4" as SameSizeVec<T>;
}
throw new Error(`Invalid vector type: ${type}`);
}
export function sameSizeUvec<T extends ValueTypes>(type: T): SameSizeUvec<T> {
if (isScalarType(type)) {
return "uint" as SameSizeUvec<T>;
}
if (isVector2Type(type)) {
return "uvec2" as SameSizeUvec<T>;
}
if (isVector3Type(type)) {
return "uvec3" as SameSizeUvec<T>;
}
if (isVector4Type(type)) {
return "uvec4" as SameSizeUvec<T>;
}
throw new Error(`Invalid vector type: ${type}`);
}
export function sameSizeIvec<T extends ValueTypes>(type: T): SameSizeIvec<T> {
if (isScalarType(type)) {
return "int" as SameSizeIvec<T>;
}
if (isVector2Type(type)) {
return "ivec2" as SameSizeIvec<T>;
}
if (isVector3Type(type)) {
return "ivec3" as SameSizeIvec<T>;
}
if (isVector4Type(type)) {
return "ivec4" as SameSizeIvec<T>;
}
throw new Error(`Invalid vector type: ${type}`);
}
export type BaseType = SimpleTypes | AllSamplerTypes;
export type UserType = { type: string };
export type DynoType = BaseType | UserType;
export type DynoJsType<T extends DynoType> = T extends "bool"
? boolean
: T extends "uint"
? number
: T extends "int"
? number
: T extends "float"
? number
: T extends "bvec2"
? [boolean, boolean]
: T extends "uvec2"
? THREE.Vector2 | [number, number] | Uint32Array
: T extends "ivec2"
? THREE.Vector2 | [number, number] | Int32Array
: T extends "vec2"
? THREE.Vector2 | [number, number] | Float32Array
: T extends "bvec3"
? [boolean, boolean, boolean]
: T extends "uvec3"
? THREE.Vector3 | [number, number, number] | Uint32Array
: T extends "ivec3"
? THREE.Vector3 | [number, number, number] | Int32Array
: T extends "vec3"
?
| THREE.Vector3
| THREE.Color
| [number, number, number]
| Float32Array
: T extends "bvec4"
? [boolean, boolean, boolean, boolean]
: T extends "uvec4"
?
| THREE.Vector4
| [number, number, number, number]
| Uint32Array
: T extends "ivec4"
?
| THREE.Vector4
| [number, number, number, number]
| Int32Array
: T extends "vec4"
?
| THREE.Vector4
| THREE.Quaternion
| [number, number, number, number]
| Float32Array
: T extends "mat2"
? THREE.Matrix2 | Float32Array
: T extends "mat2x2"
? THREE.Matrix2 | Float32Array
: T extends "mat2x3"
? Float32Array
: T extends "mat2x4"
? Float32Array
: T extends "mat3"
? THREE.Matrix3 | Float32Array
: T extends "mat3x2"
? Float32Array
: T extends "mat3x3"
? THREE.Matrix3 | Float32Array
: T extends "mat3x4"
? Float32Array
: T extends "mat4"
? THREE.Matrix4 | Float32Array
: T extends "mat4x2"
? Float32Array
: T extends "mat4x3"
? Float32Array
: T extends "mat4x4"
?
| THREE.Matrix4
| Float32Array
: T extends "usampler2D"
? THREE.Texture
: T extends "isampler2D"
? THREE.Texture
: T extends "sampler2D"
? THREE.Texture
: T extends "sampler2DShadow"
? THREE.Texture
: T extends "usampler2DArray"
? THREE.DataArrayTexture
: T extends "isampler2DArray"
? THREE.DataArrayTexture
: T extends "sampler2DArray"
? THREE.DataArrayTexture
: T extends "sampler2DArrayShadow"
? THREE.Texture
: T extends "usampler3D"
? THREE.DataArrayTexture
: T extends "isampler3D"
? THREE.DataArrayTexture
: T extends "sampler3D"
? THREE.DataArrayTexture
: T extends "usamplerCube"
? THREE.DataArrayTexture
: T extends "isamplerCube"
? THREE.DataArrayTexture
: T extends "samplerCube"
? THREE.DataArrayTexture
: T extends "samplerCubeShadow"
? THREE.Texture
: unknown;
export function typeLiteral(type: DynoType): string {
if (typeof type === "string") {
return type;
}
if (typeof type === "object" && type.type) {
return type.type;
}
throw new Error(`Invalid DynoType: ${String(type)}`);
}
export function numberAsInt(value: number): string {
return Math.trunc(value).toString();
}
export function numberAsUint(value: number): string {
const v = Math.max(0, Math.trunc(value));
return `${v.toString()}u`;
}
export function numberAsFloat(value: number): string {
return value === Number.POSITIVE_INFINITY
? "INFINITY"
: value === Number.NEGATIVE_INFINITY
? "-INFINITY"
: Number.isInteger(value)
? value.toFixed(1)
: value.toString();
}
+826
View File
@@ -0,0 +1,826 @@
import type { IUniform } from "three";
import { Dyno, dynoDeclare } from "./base";
import type { DynoJsType, DynoType } from "./types";
import { DynoOutput, type DynoValue, type HasDynoOut } from "./value";
export const uniform = <V extends DynoJsType<DynoType>>(
key: string,
type: DynoType,
value: V,
) => new DynoUniform({ key, type, value });
export const dynoBool = (value = false, key?: string) =>
new DynoBool({ key, value });
export const dynoUint = (value = 0, key?: string) =>
new DynoUint({ key, value });
export const dynoInt = (value = 0, key?: string) => new DynoInt({ key, value });
export const dynoFloat = (value = 0.0, key?: string) =>
new DynoFloat({ key, value });
export const dynoBvec2 = <V extends DynoJsType<"bvec2">>(
value: V,
key?: string,
) => new DynoBvec2({ key, value });
export const dynoUvec2 = <V extends DynoJsType<"uvec2">>(
value: V,
key?: string,
) => new DynoUvec2({ key, value });
export const dynoIvec2 = <V extends DynoJsType<"ivec2">>(
value: V,
key?: string,
) => new DynoIvec2({ key, value });
export const dynoVec2 = <V extends DynoJsType<"vec2">>(
value: V,
key?: string,
) => new DynoVec2({ key, value });
export const dynoBvec3 = <V extends DynoJsType<"bvec3">>(
value: V,
key?: string,
) => new DynoBvec3({ key, value });
export const dynoUvec3 = <V extends DynoJsType<"uvec3">>(
value: V,
key?: string,
) => new DynoUvec3({ key, value });
export const dynoIvec3 = <V extends DynoJsType<"ivec3">>(
value: V,
key?: string,
) => new DynoIvec3({ key, value });
export const dynoVec3 = <V extends DynoJsType<"vec3">>(
value: V,
key?: string,
) => new DynoVec3({ key, value });
export const dynoBvec4 = <V extends DynoJsType<"bvec4">>(
value: V,
key?: string,
) => new DynoBvec4({ key, value });
export const dynoUvec4 = <V extends DynoJsType<"uvec4">>(
value: V,
key?: string,
) => new DynoUvec4({ key, value });
export const dynoIvec4 = <V extends DynoJsType<"ivec4">>(
value: V,
key?: string,
) => new DynoIvec4({ key, value });
export const dynoVec4 = <V extends DynoJsType<"vec4">>(
value: V,
key?: string,
) => new DynoVec4({ key, value });
export const dynoMat2 = <V extends DynoJsType<"mat2">>(
value: V,
key?: string,
) => new DynoMat2({ key, value });
export const dynoMat2x2 = <V extends DynoJsType<"mat2x2">>(
value: V,
key?: string,
) => new DynoMat2x2({ key, value });
export const dynoMat2x3 = <V extends DynoJsType<"mat2x3">>(
value: V,
key?: string,
) => new DynoMat2x3({ key, value });
export const dynoMat2x4 = <V extends DynoJsType<"mat2x4">>(
value: V,
key?: string,
) => new DynoMat2x4({ key, value });
export const dynoMat3 = <V extends DynoJsType<"mat3">>(
value: V,
key?: string,
) => new DynoMat3({ key, value });
export const dynoMat3x2 = <V extends DynoJsType<"mat3x2">>(
value: V,
key?: string,
) => new DynoMat3x2({ key, value });
export const dynoMat3x3 = <V extends DynoJsType<"mat3x3">>(
value: V,
key?: string,
) => new DynoMat3x3({ key, value });
export const dynoMat3x4 = <V extends DynoJsType<"mat3x4">>(
value: V,
key?: string,
) => new DynoMat3x4({ key, value });
export const dynoMat4 = <V extends DynoJsType<"mat4">>(
value: V,
key?: string,
) => new DynoMat4({ key, value });
export const dynoMat4x2 = <V extends DynoJsType<"mat4x2">>(
value: V,
key?: string,
) => new DynoMat4x2({ key, value });
export const dynoMat4x3 = <V extends DynoJsType<"mat4x3">>(
value: V,
key?: string,
) => new DynoMat4x3({ key, value });
export const dynoMat4x4 = <V extends DynoJsType<"mat4x4">>(
value: V,
key?: string,
) => new DynoMat4x4({ key, value });
export const dynoUsampler2D = <V extends DynoJsType<"usampler2D">>(
value: V,
key?: string,
) => new DynoUsampler2D({ key, value });
export const dynoIsampler2D = <V extends DynoJsType<"isampler2D">>(
value: V,
key?: string,
) => new DynoIsampler2D({ key, value });
export const dynoSampler2D = <V extends DynoJsType<"sampler2D">>(
value: V,
key?: string,
) => new DynoSampler2D({ key, value });
export const dynoUsampler2DArray = <V extends DynoJsType<"usampler2DArray">>(
value: V,
key?: string,
) => new DynoUsampler2DArray({ key, value });
export const dynoIsampler2DArray = <V extends DynoJsType<"isampler2DArray">>(
key: string,
value: V,
) => new DynoIsampler2DArray({ key, value });
export const dynoSampler2DArray = <V extends DynoJsType<"sampler2DArray">>(
value: V,
key?: string,
) => new DynoSampler2DArray({ key, value });
export const dynoUsampler3D = <V extends DynoJsType<"usampler3D">>(
value: V,
key?: string,
) => new DynoUsampler3D({ key, value });
export const dynoIsampler3D = <V extends DynoJsType<"isampler3D">>(
value: V,
key?: string,
) => new DynoIsampler3D({ key, value });
export const dynoSampler3D = <V extends DynoJsType<"sampler3D">>(
value: V,
key?: string,
) => new DynoSampler3D({ key, value });
export const dynoUsamplerCube = <V extends DynoJsType<"usamplerCube">>(
value: V,
key?: string,
) => new DynoUsamplerCube({ key, value });
export const dynoIsamplerCube = <V extends DynoJsType<"isamplerCube">>(
value: V,
key?: string,
) => new DynoIsamplerCube({ key, value });
export const dynoSamplerCube = <V extends DynoJsType<"samplerCube">>(
value: V,
key?: string,
) => new DynoSamplerCube({ key, value });
export const dynoSampler2DShadow = <V extends DynoJsType<"sampler2DShadow">>(
value: V,
key?: string,
) => new DynoSampler2DShadow({ key, value });
export const dynoSampler2DArrayShadow = <
V extends DynoJsType<"sampler2DArrayShadow">,
>(
value: V,
key?: string,
) => new DynoSampler2DArrayShadow({ key, value });
export const dynoSamplerCubeShadow = <
V extends DynoJsType<"samplerCubeShadow">,
>(
value: V,
key?: string,
) => new DynoSamplerCubeShadow({ key, value });
export class DynoUniform<
T extends DynoType,
K extends string = "value",
V extends DynoJsType<T> = DynoJsType<T>,
>
extends Dyno<Record<string, never>, { [key in K]: T }>
implements HasDynoOut<T>
{
public type: T;
public count?: number;
public outKey: K;
public value: V;
public uniform: { value: V; type?: string };
constructor({
key,
type,
count,
value,
update,
globals,
}: {
key?: K;
type: T;
count?: number;
value: V;
update?: (value: V) => V | undefined;
globals?: ({
inputs,
outputs,
}: { inputs: unknown; outputs: { [key in K]?: string } }) => string[];
}) {
key = (key ?? "value") as K;
super({
outTypes: { [key]: type } as { [key in K]: T },
update: () => {
if (update) {
const value = update(this.value);
if (value !== undefined) {
this.value = value;
}
}
this.uniform.value = this.value;
},
generate: ({ inputs, outputs }) => {
const allGlobals = globals?.({ inputs, outputs }) ?? [];
const uniforms: Record<string, IUniform> = {};
const name = outputs[key];
if (name) {
allGlobals.push(`uniform ${dynoDeclare(name, type, count)};`);
uniforms[name] = this.uniform;
}
return { globals: allGlobals, uniforms };
},
});
this.type = type;
this.count = count;
this.value = value;
this.uniform = { value };
this.outKey = key;
}
dynoOut(): DynoValue<T> {
return new DynoOutput(this, this.outKey);
}
}
export class DynoBool<K extends string> extends DynoUniform<
"bool",
K,
boolean
> {
constructor({
key,
value,
update,
}: {
key?: K;
value: boolean;
update?: (value: boolean) => boolean | undefined;
}) {
super({ key, type: "bool", value, update });
}
}
export class DynoUint<K extends string> extends DynoUniform<"uint", K, number> {
constructor({
key,
value,
update,
}: {
key?: K;
value: number;
update?: (value: number) => number | undefined;
}) {
super({ key, type: "uint", value, update });
}
}
export class DynoInt<K extends string> extends DynoUniform<"int", K, number> {
constructor({
key,
value,
update,
}: {
key?: K;
value: number;
update?: (value: number) => number | undefined;
}) {
super({ key, type: "int", value, update });
}
}
export class DynoFloat<K extends string = "value"> extends DynoUniform<
"float",
K,
number
> {
constructor({
key,
value,
update,
}: {
key?: K;
value: number;
update?: (value: number) => number | undefined;
}) {
super({ key, type: "float", value, update });
}
}
export class DynoBvec2<
K extends string,
V extends DynoJsType<"bvec2">,
> extends DynoUniform<"bvec2", K, V> {
constructor({
key,
value,
update,
}: { key?: K; value: V; update?: (value: V) => V | undefined }) {
super({ key, type: "bvec2", value, update });
}
}
export class DynoUvec2<
K extends string,
V extends DynoJsType<"uvec2">,
> extends DynoUniform<"uvec2", K, V> {
constructor({
key,
value,
update,
}: { key?: K; value: V; update?: (value: V) => V | undefined }) {
super({ key, type: "uvec2", value, update });
}
}
export class DynoIvec2<
K extends string,
V extends DynoJsType<"ivec2">,
> extends DynoUniform<"ivec2", K, V> {
constructor({
key,
value,
update,
}: { key?: K; value: V; update?: (value: V) => V | undefined }) {
super({ key, type: "ivec2", value, update });
}
}
export class DynoVec2<
K extends string,
V extends DynoJsType<"vec2">,
> extends DynoUniform<"vec2", K, V> {
constructor({
key,
value,
update,
}: { key?: K; value: V; update?: (value: V) => V | undefined }) {
super({ key, type: "vec2", value, update });
}
}
export class DynoBvec3<
K extends string,
V extends DynoJsType<"bvec3">,
> extends DynoUniform<"bvec3", K, V> {
constructor({
key,
value,
update,
}: { key?: K; value: V; update?: (value: V) => V | undefined }) {
super({ key, type: "bvec3", value, update });
}
}
export class DynoUvec3<
V extends DynoJsType<"uvec3">,
K extends string = "value",
> extends DynoUniform<"uvec3", K, V> {
constructor({
key,
value,
update,
}: { key?: K; value: V; update?: (value: V) => V | undefined }) {
super({ key, type: "uvec3", value, update });
}
}
export class DynoIvec3<
V extends DynoJsType<"ivec3">,
K extends string = "value",
> extends DynoUniform<"ivec3", K, V> {
constructor({
key,
value,
update,
}: { key?: K; value: V; update?: (value: V) => V | undefined }) {
super({ key, type: "ivec3", value, update });
}
}
export class DynoVec3<
V extends DynoJsType<"vec3">,
K extends string = "value",
> extends DynoUniform<"vec3", K, V> {
constructor({
key,
value,
update,
}: { key?: K; value: V; update?: (value: V) => V | undefined }) {
super({ key, type: "vec3", value, update });
}
}
export class DynoBvec4<
K extends string,
V extends DynoJsType<"bvec4">,
> extends DynoUniform<"bvec4", K, V> {
constructor({
key,
value,
update,
}: { key?: K; value: V; update?: (value: V) => V | undefined }) {
super({ key, type: "bvec4", value, update });
}
}
export class DynoUvec4<
K extends string,
V extends DynoJsType<"uvec4">,
> extends DynoUniform<"uvec4", K, V> {
constructor({
key,
value,
update,
}: { key?: K; value: V; update?: (value: V) => V | undefined }) {
super({ key, type: "uvec4", value, update });
}
}
export class DynoIvec4<
K extends string,
V extends DynoJsType<"ivec4">,
> extends DynoUniform<"ivec4", K, V> {
constructor({
key,
value,
update,
}: { key?: K; value: V; update?: (value: V) => V | undefined }) {
super({ key, type: "ivec4", value, update });
}
}
export class DynoVec4<
V extends DynoJsType<"vec4">,
K extends string = "value",
> extends DynoUniform<"vec4", K, V> {
constructor({
key,
value,
update,
}: { key?: K; value: V; update?: (value: V) => V | undefined }) {
super({ key, type: "vec4", value, update });
}
}
export class DynoMat2<
K extends string,
V extends DynoJsType<"mat2">,
> extends DynoUniform<"mat2", K, V> {
constructor({
key,
value,
update,
}: { key?: K; value: V; update?: (value: V) => V | undefined }) {
super({ key, type: "mat2", value, update });
}
}
export class DynoMat2x2<
K extends string,
V extends DynoJsType<"mat2x2">,
> extends DynoUniform<"mat2x2", K, V> {
constructor({
key,
value,
update,
}: { key?: K; value: V; update?: (value: V) => V | undefined }) {
super({ key, type: "mat2x2", value, update });
}
}
export class DynoMat2x3<
K extends string,
V extends DynoJsType<"mat2x3">,
> extends DynoUniform<"mat2x3", K, V> {
constructor({
key,
value,
update,
}: { key?: K; value: V; update?: (value: V) => V | undefined }) {
super({ key, type: "mat2x3", value, update });
}
}
export class DynoMat2x4<
K extends string,
V extends DynoJsType<"mat2x4">,
> extends DynoUniform<"mat2x4", K, V> {
constructor({
key,
value,
update,
}: { key?: K; value: V; update?: (value: V) => V | undefined }) {
super({ key, type: "mat2x4", value, update });
}
}
export class DynoMat3<
K extends string,
V extends DynoJsType<"mat3">,
> extends DynoUniform<"mat3", K, V> {
constructor({
key,
value,
update,
}: { key?: K; value: V; update?: (value: V) => V | undefined }) {
super({ key, type: "mat3", value, update });
}
}
export class DynoMat3x2<
K extends string,
V extends DynoJsType<"mat3x2">,
> extends DynoUniform<"mat3x2", K, V> {
constructor({
key,
value,
update,
}: { key?: K; value: V; update?: (value: V) => V | undefined }) {
super({ key, type: "mat3x2", value, update });
}
}
export class DynoMat3x3<
K extends string,
V extends DynoJsType<"mat3x3">,
> extends DynoUniform<"mat3x3", K, V> {
constructor({
key,
value,
update,
}: { key?: K; value: V; update?: (value: V) => V | undefined }) {
super({ key, type: "mat3x3", value, update });
}
}
export class DynoMat3x4<
K extends string,
V extends DynoJsType<"mat3x4">,
> extends DynoUniform<"mat3x4", K, V> {
constructor({
key,
value,
update,
}: { key?: K; value: V; update?: (value: V) => V | undefined }) {
super({ key, type: "mat3x4", value, update });
}
}
export class DynoMat4<
K extends string,
V extends DynoJsType<"mat4">,
> extends DynoUniform<"mat4", K, V> {
constructor({
key,
value,
update,
}: { key?: K; value: V; update?: (value: V) => V | undefined }) {
super({ key, type: "mat4", value, update });
}
}
export class DynoMat4x2<
K extends string,
V extends DynoJsType<"mat4x2">,
> extends DynoUniform<"mat4x2", K, V> {
constructor({
key,
value,
update,
}: { key?: K; value: V; update?: (value: V) => V | undefined }) {
super({ key, type: "mat4x2", value, update });
}
}
export class DynoMat4x3<
K extends string,
V extends DynoJsType<"mat4x3">,
> extends DynoUniform<"mat4x3", K, V> {
constructor({
key,
value,
update,
}: { key?: K; value: V; update?: (value: V) => V | undefined }) {
super({ key, type: "mat4x3", value, update });
}
}
export class DynoMat4x4<
K extends string,
V extends DynoJsType<"mat4x4">,
> extends DynoUniform<"mat4x4", K, V> {
constructor({
key,
value,
update,
}: { key?: K; value: V; update?: (value: V) => V | undefined }) {
super({ key, type: "mat4x4", value, update });
}
}
export class DynoUsampler2D<
K extends string,
V extends DynoJsType<"usampler2D">,
> extends DynoUniform<"usampler2D", K, V> {
constructor({
key,
value,
update,
}: { key?: K; value: V; update?: (value: V) => V | undefined }) {
super({ key, type: "usampler2D", value, update });
}
}
export class DynoIsampler2D<
K extends string,
V extends DynoJsType<"isampler2D">,
> extends DynoUniform<"isampler2D", K, V> {
constructor({
key,
value,
update,
}: { key?: K; value: V; update?: (value: V) => V | undefined }) {
super({ key, type: "isampler2D", value, update });
}
}
export class DynoSampler2D<
K extends string,
V extends DynoJsType<"sampler2D">,
> extends DynoUniform<"sampler2D", K, V> {
constructor({
key,
value,
update,
}: { key?: K; value: V; update?: (value: V) => V | undefined }) {
super({ key, type: "sampler2D", value, update });
}
}
export class DynoUsampler2DArray<
K extends string,
V extends DynoJsType<"usampler2DArray">,
> extends DynoUniform<"usampler2DArray", K, V> {
constructor({
key,
value,
update,
}: { key?: K; value: V; update?: (value: V) => V | undefined }) {
super({ key, type: "usampler2DArray", value, update });
}
}
export class DynoIsampler2DArray<
K extends string,
V extends DynoJsType<"isampler2DArray">,
> extends DynoUniform<"isampler2DArray", K, V> {
constructor({
key,
value,
update,
}: { key?: K; value: V; update?: (value: V) => V | undefined }) {
super({ key, type: "isampler2DArray", value, update });
}
}
export class DynoSampler2DArray<
K extends string,
V extends DynoJsType<"sampler2DArray">,
> extends DynoUniform<"sampler2DArray", K, V> {
constructor({
key,
value,
update,
}: { key?: K; value: V; update?: (value: V) => V | undefined }) {
super({ key, type: "sampler2DArray", value, update });
}
}
export class DynoUsampler3D<
K extends string,
V extends DynoJsType<"usampler3D">,
> extends DynoUniform<"usampler3D", K, V> {
constructor({
key,
value,
update,
}: { key?: K; value: V; update?: (value: V) => V | undefined }) {
super({ key, type: "usampler3D", value, update });
}
}
export class DynoIsampler3D<
K extends string,
V extends DynoJsType<"isampler3D">,
> extends DynoUniform<"isampler3D", K, V> {
constructor({
key,
value,
update,
}: { key?: K; value: V; update?: (value: V) => V | undefined }) {
super({ key, type: "isampler3D", value, update });
}
}
export class DynoSampler3D<
K extends string,
V extends DynoJsType<"sampler3D">,
> extends DynoUniform<"sampler3D", K, V> {
constructor({
key,
value,
update,
}: { key?: K; value: V; update?: (value: V) => V | undefined }) {
super({ key, type: "sampler3D", value, update });
}
}
export class DynoUsamplerCube<
K extends string,
V extends DynoJsType<"usamplerCube">,
> extends DynoUniform<"usamplerCube", K, V> {
constructor({
key,
value,
update,
}: { key?: K; value: V; update?: (value: V) => V | undefined }) {
super({ key, type: "usamplerCube", value, update });
}
}
export class DynoIsamplerCube<
K extends string,
V extends DynoJsType<"isamplerCube">,
> extends DynoUniform<"isamplerCube", K, V> {
constructor({
key,
value,
update,
}: { key?: K; value: V; update?: (value: V) => V | undefined }) {
super({ key, type: "isamplerCube", value, update });
}
}
export class DynoSamplerCube<
K extends string,
V extends DynoJsType<"samplerCube">,
> extends DynoUniform<"samplerCube", K, V> {
constructor({
key,
value,
update,
}: { key?: K; value: V; update?: (value: V) => V | undefined }) {
super({ key, type: "samplerCube", value, update });
}
}
export class DynoSampler2DShadow<
K extends string,
V extends DynoJsType<"sampler2DShadow">,
> extends DynoUniform<"sampler2DShadow", K, V> {
constructor({
key,
value,
update,
}: { key?: K; value: V; update?: (value: V) => V | undefined }) {
super({ key, type: "sampler2DShadow", value, update });
}
}
export class DynoSampler2DArrayShadow<
K extends string,
V extends DynoJsType<"sampler2DArrayShadow">,
> extends DynoUniform<"sampler2DArrayShadow", K, V> {
constructor({
key,
value,
update,
}: { key?: K; value: V; update?: (value: V) => V | undefined }) {
super({ key, type: "sampler2DArrayShadow", value, update });
}
}
export class DynoSamplerCubeShadow<
K extends string,
V extends DynoJsType<"samplerCubeShadow">,
> extends DynoUniform<"samplerCubeShadow", K, V> {
constructor({
key,
value,
update,
}: { key?: K; value: V; update?: (value: V) => V | undefined }) {
super({ key, type: "samplerCubeShadow", value, update });
}
}
+419
View File
@@ -0,0 +1,419 @@
import { Dyno, DynoBlock, unindent } from "./base";
import { float, vec2, vec3, vec4 } from "./convert";
import { mul } from "./math";
import {
ScalarTypes,
type ValueTypes,
Vector2Types,
Vector3Types,
Vector4Types,
isIntType,
isScalarType,
isUintType,
isVector2Type,
isVector3Type,
isVector4Type,
sameSizeUvec,
} from "./types";
import {
DynoOutput,
type DynoVal,
type DynoValue,
type HasDynoOut,
dynoConst,
valType,
} from "./value";
import { combine } from "./vecmat";
export const remapIndex = (
index: DynoVal<"int">,
from: DynoVal<"int">,
to: DynoVal<"int">,
): DynoVal<"int"> => {
return new DynoRemapIndex({ index, from, to });
};
export const pcgMix = <T extends ValueTypes>(
value: DynoVal<T>,
): DynoVal<"uint"> => {
return new PcgMix({ value });
};
export const pcgNext = (state: DynoVal<"uint">): DynoVal<"uint"> => {
return new PcgNext({ state });
};
export const pcgHash = (state: DynoVal<"uint">): DynoVal<"uint"> => {
return new PcgHash({ state });
};
export const hash = <T extends ValueTypes>(
value: DynoVal<T>,
): DynoVal<"uint"> => {
return new Hash({ value });
};
export const hash2 = <T extends ValueTypes>(
value: DynoVal<T>,
): DynoVal<"uvec2"> => {
return new Hash2({ value });
};
export const hash3 = <T extends ValueTypes>(
value: DynoVal<T>,
): DynoVal<"uvec3"> => {
return new Hash3({ value });
};
export const hash4 = <T extends ValueTypes>(
value: DynoVal<T>,
): DynoVal<"uvec4"> => {
return new Hash4({ value });
};
export const hashFloat = <T extends ValueTypes>(
value: DynoVal<T>,
): DynoVal<"float"> => {
return new HashFloat({ value });
};
export const hashVec2 = <T extends ValueTypes>(
value: DynoVal<T>,
): DynoVal<"vec2"> => {
return new HashVec2({ value });
};
export const hashVec3 = <T extends ValueTypes>(
value: DynoVal<T>,
): DynoVal<"vec3"> => {
return new HashVec3({ value });
};
export const hashVec4 = <T extends ValueTypes>(
value: DynoVal<T>,
): DynoVal<"vec4"> => {
return new HashVec4({ value });
};
export class DynoRemapIndex
extends Dyno<{ from: "int"; to: "int"; index: "int" }, { index: "int" }>
implements HasDynoOut<"int">
{
constructor({
from,
to,
index,
}: { from: DynoVal<"int">; to: DynoVal<"int">; index: DynoVal<"int"> }) {
super({
inTypes: { from: "int", to: "int", index: "int" },
outTypes: { index: "int" },
inputs: { from, to, index },
statements: ({ inputs, outputs }) => {
return [
`${outputs.index} = ${inputs.index} - ${inputs.from} + ${inputs.to};`,
];
},
});
}
dynoOut(): DynoValue<"int"> {
return new DynoOutput(this, "index");
}
}
export class PcgNext<T extends "uint" | "int" | "float">
extends Dyno<{ state: T }, { state: "uint" }>
implements HasDynoOut<"uint">
{
constructor({ state }: { state: DynoVal<T> }) {
const type = valType(state);
super({
inTypes: { state: type },
outTypes: { state: "uint" },
inputs: { state },
globals: () => [
unindent(`
uint pcg_next(uint state) {
return state * 747796405u + 2891336453u;
}
`),
],
statements: ({ inputs, outputs }) => {
const toUint =
type === "uint"
? `${inputs.state}`
: type === "int"
? `uint(${inputs.state})`
: `floatBitsToUint(${inputs.state})`;
return [`${outputs.state} = pcg_next(${toUint});`];
},
});
}
dynoOut(): DynoValue<"uint"> {
return new DynoOutput(this, "state");
}
}
export class PcgHash
extends Dyno<{ state: "uint" }, { hash: "uint" }>
implements HasDynoOut<"uint">
{
constructor({ state }: { state: DynoVal<"uint"> }) {
super({
inTypes: { state: "uint" },
outTypes: { hash: "uint" },
inputs: { state },
globals: () => [
unindent(`
uint pcg_hash(uint state) {
uint hash = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u;
return (hash >> 22u) ^ hash;
}
`),
],
statements: ({ inputs, outputs }) => [
`${outputs.hash} = pcg_hash(${inputs.state});`,
],
});
}
dynoOut(): DynoValue<"uint"> {
return new DynoOutput(this, "hash");
}
}
export class PcgMix<T extends ValueTypes>
extends Dyno<{ value: T }, { state: "uint" }>
implements HasDynoOut<"uint">
{
constructor({ value }: { value: DynoVal<T> }) {
const type = valType(value);
const tempType = sameSizeUvec(type);
super({
inTypes: { value: type },
outTypes: { state: "uint" },
inputs: { value },
globals: () => [
unindent(`
uint pcg_mix(uint value) {
return value;
}
uint pcg_mix(uvec2 value) {
return value.x + 0x9e3779b9u * value.y;
}
uint pcg_mix(uvec3 value) {
return value.x + 0x9e3779b9u * value.y + 0x85ebca6bu * value.z;
}
uint pcg_mix(uvec4 value) {
return value.x + 0x9e3779b9u * value.y + 0x85ebca6bu * value.z + 0xc2b2ae35u * value.w;
}
`),
],
statements: ({ inputs, outputs }) => {
const toUvec = isUintType(type)
? `${inputs.value}`
: isIntType(type)
? `${tempType}(${inputs.value})`
: `floatBitsToUint(${inputs.value})`;
return [
`${tempType} bits = ${toUvec};`,
`${outputs.state} = pcg_mix(bits);`,
];
},
});
}
dynoOut(): DynoValue<"uint"> {
return new DynoOutput(this, "state");
}
}
export class Hash<T extends ValueTypes>
extends DynoBlock<{ value: T }, { hash: "uint" }>
implements HasDynoOut<"uint">
{
constructor({ value }: { value: DynoVal<T> }) {
super({
inTypes: { value: valType(value) },
outTypes: { hash: "uint" },
inputs: { value },
construct: ({ value }) => {
if (!value) {
throw new Error("value is required");
}
let state = new PcgMix({ value: value }).outputs.state;
state = new PcgNext({ state }).outputs.state;
return new PcgHash({ state }).outputs;
},
});
}
dynoOut(): DynoValue<"uint"> {
return new DynoOutput(this, "hash");
}
}
export class Hash2<T extends ValueTypes>
extends DynoBlock<{ value: T }, { hash: "uvec2" }>
implements HasDynoOut<"uvec2">
{
constructor({ value }: { value: DynoVal<T> }) {
super({
inTypes: { value: valType(value) },
outTypes: { hash: "uvec2" },
inputs: { value },
construct: ({ value }) => {
if (!value) {
throw new Error("value is required");
}
let state = new PcgMix({ value: value }).outputs.state;
state = new PcgNext({ state }).outputs.state;
const x = new PcgHash({ state }).outputs.hash;
state = new PcgNext({ state }).outputs.state;
const y = new PcgHash({ state }).outputs.hash;
return { hash: combine({ vectorType: "uvec2", x, y }) };
},
});
}
dynoOut(): DynoValue<"uvec2"> {
return new DynoOutput(this, "hash");
}
}
export class Hash3<T extends ValueTypes>
extends DynoBlock<{ value: T }, { hash: "uvec3" }>
implements HasDynoOut<"uvec3">
{
constructor({ value }: { value: DynoVal<T> }) {
super({
inTypes: { value: valType(value) },
outTypes: { hash: "uvec3" },
inputs: { value },
construct: ({ value }) => {
if (!value) {
throw new Error("value is required");
}
let state = new PcgMix({ value: value }).outputs.state;
state = new PcgNext({ state }).outputs.state;
const x = new PcgHash({ state }).outputs.hash;
state = new PcgNext({ state }).outputs.state;
const y = new PcgHash({ state }).outputs.hash;
state = new PcgNext({ state }).outputs.state;
const z = new PcgHash({ state }).outputs.hash;
return { hash: combine({ vectorType: "uvec3", x, y, z }) };
},
});
}
dynoOut(): DynoValue<"uvec3"> {
return new DynoOutput(this, "hash");
}
}
export class Hash4<T extends ValueTypes>
extends DynoBlock<{ value: T }, { hash: "uvec4" }>
implements HasDynoOut<"uvec4">
{
constructor({ value }: { value: DynoVal<T> }) {
super({
inTypes: { value: valType(value) },
outTypes: { hash: "uvec4" },
inputs: { value },
construct: ({ value }) => {
if (!value) {
throw new Error("value is required");
}
let state = new PcgMix({ value: value }).outputs.state;
state = new PcgNext({ state }).outputs.state;
const x = new PcgHash({ state }).outputs.hash;
state = new PcgNext({ state }).outputs.state;
const y = new PcgHash({ state }).outputs.hash;
state = new PcgNext({ state }).outputs.state;
const z = new PcgHash({ state }).outputs.hash;
state = new PcgNext({ state }).outputs.state;
const w = new PcgHash({ state }).outputs.hash;
return { hash: combine({ vectorType: "uvec4", x, y, z, w }) };
},
});
}
dynoOut(): DynoValue<"uvec4"> {
return new DynoOutput(this, "hash");
}
}
export class HashFloat<T extends ValueTypes>
extends DynoBlock<{ value: T }, { hash: "float" }>
implements HasDynoOut<"float">
{
constructor({ value }: { value: DynoVal<T> }) {
super({
inTypes: { value: valType(value) },
outTypes: { hash: "float" },
inputs: { value },
construct: ({ value }) => {
if (!value) {
throw new Error("value is required");
}
const word = hash(value);
return { hash: mul(float(word), dynoConst("float", 1 / 2 ** 32)) };
},
});
}
dynoOut(): DynoValue<"float"> {
return new DynoOutput(this, "hash");
}
}
export class HashVec2<T extends ValueTypes>
extends DynoBlock<{ value: T }, { hash: "vec2" }>
implements HasDynoOut<"vec2">
{
constructor({ value }: { value: DynoVal<T> }) {
super({
inTypes: { value: valType(value) },
outTypes: { hash: "vec2" },
inputs: { value },
construct: ({ value }) => {
if (!value) {
throw new Error("value is required");
}
const words = hash2(value);
return { hash: mul(vec2(words), dynoConst("float", 1 / 2 ** 32)) };
},
});
}
dynoOut(): DynoValue<"vec2"> {
return new DynoOutput(this, "hash");
}
}
export class HashVec3<T extends ValueTypes>
extends DynoBlock<{ value: T }, { hash: "vec3" }>
implements HasDynoOut<"vec3">
{
constructor({ value }: { value: DynoVal<T> }) {
super({
inTypes: { value: valType(value) },
outTypes: { hash: "vec3" },
inputs: { value },
construct: ({ value }) => {
if (!value) {
throw new Error("value is required");
}
const words = hash3(value);
return { hash: mul(vec3(words), dynoConst("float", 1 / 2 ** 32)) };
},
});
}
dynoOut(): DynoValue<"vec3"> {
return new DynoOutput(this, "hash");
}
}
export class HashVec4<T extends ValueTypes>
extends DynoBlock<{ value: T }, { hash: "vec4" }>
implements HasDynoOut<"vec4">
{
constructor({ value }: { value: DynoVal<T> }) {
super({
inTypes: { value: valType(value) },
outTypes: { hash: "vec4" },
inputs: { value },
construct: ({ value }) => {
if (!value) {
throw new Error("value is required");
}
const words = hash4(value);
return { hash: mul(vec4(words), dynoConst("float", 1 / 2 ** 32)) };
},
});
}
dynoOut(): DynoValue<"vec4"> {
return new DynoOutput(this, "hash");
}
}
+289
View File
@@ -0,0 +1,289 @@
import * as THREE from "three";
import type { Dyno, IOTypes } from "./base";
import {
type DynoJsType,
type DynoType,
type SimpleTypes,
isAllFloatType,
isBoolType,
isIntType,
isUintType,
numberAsFloat,
numberAsInt,
numberAsUint,
} from "./types";
export type DynoVal<T extends DynoType> = DynoValue<T> | HasDynoOut<T>;
export function valType<T extends DynoType>(val: DynoVal<T>): T {
if (val instanceof DynoValue) {
return val.type;
}
const value = val.dynoOut();
return value.type;
}
export interface HasDynoOut<T extends DynoType> {
dynoOut(): DynoValue<T>;
}
export class DynoValue<T extends DynoType> {
type: T;
// This field prevents TypeScript structural matching on objects with a "type" field
private __isDynoValue = true;
constructor(type: T) {
this.type = type;
}
}
export class DynoOutput<
T extends DynoType,
InTypes extends IOTypes,
OutTypes extends IOTypes,
> extends DynoValue<T> {
dyno: Dyno<InTypes, OutTypes>;
key: string;
constructor(dyno: Dyno<InTypes, OutTypes>, key: string) {
super(dyno.outTypes[key] as T);
this.dyno = dyno;
this.key = key;
}
}
export class DynoLiteral<T extends DynoType> extends DynoValue<T> {
literal: string;
constructor(type: T, literal: string) {
super(type);
this.literal = literal;
}
getLiteral(): string {
return this.literal;
}
}
export function dynoLiteral<T extends DynoType>(
type: T,
literal: string,
): DynoLiteral<T> {
return new DynoLiteral(type, literal);
}
export class DynoConst<T extends DynoType> extends DynoLiteral<T> {
value: DynoJsType<T>;
constructor(type: T, value: DynoJsType<T>) {
super(type, "");
this.value = value;
}
getLiteral(): string {
const { type, value } = this;
switch (type) {
case "bool":
return value ? "true" : "false";
case "uint":
return numberAsUint(value as number);
case "int":
return numberAsInt(value as number);
case "float":
return numberAsFloat(value as number);
case "bvec2": {
const v = value as [boolean, boolean];
return `bvec2(${v[0]}, ${v[1]})`;
}
case "uvec2": {
if (value instanceof THREE.Vector2) {
return `uvec2(${numberAsUint(value.x)}, ${numberAsUint(value.y)})`;
}
const v = value as [number, number] | Uint32Array;
return `uvec2(${numberAsUint(v[0])}, ${numberAsUint(v[1])})`;
}
case "ivec2": {
if (value instanceof THREE.Vector2) {
return `ivec2(${numberAsInt(value.x)}, ${numberAsInt(value.y)})`;
}
const v = value as [number, number] | Int32Array;
return `ivec2(${numberAsInt(v[0])}, ${numberAsInt(v[1])})`;
}
case "vec2": {
if (value instanceof THREE.Vector2) {
return `vec2(${numberAsFloat(value.x)}, ${numberAsFloat(value.y)})`;
}
const v = value as [number, number] | Float32Array;
return `vec2(${numberAsFloat(v[0])}, ${numberAsFloat(v[1])})`;
}
case "bvec3": {
const v = value as [boolean, boolean, boolean];
return `bvec3(${v[0]}, ${v[1]}, ${v[2]})`;
}
case "uvec3": {
if (value instanceof THREE.Vector3) {
return `uvec3(${numberAsUint(value.x)}, ${numberAsUint(value.y)}, ${numberAsUint(value.z)})`;
}
const v = value as [number, number, number] | Uint32Array;
return `uvec3(${numberAsUint(v[0])}, ${numberAsUint(v[1])}, ${numberAsUint(v[2])})`;
}
case "ivec3": {
if (value instanceof THREE.Vector3) {
return `ivec3(${numberAsInt(value.x)}, ${numberAsInt(value.y)}, ${numberAsInt(value.z)})`;
}
const v = value as [number, number, number] | Int32Array;
return `ivec3(${numberAsInt(v[0])}, ${numberAsInt(v[1])}, ${numberAsInt(v[2])})`;
}
case "vec3": {
if (value instanceof THREE.Vector3) {
return `vec3(${numberAsFloat(value.x)}, ${numberAsFloat(value.y)}, ${numberAsFloat(value.z)})`;
}
const v = value as [number, number, number] | Float32Array;
return `vec3(${numberAsFloat(v[0])}, ${numberAsFloat(v[1])}, ${numberAsFloat(v[2])})`;
}
case "bvec4": {
const v = value as [boolean, boolean, boolean, boolean];
return `bvec4(${v[0]}, ${v[1]}, ${v[2]}, ${v[3]})`;
}
case "uvec4": {
if (value instanceof THREE.Vector4) {
return `uvec4(${numberAsUint(value.x)}, ${numberAsUint(value.y)}, ${numberAsUint(value.z)}, ${numberAsUint(value.w)})`;
}
const v = value as [number, number, number, number] | Uint32Array;
return `uvec4(${numberAsUint(v[0])}, ${numberAsUint(v[1])}, ${numberAsUint(v[2])}, ${numberAsUint(v[3])})`;
}
case "ivec4": {
if (value instanceof THREE.Vector4) {
return `ivec4(${numberAsInt(value.x)}, ${numberAsInt(value.y)}, ${numberAsInt(value.z)}, ${numberAsInt(value.w)})`;
}
const v = value as [number, number, number, number] | Int32Array;
return `ivec4(${numberAsInt(v[0])}, ${numberAsInt(v[1])}, ${numberAsInt(v[2])}, ${numberAsInt(v[3])})`;
}
case "vec4": {
if (value instanceof THREE.Vector4) {
return `vec4(${numberAsFloat(value.x)}, ${numberAsFloat(value.y)}, ${numberAsFloat(value.z)}, ${numberAsFloat(value.w)})`;
}
if (value instanceof THREE.Quaternion) {
return `vec4(${numberAsFloat(value.x)}, ${numberAsFloat(value.y)}, ${numberAsFloat(value.z)}, ${numberAsFloat(value.w)})`;
}
const v = value as [number, number, number, number] | Float32Array;
return `vec4(${numberAsFloat(v[0])}, ${numberAsFloat(v[1])}, ${numberAsFloat(v[2])}, ${numberAsFloat(v[3])})`;
}
case "mat2":
case "mat2x2": {
const m = value as DynoJsType<"mat2">;
const e =
m instanceof THREE.Matrix2 ? m.elements : (value as Float32Array);
const arg = new Array(4).fill(0).map((_, i) => numberAsFloat(e[i]));
return `${type as string}(${arg.join(", ")})`;
}
case "mat2x3": {
const e = value as DynoJsType<"mat2x3">;
const arg = new Array(6).fill(0).map((_, i) => numberAsFloat(e[i]));
return `${type as string}(${arg.join(", ")})`;
}
case "mat2x4": {
const e = value as DynoJsType<"mat2x4">;
const arg = new Array(8).fill(0).map((_, i) => numberAsFloat(e[i]));
return `${type as string}(${arg.join(", ")})`;
}
case "mat3":
case "mat3x3": {
const m = value as DynoJsType<"mat3">;
const e =
m instanceof THREE.Matrix3 ? m.elements : (value as Float32Array);
const arg = new Array(9).fill(0).map((_, i) => numberAsFloat(e[i]));
return `${type as string}(${arg.join(", ")})`;
}
case "mat3x2": {
const e = value as DynoJsType<"mat3x2">;
const arg = new Array(6).fill(0).map((_, i) => numberAsFloat(e[i]));
return `${type as string}(${arg.join(", ")})`;
}
case "mat3x4": {
const e = value as DynoJsType<"mat3x4">;
const arg = new Array(12).fill(0).map((_, i) => numberAsFloat(e[i]));
return `${type as string}(${arg.join(", ")})`;
}
case "mat4":
case "mat4x4": {
const m = value as DynoJsType<"mat4">;
const e =
m instanceof THREE.Matrix4 ? m.elements : (value as Float32Array);
const arg = new Array(16).fill(0).map((_, i) => numberAsFloat(e[i]));
return `${type as string}(${arg.join(", ")})`;
}
case "mat4x2": {
const e = value as DynoJsType<"mat4x2">;
const arg = new Array(8).fill(0).map((_, i) => numberAsFloat(e[i]));
return `${type as string}(${arg.join(", ")})`;
}
case "mat4x3": {
const e = value as DynoJsType<"mat4x3">;
const arg = new Array(12).fill(0).map((_, i) => numberAsFloat(e[i]));
return `${type as string}(${arg.join(", ")})`;
}
default:
throw new Error(`Type not implemented: ${String(type)}`);
}
}
}
export function dynoConst<T extends DynoType>(
type: T,
value: DynoJsType<T>,
): DynoConst<T> {
return new DynoConst(type, value);
}
export function literalZero(type: SimpleTypes): string {
const typeString = String(type);
if (isBoolType(type)) {
return `${typeString}(false)`;
}
if (isAllFloatType(type)) {
return `${typeString}(0.0)`;
}
if (isIntType(type)) {
return `${typeString}(0)`;
}
if (isUintType(type)) {
return `${typeString}(0u)`;
}
throw new Error(`Type not implemented: ${typeString}`);
}
export function literalOne(type: SimpleTypes): string {
const typeString = String(type);
if (isBoolType(type)) {
return `${typeString}(true)`;
}
if (isAllFloatType(type)) {
return `${typeString}(1.0)`;
}
if (isIntType(type)) {
return `${typeString}(1)`;
}
if (isUintType(type)) {
return `${typeString}(1u)`;
}
throw new Error(`Type not implemented: ${typeString}`);
}
export function literalNegOne(type: SimpleTypes): string {
const typeString = String(type);
if (isBoolType(type)) {
return `${typeString}(true)`;
}
if (isAllFloatType(type)) {
return `${typeString}(-1.0)`;
}
if (isIntType(type)) {
return `${typeString}(-1)`;
}
if (isUintType(type)) {
return `${typeString}(0xFFFFFFFFu)`;
}
throw new Error(`Type not implemented: ${typeString}`);
}
+835
View File
@@ -0,0 +1,835 @@
import { BinaryOp, Dyno, TrinaryOp, UnaryOp } from "./base";
import {
type FloatTypes,
type IntTypes,
type MatFloatTypes,
type SquareMatTypes,
type UintTypes,
type VectorElementType,
type VectorTypes,
isFloatType,
isIntType,
isUintType,
vectorDim,
vectorElementType,
} from "./types";
import {
DynoOutput,
type DynoVal,
type DynoValue,
type HasDynoOut,
literalZero,
valType,
} from "./value";
export const length = <A extends "vec2" | "vec3" | "vec4">(
a: DynoVal<A>,
): DynoVal<"float"> => new Length({ a });
export const distance = <A extends "vec2" | "vec3" | "vec4">(
a: DynoVal<A>,
b: DynoVal<A>,
): DynoVal<"float"> => new Distance({ a, b });
export const dot = <A extends "vec2" | "vec3" | "vec4">(
a: DynoVal<A>,
b: DynoVal<A>,
): DynoVal<"float"> => new Dot({ a, b });
export const cross = (
a: DynoVal<"vec3">,
b: DynoVal<"vec3">,
): DynoVal<"vec3"> => new Cross({ a, b });
export const normalize = <A extends "vec2" | "vec3" | "vec4">(
a: DynoVal<A>,
): DynoVal<A> => new Normalize({ a });
export const faceforward = <A extends "vec2" | "vec3" | "vec4">(
a: DynoVal<A>,
b: DynoVal<A>,
c: DynoVal<A>,
): DynoVal<A> => new FaceForward({ a, b, c });
export const reflectVec = <A extends "vec2" | "vec3" | "vec4">(
incident: DynoVal<A>,
normal: DynoVal<A>,
): DynoVal<A> => new ReflectVec({ incident, normal });
export const refractVec = <A extends "vec2" | "vec3" | "vec4">(
incident: DynoVal<A>,
normal: DynoVal<A>,
eta: DynoVal<"float">,
): DynoVal<A> => new RefractVec({ incident, normal, eta });
export const split = <V extends VectorTypes>(vector: DynoVal<V>): Split<V> =>
new Split({ vector });
export const combine = <V extends VectorTypes, T extends VectorElementType<V>>({
vector,
vectorType,
x,
y,
z,
w,
r,
g,
b,
a,
}: {
vector?: DynoVal<V>;
vectorType?: V;
x?: DynoVal<T>;
y?: DynoVal<T>;
z?: DynoVal<T>;
w?: DynoVal<T>;
r?: DynoVal<T>;
g?: DynoVal<T>;
b?: DynoVal<T>;
a?: DynoVal<T>;
}): DynoVal<V> => new Combine({ vector, vectorType, x, y, z, w, r, g, b, a });
export const projectH = <A extends "vec3" | "vec4">(
a: DynoVal<A>,
): DynoVal<ProjectHOutput<A>> => new ProjectH({ a });
export const extendVec = <A extends "float" | "vec2" | "vec3">(
a: DynoVal<A>,
b: DynoVal<"float">,
): DynoVal<ExtendVecOutput<A>> => new ExtendVec({ a, b });
export const swizzle = <A extends VectorTypes, S extends SwizzleSelect>(
a: DynoVal<A>,
select: S,
): DynoVal<SwizzleOutput<A, SwizzleSelectLen<S>>> =>
new Swizzle({ vector: a, select });
export const compMult = <A extends MatFloatTypes>(
a: DynoVal<A>,
b: DynoVal<A>,
): DynoVal<A> => new CompMult({ a, b });
export const outer = <
A extends "vec2" | "vec3" | "vec4",
B extends "vec2" | "vec3" | "vec4",
>(
a: DynoVal<A>,
b: DynoVal<B>,
): DynoVal<OuterOutput<A, B>> => new Outer({ a, b });
export const transpose = <A extends MatFloatTypes>(
a: DynoVal<A>,
): DynoVal<TransposeOutput<A>> => new Transpose({ a });
export const determinant = <A extends SquareMatTypes>(
a: DynoVal<A>,
): DynoVal<"float"> => new Determinant({ a });
export const inverse = <A extends SquareMatTypes>(a: DynoVal<A>): DynoVal<A> =>
new Inverse({ a });
export class Length<A extends "vec2" | "vec3" | "vec4"> extends UnaryOp<
A,
"float",
"length"
> {
constructor({ a }: { a: DynoVal<A> }) {
super({ a, outTypeFunc: (aType) => "float", outKey: "length" });
this.statements = ({ inputs, outputs }) => [
`${outputs.length} = length(${inputs.a});`,
];
}
}
export class Distance<A extends "vec2" | "vec3" | "vec4"> extends BinaryOp<
A,
A,
"float",
"distance"
> {
constructor({ a, b }: { a: DynoVal<A>; b: DynoVal<A> }) {
super({ a, b, outKey: "distance", outTypeFunc: (aType, bType) => "float" });
this.statements = ({ inputs, outputs }) => [
`${outputs.distance} = distance(${inputs.a}, ${inputs.b});`,
];
}
}
export class Dot<A extends "vec2" | "vec3" | "vec4"> extends BinaryOp<
A,
A,
"float",
"dot"
> {
constructor({ a, b }: { a: DynoVal<A>; b: DynoVal<A> }) {
super({ a, b, outKey: "dot", outTypeFunc: (aType, bType) => "float" });
this.statements = ({ inputs, outputs }) => [
`${outputs.dot} = dot(${inputs.a}, ${inputs.b});`,
];
}
}
export class Cross extends BinaryOp<"vec3", "vec3", "vec3", "cross"> {
constructor({ a, b }: { a: DynoVal<"vec3">; b: DynoVal<"vec3"> }) {
super({ a, b, outKey: "cross", outTypeFunc: (aType, bType) => "vec3" });
this.statements = ({ inputs, outputs }) => [
`${outputs.cross} = cross(${inputs.a}, ${inputs.b});`,
];
}
}
export class Normalize<A extends "vec2" | "vec3" | "vec4"> extends UnaryOp<
A,
A,
"normalize"
> {
constructor({ a }: { a: DynoVal<A> }) {
super({ a, outTypeFunc: (aType) => aType, outKey: "normalize" });
this.statements = ({ inputs, outputs }) => [
`${outputs.normalize} = normalize(${inputs.a});`,
];
}
}
type ProjectHOutput<A extends "vec3" | "vec4"> = A extends "vec3"
? "vec2"
: A extends "vec4"
? "vec3"
: never;
function projectHOutputType<A extends "vec3" | "vec4">(
type: A,
): ProjectHOutput<A> {
if (type === "vec3") {
return "vec2" as ProjectHOutput<A>;
}
if (type === "vec4") {
return "vec3" as ProjectHOutput<A>;
}
throw new Error("Invalid type");
}
export class ProjectH<A extends "vec3" | "vec4"> extends UnaryOp<
A,
ProjectHOutput<A>,
"projected"
> {
constructor({ a }: { a: DynoVal<A> }) {
super({
a,
outTypeFunc: (aType) => projectHOutputType(aType),
outKey: "projected",
});
this.statements = ({ inputs, outputs }) => {
if (this.inTypes.a === "vec3") {
return [`${outputs.projected} = ${inputs.a}.xy / ${inputs.a}.z;`];
}
if (this.inTypes.a === "vec4") {
return [`${outputs.projected} = ${inputs.a}.xyz / ${inputs.a}.w;`];
}
throw new Error("Invalid type");
};
}
}
type ExtendVecOutput<A extends "float" | "vec2" | "vec3"> = A extends "float"
? "vec2"
: A extends "vec2"
? "vec3"
: A extends "vec3"
? "vec4"
: never;
function extendVecOutputType<A extends "float" | "vec2" | "vec3">(
type: A,
): ExtendVecOutput<A> {
if (type === "float") return "vec2" as ExtendVecOutput<A>;
if (type === "vec2") return "vec3" as ExtendVecOutput<A>;
if (type === "vec3") return "vec4" as ExtendVecOutput<A>;
throw new Error("Invalid type");
}
export class ExtendVec<A extends "float" | "vec2" | "vec3"> extends BinaryOp<
A,
"float",
ExtendVecOutput<A>,
"extend"
> {
constructor({ a, b }: { a: DynoVal<A>; b: DynoVal<"float"> }) {
const type = valType(a);
const outType = extendVecOutputType(type);
super({ a, b, outKey: "extend", outTypeFunc: () => outType });
this.statements = ({ inputs, outputs }) => [
`${outputs.extend} = ${outType}(${inputs.a}, ${inputs.b});`,
];
}
}
export class FaceForward<A extends "vec2" | "vec3" | "vec4"> extends TrinaryOp<
A,
A,
A,
A,
"forward"
> {
constructor({ a, b, c }: { a: DynoVal<A>; b: DynoVal<A>; c: DynoVal<A> }) {
super({
a,
b,
c,
outKey: "forward",
outTypeFunc: (aType, bType, cType) => aType,
});
this.statements = ({ inputs, outputs }) => [
`${outputs.forward} = faceforward(${inputs.a}, ${inputs.b}, ${inputs.c});`,
];
}
}
export class ReflectVec<A extends "vec2" | "vec3" | "vec4"> extends BinaryOp<
A,
A,
A,
"reflection"
> {
constructor({
incident,
normal,
}: { incident: DynoVal<A>; normal: DynoVal<A> }) {
super({
a: incident,
b: normal,
outKey: "reflection",
outTypeFunc: (aType, bType) => aType,
});
this.statements = ({ inputs, outputs }) => [
`${outputs.reflection} = reflect(${inputs.a}, ${inputs.b});`,
];
}
}
export class RefractVec<A extends "vec2" | "vec3" | "vec4"> extends TrinaryOp<
A,
A,
"float",
A,
"refraction"
> {
constructor({
incident,
normal,
eta,
}: { incident: DynoVal<A>; normal: DynoVal<A>; eta: DynoVal<"float"> }) {
super({
a: incident,
b: normal,
c: eta,
outKey: "refraction",
outTypeFunc: (aType, bType, cType) => aType,
});
this.statements = ({ inputs, outputs }) => [
`${outputs.refraction} = refract(${inputs.a}, ${inputs.b}, ${inputs.c});`,
];
}
}
export class CompMult<A extends MatFloatTypes> extends BinaryOp<
A,
A,
A,
"product"
> {
constructor({ a, b }: { a: DynoVal<A>; b: DynoVal<A> }) {
super({ a, b, outKey: "product", outTypeFunc: (aType, bType) => aType });
this.statements = ({ inputs, outputs }) => [
`${outputs.product} = matrixCompMult(${a}, ${b});`,
];
}
}
type OuterOutput<
A extends "vec2" | "vec3" | "vec4",
B extends "vec2" | "vec3" | "vec4",
> = A extends "vec2"
? B extends "vec2"
? "mat2"
: B extends "vec3"
? "mat3x2"
: B extends "vec4"
? "mat4x2"
: never
: A extends "vec3"
? B extends "vec2"
? "mat2x3"
: B extends "vec3"
? "mat3"
: B extends "vec4"
? "mat4x3"
: never
: A extends "vec4"
? B extends "vec2"
? "mat2x4"
: B extends "vec3"
? "mat3x4"
: B extends "vec4"
? "mat4"
: never
: never;
function outerOutputType<
A extends "vec2" | "vec3" | "vec4",
B extends "vec2" | "vec3" | "vec4",
>(aType: A, bType: B): OuterOutput<A, B> {
if (aType === "vec2") {
if (bType === "vec2") return "mat2" as OuterOutput<A, B>;
if (bType === "vec3") return "mat3x2" as OuterOutput<A, B>;
if (bType === "vec4") return "mat4x2" as OuterOutput<A, B>;
}
if (aType === "vec3") {
if (bType === "vec2") return "mat2x3" as OuterOutput<A, B>;
if (bType === "vec3") return "mat3" as OuterOutput<A, B>;
if (bType === "vec4") return "mat4x3" as OuterOutput<A, B>;
}
if (aType === "vec4") {
if (bType === "vec2") return "mat2x4" as OuterOutput<A, B>;
if (bType === "vec3") return "mat3x4" as OuterOutput<A, B>;
if (bType === "vec4") return "mat4" as OuterOutput<A, B>;
}
throw new Error(`Invalid outer type: ${aType}, ${bType}`);
}
export class Outer<
A extends "vec2" | "vec3" | "vec4",
B extends "vec2" | "vec3" | "vec4",
> extends BinaryOp<A, B, OuterOutput<A, B>, "outer"> {
constructor({ a, b }: { a: DynoVal<A>; b: DynoVal<B> }) {
super({ a, b, outKey: "outer", outTypeFunc: outerOutputType });
this.statements = ({ inputs, outputs }) => [
`${outputs.outer} = outerProduct(${inputs.a}, ${inputs.b});`,
];
}
}
type TransposeOutput<A extends MatFloatTypes> = A extends SquareMatTypes
? A
: A extends "mat2x3"
? "mat3x2"
: A extends "mat2x4"
? "mat4x2"
: A extends "mat3x2"
? "mat2x3"
: A extends "mat3x4"
? "mat4x3"
: A extends "mat4x2"
? "mat2x4"
: A extends "mat4x3"
? "mat3x4"
: never;
function transposeOutputType<A extends MatFloatTypes>(
type: A,
): TransposeOutput<A> {
if (type === "mat2") return "mat2" as TransposeOutput<A>;
if (type === "mat3") return "mat3" as TransposeOutput<A>;
if (type === "mat4") return "mat4" as TransposeOutput<A>;
if (type === "mat2x2") return "mat2x2" as TransposeOutput<A>;
if (type === "mat2x3") return "mat3x2" as TransposeOutput<A>;
if (type === "mat2x4") return "mat4x2" as TransposeOutput<A>;
if (type === "mat3x2") return "mat2x3" as TransposeOutput<A>;
if (type === "mat3x3") return "mat3x3" as TransposeOutput<A>;
if (type === "mat3x4") return "mat4x3" as TransposeOutput<A>;
if (type === "mat4x2") return "mat2x4" as TransposeOutput<A>;
if (type === "mat4x3") return "mat3x4" as TransposeOutput<A>;
if (type === "mat4x4") return "mat4x4" as TransposeOutput<A>;
throw new Error(`Invalid transpose type: ${type}`);
}
export class Transpose<A extends MatFloatTypes> extends UnaryOp<
A,
TransposeOutput<A>,
"transpose"
> {
constructor({ a }: { a: DynoVal<A> }) {
super({ a, outKey: "transpose", outTypeFunc: transposeOutputType });
this.statements = ({ inputs, outputs }) => [
`${outputs.transpose} = transpose(${inputs.a});`,
];
}
}
export class Determinant<A extends SquareMatTypes> extends UnaryOp<
A,
"float",
"det"
> {
constructor({ a }: { a: DynoVal<A> }) {
super({ a, outKey: "det", outTypeFunc: (aType) => "float" });
this.statements = ({ inputs, outputs }) => [
`${outputs.det} = determinant(${inputs.a});`,
];
}
}
export class Inverse<A extends SquareMatTypes> extends UnaryOp<
A,
A,
"inverse"
> {
constructor({ a }: { a: DynoVal<A> }) {
super({ a, outKey: "inverse", outTypeFunc: (aType) => aType });
this.statements = ({ inputs, outputs }) => [
`${outputs.inverse} = inverse(${a});`,
];
}
}
type SplitOutTypes<A extends VectorTypes> = A extends "vec2"
? { x: "float"; y: "float"; r: "float"; g: "float" }
: A extends "vec3"
? { x: "float"; y: "float"; z: "float"; r: "float"; g: "float"; b: "float" }
: A extends "vec4"
? {
x: "float";
y: "float";
z: "float";
w: "float";
r: "float";
g: "float";
b: "float";
a: "float";
}
: A extends "ivec2"
? { x: "int"; y: "int"; r: "int"; g: "int" }
: A extends "ivec3"
? { x: "int"; y: "int"; z: "int"; r: "int"; g: "int"; b: "int" }
: A extends "ivec4"
? {
x: "int";
y: "int";
z: "int";
w: "int";
r: "int";
g: "int";
b: "int";
a: "int";
}
: A extends "uvec2"
? { x: "uint"; y: "uint"; r: "uint"; g: "uint" }
: A extends "uvec3"
? {
x: "uint";
y: "uint";
z: "uint";
r: "uint";
g: "uint";
b: "uint";
}
: A extends "uvec4"
? {
x: "uint";
y: "uint";
z: "uint";
w: "uint";
r: "uint";
g: "uint";
b: "uint";
a: "uint";
}
: never;
function splitOutTypes<A extends VectorTypes>(type: A): SplitOutTypes<A> {
const result = (value: unknown) => value as SplitOutTypes<A>;
switch (type) {
case "vec2":
return result({ x: "float", y: "float", r: "float", g: "float" });
case "vec3":
return result({
x: "float",
y: "float",
z: "float",
r: "float",
g: "float",
b: "float",
});
case "vec4":
return result({
x: "float",
y: "float",
z: "float",
w: "float",
r: "float",
g: "float",
b: "float",
a: "float",
});
case "ivec2":
return result({ x: "int", y: "int", r: "int", g: "int" });
case "ivec3":
return result({
x: "int",
y: "int",
z: "int",
r: "int",
g: "int",
b: "int",
});
case "ivec4":
return result({
x: "int",
y: "int",
z: "int",
w: "int",
r: "int",
g: "int",
b: "int",
a: "int",
});
case "uvec2":
return result({ x: "uint", y: "uint", r: "uint", g: "uint" });
case "uvec3":
return result({
x: "uint",
y: "uint",
z: "uint",
r: "uint",
g: "uint",
b: "uint",
});
case "uvec4":
return result({
x: "uint",
y: "uint",
z: "uint",
w: "uint",
r: "uint",
g: "uint",
b: "uint",
a: "uint",
});
default:
throw new Error(`Invalid vector type: ${type}`);
}
}
export class Split<V extends VectorTypes> extends Dyno<
{ vector: V },
SplitOutTypes<V>
> {
constructor({ vector }: { vector: DynoVal<V> }) {
const type = valType(vector);
const inTypes = { vector: type };
const outTypes = splitOutTypes(inTypes.vector);
super({ inTypes, outTypes, inputs: { vector } });
this.statements = ({ inputs, outputs }) => {
const { x, y, z, w, r, g, b, a } = outputs as unknown as Record<
string,
string
>;
const { vector } = inputs;
return [
x ? `${x} = ${vector}.x;` : null,
y ? `${y} = ${vector}.y;` : null,
z ? `${z} = ${vector}.z;` : null,
w ? `${w} = ${vector}.w;` : null,
r ? `${r} = ${vector}.r;` : null,
g ? `${g} = ${vector}.g;` : null,
b ? `${b} = ${vector}.b;` : null,
a ? `${a} = ${vector}.a;` : null,
].filter(Boolean) as string[];
};
}
}
export class Combine<V extends VectorTypes, T extends VectorElementType<V>>
extends Dyno<SplitOutTypes<V> & { vector: V }, { vector: V }>
implements HasDynoOut<V>
{
constructor({
vector,
vectorType,
x,
y,
z,
w,
r,
g,
b,
a,
}: {
vector?: DynoVal<V>;
vectorType?: V;
x?: DynoVal<T>;
y?: DynoVal<T>;
z?: DynoVal<T>;
w?: DynoVal<T>;
r?: DynoVal<T>;
g?: DynoVal<T>;
b?: DynoVal<T>;
a?: DynoVal<T>;
}) {
if (!vector && !vectorType) {
throw new Error("Either vector or vectorType must be provided");
}
const vType = vectorType ?? valType(vector as DynoVal<V>);
const elType = vectorElementType(vType);
const dim = vectorDim(vType);
const inTypes = {
vector: vType,
x: elType,
y: elType,
r: elType,
g: elType,
} as unknown as SplitOutTypes<V> & { vector: V };
const inputs = { vector, x, y, r, g };
if (dim >= 3) {
Object.assign(inTypes, { z: elType, b: elType });
Object.assign(inputs, { z, b });
}
if (dim >= 4) {
Object.assign(inTypes, { w: elType, a: elType });
Object.assign(inputs, { w, a });
}
// @ts-ignore
super({ inTypes, outTypes: { vector: vType }, inputs });
this.statements = ({ inputs, outputs }) => {
const { vector } = outputs;
const {
vector: input,
x,
y,
z,
w,
r,
g,
b,
a,
} = inputs as Record<string, string>;
const statements = [
`${vector}.x = ${x ?? r ?? (input ? `${input}.x` : literalZero(elType))};`,
`${vector}.y = ${y ?? g ?? (input ? `${input}.y` : literalZero(elType))};`,
];
if (dim >= 3)
statements.push(
`${vector}.z = ${z ?? b ?? (input ? `${input}.z` : literalZero(elType))};`,
);
if (dim >= 4)
statements.push(
`${vector}.w = ${w ?? a ?? (input ? `${input}.w` : literalZero(elType))};`,
);
return statements;
};
}
dynoOut(): DynoValue<V> {
return new DynoOutput<V, SplitOutTypes<V> & { vector: V }, { vector: V }>(
this,
"vector",
);
}
}
type SwizzleOutput<
A extends VectorTypes,
Len extends number,
> = A extends FloatTypes
? Len extends 1
? "float"
: Len extends 2
? "vec2"
: Len extends 3
? "vec3"
: Len extends 4
? "vec4"
: never
: A extends IntTypes
? Len extends 1
? "int"
: Len extends 2
? "ivec2"
: Len extends 3
? "ivec3"
: Len extends 4
? "ivec4"
: never
: A extends UintTypes
? Len extends 1
? "uint"
: Len extends 2
? "uvec2"
: Len extends 3
? "uvec3"
: Len extends 4
? "uvec4"
: never
: never;
type SwizzleSelectLen<S extends SwizzleSelect> = S extends Swizzle1Select
? 1
: S extends Swizzle2Select
? 2
: S extends Swizzle3Select
? 3
: S extends Swizzle4Select
? 4
: never;
function swizzleOutputType<A extends VectorTypes, S extends SwizzleSelect>(
type: A,
swizzle: S,
): SwizzleOutput<A, SwizzleSelectLen<S>> {
let result = null;
if (isFloatType(type)) {
result =
swizzle.length === 1
? "float"
: swizzle.length === 2
? "vec2"
: swizzle.length === 3
? "vec3"
: swizzle.length === 4
? "vec4"
: null;
} else if (isIntType(type)) {
result =
swizzle.length === 1
? "int"
: swizzle.length === 2
? "ivec2"
: swizzle.length === 3
? "ivec3"
: swizzle.length === 4
? "ivec4"
: null;
} else if (isUintType(type)) {
result =
swizzle.length === 1
? "uint"
: swizzle.length === 2
? "uvec2"
: swizzle.length === 3
? "uvec3"
: swizzle.length === 4
? "uvec4"
: null;
}
if (result == null) {
throw new Error(`Invalid swizzle: ${swizzle}`);
}
return result as SwizzleOutput<A, SwizzleSelectLen<S>>;
}
type Swizzle1Select = `${"x" | "y" | "z" | "w"}|${"r" | "g" | "b" | "a"}`;
type Swizzle2Select =
| `${"x" | "y" | "z" | "w"}${"x" | "y" | "z" | "w"}`
| `${"r" | "g" | "b" | "a"}${"r" | "g" | "b" | "a"}`;
type Swizzle3Select =
| `${"x" | "y" | "z" | "w"}${"x" | "y" | "z" | "w"}${"x" | "y" | "z" | "w"}`
| `${"r" | "g" | "b" | "a"}${"r" | "g" | "b" | "a"}${"r" | "g" | "b" | "a"}`;
type Swizzle4Select =
| `${"x" | "y" | "z" | "w"}${"x" | "y" | "z" | "w"}${"x" | "y" | "z" | "w"}${"x" | "y" | "z" | "w"}`
| `${"r" | "g" | "b" | "a"}${"r" | "g" | "b" | "a"}${"r" | "g" | "b" | "a"}${"r" | "g" | "b" | "a"}`;
type SwizzleSelect =
| Swizzle1Select
| Swizzle2Select
| Swizzle3Select
| Swizzle4Select;
export class Swizzle<
A extends VectorTypes,
S extends SwizzleSelect,
> extends UnaryOp<A, SwizzleOutput<A, SwizzleSelectLen<S>>, "swizzle"> {
constructor({ vector, select }: { vector: DynoVal<A>; select: S }) {
super({
a: vector,
outKey: "swizzle",
outTypeFunc: (aType) => swizzleOutputType<A, S>(aType, select),
});
this.statements = ({ inputs, outputs }) => [
`${outputs.swizzle} = ${inputs.a}.${select};`,
];
}
}
+2
View File
@@ -0,0 +1,2 @@
export * from "./generators/static";
export * from "./generators/snow";
+276
View File
@@ -0,0 +1,276 @@
import * as THREE from "three";
import { SplatGenerator, SplatTransformer } from "../SplatGenerator";
import {
Gsplat,
add,
combine,
combineGsplat,
defineGsplat,
dynoBlock,
dynoConst,
dynoFloat,
dynoLiteral,
fract,
hashVec4,
max,
mix,
mod,
mul,
sin,
split,
sub,
vec3,
} from "../dyno";
import { dynoVec3 } from "../dyno";
// snowBox produces Gsplat trajectories that move in a deterministic fashion over time,
// with high similarity between adjacent frames. See examples/atmospheric/main.js
// for an example that creates a snowBox.
// A snowBox instance has a collection of properties that can be tuned to achieve
// different particle effects. The below DEFAULT_SNOW and DEFAULT_RAIN are example
// parameter sets that look a lot like snow and rain, and can be used as a starting
// point for further tweaking: `const mySnow = { ...DEFAULT_SNOW, density: 500 };`
export const DEFAULT_SNOW = {
box: new THREE.Box3(
new THREE.Vector3(-1, -1, -1),
new THREE.Vector3(1, 1, 1),
),
density: 100,
fallDirection: new THREE.Vector3(-1, -3, 1).normalize(),
fallVelocity: 0.02,
wanderScale: 0.04,
wanderVariance: 2,
color1: new THREE.Color(1, 1, 1),
color2: new THREE.Color(0.5, 0.5, 1),
minScale: 0.001,
maxScale: 0.005,
anisoScale: new THREE.Vector3(1, 1, 1),
};
export const DEFAULT_RAIN = {
box: new THREE.Box3(
new THREE.Vector3(-2, -1, -2),
new THREE.Vector3(2, 5, 2),
),
density: 10,
fallDirection: new THREE.Vector3(0, -1, 0),
fallVelocity: 2,
wanderScale: 0.1,
wanderVariance: 1,
color1: new THREE.Color(1, 1, 1),
color2: new THREE.Color(0.25, 0.25, 0.5),
minScale: 0.005,
maxScale: 0.01,
anisoScale: new THREE.Vector3(0.1, 1, 0.1),
};
// Calling snowBox creates a new snowBox instance and returns an object with
// the snowBox itself as well `as` a collection of controls that can be used to
// adjust the snowBox's properties over time:
//
// - snow: the SplatGenerator snowBox instance
// - min: the vec3 uniform of the snowBox minimum position
// - max: the vec3 uniform of the snowBox maximum position
// - minY: the float uniform of the snowBox minimum y-coordinate
// - color1: the vec3 uniform of the snowBox first color
// - color2: the vec3 uniform of the snowBox second color
// - opacity: the float uniform of the snowBox opacity
// - fallVelocity: the float uniform of the snowBox fall velocity
// - wanderVariance: the float uniform of the snowBox wander variance
// - wanderScale: the float uniform of the snowBox wander scale
// - fallDirection: the vec3 uniform of the snowBox fall direction
// - minScale: the float uniform of the snowBox minimum scale
// - maxScale: the float uniform of the snowBox maximum scale
// - anisoScale: the vec3 uniform of the snowBox anisotropic scale
export function snowBox({
// min and max box extents of the snowBox
box,
// minimum y-coordinate to clamp particle position, which can be used to
// fake hitting a ground plane and lingering there for a bit
minY,
// number of Gsplats to generate (default: calculated from box and density)
numSplats,
// density of Gsplats per unit volume (default: 100)
density,
// The xyz anisotropic scale of the Gsplat, which can be used for example
// to elongate rain particles (default: (1, 1, 1))
anisoScale,
// Minimum Gsplat particle scale (default: 0.001)
minScale,
// Maximum Gsplat particle scale (default: 0.005)
maxScale,
// The average direction of fall (default: (0, -1, 0))
fallDirection,
// The average speed of the fall (multiplied with fallDirection) (default: 0.02)
fallVelocity,
// The world scale of wandering overlay motion (default: 0.01)
wanderScale,
// Controls how uniformly the particles wander in sync, more variance mean
// more randomness in the motion (default: 2)
wanderVariance,
// Color 1 of the two colors interpolated between (default: (1, 1, 1))
color1,
// Color 2 of the two colors interpolated between (default: (0.5, 0.5, 1))
color2,
// The base opacity of the Gsplats (default: 1)
opacity,
// Optional callback function to call each frame.
onFrame,
}: {
box?: THREE.Box3;
minY?: number;
numSplats?: number;
density?: number;
anisoScale?: THREE.Vector3;
minScale?: number;
maxScale?: number;
fallDirection?: THREE.Vector3;
fallVelocity?: number;
wanderScale?: number;
wanderVariance?: number;
color1?: THREE.Color;
color2?: THREE.Color;
opacity?: number;
onFrame?: ({
object,
time,
deltaTime,
}: { object: SplatGenerator; time: number; deltaTime: number }) => void;
}) {
box =
box ??
new THREE.Box3(new THREE.Vector3(-1, -1, -1), new THREE.Vector3(1, 1, 1));
const volume =
(box.max.x - box.min.x) * (box.max.y - box.min.y) * (box.max.z - box.min.z);
density = density ?? 100;
numSplats =
numSplats ?? Math.max(1, Math.min(1000000, Math.round(volume * density)));
const dynoMinScale = dynoFloat(minScale ?? 0.001);
const dynoMaxScale = dynoFloat(maxScale ?? 0.005);
const dynoAnisoScale = dynoVec3(
(anisoScale?.clone() ?? new THREE.Vector3(1, 1, 1)).normalize(),
);
const dynoFallDirection = dynoVec3(
(fallDirection ?? new THREE.Vector3(0, -1, 0)).normalize(),
);
const dynoFallVelocity = dynoFloat(fallVelocity ?? 0.02);
const dynoWanderScale = dynoFloat(wanderScale ?? 0.01);
const dynoWanderVariance = dynoFloat(wanderVariance ?? 2);
const dynoColor1 = dynoVec3(color1 ?? new THREE.Color(1, 1, 1));
const dynoColor2 = dynoVec3(color2 ?? new THREE.Color(0.5, 0.5, 1));
const dynoOpacity = dynoFloat(opacity ?? 1);
const dynoTime = dynoFloat(0);
const globalOffset = dynoVec3(new THREE.Vector3(0, 0, 0));
const dynoMin = dynoVec3(box.min);
const dynoMax = dynoVec3(box.max);
const dynoMinY = dynoFloat(minY ?? Number.NEGATIVE_INFINITY);
const minMax = sub(dynoMax, dynoMin);
const snow = new SplatGenerator({
numSplats,
generator: dynoBlock(
{ index: "int" },
{ gsplat: Gsplat },
({ index }) => {
if (!index) {
throw new Error("index not defined");
}
const random = hashVec4(index);
const randomW = split(random).outputs.w;
let position = vec3(random);
let size = fract(mul(randomW, dynoConst("float", 100)));
size = sin(mul(dynoLiteral("float", "PI"), size));
size = add(dynoMinScale, mul(size, sub(dynoMaxScale, dynoMinScale)));
const scales = mul(size, dynoAnisoScale);
const intensity = fract(mul(randomW, dynoConst("float", 10)));
const hue = fract(randomW);
const color = mix(dynoColor1, dynoColor2, hue);
const rgb = mul(color, intensity);
const random2 = hashVec4(
combine({
vectorType: "ivec2",
x: index,
y: dynoConst("int", 0x1ab5),
}),
);
let perturb = vec3(random2);
let timeOffset = mul(split(random2).outputs.w, dynoWanderVariance);
timeOffset = add(dynoTime, timeOffset);
position = add(position, globalOffset);
const modulo = mod(
position,
dynoConst("vec3", new THREE.Vector3(1, 1, 1)),
);
position = add(dynoMin, mul(minMax, modulo));
const quaternion = dynoConst("vec4", new THREE.Quaternion(0, 0, 0, 1));
perturb = sin(add(vec3(timeOffset), perturb));
perturb = mul(perturb, dynoWanderScale);
let center = add(position, perturb);
let centerY = split(center).outputs.y;
centerY = max(dynoMinY, centerY);
center = combine({ vector: center, y: centerY });
let gsplat = combineGsplat({
flags: dynoLiteral("uint", "GSPLAT_FLAG_ACTIVE"),
index: index,
center,
scales,
quaternion,
rgb,
opacity: dynoOpacity,
});
gsplat = transformer.modify(gsplat);
return { gsplat };
},
{
globals: () => [defineGsplat],
},
),
update: ({ object, time, deltaTime }) => {
dynoTime.value = time;
const _updated = transformer.update(snow);
const fallDelta = dynoFallDirection.value
.clone()
.multiplyScalar(dynoFallVelocity.value * deltaTime);
globalOffset.value.add(fallDelta);
// Enable/disable splats based on opacity
object.visible = dynoOpacity.value > 0;
onFrame?.({ object, time, deltaTime });
snow.updateVersion();
},
});
const transformer: SplatTransformer = new SplatTransformer();
return {
snow,
min: dynoMin,
max: dynoMax,
minY: dynoMinY,
color1: dynoColor1,
color2: dynoColor2,
opacity: dynoOpacity,
fallVelocity: dynoFallVelocity,
wanderVariance: dynoWanderVariance,
wanderScale: dynoWanderScale,
fallDirection: dynoFallDirection,
minScale: dynoMinScale,
maxScale: dynoMaxScale,
anisoScale: dynoAnisoScale,
};
}
export type SNOW_RESULT_TYPE = ReturnType<typeof snowBox>;
+117
View File
@@ -0,0 +1,117 @@
import * as THREE from "three";
import { SplatGenerator, SplatTransformer } from "../SplatGenerator";
import {
type DynoVal,
Gsplat,
add,
combine,
combineGsplat,
defineGsplat,
div,
dynoBlock,
dynoConst,
dynoFloat,
dynoLiteral,
floatBitsToInt,
hashVec3,
imod,
mul,
split,
sub,
vec3,
} from "../dyno";
export function staticBox({
box,
cells,
dotScale,
color,
opacity,
}: {
box: THREE.Box3;
cells: THREE.Vector3;
dotScale: number;
color?: THREE.Color;
opacity?: number;
}) {
cells.x = Math.max(1, Math.round(cells.x));
cells.y = Math.max(1, Math.round(cells.y));
cells.z = Math.max(1, Math.round(cells.z));
opacity = opacity ?? 1;
const numSplats = cells.x * cells.y * cells.z;
const dynoX = dynoConst("int", cells.x);
const dynoY = dynoConst("int", cells.y);
const dynoZ = dynoConst("int", cells.z);
const dynoTime = dynoFloat(0);
const generator = new SplatGenerator({
numSplats,
generator: dynoBlock(
{ index: "int" },
{ gsplat: Gsplat },
({ index }) => {
if (!index) {
throw new Error("index is undefined");
}
const cellX = imod(index, dynoX);
const index2 = div(index, dynoX);
const cellY = imod(index2, dynoY);
const cellZ = div(index2, dynoY);
const cell = combine({
vectorType: "ivec3",
x: cellX,
y: cellY,
z: cellZ,
});
const intTime = floatBitsToInt(dynoTime);
const inputs = combine({ vectorType: "ivec2", x: index, y: intTime });
const random = hashVec3(inputs);
const min = dynoConst("vec3", box.min);
const max = dynoConst("vec3", box.max);
const size = sub(max, min);
const coord = div(add(vec3(cell), random), dynoConst("vec3", cells));
let r: DynoVal<"float">;
let g: DynoVal<"float">;
let b: DynoVal<"float">;
if (color) {
r = dynoConst("float", color.r);
g = dynoConst("float", color.g);
b = dynoConst("float", color.b);
} else {
({ r, g, b } = split(coord).outputs);
}
const rgba = combine({
vectorType: "vec4",
r,
g,
b,
a: dynoConst("float", opacity),
});
const center = add(min, mul(size, coord));
const scales = vec3(dynoConst("float", dotScale));
const quaternion = dynoConst("vec4", new THREE.Quaternion(0, 0, 0, 1));
let gsplat = combineGsplat({
flags: dynoLiteral("uint", "GSPLAT_FLAG_ACTIVE"),
index: index,
center,
scales,
quaternion,
rgba,
});
gsplat = transformer.modify(gsplat);
return { gsplat };
},
{
globals: () => [defineGsplat],
},
),
update: ({ time }) => {
dynoTime.value = time;
const _updated = transformer.update(generator);
generator.updateVersion();
},
});
const transformer: SplatTransformer = new SplatTransformer();
return generator;
}
+475
View File
@@ -0,0 +1,475 @@
import {
Color,
Matrix4,
type Object3D,
Quaternion,
Vector3,
type WebXRManager,
} from "three";
import { SplatMesh } from "./SplatMesh";
// Experimental WebXR hand tracking and movement
const DEFAULT_MOVE_INERTIA = 0.5;
const DEFAULT_ROTATE_INERTIA = 0.5;
const TOUCH_BIAS = 0.0;
export enum JointEnum {
w = "wrist",
t0 = "thumb-metacarpal",
t1 = "thumb-phalanx-proximal",
t2 = "thumb-phalanx-distal",
t3 = "thumb-tip",
i0 = "index-finger-metacarpal",
i1 = "index-finger-phalanx-proximal",
i2 = "index-finger-phalanx-intermediate",
i3 = "index-finger-phalanx-distal",
i4 = "index-finger-tip",
m0 = "middle-finger-metacarpal",
m1 = "middle-finger-phalanx-proximal",
m2 = "middle-finger-phalanx-intermediate",
m3 = "middle-finger-phalanx-distal",
m4 = "middle-finger-tip",
r0 = "ring-finger-metacarpal",
r1 = "ring-finger-phalanx-proximal",
r2 = "ring-finger-phalanx-intermediate",
r3 = "ring-finger-phalanx-distal",
r4 = "ring-finger-tip",
p0 = "pinky-finger-metacarpal",
p1 = "pinky-finger-phalanx-proximal",
p2 = "pinky-finger-phalanx-intermediate",
p3 = "pinky-finger-phalanx-distal",
p4 = "pinky-finger-tip",
}
export type JointId = keyof typeof JointEnum;
export const JOINT_IDS = Object.keys(JointEnum) as JointId[];
export const NUM_JOINTS = JOINT_IDS.length;
export const JOINT_INDEX: { [key in JointId]: number } = {
w: 0,
t0: 1,
t1: 2,
t2: 3,
t3: 4,
i0: 5,
i1: 6,
i2: 7,
i3: 8,
i4: 9,
m0: 10,
m1: 11,
m2: 12,
m3: 13,
m4: 14,
r0: 15,
r1: 16,
r2: 17,
r3: 18,
r4: 19,
p0: 20,
p1: 21,
p2: 22,
p3: 23,
p4: 24,
};
export const JOINT_RADIUS: { [key in JointId]: number } = {
w: 0.02,
t0: 0.02,
t1: 0.014,
t2: 0.0115,
t3: 0.0085,
i0: 0.022,
i1: 0.012,
i2: 0.0085,
i3: 0.0075,
i4: 0.0065,
m0: 0.021,
m1: 0.012,
m2: 0.008,
m3: 0.0075,
m4: 0.0065,
r0: 0.019,
r1: 0.011,
r2: 0.0075,
r3: 0.007,
r4: 0.006,
p0: 0.012,
p1: 0.01,
p2: 0.007,
p3: 0.0065,
p4: 0.0055,
};
export const JOINT_SEGMENTS: JointId[][] = [
["w", "t0", "t1", "t2", "t3"],
["w", "i0", "i1", "i2", "i3", "i4"],
["w", "m0", "m1", "m2", "m3", "m4"],
["w", "r0", "r1", "r2", "r3", "r4"],
["w", "p0", "p1", "p2", "p3", "p4"],
];
export const JOINT_SEGMENT_STEPS: number[][] = [
[8, 10, 8, 6],
[8, 19, 14, 8, 6],
[8, 19, 14, 8, 6],
[8, 19, 14, 8, 6],
[8, 19, 14, 8, 6],
];
export const JOINT_TIPS: JointId[] = ["t3", "i4", "m4", "r4", "p4"];
export const FINGER_TIPS: JointId[] = ["i4", "m4", "r4", "p4"];
export enum Hand {
left = "left",
right = "right",
}
export const HANDS = Object.keys(Hand) as Hand[];
export type Joint = {
position: Vector3;
quaternion: Quaternion;
radius: number;
};
export type HandJoints = { [key in JointId]?: Joint };
export type HandsJoints = { [key in Hand]?: HandJoints };
export class XrHands {
hands: HandsJoints = {};
last: HandsJoints = {};
values: Record<string, number> = {};
tests: Record<string, boolean> = {};
lastTests: Record<string, boolean> = {};
updated = false;
update({ xr, xrFrame }: { xr: WebXRManager; xrFrame: XRFrame }) {
const xrSession = xr.getSession();
if (!xrSession) {
return;
}
const referenceSpace = xr.getReferenceSpace();
if (!referenceSpace) {
return;
}
if (!xrFrame.getJointPose) {
return;
}
this.last = this.hands;
this.lastTests = this.tests;
this.hands = {};
this.values = {};
this.tests = {};
for (const inputSource of xrSession.inputSources) {
if (!inputSource.hand) {
continue;
}
const hand = inputSource.handedness as Hand;
this.hands[hand] = {};
// Iterate over JointId
for (const jointId of JOINT_IDS) {
const jointSpace = inputSource.hand.get(JointEnum[jointId]);
if (jointSpace) {
const jointPose = xrFrame.getJointPose(jointSpace, referenceSpace);
if (jointPose) {
const { position, orientation } = jointPose.transform;
this.hands[hand][jointId] = {
position: new Vector3(position.x, position.y, position.z),
quaternion: new Quaternion(
orientation.x,
orientation.y,
orientation.z,
orientation.w,
),
radius: jointPose.radius || 0.001,
};
}
}
}
}
for (const hand of HANDS) {
for (const { key, value } of [
{ key: `${hand}AllTips`, value: this.allTipsTouching(hand) },
{
key: `${hand}IndexThumb`,
value: this.touching(hand, "i4", hand, "t3"),
},
{
key: `${hand}MiddleThumb`,
value: this.touching(hand, "m4", hand, "t3"),
},
{
key: `${hand}RingThumb`,
value: this.touching(hand, "r4", hand, "t3"),
},
{
key: `${hand}PinkyThumb`,
value: this.touching(hand, "p4", hand, "t3"),
},
{ key: `${hand}TriTips`, value: this.triTipsTouching(hand) },
]) {
this.values[key] = value;
this.tests[key] =
value === 1.0
? true
: value === 0.0
? false
: (this.lastTests[key] ?? false);
}
}
}
makeGhostMesh(): SplatMesh {
const center = new Vector3();
const scales = new Vector3(0.01, 0.01, 0.01);
const quaternion = new Quaternion(0, 0, 0, 1);
const color = new Color(1, 1, 1);
const CYCLE = Math.PI * 3;
const WHITE = new Color(1, 1, 1);
let opacity = 1.0;
const mesh = new SplatMesh({
onFrame: () => {
let splatIndex = 0;
for (const handedness of HANDS) {
const active = this.values[`${handedness}MiddleThumb`];
const xrHand = this.hands[handedness];
for (const [index, segment] of JOINT_SEGMENTS.entries()) {
for (let i = 1; i < segment.length; ++i) {
const segmentSplats = JOINT_SEGMENT_STEPS[index][i - 1] * 2;
const lastSegment = i + 1 === segment.length;
const jointA = xrHand?.[segment[i - 1]];
const jointB = xrHand?.[segment[i]];
for (let j = 0; j < segmentSplats; ++j) {
const t = (j + 0.5) / segmentSplats;
opacity = 0.0;
if (jointA && jointB) {
center.copy(jointA.position).lerp(jointB.position, t);
quaternion
.copy(jointA.quaternion)
.slerp(jointB.quaternion, t);
const radiusA = JOINT_RADIUS[segment[i - 1]];
const radiusB = JOINT_RADIUS[segment[i]];
let radius = (1 - t) * radiusA + t * radiusB;
if (lastSegment && t > 0.8) {
// Round out finger tips
radius *= Math.sqrt(1 - ((t - 0.8) / 0.2) ** 2);
}
scales.set(0.65 * radius, 0.5 * radius, 0.003);
color.set(
0.55 + 0.45 * Math.sin(center.x * CYCLE),
0.55 + 0.45 * Math.sin(center.y * CYCLE),
0.55 + 0.45 * Math.sin(center.z * CYCLE),
);
if (handedness === "right") {
color.set(1 - color.r, 1 - color.g, 1 - color.b);
}
color.lerp(WHITE, active);
opacity = 0.75;
}
mesh.packedSplats.setSplat(
splatIndex,
center,
scales,
quaternion,
opacity,
color,
);
splatIndex += 1;
}
}
}
}
mesh.packedSplats.numSplats = splatIndex;
mesh.packedSplats.needsUpdate = true;
mesh.numSplats = splatIndex;
},
});
return mesh;
}
distance(
handA: Hand,
jointA: JointId,
handB: Hand,
jointB: JointId,
last = false,
): number {
const hA = last ? this.last[handA] : this.hands[handA];
const hB = last ? this.last[handB] : this.hands[handB];
const jA = hA?.[jointA];
const jB = hB?.[jointB];
if (!jA || !jB) {
return Number.POSITIVE_INFINITY;
}
return jA.position.distanceTo(jB.position);
}
separation(
handA: Hand,
jointA: JointId,
handB: Hand,
jointB: JointId,
last = false,
): number {
const d = this.distance(handA, jointA, handB, jointB, last);
if (d === Number.POSITIVE_INFINITY) {
return Number.POSITIVE_INFINITY;
}
return d - JOINT_RADIUS[jointA] - JOINT_RADIUS[jointB];
}
touching(
handA: Hand,
jointA: JointId,
handB: Hand,
jointB: JointId,
last = false,
): number {
const d = this.separation(handA, jointA, handB, jointB, last);
if (d === Number.POSITIVE_INFINITY) {
return Number.POSITIVE_INFINITY;
}
return 1 - Math.max(0, Math.min(1, d / 0.01 - TOUCH_BIAS));
}
allTipsTouching(hand: Hand, last = false): number {
return Math.min(
this.touching(hand, "t3", hand, "i4", last),
this.touching(hand, "i4", hand, "m4", last),
this.touching(hand, "m4", hand, "r4", last),
this.touching(hand, "r4", hand, "p4", last),
// this.touching(hand, "p4", hand, "t3", last),
);
}
triTipsTouching(hand: Hand, last = false): number {
return Math.min(
this.touching(hand, "t3", hand, "i4", last),
this.touching(hand, "i4", hand, "m4", last),
this.touching(hand, "m4", hand, "t3", last),
);
}
}
export class HandMovement {
xrHands: XrHands;
control: Object3D;
moveInertia: number;
rotateInertia: number;
lastGrip: { [key in Hand]?: Vector3 } = {};
lastPivot: Vector3 = new Vector3();
rotateVelocity = 0;
velocity: Vector3 = new Vector3();
constructor({
xrHands,
control,
moveInertia,
rotateInertia,
}: {
xrHands: XrHands;
control: Object3D;
moveInertia?: number;
rotateInertia?: number;
}) {
this.xrHands = xrHands;
this.control = control;
this.moveInertia = moveInertia ?? DEFAULT_MOVE_INERTIA;
this.rotateInertia = rotateInertia ?? DEFAULT_ROTATE_INERTIA;
}
update(deltaTime: number) {
const grip: { [key in Hand]?: Vector3 } = {};
for (const handedness of HANDS) {
const hand = this.xrHands.hands[handedness];
if (hand && this.xrHands.tests[`${handedness}MiddleThumb`]) {
grip[handedness] = new Vector3()
.add(hand.t3?.position ?? new Vector3())
.add(hand.i4?.position ?? new Vector3())
.add(hand.m4?.position ?? new Vector3())
.add(hand.r4?.position ?? new Vector3())
.add(hand.p4?.position ?? new Vector3())
.multiplyScalar(1 / 5);
}
}
if (grip.left && grip.right && this.lastGrip.left && this.lastGrip.right) {
const mid = grip.left.clone().add(grip.right).multiplyScalar(0.5);
const lastMid = this.lastGrip.left
.clone()
.add(this.lastGrip.right)
.multiplyScalar(0.5);
this.lastPivot = mid;
const delta = mid.clone().applyMatrix4(this.control.matrix);
delta.sub(lastMid.clone().applyMatrix4(this.control.matrix));
delta.multiplyScalar(1 / deltaTime);
this.velocity.lerp(delta, 1 - Math.exp(-20 * deltaTime));
const angle = Math.atan2(grip.left.z - mid.z, grip.left.x - mid.x);
const lastAngle = Math.atan2(
this.lastGrip.left.z - lastMid.z,
this.lastGrip.left.x - lastMid.x,
);
// Find closest rotation over circle between angle and lastAngle
let closestAngle = angle - lastAngle;
if (closestAngle > Math.PI) {
closestAngle -= Math.PI * 2;
} else if (closestAngle < -Math.PI) {
closestAngle += Math.PI * 2;
}
const rotateVelocity = closestAngle / deltaTime;
const blend = Math.exp(-20 * deltaTime);
this.rotateVelocity =
this.rotateVelocity * blend + rotateVelocity * (1 - blend);
} else {
this.rotateVelocity *= Math.exp(-deltaTime / this.rotateInertia);
if (grip.left && this.lastGrip.left) {
const delta = grip.left.clone().applyMatrix4(this.control.matrix);
delta.sub(this.lastGrip.left.clone().applyMatrix4(this.control.matrix));
delta.multiplyScalar(1 / deltaTime);
this.velocity.lerp(delta, 1 - Math.exp(-20 * deltaTime));
} else if (grip.right && this.lastGrip.right) {
const delta = grip.right.clone().applyMatrix4(this.control.matrix);
delta.sub(
this.lastGrip.right.clone().applyMatrix4(this.control.matrix),
);
delta.multiplyScalar(1 / deltaTime);
this.velocity.lerp(delta, 1 - Math.exp(-20 * deltaTime));
} else {
this.velocity.multiplyScalar(Math.exp(-deltaTime / this.moveInertia));
}
}
const negPivot = this.lastPivot.clone().negate();
const rotate = new Matrix4()
.makeTranslation(negPivot)
.premultiply(new Matrix4().makeRotationY(this.rotateVelocity * deltaTime))
.premultiply(new Matrix4().makeTranslation(this.lastPivot));
this.control.matrix.multiply(rotate);
this.control.matrix.decompose(
this.control.position,
this.control.quaternion,
this.control.scale,
);
this.control.updateMatrixWorld(true);
this.control.position.sub(this.velocity.clone().multiplyScalar(deltaTime));
this.lastGrip = grip;
}
}
+90
View File
@@ -0,0 +1,90 @@
export { ForgeRenderer, type ForgeRendererOptions } from "./ForgeRenderer";
export { ForgeViewpoint, type ForgeViewpointOptions } from "./ForgeViewpoint";
export * as dyno from "./dyno";
export {
SplatLoader,
unpackSplats,
SplatFileType,
getSplatFileType,
} from "./SplatLoader";
export { PlyReader } from "./ply";
export { SpzReader } from "./spz";
export { PackedSplats, type PackedSplatsOptions } from "./PackedSplats";
export {
SplatGenerator,
type GsplatGenerator,
SplatModifier,
type GsplatModifier,
SplatTransformer,
} from "./SplatGenerator";
export { SplatAccumulator, type GeneratorMapping } from "./SplatAccumulator";
export { Readback, type Rgba8Readback, type ReadbackBuffer } from "./Readback";
export {
SplatMesh,
type SplatMeshOptions,
type SplatMeshContext,
} from "./SplatMesh";
export { SplatSkinning, type SplatSkinningOptions } from "./SplatSkinning";
export {
SplatEdit,
type SplatEditOptions,
SplatEditSdf,
type SplatEditSdfOptions,
SplatEditSdfType,
SplatEditRgbaBlendMode,
SplatEdits,
} from "./SplatEdit";
export {
constructGrid,
constructAxes,
constructSpherePoints,
imageSplats,
textSplats,
} from "./splatConstructors";
export * as generators from "./generators";
export { VRButton } from "./vrButton";
export {
type JointId,
JointEnum,
JOINT_IDS,
NUM_JOINTS,
JOINT_INDEX,
JOINT_RADIUS,
JOINT_SEGMENTS,
JOINT_SEGMENT_STEPS,
JOINT_TIPS,
FINGER_TIPS,
Hand,
HANDS,
type Joint,
type HandJoints,
type HandsJoints,
XrHands,
HandMovement,
} from "./hands";
export { ForgeControls, FpsMovement, PointerControls } from "./controls";
export {
isMobile,
isAndroid,
isOculus,
flipPixels,
pixelsToPngUrl,
toHalf,
fromHalf,
floatToUint8,
floatToSint8,
Uint8ToFloat,
Sint8ToFloat,
setPackedSplat,
unpackSplat,
} from "./utils";
export * as utils from "./utils";
+930
View File
@@ -0,0 +1,930 @@
// PLY file format reader
export type PlyPropertyType =
| "char"
| "uchar"
| "short"
| "ushort"
| "int"
| "uint"
| "float"
| "double";
export type PlyElement = {
name: string;
count: number;
properties: Record<string, PlyProperty>;
};
export type PlyProperty = {
isList: boolean;
type: PlyPropertyType;
countType?: PlyPropertyType;
};
// Callback for parseSplats base Gsplat data
export type SplatCallback = (
index: number,
x: number,
y: number,
z: number,
scaleX: number,
scaleY: number,
scaleZ: number,
quatX: number,
quatY: number,
quatZ: number,
quatW: number,
opacity: number,
r: number,
g: number,
b: number,
) => void;
// Callback for parseSplats SH coefficients
export type SplatShCallback = (
index: number,
sh1: Float32Array,
sh2?: Float32Array,
sh3?: Float32Array,
) => void;
// A PlyReader is used to parse PLY files for Gsplat data.
// It takes a Uint8Array/ArrayBuffer as input fileBytes, parses the text header,
// and provides a method parseData to iterate over the entire binary data
// efficiently, or parseSplats to iterate over Gsplat data.
export class PlyReader {
fileBytes: Uint8Array;
header = "";
littleEndian = true;
elements: Record<string, PlyElement> = {};
comments: string[] = [];
data: DataView | null = null;
static defaultPointScale = 0.001;
numSplats = 0;
// Create a PlyReader from a Uint8Array/ArrayBuffer, no parsing done yet
constructor({ fileBytes }: { fileBytes: Uint8Array | ArrayBuffer }) {
this.fileBytes =
fileBytes instanceof ArrayBuffer ? new Uint8Array(fileBytes) : fileBytes;
}
// Identify and parse the PLY text header (assumed to be <64KB in size).
// this.elements will contain all the elements in the file, typically
// "vertex" contains the Gsplat data.
async parseHeader() {
const bufferStream = new ReadableStream({
start: (controller: ReadableStreamController<Uint8Array>) => {
// Assume the header is less than 64KB
controller.enqueue(this.fileBytes.slice(0, 65536));
controller.close();
},
});
const decoder = bufferStream
.pipeThrough(new TextDecoderStream())
.getReader();
// Find the end of the text section of the PLY file
this.header = "";
const headerTerminator = "end_header\n";
while (true) {
const { value, done } = await decoder.read();
if (done) {
throw new Error("Failed to read header");
}
this.header += value as string;
const endHeader = this.header.indexOf(headerTerminator);
if (endHeader >= 0) {
this.header = this.header.slice(0, endHeader + headerTerminator.length);
break;
}
}
// Partition the file into header and binary data
const headerLen = new TextEncoder().encode(this.header).length;
this.data = new DataView(this.fileBytes.buffer, headerLen);
this.elements = {};
let curElement: PlyElement | null = null;
this.comments = [];
this.header
.trim()
.split("\n")
.forEach((line: string, lineIndex: number) => {
const trimmedLine = line.trim();
if (lineIndex === 0) {
if (trimmedLine !== "ply") {
throw new Error("Invalid PLY header");
}
return;
}
if (trimmedLine.length === 0) {
return; // Skip empty lines
}
const fields = trimmedLine.split(" ");
switch (fields[0]) {
case "format":
if (fields[1] === "binary_little_endian") {
this.littleEndian = true;
} else if (fields[1] === "binary_big_endian") {
this.littleEndian = false;
} else {
// ascii formats not supported
throw new Error(`Unsupported PLY format: ${fields[1]}`);
}
if (fields[2] !== "1.0") {
throw new Error(`Unsupported PLY version: ${fields[2]}`);
}
break;
case "end_header":
break;
case "comment":
this.comments.push(trimmedLine.slice("comment ".length));
break;
case "element": {
const name = fields[1];
curElement = {
name,
count: Number.parseInt(fields[2]),
properties: {},
};
this.elements[name] = curElement;
break;
}
case "property":
if (curElement == null) {
throw new Error("Property must be inside an element");
}
if (fields[1] === "list") {
curElement.properties[fields[4]] = {
isList: true,
type: fields[3] as PlyPropertyType,
countType: fields[2] as PlyPropertyType,
};
} else {
curElement.properties[fields[2]] = {
isList: false,
type: fields[1] as PlyPropertyType,
};
}
break;
default:
// console.warn(`Skipping unsupported PLY keyword: ${fields[0]}`);
}
});
if (this.elements.vertex) {
this.numSplats = this.elements.vertex.count;
}
}
parseData(
elementCallback: (
element: PlyElement,
) =>
| null
| ((index: number, item: Record<string, number | number[]>) => void),
) {
// Go through the entire binary data of the PLY file, starting at offset 0
let offset = 0;
const data = this.data;
if (data == null) {
throw new Error("No data to parse");
}
for (const elementName in this.elements) {
const element = this.elements[elementName];
const { count, properties } = element;
const item: Record<string, number | number[]> = {};
// Construct an array of parser function to parse each property in an item
const parsers = [];
for (const [propertyName, property] of Object.entries(properties)) {
if (!property.isList) {
item[propertyName] = 0;
parsers.push(() => {
item[propertyName] = PARSE_FIELD[property.type](
data,
offset,
this.littleEndian,
);
offset += FIELD_BYTES[property.type];
});
} else {
// Property is a list, so parse the count first
item[propertyName] = [];
parsers.push(() => {
const list = item[propertyName] as number[];
list.length = PARSE_FIELD[property.countType as PlyPropertyType](
data,
offset,
this.littleEndian,
);
offset += FIELD_BYTES[property.countType as PlyPropertyType];
for (let i = 0; i < list.length; i++) {
list[i] = PARSE_FIELD[property.type](
data,
offset,
this.littleEndian,
);
offset += FIELD_BYTES[property.type];
}
});
}
}
// Parse all the items in the element
const callback = elementCallback(element) ?? (() => {});
for (let index = 0; index < count; index++) {
for (const parser of parsers) {
parser();
}
callback(index, item);
}
}
}
// Parse all the Gsplat data in the PLY file in go, invoking the given
// callbacks for each Gsplat.
parseSplats(splatCallback: SplatCallback, shCallback?: SplatShCallback) {
if (this.elements.vertex == null) {
throw new Error("No vertex element found");
}
let isSuperSplat = false;
const ssChunks: SSChunk[] = [];
let numSh = 0;
let sh1Props: string[] = [];
let sh2Props: string[] = [];
let sh3Props: string[] = [];
let sh1: Float32Array | undefined = undefined;
let sh2: Float32Array | undefined = undefined;
let sh3: Float32Array | undefined = undefined;
function prepareSh() {
// Prepare SH coefficient names and arrays for numSh total SH levels
const num_f_rest = NUM_SH_TO_NUM_F_REST[numSh];
sh1Props = new Array(3)
.fill(null)
.flatMap((_, k) =>
[0, 1, 2].map((_, d) => `f_rest_${k + (d * num_f_rest) / 3}`),
);
sh2Props = new Array(5)
.fill(null)
.flatMap((_, k) =>
[0, 1, 2].map((_, d) => `f_rest_${3 + k + (d * num_f_rest) / 3}`),
);
sh3Props = new Array(7)
.fill(null)
.flatMap((_, k) =>
[0, 1, 2].map((_, d) => `f_rest_${8 + k + (d * num_f_rest) / 3}`),
);
sh1 = numSh >= 1 ? new Float32Array(3 * 3) : undefined;
sh2 = numSh >= 2 ? new Float32Array(5 * 3) : undefined;
sh3 = numSh >= 3 ? new Float32Array(7 * 3) : undefined;
}
function ssShCallback(
index: number,
item: Record<string, number | number[]>,
) {
// Decode SH for SuperSplat compressed data
if (!sh1) {
throw new Error("Missing sh1");
}
for (const [i, key] of sh1Props.entries()) {
sh1[i] = ((item[key] as number) * 8) / 255 - 4;
}
if (sh2) {
for (const [i, key] of sh2Props.entries()) {
sh2[i] = ((item[key] as number) * 8) / 255 - 4;
}
}
if (sh3) {
for (const [i, key] of sh3Props.entries()) {
sh3[i] = ((item[key] as number) * 8) / 255 - 4;
}
}
shCallback?.(index, sh1, sh2, sh3);
}
function initSuperSplat(element: PlyElement) {
const {
min_x,
min_y,
min_z,
max_x,
max_y,
max_z,
min_scale_x,
min_scale_y,
min_scale_z,
max_scale_x,
max_scale_y,
max_scale_z,
min_r,
min_g,
min_b,
max_r,
max_g,
max_b,
} = element.properties;
if (
!min_x ||
!min_y ||
!min_z ||
!max_x ||
!max_y ||
!max_z ||
!min_scale_x ||
!min_scale_y ||
!min_scale_z ||
!max_scale_x ||
!max_scale_y ||
!max_scale_z ||
!min_r ||
!min_g ||
!min_b ||
!max_r ||
!max_g ||
!max_b
) {
throw new Error("Missing PLY chunk properties");
}
// SuperSplat chunks are used to quantize splat data, so we need to store them
isSuperSplat = true;
return (index: number, item: Record<string, number | number[]>) => {
const {
min_x,
min_y,
min_z,
max_x,
max_y,
max_z,
min_scale_x,
min_scale_y,
min_scale_z,
max_scale_x,
max_scale_y,
max_scale_z,
min_r,
min_g,
min_b,
max_r,
max_g,
max_b,
} = item as Record<string, number>;
ssChunks.push({
min_x,
min_y,
min_z,
max_x,
max_y,
max_z,
min_scale_x,
min_scale_y,
min_scale_z,
max_scale_x,
max_scale_y,
max_scale_z,
min_r,
min_g,
min_b,
max_r,
max_g,
max_b,
});
};
}
function decodeSuperSplat(element: PlyElement) {
// Decode SuperSplat compressed data in vertex and sh elements
if (shCallback && element.name === "sh") {
numSh = getNumSh(element.properties);
prepareSh();
return ssShCallback;
}
if (element.name !== "vertex") {
return null;
}
const { packed_position, packed_rotation, packed_scale, packed_color } =
element.properties;
if (
!packed_position ||
!packed_rotation ||
!packed_scale ||
!packed_color
) {
throw new Error(
"Missing PLY properties: packed_position, packed_rotation, packed_scale, packed_color",
);
}
const SQRT2 = Math.sqrt(2);
return (index: number, item: Record<string, number | number[]>) => {
// SuperSplat data are quantized within chunks with 256 Gsplats each
const chunk = ssChunks[index >>> 8];
if (chunk == null) {
throw new Error("Missing PLY chunk");
}
const {
min_x,
min_y,
min_z,
max_x,
max_y,
max_z,
min_scale_x,
min_scale_y,
min_scale_z,
max_scale_x,
max_scale_y,
max_scale_z,
min_r,
min_g,
min_b,
max_r,
max_g,
max_b,
} = chunk;
const { packed_position, packed_rotation, packed_scale, packed_color } =
item as Record<string, number>;
const x =
(((packed_position >>> 21) & 2047) / 2047) * (max_x - min_x) + min_x;
const y =
(((packed_position >>> 11) & 1023) / 1023) * (max_y - min_y) + min_y;
const z = ((packed_position & 2047) / 2047) * (max_z - min_z) + min_z;
const r0 = (((packed_rotation >>> 20) & 1023) / 1023 - 0.5) * SQRT2;
const r1 = (((packed_rotation >>> 10) & 1023) / 1023 - 0.5) * SQRT2;
const r2 = ((packed_rotation & 1023) / 1023 - 0.5) * SQRT2;
const rr = Math.sqrt(Math.max(0, 1.0 - r0 * r0 - r1 * r1 - r2 * r2));
const rOrder = packed_rotation >>> 30;
const quatX = rOrder === 0 ? r0 : rOrder === 1 ? rr : r1;
const quatY = rOrder <= 1 ? r1 : rOrder === 2 ? rr : r2;
const quatZ = rOrder <= 2 ? r2 : rr;
const quatW = rOrder === 0 ? rr : r0;
const scaleX = Math.exp(
(((packed_scale >>> 21) & 2047) / 2047) *
(max_scale_x - min_scale_x) +
min_scale_x,
);
const scaleY = Math.exp(
(((packed_scale >>> 11) & 1023) / 1023) *
(max_scale_y - min_scale_y) +
min_scale_y,
);
const scaleZ = Math.exp(
((packed_scale & 2047) / 2047) * (max_scale_z - min_scale_z) +
min_scale_z,
);
const r =
(((packed_color >>> 24) & 255) / 255) * (max_r - min_r) + min_r;
const g =
(((packed_color >>> 16) & 255) / 255) * (max_g - min_g) + min_g;
const b =
(((packed_color >>> 8) & 255) / 255) * (max_b - min_b) + min_b;
const opacity = (packed_color & 255) / 255;
splatCallback(
index,
x,
y,
z,
scaleX,
scaleY,
scaleZ,
quatX,
quatY,
quatZ,
quatW,
opacity,
r,
g,
b,
);
};
}
const elementCallback = (element: PlyElement) => {
if (element.name === "chunk") {
if (
this.comments.some((comment) =>
comment.toLowerCase().includes("supersplat"),
)
) {
// SuperSplat compressed format detected
return initSuperSplat(element);
}
}
if (isSuperSplat) {
return decodeSuperSplat(element);
}
if (element.name !== "vertex") {
return null;
}
const {
x,
y,
z,
scale_0,
scale_1,
scale_2,
rot_0,
rot_1,
rot_2,
rot_3,
opacity,
f_dc_0,
f_dc_1,
f_dc_2,
red,
green,
blue,
alpha,
} = element.properties;
if (!x || !y || !z) {
throw new Error("Missing PLY properties: x, y, z");
}
// Pure point cloud PLY files have no scales or rotations
const hasScales = scale_0 && scale_1 && scale_2;
const hasRots = rot_0 && rot_1 && rot_2 && rot_3;
// Quantization scale factor for argb values
const alphaDiv = alpha != null ? FIELD_SCALE[alpha.type] : 1;
const redDiv = red != null ? FIELD_SCALE[red.type] : 1;
const greenDiv = green != null ? FIELD_SCALE[green.type] : 1;
const blueDiv = blue != null ? FIELD_SCALE[blue.type] : 1;
numSh = getNumSh(element.properties);
prepareSh();
return (index: number, item: Record<string, number | number[]>) => {
const scaleX = hasScales
? Math.exp(item.scale_0 as number)
: PlyReader.defaultPointScale;
const scaleY = hasScales
? Math.exp(item.scale_1 as number)
: PlyReader.defaultPointScale;
const scaleZ = hasScales
? Math.exp(item.scale_2 as number)
: PlyReader.defaultPointScale;
const quatX = hasRots ? (item.rot_1 as number) : 0;
const quatY = hasRots ? (item.rot_2 as number) : 0;
const quatZ = hasRots ? (item.rot_3 as number) : 0;
const quatW = hasRots ? (item.rot_0 as number) : 1;
const op =
opacity != null
? 1.0 / (1.0 + Math.exp(-item.opacity as number))
: alpha != null
? (item.alpha as number) / alphaDiv
: 1.0;
const r =
f_dc_0 != null
? (item.f_dc_0 as number) * SH_C0 + 0.5
: red != null
? (item.red as number) / redDiv
: 1.0;
const g =
f_dc_1 != null
? (item.f_dc_1 as number) * SH_C0 + 0.5
: green != null
? (item.green as number) / greenDiv
: 1.0;
const b =
f_dc_2 != null
? (item.f_dc_2 as number) * SH_C0 + 0.5
: blue != null
? (item.blue as number) / blueDiv
: 1.0;
splatCallback(
index,
item.x as number,
item.y as number,
item.z as number,
scaleX,
scaleY,
scaleZ,
quatX,
quatY,
quatZ,
quatW,
op,
r,
g,
b,
);
if (shCallback && sh1) {
if (sh1) {
for (const [i, key] of sh1Props.entries()) {
sh1[i] = item[key] as number;
}
}
if (sh2) {
for (const [i, key] of sh2Props.entries()) {
sh2[i] = item[key] as number;
}
}
if (sh3) {
for (const [i, key] of sh3Props.entries()) {
sh3[i] = item[key] as number;
}
}
shCallback(index, sh1, sh2, sh3);
}
};
};
this.parseData(elementCallback);
}
// Inject RGBA values into original PLY file, which can be used to modify
// the color/opacity of the Gsplats and write out the modified PLY file.
injectRgba(rgba: Uint8Array) {
// Go through the entire binary data of the PLY file, starting at offset 0
let offset = 0;
const data = this.data;
if (data == null) {
throw new Error("No parsed data");
}
if (rgba.length !== this.numSplats * 4) {
throw new Error("Invalid RGBA array length");
}
for (const elementName in this.elements) {
const element = this.elements[elementName];
const { count, properties } = element;
const parsers = [];
let rgbaOffset = 0;
const isVertex = elementName === "vertex";
if (isVertex) {
for (const name of ["opacity", "f_dc_0", "f_dc_1", "f_dc_2"]) {
if (!properties[name] || properties[name].type !== "float") {
throw new Error(`Can't injectRgba due to property: ${name}`);
}
}
}
for (const [propertyName, property] of Object.entries(properties)) {
if (!property.isList) {
if (isVertex) {
if (
propertyName === "f_dc_0" ||
propertyName === "f_dc_1" ||
propertyName === "f_dc_2"
) {
const component = Number.parseInt(
propertyName.slice("f_dc_".length),
);
parsers.push(() => {
// Inject DC coefficients
const value =
(rgba[rgbaOffset + component] / 255 - 0.5) / SH_C0;
SET_FIELD[property.type](
data,
offset,
this.littleEndian,
value,
);
});
} else if (propertyName === "opacity") {
parsers.push(() => {
// Inject opacity sigmoid, clamped to [-100, 100]
const value = Math.max(
-100,
Math.min(
100,
-Math.log(1.0 / (rgba[rgbaOffset + 3] / 255) - 1.0),
),
);
SET_FIELD[property.type](
data,
offset,
this.littleEndian,
value,
);
});
}
}
parsers.push(() => {
offset += FIELD_BYTES[property.type];
});
} else {
parsers.push(() => {
const length = PARSE_FIELD[property.countType as PlyPropertyType](
data,
offset,
this.littleEndian,
);
offset += FIELD_BYTES[property.countType as PlyPropertyType];
offset += length * FIELD_BYTES[property.type];
});
}
}
for (let index = 0; index < count; index++) {
// Go through all the data and field parsers to compute offset
for (const parser of parsers) {
parser();
}
if (isVertex) {
rgbaOffset += 4;
}
}
}
}
}
export const SH_C0 = 0.28209479177387814;
type FieldParser = (
data: DataView,
offset: number,
littleEndian: boolean,
) => number;
type FieldSetter = (
data: DataView,
offset: number,
littleEndian: boolean,
value: number,
) => void;
const PARSE_FIELD: Record<PlyPropertyType, FieldParser> = {
char: (data: DataView, offset: number, littleEndian: boolean) => {
return data.getInt8(offset);
},
uchar: (data: DataView, offset: number, littleEndian: boolean) => {
return data.getUint8(offset);
},
short: (data: DataView, offset: number, littleEndian: boolean) => {
return data.getInt16(offset, littleEndian);
},
ushort: (data: DataView, offset: number, littleEndian: boolean) => {
return data.getUint16(offset, littleEndian);
},
int: (data: DataView, offset: number, littleEndian: boolean) => {
return data.getInt32(offset, littleEndian);
},
uint: (data: DataView, offset: number, littleEndian: boolean) => {
return data.getUint32(offset, littleEndian);
},
float: (data: DataView, offset: number, littleEndian: boolean) => {
return data.getFloat32(offset, littleEndian);
},
double: (data: DataView, offset: number, littleEndian: boolean) => {
return data.getFloat64(offset, littleEndian);
},
};
const SET_FIELD: Record<PlyPropertyType, FieldSetter> = {
char: (
data: DataView,
offset: number,
littleEndian: boolean,
value: number,
) => {
data.setInt8(offset, value);
},
uchar: (
data: DataView,
offset: number,
littleEndian: boolean,
value: number,
) => {
data.setUint8(offset, value);
},
short: (
data: DataView,
offset: number,
littleEndian: boolean,
value: number,
) => {
data.setInt16(offset, value, littleEndian);
},
ushort: (
data: DataView,
offset: number,
littleEndian: boolean,
value: number,
) => {
data.setUint16(offset, value, littleEndian);
},
int: (
data: DataView,
offset: number,
littleEndian: boolean,
value: number,
) => {
data.setInt32(offset, value, littleEndian);
},
uint: (
data: DataView,
offset: number,
littleEndian: boolean,
value: number,
) => {
data.setUint32(offset, value, littleEndian);
},
float: (
data: DataView,
offset: number,
littleEndian: boolean,
value: number,
) => {
data.setFloat32(offset, value, littleEndian);
},
double: (
data: DataView,
offset: number,
littleEndian: boolean,
value: number,
) => {
data.setFloat64(offset, value, littleEndian);
},
};
const FIELD_BYTES: Record<PlyPropertyType, number> = {
char: 1,
uchar: 1,
short: 2,
ushort: 2,
int: 4,
uint: 4,
float: 4,
double: 8,
};
const FIELD_SCALE: Record<PlyPropertyType, number> = {
char: 127,
uchar: 255,
short: 32767,
ushort: 65535,
int: 2147483647,
uint: 4294967295,
float: 1,
double: 1,
};
const NUM_F_REST_TO_NUM_SH: Record<number, number> = {
0: 0,
9: 1,
24: 2,
45: 3,
};
const NUM_SH_TO_NUM_F_REST: Record<number, number> = {
0: 0,
1: 9,
2: 24,
3: 45,
};
function getNumSh(properties: Record<string, PlyProperty>) {
let num_f_rest = 0;
while (properties[`f_rest_${num_f_rest}`]) {
num_f_rest += 1;
}
const numSh = NUM_F_REST_TO_NUM_SH[num_f_rest];
if (numSh == null) {
throw new Error(`Unsupported number of SH coefficients: ${num_f_rest}`);
}
return numSh;
}
type SSChunk = {
min_x: number;
min_y: number;
min_z: number;
max_x: number;
max_y: number;
max_z: number;
min_scale_x: number;
min_scale_y: number;
min_scale_z: number;
max_scale_x: number;
max_scale_y: number;
max_scale_z: number;
min_r: number;
min_g: number;
min_b: number;
max_r: number;
max_g: number;
max_b: number;
};
+19
View File
@@ -0,0 +1,19 @@
import * as THREE from "three";
import splatDefines from "./shaders/splatDefines.glsl";
import splatFragment from "./shaders/splatFragment.glsl";
import splatVertex from "./shaders/splatVertex.glsl";
let shaders: Record<string, string> | null = null;
export function getShaders(): Record<string, string> {
if (!shaders) {
// @ts-ignore
THREE.ShaderChunk.splatDefines = splatDefines;
shaders = {
splatVertex,
splatFragment,
};
}
return shaders;
}
+36
View File
@@ -0,0 +1,36 @@
precision highp float;
precision highp int;
precision highp sampler2D;
precision highp usampler2D;
precision highp isampler2D;
precision highp sampler2DArray;
precision highp usampler2DArray;
precision highp isampler2DArray;
precision highp sampler3D;
precision highp usampler3D;
precision highp isampler3D;
#include <splatDefines>
uniform uint targetLayer;
uniform int targetBase;
uniform int targetCount;
out uvec4 target;
{{ GLOBALS }}
void produceSplat(int index) {
{{ STATEMENTS }}
}
void main() {
int targetIndex = int(targetLayer << SPLAT_TEX_LAYER_BITS) + int(uint(gl_FragCoord.y) << SPLAT_TEX_WIDTH_BITS) + int(gl_FragCoord.x);
int index = targetIndex - targetBase;
if ((index >= 0) && (index < targetCount)) {
produceSplat(index);
} else {
target = uvec4(0u, 0u, 0u, 0u);
}
}
+36
View File
@@ -0,0 +1,36 @@
precision highp float;
precision highp int;
precision highp sampler2D;
precision highp usampler2D;
precision highp isampler2D;
precision highp sampler2DArray;
precision highp usampler2DArray;
precision highp isampler2DArray;
precision highp sampler3D;
precision highp usampler3D;
precision highp isampler3D;
#include <splatDefines>
uniform uint targetLayer;
uniform int targetBase;
uniform int targetCount;
out vec4 target;
{{ GLOBALS }}
void computeReadback(int index) {
{{ STATEMENTS }}
}
void main() {
int targetIndex = int(targetLayer << SPLAT_TEX_LAYER_BITS) + int(uint(gl_FragCoord.y) << SPLAT_TEX_WIDTH_BITS) + int(gl_FragCoord.x);
int index = targetIndex - targetBase;
if ((index >= 0) && (index < targetCount)) {
computeReadback(index);
} else {
target = vec4(0.0, 0.0, 0.0, 0.0);
}
}
+321
View File
@@ -0,0 +1,321 @@
const float LN_SCALE_MIN = -9.0;
const float LN_SCALE_MAX = 9.0;
const float LN_RESCALE = (LN_SCALE_MAX - LN_SCALE_MIN) / 254.0; // 1..=255
const uint SPLAT_TEX_WIDTH_BITS = 11u;
const uint SPLAT_TEX_HEIGHT_BITS = 11u;
const uint SPLAT_TEX_DEPTH_BITS = 11u;
const uint SPLAT_TEX_LAYER_BITS = SPLAT_TEX_WIDTH_BITS + SPLAT_TEX_HEIGHT_BITS;
const uint SPLAT_TEX_WIDTH = 1u << SPLAT_TEX_WIDTH_BITS;
const uint SPLAT_TEX_HEIGHT = 1u << SPLAT_TEX_HEIGHT_BITS;
const uint SPLAT_TEX_DEPTH = 1u << SPLAT_TEX_DEPTH_BITS;
const uint SPLAT_TEX_WIDTH_MASK = SPLAT_TEX_WIDTH - 1u;
const uint SPLAT_TEX_HEIGHT_MASK = SPLAT_TEX_HEIGHT - 1u;
const uint SPLAT_TEX_DEPTH_MASK = SPLAT_TEX_DEPTH - 1u;
const uint F16_INF = 0x7c00u;
const float PI = 3.1415926535897932384626433832795;
const float INFINITY = 1.0 / 0.0;
const float NEG_INFINITY = -INFINITY;
const float MAX_PIXEL_RADIUS = 512.0;
const float MIN_ALPHA = 0.01;
const float MAX_STDDEV = sqrt(8.0);
float sqr(float x) {
return x * x;
}
float pow4(float x) {
float x2 = x * x;
return x2 * x2;
}
float pow8(float x) {
float x4 = pow4(x);
return x4 * x4;
}
vec3 srgbToLinear(vec3 rgb) {
return pow(rgb, vec3(2.2));
}
vec3 linearToSrgb(vec3 rgb) {
return pow(rgb, vec3(1.0 / 2.2));
}
// uint encodeQuatXyz888(vec4 q) {
// // Encode quaternion in three int8s, flipping sign to remove ambiguity
// vec3 quat3 = (q.w < 0.0) ? -q.xyz : q.xyz;
// ivec3 iQuat3 = ivec3(round(clamp(quat3 * 127.0, -127.0, 127.0)));
// uvec3 uQuat3 = uvec3(iQuat3) & 0xffu;
// return (uQuat3.x << 16u) | (uQuat3.y << 8u) | uQuat3.z;
// }
// vec4 decodeQuatXyz888(uint encoded) {
// ivec3 iQuat3 = ivec3(
// int(encoded << 24u) >> 24,
// int(encoded << 16u) >> 24,
// int(encoded << 8u) >> 24
// );
// vec4 quat = vec4(vec3(iQuat3) / 127.0, 0.0);
// quat.w = sqrt(max(0.0, 1.0 - dot(quat.xyz, quat.xyz)));
// return quat;
// }
// Encode a quaternion (vec4) into a 24‐bit uint with folded octahedral mapping.
uint encodeQuatOctXy88R8(vec4 q) {
// Ensure minimal representation: flip if q.w is negative.
if (q.w < 0.0) {
q = -q;
}
// Compute rotation angle: θ = 2 * acos(q.w) ∈ [0,π]
float theta = 2.0 * acos(q.w);
float halfTheta = theta * 0.5;
float s = sin(halfTheta);
// Recover the rotation axis; use a default if nearly zero rotation.
vec3 axis = (abs(s) < 1e-6) ? vec3(1.0, 0.0, 0.0) : q.xyz / s;
// --- Folded Octahedral Mapping (inline) ---
// Compute p = (axis.x, axis.y) / (|axis.x|+|axis.y|+|axis.z|)
float sum = abs(axis.x) + abs(axis.y) + abs(axis.z);
vec2 p = vec2(axis.x, axis.y) / sum;
// If axis.z < 0, fold the mapping.
if (axis.z < 0.0) {
float oldPx = p.x;
p.x = (1.0 - abs(p.y)) * (p.x >= 0.0 ? 1.0 : -1.0);
p.y = (1.0 - abs(oldPx)) * (p.y >= 0.0 ? 1.0 : -1.0);
}
// Remap from [-1,1] to [0,1]
float u_f = p.x * 0.5 + 0.5;
float v_f = p.y * 0.5 + 0.5;
// Quantize to 8 bits (0 to 255)
uint quantU = uint(clamp(round(u_f * 255.0), 0.0, 255.0));
uint quantV = uint(clamp(round(v_f * 255.0), 0.0, 255.0));
// --- Angle Quantization ---
// Quantize θ ∈ [0,π] to 8 bits (0 to 255)
uint angleInt = uint(clamp(round((theta / 3.14159265359) * 255.0), 0.0, 255.0));
// Pack bits: bits [0–7]: quantU, [8–15]: quantV, [16–23]: angleInt.
return (angleInt << 16u) | (quantV << 8u) | quantU;
}
// Decode a 24‐bit encoded uint into a quaternion (vec4) using the folded octahedral inverse.
vec4 decodeQuatOctXy88R8(uint encoded) {
// Extract the fields.
uint quantU = encoded & uint(0xFFu); // bits 0–7
uint quantV = (encoded >> 8u) & uint(0xFFu); // bits 8–15
uint angleInt = encoded >> 16u; // bits 16–23
// Recover u and v in [0,1], then map to [-1,1].
float u_f = float(quantU) / 255.0;
float v_f = float(quantV) / 255.0;
vec2 f = vec2(u_f * 2.0 - 1.0, v_f * 2.0 - 1.0);
vec3 axis = vec3(f.xy, 1.0 - abs(f.x) - abs(f.y));
float t = max(-axis.z, 0.0);
axis.x += (axis.x >= 0.0) ? -t : t;
axis.y += (axis.y >= 0.0) ? -t : t;
axis = normalize(axis);
// Decode the angle θ ∈ [0,π].
float theta = (float(angleInt) / 255.0) * 3.14159265359;
float halfTheta = theta * 0.5;
float s = sin(halfTheta);
float w = cos(halfTheta);
return vec4(axis * s, w);
}
// // Encode a quaternion (vec4) into a 24‐bit uint by converting it to Euler angles.
// // We assume the quaternion is normalized.
// // Euler angles (roll, pitch, yaw) are assumed in radians in the range [-PI, PI].
// // Each angle is normalized: value = (angle + PI) / (2*PI) and quantized to 8 bits.
// uint encodeQuatEulerXyz888(vec4 q) {
// // Compute roll (x), pitch (y) and yaw (z) using Tait–Bryan angles.
// float sinr_cosp = 2.0 * (q.w * q.x + q.y * q.z);
// float cosr_cosp = 1.0 - 2.0 * (q.x * q.x + q.y * q.y);
// float roll = atan(sinr_cosp, cosr_cosp);
// float sinp = 2.0 * (q.w * q.y - q.z * q.x);
// float pitch = abs(sinp) >= 1.0 ? (sign(sinp) * 1.57079632679) : asin(sinp);
// float siny_cosp = 2.0 * (q.w * q.z + q.x * q.y);
// float cosy_cosp = 1.0 - 2.0 * (q.y * q.y + q.z * q.z);
// float yaw = atan(siny_cosp, cosy_cosp);
// // Normalize each angle from [-PI, PI] to [0, 1]
// float normRoll = (roll + 3.14159265359) / (2.0 * 3.14159265359);
// float normPitch = (pitch + 3.14159265359) / (2.0 * 3.14159265359);
// float normYaw = (yaw + 3.14159265359) / (2.0 * 3.14159265359);
// // Quantize each normalized angle to 8 bits (0..255)
// uint rollQ = uint(round(normRoll * 255.0));
// uint pitchQ = uint(round(normPitch * 255.0));
// uint yawQ = uint(round(normYaw * 255.0));
// // Pack into a 24-bit uint:
// // Bits 0..7 : rollQ,
// // Bits 8..15 : pitchQ,
// // Bits 16..23 : yawQ.
// return (yawQ << 16u) | (pitchQ << 8u) | rollQ;
// }
// // Decode a 24‐bit uint into a quaternion (vec4) by unpacking 8‐bit quantized Euler angles.
// // The Euler angles are assumed to be stored in the order: roll, pitch, yaw (each in [0,255]) corresponding to [-PI, PI].
// // Convert the Euler angles to a quaternion using the Tait–Bryan (roll, pitch, yaw) formula.
// vec4 decodeQuatEulerXyz888(uint encoded) {
// // Unpack each 8-bit field.
// uint rollQ = encoded & 0xFFu;
// uint pitchQ = (encoded >> 8u) & 0xFFu;
// uint yawQ = (encoded >> 16u) & 0xFFu;
// // Convert back to the [0,1] range.
// float normRoll = float(rollQ) / 255.0;
// float normPitch = float(pitchQ) / 255.0;
// float normYaw = float(yawQ) / 255.0;
// // Map from [0,1] back to [-PI, PI].
// float roll = normRoll * (2.0 * 3.14159265359) - 3.14159265359;
// float pitch = normPitch * (2.0 * 3.14159265359) - 3.14159265359;
// float yaw = normYaw * (2.0 * 3.14159265359) - 3.14159265359;
// // Convert Euler angles (roll, pitch, yaw) to quaternion.
// float cr = cos(roll * 0.5);
// float sr = sin(roll * 0.5);
// float cp = cos(pitch * 0.5);
// float sp = sin(pitch * 0.5);
// float cy = cos(yaw * 0.5);
// float sy = sin(yaw * 0.5);
// // Tait-Bryan (roll, pitch, yaw) to quaternion conversion.
// vec4 q;
// q.w = cr * cp * cy + sr * sp * sy;
// q.x = sr * cp * cy - cr * sp * sy;
// q.y = cr * sp * cy + sr * cp * sy;
// q.z = cr * cp * sy - sr * sp * cy;
// return q;
// }
// Pack a Gsplat into a uvec4
uvec4 packSplat(vec3 center, vec3 scales, vec4 quaternion, vec4 rgba) {
uvec4 uRgba = uvec4(round(clamp(rgba * 255.0, 0.0, 255.0)));
uint uQuat = encodeQuatOctXy88R8(quaternion);
// uint uQuat = encodeQuatXyz888(quaternion);
// uint uQuat = encodeQuatEulerXyz888(quaternion);
uvec3 uQuat3 = uvec3(uQuat & 0xffu, (uQuat >> 8u) & 0xffu, (uQuat >> 16u) & 0xffu);
// Encode scales in three uint8s, where 0=>0.0 and 1..=255 stores log scale
uvec3 uScales = uvec3(
(scales.x == 0.0) ? 0u : uint(round(clamp((log(scales.x) - LN_SCALE_MIN) / LN_RESCALE, 0.0, 254.0))) + 1u,
(scales.y == 0.0) ? 0u : uint(round(clamp((log(scales.y) - LN_SCALE_MIN) / LN_RESCALE, 0.0, 254.0))) + 1u,
(scales.z == 0.0) ? 0u : uint(round(clamp((log(scales.z) - LN_SCALE_MIN) / LN_RESCALE, 0.0, 254.0))) + 1u
);
// Pack it all into 4 x uint32
uint word0 = uRgba.r | (uRgba.g << 8u) | (uRgba.b << 16u) | (uRgba.a << 24u);
uint word1 = packHalf2x16(center.xy);
uint word2 = packHalf2x16(vec2(center.z, 0.0)) | (uQuat3.x << 16u) | (uQuat3.y << 24u);
uint word3 = uScales.x | (uScales.y << 8u) | (uScales.z << 16u) | (uQuat3.z << 24u);
return uvec4(word0, word1, word2, word3);
}
// Unpack a Gsplat from a uvec4
void unpackSplat(uvec4 packed, out vec3 center, out vec3 scales, out vec4 quaternion, out vec4 rgba) {
uint word0 = packed.x, word1 = packed.y, word2 = packed.z, word3 = packed.w;
uvec4 uRgba = uvec4(word0 & 0xffu, (word0 >> 8u) & 0xffu, (word0 >> 16u) & 0xffu, (word0 >> 24u) & 0xffu);
rgba = vec4(uRgba) / 255.0;
center = vec4(
unpackHalf2x16(word1),
unpackHalf2x16(word2 & 0xffffu)
).xyz;
uvec3 uScales = uvec3(word3 & 0xffu, (word3 >> 8u) & 0xffu, (word3 >> 16u) & 0xffu);
scales = vec3(
(uScales.x == 0u) ? 0.0 : exp(LN_SCALE_MIN + float(uScales.x - 1u) * LN_RESCALE),
(uScales.y == 0u) ? 0.0 : exp(LN_SCALE_MIN + float(uScales.y - 1u) * LN_RESCALE),
(uScales.z == 0u) ? 0.0 : exp(LN_SCALE_MIN + float(uScales.z - 1u) * LN_RESCALE)
);
uint uQuat = ((word2 >> 16u) & 0xFFFFu) | ((word3 >> 8u) & 0xFF0000u);
quaternion = decodeQuatOctXy88R8(uQuat);
// quaternion = decodeQuatXyz888(uQuat);
// quaternion = decodeQuatEulerXyz888(uQuat);
}
// Rotate vector v by quaternion q
vec3 quatVec(vec4 q, vec3 v) {
// Rotate vector v by quaternion q
vec3 t = 2.0 * cross(q.xyz, v);
return v + q.w * t + cross(q.xyz, t);
}
// Apply quaternion q1 after quaternion q2
vec4 quatQuat(vec4 q1, vec4 q2) {
return vec4(
q1.w * q2.x + q1.x * q2.w + q1.y * q2.z - q1.z * q2.y,
q1.w * q2.y - q1.x * q2.z + q1.y * q2.w + q1.z * q2.x,
q1.w * q2.z + q1.x * q2.y - q1.y * q2.x + q1.z * q2.w,
q1.w * q2.w - q1.x * q2.x - q1.y * q2.y - q1.z * q2.z
);
}
mat3 scaleQuaternionToMatrix(vec3 s, vec4 q) {
// Compute the matrix of scaling by s then rotating by q
return mat3(
s.x * (1.0 - 2.0 * (q.y * q.y + q.z * q.z)),
s.x * (2.0 * (q.x * q.y + q.w * q.z)),
s.x * (2.0 * (q.x * q.z - q.w * q.y)),
s.y * (2.0 * (q.x * q.y - q.w * q.z)),
s.y * (1.0 - 2.0 * (q.x * q.x + q.z * q.z)),
s.y * (2.0 * (q.y * q.z + q.w * q.x)),
s.z * (2.0 * (q.x * q.z + q.w * q.y)),
s.z * (2.0 * (q.y * q.z - q.w * q.x)),
s.z * (1.0 - 2.0 * (q.x * q.x + q.y * q.y))
);
}
// Spherical lerp between two quaternions
vec4 slerp(vec4 q1, vec4 q2, float t) {
// Compute the cosine of the angle between the two vectors
float cosHalfTheta = dot(q1, q2);
// If q1=q2 or q1=-q2 then theta = 0 and we can return q1
if (abs(cosHalfTheta) >= 0.999) {
return q1;
}
// If q1 and q2 are more than 180 degrees apart,
// we need to negate one to get the shortest path
if (cosHalfTheta < 0.0) {
q2 = -q2;
cosHalfTheta = -cosHalfTheta;
}
// Calculate temporary values
float halfTheta = acos(cosHalfTheta);
float sinHalfTheta = sqrt(1.0 - cosHalfTheta * cosHalfTheta);
// Calculate the interpolation factors
float ratioA = sin((1.0 - t) * halfTheta) / sinHalfTheta;
float ratioB = sin(t * halfTheta) / sinHalfTheta;
// Calculate the interpolated quaternion
return q1 * ratioA + q2 * ratioB;
}
ivec3 splatTexCoord(int index) {
uint x = uint(index) & SPLAT_TEX_WIDTH_MASK;
uint y = (uint(index) >> SPLAT_TEX_WIDTH_BITS) & SPLAT_TEX_HEIGHT_MASK;
uint z = uint(index) >> SPLAT_TEX_LAYER_BITS;
return ivec3(x, y, z);
}
+35
View File
@@ -0,0 +1,35 @@
precision highp float;
precision highp int;
#include <splatDefines>
uniform bool encodeLinear;
uniform float maxStdDev;
uniform bool disableFalloff;
uniform float falloff;
out vec4 fragColor;
in vec4 vRgba;
in vec2 vSplatUv;
in vec3 vNdc;
void main() {
float z = dot(vSplatUv, vSplatUv);
if (z > (maxStdDev * maxStdDev)) {
discard;
}
float alpha = vRgba.a;
alpha *= mix(1.0, exp(-0.5 * z), falloff);
if (alpha < MIN_ALPHA) {
discard;
}
vec3 rgb = vRgba.rgb;
if (encodeLinear) {
rgb = srgbToLinear(rgb);
}
fragColor = vec4(rgb, alpha);
}
+172
View File
@@ -0,0 +1,172 @@
precision highp float;
precision highp int;
precision highp usampler2DArray;
#include <splatDefines>
attribute uint splatIndex;
out vec4 vRgba;
out vec2 vSplatUv;
out vec3 vNdc;
uniform vec2 renderSize;
uniform uint numSplats;
uniform vec4 renderToViewQuat;
uniform vec3 renderToViewPos;
uniform float maxStdDev;
uniform float time;
uniform float deltaTime;
uniform bool debugFlag;
uniform bool enable2DGS;
uniform float blurAmount;
uniform float preBlurAmount;
uniform float clipXY;
uniform usampler2DArray packedSplats;
void main() {
// Default to outside the frustum so it's discarded if we return early
gl_Position = vec4(0.0, 0.0, 2.0, 1.0);
if (uint(gl_InstanceID) >= numSplats) {
return;
}
if (splatIndex == 0xffffffffu) {
// Special value reserved for "no splat"
return;
}
ivec3 texCoord = ivec3(
splatIndex & SPLAT_TEX_WIDTH_MASK,
(splatIndex >> SPLAT_TEX_WIDTH_BITS) & SPLAT_TEX_HEIGHT_MASK,
splatIndex >> SPLAT_TEX_LAYER_BITS
);
uvec4 packed = texelFetch(packedSplats, texCoord, 0);
vec3 center, scales;
vec4 quaternion, rgba;
unpackSplat(packed, center, scales, quaternion, rgba);
if (rgba.a < MIN_ALPHA) {
return;
}
bvec3 zeroScales = equal(scales, vec3(0.0));
if (all(zeroScales)) {
return;
}
// Compute the view space center of the splat
vec3 viewCenter = quatVec(renderToViewQuat, center) + renderToViewPos;
// Discard splats behind the camera
if (viewCenter.z >= 0.0) {
return;
}
// Compute the clip space center of the splat
vec4 clipCenter = projectionMatrix * vec4(viewCenter, 1.0);
// Discard splats outside near/far planes
if (abs(clipCenter.z) >= clipCenter.w) {
return;
}
// Discard splats more than clipXY times outside the XY frustum
float clip = clipXY * clipCenter.w;
if (abs(clipCenter.x) > clip || abs(clipCenter.y) > clip) {
return;
}
// Compute view space quaternion of splat
vec4 viewQuaternion = quatQuat(renderToViewQuat, quaternion);
if (enable2DGS && any(zeroScales)) {
vRgba = rgba;
vSplatUv = position.xy * maxStdDev;
vec3 offset;
if (zeroScales.z) {
offset = vec3(vSplatUv.xy * scales.xy, 0.0);
} else if (zeroScales.y) {
offset = vec3(vSplatUv.x * scales.x, 0.0, vSplatUv.y * scales.z);
} else {
offset = vec3(0.0, vSplatUv.xy * scales.yz);
}
vec3 viewPos = viewCenter + quatVec(viewQuaternion, offset);
gl_Position = projectionMatrix * vec4(viewPos, 1.0);
vNdc = gl_Position.xyz / gl_Position.w;
return;
}
// Compute NDC center of the splat
vec3 ndcCenter = clipCenter.xyz / clipCenter.w;
// Compute the 3D covariance matrix of the splat
mat3 RS = scaleQuaternionToMatrix(scales, viewQuaternion);
mat3 cov3D = RS * transpose(RS);
// Compute the Jacobian of the splat's projection at its center
vec2 focal = 0.5 * renderSize * vec2(projectionMatrix[0][0], projectionMatrix[1][1]);
float invZ = 1.0 / viewCenter.z;
vec2 J1 = focal * invZ;
vec2 J2 = -(J1 * viewCenter.xy) * invZ;
mat3 J = mat3(
J1.x, 0.0, J2.x,
0.0, J1.y, J2.y,
0.0, 0.0, 0.0
);
// Compute the 2D covariance by projecting the 3D covariance
// and picking out the XY plane components.
// Keeping below because we may need it in the future
// for skinning deformations.
// mat3 W = transpose(mat3(viewMatrix));
// mat3 T = W * J;
// mat3 cov2D = transpose(T) * cov3D * T;
mat3 cov2D = transpose(J) * cov3D * J;
float a = cov2D[0][0];
float d = cov2D[1][1];
float b = cov2D[0][1];
// Optionally pre-blur the splat to match non-antialias optimized splats
a += preBlurAmount;
d += preBlurAmount;
// Do convolution with a 0.5-pixel Gaussian for anti-aliasing: sqrt(0.3) ~= 0.5
float detOrig = a * d - b * b;
a += blurAmount;
d += blurAmount;
float det = a * d - b * b;
// Compute anti-aliasing intensity scaling factor
float blurAdjust = sqrt(max(0.0, detOrig / det));
rgba.a *= blurAdjust;
if (rgba.a < MIN_ALPHA) {
return;
}
// Compute the eigenvalue and eigenvectors of the 2D covariance matrix
float eigenAvg = 0.5 * (a + d);
float eigenDelta = sqrt(max(0.0, eigenAvg * eigenAvg - det));
float eigen1 = eigenAvg + eigenDelta;
float eigen2 = eigenAvg - eigenDelta;
vec2 eigenVec1 = normalize(vec2((abs(b) < 0.001) ? 1.0 : b, eigen1 - a));
vec2 eigenVec2 = vec2(eigenVec1.y, -eigenVec1.x);
float scale1 = position.x * min(MAX_PIXEL_RADIUS, maxStdDev * sqrt(eigen1));
float scale2 = position.y * min(MAX_PIXEL_RADIUS, maxStdDev * sqrt(eigen2));
// Compute the NDC coordinates for the ellipsoid's diagonal axes.
vec2 pixelOffset = eigenVec1 * scale1 + eigenVec2 * scale2;
vec2 ndcOffset = (2.0 / renderSize) * pixelOffset;
vec3 ndc = vec3(ndcCenter.xy + ndcOffset, ndcCenter.z);
vRgba = rgba;
vSplatUv = position.xy * maxStdDev;
vNdc = ndc;
gl_Position = vec4(ndc.xy * clipCenter.w, clipCenter.zw);
}
+413
View File
@@ -0,0 +1,413 @@
import * as THREE from "three";
import { PackedSplats } from "./PackedSplats";
import { SplatMesh } from "./SplatMesh";
export function constructGrid({
// PackedSplats object to add splats to
splats,
// min and max box extents of the grid
extents,
// step size along each grid axis
stepSize = 1,
// spherical radius of each Gsplat
pointRadius = 0.01,
// relative size of the "shadow copy" of each Gsplat placed behind it
pointShadowScale = 2.0,
// Gsplat opacity
opacity = 1.0,
// Gsplat color (THREE.Color) or function to set color for position:
// ((THREE.Color, THREE.Vector3) => void) (default: RGB-modulated grid)
color,
}: {
splats: PackedSplats;
extents: THREE.Box3;
stepSize?: number;
pointRadius?: number;
pointShadowScale?: number;
opacity?: number;
color?: THREE.Color | ((color: THREE.Color, point: THREE.Vector3) => void);
}) {
const EPSILON = 1.0e-6;
const center = new THREE.Vector3();
const scales = new THREE.Vector3();
const quaternion = new THREE.Quaternion(0, 0, 0, 1);
if (color == null) {
color = (color, point) =>
color.set(
0.55 + 0.45 * Math.cos(point.x * 1),
0.55 + 0.45 * Math.cos(point.y * 1),
0.55 + 0.45 * Math.cos(point.z * 1),
);
}
const pointColor = new THREE.Color();
for (let z = extents.min.z; z < extents.max.z + EPSILON; z += stepSize) {
for (let y = extents.min.y; y < extents.max.y + EPSILON; y += stepSize) {
for (let x = extents.min.x; x < extents.max.x + EPSILON; x += stepSize) {
center.set(x, y, z);
for (let layer = 0; layer < 2; ++layer) {
scales.setScalar(pointRadius * (layer ? 1 : pointShadowScale));
if (!layer) {
pointColor.setScalar(0.0);
} else if (typeof color === "function") {
color(pointColor, center);
} else {
pointColor.copy(color);
}
splats.pushSplat(center, scales, quaternion, opacity, pointColor);
}
}
}
}
}
export function constructAxes({
// PackedSplats object to add splats to
splats,
// scale (Gsplat scale along axis)
scale = 0.25,
// radius of the axes (Gsplat scale orthogonal to axis)
axisRadius = 0.0075,
// relative size of the "shadow copy" of each Gsplat placed behind it
axisShadowScale = 2.0,
// origins of the axes (default single axis at origin)
origins = [new THREE.Vector3()],
}: {
splats: PackedSplats;
scale?: number;
axisRadius?: number;
axisShadowScale?: number;
origins?: THREE.Vector3[];
}) {
const center = new THREE.Vector3();
const scales = new THREE.Vector3();
const quaternion = new THREE.Quaternion(0, 0, 0, 1);
const color = new THREE.Color();
const opacity = 1.0;
for (const origin of origins) {
for (let axis = 0; axis < 3; ++axis) {
center.set(
origin.x + (axis === 0 ? scale : 0),
origin.y + (axis === 1 ? scale : 0),
origin.z + (axis === 2 ? scale : 0),
);
for (let layer = 0; layer < 2; ++layer) {
scales.set(
(axis === 0 ? scale : axisRadius) * (layer ? 1 : axisShadowScale),
(axis === 1 ? scale : axisRadius) * (layer ? 1 : axisShadowScale),
(axis === 2 ? scale : axisRadius) * (layer ? 1 : axisShadowScale),
);
color.setRGB(
layer === 0 ? 0.0 : axis === 0 ? 1.0 : 0.0,
layer === 0 ? 0.0 : axis === 1 ? 1.0 : 0.0,
layer === 0 ? 0.0 : axis === 2 ? 1.0 : 0.0,
);
splats.pushSplat(center, scales, quaternion, opacity, color);
}
}
}
}
export function constructSpherePoints({
// PackedSplats object to add splats to
splats,
// center of the sphere (default: origin)
origin = new THREE.Vector3(),
// radius of the sphere
radius = 1.0,
// maximum depth of recursion for subdividing the sphere
// Warning: Gsplat count grows exponentially with depth
maxDepth = 3,
// filter function to apply to each point, for example to select
// points in a certain direction or other function ((THREE.Vector3) => boolean)
// (default: null)
filter = null,
// radius of each oriented Gsplat
pointRadius = 0.02,
// flatness of each oriented Gsplat
pointThickness = 0.001,
// color of each Gsplat (THREE.Color) or function to set color for point:
// ((THREE.Color, THREE.Vector3) => void) (default: white)
color = new THREE.Color(1, 1, 1),
}: {
splats: PackedSplats;
origin?: THREE.Vector3;
radius?: number;
maxDepth?: number;
filter?: ((point: THREE.Vector3) => boolean) | null;
pointRadius?: number;
pointThickness?: number;
color?: THREE.Color | ((color: THREE.Color, point: THREE.Vector3) => void);
}) {
const pointsHash: { [key: string]: THREE.Vector3 } = {};
function addPoint(p: THREE.Vector3) {
if (filter && !filter(p)) {
return;
}
const key = `${p.x},${p.y},${p.z}`;
if (!pointsHash[key]) {
pointsHash[key] = p;
}
}
function recurse(
depth: number,
p0: THREE.Vector3,
p1: THREE.Vector3,
p2: THREE.Vector3,
) {
addPoint(p0);
addPoint(p1);
addPoint(p2);
if (depth >= maxDepth) {
return;
}
const p01 = new THREE.Vector3().addVectors(p0, p1).normalize();
const p12 = new THREE.Vector3().addVectors(p1, p2).normalize();
const p20 = new THREE.Vector3().addVectors(p2, p0).normalize();
recurse(depth + 1, p0, p01, p20);
recurse(depth + 1, p01, p1, p12);
recurse(depth + 1, p20, p12, p2);
recurse(depth + 1, p01, p12, p20);
}
for (const x of [-1, 1]) {
for (const y of [-1, 1]) {
for (const z of [-1, 1]) {
const p0 = new THREE.Vector3(x, 0, 0);
const p1 = new THREE.Vector3(0, y, 0);
const p2 = new THREE.Vector3(0, 0, z);
recurse(0, p0, p1, p2);
}
}
}
const points = Object.values(pointsHash);
const scales = new THREE.Vector3(pointRadius, pointRadius, pointThickness);
const quaternion = new THREE.Quaternion();
const pointColor = typeof color === "function" ? new THREE.Color() : color;
for (const point of points) {
quaternion.setFromUnitVectors(new THREE.Vector3(0, 0, -1), point);
if (typeof color === "function") {
color(pointColor, point);
}
point.multiplyScalar(radius);
point.add(origin);
splats.pushSplat(point, scales, quaternion, 1.0, pointColor);
}
}
export function textSplats({
// text string to display
text,
// browser font to render text with (default: "Arial")
font,
// font size in pixels/Gsplats (default: 32)
fontSize,
// SplatMesh.recolor tint assuming white Gsplats (default: white)
color,
// Individual Gsplat color (default: white)
rgb,
// Gsplat radius (default: 0.8 covers 1-unit spacing well)
dotRadius,
// text alignment: "left", "center", "right", "start", "end" (default: "start")
textAlign,
// line spacing multiplier, lines delimited by "\n" (default: 1.0)
lineHeight,
}: {
text: string;
font?: string;
fontSize?: number;
color?: THREE.Color;
rgb?: THREE.Color;
dotRadius?: number;
textAlign?: "left" | "center" | "right" | "start" | "end";
lineHeight?: number;
}) {
font = font ?? "Arial";
fontSize = fontSize ?? 32;
color = color ?? new THREE.Color(1, 1, 1);
dotRadius = dotRadius ?? 0.8;
textAlign = textAlign ?? "start";
lineHeight = lineHeight ?? 1;
const lines = text.split("\n");
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
if (!ctx) {
throw new Error("Failed to create canvas context");
}
ctx.font = `${fontSize}px ${font}`;
ctx.textAlign = textAlign;
const metrics = ctx.measureText("");
const fontHeight =
metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent;
let minLeft = Number.POSITIVE_INFINITY;
let maxRight = Number.NEGATIVE_INFINITY;
let minTop = Number.POSITIVE_INFINITY;
let maxBottom = Number.NEGATIVE_INFINITY;
for (let line = 0; line < lines.length; ++line) {
const metrics = ctx.measureText(lines[line]);
const y = fontHeight * lineHeight * line;
minLeft = Math.min(minLeft, -metrics.actualBoundingBoxLeft);
maxRight = Math.max(maxRight, metrics.actualBoundingBoxRight);
minTop = Math.min(minTop, y - metrics.actualBoundingBoxAscent);
maxBottom = Math.max(maxBottom, y + metrics.actualBoundingBoxDescent);
}
const originLeft = Math.floor(minLeft);
const originTop = Math.floor(minTop);
const width = Math.ceil(maxRight) - originLeft;
const height = Math.ceil(maxBottom) - originTop;
canvas.width = width;
canvas.height = height;
ctx.font = `${fontSize}px ${font}`;
ctx.textAlign = textAlign;
ctx.textBaseline = "alphabetic";
ctx.fillStyle = "#FFFFFF";
for (let i = 0; i < lines.length; ++i) {
const y = fontHeight * lineHeight * i - originTop;
ctx.fillText(lines[i], -originLeft, y);
}
const imageData = ctx.getImageData(0, 0, width, height);
const rgba = new Uint8Array(imageData.data.buffer);
const splats = new PackedSplats();
const center = new THREE.Vector3();
const scales = new THREE.Vector3().setScalar(dotRadius);
const quaternion = new THREE.Quaternion(0, 0, 0, 1);
rgb = rgb ?? new THREE.Color(1, 1, 1);
let offset = 0;
for (let y = 0; y < height; ++y) {
for (let x = 0; x < width; ++x) {
const a = rgba[offset + 3];
if (a > 0) {
const opacity = a / 255;
center.set(x - 0.5 * (width - 1), 0.5 * (height - 1) - y, 0);
splats.pushSplat(center, scales, quaternion, opacity, rgb);
}
offset += 4;
}
}
const mesh = new SplatMesh({ packedSplats: splats });
mesh.recolor = color;
return mesh;
}
export function imageSplats({
// URL of the image to convert to splats (example: `url: "./image.png"`)
url,
// Radius of each Gsplat, default covers 1-unit spacing well (default: 0.8)
dotRadius,
// Subsampling factor for the image. Higher values reduce resolution,
// for example 2 will halve the width and height by averaging (default: 1)
subXY,
// Optional callback function to modify each Gsplat before it's added.
// Return null to skip adding the Gsplat, or a number to set the opacity
// and add the Gsplat with parameter values in the objects center, rgba etc. were
// passed into the forEachSplat callback. Ending the callback in `return opacity;`
// will retain the original opacity.
// ((width: number, height: number, index: number, center: THREE.Vector3, scales: THREE.Vector3, quaternion: THREE.Quaternion, opacity: number, color: THREE.Color) => number | null)
forEachSplat,
}: {
url: string;
dotRadius?: number;
subXY?: number;
forEachSplat?: (
width: number,
height: number,
index: number,
center: THREE.Vector3,
scales: THREE.Vector3,
quaternion: THREE.Quaternion,
opacity: number,
color: THREE.Color,
) => number | null;
}): SplatMesh {
dotRadius = dotRadius ?? 0.8;
subXY = Math.max(1, Math.floor(subXY ?? 1));
return new SplatMesh({
constructSplats: async (splats) => {
return new Promise((resolve, reject) => {
const img = new Image();
img.onerror = reject;
img.onload = () => {
const { width, height } = img;
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext("2d");
if (!ctx) {
reject(new Error("Failed to create canvas context"));
return;
}
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = "high";
const destWidth = Math.round(width / subXY);
const destHeight = Math.round(height / subXY);
ctx.drawImage(img, 0, 0, destWidth, destHeight);
try {
const imageData = ctx.getImageData(0, 0, destWidth, destHeight);
const rgba = new Uint8Array(imageData.data.buffer);
const center = new THREE.Vector3();
const scales = new THREE.Vector3().setScalar(dotRadius);
const quaternion = new THREE.Quaternion(0, 0, 0, 1);
const rgb = new THREE.Color();
let index = 0;
for (let y = 0; y < destHeight; ++y) {
for (let x = 0; x < destWidth; ++x) {
const offset = index * 4;
const a = rgba[offset + 3];
if (a > 0) {
let opacity = a / 255;
rgb.set(
rgba[offset + 0] / 255,
rgba[offset + 1] / 255,
rgba[offset + 2] / 255,
);
center.set(
x - 0.5 * (destWidth - 1),
0.5 * (destHeight - 1) - y,
0,
);
scales.setScalar(dotRadius);
quaternion.set(0, 0, 0, 1);
let push = true;
if (forEachSplat) {
const maybeOpacity = forEachSplat(
destWidth,
destHeight,
index,
center,
scales,
quaternion,
opacity,
rgb,
);
opacity = maybeOpacity ?? opacity;
push = maybeOpacity !== null;
}
if (push) {
splats.pushSplat(center, scales, quaternion, opacity, rgb);
}
}
index += 1;
}
}
resolve();
} catch (error) {
reject(error);
}
};
img.src = url;
});
},
});
}
+128
View File
@@ -0,0 +1,128 @@
import { getArrayBuffers } from "./utils.js";
import BundledWorker from "./worker?worker&inline";
// SplatWorker is an internal class that manages a WebWorker for executing
// longer running CPU tasks such as Gsplat file decoding and sorting.
// Although a SplatWorker can be created and used directly, the utility
// function withWorker() is recommended to allocate from a managed
// pool of SplatWorkers.
export class SplatWorker {
worker: Worker;
messages: Record<
number,
{ resolve: (value: unknown) => void; reject: (reason?: unknown) => void }
> = {};
messageIdNext = 0;
constructor() {
// this.worker = new Worker(new URL("./worker", import.meta.url), { type: "module" });
this.worker = new BundledWorker();
this.worker.onmessage = (event) => this.onMessage(event);
}
makeMessageId(): number {
return ++this.messageIdNext;
}
makeMessagePromiseId(): { id: number; promise: Promise<unknown> } {
const id = this.makeMessageId();
const promise = new Promise((resolve, reject) => {
this.messages[id] = { resolve, reject };
});
return { id, promise };
}
onMessage(event: MessageEvent) {
// console.log("SplatWorker.onMessage:", event);
const { id, result, error } = event.data;
// console.log(`SplatWorker.onMessage(${id}):`, result, error);
const handler = this.messages[id];
if (handler) {
delete this.messages[id];
if (error) {
handler.reject(error);
} else {
handler.resolve(result);
}
}
}
// Invoke an RPC on the worker with the given name and arguments.
// The normal usage of a worker is to run one activity at a time,
// but this function allows for concurrent calls, tagging each request
// with a unique message Id and awaiting a response to that same Id.
// The method will automatically transfer any ArrayBuffers in the
// arguments to the worker. If you'd like to transfer a copy of a
// buffer then you must clone it before passing to this function.
async call(name: string, args: unknown): Promise<unknown> {
const { id, promise } = this.makeMessagePromiseId();
// console.log(`SplatWorker.call(${name}):`, args);
this.worker.postMessage(
{ name, args, id },
{ transfer: getArrayBuffers(args) },
);
return promise;
}
}
let maxWorkers = 4;
let numWorkers = 0;
const freeWorkers: SplatWorker[] = [];
const workerQueue: ((worker: SplatWorker) => void)[] = [];
// Set the maximum number of workers to allocate for the pool. (default: 4)
export function setWorkerPool(count = 4) {
maxWorkers = count;
}
// Allocate a worker from the pool. If none are available and we are below the
// maximum, create a new one. Otherwise, add the request to a queue and wait
// for it to be fulfilled.
export async function allocWorker(): Promise<SplatWorker> {
const worker = freeWorkers.shift();
if (worker) {
return worker;
}
if (numWorkers < maxWorkers) {
const worker = new SplatWorker();
numWorkers += 1;
return worker;
}
return new Promise((resolve) => {
workerQueue.push(resolve);
});
}
// Return a worker to the pool. Pass the worker to any pending waiter.
export function freeWorker(worker: SplatWorker) {
if (numWorkers > maxWorkers) {
// Worker no longer needed
numWorkers -= 1;
return;
}
const waiter = workerQueue.shift();
if (waiter) {
waiter(worker);
return;
}
freeWorkers.push(worker);
}
// Allocate a worker from the pool and invoke the callback with the worker.
// When the callback completes, the worker will be returned to the pool.
export async function withWorker<T>(
callback: (worker: SplatWorker) => Promise<T>,
): Promise<T> {
const worker = await allocWorker();
try {
return await callback(worker);
} finally {
freeWorker(worker);
}
}
+185
View File
@@ -0,0 +1,185 @@
import { GunzipReader, fromHalf } from "./utils";
// SPZ file format reader
export class SpzReader {
fileBytes: Uint8Array;
reader: GunzipReader;
version: number;
numSplats: number;
shDegree: number;
fractionalBits: number;
flags: number;
flagAntiAlias: boolean;
reserved: number;
parsed: boolean;
constructor({ fileBytes }: { fileBytes: Uint8Array | ArrayBuffer }) {
this.fileBytes =
fileBytes instanceof ArrayBuffer ? new Uint8Array(fileBytes) : fileBytes;
this.reader = new GunzipReader({ fileBytes: this.fileBytes });
const header = new DataView(this.reader.read(16).buffer);
if (header.getUint32(0, true) !== 0x5053474e) {
throw new Error("Invalid SPZ file");
}
this.version = header.getUint32(4, true);
if (this.version < 1 || this.version > 2) {
throw new Error(`Unsupported SPZ version: ${this.version}`);
}
this.numSplats = header.getUint32(8, true);
this.shDegree = header.getUint8(12);
this.fractionalBits = header.getUint8(13);
this.flags = header.getUint8(14);
this.flagAntiAlias = (this.flags & 0x01) !== 0;
this.reserved = header.getUint8(15);
this.parsed = false;
}
parseSplats(
centerCallback: (index: number, x: number, y: number, z: number) => void,
alphaCallback: (index: number, alpha: number) => void,
rgbCallback: (index: number, r: number, g: number, b: number) => void,
scalesCallback: (
index: number,
scaleX: number,
scaleY: number,
scaleZ: number,
) => void,
quatCallback: (
index: number,
quatX: number,
quatY: number,
quatZ: number,
quatW: number,
) => void,
shCallback?: (
index: number,
sh1: Float32Array,
sh2?: Float32Array,
sh3?: Float32Array,
) => void,
) {
if (this.parsed) {
throw new Error("SPZ file already parsed");
}
this.parsed = true;
if (this.version === 1) {
// float16 centers
const centerBytes = this.reader.read(this.numSplats * 3 * 2);
const centerUint16 = new Uint16Array(centerBytes.buffer);
for (let i = 0; i < this.numSplats; i++) {
const i3 = i * 3;
const x = fromHalf(centerUint16[i3]);
const y = fromHalf(centerUint16[i3 + 1]);
const z = fromHalf(centerUint16[i3 + 2]);
centerCallback(i, x, y, z);
}
} else if (this.version === 2) {
// 24-bit fixed-point centers
const fixed = 1 << this.fractionalBits;
const centerBytes = this.reader.read(this.numSplats * 3 * 3);
for (let i = 0; i < this.numSplats; i++) {
const i9 = i * 9;
const x =
(((centerBytes[i9 + 2] << 24) |
(centerBytes[i9 + 1] << 16) |
(centerBytes[i9] << 8)) >>
8) /
fixed;
const y =
(((centerBytes[i9 + 5] << 24) |
(centerBytes[i9 + 4] << 16) |
(centerBytes[i9 + 3] << 8)) >>
8) /
fixed;
const z =
(((centerBytes[i9 + 8] << 24) |
(centerBytes[i9 + 7] << 16) |
(centerBytes[i9 + 6] << 8)) >>
8) /
fixed;
centerCallback(i, x, y, z);
}
} else {
throw new Error("Unreachable");
}
{
const bytes = this.reader.read(this.numSplats);
for (let i = 0; i < this.numSplats; i++) {
alphaCallback(i, bytes[i] / 255);
}
}
{
const rgbBytes = this.reader.read(this.numSplats * 3);
const scale = SH_C0 / 0.15;
for (let i = 0; i < this.numSplats; i++) {
const i3 = i * 3;
const r = (rgbBytes[i3] / 255 - 0.5) * scale + 0.5;
const g = (rgbBytes[i3 + 1] / 255 - 0.5) * scale + 0.5;
const b = (rgbBytes[i3 + 2] / 255 - 0.5) * scale + 0.5;
rgbCallback(i, r, g, b);
}
}
{
const scalesBytes = this.reader.read(this.numSplats * 3);
for (let i = 0; i < this.numSplats; i++) {
const i3 = i * 3;
const scaleX = Math.exp(scalesBytes[i3] / 16 - 10);
const scaleY = Math.exp(scalesBytes[i3 + 1] / 16 - 10);
const scaleZ = Math.exp(scalesBytes[i3 + 2] / 16 - 10);
scalesCallback(i, scaleX, scaleY, scaleZ);
}
}
{
const quatBytes = this.reader.read(this.numSplats * 3);
for (let i = 0; i < this.numSplats; i++) {
const i3 = i * 3;
const quatX = quatBytes[i3] / 127.5 - 1;
const quatY = quatBytes[i3 + 1] / 127.5 - 1;
const quatZ = quatBytes[i3 + 2] / 127.5 - 1;
const quatW = Math.sqrt(
Math.max(0, 1 - quatX * quatX - quatY * quatY - quatZ * quatZ),
);
quatCallback(i, quatX, quatY, quatZ, quatW);
}
}
if (shCallback && this.shDegree >= 1) {
const sh1 = new Float32Array(3 * 3);
const sh2 = this.shDegree >= 2 ? new Float32Array(5 * 3) : undefined;
const sh3 = this.shDegree >= 3 ? new Float32Array(7 * 3) : undefined;
const shBytes = this.reader.read(
this.numSplats * SH_DEGREE_TO_VECS[this.shDegree] * 3,
);
let offset = 0;
for (let i = 0; i < this.numSplats; i++) {
for (let j = 0; j < 9; ++j) {
sh1[j] = (shBytes[offset + j] - 128) / 128;
}
offset += 9;
if (sh2) {
for (let j = 0; j < 15; ++j) {
sh2[j] = (shBytes[offset + j] - 128) / 128;
}
offset += 15;
}
if (sh3) {
for (let j = 0; j < 21; ++j) {
sh3[j] = (shBytes[offset + j] - 128) / 128;
}
offset += 21;
}
shCallback(i, sh1, sh2, sh3);
}
}
}
}
const SH_DEGREE_TO_VECS: Record<number, number> = { 1: 3, 2: 8, 3: 15 };
const SH_C0 = 0.28209479177387814;
+1260
View File
File diff suppressed because it is too large Load Diff
+164
View File
@@ -0,0 +1,164 @@
import type * as THREE from "three";
export class VRButton {
static createButton(
renderer: THREE.WebGLRenderer,
sessionInit: XRSessionInit = {},
): HTMLElement | null {
const navigatorXr = navigator.xr;
if (!navigatorXr) {
// Only allow creation if WebXR is supported
return null;
}
const xr = navigatorXr;
const button = document.createElement("button");
renderer.xr.enabled = true;
renderer.xr.setReferenceSpaceType("local");
function showEnterVR(/*device*/) {
let currentSession: XRSession | null = null;
async function onSessionStarted(session: XRSession) {
console.log("onSessionStarted");
session.addEventListener("end", onSessionEnded);
await renderer.xr.setSession(session);
button.textContent = "EXIT VR";
currentSession = session;
}
function onSessionEnded(/*event*/) {
console.log("onSessionEnded");
currentSession?.removeEventListener("end", onSessionEnded);
button.textContent = "ENTER VR";
currentSession = null;
}
button.style.display = "";
button.style.cursor = "pointer";
button.style.left = "calc(50% - 100px)";
button.style.width = "200px";
button.style.height = "100px";
button.textContent = "ENTER VR";
// WebXR's requestReferenceSpace only works if the corresponding feature
// was requested at session creation time. For simplicity, just ask for
// the interesting ones as optional features, but be aware that the
// requestReferenceSpace call will fail if it turns out to be unavailable.
// ('local' is always available for immersive sessions and doesn't need to
// be requested separately.)
const sessionOptions: XRSessionInit = {
...sessionInit,
optionalFeatures: [
// "local-floor",
// "bounded-floor",
// "layers",
...(sessionInit.optionalFeatures || []),
],
};
button.onmouseenter = () => {
button.style.opacity = "1.0";
};
button.onmouseleave = () => {
button.style.opacity = "0.5";
};
button.onclick = () => {
if (currentSession === null) {
console.log("requesting session");
xr.requestSession("immersive-vr", sessionOptions).then(
onSessionStarted,
);
// xr.requestSession( "immersive-ar", sessionOptions ).then( onSessionStarted );
} else {
console.log("ending session");
currentSession.end();
}
};
}
function disableButton() {
button.style.display = "none";
button.style.cursor = "auto";
button.style.left = "calc(50% - 75px)";
button.style.width = "150px";
button.onmouseenter = null;
button.onmouseleave = null;
button.onclick = null;
}
function showWebXRNotFound() {
disableButton();
button.textContent = "VR NOT SUPPORTED";
}
function showVRNotAllowed(exception: any) {
disableButton();
console.warn(
"Exception when trying to call xr.isSessionSupported",
exception,
);
button.textContent = "VR NOT ALLOWED";
}
function stylizeElement(element: HTMLElement) {
element.style.position = "absolute";
element.style.bottom = "20px";
element.style.padding = "12px 6px";
element.style.border = "1px solid #fff";
element.style.borderRadius = "4px";
element.style.background = "rgba(0,0,0,0.1)";
element.style.color = "#fff";
element.style.font = "normal 13px sans-serif";
element.style.textAlign = "center";
element.style.opacity = "0.5";
element.style.outline = "none";
element.style.zIndex = "999";
}
button.id = "VRButton";
button.style.display = "none";
stylizeElement(button);
xr.isSessionSupported("immersive-vr")
.then((supported) => {
// xr.isSessionSupported( "immersive-ar" ).then( function ( supported ) {
supported ? showEnterVR() : showWebXRNotFound();
if (supported && VRButton.xrSessionIsGranted) {
button.click();
}
})
.catch(showVRNotAllowed);
return button;
}
static registerSessionGrantedListener() {
const navigatorXr = navigator.xr;
if (!navigatorXr) {
// Only allow creation if WebXR is supported
return null;
}
const xr = navigatorXr;
// WebXRViewer (based on Firefox) has a bug where addEventListener
// throws a silent exception and aborts execution entirely.
if (/WebXRViewer\//i.test(navigator.userAgent)) return;
xr.addEventListener("sessiongranted", () => {
VRButton.xrSessionIsGranted = true;
});
}
static xrSessionIsGranted = false;
}
VRButton.registerSessionGrantedListener();
+833
View File
@@ -0,0 +1,833 @@
import init_wasm, {
decode_wlg,
old_sort_splats,
sort_splats,
} from "forge-internal-rs";
import {
SCALE_MIN,
SPLAT_TEX_HEIGHT,
SPLAT_TEX_MIN_HEIGHT,
SPLAT_TEX_WIDTH,
WASM_SPLAT_SORT,
} from "./defines";
import { PlyReader } from "./ply";
import { SpzReader } from "./spz";
import {
encodeSh1Rgb,
encodeSh2Rgb,
encodeSh3Rgb,
fromHalf,
getArrayBuffers,
setPackedSplat,
setPackedSplatCenter,
setPackedSplatOpacity,
setPackedSplatQuat,
setPackedSplatRgb,
setPackedSplatScales,
} from "./utils";
// WebWorker for Forge's background CPU tasks, such as Gsplat file decoding
// and sorting.
async function onMessage(event: MessageEvent) {
// Unpack RPC function name, arguments, and ID from the main thread.
const { name, args, id }: { name: string; args: unknown; id: number } =
event.data;
// console.log(`worker.onMessage(${id}, ${name}):`, args);
// Initialize return result/error, to be filled out below.
let result = undefined;
let error = undefined;
try {
switch (name) {
case "unpackPly": {
const { packedArray, fileBytes } = args as {
packedArray: Uint32Array;
fileBytes: Uint8Array;
};
const decoded = await unpackPly({ packedArray, fileBytes });
result = {
id,
numSplats: decoded.numSplats,
packedArray: decoded.packedArray,
extra: decoded.extra,
};
break;
}
case "decodeWlg": {
const { fileBytes } = args as { fileBytes: Uint8Array };
const decoded = decode_wlg(
fileBytes,
SPLAT_TEX_WIDTH,
SPLAT_TEX_HEIGHT,
) as { numSplats: number; packedSplats: Uint32Array };
result = {
id,
numSplats: decoded.numSplats,
packedArray: decoded.packedSplats,
};
break;
}
case "decodeSpz": {
const { fileBytes } = args as { fileBytes: Uint8Array };
const decoded = unpackSpz(fileBytes);
result = {
id,
numSplats: decoded.numSplats,
packedArray: decoded.packedArray,
extra: decoded.extra,
};
break;
}
case "decodeAntiSplat": {
const { fileBytes } = args as { fileBytes: Uint8Array };
const decoded = unpackAntiSplat(fileBytes);
result = {
id,
numSplats: decoded.numSplats,
packedArray: decoded.packedArray,
};
break;
}
case "decodeKsplat": {
const { fileBytes } = args as { fileBytes: Uint8Array };
const decoded = unpackKsplat(fileBytes);
result = {
id,
numSplats: decoded.numSplats,
packedArray: decoded.packedArray,
extra: decoded.extra,
};
break;
}
case "sortSplats": {
// Sort maxSplats splats using readback data, which encodes one uint32 per
// Gsplats, with the low bytes encoding a float16 distance sort metric.
const { maxSplats, totalSplats, readback, ordering } = args as {
maxSplats: number;
totalSplats: number;
readback: Uint8Array[];
ordering: Uint32Array;
};
// Sort totalSplats splats each with 4 bytes of readback, and outputs Uint32Array ordering of splat indices
if (WASM_SPLAT_SORT) {
result = {
id,
readback,
ordering,
activeSplats: old_sort_splats(
maxSplats,
totalSplats,
readback,
ordering,
),
};
} else {
result = {
id,
readback,
...oldSortSplats({ totalSplats, readback, ordering }),
};
}
break;
}
case "sortDoubleSplats": {
// Sort numSplats splats using the readback distance metric, which encodes
// one float16 per splat (no unused high bytes like for sortSplats).
const { numSplats, readback, ordering } = args as {
numSplats: number;
readback: Uint16Array;
ordering: Uint32Array;
};
result = {
id,
readback,
ordering,
};
if (WASM_SPLAT_SORT) {
result = {
id,
readback,
ordering,
activeSplats: sort_splats(numSplats, readback, ordering),
};
} else {
result = {
id,
readback,
...sortSplats({ numSplats, readback, ordering }),
};
}
break;
}
default: {
throw new Error(`Unknown name: ${name}`);
}
}
} catch (e) {
error = e;
}
// Send the result or error back to the main thread, making sure to transfer any ArrayBuffers
self.postMessage(
{ id, result, error },
{ transfer: getArrayBuffers(result) },
);
}
async function unpackPly({
packedArray,
fileBytes,
}: { packedArray: Uint32Array; fileBytes: Uint8Array }): Promise<{
packedArray: Uint32Array;
numSplats: number;
extra: Record<string, unknown>;
}> {
const ply = new PlyReader({ fileBytes });
await ply.parseHeader();
const numSplats = ply.numSplats;
const extra: Record<string, unknown> = {};
// Anything below this is considered zero and can be rendered as 2DGS
const ZERO_CUTOFF = Math.exp(-20);
ply.parseSplats(
(
index,
x,
y,
z,
scaleX,
scaleY,
scaleZ,
quatX,
quatY,
quatZ,
quatW,
opacity,
r,
g,
b,
) => {
setPackedSplat(
packedArray,
index,
x,
y,
z,
scaleX < ZERO_CUTOFF ? 0 : Math.max(SCALE_MIN, scaleX),
scaleY < ZERO_CUTOFF ? 0 : Math.max(SCALE_MIN, scaleY),
scaleZ < ZERO_CUTOFF ? 0 : Math.max(SCALE_MIN, scaleZ),
quatX,
quatY,
quatZ,
quatW,
opacity,
r,
g,
b,
);
},
(index, sh1, sh2, sh3) => {
if (sh1) {
if (!extra.sh1) {
extra.sh1 = new Uint32Array(numSplats * 2);
}
encodeSh1Rgb(extra.sh1 as Uint32Array, index, sh1);
}
if (sh2) {
if (!extra.sh2) {
extra.sh2 = new Uint32Array(numSplats * 4);
}
encodeSh2Rgb(extra.sh2 as Uint32Array, index, sh2);
}
if (sh3) {
if (!extra.sh3) {
extra.sh3 = new Uint32Array(numSplats * 4);
}
encodeSh3Rgb(extra.sh3 as Uint32Array, index, sh3);
}
},
);
return { packedArray, numSplats, extra };
}
function computeMaxSplats(numSplats: number): number {
// Compute the size of a Gsplat array texture (2048x2048xD) that can fit
// numSplats splats, and return the total number of splats that can be stored
// in such a texture.
const width = SPLAT_TEX_WIDTH;
const height = Math.max(
SPLAT_TEX_MIN_HEIGHT,
Math.min(SPLAT_TEX_HEIGHT, Math.ceil(numSplats / width)),
);
const depth = Math.ceil(numSplats / (width * height));
return width * height * depth;
}
function unpackSpz(fileBytes: Uint8Array): {
packedArray: Uint32Array;
numSplats: number;
extra: Record<string, unknown>;
} {
const spz = new SpzReader({ fileBytes });
const numSplats = spz.numSplats;
const maxSplats = computeMaxSplats(numSplats);
const packedArray = new Uint32Array(maxSplats * 4);
const extra: Record<string, unknown> = {};
spz.parseSplats(
(index, x, y, z) => {
setPackedSplatCenter(packedArray, index, x, y, z);
},
(index, alpha) => {
setPackedSplatOpacity(packedArray, index, alpha);
},
(index, r, g, b) => {
setPackedSplatRgb(packedArray, index, r, g, b);
},
(index, scaleX, scaleY, scaleZ) => {
setPackedSplatScales(packedArray, index, scaleX, scaleY, scaleZ);
},
(index, quatX, quatY, quatZ, quatW) => {
setPackedSplatQuat(packedArray, index, quatX, quatY, quatZ, quatW);
},
(index, sh1, sh2, sh3) => {
if (sh1) {
if (!extra.sh1) {
extra.sh1 = new Uint32Array(numSplats * 2);
}
encodeSh1Rgb(extra.sh1 as Uint32Array, index, sh1);
}
if (sh2) {
if (!extra.sh2) {
extra.sh2 = new Uint32Array(numSplats * 4);
}
encodeSh2Rgb(extra.sh2 as Uint32Array, index, sh2);
}
if (sh3) {
if (!extra.sh3) {
extra.sh3 = new Uint32Array(numSplats * 4);
}
encodeSh3Rgb(extra.sh3 as Uint32Array, index, sh3);
}
},
);
return { packedArray, numSplats, extra };
}
function unpackAntiSplat(fileBytes: Uint8Array): {
packedArray: Uint32Array;
numSplats: number;
} {
const numSplats = Math.floor(fileBytes.length / 32); // 32 bytes per splat
if (numSplats * 32 !== fileBytes.length) {
throw new Error("Invalid .splat file size");
}
const maxSplats = computeMaxSplats(numSplats);
const packedArray = new Uint32Array(maxSplats * 4);
const f32 = new Float32Array(fileBytes.buffer);
for (let i = 0; i < numSplats; ++i) {
const i32 = i * 32;
const i8 = i * 8;
const x = f32[i8 + 0];
const y = f32[i8 + 1];
const z = f32[i8 + 2];
const scaleX = f32[i8 + 3];
const scaleY = f32[i8 + 4];
const scaleZ = f32[i8 + 5];
const r = fileBytes[i32 + 24] / 255;
const g = fileBytes[i32 + 25] / 255;
const b = fileBytes[i32 + 26] / 255;
const opacity = fileBytes[i32 + 27] / 255;
const quatW = (fileBytes[i32 + 28] - 128) / 128;
const quatX = (fileBytes[i32 + 29] - 128) / 128;
const quatY = (fileBytes[i32 + 30] - 128) / 128;
const quatZ = (fileBytes[i32 + 31] - 128) / 128;
setPackedSplat(
packedArray,
i,
x,
y,
z,
scaleX,
scaleY,
scaleZ,
quatX,
quatY,
quatZ,
quatW,
opacity,
r,
g,
b,
);
}
return { packedArray, numSplats };
}
type KsplatCompression = {
bytesPerCenter: number;
bytesPerScale: number;
bytesPerRotation: number;
bytesPerColor: number;
bytesPerSphericalHarmonicsComponent: number;
scaleOffsetBytes: number;
rotationOffsetBytes: number;
colorOffsetBytes: number;
sphericalHarmonicsOffsetBytes: number;
scaleRange: number;
};
const KSPLAT_COMPRESSION: Record<number, KsplatCompression> = {
0: {
bytesPerCenter: 12,
bytesPerScale: 12,
bytesPerRotation: 16,
bytesPerColor: 4,
bytesPerSphericalHarmonicsComponent: 4,
scaleOffsetBytes: 12,
rotationOffsetBytes: 24,
colorOffsetBytes: 40,
sphericalHarmonicsOffsetBytes: 44,
scaleRange: 1,
},
1: {
bytesPerCenter: 6,
bytesPerScale: 6,
bytesPerRotation: 8,
bytesPerColor: 4,
bytesPerSphericalHarmonicsComponent: 2,
scaleOffsetBytes: 6,
rotationOffsetBytes: 12,
colorOffsetBytes: 20,
sphericalHarmonicsOffsetBytes: 24,
scaleRange: 32767,
},
2: {
bytesPerCenter: 6,
bytesPerScale: 6,
bytesPerRotation: 8,
bytesPerColor: 4,
bytesPerSphericalHarmonicsComponent: 1,
scaleOffsetBytes: 6,
rotationOffsetBytes: 12,
colorOffsetBytes: 20,
sphericalHarmonicsOffsetBytes: 24,
scaleRange: 32767,
},
};
const KSPLAT_SH_DEGREE_TO_COMPONENTS: Record<number, number> = {
0: 0,
1: 9,
2: 24,
3: 45,
};
function unpackKsplat(fileBytes: Uint8Array): {
packedArray: Uint32Array;
numSplats: number;
extra: Record<string, unknown>;
} {
const HEADER_BYTES = 4096;
const SECTION_BYTES = 1024;
let headerOffset = 0;
const header = new DataView(fileBytes.buffer, headerOffset, HEADER_BYTES);
headerOffset += HEADER_BYTES;
const versionMajor = header.getUint8(0);
const versionMinor = header.getUint8(1);
if (versionMajor !== 0 || versionMinor < 1) {
throw new Error(
`Unsupported .ksplat version: ${versionMajor}.${versionMinor}`,
);
}
const maxSectionCount = header.getUint32(4, true);
// const sectionCount = header.getUint32(8, true);
// const maxSplatCount = header.getUint32(12, true);
const splatCount = header.getUint32(16, true);
const compressionLevel = header.getUint16(20, true);
if (compressionLevel < 0 || compressionLevel > 2) {
throw new Error(`Invalid .ksplat compression level: ${compressionLevel}`);
}
// const sceneCenterX = header.getFloat32(24, true);
// const sceneCenterY = header.getFloat32(28, true);
// const sceneCenterZ = header.getFloat32(32, true);
const minSphericalHarmonicsCoeff = header.getFloat32(36, true) || -1.5;
const maxSphericalHarmonicsCoeff = header.getFloat32(40, true) || 1.5;
const numSplats = splatCount;
const maxSplats = computeMaxSplats(numSplats);
const packedArray = new Uint32Array(maxSplats * 4);
const extra: Record<string, unknown> = {};
let sectionBase = HEADER_BYTES + maxSectionCount * SECTION_BYTES;
for (let section = 0; section < maxSectionCount; ++section) {
const section = new DataView(fileBytes.buffer, headerOffset, SECTION_BYTES);
headerOffset += SECTION_BYTES;
const sectionSplatCount = section.getUint32(0, true);
const sectionMaxSplatCount = section.getUint32(4, true);
const bucketSize = section.getUint32(8, true);
const bucketCount = section.getUint32(12, true);
const bucketBlockSize = section.getFloat32(16, true);
const bucketStorageSizeBytes = section.getUint16(20, true);
const compressionScaleRange =
(section.getUint32(24, true) ||
KSPLAT_COMPRESSION[compressionLevel]?.scaleRange) ??
1;
// const fullBucketCount = section.getUint32(32, true);
const partiallyFilledBucketCount = section.getUint32(36, true);
const bucketsMetaDataSizeBytes = partiallyFilledBucketCount * 4;
const bucketsStorageSizeBytes =
bucketStorageSizeBytes * bucketCount + bucketsMetaDataSizeBytes;
const sphericalHarmonicsDegree = section.getUint16(40, true);
const shComponents =
KSPLAT_SH_DEGREE_TO_COMPONENTS[sphericalHarmonicsDegree];
const {
bytesPerCenter,
bytesPerScale,
bytesPerRotation,
bytesPerColor,
bytesPerSphericalHarmonicsComponent,
scaleOffsetBytes,
rotationOffsetBytes,
colorOffsetBytes,
sphericalHarmonicsOffsetBytes,
} = KSPLAT_COMPRESSION[compressionLevel];
const bytesPerSplat =
bytesPerCenter +
bytesPerScale +
bytesPerRotation +
bytesPerColor +
shComponents * bytesPerSphericalHarmonicsComponent;
const splatDataStorageSizeBytes = bytesPerSplat * sectionMaxSplatCount;
const storageSizeBytes =
splatDataStorageSizeBytes + bucketsStorageSizeBytes;
const sh1Index = [0, 3, 6, 1, 4, 7, 2, 5, 8];
const sh2Index = [
9, 14, 19, 10, 15, 20, 11, 16, 21, 12, 17, 22, 13, 18, 23,
];
const sh3Index = [
24, 31, 38, 25, 32, 39, 26, 33, 40, 27, 34, 41, 28, 35, 42, 29, 36, 43,
30, 37, 44,
];
const sh1 =
sphericalHarmonicsDegree >= 1 ? new Float32Array(3 * 3) : undefined;
const sh2 =
sphericalHarmonicsDegree >= 2 ? new Float32Array(5 * 3) : undefined;
const sh3 =
sphericalHarmonicsDegree >= 3 ? new Float32Array(7 * 3) : undefined;
const compressionScaleFactor = bucketBlockSize / 2 / compressionScaleRange;
const bucketsBase = sectionBase + bucketsMetaDataSizeBytes;
const dataBase = sectionBase + bucketsStorageSizeBytes;
const data = new DataView(
fileBytes.buffer,
dataBase,
splatDataStorageSizeBytes,
);
const bucketArray = new Float32Array(
fileBytes.buffer,
bucketsBase,
bucketCount * 3,
);
function getSh(splatOffset: number, component: number) {
if (compressionLevel === 0) {
return data.getFloat32(
splatOffset + sphericalHarmonicsOffsetBytes + component * 4,
true,
);
}
if (compressionLevel === 1) {
return fromHalf(
data.getUint16(
splatOffset + sphericalHarmonicsOffsetBytes + component * 2,
true,
),
);
}
const t =
data.getUint8(splatOffset + sphericalHarmonicsOffsetBytes + component) /
255;
return (
minSphericalHarmonicsCoeff +
t * (maxSphericalHarmonicsCoeff - minSphericalHarmonicsCoeff)
);
}
for (let i = 0; i < sectionSplatCount; ++i) {
const splatOffset = i * bytesPerSplat;
const bucketIndex = Math.floor(i / bucketSize);
const x =
compressionLevel === 0
? data.getFloat32(splatOffset + 0, true)
: (data.getUint16(splatOffset + 0, true) - compressionScaleRange) *
compressionScaleFactor +
bucketArray[3 * bucketIndex + 0];
const y =
compressionLevel === 0
? data.getFloat32(splatOffset + 4, true)
: (data.getUint16(splatOffset + 2, true) - compressionScaleRange) *
compressionScaleFactor +
bucketArray[3 * bucketIndex + 1];
const z =
compressionLevel === 0
? data.getFloat32(splatOffset + 8, true)
: (data.getUint16(splatOffset + 4, true) - compressionScaleRange) *
compressionScaleFactor +
bucketArray[3 * bucketIndex + 2];
const scaleX =
compressionLevel === 0
? data.getFloat32(splatOffset + scaleOffsetBytes + 0, true)
: fromHalf(data.getUint16(splatOffset + scaleOffsetBytes + 0, true));
const scaleY =
compressionLevel === 0
? data.getFloat32(splatOffset + scaleOffsetBytes + 4, true)
: fromHalf(data.getUint16(splatOffset + scaleOffsetBytes + 2, true));
const scaleZ =
compressionLevel === 0
? data.getFloat32(splatOffset + scaleOffsetBytes + 8, true)
: fromHalf(data.getUint16(splatOffset + scaleOffsetBytes + 4, true));
const quatW =
compressionLevel === 0
? data.getFloat32(splatOffset + rotationOffsetBytes + 0, true)
: fromHalf(
data.getUint16(splatOffset + rotationOffsetBytes + 0, true),
);
const quatX =
compressionLevel === 0
? data.getFloat32(splatOffset + rotationOffsetBytes + 4, true)
: fromHalf(
data.getUint16(splatOffset + rotationOffsetBytes + 2, true),
);
const quatY =
compressionLevel === 0
? data.getFloat32(splatOffset + rotationOffsetBytes + 8, true)
: fromHalf(
data.getUint16(splatOffset + rotationOffsetBytes + 4, true),
);
const quatZ =
compressionLevel === 0
? data.getFloat32(splatOffset + rotationOffsetBytes + 12, true)
: fromHalf(
data.getUint16(splatOffset + rotationOffsetBytes + 6, true),
);
const r = data.getUint8(splatOffset + colorOffsetBytes + 0) / 255;
const g = data.getUint8(splatOffset + colorOffsetBytes + 1) / 255;
const b = data.getUint8(splatOffset + colorOffsetBytes + 2) / 255;
const opacity = data.getUint8(splatOffset + colorOffsetBytes + 3) / 255;
setPackedSplat(
packedArray,
i,
x,
y,
z,
scaleX,
scaleY,
scaleZ,
quatX,
quatY,
quatZ,
quatW,
opacity,
r,
g,
b,
);
if (sphericalHarmonicsDegree >= 1) {
if (sh1) {
if (!extra.sh1) {
extra.sh1 = new Uint32Array(numSplats * 2);
}
for (const [i, key] of sh1Index.entries()) {
sh1[i] = getSh(splatOffset, key);
}
encodeSh1Rgb(extra.sh1 as Uint32Array, i, sh1);
}
if (sh2) {
if (!extra.sh2) {
extra.sh2 = new Uint32Array(numSplats * 4);
}
for (const [i, key] of sh2Index.entries()) {
sh2[i] = getSh(splatOffset, key);
}
encodeSh2Rgb(extra.sh2 as Uint32Array, i, sh2);
}
if (sh3) {
if (!extra.sh3) {
extra.sh3 = new Uint32Array(numSplats * 4);
}
for (const [i, key] of sh3Index.entries()) {
sh3[i] = getSh(splatOffset, key);
}
encodeSh3Rgb(extra.sh3 as Uint32Array, i, sh3);
}
}
}
sectionBase += storageSizeBytes;
}
return { packedArray, numSplats, extra };
}
// Array of buckets for sorting float16 distances with range [0, DEPTH_INFINITY].
const DEPTH_INFINITY = 0x7c00;
const DEPTH_SIZE = DEPTH_INFINITY + 1;
let depthArray: Uint32Array | null = null;
function oldSortSplats({
totalSplats,
readback,
ordering,
}: { totalSplats: number; readback: Uint8Array[]; ordering: Uint32Array }): {
activeSplats: number;
ordering: Uint32Array;
} {
// Sort totalSplats Gsplats, each with 4 bytes of readback, and outputs Uint32Array
// of indices from most distant to nearest. Each 4 bytes encode a float16 distance
// and unused high bytes.
if (!depthArray) {
depthArray = new Uint32Array(DEPTH_SIZE);
}
depthArray.fill(0);
const readbackUint32 = readback.map((layer) => new Uint32Array(layer.buffer));
const layerSize = readbackUint32[0].length;
const numLayers = Math.ceil(totalSplats / layerSize);
let layerBase = 0;
for (let layer = 0; layer < numLayers; ++layer) {
const readbackLayer = readbackUint32[layer];
const layerSplats = Math.min(readbackLayer.length, totalSplats - layerBase);
for (let i = 0; i < layerSplats; ++i) {
const pri = readbackLayer[i] & 0x7fff;
if (pri < DEPTH_INFINITY) {
depthArray[pri] += 1;
}
}
layerBase += layerSplats;
}
let activeSplats = 0;
for (let j = 0; j < DEPTH_SIZE; ++j) {
const nextIndex = activeSplats + depthArray[j];
depthArray[j] = activeSplats;
activeSplats = nextIndex;
}
layerBase = 0;
for (let layer = 0; layer < numLayers; ++layer) {
const readbackLayer = readbackUint32[layer];
const layerSplats = Math.min(readbackLayer.length, totalSplats - layerBase);
for (let i = 0; i < layerSplats; ++i) {
const pri = readbackLayer[i] & 0x7fff;
if (pri < DEPTH_INFINITY) {
ordering[depthArray[pri]] = layerBase + i;
depthArray[pri] += 1;
}
}
layerBase += layerSplats;
}
if (depthArray[DEPTH_SIZE - 1] !== activeSplats) {
throw new Error(
`Expected ${activeSplats} active splats but got ${depthArray[DEPTH_SIZE - 1]}`,
);
}
return { activeSplats, ordering };
}
// Sort numSplats splats, each with 2 bytes of float16 readback for distance metric,
// using one bucket sort pass, outputting Uint32Array of indices.
function sortSplats({
numSplats,
readback,
ordering,
}: { numSplats: number; readback: Uint16Array; ordering: Uint32Array }): {
activeSplats: number;
ordering: Uint32Array;
} {
// Ensure depthArray is allocated and zeroed out for our buckets.
if (!depthArray) {
depthArray = new Uint32Array(DEPTH_SIZE);
}
depthArray.fill(0);
// Count the number of splats in each bucket (cull Gsplats at infinity).
for (let i = 0; i < numSplats; ++i) {
const pri = readback[i];
if (pri < DEPTH_INFINITY) {
depthArray[pri] += 1;
}
}
// Compute the beginning index of each bucket in the output array and the
// total number of active (non-infinity) splats, going in reverse order
// because we want most distant Gsplats to be first in the output array.
let activeSplats = 0;
for (let j = DEPTH_INFINITY - 1; j >= 0; --j) {
const nextIndex = activeSplats + depthArray[j];
depthArray[j] = activeSplats;
activeSplats = nextIndex;
}
// Write out the sorted indices into the output array according
// bucket order.
for (let i = 0; i < numSplats; ++i) {
const pri = readback[i];
if (pri < DEPTH_INFINITY) {
ordering[depthArray[pri]] = i;
depthArray[pri] += 1;
}
}
// Sanity check that the end of the closest bucket is the same as
// our total count of active splats (not at infinity).
if (depthArray[0] !== activeSplats) {
throw new Error(
`Expected ${activeSplats} active splats but got ${depthArray[0]}`,
);
}
return { activeSplats, ordering };
}
// Buffer to queue any messages received while initializing, for example
// early messages to unpack a Gsplat file while still initializing the WASM code.
const messageBuffer: MessageEvent[] = [];
function bufferMessage(event: MessageEvent) {
messageBuffer.push(event);
}
async function initialize() {
// Hold any messages received while initializing
self.addEventListener("message", bufferMessage);
await init_wasm();
self.removeEventListener("message", bufferMessage);
self.addEventListener("message", onMessage);
// Process any buffered messages
for (const event of messageBuffer) {
onMessage(event);
}
messageBuffer.length = 0;
}
initialize().catch(console.error);