@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.js CHANGED
@@ -37,7 +37,7 @@ function isMobileDevice() {
37
37
  }
38
38
  function getRecommendedDPR() {
39
39
  const isMobile = isMobileDevice();
40
- const maxDpr = isMobile ? 1.5 : 3;
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
- const dpr = getRecommendedDPR();
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);
@@ -8434,6 +8477,65 @@ function compressSplatsToTextures(device, data) {
8434
8477
  { bytesPerRow: width * 4 },
8435
8478
  { width, height }
8436
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
+ }
8437
8539
  return {
8438
8540
  width,
8439
8541
  height,
@@ -8442,6 +8544,10 @@ function compressSplatsToTextures(device, data) {
8442
8544
  scaleRotTexture1,
8443
8545
  scaleRotTexture2,
8444
8546
  colorTexture,
8547
+ shBasis0Texture,
8548
+ shBasis1Texture,
8549
+ shBasis2Texture,
8550
+ hasSH,
8445
8551
  boundingBox
8446
8552
  };
8447
8553
  }
@@ -8450,9 +8556,12 @@ function destroyCompressedTextures(textures) {
8450
8556
  textures.scaleRotTexture1.destroy();
8451
8557
  textures.scaleRotTexture2.destroy();
8452
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();
8453
8562
  }
8454
8563
  const DEFAULT_NUM_BUCKETS = 65536;
8455
- const IOS_NUM_BUCKETS = 4096;
8564
+ const IOS_NUM_BUCKETS = 16384;
8456
8565
  const WORKGROUP_SIZE = 256;
8457
8566
  function isIOSDevice() {
8458
8567
  if (typeof navigator === "undefined") return false;
@@ -8956,9 +9065,17 @@ class GSSplatSorterMobile {
8956
9065
  this.drawIndirectBuffer.destroy();
8957
9066
  }
8958
9067
  }
