mirror of
https://github.com/storytold/spark.git
synced 2026-10-18 00:22:59 +00:00
Initial commit
This commit is contained in:
@@ -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);
|
||||
Reference in New Issue
Block a user