@d5techs/3dgs-lib 1.0.0 → 1.1.0
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/README.md +316 -142
- package/dist/3dgs-lib.cjs +1442 -1976
- package/dist/3dgs-lib.cjs.map +1 -1
- package/dist/3dgs-lib.js +1443 -1977
- package/dist/3dgs-lib.js.map +1 -1
- package/dist/App.d.ts +1 -1
- package/dist/core/BoundingBoxRenderer.d.ts +3 -14
- package/dist/gs/GSSplatRenderer.d.ts +26 -176
- package/dist/gs/GSSplatRendererMobile.d.ts +8 -6
- package/dist/gs/GSSplatSorter.d.ts +41 -37
- package/dist/gs/IGSSplatRenderer.d.ts +10 -37
- package/dist/index.d.ts +9 -6
- package/dist/interaction/GizmoManager.d.ts +4 -41
- package/dist/loaders/GLBLoader.d.ts +3 -11
- package/dist/loaders/OBJLoader.d.ts +1 -1
- package/dist/mesh/Mesh.d.ts +6 -10
- package/dist/scene/SceneManager.d.ts +6 -6
- package/dist/scene/proxies/MeshGroupProxy.d.ts +20 -0
- package/dist/scene/proxies/SplatBoundingBoxProvider.d.ts +11 -0
- package/dist/scene/proxies/SplatTransformProxy.d.ts +17 -0
- package/dist/scene/proxies/index.d.ts +6 -0
- package/dist/types/geometry.d.ts +60 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/material.d.ts +27 -0
- package/dist/types/splat.d.ts +25 -0
- package/dist/utils/device.d.ts +17 -0
- package/dist/utils/geometry.d.ts +26 -0
- package/dist/utils/index.d.ts +6 -0
- package/dist/utils/texture.d.ts +64 -0
- package/package.json +1 -1
package/dist/3dgs-lib.js
CHANGED
|
@@ -1,6 +1,245 @@
|
|
|
1
1
|
var __defProp = Object.defineProperty;
|
|
2
2
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
3
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
+
const DEFAULT_MATERIAL = {
|
|
5
|
+
baseColorFactor: [1, 1, 1, 1],
|
|
6
|
+
baseColorTexture: null,
|
|
7
|
+
metallicFactor: 0,
|
|
8
|
+
roughnessFactor: 0.5,
|
|
9
|
+
doubleSided: false
|
|
10
|
+
};
|
|
11
|
+
const DEFAULT_OBJ_MATERIAL = {
|
|
12
|
+
baseColorFactor: [1, 1, 1, 1],
|
|
13
|
+
baseColorTexture: null,
|
|
14
|
+
metallicFactor: 0,
|
|
15
|
+
roughnessFactor: 0.5,
|
|
16
|
+
doubleSided: true
|
|
17
|
+
};
|
|
18
|
+
var SHMode = /* @__PURE__ */ ((SHMode2) => {
|
|
19
|
+
SHMode2[SHMode2["L0"] = 0] = "L0";
|
|
20
|
+
SHMode2[SHMode2["L1"] = 1] = "L1";
|
|
21
|
+
SHMode2[SHMode2["L2"] = 2] = "L2";
|
|
22
|
+
SHMode2[SHMode2["L3"] = 3] = "L3";
|
|
23
|
+
return SHMode2;
|
|
24
|
+
})(SHMode || {});
|
|
25
|
+
function isMobileDevice() {
|
|
26
|
+
if (typeof navigator === "undefined" || typeof window === "undefined") {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
const ua = navigator.userAgent || navigator.vendor || window.opera || "";
|
|
30
|
+
const isMobileUA = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(
|
|
31
|
+
ua.toLowerCase()
|
|
32
|
+
);
|
|
33
|
+
const hasTouch = "ontouchstart" in window || navigator.maxTouchPoints > 0;
|
|
34
|
+
const isSmallScreen = window.innerWidth <= 768;
|
|
35
|
+
const isIPadAsMac = navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1;
|
|
36
|
+
return isMobileUA || isIPadAsMac || hasTouch && isSmallScreen;
|
|
37
|
+
}
|
|
38
|
+
function getRecommendedDPR() {
|
|
39
|
+
const isMobile = isMobileDevice();
|
|
40
|
+
const maxDpr = isMobile ? 1.5 : 3;
|
|
41
|
+
return Math.min(window.devicePixelRatio || 1, maxDpr);
|
|
42
|
+
}
|
|
43
|
+
function isWebGPUSupported() {
|
|
44
|
+
return typeof navigator !== "undefined" && "gpu" in navigator;
|
|
45
|
+
}
|
|
46
|
+
function computeBoundingBox$1(positions) {
|
|
47
|
+
if (positions.length < 3) {
|
|
48
|
+
return {
|
|
49
|
+
min: [0, 0, 0],
|
|
50
|
+
max: [0, 0, 0],
|
|
51
|
+
center: [0, 0, 0],
|
|
52
|
+
radius: 0
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const min = [positions[0], positions[1], positions[2]];
|
|
56
|
+
const max = [positions[0], positions[1], positions[2]];
|
|
57
|
+
for (let i = 3; i < positions.length; i += 3) {
|
|
58
|
+
const x = positions[i];
|
|
59
|
+
const y = positions[i + 1];
|
|
60
|
+
const z = positions[i + 2];
|
|
61
|
+
min[0] = Math.min(min[0], x);
|
|
62
|
+
min[1] = Math.min(min[1], y);
|
|
63
|
+
min[2] = Math.min(min[2], z);
|
|
64
|
+
max[0] = Math.max(max[0], x);
|
|
65
|
+
max[1] = Math.max(max[1], y);
|
|
66
|
+
max[2] = Math.max(max[2], z);
|
|
67
|
+
}
|
|
68
|
+
const center = [
|
|
69
|
+
(min[0] + max[0]) / 2,
|
|
70
|
+
(min[1] + max[1]) / 2,
|
|
71
|
+
(min[2] + max[2]) / 2
|
|
72
|
+
];
|
|
73
|
+
const dx = max[0] - min[0];
|
|
74
|
+
const dy = max[1] - min[1];
|
|
75
|
+
const dz = max[2] - min[2];
|
|
76
|
+
const radius = Math.sqrt(dx * dx + dy * dy + dz * dz) / 2;
|
|
77
|
+
return { min, max, center, radius };
|
|
78
|
+
}
|
|
79
|
+
function mergeBoundingBoxes(boxes) {
|
|
80
|
+
if (boxes.length === 0) return null;
|
|
81
|
+
let combinedMin = [...boxes[0].min];
|
|
82
|
+
let combinedMax = [...boxes[0].max];
|
|
83
|
+
for (let i = 1; i < boxes.length; i++) {
|
|
84
|
+
const box = boxes[i];
|
|
85
|
+
combinedMin[0] = Math.min(combinedMin[0], box.min[0]);
|
|
86
|
+
combinedMin[1] = Math.min(combinedMin[1], box.min[1]);
|
|
87
|
+
combinedMin[2] = Math.min(combinedMin[2], box.min[2]);
|
|
88
|
+
combinedMax[0] = Math.max(combinedMax[0], box.max[0]);
|
|
89
|
+
combinedMax[1] = Math.max(combinedMax[1], box.max[1]);
|
|
90
|
+
combinedMax[2] = Math.max(combinedMax[2], box.max[2]);
|
|
91
|
+
}
|
|
92
|
+
const center = [
|
|
93
|
+
(combinedMin[0] + combinedMax[0]) / 2,
|
|
94
|
+
(combinedMin[1] + combinedMax[1]) / 2,
|
|
95
|
+
(combinedMin[2] + combinedMax[2]) / 2
|
|
96
|
+
];
|
|
97
|
+
const dx = combinedMax[0] - combinedMin[0];
|
|
98
|
+
const dy = combinedMax[1] - combinedMin[1];
|
|
99
|
+
const dz = combinedMax[2] - combinedMin[2];
|
|
100
|
+
const radius = Math.sqrt(dx * dx + dy * dy + dz * dz) / 2;
|
|
101
|
+
return { min: combinedMin, max: combinedMax, center, radius };
|
|
102
|
+
}
|
|
103
|
+
function createBoundingBoxFromMinMax(min, max) {
|
|
104
|
+
const center = [
|
|
105
|
+
(min[0] + max[0]) / 2,
|
|
106
|
+
(min[1] + max[1]) / 2,
|
|
107
|
+
(min[2] + max[2]) / 2
|
|
108
|
+
];
|
|
109
|
+
const dx = max[0] - min[0];
|
|
110
|
+
const dy = max[1] - min[1];
|
|
111
|
+
const dz = max[2] - min[2];
|
|
112
|
+
const radius = Math.sqrt(dx * dx + dy * dy + dz * dz) / 2;
|
|
113
|
+
return { min, max, center, radius };
|
|
114
|
+
}
|
|
115
|
+
function transformBoundingBox(bbox, modelMatrix) {
|
|
116
|
+
const corners = [
|
|
117
|
+
[bbox.min[0], bbox.min[1], bbox.min[2]],
|
|
118
|
+
[bbox.max[0], bbox.min[1], bbox.min[2]],
|
|
119
|
+
[bbox.min[0], bbox.max[1], bbox.min[2]],
|
|
120
|
+
[bbox.max[0], bbox.max[1], bbox.min[2]],
|
|
121
|
+
[bbox.min[0], bbox.min[1], bbox.max[2]],
|
|
122
|
+
[bbox.max[0], bbox.min[1], bbox.max[2]],
|
|
123
|
+
[bbox.min[0], bbox.max[1], bbox.max[2]],
|
|
124
|
+
[bbox.max[0], bbox.max[1], bbox.max[2]]
|
|
125
|
+
];
|
|
126
|
+
const m = modelMatrix;
|
|
127
|
+
let minX = Infinity, minY = Infinity, minZ = Infinity;
|
|
128
|
+
let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
|
|
129
|
+
for (const [x, y, z] of corners) {
|
|
130
|
+
const tx = m[0] * x + m[4] * y + m[8] * z + m[12];
|
|
131
|
+
const ty = m[1] * x + m[5] * y + m[9] * z + m[13];
|
|
132
|
+
const tz = m[2] * x + m[6] * y + m[10] * z + m[14];
|
|
133
|
+
minX = Math.min(minX, tx);
|
|
134
|
+
minY = Math.min(minY, ty);
|
|
135
|
+
minZ = Math.min(minZ, tz);
|
|
136
|
+
maxX = Math.max(maxX, tx);
|
|
137
|
+
maxY = Math.max(maxY, ty);
|
|
138
|
+
maxZ = Math.max(maxZ, tz);
|
|
139
|
+
}
|
|
140
|
+
return createBoundingBoxFromMinMax(
|
|
141
|
+
[minX, minY, minZ],
|
|
142
|
+
[maxX, maxY, maxZ]
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
async function loadTextureFromURL(device, url) {
|
|
146
|
+
try {
|
|
147
|
+
const response = await fetch(url);
|
|
148
|
+
if (!response.ok) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
const blob = await response.blob();
|
|
152
|
+
return loadTextureFromBlob(device, blob);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
console.warn(`Failed to load texture from URL: ${url}`, error);
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async function loadTextureFromBlob(device, blob) {
|
|
159
|
+
try {
|
|
160
|
+
const imageBitmap = await createImageBitmap(blob);
|
|
161
|
+
return createTextureFromImageBitmap(device, imageBitmap);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.warn(`Failed to create texture from blob`, error);
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async function loadTextureFromBuffer(device, buffer, mimeType = "image/png") {
|
|
168
|
+
try {
|
|
169
|
+
const uint8Array = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
|
|
170
|
+
const blob = new Blob([uint8Array], { type: mimeType });
|
|
171
|
+
return loadTextureFromBlob(device, blob);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
console.warn(`Failed to create texture from buffer`, error);
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function createTextureFromImageBitmap(device, imageBitmap) {
|
|
178
|
+
const texture = device.createTexture({
|
|
179
|
+
size: [imageBitmap.width, imageBitmap.height, 1],
|
|
180
|
+
format: "rgba8unorm",
|
|
181
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
|
|
182
|
+
});
|
|
183
|
+
device.queue.copyExternalImageToTexture(
|
|
184
|
+
{ source: imageBitmap },
|
|
185
|
+
{ texture },
|
|
186
|
+
[imageBitmap.width, imageBitmap.height]
|
|
187
|
+
);
|
|
188
|
+
return texture;
|
|
189
|
+
}
|
|
190
|
+
class TextureCache {
|
|
191
|
+
constructor(device) {
|
|
192
|
+
__publicField(this, "cache", /* @__PURE__ */ new Map());
|
|
193
|
+
__publicField(this, "device");
|
|
194
|
+
this.device = device;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* 获取或加载纹理
|
|
198
|
+
*/
|
|
199
|
+
async getOrLoad(url) {
|
|
200
|
+
if (this.cache.has(url)) {
|
|
201
|
+
return this.cache.get(url);
|
|
202
|
+
}
|
|
203
|
+
const texture = await loadTextureFromURL(this.device, url);
|
|
204
|
+
if (texture) {
|
|
205
|
+
this.cache.set(url, texture);
|
|
206
|
+
}
|
|
207
|
+
return texture;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* 检查缓存中是否存在
|
|
211
|
+
*/
|
|
212
|
+
has(url) {
|
|
213
|
+
return this.cache.has(url);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* 从缓存获取
|
|
217
|
+
*/
|
|
218
|
+
get(url) {
|
|
219
|
+
return this.cache.get(url);
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* 添加到缓存
|
|
223
|
+
*/
|
|
224
|
+
set(url, texture) {
|
|
225
|
+
this.cache.set(url, texture);
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* 清空缓存
|
|
229
|
+
*/
|
|
230
|
+
clear() {
|
|
231
|
+
this.cache.clear();
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* 销毁所有纹理并清空缓存
|
|
235
|
+
*/
|
|
236
|
+
destroy() {
|
|
237
|
+
for (const texture of this.cache.values()) {
|
|
238
|
+
texture.destroy();
|
|
239
|
+
}
|
|
240
|
+
this.cache.clear();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
4
243
|
class Renderer {
|
|
5
244
|
constructor(canvas) {
|
|
6
245
|
__publicField(this, "canvas");
|
|
@@ -130,11 +369,7 @@ class Renderer {
|
|
|
130
369
|
this.resizeObserver = new ResizeObserver((entries) => {
|
|
131
370
|
for (const entry of entries) {
|
|
132
371
|
const { width, height } = entry.contentRect;
|
|
133
|
-
const
|
|
134
|
-
navigator.userAgent.toLowerCase()
|
|
135
|
-
);
|
|
136
|
-
const maxDpr = isMobile ? 1.5 : 3;
|
|
137
|
-
const dpr = Math.min(window.devicePixelRatio || 1, maxDpr);
|
|
372
|
+
const dpr = getRecommendedDPR();
|
|
138
373
|
this.canvas.width = Math.floor(width * dpr);
|
|
139
374
|
this.canvas.height = Math.floor(height * dpr);
|
|
140
375
|
this.createDepthTexture();
|
|
@@ -203,8 +438,10 @@ class Camera {
|
|
|
203
438
|
__publicField(this, "fov", Math.PI / 4);
|
|
204
439
|
// 45度
|
|
205
440
|
__publicField(this, "aspect", 1);
|
|
206
|
-
__publicField(this, "near",
|
|
207
|
-
|
|
441
|
+
__publicField(this, "near", 0.1);
|
|
442
|
+
// 增大近平面以提高深度精度 (参考实现使用 0.1)
|
|
443
|
+
__publicField(this, "far", 1e3);
|
|
444
|
+
// 减小远平面以提高深度精度
|
|
208
445
|
// 矩阵
|
|
209
446
|
__publicField(this, "viewMatrix", new Float32Array(16));
|
|
210
447
|
__publicField(this, "projectionMatrix", new Float32Array(16));
|
|
@@ -2184,7 +2421,7 @@ class GLBLoader {
|
|
|
2184
2421
|
this.device.queue.writeBuffer(indexBuffer, 0, indexData);
|
|
2185
2422
|
}
|
|
2186
2423
|
}
|
|
2187
|
-
const boundingBox = this.
|
|
2424
|
+
const boundingBox = this.computeBoundingBoxFromPositions(positions);
|
|
2188
2425
|
const material = await this.parseMaterial(gltf, primitive.material, binData);
|
|
2189
2426
|
const mesh = new Mesh(vertexBuffer, vertexCount, indexBuffer, indexCount, boundingBox);
|
|
2190
2427
|
mesh.hasUV = hasUV;
|
|
@@ -2290,38 +2527,8 @@ class GLBLoader {
|
|
|
2290
2527
|
/**
|
|
2291
2528
|
* 计算顶点数据的 bounding box
|
|
2292
2529
|
*/
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
return {
|
|
2296
|
-
min: [0, 0, 0],
|
|
2297
|
-
max: [0, 0, 0],
|
|
2298
|
-
center: [0, 0, 0],
|
|
2299
|
-
radius: 0
|
|
2300
|
-
};
|
|
2301
|
-
}
|
|
2302
|
-
const min = [positions[0], positions[1], positions[2]];
|
|
2303
|
-
const max = [positions[0], positions[1], positions[2]];
|
|
2304
|
-
for (let i = 3; i < positions.length; i += 3) {
|
|
2305
|
-
const x = positions[i];
|
|
2306
|
-
const y = positions[i + 1];
|
|
2307
|
-
const z = positions[i + 2];
|
|
2308
|
-
min[0] = Math.min(min[0], x);
|
|
2309
|
-
min[1] = Math.min(min[1], y);
|
|
2310
|
-
min[2] = Math.min(min[2], z);
|
|
2311
|
-
max[0] = Math.max(max[0], x);
|
|
2312
|
-
max[1] = Math.max(max[1], y);
|
|
2313
|
-
max[2] = Math.max(max[2], z);
|
|
2314
|
-
}
|
|
2315
|
-
const center = [
|
|
2316
|
-
(min[0] + max[0]) / 2,
|
|
2317
|
-
(min[1] + max[1]) / 2,
|
|
2318
|
-
(min[2] + max[2]) / 2
|
|
2319
|
-
];
|
|
2320
|
-
const dx = max[0] - min[0];
|
|
2321
|
-
const dy = max[1] - min[1];
|
|
2322
|
-
const dz = max[2] - min[2];
|
|
2323
|
-
const radius = Math.sqrt(dx * dx + dy * dy + dz * dz) / 2;
|
|
2324
|
-
return { min, max, center, radius };
|
|
2530
|
+
computeBoundingBoxFromPositions(positions) {
|
|
2531
|
+
return computeBoundingBox$1(positions);
|
|
2325
2532
|
}
|
|
2326
2533
|
/**
|
|
2327
2534
|
* 获取访问器数据 - 修复字节对齐问题
|
|
@@ -3318,7 +3525,7 @@ class OBJLoader {
|
|
|
3318
3525
|
});
|
|
3319
3526
|
this.device.queue.writeBuffer(indexBuffer, 0, indexData);
|
|
3320
3527
|
}
|
|
3321
|
-
const boundingBox = this.
|
|
3528
|
+
const boundingBox = this.computeBoundingBoxFromPositions(obj.positions);
|
|
3322
3529
|
const mesh = new Mesh(vertexBuffer, vertexCount, indexBuffer, indexCount, boundingBox);
|
|
3323
3530
|
mesh.hasUV = hasUVs;
|
|
3324
3531
|
mesh.indexFormat = indexFormat;
|
|
@@ -3359,53 +3566,16 @@ class OBJLoader {
|
|
|
3359
3566
|
* 计算顶点数据的 bounding box
|
|
3360
3567
|
* Requirement 3.5: 计算并存储 bounding box 信息
|
|
3361
3568
|
*/
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
return {
|
|
3365
|
-
min: [0, 0, 0],
|
|
3366
|
-
max: [0, 0, 0],
|
|
3367
|
-
center: [0, 0, 0],
|
|
3368
|
-
radius: 0
|
|
3369
|
-
};
|
|
3370
|
-
}
|
|
3371
|
-
const min = [positions[0], positions[1], positions[2]];
|
|
3372
|
-
const max = [positions[0], positions[1], positions[2]];
|
|
3373
|
-
for (let i = 3; i < positions.length; i += 3) {
|
|
3374
|
-
const x = positions[i];
|
|
3375
|
-
const y = positions[i + 1];
|
|
3376
|
-
const z = positions[i + 2];
|
|
3377
|
-
min[0] = Math.min(min[0], x);
|
|
3378
|
-
min[1] = Math.min(min[1], y);
|
|
3379
|
-
min[2] = Math.min(min[2], z);
|
|
3380
|
-
max[0] = Math.max(max[0], x);
|
|
3381
|
-
max[1] = Math.max(max[1], y);
|
|
3382
|
-
max[2] = Math.max(max[2], z);
|
|
3383
|
-
}
|
|
3384
|
-
const center = [
|
|
3385
|
-
(min[0] + max[0]) / 2,
|
|
3386
|
-
(min[1] + max[1]) / 2,
|
|
3387
|
-
(min[2] + max[2]) / 2
|
|
3388
|
-
];
|
|
3389
|
-
const dx = max[0] - min[0];
|
|
3390
|
-
const dy = max[1] - min[1];
|
|
3391
|
-
const dz = max[2] - min[2];
|
|
3392
|
-
const radius = Math.sqrt(dx * dx + dy * dy + dz * dz) / 2;
|
|
3393
|
-
return { min, max, center, radius };
|
|
3569
|
+
computeBoundingBoxFromPositions(positions) {
|
|
3570
|
+
return computeBoundingBox$1(positions);
|
|
3394
3571
|
}
|
|
3395
3572
|
/**
|
|
3396
3573
|
* 创建材质数据
|
|
3397
3574
|
* Requirement 4.3: 将材质关联到网格
|
|
3398
3575
|
*/
|
|
3399
3576
|
async createMaterial(materialName, materials, baseUrl) {
|
|
3400
|
-
const defaultMaterial = {
|
|
3401
|
-
baseColorFactor: [1, 1, 1, 1],
|
|
3402
|
-
baseColorTexture: null,
|
|
3403
|
-
metallicFactor: 0,
|
|
3404
|
-
roughnessFactor: 0.5,
|
|
3405
|
-
doubleSided: true
|
|
3406
|
-
};
|
|
3407
3577
|
if (!materialName || !materials.has(materialName)) {
|
|
3408
|
-
return
|
|
3578
|
+
return { ...DEFAULT_OBJ_MATERIAL };
|
|
3409
3579
|
}
|
|
3410
3580
|
const parsedMaterial = materials.get(materialName);
|
|
3411
3581
|
const material = {
|
|
@@ -4205,27 +4375,20 @@ function deserializeSplat(data) {
|
|
|
4205
4375
|
}
|
|
4206
4376
|
return splats;
|
|
4207
4377
|
}
|
|
4208
|
-
const DEFAULT_NUM_BUCKETS$1 = 65536;
|
|
4209
|
-
const IOS_NUM_BUCKETS$1 = 4096;
|
|
4210
4378
|
const WORKGROUP_SIZE$1 = 256;
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
function generateCullingShaderCode$1(numBuckets) {
|
|
4217
|
-
const bucketBits = Math.log2(numBuckets);
|
|
4379
|
+
const RADIX_BITS = 8;
|
|
4380
|
+
const RADIX_SIZE = 256;
|
|
4381
|
+
const ELEMENTS_PER_THREAD = 4;
|
|
4382
|
+
const BLOCK_SIZE = WORKGROUP_SIZE$1 * ELEMENTS_PER_THREAD;
|
|
4383
|
+
function generateCullingShaderCode$1() {
|
|
4218
4384
|
return (
|
|
4219
4385
|
/* wgsl */
|
|
4220
4386
|
`
|
|
4221
4387
|
/**
|
|
4222
|
-
*
|
|
4223
|
-
*
|
|
4388
|
+
* Project & Cull Shader
|
|
4389
|
+
* 基于 rfs-gsplat-render 实现
|
|
4224
4390
|
*/
|
|
4225
4391
|
|
|
4226
|
-
const NUM_BUCKETS: u32 = ${numBuckets}u;
|
|
4227
|
-
const BUCKET_MAX: u32 = ${numBuckets - 1}u;
|
|
4228
|
-
|
|
4229
4392
|
struct Splat {
|
|
4230
4393
|
mean: vec3<f32>,
|
|
4231
4394
|
_pad0: f32,
|
|
@@ -4254,29 +4417,22 @@ struct CullingParams {
|
|
|
4254
4417
|
farPlane: f32,
|
|
4255
4418
|
screenWidth: f32,
|
|
4256
4419
|
screenHeight: f32,
|
|
4420
|
+
frustumDilation: f32,
|
|
4257
4421
|
pixelThreshold: f32,
|
|
4258
|
-
_pad0: f32,
|
|
4259
4422
|
_pad1: f32,
|
|
4260
4423
|
}
|
|
4261
4424
|
|
|
4262
|
-
struct Counters {
|
|
4263
|
-
visibleCount: atomic<u32>,
|
|
4264
|
-
}
|
|
4265
|
-
|
|
4266
4425
|
@group(0) @binding(0) var<storage, read> splats: array<Splat>;
|
|
4267
4426
|
@group(0) @binding(1) var<uniform> camera: CameraUniforms;
|
|
4268
4427
|
@group(0) @binding(2) var<uniform> params: CullingParams;
|
|
4269
|
-
@group(0) @binding(3) var<storage, read_write>
|
|
4428
|
+
@group(0) @binding(3) var<storage, read_write> depthKeys: array<u32>;
|
|
4270
4429
|
@group(0) @binding(4) var<storage, read_write> visibleIndices: array<u32>;
|
|
4271
|
-
@group(0) @binding(5) var<storage, read_write>
|
|
4272
|
-
@group(0) @binding(6) var<storage, read_write> bucketCounts: array<atomic<u32>>;
|
|
4273
|
-
@group(0) @binding(7) var<storage, read_write> drawIndirect: array<u32>;
|
|
4430
|
+
@group(0) @binding(5) var<storage, read_write> indirectBuffer: array<atomic<u32>, 4>;
|
|
4274
4431
|
|
|
4275
4432
|
fn maxScale(scale: vec3<f32>) -> f32 {
|
|
4276
4433
|
return max(max(scale.x, scale.y), scale.z);
|
|
4277
4434
|
}
|
|
4278
4435
|
|
|
4279
|
-
// 从模型矩阵提取最大缩放因子
|
|
4280
4436
|
fn getModelMaxScale(model: mat4x4<f32>) -> f32 {
|
|
4281
4437
|
let sx = length(model[0].xyz);
|
|
4282
4438
|
let sy = length(model[1].xyz);
|
|
@@ -4284,422 +4440,504 @@ fn getModelMaxScale(model: mat4x4<f32>) -> f32 {
|
|
|
4284
4440
|
return max(max(sx, sy), sz);
|
|
4285
4441
|
}
|
|
4286
4442
|
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4443
|
+
// IEEE 754 位操作编码浮点数为可排序的 u32
|
|
4444
|
+
// 参考 rfs-gsplat-render 的 encode_min_max_fp32 实现
|
|
4445
|
+
fn encodeDepthKey(val: f32) -> u32 {
|
|
4446
|
+
var bits = bitcast<u32>(val);
|
|
4447
|
+
bits ^= bitcast<u32>(bitcast<i32>(bits) >> 31) | 0x80000000u;
|
|
4448
|
+
return bits;
|
|
4449
|
+
}
|
|
4450
|
+
|
|
4451
|
+
// 视锥剔除检查
|
|
4452
|
+
// 基于 rfs-gsplat-render 的 is_in_frustum 实现
|
|
4453
|
+
fn isInFrustum(clipPos: vec4<f32>, frustumDilation: f32) -> bool {
|
|
4454
|
+
let clip = (1.0 + frustumDilation) * clipPos.w;
|
|
4293
4455
|
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
let worldPos = camera.model * vec4<f32>(splat.mean, 1.0);
|
|
4297
|
-
let viewPos = camera.view * worldPos;
|
|
4298
|
-
let z = -viewPos.z;
|
|
4456
|
+
if abs(clipPos.x) > clip { return false; }
|
|
4457
|
+
if abs(clipPos.y) > clip { return false; }
|
|
4299
4458
|
|
|
4300
|
-
|
|
4301
|
-
if
|
|
4302
|
-
return;
|
|
4459
|
+
let nearThreshold = (0.0 - frustumDilation) * clipPos.w;
|
|
4460
|
+
if clipPos.z < nearThreshold || clipPos.z > clipPos.w {
|
|
4461
|
+
return false;
|
|
4303
4462
|
}
|
|
4304
4463
|
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4464
|
+
return true;
|
|
4465
|
+
}
|
|
4466
|
+
|
|
4467
|
+
@compute @workgroup_size(${WORKGROUP_SIZE$1})
|
|
4468
|
+
fn projectAndCull(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
4469
|
+
let i = gid.x;
|
|
4470
|
+
if i >= params.splatCount { return; }
|
|
4309
4471
|
|
|
4310
|
-
|
|
4311
|
-
let fx = camera.proj[0][0];
|
|
4312
|
-
let fy = camera.proj[1][1];
|
|
4313
|
-
let x_ndc = viewPos.x * fx / z;
|
|
4314
|
-
let y_ndc = viewPos.y * fy / z;
|
|
4315
|
-
// 考虑模型缩放对 splat 半径的影响
|
|
4316
|
-
let modelScale = getModelMaxScale(camera.model);
|
|
4317
|
-
let worldRadius = maxScale(splat.scale) * modelScale * 3.0;
|
|
4318
|
-
let r_ndc = worldRadius * max(fx, fy) / z;
|
|
4472
|
+
let splat = splats[i];
|
|
4319
4473
|
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
}
|
|
4323
|
-
if (y_ndc < -1.0 - r_ndc || y_ndc > 1.0 + r_ndc) {
|
|
4324
|
-
return;
|
|
4325
|
-
}
|
|
4474
|
+
// 透明度剔除
|
|
4475
|
+
if splat.opacity < 0.004 { return; }
|
|
4326
4476
|
|
|
4327
|
-
//
|
|
4328
|
-
let
|
|
4329
|
-
let
|
|
4330
|
-
let
|
|
4477
|
+
// 变换: Local -> World -> View -> Clip
|
|
4478
|
+
let worldPos = camera.model * vec4<f32>(splat.mean, 1.0);
|
|
4479
|
+
let viewPos = camera.view * worldPos;
|
|
4480
|
+
let clipPos = camera.proj * viewPos;
|
|
4331
4481
|
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
}
|
|
4482
|
+
// 视锥剔除
|
|
4483
|
+
if !isInFrustum(clipPos, params.frustumDilation) { return; }
|
|
4335
4484
|
|
|
4336
|
-
//
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
}
|
|
4485
|
+
// 深度编码 (viewPos.z 是负数)
|
|
4486
|
+
let depth = viewPos.z;
|
|
4487
|
+
let sortableDepth = encodeDepthKey(depth);
|
|
4340
4488
|
|
|
4341
|
-
//
|
|
4342
|
-
|
|
4343
|
-
let
|
|
4344
|
-
// 反转:让远处的桶 ID 更小,这样 counting sort 后远处的在前面
|
|
4345
|
-
let depthBucket = BUCKET_MAX - u32(normalizedDepth * f32(BUCKET_MAX));
|
|
4346
|
-
// 复合 key: 深度桶 + 原始索引低位作为 tie-breaker
|
|
4347
|
-
let depthKey = (depthBucket << ${32 - bucketBits}u) | (i & ${(1 << 32 - bucketBits) - 1}u);
|
|
4489
|
+
// 原子增加可见计数并获取索引
|
|
4490
|
+
// indirectBuffer[1] 是 instance_count
|
|
4491
|
+
let visibleIdx = atomicAdd(&indirectBuffer[1], 1u);
|
|
4348
4492
|
|
|
4349
|
-
//
|
|
4350
|
-
|
|
4493
|
+
// 写入可见点列表
|
|
4494
|
+
depthKeys[visibleIdx] = sortableDepth;
|
|
4351
4495
|
visibleIndices[visibleIdx] = i;
|
|
4352
|
-
depthKeys[visibleIdx] = depthKey;
|
|
4353
|
-
|
|
4354
|
-
// 统计桶计数
|
|
4355
|
-
atomicAdd(&bucketCounts[depthBucket], 1u);
|
|
4356
|
-
}
|
|
4357
|
-
|
|
4358
|
-
@compute @workgroup_size(1)
|
|
4359
|
-
fn resetCounters() {
|
|
4360
|
-
atomicStore(&counters.visibleCount, 0u);
|
|
4361
|
-
}
|
|
4362
|
-
|
|
4363
|
-
@compute @workgroup_size(${WORKGROUP_SIZE$1})
|
|
4364
|
-
fn resetBucketCounts(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
4365
|
-
let i = gid.x;
|
|
4366
|
-
if (i < NUM_BUCKETS) {
|
|
4367
|
-
atomicStore(&bucketCounts[i], 0u);
|
|
4368
|
-
}
|
|
4369
4496
|
}
|
|
4370
4497
|
|
|
4371
4498
|
@compute @workgroup_size(1)
|
|
4372
|
-
fn
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4499
|
+
fn initIndirectBuffer() {
|
|
4500
|
+
// [vertex_count, instance_count, first_vertex, first_instance]
|
|
4501
|
+
atomicStore(&indirectBuffer[0], 4u);
|
|
4502
|
+
atomicStore(&indirectBuffer[1], 0u); // instance_count 由 cull shader 填充
|
|
4503
|
+
atomicStore(&indirectBuffer[2], 0u);
|
|
4504
|
+
atomicStore(&indirectBuffer[3], 0u);
|
|
4378
4505
|
}
|
|
4379
4506
|
`
|
|
4380
4507
|
);
|
|
4381
4508
|
}
|
|
4382
|
-
function
|
|
4509
|
+
function generateRadixSortShaderCode() {
|
|
4383
4510
|
return (
|
|
4384
4511
|
/* wgsl */
|
|
4385
4512
|
`
|
|
4386
4513
|
/**
|
|
4387
|
-
* Pass
|
|
4388
|
-
*
|
|
4514
|
+
* GPU Radix Sort - 3-Pass Architecture
|
|
4515
|
+
* 基于 rfs-gsplat-render 实现
|
|
4516
|
+
*
|
|
4517
|
+
* Pass 1: Upsweep - 构建局部直方图并累加到全局
|
|
4518
|
+
* Pass 2: Spine - 对分区和全局直方图进行前缀和
|
|
4519
|
+
* Pass 3: Downsweep - 使用计算的偏移量散射元素 (稳定排序)
|
|
4389
4520
|
*/
|
|
4390
4521
|
|
|
4391
|
-
const
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4522
|
+
const WG: u32 = ${WORKGROUP_SIZE$1}u;
|
|
4523
|
+
const RADIX_BITS: u32 = ${RADIX_BITS}u;
|
|
4524
|
+
const RADIX_SIZE: u32 = ${RADIX_SIZE}u;
|
|
4525
|
+
const RADIX_MASK: u32 = ${RADIX_SIZE - 1}u;
|
|
4526
|
+
const ELEMENTS_PER_THREAD: u32 = ${ELEMENTS_PER_THREAD}u;
|
|
4527
|
+
const BLOCK_SIZE: u32 = ${BLOCK_SIZE}u;
|
|
4528
|
+
|
|
4529
|
+
fn divCeil(a: u32, b: u32) -> u32 {
|
|
4530
|
+
return (a + b - 1u) / b;
|
|
4531
|
+
}
|
|
4532
|
+
|
|
4533
|
+
struct SortParams {
|
|
4534
|
+
maxElementCount: u32,
|
|
4535
|
+
bitShift: u32,
|
|
4536
|
+
passIndex: u32,
|
|
4537
|
+
_padding: u32,
|
|
4538
|
+
}
|
|
4539
|
+
|
|
4540
|
+
// ============================================================================
|
|
4541
|
+
// Pass 1: Upsweep - 计数局部直方图并累加到全局
|
|
4542
|
+
// ============================================================================
|
|
4543
|
+
|
|
4544
|
+
@group(0) @binding(0) var<uniform> upsweepParams: SortParams;
|
|
4545
|
+
@group(0) @binding(1) var<storage, read> indirectBufferUpsweep: array<u32>;
|
|
4546
|
+
@group(0) @binding(2) var<storage, read> keysIn: array<u32>;
|
|
4547
|
+
@group(0) @binding(3) var<storage, read_write> globalHistogram: array<atomic<u32>>;
|
|
4548
|
+
@group(0) @binding(4) var<storage, read_write> partitionHistogram: array<u32>;
|
|
4549
|
+
|
|
4550
|
+
var<workgroup> localHistogram: array<atomic<u32>, RADIX_SIZE>;
|
|
4551
|
+
|
|
4552
|
+
@compute @workgroup_size(256, 1, 1)
|
|
4553
|
+
fn upsweep(
|
|
4554
|
+
@builtin(local_invocation_id) localId: vec3<u32>,
|
|
4555
|
+
@builtin(workgroup_id) workgroupId: vec3<u32>,
|
|
4556
|
+
) {
|
|
4557
|
+
// 从 indirectBuffer[1] 读取动态可见数量 (instance_count)
|
|
4558
|
+
let numKeys = indirectBufferUpsweep[1];
|
|
4559
|
+
let numPartitions = divCeil(numKeys, BLOCK_SIZE);
|
|
4560
|
+
let partitionId = workgroupId.x;
|
|
4561
|
+
|
|
4562
|
+
if partitionId >= numPartitions { return; }
|
|
4563
|
+
|
|
4564
|
+
let tid = localId.x;
|
|
4565
|
+
let partitionStart = partitionId * BLOCK_SIZE;
|
|
4566
|
+
let shift = upsweepParams.bitShift;
|
|
4567
|
+
let passIdx = upsweepParams.passIndex;
|
|
4568
|
+
|
|
4569
|
+
// 初始化局部直方图
|
|
4570
|
+
if tid < RADIX_SIZE {
|
|
4571
|
+
atomicStore(&localHistogram[tid], 0u);
|
|
4402
4572
|
}
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
/**
|
|
4414
|
-
* Pass 3: 散射到最终排序位置
|
|
4415
|
-
* 桶数量: ${numBuckets}
|
|
4416
|
-
*/
|
|
4417
|
-
|
|
4418
|
-
const NUM_BUCKETS: u32 = ${numBuckets}u;
|
|
4419
|
-
|
|
4420
|
-
struct Counters {
|
|
4421
|
-
visibleCount: u32,
|
|
4422
|
-
}
|
|
4423
|
-
|
|
4424
|
-
@group(0) @binding(0) var<storage, read> visibleIndices: array<u32>;
|
|
4425
|
-
@group(0) @binding(1) var<storage, read> depthKeys: array<u32>;
|
|
4426
|
-
@group(0) @binding(2) var<storage, read> bucketOffsets: array<u32>;
|
|
4427
|
-
@group(0) @binding(3) var<storage, read_write> bucketPositions: array<atomic<u32>>;
|
|
4428
|
-
@group(0) @binding(4) var<storage, read_write> sortedIndices: array<u32>;
|
|
4429
|
-
@group(0) @binding(5) var<storage, read> counters: Counters;
|
|
4430
|
-
|
|
4431
|
-
@compute @workgroup_size(${WORKGROUP_SIZE$1})
|
|
4432
|
-
fn scatter(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
4433
|
-
let i = gid.x;
|
|
4434
|
-
if (i >= counters.visibleCount) {
|
|
4435
|
-
return;
|
|
4573
|
+
workgroupBarrier();
|
|
4574
|
+
|
|
4575
|
+
// 构建局部直方图
|
|
4576
|
+
for (var j = 0u; j < ELEMENTS_PER_THREAD; j++) {
|
|
4577
|
+
let keyIdx = partitionStart + tid * ELEMENTS_PER_THREAD + j;
|
|
4578
|
+
if keyIdx < numKeys {
|
|
4579
|
+
let key = keysIn[keyIdx];
|
|
4580
|
+
let bin = (key >> shift) & RADIX_MASK;
|
|
4581
|
+
atomicAdd(&localHistogram[bin], 1u);
|
|
4582
|
+
}
|
|
4436
4583
|
}
|
|
4437
4584
|
|
|
4438
|
-
|
|
4439
|
-
let originalIndex = visibleIndices[i];
|
|
4585
|
+
workgroupBarrier();
|
|
4440
4586
|
|
|
4441
|
-
//
|
|
4442
|
-
|
|
4587
|
+
// 写入分区直方图并累加到全局直方图
|
|
4588
|
+
if tid < RADIX_SIZE {
|
|
4589
|
+
let count = atomicLoad(&localHistogram[tid]);
|
|
4590
|
+
partitionHistogram[RADIX_SIZE * partitionId + tid] = count;
|
|
4591
|
+
atomicAdd(&globalHistogram[RADIX_SIZE * passIdx + tid], count);
|
|
4592
|
+
}
|
|
4593
|
+
}
|
|
4594
|
+
|
|
4595
|
+
// ============================================================================
|
|
4596
|
+
// Pass 2: Spine - 对分区和全局直方图进行前缀和
|
|
4597
|
+
// ============================================================================
|
|
4598
|
+
|
|
4599
|
+
@group(0) @binding(0) var<storage, read> indirectBufferSpine: array<u32>;
|
|
4600
|
+
@group(0) @binding(1) var<storage, read_write> globalHistogramSpine: array<u32>;
|
|
4601
|
+
@group(0) @binding(2) var<storage, read_write> partitionHistogramSpine: array<u32>;
|
|
4602
|
+
@group(0) @binding(3) var<uniform> spineParams: SortParams;
|
|
4603
|
+
|
|
4604
|
+
// 双缓冲用于无数据竞争的 Hillis-Steele scan
|
|
4605
|
+
var<workgroup> scanA: array<u32, 256>;
|
|
4606
|
+
var<workgroup> scanB: array<u32, 256>;
|
|
4607
|
+
var<workgroup> reductionShared: u32;
|
|
4608
|
+
|
|
4609
|
+
@compute @workgroup_size(256, 1, 1)
|
|
4610
|
+
fn spine(
|
|
4611
|
+
@builtin(local_invocation_id) localId: vec3<u32>,
|
|
4612
|
+
@builtin(workgroup_id) workgroupId: vec3<u32>,
|
|
4613
|
+
) {
|
|
4614
|
+
let numKeys = indirectBufferSpine[1];
|
|
4615
|
+
let numPartitions = divCeil(numKeys, BLOCK_SIZE);
|
|
4616
|
+
let bin = workgroupId.x;
|
|
4617
|
+
let tid = localId.x;
|
|
4443
4618
|
|
|
4444
|
-
|
|
4445
|
-
let bucketOffset = bucketOffsets[depthBucket];
|
|
4446
|
-
let posInBucket = atomicAdd(&bucketPositions[depthBucket], 1u);
|
|
4447
|
-
let destIdx = bucketOffset + posInBucket;
|
|
4619
|
+
if bin >= RADIX_SIZE { return; }
|
|
4448
4620
|
|
|
4449
|
-
//
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4621
|
+
// 初始化共享 reduction
|
|
4622
|
+
if tid == 0u {
|
|
4623
|
+
reductionShared = 0u;
|
|
4624
|
+
}
|
|
4625
|
+
workgroupBarrier();
|
|
4626
|
+
|
|
4627
|
+
// 处理此 bin 的所有分区(分批处理)
|
|
4628
|
+
let MAX_BATCH_SIZE = 256u;
|
|
4629
|
+
for (var batchStart = 0u; batchStart < numPartitions; batchStart += MAX_BATCH_SIZE) {
|
|
4630
|
+
let partitionIdx = batchStart + tid;
|
|
4631
|
+
let batchSize = min(MAX_BATCH_SIZE, numPartitions - batchStart);
|
|
4632
|
+
|
|
4633
|
+
// 加载此批次的值
|
|
4634
|
+
if tid < batchSize && partitionIdx < numPartitions {
|
|
4635
|
+
scanA[tid] = partitionHistogramSpine[RADIX_SIZE * partitionIdx + bin];
|
|
4636
|
+
} else {
|
|
4637
|
+
scanA[tid] = 0u;
|
|
4638
|
+
}
|
|
4639
|
+
workgroupBarrier();
|
|
4640
|
+
|
|
4641
|
+
// Hillis-Steele inclusive prefix sum (双缓冲避免数据竞争)
|
|
4642
|
+
var useA = true;
|
|
4643
|
+
var offset = 1u;
|
|
4644
|
+
for (var d = 0u; d < 8u; d++) {
|
|
4645
|
+
if useA {
|
|
4646
|
+
if tid >= offset {
|
|
4647
|
+
scanB[tid] = scanA[tid] + scanA[tid - offset];
|
|
4648
|
+
} else {
|
|
4649
|
+
scanB[tid] = scanA[tid];
|
|
4650
|
+
}
|
|
4651
|
+
} else {
|
|
4652
|
+
if tid >= offset {
|
|
4653
|
+
scanA[tid] = scanB[tid] + scanB[tid - offset];
|
|
4654
|
+
} else {
|
|
4655
|
+
scanA[tid] = scanB[tid];
|
|
4656
|
+
}
|
|
4657
|
+
}
|
|
4658
|
+
workgroupBarrier();
|
|
4659
|
+
useA = !useA;
|
|
4660
|
+
offset <<= 1u;
|
|
4661
|
+
}
|
|
4662
|
+
|
|
4663
|
+
// 8 次迭代后结果在 scanA 中
|
|
4664
|
+
|
|
4665
|
+
// 写回为 exclusive prefix sum(加上 reduction)
|
|
4666
|
+
if tid < batchSize && partitionIdx < numPartitions {
|
|
4667
|
+
var exclusive = reductionShared;
|
|
4668
|
+
if tid > 0u {
|
|
4669
|
+
exclusive += scanA[tid - 1u];
|
|
4670
|
+
}
|
|
4671
|
+
partitionHistogramSpine[RADIX_SIZE * partitionIdx + bin] = exclusive;
|
|
4672
|
+
}
|
|
4673
|
+
|
|
4674
|
+
// 更新下一批的 reduction
|
|
4675
|
+
workgroupBarrier();
|
|
4676
|
+
if tid == 0u && batchSize > 0u {
|
|
4677
|
+
reductionShared += scanA[batchSize - 1u];
|
|
4678
|
+
}
|
|
4679
|
+
workgroupBarrier();
|
|
4680
|
+
}
|
|
4681
|
+
|
|
4682
|
+
// Bin 0 的工作组同时处理全局直方图前缀和
|
|
4683
|
+
if bin == 0u {
|
|
4684
|
+
let passIdx = spineParams.passIndex;
|
|
4685
|
+
scanA[tid] = globalHistogramSpine[RADIX_SIZE * passIdx + tid];
|
|
4686
|
+
workgroupBarrier();
|
|
4687
|
+
|
|
4688
|
+
// Hillis-Steele inclusive scan (双缓冲)
|
|
4689
|
+
var useA = true;
|
|
4690
|
+
var offset = 1u;
|
|
4691
|
+
for (var d = 0u; d < 8u; d++) {
|
|
4692
|
+
if useA {
|
|
4693
|
+
if tid >= offset {
|
|
4694
|
+
scanB[tid] = scanA[tid] + scanA[tid - offset];
|
|
4695
|
+
} else {
|
|
4696
|
+
scanB[tid] = scanA[tid];
|
|
4697
|
+
}
|
|
4698
|
+
} else {
|
|
4699
|
+
if tid >= offset {
|
|
4700
|
+
scanA[tid] = scanB[tid] + scanB[tid - offset];
|
|
4701
|
+
} else {
|
|
4702
|
+
scanA[tid] = scanB[tid];
|
|
4703
|
+
}
|
|
4704
|
+
}
|
|
4705
|
+
workgroupBarrier();
|
|
4706
|
+
useA = !useA;
|
|
4707
|
+
offset <<= 1u;
|
|
4708
|
+
}
|
|
4709
|
+
|
|
4710
|
+
// 转换为 exclusive (结果在 scanA 中)
|
|
4711
|
+
var exclusive = 0u;
|
|
4712
|
+
if tid > 0u {
|
|
4713
|
+
exclusive = scanA[tid - 1u];
|
|
4714
|
+
}
|
|
4715
|
+
globalHistogramSpine[RADIX_SIZE * passIdx + tid] = exclusive;
|
|
4716
|
+
}
|
|
4717
|
+
}
|
|
4718
|
+
|
|
4719
|
+
// ============================================================================
|
|
4720
|
+
// Pass 3: Downsweep - 使用偏移量散射元素 (稳定排序)
|
|
4721
|
+
// ============================================================================
|
|
4722
|
+
|
|
4723
|
+
@group(0) @binding(0) var<uniform> downsweepParams: SortParams;
|
|
4724
|
+
@group(0) @binding(1) var<storage, read> indirectBufferDownsweep: array<u32>;
|
|
4725
|
+
@group(0) @binding(2) var<storage, read> globalHistogramDownsweep: array<u32>;
|
|
4726
|
+
@group(0) @binding(3) var<storage, read> partitionHistogramDownsweep: array<u32>;
|
|
4727
|
+
@group(0) @binding(4) var<storage, read> downsweepKeysIn: array<u32>;
|
|
4728
|
+
@group(0) @binding(5) var<storage, read> downsweepValuesIn: array<u32>;
|
|
4729
|
+
@group(0) @binding(6) var<storage, read_write> downsweepKeysOut: array<u32>;
|
|
4730
|
+
@group(0) @binding(7) var<storage, read_write> downsweepValuesOut: array<u32>;
|
|
4731
|
+
|
|
4732
|
+
var<workgroup> localKeys: array<u32, BLOCK_SIZE>;
|
|
4733
|
+
var<workgroup> localValues: array<u32, BLOCK_SIZE>;
|
|
4734
|
+
var<workgroup> localBins: array<u32, BLOCK_SIZE>;
|
|
4735
|
+
|
|
4736
|
+
@compute @workgroup_size(256, 1, 1)
|
|
4737
|
+
fn downsweep(
|
|
4738
|
+
@builtin(local_invocation_id) localId: vec3<u32>,
|
|
4739
|
+
@builtin(workgroup_id) workgroupId: vec3<u32>,
|
|
4740
|
+
) {
|
|
4741
|
+
let numKeys = indirectBufferDownsweep[1];
|
|
4742
|
+
let numPartitions = divCeil(numKeys, BLOCK_SIZE);
|
|
4743
|
+
let partitionId = workgroupId.x;
|
|
4744
|
+
|
|
4745
|
+
if partitionId >= numPartitions { return; }
|
|
4746
|
+
|
|
4747
|
+
let tid = localId.x;
|
|
4748
|
+
let partitionStart = partitionId * BLOCK_SIZE;
|
|
4749
|
+
let shift = downsweepParams.bitShift;
|
|
4750
|
+
|
|
4751
|
+
// 加载元素到共享内存
|
|
4752
|
+
for (var j = 0u; j < ELEMENTS_PER_THREAD; j++) {
|
|
4753
|
+
let keyIdx = partitionStart + tid * ELEMENTS_PER_THREAD + j;
|
|
4754
|
+
let localIdx = tid * ELEMENTS_PER_THREAD + j;
|
|
4755
|
+
|
|
4756
|
+
if keyIdx < numKeys {
|
|
4757
|
+
let key = downsweepKeysIn[keyIdx];
|
|
4758
|
+
localKeys[localIdx] = key;
|
|
4759
|
+
localValues[localIdx] = downsweepValuesIn[keyIdx];
|
|
4760
|
+
localBins[localIdx] = (key >> shift) & RADIX_MASK;
|
|
4761
|
+
} else {
|
|
4762
|
+
localBins[localIdx] = 0xFFFFFFFFu;
|
|
4763
|
+
}
|
|
4764
|
+
}
|
|
4765
|
+
|
|
4766
|
+
workgroupBarrier();
|
|
4767
|
+
|
|
4768
|
+
// 线程 0 执行顺序散射以保持稳定性
|
|
4769
|
+
// 这是 rfs-gsplat-render 的关键设计,确保稳定排序
|
|
4770
|
+
if tid == 0u {
|
|
4771
|
+
var binWritePos: array<u32, RADIX_SIZE>;
|
|
4772
|
+
|
|
4773
|
+
let passIdx = downsweepParams.passIndex;
|
|
4774
|
+
|
|
4775
|
+
// 从全局 + 分区偏移初始化写入位置
|
|
4776
|
+
for (var b = 0u; b < RADIX_SIZE; b++) {
|
|
4777
|
+
binWritePos[b] = globalHistogramDownsweep[RADIX_SIZE * passIdx + b] +
|
|
4778
|
+
partitionHistogramDownsweep[RADIX_SIZE * partitionId + b];
|
|
4779
|
+
}
|
|
4780
|
+
|
|
4781
|
+
// 按输入顺序顺序写入 (稳定)
|
|
4782
|
+
let partitionEnd = min(partitionStart + BLOCK_SIZE, numKeys);
|
|
4783
|
+
for (var k = 0u; k < BLOCK_SIZE; k++) {
|
|
4784
|
+
let keyIdx = partitionStart + k;
|
|
4785
|
+
if keyIdx < partitionEnd {
|
|
4786
|
+
let b = localBins[k];
|
|
4787
|
+
if b != 0xFFFFFFFFu {
|
|
4788
|
+
let writePos = binWritePos[b];
|
|
4789
|
+
if writePos < numKeys {
|
|
4790
|
+
downsweepKeysOut[writePos] = localKeys[k];
|
|
4791
|
+
downsweepValuesOut[writePos] = localValues[k];
|
|
4792
|
+
binWritePos[b]++;
|
|
4793
|
+
}
|
|
4794
|
+
}
|
|
4795
|
+
}
|
|
4796
|
+
}
|
|
4458
4797
|
}
|
|
4459
4798
|
}
|
|
4460
4799
|
`
|
|
4461
4800
|
);
|
|
4462
4801
|
}
|
|
4463
4802
|
class GSSplatSorter {
|
|
4464
|
-
constructor(device, splatCount, splatBuffer, cameraBuffer,
|
|
4803
|
+
constructor(device, splatCount, splatBuffer, cameraBuffer, _options = {}) {
|
|
4465
4804
|
__publicField(this, "device");
|
|
4466
4805
|
__publicField(this, "splatCount");
|
|
4467
|
-
//
|
|
4468
|
-
// Buffers
|
|
4469
|
-
// ============================================
|
|
4806
|
+
// Culling Buffers
|
|
4470
4807
|
__publicField(this, "cullingParamsBuffer");
|
|
4471
|
-
__publicField(this, "countersBuffer");
|
|
4472
|
-
__publicField(this, "visibleIndicesBuffer");
|
|
4473
4808
|
__publicField(this, "depthKeysBuffer");
|
|
4474
|
-
__publicField(this, "
|
|
4475
|
-
__publicField(this, "
|
|
4476
|
-
|
|
4809
|
+
__publicField(this, "visibleIndicesBuffer");
|
|
4810
|
+
__publicField(this, "indirectBuffer");
|
|
4811
|
+
// Radix Sort Buffers
|
|
4812
|
+
__publicField(this, "globalHistogramBuffer");
|
|
4813
|
+
__publicField(this, "partitionHistogramBuffer");
|
|
4814
|
+
__publicField(this, "keysTempBuffer");
|
|
4815
|
+
__publicField(this, "valuesTempBuffer");
|
|
4816
|
+
// 每个 pass 独立的参数 buffer (避免竞争)
|
|
4817
|
+
__publicField(this, "sortParamsBuffers", []);
|
|
4818
|
+
// Sorted output
|
|
4477
4819
|
__publicField(this, "sortedIndicesBuffer");
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
// ============================================
|
|
4482
|
-
__publicField(this, "resetCountersPipeline");
|
|
4483
|
-
__publicField(this, "resetBucketCountsPipeline");
|
|
4484
|
-
__publicField(this, "cullAndCountPipeline");
|
|
4485
|
-
__publicField(this, "updateDrawIndirectPipeline");
|
|
4486
|
-
__publicField(this, "prefixSumPipeline");
|
|
4487
|
-
__publicField(this, "resetBucketPositionsPipeline");
|
|
4488
|
-
__publicField(this, "scatterPipeline");
|
|
4489
|
-
// ============================================
|
|
4490
|
-
// Bind Groups
|
|
4491
|
-
// ============================================
|
|
4820
|
+
// Culling Pipelines
|
|
4821
|
+
__publicField(this, "initIndirectPipeline");
|
|
4822
|
+
__publicField(this, "projectCullPipeline");
|
|
4492
4823
|
__publicField(this, "cullingBindGroupLayout");
|
|
4493
4824
|
__publicField(this, "cullingBindGroup");
|
|
4494
|
-
|
|
4495
|
-
__publicField(this, "
|
|
4496
|
-
__publicField(this, "
|
|
4497
|
-
__publicField(this, "
|
|
4498
|
-
|
|
4499
|
-
__publicField(this, "
|
|
4500
|
-
__publicField(this, "
|
|
4501
|
-
//
|
|
4825
|
+
// Radix Sort Pipelines
|
|
4826
|
+
__publicField(this, "upsweepPipeline");
|
|
4827
|
+
__publicField(this, "spinePipeline");
|
|
4828
|
+
__publicField(this, "downsweepPipeline");
|
|
4829
|
+
__publicField(this, "upsweepBindGroupLayout");
|
|
4830
|
+
__publicField(this, "spineBindGroupLayout");
|
|
4831
|
+
__publicField(this, "downsweepBindGroupLayout");
|
|
4832
|
+
// Bind groups for each pass (4 passes)
|
|
4833
|
+
__publicField(this, "upsweepBindGroups", []);
|
|
4834
|
+
__publicField(this, "spineBindGroups", []);
|
|
4835
|
+
__publicField(this, "downsweepBindGroups", []);
|
|
4836
|
+
__publicField(this, "numPartitions");
|
|
4837
|
+
// 屏幕信息和剔除选项
|
|
4502
4838
|
__publicField(this, "screenWidth", 1920);
|
|
4503
4839
|
__publicField(this, "screenHeight", 1080);
|
|
4504
|
-
// 剔除选项
|
|
4505
4840
|
__publicField(this, "cullingOptions", {
|
|
4506
4841
|
nearPlane: 0.1,
|
|
4507
4842
|
farPlane: 1e3,
|
|
4508
|
-
pixelThreshold:
|
|
4843
|
+
pixelThreshold: 0,
|
|
4844
|
+
frustumDilation: 0.2
|
|
4509
4845
|
});
|
|
4510
4846
|
this.device = device;
|
|
4511
4847
|
this.splatCount = splatCount;
|
|
4512
|
-
|
|
4513
|
-
this.numBuckets = options.numBuckets ?? (isIOS ? IOS_NUM_BUCKETS$1 : DEFAULT_NUM_BUCKETS$1);
|
|
4514
|
-
const cullingCode = generateCullingShaderCode$1(this.numBuckets);
|
|
4515
|
-
const prefixSumCode = generatePrefixSumShaderCode$1(this.numBuckets);
|
|
4516
|
-
const scatterCode = generateScatterShaderCode$1(this.numBuckets);
|
|
4848
|
+
this.numPartitions = Math.ceil(splatCount / BLOCK_SIZE);
|
|
4517
4849
|
const cullingModule = device.createShaderModule({
|
|
4518
|
-
code:
|
|
4850
|
+
code: generateCullingShaderCode$1(),
|
|
4519
4851
|
label: "culling-shader"
|
|
4520
4852
|
});
|
|
4521
|
-
const
|
|
4522
|
-
code:
|
|
4523
|
-
label: "
|
|
4524
|
-
});
|
|
4525
|
-
const scatterModule = device.createShaderModule({
|
|
4526
|
-
code: scatterCode,
|
|
4527
|
-
label: "scatter-shader"
|
|
4528
|
-
});
|
|
4529
|
-
cullingModule.getCompilationInfo().then((info) => {
|
|
4530
|
-
if (info.messages.length > 0) ;
|
|
4531
|
-
});
|
|
4532
|
-
prefixSumModule.getCompilationInfo().then((info) => {
|
|
4533
|
-
if (info.messages.length > 0) ;
|
|
4534
|
-
});
|
|
4535
|
-
scatterModule.getCompilationInfo().then((info) => {
|
|
4536
|
-
if (info.messages.length > 0) ;
|
|
4853
|
+
const radixSortModule = device.createShaderModule({
|
|
4854
|
+
code: generateRadixSortShaderCode(),
|
|
4855
|
+
label: "radix-sort-shader"
|
|
4537
4856
|
});
|
|
4538
4857
|
this.cullingParamsBuffer = device.createBuffer({
|
|
4539
4858
|
size: 32,
|
|
4540
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
4859
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
4860
|
+
label: "culling-params"
|
|
4541
4861
|
});
|
|
4542
|
-
this.
|
|
4543
|
-
size:
|
|
4544
|
-
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
4862
|
+
this.depthKeysBuffer = device.createBuffer({
|
|
4863
|
+
size: splatCount * 4,
|
|
4864
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
4865
|
+
label: "depth-keys"
|
|
4545
4866
|
});
|
|
4546
4867
|
this.visibleIndicesBuffer = device.createBuffer({
|
|
4547
4868
|
size: splatCount * 4,
|
|
4548
|
-
usage: GPUBufferUsage.STORAGE
|
|
4869
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
4870
|
+
label: "visible-indices"
|
|
4549
4871
|
});
|
|
4550
|
-
this.
|
|
4551
|
-
size:
|
|
4552
|
-
usage: GPUBufferUsage.STORAGE
|
|
4872
|
+
this.indirectBuffer = device.createBuffer({
|
|
4873
|
+
size: 16,
|
|
4874
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.INDIRECT | GPUBufferUsage.COPY_DST,
|
|
4875
|
+
label: "indirect-buffer"
|
|
4553
4876
|
});
|
|
4554
|
-
this.
|
|
4555
|
-
size:
|
|
4556
|
-
|
|
4877
|
+
this.globalHistogramBuffer = device.createBuffer({
|
|
4878
|
+
size: RADIX_SIZE * 4 * 4,
|
|
4879
|
+
// 4 passes * 256 bins * 4 bytes
|
|
4880
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
4881
|
+
label: "global-histogram"
|
|
4557
4882
|
});
|
|
4558
|
-
this.
|
|
4559
|
-
size: this.
|
|
4560
|
-
usage: GPUBufferUsage.STORAGE
|
|
4883
|
+
this.partitionHistogramBuffer = device.createBuffer({
|
|
4884
|
+
size: this.numPartitions * RADIX_SIZE * 4,
|
|
4885
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
4886
|
+
label: "partition-histogram"
|
|
4561
4887
|
});
|
|
4562
|
-
this.
|
|
4563
|
-
size:
|
|
4564
|
-
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
4888
|
+
this.keysTempBuffer = device.createBuffer({
|
|
4889
|
+
size: splatCount * 4,
|
|
4890
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
4891
|
+
label: "keys-temp"
|
|
4565
4892
|
});
|
|
4566
|
-
this.
|
|
4893
|
+
this.valuesTempBuffer = device.createBuffer({
|
|
4567
4894
|
size: splatCount * 4,
|
|
4568
|
-
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.
|
|
4895
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
4896
|
+
label: "values-temp"
|
|
4569
4897
|
});
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4898
|
+
for (let i = 0; i < 4; i++) {
|
|
4899
|
+
const paramsBuffer = device.createBuffer({
|
|
4900
|
+
size: 16,
|
|
4901
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
4902
|
+
label: `sort-params-${i}`
|
|
4903
|
+
});
|
|
4904
|
+
this.sortParamsBuffers.push(paramsBuffer);
|
|
4905
|
+
const sortParams = new ArrayBuffer(16);
|
|
4906
|
+
const sortView = new DataView(sortParams);
|
|
4907
|
+
sortView.setUint32(0, splatCount, true);
|
|
4908
|
+
sortView.setUint32(4, i * RADIX_BITS, true);
|
|
4909
|
+
sortView.setUint32(8, i, true);
|
|
4910
|
+
sortView.setUint32(12, 0, true);
|
|
4911
|
+
device.queue.writeBuffer(paramsBuffer, 0, sortParams);
|
|
4912
|
+
}
|
|
4913
|
+
this.sortedIndicesBuffer = device.createBuffer({
|
|
4914
|
+
size: splatCount * 4,
|
|
4915
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
|
|
4916
|
+
label: "sorted-indices"
|
|
4573
4917
|
});
|
|
4574
4918
|
this.cullingBindGroupLayout = device.createBindGroupLayout({
|
|
4919
|
+
label: "culling-bind-group-layout",
|
|
4575
4920
|
entries: [
|
|
4576
|
-
{
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
},
|
|
4581
|
-
{
|
|
4582
|
-
binding: 1,
|
|
4583
|
-
visibility: GPUShaderStage.COMPUTE,
|
|
4584
|
-
buffer: { type: "uniform" }
|
|
4585
|
-
},
|
|
4586
|
-
{
|
|
4587
|
-
binding: 2,
|
|
4588
|
-
visibility: GPUShaderStage.COMPUTE,
|
|
4589
|
-
buffer: { type: "uniform" }
|
|
4590
|
-
},
|
|
4591
|
-
{
|
|
4592
|
-
binding: 3,
|
|
4593
|
-
visibility: GPUShaderStage.COMPUTE,
|
|
4594
|
-
buffer: { type: "storage" }
|
|
4595
|
-
},
|
|
4596
|
-
{
|
|
4597
|
-
binding: 4,
|
|
4598
|
-
visibility: GPUShaderStage.COMPUTE,
|
|
4599
|
-
buffer: { type: "storage" }
|
|
4600
|
-
},
|
|
4601
|
-
{
|
|
4602
|
-
binding: 5,
|
|
4603
|
-
visibility: GPUShaderStage.COMPUTE,
|
|
4604
|
-
buffer: { type: "storage" }
|
|
4605
|
-
},
|
|
4606
|
-
{
|
|
4607
|
-
binding: 6,
|
|
4608
|
-
visibility: GPUShaderStage.COMPUTE,
|
|
4609
|
-
buffer: { type: "storage" }
|
|
4610
|
-
},
|
|
4611
|
-
{
|
|
4612
|
-
binding: 7,
|
|
4613
|
-
visibility: GPUShaderStage.COMPUTE,
|
|
4614
|
-
buffer: { type: "storage" }
|
|
4615
|
-
}
|
|
4921
|
+
{ binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
|
|
4922
|
+
{ binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: "uniform" } },
|
|
4923
|
+
{ binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: "uniform" } },
|
|
4924
|
+
{ binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } },
|
|
4925
|
+
{ binding: 4, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } },
|
|
4926
|
+
{ binding: 5, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } }
|
|
4616
4927
|
]
|
|
4617
4928
|
});
|
|
4618
4929
|
const cullingPipelineLayout = device.createPipelineLayout({
|
|
4619
4930
|
bindGroupLayouts: [this.cullingBindGroupLayout]
|
|
4620
4931
|
});
|
|
4621
|
-
this.
|
|
4622
|
-
layout: cullingPipelineLayout,
|
|
4623
|
-
compute: { module: cullingModule, entryPoint: "resetCounters" }
|
|
4624
|
-
});
|
|
4625
|
-
this.resetBucketCountsPipeline = device.createComputePipeline({
|
|
4626
|
-
layout: cullingPipelineLayout,
|
|
4627
|
-
compute: { module: cullingModule, entryPoint: "resetBucketCounts" }
|
|
4628
|
-
});
|
|
4629
|
-
this.cullAndCountPipeline = device.createComputePipeline({
|
|
4932
|
+
this.initIndirectPipeline = device.createComputePipeline({
|
|
4630
4933
|
layout: cullingPipelineLayout,
|
|
4631
|
-
compute: { module: cullingModule, entryPoint: "
|
|
4934
|
+
compute: { module: cullingModule, entryPoint: "initIndirectBuffer" },
|
|
4935
|
+
label: "init-indirect-pipeline"
|
|
4632
4936
|
});
|
|
4633
|
-
this.
|
|
4937
|
+
this.projectCullPipeline = device.createComputePipeline({
|
|
4634
4938
|
layout: cullingPipelineLayout,
|
|
4635
|
-
compute: { module: cullingModule, entryPoint: "
|
|
4636
|
-
|
|
4637
|
-
this.prefixSumBindGroupLayout = device.createBindGroupLayout({
|
|
4638
|
-
entries: [
|
|
4639
|
-
{
|
|
4640
|
-
binding: 0,
|
|
4641
|
-
visibility: GPUShaderStage.COMPUTE,
|
|
4642
|
-
buffer: { type: "storage" }
|
|
4643
|
-
},
|
|
4644
|
-
{
|
|
4645
|
-
binding: 1,
|
|
4646
|
-
visibility: GPUShaderStage.COMPUTE,
|
|
4647
|
-
buffer: { type: "storage" }
|
|
4648
|
-
}
|
|
4649
|
-
]
|
|
4650
|
-
});
|
|
4651
|
-
const prefixSumPipelineLayout = device.createPipelineLayout({
|
|
4652
|
-
bindGroupLayouts: [this.prefixSumBindGroupLayout]
|
|
4653
|
-
});
|
|
4654
|
-
this.prefixSumPipeline = device.createComputePipeline({
|
|
4655
|
-
layout: prefixSumPipelineLayout,
|
|
4656
|
-
compute: { module: prefixSumModule, entryPoint: "prefixSum" }
|
|
4657
|
-
});
|
|
4658
|
-
this.scatterBindGroupLayout = device.createBindGroupLayout({
|
|
4659
|
-
entries: [
|
|
4660
|
-
{
|
|
4661
|
-
binding: 0,
|
|
4662
|
-
visibility: GPUShaderStage.COMPUTE,
|
|
4663
|
-
buffer: { type: "read-only-storage" }
|
|
4664
|
-
},
|
|
4665
|
-
{
|
|
4666
|
-
binding: 1,
|
|
4667
|
-
visibility: GPUShaderStage.COMPUTE,
|
|
4668
|
-
buffer: { type: "read-only-storage" }
|
|
4669
|
-
},
|
|
4670
|
-
{
|
|
4671
|
-
binding: 2,
|
|
4672
|
-
visibility: GPUShaderStage.COMPUTE,
|
|
4673
|
-
buffer: { type: "read-only-storage" }
|
|
4674
|
-
},
|
|
4675
|
-
{
|
|
4676
|
-
binding: 3,
|
|
4677
|
-
visibility: GPUShaderStage.COMPUTE,
|
|
4678
|
-
buffer: { type: "storage" }
|
|
4679
|
-
},
|
|
4680
|
-
{
|
|
4681
|
-
binding: 4,
|
|
4682
|
-
visibility: GPUShaderStage.COMPUTE,
|
|
4683
|
-
buffer: { type: "storage" }
|
|
4684
|
-
},
|
|
4685
|
-
{
|
|
4686
|
-
binding: 5,
|
|
4687
|
-
visibility: GPUShaderStage.COMPUTE,
|
|
4688
|
-
buffer: { type: "read-only-storage" }
|
|
4689
|
-
}
|
|
4690
|
-
// countersBuffer
|
|
4691
|
-
]
|
|
4692
|
-
});
|
|
4693
|
-
const scatterPipelineLayout = device.createPipelineLayout({
|
|
4694
|
-
bindGroupLayouts: [this.scatterBindGroupLayout]
|
|
4695
|
-
});
|
|
4696
|
-
this.resetBucketPositionsPipeline = device.createComputePipeline({
|
|
4697
|
-
layout: scatterPipelineLayout,
|
|
4698
|
-
compute: { module: scatterModule, entryPoint: "resetBucketPositions" }
|
|
4699
|
-
});
|
|
4700
|
-
this.scatterPipeline = device.createComputePipeline({
|
|
4701
|
-
layout: scatterPipelineLayout,
|
|
4702
|
-
compute: { module: scatterModule, entryPoint: "scatter" }
|
|
4939
|
+
compute: { module: cullingModule, entryPoint: "projectAndCull" },
|
|
4940
|
+
label: "project-cull-pipeline"
|
|
4703
4941
|
});
|
|
4704
4942
|
this.cullingBindGroup = device.createBindGroup({
|
|
4705
4943
|
layout: this.cullingBindGroupLayout,
|
|
@@ -4707,127 +4945,196 @@ class GSSplatSorter {
|
|
|
4707
4945
|
{ binding: 0, resource: { buffer: splatBuffer } },
|
|
4708
4946
|
{ binding: 1, resource: { buffer: cameraBuffer } },
|
|
4709
4947
|
{ binding: 2, resource: { buffer: this.cullingParamsBuffer } },
|
|
4710
|
-
{ binding: 3, resource: { buffer: this.
|
|
4948
|
+
{ binding: 3, resource: { buffer: this.depthKeysBuffer } },
|
|
4711
4949
|
{ binding: 4, resource: { buffer: this.visibleIndicesBuffer } },
|
|
4712
|
-
{ binding: 5, resource: { buffer: this.
|
|
4713
|
-
|
|
4714
|
-
|
|
4950
|
+
{ binding: 5, resource: { buffer: this.indirectBuffer } }
|
|
4951
|
+
],
|
|
4952
|
+
label: "culling-bind-group"
|
|
4953
|
+
});
|
|
4954
|
+
this.upsweepBindGroupLayout = device.createBindGroupLayout({
|
|
4955
|
+
label: "upsweep-layout",
|
|
4956
|
+
entries: [
|
|
4957
|
+
{ binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "uniform" } },
|
|
4958
|
+
{ binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
|
|
4959
|
+
{ binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
|
|
4960
|
+
{ binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } },
|
|
4961
|
+
{ binding: 4, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } }
|
|
4715
4962
|
]
|
|
4716
4963
|
});
|
|
4717
|
-
this.
|
|
4718
|
-
|
|
4964
|
+
this.spineBindGroupLayout = device.createBindGroupLayout({
|
|
4965
|
+
label: "spine-layout",
|
|
4719
4966
|
entries: [
|
|
4720
|
-
{ binding: 0,
|
|
4721
|
-
{ binding: 1,
|
|
4967
|
+
{ binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
|
|
4968
|
+
{ binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } },
|
|
4969
|
+
{ binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } },
|
|
4970
|
+
{ binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: "uniform" } }
|
|
4722
4971
|
]
|
|
4723
4972
|
});
|
|
4724
|
-
this.
|
|
4725
|
-
|
|
4973
|
+
this.downsweepBindGroupLayout = device.createBindGroupLayout({
|
|
4974
|
+
label: "downsweep-layout",
|
|
4726
4975
|
entries: [
|
|
4727
|
-
{ binding: 0,
|
|
4728
|
-
{ binding: 1,
|
|
4729
|
-
{ binding: 2,
|
|
4730
|
-
{ binding: 3,
|
|
4731
|
-
{ binding: 4,
|
|
4732
|
-
{ binding: 5,
|
|
4733
|
-
|
|
4976
|
+
{ binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "uniform" } },
|
|
4977
|
+
{ binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
|
|
4978
|
+
{ binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
|
|
4979
|
+
{ binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
|
|
4980
|
+
{ binding: 4, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
|
|
4981
|
+
{ binding: 5, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
|
|
4982
|
+
{ binding: 6, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } },
|
|
4983
|
+
{ binding: 7, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } }
|
|
4734
4984
|
]
|
|
4735
4985
|
});
|
|
4986
|
+
this.upsweepPipeline = device.createComputePipeline({
|
|
4987
|
+
layout: device.createPipelineLayout({ bindGroupLayouts: [this.upsweepBindGroupLayout] }),
|
|
4988
|
+
compute: { module: radixSortModule, entryPoint: "upsweep" },
|
|
4989
|
+
label: "upsweep-pipeline"
|
|
4990
|
+
});
|
|
4991
|
+
this.spinePipeline = device.createComputePipeline({
|
|
4992
|
+
layout: device.createPipelineLayout({ bindGroupLayouts: [this.spineBindGroupLayout] }),
|
|
4993
|
+
compute: { module: radixSortModule, entryPoint: "spine" },
|
|
4994
|
+
label: "spine-pipeline"
|
|
4995
|
+
});
|
|
4996
|
+
this.downsweepPipeline = device.createComputePipeline({
|
|
4997
|
+
layout: device.createPipelineLayout({ bindGroupLayouts: [this.downsweepBindGroupLayout] }),
|
|
4998
|
+
compute: { module: radixSortModule, entryPoint: "downsweep" },
|
|
4999
|
+
label: "downsweep-pipeline"
|
|
5000
|
+
});
|
|
5001
|
+
this.createRadixSortBindGroups();
|
|
4736
5002
|
}
|
|
4737
5003
|
/**
|
|
4738
|
-
*
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
*
|
|
4746
|
-
*/
|
|
4747
|
-
|
|
4748
|
-
|
|
5004
|
+
* 创建 Radix Sort 的 bind groups
|
|
5005
|
+
* 4 个 pass,使用 ping-pong buffers
|
|
5006
|
+
*
|
|
5007
|
+
* Ping-pong 模式:
|
|
5008
|
+
* - Pass 0: depthKeys/visibleIndices -> keysTempBuffer/valuesTempBuffer
|
|
5009
|
+
* - Pass 1: keysTempBuffer/valuesTempBuffer -> depthKeys/visibleIndices
|
|
5010
|
+
* - Pass 2: depthKeys/visibleIndices -> keysTempBuffer/valuesTempBuffer
|
|
5011
|
+
* - Pass 3: keysTempBuffer/valuesTempBuffer -> (depthKeys)/sortedIndicesBuffer
|
|
5012
|
+
*/
|
|
5013
|
+
createRadixSortBindGroups() {
|
|
5014
|
+
for (let passIdx = 0; passIdx < 4; passIdx++) {
|
|
5015
|
+
const isEvenPass = passIdx % 2 === 0;
|
|
5016
|
+
const keysIn = isEvenPass ? this.depthKeysBuffer : this.keysTempBuffer;
|
|
5017
|
+
const valuesIn = isEvenPass ? this.visibleIndicesBuffer : this.valuesTempBuffer;
|
|
5018
|
+
let keysOut;
|
|
5019
|
+
let valuesOut;
|
|
5020
|
+
if (isEvenPass) {
|
|
5021
|
+
keysOut = this.keysTempBuffer;
|
|
5022
|
+
valuesOut = this.valuesTempBuffer;
|
|
5023
|
+
} else {
|
|
5024
|
+
keysOut = this.depthKeysBuffer;
|
|
5025
|
+
valuesOut = passIdx === 3 ? this.sortedIndicesBuffer : this.visibleIndicesBuffer;
|
|
5026
|
+
}
|
|
5027
|
+
this.upsweepBindGroups[passIdx] = this.device.createBindGroup({
|
|
5028
|
+
layout: this.upsweepBindGroupLayout,
|
|
5029
|
+
entries: [
|
|
5030
|
+
{ binding: 0, resource: { buffer: this.sortParamsBuffers[passIdx] } },
|
|
5031
|
+
{ binding: 1, resource: { buffer: this.indirectBuffer } },
|
|
5032
|
+
{ binding: 2, resource: { buffer: keysIn } },
|
|
5033
|
+
{ binding: 3, resource: { buffer: this.globalHistogramBuffer } },
|
|
5034
|
+
{ binding: 4, resource: { buffer: this.partitionHistogramBuffer } }
|
|
5035
|
+
],
|
|
5036
|
+
label: `upsweep-bind-group-${passIdx}`
|
|
5037
|
+
});
|
|
5038
|
+
this.spineBindGroups[passIdx] = this.device.createBindGroup({
|
|
5039
|
+
layout: this.spineBindGroupLayout,
|
|
5040
|
+
entries: [
|
|
5041
|
+
{ binding: 0, resource: { buffer: this.indirectBuffer } },
|
|
5042
|
+
{ binding: 1, resource: { buffer: this.globalHistogramBuffer } },
|
|
5043
|
+
{ binding: 2, resource: { buffer: this.partitionHistogramBuffer } },
|
|
5044
|
+
{ binding: 3, resource: { buffer: this.sortParamsBuffers[passIdx] } }
|
|
5045
|
+
],
|
|
5046
|
+
label: `spine-bind-group-${passIdx}`
|
|
5047
|
+
});
|
|
5048
|
+
this.downsweepBindGroups[passIdx] = this.device.createBindGroup({
|
|
5049
|
+
layout: this.downsweepBindGroupLayout,
|
|
5050
|
+
entries: [
|
|
5051
|
+
{ binding: 0, resource: { buffer: this.sortParamsBuffers[passIdx] } },
|
|
5052
|
+
{ binding: 1, resource: { buffer: this.indirectBuffer } },
|
|
5053
|
+
{ binding: 2, resource: { buffer: this.globalHistogramBuffer } },
|
|
5054
|
+
{ binding: 3, resource: { buffer: this.partitionHistogramBuffer } },
|
|
5055
|
+
{ binding: 4, resource: { buffer: keysIn } },
|
|
5056
|
+
{ binding: 5, resource: { buffer: valuesIn } },
|
|
5057
|
+
{ binding: 6, resource: { buffer: keysOut } },
|
|
5058
|
+
{ binding: 7, resource: { buffer: valuesOut } }
|
|
5059
|
+
],
|
|
5060
|
+
label: `downsweep-bind-group-${passIdx}`
|
|
5061
|
+
});
|
|
5062
|
+
}
|
|
5063
|
+
}
|
|
5064
|
+
/**
|
|
5065
|
+
* 设置屏幕尺寸
|
|
5066
|
+
*/
|
|
5067
|
+
setScreenSize(width, height) {
|
|
5068
|
+
this.screenWidth = width;
|
|
5069
|
+
this.screenHeight = height;
|
|
5070
|
+
}
|
|
5071
|
+
/**
|
|
5072
|
+
* 设置剔除参数
|
|
5073
|
+
*/
|
|
5074
|
+
setCullingOptions(options) {
|
|
5075
|
+
this.cullingOptions = { ...this.cullingOptions, ...options };
|
|
4749
5076
|
}
|
|
4750
5077
|
/**
|
|
4751
5078
|
* 执行剔除和排序
|
|
4752
5079
|
* 每帧调用
|
|
4753
|
-
*
|
|
4754
|
-
* 优化:合并所有 compute pass 到单次 GPU 提交
|
|
4755
|
-
* WebGPU 保证同一 command buffer 中的命令按顺序执行
|
|
4756
5080
|
*/
|
|
4757
5081
|
sort() {
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
);
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
pass.end();
|
|
4792
|
-
}
|
|
4793
|
-
{
|
|
4794
|
-
const pass = encoder.beginComputePass();
|
|
4795
|
-
pass.setPipeline(this.cullAndCountPipeline);
|
|
4796
|
-
pass.setBindGroup(0, this.cullingBindGroup);
|
|
4797
|
-
pass.dispatchWorkgroups(cullWorkgroupCount);
|
|
4798
|
-
pass.end();
|
|
4799
|
-
}
|
|
4800
|
-
{
|
|
4801
|
-
const pass = encoder.beginComputePass();
|
|
4802
|
-
pass.setPipeline(this.updateDrawIndirectPipeline);
|
|
4803
|
-
pass.setBindGroup(0, this.cullingBindGroup);
|
|
4804
|
-
pass.dispatchWorkgroups(1);
|
|
4805
|
-
pass.end();
|
|
4806
|
-
}
|
|
5082
|
+
const cullingParamsData = new ArrayBuffer(32);
|
|
5083
|
+
const view = new DataView(cullingParamsData);
|
|
5084
|
+
view.setUint32(0, this.splatCount, true);
|
|
5085
|
+
view.setFloat32(4, this.cullingOptions.nearPlane, true);
|
|
5086
|
+
view.setFloat32(8, this.cullingOptions.farPlane, true);
|
|
5087
|
+
view.setFloat32(12, this.screenWidth, true);
|
|
5088
|
+
view.setFloat32(16, this.screenHeight, true);
|
|
5089
|
+
view.setFloat32(20, this.cullingOptions.frustumDilation ?? 0.2, true);
|
|
5090
|
+
view.setFloat32(24, this.cullingOptions.pixelThreshold, true);
|
|
5091
|
+
view.setFloat32(28, 0, true);
|
|
5092
|
+
this.device.queue.writeBuffer(this.cullingParamsBuffer, 0, cullingParamsData);
|
|
5093
|
+
const encoder = this.device.createCommandEncoder({ label: "splat-sort-encoder" });
|
|
5094
|
+
encoder.clearBuffer(this.depthKeysBuffer);
|
|
5095
|
+
encoder.clearBuffer(this.visibleIndicesBuffer);
|
|
5096
|
+
encoder.clearBuffer(this.keysTempBuffer);
|
|
5097
|
+
encoder.clearBuffer(this.valuesTempBuffer);
|
|
5098
|
+
encoder.clearBuffer(this.globalHistogramBuffer);
|
|
5099
|
+
encoder.clearBuffer(this.partitionHistogramBuffer);
|
|
5100
|
+
{
|
|
5101
|
+
const pass = encoder.beginComputePass({ label: "init-indirect" });
|
|
5102
|
+
pass.setPipeline(this.initIndirectPipeline);
|
|
5103
|
+
pass.setBindGroup(0, this.cullingBindGroup);
|
|
5104
|
+
pass.dispatchWorkgroups(1);
|
|
5105
|
+
pass.end();
|
|
5106
|
+
}
|
|
5107
|
+
{
|
|
5108
|
+
const pass = encoder.beginComputePass({ label: "project-cull" });
|
|
5109
|
+
pass.setPipeline(this.projectCullPipeline);
|
|
5110
|
+
pass.setBindGroup(0, this.cullingBindGroup);
|
|
5111
|
+
pass.dispatchWorkgroups(Math.ceil(this.splatCount / WORKGROUP_SIZE$1));
|
|
5112
|
+
pass.end();
|
|
5113
|
+
}
|
|
5114
|
+
for (let passIdx = 0; passIdx < 4; passIdx++) {
|
|
4807
5115
|
{
|
|
4808
|
-
const pass = encoder.beginComputePass();
|
|
4809
|
-
pass.setPipeline(this.
|
|
4810
|
-
pass.setBindGroup(0, this.
|
|
4811
|
-
pass.dispatchWorkgroups(
|
|
5116
|
+
const pass = encoder.beginComputePass({ label: `upsweep-p${passIdx}` });
|
|
5117
|
+
pass.setPipeline(this.upsweepPipeline);
|
|
5118
|
+
pass.setBindGroup(0, this.upsweepBindGroups[passIdx]);
|
|
5119
|
+
pass.dispatchWorkgroups(this.numPartitions);
|
|
4812
5120
|
pass.end();
|
|
4813
5121
|
}
|
|
4814
5122
|
{
|
|
4815
|
-
const pass = encoder.beginComputePass();
|
|
4816
|
-
pass.setPipeline(this.
|
|
4817
|
-
pass.setBindGroup(0, this.
|
|
4818
|
-
pass.dispatchWorkgroups(
|
|
5123
|
+
const pass = encoder.beginComputePass({ label: `spine-p${passIdx}` });
|
|
5124
|
+
pass.setPipeline(this.spinePipeline);
|
|
5125
|
+
pass.setBindGroup(0, this.spineBindGroups[passIdx]);
|
|
5126
|
+
pass.dispatchWorkgroups(RADIX_SIZE);
|
|
4819
5127
|
pass.end();
|
|
4820
5128
|
}
|
|
4821
5129
|
{
|
|
4822
|
-
const pass = encoder.beginComputePass();
|
|
4823
|
-
pass.setPipeline(this.
|
|
4824
|
-
pass.setBindGroup(0, this.
|
|
4825
|
-
pass.dispatchWorkgroups(
|
|
5130
|
+
const pass = encoder.beginComputePass({ label: `downsweep-p${passIdx}` });
|
|
5131
|
+
pass.setPipeline(this.downsweepPipeline);
|
|
5132
|
+
pass.setBindGroup(0, this.downsweepBindGroups[passIdx]);
|
|
5133
|
+
pass.dispatchWorkgroups(this.numPartitions);
|
|
4826
5134
|
pass.end();
|
|
4827
5135
|
}
|
|
4828
|
-
this.device.queue.submit([encoder.finish()]);
|
|
4829
|
-
} catch (error) {
|
|
4830
5136
|
}
|
|
5137
|
+
this.device.queue.submit([encoder.finish()]);
|
|
4831
5138
|
}
|
|
4832
5139
|
/**
|
|
4833
5140
|
* 获取排序后的索引 buffer(用于渲染)
|
|
@@ -4839,7 +5146,7 @@ class GSSplatSorter {
|
|
|
4839
5146
|
* 获取 DrawIndirect buffer
|
|
4840
5147
|
*/
|
|
4841
5148
|
getDrawIndirectBuffer() {
|
|
4842
|
-
return this.
|
|
5149
|
+
return this.indirectBuffer;
|
|
4843
5150
|
}
|
|
4844
5151
|
/**
|
|
4845
5152
|
* 获取 splat 总数量
|
|
@@ -4852,58 +5159,37 @@ class GSSplatSorter {
|
|
|
4852
5159
|
*/
|
|
4853
5160
|
destroy() {
|
|
4854
5161
|
this.cullingParamsBuffer.destroy();
|
|
4855
|
-
this.countersBuffer.destroy();
|
|
4856
|
-
this.visibleIndicesBuffer.destroy();
|
|
4857
5162
|
this.depthKeysBuffer.destroy();
|
|
4858
|
-
this.
|
|
4859
|
-
this.
|
|
4860
|
-
this.
|
|
5163
|
+
this.visibleIndicesBuffer.destroy();
|
|
5164
|
+
this.indirectBuffer.destroy();
|
|
5165
|
+
this.globalHistogramBuffer.destroy();
|
|
5166
|
+
this.partitionHistogramBuffer.destroy();
|
|
5167
|
+
this.keysTempBuffer.destroy();
|
|
5168
|
+
this.valuesTempBuffer.destroy();
|
|
5169
|
+
for (const buffer of this.sortParamsBuffers) {
|
|
5170
|
+
buffer.destroy();
|
|
5171
|
+
}
|
|
4861
5172
|
this.sortedIndicesBuffer.destroy();
|
|
4862
|
-
this.drawIndirectBuffer.destroy();
|
|
4863
5173
|
}
|
|
4864
5174
|
}
|
|
4865
|
-
|
|
4866
|
-
SHMode2[SHMode2["L0"] = 0] = "L0";
|
|
4867
|
-
SHMode2[SHMode2["L1"] = 1] = "L1";
|
|
4868
|
-
SHMode2[SHMode2["L2"] = 2] = "L2";
|
|
4869
|
-
SHMode2[SHMode2["L3"] = 3] = "L3";
|
|
4870
|
-
return SHMode2;
|
|
4871
|
-
})(SHMode$1 || {});
|
|
4872
|
-
function isMobileDevice$1() {
|
|
4873
|
-
if (typeof navigator === "undefined") return false;
|
|
4874
|
-
const ua = navigator.userAgent || navigator.vendor || window.opera || "";
|
|
4875
|
-
const isMobileUA = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(
|
|
4876
|
-
ua.toLowerCase()
|
|
4877
|
-
);
|
|
4878
|
-
const hasTouch = "ontouchstart" in window || navigator.maxTouchPoints > 0;
|
|
4879
|
-
const isSmallScreen = window.innerWidth <= 768;
|
|
4880
|
-
const isIPadAsMac = navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1;
|
|
4881
|
-
const result = isMobileUA || isIPadAsMac || hasTouch && isSmallScreen;
|
|
4882
|
-
return result;
|
|
4883
|
-
}
|
|
4884
|
-
var PerformanceTier = /* @__PURE__ */ ((PerformanceTier2) => {
|
|
4885
|
-
PerformanceTier2["HIGH"] = "high";
|
|
4886
|
-
PerformanceTier2["MEDIUM"] = "medium";
|
|
4887
|
-
PerformanceTier2["LOW"] = "low";
|
|
4888
|
-
return PerformanceTier2;
|
|
4889
|
-
})(PerformanceTier || {});
|
|
4890
|
-
function detectPerformanceTier(device) {
|
|
4891
|
-
const isMobile = isMobileDevice$1();
|
|
4892
|
-
device.limits.maxBufferSize;
|
|
4893
|
-
const maxStorageBufferBindingSize = device.limits.maxStorageBufferBindingSize;
|
|
4894
|
-
if (isMobile) {
|
|
4895
|
-
return "low";
|
|
4896
|
-
}
|
|
4897
|
-
if (maxStorageBufferBindingSize >= 1024 * 1024 * 1024) {
|
|
4898
|
-
return "high";
|
|
4899
|
-
} else if (maxStorageBufferBindingSize >= 256 * 1024 * 1024) {
|
|
4900
|
-
return "medium";
|
|
4901
|
-
}
|
|
4902
|
-
return "low";
|
|
4903
|
-
}
|
|
4904
|
-
const shaderCodeL0 = (
|
|
5175
|
+
const gsOptimizedShader = (
|
|
4905
5176
|
/* wgsl */
|
|
4906
5177
|
`
|
|
5178
|
+
/**
|
|
5179
|
+
* 优化的 3D Gaussian Splatting Shader
|
|
5180
|
+
* 参考 rfs-gsplat-render 实现,修复颜色和抗锯齿问题
|
|
5181
|
+
*/
|
|
5182
|
+
|
|
5183
|
+
const SQRT_8: f32 = 2.82842712475;
|
|
5184
|
+
const SH_C0: f32 = 0.28209479177387814;
|
|
5185
|
+
const SH_C1: f32 = 0.4886025119029199;
|
|
5186
|
+
// Normalized Gaussian 常量 (匹配 SuperSplat)
|
|
5187
|
+
const EXP_NEG4: f32 = 0.01831563888873418;
|
|
5188
|
+
const INV_ONE_MINUS_EXP_NEG4: f32 = 1.01865736036377408;
|
|
5189
|
+
// 低通滤波器 (正则化协方差矩阵)
|
|
5190
|
+
const LOW_PASS_FILTER: f32 = 0.3;
|
|
5191
|
+
const ALPHA_CULL_THRESHOLD: f32 = 0.00392156863;
|
|
5192
|
+
|
|
4907
5193
|
struct Uniforms {
|
|
4908
5194
|
view: mat4x4<f32>,
|
|
4909
5195
|
proj: mat4x4<f32>,
|
|
@@ -4915,17 +5201,15 @@ struct Uniforms {
|
|
|
4915
5201
|
}
|
|
4916
5202
|
|
|
4917
5203
|
struct Splat {
|
|
4918
|
-
mean:
|
|
4919
|
-
|
|
4920
|
-
scale: vec3<f32>,
|
|
4921
|
-
_pad1: f32,
|
|
5204
|
+
mean: vec3<f32>, _pad0: f32,
|
|
5205
|
+
scale: vec3<f32>, _pad1: f32,
|
|
4922
5206
|
rotation: vec4<f32>,
|
|
4923
|
-
colorDC:
|
|
4924
|
-
opacity:
|
|
4925
|
-
sh1:
|
|
4926
|
-
sh2:
|
|
4927
|
-
sh3:
|
|
4928
|
-
_pad2:
|
|
5207
|
+
colorDC: vec3<f32>,
|
|
5208
|
+
opacity: f32,
|
|
5209
|
+
sh1: array<f32, 9>,
|
|
5210
|
+
sh2: array<f32, 15>,
|
|
5211
|
+
sh3: array<f32, 21>,
|
|
5212
|
+
_pad2: array<f32, 3>,
|
|
4929
5213
|
}
|
|
4930
5214
|
|
|
4931
5215
|
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
|
|
@@ -4934,100 +5218,137 @@ struct Splat {
|
|
|
4934
5218
|
|
|
4935
5219
|
struct VertexOutput {
|
|
4936
5220
|
@builtin(position) position: vec4<f32>,
|
|
4937
|
-
@location(0)
|
|
5221
|
+
@location(0) fragPos: vec2<f32>,
|
|
4938
5222
|
@location(1) color: vec3<f32>,
|
|
4939
5223
|
@location(2) opacity: f32,
|
|
4940
5224
|
}
|
|
4941
5225
|
|
|
4942
5226
|
const QUAD_POSITIONS = array<vec2<f32>, 4>(
|
|
4943
|
-
vec2<f32>(-1.0, -1.0),
|
|
4944
|
-
vec2<f32>(
|
|
4945
|
-
vec2<f32>(-1.0, 1.0),
|
|
4946
|
-
vec2<f32>( 1.0, 1.0),
|
|
5227
|
+
vec2<f32>(-1.0, -1.0), vec2<f32>(-1.0, 1.0),
|
|
5228
|
+
vec2<f32>(1.0, -1.0), vec2<f32>(1.0, 1.0),
|
|
4947
5229
|
);
|
|
4948
5230
|
|
|
4949
|
-
//
|
|
4950
|
-
//
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
//
|
|
4955
|
-
|
|
5231
|
+
// ClipCorner 优化 (精确匹配 PlayCanvas/SuperSplat)
|
|
5232
|
+
// 从 PlayCanvas: clip = min(1.0, sqrt(-log(1.0 / (255.0 * alpha))) / 2.0)
|
|
5233
|
+
// 这根据透明度缩小 quad,排除 alpha < 1/255 的 Gaussian 区域
|
|
5234
|
+
fn computeClipFactor(alpha: f32) -> f32 {
|
|
5235
|
+
// 保护非常小的 alpha 值
|
|
5236
|
+
// 当 alpha <= 1/255 时,splat 不可见
|
|
5237
|
+
if alpha <= ALPHA_CULL_THRESHOLD { return 0.0; }
|
|
5238
|
+
// PlayCanvas 公式: clip = min(1.0, sqrt(-log(1.0 / (255.0 * alpha))) / 2.0)
|
|
5239
|
+
// 简化: -log(1/(255*a)) = log(255*a)
|
|
5240
|
+
return min(1.0, sqrt(log(255.0 * alpha)) / 2.0);
|
|
5241
|
+
}
|
|
4956
5242
|
|
|
5243
|
+
// 四元数转旋转矩阵 (PLY 格式: w, x, y, z)
|
|
4957
5244
|
fn quatToMat3(q: vec4<f32>) -> mat3x3<f32> {
|
|
4958
|
-
let
|
|
4959
|
-
let x2 = x + x; let y2 = y + y; let z2 = z + z;
|
|
4960
|
-
let xx = x * x2; let xy = x * y2; let xz = x * z2;
|
|
4961
|
-
let yy = y * y2; let yz = y * z2; let zz = z * z2;
|
|
4962
|
-
let wx = w * x2; let wy = w * y2; let wz = w * z2;
|
|
5245
|
+
let r = q.x; let x = q.y; let y = q.z; let z = q.w;
|
|
4963
5246
|
return mat3x3<f32>(
|
|
4964
|
-
vec3<f32>(1.0 - (
|
|
4965
|
-
vec3<f32>(
|
|
4966
|
-
vec3<f32>(
|
|
5247
|
+
vec3<f32>(1.0 - 2.0 * (y * y + z * z), 2.0 * (x * y + r * z), 2.0 * (x * z - r * y)),
|
|
5248
|
+
vec3<f32>(2.0 * (x * y - r * z), 1.0 - 2.0 * (x * x + z * z), 2.0 * (y * z + r * x)),
|
|
5249
|
+
vec3<f32>(2.0 * (x * z + r * y), 2.0 * (y * z - r * x), 1.0 - 2.0 * (x * x + y * y))
|
|
4967
5250
|
);
|
|
4968
5251
|
}
|
|
4969
5252
|
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
length(model[2].xyz)
|
|
4976
|
-
);
|
|
5253
|
+
fn computeCovariance3D(scale: vec3<f32>, rotation: vec4<f32>) -> mat3x3<f32> {
|
|
5254
|
+
let R = quatToMat3(rotation);
|
|
5255
|
+
let S = mat3x3<f32>(vec3<f32>(scale.x, 0.0, 0.0), vec3<f32>(0.0, scale.y, 0.0), vec3<f32>(0.0, 0.0, scale.z));
|
|
5256
|
+
let M = R * S;
|
|
5257
|
+
return M * transpose(M);
|
|
4977
5258
|
}
|
|
4978
5259
|
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
let
|
|
4983
|
-
let
|
|
4984
|
-
// 构建协方差矩阵 Sigma = R * diag(s²) * R^T
|
|
4985
|
-
let M = mat3x3<f32>(R[0] * s2.x, R[1] * s2.y, R[2] * s2.z);
|
|
4986
|
-
let Sigma = M * transpose(R);
|
|
4987
|
-
|
|
4988
|
-
let viewPos = (modelView * vec4<f32>(mean, 1.0)).xyz;
|
|
4989
|
-
let viewRot = mat3x3<f32>(modelView[0].xyz, modelView[1].xyz, modelView[2].xyz);
|
|
4990
|
-
let SigmaView = viewRot * Sigma * transpose(viewRot);
|
|
5260
|
+
// 协方差投影 (匹配参考实现)
|
|
5261
|
+
// 注意: viewCenter 是 vec4,直接使用 .xyz (不除以 w)
|
|
5262
|
+
fn projectCovariance(cov3d: mat3x3<f32>, viewCenter: vec4<f32>, focal: vec2<f32>, modelViewMat: mat4x4<f32>) -> vec3<f32> {
|
|
5263
|
+
let v = viewCenter.xyz; // 直接使用,不除以 w
|
|
5264
|
+
let s = 1.0 / (v.z * v.z);
|
|
4991
5265
|
|
|
4992
|
-
|
|
4993
|
-
let
|
|
4994
|
-
|
|
4995
|
-
|
|
5266
|
+
// Jacobian 矩阵
|
|
5267
|
+
let J = mat3x3<f32>(
|
|
5268
|
+
vec3<f32>(focal.x / v.z, 0.0, 0.0),
|
|
5269
|
+
vec3<f32>(0.0, focal.y / v.z, 0.0),
|
|
5270
|
+
vec3<f32>(-(focal.x * v.x) * s, -(focal.y * v.y) * s, 0.0)
|
|
5271
|
+
);
|
|
4996
5272
|
|
|
4997
|
-
//
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5273
|
+
// 从 model-view 矩阵提取 3x3 旋转部分 (匹配参考实现)
|
|
5274
|
+
let W = mat3x3<f32>(
|
|
5275
|
+
vec3<f32>(modelViewMat[0][0], modelViewMat[0][1], modelViewMat[0][2]),
|
|
5276
|
+
vec3<f32>(modelViewMat[1][0], modelViewMat[1][1], modelViewMat[1][2]),
|
|
5277
|
+
vec3<f32>(modelViewMat[2][0], modelViewMat[2][1], modelViewMat[2][2])
|
|
5278
|
+
);
|
|
5001
5279
|
|
|
5002
|
-
|
|
5003
|
-
let
|
|
5004
|
-
|
|
5005
|
-
return vec3<f32>(dot(j1, Sj1), dot(j1, Sj2), dot(j2, Sj2));
|
|
5280
|
+
let T = J * W;
|
|
5281
|
+
let cov2d = T * cov3d * transpose(T);
|
|
5282
|
+
return vec3<f32>(cov2d[0][0], cov2d[0][1], cov2d[1][1]);
|
|
5006
5283
|
}
|
|
5007
5284
|
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5285
|
+
struct ExtentResult {
|
|
5286
|
+
basis: vec4<f32>,
|
|
5287
|
+
adjustedOpacity: f32,
|
|
5288
|
+
}
|
|
5289
|
+
|
|
5290
|
+
// 计算 2D 投影范围
|
|
5291
|
+
// 精确匹配 PlayCanvas/SuperSplat 实现
|
|
5292
|
+
// 注意: MipSplatting 抗锯齿默认禁用,因为大多数模型不是用 MipSplatting 训练的
|
|
5293
|
+
// 如果模型是用 MipSplatting 训练的,可以启用 GSPLAT_AA 模式
|
|
5294
|
+
fn computeExtentBasisAA(cov2dIn: vec3<f32>, opacity: f32, viewportSize: vec2<f32>) -> ExtentResult {
|
|
5295
|
+
var result: ExtentResult;
|
|
5296
|
+
var cov2d = cov2dIn;
|
|
5297
|
+
var alpha = opacity;
|
|
5018
5298
|
|
|
5019
|
-
|
|
5020
|
-
//
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5299
|
+
// 添加低通滤波 (正则化) - 匹配 PlayCanvas: +0.3
|
|
5300
|
+
// 这避免了非常小的特征值导致的数值问题
|
|
5301
|
+
cov2d.x += LOW_PASS_FILTER;
|
|
5302
|
+
cov2d.z += LOW_PASS_FILTER;
|
|
5303
|
+
|
|
5304
|
+
// 特征值分解 (使用 PlayCanvas 公式)
|
|
5305
|
+
let a = cov2d.x; // diagonal1
|
|
5306
|
+
let d = cov2d.z; // diagonal2
|
|
5307
|
+
let b = cov2d.y; // offDiagonal
|
|
5308
|
+
|
|
5309
|
+
let mid = 0.5 * (a + d);
|
|
5310
|
+
let radius = length(vec2<f32>((a - d) * 0.5, b));
|
|
5311
|
+
|
|
5312
|
+
let lambda1 = mid + radius;
|
|
5313
|
+
let lambda2 = max(mid - radius, 0.1); // PlayCanvas 使用 0.1 最小值
|
|
5314
|
+
|
|
5315
|
+
// 检查特征值是否有效
|
|
5316
|
+
if lambda2 <= 0.0 {
|
|
5317
|
+
result.basis = vec4<f32>(0.0);
|
|
5318
|
+
result.adjustedOpacity = 0.0;
|
|
5319
|
+
return result;
|
|
5029
5320
|
}
|
|
5030
|
-
|
|
5321
|
+
|
|
5322
|
+
// 使用基于视口的最大限制 (匹配 PlayCanvas)
|
|
5323
|
+
let vmin = min(1024.0, min(viewportSize.x, viewportSize.y));
|
|
5324
|
+
|
|
5325
|
+
// 计算轴长度: l = 2.0 * min(sqrt(2.0 * lambda), vmin)
|
|
5326
|
+
// 这等价于 std_dev * sqrt(lambda),因为 std_dev = sqrt(8) ≈ 2.83
|
|
5327
|
+
let l1 = 2.0 * min(sqrt(2.0 * lambda1), vmin);
|
|
5328
|
+
let l2 = 2.0 * min(sqrt(2.0 * lambda2), vmin);
|
|
5329
|
+
|
|
5330
|
+
// 关键: 剔除小于 2 像素的 Gaussian (匹配 PlayCanvas)
|
|
5331
|
+
// 这消除了导致"雾化"伪影的亚像素 splat
|
|
5332
|
+
if l1 < 2.0 && l2 < 2.0 {
|
|
5333
|
+
result.basis = vec4<f32>(0.0);
|
|
5334
|
+
result.adjustedOpacity = 0.0;
|
|
5335
|
+
return result;
|
|
5336
|
+
}
|
|
5337
|
+
|
|
5338
|
+
// 从 offDiagonal 和特征值差计算特征向量
|
|
5339
|
+
// diagonalVector = normalize(vec2(offDiagonal, lambda1 - diagonal1))
|
|
5340
|
+
let diagVec = normalize(vec2<f32>(b, lambda1 - a));
|
|
5341
|
+
let eigenvector1 = diagVec;
|
|
5342
|
+
let eigenvector2 = vec2<f32>(diagVec.y, -diagVec.x);
|
|
5343
|
+
|
|
5344
|
+
// 计算基向量 (不应用额外的 splat_scale,因为我们使用默认值 1.0)
|
|
5345
|
+
result.basis = vec4<f32>(eigenvector1 * l1, eigenvector2 * l2);
|
|
5346
|
+
result.adjustedOpacity = alpha;
|
|
5347
|
+
return result;
|
|
5348
|
+
}
|
|
5349
|
+
|
|
5350
|
+
fn getModelScale3(model: mat4x4<f32>) -> vec3<f32> {
|
|
5351
|
+
return vec3<f32>(length(model[0].xyz), length(model[1].xyz), length(model[2].xyz));
|
|
5031
5352
|
}
|
|
5032
5353
|
|
|
5033
5354
|
@vertex
|
|
@@ -5036,861 +5357,238 @@ fn vs_main(@builtin(vertex_index) vertexIndex: u32, @builtin(instance_index) ins
|
|
|
5036
5357
|
let splatIndex = sortedIndices[instanceIndex];
|
|
5037
5358
|
let splat = splats[splatIndex];
|
|
5038
5359
|
let quadPos = QUAD_POSITIONS[vertexIndex];
|
|
5039
|
-
output.localUV = quadPos;
|
|
5040
5360
|
|
|
5041
|
-
//
|
|
5042
|
-
|
|
5043
|
-
let modelScale = getModelScale3(uniforms.model);
|
|
5361
|
+
// 透明度剔除
|
|
5362
|
+
if splat.opacity < ALPHA_CULL_THRESHOLD { output.position = vec4<f32>(0.0, 0.0, 2.0, 1.0); return output; }
|
|
5044
5363
|
|
|
5045
|
-
|
|
5046
|
-
let
|
|
5047
|
-
|
|
5364
|
+
// 四元数有效性检查
|
|
5365
|
+
let quatNormSqr = dot(splat.rotation, splat.rotation);
|
|
5366
|
+
if quatNormSqr < 1e-6 { output.position = vec4<f32>(0.0, 0.0, 2.0, 1.0); return output; }
|
|
5048
5367
|
|
|
5049
|
-
//
|
|
5368
|
+
// 变换到视图空间 (匹配参考实现: Local -> World -> View -> Clip)
|
|
5050
5369
|
let worldPos = uniforms.model * vec4<f32>(splat.mean, 1.0);
|
|
5051
|
-
let viewPos = uniforms.view * worldPos;
|
|
5052
|
-
|
|
5053
|
-
clipPos.x = clipPos.x + screenOffset.x * clipPos.w;
|
|
5054
|
-
clipPos.y = clipPos.y + screenOffset.y * clipPos.w;
|
|
5055
|
-
output.position = clipPos;
|
|
5056
|
-
// DC 颜色已经在 PLYLoader 中预计算: colorDC = 0.5 + SH_C0 * f_dc
|
|
5057
|
-
output.color = splat.colorDC;
|
|
5058
|
-
output.opacity = splat.opacity;
|
|
5370
|
+
let viewPos = uniforms.view * worldPos; // vec4, 保持 w 分量
|
|
5371
|
+
let clipPos = uniforms.proj * viewPos;
|
|
5059
5372
|
|
|
5060
|
-
|
|
5061
|
-
}
|
|
5062
|
-
|
|
5063
|
-
@fragment
|
|
5064
|
-
fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
|
|
5065
|
-
let r = length(input.localUV);
|
|
5066
|
-
if (r > 1.0) { discard; }
|
|
5067
|
-
let gaussianWeight = exp(-r * r * GAUSSIAN_DECAY);
|
|
5068
|
-
let alpha = input.opacity * gaussianWeight;
|
|
5069
|
-
if (alpha < 0.004) { discard; }
|
|
5070
|
-
let color = clamp(input.color, vec3<f32>(0.0), vec3<f32>(1.0));
|
|
5071
|
-
return vec4<f32>(color * alpha, alpha);
|
|
5072
|
-
}
|
|
5073
|
-
`
|
|
5074
|
-
);
|
|
5075
|
-
const shaderCodeL1 = (
|
|
5076
|
-
/* wgsl */
|
|
5077
|
-
`
|
|
5078
|
-
struct Uniforms {
|
|
5079
|
-
view: mat4x4<f32>,
|
|
5080
|
-
proj: mat4x4<f32>,
|
|
5081
|
-
model: mat4x4<f32>,
|
|
5082
|
-
cameraPos: vec3<f32>,
|
|
5083
|
-
_pad: f32,
|
|
5084
|
-
screenSize: vec2<f32>,
|
|
5085
|
-
_pad2: vec2<f32>,
|
|
5086
|
-
}
|
|
5087
|
-
|
|
5088
|
-
struct Splat {
|
|
5089
|
-
mean: vec3<f32>,
|
|
5090
|
-
_pad0: f32,
|
|
5091
|
-
scale: vec3<f32>,
|
|
5092
|
-
_pad1: f32,
|
|
5093
|
-
rotation: vec4<f32>,
|
|
5094
|
-
colorDC: vec3<f32>,
|
|
5095
|
-
opacity: f32,
|
|
5096
|
-
sh1: array<f32, 9>,
|
|
5097
|
-
sh2: array<f32, 15>,
|
|
5098
|
-
sh3: array<f32, 21>,
|
|
5099
|
-
_pad2: array<f32, 3>,
|
|
5100
|
-
}
|
|
5101
|
-
|
|
5102
|
-
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
|
|
5103
|
-
@group(0) @binding(1) var<storage, read> splats: array<Splat>;
|
|
5104
|
-
@group(0) @binding(2) var<storage, read> sortedIndices: array<u32>;
|
|
5105
|
-
|
|
5106
|
-
struct VertexOutput {
|
|
5107
|
-
@builtin(position) position: vec4<f32>,
|
|
5108
|
-
@location(0) localUV: vec2<f32>,
|
|
5109
|
-
@location(1) color: vec3<f32>,
|
|
5110
|
-
@location(2) opacity: f32,
|
|
5111
|
-
}
|
|
5112
|
-
|
|
5113
|
-
const QUAD_POSITIONS = array<vec2<f32>, 4>(
|
|
5114
|
-
vec2<f32>(-1.0, -1.0),
|
|
5115
|
-
vec2<f32>( 1.0, -1.0),
|
|
5116
|
-
vec2<f32>(-1.0, 1.0),
|
|
5117
|
-
vec2<f32>( 1.0, 1.0),
|
|
5118
|
-
);
|
|
5119
|
-
|
|
5120
|
-
const ELLIPSE_SCALE: f32 = 3.0;
|
|
5121
|
-
const GAUSSIAN_DECAY: f32 = 4.5;
|
|
5122
|
-
|
|
5123
|
-
// SH 常数
|
|
5124
|
-
const SH_C0: f32 = 0.28209479177387814; // sqrt(1/(4*pi)) - DC 颜色
|
|
5125
|
-
const SH_C1: f32 = 0.4886025119029199; // sqrt(3/(4*pi)) - L1
|
|
5126
|
-
|
|
5127
|
-
// 计算 L1 球谐函数贡献
|
|
5128
|
-
// 数据布局 (按基函数分组): [basis0_R, basis0_G, basis0_B, basis1_R, basis1_G, basis1_B, ...]
|
|
5129
|
-
// 即 sh1[0..2] = 第一个基函数的 RGB, sh1[3..5] = 第二个基函数的 RGB, sh1[6..8] = 第三个基函数的 RGB
|
|
5130
|
-
// 原始 3DGS 公式: result = SH_C1 * (-sh[0] * y + sh[1] * z - sh[2] * x)
|
|
5131
|
-
// 其中 sh[0], sh[1], sh[2] 是 vec3 (RGB)
|
|
5132
|
-
fn evalSH1(dir: vec3<f32>, sh1: array<f32, 9>) -> vec3<f32> {
|
|
5133
|
-
let x = dir.x;
|
|
5134
|
-
let y = dir.y;
|
|
5135
|
-
let z = dir.z;
|
|
5136
|
-
|
|
5137
|
-
// sh[0] = vec3(sh1[0], sh1[1], sh1[2]) - 第一个基函数 (Y_1^{-1})
|
|
5138
|
-
// sh[1] = vec3(sh1[3], sh1[4], sh1[5]) - 第二个基函数 (Y_1^0)
|
|
5139
|
-
// sh[2] = vec3(sh1[6], sh1[7], sh1[8]) - 第三个基函数 (Y_1^1)
|
|
5140
|
-
let sh0 = vec3<f32>(sh1[0], sh1[1], sh1[2]);
|
|
5141
|
-
let sh1_1 = vec3<f32>(sh1[3], sh1[4], sh1[5]);
|
|
5142
|
-
let sh2 = vec3<f32>(sh1[6], sh1[7], sh1[8]);
|
|
5143
|
-
|
|
5144
|
-
return SH_C1 * (-sh0 * y + sh1_1 * z - sh2 * x);
|
|
5145
|
-
}
|
|
5146
|
-
|
|
5147
|
-
fn quatToMat3(q: vec4<f32>) -> mat3x3<f32> {
|
|
5148
|
-
let w = q[0]; let x = q[1]; let y = q[2]; let z = q[3];
|
|
5149
|
-
let x2 = x + x; let y2 = y + y; let z2 = z + z;
|
|
5150
|
-
let xx = x * x2; let xy = x * y2; let xz = x * z2;
|
|
5151
|
-
let yy = y * y2; let yz = y * z2; let zz = z * z2;
|
|
5152
|
-
let wx = w * x2; let wy = w * y2; let wz = w * z2;
|
|
5153
|
-
return mat3x3<f32>(
|
|
5154
|
-
vec3<f32>(1.0 - (yy + zz), xy + wz, xz - wy),
|
|
5155
|
-
vec3<f32>(xy - wz, 1.0 - (xx + zz), yz + wx),
|
|
5156
|
-
vec3<f32>(xz + wy, yz - wx, 1.0 - (xx + yy))
|
|
5157
|
-
);
|
|
5158
|
-
}
|
|
5159
|
-
|
|
5160
|
-
fn getModelScale3(model: mat4x4<f32>) -> vec3<f32> {
|
|
5161
|
-
return vec3<f32>(
|
|
5162
|
-
length(model[0].xyz),
|
|
5163
|
-
length(model[1].xyz),
|
|
5164
|
-
length(model[2].xyz)
|
|
5165
|
-
);
|
|
5166
|
-
}
|
|
5167
|
-
|
|
5168
|
-
fn computeCov2D(mean: vec3<f32>, scale: vec3<f32>, rotation: vec4<f32>, modelView: mat4x4<f32>, proj: mat4x4<f32>, modelScale: vec3<f32>) -> vec3<f32> {
|
|
5169
|
-
let R = quatToMat3(rotation);
|
|
5170
|
-
let scaledScale = scale * modelScale;
|
|
5171
|
-
let s2 = scaledScale * scaledScale;
|
|
5172
|
-
let M = mat3x3<f32>(R[0] * s2.x, R[1] * s2.y, R[2] * s2.z);
|
|
5173
|
-
let Sigma = M * transpose(R);
|
|
5174
|
-
|
|
5175
|
-
let viewPos = (modelView * vec4<f32>(mean, 1.0)).xyz;
|
|
5176
|
-
let viewRot = mat3x3<f32>(modelView[0].xyz, modelView[1].xyz, modelView[2].xyz);
|
|
5177
|
-
let SigmaView = viewRot * Sigma * transpose(viewRot);
|
|
5178
|
-
|
|
5179
|
-
let fx = proj[0][0]; let fy = proj[1][1];
|
|
5180
|
-
let z = -viewPos.z;
|
|
5181
|
-
let z_clamped = max(z, 0.001);
|
|
5182
|
-
let z2 = z_clamped * z_clamped;
|
|
5183
|
-
|
|
5184
|
-
let j1 = vec3<f32>(fx / z_clamped, 0.0, fx * viewPos.x / z2);
|
|
5185
|
-
let j2 = vec3<f32>(0.0, fy / z_clamped, fy * viewPos.y / z2);
|
|
5186
|
-
let Sj1 = SigmaView * j1;
|
|
5187
|
-
let Sj2 = SigmaView * j2;
|
|
5188
|
-
return vec3<f32>(dot(j1, Sj1), dot(j1, Sj2), dot(j2, Sj2));
|
|
5189
|
-
}
|
|
5190
|
-
|
|
5191
|
-
fn computeEllipseAxes(cov2D: vec3<f32>) -> mat2x2<f32> {
|
|
5192
|
-
let a = cov2D.x; let b = cov2D.y; let c = cov2D.z;
|
|
5193
|
-
let trace = a + c;
|
|
5194
|
-
let det = a * c - b * b;
|
|
5195
|
-
let disc = trace * trace - 4.0 * det;
|
|
5196
|
-
let sqrtDisc = sqrt(max(disc, 0.0));
|
|
5197
|
-
let lambda1 = max((trace + sqrtDisc) * 0.5, 0.0);
|
|
5198
|
-
let lambda2 = max((trace - sqrtDisc) * 0.5, 0.0);
|
|
5199
|
-
let r1 = sqrt(lambda1);
|
|
5200
|
-
let r2 = sqrt(lambda2);
|
|
5201
|
-
|
|
5202
|
-
var axis1: vec2<f32>; var axis2: vec2<f32>;
|
|
5203
|
-
let eigenvecLen = abs(b) + abs(lambda1 - a);
|
|
5204
|
-
if (eigenvecLen > 1e-6) {
|
|
5205
|
-
axis1 = normalize(vec2<f32>(b, lambda1 - a));
|
|
5206
|
-
axis2 = vec2<f32>(-axis1.y, axis1.x);
|
|
5207
|
-
} else {
|
|
5208
|
-
if (a >= c) { axis1 = vec2<f32>(1.0, 0.0); axis2 = vec2<f32>(0.0, 1.0); }
|
|
5209
|
-
else { axis1 = vec2<f32>(0.0, 1.0); axis2 = vec2<f32>(1.0, 0.0); }
|
|
5210
|
-
}
|
|
5211
|
-
return mat2x2<f32>(axis1 * r1, axis2 * r2);
|
|
5212
|
-
}
|
|
5213
|
-
|
|
5214
|
-
@vertex
|
|
5215
|
-
fn vs_main(@builtin(vertex_index) vertexIndex: u32, @builtin(instance_index) instanceIndex: u32) -> VertexOutput {
|
|
5216
|
-
var output: VertexOutput;
|
|
5217
|
-
let splatIndex = sortedIndices[instanceIndex];
|
|
5218
|
-
let splat = splats[splatIndex];
|
|
5219
|
-
let quadPos = QUAD_POSITIONS[vertexIndex];
|
|
5220
|
-
output.localUV = quadPos;
|
|
5221
|
-
|
|
5222
|
-
let modelView = uniforms.view * uniforms.model;
|
|
5223
|
-
let modelScale = getModelScale3(uniforms.model);
|
|
5224
|
-
|
|
5225
|
-
let cov2D = computeCov2D(splat.mean, splat.scale, splat.rotation, modelView, uniforms.proj, modelScale);
|
|
5226
|
-
let axes = computeEllipseAxes(cov2D);
|
|
5227
|
-
let screenOffset = axes[0] * quadPos.x * ELLIPSE_SCALE + axes[1] * quadPos.y * ELLIPSE_SCALE;
|
|
5228
|
-
|
|
5229
|
-
let worldPos = uniforms.model * vec4<f32>(splat.mean, 1.0);
|
|
5230
|
-
let viewPos = uniforms.view * worldPos;
|
|
5231
|
-
var clipPos = uniforms.proj * viewPos;
|
|
5232
|
-
clipPos.x = clipPos.x + screenOffset.x * clipPos.w;
|
|
5233
|
-
clipPos.y = clipPos.y + screenOffset.y * clipPos.w;
|
|
5234
|
-
output.position = clipPos;
|
|
5235
|
-
|
|
5236
|
-
// SH 计算 - 使用局部坐标系中的方向
|
|
5237
|
-
// 将相机位置转换到局部坐标系
|
|
5238
|
-
// cam_local = A^{-1} * (cam_world - t) = (A^T / s^2) * (cam_world - t)
|
|
5239
|
-
// 其中 A 是模型矩阵的 3x3 部分,s^2 是缩放的平方
|
|
5240
|
-
let A = mat3x3<f32>(
|
|
5241
|
-
uniforms.model[0].xyz,
|
|
5242
|
-
uniforms.model[1].xyz,
|
|
5243
|
-
uniforms.model[2].xyz
|
|
5244
|
-
);
|
|
5245
|
-
let modelTranslation = uniforms.model[3].xyz;
|
|
5246
|
-
let s2 = max(1e-12, (dot(A[0], A[0]) + dot(A[1], A[1]) + dot(A[2], A[2])) / 3.0);
|
|
5247
|
-
let camLocal = (transpose(A) * (uniforms.cameraPos - modelTranslation)) / s2;
|
|
5248
|
-
// 方向:从相机指向 splat(与 visionary 一致)
|
|
5249
|
-
let dirLocal = normalize(splat.mean - camLocal);
|
|
5250
|
-
|
|
5251
|
-
let shColor = evalSH1(dirLocal, splat.sh1);
|
|
5252
|
-
// DC 颜色已经在 PLYLoader 中预计算,这里只加 SH 贡献
|
|
5253
|
-
// 使用 max 确保颜色不为负(与 visionary 一致)
|
|
5254
|
-
output.color = max(vec3<f32>(0.0), splat.colorDC + shColor);
|
|
5255
|
-
output.opacity = splat.opacity;
|
|
5256
|
-
|
|
5257
|
-
return output;
|
|
5258
|
-
}
|
|
5259
|
-
|
|
5260
|
-
@fragment
|
|
5261
|
-
fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
|
|
5262
|
-
let r = length(input.localUV);
|
|
5263
|
-
if (r > 1.0) { discard; }
|
|
5264
|
-
let gaussianWeight = exp(-r * r * GAUSSIAN_DECAY);
|
|
5265
|
-
let alpha = input.opacity * gaussianWeight;
|
|
5266
|
-
if (alpha < 0.004) { discard; }
|
|
5267
|
-
let color = clamp(input.color, vec3<f32>(0.0), vec3<f32>(1.0));
|
|
5268
|
-
return vec4<f32>(color * alpha, alpha);
|
|
5269
|
-
}
|
|
5270
|
-
`
|
|
5271
|
-
);
|
|
5272
|
-
const shaderCodeL2 = (
|
|
5273
|
-
/* wgsl */
|
|
5274
|
-
`
|
|
5275
|
-
struct Uniforms {
|
|
5276
|
-
view: mat4x4<f32>,
|
|
5277
|
-
proj: mat4x4<f32>,
|
|
5278
|
-
model: mat4x4<f32>,
|
|
5279
|
-
cameraPos: vec3<f32>,
|
|
5280
|
-
_pad: f32,
|
|
5281
|
-
screenSize: vec2<f32>,
|
|
5282
|
-
_pad2: vec2<f32>,
|
|
5283
|
-
}
|
|
5284
|
-
|
|
5285
|
-
struct Splat {
|
|
5286
|
-
mean: vec3<f32>,
|
|
5287
|
-
_pad0: f32,
|
|
5288
|
-
scale: vec3<f32>,
|
|
5289
|
-
_pad1: f32,
|
|
5290
|
-
rotation: vec4<f32>,
|
|
5291
|
-
colorDC: vec3<f32>,
|
|
5292
|
-
opacity: f32,
|
|
5293
|
-
sh1: array<f32, 9>,
|
|
5294
|
-
sh2: array<f32, 15>,
|
|
5295
|
-
sh3: array<f32, 21>,
|
|
5296
|
-
_pad2: array<f32, 3>,
|
|
5297
|
-
}
|
|
5298
|
-
|
|
5299
|
-
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
|
|
5300
|
-
@group(0) @binding(1) var<storage, read> splats: array<Splat>;
|
|
5301
|
-
@group(0) @binding(2) var<storage, read> sortedIndices: array<u32>;
|
|
5302
|
-
|
|
5303
|
-
struct VertexOutput {
|
|
5304
|
-
@builtin(position) position: vec4<f32>,
|
|
5305
|
-
@location(0) localUV: vec2<f32>,
|
|
5306
|
-
@location(1) color: vec3<f32>,
|
|
5307
|
-
@location(2) opacity: f32,
|
|
5308
|
-
}
|
|
5309
|
-
|
|
5310
|
-
const QUAD_POSITIONS = array<vec2<f32>, 4>(
|
|
5311
|
-
vec2<f32>(-1.0, -1.0),
|
|
5312
|
-
vec2<f32>( 1.0, -1.0),
|
|
5313
|
-
vec2<f32>(-1.0, 1.0),
|
|
5314
|
-
vec2<f32>( 1.0, 1.0),
|
|
5315
|
-
);
|
|
5316
|
-
|
|
5317
|
-
const ELLIPSE_SCALE: f32 = 3.0;
|
|
5318
|
-
const GAUSSIAN_DECAY: f32 = 4.5;
|
|
5319
|
-
|
|
5320
|
-
// SH 常数
|
|
5321
|
-
const SH_C0: f32 = 0.28209479177387814; // sqrt(1/(4*pi)) - DC 颜色
|
|
5322
|
-
const SH_C1: f32 = 0.4886025119029199; // sqrt(3/(4*pi)) - L1
|
|
5323
|
-
const SH_C2_0: f32 = 1.0925484305920792;
|
|
5324
|
-
const SH_C2_1: f32 = -1.0925484305920792;
|
|
5325
|
-
const SH_C2_2: f32 = 0.31539156525252005;
|
|
5326
|
-
const SH_C2_3: f32 = -1.0925484305920792;
|
|
5327
|
-
const SH_C2_4: f32 = 0.5462742152960396;
|
|
5328
|
-
|
|
5329
|
-
// 数据布局 (按基函数分组): [basis0_RGB, basis1_RGB, basis2_RGB]
|
|
5330
|
-
fn evalSH1(dir: vec3<f32>, sh1: array<f32, 9>) -> vec3<f32> {
|
|
5331
|
-
let x = dir.x;
|
|
5332
|
-
let y = dir.y;
|
|
5333
|
-
let z = dir.z;
|
|
5334
|
-
let sh0 = vec3<f32>(sh1[0], sh1[1], sh1[2]);
|
|
5335
|
-
let sh1_1 = vec3<f32>(sh1[3], sh1[4], sh1[5]);
|
|
5336
|
-
let sh2 = vec3<f32>(sh1[6], sh1[7], sh1[8]);
|
|
5337
|
-
return SH_C1 * (-sh0 * y + sh1_1 * z - sh2 * x);
|
|
5338
|
-
}
|
|
5339
|
-
|
|
5340
|
-
// 数据布局 (按基函数分组): [basis0_RGB, basis1_RGB, ..., basis4_RGB]
|
|
5341
|
-
fn evalSH2(dir: vec3<f32>, sh2: array<f32, 15>) -> vec3<f32> {
|
|
5342
|
-
let x = dir.x; let y = dir.y; let z = dir.z;
|
|
5343
|
-
let xx = x * x; let yy = y * y; let zz = z * z;
|
|
5344
|
-
let xy = x * y; let yz = y * z; let xz = x * z;
|
|
5345
|
-
|
|
5346
|
-
// L2 基函数
|
|
5347
|
-
let b0 = SH_C2_0 * xy;
|
|
5348
|
-
let b1 = SH_C2_1 * yz;
|
|
5349
|
-
let b2 = SH_C2_2 * (2.0 * zz - xx - yy);
|
|
5350
|
-
let b3 = SH_C2_3 * xz;
|
|
5351
|
-
let b4 = SH_C2_4 * (xx - yy);
|
|
5352
|
-
|
|
5353
|
-
// 数据按基函数分组: sh2[0..2]=basis0_RGB, sh2[3..5]=basis1_RGB, ...
|
|
5354
|
-
let c0 = vec3<f32>(sh2[0], sh2[1], sh2[2]);
|
|
5355
|
-
let c1 = vec3<f32>(sh2[3], sh2[4], sh2[5]);
|
|
5356
|
-
let c2 = vec3<f32>(sh2[6], sh2[7], sh2[8]);
|
|
5357
|
-
let c3 = vec3<f32>(sh2[9], sh2[10], sh2[11]);
|
|
5358
|
-
let c4 = vec3<f32>(sh2[12], sh2[13], sh2[14]);
|
|
5359
|
-
|
|
5360
|
-
return c0 * b0 + c1 * b1 + c2 * b2 + c3 * b3 + c4 * b4;
|
|
5361
|
-
}
|
|
5362
|
-
|
|
5363
|
-
fn quatToMat3(q: vec4<f32>) -> mat3x3<f32> {
|
|
5364
|
-
let w = q[0]; let x = q[1]; let y = q[2]; let z = q[3];
|
|
5365
|
-
let x2 = x + x; let y2 = y + y; let z2 = z + z;
|
|
5366
|
-
let xx = x * x2; let xy = x * y2; let xz = x * z2;
|
|
5367
|
-
let yy = y * y2; let yz = y * z2; let zz = z * z2;
|
|
5368
|
-
let wx = w * x2; let wy = w * y2; let wz = w * z2;
|
|
5369
|
-
return mat3x3<f32>(
|
|
5370
|
-
vec3<f32>(1.0 - (yy + zz), xy + wz, xz - wy),
|
|
5371
|
-
vec3<f32>(xy - wz, 1.0 - (xx + zz), yz + wx),
|
|
5372
|
-
vec3<f32>(xz + wy, yz - wx, 1.0 - (xx + yy))
|
|
5373
|
-
);
|
|
5374
|
-
}
|
|
5375
|
-
|
|
5376
|
-
fn getModelScale3(model: mat4x4<f32>) -> vec3<f32> {
|
|
5377
|
-
return vec3<f32>(
|
|
5378
|
-
length(model[0].xyz),
|
|
5379
|
-
length(model[1].xyz),
|
|
5380
|
-
length(model[2].xyz)
|
|
5381
|
-
);
|
|
5382
|
-
}
|
|
5383
|
-
|
|
5384
|
-
fn computeCov2D(mean: vec3<f32>, scale: vec3<f32>, rotation: vec4<f32>, modelView: mat4x4<f32>, proj: mat4x4<f32>, modelScale: vec3<f32>) -> vec3<f32> {
|
|
5385
|
-
let R = quatToMat3(rotation);
|
|
5386
|
-
let scaledScale = scale * modelScale;
|
|
5387
|
-
let s2 = scaledScale * scaledScale;
|
|
5388
|
-
let M = mat3x3<f32>(R[0] * s2.x, R[1] * s2.y, R[2] * s2.z);
|
|
5389
|
-
let Sigma = M * transpose(R);
|
|
5390
|
-
|
|
5391
|
-
let viewPos = (modelView * vec4<f32>(mean, 1.0)).xyz;
|
|
5392
|
-
let viewRot = mat3x3<f32>(modelView[0].xyz, modelView[1].xyz, modelView[2].xyz);
|
|
5393
|
-
let SigmaView = viewRot * Sigma * transpose(viewRot);
|
|
5394
|
-
|
|
5395
|
-
let fx = proj[0][0]; let fy = proj[1][1];
|
|
5396
|
-
let z = -viewPos.z;
|
|
5397
|
-
let z_clamped = max(z, 0.001);
|
|
5398
|
-
let z2 = z_clamped * z_clamped;
|
|
5373
|
+
// 近平面剔除 (viewPos.z 是负数,相机看向 -Z)
|
|
5374
|
+
if viewPos.z >= 0.0 { output.position = vec4<f32>(0.0, 0.0, 2.0, 1.0); return output; }
|
|
5399
5375
|
|
|
5400
|
-
|
|
5401
|
-
let
|
|
5402
|
-
let
|
|
5403
|
-
let Sj2 = SigmaView * j2;
|
|
5404
|
-
return vec3<f32>(dot(j1, Sj1), dot(j1, Sj2), dot(j2, Sj2));
|
|
5405
|
-
}
|
|
5406
|
-
|
|
5407
|
-
fn computeEllipseAxes(cov2D: vec3<f32>) -> mat2x2<f32> {
|
|
5408
|
-
let a = cov2D.x; let b = cov2D.y; let c = cov2D.z;
|
|
5409
|
-
let trace = a + c;
|
|
5410
|
-
let det = a * c - b * b;
|
|
5411
|
-
let disc = trace * trace - 4.0 * det;
|
|
5412
|
-
let sqrtDisc = sqrt(max(disc, 0.0));
|
|
5413
|
-
let lambda1 = max((trace + sqrtDisc) * 0.5, 0.0);
|
|
5414
|
-
let lambda2 = max((trace - sqrtDisc) * 0.5, 0.0);
|
|
5415
|
-
let r1 = sqrt(lambda1);
|
|
5416
|
-
let r2 = sqrt(lambda2);
|
|
5376
|
+
// NDC 计算
|
|
5377
|
+
let pW = 1.0 / (clipPos.w + 0.0000001);
|
|
5378
|
+
let ndcPos = clipPos * pW;
|
|
5417
5379
|
|
|
5418
|
-
|
|
5419
|
-
let
|
|
5420
|
-
if (
|
|
5421
|
-
|
|
5422
|
-
axis2 = vec2<f32>(-axis1.y, axis1.x);
|
|
5423
|
-
} else {
|
|
5424
|
-
if (a >= c) { axis1 = vec2<f32>(1.0, 0.0); axis2 = vec2<f32>(0.0, 1.0); }
|
|
5425
|
-
else { axis1 = vec2<f32>(0.0, 1.0); axis2 = vec2<f32>(1.0, 0.0); }
|
|
5380
|
+
// 视锥剔除 (放宽边界以避免 pop-in)
|
|
5381
|
+
let clipBound = 1.3;
|
|
5382
|
+
if abs(ndcPos.x) > clipBound || abs(ndcPos.y) > clipBound || ndcPos.z < -0.2 || ndcPos.z > 1.0 {
|
|
5383
|
+
output.position = vec4<f32>(0.0, 0.0, 2.0, 1.0); return output;
|
|
5426
5384
|
}
|
|
5427
|
-
return mat2x2<f32>(axis1 * r1, axis2 * r2);
|
|
5428
|
-
}
|
|
5429
|
-
|
|
5430
|
-
@vertex
|
|
5431
|
-
fn vs_main(@builtin(vertex_index) vertexIndex: u32, @builtin(instance_index) instanceIndex: u32) -> VertexOutput {
|
|
5432
|
-
var output: VertexOutput;
|
|
5433
|
-
let splatIndex = sortedIndices[instanceIndex];
|
|
5434
|
-
let splat = splats[splatIndex];
|
|
5435
|
-
let quadPos = QUAD_POSITIONS[vertexIndex];
|
|
5436
|
-
output.localUV = quadPos;
|
|
5437
|
-
|
|
5438
|
-
let modelView = uniforms.view * uniforms.model;
|
|
5439
|
-
let modelScale = getModelScale3(uniforms.model);
|
|
5440
|
-
|
|
5441
|
-
let cov2D = computeCov2D(splat.mean, splat.scale, splat.rotation, modelView, uniforms.proj, modelScale);
|
|
5442
|
-
let axes = computeEllipseAxes(cov2D);
|
|
5443
|
-
let screenOffset = axes[0] * quadPos.x * ELLIPSE_SCALE + axes[1] * quadPos.y * ELLIPSE_SCALE;
|
|
5444
|
-
|
|
5445
|
-
let worldPos = uniforms.model * vec4<f32>(splat.mean, 1.0);
|
|
5446
|
-
let viewPos = uniforms.view * worldPos;
|
|
5447
|
-
var clipPos = uniforms.proj * viewPos;
|
|
5448
|
-
clipPos.x = clipPos.x + screenOffset.x * clipPos.w;
|
|
5449
|
-
clipPos.y = clipPos.y + screenOffset.y * clipPos.w;
|
|
5450
|
-
output.position = clipPos;
|
|
5451
|
-
|
|
5452
|
-
// SH 计算 - 使用局部坐标系中的方向 (与 visionary 一致)
|
|
5453
|
-
let A = mat3x3<f32>(
|
|
5454
|
-
uniforms.model[0].xyz,
|
|
5455
|
-
uniforms.model[1].xyz,
|
|
5456
|
-
uniforms.model[2].xyz
|
|
5457
|
-
);
|
|
5458
|
-
let modelTranslation = uniforms.model[3].xyz;
|
|
5459
|
-
let s2 = max(1e-12, (dot(A[0], A[0]) + dot(A[1], A[1]) + dot(A[2], A[2])) / 3.0);
|
|
5460
|
-
let camLocal = (transpose(A) * (uniforms.cameraPos - modelTranslation)) / s2;
|
|
5461
|
-
let dirLocal = normalize(splat.mean - camLocal);
|
|
5462
|
-
|
|
5463
|
-
let shColor1 = evalSH1(dirLocal, splat.sh1);
|
|
5464
|
-
let shColor2 = evalSH2(dirLocal, splat.sh2);
|
|
5465
|
-
output.color = max(vec3<f32>(0.0), splat.colorDC + shColor1 + shColor2);
|
|
5466
|
-
output.opacity = splat.opacity;
|
|
5467
|
-
|
|
5468
|
-
return output;
|
|
5469
|
-
}
|
|
5470
|
-
|
|
5471
|
-
@fragment
|
|
5472
|
-
fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
|
|
5473
|
-
let r = length(input.localUV);
|
|
5474
|
-
if (r > 1.0) { discard; }
|
|
5475
|
-
let gaussianWeight = exp(-r * r * GAUSSIAN_DECAY);
|
|
5476
|
-
let alpha = input.opacity * gaussianWeight;
|
|
5477
|
-
if (alpha < 0.004) { discard; }
|
|
5478
|
-
let color = clamp(input.color, vec3<f32>(0.0), vec3<f32>(1.0));
|
|
5479
|
-
return vec4<f32>(color * alpha, alpha);
|
|
5480
|
-
}
|
|
5481
|
-
`
|
|
5482
|
-
);
|
|
5483
|
-
const shaderCodeL3 = (
|
|
5484
|
-
/* wgsl */
|
|
5485
|
-
`
|
|
5486
|
-
struct Uniforms {
|
|
5487
|
-
view: mat4x4<f32>,
|
|
5488
|
-
proj: mat4x4<f32>,
|
|
5489
|
-
model: mat4x4<f32>,
|
|
5490
|
-
cameraPos: vec3<f32>,
|
|
5491
|
-
_pad: f32,
|
|
5492
|
-
screenSize: vec2<f32>,
|
|
5493
|
-
_pad2: vec2<f32>,
|
|
5494
|
-
}
|
|
5495
|
-
|
|
5496
|
-
struct Splat {
|
|
5497
|
-
mean: vec3<f32>,
|
|
5498
|
-
_pad0: f32,
|
|
5499
|
-
scale: vec3<f32>,
|
|
5500
|
-
_pad1: f32,
|
|
5501
|
-
rotation: vec4<f32>,
|
|
5502
|
-
colorDC: vec3<f32>,
|
|
5503
|
-
opacity: f32,
|
|
5504
|
-
sh1: array<f32, 9>,
|
|
5505
|
-
sh2: array<f32, 15>,
|
|
5506
|
-
sh3: array<f32, 21>,
|
|
5507
|
-
_pad2: array<f32, 3>,
|
|
5508
|
-
}
|
|
5509
|
-
|
|
5510
|
-
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
|
|
5511
|
-
@group(0) @binding(1) var<storage, read> splats: array<Splat>;
|
|
5512
|
-
@group(0) @binding(2) var<storage, read> sortedIndices: array<u32>;
|
|
5513
|
-
|
|
5514
|
-
struct VertexOutput {
|
|
5515
|
-
@builtin(position) position: vec4<f32>,
|
|
5516
|
-
@location(0) localUV: vec2<f32>,
|
|
5517
|
-
@location(1) color: vec3<f32>,
|
|
5518
|
-
@location(2) opacity: f32,
|
|
5519
|
-
}
|
|
5520
|
-
|
|
5521
|
-
const QUAD_POSITIONS = array<vec2<f32>, 4>(
|
|
5522
|
-
vec2<f32>(-1.0, -1.0),
|
|
5523
|
-
vec2<f32>( 1.0, -1.0),
|
|
5524
|
-
vec2<f32>(-1.0, 1.0),
|
|
5525
|
-
vec2<f32>( 1.0, 1.0),
|
|
5526
|
-
);
|
|
5527
|
-
|
|
5528
|
-
const ELLIPSE_SCALE: f32 = 3.0;
|
|
5529
|
-
const GAUSSIAN_DECAY: f32 = 4.5;
|
|
5530
|
-
|
|
5531
|
-
// SH 常数
|
|
5532
|
-
const SH_C0: f32 = 0.28209479177387814; // sqrt(1/(4*pi)) - DC 颜色
|
|
5533
|
-
const SH_C1: f32 = 0.4886025119029199; // sqrt(3/(4*pi)) - L1
|
|
5534
|
-
const SH_C2_0: f32 = 1.0925484305920792;
|
|
5535
|
-
const SH_C2_1: f32 = -1.0925484305920792;
|
|
5536
|
-
const SH_C2_2: f32 = 0.31539156525252005;
|
|
5537
|
-
const SH_C2_3: f32 = -1.0925484305920792;
|
|
5538
|
-
const SH_C2_4: f32 = 0.5462742152960396;
|
|
5539
|
-
const SH_C3_0: f32 = -0.5900435899266435;
|
|
5540
|
-
const SH_C3_1: f32 = 2.890611442640554;
|
|
5541
|
-
const SH_C3_2: f32 = -0.4570457994644658;
|
|
5542
|
-
const SH_C3_3: f32 = 0.3731763325901154;
|
|
5543
|
-
const SH_C3_4: f32 = -0.4570457994644658;
|
|
5544
|
-
const SH_C3_5: f32 = 1.445305721320277;
|
|
5545
|
-
const SH_C3_6: f32 = -0.5900435899266435;
|
|
5546
|
-
|
|
5547
|
-
// 数据布局 (按基函数分组): [basis0_RGB, basis1_RGB, basis2_RGB]
|
|
5548
|
-
fn evalSH1(dir: vec3<f32>, sh1: array<f32, 9>) -> vec3<f32> {
|
|
5549
|
-
let x = dir.x;
|
|
5550
|
-
let y = dir.y;
|
|
5551
|
-
let z = dir.z;
|
|
5552
|
-
let sh0 = vec3<f32>(sh1[0], sh1[1], sh1[2]);
|
|
5553
|
-
let sh1_1 = vec3<f32>(sh1[3], sh1[4], sh1[5]);
|
|
5554
|
-
let sh2 = vec3<f32>(sh1[6], sh1[7], sh1[8]);
|
|
5555
|
-
return SH_C1 * (-sh0 * y + sh1_1 * z - sh2 * x);
|
|
5556
|
-
}
|
|
5557
|
-
|
|
5558
|
-
// 数据布局 (按基函数分组): [basis0_RGB, ..., basis4_RGB]
|
|
5559
|
-
fn evalSH2(dir: vec3<f32>, sh2: array<f32, 15>) -> vec3<f32> {
|
|
5560
|
-
let x = dir.x; let y = dir.y; let z = dir.z;
|
|
5561
|
-
let xx = x * x; let yy = y * y; let zz = z * z;
|
|
5562
|
-
let xy = x * y; let yz = y * z; let xz = x * z;
|
|
5563
|
-
|
|
5564
|
-
let b0 = SH_C2_0 * xy;
|
|
5565
|
-
let b1 = SH_C2_1 * yz;
|
|
5566
|
-
let b2 = SH_C2_2 * (2.0 * zz - xx - yy);
|
|
5567
|
-
let b3 = SH_C2_3 * xz;
|
|
5568
|
-
let b4 = SH_C2_4 * (xx - yy);
|
|
5569
|
-
|
|
5570
|
-
let c0 = vec3<f32>(sh2[0], sh2[1], sh2[2]);
|
|
5571
|
-
let c1 = vec3<f32>(sh2[3], sh2[4], sh2[5]);
|
|
5572
|
-
let c2 = vec3<f32>(sh2[6], sh2[7], sh2[8]);
|
|
5573
|
-
let c3 = vec3<f32>(sh2[9], sh2[10], sh2[11]);
|
|
5574
|
-
let c4 = vec3<f32>(sh2[12], sh2[13], sh2[14]);
|
|
5575
5385
|
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
// 数据布局 (按基函数分组): [basis0_RGB, ..., basis6_RGB]
|
|
5580
|
-
fn evalSH3(dir: vec3<f32>, sh3: array<f32, 21>) -> vec3<f32> {
|
|
5581
|
-
let x = dir.x; let y = dir.y; let z = dir.z;
|
|
5582
|
-
let xx = x * x; let yy = y * y; let zz = z * z;
|
|
5583
|
-
let xy = x * y; let yz = y * z; let xz = x * z;
|
|
5584
|
-
|
|
5585
|
-
let b0 = SH_C3_0 * y * (3.0 * xx - yy);
|
|
5586
|
-
let b1 = SH_C3_1 * xy * z;
|
|
5587
|
-
let b2 = SH_C3_2 * y * (4.0 * zz - xx - yy);
|
|
5588
|
-
let b3 = SH_C3_3 * z * (2.0 * zz - 3.0 * xx - 3.0 * yy);
|
|
5589
|
-
let b4 = SH_C3_4 * x * (4.0 * zz - xx - yy);
|
|
5590
|
-
let b5 = SH_C3_5 * z * (xx - yy);
|
|
5591
|
-
let b6 = SH_C3_6 * x * (xx - 3.0 * yy);
|
|
5592
|
-
|
|
5593
|
-
let c0 = vec3<f32>(sh3[0], sh3[1], sh3[2]);
|
|
5594
|
-
let c1 = vec3<f32>(sh3[3], sh3[4], sh3[5]);
|
|
5595
|
-
let c2 = vec3<f32>(sh3[6], sh3[7], sh3[8]);
|
|
5596
|
-
let c3 = vec3<f32>(sh3[9], sh3[10], sh3[11]);
|
|
5597
|
-
let c4 = vec3<f32>(sh3[12], sh3[13], sh3[14]);
|
|
5598
|
-
let c5 = vec3<f32>(sh3[15], sh3[16], sh3[17]);
|
|
5599
|
-
let c6 = vec3<f32>(sh3[18], sh3[19], sh3[20]);
|
|
5386
|
+
// 计算 3D 协方差 (使用原始 scale,模型缩放通过 model-view 矩阵处理)
|
|
5387
|
+
// 关键: 不要在这里应用模型缩放,协方差投影会通过 model-view 矩阵正确处理
|
|
5388
|
+
let cov3d = computeCovariance3D(splat.scale, splat.rotation);
|
|
5600
5389
|
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
let w = q[0]; let x = q[1]; let y = q[2]; let z = q[3];
|
|
5606
|
-
let x2 = x + x; let y2 = y + y; let z2 = z + z;
|
|
5607
|
-
let xx = x * x2; let xy = x * y2; let xz = x * z2;
|
|
5608
|
-
let yy = y * y2; let yz = y * z2; let zz = z * z2;
|
|
5609
|
-
let wx = w * x2; let wy = w * y2; let wz = w * z2;
|
|
5610
|
-
return mat3x3<f32>(
|
|
5611
|
-
vec3<f32>(1.0 - (yy + zz), xy + wz, xz - wy),
|
|
5612
|
-
vec3<f32>(xy - wz, 1.0 - (xx + zz), yz + wx),
|
|
5613
|
-
vec3<f32>(xz + wy, yz - wx, 1.0 - (xx + yy))
|
|
5614
|
-
);
|
|
5615
|
-
}
|
|
5616
|
-
|
|
5617
|
-
fn getModelScale3(model: mat4x4<f32>) -> vec3<f32> {
|
|
5618
|
-
return vec3<f32>(
|
|
5619
|
-
length(model[0].xyz),
|
|
5620
|
-
length(model[1].xyz),
|
|
5621
|
-
length(model[2].xyz)
|
|
5390
|
+
// 计算焦距 (匹配参考实现: abs(proj[0][0]) * 0.5 * width)
|
|
5391
|
+
let focal = vec2<f32>(
|
|
5392
|
+
abs(uniforms.proj[0][0]) * 0.5 * uniforms.screenSize.x,
|
|
5393
|
+
abs(uniforms.proj[1][1]) * 0.5 * uniforms.screenSize.y
|
|
5622
5394
|
);
|
|
5623
|
-
}
|
|
5624
|
-
|
|
5625
|
-
fn computeCov2D(mean: vec3<f32>, scale: vec3<f32>, rotation: vec4<f32>, modelView: mat4x4<f32>, proj: mat4x4<f32>, modelScale: vec3<f32>) -> vec3<f32> {
|
|
5626
|
-
let R = quatToMat3(rotation);
|
|
5627
|
-
let scaledScale = scale * modelScale;
|
|
5628
|
-
let s2 = scaledScale * scaledScale;
|
|
5629
|
-
let M = mat3x3<f32>(R[0] * s2.x, R[1] * s2.y, R[2] * s2.z);
|
|
5630
|
-
let Sigma = M * transpose(R);
|
|
5631
|
-
|
|
5632
|
-
let viewPos = (modelView * vec4<f32>(mean, 1.0)).xyz;
|
|
5633
|
-
let viewRot = mat3x3<f32>(modelView[0].xyz, modelView[1].xyz, modelView[2].xyz);
|
|
5634
|
-
let SigmaView = viewRot * Sigma * transpose(viewRot);
|
|
5635
|
-
|
|
5636
|
-
let fx = proj[0][0]; let fy = proj[1][1];
|
|
5637
|
-
let z = -viewPos.z;
|
|
5638
|
-
let z_clamped = max(z, 0.001);
|
|
5639
|
-
let z2 = z_clamped * z_clamped;
|
|
5640
5395
|
|
|
5641
|
-
|
|
5642
|
-
let
|
|
5643
|
-
let Sj1 = SigmaView * j1;
|
|
5644
|
-
let Sj2 = SigmaView * j2;
|
|
5645
|
-
return vec3<f32>(dot(j1, Sj1), dot(j1, Sj2), dot(j2, Sj2));
|
|
5646
|
-
}
|
|
5647
|
-
|
|
5648
|
-
fn computeEllipseAxes(cov2D: vec3<f32>) -> mat2x2<f32> {
|
|
5649
|
-
let a = cov2D.x; let b = cov2D.y; let c = cov2D.z;
|
|
5650
|
-
let trace = a + c;
|
|
5651
|
-
let det = a * c - b * b;
|
|
5652
|
-
let disc = trace * trace - 4.0 * det;
|
|
5653
|
-
let sqrtDisc = sqrt(max(disc, 0.0));
|
|
5654
|
-
let lambda1 = max((trace + sqrtDisc) * 0.5, 0.0);
|
|
5655
|
-
let lambda2 = max((trace - sqrtDisc) * 0.5, 0.0);
|
|
5656
|
-
let r1 = sqrt(lambda1);
|
|
5657
|
-
let r2 = sqrt(lambda2);
|
|
5396
|
+
// 计算 model-view 矩阵 (匹配参考实现)
|
|
5397
|
+
let modelViewMat = uniforms.view * uniforms.model;
|
|
5658
5398
|
|
|
5659
|
-
|
|
5660
|
-
let
|
|
5661
|
-
if (eigenvecLen > 1e-6) {
|
|
5662
|
-
axis1 = normalize(vec2<f32>(b, lambda1 - a));
|
|
5663
|
-
axis2 = vec2<f32>(-axis1.y, axis1.x);
|
|
5664
|
-
} else {
|
|
5665
|
-
if (a >= c) { axis1 = vec2<f32>(1.0, 0.0); axis2 = vec2<f32>(0.0, 1.0); }
|
|
5666
|
-
else { axis1 = vec2<f32>(0.0, 1.0); axis2 = vec2<f32>(1.0, 0.0); }
|
|
5667
|
-
}
|
|
5668
|
-
return mat2x2<f32>(axis1 * r1, axis2 * r2);
|
|
5669
|
-
}
|
|
5670
|
-
|
|
5671
|
-
@vertex
|
|
5672
|
-
fn vs_main(@builtin(vertex_index) vertexIndex: u32, @builtin(instance_index) instanceIndex: u32) -> VertexOutput {
|
|
5673
|
-
var output: VertexOutput;
|
|
5674
|
-
let splatIndex = sortedIndices[instanceIndex];
|
|
5675
|
-
let splat = splats[splatIndex];
|
|
5676
|
-
let quadPos = QUAD_POSITIONS[vertexIndex];
|
|
5677
|
-
output.localUV = quadPos;
|
|
5399
|
+
// 投影协方差到 2D (传入 viewPos 作为 vec4,不除以 w)
|
|
5400
|
+
let cov2d = projectCovariance(cov3d, viewPos, focal, modelViewMat);
|
|
5678
5401
|
|
|
5679
|
-
//
|
|
5680
|
-
let
|
|
5681
|
-
let
|
|
5682
|
-
|
|
5683
|
-
let cov2D = computeCov2D(splat.mean, splat.scale, splat.rotation, modelView, uniforms.proj, modelScale);
|
|
5684
|
-
let axes = computeEllipseAxes(cov2D);
|
|
5685
|
-
let screenOffset = axes[0] * quadPos.x * ELLIPSE_SCALE + axes[1] * quadPos.y * ELLIPSE_SCALE;
|
|
5402
|
+
// 计算范围基向量 (带抗锯齿)
|
|
5403
|
+
let extentResult = computeExtentBasisAA(cov2d, splat.opacity, uniforms.screenSize);
|
|
5404
|
+
let basis = extentResult.basis;
|
|
5405
|
+
let adjustedOpacity = extentResult.adjustedOpacity;
|
|
5686
5406
|
|
|
5687
|
-
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
var clipPos = uniforms.proj * viewPos;
|
|
5691
|
-
clipPos.x = clipPos.x + screenOffset.x * clipPos.w;
|
|
5692
|
-
clipPos.y = clipPos.y + screenOffset.y * clipPos.w;
|
|
5693
|
-
output.position = clipPos;
|
|
5407
|
+
if basis.x == 0.0 && basis.y == 0.0 && basis.z == 0.0 && basis.w == 0.0 {
|
|
5408
|
+
output.position = vec4<f32>(0.0, 0.0, 2.0, 1.0); return output;
|
|
5409
|
+
}
|
|
5694
5410
|
|
|
5695
|
-
//
|
|
5696
|
-
let
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
|
|
5703
|
-
|
|
5704
|
-
|
|
5411
|
+
// 视锥边缘剔除 (匹配 PlayCanvas)
|
|
5412
|
+
let maxExtentPixels = max(length(basis.xy), length(basis.zw));
|
|
5413
|
+
let pixelToClip = vec2<f32>(clipPos.w, clipPos.w) / uniforms.screenSize;
|
|
5414
|
+
let splatExtentClip = vec2<f32>(maxExtentPixels, maxExtentPixels) * pixelToClip;
|
|
5415
|
+
if any((abs(clipPos.xy) - splatExtentClip) > vec2<f32>(clipPos.w, clipPos.w)) {
|
|
5416
|
+
output.position = vec4<f32>(0.0, 0.0, 2.0, 1.0); return output;
|
|
5417
|
+
}
|
|
5418
|
+
|
|
5419
|
+
// ClipCorner 优化 (匹配 PlayCanvas/SuperSplat)
|
|
5420
|
+
// 根据透明度缩小 quad,排除 alpha < 1/255 的区域
|
|
5421
|
+
let clipFactor = computeClipFactor(adjustedOpacity);
|
|
5422
|
+
if clipFactor <= 0.0 { output.position = vec4<f32>(0.0, 0.0, 2.0, 1.0); return output; }
|
|
5705
5423
|
|
|
5706
|
-
|
|
5707
|
-
|
|
5708
|
-
let
|
|
5709
|
-
output.color = max(vec3<f32>(0.0), splat.colorDC + shColor1 + shColor2 + shColor3);
|
|
5710
|
-
output.opacity = splat.opacity;
|
|
5424
|
+
// 计算最终顶点位置
|
|
5425
|
+
// basis_viewport: 从像素转换到 NDC 空间
|
|
5426
|
+
let basisViewport = vec2<f32>(1.0 / uniforms.screenSize.x, 1.0 / uniforms.screenSize.y);
|
|
5711
5427
|
|
|
5428
|
+
// 用 clipFactor 缩放基向量 (缩小 quad)
|
|
5429
|
+
let basisVector1 = basis.xy * clipFactor;
|
|
5430
|
+
let basisVector2 = basis.zw * clipFactor;
|
|
5431
|
+
|
|
5432
|
+
// 计算 NDC 偏移
|
|
5433
|
+
// 注意: quadPos 在 [-1, 1] 范围内,clipFactor 只影响 quad 大小 (basis_vector)
|
|
5434
|
+
let ndcOffset = (quadPos.x * basisVector1 + quadPos.y * basisVector2) * basisViewport * 2.0;
|
|
5435
|
+
output.position = vec4<f32>(ndcPos.xy + ndcOffset, ndcPos.z, 1.0);
|
|
5436
|
+
|
|
5437
|
+
// UV 输出 - 用 clipFactor 缩放以获得正确的 Gaussian 权重
|
|
5438
|
+
output.fragPos = quadPos * clipFactor;
|
|
5439
|
+
|
|
5440
|
+
// 颜色已在 CPU 端预处理为 (dc * SH_C0 + 0.5),直接使用
|
|
5441
|
+
// 这是 3DGS 的标准颜色格式,在 sRGB 空间中
|
|
5442
|
+
output.color = splat.colorDC;
|
|
5443
|
+
output.opacity = adjustedOpacity;
|
|
5712
5444
|
return output;
|
|
5713
5445
|
}
|
|
5714
5446
|
|
|
5715
5447
|
@fragment
|
|
5716
5448
|
fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5449
|
+
if input.opacity <= 0.0 { discard; }
|
|
5450
|
+
|
|
5451
|
+
// A = 到中心的平方距离,在 UV 空间中
|
|
5452
|
+
// 由于 clipCorner 优化,fragPos 在 [-clip, clip] 范围内
|
|
5453
|
+
let A = dot(input.fragPos, input.fragPos);
|
|
5454
|
+
|
|
5455
|
+
// 丢弃单位圆外的片段
|
|
5456
|
+
if A > 1.0 { discard; }
|
|
5457
|
+
|
|
5458
|
+
// Normalized Gaussian 衰减 (精确匹配 SuperSplat normExp)
|
|
5459
|
+
// 关键修复: 在 A=1 (边界) 时返回精确的 0.0,消除边缘雾化
|
|
5460
|
+
// 在 A=0 (中心): weight = 1.0
|
|
5461
|
+
// 在 A=1 (边界): weight = 精确的 0.0 (而不是标准 exp(-4) ≈ 0.018)
|
|
5462
|
+
let weight = (exp(-4.0 * A) - EXP_NEG4) * INV_ONE_MINUS_EXP_NEG4;
|
|
5463
|
+
|
|
5464
|
+
// 组合 splat 透明度
|
|
5465
|
+
let opacity = weight * input.opacity;
|
|
5466
|
+
|
|
5467
|
+
// Alpha 阈值丢弃 (匹配 SuperSplat: if (alpha < 1.0 / 255.0) discard)
|
|
5468
|
+
if opacity < ALPHA_CULL_THRESHOLD { discard; }
|
|
5469
|
+
|
|
5470
|
+
// 颜色 clamp 到有效范围 (防止负值)
|
|
5471
|
+
let color = max(input.color, vec3<f32>(0.0));
|
|
5472
|
+
|
|
5473
|
+
// 预乘 alpha 输出 (匹配 blend mode: src=ONE, dst=ONE_MINUS_SRC_ALPHA)
|
|
5474
|
+
// 这是 3DGS 渲染的标准混合模式
|
|
5475
|
+
return vec4<f32>(color * opacity, opacity);
|
|
5724
5476
|
}
|
|
5725
5477
|
`
|
|
5726
5478
|
);
|
|
5727
|
-
var SHMode = /* @__PURE__ */ ((SHMode2) => {
|
|
5728
|
-
SHMode2[SHMode2["L0"] = 0] = "L0";
|
|
5729
|
-
SHMode2[SHMode2["L1"] = 1] = "L1";
|
|
5730
|
-
SHMode2[SHMode2["L2"] = 2] = "L2";
|
|
5731
|
-
SHMode2[SHMode2["L3"] = 3] = "L3";
|
|
5732
|
-
return SHMode2;
|
|
5733
|
-
})(SHMode || {});
|
|
5734
5479
|
const SPLAT_FLOAT_COUNT = 64;
|
|
5735
|
-
const SPLAT_COMPACT_FLOAT_COUNT = 16;
|
|
5736
|
-
const PERFORMANCE_CONFIGS = {
|
|
5737
|
-
[
|
|
5738
|
-
"high"
|
|
5739
|
-
/* HIGH */
|
|
5740
|
-
]: {
|
|
5741
|
-
maxVisibleSplats: Infinity,
|
|
5742
|
-
enableSorting: true,
|
|
5743
|
-
sortEveryNFrames: 1,
|
|
5744
|
-
useCompactFormat: false,
|
|
5745
|
-
pixelCullThreshold: 1,
|
|
5746
|
-
defaultSHMode: 1
|
|
5747
|
-
/* L1 */
|
|
5748
|
-
},
|
|
5749
|
-
[
|
|
5750
|
-
"medium"
|
|
5751
|
-
/* MEDIUM */
|
|
5752
|
-
]: {
|
|
5753
|
-
maxVisibleSplats: Infinity,
|
|
5754
|
-
enableSorting: true,
|
|
5755
|
-
sortEveryNFrames: 1,
|
|
5756
|
-
useCompactFormat: false,
|
|
5757
|
-
pixelCullThreshold: 1,
|
|
5758
|
-
defaultSHMode: 1
|
|
5759
|
-
/* L1 */
|
|
5760
|
-
},
|
|
5761
|
-
[
|
|
5762
|
-
"low"
|
|
5763
|
-
/* LOW */
|
|
5764
|
-
]: {
|
|
5765
|
-
maxVisibleSplats: Infinity,
|
|
5766
|
-
enableSorting: true,
|
|
5767
|
-
sortEveryNFrames: 1,
|
|
5768
|
-
useCompactFormat: false,
|
|
5769
|
-
pixelCullThreshold: 1,
|
|
5770
|
-
defaultSHMode: 0
|
|
5771
|
-
/* L0 */
|
|
5772
|
-
}
|
|
5773
|
-
};
|
|
5774
5480
|
class GSSplatRenderer {
|
|
5775
5481
|
constructor(renderer, camera) {
|
|
5776
5482
|
__publicField(this, "renderer");
|
|
5777
5483
|
__publicField(this, "camera");
|
|
5778
|
-
__publicField(this, "
|
|
5779
|
-
__publicField(this, "pipelineL1");
|
|
5780
|
-
__publicField(this, "pipelineL2");
|
|
5781
|
-
__publicField(this, "pipelineL3");
|
|
5782
|
-
__publicField(this, "pipelineL0Compact");
|
|
5783
|
-
// 移动端紧凑格式管线
|
|
5484
|
+
__publicField(this, "pipeline");
|
|
5784
5485
|
__publicField(this, "bindGroupLayout");
|
|
5785
|
-
__publicField(this, "bindGroupLayoutCompact");
|
|
5786
|
-
// 紧凑格式的 layout
|
|
5787
5486
|
__publicField(this, "uniformBuffer");
|
|
5788
5487
|
__publicField(this, "splatBuffer", null);
|
|
5789
5488
|
__publicField(this, "splatCount", 0);
|
|
5790
5489
|
__publicField(this, "bindGroup", null);
|
|
5791
|
-
// 深度排序器(含剔除功能)- 使用 V2 分桶稳定排序
|
|
5792
5490
|
__publicField(this, "sorter", null);
|
|
5793
|
-
|
|
5794
|
-
// 注意:在某些移动设备上可能有问题,可以禁用作为备用
|
|
5795
|
-
__publicField(this, "useDrawIndirect", true);
|
|
5796
|
-
// 是否为移动设备(用于调试)
|
|
5797
|
-
__publicField(this, "isMobile", false);
|
|
5798
|
-
// 像素剔除阈值 (小于此像素的 splat 会被剔除)
|
|
5799
|
-
__publicField(this, "pixelCullThreshold", 1);
|
|
5800
|
-
// SH 模式:L0/L1/L2/L3
|
|
5801
|
-
__publicField(this, "shMode", 1);
|
|
5802
|
-
// 点云的 bounding box(在 setData 时计算)
|
|
5491
|
+
__publicField(this, "shMode", SHMode.L0);
|
|
5803
5492
|
__publicField(this, "boundingBox", null);
|
|
5804
|
-
//
|
|
5805
|
-
// 变换相关 (position, rotation, scale)
|
|
5806
|
-
// ============================================
|
|
5493
|
+
// Transform
|
|
5807
5494
|
__publicField(this, "position", [0, 0, 0]);
|
|
5808
5495
|
__publicField(this, "rotation", [0, 0, 0]);
|
|
5809
|
-
// Euler angles (radians)
|
|
5810
5496
|
__publicField(this, "scale", [1, 1, 1]);
|
|
5811
5497
|
__publicField(this, "pivot", [0, 0, 0]);
|
|
5812
|
-
// 旋转/缩放中心点
|
|
5813
5498
|
__publicField(this, "modelMatrix", new Float32Array(16));
|
|
5814
|
-
//
|
|
5815
|
-
|
|
5816
|
-
// 移动端优化相关
|
|
5817
|
-
// ============================================
|
|
5818
|
-
__publicField(this, "performanceTier");
|
|
5819
|
-
__publicField(this, "optimizationConfig");
|
|
5820
|
-
__publicField(this, "frameCount", 0);
|
|
5821
|
-
__publicField(this, "useCompactFormat", false);
|
|
5499
|
+
// 剔除选项
|
|
5500
|
+
__publicField(this, "pixelCullThreshold", 1);
|
|
5822
5501
|
this.renderer = renderer;
|
|
5823
5502
|
this.camera = camera;
|
|
5824
|
-
this.
|
|
5825
|
-
this.performanceTier = detectPerformanceTier(renderer.device);
|
|
5826
|
-
this.optimizationConfig = { ...PERFORMANCE_CONFIGS[this.performanceTier] };
|
|
5827
|
-
this.pixelCullThreshold = this.optimizationConfig.pixelCullThreshold;
|
|
5828
|
-
this.shMode = this.optimizationConfig.defaultSHMode;
|
|
5829
|
-
this.useCompactFormat = this.optimizationConfig.useCompactFormat;
|
|
5830
|
-
this.createPipelines();
|
|
5503
|
+
this.createPipeline();
|
|
5831
5504
|
this.createUniformBuffer();
|
|
5832
5505
|
this.updateModelMatrix();
|
|
5833
5506
|
}
|
|
5834
|
-
|
|
5835
|
-
|
|
5836
|
-
|
|
5837
|
-
|
|
5838
|
-
|
|
5839
|
-
|
|
5507
|
+
createPipeline() {
|
|
5508
|
+
const device = this.renderer.device;
|
|
5509
|
+
const shaderModule = device.createShaderModule({
|
|
5510
|
+
code: gsOptimizedShader
|
|
5511
|
+
});
|
|
5512
|
+
this.bindGroupLayout = device.createBindGroupLayout({
|
|
5513
|
+
entries: [
|
|
5514
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
5515
|
+
{ binding: 1, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } },
|
|
5516
|
+
{ binding: 2, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } }
|
|
5517
|
+
]
|
|
5518
|
+
});
|
|
5519
|
+
const pipelineLayout = device.createPipelineLayout({
|
|
5520
|
+
bindGroupLayouts: [this.bindGroupLayout]
|
|
5521
|
+
});
|
|
5522
|
+
this.pipeline = device.createRenderPipeline({
|
|
5523
|
+
layout: pipelineLayout,
|
|
5524
|
+
vertex: {
|
|
5525
|
+
module: shaderModule,
|
|
5526
|
+
entryPoint: "vs_main",
|
|
5527
|
+
buffers: []
|
|
5528
|
+
},
|
|
5529
|
+
fragment: {
|
|
5530
|
+
module: shaderModule,
|
|
5531
|
+
entryPoint: "fs_main",
|
|
5532
|
+
targets: [{
|
|
5533
|
+
format: this.renderer.format,
|
|
5534
|
+
blend: {
|
|
5535
|
+
color: {
|
|
5536
|
+
srcFactor: "one",
|
|
5537
|
+
dstFactor: "one-minus-src-alpha",
|
|
5538
|
+
operation: "add"
|
|
5539
|
+
},
|
|
5540
|
+
alpha: {
|
|
5541
|
+
srcFactor: "one",
|
|
5542
|
+
dstFactor: "one-minus-src-alpha",
|
|
5543
|
+
operation: "add"
|
|
5544
|
+
}
|
|
5545
|
+
}
|
|
5546
|
+
}]
|
|
5547
|
+
},
|
|
5548
|
+
primitive: {
|
|
5549
|
+
topology: "triangle-strip"
|
|
5550
|
+
},
|
|
5551
|
+
depthStencil: {
|
|
5552
|
+
format: this.renderer.depthFormat,
|
|
5553
|
+
depthWriteEnabled: false,
|
|
5554
|
+
depthCompare: "always"
|
|
5555
|
+
}
|
|
5556
|
+
});
|
|
5557
|
+
}
|
|
5558
|
+
createUniformBuffer() {
|
|
5559
|
+
this.uniformBuffer = this.renderer.device.createBuffer({
|
|
5560
|
+
size: 224,
|
|
5561
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
5562
|
+
});
|
|
5563
|
+
}
|
|
5840
5564
|
setPosition(x, y, z) {
|
|
5841
5565
|
this.position = [x, y, z];
|
|
5842
5566
|
this.updateModelMatrix();
|
|
5843
5567
|
}
|
|
5844
|
-
/**
|
|
5845
|
-
* 获取位置
|
|
5846
|
-
*/
|
|
5847
5568
|
getPosition() {
|
|
5848
5569
|
return [...this.position];
|
|
5849
5570
|
}
|
|
5850
|
-
/**
|
|
5851
|
-
* 设置旋转 (欧拉角, 弧度)
|
|
5852
|
-
*/
|
|
5853
5571
|
setRotation(x, y, z) {
|
|
5854
5572
|
this.rotation = [x, y, z];
|
|
5855
5573
|
this.updateModelMatrix();
|
|
5856
5574
|
}
|
|
5857
|
-
/**
|
|
5858
|
-
* 获取旋转
|
|
5859
|
-
*/
|
|
5860
5575
|
getRotation() {
|
|
5861
5576
|
return [...this.rotation];
|
|
5862
5577
|
}
|
|
5863
|
-
/**
|
|
5864
|
-
* 设置缩放
|
|
5865
|
-
*/
|
|
5866
5578
|
setScale(x, y, z) {
|
|
5867
5579
|
this.scale = [x, y, z];
|
|
5868
5580
|
this.updateModelMatrix();
|
|
5869
5581
|
}
|
|
5870
|
-
/**
|
|
5871
|
-
* 获取缩放
|
|
5872
|
-
*/
|
|
5873
5582
|
getScale() {
|
|
5874
5583
|
return [...this.scale];
|
|
5875
5584
|
}
|
|
5876
|
-
/**
|
|
5877
|
-
* 设置旋转/缩放中心点 (pivot)
|
|
5878
|
-
*/
|
|
5879
5585
|
setPivot(x, y, z) {
|
|
5880
5586
|
this.pivot = [x, y, z];
|
|
5881
5587
|
this.updateModelMatrix();
|
|
5882
5588
|
}
|
|
5883
|
-
/**
|
|
5884
|
-
* 获取旋转/缩放中心点 (pivot)
|
|
5885
|
-
*/
|
|
5886
5589
|
getPivot() {
|
|
5887
5590
|
return [...this.pivot];
|
|
5888
5591
|
}
|
|
5889
|
-
/**
|
|
5890
|
-
* 更新模型矩阵
|
|
5891
|
-
* 变换顺序: T * Tp * R * S * Tp^-1
|
|
5892
|
-
* 即: 先移到原点,缩放,旋转,再移回pivot,最后应用用户平移
|
|
5893
|
-
*/
|
|
5894
5592
|
updateModelMatrix() {
|
|
5895
5593
|
const [tx, ty, tz] = this.position;
|
|
5896
5594
|
const [rx, ry, rz] = this.rotation;
|
|
@@ -5934,148 +5632,18 @@ class GSSplatRenderer {
|
|
|
5934
5632
|
this.modelMatrix[14] = finalTz;
|
|
5935
5633
|
this.modelMatrix[15] = 1;
|
|
5936
5634
|
}
|
|
5937
|
-
/**
|
|
5938
|
-
* 获取当前模型矩阵
|
|
5939
|
-
*/
|
|
5940
5635
|
getModelMatrix() {
|
|
5941
5636
|
return this.modelMatrix;
|
|
5942
5637
|
}
|
|
5943
|
-
/**
|
|
5944
|
-
* 获取当前性能等级
|
|
5945
|
-
*/
|
|
5946
|
-
getPerformanceTier() {
|
|
5947
|
-
return this.performanceTier;
|
|
5948
|
-
}
|
|
5949
|
-
/**
|
|
5950
|
-
* 手动设置优化配置
|
|
5951
|
-
*/
|
|
5952
|
-
setOptimizationConfig(config) {
|
|
5953
|
-
this.optimizationConfig = { ...this.optimizationConfig, ...config };
|
|
5954
|
-
this.pixelCullThreshold = this.optimizationConfig.pixelCullThreshold;
|
|
5955
|
-
if (config.defaultSHMode !== void 0) {
|
|
5956
|
-
this.shMode = config.defaultSHMode;
|
|
5957
|
-
}
|
|
5958
|
-
}
|
|
5959
|
-
/**
|
|
5960
|
-
* 获取当前优化配置
|
|
5961
|
-
*/
|
|
5962
|
-
getOptimizationConfig() {
|
|
5963
|
-
return { ...this.optimizationConfig };
|
|
5964
|
-
}
|
|
5965
|
-
/**
|
|
5966
|
-
* 设置 SH 模式
|
|
5967
|
-
* @param mode L0/L1/L2/L3
|
|
5968
|
-
*/
|
|
5969
5638
|
setSHMode(mode) {
|
|
5970
5639
|
this.shMode = mode;
|
|
5971
5640
|
}
|
|
5972
|
-
/**
|
|
5973
|
-
* 获取当前 SH 模式
|
|
5974
|
-
*/
|
|
5975
5641
|
getSHMode() {
|
|
5976
5642
|
return this.shMode;
|
|
5977
5643
|
}
|
|
5978
|
-
/**
|
|
5979
|
-
* 设置是否启用 DrawIndirect (剔除优化)
|
|
5980
|
-
* 启用后会在 GPU 上进行可见性剔除,仅绘制可见 splat
|
|
5981
|
-
*/
|
|
5982
|
-
setUseDrawIndirect(enabled) {
|
|
5983
|
-
this.useDrawIndirect = enabled;
|
|
5984
|
-
}
|
|
5985
|
-
/**
|
|
5986
|
-
* 设置像素剔除阈值
|
|
5987
|
-
* 屏幕上小于此像素数的 splat 会被剔除
|
|
5988
|
-
* @param threshold 像素阈值,默认 1.0
|
|
5989
|
-
*/
|
|
5990
5644
|
setPixelCullThreshold(threshold) {
|
|
5991
5645
|
this.pixelCullThreshold = threshold;
|
|
5992
5646
|
}
|
|
5993
|
-
/**
|
|
5994
|
-
* 创建渲染管线 (L0/L1/L2/L3 四个版本)
|
|
5995
|
-
*/
|
|
5996
|
-
createPipelines() {
|
|
5997
|
-
const device = this.renderer.device;
|
|
5998
|
-
const shaderModuleL0 = device.createShaderModule({ code: shaderCodeL0 });
|
|
5999
|
-
const shaderModuleL1 = device.createShaderModule({ code: shaderCodeL1 });
|
|
6000
|
-
const shaderModuleL2 = device.createShaderModule({ code: shaderCodeL2 });
|
|
6001
|
-
const shaderModuleL3 = device.createShaderModule({ code: shaderCodeL3 });
|
|
6002
|
-
this.bindGroupLayout = device.createBindGroupLayout({
|
|
6003
|
-
entries: [
|
|
6004
|
-
{
|
|
6005
|
-
// uniform buffer (view + proj matrices)
|
|
6006
|
-
binding: 0,
|
|
6007
|
-
visibility: GPUShaderStage.VERTEX,
|
|
6008
|
-
buffer: { type: "uniform" }
|
|
6009
|
-
},
|
|
6010
|
-
{
|
|
6011
|
-
// storage buffer (splats array)
|
|
6012
|
-
binding: 1,
|
|
6013
|
-
visibility: GPUShaderStage.VERTEX,
|
|
6014
|
-
buffer: { type: "read-only-storage" }
|
|
6015
|
-
},
|
|
6016
|
-
{
|
|
6017
|
-
// storage buffer (sorted indices)
|
|
6018
|
-
binding: 2,
|
|
6019
|
-
visibility: GPUShaderStage.VERTEX,
|
|
6020
|
-
buffer: { type: "read-only-storage" }
|
|
6021
|
-
}
|
|
6022
|
-
]
|
|
6023
|
-
});
|
|
6024
|
-
const pipelineLayout = device.createPipelineLayout({
|
|
6025
|
-
bindGroupLayouts: [this.bindGroupLayout]
|
|
6026
|
-
});
|
|
6027
|
-
const basePipelineDesc = {
|
|
6028
|
-
layout: pipelineLayout,
|
|
6029
|
-
primitive: {
|
|
6030
|
-
topology: "triangle-strip"
|
|
6031
|
-
},
|
|
6032
|
-
depthStencil: {
|
|
6033
|
-
format: this.renderer.depthFormat,
|
|
6034
|
-
depthWriteEnabled: false,
|
|
6035
|
-
depthCompare: "always"
|
|
6036
|
-
}
|
|
6037
|
-
};
|
|
6038
|
-
const blendState = {
|
|
6039
|
-
color: {
|
|
6040
|
-
srcFactor: "one",
|
|
6041
|
-
dstFactor: "one-minus-src-alpha",
|
|
6042
|
-
operation: "add"
|
|
6043
|
-
},
|
|
6044
|
-
alpha: {
|
|
6045
|
-
srcFactor: "one",
|
|
6046
|
-
dstFactor: "one-minus-src-alpha",
|
|
6047
|
-
operation: "add"
|
|
6048
|
-
}
|
|
6049
|
-
};
|
|
6050
|
-
const createPipeline = (module) => device.createRenderPipeline({
|
|
6051
|
-
...basePipelineDesc,
|
|
6052
|
-
vertex: { module, entryPoint: "vs_main", buffers: [] },
|
|
6053
|
-
fragment: {
|
|
6054
|
-
module,
|
|
6055
|
-
entryPoint: "fs_main",
|
|
6056
|
-
targets: [{ format: this.renderer.format, blend: blendState }]
|
|
6057
|
-
}
|
|
6058
|
-
});
|
|
6059
|
-
this.pipelineL0 = createPipeline(shaderModuleL0);
|
|
6060
|
-
this.pipelineL1 = createPipeline(shaderModuleL1);
|
|
6061
|
-
this.pipelineL2 = createPipeline(shaderModuleL2);
|
|
6062
|
-
this.pipelineL3 = createPipeline(shaderModuleL3);
|
|
6063
|
-
}
|
|
6064
|
-
/**
|
|
6065
|
-
* 创建 uniform buffer
|
|
6066
|
-
* 布局: view (64 bytes) + proj (64 bytes) + model (64 bytes) + cameraPos (12 bytes) + padding (4 bytes) + screenSize (8 bytes) + padding (8 bytes) = 224 bytes
|
|
6067
|
-
*/
|
|
6068
|
-
createUniformBuffer() {
|
|
6069
|
-
this.uniformBuffer = this.renderer.device.createBuffer({
|
|
6070
|
-
size: 224,
|
|
6071
|
-
// view (64) + proj (64) + model (64) + cameraPos (12) + padding (4) + screenSize (8) + padding (8)
|
|
6072
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
6073
|
-
});
|
|
6074
|
-
}
|
|
6075
|
-
/**
|
|
6076
|
-
* 设置 splat 数据
|
|
6077
|
-
* @param splats CPU 端的 splat 数组
|
|
6078
|
-
*/
|
|
6079
5647
|
setData(splats) {
|
|
6080
5648
|
const device = this.renderer.device;
|
|
6081
5649
|
if (this.splatBuffer) {
|
|
@@ -6085,19 +5653,7 @@ class GSSplatRenderer {
|
|
|
6085
5653
|
this.sorter.destroy();
|
|
6086
5654
|
this.sorter = null;
|
|
6087
5655
|
}
|
|
6088
|
-
splats.length;
|
|
6089
|
-
const maxSplats = this.optimizationConfig.maxVisibleSplats;
|
|
6090
|
-
if (splats.length > maxSplats && maxSplats !== Infinity) {
|
|
6091
|
-
const step = splats.length / maxSplats;
|
|
6092
|
-
const sampledSplats = [];
|
|
6093
|
-
for (let i = 0; i < maxSplats; i++) {
|
|
6094
|
-
const idx = Math.floor(i * step);
|
|
6095
|
-
sampledSplats.push(splats[idx]);
|
|
6096
|
-
}
|
|
6097
|
-
splats = sampledSplats;
|
|
6098
|
-
}
|
|
6099
5656
|
this.splatCount = splats.length;
|
|
6100
|
-
this.frameCount = 0;
|
|
6101
5657
|
if (this.splatCount === 0) {
|
|
6102
5658
|
this.splatBuffer = null;
|
|
6103
5659
|
this.bindGroup = null;
|
|
@@ -6105,12 +5661,10 @@ class GSSplatRenderer {
|
|
|
6105
5661
|
return;
|
|
6106
5662
|
}
|
|
6107
5663
|
this.boundingBox = this.computeBoundingBox(splats);
|
|
6108
|
-
const
|
|
6109
|
-
const floatCount = useCompact ? SPLAT_COMPACT_FLOAT_COUNT : SPLAT_FLOAT_COUNT;
|
|
6110
|
-
const data = new Float32Array(this.splatCount * floatCount);
|
|
5664
|
+
const data = new Float32Array(this.splatCount * SPLAT_FLOAT_COUNT);
|
|
6111
5665
|
for (let i = 0; i < this.splatCount; i++) {
|
|
6112
5666
|
const splat = splats[i];
|
|
6113
|
-
const offset = i *
|
|
5667
|
+
const offset = i * SPLAT_FLOAT_COUNT;
|
|
6114
5668
|
data[offset + 0] = splat.mean[0];
|
|
6115
5669
|
data[offset + 1] = splat.mean[1];
|
|
6116
5670
|
data[offset + 2] = splat.mean[2];
|
|
@@ -6127,27 +5681,70 @@ class GSSplatRenderer {
|
|
|
6127
5681
|
data[offset + 13] = splat.colorDC[1];
|
|
6128
5682
|
data[offset + 14] = splat.colorDC[2];
|
|
6129
5683
|
data[offset + 15] = splat.opacity;
|
|
6130
|
-
|
|
6131
|
-
|
|
6132
|
-
|
|
6133
|
-
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
data[offset + 25 + j] = shRest ? shRest[9 + j] : 0;
|
|
6137
|
-
}
|
|
6138
|
-
for (let j = 0; j < 21; j++) {
|
|
6139
|
-
data[offset + 40 + j] = shRest ? shRest[24 + j] : 0;
|
|
6140
|
-
}
|
|
6141
|
-
data[offset + 61] = 0;
|
|
6142
|
-
data[offset + 62] = 0;
|
|
6143
|
-
data[offset + 63] = 0;
|
|
5684
|
+
const shRest = splat.shRest;
|
|
5685
|
+
for (let j = 0; j < 9; j++) {
|
|
5686
|
+
data[offset + 16 + j] = shRest ? shRest[j] : 0;
|
|
5687
|
+
}
|
|
5688
|
+
for (let j = 0; j < 15; j++) {
|
|
5689
|
+
data[offset + 25 + j] = shRest ? shRest[9 + j] : 0;
|
|
6144
5690
|
}
|
|
5691
|
+
for (let j = 0; j < 21; j++) {
|
|
5692
|
+
data[offset + 40 + j] = shRest ? shRest[24 + j] : 0;
|
|
5693
|
+
}
|
|
5694
|
+
data[offset + 61] = 0;
|
|
5695
|
+
data[offset + 62] = 0;
|
|
5696
|
+
data[offset + 63] = 0;
|
|
6145
5697
|
}
|
|
6146
5698
|
this.splatBuffer = device.createBuffer({
|
|
6147
5699
|
size: data.byteLength,
|
|
6148
5700
|
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
6149
5701
|
});
|
|
6150
|
-
device.queue.writeBuffer(this.splatBuffer, 0, data);
|
|
5702
|
+
device.queue.writeBuffer(this.splatBuffer, 0, data);
|
|
5703
|
+
this.sorter = new GSSplatSorter(
|
|
5704
|
+
device,
|
|
5705
|
+
this.splatCount,
|
|
5706
|
+
this.splatBuffer,
|
|
5707
|
+
this.uniformBuffer
|
|
5708
|
+
);
|
|
5709
|
+
this.sorter.setScreenSize(this.renderer.width, this.renderer.height);
|
|
5710
|
+
this.sorter.setCullingOptions({
|
|
5711
|
+
nearPlane: this.camera.near,
|
|
5712
|
+
farPlane: this.camera.far,
|
|
5713
|
+
pixelThreshold: this.pixelCullThreshold
|
|
5714
|
+
});
|
|
5715
|
+
this.bindGroup = device.createBindGroup({
|
|
5716
|
+
layout: this.bindGroupLayout,
|
|
5717
|
+
entries: [
|
|
5718
|
+
{ binding: 0, resource: { buffer: this.uniformBuffer } },
|
|
5719
|
+
{ binding: 1, resource: { buffer: this.splatBuffer } },
|
|
5720
|
+
{ binding: 2, resource: { buffer: this.sorter.getIndicesBuffer() } }
|
|
5721
|
+
]
|
|
5722
|
+
});
|
|
5723
|
+
}
|
|
5724
|
+
setCompactData(compactData) {
|
|
5725
|
+
const device = this.renderer.device;
|
|
5726
|
+
if (this.splatBuffer) {
|
|
5727
|
+
this.splatBuffer.destroy();
|
|
5728
|
+
}
|
|
5729
|
+
if (this.sorter) {
|
|
5730
|
+
this.sorter.destroy();
|
|
5731
|
+
this.sorter = null;
|
|
5732
|
+
}
|
|
5733
|
+
this.splatCount = compactData.count;
|
|
5734
|
+
if (this.splatCount === 0) {
|
|
5735
|
+
this.splatBuffer = null;
|
|
5736
|
+
this.bindGroup = null;
|
|
5737
|
+
this.boundingBox = null;
|
|
5738
|
+
return;
|
|
5739
|
+
}
|
|
5740
|
+
this.boundingBox = this.computeBoundingBoxFromCompact(compactData);
|
|
5741
|
+
const includeSH = compactData.shCoeffs !== void 0;
|
|
5742
|
+
const gpuData = compactDataToGPUBuffer(compactData, includeSH);
|
|
5743
|
+
this.splatBuffer = device.createBuffer({
|
|
5744
|
+
size: gpuData.byteLength,
|
|
5745
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
5746
|
+
});
|
|
5747
|
+
device.queue.writeBuffer(this.splatBuffer, 0, gpuData.buffer);
|
|
6151
5748
|
this.sorter = new GSSplatSorter(
|
|
6152
5749
|
device,
|
|
6153
5750
|
this.splatCount,
|
|
@@ -6163,130 +5760,16 @@ class GSSplatRenderer {
|
|
|
6163
5760
|
this.bindGroup = device.createBindGroup({
|
|
6164
5761
|
layout: this.bindGroupLayout,
|
|
6165
5762
|
entries: [
|
|
6166
|
-
{
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
},
|
|
6170
|
-
{
|
|
6171
|
-
binding: 1,
|
|
6172
|
-
resource: { buffer: this.splatBuffer }
|
|
6173
|
-
},
|
|
6174
|
-
{
|
|
6175
|
-
binding: 2,
|
|
6176
|
-
resource: { buffer: this.sorter.getIndicesBuffer() }
|
|
6177
|
-
}
|
|
5763
|
+
{ binding: 0, resource: { buffer: this.uniformBuffer } },
|
|
5764
|
+
{ binding: 1, resource: { buffer: this.splatBuffer } },
|
|
5765
|
+
{ binding: 2, resource: { buffer: this.sorter.getIndicesBuffer() } }
|
|
6178
5766
|
]
|
|
6179
5767
|
});
|
|
6180
|
-
(data.byteLength / (1024 * 1024)).toFixed(2);
|
|
6181
|
-
}
|
|
6182
|
-
/**
|
|
6183
|
-
* 设置紧凑格式的 splat 数据(移动端优化)
|
|
6184
|
-
* 直接接受 CompactSplatData,避免创建中间对象
|
|
6185
|
-
* @param compactData 紧凑格式的 splat 数据
|
|
6186
|
-
*/
|
|
6187
|
-
setCompactData(compactData) {
|
|
6188
|
-
try {
|
|
6189
|
-
const device = this.renderer.device;
|
|
6190
|
-
if (this.splatBuffer) {
|
|
6191
|
-
this.splatBuffer.destroy();
|
|
6192
|
-
}
|
|
6193
|
-
if (this.sorter) {
|
|
6194
|
-
this.sorter.destroy();
|
|
6195
|
-
this.sorter = null;
|
|
6196
|
-
}
|
|
6197
|
-
this.splatCount = compactData.count;
|
|
6198
|
-
this.frameCount = 0;
|
|
6199
|
-
if (this.splatCount === 0) {
|
|
6200
|
-
this.splatBuffer = null;
|
|
6201
|
-
this.bindGroup = null;
|
|
6202
|
-
this.boundingBox = null;
|
|
6203
|
-
return;
|
|
6204
|
-
}
|
|
6205
|
-
this.boundingBox = this.computeBoundingBoxFromCompact(compactData);
|
|
6206
|
-
const includeSH = compactData.shCoeffs !== void 0;
|
|
6207
|
-
const gpuData = compactDataToGPUBuffer(compactData, includeSH);
|
|
6208
|
-
this.splatBuffer = device.createBuffer({
|
|
6209
|
-
size: gpuData.byteLength,
|
|
6210
|
-
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
6211
|
-
});
|
|
6212
|
-
device.queue.writeBuffer(this.splatBuffer, 0, gpuData.buffer);
|
|
6213
|
-
this.sorter = new GSSplatSorter(
|
|
6214
|
-
device,
|
|
6215
|
-
this.splatCount,
|
|
6216
|
-
this.splatBuffer,
|
|
6217
|
-
this.uniformBuffer
|
|
6218
|
-
);
|
|
6219
|
-
this.sorter.setScreenSize(this.renderer.width, this.renderer.height);
|
|
6220
|
-
this.sorter.setCullingOptions({
|
|
6221
|
-
nearPlane: this.camera.near,
|
|
6222
|
-
farPlane: this.camera.far,
|
|
6223
|
-
pixelThreshold: this.pixelCullThreshold
|
|
6224
|
-
});
|
|
6225
|
-
this.bindGroup = device.createBindGroup({
|
|
6226
|
-
layout: this.bindGroupLayout,
|
|
6227
|
-
entries: [
|
|
6228
|
-
{ binding: 0, resource: { buffer: this.uniformBuffer } },
|
|
6229
|
-
{ binding: 1, resource: { buffer: this.splatBuffer } },
|
|
6230
|
-
{ binding: 2, resource: { buffer: this.sorter.getIndicesBuffer() } }
|
|
6231
|
-
]
|
|
6232
|
-
});
|
|
6233
|
-
const memoryMB = (gpuData.byteLength / (1024 * 1024)).toFixed(2);
|
|
6234
|
-
} catch (error) {
|
|
6235
|
-
this.splatCount = 0;
|
|
6236
|
-
this.splatBuffer = null;
|
|
6237
|
-
this.bindGroup = null;
|
|
6238
|
-
this.sorter = null;
|
|
6239
|
-
}
|
|
6240
|
-
}
|
|
6241
|
-
/**
|
|
6242
|
-
* 从紧凑数据计算 bounding box
|
|
6243
|
-
*/
|
|
6244
|
-
computeBoundingBoxFromCompact(data) {
|
|
6245
|
-
if (data.count === 0) {
|
|
6246
|
-
return { min: [0, 0, 0], max: [0, 0, 0], center: [0, 0, 0], radius: 0 };
|
|
6247
|
-
}
|
|
6248
|
-
const positions = data.positions;
|
|
6249
|
-
const min = [
|
|
6250
|
-
positions[0],
|
|
6251
|
-
positions[1],
|
|
6252
|
-
positions[2]
|
|
6253
|
-
];
|
|
6254
|
-
const max = [
|
|
6255
|
-
positions[0],
|
|
6256
|
-
positions[1],
|
|
6257
|
-
positions[2]
|
|
6258
|
-
];
|
|
6259
|
-
for (let i = 1; i < data.count; i++) {
|
|
6260
|
-
const x = positions[i * 3 + 0];
|
|
6261
|
-
const y = positions[i * 3 + 1];
|
|
6262
|
-
const z = positions[i * 3 + 2];
|
|
6263
|
-
min[0] = Math.min(min[0], x);
|
|
6264
|
-
min[1] = Math.min(min[1], y);
|
|
6265
|
-
min[2] = Math.min(min[2], z);
|
|
6266
|
-
max[0] = Math.max(max[0], x);
|
|
6267
|
-
max[1] = Math.max(max[1], y);
|
|
6268
|
-
max[2] = Math.max(max[2], z);
|
|
6269
|
-
}
|
|
6270
|
-
const center = [
|
|
6271
|
-
(min[0] + max[0]) / 2,
|
|
6272
|
-
(min[1] + max[1]) / 2,
|
|
6273
|
-
(min[2] + max[2]) / 2
|
|
6274
|
-
];
|
|
6275
|
-
const dx = max[0] - min[0];
|
|
6276
|
-
const dy = max[1] - min[1];
|
|
6277
|
-
const dz = max[2] - min[2];
|
|
6278
|
-
const radius = Math.sqrt(dx * dx + dy * dy + dz * dz) / 2;
|
|
6279
|
-
return { min, max, center, radius };
|
|
6280
5768
|
}
|
|
6281
|
-
/**
|
|
6282
|
-
* 渲染 splats
|
|
6283
|
-
* @param pass 渲染通道编码器
|
|
6284
|
-
*/
|
|
6285
5769
|
render(pass) {
|
|
6286
5770
|
if (this.splatCount === 0 || !this.bindGroup || !this.sorter) {
|
|
6287
5771
|
return;
|
|
6288
5772
|
}
|
|
6289
|
-
this.frameCount++;
|
|
6290
5773
|
this.renderer.device.queue.writeBuffer(
|
|
6291
5774
|
this.uniformBuffer,
|
|
6292
5775
|
0,
|
|
@@ -6318,63 +5801,23 @@ class GSSplatRenderer {
|
|
|
6318
5801
|
farPlane: this.camera.far,
|
|
6319
5802
|
pixelThreshold: this.pixelCullThreshold
|
|
6320
5803
|
});
|
|
6321
|
-
|
|
6322
|
-
|
|
6323
|
-
if (shouldSort) {
|
|
6324
|
-
this.sorter.sort();
|
|
6325
|
-
}
|
|
6326
|
-
const pipelines = [
|
|
6327
|
-
this.pipelineL0,
|
|
6328
|
-
this.pipelineL1,
|
|
6329
|
-
this.pipelineL2,
|
|
6330
|
-
this.pipelineL3
|
|
6331
|
-
];
|
|
6332
|
-
const pipeline = pipelines[this.shMode];
|
|
6333
|
-
pass.setPipeline(pipeline);
|
|
5804
|
+
this.sorter.sort();
|
|
5805
|
+
pass.setPipeline(this.pipeline);
|
|
6334
5806
|
pass.setBindGroup(0, this.bindGroup);
|
|
6335
|
-
|
|
6336
|
-
pass.drawIndirect(this.sorter.getDrawIndirectBuffer(), 0);
|
|
6337
|
-
} else {
|
|
6338
|
-
pass.draw(4, this.splatCount);
|
|
6339
|
-
}
|
|
5807
|
+
pass.drawIndirect(this.sorter.getDrawIndirectBuffer(), 0);
|
|
6340
5808
|
}
|
|
6341
|
-
/**
|
|
6342
|
-
* 获取 splat 数量
|
|
6343
|
-
*/
|
|
6344
5809
|
getSplatCount() {
|
|
6345
5810
|
return this.splatCount;
|
|
6346
5811
|
}
|
|
6347
|
-
/**
|
|
6348
|
-
* 获取点云的 bounding box
|
|
6349
|
-
* @returns BoundingBox 或 null(如果没有点云数据)
|
|
6350
|
-
*/
|
|
6351
5812
|
getBoundingBox() {
|
|
6352
5813
|
return this.boundingBox;
|
|
6353
5814
|
}
|
|
6354
|
-
/**
|
|
6355
|
-
* 计算点云的 bounding box
|
|
6356
|
-
* @param splats splat 数组
|
|
6357
|
-
* @returns BoundingBox
|
|
6358
|
-
*/
|
|
6359
5815
|
computeBoundingBox(splats) {
|
|
6360
5816
|
if (splats.length === 0) {
|
|
6361
|
-
return {
|
|
6362
|
-
min: [0, 0, 0],
|
|
6363
|
-
max: [0, 0, 0],
|
|
6364
|
-
center: [0, 0, 0],
|
|
6365
|
-
radius: 0
|
|
6366
|
-
};
|
|
5817
|
+
return { min: [0, 0, 0], max: [0, 0, 0], center: [0, 0, 0], radius: 0 };
|
|
6367
5818
|
}
|
|
6368
|
-
const min = [
|
|
6369
|
-
|
|
6370
|
-
splats[0].mean[1],
|
|
6371
|
-
splats[0].mean[2]
|
|
6372
|
-
];
|
|
6373
|
-
const max = [
|
|
6374
|
-
splats[0].mean[0],
|
|
6375
|
-
splats[0].mean[1],
|
|
6376
|
-
splats[0].mean[2]
|
|
6377
|
-
];
|
|
5819
|
+
const min = [splats[0].mean[0], splats[0].mean[1], splats[0].mean[2]];
|
|
5820
|
+
const max = [splats[0].mean[0], splats[0].mean[1], splats[0].mean[2]];
|
|
6378
5821
|
for (let i = 1; i < splats.length; i++) {
|
|
6379
5822
|
const [x, y, z] = splats[i].mean;
|
|
6380
5823
|
min[0] = Math.min(min[0], x);
|
|
@@ -6395,30 +5838,46 @@ class GSSplatRenderer {
|
|
|
6395
5838
|
const radius = Math.sqrt(dx * dx + dy * dy + dz * dz) / 2;
|
|
6396
5839
|
return { min, max, center, radius };
|
|
6397
5840
|
}
|
|
6398
|
-
|
|
6399
|
-
|
|
6400
|
-
|
|
6401
|
-
|
|
6402
|
-
|
|
6403
|
-
|
|
5841
|
+
computeBoundingBoxFromCompact(data) {
|
|
5842
|
+
if (data.count === 0) {
|
|
5843
|
+
return { min: [0, 0, 0], max: [0, 0, 0], center: [0, 0, 0], radius: 0 };
|
|
5844
|
+
}
|
|
5845
|
+
const positions = data.positions;
|
|
5846
|
+
const min = [positions[0], positions[1], positions[2]];
|
|
5847
|
+
const max = [positions[0], positions[1], positions[2]];
|
|
5848
|
+
for (let i = 1; i < data.count; i++) {
|
|
5849
|
+
const x = positions[i * 3 + 0];
|
|
5850
|
+
const y = positions[i * 3 + 1];
|
|
5851
|
+
const z = positions[i * 3 + 2];
|
|
5852
|
+
min[0] = Math.min(min[0], x);
|
|
5853
|
+
min[1] = Math.min(min[1], y);
|
|
5854
|
+
min[2] = Math.min(min[2], z);
|
|
5855
|
+
max[0] = Math.max(max[0], x);
|
|
5856
|
+
max[1] = Math.max(max[1], y);
|
|
5857
|
+
max[2] = Math.max(max[2], z);
|
|
5858
|
+
}
|
|
5859
|
+
const center = [
|
|
5860
|
+
(min[0] + max[0]) / 2,
|
|
5861
|
+
(min[1] + max[1]) / 2,
|
|
5862
|
+
(min[2] + max[2]) / 2
|
|
5863
|
+
];
|
|
5864
|
+
const dx = max[0] - min[0];
|
|
5865
|
+
const dy = max[1] - min[1];
|
|
5866
|
+
const dz = max[2] - min[2];
|
|
5867
|
+
const radius = Math.sqrt(dx * dx + dy * dy + dz * dz) / 2;
|
|
5868
|
+
return { min, max, center, radius };
|
|
5869
|
+
}
|
|
6404
5870
|
supportsSHMode(mode) {
|
|
6405
|
-
return mode >= SHMode
|
|
5871
|
+
return mode >= SHMode.L0 && mode <= SHMode.L3;
|
|
6406
5872
|
}
|
|
6407
|
-
/**
|
|
6408
|
-
* 获取渲染器能力
|
|
6409
|
-
*/
|
|
6410
5873
|
getCapabilities() {
|
|
6411
5874
|
return {
|
|
6412
|
-
maxSHMode: SHMode
|
|
5875
|
+
maxSHMode: SHMode.L3,
|
|
6413
5876
|
supportsRawData: true,
|
|
6414
5877
|
isMobileOptimized: false,
|
|
6415
5878
|
maxSplatCount: 0
|
|
6416
|
-
// 无限制(受 GPU 内存限制)
|
|
6417
5879
|
};
|
|
6418
5880
|
}
|
|
6419
|
-
/**
|
|
6420
|
-
* 销毁资源
|
|
6421
|
-
*/
|
|
6422
5881
|
destroy() {
|
|
6423
5882
|
if (this.splatBuffer) {
|
|
6424
5883
|
this.splatBuffer.destroy();
|
|
@@ -7679,20 +7138,20 @@ class GSSplatRendererMobile {
|
|
|
7679
7138
|
* 获取当前 SH 模式
|
|
7680
7139
|
*/
|
|
7681
7140
|
getSHMode() {
|
|
7682
|
-
return SHMode
|
|
7141
|
+
return SHMode.L0;
|
|
7683
7142
|
}
|
|
7684
7143
|
/**
|
|
7685
7144
|
* 是否支持指定的 SH 模式
|
|
7686
7145
|
*/
|
|
7687
7146
|
supportsSHMode(mode) {
|
|
7688
|
-
return mode === SHMode
|
|
7147
|
+
return mode === SHMode.L0;
|
|
7689
7148
|
}
|
|
7690
7149
|
/**
|
|
7691
7150
|
* 获取渲染器能力
|
|
7692
7151
|
*/
|
|
7693
7152
|
getCapabilities() {
|
|
7694
7153
|
return {
|
|
7695
|
-
maxSHMode: SHMode
|
|
7154
|
+
maxSHMode: SHMode.L0,
|
|
7696
7155
|
supportsRawData: false,
|
|
7697
7156
|
isMobileOptimized: true,
|
|
7698
7157
|
maxSplatCount: 0
|
|
@@ -7988,11 +7447,189 @@ class SceneManager {
|
|
|
7988
7447
|
setMeshRangeColor(startIndex, count, r, g, b, a = 1) {
|
|
7989
7448
|
return this.meshRenderer.setMeshRangeColor(startIndex, count, r, g, b, a);
|
|
7990
7449
|
}
|
|
7991
|
-
/**
|
|
7992
|
-
* 销毁场景管理器
|
|
7993
|
-
*/
|
|
7994
|
-
destroy() {
|
|
7995
|
-
this.clearSplats();
|
|
7450
|
+
/**
|
|
7451
|
+
* 销毁场景管理器
|
|
7452
|
+
*/
|
|
7453
|
+
destroy() {
|
|
7454
|
+
this.clearSplats();
|
|
7455
|
+
}
|
|
7456
|
+
}
|
|
7457
|
+
class SplatTransformProxy {
|
|
7458
|
+
constructor(renderer, center) {
|
|
7459
|
+
__publicField(this, "position");
|
|
7460
|
+
__publicField(this, "rotation");
|
|
7461
|
+
__publicField(this, "scale");
|
|
7462
|
+
__publicField(this, "renderer");
|
|
7463
|
+
__publicField(this, "center");
|
|
7464
|
+
this.renderer = renderer;
|
|
7465
|
+
this.center = [...center];
|
|
7466
|
+
renderer.setPivot(center[0], center[1], center[2]);
|
|
7467
|
+
const pos = renderer.getPosition();
|
|
7468
|
+
const rot = renderer.getRotation();
|
|
7469
|
+
const scl = renderer.getScale();
|
|
7470
|
+
this.position = [
|
|
7471
|
+
pos[0] + center[0],
|
|
7472
|
+
pos[1] + center[1],
|
|
7473
|
+
pos[2] + center[2]
|
|
7474
|
+
];
|
|
7475
|
+
this.rotation = [...rot];
|
|
7476
|
+
this.scale = [...scl];
|
|
7477
|
+
}
|
|
7478
|
+
setPosition(x, y, z) {
|
|
7479
|
+
this.position = [x, y, z];
|
|
7480
|
+
this.renderer.setPosition(
|
|
7481
|
+
x - this.center[0],
|
|
7482
|
+
y - this.center[1],
|
|
7483
|
+
z - this.center[2]
|
|
7484
|
+
);
|
|
7485
|
+
}
|
|
7486
|
+
setRotation(x, y, z) {
|
|
7487
|
+
this.rotation = [x, y, z];
|
|
7488
|
+
this.renderer.setRotation(x, y, z);
|
|
7489
|
+
}
|
|
7490
|
+
setScale(x, y, z) {
|
|
7491
|
+
this.scale = [x, y, z];
|
|
7492
|
+
this.renderer.setScale(x, y, z);
|
|
7493
|
+
}
|
|
7494
|
+
}
|
|
7495
|
+
class MeshGroupProxy {
|
|
7496
|
+
constructor(meshes) {
|
|
7497
|
+
__publicField(this, "position");
|
|
7498
|
+
__publicField(this, "rotation");
|
|
7499
|
+
__publicField(this, "scale");
|
|
7500
|
+
__publicField(this, "meshes");
|
|
7501
|
+
this.meshes = meshes;
|
|
7502
|
+
if (meshes.length > 0) {
|
|
7503
|
+
const firstMesh = meshes[0];
|
|
7504
|
+
this.position = [
|
|
7505
|
+
firstMesh.position[0],
|
|
7506
|
+
firstMesh.position[1],
|
|
7507
|
+
firstMesh.position[2]
|
|
7508
|
+
];
|
|
7509
|
+
this.rotation = [
|
|
7510
|
+
firstMesh.rotation[0],
|
|
7511
|
+
firstMesh.rotation[1],
|
|
7512
|
+
firstMesh.rotation[2]
|
|
7513
|
+
];
|
|
7514
|
+
this.scale = [
|
|
7515
|
+
firstMesh.scale[0],
|
|
7516
|
+
firstMesh.scale[1],
|
|
7517
|
+
firstMesh.scale[2]
|
|
7518
|
+
];
|
|
7519
|
+
} else {
|
|
7520
|
+
this.position = [0, 0, 0];
|
|
7521
|
+
this.rotation = [0, 0, 0];
|
|
7522
|
+
this.scale = [1, 1, 1];
|
|
7523
|
+
}
|
|
7524
|
+
}
|
|
7525
|
+
setPosition(x, y, z) {
|
|
7526
|
+
this.position = [x, y, z];
|
|
7527
|
+
for (const mesh of this.meshes) {
|
|
7528
|
+
mesh.setPosition(x, y, z);
|
|
7529
|
+
}
|
|
7530
|
+
}
|
|
7531
|
+
setRotation(x, y, z) {
|
|
7532
|
+
this.rotation = [x, y, z];
|
|
7533
|
+
for (const mesh of this.meshes) {
|
|
7534
|
+
mesh.setRotation(x, y, z);
|
|
7535
|
+
}
|
|
7536
|
+
}
|
|
7537
|
+
setScale(x, y, z) {
|
|
7538
|
+
this.scale = [x, y, z];
|
|
7539
|
+
for (const mesh of this.meshes) {
|
|
7540
|
+
mesh.setScale(x, y, z);
|
|
7541
|
+
}
|
|
7542
|
+
}
|
|
7543
|
+
/**
|
|
7544
|
+
* 获取组合包围盒
|
|
7545
|
+
*/
|
|
7546
|
+
getBoundingBox() {
|
|
7547
|
+
if (this.meshes.length === 0) return null;
|
|
7548
|
+
let combinedMin = null;
|
|
7549
|
+
let combinedMax = null;
|
|
7550
|
+
for (const mesh of this.meshes) {
|
|
7551
|
+
const bbox = mesh.getWorldBoundingBox();
|
|
7552
|
+
if (!bbox) continue;
|
|
7553
|
+
if (combinedMin === null || combinedMax === null) {
|
|
7554
|
+
combinedMin = [...bbox.min];
|
|
7555
|
+
combinedMax = [...bbox.max];
|
|
7556
|
+
} else {
|
|
7557
|
+
combinedMin[0] = Math.min(combinedMin[0], bbox.min[0]);
|
|
7558
|
+
combinedMin[1] = Math.min(combinedMin[1], bbox.min[1]);
|
|
7559
|
+
combinedMin[2] = Math.min(combinedMin[2], bbox.min[2]);
|
|
7560
|
+
combinedMax[0] = Math.max(combinedMax[0], bbox.max[0]);
|
|
7561
|
+
combinedMax[1] = Math.max(combinedMax[1], bbox.max[1]);
|
|
7562
|
+
combinedMax[2] = Math.max(combinedMax[2], bbox.max[2]);
|
|
7563
|
+
}
|
|
7564
|
+
}
|
|
7565
|
+
if (combinedMin === null || combinedMax === null) return null;
|
|
7566
|
+
return { min: combinedMin, max: combinedMax };
|
|
7567
|
+
}
|
|
7568
|
+
}
|
|
7569
|
+
class SplatBoundingBoxProvider {
|
|
7570
|
+
constructor(renderer) {
|
|
7571
|
+
__publicField(this, "renderer");
|
|
7572
|
+
this.renderer = renderer;
|
|
7573
|
+
}
|
|
7574
|
+
getBoundingBox() {
|
|
7575
|
+
const bbox = this.renderer.getBoundingBox();
|
|
7576
|
+
if (!bbox) return null;
|
|
7577
|
+
const position = this.renderer.getPosition();
|
|
7578
|
+
const rotation = this.renderer.getRotation();
|
|
7579
|
+
const scale = this.renderer.getScale();
|
|
7580
|
+
const pivot = this.renderer.getPivot();
|
|
7581
|
+
const corners = [
|
|
7582
|
+
[bbox.min[0], bbox.min[1], bbox.min[2]],
|
|
7583
|
+
[bbox.max[0], bbox.min[1], bbox.min[2]],
|
|
7584
|
+
[bbox.min[0], bbox.max[1], bbox.min[2]],
|
|
7585
|
+
[bbox.max[0], bbox.max[1], bbox.min[2]],
|
|
7586
|
+
[bbox.min[0], bbox.min[1], bbox.max[2]],
|
|
7587
|
+
[bbox.max[0], bbox.min[1], bbox.max[2]],
|
|
7588
|
+
[bbox.min[0], bbox.max[1], bbox.max[2]],
|
|
7589
|
+
[bbox.max[0], bbox.max[1], bbox.max[2]]
|
|
7590
|
+
];
|
|
7591
|
+
const [sx, sy, sz] = scale;
|
|
7592
|
+
const [rx, ry, rz] = rotation;
|
|
7593
|
+
const [tx, ty, tz] = position;
|
|
7594
|
+
const [px, py, pz] = pivot;
|
|
7595
|
+
const cx = Math.cos(rx), sx1 = Math.sin(rx);
|
|
7596
|
+
const cy = Math.cos(ry), sy1 = Math.sin(ry);
|
|
7597
|
+
const cz = Math.cos(rz), sz1 = Math.sin(rz);
|
|
7598
|
+
const r00 = cy * cz;
|
|
7599
|
+
const r01 = sx1 * sy1 * cz - cx * sz1;
|
|
7600
|
+
const r02 = cx * sy1 * cz + sx1 * sz1;
|
|
7601
|
+
const r10 = cy * sz1;
|
|
7602
|
+
const r11 = sx1 * sy1 * sz1 + cx * cz;
|
|
7603
|
+
const r12 = cx * sy1 * sz1 - sx1 * cz;
|
|
7604
|
+
const r20 = -sy1;
|
|
7605
|
+
const r21 = sx1 * cy;
|
|
7606
|
+
const r22 = cx * cy;
|
|
7607
|
+
const rs00 = r00 * sx, rs01 = r01 * sy, rs02 = r02 * sz;
|
|
7608
|
+
const rs10 = r10 * sx, rs11 = r11 * sy, rs12 = r12 * sz;
|
|
7609
|
+
const rs20 = r20 * sx, rs21 = r21 * sy, rs22 = r22 * sz;
|
|
7610
|
+
const dpx = px - (rs00 * px + rs01 * py + rs02 * pz);
|
|
7611
|
+
const dpy = py - (rs10 * px + rs11 * py + rs12 * pz);
|
|
7612
|
+
const dpz = pz - (rs20 * px + rs21 * py + rs22 * pz);
|
|
7613
|
+
const finalTx = tx + dpx;
|
|
7614
|
+
const finalTy = ty + dpy;
|
|
7615
|
+
const finalTz = tz + dpz;
|
|
7616
|
+
let minX = Infinity, minY = Infinity, minZ = Infinity;
|
|
7617
|
+
let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
|
|
7618
|
+
for (const [x, y, z] of corners) {
|
|
7619
|
+
const wx = rs00 * x + rs01 * y + rs02 * z + finalTx;
|
|
7620
|
+
const wy = rs10 * x + rs11 * y + rs12 * z + finalTy;
|
|
7621
|
+
const wz = rs20 * x + rs21 * y + rs22 * z + finalTz;
|
|
7622
|
+
minX = Math.min(minX, wx);
|
|
7623
|
+
minY = Math.min(minY, wy);
|
|
7624
|
+
minZ = Math.min(minZ, wz);
|
|
7625
|
+
maxX = Math.max(maxX, wx);
|
|
7626
|
+
maxY = Math.max(maxY, wy);
|
|
7627
|
+
maxZ = Math.max(maxZ, wz);
|
|
7628
|
+
}
|
|
7629
|
+
return {
|
|
7630
|
+
min: [minX, minY, minZ],
|
|
7631
|
+
max: [maxX, maxY, maxZ]
|
|
7632
|
+
};
|
|
7996
7633
|
}
|
|
7997
7634
|
}
|
|
7998
7635
|
class Vec3 {
|
|
@@ -11177,181 +10814,6 @@ class TransformGizmoV2 {
|
|
|
11177
10814
|
this.bindGroupLayout = null;
|
|
11178
10815
|
}
|
|
11179
10816
|
}
|
|
11180
|
-
class SplatTransformProxy {
|
|
11181
|
-
constructor(renderer, center) {
|
|
11182
|
-
__publicField(this, "position");
|
|
11183
|
-
__publicField(this, "rotation");
|
|
11184
|
-
__publicField(this, "scale");
|
|
11185
|
-
__publicField(this, "renderer");
|
|
11186
|
-
__publicField(this, "center");
|
|
11187
|
-
this.renderer = renderer;
|
|
11188
|
-
this.center = [...center];
|
|
11189
|
-
renderer.setPivot(center[0], center[1], center[2]);
|
|
11190
|
-
const pos = renderer.getPosition();
|
|
11191
|
-
const rot = renderer.getRotation();
|
|
11192
|
-
const scl = renderer.getScale();
|
|
11193
|
-
this.position = [
|
|
11194
|
-
pos[0] + center[0],
|
|
11195
|
-
pos[1] + center[1],
|
|
11196
|
-
pos[2] + center[2]
|
|
11197
|
-
];
|
|
11198
|
-
this.rotation = [...rot];
|
|
11199
|
-
this.scale = [...scl];
|
|
11200
|
-
}
|
|
11201
|
-
setPosition(x, y, z) {
|
|
11202
|
-
this.position = [x, y, z];
|
|
11203
|
-
this.renderer.setPosition(
|
|
11204
|
-
x - this.center[0],
|
|
11205
|
-
y - this.center[1],
|
|
11206
|
-
z - this.center[2]
|
|
11207
|
-
);
|
|
11208
|
-
}
|
|
11209
|
-
setRotation(x, y, z) {
|
|
11210
|
-
this.rotation = [x, y, z];
|
|
11211
|
-
this.renderer.setRotation(x, y, z);
|
|
11212
|
-
}
|
|
11213
|
-
setScale(x, y, z) {
|
|
11214
|
-
this.scale = [x, y, z];
|
|
11215
|
-
this.renderer.setScale(x, y, z);
|
|
11216
|
-
}
|
|
11217
|
-
}
|
|
11218
|
-
class MeshGroupProxy {
|
|
11219
|
-
constructor(meshes) {
|
|
11220
|
-
__publicField(this, "position");
|
|
11221
|
-
__publicField(this, "rotation");
|
|
11222
|
-
__publicField(this, "scale");
|
|
11223
|
-
__publicField(this, "meshes");
|
|
11224
|
-
this.meshes = meshes;
|
|
11225
|
-
if (meshes.length > 0) {
|
|
11226
|
-
const firstMesh = meshes[0];
|
|
11227
|
-
this.position = [
|
|
11228
|
-
firstMesh.position[0],
|
|
11229
|
-
firstMesh.position[1],
|
|
11230
|
-
firstMesh.position[2]
|
|
11231
|
-
];
|
|
11232
|
-
this.rotation = [
|
|
11233
|
-
firstMesh.rotation[0],
|
|
11234
|
-
firstMesh.rotation[1],
|
|
11235
|
-
firstMesh.rotation[2]
|
|
11236
|
-
];
|
|
11237
|
-
this.scale = [
|
|
11238
|
-
firstMesh.scale[0],
|
|
11239
|
-
firstMesh.scale[1],
|
|
11240
|
-
firstMesh.scale[2]
|
|
11241
|
-
];
|
|
11242
|
-
} else {
|
|
11243
|
-
this.position = [0, 0, 0];
|
|
11244
|
-
this.rotation = [0, 0, 0];
|
|
11245
|
-
this.scale = [1, 1, 1];
|
|
11246
|
-
}
|
|
11247
|
-
}
|
|
11248
|
-
setPosition(x, y, z) {
|
|
11249
|
-
this.position = [x, y, z];
|
|
11250
|
-
for (const mesh of this.meshes) {
|
|
11251
|
-
mesh.setPosition(x, y, z);
|
|
11252
|
-
}
|
|
11253
|
-
}
|
|
11254
|
-
setRotation(x, y, z) {
|
|
11255
|
-
this.rotation = [x, y, z];
|
|
11256
|
-
for (const mesh of this.meshes) {
|
|
11257
|
-
mesh.setRotation(x, y, z);
|
|
11258
|
-
}
|
|
11259
|
-
}
|
|
11260
|
-
setScale(x, y, z) {
|
|
11261
|
-
this.scale = [x, y, z];
|
|
11262
|
-
for (const mesh of this.meshes) {
|
|
11263
|
-
mesh.setScale(x, y, z);
|
|
11264
|
-
}
|
|
11265
|
-
}
|
|
11266
|
-
getBoundingBox() {
|
|
11267
|
-
if (this.meshes.length === 0) return null;
|
|
11268
|
-
let combinedMin = null;
|
|
11269
|
-
let combinedMax = null;
|
|
11270
|
-
for (const mesh of this.meshes) {
|
|
11271
|
-
const bbox = mesh.getWorldBoundingBox();
|
|
11272
|
-
if (!bbox) continue;
|
|
11273
|
-
if (combinedMin === null || combinedMax === null) {
|
|
11274
|
-
combinedMin = [...bbox.min];
|
|
11275
|
-
combinedMax = [...bbox.max];
|
|
11276
|
-
} else {
|
|
11277
|
-
combinedMin[0] = Math.min(combinedMin[0], bbox.min[0]);
|
|
11278
|
-
combinedMin[1] = Math.min(combinedMin[1], bbox.min[1]);
|
|
11279
|
-
combinedMin[2] = Math.min(combinedMin[2], bbox.min[2]);
|
|
11280
|
-
combinedMax[0] = Math.max(combinedMax[0], bbox.max[0]);
|
|
11281
|
-
combinedMax[1] = Math.max(combinedMax[1], bbox.max[1]);
|
|
11282
|
-
combinedMax[2] = Math.max(combinedMax[2], bbox.max[2]);
|
|
11283
|
-
}
|
|
11284
|
-
}
|
|
11285
|
-
if (combinedMin === null || combinedMax === null) return null;
|
|
11286
|
-
return { min: combinedMin, max: combinedMax };
|
|
11287
|
-
}
|
|
11288
|
-
}
|
|
11289
|
-
class SplatBoundingBoxProvider {
|
|
11290
|
-
constructor(renderer) {
|
|
11291
|
-
__publicField(this, "renderer");
|
|
11292
|
-
this.renderer = renderer;
|
|
11293
|
-
}
|
|
11294
|
-
getBoundingBox() {
|
|
11295
|
-
const bbox = this.renderer.getBoundingBox();
|
|
11296
|
-
if (!bbox) return null;
|
|
11297
|
-
const position = this.renderer.getPosition();
|
|
11298
|
-
const rotation = this.renderer.getRotation();
|
|
11299
|
-
const scale = this.renderer.getScale();
|
|
11300
|
-
const pivot = this.renderer.getPivot();
|
|
11301
|
-
const corners = [
|
|
11302
|
-
[bbox.min[0], bbox.min[1], bbox.min[2]],
|
|
11303
|
-
[bbox.max[0], bbox.min[1], bbox.min[2]],
|
|
11304
|
-
[bbox.min[0], bbox.max[1], bbox.min[2]],
|
|
11305
|
-
[bbox.max[0], bbox.max[1], bbox.min[2]],
|
|
11306
|
-
[bbox.min[0], bbox.min[1], bbox.max[2]],
|
|
11307
|
-
[bbox.max[0], bbox.min[1], bbox.max[2]],
|
|
11308
|
-
[bbox.min[0], bbox.max[1], bbox.max[2]],
|
|
11309
|
-
[bbox.max[0], bbox.max[1], bbox.max[2]]
|
|
11310
|
-
];
|
|
11311
|
-
const [sx, sy, sz] = scale;
|
|
11312
|
-
const [rx, ry, rz] = rotation;
|
|
11313
|
-
const [tx, ty, tz] = position;
|
|
11314
|
-
const [px, py, pz] = pivot;
|
|
11315
|
-
const cx = Math.cos(rx), sx1 = Math.sin(rx);
|
|
11316
|
-
const cy = Math.cos(ry), sy1 = Math.sin(ry);
|
|
11317
|
-
const cz = Math.cos(rz), sz1 = Math.sin(rz);
|
|
11318
|
-
const r00 = cy * cz;
|
|
11319
|
-
const r01 = sx1 * sy1 * cz - cx * sz1;
|
|
11320
|
-
const r02 = cx * sy1 * cz + sx1 * sz1;
|
|
11321
|
-
const r10 = cy * sz1;
|
|
11322
|
-
const r11 = sx1 * sy1 * sz1 + cx * cz;
|
|
11323
|
-
const r12 = cx * sy1 * sz1 - sx1 * cz;
|
|
11324
|
-
const r20 = -sy1;
|
|
11325
|
-
const r21 = sx1 * cy;
|
|
11326
|
-
const r22 = cx * cy;
|
|
11327
|
-
const rs00 = r00 * sx, rs01 = r01 * sy, rs02 = r02 * sz;
|
|
11328
|
-
const rs10 = r10 * sx, rs11 = r11 * sy, rs12 = r12 * sz;
|
|
11329
|
-
const rs20 = r20 * sx, rs21 = r21 * sy, rs22 = r22 * sz;
|
|
11330
|
-
const dpx = px - (rs00 * px + rs01 * py + rs02 * pz);
|
|
11331
|
-
const dpy = py - (rs10 * px + rs11 * py + rs12 * pz);
|
|
11332
|
-
const dpz = pz - (rs20 * px + rs21 * py + rs22 * pz);
|
|
11333
|
-
const finalTx = tx + dpx;
|
|
11334
|
-
const finalTy = ty + dpy;
|
|
11335
|
-
const finalTz = tz + dpz;
|
|
11336
|
-
let minX = Infinity, minY = Infinity, minZ = Infinity;
|
|
11337
|
-
let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
|
|
11338
|
-
for (const [x, y, z] of corners) {
|
|
11339
|
-
const wx = rs00 * x + rs01 * y + rs02 * z + finalTx;
|
|
11340
|
-
const wy = rs10 * x + rs11 * y + rs12 * z + finalTy;
|
|
11341
|
-
const wz = rs20 * x + rs21 * y + rs22 * z + finalTz;
|
|
11342
|
-
minX = Math.min(minX, wx);
|
|
11343
|
-
minY = Math.min(minY, wy);
|
|
11344
|
-
minZ = Math.min(minZ, wz);
|
|
11345
|
-
maxX = Math.max(maxX, wx);
|
|
11346
|
-
maxY = Math.max(maxY, wy);
|
|
11347
|
-
maxZ = Math.max(maxZ, wz);
|
|
11348
|
-
}
|
|
11349
|
-
return {
|
|
11350
|
-
min: [minX, minY, minZ],
|
|
11351
|
-
max: [maxX, maxY, maxZ]
|
|
11352
|
-
};
|
|
11353
|
-
}
|
|
11354
|
-
}
|
|
11355
10817
|
class GizmoManager {
|
|
11356
10818
|
constructor(renderer, camera, canvas, controls) {
|
|
11357
10819
|
__publicField(this, "renderer");
|
|
@@ -11511,15 +10973,6 @@ class GizmoManager {
|
|
|
11511
10973
|
this.boundingBoxRenderer.destroy();
|
|
11512
10974
|
}
|
|
11513
10975
|
}
|
|
11514
|
-
function isMobileDevice() {
|
|
11515
|
-
if (typeof navigator === "undefined") return false;
|
|
11516
|
-
const ua = navigator.userAgent || navigator.vendor || window.opera || "";
|
|
11517
|
-
const isMobileUA = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(ua.toLowerCase());
|
|
11518
|
-
const hasTouch = "ontouchstart" in window || navigator.maxTouchPoints > 0;
|
|
11519
|
-
const isSmallScreen = window.innerWidth <= 768;
|
|
11520
|
-
const isIPadAsMac = navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1;
|
|
11521
|
-
return isMobileUA || isIPadAsMac || hasTouch && isSmallScreen;
|
|
11522
|
-
}
|
|
11523
10976
|
class App {
|
|
11524
10977
|
constructor(canvas) {
|
|
11525
10978
|
__publicField(this, "canvas");
|
|
@@ -11637,7 +11090,6 @@ class App {
|
|
|
11637
11090
|
} else {
|
|
11638
11091
|
gsRenderer = new GSSplatRenderer(this.renderer, this.camera);
|
|
11639
11092
|
this.useMobileRenderer = false;
|
|
11640
|
-
const tier = gsRenderer.getPerformanceTier();
|
|
11641
11093
|
const compactData = await this.parsePLYBuffer(buffer, {
|
|
11642
11094
|
maxSplats: Infinity,
|
|
11643
11095
|
loadSH: true,
|
|
@@ -11974,6 +11426,8 @@ export {
|
|
|
11974
11426
|
App,
|
|
11975
11427
|
BoundingBoxRenderer,
|
|
11976
11428
|
Camera,
|
|
11429
|
+
DEFAULT_MATERIAL,
|
|
11430
|
+
DEFAULT_OBJ_MATERIAL,
|
|
11977
11431
|
GLBLoader,
|
|
11978
11432
|
SHMode as GSSHMode,
|
|
11979
11433
|
GSSplatRenderer,
|
|
@@ -11992,21 +11446,33 @@ export {
|
|
|
11992
11446
|
OBJLoader,
|
|
11993
11447
|
OBJParser,
|
|
11994
11448
|
OrbitControls,
|
|
11995
|
-
PerformanceTier,
|
|
11996
11449
|
Renderer,
|
|
11450
|
+
SHMode,
|
|
11997
11451
|
SceneManager,
|
|
11998
11452
|
SplatBoundingBoxProvider,
|
|
11999
11453
|
SplatTransformProxy,
|
|
11454
|
+
TextureCache,
|
|
12000
11455
|
TransformGizmoV2,
|
|
12001
11456
|
ViewportGizmo,
|
|
12002
11457
|
calculateTextureDimensions,
|
|
12003
11458
|
compactDataToGPUBuffer,
|
|
12004
11459
|
compressSplatsToTextures,
|
|
11460
|
+
computeBoundingBox$1 as computeBoundingBox,
|
|
11461
|
+
createBoundingBoxFromMinMax,
|
|
11462
|
+
createTextureFromImageBitmap,
|
|
12005
11463
|
deserializeSplat,
|
|
12006
11464
|
destroyCompressedTextures,
|
|
11465
|
+
getRecommendedDPR,
|
|
11466
|
+
isMobileDevice,
|
|
11467
|
+
isWebGPUSupported,
|
|
12007
11468
|
loadPLY,
|
|
12008
11469
|
loadPLYMobile,
|
|
12009
11470
|
loadSplat,
|
|
12010
|
-
|
|
11471
|
+
loadTextureFromBlob,
|
|
11472
|
+
loadTextureFromBuffer,
|
|
11473
|
+
loadTextureFromURL,
|
|
11474
|
+
mergeBoundingBoxes,
|
|
11475
|
+
parsePLYBuffer,
|
|
11476
|
+
transformBoundingBox
|
|
12011
11477
|
};
|
|
12012
11478
|
//# sourceMappingURL=3dgs-lib.js.map
|