xuvdb 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- xuvdb/__init__.py +64 -0
- xuvdb/gpu.py +96 -0
- xuvdb/io.py +173 -0
- xuvdb/kernels.py +258 -0
- xuvdb/openvdb_bridge.py +101 -0
- xuvdb/openvdb_file.py +686 -0
- xuvdb/ray.py +132 -0
- xuvdb/runtime.py +17 -0
- xuvdb/tree.py +494 -0
- xuvdb-0.1.0.dist-info/METADATA +208 -0
- xuvdb-0.1.0.dist-info/RECORD +14 -0
- xuvdb-0.1.0.dist-info/WHEEL +5 -0
- xuvdb-0.1.0.dist-info/licenses/LICENSE +201 -0
- xuvdb-0.1.0.dist-info/top_level.txt +1 -0
xuvdb/__init__.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""XUVDB(太虚): genesis' own sparse-volume format, built on the quadrants kernel.
|
|
2
|
+
|
|
3
|
+
The name reads 太虚 (tàixū) - from《庄子·知北游》「不游乎太虚」and 张载《正蒙》「太虚无形,
|
|
4
|
+
气之本体」: the closest Chinese rendering of an unbounded, sparse index domain - unbounded (the
|
|
5
|
+
root hash domain has no extent), formless (unallocated space has no shape; sampling it returns the
|
|
6
|
+
background). The name lives at the concept layer only (docs, logs, visualization labels); API
|
|
7
|
+
identifiers stay English (`xuvdb.prune()`, never `xuvdb.sunyi()`), and the native extension
|
|
8
|
+
`.xuvdb` never uses `.vdb` - every major volume tool parses that suffix as OpenVDB and fails
|
|
9
|
+
opaquely on foreign bytes. Interop with OpenVDB is an explicit, separate export.
|
|
10
|
+
|
|
11
|
+
`VdbGrid` is a mutable sparse voxel tree (dictionary of dense leaf blocks) that fills the gap this
|
|
12
|
+
module was designed for: OpenVDB trees are CPU-bound and NanoVDB grids are GPU read-only, while an
|
|
13
|
+
XUVDB grid can be structurally edited from Python (allocate/fill/stamp/CSG/prune leaves at any time)
|
|
14
|
+
and its packed value buffer can additionally be *written* by quadrants kernels
|
|
15
|
+
(`GpuVolume.write_voxels`) without a host round trip.
|
|
16
|
+
|
|
17
|
+
Interoperability with OpenVDB is first-class and needs no OpenVDB installation:
|
|
18
|
+
|
|
19
|
+
- `write_vdb` / `read_vdb` read and write real `.vdb` files (the published stream format, verified
|
|
20
|
+
against the OpenVDB sources; file version 224, active-mask compression, zip readable, blosc not)
|
|
21
|
+
- `to_openvdb` / `from_openvdb` convert live grids through `pyopenvdb` when the bindings exist
|
|
22
|
+
(Houdini hython, Blender Python, conda-forge)
|
|
23
|
+
- `save` / `load` use the native compact `.xuvdb` format
|
|
24
|
+
|
|
25
|
+
Typical flows against the engine's existing dense solvers:
|
|
26
|
+
|
|
27
|
+
- rigid SDF (落点 1): bake `geom.sdf_val` dense grids with `VdbGrid.from_dense`, edit/CSG them
|
|
28
|
+
sparsely, sample on device via `GpuVolume.sample` / `sdf_normal` and cast rays with
|
|
29
|
+
`ray_surface_hit`
|
|
30
|
+
- MPM / smoke (落点 2, 3): pull density or velocity fields off the dense `qd.field` after a step,
|
|
31
|
+
sparsify, and hand `.vdb` files to Houdini/Blender for volume rendering, or bring Houdini caches
|
|
32
|
+
back in for initial conditions
|
|
33
|
+
- liquid & oil (落点 4, SPH): rasterize particles to fog density volumes with `scatter_particles`
|
|
34
|
+
(unit-integral SPH kernels; one grid per phase gives mixture fractions) or build particle
|
|
35
|
+
level-set surface proxies with `union_spheres`; import Houdini container/initial-surface `.vdb`
|
|
36
|
+
caches as collision/initialization SDFs sampled on device
|
|
37
|
+
|
|
38
|
+
Kernels run on any quadrants backend; call `init_runtime()` once before the first kernel call if
|
|
39
|
+
`gs.init()` has not run (mirroring `genesis.occ`).
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from .gpu import GpuVolume
|
|
43
|
+
from .io import load, save
|
|
44
|
+
from .openvdb_bridge import from_openvdb, to_openvdb
|
|
45
|
+
from .openvdb_file import read_vdb, write_vdb
|
|
46
|
+
from .ray import ray_surface_hit
|
|
47
|
+
from .runtime import init_runtime
|
|
48
|
+
from .tree import Leaf, VdbGrid
|
|
49
|
+
|
|
50
|
+
__version__ = "0.1.0"
|
|
51
|
+
|
|
52
|
+
__all__ = [
|
|
53
|
+
"GpuVolume",
|
|
54
|
+
"Leaf",
|
|
55
|
+
"VdbGrid",
|
|
56
|
+
"from_openvdb",
|
|
57
|
+
"init_runtime",
|
|
58
|
+
"load",
|
|
59
|
+
"ray_surface_hit",
|
|
60
|
+
"read_vdb",
|
|
61
|
+
"save",
|
|
62
|
+
"to_openvdb",
|
|
63
|
+
"write_vdb",
|
|
64
|
+
]
|
xuvdb/gpu.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Host-side packing of a `VdbGrid` into flat buffers consumed by `kernels.py`.
|
|
2
|
+
|
|
3
|
+
A `GpuVolume` is the read-mostly, kernel-writable view of a scalar grid - the XUVDB counterpart of
|
|
4
|
+
a NanoVDB device buffer, except that values are mutable from kernels. Topology is frozen at pack
|
|
5
|
+
time; call `sync_to_host()` to pull kernel-written values back into the host tree, and re-create
|
|
6
|
+
the volume after structural edits (leaf allocation, prune, load).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from . import kernels
|
|
12
|
+
from .runtime import init_runtime
|
|
13
|
+
from .tree import VdbGrid
|
|
14
|
+
|
|
15
|
+
_KEY_OFF = 1 << 19
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _pack_key(kx, ky, kz):
|
|
19
|
+
return (int(kx) + _KEY_OFF) * (1 << 40) + (int(ky) + _KEY_OFF) * (1 << 20) + (int(kz) + _KEY_OFF)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class GpuVolume:
|
|
23
|
+
"""Flat, key-sorted leaf table of a scalar grid plus kernel entry points."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, grid):
|
|
26
|
+
if not isinstance(grid, VdbGrid):
|
|
27
|
+
raise TypeError("GpuVolume wraps an xuvdb.VdbGrid")
|
|
28
|
+
if grid.is_vec or grid.type_code == 1:
|
|
29
|
+
raise TypeError("kernel packing currently supports float32 scalar grids")
|
|
30
|
+
if grid.n_leaves == 0:
|
|
31
|
+
raise ValueError("grid has no leaves")
|
|
32
|
+
init_runtime()
|
|
33
|
+
self.grid = grid
|
|
34
|
+
leaves = grid.leaves()
|
|
35
|
+
keys = np.array([_pack_key(*key) for key in sorted(grid._leaves)], dtype=np.int64)
|
|
36
|
+
if not np.all(keys[:-1] < keys[1:]):
|
|
37
|
+
raise ValueError("leaf keys out of range or duplicated")
|
|
38
|
+
per_leaf = grid.leaf_dim**3
|
|
39
|
+
values = np.empty(len(leaves) * per_leaf, dtype=np.float32)
|
|
40
|
+
for i, leaf in enumerate(leaves):
|
|
41
|
+
values[i * per_leaf:(i + 1) * per_leaf] = leaf.values.ravel() # z-fastest C order
|
|
42
|
+
self.keys = keys
|
|
43
|
+
self.values = values
|
|
44
|
+
self.background = float(grid.background)
|
|
45
|
+
self._dirty = False
|
|
46
|
+
|
|
47
|
+
# ------------------------------------------------------------------ queries
|
|
48
|
+
|
|
49
|
+
def sample(self, points, linear=False):
|
|
50
|
+
"""Sample the packed volume at world-space points (N,3)."""
|
|
51
|
+
points = np.ascontiguousarray(points, dtype=np.float32).reshape(-1, 3)
|
|
52
|
+
out = np.empty(len(points), dtype=np.float32)
|
|
53
|
+
kernel = kernels.kernel_sample_linear if linear else kernels.kernel_sample_nearest
|
|
54
|
+
kernel(
|
|
55
|
+
self.keys, self.values, int(self.grid.n_leaves), int(self.grid.leaf_log2), float(self.background),
|
|
56
|
+
*(float(v) for v in self.grid.origin_world), *(float(v) for v in self.grid.voxel_size),
|
|
57
|
+
points, out,
|
|
58
|
+
)
|
|
59
|
+
return out
|
|
60
|
+
|
|
61
|
+
def sdf_normal(self, points):
|
|
62
|
+
"""Central-difference SDF gradient (world-space, normalized) at world-space points."""
|
|
63
|
+
points = np.ascontiguousarray(points, dtype=np.float32).reshape(-1, 3)
|
|
64
|
+
out = np.empty((len(points), 3), dtype=np.float32)
|
|
65
|
+
kernels.kernel_sdf_normal(
|
|
66
|
+
self.keys, self.values, int(self.grid.n_leaves), int(self.grid.leaf_log2), float(self.background),
|
|
67
|
+
*(float(v) for v in self.grid.origin_world), *(float(v) for v in self.grid.voxel_size),
|
|
68
|
+
points, out,
|
|
69
|
+
)
|
|
70
|
+
return out
|
|
71
|
+
|
|
72
|
+
# ------------------------------------------------------------------ mutation
|
|
73
|
+
|
|
74
|
+
def write_voxels(self, points, new_values):
|
|
75
|
+
"""Overwrite voxel values from kernels; call `sync_to_host` to publish into the tree."""
|
|
76
|
+
points = np.ascontiguousarray(points, dtype=np.float32).reshape(-1, 3)
|
|
77
|
+
new_values = np.ascontiguousarray(new_values, dtype=np.float32).reshape(-1)
|
|
78
|
+
if len(new_values) != len(points):
|
|
79
|
+
raise ValueError("new_values must have one entry per point")
|
|
80
|
+
kernels.kernel_write_voxels(
|
|
81
|
+
self.keys, self.values, int(self.grid.n_leaves), int(self.grid.leaf_log2),
|
|
82
|
+
*(float(v) for v in self.grid.origin_world), *(float(v) for v in self.grid.voxel_size),
|
|
83
|
+
points, new_values,
|
|
84
|
+
)
|
|
85
|
+
self._dirty = True
|
|
86
|
+
return self
|
|
87
|
+
|
|
88
|
+
def sync_to_host(self):
|
|
89
|
+
"""Copy kernel-written values back into the host grid's leaves."""
|
|
90
|
+
per_leaf = self.grid.leaf_dim**3
|
|
91
|
+
for i, leaf in enumerate(self.grid.leaves()):
|
|
92
|
+
leaf.values[...] = self.values[i * per_leaf:(i + 1) * per_leaf].reshape(
|
|
93
|
+
self.grid.leaf_dim, self.grid.leaf_dim, self.grid.leaf_dim
|
|
94
|
+
)
|
|
95
|
+
self._dirty = False
|
|
96
|
+
return self.grid
|
xuvdb/io.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Serialization of XUVDB grids to the native `.xuvdb` binary format.
|
|
2
|
+
|
|
3
|
+
The extension is deliberately not `.vdb`: Houdini, Blender, Cycles and Arnold all parse `.vdb` as
|
|
4
|
+
OpenVDB, and foreign bytes under that suffix fail silently or crash - `save()` refuses such paths
|
|
5
|
+
outright. Interop with OpenVDB is an explicit, separate export (`openvdb_file.write_vdb`), which
|
|
6
|
+
emits a real OpenVDB stream.
|
|
7
|
+
|
|
8
|
+
Format (little-endian throughout; `u` = unsigned, `i` = signed, `f` = IEEE float):
|
|
9
|
+
|
|
10
|
+
magic 5 bytes "XUVDB"
|
|
11
|
+
version u8 (currently 1)
|
|
12
|
+
flags u8 (0)
|
|
13
|
+
n_grids u16
|
|
14
|
+
|
|
15
|
+
per grid:
|
|
16
|
+
name u32 length + utf-8 bytes
|
|
17
|
+
type_code u8 (0 = float32, 1 = float64, 2 = vec3 float32)
|
|
18
|
+
leaf_log2 u8
|
|
19
|
+
class_code u8 (0 unknown, 1 level set, 2 fog volume, 3 staggered)
|
|
20
|
+
reserved u8
|
|
21
|
+
voxel_size f64[3]
|
|
22
|
+
origin_world f64[3]
|
|
23
|
+
background f32 / f64 / f32[3] (per type_code)
|
|
24
|
+
n_leaves u32
|
|
25
|
+
per leaf, sorted by leaf origin (i, then j, then k):
|
|
26
|
+
origin i32[3] (in voxels, of the leaf's (0,0,0) corner)
|
|
27
|
+
mask u64[dim^3 / 64] (active bits, z-fastest bit order)
|
|
28
|
+
values value_type[dim^3] (full dense leaf, z-fastest order)
|
|
29
|
+
|
|
30
|
+
The z-fastest voxel order (linear index `n = x*dim^2 + y*dim + z`) matches the OpenVDB leaf layout,
|
|
31
|
+
so round trips through `openvdb_file.py` are transpose-free. Like OpenVDB leaf nodes, a leaf's full
|
|
32
|
+
dense buffer is stored - inactive values (e.g. a level set's -background interior) survive a save /
|
|
33
|
+
load cycle.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
import struct
|
|
37
|
+
|
|
38
|
+
import numpy as np
|
|
39
|
+
|
|
40
|
+
from .tree import GRID_CLASSES, VdbGrid
|
|
41
|
+
|
|
42
|
+
MAGIC = b"XUVDB"
|
|
43
|
+
VERSION = 1
|
|
44
|
+
|
|
45
|
+
_TYPE_BY_CODE = {0: np.float32, 1: np.float64}
|
|
46
|
+
_CODE_BY_TYPE = {np.dtype(np.float32): 0, np.dtype(np.float64): 1}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _pack_str(s):
|
|
50
|
+
b = s.encode("utf-8")
|
|
51
|
+
return struct.pack("<I", len(b)) + b
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _mask_words(active):
|
|
55
|
+
"""Pack a bool (dim,dim,dim) mask into u64 words, bit n = voxel n (z-fastest C order)."""
|
|
56
|
+
bits = np.packbits(active.reshape(-1), bitorder="little")
|
|
57
|
+
return np.pad(bits, (0, (-len(bits)) % 8)).view(np.uint64)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _mask_from_words(words, count):
|
|
61
|
+
bits = words.view(np.uint8)
|
|
62
|
+
active = np.unpackbits(bits[: (count + 7) // 8], count=count, bitorder="little")
|
|
63
|
+
return active.astype(bool)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def save(path, grids):
|
|
67
|
+
"""Write one or more grids to a `.xuvdb` file."""
|
|
68
|
+
path = str(path)
|
|
69
|
+
if path.lower().endswith(".vdb"):
|
|
70
|
+
raise ValueError(
|
|
71
|
+
f"refusing to write the XUVDB native format to {path!r}: every major volume tool "
|
|
72
|
+
"(Houdini, Blender, Cycles, Arnold) parses '.vdb' as OpenVDB and fails opaquely on "
|
|
73
|
+
"foreign bytes. Use the '.xuvdb' extension, or export a real OpenVDB stream with "
|
|
74
|
+
"write_vdb()."
|
|
75
|
+
)
|
|
76
|
+
grids = list(grids)
|
|
77
|
+
buf = bytearray()
|
|
78
|
+
buf += MAGIC
|
|
79
|
+
buf += struct.pack("<BBI", VERSION, 0, len(grids))
|
|
80
|
+
for grid in grids:
|
|
81
|
+
buf += _pack_str(grid.name)
|
|
82
|
+
bg = np.asarray(grid.background, dtype=np.float64).reshape(-1)
|
|
83
|
+
buf += struct.pack("<BBBB", grid.type_code, grid.leaf_log2, GRID_CLASSES.index(grid.grid_class), 0)
|
|
84
|
+
buf += grid.voxel_size.astype("<f8").tobytes()
|
|
85
|
+
buf += grid.origin_world.astype("<f8").tobytes()
|
|
86
|
+
if grid.type_code == 0:
|
|
87
|
+
buf += struct.pack("<f", float(bg[0]))
|
|
88
|
+
elif grid.type_code == 1:
|
|
89
|
+
buf += struct.pack("<d", float(bg[0]))
|
|
90
|
+
else:
|
|
91
|
+
buf += bg.astype("<f4").tobytes()
|
|
92
|
+
leaves = grid.leaves()
|
|
93
|
+
buf += struct.pack("<I", len(leaves))
|
|
94
|
+
for leaf in leaves:
|
|
95
|
+
buf += leaf.origin.astype("<i4").tobytes()
|
|
96
|
+
mask = _mask_words(leaf.active)
|
|
97
|
+
buf += mask.astype("<u8").tobytes()
|
|
98
|
+
vals = leaf.values.reshape(-1) # z-fastest C order
|
|
99
|
+
if grid.type_code == 1:
|
|
100
|
+
buf += vals.astype("<f8").tobytes()
|
|
101
|
+
else:
|
|
102
|
+
buf += vals.astype("<f4").tobytes()
|
|
103
|
+
with open(path, "wb") as f:
|
|
104
|
+
f.write(buf)
|
|
105
|
+
return len(buf)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class _Reader:
|
|
109
|
+
def __init__(self, data):
|
|
110
|
+
self.data = data
|
|
111
|
+
self.pos = 0
|
|
112
|
+
|
|
113
|
+
def read(self, n):
|
|
114
|
+
if self.pos + n > len(self.data):
|
|
115
|
+
raise ValueError("truncated .xuvdb stream")
|
|
116
|
+
out = self.data[self.pos:self.pos + n]
|
|
117
|
+
self.pos += n
|
|
118
|
+
return out
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def load(path):
|
|
122
|
+
"""Read all grids from a `.xuvdb` file."""
|
|
123
|
+
with open(path, "rb") as f:
|
|
124
|
+
data = f.read()
|
|
125
|
+
r = _Reader(data)
|
|
126
|
+
if r.read(len(MAGIC)) != MAGIC:
|
|
127
|
+
raise ValueError(f"{path} is not an XUVDB file")
|
|
128
|
+
version, flags, n_grids = struct.unpack("<BBI", r.read(6))
|
|
129
|
+
if version != VERSION:
|
|
130
|
+
raise ValueError(f"unsupported XUVDB version {version}")
|
|
131
|
+
if flags != 0:
|
|
132
|
+
raise ValueError(f"unsupported XUVDB flags {flags}")
|
|
133
|
+
grids = []
|
|
134
|
+
for _ in range(n_grids):
|
|
135
|
+
(name_len,) = struct.unpack("<I", r.read(4))
|
|
136
|
+
name = r.read(name_len).decode("utf-8")
|
|
137
|
+
type_code, leaf_log2, class_code, _res = struct.unpack("<BBBB", r.read(4))
|
|
138
|
+
voxel_size = np.frombuffer(r.read(24), dtype="<f8")
|
|
139
|
+
origin_world = np.frombuffer(r.read(24), dtype="<f8")
|
|
140
|
+
if type_code == 0:
|
|
141
|
+
(background,) = struct.unpack("<f", r.read(4))
|
|
142
|
+
elif type_code == 1:
|
|
143
|
+
(background,) = struct.unpack("<d", r.read(8))
|
|
144
|
+
else:
|
|
145
|
+
background = np.frombuffer(r.read(12), dtype="<f4").copy()
|
|
146
|
+
grid = VdbGrid(
|
|
147
|
+
dtype=_TYPE_BY_CODE.get(type_code, None) if type_code != 2
|
|
148
|
+
else np.dtype([("x", np.float32), ("y", np.float32), ("z", np.float32)]),
|
|
149
|
+
background=background,
|
|
150
|
+
voxel_size=voxel_size,
|
|
151
|
+
origin_world=origin_world,
|
|
152
|
+
leaf_log2=leaf_log2,
|
|
153
|
+
name=name,
|
|
154
|
+
grid_class=GRID_CLASSES[class_code],
|
|
155
|
+
)
|
|
156
|
+
(n_leaves,) = struct.unpack("<I", r.read(4))
|
|
157
|
+
dim = 1 << leaf_log2
|
|
158
|
+
words = dim**3 // 64
|
|
159
|
+
per_voxel = 3 if type_code == 2 else 1
|
|
160
|
+
item = np.dtype("<f4") if type_code != 1 else np.dtype("<f8")
|
|
161
|
+
for _leaf in range(n_leaves):
|
|
162
|
+
origin = np.frombuffer(r.read(12), dtype="<i4").astype(np.int64)
|
|
163
|
+
mask_words = np.frombuffer(r.read(words * 8), dtype="<u8")
|
|
164
|
+
active = _mask_from_words(mask_words, dim**3)
|
|
165
|
+
values_flat = np.frombuffer(r.read(dim**3 * per_voxel * item.itemsize), dtype=item)
|
|
166
|
+
leaf = grid._get_or_create_leaf((origin[0] >> leaf_log2, origin[1] >> leaf_log2, origin[2] >> leaf_log2))
|
|
167
|
+
if grid.is_vec:
|
|
168
|
+
leaf.values[...] = values_flat.reshape(dim, dim, dim, 3)
|
|
169
|
+
else:
|
|
170
|
+
leaf.values[...] = values_flat.reshape(dim, dim, dim)
|
|
171
|
+
leaf.active[...] = active.reshape(dim, dim, dim)
|
|
172
|
+
grids.append(grid)
|
|
173
|
+
return grids
|
xuvdb/kernels.py
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""Quadrants kernels for GPU-side sampling and mutation of packed XUVDB volumes.
|
|
2
|
+
|
|
3
|
+
The host packs a scalar grid's leaves into a flat, key-sorted table (`gpu.GpuVolume`): sorted
|
|
4
|
+
`int64` leaf keys, one flat float32 value buffer, and the world-space transform scalars. Kernels
|
|
5
|
+
resolve a world point to its leaf by packing the leaf coordinates into a 60-bit key and binary
|
|
6
|
+
searching the sorted key array - no hash table, so the same buffer is valid on CPU and GPU
|
|
7
|
+
backends.
|
|
8
|
+
|
|
9
|
+
Value mutation (`kernel_write_voxels`) writes the packed buffer in place: unlike NanoVDB grids,
|
|
10
|
+
values are writable from kernels. Topology stays fixed for the lifetime of a packed volume; the
|
|
11
|
+
host re-packs after structural edits (leaf allocation, prune, load).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import quadrants as qd
|
|
15
|
+
|
|
16
|
+
_KEY_OFF = 1 << 19 # per-axis leaf coordinate offset; supports +-524288 leaves per axis
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@qd.func
|
|
20
|
+
def xuvdb_pack_key(kx: qd.i64, ky: qd.i64, kz: qd.i64) -> qd.i64:
|
|
21
|
+
return (kx + qd.i64(_KEY_OFF)) * qd.i64(1 << 40) + (ky + qd.i64(_KEY_OFF)) * qd.i64(1 << 20) + (
|
|
22
|
+
kz + qd.i64(_KEY_OFF)
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@qd.func
|
|
27
|
+
def xuvdb_find_leaf(
|
|
28
|
+
keys: qd.types.ndarray(ndim=1),
|
|
29
|
+
n_leaves: int,
|
|
30
|
+
kx: qd.i64,
|
|
31
|
+
ky: qd.i64,
|
|
32
|
+
kz: qd.i64,
|
|
33
|
+
) -> int:
|
|
34
|
+
key = xuvdb_pack_key(kx, ky, kz)
|
|
35
|
+
lo = 0
|
|
36
|
+
hi = n_leaves
|
|
37
|
+
while lo < hi:
|
|
38
|
+
mid = (lo + hi) // 2
|
|
39
|
+
if keys[mid] < key:
|
|
40
|
+
lo = mid + 1
|
|
41
|
+
else:
|
|
42
|
+
hi = mid
|
|
43
|
+
found = lo
|
|
44
|
+
if lo >= n_leaves or keys[lo] != key:
|
|
45
|
+
found = -1
|
|
46
|
+
return found
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@qd.func
|
|
50
|
+
def xuvdb_voxel_value(
|
|
51
|
+
keys: qd.types.ndarray(ndim=1),
|
|
52
|
+
values: qd.types.ndarray(ndim=1),
|
|
53
|
+
n_leaves: int,
|
|
54
|
+
leaf_log2: int,
|
|
55
|
+
background: float,
|
|
56
|
+
xi: qd.i64,
|
|
57
|
+
yi: qd.i64,
|
|
58
|
+
zi: qd.i64,
|
|
59
|
+
) -> float:
|
|
60
|
+
dim = 1 << leaf_log2
|
|
61
|
+
leaf_idx = xuvdb_find_leaf(keys, n_leaves, xi >> leaf_log2, yi >> leaf_log2, zi >> leaf_log2)
|
|
62
|
+
lx = xi & qd.i64(dim - 1)
|
|
63
|
+
ly = yi & qd.i64(dim - 1)
|
|
64
|
+
lz = zi & qd.i64(dim - 1)
|
|
65
|
+
n = lx * qd.i64(dim * dim) + ly * qd.i64(dim) + lz
|
|
66
|
+
value = background
|
|
67
|
+
if leaf_idx >= 0:
|
|
68
|
+
value = values[leaf_idx * qd.i64(dim * dim * dim) + n]
|
|
69
|
+
return value
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@qd.func
|
|
73
|
+
def xuvdb_sample_nearest(
|
|
74
|
+
keys: qd.types.ndarray(ndim=1),
|
|
75
|
+
values: qd.types.ndarray(ndim=1),
|
|
76
|
+
n_leaves: int,
|
|
77
|
+
leaf_log2: int,
|
|
78
|
+
background: float,
|
|
79
|
+
o_x: float,
|
|
80
|
+
o_y: float,
|
|
81
|
+
o_z: float,
|
|
82
|
+
s_x: float,
|
|
83
|
+
s_y: float,
|
|
84
|
+
s_z: float,
|
|
85
|
+
x: float,
|
|
86
|
+
y: float,
|
|
87
|
+
z: float,
|
|
88
|
+
) -> float:
|
|
89
|
+
xi = qd.i64(qd.floor((x - o_x) / s_x + 0.5))
|
|
90
|
+
yi = qd.i64(qd.floor((y - o_y) / s_y + 0.5))
|
|
91
|
+
zi = qd.i64(qd.floor((z - o_z) / s_z + 0.5))
|
|
92
|
+
return xuvdb_voxel_value(keys, values, n_leaves, leaf_log2, background, xi, yi, zi)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@qd.func
|
|
96
|
+
def xuvdb_sample_linear(
|
|
97
|
+
keys: qd.types.ndarray(ndim=1),
|
|
98
|
+
values: qd.types.ndarray(ndim=1),
|
|
99
|
+
n_leaves: int,
|
|
100
|
+
leaf_log2: int,
|
|
101
|
+
background: float,
|
|
102
|
+
o_x: float,
|
|
103
|
+
o_y: float,
|
|
104
|
+
o_z: float,
|
|
105
|
+
s_x: float,
|
|
106
|
+
s_y: float,
|
|
107
|
+
s_z: float,
|
|
108
|
+
x: float,
|
|
109
|
+
y: float,
|
|
110
|
+
z: float,
|
|
111
|
+
) -> float:
|
|
112
|
+
cx = (x - o_x) / s_x
|
|
113
|
+
cy = (y - o_y) / s_y
|
|
114
|
+
cz = (z - o_z) / s_z
|
|
115
|
+
bx = qd.i64(qd.floor(cx))
|
|
116
|
+
by = qd.i64(qd.floor(cy))
|
|
117
|
+
bz = qd.i64(qd.floor(cz))
|
|
118
|
+
fx = cx - qd.floor(cx)
|
|
119
|
+
fy = cy - qd.floor(cy)
|
|
120
|
+
fz = cz - qd.floor(cz)
|
|
121
|
+
out = qd.f32(0.0)
|
|
122
|
+
for dx in range(2):
|
|
123
|
+
wx = fx if dx == 1 else 1.0 - fx
|
|
124
|
+
for dy in range(2):
|
|
125
|
+
wy = fy if dy == 1 else 1.0 - fy
|
|
126
|
+
for dz in range(2):
|
|
127
|
+
wz = fz if dz == 1 else 1.0 - fz
|
|
128
|
+
out += qd.f32(
|
|
129
|
+
wx * wy * wz
|
|
130
|
+
) * qd.f32(
|
|
131
|
+
xuvdb_voxel_value(keys, values, n_leaves, leaf_log2, background,
|
|
132
|
+
bx + qd.i64(dx), by + qd.i64(dy), bz + qd.i64(dz))
|
|
133
|
+
)
|
|
134
|
+
return out
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@qd.kernel
|
|
138
|
+
def kernel_sample_nearest(
|
|
139
|
+
keys: qd.types.ndarray(ndim=1),
|
|
140
|
+
values: qd.types.ndarray(ndim=1),
|
|
141
|
+
n_leaves: int,
|
|
142
|
+
leaf_log2: int,
|
|
143
|
+
background: float,
|
|
144
|
+
o_x: float,
|
|
145
|
+
o_y: float,
|
|
146
|
+
o_z: float,
|
|
147
|
+
s_x: float,
|
|
148
|
+
s_y: float,
|
|
149
|
+
s_z: float,
|
|
150
|
+
points: qd.types.ndarray(ndim=2),
|
|
151
|
+
out: qd.types.ndarray(ndim=1),
|
|
152
|
+
):
|
|
153
|
+
for i in range(points.shape[0]):
|
|
154
|
+
out[i] = xuvdb_sample_nearest(
|
|
155
|
+
keys, values, n_leaves, leaf_log2, background, o_x, o_y, o_z, s_x, s_y, s_z,
|
|
156
|
+
points[i, 0], points[i, 1], points[i, 2],
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@qd.kernel
|
|
161
|
+
def kernel_sample_linear(
|
|
162
|
+
keys: qd.types.ndarray(ndim=1),
|
|
163
|
+
values: qd.types.ndarray(ndim=1),
|
|
164
|
+
n_leaves: int,
|
|
165
|
+
leaf_log2: int,
|
|
166
|
+
background: float,
|
|
167
|
+
o_x: float,
|
|
168
|
+
o_y: float,
|
|
169
|
+
o_z: float,
|
|
170
|
+
s_x: float,
|
|
171
|
+
s_y: float,
|
|
172
|
+
s_z: float,
|
|
173
|
+
points: qd.types.ndarray(ndim=2),
|
|
174
|
+
out: qd.types.ndarray(ndim=1),
|
|
175
|
+
):
|
|
176
|
+
for i in range(points.shape[0]):
|
|
177
|
+
out[i] = xuvdb_sample_linear(
|
|
178
|
+
keys, values, n_leaves, leaf_log2, background, o_x, o_y, o_z, s_x, s_y, s_z,
|
|
179
|
+
points[i, 0], points[i, 1], points[i, 2],
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@qd.kernel
|
|
184
|
+
def kernel_sdf_normal(
|
|
185
|
+
keys: qd.types.ndarray(ndim=1),
|
|
186
|
+
values: qd.types.ndarray(ndim=1),
|
|
187
|
+
n_leaves: int,
|
|
188
|
+
leaf_log2: int,
|
|
189
|
+
background: float,
|
|
190
|
+
o_x: float,
|
|
191
|
+
o_y: float,
|
|
192
|
+
o_z: float,
|
|
193
|
+
s_x: float,
|
|
194
|
+
s_y: float,
|
|
195
|
+
s_z: float,
|
|
196
|
+
points: qd.types.ndarray(ndim=2),
|
|
197
|
+
out: qd.types.ndarray(ndim=2),
|
|
198
|
+
):
|
|
199
|
+
"""Central-difference surface normal of the SDF field at each point (world-space, normalized)."""
|
|
200
|
+
for i in range(points.shape[0]):
|
|
201
|
+
x = points[i, 0]
|
|
202
|
+
y = points[i, 1]
|
|
203
|
+
z = points[i, 2]
|
|
204
|
+
nx = xuvdb_sample_linear(keys, values, n_leaves, leaf_log2, background, o_x, o_y, o_z, s_x, s_y, s_z,
|
|
205
|
+
x + s_x, y, z) - xuvdb_sample_linear(keys, values, n_leaves, leaf_log2,
|
|
206
|
+
background, o_x, o_y, o_z, s_x, s_y, s_z,
|
|
207
|
+
x - s_x, y, z)
|
|
208
|
+
ny = xuvdb_sample_linear(keys, values, n_leaves, leaf_log2, background, o_x, o_y, o_z, s_x, s_y, s_z,
|
|
209
|
+
x, y + s_y, z) - xuvdb_sample_linear(keys, values, n_leaves, leaf_log2,
|
|
210
|
+
background, o_x, o_y, o_z, s_x, s_y, s_z,
|
|
211
|
+
x, y - s_y, z)
|
|
212
|
+
nz = xuvdb_sample_linear(keys, values, n_leaves, leaf_log2, background, o_x, o_y, o_z, s_x, s_y, s_z,
|
|
213
|
+
x, y, z + s_z) - xuvdb_sample_linear(keys, values, n_leaves, leaf_log2,
|
|
214
|
+
background, o_x, o_y, o_z, s_x, s_y, s_z,
|
|
215
|
+
x, y, z - s_z)
|
|
216
|
+
norm = qd.sqrt(nx * nx + ny * ny + nz * nz)
|
|
217
|
+
if norm > 1e-12:
|
|
218
|
+
out[i, 0] = nx / norm
|
|
219
|
+
out[i, 1] = ny / norm
|
|
220
|
+
out[i, 2] = nz / norm
|
|
221
|
+
else:
|
|
222
|
+
out[i, 0] = 0.0
|
|
223
|
+
out[i, 1] = 0.0
|
|
224
|
+
out[i, 2] = 0.0
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@qd.kernel
|
|
228
|
+
def kernel_write_voxels(
|
|
229
|
+
keys: qd.types.ndarray(ndim=1),
|
|
230
|
+
values: qd.types.ndarray(ndim=1),
|
|
231
|
+
n_leaves: int,
|
|
232
|
+
leaf_log2: int,
|
|
233
|
+
o_x: float,
|
|
234
|
+
o_y: float,
|
|
235
|
+
o_z: float,
|
|
236
|
+
s_x: float,
|
|
237
|
+
s_y: float,
|
|
238
|
+
s_z: float,
|
|
239
|
+
points: qd.types.ndarray(ndim=2),
|
|
240
|
+
new_values: qd.types.ndarray(ndim=1),
|
|
241
|
+
):
|
|
242
|
+
"""Overwrite the value of the voxel nearest each point (in-place; topology unchanged).
|
|
243
|
+
|
|
244
|
+
The active mask is host-side state and is not touched: deactivating/reactivating voxels or
|
|
245
|
+
allocating leaves requires re-packing on the host.
|
|
246
|
+
"""
|
|
247
|
+
dim = 1 << leaf_log2
|
|
248
|
+
for i in range(points.shape[0]):
|
|
249
|
+
xi = qd.i64(qd.floor((points[i, 0] - o_x) / s_x + 0.5))
|
|
250
|
+
yi = qd.i64(qd.floor((points[i, 1] - o_y) / s_y + 0.5))
|
|
251
|
+
zi = qd.i64(qd.floor((points[i, 2] - o_z) / s_z + 0.5))
|
|
252
|
+
leaf_idx = xuvdb_find_leaf(keys, n_leaves, xi >> leaf_log2, yi >> leaf_log2, zi >> leaf_log2)
|
|
253
|
+
if leaf_idx >= 0:
|
|
254
|
+
lx = xi & qd.i64(dim - 1)
|
|
255
|
+
ly = yi & qd.i64(dim - 1)
|
|
256
|
+
lz = zi & qd.i64(dim - 1)
|
|
257
|
+
n = lx * qd.i64(dim * dim) + ly * qd.i64(dim) + lz
|
|
258
|
+
values[leaf_idx * qd.i64(dim * dim * dim) + n] = new_values[i]
|
xuvdb/openvdb_bridge.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Optional in-memory bridge to `pyopenvdb` (the OpenVDB Python bindings).
|
|
2
|
+
|
|
3
|
+
Used when the bindings are importable (Houdini's hython, Blender's bundled Python, conda-forge).
|
|
4
|
+
The file-level interop path that needs no bindings at all is `openvdb_file.py`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
import pyopenvdb
|
|
11
|
+
except ImportError: # the module stays importable; the converters raise on use
|
|
12
|
+
pyopenvdb = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _require_bindings():
|
|
16
|
+
if pyopenvdb is None:
|
|
17
|
+
raise ImportError(
|
|
18
|
+
"pyopenvdb is not installed; use xuvdb.openvdb_file.read_vdb/write_vdb for "
|
|
19
|
+
"file-level interop instead"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _openvdb_grid_class(name):
|
|
24
|
+
_require_bindings()
|
|
25
|
+
return {
|
|
26
|
+
"unknown": pyopenvdb.GridClass.UNKNOWN,
|
|
27
|
+
"level set": pyopenvdb.GridClass.LEVEL_SET,
|
|
28
|
+
"fog volume": pyopenvdb.GridClass.FOG_VOLUME,
|
|
29
|
+
"staggered": pyopenvdb.GridClass.STAGGERED,
|
|
30
|
+
}[name]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _xuvdb_grid_class(ovdb_value):
|
|
34
|
+
_require_bindings()
|
|
35
|
+
for name, value in (
|
|
36
|
+
("unknown", pyopenvdb.GridClass.UNKNOWN),
|
|
37
|
+
("level set", pyopenvdb.GridClass.LEVEL_SET),
|
|
38
|
+
("fog volume", pyopenvdb.GridClass.FOG_VOLUME),
|
|
39
|
+
("staggered", pyopenvdb.GridClass.STAGGERED),
|
|
40
|
+
):
|
|
41
|
+
if int(value) == int(ovdb_value):
|
|
42
|
+
return name
|
|
43
|
+
return "unknown"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def to_openvdb(grid):
|
|
47
|
+
"""Convert a `VdbGrid` into a live `pyopenvdb` grid (FloatGrid / DoubleGrid / Vec3SGrid)."""
|
|
48
|
+
_require_bindings()
|
|
49
|
+
if grid.is_vec:
|
|
50
|
+
ovdb = pyopenvdb.Vec3SGrid(background=tuple(float(v) for v in grid.background))
|
|
51
|
+
elif grid.type_code == 1:
|
|
52
|
+
ovdb = pyopenvdb.DoubleGrid(background=float(grid.background))
|
|
53
|
+
else:
|
|
54
|
+
ovdb = pyopenvdb.FloatGrid(background=float(grid.background))
|
|
55
|
+
ovdb.name = grid.name
|
|
56
|
+
ovdb.gridClass = _openvdb_grid_class(grid.grid_class)
|
|
57
|
+
|
|
58
|
+
matrix = np.eye(4)
|
|
59
|
+
matrix[:3, :3] = np.diag(grid.voxel_size)
|
|
60
|
+
matrix[:3, 3] = grid.origin_world
|
|
61
|
+
ovdb.transform = pyopenvdb.createLinearTransform(matrix=matrix)
|
|
62
|
+
|
|
63
|
+
acc = ovdb.getAccessor()
|
|
64
|
+
for coord, value in grid.iter_voxels():
|
|
65
|
+
acc.setValueOn(coord, tuple(float(v) for v in value) if grid.is_vec else float(value))
|
|
66
|
+
return ovdb
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def from_openvdb(ovdb_grid, leaf_log2=3):
|
|
70
|
+
"""Convert a `pyopenvdb` grid into a `VdbGrid` (active tiles are materialized into leaves)."""
|
|
71
|
+
_require_bindings()
|
|
72
|
+
from .tree import VdbGrid
|
|
73
|
+
|
|
74
|
+
value_type = ovdb_grid.valueType
|
|
75
|
+
if value_type == "vec3s":
|
|
76
|
+
dtype = np.dtype([("x", np.float32), ("y", np.float32), ("z", np.float32)])
|
|
77
|
+
background = np.asarray(ovdb_grid.backgroundValue, dtype=np.float64).reshape(3)
|
|
78
|
+
elif value_type == "double":
|
|
79
|
+
dtype = np.float64
|
|
80
|
+
background = float(ovdb_grid.backgroundValue)
|
|
81
|
+
else:
|
|
82
|
+
dtype = np.float32
|
|
83
|
+
background = float(ovdb_grid.backgroundValue)
|
|
84
|
+
|
|
85
|
+
matrix = np.array(ovdb_grid.transform.matrix)
|
|
86
|
+
voxel_size = np.array([matrix[0, 0], matrix[1, 1], matrix[2, 2]])
|
|
87
|
+
origin_world = np.array([matrix[0, 3], matrix[1, 3], matrix[2, 3]])
|
|
88
|
+
grid_class = _xuvdb_grid_class(ovdb_grid.gridClass)
|
|
89
|
+
|
|
90
|
+
grid = VdbGrid(dtype=dtype, background=background, voxel_size=voxel_size, origin_world=origin_world,
|
|
91
|
+
leaf_log2=leaf_log2, name=ovdb_grid.name or "grid", grid_class=grid_class)
|
|
92
|
+
|
|
93
|
+
acc = ovdb_grid.getConstAccessor()
|
|
94
|
+
for bbox in ovdb_grid.iterActiveTiles():
|
|
95
|
+
lo, hi = np.array(bbox.min), np.array(bbox.max)
|
|
96
|
+
value = acc.getValue(tuple(int(v) for v in lo))
|
|
97
|
+
grid.fill_box(lo, hi, value, active=True)
|
|
98
|
+
for coord in ovdb_grid.iterActiveVoxels():
|
|
99
|
+
value = acc.getValue(tuple(coord))
|
|
100
|
+
grid.set_value(tuple(coord), tuple(float(v) for v in value) if grid.is_vec else float(value))
|
|
101
|
+
return grid
|