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