s3m-three 0.3.4 → 0.3.6
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/assets/s3mWorker-hZY5KqWF.js +1 -0
- package/dist/browser/s3m-three.iife.ext-three.js +26 -20
- package/dist/browser/s3m-three.iife.js +26 -20
- package/dist/core/TextureDecode.d.ts +18 -0
- package/dist/core/TextureDecode.d.ts.map +1 -0
- package/dist/core/TextureDecode.js +130 -0
- package/dist/core/TextureDecode.js.map +1 -0
- package/dist/core/TextureFormat.d.ts +22 -2
- package/dist/core/TextureFormat.d.ts.map +1 -1
- package/dist/core/TextureFormat.js +22 -2
- package/dist/core/TextureFormat.js.map +1 -1
- package/dist/render/DatasetManager.d.ts +2 -0
- package/dist/render/DatasetManager.d.ts.map +1 -1
- package/dist/render/DatasetManager.js +1 -0
- package/dist/render/DatasetManager.js.map +1 -1
- package/dist/render/FeatureStyleManager.d.ts +4 -2
- package/dist/render/FeatureStyleManager.d.ts.map +1 -1
- package/dist/render/FeatureStyleManager.js +33 -1
- package/dist/render/FeatureStyleManager.js.map +1 -1
- package/dist/render/MergeVertices.d.ts +17 -0
- package/dist/render/MergeVertices.d.ts.map +1 -0
- package/dist/render/MergeVertices.js +50 -0
- package/dist/render/MergeVertices.js.map +1 -0
- package/dist/render/S3MViewer.d.ts +2 -0
- package/dist/render/S3MViewer.d.ts.map +1 -1
- package/dist/render/S3MViewer.js +9 -4
- package/dist/render/S3MViewer.js.map +1 -1
- package/dist/render/StyleMaterial.d.ts.map +1 -1
- package/dist/render/StyleMaterial.js +6 -0
- package/dist/render/StyleMaterial.js.map +1 -1
- package/dist/render/TextureFactory.d.ts +8 -19
- package/dist/render/TextureFactory.d.ts.map +1 -1
- package/dist/render/TextureFactory.js +39 -116
- package/dist/render/TextureFactory.js.map +1 -1
- package/dist/tileset/S3MTileset.d.ts +17 -0
- package/dist/tileset/S3MTileset.d.ts.map +1 -1
- package/dist/tileset/S3MTileset.js +32 -1
- package/dist/tileset/S3MTileset.js.map +1 -1
- package/dist/worker/S3MWorkerClient.d.ts +40 -0
- package/dist/worker/S3MWorkerClient.d.ts.map +1 -0
- package/dist/worker/S3MWorkerClient.js +120 -0
- package/dist/worker/S3MWorkerClient.js.map +1 -0
- package/dist/worker/s3mWorker.d.ts +2 -0
- package/dist/worker/s3mWorker.d.ts.map +1 -0
- package/dist/worker/s3mWorker.js +48 -0
- package/dist/worker/s3mWorker.js.map +1 -0
- package/dist/worker/transferables.d.ts +14 -0
- package/dist/worker/transferables.d.ts.map +1 -0
- package/dist/worker/transferables.js +61 -0
- package/dist/worker/transferables.js.map +1 -0
- package/docs/API.md +4 -0
- package/package.json +1 -1
- package/src/core/TextureDecode.ts +158 -0
- package/src/core/TextureFormat.ts +23 -2
- package/src/render/DatasetManager.ts +3 -0
- package/src/render/FeatureStyleManager.ts +31 -2
- package/src/render/MergeVertices.ts +56 -0
- package/src/render/S3MViewer.ts +12 -4
- package/src/render/StyleMaterial.ts +6 -0
- package/src/render/TextureFactory.ts +43 -122
- package/src/tileset/S3MTileset.ts +39 -1
- package/src/worker/S3MWorkerClient.ts +150 -0
- package/src/worker/s3mWorker.ts +58 -0
- package/src/worker/transferables.ts +58 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S3M 纹理解码(three 无关,主线程 / Worker 共用同一份实现)
|
|
3
|
+
*
|
|
4
|
+
* 职责到「解出像素数据」为止:DXT 族解块成 RGBA8、CRN 先用 crunch wasm 转码回 DXT
|
|
5
|
+
* 再解块、WEBP 交给 createImageBitmap、RGB 系逐像素重排。产出是纯数据
|
|
6
|
+
* (DecodedTextureData),包成 three 对象由渲染层的 TextureFactory 负责——
|
|
7
|
+
* 这样整份文件可以原样搬进 Worker 跑。
|
|
8
|
+
*
|
|
9
|
+
* 该走哪条解码路径由 compressType 判定(见 core/TextureFormat.ts):
|
|
10
|
+
* v3.00 的 CRN 纹理 pixelFormat 是 13(ABGR)、行标的 CRN 是 32856(GL_RGBA8),
|
|
11
|
+
* 都不是 26 / 27,所以判定只能看 compressType,不能看 pixelFormat。
|
|
12
|
+
*/
|
|
13
|
+
import DXTTextureDecode from './DXTTextureDecode.js';
|
|
14
|
+
import { transcodeCRN } from './CRNDecode.js';
|
|
15
|
+
import { dxtPixelFormatOf, resolveTextureKind, PixelFormat } from './TextureFormat.js';
|
|
16
|
+
import type { S3MTexture } from './types.js';
|
|
17
|
+
|
|
18
|
+
/** 解码产物:rgba 与 bitmap 二选一,都为 null = 解码失败 / 格式不支持 */
|
|
19
|
+
export interface DecodedTextureData {
|
|
20
|
+
/** 实际走的解码路径('crn->dxt5' / 'dxt1' / 'raw' / 'webp' / 'crn(failed)' / 'unsupported(...)') */
|
|
21
|
+
kind: string;
|
|
22
|
+
width: number;
|
|
23
|
+
height: number;
|
|
24
|
+
/** RGBA8 像素(已按需翻行) */
|
|
25
|
+
rgba: Uint8Array | null;
|
|
26
|
+
/** WEBP 路径的解码结果(ImageBitmap 可 transfer,主线程直接 new THREE.Texture 包) */
|
|
27
|
+
bitmap: ImageBitmap | null;
|
|
28
|
+
hasAlpha: boolean;
|
|
29
|
+
/** CRN 转码失败时的原始错误(渲染层负责打 warn) */
|
|
30
|
+
error?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 行序翻转(原地)。S3MBLOCK 图集按 GL 方向存储(v=0 在最后一行),解码后要翻一次行 */
|
|
34
|
+
function flipRows(rgba: Uint8Array, width: number, height: number): void {
|
|
35
|
+
const row = width * 4;
|
|
36
|
+
const tmp = new Uint8Array(row);
|
|
37
|
+
for (let y = 0; y < height >> 1; y++) {
|
|
38
|
+
const a = y * row;
|
|
39
|
+
const b = (height - 1 - y) * row;
|
|
40
|
+
tmp.set(rgba.subarray(a, a + row));
|
|
41
|
+
rgba.copyWithin(a, b, b + row);
|
|
42
|
+
rgba.set(tmp, b);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 该像素格式自带 alpha 通道(DXT1 的 1bit alpha 不算,历史上没开过 alphaTest) */
|
|
47
|
+
function hasAlphaChannel(pixelFormat: number): boolean {
|
|
48
|
+
return (
|
|
49
|
+
pixelFormat === PixelFormat.DXT3 ||
|
|
50
|
+
pixelFormat === PixelFormat.DXT4 ||
|
|
51
|
+
pixelFormat === PixelFormat.DXT5 ||
|
|
52
|
+
pixelFormat === PixelFormat.ARGB ||
|
|
53
|
+
pixelFormat === PixelFormat.ABGR ||
|
|
54
|
+
pixelFormat === PixelFormat.BGRA ||
|
|
55
|
+
pixelFormat === PixelFormat.RGBA
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 解 DXT 到 RGBA8。DXT5 是 1 字节/像素,DXT1 是 0.5 字节/像素。 */
|
|
60
|
+
/** DXT 解码只需要尺寸 / 格式 / 字节,CRN 转码出来的结果也走这条路 */
|
|
61
|
+
function decodeDXT(tex: Pick<S3MTexture, 'width' | 'height' | 'pixelFormat' | 'data' | 'flipY'>): {
|
|
62
|
+
rgba: Uint8Array;
|
|
63
|
+
hasAlpha: boolean;
|
|
64
|
+
} {
|
|
65
|
+
const { width, height, pixelFormat, data } = tex;
|
|
66
|
+
const out = new Uint8Array(width * height * 4);
|
|
67
|
+
DXTTextureDecode.decode(out, width, height, data, pixelFormat);
|
|
68
|
+
if (tex.flipY) flipRows(out, width, height);
|
|
69
|
+
return { rgba: out, hasAlpha: hasAlphaChannel(pixelFormat) };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function rawRGBA(tex: S3MTexture): { rgba: Uint8Array; hasAlpha: boolean } {
|
|
73
|
+
const { width, height, pixelFormat, data } = tex;
|
|
74
|
+
const out = new Uint8Array(width * height * 4);
|
|
75
|
+
// RGBA / BGRA / ARGB / ABGR 都是 4 字节,前两个是「尾字节是 alpha」,后两个是「首字节是 alpha」
|
|
76
|
+
const hasAlpha =
|
|
77
|
+
pixelFormat === PixelFormat.RGBA ||
|
|
78
|
+
pixelFormat === PixelFormat.BGRA ||
|
|
79
|
+
pixelFormat === PixelFormat.ARGB ||
|
|
80
|
+
pixelFormat === PixelFormat.ABGR;
|
|
81
|
+
const swapRB = pixelFormat === PixelFormat.BGR || pixelFormat === PixelFormat.BGRA;
|
|
82
|
+
const alphaFirst = pixelFormat === PixelFormat.ARGB || pixelFormat === PixelFormat.ABGR;
|
|
83
|
+
const src = hasAlpha ? 4 : 3;
|
|
84
|
+
for (let i = 0, s = 0, d = 0; i < width * height; i++, s += src, d += 4) {
|
|
85
|
+
const p = alphaFirst ? s + 1 : s;
|
|
86
|
+
out[d] = swapRB ? data[p + 2] : data[p];
|
|
87
|
+
out[d + 1] = data[p + 1];
|
|
88
|
+
out[d + 2] = swapRB ? data[p] : data[p + 2];
|
|
89
|
+
out[d + 3] = hasAlpha ? data[alphaFirst ? s : s + 3] : 255;
|
|
90
|
+
}
|
|
91
|
+
if (tex.flipY) flipRows(out, width, height);
|
|
92
|
+
return { rgba: out, hasAlpha: hasAlphaChannel(pixelFormat) };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** 主线程 / Worker 共用的解码入口。不抛错:失败与不支持都体现在返回值里 */
|
|
96
|
+
export async function decodeTextureData(tex: S3MTexture): Promise<DecodedTextureData> {
|
|
97
|
+
const empty: DecodedTextureData = {
|
|
98
|
+
kind: 'unsupported(0/0)',
|
|
99
|
+
width: 0,
|
|
100
|
+
height: 0,
|
|
101
|
+
rgba: null,
|
|
102
|
+
bitmap: null,
|
|
103
|
+
hasAlpha: false,
|
|
104
|
+
};
|
|
105
|
+
if (!tex || !tex.width || !tex.height) return empty;
|
|
106
|
+
|
|
107
|
+
const kind = resolveTextureKind(tex.compressType ?? 0, tex.pixelFormat);
|
|
108
|
+
|
|
109
|
+
if (kind === 'crn') {
|
|
110
|
+
try {
|
|
111
|
+
const crn = await transcodeCRN(tex.data);
|
|
112
|
+
const decoded = decodeDXT({
|
|
113
|
+
width: crn.width,
|
|
114
|
+
height: crn.height,
|
|
115
|
+
pixelFormat: crn.dxtFormat === 1 ? PixelFormat.DXT1 : PixelFormat.DXT5,
|
|
116
|
+
data: crn.data,
|
|
117
|
+
});
|
|
118
|
+
return {
|
|
119
|
+
kind: `crn->dxt${crn.dxtFormat}`,
|
|
120
|
+
width: crn.width,
|
|
121
|
+
height: crn.height,
|
|
122
|
+
rgba: decoded.rgba,
|
|
123
|
+
bitmap: null,
|
|
124
|
+
hasAlpha: decoded.hasAlpha,
|
|
125
|
+
};
|
|
126
|
+
} catch (e) {
|
|
127
|
+
// 一张纹理坏了不该带崩整个瓦片,如实上报并降级成无贴图
|
|
128
|
+
return {
|
|
129
|
+
kind: 'crn(failed)',
|
|
130
|
+
width: tex.width,
|
|
131
|
+
height: tex.height,
|
|
132
|
+
rgba: null,
|
|
133
|
+
bitmap: null,
|
|
134
|
+
hasAlpha: false,
|
|
135
|
+
error: String((e as Error | null)?.message ?? e),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (kind === 'dxt1' || kind === 'dxt3' || kind === 'dxt4' || kind === 'dxt5') {
|
|
141
|
+
const decoded = decodeDXT({ ...tex, pixelFormat: dxtPixelFormatOf(kind) });
|
|
142
|
+
return { kind, width: tex.width, height: tex.height, rgba: decoded.rgba, bitmap: null, hasAlpha: decoded.hasAlpha };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (kind === 'webp') {
|
|
146
|
+
const blob = new Blob([tex.data as BlobPart], { type: 'image/webp' });
|
|
147
|
+
const bitmap = await createImageBitmap(blob);
|
|
148
|
+
return { kind, width: bitmap.width, height: bitmap.height, rgba: null, bitmap, hasAlpha: true };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (kind === 'raw') {
|
|
152
|
+
const decoded = rawRGBA(tex);
|
|
153
|
+
return { kind, width: tex.width, height: tex.height, rgba: decoded.rgba, bitmap: null, hasAlpha: decoded.hasAlpha };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// KTX2 / ETC 需要额外解码器,先如实上报而不是静默出错
|
|
157
|
+
return { ...empty, kind: `unsupported(${tex.compressType}/${tex.pixelFormat})` };
|
|
158
|
+
}
|
|
@@ -18,8 +18,29 @@
|
|
|
18
18
|
* 真实服务里的 CRN 纹理会被当成未知格式整包丢掉(模型变成无贴图的灰模)。
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
/**
|
|
22
|
-
|
|
21
|
+
/**
|
|
22
|
+
* S3M 像素 / 块格式枚举(iClient3D S3MPixelFormat)。
|
|
23
|
+
*
|
|
24
|
+
* 原本住在 render/TextureFactory,纹理解码下沉到 core(Worker 复用)后搬到这里;
|
|
25
|
+
* TextureFactory 保留同名 re-export,对外 API 不变。
|
|
26
|
+
*/
|
|
27
|
+
export const PixelFormat = {
|
|
28
|
+
RGB: 10,
|
|
29
|
+
BGR: 11,
|
|
30
|
+
ARGB: 12,
|
|
31
|
+
ABGR: 13,
|
|
32
|
+
BGRA: 14,
|
|
33
|
+
RGBA: 28,
|
|
34
|
+
WEBP: 25,
|
|
35
|
+
DXT1: 17,
|
|
36
|
+
DXT3: 19,
|
|
37
|
+
DXT4: 20,
|
|
38
|
+
DXT5: 21,
|
|
39
|
+
CRN_DXT5: 26,
|
|
40
|
+
STANDARD_CRN: 27,
|
|
41
|
+
} as const;
|
|
42
|
+
|
|
43
|
+
/** S3M 3.01(行标)compressType 取值 */export const TextureCompress = {
|
|
23
44
|
NONE: 0,
|
|
24
45
|
DXT1_RGB: 33776,
|
|
25
46
|
DXT1_RGBA: 33777,
|
|
@@ -31,6 +31,8 @@ export interface S3MDatasetOptions {
|
|
|
31
31
|
align?: DatasetAlignment;
|
|
32
32
|
/** 垂直高度偏移(米),在 align 对齐基准上的额外偏移,正数抬升,负数下降,默认 0 */
|
|
33
33
|
heightOffset?: number;
|
|
34
|
+
/** 是否用 Web Worker 解析经典 .s3mb 瓦片(默认开,崩溃自动降级主线程),见 S3MTilesetOptions.useWorker */
|
|
35
|
+
useWorker?: boolean;
|
|
34
36
|
/**
|
|
35
37
|
* 是否对该数据集生成几何棱线(CPU 端 EdgesGeometry)。
|
|
36
38
|
* - 不填:跟随全局 `style.edges`
|
|
@@ -194,6 +196,7 @@ export class DatasetManager {
|
|
|
194
196
|
maxConcurrent: maxConcurrent,
|
|
195
197
|
lodRangeScale: ds.lodRangeScale ?? defaultLodRangeScale,
|
|
196
198
|
buildTile: buildTile ?? null,
|
|
199
|
+
useWorker: ds.useWorker,
|
|
197
200
|
uniforms: uniforms,
|
|
198
201
|
onTile: (node) => {
|
|
199
202
|
callbacks.onTile(node, entry);
|
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* 2. 专题图分析(按业务字段分段着色、连续插值着色、字段过滤、高度拉伸)
|
|
7
7
|
* 3. 2 行状态纹理(uBatchTex)的烘焙与 GPU 上传:
|
|
8
8
|
* - Row 0: [R, G, B, elevation / hidden]
|
|
9
|
-
* - Row 1: [translationX, translationY, translationZ,
|
|
9
|
+
* - Row 1: [translationX, translationY, translationZ, opacity]
|
|
10
|
+
* 约定:alpha 0 = 无单体透明覆盖(shader 不乘),(0,1) 开区间为实际透明度,
|
|
11
|
+
* opacity <= 0 等价 hidden(走 Row 0 的 -1 隐藏位)
|
|
10
12
|
* 4. 批次冲突审计(auditBatches)
|
|
11
13
|
*/
|
|
12
14
|
import * as THREE from 'three';
|
|
@@ -26,7 +28,7 @@ export interface FeatureStyle {
|
|
|
26
28
|
elevation?: number;
|
|
27
29
|
/** 自定义单体三维平移偏移量 [dx, dy, dz](米) */
|
|
28
30
|
translation?: [number, number, number];
|
|
29
|
-
/** 自定义单体透明度(0~1
|
|
31
|
+
/** 自定义单体透明度(0~1,batch 粒度;<= 0 等价 hidden,1 / 不传 = 不覆盖) */
|
|
30
32
|
opacity?: number;
|
|
31
33
|
/** 是否隐藏该单体 */
|
|
32
34
|
hidden?: boolean;
|
|
@@ -562,6 +564,8 @@ export class FeatureStyleManager {
|
|
|
562
564
|
if (mat.userData.uBatchTexActive) {
|
|
563
565
|
mat.userData.uBatchTexActive.value = 0;
|
|
564
566
|
}
|
|
567
|
+
// 单体淡化清除后,把材质 transparent 还原成全局透明度语义
|
|
568
|
+
if (mat.transparent && mat.opacity >= 0.999) mat.transparent = false;
|
|
565
569
|
return;
|
|
566
570
|
}
|
|
567
571
|
|
|
@@ -569,6 +573,7 @@ export class FeatureStyleManager {
|
|
|
569
573
|
if (mat.userData.uBatchTexActive) {
|
|
570
574
|
mat.userData.uBatchTexActive.value = 0;
|
|
571
575
|
}
|
|
576
|
+
if (mat.transparent && mat.opacity >= 0.999) mat.transparent = false;
|
|
572
577
|
return;
|
|
573
578
|
}
|
|
574
579
|
|
|
@@ -611,6 +616,7 @@ export class FeatureStyleManager {
|
|
|
611
616
|
const colorOfBatch = new Map<number, string>();
|
|
612
617
|
const elevOfBatch = new Map<number, number>();
|
|
613
618
|
const translationOfBatch = new Map<number, [number, number, number]>();
|
|
619
|
+
const opacityOfBatch = new Map<number, number>();
|
|
614
620
|
const ownersOfBatch = new Map<number, number>();
|
|
615
621
|
|
|
616
622
|
for (const [smidStr, entry] of Object.entries(pickInfo)) {
|
|
@@ -661,6 +667,16 @@ export class FeatureStyleManager {
|
|
|
661
667
|
continue;
|
|
662
668
|
}
|
|
663
669
|
|
|
670
|
+
// 单体独立透明度:<= 0 等价 hidden;(0,1) 写入状态纹理第 2 行 alpha;>= 1 不覆盖
|
|
671
|
+
const fade = st?.opacity;
|
|
672
|
+
if (fade != null) {
|
|
673
|
+
if (fade <= 0) {
|
|
674
|
+
hiddenBatches.add(bId);
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
if (fade < 1 && !opacityOfBatch.has(bId)) opacityOfBatch.set(bId, fade);
|
|
678
|
+
}
|
|
679
|
+
|
|
664
680
|
const col = st?.color ?? this.thematicColors.get(smid);
|
|
665
681
|
if (col && !colorOfBatch.has(bId)) colorOfBatch.set(bId, col);
|
|
666
682
|
|
|
@@ -704,6 +720,19 @@ export class FeatureStyleManager {
|
|
|
704
720
|
hasActive = true;
|
|
705
721
|
}
|
|
706
722
|
|
|
723
|
+
// 单体独立透明度写在第 2 行 alpha:shader 里 0 = 无覆盖,不会误伤未设置的单体
|
|
724
|
+
let hasFade = false;
|
|
725
|
+
for (const [bId, fade] of opacityOfBatch) {
|
|
726
|
+
if (hiddenBatches.has(bId)) continue;
|
|
727
|
+
data[(w + bId) * 4 + 3] = fade;
|
|
728
|
+
hasActive = true;
|
|
729
|
+
hasFade = true;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// 有淡化单体时整块材质要开混合才有透明效果;清除后还原为全局透明度语义
|
|
733
|
+
if (hasFade && !mat.transparent) mat.transparent = true;
|
|
734
|
+
else if (!hasFade && mat.transparent && mat.opacity >= 0.999) mat.transparent = false;
|
|
735
|
+
|
|
707
736
|
let conflicts = 0;
|
|
708
737
|
for (const [bId, owners] of ownersOfBatch) {
|
|
709
738
|
if (owners < 2) continue;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 轻量版顶点焊合(只按位置)
|
|
3
|
+
*
|
|
4
|
+
* 为什么不用 three/examples/jsm/utils/BufferGeometryUtils.js 的 mergeVertices:
|
|
5
|
+
* 1. jsm 子路径导入会在宿主打包器里给 three 制造第二个依赖入口,
|
|
6
|
+
* 典型症状就是控制台「WARNING: Multiple instances of Three.js being imported.」
|
|
7
|
+
* ——同一页面出现两份 three,instanceof 全部失灵。库端唯一治本的办法是
|
|
8
|
+
* 不引入 jsm 子路径,把 import 面收敛到 `three` 一个入口。
|
|
9
|
+
* 2. 对「提棱线」这个用途,位置焊合反而是更对的行为:完整版 mergeVertices
|
|
10
|
+
* 要求所有属性都在容差内匹配,而 S3M 几何常是「每面独立顶点 + 各自 UV」,
|
|
11
|
+
* 重合顶点会因 UV 不同而焊不上,EdgesGeometry 照样把每条三角形边当边界。
|
|
12
|
+
*
|
|
13
|
+
* 输出只保留 position(EdgesGeometry 只消费位置),并把原索引重映射到焊后顶点。
|
|
14
|
+
*/
|
|
15
|
+
import * as THREE from 'three';
|
|
16
|
+
|
|
17
|
+
export function mergeVerticesByPosition(
|
|
18
|
+
geometry: THREE.BufferGeometry,
|
|
19
|
+
tolerance: number
|
|
20
|
+
): THREE.BufferGeometry {
|
|
21
|
+
const pos = geometry.getAttribute('position') as THREE.BufferAttribute | undefined;
|
|
22
|
+
if (!pos || pos.count === 0) return geometry;
|
|
23
|
+
|
|
24
|
+
const inv = 1 / Math.max(tolerance, 1e-9);
|
|
25
|
+
/** 网格哈希:坐标量化到 tolerance 网格,同格即视为重合 */
|
|
26
|
+
const map = new Map<string, number>();
|
|
27
|
+
const out: number[] = [];
|
|
28
|
+
const remap = new Int32Array(pos.count);
|
|
29
|
+
|
|
30
|
+
for (let i = 0; i < pos.count; i++) {
|
|
31
|
+
const x = pos.getX(i);
|
|
32
|
+
const y = pos.getY(i);
|
|
33
|
+
const z = pos.getZ(i);
|
|
34
|
+
const key = `${Math.round(x * inv)},${Math.round(y * inv)},${Math.round(z * inv)}`;
|
|
35
|
+
let id = map.get(key);
|
|
36
|
+
if (id === undefined) {
|
|
37
|
+
id = out.length / 3;
|
|
38
|
+
out.push(x, y, z);
|
|
39
|
+
map.set(key, id);
|
|
40
|
+
}
|
|
41
|
+
remap[i] = id;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 原几何可能带索引:按原三角形拓扑重映射,不能按顶点顺序重造三角形
|
|
45
|
+
const srcIndex = geometry.getIndex();
|
|
46
|
+
const triCount = srcIndex ? srcIndex.count : pos.count;
|
|
47
|
+
const outIndex = new Uint32Array(triCount);
|
|
48
|
+
for (let i = 0; i < triCount; i++) {
|
|
49
|
+
outIndex[i] = remap[srcIndex ? srcIndex.getX(i) : i]!;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const welded = new THREE.BufferGeometry();
|
|
53
|
+
welded.setAttribute('position', new THREE.Float32BufferAttribute(out, 3));
|
|
54
|
+
welded.setIndex(new THREE.BufferAttribute(outIndex, 1));
|
|
55
|
+
return welded;
|
|
56
|
+
}
|
package/src/render/S3MViewer.ts
CHANGED
|
@@ -21,10 +21,10 @@
|
|
|
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';
|
|
25
24
|
import { S3MTileset, usableGeoBounds } from '../tileset/S3MTileset.js';
|
|
26
25
|
import type { BuildTileFn, TileNode } from '../tileset/S3MTileset.js';
|
|
27
|
-
import { setMaxAnisotropy } from './TextureFactory.js';
|
|
26
|
+
import { setMaxAnisotropy, setTextureOffThreadDecode } from './TextureFactory.js';
|
|
27
|
+
import { decodeTextureWithWorker } from '../worker/S3MWorkerClient.js';
|
|
28
28
|
import {
|
|
29
29
|
STYLE,
|
|
30
30
|
STYLE_LABELS,
|
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
createThickLineMaterial,
|
|
44
44
|
} from './EdgeLines.js';
|
|
45
45
|
import type { LineSharedUniforms, ThickLineMaterial } from './EdgeLines.js';
|
|
46
|
+
import { mergeVerticesByPosition } from './MergeVertices.js';
|
|
46
47
|
import {
|
|
47
48
|
resolveFeature,
|
|
48
49
|
measureSegments,
|
|
@@ -193,6 +194,8 @@ export interface S3MViewerOptions {
|
|
|
193
194
|
maxTiles?: number;
|
|
194
195
|
/** 每个数据集并发请求数 */
|
|
195
196
|
maxConcurrent?: number;
|
|
197
|
+
/** 是否用 Web Worker 解析经典 .s3mb 瓦片(默认开,崩溃自动降级主线程),见 S3MTilesetOptions.useWorker */
|
|
198
|
+
useWorker?: boolean;
|
|
196
199
|
/** LOD 切换范围缩放系数(默认 1.0),> 1 更倾向保留粗模,< 1 提前换细模 */
|
|
197
200
|
lodRangeScale?: number;
|
|
198
201
|
/** 自定义瓦片构建器(测试 / 服务端渲染时注入) */
|
|
@@ -401,6 +404,8 @@ export class S3MViewer {
|
|
|
401
404
|
);
|
|
402
405
|
|
|
403
406
|
this.uniforms = createSharedUniforms();
|
|
407
|
+
// 纹理解码优先派给共享解析 Worker(崩溃自动回主线程,见 decodeTextureWithWorker)
|
|
408
|
+
setTextureOffThreadDecode(decodeTextureWithWorker);
|
|
404
409
|
this.style = { ...DEFAULT_STYLE, ...(options.style ?? {}) };
|
|
405
410
|
this.outlineMaterial = createOutlineMaterial(this.uniforms);
|
|
406
411
|
|
|
@@ -604,7 +609,8 @@ export class S3MViewer {
|
|
|
604
609
|
|
|
605
610
|
private _normalizeDatasets(options: S3MViewerOptions): S3MDatasetOptions[] {
|
|
606
611
|
if (Array.isArray(options.datasets)) return options.datasets.filter((d) => !!d?.configUrl);
|
|
607
|
-
if (options.configUrl)
|
|
612
|
+
if (options.configUrl)
|
|
613
|
+
return [{ id: 'default', name: '默认数据集', configUrl: options.configUrl, useWorker: options.useWorker }];
|
|
608
614
|
return [];
|
|
609
615
|
}
|
|
610
616
|
|
|
@@ -1544,8 +1550,10 @@ export class S3MViewer {
|
|
|
1544
1550
|
|
|
1545
1551
|
// 1. 先做顶点容差焊合,让相邻三角形正确共享边索引。
|
|
1546
1552
|
// S3M 几何常为「每面独立顶点」,不焊合的话 EdgesGeometry 会把每条三角形边都当成边界,棱线爆炸。
|
|
1553
|
+
// 注意用本地实现而不是 three/examples/jsm —— jsm 子路径导入会在宿主里
|
|
1554
|
+
// 制造第二份 three 实例("Multiple instances of Three.js" 警告的元凶)。
|
|
1547
1555
|
// tolerance 取米级模型下的 5mm,足以合并重合顶点又不会误并真实相邻面。
|
|
1548
|
-
const welded =
|
|
1556
|
+
const welded = mergeVerticesByPosition(mesh.geometry, 0.005);
|
|
1549
1557
|
// 2. 调大临界角(默认 55°,可在 style.edgeThreshold 调),过滤浅坡与曲面细分,只保留真正的棱。
|
|
1550
1558
|
const thresholdAngle = this.style.edgeThreshold ?? 55;
|
|
1551
1559
|
const source = new THREE.EdgesGeometry(welded, thresholdAngle);
|
|
@@ -499,6 +499,12 @@ const FRAG_COLOR = /* glsl */ `
|
|
|
499
499
|
diffuseColor.rgb = bState.rgb;
|
|
500
500
|
hasCustomColor = true;
|
|
501
501
|
}
|
|
502
|
+
// 单体独立透明度(状态纹理第 2 行 alpha,0 = 无覆盖)。
|
|
503
|
+
// 与全局 uOpacity(diffuseColor.a 初值)和图集贴图自带 alpha 逐级叠乘
|
|
504
|
+
vec4 bOffset = texture2D( uBatchTex, vec2( ( s3mBid + 0.5 ) / uBatchTexWidth, 0.75 ) );
|
|
505
|
+
if ( bOffset.a > 0.0 ) {
|
|
506
|
+
diffuseColor.a *= bOffset.a;
|
|
507
|
+
}
|
|
502
508
|
}
|
|
503
509
|
|
|
504
510
|
// 3. 选中单体高亮 + 其余压暗
|
|
@@ -1,32 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* S3M 纹理 → three.js Texture
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* 解码本体已下沉到 core/TextureDecode.ts(three 无关,主线程 / Worker 共用),
|
|
5
|
+
* 本文件只负责两件事:
|
|
6
|
+
* 1. 优先把解码派给 Worker(setTextureOffThreadDecode 注入,S3MViewer 装配),
|
|
7
|
+
* Worker 不可用时用同一份 decodeTextureData 在主线程兜底;
|
|
8
|
+
* 2. 把纯数据解码产物包成 three Texture 并按 atlas / 普通 分别配置采样参数。
|
|
8
9
|
*/
|
|
9
10
|
import * as THREE from 'three';
|
|
10
|
-
import DXTTextureDecode from '../core/DXTTextureDecode.js';
|
|
11
|
-
import { transcodeCRN } from '../core/CRNDecode.js';
|
|
12
|
-
import { dxtPixelFormatOf, resolveTextureKind } from '../core/TextureFormat.js';
|
|
13
11
|
import type { S3MTexture } from '../core/types.js';
|
|
12
|
+
import { decodeTextureData } from '../core/TextureDecode.js';
|
|
13
|
+
import type { DecodedTextureData } from '../core/TextureDecode.js';
|
|
14
14
|
|
|
15
|
-
export
|
|
16
|
-
RGB: 10,
|
|
17
|
-
BGR: 11,
|
|
18
|
-
ARGB: 12,
|
|
19
|
-
ABGR: 13,
|
|
20
|
-
BGRA: 14,
|
|
21
|
-
RGBA: 28,
|
|
22
|
-
WEBP: 25,
|
|
23
|
-
DXT1: 17,
|
|
24
|
-
DXT3: 19,
|
|
25
|
-
DXT4: 20,
|
|
26
|
-
DXT5: 21,
|
|
27
|
-
CRN_DXT5: 26,
|
|
28
|
-
STANDARD_CRN: 27,
|
|
29
|
-
} as const;
|
|
15
|
+
export { PixelFormat } from '../core/TextureFormat.js';
|
|
30
16
|
|
|
31
17
|
/** 由 S3MLayer 在拿到 GL 上下文后注入,用于斜视角下保持纹理清晰 */
|
|
32
18
|
let maxAnisotropy = 1;
|
|
@@ -41,6 +27,26 @@ export interface DecodedTexture {
|
|
|
41
27
|
decoded: string;
|
|
42
28
|
}
|
|
43
29
|
|
|
30
|
+
/**
|
|
31
|
+
* 注入「离线解码」实现(S3MViewer 装配:走共享解析 Worker)。
|
|
32
|
+
* 返回 null = Worker 不可用 / 崩溃,createTexture 自动退回主线程 decodeTextureData。
|
|
33
|
+
*/
|
|
34
|
+
let offThreadDecode: ((tex: S3MTexture) => Promise<DecodedTextureData | null>) | null = null;
|
|
35
|
+
|
|
36
|
+
export function setTextureOffThreadDecode(
|
|
37
|
+
fn: ((tex: S3MTexture) => Promise<DecodedTextureData | null>) | null
|
|
38
|
+
): void {
|
|
39
|
+
offThreadDecode = fn;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function decodeTexture(tex: S3MTexture): Promise<DecodedTextureData> {
|
|
43
|
+
if (offThreadDecode) {
|
|
44
|
+
const viaWorker = await offThreadDecode(tex);
|
|
45
|
+
if (viaWorker) return viaWorker;
|
|
46
|
+
}
|
|
47
|
+
return decodeTextureData(tex);
|
|
48
|
+
}
|
|
49
|
+
|
|
44
50
|
function configure<T extends THREE.Texture>(tex: T, srgb = true): T {
|
|
45
51
|
tex.wrapS = THREE.RepeatWrapping;
|
|
46
52
|
tex.wrapT = THREE.RepeatWrapping;
|
|
@@ -74,117 +80,32 @@ function configureAtlas<T extends THREE.Texture>(tex: T): T {
|
|
|
74
80
|
return tex;
|
|
75
81
|
}
|
|
76
82
|
|
|
77
|
-
/** 行序翻转(原地)。S3MBLOCK 图集按 GL 方向存储(v=0 在最后一行),解码后要翻一次行 */
|
|
78
|
-
function flipRows(rgba: Uint8Array, width: number, height: number): void {
|
|
79
|
-
const row = width * 4;
|
|
80
|
-
const tmp = new Uint8Array(row);
|
|
81
|
-
for (let y = 0; y < height >> 1; y++) {
|
|
82
|
-
const a = y * row;
|
|
83
|
-
const b = (height - 1 - y) * row;
|
|
84
|
-
tmp.set(rgba.subarray(a, a + row));
|
|
85
|
-
rgba.copyWithin(a, b, b + row);
|
|
86
|
-
rgba.set(tmp, b);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** 该像素格式自带 alpha 通道(DXT1 的 1bit alpha 不算,历史上没开过 alphaTest) */
|
|
91
|
-
function hasAlphaChannel(pixelFormat: number): boolean {
|
|
92
|
-
return (
|
|
93
|
-
pixelFormat === PixelFormat.DXT3 ||
|
|
94
|
-
pixelFormat === PixelFormat.DXT4 ||
|
|
95
|
-
pixelFormat === PixelFormat.DXT5 ||
|
|
96
|
-
pixelFormat === PixelFormat.ARGB ||
|
|
97
|
-
pixelFormat === PixelFormat.ABGR ||
|
|
98
|
-
pixelFormat === PixelFormat.BGRA ||
|
|
99
|
-
pixelFormat === PixelFormat.RGBA
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/** 解 DXT 到 RGBA8。DXT5 是 1 字节/像素,DXT1 是 0.5 字节/像素。 */
|
|
104
|
-
/** DXT 解码只需要尺寸 / 格式 / 字节,CRN 转码出来的结果也走这条路 */
|
|
105
|
-
function decodeDXT(tex: Pick<S3MTexture, 'width' | 'height' | 'pixelFormat' | 'data' | 'flipY'>): THREE.DataTexture {
|
|
106
|
-
const { width, height, pixelFormat, data } = tex;
|
|
107
|
-
const out = new Uint8Array(width * height * 4);
|
|
108
|
-
DXTTextureDecode.decode(out, width, height, data, pixelFormat);
|
|
109
|
-
if (tex.flipY) flipRows(out, width, height);
|
|
110
|
-
const dt = new THREE.DataTexture(out, width, height, THREE.RGBAFormat, THREE.UnsignedByteType);
|
|
111
|
-
if (hasAlphaChannel(pixelFormat)) dt.userData.s3mHasAlpha = true;
|
|
112
|
-
return dt;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function fromRawRGB(tex: S3MTexture): THREE.DataTexture {
|
|
116
|
-
const { width, height, pixelFormat, data } = tex;
|
|
117
|
-
const out = new Uint8Array(width * height * 4);
|
|
118
|
-
// RGBA / BGRA / ARGB / ABGR 都是 4 字节,前两个是「尾字节是 alpha」,后两个是「首字节是 alpha」
|
|
119
|
-
const hasAlpha =
|
|
120
|
-
pixelFormat === PixelFormat.RGBA ||
|
|
121
|
-
pixelFormat === PixelFormat.BGRA ||
|
|
122
|
-
pixelFormat === PixelFormat.ARGB ||
|
|
123
|
-
pixelFormat === PixelFormat.ABGR;
|
|
124
|
-
const swapRB = pixelFormat === PixelFormat.BGR || pixelFormat === PixelFormat.BGRA;
|
|
125
|
-
const alphaFirst = pixelFormat === PixelFormat.ARGB || pixelFormat === PixelFormat.ABGR;
|
|
126
|
-
const src = hasAlpha ? 4 : 3;
|
|
127
|
-
for (let i = 0, s = 0, d = 0; i < width * height; i++, s += src, d += 4) {
|
|
128
|
-
const p = alphaFirst ? s + 1 : s;
|
|
129
|
-
out[d] = swapRB ? data[p + 2] : data[p];
|
|
130
|
-
out[d + 1] = data[p + 1];
|
|
131
|
-
out[d + 2] = swapRB ? data[p] : data[p + 2];
|
|
132
|
-
out[d + 3] = hasAlpha ? data[alphaFirst ? s : s + 3] : 255;
|
|
133
|
-
}
|
|
134
|
-
if (tex.flipY) flipRows(out, width, height);
|
|
135
|
-
const dt = new THREE.DataTexture(out, width, height, THREE.RGBAFormat, THREE.UnsignedByteType);
|
|
136
|
-
if (hasAlphaChannel(pixelFormat)) dt.userData.s3mHasAlpha = true;
|
|
137
|
-
return dt;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
83
|
export async function createTexture(tex: S3MTexture | null | undefined): Promise<DecodedTexture> {
|
|
141
84
|
if (!tex || !tex.width || !tex.height) return { texture: null, decoded: 'empty' };
|
|
142
85
|
|
|
143
86
|
// 图集纹理(flipY 的原始 DXT 图集 / AtlasPack 拼出来的 RGBA 大图 / leaf 组合画布)都必须
|
|
144
87
|
// 关 GPU mip,层级由 StyleMaterial.s3mAtlasUnit 的内联金字塔自己算。
|
|
145
88
|
const isAtlas = tex.packed === true || tex.flipY === true || tex.composed === true;
|
|
146
|
-
const asAtlas = <T extends THREE.Texture>(t: T): T => (isAtlas ? configureAtlas(t) : configure(t));
|
|
147
89
|
|
|
148
|
-
|
|
149
|
-
// 行标的 CRN 是 32856(GL_RGBA8),都不是 26 / 27(见 core/TextureFormat.ts)
|
|
150
|
-
const kind = resolveTextureKind(tex.compressType ?? 0, tex.pixelFormat);
|
|
90
|
+
const data = await decodeTexture(tex);
|
|
151
91
|
|
|
152
|
-
if (
|
|
153
|
-
|
|
154
|
-
const crn = await transcodeCRN(tex.data);
|
|
155
|
-
const decoded = decodeDXT({
|
|
156
|
-
width: crn.width,
|
|
157
|
-
height: crn.height,
|
|
158
|
-
pixelFormat: crn.dxtFormat === 1 ? 17 : 21,
|
|
159
|
-
data: crn.data,
|
|
160
|
-
});
|
|
161
|
-
return { texture: asAtlas(decoded), decoded: `crn->dxt${crn.dxtFormat}` };
|
|
162
|
-
} catch (e) {
|
|
92
|
+
if (!data.rgba && !data.bitmap) {
|
|
93
|
+
if (data.error) {
|
|
163
94
|
// 一张纹理坏了不该带崩整个瓦片,如实上报并降级成无贴图
|
|
164
|
-
console.warn('[s3m] CRN 转码失败:',
|
|
165
|
-
return { texture: null, decoded: 'crn(failed)' };
|
|
95
|
+
console.warn('[s3m] CRN 转码失败:', data.error);
|
|
166
96
|
}
|
|
97
|
+
return { texture: null, decoded: data.kind };
|
|
167
98
|
}
|
|
168
99
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
100
|
+
let texture: THREE.Texture;
|
|
101
|
+
if (data.bitmap) {
|
|
102
|
+
texture = new THREE.Texture(data.bitmap);
|
|
103
|
+
} else {
|
|
104
|
+
const dt = new THREE.DataTexture(data.rgba!, data.width, data.height, THREE.RGBAFormat, THREE.UnsignedByteType);
|
|
105
|
+
if (data.hasAlpha) dt.userData.s3mHasAlpha = true;
|
|
106
|
+
texture = dt;
|
|
173
107
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const bmp = await createImageBitmap(blob);
|
|
178
|
-
const t = new THREE.Texture(bmp);
|
|
179
|
-
t.needsUpdate = true;
|
|
180
|
-
return { texture: asAtlas(t), decoded: 'webp' };
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (kind === 'raw') {
|
|
184
|
-
const decoded = fromRawRGB(tex);
|
|
185
|
-
return { texture: asAtlas(decoded), decoded: 'raw' };
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// KTX2 / ETC 需要额外解码器,先如实上报而不是静默出错
|
|
189
|
-
return { texture: null, decoded: `unsupported(${tex.compressType}/${tex.pixelFormat})` };
|
|
108
|
+
texture.needsUpdate = true;
|
|
109
|
+
// flipY 为真即 S3MBLOCK 图集(见 core/types.ts),必须无 mip
|
|
110
|
+
return { texture: isAtlas ? configureAtlas(texture) : configure(texture), decoded: data.kind };
|
|
190
111
|
}
|