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