Skip to content

[BREAKING] Update gsplat component material API #7749

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Jun 10, 2025
58 changes: 19 additions & 39 deletions src/framework/components/gsplat/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ import { Component } from '../component.js';
* @import { Entity } from '../../entity.js'
* @import { EventHandle } from '../../../core/event-handle.js'
* @import { GSplatComponentSystem } from './system.js'
* @import { Material } from '../../../scene/materials/material.js'
* @import { SplatMaterialOptions } from '../../../scene/gsplat/gsplat-material.js'
* @import { ShaderMaterial } from '../../../scene/materials/shader-material.js'
*/

/**
Expand Down Expand Up @@ -56,22 +55,22 @@ class GSplatComponent extends Component {
_instance = null;

/**
* @type {BoundingBox|null}
* @type {ShaderMaterial|null}
* @private
*/
_customAabb = null;
_materialStore = null;

/**
* @type {AssetReference}
* @type {BoundingBox|null}
* @private
*/
_assetReference;
_customAabb = null;

/**
* @type {SplatMaterialOptions|null}
* @type {AssetReference}
* @private
*/
_materialOptions = null;
_assetReference;

/**
* @type {EventHandle|null}
Expand Down Expand Up @@ -158,22 +157,16 @@ class GSplatComponent extends Component {

this._instance = value;

if (this._instance?.meshInstance) {
if (this._instance) {

// if mesh instance was created without a node, assign it here
const mi = this._instance.meshInstance;
if (!mi.node) {
mi.node = this.entity;
}

mi.castShadow = this._castShadows;
mi.setCustomAabb(this._customAabb);

// if we have custom shader options, apply them
if (this._materialOptions) {
this._instance.createMaterial(this._materialOptions);
}

if (this.enabled && this.entity.enabled) {
this.addToLayers();
}
Expand All @@ -190,26 +183,24 @@ class GSplatComponent extends Component {
return this._instance;
}

set materialOptions(value) {
this._materialOptions = Object.assign({}, value);

// apply them on the instance if it exists
/**
* @param {ShaderMaterial} value - The material instance.
*/
set material(value) {
if (this._instance) {
this._instance.createMaterial(this._materialOptions);
this._instance.material = value;
} else {
this._materialStore = value;
}
}

get materialOptions() {
return this._materialOptions;
}

/**
* Gets the material used to render the gsplat.
*
* @type {Material|undefined}
* @type {ShaderMaterial|null}
*/
get material() {
return this._instance?.material;
return this._instance?.material ?? this._materialStore ?? null;
}

/**
Expand Down Expand Up @@ -326,18 +317,6 @@ class GSplatComponent extends Component {
return this._assetReference.id;
}

/**
* Assign asset id to the component, without updating the component with the new asset.
* This can be used to assign the asset id to already fully created component.
*
* @param {Asset|number} asset - The gsplat asset or asset id to assign.
* @ignore
*/
assignAsset(asset) {
const id = asset instanceof Asset ? asset.id : asset;
this._assetReference.id = id;
}

