chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1 @@
|
||||
sandbox_mode = "workspace-write"
|
||||
@@ -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."
|
||||
@@ -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."
|
||||
@@ -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."
|
||||
@@ -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."
|
||||
@@ -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."
|
||||
@@ -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
|
||||
@@ -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."
|
||||
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
# FORCE LF ON ANY PLATFORM
|
||||
* text=auto eol=lf
|
||||
@@ -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.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
name: Custom issue template
|
||||
about: Describe this issue template's purpose here.
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
---
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -0,0 +1,12 @@
|
||||
node_modules/
|
||||
.pnpm-store/
|
||||
dist/
|
||||
build/
|
||||
.astro/
|
||||
website/.generated/
|
||||
.typedoc-*.tmp*/
|
||||
.typedoc-*.tmp.json
|
||||
*.log
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
@@ -0,0 +1,4 @@
|
||||
[submodule "external/egs-core"]
|
||||
path = external/egs-core
|
||||
url = https://github.com/manycoretech/egs.git
|
||||
branch = master
|
||||
@@ -0,0 +1,2 @@
|
||||
pnpm lint-staged -q;
|
||||
git update-index --again;
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"oxc.oxc-vscode",
|
||||
"editorconfig.editorconfig",
|
||||
"streetsidesoftware.code-spell-checker"
|
||||
]
|
||||
}
|
||||
Vendored
+25
@@ -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"
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,147 @@
|
||||
[](https://aholojs.dev/)
|
||||
|
||||
# Aholo Viewer
|
||||
|
||||

|
||||
[](https://www.npmjs.com/package/@manycore/aholo-viewer)
|
||||
[](https://www.npmjs.com/package/@manycore/aholo-splat-transform)
|
||||
[](#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).
|
||||
@@ -0,0 +1,7 @@
|
||||
# WeHub 来源说明
|
||||
|
||||
- 原始项目:`manycoretech/aholo-viewer`
|
||||
- 原始仓库:https://github.com/manycoretech/aholo-viewer
|
||||
- 导入方式:上游默认分支的最新快照
|
||||
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
|
||||
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
|
||||
+10
@@ -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
|
||||
@@ -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
|
||||
@@ -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.'
|
||||
@@ -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.
|
||||
@@ -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
@@ -0,0 +1,5 @@
|
||||
# ChangeLOG
|
||||
|
||||
## 1.0.0
|
||||
|
||||
- 发包正式包
|
||||
Vendored
+21
@@ -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.
|
||||
Vendored
+22
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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');
|
||||
});
|
||||
}
|
||||
@@ -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'],
|
||||
};
|
||||
@@ -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/**',
|
||||
],
|
||||
});
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export {
|
||||
AnimationPlugin,
|
||||
SkeletonUpdatedEvent,
|
||||
AnimationMixer,
|
||||
AnimationAction,
|
||||
AnimationFinishEvent,
|
||||
Skeleton,
|
||||
type AnimationClip,
|
||||
type KeyframeTrack,
|
||||
Blend,
|
||||
Loop,
|
||||
InterpolationMode,
|
||||
} from '@qunhe/egs-animation';
|
||||
@@ -0,0 +1 @@
|
||||
export { type ParseConfig, parseDracoBuffer } from '@qunhe/egs-draco-loader';
|
||||
@@ -0,0 +1 @@
|
||||
export { SplatSortedEvent, SplatRenderingStabilityChangedEvent } from '@qunhe/egs';
|
||||
@@ -0,0 +1 @@
|
||||
export { type LoaderConfig, type ParseResult, loadGLTF } from '@qunhe/egs-gltf-loader';
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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
Reference in New Issue
Block a user