s3m-three 0.3.7 → 0.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/anim/Animator.d.ts +111 -0
- package/dist/anim/Animator.d.ts.map +1 -0
- package/dist/anim/Animator.js +382 -0
- package/dist/anim/Animator.js.map +1 -0
- package/dist/anim/Path.d.ts +30 -0
- package/dist/anim/Path.d.ts.map +1 -0
- package/dist/anim/Path.js +100 -0
- package/dist/anim/Path.js.map +1 -0
- package/dist/anim/easing.d.ts +70 -0
- package/dist/anim/easing.d.ts.map +1 -0
- package/dist/anim/easing.js +170 -0
- package/dist/anim/easing.js.map +1 -0
- package/dist/anim/index.d.ts +6 -0
- package/dist/anim/index.d.ts.map +1 -0
- package/dist/anim/index.js +4 -0
- package/dist/anim/index.js.map +1 -0
- package/dist/browser/s3m-three.iife.ext-three.js +218 -39
- package/dist/browser/s3m-three.iife.js +218 -39
- package/dist/core/types.d.ts +20 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/render/EdgeLines.d.ts +68 -14
- package/dist/render/EdgeLines.d.ts.map +1 -1
- package/dist/render/EdgeLines.js +125 -8
- package/dist/render/EdgeLines.js.map +1 -1
- package/dist/render/FeatureStyleManager.d.ts +62 -1
- package/dist/render/FeatureStyleManager.d.ts.map +1 -1
- package/dist/render/FeatureStyleManager.js +331 -60
- package/dist/render/FeatureStyleManager.js.map +1 -1
- package/dist/render/MergeVertices.d.ts +7 -0
- package/dist/render/MergeVertices.d.ts.map +1 -1
- package/dist/render/MergeVertices.js +10 -1
- package/dist/render/MergeVertices.js.map +1 -1
- package/dist/render/OrthoProjection.d.ts +6 -0
- package/dist/render/OrthoProjection.d.ts.map +1 -1
- package/dist/render/OrthoProjection.js +48 -0
- package/dist/render/OrthoProjection.js.map +1 -1
- package/dist/render/S3MViewer.d.ts +235 -9
- package/dist/render/S3MViewer.d.ts.map +1 -1
- package/dist/render/S3MViewer.js +880 -68
- package/dist/render/S3MViewer.js.map +1 -1
- package/dist/render/StyleMaterial.d.ts +28 -0
- package/dist/render/StyleMaterial.d.ts.map +1 -1
- package/dist/render/StyleMaterial.js +190 -21
- package/dist/render/StyleMaterial.js.map +1 -1
- package/dist/tileset/S3MTileset.d.ts.map +1 -1
- package/dist/tileset/S3MTileset.js +3 -1
- package/dist/tileset/S3MTileset.js.map +1 -1
- package/docs/ANIMATION.md +534 -0
- package/docs/API.md +2 -0
- package/package.json +1 -1
- package/src/anim/Animator.ts +460 -0
- package/src/anim/Path.ts +107 -0
- package/src/anim/easing.ts +195 -0
- package/src/anim/index.ts +10 -0
- package/src/core/types.ts +10 -0
- package/src/index.ts +16 -0
- package/src/render/EdgeLines.ts +161 -15
- package/src/render/FeatureStyleManager.ts +414 -65
- package/src/render/MergeVertices.ts +11 -1
- package/src/render/OrthoProjection.ts +52 -0
- package/src/render/S3MViewer.ts +1012 -64
- package/src/render/StyleMaterial.ts +211 -22
- package/src/tileset/S3MTileset.ts +3 -1
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 缓动曲线库
|
|
3
|
+
*
|
|
4
|
+
* 纯函数,不依赖 three / DOM,可以单独在 Worker 或 Node 里跑。
|
|
5
|
+
*
|
|
6
|
+
* 三类:
|
|
7
|
+
* 1. 命名曲线 —— CSS 那套 easeIn/Out/InOut × Quad…Bounce,覆盖 90% 场景
|
|
8
|
+
* 2. cubicBezier(x1,y1,x2,y2) —— 与 CSS `cubic-bezier()` 数值对齐,用来做产品级调校
|
|
9
|
+
* 3. steps(n, position) —— 离散跳变,做逐帧动画或「咔哒」感
|
|
10
|
+
*
|
|
11
|
+
* spring 不在这里:它没有「时长」概念,靠物理收敛,所以实现为 Animator 里的一种
|
|
12
|
+
* Track 类型,而不是 (t) => number 的曲线。
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export type EasingFn = (t: number) => number;
|
|
16
|
+
|
|
17
|
+
/* ------------------------------------------------------------------ 基础 */
|
|
18
|
+
|
|
19
|
+
const c1 = 1.70158;
|
|
20
|
+
const c2 = c1 * 1.525;
|
|
21
|
+
const c3 = c1 + 1;
|
|
22
|
+
const c4 = (2 * Math.PI) / 3;
|
|
23
|
+
const c5 = (2 * Math.PI) / 4.5;
|
|
24
|
+
const n1 = 7.5625;
|
|
25
|
+
const d1 = 2.75;
|
|
26
|
+
|
|
27
|
+
function outBounce(t: number): number {
|
|
28
|
+
if (t < 1 / d1) return n1 * t * t;
|
|
29
|
+
if (t < 2 / d1) {
|
|
30
|
+
const x = t - 1.5 / d1;
|
|
31
|
+
return n1 * x * x + 0.75;
|
|
32
|
+
}
|
|
33
|
+
if (t < 2.5 / d1) {
|
|
34
|
+
const x = t - 2.25 / d1;
|
|
35
|
+
return n1 * x * x + 0.9375;
|
|
36
|
+
}
|
|
37
|
+
const x = t - 2.625 / d1;
|
|
38
|
+
return n1 * x * x + 0.984375;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function inBounce(t: number): number {
|
|
42
|
+
return 1 - outBounce(1 - t);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* ------------------------------------------------------------------ 曲线表 */
|
|
46
|
+
|
|
47
|
+
const CURVES = {
|
|
48
|
+
linear: (t) => t,
|
|
49
|
+
|
|
50
|
+
easeInQuad: (t) => t * t,
|
|
51
|
+
easeOutQuad: (t) => 1 - (1 - t) * (1 - t),
|
|
52
|
+
easeInOutQuad: (t) => (t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2),
|
|
53
|
+
|
|
54
|
+
easeInCubic: (t) => t * t * t,
|
|
55
|
+
easeOutCubic: (t) => 1 - Math.pow(1 - t, 3),
|
|
56
|
+
easeInOutCubic: (t) => (t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2),
|
|
57
|
+
|
|
58
|
+
easeInQuart: (t) => t * t * t * t,
|
|
59
|
+
easeOutQuart: (t) => 1 - Math.pow(1 - t, 4),
|
|
60
|
+
easeInOutQuart: (t) => (t < 0.5 ? 8 * t * t * t * t : 1 - Math.pow(-2 * t + 2, 4) / 2),
|
|
61
|
+
|
|
62
|
+
easeInQuint: (t) => t * t * t * t * t,
|
|
63
|
+
easeOutQuint: (t) => 1 - Math.pow(1 - t, 5),
|
|
64
|
+
easeInOutQuint: (t) => (t < 0.5 ? 16 * t * t * t * t * t : 1 - Math.pow(-2 * t + 2, 5) / 2),
|
|
65
|
+
|
|
66
|
+
easeInSine: (t) => 1 - Math.cos((t * Math.PI) / 2),
|
|
67
|
+
easeOutSine: (t) => Math.sin((t * Math.PI) / 2),
|
|
68
|
+
easeInOutSine: (t) => -(Math.cos(Math.PI * t) - 1) / 2,
|
|
69
|
+
|
|
70
|
+
easeInExpo: (t) => (t === 0 ? 0 : Math.pow(2, 10 * t - 10)),
|
|
71
|
+
easeOutExpo: (t) => (t === 1 ? 1 : 1 - Math.pow(2, -10 * t)),
|
|
72
|
+
easeInOutExpo: (t) =>
|
|
73
|
+
t === 0 ? 0 : t === 1 ? 1 : t < 0.5 ? Math.pow(2, 20 * t - 10) / 2 : (2 - Math.pow(2, -20 * t + 10)) / 2,
|
|
74
|
+
|
|
75
|
+
easeInCirc: (t) => 1 - Math.sqrt(1 - t * t),
|
|
76
|
+
easeOutCirc: (t) => Math.sqrt(1 - Math.pow(t - 1, 2)),
|
|
77
|
+
easeInOutCirc: (t) =>
|
|
78
|
+
t < 0.5
|
|
79
|
+
? (1 - Math.sqrt(1 - Math.pow(2 * t, 2))) / 2
|
|
80
|
+
: (Math.sqrt(1 - Math.pow(-2 * t + 2, 2)) + 1) / 2,
|
|
81
|
+
|
|
82
|
+
easeInBack: (t) => c3 * t * t * t - c1 * t * t,
|
|
83
|
+
easeOutBack: (t) => 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2),
|
|
84
|
+
easeInOutBack: (t) =>
|
|
85
|
+
t < 0.5
|
|
86
|
+
? (Math.pow(2 * t, 2) * ((c2 + 1) * 2 * t - c2)) / 2
|
|
87
|
+
: (Math.pow(2 * t - 2, 2) * ((c2 + 1) * (t * 2 - 2) + c2) + 2) / 2,
|
|
88
|
+
|
|
89
|
+
easeInElastic: (t) =>
|
|
90
|
+
t === 0 ? 0 : t === 1 ? 1 : -Math.pow(2, 10 * t - 10) * Math.sin((t * 10 - 10.75) * c4),
|
|
91
|
+
easeOutElastic: (t) =>
|
|
92
|
+
t === 0 ? 0 : t === 1 ? 1 : Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1,
|
|
93
|
+
easeInOutElastic: (t) =>
|
|
94
|
+
t === 0
|
|
95
|
+
? 0
|
|
96
|
+
: t === 1
|
|
97
|
+
? 1
|
|
98
|
+
: t < 0.5
|
|
99
|
+
? -(Math.pow(2, 20 * t - 10) * Math.sin((20 * t - 11.125) * c5)) / 2
|
|
100
|
+
: (Math.pow(2, -20 * t + 10) * Math.sin((20 * t - 11.125) * c5)) / 2 + 1,
|
|
101
|
+
|
|
102
|
+
easeInBounce: inBounce,
|
|
103
|
+
easeOutBounce: outBounce,
|
|
104
|
+
easeInOutBounce: (t) => (t < 0.5 ? (1 - inBounce(1 - 2 * t)) / 2 : (1 + inBounce(2 * t - 1)) / 2),
|
|
105
|
+
} satisfies Record<string, EasingFn>;
|
|
106
|
+
|
|
107
|
+
/** 全部内置曲线名(顺序稳定,方便做下拉框 / 遍历自检) */
|
|
108
|
+
export const EASING_NAMES = Object.keys(CURVES) as EasingName[];
|
|
109
|
+
|
|
110
|
+
export type EasingName = keyof typeof CURVES;
|
|
111
|
+
|
|
112
|
+
/** 按名字取曲线;名字不存在直接抛错,不做静默降级(拼错会变成「没动画」,很难查) */
|
|
113
|
+
export function easing(name: EasingName | EasingFn): EasingFn {
|
|
114
|
+
if (typeof name === 'function') return name;
|
|
115
|
+
const fn = (CURVES as Record<string, EasingFn>)[name];
|
|
116
|
+
if (!fn) throw new Error(`[s3m] 未知的缓动曲线: ${String(name)}`);
|
|
117
|
+
return fn;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* ------------------------------------------------------------------ 贝塞尔 */
|
|
121
|
+
|
|
122
|
+
const NEWTON_ITERATIONS = 8;
|
|
123
|
+
const NEWTON_MIN_SLOPE = 0.001;
|
|
124
|
+
const SUBDIVISION_PRECISION = 1e-7;
|
|
125
|
+
const SUBDIVISION_MAX_ITERATIONS = 12;
|
|
126
|
+
|
|
127
|
+
const bezA = (a1: number, a2: number): number => 1 - 3 * a2 + 3 * a1;
|
|
128
|
+
const bezB = (a1: number, a2: number): number => 3 * a2 - 6 * a1;
|
|
129
|
+
const bezC = (a1: number): number => 3 * a1;
|
|
130
|
+
|
|
131
|
+
/** 三次贝塞尔在参数 t 处的值(x 或 y 用同一组公式,只是控制点不同) */
|
|
132
|
+
function bezierAt(t: number, a1: number, a2: number): number {
|
|
133
|
+
return ((bezA(a1, a2) * t + bezB(a1, a2)) * t + bezC(a1)) * t;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function bezierSlope(t: number, a1: number, a2: number): number {
|
|
137
|
+
return 3 * bezA(a1, a2) * t * t + 2 * bezB(a1, a2) * t + bezC(a1);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* CSS `cubic-bezier(x1, y1, x2, y2)` 的等价实现。
|
|
142
|
+
*
|
|
143
|
+
* 与浏览器一致:先解 `x(t) = 输入进度` 得到参数 t,再取 `y(t)`。
|
|
144
|
+
* 用牛顿迭代,斜率过小时退化为二分——退化控制点(比如 (0,0,1,1))也稳定。
|
|
145
|
+
*/
|
|
146
|
+
export function cubicBezier(x1: number, y1: number, x2: number, y2: number): EasingFn {
|
|
147
|
+
// 线性特例:控制点落在对角线上时不必迭代
|
|
148
|
+
if (x1 === y1 && x2 === y2) return (t) => t;
|
|
149
|
+
|
|
150
|
+
const solveT = (x: number): number => {
|
|
151
|
+
let t = x;
|
|
152
|
+
for (let i = 0; i < NEWTON_ITERATIONS; i++) {
|
|
153
|
+
const slope = bezierSlope(t, x1, x2);
|
|
154
|
+
if (Math.abs(slope) < NEWTON_MIN_SLOPE) break;
|
|
155
|
+
const err = bezierAt(t, x1, x2) - x;
|
|
156
|
+
if (Math.abs(err) < SUBDIVISION_PRECISION) return t;
|
|
157
|
+
t -= err / slope;
|
|
158
|
+
}
|
|
159
|
+
// 牛顿不收敛:二分兜底
|
|
160
|
+
let lo = 0;
|
|
161
|
+
let hi = 1;
|
|
162
|
+
t = x;
|
|
163
|
+
for (let i = 0; i < SUBDIVISION_MAX_ITERATIONS; i++) {
|
|
164
|
+
const v = bezierAt(t, x1, x2);
|
|
165
|
+
if (Math.abs(v - x) < SUBDIVISION_PRECISION) return t;
|
|
166
|
+
if (v < x) lo = t;
|
|
167
|
+
else hi = t;
|
|
168
|
+
t = (lo + hi) / 2;
|
|
169
|
+
}
|
|
170
|
+
return t;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
return (t: number): number => {
|
|
174
|
+
if (t <= 0) return 0;
|
|
175
|
+
if (t >= 1) return 1;
|
|
176
|
+
return bezierAt(solveT(t), y1, y2);
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/* ------------------------------------------------------------------ 离散 */
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* 阶梯曲线。
|
|
184
|
+
*
|
|
185
|
+
* @param count 档数
|
|
186
|
+
* @param position 'end' 表示「走完一档才跳」,'start' 表示「一进档就跳」
|
|
187
|
+
*/
|
|
188
|
+
export function steps(count: number, position: 'start' | 'end' = 'end'): EasingFn {
|
|
189
|
+
const n = Math.max(1, Math.floor(count));
|
|
190
|
+
return (t: number): number => {
|
|
191
|
+
const clamped = Math.min(1, Math.max(0, t));
|
|
192
|
+
if (position === 'start') return Math.min(1, (Math.floor(clamped * n) + 1) / n);
|
|
193
|
+
return Math.min(1, Math.floor(clamped * n) / n);
|
|
194
|
+
};
|
|
195
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { Animator } from './Animator.js';
|
|
2
|
+
export type { Channel, SpringSpec, TransitionOptions } from './Animator.js';
|
|
3
|
+
export { Path } from './Path.js';
|
|
4
|
+
export {
|
|
5
|
+
easing,
|
|
6
|
+
cubicBezier,
|
|
7
|
+
steps,
|
|
8
|
+
EASING_NAMES,
|
|
9
|
+
} from './easing.js';
|
|
10
|
+
export type { EasingFn, EasingName } from './easing.js';
|
package/src/core/types.ts
CHANGED
|
@@ -281,5 +281,15 @@ export interface StyleUniforms {
|
|
|
281
281
|
uFilterHeightActive?: { value: number };
|
|
282
282
|
uRoofThreshold?: { value: number };
|
|
283
283
|
uTexMix?: { value: number };
|
|
284
|
+
/** 基础漫反射底色增益 / 直通自发光微调(0~1,默认 0.0) */
|
|
285
|
+
uAmbientGain?: { value: number };
|
|
286
|
+
/** 双纹理交叉淡化插值因子(0~1,0 = 状态 A,1 = 状态 B) */
|
|
287
|
+
uBatchMix?: { value: number };
|
|
288
|
+
/** 图层整体透明度(共享 uniform,透明度动画靠它做到 O(1)) */
|
|
289
|
+
uOpacity?: { value: number };
|
|
290
|
+
/** 棱线淡入淡出乘子(setEdges 过渡期间 0→1) */
|
|
291
|
+
uEdgeFade?: { value: number };
|
|
292
|
+
/** 整体压暗乘子(mode 切换的 fade-through) */
|
|
293
|
+
uFade?: { value: number };
|
|
284
294
|
[key: string]: { value: unknown } | undefined;
|
|
285
295
|
}
|
package/src/index.ts
CHANGED
|
@@ -78,6 +78,11 @@ export {
|
|
|
78
78
|
DatasetManager,
|
|
79
79
|
FeatureStyleManager,
|
|
80
80
|
} from './render/S3MViewer.js';
|
|
81
|
+
export {
|
|
82
|
+
resolveRotationMatrix,
|
|
83
|
+
extractEulerAngles,
|
|
84
|
+
} from './render/FeatureStyleManager.js';
|
|
85
|
+
export type { RotationMatrix9 } from './render/FeatureStyleManager.js';
|
|
81
86
|
export type {
|
|
82
87
|
DatasetAlignment,
|
|
83
88
|
S3MDatasetOptions,
|
|
@@ -96,6 +101,7 @@ export type {
|
|
|
96
101
|
GroundMode,
|
|
97
102
|
GroundOptions,
|
|
98
103
|
LightOptions,
|
|
104
|
+
PathFollowOptions,
|
|
99
105
|
} from './render/S3MViewer.js';
|
|
100
106
|
|
|
101
107
|
export { buildTileObjects } from './render/GeometryBuilder.js';
|
|
@@ -117,8 +123,18 @@ export type { StyleMode, SelectMode, StyledMaterial, StyledMaterialOptions, Outl
|
|
|
117
123
|
export { Picker } from './render/Picker.js';
|
|
118
124
|
export type { PickHit } from './render/Picker.js';
|
|
119
125
|
|
|
126
|
+
export { Animator, Path, easing, cubicBezier, steps, EASING_NAMES } from './anim/index.js';
|
|
127
|
+
export type {
|
|
128
|
+
Channel,
|
|
129
|
+
SpringSpec,
|
|
130
|
+
TransitionOptions,
|
|
131
|
+
EasingFn,
|
|
132
|
+
EasingName,
|
|
133
|
+
} from './anim/index.js';
|
|
134
|
+
|
|
120
135
|
export {
|
|
121
136
|
buildOrthoMVP,
|
|
137
|
+
buildBlendedMVP,
|
|
122
138
|
extractProjectionMatrix,
|
|
123
139
|
extractNearFar,
|
|
124
140
|
extractFovRadians,
|
package/src/render/EdgeLines.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* B 端算出来的法线正好和 A 端相反,反号一次才能拼成同一个四边形。
|
|
14
14
|
*/
|
|
15
15
|
import * as THREE from 'three';
|
|
16
|
+
import { DUMMY_BATCH_TEX } from './StyleMaterial.js';
|
|
16
17
|
|
|
17
18
|
/** 所有等宽线材质共享的分辨率 uniform(每帧更新一次即可) */
|
|
18
19
|
export interface LineSharedUniforms {
|
|
@@ -20,14 +21,36 @@ export interface LineSharedUniforms {
|
|
|
20
21
|
uDpr: { value: number };
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
/**
|
|
25
|
+
* 等宽线的「外观」uniform(颜色 / 粗细 / 透明度 / 拔高)。
|
|
26
|
+
*
|
|
27
|
+
* 棱线材质是**逐网格一份**的(见 createThickLineMaterial 的 batchUniforms 参数),
|
|
28
|
+
* 但外观必须全局统一——所以这些 `{ value }` 对象由调用方持有并共享给每一份材质,
|
|
29
|
+
* 改一次全场景生效,棱线淡入淡出才能保持 O(1)。
|
|
30
|
+
*/
|
|
31
|
+
export interface ThickLineStyleUniforms {
|
|
32
|
+
uColor: { value: THREE.Color };
|
|
33
|
+
uWidth: { value: number };
|
|
34
|
+
uOpacity: { value: number };
|
|
35
|
+
uElevation?: { value: number };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 等宽线的「单体状态」uniform:直接复用主材质 userData 里那几个对象,自动跟随 */
|
|
39
|
+
export interface ThickLineBatchUniforms {
|
|
40
|
+
uBatchTex: { value: THREE.Texture };
|
|
41
|
+
uBatchTex2?: { value: THREE.Texture };
|
|
42
|
+
uBatchMix?: { value: number };
|
|
43
|
+
uBatchTexWidth: { value: number };
|
|
44
|
+
uBatchTexActive: { value: number };
|
|
45
|
+
}
|
|
46
|
+
|
|
23
47
|
export interface ThickLineMaterial extends THREE.ShaderMaterial {
|
|
24
|
-
uniforms: LineSharedUniforms &
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
};
|
|
48
|
+
uniforms: LineSharedUniforms &
|
|
49
|
+
ThickLineStyleUniforms &
|
|
50
|
+
Required<ThickLineBatchUniforms> & {
|
|
51
|
+
uTranslation: { value: THREE.Vector3 };
|
|
52
|
+
[uniform: string]: THREE.IUniform;
|
|
53
|
+
};
|
|
31
54
|
}
|
|
32
55
|
|
|
33
56
|
export function createLineSharedUniforms(): LineSharedUniforms {
|
|
@@ -40,18 +63,66 @@ export function createLineSharedUniforms(): LineSharedUniforms {
|
|
|
40
63
|
const VERT = /* glsl */ `
|
|
41
64
|
attribute vec3 aOther;
|
|
42
65
|
attribute float aSide;
|
|
66
|
+
attribute float aBatchId;
|
|
43
67
|
|
|
44
68
|
uniform vec2 uResolution;
|
|
45
69
|
uniform float uDpr;
|
|
46
70
|
uniform float uWidth;
|
|
47
71
|
uniform float uElevation;
|
|
48
72
|
uniform vec3 uTranslation;
|
|
73
|
+
uniform sampler2D uBatchTex;
|
|
74
|
+
uniform sampler2D uBatchTex2;
|
|
75
|
+
uniform float uBatchMix;
|
|
76
|
+
uniform float uBatchTexWidth;
|
|
77
|
+
uniform int uBatchTexActive;
|
|
78
|
+
|
|
79
|
+
vec4 s3mSampleBatch( sampler2D texA, sampler2D texB, vec2 uv, float mixVal ) {
|
|
80
|
+
vec4 vA = texture2D( texA, uv );
|
|
81
|
+
if ( mixVal <= 0.0001 ) return vA;
|
|
82
|
+
vec4 vB = texture2D( texB, uv );
|
|
83
|
+
return mix( vA, vB, mixVal );
|
|
84
|
+
}
|
|
49
85
|
|
|
50
86
|
void main() {
|
|
87
|
+
// 单体平移 / 旋转 / 拔高 / 隐藏:与主着色器同一份 5 行状态纹理、同一套变换口径。
|
|
88
|
+
// 不采样的话,单体一动或一转棱线就留在原地——这正是「棱线跟随」要解决的。
|
|
89
|
+
vec3 s3mA = position;
|
|
90
|
+
vec3 s3mB = aOther;
|
|
91
|
+
vec3 s3mCustomOffset = vec3( 0.0 );
|
|
92
|
+
mat3 s3mRot = mat3( 1.0 );
|
|
93
|
+
vec3 s3mPivot = vec3( 0.0 );
|
|
94
|
+
|
|
95
|
+
if ( uBatchTexActive > 0 && aBatchId >= 0.0 ) {
|
|
96
|
+
float s3mU = ( aBatchId + 0.5 ) / uBatchTexWidth;
|
|
97
|
+
vec4 bState = s3mSampleBatch( uBatchTex, uBatchTex2, vec2( s3mU, 0.1 ), uBatchMix );
|
|
98
|
+
if ( bState.a < -0.5 ) {
|
|
99
|
+
// 该单体被隐藏:顶点着色器不能 discard,扔到裁剪空间外
|
|
100
|
+
gl_Position = vec4( 2.0, 2.0, 2.0, 1.0 );
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
vec4 bOffset = s3mSampleBatch( uBatchTex, uBatchTex2, vec2( s3mU, 0.3 ), uBatchMix );
|
|
104
|
+
s3mCustomOffset = bOffset.xyz;
|
|
105
|
+
if ( s3mCustomOffset.z == 0.0 && bState.a > 0.0 ) {
|
|
106
|
+
s3mCustomOffset.z = bState.a;
|
|
107
|
+
}
|
|
108
|
+
vec4 r2 = s3mSampleBatch( uBatchTex, uBatchTex2, vec2( s3mU, 0.5 ), uBatchMix );
|
|
109
|
+
vec4 r3 = s3mSampleBatch( uBatchTex, uBatchTex2, vec2( s3mU, 0.7 ), uBatchMix );
|
|
110
|
+
vec4 r4 = s3mSampleBatch( uBatchTex, uBatchTex2, vec2( s3mU, 0.9 ), uBatchMix );
|
|
111
|
+
s3mRot = mat3(
|
|
112
|
+
vec3( r2.x, r3.x, r4.x ),
|
|
113
|
+
vec3( r2.y, r3.y, r4.y ),
|
|
114
|
+
vec3( r2.z, r3.z, r4.z )
|
|
115
|
+
);
|
|
116
|
+
s3mPivot = vec3( r2.w, r3.w, r4.w );
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
s3mA = s3mRot * ( s3mA - s3mPivot ) + s3mPivot + s3mCustomOffset;
|
|
120
|
+
s3mB = s3mRot * ( s3mB - s3mPivot ) + s3mPivot + s3mCustomOffset;
|
|
121
|
+
|
|
51
122
|
// 选中单体平移/拔高:与着色器里 transformed += uTranslation 保持同一口径(物体空间)
|
|
52
123
|
vec3 offset = uTranslation + vec3( 0.0, 0.0, uElevation );
|
|
53
|
-
vec4 clipA = projectionMatrix * modelViewMatrix * vec4(
|
|
54
|
-
vec4 clipB = projectionMatrix * modelViewMatrix * vec4(
|
|
124
|
+
vec4 clipA = projectionMatrix * modelViewMatrix * vec4( s3mA + offset, 1.0 );
|
|
125
|
+
vec4 clipB = projectionMatrix * modelViewMatrix * vec4( s3mB + offset, 1.0 );
|
|
55
126
|
|
|
56
127
|
if ( clipA.w <= 0.0 || clipB.w <= 0.0 ) {
|
|
57
128
|
// 端点跑到相机后面,整段丢弃(裁到近平面的线段会有轻微截断,可接受)
|
|
@@ -87,19 +158,54 @@ const FRAG = /* glsl */ `
|
|
|
87
158
|
}
|
|
88
159
|
`;
|
|
89
160
|
|
|
161
|
+
/**
|
|
162
|
+
* 创建一份等宽线材质。
|
|
163
|
+
*
|
|
164
|
+
* @param styleUniforms 外观 uniform(颜色 / 粗细 / 透明度 / 拔高)。传入时**直接
|
|
165
|
+
* 复用这些 `{ value }` 对象**而不是新建——棱线材质逐网格一份,但外观必须全局统一,
|
|
166
|
+
* 共享对象才能让 `_syncEdgeStyle()` 的一次赋值作用到所有棱线上。
|
|
167
|
+
* @param batchUniforms 单体状态纹理 uniform。传入时同样直接复用主材质 userData 里
|
|
168
|
+
* 的那几个对象,主材质换纹理 / 开关 active 时棱线自动跟着走,零同步成本。
|
|
169
|
+
* 不传(或几何没有 aBatchId)时棱线不参与单体状态。
|
|
170
|
+
*/
|
|
90
171
|
export function createThickLineMaterial(
|
|
91
172
|
shared: LineSharedUniforms,
|
|
92
|
-
opts: { color?: THREE.ColorRepresentation; opacity?: number; width?: number; elevation?: number } = {}
|
|
173
|
+
opts: { color?: THREE.ColorRepresentation; opacity?: number; width?: number; elevation?: number } = {},
|
|
174
|
+
styleUniforms?: ThickLineStyleUniforms,
|
|
175
|
+
batchUniforms?: ThickLineBatchUniforms
|
|
93
176
|
): ThickLineMaterial {
|
|
177
|
+
const style: ThickLineStyleUniforms = styleUniforms ?? {
|
|
178
|
+
uColor: { value: new THREE.Color(opts.color ?? '#ffffff') },
|
|
179
|
+
uOpacity: { value: opts.opacity ?? 1 },
|
|
180
|
+
uWidth: { value: opts.width ?? 1.5 },
|
|
181
|
+
uElevation: { value: opts.elevation ?? 0 },
|
|
182
|
+
};
|
|
183
|
+
if (opts.color !== undefined && styleUniforms) style.uColor.value.set(opts.color);
|
|
184
|
+
if (opts.opacity !== undefined && styleUniforms) style.uOpacity.value = opts.opacity;
|
|
185
|
+
if (opts.width !== undefined && styleUniforms) style.uWidth.value = opts.width;
|
|
186
|
+
|
|
187
|
+
const batch: Required<ThickLineBatchUniforms> = {
|
|
188
|
+
uBatchTex: batchUniforms?.uBatchTex ?? { value: DUMMY_BATCH_TEX },
|
|
189
|
+
uBatchTex2: batchUniforms?.uBatchTex2 ?? { value: DUMMY_BATCH_TEX },
|
|
190
|
+
uBatchMix: batchUniforms?.uBatchMix ?? { value: 0 },
|
|
191
|
+
uBatchTexWidth: batchUniforms?.uBatchTexWidth ?? { value: 1 },
|
|
192
|
+
uBatchTexActive: batchUniforms?.uBatchTexActive ?? { value: 0 },
|
|
193
|
+
};
|
|
194
|
+
|
|
94
195
|
const mat = new THREE.ShaderMaterial({
|
|
95
196
|
uniforms: {
|
|
96
197
|
uResolution: shared.uResolution,
|
|
97
198
|
uDpr: shared.uDpr,
|
|
98
|
-
uColor:
|
|
99
|
-
uOpacity:
|
|
100
|
-
uWidth:
|
|
101
|
-
uElevation: { value: opts.elevation ?? 0 },
|
|
199
|
+
uColor: style.uColor,
|
|
200
|
+
uOpacity: style.uOpacity,
|
|
201
|
+
uWidth: style.uWidth,
|
|
202
|
+
uElevation: style.uElevation ?? { value: opts.elevation ?? 0 },
|
|
102
203
|
uTranslation: { value: new THREE.Vector3(0, 0, 0) },
|
|
204
|
+
uBatchTex: batch.uBatchTex,
|
|
205
|
+
uBatchTex2: batch.uBatchTex2,
|
|
206
|
+
uBatchMix: batch.uBatchMix,
|
|
207
|
+
uBatchTexWidth: batch.uBatchTexWidth,
|
|
208
|
+
uBatchTexActive: batch.uBatchTexActive,
|
|
103
209
|
},
|
|
104
210
|
vertexShader: VERT,
|
|
105
211
|
fragmentShader: FRAG,
|
|
@@ -117,14 +223,22 @@ export function createThickLineMaterial(
|
|
|
117
223
|
|
|
118
224
|
/**
|
|
119
225
|
* 线段对(每 6 个 float:x1 y1 z1 x2 y2 z2)→ 可绘制等宽线的几何体。
|
|
226
|
+
*
|
|
227
|
+
* @param batchIds 每段所属的单体 batchId(长度 = 段数)。给了它,顶点着色器才能
|
|
228
|
+
* 按单体从状态纹理取平移/拔高——棱线跟随移动靠的就是这个。不传全部填 -1,
|
|
229
|
+
* 着色器会跳过采样(等于不参与单体状态)。
|
|
120
230
|
*/
|
|
121
|
-
export function buildThickLineGeometry(
|
|
231
|
+
export function buildThickLineGeometry(
|
|
232
|
+
segments: ArrayLike<number>,
|
|
233
|
+
batchIds?: ArrayLike<number>
|
|
234
|
+
): THREE.BufferGeometry {
|
|
122
235
|
const count = Math.floor(segments.length / 6);
|
|
123
236
|
const vertexCount = count * 4;
|
|
124
237
|
|
|
125
238
|
const position = new Float32Array(vertexCount * 3);
|
|
126
239
|
const other = new Float32Array(vertexCount * 3);
|
|
127
240
|
const side = new Float32Array(vertexCount);
|
|
241
|
+
const batch = new Float32Array(vertexCount);
|
|
128
242
|
const index = vertexCount > 65535 ? new Uint32Array(count * 6) : new Uint16Array(count * 6);
|
|
129
243
|
|
|
130
244
|
for (let i = 0; i < count; i++) {
|
|
@@ -135,6 +249,7 @@ export function buildThickLineGeometry(segments: ArrayLike<number>): THREE.Buffe
|
|
|
135
249
|
const bx = segments[s + 3];
|
|
136
250
|
const by = segments[s + 4];
|
|
137
251
|
const bz = segments[s + 5];
|
|
252
|
+
const b = batchIds ? Number(batchIds[i] ?? -1) : -1;
|
|
138
253
|
|
|
139
254
|
const v = i * 4;
|
|
140
255
|
// A 端两枚
|
|
@@ -143,6 +258,7 @@ export function buildThickLineGeometry(segments: ArrayLike<number>): THREE.Buffe
|
|
|
143
258
|
// B 端两枚(side 反号)
|
|
144
259
|
setVertex(v + 2, bx, by, bz, ax, ay, az, 1);
|
|
145
260
|
setVertex(v + 3, bx, by, bz, ax, ay, az, -1);
|
|
261
|
+
for (let k = 0; k < 4; k++) batch[v + k] = b;
|
|
146
262
|
|
|
147
263
|
const o = i * 6;
|
|
148
264
|
index[o] = v;
|
|
@@ -177,11 +293,41 @@ export function buildThickLineGeometry(segments: ArrayLike<number>): THREE.Buffe
|
|
|
177
293
|
geometry.setAttribute('position', new THREE.BufferAttribute(position, 3));
|
|
178
294
|
geometry.setAttribute('aOther', new THREE.BufferAttribute(other, 3));
|
|
179
295
|
geometry.setAttribute('aSide', new THREE.BufferAttribute(side, 1));
|
|
296
|
+
geometry.setAttribute('aBatchId', new THREE.BufferAttribute(batch, 1));
|
|
180
297
|
geometry.setIndex(new THREE.BufferAttribute(index, 1));
|
|
181
298
|
geometry.computeBoundingSphere();
|
|
182
299
|
return geometry;
|
|
183
300
|
}
|
|
184
301
|
|
|
302
|
+
/**
|
|
303
|
+
* 从带 `aBatchId` 的几何里建「量化位置 → batchId」的查找表。
|
|
304
|
+
*
|
|
305
|
+
* `EdgesGeometry` 输出的线段只有坐标、没有顶点号,要知道每段棱线属于哪个单体,
|
|
306
|
+
* 只能拿端点坐标回来查。key 的量化方式必须和 `mergeVerticesByPosition` 完全一致,
|
|
307
|
+
* 否则焊过的顶点会对不上。返回 null 表示这个几何没有单体化信息。
|
|
308
|
+
*/
|
|
309
|
+
export function buildPositionBatchMap(
|
|
310
|
+
geometry: THREE.BufferGeometry,
|
|
311
|
+
tolerance: number
|
|
312
|
+
): Map<string, number> | null {
|
|
313
|
+
const pos = geometry.getAttribute('position') as THREE.BufferAttribute | undefined;
|
|
314
|
+
const bid = geometry.getAttribute('aBatchId') as THREE.BufferAttribute | undefined;
|
|
315
|
+
if (!pos || !bid || pos.count === 0 || pos.count !== bid.count) return null;
|
|
316
|
+
|
|
317
|
+
const inv = 1 / Math.max(tolerance, 1e-9);
|
|
318
|
+
const map = new Map<string, number>();
|
|
319
|
+
for (let i = 0; i < pos.count; i++) {
|
|
320
|
+
const key = positionQuantKey(pos.getX(i), pos.getY(i), pos.getZ(i), inv);
|
|
321
|
+
if (!map.has(key)) map.set(key, bid.getX(i));
|
|
322
|
+
}
|
|
323
|
+
return map.size > 0 ? map : null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** 位置量化 key:与 mergeVerticesByPosition 同一套网格,保证焊过的顶点对得上 */
|
|
327
|
+
export function positionQuantKey(x: number, y: number, z: number, inv: number): string {
|
|
328
|
+
return `${Math.round(x * inv)},${Math.round(y * inv)},${Math.round(z * inv)}`;
|
|
329
|
+
}
|
|
330
|
+
|
|
185
331
|
/* ------------------------------------------------------------------ 单体棱线提取 */
|
|
186
332
|
|
|
187
333
|
export interface EdgeVertexRange {
|