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. """ # 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 = 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()) # 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 = 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, 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]], 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() # 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]], 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() # 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