/** @private */
destroyInstance() {
if (this._instance) {
Expand Down Expand Up @@ -487,7 +466,8 @@ class GSplatComponent extends Component {
// create new instance
const asset = this._assetReference.asset;
if (asset) {
this.instance = new GSplatInstance(asset.resource, this._materialOptions || {});
this.instance = new GSplatInstance(asset.resource, this._materialStore);
this._materialStore = null;
this.customAabb = this.instance.resource.aabb.clone();
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/platform/graphics/webgpu/webgpu-graphics-device.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ class WebgpuGraphicsDevice extends GraphicsDevice {
this.samples = this.backBufferAntialias ? 4 : 1;

// WGSL features
const wgslFeatures = navigator.gpu.wgslLanguageFeatures;
const wgslFeatures = window.navigator.gpu.wgslLanguageFeatures;
this.supportsStorageTextureRead = wgslFeatures?.has('readonly_and_readwrite_storage_textures');

this.initCapsDefines();
Expand Down Expand Up @@ -291,7 +291,7 @@ class WebgpuGraphicsDevice extends GraphicsDevice {
let canvasToneMapping = 'standard';

// pixel format of the framebuffer that is the most efficient one on the system
let preferredCanvasFormat = navigator.gpu.getPreferredCanvasFormat();
let preferredCanvasFormat = window.navigator.gpu.getPreferredCanvasFormat();

// display format the user asked for
const displayFormat = this.initOptions.displayFormat;
Expand Down Expand Up @@ -346,7 +346,7 @@ class WebgpuGraphicsDevice extends GraphicsDevice {
// (this allows us to view the preferred format as srgb)
viewFormats: displayFormat === DISPLAYFORMAT_LDR_SRGB ? [this.backBufferViewFormat] : []
};
this.gpuContext.configure(this.canvasConfig);
this.gpuContext?.configure(this.canvasConfig);

this.createBackbuffer();

Expand Down
8 changes: 4 additions & 4 deletions src/platform/graphics/webgpu/webgpu-texture.js
Original file line number Diff line number Diff line change
Expand Up @@ -401,10 +401,10 @@ class WebgpuTexture {

// image types supported by copyExternalImageToTexture
isExternalImage(image) {
return (image instanceof ImageBitmap) ||
(image instanceof HTMLVideoElement) ||
(image instanceof HTMLCanvasElement) ||
(image instanceof OffscreenCanvas);
return (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap) ||
(typeof HTMLVideoElement !== 'undefined' && image instanceof HTMLVideoElement) ||
(typeof HTMLCanvasElement !== 'undefined' && image instanceof HTMLCanvasElement) ||
(typeof OffscreenCanvas !== 'undefined' && image instanceof OffscreenCanvas);
}

uploadExternalImage(device, image, mipLevel, index) {
Expand Down
26 changes: 9 additions & 17 deletions src/scene/gsplat/gsplat-compressed-resource.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@
import {
PIXELFORMAT_RGBA32F, PIXELFORMAT_RGBA32U
} from '../../platform/graphics/constants.js';
import { createGSplatMaterial } from './gsplat-material.js';

Check failure on line 5 in src/scene/gsplat/gsplat-compressed-resource.js

View workflow job for this annotation

GitHub Actions / Lint

'createGSplatMaterial' is defined but never used
import { GSplatResourceBase } from './gsplat-resource-base.js';

/**
* @import { GSplatCompressedData } from './gsplat-compressed-data.js'
* @import { GraphicsDevice } from '../../platform/graphics/graphics-device.js'
* @import { Texture } from '../../platform/graphics/texture.js';
* @import { Material } from '../materials/material.js'
* @import { SplatMaterialOptions } from './gsplat-material.js'
*/

// copy data with padding
Expand Down Expand Up @@ -93,24 +91,18 @@
this.shTexture2?.destroy();
}

/**
* @param {SplatMaterialOptions} options - The splat material options.
* @returns {Material} material - The material to set up for the splat rendering.
*/
createMaterial(options) {
const result = createGSplatMaterial(this.device, options);
result.setDefine('GSPLAT_COMPRESSED_DATA', true);
result.setParameter('packedTexture', this.packedTexture);
result.setParameter('chunkTexture', this.chunkTexture);
configureMaterial(material) {
material.setDefine('GSPLAT_COMPRESSED_DATA', true);
material.setParameter('packedTexture', this.packedTexture);
material.setParameter('chunkTexture', this.chunkTexture);
if (this.shTexture0) {
result.setDefine('SH_BANDS', 3);
result.setParameter('shTexture0', this.shTexture0);
result.setParameter('shTexture1', this.shTexture1);
result.setParameter('shTexture2', this.shTexture2);
material.setDefine('SH_BANDS', 3);
material.setParameter('shTexture0', this.shTexture0);
material.setParameter('shTexture1', this.shTexture1);
material.setParameter('shTexture2', this.shTexture2);
} else {
result.setDefine('SH_BANDS', 0);
material.setDefine('SH_BANDS', 0);
}
return result;
}

/**
Expand Down
114 changes: 79 additions & 35 deletions src/scene/gsplat/gsplat-instance.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { Mat4 } from '../../core/math/mat4.js';
import { Vec3 } from '../../core/math/vec3.js';
import { PIXELFORMAT_R32U } from '../../platform/graphics/constants.js';
import { DITHER_NONE } from '../constants.js';
import { SEMANTIC_ATTR13, SEMANTIC_POSITION, PIXELFORMAT_R32U } from '../../platform/graphics/constants.js';

Check warning on line 3 in src/scene/gsplat/gsplat-instance.js

View workflow job for this annotation

GitHub Actions / Lint

'/home/runner/work/engine/engine/src/platform/graphics/constants.js' imported multiple times
import { MeshInstance } from '../mesh-instance.js';
import { GSplatSorter } from './gsplat-sorter.js';
import { ShaderMaterial } from '../materials/shader-material.js';
import { BLEND_NONE, BLEND_PREMULTIPLIED } from '../constants.js';
import { CULLFACE_NONE } from '../../platform/graphics/constants.js';

Check warning on line 8 in src/scene/gsplat/gsplat-instance.js

View workflow job for this annotation

GitHub Actions / Lint

'/home/runner/work/engine/engine/src/platform/graphics/constants.js' imported multiple times

Check failure on line 8 in src/scene/gsplat/gsplat-instance.js

View workflow job for this annotation

GitHub Actions / Lint

'../../platform/graphics/constants.js' import is duplicated

/**
* @import { Camera } from '../camera.js'
* @import { GSplatResourceBase } from './gsplat-resource-base.js'
* @import { GraphNode } from '../graph-node.js'
* @import { Material } from '../materials/material.js'
* @import { SplatMaterialOptions } from './gsplat-material.js'
* @import { Texture } from '../../platform/graphics/texture.js'
*/

Expand All @@ -24,15 +24,15 @@
/** @type {GSplatResourceBase} */
resource;

/** @type {MeshInstance} */
meshInstance;

/** @type {Material} */
material;

/** @type {Texture} */
orderTexture;

/** @type {ShaderMaterial} */
_material;

/** @type {MeshInstance} */
meshInstance;

options = {};

/** @type {GSplatSorter | null} */
Expand All @@ -52,25 +52,43 @@

/**
* @param {GSplatResourceBase} resource - The splat instance.
* @param {SplatMaterialOptions} options - The options.
* @param {ShaderMaterial|null} material - The material instance.
*/
constructor(resource, options) {
constructor(resource, material) {
this.resource = resource;

// clone options object
options = Object.assign(this.options, options);

// create the order texture
this.orderTexture = resource.createTexture(
'splatOrder',
PIXELFORMAT_R32U,
resource.evalTextureSize(resource.numSplats)
);

// material
this.createMaterial(options);
if (material) {
// material is provided
this._material = material;
} else {
// construct the material
this._material = new ShaderMaterial({
uniqueName: 'SplatMaterial',
vertexGLSL: '#include "gsplatVS"',
fragmentGLSL: '#include "gsplatPS"',
vertexWGSL: '#include "gsplatVS"',
fragmentWGSL: '#include "gsplatPS"',
attributes: {
vertex_position: SEMANTIC_POSITION,
vertex_id_attrib: SEMANTIC_ATTR13
}
});

// default configure
this.configureMaterial(this._material);

// update
this._material.update();
}

this.meshInstance = new MeshInstance(resource.mesh, this.material);
this.meshInstance = new MeshInstance(resource.mesh, this._material);
this.meshInstance.setInstancing(resource.instanceIndices, true);
this.meshInstance.gsplatInstance = this;

Expand All @@ -82,17 +100,15 @@
const chunks = resource.chunks?.slice();

// create sorter
if (!options.dither || options.dither === DITHER_NONE) {
this.sorter = new GSplatSorter();
this.sorter.init(this.orderTexture, centers, chunks);
this.sorter.on('updated', (count) => {
// limit splat render count to exclude those behind the camera
this.meshInstance.instancingCount = Math.ceil(count / resource.instanceSize);

// update splat count on the material
this.material.setParameter('numSplats', count);
});
}
this.sorter = new GSplatSorter();
this.sorter.init(this.orderTexture, centers, chunks);
this.sorter.on('updated', (count) => {
// limit splat render count to exclude those behind the camera
this.meshInstance.instancingCount = Math.ceil(count / resource.instanceSize);

// update splat count on the material
this.material.setParameter('numSplats', count);
});
}

destroy() {
Expand All @@ -105,15 +121,43 @@
return new GSplatInstance(this.resource, this.options);
}

createMaterial(options) {
this.material = this.resource.createMaterial(options);
this.material.setParameter('splatOrder', this.orderTexture);
this.material.setParameter('alphaClip', 0.3);
if (this.meshInstance) {
this.meshInstance.material = this.material;
/**
* @param {ShaderMaterial} value - The material instance.
*/
set material(value) {
if (this.material !== value) {
// set the new material
this.material = value;

if (this.meshInstance) {
this.meshInstance.material = value;
}
}
}

get material() {
return this._material;
}

/**
* Configure the material with gsplat instance and resource properties.
*

Check failure on line 144 in src/scene/gsplat/gsplat-instance.js

View workflow job for this annotation

GitHub Actions / Lint

Trailing spaces not allowed
* @param {ShaderMaterial} material

Check failure on line 145 in src/scene/gsplat/gsplat-instance.js

View workflow job for this annotation

GitHub Actions / Lint

Trailing spaces not allowed

Check failure on line 145 in src/scene/gsplat/gsplat-instance.js

View workflow job for this annotation

GitHub Actions / Lint

Missing JSDoc @param "material" description
* @param {boolean} dither

Check failure on line 146 in src/scene/gsplat/gsplat-instance.js

View workflow job for this annotation

GitHub Actions / Lint

Trailing spaces not allowed

Check failure on line 146 in src/scene/gsplat/gsplat-instance.js

View workflow job for this annotation

GitHub Actions / Lint

Missing JSDoc @param "dither" description
*/
configureMaterial(material, dither = false) {
// allow resource to configure the material
this.resource.configureMaterial(material);

// set instance properties
material.setParameter('splatOrder', this.orderTexture);
material.setParameter('alphaClip', 0.3);
material.setDefine(`DITHER_${dither ? 'BLUENOISE' : 'NONE'}`, '');
material.cull = CULLFACE_NONE;
material.blendType = dither ? BLEND_NONE : BLEND_PREMULTIPLIED;
material.depthWrite = dither;
}

updateViewport(cameraNode) {
const camera = cameraNode?.camera;
const renderTarget = camera?.renderTarget;
Expand Down
6 changes: 6 additions & 0 deletions src/scene/gsplat/gsplat-resource-base.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@ class GSplatResourceBase {
return this.gsplatData.numSplats;
}

configureMaterial(material) {
}

evalTextureSize(count) {
}

/**
* Creates a new texture with the specified parameters.
*
Expand Down
Loading
Loading