from typing import Callable, List, Union import torch from torch.autograd import Function from torch.cuda.amp import custom_bwd, custom_fwd try: import tinycudann as tcnn except ImportError as e: print( f"Error: {e}! " "Please install tinycudann by: " "pip install git+https://github.com/NVlabs/tiny-cuda-nn/#subdirectory=bindings/torch" ) exit() class _TruncExp(Function): # pylint: disable=abstract-method # Implementation from torch-ngp: # https://github.com/ashawkey/torch-ngp/blob/93b08a0d4ec1cc6e69d85df7f0acdfb99603b628/activation.py @staticmethod @custom_fwd(cast_inputs=torch.float32) def forward(ctx, x): # pylint: disable=arguments-differ ctx.save_for_backward(x) return torch.exp(x) @staticmethod @custom_bwd def backward(ctx, g): # pylint: disable=arguments-differ x = ctx.saved_tensors[0] return g * torch.exp(torch.clamp(x, max=15)) trunc_exp = _TruncExp.apply class NGPradianceField(torch.nn.Module): """Instance-NGP radiance Field""" def __init__( self, aabb: Union[torch.Tensor, List[float]], num_dim: int = 3, use_viewdirs: bool = True, density_activation: Callable = lambda x: trunc_exp(x - 1), unbounded: bool = False, ) -> None: super().__init__() if not isinstance(aabb, torch.Tensor): aabb = torch.tensor(aabb, dtype=torch.float32) self.register_buffer("aabb", aabb) self.num_dim = num_dim self.use_viewdirs = use_viewdirs self.density_activation = density_activation self.unbounded = unbounded self.geo_feat_dim = 15 per_level_scale = 1.4472692012786865 if self.use_viewdirs: self.direction_encoding = tcnn.Encoding( n_input_dims=num_dim, encoding_config={ "otype": "Composite", "nested": [ { "n_dims_to_encode": 3, "otype": "SphericalHarmonics", "degree": 4, }, # {"otype": "Identity", "n_bins": 4, "degree": 4}, ], }, ) self.mlp_base = tcnn.NetworkWithInputEncoding( n_input_dims=num_dim, n_output_dims=1 + self.geo_feat_dim, encoding_config={ "otype": "HashGrid", "n_levels": 16, "n_features_per_level": 2, "log2_hashmap_size": 19, "base_resolution": 16, "per_level_scale": per_level_scale, }, network_config={ "otype": "FullyFusedMLP", "activation": "ReLU", "output_activation": "None", "n_neurons": 64, "n_hidden_layers": 1, }, ) self.mlp_head = tcnn.Network( n_input_dims=( ( self.direction_encoding.n_output_dims if self.use_viewdirs else 0 ) + self.geo_feat_dim ), n_output_dims=3, network_config={ "otype": "FullyFusedMLP", "activation": "ReLU", "output_activation": "Sigmoid", "n_neurons": 64, "n_hidden_layers": 2, }, ) def query_opacity(self, x, step_size): density = self.query_density(x) aabb_min, aabb_max = torch.split(self.aabb, self.num_dim, dim=-1) if self.unbounded: # TODO: [revisit] is this necessary? # 1.0 / derivative of tanh contraction x = (x - aabb_min) / (aabb_max - aabb_min) x = x - 0.5 scaling = 1.0 / ( torch.clamp(1.0 - torch.tanh(x) ** 2, min=1e6) * 0.5 ) scaling = scaling * (aabb_max - aabb_min) else: scaling = aabb_max - aabb_min step_size = step_size * scaling.norm(dim=-1, keepdim=True) # if the density is small enough those two are the same. # opacity = 1.0 - torch.exp(-density * step_size) opacity = density * step_size return opacity def query_density(self, x, return_feat: bool = False): if self.unbounded: # tanh contraction aabb_min, aabb_max = torch.split(self.aabb, self.num_dim, dim=-1) x = (x - aabb_min) / (aabb_max - aabb_min) x = x - 0.5 x = (torch.tanh(x) + 1) * 0.5 else: aabb_min, aabb_max = torch.split(self.aabb, self.num_dim, dim=-1) x = (x - aabb_min) / (aabb_max - aabb_min) selector = ((x > 0.0) & (x < 1.0)).all(dim=-1) x = ( self.mlp_base(x.view(-1, self.num_dim)) .view(list(x.shape[:-1]) + [1 + self.geo_feat_dim]) .to(x) ) density_before_activation, base_mlp_out = torch.split( x, [1, self.geo_feat_dim], dim=-1 ) density = ( self.density_activation(density_before_activation) * selector[..., None] ) if return_feat: return density, base_mlp_out else: return density def _query_rgb(self, dir, embedding): # tcnn requires directions in the range [0, 1] if self.use_viewdirs: dir = (dir + 1.0) / 2.0 d = self.direction_encoding(dir.view(-1, dir.shape[-1])) h = torch.cat([d, embedding.view(-1, self.geo_feat_dim)], dim=-1) else: h = embedding.view(-1, self.geo_feat_dim) rgb = ( self.mlp_head(h) .view(list(embedding.shape[:-1]) + [3]) .to(embedding) ) return rgb def forward( self, positions: torch.Tensor, directions: torch.Tensor = None, ): if self.use_viewdirs and (directions is not None): assert ( positions.shape == directions.shape ), f"{positions.shape} v.s. {directions.shape}" density, embedding = self.query_density(positions, return_feat=True) rgb = self._query_rgb(directions, embedding=embedding) return rgb, density