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