@d5techs/3dgs-lib 1.4.6 → 1.4.8
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 +404 -144
- package/dist/3dgs-lib.cjs.map +1 -1
- package/dist/3dgs-lib.js +404 -144
- package/dist/3dgs-lib.js.map +1 -1
- package/dist/App.d.ts +31 -0
- package/dist/core/Renderer.d.ts +19 -0
- package/dist/gs/GSSplatRendererMobile.d.ts +7 -14
- package/dist/gs/PLYLoaderMobile.d.ts +4 -3
- package/dist/gs/TextureCompressor.d.ts +4 -0
- package/package.json +1 -1
package/dist/3dgs-lib.js
CHANGED
|
@@ -37,7 +37,7 @@ function isMobileDevice() {
|
|
|
37
37
|
}
|
|
38
38
|
function getRecommendedDPR() {
|
|
39
39
|
const isMobile = isMobileDevice();
|
|
40
|
-
const maxDpr = isMobile ?
|
|
40
|
+
const maxDpr = isMobile ? 2 : 2;
|
|
41
41
|
return Math.min(window.devicePixelRatio || 1, maxDpr);
|
|
42
42
|
}
|
|
43
43
|
function isWebGPUSupported() {
|
|
@@ -252,6 +252,13 @@ class Renderer {
|
|
|
252
252
|
__publicField(this, "renderPassEncoder");
|
|
253
253
|
// ResizeObserver 引用(用于清理)
|
|
254
254
|
__publicField(this, "resizeObserver", null);
|
|
255
|
+
// 渲染缩放:0.5 = 半分辨率,1.0 = 正常,用于性能/质量权衡
|
|
256
|
+
__publicField(this, "_renderScale", 1);
|
|
257
|
+
// 自定义 DPR 覆盖:-1 表示使用自动推荐值
|
|
258
|
+
__publicField(this, "_customDPR", -1);
|
|
259
|
+
// 缓存最后一次的 CSS 尺寸,用于 renderScale 变更时重算
|
|
260
|
+
__publicField(this, "_lastCSSWidth", 0);
|
|
261
|
+
__publicField(this, "_lastCSSHeight", 0);
|
|
255
262
|
// 背景颜色
|
|
256
263
|
__publicField(this, "_clearColor", { r: 0.15, g: 0.15, b: 0.15, a: 1 });
|
|
257
264
|
this.canvas = canvas;
|
|
@@ -362,6 +369,45 @@ class Renderer {
|
|
|
362
369
|
});
|
|
363
370
|
this._depthTextureView = this._depthTexture.createView();
|
|
364
371
|
}
|
|
372
|
+
getEffectiveDPR() {
|
|
373
|
+
const baseDPR = this._customDPR > 0 ? this._customDPR : getRecommendedDPR();
|
|
374
|
+
return baseDPR * this._renderScale;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* 设置渲染缩放比例,用于性能/质量权衡
|
|
378
|
+
* 0.5 = 半分辨率(性能提升约 4 倍),1.0 = 正常分辨率
|
|
379
|
+
* 内部等效于降低 DPR,不影响 CSS 布局尺寸
|
|
380
|
+
*/
|
|
381
|
+
setRenderScale(scale) {
|
|
382
|
+
this._renderScale = Math.max(0.25, Math.min(2, scale));
|
|
383
|
+
if (this._lastCSSWidth > 0 && this._lastCSSHeight > 0) {
|
|
384
|
+
this.applySize(this._lastCSSWidth, this._lastCSSHeight);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
getRenderScale() {
|
|
388
|
+
return this._renderScale;
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* 覆盖自动 DPR 推荐值
|
|
392
|
+
* 传入 -1 恢复自动模式
|
|
393
|
+
*/
|
|
394
|
+
setDPR(dpr) {
|
|
395
|
+
this._customDPR = dpr;
|
|
396
|
+
if (this._lastCSSWidth > 0 && this._lastCSSHeight > 0) {
|
|
397
|
+
this.applySize(this._lastCSSWidth, this._lastCSSHeight);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
getDPR() {
|
|
401
|
+
return this._customDPR > 0 ? this._customDPR : getRecommendedDPR();
|
|
402
|
+
}
|
|
403
|
+
applySize(cssWidth, cssHeight) {
|
|
404
|
+
this._lastCSSWidth = cssWidth;
|
|
405
|
+
this._lastCSSHeight = cssHeight;
|
|
406
|
+
const dpr = this.getEffectiveDPR();
|
|
407
|
+
this.canvas.width = Math.max(1, Math.floor(cssWidth * dpr));
|
|
408
|
+
this.canvas.height = Math.max(1, Math.floor(cssHeight * dpr));
|
|
409
|
+
this.createDepthTexture();
|
|
410
|
+
}
|
|
365
411
|
/**
|
|
366
412
|
* 设置 resize 监听
|
|
367
413
|
*/
|
|
@@ -369,10 +415,7 @@ class Renderer {
|
|
|
369
415
|
this.resizeObserver = new ResizeObserver((entries) => {
|
|
370
416
|
for (const entry of entries) {
|
|
371
417
|
const { width, height } = entry.contentRect;
|
|
372
|
-
|
|
373
|
-
this.canvas.width = Math.floor(width * dpr);
|
|
374
|
-
this.canvas.height = Math.floor(height * dpr);
|
|
375
|
-
this.createDepthTexture();
|
|
418
|
+
this.applySize(width, height);
|
|
376
419
|
}
|
|
377
420
|
});
|
|
378
421
|
this.resizeObserver.observe(this.canvas);
|
|
@@ -4269,7 +4312,7 @@ function transformSHCoeffsYZSwap$1(sh, base, perChannel) {
|
|
|
4269
4312
|
for (let ch = 0; ch < 3; ch++) {
|
|
4270
4313
|
const a = sh[base + ch];
|
|
4271
4314
|
const b = sh[base + 3 + ch];
|
|
4272
|
-
sh[base + ch] =
|
|
4315
|
+
sh[base + ch] = b;
|
|
4273
4316
|
sh[base + 3 + ch] = -a;
|
|
4274
4317
|
}
|
|
4275
4318
|
}
|
|
@@ -4280,8 +4323,8 @@ function transformSHCoeffsYZSwap$1(sh, base, perChannel) {
|
|
|
4280
4323
|
const g2 = sh[base + 15 + ch];
|
|
4281
4324
|
const g3 = sh[base + 18 + ch];
|
|
4282
4325
|
const g4 = sh[base + 21 + ch];
|
|
4283
|
-
sh[base + 9 + ch] =
|
|
4284
|
-
sh[base + 12 + ch] = g1;
|
|
4326
|
+
sh[base + 9 + ch] = g3;
|
|
4327
|
+
sh[base + 12 + ch] = -g1;
|
|
4285
4328
|
sh[base + 15 + ch] = -0.5 * g2 - SQRT3_2 * g4;
|
|
4286
4329
|
sh[base + 18 + ch] = -g0;
|
|
4287
4330
|
sh[base + 21 + ch] = -SQRT3_2 * g2 + 0.5 * g4;
|
|
@@ -4299,9 +4342,9 @@ function transformSHCoeffsYZSwap$1(sh, base, perChannel) {
|
|
|
4299
4342
|
const g4 = sh[base + 36 + ch];
|
|
4300
4343
|
const g5 = sh[base + 39 + ch];
|
|
4301
4344
|
const g6 = sh[base + 42 + ch];
|
|
4302
|
-
sh[base + 24 + ch] = A * g3
|
|
4303
|
-
sh[base + 27 + ch] = g1;
|
|
4304
|
-
sh[base + 30 + ch] = B * g3
|
|
4345
|
+
sh[base + 24 + ch] = -A * g3 + B * g5;
|
|
4346
|
+
sh[base + 27 + ch] = -g1;
|
|
4347
|
+
sh[base + 30 + ch] = -B * g3 - A * g5;
|
|
4305
4348
|
sh[base + 33 + ch] = A * g0 + B * g2;
|
|
4306
4349
|
sh[base + 36 + ch] = -0.25 * g4 - P * g6;
|
|
4307
4350
|
sh[base + 39 + ch] = -B * g0 + A * g2;
|
|
@@ -4539,13 +4582,13 @@ async function loadPLY(url, options = {}) {
|
|
|
4539
4582
|
transformSHCoeffsYZSwap$1(shRestBuffer, shOffset, perChannel);
|
|
4540
4583
|
}
|
|
4541
4584
|
splats[i] = {
|
|
4542
|
-
mean: swapYZ ? [x, z, y] : [x, y, z],
|
|
4585
|
+
mean: swapYZ ? [x, z, -y] : [x, y, z],
|
|
4543
4586
|
scale: swapYZ ? [scale_0, scale_2, scale_1] : [scale_0, scale_1, scale_2],
|
|
4544
4587
|
rotation: swapYZ ? [
|
|
4545
|
-
|
|
4588
|
+
rot_0 * qnorm,
|
|
4546
4589
|
rot_1 * qnorm,
|
|
4547
4590
|
rot_3 * qnorm,
|
|
4548
|
-
rot_2 * qnorm
|
|
4591
|
+
-rot_2 * qnorm
|
|
4549
4592
|
] : [
|
|
4550
4593
|
rot_0 * qnorm,
|
|
4551
4594
|
rot_1 * qnorm,
|
|
@@ -4583,7 +4626,7 @@ function transformSHCoeffsYZSwap(sh, base, perChannel) {
|
|
|
4583
4626
|
for (let ch = 0; ch < 3; ch++) {
|
|
4584
4627
|
const a = sh[base + ch];
|
|
4585
4628
|
const b = sh[base + 3 + ch];
|
|
4586
|
-
sh[base + ch] =
|
|
4629
|
+
sh[base + ch] = b;
|
|
4587
4630
|
sh[base + 3 + ch] = -a;
|
|
4588
4631
|
}
|
|
4589
4632
|
}
|
|
@@ -4594,8 +4637,8 @@ function transformSHCoeffsYZSwap(sh, base, perChannel) {
|
|
|
4594
4637
|
const g2 = sh[base + 15 + ch];
|
|
4595
4638
|
const g3 = sh[base + 18 + ch];
|
|
4596
4639
|
const g4 = sh[base + 21 + ch];
|
|
4597
|
-
sh[base + 9 + ch] =
|
|
4598
|
-
sh[base + 12 + ch] = g1;
|
|
4640
|
+
sh[base + 9 + ch] = g3;
|
|
4641
|
+
sh[base + 12 + ch] = -g1;
|
|
4599
4642
|
sh[base + 15 + ch] = -0.5 * g2 - SQRT3_2 * g4;
|
|
4600
4643
|
sh[base + 18 + ch] = -g0;
|
|
4601
4644
|
sh[base + 21 + ch] = -SQRT3_2 * g2 + 0.5 * g4;
|
|
@@ -4613,9 +4656,9 @@ function transformSHCoeffsYZSwap(sh, base, perChannel) {
|
|
|
4613
4656
|
const g4 = sh[base + 36 + ch];
|
|
4614
4657
|
const g5 = sh[base + 39 + ch];
|
|
4615
4658
|
const g6 = sh[base + 42 + ch];
|
|
4616
|
-
sh[base + 24 + ch] = A * g3
|
|
4617
|
-
sh[base + 27 + ch] = g1;
|
|
4618
|
-
sh[base + 30 + ch] = B * g3
|
|
4659
|
+
sh[base + 24 + ch] = -A * g3 + B * g5;
|
|
4660
|
+
sh[base + 27 + ch] = -g1;
|
|
4661
|
+
sh[base + 30 + ch] = -B * g3 - A * g5;
|
|
4619
4662
|
sh[base + 33 + ch] = A * g0 + B * g2;
|
|
4620
4663
|
sh[base + 36 + ch] = -0.25 * g4 - P * g6;
|
|
4621
4664
|
sh[base + 39 + ch] = -B * g0 + A * g2;
|
|
@@ -4910,7 +4953,7 @@ async function parsePLYBuffer(buffer, options = {}) {
|
|
|
4910
4953
|
const pz = offsets.z >= 0 ? readProperty(dataView, base + offsets.z, types.z, littleEndian) : 0;
|
|
4911
4954
|
positions[outputIdx * 3 + 0] = px;
|
|
4912
4955
|
positions[outputIdx * 3 + 1] = swapYZ ? pz : py;
|
|
4913
|
-
positions[outputIdx * 3 + 2] = swapYZ ? py : pz;
|
|
4956
|
+
positions[outputIdx * 3 + 2] = swapYZ ? -py : pz;
|
|
4914
4957
|
const sx = offsets.scale_0 >= 0 ? Math.exp(readProperty(dataView, base + offsets.scale_0, types.scale_0, littleEndian)) : 1;
|
|
4915
4958
|
const sy = offsets.scale_1 >= 0 ? Math.exp(readProperty(dataView, base + offsets.scale_1, types.scale_1, littleEndian)) : 1;
|
|
4916
4959
|
const sz = offsets.scale_2 >= 0 ? Math.exp(readProperty(dataView, base + offsets.scale_2, types.scale_2, littleEndian)) : 1;
|
|
@@ -4924,10 +4967,10 @@ async function parsePLYBuffer(buffer, options = {}) {
|
|
|
4924
4967
|
const qlen = Math.sqrt(rot_0 * rot_0 + rot_1 * rot_1 + rot_2 * rot_2 + rot_3 * rot_3);
|
|
4925
4968
|
const qnorm = qlen > 0 ? 1 / qlen : 1;
|
|
4926
4969
|
if (swapYZ) {
|
|
4927
|
-
rotations[outputIdx * 4 + 0] =
|
|
4970
|
+
rotations[outputIdx * 4 + 0] = rot_0 * qnorm;
|
|
4928
4971
|
rotations[outputIdx * 4 + 1] = rot_1 * qnorm;
|
|
4929
4972
|
rotations[outputIdx * 4 + 2] = rot_3 * qnorm;
|
|
4930
|
-
rotations[outputIdx * 4 + 3] = rot_2 * qnorm;
|
|
4973
|
+
rotations[outputIdx * 4 + 3] = -rot_2 * qnorm;
|
|
4931
4974
|
} else {
|
|
4932
4975
|
rotations[outputIdx * 4 + 0] = rot_0 * qnorm;
|
|
4933
4976
|
rotations[outputIdx * 4 + 1] = rot_1 * qnorm;
|
|
@@ -5585,8 +5628,16 @@ async function decodeWebP(bytes) {
|
|
|
5585
5628
|
});
|
|
5586
5629
|
const w = bitmap.width;
|
|
5587
5630
|
const h = bitmap.height;
|
|
5588
|
-
|
|
5589
|
-
|
|
5631
|
+
let ctx;
|
|
5632
|
+
if (typeof OffscreenCanvas !== "undefined") {
|
|
5633
|
+
const oc = new OffscreenCanvas(w, h);
|
|
5634
|
+
ctx = oc.getContext("2d");
|
|
5635
|
+
} else {
|
|
5636
|
+
const hc = document.createElement("canvas");
|
|
5637
|
+
hc.width = w;
|
|
5638
|
+
hc.height = h;
|
|
5639
|
+
ctx = hc.getContext("2d");
|
|
5640
|
+
}
|
|
5590
5641
|
ctx.drawImage(bitmap, 0, 0);
|
|
5591
5642
|
bitmap.close();
|
|
5592
5643
|
return { width: w, height: h, data: ctx.getImageData(0, 0, w, h).data };
|
|
@@ -8426,6 +8477,65 @@ function compressSplatsToTextures(device, data) {
|
|
|
8426
8477
|
{ bytesPerRow: width * 4 },
|
|
8427
8478
|
{ width, height }
|
|
8428
8479
|
);
|
|
8480
|
+
let shBasis0Texture = null;
|
|
8481
|
+
let shBasis1Texture = null;
|
|
8482
|
+
let shBasis2Texture = null;
|
|
8483
|
+
const hasSH = !!(data.shCoeffs && data.shCoeffs.length >= count * 45);
|
|
8484
|
+
if (hasSH) {
|
|
8485
|
+
const shCoeffs = data.shCoeffs;
|
|
8486
|
+
const sh0Data = new Float32Array(totalPixels * 4);
|
|
8487
|
+
const sh1Data = new Float32Array(totalPixels * 4);
|
|
8488
|
+
const sh2Data = new Float32Array(totalPixels * 4);
|
|
8489
|
+
for (let i = 0; i < count; i++) {
|
|
8490
|
+
const base = i * 45;
|
|
8491
|
+
const px = i * 4;
|
|
8492
|
+
sh0Data[px + 0] = shCoeffs[base + 0];
|
|
8493
|
+
sh0Data[px + 1] = shCoeffs[base + 1];
|
|
8494
|
+
sh0Data[px + 2] = shCoeffs[base + 2];
|
|
8495
|
+
sh1Data[px + 0] = shCoeffs[base + 3];
|
|
8496
|
+
sh1Data[px + 1] = shCoeffs[base + 4];
|
|
8497
|
+
sh1Data[px + 2] = shCoeffs[base + 5];
|
|
8498
|
+
sh2Data[px + 0] = shCoeffs[base + 6];
|
|
8499
|
+
sh2Data[px + 1] = shCoeffs[base + 7];
|
|
8500
|
+
sh2Data[px + 2] = shCoeffs[base + 8];
|
|
8501
|
+
}
|
|
8502
|
+
shBasis0Texture = device.createTexture({
|
|
8503
|
+
size: { width, height },
|
|
8504
|
+
format: "rgba32float",
|
|
8505
|
+
usage: textureUsage,
|
|
8506
|
+
label: "sh-basis0"
|
|
8507
|
+
});
|
|
8508
|
+
shBasis1Texture = device.createTexture({
|
|
8509
|
+
size: { width, height },
|
|
8510
|
+
format: "rgba32float",
|
|
8511
|
+
usage: textureUsage,
|
|
8512
|
+
label: "sh-basis1"
|
|
8513
|
+
});
|
|
8514
|
+
shBasis2Texture = device.createTexture({
|
|
8515
|
+
size: { width, height },
|
|
8516
|
+
format: "rgba32float",
|
|
8517
|
+
usage: textureUsage,
|
|
8518
|
+
label: "sh-basis2"
|
|
8519
|
+
});
|
|
8520
|
+
device.queue.writeTexture(
|
|
8521
|
+
{ texture: shBasis0Texture },
|
|
8522
|
+
sh0Data,
|
|
8523
|
+
{ bytesPerRow: width * 16 },
|
|
8524
|
+
{ width, height }
|
|
8525
|
+
);
|
|
8526
|
+
device.queue.writeTexture(
|
|
8527
|
+
{ texture: shBasis1Texture },
|
|
8528
|
+
sh1Data,
|
|
8529
|
+
{ bytesPerRow: width * 16 },
|
|
8530
|
+
{ width, height }
|
|
8531
|
+
);
|
|
8532
|
+
device.queue.writeTexture(
|
|
8533
|
+
{ texture: shBasis2Texture },
|
|
8534
|
+
sh2Data,
|
|
8535
|
+
{ bytesPerRow: width * 16 },
|
|
8536
|
+
{ width, height }
|
|
8537
|
+
);
|
|
8538
|
+
}
|
|
8429
8539
|
return {
|
|
8430
8540
|
width,
|
|
8431
8541
|
height,
|
|
@@ -8434,6 +8544,10 @@ function compressSplatsToTextures(device, data) {
|
|
|
8434
8544
|
scaleRotTexture1,
|
|
8435
8545
|
scaleRotTexture2,
|
|
8436
8546
|
colorTexture,
|
|
8547
|
+
shBasis0Texture,
|
|
8548
|
+
shBasis1Texture,
|
|
8549
|
+
shBasis2Texture,
|
|
8550
|
+
hasSH,
|
|
8437
8551
|
boundingBox
|
|
8438
8552
|
};
|
|
8439
8553
|
}
|
|
@@ -8442,9 +8556,12 @@ function destroyCompressedTextures(textures) {
|
|
|
8442
8556
|
textures.scaleRotTexture1.destroy();
|
|
8443
8557
|
textures.scaleRotTexture2.destroy();
|
|
8444
8558
|
textures.colorTexture.destroy();
|
|
8559
|
+
if (textures.shBasis0Texture) textures.shBasis0Texture.destroy();
|
|
8560
|
+
if (textures.shBasis1Texture) textures.shBasis1Texture.destroy();
|
|
8561
|
+
if (textures.shBasis2Texture) textures.shBasis2Texture.destroy();
|
|
8445
8562
|
}
|
|
8446
8563
|
const DEFAULT_NUM_BUCKETS = 65536;
|
|
8447
|
-
const IOS_NUM_BUCKETS =
|
|
8564
|
+
const IOS_NUM_BUCKETS = 16384;
|
|
8448
8565
|
const WORKGROUP_SIZE = 256;
|
|
8449
8566
|
function isIOSDevice() {
|
|
8450
8567
|
if (typeof navigator === "undefined") return false;
|
|
@@ -8948,9 +9065,17 @@ class GSSplatSorterMobile {
|
|
|
8948
9065
|
this.drawIndirectBuffer.destroy();
|
|
8949
9066
|
}
|
|
8950
9067
|
}
|
|
8951
|
-
const
|
|
9068
|
+
const shaderCodeMobile = (
|
|
8952
9069
|
/* wgsl */
|
|
8953
9070
|
`
|
|
9071
|
+
|
|
9072
|
+
const SH_C1: f32 = 0.4886025119029199;
|
|
9073
|
+
const GAUSSIAN_K: f32 = 4.0;
|
|
9074
|
+
const EXP_NEG_K: f32 = 0.01831563888873418;
|
|
9075
|
+
const INV_ONE_MINUS_EXP_NEG_K: f32 = 1.01865736036377408;
|
|
9076
|
+
const ALPHA_CULL_THRESHOLD: f32 = 0.00392156863;
|
|
9077
|
+
const LOW_PASS_FILTER: f32 = 0.3;
|
|
9078
|
+
|
|
8954
9079
|
struct Uniforms {
|
|
8955
9080
|
view: mat4x4<f32>,
|
|
8956
9081
|
proj: mat4x4<f32>,
|
|
@@ -8959,18 +9084,21 @@ struct Uniforms {
|
|
|
8959
9084
|
_pad: f32,
|
|
8960
9085
|
screenSize: vec2<f32>,
|
|
8961
9086
|
_pad2: vec2<f32>,
|
|
8962
|
-
textureSize: vec2<f32>,
|
|
8963
|
-
|
|
9087
|
+
textureSize: vec2<f32>,
|
|
9088
|
+
shEnabled: f32,
|
|
9089
|
+
_pad3: f32,
|
|
8964
9090
|
}
|
|
8965
9091
|
|
|
8966
9092
|
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
|
|
8967
9093
|
@group(0) @binding(1) var<storage, read> sortedIndices: array<u32>;
|
|
8968
9094
|
|
|
8969
|
-
|
|
8970
|
-
@group(1) @binding(
|
|
8971
|
-
@group(1) @binding(
|
|
8972
|
-
@group(1) @binding(
|
|
8973
|
-
@group(1) @binding(
|
|
9095
|
+
@group(1) @binding(0) var positionTex: texture_2d<f32>;
|
|
9096
|
+
@group(1) @binding(1) var scaleRotTex1: texture_2d<f32>;
|
|
9097
|
+
@group(1) @binding(2) var scaleRotTex2: texture_2d<f32>;
|
|
9098
|
+
@group(1) @binding(3) var colorTex: texture_2d<f32>;
|
|
9099
|
+
@group(1) @binding(4) var shBasis0Tex: texture_2d<f32>;
|
|
9100
|
+
@group(1) @binding(5) var shBasis1Tex: texture_2d<f32>;
|
|
9101
|
+
@group(1) @binding(6) var shBasis2Tex: texture_2d<f32>;
|
|
8974
9102
|
|
|
8975
9103
|
struct VertexOutput {
|
|
8976
9104
|
@builtin(position) position: vec4<f32>,
|
|
@@ -8988,7 +9116,6 @@ const QUAD_POSITIONS = array<vec2<f32>, 4>(
|
|
|
8988
9116
|
|
|
8989
9117
|
const ELLIPSE_SCALE: f32 = 3.0;
|
|
8990
9118
|
|
|
8991
|
-
// 将索引转换为纹理坐标
|
|
8992
9119
|
fn indexToTexCoord(index: u32) -> vec2<u32> {
|
|
8993
9120
|
let texWidth = u32(uniforms.textureSize.x);
|
|
8994
9121
|
let x = index % texWidth;
|
|
@@ -8996,7 +9123,6 @@ fn indexToTexCoord(index: u32) -> vec2<u32> {
|
|
|
8996
9123
|
return vec2<u32>(x, y);
|
|
8997
9124
|
}
|
|
8998
9125
|
|
|
8999
|
-
// 四元数转旋转矩阵
|
|
9000
9126
|
fn quatToMat3(q: vec4<f32>) -> mat3x3<f32> {
|
|
9001
9127
|
let w = q[0]; let x = q[1]; let y = q[2]; let z = q[3];
|
|
9002
9128
|
let x2 = x + x; let y2 = y + y; let z2 = z + z;
|
|
@@ -9010,15 +9136,12 @@ fn quatToMat3(q: vec4<f32>) -> mat3x3<f32> {
|
|
|
9010
9136
|
);
|
|
9011
9137
|
}
|
|
9012
9138
|
|
|
9013
|
-
// 从模型矩阵提取统一缩放因子(取 X 轴向量长度)
|
|
9014
9139
|
fn getModelScale(model: mat4x4<f32>) -> f32 {
|
|
9015
9140
|
return length(model[0].xyz);
|
|
9016
9141
|
}
|
|
9017
9142
|
|
|
9018
|
-
// 计算 2D 协方差
|
|
9019
9143
|
fn computeCov2D(mean: vec3<f32>, scale: vec3<f32>, rotation: vec4<f32>, modelView: mat4x4<f32>, proj: mat4x4<f32>, modelScale: f32) -> vec3<f32> {
|
|
9020
9144
|
let R = quatToMat3(rotation);
|
|
9021
|
-
// 应用模型缩放到 splat scale
|
|
9022
9145
|
let scaledScale = scale * modelScale;
|
|
9023
9146
|
let s2 = scaledScale * scaledScale;
|
|
9024
9147
|
let M = mat3x3<f32>(R[0] * s2.x, R[1] * s2.y, R[2] * s2.z);
|
|
@@ -9030,8 +9153,6 @@ fn computeCov2D(mean: vec3<f32>, scale: vec3<f32>, rotation: vec4<f32>, modelVie
|
|
|
9030
9153
|
let z = -viewPos.z;
|
|
9031
9154
|
let z_clamped = max(z, 0.001);
|
|
9032
9155
|
let z2 = z_clamped * z_clamped;
|
|
9033
|
-
// 雅可比矩阵: 从相机坐标 (x_cam, y_cam, z_cam) 到 NDC 的偏导数
|
|
9034
|
-
// x_ndc = fx * x_cam / (-z_cam), 所以 dx_ndc/dz_cam = fx * x_cam / z_cam^2 (正号!)
|
|
9035
9156
|
let j1 = vec3<f32>(fx / z_clamped, 0.0, fx * viewPos.x / z2);
|
|
9036
9157
|
let j2 = vec3<f32>(0.0, fy / z_clamped, fy * viewPos.y / z2);
|
|
9037
9158
|
let Sj1 = SigmaView * j1;
|
|
@@ -9039,17 +9160,22 @@ fn computeCov2D(mean: vec3<f32>, scale: vec3<f32>, rotation: vec4<f32>, modelVie
|
|
|
9039
9160
|
return vec3<f32>(dot(j1, Sj1), dot(j1, Sj2), dot(j2, Sj2));
|
|
9040
9161
|
}
|
|
9041
9162
|
|
|
9042
|
-
// 计算椭圆轴
|
|
9043
9163
|
fn computeEllipseAxes(cov2D: vec3<f32>) -> mat2x2<f32> {
|
|
9044
|
-
|
|
9164
|
+
var cov = cov2D;
|
|
9165
|
+
cov.x += LOW_PASS_FILTER;
|
|
9166
|
+
cov.z += LOW_PASS_FILTER;
|
|
9167
|
+
let a = cov.x; let b = cov.y; let c = cov.z;
|
|
9045
9168
|
let trace = a + c;
|
|
9046
9169
|
let det = a * c - b * b;
|
|
9047
9170
|
let disc = trace * trace - 4.0 * det;
|
|
9048
9171
|
let sqrtDisc = sqrt(max(disc, 0.0));
|
|
9049
9172
|
let lambda1 = max((trace + sqrtDisc) * 0.5, 0.0);
|
|
9050
9173
|
let lambda2 = max((trace - sqrtDisc) * 0.5, 0.0);
|
|
9051
|
-
|
|
9052
|
-
|
|
9174
|
+
if (lambda2 <= 0.0) {
|
|
9175
|
+
return mat2x2<f32>(vec2<f32>(0.0), vec2<f32>(0.0));
|
|
9176
|
+
}
|
|
9177
|
+
let r1 = min(2.0 * sqrt(2.0 * lambda1), 1024.0);
|
|
9178
|
+
let r2 = min(2.0 * sqrt(2.0 * lambda2), 1024.0);
|
|
9053
9179
|
var axis1: vec2<f32>; var axis2: vec2<f32>;
|
|
9054
9180
|
if (abs(b) > 1e-6) {
|
|
9055
9181
|
axis1 = normalize(vec2<f32>(b, lambda1 - a));
|
|
@@ -9065,59 +9191,88 @@ fn computeEllipseAxes(cov2D: vec3<f32>) -> mat2x2<f32> {
|
|
|
9065
9191
|
fn vs_main(@builtin(vertex_index) vertexIndex: u32, @builtin(instance_index) instanceIndex: u32) -> VertexOutput {
|
|
9066
9192
|
var output: VertexOutput;
|
|
9067
9193
|
|
|
9068
|
-
// 获取排序后的索引
|
|
9069
9194
|
let splatIndex = sortedIndices[instanceIndex];
|
|
9070
9195
|
let texCoord = indexToTexCoord(splatIndex);
|
|
9071
9196
|
|
|
9072
|
-
// 从纹理采样位置数据(RGBA32Float,直接读取)
|
|
9073
9197
|
let posSample = textureLoad(positionTex, texCoord, 0);
|
|
9074
9198
|
let mean = posSample.xyz;
|
|
9075
9199
|
|
|
9076
|
-
// 从纹理采样缩放和旋转(RGBA16Float,GPU 自动转换为 f32)
|
|
9077
9200
|
let scaleRot1 = textureLoad(scaleRotTex1, texCoord, 0);
|
|
9078
9201
|
let scaleRot2 = textureLoad(scaleRotTex2, texCoord, 0);
|
|
9079
|
-
|
|
9080
9202
|
let scale = scaleRot1.xyz;
|
|
9081
9203
|
let rotation = vec4<f32>(scaleRot1.w, scaleRot2.x, scaleRot2.y, scaleRot2.z);
|
|
9082
9204
|
|
|
9083
|
-
// 从纹理采样颜色(RGBA8Unorm,GPU 自动归一化到 0-1)
|
|
9084
9205
|
let colorSample = textureLoad(colorTex, texCoord, 0);
|
|
9085
|
-
|
|
9206
|
+
var color = colorSample.rgb;
|
|
9086
9207
|
let opacity = colorSample.a;
|
|
9208
|
+
|
|
9209
|
+
if (opacity < ALPHA_CULL_THRESHOLD) {
|
|
9210
|
+
output.position = vec4<f32>(0.0, 0.0, 2.0, 1.0);
|
|
9211
|
+
return output;
|
|
9212
|
+
}
|
|
9087
9213
|
|
|
9088
|
-
// 计算顶点位置
|
|
9089
9214
|
let quadPos = QUAD_POSITIONS[vertexIndex];
|
|
9090
9215
|
output.localUV = quadPos;
|
|
9091
9216
|
|
|
9092
|
-
// 计算 modelView 矩阵和模型缩放
|
|
9093
9217
|
let modelView = uniforms.view * uniforms.model;
|
|
9094
9218
|
let modelScale = getModelScale(uniforms.model);
|
|
9095
9219
|
|
|
9096
9220
|
let cov2D = computeCov2D(mean, scale, rotation, modelView, uniforms.proj, modelScale);
|
|
9097
9221
|
let axes = computeEllipseAxes(cov2D);
|
|
9098
|
-
|
|
9222
|
+
|
|
9223
|
+
if (axes[0].x == 0.0 && axes[0].y == 0.0 && axes[1].x == 0.0 && axes[1].y == 0.0) {
|
|
9224
|
+
output.position = vec4<f32>(0.0, 0.0, 2.0, 1.0);
|
|
9225
|
+
return output;
|
|
9226
|
+
}
|
|
9227
|
+
|
|
9228
|
+
let basisViewport = vec2<f32>(1.0 / uniforms.screenSize.x, 1.0 / uniforms.screenSize.y);
|
|
9229
|
+
let ndcOffset = (quadPos.x * axes[0] + quadPos.y * axes[1]) * basisViewport * 2.0;
|
|
9099
9230
|
|
|
9100
|
-
// 应用 model 变换到 splat 位置
|
|
9101
9231
|
let worldPos = uniforms.model * vec4<f32>(mean, 1.0);
|
|
9102
9232
|
let viewPos = uniforms.view * worldPos;
|
|
9103
|
-
|
|
9104
|
-
|
|
9105
|
-
|
|
9106
|
-
|
|
9233
|
+
let clipPos = uniforms.proj * viewPos;
|
|
9234
|
+
let pW = 1.0 / (clipPos.w + 0.0000001);
|
|
9235
|
+
let ndcPos = clipPos * pW;
|
|
9236
|
+
|
|
9237
|
+
if (abs(ndcPos.x) > 1.3 || abs(ndcPos.y) > 1.3 || ndcPos.z < -0.2 || ndcPos.z > 1.0) {
|
|
9238
|
+
output.position = vec4<f32>(0.0, 0.0, 2.0, 1.0);
|
|
9239
|
+
return output;
|
|
9240
|
+
}
|
|
9241
|
+
|
|
9242
|
+
output.position = vec4<f32>(ndcPos.xy + ndcOffset, ndcPos.z, 1.0);
|
|
9243
|
+
|
|
9244
|
+
// L1 SH evaluation
|
|
9245
|
+
if (uniforms.shEnabled > 0.5) {
|
|
9246
|
+
let sh_b0 = textureLoad(shBasis0Tex, texCoord, 0).xyz;
|
|
9247
|
+
let sh_b1 = textureLoad(shBasis1Tex, texCoord, 0).xyz;
|
|
9248
|
+
let sh_b2 = textureLoad(shBasis2Tex, texCoord, 0).xyz;
|
|
9249
|
+
|
|
9250
|
+
let viewDir = worldPos.xyz - uniforms.cameraPos;
|
|
9251
|
+
let shDir = normalize(vec3<f32>(
|
|
9252
|
+
dot(viewDir, uniforms.model[0].xyz),
|
|
9253
|
+
dot(viewDir, uniforms.model[1].xyz),
|
|
9254
|
+
dot(viewDir, uniforms.model[2].xyz)
|
|
9255
|
+
));
|
|
9256
|
+
|
|
9257
|
+
color += (-SH_C1 * shDir.y) * sh_b0
|
|
9258
|
+
+ ( SH_C1 * shDir.z) * sh_b1
|
|
9259
|
+
+ (-SH_C1 * shDir.x) * sh_b2;
|
|
9260
|
+
}
|
|
9261
|
+
|
|
9107
9262
|
output.color = color;
|
|
9108
9263
|
output.opacity = opacity;
|
|
9109
|
-
|
|
9110
9264
|
return output;
|
|
9111
9265
|
}
|
|
9112
9266
|
|
|
9113
9267
|
@fragment
|
|
9114
9268
|
fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
|
|
9115
|
-
|
|
9116
|
-
|
|
9117
|
-
|
|
9118
|
-
let
|
|
9119
|
-
|
|
9120
|
-
|
|
9269
|
+
if (input.opacity <= 0.0) { discard; }
|
|
9270
|
+
let A = dot(input.localUV, input.localUV);
|
|
9271
|
+
if (A > 1.0) { discard; }
|
|
9272
|
+
let weight = (exp(-GAUSSIAN_K * A) - EXP_NEG_K) * INV_ONE_MINUS_EXP_NEG_K;
|
|
9273
|
+
let alpha = input.opacity * weight;
|
|
9274
|
+
if (alpha < ALPHA_CULL_THRESHOLD) { discard; }
|
|
9275
|
+
let color = max(input.color, vec3<f32>(0.0));
|
|
9121
9276
|
return vec4<f32>(color * alpha, alpha);
|
|
9122
9277
|
}
|
|
9123
9278
|
`
|
|
@@ -9147,6 +9302,10 @@ class GSSplatRendererMobile {
|
|
|
9147
9302
|
// 帧计数(用于排序频率控制)
|
|
9148
9303
|
__publicField(this, "frameCount", 0);
|
|
9149
9304
|
__publicField(this, "sortEveryNFrames", 1);
|
|
9305
|
+
// 相机静止检测:跳过不必要的排序
|
|
9306
|
+
__publicField(this, "lastSortViewMatrix", new Float32Array(16));
|
|
9307
|
+
__publicField(this, "lastSortProjMatrix", new Float32Array(16));
|
|
9308
|
+
__publicField(this, "sortStateInitialized", false);
|
|
9150
9309
|
// ============================================
|
|
9151
9310
|
// 变换相关 (position, rotation, scale)
|
|
9152
9311
|
// ============================================
|
|
@@ -9157,6 +9316,8 @@ class GSSplatRendererMobile {
|
|
|
9157
9316
|
__publicField(this, "pivot", [0, 0, 0]);
|
|
9158
9317
|
// 旋转/缩放中心点
|
|
9159
9318
|
__publicField(this, "modelMatrix", new Float32Array(16));
|
|
9319
|
+
// 1x1 dummy SH 纹理(当无 SH 数据时使用)
|
|
9320
|
+
__publicField(this, "dummySHTexture", null);
|
|
9160
9321
|
this.renderer = renderer;
|
|
9161
9322
|
this.camera = camera;
|
|
9162
9323
|
this.createPipeline();
|
|
@@ -9278,7 +9439,7 @@ class GSSplatRendererMobile {
|
|
|
9278
9439
|
createPipeline() {
|
|
9279
9440
|
const device = this.renderer.device;
|
|
9280
9441
|
const shaderModule = device.createShaderModule({
|
|
9281
|
-
code:
|
|
9442
|
+
code: shaderCodeMobile,
|
|
9282
9443
|
label: "mobile-splat-shader"
|
|
9283
9444
|
});
|
|
9284
9445
|
this.uniformBindGroupLayout = device.createBindGroupLayout({
|
|
@@ -9295,32 +9456,27 @@ class GSSplatRendererMobile {
|
|
|
9295
9456
|
}
|
|
9296
9457
|
]
|
|
9297
9458
|
});
|
|
9459
|
+
const texEntry = (binding) => ({
|
|
9460
|
+
binding,
|
|
9461
|
+
visibility: GPUShaderStage.VERTEX,
|
|
9462
|
+
texture: { sampleType: "unfilterable-float" }
|
|
9463
|
+
});
|
|
9298
9464
|
this.textureBindGroupLayout = device.createBindGroupLayout({
|
|
9299
9465
|
entries: [
|
|
9300
|
-
|
|
9301
|
-
|
|
9302
|
-
|
|
9303
|
-
|
|
9304
|
-
|
|
9305
|
-
|
|
9306
|
-
|
|
9307
|
-
|
|
9308
|
-
|
|
9309
|
-
|
|
9310
|
-
|
|
9311
|
-
|
|
9312
|
-
|
|
9313
|
-
|
|
9314
|
-
binding: 2,
|
|
9315
|
-
visibility: GPUShaderStage.VERTEX,
|
|
9316
|
-
texture: { sampleType: "unfilterable-float" }
|
|
9317
|
-
},
|
|
9318
|
-
{
|
|
9319
|
-
// colorTex (RGBA8Unorm)
|
|
9320
|
-
binding: 3,
|
|
9321
|
-
visibility: GPUShaderStage.VERTEX,
|
|
9322
|
-
texture: { sampleType: "unfilterable-float" }
|
|
9323
|
-
}
|
|
9466
|
+
texEntry(0),
|
|
9467
|
+
// positionTex
|
|
9468
|
+
texEntry(1),
|
|
9469
|
+
// scaleRotTex1
|
|
9470
|
+
texEntry(2),
|
|
9471
|
+
// scaleRotTex2
|
|
9472
|
+
texEntry(3),
|
|
9473
|
+
// colorTex
|
|
9474
|
+
texEntry(4),
|
|
9475
|
+
// shBasis0Tex
|
|
9476
|
+
texEntry(5),
|
|
9477
|
+
// shBasis1Tex
|
|
9478
|
+
texEntry(6)
|
|
9479
|
+
// shBasis2Tex
|
|
9324
9480
|
]
|
|
9325
9481
|
});
|
|
9326
9482
|
const pipelineLayout = device.createPipelineLayout({
|
|
@@ -9364,10 +9520,16 @@ class GSSplatRendererMobile {
|
|
|
9364
9520
|
depthCompare: "always"
|
|
9365
9521
|
}
|
|
9366
9522
|
});
|
|
9523
|
+
this.dummySHTexture = device.createTexture({
|
|
9524
|
+
size: { width: 1, height: 1 },
|
|
9525
|
+
format: "rgba32float",
|
|
9526
|
+
usage: GPUTextureUsage.TEXTURE_BINDING,
|
|
9527
|
+
label: "dummy-sh"
|
|
9528
|
+
});
|
|
9367
9529
|
}
|
|
9368
9530
|
/**
|
|
9369
9531
|
* 创建 uniform buffer
|
|
9370
|
-
* 布局: view
|
|
9532
|
+
* 布局: view(64) + proj(64) + model(64) + cameraPos(12)+pad(4) + screenSize(8)+pad(8) + textureSize(8)+shEnabled(4)+pad(4) = 240 bytes
|
|
9371
9533
|
*/
|
|
9372
9534
|
createUniformBuffer() {
|
|
9373
9535
|
this.uniformBuffer = this.renderer.device.createBuffer({
|
|
@@ -9413,6 +9575,7 @@ class GSSplatRendererMobile {
|
|
|
9413
9575
|
const memoryMB = (this.compressedTextures.width * this.compressedTextures.height * 52 / // 约 52 bytes per texel (16+16+16+4)
|
|
9414
9576
|
(1024 * 1024)).toFixed(2);
|
|
9415
9577
|
} catch (error) {
|
|
9578
|
+
console.error("[GSSplatRendererMobile] setCompactData failed:", error);
|
|
9416
9579
|
this.splatCount = 0;
|
|
9417
9580
|
this.compressedTextures = null;
|
|
9418
9581
|
this.sorter = null;
|
|
@@ -9427,35 +9590,22 @@ class GSSplatRendererMobile {
|
|
|
9427
9590
|
this.uniformBindGroup = device.createBindGroup({
|
|
9428
9591
|
layout: this.uniformBindGroupLayout,
|
|
9429
9592
|
entries: [
|
|
9430
|
-
{
|
|
9431
|
-
|
|
9432
|
-
resource: { buffer: this.uniformBuffer }
|
|
9433
|
-
},
|
|
9434
|
-
{
|
|
9435
|
-
binding: 1,
|
|
9436
|
-
resource: { buffer: this.sorter.getIndicesBuffer() }
|
|
9437
|
-
}
|
|
9593
|
+
{ binding: 0, resource: { buffer: this.uniformBuffer } },
|
|
9594
|
+
{ binding: 1, resource: { buffer: this.sorter.getIndicesBuffer() } }
|
|
9438
9595
|
]
|
|
9439
9596
|
});
|
|
9597
|
+
const dummyView = this.dummySHTexture.createView();
|
|
9598
|
+
const tex = this.compressedTextures;
|
|
9440
9599
|
this.textureBindGroup = device.createBindGroup({
|
|
9441
9600
|
layout: this.textureBindGroupLayout,
|
|
9442
9601
|
entries: [
|
|
9443
|
-
{
|
|
9444
|
-
|
|
9445
|
-
|
|
9446
|
-
},
|
|
9447
|
-
{
|
|
9448
|
-
|
|
9449
|
-
|
|
9450
|
-
},
|
|
9451
|
-
{
|
|
9452
|
-
binding: 2,
|
|
9453
|
-
resource: this.compressedTextures.scaleRotTexture2.createView()
|
|
9454
|
-
},
|
|
9455
|
-
{
|
|
9456
|
-
binding: 3,
|
|
9457
|
-
resource: this.compressedTextures.colorTexture.createView()
|
|
9458
|
-
}
|
|
9602
|
+
{ binding: 0, resource: tex.positionTexture.createView() },
|
|
9603
|
+
{ binding: 1, resource: tex.scaleRotTexture1.createView() },
|
|
9604
|
+
{ binding: 2, resource: tex.scaleRotTexture2.createView() },
|
|
9605
|
+
{ binding: 3, resource: tex.colorTexture.createView() },
|
|
9606
|
+
{ binding: 4, resource: tex.shBasis0Texture ? tex.shBasis0Texture.createView() : dummyView },
|
|
9607
|
+
{ binding: 5, resource: tex.shBasis1Texture ? tex.shBasis1Texture.createView() : dummyView },
|
|
9608
|
+
{ binding: 6, resource: tex.shBasis2Texture ? tex.shBasis2Texture.createView() : dummyView }
|
|
9459
9609
|
]
|
|
9460
9610
|
});
|
|
9461
9611
|
}
|
|
@@ -9510,10 +9660,11 @@ class GSSplatRendererMobile {
|
|
|
9510
9660
|
208,
|
|
9511
9661
|
new Float32Array([this.renderer.width, this.renderer.height, 0, 0])
|
|
9512
9662
|
);
|
|
9663
|
+
const shEnabled = this.compressedTextures.hasSH ? 1 : 0;
|
|
9513
9664
|
device.queue.writeBuffer(
|
|
9514
9665
|
this.uniformBuffer,
|
|
9515
9666
|
224,
|
|
9516
|
-
new Float32Array([this.compressedTextures.width, this.compressedTextures.height,
|
|
9667
|
+
new Float32Array([this.compressedTextures.width, this.compressedTextures.height, shEnabled, 0])
|
|
9517
9668
|
);
|
|
9518
9669
|
this.sorter.setScreenSize(this.renderer.width, this.renderer.height);
|
|
9519
9670
|
this.sorter.setCullingOptions({
|
|
@@ -9522,7 +9673,8 @@ class GSSplatRendererMobile {
|
|
|
9522
9673
|
pixelThreshold: 1
|
|
9523
9674
|
});
|
|
9524
9675
|
const isFirstFrame = this.frameCount === 1;
|
|
9525
|
-
const
|
|
9676
|
+
const cameraChanged = this.hasCameraChanged();
|
|
9677
|
+
const shouldSort = isFirstFrame || cameraChanged && this.frameCount % this.sortEveryNFrames === 0;
|
|
9526
9678
|
if (shouldSort) {
|
|
9527
9679
|
this.sorter.sort();
|
|
9528
9680
|
}
|
|
@@ -9553,36 +9705,42 @@ class GSSplatRendererMobile {
|
|
|
9553
9705
|
setSortFrequency(n) {
|
|
9554
9706
|
this.sortEveryNFrames = Math.max(1, n);
|
|
9555
9707
|
}
|
|
9708
|
+
hasCameraChanged() {
|
|
9709
|
+
const view = this.camera.viewMatrix;
|
|
9710
|
+
const proj = this.camera.projectionMatrix;
|
|
9711
|
+
if (!this.sortStateInitialized) {
|
|
9712
|
+
this.lastSortViewMatrix.set(view);
|
|
9713
|
+
this.lastSortProjMatrix.set(proj);
|
|
9714
|
+
this.sortStateInitialized = true;
|
|
9715
|
+
return true;
|
|
9716
|
+
}
|
|
9717
|
+
for (let i = 0; i < 16; i++) {
|
|
9718
|
+
if (Math.abs(view[i] - this.lastSortViewMatrix[i]) > 1e-6 || Math.abs(proj[i] - this.lastSortProjMatrix[i]) > 1e-6) {
|
|
9719
|
+
this.lastSortViewMatrix.set(view);
|
|
9720
|
+
this.lastSortProjMatrix.set(proj);
|
|
9721
|
+
return true;
|
|
9722
|
+
}
|
|
9723
|
+
}
|
|
9724
|
+
return false;
|
|
9725
|
+
}
|
|
9556
9726
|
// ============================================
|
|
9557
9727
|
// IGSSplatRenderer 接口实现 - SH 模式
|
|
9558
9728
|
// ============================================
|
|
9559
|
-
|
|
9560
|
-
* 设置 SH 模式(移动端仅支持 L0)
|
|
9561
|
-
*/
|
|
9562
|
-
setSHMode(mode) {
|
|
9729
|
+
setSHMode(_mode) {
|
|
9563
9730
|
}
|
|
9564
|
-
/**
|
|
9565
|
-
* 获取当前 SH 模式
|
|
9566
|
-
*/
|
|
9567
9731
|
getSHMode() {
|
|
9568
|
-
|
|
9732
|
+
var _a2;
|
|
9733
|
+
return ((_a2 = this.compressedTextures) == null ? void 0 : _a2.hasSH) ? SHMode.L1 : SHMode.L0;
|
|
9569
9734
|
}
|
|
9570
|
-
/**
|
|
9571
|
-
* 是否支持指定的 SH 模式
|
|
9572
|
-
*/
|
|
9573
9735
|
supportsSHMode(mode) {
|
|
9574
|
-
return mode === SHMode.L0;
|
|
9736
|
+
return mode === SHMode.L0 || mode === SHMode.L1;
|
|
9575
9737
|
}
|
|
9576
|
-
/**
|
|
9577
|
-
* 获取渲染器能力
|
|
9578
|
-
*/
|
|
9579
9738
|
getCapabilities() {
|
|
9580
9739
|
return {
|
|
9581
|
-
maxSHMode: SHMode.
|
|
9740
|
+
maxSHMode: SHMode.L1,
|
|
9582
9741
|
supportsRawData: false,
|
|
9583
9742
|
isMobileOptimized: true,
|
|
9584
9743
|
maxSplatCount: 0
|
|
9585
|
-
// 无限制(受 GPU 内存限制)
|
|
9586
9744
|
};
|
|
9587
9745
|
}
|
|
9588
9746
|
/**
|
|
@@ -9611,6 +9769,10 @@ class GSSplatRendererMobile {
|
|
|
9611
9769
|
*/
|
|
9612
9770
|
destroy() {
|
|
9613
9771
|
this.destroyInternal();
|
|
9772
|
+
if (this.dummySHTexture) {
|
|
9773
|
+
this.dummySHTexture.destroy();
|
|
9774
|
+
this.dummySHTexture = null;
|
|
9775
|
+
}
|
|
9614
9776
|
}
|
|
9615
9777
|
}
|
|
9616
9778
|
class SceneManager {
|
|
@@ -16346,6 +16508,7 @@ class App {
|
|
|
16346
16508
|
*/
|
|
16347
16509
|
async addSplat(urlOrBuffer, onProgress, isLocalFile = false, coordinateSystem = "blender") {
|
|
16348
16510
|
try {
|
|
16511
|
+
const isMobile = isMobileDevice();
|
|
16349
16512
|
let buffer;
|
|
16350
16513
|
if (typeof urlOrBuffer === "string") {
|
|
16351
16514
|
buffer = await this.fetchWithProgress(urlOrBuffer, (downloadProgress) => {
|
|
@@ -16364,26 +16527,67 @@ class App {
|
|
|
16364
16527
|
if (coordinateSystem === "blender") {
|
|
16365
16528
|
for (const s of splats) {
|
|
16366
16529
|
const [mx, my, mz] = s.mean;
|
|
16367
|
-
s.mean = [mx, mz, my];
|
|
16530
|
+
s.mean = [mx, mz, -my];
|
|
16368
16531
|
const [sx, sy, sz] = s.scale;
|
|
16369
16532
|
s.scale = [sx, sz, sy];
|
|
16370
16533
|
const [rw, rx, ry, rz] = s.rotation;
|
|
16371
|
-
s.rotation = [
|
|
16534
|
+
s.rotation = [rw, rx, rz, -ry];
|
|
16372
16535
|
}
|
|
16373
16536
|
}
|
|
16374
16537
|
if (onProgress) onProgress(90, "parse");
|
|
16375
16538
|
if (onProgress) onProgress(90, "upload");
|
|
16376
|
-
|
|
16377
|
-
|
|
16539
|
+
let gsRenderer;
|
|
16540
|
+
if (isMobile) {
|
|
16541
|
+
const mobileRenderer = new GSSplatRendererMobile(this.renderer, this.camera);
|
|
16542
|
+
this.useMobileRenderer = true;
|
|
16543
|
+
const compactData = App.splatCpuToCompactData(splats);
|
|
16544
|
+
mobileRenderer.setCompactData(compactData);
|
|
16545
|
+
this.lastCompactData = compactData;
|
|
16546
|
+
gsRenderer = mobileRenderer;
|
|
16547
|
+
} else {
|
|
16548
|
+
const desktopRenderer = new GSSplatRenderer(this.renderer, this.camera);
|
|
16549
|
+
this.useMobileRenderer = false;
|
|
16550
|
+
desktopRenderer.setData(splats);
|
|
16551
|
+
gsRenderer = desktopRenderer;
|
|
16552
|
+
}
|
|
16378
16553
|
this.sceneManager.setGSRenderer(gsRenderer);
|
|
16379
16554
|
this.hotspotManager.setGSRenderer(gsRenderer);
|
|
16380
|
-
this.useMobileRenderer = false;
|
|
16381
16555
|
if (onProgress) onProgress(100, "upload");
|
|
16382
16556
|
return splats.length;
|
|
16383
16557
|
} catch (error) {
|
|
16384
16558
|
throw error;
|
|
16385
16559
|
}
|
|
16386
16560
|
}
|
|
16561
|
+
/**
|
|
16562
|
+
* 将 SplatCPU[] 转换为 CompactSplatData(用于移动端渲染器)
|
|
16563
|
+
*/
|
|
16564
|
+
static splatCpuToCompactData(splats) {
|
|
16565
|
+
const count = splats.length;
|
|
16566
|
+
const positions = new Float32Array(count * 3);
|
|
16567
|
+
const scales = new Float32Array(count * 3);
|
|
16568
|
+
const rotations = new Float32Array(count * 4);
|
|
16569
|
+
const colors = new Float32Array(count * 3);
|
|
16570
|
+
const opacities = new Float32Array(count);
|
|
16571
|
+
for (let i = 0; i < count; i++) {
|
|
16572
|
+
const s = splats[i];
|
|
16573
|
+
const i3 = i * 3, i4 = i * 4;
|
|
16574
|
+
positions[i3] = s.mean[0];
|
|
16575
|
+
positions[i3 + 1] = s.mean[1];
|
|
16576
|
+
positions[i3 + 2] = s.mean[2];
|
|
16577
|
+
scales[i3] = s.scale[0];
|
|
16578
|
+
scales[i3 + 1] = s.scale[1];
|
|
16579
|
+
scales[i3 + 2] = s.scale[2];
|
|
16580
|
+
rotations[i4] = s.rotation[0];
|
|
16581
|
+
rotations[i4 + 1] = s.rotation[1];
|
|
16582
|
+
rotations[i4 + 2] = s.rotation[2];
|
|
16583
|
+
rotations[i4 + 3] = s.rotation[3];
|
|
16584
|
+
colors[i3] = s.colorDC[0];
|
|
16585
|
+
colors[i3 + 1] = s.colorDC[1];
|
|
16586
|
+
colors[i3 + 2] = s.colorDC[2];
|
|
16587
|
+
opacities[i] = s.opacity;
|
|
16588
|
+
}
|
|
16589
|
+
return { count, positions, scales, rotations, colors, opacities };
|
|
16590
|
+
}
|
|
16387
16591
|
/**
|
|
16388
16592
|
* 加载 SOG 文件 (Spatially Ordered Gaussians)
|
|
16389
16593
|
* @param coordinateSystem 源数据坐标系,默认 'blender'(Z-up → Y-up 自动转换)
|
|
@@ -16416,14 +16620,13 @@ class App {
|
|
|
16416
16620
|
const i3 = i * 3, i4 = i * 4;
|
|
16417
16621
|
const py = positions[i3 + 1], pz = positions[i3 + 2];
|
|
16418
16622
|
positions[i3 + 1] = pz;
|
|
16419
|
-
positions[i3 + 2] = py;
|
|
16623
|
+
positions[i3 + 2] = -py;
|
|
16420
16624
|
const sy = scales[i3 + 1], sz = scales[i3 + 2];
|
|
16421
16625
|
scales[i3 + 1] = sz;
|
|
16422
16626
|
scales[i3 + 2] = sy;
|
|
16423
|
-
const
|
|
16424
|
-
rotations[i4] = -rw;
|
|
16627
|
+
const ry = rotations[i4 + 2], rz = rotations[i4 + 3];
|
|
16425
16628
|
rotations[i4 + 2] = rz;
|
|
16426
|
-
rotations[i4 + 3] = ry;
|
|
16629
|
+
rotations[i4 + 3] = -ry;
|
|
16427
16630
|
if (shCoeffs) {
|
|
16428
16631
|
transformSHCoeffsYZSwap(shCoeffs, i * 45, 15);
|
|
16429
16632
|
}
|
|
@@ -16848,6 +17051,63 @@ class App {
|
|
|
16848
17051
|
getSceneAidsRenderer() {
|
|
16849
17052
|
return this.sceneAids;
|
|
16850
17053
|
}
|
|
17054
|
+
// ============================================
|
|
17055
|
+
// 渲染性能控制
|
|
17056
|
+
// ============================================
|
|
17057
|
+
/**
|
|
17058
|
+
* 设置渲染缩放比例(影响内部分辨率)
|
|
17059
|
+
* 0.5 = 半分辨率(性能提升约 4 倍),1.0 = 正常
|
|
17060
|
+
* 适用于移动端提质或桌面端降负载
|
|
17061
|
+
*/
|
|
17062
|
+
setRenderScale(scale) {
|
|
17063
|
+
this.renderer.setRenderScale(scale);
|
|
17064
|
+
}
|
|
17065
|
+
getRenderScale() {
|
|
17066
|
+
return this.renderer.getRenderScale();
|
|
17067
|
+
}
|
|
17068
|
+
/**
|
|
17069
|
+
* 覆盖自动 DPR,传 -1 恢复自动推荐
|
|
17070
|
+
*/
|
|
17071
|
+
setDPR(dpr) {
|
|
17072
|
+
this.renderer.setDPR(dpr);
|
|
17073
|
+
}
|
|
17074
|
+
getDPR() {
|
|
17075
|
+
return this.renderer.getDPR();
|
|
17076
|
+
}
|
|
17077
|
+
/**
|
|
17078
|
+
* 设置桌面端亚像素剔除阈值(默认 1.0)
|
|
17079
|
+
* 值越大剔除越激进,近距离性能越好,但远处细节可能丢失
|
|
17080
|
+
*/
|
|
17081
|
+
setPixelCullThreshold(threshold) {
|
|
17082
|
+
const gsRenderer = this.getGSRenderer();
|
|
17083
|
+
if (gsRenderer) {
|
|
17084
|
+
gsRenderer.setPixelCullThreshold(threshold);
|
|
17085
|
+
}
|
|
17086
|
+
}
|
|
17087
|
+
/**
|
|
17088
|
+
* 设置桌面端最大可见 splat 数(0 = 不限制)
|
|
17089
|
+
* 限制绘制数量是应对近距离卡顿最直接的手段
|
|
17090
|
+
*/
|
|
17091
|
+
setMaxVisibleSplats(count) {
|
|
17092
|
+
const gsRenderer = this.getGSRenderer();
|
|
17093
|
+
if (gsRenderer) {
|
|
17094
|
+
gsRenderer.setMaxVisibleSplats(count);
|
|
17095
|
+
}
|
|
17096
|
+
}
|
|
17097
|
+
/**
|
|
17098
|
+
* 设置排序频率(1 = 每帧,2 = 每两帧,以此类推)
|
|
17099
|
+
* 降低排序频率可提升帧率,代价是移动时短暂排序瑕疵
|
|
17100
|
+
*/
|
|
17101
|
+
setSortFrequency(frequency) {
|
|
17102
|
+
const gsRenderer = this.getGSRenderer();
|
|
17103
|
+
if (gsRenderer) {
|
|
17104
|
+
gsRenderer.setSortFrequency(frequency);
|
|
17105
|
+
}
|
|
17106
|
+
const mobileRenderer = this.getGSRendererMobile();
|
|
17107
|
+
if (mobileRenderer) {
|
|
17108
|
+
mobileRenderer.setSortFrequency(frequency);
|
|
17109
|
+
}
|
|
17110
|
+
}
|
|
16851
17111
|
/**
|
|
16852
17112
|
* 销毁应用及所有资源
|
|
16853
17113
|
*/
|