Skip to content

Commit f8921fa

Browse files
asundquidmarcos
authored andcommitted
Merge new commits from main into v2.0.0:
- 296eaee: Copy originToWorld values instead of holding a reference in SplatAccumulator (#191) - 1ecabe0: Interactive ripples effect (#194) - 589b6bc: Fix build_rust_wasm.sh script (correct shebang, don't require rustup) (#197) - 018a3ed: (already merged) Support logarithmic depth buffer (#199) - 8f90fe0: Fix reference counting for SplatAccumulator during updates (#200) - 18a33cb: portal effect (#203) - ad3a9bd: Fix ref counting and originToWorld in SparkViewpoint.prepare (#207) - 0d9f7bf: fix twister & rain effects (#211) - c9c488c: add greyscale painting and add i/o (#217) - 937262b: Interactive deform effect with bounce (#223) - 196721e: Add Demo Splat Dissolve Effects (#218) - 752cfaa: fix: resolve webpack/Next.js WASM data URL compatibility issue (#95) (#231) - 558a9a9: refactor: use magic-string for proper source maps in WASM fix (#234) - 237b181: Rename single character variable - fa71609: Add three as peerdependency (fix #238) - d6d5e7e: Ensure splatDefines ShaderChunk is ready when using Readback before SparkRenderer (#240) - 10a7541: add undo brush to splat painter example (#241) - a573987: Dispose of spherical harmonics arrays and packedArray in PackedSplats (#239) - dec8c4e: Add reference to Spark 2.0 preview in README - 1a8239b: Add Spark 2.0 example links to README
1 parent 87f310e commit f8921fa

16 files changed

Lines changed: 292 additions & 24 deletions

File tree

examples.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@
246246
'splat-painter': './splat-painter/index.html',
247247
'splat-reveal-effects': './splat-reveal-effects/index.html',
248248
'splat-shader-effects': './splat-shader-effects/index.html',
249+
'splat-dissolve-effects': './splat-dissolve-effects/index.html',
249250
'splat-transitions': './splat-transitions/index.html',
250251
'stochastic': './stochastic/index.html',
251252
'sogs': './sogs/index.html',

examples/interactive-deform/index.html

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,3 @@
4646
</body>
4747

4848
</html>
49-

examples/splat-dissolve-effects/index.html

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
<script type="module">
2525
import * as THREE from "three";
2626
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
27-
import { SplatMesh, dyno } from "@sparkjsdev/spark";
27+
import { SparkRenderer, SplatMesh, dyno } from "@sparkjsdev/spark";
2828
import { getAssetFileURL } from "/examples/js/get-asset-url.js";
2929

3030
const flyPos = new THREE.Vector3(0, 0, -0.5);
@@ -44,6 +44,12 @@
4444
renderer.setSize(window.innerWidth, window.innerHeight);
4545
}
4646

