s3m-three 0.3.9 → 0.3.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser/s3m-three.iife.ext-three.js +174 -40
- package/dist/browser/s3m-three.iife.js +174 -40
- package/dist/core/types.d.ts +16 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/render/EdgeLines.d.ts +12 -0
- package/dist/render/EdgeLines.d.ts.map +1 -1
- package/dist/render/EdgeLines.js +60 -6
- package/dist/render/EdgeLines.js.map +1 -1
- package/dist/render/FeatureStyleManager.d.ts +33 -0
- package/dist/render/FeatureStyleManager.d.ts.map +1 -1
- package/dist/render/FeatureStyleManager.js +98 -11
- package/dist/render/FeatureStyleManager.js.map +1 -1
- package/dist/render/GPUPathManager.d.ts +95 -0
- package/dist/render/GPUPathManager.d.ts.map +1 -0
- package/dist/render/GPUPathManager.js +265 -0
- package/dist/render/GPUPathManager.js.map +1 -0
- package/dist/render/S3MViewer.d.ts +47 -0
- package/dist/render/S3MViewer.d.ts.map +1 -1
- package/dist/render/S3MViewer.js +96 -0
- package/dist/render/S3MViewer.js.map +1 -1
- package/dist/render/StyleMaterial.d.ts +12 -0
- package/dist/render/StyleMaterial.d.ts.map +1 -1
- package/dist/render/StyleMaterial.js +117 -14
- package/dist/render/StyleMaterial.js.map +1 -1
- package/package.json +1 -1
- package/src/core/types.ts +8 -0
- package/src/index.ts +3 -1
- package/src/render/EdgeLines.ts +64 -6
- package/src/render/FeatureStyleManager.ts +130 -11
- package/src/render/GPUPathManager.ts +306 -0
- package/src/render/S3MViewer.ts +110 -0
- package/src/render/StyleMaterial.ts +121 -14
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GPU 路径动画管理器
|
|
3
|
+
*
|
|
4
|
+
* 负责:
|
|
5
|
+
* 1. 注册与管理多条三维离散路径(Path)
|
|
6
|
+
* 2. 将空间折线等弧长采样并烘焙为 Float32Array 格式的 2D 数据纹理 (uPathTex):
|
|
7
|
+
* - Width: SAMPLES_PER_PATH (默认 256)
|
|
8
|
+
* - Height: max(pathCount, 1)
|
|
9
|
+
* - Format: RGBAFormat, FloatType
|
|
10
|
+
* - 像素通道:
|
|
11
|
+
* - R: X 坐标 (米)
|
|
12
|
+
* - G: Y 坐标 (米)
|
|
13
|
+
* - B: Z 坐标 (米)
|
|
14
|
+
* - A: 切线偏航角 yaw (弧度, atan2(dy, dx), 经过连续角度展开 unwrap)
|
|
15
|
+
* 3. 硬件双线性插值 (LinearFilter) 支持:
|
|
16
|
+
* GPU 在顶点着色器中按归一化进度 t (0~1) 采样,硬件自动插值获得平滑轨迹与朝向
|
|
17
|
+
*/
|
|
18
|
+
import * as THREE from 'three';
|
|
19
|
+
import { Path } from '../anim/Path.js';
|
|
20
|
+
|
|
21
|
+
export const SAMPLES_PER_PATH = 256;
|
|
22
|
+
|
|
23
|
+
export interface GPUPathInfo {
|
|
24
|
+
id: number;
|
|
25
|
+
name?: string;
|
|
26
|
+
path: Path;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class GPUPathManager {
|
|
30
|
+
private _paths: Map<number, GPUPathInfo> = new Map();
|
|
31
|
+
private _nextId = 0;
|
|
32
|
+
private _texture: THREE.DataTexture | null = null;
|
|
33
|
+
private _data: Float32Array | null = null;
|
|
34
|
+
private _dirty = false;
|
|
35
|
+
|
|
36
|
+
/** 默认空纹理,防止 WebGL 未绑定报错 */
|
|
37
|
+
static readonly DUMMY_PATH_TEX: THREE.DataTexture = (() => {
|
|
38
|
+
const data = new Float32Array(SAMPLES_PER_PATH * 1 * 4);
|
|
39
|
+
const tex = new THREE.DataTexture(data, SAMPLES_PER_PATH, 1, THREE.RGBAFormat, THREE.FloatType);
|
|
40
|
+
tex.minFilter = THREE.LinearFilter;
|
|
41
|
+
tex.magFilter = THREE.LinearFilter;
|
|
42
|
+
tex.wrapS = THREE.ClampToEdgeWrapping;
|
|
43
|
+
tex.wrapT = THREE.ClampToEdgeWrapping;
|
|
44
|
+
tex.needsUpdate = true;
|
|
45
|
+
return tex;
|
|
46
|
+
})();
|
|
47
|
+
|
|
48
|
+
constructor() {}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 注册一条路径到 GPU 路径库
|
|
52
|
+
* @param pointsOrPath 折线点数组或现有的 Path 实例
|
|
53
|
+
* @param opts 注册选项(可指定固定 id 或 name)
|
|
54
|
+
* @returns 分配的路径 ID (从 0 开始的连续或离散索引)
|
|
55
|
+
*/
|
|
56
|
+
registerPath(
|
|
57
|
+
pointsOrPath: Path | ArrayLike<number> | Array<[number, number, number]>,
|
|
58
|
+
opts: { id?: number; name?: string; closed?: boolean } = {}
|
|
59
|
+
): number {
|
|
60
|
+
const path =
|
|
61
|
+
pointsOrPath instanceof Path
|
|
62
|
+
? pointsOrPath
|
|
63
|
+
: new Path(pointsOrPath, { closed: opts.closed });
|
|
64
|
+
|
|
65
|
+
let id = opts.id;
|
|
66
|
+
if (id == null) {
|
|
67
|
+
id = this._nextId++;
|
|
68
|
+
while (this._paths.has(id)) {
|
|
69
|
+
id = this._nextId++;
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
if (id >= this._nextId) {
|
|
73
|
+
this._nextId = id + 1;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
this._paths.set(id, { id, name: opts.name, path });
|
|
78
|
+
this._dirty = true;
|
|
79
|
+
return id;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 注销一条路径
|
|
84
|
+
*/
|
|
85
|
+
unregisterPath(id: number): boolean {
|
|
86
|
+
const removed = this._paths.delete(id);
|
|
87
|
+
if (removed) {
|
|
88
|
+
this._dirty = true;
|
|
89
|
+
}
|
|
90
|
+
return removed;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 获取指定 ID 的路径对象
|
|
95
|
+
*/
|
|
96
|
+
getPath(id: number): Path | undefined {
|
|
97
|
+
return this._paths.get(id)?.path;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 是否存在指定 ID 的路径
|
|
102
|
+
*/
|
|
103
|
+
hasPath(id: number): boolean {
|
|
104
|
+
return this._paths.has(id);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 获取所有已注册的路径 ID
|
|
109
|
+
*/
|
|
110
|
+
getPathIds(): number[] {
|
|
111
|
+
return Array.from(this._paths.keys()).sort((a, b) => a - b);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 清空所有已注册的路径
|
|
116
|
+
*/
|
|
117
|
+
clear(): void {
|
|
118
|
+
if (this._paths.size > 0) {
|
|
119
|
+
this._paths.clear();
|
|
120
|
+
this._nextId = 0;
|
|
121
|
+
this._dirty = true;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 当前注册的有效路径数量
|
|
127
|
+
*/
|
|
128
|
+
get count(): number {
|
|
129
|
+
return this._paths.size;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 获取 GPU 路径数据纹理,若有变动会自动触发重新烘焙
|
|
134
|
+
*/
|
|
135
|
+
getTexture(): THREE.DataTexture {
|
|
136
|
+
if (this._dirty || !this._texture) {
|
|
137
|
+
this._bake();
|
|
138
|
+
}
|
|
139
|
+
return this._texture ?? GPUPathManager.DUMMY_PATH_TEX;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 将当前路径纹理与 uniform 状态同步到材质 uniform 字典
|
|
144
|
+
*/
|
|
145
|
+
updateUniforms(uniforms: Record<string, any>): void {
|
|
146
|
+
const tex = this.getTexture();
|
|
147
|
+
const active = this._paths.size > 0 ? 1 : 0;
|
|
148
|
+
const maxId = this._getMaxPathId();
|
|
149
|
+
const height = Math.max(maxId + 1, 1);
|
|
150
|
+
|
|
151
|
+
if (uniforms.uPathTex) uniforms.uPathTex.value = tex;
|
|
152
|
+
if (uniforms.uPathCount) uniforms.uPathCount.value = height;
|
|
153
|
+
if (uniforms.uPathTexActive) uniforms.uPathTexActive.value = active;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 烘焙路径数据到纹理
|
|
158
|
+
*/
|
|
159
|
+
private _bake(): void {
|
|
160
|
+
this._dirty = false;
|
|
161
|
+
if (this._paths.size === 0) {
|
|
162
|
+
if (this._texture && this._texture !== GPUPathManager.DUMMY_PATH_TEX) {
|
|
163
|
+
this._texture.dispose();
|
|
164
|
+
}
|
|
165
|
+
this._texture = GPUPathManager.DUMMY_PATH_TEX;
|
|
166
|
+
this._data = null;
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const maxId = this._getMaxPathId();
|
|
171
|
+
const height = Math.max(maxId + 1, 1);
|
|
172
|
+
const totalPixels = SAMPLES_PER_PATH * height;
|
|
173
|
+
const bufferLength = totalPixels * 4;
|
|
174
|
+
|
|
175
|
+
if (!this._data || this._data.length !== bufferLength) {
|
|
176
|
+
if (this._texture && this._texture !== GPUPathManager.DUMMY_PATH_TEX) {
|
|
177
|
+
this._texture.dispose();
|
|
178
|
+
}
|
|
179
|
+
this._data = new Float32Array(bufferLength);
|
|
180
|
+
this._texture = new THREE.DataTexture(
|
|
181
|
+
this._data,
|
|
182
|
+
SAMPLES_PER_PATH,
|
|
183
|
+
height,
|
|
184
|
+
THREE.RGBAFormat,
|
|
185
|
+
THREE.FloatType
|
|
186
|
+
);
|
|
187
|
+
this._texture.minFilter = THREE.LinearFilter;
|
|
188
|
+
this._texture.magFilter = THREE.LinearFilter;
|
|
189
|
+
this._texture.wrapS = THREE.ClampToEdgeWrapping;
|
|
190
|
+
this._texture.wrapT = THREE.ClampToEdgeWrapping;
|
|
191
|
+
} else {
|
|
192
|
+
this._data.fill(0);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const scratchA = [0, 0, 0];
|
|
196
|
+
const scratchB = [0, 0, 0];
|
|
197
|
+
const N = SAMPLES_PER_PATH;
|
|
198
|
+
|
|
199
|
+
for (const [id, info] of this._paths) {
|
|
200
|
+
if (id < 0 || id >= height) continue;
|
|
201
|
+
const path = info.path;
|
|
202
|
+
const rowOffset = id * N * 4;
|
|
203
|
+
|
|
204
|
+
// 提取 N 个等弧长采样点位置与航向切线角
|
|
205
|
+
let lastYaw = 0;
|
|
206
|
+
for (let i = 0; i < N; i++) {
|
|
207
|
+
const t = i / (N - 1);
|
|
208
|
+
path.sampleAt(t, scratchA);
|
|
209
|
+
|
|
210
|
+
// 计算切线方向:取一个极小的前向步长
|
|
211
|
+
let yaw = lastYaw;
|
|
212
|
+
if (path.length > 1e-4) {
|
|
213
|
+
const deltaT = Math.min(1.0, t + 1e-3);
|
|
214
|
+
path.sampleAt(deltaT, scratchB);
|
|
215
|
+
let dx = scratchB[0] - scratchA[0];
|
|
216
|
+
let dy = scratchB[1] - scratchA[1];
|
|
217
|
+
if (dx * dx + dy * dy < 1e-8 && t > 0) {
|
|
218
|
+
// 前向位移过小则尝试后向差分
|
|
219
|
+
const prevT = Math.max(0.0, t - 1e-3);
|
|
220
|
+
path.sampleAt(prevT, scratchB);
|
|
221
|
+
dx = scratchA[0] - scratchB[0];
|
|
222
|
+
dy = scratchA[1] - scratchB[1];
|
|
223
|
+
}
|
|
224
|
+
if (dx * dx + dy * dy >= 1e-8) {
|
|
225
|
+
yaw = Math.atan2(dy, dx);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 连续角度展开 (Angle Unwrap):避免在 ±PI 处发生 360° 线性插值回旋
|
|
230
|
+
if (i > 0) {
|
|
231
|
+
let diff = yaw - lastYaw;
|
|
232
|
+
while (diff > Math.PI) {
|
|
233
|
+
yaw -= 2 * Math.PI;
|
|
234
|
+
diff = yaw - lastYaw;
|
|
235
|
+
}
|
|
236
|
+
while (diff < -Math.PI) {
|
|
237
|
+
yaw += 2 * Math.PI;
|
|
238
|
+
diff = yaw - lastYaw;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
lastYaw = yaw;
|
|
242
|
+
|
|
243
|
+
const pIdx = rowOffset + i * 4;
|
|
244
|
+
this._data[pIdx + 0] = scratchA[0];
|
|
245
|
+
this._data[pIdx + 1] = scratchA[1];
|
|
246
|
+
this._data[pIdx + 2] = scratchA[2];
|
|
247
|
+
this._data[pIdx + 3] = yaw;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (this._texture) {
|
|
252
|
+
this._texture.needsUpdate = true;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private _getMaxPathId(): number {
|
|
257
|
+
let max = 0;
|
|
258
|
+
for (const id of this._paths.keys()) {
|
|
259
|
+
if (id > max) max = id;
|
|
260
|
+
}
|
|
261
|
+
return max;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* 纯数学模拟采样烘焙纹理(用于单测验证硬件线性插值精度)
|
|
266
|
+
*/
|
|
267
|
+
sampleBaked(pathId: number, t: number): { position: [number, number, number]; yaw: number } {
|
|
268
|
+
if (!this._data || !this._paths.has(pathId)) {
|
|
269
|
+
return { position: [0, 0, 0], yaw: 0 };
|
|
270
|
+
}
|
|
271
|
+
const maxId = this._getMaxPathId();
|
|
272
|
+
if (pathId < 0 || pathId > maxId) {
|
|
273
|
+
return { position: [0, 0, 0], yaw: 0 };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const N = SAMPLES_PER_PATH;
|
|
277
|
+
const clampedT = Math.min(1.0, Math.max(0.0, t));
|
|
278
|
+
const sampleCoord = clampedT * (N - 1);
|
|
279
|
+
const i0 = Math.floor(sampleCoord);
|
|
280
|
+
const i1 = Math.min(N - 1, i0 + 1);
|
|
281
|
+
const frac = sampleCoord - i0;
|
|
282
|
+
|
|
283
|
+
const rowOffset = pathId * N * 4;
|
|
284
|
+
const idx0 = rowOffset + i0 * 4;
|
|
285
|
+
const idx1 = rowOffset + i1 * 4;
|
|
286
|
+
|
|
287
|
+
const x = this._data[idx0 + 0] * (1 - frac) + this._data[idx1 + 0] * frac;
|
|
288
|
+
const y = this._data[idx0 + 1] * (1 - frac) + this._data[idx1 + 1] * frac;
|
|
289
|
+
const z = this._data[idx0 + 2] * (1 - frac) + this._data[idx1 + 2] * frac;
|
|
290
|
+
const yaw = this._data[idx0 + 3] * (1 - frac) + this._data[idx1 + 3] * frac;
|
|
291
|
+
|
|
292
|
+
return { position: [x, y, z], yaw };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* 释放纹理与显存
|
|
297
|
+
*/
|
|
298
|
+
dispose(): void {
|
|
299
|
+
if (this._texture && this._texture !== GPUPathManager.DUMMY_PATH_TEX) {
|
|
300
|
+
this._texture.dispose();
|
|
301
|
+
this._texture = null;
|
|
302
|
+
}
|
|
303
|
+
this._data = null;
|
|
304
|
+
this._paths.clear();
|
|
305
|
+
}
|
|
306
|
+
}
|
package/src/render/S3MViewer.ts
CHANGED
|
@@ -41,6 +41,8 @@ import type { Channel, TransitionOptions } from '../anim/Animator.js';
|
|
|
41
41
|
import { Path } from '../anim/Path.js';
|
|
42
42
|
import type { EasingFn, EasingName } from '../anim/easing.js';
|
|
43
43
|
import type { LineSharedUniforms, ThickLineMaterial, ThickLineStyleUniforms } from './EdgeLines.js';
|
|
44
|
+
import { GPUPathManager } from './GPUPathManager.js';
|
|
45
|
+
import type { GPUPathOptions } from './FeatureStyleManager.js';
|
|
44
46
|
import {
|
|
45
47
|
buildFeatureEdgeSegments,
|
|
46
48
|
buildPositionBatchMap,
|
|
@@ -370,6 +372,9 @@ export class S3MViewer {
|
|
|
370
372
|
private readonly _datasetManager = new DatasetManager();
|
|
371
373
|
/** 单体样式、专题着色与状态纹理管理器 */
|
|
372
374
|
private readonly _featureStyleManager: FeatureStyleManager;
|
|
375
|
+
/** GPU 路径动画管理器 */
|
|
376
|
+
readonly gpuPathManager: GPUPathManager = new GPUPathManager();
|
|
377
|
+
private _customPathTime = false;
|
|
373
378
|
|
|
374
379
|
/** 兼容 datasets 映射表暴露 */
|
|
375
380
|
get datasets(): Map<string, S3MDatasetEntry> {
|
|
@@ -1132,6 +1137,96 @@ export class S3MViewer {
|
|
|
1132
1137
|
this.animator.unregister(key);
|
|
1133
1138
|
this._pathSpecs.delete(key);
|
|
1134
1139
|
}
|
|
1140
|
+
|
|
1141
|
+
/**
|
|
1142
|
+
* 注册 GPU 路径到路径库
|
|
1143
|
+
* @param points 折线点数组或 Path 实例
|
|
1144
|
+
* @param opts 选项(可指定路径 ID 或闭合)
|
|
1145
|
+
* @returns 路径 ID
|
|
1146
|
+
*/
|
|
1147
|
+
registerGPUPath(
|
|
1148
|
+
points: Path | ArrayLike<number> | Array<[number, number, number]>,
|
|
1149
|
+
opts: { id?: number; name?: string; closed?: boolean } = {}
|
|
1150
|
+
): number {
|
|
1151
|
+
const id = this.gpuPathManager.registerPath(points, opts);
|
|
1152
|
+
this.gpuPathManager.updateUniforms(this.uniforms);
|
|
1153
|
+
return id;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
/**
|
|
1157
|
+
* 注销 GPU 路径
|
|
1158
|
+
*/
|
|
1159
|
+
unregisterGPUPath(id: number): boolean {
|
|
1160
|
+
const res = this.gpuPathManager.unregisterPath(id);
|
|
1161
|
+
this.gpuPathManager.updateUniforms(this.uniforms);
|
|
1162
|
+
return res;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
/**
|
|
1166
|
+
* 获取 GPUPathManager 实例
|
|
1167
|
+
*/
|
|
1168
|
+
getGPUPathManager(): GPUPathManager {
|
|
1169
|
+
return this.gpuPathManager;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
/**
|
|
1173
|
+
* 将单体绑定到 GPU 路径动画(智慧交通车流 / 大批量巡检无人机)
|
|
1174
|
+
* @param smid 单体 ID
|
|
1175
|
+
* @param path 路径 ID 或新的路径折线
|
|
1176
|
+
* @param options 动画配置(速度、相位、循环模式、自动朝向等)
|
|
1177
|
+
*/
|
|
1178
|
+
bindGPUPath(
|
|
1179
|
+
smid: number,
|
|
1180
|
+
path: number | Path | ArrayLike<number> | Array<[number, number, number]>,
|
|
1181
|
+
options: Omit<GPUPathOptions, 'pathId'> = {}
|
|
1182
|
+
): number {
|
|
1183
|
+
let pathId: number;
|
|
1184
|
+
if (typeof path === 'number') {
|
|
1185
|
+
pathId = path;
|
|
1186
|
+
} else {
|
|
1187
|
+
pathId = this.registerGPUPath(path);
|
|
1188
|
+
}
|
|
1189
|
+
const meshes = this._meshesOf([smid]);
|
|
1190
|
+
this._featureStyleManager.bindGPUPath(smid, pathId, options, meshes);
|
|
1191
|
+
for (const mesh of meshes) mesh.frustumCulled = false;
|
|
1192
|
+
this.gpuPathManager.updateUniforms(this.uniforms);
|
|
1193
|
+
return pathId;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
/**
|
|
1197
|
+
* 解绑单体的 GPU 路径动画
|
|
1198
|
+
*/
|
|
1199
|
+
unbindGPUPath(smid: number): void {
|
|
1200
|
+
const meshes = this._meshesOf([smid]);
|
|
1201
|
+
this._featureStyleManager.unbindGPUPath(smid, meshes);
|
|
1202
|
+
this.gpuPathManager.updateUniforms(this.uniforms);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
/**
|
|
1206
|
+
* 手动设置单体在 GPU 路径上的归一化进度(0.0 ~ 1.0)
|
|
1207
|
+
*/
|
|
1208
|
+
setGPUPathProgress(smid: number, progress: number): void {
|
|
1209
|
+
const meshes = this._meshesOf([smid]);
|
|
1210
|
+
this._featureStyleManager.setGPUPathProgress(smid, progress, meshes);
|
|
1211
|
+
for (const mesh of meshes) mesh.frustumCulled = false;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
/**
|
|
1215
|
+
* 设置全局 GPU 路径时间(秒)
|
|
1216
|
+
*/
|
|
1217
|
+
setGPUPathTime(time: number, autoAdvance = false): void {
|
|
1218
|
+
if (this.uniforms.uPathTime) {
|
|
1219
|
+
this.uniforms.uPathTime.value = time;
|
|
1220
|
+
}
|
|
1221
|
+
this._customPathTime = !autoAdvance;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
/**
|
|
1225
|
+
* 获取全局 GPU 路径时间(秒)
|
|
1226
|
+
*/
|
|
1227
|
+
getGPUPathTime(): number {
|
|
1228
|
+
return this.uniforms.uPathTime?.value ?? 0;
|
|
1229
|
+
}
|
|
1135
1230
|
/**
|
|
1136
1231
|
* 单体样式过渡:颜色 / 抬高 / 透明度 / 平移可以任意组合。
|
|
1137
1232
|
*
|
|
@@ -2452,6 +2547,10 @@ export class S3MViewer {
|
|
|
2452
2547
|
uBatchMix: this.uniforms.uBatchMix,
|
|
2453
2548
|
uBatchTexWidth: styled.userData.uBatchTexWidth,
|
|
2454
2549
|
uBatchTexActive: styled.userData.uBatchTexActive,
|
|
2550
|
+
uPathTex: this.uniforms.uPathTex,
|
|
2551
|
+
uPathCount: this.uniforms.uPathCount,
|
|
2552
|
+
uPathTexActive: this.uniforms.uPathTexActive,
|
|
2553
|
+
uPathTime: this.uniforms.uPathTime,
|
|
2455
2554
|
}
|
|
2456
2555
|
: undefined;
|
|
2457
2556
|
return createThickLineMaterial(this.lineShared, {}, this._edgeStyleUniforms, batch);
|
|
@@ -2467,6 +2566,10 @@ export class S3MViewer {
|
|
|
2467
2566
|
uBatchMix: this.uniforms.uBatchMix,
|
|
2468
2567
|
uBatchTexWidth: styled.userData.uBatchTexWidth,
|
|
2469
2568
|
uBatchTexActive: styled.userData.uBatchTexActive,
|
|
2569
|
+
uPathTex: this.uniforms.uPathTex,
|
|
2570
|
+
uPathCount: this.uniforms.uPathCount,
|
|
2571
|
+
uPathTexActive: this.uniforms.uPathTexActive,
|
|
2572
|
+
uPathTime: this.uniforms.uPathTime,
|
|
2470
2573
|
}
|
|
2471
2574
|
: undefined;
|
|
2472
2575
|
return createThickLineMaterial(this.lineShared, {}, this._selEdgeStyleUniforms, batch);
|
|
@@ -3284,6 +3387,12 @@ export class S3MViewer {
|
|
|
3284
3387
|
this.uniforms.uTime.value += 0.016 * (this.style.speed ?? 1);
|
|
3285
3388
|
}
|
|
3286
3389
|
|
|
3390
|
+
this.gpuPathManager.updateUniforms(this.uniforms);
|
|
3391
|
+
const gpuPathActive = (this.uniforms.uPathTexActive?.value ?? 0) > 0;
|
|
3392
|
+
if (gpuPathActive && !this._customPathTime && this.uniforms.uPathTime) {
|
|
3393
|
+
this.uniforms.uPathTime.value += 0.016;
|
|
3394
|
+
}
|
|
3395
|
+
|
|
3287
3396
|
this._updateLights();
|
|
3288
3397
|
|
|
3289
3398
|
// 等宽棱线按设备像素撑开,分辨率每帧同步一次
|
|
@@ -3306,6 +3415,7 @@ export class S3MViewer {
|
|
|
3306
3415
|
return (
|
|
3307
3416
|
animating ||
|
|
3308
3417
|
this.style.mode === STYLE.DYNAMIC ||
|
|
3418
|
+
gpuPathActive ||
|
|
3309
3419
|
this.stats.loading > 0 ||
|
|
3310
3420
|
tilesetChanged
|
|
3311
3421
|
);
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import * as THREE from 'three';
|
|
12
12
|
import type { StyleUniforms } from '../core/types.js';
|
|
13
13
|
import { DEFAULT_FLOOR_HEIGHT } from '../core/FeatureInfo.js';
|
|
14
|
+
import { GPUPathManager } from './GPUPathManager.js';
|
|
14
15
|
|
|
15
16
|
/** 着色模式,数值与着色器里的 uMode 一一对应 */
|
|
16
17
|
export const STYLE = {
|
|
@@ -188,8 +189,9 @@ const DUMMY_DATA = new Float32Array([
|
|
|
188
189
|
1, 0, 0, 0, // Row 2: rot row 0, pivot.x
|
|
189
190
|
0, 1, 0, 0, // Row 3: rot row 1, pivot.y
|
|
190
191
|
0, 0, 1, 0, // Row 4: rot row 2, pivot.z
|
|
192
|
+
-1, 0, 0, 0, // Row 5: pathId (-1=none), speed, phase, modeFlag
|
|
191
193
|
]);
|
|
192
|
-
export const DUMMY_BATCH_TEX = new THREE.DataTexture(DUMMY_DATA, 1,
|
|
194
|
+
export const DUMMY_BATCH_TEX = new THREE.DataTexture(DUMMY_DATA, 1, 6, THREE.RGBAFormat, THREE.FloatType);
|
|
193
195
|
DUMMY_BATCH_TEX.minFilter = THREE.NearestFilter;
|
|
194
196
|
DUMMY_BATCH_TEX.magFilter = THREE.NearestFilter;
|
|
195
197
|
DUMMY_BATCH_TEX.needsUpdate = true;
|
|
@@ -235,6 +237,14 @@ export function createSharedUniforms(): StyleUniforms {
|
|
|
235
237
|
uAmbientGain: { value: 0 },
|
|
236
238
|
/** 双纹理交叉淡化插值系数(0 = 纯 uBatchTex, 1 = 纯 uBatchTex2) */
|
|
237
239
|
uBatchMix: { value: 0 },
|
|
240
|
+
/** GPU 路径数据采样纹理 */
|
|
241
|
+
uPathTex: { value: GPUPathManager.DUMMY_PATH_TEX },
|
|
242
|
+
/** GPU 路径纹理中包含的路径条数(高度) */
|
|
243
|
+
uPathCount: { value: 1 },
|
|
244
|
+
/** 是否激活 GPU 路径动画(0 = 未激活,1 = 激活) */
|
|
245
|
+
uPathTexActive: { value: 0 },
|
|
246
|
+
/** GPU 路径时间戳(秒) */
|
|
247
|
+
uPathTime: { value: 0 },
|
|
238
248
|
/**
|
|
239
249
|
* 图层整体透明度:在片元里乘进 diffuseColor.a。
|
|
240
250
|
*
|
|
@@ -277,6 +287,10 @@ const VERT_PARS = /* glsl */ `
|
|
|
277
287
|
uniform float uBatchMix;
|
|
278
288
|
uniform float uBatchTexWidth;
|
|
279
289
|
uniform int uBatchTexActive;
|
|
290
|
+
uniform sampler2D uPathTex;
|
|
291
|
+
uniform float uPathCount;
|
|
292
|
+
uniform float uPathTime;
|
|
293
|
+
uniform int uPathTexActive;
|
|
280
294
|
varying float vBatchId;
|
|
281
295
|
varying vec3 vVertColor;
|
|
282
296
|
varying float vWorldZ;
|
|
@@ -341,22 +355,63 @@ const VERT_SETUP = /* glsl */ `
|
|
|
341
355
|
float s3mBatchSetup = s3mBatchRaw();
|
|
342
356
|
if ( uBatchTexActive > 0 && s3mBatchSetup >= 0.0 ) {
|
|
343
357
|
float s3mU = ( s3mBatchSetup + 0.5 ) / uBatchTexWidth;
|
|
344
|
-
vec4 bState = s3mSampleBatchRow0( uBatchTex, uBatchTex2, vec2( s3mU, 0.
|
|
358
|
+
vec4 bState = s3mSampleBatchRow0( uBatchTex, uBatchTex2, vec2( s3mU, 0.08333333 ), uBatchMix );
|
|
345
359
|
if ( bState.a >= -0.5 ) {
|
|
346
|
-
vec4 bOffset = s3mSampleBatchRow1( uBatchTex, uBatchTex2, vec2( s3mU, 0.
|
|
360
|
+
vec4 bOffset = s3mSampleBatchRow1( uBatchTex, uBatchTex2, vec2( s3mU, 0.25 ), uBatchMix );
|
|
347
361
|
s3mCustomOffset = bOffset.xyz;
|
|
348
362
|
if ( s3mCustomOffset.z == 0.0 && bState.a > 0.0 ) {
|
|
349
363
|
s3mCustomOffset.z = bState.a;
|
|
350
364
|
}
|
|
351
|
-
vec4 r2 = s3mSampleBatch( uBatchTex, uBatchTex2, vec2( s3mU, 0.
|
|
352
|
-
vec4 r3 = s3mSampleBatch( uBatchTex, uBatchTex2, vec2( s3mU, 0.
|
|
353
|
-
vec4 r4 = s3mSampleBatch( uBatchTex, uBatchTex2, vec2( s3mU, 0.
|
|
365
|
+
vec4 r2 = s3mSampleBatch( uBatchTex, uBatchTex2, vec2( s3mU, 0.41666667 ), uBatchMix );
|
|
366
|
+
vec4 r3 = s3mSampleBatch( uBatchTex, uBatchTex2, vec2( s3mU, 0.58333333 ), uBatchMix );
|
|
367
|
+
vec4 r4 = s3mSampleBatch( uBatchTex, uBatchTex2, vec2( s3mU, 0.75 ), uBatchMix );
|
|
354
368
|
s3mRot = mat3(
|
|
355
369
|
vec3( r2.x, r3.x, r4.x ),
|
|
356
370
|
vec3( r2.y, r3.y, r4.y ),
|
|
357
371
|
vec3( r2.z, r3.z, r4.z )
|
|
358
372
|
);
|
|
359
373
|
s3mPivot = vec3( r2.w, r3.w, r4.w );
|
|
374
|
+
|
|
375
|
+
// Row 5: GPU 路径动画计算
|
|
376
|
+
if ( uPathTexActive > 0 ) {
|
|
377
|
+
vec4 r5 = s3mSampleBatch( uBatchTex, uBatchTex2, vec2( s3mU, 0.91666667 ), uBatchMix );
|
|
378
|
+
float pathId = r5.x;
|
|
379
|
+
float speed = r5.y;
|
|
380
|
+
float phase = r5.z;
|
|
381
|
+
float mode = r5.w;
|
|
382
|
+
if ( pathId >= 0.0 && mode > 0.5 && uPathCount > 0.0 ) {
|
|
383
|
+
float t = phase + uPathTime * speed;
|
|
384
|
+
bool autoHeading = true;
|
|
385
|
+
if ( mode >= 4.0 ) {
|
|
386
|
+
autoHeading = false;
|
|
387
|
+
mode -= 3.0;
|
|
388
|
+
}
|
|
389
|
+
if ( mode < 1.5 ) {
|
|
390
|
+
t = fract( t );
|
|
391
|
+
} else if ( mode < 2.5 ) {
|
|
392
|
+
t = clamp( t, 0.0, 1.0 );
|
|
393
|
+
} else {
|
|
394
|
+
t = mod( t, 2.0 );
|
|
395
|
+
t = ( t > 1.0 ) ? ( 2.0 - t ) : t;
|
|
396
|
+
}
|
|
397
|
+
float pathU = ( t * 255.0 + 0.5 ) / 256.0;
|
|
398
|
+
float pathV = ( pathId + 0.5 ) / uPathCount;
|
|
399
|
+
vec4 pathSample = texture2D( uPathTex, vec2( pathU, pathV ) );
|
|
400
|
+
vec3 pathPos = pathSample.xyz;
|
|
401
|
+
float pathYaw = pathSample.w;
|
|
402
|
+
if ( autoHeading ) {
|
|
403
|
+
float c = cos( pathYaw );
|
|
404
|
+
float s = sin( pathYaw );
|
|
405
|
+
mat3 rotHeading = mat3(
|
|
406
|
+
c, -s, 0.0,
|
|
407
|
+
s, c, 0.0,
|
|
408
|
+
0.0, 0.0, 1.0
|
|
409
|
+
);
|
|
410
|
+
s3mRot = rotHeading * s3mRot;
|
|
411
|
+
}
|
|
412
|
+
s3mCustomOffset += pathPos;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
360
415
|
}
|
|
361
416
|
}
|
|
362
417
|
objectNormal = normalize( s3mRot * objectNormal );
|
|
@@ -366,7 +421,7 @@ const VERT_ELEVATION = /* glsl */ `
|
|
|
366
421
|
float s3mBatchElev = s3mBatchRaw();
|
|
367
422
|
if ( uBatchTexActive > 0 && s3mBatchElev >= 0.0 ) {
|
|
368
423
|
float s3mU = ( s3mBatchElev + 0.5 ) / uBatchTexWidth;
|
|
369
|
-
vec4 bState = s3mSampleBatchRow0( uBatchTex, uBatchTex2, vec2( s3mU, 0.
|
|
424
|
+
vec4 bState = s3mSampleBatchRow0( uBatchTex, uBatchTex2, vec2( s3mU, 0.08333333 ), uBatchMix );
|
|
370
425
|
if ( bState.a < -0.5 ) {
|
|
371
426
|
transformed = vec3( 0.0 );
|
|
372
427
|
}
|
|
@@ -637,7 +692,7 @@ const FRAG_COLOR = /* glsl */ `
|
|
|
637
692
|
if ( uBatchTexActive > 0 && s3mBid >= 0.0 ) {
|
|
638
693
|
// 用取整后的 id 采样:否则逐像素的插值误差会让纹素在边界上来回跳,同样出彩色噪点
|
|
639
694
|
float s3mU = ( s3mBid + 0.5 ) / uBatchTexWidth;
|
|
640
|
-
vec4 bState = s3mSampleBatchRow0( uBatchTex, uBatchTex2, vec2( s3mU, 0.
|
|
695
|
+
vec4 bState = s3mSampleBatchRow0( uBatchTex, uBatchTex2, vec2( s3mU, 0.08333333 ), uBatchMix );
|
|
641
696
|
if ( bState.a < -0.5 ) {
|
|
642
697
|
discard; // 属于被过滤隐藏的单体
|
|
643
698
|
}
|
|
@@ -648,7 +703,7 @@ const FRAG_COLOR = /* glsl */ `
|
|
|
648
703
|
}
|
|
649
704
|
// 单体独立透明度(状态纹理第 2 行 alpha,0 = 无覆盖)。
|
|
650
705
|
// 与全局 uOpacity(diffuseColor.a 初值)和图集贴图自带 alpha 逐级叠乘
|
|
651
|
-
vec4 bOffset = s3mSampleBatchRow1( uBatchTex, uBatchTex2, vec2( s3mU, 0.
|
|
706
|
+
vec4 bOffset = s3mSampleBatchRow1( uBatchTex, uBatchTex2, vec2( s3mU, 0.25 ), uBatchMix );
|
|
652
707
|
if ( bOffset.a > 0.0 ) {
|
|
653
708
|
diffuseColor.a *= bOffset.a;
|
|
654
709
|
}
|
|
@@ -836,6 +891,10 @@ const OUTLINE_VERT = /* glsl */ `
|
|
|
836
891
|
uniform float uBatchMix;
|
|
837
892
|
uniform float uBatchTexWidth;
|
|
838
893
|
uniform int uBatchTexActive;
|
|
894
|
+
uniform sampler2D uPathTex;
|
|
895
|
+
uniform float uPathCount;
|
|
896
|
+
uniform float uPathTime;
|
|
897
|
+
uniform int uPathTexActive;
|
|
839
898
|
varying float vSel;
|
|
840
899
|
|
|
841
900
|
vec4 s3mSampleBatch( sampler2D texA, sampler2D texB, vec2 uv, float mixVal ) {
|
|
@@ -852,24 +911,64 @@ const OUTLINE_VERT = /* glsl */ `
|
|
|
852
911
|
vec3 s3mPivot = vec3( 0.0 );
|
|
853
912
|
if ( uBatchTexActive > 0 && aBatchId >= 0.0 ) {
|
|
854
913
|
float s3mU = ( aBatchId + 0.5 ) / uBatchTexWidth;
|
|
855
|
-
vec4 bState = s3mSampleBatch( uBatchTex, uBatchTex2, vec2( s3mU, 0.
|
|
914
|
+
vec4 bState = s3mSampleBatch( uBatchTex, uBatchTex2, vec2( s3mU, 0.08333333 ), uBatchMix );
|
|
856
915
|
if ( bState.a < -0.5 ) {
|
|
857
916
|
vSel = 0.0; // 过滤隐藏的单体不显示描边
|
|
858
917
|
} else {
|
|
859
|
-
vec4 bOffset = s3mSampleBatch( uBatchTex, uBatchTex2, vec2( s3mU, 0.
|
|
918
|
+
vec4 bOffset = s3mSampleBatch( uBatchTex, uBatchTex2, vec2( s3mU, 0.25 ), uBatchMix );
|
|
860
919
|
s3mCustomOffset = bOffset.xyz;
|
|
861
920
|
if ( s3mCustomOffset.z == 0.0 && bState.a > 0.0 ) {
|
|
862
921
|
s3mCustomOffset.z = bState.a;
|
|
863
922
|
}
|
|
864
|
-
vec4 r2 = s3mSampleBatch( uBatchTex, uBatchTex2, vec2( s3mU, 0.
|
|
865
|
-
vec4 r3 = s3mSampleBatch( uBatchTex, uBatchTex2, vec2( s3mU, 0.
|
|
866
|
-
vec4 r4 = s3mSampleBatch( uBatchTex, uBatchTex2, vec2( s3mU, 0.
|
|
923
|
+
vec4 r2 = s3mSampleBatch( uBatchTex, uBatchTex2, vec2( s3mU, 0.41666667 ), uBatchMix );
|
|
924
|
+
vec4 r3 = s3mSampleBatch( uBatchTex, uBatchTex2, vec2( s3mU, 0.58333333 ), uBatchMix );
|
|
925
|
+
vec4 r4 = s3mSampleBatch( uBatchTex, uBatchTex2, vec2( s3mU, 0.75 ), uBatchMix );
|
|
867
926
|
s3mRot = mat3(
|
|
868
927
|
vec3( r2.x, r3.x, r4.x ),
|
|
869
928
|
vec3( r2.y, r3.y, r4.y ),
|
|
870
929
|
vec3( r2.z, r3.z, r4.z )
|
|
871
930
|
);
|
|
872
931
|
s3mPivot = vec3( r2.w, r3.w, r4.w );
|
|
932
|
+
|
|
933
|
+
if ( uPathTexActive > 0 ) {
|
|
934
|
+
vec4 r5 = s3mSampleBatch( uBatchTex, uBatchTex2, vec2( s3mU, 0.91666667 ), uBatchMix );
|
|
935
|
+
float pathId = r5.x;
|
|
936
|
+
float speed = r5.y;
|
|
937
|
+
float phase = r5.z;
|
|
938
|
+
float mode = r5.w;
|
|
939
|
+
if ( pathId >= 0.0 && mode > 0.5 && uPathCount > 0.0 ) {
|
|
940
|
+
float t = phase + uPathTime * speed;
|
|
941
|
+
bool autoHeading = true;
|
|
942
|
+
if ( mode >= 4.0 ) {
|
|
943
|
+
autoHeading = false;
|
|
944
|
+
mode -= 3.0;
|
|
945
|
+
}
|
|
946
|
+
if ( mode < 1.5 ) {
|
|
947
|
+
t = fract( t );
|
|
948
|
+
} else if ( mode < 2.5 ) {
|
|
949
|
+
t = clamp( t, 0.0, 1.0 );
|
|
950
|
+
} else {
|
|
951
|
+
t = mod( t, 2.0 );
|
|
952
|
+
t = ( t > 1.0 ) ? ( 2.0 - t ) : t;
|
|
953
|
+
}
|
|
954
|
+
float pathU = ( t * 255.0 + 0.5 ) / 256.0;
|
|
955
|
+
float pathV = ( pathId + 0.5 ) / uPathCount;
|
|
956
|
+
vec4 pathSample = texture2D( uPathTex, vec2( pathU, pathV ) );
|
|
957
|
+
vec3 pathPos = pathSample.xyz;
|
|
958
|
+
float pathYaw = pathSample.w;
|
|
959
|
+
if ( autoHeading ) {
|
|
960
|
+
float c = cos( pathYaw );
|
|
961
|
+
float s = sin( pathYaw );
|
|
962
|
+
mat3 rotHeading = mat3(
|
|
963
|
+
c, -s, 0.0,
|
|
964
|
+
s, c, 0.0,
|
|
965
|
+
0.0, 0.0, 1.0
|
|
966
|
+
);
|
|
967
|
+
s3mRot = rotHeading * s3mRot;
|
|
968
|
+
}
|
|
969
|
+
s3mCustomOffset += pathPos;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
873
972
|
}
|
|
874
973
|
}
|
|
875
974
|
vec3 p = position + ( s3mRot * normal ) * uWidth * vSel;
|
|
@@ -899,6 +998,10 @@ export interface OutlineMaterial extends THREE.ShaderMaterial {
|
|
|
899
998
|
uBatchMix: { value: number };
|
|
900
999
|
uBatchTexWidth: { value: number };
|
|
901
1000
|
uBatchTexActive: { value: number };
|
|
1001
|
+
uPathTex: { value: unknown };
|
|
1002
|
+
uPathCount: { value: number };
|
|
1003
|
+
uPathTexActive: { value: number };
|
|
1004
|
+
uPathTime: { value: number };
|
|
902
1005
|
uColor: { value: THREE.Color };
|
|
903
1006
|
};
|
|
904
1007
|
}
|
|
@@ -916,6 +1019,10 @@ export function createOutlineMaterial(shared: StyleUniforms, initialWidth = 0.9)
|
|
|
916
1019
|
uBatchMix: shared.uBatchMix ?? { value: 0 },
|
|
917
1020
|
uBatchTexWidth: { value: 1.0 },
|
|
918
1021
|
uBatchTexActive: { value: 0 },
|
|
1022
|
+
uPathTex: shared.uPathTex ?? { value: GPUPathManager.DUMMY_PATH_TEX },
|
|
1023
|
+
uPathCount: shared.uPathCount ?? { value: 1 },
|
|
1024
|
+
uPathTexActive: shared.uPathTexActive ?? { value: 0 },
|
|
1025
|
+
uPathTime: shared.uPathTime ?? { value: 0 },
|
|
919
1026
|
uColor: { value: shared.uSelColor.value.clone() },
|
|
920
1027
|
},
|
|
921
1028
|
vertexShader: OUTLINE_VERT,
|