mirror of
https://github.com/storytold/spark.git
synced 2026-10-28 09:08:44 +00:00
Splat Texture (#79)
* Add experimental Splat Texture rendering and examples/splat-texture. Added star.png and heart.png to assets.json and to assets repo. Update examples/depth-of-field to compute from metric aperture size. Added SparkRenderer.renderScale debug parameter. Updated examples/editor: set default highDPI to true, added logFocalDistance, apertureAngle, refactored, grouped into debugFolder, default settings open, preset buttons for nonAA and AA settings. * Set default highDpi in examples/editor to not(isMobile).
This commit is contained in:
committed by
GitHub
parent
c0db3576bd
commit
179885595c
@@ -374,6 +374,7 @@
|
|||||||
<a href="/examples/glsl/index.html" class="example-link">GLSL Shaders</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/debug-color/index.html" class="example-link">Debug Coloring</a>
|
||||||
<a href="/examples/depth-of-field/index.html" class="example-link">Depth of Field</a>
|
<a href="/examples/depth-of-field/index.html" class="example-link">Depth of Field</a>
|
||||||
|
<a href="/examples/splat-texture/index.html" class="example-link">Splat Texture</a>
|
||||||
<a href="/examples/editor/index.html" class="example-link">Editor</a>
|
<a href="/examples/editor/index.html" class="example-link">Editor</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
|||||||
@@ -102,5 +102,13 @@
|
|||||||
"valley.spz": {
|
"valley.spz": {
|
||||||
"url": "https://sparkjs.dev/assets/splats/valley.spz",
|
"url": "https://sparkjs.dev/assets/splats/valley.spz",
|
||||||
"directory": "splats"
|
"directory": "splats"
|
||||||
|
},
|
||||||
|
"star.png": {
|
||||||
|
"url": "https://sparkjs.dev/assets/images/star.png",
|
||||||
|
"directory": "images"
|
||||||
|
},
|
||||||
|
"heart.png": {
|
||||||
|
"url": "https://sparkjs.dev/assets/images/heart.png",
|
||||||
|
"directory": "images"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,15 +38,29 @@
|
|||||||
document.body.appendChild(renderer.domElement)
|
document.body.appendChild(renderer.domElement)
|
||||||
|
|
||||||
const spark = new SparkRenderer({
|
const spark = new SparkRenderer({
|
||||||
renderer,
|
renderer,
|
||||||
apertureAngle: 0.02,
|
apertureAngle: 0.02,
|
||||||
focalDistance: 5.0,
|
focalDistance: 5.0,
|
||||||
});
|
});
|
||||||
scene.add(spark);
|
scene.add(spark);
|
||||||
|
|
||||||
|
const apertureSize = {
|
||||||
|
apertureSize: 0.1,
|
||||||
|
};
|
||||||
|
function updateApertureAngle() {
|
||||||
|
if (spark.focalDistance > 0) {
|
||||||
|
spark.apertureAngle = 2 * Math.atan(0.5 * apertureSize.apertureSize / spark.focalDistance);
|
||||||
|
} else {
|
||||||
|
spark.apertureAngle = 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateApertureAngle();
|
||||||
|
|
||||||
const gui = new GUI({ title: "DoF settings" });
|
const gui = new GUI({ title: "DoF settings" });
|
||||||
gui.add(spark, "focalDistance", 0.1, 15, 0.1);
|
gui.add(spark, "focalDistance", 0, 15, 0.01).name("Focal plane dist")
|
||||||
gui.add(spark, "apertureAngle", 0.0, 0.01 * Math.PI, 0.001);
|
.onChange(updateApertureAngle);
|
||||||
|
gui.add(apertureSize, "apertureSize", 0, 0.4, 0.01).name("Aperture size")
|
||||||
|
.onChange(updateApertureAngle);
|
||||||
|
|
||||||
const splatURL = await getAssetFileURL("valley.spz");
|
const splatURL = await getAssetFileURL("valley.spz");
|
||||||
const background = new SplatMesh({ url: splatURL });
|
const background = new SplatMesh({ url: splatURL });
|
||||||
|
|||||||
+49
-24
@@ -68,7 +68,7 @@
|
|||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
|
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
|
||||||
import { GUI } from "lil-gui";
|
import { GUI } from "lil-gui";
|
||||||
import { constructGrid, SparkControls, SparkRenderer, SplatMesh, textSplats, dyno, transcodeSpz, isPcSogs } from "@sparkjsdev/spark";
|
import { constructGrid, SparkControls, SparkRenderer, SplatMesh, textSplats, dyno, transcodeSpz, isMobile, isPcSogs } from "@sparkjsdev/spark";
|
||||||
import { getAssetFileURL } from "/examples/js/get-asset-url.js";
|
import { getAssetFileURL } from "/examples/js/get-asset-url.js";
|
||||||
|
|
||||||
const scene = new THREE.Scene();
|
const scene = new THREE.Scene();
|
||||||
@@ -113,7 +113,7 @@
|
|||||||
const gui = new GUI({
|
const gui = new GUI({
|
||||||
title: "Settings",
|
title: "Settings",
|
||||||
container: document.getElementById("main-gui")
|
container: document.getElementById("main-gui")
|
||||||
}).close();
|
});
|
||||||
const secondGui = new GUI({
|
const secondGui = new GUI({
|
||||||
title: "Splats",
|
title: "Splats",
|
||||||
container: document.getElementById("second-gui")
|
container: document.getElementById("second-gui")
|
||||||
@@ -134,7 +134,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const guiOptions = {
|
const guiOptions = {
|
||||||
highDevicePixel: false,
|
highDevicePixel: !isMobile(),
|
||||||
stats: false,
|
stats: false,
|
||||||
resetOnLoad: true,
|
resetOnLoad: true,
|
||||||
loadOffset: 0,
|
loadOffset: 0,
|
||||||
@@ -462,30 +462,55 @@
|
|||||||
controls.pointerControls.reverseSwipe = value;
|
controls.pointerControls.reverseSwipe = value;
|
||||||
});
|
});
|
||||||
|
|
||||||
gui.add(guiOptions, "highDevicePixel").name("High DPI").onChange((value) => {
|
function setHighDpi(value) {
|
||||||
renderer.setPixelRatio(value ? window.devicePixelRatio : 1);
|
renderer.setPixelRatio(value ? window.devicePixelRatio : 1);
|
||||||
const width = canvas.clientWidth;
|
const width = canvas.clientWidth;
|
||||||
const height = canvas.clientHeight;
|
const height = canvas.clientHeight;
|
||||||
renderer.setSize(width, height, false);
|
renderer.setSize(width, height, false);
|
||||||
console.log("Render size", canvas.width, canvas.height);
|
console.log("Render size", canvas.width, canvas.height);
|
||||||
|
}
|
||||||
|
setHighDpi(guiOptions.highDevicePixel);
|
||||||
|
|
||||||
|
gui.add(guiOptions, "highDevicePixel").name("High DPI").onChange((value) => {
|
||||||
|
setHighDpi(value);
|
||||||
});
|
});
|
||||||
gui.add(guiOptions, "stats").name("Show frame stats").onChange((value) => {
|
gui.add(guiOptions, "stats").name("Show frame stats").onChange((value) => {
|
||||||
stats.dom.style.display = value ? "block" : "none";
|
stats.dom.style.display = value ? "block" : "none";
|
||||||
});
|
});
|
||||||
gui.add(spark.defaultView, "sortRadial").name("Radial sort").listen();
|
gui.add(spark.defaultView, "sortRadial").name("Radial sort").listen();
|
||||||
gui.add(grid, "opacity", 0, 1, 0.01).name("Grid opacity").listen();
|
gui.add(grid, "opacity", 0, 1, 0.01).name("Grid opacity").listen();
|
||||||
|
gui.add({
|
||||||
|
logFocalDistance: 0.0,
|
||||||
|
}, "logFocalDistance", -2, 2, 0.01).name("Ln(Focal distance)").onChange((value) => {
|
||||||
|
spark.focalDistance = Math.exp(value);
|
||||||
|
});
|
||||||
|
gui.add(spark, "apertureAngle", 0, 0.01 * Math.PI, 0.001).name("Aperture angle").listen();
|
||||||
|
|
||||||
|
const debugFolder = gui.addFolder("Debug").close();
|
||||||
const normalColor = dyno.dynoBool(false);
|
const normalColor = dyno.dynoBool(false);
|
||||||
gui.add(normalColor, "value").name("Normal color").onChange(() => updateFrameSplats());
|
debugFolder.add(normalColor, "value").name("Normal color").onChange(() => updateFrameSplats());
|
||||||
|
|
||||||
gui.add(spark, "maxStdDev", 0.1, 3.0, 0.01).name("Max Gsplat stddev").listen();
|
debugFolder.add(spark, "maxStdDev", 0.1, 3.0, 0.01).name("Max Gsplat stddev").listen();
|
||||||
gui.add(spark, "falloff", 0, 1, 0.01).name("Gaussian falloff").listen();
|
debugFolder.add(spark, "falloff", 0, 1, 0.01).name("Gaussian falloff").listen();
|
||||||
gui.add(spark, "preBlurAmount", 0, 2, 0.1).name("Blur amount (no AA)");
|
debugFolder.add(spark, "preBlurAmount", 0, 2, 0.1).name("Blur amount (no AA)").listen();
|
||||||
gui.add(spark, "blurAmount", 0, 2, 0.1).name("Blur amount (AA)");
|
debugFolder.add(spark, "blurAmount", 0, 2, 0.1).name("Blur amount (AA)").listen();
|
||||||
|
debugFolder.add({
|
||||||
|
nonAA: () => {
|
||||||
|
spark.preBlurAmount = 0.3;
|
||||||
|
spark.blurAmount = 0.0;
|
||||||
|
},
|
||||||
|
}, "nonAA").name("Non-AA preset");
|
||||||
|
debugFolder.add({
|
||||||
|
AA: () => {
|
||||||
|
spark.preBlurAmount = 0.0;
|
||||||
|
spark.blurAmount = 0.3;
|
||||||
|
},
|
||||||
|
}, "AA").name("AA preset");
|
||||||
|
debugFolder.add(spark, "renderScale", 0.1, 2.0, 0.1).name("Render scale");
|
||||||
|
|
||||||
const splatsFolder = secondGui.addFolder("Files");
|
const splatsFolder = secondGui.addFolder("Files");
|
||||||
|
|
||||||
const editFolder = gui.addFolder("Edit Splats").close();
|
const clipFolder = gui.addFolder("Clip Splats").close();
|
||||||
|
|
||||||
function updateFrameSplats() {
|
function updateFrameSplats() {
|
||||||
frame.children.forEach((child) => {
|
frame.children.forEach((child) => {
|
||||||
@@ -502,13 +527,13 @@
|
|||||||
const clipMaxY = dyno.dynoFloat(5);
|
const clipMaxY = dyno.dynoFloat(5);
|
||||||
const clipMinZ = dyno.dynoFloat(-5);
|
const clipMinZ = dyno.dynoFloat(-5);
|
||||||
const clipMaxZ = dyno.dynoFloat(5);
|
const clipMaxZ = dyno.dynoFloat(5);
|
||||||
editFolder.add(clipEnable, "value").name("Enable clip").onChange(() => updateFrameSplats());
|
clipFolder.add(clipEnable, "value").name("Enable clip").onChange(() => updateFrameSplats());
|
||||||
editFolder.add(clipMinX, "value", -5, 5, 0.01).name("Min X").onChange(() => updateFrameSplats());
|
clipFolder.add(clipMinX, "value", -50, 50, 0.01).name("Min X").onChange(() => updateFrameSplats());
|
||||||
editFolder.add(clipMaxX, "value", -5, 5, 0.01).name("Max X").onChange(() => updateFrameSplats());
|
clipFolder.add(clipMaxX, "value", -50, 50, 0.01).name("Max X").onChange(() => updateFrameSplats());
|
||||||
editFolder.add(clipMinY, "value", -5, 5, 0.01).name("Min Y").onChange(() => updateFrameSplats());
|
clipFolder.add(clipMinY, "value", -50, 50, 0.01).name("Min Y").onChange(() => updateFrameSplats());
|
||||||
editFolder.add(clipMaxY, "value", -5, 5, 0.01).name("Max Y").onChange(() => updateFrameSplats());
|
clipFolder.add(clipMaxY, "value", -50, 50, 0.01).name("Max Y").onChange(() => updateFrameSplats());
|
||||||
editFolder.add(clipMinZ, "value", -5, 5, 0.01).name("Min Z").onChange(() => updateFrameSplats());
|
clipFolder.add(clipMinZ, "value", -50, 50, 0.01).name("Min Z").onChange(() => updateFrameSplats());
|
||||||
editFolder.add(clipMaxZ, "value", -5, 5, 0.01).name("Max Z").onChange(() => updateFrameSplats());
|
clipFolder.add(clipMaxZ, "value", -50, 50, 0.01).name("Max Z").onChange(() => updateFrameSplats());
|
||||||
|
|
||||||
function makeWorldModifier(mesh) {
|
function makeWorldModifier(mesh) {
|
||||||
const context = mesh.context;
|
const context = mesh.context;
|
||||||
@@ -548,7 +573,7 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const writeFolder = secondGui.addFolder("Write Gsplats").close();
|
const exportFolder = secondGui.addFolder("Export Gsplats").close();
|
||||||
const writeOptions = {
|
const writeOptions = {
|
||||||
filename: "gsplats",
|
filename: "gsplats",
|
||||||
trimOpacity: true,
|
trimOpacity: true,
|
||||||
@@ -591,12 +616,12 @@
|
|||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
writeFolder.add(writeOptions, "filename").name("Filename").listen();
|
exportFolder.add(writeOptions, "filename").name("Filename").listen();
|
||||||
writeFolder.add(writeOptions, "trimOpacity").name("Trim low opacity");
|
exportFolder.add(writeOptions, "trimOpacity").name("Trim low opacity");
|
||||||
writeFolder.add(writeOptions, "trimOpacityThreshold").name("Trim opacity <= 0..1");
|
exportFolder.add(writeOptions, "trimOpacityThreshold").name("Trim opacity <= 0..1");
|
||||||
writeFolder.add(writeOptions, "maxSh", 0, 3, 1).name("Max spherical harmonics");
|
exportFolder.add(writeOptions, "maxSh", 0, 3, 1).name("Max spherical harmonics");
|
||||||
writeFolder.add(writeOptions, "fractionalBits", 6, 24, 1).name("Fractional bits");
|
exportFolder.add(writeOptions, "fractionalBits", 6, 24, 1).name("Fractional bits");
|
||||||
writeFolder.add(writeOptions, "writeSpz").name("Create .spz and download");
|
exportFolder.add(writeOptions, "writeSpz").name("Create .spz and download");
|
||||||
|
|
||||||
function makeInstructions() {
|
function makeInstructions() {
|
||||||
const instructions = textSplats({
|
const instructions = textSplats({
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Spark • Splat Texture</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",
|
||||||
|
"lil-gui": "/examples/js/vendor/lil-gui/dist/lil-gui.esm.js",
|
||||||
|
"@sparkjsdev/spark": "/dist/spark.module.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<script type="module">
|
||||||
|
import * as THREE from "three";
|
||||||
|
import { SparkRenderer, SplatMesh, SparkControls } from "@sparkjsdev/spark";
|
||||||
|
import GUI from "lil-gui";
|
||||||
|
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.01, 100);
|
||||||
|
const renderer = new THREE.WebGLRenderer();
|
||||||
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||||
|
document.body.appendChild(renderer.domElement)
|
||||||
|
|
||||||
|
function imgToRgba(img) {
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
canvas.width = img.width;
|
||||||
|
canvas.height = img.height;
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
ctx.drawImage(img, 0, 0);
|
||||||
|
const imageData = ctx.getImageData(0, 0, img.width, img.height);
|
||||||
|
const rgba = new Uint8Array(imageData.data.buffer);
|
||||||
|
return { rgba, width: img.width, height: img.height };
|
||||||
|
}
|
||||||
|
|
||||||
|
const textureLoader = new THREE.TextureLoader();
|
||||||
|
const starUrl = await getAssetFileURL("star.png");
|
||||||
|
const star = imgToRgba((await textureLoader.loadAsync(starUrl)).image);
|
||||||
|
const heartUrl = await getAssetFileURL("heart.png");
|
||||||
|
const heart = imgToRgba((await textureLoader.loadAsync(heartUrl)).image);
|
||||||
|
|
||||||
|
const splatTexLayers = 32;
|
||||||
|
const texData = new Uint8Array(4 * star.width * star.height * splatTexLayers);
|
||||||
|
const texData2 = new Uint8Array(4 * star.width * star.height * splatTexLayers);
|
||||||
|
for (let z = 0; z < splatTexLayers; z++) {
|
||||||
|
const t = z / (splatTexLayers - 1);
|
||||||
|
const starOpacity = Math.max((t - 0.5) / 0.5, 0.0);
|
||||||
|
const heartOpacity = Math.max((0.5 - t) / 0.5, 0.0);
|
||||||
|
const gaussOpacity = Math.max(1 - Math.abs(t - 0.5) / 0.5, 0.0);
|
||||||
|
for (let y = 0; y < star.height; y++) {
|
||||||
|
for (let x = 0; x < star.width; x++) {
|
||||||
|
const inIndex = (y * star.width) + x;
|
||||||
|
const outIndex = ((z * star.height) + y) * star.width + x;
|
||||||
|
const o4 = outIndex * 4;
|
||||||
|
const i4 = inIndex * 4;
|
||||||
|
const deltaX = x / star.width - 0.5;
|
||||||
|
const deltaY = y / star.height - 0.5;
|
||||||
|
const dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY) / 0.5;
|
||||||
|
const inside = (dist < 1.0) ? 255 : 0;
|
||||||
|
let r = t * star.rgba[i4] + (1 - t) * inside;
|
||||||
|
let g = t * star.rgba[i4 + 1] + (1 - t) * inside;
|
||||||
|
let b = t * star.rgba[i4 + 2] + (1 - t) * inside;
|
||||||
|
let a = t * star.rgba[i4 + 3] + (1 - t) * inside;
|
||||||
|
texData[o4] = Math.max(0, Math.min(255, Math.round(r)));
|
||||||
|
texData[o4 + 1] = Math.max(0, Math.min(255, Math.round(g)));
|
||||||
|
texData[o4 + 2] = Math.max(0, Math.min(255, Math.round(b)));
|
||||||
|
texData[o4 + 3] = Math.max(0, Math.min(255, Math.round(a)));
|
||||||
|
|
||||||
|
r = starOpacity * star.rgba[i4] + heartOpacity * heart.rgba[i4] + inside * gaussOpacity;
|
||||||
|
g = starOpacity * star.rgba[i4 + 1] + heartOpacity * heart.rgba[i4 + 1] + inside * gaussOpacity;
|
||||||
|
b = starOpacity * star.rgba[i4 + 2] + heartOpacity * heart.rgba[i4 + 2] + inside * gaussOpacity;
|
||||||
|
a = starOpacity * star.rgba[i4 + 3] + heartOpacity * heart.rgba[i4 + 3] + inside * gaussOpacity;
|
||||||
|
texData2[o4] = Math.max(0, Math.min(255, Math.round(r)));
|
||||||
|
texData2[o4 + 1] = Math.max(0, Math.min(255, Math.round(g)));
|
||||||
|
texData2[o4 + 2] = Math.max(0, Math.min(255, Math.round(b)));
|
||||||
|
texData2[o4 + 3] = Math.max(0, Math.min(255, Math.round(a)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const texture = new THREE.Data3DTexture(texData, star.width, star.height, splatTexLayers);
|
||||||
|
texture.needsUpdate = true;
|
||||||
|
const texture2 = new THREE.Data3DTexture(texData2, star.width, star.height, splatTexLayers);
|
||||||
|
texture2.needsUpdate = true;
|
||||||
|
|
||||||
|
const spark = new SparkRenderer({
|
||||||
|
renderer,
|
||||||
|
maxStdDev: 1.0,
|
||||||
|
focalDistance: 0,
|
||||||
|
});
|
||||||
|
const splatTexture = {
|
||||||
|
enable: true,
|
||||||
|
texture: texture,
|
||||||
|
near: 1.0,
|
||||||
|
far: 15,
|
||||||
|
mid: 5,
|
||||||
|
};
|
||||||
|
spark.splatTexture = splatTexture;
|
||||||
|
scene.add(spark);
|
||||||
|
|
||||||
|
const gui = new GUI({ title: "DoF settings" });
|
||||||
|
const selectedTexture = {
|
||||||
|
selection: "texture",
|
||||||
|
};
|
||||||
|
gui.add(selectedTexture, "selection", ["none", "texture", "texture2"]).name("Splat texture").onChange(() => {
|
||||||
|
splatTexture.enable = true;
|
||||||
|
if (selectedTexture.selection === "texture") {
|
||||||
|
splatTexture.texture = texture;
|
||||||
|
} else if (selectedTexture.selection === "texture2") {
|
||||||
|
splatTexture.texture = texture2;
|
||||||
|
} else {
|
||||||
|
splatTexture.enable = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
gui.add(spark, "maxStdDev", 0.1, 3, 0.1);
|
||||||
|
gui.add(spark, "falloff", 0, 1, 0.01).name("Gaussian falloff");
|
||||||
|
|
||||||
|
const apertureSize = {
|
||||||
|
apertureSize: 0.1,
|
||||||
|
};
|
||||||
|
function updateApertureAngle() {
|
||||||
|
splatTexture.mid = spark.focalDistance;
|
||||||
|
splatTexture.near = spark.focalDistance / 5.0;
|
||||||
|
splatTexture.far = spark.focalDistance * 5.0;
|
||||||
|
if (spark.focalDistance > 0) {
|
||||||
|
spark.apertureAngle = 2 * Math.atan(0.5 * apertureSize.apertureSize / spark.focalDistance);
|
||||||
|
} else {
|
||||||
|
spark.apertureAngle = 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateApertureAngle();
|
||||||
|
|
||||||
|
gui.add(spark, "focalDistance", 0, 15, 0.01).name("Focal plane dist")
|
||||||
|
.onChange(updateApertureAngle);
|
||||||
|
gui.add(apertureSize, "apertureSize", 0, 1, 0.01).name("Aperture size")
|
||||||
|
.onChange(updateApertureAngle);
|
||||||
|
|
||||||
|
const splatURL = await getAssetFileURL("valley.spz");
|
||||||
|
const background = new SplatMesh({ url: splatURL });
|
||||||
|
background.quaternion.set(1, 0, 0, 0);
|
||||||
|
background.scale.setScalar(0.5);
|
||||||
|
scene.add(background);
|
||||||
|
|
||||||
|
const controls = new SparkControls({ canvas: renderer.domElement });
|
||||||
|
renderer.setAnimationLoop(function animate(time) {
|
||||||
|
controls.update(camera);
|
||||||
|
renderer.render(scene, camera);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -146,6 +146,7 @@
|
|||||||
<li><a href="/examples/glsl/">GLSL Shaders</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/debug-color/">Debug Coloring</a></li>
|
||||||
<li><a href="/examples/depth-of-field/">Depth of Field</a></li>
|
<li><a href="/examples/depth-of-field/">Depth of Field</a></li>
|
||||||
|
<li><a href="/examples/splat-texture/">Splat Texture</a></li>
|
||||||
<li><a href="/examples/editor/">Editor</a></li>
|
<li><a href="/examples/editor/">Editor</a></li>
|
||||||
<li><a href="/examples/viewer/">Viewer</a></li>
|
<li><a href="/examples/viewer/">Viewer</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -148,6 +148,17 @@ export class SparkRenderer extends THREE.Mesh {
|
|||||||
apertureAngle: number;
|
apertureAngle: number;
|
||||||
falloff: number;
|
falloff: number;
|
||||||
clipXY: number;
|
clipXY: number;
|
||||||
|
renderScale = 1.0;
|
||||||
|
|
||||||
|
splatTexture: null | {
|
||||||
|
enable?: boolean;
|
||||||
|
texture?: THREE.Data3DTexture;
|
||||||
|
multiply?: THREE.Matrix2;
|
||||||
|
add?: THREE.Vector2;
|
||||||
|
near?: number;
|
||||||
|
far?: number;
|
||||||
|
mid?: number;
|
||||||
|
} = null;
|
||||||
|
|
||||||
time?: number;
|
time?: number;
|
||||||
deltaTime?: number;
|
deltaTime?: number;
|
||||||
@@ -198,6 +209,8 @@ export class SparkRenderer extends THREE.Mesh {
|
|||||||
} | null = null;
|
} | null = null;
|
||||||
private static pmrem: THREE.PMREMGenerator | null = null;
|
private static pmrem: THREE.PMREMGenerator | null = null;
|
||||||
|
|
||||||
|
static EMPTY_SPLAT_TEXTURE = new THREE.Data3DTexture();
|
||||||
|
|
||||||
constructor(options: SparkRendererOptions) {
|
constructor(options: SparkRendererOptions) {
|
||||||
const uniforms = SparkRenderer.makeUniforms();
|
const uniforms = SparkRenderer.makeUniforms();
|
||||||
const shaders = getShaders();
|
const shaders = getShaders();
|
||||||
@@ -281,6 +294,9 @@ export class SparkRenderer extends THREE.Mesh {
|
|||||||
const uniforms = {
|
const uniforms = {
|
||||||
// Size of render viewport in pixels
|
// Size of render viewport in pixels
|
||||||
renderSize: { value: new THREE.Vector2() },
|
renderSize: { value: new THREE.Vector2() },
|
||||||
|
// Near and far plane distances
|
||||||
|
near: { value: 0.1 },
|
||||||
|
far: { value: 1000.0 },
|
||||||
// Total number of Gsplats in packedSplats to render
|
// Total number of Gsplats in packedSplats to render
|
||||||
numSplats: { value: 0 },
|
numSplats: { value: 0 },
|
||||||
// SplatAccumulator to view transformation quaternion
|
// SplatAccumulator to view transformation quaternion
|
||||||
@@ -304,6 +320,22 @@ export class SparkRenderer extends THREE.Mesh {
|
|||||||
falloff: { value: 1.0 },
|
falloff: { value: 1.0 },
|
||||||
// Clip Gsplats that are clipXY times beyond the +-1 frustum bounds
|
// Clip Gsplats that are clipXY times beyond the +-1 frustum bounds
|
||||||
clipXY: { value: 1.4 },
|
clipXY: { value: 1.4 },
|
||||||
|
// Debug renderSize scale factor
|
||||||
|
renderScale: { value: 1.0 },
|
||||||
|
// Enable splat texture rendering
|
||||||
|
splatTexEnable: { value: false },
|
||||||
|
// Splat texture to render
|
||||||
|
splatTexture: { type: "t", value: SparkRenderer.EMPTY_SPLAT_TEXTURE },
|
||||||
|
// Splat texture UV transform (multiply)
|
||||||
|
splatTexMul: { value: new THREE.Matrix2() },
|
||||||
|
// Splat texture UV transform (add)
|
||||||
|
splatTexAdd: { value: new THREE.Vector2() },
|
||||||
|
// Splat texture near plane distance
|
||||||
|
splatTexNear: { value: 0.1 },
|
||||||
|
// Splat texture far plane distance
|
||||||
|
splatTexFar: { value: 1000.0 },
|
||||||
|
// Splat texture mid plane distance, or 0.0 to disable
|
||||||
|
splatTexMid: { value: 0.0 },
|
||||||
// Gsplat collection to render
|
// Gsplat collection to render
|
||||||
packedSplats: { type: "t", value: PackedSplats.getEmpty() },
|
packedSplats: { type: "t", value: PackedSplats.getEmpty() },
|
||||||
// Time in seconds for time-based effects
|
// Time in seconds for time-based effects
|
||||||
@@ -433,6 +465,11 @@ export class SparkRenderer extends THREE.Mesh {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update uniforms from instance properties
|
// Update uniforms from instance properties
|
||||||
|
const typedCamera = camera as
|
||||||
|
| THREE.PerspectiveCamera
|
||||||
|
| THREE.OrthographicCamera;
|
||||||
|
this.uniforms.near.value = typedCamera.near;
|
||||||
|
this.uniforms.far.value = typedCamera.far;
|
||||||
this.uniforms.encodeLinear.value = viewpoint.encodeLinear;
|
this.uniforms.encodeLinear.value = viewpoint.encodeLinear;
|
||||||
this.uniforms.maxStdDev.value = this.maxStdDev;
|
this.uniforms.maxStdDev.value = this.maxStdDev;
|
||||||
this.uniforms.enable2DGS.value = this.enable2DGS;
|
this.uniforms.enable2DGS.value = this.enable2DGS;
|
||||||
@@ -442,6 +479,36 @@ export class SparkRenderer extends THREE.Mesh {
|
|||||||
this.uniforms.apertureAngle.value = this.apertureAngle;
|
this.uniforms.apertureAngle.value = this.apertureAngle;
|
||||||
this.uniforms.falloff.value = this.falloff;
|
this.uniforms.falloff.value = this.falloff;
|
||||||
this.uniforms.clipXY.value = this.clipXY;
|
this.uniforms.clipXY.value = this.clipXY;
|
||||||
|
this.uniforms.renderScale.value = this.renderScale;
|
||||||
|
|
||||||
|
if (this.splatTexture) {
|
||||||
|
const { enable, texture, multiply, add, near, far, mid } =
|
||||||
|
this.splatTexture;
|
||||||
|
if (enable && texture) {
|
||||||
|
this.uniforms.splatTexEnable.value = true;
|
||||||
|
this.uniforms.splatTexture.value = texture;
|
||||||
|
if (multiply) {
|
||||||
|
this.uniforms.splatTexMul.value.fromArray(multiply.elements);
|
||||||
|
} else {
|
||||||
|
this.uniforms.splatTexMul.value.set(
|
||||||
|
0.5 / this.maxStdDev,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0.5 / this.maxStdDev,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.uniforms.splatTexAdd.value.set(add?.x ?? 0.5, add?.y ?? 0.5);
|
||||||
|
this.uniforms.splatTexNear.value = near ?? this.uniforms.near.value;
|
||||||
|
this.uniforms.splatTexFar.value = far ?? this.uniforms.far.value;
|
||||||
|
this.uniforms.splatTexMid.value = mid ?? 0.0;
|
||||||
|
} else {
|
||||||
|
this.uniforms.splatTexEnable.value = false;
|
||||||
|
this.uniforms.splatTexture.value = SparkRenderer.EMPTY_SPLAT_TEXTURE;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.uniforms.splatTexEnable.value = false;
|
||||||
|
this.uniforms.splatTexture.value = SparkRenderer.EMPTY_SPLAT_TEXTURE;
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate the transform from the accumulator to the current camera
|
// Calculate the transform from the accumulator to the current camera
|
||||||
const accumToWorld =
|
const accumToWorld =
|
||||||
|
|||||||
@@ -4,11 +4,21 @@ precision highp int;
|
|||||||
|
|
||||||
#include <splatDefines>
|
#include <splatDefines>
|
||||||
|
|
||||||
|
uniform float near;
|
||||||
|
uniform float far;
|
||||||
uniform bool encodeLinear;
|
uniform bool encodeLinear;
|
||||||
uniform float maxStdDev;
|
uniform float maxStdDev;
|
||||||
uniform bool disableFalloff;
|
uniform bool disableFalloff;
|
||||||
uniform float falloff;
|
uniform float falloff;
|
||||||
|
|
||||||
|
uniform bool splatTexEnable;
|
||||||
|
uniform sampler3D splatTexture;
|
||||||
|
uniform mat2 splatTexMul;
|
||||||
|
uniform vec2 splatTexAdd;
|
||||||
|
uniform float splatTexNear;
|
||||||
|
uniform float splatTexFar;
|
||||||
|
uniform float splatTexMid;
|
||||||
|
|
||||||
out vec4 fragColor;
|
out vec4 fragColor;
|
||||||
|
|
||||||
in vec4 vRgba;
|
in vec4 vRgba;
|
||||||
@@ -16,20 +26,45 @@ in vec2 vSplatUv;
|
|||||||
in vec3 vNdc;
|
in vec3 vNdc;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
vec4 rgba = vRgba;
|
||||||
|
|
||||||
float z = dot(vSplatUv, vSplatUv);
|
float z = dot(vSplatUv, vSplatUv);
|
||||||
if (z > (maxStdDev * maxStdDev)) {
|
if (!splatTexEnable) {
|
||||||
discard;
|
if (z > (maxStdDev * maxStdDev)) {
|
||||||
|
discard;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
vec2 uv = splatTexMul * vSplatUv + splatTexAdd;
|
||||||
|
float ndcZ = vNdc.z;
|
||||||
|
float depth = (2.0 * near * far) / (far + near - ndcZ * (far - near));
|
||||||
|
float clampedFar = max(splatTexFar, splatTexNear);
|
||||||
|
float clampedDepth = clamp(depth, splatTexNear, clampedFar);
|
||||||
|
float logDepth = log2(clampedDepth + 1.0);
|
||||||
|
float logNear = log2(splatTexNear + 1.0);
|
||||||
|
float logFar = log2(clampedFar + 1.0);
|
||||||
|
|
||||||
|
float texZ;
|
||||||
|
if (splatTexMid > 0.0) {
|
||||||
|
float clampedMid = clamp(splatTexMid, splatTexNear, clampedFar);
|
||||||
|
float logMid = log2(clampedMid + 1.0);
|
||||||
|
texZ = (clampedDepth <= clampedMid) ?
|
||||||
|
(0.5 * ((logDepth - logNear) / (logMid - logNear))) :
|
||||||
|
(0.5 * ((logDepth - logMid) / (logFar - logMid)) + 0.5);
|
||||||
|
} else {
|
||||||
|
texZ = (logDepth - logNear) / (logFar - logNear);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec4 modulate = texture(splatTexture, vec3(uv, 1.0 - texZ));
|
||||||
|
rgba *= modulate;
|
||||||
}
|
}
|
||||||
|
|
||||||
float alpha = vRgba.a;
|
rgba.a *= mix(1.0, exp(-0.5 * z), falloff);
|
||||||
alpha *= mix(1.0, exp(-0.5 * z), falloff);
|
|
||||||
if (alpha < MIN_ALPHA) {
|
if (rgba.a < MIN_ALPHA) {
|
||||||
discard;
|
discard;
|
||||||
}
|
}
|
||||||
|
|
||||||
vec3 rgb = vRgba.rgb;
|
|
||||||
if (encodeLinear) {
|
if (encodeLinear) {
|
||||||
rgb = srgbToLinear(rgb);
|
rgba.rgb = srgbToLinear(rgba.rgb);
|
||||||
}
|
}
|
||||||
fragColor = vec4(rgb, alpha);
|
fragColor = rgba;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ uniform float preBlurAmount;
|
|||||||
uniform float focalDistance;
|
uniform float focalDistance;
|
||||||
uniform float apertureAngle;
|
uniform float apertureAngle;
|
||||||
uniform float clipXY;
|
uniform float clipXY;
|
||||||
|
uniform float renderScale;
|
||||||
|
|
||||||
uniform usampler2DArray packedSplats;
|
uniform usampler2DArray packedSplats;
|
||||||
|
|
||||||
@@ -111,7 +112,8 @@ void main() {
|
|||||||
mat3 cov3D = RS * transpose(RS);
|
mat3 cov3D = RS * transpose(RS);
|
||||||
|
|
||||||
// Compute the Jacobian of the splat's projection at its center
|
// Compute the Jacobian of the splat's projection at its center
|
||||||
vec2 focal = 0.5 * renderSize * vec2(projectionMatrix[0][0], projectionMatrix[1][1]);
|
vec2 scaledRenderSize = renderSize * renderScale;
|
||||||
|
vec2 focal = 0.5 * scaledRenderSize * vec2(projectionMatrix[0][0], projectionMatrix[1][1]);
|
||||||
float invZ = 1.0 / viewCenter.z;
|
float invZ = 1.0 / viewCenter.z;
|
||||||
vec2 J1 = focal * invZ;
|
vec2 J1 = focal * invZ;
|
||||||
vec2 J2 = -(J1 * viewCenter.xy) * invZ;
|
vec2 J2 = -(J1 * viewCenter.xy) * invZ;
|
||||||
@@ -175,7 +177,7 @@ void main() {
|
|||||||
|
|
||||||
// Compute the NDC coordinates for the ellipsoid's diagonal axes.
|
// Compute the NDC coordinates for the ellipsoid's diagonal axes.
|
||||||
vec2 pixelOffset = eigenVec1 * scale1 + eigenVec2 * scale2;
|
vec2 pixelOffset = eigenVec1 * scale1 + eigenVec2 * scale2;
|
||||||
vec2 ndcOffset = (2.0 / renderSize) * pixelOffset;
|
vec2 ndcOffset = (2.0 / scaledRenderSize) * pixelOffset;
|
||||||
vec3 ndc = vec3(ndcCenter.xy + ndcOffset, ndcCenter.z);
|
vec3 ndc = vec3(ndcCenter.xy + ndcOffset, ndcCenter.z);
|
||||||
|
|
||||||
vRgba = rgba;
|
vRgba = rgba;
|
||||||
|
|||||||
Reference in New Issue
Block a user