8959
- const shaderCodeMobileL0 = (
9068
+ const shaderCodeMobile = (
8960
9069
  /* wgsl */
8961
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
+
8962
9079
  struct Uniforms {
8963
9080
  view: mat4x4<f32>,
8964
9081
  proj: mat4x4<f32>,
@@ -8967,18 +9084,21 @@ struct Uniforms {
8967
9084
  _pad: f32,
8968
9085
  screenSize: vec2<f32>,
8969
9086
  _pad2: vec2<f32>,
8970
- textureSize: vec2<f32>, // 纹理尺寸 (用于坐标计算)
8971
- _pad3: vec2<f32>,
9087
+ textureSize: vec2<f32>,
9088
+ shEnabled: f32,
9089
+ _pad3: f32,
8972
9090
  }
8973
9091
 
8974
9092
  @group(0) @binding(0) var<uniform> uniforms: Uniforms;
8975
9093
  @group(0) @binding(1) var<storage, read> sortedIndices: array<u32>;
8976
9094
 
8977
- // 纹理绑定 - 4 张纹理(使用 RGBA32Float 保证精度)
8978
- @group(1) @binding(0) var positionTex: texture_2d<f32>; // RGBA32Float: xyz + unused
8979
- @group(1) @binding(1) var scaleRotTex1: texture_2d<f32>; // RGBA32Float: scale_xyz + rot_w
8980
- @group(1) @binding(2) var scaleRotTex2: texture_2d<f32>; // RGBA32Float: rot_xyz + unused
8981
- @group(1) @binding(3) var colorTex: texture_2d<f32>; // RGBA8Unorm: rgb + opacity
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>;
8982
9102
 
8983
9103
  struct VertexOutput {
8984
9104
  @builtin(position) position: vec4<f32>,
@@ -8996,7 +9116,6 @@ const QUAD_POSITIONS = array<vec2<f32>, 4>(
8996
9116
 
8997
9117
  const ELLIPSE_SCALE: f32 = 3.0;
8998
9118
 
8999
- // 将索引转换为纹理坐标
9000
9119
  fn indexToTexCoord(index: u32) -> vec2<u32> {
9001
9120
  let texWidth = u32(uniforms.textureSize.x);
9002
9121
  let x = index % texWidth;
@@ -9004,7 +9123,6 @@ fn indexToTexCoord(index: u32) -> vec2<u32> {
9004
9123
  return vec2<u32>(x, y);
9005
9124
  }
9006
9125
 
9007
- // 四元数转旋转矩阵
9008
9126
  fn quatToMat3(q: vec4<f32>) -> mat3x3<f32> {
9009
9127
  let w = q[0]; let x = q[1]; let y = q[2]; let z = q[3];
9010
9128
  let x2 = x + x; let y2 = y + y; let z2 = z + z;
@@ -9018,15 +9136,12 @@ fn quatToMat3(q: vec4<f32>) -> mat3x3<f32> {
9018
9136
  );
9019
9137
  }
9020
9138
 
9021
- // 从模型矩阵提取统一缩放因子(取 X 轴向量长度)
9022
9139
  fn getModelScale(model: mat4x4<f32>) -> f32 {
9023
9140
  return length(model[0].xyz);
9024
9141
  }
9025
9142
 
9026
- // 计算 2D 协方差
9027
9143
  fn computeCov2D(mean: vec3<f32>, scale: vec3<f32>, rotation: vec4<f32>, modelView: mat4x4<f32>, proj: mat4x4<f32>, modelScale: f32) -> vec3<f32> {
9028
9144
  let R = quatToMat3(rotation);
9029
- // 应用模型缩放到 splat scale
9030
9145
  let scaledScale = scale * modelScale;
9031
9146
  let s2 = scaledScale * scaledScale;
9032
9147
  let M = mat3x3<f32>(R[0] * s2.x, R[1] * s2.y, R[2] * s2.z);
@@ -9038,8 +9153,6 @@ fn computeCov2D(mean: vec3<f32>, scale: vec3<f32>, rotation: vec4<f32>, modelVie
9038
9153
  let z = -viewPos.z;
9039
9154
  let z_clamped = max(z, 0.001);
9040
9155
  let z2 = z_clamped * z_clamped;
9041
- // 雅可比矩阵: 从相机坐标 (x_cam, y_cam, z_cam) 到 NDC 的偏导数
9042
- // x_ndc = fx * x_cam / (-z_cam), 所以 dx_ndc/dz_cam = fx * x_cam / z_cam^2 (正号!)
9043
9156
  let j1 = vec3<f32>(fx / z_clamped, 0.0, fx * viewPos.x / z2);
9044
9157
  let j2 = vec3<f32>(0.0, fy / z_clamped, fy * viewPos.y / z2);
9045
9158
  let Sj1 = SigmaView * j1;
@@ -9047,17 +9160,22 @@ fn computeCov2D(mean: vec3<f32>, scale: vec3<f32>, rotation: vec4<f32>, modelVie
9047
9160
  return vec3<f32>(dot(j1, Sj1), dot(j1, Sj2), dot(j2, Sj2));
9048
9161
  }
9049
9162
 
9050
- // 计算椭圆轴
9051
9163
  fn computeEllipseAxes(cov2D: vec3<f32>) -> mat2x2<f32> {
9052
- let a = cov2D.x; let b = cov2D.y; let c = cov2D.z;
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;
9053
9168
  let trace = a + c;
9054
9169
  let det = a * c - b * b;
9055
9170
  let disc = trace * trace - 4.0 * det;
9056
9171
  let sqrtDisc = sqrt(max(disc, 0.0));
9057
9172
  let lambda1 = max((trace + sqrtDisc) * 0.5, 0.0);
9058
9173
  let lambda2 = max((trace - sqrtDisc) * 0.5, 0.0);
9059
- let r1 = sqrt(lambda1);
9060
- let r2 = sqrt(lambda2);
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);
9061
9179
  var axis1: vec2<f32>; var axis2: vec2<f32>;
9062
9180
  if (abs(b) > 1e-6) {
9063
9181
  axis1 = normalize(vec2<f32>(b, lambda1 - a));
@@ -9073,59 +9191,88 @@ fn computeEllipseAxes(cov2D: vec3<f32>) -> mat2x2<f32> {
9073
9191
  fn vs_main(@builtin(vertex_index) vertexIndex: u32, @builtin(instance_index) instanceIndex: u32) -> VertexOutput {
9074
9192
  var output: VertexOutput;
9075
9193
 
9076
- // 获取排序后的索引
9077
9194
  let splatIndex = sortedIndices[instanceIndex];
9078
9195
  let texCoord = indexToTexCoord(splatIndex);
9079
9196
 
9080
- // 从纹理采样位置数据(RGBA32Float,直接读取)
9081
9197
  let posSample = textureLoad(positionTex, texCoord, 0);
9082
9198
  let mean = posSample.xyz;
9083
9199
 
9084
- // 从纹理采样缩放和旋转(RGBA16Float,GPU 自动转换为 f32)
9085
9200
  let scaleRot1 = textureLoad(scaleRotTex1, texCoord, 0);
9086
9201
  let scaleRot2 = textureLoad(scaleRotTex2, texCoord, 0);
9087
-
9088
9202
  let scale = scaleRot1.xyz;
9089
9203
  let rotation = vec4<f32>(scaleRot1.w, scaleRot2.x, scaleRot2.y, scaleRot2.z);
9090
9204
 
9091
- // 从纹理采样颜色(RGBA8Unorm,GPU 自动归一化到 0-1)
9092
9205
  let colorSample = textureLoad(colorTex, texCoord, 0);
9093
- let color = colorSample.rgb;
9206
+ var color = colorSample.rgb;
9094
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
+ }
9095
9213
 
9096
- // 计算顶点位置
9097
9214
  let quadPos = QUAD_POSITIONS[vertexIndex];
9098
9215
  output.localUV = quadPos;
9099
9216
 
9100
- // 计算 modelView 矩阵和模型缩放
9101
9217
  let modelView = uniforms.view * uniforms.model;
9102
9218
  let modelScale = getModelScale(uniforms.model);
9103
9219
 
9104
9220
  let cov2D = computeCov2D(mean, scale, rotation, modelView, uniforms.proj, modelScale);
9105
9221
  let axes = computeEllipseAxes(cov2D);
9106
- let screenOffset = axes[0] * quadPos.x * ELLIPSE_SCALE + axes[1] * quadPos.y * ELLIPSE_SCALE;
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;
9107
9230
 
9108
- // 应用 model 变换到 splat 位置
9109
9231
  let worldPos = uniforms.model * vec4<f32>(mean, 1.0);
9110
9232
  let viewPos = uniforms.view * worldPos;
9111
- var clipPos = uniforms.proj * viewPos;
9112
- clipPos.x = clipPos.x + screenOffset.x * clipPos.w;
9113
- clipPos.y = clipPos.y + screenOffset.y * clipPos.w;
9114
- output.position = clipPos;
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
+
9115
9262
  output.color = color;
9116
9263
  output.opacity = opacity;
9117
-
9118
9264
  return output;
9119
9265
  }
9120
9266
 
9121
9267
  @fragment
9122
9268
  fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
9123
- let r = length(input.localUV);
9124
- if (r > 1.0) { discard; }
9125
- let gaussianWeight = exp(-r * r * 4.0);
9126
- let alpha = input.opacity * gaussianWeight;
9127
- if (alpha < 0.004) { discard; } // 丢弃几乎透明的像素
9128
- let color = clamp(input.color, vec3<f32>(0.0), vec3<f32>(1.0));
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));
9129
9276
  return vec4<f32>(color * alpha, alpha);
9130
9277
  }
9131
9278
  `
@@ -9155,6 +9302,10 @@ class GSSplatRendererMobile {
9155
9302
  // 帧计数(用于排序频率控制)
9156
9303
  __publicField(this, "frameCount", 0);
9157
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);
9158
9309
  // ============================================
9159
9310
  // 变换相关 (position, rotation, scale)
9160
9311
  // ============================================
@@ -9165,6 +9316,8 @@ class GSSplatRendererMobile {
9165
9316
  __publicField(this, "pivot", [0, 0, 0]);
9166
9317
  // 旋转/缩放中心点
9167
9318
  __publicField(this, "modelMatrix", new Float32Array(16));
9319
+ // 1x1 dummy SH 纹理(当无 SH 数据时使用)
9320
+ __publicField(this, "dummySHTexture", null);
9168
9321
  this.renderer = renderer;
9169
9322
  this.camera = camera;
9170
9323
  this.createPipeline();
@@ -9286,7 +9439,7 @@ class GSSplatRendererMobile {
9286
9439
  createPipeline() {
9287
9440
  const device = this.renderer.device;
9288
9441
  const shaderModule = device.createShaderModule({
9289
- code: shaderCodeMobileL0,
9442
+ code: shaderCodeMobile,
9290
9443
  label: "mobile-splat-shader"
9291
9444
  });
9292
9445
  this.uniformBindGroupLayout = device.createBindGroupLayout({
@@ -9303,32 +9456,27 @@ class GSSplatRendererMobile {
9303
9456
  }
9304
9457
  ]
9305
9458
  });
9459
+ const texEntry = (binding) => ({
9460
+ binding,
9461
+ visibility: GPUShaderStage.VERTEX,
9462
+ texture: { sampleType: "unfilterable-float" }
9463
+ });
9306
9464
  this.textureBindGroupLayout = device.createBindGroupLayout({
9307
9465
  entries: [
9308
- {
9309
- // positionTex (RGBA32Float)
9310
- binding: 0,
9311
- visibility: GPUShaderStage.VERTEX,
9312
- texture: { sampleType: "unfilterable-float" }
9313
- },
9314
- {
9315
- // scaleRotTex1 (RGBA32Float)
9316
- binding: 1,
9317
- visibility: GPUShaderStage.VERTEX,
9318
- texture: { sampleType: "unfilterable-float" }
9319
- },
9320
- {
9321
- // scaleRotTex2 (RGBA32Float)
9322
- binding: 2,
9323
- visibility: GPUShaderStage.VERTEX,
9324
- texture: { sampleType: "unfilterable-float" }
9325
- },
9326
- {
9327
- // colorTex (RGBA8Unorm)
9328
- binding: 3,
9329
- visibility: GPUShaderStage.VERTEX,
9330
- texture: { sampleType: "unfilterable-float" }
9331
- }
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
9332
9480
  ]
9333
9481
  });
9334
9482
  const pipelineLayout = device.createPipelineLayout({
@@ -9372,10 +9520,16 @@ class GSSplatRendererMobile {
9372
9520
  depthCompare: "always"
9373
9521
  }
9374
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
+ });
9375
9529
  }
9376
9530
  /**
9377
9531
  * 创建 uniform buffer
9378
- * 布局: view (64) + proj (64) + model (64) + cameraPos (12) + pad (4) + screenSize (8) + pad (8) + textureSize (8) + pad (8) = 240 bytes
9532
+ * 布局: view(64) + proj(64) + model(64) + cameraPos(12)+pad(4) + screenSize(8)+pad(8) + textureSize(8)+shEnabled(4)+pad(4) = 240 bytes
9379
9533
  */
9380
9534
  createUniformBuffer() {
9381
9535
  this.uniformBuffer = this.renderer.device.createBuffer({
@@ -9436,35 +9590,22 @@ class GSSplatRendererMobile {
9436
9590
  this.uniformBindGroup = device.createBindGroup({
9437
9591
  layout: this.uniformBindGroupLayout,
9438
9592
  entries: [
9439
- {
9440
- binding: 0,
9441
- resource: { buffer: this.uniformBuffer }
9442
- },
9443
- {
9444
- binding: 1,
9445
- resource: { buffer: this.sorter.getIndicesBuffer() }
9446
- }
9593
+ { binding: 0, resource: { buffer: this.uniformBuffer } },
9594
+ { binding: 1, resource: { buffer: this.sorter.getIndicesBuffer() } }
9447
9595
  ]
9448
9596
  });
9597
+ const dummyView = this.dummySHTexture.createView();
9598
+ const tex = this.compressedTextures;
9449
9599
  this.textureBindGroup = device.createBindGroup({
9450
9600
  layout: this.textureBindGroupLayout,
9451
9601
  entries: [
9452
- {
9453
- binding: 0,
9454
- resource: this.compressedTextures.positionTexture.createView()
9455
- },
9456
- {
9457
- binding: 1,
9458
- resource: this.compressedTextures.scaleRotTexture1.createView()
9459
- },
9460
- {
9461
- binding: 2,
9462
- resource: this.compressedTextures.scaleRotTexture2.createView()
9463
- },
9464
- {
9465
- binding: 3,
9466
- resource: this.compressedTextures.colorTexture.createView()
9467
- }
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 }
9468
9609
  ]
9469
9610
  });
9470
9611
  }
@@ -9519,10 +9660,11 @@ class GSSplatRendererMobile {
9519
9660
  208,
9520
9661
  new Float32Array([this.renderer.width, this.renderer.height, 0, 0])
9521
9662
  );
9663
+ const shEnabled = this.compressedTextures.hasSH ? 1 : 0;
9522
9664
  device.queue.writeBuffer(
9523
9665
  this.uniformBuffer,
9524
9666
  224,
9525
- new Float32Array([this.compressedTextures.width, this.compressedTextures.height, 0, 0])
9667
+ new Float32Array([this.compressedTextures.width, this.compressedTextures.height, shEnabled, 0])
9526
9668
  );
9527
9669
  this.sorter.setScreenSize(this.renderer.width, this.renderer.height);
9528
9670
  this.sorter.setCullingOptions({
@@ -9531,7 +9673,8 @@ class GSSplatRendererMobile {
9531
9673
  pixelThreshold: 1
9532
9674
  });
9533
9675
  const isFirstFrame = this.frameCount === 1;
9534
- const shouldSort = isFirstFrame || this.frameCount % this.sortEveryNFrames === 0;
9676
+ const cameraChanged = this.hasCameraChanged();
9677
+ const shouldSort = isFirstFrame || cameraChanged && this.frameCount % this.sortEveryNFrames === 0;
9535
9678
  if (shouldSort) {
9536
9679
  this.sorter.sort();
9537
9680
  }
@@ -9562,36 +9705,42 @@ class GSSplatRendererMobile {
9562
9705
  setSortFrequency(n) {
9563
9706
  this.sortEveryNFrames = Math.max(1, n);
9564
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
+ }
9565
9726
  // ============================================
9566
9727
  // IGSSplatRenderer 接口实现 - SH 模式
9567
9728
  // ============================================
9568
- /**
9569
- * 设置 SH 模式(移动端仅支持 L0)
9570
- */
9571
- setSHMode(mode) {
9729
+ setSHMode(_mode) {
9572
9730
  }
9573
- /**
9574
- * 获取当前 SH 模式
9575
- */
9576
9731
  getSHMode() {
9577
- return SHMode.L0;
9732
+ var _a2;
9733
+ return ((_a2 = this.compressedTextures) == null ? void 0 : _a2.hasSH) ? SHMode.L1 : SHMode.L0;
9578
9734
  }
9579
- /**
9580
- * 是否支持指定的 SH 模式
9581
- */
9582
9735
  supportsSHMode(mode) {
9583
- return mode === SHMode.L0;
9736
+ return mode === SHMode.L0 || mode === SHMode.L1;
9584
9737
  }
9585
- /**
9586
- * 获取渲染器能力
9587
- */
9588
9738
  getCapabilities() {
9589
9739
  return {
9590
- maxSHMode: SHMode.L0,
9740
+ maxSHMode: SHMode.L1,
9591
9741
  supportsRawData: false,
9592
9742
  isMobileOptimized: true,
9593
9743
  maxSplatCount: 0
9594
- // 无限制(受 GPU 内存限制)
9595
9744
  };
9596
9745
  }
9597
9746
  /**
@@ -9620,6 +9769,10 @@ class GSSplatRendererMobile {
9620
9769
  */
9621
9770
  destroy() {
9622
9771
  this.destroyInternal();
9772
+ if (this.dummySHTexture) {
9773
+ this.dummySHTexture.destroy();
9774
+ this.dummySHTexture = null;
9775
+ }
9623
9776
  }
9624
9777
  }
9625
9778
  class SceneManager {
@@ -16898,6 +17051,63 @@ class App {
16898
17051
  getSceneAidsRenderer() {
16899
17052
  return this.sceneAids;
16900
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
+ }
16901
17111
  /**
16902
17112
  * 销毁应用及所有资源
16903
17113
  */