witwin 0.0.1__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.
- witwin/core/__init__.py +68 -0
- witwin/core/geometry/__init__.py +24 -0
- witwin/core/geometry/base.py +199 -0
- witwin/core/geometry/mesh.py +426 -0
- witwin/core/geometry/mesh_sdf.py +761 -0
- witwin/core/geometry/primitives.py +511 -0
- witwin/core/geometry/smpl.py +229 -0
- witwin/core/material.py +145 -0
- witwin/core/math.py +113 -0
- witwin/core/mesh_sdf.slang +609 -0
- witwin/core/scene.py +38 -0
- witwin/core/scene_to_mitsuba.py +138 -0
- witwin-0.0.1.dist-info/METADATA +21 -0
- witwin-0.0.1.dist-info/RECORD +15 -0
- witwin-0.0.1.dist-info/WHEEL +4 -0
witwin/core/__init__.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Witwin Core - Shared geometry, materials, and scene utilities."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.0.1"
|
|
4
|
+
|
|
5
|
+
from .material import (
|
|
6
|
+
FrequencyMaterialSample,
|
|
7
|
+
Material,
|
|
8
|
+
MaterialCapabilities,
|
|
9
|
+
MaterialSpec,
|
|
10
|
+
StaticMaterialSample,
|
|
11
|
+
Structure,
|
|
12
|
+
)
|
|
13
|
+
from .geometry import (
|
|
14
|
+
Box,
|
|
15
|
+
Cone,
|
|
16
|
+
Cylinder,
|
|
17
|
+
Ellipsoid,
|
|
18
|
+
Geometry,
|
|
19
|
+
GeometryBase,
|
|
20
|
+
HollowBox,
|
|
21
|
+
Mesh,
|
|
22
|
+
Prism,
|
|
23
|
+
Pyramid,
|
|
24
|
+
SMPLBody,
|
|
25
|
+
Sphere,
|
|
26
|
+
Torus,
|
|
27
|
+
)
|
|
28
|
+
from .math import (
|
|
29
|
+
quat_from_euler,
|
|
30
|
+
quat_identity,
|
|
31
|
+
quat_multiply,
|
|
32
|
+
quat_to_rotation_matrix,
|
|
33
|
+
quat_to_rotation_matrix_np,
|
|
34
|
+
)
|
|
35
|
+
from .scene import SceneBase
|
|
36
|
+
from .scene_to_mitsuba import (
|
|
37
|
+
MitsubaRenderable,
|
|
38
|
+
MitsubaSceneHandle,
|
|
39
|
+
build_mitsuba_scene,
|
|
40
|
+
create_mitsuba_mesh,
|
|
41
|
+
update_mitsuba_scene_vertices,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"GeometryBase",
|
|
46
|
+
"Box", "Sphere", "Cylinder", "Cone", "Ellipsoid",
|
|
47
|
+
"Pyramid", "Prism", "Torus", "HollowBox",
|
|
48
|
+
"Material",
|
|
49
|
+
"MaterialCapabilities",
|
|
50
|
+
"MaterialSpec",
|
|
51
|
+
"Mesh",
|
|
52
|
+
"SMPLBody",
|
|
53
|
+
"FrequencyMaterialSample",
|
|
54
|
+
"Geometry",
|
|
55
|
+
"SceneBase",
|
|
56
|
+
"StaticMaterialSample",
|
|
57
|
+
"Structure",
|
|
58
|
+
"MitsubaRenderable",
|
|
59
|
+
"MitsubaSceneHandle",
|
|
60
|
+
"quat_from_euler",
|
|
61
|
+
"quat_identity",
|
|
62
|
+
"quat_multiply",
|
|
63
|
+
"quat_to_rotation_matrix",
|
|
64
|
+
"quat_to_rotation_matrix_np",
|
|
65
|
+
"create_mitsuba_mesh",
|
|
66
|
+
"build_mitsuba_scene",
|
|
67
|
+
"update_mitsuba_scene_vertices",
|
|
68
|
+
]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Shared differentiable geometry package."""
|
|
2
|
+
|
|
3
|
+
from .base import GeometryBase
|
|
4
|
+
from .mesh import Mesh
|
|
5
|
+
from .primitives import Box, Cone, Cylinder, Ellipsoid, HollowBox, Prism, Pyramid, Sphere, Torus
|
|
6
|
+
from .smpl import SMPLBody
|
|
7
|
+
|
|
8
|
+
Geometry = Box | Sphere | Cylinder | Cone | Ellipsoid | Pyramid | Prism | Torus | HollowBox | Mesh | SMPLBody
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"GeometryBase",
|
|
12
|
+
"Box",
|
|
13
|
+
"Sphere",
|
|
14
|
+
"Cylinder",
|
|
15
|
+
"Cone",
|
|
16
|
+
"Ellipsoid",
|
|
17
|
+
"Pyramid",
|
|
18
|
+
"Prism",
|
|
19
|
+
"Torus",
|
|
20
|
+
"HollowBox",
|
|
21
|
+
"Mesh",
|
|
22
|
+
"SMPLBody",
|
|
23
|
+
"Geometry",
|
|
24
|
+
]
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Shared geometry base classes and tensor helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import torch
|
|
7
|
+
|
|
8
|
+
from ..math import (
|
|
9
|
+
quat_from_euler,
|
|
10
|
+
quat_identity,
|
|
11
|
+
quat_to_rotation_matrix,
|
|
12
|
+
quat_to_rotation_matrix_np,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _as_position(value, *, device=None) -> torch.Tensor:
|
|
17
|
+
if isinstance(value, torch.Tensor):
|
|
18
|
+
return value.to(device=device, dtype=torch.float32)
|
|
19
|
+
return torch.tensor([float(v) for v in value], dtype=torch.float32, device=device)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _as_rotation(value, *, device=None) -> torch.Tensor:
|
|
23
|
+
"""Accept quaternion (4,), Euler tuple (3,), or None -> identity."""
|
|
24
|
+
if value is None:
|
|
25
|
+
return quat_identity(device=device)
|
|
26
|
+
if isinstance(value, torch.Tensor):
|
|
27
|
+
if value.shape == (4,):
|
|
28
|
+
return value.to(device=device, dtype=torch.float32)
|
|
29
|
+
if value.shape == (3,):
|
|
30
|
+
return quat_from_euler(value[0], value[1], value[2], device=device)
|
|
31
|
+
raise ValueError(f"rotation tensor must be shape (4,) quaternion or (3,) Euler, got {value.shape}")
|
|
32
|
+
seq = list(value)
|
|
33
|
+
if len(seq) == 4:
|
|
34
|
+
return torch.tensor([float(v) for v in seq], dtype=torch.float32, device=device)
|
|
35
|
+
if len(seq) == 3:
|
|
36
|
+
return quat_from_euler(float(seq[0]), float(seq[1]), float(seq[2]), device=device)
|
|
37
|
+
raise ValueError(f"rotation must have 3 (Euler) or 4 (quaternion) elements, got {len(seq)}")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _as_scalar(value, *, device=None) -> torch.Tensor:
|
|
41
|
+
if isinstance(value, torch.Tensor):
|
|
42
|
+
return value.to(device=device, dtype=torch.float32).reshape(())
|
|
43
|
+
return torch.tensor(float(value), dtype=torch.float32, device=device)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _as_vec3(value, *, device=None) -> torch.Tensor:
|
|
47
|
+
if isinstance(value, torch.Tensor):
|
|
48
|
+
return value.to(device=device, dtype=torch.float32)
|
|
49
|
+
if isinstance(value, (int, float)):
|
|
50
|
+
scalar = float(value)
|
|
51
|
+
return torch.tensor([scalar, scalar, scalar], dtype=torch.float32, device=device)
|
|
52
|
+
return torch.tensor([float(v) for v in value], dtype=torch.float32, device=device)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _rotate_coords(xx, yy, zz, position: torch.Tensor, rotation: torch.Tensor):
|
|
56
|
+
"""Translate + inverse-rotate 3D grid coordinates into local frame."""
|
|
57
|
+
dx = xx - position[0]
|
|
58
|
+
dy = yy - position[1]
|
|
59
|
+
dz = zz - position[2]
|
|
60
|
+
rotation_matrix = quat_to_rotation_matrix(rotation)
|
|
61
|
+
rotation_inverse = rotation_matrix.T
|
|
62
|
+
dx_rot = rotation_inverse[0, 0] * dx + rotation_inverse[0, 1] * dy + rotation_inverse[0, 2] * dz
|
|
63
|
+
dy_rot = rotation_inverse[1, 0] * dx + rotation_inverse[1, 1] * dy + rotation_inverse[1, 2] * dz
|
|
64
|
+
dz_rot = rotation_inverse[2, 0] * dx + rotation_inverse[2, 1] * dy + rotation_inverse[2, 2] * dz
|
|
65
|
+
return dx_rot, dy_rot, dz_rot
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _axial_split(dx, dy, dz, axis: str):
|
|
69
|
+
"""Return (axial, radial_a, radial_b) for a named axis."""
|
|
70
|
+
if axis == "z":
|
|
71
|
+
return dz, dx, dy
|
|
72
|
+
if axis == "y":
|
|
73
|
+
return dy, dx, dz
|
|
74
|
+
return dx, dy, dz
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _apply_rotation_np(vertices: np.ndarray, rotation_matrix: np.ndarray) -> np.ndarray:
|
|
78
|
+
"""Apply a (3,3) rotation matrix to (N,3) vertices."""
|
|
79
|
+
return (vertices @ rotation_matrix.T).astype(np.float32)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _constant_tensor(data, *, device, dtype=torch.float32) -> torch.Tensor:
|
|
83
|
+
return torch.tensor(data, dtype=dtype, device=device)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _remap_axis_torch(vertices: torch.Tensor, axis: str) -> torch.Tensor:
|
|
87
|
+
"""Remap local z-up coordinates to the requested axis."""
|
|
88
|
+
if axis == "y":
|
|
89
|
+
return vertices[:, [0, 2, 1]]
|
|
90
|
+
if axis == "x":
|
|
91
|
+
return vertices[:, [2, 1, 0]]
|
|
92
|
+
return vertices
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _coordinate_spacing(coord: torch.Tensor, dim: int) -> torch.Tensor | None:
|
|
96
|
+
if not isinstance(coord, torch.Tensor) or coord.ndim <= dim or coord.shape[dim] <= 1:
|
|
97
|
+
return None
|
|
98
|
+
delta = torch.diff(coord, dim=dim).abs()
|
|
99
|
+
positive = delta[delta > 0]
|
|
100
|
+
if positive.numel() == 0:
|
|
101
|
+
return None
|
|
102
|
+
return positive.min()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _default_beta_from_coords(xx, yy, zz, *, reference: torch.Tensor) -> torch.Tensor:
|
|
106
|
+
spacings = [
|
|
107
|
+
spacing
|
|
108
|
+
for spacing in (
|
|
109
|
+
_coordinate_spacing(xx, 0),
|
|
110
|
+
_coordinate_spacing(yy, 1),
|
|
111
|
+
_coordinate_spacing(zz, 2),
|
|
112
|
+
)
|
|
113
|
+
if spacing is not None
|
|
114
|
+
]
|
|
115
|
+
if not spacings:
|
|
116
|
+
return reference.new_tensor(1.0e-3)
|
|
117
|
+
|
|
118
|
+
spacing = spacings[0].to(device=reference.device, dtype=reference.dtype)
|
|
119
|
+
for candidate in spacings[1:]:
|
|
120
|
+
spacing = torch.minimum(spacing, candidate.to(device=reference.device, dtype=reference.dtype))
|
|
121
|
+
return spacing * 0.05
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _resolve_beta(beta, xx, yy, zz, *, reference: torch.Tensor) -> torch.Tensor:
|
|
125
|
+
if beta is None:
|
|
126
|
+
resolved = _default_beta_from_coords(xx, yy, zz, reference=reference)
|
|
127
|
+
else:
|
|
128
|
+
resolved = torch.as_tensor(beta, device=reference.device, dtype=reference.dtype)
|
|
129
|
+
return torch.clamp(resolved, min=torch.finfo(reference.dtype).eps)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def occupancy_from_signed_distance(signed_distance: torch.Tensor, *, xx, yy, zz, offset=0.0, beta=None) -> torch.Tensor:
|
|
133
|
+
offset_tensor = torch.as_tensor(offset, device=signed_distance.device, dtype=signed_distance.dtype)
|
|
134
|
+
beta_tensor = _resolve_beta(beta, xx, yy, zz, reference=signed_distance)
|
|
135
|
+
occupancy = 0.5 * (1.0 - torch.tanh((signed_distance - offset_tensor) / beta_tensor))
|
|
136
|
+
return occupancy.clamp(0.0, 1.0)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class GeometryBase:
|
|
140
|
+
"""Base class for all geometry primitives."""
|
|
141
|
+
|
|
142
|
+
kind: str = "base"
|
|
143
|
+
|
|
144
|
+
def __init__(self, position=(0, 0, 0), rotation=None, *, device=None):
|
|
145
|
+
self.position: torch.Tensor = _as_position(position, device=device)
|
|
146
|
+
self.rotation: torch.Tensor = _as_rotation(rotation, device=device)
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def device(self):
|
|
150
|
+
return self.position.device
|
|
151
|
+
|
|
152
|
+
def _local_coords(self, xx, yy, zz):
|
|
153
|
+
return _rotate_coords(xx, yy, zz, self.position, self.rotation)
|
|
154
|
+
|
|
155
|
+
def _rotation_matrix_np(self) -> np.ndarray:
|
|
156
|
+
return quat_to_rotation_matrix_np(self.rotation)
|
|
157
|
+
|
|
158
|
+
@staticmethod
|
|
159
|
+
def _validate_axis(axis: str) -> str:
|
|
160
|
+
axis_name = str(axis).lower()
|
|
161
|
+
if axis_name not in {"x", "y", "z"}:
|
|
162
|
+
raise ValueError("axis must be 'x', 'y', or 'z'.")
|
|
163
|
+
return axis_name
|
|
164
|
+
|
|
165
|
+
def _apply_axis_transform(self, vertices: torch.Tensor, axis: str) -> torch.Tensor:
|
|
166
|
+
return _remap_axis_torch(vertices, axis)
|
|
167
|
+
|
|
168
|
+
def _transform_mesh_verts(self, vertices: torch.Tensor) -> torch.Tensor:
|
|
169
|
+
if not isinstance(vertices, torch.Tensor):
|
|
170
|
+
vertices = torch.as_tensor(vertices, dtype=torch.float32, device=self.device)
|
|
171
|
+
rotation_matrix = quat_to_rotation_matrix(self.rotation.to(device=vertices.device, dtype=vertices.dtype))
|
|
172
|
+
position = self.position.to(device=vertices.device, dtype=vertices.dtype)
|
|
173
|
+
return vertices @ rotation_matrix.T + position
|
|
174
|
+
|
|
175
|
+
def with_material(self, material, **kwargs):
|
|
176
|
+
from ..material import Structure
|
|
177
|
+
|
|
178
|
+
return Structure(geometry=self, material=material, **kwargs)
|
|
179
|
+
|
|
180
|
+
def signed_distance(self, xx, yy, zz):
|
|
181
|
+
raise NotImplementedError
|
|
182
|
+
|
|
183
|
+
def to_mask(self, xx, yy, zz, offset=0.0, beta=None):
|
|
184
|
+
signed_distance = self.signed_distance(xx, yy, zz)
|
|
185
|
+
return occupancy_from_signed_distance(
|
|
186
|
+
signed_distance,
|
|
187
|
+
xx=xx,
|
|
188
|
+
yy=yy,
|
|
189
|
+
zz=zz,
|
|
190
|
+
offset=offset,
|
|
191
|
+
beta=beta,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
def to_mesh(self, segments=16):
|
|
195
|
+
raise NotImplementedError
|
|
196
|
+
|
|
197
|
+
def __repr__(self):
|
|
198
|
+
fields = ", ".join(f"{key}={value}" for key, value in self.__dict__.items() if not key.startswith("_"))
|
|
199
|
+
return f"{type(self).__name__}({fields})"
|