@babylonjs/loaders 9.19.1 → 9.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SPLAT/gaussianSplattingStream.d.ts +170 -1
- package/SPLAT/gaussianSplattingStream.js +516 -40
- package/SPLAT/gaussianSplattingStream.js.map +1 -1
- package/SPLAT/gaussianSplattingWorkBuffer.d.ts +163 -4
- package/SPLAT/gaussianSplattingWorkBuffer.js +683 -35
- package/SPLAT/gaussianSplattingWorkBuffer.js.map +1 -1
- package/SPLAT/gaussianSplattingWorkBufferShaders.d.ts +70 -2
- package/SPLAT/gaussianSplattingWorkBufferShaders.js +465 -4
- package/SPLAT/gaussianSplattingWorkBufferShaders.js.map +1 -1
- package/glTF/2.0/Extensions/KHR_interactivity/declarationMapper.d.ts +16 -1
- package/glTF/2.0/Extensions/KHR_interactivity/declarationMapper.js +232 -25
- package/glTF/2.0/Extensions/KHR_interactivity/declarationMapper.js.map +1 -1
- package/glTF/2.0/Extensions/KHR_interactivity/interactivityGraphParser.d.ts +52 -2
- package/glTF/2.0/Extensions/KHR_interactivity/interactivityGraphParser.js +108 -7
- package/glTF/2.0/Extensions/KHR_interactivity/interactivityGraphParser.js.map +1 -1
- package/glTF/2.0/Extensions/KHR_interactivity/interactivityHostResolver.d.ts +37 -0
- package/glTF/2.0/Extensions/KHR_interactivity/interactivityHostResolver.js +69 -0
- package/glTF/2.0/Extensions/KHR_interactivity/interactivityHostResolver.js.map +1 -0
- package/glTF/2.0/Extensions/KHR_interactivity/interactivityReferences.d.ts +44 -0
- package/glTF/2.0/Extensions/KHR_interactivity/interactivityReferences.js +51 -0
- package/glTF/2.0/Extensions/KHR_interactivity/interactivityReferences.js.map +1 -0
- package/glTF/2.0/Extensions/KHR_interactivity.pure.d.ts +1 -0
- package/glTF/2.0/Extensions/KHR_interactivity.pure.js +143 -12
- package/glTF/2.0/Extensions/KHR_interactivity.pure.js.map +1 -1
- package/glTF/2.0/Extensions/KHR_node_hoverability.pure.js +6 -0
- package/glTF/2.0/Extensions/KHR_node_hoverability.pure.js.map +1 -1
- package/glTF/2.0/Extensions/KHR_node_selectability.pure.js +4 -0
- package/glTF/2.0/Extensions/KHR_node_selectability.pure.js.map +1 -1
- package/glTF/2.0/Extensions/KHR_node_visibility.pure.js +11 -0
- package/glTF/2.0/Extensions/KHR_node_visibility.pure.js.map +1 -1
- package/glTF/2.0/Extensions/babylonScenePathToObjectConverter.d.ts +126 -0
- package/glTF/2.0/Extensions/babylonScenePathToObjectConverter.js +251 -0
- package/glTF/2.0/Extensions/babylonScenePathToObjectConverter.js.map +1 -0
- package/glTF/2.0/Extensions/compositePathToObjectConverter.d.ts +51 -0
- package/glTF/2.0/Extensions/compositePathToObjectConverter.js +51 -0
- package/glTF/2.0/Extensions/compositePathToObjectConverter.js.map +1 -0
- package/glTF/2.0/Extensions/gltfPathToObjectConverter.js +69 -7
- package/glTF/2.0/Extensions/gltfPathToObjectConverter.js.map +1 -1
- package/glTF/2.0/Extensions/interactivityAssetPathToObjectConverter.d.ts +41 -0
- package/glTF/2.0/Extensions/interactivityAssetPathToObjectConverter.js +108 -0
- package/glTF/2.0/Extensions/interactivityAssetPathToObjectConverter.js.map +1 -0
- package/glTF/2.0/Extensions/interactivityRefPathToObjectConverter.d.ts +28 -0
- package/glTF/2.0/Extensions/interactivityRefPathToObjectConverter.js +71 -0
- package/glTF/2.0/Extensions/interactivityRefPathToObjectConverter.js.map +1 -0
- package/glTF/2.0/Extensions/objectModelMapping.d.ts +118 -30
- package/glTF/2.0/Extensions/objectModelMapping.js +472 -70
- package/glTF/2.0/Extensions/objectModelMapping.js.map +1 -1
- package/glTF/2.0/Extensions/transmissionHelper.js +4 -0
- package/glTF/2.0/Extensions/transmissionHelper.js.map +1 -1
- package/glTF/2.0/glTFLoader.pure.js +2 -1
- package/glTF/2.0/glTFLoader.pure.js.map +1 -1
- package/package.json +3 -3
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Logger } from "@babylonjs/core/Misc/logger.js";
|
|
1
2
|
import { MultiRenderTarget } from "@babylonjs/core/Materials/Textures/multiRenderTarget.js";
|
|
2
3
|
import { ShaderMaterial } from "@babylonjs/core/Materials/shaderMaterial.js";
|
|
3
4
|
import { Constants } from "@babylonjs/core/Engines/constants.js";
|
|
@@ -5,13 +6,13 @@ import { Mesh } from "@babylonjs/core/Meshes/mesh.js";
|
|
|
5
6
|
import { VertexData } from "@babylonjs/core/Meshes/mesh.vertexData.js";
|
|
6
7
|
import { Color4 } from "@babylonjs/core/Maths/math.color.js";
|
|
7
8
|
import { Vector3, Vector4 } from "@babylonjs/core/Maths/math.vector.js";
|
|
8
|
-
import { GaussianSplattingWorkBufferVertexShaderGLSL, GaussianSplattingWorkBufferFragmentShaderGLSL, GaussianSplattingWorkBufferVertexShaderWGSL, GaussianSplattingWorkBufferFragmentShaderWGSL, GaussianSplattingWorkBufferRelayoutFragmentShaderGLSL, GaussianSplattingWorkBufferRelayoutFragmentShaderWGSL, GaussianSplattingWorkBufferRelayoutShaderName, } from "./gaussianSplattingWorkBufferShaders.js";
|
|
9
|
+
import { GaussianSplattingWorkBufferVertexShaderGLSL, GaussianSplattingWorkBufferFragmentShaderGLSL, GaussianSplattingWorkBufferVertexShaderWGSL, GaussianSplattingWorkBufferFragmentShaderWGSL, GaussianSplattingWorkBufferRelayoutFragmentShaderGLSL, GaussianSplattingWorkBufferRelayoutFragmentShaderWGSL, GaussianSplattingWorkBufferRelayoutShaderName, GaussianSplattingWorkBufferShDecodeShaderName, GaussianSplattingWorkBufferShDecodeFragmentShaderGLSL, GaussianSplattingWorkBufferShDecodeFragmentShaderWGSL, GaussianSplattingWorkBufferShCopyShaderName, GaussianSplattingWorkBufferShCopyFragmentShaderGLSL, GaussianSplattingWorkBufferShCopyFragmentShaderWGSL, GaussianSplattingWorkBufferRotationDecodeShaderName, GaussianSplattingWorkBufferRotationDecodeFragmentShaderGLSL, GaussianSplattingWorkBufferRotationDecodeFragmentShaderWGSL, GaussianSplattingWorkBufferRotCopyShaderName, GaussianSplattingWorkBufferRotCopyFragmentShaderGLSL, GaussianSplattingWorkBufferRotCopyFragmentShaderWGSL, } from "./gaussianSplattingWorkBufferShaders.js";
|
|
9
10
|
import { RawTexture } from "@babylonjs/core/Materials/Textures/rawTexture.js";
|
|
10
11
|
/**
|
|
11
12
|
* A unified, GPU-decoded Gaussian Splatting work buffer.
|
|
12
13
|
*
|
|
13
14
|
* Holds a square MRT texture set (centers / covA / covB / colors) sized to a fixed splat capacity
|
|
14
|
-
* (
|
|
15
|
+
* (`ceil(sqrt(capacity))`). Each streamed SOG file is decoded directly on the GPU
|
|
15
16
|
* (no CPU readback) into its allocated pixel range. The decoded textures are consumed unchanged by the
|
|
16
17
|
* standard (non-SOG) Gaussian Splatting draw path.
|
|
17
18
|
*
|
|
@@ -43,30 +44,271 @@ export class GaussianSplattingWorkBuffer {
|
|
|
43
44
|
get textures() {
|
|
44
45
|
return this._mrt.textures;
|
|
45
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* The baked higher-order SH textures (packed-u32, one per `ceil(coeffs*3/16)`), consumed by the draw path's
|
|
49
|
+
* `computeSHWeighted`/`decompose` as `shTexture0..N`. Empty when SH decoding is not enabled.
|
|
50
|
+
*/
|
|
51
|
+
get shTextures() {
|
|
52
|
+
return this._shMrts.map((m) => m.textures[0]);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The decoded rotation/scale textures ([rotationsA, rotationsB, rotationScale], half-float), consumed by the
|
|
56
|
+
* voxel-IBL path as `rotationsATexture`/`rotationsBTexture`/`rotationScaleTexture`. Empty when rotation decode
|
|
57
|
+
* is not enabled.
|
|
58
|
+
*/
|
|
59
|
+
get rotationTextures() {
|
|
60
|
+
return this._rotMrt ? this._rotMrt.textures : [];
|
|
61
|
+
}
|
|
46
62
|
/**
|
|
47
63
|
* Creates a work buffer sized to hold `capacity` splats.
|
|
64
|
+
*
|
|
65
|
+
* Standalone (default): the work buffer creates and owns a square MRT sized `ceil(sqrt(capacity))`, with
|
|
66
|
+
* decodes addressed from splat 0.
|
|
67
|
+
*
|
|
68
|
+
* Hosted (`externalAtlas` provided): the work buffer decodes/reads back into an externally-owned MRT (a
|
|
69
|
+
* compound mesh's shared atlas) instead of creating its own. Decodes are placed at `externalAtlas.baseOffset`
|
|
70
|
+
* (the reserved region's first splat) and addressed over `externalAtlas.width` (the wide atlas width), so the
|
|
71
|
+
* streamed splats land in the compound's atlas and sort/draw together with the static parts.
|
|
48
72
|
* @param scene hosting scene
|
|
49
73
|
* @param capacity total number of splats the work buffer must address
|
|
74
|
+
* @param externalAtlas optional external atlas to decode into instead of creating an owned square MRT
|
|
75
|
+
* @param sh optional higher-order SH decode configuration. `textureCount = ceil(coeffs*3/16)` for the max SH
|
|
76
|
+
* degree across the streamed files. Standalone: the work buffer creates that many owned single-attachment
|
|
77
|
+
* integer render targets. Hosted: `externalMrts` are the compound's shared SH atlas targets (borrowed).
|
|
78
|
+
* @param rotationScale optional rotation/scale decode configuration (for voxel-IBL shadows). When present the
|
|
79
|
+
* work buffer decodes each splat's rotation matrix + scale into a 3-attachment half-float target. Standalone:
|
|
80
|
+
* the work buffer creates and owns that target. Hosted: `externalMrt` is the compound's shared rotation atlas.
|
|
50
81
|
*/
|
|
51
|
-
constructor(scene, capacity) {
|
|
82
|
+
constructor(scene, capacity, externalAtlas, sh, rotationScale) {
|
|
52
83
|
// Relayout (defrag) copy material, created lazily on first relayout.
|
|
53
84
|
this._copyMaterial = null;
|
|
54
85
|
// Reusable destination->source index map for the relayout pass (created lazily, sized to the work buffer).
|
|
55
86
|
this._relayoutMapData = null;
|
|
56
87
|
this._relayoutMapTexture = null;
|
|
88
|
+
// Transient backup of a hosted region's four textures, held between backupRegion() and restoreRegion() so the
|
|
89
|
+
// GPU-decoded data survives the compound recreating its atlas on a grow (adding a part / a second stream).
|
|
90
|
+
this._backupMrt = null;
|
|
57
91
|
this._disposed = false;
|
|
58
92
|
// Reused WebGL framebuffer for the async centers readback (created lazily, freed in dispose).
|
|
59
93
|
this._readFbo = null;
|
|
94
|
+
// Higher-order spherical-harmonics. Each SH texture is its own single-attachment integer render target
|
|
95
|
+
// (RGBA_INTEGER/UNSIGNED_INTEGER, 16 SH scalar-bytes per texel) so a decode/relayout pass writes exactly one
|
|
96
|
+
// attachment, keeping within WebGPU's per-sample color-attachment byte budget. Empty when SH is off.
|
|
97
|
+
this._shMrts = [];
|
|
98
|
+
// True when this work buffer owns (and disposes) its SH render targets (standalone). False when they are the
|
|
99
|
+
// hosting compound's shared SH atlas (hosted) — borrowed, not disposed. Rebound alongside the core atlas.
|
|
100
|
+
this._ownsShMrts = false;
|
|
101
|
+
// The SH decode material (packed-u32 dequant), created only when SH is requested. Reused across files/passes.
|
|
102
|
+
this._shMaterial = null;
|
|
103
|
+
// Integer SH relayout/backup copy material (usampler2D -> uvec4), created lazily alongside the float copy.
|
|
104
|
+
this._shCopyMaterial = null;
|
|
105
|
+
// Transient region-sized integer backups of the SH textures, held between backupRegion() and restoreRegion()
|
|
106
|
+
// so the baked SH survives the compound recreating its SH atlas on a grow/compaction (parallel to _backupMrt).
|
|
107
|
+
this._backupShMrts = null;
|
|
108
|
+
// Rotation/scale. One 3-attachment half-float render target (rotA / rotB / rotScale) holding the per-splat
|
|
109
|
+
// rotation matrix + scale that voxel-based IBL shadowing consumes. Null when rotation decode is off.
|
|
110
|
+
this._rotMrt = null;
|
|
111
|
+
// True when this work buffer owns (and disposes) its rotation target (standalone). False when it is the hosting
|
|
112
|
+
// compound's shared rotation atlas (hosted) — borrowed, rebound alongside the core atlas, never disposed here.
|
|
113
|
+
this._ownsRotMrt = false;
|
|
114
|
+
// The rotation/scale decode material, created only when rotation is requested. Reused across files.
|
|
115
|
+
this._rotMaterial = null;
|
|
116
|
+
// Rotation/scale relayout/backup copy material (3-out float copy), created lazily alongside the float copy.
|
|
117
|
+
this._rotCopyMaterial = null;
|
|
118
|
+
// Transient region-sized backup of the rotation textures, held between backupRegion() and restoreRegion() so the
|
|
119
|
+
// decoded rotation/scale survives the compound recreating its rotation atlas on a grow/compaction.
|
|
120
|
+
this._backupRotMrt = null;
|
|
60
121
|
this._scene = scene;
|
|
61
122
|
this._shaderLanguage = scene.getEngine().isWebGPU ? 1 /* ShaderLanguage.WGSL */ : 0 /* ShaderLanguage.GLSL */;
|
|
62
|
-
this.
|
|
63
|
-
|
|
64
|
-
|
|
123
|
+
this._capacity = Math.max(1, capacity);
|
|
124
|
+
if (externalAtlas) {
|
|
125
|
+
this._mrt = externalAtlas.mrt;
|
|
126
|
+
this._textureSize = externalAtlas.width;
|
|
127
|
+
this._baseOffset = externalAtlas.baseOffset;
|
|
128
|
+
this._ownsMrt = false;
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
this._textureSize = Math.max(1, Math.ceil(Math.sqrt(Math.max(1, capacity))));
|
|
132
|
+
this._baseOffset = 0;
|
|
133
|
+
this._ownsMrt = true;
|
|
134
|
+
// The decode buffer accumulates (clear disabled) so each decode preserves previously-decoded files.
|
|
135
|
+
this._mrt = this._createMrt("gsWorkBuffer", true);
|
|
136
|
+
}
|
|
137
|
+
// Higher-order SH: one single-attachment integer target per packed-u32 SH texture. Standalone owns square
|
|
138
|
+
// targets sized like the core atlas; hosted borrows the compound's wide shared SH atlas targets.
|
|
139
|
+
if (sh && sh.textureCount > 0) {
|
|
140
|
+
if (sh.externalMrts) {
|
|
141
|
+
this._shMrts = sh.externalMrts.slice(0, sh.textureCount);
|
|
142
|
+
this._ownsShMrts = false;
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
for (let k = 0; k < sh.textureCount; k++) {
|
|
146
|
+
this._shMrts.push(this._createShMrt(`gsWorkBufferSh${k}`, true));
|
|
147
|
+
}
|
|
148
|
+
this._ownsShMrts = true;
|
|
149
|
+
}
|
|
150
|
+
this._shMaterial = this._createShMaterial();
|
|
151
|
+
}
|
|
152
|
+
// Rotation/scale: one 3-attachment half-float target. Standalone owns a square target sized like the core
|
|
153
|
+
// atlas; hosted borrows the compound's wide shared rotation atlas.
|
|
154
|
+
if (rotationScale) {
|
|
155
|
+
if (rotationScale.externalMrt) {
|
|
156
|
+
this._rotMrt = rotationScale.externalMrt;
|
|
157
|
+
this._ownsRotMrt = false;
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
this._rotMrt = this._createRotMrt("gsWorkBufferRot", true);
|
|
161
|
+
this._ownsRotMrt = true;
|
|
162
|
+
}
|
|
163
|
+
this._rotMaterial = this._createRotMaterial();
|
|
164
|
+
}
|
|
65
165
|
// One persistent decode material + fullscreen-triangle quad, reused (with per-file uniforms)
|
|
66
166
|
// for every decode so the shader is compiled only once.
|
|
67
167
|
this._material = this._createMaterial();
|
|
68
168
|
this._quad = this._createQuad();
|
|
69
169
|
this._quad.material = this._material;
|
|
170
|
+
// Hosted: start compiling the copy shader now so backupRegion()/restoreRegion() are ready by the time
|
|
171
|
+
// the compound grows its atlas (a synchronous, non-frame-driven event we can't wait on).
|
|
172
|
+
if (!this._ownsMrt) {
|
|
173
|
+
this.isRelayoutReady();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Rebinds a hosted work buffer to a new atlas MRT (after the compound recreated it on a grow). No-op for a
|
|
178
|
+
* standalone work buffer, which owns its MRT.
|
|
179
|
+
* @param mrt the compound's new shared atlas
|
|
180
|
+
*/
|
|
181
|
+
rebindAtlas(mrt) {
|
|
182
|
+
if (!this._ownsMrt) {
|
|
183
|
+
this._mrt = mrt;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Rebinds a hosted work buffer to the compound's NEW shared SH atlas (after the compound recreated it on a
|
|
188
|
+
* grow/compaction). No-op when SH isn't in use or the work buffer owns its SH targets (standalone).
|
|
189
|
+
* @param shMrts the compound's new shared SH render targets (one per packed-u32 SH texture)
|
|
190
|
+
*/
|
|
191
|
+
rebindShAtlas(shMrts) {
|
|
192
|
+
if (!this._ownsShMrts && shMrts && this._shMrts.length) {
|
|
193
|
+
this._shMrts = shMrts.slice(0, this._shMrts.length);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Rebinds a hosted work buffer to the compound's NEW shared rotation atlas (after the compound recreated it on a
|
|
198
|
+
* grow/compaction). No-op when rotation isn't in use or the work buffer owns its rotation target (standalone).
|
|
199
|
+
* @param rotMrt the compound's new shared rotation atlas
|
|
200
|
+
*/
|
|
201
|
+
rebindRotAtlas(rotMrt) {
|
|
202
|
+
if (!this._ownsRotMrt && rotMrt && this._rotMrt) {
|
|
203
|
+
this._rotMrt = rotMrt;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Relocates this hosted region to a new base splat offset in the shared atlas (used when the compound
|
|
208
|
+
* compacts its atlas and this region's rows move). Subsequent decode/render/readback and
|
|
209
|
+
* {@link restoreRegion} all address from the new base. Call between {@link backupRegion} (which read from
|
|
210
|
+
* the old base) and {@link restoreRegion} (which will write to the new base). No-op for a standalone
|
|
211
|
+
* work buffer, which owns its square MRT at base 0.
|
|
212
|
+
* @param baseOffset the region's new first splat index in the atlas
|
|
213
|
+
*/
|
|
214
|
+
setBaseOffset(baseOffset) {
|
|
215
|
+
if (!this._ownsMrt) {
|
|
216
|
+
this._baseOffset = baseOffset;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* True once the backup/restore/relayout copy shaders are compiled, so {@link backupRegion} can preserve the
|
|
221
|
+
* region across an atlas rebuild. Callers that decode into a hosted region should await this before writing
|
|
222
|
+
* data, so a later grow/compaction can never race shader compilation and drop the region.
|
|
223
|
+
*/
|
|
224
|
+
get canBackup() {
|
|
225
|
+
return this._disposed || this._ownsMrt ? false : this.isRelayoutReady();
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Copies this hosted region's four decoded textures out of the shared atlas into an internal backup MRT so
|
|
229
|
+
* they survive the compound recreating the atlas on a grow. Call immediately before the atlas is recreated,
|
|
230
|
+
* then {@link rebindAtlas} + {@link restoreRegion} after.
|
|
231
|
+
*/
|
|
232
|
+
backupRegion() {
|
|
233
|
+
if (this._disposed || this._ownsMrt) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (!this.isRelayoutReady()) {
|
|
237
|
+
// The copy shaders should be compiled well before any grow (callers warm them before decoding), so this
|
|
238
|
+
// path means a grow raced compilation; warn rather than silently drop the region's GPU data on the restore.
|
|
239
|
+
Logger.Warn("GaussianSplattingWorkBuffer: backup skipped because the copy shaders are not ready; streamed region data may be lost on the atlas rebuild.");
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const width = this._textureSize;
|
|
243
|
+
const regionRows = Math.max(1, Math.floor(this._capacity / width));
|
|
244
|
+
const baseRow = Math.floor(this._baseOffset / width);
|
|
245
|
+
if (!this._backupMrt) {
|
|
246
|
+
this._backupMrt = this._createMrt("gsAtlasBackup", false, width, regionRows);
|
|
247
|
+
}
|
|
248
|
+
// Identity copy of the region out of the atlas: with useMap=0 and dstBaseRow=-baseRow the shader reads
|
|
249
|
+
// atlas texel ((p.y + baseRow) * width + p.x) — the region's global texel — into backup texel p.
|
|
250
|
+
this._renderRelayoutPass(this._backupMrt, this._mrt.textures, this._mrt.textures[0], 0, width, width, 0, -baseRow);
|
|
251
|
+
// Same for each baked SH texture (one integer copy pass per texture).
|
|
252
|
+
if (this._shMrts.length && this._shCopyMaterial) {
|
|
253
|
+
if (!this._backupShMrts) {
|
|
254
|
+
this._backupShMrts = this._shMrts.map((_, k) => this._createShMrt(`gsShAtlasBackup${k}`, false, width, regionRows));
|
|
255
|
+
}
|
|
256
|
+
for (let k = 0; k < this._shMrts.length; k++) {
|
|
257
|
+
this._renderShCopyPass(this._backupShMrts[k], this._shMrts[k].textures[0], this._mrt.textures[0], 0, width, width, 0, -baseRow);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// Same for the rotation/scale textures (one 3-out float copy pass).
|
|
261
|
+
if (this._rotMrt && this._rotCopyMaterial) {
|
|
262
|
+
if (!this._backupRotMrt) {
|
|
263
|
+
this._backupRotMrt = this._createRotMrt("gsRotAtlasBackup", false, width, regionRows);
|
|
264
|
+
}
|
|
265
|
+
this._renderRotCopyPass(this._backupRotMrt, this._rotMrt.textures, this._mrt.textures[0], 0, width, width, 0, -baseRow);
|
|
266
|
+
}
|
|
267
|
+
this._quad.material = this._material;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Restores the backed-up region into the (new) atlas, confined by a scissor to the region's rows so no other
|
|
271
|
+
* part is touched. Call {@link rebindAtlas} to the new atlas first. Frees the backup afterwards.
|
|
272
|
+
*/
|
|
273
|
+
restoreRegion() {
|
|
274
|
+
if (this._disposed || this._ownsMrt || !this._backupMrt || !this._copyMaterial) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const width = this._textureSize;
|
|
278
|
+
const regionRows = Math.max(1, Math.floor(this._capacity / width));
|
|
279
|
+
const baseRow = Math.floor(this._baseOffset / width);
|
|
280
|
+
const engine = this._scene.getEngine();
|
|
281
|
+
engine.enableScissor(0, baseRow, width, regionRows);
|
|
282
|
+
try {
|
|
283
|
+
// useMap=0 identity with dstBaseRow=baseRow: atlas texel p reads backup texel ((p.y - baseRow)*width + p.x).
|
|
284
|
+
this._renderRelayoutPass(this._mrt, this._backupMrt.textures, this._backupMrt.textures[0], 0, width, width, 0, baseRow);
|
|
285
|
+
// Restore each baked SH texture the same way (scissored to the region's rows). The map placeholder must
|
|
286
|
+
// be a FLOAT texture (uMapTex is texture_2d<f32>) even though useMap=0 doesn't sample it — WebGPU still
|
|
287
|
+
// validates the bound texture's sample type against the layout. Use the atlas centers (F32).
|
|
288
|
+
if (this._backupShMrts && this._shMrts.length && this._shCopyMaterial) {
|
|
289
|
+
for (let k = 0; k < this._shMrts.length && k < this._backupShMrts.length; k++) {
|
|
290
|
+
this._renderShCopyPass(this._shMrts[k], this._backupShMrts[k].textures[0], this._mrt.textures[0], 0, width, width, 0, baseRow);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
// Restore the rotation/scale textures the same way (3-out float copy, scissored to the region's rows).
|
|
294
|
+
if (this._backupRotMrt && this._rotMrt && this._rotCopyMaterial) {
|
|
295
|
+
this._renderRotCopyPass(this._rotMrt, this._backupRotMrt.textures, this._mrt.textures[0], 0, width, width, 0, baseRow);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
finally {
|
|
299
|
+
engine.disableScissor();
|
|
300
|
+
this._quad.material = this._material;
|
|
301
|
+
}
|
|
302
|
+
this._backupMrt.dispose();
|
|
303
|
+
this._backupMrt = null;
|
|
304
|
+
if (this._backupShMrts) {
|
|
305
|
+
for (const mrt of this._backupShMrts) {
|
|
306
|
+
mrt.dispose();
|
|
307
|
+
}
|
|
308
|
+
this._backupShMrts = null;
|
|
309
|
+
}
|
|
310
|
+
this._backupRotMrt?.dispose();
|
|
311
|
+
this._backupRotMrt = null;
|
|
70
312
|
}
|
|
71
313
|
/**
|
|
72
314
|
* Creates a 4-attachment MRT (centers F32 / covA / covB / colors U8) sized to the work buffer. covA/covB
|
|
@@ -77,11 +319,13 @@ export class GaussianSplattingWorkBuffer {
|
|
|
77
319
|
* @param name MRT and attachment base name
|
|
78
320
|
* @param disableClear when true, clearing is suppressed so renders accumulate (the decode buffer); when
|
|
79
321
|
* false the MRT clears to zero on each render (the temporary relayout buffer, so gaps stay zeroed)
|
|
322
|
+
* @param width texture width (defaults to the work-buffer size; the region row width for a scoped relayout)
|
|
323
|
+
* @param height texture height (defaults to the work-buffer size; the region row count for a scoped relayout)
|
|
80
324
|
* @returns the created MRT
|
|
81
325
|
*/
|
|
82
|
-
_createMrt(name, disableClear) {
|
|
326
|
+
_createMrt(name, disableClear, width = this._textureSize, height = this._textureSize) {
|
|
83
327
|
const covType = this._scene.getEngine()._caps.textureHalfFloatRender ? Constants.TEXTURETYPE_HALF_FLOAT : Constants.TEXTURETYPE_FLOAT;
|
|
84
|
-
const mrt = new MultiRenderTarget(name, { width
|
|
328
|
+
const mrt = new MultiRenderTarget(name, { width, height }, 4, this._scene, {
|
|
85
329
|
types: [Constants.TEXTURETYPE_FLOAT, covType, covType, Constants.TEXTURETYPE_UNSIGNED_BYTE],
|
|
86
330
|
samplingModes: [
|
|
87
331
|
Constants.TEXTURE_NEAREST_SAMPLINGMODE,
|
|
@@ -101,6 +345,59 @@ export class GaussianSplattingWorkBuffer {
|
|
|
101
345
|
}
|
|
102
346
|
return mrt;
|
|
103
347
|
}
|
|
348
|
+
/**
|
|
349
|
+
* Creates a single-attachment integer render target (RGBA_INTEGER / UNSIGNED_INTEGER) that holds one packed-u32
|
|
350
|
+
* baked-SH texture. One attachment per pass keeps within WebGPU's per-sample color-attachment byte budget and
|
|
351
|
+
* matches the format/type of the draw path's `shTexture0..N` samplers (`_GaussianSplattingBytesPerShTexel`).
|
|
352
|
+
* @param name attachment name
|
|
353
|
+
* @param disableClear when true, clearing is suppressed so SH decodes accumulate across files
|
|
354
|
+
* @param width texture width (defaults to the work-buffer size)
|
|
355
|
+
* @param height texture height (defaults to the work-buffer size)
|
|
356
|
+
* @returns the created single-attachment integer MRT
|
|
357
|
+
*/
|
|
358
|
+
_createShMrt(name, disableClear, width = this._textureSize, height = this._textureSize) {
|
|
359
|
+
const mrt = new MultiRenderTarget(name, { width, height }, 1, this._scene, {
|
|
360
|
+
types: [Constants.TEXTURETYPE_UNSIGNED_INTEGER],
|
|
361
|
+
formats: [Constants.TEXTUREFORMAT_RGBA_INTEGER],
|
|
362
|
+
samplingModes: [Constants.TEXTURE_NEAREST_SAMPLINGMODE],
|
|
363
|
+
generateDepthBuffer: false,
|
|
364
|
+
generateDepthTexture: false,
|
|
365
|
+
generateMipMaps: false,
|
|
366
|
+
}, [name]);
|
|
367
|
+
mrt.clearColor = new Color4(0, 0, 0, 0);
|
|
368
|
+
mrt.renderList = [];
|
|
369
|
+
if (disableClear) {
|
|
370
|
+
mrt.onClearObservable.add(() => { });
|
|
371
|
+
}
|
|
372
|
+
return mrt;
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Creates a 3-attachment half-float MRT ([rotA, rotB, rotScale]) holding the per-splat rotation matrix + scale
|
|
376
|
+
* consumed by voxel-IBL shadowing. RGBA half-float when the engine can render to it (matching the covariance
|
|
377
|
+
* precision), else full float. 3 attachments = 24 B/sample, within WebGPU's per-sample budget (its own pass).
|
|
378
|
+
* @param name MRT and attachment base name
|
|
379
|
+
* @param disableClear when true, clearing is suppressed so decodes accumulate (the decode buffer)
|
|
380
|
+
* @param width texture width (defaults to the work-buffer size; the region row width for a scoped relayout)
|
|
381
|
+
* @param height texture height (defaults to the work-buffer size; the region row count for a scoped relayout)
|
|
382
|
+
* @returns the created MRT
|
|
383
|
+
*/
|
|
384
|
+
_createRotMrt(name, disableClear, width = this._textureSize, height = this._textureSize) {
|
|
385
|
+
const rotType = this._scene.getEngine()._caps.textureHalfFloatRender ? Constants.TEXTURETYPE_HALF_FLOAT : Constants.TEXTURETYPE_FLOAT;
|
|
386
|
+
const mrt = new MultiRenderTarget(name, { width, height }, 3, this._scene, {
|
|
387
|
+
types: [rotType, rotType, rotType],
|
|
388
|
+
formats: [Constants.TEXTUREFORMAT_RGBA, Constants.TEXTUREFORMAT_RGBA, Constants.TEXTUREFORMAT_RGBA],
|
|
389
|
+
samplingModes: [Constants.TEXTURE_NEAREST_SAMPLINGMODE, Constants.TEXTURE_NEAREST_SAMPLINGMODE, Constants.TEXTURE_NEAREST_SAMPLINGMODE],
|
|
390
|
+
generateDepthBuffer: false,
|
|
391
|
+
generateDepthTexture: false,
|
|
392
|
+
generateMipMaps: false,
|
|
393
|
+
}, [`${name}A`, `${name}B`, `${name}Scale`]);
|
|
394
|
+
mrt.clearColor = new Color4(0, 0, 0, 0);
|
|
395
|
+
mrt.renderList = [];
|
|
396
|
+
if (disableClear) {
|
|
397
|
+
mrt.onClearObservable.add(() => { });
|
|
398
|
+
}
|
|
399
|
+
return mrt;
|
|
400
|
+
}
|
|
104
401
|
/**
|
|
105
402
|
* Decodes one SOG file into the work buffer at the given splat offset (accumulating; previously
|
|
106
403
|
* decoded files are preserved). Resolves once the GPU decode has been issued. The caller may
|
|
@@ -112,7 +409,17 @@ export class GaussianSplattingWorkBuffer {
|
|
|
112
409
|
if (this._disposed) {
|
|
113
410
|
return;
|
|
114
411
|
}
|
|
115
|
-
this._applyPack(pack
|
|
412
|
+
this._applyPack(pack);
|
|
413
|
+
// When SH is enabled, bake EVERY file's region — even a file with no higher-order SH (a coarse LOD may drop
|
|
414
|
+
// it): those get uCoeffs=0 so the whole region neutral-fills (128), keeping mixed-degree files consistent.
|
|
415
|
+
const decodeSh = this._shMaterial !== null && this._shMrts.length > 0;
|
|
416
|
+
if (decodeSh) {
|
|
417
|
+
this._applyShPack(pack);
|
|
418
|
+
}
|
|
419
|
+
const decodeRot = this._rotMaterial !== null && this._rotMrt !== null;
|
|
420
|
+
if (decodeRot) {
|
|
421
|
+
this._applyRotPack(pack);
|
|
422
|
+
}
|
|
116
423
|
// Render the decode pass at the start of a frame (the safe point for custom render targets),
|
|
117
424
|
// once the shader is compiled — never re-entrantly from a promise/observable continuation.
|
|
118
425
|
await new Promise((resolve) => {
|
|
@@ -121,12 +428,54 @@ export class GaussianSplattingWorkBuffer {
|
|
|
121
428
|
resolve();
|
|
122
429
|
return;
|
|
123
430
|
}
|
|
124
|
-
if (!this._material.isReady(this._quad)) {
|
|
431
|
+
if (!this._material.isReady(this._quad) || (decodeSh && !this._shMaterial.isReady(this._quad)) || (decodeRot && !this._rotMaterial.isReady(this._quad))) {
|
|
125
432
|
this._scene.onBeforeRenderObservable.addOnce(attempt);
|
|
126
433
|
return;
|
|
127
434
|
}
|
|
128
|
-
this
|
|
129
|
-
|
|
435
|
+
// Scissor every pass to just this file's atlas rows so the fullscreen triangle only shades the region
|
|
436
|
+
// it writes, not the whole (wide) atlas. Correct because the shaders already discard outside the exact
|
|
437
|
+
// splat range, and the row band is a superset of it.
|
|
438
|
+
const width = this._textureSize;
|
|
439
|
+
// Bind uOffset at render time from the live _baseOffset so it shares one value with the scissor below:
|
|
440
|
+
// compactAtlas() can relocate this region (via setBaseOffset) during the defer window.
|
|
441
|
+
const globalOffset = this._baseOffset + offset;
|
|
442
|
+
this._material.setInt("uOffset", globalOffset);
|
|
443
|
+
if (decodeSh) {
|
|
444
|
+
this._shMaterial.setInt("uOffset", globalOffset);
|
|
445
|
+
}
|
|
446
|
+
if (decodeRot) {
|
|
447
|
+
this._rotMaterial.setInt("uOffset", globalOffset);
|
|
448
|
+
}
|
|
449
|
+
const firstRow = Math.floor(globalOffset / width);
|
|
450
|
+
const rowCount = Math.max(1, Math.ceil((globalOffset + pack.splatCount) / width) - firstRow);
|
|
451
|
+
const engine = this._scene.getEngine();
|
|
452
|
+
engine.enableScissor(0, firstRow, width, rowCount);
|
|
453
|
+
try {
|
|
454
|
+
this._quad.material = this._material;
|
|
455
|
+
this._mrt.renderList = [this._quad];
|
|
456
|
+
this._mrt.render();
|
|
457
|
+
// Bake this file's higher-order SH: one pass per packed-u32 SH texture (uShTextureIndex selects the
|
|
458
|
+
// 16 SH scalars written this pass). Files with fewer coefficients neutral-fill (128) higher bands.
|
|
459
|
+
if (decodeSh) {
|
|
460
|
+
for (let k = 0; k < this._shMrts.length; k++) {
|
|
461
|
+
this._shMaterial.setInt("uShTextureIndex", k);
|
|
462
|
+
this._quad.material = this._shMaterial;
|
|
463
|
+
this._shMrts[k].renderList = [this._quad];
|
|
464
|
+
this._shMrts[k].render();
|
|
465
|
+
}
|
|
466
|
+
this._quad.material = this._material;
|
|
467
|
+
}
|
|
468
|
+
// Bake this file's rotation/scale into the 3-attachment half-float rotation target (one pass).
|
|
469
|
+
if (decodeRot) {
|
|
470
|
+
this._quad.material = this._rotMaterial;
|
|
471
|
+
this._rotMrt.renderList = [this._quad];
|
|
472
|
+
this._rotMrt.render();
|
|
473
|
+
this._quad.material = this._material;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
finally {
|
|
477
|
+
engine.disableScissor();
|
|
478
|
+
}
|
|
130
479
|
resolve();
|
|
131
480
|
};
|
|
132
481
|
this._scene.onBeforeRenderObservable.addOnce(attempt);
|
|
@@ -144,7 +493,46 @@ export class GaussianSplattingWorkBuffer {
|
|
|
144
493
|
if (!this._copyMaterial) {
|
|
145
494
|
this._copyMaterial = this._createCopyMaterial();
|
|
146
495
|
}
|
|
147
|
-
|
|
496
|
+
// When SH is in use, the integer copy shader must also be compiled before a backup/restore/relayout can run.
|
|
497
|
+
if (this._shMrts.length && !this._shCopyMaterial) {
|
|
498
|
+
this._shCopyMaterial = this._createShCopyMaterial();
|
|
499
|
+
}
|
|
500
|
+
// Same for the rotation/scale 3-out float copy shader when rotation is in use.
|
|
501
|
+
if (this._rotMrt && !this._rotCopyMaterial) {
|
|
502
|
+
this._rotCopyMaterial = this._createRotCopyMaterial();
|
|
503
|
+
}
|
|
504
|
+
// `isReady` reports "not ready" when a bound sampler points at a disposed texture (a previous backup/relayout
|
|
505
|
+
// binds its temp MRTs as sources, then frees them). The copy passes bind fresh textures every call, so re-point
|
|
506
|
+
// the materials at the live atlas textures first; the check then reflects only whether the shader compiled.
|
|
507
|
+
this._bindCopyMaterialsToAtlas();
|
|
508
|
+
const shReady = this._shMrts.length === 0 || (this._shCopyMaterial !== null && this._shCopyMaterial.isReady(this._quad));
|
|
509
|
+
const rotReady = !this._rotMrt || (this._rotCopyMaterial !== null && this._rotCopyMaterial.isReady(this._quad));
|
|
510
|
+
return this._copyMaterial.isReady(this._quad) && shReady && rotReady;
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Re-points the relayout/backup copy materials' samplers at the live atlas textures (valid, never freed), so a
|
|
514
|
+
* later {@link isRelayoutReady} check isn't tripped by a stale binding to a disposed temp/backup MRT.
|
|
515
|
+
*/
|
|
516
|
+
_bindCopyMaterialsToAtlas() {
|
|
517
|
+
const t = this._mrt.textures;
|
|
518
|
+
if (this._copyMaterial) {
|
|
519
|
+
this._copyMaterial.setTexture("uMapTex", t[0]);
|
|
520
|
+
this._copyMaterial.setTexture("uSrc0", t[0]);
|
|
521
|
+
this._copyMaterial.setTexture("uSrc1", t[1]);
|
|
522
|
+
this._copyMaterial.setTexture("uSrc2", t[2]);
|
|
523
|
+
this._copyMaterial.setTexture("uSrc3", t[3]);
|
|
524
|
+
}
|
|
525
|
+
if (this._shCopyMaterial && this._shMrts.length) {
|
|
526
|
+
this._shCopyMaterial.setTexture("uMapTex", t[0]);
|
|
527
|
+
this._shCopyMaterial.setTexture("uSrcSh", this._shMrts[0].textures[0]);
|
|
528
|
+
}
|
|
529
|
+
if (this._rotCopyMaterial && this._rotMrt) {
|
|
530
|
+
const r = this._rotMrt.textures;
|
|
531
|
+
this._rotCopyMaterial.setTexture("uMapTex", t[0]);
|
|
532
|
+
this._rotCopyMaterial.setTexture("uSrc0", r[0]);
|
|
533
|
+
this._rotCopyMaterial.setTexture("uSrc1", r[1]);
|
|
534
|
+
this._rotCopyMaterial.setTexture("uSrc2", r[2]);
|
|
535
|
+
}
|
|
148
536
|
}
|
|
149
537
|
/**
|
|
150
538
|
* Relayouts the decoded work-buffer textures to a new (defragmented) splat layout, keeping the same
|
|
@@ -159,32 +547,104 @@ export class GaussianSplattingWorkBuffer {
|
|
|
159
547
|
if (this._disposed || !this._copyMaterial) {
|
|
160
548
|
return;
|
|
161
549
|
}
|
|
162
|
-
|
|
163
|
-
|
|
550
|
+
// Map dimensions: standalone maps the whole square buffer; hosted maps just the region's rows.
|
|
551
|
+
const width = this._textureSize;
|
|
552
|
+
const mapW = width;
|
|
553
|
+
const mapH = this._ownsMrt ? width : Math.max(1, Math.floor(this._capacity / width));
|
|
554
|
+
// Reuse the map buffer + its GPU texture across relayouts (dimensions are fixed for a work buffer).
|
|
164
555
|
if (!this._relayoutMapData) {
|
|
165
|
-
this._relayoutMapData = new Float32Array(
|
|
556
|
+
this._relayoutMapData = new Float32Array(mapW * mapH);
|
|
166
557
|
}
|
|
167
558
|
const mapData = this._relayoutMapData;
|
|
168
559
|
mapData.fill(-1);
|
|
169
560
|
mapData.set(srcIndexByDst.subarray(0, Math.min(srcIndexByDst.length, mapData.length)));
|
|
170
561
|
if (!this._relayoutMapTexture) {
|
|
171
|
-
this._relayoutMapTexture = new RawTexture(mapData,
|
|
562
|
+
this._relayoutMapTexture = new RawTexture(mapData, mapW, mapH, Constants.TEXTUREFORMAT_R, this._scene, false, false, Constants.TEXTURE_NEAREST_SAMPLINGMODE, Constants.TEXTURETYPE_FLOAT);
|
|
172
563
|
}
|
|
173
564
|
else {
|
|
174
565
|
this._relayoutMapTexture.update(mapData);
|
|
175
566
|
}
|
|
176
567
|
const mapTexture = this._relayoutMapTexture;
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
568
|
+
if (this._ownsMrt) {
|
|
569
|
+
// Standalone: the work buffer owns the whole square texture, so a full ping-pong is safe.
|
|
570
|
+
const temp = this._createMrt("gsRelayoutTemp", false);
|
|
571
|
+
try {
|
|
572
|
+
this._renderRelayoutPass(temp, this._mrt.textures, mapTexture, 1); // old -> temp via map (gaps cleared)
|
|
573
|
+
this._renderRelayoutPass(this._mrt, temp.textures, mapTexture, 0); // temp -> old, identity full overwrite
|
|
574
|
+
}
|
|
575
|
+
finally {
|
|
576
|
+
temp.dispose();
|
|
577
|
+
}
|
|
578
|
+
// Same ping-pong for each baked SH texture (integer copy, one temp per texture).
|
|
579
|
+
if (this._shMrts.length && this._shCopyMaterial) {
|
|
580
|
+
for (let k = 0; k < this._shMrts.length; k++) {
|
|
581
|
+
const shTemp = this._createShMrt("gsShRelayoutTemp", false);
|
|
582
|
+
try {
|
|
583
|
+
this._renderShCopyPass(shTemp, this._shMrts[k].textures[0], mapTexture, 1);
|
|
584
|
+
this._renderShCopyPass(this._shMrts[k], shTemp.textures[0], mapTexture, 0);
|
|
585
|
+
}
|
|
586
|
+
finally {
|
|
587
|
+
shTemp.dispose();
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
// Same ping-pong for the rotation/scale textures (3-out float copy).
|
|
592
|
+
if (this._rotMrt && this._rotCopyMaterial) {
|
|
593
|
+
const rotTemp = this._createRotMrt("gsRotRelayoutTemp", false);
|
|
594
|
+
try {
|
|
595
|
+
this._renderRotCopyPass(rotTemp, this._rotMrt.textures, mapTexture, 1);
|
|
596
|
+
this._renderRotCopyPass(this._rotMrt, rotTemp.textures, mapTexture, 0);
|
|
597
|
+
}
|
|
598
|
+
finally {
|
|
599
|
+
rotTemp.dispose();
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
this._quad.material = this._material;
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
// Hosted (shared compound atlas): scope the ping-pong to THIS region's row band so other parts are never
|
|
606
|
+
// touched. `_baseOffset` and `_capacity` are row-aligned (reserveStreamingPart), so the band is exact.
|
|
607
|
+
const baseRow = Math.floor(this._baseOffset / width);
|
|
608
|
+
const regionRows = mapH;
|
|
609
|
+
const engine = this._scene.getEngine();
|
|
610
|
+
// Region-sized temp (width x regionRows) — memory stays proportional to the region, not the whole atlas.
|
|
611
|
+
const temp = this._createMrt("gsRelayoutTemp", false, width, regionRows);
|
|
612
|
+
// One region-sized integer temp per baked SH texture (same ping-pong, integer format).
|
|
613
|
+
const shTemps = this._shMrts.length && this._shCopyMaterial ? this._shMrts.map((_, k) => this._createShMrt(`gsShRelayoutTemp${k}`, false, width, regionRows)) : [];
|
|
614
|
+
// One region-sized rotation temp (3-attachment float) when rotation is in use.
|
|
615
|
+
const rotTemp = this._rotMrt && this._rotCopyMaterial ? this._createRotMrt("gsRotRelayoutTemp", false, width, regionRows) : null;
|
|
180
616
|
try {
|
|
181
|
-
// Pass 1:
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
617
|
+
// Pass 1: atlas region -> temp via map. The map is region-local; uSrcBaseOffset shifts each source
|
|
618
|
+
// index to its GLOBAL atlas texel (uSrcWidth = atlas width). Temp is exactly the band, so no scissor.
|
|
619
|
+
this._renderRelayoutPass(temp, this._mrt.textures, mapTexture, 1, /*dstWidth*/ width, /*srcWidth*/ width, /*srcBaseOffset*/ this._baseOffset, /*dstBaseRow*/ 0);
|
|
620
|
+
for (let k = 0; k < shTemps.length; k++) {
|
|
621
|
+
this._renderShCopyPass(shTemps[k], this._shMrts[k].textures[0], mapTexture, 1, width, width, this._baseOffset, 0);
|
|
622
|
+
}
|
|
623
|
+
if (rotTemp) {
|
|
624
|
+
this._renderRotCopyPass(rotTemp, this._rotMrt.textures, mapTexture, 1, width, width, this._baseOffset, 0);
|
|
625
|
+
}
|
|
626
|
+
// Pass 2: temp -> atlas region, identity within the band. uDstBaseRow maps the atlas destination row
|
|
627
|
+
// back into the region-local temp; the scissor confines writes to the band so static parts are safe.
|
|
628
|
+
engine.enableScissor(0, baseRow, width, regionRows);
|
|
629
|
+
try {
|
|
630
|
+
this._renderRelayoutPass(this._mrt, temp.textures, mapTexture, 0, /*dstWidth*/ width, /*srcWidth*/ width, /*srcBaseOffset*/ 0, /*dstBaseRow*/ baseRow);
|
|
631
|
+
for (let k = 0; k < shTemps.length; k++) {
|
|
632
|
+
this._renderShCopyPass(this._shMrts[k], shTemps[k].textures[0], mapTexture, 0, width, width, 0, baseRow);
|
|
633
|
+
}
|
|
634
|
+
if (rotTemp) {
|
|
635
|
+
this._renderRotCopyPass(this._rotMrt, rotTemp.textures, mapTexture, 0, width, width, 0, baseRow);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
finally {
|
|
639
|
+
engine.disableScissor();
|
|
640
|
+
}
|
|
185
641
|
}
|
|
186
642
|
finally {
|
|
187
643
|
temp.dispose();
|
|
644
|
+
for (const t of shTemps) {
|
|
645
|
+
t.dispose();
|
|
646
|
+
}
|
|
647
|
+
rotTemp?.dispose();
|
|
188
648
|
this._quad.material = this._material;
|
|
189
649
|
}
|
|
190
650
|
}
|
|
@@ -192,19 +652,25 @@ export class GaussianSplattingWorkBuffer {
|
|
|
192
652
|
* Renders one relayout copy pass into the target MRT, sampling the given source textures.
|
|
193
653
|
* @param target destination MRT
|
|
194
654
|
* @param sources the four source work-buffer textures
|
|
195
|
-
* @param mapTexture the R32F destination-to-source index map
|
|
655
|
+
* @param mapTexture the R32F destination-to-source index map (only sampled when `useMap` is 1; any bound texture otherwise)
|
|
196
656
|
* @param useMap 1 to read source indices from the map (gaps discarded), 0 for an identity copy
|
|
657
|
+
* @param dstWidth destination width used to linearize the destination texel (defaults to the work-buffer size)
|
|
658
|
+
* @param srcWidth source width used to convert a linear source index to a texel (defaults to the work-buffer size)
|
|
659
|
+
* @param srcBaseOffset added to each mapped source index so a region-local map reads the correct global atlas texel (hosted relayout)
|
|
660
|
+
* @param dstBaseRow subtracted from the destination row so an identity copy reads the region-local temp (hosted relayout)
|
|
197
661
|
*/
|
|
198
|
-
_renderRelayoutPass(target, sources, mapTexture, useMap) {
|
|
662
|
+
_renderRelayoutPass(target, sources, mapTexture, useMap, dstWidth = this._textureSize, srcWidth = this._textureSize, srcBaseOffset = 0, dstBaseRow = 0) {
|
|
199
663
|
const material = this._copyMaterial;
|
|
200
664
|
material.setTexture("uMapTex", mapTexture);
|
|
201
665
|
material.setTexture("uSrc0", sources[0]);
|
|
202
666
|
material.setTexture("uSrc1", sources[1]);
|
|
203
667
|
material.setTexture("uSrc2", sources[2]);
|
|
204
668
|
material.setTexture("uSrc3", sources[3]);
|
|
205
|
-
material.setInt("uDstWidth",
|
|
206
|
-
material.setInt("uSrcWidth",
|
|
669
|
+
material.setInt("uDstWidth", dstWidth);
|
|
670
|
+
material.setInt("uSrcWidth", srcWidth);
|
|
207
671
|
material.setInt("uUseMap", useMap);
|
|
672
|
+
material.setInt("uSrcBaseOffset", srcBaseOffset);
|
|
673
|
+
material.setInt("uDstBaseRow", dstBaseRow);
|
|
208
674
|
this._quad.material = material;
|
|
209
675
|
target.renderList = [this._quad];
|
|
210
676
|
target.render();
|
|
@@ -216,7 +682,7 @@ export class GaussianSplattingWorkBuffer {
|
|
|
216
682
|
fragmentSource: isWGSL ? GaussianSplattingWorkBufferRelayoutFragmentShaderWGSL : GaussianSplattingWorkBufferRelayoutFragmentShaderGLSL,
|
|
217
683
|
}, {
|
|
218
684
|
attributes: ["position"],
|
|
219
|
-
uniforms: ["uDstWidth", "uSrcWidth", "uUseMap"],
|
|
685
|
+
uniforms: ["uDstWidth", "uSrcWidth", "uUseMap", "uSrcBaseOffset", "uDstBaseRow"],
|
|
220
686
|
samplers: ["uMapTex", "uSrc0", "uSrc1", "uSrc2", "uSrc3"],
|
|
221
687
|
shaderLanguage: this._shaderLanguage,
|
|
222
688
|
});
|
|
@@ -224,6 +690,88 @@ export class GaussianSplattingWorkBuffer {
|
|
|
224
690
|
material.disableDepthWrite = true;
|
|
225
691
|
return material;
|
|
226
692
|
}
|
|
693
|
+
/**
|
|
694
|
+
* Renders one INTEGER SH copy pass (one packed-u32 SH texture) into the target, sampling one integer source.
|
|
695
|
+
* Same index/map/base math as {@link _renderRelayoutPass} but for the integer SH format.
|
|
696
|
+
* @param target destination single-attachment integer MRT
|
|
697
|
+
* @param srcSh the integer SH source texture
|
|
698
|
+
* @param mapTexture the R32F destination-to-source index map (sampled only when `useMap` is 1)
|
|
699
|
+
* @param useMap 1 to read source indices from the map (gaps discarded), 0 for an identity copy
|
|
700
|
+
* @param dstWidth destination width used to linearize the destination texel
|
|
701
|
+
* @param srcWidth source width used to convert a linear source index to a texel
|
|
702
|
+
* @param srcBaseOffset added to each mapped source index (region-local map -> global atlas texel)
|
|
703
|
+
* @param dstBaseRow subtracted from the destination row for an identity copy of a region-local temp
|
|
704
|
+
*/
|
|
705
|
+
_renderShCopyPass(target, srcSh, mapTexture, useMap, dstWidth = this._textureSize, srcWidth = this._textureSize, srcBaseOffset = 0, dstBaseRow = 0) {
|
|
706
|
+
const material = this._shCopyMaterial;
|
|
707
|
+
material.setTexture("uMapTex", mapTexture);
|
|
708
|
+
material.setTexture("uSrcSh", srcSh);
|
|
709
|
+
material.setInt("uDstWidth", dstWidth);
|
|
710
|
+
material.setInt("uSrcWidth", srcWidth);
|
|
711
|
+
material.setInt("uUseMap", useMap);
|
|
712
|
+
material.setInt("uSrcBaseOffset", srcBaseOffset);
|
|
713
|
+
material.setInt("uDstBaseRow", dstBaseRow);
|
|
714
|
+
this._quad.material = material;
|
|
715
|
+
target.renderList = [this._quad];
|
|
716
|
+
target.render();
|
|
717
|
+
}
|
|
718
|
+
_createShCopyMaterial() {
|
|
719
|
+
const isWGSL = this._shaderLanguage === 1 /* ShaderLanguage.WGSL */;
|
|
720
|
+
const material = new ShaderMaterial(GaussianSplattingWorkBufferShCopyShaderName, this._scene, {
|
|
721
|
+
vertexSource: isWGSL ? GaussianSplattingWorkBufferVertexShaderWGSL : GaussianSplattingWorkBufferVertexShaderGLSL,
|
|
722
|
+
fragmentSource: isWGSL ? GaussianSplattingWorkBufferShCopyFragmentShaderWGSL : GaussianSplattingWorkBufferShCopyFragmentShaderGLSL,
|
|
723
|
+
}, {
|
|
724
|
+
attributes: ["position"],
|
|
725
|
+
uniforms: ["uDstWidth", "uSrcWidth", "uUseMap", "uSrcBaseOffset", "uDstBaseRow"],
|
|
726
|
+
samplers: ["uMapTex", "uSrcSh"],
|
|
727
|
+
shaderLanguage: this._shaderLanguage,
|
|
728
|
+
});
|
|
729
|
+
material.backFaceCulling = false;
|
|
730
|
+
material.disableDepthWrite = true;
|
|
731
|
+
return material;
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Renders one rotation/scale copy pass (the three half-float rotation textures) into the target. Same
|
|
735
|
+
* index/map/base math as {@link _renderRelayoutPass} but with three attachments.
|
|
736
|
+
* @param target destination 3-attachment MRT
|
|
737
|
+
* @param sources the three source rotation textures ([rotA, rotB, rotScale])
|
|
738
|
+
* @param mapTexture the R32F destination-to-source index map (sampled only when `useMap` is 1)
|
|
739
|
+
* @param useMap 1 to read source indices from the map (gaps discarded), 0 for an identity copy
|
|
740
|
+
* @param dstWidth destination width used to linearize the destination texel
|
|
741
|
+
* @param srcWidth source width used to convert a linear source index to a texel
|
|
742
|
+
* @param srcBaseOffset added to each mapped source index (region-local map -> global atlas texel)
|
|
743
|
+
* @param dstBaseRow subtracted from the destination row for an identity copy of a region-local temp
|
|
744
|
+
*/
|
|
745
|
+
_renderRotCopyPass(target, sources, mapTexture, useMap, dstWidth = this._textureSize, srcWidth = this._textureSize, srcBaseOffset = 0, dstBaseRow = 0) {
|
|
746
|
+
const material = this._rotCopyMaterial;
|
|
747
|
+
material.setTexture("uMapTex", mapTexture);
|
|
748
|
+
material.setTexture("uSrc0", sources[0]);
|
|
749
|
+
material.setTexture("uSrc1", sources[1]);
|
|
750
|
+
material.setTexture("uSrc2", sources[2]);
|
|
751
|
+
material.setInt("uDstWidth", dstWidth);
|
|
752
|
+
material.setInt("uSrcWidth", srcWidth);
|
|
753
|
+
material.setInt("uUseMap", useMap);
|
|
754
|
+
material.setInt("uSrcBaseOffset", srcBaseOffset);
|
|
755
|
+
material.setInt("uDstBaseRow", dstBaseRow);
|
|
756
|
+
this._quad.material = material;
|
|
757
|
+
target.renderList = [this._quad];
|
|
758
|
+
target.render();
|
|
759
|
+
}
|
|
760
|
+
_createRotCopyMaterial() {
|
|
761
|
+
const isWGSL = this._shaderLanguage === 1 /* ShaderLanguage.WGSL */;
|
|
762
|
+
const material = new ShaderMaterial(GaussianSplattingWorkBufferRotCopyShaderName, this._scene, {
|
|
763
|
+
vertexSource: isWGSL ? GaussianSplattingWorkBufferVertexShaderWGSL : GaussianSplattingWorkBufferVertexShaderGLSL,
|
|
764
|
+
fragmentSource: isWGSL ? GaussianSplattingWorkBufferRotCopyFragmentShaderWGSL : GaussianSplattingWorkBufferRotCopyFragmentShaderGLSL,
|
|
765
|
+
}, {
|
|
766
|
+
attributes: ["position"],
|
|
767
|
+
uniforms: ["uDstWidth", "uSrcWidth", "uUseMap", "uSrcBaseOffset", "uDstBaseRow"],
|
|
768
|
+
samplers: ["uMapTex", "uSrc0", "uSrc1", "uSrc2"],
|
|
769
|
+
shaderLanguage: this._shaderLanguage,
|
|
770
|
+
});
|
|
771
|
+
material.backFaceCulling = false;
|
|
772
|
+
material.disableDepthWrite = true;
|
|
773
|
+
return material;
|
|
774
|
+
}
|
|
227
775
|
/**
|
|
228
776
|
* Asynchronously reads back the decoded splat centers (stride-4 xyzw, w=1) for a contiguous splat range
|
|
229
777
|
* from the work buffer's centers texture, using a non-blocking GPU readback (WebGL2 PBO + fence, or WebGPU
|
|
@@ -240,14 +788,17 @@ export class GaussianSplattingWorkBuffer {
|
|
|
240
788
|
return null;
|
|
241
789
|
}
|
|
242
790
|
const width = this._textureSize;
|
|
791
|
+
// Shift the region-local offset by the atlas base so the readback targets the same global texels the
|
|
792
|
+
// decode wrote (0 base for a standalone work buffer).
|
|
793
|
+
const globalOffset = this._baseOffset + splatOffset;
|
|
243
794
|
// The range maps to whole texel rows [rowStart, rowEnd); read that rectangle and slice the exact range.
|
|
244
795
|
// Splat i lives at texel (i % width, floor(i / width)) in both decode and draw, so the readback (which
|
|
245
796
|
// indexes the same texture storage directly, with no UV/flip) yields splat i at buffer position
|
|
246
797
|
// i - rowStart * width on every backend.
|
|
247
|
-
const rowStart = Math.floor(
|
|
248
|
-
const rowEnd = Math.ceil((
|
|
798
|
+
const rowStart = Math.floor(globalOffset / width);
|
|
799
|
+
const rowEnd = Math.ceil((globalOffset + splatCount) / width);
|
|
249
800
|
const rowCount = rowEnd - rowStart;
|
|
250
|
-
const startInBuffer = (
|
|
801
|
+
const startInBuffer = (globalOffset - rowStart * width) * 4;
|
|
251
802
|
const sliceEnd = startInBuffer + splatCount * 4;
|
|
252
803
|
const centers = this._mrt.textures[0];
|
|
253
804
|
const engine = this._scene.getEngine();
|
|
@@ -306,9 +857,38 @@ export class GaussianSplattingWorkBuffer {
|
|
|
306
857
|
}
|
|
307
858
|
this._quad.dispose();
|
|
308
859
|
this._material.dispose(true, false);
|
|
860
|
+
this._shMaterial?.dispose(true, false);
|
|
861
|
+
this._rotMaterial?.dispose(true, false);
|
|
309
862
|
this._copyMaterial?.dispose(true, false);
|
|
863
|
+
this._shCopyMaterial?.dispose(true, false);
|
|
864
|
+
this._rotCopyMaterial?.dispose(true, false);
|
|
310
865
|
this._relayoutMapTexture?.dispose();
|
|
311
|
-
this.
|
|
866
|
+
this._backupMrt?.dispose();
|
|
867
|
+
this._backupMrt = null;
|
|
868
|
+
if (this._backupShMrts) {
|
|
869
|
+
for (const mrt of this._backupShMrts) {
|
|
870
|
+
mrt.dispose();
|
|
871
|
+
}
|
|
872
|
+
this._backupShMrts = null;
|
|
873
|
+
}
|
|
874
|
+
this._backupRotMrt?.dispose();
|
|
875
|
+
this._backupRotMrt = null;
|
|
876
|
+
// Only dispose the MRT when we own it; an external atlas belongs to the hosting compound mesh.
|
|
877
|
+
if (this._ownsMrt) {
|
|
878
|
+
this._mrt.dispose();
|
|
879
|
+
}
|
|
880
|
+
// Same ownership rule for the SH targets (standalone owns; hosted borrows the compound's shared SH atlas).
|
|
881
|
+
if (this._ownsShMrts) {
|
|
882
|
+
for (const mrt of this._shMrts) {
|
|
883
|
+
mrt.dispose();
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
this._shMrts = [];
|
|
887
|
+
// Same ownership rule for the rotation target.
|
|
888
|
+
if (this._ownsRotMrt) {
|
|
889
|
+
this._rotMrt?.dispose();
|
|
890
|
+
}
|
|
891
|
+
this._rotMrt = null;
|
|
312
892
|
}
|
|
313
893
|
_createQuad() {
|
|
314
894
|
const quad = new Mesh("gsWorkBufferQuad", this._scene);
|
|
@@ -336,7 +916,7 @@ export class GaussianSplattingWorkBuffer {
|
|
|
336
916
|
material.disableDepthWrite = true;
|
|
337
917
|
return material;
|
|
338
918
|
}
|
|
339
|
-
_applyPack(pack
|
|
919
|
+
_applyPack(pack) {
|
|
340
920
|
const material = this._material;
|
|
341
921
|
const srcWidth = pack.meansTextureL.getSize().width;
|
|
342
922
|
material.setTexture("sogMeansLTex", pack.meansTextureL);
|
|
@@ -357,7 +937,75 @@ export class GaussianSplattingWorkBuffer {
|
|
|
357
937
|
material.setVector4("sogSh0Min", new Vector4(c0Min[0], c0Min[1], c0Min[2], c0Min[3]));
|
|
358
938
|
material.setVector4("sogSh0Max", new Vector4(c0Max[0], c0Max[1], c0Max[2], c0Max[3]));
|
|
359
939
|
material.setInt("uVersion", pack.version);
|
|
360
|
-
|
|
940
|
+
// uOffset is bound at render time in decodeAsync (from the live _baseOffset).
|
|
941
|
+
material.setInt("uCount", pack.splatCount);
|
|
942
|
+
material.setInt("uDestWidth", this._textureSize);
|
|
943
|
+
material.setInt("uSrcWidth", srcWidth);
|
|
944
|
+
}
|
|
945
|
+
_createShMaterial() {
|
|
946
|
+
const isWGSL = this._shaderLanguage === 1 /* ShaderLanguage.WGSL */;
|
|
947
|
+
const material = new ShaderMaterial(GaussianSplattingWorkBufferShDecodeShaderName, this._scene, {
|
|
948
|
+
vertexSource: isWGSL ? GaussianSplattingWorkBufferVertexShaderWGSL : GaussianSplattingWorkBufferVertexShaderGLSL,
|
|
949
|
+
fragmentSource: isWGSL ? GaussianSplattingWorkBufferShDecodeFragmentShaderWGSL : GaussianSplattingWorkBufferShDecodeFragmentShaderGLSL,
|
|
950
|
+
}, {
|
|
951
|
+
attributes: ["position"],
|
|
952
|
+
uniforms: ["sogShnMin", "sogShnMax", "uVersion", "uOffset", "uCount", "uDestWidth", "uSrcWidth", "uCoeffs", "uShTextureIndex"],
|
|
953
|
+
samplers: ["sogShLabelsTex", "sogShCentroidsTex", "sogCodebookTex"],
|
|
954
|
+
shaderLanguage: this._shaderLanguage,
|
|
955
|
+
});
|
|
956
|
+
material.backFaceCulling = false;
|
|
957
|
+
material.disableDepthWrite = true;
|
|
958
|
+
return material;
|
|
959
|
+
}
|
|
960
|
+
_applyShPack(pack) {
|
|
961
|
+
const material = this._shMaterial;
|
|
962
|
+
// A file may carry no higher-order SH (coarse LOD): fall back to sh0Texture as a harmless placeholder for the
|
|
963
|
+
// label/centroid samplers and set uCoeffs=0 so the shader neutral-fills every coefficient (128 -> 0 lighting).
|
|
964
|
+
const hasSh = !!pack.shLabelsTexture && !!pack.shCentroidsTexture;
|
|
965
|
+
const labels = pack.shLabelsTexture ?? pack.sh0Texture;
|
|
966
|
+
const centroids = pack.shCentroidsTexture ?? pack.sh0Texture;
|
|
967
|
+
material.setTexture("sogShLabelsTex", labels);
|
|
968
|
+
material.setTexture("sogShCentroidsTex", centroids);
|
|
969
|
+
// Codebook only used for v2; bind a harmless placeholder otherwise so the sampler is always set.
|
|
970
|
+
material.setTexture("sogCodebookTex", pack.codebookTexture ?? labels);
|
|
971
|
+
material.setFloat("sogShnMin", pack.shnMin ?? 0);
|
|
972
|
+
material.setFloat("sogShnMax", pack.shnMax ?? 0);
|
|
973
|
+
material.setInt("uVersion", pack.version);
|
|
974
|
+
// uOffset is bound at render time in decodeAsync (from the live _baseOffset).
|
|
975
|
+
material.setInt("uCount", pack.splatCount);
|
|
976
|
+
material.setInt("uDestWidth", this._textureSize);
|
|
977
|
+
material.setInt("uSrcWidth", labels.getSize().width);
|
|
978
|
+
// Higher-order coefficient count for THIS file (bands=3 -> 15). 0 when the file has no SH -> full neutral fill.
|
|
979
|
+
material.setInt("uCoeffs", hasSh ? pack.shCoeffCount : 0);
|
|
980
|
+
}
|
|
981
|
+
_createRotMaterial() {
|
|
982
|
+
const isWGSL = this._shaderLanguage === 1 /* ShaderLanguage.WGSL */;
|
|
983
|
+
const material = new ShaderMaterial(GaussianSplattingWorkBufferRotationDecodeShaderName, this._scene, {
|
|
984
|
+
vertexSource: isWGSL ? GaussianSplattingWorkBufferVertexShaderWGSL : GaussianSplattingWorkBufferVertexShaderGLSL,
|
|
985
|
+
fragmentSource: isWGSL ? GaussianSplattingWorkBufferRotationDecodeFragmentShaderWGSL : GaussianSplattingWorkBufferRotationDecodeFragmentShaderGLSL,
|
|
986
|
+
}, {
|
|
987
|
+
attributes: ["position"],
|
|
988
|
+
uniforms: ["sogScalesMin", "sogScalesMax", "uVersion", "uOffset", "uCount", "uDestWidth", "uSrcWidth"],
|
|
989
|
+
samplers: ["sogScalesTex", "sogQuatsTex", "sogCodebookTex"],
|
|
990
|
+
shaderLanguage: this._shaderLanguage,
|
|
991
|
+
});
|
|
992
|
+
material.backFaceCulling = false;
|
|
993
|
+
material.disableDepthWrite = true;
|
|
994
|
+
return material;
|
|
995
|
+
}
|
|
996
|
+
_applyRotPack(pack) {
|
|
997
|
+
const material = this._rotMaterial;
|
|
998
|
+
const srcWidth = pack.scalesTexture.getSize().width;
|
|
999
|
+
material.setTexture("sogScalesTex", pack.scalesTexture);
|
|
1000
|
+
material.setTexture("sogQuatsTex", pack.quatsTexture);
|
|
1001
|
+
// Codebook only used for v2; bind a harmless placeholder otherwise so the sampler is always set.
|
|
1002
|
+
material.setTexture("sogCodebookTex", pack.codebookTexture ?? pack.scalesTexture);
|
|
1003
|
+
const sMin = pack.scalesMin ?? [0, 0, 0];
|
|
1004
|
+
const sMax = pack.scalesMax ?? [0, 0, 0];
|
|
1005
|
+
material.setVector3("sogScalesMin", new Vector3(sMin[0], sMin[1], sMin[2]));
|
|
1006
|
+
material.setVector3("sogScalesMax", new Vector3(sMax[0], sMax[1], sMax[2]));
|
|
1007
|
+
material.setInt("uVersion", pack.version);
|
|
1008
|
+
// uOffset is bound at render time in decodeAsync (from the live _baseOffset).
|
|
361
1009
|
material.setInt("uCount", pack.splatCount);
|
|
362
1010
|
material.setInt("uDestWidth", this._textureSize);
|
|
363
1011
|
material.setInt("uSrcWidth", srcWidth);
|