47+
<<<<<<< HEAD
48+
=======
49+
const spark = new SparkRenderer({ renderer });
50+
scene.add(spark);
51+
52+
>>>>>>> 1d58ed6 (Merge new commits from main into v2.0.0:)
4753
/*
4854
Asset: fly.spz
4955
Author: Danny Bittel (https://danybittel.ch/)

examples/splat-painter/index.html

Lines changed: 219 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,11 @@
6060
SparkRenderer,
6161
SplatMesh,
6262
RgbaArray,
63+
readRgbaArray,
6364
SparkControls,
65+
SpzWriter,
66+
unpackSplat,
67+
PackedSplats,
6468
} from "@sparkjsdev/spark";
6569
import * as THREE from "three";
6670
import { getAssetFileURL } from "../js/get-asset-url.js";
@@ -105,7 +109,7 @@
105109
const luminanceThreshold = dyno.dynoFloat(0.1);
106110
return dyno.dynoBlock({ gsplat: dyno.Gsplat }, { gsplat: dyno.Gsplat }, ({ gsplat }) => {
107111
if (!gsplat) {
108-
throw new Error("No gsplat input");
112+
throw new Error("No gsplat input");
109113
}
110114
let { center, rgb, opacity, index } = dyno.splitGsplat(gsplat).outputs;
111115
const projectionAmplitude = dyno.dot(brushDirection, dyno.sub(center, brushOrigin));
@@ -114,7 +118,11 @@
114118
const isInside = dyno.and(dyno.lessThan(distance, brushRadius),
115119
dyno.and(dyno.greaterThan(projectionAmplitude, dyno.dynoFloat(0.0)),
116120
dyno.lessThan(projectionAmplitude, brushDepth)));
121+
<<<<<<< HEAD
117122

123+
=======
124+
125+
>>>>>>> 1d58ed6 (Merge new commits from main into v2.0.0:)
118126
// Paint/erase mode: change RGB or alpha values for splats inside brush
119127
const luminanceOld = dyno.div(dyno.dot(rgb, flatColor), dyno.dynoFloat(3.0));
120128
const luminanceNew = dyno.div(dyno.dot(brushColor, flatColor), dyno.dynoFloat(3.0));
@@ -156,11 +164,13 @@
156164
splatMesh.worldModifier = brushDyno(
157165
brushEnabled,
158166
eraseEnabled,
167+
undoEnabled,
159168
brushRadius,
160169
brushDepth,
161170
brushOrigin,
162171
brushDirection,
163172
brushColor,
173+
originalRgba,
164174
);
165175
splatMesh.splatRgba = new RgbaArray().fromPackedSplats({
166176
packedSplats: splatMesh.packedSplats,
@@ -212,10 +222,8 @@
212222
}
213223
// Extract filename for export
214224
currentFileName = url.split("/").pop().split("?")[0].split(".")[0] || "painted-splat";
215-
216225
// Create an empty RgbaArray that will be populated after the mesh loads
217226
currentSplatMeshOriginalRGBA = new RgbaArray();
218-
219227
currentSplatMesh = await paintableSplatMesh(
220228
url,
221229
PARAMETERS.brushEnabled,
@@ -230,10 +238,16 @@
230238
);
231239
currentSplatMesh.quaternion.set(1, 0, 0, 0);
232240
scene.add(currentSplatMesh);
241+
242+
// Wait for the mesh to fully load before accessing packed data
243+
await currentSplatMesh.initialized;
233244

245+
<<<<<<< HEAD
234246
// Wait for the mesh to fully load before accessing packed data
235247
await currentSplatMesh.initialized;
236248

249+
=======
250+
>>>>>>> 1d58ed6 (Merge new commits from main into v2.0.0:)
237251
// Extract original RGBA directly from the packed splats
238252
currentSplatMeshOriginalRGBA = new RgbaArray();
239253
currentSplatMeshOriginalRGBA.fromPackedSplats({
@@ -522,6 +536,181 @@
522536
},
523537
};
524538

539+
const ioFolder = gui.addFolder("I/O");
540+
ioFolder.add(ioOptions, "loadFile").name("Load Splats (SPZ/PLY)");
541+
ioFolder.add(ioOptions, "saveToSpz").name("Save Splats (SPZ)");
542+
ioFolder.open();
543+
544+
545+
// I/O functionality (load and export)
546+
const ioOptions = {
547+
filename: currentFileName,
548+
maxSh: 0, // Painted splats don't preserve SH data
549+
fractionalBits: 12,
550+
loadFile: () => {
551+
// Create file input element
552+
const input = document.createElement('input');
553+
input.type = 'file';
554+
input.accept = '.spz,.ply';
555+
input.onchange = async (e) => {
556+
const file = e.target.files[0];
557+
if (!file) return;
558+
559+
// Create a blob URL from the file
560+
const url = URL.createObjectURL(file);
561+
562+
try {
563+
await loadSplatFromFile(url);
564+
console.log("Loaded file:", file.name);
565+
// Update filename for export
566+
ioOptions.filename = file.name.split(".")[0] || "painted-splat";
567+
} catch (error) {
568+
console.error("Error loading file:", error);
569+
alert("Failed to load file. Make sure it's a valid SPZ or PLY file.");
570+
} finally {
571+
// Clean up the object URL
572+
URL.revokeObjectURL(url);
573+
}
574+
};
575+
input.click();
576+
},
577+
saveToSpz: async () => {
578+
if (!currentSplatMesh) {
579+
console.error("Export failed - currentSplatMesh:", !!currentSplatMesh);
580+
alert("No splat mesh loaded for export.");
581+
return;
582+
}
583+
584+
try {
585+
console.log("Starting SPZ export with painted changes...");
586+
587+
if (!currentSplatMesh.splatRgba) {
588+
currentSplatMesh.splatRgba = spark.getRgba({
589+
generator: currentSplatMesh,
590+
rgba: currentSplatMesh.splatRgba
591+
});
592+
currentSplatMesh.updateGenerator();
593+
}
594+
595+
// const rgbaBytes = await spark.readRgba({
596+
// generator: currentSplatMesh,
597+
// rgba: currentSplatMesh.splatRgba
598+
// });
599+
const rgba = new RgbaArray();
600+
rgba.render({
601+
renderer,
602+
count: currentSplatMesh.packedSplats.numSplats,
603+
reader: dyno.dynoBlock({ index: "int" }, { rgba8: "vec4" }, ({ index }) => {
604+
const { gsplat } = currentSplatMesh.generator.apply({ index });
605+
const { rgba } = dyno.splitGsplat(gsplat).outputs;
606+
return { rgba8: rgba };
607+
}),
608+
})
609+
const rgbaBytes = await rgba.read();
610+
611+
const ogSplats = currentSplatMesh.packedSplats;
612+
const totalSplats = ogSplats.numSplats;
613+
console.log("Total splats:", totalSplats);
614+
615+
let nonZeroCount = 0;
616+
for (let i = 0; i < totalSplats; i++) {
617+
const opacity = rgbaBytes[i * 4 + 3] / 255;
618+
if (opacity > 0) {
619+
nonZeroCount++;
620+
}
621+
}
622+
623+
// Create new PackedSplats with baked changes
624+
const newPackedSplats = new PackedSplats({
625+
maxSplats: nonZeroCount,
626+
splatEncoding: ogSplats.splatEncoding,
627+
});
628+
629+
// Build splat array from baked RGBA
630+
let processedCount = 0;
631+
for (let i = 0; i < totalSplats; i++) {
632+
const rgbaOffset = i * 4;
633+
const opacity = rgbaBytes[rgbaOffset + 3] / 255;
634+
635+
// Skip erased splats (zero opacity)
636+
if (opacity === 0) {
637+
continue;
638+
}
639+
640+
// Unpack geometry from original packed array
641+
const unpacked = unpackSplat(
642+
ogSplats.packedArray,
643+
i,
644+
ogSplats.splatEncoding
645+
);
646+
647+
// Replace color/opacity with baked painted values
648+
unpacked.color.r = rgbaBytes[rgbaOffset + 0] / 255;
649+
unpacked.color.g = rgbaBytes[rgbaOffset + 1] / 255;
650+
unpacked.color.b = rgbaBytes[rgbaOffset + 2] / 255;
651+
unpacked.opacity = opacity;
652+
653+
// Push to new PackedSplats
654+
newPackedSplats.pushSplat(
655+
unpacked.center,
656+
unpacked.scales,
657+
unpacked.quaternion,
658+
unpacked.opacity,
659+
unpacked.color
660+
);
661+
662+
processedCount++;
663+
}
664+
665+
console.log(`Processed ${processedCount} splats`);
666+
667+
// Now export the PackedSplats to SPZ
668+
console.log("Creating SPZ writer...");
669+
const maxSh = ioOptions.maxSh;
670+
const spzWriter = new SpzWriter({
671+
numSplats: nonZeroCount,
672+
shDegree: maxSh,
673+
fractionalBits: ioOptions.fractionalBits,
674+
flagAntiAlias: true,
675+
});
676+
677+
console.log("Writing splats to SPZ...");
678+
// Iterate through the new packed array
679+
for (let i = 0; i < nonZeroCount; i++) {
680+
const unpacked = unpackSplat(
681+
newPackedSplats.packedArray,
682+
i,
683+
newPackedSplats.splatEncoding
684+
);
685+
686+
spzWriter.setCenter(i, unpacked.center.x, unpacked.center.y, unpacked.center.z);
687+
spzWriter.setScale(i, unpacked.scales.x, unpacked.scales.y, unpacked.scales.z);
688+
spzWriter.setQuat(i, unpacked.quaternion.x, unpacked.quaternion.y, unpacked.quaternion.z, unpacked.quaternion.w);
689+
spzWriter.setAlpha(i, unpacked.opacity);
690+
spzWriter.setRgb(i, unpacked.color.r, unpacked.color.g, unpacked.color.b);
691+
}
692+
const spzBytes = await spzWriter.finalize();
693+
if (spzWriter.clippedCount > 0) {
694+
console.log(`Clipped ${spzWriter.clippedCount} splats. Consider decreasing fractional-bits from ${ioOptions.fractionalBits} to reduce clipping.`);
695+
}
696+
697+
console.log("Creating download...");
698+
const blob = new Blob([spzBytes], { type: "application/octet-stream" });
699+
const url = URL.createObjectURL(blob);
700+
const a = document.createElement("a");
701+
a.href = url;
702+
a.download = ioOptions.filename + "-painted.spz";
703+
a.click();
704+
URL.revokeObjectURL(url);
705+
706+
console.log("SPZ file with painted changes downloaded successfully:", ioOptions.filename + "-painted.spz");
707+
} catch (error) {
708+
console.error("Error exporting SPZ:", error);
709+
console.error("Error stack:", error.stack);
710+
}
711+
},
712+
};
713+
525714
const ioFolder = gui.addFolder("I/O");
526715
ioFolder.add(ioOptions, "loadFile").name("Load Splats (SPZ/PLY)");
527716
ioFolder.add(ioOptions, "saveToSpz").name("Save Splats (SPZ)");
@@ -530,6 +719,7 @@
530719

531720
// Keyboard controls
532721
window.addEventListener('keydown', (event) => {
722+
<<<<<<< HEAD
533723
if (event.key === '1') {
534724
// Brush mode
535725
PARAMETERS.brushEnabled.value = true;
@@ -554,6 +744,32 @@
554744
PARAMETERS.controlsEnabled = false;
555745
controls.enabled = false;
556746
showModeOverlay('Undo Mode');
747+
=======
748+
if (event.key === '1') {
749+
// Brush mode
750+
PARAMETERS.brushEnabled.value = true;
751+
PARAMETERS.eraseEnabled.value = false;
752+
PARAMETERS.undoEnabled.value = false;
753+
PARAMETERS.controlsEnabled = false;
754+
controls.enabled = false;
755+
showModeOverlay('Paint Mode');
756+
} else if (event.key === '2') {
757+
// Eraser mode
758+
PARAMETERS.brushEnabled.value = false;
759+
PARAMETERS.eraseEnabled.value = true;
760+
PARAMETERS.undoEnabled.value = false;
761+
PARAMETERS.controlsEnabled = false;
762+
controls.enabled = false;
763+
showModeOverlay('Erase Mode');
764+
} else if (event.key === '3') {
765+
// Undo mode
766+
PARAMETERS.brushEnabled.value = false;
767+
PARAMETERS.eraseEnabled.value = false;
768+
PARAMETERS.undoEnabled.value = true;
769+
PARAMETERS.controlsEnabled = false;
770+
controls.enabled = false;
771+
showModeOverlay('Undo Mode');
772+
>>>>>>> 1d58ed6 (Merge new commits from main into v2.0.0:)
557773
} else if (event.key === 'Escape') {
558774
// View mode
559775
PARAMETERS.brushEnabled.value = false;

examples/splat-portal/index.html

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,4 @@
2727
<div class="legend">WASD + Mouse to move camera • Go through portal to switch between worlds</div>
2828
<script type="module" src="./main.js"></script>
2929
</body>
30-
</html>
31-
30+
</html>

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565
"fflate": "^0.8.2"
6666
},
6767
"peerDependencies": {
68-
"three": "^0.178.0"
68+
"three": "^0.180.0"
6969
},
7070
"keywords": ["3d", "three.js", "gsplats", "3dgs", "gaussian", "splats"]
7171
}

rust/build_rust_wasm.sh

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
WASM_TARGET="$(rustc --print sysroot 2>/dev/null)/lib/rustlib/wasm32-unknown-unknown"
44
# Check if "rustup" tool is installed, or the wasm32-unknown-unknown target aleady exists,
55
# in which case it's very likely that the required toolchain exists
6-
if ! [ -d "$WASM_TARGET" ] && (! command -v rustup &> /dev/null); then
7-
echo "Rust tool 'rustup' not found! Please install Rust to build."
6+
if ! [ -d "$WASM_TARGET" ] && (! command -v rustup &> /dev/null); then echo "Rust tool 'rustup' not found! Please install Rust to build."
87
echo "Visit Rust installation page: https://www.rust-lang.org/tools/install"
98
echo "- Likely install one-liner: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh"
109
echo "If you're using rust without rustup, make sure that the target 'wasm32-unknown-unknown' is installed"

0 commit comments

Comments
 (0)