@d5techs/3dgs-lib 1.4.7 → 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 +325 -115
- package/dist/3dgs-lib.cjs.map +1 -1
- package/dist/3dgs-lib.js +325 -115
- package/dist/3dgs-lib.js.map +1 -1
- package/dist/App.d.ts +27 -0
- package/dist/core/Renderer.d.ts +19 -0
- package/dist/gs/GSSplatRendererMobile.d.ts +7 -14
- 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);
|
|
@@ -8436,6 +8479,65 @@ function compressSplatsToTextures(device, data) {
|
|
|
8436
8479
|
{ bytesPerRow: width * 4 },
|
|
8437
8480
|
{ width, height }
|
|
8438
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
|
+
}
|
|
8439
8541
|
return {
|
|
8440
8542
|
width,
|
|
8441
8543
|
height,
|
|
@@ -8444,6 +8546,10 @@ function compressSplatsToTextures(device, data) {
|
|
|
8444
8546
|
scaleRotTexture1,
|
|
8445
8547
|
scaleRotTexture2,
|
|
8446
8548
|
colorTexture,
|
|
8549
|
+
shBasis0Texture,
|
|
8550
|
+
shBasis1Texture,
|
|
8551
|
+
shBasis2Texture,
|
|
8552
|
+
hasSH,
|
|
8447
8553
|
boundingBox
|
|
8448
8554
|
};
|
|
8449
8555
|
}
|
|
@@ -8452,9 +8558,12 @@ function destroyCompressedTextures(textures) {
|
|
|
8452
8558
|
textures.scaleRotTexture1.destroy();
|
|
8453
8559
|
textures.scaleRotTexture2.destroy();
|
|
8454
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();
|
|
8455
8564
|
}
|
|
8456
8565
|
const DEFAULT_NUM_BUCKETS = 65536;
|
|
8457
|
-
const IOS_NUM_BUCKETS =
|
|
8566
|
+
const IOS_NUM_BUCKETS = 16384;
|
|
8458
8567
|
const WORKGROUP_SIZE = 256;
|
|
8459
8568
|
function isIOSDevice() {
|
|
8460
8569
|
if (typeof navigator === "undefined") return false;
|
|
@@ -8958,9 +9067,17 @@ class GSSplatSorterMobile {
|
|
|
8958
9067
|
this.drawIndirectBuffer.destroy();
|
|
8959
9068
|
}
|
|
8960
9069
|
}
|
|
8961
|
-
const
|
|
9070
|
+
const shaderCodeMobile = (
|
|
8962
9071
|
/* wgsl */
|
|
8963
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
|
+
|
|
8964
9081
|
struct Uniforms {
|
|
8965
9082
|
view: mat4x4<f32>,
|
|
8966
9083
|
proj: mat4x4<f32>,
|
|
@@ -8969,18 +9086,21 @@ struct Uniforms {
|
|
|
8969
9086
|
_pad: f32,
|
|
8970
9087
|
screenSize: vec2<f32>,
|
|
8971
9088
|
_pad2: vec2<f32>,
|
|
8972
|
-
textureSize: vec2<f32>,
|
|
8973
|
-
|
|
9089
|
+
textureSize: vec2<f32>,
|
|
9090
|
+
shEnabled: f32,
|
|
9091
|
+
_pad3: f32,
|
|
8974
9092
|
}
|
|
8975
9093
|
|
|
8976
9094
|
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
|
|
8977
9095
|
@group(0) @binding(1) var<storage, read> sortedIndices: array<u32>;
|
|
8978
9096
|
|
|
8979
|
-
|
|
8980
|
-
@group(1) @binding(
|
|
8981
|
-
@group(1) @binding(
|
|
8982
|
-
@group(1) @binding(
|
|
8983
|
-
@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>;
|
|
8984
9104
|
|
|
8985
9105
|
struct VertexOutput {
|
|
8986
9106
|
@builtin(position) position: vec4<f32>,
|
|
@@ -8998,7 +9118,6 @@ const QUAD_POSITIONS = array<vec2<f32>, 4>(
|
|
|
8998
9118
|
|
|
8999
9119
|
const ELLIPSE_SCALE: f32 = 3.0;
|
|
9000
9120
|
|
|
9001
|
-
// 将索引转换为纹理坐标
|
|
9002
9121
|
fn indexToTexCoord(index: u32) -> vec2<u32> {
|
|
9003
9122
|
let texWidth = u32(uniforms.textureSize.x);
|
|
9004
9123
|
let x = index % texWidth;
|
|
@@ -9006,7 +9125,6 @@ fn indexToTexCoord(index: u32) -> vec2<u32> {
|
|
|
9006
9125
|
return vec2<u32>(x, y);
|
|
9007
9126
|
}
|
|
9008
9127
|
|
|
9009
|
-
// 四元数转旋转矩阵
|
|
9010
9128
|
fn quatToMat3(q: vec4<f32>) -> mat3x3<f32> {
|
|
9011
9129
|
let w = q[0]; let x = q[1]; let y = q[2]; let z = q[3];
|
|
9012
9130
|
let x2 = x + x; let y2 = y + y; let z2 = z + z;
|
|
@@ -9020,15 +9138,12 @@ fn quatToMat3(q: vec4<f32>) -> mat3x3<f32> {
|
|
|
9020
9138
|
);
|
|
9021
9139
|
}
|
|
9022
9140
|
|
|
9023
|
-
// 从模型矩阵提取统一缩放因子(取 X 轴向量长度)
|
|
9024
9141
|
fn getModelScale(model: mat4x4<f32>) -> f32 {
|
|
9025
9142
|
return length(model[0].xyz);
|
|
9026
9143
|
}
|
|
9027
9144
|
|
|
9028
|
-
// 计算 2D 协方差
|
|
9029
9145
|
fn computeCov2D(mean: vec3<f32>, scale: vec3<f32>, rotation: vec4<f32>, modelView: mat4x4<f32>, proj: mat4x4<f32>, modelScale: f32) -> vec3<f32> {
|
|
9030
9146
|
let R = quatToMat3(rotation);
|
|
9031
|
-
// 应用模型缩放到 splat scale
|
|
9032
9147
|
let scaledScale = scale * modelScale;
|
|
9033
9148
|
let s2 = scaledScale * scaledScale;
|
|
9034
9149
|
let M = mat3x3<f32>(R[0] * s2.x, R[1] * s2.y, R[2] * s2.z);
|
|
@@ -9040,8 +9155,6 @@ fn computeCov2D(mean: vec3<f32>, scale: vec3<f32>, rotation: vec4<f32>, modelVie
|
|
|
9040
9155
|
let z = -viewPos.z;
|
|
9041
9156
|
let z_clamped = max(z, 0.001);
|
|
9042
9157
|
let z2 = z_clamped * z_clamped;
|
|
9043
|
-
// 雅可比矩阵: 从相机坐标 (x_cam, y_cam, z_cam) 到 NDC 的偏导数
|
|
9044
|
-
// x_ndc = fx * x_cam / (-z_cam), 所以 dx_ndc/dz_cam = fx * x_cam / z_cam^2 (正号!)
|
|
9045
9158
|
let j1 = vec3<f32>(fx / z_clamped, 0.0, fx * viewPos.x / z2);
|
|
9046
9159
|
let j2 = vec3<f32>(0.0, fy / z_clamped, fy * viewPos.y / z2);
|
|
9047
9160
|
let Sj1 = SigmaView * j1;
|
|
@@ -9049,17 +9162,22 @@ fn computeCov2D(mean: vec3<f32>, scale: vec3<f32>, rotation: vec4<f32>, modelVie
|
|
|
9049
9162
|
return vec3<f32>(dot(j1, Sj1), dot(j1, Sj2), dot(j2, Sj2));
|
|
9050
9163
|
}
|
|
9051
9164
|
|
|
9052
|
-
// 计算椭圆轴
|
|
9053
9165
|
fn computeEllipseAxes(cov2D: vec3<f32>) -> mat2x2<f32> {
|
|
9054
|
-
|
|
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;
|
|
9055
9170
|
let trace = a + c;
|
|
9056
9171
|
let det = a * c - b * b;
|
|
9057
9172
|
let disc = trace * trace - 4.0 * det;
|
|
9058
9173
|
let sqrtDisc = sqrt(max(disc, 0.0));
|
|
9059
9174
|
let lambda1 = max((trace + sqrtDisc) * 0.5, 0.0);
|
|
9060
9175
|
let lambda2 = max((trace - sqrtDisc) * 0.5, 0.0);
|
|
9061
|
-
|
|
9062
|
-
|
|
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);
|
|
9063
9181
|
var axis1: vec2<f32>; var axis2: vec2<f32>;
|
|
9064
9182
|
if (abs(b) > 1e-6) {
|
|
9065
9183
|
axis1 = normalize(vec2<f32>(b, lambda1 - a));
|
|
@@ -9075,59 +9193,88 @@ fn computeEllipseAxes(cov2D: vec3<f32>) -> mat2x2<f32> {
|
|
|
9075
9193
|
fn vs_main(@builtin(vertex_index) vertexIndex: u32, @builtin(instance_index) instanceIndex: u32) -> VertexOutput {
|
|
9076
9194
|
var output: VertexOutput;
|
|
9077
9195
|
|
|
9078
|
-
// 获取排序后的索引
|
|
9079
9196
|
let splatIndex = sortedIndices[instanceIndex];
|
|
9080
9197
|
let texCoord = indexToTexCoord(splatIndex);
|
|
9081
9198
|
|
|
9082
|
-
// 从纹理采样位置数据(RGBA32Float,直接读取)
|
|
9083
9199
|
let posSample = textureLoad(positionTex, texCoord, 0);
|
|
9084
9200
|
let mean = posSample.xyz;
|
|
9085
9201
|
|
|
9086
|
-
// 从纹理采样缩放和旋转(RGBA16Float,GPU 自动转换为 f32)
|
|
9087
9202
|
let scaleRot1 = textureLoad(scaleRotTex1, texCoord, 0);
|
|
9088
9203
|
let scaleRot2 = textureLoad(scaleRotTex2, texCoord, 0);
|
|
9089
|
-
|
|
9090
9204
|
let scale = scaleRot1.xyz;
|
|
9091
9205
|
let rotation = vec4<f32>(scaleRot1.w, scaleRot2.x, scaleRot2.y, scaleRot2.z);
|
|
9092
9206
|
|
|
9093
|
-
// 从纹理采样颜色(RGBA8Unorm,GPU 自动归一化到 0-1)
|
|
9094
9207
|
let colorSample = textureLoad(colorTex, texCoord, 0);
|
|
9095
|
-
|
|
9208
|
+
var color = colorSample.rgb;
|
|
9096
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
|
+
}
|
|
9097
9215
|
|
|
9098
|
-
// 计算顶点位置
|
|
9099
9216
|
let quadPos = QUAD_POSITIONS[vertexIndex];
|
|
9100
9217
|
output.localUV = quadPos;
|
|
9101
9218
|
|
|
9102
|
-
// 计算 modelView 矩阵和模型缩放
|
|
9103
9219
|
let modelView = uniforms.view * uniforms.model;
|
|
9104
9220
|
let modelScale = getModelScale(uniforms.model);
|
|
9105
9221
|
|
|
9106
9222
|
let cov2D = computeCov2D(mean, scale, rotation, modelView, uniforms.proj, modelScale);
|
|
9107
9223
|
let axes = computeEllipseAxes(cov2D);
|
|
9108
|
-
|
|
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;
|
|
9109
9232
|
|
|
9110
|
-
// 应用 model 变换到 splat 位置
|
|
9111
9233
|
let worldPos = uniforms.model * vec4<f32>(mean, 1.0);
|
|
9112
9234
|
let viewPos = uniforms.view * worldPos;
|
|
9113
|
-
|
|
9114
|
-
|
|
9115
|
-
|
|
9116
|
-
|
|
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
|
+
|
|
9117
9264
|
output.color = color;
|
|
9118
9265
|
output.opacity = opacity;
|
|
9119
|
-
|
|
9120
9266
|
return output;
|
|
9121
9267
|
}
|
|
9122
9268
|
|
|
9123
9269
|
@fragment
|
|
9124
9270
|
fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
|
|
9125
|
-
|
|
9126
|
-
|
|
9127
|
-
|
|
9128
|
-
let
|
|
9129
|
-
|
|
9130
|
-
|
|
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));
|
|
9131
9278
|
return vec4<f32>(color * alpha, alpha);
|
|
9132
9279
|
}
|
|
9133
9280
|
`
|
|
@@ -9157,6 +9304,10 @@ class GSSplatRendererMobile {
|
|
|
9157
9304
|
// 帧计数(用于排序频率控制)
|
|
9158
9305
|
__publicField(this, "frameCount", 0);
|
|
9159
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);
|
|
9160
9311
|
// ============================================
|
|
9161
9312
|
// 变换相关 (position, rotation, scale)
|
|
9162
9313
|
// ============================================
|
|
@@ -9167,6 +9318,8 @@ class GSSplatRendererMobile {
|
|
|
9167
9318
|
__publicField(this, "pivot", [0, 0, 0]);
|
|
9168
9319
|
// 旋转/缩放中心点
|
|
9169
9320
|
__publicField(this, "modelMatrix", new Float32Array(16));
|
|
9321
|
+
// 1x1 dummy SH 纹理(当无 SH 数据时使用)
|
|
9322
|
+
__publicField(this, "dummySHTexture", null);
|
|
9170
9323
|
this.renderer = renderer;
|
|
9171
9324
|
this.camera = camera;
|
|
9172
9325
|
this.createPipeline();
|
|
@@ -9288,7 +9441,7 @@ class GSSplatRendererMobile {
|
|
|
9288
9441
|
createPipeline() {
|
|
9289
9442
|
const device = this.renderer.device;
|
|
9290
9443
|
const shaderModule = device.createShaderModule({
|
|
9291
|
-
code:
|
|
9444
|
+
code: shaderCodeMobile,
|
|
9292
9445
|
label: "mobile-splat-shader"
|
|
9293
9446
|
});
|
|
9294
9447
|
this.uniformBindGroupLayout = device.createBindGroupLayout({
|
|
@@ -9305,32 +9458,27 @@ class GSSplatRendererMobile {
|
|
|
9305
9458
|
}
|
|
9306
9459
|
]
|
|
9307
9460
|
});
|
|
9461
|
+
const texEntry = (binding) => ({
|
|
9462
|
+
binding,
|
|
9463
|
+
visibility: GPUShaderStage.VERTEX,
|
|
9464
|
+
texture: { sampleType: "unfilterable-float" }
|
|
9465
|
+
});
|
|
9308
9466
|
this.textureBindGroupLayout = device.createBindGroupLayout({
|
|
9309
9467
|
entries: [
|
|
9310
|
-
|
|
9311
|
-
|
|
9312
|
-
|
|
9313
|
-
|
|
9314
|
-
|
|
9315
|
-
|
|
9316
|
-
|
|
9317
|
-
|
|
9318
|
-
|
|
9319
|
-
|
|
9320
|
-
|
|
9321
|
-
|
|
9322
|
-
|
|
9323
|
-
|
|
9324
|
-
binding: 2,
|
|
9325
|
-
visibility: GPUShaderStage.VERTEX,
|
|
9326
|
-
texture: { sampleType: "unfilterable-float" }
|
|
9327
|
-
},
|
|
9328
|
-
{
|
|
9329
|
-
// colorTex (RGBA8Unorm)
|
|
9330
|
-
binding: 3,
|
|
9331
|
-
visibility: GPUShaderStage.VERTEX,
|
|
9332
|
-
texture: { sampleType: "unfilterable-float" }
|
|
9333
|
-
}
|
|
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
|
|
9334
9482
|
]
|
|
9335
9483
|
});
|
|
9336
9484
|
const pipelineLayout = device.createPipelineLayout({
|
|
@@ -9374,10 +9522,16 @@ class GSSplatRendererMobile {
|
|
|
9374
9522
|
depthCompare: "always"
|
|
9375
9523
|
}
|
|
9376
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
|
+
});
|
|
9377
9531
|
}
|
|
9378
9532
|
/**
|
|
9379
9533
|
* 创建 uniform buffer
|
|
9380
|
-
* 布局: view
|
|
9534
|
+
* 布局: view(64) + proj(64) + model(64) + cameraPos(12)+pad(4) + screenSize(8)+pad(8) + textureSize(8)+shEnabled(4)+pad(4) = 240 bytes
|
|
9381
9535
|
*/
|
|
9382
9536
|
createUniformBuffer() {
|
|
9383
9537
|
this.uniformBuffer = this.renderer.device.createBuffer({
|
|
@@ -9438,35 +9592,22 @@ class GSSplatRendererMobile {
|
|
|
9438
9592
|
this.uniformBindGroup = device.createBindGroup({
|
|
9439
9593
|
layout: this.uniformBindGroupLayout,
|
|
9440
9594
|
entries: [
|
|
9441
|
-
{
|
|
9442
|
-
|
|
9443
|
-
resource: { buffer: this.uniformBuffer }
|
|
9444
|
-
},
|
|
9445
|
-
{
|
|
9446
|
-
binding: 1,
|
|
9447
|
-
resource: { buffer: this.sorter.getIndicesBuffer() }
|
|
9448
|
-
}
|
|
9595
|
+
{ binding: 0, resource: { buffer: this.uniformBuffer } },
|
|
9596
|
+
{ binding: 1, resource: { buffer: this.sorter.getIndicesBuffer() } }
|
|
9449
9597
|
]
|
|
9450
9598
|
});
|
|
9599
|
+
const dummyView = this.dummySHTexture.createView();
|
|
9600
|
+
const tex = this.compressedTextures;
|
|
9451
9601
|
this.textureBindGroup = device.createBindGroup({
|
|
9452
9602
|
layout: this.textureBindGroupLayout,
|
|
9453
9603
|
entries: [
|
|
9454
|
-
{
|
|
9455
|
-
|
|
9456
|
-
|
|
9457
|
-
},
|
|
9458
|
-
{
|
|
9459
|
-
|
|
9460
|
-
|
|
9461
|
-
},
|
|
9462
|
-
{
|
|
9463
|
-
binding: 2,
|
|
9464
|
-
resource: this.compressedTextures.scaleRotTexture2.createView()
|
|
9465
|
-
},
|
|
9466
|
-
{
|
|
9467
|
-
binding: 3,
|
|
9468
|
-
resource: this.compressedTextures.colorTexture.createView()
|
|
9469
|
-
}
|
|
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 }
|
|
9470
9611
|
]
|
|
9471
9612
|
});
|
|
9472
9613
|
}
|
|
@@ -9521,10 +9662,11 @@ class GSSplatRendererMobile {
|
|
|
9521
9662
|
208,
|
|
9522
9663
|
new Float32Array([this.renderer.width, this.renderer.height, 0, 0])
|
|
9523
9664
|
);
|
|
9665
|
+
const shEnabled = this.compressedTextures.hasSH ? 1 : 0;
|
|
9524
9666
|
device.queue.writeBuffer(
|
|
9525
9667
|
this.uniformBuffer,
|
|
9526
9668
|
224,
|
|
9527
|
-
new Float32Array([this.compressedTextures.width, this.compressedTextures.height,
|
|
9669
|
+
new Float32Array([this.compressedTextures.width, this.compressedTextures.height, shEnabled, 0])
|
|
9528
9670
|
);
|
|
9529
9671
|
this.sorter.setScreenSize(this.renderer.width, this.renderer.height);
|
|
9530
9672
|
this.sorter.setCullingOptions({
|
|
@@ -9533,7 +9675,8 @@ class GSSplatRendererMobile {
|
|
|
9533
9675
|
pixelThreshold: 1
|
|
9534
9676
|
});
|
|
9535
9677
|
const isFirstFrame = this.frameCount === 1;
|
|
9536
|
-
const
|
|
9678
|
+
const cameraChanged = this.hasCameraChanged();
|
|
9679
|
+
const shouldSort = isFirstFrame || cameraChanged && this.frameCount % this.sortEveryNFrames === 0;
|
|
9537
9680
|
if (shouldSort) {
|
|
9538
9681
|
this.sorter.sort();
|
|
9539
9682
|
}
|
|
@@ -9564,36 +9707,42 @@ class GSSplatRendererMobile {
|
|
|
9564
9707
|
setSortFrequency(n) {
|
|
9565
9708
|
this.sortEveryNFrames = Math.max(1, n);
|
|
9566
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
|
+
}
|
|
9567
9728
|
// ============================================
|
|
9568
9729
|
// IGSSplatRenderer 接口实现 - SH 模式
|
|
9569
9730
|
// ============================================
|
|
9570
|
-
|
|
9571
|
-
* 设置 SH 模式(移动端仅支持 L0)
|
|
9572
|
-
*/
|
|
9573
|
-
setSHMode(mode) {
|
|
9731
|
+
setSHMode(_mode) {
|
|
9574
9732
|
}
|
|
9575
|
-
/**
|
|
9576
|
-
* 获取当前 SH 模式
|
|
9577
|
-
*/
|
|
9578
9733
|
getSHMode() {
|
|
9579
|
-
|
|
9734
|
+
var _a2;
|
|
9735
|
+
return ((_a2 = this.compressedTextures) == null ? void 0 : _a2.hasSH) ? SHMode.L1 : SHMode.L0;
|
|
9580
9736
|
}
|
|
9581
|
-
/**
|
|
9582
|
-
* 是否支持指定的 SH 模式
|
|
9583
|
-
*/
|
|
9584
9737
|
supportsSHMode(mode) {
|
|
9585
|
-
return mode === SHMode.L0;
|
|
9738
|
+
return mode === SHMode.L0 || mode === SHMode.L1;
|
|
9586
9739
|
}
|
|
9587
|
-
/**
|
|
9588
|
-
* 获取渲染器能力
|
|
9589
|
-
*/
|
|
9590
9740
|
getCapabilities() {
|
|
9591
9741
|
return {
|
|
9592
|
-
maxSHMode: SHMode.
|
|
9742
|
+
maxSHMode: SHMode.L1,
|
|
9593
9743
|
supportsRawData: false,
|
|
9594
9744
|
isMobileOptimized: true,
|
|
9595
9745
|
maxSplatCount: 0
|
|
9596
|
-
// 无限制(受 GPU 内存限制)
|
|
9597
9746
|
};
|
|
9598
9747
|
}
|
|
9599
9748
|
/**
|
|
@@ -9622,6 +9771,10 @@ class GSSplatRendererMobile {
|
|
|
9622
9771
|
*/
|
|
9623
9772
|
destroy() {
|
|
9624
9773
|
this.destroyInternal();
|
|
9774
|
+
if (this.dummySHTexture) {
|
|
9775
|
+
this.dummySHTexture.destroy();
|
|
9776
|
+
this.dummySHTexture = null;
|
|
9777
|
+
}
|
|
9625
9778
|
}
|
|
9626
9779
|
}
|
|
9627
9780
|
class SceneManager {
|
|
@@ -16900,6 +17053,63 @@ class App {
|
|
|
16900
17053
|
getSceneAidsRenderer() {
|
|
16901
17054
|
return this.sceneAids;
|
|
16902
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
|
+
}
|
|
16903
17113
|
/**
|
|
16904
17114
|
* 销毁应用及所有资源
|
|
16905
17115
|
*/
|