@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 CHANGED
@@ -39,7 +39,7 @@ function isMobileDevice() {
39
39
  }
40
40
  function getRecommendedDPR() {
41
41
  const isMobile = isMobileDevice();
42
- const maxDpr = isMobile ? 1.5 : 3;
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
- const dpr = getRecommendedDPR();
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 = 4096;
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 shaderCodeMobileL0 = (
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
- _pad3: vec2<f32>,
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
- // 纹理绑定 - 4 张纹理(使用 RGBA32Float 保证精度)
8980
- @group(1) @binding(0) var positionTex: texture_2d<f32>; // RGBA32Float: xyz + unused
8981
- @group(1) @binding(1) var scaleRotTex1: texture_2d<f32>; // RGBA32Float: scale_xyz + rot_w
8982
- @group(1) @binding(2) var scaleRotTex2: texture_2d<f32>; // RGBA32Float: rot_xyz + unused
8983
- @group(1) @binding(3) var colorTex: texture_2d<f32>; // RGBA8Unorm: rgb + opacity
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
- let a = cov2D.x; let b = cov2D.y; let c = cov2D.z;
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
- let r1 = sqrt(lambda1);
9062
- let r2 = sqrt(lambda2);
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
- let color = colorSample.rgb;
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
- let screenOffset = axes[0] * quadPos.x * ELLIPSE_SCALE + axes[1] * quadPos.y * ELLIPSE_SCALE;
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
- var clipPos = uniforms.proj * viewPos;
9114
- clipPos.x = clipPos.x + screenOffset.x * clipPos.w;
9115
- clipPos.y = clipPos.y + screenOffset.y * clipPos.w;
9116
- output.position = clipPos;
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
- let r = length(input.localUV);
9126
- if (r > 1.0) { discard; }
9127
- let gaussianWeight = exp(-r * r * 4.0);
9128
- let alpha = input.opacity * gaussianWeight;
9129
- if (alpha < 0.004) { discard; } // 丢弃几乎透明的像素
9130
- let color = clamp(input.color, vec3<f32>(0.0), vec3<f32>(1.0));
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: shaderCodeMobileL0,
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
- // positionTex (RGBA32Float)
9312
- binding: 0,
9313
- visibility: GPUShaderStage.VERTEX,
9314
- texture: { sampleType: "unfilterable-float" }
9315
- },
9316
- {
9317
- // scaleRotTex1 (RGBA32Float)
9318
- binding: 1,
9319
- visibility: GPUShaderStage.VERTEX,
9320
- texture: { sampleType: "unfilterable-float" }
9321
- },
9322
- {
9323
- // scaleRotTex2 (RGBA32Float)
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 (64) + proj (64) + model (64) + cameraPos (12) + pad (4) + screenSize (8) + pad (8) + textureSize (8) + pad (8) = 240 bytes
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
- binding: 0,
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
- binding: 0,
9456
- resource: this.compressedTextures.positionTexture.createView()
9457
- },
9458
- {
9459
- binding: 1,
9460
- resource: this.compressedTextures.scaleRotTexture1.createView()
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, 0, 0])
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 shouldSort = isFirstFrame || this.frameCount % this.sortEveryNFrames === 0;
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
- return SHMode.L0;
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.L0,
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
  */