• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

visgl / loaders.gl / 25168965430

30 Apr 2026 01:45PM UTC coverage: 59.672% (+0.2%) from 59.466%
25168965430

push

github

web-flow
[codex] Add Gaussian splat PLY parsing (#3408)

12084 of 22345 branches covered (54.08%)

Branch coverage included in aggregate %.

1200 of 1566 new or added lines in 22 files covered. (76.63%)

45 existing lines in 2 files now uncovered.

25248 of 40217 relevant lines covered (62.78%)

15337.06 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

19.75
/modules/deck-layers/src/splat-layer.ts
1
// loaders.gl
2
// SPDX-License-Identifier: MIT
3
// Copyright (c) vis.gl contributors
4

5
import type * as arrow from 'apache-arrow';
6
import {
7
  color,
8
  CompositeLayer,
9
  Layer,
10
  type Attribute,
11
  type LayerContext,
12
  picking,
13
  project32,
14
  UNIT,
15
  type CompositeLayerProps,
16
  type DefaultProps,
17
  type LayerProps,
18
  type LayerDataSource,
19
  type UpdateParameters,
20
  type Unit,
21
  type Color
22
} from '@deck.gl/core';
23
import type {BufferLayout} from '@luma.gl/core';
24
import {Geometry, Model} from '@luma.gl/engine';
25
import type {ShaderModule} from '@luma.gl/shadertools';
26
import type {MeshArrowTable, TypedArray} from '@loaders.gl/schema';
27
import {CullingVolume, Plane} from '@math.gl/culling';
28
import {SplatEngine, type SplatSortMode} from './splat/splat-engine';
29
import {getArrowTable, getGaussianSplatDataFromArrowTable} from './splat/splat-data';
30

31
const DEFAULT_COLOR = [255, 255, 255, 255] as const;
7✔
32

33
/** Public rendering modes supported by {@link SplatLayer}. */
34
export type SplatRenderMode = 'auto' | 'cpu' | 'gpu';
35

36
/** Public sorting modes supported by {@link SplatLayer}. */
37
export type PublicSplatSortMode = 'none' | 'global' | 'tile';
38

39
/** Props for {@link SplatLayer}. */
40
export type SplatLayerProps = CompositeLayerProps & {
41
  /** Gaussian splat table produced by `PLYLoader` with `ply.shape: 'arrow-table'`. */
42
  data: MeshArrowTable | arrow.Table;
43
  /** Units used by decoded splat radii. */
44
  sizeUnits?: Unit;
45
  /** Radius multiplier applied after decoding `scale_*` columns. */
46
  radiusScale?: number;
47
  /** Minimum rendered splat radius in pixels. */
48
  radiusMinPixels?: number;
49
  /** Maximum rendered splat radius in pixels. */
50
  radiusMaxPixels?: number;
51
  /** Additional multiplier applied to decoded Gaussian alpha before blending. */
52
  alphaScale?: number;
53
  /** Fallback color used when spherical harmonic DC columns are not present. */
54
  getColor?: Color;
55
  /** Selects CPU/WebGL fallback rendering or the WebGPU engine path. */
56
  renderMode?: SplatRenderMode;
57
  /** Sorting strategy used by the WebGPU engine path. */
58
  sortMode?: PublicSplatSortMode;
59
  /** Minimum normalized alpha retained by the WebGPU engine path. */
60
  alphaCutoff?: number;
61
  /** Minimum projected screen size retained by the WebGPU engine path. */
62
  screenSizeCutoffPixels?: number;
63
  /** Gaussian support radius used when deriving billboard radii and bounds. */
64
  gaussianSupportRadius?: number;
65
  /** Additional two-dimensional screen-space Gaussian kernel radius in pixels. */
66
  kernel2DSize?: number;
67
  /** Maximum one-sigma screen-space splat size in pixels before support scaling. */
68
  maxScreenSpaceSplatSize?: number;
69
};
70

71
type SplatPrimitiveLayerProps = LayerProps & {
72
  data: LayerDataSource<unknown>;
73
  sizeUnits?: Unit;
74
  radiusScale?: number;
75
  radiusMinPixels?: number;
76
  radiusMaxPixels?: number;
77
  alphaScale?: number;
78
  screenSizeCutoffPixels?: number;
79
  gaussianSupportRadius?: number;
80
  kernel2DSize?: number;
81
  maxScreenSpaceSplatSize?: number;
82
  splatEngine?: SplatEngine | null;
83
};
84

85
type SplatUniformProps = {
86
  sizeUnits: number;
87
  radiusScale: number;
88
  radiusMinPixels: number;
89
  radiusMaxPixels: number;
90
  alphaScale: number;
91
  screenSizeCutoffPixels: number;
92
  gaussianSupportRadius: number;
93
};
94

95
type DeckBinaryData = {
96
  length: number;
97
  attributes: Record<string, {value: TypedArray; size: number; type?: string}>;
98
};
99

100
type DrawOptions = {
101
  /** Shader module props supplied by deck.gl for this draw pass. */
102
  shaderModuleProps?: {
103
    /** Picking module uniforms for picking framebuffer passes. */
104
    picking?: {
105
      /** Whether this draw is writing to a picking framebuffer. */
106
      isActive?: boolean;
107
    };
108
  };
109
};
110

111
const defaultProps: DefaultProps<SplatLayerProps> = {
7✔
112
  id: 'splat-layer',
113
  sizeUnits: 'meters',
114
  radiusScale: {type: 'number', min: 0, value: 1},
115
  radiusMinPixels: {type: 'number', min: 0, value: 0},
116
  radiusMaxPixels: {type: 'number', min: 0, value: Number.MAX_SAFE_INTEGER},
117
  alphaScale: {type: 'number', min: 0, value: 1},
118
  getColor: {type: 'color', value: DEFAULT_COLOR},
119
  renderMode: 'auto',
120
  sortMode: 'global',
121
  alphaCutoff: {type: 'number', min: 0, max: 1, value: 1 / 255},
122
  screenSizeCutoffPixels: {type: 'number', min: 0, value: 0},
123
  gaussianSupportRadius: {type: 'number', min: 0, value: 3},
124
  kernel2DSize: {type: 'number', min: 0, value: 0.3},
125
  maxScreenSpaceSplatSize: {type: 'number', min: 1, value: 1024}
126
};
127

128
const splatUniforms = {
7✔
129
  name: 'splat',
130
  source: '',
131
  vs: /* glsl */ `\
132
layout(std140) uniform splatUniforms {
133
  highp int sizeUnits;
134
  float radiusScale;
135
  float radiusMinPixels;
136
  float radiusMaxPixels;
137
  float alphaScale;
138
  float screenSizeCutoffPixels;
139
  float gaussianSupportRadius;
140
} splat;
141
`,
142
  fs: '',
143
  uniformTypes: {
144
    sizeUnits: 'i32',
145
    radiusScale: 'f32',
146
    radiusMinPixels: 'f32',
147
    radiusMaxPixels: 'f32',
148
    alphaScale: 'f32',
149
    screenSizeCutoffPixels: 'f32',
150
    gaussianSupportRadius: 'f32'
151
  }
152
} as const satisfies ShaderModule<SplatUniformProps>;
153

154
const source = /* wgsl */ `\
7✔
155
struct SplatUniforms {
156
  sizeUnits: i32,
157
  radiusScale: f32,
158
  radiusMinPixels: f32,
159
  radiusMaxPixels: f32,
160
  alphaScale: f32,
161
  screenSizeCutoffPixels: f32,
162
  gaussianSupportRadius: f32,
163
};
164

165
@group(0) @binding(auto)
166
var<uniform> splat: SplatUniforms;
167

168
@group(0) @binding(auto) var<storage, read> splatPositions: array<f32>;
169
@group(0) @binding(auto) var<storage, read> splatColors: array<u32>;
170
@group(0) @binding(auto) var<storage, read> splatIndices: array<u32>;
171
@group(0) @binding(auto) var<storage, read> splatProjected: array<vec4<f32>>;
172

173
struct FragmentInputs {
174
  @builtin(position) position: vec4<f32>,
175
  @location(0) gaussianCoord: vec2<f32>,
176
  @location(1) color: vec4<f32>,
177
};
178

179
@vertex
180
fn vertexMain(
181
  @builtin(vertex_index) vertexIndex: u32,
182
  @builtin(instance_index) instanceIndex: u32
183
) -> FragmentInputs {
184
  let corner = array<vec2<f32>, 4>(
185
    vec2<f32>(-1.0, -1.0),
186
    vec2<f32>(1.0, -1.0),
187
    vec2<f32>(-1.0, 1.0),
188
    vec2<f32>(1.0, 1.0)
189
  )[vertexIndex];
190
  let splatIndex = splatIndices[instanceIndex];
191
  let positionIndex = splatIndex * 3u;
192
  let projectedBase = splatIndex * 2u;
193
  let projectedAxes = splatProjected[projectedBase];
194
  let projectedMetadata = splatProjected[projectedBase + 1u];
195
  let splatPosition = vec3<f32>(
196
    splatPositions[positionIndex],
197
    splatPositions[positionIndex + 1u],
198
    splatPositions[positionIndex + 2u]
199
  );
200
  let supportScale = splat.gaussianSupportRadius * splat.radiusScale;
201
  let rawAxis0 = projectedAxes.xy * supportScale;
202
  let rawAxis1 = projectedAxes.zw * supportScale;
203
  let rawMaxAxisPixels = max(length(rawAxis0), length(rawAxis1));
204
  let clampedMaxAxisPixels = min(
205
    max(rawMaxAxisPixels, splat.radiusMinPixels),
206
    splat.radiusMaxPixels
207
  );
208
  let axisClampScale = clampedMaxAxisPixels / max(rawMaxAxisPixels, 0.000001);
209
  let axis0 = rawAxis0 * axisClampScale;
210
  let axis1 = rawAxis1 * axisClampScale;
211
  let sizeVisibility = select(
212
    0.0,
213
    1.0,
214
    rawMaxAxisPixels >= splat.screenSizeCutoffPixels
215
  );
216
  let visibleAlpha = projectedMetadata.x * layer.opacity * splat.alphaScale * projectedMetadata.y * sizeVisibility;
217
  let packedColor = splatColors[splatIndex];
218
  let color = vec4<f32>(
219
    f32(packedColor & 255u) / 255.0,
220
    f32((packedColor >> 8u) & 255u) / 255.0,
221
    f32((packedColor >> 16u) & 255u) / 255.0,
222
    visibleAlpha
223
  );
224
  var outputs: FragmentInputs;
225
  geometry.worldPosition = splatPosition;
226
  let gaussianCoord = corner * splat.gaussianSupportRadius;
227
  geometry.uv = gaussianCoord;
228

229
  var clipPosition = project_position_to_clipspace(
230
    splatPosition,
231
    vec3<f32>(0.0, 0.0, 0.0),
232
    vec3<f32>(0.0, 0.0, 0.0)
233
  );
234
  let pixelOffset = corner.x * axis0 + corner.y * axis1;
235
  clipPosition.xy += project_pixel_size_to_clipspace(pixelOffset);
236

237
  outputs.position = clipPosition;
238
  outputs.gaussianCoord = gaussianCoord;
239
  outputs.color = color;
240
  return outputs;
241
}
242

243
@fragment
244
fn fragmentMain(inputs: FragmentInputs) -> @location(0) vec4<f32> {
245
  let radiusSquared = dot(inputs.gaussianCoord, inputs.gaussianCoord);
246
  if (radiusSquared > splat.gaussianSupportRadius * splat.gaussianSupportRadius) {
247
    discard;
248
  }
249

250
  let gaussianAlpha = exp(-0.5 * radiusSquared);
251
  let color = vec4<f32>(inputs.color.rgb, min(inputs.color.a * gaussianAlpha, 0.18));
252
  if (color.a <= 0.00392156862) {
253
    discard;
254
  }
255

256
  return color;
257
}
258
`;
259

260
const vs = /* glsl */ `\
7✔
261
#version 300 es
262
#define SHADER_NAME splat-layer-vertex-shader
263

264
in vec3 positions;
265
in vec3 instancePositions;
266
in vec3 instancePositions64Low;
267
in float instanceRadii;
268
in vec4 instanceColors;
269
in vec3 instancePickingColors;
270

271
out vec2 unitPosition;
272
out vec4 vColor;
273

274
void main(void) {
275
  geometry.worldPosition = instancePositions;
276
  geometry.uv = positions.xy;
277
  geometry.pickingColor = instancePickingColors;
278
  unitPosition = positions.xy;
279

280
  float radiusPixels = clamp(
281
    project_size_to_pixel(instanceRadii * splat.radiusScale, splat.sizeUnits),
282
    splat.radiusMinPixels,
283
    splat.radiusMaxPixels
284
  );
285

286
  gl_Position = project_position_to_clipspace(
287
    instancePositions,
288
    instancePositions64Low,
289
    vec3(0.0),
290
    geometry.position
291
  );
292
  DECKGL_FILTER_GL_POSITION(gl_Position, geometry);
293

294
  vec3 offset = vec3(positions.xy * radiusPixels, 0.0);
295
  DECKGL_FILTER_SIZE(offset, geometry);
296
  gl_Position.xy += project_pixel_size_to_clipspace(offset.xy);
297

298
  vColor = vec4(instanceColors.rgb, instanceColors.a * layer.opacity * splat.alphaScale);
299
  DECKGL_FILTER_COLOR(vColor, geometry);
300
}
301
`;
302

303
const fs = /* glsl */ `\
7✔
304
#version 300 es
305
#define SHADER_NAME splat-layer-fragment-shader
306

307
precision highp float;
308

309
in vec2 unitPosition;
310
in vec4 vColor;
311

312
out vec4 fragColor;
313

314
void main(void) {
315
  geometry.uv = unitPosition;
316
  float radiusSquared = dot(unitPosition, unitPosition);
317
  if (radiusSquared > 1.0) {
318
    discard;
319
  }
320

321
  float gaussianAlpha = exp(-6.0 * radiusSquared);
322
  fragColor = vec4(vColor.rgb, vColor.a * gaussianAlpha);
323
  if (fragColor.a <= 0.00392156862) {
324
    discard;
325
  }
326

327
  DECKGL_FILTER_COLOR(fragColor, geometry);
328
}
329
`;
330

331
/** WebGPU vertex buffer layout for the storage-buffer driven render path. */
332
const WEBGPU_SPLAT_BUFFER_LAYOUT: BufferLayout[] = [];
7✔
333

334
/**
335
 * Renders GraphDECO-style Gaussian splat PLY data parsed as an Arrow table.
336
 *
337
 * The layer expects `POSITION`, `scale_0..2`, `opacity`, and `f_dc_0..2` columns.
338
 * `scale_*` and `opacity` encodings are read from `loaders_gl.gaussian_splats.*`
339
 * field metadata when available.
340
 */
341
export class SplatLayer extends CompositeLayer<SplatLayerProps> {
342
  /** deck.gl layer name used in debugging output. */
343
  static layerName = 'SplatLayer';
7✔
344

345
  /** Default props shared across splat layers. */
346
  static defaultProps: DefaultProps = defaultProps;
7✔
347

348
  declare state: {
349
    /** WebGPU engine used when the GPU path is selected. */
350
    splatEngine?: SplatEngine;
351
    /** Last Arrow table uploaded to the WebGPU engine. */
352
    engineTable?: arrow.Table;
353
    /** Last fallback color uploaded to the WebGPU engine. */
354
    engineFallbackColor?: Color;
355
  };
356

357
  /** Updates the optional WebGPU engine when layer props change. */
358
  updateState(params: UpdateParameters<this>): void {
NEW
359
    super.updateState(params);
×
360

NEW
361
    const useGpuEngine = this.shouldUseGpuEngine();
×
NEW
362
    if (!useGpuEngine) {
×
NEW
363
      this.destroySplatEngine();
×
NEW
364
      return;
×
365
    }
366

NEW
367
    const arrowTable = getArrowTable(this.props.data);
×
NEW
368
    const fallbackColor = this.props.getColor || DEFAULT_COLOR;
×
NEW
369
    let splatEngine = this.state.splatEngine;
×
NEW
370
    if (!splatEngine) {
×
NEW
371
      splatEngine = new SplatEngine(this.context.device, this.getSplatEngineProps());
×
NEW
372
      this.setState({splatEngine});
×
373
    }
374

NEW
375
    splatEngine.setProps(this.getSplatEngineProps());
×
376

NEW
377
    if (
×
378
      params.changeFlags.dataChanged ||
×
379
      this.state.engineTable !== arrowTable ||
380
      this.state.engineFallbackColor !== fallbackColor ||
381
      params.changeFlags.propsChanged
382
    ) {
NEW
383
      splatEngine.setData(arrowTable, fallbackColor);
×
NEW
384
      this.setState({engineTable: arrowTable, engineFallbackColor: fallbackColor});
×
385
    }
386
  }
387

388
  /** Releases the WebGPU engine. */
389
  finalizeState(context: LayerContext): void {
NEW
390
    super.finalizeState(context);
×
NEW
391
    this.destroySplatEngine();
×
392
  }
393

394
  /** Renders the Arrow table through a Gaussian billboard primitive. */
395
  renderLayers(): Layer | null {
396
    const useGpuEngine = this.shouldUseGpuEngine();
1✔
397
    const arrowTable = getArrowTable(this.props.data);
1✔
398
    const splatData = useGpuEngine
1!
399
      ? {length: this.state.splatEngine?.getSplatCount() ?? arrowTable.numRows, attributes: {}}
×
400
      : getDeckBinaryDataFromGaussianSplatArrowTable(
401
          arrowTable,
402
          this.props.getColor,
403
          this.props.gaussianSupportRadius
404
        );
405

406
    return new SplatPrimitiveLayer({
1✔
407
      ...this.getSubLayerProps({id: 'splats'}),
408
      data: splatData,
409
      sizeUnits: this.props.sizeUnits,
410
      radiusScale: this.props.radiusScale,
411
      radiusMinPixels: this.props.radiusMinPixels,
412
      radiusMaxPixels: this.props.radiusMaxPixels,
413
      alphaScale: this.props.alphaScale,
414
      screenSizeCutoffPixels: this.props.screenSizeCutoffPixels,
415
      gaussianSupportRadius: this.props.gaussianSupportRadius,
416
      kernel2DSize: this.props.kernel2DSize,
417
      maxScreenSpaceSplatSize: this.props.maxScreenSpaceSplatSize,
418
      splatEngine: useGpuEngine ? this.state.splatEngine : null
1!
419
    }) as unknown as Layer;
420
  }
421

422
  private shouldUseGpuEngine(): boolean {
423
    const renderMode = this.props.renderMode || 'auto';
1!
424
    const device = this.context?.device;
1✔
425
    if (renderMode === 'cpu') {
1!
NEW
426
      return false;
×
427
    }
428
    if (device?.type === 'webgpu') {
1!
NEW
429
      return true;
×
430
    }
431
    if (renderMode === 'gpu') {
1!
NEW
432
      throw new Error('SplatLayer renderMode "gpu" requires a WebGPU device.');
×
433
    }
434
    return false;
1✔
435
  }
436

437
  private getSplatEngineProps() {
NEW
438
    return {
×
439
      sortMode: (this.props.sortMode || 'global') as SplatSortMode,
×
440
      alphaCutoff: this.props.alphaCutoff ?? 1 / 255,
×
441
      screenSizeCutoffPixels: this.props.screenSizeCutoffPixels ?? 0,
×
442
      gaussianSupportRadius: this.props.gaussianSupportRadius ?? 3,
×
443
      kernel2DSize: this.props.kernel2DSize ?? 0.3,
×
444
      maxScreenSpaceSplatSize: this.props.maxScreenSpaceSplatSize ?? 1024
×
445
    };
446
  }
447

448
  private destroySplatEngine(): void {
NEW
449
    this.state.splatEngine?.destroy();
×
NEW
450
    this.setState({splatEngine: undefined, engineTable: undefined, engineFallbackColor: undefined});
×
451
  }
452
}
453

454
/** Primitive Gaussian billboard layer used by {@link SplatLayer}. */
455
class SplatPrimitiveLayer extends Layer<Required<SplatPrimitiveLayerProps>> {
456
  /** deck.gl layer name used in debugging output. */
457
  static layerName = 'SplatPrimitiveLayer';
7✔
458

459
  /** Default props shared across primitive splat layers. */
460
  static defaultProps: DefaultProps = {
7✔
461
    sizeUnits: 'meters',
462
    radiusScale: {type: 'number', min: 0, value: 1},
463
    radiusMinPixels: {type: 'number', min: 0, value: 0},
464
    radiusMaxPixels: {type: 'number', min: 0, value: Number.MAX_SAFE_INTEGER},
465
    alphaScale: {type: 'number', min: 0, value: 1},
466
    screenSizeCutoffPixels: {type: 'number', min: 0, value: 0},
467
    gaussianSupportRadius: {type: 'number', min: 0, value: 3},
468
    kernel2DSize: {type: 'number', min: 0, value: 0.3},
469
    maxScreenSpaceSplatSize: {type: 'number', min: 1, value: 1024},
470
    splatEngine: null
471
  };
472

473
  state: {
474
    model?: Model;
475
  } = {};
1✔
476

477
  /** Returns splat shaders. */
478
  getShaders() {
NEW
479
    if (this.context.device.type === 'webgpu') {
×
NEW
480
      return super.getShaders({
×
481
        source,
482
        modules: [project32, splatUniforms]
483
      });
484
    }
485

NEW
486
    return super.getShaders({
×
487
      vs,
488
      fs,
489
      modules: [project32, color, picking, splatUniforms]
490
    });
491
  }
492

493
  /** Registers binary attributes consumed by the primitive shader. */
494
  initializeState(): void {
NEW
495
    if (this.context.device.type === 'webgpu') {
×
NEW
496
      return;
×
497
    }
498

NEW
499
    this.getAttributeManager()!.addInstanced({
×
500
      instancePositions: {
501
        size: 3,
502
        type: 'float64',
503
        fp64: this.use64bitPositions(),
504
        accessor: 'getPosition'
505
      },
506
      instanceRadii: {
507
        size: 1,
508
        accessor: 'getRadius',
509
        defaultValue: 1
510
      },
511
      instanceColors: {
512
        size: this.props.colorFormat.length,
513
        type: 'unorm8',
514
        accessor: 'getColor',
515
        defaultValue: DEFAULT_COLOR
516
      }
517
    });
518
  }
519

520
  /** Rebuilds the luma model when deck.gl shader extensions change. */
521
  updateState(params: UpdateParameters<this>): void {
NEW
522
    super.updateState(params);
×
523

NEW
524
    if (!this.state.model || params.changeFlags.extensionsChanged) {
×
NEW
525
      this.state.model?.destroy();
×
NEW
526
      this.state.model = this._getModel();
×
NEW
527
      this.getAttributeManager()!.invalidateAll();
×
528
    }
529
  }
530

531
  /** Draws all splat billboards. */
532
  draw(options: DrawOptions = {}): void {
×
NEW
533
    if (this.context.device.type === 'webgpu' && options.shaderModuleProps?.picking?.isActive) {
×
NEW
534
      return;
×
535
    }
536

537
    const {
538
      sizeUnits,
539
      radiusScale,
540
      radiusMinPixels,
541
      radiusMaxPixels,
542
      alphaScale,
543
      screenSizeCutoffPixels,
544
      gaussianSupportRadius
NEW
545
    } = this.props;
×
NEW
546
    const splatProps: SplatUniformProps = {
×
547
      sizeUnits: UNIT[sizeUnits],
548
      radiusScale,
549
      radiusMinPixels,
550
      radiusMaxPixels,
551
      alphaScale,
552
      screenSizeCutoffPixels,
553
      gaussianSupportRadius
554
    };
NEW
555
    const model = this.state.model;
×
NEW
556
    if (!model) {
×
NEW
557
      return;
×
558
    }
NEW
559
    this.props.splatEngine?.update(
×
560
      getSplatEngineUpdateProps(this.context.viewport, this.props.radiusScale)
561
    );
NEW
562
    if (this.context.device.type === 'webgpu') {
×
NEW
563
      const splatEngine = this.props.splatEngine;
×
NEW
564
      if (!splatEngine) {
×
NEW
565
        return;
×
566
      }
NEW
567
      model.setBindings(splatEngine.getRenderBindings());
×
NEW
568
      model.setInstanceCount(splatEngine.getRenderSplatCount());
×
NEW
569
      model.setVertexCount(4);
×
570
    }
NEW
571
    model.shaderInputs.setProps({splat: splatProps});
×
NEW
572
    model.draw(this.context.renderPass);
×
573
  }
574

575
  /** Applies attribute buffers while preserving the explicit WebGPU buffer layout. */
576
  protected _setModelAttributes(
577
    model: Model,
578
    changedAttributes: {[id: string]: Attribute},
579
    bufferLayoutChanged = false
×
580
  ): void {
NEW
581
    super._setModelAttributes(
×
582
      model,
583
      changedAttributes,
584
      this.context.device.type === 'webgpu' ? false : bufferLayoutChanged
×
585
    );
586
  }
587

588
  /** Builds the instanced billboard model. */
589
  protected _getModel(): Model {
590
    const bufferLayout =
NEW
591
      this.context.device.type === 'webgpu'
×
592
        ? WEBGPU_SPLAT_BUFFER_LAYOUT
593
        : this.getAttributeManager()!.getBufferLayouts();
594

NEW
595
    return new Model(this.context.device, {
×
596
      ...this.getShaders(),
597
      id: this.props.id,
598
      bufferLayout,
599
      geometry:
600
        this.context.device.type === 'webgpu'
×
601
          ? null
602
          : new Geometry({
603
              topology: 'triangle-strip',
604
              attributes: {
605
                positions: {
606
                  size: 3,
607
                  value: new Float32Array([-1, -1, 0, 1, -1, 0, -1, 1, 0, 1, 1, 0])
608
                }
609
              }
610
            }),
611
      topology: 'triangle-strip',
612
      vertexCount: 4,
613
      instanceCount: this.props.splatEngine?.getRenderSplatCount() ?? 0,
×
614
      isInstanced: true
615
    });
616
  }
617
}
618

619
/** Build draw-time engine inputs from the active deck.gl viewport. */
620
function getSplatEngineUpdateProps(viewport: any, radiusScale: number) {
NEW
621
  if (!viewport) {
×
NEW
622
    return {radiusScale};
×
623
  }
624

NEW
625
  return {
×
626
    modelViewProjectionMatrix: viewport.viewProjectionMatrix,
627
    viewportSize: [viewport.width || 1, viewport.height || 1] as [number, number],
×
628
    cullingVolume: getCullingVolume(viewport),
629
    radiusScale
630
  };
631
}
632

633
/** Build a math.gl frustum culling volume from a deck.gl viewport. */
634
function getCullingVolume(viewport: any): CullingVolume | undefined {
NEW
635
  if (typeof viewport.getFrustumPlanes !== 'function') {
×
NEW
636
    return undefined;
×
637
  }
638

NEW
639
  const planes = Object.values(viewport.getFrustumPlanes()).map(
×
NEW
640
    ({normal, distance}: any) => new Plane(normal.clone().negate(), distance)
×
641
  );
NEW
642
  return new CullingVolume(planes);
×
643
}
644

645
/** Convert a Gaussian splat Arrow table into deck.gl binary attributes. */
646
function getDeckBinaryDataFromGaussianSplatArrowTable(
647
  table: arrow.Table,
648
  fallbackColor: Color = DEFAULT_COLOR,
1✔
649
  gaussianSupportRadius: number = 3
1✔
650
): DeckBinaryData {
651
  const splatData = getGaussianSplatDataFromArrowTable(table, fallbackColor, gaussianSupportRadius);
1✔
652

653
  return {
1✔
654
    length: splatData.length,
655
    attributes: {
656
      getPosition: {value: splatData.positions, size: 3},
657
      getRadius: {value: splatData.radii, size: 1},
658
      getColor: {value: splatData.colors, size: 4, type: 'unorm8'}
659
    }
660
  };
661
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc