diff --git a/examples/src/examples/compute/particles.example.mjs b/examples/src/examples/compute/particles.example.mjs index cb4966f3089..a711f84d95a 100644 --- a/examples/src/examples/compute/particles.example.mjs +++ b/examples/src/examples/compute/particles.example.mjs @@ -214,6 +214,9 @@ assetListLoader.load(() => { fragmentWGSL: files['shader-shared.wgsl'] + files['shader-rendering.fragment.wgsl'] }); + // rendering shader needs the particle storage buffer to read the particle data + material.setParameter('particles', particleStorageBuffer); + // index buffer - two triangles (6 indices) per particle using 4 vertices const indices = new Uint32Array(numParticles * 6); for (let i = 0; i < numParticles; ++i) { diff --git a/package-lock.json b/package-lock.json index d49eb8a7b80..d976873f88c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "playcanvas", - "version": "2.12.0-beta.5", + "version": "2.12.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "playcanvas", - "version": "2.12.0-beta.5", + "version": "2.12.4", "license": "MIT", "dependencies": { "@types/webxr": "^0.5.24", diff --git a/package.json b/package.json index c40b0c18018..4c755d654c5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "playcanvas", - "version": "2.12.0-beta.5", + "version": "2.12.4", "author": "PlayCanvas ", "homepage": "https://playcanvas.com", "description": "PlayCanvas WebGL game engine", diff --git a/src/extras/gizmo/gizmo.js b/src/extras/gizmo/gizmo.js index f3b00066b04..f14eafe6edf 100644 --- a/src/extras/gizmo/gizmo.js +++ b/src/extras/gizmo/gizmo.js @@ -242,6 +242,13 @@ class Gizmo extends EventHandler { */ intersectShapes = []; + /** + * Flag to indicate whether to call `preventDefault` on pointer events. + * + * @type {boolean} + */ + preventDefault = true; + /** * Creates a new gizmo layer and adds it to the scene. * @@ -445,7 +452,9 @@ class Gizmo extends EventHandler { } const selection = this._getSelection(e.offsetX, e.offsetY); if (selection[0]) { - e.preventDefault(); + if (this.preventDefault) { + e.preventDefault(); + } e.stopPropagation(); } @@ -466,7 +475,9 @@ class Gizmo extends EventHandler { } const selection = this._getSelection(e.offsetX, e.offsetY); if (selection[0]) { - e.preventDefault(); + if (this.preventDefault) { + e.preventDefault(); + } e.stopPropagation(); } this.fire(Gizmo.EVENT_POINTERMOVE, e.offsetX, e.offsetY, selection[0]); @@ -482,7 +493,9 @@ class Gizmo extends EventHandler { } const selection = this._getSelection(e.offsetX, e.offsetY); if (selection[0]) { - e.preventDefault(); + if (this.preventDefault) { + e.preventDefault(); + } e.stopPropagation(); } @@ -497,11 +510,15 @@ class Gizmo extends EventHandler { */ _updatePosition() { position.set(0, 0, 0); - for (let i = 0; i < this.nodes.length; i++) { - const node = this.nodes[i]; - position.add(node.getPosition()); + if (this._coordSpace === 'local') { + position.copy(this.nodes[this.nodes.length - 1].getPosition()); + } else { + for (let i = 0; i < this.nodes.length; i++) { + const node = this.nodes[i]; + position.add(node.getPosition()); + } + position.mulScalar(1.0 / (this.nodes.length || 1)); } - position.mulScalar(1.0 / (this.nodes.length || 1)); if (position.equalsApprox(this.root.getLocalPosition(), UPDATE_EPSILON)) { return; @@ -539,7 +556,7 @@ class Gizmo extends EventHandler { if (this._camera.projection === PROJECTION_PERSPECTIVE) { const gizmoPos = this.root.getLocalPosition(); const cameraPos = this._camera.entity.getPosition(); - const dist = gizmoPos.distance(cameraPos); + const dist = v.sub2(gizmoPos, cameraPos).dot(this._camera.entity.forward); this._scale = Math.tan(0.5 * this._camera.fov * math.DEG_TO_RAD) * dist * PERS_SCALE_RATIO; } else { this._scale = this._camera.orthoHeight * ORTHO_SCALE_RATIO; diff --git a/src/extras/gizmo/rotate-gizmo.js b/src/extras/gizmo/rotate-gizmo.js index 8310b8a20e4..cc5c6eff5e2 100644 --- a/src/extras/gizmo/rotate-gizmo.js +++ b/src/extras/gizmo/rotate-gizmo.js @@ -18,7 +18,6 @@ import { SphereShape } from './shape/sphere-shape.js'; */ // temporary variables -const screen = new Vec2(); const point = new Vec3(); const v1 = new Vec3(); const v2 = new Vec3(); @@ -242,13 +241,13 @@ class RotateGizmo extends TransformGizmo { line.entity.enabled = false; }); - this.on(TransformGizmo.EVENT_TRANSFORMSTART, (_point, x, y) => { + this.on(TransformGizmo.EVENT_TRANSFORMSTART, (point, x, y) => { // store start screen point this._screenPos.set(x, y); this._screenStartPos.set(x, y); // store start angle - this._selectionStartAngle = this._calculateArcAngle(x, y); + this._selectionStartAngle = this._calculateArcAngle(point, x, y); // store initial node rotations this._storeNodeRotations(); @@ -281,7 +280,8 @@ class RotateGizmo extends TransformGizmo { this._setNodeRotations(axis, angleAxis, angleDelta); } else { // calculate angle axis and delta and update node rotations - let angleDelta = this._calculateArcAngle(x, y) - this._selectionStartAngle; + let angleDelta = this._calculateArcAngle(point, x, y) - this._selectionStartAngle; + if (this.snap) { angleDelta = Math.round(angleDelta / this.snapIncrement) * this.snapIncrement; } @@ -669,21 +669,30 @@ class RotateGizmo extends TransformGizmo { const ray = this._createRay(mouseWPos); const plane = this._createPlane(axis, axis === 'f' || axis === 'xyz', false); + if (!plane.intersectsRay(ray, point)) { - // use gizmo position if ray does not intersect to position angle guide correctly - return point.copy(this.root.getLocalPosition()); + // if no intersection, try inverting the ray direction + ray.direction.mulScalar(-1); + const intersection = plane.intersectsRay(ray, point); + ray.direction.mulScalar(-1); + + if (!intersection) { + // use gizmo position if ray does not intersect to position angle guide correctly + return point.copy(this.root.getLocalPosition()); + } } return point; } /** + * @param {Vec3} point - The point. * @param {number} x - The x coordinate. * @param {number} y - The y coordinate. * @returns {number} The angle. * @protected */ - _calculateArcAngle(x, y) { + _calculateArcAngle(point, x, y) { const gizmoPos = this.root.getLocalPosition(); const axis = this._selectedAxis; @@ -721,12 +730,43 @@ class RotateGizmo extends TransformGizmo { break; } case 'orbit': { - // convert gizmo position to screen space§ - const screenPos = this._camera.worldToScreen(gizmoPos, v1); + // plane facing camera so based on mouse position around gizmo + v1.sub2(point, gizmoPos); + + switch (axis) { + case 'x': { + // convert to local space + q1.copy(this._rootStartRot).invert().transformVector(v1, v1); + angle = Math.atan2(v1.z, v1.y) * math.RAD_TO_DEG; + break; + } + case 'y': { + // convert to local space + q1.copy(this._rootStartRot).invert().transformVector(v1, v1); + angle = Math.atan2(v1.x, v1.z) * math.RAD_TO_DEG; + break; + } + case 'z': { + // convert to local space + q1.copy(this._rootStartRot).invert().transformVector(v1, v1); + angle = Math.atan2(v1.y, v1.x) * math.RAD_TO_DEG; + break; + } + case 'f': { + // convert to camera space + q1.copy(this._camera.entity.getRotation()).invert().transformVector(v1, v1); + angle = Math.sign(facingDot) * Math.atan2(v1.y, v1.x) * math.RAD_TO_DEG; + break; + } + } + + // intersection point can be behind camera, so need to check to flip angle delta + const dir = v1.sub2(point, this._camera.entity.getPosition()).normalize(); + const dot = dir.dot(this._camera.entity.forward); + if (dot < 0) { + angle += 180; + } - // calculate angle based on mouse position around gizmo - const dir = screen.set(x - screenPos.x, y - screenPos.y).normalize(); - angle = Math.sign(facingDot) * Math.atan2(-dir.y, dir.x) * math.RAD_TO_DEG; break; } } diff --git a/src/extras/input/sources/dual-gesture-source.js b/src/extras/input/sources/dual-gesture-source.js index 590c532cb57..a258880020f 100644 --- a/src/extras/input/sources/dual-gesture-source.js +++ b/src/extras/input/sources/dual-gesture-source.js @@ -1,5 +1,6 @@ import { DOUBLE_TAP_THRESHOLD, DOUBLE_TAP_VARIANCE } from '../constants.js'; import { InputSource } from '../input.js'; +import { movementState } from '../utils.js'; import { VirtualJoystick } from './virtual-joystick.js'; /** @@ -29,6 +30,12 @@ const endsWith = (str, suffix) => str.indexOf(suffix, str.length - suffix.length * @augments {InputSource} */ class DualGestureSource extends InputSource { + /** + * @type {ReturnType} + * @private + */ + _movementState = movementState(); + /** * @type {`${'joystick' | 'touch'}-${'joystick' | 'touch'}`} * @private @@ -116,6 +123,7 @@ class DualGestureSource extends InputSource { */ _onPointerDown(event) { const { pointerType, pointerId, clientX, clientY } = event; + this._movementState.down(event); if (pointerType !== 'touch') { return; @@ -151,7 +159,8 @@ class DualGestureSource extends InputSource { * @private */ _onPointerMove(event) { - const { pointerType, pointerId, target, clientX, clientY, movementX, movementY } = event; + const { pointerType, pointerId, target, clientX, clientY } = event; + const [movementX, movementY] = this._movementState.move(event); if (pointerType !== 'touch') { return; @@ -188,6 +197,7 @@ class DualGestureSource extends InputSource { */ _onPointerUp(event) { const { pointerType, pointerId } = event; + this._movementState.up(event); if (pointerType !== 'touch') { return; diff --git a/src/extras/input/sources/keyboard-mouse-source.js b/src/extras/input/sources/keyboard-mouse-source.js index a57d92cb0f2..9f600f6ad30 100644 --- a/src/extras/input/sources/keyboard-mouse-source.js +++ b/src/extras/input/sources/keyboard-mouse-source.js @@ -1,4 +1,5 @@ import { InputSource } from '../input.js'; +import { movementState } from '../utils.js'; const PASSIVE = /** @type {AddEventListenerOptions & EventListenerOptions} */ ({ passive: false }); const KEY_CODES = /** @type {const} */ ({ @@ -64,6 +65,12 @@ const array = Array(KEY_COUNT).fill(0); * @augments {InputSource} */ class KeyboardMouseSource extends InputSource { + /** + * @type {ReturnType} + * @private + */ + _movementState = movementState(); + /** * The key codes for the keyboard keys. * @@ -170,6 +177,8 @@ class KeyboardMouseSource extends InputSource { * @private */ _onPointerDown(event) { + this._movementState.down(event); + if (event.pointerType !== 'mouse') { return; } @@ -196,6 +205,8 @@ class KeyboardMouseSource extends InputSource { * @private */ _onPointerMove(event) { + const [movementX, movementY] = this._movementState.move(event); + if (event.pointerType !== 'mouse') { return; } @@ -211,7 +222,8 @@ class KeyboardMouseSource extends InputSource { return; } } - this.deltas.mouse.append([event.movementX, event.movementY]); + + this.deltas.mouse.append([movementX, movementY]); } /** @@ -219,6 +231,8 @@ class KeyboardMouseSource extends InputSource { * @private */ _onPointerUp(event) { + this._movementState.up(event); + if (event.pointerType !== 'mouse') { return; } diff --git a/src/extras/input/sources/multi-touch-source.js b/src/extras/input/sources/multi-touch-source.js index 39a59e6745d..246c35f0bc3 100644 --- a/src/extras/input/sources/multi-touch-source.js +++ b/src/extras/input/sources/multi-touch-source.js @@ -1,5 +1,6 @@ import { Vec2 } from '../../../core/math/vec2.js'; import { InputSource } from '../input.js'; +import { movementState } from '../utils.js'; const tmpVa = new Vec2(); @@ -16,6 +17,12 @@ const tmpVa = new Vec2(); * @augments {InputSource} */ class MultiTouchSource extends InputSource { + /** + * @type {ReturnType} + * @private + */ + _movementState = movementState(); + /** * @type {Map} * @private @@ -53,6 +60,7 @@ class MultiTouchSource extends InputSource { */ _onPointerDown(event) { const { pointerId, pointerType } = event; + this._movementState.down(event); if (pointerType !== 'touch') { return; @@ -76,7 +84,8 @@ class MultiTouchSource extends InputSource { * @private */ _onPointerMove(event) { - const { pointerType, target, pointerId, movementX, movementY } = event; + const { pointerType, target, pointerId } = event; + const [movementX, movementY] = this._movementState.move(event); if (pointerType !== 'touch') { return; @@ -112,6 +121,7 @@ class MultiTouchSource extends InputSource { */ _onPointerUp(event) { const { pointerType, pointerId } = event; + this._movementState.up(event); if (pointerType !== 'touch') { return; diff --git a/src/extras/input/sources/single-gesture-source.js b/src/extras/input/sources/single-gesture-source.js index d65d11dd30c..27c6bb834d0 100644 --- a/src/extras/input/sources/single-gesture-source.js +++ b/src/extras/input/sources/single-gesture-source.js @@ -1,5 +1,6 @@ import { DOUBLE_TAP_THRESHOLD, DOUBLE_TAP_VARIANCE } from '../constants.js'; import { InputSource } from '../input.js'; +import { movementState } from '../utils.js'; import { VirtualJoystick } from './virtual-joystick.js'; /** @@ -14,6 +15,12 @@ import { VirtualJoystick } from './virtual-joystick.js'; * @augments {InputSource} */ class SingleGestureSource extends InputSource { + /** + * @type {ReturnType} + * @private + */ + _movementState = movementState(); + /** * @type {'joystick' | 'touch'} * @private @@ -88,6 +95,7 @@ class SingleGestureSource extends InputSource { */ _onPointerDown(event) { const { pointerType, pointerId, clientX, clientY } = event; + this._movementState.down(event); if (pointerType !== 'touch') { return; @@ -118,7 +126,8 @@ class SingleGestureSource extends InputSource { * @private */ _onPointerMove(event) { - const { pointerType, pointerId, target, clientX, clientY, movementX, movementY } = event; + const { pointerType, pointerId, target, clientX, clientY } = event; + const [movementX, movementY] = this._movementState.move(event); if (pointerType !== 'touch') { return; @@ -146,6 +155,7 @@ class SingleGestureSource extends InputSource { */ _onPointerUp(event) { const { pointerType, pointerId } = event; + this._movementState.up(event); if (pointerType !== 'touch') { return; diff --git a/src/extras/input/utils.js b/src/extras/input/utils.js new file mode 100644 index 00000000000..f7521f5e7fd --- /dev/null +++ b/src/extras/input/utils.js @@ -0,0 +1,23 @@ +// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/movementX +export const movementState = () => { + const state = new Map(); + return { + down: (/** @type {PointerEvent} */ event) => { + state.set(event.pointerId, [event.screenX, event.screenY]); + }, + move: (/** @type {PointerEvent} */ event) => { + if (!state.has(event.pointerId)) { + return [0, 0]; + } + const prev = state.get(event.pointerId); + const mvX = event.screenX - prev[0]; + const mvY = event.screenY - prev[1]; + prev[0] = event.screenX; + prev[1] = event.screenY; + return [mvX, mvY]; + }, + up: (/** @type {PointerEvent} */ event) => { + state.delete(event.pointerId); + } + }; +}; diff --git a/src/scene/gsplat-unified/gsplat-manager.js b/src/scene/gsplat-unified/gsplat-manager.js index 8f9576a59a1..4d6cc2ff09f 100644 --- a/src/scene/gsplat-unified/gsplat-manager.js +++ b/src/scene/gsplat-unified/gsplat-manager.js @@ -393,6 +393,12 @@ class GSplatManager { cameraMovedOrRotated = this.testCameraMoved(); } + Debug.call(() => { + for (const [, inst] of this.octreeInstances) { + inst.debugRender(this.scene); + } + }); + // if parameters are dirty, rebuild world state if (this.scene.gsplat.dirty) { this.layerPlacementsDirty = true; diff --git a/src/scene/gsplat-unified/gsplat-octree-instance.js b/src/scene/gsplat-unified/gsplat-octree-instance.js index 3bf6f072991..036f9b818a2 100644 --- a/src/scene/gsplat-unified/gsplat-octree-instance.js +++ b/src/scene/gsplat-unified/gsplat-octree-instance.js @@ -599,7 +599,14 @@ class GSplatOctreeInstance { // watch prefetched loads for completion to allow promotion this.pollPrefetchCompletions(); - // debug render world space bounds for octree nodes based on current LOD selection + // check if any placements need LOD update + const dirty = this.dirtyModifiedPlacements; + this.dirtyModifiedPlacements = false; + return dirty; + } + + // debug render world space bounds for octree nodes based on current LOD selection + debugRender(scene) { Debug.call(() => { if (scene.gsplat.debugNodeAabbs) { const modelMat = this.placement.node.getWorldTransform(); @@ -614,11 +621,6 @@ class GSplatOctreeInstance { } } }); - - // check if any placements need LOD update - const dirty = this.dirtyModifiedPlacements; - this.dirtyModifiedPlacements = false; - return dirty; } /** diff --git a/src/scene/gsplat/gsplat-data.js b/src/scene/gsplat/gsplat-data.js index 3db62e01031..ce8b55363c6 100644 --- a/src/scene/gsplat/gsplat-data.js +++ b/src/scene/gsplat/gsplat-data.js @@ -130,9 +130,7 @@ class GSplatData { // access a named property getProp(name, elementName = 'vertex') { - const el = this.getElement(elementName); - const storage = el && el.properties.find(p => p.name === name)?.storage; - return /** @type {Float32Array} */ (storage ?? new Float32Array(0)); + return this.getElement(elementName)?.properties.find(p => p.name === name)?.storage; } // access the named element diff --git a/src/scene/gsplat/gsplat-sogs-data.js b/src/scene/gsplat/gsplat-sogs-data.js index 583e1449418..4c06023ddd9 100644 --- a/src/scene/gsplat/gsplat-sogs-data.js +++ b/src/scene/gsplat/gsplat-sogs-data.js @@ -534,6 +534,14 @@ class GSplatSogsData { } }); + // patch codebooks starting with a null entry + ['scales', 'sh0', 'shN'].forEach((name) => { + const codebook = this.meta[name]?.codebook; + if (codebook?.[0] === null) { + codebook[0] = codebook[1] + (codebook[1] - codebook[255]) / 255; + } + }); + if (this.destroyed || device._destroyed) return; // skip the rest if the resource was destroyed await this.generateCenters(); diff --git a/src/scene/materials/material.js b/src/scene/materials/material.js index a1147ebdcc8..7b7f5daa806 100644 --- a/src/scene/materials/material.js +++ b/src/scene/materials/material.js @@ -31,6 +31,7 @@ import { ShaderChunks } from '../shader-lib/shader-chunks.js'; * @import { UniformBufferFormat } from '../../platform/graphics/uniform-buffer-format.js'; * @import { VertexFormat } from '../../platform/graphics/vertex-format.js'; * @import { ShaderChunkMap } from '../shader-lib/shader-chunk-map.js'; + * @import { StorageBuffer } from '../../platform/graphics/storage-buffer.js'; */ // blend mode mapping to op, srcBlend and dstBlend @@ -773,7 +774,7 @@ class Material { * Sets a shader parameter on a material. * * @param {string} name - The name of the parameter to set. - * @param {number|number[]|Float32Array|Texture} data - The value for the specified parameter. + * @param {number|number[]|ArrayBufferView|Texture|StorageBuffer} data - The value for the specified parameter. */ setParameter(name, data) { diff --git a/src/scene/renderer/renderer.js b/src/scene/renderer/renderer.js index cea39dbd25a..10aa5270ebe 100644 --- a/src/scene/renderer/renderer.js +++ b/src/scene/renderer/renderer.js @@ -1,7 +1,6 @@ import { Debug, DebugHelper } from '../../core/debug.js'; import { now } from '../../core/time.js'; import { BlueNoise } from '../../core/math/blue-noise.js'; -import { FloatPacking } from '../../core/math/float-packing.js'; import { Vec2 } from '../../core/math/vec2.js'; import { Vec3 } from '../../core/math/vec3.js'; import { Vec4 } from '../../core/math/vec4.js'; @@ -14,10 +13,7 @@ import { UNIFORMTYPE_MAT4, UNIFORMTYPE_MAT3, UNIFORMTYPE_VEC3, UNIFORMTYPE_VEC2, UNIFORMTYPE_FLOAT, UNIFORMTYPE_INT, SHADERSTAGE_VERTEX, SHADERSTAGE_FRAGMENT, CULLFACE_BACK, CULLFACE_FRONT, CULLFACE_NONE, - BINDGROUP_MESH_UB, - PIXELFORMAT_R16F, - FILTER_LINEAR, - ADDRESS_CLAMP_TO_EDGE + BINDGROUP_MESH_UB } from '../../platform/graphics/constants.js'; import { DebugGraphics } from '../../platform/graphics/debug-graphics.js'; import { UniformBuffer } from '../../platform/graphics/uniform-buffer.js'; @@ -32,7 +28,6 @@ import { } from '../constants.js'; import { LightCube } from '../graphics/light-cube.js'; import { getBlueNoiseTexture } from '../graphics/noise-textures.js'; -import { Texture } from '../../platform/graphics/texture.js'; import { LightTextureAtlas } from '../lighting/light-texture-atlas.js'; import { Material } from '../materials/material.js'; import { ShadowMapCache } from './shadow-map-cache.js'; @@ -104,30 +99,6 @@ const _tempSet = new Set(); const _tempMeshInstances = []; const _tempMeshInstancesSkinned = []; -// construct the exponent lookup table used in gaussian splat rendering -const createExpTableTexture = (device) => { - const expTableSize = 32; - const expTable = new Uint16Array(expTableSize); - for (let i = 0; i < expTableSize; ++i) { - const value = Math.exp(-4.0 * i / (expTableSize - 1)); - const nvalue = (value - Math.exp(-4.0)) / (1.0 - Math.exp(-4.0)); - expTable[i] = FloatPacking.float2Half(nvalue); - } - - return new Texture(device, { - name: 'internal-expTable', - width: expTableSize, - height: 1, - format: PIXELFORMAT_R16F, - mipmaps: false, - minFilter: FILTER_LINEAR, - magFilter: FILTER_LINEAR, - addressU: ADDRESS_CLAMP_TO_EDGE, - addressV: ADDRESS_CLAMP_TO_EDGE, - levels: [expTable] - }); -}; - /** * The base renderer functionality to allow implementation of specialized renderers. * @@ -273,7 +244,6 @@ class Renderer { this.blueNoiseJitterData = new Float32Array(4); this.blueNoiseJitterId = scope.resolve('blueNoiseJitter'); this.blueNoiseTextureId = scope.resolve('blueNoiseTex32'); - this.expTableTextureId = scope.resolve('expTable'); this.alphaTestId = scope.resolve('alpha_ref'); this.opacityMapId = scope.resolve('texture_opacityMap'); @@ -289,8 +259,6 @@ class Renderer { // a single instance of light cube this.lightCube = new LightCube(); this.constantLightCube = scope.resolve('lightCube[0]'); - - this.expTableTexture = createExpTableTexture(this.device); } destroy() { @@ -309,9 +277,6 @@ class Renderer { this.gsplatDirector?.destroy(); this.gsplatDirector = null; - - this.expTableTexture?.destroy(); - this.expTableTexture = null; } /** @@ -1232,7 +1197,6 @@ class Renderer { updateFrameUniforms() { // blue noise texture this.blueNoiseTextureId.setValue(getBlueNoiseTexture(this.device)); - this.expTableTextureId.setValue(this.expTableTexture); } /** diff --git a/src/scene/shader-lib/glsl/chunks/gsplat/frag/gsplat.js b/src/scene/shader-lib/glsl/chunks/gsplat/frag/gsplat.js index 952149815c5..dde1ffc470f 100644 --- a/src/scene/shader-lib/glsl/chunks/gsplat/frag/gsplat.js +++ b/src/scene/shader-lib/glsl/chunks/gsplat/frag/gsplat.js @@ -22,7 +22,12 @@ export default /* glsl */` varying mediump vec2 gaussianUV; varying mediump vec4 gaussianColor; -uniform sampler2D expTable; +const float EXP4 = exp(-4.0); +const float INV_EXP4 = 1.0 / (1.0 - EXP4); + +float normExp(float x) { + return (exp(x * -4.0) - EXP4) * INV_EXP4; +} void main(void) { mediump float A = dot(gaussianUV, gaussianUV); @@ -30,7 +35,7 @@ void main(void) { discard; } - mediump float alpha = texture2DLod(expTable, vec2(A, 0.5), 0.0).r * gaussianColor.a; + mediump float alpha = normExp(A) * gaussianColor.a; #if defined(SHADOW_PASS) || defined(PICK_PASS) || defined(PREPASS_PASS) if (alpha < alphaClip) { diff --git a/src/scene/shader-lib/wgsl/chunks/gsplat/frag/gsplat.js b/src/scene/shader-lib/wgsl/chunks/gsplat/frag/gsplat.js index 9a3cbaeee17..cf5d719908c 100644 --- a/src/scene/shader-lib/wgsl/chunks/gsplat/frag/gsplat.js +++ b/src/scene/shader-lib/wgsl/chunks/gsplat/frag/gsplat.js @@ -19,12 +19,16 @@ export default /* wgsl */` #include "floatAsUintPS" #endif +const EXP4 = exp(-4.0); +const INV_EXP4 = 1.0 / (1.0 - EXP4); + +fn normExp(x: f32) -> f32 { + return (exp(x * -4.0) - EXP4) * INV_EXP4; +} + varying gaussianUV: vec2f; varying gaussianColor: vec4f; -var expTable: texture_2d; -var expTableSampler: sampler; - @fragment fn fragmentMain(input: FragmentInput) -> FragmentOutput { var output: FragmentOutput; @@ -36,7 +40,7 @@ fn fragmentMain(input: FragmentInput) -> FragmentOutput { } // evaluate alpha - var alpha = textureSampleLevel(expTable, expTableSampler, vec2f(A, 0.5), 0).r * gaussianColor.a; + var alpha = normExp(A) * gaussianColor.a; #if defined(SHADOW_PASS) || defined(PICK_PASS) || defined(PREPASS_PASS) if (alpha < uniform.alphaClip) {