Add modifiers normalColor and depthColor. Add examples/debug-color to show how to use them on SplatMeshes. (#58)

This commit is contained in:
Andreas Sundquist
2025-06-20 14:23:45 -07:00
committed by GitHub
parent a83c99eb86
commit 690e09ac95
8 changed files with 207 additions and 0 deletions
+1
View File
@@ -372,6 +372,7 @@
<a href="/examples/particle-simulation/index.html" class="example-link">Particle Simulation</a>
<a href="/examples/webxr/index.html" class="example-link">WebXR</a>
<a href="/examples/glsl/index.html" class="example-link">GLSL Shaders</a>
<a href="/examples/debug-color/index.html" class="example-link">Debug Coloring</a>
<a href="/examples/editor/index.html" class="example-link">Editor</a>
</div>
<div class="content">
+61
View File
@@ -0,0 +1,61 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Spark • Debug Coloring</title>
<style>
body {
margin: 0;
}
canvas {
touch-action: none;
}
</style>
</head>
<body>
<script type="importmap">
{
"imports": {
"three": "/examples/js/vendor/three/build/three.module.js",
"@sparkjsdev/spark": "/dist/spark.module.js"
}
}
</script>
<script type="module">
import * as THREE from "three";
import { SplatMesh, PackedSplats, dyno, modifiers } from "@sparkjsdev/spark";
import { getAssetFileURL } from "/examples/js/get-asset-url.js";
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement)
let splatURL = await getAssetFileURL("butterfly.spz");
const butterfly = new SplatMesh({ url: splatURL });
butterfly.scale.setScalar(0.5);
butterfly.quaternion.set(1, 0, 0, 0);
butterfly.position.set(-0.5, 0, -1.5);
modifiers.setWorldNormalColor(butterfly);
scene.add(butterfly);
splatURL = await getAssetFileURL("butterfly-ai.spz");
const butterfly2 = new SplatMesh({ url: splatURL });
butterfly2.quaternion.set(1, 0, 0, 0);
butterfly2.position.set(0.5, 0, -1.5);
modifiers.setDepthColor(butterfly2, 1, 2, true);
scene.add(butterfly2);
renderer.setAnimationLoop(function animate(time) {
butterfly.rotation.y += 0.01;
butterfly2.rotation.x += 0.01;
renderer.render(scene, camera);
});
</script>
</body>
</html>
+1
View File
@@ -144,6 +144,7 @@
<li><a href="/examples/particle-simulation/">Particle Simulation</a></li>
<li><a href="/examples/webxr/">WebXR</a></li>
<li><a href="/examples/glsl/">GLSL Shaders</a></li>
<li><a href="/examples/debug-color/">Debug Coloring</a></li>
<li><a href="/examples/editor/">Editor</a></li>
<li><a href="/examples/viewer/">Viewer</a></li>
</ul>
+35
View File
@@ -70,6 +70,13 @@ export const hashVec4 = <T extends ValueTypes>(
): DynoVal<"vec4"> => {
return new HashVec4({ value });
};
export const normalizedDepth = (
z: DynoVal<"float">,
zNear: DynoVal<"float">,
zFar: DynoVal<"float">,
): DynoVal<"float"> => {
return new NormalizedDepth({ z, zNear, zFar }).outputs.depth;
};
export class DynoRemapIndex
extends Dyno<{ from: "int"; to: "int"; index: "int" }, { index: "int" }>
@@ -404,3 +411,31 @@ export class HashVec4<T extends ValueTypes>
return new DynoOutput(this, "hash");
}
}
export class NormalizedDepth
extends Dyno<
{ z: "float"; zNear: "float"; zFar: "float" },
{ depth: "float" }
>
implements HasDynoOut<"float">
{
constructor({
z,
zNear,
zFar,
}: { z: DynoVal<"float">; zNear: DynoVal<"float">; zFar: DynoVal<"float"> }) {
super({
inTypes: { z: "float", zNear: "float", zFar: "float" },
outTypes: { depth: "float" },
inputs: { z, zNear, zFar },
statements: ({ inputs, outputs }) => [
`float clamped = clamp(${inputs.z}, ${inputs.zNear}, ${inputs.zFar});`,
`${outputs.depth} = (log2(clamped + 1.0) - log2(${inputs.zNear} + 1.0)) / (log2(${inputs.zFar} + 1.0) - log2(${inputs.zNear} + 1.0));`,
],
});
}
dynoOut(): DynoValue<"float"> {
return new DynoOutput(this, "depth");
}
}
+1
View File
@@ -48,6 +48,7 @@ export {
} from "./splatConstructors";
export * as generators from "./generators";
export * as modifiers from "./modifiers";
export { VRButton } from "./vrButton";
export {
+2
View File
@@ -0,0 +1,2 @@
export * from "./modifiers/normalColor";
export * from "./modifiers/depthColor";
+60
View File
@@ -0,0 +1,60 @@
import type { SplatTransformer } from "../SplatGenerator";
import type { SplatMesh } from "../SplatMesh";
import {
type DynoVal,
Gsplat,
combineGsplat,
dynoBlock,
dynoConst,
neg,
normalizedDepth,
select,
split,
splitGsplat,
sub,
} from "../dyno";
export function makeDepthColorModifier(
splatToView: SplatTransformer,
minDepth: DynoVal<"float">,
maxDepth: DynoVal<"float">,
reverse: DynoVal<"bool">,
) {
return dynoBlock({ gsplat: Gsplat }, { gsplat: Gsplat }, ({ gsplat }) => {
if (!gsplat) {
throw new Error("No gsplat input");
}
let { center } = splitGsplat(gsplat).outputs;
center = splatToView.apply(center);
const { z } = split(center).outputs;
let depth = normalizedDepth(neg(z), minDepth, maxDepth);
depth = select(reverse, sub(dynoConst("float", 1), depth), depth);
gsplat = combineGsplat({ gsplat, r: depth, g: depth, b: depth });
return { gsplat };
});
}
export function setDepthColor(
splats: SplatMesh,
minDepth: number,
maxDepth: number,
reverse?: boolean,
) {
splats.enableWorldToView = true;
const dynoMinDepth = dynoConst("float", minDepth);
const dynoMaxDepth = dynoConst("float", maxDepth);
const dynoReverse = dynoConst("bool", reverse ?? false);
splats.worldModifier = makeDepthColorModifier(
splats.context.worldToView,
dynoMinDepth,
dynoMaxDepth,
dynoReverse,
);
splats.updateGenerator();
return {
minDepth: dynoMinDepth,
maxDepth: dynoMaxDepth,
reverse: dynoReverse,
};
}
+46
View File
@@ -0,0 +1,46 @@
import type { SplatTransformer } from "../SplatGenerator";
import type { SplatMesh } from "../SplatMesh";
import {
Gsplat,
add,
combineGsplat,
dot,
dynoBlock,
dynoConst,
greaterThanEqual,
gsplatNormal,
mul,
neg,
select,
splitGsplat,
} from "../dyno";
export function makeNormalColorModifier(splatToView: SplatTransformer) {
return dynoBlock({ gsplat: Gsplat }, { gsplat: Gsplat }, ({ gsplat }) => {
if (!gsplat) {
throw new Error("No gsplat input");
}
let normal = gsplatNormal(gsplat);
const viewGsplat = splatToView.applyGsplat(gsplat);
const viewCenter = splitGsplat(viewGsplat).outputs.center;
const viewNormal = gsplatNormal(viewGsplat);
const splatDot = dot(viewCenter, viewNormal);
const sameDir = greaterThanEqual(splatDot, dynoConst("float", 0));
normal = select(sameDir, neg(normal), normal);
const rgb = add(
mul(normal, dynoConst("float", 0.5)),
dynoConst("float", 0.5),
);
gsplat = combineGsplat({ gsplat, rgb });
return { gsplat };
});
}
export function setWorldNormalColor(splats: SplatMesh) {
splats.enableWorldToView = true;
splats.worldModifier = makeNormalColorModifier(splats.context.worldToView);
splats.updateGenerator();
}