@d5techs/3dgs-lib 1.4.84 → 1.4.86
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/dist/3dgs-lib.cjs +762 -175
- package/dist/3dgs-lib.cjs.map +1 -1
- package/dist/3dgs-lib.js +762 -175
- package/dist/3dgs-lib.js.map +1 -1
- package/dist/App.d.ts +20 -2
- package/dist/core/OrbitControls.d.ts +8 -0
- package/dist/gs/GSSplatRenderer.d.ts +30 -0
- package/dist/gs/GSSplatSorter.d.ts +12 -9
- package/dist/gs/PLYLoaderMobile.d.ts +6 -0
- package/dist/utils/DebugOverlay.d.ts +30 -0
- package/dist/utils/index.d.ts +1 -0
- package/package.json +1 -1
package/dist/3dgs-lib.cjs
CHANGED
|
@@ -38,8 +38,7 @@ function isMobileDevice() {
|
|
|
38
38
|
return isMobileUA || isIPadAsMac || hasTouch && isSmallScreen;
|
|
39
39
|
}
|
|
40
40
|
function getRecommendedDPR() {
|
|
41
|
-
const
|
|
42
|
-
const maxDpr = isMobile ? 2 : 2;
|
|
41
|
+
const maxDpr = isMobileDevice() ? 2 : 2;
|
|
43
42
|
return Math.min(window.devicePixelRatio || 1, maxDpr);
|
|
44
43
|
}
|
|
45
44
|
function isWebGPUSupported() {
|
|
@@ -144,6 +143,140 @@ function transformBoundingBox(bbox, modelMatrix) {
|
|
|
144
143
|
[maxX, maxY, maxZ]
|
|
145
144
|
);
|
|
146
145
|
}
|
|
146
|
+
class DebugOverlay {
|
|
147
|
+
constructor(maxEntries = 50) {
|
|
148
|
+
__publicField(this, "container");
|
|
149
|
+
__publicField(this, "logList");
|
|
150
|
+
__publicField(this, "maxEntries");
|
|
151
|
+
this.maxEntries = maxEntries;
|
|
152
|
+
this.container = document.createElement("div");
|
|
153
|
+
Object.assign(this.container.style, {
|
|
154
|
+
position: "fixed",
|
|
155
|
+
top: "0",
|
|
156
|
+
left: "0",
|
|
157
|
+
right: "0",
|
|
158
|
+
maxHeight: "40vh",
|
|
159
|
+
overflowY: "auto",
|
|
160
|
+
background: "rgba(0,0,0,0.85)",
|
|
161
|
+
color: "#0f0",
|
|
162
|
+
fontFamily: "monospace",
|
|
163
|
+
fontSize: "11px",
|
|
164
|
+
lineHeight: "1.4",
|
|
165
|
+
padding: "6px 8px",
|
|
166
|
+
zIndex: "99999",
|
|
167
|
+
pointerEvents: "auto",
|
|
168
|
+
whiteSpace: "pre-wrap",
|
|
169
|
+
wordBreak: "break-all"
|
|
170
|
+
});
|
|
171
|
+
const header = document.createElement("div");
|
|
172
|
+
Object.assign(header.style, {
|
|
173
|
+
display: "flex",
|
|
174
|
+
justifyContent: "space-between",
|
|
175
|
+
marginBottom: "4px",
|
|
176
|
+
borderBottom: "1px solid #333",
|
|
177
|
+
paddingBottom: "4px"
|
|
178
|
+
});
|
|
179
|
+
header.innerHTML = '<span style="color:#ff0;font-weight:bold">GPU Debug</span>';
|
|
180
|
+
const closeBtn = document.createElement("span");
|
|
181
|
+
closeBtn.textContent = "[X]";
|
|
182
|
+
closeBtn.style.color = "#f66";
|
|
183
|
+
closeBtn.style.cursor = "pointer";
|
|
184
|
+
closeBtn.onclick = () => this.hide();
|
|
185
|
+
header.appendChild(closeBtn);
|
|
186
|
+
this.logList = document.createElement("div");
|
|
187
|
+
this.container.appendChild(header);
|
|
188
|
+
this.container.appendChild(this.logList);
|
|
189
|
+
document.body.appendChild(this.container);
|
|
190
|
+
}
|
|
191
|
+
log(msg, level = "info") {
|
|
192
|
+
const colors = { info: "#0f0", warn: "#ff0", error: "#f44" };
|
|
193
|
+
const prefix = { info: "I", warn: "W", error: "E" };
|
|
194
|
+
const ts = (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", { hour12: false });
|
|
195
|
+
const entry = document.createElement("div");
|
|
196
|
+
entry.style.color = colors[level];
|
|
197
|
+
entry.textContent = `[${ts}][${prefix[level]}] ${msg}`;
|
|
198
|
+
this.logList.appendChild(entry);
|
|
199
|
+
while (this.logList.childElementCount > this.maxEntries) {
|
|
200
|
+
this.logList.removeChild(this.logList.firstChild);
|
|
201
|
+
}
|
|
202
|
+
this.container.scrollTop = this.container.scrollHeight;
|
|
203
|
+
}
|
|
204
|
+
info(msg) {
|
|
205
|
+
this.log(msg, "info");
|
|
206
|
+
}
|
|
207
|
+
warn(msg) {
|
|
208
|
+
this.log(msg, "warn");
|
|
209
|
+
}
|
|
210
|
+
error(msg) {
|
|
211
|
+
this.log(msg, "error");
|
|
212
|
+
}
|
|
213
|
+
hide() {
|
|
214
|
+
this.container.style.display = "none";
|
|
215
|
+
}
|
|
216
|
+
show() {
|
|
217
|
+
this.container.style.display = "block";
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* 绑定 WebGPU device 的全局错误捕获
|
|
221
|
+
*/
|
|
222
|
+
attachDevice(device) {
|
|
223
|
+
device.onuncapturederror = (event) => {
|
|
224
|
+
this.error(`GPU Uncaptured: ${event.error.message}`);
|
|
225
|
+
};
|
|
226
|
+
this.info(
|
|
227
|
+
`Device attached. maxBuf=${(device.limits.maxBufferSize / 1048576).toFixed(0)}MB, maxStorage=${(device.limits.maxStorageBufferBindingSize / 1048576).toFixed(0)}MB`
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* 检查 shader 编译结果
|
|
232
|
+
*/
|
|
233
|
+
async checkShader(module2, label) {
|
|
234
|
+
try {
|
|
235
|
+
const info = await module2.getCompilationInfo();
|
|
236
|
+
let hasError = false;
|
|
237
|
+
for (const msg of info.messages) {
|
|
238
|
+
const text = `Shader[${label}] ${msg.type}: L${msg.lineNum}:${msg.linePos} ${msg.message}`;
|
|
239
|
+
if (msg.type === "error") {
|
|
240
|
+
this.error(text);
|
|
241
|
+
hasError = true;
|
|
242
|
+
} else if (msg.type === "warning") {
|
|
243
|
+
this.warn(text);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (!hasError) {
|
|
247
|
+
this.info(`Shader[${label}] compiled OK`);
|
|
248
|
+
}
|
|
249
|
+
return !hasError;
|
|
250
|
+
} catch (e) {
|
|
251
|
+
this.error(`Shader[${label}] getCompilationInfo failed: ${e}`);
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* 包裹一个异步操作,捕获 validation/oom 错误
|
|
257
|
+
*/
|
|
258
|
+
async wrapGPU(device, label, fn) {
|
|
259
|
+
device.pushErrorScope("validation");
|
|
260
|
+
device.pushErrorScope("out-of-memory");
|
|
261
|
+
const result = fn();
|
|
262
|
+
const oomErr = await device.popErrorScope();
|
|
263
|
+
const valErr = await device.popErrorScope();
|
|
264
|
+
if (oomErr) this.error(`[${label}] OOM: ${oomErr.message}`);
|
|
265
|
+
if (valErr) this.error(`[${label}] Validation: ${valErr.message}`);
|
|
266
|
+
if (!oomErr && !valErr) this.info(`[${label}] OK`);
|
|
267
|
+
return result;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
let _instance = null;
|
|
271
|
+
function getDebugOverlay() {
|
|
272
|
+
return _instance;
|
|
273
|
+
}
|
|
274
|
+
function createDebugOverlay() {
|
|
275
|
+
if (!_instance) {
|
|
276
|
+
_instance = new DebugOverlay();
|
|
277
|
+
}
|
|
278
|
+
return _instance;
|
|
279
|
+
}
|
|
147
280
|
async function loadTextureFromURL(device, url) {
|
|
148
281
|
try {
|
|
149
282
|
const response = await fetch(url);
|
|
@@ -363,7 +496,16 @@ class Renderer {
|
|
|
363
496
|
}
|
|
364
497
|
});
|
|
365
498
|
this._device.lost.then((info) => {
|
|
499
|
+
const overlay2 = getDebugOverlay();
|
|
500
|
+
overlay2 == null ? void 0 : overlay2.error(`GPU device lost: ${info.reason} - ${info.message}`);
|
|
366
501
|
});
|
|
502
|
+
const overlay = getDebugOverlay();
|
|
503
|
+
if (overlay) {
|
|
504
|
+
overlay.attachDevice(this._device);
|
|
505
|
+
overlay.info(
|
|
506
|
+
`GPU: vendor=${this._gpuVendor || "unknown"}, arch=${this._gpuArchitecture || "unknown"}, apple=${this._isAppleGPU}, format=${navigator.gpu.getPreferredCanvasFormat()}`
|
|
507
|
+
);
|
|
508
|
+
}
|
|
367
509
|
this._context = this.canvas.getContext("webgpu");
|
|
368
510
|
if (!this._context) {
|
|
369
511
|
throw new Error("无法获取 WebGPU 上下文");
|
|
@@ -651,6 +793,7 @@ const _OrbitControls = class _OrbitControls {
|
|
|
651
793
|
// 键盘移动
|
|
652
794
|
__publicField(this, "moveSpeed", 0.015);
|
|
653
795
|
__publicField(this, "pressedKeys", /* @__PURE__ */ new Set());
|
|
796
|
+
__publicField(this, "_wasKeyboardMoving", false);
|
|
654
797
|
// 触摸手势状态
|
|
655
798
|
__publicField(this, "touchMode", "none");
|
|
656
799
|
__publicField(this, "lastTouchDistance", 0);
|
|
@@ -687,6 +830,9 @@ const _OrbitControls = class _OrbitControls {
|
|
|
687
830
|
this.setupEventListeners();
|
|
688
831
|
this.applySpherical();
|
|
689
832
|
}
|
|
833
|
+
get isInteracting() {
|
|
834
|
+
return this.isDragging;
|
|
835
|
+
}
|
|
690
836
|
setupEventListeners() {
|
|
691
837
|
this.canvas.addEventListener("mousedown", this.boundOnMouseDown);
|
|
692
838
|
this.canvas.addEventListener("mousemove", this.boundOnMouseMove);
|
|
@@ -824,7 +970,14 @@ const _OrbitControls = class _OrbitControls {
|
|
|
824
970
|
this.pressedKeys.delete(e.key.toLowerCase());
|
|
825
971
|
}
|
|
826
972
|
applyKeyboardMovement() {
|
|
827
|
-
if (this.pressedKeys.size === 0)
|
|
973
|
+
if (this.pressedKeys.size === 0) {
|
|
974
|
+
if (this._wasKeyboardMoving) {
|
|
975
|
+
this._wasKeyboardMoving = false;
|
|
976
|
+
this.recenterOrbitTarget();
|
|
977
|
+
}
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
this._wasKeyboardMoving = true;
|
|
828
981
|
const m = this.camera.viewMatrix;
|
|
829
982
|
const right = [m[0], m[4], m[8]];
|
|
830
983
|
const forward = [-m[2], -m[6], -m[10]];
|
|
@@ -984,9 +1137,15 @@ const _OrbitControls = class _OrbitControls {
|
|
|
984
1137
|
onTouchEnd(e) {
|
|
985
1138
|
if (e.touches.length === 0) {
|
|
986
1139
|
this.isDragging = false;
|
|
1140
|
+
if (this.touchMode === "zoom-pan") {
|
|
1141
|
+
this.recenterOrbitTarget();
|
|
1142
|
+
}
|
|
987
1143
|
this.touchMode = "none";
|
|
988
1144
|
this.lastTouchDistance = 0;
|
|
989
1145
|
} else if (e.touches.length === 1) {
|
|
1146
|
+
if (this.touchMode === "zoom-pan") {
|
|
1147
|
+
this.recenterOrbitTarget();
|
|
1148
|
+
}
|
|
990
1149
|
this.touchMode = "rotate";
|
|
991
1150
|
this.lastX = e.touches[0].clientX;
|
|
992
1151
|
this.lastY = e.touches[0].clientY;
|
|
@@ -1003,6 +1162,34 @@ const _OrbitControls = class _OrbitControls {
|
|
|
1003
1162
|
y: (touches[0].clientY + touches[1].clientY) / 2
|
|
1004
1163
|
};
|
|
1005
1164
|
}
|
|
1165
|
+
/**
|
|
1166
|
+
* 将 orbit 目标重新锚定到屏幕中心的模型表面点,
|
|
1167
|
+
* 保持相机世界坐标不变,仅重算 distance/theta/phi。
|
|
1168
|
+
* 用于 WASD 移动或触摸缩放结束后修复旋转中心偏移。
|
|
1169
|
+
*/
|
|
1170
|
+
recenterOrbitTarget() {
|
|
1171
|
+
if (!this.pickWorldPosition) return;
|
|
1172
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
1173
|
+
const hit = this.pickWorldPosition(
|
|
1174
|
+
rect.left + rect.width / 2,
|
|
1175
|
+
rect.top + rect.height / 2
|
|
1176
|
+
);
|
|
1177
|
+
if (!hit) return;
|
|
1178
|
+
const dx = this.camera.position[0] - hit[0];
|
|
1179
|
+
const dy = this.camera.position[1] - hit[1];
|
|
1180
|
+
const dz = this.camera.position[2] - hit[2];
|
|
1181
|
+
const newDist = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
1182
|
+
if (newDist < this.minDistance) return;
|
|
1183
|
+
this.camera.target[0] = hit[0];
|
|
1184
|
+
this.camera.target[1] = hit[1];
|
|
1185
|
+
this.camera.target[2] = hit[2];
|
|
1186
|
+
this.distance = newDist;
|
|
1187
|
+
this.theta = Math.atan2(dx, dz);
|
|
1188
|
+
this.phi = Math.acos(Math.min(1, Math.max(-1, dy / newDist)));
|
|
1189
|
+
this.deltaPanX = 0;
|
|
1190
|
+
this.deltaPanY = 0;
|
|
1191
|
+
this.deltaPanZ = 0;
|
|
1192
|
+
}
|
|
1006
1193
|
/**
|
|
1007
1194
|
* 将球坐标写入相机位置(内部方法,不处理阻尼)
|
|
1008
1195
|
*/
|
|
@@ -5231,9 +5418,10 @@ function compactDataToGPUBuffer(data, includeFullSH = false) {
|
|
|
5231
5418
|
}
|
|
5232
5419
|
return buffer;
|
|
5233
5420
|
} else {
|
|
5234
|
-
const
|
|
5421
|
+
const COMPACT_FLOATS = 16;
|
|
5422
|
+
const buffer = new Float32Array(count * COMPACT_FLOATS);
|
|
5235
5423
|
for (let i = 0; i < count; i++) {
|
|
5236
|
-
const offset = i *
|
|
5424
|
+
const offset = i * COMPACT_FLOATS;
|
|
5237
5425
|
buffer[offset + 0] = data.positions[i * 3 + 0];
|
|
5238
5426
|
buffer[offset + 1] = data.positions[i * 3 + 1];
|
|
5239
5427
|
buffer[offset + 2] = data.positions[i * 3 + 2];
|
|
@@ -5254,9 +5442,56 @@ function compactDataToGPUBuffer(data, includeFullSH = false) {
|
|
|
5254
5442
|
return buffer;
|
|
5255
5443
|
}
|
|
5256
5444
|
}
|
|
5445
|
+
const _f16Scratch = new Float32Array(1);
|
|
5446
|
+
const _f16ScratchU32 = new Uint32Array(_f16Scratch.buffer);
|
|
5447
|
+
function f32ToF16Bits(val) {
|
|
5448
|
+
_f16Scratch[0] = val;
|
|
5449
|
+
const bits2 = _f16ScratchU32[0];
|
|
5450
|
+
const sign = bits2 >>> 31 & 1;
|
|
5451
|
+
let exp = bits2 >>> 23 & 255;
|
|
5452
|
+
let frac = bits2 & 8388607;
|
|
5453
|
+
if (exp === 255) {
|
|
5454
|
+
return sign << 15 | 31744 | (frac ? 512 : 0);
|
|
5455
|
+
}
|
|
5456
|
+
exp = exp - 127 + 15;
|
|
5457
|
+
if (exp >= 31) {
|
|
5458
|
+
return sign << 15 | 31744;
|
|
5459
|
+
}
|
|
5460
|
+
if (exp <= 0) {
|
|
5461
|
+
if (exp < -10) return sign << 15;
|
|
5462
|
+
frac = (frac | 8388608) >> 1 - exp;
|
|
5463
|
+
return sign << 15 | frac >> 13;
|
|
5464
|
+
}
|
|
5465
|
+
return sign << 15 | exp << 10 | frac >> 13;
|
|
5466
|
+
}
|
|
5467
|
+
function compactDataToGPUBufferHalf(data) {
|
|
5468
|
+
const count = data.count;
|
|
5469
|
+
const U32_PER_SPLAT = 8;
|
|
5470
|
+
const buffer = new Uint32Array(count * U32_PER_SPLAT);
|
|
5471
|
+
const pos = data.positions;
|
|
5472
|
+
const scl = data.scales;
|
|
5473
|
+
const rot = data.rotations;
|
|
5474
|
+
const col = data.colors;
|
|
5475
|
+
const opa = data.opacities;
|
|
5476
|
+
for (let i = 0; i < count; i++) {
|
|
5477
|
+
const o = i * U32_PER_SPLAT;
|
|
5478
|
+
const i3 = i * 3;
|
|
5479
|
+
const i4 = i * 4;
|
|
5480
|
+
buffer[o] = f32ToF16Bits(pos[i3 + 1]) << 16 | f32ToF16Bits(pos[i3]);
|
|
5481
|
+
buffer[o + 1] = f32ToF16Bits(pos[i3 + 2]);
|
|
5482
|
+
buffer[o + 2] = f32ToF16Bits(scl[i3 + 1]) << 16 | f32ToF16Bits(scl[i3]);
|
|
5483
|
+
buffer[o + 3] = f32ToF16Bits(opa[i]) << 16 | f32ToF16Bits(scl[i3 + 2]);
|
|
5484
|
+
buffer[o + 4] = f32ToF16Bits(rot[i4 + 1]) << 16 | f32ToF16Bits(rot[i4]);
|
|
5485
|
+
buffer[o + 5] = f32ToF16Bits(rot[i4 + 3]) << 16 | f32ToF16Bits(rot[i4 + 2]);
|
|
5486
|
+
buffer[o + 6] = f32ToF16Bits(col[i3 + 1]) << 16 | f32ToF16Bits(col[i3]);
|
|
5487
|
+
buffer[o + 7] = f32ToF16Bits(col[i3 + 2]);
|
|
5488
|
+
}
|
|
5489
|
+
return buffer;
|
|
5490
|
+
}
|
|
5257
5491
|
const PLYLoaderMobile = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
5258
5492
|
__proto__: null,
|
|
5259
5493
|
compactDataToGPUBuffer,
|
|
5494
|
+
compactDataToGPUBufferHalf,
|
|
5260
5495
|
loadPLYMobile,
|
|
5261
5496
|
parsePLYBuffer,
|
|
5262
5497
|
transformSHCoeffsYZSwap
|
|
@@ -7316,16 +7551,38 @@ const RADIX_BITS = 8;
|
|
|
7316
7551
|
const RADIX_SIZE = 256;
|
|
7317
7552
|
const ELEMENTS_PER_THREAD = 4;
|
|
7318
7553
|
const BLOCK_SIZE = WORKGROUP_SIZE$1 * ELEMENTS_PER_THREAD;
|
|
7319
|
-
function generateCullingShaderCode$1() {
|
|
7320
|
-
|
|
7321
|
-
|
|
7322
|
-
|
|
7323
|
-
|
|
7324
|
-
|
|
7325
|
-
|
|
7326
|
-
|
|
7327
|
-
|
|
7328
|
-
|
|
7554
|
+
function generateCullingShaderCode$1(compact = false, half = false, sortBits = 32) {
|
|
7555
|
+
let splatBinding;
|
|
7556
|
+
let splatAccessCode;
|
|
7557
|
+
if (half) {
|
|
7558
|
+
splatBinding = `@group(0) @binding(0) var<storage, read> splatDataU32: array<u32>;`;
|
|
7559
|
+
splatAccessCode = `
|
|
7560
|
+
const HALF_U32S: u32 = 8u;
|
|
7561
|
+
fn getSplatMean(idx: u32) -> vec3<f32> {
|
|
7562
|
+
let b = idx * HALF_U32S;
|
|
7563
|
+
let xy = unpack2x16float(splatDataU32[b]);
|
|
7564
|
+
let zp = unpack2x16float(splatDataU32[b + 1u]);
|
|
7565
|
+
return vec3<f32>(xy.x, xy.y, zp.x);
|
|
7566
|
+
}
|
|
7567
|
+
fn getSplatScale(idx: u32) -> vec3<f32> {
|
|
7568
|
+
let b = idx * HALF_U32S;
|
|
7569
|
+
let xy = unpack2x16float(splatDataU32[b + 2u]);
|
|
7570
|
+
return vec3<f32>(xy.x, xy.y, unpack2x16float(splatDataU32[b + 3u]).x);
|
|
7571
|
+
}
|
|
7572
|
+
fn getSplatOpacity(idx: u32) -> f32 {
|
|
7573
|
+
let b = idx * HALF_U32S;
|
|
7574
|
+
return unpack2x16float(splatDataU32[b + 3u]).y;
|
|
7575
|
+
}`;
|
|
7576
|
+
} else {
|
|
7577
|
+
const splatStruct = compact ? `struct Splat {
|
|
7578
|
+
mean: vec3<f32>,
|
|
7579
|
+
_pad0: f32,
|
|
7580
|
+
scale: vec3<f32>,
|
|
7581
|
+
_pad1: f32,
|
|
7582
|
+
rotation: vec4<f32>,
|
|
7583
|
+
colorDC: vec3<f32>,
|
|
7584
|
+
opacity: f32,
|
|
7585
|
+
}` : `struct Splat {
|
|
7329
7586
|
mean: vec3<f32>,
|
|
7330
7587
|
_pad0: f32,
|
|
7331
7588
|
scale: vec3<f32>,
|
|
@@ -7337,7 +7594,24 @@ struct Splat {
|
|
|
7337
7594
|
sh2: array<f32, 15>,
|
|
7338
7595
|
sh3: array<f32, 21>,
|
|
7339
7596
|
_pad2: array<f32, 3>,
|
|
7340
|
-
}
|
|
7597
|
+
}`;
|
|
7598
|
+
splatBinding = `${splatStruct}
|
|
7599
|
+
@group(0) @binding(0) var<storage, read> splats: array<Splat>;`;
|
|
7600
|
+
splatAccessCode = `
|
|
7601
|
+
fn getSplatMean(idx: u32) -> vec3<f32> { return splats[idx].mean; }
|
|
7602
|
+
fn getSplatScale(idx: u32) -> vec3<f32> { return splats[idx].scale; }
|
|
7603
|
+
fn getSplatOpacity(idx: u32) -> f32 { return splats[idx].opacity; }`;
|
|
7604
|
+
}
|
|
7605
|
+
return (
|
|
7606
|
+
/* wgsl */
|
|
7607
|
+
`
|
|
7608
|
+
/**
|
|
7609
|
+
* Project & Cull Shader
|
|
7610
|
+
* 基于 rfs-gsplat-render 实现
|
|
7611
|
+
*/
|
|
7612
|
+
|
|
7613
|
+
${splatBinding}
|
|
7614
|
+
${splatAccessCode}
|
|
7341
7615
|
|
|
7342
7616
|
struct CameraUniforms {
|
|
7343
7617
|
view: mat4x4<f32>,
|
|
@@ -7357,10 +7631,9 @@ struct CullingParams {
|
|
|
7357
7631
|
pixelThreshold: f32,
|
|
7358
7632
|
maxVisibleCount: u32,
|
|
7359
7633
|
depthRangeLimit: f32,
|
|
7360
|
-
|
|
7634
|
+
lodSkipRate: f32,
|
|
7361
7635
|
}
|
|
7362
7636
|
|
|
7363
|
-
@group(0) @binding(0) var<storage, read> splats: array<Splat>;
|
|
7364
7637
|
@group(0) @binding(1) var<uniform> camera: CameraUniforms;
|
|
7365
7638
|
@group(0) @binding(2) var<uniform> params: CullingParams;
|
|
7366
7639
|
@group(0) @binding(3) var<storage, read_write> depthKeys: array<u32>;
|
|
@@ -7378,16 +7651,12 @@ fn getModelMaxScale(model: mat4x4<f32>) -> f32 {
|
|
|
7378
7651
|
return max(max(sx, sy), sz);
|
|
7379
7652
|
}
|
|
7380
7653
|
|
|
7381
|
-
// IEEE 754 位操作编码浮点数为可排序的 u32
|
|
7382
|
-
// 参考 rfs-gsplat-render 的 encode_min_max_fp32 实现
|
|
7383
7654
|
fn encodeDepthKey(val: f32) -> u32 {
|
|
7384
7655
|
var bits = bitcast<u32>(val);
|
|
7385
7656
|
bits ^= bitcast<u32>(bitcast<i32>(bits) >> 31) | 0x80000000u;
|
|
7386
|
-
return bits;
|
|
7657
|
+
return bits >> ${sortBits === 16 ? "16u" : "0u"};
|
|
7387
7658
|
}
|
|
7388
7659
|
|
|
7389
|
-
// 视锥剔除检查
|
|
7390
|
-
// 基于 rfs-gsplat-render 的 is_in_frustum 实现
|
|
7391
7660
|
fn isInFrustum(clipPos: vec4<f32>, frustumDilation: f32) -> bool {
|
|
7392
7661
|
let clip = (1.0 + frustumDilation) * clipPos.w;
|
|
7393
7662
|
|
|
@@ -7406,43 +7675,47 @@ fn isInFrustum(clipPos: vec4<f32>, frustumDilation: f32) -> bool {
|
|
|
7406
7675
|
fn projectAndCull(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
7407
7676
|
let i = gid.x;
|
|
7408
7677
|
if i >= params.splatCount { return; }
|
|
7678
|
+
|
|
7679
|
+
// LOD 抽稀放最前面:跳过的 splat 不做任何数据加载和矩阵运算
|
|
7680
|
+
// 哈希仅依赖 splat index → 被跳过的集合永远不变 → 零闪烁
|
|
7681
|
+
if params.lodSkipRate > 0.0 {
|
|
7682
|
+
let hash = ((i * 2654435761u) >> 16u) & 0xFFFFu;
|
|
7683
|
+
if f32(hash) < params.lodSkipRate * 65535.0 {
|
|
7684
|
+
return;
|
|
7685
|
+
}
|
|
7686
|
+
}
|
|
7409
7687
|
|
|
7410
|
-
let
|
|
7688
|
+
let splatMean = getSplatMean(i);
|
|
7689
|
+
let splatScale = getSplatScale(i);
|
|
7690
|
+
let splatOpacity = getSplatOpacity(i);
|
|
7411
7691
|
|
|
7412
|
-
|
|
7413
|
-
if splat.opacity < 0.004 { return; }
|
|
7692
|
+
if splatOpacity < 0.004 { return; }
|
|
7414
7693
|
|
|
7415
|
-
|
|
7416
|
-
let worldPos = camera.model * vec4<f32>(splat.mean, 1.0);
|
|
7694
|
+
let worldPos = camera.model * vec4<f32>(splatMean, 1.0);
|
|
7417
7695
|
let viewPos = camera.view * worldPos;
|
|
7418
7696
|
let clipPos = camera.proj * viewPos;
|
|
7419
7697
|
|
|
7420
|
-
// 视锥剔除
|
|
7421
7698
|
if !isInFrustum(clipPos, params.frustumDilation) { return; }
|
|
7422
7699
|
|
|
7423
|
-
|
|
7424
|
-
|
|
7425
|
-
|
|
7426
|
-
|
|
7427
|
-
|
|
7428
|
-
|
|
7429
|
-
|
|
7430
|
-
|
|
7700
|
+
let splatSigma = maxScale(splatScale) * getModelMaxScale(camera.model);
|
|
7701
|
+
let focalY = abs(camera.proj[1][1]) * params.screenHeight * 0.5;
|
|
7702
|
+
let projectedExtent = splatSigma * 3.0 * focalY / max(abs(viewPos.z), 0.001);
|
|
7703
|
+
|
|
7704
|
+
// 剔除投影尺寸过大的 splat(远离模型时去除周围遮挡物)
|
|
7705
|
+
if projectedExtent > params.screenHeight * 0.5 { return; }
|
|
7706
|
+
|
|
7707
|
+
// 剔除投影尺寸过小的 splat
|
|
7708
|
+
if params.pixelThreshold > 0.0 && projectedExtent < params.pixelThreshold { return; }
|
|
7431
7709
|
|
|
7432
|
-
// 深度范围限制:只渲染距相机一定深度范围内的 splat
|
|
7433
7710
|
if params.depthRangeLimit > 0.0 {
|
|
7434
7711
|
if abs(viewPos.z) > params.depthRangeLimit { return; }
|
|
7435
7712
|
}
|
|
7436
7713
|
|
|
7437
|
-
// 深度编码 (viewPos.z 是负数)
|
|
7438
7714
|
let depth = viewPos.z;
|
|
7439
7715
|
let sortableDepth = encodeDepthKey(depth);
|
|
7440
7716
|
|
|
7441
|
-
// 原子增加可见计数并获取索引
|
|
7442
|
-
// indirectBuffer[1] 是 instance_count
|
|
7443
7717
|
let visibleIdx = atomicAdd(&indirectBuffer[1], 1u);
|
|
7444
7718
|
|
|
7445
|
-
// 写入可见点列表
|
|
7446
7719
|
depthKeys[visibleIdx] = sortableDepth;
|
|
7447
7720
|
visibleIndices[visibleIdx] = i;
|
|
7448
7721
|
}
|
|
@@ -7757,7 +8030,7 @@ fn downsweep(
|
|
|
7757
8030
|
);
|
|
7758
8031
|
}
|
|
7759
8032
|
class GSSplatSorter {
|
|
7760
|
-
constructor(device, splatCount, splatBuffer, cameraBuffer, _options = {}) {
|
|
8033
|
+
constructor(device, splatCount, splatBuffer, cameraBuffer, _options = {}, compact = false, half = false, sortBits = 32) {
|
|
7761
8034
|
__publicField(this, "device");
|
|
7762
8035
|
__publicField(this, "splatCount");
|
|
7763
8036
|
// Culling Buffers
|
|
@@ -7787,11 +8060,12 @@ class GSSplatSorter {
|
|
|
7787
8060
|
__publicField(this, "upsweepBindGroupLayout");
|
|
7788
8061
|
__publicField(this, "spineBindGroupLayout");
|
|
7789
8062
|
__publicField(this, "downsweepBindGroupLayout");
|
|
7790
|
-
// Bind groups for each pass
|
|
8063
|
+
// Bind groups for each pass
|
|
7791
8064
|
__publicField(this, "upsweepBindGroups", []);
|
|
7792
8065
|
__publicField(this, "spineBindGroups", []);
|
|
7793
8066
|
__publicField(this, "downsweepBindGroups", []);
|
|
7794
8067
|
__publicField(this, "numPartitions");
|
|
8068
|
+
__publicField(this, "numSortPasses");
|
|
7795
8069
|
// 屏幕信息和剔除选项
|
|
7796
8070
|
__publicField(this, "screenWidth", 1920);
|
|
7797
8071
|
__publicField(this, "screenHeight", 1080);
|
|
@@ -7804,14 +8078,19 @@ class GSSplatSorter {
|
|
|
7804
8078
|
this.device = device;
|
|
7805
8079
|
this.splatCount = splatCount;
|
|
7806
8080
|
this.numPartitions = Math.ceil(splatCount / BLOCK_SIZE);
|
|
8081
|
+
this.numSortPasses = sortBits / RADIX_BITS;
|
|
8082
|
+
const dbg = getDebugOverlay();
|
|
8083
|
+
dbg == null ? void 0 : dbg.info(`Sorter init: ${splatCount} splats, compact=${compact}, half=${half}, sortBits=${sortBits}`);
|
|
7807
8084
|
const cullingModule = device.createShaderModule({
|
|
7808
|
-
code: generateCullingShaderCode$1(),
|
|
8085
|
+
code: generateCullingShaderCode$1(compact, half, sortBits),
|
|
7809
8086
|
label: "culling-shader"
|
|
7810
8087
|
});
|
|
8088
|
+
dbg == null ? void 0 : dbg.checkShader(cullingModule, "culling");
|
|
7811
8089
|
const radixSortModule = device.createShaderModule({
|
|
7812
8090
|
code: generateRadixSortShaderCode(),
|
|
7813
8091
|
label: "radix-sort-shader"
|
|
7814
8092
|
});
|
|
8093
|
+
dbg == null ? void 0 : dbg.checkShader(radixSortModule, "radix-sort");
|
|
7815
8094
|
this.cullingParamsBuffer = device.createBuffer({
|
|
7816
8095
|
size: 48,
|
|
7817
8096
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
@@ -7829,12 +8108,11 @@ class GSSplatSorter {
|
|
|
7829
8108
|
});
|
|
7830
8109
|
this.indirectBuffer = device.createBuffer({
|
|
7831
8110
|
size: 16,
|
|
7832
|
-
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.INDIRECT | GPUBufferUsage.COPY_DST,
|
|
8111
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.INDIRECT | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
|
|
7833
8112
|
label: "indirect-buffer"
|
|
7834
8113
|
});
|
|
7835
8114
|
this.globalHistogramBuffer = device.createBuffer({
|
|
7836
|
-
size: RADIX_SIZE *
|
|
7837
|
-
// 4 passes * 256 bins * 4 bytes
|
|
8115
|
+
size: RADIX_SIZE * this.numSortPasses * 4,
|
|
7838
8116
|
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
7839
8117
|
label: "global-histogram"
|
|
7840
8118
|
});
|
|
@@ -7853,7 +8131,7 @@ class GSSplatSorter {
|
|
|
7853
8131
|
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
7854
8132
|
label: "values-temp"
|
|
7855
8133
|
});
|
|
7856
|
-
for (let i = 0; i <
|
|
8134
|
+
for (let i = 0; i < this.numSortPasses; i++) {
|
|
7857
8135
|
const paramsBuffer = device.createBuffer({
|
|
7858
8136
|
size: 16,
|
|
7859
8137
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
@@ -7965,16 +8243,12 @@ class GSSplatSorter {
|
|
|
7965
8243
|
}
|
|
7966
8244
|
/**
|
|
7967
8245
|
* 创建 Radix Sort 的 bind groups
|
|
7968
|
-
*
|
|
7969
|
-
*
|
|
7970
|
-
* Ping-pong 模式:
|
|
7971
|
-
* - Pass 0: depthKeys/visibleIndices -> keysTempBuffer/valuesTempBuffer
|
|
7972
|
-
* - Pass 1: keysTempBuffer/valuesTempBuffer -> depthKeys/visibleIndices
|
|
7973
|
-
* - Pass 2: depthKeys/visibleIndices -> keysTempBuffer/valuesTempBuffer
|
|
7974
|
-
* - Pass 3: keysTempBuffer/valuesTempBuffer -> (depthKeys)/sortedIndicesBuffer
|
|
8246
|
+
* numSortPasses 个 pass,使用 ping-pong buffers
|
|
8247
|
+
* 最后一个 pass 的 values 输出到 sortedIndicesBuffer
|
|
7975
8248
|
*/
|
|
7976
8249
|
createRadixSortBindGroups() {
|
|
7977
|
-
|
|
8250
|
+
const lastPassIdx = this.numSortPasses - 1;
|
|
8251
|
+
for (let passIdx = 0; passIdx < this.numSortPasses; passIdx++) {
|
|
7978
8252
|
const isEvenPass = passIdx % 2 === 0;
|
|
7979
8253
|
const keysIn = isEvenPass ? this.depthKeysBuffer : this.keysTempBuffer;
|
|
7980
8254
|
const valuesIn = isEvenPass ? this.visibleIndicesBuffer : this.valuesTempBuffer;
|
|
@@ -7982,10 +8256,10 @@ class GSSplatSorter {
|
|
|
7982
8256
|
let valuesOut;
|
|
7983
8257
|
if (isEvenPass) {
|
|
7984
8258
|
keysOut = this.keysTempBuffer;
|
|
7985
|
-
valuesOut = this.valuesTempBuffer;
|
|
8259
|
+
valuesOut = passIdx === lastPassIdx ? this.sortedIndicesBuffer : this.valuesTempBuffer;
|
|
7986
8260
|
} else {
|
|
7987
8261
|
keysOut = this.depthKeysBuffer;
|
|
7988
|
-
valuesOut = passIdx ===
|
|
8262
|
+
valuesOut = passIdx === lastPassIdx ? this.sortedIndicesBuffer : this.visibleIndicesBuffer;
|
|
7989
8263
|
}
|
|
7990
8264
|
this.upsweepBindGroups[passIdx] = this.device.createBindGroup({
|
|
7991
8265
|
layout: this.upsweepBindGroupLayout,
|
|
@@ -8053,16 +8327,15 @@ class GSSplatSorter {
|
|
|
8053
8327
|
view.setFloat32(24, this.cullingOptions.pixelThreshold, true);
|
|
8054
8328
|
view.setUint32(28, this.cullingOptions.maxVisibleCount ?? 0, true);
|
|
8055
8329
|
view.setFloat32(32, this.cullingOptions.depthRangeLimit ?? 0, true);
|
|
8330
|
+
view.setFloat32(36, this.cullingOptions.lodSkipRate ?? 0, true);
|
|
8056
8331
|
this.device.queue.writeBuffer(this.cullingParamsBuffer, 0, cullingParamsData);
|
|
8332
|
+
this.device.queue.writeBuffer(
|
|
8333
|
+
this.indirectBuffer,
|
|
8334
|
+
0,
|
|
8335
|
+
new Uint32Array([4, 0, 0, 0])
|
|
8336
|
+
);
|
|
8057
8337
|
const encoder = this.device.createCommandEncoder({ label: "splat-sort-encoder" });
|
|
8058
8338
|
encoder.clearBuffer(this.globalHistogramBuffer);
|
|
8059
|
-
{
|
|
8060
|
-
const pass = encoder.beginComputePass({ label: "init-indirect" });
|
|
8061
|
-
pass.setPipeline(this.initIndirectPipeline);
|
|
8062
|
-
pass.setBindGroup(0, this.cullingBindGroup);
|
|
8063
|
-
pass.dispatchWorkgroups(1);
|
|
8064
|
-
pass.end();
|
|
8065
|
-
}
|
|
8066
8339
|
{
|
|
8067
8340
|
const pass = encoder.beginComputePass({ label: "project-cull" });
|
|
8068
8341
|
pass.setPipeline(this.projectCullPipeline);
|
|
@@ -8070,7 +8343,7 @@ class GSSplatSorter {
|
|
|
8070
8343
|
pass.dispatchWorkgroups(Math.ceil(this.splatCount / WORKGROUP_SIZE$1));
|
|
8071
8344
|
pass.end();
|
|
8072
8345
|
}
|
|
8073
|
-
for (let passIdx = 0; passIdx <
|
|
8346
|
+
for (let passIdx = 0; passIdx < this.numSortPasses; passIdx++) {
|
|
8074
8347
|
{
|
|
8075
8348
|
const pass = encoder.beginComputePass({ label: `upsweep-p${passIdx}` });
|
|
8076
8349
|
pass.setPipeline(this.upsweepPipeline);
|
|
@@ -8114,6 +8387,24 @@ class GSSplatSorter {
|
|
|
8114
8387
|
getDrawIndirectBuffer() {
|
|
8115
8388
|
return this.indirectBuffer;
|
|
8116
8389
|
}
|
|
8390
|
+
/**
|
|
8391
|
+
* 异步读回 drawIndirect buffer 内容(调试用)
|
|
8392
|
+
* 返回 [vertexCount, instanceCount, firstVertex, firstInstance]
|
|
8393
|
+
*/
|
|
8394
|
+
async readbackIndirect() {
|
|
8395
|
+
const staging = this.device.createBuffer({
|
|
8396
|
+
size: 16,
|
|
8397
|
+
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
|
|
8398
|
+
});
|
|
8399
|
+
const encoder = this.device.createCommandEncoder();
|
|
8400
|
+
encoder.copyBufferToBuffer(this.indirectBuffer, 0, staging, 0, 16);
|
|
8401
|
+
this.device.queue.submit([encoder.finish()]);
|
|
8402
|
+
await staging.mapAsync(GPUMapMode.READ);
|
|
8403
|
+
const result = new Uint32Array(staging.getMappedRange().slice(0));
|
|
8404
|
+
staging.unmap();
|
|
8405
|
+
staging.destroy();
|
|
8406
|
+
return result;
|
|
8407
|
+
}
|
|
8117
8408
|
/**
|
|
8118
8409
|
* 获取 splat 总数量
|
|
8119
8410
|
*/
|
|
@@ -8361,10 +8652,6 @@ fn computeExtentBasisAA(cov2dIn: vec3<f32>, opacity: f32, viewportSize: vec2<f32
|
|
|
8361
8652
|
// 使用基于视口的最大限制 (匹配 PlayCanvas)
|
|
8362
8653
|
let vmin = min(1024.0, min(viewportSize.x, viewportSize.y));
|
|
8363
8654
|
|
|
8364
|
-
// 计算轴长度: l = 2 * sqrt(2 * lambda) ≈ 2.83 * sqrt(lambda)
|
|
8365
|
-
// 这与 GAUSSIAN_K=4 配套使用:
|
|
8366
|
-
// 在 UV=1 边界,对应 2*sqrt(2) 个标准差的位置
|
|
8367
|
-
// exp(-4 * 1) = exp(-4) ≈ 0.018,Normalized 后精确为 0
|
|
8368
8655
|
let l1 = min(2.0 * sqrt(2.0 * lambda1), vmin);
|
|
8369
8656
|
let l2 = min(2.0 * sqrt(2.0 * lambda2), vmin);
|
|
8370
8657
|
|
|
@@ -8381,7 +8668,6 @@ fn computeExtentBasisAA(cov2dIn: vec3<f32>, opacity: f32, viewportSize: vec2<f32
|
|
|
8381
8668
|
let eigenvector1 = diagVec;
|
|
8382
8669
|
let eigenvector2 = vec2<f32>(diagVec.y, -diagVec.x);
|
|
8383
8670
|
|
|
8384
|
-
// 计算基向量 (不应用额外的 splat_scale,因为我们使用默认值 1.0)
|
|
8385
8671
|
result.basis = vec4<f32>(eigenvector1 * l1, eigenvector2 * l2);
|
|
8386
8672
|
result.adjustedOpacity = alpha;
|
|
8387
8673
|
return result;
|
|
@@ -8775,7 +9061,79 @@ fn fs_depth_normal(input: VertexOutput) -> FragOutput {
|
|
|
8775
9061
|
}
|
|
8776
9062
|
`
|
|
8777
9063
|
);
|
|
9064
|
+
const SPLAT_BYTE_SIZE = 256;
|
|
8778
9065
|
const SPLAT_FLOAT_COUNT = 64;
|
|
9066
|
+
const COMPACT_SPLAT_BYTE_SIZE = 64;
|
|
9067
|
+
const COMPACT_SPLAT_FLOAT_COUNT = 16;
|
|
9068
|
+
const HALF_SPLAT_BYTE_SIZE = 32;
|
|
9069
|
+
const HALF_SPLAT_U32_COUNT = 8;
|
|
9070
|
+
function transformShaderForCompact(code) {
|
|
9071
|
+
code = code.replace(
|
|
9072
|
+
/struct Splat \{[\s\S]*?\n\}/,
|
|
9073
|
+
`struct Splat {
|
|
9074
|
+
mean: vec3<f32>, _pad0: f32,
|
|
9075
|
+
scale: vec3<f32>, _pad1: f32,
|
|
9076
|
+
rotation: vec4<f32>,
|
|
9077
|
+
colorDC: vec3<f32>,
|
|
9078
|
+
opacity: f32,
|
|
9079
|
+
}`
|
|
9080
|
+
);
|
|
9081
|
+
code = code.replace(
|
|
9082
|
+
/fn evalSH\(splat: Splat, dir: vec3<f32>\) -> vec3<f32> \{[\s\S]*?\n\}/,
|
|
9083
|
+
`fn evalSH(splat: Splat, dir: vec3<f32>) -> vec3<f32> {
|
|
9084
|
+
return vec3<f32>(0.0);
|
|
9085
|
+
}`
|
|
9086
|
+
);
|
|
9087
|
+
return code;
|
|
9088
|
+
}
|
|
9089
|
+
function transformShaderForHalf(code) {
|
|
9090
|
+
code = code.replace(
|
|
9091
|
+
/struct Splat \{[\s\S]*?\n\}/,
|
|
9092
|
+
`struct Splat {
|
|
9093
|
+
mean: vec3<f32>, _pad0: f32,
|
|
9094
|
+
scale: vec3<f32>, _pad1: f32,
|
|
9095
|
+
rotation: vec4<f32>,
|
|
9096
|
+
colorDC: vec3<f32>,
|
|
9097
|
+
opacity: f32,
|
|
9098
|
+
}`
|
|
9099
|
+
);
|
|
9100
|
+
code = code.replace(
|
|
9101
|
+
/@group\(0\)\s*@binding\((\d+)\)\s*var<storage,\s*read>\s*splats\s*:\s*array<Splat>/,
|
|
9102
|
+
`@group(0) @binding($1) var<storage, read> splatDataU32: array<u32>`
|
|
9103
|
+
);
|
|
9104
|
+
const unpackFunctions = `
|
|
9105
|
+
const HALF_U32S: u32 = 8u;
|
|
9106
|
+
fn unpackSplatFromHalf(idx: u32) -> Splat {
|
|
9107
|
+
let b = idx * HALF_U32S;
|
|
9108
|
+
let mean_xy = unpack2x16float(splatDataU32[b]);
|
|
9109
|
+
let mean_zp = unpack2x16float(splatDataU32[b + 1u]);
|
|
9110
|
+
let sc_xy = unpack2x16float(splatDataU32[b + 2u]);
|
|
9111
|
+
let sc_z_op = unpack2x16float(splatDataU32[b + 3u]);
|
|
9112
|
+
let r_xy = unpack2x16float(splatDataU32[b + 4u]);
|
|
9113
|
+
let r_zw = unpack2x16float(splatDataU32[b + 5u]);
|
|
9114
|
+
let c_rg = unpack2x16float(splatDataU32[b + 6u]);
|
|
9115
|
+
let c_bp = unpack2x16float(splatDataU32[b + 7u]);
|
|
9116
|
+
var s: Splat;
|
|
9117
|
+
s.mean = vec3<f32>(mean_xy.x, mean_xy.y, mean_zp.x);
|
|
9118
|
+
s._pad0 = 0.0;
|
|
9119
|
+
s.scale = vec3<f32>(sc_xy.x, sc_xy.y, sc_z_op.x);
|
|
9120
|
+
s._pad1 = 0.0;
|
|
9121
|
+
s.rotation = vec4<f32>(r_xy.x, r_xy.y, r_zw.x, r_zw.y);
|
|
9122
|
+
s.colorDC = vec3<f32>(c_rg.x, c_rg.y, c_bp.x);
|
|
9123
|
+
s.opacity = sc_z_op.y;
|
|
9124
|
+
return s;
|
|
9125
|
+
}
|
|
9126
|
+
`;
|
|
9127
|
+
code = code.replace(/(@vertex|@compute)/, unpackFunctions + "$1");
|
|
9128
|
+
code = code.replace(/splats\[([^\]]+)\]/g, "unpackSplatFromHalf($1)");
|
|
9129
|
+
code = code.replace(
|
|
9130
|
+
/fn evalSH\(splat: Splat, dir: vec3<f32>\) -> vec3<f32> \{[\s\S]*?\n\}/,
|
|
9131
|
+
`fn evalSH(splat: Splat, dir: vec3<f32>) -> vec3<f32> {
|
|
9132
|
+
return vec3<f32>(0.0);
|
|
9133
|
+
}`
|
|
9134
|
+
);
|
|
9135
|
+
return code;
|
|
9136
|
+
}
|
|
8779
9137
|
const _GSSplatRenderer = class _GSSplatRenderer {
|
|
8780
9138
|
constructor(renderer, camera) {
|
|
8781
9139
|
__publicField(this, "renderer");
|
|
@@ -8789,6 +9147,12 @@ const _GSSplatRenderer = class _GSSplatRenderer {
|
|
|
8789
9147
|
__publicField(this, "sorter", null);
|
|
8790
9148
|
__publicField(this, "shMode", SHMode.L3);
|
|
8791
9149
|
__publicField(this, "boundingBox", null);
|
|
9150
|
+
/** 紧凑布局模式:64B/splat,无 SH,适合移动端 */
|
|
9151
|
+
__publicField(this, "compactLayout", false);
|
|
9152
|
+
/** 半精度模式:32B/splat,所有数据用 f16 打包 */
|
|
9153
|
+
__publicField(this, "halfPrecision", false);
|
|
9154
|
+
/** 排序位宽:16-bit 排序减少一半 radix sort pass */
|
|
9155
|
+
__publicField(this, "sortBits", 32);
|
|
8792
9156
|
// 预分配 uniform 上传缓冲区,避免每帧 GC(56 floats = 224 bytes)
|
|
8793
9157
|
__publicField(this, "uniformData", new Float32Array(56));
|
|
8794
9158
|
__publicField(this, "cpuPositions", null);
|
|
@@ -8802,6 +9166,7 @@ const _GSSplatRenderer = class _GSSplatRenderer {
|
|
|
8802
9166
|
__publicField(this, "pixelCullThreshold", 1);
|
|
8803
9167
|
__publicField(this, "maxVisibleSplats", 0);
|
|
8804
9168
|
__publicField(this, "depthRangeLimit", 0);
|
|
9169
|
+
__publicField(this, "lodSkipRate", 0);
|
|
8805
9170
|
// 排序优化:相机变化检测 + 频率控制
|
|
8806
9171
|
__publicField(this, "lastSortViewMatrix", new Float32Array(16));
|
|
8807
9172
|
__publicField(this, "lastSortProjMatrix", new Float32Array(16));
|
|
@@ -8814,6 +9179,7 @@ const _GSSplatRenderer = class _GSSplatRenderer {
|
|
|
8814
9179
|
__publicField(this, "sortStateInitialized", false);
|
|
8815
9180
|
__publicField(this, "sortFrequency", 1);
|
|
8816
9181
|
__publicField(this, "frameCounter", 0);
|
|
9182
|
+
__publicField(this, "debugFrameLogged", false);
|
|
8817
9183
|
// 编辑器状态
|
|
8818
9184
|
__publicField(this, "editorStateBuffer", null);
|
|
8819
9185
|
__publicField(this, "editorPipeline", null);
|
|
@@ -8838,15 +9204,56 @@ const _GSSplatRenderer = class _GSSplatRenderer {
|
|
|
8838
9204
|
this.createUniformBuffer();
|
|
8839
9205
|
this.updateModelMatrix();
|
|
8840
9206
|
}
|
|
9207
|
+
/**
|
|
9208
|
+
* 启用/禁用紧凑布局模式(64B/splat,无 SH)。
|
|
9209
|
+
* 必须在 setCompactData / setData 之前调用。
|
|
9210
|
+
*/
|
|
9211
|
+
setCompactLayout(compact) {
|
|
9212
|
+
if (this.compactLayout === compact) return;
|
|
9213
|
+
this.compactLayout = compact;
|
|
9214
|
+
if (compact) {
|
|
9215
|
+
this.shMode = SHMode.L0;
|
|
9216
|
+
}
|
|
9217
|
+
this.createPipeline();
|
|
9218
|
+
}
|
|
9219
|
+
/**
|
|
9220
|
+
* 设置排序位宽。16-bit 模式排序 pass 从 4 降到 2,排序速度翻倍。
|
|
9221
|
+
* 必须在 setData / setCompactData 之前调用。
|
|
9222
|
+
*/
|
|
9223
|
+
setSortBits(bits2) {
|
|
9224
|
+
this.sortBits = bits2;
|
|
9225
|
+
}
|
|
9226
|
+
/**
|
|
9227
|
+
* 启用/禁用半精度模式(32B/splat,所有数据 f16 打包)。
|
|
9228
|
+
* 自动启用 compactLayout。必须在 setCompactData 之前调用。
|
|
9229
|
+
*/
|
|
9230
|
+
setHalfPrecision(half) {
|
|
9231
|
+
if (this.halfPrecision === half) return;
|
|
9232
|
+
this.halfPrecision = half;
|
|
9233
|
+
if (half) {
|
|
9234
|
+
this.compactLayout = true;
|
|
9235
|
+
this.shMode = SHMode.L0;
|
|
9236
|
+
}
|
|
9237
|
+
this.createPipeline();
|
|
9238
|
+
}
|
|
8841
9239
|
createPipeline() {
|
|
8842
9240
|
const device = this.renderer.device;
|
|
8843
|
-
const
|
|
9241
|
+
const dbg = getDebugOverlay();
|
|
9242
|
+
let shaderCode = gsOptimizedShader.replace(
|
|
8844
9243
|
"const SH_LEVEL: u32 = 3u; // @SH_LEVEL_INJECT@",
|
|
8845
9244
|
`const SH_LEVEL: u32 = ${this.shMode}u;`
|
|
8846
9245
|
);
|
|
9246
|
+
if (this.halfPrecision) {
|
|
9247
|
+
shaderCode = transformShaderForHalf(shaderCode);
|
|
9248
|
+
} else if (this.compactLayout) {
|
|
9249
|
+
shaderCode = transformShaderForCompact(shaderCode);
|
|
9250
|
+
}
|
|
9251
|
+
dbg == null ? void 0 : dbg.info(`createPipeline: SH=${this.shMode}, compact=${this.compactLayout}, half=${this.halfPrecision}`);
|
|
8847
9252
|
const shaderModule = device.createShaderModule({
|
|
8848
|
-
code: shaderCode
|
|
9253
|
+
code: shaderCode,
|
|
9254
|
+
label: "gs-main-shader"
|
|
8849
9255
|
});
|
|
9256
|
+
dbg == null ? void 0 : dbg.checkShader(shaderModule, "gs-main");
|
|
8850
9257
|
this.bindGroupLayout = device.createBindGroupLayout({
|
|
8851
9258
|
entries: [
|
|
8852
9259
|
{
|
|
@@ -8923,8 +9330,12 @@ const _GSSplatRenderer = class _GSSplatRenderer {
|
|
|
8923
9330
|
// A采用"zero add one-minus-src-alpha"的混合模式计算Transmittance
|
|
8924
9331
|
createDepthNormalPipeline() {
|
|
8925
9332
|
const device = this.renderer.device;
|
|
9333
|
+
let dnShader = gsDepthNormalShader;
|
|
9334
|
+
if (this.compactLayout) {
|
|
9335
|
+
dnShader = transformShaderForCompact(dnShader);
|
|
9336
|
+
}
|
|
8926
9337
|
const shaderModule = device.createShaderModule({
|
|
8927
|
-
code:
|
|
9338
|
+
code: dnShader
|
|
8928
9339
|
});
|
|
8929
9340
|
const pipelineLayout = device.createPipelineLayout({
|
|
8930
9341
|
bindGroupLayouts: [this.bindGroupLayout]
|
|
@@ -9103,6 +9514,17 @@ const _GSSplatRenderer = class _GSSplatRenderer {
|
|
|
9103
9514
|
getDepthRangeLimit() {
|
|
9104
9515
|
return this.depthRangeLimit;
|
|
9105
9516
|
}
|
|
9517
|
+
/**
|
|
9518
|
+
* 设置 LOD 抽稀率(0~1)
|
|
9519
|
+
* 远处 splat 按此比例随机跳过(确定性哈希,无闪烁)
|
|
9520
|
+
* 0 = 不抽稀
|
|
9521
|
+
*/
|
|
9522
|
+
setLodSkipRate(rate) {
|
|
9523
|
+
this.lodSkipRate = Math.max(0, Math.min(1, rate));
|
|
9524
|
+
}
|
|
9525
|
+
getLodSkipRate() {
|
|
9526
|
+
return this.lodSkipRate;
|
|
9527
|
+
}
|
|
9106
9528
|
/**
|
|
9107
9529
|
* 设置排序频率
|
|
9108
9530
|
* 1 = 每帧排序(默认),2 = 每 2 帧排序一次,以此类推
|
|
@@ -9145,6 +9567,7 @@ const _GSSplatRenderer = class _GSSplatRenderer {
|
|
|
9145
9567
|
}
|
|
9146
9568
|
setData(splats) {
|
|
9147
9569
|
const device = this.renderer.device;
|
|
9570
|
+
const dbg = getDebugOverlay();
|
|
9148
9571
|
if (this.splatBuffer) {
|
|
9149
9572
|
this.splatBuffer.destroy();
|
|
9150
9573
|
}
|
|
@@ -9159,12 +9582,24 @@ const _GSSplatRenderer = class _GSSplatRenderer {
|
|
|
9159
9582
|
this.boundingBox = null;
|
|
9160
9583
|
return;
|
|
9161
9584
|
}
|
|
9585
|
+
const floatsPerSplat = this.compactLayout ? COMPACT_SPLAT_FLOAT_COUNT : SPLAT_FLOAT_COUNT;
|
|
9586
|
+
const bytesPerSplat = floatsPerSplat * 4;
|
|
9587
|
+
const maxStorageSize = device.limits.maxStorageBufferBindingSize;
|
|
9588
|
+
const maxSplatsForGPU = Math.floor(maxStorageSize / bytesPerSplat);
|
|
9589
|
+
if (this.splatCount > maxSplatsForGPU) {
|
|
9590
|
+
dbg == null ? void 0 : dbg.warn(
|
|
9591
|
+
`setData: truncating ${this.splatCount} -> ${maxSplatsForGPU} splats (maxStorageBufferBindingSize=${(maxStorageSize / 1048576).toFixed(0)}MB)`
|
|
9592
|
+
);
|
|
9593
|
+
this.splatCount = maxSplatsForGPU;
|
|
9594
|
+
splats = splats.slice(0, this.splatCount);
|
|
9595
|
+
}
|
|
9596
|
+
dbg == null ? void 0 : dbg.info(`setData: ${this.splatCount} splats, compact=${this.compactLayout}`);
|
|
9162
9597
|
this.boundingBox = this.computeBoundingBox(splats);
|
|
9163
9598
|
const positions = new Float32Array(this.splatCount * 3);
|
|
9164
|
-
const data = new Float32Array(this.splatCount *
|
|
9599
|
+
const data = new Float32Array(this.splatCount * floatsPerSplat);
|
|
9165
9600
|
for (let i = 0; i < this.splatCount; i++) {
|
|
9166
9601
|
const splat = splats[i];
|
|
9167
|
-
const offset = i *
|
|
9602
|
+
const offset = i * floatsPerSplat;
|
|
9168
9603
|
positions[i * 3 + 0] = splat.mean[0];
|
|
9169
9604
|
positions[i * 3 + 1] = splat.mean[1];
|
|
9170
9605
|
positions[i * 3 + 2] = splat.mean[2];
|
|
@@ -9184,31 +9619,39 @@ const _GSSplatRenderer = class _GSSplatRenderer {
|
|
|
9184
9619
|
data[offset + 13] = splat.colorDC[1];
|
|
9185
9620
|
data[offset + 14] = splat.colorDC[2];
|
|
9186
9621
|
data[offset + 15] = splat.opacity;
|
|
9187
|
-
|
|
9188
|
-
|
|
9189
|
-
|
|
9190
|
-
|
|
9191
|
-
|
|
9192
|
-
|
|
9193
|
-
|
|
9194
|
-
|
|
9195
|
-
|
|
9622
|
+
if (!this.compactLayout) {
|
|
9623
|
+
const shRest = splat.shRest;
|
|
9624
|
+
for (let j = 0; j < 9; j++) {
|
|
9625
|
+
data[offset + 16 + j] = shRest ? shRest[j] : 0;
|
|
9626
|
+
}
|
|
9627
|
+
for (let j = 0; j < 15; j++) {
|
|
9628
|
+
data[offset + 25 + j] = shRest ? shRest[9 + j] : 0;
|
|
9629
|
+
}
|
|
9630
|
+
for (let j = 0; j < 21; j++) {
|
|
9631
|
+
data[offset + 40 + j] = shRest ? shRest[24 + j] : 0;
|
|
9632
|
+
}
|
|
9633
|
+
data[offset + 61] = 0;
|
|
9634
|
+
data[offset + 62] = 0;
|
|
9635
|
+
data[offset + 63] = 0;
|
|
9196
9636
|
}
|
|
9197
|
-
data[offset + 61] = 0;
|
|
9198
|
-
data[offset + 62] = 0;
|
|
9199
|
-
data[offset + 63] = 0;
|
|
9200
9637
|
}
|
|
9638
|
+
dbg == null ? void 0 : dbg.info(`splatBuffer: ${(data.byteLength / 1048576).toFixed(1)}MB (${floatsPerSplat} floats/splat)`);
|
|
9201
9639
|
this.splatBuffer = device.createBuffer({
|
|
9202
9640
|
size: data.byteLength,
|
|
9203
9641
|
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
9204
9642
|
});
|
|
9205
9643
|
device.queue.writeBuffer(this.splatBuffer, 0, data);
|
|
9206
9644
|
this.cpuPositions = positions;
|
|
9645
|
+
dbg == null ? void 0 : dbg.info(`Creating sorter: compact=${this.compactLayout}, sortBits=${this.sortBits}`);
|
|
9207
9646
|
this.sorter = new GSSplatSorter(
|
|
9208
9647
|
device,
|
|
9209
9648
|
this.splatCount,
|
|
9210
9649
|
this.splatBuffer,
|
|
9211
|
-
this.uniformBuffer
|
|
9650
|
+
this.uniformBuffer,
|
|
9651
|
+
{},
|
|
9652
|
+
this.compactLayout,
|
|
9653
|
+
false,
|
|
9654
|
+
this.sortBits
|
|
9212
9655
|
);
|
|
9213
9656
|
this.sorter.setScreenSize(this.renderer.width, this.renderer.height);
|
|
9214
9657
|
this.sorter.setCullingOptions({
|
|
@@ -9224,10 +9667,12 @@ const _GSSplatRenderer = class _GSSplatRenderer {
|
|
|
9224
9667
|
{ binding: 2, resource: { buffer: this.sorter.getIndicesBuffer() } }
|
|
9225
9668
|
]
|
|
9226
9669
|
});
|
|
9670
|
+
dbg == null ? void 0 : dbg.info(`setData complete, bindGroup created`);
|
|
9227
9671
|
if (this.editorEnabled) this.rebuildEditorBindGroup();
|
|
9228
9672
|
}
|
|
9229
9673
|
setCompactData(compactData) {
|
|
9230
9674
|
const device = this.renderer.device;
|
|
9675
|
+
const dbg = getDebugOverlay();
|
|
9231
9676
|
if (this.splatBuffer) {
|
|
9232
9677
|
this.splatBuffer.destroy();
|
|
9233
9678
|
}
|
|
@@ -9242,20 +9687,98 @@ const _GSSplatRenderer = class _GSSplatRenderer {
|
|
|
9242
9687
|
this.boundingBox = null;
|
|
9243
9688
|
return;
|
|
9244
9689
|
}
|
|
9690
|
+
const bytesPerSplat = this.halfPrecision ? HALF_SPLAT_BYTE_SIZE : this.compactLayout ? COMPACT_SPLAT_BYTE_SIZE : SPLAT_BYTE_SIZE;
|
|
9691
|
+
const maxStorageSize = device.limits.maxStorageBufferBindingSize;
|
|
9692
|
+
const maxSplatsForGPU = Math.floor(maxStorageSize / bytesPerSplat);
|
|
9693
|
+
dbg == null ? void 0 : dbg.info(
|
|
9694
|
+
`GPU limits: maxStorageBinding=${(maxStorageSize / 1048576).toFixed(0)}MB, maxBuf=${(device.limits.maxBufferSize / 1048576).toFixed(0)}MB, maxSplats=${maxSplatsForGPU} (at ${bytesPerSplat}B/splat), half=${this.halfPrecision}`
|
|
9695
|
+
);
|
|
9696
|
+
if (this.splatCount > maxSplatsForGPU) {
|
|
9697
|
+
dbg == null ? void 0 : dbg.warn(
|
|
9698
|
+
`Splat count ${this.splatCount} exceeds GPU maxStorageBufferBindingSize (${(maxStorageSize / 1048576).toFixed(0)}MB = ${maxSplatsForGPU} splats). Truncating to ${maxSplatsForGPU}.`
|
|
9699
|
+
);
|
|
9700
|
+
this.splatCount = maxSplatsForGPU;
|
|
9701
|
+
}
|
|
9245
9702
|
this.boundingBox = this.computeBoundingBoxFromCompact(compactData);
|
|
9246
|
-
|
|
9247
|
-
|
|
9248
|
-
|
|
9703
|
+
let trimmedData;
|
|
9704
|
+
if (this.halfPrecision && this.lodSkipRate > 0) {
|
|
9705
|
+
const skipRate = this.lodSkipRate;
|
|
9706
|
+
const threshold = skipRate * 65535;
|
|
9707
|
+
const kept = [];
|
|
9708
|
+
for (let i = 0; i < this.splatCount; i++) {
|
|
9709
|
+
const hash = (i * 2654435761 | 0) >>> 16 & 65535;
|
|
9710
|
+
if (hash >= threshold) kept.push(i);
|
|
9711
|
+
}
|
|
9712
|
+
const n = kept.length;
|
|
9713
|
+
const pos = new Float32Array(n * 3);
|
|
9714
|
+
const scl = new Float32Array(n * 3);
|
|
9715
|
+
const rot = new Float32Array(n * 4);
|
|
9716
|
+
const col = new Float32Array(n * 3);
|
|
9717
|
+
const opa = new Float32Array(n);
|
|
9718
|
+
for (let k = 0; k < n; k++) {
|
|
9719
|
+
const i = kept[k];
|
|
9720
|
+
pos[k * 3] = compactData.positions[i * 3];
|
|
9721
|
+
pos[k * 3 + 1] = compactData.positions[i * 3 + 1];
|
|
9722
|
+
pos[k * 3 + 2] = compactData.positions[i * 3 + 2];
|
|
9723
|
+
scl[k * 3] = compactData.scales[i * 3];
|
|
9724
|
+
scl[k * 3 + 1] = compactData.scales[i * 3 + 1];
|
|
9725
|
+
scl[k * 3 + 2] = compactData.scales[i * 3 + 2];
|
|
9726
|
+
rot[k * 4] = compactData.rotations[i * 4];
|
|
9727
|
+
rot[k * 4 + 1] = compactData.rotations[i * 4 + 1];
|
|
9728
|
+
rot[k * 4 + 2] = compactData.rotations[i * 4 + 2];
|
|
9729
|
+
rot[k * 4 + 3] = compactData.rotations[i * 4 + 3];
|
|
9730
|
+
col[k * 3] = compactData.colors[i * 3];
|
|
9731
|
+
col[k * 3 + 1] = compactData.colors[i * 3 + 1];
|
|
9732
|
+
col[k * 3 + 2] = compactData.colors[i * 3 + 2];
|
|
9733
|
+
opa[k] = compactData.opacities[i];
|
|
9734
|
+
}
|
|
9735
|
+
trimmedData = { count: n, positions: pos, scales: scl, rotations: rot, colors: col, opacities: opa };
|
|
9736
|
+
this.splatCount = n;
|
|
9737
|
+
dbg == null ? void 0 : dbg.info(`CPU LOD filter: ${compactData.count} → ${n} splats (skip ${(skipRate * 100).toFixed(0)}%)`);
|
|
9738
|
+
} else {
|
|
9739
|
+
const includeSH = !this.compactLayout && compactData.shCoeffs !== void 0;
|
|
9740
|
+
trimmedData = this.splatCount < compactData.count ? {
|
|
9741
|
+
count: this.splatCount,
|
|
9742
|
+
positions: compactData.positions.slice(0, this.splatCount * 3),
|
|
9743
|
+
scales: compactData.scales.slice(0, this.splatCount * 3),
|
|
9744
|
+
rotations: compactData.rotations.slice(0, this.splatCount * 4),
|
|
9745
|
+
colors: compactData.colors.slice(0, this.splatCount * 3),
|
|
9746
|
+
opacities: compactData.opacities.slice(0, this.splatCount),
|
|
9747
|
+
shCoeffs: includeSH && compactData.shCoeffs ? compactData.shCoeffs.slice(0, this.splatCount * compactData.shCoeffs.length / compactData.count) : void 0
|
|
9748
|
+
} : compactData;
|
|
9749
|
+
}
|
|
9750
|
+
dbg == null ? void 0 : dbg.info(`setCompactData: ${this.splatCount} splats, compact=${this.compactLayout}, half=${this.halfPrecision}`);
|
|
9751
|
+
this.cpuPositions = new Float32Array(trimmedData.positions);
|
|
9752
|
+
let gpuBuf;
|
|
9753
|
+
if (this.halfPrecision) {
|
|
9754
|
+
const halfData = compactDataToGPUBufferHalf(trimmedData);
|
|
9755
|
+
gpuBuf = halfData.buffer;
|
|
9756
|
+
dbg == null ? void 0 : dbg.info(
|
|
9757
|
+
`gpuData(half): ${(halfData.byteLength / 1048576).toFixed(1)}MB, u32s=${halfData.length}, u32/splat=${HALF_SPLAT_U32_COUNT}`
|
|
9758
|
+
);
|
|
9759
|
+
} else {
|
|
9760
|
+
const includeSH = !this.compactLayout && compactData.shCoeffs !== void 0;
|
|
9761
|
+
const f32Data = compactDataToGPUBuffer(trimmedData, includeSH);
|
|
9762
|
+
gpuBuf = f32Data.buffer;
|
|
9763
|
+
dbg == null ? void 0 : dbg.info(
|
|
9764
|
+
`gpuData: ${(f32Data.byteLength / 1048576).toFixed(1)}MB, includeSH=${includeSH}, floats=${f32Data.length}, floats/splat=${f32Data.length / this.splatCount}`
|
|
9765
|
+
);
|
|
9766
|
+
}
|
|
9249
9767
|
this.splatBuffer = device.createBuffer({
|
|
9250
|
-
size:
|
|
9768
|
+
size: gpuBuf.byteLength,
|
|
9251
9769
|
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
9252
9770
|
});
|
|
9253
|
-
device.queue.writeBuffer(this.splatBuffer, 0,
|
|
9771
|
+
device.queue.writeBuffer(this.splatBuffer, 0, gpuBuf);
|
|
9772
|
+
dbg == null ? void 0 : dbg.info(`Creating sorter: compact=${this.compactLayout}, half=${this.halfPrecision}, sortBits=${this.sortBits}`);
|
|
9254
9773
|
this.sorter = new GSSplatSorter(
|
|
9255
9774
|
device,
|
|
9256
9775
|
this.splatCount,
|
|
9257
9776
|
this.splatBuffer,
|
|
9258
|
-
this.uniformBuffer
|
|
9777
|
+
this.uniformBuffer,
|
|
9778
|
+
{},
|
|
9779
|
+
this.compactLayout,
|
|
9780
|
+
this.halfPrecision,
|
|
9781
|
+
this.sortBits
|
|
9259
9782
|
);
|
|
9260
9783
|
this.sorter.setScreenSize(this.renderer.width, this.renderer.height);
|
|
9261
9784
|
this.sorter.setCullingOptions({
|
|
@@ -9271,6 +9794,7 @@ const _GSSplatRenderer = class _GSSplatRenderer {
|
|
|
9271
9794
|
{ binding: 2, resource: { buffer: this.sorter.getIndicesBuffer() } }
|
|
9272
9795
|
]
|
|
9273
9796
|
});
|
|
9797
|
+
dbg == null ? void 0 : dbg.info(`setCompactData complete, bindGroup created`);
|
|
9274
9798
|
if (this.editorEnabled) this.rebuildEditorBindGroup();
|
|
9275
9799
|
this.sortStateInitialized = false;
|
|
9276
9800
|
}
|
|
@@ -9294,7 +9818,7 @@ const _GSSplatRenderer = class _GSSplatRenderer {
|
|
|
9294
9818
|
this.renderer.device.queue.writeBuffer(this.uniformBuffer, 0, ud);
|
|
9295
9819
|
const changed = this.needsSort();
|
|
9296
9820
|
this.frameCounter++;
|
|
9297
|
-
const shouldSort =
|
|
9821
|
+
const shouldSort = !this.sortStateInitialized || changed;
|
|
9298
9822
|
if (shouldSort) {
|
|
9299
9823
|
this.sorter.setScreenSize(this.renderer.width, this.renderer.height);
|
|
9300
9824
|
this.sorter.setCullingOptions({
|
|
@@ -9302,9 +9826,24 @@ const _GSSplatRenderer = class _GSSplatRenderer {
|
|
|
9302
9826
|
farPlane: this.camera.far,
|
|
9303
9827
|
pixelThreshold: this.pixelCullThreshold,
|
|
9304
9828
|
maxVisibleCount: this.maxVisibleSplats,
|
|
9305
|
-
depthRangeLimit: this.depthRangeLimit
|
|
9829
|
+
depthRangeLimit: this.depthRangeLimit,
|
|
9830
|
+
lodSkipRate: this.lodSkipRate
|
|
9306
9831
|
});
|
|
9307
|
-
this.
|
|
9832
|
+
if (!this.debugFrameLogged) {
|
|
9833
|
+
const dbg = getDebugOverlay();
|
|
9834
|
+
const dev = this.renderer.device;
|
|
9835
|
+
dev.pushErrorScope("validation");
|
|
9836
|
+
dev.pushErrorScope("out-of-memory");
|
|
9837
|
+
this.sorter.sort();
|
|
9838
|
+
dev.popErrorScope().then((err2) => {
|
|
9839
|
+
if (err2) dbg == null ? void 0 : dbg.error(`Sort OOM: ${err2.message}`);
|
|
9840
|
+
});
|
|
9841
|
+
dev.popErrorScope().then((err2) => {
|
|
9842
|
+
if (err2) dbg == null ? void 0 : dbg.error(`Sort Validation: ${err2.message}`);
|
|
9843
|
+
});
|
|
9844
|
+
} else {
|
|
9845
|
+
this.sorter.sort();
|
|
9846
|
+
}
|
|
9308
9847
|
}
|
|
9309
9848
|
if (this.editorEnabled && this.editorPipeline && this.editorBindGroup) {
|
|
9310
9849
|
pass.setPipeline(this.editorPipeline);
|
|
@@ -9314,6 +9853,28 @@ const _GSSplatRenderer = class _GSSplatRenderer {
|
|
|
9314
9853
|
pass.setBindGroup(0, this.bindGroup);
|
|
9315
9854
|
}
|
|
9316
9855
|
pass.drawIndirect(this.sorter.getDrawIndirectBuffer(), 0);
|
|
9856
|
+
if (!this.debugFrameLogged) {
|
|
9857
|
+
this.debugFrameLogged = true;
|
|
9858
|
+
const dbg = getDebugOverlay();
|
|
9859
|
+
dbg == null ? void 0 : dbg.info(
|
|
9860
|
+
`First render: splats=${this.splatCount}, canvas=${this.renderer.width}x${this.renderer.height}, editor=${this.editorEnabled}, depthWrite=${this.depthWriteEnabled}`
|
|
9861
|
+
);
|
|
9862
|
+
if (dbg && this.sorter) {
|
|
9863
|
+
const pos2 = this.camera.position;
|
|
9864
|
+
dbg.info(`Camera pos: [${pos2[0].toFixed(2)}, ${pos2[1].toFixed(2)}, ${pos2[2].toFixed(2)}]`);
|
|
9865
|
+
dbg.info(
|
|
9866
|
+
`Culling cfg: pixelThresh=${this.pixelCullThreshold.toFixed(2)}, maxVisible=${this.maxVisibleSplats}, depthRange=${this.depthRangeLimit.toFixed(2)}, near=${this.camera.near}, far=${this.camera.far}`
|
|
9867
|
+
);
|
|
9868
|
+
this.sorter.readbackIndirect().then((data) => {
|
|
9869
|
+
dbg.info(
|
|
9870
|
+
`DrawIndirect: vtx=${data[0]}, inst=${data[1]}, firstVtx=${data[2]}, firstInst=${data[3]}`
|
|
9871
|
+
);
|
|
9872
|
+
if (data[0] === 0 && data[1] === 0) {
|
|
9873
|
+
dbg.error("ALL splats culled! visible=0");
|
|
9874
|
+
}
|
|
9875
|
+
});
|
|
9876
|
+
}
|
|
9877
|
+
}
|
|
9317
9878
|
}
|
|
9318
9879
|
getSplatCount() {
|
|
9319
9880
|
return this.splatCount;
|
|
@@ -9631,10 +10192,13 @@ const _GSSplatRenderer = class _GSSplatRenderer {
|
|
|
9631
10192
|
}
|
|
9632
10193
|
createEditorPipeline() {
|
|
9633
10194
|
const device = this.renderer.device;
|
|
9634
|
-
|
|
10195
|
+
let editorShaderCode = this.buildEditorShader().replace(
|
|
9635
10196
|
"const SH_LEVEL: u32 = 3u; // @SH_LEVEL_INJECT@",
|
|
9636
10197
|
`const SH_LEVEL: u32 = ${this.shMode}u;`
|
|
9637
10198
|
);
|
|
10199
|
+
if (this.compactLayout) {
|
|
10200
|
+
editorShaderCode = transformShaderForCompact(editorShaderCode);
|
|
10201
|
+
}
|
|
9638
10202
|
const shaderModule = device.createShaderModule({ code: editorShaderCode });
|
|
9639
10203
|
this.editorBindGroupLayout = device.createBindGroupLayout({
|
|
9640
10204
|
entries: [
|
|
@@ -18727,6 +19291,10 @@ class App {
|
|
|
18727
19291
|
__publicField(this, "animationId", 0);
|
|
18728
19292
|
// 是否使用移动端渲染器
|
|
18729
19293
|
__publicField(this, "useMobileRenderer", false);
|
|
19294
|
+
// 移动端优化开关(默认关闭,由外部显式启用)
|
|
19295
|
+
__publicField(this, "mobileOptimizationsEnabled", false);
|
|
19296
|
+
// 移动端可见 splat 硬上限(保证稳定帧率)
|
|
19297
|
+
__publicField(this, "mobileMaxVisibleCap", 0);
|
|
18730
19298
|
// 最近加载的 CompactSplatData(用于编辑器导出)
|
|
18731
19299
|
__publicField(this, "lastCompactData", null);
|
|
18732
19300
|
// 自适应性能控制
|
|
@@ -18736,6 +19304,17 @@ class App {
|
|
|
18736
19304
|
});
|
|
18737
19305
|
__publicField(this, "lastAppliedRenderScale", 1);
|
|
18738
19306
|
__publicField(this, "baseRenderScale", 1);
|
|
19307
|
+
// 动态分辨率:移动时降分辨率,静止后恢复
|
|
19308
|
+
__publicField(this, "dynamicResolutionEnabled", false);
|
|
19309
|
+
__publicField(this, "dynResLastViewMatrix", new Float32Array(16));
|
|
19310
|
+
__publicField(this, "dynResStillFrames", 0);
|
|
19311
|
+
__publicField(this, "dynResCurrentScale", 1);
|
|
19312
|
+
__publicField(this, "DYNRES_MOVE_SCALE", 0.6);
|
|
19313
|
+
// 移动时 DPR*0.6(2.0*0.6=1.2)
|
|
19314
|
+
__publicField(this, "DYNRES_STILL_SCALE", 1);
|
|
19315
|
+
// 静止时 DPR*1.0
|
|
19316
|
+
__publicField(this, "DYNRES_STILL_THRESHOLD", 2);
|
|
19317
|
+
// 静止 2 帧即恢复
|
|
18739
19318
|
// 绑定的事件处理函数
|
|
18740
19319
|
__publicField(this, "boundOnResize");
|
|
18741
19320
|
/** 额外渲染回调(在 gizmo 之前、场景辅助之后执行) */
|
|
@@ -18789,16 +19368,46 @@ class App {
|
|
|
18789
19368
|
nearDepthRangeRatio: 2
|
|
18790
19369
|
};
|
|
18791
19370
|
console.log(
|
|
18792
|
-
`[3DGS] Apple GPU detected (${this.renderer.gpuVendor}/${this.renderer.gpuArchitecture}), applying TBDR optimizations:
|
|
19371
|
+
`[3DGS] Apple GPU detected (${this.renderer.gpuVendor}/${this.renderer.gpuArchitecture}), applying TBDR optimizations: depthWrite off, aggressive culling`
|
|
18793
19372
|
);
|
|
18794
19373
|
}
|
|
18795
19374
|
/**
|
|
18796
|
-
*
|
|
19375
|
+
* 启用/禁用移动端性能优化(默认关闭)
|
|
19376
|
+
* 开启后,加载模型时将自动应用:f16 半精度、16-bit 排序、像素剔除、动态分辨率等。
|
|
19377
|
+
* 应在 init() 之后、加载模型之前调用。
|
|
19378
|
+
*/
|
|
19379
|
+
enableMobileOptimizations(enabled = true) {
|
|
19380
|
+
var _a2;
|
|
19381
|
+
this.mobileOptimizationsEnabled = enabled;
|
|
19382
|
+
if (enabled) {
|
|
19383
|
+
this.dynamicResolutionEnabled = true;
|
|
19384
|
+
createDebugOverlay();
|
|
19385
|
+
(_a2 = getDebugOverlay()) == null ? void 0 : _a2.info(
|
|
19386
|
+
`Mobile optimizations enabled. DPR=${window.devicePixelRatio}, canvas=${this.canvas.width}x${this.canvas.height}`
|
|
19387
|
+
);
|
|
19388
|
+
console.log(`[3DGS] Mobile optimizations: f16, sortBits=16, pixelThreshold=1.5, dynamicRes=ON`);
|
|
19389
|
+
} else {
|
|
19390
|
+
this.dynamicResolutionEnabled = false;
|
|
19391
|
+
}
|
|
19392
|
+
}
|
|
19393
|
+
isMobileOptimized() {
|
|
19394
|
+
return this.mobileOptimizationsEnabled;
|
|
19395
|
+
}
|
|
19396
|
+
/**
|
|
19397
|
+
* 创建 GSSplatRenderer 并自动应用平台优化
|
|
19398
|
+
* @param forMobile 移动端模式:半精度(32B/splat)+ SH L0,内存降为 1/8
|
|
18797
19399
|
*/
|
|
18798
|
-
|
|
19400
|
+
createGSRendererUnified(forMobile = false) {
|
|
18799
19401
|
const renderer = new GSSplatRenderer(this.renderer, this.camera);
|
|
18800
|
-
if (
|
|
19402
|
+
if (forMobile) {
|
|
19403
|
+
renderer.setHalfPrecision(true);
|
|
19404
|
+
renderer.setSortBits(16);
|
|
19405
|
+
renderer.setPixelCullThreshold(1.5);
|
|
19406
|
+
renderer.setSortFrequency(2);
|
|
19407
|
+
} else if (this.renderer.isAppleGPU) {
|
|
18801
19408
|
renderer.setSHMode(SHMode.L1);
|
|
19409
|
+
}
|
|
19410
|
+
if (this.renderer.isAppleGPU) {
|
|
18802
19411
|
renderer.setDepthWriteEnabled(false);
|
|
18803
19412
|
}
|
|
18804
19413
|
return renderer;
|
|
@@ -18845,7 +19454,7 @@ class App {
|
|
|
18845
19454
|
*/
|
|
18846
19455
|
async addPLY(urlOrBuffer, onProgress, isLocalFile = false, coordinateSystem = "blender") {
|
|
18847
19456
|
try {
|
|
18848
|
-
const
|
|
19457
|
+
const useMobileOpt = this.mobileOptimizationsEnabled;
|
|
18849
19458
|
let buffer;
|
|
18850
19459
|
if (typeof urlOrBuffer === "string") {
|
|
18851
19460
|
buffer = await this.fetchWithProgress(
|
|
@@ -18868,40 +19477,26 @@ class App {
|
|
|
18868
19477
|
onProgress(50 + parseProgress, "parse");
|
|
18869
19478
|
}
|
|
18870
19479
|
};
|
|
18871
|
-
|
|
18872
|
-
|
|
18873
|
-
|
|
18874
|
-
|
|
18875
|
-
const compactData = await this.parsePLYBuffer(buffer, {
|
|
18876
|
-
maxSplats: Infinity,
|
|
18877
|
-
loadSH: false,
|
|
18878
|
-
onProgress: parseProgressCallback,
|
|
18879
|
-
coordinateSystem
|
|
18880
|
-
});
|
|
18881
|
-
if (onProgress) onProgress(90, "upload");
|
|
18882
|
-
gsRenderer.setCompactData(compactData);
|
|
18883
|
-
if (onProgress) onProgress(100, "upload");
|
|
18884
|
-
this.lastCompactData = compactData;
|
|
18885
|
-
this.sceneManager.setGSRenderer(gsRenderer);
|
|
18886
|
-
this.hotspotManager.setGSRenderer(gsRenderer);
|
|
18887
|
-
return compactData.count;
|
|
18888
|
-
} else {
|
|
18889
|
-
gsRenderer = this.createDesktopGSRenderer();
|
|
18890
|
-
this.useMobileRenderer = false;
|
|
18891
|
-
const compactData = await this.parsePLYBuffer(buffer, {
|
|
18892
|
-
maxSplats: Infinity,
|
|
18893
|
-
loadSH: true,
|
|
18894
|
-
onProgress: parseProgressCallback,
|
|
18895
|
-
coordinateSystem
|
|
18896
|
-
});
|
|
18897
|
-
if (onProgress) onProgress(90, "upload");
|
|
18898
|
-
gsRenderer.setCompactData(compactData);
|
|
18899
|
-
if (onProgress) onProgress(100, "upload");
|
|
18900
|
-
this.lastCompactData = compactData;
|
|
18901
|
-
this.sceneManager.setGSRenderer(gsRenderer);
|
|
18902
|
-
this.hotspotManager.setGSRenderer(gsRenderer);
|
|
18903
|
-
return compactData.count;
|
|
19480
|
+
if (useMobileOpt) {
|
|
19481
|
+
console.log(
|
|
19482
|
+
"[3DGS] Mobile optimizations active, using SH L0"
|
|
19483
|
+
);
|
|
18904
19484
|
}
|
|
19485
|
+
const gsRenderer = this.createGSRendererUnified(useMobileOpt);
|
|
19486
|
+
this.useMobileRenderer = false;
|
|
19487
|
+
const compactData = await this.parsePLYBuffer(buffer, {
|
|
19488
|
+
maxSplats: Infinity,
|
|
19489
|
+
loadSH: !useMobileOpt,
|
|
19490
|
+
onProgress: parseProgressCallback,
|
|
19491
|
+
coordinateSystem
|
|
19492
|
+
});
|
|
19493
|
+
if (onProgress) onProgress(90, "upload");
|
|
19494
|
+
gsRenderer.setCompactData(compactData);
|
|
19495
|
+
if (onProgress) onProgress(100, "upload");
|
|
19496
|
+
this.lastCompactData = compactData;
|
|
19497
|
+
this.sceneManager.setGSRenderer(gsRenderer);
|
|
19498
|
+
this.hotspotManager.setGSRenderer(gsRenderer);
|
|
19499
|
+
return compactData.count;
|
|
18905
19500
|
} catch (error) {
|
|
18906
19501
|
throw error;
|
|
18907
19502
|
}
|
|
@@ -18912,7 +19507,7 @@ class App {
|
|
|
18912
19507
|
*/
|
|
18913
19508
|
async addSplat(urlOrBuffer, onProgress, isLocalFile = false, coordinateSystem = "blender") {
|
|
18914
19509
|
try {
|
|
18915
|
-
const
|
|
19510
|
+
const useMobileOpt = this.mobileOptimizationsEnabled;
|
|
18916
19511
|
let buffer;
|
|
18917
19512
|
if (typeof urlOrBuffer === "string") {
|
|
18918
19513
|
buffer = await this.fetchWithProgress(
|
|
@@ -18943,23 +19538,9 @@ class App {
|
|
|
18943
19538
|
}
|
|
18944
19539
|
if (onProgress) onProgress(90, "parse");
|
|
18945
19540
|
if (onProgress) onProgress(90, "upload");
|
|
18946
|
-
|
|
18947
|
-
|
|
18948
|
-
|
|
18949
|
-
this.renderer,
|
|
18950
|
-
this.camera
|
|
18951
|
-
);
|
|
18952
|
-
this.useMobileRenderer = true;
|
|
18953
|
-
const compactData = App.splatCpuToCompactData(splats);
|
|
18954
|
-
mobileRenderer.setCompactData(compactData);
|
|
18955
|
-
this.lastCompactData = compactData;
|
|
18956
|
-
gsRenderer = mobileRenderer;
|
|
18957
|
-
} else {
|
|
18958
|
-
const desktopRenderer = this.createDesktopGSRenderer();
|
|
18959
|
-
this.useMobileRenderer = false;
|
|
18960
|
-
desktopRenderer.setData(splats);
|
|
18961
|
-
gsRenderer = desktopRenderer;
|
|
18962
|
-
}
|
|
19541
|
+
const gsRenderer = this.createGSRendererUnified(useMobileOpt);
|
|
19542
|
+
this.useMobileRenderer = false;
|
|
19543
|
+
gsRenderer.setData(splats);
|
|
18963
19544
|
this.sceneManager.setGSRenderer(gsRenderer);
|
|
18964
19545
|
this.hotspotManager.setGSRenderer(gsRenderer);
|
|
18965
19546
|
if (onProgress) onProgress(100, "upload");
|
|
@@ -19004,7 +19585,7 @@ class App {
|
|
|
19004
19585
|
*/
|
|
19005
19586
|
async addSOG(urlOrBuffer, onProgress, isLocalFile = false, coordinateSystem = "blender") {
|
|
19006
19587
|
try {
|
|
19007
|
-
const
|
|
19588
|
+
const useMobileOpt = this.mobileOptimizationsEnabled;
|
|
19008
19589
|
let buffer;
|
|
19009
19590
|
if (typeof urlOrBuffer === "string") {
|
|
19010
19591
|
buffer = await this.fetchWithProgress(
|
|
@@ -19046,14 +19627,8 @@ class App {
|
|
|
19046
19627
|
}
|
|
19047
19628
|
}
|
|
19048
19629
|
if (onProgress) onProgress(90, "upload");
|
|
19049
|
-
|
|
19050
|
-
|
|
19051
|
-
gsRenderer = new GSSplatRendererMobile(this.renderer, this.camera);
|
|
19052
|
-
this.useMobileRenderer = true;
|
|
19053
|
-
} else {
|
|
19054
|
-
gsRenderer = this.createDesktopGSRenderer();
|
|
19055
|
-
this.useMobileRenderer = false;
|
|
19056
|
-
}
|
|
19630
|
+
const gsRenderer = this.createGSRendererUnified(useMobileOpt);
|
|
19631
|
+
this.useMobileRenderer = false;
|
|
19057
19632
|
gsRenderer.setCompactData(compactData);
|
|
19058
19633
|
this.lastCompactData = compactData;
|
|
19059
19634
|
this.sceneManager.setGSRenderer(gsRenderer);
|
|
@@ -19104,6 +19679,21 @@ class App {
|
|
|
19104
19679
|
this.render();
|
|
19105
19680
|
this.animationId = requestAnimationFrame(this.animate.bind(this));
|
|
19106
19681
|
}
|
|
19682
|
+
updateDynamicResolution() {
|
|
19683
|
+
if (!this.dynamicResolutionEnabled) return;
|
|
19684
|
+
const interacting = this.controls.isInteracting;
|
|
19685
|
+
if (interacting) {
|
|
19686
|
+
if (this.dynResCurrentScale !== this.DYNRES_MOVE_SCALE) {
|
|
19687
|
+
this.dynResCurrentScale = this.DYNRES_MOVE_SCALE;
|
|
19688
|
+
this.renderer.setRenderScale(this.DYNRES_MOVE_SCALE);
|
|
19689
|
+
}
|
|
19690
|
+
} else {
|
|
19691
|
+
if (this.dynResCurrentScale !== this.DYNRES_STILL_SCALE) {
|
|
19692
|
+
this.dynResCurrentScale = this.DYNRES_STILL_SCALE;
|
|
19693
|
+
this.renderer.setRenderScale(this.DYNRES_STILL_SCALE);
|
|
19694
|
+
}
|
|
19695
|
+
}
|
|
19696
|
+
}
|
|
19107
19697
|
updateAdaptivePerformance() {
|
|
19108
19698
|
if (!this.adaptivePerformanceEnabled) return;
|
|
19109
19699
|
const gsRenderer = this.getGSRenderer();
|
|
@@ -19147,7 +19737,11 @@ class App {
|
|
|
19147
19737
|
gsRenderer.setMaxVisibleSplats(0);
|
|
19148
19738
|
} else {
|
|
19149
19739
|
const ratio = cfg.nearVisibleRatio + (1 - cfg.nearVisibleRatio) * t;
|
|
19150
|
-
|
|
19740
|
+
let maxVisible = Math.round(splatCount * ratio);
|
|
19741
|
+
if (this.mobileMaxVisibleCap > 0) {
|
|
19742
|
+
maxVisible = Math.min(maxVisible, this.mobileMaxVisibleCap);
|
|
19743
|
+
}
|
|
19744
|
+
gsRenderer.setMaxVisibleSplats(maxVisible);
|
|
19151
19745
|
}
|
|
19152
19746
|
if (cfg.enableDepthRangeLimit) {
|
|
19153
19747
|
if (t >= 0.99) {
|
|
@@ -19157,16 +19751,13 @@ class App {
|
|
|
19157
19751
|
gsRenderer.setDepthRangeLimit(depthRange);
|
|
19158
19752
|
}
|
|
19159
19753
|
}
|
|
19160
|
-
|
|
19161
|
-
gsRenderer.setSortFrequency(2);
|
|
19162
|
-
} else {
|
|
19163
|
-
gsRenderer.setSortFrequency(1);
|
|
19164
|
-
}
|
|
19754
|
+
gsRenderer.setSortFrequency(1);
|
|
19165
19755
|
}
|
|
19166
19756
|
render() {
|
|
19167
19757
|
var _a2, _b2;
|
|
19168
19758
|
this.camera.setAspect(this.renderer.getAspectRatio());
|
|
19169
19759
|
this.controls.update();
|
|
19760
|
+
this.updateDynamicResolution();
|
|
19170
19761
|
this.updateAdaptivePerformance();
|
|
19171
19762
|
this.hotspotManager.updateBillboards();
|
|
19172
19763
|
if ((_a2 = this.skyboxRenderer) == null ? void 0 : _a2.isActive) {
|
|
@@ -19742,10 +20333,6 @@ class App {
|
|
|
19742
20333
|
if (gsRenderer) {
|
|
19743
20334
|
gsRenderer.setSortFrequency(frequency);
|
|
19744
20335
|
}
|
|
19745
|
-
const mobileRenderer = this.getGSRendererMobile();
|
|
19746
|
-
if (mobileRenderer) {
|
|
19747
|
-
mobileRenderer.setSortFrequency(frequency);
|
|
19748
|
-
}
|
|
19749
20336
|
}
|
|
19750
20337
|
/**
|
|
19751
20338
|
* 是否检测到 Apple GPU(M1/M2/M3 等)
|