gaussian-splat-lite 1.4.1 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3895,19 +3895,22 @@ function hasViewDependentColor(mapping) {
3895
3895
  }
3896
3896
  const SPLATS_PER_INSTANCE = 128;
3897
3897
  class SplatGeometry extends THREE.InstancedBufferGeometry {
3898
- constructor() {
3898
+ constructor(triangle = false) {
3899
3899
  super();
3900
- const vertices = new Float32Array(SPLATS_PER_INSTANCE * 12);
3901
- const indices = new Uint16Array(SPLATS_PER_INSTANCE * 6);
3900
+ const corners = triangle ? TRIANGLE_CORNERS : QUAD_CORNERS;
3901
+ const order = triangle ? TRIANGLE_INDICES : QUAD_INDICES;
3902
+ const vertexCount = corners.length / 2;
3903
+ const vertices = new Float32Array(SPLATS_PER_INSTANCE * vertexCount * 3);
3904
+ const indices = new Uint16Array(SPLATS_PER_INSTANCE * order.length);
3902
3905
  for (let splat = 0; splat < SPLATS_PER_INSTANCE; splat++) {
3903
- for (let vertex = 0; vertex < 4; vertex++) {
3904
- const offset = splat * 12 + vertex * 3;
3905
- vertices[offset] = QUAD_VERTICES[vertex * 3];
3906
- vertices[offset + 1] = QUAD_VERTICES[vertex * 3 + 1];
3906
+ for (let vertex = 0; vertex < vertexCount; vertex++) {
3907
+ const offset = (splat * vertexCount + vertex) * 3;
3908
+ vertices[offset] = corners[vertex * 2];
3909
+ vertices[offset + 1] = corners[vertex * 2 + 1];
3907
3910
  vertices[offset + 2] = splat;
3908
3911
  }
3909
- for (let index2 = 0; index2 < 6; index2++)
3910
- indices[splat * 6 + index2] = splat * 4 + QUAD_INDICES[index2];
3912
+ for (let index2 = 0; index2 < order.length; index2++)
3913
+ indices[splat * order.length + index2] = splat * vertexCount + order[index2];
3911
3914
  }
3912
3915
  this.setAttribute("position", new THREE.BufferAttribute(vertices, 3));
3913
3916
  this.setIndex(new THREE.BufferAttribute(indices, 1));
@@ -3916,21 +3919,17 @@ class SplatGeometry extends THREE.InstancedBufferGeometry {
3916
3919
  this.instanceCount = Math.ceil(count / SPLATS_PER_INSTANCE);
3917
3920
  }
3918
3921
  }
3919
- const QUAD_VERTICES = new Float32Array([
3922
+ const QUAD_CORNERS = new Float32Array([-1, -1, 1, -1, 1, 1, -1, 1]);
3923
+ const TRIANGLE_CORNERS = new Float32Array([
3924
+ -Math.sqrt(3),
3920
3925
  -1,
3926
+ Math.sqrt(3),
3921
3927
  -1,
3922
3928
  0,
3923
- 1,
3924
- -1,
3925
- 0,
3926
- 1,
3927
- 1,
3928
- 0,
3929
- -1,
3930
- 1,
3931
- 0
3929
+ 2
3932
3930
  ]);
3933
3931
  const QUAD_INDICES = new Uint16Array([0, 2, 1, 0, 3, 2]);
3932
+ const TRIANGLE_INDICES = new Uint16Array([0, 2, 1]);
3934
3933
  const variants = /* @__PURE__ */ new WeakMap();
3935
3934
  function registerStochasticSplatMaterial(material, createVariant) {
3936
3935
  variants.set(material, createVariant);
@@ -5601,10 +5600,7 @@ class ProjectedSplats {
5601
5600
  constructor(renderer, uniforms) {
5602
5601
  this.renderer = renderer;
5603
5602
  this.uniforms = uniforms;
5604
- this.indirect = new IndirectStorageBufferAttribute(
5605
- new Uint32Array([SPLATS_PER_INSTANCE * 6, 0, 0, 0, 0]),
5606
- 1
5607
- );
5603
+ this.indirect = new IndirectStorageBufferAttribute(new Uint32Array(5), 1);
5608
5604
  this.error = null;
5609
5605
  this.cache = new ProjectionCache();
5610
5606
  this.visibleCount = new StorageBufferAttribute(new Uint32Array(1), 1);
@@ -5644,7 +5640,9 @@ class ProjectedSplats {
5644
5640
  // RADIX_SORT_MODE_IDS value of the current key encoding.
5645
5641
  sortMode: { value: 0 },
5646
5642
  // NDC translation the draw applies to unjittered projections.
5647
- projectionJitter: { value: new THREE.Vector2() }
5643
+ projectionJitter: { value: new THREE.Vector2() },
5644
+ // Indices per instance of the drawn geometry, quads or triangles.
5645
+ indexCount: { value: 0 }
5648
5646
  };
5649
5647
  const count = N.storage(this.visibleCount, "uint").setName("gslVisibleCount").toReadOnly().element(0);
5650
5648
  this.sorter = new WebGPURadixSort(1, this.keys, {
@@ -5656,13 +5654,16 @@ class ProjectedSplats {
5656
5654
  this.limits.maxBufferSize
5657
5655
  )
5658
5656
  });
5659
- const drawCount = N.storage(this.indirect, "uint").setName("gslDrawIndirect").element(1);
5657
+ const drawArgs = N.storage(this.indirect, "uint").setName(
5658
+ "gslDrawIndirect"
5659
+ );
5660
5660
  const counter = N.storage(this.visibleCount, "uint").setName("gslVisibleCount").toAtomic();
5661
5661
  this.resetCount = N.Fn(() => {
5662
5662
  N.atomicStore(counter.element(0), N.uint(0));
5663
5663
  })().compute(1, [1]).setName("Splat reset visible count");
5664
5664
  this.finish = N.Fn(() => {
5665
- drawCount.assign(
5665
+ drawArgs.element(0).assign(uniformBinding(this.state, "indexCount", "uint"));
5666
+ drawArgs.element(1).assign(
5666
5667
  count.add(SPLATS_PER_INSTANCE - 1).div(N.uint(SPLATS_PER_INSTANCE))
5667
5668
  );
5668
5669
  })().compute(1, [1]).setName("Splat visible draw arguments");
@@ -5914,6 +5915,7 @@ class ProjectedSplats {
5914
5915
  const monoProjection = !multiView && unjittered && getProjectionJitter(camera.projectionMatrix, unjittered, jitter) ? unjittered : null;
5915
5916
  geometry.setIndirect(this.indirect);
5916
5917
  geometry.setSplatCount(Math.max(1, accumulator.numSplats));
5918
+ this.state.indexCount.value = geometry.index.count;
5917
5919
  const inputs = this.pendingInputs;
5918
5920
  let length = 0;
5919
5921
  inputs[length++] = accumulator;
@@ -5922,6 +5924,7 @@ class ProjectedSplats {
5922
5924
  inputs[length++] = accumulator.viewOrigin.y;
5923
5925
  inputs[length++] = accumulator.viewOrigin.z;
5924
5926
  inputs[length++] = cameras.length;
5927
+ inputs[length++] = geometry.index.count;
5925
5928
  inputs[length++] = stochastic;
5926
5929
  inputs[length++] = !stochastic && radial;
5927
5930
  inputs[length++] = sortMode;
@@ -6108,9 +6111,11 @@ class GaussianSplatRenderer extends THREE.Mesh {
6108
6111
  depthTest: options.depthTest ?? true,
6109
6112
  depthWrite: options.depthWrite ?? false
6110
6113
  });
6111
- const geometry = new SplatGeometry();
6114
+ const geometries = [new SplatGeometry(), new SplatGeometry(true)];
6115
+ const geometry = geometries[Number(stochastic)];
6112
6116
  const material = backend.selectMaterial(stochastic);
6113
6117
  super(geometry, material);
6118
+ this.geometries = geometries;
6114
6119
  this.materialState = new MaterialState(material);
6115
6120
  this.frameCallbacks = true;
6116
6121
  this.renderSize = new THREE.Vector2();
@@ -6214,7 +6219,7 @@ class GaussianSplatRenderer extends THREE.Mesh {
6214
6219
  accumulator.dispose();
6215
6220
  }
6216
6221
  this.accumulators.length = 0;
6217
- this.geometry.dispose();
6222
+ for (const geometry of this.geometries) geometry.dispose();
6218
6223
  }
6219
6224
  /** Native WebGPU sorts on the GPU before drawing; WebGL sorts in a worker. */
6220
6225
  get synchronousSort() {
@@ -6266,13 +6271,11 @@ class GaussianSplatRenderer extends THREE.Mesh {
6266
6271
  const active = this.backend.kind === "webgpu" || this.display.numSplats === 0 ? this._stochastic : this.display.hasStochasticSeeds;
6267
6272
  if (active === this.stochasticFrame) return;
6268
6273
  this.stochasticFrame = active;
6269
- this.selectMaterial();
6270
- this.setDirty();
6271
- }
6272
- selectMaterial() {
6273
- const material = this.backend.selectMaterial(this.stochasticFrame);
6274
- this.applyMaterialState(this.stochasticFrame, material);
6274
+ this.geometry = this.geometries[Number(active)];
6275
+ const material = this.backend.selectMaterial(active);
6276
+ this.applyMaterialState(active, material);
6275
6277
  this.material = material;
6278
+ this.setDirty();
6276
6279
  }
6277
6280
  applyMaterialState(stochasticActive, material = this.material) {
6278
6281
  this.materialState.sync(material, this.material);
@@ -7196,74 +7199,6 @@ ${CATMULL_ROM_WEIGHTS}
7196
7199
  fn catmullRom(image: texture_2d<f32>, uv: vec2f) -> vec4f {
7197
7200
  return catmullRomSpan(image, uv).value;
7198
7201
  }
7199
-
7200
- // Lanczos-3 without its corner taps, from nine bilinear taps: the middle pair
7201
- // of each axis is one tap and the other four pixels of an axis one each. A
7202
- // history that is resampled once per frame loses fine detail with every pass,
7203
- // and this kernel loses about half of what Catmull-Rom does. Its negative
7204
- // lobes would ring over such a history, so the result is kept inside what the
7205
- // four pixels around the position span. At a pixel's centre it returns the
7206
- // pixel.
7207
- struct LanczosWeights {
7208
- low: vec3f,
7209
- high: vec3f,
7210
- };
7211
- fn lanczosWeights(t: f32) -> LanczosWeights {
7212
- // Integer-shifted sinc taps share their sine. Angle addition supplies the
7213
- // other factor, reducing twelve sine evaluations per axis to three trig calls.
7214
- let x: f32 = 3.14159265 * t;
7215
- let s: f32 = sin(x / 3.0);
7216
- let c: f32 = 0.866025404 * cos(x / 3.0);
7217
- let scale: f32 = 0.303963551 * sin(x);
7218
- let terms: vec3f = scale * vec3f(c - 0.5 * s, -c - 0.5 * s, s);
7219
- let low: vec3f = vec3f(t + 2.0, t + 1.0, t);
7220
- let high: vec3f = vec3f(t - 1.0, t - 2.0, t - 3.0);
7221
- var w: LanczosWeights;
7222
- w.low = terms / max(low * low, vec3f(1e-20));
7223
- w.high = terms / max(high * high, vec3f(1e-20));
7224
- if (abs(x) < 1e-5) { w.low.z = 1.0; }
7225
- if (abs(3.14159265 * (t - 1.0)) < 1e-5) { w.high.x = 1.0; }
7226
- return w;
7227
- }
7228
- struct LanczosKernel {
7229
- base: vec2f,
7230
- centre: vec2f,
7231
- middle: f32,
7232
- wx: vec4f,
7233
- wy: vec4f,
7234
- total: f32,
7235
- }
7236
- fn lanczosKernel(uv: vec2f) -> LanczosKernel {
7237
- let position: vec2f = uv * P.size - 0.5;
7238
- var k: LanczosKernel;
7239
- k.base = floor(position);
7240
- let t: vec2f = position - k.base;
7241
- let x: LanczosWeights = lanczosWeights(t.x);
7242
- let y: LanczosWeights = lanczosWeights(t.y);
7243
- let middle: vec2f = vec2f(x.low.z + x.high.x, y.low.z + y.high.x);
7244
- k.centre = (k.base + vec2f(x.high.x, y.high.x) / middle + 0.5) * P.invSize;
7245
- k.middle = middle.x * middle.y;
7246
- k.wx = vec4f(x.low.xy, x.high.yz) * middle.y;
7247
- k.wy = vec4f(y.low.xy, y.high.yz) * middle.x;
7248
- k.total = k.middle;
7249
- for (var i: i32 = 0; i < 4; i++) { k.total += k.wx[i] + k.wy[i]; }
7250
- return k;
7251
- }
7252
- fn lanczosCross(image: texture_2d<f32>, k: LanczosKernel) -> vec4f {
7253
- var sum: vec4f = textureSampleLevel(image, linearSampler, k.centre, 0.0) * k.middle;
7254
- const offsets = array<f32, 4>(-1.5, -0.5, 2.5, 3.5);
7255
- for (var i: i32 = 0; i < 4; i++) {
7256
- let along: vec2f = (k.base + offsets[i]) * P.invSize;
7257
- sum += textureSampleLevel(image, linearSampler, vec2f(along.x, k.centre.y), 0.0) * k.wx[i];
7258
- sum += textureSampleLevel(image, linearSampler, vec2f(k.centre.x, along.y), 0.0) * k.wy[i];
7259
- }
7260
- let p: vec2i = vec2i(k.base);
7261
- let a: vec4f = textureLoad(image, bounded(p), 0);
7262
- let b: vec4f = textureLoad(image, bounded(p + vec2i(1, 0)), 0);
7263
- let c: vec4f = textureLoad(image, bounded(p + vec2i(0, 1)), 0);
7264
- let d: vec4f = textureLoad(image, bounded(p + vec2i(1, 1)), 0);
7265
- return clamp(sum / k.total, min(min(a, b), min(c, d)), max(max(a, b), max(c, d)));
7266
- }
7267
7202
  `
7268
7203
  );
7269
7204
  const TAPS = (
@@ -7617,7 +7552,7 @@ fn mean3x3(image: texture_2d<f32>, position: vec2f, offset: vec2f) -> vec4f {
7617
7552
  const QUALITY_RESAMPLING = (
7618
7553
  /* wgsl */
7619
7554
  `
7620
- // Full Catmull-Rom for the denoised history, retaining all four corner taps.
7555
+ // Full Catmull-Rom for Quality histories, retaining all four corner taps.
7621
7556
  fn catmullRomNine(image: texture_2d<f32>, uv: vec2f) -> vec4f {
7622
7557
  let position: vec2f = uv * P.size;
7623
7558
  ${CATMULL_ROM_WEIGHTS}
@@ -7935,7 +7870,7 @@ ${groups.map(
7935
7870
  // half-resolution result.
7936
7871
  @group(0) @binding(11) var wideTexture: texture_2d<f32>;
7937
7872
  @group(0) @binding(12) var depthPrevious: texture_2d<f32>;
7938
- ${MEAN_3X3}${weights.compact ? "" : QUALITY_RESAMPLING}${BLOCKS}
7873
+ ${weights.compact ? MEAN_3X3 : QUALITY_RESAMPLING}${BLOCKS}
7939
7874
  // The frame's existing 4x4 mean, used only for rejected surfaces.
7940
7875
  @group(0) @binding(13) var rejectedTexture: texture_2d<f32>;
7941
7876
  ${COARSE}
@@ -7983,13 +7918,10 @@ ${weights.compact ? "" : " let d: vec4f = textureLoad(filteredTexture, p, 0);"}
7983
7918
  var same: f32 = 1.0;
7984
7919
  let sparse: f32 = blockSparse(p);
7985
7920
  var sparseMotion: f32 = 0.0;
7986
- ${weights.movingMean ? ` // Where the history was, for the accumulated path's mean below.
7987
- var historyUv: vec2f = position.xy * P.invSize;` : ""}
7988
7921
  if (P.historyValid > 0.5) {
7989
7922
  let z: f32 = blockDepth(p);
7990
7923
  let h: History = reproject(position.xy, z);
7991
7924
  valid = h.valid;
7992
- ${weights.movingMean ? " historyUv = h.uv;" : ""}
7993
7925
  // The pixel's own motion, or what a turn of the camera sweeps over half
7994
7926
  // the view, whichever is more: at the centre of a turn the first is
7995
7927
  // small while every surface there shows a new face. An orthographic
@@ -8020,9 +7952,9 @@ ${weights.movingMean ? " historyUv = h.uv;" : ""}
8020
7952
  ${weights.compact ? ` // The accumulated history keeps many frames while the camera moves
8021
7953
  // slowly: read bilinearly, it lost detail with every one of them.
8022
7954
  sr = max(catmullRom(previousAccumulated, h.uv), vec4f(0.0));
8023
- dr = max(textureSampleLevel(previousDenoised, linearSampler, h.uv, 0.0), vec4f(0.0));` : ` sr = max(lanczosCross(previousAccumulated, lanczosKernel(h.uv)), vec4f(0.0));
8024
- // The two short histories take Catmull-Rom: what it blurs over a few
8025
- // frames is noise more than detail there.
7955
+ dr = max(textureSampleLevel(previousDenoised, linearSampler, h.uv, 0.0), vec4f(0.0));` : ` // Full Catmull-Rom avoids the Lanczos cross kernel's frequency gain
7956
+ // when periodic surface detail is reprojected over many frames.
7957
+ sr = max(catmullRomNine(previousAccumulated, h.uv), vec4f(0.0));
8026
7958
  dr = max(catmullRomNine(previousDenoised, h.uv), vec4f(0.0));`}
8027
7959
  a = previousStatistics(h.uv);
8028
7960
  }
@@ -8099,24 +8031,7 @@ ${weights.compact ? ` let source: vec4f = shifted;
8099
8031
  let error: f32 = sqrt(max(y2 - ys * ys, 0.0) / (ns + 1e-3) + 1e-10);
8100
8032
  let ramp: f32 = 1.0 - exp(-ns / ${float(weights.gateFrames)});
8101
8033
  let keep: f32 = ramp * (1.0 - clamp(${float(weights.gamma)} * error, 0.0, 1.0)) * trust.w;
8102
- ${weights.movingMean ? ` // While the camera moves, the accumulated path holds few samples of the
8103
- // new view and its grain shows: it is shown from the 3x3 mean of its
8104
- // history and this frame's filtered sample. What it keeps stays sharp, so
8105
- // a camera that stops settles as before. This display-only smoothing does
8106
- // not track moving objects: with a still camera they continue to use the
8107
- // existing history rejection.
8108
- var shownAccumulated: vec4f = s;
8109
- if (P.historyValid > 0.5 && P.cameraStill < 0.5) {
8110
- let held: vec4f = max(mean3x3(previousAccumulated, historyUv * P.size, vec2f(0.0)), vec4f(0.0));
8111
- let moving: f32 = speed * speed / (speed * speed + ${float(weights.movingKnee * weights.movingKnee)});
8112
- let smoothed: vec4f = (held * ws + filtered) / ds;
8113
- // Only where the two differ by what the mean's noise explains: a larger
8114
- // difference is detail, and stays.
8115
- let differ: f32 = luma(abs(smoothed.rgb - s.rgb));
8116
- let spread: f32 = ${float(weights.movingNoise)} * error * error;
8117
- shownAccumulated = mix(s, smoothed, ${float(weights.movingMean)} * moving * spread / (spread + differ * differ + 1e-10));
8118
- }
8119
- let composite: vec4f = mix(dn, shownAccumulated, keep);` : " let composite: vec4f = mix(dn, s, keep);"}
8034
+ let composite: vec4f = mix(dn, s, keep);
8120
8035
  // Stabilization: a predicted blend with the previous image, which may stay
8121
8036
  // as far from this frame's result as half of what it spans around the
8122
8037
  // pixel, plus a slack. Taking that span from this frame's result instead
@@ -10954,7 +10869,7 @@ const quality = {
10954
10869
  }
10955
10870
  };
10956
10871
  const neuralDenoiseModels = {
10957
- quality: { ...quality, movingMean: 1, movingNoise: 16 },
10872
+ quality,
10958
10873
  performance: { ...quality, compact: true, compactMix: [0.5, 0.75] }
10959
10874
  };
10960
10875
  class StochasticCoverage {