s3m-three 0.3.0 → 0.3.2
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/README.md +1 -1
- package/dist/browser/s3m-three.iife.ext-three.js +163 -15
- package/dist/browser/s3m-three.iife.js +190 -42
- package/dist/core/AtlasAlpha.d.ts +31 -0
- package/dist/core/AtlasAlpha.d.ts.map +1 -0
- package/dist/core/AtlasAlpha.js +82 -0
- package/dist/core/AtlasAlpha.js.map +1 -0
- package/dist/core/AtlasPack.d.ts +57 -0
- package/dist/core/AtlasPack.d.ts.map +1 -0
- package/dist/core/AtlasPack.js +81 -0
- package/dist/core/AtlasPack.js.map +1 -0
- package/dist/core/AtlasUpgrade.d.ts +68 -0
- package/dist/core/AtlasUpgrade.d.ts.map +1 -0
- package/dist/core/AtlasUpgrade.js +154 -0
- package/dist/core/AtlasUpgrade.js.map +1 -0
- package/dist/core/DXTTextureDecode.d.ts.map +1 -1
- package/dist/core/DXTTextureDecode.js +28 -6
- package/dist/core/DXTTextureDecode.js.map +1 -1
- package/dist/core/Draco.d.ts +52 -0
- package/dist/core/Draco.d.ts.map +1 -0
- package/dist/core/Draco.js +323 -0
- package/dist/core/Draco.js.map +1 -0
- package/dist/core/MeshOpt.d.ts +14 -3
- package/dist/core/MeshOpt.d.ts.map +1 -1
- package/dist/core/MeshOpt.js +101 -18
- package/dist/core/MeshOpt.js.map +1 -1
- package/dist/core/S3M1Parser.d.ts +16 -0
- package/dist/core/S3M1Parser.d.ts.map +1 -0
- package/dist/core/S3M1Parser.js +367 -0
- package/dist/core/S3M1Parser.js.map +1 -0
- package/dist/core/S3MBParser.d.ts +43 -1
- package/dist/core/S3MBParser.d.ts.map +1 -1
- package/dist/core/S3MBParser.js +101 -23
- package/dist/core/S3MBParser.js.map +1 -1
- package/dist/core/S3MBlockParser.d.ts +209 -0
- package/dist/core/S3MBlockParser.d.ts.map +1 -0
- package/dist/core/S3MBlockParser.js +789 -0
- package/dist/core/S3MBlockParser.js.map +1 -0
- package/dist/core/TextureFormat.d.ts +0 -1
- package/dist/core/TextureFormat.d.ts.map +1 -1
- package/dist/core/TextureFormat.js +19 -2
- package/dist/core/TextureFormat.js.map +1 -1
- package/dist/core/types.d.ts +65 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/render/DatasetManager.d.ts +11 -0
- package/dist/render/DatasetManager.d.ts.map +1 -1
- package/dist/render/DatasetManager.js +68 -3
- package/dist/render/DatasetManager.js.map +1 -1
- package/dist/render/GeometryBuilder.d.ts +3 -0
- package/dist/render/GeometryBuilder.d.ts.map +1 -1
- package/dist/render/GeometryBuilder.js +189 -17
- package/dist/render/GeometryBuilder.js.map +1 -1
- package/dist/render/GroundManager.d.ts +57 -1
- package/dist/render/GroundManager.d.ts.map +1 -1
- package/dist/render/GroundManager.js +149 -21
- package/dist/render/GroundManager.js.map +1 -1
- package/dist/render/Picker.d.ts.map +1 -1
- package/dist/render/Picker.js +12 -4
- package/dist/render/Picker.js.map +1 -1
- package/dist/render/PointCloudMaterial.d.ts +36 -0
- package/dist/render/PointCloudMaterial.d.ts.map +1 -0
- package/dist/render/PointCloudMaterial.js +159 -0
- package/dist/render/PointCloudMaterial.js.map +1 -0
- package/dist/render/S3MViewer.d.ts +75 -3
- package/dist/render/S3MViewer.d.ts.map +1 -1
- package/dist/render/S3MViewer.js +284 -54
- package/dist/render/S3MViewer.js.map +1 -1
- package/dist/render/StyleMaterial.d.ts +6 -0
- package/dist/render/StyleMaterial.d.ts.map +1 -1
- package/dist/render/StyleMaterial.js +125 -2
- package/dist/render/StyleMaterial.js.map +1 -1
- package/dist/render/TextureFactory.d.ts.map +1 -1
- package/dist/render/TextureFactory.js +64 -10
- package/dist/render/TextureFactory.js.map +1 -1
- package/dist/tileset/S3MTileset.d.ts +177 -1
- package/dist/tileset/S3MTileset.d.ts.map +1 -1
- package/dist/tileset/S3MTileset.js +754 -60
- package/dist/tileset/S3MTileset.js.map +1 -1
- package/dist/vendor/crunch_decoder.module.js +12 -18
- package/dist/vendor/draco_decoder.module.js +3178 -0
- package/docs/API.md +150 -11
- package/package.json +1 -1
- package/src/core/AtlasAlpha.ts +82 -0
- package/src/core/AtlasPack.ts +127 -0
- package/src/core/AtlasUpgrade.ts +182 -0
- package/src/core/DXTTextureDecode.ts +28 -6
- package/src/core/Draco.ts +409 -0
- package/src/core/MeshOpt.ts +124 -18
- package/src/core/S3M1Parser.ts +430 -0
- package/src/core/S3MBParser.ts +112 -21
- package/src/core/S3MBlockParser.ts +953 -0
- package/src/core/TextureFormat.ts +20 -2
- package/src/core/types.ts +63 -0
- package/src/index.ts +3 -1
- package/src/render/DatasetManager.ts +70 -5
- package/src/render/GeometryBuilder.ts +212 -16
- package/src/render/GroundManager.ts +174 -20
- package/src/render/Picker.ts +13 -4
- package/src/render/PointCloudMaterial.ts +174 -0
- package/src/render/S3MViewer.ts +322 -55
- package/src/render/StyleMaterial.ts +135 -2
- package/src/render/TextureFactory.ts +67 -11
- package/src/tileset/S3MTileset.ts +798 -65
- package/src/vendor/crunch_decoder.module.js +12 -18
- package/src/vendor/draco_decoder.module.d.ts +128 -0
- package/src/vendor/draco_decoder.module.js +3178 -0
|
@@ -0,0 +1,789 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S3MBLOCK 单文件分块瓦片(S3M 2.0+ 的 TextureSharing / StoreType=SingleFile 数据)
|
|
3
|
+
*
|
|
4
|
+
* 桌面端生成瓦片时选「文件类型 = S3MBLOCK」会把纹理与骨架分离存储:
|
|
5
|
+
* 配置里 tiles 是 N 个 `.s3mblock`,每个文件内含若干 RO 组,每组一段 pageLOD 元信息
|
|
6
|
+
* 加一份 zlib 压缩的 v2 瓦片体;纹理不跟瓦片走,而是作为「图集」内嵌在块里,
|
|
7
|
+
* 材质通过图集 id(如 `-667946976.texblock`)引用,几何顶点区间绑定到图集子区域。
|
|
8
|
+
*
|
|
9
|
+
* 布局从真实数据(SuperMap Online `3D-CBD/Xiaopin@CBD`)逐字节标定,
|
|
10
|
+
* 并与官方 `@supermapgis/s3m-content-loader` 的 S3MBlockBufferLoader 交叉验证过。
|
|
11
|
+
*
|
|
12
|
+
* 容器:
|
|
13
|
+
* ```
|
|
14
|
+
* float32 blockVersion // 2.1
|
|
15
|
+
* uint32 groupCount // RO 组数,实测 1
|
|
16
|
+
* ↑ 每组重复:
|
|
17
|
+
* String name // "0.s3mblock"
|
|
18
|
+
* if blockVersion <= 3.0:
|
|
19
|
+
* uint32 pageLodCount
|
|
20
|
+
* float32 rangeValue // 与内层瓦片的 pageLOD 相同(索引冗余)
|
|
21
|
+
* uint16 rangeMode
|
|
22
|
+
* float64 boundingSphere.center.xyz + radius
|
|
23
|
+
* String childTile // "0_00.s3mblock"
|
|
24
|
+
* float32 version // 内层瓦片版本(2.1)
|
|
25
|
+
* if version >= 3: uint32 compressedType
|
|
26
|
+
* uint32 unzipSize
|
|
27
|
+
* uint32 zipSize
|
|
28
|
+
* byte zipData[zipSize] // zlib
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* 内层(解压后)是「段目录」结构,与经典 v2 s3mb 的区别在于各段带独立长度前缀、
|
|
32
|
+
* v2 的 secondColor 独立成段、纹理段换成图集格式:
|
|
33
|
+
* ```
|
|
34
|
+
* uint32 options // bit0 = 有单体化信息
|
|
35
|
+
* uint32 size → groupNode 段(pageLOD + geode)
|
|
36
|
+
* uint32 size → 骨架段(v2 布局,复用 parseSkeletons)
|
|
37
|
+
* if version < 3: uint32 size → secondColor 段(跳过)
|
|
38
|
+
* uint32 size → 图集段(见下)
|
|
39
|
+
* String materials JSON
|
|
40
|
+
* if options & 1: uint32 size → 单体化信息段
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* 图集段(纹理共享的核心):
|
|
44
|
+
* ```
|
|
45
|
+
* uint32 count
|
|
46
|
+
* ↑ 每张图集重复:
|
|
47
|
+
* String id // "-667946976.texblock",材质 JSON 用它引用
|
|
48
|
+
* pad4
|
|
49
|
+
* uint32 level
|
|
50
|
+
* uint8 isSaveData // 1 = 纹理数据内嵌在本块
|
|
51
|
+
* uint32 width; uint32 height
|
|
52
|
+
* uint32 compressType; uint32 size; uint32 pixelFormat
|
|
53
|
+
* byte data[size] // 仅 isSaveData 时存在
|
|
54
|
+
* uint32 childTexCount → String×n // 派生图集链(祖先表)
|
|
55
|
+
* uint32 requestNameCount → String×n // 需要额外请求的 texblock 文件
|
|
56
|
+
* uint32 subTexCount // 图集子区域
|
|
57
|
+
* String subName // "60_3.texblock"
|
|
58
|
+
* pad4
|
|
59
|
+
* uint32 offsetX; offsetY; subWidth; subHeight
|
|
60
|
+
* uint32 geoCount
|
|
61
|
+
* String geoName // 骨架名
|
|
62
|
+
* uint32 vertexOffset; vertexCount; texUnitIndex
|
|
63
|
+
* ```
|
|
64
|
+
*
|
|
65
|
+
* 渲染侧不做 UV 烘焙,而是给每个顶点输出一份「所属子区域矩形」(ParsedTile.atlasRects),
|
|
66
|
+
* 由着色器逐片元算 `rect.xy + fract(uv) * rect.zw`——与官方 aTextureBatchId + batch 表
|
|
67
|
+
* 等价,但不需要官方的采样数组。顶点上取 fract 会让跨平铺边界的三角形反向扫纹理,别那样做。
|
|
68
|
+
*/
|
|
69
|
+
import { BinaryReader } from './BinaryReader.js';
|
|
70
|
+
import { parseGroupNodeEntries, parseSkeletonEntries, parsePickInfoEntries } from './S3MBParser.js';
|
|
71
|
+
import { packAtlases, decodeAtlasImage } from './AtlasPack.js';
|
|
72
|
+
async function defaultInflate(bytes) {
|
|
73
|
+
if (typeof DecompressionStream === 'undefined') {
|
|
74
|
+
throw new Error('[s3m] 当前环境无 DecompressionStream,请通过 options.inflate 注入解压实现');
|
|
75
|
+
}
|
|
76
|
+
const stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream('deflate'));
|
|
77
|
+
return new Uint8Array(await new Response(stream).arrayBuffer());
|
|
78
|
+
}
|
|
79
|
+
/* ------------------------------------------------------------------ 图集段 */
|
|
80
|
+
function parseBlockTextures(r) {
|
|
81
|
+
const count = r.u32();
|
|
82
|
+
const out = {};
|
|
83
|
+
for (let i = 0; i < count; i++) {
|
|
84
|
+
const id = r.string();
|
|
85
|
+
r.align4();
|
|
86
|
+
const level = r.u32();
|
|
87
|
+
const isSaveData = r.u8();
|
|
88
|
+
const width = r.u32();
|
|
89
|
+
const height = r.u32();
|
|
90
|
+
const compressType = r.u32();
|
|
91
|
+
const size = r.u32();
|
|
92
|
+
const pixelFormat = r.u32();
|
|
93
|
+
const data = isSaveData ? r.bytesCopy(size) : null;
|
|
94
|
+
const childTexCount = r.u32();
|
|
95
|
+
const childTexNames = [];
|
|
96
|
+
for (let j = 0; j < childTexCount; j++)
|
|
97
|
+
childTexNames.push(r.string());
|
|
98
|
+
const requestNameCount = r.u32();
|
|
99
|
+
const requestNames = [];
|
|
100
|
+
for (let j = 0; j < requestNameCount; j++)
|
|
101
|
+
requestNames.push(r.string());
|
|
102
|
+
const subTexCount = r.u32();
|
|
103
|
+
const subTexInfos = [];
|
|
104
|
+
for (let j = 0; j < subTexCount; j++) {
|
|
105
|
+
const subName = r.string();
|
|
106
|
+
// 注意:subName 后不 align(官方亦无),四个 u32 紧跟着来
|
|
107
|
+
const offsetX = r.u32();
|
|
108
|
+
const offsetY = r.u32();
|
|
109
|
+
const subWidth = r.u32();
|
|
110
|
+
const subHeight = r.u32();
|
|
111
|
+
const geoCount = r.u32();
|
|
112
|
+
const bindings = [];
|
|
113
|
+
for (let k = 0; k < geoCount; k++) {
|
|
114
|
+
const geoName = r.string();
|
|
115
|
+
const vertexOffset = r.u32();
|
|
116
|
+
const vertexCount = r.u32();
|
|
117
|
+
const texUnitIndex = r.u32();
|
|
118
|
+
bindings.push({ geoName, vertexOffset, vertexCount, texUnitIndex });
|
|
119
|
+
}
|
|
120
|
+
subTexInfos.push({ subName, offsetX, offsetY, width: subWidth, height: subHeight, bindings });
|
|
121
|
+
}
|
|
122
|
+
out[id] = { id, level, width, height, compressType, pixelFormat, data, childTexNames, requestNames, subTexInfos };
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* 把一个块里的多张图集拼成一张(原地改写 `texturePackage`)。
|
|
128
|
+
*
|
|
129
|
+
* 单独抽出来是因为它必须能**重跑**:`_fillExternalAtlases` 会把块内嵌的缩略图换成
|
|
130
|
+
* `childTexNames` 指向的高清 `.texblock`,像素变了就得重新拼、重新算 rect。
|
|
131
|
+
* 所以拼图前要把原始像素存进 `origin.data`。
|
|
132
|
+
*
|
|
133
|
+
* 已经拼过就直接返回(重复拼会把 RGBA 当 DXT 解,整张废掉)。
|
|
134
|
+
*/
|
|
135
|
+
function packTexturePackage(texturePackage) {
|
|
136
|
+
const entries = Object.values(texturePackage);
|
|
137
|
+
if (entries.some((t) => t.packed))
|
|
138
|
+
return null;
|
|
139
|
+
const packed = packAtlases(texturePackage);
|
|
140
|
+
if (!packed)
|
|
141
|
+
return null;
|
|
142
|
+
// 拼图是**逐块**做的:同一个 atlas id 在 LOD 兄弟块里参与的拼图集合可能不同,
|
|
143
|
+
// 落位就不同。纹理缓存却是跨瓦片共享的,只按 id 做 key 会让后一块复用前一块的
|
|
144
|
+
// 拼图(rect 指向旧排布)→ 整块贴错图。所以给每张拼图打一个「集合签名」,
|
|
145
|
+
// 缓存 key 带上它,集合相同才共享。
|
|
146
|
+
// leaf 组合画布再叠一层内容签名(同 id、同落位,但精灵来自不同 Tile_* 文件时内容不同)。
|
|
147
|
+
const leafSigs = entries.filter((t) => t.leafSig).map((t) => `${t.id}=${t.leafSig}`);
|
|
148
|
+
const packKey = `${packed.width}x${packed.height}#${[...packed.offsets.keys()].sort().join(',')}` +
|
|
149
|
+
(leafSigs.length ? `@${[...leafSigs].sort().join(',')}` : '');
|
|
150
|
+
for (const tex of entries) {
|
|
151
|
+
const t = tex;
|
|
152
|
+
// 每次拼都把「当前原始像素」记下来:unpack 要退回到它
|
|
153
|
+
t.origin = {
|
|
154
|
+
width: tex.width,
|
|
155
|
+
height: tex.height,
|
|
156
|
+
pixelFormat: tex.pixelFormat,
|
|
157
|
+
compressType: tex.compressType,
|
|
158
|
+
dataLength: tex.data?.length ?? 0,
|
|
159
|
+
data: tex.data ?? undefined,
|
|
160
|
+
};
|
|
161
|
+
tex.width = packed.width;
|
|
162
|
+
tex.height = packed.height;
|
|
163
|
+
tex.data = packed.data;
|
|
164
|
+
tex.compressType = 0;
|
|
165
|
+
tex.pixelFormat = 28; // RGBA
|
|
166
|
+
t.flipY = false; // 拼图按自上而下存,v=0 就是顶行
|
|
167
|
+
t.packed = true;
|
|
168
|
+
t.packKey = packKey;
|
|
169
|
+
}
|
|
170
|
+
return packed;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* 把拼图退回原始图集(`_fillExternalAtlases` 换高清图之前调用)。
|
|
174
|
+
*
|
|
175
|
+
* ★ 调用后 `tile.textures` / `tile.atlasRects` 会被清空 —— 它们描述的是**拼图**坐标系,
|
|
176
|
+
* 而此刻像素已经退回单张图集,两者不再自洽。必须紧接着调用
|
|
177
|
+
* `rebuildBlockTextureState` 才能拿去渲染。清空而不是留着旧值,是为了让「忘了重拼」
|
|
178
|
+
* 立刻表现为无贴图,而不是画面上莫名其妙的错位。
|
|
179
|
+
*/
|
|
180
|
+
export function unpackBlockTextures(tile) {
|
|
181
|
+
const tp = tile.texturePackage;
|
|
182
|
+
if (!tp)
|
|
183
|
+
return false;
|
|
184
|
+
let any = false;
|
|
185
|
+
for (const tex of Object.values(tp)) {
|
|
186
|
+
const t = tex;
|
|
187
|
+
if (!t.packed || !t.origin?.data)
|
|
188
|
+
continue;
|
|
189
|
+
tex.width = t.origin.width;
|
|
190
|
+
tex.height = t.origin.height;
|
|
191
|
+
tex.pixelFormat = t.origin.pixelFormat;
|
|
192
|
+
tex.compressType = t.origin.compressType;
|
|
193
|
+
tex.data = t.origin.data;
|
|
194
|
+
// 原始 DXT 图集退回时是底朝上(flipY=true);leaf 组合画布退回时本来就是自上而下
|
|
195
|
+
t.flipY = t.composed ? false : true;
|
|
196
|
+
t.packed = false;
|
|
197
|
+
t.rectScale = 1; // 像素退回原始尺寸,rect 参考倍率也要复位
|
|
198
|
+
delete t.packKey;
|
|
199
|
+
any = true;
|
|
200
|
+
}
|
|
201
|
+
if (any) {
|
|
202
|
+
tile.textures = {};
|
|
203
|
+
tile.atlasRects = {};
|
|
204
|
+
}
|
|
205
|
+
return any;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* 重算「纹理表 + 逐顶点矩形」。
|
|
209
|
+
*
|
|
210
|
+
* 调用时机:拼图状态刚变化过(首次解析、或换完高清图重拼)。两者必须一起重算 ——
|
|
211
|
+
* `atlasRects` 是拼图坐标系,`textures` 是喂给渲染侧的 map,**不同步就是满屏雪花**
|
|
212
|
+
* (见第十一轮事故)。
|
|
213
|
+
*/
|
|
214
|
+
export function rebuildBlockTextureState(tile) {
|
|
215
|
+
const packed = packTexturePackage(tile.texturePackage ?? {});
|
|
216
|
+
tile.atlasRects = buildAtlasRects(tile.geoPackage, tile.texturePackage ?? {}, packed);
|
|
217
|
+
tile.textures = buildTextureTable(tile.texturePackage ?? {});
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* 图集 → 渲染用的纹理表。
|
|
221
|
+
*
|
|
222
|
+
* ★ 必须在拼图**之后**建:拼图会原地改写 `width/height/data`,先建就会拿到拼图前的
|
|
223
|
+
* 块内嵌缩略图,而 `atlasRects` 是拼图坐标系的 —— 两者差 4 倍,表现为满屏彩色雪花。
|
|
224
|
+
*/
|
|
225
|
+
function buildTextureTable(texturePackage) {
|
|
226
|
+
const textures = {};
|
|
227
|
+
for (const tex of Object.values(texturePackage)) {
|
|
228
|
+
if (!tex.data)
|
|
229
|
+
continue;
|
|
230
|
+
// 拼图输出是自上而下的 RGBA,v=0 就是首行:不能再翻行,也不能让 GPU 生成 mip
|
|
231
|
+
// (GPU mip 会把相邻子区域混进来,层级必须由 StyleMaterial 的内联金字塔自己算)。
|
|
232
|
+
// leaf 组合画布(composed)同样自上而下、同样不能开 GPU mip —— 即使它没进拼图
|
|
233
|
+
// (块里只有一张图集时 packAtlases 返回 null,拼不拼的行序要求是一样的)。
|
|
234
|
+
const isPacked = tex.packed === true;
|
|
235
|
+
const composed = tex.composed === true;
|
|
236
|
+
const topDown = isPacked || composed;
|
|
237
|
+
textures[tex.id] = {
|
|
238
|
+
code: tex.id,
|
|
239
|
+
level: tex.level,
|
|
240
|
+
width: tex.width,
|
|
241
|
+
height: tex.height,
|
|
242
|
+
compressType: tex.compressType,
|
|
243
|
+
pixelFormat: tex.pixelFormat,
|
|
244
|
+
data: tex.data,
|
|
245
|
+
flipY: !topDown,
|
|
246
|
+
packed: isPacked,
|
|
247
|
+
composed,
|
|
248
|
+
packKey: tex.packKey,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
return textures;
|
|
252
|
+
}
|
|
253
|
+
function buildAtlasRects(geoPackage, texturePackage, packed) {
|
|
254
|
+
const rects = {};
|
|
255
|
+
const rectOf = (name, key) => {
|
|
256
|
+
const geo = geoPackage[name];
|
|
257
|
+
if (!geo)
|
|
258
|
+
return new Float32Array(0);
|
|
259
|
+
const entry = (rects[name] ??= {});
|
|
260
|
+
let arr = entry[key];
|
|
261
|
+
if (!arr) {
|
|
262
|
+
const n = geo.vertexPackage.verticesCount;
|
|
263
|
+
arr = entry[key] = new Float32Array(n * 4); // 全 0 = 退化矩形 = 该单元不参与
|
|
264
|
+
}
|
|
265
|
+
return arr;
|
|
266
|
+
};
|
|
267
|
+
// 一个 geo 的顶点可能分散绑定在多张图集上(官方用纹理数组按 batch 取)。
|
|
268
|
+
// 这里先把它们拼成一张大图,rect 全部换算到拼图坐标系,单张 map 就能覆盖全部顶点。
|
|
269
|
+
//
|
|
270
|
+
// ★ rect **不做任何裁切**,原样保留 (offsetX, offsetY, width, height)。
|
|
271
|
+
// width 里包含了子区域右 1/3 的内联 mip 金字塔(见 StyleMaterial.s3mAtlasLod),
|
|
272
|
+
// 按 alpha 收缩到不透明包围盒会同时破坏两件事:mip0 的 2/3 比例、以及 mip 列的偏移量,
|
|
273
|
+
// 结果就是每格采到邻格内容。透明区域交给 alphaTest 处理,不动 rect。
|
|
274
|
+
const localRects = new Map();
|
|
275
|
+
for (const [key, tex] of Object.entries(texturePackage)) {
|
|
276
|
+
if (!tex.width || !tex.height)
|
|
277
|
+
continue;
|
|
278
|
+
// subTexInfos 是「缩略图像素坐标」;换成整数倍高清图后要按 rectScale 换算,
|
|
279
|
+
// 才能和 packed.offsets(也是像素)落在同一空间。
|
|
280
|
+
const rs = tex.rectScale ?? 1;
|
|
281
|
+
localRects.set(key, tex.subTexInfos.map((sub) => ({ x: sub.offsetX * rs, y: sub.offsetY * rs, w: sub.width * rs, h: sub.height * rs })));
|
|
282
|
+
}
|
|
283
|
+
let groupId = 0;
|
|
284
|
+
for (const [key0, tex] of Object.entries(texturePackage)) {
|
|
285
|
+
if (!tex.width || !tex.height)
|
|
286
|
+
continue;
|
|
287
|
+
const off = packed ? (packed.offsets.get(key0) ?? { x: 0, y: 0 }) : { x: 0, y: 0 };
|
|
288
|
+
const locals = localRects.get(key0) ?? [];
|
|
289
|
+
tex.subTexInfos.forEach((sub, si) => {
|
|
290
|
+
groupId++;
|
|
291
|
+
const lr = locals[si] ?? { x: sub.offsetX, y: sub.offsetY, w: sub.width, h: sub.height };
|
|
292
|
+
const src = tex.origin;
|
|
293
|
+
const srcArea = src ? src.width * src.height : tex.width * tex.height;
|
|
294
|
+
const wideSprite = srcArea > 0 && (sub.width * sub.height) / srcArea >= 0.5;
|
|
295
|
+
const fit = {
|
|
296
|
+
x: (off.x + lr.x) / tex.width,
|
|
297
|
+
y: (off.y + lr.y) / tex.height,
|
|
298
|
+
w: lr.w / tex.width,
|
|
299
|
+
h: lr.h / tex.height,
|
|
300
|
+
};
|
|
301
|
+
for (const b of sub.bindings) {
|
|
302
|
+
const geo = geoPackage[b.geoName];
|
|
303
|
+
if (!geo)
|
|
304
|
+
continue;
|
|
305
|
+
const key = b.texUnitIndex === 0 ? 'uv' : 'uv2';
|
|
306
|
+
const arr = rectOf(b.geoName, key);
|
|
307
|
+
// group / groupDim 只认 **基色单元(unit0)**。
|
|
308
|
+
// 一个顶点可以同时出现在 unit0(立面)和 unit1(烘焙图)的 binding 里,
|
|
309
|
+
// 两边都写的话后写的会覆盖前者 —— 实测就是拿烘焙图的分组去算立面 UV,
|
|
310
|
+
// 整块街区的烘焙照片被贴到每一面墙上。
|
|
311
|
+
const garr = b.texUnitIndex === 0
|
|
312
|
+
? (rects[b.geoName].group ??= new Float32Array(geo.vertexPackage.verticesCount))
|
|
313
|
+
: null;
|
|
314
|
+
const darr = b.texUnitIndex === 0
|
|
315
|
+
? (rects[b.geoName].groupDim ??= new Float32Array(geo.vertexPackage.verticesCount * 3))
|
|
316
|
+
: null;
|
|
317
|
+
const end = Math.min(b.vertexOffset + b.vertexCount, geo.vertexPackage.verticesCount);
|
|
318
|
+
for (let v = b.vertexOffset; v < end; v++) {
|
|
319
|
+
arr[v * 4] = fit.x;
|
|
320
|
+
arr[v * 4 + 1] = fit.y;
|
|
321
|
+
arr[v * 4 + 2] = fit.w;
|
|
322
|
+
arr[v * 4 + 3] = fit.h;
|
|
323
|
+
if (!garr || !darr)
|
|
324
|
+
continue;
|
|
325
|
+
garr[v] = groupId;
|
|
326
|
+
// 精灵的原始像素尺寸(不是裁切后的):决定平铺时一个周期该有多大
|
|
327
|
+
darr[v * 3] = sub.width;
|
|
328
|
+
darr[v * 3 + 1] = sub.height;
|
|
329
|
+
darr[v * 3 + 2] = wideSprite ? 1 : 0;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
return rects;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* 解析独立的 `.texblock` 文件,返回**按段分组**的全部图像记录。
|
|
338
|
+
*
|
|
339
|
+
* 实测三种外层格式(判定顺序即优先级):
|
|
340
|
+
*
|
|
341
|
+
* 1. **动态 pack(kMi 协议)**:`u32 count | u32 zippedLength[count] | 段[count]`,
|
|
342
|
+
* 每段 = 普通容器。这是 TextureSharing leaf 模式引擎真正会拉的响应:
|
|
343
|
+
* `GET Tex_0_<code>.texblock?extratiles=Tex_0_000;Tex_0_009;…` —— extratiles 是
|
|
344
|
+
* **去扩展名**的 quadKey(带 `.texblock` 会被服务端解成别的内容),段顺序与之
|
|
345
|
+
* 一一对应,官方 `kMi(buffer, quadKeyIndex)` 就是取第 i 段。
|
|
346
|
+
* 守卫:count ≤ 256(官方 kMi 同限)且头部算术严丝合缝。
|
|
347
|
+
* 2. **静态 pack**:`u32 count | (offset,length)[count] | 段`,段 = 单条
|
|
348
|
+
* `{nameLen|name|unzip|zip|zlib}` 条目(没有段头 count)。`Tex_0.texblock`
|
|
349
|
+
* (275 段)是这个格式 —— 但它只是离线索引,引擎并不请求它。
|
|
350
|
+
* 3. **普通容器**:`u32 count | {nameLen|name|unzip|zip|zlib} × count`。
|
|
351
|
+
* `Tile_6_000000.texblock`(16 条记录)、`911305599.texblock`(1 条)属于这类。
|
|
352
|
+
*
|
|
353
|
+
* 段语义:pack 的第 i 段对应第 i 个请求名;容器整体算一段。
|
|
354
|
+
* 记录 `id` = 条目名(如 `117_0.texblock`),leaf 组合时用它对 `subTexInfos.subName`。
|
|
355
|
+
*
|
|
356
|
+
* 容器不再只读第一条:旧实现把 `count` 之外的条目全扔了,多记录容器
|
|
357
|
+
* (`Tile_6_000000` 这类)只能拿到第一张精灵。
|
|
358
|
+
*/
|
|
359
|
+
export async function parseTexblockSegments(arrayBuffer, options = {}) {
|
|
360
|
+
const inflate = options.inflate ?? defaultInflate;
|
|
361
|
+
const bytes = new Uint8Array(arrayBuffer);
|
|
362
|
+
const dyn = await tryDynPack(bytes, inflate);
|
|
363
|
+
if (dyn)
|
|
364
|
+
return dyn;
|
|
365
|
+
const stat = await tryStaticPack(bytes, inflate);
|
|
366
|
+
if (stat)
|
|
367
|
+
return stat;
|
|
368
|
+
const container = await readContainerEntries(bytes, inflate);
|
|
369
|
+
if (container)
|
|
370
|
+
return [container];
|
|
371
|
+
throw new Error('[s3m] .texblock 无法识别的外层格式(动态 pack / 静态 pack / 容器均不成立)');
|
|
372
|
+
}
|
|
373
|
+
/** 解压后的裸图像记录区:`ct|w|h|pf|size|data` 可多条连排,尾部允许有无关字节 */
|
|
374
|
+
function readImageRecords(raw, id) {
|
|
375
|
+
const t = new BinaryReader(raw.buffer, raw.byteOffset, raw.byteLength);
|
|
376
|
+
const out = [];
|
|
377
|
+
while (t.remaining >= 20) {
|
|
378
|
+
const compressType = t.u32();
|
|
379
|
+
const width = t.u32();
|
|
380
|
+
const height = t.u32();
|
|
381
|
+
const pixelFormat = t.u32();
|
|
382
|
+
const size = t.u32();
|
|
383
|
+
if (!width || !height || size > t.remaining)
|
|
384
|
+
break;
|
|
385
|
+
out.push({ id, width, height, compressType, pixelFormat, data: t.bytesCopy(size) });
|
|
386
|
+
}
|
|
387
|
+
return out;
|
|
388
|
+
}
|
|
389
|
+
/** 读 `count | 条目 × count` 容器(尾部允许有无关字节)。结构不成立返回 null。 */
|
|
390
|
+
async function readContainerEntries(bytes, inflate) {
|
|
391
|
+
if (bytes.byteLength < 8)
|
|
392
|
+
return null;
|
|
393
|
+
try {
|
|
394
|
+
const r = new BinaryReader(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
395
|
+
const count = r.u32();
|
|
396
|
+
if (count < 1 || count > 4096)
|
|
397
|
+
return null;
|
|
398
|
+
const out = [];
|
|
399
|
+
for (let i = 0; i < count; i++) {
|
|
400
|
+
const at = r.offset;
|
|
401
|
+
const nameLen = r.u32();
|
|
402
|
+
if (nameLen < 1 || nameLen > 4096 || r.remaining < nameLen + 8)
|
|
403
|
+
return null;
|
|
404
|
+
r.offset = at; // string() 自带长度前缀,先回退再读,避免把长度吃两遍
|
|
405
|
+
const name = r.string();
|
|
406
|
+
const unzipSize = r.u32();
|
|
407
|
+
const zipSize = r.u32();
|
|
408
|
+
if (zipSize > r.remaining)
|
|
409
|
+
return null;
|
|
410
|
+
const raw = await inflate(r.bytesCopy(zipSize));
|
|
411
|
+
if (raw.length !== unzipSize)
|
|
412
|
+
return null;
|
|
413
|
+
out.push(...readImageRecords(raw, name));
|
|
414
|
+
}
|
|
415
|
+
return out.length ? out : null;
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
/** 单条目(无 count 头):`nameLen|name|unzip|zip|zlib`,必须恰好消费整段。 */
|
|
422
|
+
async function readSingleEntry(bytes, inflate) {
|
|
423
|
+
if (bytes.byteLength < 16)
|
|
424
|
+
return null;
|
|
425
|
+
try {
|
|
426
|
+
const r = new BinaryReader(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
427
|
+
const at = r.offset;
|
|
428
|
+
const nameLen = r.u32();
|
|
429
|
+
if (nameLen < 1 || nameLen > 4096 || r.remaining < nameLen + 8)
|
|
430
|
+
return null;
|
|
431
|
+
r.offset = at; // string() 自带长度前缀,先回退再读
|
|
432
|
+
const name = r.string();
|
|
433
|
+
const unzipSize = r.u32();
|
|
434
|
+
const zipSize = r.u32();
|
|
435
|
+
if (zipSize !== r.remaining)
|
|
436
|
+
return null;
|
|
437
|
+
const raw = await inflate(r.bytesCopy(zipSize));
|
|
438
|
+
if (raw.length !== unzipSize)
|
|
439
|
+
return null;
|
|
440
|
+
const recs = readImageRecords(raw, name);
|
|
441
|
+
return recs.length ? recs : null;
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
/** 动态 pack(kMi):`count | zippedLength[] | 段[]`,段 = 容器。算术必须严丝合缝。 */
|
|
448
|
+
async function tryDynPack(bytes, inflate) {
|
|
449
|
+
if (bytes.byteLength < 8)
|
|
450
|
+
return null;
|
|
451
|
+
try {
|
|
452
|
+
const head = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
453
|
+
const count = head.getUint32(0, true);
|
|
454
|
+
if (count < 1 || count > 256)
|
|
455
|
+
return null; // 官方 kMi 同限
|
|
456
|
+
const header = 4 + 4 * count;
|
|
457
|
+
if (header > bytes.byteLength)
|
|
458
|
+
return null;
|
|
459
|
+
let sum = header;
|
|
460
|
+
const lens = [];
|
|
461
|
+
for (let i = 0; i < count; i++) {
|
|
462
|
+
const len = head.getUint32(4 + i * 4, true);
|
|
463
|
+
if (len < 8)
|
|
464
|
+
return null;
|
|
465
|
+
lens.push(len);
|
|
466
|
+
sum += len;
|
|
467
|
+
}
|
|
468
|
+
if (sum !== bytes.byteLength)
|
|
469
|
+
return null;
|
|
470
|
+
const segments = [];
|
|
471
|
+
let off = header;
|
|
472
|
+
for (const len of lens) {
|
|
473
|
+
const seg = await readContainerEntries(bytes.subarray(off, off + len), inflate);
|
|
474
|
+
if (!seg)
|
|
475
|
+
return null;
|
|
476
|
+
segments.push(seg);
|
|
477
|
+
off += len;
|
|
478
|
+
}
|
|
479
|
+
return segments;
|
|
480
|
+
}
|
|
481
|
+
catch {
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
/** 静态 pack:`count | (offset,length)[] | 段`,段 = 单条裸条目(无 count 头)。 */
|
|
486
|
+
async function tryStaticPack(bytes, inflate) {
|
|
487
|
+
if (bytes.byteLength < 12)
|
|
488
|
+
return null;
|
|
489
|
+
try {
|
|
490
|
+
const head = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
491
|
+
const count = head.getUint32(0, true);
|
|
492
|
+
if (count < 1 || count > 4096)
|
|
493
|
+
return null;
|
|
494
|
+
const tableEnd = 4 + 8 * count;
|
|
495
|
+
if (tableEnd > bytes.byteLength)
|
|
496
|
+
return null;
|
|
497
|
+
const segments = [];
|
|
498
|
+
for (let i = 0; i < count; i++) {
|
|
499
|
+
const off = head.getUint32(4 + i * 8, true);
|
|
500
|
+
const len = head.getUint32(8 + i * 8, true);
|
|
501
|
+
if (off < tableEnd || len < 8 || off > bytes.byteLength - len)
|
|
502
|
+
return null;
|
|
503
|
+
const seg = await readSingleEntry(bytes.subarray(off, off + len), inflate);
|
|
504
|
+
if (!seg)
|
|
505
|
+
return null;
|
|
506
|
+
segments.push(seg);
|
|
507
|
+
}
|
|
508
|
+
return segments;
|
|
509
|
+
}
|
|
510
|
+
catch {
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* 解析独立的 `.texblock` 子图集文件,扁平返回全部记录(`id` = 条目名)。
|
|
516
|
+
*
|
|
517
|
+
* 外层格式判定见 `parseTexblockSegments`;单记录文件与旧行为完全一致。
|
|
518
|
+
*/
|
|
519
|
+
export async function parseS3MBlockTextureFile(arrayBuffer, options = {}) {
|
|
520
|
+
const segments = await parseTexblockSegments(arrayBuffer, options);
|
|
521
|
+
return segments.flat();
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* 展开 `requestNames` —— 逐字对应官方 `lM.prototype.createTexture` 里的 `_subRequestNames` 构造:
|
|
525
|
+
*
|
|
526
|
+
* - `yke(s)` = 第一个 `.` 之前的部分(`Tex_0.texblock#…` → `Tex_0`,`Tile_6_000000.texblock` → `Tile_6_000000`);
|
|
527
|
+
* - stem 首段是 `Tex` → 按 `#` 后的 3 位码展开成 `Tex_0_<code>`(**无 `#` 的 Tex 名不产生子请求**,官方如此);
|
|
528
|
+
* - 其它 → 直接用去扩展名的名字。
|
|
529
|
+
*
|
|
530
|
+
* 这些名字就是 pack 请求的 quadKey:`extratiles` 用 `;` 连接、**不带扩展名**。
|
|
531
|
+
*/
|
|
532
|
+
export function expandTexblockRequestNames(requestNames) {
|
|
533
|
+
const yke = (s) => {
|
|
534
|
+
const dot = s.indexOf('.');
|
|
535
|
+
return dot >= 0 ? s.slice(0, dot) : s;
|
|
536
|
+
};
|
|
537
|
+
const packGroups = new Map();
|
|
538
|
+
const plain = [];
|
|
539
|
+
for (const rn of requestNames ?? []) {
|
|
540
|
+
const stem = yke(rn);
|
|
541
|
+
if (stem.split('_')[0] === 'Tex') {
|
|
542
|
+
const hash = rn.indexOf('#');
|
|
543
|
+
if (hash < 0)
|
|
544
|
+
continue; // 官方:Tex_* 无 #码 → 不产生子请求
|
|
545
|
+
const base = yke(rn.slice(0, hash));
|
|
546
|
+
const codes = rn.slice(hash + 1);
|
|
547
|
+
const group = packGroups.get(base) ?? [];
|
|
548
|
+
for (let i = 0; i + 3 <= codes.length; i += 3) {
|
|
549
|
+
const name = `${base}_${codes.slice(i, i + 3)}`;
|
|
550
|
+
if (!group.includes(name))
|
|
551
|
+
group.push(name);
|
|
552
|
+
}
|
|
553
|
+
packGroups.set(base, group);
|
|
554
|
+
}
|
|
555
|
+
else if (!plain.includes(stem)) {
|
|
556
|
+
plain.push(stem);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
const names = [];
|
|
560
|
+
for (const group of packGroups.values())
|
|
561
|
+
for (const n of group)
|
|
562
|
+
if (!names.includes(n))
|
|
563
|
+
names.push(n);
|
|
564
|
+
for (const n of plain)
|
|
565
|
+
if (!names.includes(n))
|
|
566
|
+
names.push(n);
|
|
567
|
+
return { packGroups, plain, names };
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* leaf 组合:把多张精灵按 `subTexInfos` 的 offset 贴成整张图集画布。
|
|
571
|
+
*
|
|
572
|
+
* 与官方 `requestSubTextures` 的 leaf 分支等价:
|
|
573
|
+
* - 记录按 `subName` 匹配 `subTexInfos`,没匹配上的精灵忽略、没精灵的 rect 留透明;
|
|
574
|
+
* - 贴图用**精灵自己的宽高**(官方 `width:D= C.width`),rect 仍按 subTexInfos 的
|
|
575
|
+
* sub 宽高(渲染侧照旧),实测两者相等(比例 1.00×1.00);
|
|
576
|
+
* - 精灵是「左 2/3 base + 右 1/3 内联 mip 金字塔」的完整小图,原样整张贴入,
|
|
577
|
+
* StyleMaterial 的 `s3mAtlasLod` 2/3 采样公式直接适用(见 render/StyleMaterial.ts);
|
|
578
|
+
* - DXT 解码缓冲底朝上,逐行倒序贴入(与 `packAtlases` 同一约定),画布行 0 = 源图顶行。
|
|
579
|
+
*
|
|
580
|
+
* 返回 null = 没有任何 subName 命中 / 全部解码失败。
|
|
581
|
+
*/
|
|
582
|
+
export function composeLeafAtlas(tex, sprites) {
|
|
583
|
+
const subs = tex.subTexInfos ?? [];
|
|
584
|
+
if (!subs.length || !tex.width || !tex.height)
|
|
585
|
+
return null;
|
|
586
|
+
const placed = [];
|
|
587
|
+
for (const sub of subs) {
|
|
588
|
+
const sprite = sprites.get(sub.subName);
|
|
589
|
+
if (sprite)
|
|
590
|
+
placed.push({ sub, sprite });
|
|
591
|
+
}
|
|
592
|
+
if (!placed.length)
|
|
593
|
+
return null;
|
|
594
|
+
// 画布至少装下声明尺寸 + 所有精灵(offset + 精灵宽高)
|
|
595
|
+
let W = tex.width;
|
|
596
|
+
let H = tex.height;
|
|
597
|
+
for (const { sub, sprite } of placed) {
|
|
598
|
+
W = Math.max(W, sub.offsetX + sprite.image.width);
|
|
599
|
+
H = Math.max(H, sub.offsetY + sprite.image.height);
|
|
600
|
+
}
|
|
601
|
+
const data = new Uint8Array(W * H * 4);
|
|
602
|
+
const matched = [];
|
|
603
|
+
const missing = [];
|
|
604
|
+
const mismatched = [];
|
|
605
|
+
const sigs = [];
|
|
606
|
+
let painted = 0;
|
|
607
|
+
for (const sub of subs) {
|
|
608
|
+
const sprite = sprites.get(sub.subName);
|
|
609
|
+
if (!sprite) {
|
|
610
|
+
missing.push(sub.subName);
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
const img = sprite.image;
|
|
614
|
+
if (img.width !== sub.width || img.height !== sub.height)
|
|
615
|
+
mismatched.push(sub.subName);
|
|
616
|
+
const decoded = decodeAtlasImage({
|
|
617
|
+
id: img.id,
|
|
618
|
+
width: img.width,
|
|
619
|
+
height: img.height,
|
|
620
|
+
compressType: img.compressType,
|
|
621
|
+
pixelFormat: img.pixelFormat,
|
|
622
|
+
data: img.data,
|
|
623
|
+
});
|
|
624
|
+
if (!decoded) {
|
|
625
|
+
missing.push(sub.subName); // 像素没贴上(非 DXT 且非已解 RGBA),按缺失处理
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
for (let row = 0; row < img.height; row++) {
|
|
629
|
+
const srcRow = decoded.topDown ? row : img.height - 1 - row;
|
|
630
|
+
const dstY = sub.offsetY + row;
|
|
631
|
+
if (dstY >= H)
|
|
632
|
+
break;
|
|
633
|
+
const src = srcRow * img.width * 4;
|
|
634
|
+
data.set(decoded.rgba.subarray(src, src + img.width * 4), (dstY * W + sub.offsetX) * 4);
|
|
635
|
+
}
|
|
636
|
+
painted++;
|
|
637
|
+
matched.push(sub.subName);
|
|
638
|
+
sigs.push(`${sprite.source}#${sub.subName}`);
|
|
639
|
+
}
|
|
640
|
+
if (!painted)
|
|
641
|
+
return null;
|
|
642
|
+
return { width: W, height: H, data, matched, missing, mismatched, sig: sigs.sort().join(',') };
|
|
643
|
+
}
|
|
644
|
+
/* ------------------------------------------------------------------ 容器 */
|
|
645
|
+
export async function parseS3MBlock(arrayBuffer, options = {}) {
|
|
646
|
+
const { tiles } = await parseS3MBlockGroups(arrayBuffer, options);
|
|
647
|
+
return tiles[0];
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* 解析 S3MBLOCK 容器里的**全部**组。
|
|
651
|
+
*
|
|
652
|
+
* 一个 `.s3mblock` 文件不是"一块瓦片",而是**一整棵 LOD 子树的打包**。实测
|
|
653
|
+
* `6_01.s3mblock`(549 KB)里有 5 组:
|
|
654
|
+
*
|
|
655
|
+
* ```
|
|
656
|
+
* 组0 6_01.s3mblock rv=586.8 r=293.4 child=6_0100.s3mblock
|
|
657
|
+
* 组1 6_0100.s3mblock rv=1024.0 r=293.4 child=6_010000.s3mblock
|
|
658
|
+
* 组2 6_010000.s3mblock rv=1024.0 r=338.2 child=6_01000000.s3mblock / 6_01000001.s3mblock
|
|
659
|
+
* 组3 6_01000000.s3mblock rv=1024.0 r=278.2
|
|
660
|
+
* 组4 6_01000001.s3mblock rv=1024.0 r=278.2
|
|
661
|
+
* ```
|
|
662
|
+
*
|
|
663
|
+
* childTile 指向的下一层**就在同一个文件里**,不是独立文件。只取第 0 组的话,
|
|
664
|
+
* 4/5 的几何与它们那份精细图集全被丢掉 —— 画面上永远只有最粗一层的
|
|
665
|
+
* "整栋楼缩略图"图集,这正是"官方是窗格立面、我们是整楼照片重复"的根源。
|
|
666
|
+
*/
|
|
667
|
+
export async function parseS3MBlockGroups(arrayBuffer, options = {}) {
|
|
668
|
+
const inflate = options.inflate ?? defaultInflate;
|
|
669
|
+
const r = new BinaryReader(arrayBuffer);
|
|
670
|
+
const blockVersion = r.f32();
|
|
671
|
+
const groupCount = r.u32();
|
|
672
|
+
if (groupCount < 1 || groupCount > 4096) {
|
|
673
|
+
throw new Error(`[s3m] S3MBLOCK 组数异常:${groupCount}`);
|
|
674
|
+
}
|
|
675
|
+
const tiles = [];
|
|
676
|
+
const names = [];
|
|
677
|
+
for (let g = 0; g < groupCount; g++) {
|
|
678
|
+
const name = r.string().replace(/\.s3mblock$/i, '');
|
|
679
|
+
names.push(name);
|
|
680
|
+
const pageLods = [];
|
|
681
|
+
if (blockVersion <= 3.0) {
|
|
682
|
+
const pageLodCount = r.u32();
|
|
683
|
+
for (let i = 0; i < pageLodCount; i++) {
|
|
684
|
+
const rangeValue = r.f32();
|
|
685
|
+
const rangeMode = r.u16();
|
|
686
|
+
const boundingSphere = {
|
|
687
|
+
center: { x: r.f64(), y: r.f64(), z: r.f64() },
|
|
688
|
+
radius: r.f64(),
|
|
689
|
+
};
|
|
690
|
+
const childTile = r.string();
|
|
691
|
+
pageLods.push({ rangeValue, rangeMode, boundingSphere, childTile });
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
const version = Number(r.f32().toFixed(2));
|
|
695
|
+
let compressedType = 0;
|
|
696
|
+
if (version >= 3)
|
|
697
|
+
compressedType = r.u32();
|
|
698
|
+
const unzipSize = r.u32();
|
|
699
|
+
const zipSize = r.u32();
|
|
700
|
+
const zipData = r.bytesCopy(zipSize);
|
|
701
|
+
const raw = await inflate(zipData);
|
|
702
|
+
if (raw.length !== unzipSize) {
|
|
703
|
+
throw new Error(`[s3m] S3MBLOCK 解压长度不符:声明 ${unzipSize},实际 ${raw.length}`);
|
|
704
|
+
}
|
|
705
|
+
// ---- 内层:段目录 ----
|
|
706
|
+
const t = new BinaryReader(raw.buffer, raw.byteOffset, raw.byteLength);
|
|
707
|
+
const fileOptions = t.u32();
|
|
708
|
+
let size = t.u32();
|
|
709
|
+
const groupNode = parseGroupNodeEntries(new BinaryReader(t.buffer, t.base + t.offset, size), version);
|
|
710
|
+
t.skip(size);
|
|
711
|
+
size = t.u32();
|
|
712
|
+
const geoPackage = await parseSkeletonEntries(new BinaryReader(t.buffer, t.base + t.offset, size), version);
|
|
713
|
+
t.skip(size);
|
|
714
|
+
if (version < 3) {
|
|
715
|
+
size = t.u32();
|
|
716
|
+
t.skip(size); // secondColor 独立段,渲染用不到
|
|
717
|
+
}
|
|
718
|
+
size = t.u32();
|
|
719
|
+
const texturePackage = parseBlockTextures(new BinaryReader(t.buffer, t.base + t.offset, size));
|
|
720
|
+
t.skip(size);
|
|
721
|
+
const materialsJson = t.string();
|
|
722
|
+
let materials = [];
|
|
723
|
+
if (materialsJson) {
|
|
724
|
+
try {
|
|
725
|
+
materials = JSON.parse(materialsJson.replace(/\n\0/, ''));
|
|
726
|
+
}
|
|
727
|
+
catch {
|
|
728
|
+
materials = materialsJson;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
let pickInfo = null;
|
|
732
|
+
let extensions;
|
|
733
|
+
if ((fileOptions & 1) === 1) {
|
|
734
|
+
if (version >= 3)
|
|
735
|
+
t.u32(); // IDInfoOption
|
|
736
|
+
size = t.u32();
|
|
737
|
+
pickInfo = parsePickInfoEntries(new BinaryReader(t.buffer, t.base + t.offset, size), geoPackage, version);
|
|
738
|
+
t.skip(size);
|
|
739
|
+
}
|
|
740
|
+
if (version >= 3) {
|
|
741
|
+
const ext = t.string();
|
|
742
|
+
extensions = ext ? JSON.parse(ext) : undefined;
|
|
743
|
+
}
|
|
744
|
+
// ---- 图集 → 纹理表 ----
|
|
745
|
+
//
|
|
746
|
+
// ★ 顺序有讲究:先拼图 → 再算 rect → 最后建纹理表。
|
|
747
|
+
// 拼图会原地改写 texturePackage 的 width/height/data,所以三者的坐标系必须一起定。
|
|
748
|
+
// 以前 textures 建在拼图之前,于是 atlasRects 按拼图尺寸(408×192)归一化、
|
|
749
|
+
// textures 却是拼图前的块内嵌缩略图(108×192 DXT5),渲染侧 uAtlasDim 取 map 的尺寸,
|
|
750
|
+
// texSize 只有真实值的 1/4:maxMip 从 log2(64)=6 掉到 log2(16.9)=4、
|
|
751
|
+
// dFdx(uv*texSize) 同比变小 → mip 几乎恒为 0 → 每像素采到随机纹素。
|
|
752
|
+
// 表现就是满屏彩色雪花,而且越远越糟(正是「不同视距观感不一致」)。
|
|
753
|
+
const packed = packTexturePackage(texturePackage);
|
|
754
|
+
const atlasRects = buildAtlasRects(geoPackage, texturePackage, packed);
|
|
755
|
+
const textures = buildTextureTable(texturePackage);
|
|
756
|
+
// pageLOD:容器提供的 LOD/子瓦片信息是权威;geodes 在内层 groupNode 里
|
|
757
|
+
const innerLods = groupNode.pageLods;
|
|
758
|
+
const mergedLods = pageLods.map((lod, i) => {
|
|
759
|
+
const inner = innerLods[i];
|
|
760
|
+
return {
|
|
761
|
+
rangeValue: lod.rangeValue,
|
|
762
|
+
rangeMode: lod.rangeMode,
|
|
763
|
+
boundingSphere: lod.boundingSphere,
|
|
764
|
+
childTile: lod.childTile,
|
|
765
|
+
geodes: inner?.geodes ?? [],
|
|
766
|
+
...(inner?.obb ? { obb: inner.obb } : {}),
|
|
767
|
+
};
|
|
768
|
+
});
|
|
769
|
+
tiles.push({
|
|
770
|
+
version,
|
|
771
|
+
compressedType,
|
|
772
|
+
options: fileOptions,
|
|
773
|
+
groupNode: { pageLods: mergedLods },
|
|
774
|
+
geoPackage,
|
|
775
|
+
textures,
|
|
776
|
+
materials,
|
|
777
|
+
pickInfo,
|
|
778
|
+
extensions,
|
|
779
|
+
bytesRead: t.offset,
|
|
780
|
+
bytesTotal: raw.byteLength,
|
|
781
|
+
blockVersion,
|
|
782
|
+
blockName: name,
|
|
783
|
+
texturePackage,
|
|
784
|
+
atlasRects,
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
return { tiles, names };
|
|
788
|
+
}
|
|
789
|
+
//# sourceMappingURL=S3MBlockParser.js.map
|