Skip to content

Commit 23a5959

Browse files
committed
add undo mode to splat painter
1 parent fa71609 commit 23a5959

2 files changed

Lines changed: 72 additions & 6 deletions

File tree

examples/splat-painter/index.html

Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@
6464
SpzWriter,
6565
unpackSplat,
6666
PackedSplats,
67+
RgbaArray,
68+
readRgbaArray,
6769
} from "@sparkjsdev/spark";
6870
import * as THREE from "three";
6971
import { getAssetFileURL } from "/examples/js/get-asset-url.js";
@@ -74,6 +76,7 @@
7476
controlsEnabled: true,
7577
eraseEnabled: dyno.dynoBool(false),
7678
brushEnabled: dyno.dynoBool(false),
79+
undoEnabled: dyno.dynoBool(false),
7780
brushDepth: dyno.dynoFloat(10.0),
7881
brushRadius: dyno.dynoFloat(0.05),
7982
brushOrigin: dyno.dynoVec3(new THREE.Vector3(0.0, 0.0, 0.0)),
@@ -90,23 +93,26 @@
9093
const assetID = "greyscale-bedroom.spz";
9194
let currentSplatMesh = null;
9295
let currentFileName = "painted-splat";
96+
let currentSplatMeshOriginalRGBA = null;
9397

