@d5techs/3dgs-lib 1.4.7 → 1.4.9

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);
@@ -960,514 +1003,212 @@ class OrbitControls {
960
1003
  this.velocityPanZ = 0;
961
1004
  }
962
1005
  }
963
- const gizmoShaderCode = (
964
- /* wgsl */
965
- `
966
- struct Uniforms {
967
- viewMatrix: mat4x4<f32>,
968
- projMatrix: mat4x4<f32>,
969
- }
970
-
971
- @group(0) @binding(0) var<uniform> uniforms: Uniforms;
972
-
973
- struct VertexInput {
974
- @location(0) position: vec3<f32>,
975
- @location(1) color: vec3<f32>,
976
- }
977
-
978
- struct VertexOutput {
979
- @builtin(position) position: vec4<f32>,
980
- @location(0) color: vec3<f32>,
981
- }
982
-
983
- @vertex
984
- fn vs_main(input: VertexInput) -> VertexOutput {
985
- var output: VertexOutput;
986
- let worldPos = vec4<f32>(input.position, 1.0);
987
- output.position = uniforms.projMatrix * uniforms.viewMatrix * worldPos;
988
- output.color = input.color;
989
- return output;
990
- }
991
-
992
- @fragment
993
- fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
994
- return vec4<f32>(input.color, 1.0);
995
- }
996
- `
997
- );
998
1006
  class ViewportGizmo {
999
- constructor(renderer, camera, canvas) {
1000
- __publicField(this, "renderer");
1007
+ constructor(_renderer, camera, canvas) {
1001
1008
  __publicField(this, "camera");
1002
1009
  __publicField(this, "canvas");
1003
- // 渲染资源
1004
- __publicField(this, "pipeline");
1005
- __publicField(this, "uniformBuffer");
1006
- __publicField(this, "bindGroup");
1007
- __publicField(this, "vertexBuffer");
1008
- __publicField(this, "indexBuffer");
1009
- __publicField(this, "vertexCount", 0);
1010
- __publicField(this, "indexCount", 0);
1011
- // Gizmo 配置
1012
- __publicField(this, "size", 200);
1013
- // Gizmo 尺寸(像素)
1014
- __publicField(this, "margin", 20);
1015
- // 边距
1016
- // Gizmo 投影矩阵
1017
- __publicField(this, "projMatrix", new Float32Array(16));
1018
- __publicField(this, "viewMatrix", new Float32Array(16));
1019
- // 轴配置
1020
- __publicField(this, "axes", [
1021
- { direction: [1, 0, 0], color: [0.9, 0.2, 0.2], label: "X" },
1022
- // 红色 X
1023
- { direction: [0, 1, 0], color: [0.2, 0.9, 0.2], label: "Y" },
1024
- // 绿色 Y
1025
- { direction: [0, 0, 1], color: [0.2, 0.4, 0.9], label: "Z" }
1026
- // 蓝色 Z
1027
- ]);
1028
- // 交互回调
1010
+ __publicField(this, "container");
1011
+ __publicField(this, "svg");
1012
+ __publicField(this, "group");
1013
+ __publicField(this, "shapes");
1014
+ __publicField(this, "size", 140);
1015
+ __publicField(this, "margin", 10);
1016
+ __publicField(this, "scale", 40);
1029
1017
  __publicField(this, "onAxisClick");
1030
- this.renderer = renderer;
1018
+ __publicField(this, "resizeObserver");
1031
1019
  this.camera = camera;
1032
1020
  this.canvas = canvas;
1033
- this.createPipeline();
1034
- this.createGeometry();
1035
- this.createUniformBuffer();
1036
- this.setupOrthoProjection();
1021
+ this.createSVG();
1022
+ this.setupResizeObserver();
1037
1023
  }
1038
- /**
1039
- * 设置轴点击回调
1040
- */
1041
1024
  setOnAxisClick(callback) {
1042
1025
  this.onAxisClick = callback;
1043
1026
  }
1044
- /**
1045
- * 创建渲染管线
1046
- */
1047
- createPipeline() {
1048
- const device = this.renderer.device;
1049
- const shaderModule = device.createShaderModule({
1050
- code: gizmoShaderCode
1051
- });
1052
- const bindGroupLayout = device.createBindGroupLayout({
1053
- entries: [
1054
- {
1055
- binding: 0,
1056
- visibility: GPUShaderStage.VERTEX,
1057
- buffer: { type: "uniform" }
1058
- }
1059
- ]
1060
- });
1061
- const pipelineLayout = device.createPipelineLayout({
1062
- bindGroupLayouts: [bindGroupLayout]
1027
+ createSVG() {
1028
+ const ns = "http://www.w3.org/2000/svg";
1029
+ this.container = document.createElement("div");
1030
+ Object.assign(this.container.style, {
1031
+ position: "absolute",
1032
+ width: `${this.size}px`,
1033
+ height: `${this.size}px`,
1034
+ pointerEvents: "none",
1035
+ zIndex: "10"
1063
1036
  });
1064
- const vertexBufferLayout = {
1065
- arrayStride: 24,
1066
- attributes: [
1067
- { shaderLocation: 0, offset: 0, format: "float32x3" },
1068
- { shaderLocation: 1, offset: 12, format: "float32x3" }
1069
- ]
1037
+ this.positionContainer();
1038
+ this.svg = document.createElementNS(ns, "svg");
1039
+ this.svg.setAttribute("width", this.size.toString());
1040
+ this.svg.setAttribute("height", this.size.toString());
1041
+ this.group = document.createElementNS(ns, "g");
1042
+ this.group.setAttribute(
1043
+ "transform",
1044
+ `translate(${this.size / 2}, ${this.size / 2})`
1045
+ );
1046
+ this.svg.appendChild(this.group);
1047
+ const r = "#f44";
1048
+ const g = "#4f4";
1049
+ const b = "#77f";
1050
+ this.shapes = {
1051
+ nx: this.createCircle(r, false),
1052
+ ny: this.createCircle(g, false),
1053
+ nz: this.createCircle(b, false),
1054
+ xaxis: this.createLine(r),
1055
+ yaxis: this.createLine(g),
1056
+ zaxis: this.createLine(b),
1057
+ px: this.createCircle(r, true, "X"),
1058
+ py: this.createCircle(g, true, "Y"),
1059
+ pz: this.createCircle(b, true, "Z")
1070
1060
  };
1071
- this.pipeline = device.createRenderPipeline({
1072
- layout: pipelineLayout,
1073
- vertex: {
1074
- module: shaderModule,
1075
- entryPoint: "vs_main",
1076
- buffers: [vertexBufferLayout]
1077
- },
1078
- fragment: {
1079
- module: shaderModule,
1080
- entryPoint: "fs_main",
1081
- targets: [{ format: this.renderer.format }]
1082
- },
1083
- primitive: {
1084
- topology: "triangle-list",
1085
- cullMode: "none"
1086
- },
1087
- depthStencil: {
1088
- format: this.renderer.depthFormat,
1089
- depthWriteEnabled: false,
1090
- depthCompare: "always"
1061
+ this.bindClickEvents();
1062
+ this.container.appendChild(this.svg);
1063
+ const parent = this.canvas.parentElement;
1064
+ if (parent) {
1065
+ const pos = getComputedStyle(parent).position;
1066
+ if (pos === "static") {
1067
+ parent.style.position = "relative";
1091
1068
  }
1092
- });
1069
+ parent.appendChild(this.container);
1070
+ }
1071
+ }
1072
+ createCircle(color, fill, text) {
1073
+ const ns = this.svg.namespaceURI;
1074
+ const g = document.createElementNS(ns, "g");
1075
+ const circle = document.createElementNS(ns, "circle");
1076
+ circle.setAttribute("fill", fill ? color : "#222");
1077
+ circle.setAttribute("stroke", color);
1078
+ circle.setAttribute("stroke-width", "2");
1079
+ circle.setAttribute("r", "10");
1080
+ circle.setAttribute("cx", "0");
1081
+ circle.setAttribute("cy", "0");
1082
+ circle.setAttribute("pointer-events", "all");
1083
+ g.appendChild(circle);
1084
+ g.setAttribute("cursor", "pointer");
1085
+ if (text) {
1086
+ const t = document.createElementNS(ns, "text");
1087
+ t.setAttribute("font-size", "10");
1088
+ t.setAttribute("font-family", "Arial");
1089
+ t.setAttribute("font-weight", "bold");
1090
+ t.setAttribute("text-anchor", "middle");
1091
+ t.setAttribute("alignment-baseline", "central");
1092
+ t.setAttribute("fill", "#fff");
1093
+ t.setAttribute("pointer-events", "none");
1094
+ t.textContent = text;
1095
+ g.appendChild(t);
1096
+ }
1097
+ this.group.appendChild(g);
1098
+ return g;
1099
+ }
1100
+ createLine(color) {
1101
+ const ns = this.svg.namespaceURI;
1102
+ const line = document.createElementNS(ns, "line");
1103
+ line.setAttribute("stroke", color);
1104
+ line.setAttribute("stroke-width", "2");
1105
+ this.group.appendChild(line);
1106
+ return line;
1107
+ }
1108
+ bindClickEvents() {
1109
+ const bind = (shape, axis, positive) => {
1110
+ const circle = shape.children[0];
1111
+ circle.addEventListener("pointerdown", (e) => {
1112
+ e.stopPropagation();
1113
+ if (this.onAxisClick) {
1114
+ this.onAxisClick(axis, positive);
1115
+ }
1116
+ });
1117
+ };
1118
+ bind(this.shapes.px, "X", true);
1119
+ bind(this.shapes.py, "Y", true);
1120
+ bind(this.shapes.pz, "Z", true);
1121
+ bind(this.shapes.nx, "X", false);
1122
+ bind(this.shapes.ny, "Y", false);
1123
+ bind(this.shapes.nz, "Z", false);
1124
+ }
1125
+ positionContainer() {
1126
+ const parent = this.canvas.parentElement;
1127
+ if (!parent) return;
1128
+ const canvasRect = this.canvas.getBoundingClientRect();
1129
+ const parentRect = parent.getBoundingClientRect();
1130
+ const right = parentRect.right - canvasRect.right + this.margin;
1131
+ const top = canvasRect.top - parentRect.top + this.margin;
1132
+ this.container.style.right = `${right}px`;
1133
+ this.container.style.top = `${top}px`;
1093
1134
  }
1094
- /**
1095
- * 创建 Gizmo 几何体(三个轴 + 箭头)
1096
- */
1097
- createGeometry() {
1098
- const vertices = [];
1099
- const indices = [];
1100
- let vertexOffset = 0;
1101
- const axisLength = 0.8;
1102
- const axisRadius = 0.04;
1103
- const coneLength = 0.25;
1104
- const coneRadius = 0.1;
1105
- const segments = 12;
1106
- for (const axis of this.axes) {
1107
- const [dx, dy, dz] = axis.direction;
1108
- const [r, g, b] = axis.color;
1109
- const cylResult = this.createCylinder(
1110
- [0, 0, 0],
1111
- [dx * axisLength, dy * axisLength, dz * axisLength],
1112
- axisRadius,
1113
- segments,
1114
- [r, g, b],
1115
- vertexOffset
1116
- );
1117
- vertices.push(...cylResult.vertices);
1118
- indices.push(...cylResult.indices);
1119
- vertexOffset += cylResult.vertexCount;
1120
- const coneStart = [
1121
- dx * axisLength,
1122
- dy * axisLength,
1123
- dz * axisLength
1124
- ];
1125
- const coneEnd = [
1126
- dx * (axisLength + coneLength),
1127
- dy * (axisLength + coneLength),
1128
- dz * (axisLength + coneLength)
1129
- ];
1130
- const coneResult = this.createCone(
1131
- coneStart,
1132
- coneEnd,
1133
- coneRadius,
1134
- segments,
1135
- [r, g, b],
1136
- vertexOffset
1137
- );
1138
- vertices.push(...coneResult.vertices);
1139
- indices.push(...coneResult.indices);
1140
- vertexOffset += coneResult.vertexCount;
1141
- const sphereResult = this.createSphere(
1142
- [-dx * 0.15, -dy * 0.15, -dz * 0.15],
1143
- 0.08,
1144
- 8,
1145
- [r * 0.6, g * 0.6, b * 0.6],
1146
- vertexOffset
1147
- );
1148
- vertices.push(...sphereResult.vertices);
1149
- indices.push(...sphereResult.indices);
1150
- vertexOffset += sphereResult.vertexCount;
1151
- }
1152
- const centerResult = this.createSphere(
1153
- [0, 0, 0],
1154
- 0.1,
1155
- 12,
1156
- [0.5, 0.5, 0.5],
1157
- vertexOffset
1158
- );
1159
- vertices.push(...centerResult.vertices);
1160
- indices.push(...centerResult.indices);
1161
- this.vertexCount = vertices.length / 6;
1162
- this.indexCount = indices.length;
1163
- const vertexData = new Float32Array(vertices);
1164
- const indexData = new Uint16Array(indices);
1165
- const device = this.renderer.device;
1166
- this.vertexBuffer = device.createBuffer({
1167
- size: vertexData.byteLength,
1168
- usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
1169
- });
1170
- device.queue.writeBuffer(this.vertexBuffer, 0, vertexData);
1171
- this.indexBuffer = device.createBuffer({
1172
- size: indexData.byteLength,
1173
- usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST
1135
+ setupResizeObserver() {
1136
+ this.resizeObserver = new ResizeObserver(() => {
1137
+ this.positionContainer();
1174
1138
  });
1175
- device.queue.writeBuffer(this.indexBuffer, 0, indexData);
1176
- }
1177
- /**
1178
- * 创建圆柱体几何
1179
- */
1180
- createCylinder(start, end, radius, segments, color, indexOffset) {
1181
- const vertices = [];
1182
- const indices = [];
1183
- const dx = end[0] - start[0];
1184
- const dy = end[1] - start[1];
1185
- const dz = end[2] - start[2];
1186
- const length = Math.sqrt(dx * dx + dy * dy + dz * dz);
1187
- const dir = [dx / length, dy / length, dz / length];
1188
- const up = Math.abs(dir[1]) < 0.99 ? [0, 1, 0] : [1, 0, 0];
1189
- const right = this.cross(
1190
- up,
1191
- dir
1192
- );
1193
- this.normalize(right);
1194
- const actualUp = this.cross(dir, right);
1195
- for (let i = 0; i <= segments; i++) {
1196
- const angle = i / segments * Math.PI * 2;
1197
- const cos = Math.cos(angle);
1198
- const sin = Math.sin(angle);
1199
- const nx0 = right[0] * cos + actualUp[0] * sin;
1200
- const ny0 = right[1] * cos + actualUp[1] * sin;
1201
- const nz0 = right[2] * cos + actualUp[2] * sin;
1202
- vertices.push(
1203
- start[0] + nx0 * radius,
1204
- start[1] + ny0 * radius,
1205
- start[2] + nz0 * radius,
1206
- color[0],
1207
- color[1],
1208
- color[2]
1209
- );
1210
- vertices.push(
1211
- end[0] + nx0 * radius,
1212
- end[1] + ny0 * radius,
1213
- end[2] + nz0 * radius,
1214
- color[0],
1215
- color[1],
1216
- color[2]
1217
- );
1218
- }
1219
- for (let i = 0; i < segments; i++) {
1220
- const i0 = indexOffset + i * 2;
1221
- const i1 = indexOffset + i * 2 + 1;
1222
- const i2 = indexOffset + (i + 1) * 2;
1223
- const i3 = indexOffset + (i + 1) * 2 + 1;
1224
- indices.push(i0, i1, i2, i2, i1, i3);
1225
- }
1226
- return { vertices, indices, vertexCount: (segments + 1) * 2 };
1227
- }
1228
- /**
1229
- * 创建圆锥几何
1230
- */
1231
- createCone(base, tip, radius, segments, color, indexOffset) {
1232
- const vertices = [];
1233
- const indices = [];
1234
- const dx = tip[0] - base[0];
1235
- const dy = tip[1] - base[1];
1236
- const dz = tip[2] - base[2];
1237
- const length = Math.sqrt(dx * dx + dy * dy + dz * dz);
1238
- const dir = [dx / length, dy / length, dz / length];
1239
- const up = Math.abs(dir[1]) < 0.99 ? [0, 1, 0] : [1, 0, 0];
1240
- const right = this.cross(
1241
- up,
1242
- dir
1243
- );
1244
- this.normalize(right);
1245
- const actualUp = this.cross(dir, right);
1246
- vertices.push(tip[0], tip[1], tip[2], color[0], color[1], color[2]);
1247
- for (let i = 0; i <= segments; i++) {
1248
- const angle = i / segments * Math.PI * 2;
1249
- const cos = Math.cos(angle);
1250
- const sin = Math.sin(angle);
1251
- const nx = right[0] * cos + actualUp[0] * sin;
1252
- const ny = right[1] * cos + actualUp[1] * sin;
1253
- const nz = right[2] * cos + actualUp[2] * sin;
1254
- vertices.push(
1255
- base[0] + nx * radius,
1256
- base[1] + ny * radius,
1257
- base[2] + nz * radius,
1258
- color[0],
1259
- color[1],
1260
- color[2]
1261
- );
1262
- }
1263
- for (let i = 0; i < segments; i++) {
1264
- indices.push(indexOffset, indexOffset + i + 1, indexOffset + i + 2);
1265
- }
1266
- const baseCenterIdx = indexOffset + segments + 2;
1267
- vertices.push(
1268
- base[0],
1269
- base[1],
1270
- base[2],
1271
- color[0] * 0.7,
1272
- color[1] * 0.7,
1273
- color[2] * 0.7
1274
- );
1275
- for (let i = 0; i < segments; i++) {
1276
- indices.push(baseCenterIdx, indexOffset + i + 2, indexOffset + i + 1);
1277
- }
1278
- return { vertices, indices, vertexCount: segments + 3 };
1139
+ this.resizeObserver.observe(this.canvas);
1279
1140
  }
1280
1141
  /**
1281
- * 创建球体几何
1142
+ * 每帧更新 SVG 位置(从相机视图矩阵中提取轴投影)
1282
1143
  */
1283
- createSphere(center, radius, segments, color, indexOffset) {
1284
- const vertices = [];
1285
- const indices = [];
1286
- const rings = segments / 2;
1287
- for (let ring = 0; ring <= rings; ring++) {
1288
- const phi = ring / rings * Math.PI;
1289
- const sinPhi = Math.sin(phi);
1290
- const cosPhi = Math.cos(phi);
1291
- for (let seg = 0; seg <= segments; seg++) {
1292
- const theta = seg / segments * Math.PI * 2;
1293
- const x = center[0] + radius * sinPhi * Math.cos(theta);
1294
- const y = center[1] + radius * cosPhi;
1295
- const z = center[2] + radius * sinPhi * Math.sin(theta);
1296
- vertices.push(x, y, z, color[0], color[1], color[2]);
1297
- }
1298
- }
1299
- for (let ring = 0; ring < rings; ring++) {
1300
- for (let seg = 0; seg < segments; seg++) {
1301
- const current = indexOffset + ring * (segments + 1) + seg;
1302
- const next = current + segments + 1;
1303
- indices.push(current, next, current + 1);
1304
- indices.push(current + 1, next, next + 1);
1144
+ render(_pass) {
1145
+ const vm = this.camera.viewMatrix;
1146
+ const vecx = { x: vm[0], y: vm[1], z: vm[2] };
1147
+ const vecy = { x: vm[4], y: vm[5], z: vm[6] };
1148
+ const vecz = { x: vm[8], y: vm[9], z: vm[10] };
1149
+ const s = this.scale;
1150
+ const setTransform = (el, x, y) => {
1151
+ el.setAttribute("transform", `translate(${x * s}, ${y * s})`);
1152
+ };
1153
+ const setLine = (line, x, y) => {
1154
+ line.setAttribute("x2", (x * s).toString());
1155
+ line.setAttribute("y2", (y * s).toString());
1156
+ };
1157
+ setTransform(this.shapes.px, vecx.x, -vecx.y);
1158
+ setTransform(this.shapes.nx, -vecx.x, vecx.y);
1159
+ setTransform(this.shapes.py, vecy.x, -vecy.y);
1160
+ setTransform(this.shapes.ny, -vecy.x, vecy.y);
1161
+ setTransform(this.shapes.pz, vecz.x, -vecz.y);
1162
+ setTransform(this.shapes.nz, -vecz.x, vecz.y);
1163
+ setLine(this.shapes.xaxis, vecx.x, -vecx.y);
1164
+ setLine(this.shapes.yaxis, vecy.x, -vecy.y);
1165
+ setLine(this.shapes.zaxis, vecz.x, -vecz.y);
1166
+ const order = [
1167
+ { keys: ["xaxis", "px"], depth: vecx.z },
1168
+ { keys: ["yaxis", "py"], depth: vecy.z },
1169
+ { keys: ["zaxis", "pz"], depth: vecz.z },
1170
+ { keys: ["nx"], depth: -vecx.z },
1171
+ { keys: ["ny"], depth: -vecy.z },
1172
+ { keys: ["nz"], depth: -vecz.z }
1173
+ ].sort((a, b) => a.depth - b.depth);
1174
+ const fragment = document.createDocumentFragment();
1175
+ for (const item of order) {
1176
+ for (const key of item.keys) {
1177
+ fragment.appendChild(this.shapes[key]);
1305
1178
  }
1306
1179
  }
1307
- return { vertices, indices, vertexCount: (rings + 1) * (segments + 1) };
1308
- }
1309
- /**
1310
- * 创建 uniform buffer
1311
- */
1312
- createUniformBuffer() {
1313
- const device = this.renderer.device;
1314
- this.uniformBuffer = device.createBuffer({
1315
- size: 128,
1316
- usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
1317
- });
1318
- const bindGroupLayout = this.pipeline.getBindGroupLayout(0);
1319
- this.bindGroup = device.createBindGroup({
1320
- layout: bindGroupLayout,
1321
- entries: [{ binding: 0, resource: { buffer: this.uniformBuffer } }]
1322
- });
1323
- }
1324
- /**
1325
- * 设置正交投影矩阵
1326
- */
1327
- setupOrthoProjection() {
1328
- const s = 1.5;
1329
- this.projMatrix[0] = 1 / s;
1330
- this.projMatrix[5] = 1 / s;
1331
- this.projMatrix[10] = -1 / 10;
1332
- this.projMatrix[14] = 0;
1333
- this.projMatrix[15] = 1;
1334
- }
1335
- /**
1336
- * 更新 Gizmo 视图矩阵(从相机提取旋转部分)
1337
- */
1338
- updateViewMatrix() {
1339
- const camView = this.camera.viewMatrix;
1340
- this.viewMatrix[0] = camView[0];
1341
- this.viewMatrix[1] = camView[1];
1342
- this.viewMatrix[2] = camView[2];
1343
- this.viewMatrix[3] = 0;
1344
- this.viewMatrix[4] = camView[4];
1345
- this.viewMatrix[5] = camView[5];
1346
- this.viewMatrix[6] = camView[6];
1347
- this.viewMatrix[7] = 0;
1348
- this.viewMatrix[8] = camView[8];
1349
- this.viewMatrix[9] = camView[9];
1350
- this.viewMatrix[10] = camView[10];
1351
- this.viewMatrix[11] = 0;
1352
- this.viewMatrix[12] = 0;
1353
- this.viewMatrix[13] = 0;
1354
- this.viewMatrix[14] = -3;
1355
- this.viewMatrix[15] = 1;
1356
- }
1357
- /**
1358
- * 渲染 Gizmo
1359
- */
1360
- render(pass) {
1361
- this.updateViewMatrix();
1362
- const dpr = window.devicePixelRatio || 1;
1363
- let gizmoSize = Math.floor(this.size * dpr);
1364
- const marginX = Math.floor(this.margin * dpr);
1365
- const marginY = Math.floor(this.margin * dpr);
1366
- const maxSize = Math.min(
1367
- this.canvas.width - marginX * 2,
1368
- this.canvas.height - marginY * 2
1369
- );
1370
- if (maxSize < 50) {
1371
- return;
1372
- }
1373
- gizmoSize = Math.min(gizmoSize, maxSize);
1374
- const x = Math.max(0, this.canvas.width - gizmoSize - marginX);
1375
- const y = marginY;
1376
- pass.setViewport(x, y, gizmoSize, gizmoSize, 0, 1);
1377
- pass.setScissorRect(x, y, gizmoSize, gizmoSize);
1378
- this.renderer.device.queue.writeBuffer(
1379
- this.uniformBuffer,
1380
- 0,
1381
- new Float32Array(this.viewMatrix)
1382
- );
1383
- this.renderer.device.queue.writeBuffer(
1384
- this.uniformBuffer,
1385
- 64,
1386
- new Float32Array(this.projMatrix)
1387
- );
1388
- pass.setPipeline(this.pipeline);
1389
- pass.setBindGroup(0, this.bindGroup);
1390
- pass.setVertexBuffer(0, this.vertexBuffer);
1391
- pass.setIndexBuffer(this.indexBuffer, "uint16");
1392
- pass.drawIndexed(this.indexCount);
1393
- pass.setViewport(0, 0, this.canvas.width, this.canvas.height, 0, 1);
1394
- pass.setScissorRect(0, 0, this.canvas.width, this.canvas.height);
1180
+ this.group.appendChild(fragment);
1395
1181
  }
1396
1182
  /**
1397
- * 处理点击事件,检测是否点击了某个轴
1183
+ * 点击检测 — SVG 版本通过 DOM 事件处理,此方法仅保留接口兼容
1398
1184
  */
1399
- handleClick(clientX, clientY) {
1400
- const rect = this.canvas.getBoundingClientRect();
1401
- const gizmoSize = this.size;
1402
- const marginX = this.margin;
1403
- const marginY = this.margin;
1404
- const gizmoLeft = rect.right - gizmoSize - marginX;
1405
- const gizmoTop = rect.top + marginY;
1406
- const gizmoRight = gizmoLeft + gizmoSize;
1407
- const gizmoBottom = gizmoTop + gizmoSize;
1408
- if (clientX < gizmoLeft || clientX > gizmoRight || clientY < gizmoTop || clientY > gizmoBottom) {
1409
- return false;
1410
- }
1411
- const relX = (clientX - gizmoLeft) / gizmoSize * 2 - 1;
1412
- const relY = -((clientY - gizmoTop) / gizmoSize * 2 - 1);
1413
- const clickedAxis = this.detectClickedAxis(relX, relY);
1414
- if (clickedAxis && this.onAxisClick) {
1415
- this.onAxisClick(clickedAxis.axis, clickedAxis.positive);
1416
- return true;
1417
- }
1185
+ handleClick(_clientX, _clientY) {
1418
1186
  return false;
1419
1187
  }
1420
- /**
1421
- * 检测点击了哪个轴
1422
- */
1423
- detectClickedAxis(relX, relY) {
1424
- const threshold = 0.4;
1425
- for (const axis of this.axes) {
1426
- const [dx, dy, dz] = axis.direction;
1427
- const posX = this.viewMatrix[0] * dx + this.viewMatrix[4] * dy + this.viewMatrix[8] * dz;
1428
- const posY = this.viewMatrix[1] * dx + this.viewMatrix[5] * dy + this.viewMatrix[9] * dz;
1429
- const distPos = Math.sqrt(
1430
- (relX - posX * 0.5) ** 2 + (relY - posY * 0.5) ** 2
1431
- );
1432
- if (distPos < threshold) {
1433
- return { axis: axis.label, positive: true };
1434
- }
1435
- const distNeg = Math.sqrt(
1436
- (relX + posX * 0.15) ** 2 + (relY + posY * 0.15) ** 2
1437
- );
1438
- if (distNeg < threshold * 0.5) {
1439
- return { axis: axis.label, positive: false };
1440
- }
1441
- }
1442
- return null;
1443
- }
1444
- // 向量工具函数
1445
- cross(a, b) {
1446
- return [
1447
- a[1] * b[2] - a[2] * b[1],
1448
- a[2] * b[0] - a[0] * b[2],
1449
- a[0] * b[1] - a[1] * b[0]
1450
- ];
1451
- }
1452
- normalize(v) {
1453
- const len = Math.sqrt(v[0] ** 2 + v[1] ** 2 + v[2] ** 2);
1454
- if (len > 0) {
1455
- v[0] /= len;
1456
- v[1] /= len;
1457
- v[2] /= len;
1458
- }
1459
- }
1460
- /**
1461
- * 设置 Gizmo 大小
1462
- */
1463
1188
  setSize(size) {
1464
1189
  this.size = size;
1190
+ if (this.container) {
1191
+ this.container.style.width = `${size}px`;
1192
+ this.container.style.height = `${size}px`;
1193
+ this.svg.setAttribute("width", size.toString());
1194
+ this.svg.setAttribute("height", size.toString());
1195
+ this.group.setAttribute(
1196
+ "transform",
1197
+ `translate(${size / 2}, ${size / 2})`
1198
+ );
1199
+ this.positionContainer();
1200
+ }
1465
1201
  }
1466
- /**
1467
- * 设置边距
1468
- */
1469
1202
  setMargin(margin) {
1470
1203
  this.margin = margin;
1204
+ this.positionContainer();
1205
+ }
1206
+ destroy() {
1207
+ var _a2;
1208
+ if (this.resizeObserver) {
1209
+ this.resizeObserver.disconnect();
1210
+ }
1211
+ (_a2 = this.container) == null ? void 0 : _a2.remove();
1471
1212
  }
1472
1213
  }
1473
1214
  class BoundingBoxRenderer {
@@ -8436,6 +8177,65 @@ function compressSplatsToTextures(device, data) {
8436
8177
  { bytesPerRow: width * 4 },
8437
8178
  { width, height }
8438
8179
  );
8180
+ let shBasis0Texture = null;
8181
+ let shBasis1Texture = null;
8182
+ let shBasis2Texture = null;
8183
+ const hasSH = !!(data.shCoeffs && data.shCoeffs.length >= count * 45);
8184
+ if (hasSH) {
8185
+ const shCoeffs = data.shCoeffs;
8186
+ const sh0Data = new Float32Array(totalPixels * 4);
8187
+ const sh1Data = new Float32Array(totalPixels * 4);
8188
+ const sh2Data = new Float32Array(totalPixels * 4);
8189
+ for (let i = 0; i < count; i++) {
8190
+ const base = i * 45;
8191
+ const px = i * 4;
8192
+ sh0Data[px + 0] = shCoeffs[base + 0];
8193
+ sh0Data[px + 1] = shCoeffs[base + 1];
8194
+ sh0Data[px + 2] = shCoeffs[base + 2];
8195
+ sh1Data[px + 0] = shCoeffs[base + 3];
8196
+ sh1Data[px + 1] = shCoeffs[base + 4];
8197
+ sh1Data[px + 2] = shCoeffs[base + 5];
8198
+ sh2Data[px + 0] = shCoeffs[base + 6];
8199
+ sh2Data[px + 1] = shCoeffs[base + 7];
8200
+ sh2Data[px + 2] = shCoeffs[base + 8];
8201
+ }
8202
+ shBasis0Texture = device.createTexture({
8203
+ size: { width, height },
8204
+ format: "rgba32float",
8205
+ usage: textureUsage,
8206
+ label: "sh-basis0"
8207
+ });
8208
+ shBasis1Texture = device.createTexture({
8209
+ size: { width, height },
8210
+ format: "rgba32float",
8211
+ usage: textureUsage,
8212
+ label: "sh-basis1"
8213
+ });
8214
+ shBasis2Texture = device.createTexture({
8215
+ size: { width, height },
8216
+ format: "rgba32float",
8217
+ usage: textureUsage,
8218
+ label: "sh-basis2"
8219
+ });
8220
+ device.queue.writeTexture(
8221
+ { texture: shBasis0Texture },
8222
+ sh0Data,
8223
+ { bytesPerRow: width * 16 },
8224
+ { width, height }
8225
+ );
8226
+ device.queue.writeTexture(
8227
+ { texture: shBasis1Texture },
8228
+ sh1Data,
8229
+ { bytesPerRow: width * 16 },
8230
+ { width, height }
8231
+ );
8232
+ device.queue.writeTexture(
8233
+ { texture: shBasis2Texture },
8234
+ sh2Data,
8235
+ { bytesPerRow: width * 16 },
8236
+ { width, height }
8237
+ );
8238
+ }
8439
8239
  return {
8440
8240
  width,
8441
8241
  height,
@@ -8444,6 +8244,10 @@ function compressSplatsToTextures(device, data) {
8444
8244
  scaleRotTexture1,
8445
8245
  scaleRotTexture2,
8446
8246
  colorTexture,
8247
+ shBasis0Texture,
8248
+ shBasis1Texture,
8249
+ shBasis2Texture,
8250
+ hasSH,
8447
8251
  boundingBox
8448
8252
  };
8449
8253
  }
@@ -8452,9 +8256,12 @@ function destroyCompressedTextures(textures) {
8452
8256
  textures.scaleRotTexture1.destroy();
8453
8257
  textures.scaleRotTexture2.destroy();
8454
8258
  textures.colorTexture.destroy();
8259
+ if (textures.shBasis0Texture) textures.shBasis0Texture.destroy();
8260
+ if (textures.shBasis1Texture) textures.shBasis1Texture.destroy();
8261
+ if (textures.shBasis2Texture) textures.shBasis2Texture.destroy();
8455
8262
  }
8456
8263
  const DEFAULT_NUM_BUCKETS = 65536;
8457
- const IOS_NUM_BUCKETS = 4096;
8264
+ const IOS_NUM_BUCKETS = 16384;
8458
8265
  const WORKGROUP_SIZE = 256;
8459
8266
  function isIOSDevice() {
8460
8267
  if (typeof navigator === "undefined") return false;
@@ -8958,9 +8765,17 @@ class GSSplatSorterMobile {
8958
8765
  this.drawIndirectBuffer.destroy();
8959
8766
  }
8960
8767
  }
8961
- const shaderCodeMobileL0 = (
8768
+ const shaderCodeMobile = (
8962
8769
  /* wgsl */
8963
8770
  `
8771
+
8772
+ const SH_C1: f32 = 0.4886025119029199;
8773
+ const GAUSSIAN_K: f32 = 4.0;
8774
+ const EXP_NEG_K: f32 = 0.01831563888873418;
8775
+ const INV_ONE_MINUS_EXP_NEG_K: f32 = 1.01865736036377408;
8776
+ const ALPHA_CULL_THRESHOLD: f32 = 0.00392156863;
8777
+ const LOW_PASS_FILTER: f32 = 0.3;
8778
+
8964
8779
  struct Uniforms {
8965
8780
  view: mat4x4<f32>,
8966
8781
  proj: mat4x4<f32>,
@@ -8969,18 +8784,21 @@ struct Uniforms {
8969
8784
  _pad: f32,
8970
8785
  screenSize: vec2<f32>,
8971
8786
  _pad2: vec2<f32>,
8972
- textureSize: vec2<f32>, // 纹理尺寸 (用于坐标计算)
8973
- _pad3: vec2<f32>,
8787
+ textureSize: vec2<f32>,
8788
+ shEnabled: f32,
8789
+ _pad3: f32,
8974
8790
  }
8975
8791
 
8976
8792
  @group(0) @binding(0) var<uniform> uniforms: Uniforms;
8977
8793
  @group(0) @binding(1) var<storage, read> sortedIndices: array<u32>;
8978
8794
 
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
8795
+ @group(1) @binding(0) var positionTex: texture_2d<f32>;
8796
+ @group(1) @binding(1) var scaleRotTex1: texture_2d<f32>;
8797
+ @group(1) @binding(2) var scaleRotTex2: texture_2d<f32>;
8798
+ @group(1) @binding(3) var colorTex: texture_2d<f32>;
8799
+ @group(1) @binding(4) var shBasis0Tex: texture_2d<f32>;
8800
+ @group(1) @binding(5) var shBasis1Tex: texture_2d<f32>;
8801
+ @group(1) @binding(6) var shBasis2Tex: texture_2d<f32>;
8984
8802
 
8985
8803
  struct VertexOutput {
8986
8804
  @builtin(position) position: vec4<f32>,
@@ -8998,7 +8816,6 @@ const QUAD_POSITIONS = array<vec2<f32>, 4>(
8998
8816
 
8999
8817
  const ELLIPSE_SCALE: f32 = 3.0;
9000
8818
 
9001
- // 将索引转换为纹理坐标
9002
8819
  fn indexToTexCoord(index: u32) -> vec2<u32> {
9003
8820
  let texWidth = u32(uniforms.textureSize.x);
9004
8821
  let x = index % texWidth;
@@ -9006,7 +8823,6 @@ fn indexToTexCoord(index: u32) -> vec2<u32> {
9006
8823
  return vec2<u32>(x, y);
9007
8824
  }
9008
8825
 
9009
- // 四元数转旋转矩阵
9010
8826
  fn quatToMat3(q: vec4<f32>) -> mat3x3<f32> {
9011
8827
  let w = q[0]; let x = q[1]; let y = q[2]; let z = q[3];
9012
8828
  let x2 = x + x; let y2 = y + y; let z2 = z + z;
@@ -9020,15 +8836,12 @@ fn quatToMat3(q: vec4<f32>) -> mat3x3<f32> {
9020
8836
  );
9021
8837
  }
9022
8838
 
9023
- // 从模型矩阵提取统一缩放因子(取 X 轴向量长度)
9024
8839
  fn getModelScale(model: mat4x4<f32>) -> f32 {
9025
8840
  return length(model[0].xyz);
9026
8841
  }
9027
8842
 
9028
- // 计算 2D 协方差
9029
8843
  fn computeCov2D(mean: vec3<f32>, scale: vec3<f32>, rotation: vec4<f32>, modelView: mat4x4<f32>, proj: mat4x4<f32>, modelScale: f32) -> vec3<f32> {
9030
8844
  let R = quatToMat3(rotation);
9031
- // 应用模型缩放到 splat scale
9032
8845
  let scaledScale = scale * modelScale;
9033
8846
  let s2 = scaledScale * scaledScale;
9034
8847
  let M = mat3x3<f32>(R[0] * s2.x, R[1] * s2.y, R[2] * s2.z);
@@ -9040,8 +8853,6 @@ fn computeCov2D(mean: vec3<f32>, scale: vec3<f32>, rotation: vec4<f32>, modelVie
9040
8853
  let z = -viewPos.z;
9041
8854
  let z_clamped = max(z, 0.001);
9042
8855
  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
8856
  let j1 = vec3<f32>(fx / z_clamped, 0.0, fx * viewPos.x / z2);
9046
8857
  let j2 = vec3<f32>(0.0, fy / z_clamped, fy * viewPos.y / z2);
9047
8858
  let Sj1 = SigmaView * j1;
@@ -9049,17 +8860,22 @@ fn computeCov2D(mean: vec3<f32>, scale: vec3<f32>, rotation: vec4<f32>, modelVie
9049
8860
  return vec3<f32>(dot(j1, Sj1), dot(j1, Sj2), dot(j2, Sj2));
9050
8861
  }
9051
8862
 
9052
- // 计算椭圆轴
9053
8863
  fn computeEllipseAxes(cov2D: vec3<f32>) -> mat2x2<f32> {
9054
- let a = cov2D.x; let b = cov2D.y; let c = cov2D.z;
8864
+ var cov = cov2D;
8865
+ cov.x += LOW_PASS_FILTER;
8866
+ cov.z += LOW_PASS_FILTER;
8867
+ let a = cov.x; let b = cov.y; let c = cov.z;
9055
8868
  let trace = a + c;
9056
8869
  let det = a * c - b * b;
9057
8870
  let disc = trace * trace - 4.0 * det;
9058
8871
  let sqrtDisc = sqrt(max(disc, 0.0));
9059
8872
  let lambda1 = max((trace + sqrtDisc) * 0.5, 0.0);
9060
8873
  let lambda2 = max((trace - sqrtDisc) * 0.5, 0.0);
9061
- let r1 = sqrt(lambda1);
9062
- let r2 = sqrt(lambda2);
8874
+ if (lambda2 <= 0.0) {
8875
+ return mat2x2<f32>(vec2<f32>(0.0), vec2<f32>(0.0));
8876
+ }
8877
+ let r1 = min(2.0 * sqrt(2.0 * lambda1), 1024.0);
8878
+ let r2 = min(2.0 * sqrt(2.0 * lambda2), 1024.0);
9063
8879
  var axis1: vec2<f32>; var axis2: vec2<f32>;
9064
8880
  if (abs(b) > 1e-6) {
9065
8881
  axis1 = normalize(vec2<f32>(b, lambda1 - a));
@@ -9075,59 +8891,88 @@ fn computeEllipseAxes(cov2D: vec3<f32>) -> mat2x2<f32> {
9075
8891
  fn vs_main(@builtin(vertex_index) vertexIndex: u32, @builtin(instance_index) instanceIndex: u32) -> VertexOutput {
9076
8892
  var output: VertexOutput;
9077
8893
 
9078
- // 获取排序后的索引
9079
8894
  let splatIndex = sortedIndices[instanceIndex];
9080
8895
  let texCoord = indexToTexCoord(splatIndex);
9081
8896
 
9082
- // 从纹理采样位置数据(RGBA32Float,直接读取)
9083
8897
  let posSample = textureLoad(positionTex, texCoord, 0);
9084
8898
  let mean = posSample.xyz;
9085
8899
 
9086
- // 从纹理采样缩放和旋转(RGBA16Float,GPU 自动转换为 f32)
9087
8900
  let scaleRot1 = textureLoad(scaleRotTex1, texCoord, 0);
9088
8901
  let scaleRot2 = textureLoad(scaleRotTex2, texCoord, 0);
9089
-
9090
8902
  let scale = scaleRot1.xyz;
9091
8903
  let rotation = vec4<f32>(scaleRot1.w, scaleRot2.x, scaleRot2.y, scaleRot2.z);
9092
8904
 
9093
- // 从纹理采样颜色(RGBA8Unorm,GPU 自动归一化到 0-1)
9094
8905
  let colorSample = textureLoad(colorTex, texCoord, 0);
9095
- let color = colorSample.rgb;
8906
+ var color = colorSample.rgb;
9096
8907
  let opacity = colorSample.a;
8908
+
8909
+ if (opacity < ALPHA_CULL_THRESHOLD) {
8910
+ output.position = vec4<f32>(0.0, 0.0, 2.0, 1.0);
8911
+ return output;
8912
+ }
9097
8913
 
9098
- // 计算顶点位置
9099
8914
  let quadPos = QUAD_POSITIONS[vertexIndex];
9100
8915
  output.localUV = quadPos;
9101
8916
 
9102
- // 计算 modelView 矩阵和模型缩放
9103
8917
  let modelView = uniforms.view * uniforms.model;
9104
8918
  let modelScale = getModelScale(uniforms.model);
9105
8919
 
9106
8920
  let cov2D = computeCov2D(mean, scale, rotation, modelView, uniforms.proj, modelScale);
9107
8921
  let axes = computeEllipseAxes(cov2D);
9108
- let screenOffset = axes[0] * quadPos.x * ELLIPSE_SCALE + axes[1] * quadPos.y * ELLIPSE_SCALE;
8922
+
8923
+ if (axes[0].x == 0.0 && axes[0].y == 0.0 && axes[1].x == 0.0 && axes[1].y == 0.0) {
8924
+ output.position = vec4<f32>(0.0, 0.0, 2.0, 1.0);
8925
+ return output;
8926
+ }
8927
+
8928
+ let basisViewport = vec2<f32>(1.0 / uniforms.screenSize.x, 1.0 / uniforms.screenSize.y);
8929
+ let ndcOffset = (quadPos.x * axes[0] + quadPos.y * axes[1]) * basisViewport * 2.0;
9109
8930
 
9110
- // 应用 model 变换到 splat 位置
9111
8931
  let worldPos = uniforms.model * vec4<f32>(mean, 1.0);
9112
8932
  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;
8933
+ let clipPos = uniforms.proj * viewPos;
8934
+ let pW = 1.0 / (clipPos.w + 0.0000001);
8935
+ let ndcPos = clipPos * pW;
8936
+
8937
+ if (abs(ndcPos.x) > 1.3 || abs(ndcPos.y) > 1.3 || ndcPos.z < -0.2 || ndcPos.z > 1.0) {
8938
+ output.position = vec4<f32>(0.0, 0.0, 2.0, 1.0);
8939
+ return output;
8940
+ }
8941
+
8942
+ output.position = vec4<f32>(ndcPos.xy + ndcOffset, ndcPos.z, 1.0);
8943
+
8944
+ // L1 SH evaluation
8945
+ if (uniforms.shEnabled > 0.5) {
8946
+ let sh_b0 = textureLoad(shBasis0Tex, texCoord, 0).xyz;
8947
+ let sh_b1 = textureLoad(shBasis1Tex, texCoord, 0).xyz;
8948
+ let sh_b2 = textureLoad(shBasis2Tex, texCoord, 0).xyz;
8949
+
8950
+ let viewDir = worldPos.xyz - uniforms.cameraPos;
8951
+ let shDir = normalize(vec3<f32>(
8952
+ dot(viewDir, uniforms.model[0].xyz),
8953
+ dot(viewDir, uniforms.model[1].xyz),
8954
+ dot(viewDir, uniforms.model[2].xyz)
8955
+ ));
8956
+
8957
+ color += (-SH_C1 * shDir.y) * sh_b0
8958
+ + ( SH_C1 * shDir.z) * sh_b1
8959
+ + (-SH_C1 * shDir.x) * sh_b2;
8960
+ }
8961
+
9117
8962
  output.color = color;
9118
8963
  output.opacity = opacity;
9119
-
9120
8964
  return output;
9121
8965
  }
9122
8966
 
9123
8967
  @fragment
9124
8968
  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));
8969
+ if (input.opacity <= 0.0) { discard; }
8970
+ let A = dot(input.localUV, input.localUV);
8971
+ if (A > 1.0) { discard; }
8972
+ let weight = (exp(-GAUSSIAN_K * A) - EXP_NEG_K) * INV_ONE_MINUS_EXP_NEG_K;
8973
+ let alpha = input.opacity * weight;
8974
+ if (alpha < ALPHA_CULL_THRESHOLD) { discard; }
8975
+ let color = max(input.color, vec3<f32>(0.0));
9131
8976
  return vec4<f32>(color * alpha, alpha);
9132
8977
  }
9133
8978
  `
@@ -9157,6 +9002,10 @@ class GSSplatRendererMobile {
9157
9002
  // 帧计数(用于排序频率控制)
9158
9003
  __publicField(this, "frameCount", 0);
9159
9004
  __publicField(this, "sortEveryNFrames", 1);
9005
+ // 相机静止检测:跳过不必要的排序
9006
+ __publicField(this, "lastSortViewMatrix", new Float32Array(16));
9007
+ __publicField(this, "lastSortProjMatrix", new Float32Array(16));
9008
+ __publicField(this, "sortStateInitialized", false);
9160
9009
  // ============================================
9161
9010
  // 变换相关 (position, rotation, scale)
9162
9011
  // ============================================
@@ -9167,6 +9016,8 @@ class GSSplatRendererMobile {
9167
9016
  __publicField(this, "pivot", [0, 0, 0]);
9168
9017
  // 旋转/缩放中心点
9169
9018
  __publicField(this, "modelMatrix", new Float32Array(16));
9019
+ // 1x1 dummy SH 纹理(当无 SH 数据时使用)
9020
+ __publicField(this, "dummySHTexture", null);
9170
9021
  this.renderer = renderer;
9171
9022
  this.camera = camera;
9172
9023
  this.createPipeline();
@@ -9288,7 +9139,7 @@ class GSSplatRendererMobile {
9288
9139
  createPipeline() {
9289
9140
  const device = this.renderer.device;
9290
9141
  const shaderModule = device.createShaderModule({
9291
- code: shaderCodeMobileL0,
9142
+ code: shaderCodeMobile,
9292
9143
  label: "mobile-splat-shader"
9293
9144
  });
9294
9145
  this.uniformBindGroupLayout = device.createBindGroupLayout({
@@ -9305,32 +9156,27 @@ class GSSplatRendererMobile {
9305
9156
  }
9306
9157
  ]
9307
9158
  });
9159
+ const texEntry = (binding) => ({
9160
+ binding,
9161
+ visibility: GPUShaderStage.VERTEX,
9162
+ texture: { sampleType: "unfilterable-float" }
9163
+ });
9308
9164
  this.textureBindGroupLayout = device.createBindGroupLayout({
9309
9165
  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
- }
9166
+ texEntry(0),
9167
+ // positionTex
9168
+ texEntry(1),
9169
+ // scaleRotTex1
9170
+ texEntry(2),
9171
+ // scaleRotTex2
9172
+ texEntry(3),
9173
+ // colorTex
9174
+ texEntry(4),
9175
+ // shBasis0Tex
9176
+ texEntry(5),
9177
+ // shBasis1Tex
9178
+ texEntry(6)
9179
+ // shBasis2Tex
9334
9180
  ]
9335
9181
  });
9336
9182
  const pipelineLayout = device.createPipelineLayout({
@@ -9374,10 +9220,16 @@ class GSSplatRendererMobile {
9374
9220
  depthCompare: "always"
9375
9221
  }
9376
9222
  });
9223
+ this.dummySHTexture = device.createTexture({
9224
+ size: { width: 1, height: 1 },
9225
+ format: "rgba32float",
9226
+ usage: GPUTextureUsage.TEXTURE_BINDING,
9227
+ label: "dummy-sh"
9228
+ });
9377
9229
  }
9378
9230
  /**
9379
9231
  * 创建 uniform buffer
9380
- * 布局: view (64) + proj (64) + model (64) + cameraPos (12) + pad (4) + screenSize (8) + pad (8) + textureSize (8) + pad (8) = 240 bytes
9232
+ * 布局: view(64) + proj(64) + model(64) + cameraPos(12)+pad(4) + screenSize(8)+pad(8) + textureSize(8)+shEnabled(4)+pad(4) = 240 bytes
9381
9233
  */
9382
9234
  createUniformBuffer() {
9383
9235
  this.uniformBuffer = this.renderer.device.createBuffer({
@@ -9438,35 +9290,22 @@ class GSSplatRendererMobile {
9438
9290
  this.uniformBindGroup = device.createBindGroup({
9439
9291
  layout: this.uniformBindGroupLayout,
9440
9292
  entries: [
9441
- {
9442
- binding: 0,
9443
- resource: { buffer: this.uniformBuffer }
9444
- },
9445
- {
9446
- binding: 1,
9447
- resource: { buffer: this.sorter.getIndicesBuffer() }
9448
- }
9293
+ { binding: 0, resource: { buffer: this.uniformBuffer } },
9294
+ { binding: 1, resource: { buffer: this.sorter.getIndicesBuffer() } }
9449
9295
  ]
9450
9296
  });
9297
+ const dummyView = this.dummySHTexture.createView();
9298
+ const tex = this.compressedTextures;
9451
9299
  this.textureBindGroup = device.createBindGroup({
9452
9300
  layout: this.textureBindGroupLayout,
9453
9301
  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
- }
9302
+ { binding: 0, resource: tex.positionTexture.createView() },
9303
+ { binding: 1, resource: tex.scaleRotTexture1.createView() },
9304
+ { binding: 2, resource: tex.scaleRotTexture2.createView() },
9305
+ { binding: 3, resource: tex.colorTexture.createView() },
9306
+ { binding: 4, resource: tex.shBasis0Texture ? tex.shBasis0Texture.createView() : dummyView },
9307
+ { binding: 5, resource: tex.shBasis1Texture ? tex.shBasis1Texture.createView() : dummyView },
9308
+ { binding: 6, resource: tex.shBasis2Texture ? tex.shBasis2Texture.createView() : dummyView }
9470
9309
  ]
9471
9310
  });
9472
9311
  }
@@ -9521,10 +9360,11 @@ class GSSplatRendererMobile {
9521
9360
  208,
9522
9361
  new Float32Array([this.renderer.width, this.renderer.height, 0, 0])
9523
9362
  );
9363
+ const shEnabled = this.compressedTextures.hasSH ? 1 : 0;
9524
9364
  device.queue.writeBuffer(
9525
9365
  this.uniformBuffer,
9526
9366
  224,
9527
- new Float32Array([this.compressedTextures.width, this.compressedTextures.height, 0, 0])
9367
+ new Float32Array([this.compressedTextures.width, this.compressedTextures.height, shEnabled, 0])
9528
9368
  );
9529
9369
  this.sorter.setScreenSize(this.renderer.width, this.renderer.height);
9530
9370
  this.sorter.setCullingOptions({
@@ -9533,7 +9373,8 @@ class GSSplatRendererMobile {
9533
9373
  pixelThreshold: 1
9534
9374
  });
9535
9375
  const isFirstFrame = this.frameCount === 1;
9536
- const shouldSort = isFirstFrame || this.frameCount % this.sortEveryNFrames === 0;
9376
+ const cameraChanged = this.hasCameraChanged();
9377
+ const shouldSort = isFirstFrame || cameraChanged && this.frameCount % this.sortEveryNFrames === 0;
9537
9378
  if (shouldSort) {
9538
9379
  this.sorter.sort();
9539
9380
  }
@@ -9564,36 +9405,42 @@ class GSSplatRendererMobile {
9564
9405
  setSortFrequency(n) {
9565
9406
  this.sortEveryNFrames = Math.max(1, n);
9566
9407
  }
9408
+ hasCameraChanged() {
9409
+ const view = this.camera.viewMatrix;
9410
+ const proj = this.camera.projectionMatrix;
9411
+ if (!this.sortStateInitialized) {
9412
+ this.lastSortViewMatrix.set(view);
9413
+ this.lastSortProjMatrix.set(proj);
9414
+ this.sortStateInitialized = true;
9415
+ return true;
9416
+ }
9417
+ for (let i = 0; i < 16; i++) {
9418
+ if (Math.abs(view[i] - this.lastSortViewMatrix[i]) > 1e-6 || Math.abs(proj[i] - this.lastSortProjMatrix[i]) > 1e-6) {
9419
+ this.lastSortViewMatrix.set(view);
9420
+ this.lastSortProjMatrix.set(proj);
9421
+ return true;
9422
+ }
9423
+ }
9424
+ return false;
9425
+ }
9567
9426
  // ============================================
9568
9427
  // IGSSplatRenderer 接口实现 - SH 模式
9569
9428
  // ============================================
9570
- /**
9571
- * 设置 SH 模式(移动端仅支持 L0)
9572
- */
9573
- setSHMode(mode) {
9429
+ setSHMode(_mode) {
9574
9430
  }
9575
- /**
9576
- * 获取当前 SH 模式
9577
- */
9578
9431
  getSHMode() {
9579
- return SHMode.L0;
9432
+ var _a2;
9433
+ return ((_a2 = this.compressedTextures) == null ? void 0 : _a2.hasSH) ? SHMode.L1 : SHMode.L0;
9580
9434
  }
9581
- /**
9582
- * 是否支持指定的 SH 模式
9583
- */
9584
9435
  supportsSHMode(mode) {
9585
- return mode === SHMode.L0;
9436
+ return mode === SHMode.L0 || mode === SHMode.L1;
9586
9437
  }
9587
- /**
9588
- * 获取渲染器能力
9589
- */
9590
9438
  getCapabilities() {
9591
9439
  return {
9592
- maxSHMode: SHMode.L0,
9440
+ maxSHMode: SHMode.L1,
9593
9441
  supportsRawData: false,
9594
9442
  isMobileOptimized: true,
9595
9443
  maxSplatCount: 0
9596
- // 无限制(受 GPU 内存限制)
9597
9444
  };
9598
9445
  }
9599
9446
  /**
@@ -9622,6 +9469,10 @@ class GSSplatRendererMobile {
9622
9469
  */
9623
9470
  destroy() {
9624
9471
  this.destroyInternal();
9472
+ if (this.dummySHTexture) {
9473
+ this.dummySHTexture.destroy();
9474
+ this.dummySHTexture = null;
9475
+ }
9625
9476
  }
9626
9477
  }
9627
9478
  class SceneManager {
@@ -13439,6 +13290,7 @@ class GizmoManager {
13439
13290
  this.canvas.removeEventListener("pointermove", this.boundOnPointerMove);
13440
13291
  this.canvas.removeEventListener("pointerdown", this.boundOnPointerDown);
13441
13292
  this.canvas.removeEventListener("pointerup", this.boundOnPointerUp);
13293
+ this.viewportGizmo.destroy();
13442
13294
  this.transformGizmo.destroy();
13443
13295
  this.boundingBoxRenderer.destroy();
13444
13296
  }
@@ -16900,6 +16752,63 @@ class App {
16900
16752
  getSceneAidsRenderer() {
16901
16753
  return this.sceneAids;
16902
16754
  }
16755
+ // ============================================
16756
+ // 渲染性能控制
16757
+ // ============================================
16758
+ /**
16759
+ * 设置渲染缩放比例(影响内部分辨率)
16760
+ * 0.5 = 半分辨率(性能提升约 4 倍),1.0 = 正常
16761
+ * 适用于移动端提质或桌面端降负载
16762
+ */
16763
+ setRenderScale(scale) {
16764
+ this.renderer.setRenderScale(scale);
16765
+ }
16766
+ getRenderScale() {
16767
+ return this.renderer.getRenderScale();
16768
+ }
16769
+ /**
16770
+ * 覆盖自动 DPR,传 -1 恢复自动推荐
16771
+ */
16772
+ setDPR(dpr) {
16773
+ this.renderer.setDPR(dpr);
16774
+ }
16775
+ getDPR() {
16776
+ return this.renderer.getDPR();
16777
+ }
16778
+ /**
16779
+ * 设置桌面端亚像素剔除阈值(默认 1.0)
16780
+ * 值越大剔除越激进,近距离性能越好,但远处细节可能丢失
16781
+ */
16782
+ setPixelCullThreshold(threshold) {
16783
+ const gsRenderer = this.getGSRenderer();
16784
+ if (gsRenderer) {
16785
+ gsRenderer.setPixelCullThreshold(threshold);
16786
+ }
16787
+ }
16788
+ /**
16789
+ * 设置桌面端最大可见 splat 数(0 = 不限制)
16790
+ * 限制绘制数量是应对近距离卡顿最直接的手段
16791
+ */
16792
+ setMaxVisibleSplats(count) {
16793
+ const gsRenderer = this.getGSRenderer();
16794
+ if (gsRenderer) {
16795
+ gsRenderer.setMaxVisibleSplats(count);
16796
+ }
16797
+ }
16798
+ /**
16799
+ * 设置排序频率(1 = 每帧,2 = 每两帧,以此类推)
16800
+ * 降低排序频率可提升帧率,代价是移动时短暂排序瑕疵
16801
+ */
16802
+ setSortFrequency(frequency) {
16803
+ const gsRenderer = this.getGSRenderer();
16804
+ if (gsRenderer) {
16805
+ gsRenderer.setSortFrequency(frequency);
16806
+ }
16807
+ const mobileRenderer = this.getGSRendererMobile();
16808
+ if (mobileRenderer) {
16809
+ mobileRenderer.setSortFrequency(frequency);
16810
+ }
16811
+ }
16903
16812
  /**
16904
16813
  * 销毁应用及所有资源
16905
16814
  */