UNPKG

playcanvas

Version:

Open-source WebGL/WebGPU 3D engine for the web

666 lines (663 loc) 25.9 kB
import { Debug } from '../../core/debug.js'; import { PIXELFORMAT_R32U, PIXELFORMAT_RGBA32U, PIXELFORMAT_RGBA16F, PIXELFORMAT_RGBA16U, PIXELFORMAT_RG32U } from '../../platform/graphics/constants.js'; import { ShaderMaterial } from '../materials/shader-material.js'; import { GSplatFormat } from '../gsplat/gsplat-format.js'; import { GSPLATDATA_COMPACT, GSPLAT_RENDERER_AUTO, GSPLAT_RENDERER_RASTER_CPU_SORT, GSPLAT_RENDERER_RASTER_GPU_SORT, GSPLAT_RENDERER_COMPUTE, GSPLAT_DEBUG_LOD, GSPLAT_DEBUG_HEATMAP, GSPLAT_DEBUG_NONE, GSPLAT_DEBUG_SH_UPDATE } from '../constants.js'; import glslCompactRead from '../shader-lib/glsl/chunks/gsplat/vert/formats/containerCompactRead.js'; import glslCompactWrite from '../shader-lib/glsl/chunks/gsplat/frag/formats/containerCompactWrite.js'; import glslPackedRead from '../shader-lib/glsl/chunks/gsplat/vert/formats/containerPackedRead.js'; import glslPackedWrite from '../shader-lib/glsl/chunks/gsplat/frag/formats/containerPackedWrite.js'; import wgslCompactRead from '../shader-lib/wgsl/chunks/gsplat/vert/formats/containerCompactRead.js'; import wgslCompactWrite from '../shader-lib/wgsl/chunks/gsplat/frag/formats/containerCompactWrite.js'; import wgslPackedRead from '../shader-lib/wgsl/chunks/gsplat/vert/formats/containerPackedRead.js'; import wgslPackedWrite from '../shader-lib/wgsl/chunks/gsplat/frag/formats/containerPackedWrite.js'; /** * @import { GraphicsDevice } from '../../platform/graphics/graphics-device.js' * @import { Texture } from '../../platform/graphics/texture.js' */ /** * Parameters for GSplat unified system. * * @category Graphics */ class GSplatParams { /** * @param {string} dataFormat - The data format constant. * @returns {GSplatFormat} The created format. * @private */ _createFormat(dataFormat) { let format; if (dataFormat === GSPLATDATA_COMPACT) { // Compact work buffer format (20 bytes/splat): // - dataColor (R32U): RGB color (11+11+10 bits, range [0, 4]) // - dataTransformA (RGBA32U): center.xyz (3×32-bit floats) + scale.xyz (3×8-bit log-encoded, e^-12..e^9) + alpha (8 bits) // Alpha co-located with center enables single-texture opacity early-out in compute shaders. // - dataTransformB (R32U): half-angle quaternion (11+11+10 bits) // See: https://marc-b-reynolds.github.io/quaternions/2017/05/02/QuatQuantPart1.html format = new GSplatFormat(this._device, [ { name: 'dataColor', format: PIXELFORMAT_R32U }, { name: 'dataTransformA', format: PIXELFORMAT_RGBA32U }, { name: 'dataTransformB', format: PIXELFORMAT_R32U } ], { readGLSL: glslCompactRead, readWGSL: wgslCompactRead }); format.setWriteCode(glslCompactWrite, wgslCompactWrite); } else { // Large work buffer format (32 bytes/splat): // - dataColor (RGBA16F/RGBA16U): RGBA color with alpha // - dataTransformA (RGBA32U): center.xyz (3×32-bit floats as uint) + rotation.xy (2×16-bit halfs) // - dataTransformB (RG32U): rotation.z + scale.xyz (4×16-bit halfs, scale.w derived via sqrt) const colorFormat = this._device.getRenderableHdrFormat([ PIXELFORMAT_RGBA16F ]) || PIXELFORMAT_RGBA16U; format = new GSplatFormat(this._device, [ { name: 'dataColor', format: colorFormat }, { name: 'dataTransformA', format: PIXELFORMAT_RGBA32U }, { name: 'dataTransformB', format: PIXELFORMAT_RG32U } ], { readGLSL: glslPackedRead, readWGSL: wgslPackedRead }); format.setWriteCode(glslPackedWrite, wgslPackedWrite); } format.allowStreamRemoval = true; return format; } /** * The rendering pipeline used for gaussian splatting. Can be: * * - {@link GSPLAT_RENDERER_AUTO}: Automatically selects the best pipeline for the platform. * - {@link GSPLAT_RENDERER_RASTER_CPU_SORT}: Rasterization with CPU-side sorting. * - {@link GSPLAT_RENDERER_RASTER_GPU_SORT}: Rasterization with compute shader sorting * (WebGPU only, experimental). * - {@link GSPLAT_RENDERER_COMPUTE}: Full compute pipeline (WebGPU only, experimental). * * Defaults to {@link GSPLAT_RENDERER_AUTO}. Modes requiring WebGPU fall back to * {@link GSPLAT_RENDERER_RASTER_CPU_SORT} on WebGL devices. * * @type {number} */ set renderer(value) { if (this._renderer !== value) { this._renderer = value; if (value === GSPLAT_RENDERER_AUTO) { this._currentRenderer = GSPLAT_RENDERER_RASTER_CPU_SORT; } else if ((value === GSPLAT_RENDERER_RASTER_GPU_SORT || value === GSPLAT_RENDERER_COMPUTE) && !this._device.isWebGPU) { this._currentRenderer = GSPLAT_RENDERER_RASTER_CPU_SORT; } else { this._currentRenderer = value; } } } get renderer() { return this._renderer; } /** * The current rendering pipeline in effect after platform-based fallback resolution. When * {@link renderer} is set to a mode requiring WebGPU on a WebGL device, this returns the * fallback mode actually being used. * * @type {number} */ get currentRenderer() { return this._currentRenderer; } /** * Debug rendering mode for Gaussian splats. Can be: * * - {@link GSPLAT_DEBUG_NONE}: Normal rendering (default). * - {@link GSPLAT_DEBUG_LOD}: Colorize splats by their selected LOD level. * - {@link GSPLAT_DEBUG_SH_UPDATE}: Random color per SH update pass to visualize update * frequency. * - {@link GSPLAT_DEBUG_HEATMAP}: Heatmap visualization of average splats processed per * pixel in each tile. Only supported with {@link GSPLAT_RENDERER_COMPUTE}. * * Only one debug mode can be active at a time. Defaults to {@link GSPLAT_DEBUG_NONE}. * * @type {number} */ set debug(value) { if (this._debug !== value) { const prev = this._debug; this._debug = value; if (value === GSPLAT_DEBUG_LOD || prev === GSPLAT_DEBUG_LOD || value === GSPLAT_DEBUG_HEATMAP || prev === GSPLAT_DEBUG_HEATMAP) { this.dirty = true; } } } get debug() { return this._debug; } /** @deprecated Use {@link GSplatParams#debug} with {@link GSPLAT_DEBUG_LOD} instead. */ set colorizeLod(value) { Debug.deprecated('GSplatParams#colorizeLod is deprecated. Use GSplatParams#debug = GSPLAT_DEBUG_LOD instead.'); this.debug = value ? GSPLAT_DEBUG_LOD : GSPLAT_DEBUG_NONE; } /** * @deprecated Use {@link GSplatParams#debug} with {@link GSPLAT_DEBUG_LOD} instead. * @returns {boolean} Whether LOD colorization is enabled. */ get colorizeLod() { return this._debug === GSPLAT_DEBUG_LOD; } /** * Enables or disables per-component ID storage in the work buffer. When enabled, each GSplat * component gets a unique ID written to the work buffer. This ID is used by the picking * system to identify which component was picked, but is also available to custom shaders for * effects like highlighting, animation, or any per-component differentiation. * * @type {boolean} */ set enableIds(value) { if (value && !this._enableIds) { this._enableIds = true; if (!this._format.getStream('pcId')) { this._format.addExtraStreams([ { name: 'pcId', format: PIXELFORMAT_R32U } ]); } } else if (!value && this._enableIds) { this._enableIds = false; this._format.removeExtraStreams([ 'pcId' ]); } } /** * Gets the ID storage enabled state. * * @type {boolean} */ get enableIds() { return this._enableIds; } /** * Multiplier applied to effective distance for nodes behind the camera when determining LOD. * Value 1 means no penalty; higher values drop LOD faster for nodes behind the camera. * * Note: when using a penalty > 1, it often makes sense to set a positive * {@link GSplatParams#lodUpdateAngle} so LOD is re-evaluated on camera rotation, * not just translation. * * @type {number} */ set lodBehindPenalty(value) { if (this._lodBehindPenalty !== value) { this._lodBehindPenalty = value; this.dirty = true; } } /** * Gets behind-camera LOD penalty multiplier. * * @type {number} */ get lodBehindPenalty() { return this._lodBehindPenalty; } /** * Minimum allowed LOD index (inclusive). Defaults to 0. * * @type {number} */ set lodRangeMin(value) { if (this._lodRangeMin !== value) { this._lodRangeMin = value; this.dirty = true; } } /** * Gets minimum allowed LOD index (inclusive). * * @type {number} */ get lodRangeMin() { return this._lodRangeMin; } /** * Maximum allowed LOD index (inclusive). Defaults to 10. * * @type {number} */ set lodRangeMax(value) { if (this._lodRangeMax !== value) { this._lodRangeMax = value; this.dirty = true; } } /** * Gets maximum allowed LOD index (inclusive). * * @type {number} */ get lodRangeMax() { return this._lodRangeMax; } /** * Maximum number of LOD levels allowed below the optimal level when the optimal data is not * resident in memory. The system may temporarily use a coarser LOD within this limit until the * optimal LOD is available. Defaults to 0, which disables fallback (always load optimal). * Higher values allow faster loading by using lower-quality data. * * @type {number} */ set lodUnderfillLimit(value) { if (this._lodUnderfillLimit !== value) { this._lodUnderfillLimit = value; this.dirty = true; } } /** * Gets the maximum allowed underfill LOD range. * * @type {number} */ get lodUnderfillLimit() { return this._lodUnderfillLimit; } /** * Target number of splats across all GSplats in the scene. When set > 0, * the system adjusts LOD levels globally to stay within this budget. * Set to 0 to disable budget enforcement and use LOD distances only (default). * * @type {number} */ set splatBudget(value) { if (this._splatBudget !== value) { this._splatBudget = value; this.dirty = true; } } /** * Gets the target number of splats across all GSplats in the scene. * * @type {number} */ get splatBudget() { return this._splatBudget; } /** * Gradient texture for elevation-based coloring in overdraw visualization mode. * When set, enables overdraw mode with additive blending. When null, uses normal rendering. * Texture should be (width x 1) size. World Y coordinate (0-20 range) maps to texture U coordinate. * Defaults to null. * * @type {Texture|null} */ set colorRamp(value) { if (this._colorRamp !== value) { this._colorRamp = value; this.dirty = true; } } /** * Gets the color ramp texture for overdraw visualization. * * @type {import('../../platform/graphics/texture.js').Texture|null} */ get colorRamp() { return this._colorRamp; } /** @deprecated Use {@link GSplatParams#debug} with {@link GSPLAT_DEBUG_SH_UPDATE} instead. */ set colorizeColorUpdate(value) { Debug.deprecated('GSplatParams#colorizeColorUpdate is deprecated. Use GSplatParams#debug = GSPLAT_DEBUG_SH_UPDATE instead.'); this.debug = value ? GSPLAT_DEBUG_SH_UPDATE : GSPLAT_DEBUG_NONE; } /** * @deprecated Use {@link GSplatParams#debug} with {@link GSPLAT_DEBUG_SH_UPDATE} instead. * @returns {boolean} Whether SH update colorization is enabled. */ get colorizeColorUpdate() { return this._debug === GSPLAT_DEBUG_SH_UPDATE; } /** @ignore */ set colorUpdateDistance(value) { Debug.removed('GSplatParams#colorUpdateDistance is removed. Use colorUpdateAngle instead.'); } /** @ignore */ get colorUpdateDistance() { Debug.removed('GSplatParams#colorUpdateDistance is removed. Use colorUpdateAngle instead.'); return 0; } /** @ignore */ set colorUpdateDistanceLodScale(value) { Debug.removed('GSplatParams#colorUpdateDistanceLodScale is removed. Per-node distance scaling is now automatic.'); } /** @ignore */ get colorUpdateDistanceLodScale() { Debug.removed('GSplatParams#colorUpdateDistanceLodScale is removed. Per-node distance scaling is now automatic.'); return 0; } /** @ignore */ set colorUpdateAngleLodScale(value) { Debug.removed('GSplatParams#colorUpdateAngleLodScale is removed. Per-node distance scaling is now automatic.'); } /** @ignore */ get colorUpdateAngleLodScale() { Debug.removed('GSplatParams#colorUpdateAngleLodScale is removed. Per-node distance scaling is now automatic.'); return 0; } /** * Sets the alpha threshold below which splats are discarded during shadow, pick, and prepass * rendering. Higher values create more aggressive clipping, while lower values preserve more * translucent splats. Defaults to 0.3. * * @type {number} */ set alphaClip(value) { this._material.setParameter('alphaClip', value); this._material.update(); } /** * Gets the alpha clip threshold. * * @type {number} */ get alphaClip() { return this._material.getParameter('alphaClip')?.data ?? 0.3; } /** * Sets the minimum screen-space pixel size below which splats are discarded. Defaults to 2. * * @type {number} */ set minPixelSize(value) { this._material.setParameter('minPixelSize', value); this._material.update(); } /** * Gets the minimum pixel size threshold. * * @type {number} */ get minPixelSize() { return this._material.getParameter('minPixelSize')?.data ?? 2.0; } /** * Sets the minimum visual contribution threshold for the {@link GSPLAT_RENDERER_COMPUTE} renderer. * Splats whose total screen contribution (opacity * projected area) falls below this value are * discarded. Higher values cull more aggressively, improving performance at the cost of quality. * Set to 0 to disable contribution culling. Defaults to 3. * * @type {number} */ set minContribution(value) { this._material.setParameter('minContribution', value); this._material.update(); } /** * Gets the minimum contribution threshold. * * @type {number} */ get minContribution() { return this._material.getParameter('minContribution')?.data ?? 3.0; } /** * Enables anti-aliasing compensation for Gaussian splats. Defaults to false. * * This option is intended for splat data that was generated with anti-aliasing * enabled during training/export. It improves visual stability and reduces * flickering for very small or distant splats. * * If the source splats were generated without anti-aliasing, enabling this * option may slightly soften the image or alter opacity. * @type {boolean} */ set antiAlias(value) { this._material.setDefine('GSPLAT_AA', value); this._material.update(); } /** * Gets whether anti-aliasing compensation is enabled. * * @type {boolean} */ get antiAlias() { return !!this._material.getDefine('GSPLAT_AA'); } /** * Enables 2D Gaussian Splatting mode. Defaults to false. * * Renders splats as oriented 2D surface elements instead of volumetric 3D Gaussians. * This provides a more surface-accurate appearance but requires splat data that * was generated for 2D Gaussian Splatting. * * Enabling this with standard 3D splat data may produce incorrect results. * @type {boolean} */ set twoDimensional(value) { this._material.setDefine('GSPLAT_2DGS', value); this._material.update(); } /** * Gets whether 2D Gaussian Splatting mode is enabled. * * @type {boolean} */ get twoDimensional() { return !!this._material.getDefine('GSPLAT_2DGS'); } /** * Controls the fisheye projection strength for Gaussian splats. The value is in the * range [0, 1]: * * - 0: Standard rectilinear (perspective) projection. * - (0, 1]: Increasing barrel distortion, producing a wider field of view with a * "little planet" effect at higher values. * * Enabling fisheye for the first time has a small one-off cost as new shaders are * compiled. Subsequent switches between 0 and non-zero are instantaneous. * * Only supported with perspective cameras. Has no effect with orthographic projection. * * Note: This only affects Gaussian splat rendering. Other objects in the scene (meshes, * sprites, etc.) continue to use the standard camera projection and are not distorted. * * For best results, enable {@link GSplatParams#radialSorting} when using fisheye projection * to avoid sorting artifacts caused by the wide field of view. * * Defaults to 0. * * @type {number} */ set fisheye(value) { if (this._fisheye !== value) { const wasEnabled = this._fisheye > 0; this._fisheye = value; const isEnabled = value > 0; if (wasEnabled !== isEnabled) { this._material.setDefine('GSPLAT_FISHEYE', isEnabled); this._material.update(); } } } /** * Gets the fisheye projection strength. * * @type {number} */ get fisheye() { return this._fisheye; } /** * Work buffer data format. Controls the precision and bandwidth of the intermediate work buffer * used during unified GSplat rendering. Can be set to {@link GSPLATDATA_COMPACT} (20 bytes/splat) * or {@link GSPLATDATA_LARGE} (32 bytes/splat). Defaults to {@link GSPLATDATA_COMPACT}. * * @type {string} */ set dataFormat(value) { if (this._dataFormat !== value) { this._dataFormat = value; // capture extra streams from the old format const extraStreams = this._format.extraStreams.map((s)=>({ name: s.name, format: s.format, storage: s.storage })); // create new format with the new data layout this._format = this._createFormat(value); // re-add extra streams if (extraStreams.length > 0) { this._format.addExtraStreams(extraStreams); } this.dirty = true; } } /** * Gets the work buffer data format. * * @type {string} */ get dataFormat() { return this._dataFormat; } /** * A material template that can be customized by the user. Any defines, parameters, or shader * chunks set on this material will be automatically applied to all GSplat components rendered * in unified mode. After making changes, call {@link Material#update} to for the changes to be applied * on the next frame. * * @type {ShaderMaterial} * @example * // Set a custom parameter on all GSplat materials * app.scene.gsplat.material.setParameter('myCustomParam', 1.0); * app.scene.gsplat.material.update(); */ get material() { return this._material; } /** * Format descriptor for work buffer streams. Describes the textures used by the work buffer * for intermediate storage during unified rendering. Users can add extra streams via * {@link GSplatFormat#addExtraStreams} for custom per-splat data. * * @type {GSplatFormat} * @example * // Add a custom stream to store per-splat component IDs * app.scene.gsplat.format.addExtraStreams([{ * name: 'splatId', * format: pc.PIXELFORMAT_R32U * }]); */ get format() { return this._format; } /** * Called at the end of the frame to clear dirty flags. * * @ignore */ frameEnd() { this._material.dirty = false; this.dirty = false; } /** * Creates a new GSplatParams instance. * * @param {GraphicsDevice} device - The graphics device. */ constructor(device){ /** * @type {ShaderMaterial} * @private */ this._material = new ShaderMaterial(); /** * @type {string} * @private */ this._dataFormat = GSPLATDATA_COMPACT; /** * Enables debug rendering of AABBs for GSplat objects. Defaults to false. * * @type {boolean} */ this.debugAabbs = false; /** * Enables radial sorting based on distance from camera (for cubemap rendering). When false, * uses directional sorting along camera forward vector. Defaults to false. * * Note: Radial sorting helps reduce sorting artifacts when the camera rotates (looks around), * while linear sorting is better at minimizing artifacts when the camera translates (moves). * * @type {boolean} */ this.radialSorting = false; /** * @type {number} * @private */ this._renderer = GSPLAT_RENDERER_AUTO; /** * @type {number} * @private */ this._currentRenderer = GSPLAT_RENDERER_RASTER_CPU_SORT; /** * Enables debug rendering of AABBs for GSplat octree nodes. Defaults to false. * * @type {boolean} */ this.debugNodeAabbs = false; /** * Internal dirty flag to trigger update of gsplat managers when some params change. * * @ignore * @type {boolean} */ this.dirty = false; /** * @type {number} * @private */ this._debug = GSPLAT_DEBUG_NONE; /** * @type {boolean} * @private */ this._enableIds = false; /** * Distance threshold in world units to trigger LOD updates for camera and gsplat instances. * Defaults to 1. * * @type {number} */ this.lodUpdateDistance = 1; /** * Angle threshold in degrees to trigger LOD updates based on camera rotation. Set to 0 to * disable rotation-based updates. Defaults to 0. * * @type {number} */ this.lodUpdateAngle = 0; /** * @type {number} * @private */ this._lodBehindPenalty = 1; /** * @type {number} * @private */ this._lodRangeMin = 0; /** * @type {number} * @private */ this._lodRangeMax = 10; /** * @type {number} * @private */ this._lodUnderfillLimit = 0; /** * @type {number} * @private */ this._splatBudget = 0; /** * @type {import('../../platform/graphics/texture.js').Texture|null} * @private */ this._colorRamp = null; /** * Intensity multiplier for overdraw visualization mode. Value of 1 uses alpha of 1/32, * allowing approximately 32 overdraws to reach full brightness with additive blending. * Higher values increase brightness per splat. Defaults to 1. * * @type {number} */ this.colorRampIntensity = 1; /** * Whether to apply scene fog to Gaussian splats. When false, splats ignore fog settings * even if the scene or camera has fog configured. Defaults to true. * * @type {boolean} */ this.useFog = true; /** * Viewing angle threshold in degrees for triggering spherical harmonics color updates. * When the camera translates enough to change the viewing angle to an octree node or * splat by this amount, its SH colors are re-evaluated. Distant nodes naturally update * less frequently since they require more camera movement to reach the angle threshold. * Set to 0 to update every frame where camera moves. Defaults to 10. * * @type {number} */ this.colorUpdateAngle = 10; /** * @type {number} * @private */ this._fisheye = 0; /** * Number of update ticks before unloading unused streamed resources. When a streamed resource's * reference count reaches zero, it enters a cooldown period before being unloaded. This allows * recently used data to remain in memory for quick reuse if needed again soon. Set to 0 to * unload immediately when unused. Defaults to 100. * * @type {number} */ this.cooldownTicks = 100; this._device = device; this._format = this._createFormat(GSPLATDATA_COMPACT); this._material.setParameter('alphaClip', 0.3); this._material.setParameter('minPixelSize', 2.0); this._material.setParameter('minContribution', 3.0); } } export { GSplatParams };