s3m-three 0.1.0 → 0.2.0
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/LICENSE +202 -202
- package/dist/render/DatasetManager.d.ts +10 -3
- package/dist/render/DatasetManager.d.ts.map +1 -1
- package/dist/render/DatasetManager.js +14 -5
- package/dist/render/DatasetManager.js.map +1 -1
- package/dist/render/EdgeLines.js +46 -46
- package/dist/render/GroundManager.d.ts +9 -0
- package/dist/render/GroundManager.d.ts.map +1 -1
- package/dist/render/GroundManager.js +13 -1
- package/dist/render/GroundManager.js.map +1 -1
- package/dist/render/Picker.js +66 -66
- package/dist/render/S3MViewer.d.ts +11 -3
- package/dist/render/S3MViewer.d.ts.map +1 -1
- package/dist/render/S3MViewer.js +18 -3
- package/dist/render/S3MViewer.js.map +1 -1
- package/dist/render/StyleMaterial.js +265 -265
- package/dist/vendor/meshopt_decoder.module.js +118 -118
- package/docs/API.md +20 -0
- package/package.json +1 -1
- package/src/core/BinaryReader.ts +116 -116
- package/src/core/DXTTextureDecode.ts +260 -260
- package/src/core/FeatureInfo.ts +150 -150
- package/src/core/MeshOpt.ts +401 -401
- package/src/core/S3MBParser.ts +590 -590
- package/src/core/types.ts +222 -222
- package/src/render/DatasetManager.ts +17 -6
- package/src/render/EdgeLines.ts +343 -343
- package/src/render/GeometryBuilder.ts +840 -840
- package/src/render/GroundManager.ts +14 -1
- package/src/render/Picker.ts +293 -293
- package/src/render/S3MViewer.ts +18 -3
- package/src/render/StyleMaterial.ts +602 -602
- package/src/render/TextureFactory.ts +134 -134
- package/src/tileset/S3MTileset.ts +552 -552
- package/src/vendor/meshopt_decoder.module.d.ts +31 -31
- package/src/vendor/meshopt_decoder.module.js +118 -118
- package/dist/render/CoordinateHelper.d.ts +0 -90
- package/dist/render/CoordinateHelper.d.ts.map +0 -1
- package/dist/render/CoordinateHelper.js +0 -85
- package/dist/render/CoordinateHelper.js.map +0 -1
- package/dist/render/S3MLayer.d.ts +0 -457
- package/dist/render/S3MLayer.d.ts.map +0 -1
- package/dist/render/S3MLayer.js +0 -1860
- package/dist/render/S3MLayer.js.map +0 -1
|
@@ -1,840 +1,840 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* S3M 解析结果 → three.js 可渲染对象
|
|
3
|
-
*
|
|
4
|
-
* 职责边界:这里只做「数据 → BufferGeometry / Material」的搬运,
|
|
5
|
-
* 不处理坐标系、不处理 LOD、不处理样式切换 —— 那些属于 S3MLayer。
|
|
6
|
-
*
|
|
7
|
-
* 三条约定,下游都依赖它:
|
|
8
|
-
* 1. 几何一定有 `aBatchId`(单体编号)和 `aColor`(顶点色),没有就补零/补白,
|
|
9
|
-
* 否则着色器里的 attribute 绑定会缺槽位。
|
|
10
|
-
* 2. 每个 Mesh 独享一个材质实例,但 uniform 对象是全局共享的,
|
|
11
|
-
* 所以既能逐单体高亮,又能一次改样式全场景生效。
|
|
12
|
-
* 3. `userData` 上挂 `skeletonName / geoRef / tileRef`,供拾取后反查单体属性。
|
|
13
|
-
*/
|
|
14
|
-
import * as THREE from 'three';
|
|
15
|
-
import { createTexture } from './TextureFactory.js';
|
|
16
|
-
import { createSharedUniforms, createStyledMaterial } from './StyleMaterial.js';
|
|
17
|
-
import type { StyledMaterial } from './StyleMaterial.js';
|
|
18
|
-
import type {
|
|
19
|
-
AttrDType,
|
|
20
|
-
GeoPackageEntry,
|
|
21
|
-
IndexPackage,
|
|
22
|
-
ParsedTile,
|
|
23
|
-
StyleUniforms,
|
|
24
|
-
VertexAttribute,
|
|
25
|
-
} from '../core/types.js';
|
|
26
|
-
|
|
27
|
-
/* ------------------------------------------------------------------ 索引包类型 */
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* S3M 规范里 `DrawPrimitiveType` 只到 10(多边形),但 iServer 实际发布的 v3 瓦片里
|
|
31
|
-
* 会出现 **13**:它是数据自带的「棱线包」—— 索引是线段对,不是三角形。
|
|
32
|
-
*
|
|
33
|
-
* 这条是踩过坑才认出来的:把 13 当三角形画,每条棱线都会被连成一根几十上百米的尖刺,
|
|
34
|
-
* 满屏都是放射状的三角面。按 LineSegments 画出来,正好是整片建筑轮廓线。
|
|
35
|
-
*/
|
|
36
|
-
export const LINE_PRIMITIVE = 13;
|
|
37
|
-
|
|
38
|
-
function isLinePrimitive(primitiveType: number): boolean {
|
|
39
|
-
// 1 = 点,2 = 线段,3 = 折线,13 = 棱线包
|
|
40
|
-
return primitiveType === LINE_PRIMITIVE || primitiveType === 2 || primitiveType === 3;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/* ------------------------------------------------------------------ 顶点流 */
|
|
44
|
-
|
|
45
|
-
function bytesPerElement(dtype: AttrDType): number {
|
|
46
|
-
return dtype === 'u8' ? 1 : dtype === 'u16' ? 2 : 4;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function typedArray(dtype: AttrDType, buffer: ArrayBufferLike): Float32Array | Uint8Array | Uint16Array | Uint32Array {
|
|
50
|
-
switch (dtype) {
|
|
51
|
-
case 'u8':
|
|
52
|
-
return new Uint8Array(buffer);
|
|
53
|
-
case 'u16':
|
|
54
|
-
return new Uint16Array(buffer);
|
|
55
|
-
case 'u32':
|
|
56
|
-
return new Uint32Array(buffer);
|
|
57
|
-
default:
|
|
58
|
-
return new Float32Array(buffer);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function typedArrayOf(dtype: AttrDType, length: number): Float32Array | Uint8Array | Uint16Array | Uint32Array {
|
|
63
|
-
switch (dtype) {
|
|
64
|
-
case 'u8':
|
|
65
|
-
return new Uint8Array(length);
|
|
66
|
-
case 'u16':
|
|
67
|
-
return new Uint16Array(length);
|
|
68
|
-
case 'u32':
|
|
69
|
-
return new Uint32Array(length);
|
|
70
|
-
default:
|
|
71
|
-
return new Float32Array(length);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function toBufferAttribute(attr: VertexAttribute): THREE.BufferAttribute {
|
|
76
|
-
const { data, stride, offsetInBytes, dtype = 'f32' } = attr;
|
|
77
|
-
const components = attr.components ?? (attr as unknown as { comp?: number }).comp ?? 3;
|
|
78
|
-
const bpe = bytesPerElement(dtype);
|
|
79
|
-
const itemBytes = components * bpe;
|
|
80
|
-
const actualStride = stride ?? itemBytes;
|
|
81
|
-
const actualOffset = offsetInBytes ?? 0;
|
|
82
|
-
const normalized = dtype === 'u8';
|
|
83
|
-
|
|
84
|
-
// 非交错:整段直接铺开
|
|
85
|
-
if (actualStride === itemBytes && actualOffset === 0) {
|
|
86
|
-
const view = typedArray(dtype, data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength));
|
|
87
|
-
return new THREE.BufferAttribute(view, components, normalized);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// 交错:逐元素抽取
|
|
91
|
-
const count = Math.floor(data.byteLength / actualStride);
|
|
92
|
-
const out = typedArrayOf(dtype, count * components);
|
|
93
|
-
const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
94
|
-
for (let i = 0; i < count; i++) {
|
|
95
|
-
const base = i * actualStride + actualOffset;
|
|
96
|
-
for (let c = 0; c < components; c++) {
|
|
97
|
-
const o = base + c * bpe;
|
|
98
|
-
out[i * components + c] =
|
|
99
|
-
dtype === 'u8'
|
|
100
|
-
? dv.getUint8(o)
|
|
101
|
-
: dtype === 'u16'
|
|
102
|
-
? dv.getUint16(o, true)
|
|
103
|
-
: dtype === 'u32'
|
|
104
|
-
? dv.getUint32(o, true)
|
|
105
|
-
: dv.getFloat32(o, true);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
return new THREE.BufferAttribute(out, components, normalized);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function toIndex(indexPack: IndexPackage): THREE.BufferAttribute | null {
|
|
112
|
-
const { indices, indexType, indexCount } = indexPack;
|
|
113
|
-
if (!indices || indexCount === 0) return null;
|
|
114
|
-
// 以字节宽度为准:S3M 的 indexType 在 MeshOpt 包里恒为 0,靠它会读错一半索引。
|
|
115
|
-
const width = Math.floor(indices.byteLength / indexCount);
|
|
116
|
-
const wide = width === 4 || (width !== 2 && (indexType === 1 || indexType === 3));
|
|
117
|
-
const n = indexCount * (wide ? 4 : 2);
|
|
118
|
-
if (n > indices.byteLength) return null;
|
|
119
|
-
const buf = indices.buffer.slice(indices.byteOffset, indices.byteOffset + n);
|
|
120
|
-
const view = wide ? new Uint32Array(buf) : new Uint16Array(buf);
|
|
121
|
-
return new THREE.BufferAttribute(view, 1);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/** 把 u8 顶点色转成 float vec3,供着色器直接相乘 */
|
|
125
|
-
function toColorAttribute(attr: VertexAttribute | null, vertexCount: number): THREE.BufferAttribute {
|
|
126
|
-
const out = new Float32Array(vertexCount * 3);
|
|
127
|
-
if (!attr) {
|
|
128
|
-
out.fill(1);
|
|
129
|
-
return new THREE.BufferAttribute(out, 3);
|
|
130
|
-
}
|
|
131
|
-
const src = attr.data;
|
|
132
|
-
const comp = attr.components ?? 4;
|
|
133
|
-
const scale = attr.dtype === 'u8' ? 1 / 255 : 1;
|
|
134
|
-
for (let i = 0; i < vertexCount; i++) {
|
|
135
|
-
const o = i * comp;
|
|
136
|
-
out[i * 3] = (src[o] ?? 255) * scale;
|
|
137
|
-
out[i * 3 + 1] = (src[o + 1] ?? 255) * scale;
|
|
138
|
-
out[i * 3 + 2] = (src[o + 2] ?? 255) * scale;
|
|
139
|
-
}
|
|
140
|
-
return new THREE.BufferAttribute(out, 3);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/* ------------------------------------------------------------------ 材质描述 */
|
|
144
|
-
|
|
145
|
-
interface RawMaterialUnit {
|
|
146
|
-
name?: string;
|
|
147
|
-
id?: string;
|
|
148
|
-
textureunitstate?: RawMaterialUnit;
|
|
149
|
-
texmodmatrix?: number[];
|
|
150
|
-
matrix?: number[];
|
|
151
|
-
addressmode?: { u?: number; v?: number; w?: number };
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
interface RawMaterialDef {
|
|
155
|
-
name?: string;
|
|
156
|
-
id?: string;
|
|
157
|
-
diffuse?: { r?: number; g?: number; b?: number; a?: number };
|
|
158
|
-
shininess?: number;
|
|
159
|
-
alphaMode?: string;
|
|
160
|
-
cullMode?: string;
|
|
161
|
-
transparentsorting?: boolean;
|
|
162
|
-
textureStates?: RawMaterialUnit[];
|
|
163
|
-
textureunitstates?: RawMaterialUnit[];
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
type RawMaterialEntry = RawMaterialDef & { material?: RawMaterialDef };
|
|
167
|
-
|
|
168
|
-
/** 3.x 是 `{materials:[...]}`,1.x/2.x 是 `{material:[...]}`,桌面导出偶尔直接给数组 */
|
|
169
|
-
function resolveMaterialList(doc: unknown): RawMaterialEntry[] {
|
|
170
|
-
if (!doc) return [];
|
|
171
|
-
if (Array.isArray(doc)) return doc as RawMaterialEntry[];
|
|
172
|
-
const o = doc as { materials?: unknown; material?: unknown };
|
|
173
|
-
const list = o.materials ?? o.material;
|
|
174
|
-
return Array.isArray(list) ? (list as RawMaterialEntry[]) : [];
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function unwrapMaterial(entry: RawMaterialEntry | undefined): RawMaterialDef | undefined {
|
|
178
|
-
return entry?.material ?? entry;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function materialName(m: RawMaterialDef | RawMaterialUnit | undefined): string | undefined {
|
|
182
|
-
return m?.name ?? m?.id;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function materialUnits(m: RawMaterialDef | undefined): RawMaterialUnit[] {
|
|
186
|
-
const raw = m?.textureStates ?? m?.textureunitstates ?? [];
|
|
187
|
-
return raw.map((u) => u?.textureunitstate ?? u).filter((u): u is RawMaterialUnit => !!u);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/** 判断 4x4 矩阵是否为标准单位矩阵 */
|
|
191
|
-
function isIdentityMatrix4(m: number[]): boolean {
|
|
192
|
-
if (m.length !== 16) return false;
|
|
193
|
-
return (
|
|
194
|
-
m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 0 &&
|
|
195
|
-
m[4] === 0 && m[5] === 1 && m[6] === 0 && m[7] === 0 &&
|
|
196
|
-
m[8] === 0 && m[9] === 0 && m[10] === 1 && m[11] === 0 &&
|
|
197
|
-
m[12] === 0 && m[13] === 0 && m[14] === 0 && m[15] === 1
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/** cullMode:1.x 用 none/clockwise/backward,3.x 用 CULL_NONE / CULL_BACK / CULL_FRONT */
|
|
202
|
-
function resolveSide(m: RawMaterialDef | undefined): THREE.Side {
|
|
203
|
-
const cull = String(m?.cullMode ?? '').toUpperCase();
|
|
204
|
-
if (cull === 'CULL_BACK' || cull === 'BACKWARD') return THREE.FrontSide;
|
|
205
|
-
if (cull === 'CULL_FRONT') return THREE.BackSide;
|
|
206
|
-
return THREE.DoubleSide;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/* ------------------------------------------------------------------ 入口 */
|
|
210
|
-
|
|
211
|
-
export interface TileBuildStats {
|
|
212
|
-
meshes: number;
|
|
213
|
-
vertices: number;
|
|
214
|
-
triangles: number;
|
|
215
|
-
textures: number;
|
|
216
|
-
instanced: number;
|
|
217
|
-
/** 数据自带的棱线包(primitiveType 13)数量 */
|
|
218
|
-
edgeLines: number;
|
|
219
|
-
/** 合批前原有独立网格数 */
|
|
220
|
-
originalMeshes?: number;
|
|
221
|
-
/** 被跳过的东西,加载完可以直接在控制台看 */
|
|
222
|
-
skipped: string[];
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
export interface BuildTileOptions {
|
|
226
|
-
/** 全局共享 uniform;不传则本次构建自建一份(不推荐,样式将无法全局联动) */
|
|
227
|
-
uniforms?: StyleUniforms;
|
|
228
|
-
textureCache?: Map<string, THREE.Texture>;
|
|
229
|
-
/** 是否显示数据自带的棱线包 */
|
|
230
|
-
edges?: boolean;
|
|
231
|
-
edgeColor?: THREE.ColorRepresentation;
|
|
232
|
-
/**
|
|
233
|
-
* 是否开启瓦片内同材质网格合批(默认 true)。
|
|
234
|
-
* 对倾斜摄影、大网格切片瓦片可减少 90%+ DrawCall,显著提高渲染帧率。
|
|
235
|
-
*/
|
|
236
|
-
batching?: boolean;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
export interface BuildTileResult {
|
|
240
|
-
group: THREE.Group;
|
|
241
|
-
stats: TileBuildStats;
|
|
242
|
-
objects: THREE.Object3D[];
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
interface CachedGeometry {
|
|
246
|
-
geometry: THREE.BufferGeometry;
|
|
247
|
-
pack: IndexPackage;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
interface SkeletonInfo {
|
|
251
|
-
indexPackages: IndexPackage[];
|
|
252
|
-
instanced: boolean;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
export async function buildTileObjects(
|
|
256
|
-
tile: ParsedTile,
|
|
257
|
-
options: BuildTileOptions = {}
|
|
258
|
-
): Promise<BuildTileResult> {
|
|
259
|
-
const uniforms = options.uniforms ?? createSharedUniforms();
|
|
260
|
-
const textureByCode = options.textureCache ?? new Map<string, THREE.Texture>();
|
|
261
|
-
const group = new THREE.Group();
|
|
262
|
-
const objects: THREE.Object3D[] = [];
|
|
263
|
-
const stats: TileBuildStats = {
|
|
264
|
-
meshes: 0,
|
|
265
|
-
vertices: 0,
|
|
266
|
-
triangles: 0,
|
|
267
|
-
textures: 0,
|
|
268
|
-
instanced: 0,
|
|
269
|
-
edgeLines: 0,
|
|
270
|
-
skipped: [],
|
|
271
|
-
};
|
|
272
|
-
|
|
273
|
-
// 棱线包共用一份材质,颜色随 style.edgeColor 走
|
|
274
|
-
const edgeLineMaterial = new THREE.LineBasicMaterial({
|
|
275
|
-
color: new THREE.Color(options.edgeColor ?? '#1f2937'),
|
|
276
|
-
transparent: true,
|
|
277
|
-
opacity: 0.75,
|
|
278
|
-
depthWrite: false,
|
|
279
|
-
});
|
|
280
|
-
group.userData.hasNativeEdges = false;
|
|
281
|
-
|
|
282
|
-
// 1) 纹理
|
|
283
|
-
for (const [code, tex] of Object.entries(tile.textures ?? {})) {
|
|
284
|
-
if (textureByCode.has(code)) continue;
|
|
285
|
-
const { texture, decoded } = await createTexture(tex);
|
|
286
|
-
if (texture) {
|
|
287
|
-
textureByCode.set(code, texture);
|
|
288
|
-
stats.textures++;
|
|
289
|
-
} else {
|
|
290
|
-
stats.skipped.push(`texture ${code} → ${decoded}`);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// 2) 材质描述表
|
|
295
|
-
//
|
|
296
|
-
// 注意:这里**不做**材质池化。单体状态(隐藏 / 改色 / 抬高 / 选中)是按 mesh 写在
|
|
297
|
-
// 材质自己的 uniform 上的(uBatchTex / uSelBatch),一旦多个 mesh 共用同一个材质实例,
|
|
298
|
-
// 就是「81 个 mesh 抢一份 uniform,最后写的赢」——白模瓦片里按单体操作会大面积失效。
|
|
299
|
-
// 材质对象本身很轻(program 由 customProgramCacheKey 复用),一栋一个不心疼。
|
|
300
|
-
const materialList = resolveMaterialList(tile.materials);
|
|
301
|
-
|
|
302
|
-
const makeMaterial = (code?: string): StyledMaterial => {
|
|
303
|
-
const entry = materialList.find((e) => materialName(unwrapMaterial(e)) === code);
|
|
304
|
-
const m = unwrapMaterial(entry);
|
|
305
|
-
const units = materialUnits(m);
|
|
306
|
-
const unit0 = units[0];
|
|
307
|
-
let map = unit0 ? (textureByCode.get(materialName(unit0) ?? unit0.id ?? '') ?? null) : null;
|
|
308
|
-
|
|
309
|
-
// 检查并应用 texmodmatrix 纹理变换矩阵
|
|
310
|
-
const texMod = unit0?.texmodmatrix ?? unit0?.matrix;
|
|
311
|
-
if (map && Array.isArray(texMod) && texMod.length === 16 && !isIdentityMatrix4(texMod)) {
|
|
312
|
-
map = map.clone();
|
|
313
|
-
map.matrixAutoUpdate = false;
|
|
314
|
-
// S3M texmodmatrix 为 4x4 列主序矩阵 [m0..m15],对应 2D 仿射变换:
|
|
315
|
-
// u' = m[0]*u + m[4]*v + m[12]
|
|
316
|
-
// v' = m[1]*u + m[5]*v + m[13]
|
|
317
|
-
// Three.js Matrix3.set 按行主序接收参数
|
|
318
|
-
map.matrix.set(
|
|
319
|
-
texMod[0], texMod[4], texMod[12],
|
|
320
|
-
texMod[1], texMod[5], texMod[13],
|
|
321
|
-
0, 0, 1
|
|
322
|
-
);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// 纹理寻址模式(addressmode)
|
|
326
|
-
if (map && unit0?.addressmode) {
|
|
327
|
-
const u = unit0.addressmode.u;
|
|
328
|
-
const v = unit0.addressmode.v;
|
|
329
|
-
if (u !== undefined || v !== undefined) {
|
|
330
|
-
if (map.matrixAutoUpdate) {
|
|
331
|
-
map = map.clone();
|
|
332
|
-
}
|
|
333
|
-
if (u !== undefined) map.wrapS = u === 0 ? THREE.RepeatWrapping : THREE.ClampToEdgeWrapping;
|
|
334
|
-
if (v !== undefined) map.wrapT = v === 0 ? THREE.RepeatWrapping : THREE.ClampToEdgeWrapping;
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
const diffuse = m?.diffuse ?? { r: 1, g: 1, b: 1, a: 1 };
|
|
339
|
-
const alphaMode = m?.alphaMode;
|
|
340
|
-
const transparent = alphaMode === 'AM_BLEND' || (diffuse.a ?? 1) < 0.999 || m?.transparentsorting === true;
|
|
341
|
-
|
|
342
|
-
// S3M 的 shininess 是旧式 0~128 高光指数,粗略映射到粗糙度
|
|
343
|
-
const roughness =
|
|
344
|
-
typeof m?.shininess === 'number' && m.shininess > 0 ? Math.max(0.15, 1 - m.shininess / 128) : 0.95;
|
|
345
|
-
|
|
346
|
-
const mat = createStyledMaterial({
|
|
347
|
-
uniforms,
|
|
348
|
-
map,
|
|
349
|
-
color: new THREE.Color(diffuse.r ?? 1, diffuse.g ?? 1, diffuse.b ?? 1),
|
|
350
|
-
side: resolveSide(m),
|
|
351
|
-
transparent,
|
|
352
|
-
opacity: diffuse.a ?? 1,
|
|
353
|
-
roughness,
|
|
354
|
-
});
|
|
355
|
-
return mat;
|
|
356
|
-
};
|
|
357
|
-
|
|
358
|
-
// 3) 骨架 → 几何。顶点是「骨架局部坐标」,真正位置由引用它的 Geode 矩阵决定。
|
|
359
|
-
const geoCache = new Map<string, CachedGeometry>();
|
|
360
|
-
const skeletonInfo = new Map<string, SkeletonInfo>();
|
|
361
|
-
|
|
362
|
-
for (const [name, geo] of Object.entries(tile.geoPackage ?? {})) {
|
|
363
|
-
const vp = geo.vertexPackage;
|
|
364
|
-
const positionAttr = vp.vertexAttributes.find((a) => a.semantic === 'position');
|
|
365
|
-
if (!positionAttr) {
|
|
366
|
-
stats.skipped.push(`${name} 无 position`);
|
|
367
|
-
continue;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
const vertexCount = vp.verticesCount || 0;
|
|
371
|
-
const attributes: Record<string, THREE.BufferAttribute> = { position: toBufferAttribute(positionAttr) };
|
|
372
|
-
let colorAttr: VertexAttribute | null = null;
|
|
373
|
-
let batchAttr: VertexAttribute | null = null;
|
|
374
|
-
|
|
375
|
-
for (const attr of vp.vertexAttributes) {
|
|
376
|
-
if (attr.divisor !== undefined) continue; // 实例属性不进顶点流
|
|
377
|
-
if (attr.semantic === 'position') continue;
|
|
378
|
-
if (attr.semantic === 'color') colorAttr = attr;
|
|
379
|
-
else if (attr.semantic === 'batchId') batchAttr = attr;
|
|
380
|
-
else if (attr.semantic === 'normal') attributes.normal = toBufferAttribute(attr);
|
|
381
|
-
else if (attr.semantic === 'uv') attributes.uv = toBufferAttribute(attr);
|
|
382
|
-
else if (attr.semantic === 'uv2') attributes.uv1 = toBufferAttribute(attr);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// 着色器强依赖这两个 attribute,缺了就补默认值
|
|
386
|
-
attributes.aColor = toColorAttribute(colorAttr, vertexCount);
|
|
387
|
-
attributes.aBatchId = batchAttr
|
|
388
|
-
? toBufferAttribute(batchAttr)
|
|
389
|
-
: new THREE.BufferAttribute(new Float32Array(vertexCount).fill(-1), 1);
|
|
390
|
-
|
|
391
|
-
const instanced = vp.instanceIndex !== -1 && (vp.instanceMode === 16 || vp.instanceMode === 17);
|
|
392
|
-
// 注意:indexPackages 属于 Skeleton(geoPackage[name]),不在 vertexPackage 里
|
|
393
|
-
skeletonInfo.set(name, { indexPackages: geo.indexPackages ?? [], instanced });
|
|
394
|
-
stats.vertices += vertexCount;
|
|
395
|
-
|
|
396
|
-
(geo.indexPackages ?? []).forEach((pack, pi) => {
|
|
397
|
-
const geometry = new THREE.BufferGeometry();
|
|
398
|
-
for (const [k, v] of Object.entries(attributes)) geometry.setAttribute(k, v);
|
|
399
|
-
const idx = toIndex(pack);
|
|
400
|
-
if (idx) geometry.setIndex(idx);
|
|
401
|
-
else geometry.setDrawRange(0, vertexCount);
|
|
402
|
-
|
|
403
|
-
// 如果数据本身没有法线且是三角网格,必须在绑定了 index 之后计算法线
|
|
404
|
-
// 绝不能在未索引的 attributes.position 上算,否则非连续顶点会导致法线朝向错乱甚至越界 NaN 导致死黑面
|
|
405
|
-
if (!isLinePrimitive(pack.primitiveType) && !geometry.getAttribute('normal')) {
|
|
406
|
-
geometry.computeVertexNormals();
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
geometry.computeBoundingBox();
|
|
410
|
-
geometry.computeBoundingSphere();
|
|
411
|
-
geoCache.set(`${name}|${pi}`, { geometry, pack });
|
|
412
|
-
});
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
const IDENTITY = new THREE.Matrix4();
|
|
416
|
-
|
|
417
|
-
interface CandidateMesh {
|
|
418
|
-
cached: CachedGeometry;
|
|
419
|
-
name: string;
|
|
420
|
-
geo: GeoPackageEntry;
|
|
421
|
-
matrix: THREE.Matrix4 | null;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
const batchGroups = new Map<string, CandidateMesh[]>();
|
|
425
|
-
|
|
426
|
-
const mergeBatchGeometries = (
|
|
427
|
-
candidates: CandidateMesh[],
|
|
428
|
-
material: StyledMaterial,
|
|
429
|
-
tileRef: ParsedTile
|
|
430
|
-
): THREE.Mesh => {
|
|
431
|
-
let totalVertices = 0;
|
|
432
|
-
let totalIndices = 0;
|
|
433
|
-
let hasNormal = false;
|
|
434
|
-
let hasUv = false;
|
|
435
|
-
let hasUv1 = false;
|
|
436
|
-
let hasAColor = false;
|
|
437
|
-
let hasABatchId = false;
|
|
438
|
-
|
|
439
|
-
for (const c of candidates) {
|
|
440
|
-
const g = c.cached.geometry;
|
|
441
|
-
const vCount = g.attributes.position?.count ?? 0;
|
|
442
|
-
totalVertices += vCount;
|
|
443
|
-
totalIndices += g.index ? g.index.count : vCount;
|
|
444
|
-
if (g.attributes.normal) hasNormal = true;
|
|
445
|
-
if (g.attributes.uv) hasUv = true;
|
|
446
|
-
if (g.attributes.uv1) hasUv1 = true;
|
|
447
|
-
if (g.attributes.aColor) hasAColor = true;
|
|
448
|
-
if (g.attributes.aBatchId) hasABatchId = true;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
const mergedPos = new Float32Array(totalVertices * 3);
|
|
452
|
-
const mergedNorm = hasNormal ? new Float32Array(totalVertices * 3) : null;
|
|
453
|
-
const mergedUv = hasUv ? new Float32Array(totalVertices * 2) : null;
|
|
454
|
-
const mergedUv1 = hasUv1 ? new Float32Array(totalVertices * 2) : null;
|
|
455
|
-
const mergedColor = hasAColor ? new Float32Array(totalVertices * 3) : null;
|
|
456
|
-
const mergedBatchId = hasABatchId ? new Float32Array(totalVertices) : null;
|
|
457
|
-
|
|
458
|
-
const IndexArrayType = totalVertices > 65535 ? Uint32Array : Uint16Array;
|
|
459
|
-
const mergedIndices = new IndexArrayType(totalIndices);
|
|
460
|
-
|
|
461
|
-
let vertexOffset = 0;
|
|
462
|
-
let indexOffset = 0;
|
|
463
|
-
|
|
464
|
-
const tmpVec = new THREE.Vector3();
|
|
465
|
-
const normalMatrix = new THREE.Matrix3();
|
|
466
|
-
const geoByBatchId = new Map<number, GeoPackageEntry>();
|
|
467
|
-
|
|
468
|
-
for (const c of candidates) {
|
|
469
|
-
const g = c.cached.geometry;
|
|
470
|
-
const vCount = g.attributes.position.count;
|
|
471
|
-
const m = c.matrix;
|
|
472
|
-
const hasTransform = m && !m.equals(IDENTITY);
|
|
473
|
-
if (hasTransform && hasNormal) {
|
|
474
|
-
normalMatrix.getNormalMatrix(m);
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
if (c.geo.pickInfo) {
|
|
478
|
-
for (const entry of Object.values(c.geo.pickInfo)) {
|
|
479
|
-
if (typeof entry.batchId === 'number' && entry.batchId >= 0) {
|
|
480
|
-
geoByBatchId.set(entry.batchId, c.geo);
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
// 1. Position
|
|
486
|
-
const posArr = g.attributes.position.array as ArrayLike<number>;
|
|
487
|
-
for (let i = 0; i < vCount; i++) {
|
|
488
|
-
const srcIdx = i * 3;
|
|
489
|
-
const dstIdx = (vertexOffset + i) * 3;
|
|
490
|
-
if (hasTransform) {
|
|
491
|
-
tmpVec.set(posArr[srcIdx], posArr[srcIdx + 1], posArr[srcIdx + 2]).applyMatrix4(m);
|
|
492
|
-
mergedPos[dstIdx] = tmpVec.x;
|
|
493
|
-
mergedPos[dstIdx + 1] = tmpVec.y;
|
|
494
|
-
mergedPos[dstIdx + 2] = tmpVec.z;
|
|
495
|
-
} else {
|
|
496
|
-
mergedPos[dstIdx] = posArr[srcIdx];
|
|
497
|
-
mergedPos[dstIdx + 1] = posArr[srcIdx + 1];
|
|
498
|
-
mergedPos[dstIdx + 2] = posArr[srcIdx + 2];
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
// 2. Normal
|
|
503
|
-
if (mergedNorm) {
|
|
504
|
-
const normAttr = g.attributes.normal;
|
|
505
|
-
if (normAttr) {
|
|
506
|
-
const normArr = normAttr.array as ArrayLike<number>;
|
|
507
|
-
for (let i = 0; i < vCount; i++) {
|
|
508
|
-
const srcIdx = i * 3;
|
|
509
|
-
const dstIdx = (vertexOffset + i) * 3;
|
|
510
|
-
if (hasTransform) {
|
|
511
|
-
tmpVec
|
|
512
|
-
.set(normArr[srcIdx], normArr[srcIdx + 1], normArr[srcIdx + 2])
|
|
513
|
-
.applyMatrix3(normalMatrix)
|
|
514
|
-
.normalize();
|
|
515
|
-
mergedNorm[dstIdx] = tmpVec.x;
|
|
516
|
-
mergedNorm[dstIdx + 1] = tmpVec.y;
|
|
517
|
-
mergedNorm[dstIdx + 2] = tmpVec.z;
|
|
518
|
-
} else {
|
|
519
|
-
mergedNorm[dstIdx] = normArr[srcIdx];
|
|
520
|
-
mergedNorm[dstIdx + 1] = normArr[srcIdx + 1];
|
|
521
|
-
mergedNorm[dstIdx + 2] = normArr[srcIdx + 2];
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
// 3. UV
|
|
528
|
-
if (mergedUv) {
|
|
529
|
-
const uvAttr = g.attributes.uv;
|
|
530
|
-
if (uvAttr) {
|
|
531
|
-
const uvArr = uvAttr.array as ArrayLike<number>;
|
|
532
|
-
for (let i = 0; i < vCount; i++) {
|
|
533
|
-
mergedUv[(vertexOffset + i) * 2] = uvArr[i * 2];
|
|
534
|
-
mergedUv[(vertexOffset + i) * 2 + 1] = uvArr[i * 2 + 1];
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
// 4. UV1
|
|
540
|
-
if (mergedUv1) {
|
|
541
|
-
const uv1Attr = g.attributes.uv1;
|
|
542
|
-
if (uv1Attr) {
|
|
543
|
-
const uvArr = uv1Attr.array as ArrayLike<number>;
|
|
544
|
-
for (let i = 0; i < vCount; i++) {
|
|
545
|
-
mergedUv1[(vertexOffset + i) * 2] = uvArr[i * 2];
|
|
546
|
-
mergedUv1[(vertexOffset + i) * 2 + 1] = uvArr[i * 2 + 1];
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
// 5. aColor
|
|
552
|
-
if (mergedColor) {
|
|
553
|
-
const colAttr = g.attributes.aColor;
|
|
554
|
-
if (colAttr) {
|
|
555
|
-
const colArr = colAttr.array as ArrayLike<number>;
|
|
556
|
-
for (let i = 0; i < vCount; i++) {
|
|
557
|
-
mergedColor[(vertexOffset + i) * 3] = colArr[i * 3];
|
|
558
|
-
mergedColor[(vertexOffset + i) * 3 + 1] = colArr[i * 3 + 1];
|
|
559
|
-
mergedColor[(vertexOffset + i) * 3 + 2] = colArr[i * 3 + 2];
|
|
560
|
-
}
|
|
561
|
-
} else {
|
|
562
|
-
for (let i = 0; i < vCount; i++) {
|
|
563
|
-
mergedColor[(vertexOffset + i) * 3] = 1;
|
|
564
|
-
mergedColor[(vertexOffset + i) * 3 + 1] = 1;
|
|
565
|
-
mergedColor[(vertexOffset + i) * 3 + 2] = 1;
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
// 6. aBatchId
|
|
571
|
-
if (mergedBatchId) {
|
|
572
|
-
const bAttr = g.attributes.aBatchId;
|
|
573
|
-
if (bAttr) {
|
|
574
|
-
const bArr = bAttr.array as ArrayLike<number>;
|
|
575
|
-
for (let i = 0; i < vCount; i++) {
|
|
576
|
-
mergedBatchId[vertexOffset + i] = bArr[i];
|
|
577
|
-
}
|
|
578
|
-
} else {
|
|
579
|
-
for (let i = 0; i < vCount; i++) {
|
|
580
|
-
mergedBatchId[vertexOffset + i] = -1;
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
// 7. Indices
|
|
586
|
-
if (g.index) {
|
|
587
|
-
const idxArr = g.index.array as ArrayLike<number>;
|
|
588
|
-
const iCount = g.index.count;
|
|
589
|
-
for (let k = 0; k < iCount; k++) {
|
|
590
|
-
mergedIndices[indexOffset + k] = idxArr[k] + vertexOffset;
|
|
591
|
-
}
|
|
592
|
-
indexOffset += iCount;
|
|
593
|
-
} else {
|
|
594
|
-
for (let k = 0; k < vCount; k++) {
|
|
595
|
-
mergedIndices[indexOffset + k] = k + vertexOffset;
|
|
596
|
-
}
|
|
597
|
-
indexOffset += vCount;
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
vertexOffset += vCount;
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
const mergedGeo = new THREE.BufferGeometry();
|
|
604
|
-
mergedGeo.setAttribute('position', new THREE.BufferAttribute(mergedPos, 3));
|
|
605
|
-
if (mergedNorm) mergedGeo.setAttribute('normal', new THREE.BufferAttribute(mergedNorm, 3));
|
|
606
|
-
if (mergedUv) mergedGeo.setAttribute('uv', new THREE.BufferAttribute(mergedUv, 2));
|
|
607
|
-
if (mergedUv1) mergedGeo.setAttribute('uv1', new THREE.BufferAttribute(mergedUv1, 2));
|
|
608
|
-
if (mergedColor) mergedGeo.setAttribute('aColor', new THREE.BufferAttribute(mergedColor, 3));
|
|
609
|
-
if (mergedBatchId) mergedGeo.setAttribute('aBatchId', new THREE.BufferAttribute(mergedBatchId, 1));
|
|
610
|
-
mergedGeo.setIndex(new THREE.BufferAttribute(mergedIndices, 1));
|
|
611
|
-
mergedGeo.computeBoundingBox();
|
|
612
|
-
mergedGeo.computeBoundingSphere();
|
|
613
|
-
|
|
614
|
-
const mesh = new THREE.Mesh(mergedGeo, material);
|
|
615
|
-
mesh.userData.skeletonName = candidates[0].name;
|
|
616
|
-
mesh.userData.geoRef = candidates[0].geo;
|
|
617
|
-
mesh.userData.tileRef = tileRef;
|
|
618
|
-
mesh.userData.primitiveType = 4;
|
|
619
|
-
mesh.userData.isBatched = true;
|
|
620
|
-
mesh.userData.batchedCount = candidates.length;
|
|
621
|
-
if (geoByBatchId.size > 0) {
|
|
622
|
-
mesh.userData.geoByBatchId = geoByBatchId;
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
return mesh;
|
|
626
|
-
};
|
|
627
|
-
|
|
628
|
-
const placeObject = (
|
|
629
|
-
cached: CachedGeometry,
|
|
630
|
-
name: string,
|
|
631
|
-
geo: GeoPackageEntry,
|
|
632
|
-
matrix: THREE.Matrix4 | null
|
|
633
|
-
): THREE.Object3D => {
|
|
634
|
-
const { geometry, pack } = cached;
|
|
635
|
-
const material = makeMaterial(pack.passNames?.[0]);
|
|
636
|
-
let object: THREE.Mesh | THREE.InstancedMesh;
|
|
637
|
-
|
|
638
|
-
const info = skeletonInfo.get(name);
|
|
639
|
-
const vp = geo.vertexPackage;
|
|
640
|
-
|
|
641
|
-
// 棱线包:线段,不是三角面。默认隐藏,等 setEdges(true) 再显示
|
|
642
|
-
if (isLinePrimitive(pack.primitiveType)) {
|
|
643
|
-
const lines = new THREE.LineSegments(geometry, edgeLineMaterial);
|
|
644
|
-
lines.visible = options.edges === true;
|
|
645
|
-
lines.renderOrder = 2;
|
|
646
|
-
lines.raycast = () => {};
|
|
647
|
-
lines.userData.isEdgeLines = true;
|
|
648
|
-
lines.userData.skeletonName = name;
|
|
649
|
-
if (matrix && !matrix.equals(IDENTITY)) {
|
|
650
|
-
matrix.decompose(lines.position, lines.quaternion, lines.scale);
|
|
651
|
-
}
|
|
652
|
-
group.add(lines);
|
|
653
|
-
objects.push(lines);
|
|
654
|
-
group.userData.hasNativeEdges = true;
|
|
655
|
-
stats.edgeLines++;
|
|
656
|
-
return lines;
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
if (info?.instanced && vp.instanceBuffer && vp.instanceCount > 0) {
|
|
660
|
-
// 逐实例的两条属性:
|
|
661
|
-
// aInstanceBatchId —— 单体控制位,着色器靠它查状态纹理(改色 / 隐藏 / 抬高)
|
|
662
|
-
// aInstanceIndex —— 实例序号,拾取时编码进颜色,CPU 侧再映射回 SMID
|
|
663
|
-
// 逐顶点属性 aBatchId 在这里没用(顶点数 ≠ 实例数),gpu 上按实例取值只能靠 instanced attribute。
|
|
664
|
-
if (!geometry.getAttribute('aInstanceBatchId')) {
|
|
665
|
-
const ids = vp.instanceBatchIds;
|
|
666
|
-
const batchIds =
|
|
667
|
-
ids && ids.length === vp.instanceCount ? ids.slice() : new Float32Array(vp.instanceCount).fill(-1);
|
|
668
|
-
geometry.setAttribute('aInstanceBatchId', new THREE.InstancedBufferAttribute(batchIds, 1));
|
|
669
|
-
const index = new Float32Array(vp.instanceCount);
|
|
670
|
-
for (let i = 0; i < index.length; i++) index[i] = i;
|
|
671
|
-
geometry.setAttribute('aInstanceIndex', new THREE.InstancedBufferAttribute(index, 1));
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
const instanced = new THREE.InstancedMesh(geometry, material, vp.instanceCount);
|
|
675
|
-
instanced.instanceMatrix.setUsage(THREE.StaticDrawUsage);
|
|
676
|
-
const m4 = new THREE.Matrix4();
|
|
677
|
-
const color = new THREE.Color();
|
|
678
|
-
const stride = vp.instanceMode;
|
|
679
|
-
const f32 = new Float32Array(vp.instanceBuffer.buffer, vp.instanceBuffer.byteOffset, vp.instanceCount * stride);
|
|
680
|
-
for (let i = 0; i < vp.instanceCount; i++) {
|
|
681
|
-
const o = i * stride;
|
|
682
|
-
// S3M 实例矩阵按 3x4 仿射矩阵(行主序)连续存储 12 个 float:
|
|
683
|
-
// Row 0: [r00, r01, r02, tx]
|
|
684
|
-
// Row 1: [r10, r11, r12, ty]
|
|
685
|
-
// Row 2: [r20, r21, r22, tz]
|
|
686
|
-
// Row 3: [0, 0, 0, 1] 隐式补齐
|
|
687
|
-
// 后续紧随 4 个 float 的 RGBA 颜色(stride >= 16)及可选 uint32 的 SMID(stride == 17)。
|
|
688
|
-
// 若用 fromArray 直接按列主序填充 Matrix4,会将平移分量塞进底部的投影行 (w),
|
|
689
|
-
// 造成严重的透视奇点畸变(所有顶点拉扯成放射状尖锥)。
|
|
690
|
-
m4.set(
|
|
691
|
-
f32[o + 0], f32[o + 1], f32[o + 2], f32[o + 3],
|
|
692
|
-
f32[o + 4], f32[o + 5], f32[o + 6], f32[o + 7],
|
|
693
|
-
f32[o + 8], f32[o + 9], f32[o + 10], f32[o + 11],
|
|
694
|
-
0, 0, 0, 1
|
|
695
|
-
);
|
|
696
|
-
instanced.setMatrixAt(i, m4);
|
|
697
|
-
if (stride >= 16) {
|
|
698
|
-
const r = f32[o + 12];
|
|
699
|
-
const g = f32[o + 13];
|
|
700
|
-
const b = f32[o + 14];
|
|
701
|
-
if (Number.isFinite(r) && Number.isFinite(g) && Number.isFinite(b)) {
|
|
702
|
-
color.setRGB(r, g, b);
|
|
703
|
-
instanced.setColorAt(i, color);
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
instanced.instanceMatrix.needsUpdate = true;
|
|
708
|
-
if (instanced.instanceColor) instanced.instanceColor.needsUpdate = true;
|
|
709
|
-
instanced.frustumCulled = false;
|
|
710
|
-
stats.instanced++;
|
|
711
|
-
object = instanced;
|
|
712
|
-
} else {
|
|
713
|
-
object = new THREE.Mesh(geometry, material);
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
// 用 decompose 写回 position/quaternion/scale,而不是 matrix.copy + matrixAutoUpdate=false。
|
|
717
|
-
// 后者在 three 的 updateMatrixWorld 链路里会被重算成单位矩阵,结果就是模型偏移几百米。
|
|
718
|
-
if (matrix && !matrix.equals(IDENTITY)) {
|
|
719
|
-
matrix.decompose(object.position, object.quaternion, object.scale);
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
object.userData.skeletonName = name;
|
|
723
|
-
object.userData.geoRef = geo;
|
|
724
|
-
object.userData.tileRef = tile;
|
|
725
|
-
object.userData.primitiveType = pack.primitiveType;
|
|
726
|
-
group.add(object);
|
|
727
|
-
objects.push(object);
|
|
728
|
-
|
|
729
|
-
stats.meshes++;
|
|
730
|
-
if (pack.primitiveType === 4) stats.triangles += (pack.indexCount || 0) / 3;
|
|
731
|
-
return object;
|
|
732
|
-
};
|
|
733
|
-
|
|
734
|
-
// 4) 按 Geode 摆放:一个骨架可能被多个 geode 以不同矩阵引用
|
|
735
|
-
let placed = 0;
|
|
736
|
-
for (const pageLod of tile.groupNode?.pageLods ?? []) {
|
|
737
|
-
for (const geode of pageLod.geodes ?? []) {
|
|
738
|
-
const m = geode.matrix;
|
|
739
|
-
// S3M 的 matrix4d 是列主序(OpenGL 约定),平移在第 12/13/14 位,
|
|
740
|
-
// 与 three 的 Matrix4.elements 布局一致 —— 必须用 fromArray 直接搬。
|
|
741
|
-
// 用 set() 按行主序喂进去等于转置,平移会被塞进投影行,模型直接飞上天。
|
|
742
|
-
const matrix = m && m.length === 16 ? new THREE.Matrix4().fromArray(m) : null;
|
|
743
|
-
|
|
744
|
-
for (const name of geode.skeletonNames ?? []) {
|
|
745
|
-
const info = skeletonInfo.get(name);
|
|
746
|
-
if (!info) continue;
|
|
747
|
-
const geo = tile.geoPackage[name];
|
|
748
|
-
if (!geo) continue;
|
|
749
|
-
const vp = geo.vertexPackage;
|
|
750
|
-
for (let pi = 0; pi < info.indexPackages.length; pi++) {
|
|
751
|
-
const cached = geoCache.get(`${name}|${pi}`);
|
|
752
|
-
if (!cached) continue;
|
|
753
|
-
|
|
754
|
-
// 棱线包或实例化网格:保持单独摆放
|
|
755
|
-
if (
|
|
756
|
-
isLinePrimitive(cached.pack.primitiveType) ||
|
|
757
|
-
(info.instanced && vp.instanceBuffer && vp.instanceCount > 0)
|
|
758
|
-
) {
|
|
759
|
-
placeObject(cached, name, geo, matrix);
|
|
760
|
-
placed++;
|
|
761
|
-
continue;
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
// 普通三角面网格:若开启合批,按材质进行分组收集
|
|
765
|
-
if (options.batching !== false && cached.pack.primitiveType === 4) {
|
|
766
|
-
const matKey = cached.pack.passNames?.[0] ?? '__default__';
|
|
767
|
-
let list = batchGroups.get(matKey);
|
|
768
|
-
if (!list) {
|
|
769
|
-
list = [];
|
|
770
|
-
batchGroups.set(matKey, list);
|
|
771
|
-
}
|
|
772
|
-
list.push({ cached, name, geo, matrix });
|
|
773
|
-
placed++;
|
|
774
|
-
continue;
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
placeObject(cached, name, geo, matrix);
|
|
778
|
-
placed++;
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
// 兜底:没有被任何 geode 引用的骨架,按单位矩阵渲染,避免整块消失
|
|
785
|
-
if (placed === 0) {
|
|
786
|
-
for (const [name, info] of skeletonInfo) {
|
|
787
|
-
const geo = tile.geoPackage[name];
|
|
788
|
-
if (!geo) continue;
|
|
789
|
-
const vp = geo.vertexPackage;
|
|
790
|
-
for (let pi = 0; pi < info.indexPackages.length; pi++) {
|
|
791
|
-
const cached = geoCache.get(`${name}|${pi}`);
|
|
792
|
-
if (!cached) continue;
|
|
793
|
-
|
|
794
|
-
if (
|
|
795
|
-
isLinePrimitive(cached.pack.primitiveType) ||
|
|
796
|
-
(info.instanced && vp.instanceBuffer && vp.instanceCount > 0)
|
|
797
|
-
) {
|
|
798
|
-
placeObject(cached, name, geo, null);
|
|
799
|
-
continue;
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
if (options.batching !== false && cached.pack.primitiveType === 4) {
|
|
803
|
-
const matKey = cached.pack.passNames?.[0] ?? '__default__';
|
|
804
|
-
let list = batchGroups.get(matKey);
|
|
805
|
-
if (!list) {
|
|
806
|
-
list = [];
|
|
807
|
-
batchGroups.set(matKey, list);
|
|
808
|
-
}
|
|
809
|
-
list.push({ cached, name, geo, matrix: null });
|
|
810
|
-
continue;
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
placeObject(cached, name, geo, null);
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
// 执行同材质几何合批
|
|
819
|
-
if (batchGroups.size > 0) {
|
|
820
|
-
for (const [matKey, list] of batchGroups) {
|
|
821
|
-
stats.originalMeshes = (stats.originalMeshes ?? 0) + list.length;
|
|
822
|
-
if (list.length === 1) {
|
|
823
|
-
placeObject(list[0].cached, list[0].name, list[0].geo, list[0].matrix);
|
|
824
|
-
} else {
|
|
825
|
-
const material = makeMaterial(matKey === '__default__' ? undefined : matKey);
|
|
826
|
-
const batchedMesh = mergeBatchGeometries(list, material, tile);
|
|
827
|
-
group.add(batchedMesh);
|
|
828
|
-
objects.push(batchedMesh);
|
|
829
|
-
stats.meshes++;
|
|
830
|
-
for (const item of list) {
|
|
831
|
-
stats.triangles += (item.cached.pack.indexCount || 0) / 3;
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
if (!group.children.length) stats.skipped.push('tile 无可用几何');
|
|
838
|
-
|
|
839
|
-
return { group, stats, objects };
|
|
840
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* S3M 解析结果 → three.js 可渲染对象
|
|
3
|
+
*
|
|
4
|
+
* 职责边界:这里只做「数据 → BufferGeometry / Material」的搬运,
|
|
5
|
+
* 不处理坐标系、不处理 LOD、不处理样式切换 —— 那些属于 S3MLayer。
|
|
6
|
+
*
|
|
7
|
+
* 三条约定,下游都依赖它:
|
|
8
|
+
* 1. 几何一定有 `aBatchId`(单体编号)和 `aColor`(顶点色),没有就补零/补白,
|
|
9
|
+
* 否则着色器里的 attribute 绑定会缺槽位。
|
|
10
|
+
* 2. 每个 Mesh 独享一个材质实例,但 uniform 对象是全局共享的,
|
|
11
|
+
* 所以既能逐单体高亮,又能一次改样式全场景生效。
|
|
12
|
+
* 3. `userData` 上挂 `skeletonName / geoRef / tileRef`,供拾取后反查单体属性。
|
|
13
|
+
*/
|
|
14
|
+
import * as THREE from 'three';
|
|
15
|
+
import { createTexture } from './TextureFactory.js';
|
|
16
|
+
import { createSharedUniforms, createStyledMaterial } from './StyleMaterial.js';
|
|
17
|
+
import type { StyledMaterial } from './StyleMaterial.js';
|
|
18
|
+
import type {
|
|
19
|
+
AttrDType,
|
|
20
|
+
GeoPackageEntry,
|
|
21
|
+
IndexPackage,
|
|
22
|
+
ParsedTile,
|
|
23
|
+
StyleUniforms,
|
|
24
|
+
VertexAttribute,
|
|
25
|
+
} from '../core/types.js';
|
|
26
|
+
|
|
27
|
+
/* ------------------------------------------------------------------ 索引包类型 */
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* S3M 规范里 `DrawPrimitiveType` 只到 10(多边形),但 iServer 实际发布的 v3 瓦片里
|
|
31
|
+
* 会出现 **13**:它是数据自带的「棱线包」—— 索引是线段对,不是三角形。
|
|
32
|
+
*
|
|
33
|
+
* 这条是踩过坑才认出来的:把 13 当三角形画,每条棱线都会被连成一根几十上百米的尖刺,
|
|
34
|
+
* 满屏都是放射状的三角面。按 LineSegments 画出来,正好是整片建筑轮廓线。
|
|
35
|
+
*/
|
|
36
|
+
export const LINE_PRIMITIVE = 13;
|
|
37
|
+
|
|
38
|
+
function isLinePrimitive(primitiveType: number): boolean {
|
|
39
|
+
// 1 = 点,2 = 线段,3 = 折线,13 = 棱线包
|
|
40
|
+
return primitiveType === LINE_PRIMITIVE || primitiveType === 2 || primitiveType === 3;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* ------------------------------------------------------------------ 顶点流 */
|
|
44
|
+
|
|
45
|
+
function bytesPerElement(dtype: AttrDType): number {
|
|
46
|
+
return dtype === 'u8' ? 1 : dtype === 'u16' ? 2 : 4;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function typedArray(dtype: AttrDType, buffer: ArrayBufferLike): Float32Array | Uint8Array | Uint16Array | Uint32Array {
|
|
50
|
+
switch (dtype) {
|
|
51
|
+
case 'u8':
|
|
52
|
+
return new Uint8Array(buffer);
|
|
53
|
+
case 'u16':
|
|
54
|
+
return new Uint16Array(buffer);
|
|
55
|
+
case 'u32':
|
|
56
|
+
return new Uint32Array(buffer);
|
|
57
|
+
default:
|
|
58
|
+
return new Float32Array(buffer);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function typedArrayOf(dtype: AttrDType, length: number): Float32Array | Uint8Array | Uint16Array | Uint32Array {
|
|
63
|
+
switch (dtype) {
|
|
64
|
+
case 'u8':
|
|
65
|
+
return new Uint8Array(length);
|
|
66
|
+
case 'u16':
|
|
67
|
+
return new Uint16Array(length);
|
|
68
|
+
case 'u32':
|
|
69
|
+
return new Uint32Array(length);
|
|
70
|
+
default:
|
|
71
|
+
return new Float32Array(length);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function toBufferAttribute(attr: VertexAttribute): THREE.BufferAttribute {
|
|
76
|
+
const { data, stride, offsetInBytes, dtype = 'f32' } = attr;
|
|
77
|
+
const components = attr.components ?? (attr as unknown as { comp?: number }).comp ?? 3;
|
|
78
|
+
const bpe = bytesPerElement(dtype);
|
|
79
|
+
const itemBytes = components * bpe;
|
|
80
|
+
const actualStride = stride ?? itemBytes;
|
|
81
|
+
const actualOffset = offsetInBytes ?? 0;
|
|
82
|
+
const normalized = dtype === 'u8';
|
|
83
|
+
|
|
84
|
+
// 非交错:整段直接铺开
|
|
85
|
+
if (actualStride === itemBytes && actualOffset === 0) {
|
|
86
|
+
const view = typedArray(dtype, data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength));
|
|
87
|
+
return new THREE.BufferAttribute(view, components, normalized);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 交错:逐元素抽取
|
|
91
|
+
const count = Math.floor(data.byteLength / actualStride);
|
|
92
|
+
const out = typedArrayOf(dtype, count * components);
|
|
93
|
+
const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
94
|
+
for (let i = 0; i < count; i++) {
|
|
95
|
+
const base = i * actualStride + actualOffset;
|
|
96
|
+
for (let c = 0; c < components; c++) {
|
|
97
|
+
const o = base + c * bpe;
|
|
98
|
+
out[i * components + c] =
|
|
99
|
+
dtype === 'u8'
|
|
100
|
+
? dv.getUint8(o)
|
|
101
|
+
: dtype === 'u16'
|
|
102
|
+
? dv.getUint16(o, true)
|
|
103
|
+
: dtype === 'u32'
|
|
104
|
+
? dv.getUint32(o, true)
|
|
105
|
+
: dv.getFloat32(o, true);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return new THREE.BufferAttribute(out, components, normalized);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function toIndex(indexPack: IndexPackage): THREE.BufferAttribute | null {
|
|
112
|
+
const { indices, indexType, indexCount } = indexPack;
|
|
113
|
+
if (!indices || indexCount === 0) return null;
|
|
114
|
+
// 以字节宽度为准:S3M 的 indexType 在 MeshOpt 包里恒为 0,靠它会读错一半索引。
|
|
115
|
+
const width = Math.floor(indices.byteLength / indexCount);
|
|
116
|
+
const wide = width === 4 || (width !== 2 && (indexType === 1 || indexType === 3));
|
|
117
|
+
const n = indexCount * (wide ? 4 : 2);
|
|
118
|
+
if (n > indices.byteLength) return null;
|
|
119
|
+
const buf = indices.buffer.slice(indices.byteOffset, indices.byteOffset + n);
|
|
120
|
+
const view = wide ? new Uint32Array(buf) : new Uint16Array(buf);
|
|
121
|
+
return new THREE.BufferAttribute(view, 1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** 把 u8 顶点色转成 float vec3,供着色器直接相乘 */
|
|
125
|
+
function toColorAttribute(attr: VertexAttribute | null, vertexCount: number): THREE.BufferAttribute {
|
|
126
|
+
const out = new Float32Array(vertexCount * 3);
|
|
127
|
+
if (!attr) {
|
|
128
|
+
out.fill(1);
|
|
129
|
+
return new THREE.BufferAttribute(out, 3);
|
|
130
|
+
}
|
|
131
|
+
const src = attr.data;
|
|
132
|
+
const comp = attr.components ?? 4;
|
|
133
|
+
const scale = attr.dtype === 'u8' ? 1 / 255 : 1;
|
|
134
|
+
for (let i = 0; i < vertexCount; i++) {
|
|
135
|
+
const o = i * comp;
|
|
136
|
+
out[i * 3] = (src[o] ?? 255) * scale;
|
|
137
|
+
out[i * 3 + 1] = (src[o + 1] ?? 255) * scale;
|
|
138
|
+
out[i * 3 + 2] = (src[o + 2] ?? 255) * scale;
|
|
139
|
+
}
|
|
140
|
+
return new THREE.BufferAttribute(out, 3);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* ------------------------------------------------------------------ 材质描述 */
|
|
144
|
+
|
|
145
|
+
interface RawMaterialUnit {
|
|
146
|
+
name?: string;
|
|
147
|
+
id?: string;
|
|
148
|
+
textureunitstate?: RawMaterialUnit;
|
|
149
|
+
texmodmatrix?: number[];
|
|
150
|
+
matrix?: number[];
|
|
151
|
+
addressmode?: { u?: number; v?: number; w?: number };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
interface RawMaterialDef {
|
|
155
|
+
name?: string;
|
|
156
|
+
id?: string;
|
|
157
|
+
diffuse?: { r?: number; g?: number; b?: number; a?: number };
|
|
158
|
+
shininess?: number;
|
|
159
|
+
alphaMode?: string;
|
|
160
|
+
cullMode?: string;
|
|
161
|
+
transparentsorting?: boolean;
|
|
162
|
+
textureStates?: RawMaterialUnit[];
|
|
163
|
+
textureunitstates?: RawMaterialUnit[];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
type RawMaterialEntry = RawMaterialDef & { material?: RawMaterialDef };
|
|
167
|
+
|
|
168
|
+
/** 3.x 是 `{materials:[...]}`,1.x/2.x 是 `{material:[...]}`,桌面导出偶尔直接给数组 */
|
|
169
|
+
function resolveMaterialList(doc: unknown): RawMaterialEntry[] {
|
|
170
|
+
if (!doc) return [];
|
|
171
|
+
if (Array.isArray(doc)) return doc as RawMaterialEntry[];
|
|
172
|
+
const o = doc as { materials?: unknown; material?: unknown };
|
|
173
|
+
const list = o.materials ?? o.material;
|
|
174
|
+
return Array.isArray(list) ? (list as RawMaterialEntry[]) : [];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function unwrapMaterial(entry: RawMaterialEntry | undefined): RawMaterialDef | undefined {
|
|
178
|
+
return entry?.material ?? entry;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function materialName(m: RawMaterialDef | RawMaterialUnit | undefined): string | undefined {
|
|
182
|
+
return m?.name ?? m?.id;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function materialUnits(m: RawMaterialDef | undefined): RawMaterialUnit[] {
|
|
186
|
+
const raw = m?.textureStates ?? m?.textureunitstates ?? [];
|
|
187
|
+
return raw.map((u) => u?.textureunitstate ?? u).filter((u): u is RawMaterialUnit => !!u);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** 判断 4x4 矩阵是否为标准单位矩阵 */
|
|
191
|
+
function isIdentityMatrix4(m: number[]): boolean {
|
|
192
|
+
if (m.length !== 16) return false;
|
|
193
|
+
return (
|
|
194
|
+
m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 0 &&
|
|
195
|
+
m[4] === 0 && m[5] === 1 && m[6] === 0 && m[7] === 0 &&
|
|
196
|
+
m[8] === 0 && m[9] === 0 && m[10] === 1 && m[11] === 0 &&
|
|
197
|
+
m[12] === 0 && m[13] === 0 && m[14] === 0 && m[15] === 1
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** cullMode:1.x 用 none/clockwise/backward,3.x 用 CULL_NONE / CULL_BACK / CULL_FRONT */
|
|
202
|
+
function resolveSide(m: RawMaterialDef | undefined): THREE.Side {
|
|
203
|
+
const cull = String(m?.cullMode ?? '').toUpperCase();
|
|
204
|
+
if (cull === 'CULL_BACK' || cull === 'BACKWARD') return THREE.FrontSide;
|
|
205
|
+
if (cull === 'CULL_FRONT') return THREE.BackSide;
|
|
206
|
+
return THREE.DoubleSide;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/* ------------------------------------------------------------------ 入口 */
|
|
210
|
+
|
|
211
|
+
export interface TileBuildStats {
|
|
212
|
+
meshes: number;
|
|
213
|
+
vertices: number;
|
|
214
|
+
triangles: number;
|
|
215
|
+
textures: number;
|
|
216
|
+
instanced: number;
|
|
217
|
+
/** 数据自带的棱线包(primitiveType 13)数量 */
|
|
218
|
+
edgeLines: number;
|
|
219
|
+
/** 合批前原有独立网格数 */
|
|
220
|
+
originalMeshes?: number;
|
|
221
|
+
/** 被跳过的东西,加载完可以直接在控制台看 */
|
|
222
|
+
skipped: string[];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export interface BuildTileOptions {
|
|
226
|
+
/** 全局共享 uniform;不传则本次构建自建一份(不推荐,样式将无法全局联动) */
|
|
227
|
+
uniforms?: StyleUniforms;
|
|
228
|
+
textureCache?: Map<string, THREE.Texture>;
|
|
229
|
+
/** 是否显示数据自带的棱线包 */
|
|
230
|
+
edges?: boolean;
|
|
231
|
+
edgeColor?: THREE.ColorRepresentation;
|
|
232
|
+
/**
|
|
233
|
+
* 是否开启瓦片内同材质网格合批(默认 true)。
|
|
234
|
+
* 对倾斜摄影、大网格切片瓦片可减少 90%+ DrawCall,显著提高渲染帧率。
|
|
235
|
+
*/
|
|
236
|
+
batching?: boolean;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export interface BuildTileResult {
|
|
240
|
+
group: THREE.Group;
|
|
241
|
+
stats: TileBuildStats;
|
|
242
|
+
objects: THREE.Object3D[];
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
interface CachedGeometry {
|
|
246
|
+
geometry: THREE.BufferGeometry;
|
|
247
|
+
pack: IndexPackage;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
interface SkeletonInfo {
|
|
251
|
+
indexPackages: IndexPackage[];
|
|
252
|
+
instanced: boolean;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export async function buildTileObjects(
|
|
256
|
+
tile: ParsedTile,
|
|
257
|
+
options: BuildTileOptions = {}
|
|
258
|
+
): Promise<BuildTileResult> {
|
|
259
|
+
const uniforms = options.uniforms ?? createSharedUniforms();
|
|
260
|
+
const textureByCode = options.textureCache ?? new Map<string, THREE.Texture>();
|
|
261
|
+
const group = new THREE.Group();
|
|
262
|
+
const objects: THREE.Object3D[] = [];
|
|
263
|
+
const stats: TileBuildStats = {
|
|
264
|
+
meshes: 0,
|
|
265
|
+
vertices: 0,
|
|
266
|
+
triangles: 0,
|
|
267
|
+
textures: 0,
|
|
268
|
+
instanced: 0,
|
|
269
|
+
edgeLines: 0,
|
|
270
|
+
skipped: [],
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
// 棱线包共用一份材质,颜色随 style.edgeColor 走
|
|
274
|
+
const edgeLineMaterial = new THREE.LineBasicMaterial({
|
|
275
|
+
color: new THREE.Color(options.edgeColor ?? '#1f2937'),
|
|
276
|
+
transparent: true,
|
|
277
|
+
opacity: 0.75,
|
|
278
|
+
depthWrite: false,
|
|
279
|
+
});
|
|
280
|
+
group.userData.hasNativeEdges = false;
|
|
281
|
+
|
|
282
|
+
// 1) 纹理
|
|
283
|
+
for (const [code, tex] of Object.entries(tile.textures ?? {})) {
|
|
284
|
+
if (textureByCode.has(code)) continue;
|
|
285
|
+
const { texture, decoded } = await createTexture(tex);
|
|
286
|
+
if (texture) {
|
|
287
|
+
textureByCode.set(code, texture);
|
|
288
|
+
stats.textures++;
|
|
289
|
+
} else {
|
|
290
|
+
stats.skipped.push(`texture ${code} → ${decoded}`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// 2) 材质描述表
|
|
295
|
+
//
|
|
296
|
+
// 注意:这里**不做**材质池化。单体状态(隐藏 / 改色 / 抬高 / 选中)是按 mesh 写在
|
|
297
|
+
// 材质自己的 uniform 上的(uBatchTex / uSelBatch),一旦多个 mesh 共用同一个材质实例,
|
|
298
|
+
// 就是「81 个 mesh 抢一份 uniform,最后写的赢」——白模瓦片里按单体操作会大面积失效。
|
|
299
|
+
// 材质对象本身很轻(program 由 customProgramCacheKey 复用),一栋一个不心疼。
|
|
300
|
+
const materialList = resolveMaterialList(tile.materials);
|
|
301
|
+
|
|
302
|
+
const makeMaterial = (code?: string): StyledMaterial => {
|
|
303
|
+
const entry = materialList.find((e) => materialName(unwrapMaterial(e)) === code);
|
|
304
|
+
const m = unwrapMaterial(entry);
|
|
305
|
+
const units = materialUnits(m);
|
|
306
|
+
const unit0 = units[0];
|
|
307
|
+
let map = unit0 ? (textureByCode.get(materialName(unit0) ?? unit0.id ?? '') ?? null) : null;
|
|
308
|
+
|
|
309
|
+
// 检查并应用 texmodmatrix 纹理变换矩阵
|
|
310
|
+
const texMod = unit0?.texmodmatrix ?? unit0?.matrix;
|
|
311
|
+
if (map && Array.isArray(texMod) && texMod.length === 16 && !isIdentityMatrix4(texMod)) {
|
|
312
|
+
map = map.clone();
|
|
313
|
+
map.matrixAutoUpdate = false;
|
|
314
|
+
// S3M texmodmatrix 为 4x4 列主序矩阵 [m0..m15],对应 2D 仿射变换:
|
|
315
|
+
// u' = m[0]*u + m[4]*v + m[12]
|
|
316
|
+
// v' = m[1]*u + m[5]*v + m[13]
|
|
317
|
+
// Three.js Matrix3.set 按行主序接收参数
|
|
318
|
+
map.matrix.set(
|
|
319
|
+
texMod[0], texMod[4], texMod[12],
|
|
320
|
+
texMod[1], texMod[5], texMod[13],
|
|
321
|
+
0, 0, 1
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// 纹理寻址模式(addressmode)
|
|
326
|
+
if (map && unit0?.addressmode) {
|
|
327
|
+
const u = unit0.addressmode.u;
|
|
328
|
+
const v = unit0.addressmode.v;
|
|
329
|
+
if (u !== undefined || v !== undefined) {
|
|
330
|
+
if (map.matrixAutoUpdate) {
|
|
331
|
+
map = map.clone();
|
|
332
|
+
}
|
|
333
|
+
if (u !== undefined) map.wrapS = u === 0 ? THREE.RepeatWrapping : THREE.ClampToEdgeWrapping;
|
|
334
|
+
if (v !== undefined) map.wrapT = v === 0 ? THREE.RepeatWrapping : THREE.ClampToEdgeWrapping;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const diffuse = m?.diffuse ?? { r: 1, g: 1, b: 1, a: 1 };
|
|
339
|
+
const alphaMode = m?.alphaMode;
|
|
340
|
+
const transparent = alphaMode === 'AM_BLEND' || (diffuse.a ?? 1) < 0.999 || m?.transparentsorting === true;
|
|
341
|
+
|
|
342
|
+
// S3M 的 shininess 是旧式 0~128 高光指数,粗略映射到粗糙度
|
|
343
|
+
const roughness =
|
|
344
|
+
typeof m?.shininess === 'number' && m.shininess > 0 ? Math.max(0.15, 1 - m.shininess / 128) : 0.95;
|
|
345
|
+
|
|
346
|
+
const mat = createStyledMaterial({
|
|
347
|
+
uniforms,
|
|
348
|
+
map,
|
|
349
|
+
color: new THREE.Color(diffuse.r ?? 1, diffuse.g ?? 1, diffuse.b ?? 1),
|
|
350
|
+
side: resolveSide(m),
|
|
351
|
+
transparent,
|
|
352
|
+
opacity: diffuse.a ?? 1,
|
|
353
|
+
roughness,
|
|
354
|
+
});
|
|
355
|
+
return mat;
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
// 3) 骨架 → 几何。顶点是「骨架局部坐标」,真正位置由引用它的 Geode 矩阵决定。
|
|
359
|
+
const geoCache = new Map<string, CachedGeometry>();
|
|
360
|
+
const skeletonInfo = new Map<string, SkeletonInfo>();
|
|
361
|
+
|
|
362
|
+
for (const [name, geo] of Object.entries(tile.geoPackage ?? {})) {
|
|
363
|
+
const vp = geo.vertexPackage;
|
|
364
|
+
const positionAttr = vp.vertexAttributes.find((a) => a.semantic === 'position');
|
|
365
|
+
if (!positionAttr) {
|
|
366
|
+
stats.skipped.push(`${name} 无 position`);
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const vertexCount = vp.verticesCount || 0;
|
|
371
|
+
const attributes: Record<string, THREE.BufferAttribute> = { position: toBufferAttribute(positionAttr) };
|
|
372
|
+
let colorAttr: VertexAttribute | null = null;
|
|
373
|
+
let batchAttr: VertexAttribute | null = null;
|
|
374
|
+
|
|
375
|
+
for (const attr of vp.vertexAttributes) {
|
|
376
|
+
if (attr.divisor !== undefined) continue; // 实例属性不进顶点流
|
|
377
|
+
if (attr.semantic === 'position') continue;
|
|
378
|
+
if (attr.semantic === 'color') colorAttr = attr;
|
|
379
|
+
else if (attr.semantic === 'batchId') batchAttr = attr;
|
|
380
|
+
else if (attr.semantic === 'normal') attributes.normal = toBufferAttribute(attr);
|
|
381
|
+
else if (attr.semantic === 'uv') attributes.uv = toBufferAttribute(attr);
|
|
382
|
+
else if (attr.semantic === 'uv2') attributes.uv1 = toBufferAttribute(attr);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// 着色器强依赖这两个 attribute,缺了就补默认值
|
|
386
|
+
attributes.aColor = toColorAttribute(colorAttr, vertexCount);
|
|
387
|
+
attributes.aBatchId = batchAttr
|
|
388
|
+
? toBufferAttribute(batchAttr)
|
|
389
|
+
: new THREE.BufferAttribute(new Float32Array(vertexCount).fill(-1), 1);
|
|
390
|
+
|
|
391
|
+
const instanced = vp.instanceIndex !== -1 && (vp.instanceMode === 16 || vp.instanceMode === 17);
|
|
392
|
+
// 注意:indexPackages 属于 Skeleton(geoPackage[name]),不在 vertexPackage 里
|
|
393
|
+
skeletonInfo.set(name, { indexPackages: geo.indexPackages ?? [], instanced });
|
|
394
|
+
stats.vertices += vertexCount;
|
|
395
|
+
|
|
396
|
+
(geo.indexPackages ?? []).forEach((pack, pi) => {
|
|
397
|
+
const geometry = new THREE.BufferGeometry();
|
|
398
|
+
for (const [k, v] of Object.entries(attributes)) geometry.setAttribute(k, v);
|
|
399
|
+
const idx = toIndex(pack);
|
|
400
|
+
if (idx) geometry.setIndex(idx);
|
|
401
|
+
else geometry.setDrawRange(0, vertexCount);
|
|
402
|
+
|
|
403
|
+
// 如果数据本身没有法线且是三角网格,必须在绑定了 index 之后计算法线
|
|
404
|
+
// 绝不能在未索引的 attributes.position 上算,否则非连续顶点会导致法线朝向错乱甚至越界 NaN 导致死黑面
|
|
405
|
+
if (!isLinePrimitive(pack.primitiveType) && !geometry.getAttribute('normal')) {
|
|
406
|
+
geometry.computeVertexNormals();
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
geometry.computeBoundingBox();
|
|
410
|
+
geometry.computeBoundingSphere();
|
|
411
|
+
geoCache.set(`${name}|${pi}`, { geometry, pack });
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const IDENTITY = new THREE.Matrix4();
|
|
416
|
+
|
|
417
|
+
interface CandidateMesh {
|
|
418
|
+
cached: CachedGeometry;
|
|
419
|
+
name: string;
|
|
420
|
+
geo: GeoPackageEntry;
|
|
421
|
+
matrix: THREE.Matrix4 | null;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const batchGroups = new Map<string, CandidateMesh[]>();
|
|
425
|
+
|
|
426
|
+
const mergeBatchGeometries = (
|
|
427
|
+
candidates: CandidateMesh[],
|
|
428
|
+
material: StyledMaterial,
|
|
429
|
+
tileRef: ParsedTile
|
|
430
|
+
): THREE.Mesh => {
|
|
431
|
+
let totalVertices = 0;
|
|
432
|
+
let totalIndices = 0;
|
|
433
|
+
let hasNormal = false;
|
|
434
|
+
let hasUv = false;
|
|
435
|
+
let hasUv1 = false;
|
|
436
|
+
let hasAColor = false;
|
|
437
|
+
let hasABatchId = false;
|
|
438
|
+
|
|
439
|
+
for (const c of candidates) {
|
|
440
|
+
const g = c.cached.geometry;
|
|
441
|
+
const vCount = g.attributes.position?.count ?? 0;
|
|
442
|
+
totalVertices += vCount;
|
|
443
|
+
totalIndices += g.index ? g.index.count : vCount;
|
|
444
|
+
if (g.attributes.normal) hasNormal = true;
|
|
445
|
+
if (g.attributes.uv) hasUv = true;
|
|
446
|
+
if (g.attributes.uv1) hasUv1 = true;
|
|
447
|
+
if (g.attributes.aColor) hasAColor = true;
|
|
448
|
+
if (g.attributes.aBatchId) hasABatchId = true;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const mergedPos = new Float32Array(totalVertices * 3);
|
|
452
|
+
const mergedNorm = hasNormal ? new Float32Array(totalVertices * 3) : null;
|
|
453
|
+
const mergedUv = hasUv ? new Float32Array(totalVertices * 2) : null;
|
|
454
|
+
const mergedUv1 = hasUv1 ? new Float32Array(totalVertices * 2) : null;
|
|
455
|
+
const mergedColor = hasAColor ? new Float32Array(totalVertices * 3) : null;
|
|
456
|
+
const mergedBatchId = hasABatchId ? new Float32Array(totalVertices) : null;
|
|
457
|
+
|
|
458
|
+
const IndexArrayType = totalVertices > 65535 ? Uint32Array : Uint16Array;
|
|
459
|
+
const mergedIndices = new IndexArrayType(totalIndices);
|
|
460
|
+
|
|
461
|
+
let vertexOffset = 0;
|
|
462
|
+
let indexOffset = 0;
|
|
463
|
+
|
|
464
|
+
const tmpVec = new THREE.Vector3();
|
|
465
|
+
const normalMatrix = new THREE.Matrix3();
|
|
466
|
+
const geoByBatchId = new Map<number, GeoPackageEntry>();
|
|
467
|
+
|
|
468
|
+
for (const c of candidates) {
|
|
469
|
+
const g = c.cached.geometry;
|
|
470
|
+
const vCount = g.attributes.position.count;
|
|
471
|
+
const m = c.matrix;
|
|
472
|
+
const hasTransform = m && !m.equals(IDENTITY);
|
|
473
|
+
if (hasTransform && hasNormal) {
|
|
474
|
+
normalMatrix.getNormalMatrix(m);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (c.geo.pickInfo) {
|
|
478
|
+
for (const entry of Object.values(c.geo.pickInfo)) {
|
|
479
|
+
if (typeof entry.batchId === 'number' && entry.batchId >= 0) {
|
|
480
|
+
geoByBatchId.set(entry.batchId, c.geo);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// 1. Position
|
|
486
|
+
const posArr = g.attributes.position.array as ArrayLike<number>;
|
|
487
|
+
for (let i = 0; i < vCount; i++) {
|
|
488
|
+
const srcIdx = i * 3;
|
|
489
|
+
const dstIdx = (vertexOffset + i) * 3;
|
|
490
|
+
if (hasTransform) {
|
|
491
|
+
tmpVec.set(posArr[srcIdx], posArr[srcIdx + 1], posArr[srcIdx + 2]).applyMatrix4(m);
|
|
492
|
+
mergedPos[dstIdx] = tmpVec.x;
|
|
493
|
+
mergedPos[dstIdx + 1] = tmpVec.y;
|
|
494
|
+
mergedPos[dstIdx + 2] = tmpVec.z;
|
|
495
|
+
} else {
|
|
496
|
+
mergedPos[dstIdx] = posArr[srcIdx];
|
|
497
|
+
mergedPos[dstIdx + 1] = posArr[srcIdx + 1];
|
|
498
|
+
mergedPos[dstIdx + 2] = posArr[srcIdx + 2];
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// 2. Normal
|
|
503
|
+
if (mergedNorm) {
|
|
504
|
+
const normAttr = g.attributes.normal;
|
|
505
|
+
if (normAttr) {
|
|
506
|
+
const normArr = normAttr.array as ArrayLike<number>;
|
|
507
|
+
for (let i = 0; i < vCount; i++) {
|
|
508
|
+
const srcIdx = i * 3;
|
|
509
|
+
const dstIdx = (vertexOffset + i) * 3;
|
|
510
|
+
if (hasTransform) {
|
|
511
|
+
tmpVec
|
|
512
|
+
.set(normArr[srcIdx], normArr[srcIdx + 1], normArr[srcIdx + 2])
|
|
513
|
+
.applyMatrix3(normalMatrix)
|
|
514
|
+
.normalize();
|
|
515
|
+
mergedNorm[dstIdx] = tmpVec.x;
|
|
516
|
+
mergedNorm[dstIdx + 1] = tmpVec.y;
|
|
517
|
+
mergedNorm[dstIdx + 2] = tmpVec.z;
|
|
518
|
+
} else {
|
|
519
|
+
mergedNorm[dstIdx] = normArr[srcIdx];
|
|
520
|
+
mergedNorm[dstIdx + 1] = normArr[srcIdx + 1];
|
|
521
|
+
mergedNorm[dstIdx + 2] = normArr[srcIdx + 2];
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// 3. UV
|
|
528
|
+
if (mergedUv) {
|
|
529
|
+
const uvAttr = g.attributes.uv;
|
|
530
|
+
if (uvAttr) {
|
|
531
|
+
const uvArr = uvAttr.array as ArrayLike<number>;
|
|
532
|
+
for (let i = 0; i < vCount; i++) {
|
|
533
|
+
mergedUv[(vertexOffset + i) * 2] = uvArr[i * 2];
|
|
534
|
+
mergedUv[(vertexOffset + i) * 2 + 1] = uvArr[i * 2 + 1];
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// 4. UV1
|
|
540
|
+
if (mergedUv1) {
|
|
541
|
+
const uv1Attr = g.attributes.uv1;
|
|
542
|
+
if (uv1Attr) {
|
|
543
|
+
const uvArr = uv1Attr.array as ArrayLike<number>;
|
|
544
|
+
for (let i = 0; i < vCount; i++) {
|
|
545
|
+
mergedUv1[(vertexOffset + i) * 2] = uvArr[i * 2];
|
|
546
|
+
mergedUv1[(vertexOffset + i) * 2 + 1] = uvArr[i * 2 + 1];
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// 5. aColor
|
|
552
|
+
if (mergedColor) {
|
|
553
|
+
const colAttr = g.attributes.aColor;
|
|
554
|
+
if (colAttr) {
|
|
555
|
+
const colArr = colAttr.array as ArrayLike<number>;
|
|
556
|
+
for (let i = 0; i < vCount; i++) {
|
|
557
|
+
mergedColor[(vertexOffset + i) * 3] = colArr[i * 3];
|
|
558
|
+
mergedColor[(vertexOffset + i) * 3 + 1] = colArr[i * 3 + 1];
|
|
559
|
+
mergedColor[(vertexOffset + i) * 3 + 2] = colArr[i * 3 + 2];
|
|
560
|
+
}
|
|
561
|
+
} else {
|
|
562
|
+
for (let i = 0; i < vCount; i++) {
|
|
563
|
+
mergedColor[(vertexOffset + i) * 3] = 1;
|
|
564
|
+
mergedColor[(vertexOffset + i) * 3 + 1] = 1;
|
|
565
|
+
mergedColor[(vertexOffset + i) * 3 + 2] = 1;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// 6. aBatchId
|
|
571
|
+
if (mergedBatchId) {
|
|
572
|
+
const bAttr = g.attributes.aBatchId;
|
|
573
|
+
if (bAttr) {
|
|
574
|
+
const bArr = bAttr.array as ArrayLike<number>;
|
|
575
|
+
for (let i = 0; i < vCount; i++) {
|
|
576
|
+
mergedBatchId[vertexOffset + i] = bArr[i];
|
|
577
|
+
}
|
|
578
|
+
} else {
|
|
579
|
+
for (let i = 0; i < vCount; i++) {
|
|
580
|
+
mergedBatchId[vertexOffset + i] = -1;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// 7. Indices
|
|
586
|
+
if (g.index) {
|
|
587
|
+
const idxArr = g.index.array as ArrayLike<number>;
|
|
588
|
+
const iCount = g.index.count;
|
|
589
|
+
for (let k = 0; k < iCount; k++) {
|
|
590
|
+
mergedIndices[indexOffset + k] = idxArr[k] + vertexOffset;
|
|
591
|
+
}
|
|
592
|
+
indexOffset += iCount;
|
|
593
|
+
} else {
|
|
594
|
+
for (let k = 0; k < vCount; k++) {
|
|
595
|
+
mergedIndices[indexOffset + k] = k + vertexOffset;
|
|
596
|
+
}
|
|
597
|
+
indexOffset += vCount;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
vertexOffset += vCount;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const mergedGeo = new THREE.BufferGeometry();
|
|
604
|
+
mergedGeo.setAttribute('position', new THREE.BufferAttribute(mergedPos, 3));
|
|
605
|
+
if (mergedNorm) mergedGeo.setAttribute('normal', new THREE.BufferAttribute(mergedNorm, 3));
|
|
606
|
+
if (mergedUv) mergedGeo.setAttribute('uv', new THREE.BufferAttribute(mergedUv, 2));
|
|
607
|
+
if (mergedUv1) mergedGeo.setAttribute('uv1', new THREE.BufferAttribute(mergedUv1, 2));
|
|
608
|
+
if (mergedColor) mergedGeo.setAttribute('aColor', new THREE.BufferAttribute(mergedColor, 3));
|
|
609
|
+
if (mergedBatchId) mergedGeo.setAttribute('aBatchId', new THREE.BufferAttribute(mergedBatchId, 1));
|
|
610
|
+
mergedGeo.setIndex(new THREE.BufferAttribute(mergedIndices, 1));
|
|
611
|
+
mergedGeo.computeBoundingBox();
|
|
612
|
+
mergedGeo.computeBoundingSphere();
|
|
613
|
+
|
|
614
|
+
const mesh = new THREE.Mesh(mergedGeo, material);
|
|
615
|
+
mesh.userData.skeletonName = candidates[0].name;
|
|
616
|
+
mesh.userData.geoRef = candidates[0].geo;
|
|
617
|
+
mesh.userData.tileRef = tileRef;
|
|
618
|
+
mesh.userData.primitiveType = 4;
|
|
619
|
+
mesh.userData.isBatched = true;
|
|
620
|
+
mesh.userData.batchedCount = candidates.length;
|
|
621
|
+
if (geoByBatchId.size > 0) {
|
|
622
|
+
mesh.userData.geoByBatchId = geoByBatchId;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return mesh;
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
const placeObject = (
|
|
629
|
+
cached: CachedGeometry,
|
|
630
|
+
name: string,
|
|
631
|
+
geo: GeoPackageEntry,
|
|
632
|
+
matrix: THREE.Matrix4 | null
|
|
633
|
+
): THREE.Object3D => {
|
|
634
|
+
const { geometry, pack } = cached;
|
|
635
|
+
const material = makeMaterial(pack.passNames?.[0]);
|
|
636
|
+
let object: THREE.Mesh | THREE.InstancedMesh;
|
|
637
|
+
|
|
638
|
+
const info = skeletonInfo.get(name);
|
|
639
|
+
const vp = geo.vertexPackage;
|
|
640
|
+
|
|
641
|
+
// 棱线包:线段,不是三角面。默认隐藏,等 setEdges(true) 再显示
|
|
642
|
+
if (isLinePrimitive(pack.primitiveType)) {
|
|
643
|
+
const lines = new THREE.LineSegments(geometry, edgeLineMaterial);
|
|
644
|
+
lines.visible = options.edges === true;
|
|
645
|
+
lines.renderOrder = 2;
|
|
646
|
+
lines.raycast = () => {};
|
|
647
|
+
lines.userData.isEdgeLines = true;
|
|
648
|
+
lines.userData.skeletonName = name;
|
|
649
|
+
if (matrix && !matrix.equals(IDENTITY)) {
|
|
650
|
+
matrix.decompose(lines.position, lines.quaternion, lines.scale);
|
|
651
|
+
}
|
|
652
|
+
group.add(lines);
|
|
653
|
+
objects.push(lines);
|
|
654
|
+
group.userData.hasNativeEdges = true;
|
|
655
|
+
stats.edgeLines++;
|
|
656
|
+
return lines;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (info?.instanced && vp.instanceBuffer && vp.instanceCount > 0) {
|
|
660
|
+
// 逐实例的两条属性:
|
|
661
|
+
// aInstanceBatchId —— 单体控制位,着色器靠它查状态纹理(改色 / 隐藏 / 抬高)
|
|
662
|
+
// aInstanceIndex —— 实例序号,拾取时编码进颜色,CPU 侧再映射回 SMID
|
|
663
|
+
// 逐顶点属性 aBatchId 在这里没用(顶点数 ≠ 实例数),gpu 上按实例取值只能靠 instanced attribute。
|
|
664
|
+
if (!geometry.getAttribute('aInstanceBatchId')) {
|
|
665
|
+
const ids = vp.instanceBatchIds;
|
|
666
|
+
const batchIds =
|
|
667
|
+
ids && ids.length === vp.instanceCount ? ids.slice() : new Float32Array(vp.instanceCount).fill(-1);
|
|
668
|
+
geometry.setAttribute('aInstanceBatchId', new THREE.InstancedBufferAttribute(batchIds, 1));
|
|
669
|
+
const index = new Float32Array(vp.instanceCount);
|
|
670
|
+
for (let i = 0; i < index.length; i++) index[i] = i;
|
|
671
|
+
geometry.setAttribute('aInstanceIndex', new THREE.InstancedBufferAttribute(index, 1));
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const instanced = new THREE.InstancedMesh(geometry, material, vp.instanceCount);
|
|
675
|
+
instanced.instanceMatrix.setUsage(THREE.StaticDrawUsage);
|
|
676
|
+
const m4 = new THREE.Matrix4();
|
|
677
|
+
const color = new THREE.Color();
|
|
678
|
+
const stride = vp.instanceMode;
|
|
679
|
+
const f32 = new Float32Array(vp.instanceBuffer.buffer, vp.instanceBuffer.byteOffset, vp.instanceCount * stride);
|
|
680
|
+
for (let i = 0; i < vp.instanceCount; i++) {
|
|
681
|
+
const o = i * stride;
|
|
682
|
+
// S3M 实例矩阵按 3x4 仿射矩阵(行主序)连续存储 12 个 float:
|
|
683
|
+
// Row 0: [r00, r01, r02, tx]
|
|
684
|
+
// Row 1: [r10, r11, r12, ty]
|
|
685
|
+
// Row 2: [r20, r21, r22, tz]
|
|
686
|
+
// Row 3: [0, 0, 0, 1] 隐式补齐
|
|
687
|
+
// 后续紧随 4 个 float 的 RGBA 颜色(stride >= 16)及可选 uint32 的 SMID(stride == 17)。
|
|
688
|
+
// 若用 fromArray 直接按列主序填充 Matrix4,会将平移分量塞进底部的投影行 (w),
|
|
689
|
+
// 造成严重的透视奇点畸变(所有顶点拉扯成放射状尖锥)。
|
|
690
|
+
m4.set(
|
|
691
|
+
f32[o + 0], f32[o + 1], f32[o + 2], f32[o + 3],
|
|
692
|
+
f32[o + 4], f32[o + 5], f32[o + 6], f32[o + 7],
|
|
693
|
+
f32[o + 8], f32[o + 9], f32[o + 10], f32[o + 11],
|
|
694
|
+
0, 0, 0, 1
|
|
695
|
+
);
|
|
696
|
+
instanced.setMatrixAt(i, m4);
|
|
697
|
+
if (stride >= 16) {
|
|
698
|
+
const r = f32[o + 12];
|
|
699
|
+
const g = f32[o + 13];
|
|
700
|
+
const b = f32[o + 14];
|
|
701
|
+
if (Number.isFinite(r) && Number.isFinite(g) && Number.isFinite(b)) {
|
|
702
|
+
color.setRGB(r, g, b);
|
|
703
|
+
instanced.setColorAt(i, color);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
instanced.instanceMatrix.needsUpdate = true;
|
|
708
|
+
if (instanced.instanceColor) instanced.instanceColor.needsUpdate = true;
|
|
709
|
+
instanced.frustumCulled = false;
|
|
710
|
+
stats.instanced++;
|
|
711
|
+
object = instanced;
|
|
712
|
+
} else {
|
|
713
|
+
object = new THREE.Mesh(geometry, material);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// 用 decompose 写回 position/quaternion/scale,而不是 matrix.copy + matrixAutoUpdate=false。
|
|
717
|
+
// 后者在 three 的 updateMatrixWorld 链路里会被重算成单位矩阵,结果就是模型偏移几百米。
|
|
718
|
+
if (matrix && !matrix.equals(IDENTITY)) {
|
|
719
|
+
matrix.decompose(object.position, object.quaternion, object.scale);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
object.userData.skeletonName = name;
|
|
723
|
+
object.userData.geoRef = geo;
|
|
724
|
+
object.userData.tileRef = tile;
|
|
725
|
+
object.userData.primitiveType = pack.primitiveType;
|
|
726
|
+
group.add(object);
|
|
727
|
+
objects.push(object);
|
|
728
|
+
|
|
729
|
+
stats.meshes++;
|
|
730
|
+
if (pack.primitiveType === 4) stats.triangles += (pack.indexCount || 0) / 3;
|
|
731
|
+
return object;
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
// 4) 按 Geode 摆放:一个骨架可能被多个 geode 以不同矩阵引用
|
|
735
|
+
let placed = 0;
|
|
736
|
+
for (const pageLod of tile.groupNode?.pageLods ?? []) {
|
|
737
|
+
for (const geode of pageLod.geodes ?? []) {
|
|
738
|
+
const m = geode.matrix;
|
|
739
|
+
// S3M 的 matrix4d 是列主序(OpenGL 约定),平移在第 12/13/14 位,
|
|
740
|
+
// 与 three 的 Matrix4.elements 布局一致 —— 必须用 fromArray 直接搬。
|
|
741
|
+
// 用 set() 按行主序喂进去等于转置,平移会被塞进投影行,模型直接飞上天。
|
|
742
|
+
const matrix = m && m.length === 16 ? new THREE.Matrix4().fromArray(m) : null;
|
|
743
|
+
|
|
744
|
+
for (const name of geode.skeletonNames ?? []) {
|
|
745
|
+
const info = skeletonInfo.get(name);
|
|
746
|
+
if (!info) continue;
|
|
747
|
+
const geo = tile.geoPackage[name];
|
|
748
|
+
if (!geo) continue;
|
|
749
|
+
const vp = geo.vertexPackage;
|
|
750
|
+
for (let pi = 0; pi < info.indexPackages.length; pi++) {
|
|
751
|
+
const cached = geoCache.get(`${name}|${pi}`);
|
|
752
|
+
if (!cached) continue;
|
|
753
|
+
|
|
754
|
+
// 棱线包或实例化网格:保持单独摆放
|
|
755
|
+
if (
|
|
756
|
+
isLinePrimitive(cached.pack.primitiveType) ||
|
|
757
|
+
(info.instanced && vp.instanceBuffer && vp.instanceCount > 0)
|
|
758
|
+
) {
|
|
759
|
+
placeObject(cached, name, geo, matrix);
|
|
760
|
+
placed++;
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// 普通三角面网格:若开启合批,按材质进行分组收集
|
|
765
|
+
if (options.batching !== false && cached.pack.primitiveType === 4) {
|
|
766
|
+
const matKey = cached.pack.passNames?.[0] ?? '__default__';
|
|
767
|
+
let list = batchGroups.get(matKey);
|
|
768
|
+
if (!list) {
|
|
769
|
+
list = [];
|
|
770
|
+
batchGroups.set(matKey, list);
|
|
771
|
+
}
|
|
772
|
+
list.push({ cached, name, geo, matrix });
|
|
773
|
+
placed++;
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
placeObject(cached, name, geo, matrix);
|
|
778
|
+
placed++;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// 兜底:没有被任何 geode 引用的骨架,按单位矩阵渲染,避免整块消失
|
|
785
|
+
if (placed === 0) {
|
|
786
|
+
for (const [name, info] of skeletonInfo) {
|
|
787
|
+
const geo = tile.geoPackage[name];
|
|
788
|
+
if (!geo) continue;
|
|
789
|
+
const vp = geo.vertexPackage;
|
|
790
|
+
for (let pi = 0; pi < info.indexPackages.length; pi++) {
|
|
791
|
+
const cached = geoCache.get(`${name}|${pi}`);
|
|
792
|
+
if (!cached) continue;
|
|
793
|
+
|
|
794
|
+
if (
|
|
795
|
+
isLinePrimitive(cached.pack.primitiveType) ||
|
|
796
|
+
(info.instanced && vp.instanceBuffer && vp.instanceCount > 0)
|
|
797
|
+
) {
|
|
798
|
+
placeObject(cached, name, geo, null);
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
if (options.batching !== false && cached.pack.primitiveType === 4) {
|
|
803
|
+
const matKey = cached.pack.passNames?.[0] ?? '__default__';
|
|
804
|
+
let list = batchGroups.get(matKey);
|
|
805
|
+
if (!list) {
|
|
806
|
+
list = [];
|
|
807
|
+
batchGroups.set(matKey, list);
|
|
808
|
+
}
|
|
809
|
+
list.push({ cached, name, geo, matrix: null });
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
placeObject(cached, name, geo, null);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// 执行同材质几何合批
|
|
819
|
+
if (batchGroups.size > 0) {
|
|
820
|
+
for (const [matKey, list] of batchGroups) {
|
|
821
|
+
stats.originalMeshes = (stats.originalMeshes ?? 0) + list.length;
|
|
822
|
+
if (list.length === 1) {
|
|
823
|
+
placeObject(list[0].cached, list[0].name, list[0].geo, list[0].matrix);
|
|
824
|
+
} else {
|
|
825
|
+
const material = makeMaterial(matKey === '__default__' ? undefined : matKey);
|
|
826
|
+
const batchedMesh = mergeBatchGeometries(list, material, tile);
|
|
827
|
+
group.add(batchedMesh);
|
|
828
|
+
objects.push(batchedMesh);
|
|
829
|
+
stats.meshes++;
|
|
830
|
+
for (const item of list) {
|
|
831
|
+
stats.triangles += (item.cached.pack.indexCount || 0) / 3;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
if (!group.children.length) stats.skipped.push('tile 无可用几何');
|
|
838
|
+
|
|
839
|
+
return { group, stats, objects };
|
|
840
|
+
}
|