chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:18:05 +08:00
commit acd8e21031
297 changed files with 56514 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
sandbox_mode = "workspace-write"
+38
View File
@@ -0,0 +1,38 @@
---
name: aholo-docs
description: Work on Aholo Viewer documentation and product writing. Use for README, AGENTS.md, docs/architecture.md, docs/ai/vibe-coding-guide.md, manual pages, API documentation flow, bilingual copy, release-readiness notes, and AI collaboration guidance.
---
# Aholo Docs
Use `docs/ai/vibe-coding-guide.md` as the writing source of truth.
## Core Rules
- Product: high-performance 3DGS rendering for web applications.
- Audience: frontend engineers, SDK docs readers, product-demo evaluators.
- Chinese copy: technical and concise.
- English copy: SDK documentation style.
- Keep zh-CN and en-US manual pages structurally parallel.
- Treat `website/src/content/manual/` as filesystem Markdown content, not an Astro content collection.
- Keep manual assets under `website/src/content/manual/assets/` and use local relative image references in manual Markdown.
- Use real public renderer exports in docs and examples.
- Do not document internal renderer symbols without user direction.
## Key Files
- `AGENTS.md`
- `README.md`
- `docs/architecture.md`
- `docs/ai/vibe-coding-guide.md`
- `website/src/content/manual/{zh-CN,en-US}/`
- `website/src/utils/manual.ts`
- `website/src/utils/manual-assets.js`
## Validate
Docs-only notes need path/reference scans. Manual or website-imported docs need:
```bash
pnpm.cmd check:website
```
@@ -0,0 +1,4 @@
interface:
display_name: "Aholo Docs"
short_description: "Docs, copy, manuals, and handoffs."
default_prompt: "Use $aholo-docs to update Aholo documentation, bilingual copy, or AI collaboration guidance."
+46
View File
@@ -0,0 +1,46 @@
---
name: aholo-examples
description: Work on Aholo Viewer example content pairs under website/src/content/examples. Use for creating, modifying, reviewing, or validating example *.json metadata and *.ts runners, Playground presets derived from examples, home-only example runners, config panels, async loading/caching, cleanup, and source shown in Playground.
---
# Aholo Examples
Own example content.
## Owns
- `website/src/content/examples/<slug>.json`
- `website/src/content/examples/<slug>.ts`
- Content metadata, `RenderRuntime` runner lifecycle, async loading, IndexedDB caching, config panels, and cleanup.
- Source code that appears as a Playground preset.
## References
- Use `splatting-basic`, `splatting-lod-stream`, `3d-buffer-geometry`, `3d-point-light`, and `home-interaction` as normal references.
- Read `website/src/content.config.ts` for examples metadata schema.
- Read `website/src/client/render-runtime.d.ts` for runner runtime.
- Read `website/src/utils/examples.ts` when changing surfaces, ordering, presets, or default examples.
## Contracts
- Keep JSON and TS files paired by the same slug.
- Use `surfaces` only when deviating from the default `["examples", "playground"]`; use `["home"]` for home-only runners. Do not repeat surface names.
- Keep `order` nonnegative and integral, `tags` nonempty, and `accent` as a six-digit hex color.
- Keep Chinese titles technical and concise; keep English titles in SDK documentation style.
- Import renderer APIs from `@manycore/aholo-viewer` and use actual public exports.
- Export an async default runner that accepts `RenderRuntime`.
- Check `signal.aborted` after async work and throw `AbortError` with a specific message.
- Use `loading.show()` before async fetch/decode and `loading.hide()` only after the scene is ready.
- Return a cleanup function that removes scene objects and destroys created GPU resources.
## Boundaries
- Do not change renderer public exports. Surface the need instead.
- Do not edit `packages/renderer/dist/` or `website/.generated/api/`.
- Use `aholo-site` for examples page chrome, Playground shell, client render runtime, or style work.
## Validate
- Metadata-only: `pnpm.cmd check:content`.
- Runner or website integration: `pnpm.cmd check:website`.
- Renderer API/package interaction: `pnpm.cmd check`.
@@ -0,0 +1,4 @@
interface:
display_name: "Aholo Examples"
short_description: "Create and maintain Aholo Viewer examples."
default_prompt: "Use $aholo-examples to create, modify, or validate Aholo Viewer website examples."
+24
View File
@@ -0,0 +1,24 @@
---
name: aholo-renderer
description: Work on the @manycore/aholo-viewer package. Use only when the user explicitly asks for renderer source, package build, public exports, JSDoc/API docs, generated declaration flow, renderer dist, packing, or release validation.
---
# Aholo Renderer
Work inside `packages/renderer/` and renderer-related scripts.
## Core Rules
- Renderer public API exports are user-owned.
- Do not change `packages/renderer/src/index.ts` exports unless explicitly asked.
- Do not hand-edit `external/egs-core` or `packages/renderer/dist/`.
- Do not delete `external/splat-transform`; it is a required workspace package.
- Add concise JSDoc only for user-approved public API work.
- If public API changes, regenerate docs through normal commands.
## Validate
```bash
pnpm.cmd check:renderer
pnpm.cmd build:renderer
```
@@ -0,0 +1,4 @@
interface:
display_name: "Aholo Renderer"
short_description: "Renderer package, API, and release flow."
default_prompt: "Use $aholo-renderer to work on the renderer package, public API, generated declarations, or release validation."
+41
View File
@@ -0,0 +1,41 @@
---
name: aholo-site
description: Work on Aholo Viewer Astro website and Playground surfaces under website/. Use for routes, layouts, components, page chrome, i18n UI copy, scoped CSS, client render runtime, Playground runner, preview canvas, inspector, presets, route-local Monaco, and URL parameters. Use aholo-examples for example content pairs.
---
# Aholo Site
Own the website shell and Playground integration. Do not own renderer source or example content pairs.
## Owns
- Astro routes, layouts, flat feature components, utilities, i18n, and site content under `website/`.
- Examples page chrome and preview layout, not `website/src/content/examples/*.json` or `*.ts`.
- Client render runtime under `website/src/client/`, camera/control integration, inspector, presets, and URL state.
- Playground shell in `website/src/components/PlaygroundShell.astro` and browser entry in `website/src/client/playground.ts`.
- Scoped styles: `theme.css`, `global.css`, `site.css`, `home.css`, `examples.css`, `docs.css`, `playground.css`.
## Boundaries
- Keep Monaco route-local through `website/src/components/PlaygroundShell.astro`; keep Playground browser entry in `website/src/client/playground.ts`.
- Preserve Playground URL params: `example` and `code`.
- Keep render runtime implementation in `website/src/client/render-runtime.ts` and runner contract in `website/src/client/render-runtime.d.ts`.
- Keep renderer type hints sourced from `packages/renderer/dist/index.d.ts`.
- Keep `website/src/content.config.ts` focused on Astro collections; manual pages are loaded through `website/src/utils/manual.ts`.
- Keep feature selectors out of `global.css`.
- Do not modify renderer public exports.
- Do not hand-edit `website/.generated/api/`.
- Use `aholo-examples` for example JSON/TS source pairs.
## Design
- Keep the site simple, refined, spacious, and tool-like.
- Keep homepage first-screen interactive renderer true fullscreen and resize the renderer after entering or exiting.
## Validate
```bash
pnpm.cmd check:website
```
Use `pnpm.cmd check` when renderer declarations or package output are involved.
@@ -0,0 +1,4 @@
interface:
display_name: "Aholo Site"
short_description: "Astro website, docs UI, examples, and Playground."
default_prompt: "Use $aholo-site to update Aholo Viewer website routes, styles, examples, docs UI, or Playground behavior."
+35
View File
@@ -0,0 +1,35 @@
---
name: aholo-viewer
description: Coordinate Aholo Viewer monorepo work. Use for cross-module tasks, workspace scripts, generated docs/build flow, validation routing, cleanup boundaries, or when work must be routed between examples, site, renderer, docs, and frontend design skills.
---
# Aholo Viewer
Read `AGENTS.md` first. Use this skill only when the task spans modules or needs repo-level validation/routing.
## Route
- `aholo-examples`: example content pairs under `website/src/content/examples/`, including `walk-demo`.
- `aholo-site`: Astro website shell, examples page chrome, docs UI, Playground, Monaco, client render runtime.
- `aholo-renderer`: renderer package, public API, declarations, build, dist, release checks.
- `aholo-docs`: README, AGENTS, architecture notes, manual copy/assets, AI guides, release notes.
- `frontend-design`: website visual direction, responsive polish, UI review.
## Boundaries
- Keep website work in `website/`, renderer work in `packages/renderer/`, shared automation in `scripts/`.
- Keep examples as paired metadata/runners and manual as filesystem Markdown loaded through website utilities.
- Do not hand-edit `external/egs-core`, `website/.generated/api/`, or generated `dist/`.
- Do not delete `external/splat-transform`.
- Do not change renderer public exports unless explicitly asked.
## Validate
```bash
pnpm.cmd check:website
pnpm.cmd check:renderer
pnpm.cmd check
pnpm.cmd build
```
If esbuild reports `Cannot read directory "../../../.."`, rerun the same command with approved workspace access.
@@ -0,0 +1,4 @@
interface:
display_name: "Aholo Viewer"
short_description: "Route repo-level Aholo work and validation."
default_prompt: "Use $aholo-viewer to route Aholo monorepo tasks, inspect workspace boundaries, and choose validation commands."
+177
View File
@@ -0,0 +1,177 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
+30
View File
@@ -0,0 +1,30 @@
---
name: frontend-design
description: Design and polish Aholo Viewer frontend interfaces with production-grade visual quality. Use for website UI direction, Astro page/component styling, responsive layout, visual refinement, and frontend design reviews in this repository.
---
# Frontend Design
Use this local skill for Aholo Viewer website and Playground visual design. Keep implementation consistent with the existing Astro site and scoped CSS architecture.
## Direction
- Audience: frontend engineers, SDK docs readers, and product-demo evaluators.
- Product: high-performance 3DGS rendering for web applications.
- Tone: simple, refined, spacious, comfortable, technical, and tool-like.
- Avoid generic AI aesthetics, broad marketing visuals, and overdecorated layouts.
## Rules
- Respect existing style layers: `theme.css`, `global.css`, `site.css`, `home.css`, `examples.css`, `docs.css`, `playground.css`.
- Keep feature-specific selectors out of `global.css`.
- Use restrained color, typography, spacing, and motion that fit SDK documentation and technical tooling.
- Do not make marketing-style landing sections unless the user explicitly asks.
- Ensure text, controls, canvas surfaces, and panels do not overlap on mobile or desktop.
- Use icons for compact controls when available, and keep repeated tool UI stable in size.
## Validate
```bash
pnpm.cmd check:website
```
@@ -0,0 +1,4 @@
interface:
display_name: "Frontend Design"
short_description: "Aholo website visual direction and UI polish."
default_prompt: "Use $frontend-design to refine Aholo Viewer frontend styling, layout, responsive behavior, or visual design quality."
+13
View File
@@ -0,0 +1,13 @@
# EditorConfig is awesome: https://EditorConfig.org
root = true
[*]
end_of_line = lf
indent_style = space
indent_size = 4
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.{json,yaml,yml,astro}]
indent_size = 2
+2
View File
@@ -0,0 +1,2 @@
# FORCE LF ON ANY PLATFORM
* text=auto eol=lf
+57
View File
@@ -0,0 +1,57 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Package version (please fill the releated package info)**
- @manycore/aholo-viewer: [e.g. 1.0.0]
**Desktop (please complete the following information optional, only if you found the issue):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information, optional, only if you found the issue):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Viewer Config (If possible, please complete the content, the config you used when call `setViewConfig`)**
```typescript
{
// fill the config here.
}
```
**Check List**
- [ ] Can you reproduce the issue on https://aholojs.dev/en-US/viewer/?
- [ ] Can you reproduce the issue on the latest version?
**Additional context**
Add any other context about the problem here.
+7
View File
@@ -0,0 +1,7 @@
---
name: Custom issue template
about: Describe this issue template's purpose here.
title: ''
labels: ''
assignees: ''
---
+19
View File
@@ -0,0 +1,19 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
+144
View File
@@ -0,0 +1,144 @@
on:
push:
branches:
- 'master'
paths:
- 'packages/splat-transform-native/source/**'
pull_request:
branches:
- 'master'
paths:
- 'packages/splat-transform-native/source/**'
workflow_dispatch:
name: build native
env:
NODE_VERSION: 24
jobs:
build-splat-transform-native:
name: Build splat-transform native (${{ matrix.target }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- target: win32-x64-msvc
native-script: build:native:win32-x64
os: windows-2025-vs2026
shell: pwsh
- target: win32-arm64-msvc
native-script: build:native:win32-arm64
os: windows-11-vs2026-arm
shell: pwsh
- target: linux-x64-gnu
native-script: build:native:linux-x64-gnu
os: ubuntu-22.04
shell: bash
- target: linux-arm64-gnu
native-script: build:native:linux-arm64-gnu
os: ubuntu-22.04-arm
shell: bash
- target: darwin-arm64
native-script: build:native:darwin-arm64
os: macos-26
shell: bash
defaults:
run:
shell: ${{ matrix.shell }}
steps:
- name: Checkout
uses: actions/checkout@v7
with:
submodules: recursive
lfs: true
- name: Setup node
uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install workspace dependencies
run: |
corepack enable
corepack install
pnpm install
- name: Setup env (Linux)
if: startsWith(matrix.os, 'ubuntu')
run: |
wget https://apt.llvm.org/llvm.sh
chmod a+x ./llvm.sh
sudo ./llvm.sh 21
sudo apt-get update
sudo apt-get install -y clang-21 clang-tools-21 lld-21 ninja-build pkg-config nasm
sudo update-alternatives --install /usr/bin/clang clang /usr/bin/clang-21 20
sudo update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-21 20
sudo update-alternatives --install /usr/bin/cc cc /usr/bin/clang-21 20
sudo update-alternatives --install /usr/bin/c++ c++ /usr/bin/clang++-21 20
sudo update-alternatives --set clang /usr/bin/clang-21
sudo update-alternatives --set clang++ /usr/bin/clang++-21
sudo update-alternatives --set cc /usr/bin/clang-21
sudo update-alternatives --set c++ /usr/bin/clang++-21
cmake --version
cc --version
c++ --version
ninja --version
- name: Setup env (macOS)
if: startsWith(matrix.os, 'macos')
run: |
brew install ninja nasm
cmake --version
clang --version
ninja --version
- name: Setup vcpkg (Windows)
if: startsWith(matrix.os, 'windows')
run: |
if ($env:VCPKG_INSTALLATION_ROOT -and (Test-Path $env:VCPKG_INSTALLATION_ROOT)) {
$vcpkgRoot = $env:VCPKG_INSTALLATION_ROOT
} else {
$vcpkgRoot = Join-Path $env:RUNNER_TEMP "vcpkg"
git clone https://github.com/microsoft/vcpkg $vcpkgRoot
& (Join-Path $vcpkgRoot "bootstrap-vcpkg.bat") -disableMetrics
}
"VCPKG_ROOT=$vcpkgRoot" >> $env:GITHUB_ENV
- name: Setup vcpkg (Unix)
if: ${{ !startsWith(matrix.os, 'windows') }}
run: |
if [ -n "${VCPKG_INSTALLATION_ROOT:-}" ] && [ -d "${VCPKG_INSTALLATION_ROOT}" ]; then
vcpkg_root="${VCPKG_INSTALLATION_ROOT}"
else
vcpkg_root="${RUNNER_TEMP}/vcpkg"
git clone https://github.com/microsoft/vcpkg "${vcpkg_root}"
"${vcpkg_root}/bootstrap-vcpkg.sh" -disableMetrics
fi
echo "VCPKG_ROOT=${vcpkg_root}" >> "${GITHUB_ENV}"
- name: Build splat-transform native
working-directory: packages/splat-transform-native/source
run: pnpm run ${{ matrix.native-script }}
- name: Validate native package (Windows)
if: startsWith(matrix.os, 'windows')
run: |
if (!(Test-Path ./packages/splat-transform-native/splat-transform-${{ matrix.target }}/splat-transform.node)) {
throw "Missing splat-transform.node"
}
node -e "require('./packages/splat-transform-native/splat-transform-${{ matrix.target }}/splat-transform.node'); console.log('native require ok')"
- name: Validate native package (Unix)
if: ${{ !startsWith(matrix.os, 'windows') }}
run: |
test -f ./packages/splat-transform-native/splat-transform-${{ matrix.target }}/splat-transform.node
node -e "require('./packages/splat-transform-native/splat-transform-${{ matrix.target }}/splat-transform.node'); console.log('native require ok')"
- name: Upload native package
uses: actions/upload-artifact@v7
with:
name: splat-transform-${{ matrix.target }}
path: packages/splat-transform-native/splat-transform-${{ matrix.target }}/splat-transform.node
if-no-files-found: error
+26
View File
@@ -0,0 +1,26 @@
on:
push:
branches:
- 'master'
pull_request:
branches:
- 'master'
name: check
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
with:
submodules: recursive
lfs: true
- name: Setup node
uses: actions/setup-node@v6
with:
node-version: 24
- run: |
chmod a+x ./ci/check.sh
./ci/check.sh
+123
View File
@@ -0,0 +1,123 @@
on: workflow_dispatch
name: publish
env:
NODE_VERSION: 24
jobs:
publish:
runs-on: ubuntu-latest
permissions:
actions: read
contents: write
environment:
name: publish
deployment: false
if: github.ref == 'refs/heads/master'
steps:
- name: Checkout
uses: actions/checkout@v7
with:
submodules: recursive
lfs: true
- name: Setup node
uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
- name: Check splat-transform publish status
id: splat-transform
run: |
package_name="$(node -p "require('./packages/splat-transform/package.json').name")"
package_version="$(node -p "require('./packages/splat-transform/package.json').version")"
if npm view "${package_name}@${package_version}" version --registry=https://registry.npmjs.org --silent >/dev/null 2>&1; then
echo "${package_name}@${package_version} already exists on npm."
echo "needs_publish=false" >> "$GITHUB_OUTPUT"
else
echo "${package_name}@${package_version} is not published yet."
echo "needs_publish=true" >> "$GITHUB_OUTPUT"
fi
- name: Resolve latest native build
if: steps.splat-transform.outputs.needs_publish == 'true'
id: native-build
env:
GH_TOKEN: ${{ github.token }}
run: |
run_id="$(gh api --method GET "/repos/${{ github.repository }}/actions/workflows/build-native.yaml/runs" -f branch=master -f status=success -f per_page=1 --jq '.workflow_runs[0].id // empty')"
if [ -z "$run_id" ]; then
echo "No successful build-native workflow run found on master." >&2
exit 1
fi
echo "run_id=$run_id" >> "$GITHUB_OUTPUT"
- name: Download native packages
if: steps.splat-transform.outputs.needs_publish == 'true'
uses: actions/download-artifact@v7
with:
pattern: splat-transform-*
path: packages/splat-transform-native
merge-multiple: false
repository: ${{ github.repository }}
run-id: ${{ steps.native-build.outputs.run_id }}
github-token: ${{ github.token }}
- name: Publish packages
id: publish-packages
env:
NPM_TOKEN: ${{secrets.NPM_TOKEN}}
run: |
set -o pipefail
publish_log="$(mktemp)"
chmod a+x ./ci/publish.sh
./ci/publish.sh | tee "$publish_log"
if grep -q '^published:$' "$publish_log"; then
echo "has_published=true" >> "$GITHUB_OUTPUT"
else
echo "has_published=false" >> "$GITHUB_OUTPUT"
fi
- name: Create GitHub releases
if: steps.publish-packages.outputs.has_published == 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
releases=(
"packages/splat-transform|splat-transform|false"
"packages/renderer|aholo-viewer|true"
)
for release in "${releases[@]}"; do
IFS='|' read -r package_dir tag_prefix make_latest <<< "$release"
package_name="$(node -p "require('./${package_dir}/package.json').name")"
package_version="$(node -p "require('./${package_dir}/package.json').version")"
tag_name="${tag_prefix}@${package_version}"
release_name="${tag_prefix} ${package_version}"
if gh release view "$tag_name" --repo "${{ github.repository }}" >/dev/null 2>&1; then
echo "GitHub release ${tag_name} already exists."
continue
fi
echo "Creating GitHub release ${tag_name} for ${package_name}@${package_version}."
release_notes_file="$(mktemp)"
node scripts/read-package-changelog.mjs "$package_name" "$package_version" > "$release_notes_file"
if git ls-remote --exit-code --tags origin "refs/tags/${tag_name}" >/dev/null 2>&1; then
git fetch --force --depth=1 origin "refs/tags/${tag_name}:refs/tags/${tag_name}"
if ! git cat-file -e "${tag_name}^{tag}" 2>/dev/null; then
echo "Tag ${tag_name} already exists but is not an annotated tag." >&2
exit 1
fi
echo "Annotated tag ${tag_name} already exists."
else
git tag -a "$tag_name" "$GITHUB_SHA" -m "$tag_name"
git push origin "refs/tags/${tag_name}"
fi
gh api --method POST "/repos/${{ github.repository }}/releases" \
-f tag_name="$tag_name" \
-f target_commitish="$GITHUB_SHA" \
-f name="$release_name" \
-F body=@"$release_notes_file" \
-f make_latest="$make_latest" >/dev/null
done
+12
View File
@@ -0,0 +1,12 @@
node_modules/
.pnpm-store/
dist/
build/
.astro/
website/.generated/
.typedoc-*.tmp*/
.typedoc-*.tmp.json
*.log
.env
.env.*
!.env.example
+4
View File
@@ -0,0 +1,4 @@
[submodule "external/egs-core"]
path = external/egs-core
url = https://github.com/manycoretech/egs.git
branch = master
+2
View File
@@ -0,0 +1,2 @@
pnpm lint-staged -q;
git update-index --again;
+7
View File
@@ -0,0 +1,7 @@
{
"recommendations": [
"oxc.oxc-vscode",
"editorconfig.editorconfig",
"streetsidesoftware.code-spell-checker"
]
}
+25
View File
@@ -0,0 +1,25 @@
{
"js/ts.tsdk.path": "node_modules\\typescript\\lib",
"[typescript]": {
"editor.defaultFormatter": "oxc.oxc-vscode",
},
"[javascript]": {
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"[markdown]": {
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"[yaml]": {
"editor.defaultFormatter": "oxc.oxc-vscode"
},
"cSpell.words": [
"aholo",
"gltf",
"manycore",
"Raycaster",
"oxc",
"oxlint",
"oxfmt"
],
"C_Cpp.default.cppStandard": "c++20"
}
+64
View File
@@ -0,0 +1,64 @@
# Agent Guide
AI collaboration entrypoint. Keep context small, preserve user changes, and route work to the narrowest project skill.
## Modules
- Root: `pnpm` workspace orchestration, validation, release scripts.
- Website: `website/` Astro routes, layouts, flat feature components, client render runtime, docs pages, Playground, scoped styles.
- Examples: `website/src/content/examples/` paired example metadata and `RenderRuntime` runners.
- Renderer: `packages/renderer/` source and generated package output.
- Scripts: `scripts/` submodule prep, renderer build/clean, EGS types, API docs.
- Docs: `README.md`, `docs/`, manual pages, manual assets, AI collaboration guidance.
- External: `external/egs-core` submodule and `external/splat-transform` workspace package.
- Skills: `.codex/skills/` project-local collaboration rules.
## Skill Routing
- `aholo-examples`: example `<slug>.json` and `<slug>.ts` pairs, including `walk-demo`, metadata schema, runner lifecycle.
- `aholo-site`: website shell, page chrome, client render runtime, Playground integration, route-local Monaco, scoped CSS.
- `aholo-renderer`: renderer source, public API, declarations, generated dist flow, package/release validation.
- `aholo-docs`: README, AGENTS, architecture notes, AI guides, manual copy, bilingual docs.
- `frontend-design`: visual direction, responsive UI polish, layout/design review.
- `aholo-viewer`: only for cross-module work, scripts, generated docs/build flow, cleanup boundaries, or validation routing.
## Hard Boundaries
- Do not overwrite user changes. Start with `git status --short`.
- Keep edits inside the owning module unless the task explicitly spans modules.
- Do not hand-edit `external/egs-core`, `website/.generated/api/`, or `packages/renderer/dist/`.
- Do not delete `external/splat-transform`.
- Do not change `packages/renderer/src/index.ts` exports unless asked.
- Do not compare the renderer to third-party engines or frameworks unless asked.
## Website Boundaries
- Keep Monaco route-local through `website/src/components/PlaygroundShell.astro`; keep Playground browser entry in `website/src/client/playground.ts`.
- Preserve Playground URL params: `example` and `code`.
- Keep render runtime implementation in `website/src/client/render-runtime.ts` and runner contract in `website/src/client/render-runtime.d.ts`.
- Keep examples paired as `website/src/content/examples/<slug>.json` and `<slug>.ts`.
- Keep `website/src/content.config.ts` focused on Astro content collections; manual pages use `website/src/utils/manual.ts`.
- Keep top-level feature components flat in `website/src/components/`; keep only shared groups such as `docs/` and `site/` nested.
- Keep style ownership clear: `theme.css`, `global.css`, `site.css`, `home.css`, `examples.css`, `docs.css`, `playground.css`.
- Keep feature selectors out of `global.css`.
- Homepage first-screen interactive stage must remain true fullscreen: fixed positioning, `inset: 0`, `100dvh`/`100vw`, and renderer resize after state changes.
## Collaboration
- Reply to the user in Chinese.
- Use PowerShell with `pnpm.cmd` if `pnpm` or `pnpm.ps1` fails.
- If port `4321` is already in use, assume the user's dev server is running and use it directly.
- Read `docs/architecture.md` only for package boundaries, generated API docs, build flow, or directory structure.
- Keep Chinese copy technical and concise. Keep English copy in SDK documentation style.
- For manual pages and UI translations, treat Chinese wording as the source of truth.
- Use actual public renderer exports in docs and examples.
- End handoffs with changed files, validation, skipped checks, and risks.
## Validation
- Website/layout/manual/example runtime: `pnpm.cmd check:website`.
- Content-only example metadata: `pnpm.cmd check:content`.
- Renderer/API/release work: `pnpm.cmd check`.
- Package/release changes: `pnpm.cmd build`.
- Docs-only repo notes: path/reference scan is enough unless imported by the site.
- If esbuild reports `Cannot read directory "../../../.."`, rerun the same command with approved workspace access.
+21
View File
@@ -0,0 +1,21 @@
The MIT License
Copyright © 2026 MANYCORE, INC.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+147
View File
@@ -0,0 +1,147 @@
[![logo](./website/public/aholo-logo.svg)](https://aholojs.dev/)
# Aholo Viewer
![Build Status](https://github.com/manycoretech/aholo-viewer/actions/workflows/check.yaml/badge.svg?branch=master)
[![aholo-viewer](https://img.shields.io/npm/v/@manycore/aholo-viewer?label=aholo-viewer)](https://www.npmjs.com/package/@manycore/aholo-viewer)
[![splat-transform](https://img.shields.io/npm/v/@manycore/aholo-splat-transform?label=splat-transform)](https://www.npmjs.com/package/@manycore/aholo-splat-transform)
[![license](https://img.shields.io/npm/l/@manycore/aholo-viewer)](#license)
Monorepo for the Aholo Viewer package and its documentation website.
## What is Aholo Viewer
Aholo Viewer is a high performance Renderer for 3DGS and Mesh. It uses `Chunked Steaming Lod` schema to handle huge 3DGS.
## Usage
Follow the [Manual](https://aholojs.dev/en-US/manual/getting-started/).
> If everything goes well you will see [this](https://jsfiddle.net/hs6Lp038/)
For npm package usage, see [`packages/renderer/README.md`](./packages/renderer/README.md).
For AI-assisted integration, use [`docs/ai/skills/use-aholo-viewer/SKILL.md`](./docs/ai/skills/use-aholo-viewer/SKILL.md).
## Build Requirements
- Node: >= 22.22.1
- pnpm(corepack preferred)
## Clone this repository
This repository contains some submodules as dependencies, use following command to clone the repository.
```bash
git clone --recurse-submodules https://github.com/manycoretech/aholo-viewer.git
```
## Structure
```text
aholo-viewer/
AGENTS.md Agent-facing project guide
website/ Astro website: home, manual, examples, API docs, playground
packages/renderer/ Renderer TypeScript source package
scripts/ Shared build and documentation scripts
docs/ Architecture and AI collaboration notes
external/ Required upstream and workspace dependency sources
.codex/skills/ Project-local Codex skills
```
## Commands
Run workspace commands from the repository root:
```bash
pnpm install
pnpm dev
pnpm check
pnpm build
pnpm preview
```
Targeted root commands:
```bash
pnpm build:renderer
pnpm build:website
pnpm check:content
pnpm check:renderer
pnpm check:website
pnpm docs:api
```
## Project Docs
- `AGENTS.md`: quick guide for AI agents and future coding sessions
- `docs/architecture.md`: current workspace structure and dependency flow
- `docs/ai/vibe-coding-guide.md`: detailed guide for future AI-assisted changes, writing style, and handoffs
- `docs/ai/skills/use-aholo-viewer/SKILL.md`: external npm package usage guide for AI coding agents
- `.codex/skills/`: local Codex skills split by repo area
## Codex Skills
Project-local skills live in `.codex/skills/`. Use `AGENTS.md` for the current skill map.
## External Source
`external/egs-core` is a required upstream submodule. `external/splat-transform` is a required workspace package and must stay in the repo. Treat upstream code under `external/` as read-only unless a task explicitly targets that package.
## API Docs
API docs are generated from `packages/renderer/src/index.ts` into an ignored local directory:
```text
website/.generated/api/
```
`pnpm dev`, `pnpm build`, and `pnpm check` regenerate them automatically. Run the generator directly when you want to refresh the local TypeDoc HTML and manifest without starting the site:
```bash
pnpm docs:api
```
## Content Checks
`pnpm check:content` validates manual locale parity, empty pages, example source pairs, manual image references, orphan manual images, and internal-only documentation links. It is also part of `pnpm check` and `pnpm check:website`.
## Playground URLs
The Playground keeps edited code in the URL with `lz-string`:
```text
/zh-CN/playground/?example=basic-scene&code=<compressed-source>
```
Opening a URL with `code` restores the editor content automatically.
Examples are stored in `website/src/content/examples/` as paired `<slug>.json` metadata and `<slug>.ts` source files. The same slug powers the Examples pages and Playground `example` query parameter.
## Contributors
<a href="https://github.com/manycoretech/aholo-viewer/graphs/contributors">
<img src="https://contrib.rocks/image?repo=manycoretech/aholo-viewer" />
</a>
Made with [contrib.rocks](https://contrib.rocks).
## Star History
<a href="https://www.star-history.com/?repos=manycoretech%2Faholo-viewer&type=date&legend=top-left">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=manycoretech/aholo-viewer&type=date&theme=dark&legend=top-left" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=manycoretech/aholo-viewer&type=date&legend=top-left" />
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=manycoretech/aholo-viewer&type=date&legend=top-left" />
</picture>
</a>
## Useful Links
- [Discussions](https://github.com/manycoretech/aholo-viewer/discussions)
- [Official website](https://aholojs.dev/)
- [CHANGELOG](./packages/renderer/CHANGELOG.md)
## License
[MIT License](./LICENSE).
+7
View File
@@ -0,0 +1,7 @@
# WeHub 来源说明
- 原始项目:`manycoretech/aholo-viewer`
- 原始仓库:https://github.com/manycoretech/aholo-viewer
- 导入方式:上游默认分支的最新快照
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
set -e
npm install npm -g
npm install corepack -g
corepack enable
corepack install
pnpm install
pnpm --filter @manycore/aholo-splat-transform run build
pnpm check
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -e
npm install npm -g
npm install corepack -g
corepack enable
corepack install
pnpm install
npm config set registry https://registry.npmjs.org
npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}
node scripts/publish-packages.mjs
+169
View File
@@ -0,0 +1,169 @@
---
name: use-aholo-viewer
description: Guide external npm package users and AI coding agents integrating @manycore/aholo-viewer into web applications. Use when asked to install, bootstrap, render 3D Gaussian Splatting or mesh content, configure cameras, scenes, lighting, viewer pipeline options, or write TypeScript examples for the public @manycore/aholo-viewer npm package.
---
# Use Aholo Viewer
Use this skill to build or modify user applications that consume the public `@manycore/aholo-viewer` npm package. Treat it as a full-featured browser rendering package, not as this repository's internal renderer source.
## Source Docs
Before generating non-trivial code, read the AI-oriented docs advertised by `https://aholojs.dev/llms.txt`:
- `https://aholojs.dev/llm/manual/getting-started.md` for installation and the minimal Vite setup.
- `https://aholojs.dev/llm/manual/basic-concepts.md` for scenes, objects, cameras, viewports, and the render flow.
- `https://aholojs.dev/llm/api/index.md` to confirm public exports.
- `https://aholojs.dev/llm/examples/index.md` for runnable TypeScript patterns.
Prefer these Markdown docs over scraping rendered HTML pages.
## Integration Workflow
1. Install the package in a browser app:
```bash
npm install --save @manycore/aholo-viewer
npm install --save-dev vite typescript
```
2. Import only public package exports from `@manycore/aholo-viewer`.
3. Create a real DOM container with stable dimensions before calling `createViewer`. Attach application DOM event listeners to this container, not to the engine canvas.
4. Create a camera, position it, call `lookAt`, add objects to `viewer.getScene()`, then call `viewer.setCamera(camera)`.
5. Configure rendering with `setViewerConfig(viewer, { pipeline: ... })`.
6. Render explicitly with `viewer.render()`. If the app relies on viewer-triggered invalidation, set `viewer.requestRenderHandler` to schedule another render with `requestAnimationFrame`.
7. Clean up when removing content: detach scene objects, stop render/update loops that still reference them, then release GPU resources or owned resources according to the cleanup rules below.
## Minimal Splat Example
Use this pattern for a Vite/TypeScript browser entry:
```ts
import {
BackgroundMode,
Color,
PerspectiveCamera,
SplatLoader,
SplatUtils,
Vector3,
createViewer,
setViewerConfig,
} from '@manycore/aholo-viewer';
const SPLAT_URL = 'https://holo-cos.aholo3d.cn/aholo-opensource/gs_file/bear/bear.3d71a266.sog';
const container = document.createElement('div');
container.style.width = '500px';
container.style.height = '500px';
document.body.appendChild(container);
async function main() {
const viewer = createViewer('example-viewer', container, {});
const camera = new PerspectiveCamera(60, 1, 0.1, 2000);
const response = await fetch(SPLAT_URL);
const buffer = await response.arrayBuffer();
const data = await SplatLoader.parseSplatData(
SplatLoader.SplatFileType.SOG,
new Uint8Array(buffer),
SplatLoader.SplatPackType.Compressed,
);
const splat = await SplatUtils.createSplat(data);
camera.up.set(0, -1, 0);
camera.position.set(-1.5, -0.5, 0);
camera.lookAt(new Vector3(0, 0, 0));
viewer.getScene().add(splat);
viewer.setCamera(camera);
setViewerConfig(viewer, {
pipeline: {
Background: {
background: {
active: BackgroundMode.BasicBackground,
basic: { color: new Color(0, 0, 0) },
},
ground: { enabled: false },
},
Splatting: { enabled: true },
TAA: { enabled: false },
},
});
const render = () => viewer.render();
viewer.requestRenderHandler = () => requestAnimationFrame(render);
requestAnimationFrame(render);
}
main();
```
## Common Public Exports
- Viewer setup: `createViewer`, `setViewerConfig`, `Viewer`, `Viewport`.
- Cameras and math: `PerspectiveCamera`, `OrthographicCamera`, `Vector2`, `Vector3`, `Vector4`, `Color`, `Matrix4`, `Quaternion`.
- Scene objects: `Object3D`, `Scene3D`, `Mesh`, `Splat`, `AmbientLight`, `DirectionalLight`, `PointLight`, `SpotLight`.
- Mesh content: `BufferGeometry`, `BufferAttribute`, `MeshBasicMaterial`, `MeshPhongMaterial`, `Side`, `downloadTexture`.
- Splat content: `SplatLoader`, `SplatUtils`, `CompressedSplat`, `SuperCompressedSplat`, `SogSplat`.
- Optional loaders/utilities: `GLTFLoader`, `DracoLoader`, `Animation`, `Events`.
Confirm exact symbols in `/llm/api/index.md` when using less common APIs.
## Patterns
For 3DGS content:
- Use `SplatLoader.parseSplatData(...)` to parse supported splat formats.
- Use `SplatUtils.createSplat(data)` to create a renderable splat.
- Enable `pipeline.Splatting`.
- Set camera up vectors deliberately. OpenCV-style 3DGS data commonly uses `camera.up.set(0, -1, 0)`.
For mesh content:
- Build geometry with `BufferGeometry`, `BufferAttribute`, and optional indices.
- Use `MeshPhongMaterial` with lights for shaded meshes, or `MeshBasicMaterial` for unlit objects.
- Add lights such as `AmbientLight`, `DirectionalLight`, or `PointLight` before rendering lit materials.
For interactive apps:
- Tie camera controls or animations to a frame loop that calls `viewer.render()`.
- Keep viewer container size stable and update rendering after layout changes.
- Register pointer, wheel, keyboard focus, drag/drop, resize, and custom DOM event listeners on the viewer container or another application-owned element.
- Cache large splat data when appropriate, but keep the first example simple unless the user asks for caching.
## DOM Events and Canvas Access
Treat the container as the stable application boundary:
- Listen for DOM events on the `container` passed to `createViewer(...)`, or on another element owned by the application.
- Do not attach event listeners to the viewer canvas. The canvas is managed internally by the engine and may be replaced.
- Do not persistently store the engine canvas element in application state, class fields, module globals, framework refs, or closures intended to outlive the current operation.
- If direct canvas access is unavoidable, read it just-in-time from the current viewer/container state, use it immediately, and discard the reference.
- Remove container-level event listeners during teardown before releasing viewer or scene resources.
## Resource Cleanup
Use the lightest cleanup method that matches ownership and lifecycle:
- Use `freeGPU()` when the goal is to release GPU memory while keeping the object instance logically reusable. The object may upload GPU resources again if it is rendered or updated later.
- Use `freeAllGpuResourceOwned()` when the object owns related GPU resources that should also be released. For example, `geometry.freeAllGpuResourceOwned()` releases the geometry GPU resource plus its attribute and index GPU resources; `drawable.freeAllGpuResourceOwned()` releases its materials, geometry, and drawable GPU resources.
- Use `destroy()` only when the object is permanently retired. Before calling `destroy()`, ensure the object is removed from the scene, no frame callback, async load, event listener, control, viewport, material, geometry, or app state will use it again, and it is not currently participating in a render/update pass.
- Prefer `freeGPU()` or `freeAllGpuResourceOwned()` for memory-pressure cleanup. Prefer `destroy()` for final teardown after references are severed.
- In component frameworks, cancel pending async work and stop animation/requestRender callbacks before releasing resources in unmount cleanup.
## Guardrails
- Do not hand-write code against private renderer internals or repository-only paths.
- Do not assume a server-side runtime; the viewer needs browser DOM and WebGL/WebGL2-capable canvas support.
- Do not construct `Splat` directly. Use loader and utility APIs.
- Do not register app DOM events on the engine canvas. Register on the container because the engine owns the canvas lifecycle and may replace it.
- Do not hold persistent references to the engine canvas. Read it only when needed, use it immediately, and release the reference.
- Do not call `destroy()` on resources that shared objects, cached data, render callbacks, or pending async work may still reference.
- Do not use `destroy()` as the default GPU-memory cleanup method. Use `freeGPU()` for the object's own GPU state, or `freeAllGpuResourceOwned()` when owned child resources should also release GPU state.
- Do not omit cleanup in component frameworks. In React, Vue, Svelte, or similar frameworks, dispose viewer-owned objects and cancel pending async work during unmount.
- Do not invent package exports. Check the LLM API index or installed package types.
@@ -0,0 +1,50 @@
# Claude Instructions: Use Aholo Viewer
Use these instructions when Claude is helping a user integrate the public `@manycore/aholo-viewer` npm package into a browser application.
## Activation
Apply this guidance when the user asks Claude to install, bootstrap, debug, or extend an app that uses `@manycore/aholo-viewer`, especially for:
- 3D Gaussian Splatting (3DGS) rendering.
- Mesh rendering with `BufferGeometry`, `Mesh`, lights, and materials.
- Viewer setup, camera setup, scene composition, render-loop wiring, or cleanup.
- React, Vue, Svelte, Vite, or plain TypeScript browser integrations.
## Read First
Before writing non-trivial code, read the AI-facing docs listed by `https://aholojs.dev/llms.txt`.
Preferred reading order:
1. `https://aholojs.dev/llm/manual/getting-started.md`
2. `https://aholojs.dev/llm/manual/basic-concepts.md`
3. `https://aholojs.dev/llm/api/index.md`
4. `https://aholojs.dev/llm/examples/index.md`
Use the Markdown docs instead of scraping rendered HTML.
## Implementation Rules
- Import only public exports from `@manycore/aholo-viewer`; never import internal `@qunhe/*` packages.
- Create a stable application-owned container before calling `createViewer(...)`.
- Attach DOM event listeners to the container or another app-owned element, not to the engine canvas.
- Do not persistently store the engine canvas. If canvas access is unavoidable, read it only at the point of use and discard the reference immediately.
- Use `PerspectiveCamera` or `OrthographicCamera`, then call `viewer.setCamera(camera)`.
- Add objects to `viewer.getScene()`.
- Configure the pipeline with `setViewerConfig(viewer, { pipeline: ... })`.
- Trigger rendering with `viewer.render()` or schedule rendering through `viewer.requestRenderHandler`.
- Do not construct `Splat` directly; load splat data with `SplatLoader` and create renderable splats with `SplatUtils.createSplat(...)`.
## Cleanup Rules
- Stop render loops, event listeners, controls, and pending async work before releasing resources.
- Use `freeGPU()` to release an object's own GPU resources while keeping the object reusable.
- Use `freeAllGpuResourceOwned()` when owned child resources should also release GPU state. For example, `geometry.freeAllGpuResourceOwned()` also releases attribute and index GPU resources.
- Use `destroy()` only for final teardown after the object is detached from the scene and no callback, async task, viewport, material, geometry, or app state can use it again.
## Output Style
- Prefer concise TypeScript examples over long explanations.
- For full setup steps, link to `https://aholojs.dev/en-US/manual/getting-started/` instead of duplicating the complete tutorial.
- Confirm less common exports against `https://aholojs.dev/llm/api/index.md` before using them.
@@ -0,0 +1,4 @@
interface:
display_name: 'Use Aholo Viewer'
short_description: 'Guide AI agents to integrate @manycore/aholo-viewer'
default_prompt: 'Use $use-aholo-viewer to build a Vite integration with @manycore/aholo-viewer.'
+90
View File
@@ -0,0 +1,90 @@
# Vibe Coding Guide
Concise source for AI-assisted product writing, collaboration, and handoff rules.
## Work Loop
1. Read `AGENTS.md`.
2. Use the narrowest project skill for the task.
3. Read `docs/architecture.md` only for package boundaries, generated API docs, build flow, or directory structure.
4. Run `git status --short` before editing and preserve user changes.
5. When reading Chinese files, use `rg` or `Get-Content -Encoding utf8` to avoid PowerShell encoding ambiguity.
## Product Brief
- Audience: frontend engineers, SDK documentation readers, and product-demo evaluators.
- Positioning: high-performance 3D Gaussian Splatting (3DGS) rendering for web applications.
- Stage: preparing for public release.
- Visual direction: simple, refined, spacious, comfortable, technical.
- Primary manual-check browser: Chrome.
Core value:
- Focused renderer experience for web-based 3DGS scenes.
- High-performance 3DGS rendering.
- Surrounding 3DGS facilities: examples, preview, debugging, docs, and integration support.
## Skill Map
- `aholo-viewer`: cross-area work, repo cleanup, workspace scripts, and validation routing.
- `aholo-site`: Astro website, examples, docs UI, Playground, Monaco, runner, preview, presets, URL state.
- `aholo-renderer`: renderer package, public API, build, declarations, release checks.
- `aholo-docs`: docs, manual content, bilingual copy, AI collaboration notes.
- `frontend-design`: Aholo website visual direction, responsive styling, and UI polish.
## Writing Rules
- Keep copy concise, concrete, and tool-oriented.
- Avoid placeholders, broad marketing claims, and invented API names.
- Do not compare the renderer to third-party engines or frameworks unless explicitly asked.
- Use actual public renderer exports in snippets.
- Keep Chinese copy technical and concise.
- Keep English copy in an SDK documentation style.
- Keep zh-CN and en-US manual pages structurally parallel.
## Terms
```text
3D Gaussian Splatting (3DGS) / 3DGS
Mesh / 网格
Renderer / 渲染器
Scene / 场景
Camera / 相机
Material / 材质
Render loop / 渲染循环
Playground / Playground
Examples / 示例
Manual / 手册
API Reference / API 参考
```
## Non-Negotiables
- Renderer public API exports are user-owned.
- Do not modify `packages/renderer/src/index.ts` exports unless explicitly asked.
- Do not hand-edit `external/egs-core`, `website/.generated/api/`, or generated `dist` folders.
- Do not delete `external/splat-transform`; it is a required workspace package.
- Keep Monaco route-local.
- Keep examples as paired JSON metadata and TypeScript source files.
## Validation
```text
Site/Playground/style pnpm.cmd check:website
Renderer/API/release pnpm.cmd check
Package/release pnpm.cmd build
Docs-only repo notes path/reference scan unless imported by website
```
If esbuild cannot read the workspace in a sandboxed run, rerun the same command with approved workspace access.
For browser-based website checks, prefer an already running website service and reuse its URL/port before starting a new dev server.
## Handoff
End with:
- What changed.
- Validation result.
- Skipped checks, risks, or assumptions.
- Follow-up suggestions only when useful.
+211
View File
@@ -0,0 +1,211 @@
# Project Architecture
## Overview
Aholo Viewer is split into two main products:
- `@manycore/aholo-viewer`: the TypeScript renderer package.
- `@manycore/aholo-viewer-website`: the Astro documentation and playground website.
The root package coordinates build, check, API generation, and packaging through pnpm workspace scripts.
## Workspace Layout
```text
./
package.json
pnpm-workspace.yaml
AGENTS.md
website/
package.json
astro.config.mjs
.generated/
api/
src/
components/
config/
content/
i18n/
layouts/
pages/
playground/
styles/
utils/
packages/
renderer/
package.json
tsconfig.json
src/
dist/
scripts/
build-package.mjs
check-content.mjs
clean-package.mjs
ensure-submodules.mjs
package-utils.mjs
prepare-egs-types.mjs
generate-api-docs.mjs
docs/
architecture.md
ai/
vibe-coding-guide.md
external/
egs-core/
splat-transform/
```
## Structural Roles
- Root package: workspace entry point for dependency preparation, renderer build, API generation, website checks, and release packaging.
- `packages/renderer/`: public renderer package source. `src/index.ts` is the public API barrel; package-local files hold math, events, animation, loader, and utility namespaces.
- `website/`: Astro application for home, manual, API reference, examples, and Playground. Route modules live under `pages/`; shared UI lives under `layouts/` and `components/`; route-independent data helpers live under `utils/`.
- `website/src/playground/`: route-local Playground runtime, Monaco integration, renderer adapter, camera control, and example-facing types.
- `website/src/content/`: manual pages, manual assets, and paired example metadata/source files.
- `scripts/`: shared Node automation for submodule readiness, EGS declarations, renderer packaging, API HTML generation, content validation, and clean tasks.
- `external/egs-core`: upstream submodule consumed by workspace packages and renderer packaging scripts.
- `external/splat-transform`: required workspace package used by the manual and examples workflow.
## Dependency Direction
```text
external/egs-core workspace packages
-> scripts/prepare-egs-types.mjs
-> renderer type checks and declaration bundling
packages/renderer/src/index.ts
-> scripts/build-package.mjs
-> packages/renderer/dist
-> website Playground runtime, examples, and type hints
packages/renderer/src/index.ts
-> scripts/generate-api-docs.mjs
-> website/.generated/api/
-> website pages
```
`website/` may depend on `@manycore/aholo-viewer` through the workspace package. The renderer package should not depend on the website. Website `dev`, `build`, and `check` first run the renderer build, then regenerate API HTML and manifest data.
`external/egs-core` is an upstream dependency submodule. `external/splat-transform` is a required workspace package. Scripts may read external sources and generate dependency outputs needed for local builds, but repository changes should not hand-edit upstream code unless a task explicitly targets that package.
## Root Command Graph
Root scripts keep the workspace build order explicit:
```text
pnpm dev
-> .egs:types
-> .renderer:build
-> .docs:api
-> check:content
-> .site:dev
pnpm check
-> .egs:types
-> .renderer:check
-> .renderer:build
-> .docs:api
-> check:content
-> .site:check
pnpm build
-> build:website
-> .egs:types
-> .renderer:build
-> .docs:api
-> .site:build
```
Targeted commands preserve the same boundaries: `check:renderer` prepares EGS types and runs the renderer type check, `check:website` prepares renderer/API outputs before Astro checks, and `docs:api` prepares EGS types before generating API HTML and manifest data.
## API Documentation Flow
1. Export public API from `packages/renderer/src/index.ts`.
2. Add concise JSDoc comments to public classes, functions, interfaces, and types.
3. Run `pnpm docs:api`.
4. The generated HTML fragments are written to the ignored local directory `website/.generated/api/{locale}/`.
5. A generated manifest beside those fragments drives API navigation, metadata, and table-of-contents data.
6. Astro API routes inline the TypeDoc HTML through the same DocsLayout flow used by the Manual.
Do not edit generated API HTML or manifest data by hand. `pnpm dev`, `pnpm build`, and `pnpm check` regenerate it before the website starts.
## Playground Flow
Examples live in the Astro content tree as paired metadata and source files:
```text
website/src/content/examples/
basic-scene.json
basic-scene.ts
```
The JSON file holds title, description, tags, accent, and order. The same-named TypeScript file is imported with `?raw`, passed to Monaco, and rendered in the Playground preview. Playground URLs support:
- `example`: selected preset slug.
- `code`: `lz-string` compressed editor source.
Resetting or switching presets clears custom `code` and returns to a clean example URL.
Playground type hints use `packages/renderer/dist/index.d.ts` through the workspace package. Do not add a second handwritten renderer declaration file under `website/src/playground/`.
## Content Validation
`pnpm check:content` scans manual pages, localized slugs, heading-depth parity, example source pairs, manual image references, orphan manual images, and internal-only documentation links. `pnpm check` and `pnpm check:website` run it before Astro checks.
## Website Style Layers
Website styles are split by responsibility so global changes stay small and feature surfaces remain easy to reason about:
```text
website/src/styles/
theme.css Design tokens: color, type, radii, shadows, layout widths
global.css Reset, base document styles, buttons, code, simple primitives
site.css Header, language/theme controls, listing-page shell
home.css Immersive home page and home-only interaction states
examples.css Examples list and example detail viewer chrome
docs.css Manual/API documentation layout and prose
playground.css Playground workspace, editor, preview, inspector chrome
```
`BaseLayout.astro` imports `theme.css`, `global.css`, and `site.css`. Feature pages import their own feature stylesheet only when needed. Avoid moving feature-specific selectors back into `global.css`.
The home page intentionally keeps a darker immersive style around the 3D canvas. Docs, API, examples, and Playground should stay lighter, more restrained, and tool-like.
## Build Outputs
- Website output: `website/dist/`
- Astro cache: `website/.astro/`
- Renderer output: `packages/renderer/dist/`
- `index.js`: bundled public runtime.
- `index.d.ts`: bundled public declarations, including upstream EGS types used by exported symbols.
- `splat-worker.js`: bundled worker referenced by the runtime.
Root-level `dist/` and `.astro/` are stale and should not exist after the workspace split.
Ignored generated folders can be removed when a clean workspace is needed, but they should be produced by commands rather than hand-edited.
## Architecture Improvement Opportunities
1. Keep API navigation metadata generated from the same source as API HTML.
`scripts/generate-api-docs.mjs` derives API namespaces, categories, entries, and TOC data from `packages/renderer/src/index.ts`. Keep any future API navigation changes in that generated manifest flow to avoid drift when public namespaces change.
2. Keep Playground integration behind the website adapter.
`website/src/playground/renderer-adapter.ts` is the boundary between Monaco-transpiled examples, preview lifecycle, camera control, runtime stats, and `@manycore/aholo-viewer`. New preview behavior should stay in the website adapter unless it is a reusable renderer capability that belongs in `packages/renderer/`.
3. Make external EGS consumption more declarative.
`prepare-egs-types.mjs` discovers and prepares EGS declarations, while `build-package.mjs` discovers runtime packages for esbuild aliases. A small allow-list or manifest for consumed EGS packages would make renderer packaging easier to audit and less dependent on directory traversal.
4. Treat generated output ownership as an architectural contract.
`packages/renderer/dist/`, `website/.generated/api/`, and temporary API cache folders are command-owned. Any new generated surface should also have an owning script, stale-file cleanup, and a validation path before it is referenced by website routes or package exports.
5. Keep validation mapped to changed surfaces.
Current checks are well split between renderer, website, content, and package build flows. As the site grows, add new validation to the narrowest existing command first, then wire it into `check` only when it protects a cross-area contract.
+5
View File
@@ -0,0 +1,5 @@
# ChangeLOG
## 1.0.0
- 发包正式包
+21
View File
@@ -0,0 +1,21 @@
The MIT License
Copyright © 2026 MANYCORE, INC.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+22
View File
@@ -0,0 +1,22 @@
# splat-dev-server
A 3DGS dev server
## Requirement
- node >= 22.22.1
## Usage
```bash
npm install @manycore/aholo-splat-dev-server -g
splat-dev-server [options] <dir>
Options:
--help Show help [boolean]
--version Show version number [boolean]
-a, --address Address to listen [string] [default: "127.0.0.1"]
-p, --port Port to listen [number] [default: 3000]
```
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env node
import yargs from 'yargs/yargs';
import { hideBin } from 'yargs/helpers';
import { merge } from '../src/merge.js';
import packageJson from '../package.json' with { type: 'json' }
const argv = yargs(hideBin(process.argv))
.version(packageJson.version)
.usage('merge-lod -i <meta-files...> -o <output_dir>')
.option('input', {
alias: 'i',
array: true,
type: 'string',
demandOption: true,
description: 'Input lod meta files(lod-meta.json)'
})
.option('output', {
alias: 'o',
type: 'string',
demandOption: true,
description: 'Output directory'
})
.argv;
merge(argv.input, argv.output);
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env node
import yargs from 'yargs/yargs';
import { hideBin } from 'yargs/helpers';
import { start } from '../src/server.js';
import packageJson from '../package.json' with { type: 'json' }
const argv = yargs(hideBin(process.argv))
.version(packageJson.version)
.usage('splat-dev-server [options] <dir>')
.option('address', {
alias: 'a',
type: 'string',
default: '127.0.0.1',
describe: 'Address to listen'
})
.option('port', {
alias: 'p',
type: 'number',
default: 3000,
description: 'Port to listen'
})
.argv;
start(argv.address, argv.port, argv._[0]);
+40
View File
@@ -0,0 +1,40 @@
{
"name": "@manycore/aholo-splat-dev-server",
"version": "1.0.1",
"description": "",
"author": "egs",
"repository": {
"type": "git",
"url": "https://github.com/manycoretech/aholo-viewer.git",
"directory": "external/splat-dev-server"
},
"license": "MIT",
"engines": {
"node": ">= 22.22.1"
},
"keywords": [
"aholo",
"cli",
"3d-gaussian-splatting",
"gaussian-splatting",
"server"
],
"type": "module",
"bin": {
"splat-dev-server": "bin/server.js",
"merge-lod": "bin/merge.js"
},
"files": [
"src/",
"CHANGELOG.md"
],
"dependencies": {
"express": "^5.2.1",
"yargs": "^18.0.0"
},
"binary": {
"napi_versions": [
8
]
}
}
+76
View File
@@ -0,0 +1,76 @@
import path from 'node:path';
import fs from 'node:fs/promises';
function mergeBounds(bounds) {
if (!bounds || bounds.length === 0) return { min: [0, 0, 0], max: [0, 0, 0] };
const min = [
Math.min(...bounds.map(b => b.min[0])),
Math.min(...bounds.map(b => b.min[1])),
Math.min(...bounds.map(b => b.min[2]))
];
const max = [
Math.max(...bounds.map(b => b.max[0])),
Math.max(...bounds.map(b => b.max[1])),
Math.max(...bounds.map(b => b.max[2]))
];
return { min, max };
}
export async function merge(input, output) {
try {
await fs.stat(output);
} catch {
await fs.mkdir(output, { recursive: true });
}
const meta = {
magicCode: 0x262834,
type: 'lod-splat',
version: '1.0',
counts: 0,
shDegree: 0,
levels: 5,
forwardBox: { min: [0, 0, 0], max: [0, 0, 0] },
files: [],
permanentFiles: [],
tree: [],
};
const inputMetaList = input.map(i => ({
basedir: path.dirname(i),
file: i,
output: []
}));
let index = 0;
for (const inputMeta of inputMetaList) {
const data = JSON.parse(await fs.readFile(inputMeta.file, 'utf-8'));
meta.counts += data.counts;
meta.shDegree = Math.max(meta.shDegree, data.shDegree);
meta.forwardBox = mergeBounds([meta.forwardBox, data.forwardBox]);
const copyTasks = [];
for (const file of data.files) {
const name = `chunk_${index}${path.extname(file)}`;
inputMeta.output.push(index);
meta.files.push(name);
index++;
const i = path.join(inputMeta.basedir, file);
const o = path.join(path.join(output, name));
console.log(`${i} -> ${o}`);
copyTasks.push(fs.copyFile(i, o));
}
for (const permanentFile of data.permanentFiles) {
meta.permanentFiles.push(inputMeta.output[permanentFile]);
}
for (const node of data.tree) {
meta.tree.push({
bound: node.bound,
lods: node.lods.map(l => ({
...l,
file: inputMeta.output[l.file]
}))
});
}
await Promise.all(copyTasks);
}
await fs.writeFile(path.join(output, 'lod-meta.json'), JSON.stringify(meta), 'utf-8');
}
+51
View File
@@ -0,0 +1,51 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import express from 'express';
function createApp(address, port, publicPath) {
const app = express();
const rootDir = path.resolve(publicPath);
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Range');
if (req.method === 'OPTIONS') {
return res.sendStatus(204);
}
return next();
});
app.get(/lod-meta.json$/, async (req, res, next) => {
const file = path.join(rootDir, req.path);
try {
const content = JSON.parse(await fs.readFile(file, 'utf-8'));
const dirname = path.dirname(req.path);
content.files = content.files.map(f => `http://${address}:${port}${dirname}/${f}`)
res.header('Cache-Control', 'no-cache');
return res.json(content);
} catch {
return next();
}
});
app.use(express.static(rootDir, {
setHeaders: res => {
res.header('Cache-Control', 'no-cache');
}
}));
return app;
}
export function start(address, port, publicPath) {
const app = createApp(address, port, publicPath);
app.listen(port, address, () => {
console.log('\n========================================');
console.log('Splat dev server started');
console.log(`Host: ${address}:${port}`);
console.log(`Root: ${publicPath}`);
console.log(`Base URL: http://${address}:${port}`);
console.log('========================================\n');
});
}
+4
View File
@@ -0,0 +1,4 @@
export default {
'**/*.{ts,tsx}': ['oxfmt --write --no-error-on-unmatched-pattern', 'oxlint --fix --no-error-on-unmatched-pattern'],
'**/*.{md,MD,json,txt,yml,yaml}': ['oxfmt --write --no-error-on-unmatched-pattern'],
};
+23
View File
@@ -0,0 +1,23 @@
import { defineConfig } from 'oxfmt';
export default defineConfig({
printWidth: 120,
singleQuote: true,
arrowParens: 'avoid',
sortPackageJson: false,
endOfLine: 'lf',
ignorePatterns: [
'*.log',
'.env',
'.env.*',
'.codex/',
'.vscode/',
'**/build/**',
'**/dist/**',
// website
'**/.astro/**',
'**/.generated/**',
// externals
'external/splat-dev-server/**',
],
});
+42
View File
@@ -0,0 +1,42 @@
import { defineConfig } from 'oxlint';
export default defineConfig({
env: {
browser: true,
node: true,
es2026: true,
},
plugins: ['eslint', 'typescript', 'unicorn', 'oxc', 'promise', 'import'],
ignorePatterns: [
'.github/*',
'**/build/**',
'**/dist/**',
'**/bin/**',
'**/scripts/**',
// website
'**/.astro/**',
'**/.generated/**',
// externals
'external/splat-dev-server/**',
],
rules: {
'no-console': 'off',
'guard-for-in': 'off',
'no-shadow': 'off',
'no-use-before-define': 'off',
'typescript/prefer-for-of': 'off',
'no-loss-of-precision': 'off',
'no-unused-expressions': [
'deny',
{
allowShortCircuit: true,
allowTernary: true,
},
],
'unicorn/no-new-array': 'off',
'no-extra-boolean-cast': 'off',
'typescript/no-this-alias': 'off',
'typescript/no-duplicate-enum-values': 'off',
'erasing-op': 'off',
},
});
+48
View File
@@ -0,0 +1,48 @@
{
"name": "@manycore/aholo",
"version": "0.1.0",
"private": true,
"packageManager": "pnpm@11.10.0",
"type": "module",
"engines": {
"node": ">=22.22.1"
},
"scripts": {
"prepare": "husky",
"pnpm:devPreinstall": "node scripts/ensure-submodules.mjs",
"dev": "pnpm run .renderer:build && pnpm run .docs:api && pnpm run check:content && pnpm run .site:dev",
"build": "pnpm run build:website",
"build:renderer": "pnpm run .renderer:build",
"build:website": "pnpm run .renderer:build && pnpm run .docs:api && pnpm run .site:build && pnpm run .docs:llm",
"check": "pnpm run check:style && pnpm run .renderer:check && pnpm run .renderer:build && pnpm run .docs:api && pnpm run check:content && pnpm run .site:check && pnpm run .site:build && pnpm run .docs:llm",
"check:style": "oxfmt --check && oxlint",
"check:content": "node scripts/check-content.mjs",
"check:renderer": "pnpm run .renderer:check",
"check:website": "pnpm run .renderer:build && pnpm run .docs:api && pnpm run check:content && pnpm run .site:check",
"clean": "pnpm run .renderer:clean && pnpm run .site:clean",
"docs:api": "pnpm run build:renderer && pnpm run .docs:api",
"format": "oxfmt",
"preview": "pnpm run .site:preview",
".docs:api": "node scripts/generate-api-docs.mjs",
".docs:llm": "node scripts/generate-llm-docs.mjs",
".renderer:build": "pnpm --filter @manycore/aholo-viewer run .build",
".renderer:check": "pnpm --filter @manycore/aholo-viewer run .check",
".renderer:clean": "pnpm --filter @manycore/aholo-viewer run .clean",
".site:dev": "pnpm --filter @manycore/aholo-viewer-website run .dev",
".site:build": "pnpm --filter @manycore/aholo-viewer-website run .build",
".site:preview": "pnpm --filter @manycore/aholo-viewer-website run .preview",
".site:check": "pnpm --filter @manycore/aholo-viewer-website run .check",
".site:clean": "pnpm --filter @manycore/aholo-viewer-website run .clean"
},
"devDependencies": {
"@internal/utils": "workspace:*",
"@manycore/aholo-splat-dev-server": "workspace:*",
"@manycore/aholo-splat-transform": "workspace:*",
"husky": "^9.1.7",
"lint-staged": "^17.0.8",
"oxfmt": "^0.58.0",
"oxlint": "^1.73.0",
"typedoc": "^0.28.20",
"typescript": "^6.0.3"
}
}
+75
View File
@@ -0,0 +1,75 @@
# ChangeLOG
## 1.5.1
1. Fixes
- fix `lod` scheduling to avoid temporarily creating too many active splats.
2. Changes
- move splat sorting ownership to `SplatUtils`; `SplatLoader` now only parses files.
## 1.5.0
1. Features
- add `Splatting.pack.forceUnstableEnabled` to force the complete rendering pipeline.
- add `Splatting.sort.frustumCullingEnabled` to pre-cull splats for better performance.
- this can make black borders during camera rotation more visible, so enable it carefully in production scenes.
- add `Limits` for describing `IRenderer` limits.
- some `Capabilities` fields are now marked as deprecated.
- add `lod.proxy` to reduce the number of runtime `Splat` objects.
- reduces `pack` and `precalculate` GPU cost by 50%-90% in real scenes.
2. Fixes
- fix `SplatHighlightKernel` highlights being offset when `cameraRelativeEnabled` is enabled.
- fix `clear` not taking effect correctly when `MRT` is enabled.
- fix invalid data when using low-precision packing.
- fix the `LodMeta.version` type definition.
3. Changes
- optimize texture-size calculation to improve rendering performance for scenes with a single `Splat` object.
- move sorting to a dedicated worker to avoid long parsing tasks blocking sorting.
## 1.4.1
1. Fixes
- fix `packCameraRelativeCenterIsDirty` being incorrectly evaluated as `true` when camera-relative center packing is disabled.
## 1.4.0
1. Features
- add `texture-loader`, now `downloadTexture` is a generic texture load
- supported types: image types(png, jpg, webp, etc...), ktx2 and dds
- add grouped `Splatting` config: `pack`, `raster`, `sort`, and `composite`.
- add high-precision and camera-relative splat packing, plus high-precision sorting.
- `combineSplatData` target `SplatData` support.
2. Changes
- adjust splat-related default parameters from the underlying renderer pipeline.
## 1.3.0
1. Features
- `SplatUtils`add support for `center` and `ellipsoid`
> **`constructor` has been changed, migrate: `new SplatBVH(operator)` -> `new BVH(SplatCenterPrimitiveSource(operator))`**
2. Fixes
- `SplatUtils`state texture type change to `r8uint`
## 1.2.9
1. Features
- use `api-extractor` to rollup dts.
- add `esz` and `spzV4` format support.
2. Fixes
- fix typing for `MeshBasicMaterial.setValues`
- fix typing for `MeshPhongMaterial.setValues`
- Simplify some material typings
- fix type only classes
- cleanup `package.json`
## 1.1.0
1. Features
- upgrade packages: `typescript@^6.0.3`, `tslib@^2.8.1`
- sync base packages
- remove unused module `render-cloud`
## 1.0.0
1. Features
- First release
+21
View File
@@ -0,0 +1,21 @@
The MIT License
Copyright © 2026 MANYCORE, INC.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+28
View File
@@ -0,0 +1,28 @@
# @manycore/aholo-viewer
`@manycore/aholo-viewer` is the full-featured npm package for building browser-based Aholo Viewer applications. It includes the rendering runtime and TypeScript declarations for rendering 3D Gaussian Splatting (3DGS) and mesh content in WebGL/WebGL2-capable browsers.
## Install
```bash
npm install --save @manycore/aholo-viewer
```
The package is ESM-only and ships TypeScript declarations through `dist/index.d.ts`.
## Quick Start
Follow the [Getting Started guide](https://aholojs.dev/en-US/manual/getting-started/) to create a Vite app, initialize a viewer, load a 3DGS splat, and render the first frame.
## Documentation
- Website: https://aholojs.dev/
- Getting Started: https://aholojs.dev/en-US/manual/getting-started/
- API Reference: https://aholojs.dev/en-US/api/
- Examples: https://aholojs.dev/en-US/examples/
- AI-friendly docs: https://aholojs.dev/llms.txt
- AI coding skill: https://github.com/manycoretech/aholo-viewer/blob/master/docs/ai/skills/use-aholo-viewer/SKILL.md
## License
MIT
+62
View File
@@ -0,0 +1,62 @@
{
"name": "@manycore/aholo-viewer",
"version": "1.5.1",
"description": "A compact TypeScript renderer core used by the Aholo Viewer website and examples.",
"author": "egs",
"repository": {
"type": "git",
"url": "https://github.com/manycoretech/aholo-viewer.git",
"directory": "packages/renderer"
},
"license": "MIT",
"keywords": [
"aholo",
"renderer",
"viewer",
"webgl",
"webgl2",
"3d",
"3d-gaussian-splatting",
"gaussian-splatting",
"canvas",
"html5"
],
"type": "module",
"sideEffects": false,
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"files": [
"dist",
"CHANGELOG.md"
],
"scripts": {
".build": "pnpm run .egs:types && node ../../scripts/build-package.mjs",
".check": "tsc -p tsconfig.json --noEmit",
".clean": "node ../../scripts/clean-package.mjs",
".egs:types": "node ../../scripts/prepare-egs-types.mjs"
},
"publishConfig": {
"access": "public"
},
"devDependencies": {
"@qunhe/egs": "workspace:*",
"@qunhe/egs-animation": "workspace:*",
"@qunhe/egs-draco-loader": "workspace:*",
"@qunhe/egs-gltf-loader": "workspace:*",
"@qunhe/egs-lib": "workspace:*",
"@qunhe/egs-splat-loader": "workspace:*",
"@qunhe/egs-splat-utils": "workspace:*",
"@qunhe/egs-texture-loader": "workspace:*",
"@types/node": "^22.20.0",
"esbuild": "^0.28.1",
"tslib": "^2.8.1",
"typescript": "^6.0.3"
}
}
+13
View File
@@ -0,0 +1,13 @@
export {
AnimationPlugin,
SkeletonUpdatedEvent,
AnimationMixer,
AnimationAction,
AnimationFinishEvent,
Skeleton,
type AnimationClip,
type KeyframeTrack,
Blend,
Loop,
InterpolationMode,
} from '@qunhe/egs-animation';
+1
View File
@@ -0,0 +1 @@
export { type ParseConfig, parseDracoBuffer } from '@qunhe/egs-draco-loader';
+1
View File
@@ -0,0 +1 @@
export { SplatSortedEvent, SplatRenderingStabilityChangedEvent } from '@qunhe/egs';
+1
View File
@@ -0,0 +1 @@
export { type LoaderConfig, type ParseResult, loadGLTF } from '@qunhe/egs-gltf-loader';
+376
View File
@@ -0,0 +1,376 @@
export {
type Viewer,
type Viewport,
type IViewerContext,
createViewerContext,
Object3D,
Scene3D,
Camera as Camera3D,
PerspectiveCamera,
OrthographicCamera,
type Light,
AmbientLight,
DirectionalLight,
PointLight,
SpotLight,
BufferAttribute,
InstancedBufferAttribute,
BufferGeometry,
FatLineBufferGeometry,
type Material,
PointsMaterial,
LineBasicMaterial as LineMaterial,
FatLineMaterial,
Splat,
Mesh,
Sprite,
Points,
Line,
LineSegments,
FatLineSegments,
InstanceMesh,
WebGLDrawMode as DrawMode,
WebGLTextureWrap as SamplerWrap,
WebGLTextureFilter as SamplerFilter,
Side,
DepthModes,
Blending,
WebGLBlendingEquation as BlendingEquation,
WebGLBlendingDst as BlendingFactor,
WebGLStencilOp as StencilOp,
WebGLStencilFunc as StencilFunc,
Layers,
type Intersection,
Raycaster,
TypeAssert,
DrawableRenderMode,
ToneMapping,
FilterTarget,
SplatState,
BackgroundMode,
SourceTexture,
TextureDimension,
TextureViewDimension,
TextureFormat,
// Math
Box3,
Color,
type ReadonlyColor,
Euler,
Frustum,
Matrix3,
type ReadonlyMatrix3,
Matrix4,
type ReadonlyMatrix4,
Plane,
Quaternion,
Ray,
Sphere,
Vector2,
type ReadonlyVector2,
Vector3,
type ReadonlyVector3,
Vector4,
type ReadonlyVector4,
} from '@qunhe/egs';
export { IntersectContainment, BVH, type BVHNode, type BVHSource } from '@qunhe/egs-lib';
export { downloadTexture, type LoaderOptions } from '@qunhe/egs-texture-loader';
import * as Events from './events.js';
import * as Animation from './animation.js';
import * as SplatLoader from './splat-loader.js';
import * as DracoLoader from './draco-loader.js';
import * as GLTFLoader from './gltf-loader.js';
import * as SplatUtils from './splat-utils.js';
import {
Application,
BackgroundMode,
type EngineInitializeConfig,
Viewer,
type Viewport,
Texture,
ToneMapping,
Vector3,
Color,
Vector4,
setViewerConfig as originSetViewerConfig,
MeshBasicMaterial as BaseMeshBasicMaterial,
MeshPhongMaterial as BaseMeshPhongMaterial,
SpriteMaterial as BaseSpriteMaterial,
type SourceTexture,
__INTERNAL__,
} from '@qunhe/egs';
import InstancedBufferGeometry = __INTERNAL__.InstancedBufferGeometry;
import CompressedSplat = __INTERNAL__.CompressedSplat;
import SuperCompressedSplat = __INTERNAL__.SuperCompressedSplat;
import SogSplat = __INTERNAL__.SogSplat;
export const MeshBasicMaterial: typeof BaseMeshBasicMaterial<SourceTexture> = BaseMeshBasicMaterial;
export const MeshPhongMaterial: typeof BaseMeshPhongMaterial<SourceTexture> = BaseMeshPhongMaterial;
export const SpriteMaterial: typeof BaseSpriteMaterial<SourceTexture> = BaseSpriteMaterial;
export interface IBasicBackgroundConfig {
color?: Color;
alpha?: number;
texture?: Texture | null;
}
export interface IEnvMapBackgroundConfig {
texture?: Texture;
luma?: number;
verticalRotation?: number;
horizonRotation?: number;
reverseVertical?: boolean;
reverseHorizon?: boolean;
}
export interface IGradientBackgroundConfig {
skyColor?: Color;
groundColor?: Color;
}
export interface ISkyBackgroundConfig {
enablePreSkyMap?: boolean;
luminance?: number;
turbidity?: number;
rayleigh?: number;
mieCoefficient?: number;
mieDirectionalG?: number;
}
export interface IBackgroundPluginConfig {
/**
* @default `true`
*/
enabled?: boolean;
/**
* up for background rendering, will effect ground and background
* @default `Vector3(0.0, 0.0, 1.0)`
*/
up?: Vector3;
ground?: {
/**
* enable ground grid
* @default `true`
*/
enabled?: boolean;
/**
* default ground grid size
* @default `1000000`
*/
gridSize?: number;
/**
* grid A gap
* @default `500`
*/
gridGapSizeA?: number;
/**
* grid A color
* @default `Color(1.0, 1.0, 1.0)`
*/
colorA?: Color;
/**
* grid A line width
* @default `1`
*/
lineWidthA?: number;
/**
* grid B gap
* @default `5000`
*/
gridGapSizeB?: number;
/**
* grid B color
* @default `Color(1.0, 1.0, 1.0)`
*/
colorB?: Color;
/**
* grid B line width
* @default `1`
*/
lineWidthB?: number;
/**
* enable ground color shading, by default will only render noise for ground.
* @default `false`
*/
isGroundColorEnabled?: boolean;
/**
* ground color
* @default `Color(0.7, 0.7, 0.7)`
*/
groundColor?: Color;
};
background?: {
/**
* @default `BackgroundMode.BasicBackground`
*/
active?: BackgroundMode;
/**
* basic color and texture
* active by `BackgroundMode.BasicBackground`
*/
basic?: IBasicBackgroundConfig;
/**
* sphere envmap
* active by `BackgroundMode.EnvMapBackground`
*/
envmap?: IEnvMapBackgroundConfig;
/**
* sphere gradient
* active by `BackgroundMode.GradientBackground`
*/
gradient?: IGradientBackgroundConfig;
/**
* sky box
* active by `BackgroundMode.SkyBackground`
*/
sky?: ISkyBackgroundConfig;
};
}
export interface ICompositePluginConfig {
enabled?: boolean;
multiSamplingEnabled?: boolean;
staticFrameCacheEnabled?: boolean;
}
export interface ISplattingPluginConfig {
enabled?: boolean;
/**
* gaussian pack
*/
pack?: {
highPrecisionEnabled?: boolean;
precalculateEnabled?: boolean;
cameraRelativeEnabled?: boolean;
cameraRelativeUpdateDistanceThreshold?: number;
sortedLayoutEnabled?: boolean;
};
/**
* gaussian raster
*/
raster?: {
mode?: __INTERNAL__.SplattingRenderMode;
preBlurAmount?: number;
blurAmount?: number;
focalAdjustment?: number;
maxStdDev?: number;
maxPixelRadius?: number;
detailCullingThreshold?: number;
normalizedFalloff?: boolean;
selectedColor?: Vector4;
};
/**
* gaussian sorting
*/
sort?: {
highPrecisionEnabled?: boolean;
minIntervalMs?: number;
sortSplatDistance?: number;
sortSplatCoorient?: number;
sortCameraDistance?: number;
sortCameraCoorient?: number;
depthBias?: number;
};
/**
* composite before output
*/
composite?: {
enabled?: boolean;
highPrecisionEnabled?: boolean;
};
/**
* tone mapping functions
*/
toneMapping?: {
enabled?: boolean;
toneMapping?: ToneMapping;
exposure?: number;
};
/**
* gaussian center highlight
*/
highlightKernel?: {
enabled?: boolean;
size?: number;
color?: number;
};
}
export interface ITaaPluginConfig {
/**
* enabled static TAA
* @default `true`
*/
enabled?: boolean;
}
export interface IPipelineConfig {
/**
* background and ground
*/
Background?: IBackgroundPluginConfig;
/**
* output Composite, used to optimize multi viewport rendering
*/
Composite?: ICompositePluginConfig;
/**
* gaussian splatting
*/
Splatting?: ISplattingPluginConfig;
/**
* static TAA
*/
TAA?: ITaaPluginConfig;
}
export interface IViewerConfig {
/**
* pixel ratio used for rendering
* @default `1.0`
* @remarks
* default the renderer will use physical pixel in rendering.
* if `devicePixelRatio` is large on some device, will cause performance issue, could set it to `1 / devicePixelRatio`
*/
pixelRatio?: number;
/**
* render pipeline config
*/
pipeline?: IPipelineConfig;
}
export function createViewer(name: string, container: HTMLElement, config: EngineInitializeConfig) {
return Application.getInstance().createViewer(name, container, config);
}
export function setViewerConfig(ctx: Viewer | Viewport, config: IViewerConfig) {
const viewerConfig: __INTERNAL__.IViewerConfig = {};
if (config.pixelRatio) {
viewerConfig.canvas = {
renderPixelRatio: config.pixelRatio,
};
}
if (config.pipeline) {
viewerConfig.effects = {
__INTERNAL__: config.pipeline,
};
}
originSetViewerConfig(viewerConfig, ctx.config);
}
export {
InstancedBufferGeometry,
CompressedSplat,
SuperCompressedSplat,
SogSplat,
Events,
Animation,
SplatLoader,
DracoLoader,
GLTFLoader,
SplatUtils,
};
+21
View File
@@ -0,0 +1,21 @@
export {
KsplatFile,
LccFile,
PlyFile,
SogFile,
SplatFile,
SpzFile,
type IFile,
SplatData,
RawSplatData,
CompressedSplatData,
SuperCompressedSplatData,
SogSplatData,
ISamplerFormat,
type ISampler,
SplatFileType,
SplatPackType,
parseSplatData,
type ISingleSplat,
detectSplatFileType,
} from '@qunhe/egs-splat-loader';
+16
View File
@@ -0,0 +1,16 @@
export {
SplatOperator,
SplatCenterPrimitiveSource,
SplatEllipsoidPrimitiveSource,
type SplatCenterPrimitive,
type SplatEllipsoidPrimitive,
combineSplatData,
computeDenseBox,
createSplat,
createSplatData,
createSplatModifyData,
transformSplatFile,
type LodMeta,
type LodConfig,
LodSplat,
} from '@qunhe/egs-splat-utils';
+15
View File
@@ -0,0 +1,15 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"declarationMap": true,
"noEmit": false,
"sourceMap": true,
"strictPropertyInitialization": false,
"strictFunctionTypes": false,
"useUnknownInCatchVariables": false,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src"],
"exclude": ["dist"]
}
@@ -0,0 +1,2 @@
*.node filter=lfs diff=lfs merge=lfs -text
*.dll filter=lfs diff=lfs merge=lfs -text
@@ -0,0 +1 @@
splat-transform.node
@@ -0,0 +1,23 @@
Language: Cpp
BasedOnStyle: LLVM
UseTab: Never
IndentWidth: 4
TabWidth: 4
BreakBeforeBraces: Attach
AllowShortIfStatementsOnASingleLine: false
IndentCaseLabels: false
ColumnLimit: 0
AccessModifierOffset: -4
PointerAlignment: Left
SpaceAfterTemplateKeyword: false
Cpp11BracedListStyle: false
SpaceBeforeCpp11BracedList: true
AlignAfterOpenBracket: DontAlign
ContinuationIndentWidth: 4
InsertNewlineAtEOF: true
# BreakAfterOpenBracketBracedList: true
# BreakAfterOpenBracketFunction: true
# BreakAfterOpenBracketIf: true
# BreakBeforeCloseBracketBracedList: true
# BreakBeforeCloseBracketFunction: true
# BreakBeforeCloseBracketIf: true
@@ -0,0 +1,74 @@
cmake_minimum_required(VERSION 3.31)
cmake_policy(SET CMP0091 NEW)
cmake_policy(SET CMP0042 NEW)
project(binding)
find_package(Threads REQUIRED)
find_package(Eigen3 CONFIG REQUIRED)
find_package(nanoflann CONFIG REQUIRED)
find_package(cmake-js CONFIG REQUIRED)
find_package(WebP CONFIG REQUIRED)
find_package(libavif CONFIG REQUIRED)
set(BINDING_BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}" CACHE PATH "node binding output dir")
add_library(${PROJECT_NAME} SHARED)
list(APPEND BINDING_SOURCE_FILES
"${CMAKE_CURRENT_SOURCE_DIR}/src/thread_pool.cpp"
# GAUSSIAN SPLAT
"${CMAKE_CURRENT_SOURCE_DIR}/src/splat/splat.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/splat/splat_lod.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/splat/splat_block.cpp"
# NODE API FILES
"${CMAKE_CURRENT_SOURCE_DIR}/src/node/api_splat.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/node/api_spatial.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/node/api_thread_pool.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/node/api_webp.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/node/api_avif.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/node/binding.cpp"
)
list(APPEND BINDING_HEADER_FILES
"${CMAKE_CURRENT_SOURCE_DIR}/include/future_helpers.h"
"${CMAKE_CURRENT_SOURCE_DIR}/include/container_helpers.h"
"${CMAKE_CURRENT_SOURCE_DIR}/include/thread_pool.h"
# GAUSSIAN SPLAT
"${CMAKE_CURRENT_SOURCE_DIR}/include/splat/splat.h"
"${CMAKE_CURRENT_SOURCE_DIR}/include/splat/splat_lod.h"
"${CMAKE_CURRENT_SOURCE_DIR}/include/splat/splat_block.h"
# NODE API FILES
"${CMAKE_CURRENT_SOURCE_DIR}/include/node/api_buffer.h"
"${CMAKE_CURRENT_SOURCE_DIR}/include/node/api_splat.h"
"${CMAKE_CURRENT_SOURCE_DIR}/include/node/api_spatial.h"
"${CMAKE_CURRENT_SOURCE_DIR}/include/node/api_thread_pool.h"
"${CMAKE_CURRENT_SOURCE_DIR}/include/node/api_webp.h"
"${CMAKE_CURRENT_SOURCE_DIR}/include/node/api_avif.h"
)
target_sources(${PROJECT_NAME}
PRIVATE
${BINDING_SOURCE_FILES}
PRIVATE FILE_SET binding_private_headers
TYPE HEADERS
BASE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/include"
FILES ${BINDING_HEADER_FILES}
)
target_link_libraries(${PROJECT_NAME}
PRIVATE cmake_js::node Threads::Threads Eigen3::Eigen nanoflann::nanoflann WebP::webp avif
)
set_target_properties(
${PROJECT_NAME} PROPERTIES
PREFIX ""
OUTPUT_NAME "splat-transform"
SUFFIX ".node"
CXX_EXTENSIONS OFF
COMPILE_FEATURES cxx_std_20
COMPILE_OPTIONS $<$<CXX_COMPILER_ID:MSVC>:/Zc:__cplusplus>
LINK_OPTIONS $<$<AND:$<CXX_COMPILER_ID:GNU,Clang>,$<NOT:$<PLATFORM_ID:Apple>>>:-Wl,--exclude-libs,ALL>
COMPILE_DEFINITIONS "UNICODE;_UNICODE"
INTERPROCEDURAL_OPTIMIZATION ON
LIBRARY_OUTPUT_DIRECTORY ${BINDING_BINARY_DIR}
RUNTIME_OUTPUT_DIRECTORY ${BINDING_BINARY_DIR}
RUNTIME_OUTPUT_DIRECTORY_RELEASE ${BINDING_BINARY_DIR}
LIBRARY_OUTPUT_DIRECTORY_RELEASE ${BINDING_BINARY_DIR}
)
@@ -0,0 +1,155 @@
{
"version": 10,
"cmakeMinimumRequired": {
"major": 3,
"minor": 31,
"patch": 0
},
"configurePresets": [
{
"$comment": [
"default build config for any env",
"needs: vcpkg",
"vcpkg install root should be in environments as VCPKG_ROOT"
],
"name": "default",
"toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake",
"cacheVariables": {
"CMAKE_PREFIX_PATH": "${sourceDir}/cmake"
}
},
{
"name": "x64-windows-static",
"$comment": ["build config for x64 windows"],
"inherits": "default",
"cacheVariables": {
"VCPKG_TARGET_TRIPLET": "x64-windows-static"
},
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
}
},
{
"name": "arm64-windows-static",
"$comment": ["build config for arm64 windows"],
"inherits": "default",
"cacheVariables": {
"VCPKG_TARGET_TRIPLET": "arm64-windows-static"
},
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
}
},
{
"name": "x64-linux",
"$comment": ["build config for x64 linux"],
"inherits": "default",
"cacheVariables": {
"VCPKG_TARGET_TRIPLET": "x64-linux",
"CMAKE_MAKE_PROGRAM": "/usr/bin/ninja",
"CMAKE_C_COMPILER": "/usr/bin/cc",
"CMAKE_CXX_COMPILER": "/usr/bin/c++"
},
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Linux"
}
},
{
"name": "arm64-linux",
"$comment": ["build config for arm64 linux"],
"inherits": "default",
"cacheVariables": {
"VCPKG_TARGET_TRIPLET": "arm64-linux",
"CMAKE_ASM_NASM_COMPILER": "/usr/bin/nasm",
"CMAKE_MAKE_PROGRAM": "/usr/bin/ninja",
"CMAKE_C_COMPILER": "/usr/bin/cc",
"CMAKE_CXX_COMPILER": "/usr/bin/c++"
},
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Linux"
}
},
{
"name": "arm64-osx",
"$comment": ["build config for arm64 osx"],
"inherits": "default",
"cacheVariables": {
"VCPKG_TARGET_TRIPLET": "arm64-osx",
"CMAKE_MAKE_PROGRAM": "/opt/homebrew/bin/ninja",
"CMAKE_ASM_NASM_COMPILER": "/opt/homebrew/bin/nasm",
"CMAKE_C_COMPILER": "/usr/bin/cc",
"CMAKE_CXX_COMPILER": "/usr/bin/c++"
},
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Darwin"
}
}
],
"buildPresets": [
{
"name": "default",
"configurePreset": "default",
"jobs": 8
},
{
"name": "x64-windows-static",
"inherits": "default",
"configurePreset": "x64-windows-static",
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
}
},
{
"name": "arm64-windows-static",
"inherits": "default",
"configurePreset": "arm64-windows-static",
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
}
},
{
"name": "x64-linux",
"inherits": "default",
"configurePreset": "x64-linux",
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Linux"
}
},
{
"name": "arm64-linux",
"inherits": "default",
"configurePreset": "arm64-linux",
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Linux"
}
},
{
"name": "arm64-osx",
"inherits": "default",
"configurePreset": "arm64-osx",
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Darwin"
}
}
]
}
@@ -0,0 +1,47 @@
import path from 'node:path';
import { createRequire } from 'node:module';
import yargs from 'yargs/yargs';
import { hideBin } from 'yargs/helpers';
import { spawnProcess } from '@internal/utils/process.js';
const __dirname = import.meta.dirname;
const require = createRequire(import.meta.url);
await yargs(hideBin(process.argv))
.scriptName('build')
.showHelpOnFail(false)
.strict()
.command({
command: ['build', '$0'],
builder(argv) {
argv.option('preset', {
describe: 'CMake preset',
default: 'default',
}).option('target', {
describe: 'Platform target',
demandOption: true,
});
},
async handler(argv) {
await spawnProcess(
'node',
[
require.resolve('cmake-js/bin/cmake-js'),
'build',
`--CDBINDING_BINARY_DIR=${path.resolve(`../splat-transform-${argv.target}`)}`,
'--',
'--preset',
argv.preset,
],
{
cwd: process.cwd(),
env: {
...process.env,
FORCE_COLOR: 1,
PATH: path.join(__dirname, '.`/node_modules/.bin') + path.delimiter + process.env.PATH,
},
},
).promise;
},
})
.parseAsync();
@@ -0,0 +1,25 @@
# cmake-js config for find_package
add_library(cmake_js::node INTERFACE IMPORTED)
if(MSVC)
if(CMAKE_JS_NODELIB_DEF AND CMAKE_JS_NODELIB_TARGET)
# Generate node.lib
add_custom_command(OUTPUT ${CMAKE_JS_NODELIB_TARGET}
COMMAND ${CMAKE_AR} /def:${CMAKE_JS_NODELIB_DEF} /out:${CMAKE_JS_NODELIB_TARGET} ${CMAKE_STATIC_LINKER_FLAGS}
)
add_custom_target(node_lib DEPENDS ${CMAKE_JS_NODELIB_TARGET})
target_link_libraries(cmake_js::node INTERFACE ${CMAKE_JS_NODELIB_TARGET})
add_dependencies(cmake_js::node node_lib)
endif()
endif()
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
target_link_libraries(cmake_js::node INTERFACE "delayimp.lib")
endif()
set_target_properties(cmake_js::node PROPERTIES
INTERFACE_COMPILE_FEATURES cxx_std_14
INTERFACE_COMPILE_DEFINITIONS "NAPI_VERSION=8"
INTERFACE_INCLUDE_DIRECTORIES "${CMAKE_JS_INC}"
INTERFACE_SOURCES "${CMAKE_JS_SRC}"
)
@@ -0,0 +1,39 @@
#pragma once
#include <initializer_list>
#include <ranges>
#include <vector>
namespace helpers::container {
template<typename R, typename V>
concept range_of = std::ranges::range<R> && std::same_as<std::ranges::range_value_t<R>, V>;
template<std::ranges::range T, std::ranges::range U>
requires std::same_as<std::ranges::range_value_t<T>, std::ranges::range_value_t<U>> &&
requires(T&& dst, U&& src) {
#if __cpp_lib_containers_ranges
dst.append_range(std::forward<U>(src));
#else
dst.insert(dst.end(), src.begin(), src.end());
#endif
}
inline void append_range(T&& dst, U&& src) {
#if __cpp_lib_containers_ranges
dst.append_range(std::forward<U>(src));
#else
dst.insert(dst.end(), src.begin(), src.end());
#endif
}
template<std::ranges::range T>
requires requires(T&& dst, std::initializer_list<std::ranges::range_value_t<T>>&& src) {
#if __cpp_lib_containers_ranges
dst.append_range(std::move(src));
#else
dst.insert(dst.end(), src.begin(), src.end());
#endif
}
inline void append_range(T&& dst, std::initializer_list<std::ranges::range_value_t<T>>&& src) {
append_range<T, std::initializer_list<std::ranges::range_value_t<T>>&&>(std::forward<T>(dst), std::move(src));
}
} // namespace helpers::container
@@ -0,0 +1,135 @@
#pragma once
#include <concepts>
#include <cstddef>
#include <exception>
#include <functional>
#include <future>
#include <ranges>
#include <type_traits>
namespace helpers::future {
namespace detail {
template<typename T>
struct future_value;
template<typename T>
struct future_value<std::future<T>> {
using type = T;
};
template<typename T>
concept future_range = std::ranges::range<T> && requires {
typename future_value<std::ranges::range_value_t<std::remove_cvref_t<T>>>::type;
};
template<future_range T>
using future_range_value_t = future_value<std::ranges::range_value_t<std::remove_cvref_t<T>>>::type;
} // namespace detail
struct FailurePolicyDrainAll;
// Stops draining after the first observed failure. This does not cancel remaining
// work; use only when unfinished tasks do not depend on this stack frame.
struct FailurePolicyStopOnFirstFailure;
template<typename T>
concept future_failure_policy = std::same_as<T, FailurePolicyDrainAll> || std::same_as<T, FailurePolicyStopOnFirstFailure>;
template<future_failure_policy P = FailurePolicyDrainAll, detail::future_range T>
requires std::same_as<detail::future_range_value_t<T>, void>
void drain_futures(T&& futures) {
std::exception_ptr first_exception;
for (auto& future : futures) {
try {
future.get();
} catch (...) {
if (!first_exception) {
first_exception = std::current_exception();
}
if constexpr (std::is_same_v<P, FailurePolicyStopOnFirstFailure>) {
break;
}
}
}
if (first_exception) {
std::rethrow_exception(first_exception);
}
}
template<future_failure_policy P = FailurePolicyDrainAll, detail::future_range T, typename F>
requires std::same_as<detail::future_range_value_t<T>, void> &&
std::invocable<F&, size_t>
void drain_futures(T&& futures, F&& f) {
std::exception_ptr first_exception;
size_t index = 0;
for (auto& future : futures) {
try {
future.get();
std::invoke(f, index);
} catch (...) {
if (!first_exception) {
first_exception = std::current_exception();
}
if constexpr (std::is_same_v<P, FailurePolicyStopOnFirstFailure>) {
break;
}
}
index++;
}
if (first_exception) {
std::rethrow_exception(first_exception);
}
}
template<future_failure_policy P = FailurePolicyDrainAll, detail::future_range T, typename F>
requires(!std::same_as<detail::future_range_value_t<T>, void>) &&
std::invocable<F&, detail::future_range_value_t<T>&&>
void drain_futures(T&& futures, F&& f) {
std::exception_ptr first_exception;
for (auto& future : futures) {
try {
std::invoke(f, future.get());
} catch (...) {
if (!first_exception) {
first_exception = std::current_exception();
}
if constexpr (std::is_same_v<P, FailurePolicyStopOnFirstFailure>) {
break;
}
}
}
if (first_exception) {
std::rethrow_exception(first_exception);
}
}
template<future_failure_policy P = FailurePolicyDrainAll, detail::future_range T, typename F>
requires(!std::same_as<detail::future_range_value_t<T>, void>) &&
std::invocable<F&, detail::future_range_value_t<T>&&, size_t>
void drain_futures(T&& futures, F&& f) {
std::exception_ptr first_exception;
size_t index = 0;
for (auto& future : futures) {
try {
std::invoke(f, future.get(), index);
} catch (...) {
if (!first_exception) {
first_exception = std::current_exception();
}
if constexpr (std::is_same_v<P, FailurePolicyStopOnFirstFailure>) {
break;
}
}
index++;
}
if (first_exception) {
std::rethrow_exception(first_exception);
}
}
} // namespace helpers::future
@@ -0,0 +1,9 @@
#pragma once
#include <napi.h>
namespace node_api::imaging {
Napi::Value avif_encode_rgba(const Napi::CallbackInfo& info);
Napi::Value avif_encode_rgba_batched(const Napi::CallbackInfo& info);
Napi::Value avif_decode_rgba(const Napi::CallbackInfo& info);
Napi::Value avif_decode_rgba_batched(const Napi::CallbackInfo& info);
} // namespace node_api::imaging
@@ -0,0 +1,92 @@
#pragma once
#include <memory>
#include <napi.h>
#include <span>
#include <vector>
namespace node_api::buffer {
template<typename T, typename D = std::default_delete<T>>
class UniqueBufferFinalizer {
public:
UniqueBufferFinalizer() noexcept = default;
UniqueBufferFinalizer(const UniqueBufferFinalizer<T, D>& other) = delete;
UniqueBufferFinalizer(UniqueBufferFinalizer<T, D>&& other) noexcept = default;
UniqueBufferFinalizer(std::unique_ptr<T, D>&& ptr) noexcept : ptr(std::move(ptr)) {}
UniqueBufferFinalizer(T* ptr) noexcept : ptr(ptr) {}
void operator()(Napi::Env env, T* data) noexcept {}
inline static Napi::Buffer<T> make_buffer(Napi::Env& env, std::unique_ptr<T, D>&& ptr, size_t size) noexcept {
auto finalizer = UniqueBufferFinalizer(std::move(ptr));
auto data_ptr = finalizer.ptr.get();
return Napi::Buffer<T>::NewOrCopy(env, data_ptr, size, std::move(finalizer));
}
protected:
std::unique_ptr<T, D> ptr;
};
template<typename T, typename D = std::default_delete<std::vector<T>>>
class UniqueVecBufferFinalizer : public UniqueBufferFinalizer<std::vector<T>, D> {
public:
void operator()(Napi::Env env, T* data) noexcept {}
template<typename U>
inline static Napi::Buffer<T> make_buffer(Napi::Env& env, U&& data) noexcept {
auto finalizer = UniqueVecBufferFinalizer(std::forward<U>(data));
auto data_ptr = finalizer.ptr->data();
auto data_size = finalizer.ptr->size();
return Napi::Buffer<T>::NewOrCopy(env, data_ptr, data_size, std::move(finalizer));
}
};
template<typename T>
class SharedBufferFinalizer {
public:
SharedBufferFinalizer() noexcept = default;
SharedBufferFinalizer(const SharedBufferFinalizer<T>& other) noexcept = default;
SharedBufferFinalizer(SharedBufferFinalizer<T>&& other) noexcept = default;
SharedBufferFinalizer(const std::shared_ptr<T>& ptr) noexcept : ptr(ptr) {}
SharedBufferFinalizer(std::shared_ptr<T>&& ptr) noexcept : ptr(std::move(ptr)) {}
void operator()(Napi::Env env, T* data) noexcept {}
template<typename U>
requires std::is_same_v<std::remove_reference_t<U>, std::shared_ptr<T>>
inline static Napi::Buffer<T> make_buffer(Napi::Env& env, U&& ptr, size_t size) noexcept {
auto finalizer = SharedBufferFinalizer(std::forward<U>(ptr));
auto data_ptr = finalizer.ptr.get();
return Napi::Buffer<T>::NewOrCopy(env, data_ptr, size, std::move(finalizer));
}
template<typename U>
requires std::is_same_v<std::remove_reference_t<U>, std::shared_ptr<T>>
inline static Napi::Buffer<T> make_buffer(Napi::Env& env, U&& ptr, size_t offset, size_t size) noexcept {
auto finalizer = SharedBufferFinalizer(std::forward<U>(ptr));
auto data_ptr = finalizer.ptr.get();
return Napi::Buffer<T>::NewOrCopy(env, data_ptr + offset, size, std::move(finalizer));
}
protected:
std::shared_ptr<T> ptr;
};
template<typename T>
class SharedVecBufferFinalizer : public SharedBufferFinalizer<std::vector<T>> {
public:
void operator()(Napi::Env env, T* data) noexcept {}
template<typename U>
inline static Napi::Buffer<T> make_buffer(Napi::Env& env, U&& data) noexcept {
auto finalizer = SharedVecBufferFinalizer(std::forward<U>(data));
auto data_ptr = finalizer.ptr->data();
auto data_size = finalizer.ptr->size();
return Napi::Buffer<T>::NewOrCopy(env, data_ptr, data_size, std::move(finalizer));
}
template<typename U>
inline static Napi::Buffer<T> make_buffer(Napi::Env& env, U&& data, size_t offset, size_t size) noexcept {
auto finalizer = SharedVecBufferFinalizer(std::forward<U>(data));
auto data_ptr = finalizer.ptr->data();
return Napi::Buffer<T>::NewOrCopy(env, data_ptr + offset, size, std::move(finalizer));
}
};
} // namespace node_api::buffer
@@ -0,0 +1,6 @@
#pragma once
#include <napi.h>
namespace node_api::spatial {
Napi::Value cluster_average(const Napi::CallbackInfo& info);
}
@@ -0,0 +1,7 @@
#pragma once
#include <napi.h>
namespace node_api::splat {
Napi::Value generate_lod(const Napi::CallbackInfo& info);
Napi::Value split(const Napi::CallbackInfo& info);
} // namespace node_api::splat
@@ -0,0 +1,26 @@
#pragma once
#include <algorithm>
#include <bit>
#include <cstddef>
#include <napi.h>
#include <thread>
#include <thread_pool.h>
namespace node_api::threading {
class ThreadPool : public Napi::ObjectWrap<ThreadPool> {
public:
static Napi::FunctionReference Init(Napi::Env env, Napi::Object exports);
ThreadPool(const Napi::CallbackInfo& info);
::threading::ThreadPool& impl() noexcept;
ThreadPool() = delete;
private:
static size_t calc_thread_count(const Napi::CallbackInfo& info);
Napi::Value thread_count(const Napi::CallbackInfo& info);
Napi::Value task_count(const Napi::CallbackInfo& info);
::threading::ThreadPool impl_;
};
} // namespace node_api::threading
@@ -0,0 +1,8 @@
#pragma once
#include <napi.h>
namespace node_api::imaging {
Napi::Value webp_encode_rgba_lossless(const Napi::CallbackInfo& info);
Napi::Value webp_encode_rgba(const Napi::CallbackInfo& info);
Napi::Value webp_decode_rgba(const Napi::CallbackInfo& info);
} // namespace node_api::imaging
@@ -0,0 +1,60 @@
#pragma once
#include <array>
#include <eigen3/Eigen/Dense>
#include <memory>
#include <vector>
namespace splat {
class SH {
public:
SH() noexcept;
SH(size_t size);
SH(const SH& other);
SH(SH&& other) noexcept;
SH& operator=(const SH& other);
SH& operator=(SH&& other) noexcept;
void swap(SH& other) noexcept;
SH& set_zero() noexcept;
SH& add_multiplied(const SH& other, float scalar) noexcept;
const float& operator[](size_t index) const;
float& operator[](size_t index);
float* data() noexcept;
size_t size() const noexcept;
~SH() noexcept;
private:
void reset() noexcept;
size_t size_;
std::unique_ptr<float[]> ptr;
};
struct Gaussian {
Eigen::Vector3f mean;
Eigen::Vector3f scale;
Eigen::Vector4f rotation;
Eigen::Matrix3f covariance;
SH sh;
float opacity;
Eigen::AlignedBox3f bounding_box;
void compute_covariance();
void decompose_covariance();
void compute_bounding_box(float k = 3.0f);
float area() const;
};
struct Splat {
std::vector<Gaussian> gaussians;
Eigen::AlignedBox3f bounding_box;
void compute_bounding_box();
void compute_compact_bounding_box();
};
} // namespace splat
@@ -0,0 +1,17 @@
#pragma once
#include <splat/splat.h>
#include <type_traits>
#include <vector>
namespace splat::block {
namespace detail {
std::vector<Splat> split(const Splat& splat, size_t max_block_size);
} // namespace detail
template<typename T>
requires std::is_same_v<std::remove_reference_t<T>, Splat>
std::vector<Splat> split(T&& input, double precision) {
auto max_block_size = static_cast<size_t>(static_cast<double>(input.gaussians.size()) * precision);
return detail::split(input, max_block_size);
}
} // namespace splat::block
@@ -0,0 +1,43 @@
#pragma once
#include <container_helpers.h>
#include <cstdint>
#include <splat/splat.h>
#include <vector>
namespace splat::lod {
namespace detail {
Splat reduce_gaussians(size_t id, const Splat& splat, size_t target_count, float scale_boost, size_t max_step);
}
struct SplatLod {
std::vector<Splat> splats;
std::vector<uint32_t> levels;
};
struct SplatLevelParameters {
float precision;
float scale_boost;
};
template<typename T>
requires requires(T) {
requires helpers::container::range_of<T, SplatLevelParameters>;
}
inline void generate_lod(size_t id, SplatLod& lod, T&& parameters, size_t min_size, size_t max_step) {
auto base_count = lod.splats.back().gaussians.size();
for (auto& parameter : parameters) {
auto target_count = std::max<size_t>(static_cast<size_t>(std::floor(static_cast<double>(base_count) * parameter.precision)), 1);
auto& current = lod.splats.back();
if (current.gaussians.size() > min_size && target_count < current.gaussians.size()) {
auto generated = detail::reduce_gaussians(
id,
current,
target_count,
parameter.scale_boost,
max_step);
lod.splats.push_back(std::move(generated));
}
lod.levels.push_back(lod.splats.size() - 1);
}
}
} // namespace splat::lod
@@ -0,0 +1,122 @@
#pragma once
#include <atomic>
#include <concepts>
#include <condition_variable>
#include <functional>
#include <future>
#include <memory>
#include <mutex>
#include <queue>
#include <shared_mutex>
#include <stdexcept>
#include <thread>
#include <type_traits>
#include <utility>
#include <vector>
namespace threading {
class ThreadPool {
public:
ThreadPool(size_t thread_count = std::thread::hardware_concurrency());
ThreadPool(ThreadPool&& other) noexcept;
template<typename F, typename... Args,
typename ReturnType = std::invoke_result_t<std::decay_t<F>, std::decay_t<Args>...>>
requires std::invocable<std::decay_t<F>, std::decay_t<Args>...>
std::future<ReturnType> submit_task(F&& f, Args&&... args);
size_t thread_count() const noexcept;
size_t task_count() const noexcept;
ThreadPool& operator=(ThreadPool&& other) noexcept;
void stop() noexcept;
~ThreadPool() noexcept;
ThreadPool(const ThreadPool& other) = delete;
ThreadPool& operator=(const ThreadPool& other) = delete;
private:
#ifdef __cpp_lib_move_only_function
using Task = std::move_only_function<void()>;
#else
using Task = std::function<void()>;
#endif
struct Worker {
std::atomic<bool> stopped { false };
std::thread thread;
void stop() noexcept;
void clean_up() noexcept;
~Worker() noexcept;
};
struct State {
std::vector<std::unique_ptr<Worker>> workers;
std::queue<Task> tasks;
mutable std::shared_mutex queue_mutex;
mutable std::shared_mutex worker_mutex;
std::condition_variable_any cv;
std::atomic<bool> stopped { false };
void stop() noexcept;
~State() noexcept;
};
static void worker_loop(State* state, Worker* worker);
std::unique_ptr<State> state;
};
template<typename F, typename... Args, typename ReturnType>
requires std::invocable<std::decay_t<F>, std::decay_t<Args>...>
std::future<ReturnType> ThreadPool::submit_task(F&& f, Args&&... args) {
auto* state = this->state.get();
if (state == nullptr || state->stopped.load(std::memory_order_relaxed)) {
throw std::runtime_error("Cannot submit tasks to stopped thread pool");
}
#ifdef __cpp_lib_move_only_function
auto promise = std::promise<ReturnType>();
auto future = promise.get_future();
{
auto lk = std::lock_guard(state->queue_mutex);
if (state->stopped.load(std::memory_order_relaxed)) {
throw std::runtime_error("Cannot submit tasks to stopped thread pool");
}
state->tasks.emplace([f = std::forward<F>(f), ... args = std::forward<Args>(args), promise = std::move(promise)]() mutable noexcept -> void {
try {
if constexpr (std::is_void_v<ReturnType>) {
std::invoke(std::move(f), std::move(args)...);
promise.set_value();
} else {
promise.set_value(std::invoke(std::move(f), std::move(args)...));
}
} catch (...) {
promise.set_exception(std::current_exception());
}
});
}
#else
auto promise = std::make_shared<std::promise<ReturnType>>();
auto future = promise->get_future();
{
auto lk = std::lock_guard(state->queue_mutex);
if (state->stopped.load(std::memory_order_relaxed)) {
throw std::runtime_error("Cannot submit tasks to stopped thread pool");
}
state->tasks.emplace([f = std::forward<F>(f), ... args = std::forward<Args>(args), promise = promise]() noexcept -> void {
try {
if constexpr (std::is_void_v<ReturnType>) {
std::invoke(std::move(f), std::move(args)...);
promise->set_value();
} else {
promise->set_value(std::invoke(std::move(f), std::move(args)...));
}
} catch (...) {
promise->set_exception(std::current_exception());
}
});
}
#endif
state->cv.notify_one();
return future;
}
} // namespace threading
@@ -0,0 +1,23 @@
{
"name": "@internal/splat-transform-native-source",
"version": "0.0.1",
"private": true,
"type": "module",
"dependencies": {
"cmake-js": "^8.0.0",
"node-addon-api": "^8.9.0",
"yargs": "^18.0.0"
},
"scripts": {
"build:native:win32-arm64": "node ./build.js build --preset arm64-windows-static --target win32-arm64-msvc",
"build:native:win32-x64": "node ./build.js build --preset x64-windows-static --target win32-x64-msvc",
"build:native:linux-arm64-gnu": "node ./build.js build --preset arm64-linux --target linux-arm64-gnu",
"build:native:linux-x64-gnu": "node ./build.js build --preset x64-linux --target linux-x64-gnu",
"build:native:darwin-arm64": "node ./build.js build --preset arm64-osx --target darwin-arm64"
},
"binary": {
"napi_versions": [
8
]
}
}
@@ -0,0 +1,301 @@
#include <avif/avif_cxx.h>
#include <bit>
#include <future_helpers.h>
#include <memory>
#include <node/api_avif.h>
#include <node/api_buffer.h>
#include <node/api_thread_pool.h>
#include <span>
#include <thread>
#include <thread_pool.h>
#include <vector>
namespace {
class RawData {
public:
RawData() noexcept : data_(AVIF_DATA_EMPTY) {}
RawData(RawData&& other) noexcept : data_(other.data_) {
other.data_ = AVIF_DATA_EMPTY;
}
RawData(avifRWData&& data) noexcept : data_(data) {
data = AVIF_DATA_EMPTY;
}
RawData(const RawData& other) = delete;
void operator()(Napi::Env env, uint8_t* data) noexcept {}
RawData& operator=(RawData&& other) noexcept {
if (this != &other) {
if (this->data_.data != nullptr) {
avifRWDataFree(&this->data_);
}
this->data_ = other.data_;
other.data_ = AVIF_DATA_EMPTY;
}
return *this;
}
RawData& operator=(const RawData& other) = delete;
avifRWData& data() {
return this->data_;
}
const avifRWData& data() const {
return this->data_;
}
Napi::Buffer<uint8_t> make_buffer(Napi::Env& env) {
auto ptr = this->data_.data;
auto size = this->data_.size;
return Napi::Buffer<uint8_t>::NewOrCopy(env, ptr, size, std::move(*this));
}
~RawData() {
if (this->data_.data != nullptr) {
avifRWDataFree(&this->data_);
}
}
private:
avifRWData data_;
};
class RGBImageData {
public:
RGBImageData() noexcept : data_({ 0 }) {}
RGBImageData(RGBImageData&& other) noexcept : data_(other.data_) {
other.data_ = { 0 };
}
RGBImageData(avifRGBImage&& data) noexcept : data_(data) {
data = { 0 };
}
RGBImageData(const RGBImageData& other) = delete;
void operator()(Napi::Env env, uint8_t* data) noexcept {}
RGBImageData& operator=(RGBImageData&& other) noexcept {
if (this != &other) {
if (this->data_.pixels != nullptr) {
avifRGBImageFreePixels(&this->data_);
}
this->data_ = other.data_;
other.data_ = { 0 };
}
return *this;
}
RGBImageData& operator=(const RGBImageData& other) = delete;
avifRGBImage& data() {
return this->data_;
}
const avifRGBImage& data() const {
return this->data_;
}
Napi::Buffer<uint8_t> make_buffer(Napi::Env& env) {
auto ptr = this->data_.pixels;
auto size = this->data_.rowBytes * this->data_.height;
return Napi::Buffer<uint8_t>::NewOrCopy(env, ptr, size, std::move(*this));
}
~RGBImageData() {
if (this->data_.pixels != nullptr) {
avifRGBImageFreePixels(&this->data_);
}
}
private:
avifRGBImage data_;
};
RawData avif_encode_rga_impl(std::span<uint8_t> pixels, int32_t width, int32_t height, int32_t quality, int32_t max_threads) {
avif::EncoderPtr encoder = nullptr;
avifRGBImage rgb;
std::memset(&rgb, 0, sizeof(rgb));
avif::ImagePtr image(avifImageCreate(width, height, 8, AVIF_PIXEL_FORMAT_YUV444));
avifRGBImageSetDefaults(&rgb, image.get());
rgb.pixels = pixels.data();
rgb.rowBytes = 4 * rgb.width;
avifResult convertResult = avifImageRGBToYUV(image.get(), &rgb);
if (convertResult != AVIF_RESULT_OK) {
throw std::runtime_error("Failed to convert to YUV(A)");
}
encoder.reset(avifEncoderCreate());
if (encoder == nullptr) {
throw std::runtime_error("Out of memory");
}
encoder->quality = quality;
encoder->qualityAlpha = AVIF_QUALITY_LOSSLESS;
encoder->maxThreads = max_threads;
avifResult addImageResult = avifEncoderAddImage(encoder.get(), image.get(), 1, AVIF_ADD_IMAGE_FLAG_SINGLE);
if (addImageResult != AVIF_RESULT_OK) {
throw std::runtime_error("Failed to add image to encoder");
}
RawData avifOutput;
avifResult finishResult = avifEncoderFinish(encoder.get(), &avifOutput.data());
if (finishResult != AVIF_RESULT_OK) {
throw std::runtime_error("Failed to finish encode");
}
return avifOutput;
}
RGBImageData avif_decode_rgba_impl(std::span<uint8_t> data, int32_t max_threads) {
avif::DecoderPtr decoder(avifDecoderCreate());
if (decoder == nullptr) {
throw std::runtime_error("Out of memory");
}
decoder->maxThreads = max_threads;
avifResult result = avifDecoderSetIOMemory(decoder.get(), data.data(), data.size());
if (result != AVIF_RESULT_OK) {
throw std::runtime_error("Cannot set IO on avifDecoder");
}
result = avifDecoderParse(decoder.get());
if (result != AVIF_RESULT_OK) {
throw std::runtime_error("Failed to decode image");
}
result = avifDecoderNextImage(decoder.get());
if (result != AVIF_RESULT_OK) {
throw std::runtime_error("Failed to get image");
}
RGBImageData rgb;
avifRGBImageSetDefaults(&rgb.data(), decoder->image);
result = avifRGBImageAllocatePixels(&rgb.data());
if (result != AVIF_RESULT_OK) {
throw std::runtime_error("Allocation of RGB samples failed");
}
result = avifImageYUVToRGB(decoder->image, &rgb.data());
if (result != AVIF_RESULT_OK) {
throw std::runtime_error("Conversion from YUV failed");
}
return rgb;
}
} // namespace
namespace node_api::imaging {
Napi::Value avif_encode_rgba(const Napi::CallbackInfo& info) {
auto env = info.Env();
if (info.Length() < 4 || !info[0].IsBuffer() || !info[1].IsNumber() || !info[2].IsNumber() || !info[3].IsNumber()) {
Napi::TypeError::New(env, "Wrong Arguments").ThrowAsJavaScriptException();
return env.Null();
}
auto buffer = info[0].As<Napi::Buffer<uint8_t>>();
auto width = info[1].As<Napi::Number>().Int32Value();
auto height = info[2].As<Napi::Number>().Int32Value();
auto quality = info[3].As<Napi::Number>().Int32Value();
return avif_encode_rga_impl(
std::span(buffer.Data(), buffer.Length()),
width, height,
quality, static_cast<int32_t>(std::thread::hardware_concurrency()))
.make_buffer(env);
}
Napi::Value avif_encode_rgba_batched(const Napi::CallbackInfo& info) {
auto env = info.Env();
if (info.Length() < 2 || !info[0].IsArray() || !info[1].IsObject()) {
Napi::TypeError::New(env, "Wrong Arguments").ThrowAsJavaScriptException();
return env.Null();
}
constexpr auto max_thread = 16;
auto& pool = node_api::threading::ThreadPool::Unwrap(info[1].As<Napi::Object>())->impl();
auto thread_count = pool.thread_count();
auto inputs = info[0].As<Napi::Array>();
auto input_count = inputs.Length();
auto outputs = Napi::Array::New(env, input_count);
{
auto futures = std::vector<std::future<RawData>>();
futures.reserve(inputs.Length());
for (auto i = 0; i < input_count; i++) {
auto input = inputs[i].AsValue().As<Napi::Object>();
auto buffer = input.Get("data").As<Napi::Buffer<uint8_t>>();
auto width = input.Get("width").As<Napi::Number>().Int32Value();
auto height = input.Get("height").As<Napi::Number>().Int32Value();
auto quality = input.Get("quality").As<Napi::Number>().Int32Value();
futures.push_back(pool.submit_task(avif_encode_rga_impl, std::span(buffer.Data(), buffer.Length()), width, height, quality, max_thread));
}
helpers::future::drain_futures(futures, [&](RawData&& data, size_t i) {
outputs[i] = data.make_buffer(env);
});
}
return outputs;
}
Napi::Value avif_decode_rgba(const Napi::CallbackInfo& info) {
auto env = info.Env();
if (info.Length() < 1 || !info[0].IsBuffer()) {
Napi::TypeError::New(env, "Wrong Arguments").ThrowAsJavaScriptException();
return env.Null();
}
auto buffer = info[0].As<Napi::Buffer<uint8_t>>();
auto&& rgb = avif_decode_rgba_impl(std::span(buffer.Data(), buffer.Length()),
static_cast<int32_t>(std::thread::hardware_concurrency()));
auto object = Napi::Object::New(env);
{
object.Set("width", Napi::Number::New(env, rgb.data().width));
object.Set("height", Napi::Number::New(env, rgb.data().height));
object.Set("data", rgb.make_buffer(env));
}
return object;
}
Napi::Value avif_decode_rgba_batched(const Napi::CallbackInfo& info) {
auto env = info.Env();
if (info.Length() < 2 || !info[0].IsArray() || !info[1].IsObject()) {
Napi::TypeError::New(env, "Wrong Arguments").ThrowAsJavaScriptException();
return env.Null();
}
constexpr auto max_thread = 16;
auto& pool = node_api::threading::ThreadPool::Unwrap(info[1].As<Napi::Object>())->impl();
auto thread_count = pool.thread_count();
auto inputs = info[0].As<Napi::Array>();
auto input_count = inputs.Length();
auto outputs = Napi::Array::New(env, input_count);
{
auto futures = std::vector<std::future<RGBImageData>>();
futures.reserve(inputs.Length());
for (auto i = 0; i < input_count; i++) {
auto input = inputs[i].AsValue().As<Napi::Buffer<uint8_t>>();
futures.push_back(pool.submit_task(avif_decode_rgba_impl, std::span(input.Data(), input.Length()), max_thread));
}
helpers::future::drain_futures(futures, [&](RGBImageData&& rgb, size_t i) {
auto object = Napi::Object::New(env);
object.Set("width", Napi::Number::New(env, rgb.data().width));
object.Set("height", Napi::Number::New(env, rgb.data().height));
object.Set("data", rgb.make_buffer(env));
outputs[i] = object;
});
}
return outputs;
}
} // namespace node_api::imaging
@@ -0,0 +1,150 @@
#include <algorithm>
#include <cstdint>
#include <future>
#include <future_helpers.h>
#include <memory>
#include <node/api_spatial.h>
#include <node/api_thread_pool.h>
#include <random>
#include <span>
#include <thread>
#include <thread_pool.h>
#include <vector>
namespace {
struct PartialAverage {
std::unique_ptr<float[]> sums;
std::unique_ptr<size_t[]> counts;
};
} // namespace
namespace node_api::spatial {
Napi::Value cluster_average(const Napi::CallbackInfo& info) {
auto env = info.Env();
if (info.Length() < 5 || !info[0].IsArray() || !info[1].IsBuffer() || !info[2].IsNumber() || !info[3].IsArray() || !info[4].IsObject()) {
Napi::TypeError::New(env, "Wrong Arguments").ThrowAsJavaScriptException();
return env.Null();
}
auto input_table = info[0].As<Napi::Array>();
auto input_labels = info[1].As<Napi::Buffer<uint32_t>>();
auto cluster_count = static_cast<size_t>(info[2].As<Napi::Number>().Uint32Value());
auto output_table = info[3].As<Napi::Array>();
auto thread_pool_wrapper = node_api::threading::ThreadPool::Unwrap(info[4].As<Napi::Object>());
auto col_size = input_table.Length();
auto data_table = std::vector<std::span<float>>();
auto output = std::vector<std::span<float>>();
data_table.reserve(col_size);
output.reserve(col_size);
for (auto i = 0; i < col_size; i++) {
auto data = input_table.Get(i).As<Napi::Buffer<float>>();
auto out = output_table.Get(i).As<Napi::Buffer<float>>();
data_table.emplace_back(data.Data(), data.Length());
output.emplace_back(out.Data(), out.Length());
}
auto labels = std::span<uint32_t>(input_labels.Data(), input_labels.Length());
auto row_size = data_table[0].size();
auto& pool = thread_pool_wrapper->impl();
auto partials = std::vector<PartialAverage>();
auto fallback_rows = std::vector<size_t>(cluster_count, 0);
if (row_size > 0) {
std::random_device random_device;
auto seed = (static_cast<uint64_t>(random_device()) << 32) ^ static_cast<uint64_t>(random_device());
auto rng = std::mt19937_64(seed);
auto row_distribution = std::uniform_int_distribution<size_t>(0, row_size - 1);
for (auto& row_index : fallback_rows) {
row_index = row_distribution(rng);
}
}
auto process_rows = [&](size_t begin, size_t end) -> PartialAverage {
auto partial = PartialAverage {
.sums = std::make_unique<float[]>(cluster_count * col_size),
.counts = std::make_unique<size_t[]>(cluster_count),
};
for (auto row_index = begin; row_index < end; row_index++) {
auto cluster_index = labels[row_index];
if (cluster_index >= cluster_count) {
continue;
}
partial.counts[cluster_index]++;
auto sum_offset = static_cast<size_t>(cluster_index) * col_size;
for (auto column_index = 0; column_index < col_size; column_index++) {
partial.sums[sum_offset + column_index] += data_table[column_index][row_index];
}
}
return partial;
};
auto reduce_clusters = [&](size_t begin, size_t end) {
for (auto cluster_index = begin; cluster_index < end; cluster_index++) {
size_t count = 0;
for (auto column_index = 0; column_index < col_size; column_index++) {
output[column_index][cluster_index] = 0.0f;
}
auto sum_offset = cluster_index * col_size;
for (auto& partial : partials) {
count += partial.counts[cluster_index];
for (auto column_index = 0; column_index < col_size; column_index++) {
output[column_index][cluster_index] += partial.sums[sum_offset + column_index];
}
}
if (count == 0) {
auto row_index = fallback_rows[cluster_index];
for (auto column_index = 0; column_index < col_size; column_index++) {
output[column_index][cluster_index] = data_table[column_index][row_index];
}
} else {
auto inv_count = 1.0f / static_cast<float>(count);
for (auto column_index = 0; column_index < col_size; column_index++) {
output[column_index][cluster_index] *= inv_count;
}
}
}
};
{
size_t used_thread_count = std::min(pool.thread_count(), row_size);
auto row_tasks = std::vector<std::future<PartialAverage>>();
row_tasks.reserve(used_thread_count);
auto rows_per_thread = (row_size + used_thread_count - 1) / used_thread_count;
// process rows, calculate sum per thread.
for (size_t thread_index = 0; thread_index < used_thread_count; thread_index++) {
auto begin = thread_index * rows_per_thread;
auto end = std::min(begin + rows_per_thread, row_size);
row_tasks.push_back(pool.submit_task(process_rows, begin, end));
}
partials.reserve(row_tasks.size());
helpers::future::drain_futures(row_tasks, [&](PartialAverage&& data) {
partials.push_back(std::move(data));
});
}
{
size_t used_thread_count = std::min(pool.thread_count(), cluster_count);
auto tasks = std::vector<std::future<void>>();
tasks.reserve(used_thread_count);
auto clusters_per_thread = (cluster_count + used_thread_count - 1) / used_thread_count;
// reduce threaded cluster result
for (size_t thread_index = 0; thread_index < used_thread_count; thread_index++) {
auto begin = thread_index * clusters_per_thread;
auto end = std::min(begin + clusters_per_thread, cluster_count);
tasks.push_back(pool.submit_task(reduce_clusters, begin, end));
}
helpers::future::drain_futures(tasks);
}
return env.Null();
}
} // namespace node_api::spatial
@@ -0,0 +1,357 @@
#include <algorithm>
#include <atomic>
#include <cassert>
#include <container_helpers.h>
#include <future_helpers.h>
#include <node/api_buffer.h>
#include <node/api_splat.h>
#include <node/api_thread_pool.h>
#include <numeric>
#include <ranges>
#include <span>
#include <splat/splat.h>
#include <splat/splat_block.h>
#include <splat/splat_lod.h>
#include <thread_pool.h>
namespace {
enum SplatTable {
SPLAT_TABLE_MEAN_X = 0,
SPLAT_TABLE_MEAN_Y = 1,
SPLAT_TABLE_MEAN_Z = 2,
SPLAT_TABLE_SCALE_X = 3,
SPLAT_TABLE_SCALE_Y = 4,
SPLAT_TABLE_SCALE_Z = 5,
SPLAT_TABLE_QUAT_X = 6,
SPLAT_TABLE_QUAT_Y = 7,
SPLAT_TABLE_QUAT_Z = 8,
SPLAT_TABLE_QUAT_W = 9,
SPLAT_TABLE_COLOR_R = 10,
SPLAT_TABLE_COLOR_G = 11,
SPLAT_TABLE_COLOR_B = 12,
SPLAT_TABLE_OPACITY = 13,
SPLAT_TABLE_SH_OFFSET = 14
};
inline splat::Splat read_splat(const std::vector<std::span<float>>& buffers, size_t sh_size, threading::ThreadPool& pool) {
auto thread_count = pool.thread_count();
auto read_count = std::atomic<size_t>(0);
auto gaussian_count = buffers[0].size();
auto gaussians = std::vector<splat::Gaussian>(gaussian_count);
auto gaussian_per_thread = std::max(gaussian_count / thread_count, static_cast<size_t>(1));
auto futures = std::vector<std::future<void>>();
auto used_threads = std::min(gaussian_count, thread_count);
futures.reserve(used_threads);
read_count.store(0, std::memory_order_release);
auto process_gaussian = [&, buffers](size_t start, size_t count) {
for (auto i = 0; i < count; i++) {
auto index = start + i;
auto& gaussian = gaussians[index];
gaussian.mean = Eigen::Vector3f(
buffers[SPLAT_TABLE_MEAN_X][index],
buffers[SPLAT_TABLE_MEAN_Y][index],
buffers[SPLAT_TABLE_MEAN_Z][index]);
gaussian.scale = Eigen::Vector3f(
buffers[SPLAT_TABLE_SCALE_X][index],
buffers[SPLAT_TABLE_SCALE_Y][index],
buffers[SPLAT_TABLE_SCALE_Z][index]);
gaussian.rotation = Eigen::Vector4f(
buffers[SPLAT_TABLE_QUAT_X][index],
buffers[SPLAT_TABLE_QUAT_Y][index],
buffers[SPLAT_TABLE_QUAT_Z][index],
buffers[SPLAT_TABLE_QUAT_W][index]);
gaussian.opacity = buffers[SPLAT_TABLE_OPACITY][index];
// SH
gaussian.sh = ::splat::SH(sh_size + 3);
gaussian.sh[0] = buffers[SPLAT_TABLE_COLOR_R][index];
gaussian.sh[1] = buffers[SPLAT_TABLE_COLOR_G][index];
gaussian.sh[2] = buffers[SPLAT_TABLE_COLOR_B][index];
for (auto j = 3; j < gaussian.sh.size(); j++) {
gaussian.sh[j] = buffers[SPLAT_TABLE_SH_OFFSET + j - 3][index];
}
gaussian.rotation.normalize();
gaussian.compute_covariance();
gaussian.compute_bounding_box();
}
read_count.fetch_add(count, std::memory_order_acq_rel);
};
for (auto i = 0; i < used_threads; i++) {
futures.push_back(pool.submit_task(process_gaussian, gaussian_per_thread * i, gaussian_per_thread));
}
process_gaussian(gaussian_per_thread * used_threads, gaussian_count - gaussian_per_thread * used_threads);
helpers::future::drain_futures(futures);
assert(read_count.load(std::memory_order_acquire) == gaussian_count);
auto splat = splat::Splat {
.gaussians = std::move(gaussians),
.bounding_box = Eigen::AlignedBox3f {},
};
splat.compute_bounding_box();
return splat;
}
inline std::vector<Napi::Float32Array> write_splat(
Napi::Env& env,
size_t buffer_count,
const splat::Splat& splat,
threading::ThreadPool& pool) {
auto buffers = std::vector<Napi::Float32Array>();
auto buffer_spans = std::vector<std::span<float>>();
auto thread_count = pool.thread_count();
auto written_count = std::atomic<size_t>(0);
buffers.reserve(buffer_count);
buffer_spans.reserve(buffer_count);
for (auto i = 0; i < buffer_count; i++) {
buffers.push_back(Napi::Float32Array::New(env, splat.gaussians.size()));
buffer_spans.emplace_back(buffers.back().Data(), splat.gaussians.size());
}
auto gaussian_per_thread = std::max(splat.gaussians.size() / thread_count, static_cast<size_t>(1));
auto futures = std::vector<std::future<void>>();
auto used_threads = std::min(splat.gaussians.size(), thread_count);
futures.reserve(used_threads);
written_count.store(0, std::memory_order_release);
// transform gaussian to js readable struct
auto process_gaussian = [&](size_t start, size_t count) {
for (auto i = 0; i < count; i++) {
auto index = start + i;
auto write_index = index;
auto& gaussian = splat.gaussians[index];
buffer_spans[SPLAT_TABLE_MEAN_X][write_index] = gaussian.mean.x();
buffer_spans[SPLAT_TABLE_MEAN_Y][write_index] = gaussian.mean.y();
buffer_spans[SPLAT_TABLE_MEAN_Z][write_index] = gaussian.mean.z();
buffer_spans[SPLAT_TABLE_SCALE_X][write_index] = gaussian.scale.x();
buffer_spans[SPLAT_TABLE_SCALE_Y][write_index] = gaussian.scale.y();
buffer_spans[SPLAT_TABLE_SCALE_Z][write_index] = gaussian.scale.z();
buffer_spans[SPLAT_TABLE_QUAT_X][write_index] = gaussian.rotation.x();
buffer_spans[SPLAT_TABLE_QUAT_Y][write_index] = gaussian.rotation.y();
buffer_spans[SPLAT_TABLE_QUAT_Z][write_index] = gaussian.rotation.z();
buffer_spans[SPLAT_TABLE_QUAT_W][write_index] = gaussian.rotation.w();
buffer_spans[SPLAT_TABLE_COLOR_R][write_index] = gaussian.sh[0];
buffer_spans[SPLAT_TABLE_COLOR_G][write_index] = gaussian.sh[1];
buffer_spans[SPLAT_TABLE_COLOR_B][write_index] = gaussian.sh[2];
buffer_spans[SPLAT_TABLE_OPACITY][write_index] = gaussian.opacity;
for (auto j = 3; j < gaussian.sh.size(); j++) {
buffer_spans[SPLAT_TABLE_SH_OFFSET + j - 3][write_index] = gaussian.sh[j];
}
}
written_count.fetch_add(count, std::memory_order_acq_rel);
};
for (auto i = 0; i < used_threads; i++) {
futures.push_back(pool.submit_task(process_gaussian, gaussian_per_thread * i, gaussian_per_thread));
}
process_gaussian(gaussian_per_thread * used_threads, splat.gaussians.size() - gaussian_per_thread * used_threads);
helpers::future::drain_futures(futures);
assert(written_count.load(std::memory_order_acquire) == splat.gaussians.size());
return buffers;
}
} // namespace
namespace node_api::splat {
Napi::Value generate_lod(const Napi::CallbackInfo& info) {
auto env = info.Env();
if (info.Length() < 7 || !info[0].IsArray() || !info[1].IsNumber() || !info[2].IsBuffer() || !info[3].IsNumber() || !info[4].IsNumber() || !info[5].IsNumber() || !info[6].IsObject()) {
Napi::TypeError::New(env, "Wrong Arguments").ThrowAsJavaScriptException();
return env.Null();
}
auto& pool = node_api::threading::ThreadPool::Unwrap(info[6].As<Napi::Object>())->impl();
size_t thread_count = pool.thread_count();
auto sh_size = info[1].As<Napi::Number>().Uint32Value();
std::vector<::splat::Splat> blocks;
// read & create blocks.
{
auto buffers = std::vector<std::span<float>>();
auto array = info[0].As<Napi::Array>();
buffers.reserve(sh_size + SPLAT_TABLE_SH_OFFSET);
for (auto i = 0; i < sh_size + SPLAT_TABLE_SH_OFFSET; i++) {
auto buffer = array[i].AsValue().As<Napi::Buffer<float>>();
buffers.emplace_back(buffer.Data(), buffer.Length());
}
blocks = ::splat::block::split(
read_splat(buffers, sh_size, pool),
info[3].As<Napi::Number>().DoubleValue());
}
auto level_parameters = info[2].As<Napi::Buffer<::splat::lod::SplatLevelParameters>>();
auto levels = level_parameters.Length();
auto level_parameters_span = std::span(level_parameters.Data() + 1, level_parameters.Length() - 1);
auto results = std::vector<::splat::Splat>();
auto gaussian_count = std::make_unique<std::vector<uint32_t>>();
auto block_boxes = std::make_unique<std::vector<float>>();
auto block_refs = std::make_unique<std::vector<uint32_t>>();
// reverse data, small.
results.reserve(blocks.size() * levels);
gaussian_count->reserve(blocks.size() * levels);
block_boxes->reserve(blocks.size() * 6);
block_refs->reserve(blocks.size() * levels);
{
auto futures = std::vector<std::future<::splat::lod::SplatLod>>();
auto used_threads = std::min(blocks.size(), thread_count);
auto min_size = info[4].As<Napi::Number>().Uint32Value();
auto max_step = info[5].As<Napi::Number>().Uint32Value();
futures.reserve(blocks.size());
auto process_block = [&, levels, max_step](size_t index) -> ::splat::lod::SplatLod {
auto& block = blocks[index];
auto lod = ::splat::lod::SplatLod();
lod.levels.reserve(levels);
lod.splats.reserve(levels);
lod.levels.push_back(0);
lod.splats.push_back(std::move(block));
if (level_parameters_span.size() > 0) {
::splat::lod::generate_lod(index, lod, level_parameters_span, min_size, max_step);
}
return lod;
};
for (auto i = 0; i < blocks.size(); i++) {
futures.push_back(pool.submit_task(process_block, i));
}
helpers::future::drain_futures(futures, [&](::splat::lod::SplatLod block) {
auto base_offset = static_cast<uint32_t>(results.size());
{
auto& bbx = block.splats.front().bounding_box;
helpers::container::append_range(*block_boxes, bbx.min());
helpers::container::append_range(*block_boxes, bbx.max());
}
helpers::container::append_range(*gaussian_count, block.splats | std::views::transform([](::splat::Splat& splat) -> uint32_t {
return static_cast<uint32_t>(splat.gaussians.size());
}));
helpers::container::append_range(*block_refs, block.levels | std::views::transform([base_offset](uint32_t ref) -> uint32_t {
return ref + base_offset;
}));
for (auto& splat : block.splats) {
results.push_back(std::move(splat));
}
});
// release blocks
{
auto _ = std::move(blocks);
}
}
// auto total = std::reduce(gaussian_count->begin(), gaussian_count->end());
auto buffers_per_splat = sh_size + SPLAT_TABLE_SH_OFFSET;
auto buffers = std::vector<Napi::Float32Array>();
buffers.reserve(buffers_per_splat * results.size());
for (auto& splat : results) {
helpers::container::append_range(buffers, write_splat(env, buffers_per_splat, splat, pool));
// free data already transformed.
{
auto _ = std::move(splat);
}
}
auto object = Napi::Object::New(env);
{
auto data = Napi::Array::New(env, buffers.size());
auto offset = 0;
object.Set("data", data);
for (auto i = 0; i < buffers.size(); i++) {
data[i] = buffers[i];
}
auto _ = std::move(buffers);
}
object.Set("blockBoxes", node_api::buffer::UniqueVecBufferFinalizer<float>::make_buffer(env, std::move(block_boxes)));
object.Set("blockRefs", node_api::buffer::UniqueVecBufferFinalizer<uint32_t>::make_buffer(env, std::move(block_refs)));
object.Set("gaussianCount", node_api::buffer::UniqueVecBufferFinalizer<uint32_t>::make_buffer(env, std::move(gaussian_count)));
return object;
}
Napi::Value split(const Napi::CallbackInfo& info) {
auto env = info.Env();
if (info.Length() < 4 || !info[0].IsArray() || !info[1].IsNumber() || !info[2].IsNumber() || !info[3].IsObject()) {
Napi::TypeError::New(env, "Wrong Arguments").ThrowAsJavaScriptException();
return env.Null();
}
auto& pool = node_api::threading::ThreadPool::Unwrap(info[3].As<Napi::Object>())->impl();
size_t thread_count = pool.thread_count();
auto sh_size = info[1].As<Napi::Number>().Uint32Value();
std::vector<::splat::Splat> blocks;
// read & create blocks.
{
auto buffers = std::vector<std::span<float>>();
auto array = info[0].As<Napi::Array>();
buffers.reserve(sh_size + SPLAT_TABLE_SH_OFFSET);
for (auto i = 0; i < sh_size + SPLAT_TABLE_SH_OFFSET; i++) {
auto buffer = array[i].AsValue().As<Napi::Buffer<float>>();
buffers.emplace_back(buffer.Data(), buffer.Length());
}
blocks = ::splat::block::split(
read_splat(buffers, sh_size, pool),
info[2].As<Napi::Number>().DoubleValue());
}
auto buffers_per_splat = sh_size + SPLAT_TABLE_SH_OFFSET;
auto block_boxes = std::make_unique<std::vector<float>>();
auto gaussian_count = std::make_unique<std::vector<uint32_t>>();
auto buffers = std::vector<Napi::Float32Array>();
block_boxes->reserve(blocks.size() * 6);
gaussian_count->reserve(blocks.size());
buffers.reserve(buffers_per_splat * blocks.size());
for (auto& splat : blocks) {
helpers::container::append_range(buffers, write_splat(env, buffers_per_splat, splat, pool));
gaussian_count->push_back(static_cast<uint32_t>(splat.gaussians.size()));
{
auto& bbx = splat.bounding_box;
helpers::container::append_range(*block_boxes, bbx.min());
helpers::container::append_range(*block_boxes, bbx.max());
}
// free data already transformed.
{
auto _ = std::move(splat);
}
}
auto object = Napi::Object::New(env);
{
auto data = Napi::Array::New(env, buffers.size());
auto offset = 0;
object.Set("data", data);
for (auto i = 0; i < buffers.size(); i++) {
data[i] = buffers[i];
}
auto _ = std::move(buffers);
}
object.Set("blockBoxes", node_api::buffer::UniqueVecBufferFinalizer<float>::make_buffer(env, std::move(block_boxes)));
object.Set("gaussianCount", node_api::buffer::UniqueVecBufferFinalizer<uint32_t>::make_buffer(env, std::move(gaussian_count)));
return object;
}
} // namespace node_api::splat
@@ -0,0 +1,42 @@
#include <napi.h>
#include <node/api_thread_pool.h>
#include <thread_pool.h>
namespace node_api::threading {
Napi::FunctionReference ThreadPool::Init(Napi::Env env, Napi::Object exports) {
auto ctor = DefineClass(env, "ThreadPool", {
InstanceAccessor("threadCount", &ThreadPool::thread_count, nullptr, static_cast<napi_property_attributes>(napi_enumerable | napi_configurable)),
InstanceAccessor("taskCount", &ThreadPool::task_count, nullptr, static_cast<napi_property_attributes>(napi_enumerable | napi_configurable)),
});
exports.Set("ThreadPool", ctor);
return Napi::Persistent(ctor);
}
ThreadPool::ThreadPool(const Napi::CallbackInfo& info)
: Napi::ObjectWrap<ThreadPool>(info), impl_(calc_thread_count(info)) {
}
Napi::Value ThreadPool::thread_count(const Napi::CallbackInfo& info) {
return Napi::Number::New(info.Env(), static_cast<double>(this->impl_.thread_count()));
}
Napi::Value ThreadPool::task_count(const Napi::CallbackInfo& info) {
return Napi::Number::New(info.Env(), static_cast<double>(this->impl_.task_count()));
}
::threading::ThreadPool& ThreadPool::impl() noexcept {
return this->impl_;
}
size_t ThreadPool::calc_thread_count(const Napi::CallbackInfo& info) {
size_t thread_count = 0u;
if (info.Length() > 0 && info[0].IsNumber()) {
thread_count = info[0].As<Napi::Number>().Uint32Value();
}
if (thread_count == 0) {
thread_count = std::min(std::max(std::bit_ceil(std::thread::hardware_concurrency()) / 2u, 1u), 16u);
}
return thread_count;
}
} // namespace node_api::threading
@@ -0,0 +1,108 @@
#include <node/api_buffer.h>
#include <node/api_webp.h>
#include <span>
#include <webp/decode.h>
#include <webp/encode.h>
namespace {
size_t webp_encode_rgba(const std::span<uint8_t>& data, int32_t width, int32_t height, bool lossless, float quality, uint8_t** output) {
WebPPicture pic;
WebPConfig config;
WebPMemoryWriter wrt;
int ok;
if (output == nullptr) {
return 0;
}
if (!WebPConfigPreset(&config, WEBP_PRESET_DEFAULT, quality) ||
!WebPPictureInit(&pic)) {
return 0; // shouldn't happen, except if system installation is broken
}
config.lossless = static_cast<int32_t>(lossless);
config.exact = 1;
pic.use_argb = static_cast<int32_t>(lossless);
pic.width = width;
pic.height = height;
pic.writer = WebPMemoryWrite;
pic.custom_ptr = &wrt;
WebPMemoryWriterInit(&wrt);
ok = WebPPictureImportRGBA(&pic, data.data(), 4 * width) && WebPEncode(&config, &pic);
WebPPictureFree(&pic);
if (!ok) {
WebPMemoryWriterClear(&wrt);
return 0;
}
*output = wrt.mem;
return wrt.size;
}
} // namespace
namespace node_api::imaging {
Napi::Value webp_encode_rgba(const Napi::CallbackInfo& info) {
auto env = info.Env();
if (info.Length() < 4 || !info[0].IsBuffer() || !info[1].IsNumber() || !info[2].IsNumber() || !info[3].IsNumber()) {
Napi::TypeError::New(env, "Wrong Arguments").ThrowAsJavaScriptException();
return env.Null();
}
auto buffer = info[0].As<Napi::Buffer<uint8_t>>();
auto width = info[1].As<Napi::Number>().Int32Value();
auto height = info[2].As<Napi::Number>().Int32Value();
auto quality = info[3].As<Napi::Number>().FloatValue();
auto span = std::span<uint8_t>(buffer.Data(), buffer.Length());
uint8_t* output = nullptr;
auto compressed = ::webp_encode_rgba(span, width, height, false, quality, &output);
if (compressed == 0) {
Napi::TypeError::New(env, "WebP Encode Failed").ThrowAsJavaScriptException();
return env.Null();
}
return node_api::buffer::UniqueBufferFinalizer<uint8_t, decltype(&WebPFree)>::make_buffer(env, std::unique_ptr<uint8_t, decltype(&WebPFree)>(output, WebPFree), compressed);
}
Napi::Value webp_encode_rgba_lossless(const Napi::CallbackInfo& info) {
auto env = info.Env();
if (info.Length() < 3 || !info[0].IsBuffer() || !info[1].IsNumber() || !info[2].IsNumber()) {
Napi::TypeError::New(env, "Wrong Arguments").ThrowAsJavaScriptException();
return env.Null();
}
auto buffer = info[0].As<Napi::Buffer<uint8_t>>();
auto width = info[1].As<Napi::Number>().Int32Value();
auto height = info[2].As<Napi::Number>().Int32Value();
auto span = std::span<uint8_t>(buffer.Data(), buffer.Length());
uint8_t* output = nullptr;
auto compressed = ::webp_encode_rgba(span, width, height, true, 70, &output);
if (compressed == 0) {
Napi::TypeError::New(env, "WebP Encode Failed").ThrowAsJavaScriptException();
return env.Null();
}
return node_api::buffer::UniqueBufferFinalizer<uint8_t, decltype(&WebPFree)>::make_buffer(env, std::unique_ptr<uint8_t, decltype(&WebPFree)>(output, WebPFree), compressed);
}
Napi::Value webp_decode_rgba(const Napi::CallbackInfo& info) {
auto env = info.Env();
if (info.Length() < 1 || !info[0].IsBuffer()) {
Napi::TypeError::New(env, "Wrong Arguments").ThrowAsJavaScriptException();
return env.Null();
}
auto buffer = info[0].As<Napi::Buffer<uint8_t>>();
int32_t width = 0;
int32_t height = 0;
auto rgba = WebPDecodeRGBA(buffer.Data(), buffer.Length(), &width, &height);
if (rgba == nullptr) {
Napi::TypeError::New(env, "WebP Decode Failed").ThrowAsJavaScriptException();
return env.Null();
}
auto object = Napi::Object::New(env);
{
object.Set("data", node_api::buffer::UniqueBufferFinalizer<uint8_t, decltype(&WebPFree)>::make_buffer(env, std::unique_ptr<uint8_t, decltype(&WebPFree)>(rgba, WebPFree), width * height * 4));
object.Set("width", Napi::Number::New(env, width));
object.Set("height", Napi::Number::New(env, height));
}
return object;
}
} // namespace node_api::imaging
@@ -0,0 +1,57 @@
#include <concepts>
#include <napi.h>
#include <node/api_avif.h>
#include <node/api_spatial.h>
#include <node/api_splat.h>
#include <node/api_thread_pool.h>
#include <node/api_webp.h>
#include <type_traits>
#include <utility>
namespace {
struct InstanceData {
Napi::FunctionReference thread_pool;
};
template<typename F>
requires requires(F f, const Napi::CallbackInfo& info) {
{ f(info) } -> std::same_as<Napi::Value>;
}
auto wrap_call(F&& f) {
return [f = std::forward<F>(f)](const Napi::CallbackInfo& info) {
try {
return f(info);
} catch (const Napi::Error&) {
throw;
} catch (const std::exception& ex) {
auto env = info.Env();
Napi::Error::New(env, ex.what()).ThrowAsJavaScriptException();
return env.Null();
} catch (...) {
auto env = info.Env();
Napi::Error::New(env, "Unknown runtime exception").ThrowAsJavaScriptException();
return env.Null();
}
};
}
} // namespace
static Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set("generate_splat_lod", Napi::Function::New(env, wrap_call(node_api::splat::generate_lod)));
exports.Set("split_splat", Napi::Function::New(env, wrap_call(node_api::splat::split)));
exports.Set("webp_encode_rgba", Napi::Function::New(env, wrap_call(node_api::imaging::webp_encode_rgba)));
exports.Set("webp_encode_rgba_lossless", Napi::Function::New(env, wrap_call(node_api::imaging::webp_encode_rgba_lossless)));
exports.Set("webp_decode_rgba", Napi::Function::New(env, wrap_call(node_api::imaging::webp_decode_rgba)));
exports.Set("avif_encode_rgba", Napi::Function::New(env, wrap_call(node_api::imaging::avif_encode_rgba)));
exports.Set("avif_encode_rgba_batched", Napi::Function::New(env, wrap_call(node_api::imaging::avif_encode_rgba_batched)));
exports.Set("avif_decode_rgba", Napi::Function::New(env, wrap_call(node_api::imaging::avif_decode_rgba)));
exports.Set("avif_decode_rgba_batched", Napi::Function::New(env, wrap_call(node_api::imaging::avif_decode_rgba_batched)));
exports.Set("cluster_average", Napi::Function::New(env, wrap_call(node_api::spatial::cluster_average)));
auto instance_data = new InstanceData {
.thread_pool = node_api::threading::ThreadPool::Init(env, exports)
};
env.SetInstanceData<InstanceData>(instance_data);
return exports;
}
NODE_API_MODULE(addon_napi, Init)
@@ -0,0 +1,188 @@
#include <algorithm>
#include <cassert>
#include <cmath>
#include <cstring>
#include <eigen3/Eigen/Dense>
#include <numbers>
#include <splat/splat.h>
#include <stdexcept>
#include <tuple>
#include <utility>
namespace splat {
SH::SH() noexcept : size_(0), ptr(nullptr) {
}
SH::SH(size_t size) : size_(size), ptr(size > 0 ? std::make_unique<float[]>(size) : nullptr) {
}
SH::SH(const SH& other) : size_(other.size_), ptr(other.size_ > 0 ? std::make_unique<float[]>(other.size_) : nullptr) {
if (this->ptr != nullptr) {
std::copy(other.ptr.get(), other.ptr.get() + this->size_, this->ptr.get());
}
}
SH::SH(SH&& other) noexcept : size_(std::exchange(other.size_, 0)),
ptr(std::exchange(other.ptr, nullptr)) {
}
SH& SH::operator=(const SH& other) {
if (this != &other) {
this->reset();
if (other.size_ > 0) {
this->ptr = std::make_unique<float[]>(other.size_);
std::copy(other.ptr.get(), other.ptr.get() + other.size_, this->ptr.get());
}
this->size_ = other.size_;
}
return *this;
}
SH& SH::operator=(SH&& other) noexcept {
if (this != &other) {
this->size_ = std::exchange(other.size_, 0);
this->ptr = std::exchange(other.ptr, nullptr);
}
return *this;
}
void SH::swap(SH& other) noexcept {
std::swap(this->ptr, other.ptr);
std::swap(this->size_, other.size_);
}
SH& SH::set_zero() noexcept {
if (this->ptr) {
std::memset(this->ptr.get(), 0, sizeof(float) * this->size_);
}
return *this;
}
SH& SH::add_multiplied(const SH& other, float scalar) noexcept {
assert(other.size_ == this->size_);
for (auto i = 0; i < this->size_; i++) {
this->ptr[i] += other.ptr[i] * scalar;
}
return *this;
}
const float& SH::operator[](size_t index) const {
return this->ptr[index];
}
float& SH::operator[](size_t index) {
return this->ptr[index];
}
void SH::reset() noexcept {
this->ptr.reset();
this->size_ = 0;
}
float* SH::data() noexcept {
return this->ptr.get();
}
size_t SH::size() const noexcept {
return this->size_;
}
SH::~SH() noexcept {
}
void Gaussian::compute_bounding_box(float k) {
auto sigma = this->scale.cwiseAbs();
this->bounding_box = Eigen::AlignedBox3f(this->mean - k * sigma, this->mean + k * sigma);
}
void Gaussian::compute_covariance() {
Eigen::Matrix3f s = Eigen::Matrix3f::Identity();
s(0, 0) = this->scale.x();
s(1, 1) = this->scale.y();
s(2, 2) = this->scale.z();
auto r = Eigen::Matrix3f(Eigen::Quaternionf(this->rotation));
Eigen::Matrix3f t = r * s;
this->covariance = t * t.transpose();
}
void Gaussian::decompose_covariance() {
auto matrix = this->covariance;
Eigen::SelfAdjointEigenSolver<Eigen::Matrix3f> eigen_solver(matrix);
auto eigen_values = eigen_solver.eigenvalues();
auto eigen_vectors = eigen_solver.eigenvectors();
if (eigen_values.hasNaN()) {
throw std::runtime_error("Found Nans in covariance decomposing!");
}
int loops = 0;
while (eigen_values[0] == 0 || eigen_values[1] == 0 || eigen_values[2] == 0) {
matrix(0, 0) += std::max(matrix(0, 0) * 0.0001f, std::numeric_limits<float>::epsilon());
matrix(1, 1) += std::max(matrix(1, 1) * 0.0001f, std::numeric_limits<float>::epsilon());
matrix(2, 2) += std::max(matrix(2, 2) * 0.0001f, std::numeric_limits<float>::epsilon());
Eigen::SelfAdjointEigenSolver<Eigen::Matrix3f> eigen_solver2(matrix);
eigen_values = eigen_solver2.eigenvalues();
eigen_vectors = eigen_solver2.eigenvectors();
loops++;
}
auto v1 = eigen_vectors.col(0);
auto v2 = eigen_vectors.col(1);
auto v3 = eigen_vectors.col(2);
auto test = v1.cross(v2);
if (test.dot(v3) < 0) {
eigen_vectors.col(2) *= -1;
}
float a = std::sqrt(std::abs(eigen_values.x()));
float b = std::sqrt(std::abs(eigen_values.y()));
float c = std::sqrt(std::abs(eigen_values.z()));
this->scale = { a, b, c };
auto q = Eigen::Quaternionf(eigen_vectors);
this->rotation = { q.x(), q.y(), q.z(), q.w() };
this->rotation.normalize();
}
float Gaussian::area() const {
constexpr float P = 1.6075f;
auto numerator = std::pow(this->scale.x() * this->scale.y(), P) + std::pow(this->scale.x() * this->scale.z(), P) +
std::pow(this->scale.y() * this->scale.z(), P);
return 4.0f * std::numbers::pi_v<float> * std::pow(numerator / 3.0f, 1.0f / P);
}
void Splat::compute_bounding_box() {
this->bounding_box = Eigen::AlignedBox3f();
for (auto& gaussian : this->gaussians) {
this->bounding_box.extend(gaussian.bounding_box);
}
}
void Splat::compute_compact_bounding_box() {
this->bounding_box = Eigen::AlignedBox3f();
auto ref = std::vector<std::tuple<size_t, float>>();
ref.reserve(this->gaussians.size());
for (auto i = 0; i < this->gaussians.size(); i++) {
ref.emplace_back(i, this->gaussians[i].area());
}
std::sort(ref.begin(), ref.end(), [](auto& a, auto& b) -> bool { return std::get<1>(a) < std::get<1>(b); });
auto needed = static_cast<size_t>(static_cast<double>(this->gaussians.size()) * 0.95);
if (needed == 0) {
needed = this->gaussians.size();
}
for (auto i = 0; i < needed; i++) {
this->bounding_box.extend(this->gaussians[std::get<0>(ref[i])].bounding_box);
}
}
} // namespace splat
@@ -0,0 +1,137 @@
#include <array>
#include <container_helpers.h>
#include <eigen3/Eigen/Dense>
#include <queue>
#include <ranges>
#include <splat/splat_block.h>
#include <utility>
#include <vector>
namespace {
struct RefSplat {
const splat::Splat& source;
std::vector<size_t> gaussians;
Eigen::AlignedBox3f box;
bool need_split;
RefSplat(const splat::Splat& splat) : source(splat), box(splat.bounding_box), need_split(true) {
this->gaussians.reserve(splat.gaussians.size());
for (auto i = 0; i < splat.gaussians.size(); i++) {
this->gaussians.push_back(i);
}
}
RefSplat(RefSplat& splat) : source(splat.source), need_split(true) {}
RefSplat(RefSplat&& splat) = default;
void compute_bounding_box() {
this->box = Eigen::AlignedBox3f();
for (auto index : this->gaussians) {
this->box.extend(this->source.gaussians[index].bounding_box);
}
}
void compute_need_split(const Eigen::AlignedBox3f& parent_box, size_t max_block_size) {
this->need_split = this->gaussians.size() > max_block_size &&
((this->box.min() - parent_box.min()).norm() > 0.001 ||
(this->box.max() - parent_box.max()).norm() > 0.001);
}
splat::Splat to_owned() {
std::vector<splat::Gaussian> gaussians;
std::vector<float> sh;
gaussians.reserve(this->gaussians.size());
for (auto index : this->gaussians) {
gaussians.push_back(this->source.gaussians[index]);
}
{
auto _ = std::move(this->gaussians);
}
auto result = splat::Splat {
.gaussians = std::move(gaussians),
.bounding_box = this->box,
};
result.compute_compact_bounding_box();
return result;
}
};
std::array<Eigen::AlignedBox3f, 8> split_box(const Eigen::AlignedBox3f& box) {
auto center = box.center();
return {
Eigen::AlignedBox3f().extend(center).extend(box.corner(Eigen::AlignedBox3f::CornerType::BottomLeftFloor)),
Eigen::AlignedBox3f().extend(center).extend(box.corner(Eigen::AlignedBox3f::CornerType::BottomRightFloor)),
Eigen::AlignedBox3f().extend(center).extend(box.corner(Eigen::AlignedBox3f::CornerType::TopLeftFloor)),
Eigen::AlignedBox3f().extend(center).extend(box.corner(Eigen::AlignedBox3f::CornerType::TopRightFloor)),
Eigen::AlignedBox3f().extend(center).extend(box.corner(Eigen::AlignedBox3f::CornerType::BottomLeftCeil)),
Eigen::AlignedBox3f().extend(center).extend(box.corner(Eigen::AlignedBox3f::CornerType::BottomRightCeil)),
Eigen::AlignedBox3f().extend(center).extend(box.corner(Eigen::AlignedBox3f::CornerType::TopLeftCeil)),
Eigen::AlignedBox3f().extend(center).extend(box.corner(Eigen::AlignedBox3f::CornerType::TopRightCeil)),
};
}
std::array<RefSplat, 8> split_block(RefSplat& splat, size_t max_block_size) {
auto boxes = split_box(splat.box);
std::array<RefSplat, 8> result { RefSplat(splat),
RefSplat(splat),
RefSplat(splat),
RefSplat(splat),
RefSplat(splat),
RefSplat(splat),
RefSplat(splat),
RefSplat(splat) };
for (auto& r : result) {
r.gaussians.reserve(splat.gaussians.size() / 4);
}
for (auto index : splat.gaussians) {
auto& box = splat.source.gaussians[index].bounding_box;
auto max_interaction = 0.0;
auto max_interaction_index = 0;
for (auto i = 0; i < 8; i++) {
auto intersection = boxes[i].intersection(box);
if (!intersection.isEmpty()) {
auto current = intersection.volume();
if (current > max_interaction) {
max_interaction = current;
max_interaction_index = i;
}
}
}
result[max_interaction_index].gaussians.push_back(index);
}
for (auto i = 0; i < result.size(); i++) {
result[i].box = boxes[i];
result[i].compute_need_split(splat.box, max_block_size);
}
return result;
}
} // namespace
namespace splat::block::detail {
std::vector<Splat> split(const Splat& splat, size_t max_block_size) {
if (splat.gaussians.size() <= max_block_size) {
return std::vector({ splat });
}
std::queue<RefSplat> queue;
std::vector<Splat> results;
queue.emplace(splat);
while (!queue.empty()) {
auto r = std::move(queue.front());
queue.pop();
if (r.need_split) {
for (auto& splitted : split_block(r, max_block_size)) {
queue.emplace(std::move(splitted));
}
} else if (r.gaussians.size() > 0) {
results.emplace_back(r.to_owned());
}
}
return results;
}
} // namespace splat::block::detail
@@ -0,0 +1,396 @@
#include <algorithm>
#include <bit>
#include <cmath>
#include <container_helpers.h>
#include <eigen3/Eigen/Dense>
#include <initializer_list>
#include <nanoflann.hpp>
#include <numbers>
#include <ranges>
#include <splat/splat.h>
#include <splat/splat_lod.h>
#include <thread>
#include <tuple>
#include <vector>
namespace {
constexpr double TWO_PI_POW_1_5 = 0x1.f7fccep+3;
constexpr double EPS_COV = 1e-8;
constexpr double LOG_2PI = 1.8378771;
constexpr float OPACITY_PRUNE_THRESHOLD = 0.005f;
class GaussianCloud {
public:
GaussianCloud(const splat::Splat& splat) : splat(splat) {}
inline size_t kdtree_get_point_count() const { return splat.gaussians.size(); }
inline float kdtree_get_pt(size_t i, int d) const { return splat.gaussians[i].mean[d]; }
template<class BBOX>
bool kdtree_get_bbox(BBOX&) const { return false; }
private:
const splat::Splat& splat;
};
struct GaussianCache {
Eigen::Matrix3d rotation;
Eigen::Matrix3d rotation_transpose;
Eigen::Matrix3d sigma;
Eigen::Vector3d variances;
Eigen::Vector3d inv_diag;
double log_det;
double mass;
};
using KDTree = nanoflann::KDTreeSingleIndexAdaptor<
nanoflann::L2_Simple_Adaptor<float, GaussianCloud>,
GaussianCloud, 3>;
// https://github.com/graphdeco-inria/gaussian-hierarchy/
static splat::Gaussian merge_gaussians(const splat::Splat& input, const std::vector<uint32_t>& idx, float scale_boost) {
splat::Gaussian out;
out.mean.setZero();
out.covariance.setZero();
out.scale.setZero();
out.rotation.setZero();
out.sh = splat::SH(input.gaussians[0].sh.size());
out.sh.set_zero();
out.opacity = 0.0;
std::vector<float> weights(idx.size());
float total_weight = 0.0f;
for (auto i = 0; i < idx.size(); i++) {
const auto& g = input.gaussians[idx[i]];
weights[i] = g.area() * g.opacity;
total_weight += weights[i];
}
for (auto& weight : weights) {
weight /= total_weight;
}
for (auto i = 0; i < idx.size(); i++) {
const auto& g = input.gaussians[idx[i]];
out.mean += g.mean * weights[i];
out.sh.add_multiplied(g.sh, weights[i]);
}
for (auto i = 0; i < idx.size(); i++) {
const auto& g = input.gaussians[idx[i]];
auto d = g.mean - out.mean;
out.covariance += weights[i] * (g.covariance + d * d.transpose());
}
out.decompose_covariance();
out.scale *= scale_boost;
out.opacity = total_weight / out.area();
if (out.opacity > 1.0f) {
out.scale *= std::pow(out.opacity, 1.0f / 3.0f);
out.opacity = 1.0;
}
out.compute_covariance();
return out;
}
static Eigen::Matrix3d sigma_from_rotation_variances(const Eigen::Matrix3d& rotation, const Eigen::Vector3d& variances) {
Eigen::Matrix3d s = Eigen::Matrix3d::Identity();
s(0, 0) = variances.x();
s(1, 1) = variances.y();
s(2, 2) = variances.z();
return rotation * s * rotation.transpose();
}
static void build_cache(const splat::Splat& splat, std::vector<GaussianCache>& caches) {
static Eigen::Vector3d VECTOR_ONE(1.0, 1.0, 1.0);
static Eigen::Vector3d VECTOR_EPS(EPS_COV, EPS_COV, EPS_COV);
for (auto& gaussian : splat.gaussians) {
auto rotation = Eigen::Matrix3d(Eigen::Quaterniond(gaussian.rotation.w(), gaussian.rotation.x(), gaussian.rotation.y(), gaussian.rotation.z()));
auto t = rotation.transpose();
Eigen::Vector3f vf = gaussian.scale.cwiseMax(1e-12f).cwiseSquare();
Eigen::Vector3d v = Eigen::Vector3d(vf.x(), vf.y(), vf.z()) + VECTOR_EPS;
Eigen::Vector3d safe_v = v.cwiseMax(1e-30);
caches.push_back(GaussianCache {
.rotation = rotation,
.rotation_transpose = t,
.sigma = sigma_from_rotation_variances(rotation, v),
.variances = v,
.inv_diag = VECTOR_ONE.array() / safe_v.array(),
.log_det = safe_v.array().log().sum(),
.mass = TWO_PI_POW_1_5 * gaussian.opacity * gaussian.scale.prod() + 1e-12 });
}
}
std::vector<Eigen::Vector3d> make_gaussian_samples(size_t n, uint32_t seed) {
std::vector<Eigen::Vector3d> result;
result.reserve(n);
auto rand = [&seed]() -> double {
auto t = (seed += 0x6d2b79f5);
t = (t ^ (t >> 15)) * (t | 1);
t ^= t + (t ^ (t >> 7)) * (t | 61);
return static_cast<double>((t ^ (t >> 14)) >> 0) / 4294967296.0;
};
for (auto i = 0; i < n; i++) {
auto u1 = std::max(rand(), 1e-12);
auto u2 = rand();
auto u3 = std::max(rand(), 1e-12);
auto u4 = rand();
auto r1 = std::sqrt(-2 * std::log(u1));
auto t1 = 2 * std::numbers::pi_v<double> * u2;
auto r2 = std::sqrt(-2 * std::log(u3));
auto t2 = 2 * std::numbers::pi_v<double> * u4;
result.emplace_back(r1 * std::cos(t1), r1 * std::sin(t1), r2 * std::cos(t2));
}
return result;
}
static double gauss_log_pdf_diagrot(const Eigen::Vector3d& v, const Eigen::Vector3d& m, const Eigen::Matrix3d& r, const Eigen::Vector3d& inv, double log_det) {
Eigen::Vector3d d = v - m;
Eigen::Vector3d y = r * d;
double quad = y.cwiseSquare().transpose() * inv;
return -0.5 * (3.0 * LOG_2PI + log_det + quad);
}
static double log_add_exp(double a, double b) {
if (-std::numeric_limits<double>::infinity() == a) {
return b;
}
if (-std::numeric_limits<double>::infinity() == b) {
return a;
}
auto m = std::max(a, b);
return m + std::log(std::exp(a - m) + std::exp(b - m));
};
static double compute_edge_cost(const splat::Splat& splat, const std::vector<GaussianCache>& caches, const std::vector<Eigen::Vector3d>& samples, std::tuple<uint32_t, uint32_t>& edge) {
auto [i, j] = edge;
auto& u = splat.gaussians[i];
auto& v = splat.gaussians[j];
auto& cache_i = caches[i];
auto& cache_j = caches[j];
auto wi = cache_i.mass;
auto wj = cache_j.mass;
auto w = wi + wj;
auto w_safe = w > 0 ? w : 1;
auto pi = wi / w_safe;
pi = std::max(1e-12, std::min(1.0 - 1e-12, pi));
auto pj = 1 - pi;
auto log_pi = std::log(pi);
auto log_pj = std::log(pj);
Eigen::Vector3d u_mean(u.mean.x(), u.mean.y(), u.mean.z());
Eigen::Vector3d v_mean(v.mean.x(), v.mean.y(), v.mean.z());
auto mm = pi * u_mean + pj * v_mean;
auto di = u_mean - mm;
auto dj = v_mean - mm;
Eigen::Matrix3d sigma = pi * cache_i.sigma + pj * cache_j.sigma;
sigma += pi * (di * di.transpose()) + pj * (dj * dj.transpose());
// Force symmetry + regularize
sigma(0, 1) = sigma(1, 0) = 0.5 * (sigma(0, 1) + sigma(1, 0));
sigma(0, 2) = sigma(2, 0) = 0.5 * (sigma(0, 2) + sigma(2, 0));
sigma(1, 2) = sigma(2, 1) = 0.5 * (sigma(1, 2) + sigma(2, 1));
sigma(0, 0) += EPS_COV;
sigma(1, 1) += EPS_COV;
sigma(2, 2) += EPS_COV;
// det should never be neg.
auto det_m = std::max(sigma.determinant(), 1e-30);
auto log_det_m = std::log(det_m);
// E_p[-log q_m] computed analytically as entropy of merged Gaussian
auto ep_neg_log_q = 0.5 * (3.0 * LOG_2PI + log_det_m + 3.0);
// Sample from each component separately with same z-vectors
Eigen::Vector3d std_i = cache_i.variances.cwiseMax(0).cwiseSqrt();
Eigen::Vector3d std_j = cache_j.variances.cwiseMax(0).cwiseSqrt();
auto sum_log_p_on_i = 0.0;
auto sum_log_p_on_j = 0.0;
for (auto& sample : samples) {
Eigen::Vector3d ti = std_i.array() * sample.array();
Eigen::Vector3d tj = std_j.array() * sample.array();
Eigen::Vector3d xi = u_mean + cache_i.rotation * ti;
Eigen::Vector3d xj = v_mean + cache_j.rotation * tj;
// Evaluate log p_ij at samples from component i
auto log_ni_on_i = gauss_log_pdf_diagrot(xi, u_mean, cache_i.rotation_transpose, cache_i.inv_diag, cache_i.log_det);
auto log_nj_on_i = gauss_log_pdf_diagrot(xi, v_mean, cache_j.rotation_transpose, cache_j.inv_diag, cache_j.log_det);
sum_log_p_on_i += log_add_exp(log_pi + log_ni_on_i, log_pj + log_nj_on_i);
// Evaluate log p_ij at samples from component j
auto log_ni_on_j = gauss_log_pdf_diagrot(xj, u_mean, cache_i.rotation_transpose, cache_i.inv_diag, cache_i.log_det);
auto log_nj_on_j = gauss_log_pdf_diagrot(xj, v_mean, cache_j.rotation_transpose, cache_j.inv_diag, cache_j.log_det);
sum_log_p_on_j += log_add_exp(log_pi + log_ni_on_j, log_pj + log_nj_on_j);
}
auto ei = sum_log_p_on_i / samples.size();
auto ej = sum_log_p_on_j / samples.size();
auto ep_log_p = pi * ei + pj * ej;
auto geo = ep_log_p + ep_neg_log_q;
auto c_sh = 0.0;
for (auto i = 0; i < u.sh.size(); i++) {
auto d = u.sh[i] - v.sh[i];
c_sh += (d * d);
}
return geo + c_sh;
}
inline static bool validate_gaussian(const splat::Gaussian& gaussian) {
return gaussian.scale.cwiseNotEqual(0.0f).all() &&
gaussian.opacity >= OPACITY_PRUNE_THRESHOLD;
}
} // namespace
namespace splat::lod::detail {
Splat reduce_gaussians(size_t id, const Splat& input, size_t target_count, float scale_boost, size_t max_step) {
assert(input.gaussians.size() > target_count);
Splat current;
uint32_t knn_k = 16;
auto samples = make_gaussian_samples(1, 0);
std::vector<uint32_t> out_indices;
std::vector<float> out_distances;
std::vector<size_t> order;
std::vector<std::tuple<uint32_t, uint32_t>> edges;
std::vector<std::tuple<uint32_t, uint32_t>> pairs;
std::vector<bool> used;
std::vector<double> costs;
std::vector<GaussianCache> caches;
auto step = 0;
// prune gaussians
current.gaussians.reserve(input.gaussians.size());
helpers::container::append_range(current.gaussians, input.gaussians | std::views::filter(validate_gaussian));
while (current.gaussians.size() > target_count && current.gaussians.size() > 0 && step < max_step) {
GaussianCloud cloud(current);
KDTree tree(3, cloud,
nanoflann::KDTreeSingleIndexAdaptorParams(
10, nanoflann::KDTreeSingleIndexAdaptorFlags::None,
std::max(std::bit_floor(std::thread::hardware_concurrency()) / 2u, 1u)));
auto k_eff = std::min<uint32_t>(std::max<uint32_t>(1, knn_k), std::max<uint32_t>(1, current.gaussians.size() - 1));
size_t query_count = static_cast<size_t>(std::min<size_t>(current.gaussians.size(), k_eff + 1));
auto max_pairs = current.gaussians.size() - target_count;
assert(max_pairs > 0);
Splat generated;
edges.clear();
edges.reserve(current.gaussians.size() * k_eff);
caches.clear();
caches.reserve(current.gaussians.size());
generated.gaussians.reserve(current.gaussians.size() / 2);
build_cache(current, caches);
// knn search
for (auto i = 0; i < current.gaussians.size(); i++) {
out_indices.assign(query_count, 0);
out_distances.assign(query_count, 0.0);
auto result_count = tree.knnSearch(current.gaussians[i].mean.data(), query_count, out_indices.data(), out_distances.data());
const size_t take = std::min<size_t>(k_eff, result_count > 0 ? result_count - 1 : size_t { 0 });
for (auto j = 0; j < take; j++) {
auto neighbor = static_cast<int>(out_indices[j + 1]);
if (neighbor <= i) {
continue;
}
edges.emplace_back(i, neighbor);
}
}
assert(edges.size() > 0);
// compute edge costs.
{
costs.clear();
costs.reserve(edges.size());
for (auto& edge : edges) {
costs.push_back(compute_edge_cost(current, caches, samples, edge));
}
}
// take pairs
{
order.clear();
order.reserve(edges.size());
for (auto i = 0; i < costs.size(); ++i) {
if (std::isfinite(costs[i])) {
order.push_back(i);
}
}
std::stable_sort(order.begin(), order.end(), [&](const size_t lhs, const size_t rhs) {
return costs[lhs] < costs[rhs];
});
used.assign(current.gaussians.size(), false);
pairs.clear();
pairs.reserve(max_pairs);
for (auto edge_idx : order) {
const auto [u, v] = edges[edge_idx];
if (used[u] || used[v]) {
continue;
}
used[u] = true;
used[v] = true;
pairs.emplace_back(u, v);
if (pairs.size() >= max_pairs) {
break;
}
}
}
// merge pairs
for (auto& pair : pairs) {
auto [u, v] = pair;
auto g = merge_gaussians(current, { u, v }, scale_boost);
if (validate_gaussian(g)) {
generated.gaussians.push_back(std::move(g));
}
}
// keep unused gaussian
for (auto i = 0; i < current.gaussians.size(); i++) {
if (!used[i]) {
generated.gaussians.push_back(current.gaussians[i]);
}
}
current = std::move(generated);
// trace hard working on current block.
if (step >= max_step / 2) {
printf("block: %zu, step: %d, required: %zu, current: %zu(%.2f%%)\n",
id, step, target_count, current.gaussians.size(),
static_cast<double>(current.gaussians.size()) / static_cast<double>(target_count) * 100.0);
}
// cannot reduce any more...
if (pairs.size() == 0) {
break;
}
step++;
}
current.gaussians.shrink_to_fit();
return current;
}
} // namespace splat::lod::detail
@@ -0,0 +1,134 @@
#include <functional>
#include <memory>
#include <mutex>
#include <shared_mutex>
#include <thread>
#include <thread_pool.h>
#include <utility>
namespace threading {
ThreadPool::ThreadPool(size_t thread_count)
: state(std::make_unique<State>()) {
if (thread_count == 0) {
thread_count = 1;
}
this->state->workers.reserve(thread_count);
for (auto i = 0; i < thread_count; i++) {
auto worker = std::make_unique<Worker>();
worker->thread = std::thread(ThreadPool::worker_loop, this->state.get(), worker.get());
this->state->workers.push_back(std::move(worker));
}
}
ThreadPool::ThreadPool(ThreadPool&& other) noexcept
: state(std::move(other.state)) {
}
ThreadPool& ThreadPool::operator=(ThreadPool&& other) noexcept {
if (this != &other) {
this->stop();
this->state = std::move(other.state);
}
return *this;
}
size_t ThreadPool::thread_count() const noexcept {
if (this->state == nullptr) {
return 0;
}
auto lk = std::shared_lock(this->state->worker_mutex);
return this->state->workers.size();
}
size_t ThreadPool::task_count() const noexcept {
if (this->state == nullptr) {
return 0;
}
auto lk = std::shared_lock(this->state->queue_mutex);
return this->state->tasks.size();
}
void ThreadPool::stop() noexcept {
if (this->state == nullptr) {
return;
}
this->state->stop();
}
ThreadPool::~ThreadPool() noexcept {
this->stop();
}
void ThreadPool::State::stop() noexcept {
bool expected = false;
if (!this->stopped.compare_exchange_strong(expected, true, std::memory_order_relaxed)) {
return;
}
{
auto lk = std::unique_lock(this->queue_mutex);
auto _ = std::move(this->tasks);
lk.unlock();
}
{
auto lk = std::lock_guard(this->worker_mutex);
for (auto& worker : this->workers) {
worker->stop();
}
}
this->cv.notify_all();
for (auto& worker : this->workers) {
worker->clean_up();
}
}
ThreadPool::State::~State() noexcept {
this->stop();
}
void ThreadPool::Worker::stop() noexcept {
this->stopped.store(true, std::memory_order_relaxed);
}
void ThreadPool::Worker::clean_up() noexcept {
this->stop();
if (this->thread.joinable()) {
try {
this->thread.join();
} catch (...) {
}
}
}
ThreadPool::Worker::~Worker() noexcept {
this->clean_up();
}
void ThreadPool::worker_loop(ThreadPool::State* state, ThreadPool::Worker* worker) {
while (true) {
ThreadPool::Task task;
{
auto lk = std::unique_lock(state->queue_mutex);
state->cv.wait(lk, [&]() {
return state->stopped.load(std::memory_order_relaxed) || worker->stopped.load(std::memory_order_relaxed) || !state->tasks.empty();
});
if (state->stopped.load(std::memory_order_relaxed) || worker->stopped.load(std::memory_order_relaxed) || state->tasks.empty()) {
return;
}
task = std::move(state->tasks.front());
state->tasks.pop();
}
task();
}
}
} // namespace threading
@@ -0,0 +1,14 @@
{
"default-registry": {
"kind": "git",
"baseline": "ac7af7424cbaf9057cb246b620f455303dccd6ed",
"repository": "https://github.com/microsoft/vcpkg"
},
"registries": [
{
"kind": "artifact",
"location": "https://github.com/microsoft/vcpkg-ce-catalog/archive/refs/heads/main.zip",
"name": "microsoft"
}
]
}
@@ -0,0 +1,11 @@
{
"dependencies": [
"eigen3",
"nanoflann",
"libwebp",
{
"name": "libavif",
"features": ["aom"]
}
]
}
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9a523c583e9d0ed4134f909e09c0af0ce01b6833ec992c978f2d950850b89fb8
size 9555800
@@ -0,0 +1,15 @@
{
"name": "@manycore/aholo-splat-transform-darwin-arm64",
"version": "0.0.0",
"type": "commonjs",
"files": [
"splat-transform.node",
"dawn.node"
],
"os": [
"darwin"
],
"cpu": [
"arm64"
]
}
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:211218cc2d80c19ac80604ff8684ffc87ae79bf34224b4f379272203a8e91ef1
size 12373936
@@ -0,0 +1,18 @@
{
"name": "@manycore/aholo-splat-transform-linux-arm64-gnu",
"version": "0.0.0",
"type": "commonjs",
"files": [
"splat-transform.node",
"dawn.node"
],
"os": [
"linux"
],
"cpu": [
"arm64"
],
"libc": [
"glibc"
]
}
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1c2f65499115771f9355965294e694d5de6a33a6dbdde4187f3c5fbe64c7cc6f
size 12732032
@@ -0,0 +1,18 @@
{
"name": "@manycore/aholo-splat-transform-linux-x64-gnu",
"version": "0.0.0",
"type": "commonjs",
"files": [
"splat-transform.node",
"dawn.node"
],
"os": [
"linux"
],
"cpu": [
"x64"
],
"libc": [
"glibc"
]
}

Some files were not shown because too many files have changed in this diff Show More