mirror of
https://github.com/storytold/spark.git
synced 2026-10-10 19:51:09 +00:00
830 lines
28 KiB
TypeScript
830 lines
28 KiB
TypeScript
import * as THREE from "three";
|
||
|
||
import {
|
||
Dyno,
|
||
DynoInt,
|
||
DynoUniform,
|
||
type DynoVal,
|
||
Gsplat,
|
||
unindent,
|
||
unindentLines,
|
||
} from "./dyno";
|
||
import { newArray } from "./utils";
|
||
|
||
// Spark 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 = [];
|
||
}
|
||
if (!this.sdfs.includes(sdf)) {
|
||
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);
|