| | from typing import * |
| | import numpy as np |
| | import torch |
| | from .. import _C |
| | from flex_gemm.kernels import cuda as flexgemm_kernels |
| |
|
| | __all__ = [ |
| | "mesh_to_flexible_dual_grid", |
| | "flexible_dual_grid_to_mesh", |
| | ] |
| |
|
| | @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 dual contouring algorithm. |
| | boundary_weight (float): The weight of the boundary term in the dual contouring algorithm. |
| | regularization_weight (float): The weight of the regularization term in the dual contouring algorithm. |
| | 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. |
| | """ |
| | |
| | |
| | vertices = vertices.float() |
| | faces = faces.int() |
| |
|
| | |
| | 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)}" |
| |
|
| | |
| | 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() |
| |
|
| | |
| | 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() |
| | |
| | |
| | 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. |
| | """ |
| | |
| | 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]], |
| | [[0, 0, 0], [1, 0, 0], [1, 0, 1], [0, 0, 1]], |
| | [[0, 0, 0], [0, 1, 0], [1, 1, 0], [1, 0, 0]], |
| | ], 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) |
| |
|
| | |
| | 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)}" |
| |
|
| | |
| | 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)}" |
| |
|
| | |
| | N = dual_vertices.shape[0] |
| | mesh_vertices = (coords.float() + dual_vertices) / (2 * N) - 0.5 |
| |
|
| | |
| | hashmap = torch.full((2 * int(2 * N),), 0xffffffff, dtype=torch.uint32, device=coords.device) |
| | flexgemm_kernels.hashmap_insert_3d_idx_as_val_cuda(hashmap, torch.cat([torch.zeros_like(coords[:, :1]), coords], dim=-1), *grid_size.tolist()) |
| |
|
| | |
| | edge_neighbor_voxel = coords.reshape(N, 1, 1, 3) + flexible_dual_grid_to_mesh.edge_neighbor_voxel_offset |
| | connected_voxel = edge_neighbor_voxel[intersected_flag] |
| | 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 = flexgemm_kernels.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 = quad_indices.shape[0] |
| |
|
| | |
| | if not train: |
| | mesh_vertices = (coords.float() + dual_vertices) * voxel_size + aabb[0].reshape(1, 3) |
| | if split_weight is None: |
| | |
| | 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]], dim=1) |
| | 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]], dim=1) |
| | normals0 = normals0 / torch.norm(normals0, dim=1, keepdim=True) |
| | normals1 = normals1 / torch.norm(normals1, dim=1, keepdim=True) |
| | align0 = (normals0 * normals1).sum(dim=1, keepdim=True).abs() |
| | |
| | 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]], dim=1) |
| | 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]], dim=1) |
| | normals0 = normals0 / torch.norm(normals0, dim=1, keepdim=True) |
| | normals1 = normals1 / torch.norm(normals1, dim=1, keepdim=True) |
| | align1 = (normals0 * normals1).sum(dim=1, keepdim=True).abs() |
| | |
| | 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 |
| |
|