Skip to content

Commit e865e95

Browse files
Generate tileable normal maps (#2505)
1 parent d9d3ace commit e865e95

File tree

1 file changed

+50
-23
lines changed

1 file changed

+50
-23
lines changed

backend/src/packages/chaiNNer_standard/material_textures/normal_map/normal_map_generator.py

Lines changed: 50 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
11
from __future__ import annotations
22

3+
import math
34
from enum import Enum
45

56
import cv2
67
import numpy as np
78

89
import navi
910
from nodes.groups import if_enum_group
10-
from nodes.impl.image_utils import fast_gaussian_blur
11+
from nodes.impl.image_utils import BorderType, create_border, fast_gaussian_blur
1112
from nodes.impl.normals.edge_filter import EdgeFilter, get_filter_kernels
1213
from nodes.impl.normals.height import HeightSource, get_height_map
1314
from nodes.properties.inputs import (
15+
BoolInput,
1416
EnumInput,
1517
ImageInput,
1618
NormalChannelInvertInput,
1719
SliderInput,
1820
)
1921
from nodes.properties.outputs import ImageOutput
20-
from nodes.utils.utils import get_h_w_c
22+
from nodes.utils.utils import Padding, get_h_w_c
2123

2224
from .. import normal_map_group
2325

@@ -64,11 +66,18 @@ def normalize(x: np.ndarray, y: np.ndarray):
6466
icon="MdOutlineAutoFixHigh",
6567
inputs=[
6668
ImageInput("Image", channels=[1, 3, 4]),
69+
BoolInput("Tileable", default=False)
70+
.with_docs(
71+
"If enabled, the input texture will be treated as tileable and a tileable normal map will be created.",
72+
hint=True,
73+
)
74+
.with_id(16),
6775
EnumInput(
6876
HeightSource,
6977
label="Height Source",
7078
default=HeightSource.AVERAGE_RGB,
71-
).with_docs(
79+
)
80+
.with_docs(
7281
"Given the R, G, B, A channels of the input image, a height map will be calculated as follows:",
7382
"- Average RGB: `Height = (R + G + B) / 3`",
7483
"- Max RGB: `Height = max(R, G, B)`",
@@ -77,16 +86,19 @@ def normalize(x: np.ndarray, y: np.ndarray):
7786
"- Green: `Height = G`",
7887
"- Blue: `Height = B`",
7988
"- Alpha: `Height = A`",
80-
),
89+
)
90+
.with_id(1),
8191
SliderInput(
8292
"Blur/Sharp",
8393
minimum=-20,
8494
maximum=20,
8595
default=0,
8696
precision=1,
87-
).with_docs(
97+
)
98+
.with_docs(
8899
"A quick way to blur or sharpen the height map. Negative values blur, positive values sharpen."
89-
),
100+
)
101+
.with_id(2),
90102
SliderInput(
91103
"Min Z",
92104
minimum=0,
@@ -95,10 +107,12 @@ def normalize(x: np.ndarray, y: np.ndarray):
95107
precision=3,
96108
slider_step=0.01,
97109
controls_step=0.05,
98-
).with_docs(
110+
)
111+
.with_docs(
99112
"A minimum height that can be used to cut off low height values.",
100113
"This value is generally only useful in specific circumstances, so it's usually best to leave it at 0.",
101-
),
114+
)
115+
.with_id(3),
102116
SliderInput(
103117
"Scale",
104118
minimum=0,
@@ -107,10 +121,12 @@ def normalize(x: np.ndarray, y: np.ndarray):
107121
precision=3,
108122
controls_step=0.1,
109123
scale="log-offset",
110-
).with_docs(
124+
)
125+
.with_docs(
111126
"A factor applied to the height map.",
112127
"The smaller the scale, the most flat the output normal map will be. The large the scale, the more pronounced the normal map will be.",
113-
),
128+
)
129+
.with_id(4),
114130
EnumInput(
115131
EdgeFilter,
116132
label="Filter",
@@ -230,6 +246,7 @@ def normalize(x: np.ndarray, y: np.ndarray):
230246
)
231247
def normal_map_generator_node(
232248
img: np.ndarray,
249+
tileable: bool,
233250
height_source: HeightSource,
234251
blur_sharp: float,
235252
min_z: float,
@@ -249,19 +266,6 @@ def normal_map_generator_node(
249266
h, w, c = get_h_w_c(img)
250267
height = get_height_map(img, height_source)
251268

252-
if blur_sharp < 0:
253-
# blur
254-
height = fast_gaussian_blur(height, -blur_sharp)
255-
elif blur_sharp > 0:
256-
# sharpen
257-
blurred = fast_gaussian_blur(height, blur_sharp)
258-
height = cv2.addWeighted(height, 2.0, blurred, -1.0, 0)
259-
260-
if min_z > 0:
261-
height = np.maximum(min_z, height)
262-
if scale != 0:
263-
height = height * scale # type: ignore
264-
265269
filter_x, filter_y = get_filter_kernels(
266270
edge_filter,
267271
gauss_parameter=[
@@ -276,9 +280,32 @@ def normal_map_generator_node(
276280
],
277281
)
278282

283+
padding = 0
284+
if tileable:
285+
padding = max(1, filter_x.shape[0] // 2, math.ceil(abs(blur_sharp) * 2))
286+
height = create_border(height, BorderType.WRAP, Padding.all(padding))
287+
288+
if blur_sharp < 0:
289+
# blur
290+
height = fast_gaussian_blur(height, -blur_sharp)
291+
elif blur_sharp > 0:
292+
# sharpen
293+
blurred = fast_gaussian_blur(height, blur_sharp)
294+
height = cv2.addWeighted(height, 2.0, blurred, -1.0, 0)
295+
296+
if min_z > 0:
297+
height = np.maximum(min_z, height)
298+
if scale != 0:
299+
height = height * scale # type: ignore
300+
279301
dx = cv2.filter2D(height, -1, filter_x)
280302
dy = cv2.filter2D(height, -1, filter_y)
281303

304+
if padding > 0:
305+
dx = dx[padding:-padding, padding:-padding]
306+
dy = dy[padding:-padding, padding:-padding]
307+
height = height[padding:-padding, padding:-padding]
308+
282309
x, y, z = normalize(dx, dy)
283310

284311
if invert & 1 != 0:

0 commit comments

Comments
 (0)