fractex 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fractex/3d.py +1585 -0
- fractex/__init__.py +38 -0
- fractex/advanced.py +170 -0
- fractex/cli.py +81 -0
- fractex/core.py +508 -0
- fractex/dynamic_textures_3d.py +1935 -0
- fractex/examples/3d.py +109 -0
- fractex/examples/3d_integration.py +113 -0
- fractex/examples/3d_integration_2d.py +59 -0
- fractex/examples/__init__.py +34 -0
- fractex/examples/_output.py +115 -0
- fractex/examples/architecture_pattern.py +61 -0
- fractex/examples/atmosphere.py +54 -0
- fractex/examples/composite_material.py +63 -0
- fractex/examples/crystal_cave.py +61 -0
- fractex/examples/custom_pattern.py +114 -0
- fractex/examples/game_integration.py +86 -0
- fractex/examples/game_texture.py +178 -0
- fractex/examples/integration.py +102 -0
- fractex/examples/physic_integration.py +70 -0
- fractex/examples/splash.py +159 -0
- fractex/examples/terrain.py +76 -0
- fractex/examples/underwater.py +94 -0
- fractex/examples/underwater_volkano.py +112 -0
- fractex/geometric_patterns_3d.py +2372 -0
- fractex/interactive.py +158 -0
- fractex/simplex_noise.py +1113 -0
- fractex/texture_blending.py +1377 -0
- fractex/volume_scattering.py +1263 -0
- fractex/volume_textures.py +8 -0
- fractex-0.1.0.dist-info/METADATA +100 -0
- fractex-0.1.0.dist-info/RECORD +36 -0
- fractex-0.1.0.dist-info/WHEEL +5 -0
- fractex-0.1.0.dist-info/entry_points.txt +2 -0
- fractex-0.1.0.dist-info/licenses/LICENSE +21 -0
- fractex-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1377 @@
|
|
|
1
|
+
# fractex/texture_blending.py
|
|
2
|
+
"""
|
|
3
|
+
Продвинутые алгоритмы смешивания фрактальных текстур
|
|
4
|
+
Поддержка многослойных материалов, плавных переходов и сложных эффектов
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from typing import Dict, List, Tuple, Optional, Union, Callable
|
|
9
|
+
from numba import jit, prange, vectorize, float32, float64
|
|
10
|
+
import warnings
|
|
11
|
+
|
|
12
|
+
# ----------------------------------------------------------------------
|
|
13
|
+
# Базовые функции смешивания (оптимизированные с Numba)
|
|
14
|
+
# ----------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
@vectorize([float32(float32, float32, float32),
|
|
17
|
+
float64(float64, float64, float64)])
|
|
18
|
+
def lerp(a: np.ndarray, b: np.ndarray, t: np.ndarray) -> np.ndarray:
|
|
19
|
+
"""Линейная интерполяция (быстрая векторизованная версия)"""
|
|
20
|
+
return a + t * (b - a)
|
|
21
|
+
|
|
22
|
+
@jit(nopython=True, cache=True)
|
|
23
|
+
def smoothstep(edge0: float, edge1: float, x: float) -> float:
|
|
24
|
+
"""Гладкая ступенчатая функция"""
|
|
25
|
+
x = np.clip((x - edge0) / (edge1 - edge0), 0.0, 1.0)
|
|
26
|
+
return x * x * (3.0 - 2.0 * x)
|
|
27
|
+
|
|
28
|
+
@jit(nopython=True, cache=True)
|
|
29
|
+
def smootherstep(edge0: float, edge1: float, x: float) -> float:
|
|
30
|
+
"""Еще более гладкая ступенчатая функция"""
|
|
31
|
+
x = np.clip((x - edge0) / (edge1 - edge0), 0.0, 1.0)
|
|
32
|
+
return x * x * x * (x * (x * 6 - 15) + 10)
|
|
33
|
+
|
|
34
|
+
@jit(nopython=True, cache=True)
|
|
35
|
+
def sigmoid_mix(x: float, sharpness: float = 10.0) -> float:
|
|
36
|
+
"""Сигмоидальное смешивание для плавных переходов"""
|
|
37
|
+
return 1.0 / (1.0 + np.exp(-sharpness * (x - 0.5)))
|
|
38
|
+
|
|
39
|
+
# ----------------------------------------------------------------------
|
|
40
|
+
# Класс для работы с масками смешивания
|
|
41
|
+
# ----------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
class BlendMask:
|
|
44
|
+
"""Маска для контролируемого смешивания текстур"""
|
|
45
|
+
|
|
46
|
+
def __init__(self,
|
|
47
|
+
mask_type: str = "linear",
|
|
48
|
+
seed: int = 42,
|
|
49
|
+
parameters: Optional[Dict] = None):
|
|
50
|
+
|
|
51
|
+
self.mask_type = mask_type
|
|
52
|
+
self.seed = seed
|
|
53
|
+
self.params = parameters or {}
|
|
54
|
+
self._cache = {}
|
|
55
|
+
|
|
56
|
+
# Доступные типы масок
|
|
57
|
+
self.mask_generators = {
|
|
58
|
+
"linear": self._linear_mask,
|
|
59
|
+
"gradient": self._gradient_mask,
|
|
60
|
+
"noise": self._noise_mask,
|
|
61
|
+
"radial": self._radial_mask,
|
|
62
|
+
"voronoi": self._voronoi_mask,
|
|
63
|
+
"cellular": self._cellular_mask,
|
|
64
|
+
"fractal": self._fractal_mask,
|
|
65
|
+
"height_based": self._height_based_mask,
|
|
66
|
+
"slope_based": self._slope_based_mask,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
def generate(self,
|
|
70
|
+
width: int,
|
|
71
|
+
height: int,
|
|
72
|
+
position: Tuple[float, float] = (0, 0),
|
|
73
|
+
scale: float = 1.0) -> np.ndarray:
|
|
74
|
+
"""Генерация маски смешивания"""
|
|
75
|
+
|
|
76
|
+
# Проверка кэша
|
|
77
|
+
cache_key = f"{self.mask_type}_{width}_{height}_{position}_{scale}"
|
|
78
|
+
if cache_key in self._cache:
|
|
79
|
+
return self._cache[cache_key].copy()
|
|
80
|
+
|
|
81
|
+
# Генерация координатной сетки
|
|
82
|
+
x = np.linspace(position[0], position[0] + width * scale, width)
|
|
83
|
+
y = np.linspace(position[1], position[1] + height * scale, height)
|
|
84
|
+
xx, yy = np.meshgrid(x, y)
|
|
85
|
+
|
|
86
|
+
# Выбор генератора маски
|
|
87
|
+
if self.mask_type in self.mask_generators:
|
|
88
|
+
mask = self.mask_generators[self.mask_type](xx, yy, **self.params)
|
|
89
|
+
else:
|
|
90
|
+
mask = np.ones((height, width), dtype=np.float32)
|
|
91
|
+
|
|
92
|
+
# Кэширование
|
|
93
|
+
self._cache[cache_key] = mask.copy()
|
|
94
|
+
if len(self._cache) > 50: # Ограничение размера кэша
|
|
95
|
+
self._cache.pop(next(iter(self._cache)))
|
|
96
|
+
|
|
97
|
+
return mask
|
|
98
|
+
|
|
99
|
+
def _linear_mask(self, xx, yy, direction='horizontal', **kwargs):
|
|
100
|
+
"""Линейный градиент"""
|
|
101
|
+
if direction == 'horizontal':
|
|
102
|
+
return (xx - xx.min()) / (xx.max() - xx.min())
|
|
103
|
+
elif direction == 'vertical':
|
|
104
|
+
return (yy - yy.min()) / (yy.max() - yy.min())
|
|
105
|
+
elif direction == 'diagonal':
|
|
106
|
+
return ((xx - xx.min()) + (yy - yy.min())) / \
|
|
107
|
+
((xx.max() - xx.min()) + (yy.max() - yy.min()))
|
|
108
|
+
else:
|
|
109
|
+
# Произвольное направление
|
|
110
|
+
angle = kwargs.get('angle', 45)
|
|
111
|
+
angle_rad = np.radians(angle)
|
|
112
|
+
return (xx * np.cos(angle_rad) + yy * np.sin(angle_rad))
|
|
113
|
+
|
|
114
|
+
def _gradient_mask(self, xx, yy, center=(0.5, 0.5), radius=1.0, **kwargs):
|
|
115
|
+
"""Радиальный градиент"""
|
|
116
|
+
center_x = center[0] * (xx.max() - xx.min()) + xx.min()
|
|
117
|
+
center_y = center[1] * (yy.max() - yy.min()) + yy.min()
|
|
118
|
+
|
|
119
|
+
dist = np.sqrt((xx - center_x)**2 + (yy - center_y)**2)
|
|
120
|
+
mask = 1.0 - np.clip(dist / radius, 0, 1)
|
|
121
|
+
|
|
122
|
+
# Применяем функцию смягчения
|
|
123
|
+
falloff = kwargs.get('falloff', 'smoothstep')
|
|
124
|
+
if falloff == 'smoothstep':
|
|
125
|
+
mask = smoothstep(0.0, 1.0, mask)
|
|
126
|
+
elif falloff == 'smootherstep':
|
|
127
|
+
mask = smootherstep(0.0, 1.0, mask)
|
|
128
|
+
elif falloff == 'exponential':
|
|
129
|
+
exponent = kwargs.get('exponent', 2.0)
|
|
130
|
+
mask = np.power(mask, exponent)
|
|
131
|
+
|
|
132
|
+
return mask
|
|
133
|
+
|
|
134
|
+
def _noise_mask(self, xx, yy, noise_scale=0.01, octaves=3, **kwargs):
|
|
135
|
+
"""Маска на основе шума"""
|
|
136
|
+
try:
|
|
137
|
+
from .simplex_noise import SimplexNoise
|
|
138
|
+
noise_gen = SimplexNoise(seed=self.seed)
|
|
139
|
+
|
|
140
|
+
# Генерируем фрактальный шум
|
|
141
|
+
noise = noise_gen.fractal_noise_2d(
|
|
142
|
+
xx, yy,
|
|
143
|
+
octaves=octaves,
|
|
144
|
+
persistence=kwargs.get('persistence', 0.5),
|
|
145
|
+
lacunarity=kwargs.get('lacunarity', 2.0),
|
|
146
|
+
base_scale=noise_scale
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Нормализуем к [0, 1]
|
|
150
|
+
noise = (noise - noise.min()) / (noise.max() - noise.min())
|
|
151
|
+
|
|
152
|
+
# Применяем пороговое значение если нужно
|
|
153
|
+
threshold = kwargs.get('threshold', None)
|
|
154
|
+
if threshold is not None:
|
|
155
|
+
noise = np.where(noise > threshold, 1.0, 0.0)
|
|
156
|
+
|
|
157
|
+
return noise
|
|
158
|
+
except ImportError:
|
|
159
|
+
# Запасной вариант - простой синусоидальный паттерн
|
|
160
|
+
return (np.sin(xx * 0.1) * np.sin(yy * 0.1) + 1) / 2
|
|
161
|
+
|
|
162
|
+
def _radial_mask(self, xx, yy, center=(0.5, 0.5), **kwargs):
|
|
163
|
+
"""Концентрические круги"""
|
|
164
|
+
center_x = center[0] * (xx.max() - xx.min()) + xx.min()
|
|
165
|
+
center_y = center[1] * (yy.max() - yy.min()) + yy.min()
|
|
166
|
+
|
|
167
|
+
dist = np.sqrt((xx - center_x)**2 + (yy - center_y)**2)
|
|
168
|
+
frequency = kwargs.get('frequency', 10.0)
|
|
169
|
+
|
|
170
|
+
mask = np.sin(dist * frequency) * 0.5 + 0.5
|
|
171
|
+
return mask
|
|
172
|
+
|
|
173
|
+
def _voronoi_mask(self, xx, yy, num_points=10, **kwargs):
|
|
174
|
+
"""Маска на основе диаграмм Вороного"""
|
|
175
|
+
np.random.seed(self.seed)
|
|
176
|
+
|
|
177
|
+
# Генерируем случайные точки
|
|
178
|
+
points_x = np.random.rand(num_points) * (xx.max() - xx.min()) + xx.min()
|
|
179
|
+
points_y = np.random.rand(num_points) * (yy.max() - yy.min()) + yy.min()
|
|
180
|
+
|
|
181
|
+
# Для каждой точки вычисляем расстояние до ближайшей точки Вороного
|
|
182
|
+
mask = np.zeros_like(xx, dtype=np.float32)
|
|
183
|
+
|
|
184
|
+
for i in range(xx.shape[0]):
|
|
185
|
+
for j in range(xx.shape[1]):
|
|
186
|
+
distances = np.sqrt((points_x - xx[i, j])**2 +
|
|
187
|
+
(points_y - yy[i, j])**2)
|
|
188
|
+
mask[i, j] = np.min(distances)
|
|
189
|
+
|
|
190
|
+
# Нормализация
|
|
191
|
+
mask = (mask - mask.min()) / (mask.max() - mask.min())
|
|
192
|
+
|
|
193
|
+
# Инвертирование если нужно
|
|
194
|
+
if kwargs.get('invert', False):
|
|
195
|
+
mask = 1.0 - mask
|
|
196
|
+
|
|
197
|
+
return mask
|
|
198
|
+
|
|
199
|
+
def _cellular_mask(self, xx, yy, **kwargs):
|
|
200
|
+
"""Клеточный шум (cellular noise)"""
|
|
201
|
+
np.random.seed(self.seed)
|
|
202
|
+
|
|
203
|
+
# Упрощенная версия клеточного шума
|
|
204
|
+
num_cells = kwargs.get('num_cells', 20)
|
|
205
|
+
cell_size = kwargs.get('cell_size', 0.1)
|
|
206
|
+
|
|
207
|
+
# Создаем случайные центры клеток
|
|
208
|
+
cells_x = np.random.rand(num_cells)
|
|
209
|
+
cells_y = np.random.rand(num_cells)
|
|
210
|
+
|
|
211
|
+
# Нормализуем координаты к [0, 1] для простоты
|
|
212
|
+
xx_norm = (xx - xx.min()) / (xx.max() - xx.min())
|
|
213
|
+
yy_norm = (yy - yy.min()) / (yy.max() - yy.min())
|
|
214
|
+
|
|
215
|
+
mask = np.zeros_like(xx_norm)
|
|
216
|
+
|
|
217
|
+
for i in range(xx_norm.shape[0]):
|
|
218
|
+
for j in range(xx_norm.shape[1]):
|
|
219
|
+
# Находим ближайшую и вторую ближайшую клетки
|
|
220
|
+
distances = []
|
|
221
|
+
for c in range(num_cells):
|
|
222
|
+
dx = xx_norm[i, j] - cells_x[c]
|
|
223
|
+
dy = yy_norm[i, j] - cells_y[c]
|
|
224
|
+
dist = np.sqrt(dx*dx + dy*dy)
|
|
225
|
+
distances.append(dist)
|
|
226
|
+
|
|
227
|
+
distances.sort()
|
|
228
|
+
# Используем разницу между ближайшими клетками
|
|
229
|
+
mask[i, j] = distances[1] - distances[0]
|
|
230
|
+
|
|
231
|
+
# Нормализация
|
|
232
|
+
mask = (mask - mask.min()) / (mask.max() - mask.min())
|
|
233
|
+
return mask
|
|
234
|
+
|
|
235
|
+
def _fractal_mask(self, xx, yy, **kwargs):
|
|
236
|
+
"""Фрактальная маска (комбинация нескольких масок)"""
|
|
237
|
+
# Создаем несколько масок с разными параметрами
|
|
238
|
+
mask1 = self._noise_mask(xx, yy, noise_scale=0.01, octaves=2)
|
|
239
|
+
mask2 = self._noise_mask(xx * 2, yy * 2, noise_scale=0.02, octaves=3)
|
|
240
|
+
mask3 = self._radial_mask(xx, yy, frequency=5)
|
|
241
|
+
|
|
242
|
+
# Комбинируем
|
|
243
|
+
blend_mode = kwargs.get('blend_mode', 'multiply')
|
|
244
|
+
|
|
245
|
+
if blend_mode == 'multiply':
|
|
246
|
+
mask = mask1 * mask2 * mask3
|
|
247
|
+
elif blend_mode == 'add':
|
|
248
|
+
mask = (mask1 + mask2 + mask3) / 3
|
|
249
|
+
elif blend_mode == 'max':
|
|
250
|
+
mask = np.maximum(np.maximum(mask1, mask2), mask3)
|
|
251
|
+
else:
|
|
252
|
+
mask = mask1
|
|
253
|
+
|
|
254
|
+
return mask
|
|
255
|
+
|
|
256
|
+
def _height_based_mask(self, xx, yy, height_map=None, **kwargs):
|
|
257
|
+
"""Маска на основе карты высот"""
|
|
258
|
+
if height_map is None:
|
|
259
|
+
# Генерируем простую карту высот если не предоставлена
|
|
260
|
+
height_map = self._noise_mask(xx, yy, noise_scale=0.02, octaves=4)
|
|
261
|
+
|
|
262
|
+
min_height = kwargs.get('min_height', 0.3)
|
|
263
|
+
max_height = kwargs.get('max_height', 0.7)
|
|
264
|
+
softness = kwargs.get('softness', 0.1)
|
|
265
|
+
|
|
266
|
+
# Создаем маску с плавным переходом
|
|
267
|
+
mask = np.clip((height_map - min_height) / softness, 0, 1)
|
|
268
|
+
mask *= np.clip(1 - (height_map - max_height) / softness, 0, 1)
|
|
269
|
+
|
|
270
|
+
return mask
|
|
271
|
+
|
|
272
|
+
def _slope_based_mask(self, xx, yy, height_map=None, **kwargs):
|
|
273
|
+
"""Маска на основе наклона (производной высоты)"""
|
|
274
|
+
if height_map is None:
|
|
275
|
+
height_map = self._noise_mask(xx, yy, noise_scale=0.02, octaves=4)
|
|
276
|
+
|
|
277
|
+
# Вычисляем градиент (наклон)
|
|
278
|
+
gradient_x = np.gradient(height_map, axis=1)
|
|
279
|
+
gradient_y = np.gradient(height_map, axis=0)
|
|
280
|
+
slope = np.sqrt(gradient_x**2 + gradient_y**2)
|
|
281
|
+
|
|
282
|
+
max_slope = kwargs.get('max_slope', 0.5)
|
|
283
|
+
mask = np.clip(1.0 - slope / max_slope, 0, 1)
|
|
284
|
+
|
|
285
|
+
return mask
|
|
286
|
+
|
|
287
|
+
# ----------------------------------------------------------------------
|
|
288
|
+
# Основной класс смешивания текстур
|
|
289
|
+
# ----------------------------------------------------------------------
|
|
290
|
+
|
|
291
|
+
class TextureBlender:
|
|
292
|
+
"""Продвинутый блендер для смешивания фрактальных текстур"""
|
|
293
|
+
|
|
294
|
+
def __init__(self,
|
|
295
|
+
blend_space: str = "linear", # linear, logarithmic, perceptual
|
|
296
|
+
gamma_correction: bool = True,
|
|
297
|
+
cache_size: int = 100):
|
|
298
|
+
|
|
299
|
+
self.blend_space = blend_space
|
|
300
|
+
self.gamma_correction = gamma_correction
|
|
301
|
+
self.cache_size = cache_size
|
|
302
|
+
self._result_cache = {}
|
|
303
|
+
|
|
304
|
+
# Режимы смешивания
|
|
305
|
+
self.blend_modes = {
|
|
306
|
+
# Основные режимы
|
|
307
|
+
"overlay": self._blend_overlay,
|
|
308
|
+
"multiply": self._blend_multiply,
|
|
309
|
+
"screen": self._blend_screen,
|
|
310
|
+
"add": self._blend_add,
|
|
311
|
+
"subtract": self._blend_subtract,
|
|
312
|
+
"difference": self._blend_difference,
|
|
313
|
+
"divide": self._blend_divide,
|
|
314
|
+
|
|
315
|
+
# Режимы наложения
|
|
316
|
+
"normal": self._blend_normal,
|
|
317
|
+
"dissolve": self._blend_dissolve,
|
|
318
|
+
|
|
319
|
+
# Режимы затемнения
|
|
320
|
+
"darken": self._blend_darken,
|
|
321
|
+
"color_burn": self._blend_color_burn,
|
|
322
|
+
"linear_burn": self._blend_linear_burn,
|
|
323
|
+
|
|
324
|
+
# Режимы осветления
|
|
325
|
+
"lighten": self._blend_lighten,
|
|
326
|
+
"color_dodge": self._blend_color_dodge,
|
|
327
|
+
"linear_dodge": self._blend_linear_dodge,
|
|
328
|
+
|
|
329
|
+
# Контрастные режимы
|
|
330
|
+
"soft_light": self._blend_soft_light,
|
|
331
|
+
"hard_light": self._blend_hard_light,
|
|
332
|
+
"vivid_light": self._blend_vivid_light,
|
|
333
|
+
"linear_light": self._blend_linear_light,
|
|
334
|
+
"pin_light": self._blend_pin_light,
|
|
335
|
+
"hard_mix": self._blend_hard_mix,
|
|
336
|
+
|
|
337
|
+
# Компонентные режимы
|
|
338
|
+
"hue": self._blend_hue,
|
|
339
|
+
"saturation": self._blend_saturation,
|
|
340
|
+
"color": self._blend_color,
|
|
341
|
+
"luminosity": self._blend_luminosity,
|
|
342
|
+
|
|
343
|
+
# Кастомные режимы для текстур
|
|
344
|
+
"height_blend": self._blend_height_based,
|
|
345
|
+
"slope_blend": self._blend_slope_based,
|
|
346
|
+
"edge_blend": self._blend_edge_aware,
|
|
347
|
+
"detail_preserving": self._blend_detail_preserving,
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
def blend(self,
|
|
351
|
+
texture_a: np.ndarray,
|
|
352
|
+
texture_b: np.ndarray,
|
|
353
|
+
blend_mode: str = "overlay",
|
|
354
|
+
opacity: float = 1.0,
|
|
355
|
+
mask: Optional[np.ndarray] = None,
|
|
356
|
+
**kwargs) -> np.ndarray:
|
|
357
|
+
"""
|
|
358
|
+
Основная функция смешивания двух текстур
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
texture_a: Базовая текстура (H, W, C)
|
|
362
|
+
texture_b: Накладываемая текстура (H, W, C)
|
|
363
|
+
blend_mode: Режим смешивания
|
|
364
|
+
opacity: Прозрачность наложения (0-1)
|
|
365
|
+
mask: Маска смешивания (H, W) или (H, W, 1)
|
|
366
|
+
**kwargs: Дополнительные параметры для режима смешивания
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
Смешанная текстура (H, W, C)
|
|
370
|
+
"""
|
|
371
|
+
# Проверка размеров
|
|
372
|
+
if texture_a.shape != texture_b.shape:
|
|
373
|
+
raise ValueError(f"Texture shapes must match: {texture_a.shape} != {texture_b.shape}")
|
|
374
|
+
|
|
375
|
+
# Проверка маски
|
|
376
|
+
if mask is not None:
|
|
377
|
+
if mask.ndim == 2:
|
|
378
|
+
mask = mask[..., np.newaxis]
|
|
379
|
+
if mask.shape[:2] != texture_a.shape[:2]:
|
|
380
|
+
raise ValueError(f"Mask shape {mask.shape[:2]} doesn't match texture shape {texture_a.shape[:2]}")
|
|
381
|
+
|
|
382
|
+
# Выбор режима смешивания
|
|
383
|
+
if blend_mode not in self.blend_modes:
|
|
384
|
+
warnings.warn(f"Blend mode '{blend_mode}' not found. Using 'overlay'.")
|
|
385
|
+
blend_mode = "overlay"
|
|
386
|
+
|
|
387
|
+
blend_func = self.blend_modes[blend_mode]
|
|
388
|
+
|
|
389
|
+
# Применяем гамма-коррекцию если нужно
|
|
390
|
+
if self.gamma_correction and blend_mode not in ["hue", "saturation", "color", "luminosity"]:
|
|
391
|
+
texture_a = self._gamma_correct(texture_a, inverse=False)
|
|
392
|
+
texture_b = self._gamma_correct(texture_b, inverse=False)
|
|
393
|
+
|
|
394
|
+
# Смешивание
|
|
395
|
+
result = blend_func(texture_a, texture_b, **kwargs)
|
|
396
|
+
|
|
397
|
+
# Применяем маску если есть
|
|
398
|
+
if mask is not None:
|
|
399
|
+
result = lerp(texture_a, result, mask)
|
|
400
|
+
|
|
401
|
+
# Применяем общую прозрачность
|
|
402
|
+
if opacity < 1.0:
|
|
403
|
+
result = lerp(texture_a, result, opacity)
|
|
404
|
+
|
|
405
|
+
# Обратная гамма-коррекция
|
|
406
|
+
if self.gamma_correction and blend_mode not in ["hue", "saturation", "color", "luminosity"]:
|
|
407
|
+
result = self._gamma_correct(result, inverse=True)
|
|
408
|
+
|
|
409
|
+
return np.clip(result, 0, 1)
|
|
410
|
+
|
|
411
|
+
def blend_multiple(self,
|
|
412
|
+
textures: List[np.ndarray],
|
|
413
|
+
blend_modes: List[str],
|
|
414
|
+
opacities: List[float],
|
|
415
|
+
masks: Optional[List[np.ndarray]] = None) -> np.ndarray:
|
|
416
|
+
"""
|
|
417
|
+
Смешивание нескольких текстур последовательно
|
|
418
|
+
|
|
419
|
+
Args:
|
|
420
|
+
textures: Список текстур для смешивания
|
|
421
|
+
blend_modes: Список режимов смешивания
|
|
422
|
+
opacities: Список прозрачностей
|
|
423
|
+
masks: Список масок (может быть None)
|
|
424
|
+
|
|
425
|
+
Returns:
|
|
426
|
+
Итоговая смешанная текстура
|
|
427
|
+
"""
|
|
428
|
+
if len(textures) < 2:
|
|
429
|
+
return textures[0] if textures else np.array([])
|
|
430
|
+
|
|
431
|
+
if masks is None:
|
|
432
|
+
masks = [None] * len(textures)
|
|
433
|
+
|
|
434
|
+
# Начинаем с первой текстуры
|
|
435
|
+
result = textures[0].copy()
|
|
436
|
+
|
|
437
|
+
# Последовательно применяем смешивание
|
|
438
|
+
for i in range(1, len(textures)):
|
|
439
|
+
result = self.blend(
|
|
440
|
+
result, textures[i],
|
|
441
|
+
blend_mode=blend_modes[i-1] if i-1 < len(blend_modes) else "overlay",
|
|
442
|
+
opacity=opacities[i-1] if i-1 < len(opacities) else 1.0,
|
|
443
|
+
mask=masks[i-1] if i-1 < len(masks) else None
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
return result
|
|
447
|
+
|
|
448
|
+
def blend_layer_stack(self,
|
|
449
|
+
base_texture: np.ndarray,
|
|
450
|
+
layers: List[Dict]) -> np.ndarray:
|
|
451
|
+
"""
|
|
452
|
+
Смешивание текстуры со стеком слоев
|
|
453
|
+
|
|
454
|
+
Args:
|
|
455
|
+
base_texture: Базовая текстура
|
|
456
|
+
layers: Список слоев, каждый слой - словарь с параметрами:
|
|
457
|
+
{
|
|
458
|
+
'texture': np.ndarray,
|
|
459
|
+
'blend_mode': str,
|
|
460
|
+
'opacity': float,
|
|
461
|
+
'mask': Optional[np.ndarray],
|
|
462
|
+
'mask_params': Dict (опционально для генерации маски)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
Returns:
|
|
466
|
+
Итоговая текстура
|
|
467
|
+
"""
|
|
468
|
+
result = base_texture.copy()
|
|
469
|
+
|
|
470
|
+
for layer in layers:
|
|
471
|
+
# Генерация маски если есть параметры
|
|
472
|
+
mask = layer.get('mask')
|
|
473
|
+
if mask is None and 'mask_params' in layer:
|
|
474
|
+
mask_params = layer['mask_params']
|
|
475
|
+
height, width = base_texture.shape[:2]
|
|
476
|
+
|
|
477
|
+
# Создаем маску
|
|
478
|
+
mask_gen = BlendMask(**mask_params)
|
|
479
|
+
position = mask_params.get('position', (0, 0))
|
|
480
|
+
scale = mask_params.get('scale', 1.0)
|
|
481
|
+
mask = mask_gen.generate(width, height, position, scale)
|
|
482
|
+
|
|
483
|
+
# Смешивание
|
|
484
|
+
result = self.blend(
|
|
485
|
+
result, layer['texture'],
|
|
486
|
+
blend_mode=layer.get('blend_mode', 'overlay'),
|
|
487
|
+
opacity=layer.get('opacity', 1.0),
|
|
488
|
+
mask=mask
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
return result
|
|
492
|
+
|
|
493
|
+
# ------------------------------------------------------------------
|
|
494
|
+
# Реализации режимов смешивания
|
|
495
|
+
# ------------------------------------------------------------------
|
|
496
|
+
|
|
497
|
+
def _blend_normal(self, a: np.ndarray, b: np.ndarray, **kwargs) -> np.ndarray:
|
|
498
|
+
"""Обычное наложение (просто заменяет a на b)"""
|
|
499
|
+
return b
|
|
500
|
+
|
|
501
|
+
def _blend_dissolve(self, a: np.ndarray, b: np.ndarray, **kwargs) -> np.ndarray:
|
|
502
|
+
"""Диссолв - случайная замена пикселей"""
|
|
503
|
+
np.random.seed(kwargs.get('seed', 42))
|
|
504
|
+
random_mask = np.random.rand(*a.shape[:2])
|
|
505
|
+
threshold = kwargs.get('threshold', 0.5)
|
|
506
|
+
|
|
507
|
+
mask = (random_mask > threshold).astype(np.float32)[..., np.newaxis]
|
|
508
|
+
if a.shape[-1] == 4: # С альфа-каналом
|
|
509
|
+
mask = np.repeat(mask, 4, axis=2)
|
|
510
|
+
|
|
511
|
+
return lerp(a, b, mask)
|
|
512
|
+
|
|
513
|
+
def _blend_darken(self, a: np.ndarray, b: np.ndarray, **kwargs) -> np.ndarray:
|
|
514
|
+
"""Берет темнейший из двух пикселей"""
|
|
515
|
+
return np.minimum(a, b)
|
|
516
|
+
|
|
517
|
+
def _blend_multiply(self, a: np.ndarray, b: np.ndarray, **kwargs) -> np.ndarray:
|
|
518
|
+
"""Умножение - затемняет изображение"""
|
|
519
|
+
return a * b
|
|
520
|
+
|
|
521
|
+
def _blend_color_burn(self, a: np.ndarray, b: np.ndarray, **kwargs) -> np.ndarray:
|
|
522
|
+
"""Color Burn - сильное затемнение"""
|
|
523
|
+
eps = 1e-7
|
|
524
|
+
return 1 - (1 - a) / np.maximum(b, eps)
|
|
525
|
+
|
|
526
|
+
def _blend_linear_burn(self, a: np.ndarray, b: np.ndarray, **kwargs) -> np.ndarray:
|
|
527
|
+
"""Linear Burn - линейное затемнение"""
|
|
528
|
+
return np.maximum(a + b - 1, 0)
|
|
529
|
+
|
|
530
|
+
def _blend_lighten(self, a: np.ndarray, b: np.ndarray, **kwargs) -> np.ndarray:
|
|
531
|
+
"""Берет светлейший из двух пикселей"""
|
|
532
|
+
return np.maximum(a, b)
|
|
533
|
+
|
|
534
|
+
def _blend_screen(self, a: np.ndarray, b: np.ndarray, **kwargs) -> np.ndarray:
|
|
535
|
+
"""Screen - осветляет изображение"""
|
|
536
|
+
return 1 - (1 - a) * (1 - b)
|
|
537
|
+
|
|
538
|
+
def _blend_color_dodge(self, a: np.ndarray, b: np.ndarray, **kwargs) -> np.ndarray:
|
|
539
|
+
"""Color Dodge - сильное осветление"""
|
|
540
|
+
eps = 1e-7
|
|
541
|
+
return a / np.maximum(1 - b, eps)
|
|
542
|
+
|
|
543
|
+
def _blend_linear_dodge(self, a: np.ndarray, b: np.ndarray, **kwargs) -> np.ndarray:
|
|
544
|
+
"""Linear Dodge (Add) - сложение"""
|
|
545
|
+
return np.minimum(a + b, 1)
|
|
546
|
+
|
|
547
|
+
def _blend_add(self, a: np.ndarray, b: np.ndarray, **kwargs) -> np.ndarray:
|
|
548
|
+
"""Простое сложение"""
|
|
549
|
+
return self._blend_linear_dodge(a, b)
|
|
550
|
+
|
|
551
|
+
def _blend_overlay(self, a: np.ndarray, b: np.ndarray, **kwargs) -> np.ndarray:
|
|
552
|
+
"""Overlay - комбинация Multiply и Screen"""
|
|
553
|
+
mask = a < 0.5
|
|
554
|
+
result = np.zeros_like(a)
|
|
555
|
+
result[mask] = 2 * a[mask] * b[mask]
|
|
556
|
+
result[~mask] = 1 - 2 * (1 - a[~mask]) * (1 - b[~mask])
|
|
557
|
+
return result
|
|
558
|
+
|
|
559
|
+
def _blend_soft_light(self, a: np.ndarray, b: np.ndarray, **kwargs) -> np.ndarray:
|
|
560
|
+
"""Soft Light - мягкий свет"""
|
|
561
|
+
# Формула из Photoshop
|
|
562
|
+
mask = b < 0.5
|
|
563
|
+
result = np.zeros_like(a)
|
|
564
|
+
result[mask] = 2 * a[mask] * b[mask] + a[mask] * a[mask] * (1 - 2 * b[mask])
|
|
565
|
+
result[~mask] = 2 * a[~mask] * (1 - b[~mask]) + np.sqrt(a[~mask]) * (2 * b[~mask] - 1)
|
|
566
|
+
return result
|
|
567
|
+
|
|
568
|
+
def _blend_hard_light(self, a: np.ndarray, b: np.ndarray, **kwargs) -> np.ndarray:
|
|
569
|
+
"""Hard Light - Overlay, но с swapped inputs"""
|
|
570
|
+
return self._blend_overlay(b, a) # Просто меняем местами a и b
|
|
571
|
+
|
|
572
|
+
def _blend_vivid_light(self, a: np.ndarray, b: np.ndarray, **kwargs) -> np.ndarray:
|
|
573
|
+
"""Vivid Light - комбинация Color Burn и Color Dodge"""
|
|
574
|
+
eps = 1e-7
|
|
575
|
+
mask = b < 0.5
|
|
576
|
+
result = np.zeros_like(a)
|
|
577
|
+
|
|
578
|
+
# Color Burn для темных областей
|
|
579
|
+
result[mask] = 1 - (1 - a[mask]) / np.maximum(2 * b[mask], eps)
|
|
580
|
+
|
|
581
|
+
# Color Dodge для светлых областей
|
|
582
|
+
result[~mask] = a[~mask] / np.maximum(2 * (1 - b[~mask]), eps)
|
|
583
|
+
|
|
584
|
+
return np.clip(result, 0, 1)
|
|
585
|
+
|
|
586
|
+
def _blend_linear_light(self, a: np.ndarray, b: np.ndarray, **kwargs) -> np.ndarray:
|
|
587
|
+
"""Linear Light - комбинация Linear Burn и Linear Dodge"""
|
|
588
|
+
result = a + 2 * b - 1
|
|
589
|
+
return np.clip(result, 0, 1)
|
|
590
|
+
|
|
591
|
+
def _blend_pin_light(self, a: np.ndarray, b: np.ndarray, **kwargs) -> np.ndarray:
|
|
592
|
+
"""Pin Light - комбинация Darken и Lighten"""
|
|
593
|
+
mask1 = b < 0.5
|
|
594
|
+
mask2 = b > 0.5
|
|
595
|
+
|
|
596
|
+
result = a.copy()
|
|
597
|
+
result[mask1] = np.minimum(a[mask1], 2 * b[mask1])
|
|
598
|
+
result[mask2] = np.maximum(a[mask2], 2 * (b[mask2] - 0.5))
|
|
599
|
+
|
|
600
|
+
return result
|
|
601
|
+
|
|
602
|
+
def _blend_hard_mix(self, a: np.ndarray, b: np.ndarray, **kwargs) -> np.ndarray:
|
|
603
|
+
"""Hard Mix - пороговое смешивание"""
|
|
604
|
+
result = self._blend_vivid_light(a, b)
|
|
605
|
+
return np.where(result < 0.5, 0, 1)
|
|
606
|
+
|
|
607
|
+
def _blend_difference(self, a: np.ndarray, b: np.ndarray, **kwargs) -> np.ndarray:
|
|
608
|
+
"""Difference - абсолютная разница"""
|
|
609
|
+
return np.abs(a - b)
|
|
610
|
+
|
|
611
|
+
def _blend_subtract(self, a: np.ndarray, b: np.ndarray, **kwargs) -> np.ndarray:
|
|
612
|
+
"""Subtract - вычитание"""
|
|
613
|
+
return np.maximum(a - b, 0)
|
|
614
|
+
|
|
615
|
+
def _blend_divide(self, a: np.ndarray, b: np.ndarray, **kwargs) -> np.ndarray:
|
|
616
|
+
"""Divide - деление"""
|
|
617
|
+
eps = 1e-7
|
|
618
|
+
return a / np.maximum(b, eps)
|
|
619
|
+
|
|
620
|
+
def _blend_hue(self, a: np.ndarray, b: np.ndarray, **kwargs) -> np.ndarray:
|
|
621
|
+
"""Сохраняет hue из b, saturation и luminosity из a"""
|
|
622
|
+
a_hsv = self._rgb_to_hsv(a)
|
|
623
|
+
b_hsv = self._rgb_to_hsv(b)
|
|
624
|
+
|
|
625
|
+
result_hsv = np.stack([b_hsv[..., 0], a_hsv[..., 1], a_hsv[..., 2]], axis=-1)
|
|
626
|
+
return self._hsv_to_rgb(result_hsv)
|
|
627
|
+
|
|
628
|
+
def _blend_saturation(self, a: np.ndarray, b: np.ndarray, **kwargs) -> np.ndarray:
|
|
629
|
+
"""Сохраняет saturation из b, hue и luminosity из a"""
|
|
630
|
+
a_hsv = self._rgb_to_hsv(a)
|
|
631
|
+
b_hsv = self._rgb_to_hsv(b)
|
|
632
|
+
|
|
633
|
+
result_hsv = np.stack([a_hsv[..., 0], b_hsv[..., 1], a_hsv[..., 2]], axis=-1)
|
|
634
|
+
return self._hsv_to_rgb(result_hsv)
|
|
635
|
+
|
|
636
|
+
def _blend_color(self, a: np.ndarray, b: np.ndarray, **kwargs) -> np.ndarray:
|
|
637
|
+
"""Сохраняет hue и saturation из b, luminosity из a"""
|
|
638
|
+
a_hsv = self._rgb_to_hsv(a)
|
|
639
|
+
b_hsv = self._rgb_to_hsv(b)
|
|
640
|
+
|
|
641
|
+
result_hsv = np.stack([b_hsv[..., 0], b_hsv[..., 1], a_hsv[..., 2]], axis=-1)
|
|
642
|
+
return self._hsv_to_rgb(result_hsv)
|
|
643
|
+
|
|
644
|
+
def _blend_luminosity(self, a: np.ndarray, b: np.ndarray, **kwargs) -> np.ndarray:
|
|
645
|
+
"""Сохраняет luminosity из b, hue и saturation из a"""
|
|
646
|
+
a_hsv = self._rgb_to_hsv(a)
|
|
647
|
+
b_hsv = self._rgb_to_hsv(b)
|
|
648
|
+
|
|
649
|
+
result_hsv = np.stack([a_hsv[..., 0], a_hsv[..., 1], b_hsv[..., 2]], axis=-1)
|
|
650
|
+
return self._hsv_to_rgb(result_hsv)
|
|
651
|
+
|
|
652
|
+
def _blend_height_based(self, a: np.ndarray, b: np.ndarray,
|
|
653
|
+
height_map: np.ndarray, **kwargs) -> np.ndarray:
|
|
654
|
+
"""Смешивание на основе карты высот"""
|
|
655
|
+
# Нормализуем карту высот
|
|
656
|
+
height_norm = (height_map - height_map.min()) / (height_map.max() - height_map.min())
|
|
657
|
+
|
|
658
|
+
# Параметры перехода
|
|
659
|
+
low_threshold = kwargs.get('low_threshold', 0.3)
|
|
660
|
+
high_threshold = kwargs.get('high_threshold', 0.7)
|
|
661
|
+
transition = kwargs.get('transition', 0.1)
|
|
662
|
+
|
|
663
|
+
# Создаем маску смешивания
|
|
664
|
+
blend_mask = np.zeros_like(height_norm)
|
|
665
|
+
|
|
666
|
+
# Нижний переход
|
|
667
|
+
mask_low = height_norm < low_threshold
|
|
668
|
+
blend_mask[mask_low] = 0
|
|
669
|
+
|
|
670
|
+
# Верхний переход
|
|
671
|
+
mask_high = height_norm > high_threshold
|
|
672
|
+
blend_mask[mask_high] = 1
|
|
673
|
+
|
|
674
|
+
# Переходная зона
|
|
675
|
+
mask_transition = ~mask_low & ~mask_high
|
|
676
|
+
height_transition = height_norm[mask_transition]
|
|
677
|
+
|
|
678
|
+
# Плавный переход в переходной зоне
|
|
679
|
+
t = (height_transition - low_threshold) / (high_threshold - low_threshold)
|
|
680
|
+
if transition > 0:
|
|
681
|
+
t = smoothstep(0, 1, t)
|
|
682
|
+
|
|
683
|
+
blend_mask[mask_transition] = t
|
|
684
|
+
|
|
685
|
+
# Применяем смешивание
|
|
686
|
+
if blend_mask.ndim == 2 and a.ndim == 3:
|
|
687
|
+
blend_mask = blend_mask[..., np.newaxis]
|
|
688
|
+
|
|
689
|
+
return lerp(a, b, blend_mask)
|
|
690
|
+
|
|
691
|
+
def _blend_slope_based(self, a: np.ndarray, b: np.ndarray,
|
|
692
|
+
height_map: np.ndarray, **kwargs) -> np.ndarray:
|
|
693
|
+
"""Смешивание на основе наклона (slope)"""
|
|
694
|
+
# Вычисляем градиент высоты
|
|
695
|
+
grad_x = np.gradient(height_map, axis=1)
|
|
696
|
+
grad_y = np.gradient(height_map, axis=0)
|
|
697
|
+
slope = np.sqrt(grad_x**2 + grad_y**2)
|
|
698
|
+
|
|
699
|
+
# Нормализуем наклон
|
|
700
|
+
slope_norm = slope / np.max(slope)
|
|
701
|
+
|
|
702
|
+
# Параметры
|
|
703
|
+
max_slope = kwargs.get('max_slope', 0.5)
|
|
704
|
+
sharpness = kwargs.get('sharpness', 10.0)
|
|
705
|
+
|
|
706
|
+
# Маска: 1 на ровных поверхностях, 0 на крутых склонах
|
|
707
|
+
blend_mask = np.clip(1.0 - slope_norm / max_slope, 0, 1)
|
|
708
|
+
|
|
709
|
+
# Применяем резкость перехода
|
|
710
|
+
if sharpness != 1.0:
|
|
711
|
+
blend_mask = np.power(blend_mask, sharpness)
|
|
712
|
+
|
|
713
|
+
# Расширяем маску для многоканальных текстур
|
|
714
|
+
if blend_mask.ndim == 2 and a.ndim == 3:
|
|
715
|
+
blend_mask = blend_mask[..., np.newaxis]
|
|
716
|
+
|
|
717
|
+
return lerp(b, a, blend_mask) # На склонах - текстура A, на равнинах - B
|
|
718
|
+
|
|
719
|
+
def _blend_edge_aware(self, a: np.ndarray, b: np.ndarray, **kwargs) -> np.ndarray:
|
|
720
|
+
"""Edge-aware blending - сохраняет границы"""
|
|
721
|
+
# Вычисляем градиенты обеих текстур
|
|
722
|
+
if a.ndim == 3:
|
|
723
|
+
# Используем luminance для градиентов
|
|
724
|
+
a_gray = self._rgb_to_luminance(a)
|
|
725
|
+
b_gray = self._rgb_to_luminance(b)
|
|
726
|
+
else:
|
|
727
|
+
a_gray = a
|
|
728
|
+
b_gray = b
|
|
729
|
+
|
|
730
|
+
# Градиенты
|
|
731
|
+
a_grad_x = np.gradient(a_gray, axis=1)
|
|
732
|
+
a_grad_y = np.gradient(a_gray, axis=0)
|
|
733
|
+
a_grad = np.sqrt(a_grad_x**2 + a_grad_y**2)
|
|
734
|
+
|
|
735
|
+
b_grad_x = np.gradient(b_gray, axis=1)
|
|
736
|
+
b_grad_y = np.gradient(b_gray, axis=0)
|
|
737
|
+
b_grad = np.sqrt(b_grad_x**2 + b_grad_y**2)
|
|
738
|
+
|
|
739
|
+
# Маска на основе сравнения градиентов
|
|
740
|
+
# Сохраняем более выраженные границы
|
|
741
|
+
edge_strength = np.maximum(a_grad, b_grad)
|
|
742
|
+
mask = a_grad / np.maximum(edge_strength, 1e-7)
|
|
743
|
+
|
|
744
|
+
# Расширяем маску
|
|
745
|
+
if mask.ndim == 2 and a.ndim == 3:
|
|
746
|
+
mask = mask[..., np.newaxis]
|
|
747
|
+
|
|
748
|
+
return lerp(b, a, mask)
|
|
749
|
+
|
|
750
|
+
def _blend_detail_preserving(self, a: np.ndarray, b: np.ndarray, **kwargs) -> np.ndarray:
|
|
751
|
+
"""Смешивание с сохранением деталей"""
|
|
752
|
+
# Выделяем высокочастотные детали из A
|
|
753
|
+
from scipy import ndimage
|
|
754
|
+
|
|
755
|
+
# Низкочастотная версия A
|
|
756
|
+
a_low = ndimage.gaussian_filter(a, sigma=kwargs.get('sigma', 2.0))
|
|
757
|
+
|
|
758
|
+
# Высокочастотные детали A
|
|
759
|
+
a_high = a - a_low
|
|
760
|
+
|
|
761
|
+
# Смешиваем низкие частоты
|
|
762
|
+
b_low = ndimage.gaussian_filter(b, sigma=kwargs.get('sigma', 2.0))
|
|
763
|
+
blended_low = self._blend_overlay(a_low, b_low)
|
|
764
|
+
|
|
765
|
+
# Добавляем обратно высокочастотные детали из A
|
|
766
|
+
result = blended_low + a_high * kwargs.get('detail_strength', 0.7)
|
|
767
|
+
|
|
768
|
+
return np.clip(result, 0, 1)
|
|
769
|
+
|
|
770
|
+
# ------------------------------------------------------------------
|
|
771
|
+
# Вспомогательные функции
|
|
772
|
+
# ------------------------------------------------------------------
|
|
773
|
+
|
|
774
|
+
def _gamma_correct(self, img: np.ndarray, inverse: bool = False) -> np.ndarray:
|
|
775
|
+
"""Гамма-коррекция для линейного/перцептуального пространства"""
|
|
776
|
+
if self.blend_space == "linear" or not self.gamma_correction:
|
|
777
|
+
return img
|
|
778
|
+
|
|
779
|
+
gamma = 2.2
|
|
780
|
+
if inverse:
|
|
781
|
+
return np.power(img, gamma)
|
|
782
|
+
else:
|
|
783
|
+
return np.power(img, 1.0/gamma)
|
|
784
|
+
|
|
785
|
+
def _rgb_to_hsv(self, rgb: np.ndarray) -> np.ndarray:
|
|
786
|
+
"""Конвертация RGB в HSV"""
|
|
787
|
+
# Упрощенная реализация
|
|
788
|
+
max_val = np.max(rgb, axis=-1)
|
|
789
|
+
min_val = np.min(rgb, axis=-1)
|
|
790
|
+
delta = max_val - min_val
|
|
791
|
+
|
|
792
|
+
hue = np.zeros_like(max_val)
|
|
793
|
+
saturation = np.zeros_like(max_val)
|
|
794
|
+
value = max_val
|
|
795
|
+
|
|
796
|
+
# Hue
|
|
797
|
+
mask = delta > 0
|
|
798
|
+
r, g, b = rgb[..., 0], rgb[..., 1], rgb[..., 2]
|
|
799
|
+
|
|
800
|
+
# Красный - максимум
|
|
801
|
+
mask_r = mask & (max_val == r)
|
|
802
|
+
hue[mask_r] = (g[mask_r] - b[mask_r]) / delta[mask_r]
|
|
803
|
+
|
|
804
|
+
# Зеленый - максимум
|
|
805
|
+
mask_g = mask & (max_val == g)
|
|
806
|
+
hue[mask_g] = 2.0 + (b[mask_g] - r[mask_g]) / delta[mask_g]
|
|
807
|
+
|
|
808
|
+
# Синий - максимум
|
|
809
|
+
mask_b = mask & (max_val == b)
|
|
810
|
+
hue[mask_b] = 4.0 + (r[mask_b] - g[mask_b]) / delta[mask_b]
|
|
811
|
+
|
|
812
|
+
hue = (hue / 6.0) % 1.0
|
|
813
|
+
|
|
814
|
+
# Saturation
|
|
815
|
+
saturation[mask] = delta[mask] / max_val[mask]
|
|
816
|
+
|
|
817
|
+
return np.stack([hue, saturation, value], axis=-1)
|
|
818
|
+
|
|
819
|
+
def _hsv_to_rgb(self, hsv: np.ndarray) -> np.ndarray:
|
|
820
|
+
"""Конвертация HSV в RGB"""
|
|
821
|
+
h, s, v = hsv[..., 0], hsv[..., 1], hsv[..., 2]
|
|
822
|
+
|
|
823
|
+
h = (h * 6.0) % 6.0
|
|
824
|
+
i = np.floor(h).astype(int)
|
|
825
|
+
f = h - i
|
|
826
|
+
|
|
827
|
+
p = v * (1 - s)
|
|
828
|
+
q = v * (1 - s * f)
|
|
829
|
+
t = v * (1 - s * (1 - f))
|
|
830
|
+
|
|
831
|
+
rgb = np.zeros_like(hsv)
|
|
832
|
+
|
|
833
|
+
# 6 случаев
|
|
834
|
+
mask0 = i == 0
|
|
835
|
+
rgb[mask0, 0] = v[mask0]
|
|
836
|
+
rgb[mask0, 1] = t[mask0]
|
|
837
|
+
rgb[mask0, 2] = p[mask0]
|
|
838
|
+
|
|
839
|
+
mask1 = i == 1
|
|
840
|
+
rgb[mask1, 0] = q[mask1]
|
|
841
|
+
rgb[mask1, 1] = v[mask1]
|
|
842
|
+
rgb[mask1, 2] = p[mask1]
|
|
843
|
+
|
|
844
|
+
mask2 = i == 2
|
|
845
|
+
rgb[mask2, 0] = p[mask2]
|
|
846
|
+
rgb[mask2, 1] = v[mask2]
|
|
847
|
+
rgb[mask2, 2] = t[mask2]
|
|
848
|
+
|
|
849
|
+
mask3 = i == 3
|
|
850
|
+
rgb[mask3, 0] = p[mask3]
|
|
851
|
+
rgb[mask3, 1] = q[mask3]
|
|
852
|
+
rgb[mask3, 2] = v[mask3]
|
|
853
|
+
|
|
854
|
+
mask4 = i == 4
|
|
855
|
+
rgb[mask4, 0] = t[mask4]
|
|
856
|
+
rgb[mask4, 1] = p[mask4]
|
|
857
|
+
rgb[mask4, 2] = v[mask4]
|
|
858
|
+
|
|
859
|
+
mask5 = i == 5
|
|
860
|
+
rgb[mask5, 0] = v[mask5]
|
|
861
|
+
rgb[mask5, 1] = p[mask5]
|
|
862
|
+
rgb[mask5, 2] = q[mask5]
|
|
863
|
+
|
|
864
|
+
return np.clip(rgb, 0, 1)
|
|
865
|
+
|
|
866
|
+
def _rgb_to_luminance(self, rgb: np.ndarray) -> np.ndarray:
|
|
867
|
+
"""Конвертация RGB в luminance (яркость)"""
|
|
868
|
+
# Стандартные коэффициенты для восприятия
|
|
869
|
+
return 0.2126 * rgb[..., 0] + 0.7152 * rgb[..., 1] + 0.0722 * rgb[..., 2]
|
|
870
|
+
|
|
871
|
+
# ----------------------------------------------------------------------
|
|
872
|
+
# Специализированные блендеры для terrain и природных материалов
|
|
873
|
+
# ----------------------------------------------------------------------
|
|
874
|
+
|
|
875
|
+
class TerrainTextureBlender:
|
|
876
|
+
"""Специализированный блендер для создания terrain текстур"""
|
|
877
|
+
|
|
878
|
+
def __init__(self, seed: int = 42):
|
|
879
|
+
self.seed = seed
|
|
880
|
+
self.blender = TextureBlender()
|
|
881
|
+
self.mask_gen = BlendMask(seed=seed)
|
|
882
|
+
|
|
883
|
+
# Предопределенные наборы текстур для разных биомов
|
|
884
|
+
self.biome_presets = {
|
|
885
|
+
"temperate": {
|
|
886
|
+
"textures": ["grass", "dirt", "rock", "forest"],
|
|
887
|
+
"height_ranges": [(0.0, 0.3), (0.2, 0.5), (0.4, 0.8), (0.7, 1.0)],
|
|
888
|
+
"slope_thresholds": [0.3, 0.5, 0.7],
|
|
889
|
+
},
|
|
890
|
+
"desert": {
|
|
891
|
+
"textures": ["sand", "rock", "cliff"],
|
|
892
|
+
"height_ranges": [(0.0, 0.4), (0.3, 0.7), (0.6, 1.0)],
|
|
893
|
+
"slope_thresholds": [0.2, 0.4],
|
|
894
|
+
},
|
|
895
|
+
"arctic": {
|
|
896
|
+
"textures": ["snow", "rock", "ice"],
|
|
897
|
+
"height_ranges": [(0.0, 0.5), (0.4, 0.8), (0.7, 1.0)],
|
|
898
|
+
"slope_thresholds": [0.4, 0.6],
|
|
899
|
+
},
|
|
900
|
+
"mountain": {
|
|
901
|
+
"textures": ["forest", "rock", "snow"],
|
|
902
|
+
"height_ranges": [(0.0, 0.4), (0.3, 0.7), (0.6, 1.0)],
|
|
903
|
+
"slope_thresholds": [0.5, 0.8],
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
def create_terrain_material(self,
|
|
908
|
+
height_map: np.ndarray,
|
|
909
|
+
texture_layers: Dict[str, np.ndarray],
|
|
910
|
+
biome: str = "temperate",
|
|
911
|
+
custom_params: Optional[Dict] = None) -> np.ndarray:
|
|
912
|
+
"""
|
|
913
|
+
Создание сложного terrain материала на основе карты высот
|
|
914
|
+
|
|
915
|
+
Args:
|
|
916
|
+
height_map: Карта высот (H, W)
|
|
917
|
+
texture_layers: Словарь текстур для слоев
|
|
918
|
+
biome: Тип биома (preset)
|
|
919
|
+
custom_params: Кастомные параметры смешивания
|
|
920
|
+
|
|
921
|
+
Returns:
|
|
922
|
+
Итоговая terrain текстура (H, W, 4)
|
|
923
|
+
"""
|
|
924
|
+
# Получаем preset для биома
|
|
925
|
+
if biome in self.biome_presets:
|
|
926
|
+
preset = self.biome_presets[biome]
|
|
927
|
+
else:
|
|
928
|
+
preset = self.biome_presets["temperate"]
|
|
929
|
+
|
|
930
|
+
# Объединяем с кастомными параметрами
|
|
931
|
+
params = preset.copy()
|
|
932
|
+
if custom_params:
|
|
933
|
+
params.update(custom_params)
|
|
934
|
+
|
|
935
|
+
# Нормализуем карту высот
|
|
936
|
+
height_norm = (height_map - height_map.min()) / (height_map.max() - height_map.min())
|
|
937
|
+
|
|
938
|
+
# Вычисляем карту наклона
|
|
939
|
+
grad_x = np.gradient(height_norm, axis=1)
|
|
940
|
+
grad_y = np.gradient(height_norm, axis=0)
|
|
941
|
+
slope = np.sqrt(grad_x**2 + grad_y**2)
|
|
942
|
+
|
|
943
|
+
# Начинаем с базовой текстуры
|
|
944
|
+
base_texture_name = params["textures"][0]
|
|
945
|
+
if base_texture_name in texture_layers:
|
|
946
|
+
result = texture_layers[base_texture_name].copy()
|
|
947
|
+
else:
|
|
948
|
+
# Создаем простую текстуру если отсутствует
|
|
949
|
+
result = np.ones((*height_map.shape, 4), dtype=np.float32) * 0.5
|
|
950
|
+
|
|
951
|
+
# Последовательно добавляем слои
|
|
952
|
+
for i in range(1, len(params["textures"])):
|
|
953
|
+
texture_name = params["textures"][i]
|
|
954
|
+
if texture_name not in texture_layers:
|
|
955
|
+
continue
|
|
956
|
+
|
|
957
|
+
# Получаем диапазон высот для этого слоя
|
|
958
|
+
height_range = params["height_ranges"][i]
|
|
959
|
+
|
|
960
|
+
# Создаем маску на основе высоты
|
|
961
|
+
height_mask = np.zeros_like(height_norm)
|
|
962
|
+
|
|
963
|
+
# Плавный переход между слоями
|
|
964
|
+
transition = 0.1 # Ширина переходной зоны
|
|
965
|
+
|
|
966
|
+
# Нижняя граница
|
|
967
|
+
low_start = height_range[0] - transition/2
|
|
968
|
+
low_end = height_range[0] + transition/2
|
|
969
|
+
|
|
970
|
+
# Верхняя граница
|
|
971
|
+
high_start = height_range[1] - transition/2
|
|
972
|
+
high_end = height_range[1] + transition/2
|
|
973
|
+
|
|
974
|
+
# Нижний переход
|
|
975
|
+
mask_low = height_norm <= low_start
|
|
976
|
+
height_mask[mask_low] = 0
|
|
977
|
+
|
|
978
|
+
# Верхний переход
|
|
979
|
+
mask_high = height_norm >= high_end
|
|
980
|
+
height_mask[mask_high] = 0
|
|
981
|
+
|
|
982
|
+
# Нижняя переходная зона
|
|
983
|
+
mask_low_trans = (height_norm > low_start) & (height_norm <= low_end)
|
|
984
|
+
if np.any(mask_low_trans):
|
|
985
|
+
t_low = (height_norm[mask_low_trans] - low_start) / transition
|
|
986
|
+
height_mask[mask_low_trans] = smootherstep(0, 1, t_low)
|
|
987
|
+
|
|
988
|
+
# Верхняя переходная зона
|
|
989
|
+
mask_high_trans = (height_norm >= high_start) & (height_norm < high_end)
|
|
990
|
+
if np.any(mask_high_trans):
|
|
991
|
+
t_high = 1 - (height_norm[mask_high_trans] - high_start) / transition
|
|
992
|
+
height_mask[mask_high_trans] = smootherstep(0, 1, t_high)
|
|
993
|
+
|
|
994
|
+
# Основная зона
|
|
995
|
+
mask_mid = (height_norm > low_end) & (height_norm < high_start)
|
|
996
|
+
height_mask[mask_mid] = 1.0
|
|
997
|
+
|
|
998
|
+
# Корректируем маску на основе наклона для некоторых слоев
|
|
999
|
+
if i > 0: # Для не-базовых слоев
|
|
1000
|
+
slope_threshold = params["slope_thresholds"][i-1] if i-1 < len(params["slope_thresholds"]) else 0.5
|
|
1001
|
+
slope_mask = np.where(slope > slope_threshold, 0, 1)
|
|
1002
|
+
height_mask *= slope_mask
|
|
1003
|
+
|
|
1004
|
+
# Смешиваем слой
|
|
1005
|
+
result = self.blender.blend(
|
|
1006
|
+
result, texture_layers[texture_name],
|
|
1007
|
+
blend_mode="overlay",
|
|
1008
|
+
opacity=1.0,
|
|
1009
|
+
mask=height_mask[..., np.newaxis]
|
|
1010
|
+
)
|
|
1011
|
+
|
|
1012
|
+
return result
|
|
1013
|
+
|
|
1014
|
+
def add_detail_layers(self,
|
|
1015
|
+
base_texture: np.ndarray,
|
|
1016
|
+
detail_textures: List[np.ndarray],
|
|
1017
|
+
scale_factors: List[float] = None,
|
|
1018
|
+
blend_modes: List[str] = None) -> np.ndarray:
|
|
1019
|
+
"""
|
|
1020
|
+
Добавление детализированных слоев (трава, камни, etc.)
|
|
1021
|
+
|
|
1022
|
+
Args:
|
|
1023
|
+
base_texture: Базовая текстура
|
|
1024
|
+
detail_textures: Список текстур деталей
|
|
1025
|
+
scale_factors: Масштаб для каждой текстуры
|
|
1026
|
+
blend_modes: Режимы смешивания для каждой текстуры
|
|
1027
|
+
|
|
1028
|
+
Returns:
|
|
1029
|
+
Детализированная текстура
|
|
1030
|
+
"""
|
|
1031
|
+
result = base_texture.copy()
|
|
1032
|
+
|
|
1033
|
+
if scale_factors is None:
|
|
1034
|
+
scale_factors = [1.0] * len(detail_textures)
|
|
1035
|
+
|
|
1036
|
+
if blend_modes is None:
|
|
1037
|
+
blend_modes = ["overlay"] * len(detail_textures)
|
|
1038
|
+
|
|
1039
|
+
for i, (detail_tex, scale, blend_mode) in enumerate(zip(detail_textures, scale_factors, blend_modes)):
|
|
1040
|
+
# Создаем маску для деталей на основе шума
|
|
1041
|
+
height, width = base_texture.shape[:2]
|
|
1042
|
+
mask_gen = BlendMask(
|
|
1043
|
+
mask_type="noise",
|
|
1044
|
+
parameters={
|
|
1045
|
+
"noise_scale": 0.05 * scale,
|
|
1046
|
+
"octaves": 3,
|
|
1047
|
+
"threshold": 0.6,
|
|
1048
|
+
},
|
|
1049
|
+
)
|
|
1050
|
+
mask = mask_gen.generate(width, height)
|
|
1051
|
+
|
|
1052
|
+
# Применяем детали
|
|
1053
|
+
result = self.blender.blend(
|
|
1054
|
+
result, detail_tex,
|
|
1055
|
+
blend_mode=blend_mode,
|
|
1056
|
+
opacity=0.7,
|
|
1057
|
+
mask=mask[..., np.newaxis]
|
|
1058
|
+
)
|
|
1059
|
+
|
|
1060
|
+
return result
|
|
1061
|
+
|
|
1062
|
+
def create_biome_transition(self,
|
|
1063
|
+
terrain_a: np.ndarray,
|
|
1064
|
+
terrain_b: np.ndarray,
|
|
1065
|
+
transition_map: np.ndarray,
|
|
1066
|
+
transition_width: float = 0.3) -> np.ndarray:
|
|
1067
|
+
"""
|
|
1068
|
+
Создание плавного перехода между двумя биомами
|
|
1069
|
+
|
|
1070
|
+
Args:
|
|
1071
|
+
terrain_a: Текстура первого биома
|
|
1072
|
+
terrain_b: Текстура второго биома
|
|
1073
|
+
transition_map: Карта перехода (0 = биом A, 1 = биом B)
|
|
1074
|
+
transition_width: Ширина переходной зоны
|
|
1075
|
+
|
|
1076
|
+
Returns:
|
|
1077
|
+
Текстура с плавным переходом
|
|
1078
|
+
"""
|
|
1079
|
+
# Создаем маску перехода с плавными краями
|
|
1080
|
+
transition_smooth = np.zeros_like(transition_map)
|
|
1081
|
+
|
|
1082
|
+
# Плавные переходы
|
|
1083
|
+
mask_low = transition_map <= 0.5 - transition_width/2
|
|
1084
|
+
transition_smooth[mask_low] = 0
|
|
1085
|
+
|
|
1086
|
+
mask_high = transition_map >= 0.5 + transition_width/2
|
|
1087
|
+
transition_smooth[mask_high] = 1
|
|
1088
|
+
|
|
1089
|
+
# Переходная зона
|
|
1090
|
+
mask_trans = ~mask_low & ~mask_high
|
|
1091
|
+
if np.any(mask_trans):
|
|
1092
|
+
t = (transition_map[mask_trans] - (0.5 - transition_width/2)) / transition_width
|
|
1093
|
+
transition_smooth[mask_trans] = smootherstep(0, 1, t)
|
|
1094
|
+
|
|
1095
|
+
# Расширяем маску для RGB(A)
|
|
1096
|
+
if transition_smooth.ndim == 2 and terrain_a.ndim == 3:
|
|
1097
|
+
transition_smooth = transition_smooth[..., np.newaxis]
|
|
1098
|
+
|
|
1099
|
+
# Смешиваем
|
|
1100
|
+
return lerp(terrain_a, terrain_b, transition_smooth)
|
|
1101
|
+
|
|
1102
|
+
# ----------------------------------------------------------------------
|
|
1103
|
+
# Продвинутые алгоритмы смешивания для специальных эффектов
|
|
1104
|
+
# ----------------------------------------------------------------------
|
|
1105
|
+
|
|
1106
|
+
class AdvancedTextureBlending:
|
|
1107
|
+
"""Продвинутые алгоритмы для сложных эффектов смешивания"""
|
|
1108
|
+
|
|
1109
|
+
@staticmethod
|
|
1110
|
+
def triplanar_mapping(texture: np.ndarray,
|
|
1111
|
+
positions: np.ndarray,
|
|
1112
|
+
normals: np.ndarray,
|
|
1113
|
+
scale: float = 1.0) -> np.ndarray:
|
|
1114
|
+
"""
|
|
1115
|
+
Triplanar mapping - проекция текстуры на 3D поверхность
|
|
1116
|
+
без искажений на крутых склонах
|
|
1117
|
+
|
|
1118
|
+
Args:
|
|
1119
|
+
texture: 2D текстура для проекции
|
|
1120
|
+
positions: Позиции вершин (N, 3)
|
|
1121
|
+
normals: Нормали вершин (N, 3)
|
|
1122
|
+
scale: Масштаб текстуры
|
|
1123
|
+
|
|
1124
|
+
Returns:
|
|
1125
|
+
Текстура, спроецированная на 3D поверхность
|
|
1126
|
+
"""
|
|
1127
|
+
# Для каждой оси (X, Y, Z) проецируем текстуру
|
|
1128
|
+
# и смешиваем на основе нормалей
|
|
1129
|
+
|
|
1130
|
+
# Упрощенная реализация для демонстрации
|
|
1131
|
+
# В реальности нужна более сложная логика для 3D текстур
|
|
1132
|
+
|
|
1133
|
+
# Веса для каждой плоскости
|
|
1134
|
+
weights = np.abs(normals)
|
|
1135
|
+
weights = weights / np.sum(weights, axis=1, keepdims=True)
|
|
1136
|
+
|
|
1137
|
+
# Здесь должна быть логика выборки из текстуры для каждой плоскости
|
|
1138
|
+
# ...
|
|
1139
|
+
|
|
1140
|
+
# Возвращаем placeholder
|
|
1141
|
+
return np.zeros((*positions.shape[:-1], texture.shape[-1]), dtype=np.float32)
|
|
1142
|
+
|
|
1143
|
+
@staticmethod
|
|
1144
|
+
def parallax_occlusion_mapping(height_map: np.ndarray,
|
|
1145
|
+
base_texture: np.ndarray,
|
|
1146
|
+
view_direction: np.ndarray,
|
|
1147
|
+
strength: float = 0.1,
|
|
1148
|
+
num_layers: int = 10) -> np.ndarray:
|
|
1149
|
+
"""
|
|
1150
|
+
Parallax Occlusion Mapping - псевдо-3D эффект
|
|
1151
|
+
|
|
1152
|
+
Args:
|
|
1153
|
+
height_map: Карта высот для смещения
|
|
1154
|
+
base_texture: Базовая текстура
|
|
1155
|
+
view_direction: Направление взгляда (H, W, 3)
|
|
1156
|
+
strength: Сила эффекта
|
|
1157
|
+
num_layers: Количество слоев для ray marching
|
|
1158
|
+
|
|
1159
|
+
Returns:
|
|
1160
|
+
Текстура с эффектом глубины
|
|
1161
|
+
"""
|
|
1162
|
+
height, width = height_map.shape[:2]
|
|
1163
|
+
result = np.zeros_like(base_texture)
|
|
1164
|
+
|
|
1165
|
+
# Упрощенная реализация
|
|
1166
|
+
for i in range(num_layers):
|
|
1167
|
+
# Вычисляем текущий слой
|
|
1168
|
+
layer_depth = i / num_layers
|
|
1169
|
+
|
|
1170
|
+
# Смещение на основе карты высот и направления взгляда
|
|
1171
|
+
offset = view_direction[..., :2] * strength * layer_depth * height_map[..., np.newaxis]
|
|
1172
|
+
|
|
1173
|
+
# Вычисляем координаты для выборки
|
|
1174
|
+
# (нужна реальная логика билинейной интерполяции)
|
|
1175
|
+
pass
|
|
1176
|
+
|
|
1177
|
+
return result
|
|
1178
|
+
|
|
1179
|
+
@staticmethod
|
|
1180
|
+
def distance_field_blending(texture_a: np.ndarray,
|
|
1181
|
+
texture_b: np.ndarray,
|
|
1182
|
+
distance_field: np.ndarray,
|
|
1183
|
+
threshold: float = 0.5,
|
|
1184
|
+
smoothness: float = 0.1) -> np.ndarray:
|
|
1185
|
+
"""
|
|
1186
|
+
Смешивание на основе distance field (полей расстояний)
|
|
1187
|
+
|
|
1188
|
+
Args:
|
|
1189
|
+
texture_a: Текстура A
|
|
1190
|
+
texture_b: Текстура B
|
|
1191
|
+
distance_field: Поле расстояний (чем ближе к 0, тем ближе к границе)
|
|
1192
|
+
threshold: Пороговое значение
|
|
1193
|
+
smoothness: Плавность перехода
|
|
1194
|
+
|
|
1195
|
+
Returns:
|
|
1196
|
+
Смешанная текстура
|
|
1197
|
+
"""
|
|
1198
|
+
# Нормализуем distance field
|
|
1199
|
+
df_norm = (distance_field - distance_field.min()) / (distance_field.max() - distance_field.min())
|
|
1200
|
+
|
|
1201
|
+
# Создаем маску на основе расстояния до границы
|
|
1202
|
+
mask = np.zeros_like(df_norm)
|
|
1203
|
+
|
|
1204
|
+
# Область A
|
|
1205
|
+
mask_a = df_norm < threshold - smoothness
|
|
1206
|
+
mask[mask_a] = 0
|
|
1207
|
+
|
|
1208
|
+
# Область B
|
|
1209
|
+
mask_b = df_norm > threshold + smoothness
|
|
1210
|
+
mask[mask_b] = 1
|
|
1211
|
+
|
|
1212
|
+
# Переходная область
|
|
1213
|
+
mask_trans = ~mask_a & ~mask_b
|
|
1214
|
+
if np.any(mask_trans):
|
|
1215
|
+
t = (df_norm[mask_trans] - (threshold - smoothness)) / (2 * smoothness)
|
|
1216
|
+
mask[mask_trans] = smootherstep(0, 1, t)
|
|
1217
|
+
|
|
1218
|
+
# Расширяем маску если нужно
|
|
1219
|
+
if mask.ndim == 2 and texture_a.ndim == 3:
|
|
1220
|
+
mask = mask[..., np.newaxis]
|
|
1221
|
+
|
|
1222
|
+
return lerp(texture_a, texture_b, mask)
|
|
1223
|
+
|
|
1224
|
+
# ----------------------------------------------------------------------
|
|
1225
|
+
# Примеры использования
|
|
1226
|
+
# ----------------------------------------------------------------------
|
|
1227
|
+
|
|
1228
|
+
def example_terrain_creation():
|
|
1229
|
+
"""Пример создания сложного terrain материала"""
|
|
1230
|
+
|
|
1231
|
+
print("Creating complex terrain material example...")
|
|
1232
|
+
|
|
1233
|
+
# Инициализация
|
|
1234
|
+
terrain_blender = TerrainTextureBlender(seed=42)
|
|
1235
|
+
|
|
1236
|
+
# Создаем тестовые текстуры (в реальности они были бы сгенерированы)
|
|
1237
|
+
height, width = 512, 512
|
|
1238
|
+
|
|
1239
|
+
# Карта высот (симуляция горного ландшафта)
|
|
1240
|
+
x = np.linspace(0, 10, width)
|
|
1241
|
+
y = np.linspace(0, 10, height)
|
|
1242
|
+
xx, yy = np.meshgrid(x, y)
|
|
1243
|
+
|
|
1244
|
+
height_map = np.sin(xx) * np.cos(yy) * 0.5 + 0.5
|
|
1245
|
+
|
|
1246
|
+
# Тестовые текстуры слоев
|
|
1247
|
+
texture_layers = {}
|
|
1248
|
+
|
|
1249
|
+
# Grass texture (зеленый)
|
|
1250
|
+
grass = np.zeros((height, width, 4), dtype=np.float32)
|
|
1251
|
+
grass[..., 0] = 0.2 + np.sin(xx * 5) * 0.1 # R
|
|
1252
|
+
grass[..., 1] = 0.6 + np.cos(yy * 3) * 0.2 # G
|
|
1253
|
+
grass[..., 2] = 0.1 + np.sin(xx + yy) * 0.05 # B
|
|
1254
|
+
grass[..., 3] = 1.0 # A
|
|
1255
|
+
texture_layers["grass"] = grass
|
|
1256
|
+
|
|
1257
|
+
# Dirt texture (коричневый)
|
|
1258
|
+
dirt = np.zeros((height, width, 4), dtype=np.float32)
|
|
1259
|
+
dirt[..., 0] = 0.4 + np.sin(xx * 3) * 0.1 # R
|
|
1260
|
+
dirt[..., 1] = 0.3 + np.cos(yy * 2) * 0.1 # G
|
|
1261
|
+
dirt[..., 2] = 0.2 + np.sin(xx * yy) * 0.05 # B
|
|
1262
|
+
dirt[..., 3] = 1.0 # A
|
|
1263
|
+
texture_layers["dirt"] = dirt
|
|
1264
|
+
|
|
1265
|
+
# Rock texture (серый)
|
|
1266
|
+
rock = np.zeros((height, width, 4), dtype=np.float32)
|
|
1267
|
+
rock[..., 0] = 0.5 + np.sin(xx * 10) * 0.2 # R
|
|
1268
|
+
rock[..., 1] = 0.5 + np.cos(yy * 8) * 0.2 # G
|
|
1269
|
+
rock[..., 2] = 0.5 + np.sin(xx * yy * 2) * 0.2 # B
|
|
1270
|
+
rock[..., 3] = 1.0 # A
|
|
1271
|
+
texture_layers["rock"] = rock
|
|
1272
|
+
|
|
1273
|
+
# Forest texture (темно-зеленый)
|
|
1274
|
+
forest = np.zeros((height, width, 4), dtype=np.float32)
|
|
1275
|
+
forest[..., 0] = 0.1 + np.sin(xx * 7) * 0.05 # R
|
|
1276
|
+
forest[..., 1] = 0.4 + np.cos(yy * 5) * 0.1 # G
|
|
1277
|
+
forest[..., 2] = 0.1 + np.sin(xx + yy * 2) * 0.05 # B
|
|
1278
|
+
forest[..., 3] = 1.0 # A
|
|
1279
|
+
texture_layers["forest"] = forest
|
|
1280
|
+
|
|
1281
|
+
# Создаем terrain материал
|
|
1282
|
+
terrain = terrain_blender.create_terrain_material(
|
|
1283
|
+
height_map=height_map,
|
|
1284
|
+
texture_layers=texture_layers,
|
|
1285
|
+
biome="mountain",
|
|
1286
|
+
custom_params={
|
|
1287
|
+
"height_ranges": [(0.0, 0.3), (0.2, 0.5), (0.4, 0.8), (0.7, 1.0)],
|
|
1288
|
+
"slope_thresholds": [0.4, 0.6, 0.8]
|
|
1289
|
+
}
|
|
1290
|
+
)
|
|
1291
|
+
|
|
1292
|
+
print(f"Terrain texture shape: {terrain.shape}")
|
|
1293
|
+
print(f"Min/Max: {terrain.min():.3f}/{terrain.max():.3f}")
|
|
1294
|
+
|
|
1295
|
+
return terrain
|
|
1296
|
+
|
|
1297
|
+
def example_advanced_blending():
|
|
1298
|
+
"""Пример продвинутого смешивания текстур"""
|
|
1299
|
+
|
|
1300
|
+
print("\nAdvanced texture blending example...")
|
|
1301
|
+
|
|
1302
|
+
# Инициализация
|
|
1303
|
+
blender = TextureBlender()
|
|
1304
|
+
|
|
1305
|
+
# Создаем тестовые текстуры
|
|
1306
|
+
size = 256
|
|
1307
|
+
texture1 = np.zeros((size, size, 3), dtype=np.float32)
|
|
1308
|
+
texture2 = np.zeros((size, size, 3), dtype=np.float32)
|
|
1309
|
+
|
|
1310
|
+
# Простые градиентные текстуры
|
|
1311
|
+
for i in range(size):
|
|
1312
|
+
for j in range(size):
|
|
1313
|
+
# Текстура 1: Вертикальный градиент
|
|
1314
|
+
texture1[i, j, 0] = i / size # Красный
|
|
1315
|
+
texture1[i, j, 1] = j / size # Зеленый
|
|
1316
|
+
texture1[i, j, 2] = 0.5 # Синий
|
|
1317
|
+
|
|
1318
|
+
# Текстура 2: Горизонтальный градиент с шумом
|
|
1319
|
+
texture2[i, j, 0] = j / size + np.sin(i * 0.1) * 0.2
|
|
1320
|
+
texture2[i, j, 1] = i / size * 0.5
|
|
1321
|
+
texture2[i, j, 2] = j / size
|
|
1322
|
+
|
|
1323
|
+
# Создаем маску смешивания (радиальный градиент)
|
|
1324
|
+
mask_gen = BlendMask(mask_type="gradient",
|
|
1325
|
+
parameters={"center": (0.5, 0.5), "radius": 0.8})
|
|
1326
|
+
mask = mask_gen.generate(size, size, scale=0.01)
|
|
1327
|
+
|
|
1328
|
+
# Тестируем разные режимы смешивания
|
|
1329
|
+
blend_modes = ["overlay", "multiply", "screen", "soft_light", "hard_light"]
|
|
1330
|
+
|
|
1331
|
+
results = {}
|
|
1332
|
+
for mode in blend_modes:
|
|
1333
|
+
result = blender.blend(texture1, texture2,
|
|
1334
|
+
blend_mode=mode,
|
|
1335
|
+
opacity=1.0,
|
|
1336
|
+
mask=mask)
|
|
1337
|
+
results[mode] = result
|
|
1338
|
+
|
|
1339
|
+
print(f"Generated {len(results)} blended textures")
|
|
1340
|
+
|
|
1341
|
+
return results
|
|
1342
|
+
|
|
1343
|
+
if __name__ == "__main__":
|
|
1344
|
+
print("Texture Blending Algorithms")
|
|
1345
|
+
print("=" * 60)
|
|
1346
|
+
|
|
1347
|
+
# Пример создания terrain
|
|
1348
|
+
terrain = example_terrain_creation()
|
|
1349
|
+
|
|
1350
|
+
# Пример продвинутого смешивания
|
|
1351
|
+
blended_textures = example_advanced_blending()
|
|
1352
|
+
|
|
1353
|
+
print("\n" + "=" * 60)
|
|
1354
|
+
print("Available blend modes in TextureBlender:")
|
|
1355
|
+
print("-" * 40)
|
|
1356
|
+
|
|
1357
|
+
blender = TextureBlender()
|
|
1358
|
+
modes = list(blender.blend_modes.keys())
|
|
1359
|
+
|
|
1360
|
+
# Группируем по категориям
|
|
1361
|
+
categories = {
|
|
1362
|
+
"Basic": ["normal", "dissolve"],
|
|
1363
|
+
"Darkening": ["darken", "multiply", "color_burn", "linear_burn"],
|
|
1364
|
+
"Lightening": ["lighten", "screen", "color_dodge", "linear_dodge", "add"],
|
|
1365
|
+
"Contrast": ["overlay", "soft_light", "hard_light", "vivid_light",
|
|
1366
|
+
"linear_light", "pin_light", "hard_mix"],
|
|
1367
|
+
"Comparative": ["difference", "subtract", "divide"],
|
|
1368
|
+
"Component": ["hue", "saturation", "color", "luminosity"],
|
|
1369
|
+
"Specialized": ["height_blend", "slope_blend", "edge_blend", "detail_preserving"]
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
for category, mode_list in categories.items():
|
|
1373
|
+
print(f"\n{category}:")
|
|
1374
|
+
print(f" {', '.join(mode_list)}")
|
|
1375
|
+
|
|
1376
|
+
print("\n" + "=" * 60)
|
|
1377
|
+
print("Texture blending system ready for use!")
|