mirror of
https://github.com/storytold/spark.git
synced 2026-10-17 00:12:59 +00:00
Initial commit
This commit is contained in:
@@ -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),
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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";
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
};
|
||||
@@ -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;`,
|
||||
];
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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} `)};`];
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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});`];
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
@@ -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[];
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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});`,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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}`);
|
||||
}
|
||||
@@ -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};`,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./generators/static";
|
||||
export * from "./generators/snow";
|
||||
@@ -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>;
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+164
@@ -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
@@ -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);
|
||||
Reference in New Issue
Block a user