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