s3m-three 0.4.15 → 0.4.16

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.
@@ -0,0 +1,236 @@
1
+ /**
2
+ * GPU 描边后处理(深度 + 法线)
3
+ *
4
+ * 为什么不用「材质里求法线导数」:`dFdx` 只在 2×2 quad 内求差,quad 里落到三角形外的
5
+ * 像素是**同一个三角形的外推值**,不是隔壁三角形的真实值 —— 所以片元着色器永远看不到
6
+ * 三角形边界,而平面着色几何的折边恰好就是三角形边界,一条都描不出来(实测法线导数
7
+ * 恒为 0)。业界做描边都走「法线/深度缓冲 + 后处理」,就是这里这套。
8
+ *
9
+ * 流程(每帧):
10
+ * 1. 用 `MeshNormalMaterial` 覆盖材质把整个场景渲进一张 RT:颜色 = 视空间法线,
11
+ * 深度 = 深度纹理。这一遍没有贴图、没有光照,成本基本就是几何 + 填充。
12
+ * 2. 画一个全屏 quad:在 RT 上按 `uRadius` 采样 4 邻域,取「法线差」和「相对深度差」
13
+ * 的最大值当描边强度。采样偏移就是线宽,也就是「外扩」半径。
14
+ * 3. quad 里 `gl_FragDepth` 写回该像素在 RT 里的深度,于是它和宿主(MapLibre 的底图 /
15
+ * 地形 / 其他图层)共用同一个深度缓冲 —— 被地形挡住的地方描边也会被挡住。
16
+ *
17
+ * 只支持 WebGL2(`gl_FragDepth` + `DepthTexture` 都是核心特性)。WebGL1 上 `supported()`
18
+ * 返回 false,调用方静默跳过,不影响其它画法。
19
+ */
20
+ import * as THREE from 'three';
21
+
22
+ /** 描边参数(全部来自 style,每帧传一次) */
23
+ export interface GpuEdgeParams {
24
+ /** 描边颜色 */
25
+ color: THREE.Color;
26
+ /** 描边强度(已乘过 edgeOpacity / 整体不透明度 / 淡入系数) */
27
+ opacity: number;
28
+ /** 采样偏移(设备像素):线宽 ≈ 2×radius,也就是「外扩」半径 */
29
+ radius: number;
30
+ /** 法线差阈值:相邻采样的法线差超过它就算棱(|Δn| = 2·sin(θ/2) 量级) */
31
+ normalThreshold: number;
32
+ /** 相对深度差阈值:深度突变(几何轮廓 / 前后遮挡)也算棱 */
33
+ depthThreshold: number;
34
+ }
35
+
36
+ const QUAD_VERT = /* glsl */ `
37
+ out vec2 vUv;
38
+ void main() {
39
+ vUv = uv;
40
+ // 直接给裁剪空间坐标,不走任何矩阵 —— 全屏 quad 没有相机语义
41
+ gl_Position = vec4( position.xy, 0.0, 1.0 );
42
+ }
43
+ `;
44
+
45
+ const QUAD_FRAG = /* glsl */ `
46
+ precision highp float;
47
+
48
+ uniform sampler2D tNormal;
49
+ uniform sampler2D tDepth;
50
+ uniform vec2 uTexel;
51
+ uniform float uRadius;
52
+ uniform float uNormalThreshold;
53
+ uniform float uDepthThreshold;
54
+ uniform vec3 uColor;
55
+ uniform float uOpacity;
56
+
57
+ in vec2 vUv;
58
+ out vec4 fragColor;
59
+
60
+ /** 两个采样的法线差(法线在 RT 里编码成 0.5 + n/2) */
61
+ float normalDelta( vec2 a, vec2 b ) {
62
+ vec3 na = texture( tNormal, a ).xyz * 2.0 - 1.0;
63
+ vec3 nb = texture( tNormal, b ).xyz * 2.0 - 1.0;
64
+ return length( na - nb );
65
+ }
66
+
67
+ /**
68
+ * 两个采样的相对深度差。
69
+ * 除以较小深度是为了让「远处同样米级的落差」也有可比性:深度缓冲是非线性的,
70
+ * 远处 1 米的落差在缓冲里可能只差 1e-5,不归一化就永远达不到阈值。
71
+ */
72
+ float depthDelta( vec2 a, vec2 b ) {
73
+ float da = texture( tDepth, a ).x;
74
+ float db = texture( tDepth, b ).x;
75
+ return abs( da - db ) / max( min( da, db ), 1e-6 );
76
+ }
77
+
78
+ void main() {
79
+ float d = texture( tDepth, vUv ).x;
80
+ // 没有几何的地方(清屏深度)不画,否则整片天空都会描边
81
+ if ( d >= 0.999999 ) discard;
82
+
83
+ vec2 o = uTexel * max( uRadius, 1.0 );
84
+ vec2 dx = vec2( o.x, 0.0 );
85
+ vec2 dy = vec2( 0.0, o.y );
86
+
87
+ float n = max(
88
+ max( normalDelta( vUv, vUv + dx ), normalDelta( vUv, vUv - dx ) ),
89
+ max( normalDelta( vUv, vUv + dy ), normalDelta( vUv, vUv - dy ) )
90
+ );
91
+ float dd = max(
92
+ max( depthDelta( vUv, vUv + dx ), depthDelta( vUv, vUv - dx ) ),
93
+ max( depthDelta( vUv, vUv + dy ), depthDelta( vUv, vUv - dy ) )
94
+ );
95
+
96
+ float m = max(
97
+ smoothstep( uNormalThreshold, uNormalThreshold * 1.6, n ),
98
+ smoothstep( uDepthThreshold, uDepthThreshold * 2.0, dd )
99
+ );
100
+ float a = m * uOpacity;
101
+ if ( a <= 0.004 ) discard;
102
+
103
+ // 写回该像素在本层里的深度:与宿主共用深度缓冲,被地形挡住就不显示
104
+ gl_FragDepth = d;
105
+ fragColor = vec4( uColor, a );
106
+ }
107
+ `;
108
+
109
+ export class GpuEdgePass {
110
+ private rt: THREE.WebGLRenderTarget | null = null;
111
+ private width = 0;
112
+ private height = 0;
113
+ /** 法线通道用的覆盖材质:视空间法线编码进颜色,深度走深度纹理 */
114
+ private readonly normalMaterial = new THREE.MeshNormalMaterial();
115
+ private readonly quadScene = new THREE.Scene();
116
+ private readonly quadCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
117
+ readonly material: THREE.ShaderMaterial;
118
+
119
+ constructor() {
120
+ this.material = new THREE.ShaderMaterial({
121
+ glslVersion: THREE.GLSL3,
122
+ uniforms: {
123
+ tNormal: { value: null },
124
+ tDepth: { value: null },
125
+ uTexel: { value: new THREE.Vector2(1, 1) },
126
+ uRadius: { value: 1.5 },
127
+ uNormalThreshold: { value: 0.55 },
128
+ uDepthThreshold: { value: 0.08 },
129
+ uColor: { value: new THREE.Color('#334155') },
130
+ uOpacity: { value: 0.6 },
131
+ },
132
+ vertexShader: QUAD_VERT,
133
+ fragmentShader: QUAD_FRAG,
134
+ transparent: true,
135
+ depthTest: true,
136
+ depthWrite: false,
137
+ side: THREE.DoubleSide,
138
+ blending: THREE.NormalBlending,
139
+ });
140
+ const quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), this.material);
141
+ quad.frustumCulled = false;
142
+ quad.raycast = () => {};
143
+ this.quadScene.add(quad);
144
+ }
145
+
146
+ /** `gl_FragDepth` 与深度纹理都是 WebGL2 核心特性,WebGL1 直接不参与 */
147
+ supported(renderer: THREE.WebGLRenderer): boolean {
148
+ return (renderer.capabilities as { isWebGL2?: boolean }).isWebGL2 === true;
149
+ }
150
+
151
+ /** 尺寸变了就重建 RT(canvas 跟着宿主缩放,必须每帧校验) */
152
+ private ensureTarget(width: number, height: number): THREE.WebGLRenderTarget {
153
+ if (this.rt && this.width === width && this.height === height) return this.rt;
154
+ this.disposeTarget();
155
+ const rt = new THREE.WebGLRenderTarget(Math.max(1, width), Math.max(1, height), {
156
+ minFilter: THREE.NearestFilter,
157
+ magFilter: THREE.NearestFilter,
158
+ format: THREE.RGBAFormat,
159
+ type: THREE.UnsignedByteType,
160
+ depthBuffer: true,
161
+ stencilBuffer: false,
162
+ generateMipmaps: false,
163
+ });
164
+ rt.texture.generateMipmaps = false;
165
+ rt.depthTexture = new THREE.DepthTexture(Math.max(1, width), Math.max(1, height), THREE.UnsignedIntType);
166
+ this.rt = rt;
167
+ this.width = width;
168
+ this.height = height;
169
+ return rt;
170
+ }
171
+
172
+ /**
173
+ * 渲一遍法线 + 深度,再把描边叠加到当前 framebuffer(调用方已画完主场景)。
174
+ *
175
+ * 注意顺序:主场景必须先画完 —— 描边 quad 要参与宿主的深度测试,
176
+ * 依赖主场景已经写好的深度。
177
+ */
178
+ render(
179
+ renderer: THREE.WebGLRenderer,
180
+ scene: THREE.Scene,
181
+ camera: THREE.Camera,
182
+ width: number,
183
+ height: number,
184
+ params: GpuEdgeParams
185
+ ): void {
186
+ if (!this.supported(renderer) || width <= 0 || height <= 0) return;
187
+ const rt = this.ensureTarget(width, height);
188
+
189
+ // ---- 1. 法线 + 深度到 RT(覆盖材质,无贴图无光照) ----
190
+ const prevOverride = scene.overrideMaterial;
191
+ const prevClear = renderer.getClearColor(new THREE.Color());
192
+ const prevClearAlpha = renderer.getClearAlpha();
193
+ scene.overrideMaterial = this.normalMaterial;
194
+ renderer.setRenderTarget(rt);
195
+ renderer.setClearColor(0x000000, 0);
196
+ renderer.clear(true, true, true);
197
+ renderer.render(scene, camera);
198
+ scene.overrideMaterial = prevOverride;
199
+ renderer.setRenderTarget(null);
200
+ renderer.setClearColor(prevClear, prevClearAlpha);
201
+
202
+ // ---- 2. 全屏描边叠加 ----
203
+ const u = this.material.uniforms;
204
+ u.tNormal.value = rt.texture;
205
+ u.tDepth.value = rt.depthTexture;
206
+ (u.uTexel.value as THREE.Vector2).set(1 / Math.max(1, width), 1 / Math.max(1, height));
207
+ u.uRadius.value = Math.max(1, params.radius);
208
+ u.uNormalThreshold.value = Math.max(0.001, params.normalThreshold);
209
+ u.uDepthThreshold.value = Math.max(0.001, params.depthThreshold);
210
+ (u.uColor.value as THREE.Color).copy(params.color);
211
+ u.uOpacity.value = params.opacity;
212
+
213
+ renderer.setViewport(0, 0, width, height);
214
+ renderer.resetState();
215
+ renderer.render(this.quadScene, this.quadCamera);
216
+ }
217
+
218
+ private disposeTarget(): void {
219
+ if (!this.rt) return;
220
+ this.rt.depthTexture?.dispose();
221
+ this.rt.dispose();
222
+ this.rt = null;
223
+ this.width = 0;
224
+ this.height = 0;
225
+ }
226
+
227
+ dispose(): void {
228
+ this.disposeTarget();
229
+ this.material.dispose();
230
+ this.normalMaterial.dispose();
231
+ this.quadScene.traverse((o) => {
232
+ const mesh = o as THREE.Mesh;
233
+ mesh.geometry?.dispose?.();
234
+ });
235
+ }
236
+ }