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