s3m-three 0.3.2 → 0.3.4
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 +11 -11
- package/dist/browser/s3m-three.iife.js +11 -11
- package/dist/render/DatasetManager.d.ts +6 -0
- package/dist/render/DatasetManager.d.ts.map +1 -1
- package/dist/render/DatasetManager.js.map +1 -1
- package/dist/render/S3MViewer.d.ts +34 -0
- package/dist/render/S3MViewer.d.ts.map +1 -1
- package/dist/render/S3MViewer.js +152 -14
- package/dist/render/S3MViewer.js.map +1 -1
- package/dist/render/StyleMaterial.d.ts +2 -0
- package/dist/render/StyleMaterial.d.ts.map +1 -1
- package/dist/render/StyleMaterial.js +3 -2
- package/dist/render/StyleMaterial.js.map +1 -1
- package/package.json +1 -1
- package/src/render/DatasetManager.ts +6 -0
- package/src/render/S3MViewer.ts +158 -16
- package/src/render/StyleMaterial.ts +5 -2
package/src/render/S3MViewer.ts
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* MapLibre / Mapbox 的胶水层在 s3m-maplibre 包的 S3MLayer.ts。
|
|
22
22
|
*/
|
|
23
23
|
import * as THREE from 'three';
|
|
24
|
+
import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js';
|
|
24
25
|
import { S3MTileset, usableGeoBounds } from '../tileset/S3MTileset.js';
|
|
25
26
|
import type { BuildTileFn, TileNode } from '../tileset/S3MTileset.js';
|
|
26
27
|
import { setMaxAnisotropy } from './TextureFactory.js';
|
|
@@ -321,6 +322,8 @@ export class S3MViewer {
|
|
|
321
322
|
private _selectionEdges: THREE.Mesh | null = null;
|
|
322
323
|
private _selectionEdgeMeshes: THREE.Mesh[] = [];
|
|
323
324
|
private _edgesEnabled = false;
|
|
325
|
+
/** 预制棱线缓存(Crease Cache):从外部 source-creases 载入的全局统一 LineSegments,性能最高、视觉最干净 */
|
|
326
|
+
private _creaseLines: THREE.LineSegments | null = null;
|
|
324
327
|
|
|
325
328
|
/** 业务属性查询(可选,配了 `attributes` 才有) */
|
|
326
329
|
attributeQuery: AttributeQuery | null = null;
|
|
@@ -413,6 +416,11 @@ export class S3MViewer {
|
|
|
413
416
|
width: this.style.outlineWidthPx ?? 3,
|
|
414
417
|
});
|
|
415
418
|
|
|
419
|
+
// 关键:style.edges 是个开关,但构造函数从没据此启用过棱线,导致
|
|
420
|
+
// `style: { edges: true }` 形同虚设、_edgesEnabled 永远是 false。
|
|
421
|
+
// 这里按 style 实际值调一次 setEdges,让开关真正生效(与 setEdges 后来的手工调用一致)。
|
|
422
|
+
this.setEdges(!!this.style.edges);
|
|
423
|
+
|
|
416
424
|
this._fps.last = performance.now();
|
|
417
425
|
this._datasetQueue = this._normalizeDatasets(options);
|
|
418
426
|
if (options.attributes) this.attributeQuery = new AttributeQuery(options.attributes);
|
|
@@ -424,19 +432,25 @@ export class S3MViewer {
|
|
|
424
432
|
* 绑定宿主画布与 WebGL 上下文,建场景并启动数据集队列。
|
|
425
433
|
* MapLibre 宿主由 S3MLayer 在 onAdd 里调用;纯 three 宿主直接传自己的 canvas / gl。
|
|
426
434
|
*/
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
this.renderer = new THREE.WebGLRenderer({
|
|
435
|
+
/** 创建并配置一个绑定到给定 gl 上下文的 three renderer(mount 与上下文恢复共用) */
|
|
436
|
+
private _createRenderer(canvas: HTMLCanvasElement, gl: WebGLRenderingContext | WebGL2RenderingContext): THREE.WebGLRenderer {
|
|
437
|
+
const r = new THREE.WebGLRenderer({
|
|
431
438
|
canvas,
|
|
432
439
|
context: gl as WebGLRenderingContext,
|
|
433
440
|
antialias: true,
|
|
434
441
|
});
|
|
435
|
-
|
|
436
|
-
|
|
442
|
+
r.autoClear = false;
|
|
443
|
+
r.setPixelRatio(1);
|
|
437
444
|
// 白模 albedo 接近 1,不做色调映射会过曝成一片纯白、丢失明暗关系
|
|
438
|
-
|
|
439
|
-
|
|
445
|
+
r.toneMapping = THREE.ACESFilmicToneMapping;
|
|
446
|
+
r.toneMappingExposure = 1.0;
|
|
447
|
+
return r;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
mount(canvas: HTMLCanvasElement, gl: WebGLRenderingContext | WebGL2RenderingContext): void {
|
|
451
|
+
this.canvas = canvas;
|
|
452
|
+
|
|
453
|
+
this.renderer = this._createRenderer(canvas, gl);
|
|
440
454
|
setMaxAnisotropy(this.renderer.capabilities.getMaxAnisotropy());
|
|
441
455
|
|
|
442
456
|
this.scene = new THREE.Scene();
|
|
@@ -454,9 +468,42 @@ export class S3MViewer {
|
|
|
454
468
|
this._recomputeAnchors();
|
|
455
469
|
}
|
|
456
470
|
|
|
471
|
+
/**
|
|
472
|
+
* WebGL 上下文恢复后原地换新 renderer,保留已经加载好的场景与瓦片。
|
|
473
|
+
*
|
|
474
|
+
* MapLibre 在 `webglcontextlost` 时会销毁 style 并丢弃自定义图层(自定义图层
|
|
475
|
+
* 无法序列化进 style,所以恢复后不会自动加回)。但自定义图层的 `onRemove` 不会被
|
|
476
|
+
* 调用,因此我们的 scene / 几何体 / 材质 / 纹理 / 已加载瓦片都还在内存里。
|
|
477
|
+
*
|
|
478
|
+
* three 的 GPU 状态是按 renderer 实例缓存的,所以只需把 renderer / picker 换成
|
|
479
|
+
* 绑在新 gl 上的新实例,下一帧渲染时 three 会自动把现存的几何体、材质、纹理
|
|
480
|
+
* 重新上传到新上下文 —— 不需要重建场景,也不需要重新拉瓦片。
|
|
481
|
+
*/
|
|
482
|
+
restoreContext(gl: WebGLRenderingContext | WebGL2RenderingContext): void {
|
|
483
|
+
if (!this.canvas) {
|
|
484
|
+
// 理论不会走到:没 mount 过就没有上下文可恢复
|
|
485
|
+
console.warn('[s3m] restoreContext 调用过早(尚未 mount),按首次挂载处理');
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
try {
|
|
489
|
+
// 旧上下文已失效,dispose 可能抛错,忽略即可
|
|
490
|
+
this.renderer?.dispose();
|
|
491
|
+
} catch {
|
|
492
|
+
/* noop */
|
|
493
|
+
}
|
|
494
|
+
this.renderer = this._createRenderer(this.canvas, gl);
|
|
495
|
+
setMaxAnisotropy(this.renderer.capabilities.getMaxAnisotropy());
|
|
496
|
+
this.picker?.dispose();
|
|
497
|
+
this.picker = new Picker(this.renderer);
|
|
498
|
+
// 共享 uniform / 材质引用不变,three 重新编译着色器时会自然接管;
|
|
499
|
+
// 这里只确保下一帧尽快驱动一次渲染。
|
|
500
|
+
this._requestRepaint();
|
|
501
|
+
}
|
|
502
|
+
|
|
457
503
|
/** 解绑场景并释放全部 GPU 资源。对应 MapLibre 图层的 onRemove()。 */
|
|
458
504
|
unmount(): void {
|
|
459
505
|
for (const id of [...this.datasets.keys()]) this.removeDataset(id);
|
|
506
|
+
this.clearCreases();
|
|
460
507
|
this.groundManager.dispose();
|
|
461
508
|
this.picker?.dispose();
|
|
462
509
|
this.picker = null;
|
|
@@ -958,7 +1005,11 @@ export class S3MViewer {
|
|
|
958
1005
|
this._indexMeshSmids(mesh);
|
|
959
1006
|
this._addMeshHeight(mesh);
|
|
960
1007
|
// 点云没有「棱」:给一堆孤立点建 EdgesGeometry 又慢又没意义
|
|
961
|
-
|
|
1008
|
+
// 点云没有「棱」:给一堆孤立点建 EdgesGeometry 又慢又没意义。
|
|
1009
|
+
// 自动描边受三道开关控制:全局 style.edges / 数据集 enableEdges / 地形绿地等关键字跳过。
|
|
1010
|
+
if (!hasNativeEdges && !isPoints && this._datasetEdgesEnabled(entry) && !this._shouldSkipAutoEdges(ud, entry.name)) {
|
|
1011
|
+
this._ensureEdges(mesh);
|
|
1012
|
+
}
|
|
962
1013
|
|
|
963
1014
|
// 新瓦片要继承当前的不透明度与着色方式
|
|
964
1015
|
if (isPoints) {
|
|
@@ -1128,8 +1179,12 @@ export class S3MViewer {
|
|
|
1128
1179
|
// 点云不走 StyleMaterial 的 uMode,模式切换要单独同步它的材质
|
|
1129
1180
|
if ('mode' in patch) this._applyPointCloudStyles();
|
|
1130
1181
|
if ('shading' in patch) this._applyShading();
|
|
1131
|
-
|
|
1132
|
-
|
|
1182
|
+
// 临界角变了要重建 CPU 棱线(EdgesGeometry 参数),开/关走 setEdges 整体刷新
|
|
1183
|
+
if ('edgeThreshold' in patch && this._edgesEnabled) {
|
|
1184
|
+
this.setEdges(true);
|
|
1185
|
+
} else if ('edges' in patch) {
|
|
1186
|
+
this.setEdges(!!this.style.edges);
|
|
1187
|
+
} else if ('edgeColor' in patch || 'edgeWidth' in patch || 'opacity' in patch || 'edgeOpacity' in patch) {
|
|
1133
1188
|
this._refreshEdgeLines();
|
|
1134
1189
|
}
|
|
1135
1190
|
if (this._selection) {
|
|
@@ -1371,14 +1426,66 @@ export class S3MViewer {
|
|
|
1371
1426
|
step();
|
|
1372
1427
|
}
|
|
1373
1428
|
|
|
1429
|
+
/**
|
|
1430
|
+
* 挂载预制棱线缓存(Crease Cache)。
|
|
1431
|
+
*
|
|
1432
|
+
* 直接把 `source-creases.f32` 这类 Float32Array 缓冲载入为一组全局统一的
|
|
1433
|
+
* LineSegments —— 相比逐 Mesh 现算 EdgesGeometry,性能高一个量级,视觉也最干净。
|
|
1434
|
+
* 坐标按「场景本地坐标」(相对场景锚点的米)处理,与瓦片几何同一坐标系。
|
|
1435
|
+
*
|
|
1436
|
+
* @param url 棱线缓存地址(二进制 Float32 序列:x,y,z,x,y,z,… 成对成段)
|
|
1437
|
+
*/
|
|
1438
|
+
async loadCreases(url: string): Promise<void> {
|
|
1439
|
+
const resp = await fetch(url);
|
|
1440
|
+
if (!resp.ok) throw new Error(`[s3m] 棱线缓存加载失败: ${resp.status} ${url}`);
|
|
1441
|
+
const buf = await resp.arrayBuffer();
|
|
1442
|
+
const arr = new Float32Array(buf);
|
|
1443
|
+
if (arr.length < 6) return;
|
|
1444
|
+
|
|
1445
|
+
const geo = new THREE.BufferGeometry();
|
|
1446
|
+
geo.setAttribute('position', new THREE.BufferAttribute(arr, 3));
|
|
1447
|
+
const s = this.style;
|
|
1448
|
+
const mat = new THREE.LineBasicMaterial({
|
|
1449
|
+
color: new THREE.Color(s.edgeColor),
|
|
1450
|
+
transparent: true,
|
|
1451
|
+
opacity: Math.min(1, Math.max(0, s.edgeOpacity ?? 0.6)) * Math.min(1, Math.max(0.05, s.opacity ?? 1)),
|
|
1452
|
+
depthWrite: false,
|
|
1453
|
+
});
|
|
1454
|
+
const lines = new THREE.LineSegments(geo, mat);
|
|
1455
|
+
lines.renderOrder = 2;
|
|
1456
|
+
lines.frustumCulled = false;
|
|
1457
|
+
lines.raycast = () => {};
|
|
1458
|
+
|
|
1459
|
+
this.clearCreases();
|
|
1460
|
+
this._creaseLines = lines;
|
|
1461
|
+
this.scene?.add(lines);
|
|
1462
|
+
this._requestRepaint();
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
/** 移除并释放预制棱线缓存 */
|
|
1466
|
+
clearCreases(): void {
|
|
1467
|
+
if (!this._creaseLines) return;
|
|
1468
|
+
this.scene?.remove(this._creaseLines);
|
|
1469
|
+
this._creaseLines.geometry.dispose();
|
|
1470
|
+
(this._creaseLines.material as THREE.Material)?.dispose?.();
|
|
1471
|
+
this._creaseLines = null;
|
|
1472
|
+
this._requestRepaint();
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1374
1475
|
/** 让全局棱线材质跟上 style.edgeColor / edgeWidth / edgeOpacity */
|
|
1375
1476
|
private _syncEdgeStyle(): void {
|
|
1376
1477
|
const s = this.style;
|
|
1377
1478
|
const u = this.edgeMaterial.uniforms;
|
|
1378
1479
|
u.uColor.value.set(s.edgeColor);
|
|
1379
|
-
u.uWidth.value = Math.max(0.1, s.edgeWidth ?? 1.
|
|
1480
|
+
u.uWidth.value = Math.max(0.1, s.edgeWidth ?? 1.5);
|
|
1380
1481
|
u.uOpacity.value =
|
|
1381
|
-
Math.min(1, Math.max(0, s.edgeOpacity ?? 0.
|
|
1482
|
+
Math.min(1, Math.max(0, s.edgeOpacity ?? 0.6)) * Math.min(1, Math.max(0.05, s.opacity ?? 1));
|
|
1483
|
+
// 预制棱线缓存(LineBasicMaterial)同步颜色 / 不透明度
|
|
1484
|
+
if (this._creaseLines) {
|
|
1485
|
+
const m = this._creaseLines.material as THREE.LineBasicMaterial;
|
|
1486
|
+
m.color.set(s.edgeColor);
|
|
1487
|
+
m.opacity = Math.min(1, Math.max(0, s.edgeOpacity ?? 0.6)) * Math.min(1, Math.max(0.05, s.opacity ?? 1));
|
|
1488
|
+
}
|
|
1382
1489
|
}
|
|
1383
1490
|
|
|
1384
1491
|
/** 让数据自带的棱线跟着 style.edgeColor / opacity 走(这些是 LineBasicMaterial,改不了粗细) */
|
|
@@ -1386,7 +1493,7 @@ export class S3MViewer {
|
|
|
1386
1493
|
const mat = lines.material as THREE.LineBasicMaterial;
|
|
1387
1494
|
if (!mat || !mat.color) return;
|
|
1388
1495
|
mat.color.set(this.style.edgeColor);
|
|
1389
|
-
const baseOpacity = this.style.edgeOpacity ?? 0.
|
|
1496
|
+
const baseOpacity = this.style.edgeOpacity ?? 0.6;
|
|
1390
1497
|
mat.opacity = baseOpacity * Math.min(1, Math.max(0.05, this.style.opacity ?? 1));
|
|
1391
1498
|
mat.transparent = true;
|
|
1392
1499
|
mat.polygonOffset = true;
|
|
@@ -1404,14 +1511,46 @@ export class S3MViewer {
|
|
|
1404
1511
|
}
|
|
1405
1512
|
}
|
|
1406
1513
|
|
|
1514
|
+
/** 地形 / 绿地 / 道路 / 地面铺装等不参与自动描边(数据自带棱线包另说) */
|
|
1515
|
+
private static readonly _EDGE_SKIP_KEYWORDS = [
|
|
1516
|
+
'terrain', 'ground', 'vegetation', 'green', 'road', 'dem',
|
|
1517
|
+
'绿地', '道路', '地面', '地形', '植被', '铺装',
|
|
1518
|
+
];
|
|
1519
|
+
|
|
1520
|
+
/** 数据集级别的棱线开关:enableEdges 优先,否则跟随全局 style.edges */
|
|
1521
|
+
private _datasetEdgesEnabled(entry: S3MDatasetEntry): boolean {
|
|
1522
|
+
const opt = entry.options?.enableEdges;
|
|
1523
|
+
if (opt === true) return true;
|
|
1524
|
+
if (opt === false) return false;
|
|
1525
|
+
return this._edgesEnabled;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
/** 该 mesh 是否命中「跳过自动描边」的关键字(skeletonName / 数据集名) */
|
|
1529
|
+
private _shouldSkipAutoEdges(ud: MeshUserData, datasetName: string): boolean {
|
|
1530
|
+
const hay = `${ud.skeletonName ?? ''} ${datasetName}`.toLowerCase();
|
|
1531
|
+
return S3MViewer._EDGE_SKIP_KEYWORDS.some((k) => hay.includes(k));
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1407
1534
|
private _ensureEdges(mesh: THREE.Mesh): void {
|
|
1408
1535
|
const ud = mesh.userData as MeshUserData;
|
|
1409
1536
|
if (ud.edges || ud.nativeEdges || !mesh.geometry) return;
|
|
1410
|
-
|
|
1537
|
+
// 地形 / 绿地 / 道路等不参与自动描边(即使全局开棱线也要跳过)
|
|
1538
|
+
if (this._shouldSkipAutoEdges(ud, '')) return;
|
|
1539
|
+
|
|
1540
|
+
const posAttr = mesh.geometry.getAttribute('position');
|
|
1541
|
+
const indexCount = mesh.geometry.index?.count ?? (posAttr ? posAttr.count : 0);
|
|
1411
1542
|
// 棱线是 CPU 端建的,超大几何直接跳过,避免拖慢加载
|
|
1412
1543
|
if (indexCount > 120000) return;
|
|
1413
1544
|
|
|
1414
|
-
|
|
1545
|
+
// 1. 先做顶点容差焊合,让相邻三角形正确共享边索引。
|
|
1546
|
+
// S3M 几何常为「每面独立顶点」,不焊合的话 EdgesGeometry 会把每条三角形边都当成边界,棱线爆炸。
|
|
1547
|
+
// tolerance 取米级模型下的 5mm,足以合并重合顶点又不会误并真实相邻面。
|
|
1548
|
+
const welded = BufferGeometryUtils.mergeVertices(mesh.geometry, 0.005);
|
|
1549
|
+
// 2. 调大临界角(默认 55°,可在 style.edgeThreshold 调),过滤浅坡与曲面细分,只保留真正的棱。
|
|
1550
|
+
const thresholdAngle = this.style.edgeThreshold ?? 55;
|
|
1551
|
+
const source = new THREE.EdgesGeometry(welded, thresholdAngle);
|
|
1552
|
+
welded.dispose();
|
|
1553
|
+
|
|
1415
1554
|
const attr = source.getAttribute('position') as THREE.BufferAttribute | undefined;
|
|
1416
1555
|
const segments = attr ? (attr.array as Float32Array) : null;
|
|
1417
1556
|
source.dispose();
|
|
@@ -2132,6 +2271,9 @@ export class S3MViewer {
|
|
|
2132
2271
|
canvasHeight: number;
|
|
2133
2272
|
}): boolean {
|
|
2134
2273
|
if (!this.renderer || !this.scene || !this.camera || !this.sceneAnchor || !this.canvas) return false;
|
|
2274
|
+
// 上下文丢失期间(丢失→恢复之间)不要尝试渲染,否则会刷一堆 GL 错误
|
|
2275
|
+
const gl = this.renderer.getContext() as WebGLRenderingContext | null;
|
|
2276
|
+
if (gl && typeof gl.isContextLost === 'function' && gl.isContextLost()) return false;
|
|
2135
2277
|
|
|
2136
2278
|
// 脏标记正常由微任务刷掉;这里再兜一次底,保证「本帧画出来的」一定是最新样式
|
|
2137
2279
|
this.flushOverrides();
|
|
@@ -110,6 +110,8 @@ export interface S3MStyle {
|
|
|
110
110
|
edgeWidth?: number;
|
|
111
111
|
edgeColor: string;
|
|
112
112
|
edgeOpacity?: number;
|
|
113
|
+
/** 棱线临界角(度):相邻面法线夹角超过该值才描边,过滤掉浅坡与曲面细分。默认 55° */
|
|
114
|
+
edgeThreshold?: number;
|
|
113
115
|
roofThreshold?: number;
|
|
114
116
|
/** 是否与模型原色贴图叠加混合(0 = 纯色覆盖,1 = 叠乘原色贴图) */
|
|
115
117
|
texMix?: number;
|
|
@@ -139,9 +141,10 @@ export const DEFAULT_STYLE: S3MStyle = {
|
|
|
139
141
|
outlineWidth: 0.9,
|
|
140
142
|
outlineWidthPx: 3,
|
|
141
143
|
edges: false,
|
|
142
|
-
edgeWidth: 1.
|
|
144
|
+
edgeWidth: 1.5,
|
|
143
145
|
edgeColor: '#334155',
|
|
144
|
-
edgeOpacity: 0.
|
|
146
|
+
edgeOpacity: 0.6,
|
|
147
|
+
edgeThreshold: 55,
|
|
145
148
|
roofThreshold: 0.65,
|
|
146
149
|
texMix: 0,
|
|
147
150
|
depthWrite: true,
|