@d5techs/3dgs-lib 1.4.83 → 1.4.85
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 +748 -357
- package/dist/3dgs-lib.cjs.map +1 -1
- package/dist/3dgs-lib.js +748 -357
- package/dist/3dgs-lib.js.map +1 -1
- package/dist/App.d.ts +18 -2
- package/dist/core/OrbitControls.d.ts +1 -0
- package/dist/editor/SplatEditor.d.ts +4 -0
- package/dist/editor/tools/BoxSelection.d.ts +2 -12
- package/dist/editor/tools/SphereSelection.d.ts +2 -9
- 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 上下文");
|
|
@@ -687,6 +829,9 @@ const _OrbitControls = class _OrbitControls {
|
|
|
687
829
|
this.setupEventListeners();
|
|
688
830
|
this.applySpherical();
|
|
689
831
|
}
|
|
832
|
+
get isInteracting() {
|
|
833
|
+
return this.isDragging;
|
|
834
|
+
}
|
|
690
835
|
setupEventListeners() {
|
|
691
836
|
this.canvas.addEventListener("mousedown", this.boundOnMouseDown);
|
|
692
837
|
this.canvas.addEventListener("mousemove", this.boundOnMouseMove);
|
|
@@ -5231,9 +5376,10 @@ function compactDataToGPUBuffer(data, includeFullSH = false) {
|
|
|
5231
5376
|
}
|
|
5232
5377
|
return buffer;
|
|
5233
5378
|
} else {
|
|
5234
|
-
const
|
|
5379
|
+
const COMPACT_FLOATS = 16;
|
|
5380
|
+
const buffer = new Float32Array(count * COMPACT_FLOATS);
|
|
5235
5381
|
for (let i = 0; i < count; i++) {
|
|
5236
|
-
const offset = i *
|
|
5382
|
+
const offset = i * COMPACT_FLOATS;
|
|
5237
5383
|
buffer[offset + 0] = data.positions[i * 3 + 0];
|
|
5238
5384
|
buffer[offset + 1] = data.positions[i * 3 + 1];
|
|
5239
5385
|
buffer[offset + 2] = data.positions[i * 3 + 2];
|
|
@@ -5254,9 +5400,56 @@ function compactDataToGPUBuffer(data, includeFullSH = false) {
|
|
|
5254
5400
|
return buffer;
|
|
5255
5401
|
}
|
|
5256
5402
|
}
|
|
5403
|
+
const _f16Scratch = new Float32Array(1);
|
|
5404
|
+
const _f16ScratchU32 = new Uint32Array(_f16Scratch.buffer);
|
|
5405
|
+
function f32ToF16Bits(val) {
|
|
5406
|
+
_f16Scratch[0] = val;
|
|
5407
|
+
const bits2 = _f16ScratchU32[0];
|
|
5408
|
+
const sign = bits2 >>> 31 & 1;
|
|
5409
|
+
let exp = bits2 >>> 23 & 255;
|
|
5410
|
+
let frac = bits2 & 8388607;
|
|
5411
|
+
if (exp === 255) {
|
|
5412
|
+
return sign << 15 | 31744 | (frac ? 512 : 0);
|
|
5413
|
+
}
|
|
5414
|
+
exp = exp - 127 + 15;
|
|
5415
|
+
if (exp >= 31) {
|
|
5416
|
+
return sign << 15 | 31744;
|
|
5417
|
+
}
|
|
5418
|
+
if (exp <= 0) {
|
|
5419
|
+
if (exp < -10) return sign << 15;
|
|
5420
|
+
frac = (frac | 8388608) >> 1 - exp;
|
|
5421
|
+
return sign << 15 | frac >> 13;
|
|
5422
|
+
}
|
|
5423
|
+
return sign << 15 | exp << 10 | frac >> 13;
|
|
5424
|
+
}
|
|
5425
|
+
function compactDataToGPUBufferHalf(data) {
|
|
5426
|
+
const count = data.count;
|
|
5427
|
+
const U32_PER_SPLAT = 8;
|
|
5428
|
+
const buffer = new Uint32Array(count * U32_PER_SPLAT);
|
|
5429
|
+
const pos = data.positions;
|
|
5430
|
+
const scl = data.scales;
|
|
5431
|
+
const rot = data.rotations;
|
|
5432
|
+
const col = data.colors;
|
|
5433
|
+
const opa = data.opacities;
|
|
5434
|
+
for (let i = 0; i < count; i++) {
|
|
5435
|
+
const o = i * U32_PER_SPLAT;
|
|
5436
|
+
const i3 = i * 3;
|
|
5437
|
+
const i4 = i * 4;
|
|
5438
|
+
buffer[o] = f32ToF16Bits(pos[i3 + 1]) << 16 | f32ToF16Bits(pos[i3]);
|
|
5439
|
+
buffer[o + 1] = f32ToF16Bits(pos[i3 + 2]);
|
|
5440
|
+
buffer[o + 2] = f32ToF16Bits(scl[i3 + 1]) << 16 | f32ToF16Bits(scl[i3]);
|
|
5441
|
+
buffer[o + 3] = f32ToF16Bits(opa[i]) << 16 | f32ToF16Bits(scl[i3 + 2]);
|
|
5442
|
+
buffer[o + 4] = f32ToF16Bits(rot[i4 + 1]) << 16 | f32ToF16Bits(rot[i4]);
|
|
5443
|
+
buffer[o + 5] = f32ToF16Bits(rot[i4 + 3]) << 16 | f32ToF16Bits(rot[i4 + 2]);
|
|
5444
|
+
buffer[o + 6] = f32ToF16Bits(col[i3 + 1]) << 16 | f32ToF16Bits(col[i3]);
|
|
5445
|
+
buffer[o + 7] = f32ToF16Bits(col[i3 + 2]);
|
|
5446
|
+
}
|
|
5447
|
+
return buffer;
|
|
5448
|
+
}
|
|
5257
5449
|
const PLYLoaderMobile = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
5258
5450
|
__proto__: null,
|
|
5259
5451
|
compactDataToGPUBuffer,
|
|
5452
|
+
compactDataToGPUBufferHalf,
|
|
5260
5453
|
loadPLYMobile,
|
|
5261
5454
|
parsePLYBuffer,
|
|
5262
5455
|
transformSHCoeffsYZSwap
|
|
@@ -7316,16 +7509,38 @@ const RADIX_BITS = 8;
|
|
|
7316
7509
|
const RADIX_SIZE = 256;
|
|
7317
7510
|
const ELEMENTS_PER_THREAD = 4;
|
|
7318
7511
|
const BLOCK_SIZE = WORKGROUP_SIZE$1 * ELEMENTS_PER_THREAD;
|
|
7319
|
-
function generateCullingShaderCode$1() {
|
|
7320
|
-
|
|
7321
|
-
|
|
7322
|
-
|
|
7323
|
-
|
|
7324
|
-
|
|
7325
|
-
|
|
7326
|
-
|
|
7327
|
-
|
|
7328
|
-
|
|
7512
|
+
function generateCullingShaderCode$1(compact = false, half = false, sortBits = 32) {
|
|
7513
|
+
let splatBinding;
|
|
7514
|
+
let splatAccessCode;
|
|
7515
|
+
if (half) {
|
|
7516
|
+
splatBinding = `@group(0) @binding(0) var<storage, read> splatDataU32: array<u32>;`;
|
|
7517
|
+
splatAccessCode = `
|
|
7518
|
+
const HALF_U32S: u32 = 8u;
|
|
7519
|
+
fn getSplatMean(idx: u32) -> vec3<f32> {
|
|
7520
|
+
let b = idx * HALF_U32S;
|
|
7521
|
+
let xy = unpack2x16float(splatDataU32[b]);
|
|
7522
|
+
let zp = unpack2x16float(splatDataU32[b + 1u]);
|
|
7523
|
+
return vec3<f32>(xy.x, xy.y, zp.x);
|
|
7524
|
+
}
|
|
7525
|
+
fn getSplatScale(idx: u32) -> vec3<f32> {
|
|
7526
|
+
let b = idx * HALF_U32S;
|
|
7527
|
+
let xy = unpack2x16float(splatDataU32[b + 2u]);
|
|
7528
|
+
return vec3<f32>(xy.x, xy.y, unpack2x16float(splatDataU32[b + 3u]).x);
|
|
7529
|
+
}
|
|
7530
|
+
fn getSplatOpacity(idx: u32) -> f32 {
|
|
7531
|
+
let b = idx * HALF_U32S;
|
|
7532
|
+
return unpack2x16float(splatDataU32[b + 3u]).y;
|
|
7533
|
+
}`;
|
|
7534
|
+
} else {
|
|
7535
|
+
const splatStruct = compact ? `struct Splat {
|
|
7536
|
+
mean: vec3<f32>,
|
|
7537
|
+
_pad0: f32,
|
|
7538
|
+
scale: vec3<f32>,
|
|
7539
|
+
_pad1: f32,
|
|
7540
|
+
rotation: vec4<f32>,
|
|
7541
|
+
colorDC: vec3<f32>,
|
|
7542
|
+
opacity: f32,
|
|
7543
|
+
}` : `struct Splat {
|
|
7329
7544
|
mean: vec3<f32>,
|
|
7330
7545
|
_pad0: f32,
|
|
7331
7546
|
scale: vec3<f32>,
|
|
@@ -7337,7 +7552,24 @@ struct Splat {
|
|
|
7337
7552
|
sh2: array<f32, 15>,
|
|
7338
7553
|
sh3: array<f32, 21>,
|
|
7339
7554
|
_pad2: array<f32, 3>,
|
|
7340
|
-
}
|
|
7555
|
+
}`;
|
|
7556
|
+
splatBinding = `${splatStruct}
|
|
7557
|
+
@group(0) @binding(0) var<storage, read> splats: array<Splat>;`;
|
|
7558
|
+
splatAccessCode = `
|
|
7559
|
+
fn getSplatMean(idx: u32) -> vec3<f32> { return splats[idx].mean; }
|
|
7560
|
+
fn getSplatScale(idx: u32) -> vec3<f32> { return splats[idx].scale; }
|
|
7561
|
+
fn getSplatOpacity(idx: u32) -> f32 { return splats[idx].opacity; }`;
|
|
7562
|
+
}
|
|
7563
|
+
return (
|
|
7564
|
+
/* wgsl */
|
|
7565
|
+
`
|
|
7566
|
+
/**
|
|
7567
|
+
* Project & Cull Shader
|
|
7568
|
+
* 基于 rfs-gsplat-render 实现
|
|
7569
|
+
*/
|
|
7570
|
+
|
|
7571
|
+
${splatBinding}
|
|
7572
|
+
${splatAccessCode}
|
|
7341
7573
|
|
|
7342
7574
|
struct CameraUniforms {
|
|
7343
7575
|
view: mat4x4<f32>,
|
|
@@ -7357,10 +7589,9 @@ struct CullingParams {
|
|
|
7357
7589
|
pixelThreshold: f32,
|
|
7358
7590
|
maxVisibleCount: u32,
|
|
7359
7591
|
depthRangeLimit: f32,
|
|
7360
|
-
|
|
7592
|
+
lodSkipRate: f32,
|
|
7361
7593
|
}
|
|
7362
7594
|
|
|
7363
|
-
@group(0) @binding(0) var<storage, read> splats: array<Splat>;
|
|
7364
7595
|
@group(0) @binding(1) var<uniform> camera: CameraUniforms;
|
|
7365
7596
|
@group(0) @binding(2) var<uniform> params: CullingParams;
|
|
7366
7597
|
@group(0) @binding(3) var<storage, read_write> depthKeys: array<u32>;
|
|
@@ -7378,16 +7609,12 @@ fn getModelMaxScale(model: mat4x4<f32>) -> f32 {
|
|
|
7378
7609
|
return max(max(sx, sy), sz);
|
|
7379
7610
|
}
|
|
7380
7611
|
|
|
7381
|
-
// IEEE 754 位操作编码浮点数为可排序的 u32
|
|
7382
|
-
// 参考 rfs-gsplat-render 的 encode_min_max_fp32 实现
|
|
7383
7612
|
fn encodeDepthKey(val: f32) -> u32 {
|
|
7384
7613
|
var bits = bitcast<u32>(val);
|
|
7385
7614
|
bits ^= bitcast<u32>(bitcast<i32>(bits) >> 31) | 0x80000000u;
|
|
7386
|
-
return bits;
|
|
7615
|
+
return bits >> ${sortBits === 16 ? "16u" : "0u"};
|
|
7387
7616
|
}
|
|
7388
7617
|
|
|
7389
|
-
// 视锥剔除检查
|
|
7390
|
-
// 基于 rfs-gsplat-render 的 is_in_frustum 实现
|
|
7391
7618
|
fn isInFrustum(clipPos: vec4<f32>, frustumDilation: f32) -> bool {
|
|
7392
7619
|
let clip = (1.0 + frustumDilation) * clipPos.w;
|
|
7393
7620
|
|
|
@@ -7406,43 +7633,47 @@ fn isInFrustum(clipPos: vec4<f32>, frustumDilation: f32) -> bool {
|
|
|
7406
7633
|
fn projectAndCull(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
7407
7634
|
let i = gid.x;
|
|
7408
7635
|
if i >= params.splatCount { return; }
|
|
7636
|
+
|
|
7637
|
+
// LOD 抽稀放最前面:跳过的 splat 不做任何数据加载和矩阵运算
|
|
7638
|
+
// 哈希仅依赖 splat index → 被跳过的集合永远不变 → 零闪烁
|
|
7639
|
+
if params.lodSkipRate > 0.0 {
|
|
7640
|
+
let hash = ((i * 2654435761u) >> 16u) & 0xFFFFu;
|
|
7641
|
+
if f32(hash) < params.lodSkipRate * 65535.0 {
|
|
7642
|
+
return;
|
|
7643
|
+
}
|
|
7644
|
+
}
|
|
7409
7645
|
|
|
7410
|
-
let
|
|
7646
|
+
let splatMean = getSplatMean(i);
|
|
7647
|
+
let splatScale = getSplatScale(i);
|
|
7648
|
+
let splatOpacity = getSplatOpacity(i);
|
|
7411
7649
|
|
|
7412
|
-
|
|
7413
|
-
if splat.opacity < 0.004 { return; }
|
|
7650
|
+
if splatOpacity < 0.004 { return; }
|
|
7414
7651
|
|
|
7415
|
-
|
|
7416
|
-
let worldPos = camera.model * vec4<f32>(splat.mean, 1.0);
|
|
7652
|
+
let worldPos = camera.model * vec4<f32>(splatMean, 1.0);
|
|
7417
7653
|
let viewPos = camera.view * worldPos;
|
|
7418
7654
|
let clipPos = camera.proj * viewPos;
|
|
7419
7655
|
|
|
7420
|
-
// 视锥剔除
|
|
7421
7656
|
if !isInFrustum(clipPos, params.frustumDilation) { return; }
|
|
7422
7657
|
|
|
7423
|
-
|
|
7424
|
-
|
|
7425
|
-
|
|
7426
|
-
|
|
7427
|
-
|
|
7428
|
-
|
|
7429
|
-
|
|
7430
|
-
|
|
7658
|
+
let splatSigma = maxScale(splatScale) * getModelMaxScale(camera.model);
|
|
7659
|
+
let focalY = abs(camera.proj[1][1]) * params.screenHeight * 0.5;
|
|
7660
|
+
let projectedExtent = splatSigma * 3.0 * focalY / max(abs(viewPos.z), 0.001);
|
|
7661
|
+
|
|
7662
|
+
// 剔除投影尺寸过大的 splat(远离模型时去除周围遮挡物)
|
|
7663
|
+
if projectedExtent > params.screenHeight * 0.5 { return; }
|
|
7664
|
+
|
|
7665
|
+
// 剔除投影尺寸过小的 splat
|
|
7666
|
+
if params.pixelThreshold > 0.0 && projectedExtent < params.pixelThreshold { return; }
|
|
7431
7667
|
|
|
7432
|
-
// 深度范围限制:只渲染距相机一定深度范围内的 splat
|
|
7433
7668
|
if params.depthRangeLimit > 0.0 {
|
|
7434
7669
|
if abs(viewPos.z) > params.depthRangeLimit { return; }
|
|
7435
7670
|
}
|
|
7436
7671
|
|
|
7437
|
-
// 深度编码 (viewPos.z 是负数)
|
|
7438
7672
|
let depth = viewPos.z;
|
|
7439
7673
|
let sortableDepth = encodeDepthKey(depth);
|
|
7440
7674
|
|
|
7441
|
-
// 原子增加可见计数并获取索引
|
|
7442
|
-
// indirectBuffer[1] 是 instance_count
|
|
7443
7675
|
let visibleIdx = atomicAdd(&indirectBuffer[1], 1u);
|
|
7444
7676
|
|
|
7445
|
-
// 写入可见点列表
|
|
7446
7677
|
depthKeys[visibleIdx] = sortableDepth;
|
|
7447
7678
|
visibleIndices[visibleIdx] = i;
|
|
7448
7679
|
}
|
|
@@ -7757,7 +7988,7 @@ fn downsweep(
|
|
|
7757
7988
|
);
|
|
7758
7989
|
}
|
|
7759
7990
|
class GSSplatSorter {
|
|
7760
|
-
constructor(device, splatCount, splatBuffer, cameraBuffer, _options = {}) {
|
|
7991
|
+
constructor(device, splatCount, splatBuffer, cameraBuffer, _options = {}, compact = false, half = false, sortBits = 32) {
|
|
7761
7992
|
__publicField(this, "device");
|
|
7762
7993
|
__publicField(this, "splatCount");
|
|
7763
7994
|
// Culling Buffers
|
|
@@ -7787,11 +8018,12 @@ class GSSplatSorter {
|
|
|
7787
8018
|
__publicField(this, "upsweepBindGroupLayout");
|
|
7788
8019
|
__publicField(this, "spineBindGroupLayout");
|
|
7789
8020
|
__publicField(this, "downsweepBindGroupLayout");
|
|
7790
|
-
// Bind groups for each pass
|
|
8021
|
+
// Bind groups for each pass
|
|
7791
8022
|
__publicField(this, "upsweepBindGroups", []);
|
|
7792
8023
|
__publicField(this, "spineBindGroups", []);
|
|
7793
8024
|
__publicField(this, "downsweepBindGroups", []);
|
|
7794
8025
|
__publicField(this, "numPartitions");
|
|
8026
|
+
__publicField(this, "numSortPasses");
|
|
7795
8027
|
// 屏幕信息和剔除选项
|
|
7796
8028
|
__publicField(this, "screenWidth", 1920);
|
|
7797
8029
|
__publicField(this, "screenHeight", 1080);
|
|
@@ -7804,14 +8036,19 @@ class GSSplatSorter {
|
|
|
7804
8036
|
this.device = device;
|
|
7805
8037
|
this.splatCount = splatCount;
|
|
7806
8038
|
this.numPartitions = Math.ceil(splatCount / BLOCK_SIZE);
|
|
8039
|
+
this.numSortPasses = sortBits / RADIX_BITS;
|
|
8040
|
+
const dbg = getDebugOverlay();
|
|
8041
|
+
dbg == null ? void 0 : dbg.info(`Sorter init: ${splatCount} splats, compact=${compact}, half=${half}, sortBits=${sortBits}`);
|
|
7807
8042
|
const cullingModule = device.createShaderModule({
|
|
7808
|
-
code: generateCullingShaderCode$1(),
|
|
8043
|
+
code: generateCullingShaderCode$1(compact, half, sortBits),
|
|
7809
8044
|
label: "culling-shader"
|
|
7810
8045
|
});
|
|
8046
|
+
dbg == null ? void 0 : dbg.checkShader(cullingModule, "culling");
|
|
7811
8047
|
const radixSortModule = device.createShaderModule({
|
|
7812
8048
|
code: generateRadixSortShaderCode(),
|
|
7813
8049
|
label: "radix-sort-shader"
|
|
7814
8050
|
});
|
|
8051
|
+
dbg == null ? void 0 : dbg.checkShader(radixSortModule, "radix-sort");
|
|
7815
8052
|
this.cullingParamsBuffer = device.createBuffer({
|
|
7816
8053
|
size: 48,
|
|
7817
8054
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
@@ -7829,12 +8066,11 @@ class GSSplatSorter {
|
|
|
7829
8066
|
});
|
|
7830
8067
|
this.indirectBuffer = device.createBuffer({
|
|
7831
8068
|
size: 16,
|
|
7832
|
-
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.INDIRECT | GPUBufferUsage.COPY_DST,
|
|
8069
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.INDIRECT | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
|
|
7833
8070
|
label: "indirect-buffer"
|
|
7834
8071
|
});
|
|
7835
8072
|
this.globalHistogramBuffer = device.createBuffer({
|
|
7836
|
-
size: RADIX_SIZE *
|
|
7837
|
-
// 4 passes * 256 bins * 4 bytes
|
|
8073
|
+
size: RADIX_SIZE * this.numSortPasses * 4,
|
|
7838
8074
|
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
7839
8075
|
label: "global-histogram"
|
|
7840
8076
|
});
|
|
@@ -7853,7 +8089,7 @@ class GSSplatSorter {
|
|
|
7853
8089
|
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
7854
8090
|
label: "values-temp"
|
|
7855
8091
|
});
|
|
7856
|
-
for (let i = 0; i <
|
|
8092
|
+
for (let i = 0; i < this.numSortPasses; i++) {
|
|
7857
8093
|
const paramsBuffer = device.createBuffer({
|
|
7858
8094
|
size: 16,
|
|
7859
8095
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
@@ -7965,16 +8201,12 @@ class GSSplatSorter {
|
|
|
7965
8201
|
}
|
|
7966
8202
|
/**
|
|
7967
8203
|
* 创建 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
|
|
8204
|
+
* numSortPasses 个 pass,使用 ping-pong buffers
|
|
8205
|
+
* 最后一个 pass 的 values 输出到 sortedIndicesBuffer
|
|
7975
8206
|
*/
|
|
7976
8207
|
createRadixSortBindGroups() {
|
|
7977
|
-
|
|
8208
|
+
const lastPassIdx = this.numSortPasses - 1;
|
|
8209
|
+
for (let passIdx = 0; passIdx < this.numSortPasses; passIdx++) {
|
|
7978
8210
|
const isEvenPass = passIdx % 2 === 0;
|
|
7979
8211
|
const keysIn = isEvenPass ? this.depthKeysBuffer : this.keysTempBuffer;
|
|
7980
8212
|
const valuesIn = isEvenPass ? this.visibleIndicesBuffer : this.valuesTempBuffer;
|
|
@@ -7982,10 +8214,10 @@ class GSSplatSorter {
|
|
|
7982
8214
|
let valuesOut;
|
|
7983
8215
|
if (isEvenPass) {
|
|
7984
8216
|
keysOut = this.keysTempBuffer;
|
|
7985
|
-
valuesOut = this.valuesTempBuffer;
|
|
8217
|
+
valuesOut = passIdx === lastPassIdx ? this.sortedIndicesBuffer : this.valuesTempBuffer;
|
|
7986
8218
|
} else {
|
|
7987
8219
|
keysOut = this.depthKeysBuffer;
|
|
7988
|
-
valuesOut = passIdx ===
|
|
8220
|
+
valuesOut = passIdx === lastPassIdx ? this.sortedIndicesBuffer : this.visibleIndicesBuffer;
|
|
7989
8221
|
}
|
|
7990
8222
|
this.upsweepBindGroups[passIdx] = this.device.createBindGroup({
|
|
7991
8223
|
layout: this.upsweepBindGroupLayout,
|
|
@@ -8053,16 +8285,15 @@ class GSSplatSorter {
|
|
|
8053
8285
|
view.setFloat32(24, this.cullingOptions.pixelThreshold, true);
|
|
8054
8286
|
view.setUint32(28, this.cullingOptions.maxVisibleCount ?? 0, true);
|
|
8055
8287
|
view.setFloat32(32, this.cullingOptions.depthRangeLimit ?? 0, true);
|
|
8288
|
+
view.setFloat32(36, this.cullingOptions.lodSkipRate ?? 0, true);
|
|
8056
8289
|
this.device.queue.writeBuffer(this.cullingParamsBuffer, 0, cullingParamsData);
|
|
8290
|
+
this.device.queue.writeBuffer(
|
|
8291
|
+
this.indirectBuffer,
|
|
8292
|
+
0,
|
|
8293
|
+
new Uint32Array([4, 0, 0, 0])
|
|
8294
|
+
);
|
|
8057
8295
|
const encoder = this.device.createCommandEncoder({ label: "splat-sort-encoder" });
|
|
8058
8296
|
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
8297
|
{
|
|
8067
8298
|
const pass = encoder.beginComputePass({ label: "project-cull" });
|
|
8068
8299
|
pass.setPipeline(this.projectCullPipeline);
|
|
@@ -8070,7 +8301,7 @@ class GSSplatSorter {
|
|
|
8070
8301
|
pass.dispatchWorkgroups(Math.ceil(this.splatCount / WORKGROUP_SIZE$1));
|
|
8071
8302
|
pass.end();
|
|
8072
8303
|
}
|
|
8073
|
-
for (let passIdx = 0; passIdx <
|
|
8304
|
+
for (let passIdx = 0; passIdx < this.numSortPasses; passIdx++) {
|
|
8074
8305
|
{
|
|
8075
8306
|
const pass = encoder.beginComputePass({ label: `upsweep-p${passIdx}` });
|
|
8076
8307
|
pass.setPipeline(this.upsweepPipeline);
|
|
@@ -8114,6 +8345,24 @@ class GSSplatSorter {
|
|
|
8114
8345
|
getDrawIndirectBuffer() {
|
|
8115
8346
|
return this.indirectBuffer;
|
|
8116
8347
|
}
|
|
8348
|
+
/**
|
|
8349
|
+
* 异步读回 drawIndirect buffer 内容(调试用)
|
|
8350
|
+
* 返回 [vertexCount, instanceCount, firstVertex, firstInstance]
|
|
8351
|
+
*/
|
|
8352
|
+
async readbackIndirect() {
|
|
8353
|
+
const staging = this.device.createBuffer({
|
|
8354
|
+
size: 16,
|
|
8355
|
+
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
|
|
8356
|
+
});
|
|
8357
|
+
const encoder = this.device.createCommandEncoder();
|
|
8358
|
+
encoder.copyBufferToBuffer(this.indirectBuffer, 0, staging, 0, 16);
|
|
8359
|
+
this.device.queue.submit([encoder.finish()]);
|
|
8360
|
+
await staging.mapAsync(GPUMapMode.READ);
|
|
8361
|
+
const result = new Uint32Array(staging.getMappedRange().slice(0));
|
|
8362
|
+
staging.unmap();
|
|
8363
|
+
staging.destroy();
|
|
8364
|
+
return result;
|
|
8365
|
+
}
|
|
8117
8366
|
/**
|
|
8118
8367
|
* 获取 splat 总数量
|
|
8119
8368
|
*/
|
|
@@ -8361,10 +8610,6 @@ fn computeExtentBasisAA(cov2dIn: vec3<f32>, opacity: f32, viewportSize: vec2<f32
|
|
|
8361
8610
|
// 使用基于视口的最大限制 (匹配 PlayCanvas)
|
|
8362
8611
|
let vmin = min(1024.0, min(viewportSize.x, viewportSize.y));
|
|
8363
8612
|
|
|
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
8613
|
let l1 = min(2.0 * sqrt(2.0 * lambda1), vmin);
|
|
8369
8614
|
let l2 = min(2.0 * sqrt(2.0 * lambda2), vmin);
|
|
8370
8615
|
|
|
@@ -8381,7 +8626,6 @@ fn computeExtentBasisAA(cov2dIn: vec3<f32>, opacity: f32, viewportSize: vec2<f32
|
|
|
8381
8626
|
let eigenvector1 = diagVec;
|
|
8382
8627
|
let eigenvector2 = vec2<f32>(diagVec.y, -diagVec.x);
|
|
8383
8628
|
|
|
8384
|
-
// 计算基向量 (不应用额外的 splat_scale,因为我们使用默认值 1.0)
|
|
8385
8629
|
result.basis = vec4<f32>(eigenvector1 * l1, eigenvector2 * l2);
|
|
8386
8630
|
result.adjustedOpacity = alpha;
|
|
8387
8631
|
return result;
|
|
@@ -8775,7 +9019,79 @@ fn fs_depth_normal(input: VertexOutput) -> FragOutput {
|
|
|
8775
9019
|
}
|
|
8776
9020
|
`
|
|
8777
9021
|
);
|
|
9022
|
+
const SPLAT_BYTE_SIZE = 256;
|
|
8778
9023
|
const SPLAT_FLOAT_COUNT = 64;
|
|
9024
|
+
const COMPACT_SPLAT_BYTE_SIZE = 64;
|
|
9025
|
+
const COMPACT_SPLAT_FLOAT_COUNT = 16;
|
|
9026
|
+
const HALF_SPLAT_BYTE_SIZE = 32;
|
|
9027
|
+
const HALF_SPLAT_U32_COUNT = 8;
|
|
9028
|
+
function transformShaderForCompact(code) {
|
|
9029
|
+
code = code.replace(
|
|
9030
|
+
/struct Splat \{[\s\S]*?\n\}/,
|
|
9031
|
+
`struct Splat {
|
|
9032
|
+
mean: vec3<f32>, _pad0: f32,
|
|
9033
|
+
scale: vec3<f32>, _pad1: f32,
|
|
9034
|
+
rotation: vec4<f32>,
|
|
9035
|
+
colorDC: vec3<f32>,
|
|
9036
|
+
opacity: f32,
|
|
9037
|
+
}`
|
|
9038
|
+
);
|
|
9039
|
+
code = code.replace(
|
|
9040
|
+
/fn evalSH\(splat: Splat, dir: vec3<f32>\) -> vec3<f32> \{[\s\S]*?\n\}/,
|
|
9041
|
+
`fn evalSH(splat: Splat, dir: vec3<f32>) -> vec3<f32> {
|
|
9042
|
+
return vec3<f32>(0.0);
|
|
9043
|
+
}`
|
|
9044
|
+
);
|
|
9045
|
+
return code;
|
|
9046
|
+
}
|
|
9047
|
+
function transformShaderForHalf(code) {
|
|
9048
|
+
code = code.replace(
|
|
9049
|
+
/struct Splat \{[\s\S]*?\n\}/,
|
|
9050
|
+
`struct Splat {
|
|
9051
|
+
mean: vec3<f32>, _pad0: f32,
|
|
9052
|
+
scale: vec3<f32>, _pad1: f32,
|
|
9053
|
+
rotation: vec4<f32>,
|
|
9054
|
+
colorDC: vec3<f32>,
|
|
9055
|
+
opacity: f32,
|
|
9056
|
+
}`
|
|
9057
|
+
);
|
|
9058
|
+
code = code.replace(
|
|
9059
|
+
/@group\(0\)\s*@binding\((\d+)\)\s*var<storage,\s*read>\s*splats\s*:\s*array<Splat>/,
|
|
9060
|
+
`@group(0) @binding($1) var<storage, read> splatDataU32: array<u32>`
|
|
9061
|
+
);
|
|
9062
|
+
const unpackFunctions = `
|
|
9063
|
+
const HALF_U32S: u32 = 8u;
|
|
9064
|
+
fn unpackSplatFromHalf(idx: u32) -> Splat {
|
|
9065
|
+
let b = idx * HALF_U32S;
|
|
9066
|
+
let mean_xy = unpack2x16float(splatDataU32[b]);
|
|
9067
|
+
let mean_zp = unpack2x16float(splatDataU32[b + 1u]);
|
|
9068
|
+
let sc_xy = unpack2x16float(splatDataU32[b + 2u]);
|
|
9069
|
+
let sc_z_op = unpack2x16float(splatDataU32[b + 3u]);
|
|
9070
|
+
let r_xy = unpack2x16float(splatDataU32[b + 4u]);
|
|
9071
|
+
let r_zw = unpack2x16float(splatDataU32[b + 5u]);
|
|
9072
|
+
let c_rg = unpack2x16float(splatDataU32[b + 6u]);
|
|
9073
|
+
let c_bp = unpack2x16float(splatDataU32[b + 7u]);
|
|
9074
|
+
var s: Splat;
|
|
9075
|
+
s.mean = vec3<f32>(mean_xy.x, mean_xy.y, mean_zp.x);
|
|
9076
|
+
s._pad0 = 0.0;
|
|
9077
|
+
s.scale = vec3<f32>(sc_xy.x, sc_xy.y, sc_z_op.x);
|
|
9078
|
+
s._pad1 = 0.0;
|
|
9079
|
+
s.rotation = vec4<f32>(r_xy.x, r_xy.y, r_zw.x, r_zw.y);
|
|
9080
|
+
s.colorDC = vec3<f32>(c_rg.x, c_rg.y, c_bp.x);
|
|
9081
|
+
s.opacity = sc_z_op.y;
|
|
9082
|
+
return s;
|
|
9083
|
+
}
|
|
9084
|
+
`;
|
|
9085
|
+
code = code.replace(/(@vertex|@compute)/, unpackFunctions + "$1");
|
|
9086
|
+
code = code.replace(/splats\[([^\]]+)\]/g, "unpackSplatFromHalf($1)");
|
|
9087
|
+
code = code.replace(
|
|
9088
|
+
/fn evalSH\(splat: Splat, dir: vec3<f32>\) -> vec3<f32> \{[\s\S]*?\n\}/,
|
|
9089
|
+
`fn evalSH(splat: Splat, dir: vec3<f32>) -> vec3<f32> {
|
|
9090
|
+
return vec3<f32>(0.0);
|
|
9091
|
+
}`
|
|
9092
|
+
);
|
|
9093
|
+
return code;
|
|
9094
|
+
}
|
|
8779
9095
|
const _GSSplatRenderer = class _GSSplatRenderer {
|
|
8780
9096
|
constructor(renderer, camera) {
|
|
8781
9097
|
__publicField(this, "renderer");
|
|
@@ -8789,6 +9105,12 @@ const _GSSplatRenderer = class _GSSplatRenderer {
|
|
|
8789
9105
|
__publicField(this, "sorter", null);
|
|
8790
9106
|
__publicField(this, "shMode", SHMode.L3);
|
|
8791
9107
|
__publicField(this, "boundingBox", null);
|
|
9108
|
+
/** 紧凑布局模式:64B/splat,无 SH,适合移动端 */
|
|
9109
|
+
__publicField(this, "compactLayout", false);
|
|
9110
|
+
/** 半精度模式:32B/splat,所有数据用 f16 打包 */
|
|
9111
|
+
__publicField(this, "halfPrecision", false);
|
|
9112
|
+
/** 排序位宽:16-bit 排序减少一半 radix sort pass */
|
|
9113
|
+
__publicField(this, "sortBits", 32);
|
|
8792
9114
|
// 预分配 uniform 上传缓冲区,避免每帧 GC(56 floats = 224 bytes)
|
|
8793
9115
|
__publicField(this, "uniformData", new Float32Array(56));
|
|
8794
9116
|
__publicField(this, "cpuPositions", null);
|
|
@@ -8802,6 +9124,7 @@ const _GSSplatRenderer = class _GSSplatRenderer {
|
|
|
8802
9124
|
__publicField(this, "pixelCullThreshold", 1);
|
|
8803
9125
|
__publicField(this, "maxVisibleSplats", 0);
|
|
8804
9126
|
__publicField(this, "depthRangeLimit", 0);
|
|
9127
|
+
__publicField(this, "lodSkipRate", 0);
|
|
8805
9128
|
// 排序优化:相机变化检测 + 频率控制
|
|
8806
9129
|
__publicField(this, "lastSortViewMatrix", new Float32Array(16));
|
|
8807
9130
|
__publicField(this, "lastSortProjMatrix", new Float32Array(16));
|
|
@@ -8814,6 +9137,7 @@ const _GSSplatRenderer = class _GSSplatRenderer {
|
|
|
8814
9137
|
__publicField(this, "sortStateInitialized", false);
|
|
8815
9138
|
__publicField(this, "sortFrequency", 1);
|
|
8816
9139
|
__publicField(this, "frameCounter", 0);
|
|
9140
|
+
__publicField(this, "debugFrameLogged", false);
|
|
8817
9141
|
// 编辑器状态
|
|
8818
9142
|
__publicField(this, "editorStateBuffer", null);
|
|
8819
9143
|
__publicField(this, "editorPipeline", null);
|
|
@@ -8838,15 +9162,56 @@ const _GSSplatRenderer = class _GSSplatRenderer {
|
|
|
8838
9162
|
this.createUniformBuffer();
|
|
8839
9163
|
this.updateModelMatrix();
|
|
8840
9164
|
}
|
|
9165
|
+
/**
|
|
9166
|
+
* 启用/禁用紧凑布局模式(64B/splat,无 SH)。
|
|
9167
|
+
* 必须在 setCompactData / setData 之前调用。
|
|
9168
|
+
*/
|
|
9169
|
+
setCompactLayout(compact) {
|
|
9170
|
+
if (this.compactLayout === compact) return;
|
|
9171
|
+
this.compactLayout = compact;
|
|
9172
|
+
if (compact) {
|
|
9173
|
+
this.shMode = SHMode.L0;
|
|
9174
|
+
}
|
|
9175
|
+
this.createPipeline();
|
|
9176
|
+
}
|
|
9177
|
+
/**
|
|
9178
|
+
* 设置排序位宽。16-bit 模式排序 pass 从 4 降到 2,排序速度翻倍。
|
|
9179
|
+
* 必须在 setData / setCompactData 之前调用。
|
|
9180
|
+
*/
|
|
9181
|
+
setSortBits(bits2) {
|
|
9182
|
+
this.sortBits = bits2;
|
|
9183
|
+
}
|
|
9184
|
+
/**
|
|
9185
|
+
* 启用/禁用半精度模式(32B/splat,所有数据 f16 打包)。
|
|
9186
|
+
* 自动启用 compactLayout。必须在 setCompactData 之前调用。
|
|
9187
|
+
*/
|
|
9188
|
+
setHalfPrecision(half) {
|
|
9189
|
+
if (this.halfPrecision === half) return;
|
|
9190
|
+
this.halfPrecision = half;
|
|
9191
|
+
if (half) {
|
|
9192
|
+
this.compactLayout = true;
|
|
9193
|
+
this.shMode = SHMode.L0;
|
|
9194
|
+
}
|
|
9195
|
+
this.createPipeline();
|
|
9196
|
+
}
|
|
8841
9197
|
createPipeline() {
|
|
8842
9198
|
const device = this.renderer.device;
|
|
8843
|
-
const
|
|
9199
|
+
const dbg = getDebugOverlay();
|
|
9200
|
+
let shaderCode = gsOptimizedShader.replace(
|
|
8844
9201
|
"const SH_LEVEL: u32 = 3u; // @SH_LEVEL_INJECT@",
|
|
8845
9202
|
`const SH_LEVEL: u32 = ${this.shMode}u;`
|
|
8846
9203
|
);
|
|
9204
|
+
if (this.halfPrecision) {
|
|
9205
|
+
shaderCode = transformShaderForHalf(shaderCode);
|
|
9206
|
+
} else if (this.compactLayout) {
|
|
9207
|
+
shaderCode = transformShaderForCompact(shaderCode);
|
|
9208
|
+
}
|
|
9209
|
+
dbg == null ? void 0 : dbg.info(`createPipeline: SH=${this.shMode}, compact=${this.compactLayout}, half=${this.halfPrecision}`);
|
|
8847
9210
|
const shaderModule = device.createShaderModule({
|
|
8848
|
-
code: shaderCode
|
|
9211
|
+
code: shaderCode,
|
|
9212
|
+
label: "gs-main-shader"
|
|
8849
9213
|
});
|
|
9214
|
+
dbg == null ? void 0 : dbg.checkShader(shaderModule, "gs-main");
|
|
8850
9215
|
this.bindGroupLayout = device.createBindGroupLayout({
|
|
8851
9216
|
entries: [
|
|
8852
9217
|
{
|
|
@@ -8923,8 +9288,12 @@ const _GSSplatRenderer = class _GSSplatRenderer {
|
|
|
8923
9288
|
// A采用"zero add one-minus-src-alpha"的混合模式计算Transmittance
|
|
8924
9289
|
createDepthNormalPipeline() {
|
|
8925
9290
|
const device = this.renderer.device;
|
|
9291
|
+
let dnShader = gsDepthNormalShader;
|
|
9292
|
+
if (this.compactLayout) {
|
|
9293
|
+
dnShader = transformShaderForCompact(dnShader);
|
|
9294
|
+
}
|
|
8926
9295
|
const shaderModule = device.createShaderModule({
|
|
8927
|
-
code:
|
|
9296
|
+
code: dnShader
|
|
8928
9297
|
});
|
|
8929
9298
|
const pipelineLayout = device.createPipelineLayout({
|
|
8930
9299
|
bindGroupLayouts: [this.bindGroupLayout]
|
|
@@ -9103,6 +9472,17 @@ const _GSSplatRenderer = class _GSSplatRenderer {
|
|
|
9103
9472
|
getDepthRangeLimit() {
|
|
9104
9473
|
return this.depthRangeLimit;
|
|
9105
9474
|
}
|
|
9475
|
+
/**
|
|
9476
|
+
* 设置 LOD 抽稀率(0~1)
|
|
9477
|
+
* 远处 splat 按此比例随机跳过(确定性哈希,无闪烁)
|
|
9478
|
+
* 0 = 不抽稀
|
|
9479
|
+
*/
|
|
9480
|
+
setLodSkipRate(rate) {
|
|
9481
|
+
this.lodSkipRate = Math.max(0, Math.min(1, rate));
|
|
9482
|
+
}
|
|
9483
|
+
getLodSkipRate() {
|
|
9484
|
+
return this.lodSkipRate;
|
|
9485
|
+
}
|
|
9106
9486
|
/**
|
|
9107
9487
|
* 设置排序频率
|
|
9108
9488
|
* 1 = 每帧排序(默认),2 = 每 2 帧排序一次,以此类推
|
|
@@ -9145,6 +9525,7 @@ const _GSSplatRenderer = class _GSSplatRenderer {
|
|
|
9145
9525
|
}
|
|
9146
9526
|
setData(splats) {
|
|
9147
9527
|
const device = this.renderer.device;
|
|
9528
|
+
const dbg = getDebugOverlay();
|
|
9148
9529
|
if (this.splatBuffer) {
|
|
9149
9530
|
this.splatBuffer.destroy();
|
|
9150
9531
|
}
|
|
@@ -9159,12 +9540,24 @@ const _GSSplatRenderer = class _GSSplatRenderer {
|
|
|
9159
9540
|
this.boundingBox = null;
|
|
9160
9541
|
return;
|
|
9161
9542
|
}
|
|
9543
|
+
const floatsPerSplat = this.compactLayout ? COMPACT_SPLAT_FLOAT_COUNT : SPLAT_FLOAT_COUNT;
|
|
9544
|
+
const bytesPerSplat = floatsPerSplat * 4;
|
|
9545
|
+
const maxStorageSize = device.limits.maxStorageBufferBindingSize;
|
|
9546
|
+
const maxSplatsForGPU = Math.floor(maxStorageSize / bytesPerSplat);
|
|
9547
|
+
if (this.splatCount > maxSplatsForGPU) {
|
|
9548
|
+
dbg == null ? void 0 : dbg.warn(
|
|
9549
|
+
`setData: truncating ${this.splatCount} -> ${maxSplatsForGPU} splats (maxStorageBufferBindingSize=${(maxStorageSize / 1048576).toFixed(0)}MB)`
|
|
9550
|
+
);
|
|
9551
|
+
this.splatCount = maxSplatsForGPU;
|
|
9552
|
+
splats = splats.slice(0, this.splatCount);
|
|
9553
|
+
}
|
|
9554
|
+
dbg == null ? void 0 : dbg.info(`setData: ${this.splatCount} splats, compact=${this.compactLayout}`);
|
|
9162
9555
|
this.boundingBox = this.computeBoundingBox(splats);
|
|
9163
9556
|
const positions = new Float32Array(this.splatCount * 3);
|
|
9164
|
-
const data = new Float32Array(this.splatCount *
|
|
9557
|
+
const data = new Float32Array(this.splatCount * floatsPerSplat);
|
|
9165
9558
|
for (let i = 0; i < this.splatCount; i++) {
|
|
9166
9559
|
const splat = splats[i];
|
|
9167
|
-
const offset = i *
|
|
9560
|
+
const offset = i * floatsPerSplat;
|
|
9168
9561
|
positions[i * 3 + 0] = splat.mean[0];
|
|
9169
9562
|
positions[i * 3 + 1] = splat.mean[1];
|
|
9170
9563
|
positions[i * 3 + 2] = splat.mean[2];
|
|
@@ -9184,31 +9577,39 @@ const _GSSplatRenderer = class _GSSplatRenderer {
|
|
|
9184
9577
|
data[offset + 13] = splat.colorDC[1];
|
|
9185
9578
|
data[offset + 14] = splat.colorDC[2];
|
|
9186
9579
|
data[offset + 15] = splat.opacity;
|
|
9187
|
-
|
|
9188
|
-
|
|
9189
|
-
|
|
9190
|
-
|
|
9191
|
-
|
|
9192
|
-
|
|
9193
|
-
|
|
9194
|
-
|
|
9195
|
-
|
|
9580
|
+
if (!this.compactLayout) {
|
|
9581
|
+
const shRest = splat.shRest;
|
|
9582
|
+
for (let j = 0; j < 9; j++) {
|
|
9583
|
+
data[offset + 16 + j] = shRest ? shRest[j] : 0;
|
|
9584
|
+
}
|
|
9585
|
+
for (let j = 0; j < 15; j++) {
|
|
9586
|
+
data[offset + 25 + j] = shRest ? shRest[9 + j] : 0;
|
|
9587
|
+
}
|
|
9588
|
+
for (let j = 0; j < 21; j++) {
|
|
9589
|
+
data[offset + 40 + j] = shRest ? shRest[24 + j] : 0;
|
|
9590
|
+
}
|
|
9591
|
+
data[offset + 61] = 0;
|
|
9592
|
+
data[offset + 62] = 0;
|
|
9593
|
+
data[offset + 63] = 0;
|
|
9196
9594
|
}
|
|
9197
|
-
data[offset + 61] = 0;
|
|
9198
|
-
data[offset + 62] = 0;
|
|
9199
|
-
data[offset + 63] = 0;
|
|
9200
9595
|
}
|
|
9596
|
+
dbg == null ? void 0 : dbg.info(`splatBuffer: ${(data.byteLength / 1048576).toFixed(1)}MB (${floatsPerSplat} floats/splat)`);
|
|
9201
9597
|
this.splatBuffer = device.createBuffer({
|
|
9202
9598
|
size: data.byteLength,
|
|
9203
9599
|
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
9204
9600
|
});
|
|
9205
9601
|
device.queue.writeBuffer(this.splatBuffer, 0, data);
|
|
9206
9602
|
this.cpuPositions = positions;
|
|
9603
|
+
dbg == null ? void 0 : dbg.info(`Creating sorter: compact=${this.compactLayout}, sortBits=${this.sortBits}`);
|
|
9207
9604
|
this.sorter = new GSSplatSorter(
|
|
9208
9605
|
device,
|
|
9209
9606
|
this.splatCount,
|
|
9210
9607
|
this.splatBuffer,
|
|
9211
|
-
this.uniformBuffer
|
|
9608
|
+
this.uniformBuffer,
|
|
9609
|
+
{},
|
|
9610
|
+
this.compactLayout,
|
|
9611
|
+
false,
|
|
9612
|
+
this.sortBits
|
|
9212
9613
|
);
|
|
9213
9614
|
this.sorter.setScreenSize(this.renderer.width, this.renderer.height);
|
|
9214
9615
|
this.sorter.setCullingOptions({
|
|
@@ -9224,10 +9625,12 @@ const _GSSplatRenderer = class _GSSplatRenderer {
|
|
|
9224
9625
|
{ binding: 2, resource: { buffer: this.sorter.getIndicesBuffer() } }
|
|
9225
9626
|
]
|
|
9226
9627
|
});
|
|
9628
|
+
dbg == null ? void 0 : dbg.info(`setData complete, bindGroup created`);
|
|
9227
9629
|
if (this.editorEnabled) this.rebuildEditorBindGroup();
|
|
9228
9630
|
}
|
|
9229
9631
|
setCompactData(compactData) {
|
|
9230
9632
|
const device = this.renderer.device;
|
|
9633
|
+
const dbg = getDebugOverlay();
|
|
9231
9634
|
if (this.splatBuffer) {
|
|
9232
9635
|
this.splatBuffer.destroy();
|
|
9233
9636
|
}
|
|
@@ -9242,20 +9645,98 @@ const _GSSplatRenderer = class _GSSplatRenderer {
|
|
|
9242
9645
|
this.boundingBox = null;
|
|
9243
9646
|
return;
|
|
9244
9647
|
}
|
|
9648
|
+
const bytesPerSplat = this.halfPrecision ? HALF_SPLAT_BYTE_SIZE : this.compactLayout ? COMPACT_SPLAT_BYTE_SIZE : SPLAT_BYTE_SIZE;
|
|
9649
|
+
const maxStorageSize = device.limits.maxStorageBufferBindingSize;
|
|
9650
|
+
const maxSplatsForGPU = Math.floor(maxStorageSize / bytesPerSplat);
|
|
9651
|
+
dbg == null ? void 0 : dbg.info(
|
|
9652
|
+
`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}`
|
|
9653
|
+
);
|
|
9654
|
+
if (this.splatCount > maxSplatsForGPU) {
|
|
9655
|
+
dbg == null ? void 0 : dbg.warn(
|
|
9656
|
+
`Splat count ${this.splatCount} exceeds GPU maxStorageBufferBindingSize (${(maxStorageSize / 1048576).toFixed(0)}MB = ${maxSplatsForGPU} splats). Truncating to ${maxSplatsForGPU}.`
|
|
9657
|
+
);
|
|
9658
|
+
this.splatCount = maxSplatsForGPU;
|
|
9659
|
+
}
|
|
9245
9660
|
this.boundingBox = this.computeBoundingBoxFromCompact(compactData);
|
|
9246
|
-
|
|
9247
|
-
|
|
9248
|
-
|
|
9661
|
+
let trimmedData;
|
|
9662
|
+
if (this.halfPrecision && this.lodSkipRate > 0) {
|
|
9663
|
+
const skipRate = this.lodSkipRate;
|
|
9664
|
+
const threshold = skipRate * 65535;
|
|
9665
|
+
const kept = [];
|
|
9666
|
+
for (let i = 0; i < this.splatCount; i++) {
|
|
9667
|
+
const hash = (i * 2654435761 | 0) >>> 16 & 65535;
|
|
9668
|
+
if (hash >= threshold) kept.push(i);
|
|
9669
|
+
}
|
|
9670
|
+
const n = kept.length;
|
|
9671
|
+
const pos = new Float32Array(n * 3);
|
|
9672
|
+
const scl = new Float32Array(n * 3);
|
|
9673
|
+
const rot = new Float32Array(n * 4);
|
|
9674
|
+
const col = new Float32Array(n * 3);
|
|
9675
|
+
const opa = new Float32Array(n);
|
|
9676
|
+
for (let k = 0; k < n; k++) {
|
|
9677
|
+
const i = kept[k];
|
|
9678
|
+
pos[k * 3] = compactData.positions[i * 3];
|
|
9679
|
+
pos[k * 3 + 1] = compactData.positions[i * 3 + 1];
|
|
9680
|
+
pos[k * 3 + 2] = compactData.positions[i * 3 + 2];
|
|
9681
|
+
scl[k * 3] = compactData.scales[i * 3];
|
|
9682
|
+
scl[k * 3 + 1] = compactData.scales[i * 3 + 1];
|
|
9683
|
+
scl[k * 3 + 2] = compactData.scales[i * 3 + 2];
|
|
9684
|
+
rot[k * 4] = compactData.rotations[i * 4];
|
|
9685
|
+
rot[k * 4 + 1] = compactData.rotations[i * 4 + 1];
|
|
9686
|
+
rot[k * 4 + 2] = compactData.rotations[i * 4 + 2];
|
|
9687
|
+
rot[k * 4 + 3] = compactData.rotations[i * 4 + 3];
|
|
9688
|
+
col[k * 3] = compactData.colors[i * 3];
|
|
9689
|
+
col[k * 3 + 1] = compactData.colors[i * 3 + 1];
|
|
9690
|
+
col[k * 3 + 2] = compactData.colors[i * 3 + 2];
|
|
9691
|
+
opa[k] = compactData.opacities[i];
|
|
9692
|
+
}
|
|
9693
|
+
trimmedData = { count: n, positions: pos, scales: scl, rotations: rot, colors: col, opacities: opa };
|
|
9694
|
+
this.splatCount = n;
|
|
9695
|
+
dbg == null ? void 0 : dbg.info(`CPU LOD filter: ${compactData.count} → ${n} splats (skip ${(skipRate * 100).toFixed(0)}%)`);
|
|
9696
|
+
} else {
|
|
9697
|
+
const includeSH = !this.compactLayout && compactData.shCoeffs !== void 0;
|
|
9698
|
+
trimmedData = this.splatCount < compactData.count ? {
|
|
9699
|
+
count: this.splatCount,
|
|
9700
|
+
positions: compactData.positions.slice(0, this.splatCount * 3),
|
|
9701
|
+
scales: compactData.scales.slice(0, this.splatCount * 3),
|
|
9702
|
+
rotations: compactData.rotations.slice(0, this.splatCount * 4),
|
|
9703
|
+
colors: compactData.colors.slice(0, this.splatCount * 3),
|
|
9704
|
+
opacities: compactData.opacities.slice(0, this.splatCount),
|
|
9705
|
+
shCoeffs: includeSH && compactData.shCoeffs ? compactData.shCoeffs.slice(0, this.splatCount * compactData.shCoeffs.length / compactData.count) : void 0
|
|
9706
|
+
} : compactData;
|
|
9707
|
+
}
|
|
9708
|
+
dbg == null ? void 0 : dbg.info(`setCompactData: ${this.splatCount} splats, compact=${this.compactLayout}, half=${this.halfPrecision}`);
|
|
9709
|
+
this.cpuPositions = new Float32Array(trimmedData.positions);
|
|
9710
|
+
let gpuBuf;
|
|
9711
|
+
if (this.halfPrecision) {
|
|
9712
|
+
const halfData = compactDataToGPUBufferHalf(trimmedData);
|
|
9713
|
+
gpuBuf = halfData.buffer;
|
|
9714
|
+
dbg == null ? void 0 : dbg.info(
|
|
9715
|
+
`gpuData(half): ${(halfData.byteLength / 1048576).toFixed(1)}MB, u32s=${halfData.length}, u32/splat=${HALF_SPLAT_U32_COUNT}`
|
|
9716
|
+
);
|
|
9717
|
+
} else {
|
|
9718
|
+
const includeSH = !this.compactLayout && compactData.shCoeffs !== void 0;
|
|
9719
|
+
const f32Data = compactDataToGPUBuffer(trimmedData, includeSH);
|
|
9720
|
+
gpuBuf = f32Data.buffer;
|
|
9721
|
+
dbg == null ? void 0 : dbg.info(
|
|
9722
|
+
`gpuData: ${(f32Data.byteLength / 1048576).toFixed(1)}MB, includeSH=${includeSH}, floats=${f32Data.length}, floats/splat=${f32Data.length / this.splatCount}`
|
|
9723
|
+
);
|
|
9724
|
+
}
|
|
9249
9725
|
this.splatBuffer = device.createBuffer({
|
|
9250
|
-
size:
|
|
9726
|
+
size: gpuBuf.byteLength,
|
|
9251
9727
|
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
9252
9728
|
});
|
|
9253
|
-
device.queue.writeBuffer(this.splatBuffer, 0,
|
|
9729
|
+
device.queue.writeBuffer(this.splatBuffer, 0, gpuBuf);
|
|
9730
|
+
dbg == null ? void 0 : dbg.info(`Creating sorter: compact=${this.compactLayout}, half=${this.halfPrecision}, sortBits=${this.sortBits}`);
|
|
9254
9731
|
this.sorter = new GSSplatSorter(
|
|
9255
9732
|
device,
|
|
9256
9733
|
this.splatCount,
|
|
9257
9734
|
this.splatBuffer,
|
|
9258
|
-
this.uniformBuffer
|
|
9735
|
+
this.uniformBuffer,
|
|
9736
|
+
{},
|
|
9737
|
+
this.compactLayout,
|
|
9738
|
+
this.halfPrecision,
|
|
9739
|
+
this.sortBits
|
|
9259
9740
|
);
|
|
9260
9741
|
this.sorter.setScreenSize(this.renderer.width, this.renderer.height);
|
|
9261
9742
|
this.sorter.setCullingOptions({
|
|
@@ -9271,6 +9752,7 @@ const _GSSplatRenderer = class _GSSplatRenderer {
|
|
|
9271
9752
|
{ binding: 2, resource: { buffer: this.sorter.getIndicesBuffer() } }
|
|
9272
9753
|
]
|
|
9273
9754
|
});
|
|
9755
|
+
dbg == null ? void 0 : dbg.info(`setCompactData complete, bindGroup created`);
|
|
9274
9756
|
if (this.editorEnabled) this.rebuildEditorBindGroup();
|
|
9275
9757
|
this.sortStateInitialized = false;
|
|
9276
9758
|
}
|
|
@@ -9294,7 +9776,7 @@ const _GSSplatRenderer = class _GSSplatRenderer {
|
|
|
9294
9776
|
this.renderer.device.queue.writeBuffer(this.uniformBuffer, 0, ud);
|
|
9295
9777
|
const changed = this.needsSort();
|
|
9296
9778
|
this.frameCounter++;
|
|
9297
|
-
const shouldSort =
|
|
9779
|
+
const shouldSort = !this.sortStateInitialized || changed;
|
|
9298
9780
|
if (shouldSort) {
|
|
9299
9781
|
this.sorter.setScreenSize(this.renderer.width, this.renderer.height);
|
|
9300
9782
|
this.sorter.setCullingOptions({
|
|
@@ -9302,9 +9784,24 @@ const _GSSplatRenderer = class _GSSplatRenderer {
|
|
|
9302
9784
|
farPlane: this.camera.far,
|
|
9303
9785
|
pixelThreshold: this.pixelCullThreshold,
|
|
9304
9786
|
maxVisibleCount: this.maxVisibleSplats,
|
|
9305
|
-
depthRangeLimit: this.depthRangeLimit
|
|
9787
|
+
depthRangeLimit: this.depthRangeLimit,
|
|
9788
|
+
lodSkipRate: this.lodSkipRate
|
|
9306
9789
|
});
|
|
9307
|
-
this.
|
|
9790
|
+
if (!this.debugFrameLogged) {
|
|
9791
|
+
const dbg = getDebugOverlay();
|
|
9792
|
+
const dev = this.renderer.device;
|
|
9793
|
+
dev.pushErrorScope("validation");
|
|
9794
|
+
dev.pushErrorScope("out-of-memory");
|
|
9795
|
+
this.sorter.sort();
|
|
9796
|
+
dev.popErrorScope().then((err2) => {
|
|
9797
|
+
if (err2) dbg == null ? void 0 : dbg.error(`Sort OOM: ${err2.message}`);
|
|
9798
|
+
});
|
|
9799
|
+
dev.popErrorScope().then((err2) => {
|
|
9800
|
+
if (err2) dbg == null ? void 0 : dbg.error(`Sort Validation: ${err2.message}`);
|
|
9801
|
+
});
|
|
9802
|
+
} else {
|
|
9803
|
+
this.sorter.sort();
|
|
9804
|
+
}
|
|
9308
9805
|
}
|
|
9309
9806
|
if (this.editorEnabled && this.editorPipeline && this.editorBindGroup) {
|
|
9310
9807
|
pass.setPipeline(this.editorPipeline);
|
|
@@ -9314,6 +9811,28 @@ const _GSSplatRenderer = class _GSSplatRenderer {
|
|
|
9314
9811
|
pass.setBindGroup(0, this.bindGroup);
|
|
9315
9812
|
}
|
|
9316
9813
|
pass.drawIndirect(this.sorter.getDrawIndirectBuffer(), 0);
|
|
9814
|
+
if (!this.debugFrameLogged) {
|
|
9815
|
+
this.debugFrameLogged = true;
|
|
9816
|
+
const dbg = getDebugOverlay();
|
|
9817
|
+
dbg == null ? void 0 : dbg.info(
|
|
9818
|
+
`First render: splats=${this.splatCount}, canvas=${this.renderer.width}x${this.renderer.height}, editor=${this.editorEnabled}, depthWrite=${this.depthWriteEnabled}`
|
|
9819
|
+
);
|
|
9820
|
+
if (dbg && this.sorter) {
|
|
9821
|
+
const pos2 = this.camera.position;
|
|
9822
|
+
dbg.info(`Camera pos: [${pos2[0].toFixed(2)}, ${pos2[1].toFixed(2)}, ${pos2[2].toFixed(2)}]`);
|
|
9823
|
+
dbg.info(
|
|
9824
|
+
`Culling cfg: pixelThresh=${this.pixelCullThreshold.toFixed(2)}, maxVisible=${this.maxVisibleSplats}, depthRange=${this.depthRangeLimit.toFixed(2)}, near=${this.camera.near}, far=${this.camera.far}`
|
|
9825
|
+
);
|
|
9826
|
+
this.sorter.readbackIndirect().then((data) => {
|
|
9827
|
+
dbg.info(
|
|
9828
|
+
`DrawIndirect: vtx=${data[0]}, inst=${data[1]}, firstVtx=${data[2]}, firstInst=${data[3]}`
|
|
9829
|
+
);
|
|
9830
|
+
if (data[0] === 0 && data[1] === 0) {
|
|
9831
|
+
dbg.error("ALL splats culled! visible=0");
|
|
9832
|
+
}
|
|
9833
|
+
});
|
|
9834
|
+
}
|
|
9835
|
+
}
|
|
9317
9836
|
}
|
|
9318
9837
|
getSplatCount() {
|
|
9319
9838
|
return this.splatCount;
|
|
@@ -9631,10 +10150,13 @@ const _GSSplatRenderer = class _GSSplatRenderer {
|
|
|
9631
10150
|
}
|
|
9632
10151
|
createEditorPipeline() {
|
|
9633
10152
|
const device = this.renderer.device;
|
|
9634
|
-
|
|
10153
|
+
let editorShaderCode = this.buildEditorShader().replace(
|
|
9635
10154
|
"const SH_LEVEL: u32 = 3u; // @SH_LEVEL_INJECT@",
|
|
9636
10155
|
`const SH_LEVEL: u32 = ${this.shMode}u;`
|
|
9637
10156
|
);
|
|
10157
|
+
if (this.compactLayout) {
|
|
10158
|
+
editorShaderCode = transformShaderForCompact(editorShaderCode);
|
|
10159
|
+
}
|
|
9638
10160
|
const shaderModule = device.createShaderModule({ code: editorShaderCode });
|
|
9639
10161
|
this.editorBindGroupLayout = device.createBindGroupLayout({
|
|
9640
10162
|
entries: [
|
|
@@ -17568,186 +18090,31 @@ class EyedropperSelection {
|
|
|
17568
18090
|
}
|
|
17569
18091
|
}
|
|
17570
18092
|
class SphereSelection {
|
|
17571
|
-
constructor(
|
|
17572
|
-
__publicField(this, "parent");
|
|
17573
|
-
__publicField(this, "toolbar");
|
|
17574
|
-
__publicField(this, "callbacks");
|
|
18093
|
+
constructor(onRadiusChanged) {
|
|
17575
18094
|
__publicField(this, "_radius", 1);
|
|
17576
|
-
__publicField(this, "
|
|
17577
|
-
this.
|
|
17578
|
-
this.callbacks = callbacks;
|
|
17579
|
-
this.toolbar = document.createElement("div");
|
|
17580
|
-
this.toolbar.className = "volume-select-toolbar";
|
|
17581
|
-
this.toolbar.style.cssText = `
|
|
17582
|
-
position:absolute; bottom:90px; left:50%; transform:translateX(-50%);
|
|
17583
|
-
display:none; z-index:20; background:rgba(30,30,30,0.92);
|
|
17584
|
-
border-radius:8px; padding:6px 10px; gap:6px;
|
|
17585
|
-
align-items:center; font-size:13px; color:#ddd;
|
|
17586
|
-
backdrop-filter:blur(6px); user-select:none; white-space:nowrap;
|
|
17587
|
-
box-shadow:0 2px 12px rgba(0,0,0,0.4);
|
|
17588
|
-
`;
|
|
17589
|
-
this.toolbar.addEventListener("pointerdown", (e) => e.stopPropagation());
|
|
17590
|
-
this.toolbar.addEventListener("wheel", (e) => e.stopPropagation());
|
|
17591
|
-
const mkBtn = (label2, op) => {
|
|
17592
|
-
const btn = document.createElement("button");
|
|
17593
|
-
btn.textContent = label2;
|
|
17594
|
-
btn.style.cssText = `
|
|
17595
|
-
padding:4px 12px; border:1px solid #555; border-radius:4px;
|
|
17596
|
-
background:#333; color:#ddd; cursor:pointer; font-size:13px;
|
|
17597
|
-
transition: background 0.15s;
|
|
17598
|
-
`;
|
|
17599
|
-
btn.addEventListener("mouseenter", () => {
|
|
17600
|
-
btn.style.background = "#555";
|
|
17601
|
-
});
|
|
17602
|
-
btn.addEventListener("mouseleave", () => {
|
|
17603
|
-
btn.style.background = "#333";
|
|
17604
|
-
});
|
|
17605
|
-
btn.addEventListener("pointerdown", (e) => {
|
|
17606
|
-
e.stopPropagation();
|
|
17607
|
-
this.callbacks.onApply(op);
|
|
17608
|
-
});
|
|
17609
|
-
return btn;
|
|
17610
|
-
};
|
|
17611
|
-
const setBtn = mkBtn("Set", "set");
|
|
17612
|
-
const addBtn = mkBtn("Add", "add");
|
|
17613
|
-
const removeBtn = mkBtn("Remove", "remove");
|
|
17614
|
-
const inputWrap = document.createElement("span");
|
|
17615
|
-
inputWrap.style.cssText = "display:inline-flex; align-items:center; gap:2px;";
|
|
17616
|
-
const input = document.createElement("input");
|
|
17617
|
-
input.type = "number";
|
|
17618
|
-
input.min = "1";
|
|
17619
|
-
input.step = "1";
|
|
17620
|
-
input.value = String(this._radius);
|
|
17621
|
-
input.style.cssText = `
|
|
17622
|
-
width:40px; padding:3px 4px; border:1px solid #555; border-radius:4px;
|
|
17623
|
-
background:#222; color:#ddd; font-size:12px; text-align:center;
|
|
17624
|
-
`;
|
|
17625
|
-
const label = document.createElement("span");
|
|
17626
|
-
label.textContent = "Radius";
|
|
17627
|
-
label.style.cssText = "color:#888; font-size:11px;";
|
|
17628
|
-
input.addEventListener("change", () => {
|
|
17629
|
-
const v = Math.max(1, Math.round(parseFloat(input.value) || 1));
|
|
17630
|
-
input.value = String(v);
|
|
17631
|
-
this._radius = v;
|
|
17632
|
-
this.callbacks.onRadiusChanged(v);
|
|
17633
|
-
});
|
|
17634
|
-
inputWrap.appendChild(input);
|
|
17635
|
-
inputWrap.appendChild(label);
|
|
17636
|
-
this.radiusInput = input;
|
|
17637
|
-
this.toolbar.appendChild(setBtn);
|
|
17638
|
-
this.toolbar.appendChild(addBtn);
|
|
17639
|
-
this.toolbar.appendChild(removeBtn);
|
|
17640
|
-
this.toolbar.appendChild(inputWrap);
|
|
17641
|
-
parent.appendChild(this.toolbar);
|
|
18095
|
+
__publicField(this, "_onRadiusChanged");
|
|
18096
|
+
this._onRadiusChanged = onRadiusChanged;
|
|
17642
18097
|
}
|
|
17643
18098
|
get radius() {
|
|
17644
18099
|
return this._radius;
|
|
17645
18100
|
}
|
|
17646
18101
|
setRadius(radius) {
|
|
17647
|
-
|
|
17648
|
-
this.
|
|
18102
|
+
var _a2;
|
|
18103
|
+
this._radius = radius;
|
|
18104
|
+
(_a2 = this._onRadiusChanged) == null ? void 0 : _a2.call(this, radius);
|
|
17649
18105
|
}
|
|
17650
18106
|
activate() {
|
|
17651
|
-
this.toolbar.style.display = "flex";
|
|
17652
18107
|
}
|
|
17653
18108
|
deactivate() {
|
|
17654
|
-
this.toolbar.style.display = "none";
|
|
17655
18109
|
}
|
|
17656
18110
|
}
|
|
17657
18111
|
class BoxSelection {
|
|
17658
|
-
constructor(
|
|
17659
|
-
__publicField(this, "parent");
|
|
17660
|
-
__publicField(this, "toolbar");
|
|
17661
|
-
__publicField(this, "callbacks");
|
|
18112
|
+
constructor(onDimensionsChanged) {
|
|
17662
18113
|
__publicField(this, "_lenX", 2);
|
|
17663
18114
|
__publicField(this, "_lenY", 2);
|
|
17664
18115
|
__publicField(this, "_lenZ", 2);
|
|
17665
|
-
__publicField(this, "
|
|
17666
|
-
|
|
17667
|
-
__publicField(this, "inputZ");
|
|
17668
|
-
this.parent = parent;
|
|
17669
|
-
this.callbacks = callbacks;
|
|
17670
|
-
this.toolbar = document.createElement("div");
|
|
17671
|
-
this.toolbar.className = "volume-select-toolbar";
|
|
17672
|
-
this.toolbar.style.cssText = `
|
|
17673
|
-
position:absolute; bottom:90px; left:50%; transform:translateX(-50%);
|
|
17674
|
-
display:none; z-index:20; background:rgba(30,30,30,0.92);
|
|
17675
|
-
border-radius:8px; padding:6px 10px; gap:6px;
|
|
17676
|
-
align-items:center; font-size:13px; color:#ddd;
|
|
17677
|
-
backdrop-filter:blur(6px); user-select:none; white-space:nowrap;
|
|
17678
|
-
box-shadow:0 2px 12px rgba(0,0,0,0.4);
|
|
17679
|
-
`;
|
|
17680
|
-
this.toolbar.addEventListener("pointerdown", (e) => e.stopPropagation());
|
|
17681
|
-
this.toolbar.addEventListener("wheel", (e) => e.stopPropagation());
|
|
17682
|
-
const mkBtn = (label, op) => {
|
|
17683
|
-
const btn = document.createElement("button");
|
|
17684
|
-
btn.textContent = label;
|
|
17685
|
-
btn.style.cssText = `
|
|
17686
|
-
padding:4px 12px; border:1px solid #555; border-radius:4px;
|
|
17687
|
-
background:#333; color:#ddd; cursor:pointer; font-size:13px;
|
|
17688
|
-
transition: background 0.15s;
|
|
17689
|
-
`;
|
|
17690
|
-
btn.addEventListener("mouseenter", () => {
|
|
17691
|
-
btn.style.background = "#555";
|
|
17692
|
-
});
|
|
17693
|
-
btn.addEventListener("mouseleave", () => {
|
|
17694
|
-
btn.style.background = "#333";
|
|
17695
|
-
});
|
|
17696
|
-
btn.addEventListener("pointerdown", (e) => {
|
|
17697
|
-
e.stopPropagation();
|
|
17698
|
-
this.callbacks.onApply(op);
|
|
17699
|
-
});
|
|
17700
|
-
return btn;
|
|
17701
|
-
};
|
|
17702
|
-
const mkInput = (placeholder, initial, onChange) => {
|
|
17703
|
-
const wrap = document.createElement("span");
|
|
17704
|
-
wrap.style.cssText = "display:inline-flex; align-items:center; gap:2px;";
|
|
17705
|
-
const input = document.createElement("input");
|
|
17706
|
-
input.type = "number";
|
|
17707
|
-
input.min = "1";
|
|
17708
|
-
input.step = "1";
|
|
17709
|
-
input.value = String(Math.round(initial));
|
|
17710
|
-
input.style.cssText = `
|
|
17711
|
-
width:40px; padding:3px 4px; border:1px solid #555; border-radius:4px;
|
|
17712
|
-
background:#222; color:#ddd; font-size:12px; text-align:center;
|
|
17713
|
-
`;
|
|
17714
|
-
const label = document.createElement("span");
|
|
17715
|
-
label.textContent = placeholder;
|
|
17716
|
-
label.style.cssText = "color:#888; font-size:11px;";
|
|
17717
|
-
input.addEventListener("change", () => {
|
|
17718
|
-
const v = Math.max(1, Math.round(parseFloat(input.value) || 1));
|
|
17719
|
-
input.value = String(v);
|
|
17720
|
-
onChange(v);
|
|
17721
|
-
});
|
|
17722
|
-
wrap.appendChild(input);
|
|
17723
|
-
wrap.appendChild(label);
|
|
17724
|
-
return { wrap, input };
|
|
17725
|
-
};
|
|
17726
|
-
const setBtn = mkBtn("Set", "set");
|
|
17727
|
-
const addBtn = mkBtn("Add", "add");
|
|
17728
|
-
const removeBtn = mkBtn("Remove", "remove");
|
|
17729
|
-
const xInput = mkInput("LenX", this._lenX, (v) => {
|
|
17730
|
-
this._lenX = v;
|
|
17731
|
-
this.emitDims();
|
|
17732
|
-
});
|
|
17733
|
-
const yInput = mkInput("LenY", this._lenY, (v) => {
|
|
17734
|
-
this._lenY = v;
|
|
17735
|
-
this.emitDims();
|
|
17736
|
-
});
|
|
17737
|
-
const zInput = mkInput("LenZ", this._lenZ, (v) => {
|
|
17738
|
-
this._lenZ = v;
|
|
17739
|
-
this.emitDims();
|
|
17740
|
-
});
|
|
17741
|
-
this.inputX = xInput.input;
|
|
17742
|
-
this.inputY = yInput.input;
|
|
17743
|
-
this.inputZ = zInput.input;
|
|
17744
|
-
this.toolbar.appendChild(setBtn);
|
|
17745
|
-
this.toolbar.appendChild(addBtn);
|
|
17746
|
-
this.toolbar.appendChild(removeBtn);
|
|
17747
|
-
this.toolbar.appendChild(xInput.wrap);
|
|
17748
|
-
this.toolbar.appendChild(yInput.wrap);
|
|
17749
|
-
this.toolbar.appendChild(zInput.wrap);
|
|
17750
|
-
parent.appendChild(this.toolbar);
|
|
18116
|
+
__publicField(this, "_onDimensionsChanged");
|
|
18117
|
+
this._onDimensionsChanged = onDimensionsChanged;
|
|
17751
18118
|
}
|
|
17752
18119
|
get lenX() {
|
|
17753
18120
|
return this._lenX;
|
|
@@ -17759,21 +18126,15 @@ class BoxSelection {
|
|
|
17759
18126
|
return this._lenZ;
|
|
17760
18127
|
}
|
|
17761
18128
|
setDimensions(lenX, lenY, lenZ) {
|
|
17762
|
-
|
|
17763
|
-
this.
|
|
17764
|
-
this.
|
|
17765
|
-
this.
|
|
17766
|
-
this.
|
|
17767
|
-
this.inputZ.value = String(this._lenZ);
|
|
18129
|
+
var _a2;
|
|
18130
|
+
this._lenX = lenX;
|
|
18131
|
+
this._lenY = lenY;
|
|
18132
|
+
this._lenZ = lenZ;
|
|
18133
|
+
(_a2 = this._onDimensionsChanged) == null ? void 0 : _a2.call(this, lenX, lenY, lenZ);
|
|
17768
18134
|
}
|
|
17769
18135
|
activate() {
|
|
17770
|
-
this.toolbar.style.display = "flex";
|
|
17771
18136
|
}
|
|
17772
18137
|
deactivate() {
|
|
17773
|
-
this.toolbar.style.display = "none";
|
|
17774
|
-
}
|
|
17775
|
-
emitDims() {
|
|
17776
|
-
this.callbacks.onDimensionsChanged(this._lenX, this._lenY, this._lenZ);
|
|
17777
18138
|
}
|
|
17778
18139
|
}
|
|
17779
18140
|
function exportEditedPLY(positions, scales, rotations, colors, opacities, shCoeffs, state) {
|
|
@@ -18280,20 +18641,14 @@ class SplatEditor {
|
|
|
18280
18641
|
if (this.gpuRenderer) {
|
|
18281
18642
|
this.volumeRenderer = new SelectionVolumeRenderer(this.gpuRenderer, this.camera);
|
|
18282
18643
|
}
|
|
18283
|
-
this.sphereTool = new SphereSelection(
|
|
18284
|
-
|
|
18285
|
-
|
|
18286
|
-
var _a2;
|
|
18287
|
-
(_a2 = this.volumeRenderer) == null ? void 0 : _a2.setDimensions(radius, 0, 0);
|
|
18288
|
-
}
|
|
18644
|
+
this.sphereTool = new SphereSelection((radius) => {
|
|
18645
|
+
var _a2;
|
|
18646
|
+
(_a2 = this.volumeRenderer) == null ? void 0 : _a2.setDimensions(radius, 0, 0);
|
|
18289
18647
|
});
|
|
18290
18648
|
this.toolManager.register("sphere", this.sphereTool);
|
|
18291
|
-
this.boxTool = new BoxSelection(
|
|
18292
|
-
|
|
18293
|
-
|
|
18294
|
-
var _a2;
|
|
18295
|
-
(_a2 = this.volumeRenderer) == null ? void 0 : _a2.setDimensions(lx, ly, lz);
|
|
18296
|
-
}
|
|
18649
|
+
this.boxTool = new BoxSelection((lx, ly, lz) => {
|
|
18650
|
+
var _a2;
|
|
18651
|
+
(_a2 = this.volumeRenderer) == null ? void 0 : _a2.setDimensions(lx, ly, lz);
|
|
18297
18652
|
});
|
|
18298
18653
|
this.toolManager.register("box", this.boxTool);
|
|
18299
18654
|
this.gsRenderer.setEditorState(this.splatState.data);
|
|
@@ -18651,6 +19006,24 @@ class SplatEditor {
|
|
|
18651
19006
|
}
|
|
18652
19007
|
};
|
|
18653
19008
|
}
|
|
19009
|
+
// ============================================
|
|
19010
|
+
// 体积工具参数 API(供前端 UI 调用)
|
|
19011
|
+
// ============================================
|
|
19012
|
+
setBoxDimensions(lx, ly, lz) {
|
|
19013
|
+
var _a2;
|
|
19014
|
+
(_a2 = this.boxTool) == null ? void 0 : _a2.setDimensions(lx, ly, lz);
|
|
19015
|
+
}
|
|
19016
|
+
getBoxDimensions() {
|
|
19017
|
+
return this.boxTool ? [this.boxTool.lenX, this.boxTool.lenY, this.boxTool.lenZ] : [2, 2, 2];
|
|
19018
|
+
}
|
|
19019
|
+
setSphereRadius(radius) {
|
|
19020
|
+
var _a2;
|
|
19021
|
+
(_a2 = this.sphereTool) == null ? void 0 : _a2.setRadius(radius);
|
|
19022
|
+
}
|
|
19023
|
+
getSphereRadius() {
|
|
19024
|
+
var _a2;
|
|
19025
|
+
return ((_a2 = this.sphereTool) == null ? void 0 : _a2.radius) ?? 1;
|
|
19026
|
+
}
|
|
18654
19027
|
/**
|
|
18655
19028
|
* 获取模型世界空间包围盒(中心 + 尺寸)
|
|
18656
19029
|
*/
|
|
@@ -18876,6 +19249,10 @@ class App {
|
|
|
18876
19249
|
__publicField(this, "animationId", 0);
|
|
18877
19250
|
// 是否使用移动端渲染器
|
|
18878
19251
|
__publicField(this, "useMobileRenderer", false);
|
|
19252
|
+
// 缓存移动端检测结果,避免每帧调用
|
|
19253
|
+
__publicField(this, "isMobile", false);
|
|
19254
|
+
// 移动端可见 splat 硬上限(保证稳定帧率)
|
|
19255
|
+
__publicField(this, "mobileMaxVisibleCap", 0);
|
|
18879
19256
|
// 最近加载的 CompactSplatData(用于编辑器导出)
|
|
18880
19257
|
__publicField(this, "lastCompactData", null);
|
|
18881
19258
|
// 自适应性能控制
|
|
@@ -18885,6 +19262,17 @@ class App {
|
|
|
18885
19262
|
});
|
|
18886
19263
|
__publicField(this, "lastAppliedRenderScale", 1);
|
|
18887
19264
|
__publicField(this, "baseRenderScale", 1);
|
|
19265
|
+
// 动态分辨率:移动时降分辨率,静止后恢复
|
|
19266
|
+
__publicField(this, "dynamicResolutionEnabled", false);
|
|
19267
|
+
__publicField(this, "dynResLastViewMatrix", new Float32Array(16));
|
|
19268
|
+
__publicField(this, "dynResStillFrames", 0);
|
|
19269
|
+
__publicField(this, "dynResCurrentScale", 1);
|
|
19270
|
+
__publicField(this, "DYNRES_MOVE_SCALE", 0.6);
|
|
19271
|
+
// 移动时 DPR*0.6(2.0*0.6=1.2)
|
|
19272
|
+
__publicField(this, "DYNRES_STILL_SCALE", 1);
|
|
19273
|
+
// 静止时 DPR*1.0
|
|
19274
|
+
__publicField(this, "DYNRES_STILL_THRESHOLD", 2);
|
|
19275
|
+
// 静止 2 帧即恢复
|
|
18888
19276
|
// 绑定的事件处理函数
|
|
18889
19277
|
__publicField(this, "boundOnResize");
|
|
18890
19278
|
/** 额外渲染回调(在 gizmo 之前、场景辅助之后执行) */
|
|
@@ -18896,6 +19284,7 @@ class App {
|
|
|
18896
19284
|
* 初始化应用
|
|
18897
19285
|
*/
|
|
18898
19286
|
async init() {
|
|
19287
|
+
var _a2;
|
|
18899
19288
|
this.renderer = new Renderer(this.canvas);
|
|
18900
19289
|
await this.renderer.init();
|
|
18901
19290
|
this.camera = new Camera();
|
|
@@ -18924,6 +19313,14 @@ class App {
|
|
|
18924
19313
|
if (this.renderer.isAppleGPU) {
|
|
18925
19314
|
this.applyAppleGPUDefaults();
|
|
18926
19315
|
}
|
|
19316
|
+
this.isMobile = isMobileDevice();
|
|
19317
|
+
if (this.isMobile) {
|
|
19318
|
+
createDebugOverlay();
|
|
19319
|
+
(_a2 = getDebugOverlay()) == null ? void 0 : _a2.info(
|
|
19320
|
+
`Mobile detected. DPR=${window.devicePixelRatio}, canvas=${this.canvas.width}x${this.canvas.height}`
|
|
19321
|
+
);
|
|
19322
|
+
this.applyMobileDefaults();
|
|
19323
|
+
}
|
|
18927
19324
|
}
|
|
18928
19325
|
/**
|
|
18929
19326
|
* Apple GPU (M1/M2/M3 等) 自动优化配置
|
|
@@ -18938,16 +19335,32 @@ class App {
|
|
|
18938
19335
|
nearDepthRangeRatio: 2
|
|
18939
19336
|
};
|
|
18940
19337
|
console.log(
|
|
18941
|
-
`[3DGS] Apple GPU detected (${this.renderer.gpuVendor}/${this.renderer.gpuArchitecture}), applying TBDR optimizations:
|
|
19338
|
+
`[3DGS] Apple GPU detected (${this.renderer.gpuVendor}/${this.renderer.gpuArchitecture}), applying TBDR optimizations: depthWrite off, aggressive culling`
|
|
18942
19339
|
);
|
|
18943
19340
|
}
|
|
18944
19341
|
/**
|
|
18945
|
-
*
|
|
19342
|
+
* 移动端自动性能优化
|
|
19343
|
+
* 核心策略:降 DPR + 降分辨率 + 激进剔除 + 降排序频率
|
|
19344
|
+
*/
|
|
19345
|
+
applyMobileDefaults() {
|
|
19346
|
+
this.dynamicResolutionEnabled = true;
|
|
19347
|
+
console.log(`[3DGS] Mobile: f16, sortBits=16, pixelThreshold=2.5, DPR=1.5, dynamicRes=ON`);
|
|
19348
|
+
}
|
|
19349
|
+
/**
|
|
19350
|
+
* 创建 GSSplatRenderer 并自动应用平台优化
|
|
19351
|
+
* @param forMobile 移动端模式:半精度(32B/splat)+ SH L0,内存降为 1/8
|
|
18946
19352
|
*/
|
|
18947
|
-
|
|
19353
|
+
createGSRendererUnified(forMobile = false) {
|
|
18948
19354
|
const renderer = new GSSplatRenderer(this.renderer, this.camera);
|
|
18949
|
-
if (
|
|
19355
|
+
if (forMobile) {
|
|
19356
|
+
renderer.setHalfPrecision(true);
|
|
19357
|
+
renderer.setSortBits(16);
|
|
19358
|
+
renderer.setPixelCullThreshold(1.5);
|
|
19359
|
+
renderer.setSortFrequency(2);
|
|
19360
|
+
} else if (this.renderer.isAppleGPU) {
|
|
18950
19361
|
renderer.setSHMode(SHMode.L1);
|
|
19362
|
+
}
|
|
19363
|
+
if (this.renderer.isAppleGPU) {
|
|
18951
19364
|
renderer.setDepthWriteEnabled(false);
|
|
18952
19365
|
}
|
|
18953
19366
|
return renderer;
|
|
@@ -19017,40 +19430,26 @@ class App {
|
|
|
19017
19430
|
onProgress(50 + parseProgress, "parse");
|
|
19018
19431
|
}
|
|
19019
19432
|
};
|
|
19020
|
-
let gsRenderer;
|
|
19021
19433
|
if (isMobile) {
|
|
19022
|
-
|
|
19023
|
-
|
|
19024
|
-
|
|
19025
|
-
maxSplats: Infinity,
|
|
19026
|
-
loadSH: false,
|
|
19027
|
-
onProgress: parseProgressCallback,
|
|
19028
|
-
coordinateSystem
|
|
19029
|
-
});
|
|
19030
|
-
if (onProgress) onProgress(90, "upload");
|
|
19031
|
-
gsRenderer.setCompactData(compactData);
|
|
19032
|
-
if (onProgress) onProgress(100, "upload");
|
|
19033
|
-
this.lastCompactData = compactData;
|
|
19034
|
-
this.sceneManager.setGSRenderer(gsRenderer);
|
|
19035
|
-
this.hotspotManager.setGSRenderer(gsRenderer);
|
|
19036
|
-
return compactData.count;
|
|
19037
|
-
} else {
|
|
19038
|
-
gsRenderer = this.createDesktopGSRenderer();
|
|
19039
|
-
this.useMobileRenderer = false;
|
|
19040
|
-
const compactData = await this.parsePLYBuffer(buffer, {
|
|
19041
|
-
maxSplats: Infinity,
|
|
19042
|
-
loadSH: true,
|
|
19043
|
-
onProgress: parseProgressCallback,
|
|
19044
|
-
coordinateSystem
|
|
19045
|
-
});
|
|
19046
|
-
if (onProgress) onProgress(90, "upload");
|
|
19047
|
-
gsRenderer.setCompactData(compactData);
|
|
19048
|
-
if (onProgress) onProgress(100, "upload");
|
|
19049
|
-
this.lastCompactData = compactData;
|
|
19050
|
-
this.sceneManager.setGSRenderer(gsRenderer);
|
|
19051
|
-
this.hotspotManager.setGSRenderer(gsRenderer);
|
|
19052
|
-
return compactData.count;
|
|
19434
|
+
console.log(
|
|
19435
|
+
"[3DGS] Mobile device detected, using unified desktop renderer with SH L0"
|
|
19436
|
+
);
|
|
19053
19437
|
}
|
|
19438
|
+
const gsRenderer = this.createGSRendererUnified(isMobile);
|
|
19439
|
+
this.useMobileRenderer = false;
|
|
19440
|
+
const compactData = await this.parsePLYBuffer(buffer, {
|
|
19441
|
+
maxSplats: Infinity,
|
|
19442
|
+
loadSH: !isMobile,
|
|
19443
|
+
onProgress: parseProgressCallback,
|
|
19444
|
+
coordinateSystem
|
|
19445
|
+
});
|
|
19446
|
+
if (onProgress) onProgress(90, "upload");
|
|
19447
|
+
gsRenderer.setCompactData(compactData);
|
|
19448
|
+
if (onProgress) onProgress(100, "upload");
|
|
19449
|
+
this.lastCompactData = compactData;
|
|
19450
|
+
this.sceneManager.setGSRenderer(gsRenderer);
|
|
19451
|
+
this.hotspotManager.setGSRenderer(gsRenderer);
|
|
19452
|
+
return compactData.count;
|
|
19054
19453
|
} catch (error) {
|
|
19055
19454
|
throw error;
|
|
19056
19455
|
}
|
|
@@ -19092,23 +19491,9 @@ class App {
|
|
|
19092
19491
|
}
|
|
19093
19492
|
if (onProgress) onProgress(90, "parse");
|
|
19094
19493
|
if (onProgress) onProgress(90, "upload");
|
|
19095
|
-
|
|
19096
|
-
|
|
19097
|
-
|
|
19098
|
-
this.renderer,
|
|
19099
|
-
this.camera
|
|
19100
|
-
);
|
|
19101
|
-
this.useMobileRenderer = true;
|
|
19102
|
-
const compactData = App.splatCpuToCompactData(splats);
|
|
19103
|
-
mobileRenderer.setCompactData(compactData);
|
|
19104
|
-
this.lastCompactData = compactData;
|
|
19105
|
-
gsRenderer = mobileRenderer;
|
|
19106
|
-
} else {
|
|
19107
|
-
const desktopRenderer = this.createDesktopGSRenderer();
|
|
19108
|
-
this.useMobileRenderer = false;
|
|
19109
|
-
desktopRenderer.setData(splats);
|
|
19110
|
-
gsRenderer = desktopRenderer;
|
|
19111
|
-
}
|
|
19494
|
+
const gsRenderer = this.createGSRendererUnified(isMobile);
|
|
19495
|
+
this.useMobileRenderer = false;
|
|
19496
|
+
gsRenderer.setData(splats);
|
|
19112
19497
|
this.sceneManager.setGSRenderer(gsRenderer);
|
|
19113
19498
|
this.hotspotManager.setGSRenderer(gsRenderer);
|
|
19114
19499
|
if (onProgress) onProgress(100, "upload");
|
|
@@ -19195,14 +19580,8 @@ class App {
|
|
|
19195
19580
|
}
|
|
19196
19581
|
}
|
|
19197
19582
|
if (onProgress) onProgress(90, "upload");
|
|
19198
|
-
|
|
19199
|
-
|
|
19200
|
-
gsRenderer = new GSSplatRendererMobile(this.renderer, this.camera);
|
|
19201
|
-
this.useMobileRenderer = true;
|
|
19202
|
-
} else {
|
|
19203
|
-
gsRenderer = this.createDesktopGSRenderer();
|
|
19204
|
-
this.useMobileRenderer = false;
|
|
19205
|
-
}
|
|
19583
|
+
const gsRenderer = this.createGSRendererUnified(isMobile);
|
|
19584
|
+
this.useMobileRenderer = false;
|
|
19206
19585
|
gsRenderer.setCompactData(compactData);
|
|
19207
19586
|
this.lastCompactData = compactData;
|
|
19208
19587
|
this.sceneManager.setGSRenderer(gsRenderer);
|
|
@@ -19253,6 +19632,21 @@ class App {
|
|
|
19253
19632
|
this.render();
|
|
19254
19633
|
this.animationId = requestAnimationFrame(this.animate.bind(this));
|
|
19255
19634
|
}
|
|
19635
|
+
updateDynamicResolution() {
|
|
19636
|
+
if (!this.dynamicResolutionEnabled) return;
|
|
19637
|
+
const interacting = this.controls.isInteracting;
|
|
19638
|
+
if (interacting) {
|
|
19639
|
+
if (this.dynResCurrentScale !== this.DYNRES_MOVE_SCALE) {
|
|
19640
|
+
this.dynResCurrentScale = this.DYNRES_MOVE_SCALE;
|
|
19641
|
+
this.renderer.setRenderScale(this.DYNRES_MOVE_SCALE);
|
|
19642
|
+
}
|
|
19643
|
+
} else {
|
|
19644
|
+
if (this.dynResCurrentScale !== this.DYNRES_STILL_SCALE) {
|
|
19645
|
+
this.dynResCurrentScale = this.DYNRES_STILL_SCALE;
|
|
19646
|
+
this.renderer.setRenderScale(this.DYNRES_STILL_SCALE);
|
|
19647
|
+
}
|
|
19648
|
+
}
|
|
19649
|
+
}
|
|
19256
19650
|
updateAdaptivePerformance() {
|
|
19257
19651
|
if (!this.adaptivePerformanceEnabled) return;
|
|
19258
19652
|
const gsRenderer = this.getGSRenderer();
|
|
@@ -19296,7 +19690,11 @@ class App {
|
|
|
19296
19690
|
gsRenderer.setMaxVisibleSplats(0);
|
|
19297
19691
|
} else {
|
|
19298
19692
|
const ratio = cfg.nearVisibleRatio + (1 - cfg.nearVisibleRatio) * t;
|
|
19299
|
-
|
|
19693
|
+
let maxVisible = Math.round(splatCount * ratio);
|
|
19694
|
+
if (this.mobileMaxVisibleCap > 0) {
|
|
19695
|
+
maxVisible = Math.min(maxVisible, this.mobileMaxVisibleCap);
|
|
19696
|
+
}
|
|
19697
|
+
gsRenderer.setMaxVisibleSplats(maxVisible);
|
|
19300
19698
|
}
|
|
19301
19699
|
if (cfg.enableDepthRangeLimit) {
|
|
19302
19700
|
if (t >= 0.99) {
|
|
@@ -19306,16 +19704,13 @@ class App {
|
|
|
19306
19704
|
gsRenderer.setDepthRangeLimit(depthRange);
|
|
19307
19705
|
}
|
|
19308
19706
|
}
|
|
19309
|
-
|
|
19310
|
-
gsRenderer.setSortFrequency(2);
|
|
19311
|
-
} else {
|
|
19312
|
-
gsRenderer.setSortFrequency(1);
|
|
19313
|
-
}
|
|
19707
|
+
gsRenderer.setSortFrequency(1);
|
|
19314
19708
|
}
|
|
19315
19709
|
render() {
|
|
19316
19710
|
var _a2, _b2;
|
|
19317
19711
|
this.camera.setAspect(this.renderer.getAspectRatio());
|
|
19318
19712
|
this.controls.update();
|
|
19713
|
+
this.updateDynamicResolution();
|
|
19319
19714
|
this.updateAdaptivePerformance();
|
|
19320
19715
|
this.hotspotManager.updateBillboards();
|
|
19321
19716
|
if ((_a2 = this.skyboxRenderer) == null ? void 0 : _a2.isActive) {
|
|
@@ -19891,10 +20286,6 @@ class App {
|
|
|
19891
20286
|
if (gsRenderer) {
|
|
19892
20287
|
gsRenderer.setSortFrequency(frequency);
|
|
19893
20288
|
}
|
|
19894
|
-
const mobileRenderer = this.getGSRendererMobile();
|
|
19895
|
-
if (mobileRenderer) {
|
|
19896
|
-
mobileRenderer.setSortFrequency(frequency);
|
|
19897
|
-
}
|
|
19898
20289
|
}
|
|
19899
20290
|
/**
|
|
19900
20291
|
* 是否检测到 Apple GPU(M1/M2/M3 等)
|