File size: 3,084 Bytes
c19ca42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
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}")