diff --git a/.claude/settings.local.json b/.claude/settings.local.json
new file mode 100644
index 0000000000000000000000000000000000000000..7ae7fa1c5e1b1f8ea0b30dffb6bb23d7b18b5763
--- /dev/null
+++ b/.claude/settings.local.json
@@ -0,0 +1,68 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(git checkout:*)",
+ "WebFetch(domain:viser.studio)",
+ "WebSearch",
+ "WebFetch(domain:github.com)",
+ "mcp__plugin_context7_context7__resolve-library-id",
+ "mcp__plugin_context7_context7__query-docs",
+ "Bash(python -c:*)",
+ "Bash(uv add:*)",
+ "Bash(uv:*)",
+ "Bash(grep:*)",
+ "Bash(nvidia-smi:*)",
+ "Bash(nvcc:*)",
+ "Bash(where:*)",
+ "Bash(gcc:*)",
+ "Bash(cl)",
+ "Bash(python:*)",
+ "Bash(DISTUTILS_USE_SDK=1 uv pip install:*)",
+ "Bash(curl:*)",
+ "Bash(export PATH=\"/c/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.44.35207/bin/Hostx64/x64:$PATH\")",
+ "Bash(git submodule:*)",
+ "Bash(set \"PATH=C:\\\\Program Files\\\\Microsoft Visual Studio\\\\2022\\\\Community\\\\VC\\\\Tools\\\\MSVC\\\\14.42.34433\\\\bin\\\\Hostx64\\\\x64;%PATH%\")",
+ "Bash(set \"ATTN_BACKEND=xformers\")",
+ "Bash(cmd /c \"set PATH=C:\\\\Program Files\\\\Microsoft Visual Studio\\\\2022\\\\Community\\\\VC\\\\Tools\\\\MSVC\\\\14.42.34433\\\\bin\\\\Hostx64\\\\x64;%PATH% && set ATTN_BACKEND=xformers && uv run python visualize_flow.py --image assets/example_image/T.png\")",
+ "Bash(powershell -Command \"$env:ATTN_BACKEND=''xformers''; $env:PATH=''C:\\\\Program Files\\\\Microsoft Visual Studio\\\\2022\\\\Community\\\\VC\\\\Tools\\\\MSVC\\\\14.42.34433\\\\bin\\\\Hostx64\\\\x64;'' + $env:PATH; uv run python visualize_flow.py --image assets/example_image/T.png\")",
+ "Bash(timeout:*)",
+ "Bash(.venvScriptspython.exe -c \"from huggingface_hub import whoami; print\\(whoami\\(\\)\\)\")",
+ "Bash(.venv/Scripts/python.exe:*)",
+ "Bash(.venv/Scripts/pip.exe install:*)",
+ "Bash(cd:*)",
+ "Bash(ping:*)",
+ "Bash(conda activate:*)",
+ "Bash(pkill:*)",
+ "Bash(tasklist:*)",
+ "Bash(wmic OS get:*)",
+ "Bash(powershell:*)",
+ "Bash(dir /b /s \"C:\\\\Users\\\\opsiclear\\\\Desktop\\\\projects\\\\Trellis2_multi_image_conditioning\\\\trellis2\\\\pipelines\"\")",
+ "Bash(findstr:*)",
+ "Bash(netstat:*)",
+ "Bash(taskkill:*)",
+ "Bash(git add:*)",
+ "Bash(git commit:*)",
+ "Bash(git push:*)",
+ "Bash(gh auth:*)",
+ "Bash(git config:*)",
+ "Bash(git ls-tree:*)",
+ "Bash(ls:*)",
+ "Bash(wc:*)",
+ "Bash(git rm:*)",
+ "Bash(git clone:*)",
+ "Bash(huggingface-cli upload:*)",
+ "Bash(pip install:*)",
+ "Bash(\"C:/Users/opsiclear/AppData/Local/Packages/PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0/LocalCache/local-packages/Python312/Scripts/hf.exe\" upload OpsiClear/Trellis.2.multi-image \"C:/Users/opsiclear/Desktop/projects/Trellis.2.multi-image\" . --repo-type=space)",
+ "Bash(\"C:/Users/opsiclear/AppData/Local/Packages/PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0/LocalCache/local-packages/Python312/Scripts/hf.exe\" login)",
+ "Bash(\"C:/Users/opsiclear/AppData/Local/Packages/PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0/LocalCache/local-packages/Python312/Scripts/hf.exe\" --help)",
+ "Bash(\"C:/Users/opsiclear/AppData/Local/Packages/PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0/LocalCache/local-packages/Python312/Scripts/hf.exe\" auth --help)",
+ "Bash(\"C:/Users/opsiclear/AppData/Local/Packages/PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0/LocalCache/local-packages/Python312/Scripts/hf.exe\" auth whoami)",
+ "Bash(C:UsersopsiclearAppDataRoamingPythonPython310Scriptshuggingface-cli.exe repo info spaces/OpsiClear/Trellis.2.multi-image)",
+ "Bash(\"C:\\\\Users\\\\opsiclear\\\\AppData\\\\Roaming\\\\Python\\\\Python310\\\\Scripts\\\\hf.exe\" upload spaces/OpsiClear/Trellis.2.multi-image README.md --commit-message \"Add suggested_hardware: a100-large for GPU support\")",
+ "Bash(..venvScriptspython.exe app_local.py)",
+ "Bash(pip show:*)",
+ "Bash(huggingface-cli whoami:*)",
+ "Bash(git remote add:*)"
+ ]
+ }
+}
diff --git a/.gitattributes b/.gitattributes
index 8ad10c3b833597eb4d60f464758834c34d1fcfcf..e2c84149d783f14322c85ce9f64022b5c28e928e 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -132,3 +132,26 @@ assets/hdri/night.exr filter=lfs diff=lfs merge=lfs -text
assets/hdri/sunrise.exr filter=lfs diff=lfs merge=lfs -text
assets/hdri/sunset.exr filter=lfs diff=lfs merge=lfs -text
assets/teaser.webp filter=lfs diff=lfs merge=lfs -text
+o-voxel/assets/overview.webp filter=lfs diff=lfs merge=lfs -text
+o-voxel/build/temp.win-amd64-cpython-311/Release/.ninja_deps filter=lfs diff=lfs merge=lfs -text
+o-voxel/build/temp.win-amd64-cpython-311/Release/src/convert/flexible_dual_grid.obj filter=lfs diff=lfs merge=lfs -text
+o-voxel/build/temp.win-amd64-cpython-311/Release/src/convert/volumetic_attr.obj filter=lfs diff=lfs merge=lfs -text
+o-voxel/build/temp.win-amd64-cpython-311/Release/src/ext.obj filter=lfs diff=lfs merge=lfs -text
+o-voxel/build/temp.win-amd64-cpython-311/Release/src/hash/hash.obj filter=lfs diff=lfs merge=lfs -text
+o-voxel/build/temp.win-amd64-cpython-311/Release/src/io/filter_neighbor.obj filter=lfs diff=lfs merge=lfs -text
+o-voxel/build/temp.win-amd64-cpython-311/Release/src/io/filter_parent.obj filter=lfs diff=lfs merge=lfs -text
+o-voxel/build/temp.win-amd64-cpython-311/Release/src/io/svo.obj filter=lfs diff=lfs merge=lfs -text
+o-voxel/build/temp.win-amd64-cpython-311/Release/src/rasterize/rasterize.obj filter=lfs diff=lfs merge=lfs -text
+o-voxel/build/temp.win-amd64-cpython-311/Release/src/serialize/api.obj filter=lfs diff=lfs merge=lfs -text
+outputs/step_meshes/step_000.glb filter=lfs diff=lfs merge=lfs -text
+outputs/step_meshes/step_001.glb filter=lfs diff=lfs merge=lfs -text
+outputs/step_meshes/step_002.glb filter=lfs diff=lfs merge=lfs -text
+outputs/step_meshes/step_003.glb filter=lfs diff=lfs merge=lfs -text
+outputs/step_meshes/step_004.glb filter=lfs diff=lfs merge=lfs -text
+outputs/step_meshes/step_005.glb filter=lfs diff=lfs merge=lfs -text
+outputs/step_meshes/step_006.glb filter=lfs diff=lfs merge=lfs -text
+outputs/step_meshes/step_007.glb filter=lfs diff=lfs merge=lfs -text
+outputs/step_meshes/step_008.glb filter=lfs diff=lfs merge=lfs -text
+outputs/step_meshes/step_009.glb filter=lfs diff=lfs merge=lfs -text
+outputs/step_meshes/step_010.glb filter=lfs diff=lfs merge=lfs -text
+outputs/step_meshes/step_011.glb filter=lfs diff=lfs merge=lfs -text
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..12aeee20b64d51c2afe471aed850f6bca55072d6
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,207 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[codz]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py.cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# UV
+# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+#uv.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+#poetry.toml
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
+# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
+#pdm.lock
+#pdm.toml
+.pdm-python
+.pdm-build/
+
+# pixi
+# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
+#pixi.lock
+# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
+# in the .venv directory. It is recommended not to include this directory in version control.
+.pixi
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.envrc
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
+
+# Abstra
+# Abstra is an AI-powered process automation framework.
+# Ignore directories containing user credentials, local state, and settings.
+# Learn more at https://abstra.io/docs
+.abstra/
+
+# Visual Studio Code
+# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
+# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
+# and can be added to the global gitignore or merged into this file. However, if you prefer,
+# you could uncomment the following to ignore the entire vscode folder
+# .vscode/
+
+# Ruff stuff:
+.ruff_cache/
+
+# PyPI configuration file
+.pypirc
+
+# Cursor
+# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
+# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
+# refer to https://docs.cursor.com/context/ignore-files
+.cursorignore
+.cursorindexingignore
+
+# Marimo
+marimo/_static/
+marimo/_lsp/
+__marimo__/
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000000000000000000000000000000000000..0b9215af1c52939b19bf021cb9814cd37394ef45
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "o-voxel/third_party/eigen"]
+ path = o-voxel/third_party/eigen
+ url = https://gitlab.com/libeigen/eigen.git
diff --git a/README.md b/README.md
index 79cc5a3c4d3511df28e119cf6215e449a1eeb237..6439221b562205f6bf697da9411b308794f4ce9e 100644
--- a/README.md
+++ b/README.md
@@ -5,12 +5,10 @@ colorFrom: blue
colorTo: purple
sdk: gradio
sdk_version: 6.1.0
-python_version: "3.10"
app_file: app.py
pinned: false
license: mit
short_description: Multi-view image to 3D generation
-suggested_hardware: a100-large
---
# TRELLIS.2 Multi-Image Conditioning Fork
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000000000000000000000000000000000000..fd4ce163a4d96de6e81c25d708118bcbfc80e40b
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,14 @@
+
+
+## Security
+
+Microsoft takes the security of our software products and services seriously, which
+includes all source code repositories in our GitHub organizations.
+
+**Please do not report security vulnerabilities through public GitHub issues.**
+
+For security reporting information, locations, contact information, and policies,
+please review the latest guidance for Microsoft repositories at
+[https://aka.ms/SECURITY.md](https://aka.ms/SECURITY.md).
+
+
\ No newline at end of file
diff --git a/app.py b/app.py
index 594933e67701d4258f697e1c4799b85f37519ba3..f4feb3679cff39de6375ebfa657824ff327f1f2f 100644
--- a/app.py
+++ b/app.py
@@ -13,78 +13,17 @@ from datetime import datetime
import shutil
import cv2
from typing import *
+import torch
import numpy as np
from PIL import Image
import base64
import io
import tempfile
-
-# Lazy imports - will be loaded when GPU is available
-torch = None
-SparseTensor = None
-Trellis2ImageTo3DPipeline = None
-EnvMap = None
-render_utils = None
-o_voxel = None
-
-# Global state - initialized on first GPU call
-pipeline = None
-envmap = None
-_initialized = False
-
-
-def _lazy_import():
- """Import GPU-dependent modules. Must be called from within a @spaces.GPU function."""
- global torch, SparseTensor, Trellis2ImageTo3DPipeline, EnvMap, render_utils, o_voxel
- if torch is None:
- import torch as _torch
- torch = _torch
- if SparseTensor is None:
- from trellis2.modules.sparse import SparseTensor as _SparseTensor
- SparseTensor = _SparseTensor
- if Trellis2ImageTo3DPipeline is None:
- from trellis2.pipelines import Trellis2ImageTo3DPipeline as _Trellis2ImageTo3DPipeline
- Trellis2ImageTo3DPipeline = _Trellis2ImageTo3DPipeline
- if EnvMap is None:
- from trellis2.renderers import EnvMap as _EnvMap
- EnvMap = _EnvMap
- if render_utils is None:
- from trellis2.utils import render_utils as _render_utils
- render_utils = _render_utils
- if o_voxel is None:
- import o_voxel as _o_voxel
- o_voxel = _o_voxel
-
-
-def _initialize_pipeline():
- """Initialize the pipeline and environment maps. Must be called from within a @spaces.GPU function."""
- global pipeline, envmap, _initialized
- if _initialized:
- return
-
- _lazy_import()
-
- pipeline = Trellis2ImageTo3DPipeline.from_pretrained('microsoft/TRELLIS.2-4B')
- pipeline.rembg_model = None
- pipeline.low_vram = False
- pipeline.cuda()
-
- envmap = {
- 'forest': EnvMap(torch.tensor(
- cv2.cvtColor(cv2.imread('assets/hdri/forest.exr', cv2.IMREAD_UNCHANGED), cv2.COLOR_BGR2RGB),
- dtype=torch.float32, device='cuda'
- )),
- 'sunset': EnvMap(torch.tensor(
- cv2.cvtColor(cv2.imread('assets/hdri/sunset.exr', cv2.IMREAD_UNCHANGED), cv2.COLOR_BGR2RGB),
- dtype=torch.float32, device='cuda'
- )),
- 'courtyard': EnvMap(torch.tensor(
- cv2.cvtColor(cv2.imread('assets/hdri/courtyard.exr', cv2.IMREAD_UNCHANGED), cv2.COLOR_BGR2RGB),
- dtype=torch.float32, device='cuda'
- )),
- }
-
- _initialized = True
+from trellis2.modules.sparse import SparseTensor
+from trellis2.pipelines import Trellis2ImageTo3DPipeline
+from trellis2.renderers import EnvMap
+from trellis2.utils import render_utils
+import o_voxel
MAX_SEED = np.iinfo(np.int32).max
@@ -103,30 +42,54 @@ DEFAULT_STEP = 3
css = """
-/* ColmapView Dark Theme */
-:root {
- --body-background-fill: #0a0a0a !important;
- --background-fill-primary: #0f0f0f !important;
- --background-fill-secondary: #161616 !important;
- --block-background-fill: #161616 !important;
- --input-background-fill: #1a1a1a !important;
- --body-text-color: #e8e8e8 !important;
- --block-label-text-color: #8a8a8a !important;
- --block-title-text-color: #e8e8e8 !important;
- --border-color-primary: #2a2a2a !important;
- --color-accent: #b8b8b8 !important;
- --color-accent-soft: rgba(184, 184, 184, 0.15) !important;
- --button-primary-background-fill: #b8b8b8 !important;
- --button-primary-text-color: #0a0a0a !important;
+/* Overwrite Gradio Default Style */
+.stepper-wrapper {
+ padding: 0;
+}
+
+.stepper-container {
+ padding: 0;
+ align-items: center;
+}
+
+.step-button {
+ flex-direction: row;
+}
+
+.step-connector {
+ transform: none;
+}
+
+.step-number {
+ width: 16px;
+ height: 16px;
+}
+
+.step-label {
+ position: relative;
+ bottom: 0;
+}
+
+.wrap.center.full {
+ inset: 0;
+ height: 100%;
}
-body { background: #0a0a0a !important; }
-.gradio-container { background: #0f0f0f !important; }
-.dark { background: #0f0f0f !important; }
+.wrap.center.full.translucent {
+ background: var(--block-background-fill);
+}
+
+.meta-text-center {
+ display: block !important;
+ position: absolute !important;
+ top: unset !important;
+ bottom: 0 !important;
+ right: 0 !important;
+ transform: unset !important;
+}
-/* Previewer (required for custom HTML viewer) */
+/* Previewer */
.previewer-container {
- background: #0a0a0a;
position: relative;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
width: 100%;
@@ -177,6 +140,7 @@ body { background: #0a0a0a !important; }
opacity: 100%;
}
+/* Row 1: Display Modes */
.previewer-container .mode-row {
width: 100%;
display: flex;
@@ -202,6 +166,7 @@ body { background: #0a0a0a !important; }
transform: scale(1.1);
}
+/* Row 2: Display Image */
.previewer-container .display-row {
margin-bottom: 20px;
min-height: 400px;
@@ -222,6 +187,7 @@ body { background: #0a0a0a !important; }
display: block;
}
+/* Row 3: Custom HTML Slider */
.previewer-container .slider-row {
width: 100%;
display: flex;
@@ -259,6 +225,7 @@ body { background: #0a0a0a !important; }
transform: scale(1.2);
}
+/* Overwrite Previewer Block Style */
.gradio-container .padded:has(.previewer-container) {
padding: 0 !important;
}
@@ -288,9 +255,11 @@ head = """
}
// 2. Hide ALL images
+ // We select all elements with class 'previewer-main-image'
allImgs.forEach(img => img.classList.remove('visible'));
// 3. Construct the specific ID for the current state
+ // Format: view-m{mode}-s{step}
const targetId = 'view-m' + mode + '-s' + step;
const targetImg = document.getElementById(targetId);
@@ -320,10 +289,10 @@ head = """
"""
-empty_html = """
+empty_html = f"""
"""
@@ -343,8 +312,7 @@ def start_session(req: gr.Request):
def end_session(req: gr.Request):
user_dir = os.path.join(TMP_DIR, str(req.session_hash))
- if os.path.exists(user_dir):
- shutil.rmtree(user_dir)
+ shutil.rmtree(user_dir)
def remove_background(input: Image.Image) -> Image.Image:
@@ -357,7 +325,10 @@ def remove_background(input: Image.Image) -> Image.Image:
def preprocess_image(input: Image.Image) -> Image.Image:
- """Preprocess a single input image."""
+ """
+ Preprocess the input image.
+ """
+ # if has alpha channel, use it directly; otherwise, remove background
has_alpha = False
if input.mode == 'RGBA':
alpha = np.array(input)[:, :, 3]
@@ -379,7 +350,7 @@ def preprocess_image(input: Image.Image) -> Image.Image:
size = max(bbox[2] - bbox[0], bbox[3] - bbox[1])
size = int(size * 1)
bbox = center[0] - size // 2, center[1] - size // 2, center[0] + size // 2, center[1] + size // 2
- output = output.crop(bbox)
+ output = output.crop(bbox) # type: ignore
output = np.array(output).astype(np.float32) / 255
output = output[:, :, :3] * output[:, :, 3:4]
output = Image.fromarray((output * 255).astype(np.uint8))
@@ -387,16 +358,17 @@ def preprocess_image(input: Image.Image) -> Image.Image:
def preprocess_images(images: List[Tuple[Image.Image, str]]) -> List[Image.Image]:
- """Preprocess a list of input images. Uses parallel processing."""
- if not images:
- return []
- imgs = [img[0] if isinstance(img, tuple) else img for img in images]
- with ThreadPoolExecutor(max_workers=min(4, len(imgs))) as executor:
- processed_images = list(executor.map(preprocess_image, imgs))
+ """
+ Preprocess a list of input images for multi-image conditioning.
+ Uses parallel processing for faster background removal.
+ """
+ images = [image[0] for image in images]
+ with ThreadPoolExecutor(max_workers=min(4, len(images))) as executor:
+ processed_images = list(executor.map(preprocess_image, images))
return processed_images
-def pack_state(latents):
+def pack_state(latents: Tuple[SparseTensor, SparseTensor, int]) -> dict:
shape_slat, tex_slat, res = latents
return {
'shape_slat_feats': shape_slat.feats.cpu().numpy(),
@@ -406,8 +378,7 @@ def pack_state(latents):
}
-def unpack_state(state: dict):
- _lazy_import()
+def unpack_state(state: dict) -> Tuple[SparseTensor, SparseTensor, int]:
shape_slat = SparseTensor(
feats=torch.from_numpy(state['shape_slat_feats']).cuda(),
coords=torch.from_numpy(state['coords']).cuda(),
@@ -417,32 +388,33 @@ def unpack_state(state: dict):
def get_seed(randomize_seed: bool, seed: int) -> int:
+ """
+ Get the random seed.
+ """
return np.random.randint(0, MAX_SEED) if randomize_seed else seed
def prepare_multi_example() -> List[Image.Image]:
- """Prepare multi-image examples as concatenated images for gr.Examples."""
- example_dir = "assets/example_multi_image"
- if not os.path.exists(example_dir):
- return []
- cases = list(set([f.split('_')[0] for f in os.listdir(example_dir) if '_' in f and f.endswith('.png')]))
+ """
+ Prepare multi-image examples for the gallery.
+ """
+ multi_case = list(set([i.split('_')[0] for i in os.listdir("assets/example_multi_image")]))
images = []
- for case in sorted(cases):
- case_images = []
- for i in range(1, 10):
- img_path = f'{example_dir}/{case}_{i}.png'
- if os.path.exists(img_path):
- img = Image.open(img_path)
- W, H = img.size
- img = img.resize((int(W / H * 512), 512))
- case_images.append(np.array(img))
- if case_images:
- images.append(Image.fromarray(np.concatenate(case_images, axis=1)))
+ for case in multi_case:
+ _images = []
+ for i in range(1, 4):
+ img = Image.open(f'assets/example_multi_image/{case}_{i}.png')
+ W, H = img.size
+ img = img.resize((int(W / H * 512), 512))
+ _images.append(np.array(img))
+ images.append(Image.fromarray(np.concatenate(_images, axis=1)))
return images
def split_image(image: Image.Image) -> List[Image.Image]:
- """Split a concatenated multi-view image into separate images based on alpha."""
+ """
+ Split a concatenated image into multiple views.
+ """
image = np.array(image)
alpha = image[..., 3]
alpha = np.any(alpha > 0, axis=0)
@@ -451,12 +423,12 @@ def split_image(image: Image.Image) -> List[Image.Image]:
images = []
for s, e in zip(start_pos, end_pos):
images.append(Image.fromarray(image[:, s:e+1]))
- return [preprocess_image(img) for img in images]
+ return [preprocess_image(image) for image in images]
@spaces.GPU(duration=120)
def image_to_3d(
- images: List[Tuple[Image.Image, str]],
+ image: Image.Image,
seed: int,
resolution: str,
ss_guidance_strength: float,
@@ -471,24 +443,16 @@ def image_to_3d(
tex_slat_guidance_rescale: float,
tex_slat_sampling_steps: int,
tex_slat_rescale_t: float,
- multiimage_algo: Literal["multidiffusion", "stochastic"],
req: gr.Request,
progress=gr.Progress(track_tqdm=True),
+ multiimages: List[Tuple[Image.Image, str]] = None,
+ is_multiimage: bool = False,
+ multiimage_algo: Literal["multidiffusion", "stochastic"] = "stochastic",
) -> str:
- # Initialize pipeline on first call
- _initialize_pipeline()
-
- # Extract images from gallery format
- if not images:
- raise gr.Error("Please upload at least one image")
-
- imgs = [img[0] if isinstance(img, tuple) else img for img in images]
-
# --- Sampling ---
- if len(imgs) == 1:
- # Single image mode
+ if not is_multiimage:
outputs, latents = pipeline.run(
- imgs[0],
+ image,
seed=seed,
preprocess_image=False,
sparse_structure_sampler_params={
@@ -517,9 +481,8 @@ def image_to_3d(
return_latent=True,
)
else:
- # Multi-image mode
outputs, latents = pipeline.run_multi_image(
- imgs,
+ [image[0] for image in multiimages],
seed=seed,
preprocess_image=False,
sparse_structure_sampler_params={
@@ -548,44 +511,85 @@ def image_to_3d(
return_latent=True,
mode=multiimage_algo,
)
-
mesh = outputs[0]
- mesh.simplify(16777216)
- render_images = render_utils.render_snapshot(mesh, resolution=1024, r=2, fov=36, nviews=STEPS, envmap=envmap)
+ mesh.simplify(16777216) # nvdiffrast limit
+ images = render_utils.render_snapshot(mesh, resolution=1024, r=2, fov=36, nviews=STEPS, envmap=envmap)
state = pack_state(latents)
torch.cuda.empty_cache()
# --- HTML Construction ---
+ # The Stack of 48 Images - encode in parallel for speed
+ def encode_preview_image(args):
+ m_idx, s_idx, render_key = args
+ img_base64 = image_to_base64(Image.fromarray(images[render_key][s_idx]))
+ return (m_idx, s_idx, img_base64)
+
+ encode_tasks = [
+ (m_idx, s_idx, mode['render_key'])
+ for m_idx, mode in enumerate(MODES)
+ for s_idx in range(STEPS)
+ ]
+
+ with ThreadPoolExecutor(max_workers=8) as executor:
+ encoded_results = list(executor.map(encode_preview_image, encode_tasks))
+
+ # Build HTML from encoded results
+ encoded_map = {(m, s): b64 for m, s, b64 in encoded_results}
images_html = ""
for m_idx, mode in enumerate(MODES):
for s_idx in range(STEPS):
unique_id = f"view-m{m_idx}-s{s_idx}"
is_visible = (m_idx == DEFAULT_MODE and s_idx == DEFAULT_STEP)
vis_class = "visible" if is_visible else ""
- img_base64 = image_to_base64(Image.fromarray(render_images[mode['render_key']][s_idx]))
- images_html += f'
'
+ img_base64 = encoded_map[(m_idx, s_idx)]
+
+ images_html += f"""
+
+ """
+ # Button Row HTML
btns_html = ""
for idx, mode in enumerate(MODES):
active_class = "active" if idx == DEFAULT_MODE else ""
- btns_html += f'
'
-
+ # Note: onclick calls the JS function defined in Head
+ btns_html += f"""
+
+ """
+
+ # Assemble the full component
full_html = f"""
-
Tips
+
💡Tips
-
Render Mode - Click buttons to switch render modes.
-
View Angle - Drag slider to change view.
+
â— Render Mode - Click on the circular buttons to switch between different render modes.
+
â— View Angle - Drag the slider to change the view angle.
-
{images_html}
-
{btns_html}
+
+
+
+ {images_html}
+
+
+
+
+ {btns_html}
+
+
+
"""
+
return state, full_html
@@ -597,12 +601,21 @@ def extract_glb(
req: gr.Request,
progress=gr.Progress(track_tqdm=True),
) -> Tuple[str, str]:
- _initialize_pipeline()
+ """
+ Extract a GLB file from the 3D model.
+
+ Args:
+ state (dict): The state of the generated 3D model.
+ decimation_target (int): The target face count for decimation.
+ texture_size (int): The texture resolution.
+ Returns:
+ str: The path to the extracted GLB file.
+ """
user_dir = os.path.join(TMP_DIR, str(req.session_hash))
shape_slat, tex_slat, res = unpack_state(state)
mesh = pipeline.decode_latent(shape_slat, tex_slat, res)[0]
- mesh.simplify(16777216)
+ mesh.simplify(16777216) # nvdiffrast limit
glb = o_voxel.postprocess.to_glb(
vertices=mesh.vertices,
faces=mesh.faces,
@@ -629,22 +642,22 @@ def extract_glb(
with gr.Blocks(delete_cache=(600, 600)) as demo:
gr.Markdown("""
- ## Multi-View Image to 3D Asset with [TRELLIS.2](https://microsoft.github.io/TRELLIS.2)
- * Upload one or more images and click Generate to create a 3D asset.
- * Multiple views from different angles will produce better results.
- * Click Extract GLB to export and download the generated GLB file.
+ ## Image to 3D Asset with [TRELLIS.2](https://microsoft.github.io/TRELLIS.2)
+ * Upload an image (preferably with an alpha-masked foreground object) and click Generate to create a 3D asset.
+ * Click Extract GLB to export and download the generated GLB file if you're satisfied with the result. Otherwise, try another time.
""")
with gr.Row():
with gr.Column(scale=1, min_width=360):
- image_prompt = gr.Gallery(
- label="Input Images",
- format="png",
- type="pil",
- height=400,
- columns=3,
- object_fit="contain"
- )
+ with gr.Tabs() as input_tabs:
+ with gr.Tab(label="Single Image", id=0) as single_image_input_tab:
+ image_prompt = gr.Image(label="Image Prompt", format="png", image_mode="RGBA", type="pil", height=400)
+ with gr.Tab(label="Multiple Images", id=1) as multiimage_input_tab:
+ multiimage_prompt = gr.Gallery(label="Image Prompt", format="png", type="pil", height=400, columns=3)
+ gr.Markdown("""
+ Input different views of the object in separate images.
+ *NOTE: this is an experimental algorithm without training a specialized model. It may not produce the best results for all images, especially those having different poses or inconsistent details.*
+ """)
resolution = gr.Radio(["512", "1024", "1536"], label="Resolution", value="1024")
seed = gr.Slider(0, MAX_SEED, label="Seed", value=0, step=1)
@@ -676,44 +689,73 @@ with gr.Blocks(delete_cache=(600, 600)) as demo:
multiimage_algo = gr.Radio(["stochastic", "multidiffusion"], label="Multi-image Algorithm", value="stochastic")
with gr.Column(scale=10):
- with gr.Walkthrough(selected=0) as walkthrough:
- with gr.Step("Preview", id=0):
+ with gr.Tabs() as tabs:
+ with gr.Tab("Preview", id=0):
preview_output = gr.HTML(empty_html, label="3D Asset Preview", show_label=True, container=True)
extract_btn = gr.Button("Extract GLB")
- with gr.Step("Extract", id=1):
+ with gr.Tab("Extract", id=1):
glb_output = gr.Model3D(label="Extracted GLB", height=724, show_label=True, display_mode="solid", clear_color=(0.25, 0.25, 0.25, 1.0))
download_btn = gr.DownloadButton(label="Download GLB")
- gr.Markdown("*GLB extraction may take 30+ seconds.*")
+ gr.Markdown("*We are actively working on improving the speed of GLB extraction. Currently, it may take half a minute or more and face count is limited.*")
- with gr.Column(scale=1, min_width=200):
- # Hidden image for examples input
- example_image = gr.Image(visible=False, type="pil", image_mode="RGBA")
- gr.Markdown("### Multi-View Examples")
+ with gr.Column(scale=1, min_width=172) as single_image_example:
examples = gr.Examples(
+ examples=[
+ f'assets/example_image/{image}'
+ for image in os.listdir("assets/example_image")
+ ],
+ inputs=[image_prompt],
+ fn=preprocess_image,
+ outputs=[image_prompt],
+ run_on_click=True,
+ examples_per_page=18,
+ )
+
+ with gr.Column(visible=True) as multiimage_example:
+ examples_multi = gr.Examples(
examples=prepare_multi_example(),
- inputs=[example_image],
+ label="Multi Image Examples",
+ inputs=[image_prompt],
fn=split_image,
- outputs=[image_prompt],
+ outputs=[multiimage_prompt],
run_on_click=True,
- examples_per_page=12,
+ examples_per_page=8,
)
+ is_multiimage = gr.State(False)
output_buf = gr.State()
+
# Handlers
demo.load(start_session)
demo.unload(end_session)
+ single_image_input_tab.select(
+ lambda: (False, gr.update(visible=True), gr.update(visible=True)),
+ outputs=[is_multiimage, single_image_example, multiimage_example]
+ )
+ multiimage_input_tab.select(
+ lambda: (True, gr.update(visible=True), gr.update(visible=True)),
+ outputs=[is_multiimage, single_image_example, multiimage_example]
+ )
+
image_prompt.upload(
- preprocess_images,
+ preprocess_image,
inputs=[image_prompt],
outputs=[image_prompt],
)
+ multiimage_prompt.upload(
+ preprocess_images,
+ inputs=[multiimage_prompt],
+ outputs=[multiimage_prompt],
+ )
generate_btn.click(
- get_seed, inputs=[randomize_seed, seed], outputs=[seed],
+ get_seed,
+ inputs=[randomize_seed, seed],
+ outputs=[seed],
).then(
- lambda: gr.Walkthrough(selected=0), outputs=walkthrough
+ lambda: gr.Tabs(selected=0), outputs=tabs
).then(
image_to_3d,
inputs=[
@@ -721,13 +763,13 @@ with gr.Blocks(delete_cache=(600, 600)) as demo:
ss_guidance_strength, ss_guidance_rescale, ss_sampling_steps, ss_rescale_t,
shape_slat_guidance_strength, shape_slat_guidance_rescale, shape_slat_sampling_steps, shape_slat_rescale_t,
tex_slat_guidance_strength, tex_slat_guidance_rescale, tex_slat_sampling_steps, tex_slat_rescale_t,
- multiimage_algo
+ multiimage_prompt, is_multiimage, multiimage_algo
],
outputs=[output_buf, preview_output],
)
extract_btn.click(
- lambda: gr.Walkthrough(selected=1), outputs=walkthrough
+ lambda: gr.Tabs(selected=1), outputs=tabs
).then(
extract_glb,
inputs=[output_buf, decimation_target, texture_size],
@@ -735,13 +777,35 @@ with gr.Blocks(delete_cache=(600, 600)) as demo:
)
+# Launch the Gradio app
if __name__ == "__main__":
os.makedirs(TMP_DIR, exist_ok=True)
+ # Construct ui components
+ btn_img_base64_strs = {}
for i in range(len(MODES)):
icon = Image.open(MODES[i]['icon'])
MODES[i]['icon_base64'] = image_to_base64(icon)
rmbg_client = Client("briaai/BRIA-RMBG-2.0")
+ pipeline = Trellis2ImageTo3DPipeline.from_pretrained('microsoft/TRELLIS.2-4B')
+ pipeline.rembg_model = None
+ pipeline.low_vram = False
+ pipeline.cuda()
+
+ envmap = {
+ 'forest': EnvMap(torch.tensor(
+ cv2.cvtColor(cv2.imread('assets/hdri/forest.exr', cv2.IMREAD_UNCHANGED), cv2.COLOR_BGR2RGB),
+ dtype=torch.float32, device='cuda'
+ )),
+ 'sunset': EnvMap(torch.tensor(
+ cv2.cvtColor(cv2.imread('assets/hdri/sunset.exr', cv2.IMREAD_UNCHANGED), cv2.COLOR_BGR2RGB),
+ dtype=torch.float32, device='cuda'
+ )),
+ 'courtyard': EnvMap(torch.tensor(
+ cv2.cvtColor(cv2.imread('assets/hdri/courtyard.exr', cv2.IMREAD_UNCHANGED), cv2.COLOR_BGR2RGB),
+ dtype=torch.float32, device='cuda'
+ )),
+ }
demo.launch(css=css, head=head)
diff --git a/app_texturing.py b/app_texturing.py
new file mode 100644
index 0000000000000000000000000000000000000000..a4e144eea3e05420a98854742c121bee9c3cb883
--- /dev/null
+++ b/app_texturing.py
@@ -0,0 +1,151 @@
+import gradio as gr
+
+import os
+os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "expandable_segments:True"
+from datetime import datetime
+import shutil
+from typing import *
+import torch
+import numpy as np
+import trimesh
+from PIL import Image
+from trellis2.pipelines import Trellis2TexturingPipeline
+
+
+MAX_SEED = np.iinfo(np.int32).max
+TMP_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'tmp')
+
+
+def start_session(req: gr.Request):
+ user_dir = os.path.join(TMP_DIR, str(req.session_hash))
+ os.makedirs(user_dir, exist_ok=True)
+
+
+def end_session(req: gr.Request):
+ user_dir = os.path.join(TMP_DIR, str(req.session_hash))
+ shutil.rmtree(user_dir)
+
+
+def preprocess_image(image: Image.Image) -> Image.Image:
+ """
+ Preprocess the input image.
+
+ Args:
+ image (Image.Image): The input image.
+
+ Returns:
+ Image.Image: The preprocessed image.
+ """
+ processed_image = pipeline.preprocess_image(image)
+ return processed_image
+
+
+def get_seed(randomize_seed: bool, seed: int) -> int:
+ """
+ Get the random seed.
+ """
+ return np.random.randint(0, MAX_SEED) if randomize_seed else seed
+
+
+def shapeimage_to_tex(
+ mesh_file: str,
+ image: Image.Image,
+ seed: int,
+ resolution: str,
+ texture_size: int,
+ tex_slat_guidance_strength: float,
+ tex_slat_guidance_rescale: float,
+ tex_slat_sampling_steps: int,
+ tex_slat_rescale_t: float,
+ req: gr.Request,
+ progress=gr.Progress(track_tqdm=True),
+) -> str:
+ mesh = trimesh.load(mesh_file)
+ if isinstance(mesh, trimesh.Scene):
+ mesh = mesh.to_mesh()
+ output = pipeline.run(
+ mesh,
+ image,
+ seed=seed,
+ preprocess_image=False,
+ tex_slat_sampler_params={
+ "steps": tex_slat_sampling_steps,
+ "guidance_strength": tex_slat_guidance_strength,
+ "guidance_rescale": tex_slat_guidance_rescale,
+ "rescale_t": tex_slat_rescale_t,
+ },
+ resolution=int(resolution),
+ texture_size=texture_size,
+ )
+ now = datetime.now()
+ timestamp = now.strftime("%Y-%m-%dT%H%M%S") + f".{now.microsecond // 1000:03d}"
+ user_dir = os.path.join(TMP_DIR, str(req.session_hash))
+ os.makedirs(user_dir, exist_ok=True)
+ glb_path = os.path.join(user_dir, f'sample_{timestamp}.glb')
+ output.export(glb_path, extension_webp=True)
+ torch.cuda.empty_cache()
+ return glb_path, glb_path
+
+
+with gr.Blocks(delete_cache=(600, 600)) as demo:
+ gr.Markdown("""
+ ## Texturing a mesh with [TRELLIS.2](https://microsoft.github.io/TRELLIS.2)
+ * Upload a mesh and corresponding reference image (preferably with an alpha-masked foreground object) and click Generate to create a textured 3D asset.
+ """)
+
+ with gr.Row():
+ with gr.Column(scale=1, min_width=360):
+ mesh_file = gr.File(label="Upload Mesh", file_types=[".ply", ".obj", ".glb", ".gltf"], file_count="single")
+ image_prompt = gr.Image(label="Image Prompt", format="png", image_mode="RGBA", type="pil", height=400)
+
+ resolution = gr.Radio(["512", "1024", "1536"], label="Resolution", value="1024")
+ seed = gr.Slider(0, MAX_SEED, label="Seed", value=0, step=1)
+ randomize_seed = gr.Checkbox(label="Randomize Seed", value=True)
+ texture_size = gr.Slider(1024, 4096, label="Texture Size", value=2048, step=1024)
+
+ generate_btn = gr.Button("Generate")
+
+ with gr.Accordion(label="Advanced Settings", open=False):
+ with gr.Row():
+ tex_slat_guidance_strength = gr.Slider(1.0, 10.0, label="Guidance Strength", value=1.0, step=0.1)
+ tex_slat_guidance_rescale = gr.Slider(0.0, 1.0, label="Guidance Rescale", value=0.0, step=0.01)
+ tex_slat_sampling_steps = gr.Slider(1, 50, label="Sampling Steps", value=12, step=1)
+ tex_slat_rescale_t = gr.Slider(1.0, 6.0, label="Rescale T", value=3.0, step=0.1)
+
+ with gr.Column(scale=10):
+ glb_output = gr.Model3D(label="Extracted GLB", height=724, show_label=True, display_mode="solid", clear_color=(0.25, 0.25, 0.25, 1.0))
+ download_btn = gr.DownloadButton(label="Download GLB")
+
+
+ # Handlers
+ demo.load(start_session)
+ demo.unload(end_session)
+
+ image_prompt.upload(
+ preprocess_image,
+ inputs=[image_prompt],
+ outputs=[image_prompt],
+ )
+
+ generate_btn.click(
+ get_seed,
+ inputs=[randomize_seed, seed],
+ outputs=[seed],
+ ).then(
+ shapeimage_to_tex,
+ inputs=[
+ mesh_file, image_prompt, seed, resolution, texture_size,
+ tex_slat_guidance_strength, tex_slat_guidance_rescale, tex_slat_sampling_steps, tex_slat_rescale_t,
+ ],
+ outputs=[glb_output, download_btn],
+ )
+
+
+# Launch the Gradio app
+if __name__ == "__main__":
+ os.makedirs(TMP_DIR, exist_ok=True)
+
+ pipeline = Trellis2TexturingPipeline.from_pretrained('microsoft/TRELLIS.2-4B', config_file="texturing_pipeline.json")
+ pipeline.cuda()
+
+ demo.launch()
diff --git a/o-voxel/README.md b/o-voxel/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..0ced733c3b168aea690abb36ef19a96e3d68b6d9
--- /dev/null
+++ b/o-voxel/README.md
@@ -0,0 +1,174 @@
+# O-Voxel: A Native 3D Representation
+
+**O-Voxel** is a sparse, voxel-based native 3D representation designed for high-quality 3D generation and reconstruction. Unlike traditional methods that rely on fields (e.g., Occupancy fields, SDFs), O-Voxel utilizes a **Flexible Dual Grid** formulation to robustly represent surfaces with arbitrary topology (including non-manifold and open surfaces) and **volumetric surface properties** such as Physically-Based Rendering (PBR) material attributes.
+
+This library provides an efficient implementation for the instant bidirectional conversion between Meshes and O-Voxels, along with tools for sparse voxel compression, serialization, and rendering.
+
+
+
+## Key Features
+
+- **🧱 Flexible Dual Grid**: A geometry representation that solves a enhanced QEF (Quadratic Error Function) to accurately capture sharp features and open boundaries without requiring watertight meshes.
+- **🎨 Volumetric PBR Attributes**: Native support for physically-based rendering properties (Base Color, Metallic, Roughness, Opacity) aligned with the sparse voxel grid.
+- **âš¡ Instant Bidirectional Conversion**: Rapid `Mesh <-> O-Voxel` conversion without expensive SDF evaluation, flood-filling, or iterative optimization.
+- **💾 Efficient Compression**: Supports custom `.vxz` format for compact storage of sparse voxel structures using Z-order/Hilbert curve encoding.
+- **ðŸ› ï¸ Production Ready**: Tools to export converted assets directly to `.glb` with UV unwrapping and texture baking.
+
+## Installation
+
+```bash
+git clone -b main https://github.com/microsoft/TRELLIS.2.git --recursive
+pip install TRELLIS.2/o_voxel --no-build-isolation
+```
+
+## Quick Start
+
+> See also the [examples](examples) directory for more detailed usage.
+
+### 1. Convert Mesh to O-Voxel [[link]](examples/mesh2ovox.py)
+Convert a standard 3D mesh (with textures) into the O-Voxel representation.
+
+```python
+asset = trimesh.load("path/to/mesh.glb")
+
+# 1. Geometry Voxelization (Flexible Dual Grid)
+# Returns: occupied indices, dual vertices (QEF solution), and edge intersected
+mesh = asset.to_mesh()
+vertices = torch.from_numpy(mesh.vertices).float()
+faces = torch.from_numpy(mesh.faces).long()
+voxel_indices, dual_vertices, intersected = o_voxel.convert.mesh_to_flexible_dual_grid(
+ vertices, faces,
+ grid_size=RES, # Resolution
+ aabb=[[-0.5,-0.5,-0.5],[0.5,0.5,0.5]], # Axis-aligned bounding box
+ face_weight=1.0, # Face term weight in QEF
+ boundary_weight=0.2, # Boundary term weight in QEF
+ regularization_weight=1e-2, # Regularization term weight in QEF
+ timing=True
+)
+## sort to ensure align between geometry and material voxelization
+vid = o_voxel.serialize.encode_seq(voxel_indices)
+mapping = torch.argsort(vid)
+voxel_indices = voxel_indices[mapping]
+dual_vertices = dual_vertices[mapping]
+intersected = intersected[mapping]
+
+# 2. Material Voxelization (Volumetric Attributes)
+# Returns: dict containing 'base_color', 'metallic', 'roughness', etc.
+voxel_indices_mat, attributes = o_voxel.convert.textured_mesh_to_volumetric_attr(
+ asset,
+ grid_size=RES,
+ aabb=[[-0.5,-0.5,-0.5],[0.5,0.5,0.5]],
+ timing=True
+)
+## sort to ensure align between geometry and material voxelization
+vid_mat = o_voxel.serialize.encode_seq(voxel_indices_mat)
+mapping_mat = torch.argsort(vid_mat)
+attributes = {k: v[mapping_mat] for k, v in attributes.items()}
+
+# Save to compressed .vxz format
+## packing
+dual_vertices = dual_vertices * RES - voxel_indices
+dual_vertices = (torch.clamp(dual_vertices, 0, 1) * 255).type(torch.uint8)
+intersected = (intersected[:, 0:1] + 2 * intersected[:, 1:2] + 4 * intersected[:, 2:3]).type(torch.uint8)
+attributes['dual_vertices'] = dual_vertices
+attributes['intersected'] = intersected
+o_voxel.io.write("ovoxel_helmet.vxz", voxel_indices, attributes)
+```
+
+### 2. Recover Mesh from O-Voxel [[link]](examples/ovox2mesh.py)
+Reconstruct the surface mesh from the sparse voxel data.
+
+```python
+# Load data
+coords, data = o_voxel.io.read("path/to/ovoxel.vxz")
+dual_vertices = data['dual_vertices']
+intersected = data['intersected']
+base_color = data['base_color']
+## ... other attributes omitted for brevity
+
+# Depack
+dual_vertices = dual_vertices / 255
+intersected = torch.cat([
+ intersected % 2,
+ intersected // 2 % 2,
+ intersected // 4 % 2,
+], dim=-1).bool()
+
+# Extract Mesh
+# O-Voxel connects dual vertices to form quads, optionally splitting them
+# based on geometric features.
+rec_verts, rec_faces = o_voxel.convert.flexible_dual_grid_to_mesh(
+ coords.cuda(),
+ dual_vertices.cuda(),
+ intersected.cuda(),
+ split_weight=None, # Auto-split based on min angle if None
+ grid_size=RES,
+ aabb=[[-0.5,-0.5,-0.5],[0.5,0.5,0.5]],
+)
+```
+
+### 3. Export to GLB [[link]](examples/ovox2glb.py)
+For visualization in standard 3D viewers, you can clean, UV-unwrap, and bake the volumetric attributes into textures.
+
+```python
+# Assuming you have the reconstructed verts/faces and volume attributes
+mesh = o_voxel.postprocess.to_glb(
+ vertices=rec_verts,
+ faces=rec_faces,
+ attr_volume=attr_tensor, # Concatenated attributes
+ coords=coords,
+ attr_layout={'base_color': slice(0,3), 'metallic': slice(3,4), ...},
+ grid_size=RES,
+ aabb=[[-0.5,-0.5,-0.5],[0.5,0.5,0.5]],
+ decimation_target=100000,
+ texture_size=2048,
+ verbose=True,
+)
+mesh.export("rec_helmet.glb")
+```
+
+### 4. Voxel Rendering [[link]](examples/render_ovox.py)
+Render the voxel representation directly.
+
+```python
+# Load data
+coords, data = o_voxel.io.read("ovoxel_helmet.vxz")
+position = (coords / RES - 0.5).cuda()
+base_color = (data['base_color'] / 255).cuda()
+
+# Render
+renderer = o_voxel.rasterize.VoxelRenderer(
+ rendering_options={"resolution": 512, "ssaa": 2}
+)
+output = renderer.render(
+ position=position, # Voxel centers
+ attrs=base_color, # Color/Opacity etc.
+ voxel_size=1.0/RES,
+ extrinsics=extr,
+ intrinsics=intr
+)
+# output.attr contains the rendered image (C, H, W)
+```
+
+## API Overview
+
+### `o_voxel.convert`
+Core algorithms for the conversion between meshes and O-Voxels.
+* `mesh_to_flexible_dual_grid`: Determines the active sparse voxels and solves the QEF to determine dual vertex positions within voxels based on mesh-voxel grid intersections.
+* `flexible_dual_grid_to_mesh`: Reconnects dual vertices to form a surface.
+* `textured_mesh_to_volumetric_attr`: Samples texture maps into voxel space.
+
+### `o_voxel.io`
+Handles sparse voxel file I/O operations.
+* **Formats**: `.npz` (NumPy), `.ply` (Point Cloud), `.vxz` (Custom compressed, recommended).
+* **Functions**: `read()`, `write()`.
+
+### `o_voxel.serialize`
+Utilities for spatial hashing and ordering.
+* `encode_seq` / `decode_seq`: Converts 3D coordinates to/from Morton codes (Z-order) or Hilbert curves for efficient storage and processing.
+
+### `o_voxel.rasterize`
+* `VoxelRenderer`: A lightweight renderer for sparse voxel visualization during training.
+
+### `o_voxel.postprocess`
+* `to_glb`: A comprehensive pipeline for mesh cleaning, remeshing, UV unwrapping, and texture baking.
diff --git a/o-voxel/assets/overview.webp b/o-voxel/assets/overview.webp
new file mode 100644
index 0000000000000000000000000000000000000000..5317e1364e4f0d497369cca7d00d4698bc339903
--- /dev/null
+++ b/o-voxel/assets/overview.webp
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ba51a74520803e36ec5be8e1aab1c71fea8a8df78a0502f0736e338ff4e97f93
+size 391770
diff --git a/o-voxel/build/lib.win-amd64-cpython-311/o_voxel/__init__.py b/o-voxel/build/lib.win-amd64-cpython-311/o_voxel/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..55ffd986b47551517fbf1d1538b40f77ec5ee8f8
--- /dev/null
+++ b/o-voxel/build/lib.win-amd64-cpython-311/o_voxel/__init__.py
@@ -0,0 +1,7 @@
+from . import (
+ convert,
+ io,
+ postprocess,
+ rasterize,
+ serialize
+)
\ No newline at end of file
diff --git a/o-voxel/build/lib.win-amd64-cpython-311/o_voxel/convert/__init__.py b/o-voxel/build/lib.win-amd64-cpython-311/o_voxel/convert/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..25755f9b06a2cf37856f3be043e3928a5e23510c
--- /dev/null
+++ b/o-voxel/build/lib.win-amd64-cpython-311/o_voxel/convert/__init__.py
@@ -0,0 +1,2 @@
+from .flexible_dual_grid import *
+from .volumetic_attr import *
\ No newline at end of file
diff --git a/o-voxel/build/lib.win-amd64-cpython-311/o_voxel/convert/flexible_dual_grid.py b/o-voxel/build/lib.win-amd64-cpython-311/o_voxel/convert/flexible_dual_grid.py
new file mode 100644
index 0000000000000000000000000000000000000000..51b8b0552fe697e95f7496a370a56c538c8abd10
--- /dev/null
+++ b/o-voxel/build/lib.win-amd64-cpython-311/o_voxel/convert/flexible_dual_grid.py
@@ -0,0 +1,283 @@
+from typing import *
+import numpy as np
+import torch
+from .. import _C
+
+__all__ = [
+ "mesh_to_flexible_dual_grid",
+ "flexible_dual_grid_to_mesh",
+]
+
+
+def _init_hashmap(grid_size, capacity, device):
+ VOL = (grid_size[0] * grid_size[1] * grid_size[2]).item()
+
+ # If the number of elements in the tensor is less than 2^32, use uint32 as the hashmap type, otherwise use uint64.
+ if VOL < 2**32:
+ hashmap_keys = torch.full((capacity,), torch.iinfo(torch.uint32).max, dtype=torch.uint32, device=device)
+ elif VOL < 2**64:
+ hashmap_keys = torch.full((capacity,), torch.iinfo(torch.uint64).max, dtype=torch.uint64, device=device)
+ else:
+ raise ValueError(f"The spatial size is too large to fit in a hashmap. Get volumn {VOL} > 2^64.")
+
+ hashmap_vals = torch.empty((capacity,), dtype=torch.uint32, device=device)
+
+ return hashmap_keys, hashmap_vals
+
+
+@torch.no_grad()
+def mesh_to_flexible_dual_grid(
+ vertices: torch.Tensor,
+ faces: torch.Tensor,
+ voxel_size: Union[float, list, tuple, np.ndarray, torch.Tensor] = None,
+ grid_size: Union[int, list, tuple, np.ndarray, torch.Tensor] = None,
+ aabb: Union[list, tuple, np.ndarray, torch.Tensor] = None,
+ face_weight: float = 1.0,
+ boundary_weight: float = 1.0,
+ regularization_weight: float = 0.1,
+ timing: bool = False,
+) -> Union[torch.Tensor, torch.Tensor, torch.Tensor]:
+ """
+ Voxelize a mesh into a sparse voxel grid.
+
+ Args:
+ vertices (torch.Tensor): The vertices of the mesh.
+ faces (torch.Tensor): The faces of the mesh.
+ voxel_size (float, list, tuple, np.ndarray, torch.Tensor): The size of each voxel.
+ grid_size (int, list, tuple, np.ndarray, torch.Tensor): The size of the grid.
+ NOTE: One of voxel_size and grid_size must be provided.
+ aabb (list, tuple, np.ndarray, torch.Tensor): The axis-aligned bounding box of the mesh.
+ If not provided, it will be computed automatically.
+ face_weight (float): The weight of the face term in the QEF when solving the dual vertices.
+ boundary_weight (float): The weight of the boundary term in the QEF when solving the dual vertices.
+ regularization_weight (float): The weight of the regularization term in the QEF when solving the dual vertices.
+ timing (bool): Whether to time the voxelization process.
+
+ Returns:
+ torch.Tensor: The indices of the voxels that are occupied by the mesh.
+ The shape of the tensor is (N, 3), where N is the number of occupied voxels.
+ torch.Tensor: The dual vertices of the mesh.
+ torch.Tensor: The intersected flag of each voxel.
+ """
+
+ # Load mesh
+ vertices = vertices.float()
+ faces = faces.int()
+
+ # Voxelize settings
+ assert voxel_size is not None or grid_size is not None, "Either voxel_size or grid_size must be provided"
+
+ if voxel_size is not None:
+ if isinstance(voxel_size, float):
+ voxel_size = [voxel_size, voxel_size, voxel_size]
+ if isinstance(voxel_size, (list, tuple)):
+ voxel_size = np.array(voxel_size)
+ if isinstance(voxel_size, np.ndarray):
+ voxel_size = torch.tensor(voxel_size, dtype=torch.float32)
+ assert isinstance(voxel_size, torch.Tensor), f"voxel_size must be a float, list, tuple, np.ndarray, or torch.Tensor, but got {type(voxel_size)}"
+ assert voxel_size.dim() == 1, f"voxel_size must be a 1D tensor, but got {voxel_size.shape}"
+ assert voxel_size.size(0) == 3, f"voxel_size must have 3 elements, but got {voxel_size.size(0)}"
+
+ if grid_size is not None:
+ if isinstance(grid_size, int):
+ grid_size = [grid_size, grid_size, grid_size]
+ if isinstance(grid_size, (list, tuple)):
+ grid_size = np.array(grid_size)
+ if isinstance(grid_size, np.ndarray):
+ grid_size = torch.tensor(grid_size, dtype=torch.int32)
+ assert isinstance(grid_size, torch.Tensor), f"grid_size must be an int, list, tuple, np.ndarray, or torch.Tensor, but got {type(grid_size)}"
+ assert grid_size.dim() == 1, f"grid_size must be a 1D tensor, but got {grid_size.shape}"
+ assert grid_size.size(0) == 3, f"grid_size must have 3 elements, but got {grid_size.size(0)}"
+
+ if aabb is not None:
+ if isinstance(aabb, (list, tuple)):
+ aabb = np.array(aabb)
+ if isinstance(aabb, np.ndarray):
+ aabb = torch.tensor(aabb, dtype=torch.float32)
+ assert isinstance(aabb, torch.Tensor), f"aabb must be a list, tuple, np.ndarray, or torch.Tensor, but got {type(aabb)}"
+ assert aabb.dim() == 2, f"aabb must be a 2D tensor, but got {aabb.shape}"
+ assert aabb.size(0) == 2, f"aabb must have 2 rows, but got {aabb.size(0)}"
+ assert aabb.size(1) == 3, f"aabb must have 3 columns, but got {aabb.size(1)}"
+
+ # Auto adjust aabb
+ if aabb is None:
+ min_xyz = vertices.min(dim=0).values
+ max_xyz = vertices.max(dim=0).values
+
+ if voxel_size is not None:
+ padding = torch.ceil((max_xyz - min_xyz) / voxel_size) * voxel_size - (max_xyz - min_xyz)
+ min_xyz -= padding * 0.5
+ max_xyz += padding * 0.5
+ if grid_size is not None:
+ padding = (max_xyz - min_xyz) / (grid_size - 1)
+ min_xyz -= padding * 0.5
+ max_xyz += padding * 0.5
+
+ aabb = torch.stack([min_xyz, max_xyz], dim=0).float().cuda()
+
+ # Fill voxel size or grid size
+ if voxel_size is None:
+ voxel_size = (aabb[1] - aabb[0]) / grid_size
+ if grid_size is None:
+ grid_size = ((aabb[1] - aabb[0]) / voxel_size).round().int()
+
+ # subdivide mesh
+ vertices = vertices - aabb[0].reshape(1, 3)
+ grid_range = torch.stack([torch.zeros_like(grid_size), grid_size], dim=0).int()
+
+ ret = _C.mesh_to_flexible_dual_grid_cpu(
+ vertices,
+ faces,
+ voxel_size,
+ grid_range,
+ face_weight,
+ boundary_weight,
+ regularization_weight,
+ timing,
+ )
+
+ return ret
+
+
+def flexible_dual_grid_to_mesh(
+ coords: torch.Tensor,
+ dual_vertices: torch.Tensor,
+ intersected_flag: torch.Tensor,
+ split_weight: Union[torch.Tensor, None],
+ aabb: Union[list, tuple, np.ndarray, torch.Tensor],
+ voxel_size: Union[float, list, tuple, np.ndarray, torch.Tensor] = None,
+ grid_size: Union[int, list, tuple, np.ndarray, torch.Tensor] = None,
+ train: bool = False,
+):
+ """
+ Extract mesh from sparse voxel structures using flexible dual grid.
+
+ Args:
+ coords (torch.Tensor): The coordinates of the voxels.
+ dual_vertices (torch.Tensor): The dual vertices.
+ intersected_flag (torch.Tensor): The intersected flag.
+ split_weight (torch.Tensor): The split weight of each dual quad. If None, the algorithm
+ will split based on minimum angle.
+ aabb (list, tuple, np.ndarray, torch.Tensor): The axis-aligned bounding box of the mesh.
+ voxel_size (float, list, tuple, np.ndarray, torch.Tensor): The size of each voxel.
+ grid_size (int, list, tuple, np.ndarray, torch.Tensor): The size of the grid.
+ NOTE: One of voxel_size and grid_size must be provided.
+ train (bool): Whether to use training mode.
+
+ Returns:
+ vertices (torch.Tensor): The vertices of the mesh.
+ faces (torch.Tensor): The faces of the mesh.
+ """
+ # Static variables
+ if not hasattr(flexible_dual_grid_to_mesh, "edge_neighbor_voxel_offset"):
+ flexible_dual_grid_to_mesh.edge_neighbor_voxel_offset = torch.tensor([
+ [[0, 0, 0], [0, 0, 1], [0, 1, 1], [0, 1, 0]], # x-axis
+ [[0, 0, 0], [1, 0, 0], [1, 0, 1], [0, 0, 1]], # y-axis
+ [[0, 0, 0], [0, 1, 0], [1, 1, 0], [1, 0, 0]], # z-axis
+ ], dtype=torch.int, device=coords.device).unsqueeze(0)
+ if not hasattr(flexible_dual_grid_to_mesh, "quad_split_1"):
+ flexible_dual_grid_to_mesh.quad_split_1 = torch.tensor([0, 1, 2, 0, 2, 3], dtype=torch.long, device=coords.device, requires_grad=False)
+ if not hasattr(flexible_dual_grid_to_mesh, "quad_split_2"):
+ flexible_dual_grid_to_mesh.quad_split_2 = torch.tensor([0, 1, 3, 3, 1, 2], dtype=torch.long, device=coords.device, requires_grad=False)
+ if not hasattr(flexible_dual_grid_to_mesh, "quad_split_train"):
+ flexible_dual_grid_to_mesh.quad_split_train = torch.tensor([0, 1, 4, 1, 2, 4, 2, 3, 4, 3, 0, 4], dtype=torch.long, device=coords.device, requires_grad=False)
+
+ # AABB
+ if isinstance(aabb, (list, tuple)):
+ aabb = np.array(aabb)
+ if isinstance(aabb, np.ndarray):
+ aabb = torch.tensor(aabb, dtype=torch.float32, device=coords.device)
+ assert isinstance(aabb, torch.Tensor), f"aabb must be a list, tuple, np.ndarray, or torch.Tensor, but got {type(aabb)}"
+ assert aabb.dim() == 2, f"aabb must be a 2D tensor, but got {aabb.shape}"
+ assert aabb.size(0) == 2, f"aabb must have 2 rows, but got {aabb.size(0)}"
+ assert aabb.size(1) == 3, f"aabb must have 3 columns, but got {aabb.size(1)}"
+
+ # Voxel size
+ if voxel_size is not None:
+ if isinstance(voxel_size, float):
+ voxel_size = [voxel_size, voxel_size, voxel_size]
+ if isinstance(voxel_size, (list, tuple)):
+ voxel_size = np.array(voxel_size)
+ if isinstance(voxel_size, np.ndarray):
+ voxel_size = torch.tensor(voxel_size, dtype=torch.float32, device=coords.device)
+ grid_size = ((aabb[1] - aabb[0]) / voxel_size).round().int()
+ else:
+ assert grid_size is not None, "Either voxel_size or grid_size must be provided"
+ if isinstance(grid_size, int):
+ grid_size = [grid_size, grid_size, grid_size]
+ if isinstance(grid_size, (list, tuple)):
+ grid_size = np.array(grid_size)
+ if isinstance(grid_size, np.ndarray):
+ grid_size = torch.tensor(grid_size, dtype=torch.int32, device=coords.device)
+ voxel_size = (aabb[1] - aabb[0]) / grid_size
+ assert isinstance(voxel_size, torch.Tensor), f"voxel_size must be a float, list, tuple, np.ndarray, or torch.Tensor, but got {type(voxel_size)}"
+ assert voxel_size.dim() == 1, f"voxel_size must be a 1D tensor, but got {voxel_size.shape}"
+ assert voxel_size.size(0) == 3, f"voxel_size must have 3 elements, but got {voxel_size.size(0)}"
+ assert isinstance(grid_size, torch.Tensor), f"grid_size must be an int, list, tuple, np.ndarray, or torch.Tensor, but got {type(grid_size)}"
+ assert grid_size.dim() == 1, f"grid_size must be a 1D tensor, but got {grid_size.shape}"
+ assert grid_size.size(0) == 3, f"grid_size must have 3 elements, but got {grid_size.size(0)}"
+
+ # Extract mesh
+ N = dual_vertices.shape[0]
+ mesh_vertices = (coords.float() + dual_vertices) / (2 * N) - 0.5
+
+ # Store active voxels into hashmap
+ hashmap = _init_hashmap(grid_size, 2 * N, device=coords.device)
+ _C.hashmap_insert_3d_idx_as_val_cuda(*hashmap, torch.cat([torch.zeros_like(coords[:, :1]), coords], dim=-1), *grid_size.tolist())
+
+ # Find connected voxels
+ edge_neighbor_voxel = coords.reshape(N, 1, 1, 3) + flexible_dual_grid_to_mesh.edge_neighbor_voxel_offset # (N, 3, 4, 3)
+ connected_voxel = edge_neighbor_voxel[intersected_flag] # (M, 4, 3)
+ M = connected_voxel.shape[0]
+ connected_voxel_hash_key = torch.cat([
+ torch.zeros((M * 4, 1), dtype=torch.int, device=coords.device),
+ connected_voxel.reshape(-1, 3)
+ ], dim=1)
+ connected_voxel_indices = _C.hashmap_lookup_3d_cuda(*hashmap, connected_voxel_hash_key, *grid_size.tolist()).reshape(M, 4).int()
+ connected_voxel_valid = (connected_voxel_indices != 0xffffffff).all(dim=1)
+ quad_indices = connected_voxel_indices[connected_voxel_valid].int() # (L, 4)
+ L = quad_indices.shape[0]
+
+ # Construct triangles
+ if not train:
+ mesh_vertices = (coords.float() + dual_vertices) * voxel_size + aabb[0].reshape(1, 3)
+ if split_weight is None:
+ # if split 1
+ atempt_triangles_0 = quad_indices[:, flexible_dual_grid_to_mesh.quad_split_1]
+ normals0 = torch.cross(mesh_vertices[atempt_triangles_0[:, 1]] - mesh_vertices[atempt_triangles_0[:, 0]], mesh_vertices[atempt_triangles_0[:, 2]] - mesh_vertices[atempt_triangles_0[:, 0]])
+ normals1 = torch.cross(mesh_vertices[atempt_triangles_0[:, 2]] - mesh_vertices[atempt_triangles_0[:, 1]], mesh_vertices[atempt_triangles_0[:, 3]] - mesh_vertices[atempt_triangles_0[:, 1]])
+ align0 = (normals0 * normals1).sum(dim=1, keepdim=True).abs()
+ # if split 2
+ atempt_triangles_1 = quad_indices[:, flexible_dual_grid_to_mesh.quad_split_2]
+ normals0 = torch.cross(mesh_vertices[atempt_triangles_1[:, 1]] - mesh_vertices[atempt_triangles_1[:, 0]], mesh_vertices[atempt_triangles_1[:, 2]] - mesh_vertices[atempt_triangles_1[:, 0]])
+ normals1 = torch.cross(mesh_vertices[atempt_triangles_1[:, 2]] - mesh_vertices[atempt_triangles_1[:, 1]], mesh_vertices[atempt_triangles_1[:, 3]] - mesh_vertices[atempt_triangles_1[:, 1]])
+ align1 = (normals0 * normals1).sum(dim=1, keepdim=True).abs()
+ # select split
+ mesh_triangles = torch.where(align0 > align1, atempt_triangles_0, atempt_triangles_1).reshape(-1, 3)
+ else:
+ split_weight_ws = split_weight[quad_indices]
+ split_weight_ws_02 = split_weight_ws[:, 0] * split_weight_ws[:, 2]
+ split_weight_ws_13 = split_weight_ws[:, 1] * split_weight_ws[:, 3]
+ mesh_triangles = torch.where(
+ split_weight_ws_02 > split_weight_ws_13,
+ quad_indices[:, flexible_dual_grid_to_mesh.quad_split_1],
+ quad_indices[:, flexible_dual_grid_to_mesh.quad_split_2]
+ ).reshape(-1, 3)
+ else:
+ assert split_weight is not None, "split_weight must be provided in training mode"
+ mesh_vertices = (coords.float() + dual_vertices) * voxel_size + aabb[0].reshape(1, 3)
+ quad_vs = mesh_vertices[quad_indices]
+ mean_v02 = (quad_vs[:, 0] + quad_vs[:, 2]) / 2
+ mean_v13 = (quad_vs[:, 1] + quad_vs[:, 3]) / 2
+ split_weight_ws = split_weight[quad_indices]
+ split_weight_ws_02 = split_weight_ws[:, 0] * split_weight_ws[:, 2]
+ split_weight_ws_13 = split_weight_ws[:, 1] * split_weight_ws[:, 3]
+ mid_vertices = (
+ split_weight_ws_02 * mean_v02 +
+ split_weight_ws_13 * mean_v13
+ ) / (split_weight_ws_02 + split_weight_ws_13)
+ mesh_vertices = torch.cat([mesh_vertices, mid_vertices], dim=0)
+ quad_indices = torch.cat([quad_indices, torch.arange(N, N + L, device='cuda').unsqueeze(1)], dim=1)
+ mesh_triangles = quad_indices[:, flexible_dual_grid_to_mesh.quad_split_train].reshape(-1, 3)
+
+ return mesh_vertices, mesh_triangles
diff --git a/o-voxel/build/lib.win-amd64-cpython-311/o_voxel/convert/volumetic_attr.py b/o-voxel/build/lib.win-amd64-cpython-311/o_voxel/convert/volumetic_attr.py
new file mode 100644
index 0000000000000000000000000000000000000000..fe24bfe876f01cceb02bdb5859232fa95779b5c6
--- /dev/null
+++ b/o-voxel/build/lib.win-amd64-cpython-311/o_voxel/convert/volumetic_attr.py
@@ -0,0 +1,583 @@
+from typing import *
+import io
+from PIL import Image
+import torch
+import numpy as np
+from tqdm import tqdm
+import trimesh
+import trimesh.visual
+
+from .. import _C
+
+__all__ = [
+ "textured_mesh_to_volumetric_attr",
+ "blender_dump_to_volumetric_attr"
+]
+
+
+ALPHA_MODE_ENUM = {
+ "OPAQUE": 0,
+ "MASK": 1,
+ "BLEND": 2,
+}
+
+
+def is_power_of_two(n: int) -> bool:
+ return n > 0 and (n & (n - 1)) == 0
+
+
+def nearest_power_of_two(n: int) -> int:
+ if n < 1:
+ raise ValueError("n must be >= 1")
+ if is_power_of_two(n):
+ return n
+ lower = 2 ** (n.bit_length() - 1)
+ upper = 2 ** n.bit_length()
+ if n - lower < upper - n:
+ return lower
+ else:
+ return upper
+
+
+def textured_mesh_to_volumetric_attr(
+ mesh: Union[trimesh.Scene, trimesh.Trimesh, str],
+ voxel_size: Union[float, list, tuple, np.ndarray, torch.Tensor] = None,
+ grid_size: Union[int, list, tuple, np.ndarray, torch.Tensor] = None,
+ aabb: Union[list, tuple, np.ndarray, torch.Tensor] = None,
+ mip_level_offset: float = 0.0,
+ verbose: bool = False,
+ timing: bool = False,
+) -> Union[torch.Tensor, Dict[str, torch.Tensor]]:
+ """
+ Voxelize a mesh into a sparse voxel grid with PBR properties.
+
+ Args:
+ mesh (trimesh.Scene, trimesh.Trimesh, str): The input mesh.
+ If a string is provided, it will be loaded as a mesh using trimesh.load().
+ voxel_size (float, list, tuple, np.ndarray, torch.Tensor): The size of each voxel.
+ grid_size (int, list, tuple, np.ndarray, torch.Tensor): The size of the grid.
+ NOTE: One of voxel_size and grid_size must be provided.
+ aabb (list, tuple, np.ndarray, torch.Tensor): The axis-aligned bounding box of the mesh.
+ If not provided, it will be computed automatically.
+ tile_size (int): The size of the tiles used for each individual voxelization.
+ mip_level_offset (float): The mip level offset for texture mip level selection.
+ verbose (bool): Whether to print the settings.
+ timing (bool): Whether to print the timing information.
+
+ Returns:
+ torch.Tensor: The indices of the voxels that are occupied by the mesh.
+ Dict[str, torch.Tensor]: A dictionary containing the following keys:
+ - "base_color": The base color of the occupied voxels.
+ - "metallic": The metallic value of the occupied voxels.
+ - "roughness": The roughness value of the occupied voxels.
+ - "emissive": The emissive value of the occupied voxels.
+ - "alpha": The alpha value of the occupied voxels.
+ - "normal": The normal of the occupied voxels.
+ """
+
+ # Load mesh
+ if isinstance(mesh, str):
+ mesh = trimesh.load(mesh)
+ if isinstance(mesh, trimesh.Scene):
+ groups = mesh.dump()
+ if isinstance(mesh, trimesh.Trimesh):
+ groups = [mesh]
+ scene = trimesh.Scene(groups)
+
+ # Voxelize settings
+ assert voxel_size is not None or grid_size is not None, "Either voxel_size or grid_size must be provided"
+
+ if voxel_size is not None:
+ if isinstance(voxel_size, float):
+ voxel_size = [voxel_size, voxel_size, voxel_size]
+ if isinstance(voxel_size, (list, tuple)):
+ voxel_size = np.array(voxel_size)
+ if isinstance(voxel_size, np.ndarray):
+ voxel_size = torch.tensor(voxel_size, dtype=torch.float32)
+ assert isinstance(voxel_size, torch.Tensor), f"voxel_size must be a float, list, tuple, np.ndarray, or torch.Tensor, but got {type(voxel_size)}"
+ assert voxel_size.dim() == 1, f"voxel_size must be a 1D tensor, but got {voxel_size.shape}"
+ assert voxel_size.size(0) == 3, f"voxel_size must have 3 elements, but got {voxel_size.size(0)}"
+
+ if grid_size is not None:
+ if isinstance(grid_size, int):
+ grid_size = [grid_size, grid_size, grid_size]
+ if isinstance(grid_size, (list, tuple)):
+ grid_size = np.array(grid_size)
+ if isinstance(grid_size, np.ndarray):
+ grid_size = torch.tensor(grid_size, dtype=torch.int32)
+ assert isinstance(grid_size, torch.Tensor), f"grid_size must be an int, list, tuple, np.ndarray, or torch.Tensor, but got {type(grid_size)}"
+ assert grid_size.dim() == 1, f"grid_size must be a 1D tensor, but got {grid_size.shape}"
+ assert grid_size.size(0) == 3, f"grid_size must have 3 elements, but got {grid_size.size(0)}"
+
+ if aabb is not None:
+ if isinstance(aabb, (list, tuple)):
+ aabb = np.array(aabb)
+ if isinstance(aabb, np.ndarray):
+ aabb = torch.tensor(aabb, dtype=torch.float32)
+ assert isinstance(aabb, torch.Tensor), f"aabb must be a list, tuple, np.ndarray, or torch.Tensor, but got {type(aabb)}"
+ assert aabb.dim() == 2, f"aabb must be a 2D tensor, but got {aabb.shape}"
+ assert aabb.size(0) == 2, f"aabb must have 2 rows, but got {aabb.size(0)}"
+ assert aabb.size(1) == 3, f"aabb must have 3 columns, but got {aabb.size(1)}"
+
+ # Auto adjust aabb
+ if aabb is None:
+ aabb = scene.bounds
+ min_xyz = aabb[0]
+ max_xyz = aabb[1]
+
+ if voxel_size is not None:
+ padding = torch.ceil((max_xyz - min_xyz) / voxel_size) * voxel_size - (max_xyz - min_xyz)
+ min_xyz -= padding * 0.5
+ max_xyz += padding * 0.5
+ if grid_size is not None:
+ padding = (max_xyz - min_xyz) / (grid_size - 1)
+ min_xyz -= padding * 0.5
+ max_xyz += padding * 0.5
+
+ aabb = torch.stack([min_xyz, max_xyz], dim=0).float()
+
+ # Fill voxel size or grid size
+ if voxel_size is None:
+ voxel_size = (aabb[1] - aabb[0]) / grid_size
+ if grid_size is None:
+ grid_size = ((aabb[1] - aabb[0]) / voxel_size).round().int()
+
+ grid_range = torch.stack([torch.zeros_like(grid_size), grid_size], dim=0).int()
+
+ # Print settings
+ if verbose:
+ print(f"Voxelize settings:")
+ print(f" Voxel size: {voxel_size}")
+ print(f" Grid size: {grid_size}")
+ print(f" AABB: {aabb}")
+
+ # Load Scene
+ scene_buffers = {
+ 'triangles': [],
+ 'normals': [],
+ 'uvs': [],
+ 'material_ids': [],
+ 'base_color_factor': [],
+ 'base_color_texture': [],
+ 'metallic_factor': [],
+ 'metallic_texture': [],
+ 'roughness_factor': [],
+ 'roughness_texture': [],
+ 'emissive_factor': [],
+ 'emissive_texture': [],
+ 'alpha_mode': [],
+ 'alpha_cutoff': [],
+ 'alpha_factor': [],
+ 'alpha_texture': [],
+ 'normal_texture': [],
+ }
+ for sid, (name, g) in tqdm(enumerate(scene.geometry.items()), total=len(scene.geometry), desc="Loading Scene", disable=not verbose):
+ if verbose:
+ print(f"Geometry: {name}")
+ print(f" Visual: {g.visual}")
+ print(f" Triangles: {g.triangles.shape[0]}")
+ print(f" Vertices: {g.vertices.shape[0]}")
+ print(f" Normals: {g.vertex_normals.shape[0]}")
+ if g.visual.material.baseColorFactor is not None:
+ print(f" Base color factor: {g.visual.material.baseColorFactor}")
+ if g.visual.material.baseColorTexture is not None:
+ print(f" Base color texture: {g.visual.material.baseColorTexture.size} {g.visual.material.baseColorTexture.mode}")
+ if g.visual.material.metallicFactor is not None:
+ print(f" Metallic factor: {g.visual.material.metallicFactor}")
+ if g.visual.material.roughnessFactor is not None:
+ print(f" Roughness factor: {g.visual.material.roughnessFactor}")
+ if g.visual.material.metallicRoughnessTexture is not None:
+ print(f" Metallic roughness texture: {g.visual.material.metallicRoughnessTexture.size} {g.visual.material.metallicRoughnessTexture.mode}")
+ if g.visual.material.emissiveFactor is not None:
+ print(f" Emissive factor: {g.visual.material.emissiveFactor}")
+ if g.visual.material.emissiveTexture is not None:
+ print(f" Emissive texture: {g.visual.material.emissiveTexture.size} {g.visual.material.emissiveTexture.mode}")
+ if g.visual.material.alphaMode is not None:
+ print(f" Alpha mode: {g.visual.material.alphaMode}")
+ if g.visual.material.alphaCutoff is not None:
+ print(f" Alpha cutoff: {g.visual.material.alphaCutoff}")
+ if g.visual.material.normalTexture is not None:
+ print(f" Normal texture: {g.visual.material.normalTexture.size} {g.visual.material.normalTexture.mode}")
+
+ assert isinstance(g, trimesh.Trimesh), f"Only trimesh.Trimesh is supported, but got {type(g)}"
+ assert isinstance(g.visual, trimesh.visual.TextureVisuals), f"Only trimesh.visual.TextureVisuals is supported, but got {type(g.visual)}"
+ assert isinstance(g.visual.material, trimesh.visual.material.PBRMaterial), f"Only trimesh.visual.material.PBRMaterial is supported, but got {type(g.visual.material)}"
+ triangles = torch.tensor(g.triangles, dtype=torch.float32) - aabb[0].reshape(1, 1, 3) # [N, 3, 3]
+ normals = torch.tensor(g.vertex_normals[g.faces], dtype=torch.float32) # [N, 3, 3]
+ uvs = torch.tensor(g.visual.uv[g.faces], dtype=torch.float32) if g.visual.uv is not None \
+ else torch.zeros(g.triangles.shape[0], 3, 2, dtype=torch.float32) # [N, 3, 2]
+ baseColorFactor = torch.tensor(g.visual.material.baseColorFactor / 255, dtype=torch.float32) if g.visual.material.baseColorFactor is not None \
+ else torch.ones(3, dtype=torch.float32) # [3]
+ baseColorTexture = torch.tensor(np.array(g.visual.material.baseColorTexture.convert('RGBA'))[..., :3], dtype=torch.uint8) if g.visual.material.baseColorTexture is not None \
+ else torch.tensor([]) # [H, W, 3]
+ metallicFactor = g.visual.material.metallicFactor if g.visual.material.metallicFactor is not None else 1.0
+ metallicTexture = torch.tensor(np.array(g.visual.material.metallicRoughnessTexture.convert('RGB'))[..., 2], dtype=torch.uint8) if g.visual.material.metallicRoughnessTexture is not None \
+ else torch.tensor([]) # [H, W]
+ roughnessFactor = g.visual.material.roughnessFactor if g.visual.material.roughnessFactor is not None else 1.0
+ roughnessTexture = torch.tensor(np.array(g.visual.material.metallicRoughnessTexture.convert('RGB'))[..., 1], dtype=torch.uint8) if g.visual.material.metallicRoughnessTexture is not None \
+ else torch.tensor([]) # [H, W]
+ emissiveFactor = torch.tensor(g.visual.material.emissiveFactor, dtype=torch.float32) if g.visual.material.emissiveFactor is not None \
+ else torch.zeros(3, dtype=torch.float32) # [3]
+ emissiveTexture = torch.tensor(np.array(g.visual.material.emissiveTexture.convert('RGB'))[..., :3], dtype=torch.uint8) if g.visual.material.emissiveTexture is not None \
+ else torch.tensor([]) # [H, W, 3]
+ alphaMode = ALPHA_MODE_ENUM[g.visual.material.alphaMode] if g.visual.material.alphaMode in ALPHA_MODE_ENUM else 0
+ alphaCutoff = g.visual.material.alphaCutoff if g.visual.material.alphaCutoff is not None else 0.5
+ alphaFactor = g.visual.material.baseColorFactor[3] / 255 if g.visual.material.baseColorFactor is not None else 1.0
+ alphaTexture = torch.tensor(np.array(g.visual.material.baseColorTexture.convert('RGBA'))[..., 3], dtype=torch.uint8) if g.visual.material.baseColorTexture is not None and alphaMode != 0 \
+ else torch.tensor([]) # [H, W]
+ normalTexture = torch.tensor(np.array(g.visual.material.normalTexture.convert('RGB'))[..., :3], dtype=torch.uint8) if g.visual.material.normalTexture is not None \
+ else torch.tensor([]) # [H, W, 3]
+
+ scene_buffers['triangles'].append(triangles)
+ scene_buffers['normals'].append(normals)
+ scene_buffers['uvs'].append(uvs)
+ scene_buffers['material_ids'].append(torch.full((triangles.shape[0],), sid, dtype=torch.int32))
+ scene_buffers['base_color_factor'].append(baseColorFactor)
+ scene_buffers['base_color_texture'].append(baseColorTexture)
+ scene_buffers['metallic_factor'].append(metallicFactor)
+ scene_buffers['metallic_texture'].append(metallicTexture)
+ scene_buffers['roughness_factor'].append(roughnessFactor)
+ scene_buffers['roughness_texture'].append(roughnessTexture)
+ scene_buffers['emissive_factor'].append(emissiveFactor)
+ scene_buffers['emissive_texture'].append(emissiveTexture)
+ scene_buffers['alpha_mode'].append(alphaMode)
+ scene_buffers['alpha_cutoff'].append(alphaCutoff)
+ scene_buffers['alpha_factor'].append(alphaFactor)
+ scene_buffers['alpha_texture'].append(alphaTexture)
+ scene_buffers['normal_texture'].append(normalTexture)
+
+ scene_buffers['triangles'] = torch.cat(scene_buffers['triangles'], dim=0) # [N, 3, 3]
+ scene_buffers['normals'] = torch.cat(scene_buffers['normals'], dim=0) # [N, 3, 3]
+ scene_buffers['uvs'] = torch.cat(scene_buffers['uvs'], dim=0) # [N, 3, 2]
+ scene_buffers['material_ids'] = torch.cat(scene_buffers['material_ids'], dim=0) # [N]
+
+ # Voxelize
+ out_tuple = _C.textured_mesh_to_volumetric_attr_cpu(
+ voxel_size,
+ grid_range,
+ scene_buffers["triangles"],
+ scene_buffers["normals"],
+ scene_buffers["uvs"],
+ scene_buffers["material_ids"],
+ scene_buffers["base_color_factor"],
+ scene_buffers["base_color_texture"],
+ [1] * len(scene_buffers["base_color_texture"]),
+ [0] * len(scene_buffers["base_color_texture"]),
+ scene_buffers["metallic_factor"],
+ scene_buffers["metallic_texture"],
+ [1] * len(scene_buffers["metallic_texture"]),
+ [0] * len(scene_buffers["metallic_texture"]),
+ scene_buffers["roughness_factor"],
+ scene_buffers["roughness_texture"],
+ [1] * len(scene_buffers["roughness_texture"]),
+ [0] * len(scene_buffers["roughness_texture"]),
+ scene_buffers["emissive_factor"],
+ scene_buffers["emissive_texture"],
+ [1] * len(scene_buffers["emissive_texture"]),
+ [0] * len(scene_buffers["emissive_texture"]),
+ scene_buffers["alpha_mode"],
+ scene_buffers["alpha_cutoff"],
+ scene_buffers["alpha_factor"],
+ scene_buffers["alpha_texture"],
+ [1] * len(scene_buffers["alpha_texture"]),
+ [0] * len(scene_buffers["alpha_texture"]),
+ scene_buffers["normal_texture"],
+ [1] * len(scene_buffers["normal_texture"]),
+ [0] * len(scene_buffers["normal_texture"]),
+ mip_level_offset,
+ timing,
+ )
+
+ # Post process
+ coord = out_tuple[0]
+ attr = {
+ "base_color": torch.clamp(out_tuple[1] * 255, 0, 255).byte().reshape(-1, 3),
+ "metallic": torch.clamp(out_tuple[2] * 255, 0, 255).byte().reshape(-1, 1),
+ "roughness": torch.clamp(out_tuple[3] * 255, 0, 255).byte().reshape(-1, 1),
+ "emissive": torch.clamp(out_tuple[4] * 255, 0, 255).byte().reshape(-1, 3),
+ "alpha": torch.clamp(out_tuple[5] * 255, 0, 255).byte().reshape(-1, 1),
+ "normal": torch.clamp((out_tuple[6] * 0.5 + 0.5) * 255, 0, 255).byte().reshape(-1, 3),
+ }
+
+ return coord, attr
+
+
+def blender_dump_to_volumetric_attr(
+ dump: Dict[str, Any],
+ voxel_size: Union[float, list, tuple, np.ndarray, torch.Tensor] = None,
+ grid_size: Union[int, list, tuple, np.ndarray, torch.Tensor] = None,
+ aabb: Union[list, tuple, np.ndarray, torch.Tensor] = None,
+ mip_level_offset: float = 0.0,
+ verbose: bool = False,
+ timing: bool = False,
+) -> Union[torch.Tensor, Dict[str, torch.Tensor]]:
+ """
+ Voxelize a mesh into a sparse voxel grid with PBR properties.
+
+ Args:
+ dump (Dict[str, Any]): Dumped data from a blender scene.
+ voxel_size (float, list, tuple, np.ndarray, torch.Tensor): The size of each voxel.
+ grid_size (int, list, tuple, np.ndarray, torch.Tensor): The size of the grid.
+ NOTE: One of voxel_size and grid_size must be provided.
+ aabb (list, tuple, np.ndarray, torch.Tensor): The axis-aligned bounding box of the mesh.
+ If not provided, it will be computed automatically.
+ mip_level_offset (float): The mip level offset for texture mip level selection.
+ verbose (bool): Whether to print the settings.
+ timing (bool): Whether to print the timing information.
+
+ Returns:
+ torch.Tensor: The indices of the voxels that are occupied by the mesh.
+ Dict[str, torch.Tensor]: A dictionary containing the following keys:
+ - "base_color": The base color of the occupied voxels.
+ - "metallic": The metallic value of the occupied voxels.
+ - "roughness": The roughness value of the occupied voxels.
+ - "emissive": The emissive value of the occupied voxels.
+ - "alpha": The alpha value of the occupied voxels.
+ - "normal": The normal of the occupied voxels.
+ """
+ # Voxelize settings
+ assert voxel_size is not None or grid_size is not None, "Either voxel_size or grid_size must be provided"
+
+ if voxel_size is not None:
+ if isinstance(voxel_size, float):
+ voxel_size = [voxel_size, voxel_size, voxel_size]
+ if isinstance(voxel_size, (list, tuple)):
+ voxel_size = np.array(voxel_size)
+ if isinstance(voxel_size, np.ndarray):
+ voxel_size = torch.tensor(voxel_size, dtype=torch.float32)
+ assert isinstance(voxel_size, torch.Tensor), f"voxel_size must be a float, list, tuple, np.ndarray, or torch.Tensor, but got {type(voxel_size)}"
+ assert voxel_size.dim() == 1, f"voxel_size must be a 1D tensor, but got {voxel_size.shape}"
+ assert voxel_size.size(0) == 3, f"voxel_size must have 3 elements, but got {voxel_size.size(0)}"
+
+ if grid_size is not None:
+ if isinstance(grid_size, int):
+ grid_size = [grid_size, grid_size, grid_size]
+ if isinstance(grid_size, (list, tuple)):
+ grid_size = np.array(grid_size)
+ if isinstance(grid_size, np.ndarray):
+ grid_size = torch.tensor(grid_size, dtype=torch.int32)
+ assert isinstance(grid_size, torch.Tensor), f"grid_size must be an int, list, tuple, np.ndarray, or torch.Tensor, but got {type(grid_size)}"
+ assert grid_size.dim() == 1, f"grid_size must be a 1D tensor, but got {grid_size.shape}"
+ assert grid_size.size(0) == 3, f"grid_size must have 3 elements, but got {grid_size.size(0)}"
+
+ if aabb is not None:
+ if isinstance(aabb, (list, tuple)):
+ aabb = np.array(aabb)
+ if isinstance(aabb, np.ndarray):
+ aabb = torch.tensor(aabb, dtype=torch.float32)
+ assert isinstance(aabb, torch.Tensor), f"aabb must be a list, tuple, np.ndarray, or torch.Tensor, but got {type(aabb)}"
+ assert aabb.dim() == 2, f"aabb must be a 2D tensor, but got {aabb.shape}"
+ assert aabb.size(0) == 2, f"aabb must have 2 rows, but got {aabb.size(0)}"
+ assert aabb.size(1) == 3, f"aabb must have 3 columns, but got {aabb.size(1)}"
+
+ # Auto adjust aabb
+ if aabb is None:
+ min_xyz = np.min([
+ object['vertices'].min(axis=0)
+ for object in dump['objects']
+ ], axis=0)
+ max_xyz = np.max([
+ object['vertices'].max(axis=0)
+ for object in dump['objects']
+ ], axis=0)
+
+ if voxel_size is not None:
+ padding = torch.ceil((max_xyz - min_xyz) / voxel_size) * voxel_size - (max_xyz - min_xyz)
+ min_xyz -= padding * 0.5
+ max_xyz += padding * 0.5
+ if grid_size is not None:
+ padding = (max_xyz - min_xyz) / (grid_size - 1)
+ min_xyz -= padding * 0.5
+ max_xyz += padding * 0.5
+
+ aabb = torch.stack([min_xyz, max_xyz], dim=0).float()
+
+ # Fill voxel size or grid size
+ if voxel_size is None:
+ voxel_size = (aabb[1] - aabb[0]) / grid_size
+ if grid_size is None:
+ grid_size = ((aabb[1] - aabb[0]) / voxel_size).round().int()
+
+ grid_range = torch.stack([torch.zeros_like(grid_size), grid_size], dim=0).int()
+
+ # Print settings
+ if verbose:
+ print(f"Voxelize settings:")
+ print(f" Voxel size: {voxel_size}")
+ print(f" Grid size: {grid_size}")
+ print(f" AABB: {aabb}")
+
+ # Load Scene
+ scene_buffers = {
+ 'triangles': [],
+ 'normals': [],
+ 'uvs': [],
+ 'material_ids': [],
+ 'base_color_factor': [],
+ 'base_color_texture': [],
+ 'base_color_texture_filter': [],
+ 'base_color_texture_wrap': [],
+ 'metallic_factor': [],
+ 'metallic_texture': [],
+ 'metallic_texture_filter': [],
+ 'metallic_texture_wrap': [],
+ 'roughness_factor': [],
+ 'roughness_texture': [],
+ 'roughness_texture_filter': [],
+ 'roughness_texture_wrap': [],
+ 'alpha_mode': [],
+ 'alpha_cutoff': [],
+ 'alpha_factor': [],
+ 'alpha_texture': [],
+ 'alpha_texture_filter': [],
+ 'alpha_texture_wrap': [],
+ }
+
+ def load_texture(pack):
+ png_bytes = pack['image']
+ image = Image.open(io.BytesIO(png_bytes))
+ if image.width != image.height or not is_power_of_two(image.width):
+ size = nearest_power_of_two(max(image.width, image.height))
+ image = image.resize((size, size), Image.LANCZOS)
+ texture = torch.tensor(np.array(image), dtype=torch.uint8)
+ filter_mode = {
+ 'Linear': 1,
+ 'Closest': 0,
+ 'Cubic': 1,
+ 'Smart': 1,
+ }[pack['interpolation']]
+ wrap_mode = {
+ 'REPEAT': 0,
+ 'EXTEND': 1,
+ 'CLIP': 1,
+ 'MIRROR': 2,
+ }[pack['extension']]
+ return texture, filter_mode, wrap_mode
+
+ for material in dump['materials']:
+ baseColorFactor = torch.tensor(material['baseColorFactor'][:3], dtype=torch.float32)
+ if material['baseColorTexture'] is not None:
+ baseColorTexture, baseColorTextureFilter, baseColorTextureWrap = \
+ load_texture(material['baseColorTexture'])
+ assert baseColorTexture.shape[2] == 3, f"Base color texture must have 3 channels, but got {baseColorTexture.shape[2]}"
+ else:
+ baseColorTexture = torch.tensor([])
+ baseColorTextureFilter = 0
+ baseColorTextureWrap = 0
+ scene_buffers['base_color_factor'].append(baseColorFactor)
+ scene_buffers['base_color_texture'].append(baseColorTexture)
+ scene_buffers['base_color_texture_filter'].append(baseColorTextureFilter)
+ scene_buffers['base_color_texture_wrap'].append(baseColorTextureWrap)
+
+ metallicFactor = material['metallicFactor']
+ if material['metallicTexture'] is not None:
+ metallicTexture, metallicTextureFilter, metallicTextureWrap = \
+ load_texture(material['metallicTexture'])
+ assert metallicTexture.dim() == 2, f"Metallic roughness texture must have 2 dimensions, but got {metallicTexture.dim()}"
+ else:
+ metallicTexture = torch.tensor([])
+ metallicTextureFilter = 0
+ metallicTextureWrap = 0
+ scene_buffers['metallic_factor'].append(metallicFactor)
+ scene_buffers['metallic_texture'].append(metallicTexture)
+ scene_buffers['metallic_texture_filter'].append(metallicTextureFilter)
+ scene_buffers['metallic_texture_wrap'].append(metallicTextureWrap)
+
+ roughnessFactor = material['roughnessFactor']
+ if material['roughnessTexture'] is not None:
+ roughnessTexture, roughnessTextureFilter, roughnessTextureWrap = \
+ load_texture(material['roughnessTexture'])
+ assert roughnessTexture.dim() == 2, f"Metallic roughness texture must have 2 dimensions, but got {roughnessTexture.dim()}"
+ else:
+ roughnessTexture = torch.tensor([])
+ roughnessTextureFilter = 0
+ roughnessTextureWrap = 0
+ scene_buffers['roughness_factor'].append(roughnessFactor)
+ scene_buffers['roughness_texture'].append(roughnessTexture)
+ scene_buffers['roughness_texture_filter'].append(roughnessTextureFilter)
+ scene_buffers['roughness_texture_wrap'].append(roughnessTextureWrap)
+
+ alphaMode = ALPHA_MODE_ENUM[material['alphaMode']]
+ alphaCutoff = material['alphaCutoff']
+ alphaFactor = material['alphaFactor']
+ if material['alphaTexture'] is not None:
+ alphaTexture, alphaTextureFilter, alphaTextureWrap = \
+ load_texture(material['alphaTexture'])
+ assert alphaTexture.dim() == 2, f"Alpha texture must have 2 dimensions, but got {alphaTexture.dim()}"
+ else:
+ alphaTexture = torch.tensor([])
+ alphaTextureFilter = 0
+ alphaTextureWrap = 0
+ scene_buffers['alpha_mode'].append(alphaMode)
+ scene_buffers['alpha_cutoff'].append(alphaCutoff)
+ scene_buffers['alpha_factor'].append(alphaFactor)
+ scene_buffers['alpha_texture'].append(alphaTexture)
+ scene_buffers['alpha_texture_filter'].append(alphaTextureFilter)
+ scene_buffers['alpha_texture_wrap'].append(alphaTextureWrap)
+
+ for object in dump['objects']:
+ triangles = torch.tensor(object['vertices'][object['faces']], dtype=torch.float32).reshape(-1, 3, 3) - aabb[0].reshape(1, 1, 3)
+ normails = torch.tensor(object['normals'], dtype=torch.float32)
+ uvs = torch.tensor(object['uvs'], dtype=torch.float32) if object['uvs'] is not None else torch.zeros(triangles.shape[0], 3, 2, dtype=torch.float32)
+ material_id = torch.tensor(object['mat_ids'], dtype=torch.int32)
+ scene_buffers['triangles'].append(triangles)
+ scene_buffers['normals'].append(normails)
+ scene_buffers['uvs'].append(uvs)
+ scene_buffers['material_ids'].append(material_id)
+
+ scene_buffers['triangles'] = torch.cat(scene_buffers['triangles'], dim=0) # [N, 3, 3]
+ scene_buffers['normals'] = torch.cat(scene_buffers['normals'], dim=0) # [N, 3, 3]
+ scene_buffers['uvs'] = torch.cat(scene_buffers['uvs'], dim=0) # [N, 3, 2]
+ scene_buffers['material_ids'] = torch.cat(scene_buffers['material_ids'], dim=0) # [N]
+
+ scene_buffers['uvs'][:, :, 1] = 1 - scene_buffers['uvs'][:, :, 1] # Flip v coordinate
+
+ # Voxelize
+ out_tuple = _C.textured_mesh_to_volumetric_attr_cpu(
+ voxel_size,
+ grid_range,
+ scene_buffers["triangles"],
+ scene_buffers["normals"],
+ scene_buffers["uvs"],
+ scene_buffers["material_ids"],
+ scene_buffers["base_color_factor"],
+ scene_buffers["base_color_texture"],
+ scene_buffers["base_color_texture_filter"],
+ scene_buffers["base_color_texture_wrap"],
+ scene_buffers["metallic_factor"],
+ scene_buffers["metallic_texture"],
+ scene_buffers["metallic_texture_filter"],
+ scene_buffers["metallic_texture_wrap"],
+ scene_buffers["roughness_factor"],
+ scene_buffers["roughness_texture"],
+ scene_buffers["roughness_texture_filter"],
+ scene_buffers["roughness_texture_wrap"],
+ [torch.zeros(3, dtype=torch.float32) for _ in range(len(scene_buffers["base_color_texture"]))],
+ [torch.tensor([]) for _ in range(len(scene_buffers["base_color_texture"]))],
+ [0] * len(scene_buffers["base_color_texture"]),
+ [0] * len(scene_buffers["base_color_texture"]),
+ scene_buffers["alpha_mode"],
+ scene_buffers["alpha_cutoff"],
+ scene_buffers["alpha_factor"],
+ scene_buffers["alpha_texture"],
+ scene_buffers["alpha_texture_filter"],
+ scene_buffers["alpha_texture_wrap"],
+ [torch.tensor([]) for _ in range(len(scene_buffers["base_color_texture"]))],
+ [0] * len(scene_buffers["base_color_texture"]),
+ [0] * len(scene_buffers["base_color_texture"]),
+ mip_level_offset,
+ timing,
+ )
+
+ # Post process
+ coord = out_tuple[0]
+ attr = {
+ "base_color": torch.clamp(out_tuple[1] * 255, 0, 255).byte().reshape(-1, 3),
+ "metallic": torch.clamp(out_tuple[2] * 255, 0, 255).byte().reshape(-1, 1),
+ "roughness": torch.clamp(out_tuple[3] * 255, 0, 255).byte().reshape(-1, 1),
+ "emissive": torch.clamp(out_tuple[4] * 255, 0, 255).byte().reshape(-1, 3),
+ "alpha": torch.clamp(out_tuple[5] * 255, 0, 255).byte().reshape(-1, 1),
+ "normal": torch.clamp((out_tuple[6] * 0.5 + 0.5) * 255, 0, 255).byte().reshape(-1, 3),
+ }
+
+ return coord, attr
\ No newline at end of file
diff --git a/o-voxel/build/lib.win-amd64-cpython-311/o_voxel/io/__init__.py b/o-voxel/build/lib.win-amd64-cpython-311/o_voxel/io/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..db7eca220accb46c62dfac93f078c2938969866a
--- /dev/null
+++ b/o-voxel/build/lib.win-amd64-cpython-311/o_voxel/io/__init__.py
@@ -0,0 +1,45 @@
+from typing import Dict, Union
+import torch
+from .ply import *
+from .npz import *
+from .vxz import *
+
+
+def read(file_path: str) -> Union[torch.Tensor, Dict[str, torch.Tensor]]:
+ """
+ Read a file containing voxels.
+
+ Args:
+ file_path: Path to the file.
+
+ Returns:
+ torch.Tensor: the coordinates of the voxels.
+ Dict[str, torch.Tensor]: the attributes of the voxels.
+ """
+ if file_path.endswith('.npz'):
+ return read_npz(file_path)
+ elif file_path.endswith('.ply'):
+ return read_ply(file_path)
+ elif file_path.endswith('.vxz'):
+ return read_vxz(file_path)
+ else:
+ raise ValueError(f"Unsupported file type {file_path}")
+
+
+def write(file_path: str, coord: torch.Tensor, attr: Dict[str, torch.Tensor], **kwargs):
+ """
+ Write a file containing voxels.
+
+ Args:
+ file_path: Path to the file.
+ coord: the coordinates of the voxels.
+ attr: the attributes of the voxels.
+ """
+ if file_path.endswith('.npz'):
+ write_npz(file_path, coord, attr, **kwargs)
+ elif file_path.endswith('.ply'):
+ write_ply(file_path, coord, attr, **kwargs)
+ elif file_path.endswith('.vxz'):
+ write_vxz(file_path, coord, attr, **kwargs)
+ else:
+ raise ValueError(f"Unsupported file type {file_path}")
diff --git a/o-voxel/build/lib.win-amd64-cpython-311/o_voxel/io/npz.py b/o-voxel/build/lib.win-amd64-cpython-311/o_voxel/io/npz.py
new file mode 100644
index 0000000000000000000000000000000000000000..17da9efe2b937540282cbaf25c385f19d0848be9
--- /dev/null
+++ b/o-voxel/build/lib.win-amd64-cpython-311/o_voxel/io/npz.py
@@ -0,0 +1,43 @@
+from typing import *
+import torch
+import numpy as np
+
+
+__all__ = [
+ "read_npz",
+ "write_npz",
+]
+
+
+def read_npz(file) -> Union[torch.Tensor, Dict[str, torch.Tensor]]:
+ """
+ Read a NPZ file containing voxels.
+
+ Args:
+ file_path: Path or file object from which to read the NPZ file.
+
+ Returns:
+ torch.Tensor: the coordinates of the voxels.
+ Dict[str, torch.Tensor]: the attributes of the voxels.
+ """
+ data = np.load(file)
+ coord = torch.from_numpy(data['coord']).int()
+ attr = {k: torch.from_numpy(v) for k, v in data.items() if k!= 'coord'}
+ return coord, attr
+
+
+def write_npz(file, coord: torch.Tensor, attr: Dict[str, torch.Tensor], compress=True):
+ """
+ Write a NPZ file containing voxels.
+
+ Args:
+ file_path: Path or file object to which to write the NPZ file.
+ coord: the coordinates of the voxels.
+ attr: the attributes of the voxels.
+ """
+ data = {'coord': coord.cpu().numpy().astype(np.uint16)}
+ data.update({k: v.cpu().numpy() for k, v in attr.items()})
+ if compress:
+ np.savez_compressed(file, **data)
+ else:
+ np.savez(file, **data)
diff --git a/o-voxel/build/lib.win-amd64-cpython-311/o_voxel/io/ply.py b/o-voxel/build/lib.win-amd64-cpython-311/o_voxel/io/ply.py
new file mode 100644
index 0000000000000000000000000000000000000000..747693218fabfaca994e1f23707878c3cae7b4c9
--- /dev/null
+++ b/o-voxel/build/lib.win-amd64-cpython-311/o_voxel/io/ply.py
@@ -0,0 +1,72 @@
+from typing import *
+import io
+import torch
+import numpy as np
+import plyfile
+
+
+__all__ = [
+ "read_ply",
+ "write_ply",
+]
+
+
+DTYPE_MAP = {
+ torch.uint8: 'u1',
+ torch.uint16: 'u2',
+ torch.uint32: 'u4',
+ torch.int8: 'i1',
+ torch.int16: 'i2',
+ torch.int32: 'i4',
+ torch.float32: 'f4',
+ torch.float64: 'f8'
+}
+
+
+def read_ply(file) -> Union[torch.Tensor, Dict[str, torch.Tensor]]:
+ """
+ Read a PLY file containing voxels.
+
+ Args:
+ file: Path or file-like object of the PLY file.
+
+ Returns:
+ torch.Tensor: the coordinates of the voxels.
+ Dict[str, torch.Tensor]: the attributes of the voxels.
+ """
+ plydata = plyfile.PlyData.read(file)
+ xyz = np.stack([plydata.elements[0][k] for k in ['x', 'y', 'z']], axis=1)
+ coord = np.round(xyz).astype(int)
+ coord = torch.from_numpy(coord)
+
+ attr_keys = [k for k in plydata.elements[0].data.dtype.names if k not in ['x', 'y', 'z']]
+ attr_names = ['_'.join(k.split('_')[:-1]) for k in attr_keys]
+ attr_chs = [sum([1 for k in attr_keys if k.startswith(f'{name}_')]) for name in attr_names]
+
+ attr = {}
+ for i, name in enumerate(attr_names):
+ attr[name] = np.stack([plydata.elements[0][f'{name}_{j}'] for j in range(attr_chs[i])], axis=1)
+ attr = {k: torch.from_numpy(v) for k, v in attr.items()}
+
+ return coord, attr
+
+
+def write_ply(file, coord: torch.Tensor, attr: Dict[str, torch.Tensor]):
+ """
+ Write a PLY file containing voxels.
+
+ Args:
+ file: Path or file-like object of the PLY file.
+ coord: the coordinates of the voxels.
+ attr: the attributes of the voxels.
+ """
+ dtypes = [('x', 'f4'), ('y', 'f4'), ('z', 'f4')]
+ for k, v in attr.items():
+ for j in range(v.shape[-1]):
+ assert v.dtype in DTYPE_MAP, f"Unsupported data type {v.dtype} for attribute {k}"
+ dtypes.append((f'{k}_{j}', DTYPE_MAP[v.dtype]))
+ data = np.empty(len(coord), dtype=dtypes)
+ all_chs = np.concatenate([coord.cpu().numpy().astype(np.float32)] + [v.cpu().numpy() for v in attr.values()], axis=1)
+ data[:] = list(map(tuple, all_chs))
+ plyfile.PlyData([plyfile.PlyElement.describe(data, 'vertex')]).write(file)
+
\ No newline at end of file
diff --git a/o-voxel/build/lib.win-amd64-cpython-311/o_voxel/io/vxz.py b/o-voxel/build/lib.win-amd64-cpython-311/o_voxel/io/vxz.py
new file mode 100644
index 0000000000000000000000000000000000000000..91fba74d26d2edaabbcd7edb707d272cbca25b8a
--- /dev/null
+++ b/o-voxel/build/lib.win-amd64-cpython-311/o_voxel/io/vxz.py
@@ -0,0 +1,365 @@
+from typing import *
+import os
+import json
+import struct
+import torch
+import numpy as np
+import zlib
+import lzma
+import zstandard
+from concurrent.futures import ThreadPoolExecutor
+from ..serialize import encode_seq, decode_seq
+from .. import _C
+
+
+__all__ = [
+ "read_vxz",
+ "read_vxz_info",
+ "write_vxz",
+]
+
+
+"""
+VXZ format
+
+Header:
+- file type (3 bytes) - 'VXZ'
+- version (1 byte) - 0
+- binary start offset (4 bytes)
+- structure (json) -
+{
+ "num_voxel": int,
+ "chunk_size": int,
+ "filter": str,
+ "compression": str,
+ "compression_level": int,
+ "raw_size": int,
+ "compressed_size": int,
+ "compress_ratio": float,
+ "attr_interleave": str,
+ "attr": [
+ {"name": str, "chs": int},
+ ...
+ ]
+ "chunks": [
+ {
+ "ptr": [offset, length], # offset from global binary start
+ "svo": [offset, length], # offset from this chunk start
+ "attr": [offset, length], # offset from this chunk start
+ },
+ ...
+ ]
+}
+- binary data
+"""
+
+DEFAULT_COMPRESION_LEVEL = {
+ 'none': 0,
+ 'deflate': 9,
+ 'lzma': 9,
+ 'zstd': 22,
+}
+
+
+def _compress(data: bytes, algo: Literal['none', 'deflate', 'lzma', 'zstd'], level: int) -> bytes:
+ if algo == 'none':
+ return data
+ if level is None:
+ level = DEFAULT_COMPRESION_LEVEL[algo]
+ if algo == 'deflate':
+ compresser = zlib.compressobj(level, wbits=-15)
+ return compresser.compress(data) + compresser.flush()
+ if algo == 'lzma':
+ compresser = lzma.LZMACompressor(format=lzma.FORMAT_RAW, filters=[{'id': lzma.FILTER_LZMA2, 'preset': level}])
+ return compresser.compress(data) + compresser.flush()
+ if algo == 'zstd':
+ compresser = zstandard.ZstdCompressor(level=level, write_checksum=False, write_content_size=True, threads=-1)
+ return compresser.compress(data)
+ raise ValueError(f"Invalid compression algorithm: {algo}")
+
+
+def _decompress(data: bytes, algo: Literal['none', 'deflate', 'lzma', 'zstd'], level: int) -> bytes:
+ if algo == 'none':
+ return data
+ if level is None:
+ level = DEFAULT_COMPRESION_LEVEL[algo]
+ if algo == 'deflate':
+ decompresser = zlib.decompressobj(wbits=-15)
+ return decompresser.decompress(data) + decompresser.flush()
+ if algo == 'lzma':
+ decompresser = lzma.LZMADecompressor(format=lzma.FORMAT_RAW, filters=[{'id': lzma.FILTER_LZMA2, 'preset': level}])
+ return decompresser.decompress(data)
+ if algo == 'zstd':
+ decompresser = zstandard.ZstdDecompressor(format=zstandard.FORMAT_ZSTD1)
+ return decompresser.decompress(data)
+ raise ValueError(f"Invalid compression algorithm: {algo}")
+
+
+def read_vxz_info(file) -> Dict:
+ """
+ Read the header of a VXZ file without decompressing the binary data.
+
+ Args:
+ file_path: Path or file-like object to the VXZ file.
+
+ Returns:
+ Dict: the header of the VXZ file.
+ """
+ if isinstance(file, str):
+ with open(file, 'rb') as f:
+ file_data = f.read()
+ else:
+ file_data = file.read()
+
+ assert file_data[:3] == b'VXZ', "Invalid file type"
+ version = file_data[3]
+ assert version == 0, "Invalid file version"
+
+ bin_start = struct.unpack('>I', file_data[4:8])[0]
+ structure_data = json.loads(file_data[8:bin_start].decode())
+ return structure_data
+
+
+def read_vxz(file, num_threads: int = -1) -> Union[torch.Tensor, Dict[str, torch.Tensor]]:
+ """
+ Read a VXZ file containing voxels.
+
+ Args:
+ file_path: Path or file-like object to the VXZ file.
+ num_threads: the number of threads to use for reading the file.
+
+ Returns:
+ torch.Tensor: the coordinates of the voxels.
+ Dict[str, torch.Tensor]: the attributes of the voxels.
+ """
+ if isinstance(file, str):
+ with open(file, 'rb') as f:
+ file_data = f.read()
+ else:
+ file_data = file.read()
+
+ num_threads = num_threads if num_threads > 0 else os.cpu_count()
+
+ # Parse header
+ assert file_data[:3] == b'VXZ', "Invalid file type"
+ version = file_data[3]
+ assert version == 0, "Invalid file version"
+
+ bin_start = struct.unpack('>I', file_data[4:8])[0]
+ structure_data = json.loads(file_data[8:bin_start].decode())
+ bin_data = file_data[bin_start:]
+
+ # Decode chunks
+ chunk_size = structure_data['chunk_size']
+ chunk_depth = np.log2(chunk_size)
+ assert chunk_depth.is_integer(), f"Chunk size must be a power of 2, got {chunk_size}"
+ chunk_depth = int(chunk_depth)
+
+ def worker(chunk_info):
+ decompressed = {}
+ chunk_data = bin_data[chunk_info['ptr'][0]:chunk_info['ptr'][0]+chunk_info['ptr'][1]]
+ for k, v in chunk_info.items():
+ if k in ['ptr', 'idx']:
+ continue
+ decompressed[k] = np.frombuffer(_decompress(chunk_data[v[0]:v[0]+v[1]], structure_data['compression'], structure_data['compression_level']), dtype=np.uint8)
+ svo = torch.tensor(np.frombuffer(decompressed['svo'], dtype=np.uint8))
+ morton_code = _C.decode_sparse_voxel_octree_cpu(svo, chunk_depth)
+ coord = decode_seq(morton_code.int()).cpu()
+
+ # deinterleave attributes
+ if structure_data['attr_interleave'] == 'none':
+ all_attr = []
+ for k, chs in structure_data['attr']:
+ for i in range(chs):
+ all_attr.append(torch.tensor(decompressed[f'{k}_{i}']))
+ all_attr = torch.stack(all_attr, dim=1)
+ elif structure_data['attr_interleave'] == 'as_is':
+ all_attr = []
+ for k, chs in structure_data['attr']:
+ all_attr.append(torch.tensor(decompressed[k].reshape(-1, chs)))
+ all_attr = torch.cat(all_attr, dim=1)
+ elif structure_data['attr_interleave'] == 'all':
+ all_chs = sum(chs for k, chs in structure_data['attr'])
+ all_attr = decompressed['attr'].reshape(-1, all_chs)
+
+ # unfilter
+ if structure_data['filter'] == 'none':
+ pass
+ elif structure_data['filter'] == 'parent':
+ all_attr = _C.decode_sparse_voxel_octree_attr_parent_cpu(svo, chunk_depth, all_attr)
+ elif structure_data['filter'] == 'neighbor':
+ all_attr = _C.decode_sparse_voxel_octree_attr_neighbor_cpu(coord, chunk_size, all_attr)
+
+ # final
+ attr = {}
+ ch = 0
+ for k, chs in structure_data['attr']:
+ attr[k] = all_attr[:, ch:ch+chs]
+ ch += chs
+ return {
+ 'coord': coord,
+ 'attr': attr,
+ }
+
+ if num_threads == 1:
+ chunks = [worker(info) for info in structure_data['chunks']]
+ else:
+ with ThreadPoolExecutor(max_workers=num_threads) as executor:
+ chunks = list(executor.map(worker, structure_data['chunks']))
+
+ # Combine chunks
+ coord = []
+ attr = {k: [] for k, _ in structure_data['attr']}
+ for info, chunk in zip(structure_data['chunks'], chunks):
+ coord.append(chunk['coord'] + torch.tensor([[info['idx'][0] * chunk_size, info['idx'][1] * chunk_size, info['idx'][2] * chunk_size]]).int())
+ for k, v in chunk['attr'].items():
+ attr[k].append(v)
+ coord = torch.cat(coord, dim=0)
+ for k, v in attr.items():
+ attr[k] = torch.cat(v, dim=0)
+ return coord, attr
+
+
+def write_vxz(
+ file,
+ coord: torch.Tensor,
+ attr: Dict[str, torch.Tensor],
+ chunk_size: int = 256,
+ filter: Literal['none', 'parent', 'neighbor'] = 'none',
+ compression: Literal['none', 'deflate', 'lzma', 'zstd'] = 'lzma',
+ compression_level: Optional[int] = None,
+ attr_interleave: Literal['none', 'as_is', 'all'] = 'as_is',
+ num_threads: int = -1,
+):
+ """
+ Write a VXZ file containing voxels.
+
+ Args:
+ file: Path or file-like object to the VXZ file.
+ coord: the coordinates of the voxels.
+ attr: the attributes of the voxels.
+ chunk_size: the size of each chunk.
+ filter: the filter to apply to the voxels.
+ compression: the compression algorithm to use.
+ compression_level: the level of compression.
+ attr_interleave: how to interleave the attributes.
+ num_threads: the number of threads to use for compression.
+ """
+ # Check
+ for k, v in attr.items():
+ assert coord.shape[0] == v.shape[0], f"Number of coordinates and attributes do not match for key {k}"
+ assert v.dtype == torch.uint8, f"Attributes must be uint8, got {v.dtype} for key {k}"
+ assert attr_interleave in ['none', 'as_is', 'all'], f"Invalid attr_interleave value: {attr_interleave}"
+
+ compression_level = compression_level or DEFAULT_COMPRESION_LEVEL[compression]
+ num_threads = num_threads if num_threads > 0 else os.cpu_count()
+
+ file_info = {
+ 'num_voxel': coord.shape[0],
+ 'chunk_size': chunk_size,
+ 'filter': filter,
+ 'compression': compression,
+ 'compression_level': compression_level,
+ 'raw_size': sum([coord.numel() * 4] + [v.numel() for v in attr.values()]),
+ 'compressed_size': 0,
+ 'compress_ratio': 0.0,
+ 'attr_interleave': attr_interleave,
+ 'attr': [[k, v.shape[1]] for k, v in attr.items()],
+ 'chunks': [],
+ }
+ bin_data = b''
+
+ # Split into chunks
+ chunk_depth = np.log2(chunk_size)
+ assert chunk_depth.is_integer(), f"Chunk size must be a power of 2, got {chunk_size}"
+ chunk_depth = int(chunk_depth)
+
+ chunk_coord = coord // chunk_size
+ coord = coord % chunk_size
+ unique_chunk_coord, inverse = torch.unique(chunk_coord, dim=0, return_inverse=True)
+
+ chunks = []
+ for idx, chunk_xyz in enumerate(unique_chunk_coord.tolist()):
+ chunk_mask = (inverse == idx)
+ chunks.append({
+ 'idx': chunk_xyz,
+ 'coord': coord[chunk_mask],
+ 'attr': {k: v[chunk_mask] for k, v in attr.items()},
+ })
+
+ # Compress each chunk
+ with ThreadPoolExecutor(max_workers=num_threads) as executor:
+ def worker(chunk):
+ ## compress to binary
+ coord = chunk['coord']
+ morton_code = encode_seq(coord)
+ sorted_idx = morton_code.argsort().cpu()
+ coord = coord.cpu()[sorted_idx]
+ morton_code = morton_code.cpu()[sorted_idx]
+ attr = torch.cat([v.cpu()[sorted_idx] for v in chunk['attr'].values()], dim=1)
+ svo = _C.encode_sparse_voxel_octree_cpu(morton_code, chunk_depth)
+ svo_bytes = _compress(svo.numpy().tobytes(), compression, compression_level)
+
+ # filter
+ if filter == 'none':
+ attr = attr.numpy()
+ elif filter == 'parent':
+ attr = _C.encode_sparse_voxel_octree_attr_parent_cpu(svo, chunk_depth, attr).numpy()
+ elif filter == 'neighbor':
+ attr = _C.encode_sparse_voxel_octree_attr_neighbor_cpu(coord, chunk_size, attr).numpy()
+
+ # interleave attributes
+ attr_bytes = {}
+ if attr_interleave == 'none':
+ ch = 0
+ for k, chs in file_info['attr']:
+ for i in range(chs):
+ attr_bytes[f'{k}_{i}'] = _compress(attr[:, ch].tobytes(), compression, compression_level)
+ ch += 1
+ elif attr_interleave == 'as_is':
+ ch = 0
+ for k, chs in file_info['attr']:
+ attr_bytes[k] = _compress(attr[:, ch:ch+chs].tobytes(), compression, compression_level)
+ ch += chs
+ elif attr_interleave == 'all':
+ attr_bytes['attr'] = _compress(attr.tobytes(), compression, compression_level)
+
+ ## buffer for each chunk
+ chunk_info = {'idx': chunk['idx']}
+ bin_data = b''
+
+ ### svo
+ chunk_info['svo'] = [len(bin_data), len(svo_bytes)]
+ bin_data += svo_bytes
+
+ ### attr
+ for k, v in attr_bytes.items():
+ chunk_info[k] = [len(bin_data), len(v)]
+ bin_data += v
+
+ return chunk_info, bin_data
+
+ chunks = list(executor.map(worker, chunks))
+
+ for chunk_info, chunk_data in chunks:
+ chunk_info['ptr'] = [len(bin_data), len(chunk_data)]
+ bin_data += chunk_data
+ file_info['chunks'].append(chunk_info)
+
+ file_info['compressed_size'] = len(bin_data)
+ file_info['compress_ratio'] = file_info['raw_size'] / file_info['compressed_size']
+
+ # File parts
+ structure_data = json.dumps(file_info).encode()
+ header = b'VXZ\x00' + struct.pack('>I', len(structure_data) + 8)
+
+ # Write to file
+ if isinstance(file, str):
+ with open(file, 'wb') as f:
+ f.write(header)
+ f.write(structure_data)
+ f.write(bin_data)
+ else:
+ file.write(header)
+ file.write(structure_data)
+ file.write(bin_data)
diff --git a/o-voxel/build/lib.win-amd64-cpython-311/o_voxel/postprocess.py b/o-voxel/build/lib.win-amd64-cpython-311/o_voxel/postprocess.py
new file mode 100644
index 0000000000000000000000000000000000000000..217155953d09ffd5393a1756051983e7013e62fb
--- /dev/null
+++ b/o-voxel/build/lib.win-amd64-cpython-311/o_voxel/postprocess.py
@@ -0,0 +1,331 @@
+from typing import *
+from tqdm import tqdm
+import numpy as np
+import torch
+import cv2
+from PIL import Image
+import trimesh
+import trimesh.visual
+from flex_gemm.ops.grid_sample import grid_sample_3d
+import nvdiffrast.torch as dr
+import cumesh
+
+
+def to_glb(
+ vertices: torch.Tensor,
+ faces: torch.Tensor,
+ attr_volume: torch.Tensor,
+ coords: torch.Tensor,
+ attr_layout: Dict[str, slice],
+ aabb: Union[list, tuple, np.ndarray, torch.Tensor],
+ voxel_size: Union[float, list, tuple, np.ndarray, torch.Tensor] = None,
+ grid_size: Union[int, list, tuple, np.ndarray, torch.Tensor] = None,
+ decimation_target: int = 1000000,
+ texture_size: int = 2048,
+ remesh: bool = False,
+ remesh_band: float = 1,
+ remesh_project: float = 0.9,
+ mesh_cluster_threshold_cone_half_angle_rad=np.radians(90.0),
+ mesh_cluster_refine_iterations=0,
+ mesh_cluster_global_iterations=1,
+ mesh_cluster_smooth_strength=1,
+ verbose: bool = False,
+ use_tqdm: bool = False,
+):
+ """
+ Convert an extracted mesh to a GLB file.
+ Performs cleaning, optional remeshing, UV unwrapping, and texture baking from a volume.
+
+ Args:
+ vertices: (N, 3) tensor of vertex positions
+ faces: (M, 3) tensor of vertex indices
+ attr_volume: (L, C) features of a sprase tensor for attribute interpolation
+ coords: (L, 3) tensor of coordinates for each voxel
+ attr_layout: dictionary of slice objects for each attribute
+ aabb: (2, 3) tensor of minimum and maximum coordinates of the volume
+ voxel_size: (3,) tensor of size of each voxel
+ grid_size: (3,) tensor of number of voxels in each dimension
+ decimation_target: target number of vertices for mesh simplification
+ texture_size: size of the texture for baking
+ remesh: whether to perform remeshing
+ remesh_band: size of the remeshing band
+ remesh_project: projection factor for remeshing
+ mesh_cluster_threshold_cone_half_angle_rad: threshold for cone-based clustering in uv unwrapping
+ mesh_cluster_refine_iterations: number of iterations for refining clusters in uv unwrapping
+ mesh_cluster_global_iterations: number of global iterations for clustering in uv unwrapping
+ mesh_cluster_smooth_strength: strength of smoothing for clustering in uv unwrapping
+ verbose: whether to print verbose messages
+ use_tqdm: whether to use tqdm to display progress bar
+ """
+ # --- Input Normalization (AABB, Voxel Size, Grid Size) ---
+ if isinstance(aabb, (list, tuple)):
+ aabb = np.array(aabb)
+ if isinstance(aabb, np.ndarray):
+ aabb = torch.tensor(aabb, dtype=torch.float32, device=coords.device)
+ assert isinstance(aabb, torch.Tensor), f"aabb must be a list, tuple, np.ndarray, or torch.Tensor, but got {type(aabb)}"
+ assert aabb.dim() == 2, f"aabb must be a 2D tensor, but got {aabb.shape}"
+ assert aabb.size(0) == 2, f"aabb must have 2 rows, but got {aabb.size(0)}"
+ assert aabb.size(1) == 3, f"aabb must have 3 columns, but got {aabb.size(1)}"
+
+ # Calculate grid dimensions based on AABB and voxel size
+ if voxel_size is not None:
+ if isinstance(voxel_size, float):
+ voxel_size = [voxel_size, voxel_size, voxel_size]
+ if isinstance(voxel_size, (list, tuple)):
+ voxel_size = np.array(voxel_size)
+ if isinstance(voxel_size, np.ndarray):
+ voxel_size = torch.tensor(voxel_size, dtype=torch.float32, device=coords.device)
+ grid_size = ((aabb[1] - aabb[0]) / voxel_size).round().int()
+ else:
+ assert grid_size is not None, "Either voxel_size or grid_size must be provided"
+ if isinstance(grid_size, int):
+ grid_size = [grid_size, grid_size, grid_size]
+ if isinstance(grid_size, (list, tuple)):
+ grid_size = np.array(grid_size)
+ if isinstance(grid_size, np.ndarray):
+ grid_size = torch.tensor(grid_size, dtype=torch.int32, device=coords.device)
+ voxel_size = (aabb[1] - aabb[0]) / grid_size
+
+ # Assertions for dimensions
+ assert isinstance(voxel_size, torch.Tensor)
+ assert voxel_size.dim() == 1 and voxel_size.size(0) == 3
+ assert isinstance(grid_size, torch.Tensor)
+ assert grid_size.dim() == 1 and grid_size.size(0) == 3
+
+ if use_tqdm:
+ pbar = tqdm(total=6, desc="Extracting GLB")
+ if verbose:
+ print(f"Original mesh: {vertices.shape[0]} vertices, {faces.shape[0]} faces")
+
+ # Move data to GPU
+ vertices = vertices.cuda()
+ faces = faces.cuda()
+
+ # Initialize CUDA mesh handler
+ mesh = cumesh.CuMesh()
+ mesh.init(vertices, faces)
+
+ # --- Initial Mesh Cleaning ---
+ # Fills holes as much as we can before processing
+ mesh.fill_holes(max_hole_perimeter=3e-2)
+ if verbose:
+ print(f"After filling holes: {mesh.num_vertices} vertices, {mesh.num_faces} faces")
+ vertices, faces = mesh.read()
+ if use_tqdm:
+ pbar.update(1)
+
+ # Build BVH for the current mesh to guide remeshing
+ if use_tqdm:
+ pbar.set_description("Building BVH")
+ if verbose:
+ print(f"Building BVH for current mesh...", end='', flush=True)
+ bvh = cumesh.cuBVH(vertices, faces)
+ if use_tqdm:
+ pbar.update(1)
+ if verbose:
+ print("Done")
+
+ if use_tqdm:
+ pbar.set_description("Cleaning mesh")
+ if verbose:
+ print("Cleaning mesh...")
+
+ # --- Branch 1: Standard Pipeline (Simplification & Cleaning) ---
+ if not remesh:
+ # Step 1: Aggressive simplification (3x target)
+ mesh.simplify(decimation_target * 3, verbose=verbose)
+ if verbose:
+ print(f"After inital simplification: {mesh.num_vertices} vertices, {mesh.num_faces} faces")
+
+ # Step 2: Clean up topology (duplicates, non-manifolds, isolated parts)
+ mesh.remove_duplicate_faces()
+ mesh.repair_non_manifold_edges()
+ mesh.remove_small_connected_components(1e-5)
+ mesh.fill_holes(max_hole_perimeter=3e-2)
+ if verbose:
+ print(f"After initial cleanup: {mesh.num_vertices} vertices, {mesh.num_faces} faces")
+
+ # Step 3: Final simplification to target count
+ mesh.simplify(decimation_target, verbose=verbose)
+ if verbose:
+ print(f"After final simplification: {mesh.num_vertices} vertices, {mesh.num_faces} faces")
+
+ # Step 4: Final Cleanup loop
+ mesh.remove_duplicate_faces()
+ mesh.repair_non_manifold_edges()
+ mesh.remove_small_connected_components(1e-5)
+ mesh.fill_holes(max_hole_perimeter=3e-2)
+ if verbose:
+ print(f"After final cleanup: {mesh.num_vertices} vertices, {mesh.num_faces} faces")
+
+ # Step 5: Unify face orientations
+ mesh.unify_face_orientations()
+
+ # --- Branch 2: Remeshing Pipeline ---
+ else:
+ center = aabb.mean(dim=0)
+ scale = (aabb[1] - aabb[0]).max().item()
+ resolution = grid_size.max().item()
+
+ # Perform Dual Contouring remeshing (rebuilds topology)
+ mesh.init(*cumesh.remeshing.remesh_narrow_band_dc(
+ vertices, faces,
+ center = center,
+ scale = (resolution + 3 * remesh_band) / resolution * scale,
+ resolution = resolution,
+ band = remesh_band,
+ project_back = remesh_project, # Snaps vertices back to original surface
+ verbose = verbose,
+ bvh = bvh,
+ ))
+ if verbose:
+ print(f"After remeshing: {mesh.num_vertices} vertices, {mesh.num_faces} faces")
+
+ # Simplify and clean the remeshed result (similar logic to above)
+ mesh.simplify(decimation_target, verbose=verbose)
+ if verbose:
+ print(f"After simplifying: {mesh.num_vertices} vertices, {mesh.num_faces} faces")
+
+ if use_tqdm:
+ pbar.update(1)
+ if verbose:
+ print("Done")
+
+
+ # --- UV Parameterization ---
+ if use_tqdm:
+ pbar.set_description("Parameterizing new mesh")
+ if verbose:
+ print("Parameterizing new mesh...")
+
+ out_vertices, out_faces, out_uvs, out_vmaps = mesh.uv_unwrap(
+ compute_charts_kwargs={
+ "threshold_cone_half_angle_rad": mesh_cluster_threshold_cone_half_angle_rad,
+ "refine_iterations": mesh_cluster_refine_iterations,
+ "global_iterations": mesh_cluster_global_iterations,
+ "smooth_strength": mesh_cluster_smooth_strength,
+ },
+ return_vmaps=True,
+ verbose=verbose,
+ )
+ out_vertices = out_vertices.cuda()
+ out_faces = out_faces.cuda()
+ out_uvs = out_uvs.cuda()
+ out_vmaps = out_vmaps.cuda()
+ mesh.compute_vertex_normals()
+ out_normals = mesh.read_vertex_normals()[out_vmaps]
+
+ if use_tqdm:
+ pbar.update(1)
+ if verbose:
+ print("Done")
+
+ # --- Texture Baking (Attribute Sampling) ---
+ if use_tqdm:
+ pbar.set_description("Sampling attributes")
+ if verbose:
+ print("Sampling attributes...", end='', flush=True)
+
+ # Setup differentiable rasterizer context
+ ctx = dr.RasterizeCudaContext()
+ # Prepare UV coordinates for rasterization (rendering in UV space)
+ uvs_rast = torch.cat([out_uvs * 2 - 1, torch.zeros_like(out_uvs[:, :1]), torch.ones_like(out_uvs[:, :1])], dim=-1).unsqueeze(0)
+ rast = torch.zeros((1, texture_size, texture_size, 4), device='cuda', dtype=torch.float32)
+
+ # Rasterize in chunks to save memory
+ for i in range(0, out_faces.shape[0], 100000):
+ rast_chunk, _ = dr.rasterize(
+ ctx, uvs_rast, out_faces[i:i+100000],
+ resolution=[texture_size, texture_size],
+ )
+ mask_chunk = rast_chunk[..., 3:4] > 0
+ rast_chunk[..., 3:4] += i # Store face ID in alpha channel
+ rast = torch.where(mask_chunk, rast_chunk, rast)
+
+ # Mask of valid pixels in texture
+ mask = rast[0, ..., 3] > 0
+
+ # Interpolate 3D positions in UV space (finding 3D coord for every texel)
+ pos = dr.interpolate(out_vertices.unsqueeze(0), rast, out_faces)[0][0]
+ valid_pos = pos[mask]
+
+ # Map these positions back to the *original* high-res mesh to get accurate attributes
+ # This corrects geometric errors introduced by simplification/remeshing
+ _, face_id, uvw = bvh.unsigned_distance(valid_pos, return_uvw=True)
+ orig_tri_verts = vertices[faces[face_id.long()]] # (N_new, 3, 3)
+ valid_pos = (orig_tri_verts * uvw.unsqueeze(-1)).sum(dim=1)
+
+ # Trilinear sampling from the attribute volume (Color, Material props)
+ attrs = torch.zeros(texture_size, texture_size, attr_volume.shape[1], device='cuda')
+ attrs[mask] = grid_sample_3d(
+ attr_volume,
+ torch.cat([torch.zeros_like(coords[:, :1]), coords], dim=-1),
+ shape=torch.Size([1, attr_volume.shape[1], *grid_size.tolist()]),
+ grid=((valid_pos - aabb[0]) / voxel_size).reshape(1, -1, 3),
+ mode='trilinear',
+ )
+ if use_tqdm:
+ pbar.update(1)
+ if verbose:
+ print("Done")
+
+ # --- Texture Post-Processing & Material Construction ---
+ if use_tqdm:
+ pbar.set_description("Finalizing mesh")
+ if verbose:
+ print("Finalizing mesh...", end='', flush=True)
+
+ mask = mask.cpu().numpy()
+
+ # Extract channels based on layout (BaseColor, Metallic, Roughness, Alpha)
+ base_color = np.clip(attrs[..., attr_layout['base_color']].cpu().numpy() * 255, 0, 255).astype(np.uint8)
+ metallic = np.clip(attrs[..., attr_layout['metallic']].cpu().numpy() * 255, 0, 255).astype(np.uint8)
+ roughness = np.clip(attrs[..., attr_layout['roughness']].cpu().numpy() * 255, 0, 255).astype(np.uint8)
+ alpha = np.clip(attrs[..., attr_layout['alpha']].cpu().numpy() * 255, 0, 255).astype(np.uint8)
+ alpha_mode = 'OPAQUE'
+
+ # Inpainting: fill gaps (dilation) to prevent black seams at UV boundaries
+ mask_inv = (~mask).astype(np.uint8)
+ base_color = cv2.inpaint(base_color, mask_inv, 3, cv2.INPAINT_TELEA)
+ metallic = cv2.inpaint(metallic, mask_inv, 1, cv2.INPAINT_TELEA)[..., None]
+ roughness = cv2.inpaint(roughness, mask_inv, 1, cv2.INPAINT_TELEA)[..., None]
+ alpha = cv2.inpaint(alpha, mask_inv, 1, cv2.INPAINT_TELEA)[..., None]
+
+ # Create PBR material
+ # Standard PBR packs Metallic and Roughness into Blue and Green channels
+ material = trimesh.visual.material.PBRMaterial(
+ baseColorTexture=Image.fromarray(np.concatenate([base_color, alpha], axis=-1)),
+ baseColorFactor=np.array([255, 255, 255, 255], dtype=np.uint8),
+ metallicRoughnessTexture=Image.fromarray(np.concatenate([np.zeros_like(metallic), roughness, metallic], axis=-1)),
+ metallicFactor=1.0,
+ roughnessFactor=1.0,
+ alphaMode=alpha_mode,
+ doubleSided=True if not remesh else False,
+ )
+
+ # --- Coordinate System Conversion & Final Object ---
+ vertices_np = out_vertices.cpu().numpy()
+ faces_np = out_faces.cpu().numpy()
+ uvs_np = out_uvs.cpu().numpy()
+ normals_np = out_normals.cpu().numpy()
+
+ # Swap Y and Z axes, invert Y (common conversion for GLB compatibility)
+ vertices_np[:, 1], vertices_np[:, 2] = vertices_np[:, 2], -vertices_np[:, 1]
+ normals_np[:, 1], normals_np[:, 2] = normals_np[:, 2], -normals_np[:, 1]
+ uvs_np[:, 1] = 1 - uvs_np[:, 1] # Flip UV V-coordinate
+
+ textured_mesh = trimesh.Trimesh(
+ vertices=vertices_np,
+ faces=faces_np,
+ vertex_normals=normals_np,
+ process=False,
+ visual=trimesh.visual.TextureVisuals(uv=uvs_np, material=material)
+ )
+
+ if use_tqdm:
+ pbar.update(1)
+ pbar.close()
+ if verbose:
+ print("Done")
+
+ return textured_mesh
\ No newline at end of file
diff --git a/o-voxel/build/lib.win-amd64-cpython-311/o_voxel/rasterize.py b/o-voxel/build/lib.win-amd64-cpython-311/o_voxel/rasterize.py
new file mode 100644
index 0000000000000000000000000000000000000000..63ae53b61e0cb501eb274b342bc5d337adfabfee
--- /dev/null
+++ b/o-voxel/build/lib.win-amd64-cpython-311/o_voxel/rasterize.py
@@ -0,0 +1,111 @@
+import torch
+import torch.nn.functional as F
+from easydict import EasyDict as edict
+from . import _C
+
+
+def intrinsics_to_projection(
+ intrinsics: torch.Tensor,
+ near: float,
+ far: float,
+ ) -> torch.Tensor:
+ """
+ OpenCV intrinsics to OpenGL perspective matrix
+
+ Args:
+ intrinsics (torch.Tensor): [3, 3] OpenCV intrinsics matrix
+ near (float): near plane to clip
+ far (float): far plane to clip
+ Returns:
+ (torch.Tensor): [4, 4] OpenGL perspective matrix
+ """
+ fx, fy = intrinsics[0, 0], intrinsics[1, 1]
+ cx, cy = intrinsics[0, 2], intrinsics[1, 2]
+ ret = torch.zeros((4, 4), dtype=intrinsics.dtype, device=intrinsics.device)
+ ret[0, 0] = 2 * fx
+ ret[1, 1] = 2 * fy
+ ret[0, 2] = 2 * cx - 1
+ ret[1, 2] = - 2 * cy + 1
+ ret[2, 2] = far / (far - near)
+ ret[2, 3] = near * far / (near - far)
+ ret[3, 2] = 1.
+ return ret
+
+
+class VoxelRenderer:
+ """
+ Renderer for the Voxel representation.
+
+ Args:
+ rendering_options (dict): Rendering options.
+ """
+
+ def __init__(self, rendering_options={}) -> None:
+ self.rendering_options = edict({
+ "resolution": None,
+ "near": 0.1,
+ "far": 10.0,
+ "ssaa": 1,
+ })
+ self.rendering_options.update(rendering_options)
+
+ def render(
+ self,
+ position: torch.Tensor,
+ attrs: torch.Tensor,
+ voxel_size: float,
+ extrinsics: torch.Tensor,
+ intrinsics: torch.Tensor,
+ ) -> edict:
+ """
+ Render the octree.
+
+ Args:
+ position (torch.Tensor): (N, 3) xyz positions
+ attrs (torch.Tensor): (N, C) attributes
+ voxel_size (float): voxel size
+ extrinsics (torch.Tensor): (4, 4) camera extrinsics
+ intrinsics (torch.Tensor): (3, 3) camera intrinsics
+
+ Returns:
+ edict containing:
+ attr (torch.Tensor): (C, H, W) rendered color
+ depth (torch.Tensor): (H, W) rendered depth
+ alpha (torch.Tensor): (H, W) rendered alpha
+ """
+ resolution = self.rendering_options["resolution"]
+ near = self.rendering_options["near"]
+ far = self.rendering_options["far"]
+ ssaa = self.rendering_options["ssaa"]
+
+ view = extrinsics
+ perspective = intrinsics_to_projection(intrinsics, near, far)
+ camera = torch.inverse(view)[:3, 3]
+ focalx = intrinsics[0, 0]
+ focaly = intrinsics[1, 1]
+ args = (
+ position,
+ attrs,
+ voxel_size,
+ view.T.contiguous(),
+ (perspective @ view).T.contiguous(),
+ camera,
+ 0.5 / focalx,
+ 0.5 / focaly,
+ resolution * ssaa,
+ resolution * ssaa,
+ )
+ color, depth, alpha = _C.rasterize_voxels_cuda(*args)
+
+ if ssaa > 1:
+ color = F.interpolate(color[None], size=(resolution, resolution), mode='bilinear', align_corners=False, antialias=True).squeeze()
+ depth = F.interpolate(depth[None, None], size=(resolution, resolution), mode='bilinear', align_corners=False, antialias=True).squeeze()
+ alpha = F.interpolate(alpha[None, None], size=(resolution, resolution), mode='bilinear', align_corners=False, antialias=True).squeeze()
+
+ ret = edict({
+ 'attr': color,
+ 'depth': depth,
+ 'alpha': alpha,
+ })
+ return ret
+
\ No newline at end of file
diff --git a/o-voxel/build/lib.win-amd64-cpython-311/o_voxel/serialize.py b/o-voxel/build/lib.win-amd64-cpython-311/o_voxel/serialize.py
new file mode 100644
index 0000000000000000000000000000000000000000..daf7598059ceb40e2aca64f97503c36ae5ccba0a
--- /dev/null
+++ b/o-voxel/build/lib.win-amd64-cpython-311/o_voxel/serialize.py
@@ -0,0 +1,68 @@
+from typing import *
+import torch
+from . import _C
+
+
+@torch.no_grad()
+def encode_seq(coords: torch.Tensor, permute: List[int] = [0, 1, 2], mode: Literal['z_order', 'hilbert'] = 'z_order') -> torch.Tensor:
+ """
+ Encodes 3D coordinates into a 30-bit code.
+
+ Args:
+ coords: a tensor of shape [N, 3] containing the 3D coordinates.
+ permute: the permutation of the coordinates.
+ mode: the encoding mode to use.
+ """
+ assert coords.shape[-1] == 3 and coords.ndim == 2, "Input coordinates must be of shape [N, 3]"
+ x = coords[:, permute[0]].int()
+ y = coords[:, permute[1]].int()
+ z = coords[:, permute[2]].int()
+ if mode == 'z_order':
+ if coords.device.type == 'cpu':
+ return _C.z_order_encode_cpu(x, y, z)
+ elif coords.device.type == 'cuda':
+ return _C.z_order_encode_cuda(x, y, z)
+ else:
+ raise ValueError(f"Unsupported device type: {coords.device.type}")
+ elif mode == 'hilbert':
+ if coords.device.type == 'cpu':
+ return _C.hilbert_encode_cpu(x, y, z)
+ elif coords.device.type == 'cuda':
+ return _C.hilbert_encode_cuda(x, y, z)
+ else:
+ raise ValueError(f"Unsupported device type: {coords.device.type}")
+ else:
+ raise ValueError(f"Unknown encoding mode: {mode}")
+
+
+@torch.no_grad()
+def decode_seq(code: torch.Tensor, permute: List[int] = [0, 1, 2], mode: Literal['z_order', 'hilbert'] = 'z_order') -> torch.Tensor:
+ """
+ Decodes a 30-bit code into 3D coordinates.
+
+ Args:
+ code: a tensor of shape [N] containing the 30-bit code.
+ permute: the permutation of the coordinates.
+ mode: the decoding mode to use.
+ """
+ assert code.ndim == 1, "Input code must be of shape [N]"
+ if mode == 'z_order':
+ if code.device.type == 'cpu':
+ coords = _C.z_order_decode_cpu(code)
+ elif code.device.type == 'cuda':
+ coords = _C.z_order_decode_cuda(code)
+ else:
+ raise ValueError(f"Unsupported device type: {code.device.type}")
+ elif mode == 'hilbert':
+ if code.device.type == 'cpu':
+ coords = _C.hilbert_decode_cpu(code)
+ elif code.device.type == 'cuda':
+ coords = _C.hilbert_decode_cuda(code)
+ else:
+ raise ValueError(f"Unsupported device type: {code.device.type}")
+ else:
+ raise ValueError(f"Unknown decoding mode: {mode}")
+ x = coords[permute.index(0)]
+ y = coords[permute.index(1)]
+ z = coords[permute.index(2)]
+ return torch.stack([x, y, z], dim=-1)
diff --git a/o-voxel/build/temp.win-amd64-cpython-311/Release/.ninja_deps b/o-voxel/build/temp.win-amd64-cpython-311/Release/.ninja_deps
new file mode 100644
index 0000000000000000000000000000000000000000..e95a189815280ed79c26330ecb1c51fd0e5306c1
--- /dev/null
+++ b/o-voxel/build/temp.win-amd64-cpython-311/Release/.ninja_deps
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7fadf53ce27c1cc064c4c4e7d4acdca36f011728eb7a355d2fe876d06d11ef89
+size 1473980
diff --git a/o-voxel/build/temp.win-amd64-cpython-311/Release/.ninja_log b/o-voxel/build/temp.win-amd64-cpython-311/Release/.ninja_log
new file mode 100644
index 0000000000000000000000000000000000000000..a2f12b1d6b585fcc5338da9ea8e3ed21f44315c1
--- /dev/null
+++ b/o-voxel/build/temp.win-amd64-cpython-311/Release/.ninja_log
@@ -0,0 +1,12 @@
+# ninja log v7
+43 5089 7920314585696679 C:/Users/opsiclear/Desktop/projects/Trellis2_multi_image_conditioning/o-voxel/build/temp.win-amd64-cpython-311/Release/src/serialize/z_order.obj aba9bdfd7758963
+40 5094 7920314585696679 C:/Users/opsiclear/Desktop/projects/Trellis2_multi_image_conditioning/o-voxel/build/temp.win-amd64-cpython-311/Release/src/serialize/hilbert.obj 96320bdff7b77437
+30 12370 7920314585499614 C:/Users/opsiclear/Desktop/projects/Trellis2_multi_image_conditioning/o-voxel/build/temp.win-amd64-cpython-311/Release/src/io/svo.obj 2237e66b874990a
+23 12418 7920314585499614 C:/Users/opsiclear/Desktop/projects/Trellis2_multi_image_conditioning/o-voxel/build/temp.win-amd64-cpython-311/Release/src/io/filter_neighbor.obj 41021b78b504c47e
+26 12470 7920314585499614 C:/Users/opsiclear/Desktop/projects/Trellis2_multi_image_conditioning/o-voxel/build/temp.win-amd64-cpython-311/Release/src/io/filter_parent.obj 471c5c41ea624cff
+13 13565 7920314585421492 C:/Users/opsiclear/Desktop/projects/Trellis2_multi_image_conditioning/o-voxel/build/temp.win-amd64-cpython-311/Release/src/convert/volumetic_attr.obj a880e2e3fea2c1dc
+16 14155 7920314585421492 C:/Users/opsiclear/Desktop/projects/Trellis2_multi_image_conditioning/o-voxel/build/temp.win-amd64-cpython-311/Release/src/ext.obj c49c64d83f84cba7
+9 22492 7920314585385751 C:/Users/opsiclear/Desktop/projects/Trellis2_multi_image_conditioning/o-voxel/build/temp.win-amd64-cpython-311/Release/src/convert/flexible_dual_grid.obj 387210cbde44cf56
+36 39184 7920314585658621 C:/Users/opsiclear/Desktop/projects/Trellis2_multi_image_conditioning/o-voxel/build/temp.win-amd64-cpython-311/Release/src/serialize/api.obj 9d1bef8355fab5c1
+19 39211 7920314585489571 C:/Users/opsiclear/Desktop/projects/Trellis2_multi_image_conditioning/o-voxel/build/temp.win-amd64-cpython-311/Release/src/hash/hash.obj ca81a4c30cd1e199
+33 40641 7920314585629483 C:/Users/opsiclear/Desktop/projects/Trellis2_multi_image_conditioning/o-voxel/build/temp.win-amd64-cpython-311/Release/src/rasterize/rasterize.obj cacdf260d45d5cc
diff --git a/o-voxel/build/temp.win-amd64-cpython-311/Release/build.ninja b/o-voxel/build/temp.win-amd64-cpython-311/Release/build.ninja
new file mode 100644
index 0000000000000000000000000000000000000000..5071ecdfa30b3a456e33aa1aeee76643a603bcfc
--- /dev/null
+++ b/o-voxel/build/temp.win-amd64-cpython-311/Release/build.ninja
@@ -0,0 +1,46 @@
+ninja_required_version = 1.3
+cxx = cl
+nvcc = C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.0\bin\nvcc
+
+cflags = /nologo /O2 /W3 /GL /DNDEBUG /MD -IC:\Users\opsiclear\Desktop\projects\Trellis2_multi_image_conditioning\o-voxel\third_party/eigen -IC:\Users\opsiclear\Desktop\projects\Trellis2_multi_image_conditioning\.venv\Lib\site-packages\torch\include -IC:\Users\opsiclear\Desktop\projects\Trellis2_multi_image_conditioning\.venv\Lib\site-packages\torch\include\torch\csrc\api\include "-IC:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.0\include" -IC:\Users\opsiclear\Desktop\projects\Trellis2_multi_image_conditioning\.venv\include -IC:\Users\opsiclear\AppData\Roaming\uv\python\cpython-3.11.13-windows-x86_64-none\include -IC:\Users\opsiclear\AppData\Roaming\uv\python\cpython-3.11.13-windows-x86_64-none\Include "-IC:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.44.35207\include" "-IC:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.44.35207\ATLMFC\include" "-IC:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\VS\include" "-IC:\Program Files (x86)\Windows Kits\10\include\10.0.26100.0\ucrt" "-IC:\Program Files (x86)\Windows Kits\10\\include\10.0.26100.0\\um" "-IC:\Program Files (x86)\Windows Kits\10\\include\10.0.26100.0\\shared" "-IC:\Program Files (x86)\Windows Kits\10\\include\10.0.26100.0\\winrt" "-IC:\Program Files (x86)\Windows Kits\10\\include\10.0.26100.0\\cppwinrt" "-IC:\Program Files (x86)\Windows Kits\NETFXSDK\4.8\include\um" /MD /wd4819 /wd4251 /wd4244 /wd4267 /wd4275 /wd4018 /wd4190 /wd4624 /wd4067 /wd4068 /EHsc
+post_cflags = /O2 /std:c++20 -DTORCH_API_INCLUDE_EXTENSION_H -DTORCH_EXTENSION_NAME=_C
+cuda_cflags = -std=c++17 -Xcompiler /MD -Xcompiler /wd4819 -Xcompiler /wd4251 -Xcompiler /wd4244 -Xcompiler /wd4267 -Xcompiler /wd4275 -Xcompiler /wd4018 -Xcompiler /wd4190 -Xcompiler /wd4624 -Xcompiler /wd4067 -Xcompiler /wd4068 -Xcompiler /EHsc --use-local-env -Xcudafe --diag_suppress=base_class_has_different_dll_interface -Xcudafe --diag_suppress=field_without_dll_interface -Xcudafe --diag_suppress=dll_interface_conflict_none_assumed -Xcudafe --diag_suppress=dll_interface_conflict_dllexport_assumed -IC:\Users\opsiclear\Desktop\projects\Trellis2_multi_image_conditioning\o-voxel\third_party/eigen -IC:\Users\opsiclear\Desktop\projects\Trellis2_multi_image_conditioning\.venv\Lib\site-packages\torch\include -IC:\Users\opsiclear\Desktop\projects\Trellis2_multi_image_conditioning\.venv\Lib\site-packages\torch\include\torch\csrc\api\include "-IC:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.0\include" -IC:\Users\opsiclear\Desktop\projects\Trellis2_multi_image_conditioning\.venv\include -IC:\Users\opsiclear\AppData\Roaming\uv\python\cpython-3.11.13-windows-x86_64-none\include -IC:\Users\opsiclear\AppData\Roaming\uv\python\cpython-3.11.13-windows-x86_64-none\Include "-IC:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.44.35207\include" "-IC:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.44.35207\ATLMFC\include" "-IC:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\VS\include" "-IC:\Program Files (x86)\Windows Kits\10\include\10.0.26100.0\ucrt" "-IC:\Program Files (x86)\Windows Kits\10\\include\10.0.26100.0\\um" "-IC:\Program Files (x86)\Windows Kits\10\\include\10.0.26100.0\\shared" "-IC:\Program Files (x86)\Windows Kits\10\\include\10.0.26100.0\\winrt" "-IC:\Program Files (x86)\Windows Kits\10\\include\10.0.26100.0\\cppwinrt" "-IC:\Program Files (x86)\Windows Kits\NETFXSDK\4.8\include\um"
+cuda_post_cflags = -D__CUDA_NO_HALF_OPERATORS__ -D__CUDA_NO_HALF_CONVERSIONS__ -D__CUDA_NO_BFLOAT16_CONVERSIONS__ -D__CUDA_NO_HALF2_OPERATORS__ --expt-relaxed-constexpr -O3 -std=c++20 -DTORCH_API_INCLUDE_EXTENSION_H -DTORCH_EXTENSION_NAME=_C -gencode=arch=compute_120,code=compute_120 -gencode=arch=compute_120,code=sm_120
+cuda_dlink_post_cflags =
+sycl_dlink_post_cflags =
+ldflags =
+
+rule compile
+ command = cl /showIncludes $cflags -c $in /Fo$out $post_cflags
+ deps = msvc
+
+rule cuda_compile
+ depfile = $out.d
+ deps = gcc
+ command = $nvcc --generate-dependencies-with-compile --dependency-output $out.d $cuda_cflags -c $in -o $out $cuda_post_cflags
+
+
+
+
+
+
+
+build C$:\Users\opsiclear\Desktop\projects\Trellis2_multi_image_conditioning\o-voxel\build\temp.win-amd64-cpython-311\Release\src/convert/flexible_dual_grid.obj: compile C$:\Users\opsiclear\Desktop\projects\Trellis2_multi_image_conditioning\o-voxel\src\convert\flexible_dual_grid.cpp
+build C$:\Users\opsiclear\Desktop\projects\Trellis2_multi_image_conditioning\o-voxel\build\temp.win-amd64-cpython-311\Release\src/convert/volumetic_attr.obj: compile C$:\Users\opsiclear\Desktop\projects\Trellis2_multi_image_conditioning\o-voxel\src\convert\volumetic_attr.cpp
+build C$:\Users\opsiclear\Desktop\projects\Trellis2_multi_image_conditioning\o-voxel\build\temp.win-amd64-cpython-311\Release\src/ext.obj: compile C$:\Users\opsiclear\Desktop\projects\Trellis2_multi_image_conditioning\o-voxel\src\ext.cpp
+build C$:\Users\opsiclear\Desktop\projects\Trellis2_multi_image_conditioning\o-voxel\build\temp.win-amd64-cpython-311\Release\src/hash/hash.obj: cuda_compile C$:\Users\opsiclear\Desktop\projects\Trellis2_multi_image_conditioning\o-voxel\src\hash\hash.cu
+build C$:\Users\opsiclear\Desktop\projects\Trellis2_multi_image_conditioning\o-voxel\build\temp.win-amd64-cpython-311\Release\src/io/filter_neighbor.obj: compile C$:\Users\opsiclear\Desktop\projects\Trellis2_multi_image_conditioning\o-voxel\src\io\filter_neighbor.cpp
+build C$:\Users\opsiclear\Desktop\projects\Trellis2_multi_image_conditioning\o-voxel\build\temp.win-amd64-cpython-311\Release\src/io/filter_parent.obj: compile C$:\Users\opsiclear\Desktop\projects\Trellis2_multi_image_conditioning\o-voxel\src\io\filter_parent.cpp
+build C$:\Users\opsiclear\Desktop\projects\Trellis2_multi_image_conditioning\o-voxel\build\temp.win-amd64-cpython-311\Release\src/io/svo.obj: compile C$:\Users\opsiclear\Desktop\projects\Trellis2_multi_image_conditioning\o-voxel\src\io\svo.cpp
+build C$:\Users\opsiclear\Desktop\projects\Trellis2_multi_image_conditioning\o-voxel\build\temp.win-amd64-cpython-311\Release\src/rasterize/rasterize.obj: cuda_compile C$:\Users\opsiclear\Desktop\projects\Trellis2_multi_image_conditioning\o-voxel\src\rasterize\rasterize.cu
+build C$:\Users\opsiclear\Desktop\projects\Trellis2_multi_image_conditioning\o-voxel\build\temp.win-amd64-cpython-311\Release\src/serialize/api.obj: cuda_compile C$:\Users\opsiclear\Desktop\projects\Trellis2_multi_image_conditioning\o-voxel\src\serialize\api.cu
+build C$:\Users\opsiclear\Desktop\projects\Trellis2_multi_image_conditioning\o-voxel\build\temp.win-amd64-cpython-311\Release\src/serialize/hilbert.obj: cuda_compile C$:\Users\opsiclear\Desktop\projects\Trellis2_multi_image_conditioning\o-voxel\src\serialize\hilbert.cu
+build C$:\Users\opsiclear\Desktop\projects\Trellis2_multi_image_conditioning\o-voxel\build\temp.win-amd64-cpython-311\Release\src/serialize/z_order.obj: cuda_compile C$:\Users\opsiclear\Desktop\projects\Trellis2_multi_image_conditioning\o-voxel\src\serialize\z_order.cu
+
+
+
+
+
+
+
+
diff --git a/o-voxel/build/temp.win-amd64-cpython-311/Release/src/convert/_C.cp311-win_amd64.exp b/o-voxel/build/temp.win-amd64-cpython-311/Release/src/convert/_C.cp311-win_amd64.exp
new file mode 100644
index 0000000000000000000000000000000000000000..9c983c8e043c7b99a165862f0ecaecc2849b3c00
Binary files /dev/null and b/o-voxel/build/temp.win-amd64-cpython-311/Release/src/convert/_C.cp311-win_amd64.exp differ
diff --git a/o-voxel/build/temp.win-amd64-cpython-311/Release/src/convert/_C.cp311-win_amd64.lib b/o-voxel/build/temp.win-amd64-cpython-311/Release/src/convert/_C.cp311-win_amd64.lib
new file mode 100644
index 0000000000000000000000000000000000000000..8518a45a550e692b8711112632f78b1d556035c0
Binary files /dev/null and b/o-voxel/build/temp.win-amd64-cpython-311/Release/src/convert/_C.cp311-win_amd64.lib differ
diff --git a/o-voxel/build/temp.win-amd64-cpython-311/Release/src/convert/flexible_dual_grid.obj b/o-voxel/build/temp.win-amd64-cpython-311/Release/src/convert/flexible_dual_grid.obj
new file mode 100644
index 0000000000000000000000000000000000000000..dcb19b420a3816f33a96453450eb68adf91393e1
--- /dev/null
+++ b/o-voxel/build/temp.win-amd64-cpython-311/Release/src/convert/flexible_dual_grid.obj
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:199b03ff7c85fe0c41817df7fb0ac4b69ba5fced59da8298955daaad564dddd2
+size 101177043
diff --git a/o-voxel/build/temp.win-amd64-cpython-311/Release/src/convert/volumetic_attr.obj b/o-voxel/build/temp.win-amd64-cpython-311/Release/src/convert/volumetic_attr.obj
new file mode 100644
index 0000000000000000000000000000000000000000..04968542e9280b5401f017cac60b4d8c329ffb9e
--- /dev/null
+++ b/o-voxel/build/temp.win-amd64-cpython-311/Release/src/convert/volumetic_attr.obj
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:881ae0ab39efe22c1cadae7db8483ce3c076f66b6654a0d16d333e86540e3c84
+size 54681553
diff --git a/o-voxel/build/temp.win-amd64-cpython-311/Release/src/ext.obj b/o-voxel/build/temp.win-amd64-cpython-311/Release/src/ext.obj
new file mode 100644
index 0000000000000000000000000000000000000000..4473492976c2302474a20944fbf316de928bf4b3
--- /dev/null
+++ b/o-voxel/build/temp.win-amd64-cpython-311/Release/src/ext.obj
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:25482cf27a40cae07b0d0cce67e03d390dafbb1a53bc668c0bd4ad03fc8a41c7
+size 60112845
diff --git a/o-voxel/build/temp.win-amd64-cpython-311/Release/src/hash/hash.obj b/o-voxel/build/temp.win-amd64-cpython-311/Release/src/hash/hash.obj
new file mode 100644
index 0000000000000000000000000000000000000000..9d4c684a17f549a5b60c4932894ae8678c10a067
--- /dev/null
+++ b/o-voxel/build/temp.win-amd64-cpython-311/Release/src/hash/hash.obj
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2e7aad51236896fdf39da8586753443ad460ba3827580120f0fa7e36a79c9fc2
+size 3310522
diff --git a/o-voxel/build/temp.win-amd64-cpython-311/Release/src/io/filter_neighbor.obj b/o-voxel/build/temp.win-amd64-cpython-311/Release/src/io/filter_neighbor.obj
new file mode 100644
index 0000000000000000000000000000000000000000..45798a7df4d8ada4cd34760c199eaf975065b0ab
--- /dev/null
+++ b/o-voxel/build/temp.win-amd64-cpython-311/Release/src/io/filter_neighbor.obj
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6d1b9b3e1de6293d8a846594b4666662c7713c8656988343ec237760a23a8184
+size 49303588
diff --git a/o-voxel/build/temp.win-amd64-cpython-311/Release/src/io/filter_parent.obj b/o-voxel/build/temp.win-amd64-cpython-311/Release/src/io/filter_parent.obj
new file mode 100644
index 0000000000000000000000000000000000000000..582550203b2c7c120ae5d927e66f1bac72d81a13
--- /dev/null
+++ b/o-voxel/build/temp.win-amd64-cpython-311/Release/src/io/filter_parent.obj
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:be06f0928225aaa96f784d6c54e43cd248fc9077b3ff3761bab35c78188a5f91
+size 49318336
diff --git a/o-voxel/build/temp.win-amd64-cpython-311/Release/src/io/svo.obj b/o-voxel/build/temp.win-amd64-cpython-311/Release/src/io/svo.obj
new file mode 100644
index 0000000000000000000000000000000000000000..a1cebf153e29124b256e2403b1b12c9bcb92b30d
--- /dev/null
+++ b/o-voxel/build/temp.win-amd64-cpython-311/Release/src/io/svo.obj
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:84252f221ffdca2a21050f669078f59080986b7a623fbd4d4f25358c35c9e125
+size 49340082
diff --git a/o-voxel/build/temp.win-amd64-cpython-311/Release/src/rasterize/rasterize.obj b/o-voxel/build/temp.win-amd64-cpython-311/Release/src/rasterize/rasterize.obj
new file mode 100644
index 0000000000000000000000000000000000000000..4084ff387308dfdd654446c82329e703402cbd7c
--- /dev/null
+++ b/o-voxel/build/temp.win-amd64-cpython-311/Release/src/rasterize/rasterize.obj
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5c5b1d508b498de72395b0aee27f4d8db3f53463c5a3c5ec5f0f30b8652c5c3f
+size 3082508
diff --git a/o-voxel/build/temp.win-amd64-cpython-311/Release/src/serialize/api.obj b/o-voxel/build/temp.win-amd64-cpython-311/Release/src/serialize/api.obj
new file mode 100644
index 0000000000000000000000000000000000000000..80494f891b26de4b89aa0b09b640083e05d956a0
--- /dev/null
+++ b/o-voxel/build/temp.win-amd64-cpython-311/Release/src/serialize/api.obj
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0b1750282d12d2693bda53c40f7ff1996e502116ddc5b49d9f73459120133302
+size 3021052
diff --git a/o-voxel/build/temp.win-amd64-cpython-311/Release/src/serialize/hilbert.obj b/o-voxel/build/temp.win-amd64-cpython-311/Release/src/serialize/hilbert.obj
new file mode 100644
index 0000000000000000000000000000000000000000..6a5dd068915ad8936e1cfc077c1ae9463fa9f19a
Binary files /dev/null and b/o-voxel/build/temp.win-amd64-cpython-311/Release/src/serialize/hilbert.obj differ
diff --git a/o-voxel/build/temp.win-amd64-cpython-311/Release/src/serialize/z_order.obj b/o-voxel/build/temp.win-amd64-cpython-311/Release/src/serialize/z_order.obj
new file mode 100644
index 0000000000000000000000000000000000000000..177c37c2a4b00469e6d77a25c09788e7ab06492f
Binary files /dev/null and b/o-voxel/build/temp.win-amd64-cpython-311/Release/src/serialize/z_order.obj differ
diff --git a/o-voxel/examples/mesh2ovox.py b/o-voxel/examples/mesh2ovox.py
new file mode 100644
index 0000000000000000000000000000000000000000..12fa2c9d7540ef74e0afcd4e0a8258b191d2fc90
--- /dev/null
+++ b/o-voxel/examples/mesh2ovox.py
@@ -0,0 +1,57 @@
+import torch
+import o_voxel
+import utils
+
+RES = 512
+
+asset = utils.get_helmet()
+
+# 0. Normalize asset to unit cube
+aabb = asset.bounding_box.bounds
+center = (aabb[0] + aabb[1]) / 2
+scale = 0.99999 / (aabb[1] - aabb[0]).max() # To avoid numerical issues
+asset.apply_translation(-center)
+asset.apply_scale(scale)
+
+# 1. Geometry Voxelization (Flexible Dual Grid)
+# Returns: occupied indices, dual vertices (QEF solution), and edge intersected
+mesh = asset.to_mesh()
+vertices = torch.from_numpy(mesh.vertices).float()
+faces = torch.from_numpy(mesh.faces).long()
+voxel_indices, dual_vertices, intersected = o_voxel.convert.mesh_to_flexible_dual_grid(
+ vertices, faces,
+ grid_size=RES, # Resolution
+ aabb=[[-0.5,-0.5,-0.5],[0.5,0.5,0.5]], # Axis-aligned bounding box
+ face_weight=1.0, # Face term weight in QEF
+ boundary_weight=0.2, # Boundary term weight in QEF
+ regularization_weight=1e-2, # Regularization term weight in QEF
+ timing=True
+)
+## sort to ensure align between geometry and material voxelization
+vid = o_voxel.serialize.encode_seq(voxel_indices)
+mapping = torch.argsort(vid)
+voxel_indices = voxel_indices[mapping]
+dual_vertices = dual_vertices[mapping]
+intersected = intersected[mapping]
+
+# 2. Material Voxelization (Volumetric Attributes)
+# Returns: dict containing 'base_color', 'metallic', 'roughness', etc.
+voxel_indices_mat, attributes = o_voxel.convert.textured_mesh_to_volumetric_attr(
+ asset,
+ grid_size=RES,
+ aabb=[[-0.5,-0.5,-0.5],[0.5,0.5,0.5]],
+ timing=True
+)
+## sort to ensure align between geometry and material voxelization
+vid_mat = o_voxel.serialize.encode_seq(voxel_indices_mat)
+mapping_mat = torch.argsort(vid_mat)
+attributes = {k: v[mapping_mat] for k, v in attributes.items()}
+
+# Save to compressed .vxz format
+## packing
+dual_vertices = dual_vertices * RES - voxel_indices
+dual_vertices = (torch.clamp(dual_vertices, 0, 1) * 255).type(torch.uint8)
+intersected = (intersected[:, 0:1] + 2 * intersected[:, 1:2] + 4 * intersected[:, 2:3]).type(torch.uint8)
+attributes['dual_vertices'] = dual_vertices
+attributes['intersected'] = intersected
+o_voxel.io.write("ovoxel_helmet.vxz", voxel_indices, attributes)
\ No newline at end of file
diff --git a/o-voxel/examples/ovox2glb.py b/o-voxel/examples/ovox2glb.py
new file mode 100644
index 0000000000000000000000000000000000000000..9ffb654cd4f9e0138c5d38b9fafaa72608bcc2d8
--- /dev/null
+++ b/o-voxel/examples/ovox2glb.py
@@ -0,0 +1,52 @@
+import torch
+import o_voxel
+
+RES = 512
+
+# Load data
+coords, data = o_voxel.io.read("ovoxel_helmet.vxz")
+dual_vertices = data['dual_vertices']
+intersected = data['intersected']
+base_color = data['base_color']
+metallic = data['metallic']
+roughness = data['roughness']
+alpha = data['alpha']
+
+# Depack
+dual_vertices = dual_vertices / 255
+intersected = torch.cat([
+ intersected % 2,
+ intersected // 2 % 2,
+ intersected // 4 % 2,
+], dim=-1).bool()
+
+# Extract Mesh
+# O-Voxel connects dual vertices to form quads, optionally splitting them
+# based on geometric features.
+rec_verts, rec_faces = o_voxel.convert.flexible_dual_grid_to_mesh(
+ coords.cuda(),
+ dual_vertices.cuda(),
+ intersected.cuda(),
+ split_weight=None, # Auto-split based on min angle if None
+ grid_size=RES,
+ aabb=[[-0.5,-0.5,-0.5],[0.5,0.5,0.5]],
+)
+
+# Post-process
+attr_volume = torch.cat([base_color.cuda(), metallic.cuda(), roughness.cuda(), alpha.cuda()], dim=-1) / 255
+attr_layout = {'base_color': slice(0,3), 'metallic': slice(3,4), 'roughness': slice(4,5), 'alpha': slice(5,6)}
+mesh = o_voxel.postprocess.to_glb(
+ vertices=rec_verts,
+ faces=rec_faces,
+ attr_volume=attr_volume,
+ coords=coords.cuda(),
+ attr_layout=attr_layout,
+ grid_size=RES,
+ aabb=[[-0.5,-0.5,-0.5],[0.5,0.5,0.5]],
+ decimation_target=100000,
+ texture_size=2048,
+ verbose=True,
+)
+
+# Save as glb
+mesh.export("rec_helmet.glb")
diff --git a/o-voxel/examples/ovox2mesh.py b/o-voxel/examples/ovox2mesh.py
new file mode 100644
index 0000000000000000000000000000000000000000..5644cd82b7ec71c590ef1ccb0cb04d967c378c61
--- /dev/null
+++ b/o-voxel/examples/ovox2mesh.py
@@ -0,0 +1,45 @@
+import torch
+import o_voxel
+import trimesh
+import trimesh.visual
+
+RES = 512
+
+# Load data
+coords, data = o_voxel.io.read("ovoxel_helmet.vxz")
+dual_vertices = data['dual_vertices']
+intersected = data['intersected']
+base_color = data['base_color']
+metallic = data['metallic']
+roughness = data['roughness']
+alpha = data['alpha']
+
+# Depack
+dual_vertices = dual_vertices / 255
+intersected = torch.cat([
+ intersected % 2,
+ intersected // 2 % 2,
+ intersected // 4 % 2,
+], dim=-1).bool()
+
+# Extract Mesh
+# O-Voxel connects dual vertices to form quads, optionally splitting them
+# based on geometric features.
+rec_verts, rec_faces = o_voxel.convert.flexible_dual_grid_to_mesh(
+ coords.cuda(),
+ dual_vertices.cuda(),
+ intersected.cuda(),
+ split_weight=None, # Auto-split based on min angle if None
+ grid_size=RES,
+ aabb=[[-0.5,-0.5,-0.5],[0.5,0.5,0.5]],
+)
+
+# Save as ply
+visual = trimesh.visual.ColorVisuals(
+ vertex_colors=base_color,
+)
+mesh = trimesh.Trimesh(
+ vertices=rec_verts.cpu(), faces=rec_faces.cpu(), visual=visual,
+ process=False
+)
+mesh.export("rec_helmet.ply")
diff --git a/o-voxel/examples/render_ovox.py b/o-voxel/examples/render_ovox.py
new file mode 100644
index 0000000000000000000000000000000000000000..09d49f63e0b7fd957bad7c3407d464cc1d0f2eef
--- /dev/null
+++ b/o-voxel/examples/render_ovox.py
@@ -0,0 +1,39 @@
+import torch
+import numpy as np
+import imageio
+import o_voxel
+import utils3d
+
+RES = 512
+
+# Load data
+coords, data = o_voxel.io.read("ovoxel_helmet.vxz")
+position = (coords / RES - 0.5).cuda()
+base_color = (data['base_color'] / 255).cuda()
+
+# Setup camera
+extr = utils3d.extrinsics_look_at(
+ eye=torch.tensor([1.2, 0.5, 1.2]),
+ look_at=torch.tensor([0.0, 0.0, 0.0]),
+ up=torch.tensor([0.0, 1.0, 0.0])
+).cuda()
+intr = utils3d.intrinsics_from_fov_xy(
+ fov_x=torch.deg2rad(torch.tensor(45.0)),
+ fov_y=torch.deg2rad(torch.tensor(45.0)),
+).cuda()
+
+# Render
+renderer = o_voxel.rasterize.VoxelRenderer(
+ rendering_options={"resolution": 512, "ssaa": 2}
+)
+output = renderer.render(
+ position=position, # Voxel centers
+ attrs=base_color, # Color/Opacity etc.
+ voxel_size=1.0/RES,
+ extrinsics=extr,
+ intrinsics=intr
+)
+image = np.clip(
+ output.attr.permute(1, 2, 0).cpu().numpy() * 255, 0, 255
+).astype(np.uint8)
+imageio.imwrite("ovoxel_helmet_visualization.png", image)
diff --git a/o-voxel/examples/utils.py b/o-voxel/examples/utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..75750e4ffc4b98fa1a5f6d6f2e2e62e8cc094844
--- /dev/null
+++ b/o-voxel/examples/utils.py
@@ -0,0 +1,27 @@
+import os
+import requests
+import tarfile
+import trimesh
+
+HELMET_URL = "https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/refs/heads/main/2.0/DamagedHelmet/glTF-Binary/DamagedHelmet.glb"
+CACHE_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), "cache")
+
+
+def download_file(url, path):
+ print(f"Downloading from {url} ...")
+ resp = requests.get(url, stream=True)
+ resp.raise_for_status()
+
+ with open(path, "wb") as f:
+ for chunk in resp.iter_content(chunk_size=8192):
+ f.write(chunk)
+
+ print(f"Saved to {path}")
+
+
+def get_helmet() -> trimesh.Trimesh:
+ HELMET_PATH = os.path.join(CACHE_DIR, "helmet.glb")
+ if not os.path.exists(HELMET_PATH):
+ os.makedirs(CACHE_DIR, exist_ok=True)
+ download_file(HELMET_URL, HELMET_PATH)
+ return trimesh.load(HELMET_PATH)
diff --git a/o-voxel/o_voxel.egg-info/PKG-INFO b/o-voxel/o_voxel.egg-info/PKG-INFO
new file mode 100644
index 0000000000000000000000000000000000000000..2fb624924f60112ee8cfcdbab689e54f635fa5a2
--- /dev/null
+++ b/o-voxel/o_voxel.egg-info/PKG-INFO
@@ -0,0 +1,15 @@
+Metadata-Version: 2.1
+Name: o_voxel
+Version: 0.0.1
+Summary: All about voxel.
+Author-email: Jianfeng Xiang
+Requires-Python: >=3.8
+Requires-Dist: torch
+Requires-Dist: numpy
+Requires-Dist: plyfile
+Requires-Dist: trimesh
+Requires-Dist: tqdm
+Requires-Dist: zstandard
+Requires-Dist: easydict
+Requires-Dist: cumesh@ git+https://github.com/JeffreyXiang/CuMesh.git
+Requires-Dist: flex_gemm@ git+https://github.com/JeffreyXiang/FlexGEMM.git
diff --git a/o-voxel/o_voxel.egg-info/SOURCES.txt b/o-voxel/o_voxel.egg-info/SOURCES.txt
new file mode 100644
index 0000000000000000000000000000000000000000..9e14690d1a030c99ef6ec2c48e6ac111251c0d77
--- /dev/null
+++ b/o-voxel/o_voxel.egg-info/SOURCES.txt
@@ -0,0 +1,30 @@
+README.md
+pyproject.toml
+setup.py
+o_voxel/__init__.py
+o_voxel/postprocess.py
+o_voxel/rasterize.py
+o_voxel/serialize.py
+o_voxel.egg-info/PKG-INFO
+o_voxel.egg-info/SOURCES.txt
+o_voxel.egg-info/dependency_links.txt
+o_voxel.egg-info/requires.txt
+o_voxel.egg-info/top_level.txt
+o_voxel/convert/__init__.py
+o_voxel/convert/flexible_dual_grid.py
+o_voxel/convert/volumetic_attr.py
+o_voxel/io/__init__.py
+o_voxel/io/npz.py
+o_voxel/io/ply.py
+o_voxel/io/vxz.py
+src/ext.cpp
+src/convert/flexible_dual_grid.cpp
+src/convert/volumetic_attr.cpp
+src/hash/hash.cu
+src/io/filter_neighbor.cpp
+src/io/filter_parent.cpp
+src/io/svo.cpp
+src/rasterize/rasterize.cu
+src/serialize/api.cu
+src/serialize/hilbert.cu
+src/serialize/z_order.cu
\ No newline at end of file
diff --git a/o-voxel/o_voxel.egg-info/dependency_links.txt b/o-voxel/o_voxel.egg-info/dependency_links.txt
new file mode 100644
index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc
--- /dev/null
+++ b/o-voxel/o_voxel.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/o-voxel/o_voxel.egg-info/requires.txt b/o-voxel/o_voxel.egg-info/requires.txt
new file mode 100644
index 0000000000000000000000000000000000000000..9a6d507faebf064501723f6beefb1c9da4c9ce12
--- /dev/null
+++ b/o-voxel/o_voxel.egg-info/requires.txt
@@ -0,0 +1,9 @@
+torch
+numpy
+plyfile
+trimesh
+tqdm
+zstandard
+easydict
+cumesh@ git+https://github.com/JeffreyXiang/CuMesh.git
+flex_gemm@ git+https://github.com/JeffreyXiang/FlexGEMM.git
diff --git a/o-voxel/o_voxel.egg-info/top_level.txt b/o-voxel/o_voxel.egg-info/top_level.txt
new file mode 100644
index 0000000000000000000000000000000000000000..867781a47f7be9716aac3839808851f0f8495384
--- /dev/null
+++ b/o-voxel/o_voxel.egg-info/top_level.txt
@@ -0,0 +1 @@
+o_voxel
diff --git a/o-voxel/o_voxel/__init__.py b/o-voxel/o_voxel/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..55ffd986b47551517fbf1d1538b40f77ec5ee8f8
--- /dev/null
+++ b/o-voxel/o_voxel/__init__.py
@@ -0,0 +1,7 @@
+from . import (
+ convert,
+ io,
+ postprocess,
+ rasterize,
+ serialize
+)
\ No newline at end of file
diff --git a/o-voxel/o_voxel/convert/__init__.py b/o-voxel/o_voxel/convert/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..25755f9b06a2cf37856f3be043e3928a5e23510c
--- /dev/null
+++ b/o-voxel/o_voxel/convert/__init__.py
@@ -0,0 +1,2 @@
+from .flexible_dual_grid import *
+from .volumetic_attr import *
\ No newline at end of file
diff --git a/o-voxel/o_voxel/convert/flexible_dual_grid.py b/o-voxel/o_voxel/convert/flexible_dual_grid.py
new file mode 100644
index 0000000000000000000000000000000000000000..51b8b0552fe697e95f7496a370a56c538c8abd10
--- /dev/null
+++ b/o-voxel/o_voxel/convert/flexible_dual_grid.py
@@ -0,0 +1,283 @@
+from typing import *
+import numpy as np
+import torch
+from .. import _C
+
+__all__ = [
+ "mesh_to_flexible_dual_grid",
+ "flexible_dual_grid_to_mesh",
+]
+
+
+def _init_hashmap(grid_size, capacity, device):
+ VOL = (grid_size[0] * grid_size[1] * grid_size[2]).item()
+
+ # If the number of elements in the tensor is less than 2^32, use uint32 as the hashmap type, otherwise use uint64.
+ if VOL < 2**32:
+ hashmap_keys = torch.full((capacity,), torch.iinfo(torch.uint32).max, dtype=torch.uint32, device=device)
+ elif VOL < 2**64:
+ hashmap_keys = torch.full((capacity,), torch.iinfo(torch.uint64).max, dtype=torch.uint64, device=device)
+ else:
+ raise ValueError(f"The spatial size is too large to fit in a hashmap. Get volumn {VOL} > 2^64.")
+
+ hashmap_vals = torch.empty((capacity,), dtype=torch.uint32, device=device)
+
+ return hashmap_keys, hashmap_vals
+
+
+@torch.no_grad()
+def mesh_to_flexible_dual_grid(
+ vertices: torch.Tensor,
+ faces: torch.Tensor,
+ voxel_size: Union[float, list, tuple, np.ndarray, torch.Tensor] = None,
+ grid_size: Union[int, list, tuple, np.ndarray, torch.Tensor] = None,
+ aabb: Union[list, tuple, np.ndarray, torch.Tensor] = None,
+ face_weight: float = 1.0,
+ boundary_weight: float = 1.0,
+ regularization_weight: float = 0.1,
+ timing: bool = False,
+) -> Union[torch.Tensor, torch.Tensor, torch.Tensor]:
+ """
+ Voxelize a mesh into a sparse voxel grid.
+
+ Args:
+ vertices (torch.Tensor): The vertices of the mesh.
+ faces (torch.Tensor): The faces of the mesh.
+ voxel_size (float, list, tuple, np.ndarray, torch.Tensor): The size of each voxel.
+ grid_size (int, list, tuple, np.ndarray, torch.Tensor): The size of the grid.
+ NOTE: One of voxel_size and grid_size must be provided.
+ aabb (list, tuple, np.ndarray, torch.Tensor): The axis-aligned bounding box of the mesh.
+ If not provided, it will be computed automatically.
+ face_weight (float): The weight of the face term in the QEF when solving the dual vertices.
+ boundary_weight (float): The weight of the boundary term in the QEF when solving the dual vertices.
+ regularization_weight (float): The weight of the regularization term in the QEF when solving the dual vertices.
+ timing (bool): Whether to time the voxelization process.
+
+ Returns:
+ torch.Tensor: The indices of the voxels that are occupied by the mesh.
+ The shape of the tensor is (N, 3), where N is the number of occupied voxels.
+ torch.Tensor: The dual vertices of the mesh.
+ torch.Tensor: The intersected flag of each voxel.
+ """
+
+ # Load mesh
+ vertices = vertices.float()
+ faces = faces.int()
+
+ # Voxelize settings
+ assert voxel_size is not None or grid_size is not None, "Either voxel_size or grid_size must be provided"
+
+ if voxel_size is not None:
+ if isinstance(voxel_size, float):
+ voxel_size = [voxel_size, voxel_size, voxel_size]
+ if isinstance(voxel_size, (list, tuple)):
+ voxel_size = np.array(voxel_size)
+ if isinstance(voxel_size, np.ndarray):
+ voxel_size = torch.tensor(voxel_size, dtype=torch.float32)
+ assert isinstance(voxel_size, torch.Tensor), f"voxel_size must be a float, list, tuple, np.ndarray, or torch.Tensor, but got {type(voxel_size)}"
+ assert voxel_size.dim() == 1, f"voxel_size must be a 1D tensor, but got {voxel_size.shape}"
+ assert voxel_size.size(0) == 3, f"voxel_size must have 3 elements, but got {voxel_size.size(0)}"
+
+ if grid_size is not None:
+ if isinstance(grid_size, int):
+ grid_size = [grid_size, grid_size, grid_size]
+ if isinstance(grid_size, (list, tuple)):
+ grid_size = np.array(grid_size)
+ if isinstance(grid_size, np.ndarray):
+ grid_size = torch.tensor(grid_size, dtype=torch.int32)
+ assert isinstance(grid_size, torch.Tensor), f"grid_size must be an int, list, tuple, np.ndarray, or torch.Tensor, but got {type(grid_size)}"
+ assert grid_size.dim() == 1, f"grid_size must be a 1D tensor, but got {grid_size.shape}"
+ assert grid_size.size(0) == 3, f"grid_size must have 3 elements, but got {grid_size.size(0)}"
+
+ if aabb is not None:
+ if isinstance(aabb, (list, tuple)):
+ aabb = np.array(aabb)
+ if isinstance(aabb, np.ndarray):
+ aabb = torch.tensor(aabb, dtype=torch.float32)
+ assert isinstance(aabb, torch.Tensor), f"aabb must be a list, tuple, np.ndarray, or torch.Tensor, but got {type(aabb)}"
+ assert aabb.dim() == 2, f"aabb must be a 2D tensor, but got {aabb.shape}"
+ assert aabb.size(0) == 2, f"aabb must have 2 rows, but got {aabb.size(0)}"
+ assert aabb.size(1) == 3, f"aabb must have 3 columns, but got {aabb.size(1)}"
+
+ # Auto adjust aabb
+ if aabb is None:
+ min_xyz = vertices.min(dim=0).values
+ max_xyz = vertices.max(dim=0).values
+
+ if voxel_size is not None:
+ padding = torch.ceil((max_xyz - min_xyz) / voxel_size) * voxel_size - (max_xyz - min_xyz)
+ min_xyz -= padding * 0.5
+ max_xyz += padding * 0.5
+ if grid_size is not None:
+ padding = (max_xyz - min_xyz) / (grid_size - 1)
+ min_xyz -= padding * 0.5
+ max_xyz += padding * 0.5
+
+ aabb = torch.stack([min_xyz, max_xyz], dim=0).float().cuda()
+
+ # Fill voxel size or grid size
+ if voxel_size is None:
+ voxel_size = (aabb[1] - aabb[0]) / grid_size
+ if grid_size is None:
+ grid_size = ((aabb[1] - aabb[0]) / voxel_size).round().int()
+
+ # subdivide mesh
+ vertices = vertices - aabb[0].reshape(1, 3)
+ grid_range = torch.stack([torch.zeros_like(grid_size), grid_size], dim=0).int()
+
+ ret = _C.mesh_to_flexible_dual_grid_cpu(
+ vertices,
+ faces,
+ voxel_size,
+ grid_range,
+ face_weight,
+ boundary_weight,
+ regularization_weight,
+ timing,
+ )
+
+ return ret
+
+
+def flexible_dual_grid_to_mesh(
+ coords: torch.Tensor,
+ dual_vertices: torch.Tensor,
+ intersected_flag: torch.Tensor,
+ split_weight: Union[torch.Tensor, None],
+ aabb: Union[list, tuple, np.ndarray, torch.Tensor],
+ voxel_size: Union[float, list, tuple, np.ndarray, torch.Tensor] = None,
+ grid_size: Union[int, list, tuple, np.ndarray, torch.Tensor] = None,
+ train: bool = False,
+):
+ """
+ Extract mesh from sparse voxel structures using flexible dual grid.
+
+ Args:
+ coords (torch.Tensor): The coordinates of the voxels.
+ dual_vertices (torch.Tensor): The dual vertices.
+ intersected_flag (torch.Tensor): The intersected flag.
+ split_weight (torch.Tensor): The split weight of each dual quad. If None, the algorithm
+ will split based on minimum angle.
+ aabb (list, tuple, np.ndarray, torch.Tensor): The axis-aligned bounding box of the mesh.
+ voxel_size (float, list, tuple, np.ndarray, torch.Tensor): The size of each voxel.
+ grid_size (int, list, tuple, np.ndarray, torch.Tensor): The size of the grid.
+ NOTE: One of voxel_size and grid_size must be provided.
+ train (bool): Whether to use training mode.
+
+ Returns:
+ vertices (torch.Tensor): The vertices of the mesh.
+ faces (torch.Tensor): The faces of the mesh.
+ """
+ # Static variables
+ if not hasattr(flexible_dual_grid_to_mesh, "edge_neighbor_voxel_offset"):
+ flexible_dual_grid_to_mesh.edge_neighbor_voxel_offset = torch.tensor([
+ [[0, 0, 0], [0, 0, 1], [0, 1, 1], [0, 1, 0]], # x-axis
+ [[0, 0, 0], [1, 0, 0], [1, 0, 1], [0, 0, 1]], # y-axis
+ [[0, 0, 0], [0, 1, 0], [1, 1, 0], [1, 0, 0]], # z-axis
+ ], dtype=torch.int, device=coords.device).unsqueeze(0)
+ if not hasattr(flexible_dual_grid_to_mesh, "quad_split_1"):
+ flexible_dual_grid_to_mesh.quad_split_1 = torch.tensor([0, 1, 2, 0, 2, 3], dtype=torch.long, device=coords.device, requires_grad=False)
+ if not hasattr(flexible_dual_grid_to_mesh, "quad_split_2"):
+ flexible_dual_grid_to_mesh.quad_split_2 = torch.tensor([0, 1, 3, 3, 1, 2], dtype=torch.long, device=coords.device, requires_grad=False)
+ if not hasattr(flexible_dual_grid_to_mesh, "quad_split_train"):
+ flexible_dual_grid_to_mesh.quad_split_train = torch.tensor([0, 1, 4, 1, 2, 4, 2, 3, 4, 3, 0, 4], dtype=torch.long, device=coords.device, requires_grad=False)
+
+ # AABB
+ if isinstance(aabb, (list, tuple)):
+ aabb = np.array(aabb)
+ if isinstance(aabb, np.ndarray):
+ aabb = torch.tensor(aabb, dtype=torch.float32, device=coords.device)
+ assert isinstance(aabb, torch.Tensor), f"aabb must be a list, tuple, np.ndarray, or torch.Tensor, but got {type(aabb)}"
+ assert aabb.dim() == 2, f"aabb must be a 2D tensor, but got {aabb.shape}"
+ assert aabb.size(0) == 2, f"aabb must have 2 rows, but got {aabb.size(0)}"
+ assert aabb.size(1) == 3, f"aabb must have 3 columns, but got {aabb.size(1)}"
+
+ # Voxel size
+ if voxel_size is not None:
+ if isinstance(voxel_size, float):
+ voxel_size = [voxel_size, voxel_size, voxel_size]
+ if isinstance(voxel_size, (list, tuple)):
+ voxel_size = np.array(voxel_size)
+ if isinstance(voxel_size, np.ndarray):
+ voxel_size = torch.tensor(voxel_size, dtype=torch.float32, device=coords.device)
+ grid_size = ((aabb[1] - aabb[0]) / voxel_size).round().int()
+ else:
+ assert grid_size is not None, "Either voxel_size or grid_size must be provided"
+ if isinstance(grid_size, int):
+ grid_size = [grid_size, grid_size, grid_size]
+ if isinstance(grid_size, (list, tuple)):
+ grid_size = np.array(grid_size)
+ if isinstance(grid_size, np.ndarray):
+ grid_size = torch.tensor(grid_size, dtype=torch.int32, device=coords.device)
+ voxel_size = (aabb[1] - aabb[0]) / grid_size
+ assert isinstance(voxel_size, torch.Tensor), f"voxel_size must be a float, list, tuple, np.ndarray, or torch.Tensor, but got {type(voxel_size)}"
+ assert voxel_size.dim() == 1, f"voxel_size must be a 1D tensor, but got {voxel_size.shape}"
+ assert voxel_size.size(0) == 3, f"voxel_size must have 3 elements, but got {voxel_size.size(0)}"
+ assert isinstance(grid_size, torch.Tensor), f"grid_size must be an int, list, tuple, np.ndarray, or torch.Tensor, but got {type(grid_size)}"
+ assert grid_size.dim() == 1, f"grid_size must be a 1D tensor, but got {grid_size.shape}"
+ assert grid_size.size(0) == 3, f"grid_size must have 3 elements, but got {grid_size.size(0)}"
+
+ # Extract mesh
+ N = dual_vertices.shape[0]
+ mesh_vertices = (coords.float() + dual_vertices) / (2 * N) - 0.5
+
+ # Store active voxels into hashmap
+ hashmap = _init_hashmap(grid_size, 2 * N, device=coords.device)
+ _C.hashmap_insert_3d_idx_as_val_cuda(*hashmap, torch.cat([torch.zeros_like(coords[:, :1]), coords], dim=-1), *grid_size.tolist())
+
+ # Find connected voxels
+ edge_neighbor_voxel = coords.reshape(N, 1, 1, 3) + flexible_dual_grid_to_mesh.edge_neighbor_voxel_offset # (N, 3, 4, 3)
+ connected_voxel = edge_neighbor_voxel[intersected_flag] # (M, 4, 3)
+ M = connected_voxel.shape[0]
+ connected_voxel_hash_key = torch.cat([
+ torch.zeros((M * 4, 1), dtype=torch.int, device=coords.device),
+ connected_voxel.reshape(-1, 3)
+ ], dim=1)
+ connected_voxel_indices = _C.hashmap_lookup_3d_cuda(*hashmap, connected_voxel_hash_key, *grid_size.tolist()).reshape(M, 4).int()
+ connected_voxel_valid = (connected_voxel_indices != 0xffffffff).all(dim=1)
+ quad_indices = connected_voxel_indices[connected_voxel_valid].int() # (L, 4)
+ L = quad_indices.shape[0]
+
+ # Construct triangles
+ if not train:
+ mesh_vertices = (coords.float() + dual_vertices) * voxel_size + aabb[0].reshape(1, 3)
+ if split_weight is None:
+ # if split 1
+ atempt_triangles_0 = quad_indices[:, flexible_dual_grid_to_mesh.quad_split_1]
+ normals0 = torch.cross(mesh_vertices[atempt_triangles_0[:, 1]] - mesh_vertices[atempt_triangles_0[:, 0]], mesh_vertices[atempt_triangles_0[:, 2]] - mesh_vertices[atempt_triangles_0[:, 0]])
+ normals1 = torch.cross(mesh_vertices[atempt_triangles_0[:, 2]] - mesh_vertices[atempt_triangles_0[:, 1]], mesh_vertices[atempt_triangles_0[:, 3]] - mesh_vertices[atempt_triangles_0[:, 1]])
+ align0 = (normals0 * normals1).sum(dim=1, keepdim=True).abs()
+ # if split 2
+ atempt_triangles_1 = quad_indices[:, flexible_dual_grid_to_mesh.quad_split_2]
+ normals0 = torch.cross(mesh_vertices[atempt_triangles_1[:, 1]] - mesh_vertices[atempt_triangles_1[:, 0]], mesh_vertices[atempt_triangles_1[:, 2]] - mesh_vertices[atempt_triangles_1[:, 0]])
+ normals1 = torch.cross(mesh_vertices[atempt_triangles_1[:, 2]] - mesh_vertices[atempt_triangles_1[:, 1]], mesh_vertices[atempt_triangles_1[:, 3]] - mesh_vertices[atempt_triangles_1[:, 1]])
+ align1 = (normals0 * normals1).sum(dim=1, keepdim=True).abs()
+ # select split
+ mesh_triangles = torch.where(align0 > align1, atempt_triangles_0, atempt_triangles_1).reshape(-1, 3)
+ else:
+ split_weight_ws = split_weight[quad_indices]
+ split_weight_ws_02 = split_weight_ws[:, 0] * split_weight_ws[:, 2]
+ split_weight_ws_13 = split_weight_ws[:, 1] * split_weight_ws[:, 3]
+ mesh_triangles = torch.where(
+ split_weight_ws_02 > split_weight_ws_13,
+ quad_indices[:, flexible_dual_grid_to_mesh.quad_split_1],
+ quad_indices[:, flexible_dual_grid_to_mesh.quad_split_2]
+ ).reshape(-1, 3)
+ else:
+ assert split_weight is not None, "split_weight must be provided in training mode"
+ mesh_vertices = (coords.float() + dual_vertices) * voxel_size + aabb[0].reshape(1, 3)
+ quad_vs = mesh_vertices[quad_indices]
+ mean_v02 = (quad_vs[:, 0] + quad_vs[:, 2]) / 2
+ mean_v13 = (quad_vs[:, 1] + quad_vs[:, 3]) / 2
+ split_weight_ws = split_weight[quad_indices]
+ split_weight_ws_02 = split_weight_ws[:, 0] * split_weight_ws[:, 2]
+ split_weight_ws_13 = split_weight_ws[:, 1] * split_weight_ws[:, 3]
+ mid_vertices = (
+ split_weight_ws_02 * mean_v02 +
+ split_weight_ws_13 * mean_v13
+ ) / (split_weight_ws_02 + split_weight_ws_13)
+ mesh_vertices = torch.cat([mesh_vertices, mid_vertices], dim=0)
+ quad_indices = torch.cat([quad_indices, torch.arange(N, N + L, device='cuda').unsqueeze(1)], dim=1)
+ mesh_triangles = quad_indices[:, flexible_dual_grid_to_mesh.quad_split_train].reshape(-1, 3)
+
+ return mesh_vertices, mesh_triangles
diff --git a/o-voxel/o_voxel/convert/volumetic_attr.py b/o-voxel/o_voxel/convert/volumetic_attr.py
new file mode 100644
index 0000000000000000000000000000000000000000..fe24bfe876f01cceb02bdb5859232fa95779b5c6
--- /dev/null
+++ b/o-voxel/o_voxel/convert/volumetic_attr.py
@@ -0,0 +1,583 @@
+from typing import *
+import io
+from PIL import Image
+import torch
+import numpy as np
+from tqdm import tqdm
+import trimesh
+import trimesh.visual
+
+from .. import _C
+
+__all__ = [
+ "textured_mesh_to_volumetric_attr",
+ "blender_dump_to_volumetric_attr"
+]
+
+
+ALPHA_MODE_ENUM = {
+ "OPAQUE": 0,
+ "MASK": 1,
+ "BLEND": 2,
+}
+
+
+def is_power_of_two(n: int) -> bool:
+ return n > 0 and (n & (n - 1)) == 0
+
+
+def nearest_power_of_two(n: int) -> int:
+ if n < 1:
+ raise ValueError("n must be >= 1")
+ if is_power_of_two(n):
+ return n
+ lower = 2 ** (n.bit_length() - 1)
+ upper = 2 ** n.bit_length()
+ if n - lower < upper - n:
+ return lower
+ else:
+ return upper
+
+
+def textured_mesh_to_volumetric_attr(
+ mesh: Union[trimesh.Scene, trimesh.Trimesh, str],
+ voxel_size: Union[float, list, tuple, np.ndarray, torch.Tensor] = None,
+ grid_size: Union[int, list, tuple, np.ndarray, torch.Tensor] = None,
+ aabb: Union[list, tuple, np.ndarray, torch.Tensor] = None,
+ mip_level_offset: float = 0.0,
+ verbose: bool = False,
+ timing: bool = False,
+) -> Union[torch.Tensor, Dict[str, torch.Tensor]]:
+ """
+ Voxelize a mesh into a sparse voxel grid with PBR properties.
+
+ Args:
+ mesh (trimesh.Scene, trimesh.Trimesh, str): The input mesh.
+ If a string is provided, it will be loaded as a mesh using trimesh.load().
+ voxel_size (float, list, tuple, np.ndarray, torch.Tensor): The size of each voxel.
+ grid_size (int, list, tuple, np.ndarray, torch.Tensor): The size of the grid.
+ NOTE: One of voxel_size and grid_size must be provided.
+ aabb (list, tuple, np.ndarray, torch.Tensor): The axis-aligned bounding box of the mesh.
+ If not provided, it will be computed automatically.
+ tile_size (int): The size of the tiles used for each individual voxelization.
+ mip_level_offset (float): The mip level offset for texture mip level selection.
+ verbose (bool): Whether to print the settings.
+ timing (bool): Whether to print the timing information.
+
+ Returns:
+ torch.Tensor: The indices of the voxels that are occupied by the mesh.
+ Dict[str, torch.Tensor]: A dictionary containing the following keys:
+ - "base_color": The base color of the occupied voxels.
+ - "metallic": The metallic value of the occupied voxels.
+ - "roughness": The roughness value of the occupied voxels.
+ - "emissive": The emissive value of the occupied voxels.
+ - "alpha": The alpha value of the occupied voxels.
+ - "normal": The normal of the occupied voxels.
+ """
+
+ # Load mesh
+ if isinstance(mesh, str):
+ mesh = trimesh.load(mesh)
+ if isinstance(mesh, trimesh.Scene):
+ groups = mesh.dump()
+ if isinstance(mesh, trimesh.Trimesh):
+ groups = [mesh]
+ scene = trimesh.Scene(groups)
+
+ # Voxelize settings
+ assert voxel_size is not None or grid_size is not None, "Either voxel_size or grid_size must be provided"
+
+ if voxel_size is not None:
+ if isinstance(voxel_size, float):
+ voxel_size = [voxel_size, voxel_size, voxel_size]
+ if isinstance(voxel_size, (list, tuple)):
+ voxel_size = np.array(voxel_size)
+ if isinstance(voxel_size, np.ndarray):
+ voxel_size = torch.tensor(voxel_size, dtype=torch.float32)
+ assert isinstance(voxel_size, torch.Tensor), f"voxel_size must be a float, list, tuple, np.ndarray, or torch.Tensor, but got {type(voxel_size)}"
+ assert voxel_size.dim() == 1, f"voxel_size must be a 1D tensor, but got {voxel_size.shape}"
+ assert voxel_size.size(0) == 3, f"voxel_size must have 3 elements, but got {voxel_size.size(0)}"
+
+ if grid_size is not None:
+ if isinstance(grid_size, int):
+ grid_size = [grid_size, grid_size, grid_size]
+ if isinstance(grid_size, (list, tuple)):
+ grid_size = np.array(grid_size)
+ if isinstance(grid_size, np.ndarray):
+ grid_size = torch.tensor(grid_size, dtype=torch.int32)
+ assert isinstance(grid_size, torch.Tensor), f"grid_size must be an int, list, tuple, np.ndarray, or torch.Tensor, but got {type(grid_size)}"
+ assert grid_size.dim() == 1, f"grid_size must be a 1D tensor, but got {grid_size.shape}"
+ assert grid_size.size(0) == 3, f"grid_size must have 3 elements, but got {grid_size.size(0)}"
+
+ if aabb is not None:
+ if isinstance(aabb, (list, tuple)):
+ aabb = np.array(aabb)
+ if isinstance(aabb, np.ndarray):
+ aabb = torch.tensor(aabb, dtype=torch.float32)
+ assert isinstance(aabb, torch.Tensor), f"aabb must be a list, tuple, np.ndarray, or torch.Tensor, but got {type(aabb)}"
+ assert aabb.dim() == 2, f"aabb must be a 2D tensor, but got {aabb.shape}"
+ assert aabb.size(0) == 2, f"aabb must have 2 rows, but got {aabb.size(0)}"
+ assert aabb.size(1) == 3, f"aabb must have 3 columns, but got {aabb.size(1)}"
+
+ # Auto adjust aabb
+ if aabb is None:
+ aabb = scene.bounds
+ min_xyz = aabb[0]
+ max_xyz = aabb[1]
+
+ if voxel_size is not None:
+ padding = torch.ceil((max_xyz - min_xyz) / voxel_size) * voxel_size - (max_xyz - min_xyz)
+ min_xyz -= padding * 0.5
+ max_xyz += padding * 0.5
+ if grid_size is not None:
+ padding = (max_xyz - min_xyz) / (grid_size - 1)
+ min_xyz -= padding * 0.5
+ max_xyz += padding * 0.5
+
+ aabb = torch.stack([min_xyz, max_xyz], dim=0).float()
+
+ # Fill voxel size or grid size
+ if voxel_size is None:
+ voxel_size = (aabb[1] - aabb[0]) / grid_size
+ if grid_size is None:
+ grid_size = ((aabb[1] - aabb[0]) / voxel_size).round().int()
+
+ grid_range = torch.stack([torch.zeros_like(grid_size), grid_size], dim=0).int()
+
+ # Print settings
+ if verbose:
+ print(f"Voxelize settings:")
+ print(f" Voxel size: {voxel_size}")
+ print(f" Grid size: {grid_size}")
+ print(f" AABB: {aabb}")
+
+ # Load Scene
+ scene_buffers = {
+ 'triangles': [],
+ 'normals': [],
+ 'uvs': [],
+ 'material_ids': [],
+ 'base_color_factor': [],
+ 'base_color_texture': [],
+ 'metallic_factor': [],
+ 'metallic_texture': [],
+ 'roughness_factor': [],
+ 'roughness_texture': [],
+ 'emissive_factor': [],
+ 'emissive_texture': [],
+ 'alpha_mode': [],
+ 'alpha_cutoff': [],
+ 'alpha_factor': [],
+ 'alpha_texture': [],
+ 'normal_texture': [],
+ }
+ for sid, (name, g) in tqdm(enumerate(scene.geometry.items()), total=len(scene.geometry), desc="Loading Scene", disable=not verbose):
+ if verbose:
+ print(f"Geometry: {name}")
+ print(f" Visual: {g.visual}")
+ print(f" Triangles: {g.triangles.shape[0]}")
+ print(f" Vertices: {g.vertices.shape[0]}")
+ print(f" Normals: {g.vertex_normals.shape[0]}")
+ if g.visual.material.baseColorFactor is not None:
+ print(f" Base color factor: {g.visual.material.baseColorFactor}")
+ if g.visual.material.baseColorTexture is not None:
+ print(f" Base color texture: {g.visual.material.baseColorTexture.size} {g.visual.material.baseColorTexture.mode}")
+ if g.visual.material.metallicFactor is not None:
+ print(f" Metallic factor: {g.visual.material.metallicFactor}")
+ if g.visual.material.roughnessFactor is not None:
+ print(f" Roughness factor: {g.visual.material.roughnessFactor}")
+ if g.visual.material.metallicRoughnessTexture is not None:
+ print(f" Metallic roughness texture: {g.visual.material.metallicRoughnessTexture.size} {g.visual.material.metallicRoughnessTexture.mode}")
+ if g.visual.material.emissiveFactor is not None:
+ print(f" Emissive factor: {g.visual.material.emissiveFactor}")
+ if g.visual.material.emissiveTexture is not None:
+ print(f" Emissive texture: {g.visual.material.emissiveTexture.size} {g.visual.material.emissiveTexture.mode}")
+ if g.visual.material.alphaMode is not None:
+ print(f" Alpha mode: {g.visual.material.alphaMode}")
+ if g.visual.material.alphaCutoff is not None:
+ print(f" Alpha cutoff: {g.visual.material.alphaCutoff}")
+ if g.visual.material.normalTexture is not None:
+ print(f" Normal texture: {g.visual.material.normalTexture.size} {g.visual.material.normalTexture.mode}")
+
+ assert isinstance(g, trimesh.Trimesh), f"Only trimesh.Trimesh is supported, but got {type(g)}"
+ assert isinstance(g.visual, trimesh.visual.TextureVisuals), f"Only trimesh.visual.TextureVisuals is supported, but got {type(g.visual)}"
+ assert isinstance(g.visual.material, trimesh.visual.material.PBRMaterial), f"Only trimesh.visual.material.PBRMaterial is supported, but got {type(g.visual.material)}"
+ triangles = torch.tensor(g.triangles, dtype=torch.float32) - aabb[0].reshape(1, 1, 3) # [N, 3, 3]
+ normals = torch.tensor(g.vertex_normals[g.faces], dtype=torch.float32) # [N, 3, 3]
+ uvs = torch.tensor(g.visual.uv[g.faces], dtype=torch.float32) if g.visual.uv is not None \
+ else torch.zeros(g.triangles.shape[0], 3, 2, dtype=torch.float32) # [N, 3, 2]
+ baseColorFactor = torch.tensor(g.visual.material.baseColorFactor / 255, dtype=torch.float32) if g.visual.material.baseColorFactor is not None \
+ else torch.ones(3, dtype=torch.float32) # [3]
+ baseColorTexture = torch.tensor(np.array(g.visual.material.baseColorTexture.convert('RGBA'))[..., :3], dtype=torch.uint8) if g.visual.material.baseColorTexture is not None \
+ else torch.tensor([]) # [H, W, 3]
+ metallicFactor = g.visual.material.metallicFactor if g.visual.material.metallicFactor is not None else 1.0
+ metallicTexture = torch.tensor(np.array(g.visual.material.metallicRoughnessTexture.convert('RGB'))[..., 2], dtype=torch.uint8) if g.visual.material.metallicRoughnessTexture is not None \
+ else torch.tensor([]) # [H, W]
+ roughnessFactor = g.visual.material.roughnessFactor if g.visual.material.roughnessFactor is not None else 1.0
+ roughnessTexture = torch.tensor(np.array(g.visual.material.metallicRoughnessTexture.convert('RGB'))[..., 1], dtype=torch.uint8) if g.visual.material.metallicRoughnessTexture is not None \
+ else torch.tensor([]) # [H, W]
+ emissiveFactor = torch.tensor(g.visual.material.emissiveFactor, dtype=torch.float32) if g.visual.material.emissiveFactor is not None \
+ else torch.zeros(3, dtype=torch.float32) # [3]
+ emissiveTexture = torch.tensor(np.array(g.visual.material.emissiveTexture.convert('RGB'))[..., :3], dtype=torch.uint8) if g.visual.material.emissiveTexture is not None \
+ else torch.tensor([]) # [H, W, 3]
+ alphaMode = ALPHA_MODE_ENUM[g.visual.material.alphaMode] if g.visual.material.alphaMode in ALPHA_MODE_ENUM else 0
+ alphaCutoff = g.visual.material.alphaCutoff if g.visual.material.alphaCutoff is not None else 0.5
+ alphaFactor = g.visual.material.baseColorFactor[3] / 255 if g.visual.material.baseColorFactor is not None else 1.0
+ alphaTexture = torch.tensor(np.array(g.visual.material.baseColorTexture.convert('RGBA'))[..., 3], dtype=torch.uint8) if g.visual.material.baseColorTexture is not None and alphaMode != 0 \
+ else torch.tensor([]) # [H, W]
+ normalTexture = torch.tensor(np.array(g.visual.material.normalTexture.convert('RGB'))[..., :3], dtype=torch.uint8) if g.visual.material.normalTexture is not None \
+ else torch.tensor([]) # [H, W, 3]
+
+ scene_buffers['triangles'].append(triangles)
+ scene_buffers['normals'].append(normals)
+ scene_buffers['uvs'].append(uvs)
+ scene_buffers['material_ids'].append(torch.full((triangles.shape[0],), sid, dtype=torch.int32))
+ scene_buffers['base_color_factor'].append(baseColorFactor)
+ scene_buffers['base_color_texture'].append(baseColorTexture)
+ scene_buffers['metallic_factor'].append(metallicFactor)
+ scene_buffers['metallic_texture'].append(metallicTexture)
+ scene_buffers['roughness_factor'].append(roughnessFactor)
+ scene_buffers['roughness_texture'].append(roughnessTexture)
+ scene_buffers['emissive_factor'].append(emissiveFactor)
+ scene_buffers['emissive_texture'].append(emissiveTexture)
+ scene_buffers['alpha_mode'].append(alphaMode)
+ scene_buffers['alpha_cutoff'].append(alphaCutoff)
+ scene_buffers['alpha_factor'].append(alphaFactor)
+ scene_buffers['alpha_texture'].append(alphaTexture)
+ scene_buffers['normal_texture'].append(normalTexture)
+
+ scene_buffers['triangles'] = torch.cat(scene_buffers['triangles'], dim=0) # [N, 3, 3]
+ scene_buffers['normals'] = torch.cat(scene_buffers['normals'], dim=0) # [N, 3, 3]
+ scene_buffers['uvs'] = torch.cat(scene_buffers['uvs'], dim=0) # [N, 3, 2]
+ scene_buffers['material_ids'] = torch.cat(scene_buffers['material_ids'], dim=0) # [N]
+
+ # Voxelize
+ out_tuple = _C.textured_mesh_to_volumetric_attr_cpu(
+ voxel_size,
+ grid_range,
+ scene_buffers["triangles"],
+ scene_buffers["normals"],
+ scene_buffers["uvs"],
+ scene_buffers["material_ids"],
+ scene_buffers["base_color_factor"],
+ scene_buffers["base_color_texture"],
+ [1] * len(scene_buffers["base_color_texture"]),
+ [0] * len(scene_buffers["base_color_texture"]),
+ scene_buffers["metallic_factor"],
+ scene_buffers["metallic_texture"],
+ [1] * len(scene_buffers["metallic_texture"]),
+ [0] * len(scene_buffers["metallic_texture"]),
+ scene_buffers["roughness_factor"],
+ scene_buffers["roughness_texture"],
+ [1] * len(scene_buffers["roughness_texture"]),
+ [0] * len(scene_buffers["roughness_texture"]),
+ scene_buffers["emissive_factor"],
+ scene_buffers["emissive_texture"],
+ [1] * len(scene_buffers["emissive_texture"]),
+ [0] * len(scene_buffers["emissive_texture"]),
+ scene_buffers["alpha_mode"],
+ scene_buffers["alpha_cutoff"],
+ scene_buffers["alpha_factor"],
+ scene_buffers["alpha_texture"],
+ [1] * len(scene_buffers["alpha_texture"]),
+ [0] * len(scene_buffers["alpha_texture"]),
+ scene_buffers["normal_texture"],
+ [1] * len(scene_buffers["normal_texture"]),
+ [0] * len(scene_buffers["normal_texture"]),
+ mip_level_offset,
+ timing,
+ )
+
+ # Post process
+ coord = out_tuple[0]
+ attr = {
+ "base_color": torch.clamp(out_tuple[1] * 255, 0, 255).byte().reshape(-1, 3),
+ "metallic": torch.clamp(out_tuple[2] * 255, 0, 255).byte().reshape(-1, 1),
+ "roughness": torch.clamp(out_tuple[3] * 255, 0, 255).byte().reshape(-1, 1),
+ "emissive": torch.clamp(out_tuple[4] * 255, 0, 255).byte().reshape(-1, 3),
+ "alpha": torch.clamp(out_tuple[5] * 255, 0, 255).byte().reshape(-1, 1),
+ "normal": torch.clamp((out_tuple[6] * 0.5 + 0.5) * 255, 0, 255).byte().reshape(-1, 3),
+ }
+
+ return coord, attr
+
+
+def blender_dump_to_volumetric_attr(
+ dump: Dict[str, Any],
+ voxel_size: Union[float, list, tuple, np.ndarray, torch.Tensor] = None,
+ grid_size: Union[int, list, tuple, np.ndarray, torch.Tensor] = None,
+ aabb: Union[list, tuple, np.ndarray, torch.Tensor] = None,
+ mip_level_offset: float = 0.0,
+ verbose: bool = False,
+ timing: bool = False,
+) -> Union[torch.Tensor, Dict[str, torch.Tensor]]:
+ """
+ Voxelize a mesh into a sparse voxel grid with PBR properties.
+
+ Args:
+ dump (Dict[str, Any]): Dumped data from a blender scene.
+ voxel_size (float, list, tuple, np.ndarray, torch.Tensor): The size of each voxel.
+ grid_size (int, list, tuple, np.ndarray, torch.Tensor): The size of the grid.
+ NOTE: One of voxel_size and grid_size must be provided.
+ aabb (list, tuple, np.ndarray, torch.Tensor): The axis-aligned bounding box of the mesh.
+ If not provided, it will be computed automatically.
+ mip_level_offset (float): The mip level offset for texture mip level selection.
+ verbose (bool): Whether to print the settings.
+ timing (bool): Whether to print the timing information.
+
+ Returns:
+ torch.Tensor: The indices of the voxels that are occupied by the mesh.
+ Dict[str, torch.Tensor]: A dictionary containing the following keys:
+ - "base_color": The base color of the occupied voxels.
+ - "metallic": The metallic value of the occupied voxels.
+ - "roughness": The roughness value of the occupied voxels.
+ - "emissive": The emissive value of the occupied voxels.
+ - "alpha": The alpha value of the occupied voxels.
+ - "normal": The normal of the occupied voxels.
+ """
+ # Voxelize settings
+ assert voxel_size is not None or grid_size is not None, "Either voxel_size or grid_size must be provided"
+
+ if voxel_size is not None:
+ if isinstance(voxel_size, float):
+ voxel_size = [voxel_size, voxel_size, voxel_size]
+ if isinstance(voxel_size, (list, tuple)):
+ voxel_size = np.array(voxel_size)
+ if isinstance(voxel_size, np.ndarray):
+ voxel_size = torch.tensor(voxel_size, dtype=torch.float32)
+ assert isinstance(voxel_size, torch.Tensor), f"voxel_size must be a float, list, tuple, np.ndarray, or torch.Tensor, but got {type(voxel_size)}"
+ assert voxel_size.dim() == 1, f"voxel_size must be a 1D tensor, but got {voxel_size.shape}"
+ assert voxel_size.size(0) == 3, f"voxel_size must have 3 elements, but got {voxel_size.size(0)}"
+
+ if grid_size is not None:
+ if isinstance(grid_size, int):
+ grid_size = [grid_size, grid_size, grid_size]
+ if isinstance(grid_size, (list, tuple)):
+ grid_size = np.array(grid_size)
+ if isinstance(grid_size, np.ndarray):
+ grid_size = torch.tensor(grid_size, dtype=torch.int32)
+ assert isinstance(grid_size, torch.Tensor), f"grid_size must be an int, list, tuple, np.ndarray, or torch.Tensor, but got {type(grid_size)}"
+ assert grid_size.dim() == 1, f"grid_size must be a 1D tensor, but got {grid_size.shape}"
+ assert grid_size.size(0) == 3, f"grid_size must have 3 elements, but got {grid_size.size(0)}"
+
+ if aabb is not None:
+ if isinstance(aabb, (list, tuple)):
+ aabb = np.array(aabb)
+ if isinstance(aabb, np.ndarray):
+ aabb = torch.tensor(aabb, dtype=torch.float32)
+ assert isinstance(aabb, torch.Tensor), f"aabb must be a list, tuple, np.ndarray, or torch.Tensor, but got {type(aabb)}"
+ assert aabb.dim() == 2, f"aabb must be a 2D tensor, but got {aabb.shape}"
+ assert aabb.size(0) == 2, f"aabb must have 2 rows, but got {aabb.size(0)}"
+ assert aabb.size(1) == 3, f"aabb must have 3 columns, but got {aabb.size(1)}"
+
+ # Auto adjust aabb
+ if aabb is None:
+ min_xyz = np.min([
+ object['vertices'].min(axis=0)
+ for object in dump['objects']
+ ], axis=0)
+ max_xyz = np.max([
+ object['vertices'].max(axis=0)
+ for object in dump['objects']
+ ], axis=0)
+
+ if voxel_size is not None:
+ padding = torch.ceil((max_xyz - min_xyz) / voxel_size) * voxel_size - (max_xyz - min_xyz)
+ min_xyz -= padding * 0.5
+ max_xyz += padding * 0.5
+ if grid_size is not None:
+ padding = (max_xyz - min_xyz) / (grid_size - 1)
+ min_xyz -= padding * 0.5
+ max_xyz += padding * 0.5
+
+ aabb = torch.stack([min_xyz, max_xyz], dim=0).float()
+
+ # Fill voxel size or grid size
+ if voxel_size is None:
+ voxel_size = (aabb[1] - aabb[0]) / grid_size
+ if grid_size is None:
+ grid_size = ((aabb[1] - aabb[0]) / voxel_size).round().int()
+
+ grid_range = torch.stack([torch.zeros_like(grid_size), grid_size], dim=0).int()
+
+ # Print settings
+ if verbose:
+ print(f"Voxelize settings:")
+ print(f" Voxel size: {voxel_size}")
+ print(f" Grid size: {grid_size}")
+ print(f" AABB: {aabb}")
+
+ # Load Scene
+ scene_buffers = {
+ 'triangles': [],
+ 'normals': [],
+ 'uvs': [],
+ 'material_ids': [],
+ 'base_color_factor': [],
+ 'base_color_texture': [],
+ 'base_color_texture_filter': [],
+ 'base_color_texture_wrap': [],
+ 'metallic_factor': [],
+ 'metallic_texture': [],
+ 'metallic_texture_filter': [],
+ 'metallic_texture_wrap': [],
+ 'roughness_factor': [],
+ 'roughness_texture': [],
+ 'roughness_texture_filter': [],
+ 'roughness_texture_wrap': [],
+ 'alpha_mode': [],
+ 'alpha_cutoff': [],
+ 'alpha_factor': [],
+ 'alpha_texture': [],
+ 'alpha_texture_filter': [],
+ 'alpha_texture_wrap': [],
+ }
+
+ def load_texture(pack):
+ png_bytes = pack['image']
+ image = Image.open(io.BytesIO(png_bytes))
+ if image.width != image.height or not is_power_of_two(image.width):
+ size = nearest_power_of_two(max(image.width, image.height))
+ image = image.resize((size, size), Image.LANCZOS)
+ texture = torch.tensor(np.array(image), dtype=torch.uint8)
+ filter_mode = {
+ 'Linear': 1,
+ 'Closest': 0,
+ 'Cubic': 1,
+ 'Smart': 1,
+ }[pack['interpolation']]
+ wrap_mode = {
+ 'REPEAT': 0,
+ 'EXTEND': 1,
+ 'CLIP': 1,
+ 'MIRROR': 2,
+ }[pack['extension']]
+ return texture, filter_mode, wrap_mode
+
+ for material in dump['materials']:
+ baseColorFactor = torch.tensor(material['baseColorFactor'][:3], dtype=torch.float32)
+ if material['baseColorTexture'] is not None:
+ baseColorTexture, baseColorTextureFilter, baseColorTextureWrap = \
+ load_texture(material['baseColorTexture'])
+ assert baseColorTexture.shape[2] == 3, f"Base color texture must have 3 channels, but got {baseColorTexture.shape[2]}"
+ else:
+ baseColorTexture = torch.tensor([])
+ baseColorTextureFilter = 0
+ baseColorTextureWrap = 0
+ scene_buffers['base_color_factor'].append(baseColorFactor)
+ scene_buffers['base_color_texture'].append(baseColorTexture)
+ scene_buffers['base_color_texture_filter'].append(baseColorTextureFilter)
+ scene_buffers['base_color_texture_wrap'].append(baseColorTextureWrap)
+
+ metallicFactor = material['metallicFactor']
+ if material['metallicTexture'] is not None:
+ metallicTexture, metallicTextureFilter, metallicTextureWrap = \
+ load_texture(material['metallicTexture'])
+ assert metallicTexture.dim() == 2, f"Metallic roughness texture must have 2 dimensions, but got {metallicTexture.dim()}"
+ else:
+ metallicTexture = torch.tensor([])
+ metallicTextureFilter = 0
+ metallicTextureWrap = 0
+ scene_buffers['metallic_factor'].append(metallicFactor)
+ scene_buffers['metallic_texture'].append(metallicTexture)
+ scene_buffers['metallic_texture_filter'].append(metallicTextureFilter)
+ scene_buffers['metallic_texture_wrap'].append(metallicTextureWrap)
+
+ roughnessFactor = material['roughnessFactor']
+ if material['roughnessTexture'] is not None:
+ roughnessTexture, roughnessTextureFilter, roughnessTextureWrap = \
+ load_texture(material['roughnessTexture'])
+ assert roughnessTexture.dim() == 2, f"Metallic roughness texture must have 2 dimensions, but got {roughnessTexture.dim()}"
+ else:
+ roughnessTexture = torch.tensor([])
+ roughnessTextureFilter = 0
+ roughnessTextureWrap = 0
+ scene_buffers['roughness_factor'].append(roughnessFactor)
+ scene_buffers['roughness_texture'].append(roughnessTexture)
+ scene_buffers['roughness_texture_filter'].append(roughnessTextureFilter)
+ scene_buffers['roughness_texture_wrap'].append(roughnessTextureWrap)
+
+ alphaMode = ALPHA_MODE_ENUM[material['alphaMode']]
+ alphaCutoff = material['alphaCutoff']
+ alphaFactor = material['alphaFactor']
+ if material['alphaTexture'] is not None:
+ alphaTexture, alphaTextureFilter, alphaTextureWrap = \
+ load_texture(material['alphaTexture'])
+ assert alphaTexture.dim() == 2, f"Alpha texture must have 2 dimensions, but got {alphaTexture.dim()}"
+ else:
+ alphaTexture = torch.tensor([])
+ alphaTextureFilter = 0
+ alphaTextureWrap = 0
+ scene_buffers['alpha_mode'].append(alphaMode)
+ scene_buffers['alpha_cutoff'].append(alphaCutoff)
+ scene_buffers['alpha_factor'].append(alphaFactor)
+ scene_buffers['alpha_texture'].append(alphaTexture)
+ scene_buffers['alpha_texture_filter'].append(alphaTextureFilter)
+ scene_buffers['alpha_texture_wrap'].append(alphaTextureWrap)
+
+ for object in dump['objects']:
+ triangles = torch.tensor(object['vertices'][object['faces']], dtype=torch.float32).reshape(-1, 3, 3) - aabb[0].reshape(1, 1, 3)
+ normails = torch.tensor(object['normals'], dtype=torch.float32)
+ uvs = torch.tensor(object['uvs'], dtype=torch.float32) if object['uvs'] is not None else torch.zeros(triangles.shape[0], 3, 2, dtype=torch.float32)
+ material_id = torch.tensor(object['mat_ids'], dtype=torch.int32)
+ scene_buffers['triangles'].append(triangles)
+ scene_buffers['normals'].append(normails)
+ scene_buffers['uvs'].append(uvs)
+ scene_buffers['material_ids'].append(material_id)
+
+ scene_buffers['triangles'] = torch.cat(scene_buffers['triangles'], dim=0) # [N, 3, 3]
+ scene_buffers['normals'] = torch.cat(scene_buffers['normals'], dim=0) # [N, 3, 3]
+ scene_buffers['uvs'] = torch.cat(scene_buffers['uvs'], dim=0) # [N, 3, 2]
+ scene_buffers['material_ids'] = torch.cat(scene_buffers['material_ids'], dim=0) # [N]
+
+ scene_buffers['uvs'][:, :, 1] = 1 - scene_buffers['uvs'][:, :, 1] # Flip v coordinate
+
+ # Voxelize
+ out_tuple = _C.textured_mesh_to_volumetric_attr_cpu(
+ voxel_size,
+ grid_range,
+ scene_buffers["triangles"],
+ scene_buffers["normals"],
+ scene_buffers["uvs"],
+ scene_buffers["material_ids"],
+ scene_buffers["base_color_factor"],
+ scene_buffers["base_color_texture"],
+ scene_buffers["base_color_texture_filter"],
+ scene_buffers["base_color_texture_wrap"],
+ scene_buffers["metallic_factor"],
+ scene_buffers["metallic_texture"],
+ scene_buffers["metallic_texture_filter"],
+ scene_buffers["metallic_texture_wrap"],
+ scene_buffers["roughness_factor"],
+ scene_buffers["roughness_texture"],
+ scene_buffers["roughness_texture_filter"],
+ scene_buffers["roughness_texture_wrap"],
+ [torch.zeros(3, dtype=torch.float32) for _ in range(len(scene_buffers["base_color_texture"]))],
+ [torch.tensor([]) for _ in range(len(scene_buffers["base_color_texture"]))],
+ [0] * len(scene_buffers["base_color_texture"]),
+ [0] * len(scene_buffers["base_color_texture"]),
+ scene_buffers["alpha_mode"],
+ scene_buffers["alpha_cutoff"],
+ scene_buffers["alpha_factor"],
+ scene_buffers["alpha_texture"],
+ scene_buffers["alpha_texture_filter"],
+ scene_buffers["alpha_texture_wrap"],
+ [torch.tensor([]) for _ in range(len(scene_buffers["base_color_texture"]))],
+ [0] * len(scene_buffers["base_color_texture"]),
+ [0] * len(scene_buffers["base_color_texture"]),
+ mip_level_offset,
+ timing,
+ )
+
+ # Post process
+ coord = out_tuple[0]
+ attr = {
+ "base_color": torch.clamp(out_tuple[1] * 255, 0, 255).byte().reshape(-1, 3),
+ "metallic": torch.clamp(out_tuple[2] * 255, 0, 255).byte().reshape(-1, 1),
+ "roughness": torch.clamp(out_tuple[3] * 255, 0, 255).byte().reshape(-1, 1),
+ "emissive": torch.clamp(out_tuple[4] * 255, 0, 255).byte().reshape(-1, 3),
+ "alpha": torch.clamp(out_tuple[5] * 255, 0, 255).byte().reshape(-1, 1),
+ "normal": torch.clamp((out_tuple[6] * 0.5 + 0.5) * 255, 0, 255).byte().reshape(-1, 3),
+ }
+
+ return coord, attr
\ No newline at end of file
diff --git a/o-voxel/o_voxel/io/__init__.py b/o-voxel/o_voxel/io/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..db7eca220accb46c62dfac93f078c2938969866a
--- /dev/null
+++ b/o-voxel/o_voxel/io/__init__.py
@@ -0,0 +1,45 @@
+from typing import Dict, Union
+import torch
+from .ply import *
+from .npz import *
+from .vxz import *
+
+
+def read(file_path: str) -> Union[torch.Tensor, Dict[str, torch.Tensor]]:
+ """
+ Read a file containing voxels.
+
+ Args:
+ file_path: Path to the file.
+
+ Returns:
+ torch.Tensor: the coordinates of the voxels.
+ Dict[str, torch.Tensor]: the attributes of the voxels.
+ """
+ if file_path.endswith('.npz'):
+ return read_npz(file_path)
+ elif file_path.endswith('.ply'):
+ return read_ply(file_path)
+ elif file_path.endswith('.vxz'):
+ return read_vxz(file_path)
+ else:
+ raise ValueError(f"Unsupported file type {file_path}")
+
+
+def write(file_path: str, coord: torch.Tensor, attr: Dict[str, torch.Tensor], **kwargs):
+ """
+ Write a file containing voxels.
+
+ Args:
+ file_path: Path to the file.
+ coord: the coordinates of the voxels.
+ attr: the attributes of the voxels.
+ """
+ if file_path.endswith('.npz'):
+ write_npz(file_path, coord, attr, **kwargs)
+ elif file_path.endswith('.ply'):
+ write_ply(file_path, coord, attr, **kwargs)
+ elif file_path.endswith('.vxz'):
+ write_vxz(file_path, coord, attr, **kwargs)
+ else:
+ raise ValueError(f"Unsupported file type {file_path}")
diff --git a/o-voxel/o_voxel/io/npz.py b/o-voxel/o_voxel/io/npz.py
new file mode 100644
index 0000000000000000000000000000000000000000..17da9efe2b937540282cbaf25c385f19d0848be9
--- /dev/null
+++ b/o-voxel/o_voxel/io/npz.py
@@ -0,0 +1,43 @@
+from typing import *
+import torch
+import numpy as np
+
+
+__all__ = [
+ "read_npz",
+ "write_npz",
+]
+
+
+def read_npz(file) -> Union[torch.Tensor, Dict[str, torch.Tensor]]:
+ """
+ Read a NPZ file containing voxels.
+
+ Args:
+ file_path: Path or file object from which to read the NPZ file.
+
+ Returns:
+ torch.Tensor: the coordinates of the voxels.
+ Dict[str, torch.Tensor]: the attributes of the voxels.
+ """
+ data = np.load(file)
+ coord = torch.from_numpy(data['coord']).int()
+ attr = {k: torch.from_numpy(v) for k, v in data.items() if k!= 'coord'}
+ return coord, attr
+
+
+def write_npz(file, coord: torch.Tensor, attr: Dict[str, torch.Tensor], compress=True):
+ """
+ Write a NPZ file containing voxels.
+
+ Args:
+ file_path: Path or file object to which to write the NPZ file.
+ coord: the coordinates of the voxels.
+ attr: the attributes of the voxels.
+ """
+ data = {'coord': coord.cpu().numpy().astype(np.uint16)}
+ data.update({k: v.cpu().numpy() for k, v in attr.items()})
+ if compress:
+ np.savez_compressed(file, **data)
+ else:
+ np.savez(file, **data)
diff --git a/o-voxel/o_voxel/io/ply.py b/o-voxel/o_voxel/io/ply.py
new file mode 100644
index 0000000000000000000000000000000000000000..747693218fabfaca994e1f23707878c3cae7b4c9
--- /dev/null
+++ b/o-voxel/o_voxel/io/ply.py
@@ -0,0 +1,72 @@
+from typing import *
+import io
+import torch
+import numpy as np
+import plyfile
+
+
+__all__ = [
+ "read_ply",
+ "write_ply",
+]
+
+
+DTYPE_MAP = {
+ torch.uint8: 'u1',
+ torch.uint16: 'u2',
+ torch.uint32: 'u4',
+ torch.int8: 'i1',
+ torch.int16: 'i2',
+ torch.int32: 'i4',
+ torch.float32: 'f4',
+ torch.float64: 'f8'
+}
+
+
+def read_ply(file) -> Union[torch.Tensor, Dict[str, torch.Tensor]]:
+ """
+ Read a PLY file containing voxels.
+
+ Args:
+ file: Path or file-like object of the PLY file.
+
+ Returns:
+ torch.Tensor: the coordinates of the voxels.
+ Dict[str, torch.Tensor]: the attributes of the voxels.
+ """
+ plydata = plyfile.PlyData.read(file)
+ xyz = np.stack([plydata.elements[0][k] for k in ['x', 'y', 'z']], axis=1)
+ coord = np.round(xyz).astype(int)
+ coord = torch.from_numpy(coord)
+
+ attr_keys = [k for k in plydata.elements[0].data.dtype.names if k not in ['x', 'y', 'z']]
+ attr_names = ['_'.join(k.split('_')[:-1]) for k in attr_keys]
+ attr_chs = [sum([1 for k in attr_keys if k.startswith(f'{name}_')]) for name in attr_names]
+
+ attr = {}
+ for i, name in enumerate(attr_names):
+ attr[name] = np.stack([plydata.elements[0][f'{name}_{j}'] for j in range(attr_chs[i])], axis=1)
+ attr = {k: torch.from_numpy(v) for k, v in attr.items()}
+
+ return coord, attr
+
+
+def write_ply(file, coord: torch.Tensor, attr: Dict[str, torch.Tensor]):
+ """
+ Write a PLY file containing voxels.
+
+ Args:
+ file: Path or file-like object of the PLY file.
+ coord: the coordinates of the voxels.
+ attr: the attributes of the voxels.
+ """
+ dtypes = [('x', 'f4'), ('y', 'f4'), ('z', 'f4')]
+ for k, v in attr.items():
+ for j in range(v.shape[-1]):
+ assert v.dtype in DTYPE_MAP, f"Unsupported data type {v.dtype} for attribute {k}"
+ dtypes.append((f'{k}_{j}', DTYPE_MAP[v.dtype]))
+ data = np.empty(len(coord), dtype=dtypes)
+ all_chs = np.concatenate([coord.cpu().numpy().astype(np.float32)] + [v.cpu().numpy() for v in attr.values()], axis=1)
+ data[:] = list(map(tuple, all_chs))
+ plyfile.PlyData([plyfile.PlyElement.describe(data, 'vertex')]).write(file)
+
\ No newline at end of file
diff --git a/o-voxel/o_voxel/io/vxz.py b/o-voxel/o_voxel/io/vxz.py
new file mode 100644
index 0000000000000000000000000000000000000000..91fba74d26d2edaabbcd7edb707d272cbca25b8a
--- /dev/null
+++ b/o-voxel/o_voxel/io/vxz.py
@@ -0,0 +1,365 @@
+from typing import *
+import os
+import json
+import struct
+import torch
+import numpy as np
+import zlib
+import lzma
+import zstandard
+from concurrent.futures import ThreadPoolExecutor
+from ..serialize import encode_seq, decode_seq
+from .. import _C
+
+
+__all__ = [
+ "read_vxz",
+ "read_vxz_info",
+ "write_vxz",
+]
+
+
+"""
+VXZ format
+
+Header:
+- file type (3 bytes) - 'VXZ'
+- version (1 byte) - 0
+- binary start offset (4 bytes)
+- structure (json) -
+{
+ "num_voxel": int,
+ "chunk_size": int,
+ "filter": str,
+ "compression": str,
+ "compression_level": int,
+ "raw_size": int,
+ "compressed_size": int,
+ "compress_ratio": float,
+ "attr_interleave": str,
+ "attr": [
+ {"name": str, "chs": int},
+ ...
+ ]
+ "chunks": [
+ {
+ "ptr": [offset, length], # offset from global binary start
+ "svo": [offset, length], # offset from this chunk start
+ "attr": [offset, length], # offset from this chunk start
+ },
+ ...
+ ]
+}
+- binary data
+"""
+
+DEFAULT_COMPRESION_LEVEL = {
+ 'none': 0,
+ 'deflate': 9,
+ 'lzma': 9,
+ 'zstd': 22,
+}
+
+
+def _compress(data: bytes, algo: Literal['none', 'deflate', 'lzma', 'zstd'], level: int) -> bytes:
+ if algo == 'none':
+ return data
+ if level is None:
+ level = DEFAULT_COMPRESION_LEVEL[algo]
+ if algo == 'deflate':
+ compresser = zlib.compressobj(level, wbits=-15)
+ return compresser.compress(data) + compresser.flush()
+ if algo == 'lzma':
+ compresser = lzma.LZMACompressor(format=lzma.FORMAT_RAW, filters=[{'id': lzma.FILTER_LZMA2, 'preset': level}])
+ return compresser.compress(data) + compresser.flush()
+ if algo == 'zstd':
+ compresser = zstandard.ZstdCompressor(level=level, write_checksum=False, write_content_size=True, threads=-1)
+ return compresser.compress(data)
+ raise ValueError(f"Invalid compression algorithm: {algo}")
+
+
+def _decompress(data: bytes, algo: Literal['none', 'deflate', 'lzma', 'zstd'], level: int) -> bytes:
+ if algo == 'none':
+ return data
+ if level is None:
+ level = DEFAULT_COMPRESION_LEVEL[algo]
+ if algo == 'deflate':
+ decompresser = zlib.decompressobj(wbits=-15)
+ return decompresser.decompress(data) + decompresser.flush()
+ if algo == 'lzma':
+ decompresser = lzma.LZMADecompressor(format=lzma.FORMAT_RAW, filters=[{'id': lzma.FILTER_LZMA2, 'preset': level}])
+ return decompresser.decompress(data)
+ if algo == 'zstd':
+ decompresser = zstandard.ZstdDecompressor(format=zstandard.FORMAT_ZSTD1)
+ return decompresser.decompress(data)
+ raise ValueError(f"Invalid compression algorithm: {algo}")
+
+
+def read_vxz_info(file) -> Dict:
+ """
+ Read the header of a VXZ file without decompressing the binary data.
+
+ Args:
+ file_path: Path or file-like object to the VXZ file.
+
+ Returns:
+ Dict: the header of the VXZ file.
+ """
+ if isinstance(file, str):
+ with open(file, 'rb') as f:
+ file_data = f.read()
+ else:
+ file_data = file.read()
+
+ assert file_data[:3] == b'VXZ', "Invalid file type"
+ version = file_data[3]
+ assert version == 0, "Invalid file version"
+
+ bin_start = struct.unpack('>I', file_data[4:8])[0]
+ structure_data = json.loads(file_data[8:bin_start].decode())
+ return structure_data
+
+
+def read_vxz(file, num_threads: int = -1) -> Union[torch.Tensor, Dict[str, torch.Tensor]]:
+ """
+ Read a VXZ file containing voxels.
+
+ Args:
+ file_path: Path or file-like object to the VXZ file.
+ num_threads: the number of threads to use for reading the file.
+
+ Returns:
+ torch.Tensor: the coordinates of the voxels.
+ Dict[str, torch.Tensor]: the attributes of the voxels.
+ """
+ if isinstance(file, str):
+ with open(file, 'rb') as f:
+ file_data = f.read()
+ else:
+ file_data = file.read()
+
+ num_threads = num_threads if num_threads > 0 else os.cpu_count()
+
+ # Parse header
+ assert file_data[:3] == b'VXZ', "Invalid file type"
+ version = file_data[3]
+ assert version == 0, "Invalid file version"
+
+ bin_start = struct.unpack('>I', file_data[4:8])[0]
+ structure_data = json.loads(file_data[8:bin_start].decode())
+ bin_data = file_data[bin_start:]
+
+ # Decode chunks
+ chunk_size = structure_data['chunk_size']
+ chunk_depth = np.log2(chunk_size)
+ assert chunk_depth.is_integer(), f"Chunk size must be a power of 2, got {chunk_size}"
+ chunk_depth = int(chunk_depth)
+
+ def worker(chunk_info):
+ decompressed = {}
+ chunk_data = bin_data[chunk_info['ptr'][0]:chunk_info['ptr'][0]+chunk_info['ptr'][1]]
+ for k, v in chunk_info.items():
+ if k in ['ptr', 'idx']:
+ continue
+ decompressed[k] = np.frombuffer(_decompress(chunk_data[v[0]:v[0]+v[1]], structure_data['compression'], structure_data['compression_level']), dtype=np.uint8)
+ svo = torch.tensor(np.frombuffer(decompressed['svo'], dtype=np.uint8))
+ morton_code = _C.decode_sparse_voxel_octree_cpu(svo, chunk_depth)
+ coord = decode_seq(morton_code.int()).cpu()
+
+ # deinterleave attributes
+ if structure_data['attr_interleave'] == 'none':
+ all_attr = []
+ for k, chs in structure_data['attr']:
+ for i in range(chs):
+ all_attr.append(torch.tensor(decompressed[f'{k}_{i}']))
+ all_attr = torch.stack(all_attr, dim=1)
+ elif structure_data['attr_interleave'] == 'as_is':
+ all_attr = []
+ for k, chs in structure_data['attr']:
+ all_attr.append(torch.tensor(decompressed[k].reshape(-1, chs)))
+ all_attr = torch.cat(all_attr, dim=1)
+ elif structure_data['attr_interleave'] == 'all':
+ all_chs = sum(chs for k, chs in structure_data['attr'])
+ all_attr = decompressed['attr'].reshape(-1, all_chs)
+
+ # unfilter
+ if structure_data['filter'] == 'none':
+ pass
+ elif structure_data['filter'] == 'parent':
+ all_attr = _C.decode_sparse_voxel_octree_attr_parent_cpu(svo, chunk_depth, all_attr)
+ elif structure_data['filter'] == 'neighbor':
+ all_attr = _C.decode_sparse_voxel_octree_attr_neighbor_cpu(coord, chunk_size, all_attr)
+
+ # final
+ attr = {}
+ ch = 0
+ for k, chs in structure_data['attr']:
+ attr[k] = all_attr[:, ch:ch+chs]
+ ch += chs
+ return {
+ 'coord': coord,
+ 'attr': attr,
+ }
+
+ if num_threads == 1:
+ chunks = [worker(info) for info in structure_data['chunks']]
+ else:
+ with ThreadPoolExecutor(max_workers=num_threads) as executor:
+ chunks = list(executor.map(worker, structure_data['chunks']))
+
+ # Combine chunks
+ coord = []
+ attr = {k: [] for k, _ in structure_data['attr']}
+ for info, chunk in zip(structure_data['chunks'], chunks):
+ coord.append(chunk['coord'] + torch.tensor([[info['idx'][0] * chunk_size, info['idx'][1] * chunk_size, info['idx'][2] * chunk_size]]).int())
+ for k, v in chunk['attr'].items():
+ attr[k].append(v)
+ coord = torch.cat(coord, dim=0)
+ for k, v in attr.items():
+ attr[k] = torch.cat(v, dim=0)
+ return coord, attr
+
+
+def write_vxz(
+ file,
+ coord: torch.Tensor,
+ attr: Dict[str, torch.Tensor],
+ chunk_size: int = 256,
+ filter: Literal['none', 'parent', 'neighbor'] = 'none',
+ compression: Literal['none', 'deflate', 'lzma', 'zstd'] = 'lzma',
+ compression_level: Optional[int] = None,
+ attr_interleave: Literal['none', 'as_is', 'all'] = 'as_is',
+ num_threads: int = -1,
+):
+ """
+ Write a VXZ file containing voxels.
+
+ Args:
+ file: Path or file-like object to the VXZ file.
+ coord: the coordinates of the voxels.
+ attr: the attributes of the voxels.
+ chunk_size: the size of each chunk.
+ filter: the filter to apply to the voxels.
+ compression: the compression algorithm to use.
+ compression_level: the level of compression.
+ attr_interleave: how to interleave the attributes.
+ num_threads: the number of threads to use for compression.
+ """
+ # Check
+ for k, v in attr.items():
+ assert coord.shape[0] == v.shape[0], f"Number of coordinates and attributes do not match for key {k}"
+ assert v.dtype == torch.uint8, f"Attributes must be uint8, got {v.dtype} for key {k}"
+ assert attr_interleave in ['none', 'as_is', 'all'], f"Invalid attr_interleave value: {attr_interleave}"
+
+ compression_level = compression_level or DEFAULT_COMPRESION_LEVEL[compression]
+ num_threads = num_threads if num_threads > 0 else os.cpu_count()
+
+ file_info = {
+ 'num_voxel': coord.shape[0],
+ 'chunk_size': chunk_size,
+ 'filter': filter,
+ 'compression': compression,
+ 'compression_level': compression_level,
+ 'raw_size': sum([coord.numel() * 4] + [v.numel() for v in attr.values()]),
+ 'compressed_size': 0,
+ 'compress_ratio': 0.0,
+ 'attr_interleave': attr_interleave,
+ 'attr': [[k, v.shape[1]] for k, v in attr.items()],
+ 'chunks': [],
+ }
+ bin_data = b''
+
+ # Split into chunks
+ chunk_depth = np.log2(chunk_size)
+ assert chunk_depth.is_integer(), f"Chunk size must be a power of 2, got {chunk_size}"
+ chunk_depth = int(chunk_depth)
+
+ chunk_coord = coord // chunk_size
+ coord = coord % chunk_size
+ unique_chunk_coord, inverse = torch.unique(chunk_coord, dim=0, return_inverse=True)
+
+ chunks = []
+ for idx, chunk_xyz in enumerate(unique_chunk_coord.tolist()):
+ chunk_mask = (inverse == idx)
+ chunks.append({
+ 'idx': chunk_xyz,
+ 'coord': coord[chunk_mask],
+ 'attr': {k: v[chunk_mask] for k, v in attr.items()},
+ })
+
+ # Compress each chunk
+ with ThreadPoolExecutor(max_workers=num_threads) as executor:
+ def worker(chunk):
+ ## compress to binary
+ coord = chunk['coord']
+ morton_code = encode_seq(coord)
+ sorted_idx = morton_code.argsort().cpu()
+ coord = coord.cpu()[sorted_idx]
+ morton_code = morton_code.cpu()[sorted_idx]
+ attr = torch.cat([v.cpu()[sorted_idx] for v in chunk['attr'].values()], dim=1)
+ svo = _C.encode_sparse_voxel_octree_cpu(morton_code, chunk_depth)
+ svo_bytes = _compress(svo.numpy().tobytes(), compression, compression_level)
+
+ # filter
+ if filter == 'none':
+ attr = attr.numpy()
+ elif filter == 'parent':
+ attr = _C.encode_sparse_voxel_octree_attr_parent_cpu(svo, chunk_depth, attr).numpy()
+ elif filter == 'neighbor':
+ attr = _C.encode_sparse_voxel_octree_attr_neighbor_cpu(coord, chunk_size, attr).numpy()
+
+ # interleave attributes
+ attr_bytes = {}
+ if attr_interleave == 'none':
+ ch = 0
+ for k, chs in file_info['attr']:
+ for i in range(chs):
+ attr_bytes[f'{k}_{i}'] = _compress(attr[:, ch].tobytes(), compression, compression_level)
+ ch += 1
+ elif attr_interleave == 'as_is':
+ ch = 0
+ for k, chs in file_info['attr']:
+ attr_bytes[k] = _compress(attr[:, ch:ch+chs].tobytes(), compression, compression_level)
+ ch += chs
+ elif attr_interleave == 'all':
+ attr_bytes['attr'] = _compress(attr.tobytes(), compression, compression_level)
+
+ ## buffer for each chunk
+ chunk_info = {'idx': chunk['idx']}
+ bin_data = b''
+
+ ### svo
+ chunk_info['svo'] = [len(bin_data), len(svo_bytes)]
+ bin_data += svo_bytes
+
+ ### attr
+ for k, v in attr_bytes.items():
+ chunk_info[k] = [len(bin_data), len(v)]
+ bin_data += v
+
+ return chunk_info, bin_data
+
+ chunks = list(executor.map(worker, chunks))
+
+ for chunk_info, chunk_data in chunks:
+ chunk_info['ptr'] = [len(bin_data), len(chunk_data)]
+ bin_data += chunk_data
+ file_info['chunks'].append(chunk_info)
+
+ file_info['compressed_size'] = len(bin_data)
+ file_info['compress_ratio'] = file_info['raw_size'] / file_info['compressed_size']
+
+ # File parts
+ structure_data = json.dumps(file_info).encode()
+ header = b'VXZ\x00' + struct.pack('>I', len(structure_data) + 8)
+
+ # Write to file
+ if isinstance(file, str):
+ with open(file, 'wb') as f:
+ f.write(header)
+ f.write(structure_data)
+ f.write(bin_data)
+ else:
+ file.write(header)
+ file.write(structure_data)
+ file.write(bin_data)
diff --git a/o-voxel/o_voxel/postprocess.py b/o-voxel/o_voxel/postprocess.py
new file mode 100644
index 0000000000000000000000000000000000000000..217155953d09ffd5393a1756051983e7013e62fb
--- /dev/null
+++ b/o-voxel/o_voxel/postprocess.py
@@ -0,0 +1,331 @@
+from typing import *
+from tqdm import tqdm
+import numpy as np
+import torch
+import cv2
+from PIL import Image
+import trimesh
+import trimesh.visual
+from flex_gemm.ops.grid_sample import grid_sample_3d
+import nvdiffrast.torch as dr
+import cumesh
+
+
+def to_glb(
+ vertices: torch.Tensor,
+ faces: torch.Tensor,
+ attr_volume: torch.Tensor,
+ coords: torch.Tensor,
+ attr_layout: Dict[str, slice],
+ aabb: Union[list, tuple, np.ndarray, torch.Tensor],
+ voxel_size: Union[float, list, tuple, np.ndarray, torch.Tensor] = None,
+ grid_size: Union[int, list, tuple, np.ndarray, torch.Tensor] = None,
+ decimation_target: int = 1000000,
+ texture_size: int = 2048,
+ remesh: bool = False,
+ remesh_band: float = 1,
+ remesh_project: float = 0.9,
+ mesh_cluster_threshold_cone_half_angle_rad=np.radians(90.0),
+ mesh_cluster_refine_iterations=0,
+ mesh_cluster_global_iterations=1,
+ mesh_cluster_smooth_strength=1,
+ verbose: bool = False,
+ use_tqdm: bool = False,
+):
+ """
+ Convert an extracted mesh to a GLB file.
+ Performs cleaning, optional remeshing, UV unwrapping, and texture baking from a volume.
+
+ Args:
+ vertices: (N, 3) tensor of vertex positions
+ faces: (M, 3) tensor of vertex indices
+ attr_volume: (L, C) features of a sprase tensor for attribute interpolation
+ coords: (L, 3) tensor of coordinates for each voxel
+ attr_layout: dictionary of slice objects for each attribute
+ aabb: (2, 3) tensor of minimum and maximum coordinates of the volume
+ voxel_size: (3,) tensor of size of each voxel
+ grid_size: (3,) tensor of number of voxels in each dimension
+ decimation_target: target number of vertices for mesh simplification
+ texture_size: size of the texture for baking
+ remesh: whether to perform remeshing
+ remesh_band: size of the remeshing band
+ remesh_project: projection factor for remeshing
+ mesh_cluster_threshold_cone_half_angle_rad: threshold for cone-based clustering in uv unwrapping
+ mesh_cluster_refine_iterations: number of iterations for refining clusters in uv unwrapping
+ mesh_cluster_global_iterations: number of global iterations for clustering in uv unwrapping
+ mesh_cluster_smooth_strength: strength of smoothing for clustering in uv unwrapping
+ verbose: whether to print verbose messages
+ use_tqdm: whether to use tqdm to display progress bar
+ """
+ # --- Input Normalization (AABB, Voxel Size, Grid Size) ---
+ if isinstance(aabb, (list, tuple)):
+ aabb = np.array(aabb)
+ if isinstance(aabb, np.ndarray):
+ aabb = torch.tensor(aabb, dtype=torch.float32, device=coords.device)
+ assert isinstance(aabb, torch.Tensor), f"aabb must be a list, tuple, np.ndarray, or torch.Tensor, but got {type(aabb)}"
+ assert aabb.dim() == 2, f"aabb must be a 2D tensor, but got {aabb.shape}"
+ assert aabb.size(0) == 2, f"aabb must have 2 rows, but got {aabb.size(0)}"
+ assert aabb.size(1) == 3, f"aabb must have 3 columns, but got {aabb.size(1)}"
+
+ # Calculate grid dimensions based on AABB and voxel size
+ if voxel_size is not None:
+ if isinstance(voxel_size, float):
+ voxel_size = [voxel_size, voxel_size, voxel_size]
+ if isinstance(voxel_size, (list, tuple)):
+ voxel_size = np.array(voxel_size)
+ if isinstance(voxel_size, np.ndarray):
+ voxel_size = torch.tensor(voxel_size, dtype=torch.float32, device=coords.device)
+ grid_size = ((aabb[1] - aabb[0]) / voxel_size).round().int()
+ else:
+ assert grid_size is not None, "Either voxel_size or grid_size must be provided"
+ if isinstance(grid_size, int):
+ grid_size = [grid_size, grid_size, grid_size]
+ if isinstance(grid_size, (list, tuple)):
+ grid_size = np.array(grid_size)
+ if isinstance(grid_size, np.ndarray):
+ grid_size = torch.tensor(grid_size, dtype=torch.int32, device=coords.device)
+ voxel_size = (aabb[1] - aabb[0]) / grid_size
+
+ # Assertions for dimensions
+ assert isinstance(voxel_size, torch.Tensor)
+ assert voxel_size.dim() == 1 and voxel_size.size(0) == 3
+ assert isinstance(grid_size, torch.Tensor)
+ assert grid_size.dim() == 1 and grid_size.size(0) == 3
+
+ if use_tqdm:
+ pbar = tqdm(total=6, desc="Extracting GLB")
+ if verbose:
+ print(f"Original mesh: {vertices.shape[0]} vertices, {faces.shape[0]} faces")
+
+ # Move data to GPU
+ vertices = vertices.cuda()
+ faces = faces.cuda()
+
+ # Initialize CUDA mesh handler
+ mesh = cumesh.CuMesh()
+ mesh.init(vertices, faces)
+
+ # --- Initial Mesh Cleaning ---
+ # Fills holes as much as we can before processing
+ mesh.fill_holes(max_hole_perimeter=3e-2)
+ if verbose:
+ print(f"After filling holes: {mesh.num_vertices} vertices, {mesh.num_faces} faces")
+ vertices, faces = mesh.read()
+ if use_tqdm:
+ pbar.update(1)
+
+ # Build BVH for the current mesh to guide remeshing
+ if use_tqdm:
+ pbar.set_description("Building BVH")
+ if verbose:
+ print(f"Building BVH for current mesh...", end='', flush=True)
+ bvh = cumesh.cuBVH(vertices, faces)
+ if use_tqdm:
+ pbar.update(1)
+ if verbose:
+ print("Done")
+
+ if use_tqdm:
+ pbar.set_description("Cleaning mesh")
+ if verbose:
+ print("Cleaning mesh...")
+
+ # --- Branch 1: Standard Pipeline (Simplification & Cleaning) ---
+ if not remesh:
+ # Step 1: Aggressive simplification (3x target)
+ mesh.simplify(decimation_target * 3, verbose=verbose)
+ if verbose:
+ print(f"After inital simplification: {mesh.num_vertices} vertices, {mesh.num_faces} faces")
+
+ # Step 2: Clean up topology (duplicates, non-manifolds, isolated parts)
+ mesh.remove_duplicate_faces()
+ mesh.repair_non_manifold_edges()
+ mesh.remove_small_connected_components(1e-5)
+ mesh.fill_holes(max_hole_perimeter=3e-2)
+ if verbose:
+ print(f"After initial cleanup: {mesh.num_vertices} vertices, {mesh.num_faces} faces")
+
+ # Step 3: Final simplification to target count
+ mesh.simplify(decimation_target, verbose=verbose)
+ if verbose:
+ print(f"After final simplification: {mesh.num_vertices} vertices, {mesh.num_faces} faces")
+
+ # Step 4: Final Cleanup loop
+ mesh.remove_duplicate_faces()
+ mesh.repair_non_manifold_edges()
+ mesh.remove_small_connected_components(1e-5)
+ mesh.fill_holes(max_hole_perimeter=3e-2)
+ if verbose:
+ print(f"After final cleanup: {mesh.num_vertices} vertices, {mesh.num_faces} faces")
+
+ # Step 5: Unify face orientations
+ mesh.unify_face_orientations()
+
+ # --- Branch 2: Remeshing Pipeline ---
+ else:
+ center = aabb.mean(dim=0)
+ scale = (aabb[1] - aabb[0]).max().item()
+ resolution = grid_size.max().item()
+
+ # Perform Dual Contouring remeshing (rebuilds topology)
+ mesh.init(*cumesh.remeshing.remesh_narrow_band_dc(
+ vertices, faces,
+ center = center,
+ scale = (resolution + 3 * remesh_band) / resolution * scale,
+ resolution = resolution,
+ band = remesh_band,
+ project_back = remesh_project, # Snaps vertices back to original surface
+ verbose = verbose,
+ bvh = bvh,
+ ))
+ if verbose:
+ print(f"After remeshing: {mesh.num_vertices} vertices, {mesh.num_faces} faces")
+
+ # Simplify and clean the remeshed result (similar logic to above)
+ mesh.simplify(decimation_target, verbose=verbose)
+ if verbose:
+ print(f"After simplifying: {mesh.num_vertices} vertices, {mesh.num_faces} faces")
+
+ if use_tqdm:
+ pbar.update(1)
+ if verbose:
+ print("Done")
+
+
+ # --- UV Parameterization ---
+ if use_tqdm:
+ pbar.set_description("Parameterizing new mesh")
+ if verbose:
+ print("Parameterizing new mesh...")
+
+ out_vertices, out_faces, out_uvs, out_vmaps = mesh.uv_unwrap(
+ compute_charts_kwargs={
+ "threshold_cone_half_angle_rad": mesh_cluster_threshold_cone_half_angle_rad,
+ "refine_iterations": mesh_cluster_refine_iterations,
+ "global_iterations": mesh_cluster_global_iterations,
+ "smooth_strength": mesh_cluster_smooth_strength,
+ },
+ return_vmaps=True,
+ verbose=verbose,
+ )
+ out_vertices = out_vertices.cuda()
+ out_faces = out_faces.cuda()
+ out_uvs = out_uvs.cuda()
+ out_vmaps = out_vmaps.cuda()
+ mesh.compute_vertex_normals()
+ out_normals = mesh.read_vertex_normals()[out_vmaps]
+
+ if use_tqdm:
+ pbar.update(1)
+ if verbose:
+ print("Done")
+
+ # --- Texture Baking (Attribute Sampling) ---
+ if use_tqdm:
+ pbar.set_description("Sampling attributes")
+ if verbose:
+ print("Sampling attributes...", end='', flush=True)
+
+ # Setup differentiable rasterizer context
+ ctx = dr.RasterizeCudaContext()
+ # Prepare UV coordinates for rasterization (rendering in UV space)
+ uvs_rast = torch.cat([out_uvs * 2 - 1, torch.zeros_like(out_uvs[:, :1]), torch.ones_like(out_uvs[:, :1])], dim=-1).unsqueeze(0)
+ rast = torch.zeros((1, texture_size, texture_size, 4), device='cuda', dtype=torch.float32)
+
+ # Rasterize in chunks to save memory
+ for i in range(0, out_faces.shape[0], 100000):
+ rast_chunk, _ = dr.rasterize(
+ ctx, uvs_rast, out_faces[i:i+100000],
+ resolution=[texture_size, texture_size],
+ )
+ mask_chunk = rast_chunk[..., 3:4] > 0
+ rast_chunk[..., 3:4] += i # Store face ID in alpha channel
+ rast = torch.where(mask_chunk, rast_chunk, rast)
+
+ # Mask of valid pixels in texture
+ mask = rast[0, ..., 3] > 0
+
+ # Interpolate 3D positions in UV space (finding 3D coord for every texel)
+ pos = dr.interpolate(out_vertices.unsqueeze(0), rast, out_faces)[0][0]
+ valid_pos = pos[mask]
+
+ # Map these positions back to the *original* high-res mesh to get accurate attributes
+ # This corrects geometric errors introduced by simplification/remeshing
+ _, face_id, uvw = bvh.unsigned_distance(valid_pos, return_uvw=True)
+ orig_tri_verts = vertices[faces[face_id.long()]] # (N_new, 3, 3)
+ valid_pos = (orig_tri_verts * uvw.unsqueeze(-1)).sum(dim=1)
+
+ # Trilinear sampling from the attribute volume (Color, Material props)
+ attrs = torch.zeros(texture_size, texture_size, attr_volume.shape[1], device='cuda')
+ attrs[mask] = grid_sample_3d(
+ attr_volume,
+ torch.cat([torch.zeros_like(coords[:, :1]), coords], dim=-1),
+ shape=torch.Size([1, attr_volume.shape[1], *grid_size.tolist()]),
+ grid=((valid_pos - aabb[0]) / voxel_size).reshape(1, -1, 3),
+ mode='trilinear',
+ )
+ if use_tqdm:
+ pbar.update(1)
+ if verbose:
+ print("Done")
+
+ # --- Texture Post-Processing & Material Construction ---
+ if use_tqdm:
+ pbar.set_description("Finalizing mesh")
+ if verbose:
+ print("Finalizing mesh...", end='', flush=True)
+
+ mask = mask.cpu().numpy()
+
+ # Extract channels based on layout (BaseColor, Metallic, Roughness, Alpha)
+ base_color = np.clip(attrs[..., attr_layout['base_color']].cpu().numpy() * 255, 0, 255).astype(np.uint8)
+ metallic = np.clip(attrs[..., attr_layout['metallic']].cpu().numpy() * 255, 0, 255).astype(np.uint8)
+ roughness = np.clip(attrs[..., attr_layout['roughness']].cpu().numpy() * 255, 0, 255).astype(np.uint8)
+ alpha = np.clip(attrs[..., attr_layout['alpha']].cpu().numpy() * 255, 0, 255).astype(np.uint8)
+ alpha_mode = 'OPAQUE'
+
+ # Inpainting: fill gaps (dilation) to prevent black seams at UV boundaries
+ mask_inv = (~mask).astype(np.uint8)
+ base_color = cv2.inpaint(base_color, mask_inv, 3, cv2.INPAINT_TELEA)
+ metallic = cv2.inpaint(metallic, mask_inv, 1, cv2.INPAINT_TELEA)[..., None]
+ roughness = cv2.inpaint(roughness, mask_inv, 1, cv2.INPAINT_TELEA)[..., None]
+ alpha = cv2.inpaint(alpha, mask_inv, 1, cv2.INPAINT_TELEA)[..., None]
+
+ # Create PBR material
+ # Standard PBR packs Metallic and Roughness into Blue and Green channels
+ material = trimesh.visual.material.PBRMaterial(
+ baseColorTexture=Image.fromarray(np.concatenate([base_color, alpha], axis=-1)),
+ baseColorFactor=np.array([255, 255, 255, 255], dtype=np.uint8),
+ metallicRoughnessTexture=Image.fromarray(np.concatenate([np.zeros_like(metallic), roughness, metallic], axis=-1)),
+ metallicFactor=1.0,
+ roughnessFactor=1.0,
+ alphaMode=alpha_mode,
+ doubleSided=True if not remesh else False,
+ )
+
+ # --- Coordinate System Conversion & Final Object ---
+ vertices_np = out_vertices.cpu().numpy()
+ faces_np = out_faces.cpu().numpy()
+ uvs_np = out_uvs.cpu().numpy()
+ normals_np = out_normals.cpu().numpy()
+
+ # Swap Y and Z axes, invert Y (common conversion for GLB compatibility)
+ vertices_np[:, 1], vertices_np[:, 2] = vertices_np[:, 2], -vertices_np[:, 1]
+ normals_np[:, 1], normals_np[:, 2] = normals_np[:, 2], -normals_np[:, 1]
+ uvs_np[:, 1] = 1 - uvs_np[:, 1] # Flip UV V-coordinate
+
+ textured_mesh = trimesh.Trimesh(
+ vertices=vertices_np,
+ faces=faces_np,
+ vertex_normals=normals_np,
+ process=False,
+ visual=trimesh.visual.TextureVisuals(uv=uvs_np, material=material)
+ )
+
+ if use_tqdm:
+ pbar.update(1)
+ pbar.close()
+ if verbose:
+ print("Done")
+
+ return textured_mesh
\ No newline at end of file
diff --git a/o-voxel/o_voxel/rasterize.py b/o-voxel/o_voxel/rasterize.py
new file mode 100644
index 0000000000000000000000000000000000000000..63ae53b61e0cb501eb274b342bc5d337adfabfee
--- /dev/null
+++ b/o-voxel/o_voxel/rasterize.py
@@ -0,0 +1,111 @@
+import torch
+import torch.nn.functional as F
+from easydict import EasyDict as edict
+from . import _C
+
+
+def intrinsics_to_projection(
+ intrinsics: torch.Tensor,
+ near: float,
+ far: float,
+ ) -> torch.Tensor:
+ """
+ OpenCV intrinsics to OpenGL perspective matrix
+
+ Args:
+ intrinsics (torch.Tensor): [3, 3] OpenCV intrinsics matrix
+ near (float): near plane to clip
+ far (float): far plane to clip
+ Returns:
+ (torch.Tensor): [4, 4] OpenGL perspective matrix
+ """
+ fx, fy = intrinsics[0, 0], intrinsics[1, 1]
+ cx, cy = intrinsics[0, 2], intrinsics[1, 2]
+ ret = torch.zeros((4, 4), dtype=intrinsics.dtype, device=intrinsics.device)
+ ret[0, 0] = 2 * fx
+ ret[1, 1] = 2 * fy
+ ret[0, 2] = 2 * cx - 1
+ ret[1, 2] = - 2 * cy + 1
+ ret[2, 2] = far / (far - near)
+ ret[2, 3] = near * far / (near - far)
+ ret[3, 2] = 1.
+ return ret
+
+
+class VoxelRenderer:
+ """
+ Renderer for the Voxel representation.
+
+ Args:
+ rendering_options (dict): Rendering options.
+ """
+
+ def __init__(self, rendering_options={}) -> None:
+ self.rendering_options = edict({
+ "resolution": None,
+ "near": 0.1,
+ "far": 10.0,
+ "ssaa": 1,
+ })
+ self.rendering_options.update(rendering_options)
+
+ def render(
+ self,
+ position: torch.Tensor,
+ attrs: torch.Tensor,
+ voxel_size: float,
+ extrinsics: torch.Tensor,
+ intrinsics: torch.Tensor,
+ ) -> edict:
+ """
+ Render the octree.
+
+ Args:
+ position (torch.Tensor): (N, 3) xyz positions
+ attrs (torch.Tensor): (N, C) attributes
+ voxel_size (float): voxel size
+ extrinsics (torch.Tensor): (4, 4) camera extrinsics
+ intrinsics (torch.Tensor): (3, 3) camera intrinsics
+
+ Returns:
+ edict containing:
+ attr (torch.Tensor): (C, H, W) rendered color
+ depth (torch.Tensor): (H, W) rendered depth
+ alpha (torch.Tensor): (H, W) rendered alpha
+ """
+ resolution = self.rendering_options["resolution"]
+ near = self.rendering_options["near"]
+ far = self.rendering_options["far"]
+ ssaa = self.rendering_options["ssaa"]
+
+ view = extrinsics
+ perspective = intrinsics_to_projection(intrinsics, near, far)
+ camera = torch.inverse(view)[:3, 3]
+ focalx = intrinsics[0, 0]
+ focaly = intrinsics[1, 1]
+ args = (
+ position,
+ attrs,
+ voxel_size,
+ view.T.contiguous(),
+ (perspective @ view).T.contiguous(),
+ camera,
+ 0.5 / focalx,
+ 0.5 / focaly,
+ resolution * ssaa,
+ resolution * ssaa,
+ )
+ color, depth, alpha = _C.rasterize_voxels_cuda(*args)
+
+ if ssaa > 1:
+ color = F.interpolate(color[None], size=(resolution, resolution), mode='bilinear', align_corners=False, antialias=True).squeeze()
+ depth = F.interpolate(depth[None, None], size=(resolution, resolution), mode='bilinear', align_corners=False, antialias=True).squeeze()
+ alpha = F.interpolate(alpha[None, None], size=(resolution, resolution), mode='bilinear', align_corners=False, antialias=True).squeeze()
+
+ ret = edict({
+ 'attr': color,
+ 'depth': depth,
+ 'alpha': alpha,
+ })
+ return ret
+
\ No newline at end of file
diff --git a/o-voxel/o_voxel/serialize.py b/o-voxel/o_voxel/serialize.py
new file mode 100644
index 0000000000000000000000000000000000000000..daf7598059ceb40e2aca64f97503c36ae5ccba0a
--- /dev/null
+++ b/o-voxel/o_voxel/serialize.py
@@ -0,0 +1,68 @@
+from typing import *
+import torch
+from . import _C
+
+
+@torch.no_grad()
+def encode_seq(coords: torch.Tensor, permute: List[int] = [0, 1, 2], mode: Literal['z_order', 'hilbert'] = 'z_order') -> torch.Tensor:
+ """
+ Encodes 3D coordinates into a 30-bit code.
+
+ Args:
+ coords: a tensor of shape [N, 3] containing the 3D coordinates.
+ permute: the permutation of the coordinates.
+ mode: the encoding mode to use.
+ """
+ assert coords.shape[-1] == 3 and coords.ndim == 2, "Input coordinates must be of shape [N, 3]"
+ x = coords[:, permute[0]].int()
+ y = coords[:, permute[1]].int()
+ z = coords[:, permute[2]].int()
+ if mode == 'z_order':
+ if coords.device.type == 'cpu':
+ return _C.z_order_encode_cpu(x, y, z)
+ elif coords.device.type == 'cuda':
+ return _C.z_order_encode_cuda(x, y, z)
+ else:
+ raise ValueError(f"Unsupported device type: {coords.device.type}")
+ elif mode == 'hilbert':
+ if coords.device.type == 'cpu':
+ return _C.hilbert_encode_cpu(x, y, z)
+ elif coords.device.type == 'cuda':
+ return _C.hilbert_encode_cuda(x, y, z)
+ else:
+ raise ValueError(f"Unsupported device type: {coords.device.type}")
+ else:
+ raise ValueError(f"Unknown encoding mode: {mode}")
+
+
+@torch.no_grad()
+def decode_seq(code: torch.Tensor, permute: List[int] = [0, 1, 2], mode: Literal['z_order', 'hilbert'] = 'z_order') -> torch.Tensor:
+ """
+ Decodes a 30-bit code into 3D coordinates.
+
+ Args:
+ code: a tensor of shape [N] containing the 30-bit code.
+ permute: the permutation of the coordinates.
+ mode: the decoding mode to use.
+ """
+ assert code.ndim == 1, "Input code must be of shape [N]"
+ if mode == 'z_order':
+ if code.device.type == 'cpu':
+ coords = _C.z_order_decode_cpu(code)
+ elif code.device.type == 'cuda':
+ coords = _C.z_order_decode_cuda(code)
+ else:
+ raise ValueError(f"Unsupported device type: {code.device.type}")
+ elif mode == 'hilbert':
+ if code.device.type == 'cpu':
+ coords = _C.hilbert_decode_cpu(code)
+ elif code.device.type == 'cuda':
+ coords = _C.hilbert_decode_cuda(code)
+ else:
+ raise ValueError(f"Unsupported device type: {code.device.type}")
+ else:
+ raise ValueError(f"Unknown decoding mode: {mode}")
+ x = coords[permute.index(0)]
+ y = coords[permute.index(1)]
+ z = coords[permute.index(2)]
+ return torch.stack([x, y, z], dim=-1)
diff --git a/o-voxel/pyproject.toml b/o-voxel/pyproject.toml
new file mode 100644
index 0000000000000000000000000000000000000000..11cc2b37fffcd91084a78990dc39d253c18bb0ec
--- /dev/null
+++ b/o-voxel/pyproject.toml
@@ -0,0 +1,34 @@
+[build-system]
+requires = [
+ "setuptools>=64",
+ "wheel",
+ "torch",
+ "numpy",
+ "plyfile",
+ "trimesh",
+ "tqdm",
+ "zstandard",
+ "easydict"
+]
+build-backend = "setuptools.build_meta"
+
+
+[project]
+name = "o_voxel"
+version = "0.0.1"
+description = "All about voxel."
+requires-python = ">=3.8"
+authors = [
+ { name = "Jianfeng Xiang", email = "belljig@outlook.com" }
+]
+dependencies = [
+ "torch",
+ "numpy",
+ "plyfile",
+ "trimesh",
+ "tqdm",
+ "zstandard",
+ "easydict",
+ "cumesh @ git+https://github.com/JeffreyXiang/CuMesh.git",
+ "flex_gemm @ git+https://github.com/JeffreyXiang/FlexGEMM.git",
+]
diff --git a/o-voxel/setup.py b/o-voxel/setup.py
new file mode 100644
index 0000000000000000000000000000000000000000..a8dfa83c1b65c3834a31528e7276f872a6649f69
--- /dev/null
+++ b/o-voxel/setup.py
@@ -0,0 +1,81 @@
+from setuptools import setup
+from torch.utils.cpp_extension import CUDAExtension, BuildExtension, IS_HIP_EXTENSION
+import os
+import sys
+
+ROOT = os.path.dirname(os.path.abspath(__file__))
+BUILD_TARGET = os.environ.get("BUILD_TARGET", "auto")
+
+if BUILD_TARGET == "auto":
+ if IS_HIP_EXTENSION:
+ IS_HIP = True
+ else:
+ IS_HIP = False
+else:
+ if BUILD_TARGET == "cuda":
+ IS_HIP = False
+ elif BUILD_TARGET == "rocm":
+ IS_HIP = True
+
+if not IS_HIP:
+ cc_flag = []
+else:
+ archs = os.getenv("GPU_ARCHS", "native").split(";")
+ cc_flag = [f"--offload-arch={arch}" for arch in archs]
+
+# Platform-specific compiler flags
+IS_WINDOWS = sys.platform == "win32"
+
+if IS_WINDOWS:
+ # MSVC flags
+ # Use C++20 for better std::byte handling
+ cxx_flags = ["/O2", "/std:c++20"]
+ nvcc_flags = ["-O3", "-std=c++20"] + cc_flag
+else:
+ # GCC/Clang flags
+ cxx_flags = ["-O3", "-std=c++17"]
+ nvcc_flags = ["-O3", "-std=c++17"] + cc_flag
+
+setup(
+ name="o_voxel",
+ packages=[
+ 'o_voxel',
+ 'o_voxel.convert',
+ 'o_voxel.io',
+ ],
+ ext_modules=[
+ CUDAExtension(
+ name="o_voxel._C",
+ sources=[
+ # Hashmap functions
+ "src/hash/hash.cu",
+ # Convert functions
+ "src/convert/flexible_dual_grid.cpp",
+ "src/convert/volumetic_attr.cpp",
+ ## Serialization functions
+ "src/serialize/api.cu",
+ "src/serialize/hilbert.cu",
+ "src/serialize/z_order.cu",
+ # IO functions
+ "src/io/svo.cpp",
+ "src/io/filter_parent.cpp",
+ "src/io/filter_neighbor.cpp",
+ # Rasterization functions
+ "src/rasterize/rasterize.cu",
+
+ # main
+ "src/ext.cpp",
+ ],
+ include_dirs=[
+ os.path.join(ROOT, "third_party/eigen"),
+ ],
+ extra_compile_args={
+ "cxx": cxx_flags,
+ "nvcc": nvcc_flags,
+ }
+ )
+ ],
+ cmdclass={
+ 'build_ext': BuildExtension
+ }
+)
diff --git a/o-voxel/src/convert/api.h b/o-voxel/src/convert/api.h
new file mode 100644
index 0000000000000000000000000000000000000000..e5d264d22ec55456abcf0ba6fbc6798142b62f15
--- /dev/null
+++ b/o-voxel/src/convert/api.h
@@ -0,0 +1,122 @@
+/*
+ * O-Voxel Convertion API
+ *
+ * Copyright (C) 2025, Jianfeng XIANG
+ * All rights reserved.
+ *
+ * Licensed under The MIT License [see LICENSE for details]
+ *
+ * Written by Jianfeng XIANG
+ */
+
+#pragma once
+#include
+
+
+/**
+ * Extract flexible dual grid from a triangle mesh.
+ *
+ * @param vertices: Tensor of shape (N, 3) containing vertex positions.
+ * @param faces: Tensor of shape (M, 3) containing triangle vertex indices.
+ * @param voxel_size: Tensor of shape (3,) containing the voxel size in each dimension.
+ * @param grid_range: Tensor of shape (2, 3) containing the minimum and maximum coordinates of the grid range.
+ * @param face_weight: Weight for the face edges in the QEM computation.
+ * @param boundary_weight: Weight for the boundary edges in the QEM computation.
+ * @param regularization_weight: Regularization factor to apply to the QEM matrices.
+ * @param timing: Boolean flag to indicate whether to print timing information.
+ *
+ * @return a tuple ((x, y, z), vertices, intersected, faces) containing the remeshed vertices and the corresponding voxel grid.
+ */
+std::tuple mesh_to_flexible_dual_grid_cpu(
+ const torch::Tensor& vertices,
+ const torch::Tensor& faces,
+ const torch::Tensor& voxel_size,
+ const torch::Tensor& grid_range,
+ float face_weight,
+ float boundary_weight,
+ float regularization_weight,
+ bool timing
+);
+
+
+/**
+ * Voxelizes a triangle mesh with PBR materials
+ *
+ * @param voxel_size [3] tensor containing the size of a voxel
+ * @param grid_range [6] tensor containing the size of the grid
+ * @param vertices [N_tri, 3, 3] array containing the triangle vertices
+ * @param normals [N_tri, 3, 3] array containing the triangle vertex normals
+ * @param uvs [N_tri, 3, 2] tensor containing the texture coordinates
+ * @param materialIds [N_tri] tensor containing the material ids
+ * @param baseColorFactor list of [3] tensor containing the base color factor
+ * @param baseColorTexture list of [H, W, 3] tensor containing the base color texture
+ * @param baseColorTextureFilter list of int indicating the base color texture filter (0: NEAREST, 1: LINEAR)
+ * @param baseColorTextureWrap list of int indicating the base color texture wrap (0: REPEAT, 1: CLAMP_TO_EDGE, 2: MIRRORED_REPEAT)
+ * @param metallicFactor list of float containing the metallic factor
+ * @param metallicTexture list of [H, W] tensor containing the metallic texture
+ * @param metallicTextureFilter list of int indicating the metallic texture filter (0: NEAREST, 1: LINEAR)
+ * @param metallicTextureWrap list of int indicating the metallic texture wrap (0: REPEAT, 1: CLAMP_TO_EDGE, 2: MIRRORED_REPEAT)
+ * @param roughnessFactor list of float containing the roughness factor
+ * @param roughnessTexture list of [H, W] tensor containing the roughness texture
+ * @param roughnessTextureFilter list of int indicating the roughness texture filter (0: NEAREST, 1: LINEAR)
+ * @param roughnessTextureWrap list of int indicating the roughness texture wrap (0: REPEAT, 1: CLAMP_TO_EDGE, 2: MIRRORED_REPEAT)
+ * @param emissiveFactor list of [3] tensor containing the emissive factor
+ * @param emissiveTexture list of [H, W, 3] tensor containing the emissive texture
+ * @param emissiveTextureFilter list of int indicating the emissive texture filter (0: NEAREST, 1: LINEAR)
+ * @param emissiveTextureWrap list of int indicating the emissive texture wrap (0: REPEAT, 1: CLAMP_TO_EDGE, 2: MIRRORED_REPEAT)
+ * @param alphaMode list of int indicating the alpha mode (0: OPAQUE, 1: MASK, 2: BLEND)
+ * @param alphaCutoff list of float containing the alpha cutoff
+ * @param alphaFactor list of float containing the alpha factor
+ * @param alphaTexture list of [H, W] tensor containing the alpha texture
+ * @param alphaTextureFilter list of int indicating the alpha texture filter (0: NEAREST, 1: LINEAR)
+ * @param alphaTextureWrap list of int indicating the alpha texture wrap (0: REPEAT, 1: CLAMP_TO_EDGE, 2: MIRRORED_REPEAT)
+ * @param normalTexture list of [H, W, 3] tensor containing the normal texture
+ * @param normalTextureFilter list of int indicating the normal texture filter (0: NEAREST, 1: LINEAR)
+ * @param normalTextureWrap list of int indicating the normal texture wrap (0: REPEAT, 1: CLAMP_TO_EDGE, 2: MIRRORED_REPEAT)
+ * @param mipLevelOffset float indicating the mip level offset for texture mipmap
+ *
+ * @return tuple containing:
+ * - coords: tensor of shape [N, 3] containing the voxel coordinates
+ * - out_baseColor: tensor of shape [N, 3] containing the base color of each voxel
+ * - out_metallic: tensor of shape [N, 1] containing the metallic of each voxel
+ * - out_roughness: tensor of shape [N, 1] containing the roughness of each voxel
+ * - out_emissive: tensor of shape [N, 3] containing the emissive of each voxel
+ * - out_alpha: tensor of shape [N, 1] containing the alpha of each voxel
+ * - out_normal: tensor of shape [N, 3] containing the normal of each voxel
+ */
+std::tuple
+textured_mesh_to_volumetric_attr_cpu(
+ const torch::Tensor& voxel_size,
+ const torch::Tensor& grid_range,
+ const torch::Tensor& vertices,
+ const torch::Tensor& normals,
+ const torch::Tensor& uvs,
+ const torch::Tensor& materialIds,
+ const std::vector& baseColorFactor,
+ const std::vector& baseColorTexture,
+ const std::vector& baseColorTextureFilter,
+ const std::vector& baseColorTextureWrap,
+ const std::vector& metallicFactor,
+ const std::vector& metallicTexture,
+ const std::vector& metallicTextureFilter,
+ const std::vector& metallicTextureWrap,
+ const std::vector& roughnessFactor,
+ const std::vector& roughnessTexture,
+ const std::vector& roughnessTextureFilter,
+ const std::vector& roughnessTextureWrap,
+ const std::vector& emissiveFactor,
+ const std::vector& emissiveTexture,
+ const std::vector& emissiveTextureFilter,
+ const std::vector& emissiveTextureWrap,
+ const std::vector& alphaMode,
+ const std::vector& alphaCutoff,
+ const std::vector& alphaFactor,
+ const std::vector& alphaTexture,
+ const std::vector& alphaTextureFilter,
+ const std::vector& alphaTextureWrap,
+ const std::vector& normalTexture,
+ const std::vector& normalTextureFilter,
+ const std::vector& normalTextureWrap,
+ const float mipLevelOffset,
+ const bool timing
+);
diff --git a/o-voxel/src/convert/flexible_dual_grid.cpp b/o-voxel/src/convert/flexible_dual_grid.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..0e6cb805d13ce2654e6ffa93315a7456eeabf498
--- /dev/null
+++ b/o-voxel/src/convert/flexible_dual_grid.cpp
@@ -0,0 +1,775 @@
+#include
+#include
+#include
+#include
+#include
+
+#include "api.h"
+
+
+constexpr size_t kInvalidIndex = std::numeric_limits::max();
+
+
+struct float3 {float x, y, z; float& operator[](int i) {return (&x)[i];}};
+struct int3 {int x, y, z; int& operator[](int i) {return (&x)[i];}};
+struct int4 {int x, y, z, w; int& operator[](int i) {return (&x)[i];}};
+struct bool3 {bool x, y, z; bool& operator[](int i) {return (&x)[i];}};
+
+
+template
+static inline U lerp(const T& a, const T& b, const T& t, const U& val_a, const U& val_b) {
+ if (a == b) return val_a; // Avoid divide by zero
+ T alpha = (t - a) / (b - a);
+ return (1 - alpha) * val_a + alpha * val_b;
+}
+
+
+template
+static auto get_or_default(const Map& map, const Key& key, const Default& default_val) -> typename Map::mapped_type {
+ auto it = map.find(key);
+ return (it != map.end()) ? it->second : default_val;
+}
+
+
+// 3D voxel coordinate
+struct VoxelCoord {
+ int x, y, z;
+
+ int& operator[](int i) {
+ return (&x)[i];
+ }
+
+ bool operator==(const VoxelCoord& other) const {
+ return x == other.x && y == other.y && z == other.z;
+ }
+};
+
+// Hash function for VoxelCoord to use in unordered_map
+namespace std {
+template <>
+struct hash {
+ size_t operator()(const VoxelCoord& v) const {
+ const std::size_t p1 = 73856093;
+ const std::size_t p2 = 19349663;
+ const std::size_t p3 = 83492791;
+ return (std::size_t)(v.x) * p1 ^ (std::size_t)(v.y) * p2 ^ (std::size_t)(v.z) * p3;
+ }
+};
+}
+
+
+void intersect_qef(
+ const Eigen::Vector3f& voxel_size,
+ const Eigen::Vector3i& grid_min,
+ const Eigen::Vector3i& grid_max,
+ const std::vector& triangles, // 3 vertices per triangle
+ std::unordered_map& hash_table, // Hash table for voxel lookup
+ std::vector& voxels, // Output: Voxel coordinates
+ std::vector& means, // Output: Mean vertex positions for each voxel
+ std::vector& cnt, // Output: Number of intersections for each voxel
+ std::vector& intersected, // Output: Whether edge of voxel intersects with triangle
+ std::vector& qefs // Output: QEF matrices for each voxel
+) {
+ const size_t N_tri = triangles.size() / 3;
+
+ for (size_t i = 0; i < N_tri; ++i) {
+ const Eigen::Vector3f& v0 = triangles[i * 3 + 0];
+ const Eigen::Vector3f& v1 = triangles[i * 3 + 1];
+ const Eigen::Vector3f& v2 = triangles[i * 3 + 2];
+
+ // Compute edge vectors and face normal
+ Eigen::Vector3f e0 = v1 - v0;
+ Eigen::Vector3f e1 = v2 - v1;
+ Eigen::Vector3f n = e0.cross(e1).normalized();
+ Eigen::Vector4f plane;
+ plane << n.x(), n.y(), n.z(), -n.dot(v0);
+ auto Q = plane * plane.transpose();
+
+ // Scan-line algorithm to find intersections with the voxel grid from three directions
+ /*
+ t0
+ | \
+ | t1
+ | /
+ t2
+ */
+ auto scan_line_fill = [&] (const int ax2) {
+ int ax0 = (ax2 + 1) % 3;
+ int ax1 = (ax2 + 2) % 3;
+
+ // Canonical question
+ std::array t = {
+ Eigen::Vector3d(v0[ax0], v0[ax1], v0[ax2]),
+ Eigen::Vector3d(v1[ax0], v1[ax1], v1[ax2]),
+ Eigen::Vector3d(v2[ax0], v2[ax1], v2[ax2])
+ };
+ std::sort(t.begin(), t.end(), [](const Eigen::Vector3d& a, const Eigen::Vector3d& b) { return a.y() < b.y(); });
+
+ // Scan-line algorithm
+ int start = std::clamp(int(t[0].y() / voxel_size[ax1]), grid_min[ax1], grid_max[ax1] - 1);
+ int mid = std::clamp(int(t[1].y() / voxel_size[ax1]), grid_min[ax1], grid_max[ax1] - 1);
+ int end = std::clamp(int(t[2].y() / voxel_size[ax1]), grid_min[ax1], grid_max[ax1] - 1);
+
+ auto scan_line_half = [&] (const int row_start, const int row_end, const Eigen::Vector3d t0, const Eigen::Vector3d t1, const Eigen::Vector3d t2) {
+ /*
+ t0
+ | \
+ t3-t4
+ | \
+ t1---t2
+ */
+ for (int y_idx = row_start; y_idx < row_end; ++y_idx) {
+ double y = (y_idx + 1) * voxel_size[ax1];
+ Eigen::Vector2d t3 = lerp(t0.y(), t1.y(), y, Eigen::Vector2d(t0.x(), t0.z()), Eigen::Vector2d(t1.x(), t1.z()));
+ Eigen::Vector2d t4 = lerp(t0.y(), t2.y(), y, Eigen::Vector2d(t0.x(), t0.z()), Eigen::Vector2d(t2.x(), t2.z()));
+ if (t3.x() > t4.x()) std::swap(t3, t4);
+ int line_start = std::clamp(int(t3.x() / voxel_size[ax0]), grid_min[ax0], grid_max[ax0] - 1);
+ int line_end = std::clamp(int(t4.x() / voxel_size[ax0]), grid_min[ax0], grid_max[ax0] - 1);
+ for (int x_idx = line_start; x_idx < line_end; ++x_idx) {
+ double x = (x_idx + 1) * voxel_size[ax0];
+ double z = lerp(t3.x(), t4.x(), x, t3.y(), t4.y());
+ int z_idx = int(z / voxel_size[ax2]);
+ if (z_idx >= grid_min[ax2] && z_idx < grid_max[ax2]) {
+ // For 4-connected voxels
+ for (int dx = 0; dx < 2; ++dx) {
+ for (int dy = 0; dy < 2; ++dy) {
+ VoxelCoord coord;
+ coord[ax0] = x_idx + dx; coord[ax1] = y_idx + dy; coord[ax2] = z_idx;
+ Eigen::Vector3d intersect;
+ intersect[ax0] = x; intersect[ax1] = y; intersect[ax2] = z;
+ auto kv = hash_table.find(coord);
+ if (kv == hash_table.end()) {
+ hash_table[coord] = voxels.size();
+ voxels.push_back({coord.x, coord.y, coord.z});
+ means.push_back(intersect.cast());
+ cnt.push_back(1);
+ intersected.push_back({false, false, false});
+ qefs.push_back(Q);
+ if (dx == 0 && dy == 0)
+ intersected.back()[ax2] = true;
+ }
+ else {
+ auto i = kv->second;
+ means[i] += intersect.cast();
+ cnt[i] += 1;
+ if (dx == 0 && dy == 0)
+ intersected[i][ax2] = true;
+ qefs[i] += Q;
+ }
+ }
+ }
+ }
+ }
+ }
+ };
+ scan_line_half(start, mid, t[0], t[1], t[2]);
+ scan_line_half(mid, end, t[2], t[1], t[0]);
+ };
+ scan_line_fill(0);
+ scan_line_fill(1);
+ scan_line_fill(2);
+ }
+}
+
+
+void face_qef(
+ const Eigen::Vector3f& voxel_size,
+ const Eigen::Vector3i& grid_min,
+ const Eigen::Vector3i& grid_max,
+ const std::vector& triangles, // 3 vertices per triangle
+ std::unordered_map& hash_table, // Hash table for voxel lookup
+ std::vector& qefs // Output: QEF matrices for each voxel
+) {
+ const size_t N_tri = triangles.size() / 3;
+
+ for (size_t i = 0; i < N_tri; ++i) {
+ const Eigen::Vector3f& v0 = triangles[i * 3 + 0];
+ const Eigen::Vector3f& v1 = triangles[i * 3 + 1];
+ const Eigen::Vector3f& v2 = triangles[i * 3 + 2];
+
+ // Compute edge vectors and face normal
+ Eigen::Vector3f e0 = v1 - v0;
+ Eigen::Vector3f e1 = v2 - v1;
+ Eigen::Vector3f e2 = v0 - v2;
+ Eigen::Vector3f n = e0.cross(e1).normalized();
+ Eigen::Vector4f plane;
+ plane << n.x(), n.y(), n.z(), -n.dot(v0);
+ auto Q = plane * plane.transpose();
+
+ // Compute triangle bounding box in voxel coordinates
+ Eigen::Vector3f bb_min_f = v0.cwiseMin(v1).cwiseMin(v2).cwiseQuotient(voxel_size);
+ Eigen::Vector3f bb_max_f = v0.cwiseMax(v1).cwiseMax(v2).cwiseQuotient(voxel_size);
+
+ Eigen::Vector3i bb_min(std::max(static_cast(bb_min_f.x()), grid_min.x()),
+ std::max(static_cast(bb_min_f.y()), grid_min.y()),
+ std::max(static_cast(bb_min_f.z()), grid_min.z()));
+ Eigen::Vector3i bb_max(std::min(static_cast(bb_max_f.x() + 1), grid_max.x()),
+ std::min(static_cast(bb_max_f.y() + 1), grid_max.y()),
+ std::min(static_cast(bb_max_f.z() + 1), grid_max.z()));
+
+ // Plane test setup
+ Eigen::Vector3f c(
+ n.x() > 0.0f ? voxel_size.x() : 0.0f,
+ n.y() > 0.0f ? voxel_size.y() : 0.0f,
+ n.z() > 0.0f ? voxel_size.z() : 0.0f
+ );
+ float d1 = n.dot(c - v0);
+ float d2 = n.dot(voxel_size - c - v0);
+
+ // XY plane projection test setup
+ int mul_xy = (n.z() < 0.0f) ? -1 : 1;
+ Eigen::Vector2f n_xy_e0(-mul_xy * e0.y(), mul_xy * e0.x());
+ Eigen::Vector2f n_xy_e1(-mul_xy * e1.y(), mul_xy * e1.x());
+ Eigen::Vector2f n_xy_e2(-mul_xy * e2.y(), mul_xy * e2.x());
+
+ float d_xy_e0 = -n_xy_e0.dot(v0.head<2>()) + n_xy_e0.cwiseMax(0.0f).dot(voxel_size.head<2>());
+ float d_xy_e1 = -n_xy_e1.dot(v1.head<2>()) + n_xy_e1.cwiseMax(0.0f).dot(voxel_size.head<2>());
+ float d_xy_e2 = -n_xy_e2.dot(v2.head<2>()) + n_xy_e2.cwiseMax(0.0f).dot(voxel_size.head<2>());
+
+ // YZ plane projection test setup
+ int mul_yz = (n.x() < 0.0f) ? -1 : 1;
+ Eigen::Vector2f n_yz_e0(-mul_yz * e0.z(), mul_yz * e0.y());
+ Eigen::Vector2f n_yz_e1(-mul_yz * e1.z(), mul_yz * e1.y());
+ Eigen::Vector2f n_yz_e2(-mul_yz * e2.z(), mul_yz * e2.y());
+
+ float d_yz_e0 = -n_yz_e0.dot(Eigen::Vector2f(v0.y(), v0.z())) + n_yz_e0.cwiseMax(0.0f).dot(Eigen::Vector2f(voxel_size.y(), voxel_size.z()));
+ float d_yz_e1 = -n_yz_e1.dot(Eigen::Vector2f(v1.y(), v1.z())) + n_yz_e1.cwiseMax(0.0f).dot(Eigen::Vector2f(voxel_size.y(), voxel_size.z()));
+ float d_yz_e2 = -n_yz_e2.dot(Eigen::Vector2f(v2.y(), v2.z())) + n_yz_e2.cwiseMax(0.0f).dot(Eigen::Vector2f(voxel_size.y(), voxel_size.z()));
+
+ // ZX plane projection test setup
+ int mul_zx = (n.y() < 0.0f) ? -1 : 1;
+ Eigen::Vector2f n_zx_e0(-mul_zx * e0.x(), mul_zx * e0.z());
+ Eigen::Vector2f n_zx_e1(-mul_zx * e1.x(), mul_zx * e1.z());
+ Eigen::Vector2f n_zx_e2(-mul_zx * e2.x(), mul_zx * e2.z());
+
+ float d_zx_e0 = -n_zx_e0.dot(Eigen::Vector2f(v0.z(), v0.x())) + n_zx_e0.cwiseMax(0.0f).dot(Eigen::Vector2f(voxel_size.z(), voxel_size.x()));
+ float d_zx_e1 = -n_zx_e1.dot(Eigen::Vector2f(v1.z(), v1.x())) + n_zx_e1.cwiseMax(0.0f).dot(Eigen::Vector2f(voxel_size.z(), voxel_size.x()));
+ float d_zx_e2 = -n_zx_e2.dot(Eigen::Vector2f(v2.z(), v2.x())) + n_zx_e2.cwiseMax(0.0f).dot(Eigen::Vector2f(voxel_size.z(), voxel_size.x()));
+
+ // Loop over candidate voxels inside bounding box
+ for (int z = bb_min.z(); z < bb_max.z(); ++z) {
+ for (int y = bb_min.y(); y < bb_max.y(); ++y) {
+ for (int x = bb_min.x(); x < bb_max.x(); ++x) {
+ // Voxel center
+ Eigen::Vector3f p = voxel_size.cwiseProduct(Eigen::Vector3f(x, y, z));
+
+ // Plane through box test
+ float nDOTp = n.dot(p);
+ if (((nDOTp + d1) * (nDOTp + d2)) > 0.0f) continue;
+
+ // XY projection test
+ Eigen::Vector2f p_xy(p.x(), p.y());
+ if (n_xy_e0.dot(p_xy) + d_xy_e0 < 0) continue;
+ if (n_xy_e1.dot(p_xy) + d_xy_e1 < 0) continue;
+ if (n_xy_e2.dot(p_xy) + d_xy_e2 < 0) continue;
+
+ // YZ projection test
+ Eigen::Vector2f p_yz(p.y(), p.z());
+ if (n_yz_e0.dot(p_yz) + d_yz_e0 < 0) continue;
+ if (n_yz_e1.dot(p_yz) + d_yz_e1 < 0) continue;
+ if (n_yz_e2.dot(p_yz) + d_yz_e2 < 0) continue;
+
+ // ZX projection test
+ Eigen::Vector2f p_zx(p.z(), p.x());
+ if (n_zx_e0.dot(p_zx) + d_zx_e0 < 0) continue;
+ if (n_zx_e1.dot(p_zx) + d_zx_e1 < 0) continue;
+ if (n_zx_e2.dot(p_zx) + d_zx_e2 < 0) continue;
+
+ // Passed all tests — mark voxel
+ auto coord = VoxelCoord{x, y, z};
+ auto kv = hash_table.find(coord);
+ if (kv != hash_table.end()) {
+ qefs[kv->second] += Q;
+ }
+ }
+ }
+ }
+ }
+}
+
+
+void boundry_qef(
+ const Eigen::Vector3f& voxel_size,
+ const Eigen::Vector3i& grid_min,
+ const Eigen::Vector3i& grid_max,
+ const std::vector& boundries, // 2 vertices per segment
+ const float boundary_weight, // Weight for boundary edges
+ std::unordered_map& hash_table, // Hash table for voxel lookup
+ std::vector& qefs // Output: QEF matrices for each voxel
+) {
+ for (size_t i = 0; i < boundries.size() / 2; ++i) {
+ const Eigen::Vector3f& v0 = boundries[i * 2 + 0];
+ const Eigen::Vector3f& v1 = boundries[i * 2 + 1];
+
+ // Calculate the QEF for the edge (boundary) defined by v0 and v1
+ Eigen::Vector3d dir(v1.x() - v0.x(), v1.y() - v0.y(), v1.z() - v0.z());
+ double segment_length = dir.norm();
+ if (segment_length < 1e-6) continue; // Skip degenerate edges (zero-length)
+ dir.normalize(); // unit direction vector
+
+ // Projection matrix orthogonal to the direction: I - d d^T
+ Eigen::Matrix3f A = Eigen::Matrix3f::Identity() - (dir * dir.transpose()).cast();
+
+ // b = -A * v0
+ Eigen::Vector3f b = -A * v0;
+
+ // c = v0^T * A * v0
+ float c = v0.transpose() * A * v0;
+
+ // Now pack this into a 4x4 QEF matrix
+ Eigen::Matrix4f Q = Eigen::Matrix4f::Zero();
+ Q.block<3, 3>(0, 0) = A;
+ Q.block<3, 1>(0, 3) = b;
+ Q.block<1, 3>(3, 0) = b.transpose();
+ Q(3, 3) = c;
+
+ // DDA Traversal logic directly inside the function
+
+ // Starting and ending voxel coordinates
+ Eigen::Vector3i v0_voxel = (v0.cwiseQuotient(voxel_size)).array().floor().cast();
+ Eigen::Vector3i v1_voxel = (v1.cwiseQuotient(voxel_size)).array().floor().cast();
+
+ // Determine step direction for each axis based on the line direction
+ Eigen::Vector3i step = (dir.array() > 0).select(Eigen::Vector3i(1, 1, 1), Eigen::Vector3i(-1, -1, -1));
+
+ Eigen::Vector3d tMax, tDelta;
+ for (int axis = 0; axis < 3; ++axis) {
+ if (dir[axis] == 0.0) {
+ tMax[axis] = std::numeric_limits::infinity();
+ tDelta[axis] = std::numeric_limits::infinity();
+ } else {
+ float voxel_border = voxel_size[axis] * (v0_voxel[axis] + (step[axis] > 0 ? 1 : 0));
+ tMax[axis] = (voxel_border - v0[axis]) / dir[axis];
+ tDelta[axis] = voxel_size[axis] / std::abs(dir[axis]);
+ }
+ }
+
+ // Current voxel position
+ Eigen::Vector3i current = v0_voxel;
+
+ // Store the voxel we start at
+ std::vector voxels;
+ voxels.push_back({current.x(), current.y(), current.z()});
+
+ // Traverse the voxels
+ while (true) {
+ int axis;
+ if (tMax.x() < tMax.y()) {
+ axis = (tMax.x() < tMax.z()) ? 0 : 2;
+ } else {
+ axis = (tMax.y() < tMax.z()) ? 1 : 2;
+ }
+
+ if (tMax[axis] > segment_length) break;
+
+ current[axis] += step[axis];
+ tMax[axis] += tDelta[axis];
+
+ voxels.push_back({current.x(), current.y(), current.z()});
+ }
+
+ // Accumulate QEF for each voxel passed through
+ for (const auto& coord : voxels) {
+ // Make sure the voxel is within bounds
+ if ((coord.x < grid_min.x() || coord.x >= grid_max.x()) ||
+ (coord.y < grid_min.y() || coord.y >= grid_max.y()) ||
+ (coord.z < grid_min.z() || coord.z >= grid_max.z())) continue;
+ if (!hash_table.count(coord)) continue; // Skip if voxel not in hash table
+
+ // Accumulate the QEF for this voxel
+ qefs[hash_table[coord]] += boundary_weight * Q; // Scale by boundary weight
+ }
+ }
+}
+
+
+std::array quad_to_2tri(
+ const std::vector& vertices,
+ const int4& quad_indices
+) {
+ int ia = quad_indices.x;
+ int ib = quad_indices.y;
+ int ic = quad_indices.z;
+ int id = quad_indices.w;
+
+ Eigen::Vector3f a(vertices[ia].x, vertices[ia].y, vertices[ia].z);
+ Eigen::Vector3f b(vertices[ib].x, vertices[ib].y, vertices[ib].z);
+ Eigen::Vector3f c(vertices[ic].x, vertices[ic].y, vertices[ic].z);
+ Eigen::Vector3f d(vertices[id].x, vertices[id].y, vertices[id].z);
+
+ // diagonal AC
+ Eigen::Vector3f n_abc = (b - a).cross(c - a).normalized();
+ Eigen::Vector3f n_acd = (c - a).cross(d - a).normalized();
+ float angle_ac = std::acos(std::clamp(n_abc.dot(n_acd), -1.0f, 1.0f));
+
+ // diagonal BD
+ Eigen::Vector3f n_abd = (b - a).cross(d - a).normalized();
+ Eigen::Vector3f n_bcd = (c - b).cross(d - b).normalized();
+ float angle_bd = std::acos(std::clamp(n_abd.dot(n_bcd), -1.0f, 1.0f));
+
+ if (angle_ac <= angle_bd) {
+ return {int3{ia, ib, ic}, int3{ia, ic, id}};
+ } else {
+ return {int3{ia, ib, id}, int3{ib, ic, id}};
+ }
+}
+
+
+void face_from_dual_vertices(
+ const std::unordered_map& hash_table,
+ const std::vector& voxels,
+ const std::vector& dual_vertices,
+ const std::vector& intersected,
+ std::vector& face_indices
+) {
+ for (int i = 0; i < dual_vertices.size(); ++i) {
+ int3 coord = voxels[i];
+ bool3 is_intersected = intersected[i];
+
+ // Check existence of neighboring 6 voxels
+ size_t neigh_indices[6] = {
+ get_or_default(hash_table, VoxelCoord{coord.x + 1, coord.y, coord.z}, kInvalidIndex),
+ get_or_default(hash_table, VoxelCoord{coord.x, coord.y + 1, coord.z}, kInvalidIndex),
+ get_or_default(hash_table, VoxelCoord{coord.x + 1, coord.y + 1, coord.z}, kInvalidIndex),
+ get_or_default(hash_table, VoxelCoord{coord.x, coord.y, coord.z + 1}, kInvalidIndex),
+ get_or_default(hash_table, VoxelCoord{coord.x + 1, coord.y, coord.z + 1}, kInvalidIndex),
+ get_or_default(hash_table, VoxelCoord{coord.x, coord.y + 1, coord.z + 1}, kInvalidIndex)
+ };
+
+ // xy-plane
+ if (is_intersected[2] && neigh_indices[0] != kInvalidIndex && neigh_indices[1] != kInvalidIndex && neigh_indices[2] != kInvalidIndex) {
+ int4 quad_indices{i, neigh_indices[0], neigh_indices[2], neigh_indices[1]};
+ auto tri_indices = quad_to_2tri(dual_vertices, quad_indices);
+ face_indices.insert(face_indices.end(), tri_indices.begin(), tri_indices.end());
+ }
+ // yz-plane
+ if (is_intersected[0] && neigh_indices[1] != kInvalidIndex && neigh_indices[3] != kInvalidIndex && neigh_indices[5] != kInvalidIndex) {
+ int4 quad_indices{i, neigh_indices[1], neigh_indices[5], neigh_indices[3]};
+ auto tri_indices = quad_to_2tri(dual_vertices, quad_indices);
+ face_indices.insert(face_indices.end(), tri_indices.begin(), tri_indices.end());
+ }
+ // xz-plane
+ if (is_intersected[1] && neigh_indices[0] != kInvalidIndex && neigh_indices[3] != kInvalidIndex && neigh_indices[4] != kInvalidIndex) {
+ int4 quad_indices{i, neigh_indices[0], neigh_indices[4], neigh_indices[3]};
+ auto tri_indices = quad_to_2tri(dual_vertices, quad_indices);
+ face_indices.insert(face_indices.end(), tri_indices.begin(), tri_indices.end());
+ }
+ }
+}
+
+/**
+ * Extract flexible dual grid from a triangle mesh.
+ *
+ * @param vertices: Tensor of shape (N, 3) containing vertex positions.
+ * @param faces: Tensor of shape (M, 3) containing triangle vertex indices.
+ * @param voxel_size: Tensor of shape (3,) containing the voxel size in each dimension.
+ * @param grid_range: Tensor of shape (2, 3) containing the minimum and maximum coordinates of the grid range.
+ * @param face_weight: Weight for the face edges in the QEF computation.
+ * @param boundary_weight: Weight for the boundary edges in the QEF computation.
+ * @param regularization_weight: Regularization factor to apply to the QEF matrices.
+ * @param timing: Boolean flag to indicate whether to print timing information.
+ *
+ * @return a tuple ((x, y, z), vertices, intersected, faces) containing the remeshed vertices and the corresponding voxel grid.
+ */
+std::tuple mesh_to_flexible_dual_grid_cpu(
+ const torch::Tensor& vertices,
+ const torch::Tensor& faces,
+ const torch::Tensor& voxel_size,
+ const torch::Tensor& grid_range,
+ float face_weight,
+ float boundary_weight,
+ float regularization_weight,
+ bool timing
+) {
+ const int F = faces.size(0);
+ const float* v_ptr = vertices.data_ptr();
+ const int* f_ptr = faces.data_ptr();
+ const float* voxel_size_ptr = voxel_size.data_ptr();
+ const int* grid_range_ptr = grid_range.data_ptr();
+ clock_t start, end;
+ std::unordered_map hash_table;
+ std::vector voxels; // Voxel coordinates
+ std::vector means; // Mean vertex positions for each voxel
+ std::vector cnt; // Number of intersections for each voxel
+ std::vector intersected; // Indicate whether edges of voxels intersect with surface
+ std::vector qefs; // QEF matrices for each voxel
+
+ // Convert tensors to Eigen types
+ Eigen::Vector3f e_voxel_size(voxel_size_ptr[0], voxel_size_ptr[1], voxel_size_ptr[2]);
+ Eigen::Vector3i e_grid_min(grid_range_ptr[0], grid_range_ptr[1], grid_range_ptr[2]);
+ Eigen::Vector3i e_grid_max(grid_range_ptr[3], grid_range_ptr[4], grid_range_ptr[5]);
+
+ // Intersect QEF computation
+ start = clock();
+ std::vector triangles;
+ triangles.reserve(F * 3);
+ for (int f = 0; f < F; ++f) {
+ for (int v = 0; v < 3; ++v) {
+ triangles.push_back(Eigen::Vector3f(
+ v_ptr[f_ptr[f * 3 + v] * 3 + 0],
+ v_ptr[f_ptr[f * 3 + v] * 3 + 1],
+ v_ptr[f_ptr[f * 3 + v] * 3 + 2]
+ ));
+ }
+ }
+ intersect_qef(e_voxel_size, e_grid_min, e_grid_max, triangles, hash_table, voxels, means, cnt, intersected, qefs);
+ end = clock();
+ if (timing) std::cout << "Intersect QEF computation took " << double(end - start) / CLOCKS_PER_SEC << " seconds." << std::endl;
+
+ // Face QEF computation
+ if (face_weight > 0.0f) {
+ start = clock();
+ face_qef(e_voxel_size, e_grid_min, e_grid_max, triangles, hash_table, qefs);
+ end = clock();
+ if (timing) std::cout << "Face QEF computation took " << double(end - start) / CLOCKS_PER_SEC << " seconds." << std::endl;
+ }
+
+ // Boundary QEF computation
+ if (boundary_weight > 0.0f) {
+ start = clock();
+ std::map, int> edge_count;
+ for (int f = 0; f < F; ++f) {
+ for (int v0 = 0; v0 < 3; ++v0) {
+ int e0 = f_ptr[f * 3 + v0];
+ int e1 = f_ptr[f * 3 + (v0 + 1) % 3];
+ if (e0 > e1) std::swap(e0, e1);
+ edge_count[std::make_pair(e0, e1)]++;
+ }
+ }
+ std::vector boundries;
+ for (const auto& e : edge_count) {
+ if (e.second == 1) {
+ int v0 = e.first.first;
+ int v1 = e.first.second;
+ boundries.push_back(Eigen::Vector3f(
+ v_ptr[v0 * 3 + 0],
+ v_ptr[v0 * 3 + 1],
+ v_ptr[v0 * 3 + 2]
+ ));
+ boundries.push_back(Eigen::Vector3f(
+ v_ptr[v1 * 3 + 0],
+ v_ptr[v1 * 3 + 1],
+ v_ptr[v1 * 3 + 2]
+ ));
+ }
+ }
+ boundry_qef(e_voxel_size, e_grid_min, e_grid_max, boundries, boundary_weight, hash_table, qefs);
+ end = clock();
+ if (timing) std::cout << "Boundary QEF computation took " << double(end - start) / CLOCKS_PER_SEC << " seconds." << std::endl;
+ }
+
+ // Solve the QEF system to obtain final dual vertices
+ start = clock();
+ std::vector dual_vertices(voxels.size());
+ for (int i = 0; i < voxels.size(); ++i) {
+ int3 coord = voxels[i];
+ Eigen::Matrix4f Q = qefs[i];
+ float min_corner[3] = {
+ coord.x * e_voxel_size.x(),
+ coord.y * e_voxel_size.y(),
+ coord.z * e_voxel_size.z()
+ };
+ float max_corner[3] = {
+ (coord.x + 1) * e_voxel_size.x(),
+ (coord.y + 1) * e_voxel_size.y(),
+ (coord.z + 1) * e_voxel_size.z()
+ };
+
+ // Add regularization term
+ if (regularization_weight > 0.0f) {
+ Eigen::Vector3f p = means[i] / cnt[i];
+
+ // Construct the QEF matrix for this vertex
+ Eigen::Matrix4f Qreg = Eigen::Matrix4f::Zero();
+ Qreg.topLeftCorner<3,3>() = Eigen::Matrix3f::Identity();
+ Qreg.block<3,1>(0,3) = -p;
+ Qreg.block<1,3>(3,0) = -p.transpose();
+ Qreg(3,3) = p.dot(p);
+
+ Q += regularization_weight * cnt[i] * Qreg; // Scale by regularization weight
+ }
+
+ // Solve unconstrained
+ Eigen::Matrix3f A = Q.topLeftCorner<3, 3>();
+ Eigen::Vector3f b = -Q.block<3, 1>(0, 3);
+ Eigen::Vector3f v_new = A.colPivHouseholderQr().solve(b);
+
+ if (!(
+ v_new.x() >= min_corner[0] && v_new.x() <= max_corner[0] &&
+ v_new.y() >= min_corner[1] && v_new.y() <= max_corner[1] &&
+ v_new.z() >= min_corner[2] && v_new.z() <= max_corner[2]
+ )) {
+ // Starting enumeration of constraints
+ float best = std::numeric_limits::infinity();
+
+ // Solve single-constraint
+ auto solve_single_constraint = [&](int fixed_axis) {
+ int ax1 = (fixed_axis + 1) % 3;
+ int ax2 = (fixed_axis + 2) % 3;
+
+ Eigen::Matrix2f A;
+ Eigen::Matrix2f B;
+ Eigen::Vector2f q, b, x;
+
+ A << Q(ax1, ax1), Q(ax1, ax2),
+ Q(ax2, ax1), Q(ax2, ax2);
+ B << Q(ax1, fixed_axis), Q(ax1, 3),
+ Q(ax2, fixed_axis), Q(ax2, 3);
+ auto Asol = A.colPivHouseholderQr();
+
+ // if lower bound
+ q << min_corner[fixed_axis], 1;
+ b = -B * q;
+ x = Asol.solve(b);
+ if (
+ x.x() >= min_corner[ax1] && x.x() <= max_corner[ax1] &&
+ x.y() >= min_corner[ax2] && x.y() <= max_corner[ax2]
+ ) {
+ Eigen::Vector4f p;
+ p[fixed_axis] = min_corner[fixed_axis];
+ p[ax1] = x.x();
+ p[ax2] = x.y();
+ p[3] = 1.0f;
+ float err = p.transpose() * Q * p;
+ if (err < best) {
+ best = err;
+ v_new << p[0], p[1], p[2];
+ }
+ }
+
+ // if upper bound
+ q << max_corner[fixed_axis], 1;
+ b = -B * q;
+ x = Asol.solve(b);
+ if (
+ x.x() >= min_corner[ax1] && x.x() <= max_corner[ax1] &&
+ x.y() >= min_corner[ax2] && x.y() <= max_corner[ax2]
+ ) {
+ Eigen::Vector4f p;
+ p[fixed_axis] = max_corner[fixed_axis];
+ p[ax1] = x.x();
+ p[ax2] = x.y();
+ p[3] = 1.0f;
+ float err = p.transpose() * Q * p;
+ if (err < best) {
+ best = err;
+ v_new << p[0], p[1], p[2];
+ }
+ }
+ };
+ solve_single_constraint(0); // fix x
+ solve_single_constraint(1); // fix y
+ solve_single_constraint(2); // fix z
+
+ // Solve two-constraint
+ auto solve_two_constraint = [&](int free_axis) {
+ int ax1 = (free_axis + 1) % 3;
+ int ax2 = (free_axis + 2) % 3;
+
+ float a, x;
+ Eigen::Vector3f b, q;
+
+ a = Q(free_axis, free_axis);
+ b << Q(free_axis, ax1), Q(free_axis, ax2), Q(free_axis, 3);
+
+ // if lower-lower bound
+ q << min_corner[ax1], min_corner[ax2], 1;
+ x = -(b.dot(q)) / a;
+ if (x >= min_corner[free_axis] && x <= max_corner[free_axis]) {
+ Eigen::Vector4f p;
+ p[free_axis] = x;
+ p[ax1] = min_corner[ax1];
+ p[ax2] = min_corner[ax2];
+ p[3] = 1.0f;
+ float err = p.transpose() * Q * p;
+ if (err < best) {
+ best = err;
+ v_new << p[0], p[1], p[2];
+ }
+ }
+
+ // if lower-upper bound
+ q << min_corner[ax1], max_corner[ax2], 1;
+ x = -(b.dot(q)) / a;
+ if (x >= min_corner[free_axis] && x <= max_corner[free_axis]) {
+ Eigen::Vector4f p;
+ p[free_axis] = x;
+ p[ax1] = min_corner[ax1];
+ p[ax2] = max_corner[ax2];
+ p[3] = 1.0f;
+ float err = p.transpose() * Q * p;
+ if (err < best) {
+ best = err;
+ v_new << p[0], p[1], p[2];
+ }
+ }
+
+ // if upper-lower bound
+ q << max_corner[ax1], min_corner[ax2], 1;
+ x = -(b.dot(q)) / a;
+ if (x >= min_corner[free_axis] && x <= max_corner[free_axis]) {
+ Eigen::Vector4f p;
+ p[free_axis] = x;
+ p[ax1] = max_corner[ax1];
+ p[ax2] = min_corner[ax2];
+ p[3] = 1.0f;
+ float err = p.transpose() * Q * p;
+ if (err < best) {
+ best = err;
+ v_new << p[0], p[1], p[2];
+ }
+ }
+
+ // if upper-upper bound
+ q << max_corner[ax1], max_corner[ax2], 1;
+ x = -(b.dot(q)) / a;
+ if (x >= min_corner[free_axis] && x <= max_corner[free_axis]) {
+ Eigen::Vector4f p;
+ p[free_axis] = x;
+ p[ax1] = max_corner[ax1];
+ p[ax2] = max_corner[ax2];
+ p[3] = 1.0f;
+ float err = p.transpose() * Q * p;
+ if (err < best) {
+ best = err;
+ v_new << p[0], p[1], p[2];
+ }
+ }
+ };
+ solve_two_constraint(0); // free x
+ solve_two_constraint(1); // free y
+ solve_two_constraint(2); // free z
+
+ // Solve three-constraint
+ for (int x_constraint = 0; x_constraint < 2; ++x_constraint) {
+ for (int y_constraint = 0; y_constraint < 2; ++y_constraint) {
+ for (int z_constraint = 0; z_constraint < 2; ++z_constraint) {
+ Eigen::Vector4f p;
+ p[0] = x_constraint ? min_corner[0] : max_corner[0];
+ p[1] = y_constraint ? min_corner[1] : max_corner[1];
+ p[2] = z_constraint ? min_corner[2] : max_corner[2];
+ p[3] = 1.0f;
+
+ float err = p.transpose() * Q * p;
+ if (err < best) {
+ best = err;
+ v_new << p[0], p[1], p[2];
+ }
+ }
+ }
+ }
+ }
+
+ // Store the dual vertex and voxel grid coordinates
+ dual_vertices[i] = float3{v_new.x(), v_new.y(), v_new.z()};
+ }
+ end = clock();
+ if (timing) std::cout << "Dual vertices computation took " << double(end - start) / CLOCKS_PER_SEC << " seconds." << std::endl;
+
+ return std::make_tuple(
+ torch::from_blob(voxels.data(), {int(voxels .size()), 3}, torch::kInt32).clone(),
+ torch::from_blob(dual_vertices.data(), {int(dual_vertices.size()), 3}, torch::kFloat32).clone(),
+ torch::from_blob(intersected.data(), {int(intersected.size()), 3}, torch::kBool).clone()
+ );
+}
+
diff --git a/o-voxel/src/convert/volumetic_attr.cpp b/o-voxel/src/convert/volumetic_attr.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..9b9e94f143c4c544be443a9e80b7233f84673d33
--- /dev/null
+++ b/o-voxel/src/convert/volumetic_attr.cpp
@@ -0,0 +1,872 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "api.h"
+
+
+constexpr size_t kInvalidIndex = std::numeric_limits::max();
+
+
+static bool is_power_of_two(int n) {
+ return n > 0 && (n & (n - 1)) == 0;
+}
+
+
+template
+static inline U lerp(const T& a, const T& b, const T& t, const U& val_a, const U& val_b) {
+ if (a == b) return val_a; // Avoid divide by zero
+ T alpha = (t - a) / (b - a);
+ return (1 - alpha) * val_a + alpha * val_b;
+}
+
+
+template
+static auto get_or_default(const Map& map, const Key& key, const Default& default_val) -> typename Map::mapped_type {
+ auto it = map.find(key);
+ return (it != map.end()) ? it->second : default_val;
+}
+
+
+// 3D voxel coordinate
+struct VoxelCoord {
+ int x, y, z;
+
+ int& operator[](int i) {
+ return (&x)[i];
+ }
+
+ bool operator==(const VoxelCoord& other) const {
+ return x == other.x && y == other.y && z == other.z;
+ }
+};
+
+// Hash function for VoxelCoord to use in unordered_map
+namespace std {
+template <>
+struct hash {
+ size_t operator()(const VoxelCoord& v) const {
+ const std::size_t p1 = 73856093;
+ const std::size_t p2 = 19349663;
+ const std::size_t p3 = 83492791;
+ return (std::size_t)(v.x) * p1 ^ (std::size_t)(v.y) * p2 ^ (std::size_t)(v.z) * p3;
+ }
+};
+}
+
+
+/**
+ * Compute the Normal Tangent and Bitangent vectors for a triangle.
+ *
+ * @param v0 The first vertex of the triangle.
+ * @param v1 The second vertex of the triangle.
+ * @param v2 The third vertex of the triangle.
+ * @param uv0 The texture coordinates of the first vertex.
+ * @param uv1 The texture coordinates of the second vertex.
+ * @param uv2 The texture coordinates of the third vertex.
+ *
+ * @return A tuple containing:
+ * - t The tangent vector.
+ * - b The bitangent vector.
+ * - n The normal vector.
+ * - mip_length The norms of the partial derivatives of the 3D coordinates with respect to the 2D texture coordinates.
+ */
+static std::tuple compute_TBN(
+ const Eigen::Vector3f& v0,
+ const Eigen::Vector3f& v1,
+ const Eigen::Vector3f& v2,
+ const Eigen::Vector2f& uv0,
+ const Eigen::Vector2f& uv1,
+ const Eigen::Vector2f& uv2
+) {
+ Eigen::Vector3f e1 = v1 - v0;
+ Eigen::Vector3f e2 = v2 - v0;
+ Eigen::Vector2f duv1 = uv1 - uv0;
+ Eigen::Vector2f duv2 = uv2 - uv0;
+ Eigen::Vector3f n = e1.cross(e2).normalized();
+
+ float det = duv1.x() * duv2.y() - duv1.y() * duv2.x();
+ if (fabs(det) < 1e-6) {
+ // Use default
+ Eigen::Vector3f t(1.0f, 0.0f, 0.0f);
+ Eigen::Vector3f b(0.0f, 1.0f, 0.0f);
+ Eigen::Vector2f mip_length(1e6, 1e6);
+ return std::make_tuple(t, b, n, mip_length);
+ }
+
+ float invDet = 1.0f / det;
+ Eigen::Vector3f t = (duv2.y() * e1 - duv1.y() * e2);
+ Eigen::Vector3f b = (duv1.x() * e2 - duv2.x() * e1);
+ float t_norm = t.norm();
+ float b_norm = b.norm();
+ t = t / t_norm;
+ b = b / b_norm;
+ Eigen::Vector2f mip_length(invDet * t_norm, invDet * b_norm);
+
+ return std::make_tuple(t, b, n, mip_length);
+}
+
+
+/**
+ * Project a point onto a triangle defined by three vertices.
+ *
+ * @param p The point to project.
+ * @param a The first vertex of the triangle.
+ * @param b The second vertex of the triangle.
+ * @param c The third vertex of the triangle.
+ * @param n The normal of the triangle.
+ *
+ * @return The projected point represented as barycentric coordinates (u, v, w) and distance from the plane.
+ */
+static Eigen::Vector4f project_onto_triangle(
+ const Eigen::Vector3f& p,
+ const Eigen::Vector3f& a,
+ const Eigen::Vector3f& b,
+ const Eigen::Vector3f& c,
+ const Eigen::Vector3f& n
+) {
+ float d = (p - a).dot(n);
+
+ Eigen::Vector3f p_proj = p - d * n;
+ Eigen::Vector3f ab = b - a;
+ Eigen::Vector3f ac = c - a;
+ Eigen::Vector3f ap = p_proj - a;
+
+ float d00 = ab.dot(ab);
+ float d01 = ab.dot(ac);
+ float d11 = ac.dot(ac);
+ float d20 = ap.dot(ab);
+ float d21 = ap.dot(ac);
+
+ float denom = d00 * d11 - d01 * d01;
+ float v = (d11 * d20 - d01 * d21) / denom;
+ float w = (d00 * d21 - d01 * d20) / denom;
+ float u = 1.0f - v - w;
+
+ return Eigen::Vector4f(u, v, w, d);
+}
+
+
+static inline int wrap_texcoord(const int& x, const int& W, const int& filter) {
+ if (filter == 0) { // REPEAT
+ return (x % W + W) % W;
+ } else if (filter == 1) { // CLAMP_TO_EDGE
+ return std::max(0, std::min(x, W - 1));
+ } else if (filter == 2) { // MIRROR_REPEAT
+ int period = 2 * W;
+ int x_mod = (x % period + period) % period;
+ return (x_mod < W) ? x_mod : (period - x_mod - 1);
+ } else {
+ // Default to repeat
+ return (x % W + W) % W;
+ }
+}
+
+
+static std::vector> build_mipmaps(
+ const uint8_t* texture,
+ const int& H, const int& W, const int& C
+) {
+ if (H != W || !is_power_of_two(H)) {
+ throw std::invalid_argument("Texture width and height must be equal and a power of two.");
+ }
+ std::vector> mipmaps;
+ const uint8_t* cur_map = texture;
+ int cur_H = H;
+ int cur_W = W;
+ int next_H = cur_H >> 1;
+ int next_W = cur_W >> 1;
+ while (next_H > 0 && next_W > 0) {
+ std::vector next_map(next_H * next_W * C);
+ for (int y = 0; y < next_H; y++) {
+ for (int x = 0; x < next_W; x++) {
+ for (int c = 0; c < C; c++) {
+ size_t sum = 0;
+ size_t xx = static_cast(x) << 1;
+ size_t yy = static_cast(y) << 1;
+ sum += cur_map[yy * static_cast(cur_W) * C + xx * C + c];
+ sum += cur_map[(yy + 1) * static_cast(cur_W) * C + xx * C + c];
+ sum += cur_map[yy * static_cast(cur_W) * C + (xx + 1) * C + c];
+ sum += cur_map[(yy + 1) * static_cast(cur_W) * C + (xx + 1) * C + c];
+ next_map[y * next_W * C + x * C + c] = static_cast(sum / 4);
+ }
+ }
+ }
+ mipmaps.push_back(std::move(next_map));
+ cur_map = mipmaps.back().data();
+ cur_H = next_H;
+ cur_W = next_W;
+ next_H = cur_H >> 1;
+ next_W = cur_W >> 1;
+ }
+ return mipmaps;
+}
+
+
+static void sample_texture(
+ const uint8_t* texture,
+ const int& H, const int& W, const int& C,
+ const float& u, const float& v,
+ const int& filter, const int& wrap,
+ float* color
+) {
+ float x = u * W;
+ float y = (1 - v) * H;
+ if (filter == 0) { // NEAREST
+ int x_int = floorf(x);
+ int y_int = floorf(y);
+ x_int = wrap_texcoord(x_int, W, wrap);
+ y_int = wrap_texcoord(y_int, H, wrap);
+ for (int c = 0; c < C; c++) {
+ color[c] = texture[y_int * W * C + x_int * C + c] / 255.0f;
+ }
+ }
+ else { // LINEAR
+ int x_low = floorf(x - 0.5);
+ int x_high = x_low + 1;
+ int y_low = floorf(y - 0.5);
+ int y_high = y_low + 1;
+ float w_x = x - x_low - 0.5;
+ float w_y = y - y_low - 0.5;
+ x_low = wrap_texcoord(x_low, W, wrap);
+ x_high = wrap_texcoord(x_high, W, wrap);
+ y_low = wrap_texcoord(y_low, H, wrap);
+ y_high = wrap_texcoord(y_high, H, wrap);
+ for (int c = 0; c < C; c++) {
+ color[c] = (1 - w_x) * (1 - w_y) * texture[y_low * W * C + x_low * C + c] +
+ w_x * (1 - w_y) * texture[y_low * W * C + x_high * C + c] +
+ (1 - w_x) * w_y * texture[y_high * W * C + x_low * C + c] +
+ w_x * w_y * texture[y_high * W * C + x_high * C + c];
+ color[c] /= 255.0f;
+ }
+ }
+}
+
+
+static void sample_texture_mipmap(
+ const uint8_t* texture,
+ const int& H, const int& W, const int& C,
+ const std::vector>& mipmaps,
+ const float& u, const float& v, const float& mip_length, const float& mipLevelOffset,
+ const int& filter, const int& wrap,
+ float* color
+) {
+ if (filter == 0) { // NEAREST
+ sample_texture(texture, H, W, C, u, v, filter, wrap, color);
+ }
+ else { // LINEAR
+ float mip_level = std::log2(mip_length * H) + mipLevelOffset;
+ if (!std::isfinite(mip_level) || mip_level <= 0 || mipmaps.empty()) {
+ sample_texture(texture, H, W, C, u, v, filter, wrap, color);
+ }
+ else if (mip_level >= mipmaps.size()) {
+ sample_texture(mipmaps[mipmaps.size() - 1].data(), H >> mipmaps.size(), W >> mipmaps.size(), C, u, v, filter, wrap, color);
+ }
+ else {
+ int lower_mip_level = std::floor(mip_level);
+ int upper_mip_level = lower_mip_level + 1;
+ float mip_frac = mip_level - lower_mip_level;
+ const uint8_t* lower_mip_ptr = lower_mip_level == 0 ? texture : mipmaps[lower_mip_level - 1].data();
+ const uint8_t* upper_mip_ptr = mipmaps[upper_mip_level - 1].data();
+ int lower_mip_H = H >> lower_mip_level;
+ int lower_mip_W = W >> lower_mip_level;
+ int upper_mip_H = H >> upper_mip_level;
+ int upper_mip_W = W >> upper_mip_level;
+ std::vector lower_mip_sample(C);
+ std::vector upper_mip_sample(C);
+ sample_texture(lower_mip_ptr, lower_mip_H, lower_mip_W, C, u, v, filter, wrap, lower_mip_sample.data());
+ sample_texture(upper_mip_ptr, upper_mip_H, upper_mip_W, C, u, v, filter, wrap, upper_mip_sample.data());
+ for (int c = 0; c < C; c++) {
+ color[c] = (1 - mip_frac) * lower_mip_sample[c] + mip_frac * upper_mip_sample[c];
+ }
+ }
+ }
+}
+
+
+static std::tuple, std::vector, std::vector, std::vector, std::vector, std::vector, std::vector>
+voxelize_trimesh_pbr_impl(
+ const float* voxel_size,
+ const int* grid_range,
+ const int N_tri,
+ const float* vertices,
+ const float* normals,
+ const float* uvs,
+ const int* materialIds,
+ const std::vector baseColorFactor,
+ const std::vector baseColorTexture,
+ const std::vector H_bcTex, const std::vector W_bcTex,
+ const std::vector baseColorTextureFilter,
+ const std::vector baseColorTextureWrap,
+ const std::vector metallicFactor,
+ const std::vector metallicTexture,
+ const std::vector H_mtlTex, const std::vector W_mtlTex,
+ const std::vector metallicTextureFilter,
+ const std::vector metallicTextureWrap,
+ const std::vector roughnessFactor,
+ const std::vector roughnessTexture,
+ const std::vector H_rghTex, const std::vector W_rghTex,
+ const std::vector roughnessTextureFilter,
+ const std::vector roughnessTextureWrap,
+ const std::vector emissiveFactor,
+ const std::vector emissiveTexture,
+ const std::vector H_emTex, const std::vector W_emTex,
+ const std::vector emissiveTextureFilter,
+ const std::vector emissiveTextureWrap,
+ const std::vector alphaMode,
+ const std::vector alphaCutoff,
+ const std::vector alphaFactor,
+ const std::vector alphaTexture,
+ const std::vector H_aTex, const std::vector W_aTex,
+ const std::vector alphaTextureFilter,
+ const std::vector alphaTextureWrap,
+ const std::vector normalTexture,
+ const std::vector H_nTex, const std::vector W_nTex,
+ const std::vector normalTextureFilter,
+ const std::vector normalTextureWrap,
+ const float mipLevelOffset,
+ const bool timing
+) {
+ clock_t start, end;
+
+ // Common variables used in the voxelization process
+ Eigen::Vector3f delta_p(voxel_size[0], voxel_size[1], voxel_size[2]);
+ Eigen::Vector3i grid_min(grid_range[0], grid_range[1], grid_range[2]);
+ Eigen::Vector3i grid_max(grid_range[3], grid_range[4], grid_range[5]);
+
+ // Construct Mipmaps
+ start = clock();
+ std::vector>> baseColorMipmaps(baseColorTexture.size());
+ std::vector>> metallicMipmaps(metallicTexture.size());
+ std::vector>> roughnessMipmaps(roughnessTexture.size());
+ std::vector>> emissiveMipmaps(emissiveTexture.size());
+ std::vector>> alphaMipmaps(alphaTexture.size());
+ std::vector>> normalMipmaps(normalTexture.size());
+ for (size_t i = 0; i < baseColorTexture.size(); i++) {
+ if (baseColorTexture[i] != nullptr && baseColorTextureFilter[i] != 0) {
+ baseColorMipmaps[i] = build_mipmaps(baseColorTexture[i], H_bcTex[i], W_bcTex[i], 3);
+ }
+ }
+ for (size_t i = 0; i < metallicTexture.size(); i++) {
+ if (metallicTexture[i] != nullptr && metallicTextureFilter[i] != 0) {
+ metallicMipmaps[i] = build_mipmaps(metallicTexture[i], H_mtlTex[i], W_mtlTex[i], 1);
+ }
+ }
+ for (size_t i = 0; i < roughnessTexture.size(); i++) {
+ if (roughnessTexture[i] != nullptr && roughnessTextureFilter[i] != 0) {
+ roughnessMipmaps[i] = build_mipmaps(roughnessTexture[i], H_rghTex[i], W_rghTex[i], 1);
+ }
+ }
+ for (size_t i = 0; i < emissiveTexture.size(); i++) {
+ if (emissiveTexture[i] != nullptr && emissiveTextureFilter[i] != 0) {
+ emissiveMipmaps[i] = build_mipmaps(emissiveTexture[i], H_emTex[i], W_emTex[i], 3);
+ }
+ }
+ for (size_t i = 0; i < alphaTexture.size(); i++) {
+ if (alphaTexture[i] != nullptr && alphaTextureFilter[i] != 0) {
+ alphaMipmaps[i] = build_mipmaps(alphaTexture[i], H_aTex[i], W_aTex[i], 1);
+ }
+ }
+ for (size_t i = 0; i < normalTexture.size(); i++) {
+ if (normalTexture[i] != nullptr && normalTextureFilter[i] != 0) {
+ normalMipmaps[i] = build_mipmaps(normalTexture[i], H_nTex[i], W_nTex[i], 3);
+ }
+ }
+ end = clock();
+ if (timing) std::cout << "Mipmaps construction took " << double(end - start) / CLOCKS_PER_SEC << " seconds." << std::endl;
+
+ // Buffers
+ std::unordered_map hash_table;
+ std::vector coords;
+ std::vector buf_weights;
+ std::vector buf_baseColors;
+ std::vector buf_metallics;
+ std::vector buf_roughnesses;
+ std::vector buf_emissives;
+ std::vector buf_alphas;
+ std::vector buf_normals;
+
+ // Enumerate all triangles
+ start = clock();
+ for (size_t tid = 0; tid < N_tri; tid++) {
+ // COMPUTE COMMON TRIANGLE PROPERTIES
+ // Move vertices to origin using bbox
+ size_t ptr = tid * 9;
+ Eigen::Vector3f v0(vertices[ptr], vertices[ptr + 1], vertices[ptr + 2]);
+ Eigen::Vector3f v1(vertices[ptr + 3], vertices[ptr + 4], vertices[ptr + 5]);
+ Eigen::Vector3f v2(vertices[ptr + 6], vertices[ptr + 7], vertices[ptr + 8]);
+ // Normals
+ Eigen::Vector3f n0(normals[ptr], normals[ptr + 1], normals[ptr + 2]);
+ Eigen::Vector3f n1(normals[ptr + 3], normals[ptr + 4], normals[ptr + 5]);
+ Eigen::Vector3f n2(normals[ptr + 6], normals[ptr + 7], normals[ptr + 8]);
+ // UV vectors
+ ptr = tid * 6;
+ Eigen::Vector2f uv0(uvs[ptr], uvs[ptr + 1]);
+ Eigen::Vector2f uv1(uvs[ptr + 2], uvs[ptr + 3]);
+ Eigen::Vector2f uv2(uvs[ptr + 4], uvs[ptr + 5]);
+ // TBN
+ auto tbn = compute_TBN(v0, v1, v2, uv0, uv1, uv2);
+ Eigen::Vector3f t = std::get<0>(tbn);
+ Eigen::Vector3f b = std::get<1>(tbn);
+ Eigen::Vector3f n = std::get<2>(tbn);
+ Eigen::Vector2f v_mip_length = std::get<3>(tbn);
+ float mip_length = delta_p.maxCoeff() / std::sqrt(v_mip_length.x() * v_mip_length.y());
+ // Material ID
+ int mid = materialIds[tid];
+
+ // Find intersected voxel for each triangle
+ std::unordered_set intersected_voxels;
+ // Scan-line algorithm to find intersections with the voxel grid from three directions
+ /*
+ t0
+ | \
+ | t1
+ | /
+ t2
+ */
+ auto scan_line_fill = [&] (const int ax2) {
+ int ax0 = (ax2 + 1) % 3;
+ int ax1 = (ax2 + 2) % 3;
+
+ // Canonical question
+ std::array t = {
+ Eigen::Vector3d(v0[ax0], v0[ax1], v0[ax2]),
+ Eigen::Vector3d(v1[ax0], v1[ax1], v1[ax2]),
+ Eigen::Vector3d(v2[ax0], v2[ax1], v2[ax2])
+ };
+ std::sort(t.begin(), t.end(), [](const Eigen::Vector3d& a, const Eigen::Vector3d& b) { return a.y() < b.y(); });
+
+ // Scan-line algorithm
+ int start = std::clamp(int(t[0].y() / voxel_size[ax1]), grid_min[ax1], grid_max[ax1] - 1);
+ int mid = std::clamp(int(t[1].y() / voxel_size[ax1]), grid_min[ax1], grid_max[ax1] - 1);
+ int end = std::clamp(int(t[2].y() / voxel_size[ax1]), grid_min[ax1], grid_max[ax1] - 1);
+
+ auto scan_line_half = [&] (const int row_start, const int row_end, const Eigen::Vector3d t0, const Eigen::Vector3d t1, const Eigen::Vector3d t2) {
+ /*
+ t0
+ | \
+ t3-t4
+ | \
+ t1---t2
+ */
+ for (int y_idx = row_start; y_idx < row_end; ++y_idx) {
+ double y = (y_idx + 1) * voxel_size[ax1];
+ Eigen::Vector2d t3 = lerp(t0.y(), t1.y(), y, Eigen::Vector2d(t0.x(), t0.z()), Eigen::Vector2d(t1.x(), t1.z()));
+ Eigen::Vector2d t4 = lerp(t0.y(), t2.y(), y, Eigen::Vector2d(t0.x(), t0.z()), Eigen::Vector2d(t2.x(), t2.z()));
+ if (t3.x() > t4.x()) std::swap(t3, t4);
+ int line_start = std::clamp(int(t3.x() / voxel_size[ax0]), grid_min[ax0], grid_max[ax0] - 1);
+ int line_end = std::clamp(int(t4.x() / voxel_size[ax0]), grid_min[ax0], grid_max[ax0] - 1);
+ for (int x_idx = line_start; x_idx < line_end; ++x_idx) {
+ double x = (x_idx + 1) * voxel_size[ax0];
+ double z = lerp(t3.x(), t4.x(), x, t3.y(), t4.y());
+ int z_idx = int(z / voxel_size[ax2]);
+ if (z_idx >= grid_min[ax2] && z_idx < grid_max[ax2]) {
+ // For 4-connected voxels
+ for (int dx = 0; dx < 2; ++dx) {
+ for (int dy = 0; dy < 2; ++dy) {
+ VoxelCoord coord;
+ coord[ax0] = x_idx + dx; coord[ax1] = y_idx + dy; coord[ax2] = z_idx;
+ intersected_voxels.insert(coord);
+ }
+ }
+ }
+ }
+ }
+ };
+ scan_line_half(start, mid, t[0], t[1], t[2]);
+ scan_line_half(mid, end, t[2], t[1], t[0]);
+ };
+ scan_line_fill(0);
+ scan_line_fill(1);
+ scan_line_fill(2);
+
+ // For all intersected voxels, ample texture and write to voxel grid
+ for (auto voxel : intersected_voxels) {
+ int x = voxel.x;
+ int y = voxel.y;
+ int z = voxel.z;
+
+ // Compute barycentric coordinates and weight
+ Eigen::Vector4f barycentric = project_onto_triangle(
+ Eigen::Vector3f((x + 0.5f) * delta_p.x(), (y + 0.5f) * delta_p.y(), (z + 0.5f) * delta_p.z()),
+ v0, v1, v2, n
+ );
+ Eigen::Vector2f uv = {
+ barycentric.x() * uv0.x() + barycentric.y() * uv1.x() + barycentric.z() * uv2.x(),
+ barycentric.x() * uv0.y() + barycentric.y() * uv1.y() + barycentric.z() * uv2.y()
+ };
+ Eigen::Vector3f int_n = {
+ barycentric.x() * n0.x() + barycentric.y() * n1.x() + barycentric.z() * n2.x(),
+ barycentric.x() * n0.y() + barycentric.y() * n1.y() + barycentric.z() * n2.y(),
+ barycentric.x() * n0.z() + barycentric.y() * n1.z() + barycentric.z() * n2.z()
+ };
+ float weight = 1 - barycentric.w();
+
+ /// base color
+ float baseColor[3] = {1, 1, 1};
+ if (baseColorTexture[mid]) {
+ sample_texture_mipmap(
+ baseColorTexture[mid],
+ H_bcTex[mid], W_bcTex[mid], 3,
+ baseColorMipmaps[mid],
+ uv.x(), uv.y(), mip_length, mipLevelOffset,
+ baseColorTextureFilter[mid], baseColorTextureWrap[mid],
+ baseColor
+ );
+ }
+ baseColor[0] *= baseColorFactor[mid][0];
+ baseColor[1] *= baseColorFactor[mid][1];
+ baseColor[2] *= baseColorFactor[mid][2];
+
+ /// metallic
+ float metallic = 1.0f;
+ if (metallicTexture[mid]) {
+ sample_texture_mipmap(
+ metallicTexture[mid],
+ H_mtlTex[mid], W_mtlTex[mid], 1,
+ metallicMipmaps[mid],
+ uv.x(), uv.y(), mip_length, mipLevelOffset,
+ metallicTextureFilter[mid], metallicTextureWrap[mid],
+ &metallic
+ );
+ }
+ metallic *= metallicFactor[mid];
+
+ /// roughness
+ float roughness = 1.0f;
+ if (roughnessTexture[mid]) {
+ sample_texture_mipmap(
+ roughnessTexture[mid],
+ H_rghTex[mid], W_rghTex[mid], 1,
+ roughnessMipmaps[mid],
+ uv.x(), uv.y(), mip_length, mipLevelOffset,
+ roughnessTextureFilter[mid], roughnessTextureWrap[mid],
+ &roughness
+ );
+ }
+ roughness *= roughnessFactor[mid];
+
+ /// emissive
+ float emissive[3] = {1, 1, 1};
+ if (emissiveTexture[mid]) {
+ sample_texture_mipmap(
+ emissiveTexture[mid],
+ H_emTex[mid], W_emTex[mid], 3,
+ roughnessMipmaps[mid],
+ uv.x(), uv.y(), mip_length, mipLevelOffset,
+ emissiveTextureFilter[mid], emissiveTextureWrap[mid],
+ emissive
+ );
+ }
+ emissive[0] *= emissiveFactor[mid][0];
+ emissive[1] *= emissiveFactor[mid][1];
+ emissive[2] *= emissiveFactor[mid][2];
+
+ /// alpha
+ float alpha = 1.0f;
+ if (alphaMode[mid] != 0) {
+ if (alphaTexture[mid]) {
+ sample_texture_mipmap(
+ alphaTexture[mid],
+ H_aTex[mid], W_aTex[mid], 1,
+ alphaMipmaps[mid],
+ uv.x(), uv.y(), mip_length, mipLevelOffset,
+ alphaTextureFilter[mid], alphaTextureWrap[mid],
+ &alpha
+ );
+ }
+ alpha *= alphaFactor[mid];
+ if (alphaMode[mid] == 1) { // MASK
+ alpha = alpha < alphaCutoff[mid] ? 0.0f : 1.0f;
+ }
+ }
+
+ /// normal
+ float normal[3] = {int_n.x(), int_n.y(), int_n.z()};
+ if (normalTexture[mid]) {
+ sample_texture_mipmap(
+ normalTexture[mid],
+ H_nTex[mid], W_nTex[mid], 3,
+ normalMipmaps[mid],
+ uv.x(), uv.y(), mip_length, mipLevelOffset,
+ normalTextureFilter[mid], normalTextureWrap[mid],
+ normal
+ );
+ normal[0] = normal[0] * 2 - 1;
+ normal[1] = normal[1] * 2 - 1;
+ normal[2] = normal[2] * 2 - 1;
+ Eigen::Vector3f _n = (normal[0] * t + normal[1] * b + normal[2] * int_n).normalized();
+ normal[0] = _n.x();
+ normal[1] = _n.y();
+ normal[2] = _n.z();
+ }
+
+ // Write to voxel grid
+ auto coord = VoxelCoord{x-grid_min.x(), y-grid_min.y(), z-grid_min.z()};
+ auto kv = hash_table.find(coord);
+ if (kv == hash_table.end()) {
+ hash_table[coord] = coords.size();
+ coords.push_back({coord.x, coord.y, coord.z});
+ buf_weights.push_back(weight);
+ buf_baseColors.push_back(Eigen::Vector3f(baseColor[0], baseColor[1], baseColor[2]) * weight);
+ buf_metallics.push_back(metallic * weight);
+ buf_roughnesses.push_back(roughness * weight);
+ buf_emissives.push_back(Eigen::Vector3f(emissive[0], emissive[1], emissive[2]) * weight);
+ buf_alphas.push_back(alpha * weight);
+ buf_normals.push_back(Eigen::Vector3f(normal[0], normal[1], normal[2]) * weight);
+ }
+ else {
+ auto i = kv->second;
+ buf_weights[i] += weight;
+ buf_baseColors[i] += Eigen::Vector3f(baseColor[0], baseColor[1], baseColor[2]) * weight;
+ buf_metallics[i] += metallic * weight;
+ buf_roughnesses[i] += roughness * weight;
+ buf_emissives[i] += Eigen::Vector3f(emissive[0], emissive[1], emissive[2]) * weight;
+ buf_alphas[i] += alpha * weight;
+ buf_normals[i] += Eigen::Vector3f(normal[0], normal[1], normal[2]) * weight;
+ }
+ }
+ }
+ end = clock();
+ if (timing) std::cout << "Voxelization took " << double(end - start) / CLOCKS_PER_SEC << " seconds." << std::endl;
+
+ // Normalize buffers
+ start = clock();
+ std::vector out_coord(coords.size() * 3);
+ std::vector out_baseColor(coords.size() * 3);
+ std::vector out_metallic(coords.size());
+ std::vector out_roughness(coords.size());
+ std::vector out_emissive(coords.size() * 3);
+ std::vector out_alpha(coords.size());
+ std::vector out_normal(coords.size() * 3);
+ for (int i = 0; i < coords.size(); i++) {
+ out_coord[i * 3 + 0] = coords[i].x;
+ out_coord[i * 3 + 1] = coords[i].y;
+ out_coord[i * 3 + 2] = coords[i].z;
+ out_baseColor[i * 3 + 0] = buf_baseColors[i].x() / buf_weights[i];
+ out_baseColor[i * 3 + 1] = buf_baseColors[i].y() / buf_weights[i];
+ out_baseColor[i * 3 + 2] = buf_baseColors[i].z() / buf_weights[i];
+ out_metallic[i] = buf_metallics[i] / buf_weights[i];
+ out_roughness[i] = buf_roughnesses[i] / buf_weights[i];
+ out_emissive[i * 3 + 0] = buf_emissives[i].x() / buf_weights[i];
+ out_emissive[i * 3 + 1] = buf_emissives[i].y() / buf_weights[i];
+ out_emissive[i * 3 + 2] = buf_emissives[i].z() / buf_weights[i];
+ out_alpha[i] = buf_alphas[i] / buf_weights[i];
+ out_normal[i * 3 + 0] = buf_normals[i].x() / buf_weights[i];
+ out_normal[i * 3 + 1] = buf_normals[i].y() / buf_weights[i];
+ out_normal[i * 3 + 2] = buf_normals[i].z() / buf_weights[i];
+ }
+ end = clock();
+ if (timing) std::cout << "Normalization took " << double(end - start) / CLOCKS_PER_SEC << " seconds." << std::endl;
+
+ return std::make_tuple(
+ std::move(out_coord),
+ std::move(out_baseColor),
+ std::move(out_metallic),
+ std::move(out_roughness),
+ std::move(out_emissive),
+ std::move(out_alpha),
+ std::move(out_normal)
+ );
+}
+
+
+std::tuple
+textured_mesh_to_volumetric_attr_cpu(
+ const torch::Tensor& voxel_size,
+ const torch::Tensor& grid_range,
+ const torch::Tensor& vertices,
+ const torch::Tensor& normals,
+ const torch::Tensor& uvs,
+ const torch::Tensor& materialIds,
+ const std::vector& baseColorFactor,
+ const std::vector& baseColorTexture,
+ const std::vector& baseColorTextureFilter,
+ const std::vector& baseColorTextureWrap,
+ const std::vector& metallicFactor,
+ const std::vector& metallicTexture,
+ const std::vector& metallicTextureFilter,
+ const std::vector& metallicTextureWrap,
+ const std::vector& roughnessFactor,
+ const std::vector& roughnessTexture,
+ const std::vector& roughnessTextureFilter,
+ const std::vector& roughnessTextureWrap,
+ const std::vector& emissiveFactor,
+ const std::vector& emissiveTexture,
+ const std::vector& emissiveTextureFilter,
+ const std::vector& emissiveTextureWrap,
+ const std::vector& alphaMode,
+ const std::vector& alphaCutoff,
+ const std::vector& alphaFactor,
+ const std::vector& alphaTexture,
+ const std::vector& alphaTextureFilter,
+ const std::vector& alphaTextureWrap,
+ const std::vector& normalTexture,
+ const std::vector& normalTextureFilter,
+ const std::vector& normalTextureWrap,
+ const float mipLevelOffset,
+ const bool timing
+) {
+ auto N_mat = baseColorFactor.size();
+ int N_tri = vertices.size(0);
+
+ // Get the size of the input tensors
+ std::vector baseColorFactor_ptrs(N_mat);
+ std::vector baseColorTexture_ptrs(N_mat);
+ std::vector H_bcTex(N_mat), W_bcTex(N_mat);
+ std::vector metallicFactor_vec(N_mat);
+ std::vector metallicTexture_ptrs(N_mat);
+ std::vector H_mtlTex(N_mat), W_mtlTex(N_mat);
+ std::vector roughnessFactor_vec(N_mat);
+ std::vector roughnessTexture_ptrs(N_mat);
+ std::vector H_rghTex(N_mat), W_rghTex(N_mat);
+ std::vector emissiveFactor_ptrs(N_mat);
+ std::vector emissiveTexture_ptrs(N_mat);
+ std::vector H_emTex(N_mat), W_emTex(N_mat);
+ std::vector alphaMode_vec(N_mat);
+ std::vector alphaCutoff_vec(N_mat);
+ std::vector alphaFactor_vec(N_mat);
+ std::vector alphaTexture_ptrs(N_mat);
+ std::vector H_aTex(N_mat), W_aTex(N_mat);
+ std::vector normalTexture_ptrs(N_mat);
+ std::vector H_nTex(N_mat), W_nTex(N_mat);
+
+ for (int i = 0; i < N_mat; ++i) {
+ baseColorFactor_ptrs[i] = baseColorFactor[i].contiguous().data_ptr();
+ if (baseColorTexture[i].numel() > 0) {
+ baseColorTexture_ptrs[i] = baseColorTexture[i].contiguous().data_ptr();
+ H_bcTex[i] = baseColorTexture[i].size(0);
+ W_bcTex[i] = baseColorTexture[i].size(1);
+ }
+ else {
+ baseColorTexture_ptrs[i] = nullptr;
+ H_bcTex[i] = 0;
+ W_bcTex[i] = 0;
+ }
+ metallicFactor_vec[i] = metallicFactor[i];
+ if (metallicTexture[i].numel() > 0) {
+ metallicTexture_ptrs[i] = metallicTexture[i].contiguous().data_ptr();
+ H_mtlTex[i] = metallicTexture[i].size(0);
+ W_mtlTex[i] = metallicTexture[i].size(1);
+ }
+ else {
+ metallicTexture_ptrs[i] = nullptr;
+ H_mtlTex[i] = 0;
+ W_mtlTex[i] = 0;
+ }
+ roughnessFactor_vec[i] = roughnessFactor[i];
+ if (roughnessTexture[i].numel() > 0) {
+ roughnessTexture_ptrs[i] = roughnessTexture[i].contiguous().data_ptr();
+ H_rghTex[i] = roughnessTexture[i].size(0);
+ W_rghTex[i] = roughnessTexture[i].size(1);
+ }
+ else {
+ roughnessTexture_ptrs[i] = nullptr;
+ H_rghTex[i] = 0;
+ W_rghTex[i] = 0;
+ }
+ emissiveFactor_ptrs[i] = emissiveFactor[i].contiguous().data_ptr();
+ if (emissiveTexture[i].numel() > 0) {
+ emissiveTexture_ptrs[i] = emissiveTexture[i].contiguous().data_ptr();
+ H_emTex[i] = emissiveTexture[i].size(0);
+ W_emTex[i] = emissiveTexture[i].size(1);
+ }
+ else {
+ emissiveTexture_ptrs[i] = nullptr;
+ H_emTex[i] = 0;
+ W_emTex[i] = 0;
+ }
+ alphaMode_vec[i] = alphaMode[i];
+ alphaCutoff_vec[i] = alphaCutoff[i];
+ alphaFactor_vec[i] = alphaFactor[i];
+ if (alphaTexture[i].numel() > 0) {
+ alphaTexture_ptrs[i] = alphaTexture[i].contiguous().data_ptr();
+ H_aTex[i] = alphaTexture[i].size(0);
+ W_aTex[i] = alphaTexture[i].size(1);
+ }
+ else {
+ alphaTexture_ptrs[i] = nullptr;
+ H_aTex[i] = 0;
+ W_aTex[i] = 0;
+ }
+ if (normalTexture[i].numel() > 0) {
+ normalTexture_ptrs[i] = normalTexture[i].contiguous().data_ptr();
+ H_nTex[i] = normalTexture[i].size(0);
+ W_nTex[i] = normalTexture[i].size(1);
+ }
+ else {
+ normalTexture_ptrs[i] = nullptr;
+ H_nTex[i] = 0;
+ W_nTex[i] = 0;
+ }
+ }
+
+ auto outputs = voxelize_trimesh_pbr_impl(
+ voxel_size.contiguous().data_ptr(),
+ grid_range.contiguous().data_ptr(),
+ N_tri,
+ vertices.contiguous().data_ptr(),
+ normals.contiguous().data_ptr(),
+ uvs.contiguous().data_ptr(),
+ materialIds.contiguous().data_ptr(),
+ baseColorFactor_ptrs,
+ baseColorTexture_ptrs,
+ H_bcTex, W_bcTex,
+ baseColorTextureFilter, baseColorTextureWrap,
+ metallicFactor_vec,
+ metallicTexture_ptrs,
+ H_mtlTex, W_mtlTex,
+ metallicTextureFilter, metallicTextureWrap,
+ roughnessFactor_vec,
+ roughnessTexture_ptrs,
+ H_rghTex, W_rghTex,
+ roughnessTextureFilter, roughnessTextureWrap,
+ emissiveFactor_ptrs,
+ emissiveTexture_ptrs,
+ H_emTex, W_emTex,
+ emissiveTextureFilter, emissiveTextureWrap,
+ alphaMode_vec,
+ alphaCutoff_vec,
+ alphaFactor_vec,
+ alphaTexture_ptrs,
+ H_aTex, W_aTex,
+ alphaTextureFilter, alphaTextureWrap,
+ normalTexture_ptrs,
+ H_nTex, W_nTex,
+ normalTextureFilter, normalTextureWrap,
+ mipLevelOffset,
+ timing
+ );
+
+ std::vector coords_vec = std::get<0>(outputs);
+ std::vector baseColors_vec = std::get<1>(outputs);
+ std::vector metallics_vec = std::get<2>(outputs);
+ std::vector roughnesses_vec = std::get<3>(outputs);
+ std::vector emissives_vec = std::get<4>(outputs);
+ std::vector alphas_vec = std::get<5>(outputs);
+ std::vector normals_vec = std::get<6>(outputs);
+
+ // Create output tensors
+ auto out_coords = torch::from_blob(coords_vec.data(), {static_cast(coords_vec.size() / 3), 3}, torch::kInt32).clone();
+ auto out_baseColors = torch::from_blob(baseColors_vec.data(), {static_cast(baseColors_vec.size() / 3), 3}, torch::kFloat32).clone();
+ auto out_metallics = torch::from_blob(metallics_vec.data(), {static_cast(metallics_vec.size())}, torch::kFloat32).clone();
+ auto out_roughnesses = torch::from_blob(roughnesses_vec.data(), {static_cast(roughnesses_vec.size())}, torch::kFloat32).clone();
+ auto out_emissives = torch::from_blob(emissives_vec.data(), {static_cast