Spaces:
Runtime error
Runtime error
import math | |
from enum import Enum | |
import numpy as np | |
from .util import XYZ, normalize_normals | |
class AdditionMethod(Enum): | |
PARTIAL_DERIVATIVES = 0 | |
""" | |
The addition works by converting the normals into 2D slopes and then adding | |
the slopes. The sum of the slopes is then converted back into normals. | |
When adding 2 normal maps, the normals themselves are not added; | |
Instead, the heightmaps that those normals represent are added. | |
Conceptually, this entails converting the normals into slopes | |
(the derivatives of the heightmap), integrating the slopes to get | |
the heightmaps, adding the heightmaps, then performing the reverse | |
on the added heightmaps. Practically, this is unnecessary, as adding | |
the slopes together is equivalent to adding the heightmaps. | |
""" | |
ANGLES = 1 | |
""" | |
The addition works by converting the normals into 2 angles, one angle the | |
X axis and one along the Y axis. Those 2 angles are then added together. | |
Since this might create angles outside the range of -90ยฐ to 90ยฐ, | |
the resulting angles are clamped to this range. | |
""" | |
def __partial_derivatives(n1: XYZ, n2: XYZ, f1: float, f2: float) -> XYZ: | |
x1, y1, z1 = n1 | |
x2, y2, z2 = n2 | |
# Slopes aren't defined for z=0, so set to near-zero decimal | |
z1 = np.maximum(z1, 0.001, out=z1) | |
z2 = np.maximum(z2, 0.001, out=z2) | |
# This works as follows: | |
# 1. Use the normals n,m to calculate 3D planes (the slopes) centered at origin p_n,p_m. | |
# 2. Calculate the Z values of those planes at a_xy=(1,0) and b_xy=(0,1). | |
# 3. Add the Z values to together (weighted using their strength): | |
# a_z = p_n[a_xy] * n_strength + p_m[a_xy] * m_strength, same for b_xy. | |
# 4. Define a=(1,0,a_z), b=(0,1,b_z). | |
# 5. The final normal will be normalize(cross(a,b)). | |
# This works out as: | |
n_f = f1 / z1 | |
m_f = f2 / z2 | |
x = x1 * n_f + x2 * m_f | |
y = y1 * n_f + y2 * m_f | |
l_r = 1 / np.sqrt(np.square(x) + np.square(y) + 1) | |
x *= l_r | |
y *= l_r | |
z = l_r | |
return x, y, z | |
def __clamp_angles(angles: np.ndarray) -> np.ndarray: | |
return np.clip(angles, -math.pi / 2, math.pi / 2) | |
def __angles(n1: XYZ, n2: XYZ, f1: float, f2: float) -> XYZ: | |
x1, y1, _ = n1 | |
x2, y2, _ = n2 | |
return normalize_normals( | |
np.sin(__clamp_angles(np.arcsin(x1) * f1 + np.arcsin(x2) * f2)), | |
np.sin(__clamp_angles(np.arcsin(y1) * f1 + np.arcsin(y2) * f2)), | |
) | |
def add_normals( | |
method: AdditionMethod, | |
n1: np.ndarray, | |
n2: np.ndarray, | |
f1: float = 1, | |
f2: float = 1, | |
) -> XYZ: | |
# Convert BGR to XY | |
x1 = n1[:, :, 2] * 2 - 1 | |
y1 = n1[:, :, 1] * 2 - 1 | |
x2 = n2[:, :, 2] * 2 - 1 | |
y2 = n2[:, :, 1] * 2 - 1 | |
xyz1 = normalize_normals(x1, y1) | |
xyz2 = normalize_normals(x2, y2) | |
if method is AdditionMethod.PARTIAL_DERIVATIVES: | |
return __partial_derivatives(xyz1, xyz2, f1, f2) | |
elif method is AdditionMethod.ANGLES: | |
return __angles(xyz1, xyz2, f1, f2) | |
else: | |
raise AssertionError(f"Invalid normal addition method {method}") | |