s3m-three 0.3.5 → 0.3.7
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-D7AV6O30.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 +39 -0
- package/dist/core/TextureDecode.d.ts.map +1 -0
- package/dist/core/TextureDecode.js +174 -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/core/TileGeometry.d.ts +92 -0
- package/dist/core/TileGeometry.d.ts.map +1 -0
- package/dist/core/TileGeometry.js +607 -0
- package/dist/core/TileGeometry.js.map +1 -0
- 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/GeometryBuilder.d.ts +8 -10
- package/dist/render/GeometryBuilder.d.ts.map +1 -1
- package/dist/render/GeometryBuilder.js +77 -555
- package/dist/render/GeometryBuilder.js.map +1 -1
- package/dist/render/S3MViewer.d.ts +4 -0
- package/dist/render/S3MViewer.d.ts.map +1 -1
- package/dist/render/S3MViewer.js +13 -2
- 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 +9 -19
- package/dist/render/TextureFactory.d.ts.map +1 -1
- package/dist/render/TextureFactory.js +62 -115
- package/dist/render/TextureFactory.js.map +1 -1
- package/dist/tileset/S3MTileset.d.ts +21 -0
- package/dist/tileset/S3MTileset.d.ts.map +1 -1
- package/dist/tileset/S3MTileset.js +41 -2
- package/dist/tileset/S3MTileset.js.map +1 -1
- package/dist/worker/S3MWorkerClient.d.ts +48 -0
- package/dist/worker/S3MWorkerClient.d.ts.map +1 -0
- package/dist/worker/S3MWorkerClient.js +124 -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 +56 -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 +6 -0
- package/package.json +1 -1
- package/src/core/TextureDecode.ts +224 -0
- package/src/core/TextureFormat.ts +23 -2
- package/src/core/TileGeometry.ts +745 -0
- package/src/render/DatasetManager.ts +3 -0
- package/src/render/FeatureStyleManager.ts +31 -2
- package/src/render/GeometryBuilder.ts +91 -617
- package/src/render/S3MViewer.ts +18 -4
- package/src/render/StyleMaterial.ts +6 -0
- package/src/render/TextureFactory.ts +78 -123
- package/src/tileset/S3MTileset.ts +55 -3
- package/src/worker/S3MWorkerClient.ts +159 -0
- package/src/worker/s3mWorker.ts +76 -0
- package/src/worker/transferables.ts +58 -0
package/src/render/S3MViewer.ts
CHANGED
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
import * as THREE from 'three';
|
|
24
24
|
import { S3MTileset, usableGeoBounds } from '../tileset/S3MTileset.js';
|
|
25
25
|
import type { BuildTileFn, TileNode } from '../tileset/S3MTileset.js';
|
|
26
|
-
import { setMaxAnisotropy } from './TextureFactory.js';
|
|
26
|
+
import { setMaxAnisotropy, setCompressedTexturesSupported, setTextureOffThreadDecode } from './TextureFactory.js';
|
|
27
|
+
import { decodeTextureWithWorker } from '../worker/S3MWorkerClient.js';
|
|
27
28
|
import {
|
|
28
29
|
STYLE,
|
|
29
30
|
STYLE_LABELS,
|
|
@@ -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
|
|
|
@@ -433,8 +438,7 @@ export class S3MViewer {
|
|
|
433
438
|
* MapLibre 宿主由 S3MLayer 在 onAdd 里调用;纯 three 宿主直接传自己的 canvas / gl。
|
|
434
439
|
*/
|
|
435
440
|
/** 创建并配置一个绑定到给定 gl 上下文的 three renderer(mount 与上下文恢复共用) */
|
|
436
|
-
private _createRenderer(canvas: HTMLCanvasElement, gl: WebGLRenderingContext | WebGL2RenderingContext): THREE.WebGLRenderer {
|
|
437
|
-
const r = new THREE.WebGLRenderer({
|
|
441
|
+
private _createRenderer(canvas: HTMLCanvasElement, gl: WebGLRenderingContext | WebGL2RenderingContext): THREE.WebGLRenderer { const r = new THREE.WebGLRenderer({
|
|
438
442
|
canvas,
|
|
439
443
|
context: gl as WebGLRenderingContext,
|
|
440
444
|
antialias: true,
|
|
@@ -447,11 +451,19 @@ export class S3MViewer {
|
|
|
447
451
|
return r;
|
|
448
452
|
}
|
|
449
453
|
|
|
454
|
+
/** 按 GL 能力开/关 DXT 压缩直传:两个 s3tc 扩展都要在(srgb 缺了贴图会发白失配) */
|
|
455
|
+
private _syncTextureCapabilities(gl: WebGLRenderingContext | WebGL2RenderingContext): void {
|
|
456
|
+
const s3tc = !!gl.getExtension('WEBGL_compressed_texture_s3tc');
|
|
457
|
+
const s3tcSrgb = !!gl.getExtension('WEBGL_compressed_texture_s3tc_srgb');
|
|
458
|
+
setCompressedTexturesSupported(s3tc && s3tcSrgb);
|
|
459
|
+
}
|
|
460
|
+
|
|
450
461
|
mount(canvas: HTMLCanvasElement, gl: WebGLRenderingContext | WebGL2RenderingContext): void {
|
|
451
462
|
this.canvas = canvas;
|
|
452
463
|
|
|
453
464
|
this.renderer = this._createRenderer(canvas, gl);
|
|
454
465
|
setMaxAnisotropy(this.renderer.capabilities.getMaxAnisotropy());
|
|
466
|
+
this._syncTextureCapabilities(gl);
|
|
455
467
|
|
|
456
468
|
this.scene = new THREE.Scene();
|
|
457
469
|
this.scene.add(this.groundManager.group);
|
|
@@ -493,6 +505,7 @@ export class S3MViewer {
|
|
|
493
505
|
}
|
|
494
506
|
this.renderer = this._createRenderer(this.canvas, gl);
|
|
495
507
|
setMaxAnisotropy(this.renderer.capabilities.getMaxAnisotropy());
|
|
508
|
+
this._syncTextureCapabilities(gl);
|
|
496
509
|
this.picker?.dispose();
|
|
497
510
|
this.picker = new Picker(this.renderer);
|
|
498
511
|
// 共享 uniform / 材质引用不变,three 重新编译着色器时会自然接管;
|
|
@@ -604,7 +617,8 @@ export class S3MViewer {
|
|
|
604
617
|
|
|
605
618
|
private _normalizeDatasets(options: S3MViewerOptions): S3MDatasetOptions[] {
|
|
606
619
|
if (Array.isArray(options.datasets)) return options.datasets.filter((d) => !!d?.configUrl);
|
|
607
|
-
if (options.configUrl)
|
|
620
|
+
if (options.configUrl)
|
|
621
|
+
return [{ id: 'default', name: '默认数据集', configUrl: options.configUrl, useWorker: options.useWorker }];
|
|
608
622
|
return [];
|
|
609
623
|
}
|
|
610
624
|
|
|
@@ -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;
|
|
@@ -35,12 +21,46 @@ export function setMaxAnisotropy(n: number): void {
|
|
|
35
21
|
maxAnisotropy = Math.max(1, n | 0);
|
|
36
22
|
}
|
|
37
23
|
|
|
24
|
+
/**
|
|
25
|
+
* DXT 压缩直传开关(S3MViewer 在 mount / 上下文恢复时按 GL 能力注入):
|
|
26
|
+
* 需要 WEBGL_compressed_texture_s3tc 与 s3tc_srgb 两个扩展同时可用 ——
|
|
27
|
+
* 纹理是 SRGBColorSpace,缺 srgb 变体会出现贴图发白的颜色失配。
|
|
28
|
+
* 移动端大多没有 s3tc,自动回退 Worker 解码成 RGBA 上传,行为同旧版。
|
|
29
|
+
*/
|
|
30
|
+
let compressedTexturesSupported = false;
|
|
31
|
+
|
|
32
|
+
export function setCompressedTexturesSupported(v: boolean): void {
|
|
33
|
+
compressedTexturesSupported = v;
|
|
34
|
+
}
|
|
35
|
+
|
|
38
36
|
export interface DecodedTexture {
|
|
39
37
|
texture: THREE.Texture | null;
|
|
40
38
|
/** 实际走的解码路径,`unsupported(26)` 这类值会进 stats.skipped */
|
|
41
39
|
decoded: string;
|
|
42
40
|
}
|
|
43
41
|
|
|
42
|
+
/**
|
|
43
|
+
* 注入「离线解码」实现(S3MViewer 装配:走共享解析 Worker)。
|
|
44
|
+
* 返回 null = Worker 不可用 / 崩溃,createTexture 自动退回主线程 decodeTextureData。
|
|
45
|
+
*/
|
|
46
|
+
let offThreadDecode:
|
|
47
|
+
| ((tex: S3MTexture, compressed: boolean) => Promise<DecodedTextureData | null>)
|
|
48
|
+
| null = null;
|
|
49
|
+
|
|
50
|
+
export function setTextureOffThreadDecode(
|
|
51
|
+
fn: ((tex: S3MTexture, compressed: boolean) => Promise<DecodedTextureData | null>) | null
|
|
52
|
+
): void {
|
|
53
|
+
offThreadDecode = fn;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function decodeTexture(tex: S3MTexture, compressed: boolean): Promise<DecodedTextureData> {
|
|
57
|
+
if (offThreadDecode) {
|
|
58
|
+
const viaWorker = await offThreadDecode(tex, compressed);
|
|
59
|
+
if (viaWorker) return viaWorker;
|
|
60
|
+
}
|
|
61
|
+
return decodeTextureData(tex, { compressed });
|
|
62
|
+
}
|
|
63
|
+
|
|
44
64
|
function configure<T extends THREE.Texture>(tex: T, srgb = true): T {
|
|
45
65
|
tex.wrapS = THREE.RepeatWrapping;
|
|
46
66
|
tex.wrapT = THREE.RepeatWrapping;
|
|
@@ -74,117 +94,52 @@ function configureAtlas<T extends THREE.Texture>(tex: T): T {
|
|
|
74
94
|
return tex;
|
|
75
95
|
}
|
|
76
96
|
|
|
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
97
|
export async function createTexture(tex: S3MTexture | null | undefined): Promise<DecodedTexture> {
|
|
141
98
|
if (!tex || !tex.width || !tex.height) return { texture: null, decoded: 'empty' };
|
|
142
99
|
|
|
143
100
|
// 图集纹理(flipY 的原始 DXT 图集 / AtlasPack 拼出来的 RGBA 大图 / leaf 组合画布)都必须
|
|
144
101
|
// 关 GPU mip,层级由 StyleMaterial.s3mAtlasUnit 的内联金字塔自己算。
|
|
145
102
|
const isAtlas = tex.packed === true || tex.flipY === true || tex.composed === true;
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
try {
|
|
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) {
|
|
103
|
+
|
|
104
|
+
// 压缩直传只在「宿主 GL 支持 + 非图集」时开:图集要 flipY / 无 mip,压缩上传做不了翻行
|
|
105
|
+
const wantCompressed = compressedTexturesSupported && !isAtlas;
|
|
106
|
+
const data = await decodeTexture(tex, wantCompressed);
|
|
107
|
+
|
|
108
|
+
if (!data.rgba && !data.blocks && !data.bitmap) {
|
|
109
|
+
if (data.error) {
|
|
163
110
|
// 一张纹理坏了不该带崩整个瓦片,如实上报并降级成无贴图
|
|
164
|
-
console.warn('[s3m] CRN 转码失败:',
|
|
165
|
-
return { texture: null, decoded: 'crn(failed)' };
|
|
111
|
+
console.warn('[s3m] CRN 转码失败:', data.error);
|
|
166
112
|
}
|
|
113
|
+
return { texture: null, decoded: data.kind };
|
|
167
114
|
}
|
|
168
115
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
116
|
+
let texture: THREE.Texture;
|
|
117
|
+
if (data.bitmap) {
|
|
118
|
+
texture = new THREE.Texture(data.bitmap);
|
|
119
|
+
} else if (data.blocks && data.blockFormat) {
|
|
120
|
+
// DXT 块直传:GPU 硬件解压,省 4~8 倍上传带宽与显存。压缩纹理不能 generateMipmaps
|
|
121
|
+
// (WebGL 限制),远处靠 LinearFilter 兜底;s3mHasAlpha 走 alphaTest 的 DXT1 1bit
|
|
122
|
+
// alpha 历史上没开过,DXT5 的 hasAlpha 由 decodeTextureData 判定
|
|
123
|
+
const format =
|
|
124
|
+
data.blockFormat === 'dxt1' ? THREE.RGB_S3TC_DXT1_Format : THREE.RGBA_S3TC_DXT5_Format;
|
|
125
|
+
texture = new THREE.CompressedTexture(
|
|
126
|
+
[{ data: data.blocks, width: data.width, height: data.height }],
|
|
127
|
+
data.width,
|
|
128
|
+
data.height,
|
|
129
|
+
format,
|
|
130
|
+
THREE.UnsignedByteType
|
|
131
|
+
);
|
|
132
|
+
} else {
|
|
133
|
+
const dt = new THREE.DataTexture(data.rgba!, data.width, data.height, THREE.RGBAFormat, THREE.UnsignedByteType);
|
|
134
|
+
if (data.hasAlpha) dt.userData.s3mHasAlpha = true;
|
|
135
|
+
texture = dt;
|
|
173
136
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
t.needsUpdate = true;
|
|
180
|
-
return { texture: asAtlas(t), decoded: 'webp' };
|
|
137
|
+
texture.needsUpdate = true;
|
|
138
|
+
const configured = isAtlas ? configureAtlas(texture) : configure(texture);
|
|
139
|
+
if (data.blocks) {
|
|
140
|
+
configured.generateMipmaps = false;
|
|
141
|
+
configured.minFilter = THREE.LinearFilter;
|
|
181
142
|
}
|
|
182
|
-
|
|
183
|
-
|
|
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})` };
|
|
143
|
+
// flipY 为真即 S3MBLOCK 图集(见 core/types.ts),必须无 mip
|
|
144
|
+
return { texture: configured, decoded: data.kind };
|
|
190
145
|
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* 命中阈值就请求 childTile,实现渐进式细化。
|
|
8
8
|
*/
|
|
9
9
|
import { parseS3MB } from '../core/S3MBParser.js';
|
|
10
|
+
import { getSharedS3MWorker, S3MWorkerClient, WorkerDeadError } from '../worker/S3MWorkerClient.js';
|
|
10
11
|
import {
|
|
11
12
|
composeLeafAtlas,
|
|
12
13
|
expandTexblockRequestNames,
|
|
@@ -26,6 +27,7 @@ import type {
|
|
|
26
27
|
TexblockRequestPlan,
|
|
27
28
|
} from '../core/S3MBlockParser.js';
|
|
28
29
|
import type { Anchor, ParsedTile, Stats, StyleUniforms } from '../core/types.js';
|
|
30
|
+
import type { TileGeometryData } from '../core/TileGeometry.js';
|
|
29
31
|
import type { Group, Texture } from 'three';
|
|
30
32
|
|
|
31
33
|
function dirOf(url: string): string {
|
|
@@ -225,7 +227,12 @@ export interface TileNode {
|
|
|
225
227
|
/** 瓦片构建器可注入,用于测试 / 服务端渲染(此时不需要 three) */
|
|
226
228
|
export type BuildTileFn = (
|
|
227
229
|
tile: ParsedTile,
|
|
228
|
-
options: {
|
|
230
|
+
options: {
|
|
231
|
+
textureCache: Map<string, Texture>;
|
|
232
|
+
uniforms: StyleUniforms | null;
|
|
233
|
+
/** Worker 侧已完成的纯数据几何组装(经典 .s3mb 路径),构建器直接包装成 three 对象 */
|
|
234
|
+
assembled?: TileGeometryData;
|
|
235
|
+
}
|
|
229
236
|
) => Promise<{ group: Group; stats: Partial<Stats> }>;
|
|
230
237
|
|
|
231
238
|
export interface S3MTilesetOptions {
|
|
@@ -235,6 +242,15 @@ export interface S3MTilesetOptions {
|
|
|
235
242
|
/** 原始图像/子图集缓存上限(LRU),默认 128 */
|
|
236
243
|
maxImageCache?: number;
|
|
237
244
|
buildTile?: BuildTileFn | null;
|
|
245
|
+
/**
|
|
246
|
+
* 是否用 Web Worker 解析经典 `.s3mb` 瓦片(默认开)。
|
|
247
|
+
*
|
|
248
|
+
* Worker 里跑 parseS3MB 全链路(inflate / Draco / MeshOpt 内联 wasm 解码),
|
|
249
|
+
* 主线程只做 fetch 与几何组装,消除加载阶段的解析卡顿。
|
|
250
|
+
* Worker 创建失败或崩溃时自动**永久**降级回主线程解析,行为与关闭时完全一致。
|
|
251
|
+
* S3MBLOCK 路径(图集补齐与主线程缓存强耦合)暂不进 Worker,仍主线程解析。
|
|
252
|
+
*/
|
|
253
|
+
useWorker?: boolean;
|
|
238
254
|
uniforms?: StyleUniforms | null;
|
|
239
255
|
onTile?: (node: TileNode) => void;
|
|
240
256
|
/** 超过 maxTiles 被淘汰时回调,调用方要自己把场景里的对象和拾取表清掉 */
|
|
@@ -330,6 +346,8 @@ export class S3MTileset {
|
|
|
330
346
|
private isS3MV1 = false;
|
|
331
347
|
private _buildTile: BuildTileFn | null;
|
|
332
348
|
private readonly _uniforms: StyleUniforms | null;
|
|
349
|
+
/** 经典 .s3mb 的 Worker 解析客户端;undefined = 尚未尝试创建,null = 不可用/已降级 */
|
|
350
|
+
private _workerClient: S3MWorkerClient | null | undefined; private readonly _useWorker: boolean;
|
|
333
351
|
private _queue: string[] = [];
|
|
334
352
|
private _active = 0;
|
|
335
353
|
|
|
@@ -346,6 +364,7 @@ export class S3MTileset {
|
|
|
346
364
|
this._buildTile = options.buildTile ?? null;
|
|
347
365
|
// 着色器共享 uniform,由 S3MLayer 持有并传进来
|
|
348
366
|
this._uniforms = options.uniforms ?? null;
|
|
367
|
+
this._useWorker = options.useWorker !== false;
|
|
349
368
|
|
|
350
369
|
// 并发与总量限制:iServer 公开服务上别把人家打爆
|
|
351
370
|
this.maxConcurrent = options.maxConcurrent ?? 4;
|
|
@@ -677,6 +696,31 @@ export class S3MTileset {
|
|
|
677
696
|
return this._buildTile;
|
|
678
697
|
}
|
|
679
698
|
|
|
699
|
+
/**
|
|
700
|
+
* 经典 `.s3mb` 瓦片解析:优先走 Worker(inflate / Draco / MeshOpt / 几何组装
|
|
701
|
+
* 全在 Worker 做,一次往返带回 tile + 纯数据几何),Worker 不可用或已崩溃时
|
|
702
|
+
* 主线程兜底,两条路径输出完全一致。
|
|
703
|
+
*/
|
|
704
|
+
private async _parseClassicTile(buffer: ArrayBuffer): Promise<{ tile: ParsedTile; geometry?: TileGeometryData }> {
|
|
705
|
+
if (this._useWorker && this._workerClient === undefined) {
|
|
706
|
+
// 与纹理解码共用进程级单例,避免每个数据集各起一个 Worker
|
|
707
|
+
this._workerClient = getSharedS3MWorker();
|
|
708
|
+
}
|
|
709
|
+
if (this._workerClient) {
|
|
710
|
+
try {
|
|
711
|
+
return await this._workerClient.parseTile(buffer, true);
|
|
712
|
+
} catch (e) {
|
|
713
|
+
if (e instanceof WorkerDeadError) {
|
|
714
|
+
// 脚本加载失败 / 崩溃属于环境问题,永久退回主线程;瓦片本身的问题照常抛
|
|
715
|
+
this._workerClient = null;
|
|
716
|
+
} else {
|
|
717
|
+
throw e;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return { tile: await parseS3MB(buffer) };
|
|
722
|
+
}
|
|
723
|
+
|
|
680
724
|
/**
|
|
681
725
|
* 记住一个块文件里解析出来的全部 LOD 组。
|
|
682
726
|
*
|
|
@@ -948,6 +992,7 @@ export class S3MTileset {
|
|
|
948
992
|
const isBlock = /\.s3mblock($|\?)/i.test(baseUrl);
|
|
949
993
|
|
|
950
994
|
let tile: ParsedTile;
|
|
995
|
+
let assembled: TileGeometryData | undefined;
|
|
951
996
|
let groupNames: string[] = [];
|
|
952
997
|
const cachedSet = this._groupCache.get(baseUrl);
|
|
953
998
|
if (cachedSet) {
|
|
@@ -970,7 +1015,9 @@ export class S3MTileset {
|
|
|
970
1015
|
groupNames = set.names;
|
|
971
1016
|
tile = set.tiles[groupIndex] ?? set.tiles[0]!;
|
|
972
1017
|
} else {
|
|
973
|
-
|
|
1018
|
+
const parsed = await this._parseClassicTile(buffer);
|
|
1019
|
+
tile = parsed.tile;
|
|
1020
|
+
assembled = parsed.geometry;
|
|
974
1021
|
}
|
|
975
1022
|
}
|
|
976
1023
|
if (isBlock) {
|
|
@@ -980,7 +1027,11 @@ export class S3MTileset {
|
|
|
980
1027
|
}
|
|
981
1028
|
|
|
982
1029
|
const build = await this._builder();
|
|
983
|
-
const buildRes = await build(tile, {
|
|
1030
|
+
const buildRes = await build(tile, {
|
|
1031
|
+
textureCache: this.textureCache,
|
|
1032
|
+
uniforms: this._uniforms,
|
|
1033
|
+
assembled,
|
|
1034
|
+
});
|
|
984
1035
|
const { group, stats } = buildRes;
|
|
985
1036
|
const texKeys = (buildRes as any).textureKeys ?? (group.userData?.textureKeys as string[] | undefined) ?? Object.keys(tile.textures ?? {});
|
|
986
1037
|
this.registerTileTextures(url, texKeys);
|
|
@@ -1269,6 +1320,7 @@ export class S3MTileset {
|
|
|
1269
1320
|
}
|
|
1270
1321
|
|
|
1271
1322
|
dispose(): void {
|
|
1323
|
+
this._workerClient?.dispose();
|
|
1272
1324
|
for (const node of this.nodes.values()) {
|
|
1273
1325
|
node.group.traverse((o) => {
|
|
1274
1326
|
const mesh = o as {
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 解析 Worker 的主线程客户端。
|
|
3
|
+
*
|
|
4
|
+
* 生命周期约定(由 S3MTileset 使用):
|
|
5
|
+
* - `create()` 返回 null = 环境不可用(无 Worker / 脚本同步创建失败),调用方直接走主线程解析;
|
|
6
|
+
* - `parseTile()` 抛 `WorkerDeadError` = Worker 脚本加载失败或已崩溃,调用方应**永久**
|
|
7
|
+
* 退回主线程解析(不要再给这个 client 发活,也别让每块瓦片都白白失败两轮重试);
|
|
8
|
+
* - 抛普通 Error = Worker 活着、但解析本身失败(网络字节坏了 / S3M1 容器永久错误等),
|
|
9
|
+
* 语义与主线程 `parseS3MB` 抛错完全一致,原样向上传。
|
|
10
|
+
*/
|
|
11
|
+
import type { ParsedTile, S3MTexture } from '../core/types.js';
|
|
12
|
+
import type { DecodedTextureData } from '../core/TextureDecode.js';
|
|
13
|
+
import type { TileGeometryData } from '../core/TileGeometry.js';
|
|
14
|
+
|
|
15
|
+
/** Worker 崩溃 / 不可用 —— 与「瓦片数据本身有问题」区分开,调用方据此决定降级还是重试 */
|
|
16
|
+
export class WorkerDeadError extends Error {
|
|
17
|
+
constructor(message: string) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = 'WorkerDeadError';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface ParseOkResponse {
|
|
24
|
+
id: number;
|
|
25
|
+
ok: true;
|
|
26
|
+
tile: ParsedTile;
|
|
27
|
+
geometry?: TileGeometryData;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface TexOkResponse {
|
|
31
|
+
id: number;
|
|
32
|
+
ok: true;
|
|
33
|
+
data: DecodedTextureData;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface ErrResponse {
|
|
37
|
+
id: number;
|
|
38
|
+
ok: false;
|
|
39
|
+
message: string;
|
|
40
|
+
permanent: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type Response = ParseOkResponse | TexOkResponse | ErrResponse;
|
|
44
|
+
|
|
45
|
+
export class S3MWorkerClient {
|
|
46
|
+
private readonly _worker: Worker;
|
|
47
|
+
private _seq = 0;
|
|
48
|
+
private readonly _pending = new Map<number, { resolve: (v: unknown) => void; reject: (err: unknown) => void }>();
|
|
49
|
+
/** true = 已崩溃 / 已终止,后续请求直接拒绝 */
|
|
50
|
+
dead = false;
|
|
51
|
+
|
|
52
|
+
/** 创建失败返回 null(不抛),调用方据此走主线程解析 */
|
|
53
|
+
static create(): S3MWorkerClient | null {
|
|
54
|
+
if (typeof Worker === 'undefined') return null;
|
|
55
|
+
try {
|
|
56
|
+
// 路径锚定本模块的编译产物(dist/worker/s3mWorker.js);
|
|
57
|
+
// Vite 打包时会把这个 pattern 转成独立 worker 资产,IIFE 直引场景由死锁降级兜底
|
|
58
|
+
const worker = new Worker(new URL('./s3mWorker.js', import.meta.url), { type: 'module' });
|
|
59
|
+
return new S3MWorkerClient(worker);
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private constructor(worker: Worker) {
|
|
66
|
+
this._worker = worker;
|
|
67
|
+
worker.onmessage = (e: MessageEvent<Response>) => {
|
|
68
|
+
const msg = e.data;
|
|
69
|
+
const waiter = this._pending.get(msg.id);
|
|
70
|
+
if (!waiter) return;
|
|
71
|
+
this._pending.delete(msg.id);
|
|
72
|
+
if (!msg.ok) {
|
|
73
|
+
// 还原主线程 parseS3MB 的错误形状:permanent 标记驱动瓦片级的「永久失败」分支
|
|
74
|
+
const err = new Error(msg.message);
|
|
75
|
+
(err as { permanent?: boolean }).permanent = msg.permanent;
|
|
76
|
+
waiter.reject(err);
|
|
77
|
+
} else if ('tile' in msg) {
|
|
78
|
+
waiter.resolve({ tile: msg.tile, geometry: msg.geometry });
|
|
79
|
+
} else {
|
|
80
|
+
waiter.resolve(msg.data);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
worker.onerror = () => this._die('s3m 解析 Worker 加载失败/崩溃');
|
|
84
|
+
worker.onmessageerror = () => this._die('s3m 解析 Worker 消息无法反序列化');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 解析一块瓦片。buffer 会被 structured clone(不 transfer),主线程保留原 buffer 以便崩溃后降级重解。
|
|
89
|
+
* assemble = true 时顺带在 Worker 里做纯数据几何组装,返回 { tile, geometry } 一次带回。
|
|
90
|
+
*/
|
|
91
|
+
parseTile(buffer: ArrayBuffer, assemble = false, batching = true): Promise<{ tile: ParsedTile; geometry?: TileGeometryData }> {
|
|
92
|
+
if (this.dead) return Promise.reject(new WorkerDeadError('s3m 解析 Worker 已终止'));
|
|
93
|
+
const id = ++this._seq;
|
|
94
|
+
return new Promise<{ tile: ParsedTile; geometry?: TileGeometryData }>((resolve, reject) => {
|
|
95
|
+
this._pending.set(id, { resolve: resolve as (v: unknown) => void, reject });
|
|
96
|
+
this._worker.postMessage({ type: 'parse', id, buffer, assemble, batching });
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** 解码一张纹理(DXT / CRN / WEBP / RGB 系)。tex.data 走 structured clone,主线程保留原数据 */
|
|
101
|
+
decodeTexture(tex: S3MTexture, compressed = false): Promise<DecodedTextureData> {
|
|
102
|
+
if (this.dead) return Promise.reject(new WorkerDeadError('s3m 解析 Worker 已终止'));
|
|
103
|
+
const id = ++this._seq;
|
|
104
|
+
return new Promise<DecodedTextureData>((resolve, reject) => {
|
|
105
|
+
this._pending.set(id, { resolve: resolve as (v: unknown) => void, reject });
|
|
106
|
+
this._worker.postMessage({ type: 'tex', id, tex, compressed });
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
dispose(): void {
|
|
111
|
+
this._die('dispose');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private _die(reason: string): void {
|
|
115
|
+
if (this.dead) return;
|
|
116
|
+
this.dead = true;
|
|
117
|
+
try {
|
|
118
|
+
this._worker.terminate();
|
|
119
|
+
} catch {
|
|
120
|
+
/* 已死的 worker terminate 不会再抛 */
|
|
121
|
+
}
|
|
122
|
+
const err = new WorkerDeadError(`${reason},退回主线程解析`);
|
|
123
|
+
for (const waiter of this._pending.values()) waiter.reject(err);
|
|
124
|
+
this._pending.clear();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* ------------------------------------------------------------------ 共享单例 */
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 进程级共享的解析 Worker(解析 + 纹理解码共用一个线程,避免每个数据集 /
|
|
132
|
+
* 每个渲染层各起一个 Worker 把内存和调度打爆)。懒创建,崩溃后返回 null。
|
|
133
|
+
*/
|
|
134
|
+
let sharedClient: S3MWorkerClient | null | undefined;
|
|
135
|
+
|
|
136
|
+
export function getSharedS3MWorker(): S3MWorkerClient | null {
|
|
137
|
+
if (sharedClient === undefined) sharedClient = S3MWorkerClient.create();
|
|
138
|
+
if (sharedClient && sharedClient.dead) return null;
|
|
139
|
+
return sharedClient;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 纹理解码的「Worker 优先」入口,供 TextureFactory.setTextureOffThreadDecode 装配。
|
|
144
|
+
* 永不 reject:Worker 不可用 / 崩溃 / 单张解码失败都返回 null,调用方主线程兜底
|
|
145
|
+
* (解码失败本身在 decodeTextureData 里也不抛错,走 crn(failed) 之类的标记)。
|
|
146
|
+
* compressed 由 TextureFactory 按宿主 GL 能力传入,Worker 端据此跳过 DXT 解码。
|
|
147
|
+
*/
|
|
148
|
+
export async function decodeTextureWithWorker(
|
|
149
|
+
tex: S3MTexture,
|
|
150
|
+
compressed: boolean
|
|
151
|
+
): Promise<DecodedTextureData | null> {
|
|
152
|
+
const client = getSharedS3MWorker();
|
|
153
|
+
if (!client) return null;
|
|
154
|
+
try {
|
|
155
|
+
return await client.decodeTexture(tex, compressed);
|
|
156
|
+
} catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|