├── .clang-format
├── .gitignore
├── .prettierrc
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── README_EN.md
├── README_ZH.md
├── SPX_EN.md
├── SPX_ZH.md
├── buildPkg.js
├── index.html
├── package-lock.json
├── package.json
├── pkg
├── README.md
└── package.json
├── public
└── favicon.ico
├── src
├── index.ts
└── reall3d
│ ├── api
│ └── SetupApi.ts
│ ├── assets
│ └── icons
│ │ ├── delete.svg
│ │ ├── edit.svg
│ │ ├── point1.svg
│ │ ├── point2.svg
│ │ ├── point3.svg
│ │ └── position1.svg
│ ├── controls
│ ├── CameraControls.ts
│ ├── SetupCameraControls.ts
│ └── SetupFlying.ts
│ ├── events
│ ├── EventConstants.ts
│ ├── EventListener.ts
│ └── Events.ts
│ ├── internal
│ ├── Index.ts
│ └── WebglVars.ts
│ ├── mapviewer
│ ├── Reall3dMapViewer.ts
│ ├── Reall3dMapViewerOptions.ts
│ ├── events
│ │ └── MapEventListener.ts
│ ├── tween
│ │ └── SetupTween.ts
│ ├── utils
│ │ └── MapUtils.ts
│ └── warpsplatmesh
│ │ └── WarpSplatMesh.ts
│ ├── meshs
│ ├── controlplane
│ │ ├── ArrowHelper.ts
│ │ └── SetupControlPlane.ts
│ ├── focusmaker
│ │ └── SetupFocusMarker.ts
│ ├── mark
│ │ ├── MarkCirclePlan.ts
│ │ ├── MarkDistanceLine.ts
│ │ ├── MarkMulitPlans.ts
│ │ ├── MarkMultiLines.ts
│ │ ├── MarkSinglePoint.ts
│ │ ├── SetupMark.ts
│ │ └── data
│ │ │ ├── MarkData.ts
│ │ │ ├── MarkDataCirclePlan.ts
│ │ │ ├── MarkDataDistanceLine.ts
│ │ │ ├── MarkDataMultiLines.ts
│ │ │ ├── MarkDataMultiPlans.ts
│ │ │ └── MarkDataSinglePoint.ts
│ └── splatmesh
│ │ ├── SetupSplatMesh.ts
│ │ ├── SplatMesh.ts
│ │ ├── SplatMeshOptions.ts
│ │ └── SplatMeshWebgl.ts
│ ├── modeldata
│ ├── ModelData.ts
│ ├── ModelOptions.ts
│ ├── SplatTexdata.ts
│ ├── SplatTexdataManager.ts
│ ├── loaders
│ │ ├── PlyLoader.ts
│ │ ├── SplatLoader.ts
│ │ ├── SpxLoader.ts
│ │ └── SpzLoader.ts
│ ├── text
│ │ └── SetupGaussianText.ts
│ └── wasm
│ │ ├── WasmParser.ts
│ │ └── open.cpp
│ ├── pkg.ts
│ ├── raycaster
│ └── SetupRaycaster.ts
│ ├── sorter
│ ├── SetupSorter.ts
│ └── Sorter.ts
│ ├── style
│ └── style.less
│ ├── utils
│ ├── CommonUtils.ts
│ ├── ViewerUtils.ts
│ └── consts
│ │ ├── GlobalConstants.ts
│ │ ├── Index.ts
│ │ └── WkConstants.ts
│ └── viewer
│ ├── Reall3dViewer.ts
│ └── Reall3dViewerOptions.ts
├── tsconfig.json
├── vite-pkg.config.ts
└── vite.config.ts
/.clang-format:
--------------------------------------------------------------------------------
1 | BasedOnStyle: LLVM
2 | BreakBeforeBraces: Attach
3 | IndentWidth: 4
4 | UseTab: Never
5 | ColumnLimit: 180
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vite/
2 | /**/dist/
3 | /**/build/
4 | /**/node_modules/
5 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 150,
3 | "tabWidth": 4,
4 | "useTabs": false,
5 | "semi": true,
6 | "singleQuote": true,
7 | "quoteProps": "as-needed",
8 | "jsxSingleQuote": false,
9 | "trailingComma": "all",
10 | "bracketSpacing": true,
11 | "jsxBracketSameLine": false,
12 | "arrowParens": "avoid",
13 | "rangeStart": 0,
14 | "requirePragma": false,
15 | "insertPragma": false,
16 | "proseWrap": "preserve",
17 | "htmlWhitespaceSensitivity": "css",
18 | "vueIndentScriptAndStyle": false,
19 | "endOfLine": "auto",
20 | "embeddedLanguageFormatting": "auto",
21 | "singleAttributePerLine": false,
22 | "wrapAttributes": true
23 | }
24 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "prettier.configPath": ".prettierrc",
3 | "editor.defaultFormatter": "esbenp.prettier-vscode",
4 | "editor.formatOnSave": true,
5 | "[cpp]": {
6 | "editor.defaultFormatter": "ms-vscode.cpptools"
7 | },
8 | "C_Cpp.errorSquiggles": "disabled"
9 | }
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 reall3d.com
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # Reall3dViewer
6 |
7 | `Reall3dViewer` is a 3D Gaussian Splatting viewer built on Three.js. Crafting an exceptional 3DGS viewer is no small feat, which is why we've chosen to open-source our project. We hope to harness the collective wisdom and efforts of the community to drive the advancement of 3DGS applications together!
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | ## Features
22 | - [x] Support formats: `.ply`, `.splat`, `.spx`, `.spz`
23 | - [x] Support mark and measurement
24 | - [x] Support text watermark
25 | - [x] Support 1st to 3nd degree spherical harmonics
26 | - [x] Support rendering models within maps
27 |
28 |
29 | ## Live demo
30 | - https://reall3d.com/reall3dviewer/index.html
31 |
32 |
33 | ## `.spx`
34 |
35 | - Format Specification: https://github.com/reall3d-com/Reall3dViewer/blob/main/SPX_EN.md
36 | - Conversion Tool: https://github.com/gotoeasy/gsbox
37 |
38 | ## Basic Usage
39 |
40 | use source code
41 | ```shell
42 | # develop
43 | npm run dev
44 |
45 | # build
46 | npm run build
47 |
48 | # open a web browser to render your 3dgs model
49 | # http://hostname:port/index.html?url=your-model-link-address
50 |
51 | # .spx file can be obtained through conversion using the gsbox
52 | gsbox p2x -i /path/to/input.ply -o /path/to/output.spx -sh 0
53 | ```
54 |
55 | use npm package [sample project here](https://github.com/reall3d-com/reall3dviewer-samples-use-npm-package)
56 | ```shell
57 | # install
58 | npm install @reall3d/reall3dviewer
59 |
60 | # use built-in viewer
61 | const viewer = new Reall3dViewer({ root: '#gsviewer' });
62 | viewer.addModel(`https://reall3d.com/demo-models/yz.spx`);
63 |
64 | # use splat mesh
65 | const splatMesh = new SplatMesh({ renderer, scene, controls});
66 | splatMesh.addModel({ url: 'https://reall3d.com/demo-models/yz.spx' });
67 | scene.add(splatMesh);
68 | ```
69 |
70 |
71 | ## TODO
72 | - Continuously optimize and enhance rendering performance
73 | - Large scene
74 |
75 | ## Release History
76 | https://github.com/reall3d-com/Reall3dViewer/releases
77 |
78 |
79 | ## Acknowledgments
80 | We would like to express our gratitude to the following projects for their valuable reference implementations
81 | - https://github.com/antimatter15/splat
82 | - https://github.com/mkkellogg/GaussianSplats3D
83 | - https://github.com/huggingface/gsplat.js
84 | - https://github.com/playcanvas/supersplat
85 | - https://github.com/sxguojf/three-tile
86 |
87 |
88 | ## Contact
89 | Feel free to submit an issue on the project page. Our commercial version offers a 3DGS model format optimization tool and supports embedding watermarks to protect model ownership. Please don't hesitate to contact us.
90 | - Site: https://reall3d.com
91 | - Email: ai@geohold.com
92 |
--------------------------------------------------------------------------------
/README_EN.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # Reall3dViewer
6 |
7 | `Reall3dViewer` is a 3D Gaussian Splatting viewer built on Three.js. Crafting an exceptional 3DGS viewer is no small feat, which is why we've chosen to open-source our project. We hope to harness the collective wisdom and efforts of the community to drive the advancement of 3DGS applications together!
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | ## Features
22 | - [x] Support formats: `.ply`, `.splat`, `.spx`, `.spz`
23 | - [x] Support mark and measurement
24 | - [x] Support text watermark
25 | - [x] Support 1st to 3nd degree spherical harmonics
26 | - [x] Support rendering models within maps
27 |
28 |
29 | ## Live demo
30 | - https://reall3d.com/reall3dviewer/index.html
31 |
32 |
33 | ## `.spx`
34 |
35 | - Format Specification: https://github.com/reall3d-com/Reall3dViewer/blob/main/SPX_EN.md
36 | - Conversion Tool: https://github.com/gotoeasy/gsbox
37 |
38 | ## Basic Usage
39 |
40 | use source code
41 | ```shell
42 | # develop
43 | npm run dev
44 |
45 | # build
46 | npm run build
47 |
48 | # open a web browser to render your 3dgs model
49 | # http://hostname:port/index.html?url=your-model-link-address
50 |
51 | # .spx file can be obtained through conversion using the gsbox
52 | gsbox p2x -i /path/to/input.ply -o /path/to/output.spx -sh 0
53 | ```
54 |
55 | use npm package [sample project here](https://github.com/reall3d-com/reall3dviewer-samples-use-npm-package)
56 | ```shell
57 | # install
58 | npm install @reall3d/reall3dviewer
59 |
60 | # use built-in viewer
61 | const viewer = new Reall3dViewer({ root: '#gsviewer' });
62 | viewer.addModel(`https://reall3d.com/demo-models/yz.spx`);
63 |
64 | # use splat mesh
65 | const splatMesh = new SplatMesh({ renderer, scene, controls});
66 | splatMesh.addModel({ url: 'https://reall3d.com/demo-models/yz.spx' });
67 | scene.add(splatMesh);
68 | ```
69 |
70 |
71 | ## TODO
72 | - Continuously optimize and enhance rendering performance
73 | - Large scene
74 |
75 | ## Release History
76 | https://github.com/reall3d-com/Reall3dViewer/releases
77 |
78 |
79 | ## Acknowledgments
80 | We would like to express our gratitude to the following projects for their valuable reference implementations
81 | - https://github.com/antimatter15/splat
82 | - https://github.com/mkkellogg/GaussianSplats3D
83 | - https://github.com/huggingface/gsplat.js
84 | - https://github.com/playcanvas/supersplat
85 | - https://github.com/sxguojf/three-tile
86 |
87 |
88 | ## Contact
89 | Feel free to submit an issue on the project page. Our commercial version offers a 3DGS model format optimization tool and supports embedding watermarks to protect model ownership. Please don't hesitate to contact us.
90 | - Site: https://reall3d.com
91 | - Email: ai@geohold.com
92 |
--------------------------------------------------------------------------------
/README_ZH.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # Reall3dViewer
6 |
7 | `Reall3dViewer`是一个基于`Three.js`的`3D Gaussian Splatting`渲染器。打造卓越的`3DGS`渲染器并非易事,我们选择开源,希望能集思广益,群策群力,共同为推动`3DGS`应用发展助一臂之力!
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | ## 特点
22 | - [x] 支持格式: `.ply`、`.splat`、`.spx`、`.spz`
23 | - [x] 支持标注测量
24 | - [x] 支持文字水印
25 | - [x] 支持1~3级球谐系数
26 | - [x] 支持地图场景渲染
27 |
28 |
29 | ## 在线演示
30 | - https://reall3d.com/reall3dviewer/index.html
31 |
32 |
33 | ## `.spx`
34 |
35 | - 格式说明: https://github.com/reall3d-com/Reall3dViewer/blob/main/SPX_ZH.md
36 | - 转换工具: https://github.com/gotoeasy/gsbox
37 |
38 |
39 | ## 用法
40 |
41 | 使用源码方式
42 | ```shell
43 | # develop
44 | npm run dev
45 |
46 | # build
47 | npm run build
48 |
49 | # open a web browser to render your 3dgs model
50 | # http://hostname:port/index.html?url=your-model-link-address
51 |
52 | # .spx file can be obtained through conversion using the gsbox
53 | gsbox p2x -i /path/to/input.ply -o /path/to/output.spx -sh 0
54 | ```
55 |
56 |
57 | 使用 npm 包方式 [例子工程在这](https://github.com/reall3d-com/reall3dviewer-samples-use-npm-package)
58 | ```shell
59 | # install
60 | npm install @reall3d/reall3dviewer
61 |
62 | # use built-in viewer
63 | const viewer = new Reall3dViewer({ root: '#gsviewer' });
64 | viewer.addModel(`https://reall3d.com/demo-models/yz.spx`);
65 |
66 | # use splat mesh
67 | const splatMesh = new SplatMesh({ renderer, scene, controls});
68 | splatMesh.addModel({ url: 'https://reall3d.com/demo-models/yz.spx' });
69 | scene.add(splatMesh);
70 | ```
71 |
72 |
73 | ## TODO
74 | - 持续优化增强渲染性能
75 | - 大场景
76 |
77 |
78 | ## 履历
79 | https://github.com/reall3d-com/Reall3dViewer/releases
80 |
81 |
82 | ## 鸣谢
83 | 感谢以下项目提供的参考实现
84 | - https://github.com/antimatter15/splat
85 | - https://github.com/mkkellogg/GaussianSplats3D
86 | - https://github.com/huggingface/gsplat.js
87 | - https://github.com/playcanvas/supersplat
88 | - https://github.com/sxguojf/three-tile
89 |
90 |
91 | ## 联系
92 | 欢迎在项目页面上提交`issue`,商业版提供模型格式优化工具,支持嵌入水印保护模型产权,请随时与我们联系。
93 | - Site: https://reall3d.com
94 | - Email: ai@geohold.com
95 |
--------------------------------------------------------------------------------
/SPX_EN.md:
--------------------------------------------------------------------------------
1 | ## SPX File Format Specification
2 | The `.spx` format is a 3DGS model format designed to be fle**X**ible, e**X**tensible, and e**X**clusive.
3 |
4 |
5 |
6 |
7 |
8 | - [x] `Flexible` Optimized file header structure, flexible data blocks, and effective compression
9 | - [x] `Extensible` Open format with reserved fields for future expansion
10 | - [x] `Exclusive` Custom format identifiers for proprietary data protection
11 |
12 |
13 |
14 | ## File Header (128 bytes)
15 | Fixed-length header for format identification, containing bounding box data for sorting optimization and custom identifiers.
16 |
17 | | Byte Offset | Type | Field Name | Description |
18 | |-------------|-----------|-----------------------|-----------------------------------------------------------------------------|
19 | | 0–2 | ASCII | `*`Magic | Fixed value `spx` |
20 | | 3 | uint8 | `*`Version | Current version: `1` |
21 | | 4–7 | uint32 | `*`Gaussian Count | Total number of Gaussian points |
22 | | 8–11 | float32 | `*`MinX | Bounding box minimum X coordinate |
23 | | 12–15 | float32 | `*`MaxX | Bounding box maximum X coordinate |
24 | | 16–19 | float32 | `*`MinY | Bounding box minimum Y coordinate |
25 | | 20–23 | float32 | `*`MaxY | Bounding box maximum Y coordinate |
26 | | 24–27 | float32 | `*`MinZ | Bounding box minimum Z coordinate |
27 | | 28–31 | float32 | `*`MaxZ | Bounding box maximum Z coordinate |
28 | | 32–35 | float32 | Min Center Height | Min model center height (Y-axis) |
29 | | 36–39 | float32 | Max Center Height | Max model center height (Y-axis) |
30 | | 40–43 | uint32 | Creation Date | Date in `YYYYMMDD` format |
31 | | 44–47 | uint32 | `*`Creater ID | A unique value (other than `0` reserved for official use) to identify the creater |
32 | | 48–51 | uint32 | `*`Exclusive ID | A non-zero value (where `0` indicates public formats) defines a proprietary/private data block format |
33 | | 52 | uint8 | SH degree | Allowed values: `0,1,2,3`. Others → `0` |
34 | | 53 | uint8 | Flag1 | Distinguishes different model forms; default is `0` |
35 | | 54 | uint8 | Flag2 | Indicating whether it is inverted. default is `0` |
36 | | 55 | uint8 | Flag3 | Reserved |
37 | | 56–63 | - | Reserved | Reserved |
38 | | 64–123 | ASCII | Comment | Maximum 60 ASCII characters |
39 | | 124–127 | uint32 | `*`Checksum | Validates file integrity (creater-specific) |
40 |
41 | ---
42 |
43 | ## Data Blocks
44 | Data blocks consist of a fixed header followed by customizable content.
45 |
46 | ### Data Block Structure
47 | | Byte Offset | Type | Field Name | Description |
48 | |-------------|-----------|-----------------------|-----------------------------------------------------------------------------|
49 | | 0–3 | int32 | `*`Block Length | Length of content (excluding this field). `Negative if compressed with gzip` |
50 | | 0–n | bytes | `*`Block Content | Actual data (format defined below) |
51 |
52 | ### Data Block Content
53 | | Byte Offset | Type | Field Name | Description |
54 | |-------------|-----------|-----------------------|-----------------------------------------------------------------------------|
55 | | 0–3 | uint32 | `*`Count | Number of Gaussians in this block |
56 | | 4–7 | uint32 | `*`Format ID | Identifies data layout (0–255 = open formats; >255 = exclusive) |
57 | | 8–n | bytes | `*`Data | Structured per Format ID |
58 |
59 | ---
60 |
61 | ## Open Block Content Formats
62 |
63 | he data block format encompasses both open and exclusive formats. The reserved range from `0 to 255` is designated for defining the open format, while other values are employed for exclusive formats.
64 |
65 |
66 |
67 |
68 | ✅ Open Format `20`, basic data
69 |
70 | | Byte Offset | Type | Field Name | Description |
71 | |-------------|-----------|-----------------------|-----------------------------------------------------------------------------|
72 | | 0–3 | uint32 | `*`Gaussian Count | Number of Gaussians |
73 | | 4–7 | uint32 | `*`Format ID | `20` |
74 | | 8–n | bytes | `*`Data | x...y...z...sx...sy...sz...r...g...b...a...rw...rx...ry...rz... |
75 |
76 | - `x,y,z` Coordinates, 24-bit precision (`x`, `y`, `z`).
77 | - `sx,sy,sz` Scale, 8-bit per axis (`sx`, `sy`, `sz`).
78 | - `r,g,b,a` Color, RGBA channels (8-bit each).
79 | - `rw,rx,ry,rz` Rotation, Quaternion components (8-bit each).
80 |
81 | ---
82 |
83 |
84 | ✅ Open Format `1`, data of SH degree 1 (SH1 only)
85 |
86 |
87 | | Byte Offset | Type | Field Name | Description |
88 | |----------|------|------|------|
89 | | 0–3 | uint32 | `*`Gaussian Count | Number of Gaussians |
90 | | 4–7 | uint32 | `*`Format ID | `1` data of Spherical harmonics (SH) degree 1 |
91 | | 8~n | bytes | `*`Data | sh0...sh8,sh0...sh8,... |
92 |
93 | - `sh0...sh8` Spherical harmonics (8-bit each)
94 |
95 | ---
96 |
97 |
98 |
99 | ✅ Open Format `2`, data of SH degree 2 (SH1 + SH2)
100 |
101 |
102 | | Byte Offset | Type | Field Name | Description |
103 | |----------|------|------|------|
104 | | 0–3 | uint32 | `*`Gaussian Count | Number of Gaussians |
105 | | 4–7 | uint32 | `*`Format ID | `2` data of Spherical harmonics (SH) degree 1 and 2 |
106 | | 8~n | bytes | `*`Data | sh0...sh23,sh0...sh23,... |
107 |
108 | - `sh0...sh23` Spherical harmonics (8-bit each)
109 |
110 | ---
111 |
112 |
113 | ✅ Open Format `3`, data of SH degree 3 (SH3 only)
114 |
115 |
116 | | Byte Offset | Type | Field Name | Description |
117 | |----------|------|------|------|
118 | | 0–3 | uint32 | `*`Gaussian Count | Number of Gaussians |
119 | | 4–7 | uint32 | `*`Format ID | `3` data of Spherical harmonics (SH) degree 3 |
120 | | 8~n | bytes | `*`Data | sh24...sh44,sh24...sh44,... |
121 |
122 | - `sh24...sh44` Spherical harmonics (8-bit each)
123 |
124 | ---
125 |
126 |
--------------------------------------------------------------------------------
/SPX_ZH.md:
--------------------------------------------------------------------------------
1 | # SPX 文件格式说明
2 | `.spx` 是一个被设计为具备灵活性、可扩展性以及支持专属保护的 `3DGS` 模型格式
3 |
4 |
5 |
6 | - [x] `灵活性` 优化的文件头,有效的压缩率,灵活的数据块
7 | - [x] `扩展性` 已开放的格式,并预留方案方便扩展
8 | - [x] `专属性` 可自定义专属格式,有效保护数据
9 |
10 |
11 |
12 | ## 文件头 (128 bytes)
13 |
14 | 文件头固定长 `128` 字节,固定的前缀用于文件格式识别,文件头包含模型的包围盒数据用来优化排序计算,专属识别号用于提示包含特定的数据块格式
15 |
16 | | 字节偏移 | 类型 | 名称 | 说明 |
17 | |----------|------|------|------|
18 | | 0~2 | ASCII | `*`固定 | 固定值 `spx` |
19 | | 3 | uint8 | `*`版本号 | 当前只有 `1` |
20 | | 4~7 | uint32 | `*`高斯点数 | |
21 | | 8~11 | float32 | `*`MinX | 包围盒的minX |
22 | | 12~15 | float32 | `*`MaxX | 包围盒的maxX |
23 | | 16~19 | float32 | `*`MinY | 包围盒的minY |
24 | | 20~23 | float32 | `*`MaxY | 包围盒的maxY |
25 | | 24~27 | float32 | `*`MinZ | 包围盒的minZ |
26 | | 28~31 | float32 | `*`MaxZ | 包围盒的maxZ |
27 | | 32~35 | float32 | MinTopY | 最小中心高度 |
28 | | 36~39 | float32 | MaxTopY | 最大中心高度 |
29 | | 40~43 | uint32 | 创建日期 | YYYYMMDD |
30 | | 44~47 | uint32 | `*`生成器识别号 | 自定义`0(官方)`以外用的唯一值来标识生成器自己 |
31 | | 48~51 | uint32 | `*`专属识别号 | 自定义`0(公开)`以外表示非公开的自定义数据块格式 |
32 | | 52 | uint8 | 球谐系数级别 | `0,1,2,3`其他数值按`0`看待 |
33 | | 53 | uint8 | 标识1 | 用以区分不同形式的模型,默认`0` |
34 | | 54 | uint8 | 标识2 | 是否倒立,默认`0` |
35 | | 55 | uint8 | 标识3 | 预留 |
36 | | 56–63 | | 预留 | |
37 | | 64~123 | ASCII | 注释 | 最长60个ASCII字符 |
38 | | 124~127 | uint32 | `*`校验码 | 用于生成器校验是否为自己生成的模型 |
39 |
40 |
41 | ---
42 |
43 |
44 | ## 数据块
45 |
46 | 数据块通常由多个组成,每个数据块有基本的固定格式,其中数据部分支持自定义格式
47 |
48 | ### 数据块结构
49 |
50 | | 字节偏移 | 类型 | 名称 | 说明 |
51 | |----------|------|------|------|
52 | | 0~3 | int32 | `*`数据块的长度 | 长度不包含本字段,负值表示数据块内容是否有gzip压缩 |
53 | | 0~n | bytes | `*`数据块内容 | |
54 |
55 |
56 | ### 数据块内容
57 |
58 | | 字节偏移 | 类型 | 名称 | 说明 |
59 | |----------|------|------|------|
60 | | 0~3 | uint32 | `*`数量 | 高斯数量 |
61 | | 4~7 | uint32 | `*`格式识别号 | 用于标识数据格式 |
62 | | 8~n | bytes | `*`数据 | |
63 |
64 |
65 | ---
66 |
67 |
68 | ## 开放的数据块内容格式
69 |
70 | 数据块内容的格式包含开放格式和自定义专属格式,预留 `0~255` 用于定义开放格式,自定义专属格式时使用其他数值
71 |
72 |
73 |
74 |
75 | ✅ 开放格式`20`,基本数据
76 |
77 |
78 | | 字节偏移 | 类型 | 名称 | 说明 |
79 | |----------|------|------|------|
80 | | 0~3 | uint32 | `*`数量 | 高斯数量 |
81 | | 4~7 | uint32 | `*`格式识别号 | `20`基本数据 |
82 | | 8~n | bytes | `*`数据 | x...y...z...sx...sy...sz...r...g...b...a...rw...rx...ry...rz... |
83 |
84 | - `x,y,z` 坐标,24位编码
85 | - `sx,sy,sz` 缩放,单字节编码
86 | - `r,g,b,a` 颜色和透明度,单字节编码
87 | - `rw,rx,ry,rz` 旋转,单字节编码
88 |
89 | ---
90 |
91 |
92 |
93 | ✅ 开放格式`1`,球谐系数1级数据(仅1级数据)
94 |
95 |
96 | | 字节偏移 | 类型 | 名称 | 说明 |
97 | |----------|------|------|------|
98 | | 0~3 | uint32 | `*`数量 | 高斯数量 |
99 | | 4~7 | uint32 | `*`格式识别号 | `1`球谐系数1级数据 |
100 | | 8~n | bytes | `*`数据 | sh0...sh8,sh0...sh8,... |
101 |
102 | - `sh0...sh8` 球谐系数,单字节编码
103 |
104 | ---
105 |
106 |
107 | ✅ 开放格式`2`,球谐系数2级数据(含1级数据)
108 |
109 |
110 | | 字节偏移 | 类型 | 名称 | 说明 |
111 | |----------|------|------|------|
112 | | 0~3 | uint32 | `*`数量 | 高斯数量 |
113 | | 4~7 | uint32 | `*`格式识别号 | `2`球谐系数1级和2级数据 |
114 | | 8~n | bytes | `*`数据 | sh0...sh23,sh0...sh23,... |
115 |
116 | - `sh0...sh23` 球谐系数,单字节编码
117 |
118 | ---
119 |
120 |
121 | ✅ 开放格式`3`,球谐系数3级数据(仅3级数据)
122 |
123 |
124 | | 字节偏移 | 类型 | 名称 | 说明 |
125 | |----------|------|------|------|
126 | | 0~3 | uint32 | `*`数量 | 高斯数量 |
127 | | 4~7 | uint32 | `*`格式识别号 | `3`球谐系数3级数据 |
128 | | 8~n | bytes | `*`数据 | sh24...sh44,sh24...sh44,... |
129 |
130 | - `sh24...sh44` 球谐系数,单字节编码
131 |
132 | ---
133 |
--------------------------------------------------------------------------------
/buildPkg.js:
--------------------------------------------------------------------------------
1 | import * as fs from 'fs';
2 | import * as path from 'path';
3 |
4 | const srcFile = './src/reall3d/sorter/SetupSorter.ts';
5 | const bakFile = './pkg/SetupSorter.ts';
6 | const assetsDir = './pkg/dist/assets';
7 | const descFile = './pkg/dist/pkg.d.ts';
8 | const faviconFile = './pkg/dist/favicon.ico';
9 |
10 | if (process.argv.length > 2) {
11 | process.argv[2] === '--before' && beforeBuildPkg();
12 | process.argv[2] === '--after' && afterBuildPkg();
13 | }
14 |
15 | function afterBuildPkg() {
16 | fixDescFile(); // 修复 .d.ts
17 | write(srcFile, read(bakFile)); // 从备份文件中恢复SetupSorter.ts
18 | fs.unlinkSync(faviconFile); // 删除多余文件
19 | fs.unlinkSync(bakFile); // 删除备份文件
20 | }
21 |
22 | function fixDescFile() {
23 | const lines = read(descFile).split('\n');
24 | for (let i = 0; i < lines.length; i++) {
25 | if (lines[i].startsWith('declare interface')) {
26 | lines[i] = 'export ' + lines[i];
27 | }
28 | }
29 | write(descFile, lines.join('\n'));
30 | }
31 |
32 | function beforeBuildPkg() {
33 | write(bakFile, read(srcFile));
34 |
35 | const SorterFile = findSorterFile(assetsDir);
36 | console.info(SorterFile);
37 | const base64String = fs.readFileSync(SorterFile).toString('base64');
38 |
39 | const devLines = read(srcFile).split('\n');
40 | const pkgLines = [];
41 | for (let i = 0; i < devLines.length; i++) {
42 | if (devLines[i].includes(`new URL('./Sorter.ts', import.meta.url)`)) {
43 | pkgLines.push(` const SorterBase64 = '';` + '\r');
44 | pkgLines.push(` const workerUrl = URL.createObjectURL(new Blob([atob(SorterBase64)], { type: 'text/javascript' }));` + '\r');
45 | pkgLines.push(` const worker = new Worker(new URL(workerUrl, import.meta.url), { type: 'module' });` + '\r');
46 | } else {
47 | pkgLines.push(devLines[i]);
48 | }
49 | }
50 | for (let i = 0; i < pkgLines.length; i++) {
51 | if (pkgLines[i].includes('const SorterBase64 =')) {
52 | if (pkgLines[i].trim() === 'const SorterBase64 =') {
53 | pkgLines[i + 1] = ` '${base64String}';` + '\r';
54 | } else {
55 | pkgLines[i] = ` const SorterBase64 = '${base64String}';` + '\r';
56 | }
57 | }
58 | }
59 | write(srcFile, pkgLines.join('\n'));
60 | }
61 |
62 | function read(file, encoding = 'utf-8') {
63 | return fs.readFileSync(file, encoding);
64 | }
65 |
66 | function write(file, text = '', encoding = 'utf-8') {
67 | fs.writeFileSync(file, text, encoding);
68 | }
69 |
70 | function findSorterFile(directoryPath) {
71 | const files = fs.readdirSync(directoryPath);
72 | const sorterFiles = files.filter(file => file.startsWith('Sorter') && file.endsWith('.js'));
73 | return path.join(directoryPath, sorterFiles[0]);
74 | }
75 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reall3dviewer",
3 | "version": "1.0.0",
4 | "type": "module",
5 | "scripts": {
6 | "dev": "vite",
7 | "build": "tsc && vite build",
8 | "build-pkg": "tsc && vite build --config vite-pkg.config.ts && node buildPkg.js --before && vite build --config vite-pkg.config.ts && node buildPkg.js --after",
9 | "preview": "vite preview"
10 | },
11 | "dependencies": {
12 | "@gotoeasy/three-tile": "^0.8.7",
13 | "@tweenjs/tween.js": "^25.0.0",
14 | "three": "^0.171.0"
15 | },
16 | "devDependencies": {
17 | "@types/node": "^22.13.5",
18 | "@types/three": "^0.171.0",
19 | "@vituum/vite-plugin-postcss": "^1.1.0",
20 | "less": "^4.2.2",
21 | "typescript": "^5.6.3",
22 | "vite": "^6.1.1",
23 | "vite-plugin-svg-icons": "^2.0.1"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/pkg/README.md:
--------------------------------------------------------------------------------
1 | # @reall3d/reall3dviewer
2 |
3 | Install
4 | ```shell
5 | npm i @reall3d/reall3dviewer
6 | ```
7 |
8 | Use Reall3dViewer
9 | ```js
10 | const viewer = new Reall3dViewer({ root: '#viewer2' });
11 | viewer.addModel(`https://reall3d.com/demo-models/yz.spx`);
12 | ```
13 |
14 | Use Reall3dMapViewer
15 | ```js
16 | const mapViewer = new Reall3dMapViewer({ root: '#viewer4' });
17 | mapViewer.addScenes('https://reall3d.com/demo-models/map/00.scenes.json');
18 | ```
19 |
20 | # Links
21 |
22 | - https://github.com/reall3d-com/Reall3dViewer
23 | - https://github.com/reall3d-com/reall3dviewer-samples-use-npm-package
24 | - https://github.com/gotoeasy/gsbox
25 |
--------------------------------------------------------------------------------
/pkg/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@reall3d/reall3dviewer",
3 | "version": "0.1.3",
4 | "type": "module",
5 | "files": [
6 | "dist"
7 | ],
8 | "main": "./dist/pkg.umd.cjs",
9 | "module": "./dist/pkg.js",
10 | "types": "./dist/pkg.d.ts",
11 | "exports": {
12 | ".": {
13 | "import": "./dist/pkg.js",
14 | "require": "./dist/pkg.umd.cjs",
15 | "types": "./dist/pkg.d.ts"
16 | },
17 | "./dist/style.css": "./dist/style.css"
18 | },
19 | "peerDependencies": {
20 | "@gotoeasy/three-tile": "^0.8.7",
21 | "@tweenjs/tween.js": "^25.0.0",
22 | "three": "^0.171.0"
23 | },
24 | "description": "reall3dviewer",
25 | "license": "MIT",
26 | "homepage": "https://github.com/reall3d-com/Reall3dViewer"
27 | }
28 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reall3d-com/Reall3dViewer/b9fade459224497690501a7dbccfba1d52c178c3/public/favicon.ico
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | // ==============================================
2 | // Copyright (c) 2025 reall3d.com, MIT license
3 | // ==============================================
4 | import './reall3d/style/style.less';
5 | import 'virtual:svg-icons-register';
6 |
7 | import { Reall3dViewer } from './reall3d/viewer/Reall3dViewer';
8 | import { Reall3dViewerOptions } from './reall3d/viewer/Reall3dViewerOptions';
9 | import { Reall3dMapViewer } from './reall3d/mapviewer/Reall3dMapViewer';
10 |
11 | const params: URLSearchParams = new URLSearchParams(location.search);
12 | let url = params.get('url');
13 | const debugMode = !!params.get('debug');
14 |
15 | const maxRenderCountOfPc = 384 * 10000;
16 | const shDegree = 3;
17 |
18 | let viewer: Reall3dViewer;
19 | let mapViewer: Reall3dMapViewer;
20 | if (url) {
21 | viewer = new Reall3dViewer({ debugMode });
22 | viewer.addModel(url);
23 | debugMode && initDevMode(true);
24 | } else {
25 | viewer = new Reall3dViewer({ debugMode: true, maxRenderCountOfPc, shDegree });
26 | viewer.addModel(`https://reall3d.com/demo-models/hornedlizard.spx`);
27 |
28 | initDevMode();
29 | }
30 |
31 | // 以下仅开发模式使用
32 | function initDevMode(infoOnly = false) {
33 | document.querySelectorAll('.prd-mode').forEach(dom => dom['style'].removeProperty('display'));
34 | let spans: NodeListOf = document.querySelectorAll('#gsviewer .operation span');
35 | let jsHeapSizeLimit = (performance['memory'] || { usedJSHeapSize: 0, totalJSHeapSize: 0, jsHeapSizeLimit: 0 }).jsHeapSizeLimit;
36 | !jsHeapSizeLimit && document.querySelectorAll('.tr-memory').forEach(dom => dom.classList.toggle('hidden'));
37 | navigator.userAgent.includes('Mobi') && document.querySelectorAll('.tr-pc-only').forEach(dom => dom.classList.toggle('hidden'));
38 | document.querySelectorAll('.dev-panel').forEach(dom => dom['style'].removeProperty('display'));
39 | !infoOnly &&
40 | Array.from(spans).forEach(span => {
41 | span.addEventListener('click', function (e: MouseEvent) {
42 | let target: HTMLSpanElement = e.target as HTMLSpanElement;
43 | fnClick(target.className);
44 | });
45 | });
46 | infoOnly && document.querySelectorAll('.operation').forEach(dom => (dom['style'].display = 'none'));
47 |
48 | const gstext: HTMLInputElement = document.querySelector('.gstext');
49 | if (gstext) {
50 | gstext.addEventListener('keyup', function (e: Event) {
51 | viewer && window['$api']?.setWaterMark(gstext.value, false);
52 | });
53 | }
54 | }
55 |
56 | function fnClick(className: string) {
57 | if (className == 'switch-debug') {
58 | let txt = document.querySelector('#gsviewer .debug').classList.toggle('hidden') ? '+' : '-';
59 | document.querySelector('#gsviewer .switch-debug').innerHTML = txt;
60 | } else if (className == 'op-show') {
61 | let txt = document.querySelector('#gsviewer .operation table').classList.toggle('plus') ? '+' : '-';
62 | document.querySelector('#gsviewer .op-show').innerHTML = txt;
63 | } else if (className == 'demo1') {
64 | viewer?.dispose();
65 | mapViewer?.dispose();
66 | viewer = viewer || new Reall3dViewer({ debugMode: true, maxRenderCountOfPc, shDegree });
67 | viewer.reset({ debugMode: true });
68 | setTimeout(() => viewer.addModel(`https://reall3d.com/demo-models/yz.spx`), 50); // Let it GC
69 | } else if (className == 'demo2') {
70 | viewer?.dispose();
71 | mapViewer?.dispose();
72 | viewer = viewer || new Reall3dViewer({ debugMode: true, maxRenderCountOfPc, shDegree });
73 | viewer.reset({ debugMode: true });
74 | setTimeout(() => viewer.addModel(`https://reall3d.com/demo-models/jtstjg.spx`), 50); // Let it GC
75 | } else if (className == 'demo3') {
76 | viewer?.dispose();
77 | mapViewer?.dispose();
78 | viewer = viewer || new Reall3dViewer({ debugMode: true, maxRenderCountOfPc, shDegree });
79 | viewer.reset({ debugMode: true });
80 | setTimeout(() => viewer.addModel(`https://reall3d.com/demo-models/djj.spx`), 50); // Let it GC
81 | } else if (className == 'demo4') {
82 | viewer?.dispose();
83 | mapViewer?.dispose();
84 | viewer = viewer || new Reall3dViewer({ debugMode: true, maxRenderCountOfPc, shDegree });
85 | viewer.reset({ debugMode: true });
86 | setTimeout(() => viewer.addModel(`https://reall3d.com/demo-models/bzg.spx`), 50); // Let it GC
87 | } else if (className == 'big-lod') {
88 | // TODO 大场景LOD,重构改进使用spx
89 | // setTimeout(() => viewer.addScene(`https://reall3d.com/demo-models/lod-demo-spx.scene.json`), 50); // Let it GC
90 | } else if (className == 'switch-rotate') {
91 | let opts: Reall3dViewerOptions = viewer?.options();
92 | viewer?.options({ autoRotate: !opts.autoRotate });
93 | } else if (className == 'switch-pointcloudMode') {
94 | viewer?.options({ pointcloudMode: !viewer?.options().pointcloudMode });
95 | } else if (className == 'switch-deiplay-mode') {
96 | viewer?.switchDeiplayMode();
97 | } else if (className == 'add-lightFactor') {
98 | viewer?.options({ lightFactor: viewer?.options().lightFactor + 0.1 });
99 | } else if (className == 'default-lightFactor') {
100 | viewer?.options({ lightFactor: 1 });
101 | } else if (className == 'sub-lightFactor') {
102 | let opts: Reall3dViewerOptions = viewer?.options();
103 | viewer?.options({ lightFactor: opts.lightFactor - 0.1 });
104 | } else if (className == 'show-watermark') {
105 | viewer?.fire(1, 1);
106 | } else if (className == 'hide-watermark') {
107 | viewer?.fire(1, 0);
108 | } else if (className == 'mark-show') {
109 | viewer?.options({ markVisible: true });
110 | } else if (className == 'mark-hide') {
111 | viewer?.options({ markVisible: false });
112 | } else if (className == 'mark-save') {
113 | viewer?.fire(6);
114 | } else if (className == 'mark-del') {
115 | viewer?.fire(7);
116 | } else if (className == 'mark-point') {
117 | viewer?.options({ markMode: true, markType: 'point' });
118 | } else if (className == 'mark-lines') {
119 | viewer?.options({ markMode: true, markType: 'lines' });
120 | } else if (className == 'mark-plans') {
121 | viewer?.options({ markMode: true, markType: 'plans' });
122 | } else if (className == 'mark-distance') {
123 | viewer?.options({ markMode: true, markType: 'distance' });
124 | } else if (className == 'mark-circle') {
125 | viewer?.options({ markMode: true, markType: 'circle' });
126 | } else if (className == 'add-pos') {
127 | viewer?.fire(2);
128 | } else if (className == 'fly') {
129 | viewer?.fire(3);
130 | } else if (className == 'clear-pos') {
131 | viewer?.fire(4);
132 | } else if (className == 'fly-save') {
133 | viewer?.fire(5);
134 | } else if (className == 'add-sh') {
135 | viewer?.fire(8, 1);
136 | } else if (className == 'default-sh') {
137 | viewer?.fire(8);
138 | } else if (className == 'sub-sh') {
139 | viewer?.fire(8, -1);
140 | } else if (className == 'map') {
141 | viewer?.dispose();
142 | mapViewer?.dispose();
143 | viewer = null;
144 | document.querySelector('.debug.dev-panel')?.classList?.add('map');
145 | document.querySelector('#map')?.classList?.remove('hidden');
146 | mapViewer = new Reall3dMapViewer({ debugMode: true });
147 | mapViewer.addScenes('https://reall3d.com/demo-models/map/00.scenes.json');
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/src/reall3d/api/SetupApi.ts:
--------------------------------------------------------------------------------
1 | // ==============================================
2 | // Copyright (c) 2025 reall3d.com, MIT license
3 | // ==============================================
4 | import { Events } from '../events/Events';
5 | import { HttpPostMetaData, HttpQueryGaussianText } from '../events/EventConstants';
6 | import { ViewerVersion } from '../utils/consts/GlobalConstants';
7 | import { MetaData } from '../modeldata/ModelData';
8 | export function setupApi(events: Events) {
9 | const on = (key: number, fn?: Function, multiFn?: boolean): Function | Function[] => events.on(key, fn, multiFn);
10 |
11 | on(HttpPostMetaData, (meta: MetaData) => {
12 | // TODO post meta data to server here
13 | const url = meta.url;
14 | const metaClone: MetaData = { ...meta };
15 | delete metaClone.url;
16 | const metaJson = JSON.stringify(metaClone, null, 2);
17 | console.info(metaJson);
18 | });
19 |
20 | on(HttpQueryGaussianText, (text: string = '') => {
21 | const url = 'https://reall3d.com/gsfont/api/getGaussianText';
22 | const formData = new FormData();
23 | formData.append('text', text.substring(0, 100)); // 限制查取最大100字
24 | formData.append('ver', ViewerVersion);
25 |
26 | return new Promise(resolve => {
27 | fetch(url, { method: 'POST', body: formData })
28 | .then(response => (response.ok ? response.json() : {}))
29 | .then((data: any) => (data.success ? resolve(JSON.parse(data.data)) : resolve([])))
30 | .catch(e => resolve([]));
31 | });
32 | });
33 | }
34 |
--------------------------------------------------------------------------------
/src/reall3d/assets/icons/delete.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/reall3d/assets/icons/edit.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/reall3d/assets/icons/point1.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/reall3d/assets/icons/point2.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/reall3d/assets/icons/point3.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/reall3d/assets/icons/position1.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/reall3d/controls/CameraControls.ts:
--------------------------------------------------------------------------------
1 | // ==============================================
2 | // Copyright (c) 2025 reall3d.com, MIT license
3 | // ==============================================
4 | import { Camera, PerspectiveCamera, Vector3 } from 'three';
5 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
6 | import { Reall3dViewerOptions } from '../viewer/Reall3dViewerOptions';
7 | import { isMobile } from '../utils/consts/GlobalConstants';
8 |
9 | /**
10 | * 旋转控制器
11 | */
12 | export class CameraControls extends OrbitControls {
13 | constructor(opts: Reall3dViewerOptions) {
14 | const camera: Camera = opts.camera;
15 | super(camera, opts.renderer.domElement);
16 |
17 | const that = this;
18 | that.dampingFactor = 0.1;
19 | that.rotateSpeed = 0.4;
20 | that.updateByOptions(opts);
21 | }
22 |
23 | public updateByOptions(opts: Reall3dViewerOptions = {}) {
24 | const that = this;
25 |
26 | opts.enableDamping !== undefined && (that.enableDamping = opts.enableDamping);
27 | opts.autoRotate !== undefined && (that.autoRotate = opts.autoRotate);
28 | opts.enableZoom !== undefined && (that.enableZoom = opts.enableZoom);
29 | opts.enableRotate !== undefined && (that.enableRotate = opts.enableRotate);
30 | opts.enablePan !== undefined && (that.enablePan = opts.enablePan);
31 | opts.minDistance !== undefined && (that.minDistance = opts.minDistance);
32 | opts.maxDistance !== undefined && (that.maxDistance = opts.maxDistance);
33 | opts.minPolarAngle !== undefined && (that.minPolarAngle = opts.minPolarAngle);
34 | opts.maxPolarAngle !== undefined && (that.maxPolarAngle = opts.maxPolarAngle);
35 |
36 | opts.fov !== undefined && ((that.object as PerspectiveCamera).fov = opts.fov);
37 | opts.near !== undefined && ((that.object as PerspectiveCamera).near = opts.near);
38 | opts.far !== undefined && ((that.object as PerspectiveCamera).far = opts.far);
39 | opts.position && that.object.position.fromArray(opts.position);
40 | opts.lookAt && that.target.fromArray(opts.lookAt);
41 | opts.lookUp && that.object.up.fromArray(opts.lookUp);
42 |
43 | // @ts-ignore
44 | isMobile && that._dollyOut?.(0.75); // 手机适当缩小
45 | that.updateRotateAxis();
46 | that.update();
47 | }
48 |
49 | /**
50 | * 更新旋转轴
51 | */
52 | public updateRotateAxis() {
53 | // @ts-ignore
54 | this._quat?.setFromUnitVectors?.(this.object.up, new Vector3(0, 1, 0));
55 | // @ts-ignore
56 | this._quatInverse = this._quat?.clone?.()?.invert?.();
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/reall3d/controls/SetupCameraControls.ts:
--------------------------------------------------------------------------------
1 | // ==============================================
2 | // Copyright (c) 2025 reall3d.com, MIT license
3 | // ==============================================
4 | import {
5 | GetCameraInfo,
6 | GetCameraLookAt,
7 | GetCameraLookUp,
8 | GetCameraPosition,
9 | GetCameraFov,
10 | GetControls,
11 | ControlsUpdate,
12 | ControlsUpdateRotateAxis,
13 | IsCameraChangedNeedUpdate,
14 | CameraSetLookAt,
15 | FocusMarkerUpdate,
16 | RunLoopByFrame,
17 | ControlPlaneUpdate,
18 | FocusMarkerAutoDisappear,
19 | } from '../events/EventConstants';
20 | import { PerspectiveCamera, Vector3 } from 'three';
21 | import { Events } from '../events/Events';
22 | import { GetCamera } from '../events/EventConstants';
23 | import { CameraControls } from './CameraControls';
24 | import { OrbitControls } from 'three/examples/jsm/Addons.js';
25 |
26 | /**
27 | * 相机参数信息
28 | */
29 | export interface CameraInfo {
30 | /**
31 | * 相机视场
32 | */
33 | fov?: number;
34 |
35 | /**
36 | * 相机近截面距离
37 | */
38 | near?: number;
39 |
40 | /**
41 | * 相机远截面距离
42 | */
43 | far?: number;
44 |
45 | /**
46 | * 相机宽高比
47 | */
48 | aspect?: number;
49 |
50 | /**
51 | * 相机位置
52 | */
53 | position: number[];
54 |
55 | /**
56 | * 相机注视点
57 | */
58 | lookAt?: number[];
59 |
60 | /**
61 | * 相机上向量
62 | */
63 | lookUp?: number[];
64 | }
65 |
66 | export function setupCameraControls(events: Events) {
67 | const on = (key: number, fn?: Function, multiFn?: boolean): Function | Function[] => events.on(key, fn, multiFn);
68 | const fire = (key: number, ...args: any): any => events.fire(key, ...args);
69 |
70 | const controls: OrbitControls = fire(GetControls);
71 | on(GetCameraFov, () => fire(GetCamera).fov);
72 | on(GetCameraPosition, (copy: boolean = false) => (copy ? controls.object.position.clone() : controls.object.position));
73 | on(GetCameraLookAt, (copy: boolean = false) => (copy ? controls.target.clone() : controls.target));
74 | on(GetCameraLookUp, (copy: boolean = false) => (copy ? fire(GetCamera).up.clone() : fire(GetCamera).up));
75 |
76 | let oEnables: any;
77 | const aryProcessAnimate: any[] = [];
78 | on(CameraSetLookAt, (target: Vector3, animate: boolean = false, rotateAnimate: boolean) => {
79 | fire(FocusMarkerUpdate, target);
80 | if (!animate) {
81 | controls.target.copy(target);
82 | const direction = new Vector3().subVectors(target, controls.object.position);
83 | direction.length() < 1 && controls.object.position.copy(target).sub(direction.setLength(1));
84 | direction.length() > 50 && controls.object.position.copy(target).sub(direction.setLength(50));
85 | fire(ControlPlaneUpdate);
86 | fire(FocusMarkerAutoDisappear);
87 | return;
88 | }
89 |
90 | while (aryProcessAnimate.length) aryProcessAnimate.pop().stop = true;
91 | let process = { alpha: 0, time: Date.now(), stop: false };
92 | aryProcessAnimate.push(process);
93 |
94 | // 适当时间内禁用拖动旋转避免操作冲突
95 | oEnables = oEnables || { enablePan: controls.enablePan, enableRotate: controls.enableRotate };
96 | controls.enablePan = false;
97 | controls.enableRotate = false;
98 |
99 | const oldTarget: Vector3 = fire(GetCameraLookAt, true);
100 | const oldPos: Vector3 = fire(GetCameraPosition, true);
101 | const dir = oldTarget.clone().sub(oldPos).normalize();
102 | const newPos = target.clone().sub(dir.multiplyScalar(target.clone().sub(oldPos).dot(dir)));
103 |
104 | fire(
105 | RunLoopByFrame,
106 | () => {
107 | process.alpha = (Date.now() - process.time) / 600;
108 | fire(GetControls).target.copy(oldTarget.clone().lerp(target, process.alpha));
109 | !rotateAnimate && fire(GetControls).object.position.copy(oldPos.clone().lerp(newPos, process.alpha));
110 | fire(ControlPlaneUpdate);
111 | if (process.alpha >= 0.9) {
112 | controls.enablePan = oEnables.enablePan;
113 | controls.enableRotate = oEnables.enableRotate;
114 | }
115 | if (process.alpha >= 1) {
116 | process.stop = true;
117 | fire(FocusMarkerAutoDisappear);
118 | }
119 | },
120 | () => !process.stop,
121 | );
122 | });
123 |
124 | on(GetCameraInfo, (): CameraInfo => {
125 | let position = fire(GetCameraPosition).toArray();
126 | let lookUp = fire(GetCameraLookUp).toArray();
127 | let lookAt = fire(GetCameraLookAt).toArray();
128 | return { position, lookUp, lookAt };
129 | });
130 |
131 | on(ControlsUpdate, () => (fire(GetControls) as CameraControls).update());
132 | on(ControlsUpdateRotateAxis, () => (fire(GetControls) as CameraControls).updateRotateAxis());
133 |
134 | // ---------------------
135 | const epsilon = 0.01;
136 | let lastCameraPosition: Vector3 = new Vector3();
137 | let lastCameraDirection: Vector3 = new Vector3();
138 | let lastCameraFov: number = 0;
139 | on(IsCameraChangedNeedUpdate, () => {
140 | const camera: PerspectiveCamera = fire(GetControls).object;
141 | const fov = camera.fov;
142 | const position = camera.position.clone();
143 | const direction = camera.getWorldDirection(new Vector3());
144 | if (
145 | Math.abs(lastCameraFov - fov) < epsilon &&
146 | Math.abs(position.x - lastCameraPosition.x) < epsilon &&
147 | Math.abs(position.y - lastCameraPosition.y) < epsilon &&
148 | Math.abs(position.z - lastCameraPosition.z) < epsilon &&
149 | Math.abs(direction.x - lastCameraDirection.x) < epsilon &&
150 | Math.abs(direction.y - lastCameraDirection.y) < epsilon &&
151 | Math.abs(direction.z - lastCameraDirection.z) < epsilon
152 | ) {
153 | return false;
154 | }
155 | lastCameraFov = fov;
156 | lastCameraPosition = position;
157 | lastCameraDirection = direction;
158 | return true;
159 | });
160 | }
161 |
--------------------------------------------------------------------------------
/src/reall3d/controls/SetupFlying.ts:
--------------------------------------------------------------------------------
1 | // ==============================================
2 | // Copyright (c) 2025 reall3d.com, MIT license
3 | // ==============================================
4 | import { Events } from '../events/Events';
5 | import {
6 | AddFlyPosition,
7 | ClearFlyPosition,
8 | FlySavePositions,
9 | GetControls,
10 | GetFlyPositionArray,
11 | GetFlyPositions,
12 | GetFlyTargetArray,
13 | GetSplatMesh,
14 | HttpPostMetaData,
15 | OnSetFlyPositions,
16 | OnSetFlyTargets,
17 | OnViewerAfterUpdate,
18 | StopAutoRotate,
19 | Flying,
20 | FlyDisable,
21 | FlyEnable,
22 | FlyOnce,
23 | } from '../events/EventConstants';
24 | import { CameraControls } from './CameraControls';
25 | import { CatmullRomCurve3, Vector3 } from 'three';
26 | import { MetaData } from '../modeldata/ModelData';
27 | export function setupFlying(events: Events) {
28 | const fire = (key: number, ...args: any): any => events.fire(key, ...args);
29 | const on = (key: number, fn?: Function, multiFn?: boolean): Function | Function[] => events.on(key, fn, multiFn);
30 |
31 | const flyPositions: Vector3[] = [];
32 | const flyTargets: Vector3[] = [];
33 | let flyEnable: boolean = false;
34 | let flyOnceDone: boolean = false;
35 |
36 | on(FlyDisable, () => (flyEnable = false));
37 | on(FlyEnable, () => (flyEnable = true));
38 | on(GetFlyPositions, () => flyPositions);
39 | on(GetFlyPositionArray, () => {
40 | const rs = [];
41 | for (let i = 0, max = flyPositions.length; i < max; i++) {
42 | rs.push(...flyPositions[i].toArray());
43 | }
44 | return rs;
45 | });
46 | on(GetFlyTargetArray, () => {
47 | const rs = [];
48 | for (let i = 0, max = flyTargets.length; i < max; i++) {
49 | rs.push(...flyTargets[i].toArray());
50 | }
51 | return rs;
52 | });
53 | on(OnSetFlyPositions, (v3s: number[]) => {
54 | for (let i = 0, max = (v3s.length / 3) | 0; i < max; i++) {
55 | flyPositions[i] = new Vector3(v3s[i * 3 + 0], v3s[i * 3 + 1], v3s[i * 3 + 2]);
56 | }
57 | });
58 | on(OnSetFlyTargets, (v3s: number[]) => {
59 | for (let i = 0, max = (v3s.length / 3) | 0; i < max; i++) {
60 | flyTargets[i] = new Vector3(v3s[i * 3 + 0], v3s[i * 3 + 1], v3s[i * 3 + 2]);
61 | }
62 | });
63 | on(AddFlyPosition, () => {
64 | const controls: CameraControls = fire(GetControls);
65 | flyPositions.push(controls.object.position.clone());
66 | flyTargets.push(controls.target.clone());
67 | });
68 | on(ClearFlyPosition, () => {
69 | flyPositions.length = 0;
70 | flyTargets.length = 0;
71 | });
72 | on(FlySavePositions, async () => {
73 | const meta: MetaData = fire(GetSplatMesh).meta || {};
74 | if (flyPositions.length) {
75 | const positions: number[] = [];
76 | const targets: number[] = [];
77 | for (let i = 0, max = flyPositions.length; i < max; i++) {
78 | positions.push(...flyPositions[i].toArray());
79 | targets.push(...flyTargets[i].toArray());
80 | }
81 | meta.flyPositions = positions;
82 | meta.flyTargets = targets;
83 | } else {
84 | delete meta.flyPositions;
85 | delete meta.flyTargets;
86 | }
87 |
88 | return await fire(HttpPostMetaData, meta);
89 | });
90 |
91 | on(FlyOnce, () => {
92 | if (flyOnceDone) return;
93 | (flyOnceDone = true) && fire(Flying);
94 | });
95 |
96 | let t = 0; // 插值因子
97 | const flyTotalTime = 120 * 1000;
98 | let flyStartTime = 0;
99 | let curvePos: CatmullRomCurve3 | null;
100 | let curveTgt: CatmullRomCurve3 | null;
101 | on(Flying, (force: boolean) => {
102 | t = 0;
103 | flyStartTime = Date.now();
104 | curvePos = null;
105 | curveTgt = null;
106 | if (!flyPositions.length) return;
107 | if (!force && !fire(GetControls).autoRotate) return; // 避免在非自动旋转模式下执行
108 |
109 | const controls: CameraControls = fire(GetControls);
110 |
111 | const points: Vector3[] = [controls.object.position.clone()];
112 | const tgts: Vector3[] = [controls.target.clone()];
113 | // const points: Vector3[] = [];
114 | // const tgts: Vector3[] = [];
115 | const all: Vector3[] = fire(GetFlyPositions) || [];
116 | for (let i = 0, max = Math.min(all.length, 100); i < max; i++) {
117 | all[i] && points.push(all[i]);
118 | flyTargets[i] && tgts.push(flyTargets[i]);
119 | }
120 | curvePos = new CatmullRomCurve3(points);
121 | curvePos.closed = true;
122 | curveTgt = new CatmullRomCurve3(tgts);
123 | curveTgt.closed = true;
124 |
125 | fire(FlyEnable);
126 | fire(StopAutoRotate, false);
127 | });
128 |
129 | on(
130 | OnViewerAfterUpdate,
131 | () => {
132 | if (Date.now() - flyStartTime > flyTotalTime) fire(FlyDisable);
133 | if (!flyEnable || !curvePos || !curveTgt) return;
134 |
135 | const controls: CameraControls = fire(GetControls);
136 |
137 | t = (Date.now() - flyStartTime) / flyTotalTime;
138 | const pt = curvePos.getPoint(t);
139 | const tgt = curveTgt.getPoint(t);
140 |
141 | controls.object.position.set(pt.x, pt.y, pt.z);
142 | controls.target.set(tgt.x, tgt.y, tgt.z);
143 | },
144 | true,
145 | );
146 | }
147 |
--------------------------------------------------------------------------------
/src/reall3d/events/EventConstants.ts:
--------------------------------------------------------------------------------
1 | // ==============================================
2 | // Copyright (c) 2025 reall3d.com, MIT license
3 | // ==============================================
4 | let i = 1;
5 |
6 | /** 帧率循环调用 */
7 | export const RunLoopByFrame = i++;
8 | /** 定时循环调用 */
9 | export const RunLoopByTime = i++;
10 | /** 提交元数据到服务器 */
11 | export const HttpPostMetaData = i++;
12 | /** 取文本高斯数据 */
13 | export const HttpQueryGaussianText = i++;
14 | /** 计算平面中心点 */
15 | export const ComputePlansCenter = i++;
16 | /** 计算多个平面的面积 */
17 | export const ComputePlansArea = i++;
18 | /** 按数据重新计算多个平面的面积 */
19 | export const ReComputePlansArea = i++;
20 | /** 计算三角面的面积 */
21 | export const ComputePoint3Area = i++;
22 | /** 取得相关对象 */
23 | export const GetWorker = i++;
24 | /** 取得相关对象 */
25 | export const GetCanvas = i++;
26 |
27 | /** 取得相关对象 */
28 | export const GetCamera = i++;
29 | /** 取得相关对象 */
30 | export const GetControls = i++;
31 | /** 取得当前相机参数信息 */
32 | export const GetCameraInfo = i++;
33 | /** 设定相机视点 */
34 | export const CameraSetLookAt = i++;
35 | /** 取相机视点 */
36 | export const GetCameraLookAt = i++;
37 | /** 取相机上向量 */
38 | export const GetCameraLookUp = i++;
39 | /** 取相机位置 */
40 | export const GetCameraPosition = i++;
41 | /** 取相机Fov */
42 | export const GetCameraFov = i++;
43 | /** 控制器更新 */
44 | export const ControlsUpdate = i++;
45 | /** 控制器更新旋转轴 */
46 | export const ControlsUpdateRotateAxis = i++;
47 |
48 | /** 取视图投影矩阵数组 */
49 | export const GetViewProjectionMatrixArray = i++;
50 | /** 取视图投影矩阵 */
51 | export const GetViewProjectionMatrix = i++;
52 | /** 排序 */
53 | export const WorkerSort = i++;
54 | /** 销毁 */
55 | export const WorkerDispose = i++;
56 | /** 销毁 */
57 | export const EventListenerDispose = i++;
58 | /** 编码 base64 */
59 | export const EncodeBase64 = i++;
60 | /** 解码 base64 */
61 | export const DecodeBase64 = i++;
62 | /** 开始自动旋转 */
63 | export const StartAutoRotate = i++;
64 | /** 停止自动旋转 */
65 | export const StopAutoRotate = i++;
66 | /** 加载模型开始 */
67 | export const LoaderModelStart = i++;
68 |
69 | /** 渲染信息 */
70 | export const Information = i++;
71 | /** 当前时点限制渲染的的高斯点数(包含了附加的动态文字水印数) */
72 | export const GetMaxRenderCount = i++;
73 | /** 渲染帧率 */
74 | export const ComputeFps = i++;
75 | /** Splat全局变量 */
76 | export const CreateSplatUniforms = i++;
77 | /** Splat几何体 */
78 | export const CreateSplatGeometry = i++;
79 | /** Splat材质 */
80 | export const CreateSplatMaterial = i++;
81 | /** Splat网格 */
82 | export const CreateSplatMesh = i++;
83 | /** 取Splat几何体 */
84 | export const GetSplatGeometry = i++;
85 | /** 取Splat材质 */
86 | export const GetSplatMaterial = i++;
87 | /** Splat更新焦距 */
88 | export const SplatUpdateFocal = i++;
89 |
90 | /** Splat更新视口 */
91 | export const SplatUpdateViewport = i++;
92 | /** Splat更新索引缓冲数据 */
93 | export const SplatUpdateSplatIndex = i++;
94 | /** Splat更新纹理 */
95 | export const SplatUpdateTexture = i++;
96 | /** Splat更新使用中索引 */
97 | export const SplatUpdateUsingIndex = i++;
98 | /** Splat更新点云模式 */
99 | export const SplatUpdatePointMode = i++;
100 | /** Splat更新场景模式 */
101 | export const SplatUpdateBigSceneMode = i++;
102 | /** Splat更新亮度系数 */
103 | export const SplatUpdateLightFactor = i++;
104 | /** Splat更新中心高点 */
105 | export const SplatUpdateTopY = i++;
106 | /** Splat更新可见半径 */
107 | export const SplatUpdateCurrentVisibleRadius = i++;
108 | /** Splat更新光圈半径 */
109 | export const SplatUpdateCurrentLightRadius = i++;
110 |
111 | /** Splat更新标记点 */
112 | export const SplatUpdateMarkPoint = i++;
113 | /** Splat更新系统时间 */
114 | export const SplatUpdatePerformanceNow = i++;
115 | /** Splat更新水印显示与否 */
116 | export const SplatUpdateShowWaterMark = i++;
117 | /** Splat更新调试效果 */
118 | export const SplatUpdateDebugEffect = i++;
119 | /** Splat更新球谐系数级别 */
120 | export const SplatUpdateShDegree = i++;
121 | /** Splat几何体销毁 */
122 | export const SplatGeometryDispose = i++;
123 | /** Splat材质销毁 */
124 | export const SplatMaterialDispose = i++;
125 | /** 默认渲染帧率计数器更新 */
126 | export const CountFpsDefault = i++;
127 | /** 默认渲染帧率 */
128 | export const GetFpsDefault = i++;
129 | /** 真实渲染帧率计数器更新 */
130 | export const CountFpsReal = i++;
131 |
132 | /** 真实渲染帧率 */
133 | export const GetFpsReal = i++;
134 | /** 销毁 */
135 | export const ViewerUtilsDispose = i++;
136 | /** 销毁 */
137 | export const CommonUtilsDispose = i++;
138 | /** 取得渲染器选项 */
139 | export const GetOptions = i++;
140 | /** 画布尺寸 */
141 | export const GetCanvasSize = i++;
142 | /** 取渲染器 */
143 | export const GetRenderer = i++;
144 | /** 取场景 */
145 | export const GetScene = i++;
146 | /** 渲染器销毁 */
147 | export const ViewerDispose = i++;
148 | /** 是否相机视角发生变化需要渲染 */
149 | export const IsCameraChangedNeedUpdate = i++;
150 | /** 是否相机视角发生变化需要重新加载数据 */
151 | export const IsCameraChangedNeedLoadData = i++;
152 |
153 | /** 是否大场景模式 */
154 | export const IsBigSceneMode = i++;
155 | /** 是否点云模式 */
156 | export const IsPointcloudMode = i++;
157 | /** 是否调试模式 */
158 | export const IsDebugMode = i++;
159 | /** 添加模型 */
160 | export const SplatTexdataManagerAddModel = i++;
161 | /** 数据是否有变化(大场景用) */
162 | export const SplatTexdataManagerDataChanged = i++;
163 | /** 销毁 */
164 | export const SplatTexdataManagerDispose = i++;
165 | /** 销毁 */
166 | export const SplatMeshDispose = i++;
167 | /** 切换显示模式(通常仅小场景使用) */
168 | export const SplatMeshSwitchDisplayMode = i++;
169 | /** 小场景渐进加载(圆圈扩大) */
170 | export const SplatMeshCycleZoom = i++;
171 | /** 转字符串 */
172 | export const Vector3ToString = i++;
173 |
174 | /** 模型文件下载开始 */
175 | export const OnFetchStart = i++;
176 | /** 模型文件下载中 */
177 | export const OnFetching = i++;
178 | /** 模型文件下载结束 */
179 | export const OnFetchStop = i++;
180 | /** 是否加载中(小场景适用) */
181 | export const IsFetching = i++;
182 | /** 数据上传就绪的渲染数(小场景适用) */
183 | export const OnTextureReadySplatCount = i++;
184 | /** 数据是否已下载结束并准备就绪(小场景适用) */
185 | export const IsSmallSceneRenderDataReady = i++;
186 | /** 是否可以更新纹理 */
187 | export const CanUpdateTexture = i++;
188 | /** 检查执行键盘按键动作处理 */
189 | export const KeyActionCheckAndExecute = i++;
190 | /** 视线轴旋转 */
191 | export const RotateAt = i++;
192 | /** 视线轴左旋 */
193 | export const RotateLeft = i++;
194 |
195 | /** 视线轴右旋 */
196 | export const RotateRight = i++;
197 | /** 取活动点数据 */
198 | export const GetSplatActivePoints = i++;
199 | /** 射线拾取点 */
200 | export const RaycasterRayIntersectPoints = i++;
201 | /** 射线与点的距离 */
202 | export const RaycasterRayDistanceToPoint = i++;
203 | /** 调整视点为拾取点 */
204 | export const SelectPointAndLookAt = i++;
205 | /** 标注选点 */
206 | export const SelectMarkPoint = i++;
207 | /** 清除标注选点 */
208 | export const ClearMarkPoint = i++;
209 | /** 取焦点标记材质 */
210 | export const GetFocusMarkerMaterial = i++;
211 | /** 刷新焦点标记网格 */
212 | export const FocusMarkerUpdate = i++;
213 | /** 取相机方向 */
214 | export const GetCameraDirection = i++;
215 |
216 | /** 焦点标记材质设定透明度 */
217 | export const FocusMarkerSetOpacity = i++;
218 | /** 焦点标记更新缩放比例 */
219 | export const FocusMarkerUpdateScale = i++;
220 | /** 焦点标记自动消失 */
221 | export const FocusMarkerAutoDisappear = i++;
222 | /** 控制平面 */
223 | export const GetControlPlane = i++;
224 | /** 控制平面显示控制 */
225 | export const ControlPlaneSwitchVisible = i++;
226 | /** 控制平面刷新 */
227 | export const ControlPlaneUpdate = i++;
228 | /** 控制平面是否可见 */
229 | export const IsControlPlaneVisible = i++;
230 | /** 渲染前处理 */
231 | export const OnViewerBeforeUpdate = i++;
232 | /** 渲染处理 */
233 | export const OnViewerUpdate = i++;
234 | /** 渲染后处理 */
235 | export const OnViewerAfterUpdate = i++;
236 |
237 | /** 设定水印文字 */
238 | export const OnSetWaterMark = i++;
239 | /** 取当前缓存的水印文字 */
240 | export const GetCachedWaterMark = i++;
241 | /** 通知渲染器需要刷新 */
242 | export const NotifyViewerNeedUpdate = i++;
243 | /** 通知渲染器需要刷新 */
244 | export const ViewerNeedUpdate = i++;
245 | /** 更新渲染器选项的点云模式 */
246 | export const ViewerSetPointcloudMode = i++;
247 | /** 渲染器检查是否需要刷新 */
248 | export const ViewerCheckNeedUpdate = i++;
249 | /** 渲染器设定Splat点云模式 */
250 | export const SplatSetPointcloudMode = i++;
251 | /** 渲染器切换Splat显示模式 */
252 | export const SplatSwitchDisplayMode = i++;
253 | /** 取标注包裹元素 */
254 | export const GetMarkWarpElement = i++;
255 | /** 取CSS3DRenderer */
256 | export const GetCSS3DRenderer = i++;
257 |
258 | /** 销毁 */
259 | export const CSS3DRendererDispose = i++;
260 | /** 添加标注弱引用缓存 */
261 | export const AddMarkToWeakRef = i++;
262 | /** 从弱引用缓存取标注对象 */
263 | export const GetMarkFromWeakRef = i++;
264 | /** 删除标注弱引用缓存 */
265 | export const DeleteMarkWeakRef = i++;
266 | /** 按数据更新指定名称的标注 */
267 | export const UpdateMarkByName = i++;
268 | /** 按米比例尺更新全部标注 */
269 | export const UpdateAllMarkByMeterScale = i++;
270 | /** 按名称取标注数据 */
271 | export const GetMarkDataByName = i++;
272 | /** 标注点 */
273 | export const MarkPoint = i++;
274 | /** 标注线 */
275 | export const MarkLine = i++;
276 | /** 标注面 */
277 | export const MarkPlan = i++;
278 |
279 | /** 标注距离 */
280 | export const MarkDistance = i++;
281 | /** 标注面积 */
282 | export const MarkArea = i++;
283 | /** 标注结束 */
284 | export const MarkFinish = i++;
285 | /** 标注更新可见状态 */
286 | export const MarkUpdateVisible = i++;
287 | /** 标注数据保存 */
288 | export const MetaMarkSaveData = i++;
289 | /** 保存小场景相机信息 */
290 | export const MetaSaveSmallSceneCameraInfo = i++;
291 | /** 标注数据删除 */
292 | export const MetaMarkRemoveData = i++;
293 | /** 保存水印 */
294 | export const MetaSaveWatermark = i++;
295 | /** 加载小场景元数据(相机初始化,标注待激活显示) */
296 | export const LoadSmallSceneMetaData = i++;
297 | /** 遍历销毁并清空Object3D的子对象 */
298 | export const TraverseDisposeAndClear = i++;
299 |
300 | /** 取消当前正在进行的标注 */
301 | export const CancelCurrentMark = i++;
302 | /** 取高斯文本 */
303 | export const GetGaussianText = i++;
304 | /** 设定高斯文本 */
305 | export const SetGaussianText = i++;
306 | /** 取相机飞行轨迹 */
307 | export const GetFlyPositions = i++;
308 | /** 取相机飞行轨迹(数组形式,用于存盘) */
309 | export const GetFlyPositionArray = i++;
310 | /** 取相机飞行视点轨迹(数组形式,用于存盘) */
311 | export const GetFlyTargetArray = i++;
312 | /** 添加相机飞行轨迹点 */
313 | export const AddFlyPosition = i++;
314 | /** 保存相机飞行轨迹点 */
315 | export const FlySavePositions = i++;
316 | /** 清空相机飞行轨迹点 */
317 | export const ClearFlyPosition = i++;
318 | /** 设定相机飞行轨迹 */
319 | export const OnSetFlyPositions = i++;
320 |
321 | /** 设定相机飞行视点轨迹 */
322 | export const OnSetFlyTargets = i++;
323 | /** 相机飞行控制 */
324 | export const Flying = i++;
325 | /** 相机飞行控制(仅一次) */
326 | export const FlyOnce = i++;
327 | /** 允许相机飞行控制 */
328 | export const FlyEnable = i++;
329 | /** 禁止相机飞行控制 */
330 | export const FlyDisable = i++;
331 | /** 取SplatMesh实例 */
332 | export const GetSplatMesh = i++;
333 | /** 打印信息(开发调试用) */
334 | export const PrintInfo = i++;
335 | /** 上传纹理 */
336 | export const UploadSplatTexture = i++;
337 | /** 上传纹理完成 */
338 | export const UploadSplatTextureDone = i++;
339 | /** 球谐系数纹理高度 */
340 | export const GetShTexheight = i++;
341 |
342 | /** Splat更新球谐系数纹理(1,2级) */
343 | export const SplatUpdateSh12Texture = i++;
344 | /** Splat更新球谐系数纹理(3级) */
345 | export const SplatUpdateSh3Texture = i++;
346 | /** 模型数据的球谐系数级别 */
347 | export const GetModelShDegree = i++;
348 | /** 当前以多少球谐系数级别在显示 */
349 | export const GetCurrentDisplayShDegree = i++;
350 | /** 取模型包围盒中心点 */
351 | export const GetAabbCenter = i++;
352 | /** 聚焦包围盒中心点 */
353 | export const FocusAabbCenter = i++;
354 |
355 | /** 创建地图渲染器 */
356 | export const MapCreateRenderer = i++;
357 | /** 创建地图场景 */
358 | export const MapCreateScene = i++;
359 | /** 创建地图相机 */
360 | export const MapCreateCamera = i++;
361 | /** 创建地图控制器 */
362 | export const MapCreateControls = i++;
363 | /** 创建光源 */
364 | export const MapCreateDirLight = i++;
365 | /** 取一个活动的splatMesh实例(仅用于地图单个高斯模型调整) */
366 | export const MapGetSplatMesh = i++;
367 | /** 按X轴旋转 */
368 | export const MapSplatMeshRotateX = i++;
369 | /** 按Y轴旋转 */
370 | export const MapSplatMeshRotateY = i++;
371 | /** 按Z轴旋转 */
372 | export const MapSplatMeshRotateZ = i++;
373 | /** 按X轴平移 */
374 | export const MapSplatMeshMoveX = i++;
375 | /** 按Y轴平移 */
376 | export const MapSplatMeshMoveY = i++;
377 | /** 按Z轴平移 */
378 | export const MapSplatMeshMoveZ = i++;
379 | /** 设定位置 */
380 | export const MapSplatMeshSetPosition = i++;
381 | /** 缩放 */
382 | export const MapSplatMeshScale = i++;
383 | /** 切换显示隐藏 */
384 | export const MapSplatMeshShowHide = i++;
385 | /** 保存模型矩阵 */
386 | export const MapSplatMeshSaveModelMatrix = i++;
387 | /** 对多个SplatMesh实例的渲染顺序进行排序 */
388 | export const MapSortSplatMeshRenderOrder = i++;
389 | /** 遍历并清空销毁场景中的所有对象 */
390 | export const MapSceneTraverseDispose = i++;
391 | /** 飞向目标 */
392 | export const MapFlyToTarget = i++;
393 |
394 | /** 相机飞行控制 */
395 | export const TweenFly = i++;
396 | /** 相机飞行控制(仅一次) */
397 | export const TweenFlyOnce = i++;
398 | /** 允许相机飞行控制 */
399 | export const TweenFlyEnable = i++;
400 | /** 禁止相机飞行控制 */
401 | export const TweenFlyDisable = i++;
402 |
--------------------------------------------------------------------------------
/src/reall3d/events/Events.ts:
--------------------------------------------------------------------------------
1 | // ==============================================
2 | // Copyright (c) 2025 reall3d.com, MIT license
3 | // ==============================================
4 | export class Events {
5 | private map: Map;
6 |
7 | constructor() {
8 | this.map = new Map();
9 | }
10 |
11 | public on(key: number, fn: Function = null, multiFn: boolean = false): Function | Function[] {
12 | // key不对时,不处理
13 | if (!key) {
14 | console.error('Invalid event key', key);
15 | return null;
16 | }
17 |
18 | // 无fn时,返回值
19 | if (!fn) return this.map.get(key);
20 |
21 | // 设定为单方法或方法数组(类别冲突时,不处理)
22 | if (multiFn) {
23 | // 多处理方法
24 | let ary: Function | Function[] = this.map.get(key);
25 | if (!ary) {
26 | ary = [];
27 | this.map.set(key, ary);
28 | ary.push(fn);
29 | } else if (typeof ary == 'function') {
30 | console.error('Invalid event type', 'multiFn=true', key);
31 | } else {
32 | ary.push(fn);
33 | }
34 | } else {
35 | // 单处理方法
36 | let ary: Function | Function[] = this.map.get(key);
37 | if (!ary) {
38 | this.map.set(key, fn);
39 | } else if (typeof ary == 'function') {
40 | console.warn('Replace event', key);
41 | } else {
42 | console.error('Invalid event type', 'multiFn=false', key);
43 | }
44 | }
45 |
46 | return this.map.get(key);
47 | }
48 |
49 | public fire(key: number, ...args: any): any {
50 | const fn = this.map.get(key);
51 | if (!fn) {
52 | // this.map.size && console.warn('Undefined event:', key, '(', ...args, ')');
53 | this.map.size && console.log('Undefined event:', key, '(', ...args, ')');
54 | return;
55 | }
56 | if (typeof fn == 'function') {
57 | return fn(...args);
58 | }
59 |
60 | let rs: any[] = [];
61 | fn.forEach(f => rs.push(f(...args)));
62 | return rs;
63 | }
64 | public tryFire(key: number, ...args: any): any {
65 | return this.map.get(key) ? this.fire(key, ...args) : undefined;
66 | }
67 |
68 | public off(key: number): void {
69 | this.map.delete(key);
70 | }
71 |
72 | public clear(): void {
73 | this.map.clear();
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/reall3d/internal/Index.ts:
--------------------------------------------------------------------------------
1 | // ==============================================
2 | // Copyright (c) 2025 reall3d.com, MIT license
3 | // ==============================================
4 | export * from '../meshs/splatmesh/SplatMeshWebgl';
5 |
6 | export * from './WebglVars';
7 |
--------------------------------------------------------------------------------
/src/reall3d/internal/WebglVars.ts:
--------------------------------------------------------------------------------
1 | // ==============================================
2 | // Copyright (c) 2025 reall3d.com, MIT license
3 | // ==============================================
4 | export const VarCurrentVisibleRadius = 'currentVisibleRadius';
5 | export const VarCurrentLightRadius = 'currentLightRadius';
6 | export const VarSplatShTexture12 = 'splatShTexture12';
7 | export const VarSplatShTexture3 = 'splatShTexture3';
8 | export const VarPerformanceNow = 'performanceNow';
9 | export const VarWaterMarkColor = 'waterMarkColor';
10 | export const VarShowWaterMark = 'showWaterMark';
11 | export const VarSplatTexture0 = 'splatTexture0';
12 | export const VarSplatTexture1 = 'splatTexture1';
13 | export const VarBigSceneMode = 'bigSceneMode';
14 | export const VarLightFactor = 'lightFactor';
15 | export const VarDebugEffect = 'debugEffect';
16 | export const VarUsingIndex = 'usingIndex';
17 | export const VarSplatIndex = 'splatIndex';
18 | export const VarVPosition = 'vPosition';
19 | export const VarPointMode = 'pointMode';
20 | export const VarMarkPoint = 'markPoint';
21 | export const VarShDegree = 'shDegree';
22 | export const VarViewport = 'viewport';
23 | export const VarCenState = 'cenState';
24 | export const VarOpacity = 'opacity';
25 | export const VarVColor = 'vColor';
26 | export const VarFocal = 'focal';
27 | export const VarTopY = 'topY';
28 |
--------------------------------------------------------------------------------
/src/reall3d/mapviewer/Reall3dMapViewer.ts:
--------------------------------------------------------------------------------
1 | // ==============================================
2 | // Copyright (c) 2025 reall3d.com, MIT license
3 | // ==============================================
4 | import { AmbientLight, Clock, DirectionalLight, EventDispatcher, PerspectiveCamera, Scene, Vector3, WebGLRenderer } from 'three';
5 | import { MapControls } from 'three/examples/jsm/controls/MapControls.js';
6 | import { Events } from '../events/Events';
7 | import { Reall3dMapViewerOptions } from './Reall3dMapViewerOptions';
8 | import {
9 | CountFpsDefault,
10 | CountFpsReal,
11 | GetCameraFov,
12 | GetCameraLookAt,
13 | GetCameraLookUp,
14 | GetCameraPosition,
15 | GetFpsDefault,
16 | GetFpsReal,
17 | GetOptions,
18 | ViewerDispose,
19 | Information,
20 | IsDebugMode,
21 | KeyActionCheckAndExecute,
22 | MapCreateControls,
23 | MapCreateDirLight,
24 | MapCreateRenderer,
25 | MapCreateScene,
26 | OnViewerAfterUpdate,
27 | OnViewerBeforeUpdate,
28 | OnViewerUpdate,
29 | Vector3ToString,
30 | MapSortSplatMeshRenderOrder,
31 | MapSceneTraverseDispose,
32 | CSS3DRendererDispose,
33 | GetCSS3DRenderer,
34 | GetCamera,
35 | } from '../events/EventConstants';
36 | import { initMapViewerOptions, initTileMap, setupMapUtils } from './utils/MapUtils';
37 | import { setupCommonUtils } from '../utils/CommonUtils';
38 | import { setupMapEventListener } from './events/MapEventListener';
39 | import { setupApi } from '../api/SetupApi';
40 | import { setupRaycaster } from '../raycaster/SetupRaycaster';
41 | import { setupMark } from '../meshs/mark/SetupMark';
42 | import { CSS3DRenderer } from 'three/examples/jsm/Addons.js';
43 | import { WarpSplatMesh } from './warpsplatmesh/WarpSplatMesh';
44 | import { ViewerVersion } from '../utils/consts/GlobalConstants';
45 | import * as tt from '@gotoeasy/three-tile';
46 |
47 | /**
48 | * 地图渲染器
49 | */
50 | export class Reall3dMapViewer extends EventDispatcher {
51 | public scene: Scene;
52 | public renderer: WebGLRenderer;
53 | public camera: PerspectiveCamera;
54 | public controls: MapControls;
55 | public ambLight: AmbientLight;
56 | public dirLight: DirectionalLight;
57 | public container: HTMLElement;
58 | public tileMap: tt.TileMap;
59 | public events: Events;
60 |
61 | private clock: Clock = new Clock();
62 | private updateTime: number = 0;
63 | private disposed: boolean = false;
64 |
65 | constructor(options: Reall3dMapViewerOptions = {}) {
66 | console.info('Reall3dMapViewer', ViewerVersion);
67 | super();
68 |
69 | const that = this;
70 | const events = new Events();
71 | that.events = events;
72 | const on = (key: number, fn?: Function, multiFn?: boolean): Function | Function[] => events.on(key, fn, multiFn);
73 | const fire = (key: number, ...args: any): any => events.fire(key, ...args);
74 |
75 | that.tileMap = initTileMap();
76 | const opts: Reall3dMapViewerOptions = initMapViewerOptions(options);
77 | on(GetOptions, () => opts);
78 |
79 | setupCommonUtils(events);
80 | setupApi(events);
81 | setupMapUtils(events);
82 | setupRaycaster(events);
83 |
84 | that.camera = new PerspectiveCamera(60, 1, 0.01, 100);
85 | on(GetCamera, () => that.camera);
86 |
87 | that.container = opts.root as HTMLElement;
88 | that.renderer = fire(MapCreateRenderer);
89 | that.scene = fire(MapCreateScene);
90 | that.controls = fire(MapCreateControls);
91 | that.ambLight = new AmbientLight(0xffffff, 1);
92 | that.scene.add(that.ambLight);
93 | that.dirLight = fire(MapCreateDirLight);
94 | that.scene.add(that.dirLight);
95 | that.scene.add(that.tileMap);
96 | that.container.appendChild(that.renderer.domElement);
97 |
98 | setupMark(events);
99 | setupMapEventListener(events);
100 |
101 | window.addEventListener('resize', that.resize.bind(that));
102 | that.resize();
103 | that.renderer.setAnimationLoop(that.animate.bind(that));
104 |
105 | on(ViewerDispose, () => that.dispose());
106 |
107 | on(
108 | OnViewerBeforeUpdate,
109 | () => {
110 | fire(CountFpsReal);
111 | that.controls.update();
112 | fire(MapSortSplatMeshRenderOrder);
113 | fire(KeyActionCheckAndExecute);
114 | },
115 | true,
116 | );
117 |
118 | on(
119 | OnViewerUpdate,
120 | () => {
121 | that.tileMap.update(that.camera);
122 | try {
123 | that.renderer.render(that.scene, that.camera);
124 | } catch (e) {
125 | console.warn(e.message);
126 | }
127 | },
128 | true,
129 | );
130 |
131 | on(
132 | OnViewerAfterUpdate,
133 | () => {
134 | that.dispatchEvent({ type: 'update', delta: that.clock.getDelta() });
135 | that.updateTime = Date.now();
136 |
137 | fire(IsDebugMode) &&
138 | fire(Information, {
139 | fps: fire(GetFpsDefault),
140 | realFps: fire(GetFpsReal),
141 | fov: fire(GetCameraFov),
142 | position: fire(Vector3ToString, fire(GetCameraPosition)),
143 | lookAt: fire(Vector3ToString, fire(GetCameraLookAt)),
144 | lookUp: fire(Vector3ToString, fire(GetCameraLookUp)),
145 | });
146 | },
147 | true,
148 | );
149 | }
150 |
151 | /**
152 | * 打开地图场景
153 | * @param 场景索引文件地址
154 | */
155 | public addScenes(urlScenesJson: string) {
156 | const that = this;
157 | fetch(urlScenesJson, { mode: 'cors', credentials: 'omit', cache: 'reload' })
158 | .then(response => (!response.ok ? {} : response.json()))
159 | .then((data: ScenesJsonData) => {
160 | const position = new Vector3().fromArray(data.position || [17000, 30000, -35000]);
161 | const lookAt = new Vector3().fromArray(data.lookAt || [17000, 0, -35000]);
162 | that.controls.object.position.copy(position);
163 | that.controls.target.copy(lookAt);
164 | that.dirLight.target.position.copy(lookAt);
165 |
166 | const set = new Set();
167 | for (let url of data.scenes) {
168 | if (!set.has(url)) {
169 | new WarpSplatMesh(url, that);
170 | set.add(url);
171 | }
172 | }
173 | })
174 | .catch(e => {
175 | console.error(e.message);
176 | });
177 | }
178 |
179 | private resize() {
180 | const that = this;
181 | if (that.disposed) return;
182 | const { width, height, top, left } = that.container.getBoundingClientRect();
183 | that.renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
184 | that.renderer.setSize(width, height);
185 | that.camera.aspect = width / height;
186 | that.camera.updateProjectionMatrix();
187 | const cSS3DRenderer: CSS3DRenderer = that.events.fire(GetCSS3DRenderer);
188 | cSS3DRenderer.setSize(width, height);
189 | cSS3DRenderer.domElement.style.position = 'absolute';
190 | cSS3DRenderer.domElement.style.left = `${left}px`;
191 | cSS3DRenderer.domElement.style.top = `${top}px`;
192 | }
193 |
194 | private animate() {
195 | const that = this;
196 | const fire = (key: number, ...args: any): any => that.events.fire(key, ...args);
197 | fire(CountFpsDefault);
198 | if (Date.now() - that.updateTime > 30) {
199 | fire(OnViewerBeforeUpdate);
200 | fire(OnViewerUpdate);
201 | fire(OnViewerAfterUpdate);
202 | }
203 | }
204 |
205 | /**
206 | * 销毁
207 | */
208 | public dispose() {
209 | const that = this;
210 | if (that.disposed) return;
211 | that.disposed = true;
212 |
213 | const canvas = that.renderer.domElement;
214 |
215 | that.events.fire(CSS3DRendererDispose);
216 | that.events.fire(MapSceneTraverseDispose);
217 | that.renderer.clear();
218 | that.renderer.dispose();
219 | that.events.clear();
220 |
221 | that.scene = null;
222 | that.renderer = null;
223 | that.camera = null;
224 | that.controls = null;
225 | that.ambLight = null;
226 | that.dirLight = null;
227 | that.container.removeChild(canvas);
228 | that.container.classList.add('hidden');
229 | that.container = null;
230 | that.clock = null;
231 | that.events = null;
232 | that.tileMap = null;
233 |
234 | document.querySelector('#gsviewer .debug.dev-panel')?.classList?.remove('map');
235 | }
236 | }
237 |
238 | /**
239 | * 地图入口索引文件
240 | */
241 | interface ScenesJsonData {
242 | /**
243 | * 名称
244 | */
245 | name?: string;
246 |
247 | /**
248 | * 版本
249 | */
250 | version?: string;
251 |
252 | /**
253 | * 初始相机位置
254 | */
255 | position?: number[];
256 |
257 | /**
258 | * 初始相机视点
259 | */
260 | lookAt?: number[];
261 |
262 | /**
263 | * 场景url列表
264 | */
265 | scenes?: string[];
266 | }
267 |
--------------------------------------------------------------------------------
/src/reall3d/mapviewer/Reall3dMapViewerOptions.ts:
--------------------------------------------------------------------------------
1 | // ==============================================
2 | // Copyright (c) 2025 reall3d.com, MIT license
3 | // ==============================================
4 | /**
5 | * 地图配置项
6 | */
7 | export declare interface Reall3dMapViewerOptions {
8 | /**
9 | * 容器元素或其选择器,默认选择器为'#map',找不到时将自动创建
10 | */
11 | root?: HTMLElement | string;
12 |
13 | /**
14 | * 是否允许键盘操作,默认false
15 | */
16 | enableKeyboard?: boolean;
17 |
18 | /**
19 | * 拖动范围最小值,默认[-20000, 0.1, -60000]
20 | */
21 | minPan?: number[];
22 |
23 | /**
24 | * 拖动范围最大值,默认[50000, 10000, 0]
25 | */
26 | maxPan?: number[];
27 |
28 | /**
29 | * 背景色(默认 '#dbf0ff')
30 | */
31 | background?: string;
32 |
33 | /**
34 | * 是否调试模式,生产环境默认false
35 | */
36 | debugMode?: boolean;
37 | }
38 |
--------------------------------------------------------------------------------
/src/reall3d/mapviewer/tween/SetupTween.ts:
--------------------------------------------------------------------------------
1 | // ==============================================
2 | // Copyright (c) 2025 reall3d.com, MIT license
3 | // ==============================================
4 | import { Events } from '../../events/Events';
5 | import {
6 | AddFlyPosition,
7 | ClearFlyPosition,
8 | GetControls,
9 | GetFlyPositionArray,
10 | GetFlyPositions,
11 | GetFlyTargetArray,
12 | OnSetFlyPositions,
13 | OnSetFlyTargets,
14 | OnViewerAfterUpdate,
15 | StopAutoRotate,
16 | TweenFly,
17 | TweenFlyDisable,
18 | TweenFlyEnable,
19 | TweenFlyOnce,
20 | } from '../../events/EventConstants';
21 | import { Easing, Tween } from '@tweenjs/tween.js';
22 | import { CameraControls } from '../../controls/CameraControls';
23 | import { Vector3 } from 'three';
24 | export function setupTween(events: Events) {
25 | const fire = (key: number, ...args: any): any => events.fire(key, ...args);
26 | const on = (key: number, fn?: Function, multiFn?: boolean): Function | Function[] => events.on(key, fn, multiFn);
27 |
28 | const flyPositions: Vector3[] = [];
29 | const flyTargets: Vector3[] = [];
30 | let tween: Tween;
31 | let flyEnable: boolean = false;
32 | let flyOnceDone: boolean = false;
33 |
34 | on(TweenFlyDisable, () => (flyEnable = false));
35 | on(TweenFlyEnable, () => (flyEnable = true));
36 | on(GetFlyPositions, () => flyPositions);
37 | on(GetFlyPositionArray, () => {
38 | const rs = [];
39 | for (let i = 0, max = flyPositions.length; i < max; i++) {
40 | rs.push(...flyPositions[i].toArray());
41 | }
42 | return rs;
43 | });
44 | on(GetFlyTargetArray, () => {
45 | const rs = [];
46 | for (let i = 0, max = flyTargets.length; i < max; i++) {
47 | rs.push(...flyTargets[i].toArray());
48 | }
49 | return rs;
50 | });
51 | on(OnSetFlyPositions, (v3s: number[]) => {
52 | for (let i = 0, max = (v3s.length / 3) | 0; i < max; i++) {
53 | flyPositions[i] = new Vector3(v3s[i * 3 + 0], v3s[i * 3 + 1], v3s[i * 3 + 2]);
54 | }
55 | });
56 | on(OnSetFlyTargets, (v3s: number[]) => {
57 | for (let i = 0, max = (v3s.length / 3) | 0; i < max; i++) {
58 | flyTargets[i] = new Vector3(v3s[i * 3 + 0], v3s[i * 3 + 1], v3s[i * 3 + 2]);
59 | }
60 | });
61 | on(AddFlyPosition, () => {
62 | const controls: CameraControls = fire(GetControls);
63 | flyPositions.push(controls.object.position.clone());
64 | flyTargets.push(controls.target.clone());
65 | });
66 | on(ClearFlyPosition, () => {
67 | flyPositions.length = 0;
68 | flyTargets.length = 0;
69 | });
70 |
71 | on(TweenFlyOnce, (idx: number = 0) => {
72 | if (flyOnceDone) return;
73 | (flyOnceDone = true) && fire(TweenFly);
74 | });
75 |
76 | on(TweenFly, (idx: number = 0, force: boolean = true) => {
77 | force && fire(TweenFlyEnable);
78 | if (idx < 0 || idx > 100 || !flyEnable) return; // 最多支持100个位置
79 |
80 | const toPos: Vector3 = (fire(GetFlyPositions) || [])[idx];
81 | if (!toPos) {
82 | fire(TweenFly, idx + 1, false);
83 | return;
84 | }
85 |
86 | fire(StopAutoRotate, false);
87 |
88 | const controls: CameraControls = fire(GetControls);
89 | const toTgt: Vector3 = flyTargets[idx] || controls.target.clone();
90 | const pos: Vector3 = controls.object.position.clone();
91 | const tgt: Vector3 = controls.target.clone();
92 | const pt = { ...pos, tx: tgt.x, ty: tgt.y, tz: tgt.z };
93 | tween = new Tween(pt).to({ x: toPos.x, y: toPos.y, z: toPos.z, tx: toTgt.x, ty: toTgt.y, tz: toTgt.z }, 3000);
94 | tween
95 | .delay(200)
96 | .easing(Easing.Sinusoidal.InOut)
97 | .start()
98 | .onUpdate(() => {
99 | if (flyEnable) {
100 | controls.object.position.set(pt.x, pt.y, pt.z);
101 | controls.target.set(pt.tx, pt.ty, pt.tz);
102 | }
103 | })
104 | .onComplete(() => {
105 | tween = null;
106 | fire(TweenFly, idx + 1, false);
107 | })
108 | .onStop(() => {
109 | tween = null;
110 | });
111 | });
112 |
113 | on(
114 | OnViewerAfterUpdate,
115 | () => {
116 | flyEnable && tween?.update();
117 | },
118 | true,
119 | );
120 | }
121 |
--------------------------------------------------------------------------------
/src/reall3d/mapviewer/warpsplatmesh/WarpSplatMesh.ts:
--------------------------------------------------------------------------------
1 | // ==============================================
2 | // Copyright (c) 2025 reall3d.com, MIT license
3 | // ==============================================
4 | import { Matrix4, Mesh, Vector3 } from 'three';
5 | import { MapControls } from 'three/examples/jsm/controls/MapControls.js';
6 | import { GetOptions, SetGaussianText } from '../../events/EventConstants';
7 | import { CSS3DSprite } from 'three/examples/jsm/Addons.js';
8 | import { Easing, Tween } from '@tweenjs/tween.js';
9 | import { SplatMesh } from '../../meshs/splatmesh/SplatMesh';
10 | import { SplatMeshOptions } from '../../meshs/splatmesh/SplatMeshOptions';
11 | import { MetaData } from '../../modeldata/ModelData';
12 | import { Reall3dMapViewer } from '../Reall3dMapViewer';
13 | import { Reall3dMapViewerOptions } from '../Reall3dMapViewerOptions';
14 |
15 | const isMobile = navigator.userAgent.includes('Mobi');
16 |
17 | export class WarpSplatMesh extends Mesh {
18 | public readonly isWarpSplatMesh: boolean = true;
19 | public meta: MetaData;
20 | public lastActiveTime: number = Date.now();
21 | public splatMesh: SplatMesh;
22 | public active: boolean = false;
23 | private opts: SplatMeshOptions;
24 | private css3dTag: CSS3DSprite;
25 | private mapViewer: Reall3dMapViewer;
26 | private disposed: boolean = false;
27 |
28 | constructor(sceneUrl: string, mapViewer: Reall3dMapViewer) {
29 | super();
30 | this.mapViewer = mapViewer;
31 | this.addScene(sceneUrl);
32 | this.frustumCulled = false;
33 | }
34 |
35 | private async addScene(sceneUrl: string) {
36 | const that = this;
37 | const { renderer, scene, controls, tileMap } = that.mapViewer;
38 | fetch(sceneUrl, { mode: 'cors', credentials: 'omit', cache: 'reload' })
39 | .then(response => (!response.ok ? {} : response.json()))
40 | .then((data: MetaData) => {
41 | const matrix = new Matrix4();
42 | if (data.transform) {
43 | matrix.fromArray(data.transform);
44 | } else if (data.WGS84) {
45 | const pos = tileMap.geo2world(new Vector3().fromArray(data.WGS84));
46 | matrix.makeTranslation(pos.x, pos.y, pos.z);
47 | }
48 | data.autoCut && (data.autoCut = Math.min(Math.max(data.autoCut, 1), 50));
49 | const bigSceneMode = data.autoCut && data.autoCut > 1;
50 | const pointcloudMode = false;
51 | const depthTest = false;
52 | const showWatermark = data.showWatermark !== false;
53 | const opts: SplatMeshOptions = { renderer, scene, controls, pointcloudMode, bigSceneMode, matrix, showWatermark, depthTest };
54 | opts.maxRenderCountOfMobile ??= opts.bigSceneMode ? 128 * 10240 : 400 * 10000;
55 | opts.maxRenderCountOfPc ??= opts.bigSceneMode ? 320 * 10000 : 400 * 10000;
56 | opts.debugMode = (this.mapViewer.events.fire(GetOptions) as Reall3dMapViewerOptions).debugMode;
57 | that.opts = opts;
58 | that.meta = data;
59 | scene.add(that);
60 | that.initCSS3DSprite(opts);
61 | that.applyMatrix4(matrix);
62 | })
63 | .catch(e => {
64 | console.error(e.message);
65 | });
66 | }
67 |
68 | private async initCSS3DSprite(opts: SplatMeshOptions) {
69 | const that = this;
70 | const controls: MapControls = opts.controls;
71 | const tagWarp: HTMLDivElement = document.createElement('div');
72 | tagWarp.innerHTML = `
73 |
74 |
`;
75 | tagWarp.classList.add('splatmesh-point');
76 | tagWarp.style.position = 'absolute';
77 | tagWarp.style.borderRadius = '4px';
78 | tagWarp.style.cursor = 'pointer';
79 | let tween: Tween = null;
80 | tagWarp.onclick = () => {
81 | if (tween) return;
82 |
83 | const oldTarget = controls.target.clone(); // 原始视点
84 | const oldPos = controls.object.position.clone(); // 原始位置
85 | const newTarget = that.position.clone(); // 新视点
86 | const distance = isMobile ? 6 : 2; // 相机与新视点的距离
87 | const oldDir = oldTarget.clone().sub(oldPos).normalize(); // 计算原视线向量
88 | const newDir = oldDir.clone(); // 计算新视线向量(与原视线向量平行)
89 | const newPos = newTarget.clone().sub(newDir.multiplyScalar(distance)); // 计算相机的新位置
90 |
91 | const pt = { x: oldPos.x, y: oldPos.y, z: oldPos.z, tx: oldTarget.x, ty: oldTarget.y, tz: oldTarget.z };
92 | const to = { x: newPos.x, y: newPos.y, z: newPos.z, tx: newTarget.x, ty: newTarget.y, tz: newTarget.z };
93 | tween = new Tween(pt).to(to, 3500);
94 | tween
95 | .easing(Easing.Sinusoidal.InOut)
96 | .start()
97 | .onUpdate(() => {
98 | controls.object.position.set(pt.x, pt.y, pt.z);
99 | controls.target.set(pt.tx, pt.ty, pt.tz);
100 | })
101 | .onComplete(() => {
102 | tween = null;
103 | });
104 | };
105 | tagWarp.oncontextmenu = (e: MouseEvent) => e.preventDefault();
106 | const css3dTag = new CSS3DSprite(tagWarp);
107 | css3dTag.element.style.pointerEvents = 'none';
108 | css3dTag.visible = false;
109 | css3dTag.applyMatrix4(opts.matrix);
110 | that.css3dTag = css3dTag;
111 | opts.scene.add(css3dTag);
112 |
113 | // @ts-ignore
114 | const onMouseWheel = (e: WheelEvent) => this.mapViewer.controls._onMouseWheel(e);
115 | tagWarp.addEventListener('wheel', onMouseWheel, { passive: false });
116 | // @ts-ignore
117 | css3dTag.dispose = () => tagWarp.removeEventListener('wheel', onMouseWheel);
118 |
119 | that.onBeforeRender = () => {
120 | tween?.update();
121 |
122 | const MinDistance = isMobile ? 50 : 30;
123 | const MaxDistance = 100;
124 | const distance = that.position.distanceTo(that.mapViewer.controls.object.position);
125 | if (distance > MinDistance) {
126 | that.css3dTag.visible = that.opts.controls.object.position.y > 2;
127 | let scale = 0.002 * distance;
128 | css3dTag.scale.set(scale, scale, scale);
129 | that.css3dTag.visible = controls.object.position.y < 10 ? distance < MaxDistance : true; // 相机太低时,太远的不显示
130 | that.splatMesh && (that.splatMesh.visible = false);
131 | } else {
132 | if (!that.active) {
133 | that.splatMesh && (that.splatMesh.visible = false);
134 | let scale = 0.002 * distance;
135 | css3dTag.scale.set(scale, scale, scale);
136 | that.css3dTag.visible = true;
137 | return;
138 | }
139 |
140 | that.lastActiveTime = Date.now();
141 | that.css3dTag.visible = false;
142 |
143 | if (that.splatMesh) {
144 | that.splatMesh.visible = true;
145 | } else {
146 | const meta = that.meta;
147 | const opts: SplatMeshOptions = { ...that.opts };
148 | meta.autoCut && (opts.bigSceneMode = true);
149 | const splatMesh = new SplatMesh(opts);
150 | that.splatMesh = splatMesh;
151 | that.opts.scene.add(splatMesh);
152 | splatMesh.applyMatrix4(that.opts.matrix);
153 | splatMesh.meta = meta;
154 | const watermark = meta.watermark || meta.name || ''; // 水印文字
155 | meta.showWatermark = meta.showWatermark !== false; // 是否显示水印文字
156 | splatMesh.fire(SetGaussianText, watermark, true, false);
157 | splatMesh.addModel({ url: that.meta.url }, that.meta);
158 | }
159 | }
160 | };
161 |
162 | that.onAfterRender = () => {
163 | if (that.splatMesh && (!that.active || Date.now() - that.lastActiveTime > 1 * 60 * 1000)) {
164 | setTimeout(() => {
165 | that.splatMesh?.dispose();
166 | that.splatMesh = null;
167 | }, 5);
168 | }
169 | };
170 | }
171 |
172 | /**
173 | * 销毁
174 | */
175 | public dispose(): void {
176 | const that = this;
177 | if (that.disposed) return;
178 | that.disposed = true;
179 |
180 | that.opts.scene.remove(that.css3dTag);
181 | that.splatMesh?.dispose();
182 |
183 | that.meta = null;
184 | that.splatMesh = null;
185 | that.opts = null;
186 | that.css3dTag = null;
187 | that.mapViewer = null;
188 | }
189 | }
190 |
--------------------------------------------------------------------------------
/src/reall3d/meshs/controlplane/ArrowHelper.ts:
--------------------------------------------------------------------------------
1 | // ==============================================
2 | // Copyright (c) 2025 reall3d.com, MIT license
3 | // ==============================================
4 | import { CylinderGeometry, DoubleSide, Mesh, MeshBasicMaterial, Object3D, Vector3 } from 'three';
5 |
6 | export class ArrowHelper extends Object3D {
7 | private line: Mesh;
8 | private cone: Mesh;
9 | public declare type: string;
10 | private _axis: Vector3 = new Vector3();
11 |
12 | constructor(
13 | dir = new Vector3(0, 0, 1),
14 | origin = new Vector3(0, 0, 0),
15 | length = 1,
16 | radius = 0.1,
17 | color = 0xffff00,
18 | headLength = length * 0.2,
19 | headRadius = headLength * 0.2,
20 | ) {
21 | super();
22 |
23 | this.type = 'ArrowHelper';
24 |
25 | const lineGeometry = new CylinderGeometry(radius, radius, length, 32);
26 | lineGeometry.translate(0, length / 2.0, 0);
27 | const coneGeometry = new CylinderGeometry(0, headRadius, headLength, 32);
28 | coneGeometry.translate(0, length, 0);
29 |
30 | this.position.copy(origin);
31 |
32 | const lineMaterial = new MeshBasicMaterial({ color: color, toneMapped: false });
33 | lineMaterial.side = DoubleSide;
34 | this.line = new Mesh(lineGeometry, lineMaterial);
35 | this.line.matrixAutoUpdate = false;
36 | // @ts-ignore
37 | this.line.ignoreIntersect = true;
38 | this.add(this.line);
39 |
40 | const coneMaterial = new MeshBasicMaterial({ color: color, toneMapped: false });
41 | coneMaterial.side = DoubleSide;
42 | this.cone = new Mesh(coneGeometry, coneMaterial);
43 | this.cone.matrixAutoUpdate = false;
44 | // @ts-ignore
45 | this.cone.ignoreIntersect = true;
46 | this.add(this.cone);
47 |
48 | this.setDirection(dir);
49 | this.renderOrder = 99999;
50 | }
51 |
52 | setDirection(dir) {
53 | if (dir.y > 0.99999) {
54 | this.quaternion.set(0, 0, 0, 1);
55 | } else if (dir.y < -0.99999) {
56 | this.quaternion.set(1, 0, 0, 0);
57 | } else {
58 | this._axis.set(dir.z, 0, -dir.x).normalize();
59 | const radians = Math.acos(dir.y);
60 | this.quaternion.setFromAxisAngle(this._axis, radians);
61 | }
62 | }
63 |
64 | setColor(color) {
65 | // @ts-ignore
66 | this.line.material.color.set(color);
67 | // @ts-ignore
68 | this.cone.material.color.set(color);
69 | }
70 |
71 | copy(source) {
72 | super.copy(source, false);
73 | this.line.copy(source.line);
74 | this.cone.copy(source.cone);
75 | return this;
76 | }
77 |
78 | dispose() {
79 | this.line.geometry.dispose();
80 | // @ts-ignore
81 | this.line.material.dispose();
82 | this.cone.geometry.dispose();
83 | // @ts-ignore
84 | this.cone.material.dispose();
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/reall3d/meshs/controlplane/SetupControlPlane.ts:
--------------------------------------------------------------------------------
1 | // ==============================================
2 | // Copyright (c) 2025 reall3d.com, MIT license
3 | // ==============================================
4 | import { DoubleSide, Mesh, MeshBasicMaterial, Object3D, PlaneGeometry, Quaternion, Vector3 } from 'three';
5 | import { Events } from '../../events/Events';
6 | import {
7 | ControlPlaneUpdate,
8 | ControlPlaneSwitchVisible,
9 | GetCameraLookAt,
10 | GetCameraLookUp,
11 | GetControlPlane,
12 | GetScene,
13 | ViewerNeedUpdate,
14 | IsControlPlaneVisible,
15 | } from '../../events/EventConstants';
16 | import { ArrowHelper } from './ArrowHelper';
17 |
18 | export function setupControlPlane(events: Events) {
19 | const on = (key: number, fn?: Function, multiFn?: boolean): Function | Function[] => events.on(key, fn, multiFn);
20 | const fire = (key: number, ...args: any): any => events.fire(key, ...args);
21 |
22 | const planeGeometry = new PlaneGeometry(1, 1);
23 | planeGeometry.rotateX(-Math.PI / 2);
24 | const planeMaterial = new MeshBasicMaterial({ color: 0xffffff });
25 | planeMaterial.transparent = true;
26 | planeMaterial.opacity = 0.6;
27 | planeMaterial.depthTest = false;
28 | planeMaterial.depthWrite = false;
29 | planeMaterial.side = DoubleSide;
30 | const planeMesh = new Mesh(planeGeometry, planeMaterial);
31 | // @ts-ignore
32 | planeMesh.ignoreIntersect = true;
33 |
34 | const arrowDir = new Vector3(0, -1, 0);
35 | arrowDir.normalize();
36 | const arrowOrigin = new Vector3(0, 0, 0);
37 | const arrowLength = 0.5;
38 | const arrowRadius = 0.01;
39 | const arrowColor = 0xffff66; // 0x00dd00;
40 | const headLength = 0.1;
41 | const headWidth = 0.03;
42 | const arrowHelper = new ArrowHelper(arrowDir, arrowOrigin, arrowLength, arrowRadius, arrowColor, headLength, headWidth);
43 |
44 | const controlPlane = new Object3D();
45 | controlPlane.add(planeMesh);
46 | controlPlane.add(arrowHelper);
47 | controlPlane.renderOrder = 99999;
48 | planeMesh.renderOrder = 99999;
49 | // arrowHelper.renderOrder = 99999;
50 | controlPlane.visible = false;
51 |
52 | fire(GetScene).add(controlPlane);
53 |
54 | on(GetControlPlane, () => controlPlane);
55 | on(ControlPlaneSwitchVisible, (visible?: boolean) => {
56 | fire(ControlPlaneUpdate, true);
57 | controlPlane.visible = visible === undefined ? !controlPlane.visible : visible;
58 | fire(ViewerNeedUpdate);
59 | });
60 | on(IsControlPlaneVisible, () => controlPlane.visible);
61 |
62 | on(ControlPlaneUpdate, (force: boolean = false) => {
63 | if (force || controlPlane.visible) {
64 | const tempQuaternion = new Quaternion();
65 | const defaultUp = new Vector3(0, -1, 0);
66 | tempQuaternion.setFromUnitVectors(defaultUp, fire(GetCameraLookUp));
67 | controlPlane.position.copy(fire(GetCameraLookAt));
68 | controlPlane.quaternion.copy(tempQuaternion);
69 | }
70 | });
71 | }
72 |
--------------------------------------------------------------------------------
/src/reall3d/meshs/focusmaker/SetupFocusMarker.ts:
--------------------------------------------------------------------------------
1 | // ==============================================
2 | // Copyright (c) 2025 reall3d.com, MIT license
3 | // ==============================================
4 | import { Scene, Vector3 } from 'three';
5 | import { Events } from '../../events/Events';
6 | import {
7 | ViewerNeedUpdate,
8 | FocusMarkerSetOpacity,
9 | FocusMarkerAutoDisappear,
10 | FocusMarkerUpdate,
11 | GetScene,
12 | GetCameraPosition,
13 | RunLoopByFrame,
14 | FocusMarkerUpdateScale,
15 | GetCanvasSize,
16 | } from '../../events/EventConstants';
17 | import { CSS3DSprite } from 'three/examples/jsm/Addons.js';
18 |
19 | export function setupFocusMarker(events: Events) {
20 | let disposed = false;
21 | const on = (key: number, fn?: Function, multiFn?: boolean): Function | Function[] => events.on(key, fn, multiFn);
22 | const fire = (key: number, ...args: any): any => events.fire(key, ...args);
23 |
24 | let css3dFocusMarker: CSS3DSprite = initFocusMarker();
25 | const aryProcessDisappear: any[] = [];
26 |
27 | on(FocusMarkerUpdateScale, () => {
28 | const { height } = fire(GetCanvasSize);
29 | const newScale = ((fire(GetCameraPosition) as Vector3).distanceTo(css3dFocusMarker.position) * 3.2) / height;
30 | css3dFocusMarker.scale.set(newScale, newScale, newScale);
31 | });
32 | on(FocusMarkerSetOpacity, (val: number) => {
33 | if (disposed) return;
34 | fire(FocusMarkerUpdateScale);
35 | css3dFocusMarker.visible = val > 0.1;
36 | css3dFocusMarker.element.style.opacity = val + '';
37 | fire(ViewerNeedUpdate);
38 | });
39 |
40 | on(FocusMarkerUpdate, (focusPosition: Vector3) => {
41 | if (disposed) return;
42 | css3dFocusMarker.position.copy(focusPosition);
43 | while (aryProcessDisappear.length) aryProcessDisappear.pop().stop = true;
44 | fire(FocusMarkerSetOpacity, 1);
45 | fire(ViewerNeedUpdate);
46 | });
47 |
48 | on(FocusMarkerAutoDisappear, () => {
49 | while (aryProcessDisappear.length) aryProcessDisappear.pop().stop = true;
50 | let process = { opacity: 1.0, time: Date.now(), stop: false };
51 | aryProcessDisappear.push(process);
52 |
53 | fire(
54 | RunLoopByFrame,
55 | () => {
56 | process = aryProcessDisappear[0];
57 | !process && fire(FocusMarkerSetOpacity, 1);
58 | if (!disposed && process) {
59 | process.opacity = 1 - Math.min((Date.now() - process.time) / 1500, 1);
60 | if (process.opacity < 0.2) {
61 | process.opacity = 0;
62 | process.stop = true;
63 | }
64 | fire(FocusMarkerSetOpacity, process.opacity);
65 | }
66 | },
67 | () => !disposed && !process?.stop,
68 | );
69 | });
70 |
71 | function initFocusMarker() {
72 | const tagWarp: HTMLDivElement = document.createElement('div');
73 | tagWarp.innerHTML = ` `;
74 | tagWarp.classList.add('css3d-focus-marker');
75 | tagWarp.style.position = 'absolute';
76 | const css3dTag = new CSS3DSprite(tagWarp);
77 | css3dTag.element.style.pointerEvents = 'none';
78 | css3dTag.element.style.opacity = '1';
79 | css3dTag.visible = false;
80 | (fire(GetScene) as Scene).add(css3dTag);
81 |
82 | css3dTag.onBeforeRender = () => fire(FocusMarkerUpdateScale);
83 | return css3dTag;
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/reall3d/meshs/mark/MarkSinglePoint.ts:
--------------------------------------------------------------------------------
1 | // ==============================================
2 | // Copyright (c) 2025 reall3d.com, MIT license
3 | // ==============================================
4 | import { Group, Vector3 } from 'three';
5 | import { CSS3DSprite } from 'three/examples/jsm/Addons.js';
6 | import { Events } from '../../events/Events';
7 | import {
8 | AddMarkToWeakRef,
9 | DeleteMarkWeakRef,
10 | GetOptions,
11 | GetScene,
12 | MarkFinish,
13 | StopAutoRotate,
14 | TraverseDisposeAndClear,
15 | ViewerNeedUpdate,
16 | } from '../../events/EventConstants';
17 | import { MarkData } from './data/MarkData';
18 | import { MarkDataSinglePoint } from './data/MarkDataSinglePoint';
19 |
20 | export class MarkSinglePoint extends Group {
21 | public readonly isMark: boolean = true;
22 | private disposed: boolean = false;
23 | private events: Events;
24 | private data: MarkDataSinglePoint;
25 | private css3dTag: CSS3DSprite;
26 |
27 | constructor(events: Events, obj: Vector3 | MarkDataSinglePoint, name?: string) {
28 | super();
29 | this.events = events;
30 | const that = this;
31 |
32 | let data: MarkDataSinglePoint;
33 | if (obj instanceof Vector3) {
34 | const cnt: number = document.querySelectorAll('.mark-wrap-point').length + 1;
35 | data = {
36 | type: 'MarkSinglePoint',
37 | name: name || 'point' + Date.now(),
38 | point: obj.toArray(),
39 | iconName: '#svgicon-point2',
40 | iconColor: '#eeee00',
41 | iconOpacity: 0.8,
42 | mainTagColor: '#c4c4c4',
43 | mainTagBackground: '#2E2E30',
44 | mainTagOpacity: 0.8,
45 | title: '标记点' + cnt,
46 | note: '',
47 | };
48 | } else {
49 | data = {
50 | type: 'MarkSinglePoint',
51 | name: obj.name || 'point' + Date.now(),
52 | point: [...obj.point],
53 | iconName: obj.iconName || '#svgicon-point2',
54 | iconColor: obj.iconColor || '#eeee00',
55 | iconOpacity: obj.iconOpacity || 0.8,
56 | mainTagColor: obj.mainTagColor || '#c4c4c4',
57 | mainTagBackground: obj.mainTagBackground || '#2E2E30',
58 | mainTagOpacity: obj.mainTagOpacity || 0.8,
59 | title: obj.title || '标记点',
60 | note: obj.note || '',
61 | };
62 | }
63 |
64 | const tagWarp: HTMLDivElement = document.createElement('div');
65 | tagWarp.innerHTML = `
66 | ${data.title}
67 |
68 |
`;
69 | tagWarp.classList.add('mark-wrap-point', `mark-wrap-${data.name}`);
70 | tagWarp.style.position = 'absolute';
71 | tagWarp.style.borderRadius = '4px';
72 | tagWarp.style.cursor = 'pointer';
73 | tagWarp.onclick = () => {
74 | if (that.events.fire(GetOptions).markMode) return;
75 | // @ts-ignore
76 | const onActiveMark = parent?.onActiveMark;
77 | onActiveMark?.(that.getMarkData(true));
78 | that.events.fire(StopAutoRotate);
79 | };
80 | tagWarp.oncontextmenu = (e: MouseEvent) => e.preventDefault();
81 |
82 | const css3dTag = new CSS3DSprite(tagWarp);
83 | css3dTag.position.set(data.point[0], data.point[1], data.point[2]);
84 | css3dTag.element.style.pointerEvents = 'none';
85 | css3dTag.scale.set(0.01, 0.01, 0.01);
86 |
87 | that.data = data;
88 | that.css3dTag = css3dTag;
89 | that.add(css3dTag);
90 | events.fire(AddMarkToWeakRef, that);
91 | }
92 |
93 | /**
94 | * 绘制更新
95 | */
96 | public drawUpdate(data?: MarkDataSinglePoint, saveData: boolean = true) {
97 | if (this.disposed) return;
98 | const that = this;
99 |
100 | if (data?.iconName) {
101 | saveData && (that.data.iconName = data.iconName);
102 | const svg: SVGElement = this.css3dTag.element.querySelector(`.mark-wrap-${that.data.name} svg`);
103 | svg.innerHTML = ` `;
104 | }
105 | if (data?.iconColor) {
106 | saveData && (that.data.iconColor = data.iconColor);
107 | const svg: SVGElement = this.css3dTag.element.querySelector(`.mark-wrap-${that.data.name} svg`);
108 | svg.style.color = data.iconColor;
109 | }
110 | if (data?.iconOpacity) {
111 | saveData && (that.data.iconOpacity = data.iconOpacity);
112 | const svg: SVGElement = this.css3dTag.element.querySelector(`.mark-wrap-${that.data.name} svg`);
113 | svg.style.opacity = data.iconOpacity.toString();
114 | }
115 | if (data?.mainTagColor) {
116 | saveData && (that.data.mainTagColor = data.mainTagColor);
117 | (this.css3dTag.element.querySelector(`.${that.data.name}`) as HTMLSpanElement).style.color = data.mainTagColor;
118 | }
119 | if (data?.mainTagBackground) {
120 | saveData && (that.data.mainTagBackground = data.mainTagBackground);
121 | (this.css3dTag.element.querySelector(`.${that.data.name}`) as HTMLSpanElement).style.background = data.mainTagBackground;
122 | }
123 | if (data?.mainTagOpacity) {
124 | saveData && (that.data.mainTagOpacity = data.mainTagOpacity);
125 | (this.css3dTag.element.querySelector(`.${that.data.name}`) as HTMLSpanElement).style.opacity = data.mainTagOpacity.toString();
126 | }
127 | if (data?.title !== undefined) {
128 | saveData && (that.data.title = data.title);
129 | (this.css3dTag.element.querySelector(`.${that.data.name}`) as HTMLSpanElement).innerText = data.title;
130 | }
131 | if (data?.note !== undefined) {
132 | saveData && (that.data.note = data.note);
133 | }
134 |
135 | that.events.fire(ViewerNeedUpdate);
136 | }
137 |
138 | public resetMeterScale(markData: any) {
139 | if (markData?.meterScale === undefined) return;
140 | this.events.fire(GetOptions).meterScale = markData.meterScale;
141 | }
142 |
143 | /**
144 | * 绘制结束
145 | */
146 | public drawFinish() {
147 | if (this.disposed) return;
148 | const that = this;
149 | that.events.fire(MarkFinish);
150 |
151 | // @ts-ignore
152 | const onActiveMark = parent?.onActiveMark;
153 | const data: any = that.getMarkData(true);
154 | data.isNew = true;
155 | data.meterScale = that.events.fire(GetOptions).meterScale;
156 | onActiveMark?.(data);
157 | }
158 |
159 | public getMarkData(simple: boolean = false): MarkData {
160 | const data: MarkDataSinglePoint = { ...this.data };
161 | if (simple) {
162 | delete data.point;
163 | } else {
164 | data.point = [...data.point];
165 | }
166 | return data;
167 | }
168 |
169 | public dispose() {
170 | if (this.disposed) return;
171 | const that = this;
172 | that.disposed = true;
173 |
174 | that.events.fire(TraverseDisposeAndClear, that);
175 | that.events.fire(GetScene).remove(that);
176 | that.events.fire(DeleteMarkWeakRef, that);
177 |
178 | const wrap: HTMLDivElement = document.querySelector(`.mark-wrap-${that.data.name}`);
179 | wrap?.parentElement?.removeChild?.(wrap);
180 |
181 | that.events = null;
182 | that.data = null;
183 | that.css3dTag = null;
184 | }
185 | }
186 |
--------------------------------------------------------------------------------
/src/reall3d/meshs/mark/SetupMark.ts:
--------------------------------------------------------------------------------
1 | // ==============================================
2 | // Copyright (c) 2025 reall3d.com, MIT license
3 | // ==============================================
4 | import { MarkCirclePlan } from './MarkCirclePlan';
5 | import {
6 | AddMarkToWeakRef,
7 | ClearMarkPoint,
8 | ComputePlansArea,
9 | ComputePlansCenter,
10 | ComputePoint3Area,
11 | CSS3DRendererDispose,
12 | GetCamera,
13 | GetCSS3DRenderer,
14 | GetMarkWarpElement,
15 | GetOptions,
16 | GetScene,
17 | MarkFinish,
18 | LoadSmallSceneMetaData,
19 | MetaMarkRemoveData,
20 | MetaMarkSaveData,
21 | MarkUpdateVisible,
22 | OnViewerUpdate,
23 | HttpPostMetaData,
24 | ViewerNeedUpdate,
25 | DeleteMarkWeakRef,
26 | GetMarkFromWeakRef,
27 | GetMarkDataByName,
28 | UpdateMarkByName,
29 | MetaSaveSmallSceneCameraInfo,
30 | GetCameraInfo,
31 | UpdateAllMarkByMeterScale,
32 | ReComputePlansArea,
33 | Information,
34 | GetCachedWaterMark,
35 | MetaSaveWatermark,
36 | OnSetFlyPositions,
37 | OnSetFlyTargets,
38 | GetSplatMesh,
39 | GetControls,
40 | } from './../../events/EventConstants';
41 | import { MarkMultiLines } from './MarkMultiLines';
42 | import { CSS3DRenderer } from 'three/examples/jsm/Addons.js';
43 | import { Events } from '../../events/Events';
44 | import { Object3D, Vector3 } from 'three';
45 | import { Reall3dViewerOptions } from '../../viewer/Reall3dViewerOptions';
46 | import { MarkDistanceLine } from './MarkDistanceLine';
47 | import { MarkData } from './data/MarkData';
48 | import { MarkSinglePoint } from './MarkSinglePoint';
49 | import { MarkMultiPlans } from './MarkMulitPlans';
50 | import { MarkDataSinglePoint } from './data/MarkDataSinglePoint';
51 | import { MarkDataMultiPlans } from './data/MarkDataMultiPlans';
52 | import { MarkDataMultiLines } from './data/MarkDataMultiLines';
53 | import { MarkDataDistanceLine } from './data/MarkDataDistanceLine';
54 | import { MetaData } from '../../modeldata/ModelData';
55 | import { CameraControls } from '../../controls/CameraControls';
56 |
57 | export function setupMark(events: Events) {
58 | const on = (key: number, fn?: Function, multiFn?: boolean): Function | Function[] => events.on(key, fn, multiFn);
59 | const fire = (key: number, ...args: any): any => events.fire(key, ...args);
60 | const markMap: Map> = new Map();
61 |
62 | const divMarkWarp: HTMLDivElement = document.createElement('div');
63 | divMarkWarp.classList.add('mark-warp');
64 | document.body.appendChild(divMarkWarp);
65 |
66 | const css3DRenderer = new CSS3DRenderer();
67 | css3DRenderer.setSize(innerWidth, innerHeight);
68 | css3DRenderer.domElement.style.position = 'absolute';
69 | css3DRenderer.domElement.style.top = '0px';
70 | css3DRenderer.domElement.style.pointerEvents = 'none';
71 | divMarkWarp.appendChild(css3DRenderer.domElement);
72 |
73 | on(GetMarkWarpElement, () => divMarkWarp);
74 | on(GetCSS3DRenderer, () => css3DRenderer);
75 | on(CSS3DRendererDispose, () => document.body.removeChild(divMarkWarp));
76 | on(OnViewerUpdate, () => css3DRenderer.render(fire(GetScene), fire(GetCamera)), true);
77 |
78 | on(AddMarkToWeakRef, (mark: any) => {
79 | const name: string = mark?.getMarkData?.()?.name || mark?.name;
80 | if (!name) return;
81 | markMap.set(name, new WeakRef(mark));
82 | });
83 | on(DeleteMarkWeakRef, (mark: any) => {
84 | const name: string = mark?.getMarkData?.()?.name || mark?.name;
85 | markMap.delete(name);
86 | });
87 | on(GetMarkFromWeakRef, (name: string): any => markMap.get(name)?.deref());
88 | on(UpdateMarkByName, (name: string, data: MarkDataSinglePoint | MarkDataMultiPlans | MarkDataMultiLines | MarkDataDistanceLine) => {
89 | const mark = fire(GetMarkFromWeakRef, name);
90 | if (!mark || !data) return;
91 | mark.drawUpdate?.(data);
92 | });
93 | on(GetMarkDataByName, (name: string): MarkDataSinglePoint | MarkDataMultiPlans | MarkDataMultiLines | MarkDataDistanceLine => {
94 | const mark = fire(GetMarkFromWeakRef, name);
95 | if (!mark) return {};
96 | return mark.getMarkData?.();
97 | });
98 |
99 | on(MarkUpdateVisible, (visible?: boolean) => {
100 | if (visible !== undefined) fire(GetOptions).markVisible = visible;
101 | fire(GetScene).traverse((child: Object3D) => child['isMark'] && (child.visible = fire(GetOptions).markVisible));
102 | fire(ViewerNeedUpdate);
103 | });
104 |
105 | on(MetaSaveSmallSceneCameraInfo, async (): Promise => {
106 | const marks = [];
107 | fire(GetScene).traverse((child: any) => {
108 | if (child.isMark) {
109 | const data = child.getMarkData?.();
110 | data && marks.push(data);
111 | }
112 | });
113 |
114 | const meta: MetaData = fire(GetSplatMesh).meta || {};
115 | if (marks.length) {
116 | meta.marks = marks;
117 | } else {
118 | delete meta.marks;
119 | }
120 | meta.cameraInfo = fire(GetCameraInfo);
121 |
122 | return await fire(HttpPostMetaData, meta);
123 | });
124 |
125 | on(MetaMarkSaveData, async (): Promise => {
126 | const marks = [];
127 | fire(GetScene).traverse((child: any) => {
128 | if (child.isMark) {
129 | const data = child.getMarkData?.();
130 | data && marks.push(data);
131 | }
132 | });
133 |
134 | const meta: MetaData = fire(GetSplatMesh).meta || {};
135 | if (marks.length) {
136 | meta.marks = marks;
137 | } else {
138 | delete meta.marks;
139 | }
140 |
141 | return await fire(HttpPostMetaData, meta);
142 | });
143 |
144 | on(MetaMarkRemoveData, async (): Promise => {
145 | const meta: MetaData = fire(GetSplatMesh).meta || {};
146 | delete meta.marks;
147 |
148 | const rs = await fire(HttpPostMetaData, meta);
149 |
150 | const marks: WeakRef[] = [];
151 | markMap.forEach(item => marks.push(item));
152 | marks.forEach(item => item.deref()?.dispose?.());
153 | fire(ViewerNeedUpdate);
154 |
155 | return rs;
156 | });
157 |
158 | on(MetaSaveWatermark, async (): Promise => {
159 | const marks = [];
160 | fire(GetScene).traverse((child: any) => {
161 | if (child.isMark) {
162 | const data = child.getMarkData?.();
163 | data && marks.push(data);
164 | }
165 | });
166 | const meta: MetaData = fire(GetSplatMesh).meta || {};
167 | meta.watermark = fire(GetCachedWaterMark) || '';
168 |
169 | return await fire(HttpPostMetaData, meta);
170 | });
171 |
172 | on(LoadSmallSceneMetaData, (metaData: MetaData) => {
173 | if (metaData.meterScale) {
174 | fire(GetOptions).meterScale = metaData.meterScale;
175 | fire(Information, { scale: `1 : ${fire(GetOptions).meterScale} m` });
176 | }
177 | (fire(GetControls) as CameraControls).updateByOptions({ ...metaData, ...(metaData.cameraInfo || {}) });
178 |
179 | const marks = metaData.marks || [];
180 |
181 | // 初始化标注,隐藏待激活显示
182 | marks.forEach((data: MarkData) => {
183 | if (data.type === 'MarkSinglePoint') {
184 | // 单点
185 | const mark = new MarkSinglePoint(events, data);
186 | mark.visible = false;
187 | fire(GetScene).add(mark);
188 | } else if (data.type === 'MarkDistanceLine') {
189 | // 测量距离
190 | const mark = new MarkDistanceLine(events);
191 | mark.draw(data);
192 | mark.visible = false;
193 | fire(GetScene).add(mark);
194 | } else if (data.type === 'MarkMultiLines') {
195 | // 折线
196 | const mark = new MarkMultiLines(events);
197 | mark.draw(data, true);
198 | mark.visible = false;
199 | fire(GetScene).add(mark);
200 | } else if (data.type === 'MarkMultiPlans') {
201 | // 多面
202 | const mark = new MarkMultiPlans(events);
203 | mark.draw(data, true);
204 | mark.visible = false;
205 | fire(GetScene).add(mark);
206 | } else if (data.type === 'MarkCirclePlan') {
207 | // 圆面
208 | const mark = new MarkCirclePlan(events);
209 | mark.draw(data);
210 | mark.visible = false;
211 | fire(GetScene).add(mark);
212 | }
213 | });
214 |
215 | fire(OnSetFlyPositions, metaData.flyPositions || []);
216 | fire(OnSetFlyTargets, metaData.flyTargets || []);
217 | });
218 |
219 | on(MarkFinish, () => {
220 | const opts: Reall3dViewerOptions = fire(GetOptions);
221 | opts.markMode = false;
222 | fire(ClearMarkPoint);
223 | fire(ViewerNeedUpdate);
224 | });
225 |
226 | on(UpdateAllMarkByMeterScale, (markData: any, saveData: boolean = true) => {
227 | const meterScale: number = markData?.meterScale;
228 | if (!meterScale) return;
229 | if (typeof meterScale !== 'number' || meterScale <= 0) {
230 | console.warn('meterScale is not a number or <= 0', markData);
231 | return;
232 | }
233 |
234 | saveData && (fire(GetOptions).meterScale = markData.meterScale);
235 | fire(Information, { scale: `1 : ${meterScale} m` });
236 |
237 | for (const value of markMap.values()) {
238 | const mark = value.deref();
239 | if (!mark) continue;
240 |
241 | if (mark instanceof MarkDistanceLine) {
242 | (mark as MarkDistanceLine).updateByMeterScale(meterScale);
243 | } else if (mark instanceof MarkMultiLines) {
244 | (mark as MarkMultiLines).updateByMeterScale(meterScale);
245 | } else if (mark instanceof MarkMultiPlans) {
246 | (mark as MarkMultiPlans).updateByMeterScale(meterScale);
247 | }
248 | }
249 | });
250 |
251 | on(ComputePlansCenter, (positions: number[]): Vector3 => {
252 | const p0 = new Vector3().fromArray(positions.slice(0, 3));
253 | const p1 = new Vector3().fromArray(positions.slice(-6, -3));
254 | const p2 = new Vector3().fromArray(positions.slice(-3));
255 | const eq02 = p0.distanceTo(p2) < 0.0001;
256 | const eq12 = p1.distanceTo(p2) < 0.0001;
257 | const center = new Vector3();
258 | const cnt = eq02 || eq12 ? positions.length / 3 - 1 : positions.length / 3;
259 | for (let i = 0; i < cnt; i++) {
260 | center.add(new Vector3(positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2]));
261 | }
262 | center.divideScalar(cnt);
263 | return center;
264 | });
265 |
266 | on(ComputePlansArea, (positions: number[]): number => {
267 | const points: Vector3[] = [];
268 | for (let i = 0, cnt = positions.length / 3; i < cnt; i++) {
269 | points.push(new Vector3(positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2]));
270 | }
271 | const eq1 = points[0].distanceTo(points[points.length - 1]) < 0.0001;
272 | const eq2 = points[points.length - 2].distanceTo(points[points.length - 1]) < 0.0001;
273 | (eq1 || eq2) && points.pop();
274 |
275 | if (points.length < 3) return 0;
276 |
277 | let total: number = 0;
278 | for (let i = 0, cnt = points.length - 2; i < cnt; i++) {
279 | total += fire(ComputePoint3Area, points[0], points[i + 1], points[i + 2], fire(GetOptions).meterScale);
280 | }
281 | return total;
282 | });
283 |
284 | on(ReComputePlansArea, (points: Vector3[], meterScale: number): number => {
285 | let total: number = 0;
286 | for (let i = 0, cnt = points.length - 2; i < cnt; i++) {
287 | total += fire(ComputePoint3Area, points[0], points[i + 1], points[i + 2], meterScale);
288 | }
289 | return total;
290 | });
291 |
292 | on(ComputePoint3Area, (p1: Vector3, p2: Vector3, p3: Vector3, meterScale: number): number => {
293 | const a = p1.distanceTo(p2) * meterScale;
294 | const b = p2.distanceTo(p3) * meterScale;
295 | const c = p3.distanceTo(p1) * meterScale;
296 | const s = (a + b + c) / 2;
297 | return Math.sqrt(s * (s - a) * (s - b) * (s - c));
298 | });
299 | }
300 |
--------------------------------------------------------------------------------
/src/reall3d/meshs/mark/data/MarkData.ts:
--------------------------------------------------------------------------------
1 | // ==============================================
2 | // Copyright (c) 2025 reall3d.com, MIT license
3 | // ==============================================
4 | export interface MarkData {
5 | /** 类型 */
6 | type?: 'MarkDistanceLine' | 'MarkSinglePoint' | 'MarkMultiLines' | 'MarkMultiPlans' | 'MarkCirclePlan' | undefined;
7 | /** 名称(样式类名等标识用) */
8 | name?: string;
9 | }
10 |
--------------------------------------------------------------------------------
/src/reall3d/meshs/mark/data/MarkDataCirclePlan.ts:
--------------------------------------------------------------------------------
1 | // ==============================================
2 | // Copyright (c) 2025 reall3d.com, MIT license
3 | // ==============================================
4 | import { MarkData } from './MarkData';
5 |
6 | /**
7 | * 圆面数据
8 | */
9 | export interface MarkDataCirclePlan extends MarkData {
10 | /** 圆心点 */
11 | startPoint?: number[];
12 | /** 圆半径 */
13 | radius?: number;
14 | /** 圆颜色,默认 #eeee00 */
15 | circleColor?: string;
16 | /** 圆颜色透明度,默认0.5 */
17 | circleOpacity?: number;
18 | /** 主标签字体颜色,默认 #c4c4c4 */
19 | mainTagColor?: string;
20 | /** 主标签背景颜色,默认 #2E2E30 */
21 | mainTagBackground?: string;
22 | /** 主标签透明度,默认 0.8 */
23 | mainTagOpacity?: number;
24 | /** 主标签标签是否显示,默认 true */
25 | mainTagVisible?: boolean;
26 | /** 标签字体颜色,默认 #000000 */
27 | circleTagColor?: string;
28 | /** 标签背景颜色,默认 #e0ffff */
29 | circleTagBackground?: string;
30 | /** 标签透明度,默认 0.9 */
31 | circleTagOpacity?: number;
32 | /** 标签是否显示,默认 true */
33 | circleTagVisible?: boolean;
34 | /** 标题 */
35 | title?: string;
36 | /** 说明 */
37 | note?: string;
38 | }
39 |
--------------------------------------------------------------------------------
/src/reall3d/meshs/mark/data/MarkDataDistanceLine.ts:
--------------------------------------------------------------------------------
1 | // ==============================================
2 | // Copyright (c) 2025 reall3d.com, MIT license
3 | // ==============================================
4 | import { MarkData } from './MarkData';
5 |
6 | /**
7 | * 距离测量线数据
8 | */
9 | export interface MarkDataDistanceLine extends MarkData {
10 | /** 起点 */
11 | startPoint?: number[];
12 | /** 终点 */
13 | endPoint?: number[];
14 | /** 线颜色,默认 #eeee00 */
15 | lineColor?: string;
16 | /** 线宽,默认 3 */
17 | lineWidth?: number;
18 | /** 主标签字体颜色,默认 #c4c4c4 */
19 | mainTagColor?: string;
20 | /** 主标签背景颜色,默认 #2E2E30 */
21 | mainTagBackground?: string;
22 | /** 主标签透明度,默认 0.8 */
23 | mainTagOpacity?: number;
24 | /** 主标签标签是否显示,默认 true */
25 | mainTagVisible?: boolean;
26 | /** 标签字体颜色,默认 #000000 */
27 | distanceTagColor?: string;
28 | /** 标签背景颜色,默认 #e0ffff */
29 | distanceTagBackground?: string;
30 | /** 标签透明度,默认 0.9 */
31 | distanceTagOpacity?: number;
32 | /** 距离标签是否显示,默认 true */
33 | distanceTagVisible?: boolean;
34 | /** 标题 */
35 | title?: string;
36 | /** 说明 */
37 | note?: string;
38 | }
39 |
--------------------------------------------------------------------------------
/src/reall3d/meshs/mark/data/MarkDataMultiLines.ts:
--------------------------------------------------------------------------------
1 | // ==============================================
2 | // Copyright (c) 2025 reall3d.com, MIT license
3 | // ==============================================
4 | import { MarkData } from './MarkData';
5 |
6 | /**
7 | * 多线段连接线数据
8 | */
9 | export interface MarkDataMultiLines extends MarkData {
10 | /** 点 */
11 | points?: number[];
12 | /** 线颜色,默认 #eeee00 */
13 | lineColor?: string;
14 | /** 线宽,默认 3 */
15 | lineWidth?: number;
16 | /** 主标签字体颜色,默认 #c4c4c4 */
17 | mainTagColor?: string;
18 | /** 主标签背景颜色,默认 #2E2E30 */
19 | mainTagBackground?: string;
20 | /** 主标签透明度,默认 0.8 */
21 | mainTagOpacity?: number;
22 | /** 主标签标签是否显示,默认 true */
23 | mainTagVisible?: boolean;
24 | /** 距离标签字体颜色,默认 #000000 */
25 | distanceTagColor?: string;
26 | /** 距离标签背景颜色,默认 #e0ffff */
27 | distanceTagBackground?: string;
28 | /** 距离标签透明度,默认 0.9 */
29 | distanceTagOpacity?: number;
30 | /** 距离标签是否显示,默认 true */
31 | distanceTagVisible?: boolean;
32 | /** 标题 */
33 | title?: string;
34 | /** 说明 */
35 | note?: string;
36 | }
37 |
--------------------------------------------------------------------------------
/src/reall3d/meshs/mark/data/MarkDataMultiPlans.ts:
--------------------------------------------------------------------------------
1 | // ==============================================
2 | // Copyright (c) 2025 reall3d.com, MIT license
3 | // ==============================================
4 | import { MarkData } from './MarkData';
5 |
6 | /**
7 | * 多三角面数据
8 | */
9 | export interface MarkDataMultiPlans extends MarkData {
10 | /** 点 */
11 | points?: number[];
12 | /** 线颜色(面颜色共用),默认 #eeee00 */
13 | lineColor?: string;
14 | /** 线宽,默认 3 */
15 | lineWidth?: number;
16 | /** 主标签字体颜色,默认 #c4c4c4 */
17 | mainTagColor?: string;
18 | /** 主标签背景颜色,默认 #2E2E30 */
19 | mainTagBackground?: string;
20 | /** 主标签透明度,默认 0.8 */
21 | mainTagOpacity?: number;
22 | /** 主标签标签是否显示,默认 true */
23 | mainTagVisible?: boolean;
24 | /** 面积标签字体颜色,默认 #000000 */
25 | areaTagColor?: string;
26 | /** 面积标签背景颜色,默认 #e0ffff */
27 | areaTagBackground?: string;
28 | /** 面积标签透明度,默认 0.9 */
29 | areaTagOpacity?: number;
30 | /** 面积标签是否显示,默认 true */
31 | areaTagVisible?: boolean;
32 | /** 距离标签是否显示,默认 true */
33 | distanceTagVisible?: boolean;
34 | /** 面透明度,默认 0.5 */
35 | planOpacity?: number;
36 | /** 标题 */
37 | title?: string;
38 | /** 说明 */
39 | note?: string;
40 | }
41 |
--------------------------------------------------------------------------------
/src/reall3d/meshs/mark/data/MarkDataSinglePoint.ts:
--------------------------------------------------------------------------------
1 | // ==============================================
2 | // Copyright (c) 2025 reall3d.com, MIT license
3 | // ==============================================
4 | import { MarkData } from './MarkData';
5 |
6 | /**
7 | * 单点数据
8 | */
9 | export interface MarkDataSinglePoint extends MarkData {
10 | /** 点位置 */
11 | point?: number[];
12 | /** 图标名称,默认 'pointIcon1' */
13 | iconName?: string;
14 | /** 图标颜色,默认 #F78A14 */
15 | iconColor?: string;
16 | /** 图标透明度,默认 0.8 */
17 | iconOpacity?: number;
18 | /** 主标签字体颜色,默认 #c4c4c4 */
19 | mainTagColor?: string;
20 | /** 主标签背景颜色,默认 #2E2E30 */
21 | mainTagBackground?: string;
22 | /** 主标签透明度,默认 0.8 */
23 | mainTagOpacity?: number;
24 | /** 标题 */
25 | title?: string;
26 | /** 说明 */
27 | note?: string;
28 | }
29 |
--------------------------------------------------------------------------------
/src/reall3d/meshs/splatmesh/SplatMesh.ts:
--------------------------------------------------------------------------------
1 | // ==============================================
2 | // Copyright (c) 2025 reall3d.com, MIT license
3 | // ==============================================
4 | import { Mesh, PerspectiveCamera, Vector3 } from 'three';
5 | import { Events } from '../../events/Events';
6 | import {
7 | SplatUpdatePerformanceNow,
8 | SplatUpdatePointMode,
9 | SplatUpdateLightFactor,
10 | IsPointcloudMode,
11 | CreateSplatMesh,
12 | SplatMeshDispose,
13 | SplatTexdataManagerDispose,
14 | WorkerDispose,
15 | GetScene,
16 | GetOptions,
17 | GetCanvas,
18 | GetRenderer,
19 | IsBigSceneMode,
20 | SplatTexdataManagerAddModel,
21 | WorkerSort,
22 | SplatTexdataManagerDataChanged,
23 | NotifyViewerNeedUpdate,
24 | ViewerNeedUpdate,
25 | TraverseDisposeAndClear,
26 | GetCamera,
27 | GetCameraFov,
28 | GetCameraPosition,
29 | GetViewProjectionMatrixArray,
30 | GetViewProjectionMatrix,
31 | CommonUtilsDispose,
32 | GetSplatMesh,
33 | GetCameraLookAt,
34 | GetCameraDirection,
35 | } from '../../events/EventConstants';
36 | import { setupSplatTextureManager } from '../../modeldata/SplatTexdataManager';
37 | import { SplatMeshOptions } from './SplatMeshOptions';
38 | import { ModelOptions } from '../../modeldata/ModelOptions';
39 | import { setupSplatMesh } from './SetupSplatMesh';
40 | import { setupGaussianText } from '../../modeldata/text/SetupGaussianText';
41 | import { setupApi } from '../../api/SetupApi';
42 | import { initSplatMeshOptions } from '../../utils/ViewerUtils';
43 | import { setupCommonUtils } from '../../utils/CommonUtils';
44 | import { MetaData } from '../../modeldata/ModelData';
45 | import { setupSorter } from '../../sorter/SetupSorter';
46 |
47 | /**
48 | * Gaussian splatting mesh
49 | */
50 | export class SplatMesh extends Mesh {
51 | public readonly isSplatMesh: boolean = true;
52 | public meta: MetaData;
53 | private disposed: boolean = false;
54 | private events: Events;
55 | private opts: SplatMeshOptions;
56 |
57 | /**
58 | * 构造函数
59 | * @param options 渲染器、场景、相机都应该传入
60 | */
61 | constructor(options: SplatMeshOptions) {
62 | super();
63 |
64 | const events = new Events();
65 | const on = (key: number, fn?: Function, multiFn?: boolean): Function | Function[] => events.on(key, fn, multiFn);
66 | const fire = (key: number, ...args: any): any => events.fire(key, ...args);
67 |
68 | // 默认参数校验设定
69 | const opts: SplatMeshOptions = initSplatMeshOptions(options);
70 |
71 | const camera = opts.controls.object as PerspectiveCamera;
72 | on(GetOptions, () => opts);
73 | on(GetCanvas, () => opts.renderer.domElement);
74 | on(GetCamera, () => camera);
75 | on(GetCameraFov, () => camera.fov);
76 | on(GetCameraPosition, (copy: boolean = false) => (copy ? camera.position.clone() : camera.position));
77 | on(GetCameraLookAt, (copy: boolean = false) => (copy ? opts.controls.target.clone() : opts.controls.target));
78 | on(GetViewProjectionMatrixArray, () => camera.projectionMatrix.clone().multiply(camera.matrixWorldInverse).multiply(this.matrix).toArray());
79 | on(GetViewProjectionMatrix, () => camera.projectionMatrix.clone().multiply(camera.matrixWorldInverse));
80 | on(GetCameraDirection, () => camera.getWorldDirection(new Vector3()).toArray());
81 | on(GetRenderer, () => opts.renderer);
82 | on(GetScene, () => opts.scene);
83 | on(IsBigSceneMode, () => opts.bigSceneMode);
84 | on(IsPointcloudMode, () => opts.pointcloudMode);
85 | on(GetSplatMesh, () => this);
86 |
87 | on(NotifyViewerNeedUpdate, () => opts.viewerEvents?.fire(ViewerNeedUpdate));
88 |
89 | setupCommonUtils(events);
90 | setupApi(events);
91 | setupSplatTextureManager(events);
92 | setupSorter(events);
93 | setupSplatMesh(events);
94 | setupGaussianText(events);
95 |
96 | this.name = `${opts.name || this.id}`;
97 |
98 | this.events = events;
99 | this.opts = opts;
100 |
101 | (async () => {
102 | this.copy(await events.fire(CreateSplatMesh));
103 | opts.matrix && this.applyMatrix4(opts.matrix);
104 | this.frustumCulled = false;
105 | this.onBeforeRender = () => {
106 | fire(WorkerSort);
107 | fire(SplatUpdatePerformanceNow, performance.now());
108 | };
109 | this.onAfterRender = () => {
110 | fire(SplatTexdataManagerDataChanged, 10000) && fire(NotifyViewerNeedUpdate); // 纹理数据更新后10秒内总是要刷新
111 | };
112 | })();
113 | }
114 |
115 | /**
116 | * 设定或者获取最新配置项
117 | * @param opts 配置项
118 | * @returns 最新配置项
119 | */
120 | public options(opts?: SplatMeshOptions): SplatMeshOptions {
121 | if (this.disposed) return;
122 | const fire = (key: number, ...args: any): any => this.events.fire(key, ...args);
123 | const thisOpts = this.opts;
124 |
125 | if (opts) {
126 | opts.pointcloudMode !== undefined && fire(SplatUpdatePointMode, opts.pointcloudMode);
127 | opts.lightFactor !== undefined && fire(SplatUpdateLightFactor, opts.lightFactor);
128 | opts.maxRenderCountOfMobile !== undefined && (thisOpts.maxRenderCountOfMobile = opts.maxRenderCountOfMobile);
129 | opts.maxRenderCountOfPc !== undefined && (thisOpts.maxRenderCountOfPc = opts.maxRenderCountOfPc);
130 |
131 | fire(NotifyViewerNeedUpdate);
132 | }
133 | return { ...thisOpts };
134 | }
135 |
136 | /**
137 | * 添加渲染指定高斯模型
138 | * @param opts 高斯模型选项
139 | * @param meta 元数据
140 | */
141 | public async addModel(opts: ModelOptions, meta: MetaData = {}): Promise {
142 | if (this.disposed) return;
143 | this.meta = meta;
144 | await this.events.fire(SplatTexdataManagerAddModel, opts, meta);
145 | }
146 |
147 | public fire(key: number, ...args: any): any {
148 | if (this.disposed) return;
149 | return this.events.fire(key, ...args);
150 | }
151 |
152 | /**
153 | * 销毁
154 | */
155 | public dispose(): void {
156 | if (this.disposed) return;
157 | this.disposed = true;
158 | const fire = (key: number, ...args: any): any => this.events.fire(key, ...args);
159 |
160 | fire(TraverseDisposeAndClear, this);
161 | fire(GetScene).remove(this);
162 |
163 | fire(CommonUtilsDispose);
164 | fire(SplatTexdataManagerDispose);
165 | fire(WorkerDispose);
166 | fire(SplatMeshDispose);
167 |
168 | this.events.clear();
169 | this.events = null;
170 | this.opts = null;
171 | this.onAfterRender = null;
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/src/reall3d/meshs/splatmesh/SplatMeshOptions.ts:
--------------------------------------------------------------------------------
1 | // ==============================================
2 | // Copyright (c) 2025 reall3d.com, MIT license
3 | // ==============================================
4 | import { Matrix4, Renderer, Scene } from 'three';
5 | import { Events } from '../../events/Events';
6 | import { OrbitControls } from 'three/examples/jsm/Addons.js';
7 |
8 | /**
9 | * 高斯网格配置项
10 | */
11 | export interface SplatMeshOptions {
12 | /**
13 | * 名称
14 | */
15 | name?: string;
16 |
17 | /**
18 | * 指定渲染器对象传入使用
19 | */
20 | renderer: Renderer;
21 |
22 | /**
23 | * 指定场景对象传入使用
24 | */
25 | scene: Scene;
26 |
27 | /**
28 | * 控制器
29 | */
30 | controls?: OrbitControls;
31 |
32 | /**
33 | * 模型矩阵
34 | */
35 | matrix?: Matrix4;
36 |
37 | /**
38 | * 渲染器事件管理器
39 | */
40 | viewerEvents?: Events;
41 |
42 | /**
43 | * 是否调试模式,生产环境默认false
44 | */
45 | debugMode?: boolean;
46 |
47 | /**
48 | * 是否大场景模式,初始化后不可修改
49 | */
50 | bigSceneMode?: boolean;
51 |
52 | /**
53 | * 是否点云模式渲染,默认为true
54 | * 支持通过viewer.options()动态更新
55 | */
56 | pointcloudMode?: boolean;
57 |
58 | /**
59 | * 移动端可渲染的高斯点数量限制
60 | * 支持通过viewer.options()动态更新
61 | */
62 | maxRenderCountOfMobile?: number;
63 |
64 | /**
65 | * PC端可渲染的高斯点数量限制
66 | * 支持通过viewer.options()动态更新
67 | */
68 | maxRenderCountOfPc?: number;
69 |
70 | /**
71 | * 颜色亮度系数,默认1.0
72 | */
73 | lightFactor?: number;
74 |
75 | /**
76 | * 是否显示水印,默认true
77 | */
78 | showWatermark?: boolean;
79 |
80 | /**
81 | * 球谐系数的渲染级别,默认0
82 | */
83 | shDegree?: number;
84 |
85 | /**
86 | * 是否开启深度测试,默认true
87 | */
88 | depthTest?: boolean;
89 | }
90 |
--------------------------------------------------------------------------------
/src/reall3d/modeldata/ModelData.ts:
--------------------------------------------------------------------------------
1 | // ==============================================
2 | // Copyright (c) 2025 reall3d.com, MIT license
3 | // ==============================================
4 | import { Vector3 } from 'three';
5 | import { CameraInfo } from '../controls/SetupCameraControls';
6 | import { ModelOptions } from './ModelOptions';
7 |
8 | /**
9 | * spx文件头信息
10 | */
11 | export class SpxHeader {
12 | public Fixed: string;
13 | public Version: number;
14 | public SplatCount: number;
15 | public MinX: number;
16 | public MaxX: number;
17 | public MinY: number;
18 | public MaxY: number;
19 | public MinZ: number;
20 | public MaxZ: number;
21 | public MinTopY: number;
22 | public MaxTopY: number;
23 | public CreateDate: number;
24 | public CreaterId: number;
25 | public ExclusiveId: number;
26 | public ShDegree: number;
27 | public Flag1: number;
28 | public Flag2: number;
29 | public Flag3: number;
30 | public Reserve1: number;
31 | public Reserve2: number;
32 | public Comment: string;
33 |
34 | public HashCheck: boolean;
35 | }
36 |
37 | /**
38 | * Splat模型
39 | */
40 | export class SplatModel {
41 | /** 模型选项 */
42 | public readonly opts: ModelOptions;
43 |
44 | /** 模型文件大小 */
45 | public fileSize: number = 0;
46 | /** 模型已下载大小 */
47 | public downloadSize: number = 0;
48 |
49 | /** 模型状态 */
50 | public status: ModelStatus = ModelStatus.FetchReady;
51 |
52 | /** 模型数据 */
53 | public splatData: Uint8Array = null;
54 | /** 模型水印数据 */
55 | public watermarkData: Uint8Array = null;
56 | /** 模型数据数量 */
57 | public dataSplatCount: number = 0;
58 | /** 模型水印数量 */
59 | public watermarkCount: number = 0;
60 |
61 | /** 球谐系数(1级,或1级和2级) */
62 | public sh12Data: Uint8Array[] = [];
63 | /** 球谐系数(仅3级) */
64 | public sh3Data: Uint8Array[] = [];
65 | /** 已下载的球谐系数(1级,或1级和2级)数量 */
66 | public sh12Count: number = 0;
67 | /** 已下载的球谐系数(仅3级)数量 */
68 | public sh3Count: number = 0;
69 |
70 | /** 一个高斯点数据长度 */
71 | public rowLength: number = 0;
72 | /** 模型的高斯数量 */
73 | public modelSplatCount: number = -1;
74 | /** 已下载的高斯数量 */
75 | public downloadSplatCount: number = 0;
76 | /** 待渲染的高斯数量(大场景时动态计算需要渲染的数量) */
77 | public renderSplatCount: number = 0;
78 |
79 | /** 中断控制器 */
80 | public abortController: AbortController;
81 |
82 | /** spx格式模型的头信息 */
83 | public header: SpxHeader = null;
84 |
85 | public dataShDegree: number = 0;
86 |
87 | public meta: MetaData;
88 | public map: Map;
89 |
90 | public minX: number = Infinity;
91 | public maxX: number = -Infinity;
92 | public minY: number = Infinity;
93 | public maxY: number = -Infinity;
94 | public minZ: number = Infinity;
95 | public maxZ: number = -Infinity;
96 | public topY: number = 0;
97 | public currentRadius: number = 0;
98 | public aabbCenter: Vector3;
99 |
100 | public notifyFetchStopDone: boolean;
101 | public smallSceneUploadDone: boolean;
102 | public textWatermarkVersion: number = 0;
103 | public lastTextWatermarkVersion: number = 0;
104 |
105 | public fetchLimit: number = 0;
106 |
107 | public activePoints: any;
108 |
109 | constructor(opts: ModelOptions, meta: MetaData = {}) {
110 | this.opts = { ...opts };
111 |
112 | this.meta = meta;
113 | meta.autoCut && (this.map = new Map());
114 |
115 | if (!opts.format) {
116 | if (opts.url?.endsWith('.spx')) {
117 | this.opts.format = 'spx';
118 | } else if (opts.url?.endsWith('.splat')) {
119 | this.opts.format = 'splat';
120 | } else if (opts.url?.endsWith('.ply')) {
121 | this.opts.format = 'ply';
122 | } else if (opts.url?.endsWith('.spz')) {
123 | this.opts.format = 'spz';
124 | } else {
125 | console.error('unknow format!');
126 | }
127 | }
128 | this.abortController = new AbortController();
129 | }
130 | }
131 |
132 | /**
133 | * 大场景用切割的数据块
134 | */
135 | export interface CutData {
136 | /** 块中数据的高斯点数 */
137 | splatCount?: number;
138 | /** 块中数据 */
139 | splatData?: Uint8Array;
140 |
141 | // 块的包围盒
142 | minX?: number;
143 | maxX?: number;
144 | minY?: number;
145 | maxY?: number;
146 | minZ?: number;
147 | maxZ?: number;
148 | // 块的包围球
149 | centerX?: number;
150 | centerY?: number;
151 | centerZ?: number;
152 | radius?: number;
153 |
154 | /** 当前待渲染点数(动态计算使用) */
155 | currentRenderCnt?: number;
156 | /** 离相机距离(动态计算使用) */
157 | distance?: number;
158 | }
159 |
160 | /**
161 | * 模型状态
162 | */
163 | export enum ModelStatus {
164 | /** 就绪 */
165 | FetchReady = 0,
166 | /** 请求中 */
167 | Fetching,
168 | /** 正常完成 */
169 | FetchDone,
170 | /** 请求途中被中断 */
171 | FetchAborted,
172 | /** 请求失败 */
173 | FetchFailed,
174 | /** 无效的模型格式或数据 */
175 | Invalid,
176 | }
177 |
178 | /**
179 | * 元数据
180 | */
181 | export interface MetaData {
182 | /** 名称 */
183 | name?: string;
184 | /** 版本 */
185 | version?: string;
186 | /** 更新日期(YYYYMMDD) */
187 | updateDate?: number;
188 |
189 | /** 是否自动旋转 */
190 | autoRotate?: boolean;
191 | /** 是否调试模式 */
192 | debugMode?: boolean;
193 | /** 是否点云模式 */
194 | pointcloudMode?: boolean;
195 | /** 移动端最大渲染数量 */
196 | maxRenderCountOfMobile?: number;
197 | /** PC端最大渲染数量 */
198 | maxRenderCountOfPc?: number;
199 | /** 移动端最大下载数量 */
200 | mobileDownloadLimitSplatCount?: number;
201 | /** PC端最大下载数量 */
202 | pcDownloadLimitSplatCount?: number;
203 |
204 | /** 米比例尺 */
205 | meterScale?: number;
206 | /** 文字水印 */
207 | watermark?: string;
208 | /** 是否显示水印 */
209 | showWatermark?: boolean;
210 | /** 相机参数 */
211 | cameraInfo?: CameraInfo;
212 | /** 标注 */
213 | marks?: any[];
214 | /** 飞翔相机位置点 */
215 | flyPositions?: number[];
216 | /** 飞翔相机注视点 */
217 | flyTargets?: number[];
218 |
219 | /** 自动切割数量 */
220 | autoCut?: number;
221 | /** 变换矩阵 */
222 | transform?: number[];
223 | /** 定位坐标(EPSG:4326 WGS 84) */
224 | WGS84?: number[];
225 | /** 模型地址 */
226 | url?: string;
227 | }
228 |
--------------------------------------------------------------------------------
/src/reall3d/modeldata/ModelOptions.ts:
--------------------------------------------------------------------------------
1 | // ==============================================
2 | // Copyright (c) 2025 reall3d.com, MIT license
3 | // ==============================================
4 | /**
5 | * 高斯模型选项
6 | */
7 | export interface ModelOptions {
8 | /**
9 | * 模型地址
10 | */
11 | url: string;
12 |
13 | /**
14 | * 模型格式(ply | splat | spx | spz),默认自动识别
15 | */
16 | format?: 'ply' | 'splat' | 'spx' | 'spz';
17 |
18 | /**
19 | * 是否重新下载
20 | */
21 | fetchReload?: boolean;
22 | }
23 |
--------------------------------------------------------------------------------
/src/reall3d/modeldata/SplatTexdata.ts:
--------------------------------------------------------------------------------
1 | // ==============================================
2 | // Copyright (c) 2025 reall3d.com, MIT license
3 | // ==============================================
4 | /**
5 | * 纹理
6 | */
7 | interface SplatTexdata {
8 | /** 索引(0 | 1) */
9 | index: number;
10 | /** 纹理版本(毫秒时间戳) */
11 | version?: number;
12 | /** 纹理数据 */
13 | txdata?: Uint32Array;
14 | /** 坐标数据 */
15 | xyz?: Float32Array;
16 | /** 纹理数据就绪标志 */
17 | textureReady?: boolean;
18 | /** 纹理数据就绪时间点 */
19 | textureReadyTime?: number;
20 |
21 | /** 包围盒极限点 */
22 | minX?: number;
23 | /** 包围盒极限点 */
24 | maxX?: number;
25 | /** 包围盒极限点 */
26 | minY?: number;
27 | /** 包围盒极限点 */
28 | maxY?: number;
29 | /** 包围盒极限点 */
30 | minZ?: number;
31 | /** 包围盒极限点 */
32 | maxZ?: number;
33 |
34 | /** 待渲染的Splat数量 */
35 | renderSplatCount?: number;
36 | /** 可见且可用的Splat数量 */
37 | visibleSplatCount?: number;
38 | /** 所有处理中的模型Splat数量合计 */
39 | modelSplatCount?: number;
40 | /** 模型数据中的水印数量 */
41 | watermarkCount?: number;
42 | }
43 |
--------------------------------------------------------------------------------
/src/reall3d/modeldata/loaders/SplatLoader.ts:
--------------------------------------------------------------------------------
1 | // ==============================================
2 | // Copyright (c) 2025 reall3d.com, MIT license
3 | // ==============================================
4 | import { Vector3 } from 'three';
5 | import { isMobile, SplatDataSize32 } from '../../utils/consts/GlobalConstants';
6 | import { ModelStatus, SplatModel } from '../ModelData';
7 | import { parseSplatToTexdata } from '../wasm/WasmParser';
8 |
9 | const maxProcessCnt = isMobile ? 20480 : 51200;
10 |
11 | export async function loadSplat(model: SplatModel) {
12 | let bytesRead = 0;
13 |
14 | try {
15 | model.status = ModelStatus.Fetching;
16 | const signal: AbortSignal = model.abortController.signal;
17 | const cache = model.opts.fetchReload ? 'reload' : 'default';
18 | const req = await fetch(model.opts.url, { mode: 'cors', credentials: 'omit', cache, signal });
19 | if (req.status != 200) {
20 | console.warn(`fetch error: ${req.status}`);
21 | model.status === ModelStatus.Fetching && (model.status = ModelStatus.FetchFailed);
22 | return;
23 | }
24 | const reader = req.body.getReader();
25 | const contentLength = parseInt(req.headers.get('content-length') || '0');
26 | model.rowLength = 32;
27 | model.fileSize = contentLength;
28 | const maxVertexCount = (contentLength / model.rowLength) | 0;
29 | if (maxVertexCount < 1) {
30 | console.warn('data empty', model.opts.url);
31 | model.status === ModelStatus.Fetching && (model.status = ModelStatus.Invalid);
32 | return;
33 | }
34 |
35 | model.modelSplatCount = maxVertexCount;
36 | model.downloadSplatCount = 0;
37 | model.splatData = new Uint8Array(Math.min(model.modelSplatCount, model.fetchLimit) * 32);
38 | model.watermarkData = new Uint8Array(0);
39 |
40 | let perValue = new Uint8Array(32);
41 | let perByteLen: number = 0;
42 |
43 | while (true) {
44 | let { done, value } = await reader.read();
45 | if (done) break;
46 |
47 | if (perByteLen + value.byteLength < model.rowLength) {
48 | // 不足1点不必解析
49 | perValue.set(value, perByteLen);
50 | perByteLen += value.byteLength;
51 |
52 | bytesRead += value.length;
53 | model.downloadSize = bytesRead;
54 | } else {
55 | // 解析并设定数据
56 | perByteLen = await parseSplatAndSetSplatData(model, perByteLen, perValue, value);
57 | perByteLen && perValue.set(value.slice(value.byteLength - perByteLen), 0);
58 | }
59 |
60 | // 超过限制时终止下载
61 | model.downloadSplatCount >= model.fetchLimit && model.abortController.abort();
62 | }
63 | } catch (e) {
64 | if (e.name === 'AbortError') {
65 | console.log('Fetch Abort', model.opts.url);
66 | model.status === ModelStatus.Fetching && (model.status = ModelStatus.FetchAborted);
67 | } else {
68 | console.error(e);
69 | model.status === ModelStatus.Fetching && (model.status = ModelStatus.FetchFailed);
70 | }
71 | } finally {
72 | model.status === ModelStatus.Fetching && (model.status = ModelStatus.FetchDone);
73 | }
74 |
75 | async function parseSplatAndSetSplatData(model: SplatModel, perByteLen: number, perValue: Uint8Array, newValue: Uint8Array): Promise {
76 | return new Promise(async resolve => {
77 | let cntSplat = ((perByteLen + newValue.byteLength) / model.rowLength) | 0;
78 | let leave: number = (perByteLen + newValue.byteLength) % model.rowLength;
79 | let value: Uint8Array;
80 | if (perByteLen) {
81 | value = new Uint8Array(cntSplat * model.rowLength);
82 | value.set(perValue.slice(0, perByteLen), 0);
83 | value.set(newValue.slice(0, newValue.byteLength - leave), perByteLen);
84 | } else {
85 | value = newValue.slice(0, cntSplat * model.rowLength);
86 | }
87 |
88 | // 丢弃超出限制范围外的数据
89 | if (model.downloadSplatCount + cntSplat > model.fetchLimit) {
90 | cntSplat = model.fetchLimit - model.downloadSplatCount;
91 | leave = 0;
92 | }
93 |
94 | const fnParseSplat = async () => {
95 | if (cntSplat > maxProcessCnt) {
96 | const data: Uint8Array = await parseSplatToTexdata(value, maxProcessCnt);
97 | setSplatData(model, data);
98 | model.downloadSplatCount += maxProcessCnt;
99 | bytesRead += maxProcessCnt * model.rowLength;
100 | model.downloadSize = bytesRead;
101 |
102 | cntSplat -= maxProcessCnt;
103 | value = value.slice(maxProcessCnt * model.rowLength);
104 | setTimeout(fnParseSplat);
105 | } else {
106 | const data: Uint8Array = await parseSplatToTexdata(value, cntSplat);
107 | setSplatData(model, data);
108 | model.downloadSplatCount += cntSplat;
109 | bytesRead += cntSplat * model.rowLength;
110 | model.downloadSize = bytesRead;
111 |
112 | resolve(leave);
113 | }
114 | };
115 |
116 | await fnParseSplat();
117 | });
118 | }
119 | }
120 |
121 | function setSplatData(model: SplatModel, data: Uint8Array) {
122 | let dataCnt = data.byteLength / SplatDataSize32;
123 | const maxSplatDataCnt = Math.min(model.fetchLimit, model.modelSplatCount);
124 | if (model.dataSplatCount + dataCnt > maxSplatDataCnt) {
125 | dataCnt = maxSplatDataCnt - model.dataSplatCount; // 丢弃超出限制的部分
126 | model.splatData.set(data.slice(0, dataCnt * SplatDataSize32), model.dataSplatCount * SplatDataSize32);
127 | } else {
128 | model.splatData.set(data, model.dataSplatCount * SplatDataSize32);
129 | }
130 |
131 | const f32s: Float32Array = new Float32Array(data.buffer);
132 | for (let i = 0, x = 0, y = 0, z = 0; i < dataCnt; i++) {
133 | x = f32s[i * 8];
134 | y = f32s[i * 8 + 1];
135 | z = f32s[i * 8 + 2];
136 | model.minX = Math.min(model.minX, x);
137 | model.maxX = Math.max(model.maxX, x);
138 | model.minY = Math.min(model.minY, y);
139 | model.maxY = Math.max(model.maxY, y);
140 | model.minZ = Math.min(model.minZ, z);
141 | model.maxZ = Math.max(model.maxZ, z);
142 | }
143 | model.dataSplatCount += dataCnt;
144 |
145 | const topY = model.header?.MinTopY || 0;
146 | model.currentRadius = Math.sqrt(model.maxX * model.maxX + topY * topY + model.maxZ * model.maxZ);
147 | model.aabbCenter = new Vector3((model.minX + model.maxX) / 2, (model.minY + model.maxY) / 2, (model.minZ + model.maxZ) / 2);
148 | }
149 |
--------------------------------------------------------------------------------
/src/reall3d/modeldata/text/SetupGaussianText.ts:
--------------------------------------------------------------------------------
1 | // ==============================================
2 | // Copyright (c) 2025 reall3d.com, MIT license
3 | // ==============================================
4 | import { GetGaussianText, HttpQueryGaussianText } from '../../events/EventConstants';
5 | import { Events } from '../../events/Events';
6 | import { HalfChars, SplatDataSize32 } from '../../utils/consts/GlobalConstants';
7 | import { parseWordToTexdata } from '../wasm/WasmParser';
8 |
9 | export function setupGaussianText(events: Events) {
10 | const fire = (key: number, ...args: any): any => events.fire(key, ...args);
11 | const on = (key: number, fn?: Function, multiFn?: boolean): Function | Function[] => events.on(key, fn, multiFn);
12 | const halfSet = new Set(HalfChars.split(''));
13 |
14 | on(GetGaussianText, async (text: string = '', isY: boolean = true, isNgativeY: boolean = true): Promise => {
15 | const words = text.trim().substring(0, 100);
16 | let dataJson: number[][] = await fire(HttpQueryGaussianText, words); // 限制最多100字
17 |
18 | let wordsJson: number[][][] = [];
19 | for (let i = 0; i < dataJson.length; i++) {
20 | let wnums: number[][] = [];
21 | let nums: number[] = dataJson[i];
22 | for (let j = 0; j < nums.length; j++) {
23 | wnums.push([((nums[j] % 20) - 10) * 0.02, (((nums[j] / 20) | 0) - 10) * 0.02]);
24 | }
25 | wordsJson.push(wnums);
26 | }
27 |
28 | let wsize: number[] = [];
29 | let ary = words.split('');
30 | for (let i = 0; i < ary.length; i++) {
31 | wsize[i] = halfSet.has(ary[i]) ? 0.22 : 0.4;
32 | }
33 |
34 | let cnt = (ary.length / 2) | 0;
35 | let offset = wsize[cnt] / 2;
36 | let isEven = !(ary.length % 2); // 是否偶数个
37 | let wOffset = isEven ? 0 : -offset;
38 | for (let i = cnt - 1; i >= 0; i--) {
39 | wOffset -= wsize[i] / 2;
40 | for (let nums of wordsJson[i]) nums[0] += wOffset;
41 | wOffset -= wsize[i] / 2;
42 | }
43 | offset = wsize[cnt] / 2;
44 | wOffset = isEven ? 0 : offset;
45 | for (let i = wordsJson.length - cnt; i < wordsJson.length; i++) {
46 | wOffset += wsize[i] / 2;
47 | for (let nums of wordsJson[i]) nums[0] += wOffset;
48 | wOffset += wsize[i] / 2;
49 | }
50 |
51 | let gsCount = 0;
52 | for (let wordJson of wordsJson) {
53 | gsCount += wordJson.length;
54 | }
55 |
56 | const data = new Uint8Array(gsCount * SplatDataSize32);
57 | let i = 0;
58 | for (let wordJson of wordsJson) {
59 | for (let nums of wordJson) {
60 | data.set(await parseWordToTexdata(nums[0], nums[1], isY, isNgativeY), SplatDataSize32 * i++);
61 | }
62 | }
63 | return data;
64 | });
65 | }
66 |
--------------------------------------------------------------------------------
/src/reall3d/modeldata/wasm/WasmParser.ts:
--------------------------------------------------------------------------------
1 | // ==============================================
2 | // Copyright (c) 2025 reall3d.com, MIT license
3 | // ==============================================
4 | import {
5 | SplatDataSize16,
6 | SplatDataSize32,
7 | SpxBlockFormatSH1,
8 | SpxBlockFormatSH2,
9 | SpxBlockFormatSH3,
10 | WasmBlockSize,
11 | } from '../../utils/consts/GlobalConstants';
12 | import { SpxHeader } from '../ModelData';
13 |
14 | const WasmBase64 =
15 | 'AGFzbQEAAAAADwhkeWxpbmsuMAEEAAAAAAEeBmABfQF9YAAAYAF9AX9gAX8Bf2ACf38AYAJ/fwF/AhoCA2VudgRleHBmAAADZW52Bm1lbW9yeQIAAAMGBQECAwQFByEEEV9fd2FzbV9jYWxsX2N0b3JzAAEBSAADAXcABAFEAAUKigsFAwABC3IBBH8gALwiBEH///8DcSEBAkAgBEEXdkH/AXEiAkUNACACQfAATQRAIAFBgICABHJB8QAgAmt2IQEMAQsgAkGNAUsEQEGA+AEhA0EAIQEMAQsgAkEKdEGAgAdrIQMLIAMgBEEQdkGAgAJxciABQQ12cgsyAQJ/QcGmFiEBA0AgACACai0AACABQSFscyEBIAJBAWoiAkH8AEcNAAsgASAAKAJ8RwszACAAQQhBBCABQQFGG2pBADYCACAAQQA2ABwgAP0MAAABAIwRFgCVAWwS7YEyE/0LAgwLqQkDKH8OfQR8IAEoAgRBi/AJRgR/IAEoAgAiBEEASgRAIAFBCWoiAiAEQRNsaiEIIAIgBEESbGohCSACIARBEWxqIQogAiAEQQR0aiELIAIgBEEPbGohDCACIARBDmxqIQ0gAiAEQQ1saiEOIAIgBEEMbGohDyACIARBC2xqIRAgAiAEQQpsaiERIAIgBEEJbGohEiACIARBBmxqIRMgAiAEQQNsaiEUIAEtAAghFUEAIQIDQCACIAlqLQAAIQcgAiAKai0AACEWIAIgC2otAAAhFyACIAxqLQAAIRggAiANai0AACEZIAIgDmotAAAhGiACIA9qLQAAIRsgAiAQai0AACEcIAIgEWotAAAhHSACIBJqLQAAIR4gEyACQQNsIgNqIgUtAAAhHyADIBRqIgYtAAAhICABIANqIgMtAAohISADLQAJISIgBS0AASEjIAYtAAEhJCADLQALIiXAISYgBS0AAiInwCEoIAYtAAIiBsAhKUEAIQUgFQRAIAggAkEBdGoiAy0AASIFQQh0QYD+AXEgAy0AAHIgBUEJdEGAgARxciEFCyAes0MAAIA9lEMAACDBkhAAIS8gHbNDAACAPZRDAAAgwZIQACExIByzQwAAgD2UQwAAIMGSEAAhMkMAAAAAQwAAgD8gB7NDAAAAw5JDAAAAPJQiKiAqlCAXs0MAAADDkkMAAAA8lCIrICuUIBazQwAAAMOSQwAAADyUIiwgLJSSkpMiLZEgLUMAAAAAXRshLSAAIAJBBXRqIgMgIUEIdCAiciAlQRB0ciIHQYCAgHhyIAcgJkEASBuyQwAAgDmUOAIAIAMgBTYCDCADICNBCHQgH3IgJ0EQdHIiBUGAgIB4ciAFIChBAEgbskMAAIA5lDgCCCADICRBCHQgIHIgBkEQdHIiBUGAgIB4ciAFIClBAEgbskMAAIA5lDgCBCADICsgKiAqlCAsICyUIC0gLZQgKyArlJKSkpEiLpUiKyAqIC6VIiqUIjQgLSAulSIwICwgLpUiLJQiNZK7IjggOKAgMrsiOKK2Ii0gLZREAAAAAAAA8D8gLCAslCIyICogKpQiNpK7IjogOqChIC+7IjqitiIuIC6UICsgLJQiMyAwICqUIjeTuyI7IDugIDG7IjuitiIvIC+UkpJDAACAQJQQAiAtICwgKpQiMSAwICuUIjCTuyI5IDmgIDiitiIqlCAuIDMgN5K7IjkgOaAgOqK2IiyUIC9EAAAAAAAA8D8gKyArlCIzIDaSuyI5IDmgoSA7orYiK5SSkkMAAIBAlBACQRB0cjYCECADIC1EAAAAAAAA8D8gMyAykrsiOSA5oKEgOKK2Ii2UIC4gNCA1k7siOCA4oCA6orYiLpQgLyAxIDCSuyI4IDigIDuitiIvlJKSQwAAgECUEAIgKiAqlCAsICyUICsgK5SSkkMAAIBAlBACQRB0cjYCFCADIBpBCHQgG3IgGUEQdHIgGEEYdHI2AhwgAyAqIC2UICwgLpQgKyAvlJKSQwAAgECUEAIgLSAtlCAuIC6UIC8gL5SSkkMAAIBAlBACQRB0cjYCGCACQQFqIgIgBEcNAAsLQQAFQQELCw==';
16 | const WasmOpenBase64 =
17 | 'AGFzbQEAAAAADwhkeWxpbmsuMAEEAAAAAAEpBmACf38Bf2ABfQF9YAAAYAF9AX9gAX8Bf2ANf399fX19fX19fX19fwACGgIDZW52BGV4cGYAAQNlbnYGbWVtb3J5AgAAAwcGAgMEBQAAByEEEV9fd2FzbV9jYWxsX2N0b3JzAAEBSAADAXMABQFEAAYKnhUGAwABC3IBBH8gALwiBEH///8DcSEBAkAgBEEXdkH/AXEiAkUNACACQfAATQRAIAFBgICABHJB8QAgAmt2IQEMAQsgAkGNAUsEQEGA+AEhA0EAIQEMAQsgAkEKdEGAgAdrIQMLIAMgBEEQdkGAgAJxciABQQ12cgsyAQJ/QZWjAyEBA0AgACACai0AACABQSFscyEBIAJBAWoiAkH8AEcNAAsgASAAKAJ8RwvpAwIEfAR9IAAgAUECdGoiACACOAIAIABBADYCDCAAIAQ4AgggACADOAIEIAAgCSALIAuUIAogCpQgCCAIlCAJIAmUkpKSkSIElSICIAsgBJUiA5QiCSAIIASVIgggCiAElSIElCIKkrsiDSANoCAHuyINorYiByAHlEQAAAAAAADwPyAEIASUIgsgAyADlCISkrsiDyAPoKEgBbsiD6K2IgUgBZQgAiAElCIRIAggA5QiE5O7IhAgEKAgBrsiEKK2IgYgBpSSkkMAAIBAlBACIAcgBCADlCIUIAggApQiCJO7Ig4gDqAgDaK2IgOUIAUgESATkrsiDiAOoCAPorYiBJQgBkQAAAAAAADwPyACIAKUIhEgEpK7Ig4gDqChIBCitiIClJKSQwAAgECUEAJBEHRyNgIQIAAgB0QAAAAAAADwPyARIAuSuyIOIA6goSANorYiB5QgBSAJIAqTuyINIA2gIA+itiIFlCAGIBQgCJK7Ig0gDaAgEKK2IgaUkpJDAACAQJQQAiADIAOUIAQgBJQgAiAClJKSQwAAgECUEAJBEHRyNgIUIAAgDDYCHCAAIAMgB5QgBCAFlCACIAaUkpJDAACAQJQQAiAHIAeUIAUgBZQgBiAGlJKSQwAAgECUEAJBEHRyNgIYC70BAQJ/IAFBAEoEQANAIAAgA0EDdCAAIANBBXRqIgIqAgAgAioCBCACKgIIIAIqAgwgAioCECACKgIUIAItABy4RAAAAAAAAGDAoEQAAAAAAACAP6K2IAItAB24RAAAAAAAAGDAoEQAAAAAAACAP6K2IAItAB64RAAAAAAAAGDAoEQAAAAAAACAP6K2IAItAB+4RAAAAAAAAGDAoEQAAAAAAACAP6K2IAIoAhgQBCADQQFqIgMgAUcNAAsLQQALxw4BHH8CfwJAAkACQAJAAkAgASgCBCICQQFrDgMBAgMAC0EBIAJBFEcNBBogASgCACIDQQBKBEAgAUEIaiICIANBE2xqIQUgAiADQRJsaiEGIAIgA0ERbGohCCACIANBBHRqIQkgAiADQQ9saiEKIAIgA0EObGohCyACIANBDWxqIQwgAiADQQxsaiENIAIgA0ELbGohDiACIANBCmxqIQ8gAiADQQlsaiEQIAIgA0EGbGohESACIANBA2xqIRJBACECA0AgAiAMai0AACETIAIgDWotAAAhFCACIAtqLQAAIRUgAiAKai0AACEWIAIgBWotAAAhFyACIAZqLQAAIRggAiAIai0AACEZIAIgCWotAAAhGyACIA5qLQAAIRwgAiAPai0AACEdIAAgAkEDdCABIAJBA2wiBGoiBy8ACCAHLAAKIgdB/wFxQRB0ciIaQYCAgHhyIBogB0EASBuyQwAAgDmUIAQgEmoiBy8AACAHLAACIgdB/wFxQRB0ciIaQYCAgHhyIBogB0EASBuyQwAAgDmUIAQgEWoiBC8AACAELAACIgRB/wFxQRB0ciIHQYCAgHhyIAcgBEEASBuyQwAAgDmUIAIgEGotAACzQwAAgD2UQwAAIMGSEAAgHbNDAACAPZRDAAAgwZIQACAcs0MAAIA9lEMAACDBkhAAIBu4RAAAAAAAAGDAoEQAAAAAAACAP6K2IBm4RAAAAAAAAGDAoEQAAAAAAACAP6K2IBi4RAAAAAAAAGDAoEQAAAAAAACAP6K2IBe4RAAAAAAAAGDAoEQAAAAAAACAP6K2IBQgE0EIdHIgFUEQdHIgFkEYdHIQBCACQQFqIgIgA0cNAAsLDAMLIAEoAgAiBUEASgRAA0AgASADQQlsaiICLQANIQYgAi0ADCEIIAItAAshCSACLQAKIQogAi0ACSELIAItAAghDCACLQAQIQ0gAi0ADyEOIAItAA4hAiAAIANBBHRqIgRCgICAgBA3AgggBCANQRB0QYCA4AdxIA5BFXRBgICA+AFxIAJBGnRBgICAgH5xcnI2AgQgBCAGQQF2QfwAcSAIQQR0QYAfcSAJQQl0QYDgB3EgCkEOdEGAgPgBcSALQRN0QYCAgD5xIAxBGHRBgICAQHFycnJyciACQQZ2cjYCACADQQFqIgMgBUcNAAsLDAILIAEoAgAiCEEASgRAA0AgASADQRhsaiICLQAaIQkgAi0AGSEKIAItABghCyACLQAXIQwgAi0AFiENIAItABUhDiACLQANIQ8gAi0ADCEQIAItAAshESACLQAKIRIgAi0ACSETIAItAAghFCACLQAUIQUgAi0AEyEVIAItABIhFiACLQARIRcgAi0AECEYIAItAA8hGSACLQAOIQYgACADQQR0aiIEIAItAB9BBXRBgD5xIAItAB5BCnRBgMAPcSACLQAdQQ90QYCA8ANxIAItABxBFHRBgICA/ABxIAItABsiAkEZdEGAgICAf3FycnJyQQFyNgIMIAQgFUEBdEHwA3EgFkEGdEGA/ABxIBdBC3RBgIAfcSAYQRB0QYCA4AdxIBlBFXRBgICA+AFxIAZBGnRBgICAgH5xcnJycnIgBUEEdnI2AgQgBCAPQQF2QfwAcSAQQQR0QYAfcSARQQl0QYDgB3EgEkEOdEGAgPgBcSATQRN0QYCAgD5xIBRBGHRBgICAQHFycnJyciAGQQZ2cjYCACAEIAlBAnZBPnEgCkEDdEHAD3EgC0EIdEGA8ANxIAxBDXRBgID8AHEgDUESdEGAgIAfcSAOQRd0QYCAgOAHcSAFQRx0QYCAgIB4cXJycnJyciACQQd2cjYCCCADQQFqIgMgCEcNAAsLDAELIAEoAgAiCEEASgRAA0AgASADQRVsaiICLQAaIQkgAi0AGSEKIAItABghCyACLQAXIQwgAi0AFiENIAItABUhDiACLQANIQ8gAi0ADCEQIAItAAshESACLQAKIRIgAi0ACSETIAItAAghFCACLQAUIQUgAi0AEyEVIAItABIhFiACLQARIRcgAi0AECEYIAItAA8hGSACLQAOIQYgACADQQR0aiIEIAItABxBFHRBgICA/ABxIAItABsiAkEZdEGAgICAf3FyQQFyNgIMIAQgFUEBdEHwA3EgFkEGdEGA/ABxIBdBC3RBgIAfcSAYQRB0QYCA4AdxIBlBFXRBgICA+AFxIAZBGnRBgICAgH5xcnJycnIgBUEEdnI2AgQgBCAPQQF2QfwAcSAQQQR0QYAfcSARQQl0QYDgB3EgEkEOdEGAgPgBcSATQRN0QYCAgD5xIBRBGHRBgICAQHFycnJyciAGQQZ2cjYCACAEIAlBAnZBPnEgCkEDdEHAD3EgC0EIdEGA8ANxIAxBDXRBgID8AHEgDUESdEGAgIAfcSAOQRd0QYCAgOAHcSAFQRx0QYCAgIB4cXJycnJyciACQQd2cjYCCCADQQFqIgMgCEcNAAsLC0EACws=';
18 |
19 | export async function parseSpxHeader(header: Uint8Array): Promise {
20 | const ui32s = new Uint32Array(header.buffer);
21 | const f32s = new Float32Array(header.buffer);
22 | const head = new SpxHeader();
23 | head.Fixed = String.fromCharCode(header[0]) + String.fromCharCode(header[1]) + String.fromCharCode(header[2]);
24 | head.Version = header[3];
25 | head.SplatCount = ui32s[1];
26 | head.MinX = f32s[2];
27 | head.MaxX = f32s[3];
28 | head.MinY = f32s[4];
29 | head.MaxY = f32s[5];
30 | head.MinZ = f32s[6];
31 | head.MaxZ = f32s[7];
32 | head.MinTopY = f32s[8];
33 | head.MaxTopY = f32s[9];
34 | head.CreateDate = ui32s[10];
35 | head.CreaterId = ui32s[11];
36 | head.ExclusiveId = ui32s[12];
37 | head.ShDegree = header[52];
38 | head.Flag1 = header[53];
39 | head.Flag2 = header[54];
40 | head.Flag3 = header[55];
41 | head.Reserve1 = ui32s[14];
42 | head.Reserve2 = ui32s[15];
43 |
44 | let comment: string = '';
45 | for (let i = 64; i < 124; i++) {
46 | comment += String.fromCharCode(header[i]);
47 | }
48 | head.Comment = comment.trim();
49 |
50 | head.HashCheck = true;
51 | if (head.Fixed !== 'spx' && head.Version !== 1) {
52 | return null;
53 | }
54 |
55 | // 哈希校验(检查模型是否由特定工具生成)
56 | const wasmBase64 = head.CreaterId == 1202056903 ? WasmOpenBase64 : WasmBase64;
57 | const wasmModule = WebAssembly.compile(Uint8Array.from(atob(wasmBase64), c => c.charCodeAt(0)).buffer);
58 | const memory = new WebAssembly.Memory({ initial: 1, maximum: 1 });
59 | const instance = await WebAssembly.instantiate(await wasmModule, { env: { memory, expf } });
60 | const headerParser: any = instance.exports.H;
61 |
62 | const wasmMemory = new Uint8Array(memory.buffer);
63 | wasmMemory.set(header, 0);
64 | const code: number = headerParser(0);
65 | if (code) {
66 | head.HashCheck = false;
67 | }
68 |
69 | return head;
70 | }
71 |
72 | interface SpxBlockResult {
73 | splatCount: number;
74 | blockFormat: number;
75 | datas?: Uint8Array;
76 | isSplat?: boolean;
77 | isSh?: boolean;
78 | isSh1?: boolean;
79 | isSh2?: boolean;
80 | isSh3?: boolean;
81 | success: boolean;
82 | }
83 |
84 | export async function parseSpxBlockData(data: Uint8Array): Promise {
85 | const ui32s = new Uint32Array(data.slice(0, 8).buffer);
86 | const splatCount = ui32s[0];
87 | const blockFormat = ui32s[1];
88 | const isSh1: boolean = SpxBlockFormatSH1 == blockFormat;
89 | const isSh2: boolean = SpxBlockFormatSH2 == blockFormat;
90 | const isSh3: boolean = SpxBlockFormatSH3 == blockFormat;
91 | const isSh: boolean = isSh1 || isSh2 || isSh3;
92 | const isSplat: boolean = !isSh;
93 |
94 | const wasmBase64 = blockFormat == 20 || isSh ? WasmOpenBase64 : WasmBase64;
95 | const resultByteLength = splatCount * (isSh ? SplatDataSize16 : SplatDataSize32);
96 | const wasmModule = WebAssembly.compile(Uint8Array.from(atob(wasmBase64), c => c.charCodeAt(0)).buffer);
97 | const blockCnt: number = Math.floor((resultByteLength + data.byteLength) / WasmBlockSize) + 2;
98 | const memory = new WebAssembly.Memory({ initial: blockCnt, maximum: blockCnt });
99 | const instance = await WebAssembly.instantiate(await wasmModule, { env: { memory, expf } });
100 | const dataParser: any = instance.exports.D;
101 |
102 | const wasmMemory = new Uint8Array(memory.buffer);
103 | wasmMemory.set(data, resultByteLength);
104 | const code = dataParser(0, resultByteLength);
105 | if (code) return { splatCount, blockFormat, success: false };
106 | return { splatCount, blockFormat, success: true, datas: wasmMemory.slice(0, resultByteLength), isSplat, isSh, isSh1, isSh2, isSh3 };
107 | }
108 |
109 | export async function parseSplatToTexdata(data: Uint8Array, splatCount: number): Promise {
110 | const wasmModule = WebAssembly.compile(Uint8Array.from(atob(WasmOpenBase64), c => c.charCodeAt(0)).buffer);
111 | const blockCnt = Math.floor((splatCount * SplatDataSize32) / WasmBlockSize) + 2;
112 | const memory = new WebAssembly.Memory({ initial: blockCnt, maximum: blockCnt });
113 | const instance = await WebAssembly.instantiate(await wasmModule, { env: { memory, expf } });
114 | const dataParser: any = instance.exports.s;
115 |
116 | const wasmMemory = new Uint8Array(memory.buffer);
117 | wasmMemory.set(data.slice(0, splatCount * SplatDataSize32), 0);
118 | const code: number = dataParser(0, splatCount);
119 | if (code) {
120 | console.error('splat data parser failed:', code);
121 | return new Uint8Array(0);
122 | }
123 |
124 | return wasmMemory.slice(0, splatCount * SplatDataSize32);
125 | }
126 |
127 | export async function parseWordToTexdata(x: number, y0z: number, isY: boolean = true, isNgativeY: boolean = true): Promise {
128 | const wasmModule = WebAssembly.compile(Uint8Array.from(atob(WasmBase64), c => c.charCodeAt(0)).buffer);
129 | const memory = new WebAssembly.Memory({ initial: 1, maximum: 1 });
130 | const instance = await WebAssembly.instantiate(await wasmModule, { env: { memory, expf } });
131 | const dataSplat: any = instance.exports.w;
132 |
133 | const wasmMemory = new Uint8Array(memory.buffer);
134 | const f32s = new Float32Array(wasmMemory.buffer);
135 | const ngativeY = isNgativeY ? -1 : 1;
136 | f32s[0] = x;
137 | isY ? (f32s[1] = ngativeY * y0z) : (f32s[2] = ngativeY * y0z);
138 | dataSplat(0, isY ? 1 : 0);
139 | return wasmMemory.slice(0, SplatDataSize32);
140 | }
141 |
142 | function expf(v: number) {
143 | return Math.exp(v);
144 | }
145 |
--------------------------------------------------------------------------------
/src/reall3d/pkg.ts:
--------------------------------------------------------------------------------
1 | // ==============================================
2 | // Copyright (c) 2025 reall3d.com, MIT license
3 | // ==============================================
4 | export { Reall3dViewer } from './viewer/Reall3dViewer';
5 | export { Reall3dMapViewer } from './mapviewer/Reall3dMapViewer';
6 | export { SplatMesh } from './meshs/splatmesh/SplatMesh';
7 |
--------------------------------------------------------------------------------
/src/reall3d/raycaster/SetupRaycaster.ts:
--------------------------------------------------------------------------------
1 | // ==============================================
2 | // Copyright (c) 2025 reall3d.com, MIT license
3 | // ==============================================
4 | import { Matrix4, Object3D, PerspectiveCamera, Raycaster, Scene, Sphere, Vector2, Vector3, Vector4 } from 'three';
5 | import { Events } from '../events/Events';
6 | import {
7 | GetCamera,
8 | GetCanvasSize,
9 | GetScene,
10 | GetSplatActivePoints,
11 | RaycasterRayDistanceToPoint,
12 | RaycasterRayIntersectPoints,
13 | } from '../events/EventConstants';
14 | import { SplatMesh } from '../meshs/splatmesh/SplatMesh';
15 |
16 | export function setupRaycaster(events: Events) {
17 | const raycaster: Raycaster = new Raycaster();
18 | const MinNdcDistance: number = 0.02;
19 |
20 | const on = (key: number, fn?: Function, multiFn?: boolean): Function | Function[] => events.on(key, fn, multiFn);
21 | const fire = (key: number, ...args: any): any => events.fire(key, ...args);
22 |
23 | on(RaycasterRayIntersectPoints, async (mouseClientX: number, mouseClientY: number): Promise => {
24 | const { width, height, left, top } = fire(GetCanvasSize);
25 | const mouse = new Vector2();
26 | mouse.x = ((mouseClientX - left) / width) * 2 - 1;
27 | mouse.y = ((top - mouseClientY) / height) * 2 + 1;
28 |
29 | const camera: PerspectiveCamera = fire(GetCamera);
30 | raycaster.setFromCamera(mouse, camera);
31 |
32 | const spheres: Sphere[] = [];
33 | const scene: Scene = fire(GetScene);
34 | const objectMeshs = [];
35 | const objectSplats: SplatMesh[] = [];
36 | scene.traverse(function (child: Object3D) {
37 | if (child instanceof SplatMesh) {
38 | objectSplats.push(child);
39 | } else {
40 | child['isMesh'] && !child['ignoreIntersect'] && !child['isMark'] && objectMeshs.push(child);
41 | }
42 | });
43 |
44 | const intersectMeshs = raycaster.intersectObjects(objectMeshs, true); // 常规mesh交点检测
45 | for (let i = 0; i < intersectMeshs.length; i++) {
46 | spheres.push(new Sphere(intersectMeshs[i].point, raycaster.ray.distanceToPoint(intersectMeshs[i].point)));
47 | }
48 |
49 | // console.time('raycaster');
50 | const viewProj: Matrix4 = camera.projectionMatrix.clone().multiply(camera.matrixWorldInverse);
51 | for (let i = 0; i < objectSplats.length; i++) {
52 | const rs: any = objectSplats[i].fire(GetSplatActivePoints);
53 | if (!rs) continue;
54 |
55 | if (rs.length !== undefined) {
56 | // 坐标数组计算
57 | const activePoints: Float32Array = rs;
58 | const cnt = activePoints.length / 3;
59 | for (let j = 0; j < cnt; j++) {
60 | const point: Vector3 = new Vector3(activePoints[3 * j + 0], activePoints[3 * j + 1], activePoints[3 * j + 2]);
61 | const projectedPoint = new Vector4(point.x, point.y, point.z, 1).applyMatrix4(viewProj);
62 | const ndcX = projectedPoint.x / projectedPoint.w;
63 | const ndcY = projectedPoint.y / projectedPoint.w;
64 | const ndcDistance = Math.sqrt((ndcX - mouse.x) ** 2 + (ndcY - mouse.y) ** 2);
65 | ndcDistance <= MinNdcDistance && spheres.push(new Sphere(point, raycaster.ray.distanceToPoint(point)));
66 | }
67 | } else {
68 | // 分块计算
69 | for (let key of Object.keys(rs)) {
70 | const xyzs: string[] = key.split(',');
71 | const center: Vector3 = new Vector3(Number(xyzs[0]), Number(xyzs[1]), Number(xyzs[2])); // 边长为2的立方体中心点
72 | if (raycaster.ray.distanceToPoint(center) <= 1.4143) {
73 | const points: number[] = rs[key];
74 | for (let j = 0, cnt = points.length / 3; j < cnt; j++) {
75 | const point: Vector3 = new Vector3(points[3 * j + 0], points[3 * j + 1], points[3 * j + 2]);
76 | const projectedPoint = new Vector4(point.x, point.y, point.z, 1).applyMatrix4(viewProj);
77 | const ndcX = projectedPoint.x / projectedPoint.w;
78 | const ndcY = projectedPoint.y / projectedPoint.w;
79 | const ndcDistance = Math.sqrt((ndcX - mouse.x) ** 2 + (ndcY - mouse.y) ** 2);
80 | ndcDistance <= MinNdcDistance && spheres.push(new Sphere(point, raycaster.ray.distanceToPoint(point)));
81 | }
82 | }
83 | }
84 | }
85 | }
86 | // console.timeEnd('raycaster');
87 |
88 | spheres.sort((a: Sphere, b: Sphere) => a.radius - b.radius);
89 |
90 | const rs: Vector3[] = [];
91 | for (let i = 0; i < spheres.length; i++) {
92 | rs.push(spheres[i].center);
93 | }
94 | return rs;
95 | });
96 |
97 | on(RaycasterRayDistanceToPoint, (mouseClientX: number, mouseClientY: number, point: Vector3): number => {
98 | const { width, height, left, top } = fire(GetCanvasSize);
99 | const mouse = new Vector2();
100 | mouse.x = ((mouseClientX - left) / width) * 2 - 1;
101 | mouse.y = ((top - mouseClientY) / height) * 2 + 1;
102 |
103 | const camera: PerspectiveCamera = fire(GetCamera);
104 | raycaster.setFromCamera(mouse, camera);
105 | return raycaster.ray.distanceToPoint(point);
106 | });
107 | }
108 |
--------------------------------------------------------------------------------
/src/reall3d/sorter/SetupSorter.ts:
--------------------------------------------------------------------------------
1 | // ==============================================
2 | // Copyright (c) 2025 reall3d.com, MIT license
3 | // ==============================================
4 | import { Events } from '../events/Events';
5 | import { GetWorker, WorkerSort, WorkerDispose, GetViewProjectionMatrixArray, GetMaxRenderCount, IsBigSceneMode } from '../events/EventConstants';
6 | import { WkInit, WkIsBigSceneMode, WkMaxRenderCount, WkViewProjection } from '../utils/consts/WkConstants';
7 |
8 | export function setupSorter(events: Events) {
9 | const on = (key: number, fn?: Function, multiFn?: boolean): Function | Function[] => events.on(key, fn, multiFn);
10 | const fire = (key: number, ...args: any): any => events.fire(key, ...args);
11 | const worker = new Worker(new URL('./Sorter.ts', import.meta.url), { type: 'module' });
12 |
13 | on(GetWorker, () => worker);
14 | on(WorkerSort, () => worker.postMessage({ [WkViewProjection]: fire(GetViewProjectionMatrixArray) }));
15 | on(WorkerDispose, () => worker.terminate());
16 |
17 | (async () => {
18 | worker.postMessage({ [WkInit]: true, [WkMaxRenderCount]: await fire(GetMaxRenderCount), [WkIsBigSceneMode]: fire(IsBigSceneMode) });
19 | })();
20 | }
21 |
--------------------------------------------------------------------------------
/src/reall3d/sorter/Sorter.ts:
--------------------------------------------------------------------------------
1 | // ==============================================
2 | // Copyright (c) 2025 reall3d.com, MIT license
3 | // ==============================================
4 | import { WkInit, WkWatermarkCount } from '../utils/consts/WkConstants';
5 | import {
6 | WkIndex,
7 | WkIsBigSceneMode,
8 | WkMaxRenderCount,
9 | WkModelSplatCount,
10 | WkRenderSplatCount,
11 | WkSortTime,
12 | WkSplatIndex,
13 | WkTextureReady,
14 | WkVersion,
15 | WkViewProjection,
16 | WkVisibleSplatCount,
17 | WkSortStartTime,
18 | WkMinX,
19 | WkMaxX,
20 | WkMinY,
21 | WkMaxY,
22 | WkMinZ,
23 | WkMaxZ,
24 | WkXyz,
25 | } from '../utils/consts/WkConstants';
26 | import { isMobile } from '../utils/consts/GlobalConstants';
27 |
28 | const worker: Worker = self as any;
29 | let texture0: SplatTexdata = { index: 0, version: 0 };
30 | let texture1: SplatTexdata = { index: 1, version: 0 };
31 | let isSorterReady: boolean = false;
32 |
33 | let sortRunning: boolean;
34 | const Epsilon: number = isMobile ? 0.2 : 0.2;
35 | let viewProj: number[];
36 | let lastViewProj: number[] = [];
37 | let distances: Int32Array;
38 |
39 | let lastSortVersion: number = 0;
40 | let isBigSceneMode: boolean;
41 |
42 | function runSort(sortViewProj: number[]) {
43 | if (!isSorterReady) return; // 尚未就绪
44 | let texture: SplatTexdata = texture0.version > texture1.version ? texture0 : texture1;
45 | if (!texture.version) return; // 初期还没有数据
46 |
47 | const { xyz, renderSplatCount, visibleSplatCount, modelSplatCount, watermarkCount, index, version } = texture;
48 |
49 | if (lastSortVersion === version) {
50 | let diff =
51 | Math.abs(lastViewProj[2] - sortViewProj[2]) +
52 | Math.abs(lastViewProj[6] - sortViewProj[6]) +
53 | Math.abs(lastViewProj[10] - sortViewProj[10]) +
54 | Math.abs(lastViewProj[14] - sortViewProj[14]);
55 | if (diff < Epsilon) {
56 | return;
57 | }
58 | }
59 | lastViewProj = sortViewProj;
60 | lastSortVersion = version;
61 |
62 | let startTime = Date.now();
63 | let depthIndex: Uint32Array;
64 | if (!renderSplatCount) {
65 | // 没有渲染数据时直接处理
66 | depthIndex = new Uint32Array(0);
67 | worker.postMessage(
68 | {
69 | [WkSplatIndex]: depthIndex,
70 | [WkRenderSplatCount]: renderSplatCount,
71 | [WkVisibleSplatCount]: visibleSplatCount,
72 | [WkModelSplatCount]: modelSplatCount,
73 | [WkIndex]: index,
74 | [WkVersion]: version,
75 | [WkSortTime]: 0,
76 | [WkSortStartTime]: startTime,
77 | },
78 | [depthIndex.buffer],
79 | );
80 | return;
81 | }
82 |
83 | // 排序
84 | let sortTime = 0;
85 | const dataCount = renderSplatCount - watermarkCount;
86 | depthIndex = new Uint32Array(renderSplatCount);
87 | const { maxDepth, minDepth } = getDepth(texture, viewProj);
88 | if (maxDepth - minDepth <= 0.00001) {
89 | for (let i = 0; i < renderSplatCount; i++) depthIndex[i] = i;
90 | } else {
91 | // 数据
92 | let bucketCnt: number = Math.min(dataCount, 65535);
93 | let depthInv: number = (bucketCnt - 1) / (maxDepth - minDepth);
94 | let counters: Int32Array = new Int32Array(bucketCnt);
95 | for (let i = 0, idx = 0; i < dataCount; i++) {
96 | idx = ((computeDepth(sortViewProj, xyz[3 * i], xyz[3 * i + 1], xyz[3 * i + 2]) - minDepth) * depthInv) | 0;
97 | counters[(distances[i] = idx)]++;
98 | }
99 | for (let i = 1; i < bucketCnt; i++) counters[i] += counters[i - 1];
100 | for (let i = 0; i < dataCount; i++) depthIndex[--counters[distances[i]]] = i;
101 |
102 | // 水印
103 | if (watermarkCount) {
104 | bucketCnt = Math.min(Math.max((watermarkCount / 8) | 0, 512), 65535);
105 | depthInv = (bucketCnt - 1) / (maxDepth - minDepth);
106 | counters = new Int32Array(bucketCnt);
107 | for (let i = dataCount, idx = 0; i < renderSplatCount; i++) {
108 | idx = ((computeDepth(sortViewProj, xyz[3 * i], xyz[3 * i + 1], xyz[3 * i + 2]) - minDepth) * depthInv) | 0;
109 | counters[(distances[i - dataCount] = idx)]++;
110 | }
111 | for (let i = 1; i < bucketCnt; i++) counters[i] += counters[i - 1];
112 | for (let i = 0; i < watermarkCount; i++) depthIndex[dataCount + --counters[distances[i]]] = dataCount + i;
113 | }
114 | }
115 |
116 | sortTime = Date.now() - startTime;
117 | worker.postMessage(
118 | {
119 | [WkSplatIndex]: depthIndex,
120 | [WkRenderSplatCount]: renderSplatCount,
121 | [WkVisibleSplatCount]: visibleSplatCount,
122 | [WkModelSplatCount]: modelSplatCount,
123 | [WkIndex]: index,
124 | [WkVersion]: version,
125 | [WkSortStartTime]: startTime,
126 | [WkSortTime]: sortTime,
127 | },
128 | [depthIndex.buffer],
129 | );
130 | }
131 |
132 | function computeDepth(svp: number[], x: number, y: number, z: number): number {
133 | // return (svp[2] * x + svp[6] * y + svp[10] * z) * -4096;
134 | return -(svp[2] * x + svp[6] * y + svp[10] * z);
135 | // return -(svp[2] * x + svp[6] * y + svp[10] * z + svp[14]);
136 | }
137 |
138 | function getDepth(texture: SplatTexdata, sortViewProj: number[]): any {
139 | let maxDepth = -Infinity;
140 | let minDepth = Infinity;
141 | let dep = 0;
142 | dep = computeDepth(sortViewProj, texture.minX, texture.minY, texture.minZ);
143 | maxDepth = Math.max(maxDepth, dep);
144 | minDepth = Math.min(minDepth, dep);
145 | dep = computeDepth(sortViewProj, texture.minX, texture.minY, texture.maxZ);
146 | maxDepth = Math.max(maxDepth, dep);
147 | minDepth = Math.min(minDepth, dep);
148 | dep = computeDepth(sortViewProj, texture.minX, texture.maxY, texture.minZ);
149 | maxDepth = Math.max(maxDepth, dep);
150 | minDepth = Math.min(minDepth, dep);
151 | dep = computeDepth(sortViewProj, texture.minX, texture.maxY, texture.maxZ);
152 | maxDepth = Math.max(maxDepth, dep);
153 | minDepth = Math.min(minDepth, dep);
154 | dep = computeDepth(sortViewProj, texture.maxX, texture.minY, texture.minZ);
155 | maxDepth = Math.max(maxDepth, dep);
156 | minDepth = Math.min(minDepth, dep);
157 | dep = computeDepth(sortViewProj, texture.maxX, texture.minY, texture.maxZ);
158 | maxDepth = Math.max(maxDepth, dep);
159 | minDepth = Math.min(minDepth, dep);
160 | dep = computeDepth(sortViewProj, texture.maxX, texture.maxY, texture.minZ);
161 | maxDepth = Math.max(maxDepth, dep);
162 | minDepth = Math.min(minDepth, dep);
163 | dep = computeDepth(sortViewProj, texture.maxX, texture.maxY, texture.maxZ);
164 | maxDepth = Math.max(maxDepth, dep);
165 | minDepth = Math.min(minDepth, dep);
166 | return { maxDepth, minDepth };
167 | }
168 |
169 | const throttledSort = () => {
170 | if (!sortRunning) {
171 | sortRunning = true;
172 | const sortViewProj = viewProj;
173 | runSort(sortViewProj);
174 | setTimeout(() => !(sortRunning = false) && sortViewProj !== viewProj && throttledSort());
175 | }
176 | };
177 |
178 | worker.onmessage = (e: any) => {
179 | const data: any = e.data;
180 | if (data[WkTextureReady]) {
181 | let texture = !isBigSceneMode || data[WkIndex] === 0 ? texture0 : texture1;
182 |
183 | texture.minX = data[WkMinX];
184 | texture.maxX = data[WkMaxX];
185 | texture.minY = data[WkMinY];
186 | texture.maxY = data[WkMaxY];
187 | texture.minZ = data[WkMinZ];
188 | texture.maxZ = data[WkMaxZ];
189 | texture.xyz = new Float32Array(data[WkXyz].buffer);
190 | texture.watermarkCount = data[WkWatermarkCount];
191 | texture.version = data[WkVersion];
192 | texture.renderSplatCount = data[WkRenderSplatCount];
193 | texture.visibleSplatCount = data[WkVisibleSplatCount];
194 | texture.modelSplatCount = data[WkModelSplatCount];
195 |
196 | texture.textureReady = true;
197 | texture.textureReadyTime = Date.now();
198 | } else if (data[WkViewProjection]) {
199 | viewProj = data[WkViewProjection];
200 | throttledSort();
201 | } else if (data[WkInit]) {
202 | isBigSceneMode = data[WkIsBigSceneMode];
203 | distances = new Int32Array(data[WkMaxRenderCount]);
204 | isSorterReady = true;
205 | }
206 | };
207 |
--------------------------------------------------------------------------------
/src/reall3d/style/style.less:
--------------------------------------------------------------------------------
1 | // ==============================================
2 | // Copyright (c) 2025 reall3d.com, MIT license
3 | // ==============================================
4 | body {
5 | overflow: hidden;
6 | margin: 0;
7 | height: 100vh;
8 | width: 100vw;
9 | font-family: PingFang SC, Microsoft YaHei, sans-serif;
10 | font-size: 14px;
11 | background: black;
12 | color: white;
13 | }
14 |
15 | #map {
16 | position: absolute;
17 | top: 0;
18 | left: 0;
19 | width: 100%;
20 | height: 100%;
21 | }
22 |
23 | .gsviewer-canvas {
24 | width: 100%;
25 | height: 100%;
26 | touch-action: none;
27 | }
28 |
29 | #gsviewer {
30 | position: absolute;
31 | top: 0;
32 | left: 0;
33 | width: 100%;
34 | height: 100%;
35 |
36 | .logo {
37 | z-index: 9999;
38 | width: 40px;
39 | height: 40px;
40 | animation-fill-mode: backwards;
41 | }
42 |
43 | .logo.loading {
44 | animation: spin infinite 2s linear;
45 | }
46 |
47 | @keyframes spin {
48 | from {
49 | transform: rotate(0deg);
50 | }
51 |
52 | to {
53 | transform: rotate(360deg);
54 | }
55 | }
56 |
57 | .hidden {
58 | display: none !important;
59 | }
60 |
61 | .info {
62 | z-index: 9999;
63 | position: absolute;
64 | bottom: 20px;
65 | right: 20px;
66 | color: #eee;
67 | background-color: #333;
68 | padding: 3px 10px;
69 | }
70 |
71 | .debug {
72 | z-index: 9999;
73 | position: absolute;
74 | bottom: 20px;
75 | left: 20px;
76 | color: #eee;
77 | background-color: #333;
78 | padding: 5px 10px;
79 | opacity: 0.85;
80 | pointer-events: none;
81 |
82 | table {
83 | width: 100%;
84 | font-size: 14px;
85 | }
86 |
87 | &.map .map-hidden {
88 | display: none;
89 | }
90 | }
91 |
92 | .operation {
93 | z-index: 9999;
94 | position: absolute;
95 | top: 30px;
96 | right: 20px;
97 | color: #eee;
98 | background-color: #333;
99 | padding: 5px 5px;
100 | opacity: 0.85;
101 |
102 | table {
103 | width: 100%;
104 | font-size: 14px;
105 |
106 | tr {
107 | margin-top: 10px;
108 | margin-bottom: 10px;
109 |
110 | span {
111 | cursor: pointer;
112 | padding-left: 2px;
113 | padding-right: 2px;
114 | user-select: none;
115 |
116 | &.disable {
117 | cursor: not-allowed;
118 | }
119 | }
120 |
121 | span:hover {
122 | background-color: #999;
123 | }
124 | }
125 |
126 | &.plus tr.tr-data {
127 | display: none;
128 | }
129 | }
130 | }
131 |
132 | // 画字
133 | .grid-wrapper-text {
134 | display: flex !important;
135 | flex-direction: column;
136 | justify-content: center;
137 | align-items: center;
138 | height: 100vh;
139 | }
140 |
141 | .grid-container {
142 | display: grid;
143 | grid-template-columns: repeat(20, 1fr);
144 | grid-template-rows: repeat(20, 1fr);
145 | gap: 1px;
146 | width: 400px;
147 | height: 400px;
148 | background-color: #aaa;
149 | position: relative;
150 | overflow: visible;
151 | user-select: none;
152 | }
153 |
154 | .grid-item {
155 | background-color: #000;
156 | transition: background-color 0.3s;
157 | user-select: none;
158 | }
159 |
160 | .yellow {
161 | background-color: #ff0;
162 | }
163 |
164 | .grid-line {
165 | background-color: #ddd;
166 | position: absolute;
167 | z-index: 5;
168 | }
169 |
170 | .grid-line.vertical {
171 | width: 100%;
172 | height: 1px;
173 | top: 50%;
174 | }
175 |
176 | .grid-line.horizontal {
177 | height: 100%;
178 | width: 1px;
179 | left: 50%;
180 | }
181 |
182 | .grid-line.left {
183 | height: 100%;
184 | width: 1px;
185 | left: 0%;
186 | background-color: #aaa;
187 | }
188 |
189 | .grid-line.top {
190 | width: 100%;
191 | height: 1px;
192 | top: 0%;
193 | background-color: #aaa;
194 | }
195 |
196 | .overlay-text {
197 | position: absolute;
198 | padding-top: 20px;
199 | top: 0px;
200 | left: 0;
201 | width: 400px;
202 | height: 400px;
203 | pointer-events: none;
204 | display: flex;
205 | justify-content: center;
206 | align-items: center;
207 | font-size: 380px;
208 | color: rgba(255, 255, 255, 0.3);
209 | z-index: 10;
210 | }
211 | }
212 |
--------------------------------------------------------------------------------
/src/reall3d/utils/ViewerUtils.ts:
--------------------------------------------------------------------------------
1 | // ==============================================
2 | // Copyright (c) 2025 reall3d.com, MIT license
3 | // ==============================================
4 | import { PerspectiveCamera, Vector3, WebGLRenderer } from 'three';
5 | import { Events } from '../events/Events';
6 | import {
7 | ComputeFps,
8 | GetFpsReal,
9 | CountFpsReal,
10 | Vector3ToString,
11 | CountFpsDefault,
12 | GetFpsDefault,
13 | Information,
14 | IsDebugMode,
15 | ViewerUtilsDispose,
16 | OnViewerBeforeUpdate,
17 | ControlsUpdate,
18 | OnViewerUpdate,
19 | ViewerDispose,
20 | GetCameraFov,
21 | GetCameraPosition,
22 | GetCameraLookAt,
23 | GetCameraLookUp,
24 | } from '../events/EventConstants';
25 | import { SplatMeshOptions } from '../meshs/splatmesh/SplatMeshOptions';
26 | import { Reall3dViewerOptions } from '../viewer/Reall3dViewerOptions';
27 |
28 | export function setupViewerUtils(events: Events) {
29 | let disposed: boolean = false;
30 | const on = (key: number, fn?: Function, multiFn?: boolean): Function | Function[] => events.on(key, fn, multiFn);
31 | const fire = (key: number, ...args: any): any => events.fire(key, ...args);
32 |
33 | on(ViewerUtilsDispose, () => (disposed = true));
34 |
35 | const fpsMap: Map = new Map();
36 | const fpsRealMap: Map = new Map();
37 | on(CountFpsDefault, () => fire(IsDebugMode) && fpsMap.set(Date.now(), 1));
38 | on(GetFpsDefault, () => fire(IsDebugMode) && fire(ComputeFps, fpsMap));
39 | on(CountFpsReal, () => fire(IsDebugMode) && fpsRealMap.set(Date.now(), 1));
40 | on(GetFpsReal, () => fire(IsDebugMode) && fire(ComputeFps, fpsRealMap));
41 | on(
42 | OnViewerUpdate,
43 | () => {
44 | if (disposed) return;
45 | fire(CountFpsReal);
46 | fire(IsDebugMode) &&
47 | fire(Information, {
48 | fov: fire(GetCameraFov),
49 | position: fire(Vector3ToString, fire(GetCameraPosition)),
50 | lookAt: fire(Vector3ToString, fire(GetCameraLookAt)),
51 | lookUp: fire(Vector3ToString, fire(GetCameraLookUp)),
52 | });
53 | },
54 | true,
55 | );
56 |
57 | let iRender: number = 0;
58 | on(
59 | OnViewerBeforeUpdate,
60 | () => {
61 | if (disposed) return;
62 | fire(ControlsUpdate);
63 | if (fire(IsDebugMode)) {
64 | fire(CountFpsDefault);
65 | !(iRender++ % 5) && fire(Information, { fps: fire(GetFpsDefault), realFps: fire(GetFpsReal) });
66 | }
67 | },
68 | true,
69 | );
70 |
71 | on(ComputeFps, (map: Map) => {
72 | let dels: number[] = [];
73 | let now: number = Date.now();
74 | let rs: number = 0;
75 | for (const key of map.keys()) {
76 | now - key <= 1000 ? rs++ : dels.push(key);
77 | }
78 | dels.forEach(key => map.delete(key));
79 | return Math.min(rs, 30);
80 | });
81 |
82 | window.addEventListener('beforeunload', () => fire(ViewerDispose));
83 | }
84 |
85 | export function initSplatMeshOptions(options: SplatMeshOptions): SplatMeshOptions {
86 | const opts: SplatMeshOptions = { ...options };
87 |
88 | // 默认参数校验设定
89 | opts.bigSceneMode ??= false;
90 | opts.pointcloudMode ??= !opts.bigSceneMode; // 小场景默认点云模式,大场景默认正常模式
91 | opts.lightFactor ??= 1.0;
92 | opts.name ??= '';
93 | opts.showWatermark ??= true;
94 | opts.shDegree ??= 0;
95 | opts.depthTest ??= true;
96 | opts.debugMode ??= false;
97 | opts.maxRenderCountOfMobile ??= opts.bigSceneMode ? 256 * 10000 : (256 + 128) * 10240;
98 | opts.maxRenderCountOfPc ??= opts.bigSceneMode ? (256 + 64) * 10000 : (256 + 128) * 10000;
99 |
100 | return opts;
101 | }
102 |
103 | export function initGsViewerOptions(options: Reall3dViewerOptions): Reall3dViewerOptions {
104 | const opts: Reall3dViewerOptions = { ...options };
105 |
106 | // 默认参数校验设定
107 | opts.position = opts.position ? [...opts.position] : [0, -5, 15];
108 | opts.lookAt = opts.lookAt ? [...opts.lookAt] : [0, 0, 0];
109 | opts.lookUp = opts.lookUp ? [...opts.lookUp] : [0, -1, 0];
110 | opts.fov ??= 45;
111 | opts.near ??= 0.001;
112 | opts.far ??= 1000;
113 | opts.enableDamping ??= true;
114 | opts.autoRotate ??= true;
115 | opts.enableZoom ??= true;
116 | opts.enableRotate ??= true;
117 | opts.enablePan ??= true;
118 | opts.enableKeyboard ??= true;
119 | opts.bigSceneMode ??= false;
120 | opts.pointcloudMode ??= !opts.bigSceneMode; // 小场景默认点云模式,大场景默认正常模式
121 | opts.lightFactor ??= 1.1;
122 | opts.debugMode ??= location.protocol === 'http:' || /^test\./.test(location.host); // 生产环境不开启
123 | opts.markMode ??= false;
124 | opts.markVisible ??= true;
125 | opts.meterScale ??= 1;
126 | opts.background ??= '#000000';
127 | opts.minDistance ??= 0.1;
128 | opts.maxDistance ??= 100;
129 |
130 | return opts;
131 | }
132 |
133 | export function initRenderer(opts: Reall3dViewerOptions): WebGLRenderer {
134 | let root: HTMLElement;
135 | if (opts.root) {
136 | root = typeof opts.root === 'string' ? document.querySelector(opts.root) || document.querySelector('#gsviewer') : opts.root;
137 | } else {
138 | root = document.querySelector('#gsviewer');
139 | }
140 | if (!root) {
141 | root = document.createElement('div');
142 | root.id = 'gsviewer';
143 | document.body.appendChild(root);
144 | }
145 |
146 | let renderer = null;
147 | if (!opts.renderer) {
148 | renderer = new WebGLRenderer({ antialias: false, stencil: true, logarithmicDepthBuffer: true, precision: 'highp' });
149 | renderer.setSize(root.clientWidth, root.clientHeight);
150 | renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
151 | opts.renderer = renderer;
152 | } else {
153 | renderer = opts.renderer;
154 | }
155 |
156 | const canvas = renderer.domElement;
157 | canvas.classList.add('gsviewer-canvas');
158 | root.appendChild(renderer.domElement);
159 | return renderer;
160 | }
161 |
162 | export function initCamera(opts: Reall3dViewerOptions): PerspectiveCamera {
163 | let camera = opts.camera;
164 | if (!camera) {
165 | const canvas: HTMLCanvasElement = opts.renderer.domElement;
166 | const aspect = canvas.width / canvas.height;
167 | let lookUp: Vector3 = new Vector3().fromArray(opts.lookUp);
168 | let lookAt: Vector3 = new Vector3().fromArray(opts.lookAt);
169 | let position = new Vector3().fromArray(opts.position);
170 |
171 | camera = new PerspectiveCamera(opts.fov, aspect, opts.near, opts.far);
172 | camera.position.copy(position);
173 | camera.up.copy(lookUp).normalize();
174 | camera.lookAt(lookAt);
175 | opts.camera = camera;
176 | }
177 | return opts.camera;
178 | }
179 |
180 | export function copyGsViewerOptions(gsViewerOptions: Reall3dViewerOptions): SplatMeshOptions {
181 | const { renderer, scene } = gsViewerOptions;
182 | const opts: SplatMeshOptions = { renderer, scene };
183 | opts.viewerEvents = gsViewerOptions.viewerEvents;
184 | opts.debugMode = gsViewerOptions.debugMode;
185 | opts.renderer = gsViewerOptions.renderer;
186 | opts.scene = gsViewerOptions.scene;
187 | opts.controls = gsViewerOptions.controls;
188 | opts.bigSceneMode = gsViewerOptions.bigSceneMode;
189 | opts.pointcloudMode = gsViewerOptions.pointcloudMode;
190 | opts.maxRenderCountOfMobile = gsViewerOptions.maxRenderCountOfMobile;
191 | opts.maxRenderCountOfPc = gsViewerOptions.maxRenderCountOfPc;
192 | opts.lightFactor = gsViewerOptions.lightFactor;
193 | opts.shDegree = gsViewerOptions.shDegree;
194 |
195 | return opts;
196 | }
197 |
--------------------------------------------------------------------------------
/src/reall3d/utils/consts/GlobalConstants.ts:
--------------------------------------------------------------------------------
1 | // ==============================================
2 | // Copyright (c) 2025 reall3d.com, MIT license
3 | // ==============================================
4 | export const ViewerVersion = 'v1.5.0'; // Reall3dViewer 版本
5 |
6 | export const isMobile = navigator.userAgent.includes('Mobi');
7 | export const HalfChars = 'QWERTYUIOPLKJHGFDSAZXCVBNM1234567890qwertyuioplkjhgfdsazxcvbnm`~!@#$%^&*()-_=+\\|]}[{\'";::,<.>//? \t';
8 | export const BinHeaderSize = 140;
9 | export const SpxHeaderSize = 128;
10 | export const DataSize36 = 36;
11 | export const DataSize32 = 32;
12 | export const SplatDataSize32 = 32;
13 | export const SplatDataSize20 = 20;
14 | export const SplatDataSize16 = 16;
15 | export const WasmBlockSize: number = 64 * 1024;
16 | export const MobileDownloadLimitSplatCount = 1024 * 10000; // 移动端高斯点数下载限制
17 | export const PcDownloadLimitSplatCount = 10240 * 10000; // PC端高斯点数下载限制
18 | export const SH_C0 = 0.28209479177387814;
19 |
20 | /** 【官方创建者Reall3d】Creater: Reall3d */
21 | export const SpxCreaterReall3d = 0;
22 | /** 【spx中定义的公开格式】spx open format */
23 | export const SpxOpenFormat0 = 0;
24 |
25 | /** 【spx中定义的公开数据块格式】Open Block Content Format 20, basic data */
26 | export const SpxBlockFormatData20 = 20;
27 | /** 【spx中定义的公开数据块格式】Open Block Content Format 1, data of SH degree 1 (SH1 only) */
28 | export const SpxBlockFormatSH1 = 1;
29 | /** 【spx中定义的公开数据块格式】Open Block Content Format 1, data of SH degree 2 (SH1 + SH2) */
30 | export const SpxBlockFormatSH2 = 2;
31 | /** 【spx中定义的公开数据块格式】Open Block Content Format 1, data of SH degree 3 (SH3 only) */
32 | export const SpxBlockFormatSH3 = 3;
33 |
34 | /** 【Reall3D扩展的专属格式】the exclusive format extended by reall3d */
35 | export const SpxExclusiveFormatReall3d = 3141592653;
36 |
--------------------------------------------------------------------------------
/src/reall3d/utils/consts/Index.ts:
--------------------------------------------------------------------------------
1 | // ==============================================
2 | // Copyright (c) 2025 reall3d.com, MIT license
3 | // ==============================================
4 | export * from './GlobalConstants';
5 | export * from './WkConstants';
6 |
--------------------------------------------------------------------------------
/src/reall3d/utils/consts/WkConstants.ts:
--------------------------------------------------------------------------------
1 | // ==============================================
2 | // Copyright (c) 2025 reall3d.com, MIT license
3 | // ==============================================
4 | // Worker常量
5 | let n: number = 0;
6 | /** Splat纹理 */
7 | export const WkTexdata = `$${n++}`;
8 | /** Splat索引 */
9 | export const WkSplatIndex = `$${n++}`;
10 | /** 当前模型已下载数据的最大半径 */
11 | export const WkCurrentMaxRadius = `$${n++}`;
12 | /** 模型的最大半径(小场景bin格式用) */
13 | export const WkMaxRadius = `$${n++}`;
14 | /** 模型中心点高度(小场景bin格式用) */
15 | export const WkTopY = `$${n++}`;
16 | /** 纹理索引 */
17 | export const WkIndex = `$${n++}`;
18 | /** 数据版本(时间戳) */
19 | export const WkVersion = `$${n++}`;
20 | /** 渲染的高斯数量 */
21 | export const WkRenderSplatCount = `$${n++}`;
22 | /** 可见的高斯数量 */
23 | export const WkVisibleSplatCount = `$${n++}`;
24 | /** 模型中的高斯数量 */
25 | export const WkModelSplatCount = `$${n++}`;
26 | /** 排序开始时间 */
27 | export const WkSortStartTime = `$${n++}`;
28 | /** 排序消耗时间 */
29 | export const WkSortTime = `$${n++}`;
30 | /** 纹理就绪 */
31 | export const WkTextureReady = `$${n++}`;
32 | /** 模型数据缓冲 */
33 | export const WkSplatDataBuffer = `$${n++}`;
34 | /** 是否大场景 */
35 | export const WkIsBigSceneMode = `$${n++}`;
36 | /** 最大渲染数量 */
37 | export const WkMaxRenderCount = `$${n++}`;
38 | /** bin格式版本 */
39 | export const WkBinVersion = `$${n++}`;
40 | /** 视图投影矩阵 */
41 | export const WkViewProjection = `$${n++}`;
42 | /** 上传纹理的版本 */
43 | export const WkUploadTextureVersion = `$${n++}`;
44 | /** 排序用已就绪的坐标 */
45 | export const WkXyz = `$${n++}`;
46 | /** 排序用已就绪的水印坐标 */
47 | export const WkWxyz = `$${n++}`;
48 | /** 包围盒 */
49 | export const WkMinX = `$${n++}`;
50 | /** 包围盒 */
51 | export const WkMaxX = `$${n++}`;
52 | /** 包围盒 */
53 | export const WkMinY = `$${n++}`;
54 | /** 包围盒 */
55 | export const WkMaxY = `$${n++}`;
56 | /** 包围盒 */
57 | export const WkMinZ = `$${n++}`;
58 | /** 包围盒 */
59 | export const WkMaxZ = `$${n++}`;
60 | /** 初始化 */
61 | export const WkInit = `$${n++}`;
62 | /** 水印数量 */
63 | export const WkWatermarkCount = `$${n++}`;
64 | /** 相机方向 */
65 | export const WkCameraDirection = `$${n++}`;
66 |
--------------------------------------------------------------------------------
/src/reall3d/viewer/Reall3dViewerOptions.ts:
--------------------------------------------------------------------------------
1 | // ==============================================
2 | // Copyright (c) 2025 reall3d.com, MIT license
3 | // ==============================================
4 | import { PerspectiveCamera, Renderer, Scene } from 'three';
5 | import { OrbitControls } from 'three/examples/jsm/Addons.js';
6 | import { Events } from '../events/Events';
7 |
8 | /**
9 | * 高斯网格配置项
10 | */
11 | export interface Reall3dViewerOptions {
12 | /**
13 | * 指定渲染器对象传入使用,未定义时自动生成
14 | */
15 | renderer?: Renderer | undefined;
16 |
17 | /**
18 | * 指定场景对象传入使用,未定义时自动生成
19 | */
20 | scene?: Scene | undefined;
21 |
22 | /**
23 | * 指定相机对象传入使用,未定义时自动生成
24 | */
25 | camera?: PerspectiveCamera | undefined;
26 |
27 | /**
28 | * 控制器
29 | */
30 | controls?: OrbitControls;
31 |
32 | /**
33 | * 渲染器事件管理器
34 | */
35 | viewerEvents?: Events | undefined;
36 |
37 | /**
38 | * 是否调试模式,生产环境默认false
39 | */
40 | debugMode?: boolean | undefined;
41 |
42 | /**
43 | * 是否大场景模式,初始化后不可修改
44 | */
45 | bigSceneMode?: boolean;
46 |
47 | /**
48 | * 是否点云模式渲染,默认为true
49 | * 支持通过viewer.options()动态更新
50 | */
51 | pointcloudMode?: boolean | undefined;
52 |
53 | /**
54 | * 移动端可渲染的高斯点数量限制
55 | * 支持通过viewer.options()动态更新
56 | */
57 | maxRenderCountOfMobile?: number | undefined;
58 |
59 | /**
60 | * PC端可渲染的高斯点数量限制
61 | * 支持通过viewer.options()动态更新
62 | */
63 | maxRenderCountOfPc?: number | undefined;
64 |
65 | /**
66 | * 颜色亮度系数,默认1.1
67 | */
68 | lightFactor?: number | undefined;
69 |
70 | /**
71 | * 容器元素或其选择器,默认选择器为'#gsviewer',自动创建画布时若找不到容器节点,将在body下自动创建容器
72 | */
73 | root?: HTMLElement | string | undefined;
74 |
75 | /**
76 | * 相机视场,默认 45
77 | */
78 | fov?: number | undefined;
79 |
80 | /**
81 | * 相机近截面距离,默认 0.1
82 | */
83 | near?: number | undefined;
84 |
85 | /**
86 | * 相机远截面距离,默认 1000
87 | */
88 | far?: number | undefined;
89 |
90 | /**
91 | * 相机位置,默认 [0, -5, 15]
92 | */
93 | position?: number[] | undefined;
94 |
95 | /**
96 | * 相机注视点,默认 [0, 0, 0]
97 | */
98 | lookAt?: number[] | undefined;
99 |
100 | /**
101 | * 相机上向量,默认 [0, -1, 0]
102 | */
103 | lookUp?: number[] | undefined;
104 |
105 | /**
106 | * 是否自动旋转,默认true
107 | * 支持通过viewer.options()动态更新
108 | */
109 | autoRotate?: boolean | undefined;
110 |
111 | /**
112 | * 是否启用阻尼效果,默认true
113 | */
114 | enableDamping?: boolean | undefined;
115 |
116 | /**
117 | * 是否允许操作缩放,默认true
118 | */
119 | enableZoom?: boolean | undefined;
120 |
121 | /**
122 | * 是否允许操作旋转,默认true
123 | */
124 | enableRotate?: boolean | undefined;
125 |
126 | /**
127 | * 是否允许操作拖动,默认true
128 | */
129 | enablePan?: boolean | undefined;
130 |
131 | /**
132 | * 最小视距
133 | */
134 | minDistance?: number | undefined;
135 |
136 | /**
137 | * 最大视距
138 | */
139 | maxDistance?: number | undefined;
140 |
141 | /**
142 | * 最小倾斜角度
143 | */
144 | minPolarAngle?: number | undefined;
145 |
146 | /**
147 | * 最大倾斜角度
148 | */
149 | maxPolarAngle?: number | undefined;
150 |
151 | /**
152 | * 是否允许键盘操作,默认true
153 | */
154 | enableKeyboard?: boolean | undefined;
155 |
156 | /**
157 | * 标注模式,默认false
158 | */
159 | markMode?: boolean | undefined;
160 |
161 | /**
162 | * 标注类型(点、线、面、距离、面积、圆),默认undefined
163 | */
164 | markType?: 'point' | 'lines' | 'plans' | 'distance' | 'area' | 'circle' | undefined;
165 |
166 | /**
167 | * 标注是否显示,默认true
168 | */
169 | markVisible?: boolean | undefined;
170 |
171 | /**
172 | * 米单位比例尺(1单位长度等于多少米),默认1
173 | */
174 | meterScale?: number | undefined;
175 |
176 | /**
177 | * 是否禁止直接拖拽本地文件进行查看,默认false
178 | */
179 | disableDropLocalFile?: boolean | undefined;
180 |
181 | /**
182 | * 球谐系数的渲染级别,默认为模型数据的最大可渲染级别
183 | */
184 | shDegree?: number | undefined;
185 |
186 | /**
187 | * 背景色(默认 '#000000')
188 | */
189 | background?: string;
190 | }
191 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "module": "ESNext",
5 | "lib": ["ES2020", "ES2021", "DOM", "DOM.Iterable"],
6 | "moduleResolution": "bundler",
7 | "strict": false,
8 | "esModuleInterop": true,
9 | "skipLibCheck": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "outDir": "./dist",
12 | },
13 | "include": ["src"]
14 | }
--------------------------------------------------------------------------------
/vite-pkg.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import dts from 'vite-plugin-dts';
3 |
4 | export default defineConfig({
5 | plugins: [
6 | dts({
7 | outDir: ['./pkg/dist'],
8 | rollupTypes: true,
9 | }),
10 | ],
11 | build: {
12 | target: 'es2020',
13 | outDir: './pkg/dist',
14 | lib: {
15 | entry: './src/reall3d/pkg.ts',
16 | name: 'reall3dviewer',
17 | fileName: 'pkg',
18 | },
19 | rollupOptions: {
20 | external: ['three', '@gotoeasy/three-tile'],
21 | output: {
22 | globals: {
23 | three: 'THREE',
24 | '@gotoeasy/three-tile': 'tt',
25 | },
26 | assetFileNames: asset => (asset.name?.endsWith('.css') ? 'style.css' : '[name].[extname]'),
27 | },
28 | },
29 | },
30 | });
31 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import { resolve } from 'path';
3 | import postcss from '@vituum/vite-plugin-postcss';
4 | import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
5 |
6 | export default defineConfig({
7 | plugins: [
8 | postcss(),
9 | createSvgIconsPlugin({
10 | iconDirs: [`${resolve(__dirname, 'src/reall3d/assets/icons')}`],
11 | symbolId: 'svgicon-[name]',
12 | }),
13 | ],
14 | server: {
15 | port: 3100,
16 | open: true,
17 | },
18 | preview: {
19 | port: 4100,
20 | },
21 | base: './',
22 | publicDir: 'public',
23 |
24 | esbuild: {
25 | pure: ['console.log', 'console.debug'],
26 | },
27 |
28 | build: {
29 | chunkSizeWarningLimit: 2048,
30 | sourcemap: false,
31 | rollupOptions: {
32 | output: {
33 | chunkFileNames: 'assets/chunk-[hash].js',
34 | entryFileNames: 'assets/entry-[hash].js',
35 | assetFileNames: 'assets/asset-[hash].[ext]',
36 | },
37 | },
38 | },
39 | });
40 |
--------------------------------------------------------------------------------