s3m-three 0.3.14 → 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.
- package/dist/browser/s3m-three.iife.ext-three.js +100 -24
- package/dist/browser/s3m-three.iife.js +100 -24
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/render/DatasetManager.d.ts +3 -1
- package/dist/render/DatasetManager.d.ts.map +1 -1
- package/dist/render/DatasetManager.js +2 -1
- package/dist/render/DatasetManager.js.map +1 -1
- package/dist/render/EdgeLines.d.ts +14 -2
- package/dist/render/EdgeLines.d.ts.map +1 -1
- package/dist/render/EdgeLines.js +45 -8
- package/dist/render/EdgeLines.js.map +1 -1
- package/dist/render/GpuEdgePass.d.ts +58 -0
- package/dist/render/GpuEdgePass.d.ts.map +1 -0
- package/dist/render/GpuEdgePass.js +207 -0
- package/dist/render/GpuEdgePass.js.map +1 -0
- package/dist/render/S3MViewer.d.ts +75 -7
- package/dist/render/S3MViewer.d.ts.map +1 -1
- package/dist/render/S3MViewer.js +270 -52
- package/dist/render/S3MViewer.js.map +1 -1
- package/dist/render/StyleMaterial.d.ts +108 -6
- package/dist/render/StyleMaterial.d.ts.map +1 -1
- package/dist/render/StyleMaterial.js +90 -10
- package/dist/render/StyleMaterial.js.map +1 -1
- package/docs/API.md +42 -5
- package/package.json +1 -1
- package/src/index.ts +6 -1
- package/src/render/DatasetManager.ts +5 -1
- package/src/render/EdgeLines.ts +50 -8
- package/src/render/GpuEdgePass.ts +236 -0
- package/src/render/S3MViewer.ts +310 -52
- package/src/render/StyleMaterial.ts +174 -13
package/src/render/EdgeLines.ts
CHANGED
|
@@ -34,6 +34,8 @@ export interface ThickLineStyleUniforms {
|
|
|
34
34
|
uWidth: { value: number };
|
|
35
35
|
uOpacity: { value: number };
|
|
36
36
|
uElevation?: { value: number };
|
|
37
|
+
/** 沿边法线外推的距离(米)。不传或 0 = 线贴在面上,与改动前一致 */
|
|
38
|
+
uOffset?: { value: number };
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
/** 等宽线的「单体状态」uniform:直接复用主材质 userData 里那几个对象,自动跟随 */
|
|
@@ -54,6 +56,7 @@ export interface ThickLineMaterial extends THREE.ShaderMaterial {
|
|
|
54
56
|
ThickLineStyleUniforms &
|
|
55
57
|
Required<ThickLineBatchUniforms> & {
|
|
56
58
|
uTranslation: { value: THREE.Vector3 };
|
|
59
|
+
uOffset: { value: number };
|
|
57
60
|
[uniform: string]: THREE.IUniform;
|
|
58
61
|
};
|
|
59
62
|
}
|
|
@@ -69,11 +72,13 @@ const VERT = /* glsl */ `
|
|
|
69
72
|
attribute vec3 aOther;
|
|
70
73
|
attribute float aSide;
|
|
71
74
|
attribute float aBatchId;
|
|
75
|
+
attribute vec3 aNormal;
|
|
72
76
|
|
|
73
77
|
uniform vec2 uResolution;
|
|
74
78
|
uniform float uDpr;
|
|
75
79
|
uniform float uWidth;
|
|
76
80
|
uniform float uElevation;
|
|
81
|
+
uniform float uOffset;
|
|
77
82
|
uniform vec3 uTranslation;
|
|
78
83
|
uniform sampler2D uBatchTex;
|
|
79
84
|
uniform sampler2D uBatchTex2;
|
|
@@ -95,8 +100,11 @@ const VERT = /* glsl */ `
|
|
|
95
100
|
void main() {
|
|
96
101
|
// 单体平移 / 旋转 / 拔高 / 隐藏:与主着色器同一份 6 行状态纹理、同一套变换口径。
|
|
97
102
|
// 不采样的话,单体一动或一转棱线就留在原地——这正是「棱线跟随」要解决的。
|
|
98
|
-
|
|
99
|
-
|
|
103
|
+
// 外推画法:整段沿「边法线」挪出去 uOffset 米(aNormal 全 0 时等于没挪,
|
|
104
|
+
// 普通折边线走的还是原来那条路径)。必须在旋转 / 拔高之前做,才是物体空间里的外扩。
|
|
105
|
+
vec3 s3mEdgeOffset = aNormal * uOffset;
|
|
106
|
+
vec3 s3mA = position + s3mEdgeOffset;
|
|
107
|
+
vec3 s3mB = aOther + s3mEdgeOffset;
|
|
100
108
|
vec3 s3mCustomOffset = vec3( 0.0 );
|
|
101
109
|
mat3 s3mRot = mat3( 1.0 );
|
|
102
110
|
vec3 s3mPivot = vec3( 0.0 );
|
|
@@ -257,6 +265,7 @@ export function createThickLineMaterial(
|
|
|
257
265
|
uOpacity: style.uOpacity,
|
|
258
266
|
uWidth: style.uWidth,
|
|
259
267
|
uElevation: style.uElevation ?? { value: opts.elevation ?? 0 },
|
|
268
|
+
uOffset: style.uOffset ?? { value: 0 },
|
|
260
269
|
uTranslation: { value: new THREE.Vector3(0, 0, 0) },
|
|
261
270
|
uBatchTex: batch.uBatchTex,
|
|
262
271
|
uBatchTex2: batch.uBatchTex2,
|
|
@@ -288,10 +297,13 @@ export function createThickLineMaterial(
|
|
|
288
297
|
* @param batchIds 每段所属的单体 batchId(长度 = 段数)。给了它,顶点着色器才能
|
|
289
298
|
* 按单体从状态纹理取平移/拔高——棱线跟随移动靠的就是这个。不传全部填 -1,
|
|
290
299
|
* 着色器会跳过采样(等于不参与单体状态)。
|
|
300
|
+
* @param normals 每段一条「边法线」(长度 = 段数 × 6,两端点各一份,同一条棱共用)。
|
|
301
|
+
* 只有外推画法需要;不传就全填 0,`uOffset` 乘上去等于没推,普通折边线的行为不变。
|
|
291
302
|
*/
|
|
292
303
|
export function buildThickLineGeometry(
|
|
293
304
|
segments: ArrayLike<number>,
|
|
294
|
-
batchIds?: ArrayLike<number
|
|
305
|
+
batchIds?: ArrayLike<number>,
|
|
306
|
+
normals?: ArrayLike<number>
|
|
295
307
|
): THREE.BufferGeometry {
|
|
296
308
|
const count = Math.floor(segments.length / 6);
|
|
297
309
|
const vertexCount = count * 4;
|
|
@@ -300,6 +312,7 @@ export function buildThickLineGeometry(
|
|
|
300
312
|
const other = new Float32Array(vertexCount * 3);
|
|
301
313
|
const side = new Float32Array(vertexCount);
|
|
302
314
|
const batch = new Float32Array(vertexCount);
|
|
315
|
+
const normal = new Float32Array(vertexCount * 3);
|
|
303
316
|
const index = vertexCount > 65535 ? new Uint32Array(count * 6) : new Uint16Array(count * 6);
|
|
304
317
|
|
|
305
318
|
for (let i = 0; i < count; i++) {
|
|
@@ -311,6 +324,9 @@ export function buildThickLineGeometry(
|
|
|
311
324
|
const by = segments[s + 4];
|
|
312
325
|
const bz = segments[s + 5];
|
|
313
326
|
const b = batchIds ? Number(batchIds[i] ?? -1) : -1;
|
|
327
|
+
const nx = normals ? Number(normals[s] ?? 0) : 0;
|
|
328
|
+
const ny = normals ? Number(normals[s + 1] ?? 0) : 0;
|
|
329
|
+
const nz = normals ? Number(normals[s + 2] ?? 0) : 0;
|
|
314
330
|
|
|
315
331
|
const v = i * 4;
|
|
316
332
|
// A 端两枚
|
|
@@ -319,7 +335,13 @@ export function buildThickLineGeometry(
|
|
|
319
335
|
// B 端两枚(side 反号)
|
|
320
336
|
setVertex(v + 2, bx, by, bz, ax, ay, az, 1);
|
|
321
337
|
setVertex(v + 3, bx, by, bz, ax, ay, az, -1);
|
|
322
|
-
for (let k = 0; k < 4; k++)
|
|
338
|
+
for (let k = 0; k < 4; k++) {
|
|
339
|
+
batch[v + k] = b;
|
|
340
|
+
const p = (v + k) * 3;
|
|
341
|
+
normal[p] = nx;
|
|
342
|
+
normal[p + 1] = ny;
|
|
343
|
+
normal[p + 2] = nz;
|
|
344
|
+
}
|
|
323
345
|
|
|
324
346
|
const o = i * 6;
|
|
325
347
|
index[o] = v;
|
|
@@ -355,6 +377,7 @@ export function buildThickLineGeometry(
|
|
|
355
377
|
geometry.setAttribute('aOther', new THREE.BufferAttribute(other, 3));
|
|
356
378
|
geometry.setAttribute('aSide', new THREE.BufferAttribute(side, 1));
|
|
357
379
|
geometry.setAttribute('aBatchId', new THREE.BufferAttribute(batch, 1));
|
|
380
|
+
geometry.setAttribute('aNormal', new THREE.BufferAttribute(normal, 3));
|
|
358
381
|
geometry.setIndex(new THREE.BufferAttribute(index, 1));
|
|
359
382
|
geometry.computeBoundingSphere();
|
|
360
383
|
return geometry;
|
|
@@ -411,11 +434,15 @@ const QUANT = 100;
|
|
|
411
434
|
*
|
|
412
435
|
* 非索引几何没有共享顶点,直接用顶点号去重会让每条边都被判成边界边。
|
|
413
436
|
* 所以这里先按位置量化把顶点归一化成 canonical id,再走同一套逻辑。
|
|
437
|
+
*
|
|
438
|
+
* @param outNormals 传了就把「边法线」按段追加进去(每段 6 个 float,两端点各一份)。
|
|
439
|
+
* 折边取相邻两面的平均法线,边界边取唯一那一面的法线。外推画法靠它决定往哪推。
|
|
414
440
|
*/
|
|
415
441
|
export function buildFeatureEdgeSegments(
|
|
416
442
|
geometry: THREE.BufferGeometry,
|
|
417
443
|
ranges?: EdgeVertexRange[] | null,
|
|
418
|
-
creaseAngle = 40
|
|
444
|
+
creaseAngle = 40,
|
|
445
|
+
outNormals?: number[]
|
|
419
446
|
): Float32Array | null {
|
|
420
447
|
const position = geometry.getAttribute('position') as THREE.BufferAttribute | undefined;
|
|
421
448
|
if (!position) return null;
|
|
@@ -480,11 +507,12 @@ export function buildFeatureEdgeSegments(
|
|
|
480
507
|
let ny = 0;
|
|
481
508
|
let nz = 0;
|
|
482
509
|
|
|
483
|
-
const emit = (lo: number, hi: number): boolean => {
|
|
510
|
+
const emit = (lo: number, hi: number, ex: number, ey: number, ez: number): boolean => {
|
|
484
511
|
if (out.length >= MAX_EDGE_SEGMENTS * 6) return false;
|
|
485
512
|
const a = repIdx(lo);
|
|
486
513
|
const b = repIdx(hi);
|
|
487
514
|
out.push(px[a], py[a], pz[a], px[b], py[b], pz[b]);
|
|
515
|
+
outNormals?.push(ex, ey, ez, ex, ey, ez);
|
|
488
516
|
return true;
|
|
489
517
|
};
|
|
490
518
|
|
|
@@ -502,7 +530,21 @@ export function buildFeatureEdgeSegments(
|
|
|
502
530
|
// 与首个共用该边的三角形比夹角
|
|
503
531
|
const dot = nx * normals[first * 3] + ny * normals[first * 3 + 1] + nz * normals[first * 3 + 2];
|
|
504
532
|
if (dot < cosLimit) {
|
|
505
|
-
|
|
533
|
+
// 两面的平均法线就是这条棱「往外」的方向;两面接近反向时退回当前面法线
|
|
534
|
+
let ex = normals[first * 3] + nx;
|
|
535
|
+
let ey = normals[first * 3 + 1] + ny;
|
|
536
|
+
let ez = normals[first * 3 + 2] + nz;
|
|
537
|
+
const l = Math.hypot(ex, ey, ez);
|
|
538
|
+
if (l > 1e-9) {
|
|
539
|
+
ex /= l;
|
|
540
|
+
ey /= l;
|
|
541
|
+
ez /= l;
|
|
542
|
+
} else {
|
|
543
|
+
ex = nx;
|
|
544
|
+
ey = ny;
|
|
545
|
+
ez = nz;
|
|
546
|
+
}
|
|
547
|
+
emit(lo, hi, ex, ey, ez);
|
|
506
548
|
seen.set(key, -1);
|
|
507
549
|
} else {
|
|
508
550
|
seen.set(key, -2);
|
|
@@ -543,7 +585,7 @@ export function buildFeatureEdgeSegments(
|
|
|
543
585
|
if (first < 0) continue;
|
|
544
586
|
const lo = Math.floor(key / span);
|
|
545
587
|
const hi = key - lo * span;
|
|
546
|
-
if (!emit(lo, hi)) break;
|
|
588
|
+
if (!emit(lo, hi, normals[first * 3], normals[first * 3 + 1], normals[first * 3 + 2])) break;
|
|
547
589
|
}
|
|
548
590
|
|
|
549
591
|
return out.length > 0 ? new Float32Array(out) : null;
|
|
@@ -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
|
+
}
|