9498
function brushDyno(
9599
brushEnabled,
96100
eraseEnabled,
101+
undoEnabled,
97102
brushRadius,
98103
brushDepth,
99104
brushOrigin,
100105
brushDirection,
101106
brushColor,
107+
originalRgba,
102108
) {
103109
const flatColor = dyno.dynoVec3(new THREE.Vector3(1.0, 1.0, 1.0));
104110
const luminanceThreshold = dyno.dynoFloat(0.1);
105111
return dyno.dynoBlock({ gsplat: dyno.Gsplat }, { gsplat: dyno.Gsplat }, ({ gsplat }) => {
106112
if (!gsplat) {
107113
throw new Error("No gsplat input");
108114
}
109-
let { center, rgb, opacity } = dyno.splitGsplat(gsplat).outputs;
115+
let { center, rgb, opacity, index } = dyno.splitGsplat(gsplat).outputs;
110116
const projectionAmplitude = dyno.dot(brushDirection, dyno.sub(center, brushOrigin));
111117
const projectedCenter = dyno.add(brushOrigin, dyno.mul(brushDirection, projectionAmplitude));
112118
const distance = dyno.length(dyno.sub(projectedCenter, center)); // distance from projected center to actual center
@@ -117,8 +123,20 @@
117123
const luminanceNew = dyno.div(dyno.dot(brushColor, flatColor), dyno.dynoFloat(3.0));
118124
const weightedRgb = dyno.mul(brushColor, dyno.div(luminanceOld, luminanceNew));
119125
const isLuminanceAboveThreshold = dyno.greaterThan(luminanceOld, luminanceThreshold);
120-
const newRgb = dyno.select(dyno.and(dyno.and(brushEnabled, isInside), isLuminanceAboveThreshold), weightedRgb, rgb);
121-
const newOpacity = dyno.select(eraseEnabled, dyno.select(isInside, dyno.dynoFloat(0.0), opacity), opacity);
126+
let newRgb = dyno.select(dyno.and(dyno.and(brushEnabled, isInside), isLuminanceAboveThreshold), weightedRgb, rgb);
127+
let newOpacity = dyno.select(eraseEnabled, dyno.select(isInside, dyno.dynoFloat(0.0), opacity), opacity);
128+
129+
// Undo mode: restore original RGBA for splats inside brush
130+
const originalRgbaValue = readRgbaArray(originalRgba, index);
131+
const originalRgbVec = dyno.vec3(
132+
dyno.swizzle(originalRgbaValue, "x"),
133+
dyno.swizzle(originalRgbaValue, "y"),
134+
dyno.swizzle(originalRgbaValue, "z")
135+
);
136+
const originalOpacityVal = dyno.swizzle(originalRgbaValue, "w");
137+
newRgb = dyno.select(dyno.and(undoEnabled, isInside), originalRgbVec, newRgb);
138+
newOpacity = dyno.select(dyno.and(undoEnabled, isInside), originalOpacityVal, newOpacity);
139+
122140
gsplat = dyno.combineGsplat({ gsplat, rgb: newRgb, opacity: newOpacity });
123141
return { gsplat };
124142
});
@@ -128,11 +146,13 @@
128146
url,
129147
brushEnabled,
130148
eraseEnabled,
149+
undoEnabled,
131150
brushRadius,
132151
brushDepth,
133152
brushOrigin,
134153
brushDirection,
135154
brushColor,
155+
originalRgba,
136156
) {
137157
const splatMesh = new SplatMesh({
138158
url: url,
@@ -143,11 +163,13 @@
143163
splatMesh.worldModifier = brushDyno(
144164
brushEnabled,
145165
eraseEnabled,
166+
undoEnabled,
146167
brushRadius,
147168
brushDepth,
148169
brushOrigin,
149170
brushDirection,
150171
brushColor,
172+
originalRgba,
151173
);
152174
splatMesh.updateGenerator();
153175
return splatMesh;
@@ -193,19 +215,50 @@
193215
}
194216
// Extract filename for export
195217
currentFileName = url.split("/").pop().split("?")[0].split(".")[0] || "painted-splat";
218+
219+
// Create an empty RgbaArray that will be populated after the mesh loads
220+
currentSplatMeshOriginalRGBA = new RgbaArray();
221+
196222
currentSplatMesh = await paintableSplatMesh(
197223
url,
198224
PARAMETERS.brushEnabled,
199225
PARAMETERS.eraseEnabled,
226+
PARAMETERS.undoEnabled,
200227
PARAMETERS.brushRadius,
201228
PARAMETERS.brushDepth,
202229
PARAMETERS.brushOrigin,
203230
PARAMETERS.brushDirection,
204-
PARAMETERS.brushColor
231+
PARAMETERS.brushColor,
232+
currentSplatMeshOriginalRGBA.dyno
205233
);
206234
currentSplatMesh.quaternion.set(1, 0, 0, 0);
207235
scene.add(currentSplatMesh);
208236

237+
// Wait for the mesh to fully load before accessing packed data
238+
await currentSplatMesh.initialized;
239+
240+
// Extract original RGBA directly from the packed splats
241+
currentSplatMeshOriginalRGBA = new RgbaArray();
242+
currentSplatMeshOriginalRGBA.fromPackedSplats({
243+
packedSplats: currentSplatMesh.packedSplats,
244+
base: 0,
245+
count: currentSplatMesh.packedSplats.numSplats,
246+
renderer: renderer
247+
});
248+
249+
// Update the world modifier with the populated original RGBA
250+
currentSplatMesh.worldModifier = brushDyno(
251+
PARAMETERS.brushEnabled,
252+
PARAMETERS.eraseEnabled,
253+
PARAMETERS.undoEnabled,
254+
PARAMETERS.brushRadius,
255+
PARAMETERS.brushDepth,
256+
PARAMETERS.brushOrigin,
257+
PARAMETERS.brushDirection,
258+
PARAMETERS.brushColor,
259+
currentSplatMeshOriginalRGBA.dyno
260+
);
261+
currentSplatMesh.updateGenerator();
209262
}
210263

211264
await loadSplatFromFile(await getAssetFileURL(assetID));
@@ -288,7 +341,8 @@
288341
// Instructions section
289342
const instructions = {
290343
brush: "Brush Mode",
291-
erase: "Erase Mode",
344+
erase: "Erase Mode",
345+
undo: "Undo Mode",
292346
none: "View Mode",
293347
increase: "Increase Brush Size",
294348
decrease: "Decrease Brush Size",
@@ -298,6 +352,7 @@
298352
const instructionsFolder = gui.addFolder("Instructions");
299353
instructionsFolder.add(instructions, "brush").name("1:").disable();
300354
instructionsFolder.add(instructions, "erase").name("2:").disable();
355+
instructionsFolder.add(instructions, "undo").name("3:").disable();
301356
instructionsFolder.add(instructions, "none").name("Esc:").disable();
302357
instructionsFolder.add(instructions, "increase").name("=:").disable();
303358
instructionsFolder.add(instructions, "decrease").name("-:").disable();
@@ -481,20 +536,31 @@
481536
// Brush mode
482537
PARAMETERS.brushEnabled.value = true;
483538
PARAMETERS.eraseEnabled.value = false;
539+
PARAMETERS.undoEnabled.value = false;
484540
PARAMETERS.controlsEnabled = false;
485541
controls.enabled = false;
486542
showModeOverlay('Paint Mode');
487543
} else if (event.key === '2') {
488544
// Eraser mode
489545
PARAMETERS.brushEnabled.value = false;
490546
PARAMETERS.eraseEnabled.value = true;
547+
PARAMETERS.undoEnabled.value = false;
491548
PARAMETERS.controlsEnabled = false;
492549
controls.enabled = false;
493550
showModeOverlay('Erase Mode');
551+
} else if (event.key === '3') {
552+
// Undo mode
553+
PARAMETERS.brushEnabled.value = false;
554+
PARAMETERS.eraseEnabled.value = false;
555+
PARAMETERS.undoEnabled.value = true;
556+
PARAMETERS.controlsEnabled = false;
557+
controls.enabled = false;
558+
showModeOverlay('Undo Mode');
494559
} else if (event.key === 'Escape') {
495560
// View mode
496561
PARAMETERS.brushEnabled.value = false;
497562
PARAMETERS.eraseEnabled.value = false;
563+
PARAMETERS.undoEnabled.value = false;
498564
PARAMETERS.controlsEnabled = true;
499565
controls.enabled = true;
500566
showModeOverlay('View Mode');

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ export { SparkViewpoint, type SparkViewpointOptions } from "./SparkViewpoint";
33

44
export * as dyno from "./dyno";
55

6-
export { RgbaArray } from "./RgbaArray";
6+
export { RgbaArray, readRgbaArray } from "./RgbaArray";
77

88
export {
99
SplatLoader,

0 commit comments

Comments
 (0)