s3m-three 0.4.18 → 0.4.20
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-DvAUA8NJ.js +1 -0
- package/dist/browser/s3m-three.iife.ext-three.js +619 -304
- package/dist/browser/s3m-three.iife.js +647 -332
- package/dist/core/Draco.d.ts.map +1 -1
- package/dist/core/Draco.js +0 -1
- package/dist/core/Draco.js.map +1 -1
- package/dist/core/GaussianSplat.d.ts +64 -0
- package/dist/core/GaussianSplat.d.ts.map +1 -0
- package/dist/core/GaussianSplat.js +139 -0
- package/dist/core/GaussianSplat.js.map +1 -0
- package/dist/core/S3M1Parser.d.ts +1 -1
- package/dist/core/S3M1Parser.d.ts.map +1 -1
- package/dist/core/S3M1Parser.js +320 -25
- package/dist/core/S3M1Parser.js.map +1 -1
- package/dist/core/S3MBParser.d.ts.map +1 -1
- package/dist/core/S3MBParser.js +13 -0
- package/dist/core/S3MBParser.js.map +1 -1
- package/dist/core/S3MBlockParser.d.ts.map +1 -1
- package/dist/core/S3MBlockParser.js +83 -28
- package/dist/core/S3MBlockParser.js.map +1 -1
- package/dist/core/SplatLod.d.ts +36 -0
- package/dist/core/SplatLod.d.ts.map +1 -0
- package/dist/core/SplatLod.js +54 -0
- package/dist/core/SplatLod.js.map +1 -0
- package/dist/core/SplatProjection.d.ts +75 -0
- package/dist/core/SplatProjection.d.ts.map +1 -0
- package/dist/core/SplatProjection.js +162 -0
- package/dist/core/SplatProjection.js.map +1 -0
- package/dist/core/SplatSort.d.ts +26 -0
- package/dist/core/SplatSort.d.ts.map +1 -0
- package/dist/core/SplatSort.js +61 -0
- package/dist/core/SplatSort.js.map +1 -0
- package/dist/core/TexCoordZ.d.ts +94 -0
- package/dist/core/TexCoordZ.d.ts.map +1 -0
- package/dist/core/TexCoordZ.js +125 -0
- package/dist/core/TexCoordZ.js.map +1 -0
- package/dist/core/TextureDecode.d.ts +19 -0
- package/dist/core/TextureDecode.d.ts.map +1 -1
- package/dist/core/TextureDecode.js +53 -11
- package/dist/core/TextureDecode.js.map +1 -1
- package/dist/core/TileGeometry.d.ts +39 -0
- package/dist/core/TileGeometry.d.ts.map +1 -1
- package/dist/core/TileGeometry.js +133 -14
- package/dist/core/TileGeometry.js.map +1 -1
- package/dist/core/degradations.d.ts +37 -0
- package/dist/core/degradations.d.ts.map +1 -0
- package/dist/core/degradations.js +32 -0
- package/dist/core/degradations.js.map +1 -0
- package/dist/core/types.d.ts +36 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/render/DatasetManager.d.ts +11 -0
- package/dist/render/DatasetManager.d.ts.map +1 -1
- package/dist/render/DatasetManager.js +2 -0
- package/dist/render/DatasetManager.js.map +1 -1
- package/dist/render/FeatureStyleManager.d.ts.map +1 -1
- package/dist/render/FeatureStyleManager.js +7 -5
- package/dist/render/FeatureStyleManager.js.map +1 -1
- package/dist/render/GeometryBuilder.d.ts +31 -0
- package/dist/render/GeometryBuilder.d.ts.map +1 -1
- package/dist/render/GeometryBuilder.js +229 -6
- package/dist/render/GeometryBuilder.js.map +1 -1
- package/dist/render/Picker.d.ts.map +1 -1
- package/dist/render/Picker.js +19 -5
- package/dist/render/Picker.js.map +1 -1
- package/dist/render/S3MViewer.d.ts +106 -379
- package/dist/render/S3MViewer.d.ts.map +1 -1
- package/dist/render/S3MViewer.js +324 -1494
- package/dist/render/S3MViewer.js.map +1 -1
- package/dist/render/SplatMaterial.d.ts +103 -0
- package/dist/render/SplatMaterial.d.ts.map +1 -0
- package/dist/render/SplatMaterial.js +388 -0
- package/dist/render/SplatMaterial.js.map +1 -0
- package/dist/render/SplatSorter.d.ts +14 -0
- package/dist/render/SplatSorter.d.ts.map +1 -0
- package/dist/render/SplatSorter.js +66 -0
- package/dist/render/SplatSorter.js.map +1 -0
- package/dist/render/StyleMaterial.d.ts +48 -0
- package/dist/render/StyleMaterial.d.ts.map +1 -1
- package/dist/render/StyleMaterial.js +292 -69
- package/dist/render/StyleMaterial.js.map +1 -1
- package/dist/render/TextureFactory.d.ts +10 -0
- package/dist/render/TextureFactory.d.ts.map +1 -1
- package/dist/render/TextureFactory.js +35 -5
- package/dist/render/TextureFactory.js.map +1 -1
- package/dist/render/lightingPreset.d.ts +53 -0
- package/dist/render/lightingPreset.d.ts.map +1 -0
- package/dist/render/lightingPreset.js +36 -0
- package/dist/render/lightingPreset.js.map +1 -0
- package/dist/render/materialAlpha.d.ts +51 -0
- package/dist/render/materialAlpha.d.ts.map +1 -0
- package/dist/render/materialAlpha.js +68 -0
- package/dist/render/materialAlpha.js.map +1 -0
- package/dist/render/viewer/EdgeDecorator.d.ts +206 -0
- package/dist/render/viewer/EdgeDecorator.d.ts.map +1 -0
- package/dist/render/viewer/EdgeDecorator.js +628 -0
- package/dist/render/viewer/EdgeDecorator.js.map +1 -0
- package/dist/render/viewer/HeightIndex.d.ts +36 -0
- package/dist/render/viewer/HeightIndex.d.ts.map +1 -0
- package/dist/render/viewer/HeightIndex.js +98 -0
- package/dist/render/viewer/HeightIndex.js.map +1 -0
- package/dist/render/viewer/LightRig.d.ts +88 -0
- package/dist/render/viewer/LightRig.d.ts.map +1 -0
- package/dist/render/viewer/LightRig.js +229 -0
- package/dist/render/viewer/LightRig.js.map +1 -0
- package/dist/render/viewer/PickService.d.ts +109 -0
- package/dist/render/viewer/PickService.d.ts.map +1 -0
- package/dist/render/viewer/PickService.js +591 -0
- package/dist/render/viewer/PickService.js.map +1 -0
- package/dist/render/viewer/SelectionDecorator.d.ts +84 -0
- package/dist/render/viewer/SelectionDecorator.d.ts.map +1 -0
- package/dist/render/viewer/SelectionDecorator.js +339 -0
- package/dist/render/viewer/SelectionDecorator.js.map +1 -0
- package/dist/render/viewer/SplatLayer.d.ts +26 -0
- package/dist/render/viewer/SplatLayer.d.ts.map +1 -0
- package/dist/render/viewer/SplatLayer.js +44 -0
- package/dist/render/viewer/SplatLayer.js.map +1 -0
- package/dist/render/viewer/types.d.ts +250 -0
- package/dist/render/viewer/types.d.ts.map +1 -0
- package/dist/render/viewer/types.js +3 -0
- package/dist/render/viewer/types.js.map +1 -0
- package/dist/tileset/S3MTileset.d.ts +42 -1
- package/dist/tileset/S3MTileset.d.ts.map +1 -1
- package/dist/tileset/S3MTileset.js +52 -2
- package/dist/tileset/S3MTileset.js.map +1 -1
- package/dist/vendor/browser-node-stub.js +10 -0
- package/dist/worker/s3mWorker.js +1166 -979
- package/dist/worker/s3mWorker.js.map +1 -1
- package/docs/API.md +55 -1
- package/docs/FORMAT-NOTES.md +362 -0
- package/package.json +1 -1
- package/src/core/Draco.ts +1 -2
- package/src/core/GaussianSplat.ts +175 -0
- package/src/core/S3M1Parser.ts +330 -24
- package/src/core/S3MBParser.ts +18 -0
- package/src/core/S3MBlockParser.ts +83 -27
- package/src/core/SplatLod.ts +83 -0
- package/src/core/SplatProjection.ts +231 -0
- package/src/core/SplatSort.ts +92 -0
- package/src/core/TexCoordZ.ts +160 -0
- package/src/core/TextureDecode.ts +63 -11
- package/src/core/TileGeometry.ts +151 -14
- package/src/core/degradations.ts +74 -0
- package/src/core/types.ts +32 -0
- package/src/index.ts +8 -1
- package/src/render/DatasetManager.ts +10 -0
- package/src/render/FeatureStyleManager.ts +8 -6
- package/src/render/GeometryBuilder.ts +271 -7
- package/src/render/Picker.ts +21 -5
- package/src/render/S3MViewer.ts +358 -1727
- package/src/render/SplatMaterial.ts +433 -0
- package/src/render/SplatSorter.ts +66 -0
- package/src/render/StyleMaterial.ts +339 -69
- package/src/render/TextureFactory.ts +39 -5
- package/src/render/lightingPreset.ts +76 -0
- package/src/render/materialAlpha.ts +75 -0
- package/src/render/viewer/EdgeDecorator.ts +722 -0
- package/src/render/viewer/HeightIndex.ts +94 -0
- package/src/render/viewer/LightRig.ts +267 -0
- package/src/render/viewer/PickService.ts +651 -0
- package/src/render/viewer/SelectionDecorator.ts +377 -0
- package/src/render/viewer/SplatLayer.ts +44 -0
- package/src/render/viewer/types.ts +258 -0
- package/src/tileset/S3MTileset.ts +78 -2
- package/src/vendor/browser-node-stub.js +10 -0
- package/dist/browser/assets/s3mWorker-BjPOUgjg.js +0 -1
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 泼溅 / 点云瓦片的细化判据
|
|
3
|
+
*
|
|
4
|
+
* 判据只有一条:**单个高斯在屏幕上有多大**。3σ 半轴超过 `SPLAT_REFINE_PX` 像素
|
|
5
|
+
* 就再细化一层 —— 一个高斯铺出十几二十个像素时,它已经把整片屋顶/柱子糊成一坨,
|
|
6
|
+
* 细节全在「这一片高斯自己」里,再远的层级也换不回来。
|
|
7
|
+
*
|
|
8
|
+
* 走过两轮弯路,都不要再回去:
|
|
9
|
+
*
|
|
10
|
+
* 1. **包围球投影像素 vs rangeValue**(网格 LOD 的判据)。泼溅每层的 σ 是同一个
|
|
11
|
+
* 物理量,包围球半径和 rangeValue 跟「画面够不够密」没有因果关系。实测寿宁阁
|
|
12
|
+
* 的 rangeMode=2、rangeValue≈0.066·半径,按它算在任何距离都远大于阈值,等于
|
|
13
|
+
* 「永远细化到叶子」。
|
|
14
|
+
*
|
|
15
|
+
* 2. **屏幕覆盖率**(`高斯数/包围球投影面积 × 单个高斯面积`)。看起来很有道理,
|
|
16
|
+
* 但实测下来是个陷阱:根瓦片 σ=0.21 m、1888 个点,覆盖率就已经算到 1.7;
|
|
17
|
+
* 子瓦片直接跳到 8 —— 于是**在第二层就停了**,最终只画了 35.6 万个高斯,
|
|
18
|
+
* 占总量 2.3%。而官方是画到叶子层。21 cm 的高斯画不出 30 cm 宽的柱子,
|
|
19
|
+
* 画面上就是「柱子没了、到处是一坨坨横着的糊斑」。
|
|
20
|
+
* 覆盖率回答的是「屏幕盖满了没有」,盖满了也完全可能全是粗高斯。
|
|
21
|
+
*
|
|
22
|
+
* 所以现在改成屏幕尺寸判据。它天然自带下限:`splatPx` 已经小到着色器会剔除的量级
|
|
23
|
+
* (见 SplatProjection 的 `SPLAT_CULL_PIXELS`)时不再细化,再下一层只是白下载。
|
|
24
|
+
*/
|
|
25
|
+
import { SPLAT_SIGMA } from './SplatProjection.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 3σ 半轴超过这个像素数就继续细化。
|
|
29
|
+
*
|
|
30
|
+
* 4 px 意味着单个高斯在屏幕上约 8 px 宽 —— 再细下去肉眼分辨不出,却要多占几倍显存
|
|
31
|
+
* (每深一层点数约 ×5)。着色器的亚像素剔除线是 2 px 半径,这里留了一倍余量。
|
|
32
|
+
*/
|
|
33
|
+
export const SPLAT_REFINE_PX = 4.0;
|
|
34
|
+
|
|
35
|
+
/** 垂直视场角写死 45°,与 LOD 其它路径、官方 PageLod 口径一致 */
|
|
36
|
+
const HALF_FOV_TAN = Math.tan(Math.PI / 8);
|
|
37
|
+
|
|
38
|
+
/** 高斯实际可见面积相对 π(3σ)² 的比例(exp(-4r²) 衰减下的等效半径) */
|
|
39
|
+
const EFFECTIVE_AREA_RATIO = 0.25;
|
|
40
|
+
|
|
41
|
+
export interface SplatLodInput {
|
|
42
|
+
/** 该瓦片里的高斯数 */
|
|
43
|
+
splats: number;
|
|
44
|
+
/** 包围球半径(米) */
|
|
45
|
+
radius: number;
|
|
46
|
+
/** 相机到球心距离(米) */
|
|
47
|
+
distance: number;
|
|
48
|
+
/** 该瓦片高斯的代表性半轴(米);0/缺省 = 未知,退回调用方的通用判据 */
|
|
49
|
+
sigma: number;
|
|
50
|
+
/** 绘图缓冲高度(像素) */
|
|
51
|
+
screenHeight: number;
|
|
52
|
+
/** 是否还有子瓦片 */
|
|
53
|
+
hasChild: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface SplatLodResult {
|
|
57
|
+
/** 3σ 半轴在屏幕上的长度(像素)—— 判据本体 */
|
|
58
|
+
splatPx: number;
|
|
59
|
+
/** 每像素高斯数(诊断用) */
|
|
60
|
+
density: number;
|
|
61
|
+
/** 屏幕覆盖率(诊断用;**不是**判据,原因见文件头) */
|
|
62
|
+
coverage: number;
|
|
63
|
+
needRefine: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 决定泼溅瓦片要不要再细化一层。
|
|
68
|
+
* `sigma <= 0` 时返回 `needRefine: false`,调用方应退回原判据。
|
|
69
|
+
*/
|
|
70
|
+
export function splatLodDecision(o: SplatLodInput): SplatLodResult {
|
|
71
|
+
const dist = Math.max(o.distance, 1e-6);
|
|
72
|
+
const ppm = o.screenHeight / 2 / HALF_FOV_TAN / dist;
|
|
73
|
+
const splatPx = SPLAT_SIGMA * Math.max(o.sigma, 0) * ppm;
|
|
74
|
+
if (!(o.sigma > 0) || !(o.radius > 0)) {
|
|
75
|
+
return { splatPx, density: 0, coverage: 0, needRefine: false };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const areaPx = Math.PI * (ppm * o.radius) ** 2;
|
|
79
|
+
const density = areaPx > 1e-6 ? o.splats / areaPx : 0;
|
|
80
|
+
const coverage = density * Math.PI * splatPx * splatPx * EFFECTIVE_AREA_RATIO;
|
|
81
|
+
const needRefine = o.hasChild && splatPx > SPLAT_REFINE_PX;
|
|
82
|
+
return { splatPx, density, coverage, needRefine };
|
|
83
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 高斯泼溅的屏幕空间 EWA 投影(纯数学,供单测与顶点着色器共用同一套公式)
|
|
3
|
+
*
|
|
4
|
+
* 旧做法是把两个主轴投到「垂直于视线」的平面上、丢掉 z 分量。薄片高斯侧对
|
|
5
|
+
* 相机时被丢的那根轴直接归零,四边形塌成一条线 → 满墙空洞;远处高斯不足
|
|
6
|
+
* 一个像素时又会被光栅化漏掉。这里换成标准 EWA:
|
|
7
|
+
*
|
|
8
|
+
* Σ′ = J · (W R S Sᵀ Rᵀ Wᵀ) · Jᵀ (3D 协方差 → 屏幕 2×2)
|
|
9
|
+
*
|
|
10
|
+
* J 是 ndc 对视图坐标的雅可比,**直接用投影矩阵的元素**求,不假设它是标准
|
|
11
|
+
* 针孔透视 —— 本项目的 projectionMatrix 是 ENU→clip 的完整矩阵(w 行不是
|
|
12
|
+
* 恒等的 (0,0,-1,0)),套用教科书简式会算错。
|
|
13
|
+
*
|
|
14
|
+
* 得到的 2×2 做特征分解,两个特征向量 × 3σ 就是四边形在 ndc 里的两根半轴。
|
|
15
|
+
* 另有两道像素级处理,口径都照抄官方顶点着色器:
|
|
16
|
+
* - 低通:协方差对角加 0.3px 的方差(3DGS 论文的抗锯齿口径);
|
|
17
|
+
* - 亚像素剔除:两根半轴都短于 2px 就整片丢弃(官方 covVectors<2 判据)。
|
|
18
|
+
* 这一条是「彩色马赛克」的对症解:泼溅数据被细化到亚像素时,一个像素上
|
|
19
|
+
* 压着几十个高斯,逐个画出来就是满屏噪点。
|
|
20
|
+
*
|
|
21
|
+
* 半轴**不会被放大**到可见尺寸。早先版本有个 minPixels 把细轴抬到 0.7px,
|
|
22
|
+
* 那等于把每片薄高斯都变成一个硬点,噪点依旧、覆盖还失真 —— 已删除。
|
|
23
|
+
*
|
|
24
|
+
* 着色器(render/SplatMaterial.ts)里是这套公式的逐行 GLSL 版,改公式两边同步。
|
|
25
|
+
*/
|
|
26
|
+
import type * as THREE from 'three';
|
|
27
|
+
|
|
28
|
+
/** 3σ 覆盖 99.7% 的能量,与 3DGS 论文的裁剪口径一致 */
|
|
29
|
+
export const SPLAT_SIGMA = 3.0;
|
|
30
|
+
/** 抗锯齿低通的标准差(像素),3DGS 论文取 0.3 */
|
|
31
|
+
export const SPLAT_LOW_PASS_PIXELS = 0.3;
|
|
32
|
+
/**
|
|
33
|
+
* 半轴的最小像素长度(≈ 官方 lambda2 下限 0.1px² 对应的 sqrt(2*0.1))。
|
|
34
|
+
* 只防完全退化,绝不是「抬成可见尺寸」—— 后者是彩色噪点的来源。
|
|
35
|
+
*/
|
|
36
|
+
export const SPLAT_MIN_AXIS_PIXELS = 0.45;
|
|
37
|
+
/** 两根轴都短于这个像素数就整片丢弃(官方:dot(cov, cov) < 4.0 两轴同时成立) */
|
|
38
|
+
export const SPLAT_CULL_PIXELS = 2.0;
|
|
39
|
+
/**
|
|
40
|
+
* 半轴的像素上限。EWA 在中心贴近相机平面时会数值爆炸(实测近距离 max 4.2e6 px):
|
|
41
|
+
* 一个 quad 跨满整屏、只跟视锥相交出一个角,看上去就是锐角三角面和放射状拉丝。
|
|
42
|
+
* 1024px 只砍这些异常投影,正常建筑(放大到贴脸看也就几百 px)碰不到。
|
|
43
|
+
*/
|
|
44
|
+
export const SPLAT_MAX_AXIS_PIXELS = 1024;
|
|
45
|
+
|
|
46
|
+
export interface SplatScreenAxesOptions {
|
|
47
|
+
/** 视图空间中心(已乘过 modelViewMatrix) */
|
|
48
|
+
center: ArrayLike<number>;
|
|
49
|
+
/** 单位四元数 (x, y, z, w) */
|
|
50
|
+
quat: ArrayLike<number>;
|
|
51
|
+
/** 三个主轴半径(米) */
|
|
52
|
+
scale: ArrayLike<number>;
|
|
53
|
+
/** 只取旋转/缩放部分,用于把协方差带进视图空间 */
|
|
54
|
+
modelView: THREE.Matrix4;
|
|
55
|
+
/** 视图→clip 的完整矩阵(不要求是标准透视) */
|
|
56
|
+
projection: THREE.Matrix4;
|
|
57
|
+
viewportWidth: number;
|
|
58
|
+
viewportHeight: number;
|
|
59
|
+
/** 两根轴都短于这个像素数就剔除(返回 null);0 = 不剔除 */
|
|
60
|
+
cullPixels: number;
|
|
61
|
+
/** 半轴像素上限,超过则整体等比缩小;0 / 不传 = 不封顶 */
|
|
62
|
+
maxPixels?: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface SplatScreenAxes {
|
|
66
|
+
/** ndc 半轴 A(对应四边形的 x 方向),已含 3σ 与钳制 */
|
|
67
|
+
ax: number;
|
|
68
|
+
ay: number;
|
|
69
|
+
/** ndc 半轴 B */
|
|
70
|
+
bx: number;
|
|
71
|
+
by: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 返回四边形在 ndc 里的两根半轴;相机后方(w<=0)返回 null。
|
|
76
|
+
* 调用方用 `clip.xy += (position.x*A + position.y*B) * clip.w` 出顶点。
|
|
77
|
+
*/
|
|
78
|
+
export function splatScreenAxes(o: SplatScreenAxesOptions): SplatScreenAxes | null {
|
|
79
|
+
const p = o.projection.elements; // 列主序
|
|
80
|
+
const cx = o.center[0]!, cy = o.center[1]!, cz = o.center[2]!;
|
|
81
|
+
|
|
82
|
+
const clipX = p[0]! * cx + p[4]! * cy + p[8]! * cz + p[12]!;
|
|
83
|
+
const clipY = p[1]! * cx + p[5]! * cy + p[9]! * cz + p[13]!;
|
|
84
|
+
const w = p[3]! * cx + p[7]! * cy + p[11]! * cz + p[15]!;
|
|
85
|
+
if (!(w > 1e-6)) return null;
|
|
86
|
+
|
|
87
|
+
// M = W · R · diag(s),行向量取出后 Σ = M Mᵀ 只需要 6 个点积
|
|
88
|
+
const m = o.modelView.elements;
|
|
89
|
+
const [qx, qy, qz, qw] = [o.quat[0]!, o.quat[1]!, o.quat[2]!, o.quat[3]!];
|
|
90
|
+
const xx = qx * qx, yy = qy * qy, zz = qz * qz;
|
|
91
|
+
const xy = qx * qy, xz = qx * qz, yz = qy * qz;
|
|
92
|
+
const wx = qw * qx, wy = qw * qy, wz = qw * qz;
|
|
93
|
+
// R 的列(列主序):R[col][row]
|
|
94
|
+
const r00 = 1 - 2 * (yy + zz), r01 = 2 * (xy - wz), r02 = 2 * (xz + wy);
|
|
95
|
+
const r10 = 2 * (xy + wz), r11 = 1 - 2 * (xx + zz), r12 = 2 * (yz - wx);
|
|
96
|
+
const r20 = 2 * (xz - wy), r21 = 2 * (yz + wx), r22 = 1 - 2 * (xx + yy);
|
|
97
|
+
const sx = o.scale[0]!, sy = o.scale[1]!, sz = o.scale[2]!;
|
|
98
|
+
|
|
99
|
+
// W · R · diag(s):W 的第 i 列乘 R 的第 j 列再乘 s_j —— 直接展开成 M 的列
|
|
100
|
+
// M[col] = W * (R[col] * s_col)
|
|
101
|
+
const m00 = (m[0]! * r00 + m[4]! * r10 + m[8]! * r20) * sx;
|
|
102
|
+
const m10 = (m[1]! * r00 + m[5]! * r10 + m[9]! * r20) * sx;
|
|
103
|
+
const m20 = (m[2]! * r00 + m[6]! * r10 + m[10]! * r20) * sx;
|
|
104
|
+
const m01 = (m[0]! * r01 + m[4]! * r11 + m[8]! * r21) * sy;
|
|
105
|
+
const m11 = (m[1]! * r01 + m[5]! * r11 + m[9]! * r21) * sy;
|
|
106
|
+
const m21 = (m[2]! * r01 + m[6]! * r11 + m[10]! * r21) * sy;
|
|
107
|
+
const m02 = (m[0]! * r02 + m[4]! * r12 + m[8]! * r22) * sz;
|
|
108
|
+
const m12 = (m[1]! * r02 + m[5]! * r12 + m[9]! * r22) * sz;
|
|
109
|
+
const m22 = (m[2]! * r02 + m[6]! * r12 + m[10]! * r22) * sz;
|
|
110
|
+
|
|
111
|
+
// Σ 的上三角:行 i · 行 j
|
|
112
|
+
const v00 = m00 * m00 + m01 * m01 + m02 * m02;
|
|
113
|
+
const v01 = m00 * m10 + m01 * m11 + m02 * m12;
|
|
114
|
+
const v02 = m00 * m20 + m01 * m21 + m02 * m22;
|
|
115
|
+
const v11 = m10 * m10 + m11 * m11 + m12 * m12;
|
|
116
|
+
const v12 = m10 * m20 + m11 * m21 + m12 * m22;
|
|
117
|
+
const v22 = m20 * m20 + m21 * m21 + m22 * m22;
|
|
118
|
+
|
|
119
|
+
// 雅可比:ndc = clip.xy / w,对视图坐标求偏导
|
|
120
|
+
const w2 = w * w;
|
|
121
|
+
const j00 = (p[0]! * w - clipX * p[3]!) / w2;
|
|
122
|
+
const j01 = (p[4]! * w - clipX * p[7]!) / w2;
|
|
123
|
+
const j02 = (p[8]! * w - clipX * p[11]!) / w2;
|
|
124
|
+
const j10 = (p[1]! * w - clipY * p[3]!) / w2;
|
|
125
|
+
const j11 = (p[5]! * w - clipY * p[7]!) / w2;
|
|
126
|
+
const j12 = (p[9]! * w - clipY * p[11]!) / w2;
|
|
127
|
+
|
|
128
|
+
// C = J Σ Jᵀ(2×2 对称)
|
|
129
|
+
const c00 =
|
|
130
|
+
j00 * (v00 * j00 + v01 * j01 + v02 * j02) +
|
|
131
|
+
j01 * (v01 * j00 + v11 * j01 + v12 * j02) +
|
|
132
|
+
j02 * (v02 * j00 + v12 * j01 + v22 * j02);
|
|
133
|
+
const c01 =
|
|
134
|
+
j00 * (v00 * j10 + v01 * j11 + v02 * j12) +
|
|
135
|
+
j01 * (v01 * j10 + v11 * j11 + v12 * j12) +
|
|
136
|
+
j02 * (v02 * j10 + v12 * j11 + v22 * j12);
|
|
137
|
+
const c11 =
|
|
138
|
+
j10 * (v00 * j10 + v01 * j11 + v02 * j12) +
|
|
139
|
+
j11 * (v01 * j10 + v11 * j11 + v12 * j12) +
|
|
140
|
+
j12 * (v02 * j10 + v12 * j11 + v22 * j12);
|
|
141
|
+
|
|
142
|
+
// 抗锯齿低通:对角各加一个亚像素方差
|
|
143
|
+
const lpx = (2 * SPLAT_LOW_PASS_PIXELS) / o.viewportWidth;
|
|
144
|
+
const lpy = (2 * SPLAT_LOW_PASS_PIXELS) / o.viewportHeight;
|
|
145
|
+
const d00 = c00 + lpx * lpx;
|
|
146
|
+
const d11 = c11 + lpy * lpy;
|
|
147
|
+
|
|
148
|
+
// 2×2 特征分解(解析解,无迭代)
|
|
149
|
+
const mid = 0.5 * (d00 + d11);
|
|
150
|
+
const half = 0.5 * (d00 - d11);
|
|
151
|
+
const disc = Math.sqrt(Math.max(half * half + c01 * c01, 0));
|
|
152
|
+
const l1 = mid + disc;
|
|
153
|
+
const l2 = Math.max(mid - disc, 0);
|
|
154
|
+
|
|
155
|
+
let v1x = c01, v1y = l1 - d00;
|
|
156
|
+
const vlen = Math.hypot(v1x, v1y);
|
|
157
|
+
if (vlen > 1e-20) {
|
|
158
|
+
v1x /= vlen;
|
|
159
|
+
v1y /= vlen;
|
|
160
|
+
} else {
|
|
161
|
+
v1x = 1;
|
|
162
|
+
v1y = 0;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let r1 = SPLAT_SIGMA * Math.sqrt(Math.max(l1, 0));
|
|
166
|
+
let r2 = SPLAT_SIGMA * Math.sqrt(l2);
|
|
167
|
+
|
|
168
|
+
let ax = v1x * r1, ay = v1y * r1;
|
|
169
|
+
let bx = -v1y * r2, by = v1x * r2;
|
|
170
|
+
|
|
171
|
+
// 两道像素级处理,数值口径逐字照抄官方顶点着色器:
|
|
172
|
+
//
|
|
173
|
+
// ① 细轴下限。官方 `lambda2 = max(mid - radius, 0.1)`,即方差不低于 0.1px²,
|
|
174
|
+
// 对应半轴 sqrt(2*0.1) ≈ 0.447px。它只防「完全退化」,**不会**把一根
|
|
175
|
+
// 本来只有 1mm 的薄轴放大到肉眼可见 —— 放大才是噪点的来源。
|
|
176
|
+
// ② 亚像素剔除。官方 `if (dot(cov.xy,cov.xy) < 4.0 && dot(cov.zw,cov.zw) < 4.0)
|
|
177
|
+
// discard`:两根轴都短于 2px 的高斯直接不画。泼溅数据被过度细化时,
|
|
178
|
+
// 一个像素上会压着几十个亚像素高斯,逐个抬成 1px 硬点就是满屏彩色马赛克;
|
|
179
|
+
// 丢掉它们,粗层的大高斯自然接管,画面才干净。
|
|
180
|
+
const px = o.viewportWidth / 2;
|
|
181
|
+
const py = o.viewportHeight / 2;
|
|
182
|
+
|
|
183
|
+
// ③ 上限封顶(先于下限)。中心贴近相机平面时雅可比爆炸,半轴能到几百万像素。
|
|
184
|
+
// 等比缩小两根轴,长宽比保持不变 —— 各向异性形状不能被压圆。
|
|
185
|
+
// 不做这一步的表现是:一个 quad 跨满屏、与视锥只相交出一个角,
|
|
186
|
+
// 画面上就是散落的锐角三角面和放射状拉丝。
|
|
187
|
+
if (o.maxPixels && o.maxPixels > 0) {
|
|
188
|
+
const la0 = Math.hypot(ax * px, ay * py);
|
|
189
|
+
const lb0 = Math.hypot(bx * px, by * py);
|
|
190
|
+
const biggest = Math.max(la0, lb0);
|
|
191
|
+
if (biggest > o.maxPixels) {
|
|
192
|
+
const k = o.maxPixels / biggest;
|
|
193
|
+
ax *= k;
|
|
194
|
+
ay *= k;
|
|
195
|
+
bx *= k;
|
|
196
|
+
by *= k;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const floorPx = SPLAT_MIN_AXIS_PIXELS;
|
|
201
|
+
const la = Math.hypot(ax * px, ay * py);
|
|
202
|
+
const lb = Math.hypot(bx * px, by * py);
|
|
203
|
+
if (la < floorPx) {
|
|
204
|
+
if (la > 1e-20) {
|
|
205
|
+
const k = floorPx / la;
|
|
206
|
+
ax *= k;
|
|
207
|
+
ay *= k;
|
|
208
|
+
} else {
|
|
209
|
+
ax = (2 * floorPx) / o.viewportWidth;
|
|
210
|
+
ay = 0;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (lb < floorPx) {
|
|
214
|
+
if (lb > 1e-20) {
|
|
215
|
+
const k = floorPx / lb;
|
|
216
|
+
bx *= k;
|
|
217
|
+
by *= k;
|
|
218
|
+
} else {
|
|
219
|
+
bx = 0;
|
|
220
|
+
by = (2 * floorPx) / o.viewportHeight;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (o.cullPixels > 0) {
|
|
225
|
+
const ca = Math.hypot(ax * px, ay * py);
|
|
226
|
+
const cb = Math.hypot(bx * px, by * py);
|
|
227
|
+
if (ca < o.cullPixels && cb < o.cullPixels) return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return { ax, ay, bx, by };
|
|
231
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 高斯泼溅的 back-to-front 排序(纯数学,可单测)
|
|
3
|
+
*
|
|
4
|
+
* alpha 混合是顺序相关的:先画近的再画远的,远的颜色会盖到近的上头,
|
|
5
|
+
* 画面就是一片随视角乱闪的彩色噪点。3DGS 必须按视深从远到近画。
|
|
6
|
+
*
|
|
7
|
+
* 排序键是**视图空间 z**(相机前方为负),升序 = 远的先画。
|
|
8
|
+
* view 矩阵由调用方给(通常是 camera.matrixWorldInverse × mesh.matrixWorld),
|
|
9
|
+
* 本模块不关心相机朝向约定,只取矩阵第三行做点积。
|
|
10
|
+
*
|
|
11
|
+
* ## 为什么是基数排序
|
|
12
|
+
*
|
|
13
|
+
* 这里是**每帧**都要跑的路径(相机一动就重排),原来的
|
|
14
|
+
* `TypedArray.sort((a, b) => keys[a] - keys[b])` 有两个代价:
|
|
15
|
+
* - 比较函数是 JS 调用,10 万点实测 38 ms/帧;
|
|
16
|
+
* - 每次调用都 `new Float32Array(count)` + `new Uint32Array(count)`,每帧分配 800 KB。
|
|
17
|
+
*
|
|
18
|
+
* 换成 4 遍 8 位 LSD 基数排序后实测 3 ms,且缓冲全部复用。没选桶排序是因为
|
|
19
|
+
* 深度分布会很不均匀(正对相机的墙面,几万个点的视深几乎相同),桶内再排会退化。
|
|
20
|
+
*
|
|
21
|
+
* 排序键先转成「无符号可比」的 32 位整数(IEEE754 的标准技巧),
|
|
22
|
+
* 这样无符号整数比较的大小顺序与 float 完全一致,基数排序才成立。
|
|
23
|
+
*/
|
|
24
|
+
import type * as THREE from 'three';
|
|
25
|
+
|
|
26
|
+
/** 模块级 scratch:只增不减。排序是同步的,同一时刻只有一个在跑 */
|
|
27
|
+
let _keys = new Uint32Array(0);
|
|
28
|
+
let _scratch = new Uint32Array(0);
|
|
29
|
+
const _counts = new Uint32Array(256);
|
|
30
|
+
|
|
31
|
+
/** float → 无符号可比的 32 位键(正数翻符号位,负数按位取反) */
|
|
32
|
+
const _f32 = new Float32Array(1);
|
|
33
|
+
const _u32 = new Uint32Array(_f32.buffer);
|
|
34
|
+
|
|
35
|
+
function sortableBits(k: number): number {
|
|
36
|
+
_f32[0] = k;
|
|
37
|
+
const u = _u32[0]!;
|
|
38
|
+
// 负数的 IEEE754 表示是「符号位 1 + 绝对值」,直接按无符号比会把 -1 排到 0 后面
|
|
39
|
+
return u & 0x80000000 ? ~u >>> 0 : (u | 0x80000000) >>> 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function splatDepthOrder(
|
|
43
|
+
positions: Float32Array,
|
|
44
|
+
count: number,
|
|
45
|
+
view: THREE.Matrix4,
|
|
46
|
+
out?: Uint32Array
|
|
47
|
+
): Uint32Array {
|
|
48
|
+
const idx = out && out.length >= count ? out : new Uint32Array(count);
|
|
49
|
+
if (count <= 1) {
|
|
50
|
+
if (count === 1) idx[0] = 0;
|
|
51
|
+
return idx;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (_keys.length < count) _keys = new Uint32Array(count);
|
|
55
|
+
if (_scratch.length < count) _scratch = new Uint32Array(count);
|
|
56
|
+
const keys = _keys;
|
|
57
|
+
const scratch = _scratch;
|
|
58
|
+
|
|
59
|
+
const e = view.elements;
|
|
60
|
+
const r0 = e[2]!, r1 = e[6]!, r2 = e[10]!, r3 = e[14]!;
|
|
61
|
+
for (let i = 0; i < count; i++) {
|
|
62
|
+
keys[i] = sortableBits(r0 * positions[i * 3]! + r1 * positions[i * 3 + 1]! + r2 * positions[i * 3 + 2]! + r3);
|
|
63
|
+
idx[i] = i;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 4 遍 × 8 位。偶数遍 → 结果一定回到 idx,不用再拷一次
|
|
67
|
+
let src = idx;
|
|
68
|
+
let dst = scratch;
|
|
69
|
+
for (let pass = 0; pass < 4; pass++) {
|
|
70
|
+
const shift = pass * 8;
|
|
71
|
+
_counts.fill(0);
|
|
72
|
+
for (let i = 0; i < count; i++) _counts[(keys[src[i]!]! >>> shift) & 0xff]!++;
|
|
73
|
+
// 前缀和 → 每个桶的起始写位置
|
|
74
|
+
let sum = 0;
|
|
75
|
+
for (let b = 0; b < 256; b++) {
|
|
76
|
+
const c = _counts[b]!;
|
|
77
|
+
_counts[b] = sum;
|
|
78
|
+
sum += c;
|
|
79
|
+
}
|
|
80
|
+
for (let i = 0; i < count; i++) {
|
|
81
|
+
const v = src[i]!;
|
|
82
|
+
dst[_counts[(keys[v]! >>> shift) & 0xff]!++] = v;
|
|
83
|
+
}
|
|
84
|
+
const t = src;
|
|
85
|
+
src = dst;
|
|
86
|
+
dst = t;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 4 遍之后 src 就是 idx(起始 src=idx,偶数次交换后回到 idx);防御性兜一下
|
|
90
|
+
if (src !== idx) idx.set(src);
|
|
91
|
+
return idx;
|
|
92
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S3M 的 uv 第三分量(顶点属性 `custom0`,官方叫 `aTextureCoordMatrix`)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要它:官方引擎对普通(非 S3MBLOCK)S3M 瓦片并不是「有纹理就整张平铺」。
|
|
5
|
+
* 每个顶点在 uv 之外还带一个 z,iClient3D 在**顶点着色器**里拿它解出一个纹理矩阵:
|
|
6
|
+
*
|
|
7
|
+
* `getTextureMatrixFromZValue( floor(z), xTran, yTran, scale )`
|
|
8
|
+
* bit[0:7] → yTran (图集列号)
|
|
9
|
+
* bit[8:15] → xTran (图集行号)
|
|
10
|
+
* bit[16:19] → level (scale = 1 / 2^level,即图集是 2^level × 2^level 格)
|
|
11
|
+
* 三档哨兵(官方 bundle getTextureColor / getUV 实测):
|
|
12
|
+
* z < -99900 → `return vec4(1.0)` 纯白不采样(平涂顶点色面,值 -99999)
|
|
13
|
+
* z < -9000 → 「uv 原样平铺整图」:calculateTexCoord 里 `out = inTexCoord.xy`
|
|
14
|
+
* (不 fract 不缩放),普通 texture2D(自动 mip)采样
|
|
15
|
+
* 其余 → 图集子区域公式
|
|
16
|
+
*
|
|
17
|
+
* 片元侧是 `calculateMipLevel` + `calculateTexCoord`:
|
|
18
|
+
*
|
|
19
|
+
* fTile = scale * texWidth // 一格在图里的像素宽
|
|
20
|
+
* mip = clamp( 0.5*log2(d), 0, log2(fTile) - 1.62 )
|
|
21
|
+
* f = clamp( fract(uv), 2^mip/fTile, 1 - 2^mip/fTile ) // 越糊越往格子中心收缩
|
|
22
|
+
* out = ( f + T ) * scale
|
|
23
|
+
*
|
|
24
|
+
* 实测 SuperMap Online 的 `3D-BIMbuilding-2`(OSGB 转 S3M,全部 7 个根瓦片):
|
|
25
|
+
* z ∈ { -99999, 0, 65536, 65537, 65792, 65793, 262144, 327680 }
|
|
26
|
+
* uv 是米制世界坐标(1 单位 ≈ 1 m),每米一个纹理周期。
|
|
27
|
+
*
|
|
28
|
+
* 本文件是这段数学的**唯一可执行版本**:着色器里是它的 GLSL 直译
|
|
29
|
+
* (见 render/StyleMaterial.ts 的 VERT_TEXCOORD_Z / s3mTexCoordZUnit),
|
|
30
|
+
* 单测 tests/unit/texcoord-z-math.test.ts 钉住两边必须一致。
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/** 官方哨兵:`inTexCoord.z < -9000` 走「uv 原样平铺」分支(getTexColorForS3M / calculateTexCoord) */
|
|
34
|
+
export const TEX_COORD_Z_DISABLED_BELOW = -9000;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 官方白色哨兵:`texCoordZ < -99900` 时 getTextureColor 直接 `return vec4(1.0)`,
|
|
38
|
+
* **完全不采样纹理**(数据里是 -99999)。最终颜色 = 白 × 材质 diffuse × 顶点色,
|
|
39
|
+
* 即「平涂顶点色面」—— BIM 场景的地面标线 / 色块就是这类(SMID 3464:灰色地面 +
|
|
40
|
+
* 黄色标线 + 暗红区域,全靠顶点色)。
|
|
41
|
+
*
|
|
42
|
+
* 注意与 -9000 平铺哨兵的区分:-99999 一旦按平铺处理,uv(-85..85 这类米制值)
|
|
43
|
+
* 会把整张纹理平铺上百次,表现为「uv 整图」——这正是三档语义漏掉白色档的教训。
|
|
44
|
+
*/
|
|
45
|
+
export const TEX_COORD_Z_WHITE_BELOW = -99900;
|
|
46
|
+
|
|
47
|
+
/** 官方 calculateMipLevel 里那个上限收缩量 */
|
|
48
|
+
export const MIP_LIMIT_BIAS = 1.62;
|
|
49
|
+
|
|
50
|
+
/** 该顶点是否走「uv 原样平铺」分支。没有这条属性的数据 z 取 0,返回 false,行为不变。 */
|
|
51
|
+
export function isTexCoordDisabled(z: number): boolean {
|
|
52
|
+
return z < TEX_COORD_Z_DISABLED_BELOW;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 该顶点是否走「纯白、不采样」分支(z < -99900,实测值 -99999)。优先级高于平铺。 */
|
|
56
|
+
export function isTexCoordWhite(z: number): boolean {
|
|
57
|
+
return z < TEX_COORD_Z_WHITE_BELOW;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** 解码结果。`disabled` 为真时其余字段无意义(着色器里对应 scale = -1;`white` 时 scale = -2) */
|
|
61
|
+
export interface TexMatrix {
|
|
62
|
+
disabled: boolean;
|
|
63
|
+
/** z < -99900:纯白不采样(见 TEX_COORD_Z_WHITE_BELOW) */
|
|
64
|
+
white: boolean;
|
|
65
|
+
/** 一格占整张纹理的比例,1/2^level */
|
|
66
|
+
scale: number;
|
|
67
|
+
/** 图集列号 */
|
|
68
|
+
xTran: number;
|
|
69
|
+
/** 图集行号 */
|
|
70
|
+
yTran: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 官方 `getTextureMatrixFromZValue` 的逐行移植。
|
|
75
|
+
*
|
|
76
|
+
* 注意 `nZ <= 0` 时官方直接 return,**不改**外部初值 —— 也就是单位变换
|
|
77
|
+
* `scale=1, T=(0,0)`,等价于整张纹理按 uv 平铺。
|
|
78
|
+
*
|
|
79
|
+
* 着色器侧的档位编码:scale = -2 → 白;-1 → 平铺;>0 → 图集公式。
|
|
80
|
+
*/
|
|
81
|
+
export function decodeTexMatrix(z: number): TexMatrix {
|
|
82
|
+
if (isTexCoordWhite(z)) {
|
|
83
|
+
return { disabled: true, white: true, scale: -2, xTran: 0, yTran: 0 };
|
|
84
|
+
}
|
|
85
|
+
if (isTexCoordDisabled(z)) {
|
|
86
|
+
return { disabled: true, white: false, scale: -1, xTran: 0, yTran: 0 };
|
|
87
|
+
}
|
|
88
|
+
const nz = Math.floor(z);
|
|
89
|
+
if (nz <= 0) {
|
|
90
|
+
return { disabled: false, white: false, scale: 1, xTran: 0, yTran: 0 };
|
|
91
|
+
}
|
|
92
|
+
const nDel8 = Math.floor(nz / 256);
|
|
93
|
+
const nDel16 = Math.floor(nDel8 / 256);
|
|
94
|
+
const nDel20 = Math.floor(nDel16 / 16);
|
|
95
|
+
return {
|
|
96
|
+
disabled: false,
|
|
97
|
+
white: false,
|
|
98
|
+
xTran: nDel8 - nDel16 * 256,
|
|
99
|
+
yTran: nz - nDel8 * 256,
|
|
100
|
+
scale: 1 / Math.pow(2, nDel16 - nDel20 * 16),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** 官方 `vTexMatrix.w = log2( uTexture0Width * vTexMatrix.z )` */
|
|
105
|
+
export function texMaxMip(texWidth: number, scale: number): number {
|
|
106
|
+
return Math.log2(texWidth * scale);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const clamp = (v: number, lo: number, hi: number) => Math.min(Math.max(v, lo), hi);
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 官方 `calculateMipLevel` 的逐行移植。
|
|
113
|
+
*
|
|
114
|
+
* 四个导数参数是 `dFdx(u) / dvDx / duDy / dFdy(v)`(uv 单位每像素),
|
|
115
|
+
* 内部乘 `fTile = scale*texWidth` 换成纹素单位 —— 与官方
|
|
116
|
+
* `dFdx( inTexCoord * vecTile )` 完全一致。
|
|
117
|
+
*
|
|
118
|
+
* ★ 必须用**未 fract 的 uv** 求导:先 fract 再求导会在平铺接缝处炸出一个
|
|
119
|
+
* 巨大导数,硬件会把整块面采成最糊的 mip,表现为每条接缝一条暗带。
|
|
120
|
+
*/
|
|
121
|
+
export function texMipLevel(
|
|
122
|
+
duDx: number,
|
|
123
|
+
dvDx: number,
|
|
124
|
+
duDy: number,
|
|
125
|
+
dvDy: number,
|
|
126
|
+
fTile: number,
|
|
127
|
+
maxMip: number
|
|
128
|
+
): number {
|
|
129
|
+
const xU = duDx * fTile;
|
|
130
|
+
const xV = dvDx * fTile;
|
|
131
|
+
const yU = duDy * fTile;
|
|
132
|
+
const yV = dvDy * fTile;
|
|
133
|
+
const dotX = xU * xU + xV * xV;
|
|
134
|
+
const dotY = yU * yU + yV * yV;
|
|
135
|
+
const dMax = Math.max(dotX, dotY);
|
|
136
|
+
const dMin = Math.min(dotX, dotY);
|
|
137
|
+
const off = clamp((dMax - dMin) / Math.max(dMax + dMin, 1e-8), 0, 1);
|
|
138
|
+
const d = dMax * (1 - off) + dMin * off;
|
|
139
|
+
return clamp(0.5 * Math.log2(Math.max(d, 1e-8)), 0, maxMip - MIP_LIMIT_BIAS);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 官方 `calculateTexCoord` 的逐行移植:fract → 按 mip 内缩 → 平移 + 缩放。
|
|
144
|
+
*
|
|
145
|
+
* `mip` 必须已经过 `texMipLevel` 的上限裁剪(≤ maxMip - 1.62),
|
|
146
|
+
* 否则 `2^mip/fTile > 0.5` 时 clamp 的上下界会翻转,官方同样会出问题。
|
|
147
|
+
*/
|
|
148
|
+
export function sampleTexCoord(
|
|
149
|
+
m: TexMatrix,
|
|
150
|
+
u: number,
|
|
151
|
+
v: number,
|
|
152
|
+
texWidth: number,
|
|
153
|
+
mip: number
|
|
154
|
+
): { u: number; v: number } {
|
|
155
|
+
const fTile = Math.max(m.scale * texWidth, 1);
|
|
156
|
+
const inner = Math.pow(2, mip) / fTile;
|
|
157
|
+
const fu = clamp(u - Math.floor(u), inner, 1 - inner);
|
|
158
|
+
const fv = clamp(v - Math.floor(v), inner, 1 - inner);
|
|
159
|
+
return { u: (fu + m.xTran) * m.scale, v: (fv + m.yTran) * m.scale };
|
|
160
|
+
}
|
|
@@ -32,6 +32,12 @@ export interface DecodedTextureData {
|
|
|
32
32
|
*/
|
|
33
33
|
blocks?: Uint8Array;
|
|
34
34
|
blockFormat?: 'dxt1' | 'dxt5';
|
|
35
|
+
/**
|
|
36
|
+
* 数据自带的 DXT 内联 mip 链(S3M 纹理的 data 常是 level0 + 完整链,`tex.level`
|
|
37
|
+
* 记录层数)。官方 iClient3D 把整条链逐层上传,远处采样高层级平均成纯色。
|
|
38
|
+
* 与 blocks 同时存在;渲染层用它构造多层 CompressedTexture。
|
|
39
|
+
*/
|
|
40
|
+
mipmaps?: { data: Uint8Array; width: number; height: number }[];
|
|
35
41
|
/** CRN 转码失败时的原始错误(渲染层负责打 warn) */
|
|
36
42
|
error?: string;
|
|
37
43
|
}
|
|
@@ -195,6 +201,33 @@ export function expectedDxtByteLength(width: number, height: number, format: 'dx
|
|
|
195
201
|
return bw * bh * (format === 'dxt1' ? 8 : 16);
|
|
196
202
|
}
|
|
197
203
|
|
|
204
|
+
/**
|
|
205
|
+
* 切 DXT 内联 mip 链:`data` = level0 + 逐层减半直到 1×1,每层 ceil(w/4)*ceil(h/4)*blockBytes。
|
|
206
|
+
* 切不出与总字节数严丝合缝的链(不是链 / 格式对不上)返回 null,调用方回退。
|
|
207
|
+
*/
|
|
208
|
+
export function sliceDxtMipChain(
|
|
209
|
+
data: Uint8Array,
|
|
210
|
+
width: number,
|
|
211
|
+
height: number,
|
|
212
|
+
blockBytes: number
|
|
213
|
+
): { data: Uint8Array; width: number; height: number }[] | null {
|
|
214
|
+
const levels: { data: Uint8Array; width: number; height: number }[] = [];
|
|
215
|
+
let w = width;
|
|
216
|
+
let h = height;
|
|
217
|
+
let off = 0;
|
|
218
|
+
for (;;) {
|
|
219
|
+
const bytes = Math.ceil(w / 4) * Math.ceil(h / 4) * blockBytes;
|
|
220
|
+
if (off + bytes > data.byteLength) return null;
|
|
221
|
+
levels.push({ data: data.subarray(off, off + bytes), width: w, height: h });
|
|
222
|
+
off += bytes;
|
|
223
|
+
if (w === 1 && h === 1) break;
|
|
224
|
+
w = Math.max(1, w >> 1);
|
|
225
|
+
h = Math.max(1, h >> 1);
|
|
226
|
+
}
|
|
227
|
+
if (off !== data.byteLength) return null;
|
|
228
|
+
return levels;
|
|
229
|
+
}
|
|
230
|
+
|
|
198
231
|
/** 主线程 / Worker 共用的解码入口。不抛错:失败与不支持都体现在返回值里 */
|
|
199
232
|
export async function decodeTextureData(
|
|
200
233
|
tex: S3MTexture,
|
|
@@ -230,6 +263,7 @@ async function decodeTextureDataRaw(
|
|
|
230
263
|
const compressible = (data: Uint8Array, w: number, h: number, format: 'dxt1' | 'dxt5'): boolean =>
|
|
231
264
|
data.byteLength === expectedDxtByteLength(w, h, format);
|
|
232
265
|
|
|
266
|
+
|
|
233
267
|
if (kind === 'crn') {
|
|
234
268
|
try {
|
|
235
269
|
const crn = await transcodeCRN(tex.data);
|
|
@@ -279,17 +313,35 @@ async function decodeTextureDataRaw(
|
|
|
279
313
|
if (kind === 'dxt1' || kind === 'dxt3' || kind === 'dxt4' || kind === 'dxt5') {
|
|
280
314
|
const pf = dxtPixelFormatOf(kind);
|
|
281
315
|
// dxt3/dxt4 少见,不进直传白名单;块长与 4×4 对齐公式不符的也回退解码
|
|
282
|
-
if (canCompress && (kind === 'dxt1' || kind === 'dxt5')
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
316
|
+
if (canCompress && (kind === 'dxt1' || kind === 'dxt5')) {
|
|
317
|
+
const level0Bytes = expectedDxtByteLength(tex.width, tex.height, kind);
|
|
318
|
+
if (tex.data.byteLength === level0Bytes && compressible(tex.data, tex.width, tex.height, kind)) {
|
|
319
|
+
return {
|
|
320
|
+
kind: `${kind}(c)`,
|
|
321
|
+
width: tex.width,
|
|
322
|
+
height: tex.height,
|
|
323
|
+
rgba: null,
|
|
324
|
+
bitmap: null,
|
|
325
|
+
hasAlpha: kind === 'dxt5',
|
|
326
|
+
blocks: tex.data,
|
|
327
|
+
blockFormat: kind,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
// 带内联 mip 链:整条切出来逐层上传(官方同款)。切不齐(字节不吻合)再回退解码
|
|
331
|
+
const chain = sliceDxtMipChain(tex.data, tex.width, tex.height, kind === 'dxt1' ? 8 : 16);
|
|
332
|
+
if (chain && chain.length > 1) {
|
|
333
|
+
return {
|
|
334
|
+
kind: `${kind}(chain)`,
|
|
335
|
+
width: tex.width,
|
|
336
|
+
height: tex.height,
|
|
337
|
+
rgba: null,
|
|
338
|
+
bitmap: null,
|
|
339
|
+
hasAlpha: kind === 'dxt5',
|
|
340
|
+
blocks: tex.data,
|
|
341
|
+
blockFormat: kind,
|
|
342
|
+
mipmaps: chain,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
293
345
|
}
|
|
294
346
|
const decoded = decodeDXT({ ...tex, pixelFormat: pf });
|
|
295
347
|
return { kind, width: tex.width, height: tex.height, rgba: decoded.rgba, bitmap: null, hasAlpha: decoded.hasAlpha };
|