@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.js
CHANGED
|
@@ -37,7 +37,7 @@ function isMobileDevice() {
|
|
|
37
37
|
}
|
|
38
38
|
function getRecommendedDPR() {
|
|
39
39
|
const isMobile = isMobileDevice();
|
|
40
|
-
const maxDpr = isMobile ?
|
|
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
|
-
|
|
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(
|
|
998
|
-
__publicField(this, "renderer");
|
|
1005
|
+
constructor(_renderer, camera, canvas) {
|
|
999
1006
|
__publicField(this, "camera");
|
|
1000
1007
|
__publicField(this, "canvas");
|
|
1001
|
-
|
|
1002
|
-
__publicField(this, "
|
|
1003
|
-
__publicField(this, "
|
|
1004
|
-
__publicField(this, "
|
|
1005
|
-
__publicField(this, "
|
|
1006
|
-
__publicField(this, "
|
|
1007
|
-
__publicField(this, "
|
|
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
|
|
1016
|
+
__publicField(this, "resizeObserver");
|
|
1029
1017
|
this.camera = camera;
|
|
1030
1018
|
this.canvas = canvas;
|
|
1031
|
-
this.
|
|
1032
|
-
this.
|
|
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
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
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
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
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.
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1282
|
-
const
|
|
1283
|
-
const
|
|
1284
|
-
const
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
8978
|
-
@group(1) @binding(
|
|
8979
|
-
@group(1) @binding(
|
|
8980
|
-
@group(1) @binding(
|
|
8981
|
-
@group(1) @binding(
|
|
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
|
-
|
|
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
|
-
|
|
9060
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9112
|
-
|
|
9113
|
-
|
|
9114
|
-
|
|
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
|
-
|
|
9124
|
-
|
|
9125
|
-
|
|
9126
|
-
let
|
|
9127
|
-
|
|
9128
|
-
|
|
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:
|
|
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
|
-
|
|
9310
|
-
|
|
9311
|
-
|
|
9312
|
-
|
|
9313
|
-
|
|
9314
|
-
|
|
9315
|
-
|
|
9316
|
-
|
|
9317
|
-
|
|
9318
|
-
|
|
9319
|
-
|
|
9320
|
-
|
|
9321
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
9454
|
-
|
|
9455
|
-
},
|
|
9456
|
-
{
|
|
9457
|
-
|
|
9458
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
*/
|