@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 +502 -593
- package/dist/3dgs-lib.cjs.map +1 -1
- package/dist/3dgs-lib.js +502 -593
- package/dist/3dgs-lib.js.map +1 -1
- package/dist/App.d.ts +27 -0
- package/dist/core/Renderer.d.ts +19 -0
- package/dist/core/gizmo/ViewportGizmo.d.ts +20 -65
- package/dist/gs/GSSplatRendererMobile.d.ts +7 -14
- package/dist/gs/TextureCompressor.d.ts +4 -0
- package/package.json +1 -1
package/dist/3dgs-lib.cjs
CHANGED
|
@@ -39,7 +39,7 @@ function isMobileDevice() {
|
|
|
39
39
|
}
|
|
40
40
|
function getRecommendedDPR() {
|
|
41
41
|
const isMobile = isMobileDevice();
|
|
42
|
-
const maxDpr = isMobile ?
|
|
42
|
+
const maxDpr = isMobile ? 2 : 2;
|
|
43
43
|
return Math.min(window.devicePixelRatio || 1, maxDpr);
|
|
44
44
|
}
|
|
45
45
|
function isWebGPUSupported() {
|
|
@@ -254,6 +254,13 @@ class Renderer {
|
|
|
254
254
|
__publicField(this, "renderPassEncoder");
|
|
255
255
|
// ResizeObserver 引用(用于清理)
|
|
256
256
|
__publicField(this, "resizeObserver", null);
|
|
257
|
+
// 渲染缩放:0.5 = 半分辨率,1.0 = 正常,用于性能/质量权衡
|
|
258
|
+
__publicField(this, "_renderScale", 1);
|
|
259
|
+
// 自定义 DPR 覆盖:-1 表示使用自动推荐值
|
|
260
|
+
__publicField(this, "_customDPR", -1);
|
|
261
|
+
// 缓存最后一次的 CSS 尺寸,用于 renderScale 变更时重算
|
|
262
|
+
__publicField(this, "_lastCSSWidth", 0);
|
|
263
|
+
__publicField(this, "_lastCSSHeight", 0);
|
|
257
264
|
// 背景颜色
|
|
258
265
|
__publicField(this, "_clearColor", { r: 0.15, g: 0.15, b: 0.15, a: 1 });
|
|
259
266
|
this.canvas = canvas;
|
|
@@ -364,6 +371,45 @@ class Renderer {
|
|
|
364
371
|
});
|
|
365
372
|
this._depthTextureView = this._depthTexture.createView();
|
|
366
373
|
}
|
|
374
|
+
getEffectiveDPR() {
|
|
375
|
+
const baseDPR = this._customDPR > 0 ? this._customDPR : getRecommendedDPR();
|
|
376
|
+
return baseDPR * this._renderScale;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* 设置渲染缩放比例,用于性能/质量权衡
|
|
380
|
+
* 0.5 = 半分辨率(性能提升约 4 倍),1.0 = 正常分辨率
|
|
381
|
+
* 内部等效于降低 DPR,不影响 CSS 布局尺寸
|
|
382
|
+
*/
|
|
383
|
+
setRenderScale(scale) {
|
|
384
|
+
this._renderScale = Math.max(0.25, Math.min(2, scale));
|
|
385
|
+
if (this._lastCSSWidth > 0 && this._lastCSSHeight > 0) {
|
|
386
|
+
this.applySize(this._lastCSSWidth, this._lastCSSHeight);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
getRenderScale() {
|
|
390
|
+
return this._renderScale;
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* 覆盖自动 DPR 推荐值
|
|
394
|
+
* 传入 -1 恢复自动模式
|
|
395
|
+
*/
|
|
396
|
+
setDPR(dpr) {
|
|
397
|
+
this._customDPR = dpr;
|
|
398
|
+
if (this._lastCSSWidth > 0 && this._lastCSSHeight > 0) {
|
|
399
|
+
this.applySize(this._lastCSSWidth, this._lastCSSHeight);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
getDPR() {
|
|
403
|
+
return this._customDPR > 0 ? this._customDPR : getRecommendedDPR();
|
|
404
|
+
}
|
|
405
|
+
applySize(cssWidth, cssHeight) {
|
|
406
|
+
this._lastCSSWidth = cssWidth;
|
|
407
|
+
this._lastCSSHeight = cssHeight;
|
|
408
|
+
const dpr = this.getEffectiveDPR();
|
|
409
|
+
this.canvas.width = Math.max(1, Math.floor(cssWidth * dpr));
|
|
410
|
+
this.canvas.height = Math.max(1, Math.floor(cssHeight * dpr));
|
|
411
|
+
this.createDepthTexture();
|
|
412
|
+
}
|
|
367
413
|
/**
|
|
368
414
|
* 设置 resize 监听
|
|
369
415
|
*/
|
|
@@ -371,10 +417,7 @@ class Renderer {
|
|
|
371
417
|
this.resizeObserver = new ResizeObserver((entries) => {
|
|
372
418
|
for (const entry of entries) {
|
|
373
419
|
const { width, height } = entry.contentRect;
|
|
374
|
-
|
|
375
|
-
this.canvas.width = Math.floor(width * dpr);
|
|
376
|
-
this.canvas.height = Math.floor(height * dpr);
|
|
377
|
-
this.createDepthTexture();
|
|
420
|
+
this.applySize(width, height);
|
|
378
421
|
}
|
|
379
422
|
});
|
|
380
423
|
this.resizeObserver.observe(this.canvas);
|
|
@@ -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(
|
|
1000
|
-
__publicField(this, "renderer");
|
|
1007
|
+
constructor(_renderer, camera, canvas) {
|
|
1001
1008
|
__publicField(this, "camera");
|
|
1002
1009
|
__publicField(this, "canvas");
|
|
1003
|
-
|
|
1004
|
-
__publicField(this, "
|
|
1005
|
-
__publicField(this, "
|
|
1006
|
-
__publicField(this, "
|
|
1007
|
-
__publicField(this, "
|
|
1008
|
-
__publicField(this, "
|
|
1009
|
-
__publicField(this, "
|
|
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
|
|
1018
|
+
__publicField(this, "resizeObserver");
|
|
1031
1019
|
this.camera = camera;
|
|
1032
1020
|
this.canvas = canvas;
|
|
1033
|
-
this.
|
|
1034
|
-
this.
|
|
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
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
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
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
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.
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1284
|
-
const
|
|
1285
|
-
const
|
|
1286
|
-
const
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
8980
|
-
@group(1) @binding(
|
|
8981
|
-
@group(1) @binding(
|
|
8982
|
-
@group(1) @binding(
|
|
8983
|
-
@group(1) @binding(
|
|
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
|
-
|
|
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
|
-
|
|
9062
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9114
|
-
|
|
9115
|
-
|
|
9116
|
-
|
|
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
|
-
|
|
9126
|
-
|
|
9127
|
-
|
|
9128
|
-
let
|
|
9129
|
-
|
|
9130
|
-
|
|
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:
|
|
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
|
-
|
|
9312
|
-
|
|
9313
|
-
|
|
9314
|
-
|
|
9315
|
-
|
|
9316
|
-
|
|
9317
|
-
|
|
9318
|
-
|
|
9319
|
-
|
|
9320
|
-
|
|
9321
|
-
|
|
9322
|
-
|
|
9323
|
-
|
|
9324
|
-
binding: 2,
|
|
9325
|
-
visibility: GPUShaderStage.VERTEX,
|
|
9326
|
-
texture: { sampleType: "unfilterable-float" }
|
|
9327
|
-
},
|
|
9328
|
-
{
|
|
9329
|
-
// colorTex (RGBA8Unorm)
|
|
9330
|
-
binding: 3,
|
|
9331
|
-
visibility: GPUShaderStage.VERTEX,
|
|
9332
|
-
texture: { sampleType: "unfilterable-float" }
|
|
9333
|
-
}
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
9456
|
-
|
|
9457
|
-
},
|
|
9458
|
-
{
|
|
9459
|
-
|
|
9460
|
-
|
|
9461
|
-
},
|
|
9462
|
-
{
|
|
9463
|
-
binding: 2,
|
|
9464
|
-
resource: this.compressedTextures.scaleRotTexture2.createView()
|
|
9465
|
-
},
|
|
9466
|
-
{
|
|
9467
|
-
binding: 3,
|
|
9468
|
-
resource: this.compressedTextures.colorTexture.createView()
|
|
9469
|
-
}
|
|
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,
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
*/
|