11from __future__ import annotations
22
3+ import math
34from enum import Enum
45
56import cv2
67import numpy as np
78
89import navi
910from 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
1112from nodes .impl .normals .edge_filter import EdgeFilter , get_filter_kernels
1213from nodes .impl .normals .height import HeightSource , get_height_map
1314from nodes .properties .inputs import (
15+ BoolInput ,
1416 EnumInput ,
1517 ImageInput ,
1618 NormalChannelInvertInput ,
1719 SliderInput ,
1820)
1921from 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
2224from .. 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)
231247def 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