Skip to content

Commit 3cac8ca

Browse files
author
Martin Valigursky
committed
Source geometry from work buffer for SOG spherical harmonics updates
Color-only (spherical harmonics) work buffer updates for SOG resources now reconstruct geometry from the work buffer's stored world-space transform data instead of re-reading and dequantizing the source SOG textures (means_l/means_u/quats/scales). - Add gsplatWorkBufferGeometry chunk (GLSL + WGSL) that decodes the stored center/rotation/scale (both compact and packed work buffer encodings) and converts them back to local space, preserving the getCenter/getRotation/getScale API used by setWorkBufferModifier code. - Add a work-buffer-geometry path to gsplatCopyToWorkbuffer and unify the shared color/SH tail across both paths. - Add a dataFormat layout tag to GSplatFormat (set by GSplatParams) so the decode variant is selected from the format identity rather than by inspecting stream pixel formats. - Gate behind GSplatResourceBase#supportsWorkBufferGeometry (SOG only).
1 parent 0e2e3f3 commit 3cac8ca

13 files changed

Lines changed: 401 additions & 63 deletions

File tree

src/scene/gsplat-unified/gsplat-params.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ class GSplatParams {
120120
}
121121

122122
format.allowStreamRemoval = true;
123+
format.dataFormat = dataFormat;
123124
return format;
124125
}
125126

src/scene/gsplat-unified/gsplat-work-buffer-render-pass.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { TextureUtils } from '../../platform/graphics/texture-utils.js';
1717
*/
1818

