from typing import Tuple import numpy as np XYZ = Tuple[np.ndarray, np.ndarray, np.ndarray] """ The normalized XYZ components of a normal map. Z is guaranteed to be >= 0. """ def normalize_normals(x: np.ndarray, y: np.ndarray) -> XYZ: # The square of the length of X and Y l_sq = np.square(x) + np.square(y) # If the length of X and Y is >1, then make it 1 l = np.sqrt(np.maximum(l_sq, 1)) x /= l y /= l l_sq = np.minimum(l_sq, 1, out=l_sq) # Compute Z z = np.sqrt(1 - l_sq) return x, y, z def gr_to_xyz(n: np.ndarray) -> XYZ: """ Takes a BGR or BGRA image and converts it into XYZ normal components only by looking at the R and G channels. """ x = n[:, :, 2] * 2 - 1 y = n[:, :, 1] * 2 - 1 return normalize_normals(x, y) def xyz_to_bgr(xyz: XYZ) -> np.ndarray: """ Converts the given XYZ components into an BGR image. """ x, y, z = xyz r = (x + 1) * 0.5 g = (y + 1) * 0.5 b = z return np.dstack((b, g, r)) def octahedral_gr_to_xyz(n: np.ndarray) -> XYZ: """ Takes a BGR or BGRA image of octahedral (RTX Remix) normals and converts it into XYZ normal components only by looking at the R and G channels. """ r = n[:, :, 2] * 2 - 1 g = n[:, :, 1] * 2 - 1 x: np.ndarray = (r + g) / 2 y: np.ndarray = r - x z: np.ndarray = 1 - np.abs(x) - np.abs(y) length = np.sqrt(np.square(x) + np.square(y) + np.square(z)) x /= length y /= length z /= length # type: ignore return x, y, z def xyz_to_octahedral_bgr(xyz: XYZ) -> np.ndarray: """ Converts the given XYZ components into an BGR image with normals using octahedral (RTX Remix) encoding. For more information about octahedral normals, see: https://knarkowicz.wordpress.com/2014/04/16/octahedron-normal-vector-encoding/ https://jcgt.org/published/0003/02/01/ """ x, y, z = xyz absolute = np.abs(x) + np.abs(y) + np.abs(z) x /= absolute y /= absolute # This is a trick used in RTX Remix to more efficiently encode normals. # Octahedral normals are defined for the whole range of values (so all possible unit vectors). # However, we know that we are working with hemispheric normal maps (z>=0) # and can use this knowledge to remap xy values to assume all possible values in [-1..1]x[-1..1]. r = x + y g = x - y r = (r + 1) * 0.5 g = (g + 1) * 0.5 b = np.zeros(x.shape, dtype=np.float32) return np.dstack((b, g, r))