├── .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 | --------------------------------------------------------------------------------