s3m-three 0.4.17 → 0.4.19
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-4iTAmWIu.js +1 -0
- package/dist/browser/s3m-three.iife.ext-three.js +406 -91
- package/dist/browser/s3m-three.iife.js +434 -119
- package/dist/core/AttributeQuery.d.ts +13 -0
- package/dist/core/AttributeQuery.d.ts.map +1 -1
- package/dist/core/AttributeQuery.js +20 -3
- package/dist/core/AttributeQuery.js.map +1 -1
- package/dist/core/AuthFetch.d.ts +52 -0
- package/dist/core/AuthFetch.d.ts.map +1 -0
- package/dist/core/AuthFetch.js +44 -0
- package/dist/core/AuthFetch.js.map +1 -0
- 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 +56 -14
- 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 +13 -0
- package/dist/core/SplatSort.d.ts.map +1 -0
- package/dist/core/SplatSort.js +14 -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 +29 -0
- package/dist/core/TileGeometry.d.ts.map +1 -1
- package/dist/core/TileGeometry.js +130 -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 +11 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/render/DatasetManager.d.ts +18 -1
- package/dist/render/DatasetManager.d.ts.map +1 -1
- package/dist/render/DatasetManager.js +4 -1
- 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 +24 -0
- package/dist/render/GeometryBuilder.d.ts.map +1 -1
- package/dist/render/GeometryBuilder.js +196 -5
- package/dist/render/GeometryBuilder.js.map +1 -1
- package/dist/render/HostRenderer.d.ts +105 -0
- package/dist/render/HostRenderer.d.ts.map +1 -0
- package/dist/render/HostRenderer.js +170 -0
- package/dist/render/HostRenderer.js.map +1 -0
- 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 +79 -0
- package/dist/render/S3MViewer.d.ts.map +1 -1
- package/dist/render/S3MViewer.js +235 -16
- 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 +59 -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/tileset/S3MTileset.d.ts +50 -0
- package/dist/tileset/S3MTileset.d.ts.map +1 -1
- package/dist/tileset/S3MTileset.js +60 -5
- 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 +338 -0
- package/package.json +1 -1
- package/src/core/AttributeQuery.ts +26 -3
- package/src/core/AuthFetch.ts +82 -0
- 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 +56 -11
- package/src/core/SplatLod.ts +83 -0
- package/src/core/SplatProjection.ts +231 -0
- package/src/core/SplatSort.ts +30 -0
- package/src/core/TexCoordZ.ts +160 -0
- package/src/core/TextureDecode.ts +63 -11
- package/src/core/TileGeometry.ts +138 -14
- package/src/core/degradations.ts +74 -0
- package/src/core/types.ts +32 -0
- package/src/index.ts +12 -1
- package/src/render/DatasetManager.ts +19 -1
- package/src/render/FeatureStyleManager.ts +7 -5
- package/src/render/GeometryBuilder.ts +239 -6
- package/src/render/HostRenderer.ts +222 -0
- package/src/render/Picker.ts +21 -5
- package/src/render/S3MViewer.ts +277 -15
- package/src/render/SplatMaterial.ts +433 -0
- package/src/render/SplatSorter.ts +59 -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/tileset/S3MTileset.ts +91 -5
- package/src/vendor/browser-node-stub.js +10 -0
- package/dist/browser/assets/s3mWorker-BjPOUgjg.js +0 -1
|
@@ -73,6 +73,8 @@ import { parseGroupNodeEntries, parseSkeletonEntries, parsePickInfoEntries } fro
|
|
|
73
73
|
import { atlasAlphaMask, shrinkRectToOpaque } from './AtlasAlpha.js';
|
|
74
74
|
import { packAtlases, decodeAtlasImage } from './AtlasPack.js';
|
|
75
75
|
import type { PackedAtlases } from './AtlasPack.js';
|
|
76
|
+
import { transcodeCRN } from './CRNDecode.js';
|
|
77
|
+
import { resolveTextureKind, PixelFormat, TextureCompress } from './TextureFormat.js';
|
|
76
78
|
|
|
77
79
|
/** 图集子区域,以及它绑定的几何顶点区间 */
|
|
78
80
|
export interface S3MSubTextureBinding {
|
|
@@ -487,13 +489,35 @@ export async function parseTexblockSegments(
|
|
|
487
489
|
): Promise<S3MBlockAtlasImage[][]> {
|
|
488
490
|
const inflate = options.inflate ?? defaultInflate;
|
|
489
491
|
const bytes = new Uint8Array(arrayBuffer);
|
|
490
|
-
|
|
491
|
-
if (
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
492
|
+
let segments: S3MBlockAtlasImage[][] | null = await tryDynPack(bytes, inflate);
|
|
493
|
+
if (!segments) segments = await tryStaticPack(bytes, inflate);
|
|
494
|
+
if (!segments) {
|
|
495
|
+
const container = await readContainerEntries(bytes, inflate);
|
|
496
|
+
if (container) segments = [container];
|
|
497
|
+
}
|
|
498
|
+
if (!segments) {
|
|
499
|
+
throw new Error('[s3m] .texblock 无法识别的外层格式(动态 pack / 静态 pack / 容器均不成立)');
|
|
500
|
+
}
|
|
501
|
+
// 3D-CBD-2 的 texblock 图像是 CRN 压缩(ct=26/27,'Hx' 魔数)。leaf 组合管线
|
|
502
|
+
// (composeLeafAtlas → decodeAtlasImage)只认 DXT / 已解 RGBA,这里统一在解析层
|
|
503
|
+
// 转码成 DXT:与主纹理解码的 crn->dxt 路径同一份 crunch wasm。
|
|
504
|
+
// 转码失败的记录原样保留(下游按缺失精灵处理并计数,不静默吞)。
|
|
505
|
+
for (const seg of segments) {
|
|
506
|
+
for (const rec of seg) {
|
|
507
|
+
if (resolveTextureKind(rec.compressType, rec.pixelFormat) !== 'crn') continue;
|
|
508
|
+
try {
|
|
509
|
+
const crn = await transcodeCRN(rec.data);
|
|
510
|
+
rec.data = crn.data;
|
|
511
|
+
rec.width = crn.width;
|
|
512
|
+
rec.height = crn.height;
|
|
513
|
+
rec.compressType = crn.dxtFormat === 1 ? TextureCompress.DXT1_RGBA : 14; // 14 = DXT5(见 TextureFormat 实测注释)
|
|
514
|
+
rec.pixelFormat = crn.dxtFormat === 1 ? PixelFormat.DXT1 : PixelFormat.DXT5;
|
|
515
|
+
} catch {
|
|
516
|
+
// 保留原记录,交给下游缺失统计
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return segments;
|
|
497
521
|
}
|
|
498
522
|
|
|
499
523
|
/** 解压后的裸图像记录区:`ct|w|h|pf|size|data` 可多条连排,尾部允许有无关字节 */
|
|
@@ -512,6 +536,27 @@ function readImageRecords(raw: Uint8Array, id: string): S3MBlockAtlasImage[] {
|
|
|
512
536
|
return out;
|
|
513
537
|
}
|
|
514
538
|
|
|
539
|
+
/**
|
|
540
|
+
* 条目负载解压。`zip == unzip` 是**存储态**(未压缩,内容直接就是图像记录区):
|
|
541
|
+
* 3D-CBD-2 的 Tex_0_000.texblock 实测存在这种条目,无条件 inflate 会把整包
|
|
542
|
+
* 判成「无法识别的外层格式」。zlib 流必有 ~11 字节头尾开销,zip == unzip
|
|
543
|
+
* 不可能是有效 zlib,此判定安全。
|
|
544
|
+
*/
|
|
545
|
+
async function inflateEntryPayload(
|
|
546
|
+
payload: Uint8Array,
|
|
547
|
+
unzipSize: number,
|
|
548
|
+
zipSize: number,
|
|
549
|
+
inflate: InflateFn
|
|
550
|
+
): Promise<Uint8Array | null> {
|
|
551
|
+
if (zipSize === unzipSize) return payload;
|
|
552
|
+
try {
|
|
553
|
+
const raw = await inflate(payload);
|
|
554
|
+
return raw.length === unzipSize ? raw : null;
|
|
555
|
+
} catch {
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
515
560
|
/** 读 `count | 条目 × count` 容器(尾部允许有无关字节)。结构不成立返回 null。 */
|
|
516
561
|
async function readContainerEntries(bytes: Uint8Array, inflate: InflateFn): Promise<S3MBlockAtlasImage[] | null> {
|
|
517
562
|
if (bytes.byteLength < 8) return null;
|
|
@@ -529,8 +574,8 @@ async function readContainerEntries(bytes: Uint8Array, inflate: InflateFn): Prom
|
|
|
529
574
|
const unzipSize = r.u32();
|
|
530
575
|
const zipSize = r.u32();
|
|
531
576
|
if (zipSize > r.remaining) return null;
|
|
532
|
-
const raw = await
|
|
533
|
-
if (raw.length !== unzipSize) return null;
|
|
577
|
+
const raw = await inflateEntryPayload(r.bytesCopy(zipSize), unzipSize, zipSize, inflate);
|
|
578
|
+
if (!raw || raw.length !== unzipSize) return null;
|
|
534
579
|
out.push(...readImageRecords(raw, name));
|
|
535
580
|
}
|
|
536
581
|
return out.length ? out : null;
|
|
@@ -552,8 +597,8 @@ async function readSingleEntry(bytes: Uint8Array, inflate: InflateFn): Promise<S
|
|
|
552
597
|
const unzipSize = r.u32();
|
|
553
598
|
const zipSize = r.u32();
|
|
554
599
|
if (zipSize !== r.remaining) return null;
|
|
555
|
-
const raw = await
|
|
556
|
-
if (raw.length !== unzipSize) return null;
|
|
600
|
+
const raw = await inflateEntryPayload(r.bytesCopy(zipSize), unzipSize, zipSize, inflate);
|
|
601
|
+
if (!raw || raw.length !== unzipSize) return null;
|
|
557
602
|
const recs = readImageRecords(raw, name);
|
|
558
603
|
return recs.length ? recs : null;
|
|
559
604
|
} catch {
|
|
@@ -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,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 高斯泼溅的 back-to-front 排序键(纯数学,可单测)
|
|
3
|
+
*
|
|
4
|
+
* alpha 混合是顺序相关的:先画近的再画远的,远的颜色会盖到近的上头,
|
|
5
|
+
* 画面就是一片随视角乱闪的彩色噪点。3DGS 必须按视深从远到近画。
|
|
6
|
+
*
|
|
7
|
+
* 排序键是**视图空间 z**(相机前方为负),升序 = 远的先画。
|
|
8
|
+
* view 矩阵由调用方给(通常是 camera.matrixWorldInverse × mesh.matrixWorld),
|
|
9
|
+
* 本模块不关心相机朝向约定,只取矩阵第三行做点积。
|
|
10
|
+
*/
|
|
11
|
+
import type * as THREE from 'three';
|
|
12
|
+
|
|
13
|
+
export function splatDepthOrder(
|
|
14
|
+
positions: Float32Array,
|
|
15
|
+
count: number,
|
|
16
|
+
view: THREE.Matrix4,
|
|
17
|
+
out?: Uint32Array
|
|
18
|
+
): Uint32Array {
|
|
19
|
+
const idx = out && out.length >= count ? out : new Uint32Array(count);
|
|
20
|
+
const keys = new Float32Array(count);
|
|
21
|
+
const e = view.elements;
|
|
22
|
+
const r0 = e[2]!, r1 = e[6]!, r2 = e[10]!, r3 = e[14]!;
|
|
23
|
+
for (let i = 0; i < count; i++) {
|
|
24
|
+
idx[i] = i;
|
|
25
|
+
keys[i] = r0 * positions[i * 3]! + r1 * positions[i * 3 + 1]! + r2 * positions[i * 3 + 2]! + r3;
|
|
26
|
+
}
|
|
27
|
+
// TypedArray.sort 带比较函数时是数值排序,且 V8/JSC 对相同键稳定
|
|
28
|
+
idx.sort((a, b) => keys[a]! - keys[b]!);
|
|
29
|
+
return idx;
|
|
30
|
+
}
|
|
@@ -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
|
+
}
|