Spaces:
Running
Running
| """Differentiable height map to normal map conversion. | |
| Computes normals from a height field using finite differences (Sobel-like), | |
| producing tangent-space normals in [0, 1] range matching the PBR convention. | |
| """ | |
| import torch | |
| import torch.nn.functional as F | |
| def height_to_normal(height: torch.Tensor, intensity: float = 1.0) -> torch.Tensor: | |
| """Convert a height map to a tangent-space normal map. | |
| Uses central differences to compute surface gradients, then constructs | |
| the normal vector and converts to [0, 1] storage range. | |
| Args: | |
| height: (B, 1, H, W) height map in [0, 1] | |
| intensity: Strength of the normal detail (higher = more pronounced bumps) | |
| Returns: | |
| (B, 3, H, W) normal map in [0, 1], tangent-space convention: | |
| R=X, G=Y, B=Z, where 0.5 means zero for X/Y. | |
| """ | |
| kernel_x = torch.tensor([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], | |
| dtype=height.dtype, device=height.device).view(1, 1, 3, 3) / 4.0 | |
| kernel_y = torch.tensor([[-1, -2, -1], [0, 0, 0], [1, 2, 1]], | |
| dtype=height.dtype, device=height.device).view(1, 1, 3, 3) / 4.0 | |
| padded = F.pad(height, (1, 1, 1, 1), mode="replicate") | |
| dh_dx = F.conv2d(padded, kernel_x) * intensity | |
| dh_dy = F.conv2d(padded, kernel_y) * intensity | |
| nx = -dh_dx | |
| ny = -dh_dy | |
| nz = torch.ones_like(nx) | |
| normal = torch.cat([nx, ny, nz], dim=1) | |
| normal = F.normalize(normal, dim=1, eps=1e-6) | |
| # Convert from [-1, 1] to [0, 1] storage | |
| normal = normal * 0.5 + 0.5 | |
| return normal | |