s3m-three 0.4.25 → 0.4.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser/assets/{s3mWorker-jc9Oly5K.js → s3mWorker-BfSBNvF5.js} +1 -1
- package/dist/browser/s3m-three.iife.ext-three.js +119 -36
- package/dist/browser/s3m-three.iife.js +119 -36
- package/dist/core/BakeMercator.d.ts +54 -0
- package/dist/core/BakeMercator.d.ts.map +1 -0
- package/dist/core/BakeMercator.js +65 -0
- package/dist/core/BakeMercator.js.map +1 -0
- package/dist/core/CustomAttributes.d.ts +7 -0
- package/dist/core/CustomAttributes.d.ts.map +1 -0
- package/dist/core/CustomAttributes.js +48 -0
- package/dist/core/CustomAttributes.js.map +1 -0
- package/dist/core/Draco.d.ts.map +1 -1
- package/dist/core/Draco.js +149 -2
- package/dist/core/Draco.js.map +1 -1
- package/dist/core/MeshOpt.d.ts.map +1 -1
- package/dist/core/MeshOpt.js +2 -0
- package/dist/core/MeshOpt.js.map +1 -1
- package/dist/core/S3M1Parser.d.ts.map +1 -1
- package/dist/core/S3M1Parser.js +114 -27
- package/dist/core/S3M1Parser.js.map +1 -1
- package/dist/core/S3MBParser.d.ts +5 -2
- package/dist/core/S3MBParser.d.ts.map +1 -1
- package/dist/core/S3MBParser.js +27 -12
- package/dist/core/S3MBParser.js.map +1 -1
- package/dist/core/TileGeometry.d.ts +8 -0
- package/dist/core/TileGeometry.d.ts.map +1 -1
- package/dist/core/TileGeometry.js +45 -7
- package/dist/core/TileGeometry.js.map +1 -1
- package/dist/core/types.d.ts +1 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/render/DatasetManager.d.ts +27 -0
- package/dist/render/DatasetManager.d.ts.map +1 -1
- package/dist/render/DatasetManager.js +18 -4
- package/dist/render/DatasetManager.js.map +1 -1
- package/dist/render/GeoAdapter.d.ts +49 -24
- package/dist/render/GeoAdapter.d.ts.map +1 -1
- package/dist/render/GeoAdapter.js +96 -8
- package/dist/render/GeoAdapter.js.map +1 -1
- package/dist/render/GeometryBuilder.d.ts +17 -0
- package/dist/render/GeometryBuilder.d.ts.map +1 -1
- package/dist/render/GeometryBuilder.js +67 -4
- package/dist/render/GeometryBuilder.js.map +1 -1
- package/dist/render/MotionBlurPass.d.ts +130 -0
- package/dist/render/MotionBlurPass.d.ts.map +1 -0
- package/dist/render/MotionBlurPass.js +394 -0
- package/dist/render/MotionBlurPass.js.map +1 -0
- package/dist/render/S3MViewer.d.ts +99 -2
- package/dist/render/S3MViewer.d.ts.map +1 -1
- package/dist/render/S3MViewer.js +345 -13
- package/dist/render/S3MViewer.js.map +1 -1
- package/dist/render/StyleMaterial.d.ts +5 -0
- package/dist/render/StyleMaterial.d.ts.map +1 -1
- package/dist/render/StyleMaterial.js +21 -0
- package/dist/render/StyleMaterial.js.map +1 -1
- package/dist/render/tilePlacement.d.ts +84 -0
- package/dist/render/tilePlacement.d.ts.map +1 -0
- package/dist/render/tilePlacement.js +71 -0
- package/dist/render/tilePlacement.js.map +1 -0
- package/dist/render/viewer/EdgeDecorator.d.ts +1 -0
- package/dist/render/viewer/EdgeDecorator.d.ts.map +1 -1
- package/dist/render/viewer/EdgeDecorator.js +3 -0
- package/dist/render/viewer/EdgeDecorator.js.map +1 -1
- package/dist/render/viewer/PickService.d.ts +13 -0
- package/dist/render/viewer/PickService.d.ts.map +1 -1
- package/dist/render/viewer/PickService.js +24 -15
- package/dist/render/viewer/PickService.js.map +1 -1
- package/dist/render/viewer/types.d.ts +75 -0
- package/dist/render/viewer/types.d.ts.map +1 -1
- package/dist/render/viewer/types.js.map +1 -1
- package/dist/tileset/S3MTileset.d.ts +62 -1
- package/dist/tileset/S3MTileset.d.ts.map +1 -1
- package/dist/tileset/S3MTileset.js +273 -6
- package/dist/tileset/S3MTileset.js.map +1 -1
- package/dist/worker/S3MWorkerClient.d.ts +1 -1
- package/dist/worker/S3MWorkerClient.d.ts.map +1 -1
- package/dist/worker/S3MWorkerClient.js +2 -2
- package/dist/worker/S3MWorkerClient.js.map +1 -1
- package/dist/worker/s3mWorker.js +844 -635
- package/dist/worker/s3mWorker.js.map +1 -1
- package/docs/API.md +36 -5
- package/docs/FORMAT-NOTES.md +10 -0
- package/package.json +1 -1
- package/src/core/BakeMercator.ts +96 -0
- package/src/core/CustomAttributes.ts +53 -0
- package/src/core/Draco.ts +168 -6
- package/src/core/MeshOpt.ts +2 -0
- package/src/core/S3M1Parser.ts +102 -22
- package/src/core/S3MBParser.ts +33 -14
- package/src/core/TileGeometry.ts +60 -7
- package/src/core/types.ts +1 -0
- package/src/index.ts +15 -3
- package/src/render/DatasetManager.ts +43 -5
- package/src/render/GeoAdapter.ts +139 -11
- package/src/render/GeometryBuilder.ts +90 -4
- package/src/render/MotionBlurPass.ts +453 -0
- package/src/render/S3MViewer.ts +384 -17
- package/src/render/StyleMaterial.ts +26 -0
- package/src/render/tilePlacement.ts +121 -0
- package/src/render/viewer/EdgeDecorator.ts +3 -0
- package/src/render/viewer/PickService.ts +44 -16
- package/src/render/viewer/types.ts +73 -0
- package/src/tileset/S3MTileset.ts +300 -6
- package/src/vendor/draco_decoder.module.d.ts +15 -1
- package/src/worker/S3MWorkerClient.ts +7 -2
- package/src/worker/s3mWorker.ts +6 -2
package/src/core/S3M1Parser.ts
CHANGED
|
@@ -225,14 +225,45 @@ interface MeshRecord {
|
|
|
225
225
|
position: Float32Array;
|
|
226
226
|
uv: Float32Array;
|
|
227
227
|
indexCount: number;
|
|
228
|
-
indices: Uint16Array;
|
|
228
|
+
indices: Uint16Array | Uint32Array;
|
|
229
|
+
primitiveType: number;
|
|
229
230
|
materialId: string;
|
|
230
231
|
/** 实例矩阵(每实例 16 float:3×4 行主序 + RGBA),没有实例化时为 undefined */
|
|
231
232
|
instances?: Float32Array;
|
|
232
233
|
}
|
|
233
234
|
|
|
234
|
-
/**
|
|
235
|
-
|
|
235
|
+
/**
|
|
236
|
+
* 校验 p 偏移处是否为合法的 S3M 1.x 索引头:
|
|
237
|
+
* dword[0] = 1 (idxKind)
|
|
238
|
+
* dword[1] = indexCount
|
|
239
|
+
* dword[2] = mark,按字节小端存储:
|
|
240
|
+
* byte 0: indexType (0=u16, 1/3=u32)
|
|
241
|
+
* byte 1: useIndex (恒 1)
|
|
242
|
+
* byte 2: primitiveType (1=POINTS, 2=LINES, 3=LINE_STRIP, 4=TRIANGLES, 5/6=STRIP/FAN, 13=EDGES)
|
|
243
|
+
* byte 3: reserved (恒 0)
|
|
244
|
+
* 校验紧随其后的索引数据(4 字节对齐)之后,记录尾标记恒为 1。
|
|
245
|
+
*/
|
|
246
|
+
function isValidIndexHeader(view: DataView, p: number): boolean {
|
|
247
|
+
if (view.getUint32(p, true) !== 1) return false;
|
|
248
|
+
const count = view.getUint32(p + 4, true);
|
|
249
|
+
if (count === 0 || count > 10_000_000) return false;
|
|
250
|
+
const mark = view.getUint32(p + 8, true);
|
|
251
|
+
const indexType = mark & 0xff;
|
|
252
|
+
const useIndex = (mark >> 8) & 0xff;
|
|
253
|
+
const primitiveType = (mark >> 16) & 0xff;
|
|
254
|
+
const reserved = (mark >> 24) & 0xff;
|
|
255
|
+
if (useIndex !== 1 || reserved !== 0) return false;
|
|
256
|
+
if (indexType !== 0 && indexType !== 1 && indexType !== 3) return false;
|
|
257
|
+
if (![1, 2, 3, 4, 5, 6, 13].includes(primitiveType)) return false;
|
|
258
|
+
if (primitiveType === 4 && count % 3 !== 0) return false;
|
|
259
|
+
if (primitiveType === 2 && count % 2 !== 0) return false;
|
|
260
|
+
|
|
261
|
+
const wide = indexType === 1 || indexType === 3;
|
|
262
|
+
const indexBytes = count * (wide ? 4 : 2);
|
|
263
|
+
const tailOffset = (p + 12 + indexBytes + 3) & ~3;
|
|
264
|
+
if (tailOffset + 8 > view.byteLength) return false;
|
|
265
|
+
return view.getUint32(tailOffset, true) === 1;
|
|
266
|
+
}
|
|
236
267
|
|
|
237
268
|
/**
|
|
238
269
|
* v1 里每个实例记录固定 20 个 float(80 字节):
|
|
@@ -321,11 +352,7 @@ function readInstances(
|
|
|
321
352
|
function findIndexHeader(view: DataView, from: number): number {
|
|
322
353
|
const limit = view.byteLength - 12;
|
|
323
354
|
for (let p = from; p <= limit; p += 4) {
|
|
324
|
-
if (view
|
|
325
|
-
const count = view.getUint32(p + 4, true);
|
|
326
|
-
if (count === 0 || count % 3 !== 0) continue;
|
|
327
|
-
if (view.getUint32(p + 8, true) !== INDEX_HEADER_MARK) continue;
|
|
328
|
-
return p;
|
|
355
|
+
if (isValidIndexHeader(view, p)) return p;
|
|
329
356
|
}
|
|
330
357
|
return -1;
|
|
331
358
|
}
|
|
@@ -389,6 +416,39 @@ function applyQuantBox(position: Float32Array, box: Float32Array): boolean {
|
|
|
389
416
|
return true;
|
|
390
417
|
}
|
|
391
418
|
|
|
419
|
+
/**
|
|
420
|
+
* 从版本 1 属性块中尝试提取 UV 坐标:
|
|
421
|
+
* 倾斜摄影(OSGB)等数据中,UV 块通常紧贴在索引头前:
|
|
422
|
+
* 头: [count: u32, dim: u32 = 2]
|
|
423
|
+
* 数据: V * 8 字节 float32 (u, v)
|
|
424
|
+
*/
|
|
425
|
+
function findV1UV(view: DataView, from: number, to: number, V: number): Float32Array | null {
|
|
426
|
+
const head = to - V * 8 - 8;
|
|
427
|
+
if (head >= from && view.getUint32(head, true) === V && view.getUint32(head + 4, true) === 2) {
|
|
428
|
+
let uv = new Float32Array(V * 2);
|
|
429
|
+
for (let i = 0; i < V * 2; i++) {
|
|
430
|
+
uv[i] = view.getFloat32(head + 8 + i * 4, true);
|
|
431
|
+
}
|
|
432
|
+
return uv;
|
|
433
|
+
}
|
|
434
|
+
for (let p = to - V * 8 - 8; p >= from; p -= 4) {
|
|
435
|
+
if (view.getUint32(p, true) === V && view.getUint32(p + 4, true) === 2) {
|
|
436
|
+
let uv = new Float32Array(V * 2);
|
|
437
|
+
let ok = true;
|
|
438
|
+
for (let i = 0; i < V * 2; i++) {
|
|
439
|
+
const val = view.getFloat32(p + 8 + i * 4, true);
|
|
440
|
+
if (!Number.isFinite(val) || Math.abs(val) > 1e4) {
|
|
441
|
+
ok = false;
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
uv[i] = val;
|
|
445
|
+
}
|
|
446
|
+
if (ok) return uv;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
|
|
392
452
|
function readGeometry(
|
|
393
453
|
br: BinaryReader,
|
|
394
454
|
geoSize: number
|
|
@@ -433,7 +493,7 @@ function readGeometry(
|
|
|
433
493
|
position[i * 3 + 2] = z;
|
|
434
494
|
}
|
|
435
495
|
|
|
436
|
-
|
|
496
|
+
let uv = new Float32Array(V * 2);
|
|
437
497
|
let instances: Float32Array | undefined;
|
|
438
498
|
if (v0UvAt16) {
|
|
439
499
|
// 版本 0:UV 数组头 [0,0,0,kind=1,count=V,flag=2]
|
|
@@ -456,7 +516,12 @@ function readGeometry(
|
|
|
456
516
|
throw new Error(`[s3m] S3M 1.x 属性段无法定位索引头:网格 ${geoName}(偏移 ${br.offset}/${geoSize})`);
|
|
457
517
|
}
|
|
458
518
|
if (at !== br.offset) {
|
|
459
|
-
|
|
519
|
+
const foundUv = findV1UV(br.view, br.offset, at, V);
|
|
520
|
+
if (foundUv) {
|
|
521
|
+
uv = foundUv;
|
|
522
|
+
} else {
|
|
523
|
+
skippedAttrs++;
|
|
524
|
+
}
|
|
460
525
|
// 优先认实例化:柱状图是真·实例化的(一份模板 + N 个变换矩阵),
|
|
461
526
|
// 顶点本来就该保持单位盒,靠矩阵摆位/缩放/上色。
|
|
462
527
|
const inst = readInstances(br.view, br.offset, at);
|
|
@@ -471,15 +536,29 @@ function readGeometry(
|
|
|
471
536
|
}
|
|
472
537
|
}
|
|
473
538
|
|
|
474
|
-
// 索引头 [1, indexCount,
|
|
539
|
+
// 索引头 [1, indexCount, mark]
|
|
475
540
|
const idxKind = br.u32();
|
|
476
541
|
const indexCount = br.u32();
|
|
477
|
-
br.u32();
|
|
478
|
-
|
|
479
|
-
|
|
542
|
+
const mark = br.u32();
|
|
543
|
+
const indexType = mark & 0xff;
|
|
544
|
+
const useIndex = (mark >> 8) & 0xff;
|
|
545
|
+
const primitiveType = (mark >> 16) & 0xff;
|
|
546
|
+
if (idxKind !== 1 || indexCount === 0 || useIndex !== 1) {
|
|
547
|
+
throw new Error(`[s3m] S3M 1.x 索引头不符:网格 ${geoName} kind=${idxKind} count=${indexCount} mark=0x${mark.toString(16)}`);
|
|
548
|
+
}
|
|
549
|
+
if (primitiveType === 4 && indexCount % 3 !== 0) {
|
|
550
|
+
throw new Error(`[s3m] S3M 1.x 索引头不符:网格 ${geoName} 三角形索引数 ${indexCount} 非 3 的倍数`);
|
|
551
|
+
}
|
|
552
|
+
if (primitiveType === 2 && indexCount % 2 !== 0) {
|
|
553
|
+
throw new Error(`[s3m] S3M 1.x 索引头不符:网格 ${geoName} 线段索引数 ${indexCount} 非 2 的倍数`);
|
|
554
|
+
}
|
|
555
|
+
const wide = indexType === 1 || indexType === 3;
|
|
556
|
+
const indices = wide ? new Uint32Array(indexCount) : new Uint16Array(indexCount);
|
|
557
|
+
if (wide) {
|
|
558
|
+
for (let i = 0; i < indexCount; i++) indices[i] = br.u32();
|
|
559
|
+
} else {
|
|
560
|
+
for (let i = 0; i < indexCount; i++) indices[i] = br.u16();
|
|
480
561
|
}
|
|
481
|
-
const indices = new Uint16Array(indexCount);
|
|
482
|
-
for (let i = 0; i < indexCount; i++) indices[i] = br.u16();
|
|
483
562
|
if (br.offset > geoSize) throw new Error(`[s3m] S3M 1.x 索引越界:网格 ${geoName}`);
|
|
484
563
|
br.align4();
|
|
485
564
|
|
|
@@ -495,6 +574,7 @@ function readGeometry(
|
|
|
495
574
|
uv,
|
|
496
575
|
indexCount,
|
|
497
576
|
indices,
|
|
577
|
+
primitiveType,
|
|
498
578
|
materialId,
|
|
499
579
|
...(instances ? { instances } : {}),
|
|
500
580
|
});
|
|
@@ -511,10 +591,10 @@ function readGeometry(
|
|
|
511
591
|
*/
|
|
512
592
|
export function pixelFormatOfBpp(bpp: number, rawFormat: number): number {
|
|
513
593
|
if (!Number.isFinite(bpp) || bpp <= 0) return rawFormat;
|
|
514
|
-
if (Math.abs(bpp - 0.5) < 0.
|
|
515
|
-
if (Math.abs(bpp - 1) < 0.
|
|
516
|
-
if (Math.abs(bpp - 4) < 0.
|
|
517
|
-
if (Math.abs(bpp - 3) < 0.
|
|
594
|
+
if (Math.abs(bpp - 0.5) < 0.05 || (bpp >= 0.6 && bpp <= 0.75)) return 17; // DXT1(含全 mip 链 ~0.667 B/px)
|
|
595
|
+
if (Math.abs(bpp - 1) < 0.05 || (bpp >= 1.25 && bpp <= 1.5)) return 21; // DXT5(含全 mip 链 ~1.333~1.44 B/px)
|
|
596
|
+
if (Math.abs(bpp - 4) < 0.05) return 14; // BGRA
|
|
597
|
+
if (Math.abs(bpp - 3) < 0.05) return 10; // RGB
|
|
518
598
|
console.warn(`[s3m] S3M 1.x 纹理密度 ${bpp.toFixed(3)} B/px 不在已知档位,按原 format=${rawFormat} 交给上层判定`);
|
|
519
599
|
return rawFormat;
|
|
520
600
|
}
|
|
@@ -668,9 +748,9 @@ export async function parseS3M1(arrayBuffer: ArrayBuffer, options: ParseS3M1Opti
|
|
|
668
748
|
}
|
|
669
749
|
const pack: IndexPackage = {
|
|
670
750
|
indexCount: mesh.indexCount,
|
|
671
|
-
indexType:
|
|
751
|
+
indexType: mesh.indices instanceof Uint32Array ? 1 : 0,
|
|
672
752
|
useIndex: 1,
|
|
673
|
-
primitiveType: 4,
|
|
753
|
+
primitiveType: mesh.primitiveType ?? 4,
|
|
674
754
|
indices: new Uint8Array(mesh.indices.buffer, mesh.indices.byteOffset, mesh.indices.byteLength),
|
|
675
755
|
passNames: [mesh.materialId],
|
|
676
756
|
};
|
package/src/core/S3MBParser.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { BinaryReader } from './BinaryReader.js';
|
|
13
13
|
import { parseDracoGeometry } from './Draco.js';
|
|
14
14
|
import { parseMeshOptGeometry } from './MeshOpt.js';
|
|
15
|
+
import { applyCustomAttributeSemantics } from './CustomAttributes.js';
|
|
15
16
|
import { isS3M1Buffer, parseS3M1 } from './S3M1Parser.js';
|
|
16
17
|
import type { DegradationKind } from './degradations.js';
|
|
17
18
|
import type {
|
|
@@ -53,6 +54,8 @@ export interface ParseOptions {
|
|
|
53
54
|
inflate?: InflateFn;
|
|
54
55
|
/** 逐段追踪偏移,调试解析用 */
|
|
55
56
|
trace?: (section: string, detail?: unknown, offset?: number) => void;
|
|
57
|
+
/** config.json 声明的顶点属性描述清单(用于语义重映射与维度判定) */
|
|
58
|
+
vertexAttributeDescript?: unknown;
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
/** 默认解压:zlib(文件头 78 9C / 78 DA)。浏览器与 Node 18+ 均可用。 */
|
|
@@ -73,9 +76,8 @@ function pushAttr(
|
|
|
73
76
|
offset = 0,
|
|
74
77
|
divisor?: number,
|
|
75
78
|
dtype: AttrDType = 'f32'
|
|
76
|
-
):
|
|
77
|
-
|
|
78
|
-
vp.vertexAttributes.push({
|
|
79
|
+
): VertexAttribute {
|
|
80
|
+
const attr: VertexAttribute = {
|
|
79
81
|
semantic,
|
|
80
82
|
data,
|
|
81
83
|
components,
|
|
@@ -83,7 +85,10 @@ function pushAttr(
|
|
|
83
85
|
offsetInBytes: offset,
|
|
84
86
|
divisor,
|
|
85
87
|
dtype,
|
|
86
|
-
}
|
|
88
|
+
};
|
|
89
|
+
vp.attrLocation[semantic] = vp.vertexAttributes.length;
|
|
90
|
+
vp.vertexAttributes.push(attr);
|
|
91
|
+
return attr;
|
|
87
92
|
}
|
|
88
93
|
|
|
89
94
|
function newVertexPackage(): VertexPackage {
|
|
@@ -248,15 +253,19 @@ function parseInstanceInfo(r: BinaryReader, vp: VertexPackage, version: number):
|
|
|
248
253
|
}
|
|
249
254
|
}
|
|
250
255
|
|
|
251
|
-
|
|
256
|
+
export { applyCustomAttributeSemantics } from './CustomAttributes.js';
|
|
257
|
+
|
|
258
|
+
function parseVertexAttrExtension(r: BinaryReader, vp: VertexPackage): VertexAttribute[] {
|
|
252
259
|
const n = r.u32();
|
|
260
|
+
const attrs: VertexAttribute[] = [];
|
|
253
261
|
for (let i = 0; i < n; i++) {
|
|
254
262
|
const count = r.u32();
|
|
255
263
|
const dim = r.u16();
|
|
256
264
|
const type = r.u16();
|
|
257
265
|
const size = AttrTypeSize[type] ?? 4;
|
|
258
|
-
pushAttr(vp, `custom${i}`, r.bytes(count * dim * size), dim, dim * size);
|
|
266
|
+
attrs.push(pushAttr(vp, `custom${i}`, r.bytes(count * dim * size), dim, dim * size));
|
|
259
267
|
}
|
|
268
|
+
return attrs;
|
|
260
269
|
}
|
|
261
270
|
|
|
262
271
|
function parseTangent(r: BinaryReader): void {
|
|
@@ -267,7 +276,7 @@ function parseTangent(r: BinaryReader): void {
|
|
|
267
276
|
r.skip(n * dim * 4);
|
|
268
277
|
}
|
|
269
278
|
|
|
270
|
-
function parseStandardSkeleton(r: BinaryReader, vp: VertexPackage, version: number): void {
|
|
279
|
+
function parseStandardSkeleton(r: BinaryReader, vp: VertexPackage, version: number, options?: ParseOptions): void {
|
|
271
280
|
if (version >= 3) r.u32(); // streamSize
|
|
272
281
|
|
|
273
282
|
parseVertex(r, vp);
|
|
@@ -278,9 +287,17 @@ function parseStandardSkeleton(r: BinaryReader, vp: VertexPackage, version: numb
|
|
|
278
287
|
parseInstanceInfo(r, vp, version);
|
|
279
288
|
|
|
280
289
|
if (version >= 3) {
|
|
281
|
-
parseVertexAttrExtension(r, vp);
|
|
290
|
+
const extAttrs = parseVertexAttrExtension(r, vp);
|
|
282
291
|
const descript = r.string();
|
|
283
|
-
|
|
292
|
+
try {
|
|
293
|
+
vp.customVertexAttribute = descript ? JSON.parse(descript) : undefined;
|
|
294
|
+
} catch {
|
|
295
|
+
vp.customVertexAttribute = descript;
|
|
296
|
+
}
|
|
297
|
+
if (!vp.customVertexAttribute && options?.vertexAttributeDescript) {
|
|
298
|
+
vp.customVertexAttribute = options.vertexAttributeDescript;
|
|
299
|
+
}
|
|
300
|
+
applyCustomAttributeSemantics(vp, extAttrs, options?.vertexAttributeDescript);
|
|
284
301
|
r.align4();
|
|
285
302
|
parseTangent(r);
|
|
286
303
|
}
|
|
@@ -386,7 +403,8 @@ function isFiniteBox(box: { center: Vec3; xExtent: Vec3; yExtent: Vec3; zExtent:
|
|
|
386
403
|
export async function parseSkeletonEntries(
|
|
387
404
|
r: BinaryReader,
|
|
388
405
|
version: number,
|
|
389
|
-
trace?: ParseOptions['trace']
|
|
406
|
+
trace?: ParseOptions['trace'],
|
|
407
|
+
options?: ParseOptions
|
|
390
408
|
): Promise<Record<string, GeoPackageEntry>> {
|
|
391
409
|
const count = r.u32();
|
|
392
410
|
const geoPackage: Record<string, GeoPackageEntry> = {};
|
|
@@ -419,7 +437,7 @@ export async function parseSkeletonEntries(
|
|
|
419
437
|
let indexPackages: IndexPackage[] = [];
|
|
420
438
|
|
|
421
439
|
if (tag === VertexTag.STANDARD) {
|
|
422
|
-
parseStandardSkeleton(r, vp, version);
|
|
440
|
+
parseStandardSkeleton(r, vp, version, options);
|
|
423
441
|
indexPackages = parseIndexPackages(r, version);
|
|
424
442
|
} else if (tag === VertexTag.COMPRESSED) {
|
|
425
443
|
if (version >= 3) {
|
|
@@ -462,10 +480,11 @@ export async function parseSkeletonEntries(
|
|
|
462
480
|
export async function parseSkeletons(
|
|
463
481
|
r: BinaryReader,
|
|
464
482
|
version: number,
|
|
465
|
-
trace?: ParseOptions['trace']
|
|
483
|
+
trace?: ParseOptions['trace'],
|
|
484
|
+
options?: ParseOptions
|
|
466
485
|
): Promise<Record<string, GeoPackageEntry>> {
|
|
467
486
|
r.u32(); // streamSize
|
|
468
|
-
const geoPackage = await parseSkeletonEntries(r, version, trace);
|
|
487
|
+
const geoPackage = await parseSkeletonEntries(r, version, trace, options);
|
|
469
488
|
|
|
470
489
|
// v1/v2 在骨架数组之后还有一段附加流。官方写法是
|
|
471
490
|
// bytesOffset += secColorSize; bytesOffset += 4;
|
|
@@ -649,7 +668,7 @@ export async function parseS3MB(arrayBuffer: ArrayBuffer, options: ParseOptions
|
|
|
649
668
|
trace?.('fileOptions', fileOptions, t.offset);
|
|
650
669
|
const groupNode = parseGroupNode(t, version);
|
|
651
670
|
trace?.('groupNode', groupNode.pageLods.length, t.offset);
|
|
652
|
-
const geoPackage = await parseSkeletons(t, version, trace);
|
|
671
|
+
const geoPackage = await parseSkeletons(t, version, trace, options);
|
|
653
672
|
trace?.('skeletons', Object.keys(geoPackage).length, t.offset);
|
|
654
673
|
const textures = parseTextures(t);
|
|
655
674
|
trace?.('textures', Object.keys(textures).length, t.offset);
|
package/src/core/TileGeometry.ts
CHANGED
|
@@ -20,6 +20,8 @@ import type {
|
|
|
20
20
|
ParsedTile,
|
|
21
21
|
VertexAttribute,
|
|
22
22
|
} from './types.js';
|
|
23
|
+
import { applyCustomAttributeSemantics } from './CustomAttributes.js';
|
|
24
|
+
import { bakeMercatorOffsets, type TileAnchorLike, type TileCenterLike } from './BakeMercator.js';
|
|
23
25
|
|
|
24
26
|
/**
|
|
25
27
|
* S3M 规范里 `DrawPrimitiveType` 的两个特殊值(原住 GeometryBuilder,随组装层
|
|
@@ -248,6 +250,8 @@ export interface PrimitiveAttributes {
|
|
|
248
250
|
* 只有存在该流时才产生;解码语义见 core/TexCoordZ.ts。
|
|
249
251
|
*/
|
|
250
252
|
aTexCoordZ?: PlainAttribute;
|
|
253
|
+
/** 逐顶点精确 Web 墨卡托偏移(Issue #15) */
|
|
254
|
+
aMercatorOffset?: PlainAttribute;
|
|
251
255
|
}
|
|
252
256
|
|
|
253
257
|
export interface PrimitiveData {
|
|
@@ -297,6 +301,11 @@ export interface AssembleOptions {
|
|
|
297
301
|
* `useWorker: false` 或 Worker 崩溃降级时它落在主线程上。
|
|
298
302
|
*/
|
|
299
303
|
dedupeCoincident?: boolean;
|
|
304
|
+
/** 是否烘焙精确 Web 墨卡托偏移(Issue #15) */
|
|
305
|
+
bakeMercator?: boolean;
|
|
306
|
+
tileAnchor?: TileAnchorLike;
|
|
307
|
+
tileCenter?: TileCenterLike;
|
|
308
|
+
vertexAttributeDescript?: unknown;
|
|
300
309
|
}
|
|
301
310
|
|
|
302
311
|
/* ------------------------------------------------------------------ 合批合并 */
|
|
@@ -556,8 +565,15 @@ interface SkeletonAsm {
|
|
|
556
565
|
}
|
|
557
566
|
|
|
558
567
|
/** 每骨架只解一次交错流(同一骨架被多个 pack / geode 引用时共享同一份数据) */
|
|
559
|
-
function assembleSkeleton(
|
|
568
|
+
function assembleSkeleton(
|
|
569
|
+
name: string,
|
|
570
|
+
geo: GeoPackageEntry,
|
|
571
|
+
tile: ParsedTile,
|
|
572
|
+
skipped: string[],
|
|
573
|
+
options?: AssembleOptions
|
|
574
|
+
): SkeletonAsm | null {
|
|
560
575
|
const vp = geo.vertexPackage;
|
|
576
|
+
applyCustomAttributeSemantics(vp, undefined, (tile as any).vertexAttributeDescript ?? options?.vertexAttributeDescript);
|
|
561
577
|
const positionAttr = vp.vertexAttributes.find((a) => a.semantic === 'position');
|
|
562
578
|
if (!positionAttr) {
|
|
563
579
|
skipped.push(`${name} 无 position`);
|
|
@@ -585,11 +601,29 @@ function assembleSkeleton(name: string, geo: GeoPackageEntry, tile: ParsedTile,
|
|
|
585
601
|
else if (attr.semantic === 'normal') normalData = deinterleave(attr);
|
|
586
602
|
else if (attr.semantic === 'uv') uvData = deinterleave(attr);
|
|
587
603
|
else if (attr.semantic === 'uv2') uv1Data = deinterleave(attr);
|
|
588
|
-
//
|
|
589
|
-
// 用图集的哪个子区域、按 2
|
|
590
|
-
//
|
|
591
|
-
|
|
592
|
-
|
|
604
|
+
// uv 第三分量(官方 aTextureCoordMatrix):决定「这张贴图用不用、
|
|
605
|
+
// 用图集的哪个子区域、按 2 的几次幂缩放」。语义见 core/TexCoordZ.ts。
|
|
606
|
+
// 注意:按语义查槽(找 TextureCoordMatrix),不要固定取 custom0;
|
|
607
|
+
// 绝对不能把 VertexWeight(高度值)等其它语义属性当成贴图矩阵;
|
|
608
|
+
// 若全为哨兵(-99999)则视为无第三分量,跳过采样/图集选择逻辑。
|
|
609
|
+
else if (
|
|
610
|
+
(attr.semantic === 'TextureCoordMatrix' ||
|
|
611
|
+
attr.semantic === 'aTextureCoordMatrix' ||
|
|
612
|
+
(attr.semantic === 'custom0' && !vp.vertexAttributes.some((a) => a.semantic === 'TextureCoordMatrix' || a.semantic === 'VertexWeight'))) &&
|
|
613
|
+
(attr.components ?? 1) >= 1
|
|
614
|
+
) {
|
|
615
|
+
const plain = deinterleave(attr);
|
|
616
|
+
const arr = plain.array as Float32Array;
|
|
617
|
+
let hasValid = false;
|
|
618
|
+
for (let i = 0; i < arr.length; i++) {
|
|
619
|
+
if (arr[i]! >= -99900) {
|
|
620
|
+
hasValid = true;
|
|
621
|
+
break;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
if (hasValid) {
|
|
625
|
+
texCoordZData = plain;
|
|
626
|
+
}
|
|
593
627
|
}
|
|
594
628
|
}
|
|
595
629
|
|
|
@@ -752,7 +786,7 @@ export function assembleTileGeometry(tile: ParsedTile, options: AssembleOptions
|
|
|
752
786
|
|
|
753
787
|
const skeletons = new Map<string, SkeletonAsm>();
|
|
754
788
|
for (const [name, geo] of Object.entries(tile.geoPackage ?? {})) {
|
|
755
|
-
const asm = assembleSkeleton(name, geo, tile, skipped);
|
|
789
|
+
const asm = assembleSkeleton(name, geo, tile, skipped, options);
|
|
756
790
|
if (asm) {
|
|
757
791
|
skeletons.set(name, asm);
|
|
758
792
|
stats.vertices += geo.vertexPackage.verticesCount || 0;
|
|
@@ -878,5 +912,24 @@ export function assembleTileGeometry(tile: ParsedTile, options: AssembleOptions
|
|
|
878
912
|
}
|
|
879
913
|
}
|
|
880
914
|
|
|
915
|
+
// 逐顶点烘焙精确 Web 墨卡托(Issue #15)
|
|
916
|
+
if (options.bakeMercator && options.tileAnchor) {
|
|
917
|
+
for (const p of primitives) {
|
|
918
|
+
const positions = p.attributes.position?.array;
|
|
919
|
+
if (!positions || p.vertexCount === 0) continue;
|
|
920
|
+
const center = options.tileCenter ?? {
|
|
921
|
+
x: p.bounds.sphereCenter[0],
|
|
922
|
+
y: p.bounds.sphereCenter[1],
|
|
923
|
+
z: p.bounds.sphereCenter[2],
|
|
924
|
+
};
|
|
925
|
+
const offsets = bakeMercatorOffsets(positions, p.vertexCount, options.tileAnchor, center);
|
|
926
|
+
p.attributes.aMercatorOffset = {
|
|
927
|
+
array: offsets,
|
|
928
|
+
components: 3,
|
|
929
|
+
normalized: false,
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
881
934
|
return { primitives, stats };
|
|
882
935
|
}
|
package/src/core/types.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -51,17 +51,25 @@ export type {
|
|
|
51
51
|
|
|
52
52
|
// —— 地理坐标适配(宿主无关接口 + 墨卡托兜底)
|
|
53
53
|
export {
|
|
54
|
+
WGS84_A,
|
|
55
|
+
WGS84_B,
|
|
56
|
+
WGS84_F,
|
|
57
|
+
WGS84_E2,
|
|
54
58
|
EARTH_RADIUS,
|
|
59
|
+
primeVerticalRadius,
|
|
60
|
+
meridianRadius,
|
|
61
|
+
enuToLngLat,
|
|
62
|
+
lngLatToEnu,
|
|
55
63
|
mercatorFromLngLat,
|
|
56
64
|
lngLatFromMercator,
|
|
57
65
|
worldFromLngLat,
|
|
58
66
|
lngLatFromWorld,
|
|
59
67
|
} from './render/GeoAdapter.js';
|
|
60
|
-
export type { GeoAdapter } from './render/GeoAdapter.js';
|
|
68
|
+
export type { GeoAdapter, WorldCoordinateResult } from './render/GeoAdapter.js';
|
|
61
69
|
|
|
62
70
|
// —— 瓦片层
|
|
63
|
-
export { S3MTileset } from './tileset/S3MTileset.js';
|
|
64
|
-
export type { TileNode, BuildTileFn, S3MTilesetOptions } from './tileset/S3MTileset.js';
|
|
71
|
+
export { S3MTileset, parseConfigText, classifyDatasetKind } from './tileset/S3MTileset.js';
|
|
72
|
+
export type { TileNode, BuildTileFn, S3MTilesetOptions, DatasetKind } from './tileset/S3MTileset.js';
|
|
65
73
|
|
|
66
74
|
// —— 渲染层
|
|
67
75
|
export { S3MViewer } from './render/S3MViewer.js';
|
|
@@ -110,8 +118,10 @@ export type {
|
|
|
110
118
|
GroundOptions,
|
|
111
119
|
LightOptions,
|
|
112
120
|
PathFollowOptions,
|
|
121
|
+
DepthMode,
|
|
113
122
|
} from './render/S3MViewer.js';
|
|
114
123
|
|
|
124
|
+
export { normalizeDepthMode } from './render/S3MViewer.js';
|
|
115
125
|
export { LIGHT_PRESETS, DEFAULT_LIGHT_PRESET, SUPERMAP_LIGHT_PRESET, resolveLightingPreset } from './render/lightingPreset.js';
|
|
116
126
|
export type { LightingPreset, PresetLightIntensities } from './render/lightingPreset.js';
|
|
117
127
|
|
|
@@ -138,6 +148,8 @@ export { setMaterialQuality, getMaterialQuality } from './render/StyleMaterial.j
|
|
|
138
148
|
|
|
139
149
|
export { Picker } from './render/Picker.js';
|
|
140
150
|
export { GpuEdgePass } from './render/GpuEdgePass.js';
|
|
151
|
+
export { MotionBlurPass, normalizeMotionBlur, autoMotionBlurDamp } from './render/MotionBlurPass.js';
|
|
152
|
+
export type { MotionBlurOptions, MotionBlurConfig, MotionBlurParams } from './render/MotionBlurPass.js';
|
|
141
153
|
|
|
142
154
|
/* 三维空间分析内核:纯数学,不依赖 WebGL,可单独用于服务端 / Worker */
|
|
143
155
|
export * from './analysis/index.js';
|
|
@@ -14,6 +14,7 @@ import type { Anchor } from '../core/types.js';
|
|
|
14
14
|
import type { UnsupportedPolicy } from '../core/degradations.js';
|
|
15
15
|
import { worldFromLngLat, type GeoAdapter } from './GeoAdapter.js';
|
|
16
16
|
import type { TextureCreateOptions } from './TextureFactory.js';
|
|
17
|
+
import type { DepthMode } from './viewer/types.js';
|
|
17
18
|
|
|
18
19
|
export type DatasetAlignment = 'origin' | 'bottom' | 'top' | 'center';
|
|
19
20
|
|
|
@@ -42,6 +43,11 @@ export interface S3MDatasetOptions {
|
|
|
42
43
|
heightOffset?: number;
|
|
43
44
|
/** 是否用 Web Worker 解析经典 .s3mb 瓦片(默认开,崩溃自动降级主线程),见 S3MTilesetOptions.useWorker */
|
|
44
45
|
useWorker?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* 本数据集推荐的深度模式('scene' | 'clear' | 'auto')。
|
|
48
|
+
* 当图层/Viewer 的 depthMode 为 'auto' 时,若任一可见数据集标注为 'clear',则整层以 'clear' 绘制。
|
|
49
|
+
*/
|
|
50
|
+
depthMode?: DepthMode;
|
|
45
51
|
/** 三角面材质深度偏移(共面摩尔纹 / 重合面归属),见 BuildTileOptions.polygonOffset */
|
|
46
52
|
polygonOffset?: { factor: number; units: number };
|
|
47
53
|
/**
|
|
@@ -49,6 +55,7 @@ export interface S3MDatasetOptions {
|
|
|
49
55
|
* 只影响该数据集**之后创建**的材质。
|
|
50
56
|
*/
|
|
51
57
|
materialQuality?: 'pbr' | 'lambert';
|
|
58
|
+
bakeMercator?: boolean;
|
|
52
59
|
/**
|
|
53
60
|
* 是否对该数据集生成几何棱线(CPU 端 EdgesGeometry)。
|
|
54
61
|
* - 不填:跟随全局 `style.edges`
|
|
@@ -65,6 +72,13 @@ export interface S3MDatasetOptions {
|
|
|
65
72
|
* 目前只有 3DGS:按点渲染,做不出泼溅。见 `S3MTilesetOptions.unsupported`。
|
|
66
73
|
*/
|
|
67
74
|
unsupported?: UnsupportedPolicy;
|
|
75
|
+
/**
|
|
76
|
+
* LOD 细化过渡策略:
|
|
77
|
+
* - 'binary'(默认):整节点二值切换
|
|
78
|
+
* - 'incremental':增量细化(primary / fallback 双列表 + LEQUAL 深度推移)
|
|
79
|
+
*/
|
|
80
|
+
lodTransition?: 'binary' | 'incremental';
|
|
81
|
+
fallbackOffset?: { factor?: number; units?: number };
|
|
68
82
|
}
|
|
69
83
|
|
|
70
84
|
export interface S3MDatasetEntry {
|
|
@@ -97,13 +111,32 @@ export interface DatasetManagerCallbacks {
|
|
|
97
111
|
|
|
98
112
|
export class DatasetManager {
|
|
99
113
|
readonly datasets = new Map<string, S3MDatasetEntry>();
|
|
100
|
-
sceneAnchor: {
|
|
114
|
+
sceneAnchor: {
|
|
115
|
+
x: number;
|
|
116
|
+
y: number;
|
|
117
|
+
z: number;
|
|
118
|
+
meterInMercatorEast?: number;
|
|
119
|
+
meterInMercatorNorth?: number;
|
|
120
|
+
meterInMercatorUp?: number;
|
|
121
|
+
meterInMercator: number;
|
|
122
|
+
altitude?: number;
|
|
123
|
+
} | null = null;
|
|
101
124
|
totalMaxTiles: number = 0;
|
|
102
125
|
/**
|
|
103
126
|
* 图层级默认光照模型档位(`undefined` = 跟随模块级默认)。
|
|
104
127
|
* 数据集自己指定了 `materialQuality` 就用自己的。
|
|
105
128
|
*/
|
|
106
129
|
defaultMaterialQuality: 'pbr' | 'lambert' | undefined = undefined;
|
|
130
|
+
defaultLodTransition: 'binary' | 'incremental' = 'binary';
|
|
131
|
+
defaultFallbackOffset?: { factor?: number; units?: number };
|
|
132
|
+
|
|
133
|
+
setDefaultLodTransition(mode: 'binary' | 'incremental'): void {
|
|
134
|
+
this.defaultLodTransition = mode;
|
|
135
|
+
for (const entry of this.datasets.values()) {
|
|
136
|
+
entry.tileset?.setLodTransition(mode);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
defaultBakeMercator: boolean | undefined = undefined;
|
|
107
140
|
|
|
108
141
|
/**
|
|
109
142
|
* 图层档位变更:同步给已存在的数据集(它们之后加载的瓦片才用新档位;
|
|
@@ -264,6 +297,8 @@ export class DatasetManager {
|
|
|
264
297
|
maxMemory: ds.maxMemory ?? maxMemory,
|
|
265
298
|
maxImageCache: ds.maxImageCache ?? maxImageCache,
|
|
266
299
|
lodRangeScale: ds.lodRangeScale ?? defaultLodRangeScale,
|
|
300
|
+
lodTransition: ds.lodTransition ?? this.defaultLodTransition,
|
|
301
|
+
fallbackOffset: ds.fallbackOffset ?? this.defaultFallbackOffset,
|
|
267
302
|
buildTile: buildTile ?? null,
|
|
268
303
|
polygonOffset: ds.polygonOffset,
|
|
269
304
|
// 数据集 > 图层 > 模块级默认(undefined 一路传下去,由 createStyledMaterial 回落)
|
|
@@ -272,6 +307,7 @@ export class DatasetManager {
|
|
|
272
307
|
colorPipeline: this.defaultColorPipeline,
|
|
273
308
|
textureOptions: this.defaultTextureOptions,
|
|
274
309
|
useWorker: ds.useWorker,
|
|
310
|
+
bakeMercator: ds.bakeMercator ?? this.defaultBakeMercator,
|
|
275
311
|
authenticate: ds.authenticate ?? authenticate,
|
|
276
312
|
unsupported: ds.unsupported,
|
|
277
313
|
uniforms: uniforms,
|
|
@@ -359,12 +395,14 @@ export class DatasetManager {
|
|
|
359
395
|
if (!this.sceneAnchor) {
|
|
360
396
|
this.sceneAnchor = worldFromLngLat(geo, entry.anchor.lon, entry.anchor.lat, entry.anchor.height ?? 0);
|
|
361
397
|
}
|
|
362
|
-
const
|
|
398
|
+
const sEast = this.sceneAnchor.meterInMercatorEast ?? this.sceneAnchor.meterInMercator;
|
|
399
|
+
const sNorth = this.sceneAnchor.meterInMercatorNorth ?? this.sceneAnchor.meterInMercator;
|
|
400
|
+
const sUp = sEast;
|
|
363
401
|
const a = worldFromLngLat(geo, entry.anchor.lon, entry.anchor.lat, entry.anchor.height ?? 0);
|
|
364
402
|
entry.offset = [
|
|
365
|
-
(a.x - this.sceneAnchor.x) /
|
|
366
|
-
-(a.y - this.sceneAnchor.y) /
|
|
367
|
-
(a.z - this.sceneAnchor.z) /
|
|
403
|
+
(a.x - this.sceneAnchor.x) / sEast,
|
|
404
|
+
-(a.y - this.sceneAnchor.y) / sNorth,
|
|
405
|
+
(a.z - this.sceneAnchor.z) / sUp + (entry.heightOffset ?? 0),
|
|
368
406
|
];
|
|
369
407
|
entry.root.position.set(entry.offset[0], entry.offset[1], entry.offset[2]);
|
|
370
408
|
entry.root.updateMatrixWorld(true);
|