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