1919
const _viewMat = new Mat4();
20+
const _invModelMat = new Mat4();
2021
const _modelScale = new Vec3();
2122
const _modelRotation = new Quat();
2223
const _tmpSize = new Vec2();
@@ -58,6 +59,9 @@ class GSplatWorkBufferRenderPass extends RenderPass {
5859
/** @type {Float32Array} */
5960
_modelRotationData = new Float32Array(4);
6061

62+
/** @type {Float32Array} */
63+
_cameraPositionData = new Float32Array(3);
64+
6165
/** @type {Int32Array} */
6266
_textureSize = new Int32Array(2);
6367

@@ -204,6 +208,19 @@ class GSplatWorkBufferRenderPass extends RenderPass {
204208
const viewMat = _viewMat.copy(viewInvMat).invert();
205209
device.scope.resolve('matrix_view').setValue(viewMat.data);
206210

211+
// work-buffer-sourced geometry inputs for color-only (SH) updates
212+
// (used by shaders compiled with GSPLAT_WORKBUFFER_GEOMETRY)
213+
if (this.colorOnly) {
214+
device.scope.resolve('uWorkBufferTransformA').setValue(this.workBuffer.getTexture('dataTransformA'));
215+
device.scope.resolve('uWorkBufferTransformB').setValue(this.workBuffer.getTexture('dataTransformB'));
216+
217+
const cameraPos = cameraNode.getPosition();
218+
this._cameraPositionData[0] = cameraPos.x;
219+
this._cameraPositionData[1] = cameraPos.y;
220+
this._cameraPositionData[2] = cameraPos.z;
221+
device.scope.resolve('uCameraPosition').setValue(this._cameraPositionData);
222+
}
223+
207224
// render each splat info
208225
for (let i = 0; i < splats.length; i++) {
209226
const count = _partialData[i * 2 + 1];
@@ -281,6 +298,13 @@ class GSplatWorkBufferRenderPass extends RenderPass {
281298
scope.resolve('model_scale').setValue(this._modelScaleData);
282299
scope.resolve('model_rotation').setValue(this._modelRotationData);
283300

301+
// inverse model matrix, used by GSPLAT_WORKBUFFER_GEOMETRY shaders to convert stored
302+
// world-space data back to local space
303+
if (this.colorOnly) {
304+
_invModelMat.copy(worldTransform).invert();
305+
scope.resolve('matrix_model_inverse').setValue(_invModelMat.data);
306+
}
307+
284308
// Set placement ID for picking (unconditionally - cheap even if shader doesn't use it)
285309
scope.resolve('uId').setValue(splatInfo.placementId);
286310

src/scene/gsplat/gsplat-format.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,17 @@ class GSplatFormat {
111111
*/
112112
allowStreamRemoval = false;
113113

114+
/**
115+
* Work buffer data layout identifier (one of GSPLATDATA_*), or null for resource formats.
116+
* Set by {@link GSplatParams} when creating a work buffer format. Identifies how transform
117+
* data is encoded, so consumers (e.g. the work-buffer-sourced spherical harmonics update)
118+
* can select the matching decode without inspecting individual stream pixel formats.
119+
*
120+
* @type {string|null}
121+
* @ignore
122+
*/
123+
dataFormat = null;
124+
114125
/**
115126
* Extra streams added via addExtraStreams(). For resource formats, streams can only be
116127
* added, never removed. For work buffer formats (where {@link allowStreamRemoval} is true),

src/scene/gsplat/gsplat-resource-base.js

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Debug } from '../../core/debug.js';
22
import { BoundingBox } from '../../core/shape/bounding-box.js';
3+
import { GSPLATDATA_COMPACT } from '../constants.js';
34
import { Mesh } from '../mesh.js';
45
import { ShaderMaterial } from '../materials/shader-material.js';
56
import { WorkBufferRenderInfo } from '../gsplat-unified/gsplat-work-buffer.js';
@@ -241,6 +242,19 @@ class GSplatResourceBase {
241242
}
242243
}
243244

245+
/**
246+
* True when this resource's color-only work buffer updates (spherical harmonics refresh) can
247+
* source geometry from the work buffer itself instead of re-reading the source textures. The
248+
* resource format's read chunk must compile out getCenter/getRotation/getScale when
249+
* GSPLAT_WORKBUFFER_GEOMETRY is defined (see gsplatWorkBufferGeometryPS chunk).
250+
*
251+
* @type {boolean}
252+
* @ignore
253+
*/
254+
get supportsWorkBufferGeometry() {
255+
return false;
256+
}
257+
244258
/**
245259
* Get or create a QuadRender for rendering to work buffer.
246260
*
@@ -257,7 +271,17 @@ class GSplatResourceBase {
257271
// configure defines to fetch cached data
258272
this.configureMaterialDefines(tempMap);
259273
tempMap.set('GSPLAT_LOD', '');
260-
if (colorOnly) tempMap.set('GSPLAT_COLOR_ONLY', '');
274+
if (colorOnly) {
275+
tempMap.set('GSPLAT_COLOR_ONLY', '');
276+
277+
// source geometry from the work buffer instead of source textures
278+
if (this.supportsWorkBufferGeometry) {
279+
tempMap.set('GSPLAT_WORKBUFFER_GEOMETRY', '');
280+
if (workBufferFormat.dataFormat === GSPLATDATA_COMPACT) {
281+
tempMap.set('GSPLAT_WORKBUFFER_COMPACT', '');
282+
}
283+
}
284+
}
261285

262286
let definesKey = '';
263287
for (const [k, v] of tempMap) {

src/scene/gsplat/gsplat-sog-resource.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,12 @@ class GSplatSogResource extends GSplatResourceBase {
147147
defines.set('SOG_V2', '');
148148
}
149149
}
150+
151+
// SOG geometry getters compile out under GSPLAT_WORKBUFFER_GEOMETRY (see sog.js chunk),
152+
// letting color-only (SH) updates skip the means/quats/scales source reads
153+
get supportsWorkBufferGeometry() {
154+
return true;
155+
}
150156
}
151157

152158
export { GSplatSogResource };

src/scene/shader-lib/glsl/chunks/gsplat/frag/gsplatCopyToWorkbuffer.js

Lines changed: 62 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ export default /* glsl */`
33
44
#define GSPLAT_CENTER_NOPROJ
55
6+
// pre-computed model matrix decomposition (declared before includes, used by
7+
// gsplatWorkBufferGeometryPS)
8+
uniform vec3 model_scale;
9+
uniform vec4 model_rotation; // (x,y,z,w) format
10+
611
#include "gsplatHelpersVS"
712
#include "gsplatFormatVS"
813
#include "gsplatStructsVS"
@@ -11,6 +16,7 @@ export default /* glsl */`
1116
#include "gsplatEvalSHVS"
1217
#include "gsplatQuatToMat3VS"
1318
#include "gsplatReadVS"
19+
#include "gsplatWorkBufferGeometryPS"
1420
#include "gsplatWorkBufferOutputVS"
1521
#include "gsplatWriteVS"
1622
#include "gsplatModifyVS"
@@ -20,10 +26,6 @@ flat varying ivec4 vSubDraw;
2026
2127
uniform vec3 uColorMultiply;
2228
23-
// pre-computed model matrix decomposition
24-
uniform vec3 model_scale;
25-
uniform vec4 model_rotation; // (x,y,z,w) format
26-
2729
#ifdef GSPLAT_ID
2830
uniform uint uId;
2931
#endif
@@ -37,43 +39,71 @@ void main(void) {
3739
// Initialize global splat for format read functions
3840
setSplat(originalIndex);
3941
40-
// read center in local space
41-
vec3 modelCenter = getCenter();
42+
// World-space geometry to store. Rotation/scale default to identity and are only computed on
43+
// the full-render path; under GSPLAT_COLOR_ONLY they are ignored by writeSplat (DCE'd).
44+
vec3 worldCenter;
45+
vec4 worldRotation = vec4(0.0, 0.0, 0.0, 1.0);
46+
vec3 worldScale = vec3(1.0);
47+
#if SH_BANDS > 0
48+
vec3 dir; // model-space view direction
49+
#endif
50+
51+
#ifdef GSPLAT_WORKBUFFER_GEOMETRY
4252
43-
// compute world-space center for storage
44-
vec3 worldCenter = (matrix_model * vec4(modelCenter, 1.0)).xyz;
45-
SplatCenter center;
46-
initCenter(modelCenter, center);
53+
// Color-only update sourcing geometry from previously written work buffer data at this
54+
// destination pixel. The stored center already includes the model transform and
55+
// modifySplatCenter / modifySplatRotationScale, so neither is re-applied here.
56+
initWorkBufferGeometry(ivec2(gl_FragCoord.xy));
57+
worldCenter = workBufferWorldCenter();
4758
48-
// Get source rotation and scale
49-
// getRotation() returns (w,x,y,z) format, convert to (x,y,z,w) for quatMul
50-
vec4 srcRotation = getRotation().yzwx;
51-
vec3 srcScale = getScale();
59+
#if SH_BANDS > 0
60+
// model-space view direction (matches the source path up to non-uniform model scale)
61+
dir = normalize(quatRotateInv(model_rotation, worldCenter - uCameraPosition));
62+
#endif
5263
53-
// Combine: world = model * source (both in x,y,z,w format)
54-
vec4 worldRotation = quatMul(model_rotation, srcRotation);
55-
// Ensure w is positive so sqrt() reconstruction works correctly
56-
// (quaternions q and -q represent the same rotation)
57-
if (worldRotation.w < 0.0) {
58-
worldRotation = -worldRotation;
59-
}
60-
vec3 worldScale = model_scale * srcScale;
64+
#else
6165
62-
// Apply custom center modification
63-
vec3 originalCenter = worldCenter;
64-
modifySplatCenter(worldCenter);
66+
// read center in local space
67+
vec3 modelCenter = getCenter();
6568
66-
// Apply custom rotation/scale modification
67-
modifySplatRotationScale(originalCenter, worldCenter, worldRotation, worldScale);
69+
// compute world-space center for storage
70+
worldCenter = (matrix_model * vec4(modelCenter, 1.0)).xyz;
71+
SplatCenter center;
72+
initCenter(modelCenter, center);
73+
74+
// Get source rotation and scale
75+
// getRotation() returns (w,x,y,z) format, convert to (x,y,z,w) for quatMul
76+
vec4 srcRotation = getRotation().yzwx;
77+
vec3 srcScale = getScale();
78+
79+
// Combine: world = model * source (both in x,y,z,w format)
80+
worldRotation = quatMul(model_rotation, srcRotation);
81+
// Ensure w is positive so sqrt() reconstruction works correctly
82+
// (quaternions q and -q represent the same rotation)
83+
if (worldRotation.w < 0.0) {
84+
worldRotation = -worldRotation;
85+
}
86+
worldScale = model_scale * srcScale;
87+
88+
// Apply custom center modification
89+
vec3 originalCenter = worldCenter;
90+
modifySplatCenter(worldCenter);
91+
92+
// Apply custom rotation/scale modification
93+
modifySplatRotationScale(originalCenter, worldCenter, worldRotation, worldScale);
94+
95+
#if SH_BANDS > 0
96+
// calculate the model-space view direction
97+
dir = normalize(center.view * mat3(center.modelView));
98+
#endif
99+
100+
#endif
68101
69102
// read color
70103
vec4 color = getColor();
71104
72105
// evaluate spherical harmonics
73106
#if SH_BANDS > 0
74-
// calculate the model-space view direction
75-
vec3 dir = normalize(center.view * mat3(center.modelView));
76-
77107
// read sh coefficients
78108
vec3 sh[SH_COEFFS];
79109
float scale;
@@ -88,7 +118,8 @@ void main(void) {
88118
89119
color.xyz *= uColorMultiply;
90120
91-
// write color + transform using format-specific encoding
121+
// write color + transform using format-specific encoding (rotation/scale ignored under
122+
// GSPLAT_COLOR_ONLY)
92123
writeSplat(worldCenter, worldRotation, worldScale, color);
93124
94125
#ifdef GSPLAT_ID
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// Work-buffer-backed geometry for color-only (spherical harmonics) work buffer updates.
2+
// Instead of re-reading geometry from the source format textures, getCenter/getRotation/getScale
3+
// read back the world-space data previously written to the work buffer at the destination pixel,
4+
// and convert it to the splat's local space so user modifier code keeps working. The stored data
5+
// already includes the model transform and modifySplatCenter / modifySplatRotationScale, so
6+
// neither is re-applied by this path.
7+
export default /* glsl */`
8+
#ifdef GSPLAT_WORKBUFFER_GEOMETRY
9+
10+
// world-space transform data previously written to the work buffer (see gsplatWriteVS)
11+
uniform highp usampler2D uWorkBufferTransformA;
12+
uniform highp usampler2D uWorkBufferTransformB;
13+
14+
// inverse of matrix_model, to convert stored world-space data back to local space
15+
uniform mat4 matrix_model_inverse;
16+
17+
// world-space camera position
18+
uniform vec3 uCameraPosition;
19+
20+
ivec2 wbCoord;
21+
uvec4 wbTransformA;
22+
23+
// cache transformA at the destination pixel; must be called before any getters
24+
void initWorkBufferGeometry(ivec2 coord) {
25+
wbCoord = coord;
26+
wbTransformA = texelFetch(uWorkBufferTransformA, coord, 0);
27+
}
28+
29+
vec3 workBufferWorldCenter() {
30+
return vec3(uintBitsToFloat(wbTransformA.x), uintBitsToFloat(wbTransformA.y), uintBitsToFloat(wbTransformA.z));
31+
}
32+
33+
// world-space rotation (x,y,z,w), decoded to match the work buffer write encoding
34+
vec4 workBufferWorldRotation() {
35+
#ifdef GSPLAT_WORKBUFFER_COMPACT
36+
// half-angle projected quaternion, 11+11+10 bits (see containerCompactWrite)
37+
uint data = texelFetch(uWorkBufferTransformB, wbCoord, 0).x;
38+
vec3 p = vec3(
39+
float(data & 0x7FFu) / 2047.0 * 2.0 - 1.0,
40+
float((data >> 11u) & 0x7FFu) / 2047.0 * 2.0 - 1.0,
41+
float((data >> 22u) & 0x3FFu) / 1023.0 * 2.0 - 1.0
42+
);
43+
float d = dot(p, p);
44+
return vec4(sqrt(max(0.0, 2.0 - d)) * p, 1.0 - d);
45+
#else
46+
// rotation.xy in transformA.w, rotation.z in transformB.x (see containerPackedWrite)
47+
vec2 rotXY = unpackHalf2x16(wbTransformA.w);
48+
vec3 r = vec3(rotXY, unpackHalf2x16(texelFetch(uWorkBufferTransformB, wbCoord, 0).x).x);
49+
return vec4(r, sqrt(max(0.0, 1.0 - dot(r, r))));
50+
#endif
51+
}
52+
53+
vec3 workBufferWorldScale() {
54+
#ifdef GSPLAT_WORKBUFFER_COMPACT
55+
// log-encoded scale, 3x8 bits: 0 = true zero, 1-255 maps to e^-12..e^9 (see containerCompactWrite)
56+
uint data = wbTransformA.w;
57+
float sx = float(data & 0xFFu);
58+
float sy = float((data >> 8u) & 0xFFu);
59+
float sz = float((data >> 16u) & 0xFFu);
60+
const float logRange = 21.0 / 255.0;
61+
const float logMin = -12.0;
62+
return vec3(
63+
sx == 0.0 ? 0.0 : exp(sx * logRange + logMin),
64+
sy == 0.0 ? 0.0 : exp(sy * logRange + logMin),
65+
sz == 0.0 ? 0.0 : exp(sz * logRange + logMin)
66+
);
67+
#else
68+
uvec2 b = texelFetch(uWorkBufferTransformB, wbCoord, 0).xy;
69+
return vec3(unpackHalf2x16(b.x).y, unpackHalf2x16(b.y));
70+
#endif
71+
}
72+
73+
// rotate vector by the inverse of unit quaternion q (x,y,z,w)
74+
vec3 quatRotateInv(vec4 q, vec3 v) {
75+
vec3 t = -q.xyz;
76+
return v + 2.0 * cross(t, cross(t, v) + q.w * v);
77+
}
78+
79+
// Source-format-compatible getters for user modifier code: local-space values reconstructed
80+
// from the stored world-space data (quantized by the work buffer format, so rotation and
81+
// scale are approximate).
82+
vec3 getCenter() {
83+
return (matrix_model_inverse * vec4(workBufferWorldCenter(), 1.0)).xyz;
84+
}
85+
86+
// returns (w,x,y,z) to match the source format getRotation convention
87+
vec4 getRotation() {
88+
vec4 worldRotation = workBufferWorldRotation();
89+
vec4 localRotation = quatMul(vec4(-model_rotation.xyz, model_rotation.w), worldRotation);
90+
return localRotation.wxyz;
91+
}
92+
93+
vec3 getScale() {
94+
return workBufferWorldScale() / model_scale;
95+
}
96+
97+
#endif
98+
`;

src/scene/shader-lib/glsl/chunks/gsplat/vert/formats/sog.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ const float norm = sqrt(2.0);
2424
float lutShN(int b) { return texelFetch(sogCodebook, ivec2(b, 0), 0).b; }
2525
#endif
2626
27+
// geometry getters are provided by gsplatWorkBufferGeometryPS when sourcing from the work buffer
28+
#ifndef GSPLAT_WORKBUFFER_GEOMETRY
29+
2730
// read the model-space center of the gaussian (16-bit per-axis, low + high byte)
2831
vec3 getCenter() {
2932
vec3 l = texelFetch(means_l, splat.uv, 0).xyz;
@@ -55,6 +58,8 @@ vec3 getScale() {
5558
return exp(logS);
5659
}
5760
61+
#endif
62+
5863
vec4 getColor() {
5964
vec4 c = texelFetch(sh0, splat.uv, 0);
6065
#ifdef SOG_V2

0 commit comments

Comments
 (0)