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
fractex/simplex_noise.py
ADDED
|
@@ -0,0 +1,1113 @@
|
|
|
1
|
+
# fractex/simplex_noise.py
|
|
2
|
+
"""
|
|
3
|
+
Полная реализация 2D, 3D и 4D симплекс-шума
|
|
4
|
+
На основе алгоритма Стифана Густавсона (Stefan Gustavson)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from typing import Tuple, Optional, Union
|
|
9
|
+
from numba import jit, prange
|
|
10
|
+
import math
|
|
11
|
+
|
|
12
|
+
# ----------------------------------------------------------------------
|
|
13
|
+
# Константы для симплекс-шума
|
|
14
|
+
# ----------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
# Градиенты для 2D, 3D и 4D шума
|
|
17
|
+
_GRAD3 = np.array([
|
|
18
|
+
[1, 1, 0], [-1, 1, 0], [1, -1, 0], [-1, -1, 0],
|
|
19
|
+
[1, 0, 1], [-1, 0, 1], [1, 0, -1], [-1, 0, -1],
|
|
20
|
+
[0, 1, 1], [0, -1, 1], [0, 1, -1], [0, -1, -1]
|
|
21
|
+
], dtype=np.float32)
|
|
22
|
+
|
|
23
|
+
_GRAD4 = np.array([
|
|
24
|
+
[0, 1, 1, 1], [0, 1, 1, -1], [0, 1, -1, 1], [0, 1, -1, -1],
|
|
25
|
+
[0, -1, 1, 1], [0, -1, 1, -1], [0, -1, -1, 1], [0, -1, -1, -1],
|
|
26
|
+
[1, 0, 1, 1], [1, 0, 1, -1], [1, 0, -1, 1], [1, 0, -1, -1],
|
|
27
|
+
[-1, 0, 1, 1], [-1, 0, 1, -1], [-1, 0, -1, 1], [-1, 0, -1, -1],
|
|
28
|
+
[1, 1, 0, 1], [1, 1, 0, -1], [1, -1, 0, 1], [1, -1, 0, -1],
|
|
29
|
+
[-1, 1, 0, 1], [-1, 1, 0, -1], [-1, -1, 0, 1], [-1, -1, 0, -1],
|
|
30
|
+
[1, 1, 1, 0], [1, 1, -1, 0], [1, -1, 1, 0], [1, -1, -1, 0],
|
|
31
|
+
[-1, 1, 1, 0], [-1, 1, -1, 0], [-1, -1, 1, 0], [-1, -1, -1, 0]
|
|
32
|
+
], dtype=np.float32)
|
|
33
|
+
|
|
34
|
+
# Таблица перестановок (классическая из шума Перлина)
|
|
35
|
+
_PERM = np.array([
|
|
36
|
+
151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140,
|
|
37
|
+
36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, 247, 120,
|
|
38
|
+
234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33,
|
|
39
|
+
88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, 74, 165, 71,
|
|
40
|
+
134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133,
|
|
41
|
+
230, 220, 105, 92, 41, 55, 46, 245, 40, 244, 102, 143, 54, 65, 25, 63, 161,
|
|
42
|
+
1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169, 200, 196, 135, 130,
|
|
43
|
+
116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64, 52, 217, 226, 250,
|
|
44
|
+
124, 123, 5, 202, 38, 147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227,
|
|
45
|
+
47, 16, 58, 17, 182, 189, 28, 42, 223, 183, 170, 213, 119, 248, 152, 2, 44,
|
|
46
|
+
154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9, 129, 22, 39, 253, 19, 98,
|
|
47
|
+
108, 110, 79, 113, 224, 232, 178, 185, 112, 104, 218, 246, 97, 228, 251, 34,
|
|
48
|
+
242, 193, 238, 210, 144, 12, 191, 179, 162, 241, 81, 51, 145, 235, 249, 14,
|
|
49
|
+
239, 107, 49, 192, 214, 31, 181, 199, 106, 157, 184, 84, 204, 176, 115, 121,
|
|
50
|
+
50, 45, 127, 4, 150, 254, 138, 236, 205, 93, 222, 114, 67, 29, 24, 72, 243,
|
|
51
|
+
141, 128, 195, 78, 66, 215, 61, 156, 180
|
|
52
|
+
], dtype=np.uint8)
|
|
53
|
+
|
|
54
|
+
# Дублируем для быстрого доступа
|
|
55
|
+
_PERM_EXTENDED = np.concatenate([_PERM, _PERM])
|
|
56
|
+
|
|
57
|
+
# Коэффициенты для симплекс-шума
|
|
58
|
+
_F2 = 0.5 * (np.sqrt(3.0) - 1.0)
|
|
59
|
+
_G2 = (3.0 - np.sqrt(3.0)) / 6.0
|
|
60
|
+
_F3 = 1.0 / 3.0
|
|
61
|
+
_G3 = 1.0 / 6.0
|
|
62
|
+
_F4 = (np.sqrt(5.0) - 1.0) / 4.0
|
|
63
|
+
_G4 = (5.0 - np.sqrt(5.0)) / 20.0
|
|
64
|
+
|
|
65
|
+
# ----------------------------------------------------------------------
|
|
66
|
+
# Вспомогательные функции
|
|
67
|
+
# ----------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
@jit(nopython=True, cache=True)
|
|
70
|
+
def _dot2(g: np.ndarray, x: float, y: float) -> float:
|
|
71
|
+
"""Скалярное произведение 2D градиента с вектором (x, y)"""
|
|
72
|
+
return g[0] * x + g[1] * y
|
|
73
|
+
|
|
74
|
+
@jit(nopython=True, cache=True)
|
|
75
|
+
def _dot3(g: np.ndarray, x: float, y: float, z: float) -> float:
|
|
76
|
+
"""Скалярное произведение 3D градиента с вектором (x, y, z)"""
|
|
77
|
+
return g[0] * x + g[1] * y + g[2] * z
|
|
78
|
+
|
|
79
|
+
@jit(nopython=True, cache=True)
|
|
80
|
+
def _dot4(g: np.ndarray, x: float, y: float, z: float, w: float) -> float:
|
|
81
|
+
"""Скалярное произведение 4D градиента с вектором (x, y, z, w)"""
|
|
82
|
+
return g[0] * x + g[1] * y + g[2] * z + g[3] * w
|
|
83
|
+
|
|
84
|
+
@jit(nopython=True, cache=True)
|
|
85
|
+
def _fast_floor(x: float) -> int:
|
|
86
|
+
"""Быстрое вычисление floor для положительных и отрицательных чисел"""
|
|
87
|
+
xi = int(x)
|
|
88
|
+
return xi if x >= xi else xi - 1
|
|
89
|
+
|
|
90
|
+
# ----------------------------------------------------------------------
|
|
91
|
+
# 2D симплекс-шум
|
|
92
|
+
# ----------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
@jit(nopython=True, cache=True)
|
|
95
|
+
def simplex_noise_2d(x: float, y: float, perm: np.ndarray = _PERM_EXTENDED) -> float:
|
|
96
|
+
"""
|
|
97
|
+
2D симплекс-шум
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
x, y: Координаты
|
|
101
|
+
perm: Таблица перестановок (длиной 512)
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Значение шума в диапазоне примерно [-1, 1]
|
|
105
|
+
"""
|
|
106
|
+
# Константы для 2D симплекса
|
|
107
|
+
F2 = _F2
|
|
108
|
+
G2 = _G2
|
|
109
|
+
|
|
110
|
+
# Шаг 1: Скалярная сумма для определения симплекса
|
|
111
|
+
s = (x + y) * F2
|
|
112
|
+
i = _fast_floor(x + s)
|
|
113
|
+
j = _fast_floor(y + s)
|
|
114
|
+
|
|
115
|
+
t = (i + j) * G2
|
|
116
|
+
X0 = i - t
|
|
117
|
+
Y0 = j - t
|
|
118
|
+
x0 = x - X0
|
|
119
|
+
y0 = y - Y0
|
|
120
|
+
|
|
121
|
+
# Шаг 2: Определяем, в каком треугольнике находимся (верхний или нижний)
|
|
122
|
+
i1, j1 = (1, 0) if x0 > y0 else (0, 1)
|
|
123
|
+
|
|
124
|
+
# Координаты внутри симплекса
|
|
125
|
+
x1 = x0 - i1 + G2
|
|
126
|
+
y1 = y0 - j1 + G2
|
|
127
|
+
x2 = x0 - 1.0 + 2.0 * G2
|
|
128
|
+
y2 = y0 - 1.0 + 2.0 * G2
|
|
129
|
+
|
|
130
|
+
# Шаг 3: Хешируем углы симплекса
|
|
131
|
+
ii = i & 255
|
|
132
|
+
jj = j & 255
|
|
133
|
+
|
|
134
|
+
# Градиенты в трех углах
|
|
135
|
+
gi0 = perm[ii + perm[jj]] % 12
|
|
136
|
+
gi1 = perm[ii + i1 + perm[jj + j1]] % 12
|
|
137
|
+
gi2 = perm[ii + 1 + perm[jj + 1]] % 12
|
|
138
|
+
|
|
139
|
+
# Шаг 4: Вычисляем вклад от каждого угла
|
|
140
|
+
t0 = 0.5 - x0 * x0 - y0 * y0
|
|
141
|
+
n0 = 0.0
|
|
142
|
+
if t0 > 0:
|
|
143
|
+
t0 *= t0
|
|
144
|
+
n0 = t0 * t0 * _dot2(_GRAD3[gi0], x0, y0)
|
|
145
|
+
|
|
146
|
+
t1 = 0.5 - x1 * x1 - y1 * y1
|
|
147
|
+
n1 = 0.0
|
|
148
|
+
if t1 > 0:
|
|
149
|
+
t1 *= t1
|
|
150
|
+
n1 = t1 * t1 * _dot2(_GRAD3[gi1], x1, y1)
|
|
151
|
+
|
|
152
|
+
t2 = 0.5 - x2 * x2 - y2 * y2
|
|
153
|
+
n2 = 0.0
|
|
154
|
+
if t2 > 0:
|
|
155
|
+
t2 *= t2
|
|
156
|
+
n2 = t2 * t2 * _dot2(_GRAD3[gi2], x2, y2)
|
|
157
|
+
|
|
158
|
+
# Шаг 5: Возвращаем результат
|
|
159
|
+
return 70.0 * (n0 + n1 + n2)
|
|
160
|
+
|
|
161
|
+
# ----------------------------------------------------------------------
|
|
162
|
+
# 3D симплекс-шум
|
|
163
|
+
# ----------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
@jit(nopython=True, cache=True)
|
|
166
|
+
def simplex_noise_3d(x: float, y: float, z: float, perm: np.ndarray = _PERM_EXTENDED) -> float:
|
|
167
|
+
"""
|
|
168
|
+
3D симплекс-шум
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
x, y, z: Координаты
|
|
172
|
+
perm: Таблица перестановок (длиной 512)
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Значение шума в диапазоне примерно [-1, 1]
|
|
176
|
+
"""
|
|
177
|
+
# Константы для 3D симплекса
|
|
178
|
+
F3 = _F3
|
|
179
|
+
G3 = _G3
|
|
180
|
+
|
|
181
|
+
# Шаг 1: Скалярная сумма для определения симплекса
|
|
182
|
+
s = (x + y + z) * F3
|
|
183
|
+
i = _fast_floor(x + s)
|
|
184
|
+
j = _fast_floor(y + s)
|
|
185
|
+
k = _fast_floor(z + s)
|
|
186
|
+
|
|
187
|
+
t = (i + j + k) * G3
|
|
188
|
+
X0 = i - t
|
|
189
|
+
Y0 = j - t
|
|
190
|
+
Z0 = k - t
|
|
191
|
+
x0 = x - X0
|
|
192
|
+
y0 = y - Y0
|
|
193
|
+
z0 = z - Z0
|
|
194
|
+
|
|
195
|
+
# Шаг 2: Определяем, в каком тетраэдре находимся
|
|
196
|
+
if x0 >= y0:
|
|
197
|
+
if y0 >= z0: # XYZ order
|
|
198
|
+
i1, j1, k1 = 1, 0, 0
|
|
199
|
+
i2, j2, k2 = 1, 1, 0
|
|
200
|
+
elif x0 >= z0: # XZY order
|
|
201
|
+
i1, j1, k1 = 1, 0, 0
|
|
202
|
+
i2, j2, k2 = 1, 0, 1
|
|
203
|
+
else: # ZXY order
|
|
204
|
+
i1, j1, k1 = 0, 0, 1
|
|
205
|
+
i2, j2, k2 = 1, 0, 1
|
|
206
|
+
else:
|
|
207
|
+
if y0 < z0: # ZYX order
|
|
208
|
+
i1, j1, k1 = 0, 0, 1
|
|
209
|
+
i2, j2, k2 = 0, 1, 1
|
|
210
|
+
elif x0 < z0: # YZX order
|
|
211
|
+
i1, j1, k1 = 0, 1, 0
|
|
212
|
+
i2, j2, k2 = 0, 1, 1
|
|
213
|
+
else: # YXZ order
|
|
214
|
+
i1, j1, k1 = 0, 1, 0
|
|
215
|
+
i2, j2, k2 = 1, 1, 0
|
|
216
|
+
|
|
217
|
+
# Координаты внутри симплекса
|
|
218
|
+
x1 = x0 - i1 + G3
|
|
219
|
+
y1 = y0 - j1 + G3
|
|
220
|
+
z1 = z0 - k1 + G3
|
|
221
|
+
x2 = x0 - i2 + 2.0 * G3
|
|
222
|
+
y2 = y0 - j2 + 2.0 * G3
|
|
223
|
+
z2 = z0 - k2 + 2.0 * G3
|
|
224
|
+
x3 = x0 - 1.0 + 3.0 * G3
|
|
225
|
+
y3 = y0 - 1.0 + 3.0 * G3
|
|
226
|
+
z3 = z0 - 1.0 + 3.0 * G3
|
|
227
|
+
|
|
228
|
+
# Шаг 3: Хешируем углы тетраэдра
|
|
229
|
+
ii = i & 255
|
|
230
|
+
jj = j & 255
|
|
231
|
+
kk = k & 255
|
|
232
|
+
|
|
233
|
+
# Градиенты в четырех углах
|
|
234
|
+
gi0 = perm[ii + perm[jj + perm[kk]]] % 12
|
|
235
|
+
gi1 = perm[ii + i1 + perm[jj + j1 + perm[kk + k1]]] % 12
|
|
236
|
+
gi2 = perm[ii + i2 + perm[jj + j2 + perm[kk + k2]]] % 12
|
|
237
|
+
gi3 = perm[ii + 1 + perm[jj + 1 + perm[kk + 1]]] % 12
|
|
238
|
+
|
|
239
|
+
# Шаг 4: Вычисляем вклад от каждого угла
|
|
240
|
+
t0 = 0.6 - x0 * x0 - y0 * y0 - z0 * z0
|
|
241
|
+
n0 = 0.0
|
|
242
|
+
if t0 > 0:
|
|
243
|
+
t0 *= t0
|
|
244
|
+
n0 = t0 * t0 * _dot3(_GRAD3[gi0], x0, y0, z0)
|
|
245
|
+
|
|
246
|
+
t1 = 0.6 - x1 * x1 - y1 * y1 - z1 * z1
|
|
247
|
+
n1 = 0.0
|
|
248
|
+
if t1 > 0:
|
|
249
|
+
t1 *= t1
|
|
250
|
+
n1 = t1 * t1 * _dot3(_GRAD3[gi1], x1, y1, z1)
|
|
251
|
+
|
|
252
|
+
t2 = 0.6 - x2 * x2 - y2 * y2 - z2 * z2
|
|
253
|
+
n2 = 0.0
|
|
254
|
+
if t2 > 0:
|
|
255
|
+
t2 *= t2
|
|
256
|
+
n2 = t2 * t2 * _dot3(_GRAD3[gi2], x2, y2, z2)
|
|
257
|
+
|
|
258
|
+
t3 = 0.6 - x3 * x3 - y3 * y3 - z3 * z3
|
|
259
|
+
n3 = 0.0
|
|
260
|
+
if t3 > 0:
|
|
261
|
+
t3 *= t3
|
|
262
|
+
n3 = t3 * t3 * _dot3(_GRAD3[gi3], x3, y3, z3)
|
|
263
|
+
|
|
264
|
+
# Шаг 5: Возвращаем результат
|
|
265
|
+
return 32.0 * (n0 + n1 + n2 + n3)
|
|
266
|
+
|
|
267
|
+
# ----------------------------------------------------------------------
|
|
268
|
+
# 4D симплекс-шум (для анимированных текстур)
|
|
269
|
+
# ----------------------------------------------------------------------
|
|
270
|
+
|
|
271
|
+
@jit(nopython=True, cache=True)
|
|
272
|
+
def simplex_noise_4d(x: float, y: float, z: float, w: float,
|
|
273
|
+
perm: np.ndarray = _PERM_EXTENDED) -> float:
|
|
274
|
+
"""
|
|
275
|
+
4D симплекс-шум
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
x, y, z, w: Координаты
|
|
279
|
+
perm: Таблица перестановок (длиной 512)
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
Значение шума в диапазоне примерно [-1, 1]
|
|
283
|
+
"""
|
|
284
|
+
# Константы для 4D симплекса
|
|
285
|
+
F4 = _F4
|
|
286
|
+
G4 = _G4
|
|
287
|
+
|
|
288
|
+
# Шаг 1: Скалярная сумма для определения симплекса
|
|
289
|
+
s = (x + y + z + w) * F4
|
|
290
|
+
i = _fast_floor(x + s)
|
|
291
|
+
j = _fast_floor(y + s)
|
|
292
|
+
k = _fast_floor(z + s)
|
|
293
|
+
l = _fast_floor(w + s)
|
|
294
|
+
|
|
295
|
+
t = (i + j + k + l) * G4
|
|
296
|
+
X0 = i - t
|
|
297
|
+
Y0 = j - t
|
|
298
|
+
Z0 = k - t
|
|
299
|
+
W0 = l - t
|
|
300
|
+
x0 = x - X0
|
|
301
|
+
y0 = y - Y0
|
|
302
|
+
z0 = z - Z0
|
|
303
|
+
w0 = w - W0
|
|
304
|
+
|
|
305
|
+
# Шаг 2: Сортируем координаты для определения симплекса
|
|
306
|
+
# Это сложная часть - нужно определить, в каком из 24 4D симплексов мы находимся
|
|
307
|
+
|
|
308
|
+
# Создаем массив для сортировки
|
|
309
|
+
c1 = 0
|
|
310
|
+
c2 = 0
|
|
311
|
+
c3 = 0
|
|
312
|
+
c4 = 0
|
|
313
|
+
|
|
314
|
+
if x0 > y0:
|
|
315
|
+
c1 ^= 1
|
|
316
|
+
c2 ^= 1
|
|
317
|
+
if x0 > z0:
|
|
318
|
+
c1 ^= 1
|
|
319
|
+
c3 ^= 1
|
|
320
|
+
if x0 > w0:
|
|
321
|
+
c1 ^= 1
|
|
322
|
+
c4 ^= 1
|
|
323
|
+
if y0 > z0:
|
|
324
|
+
c2 ^= 1
|
|
325
|
+
c3 ^= 1
|
|
326
|
+
if y0 > w0:
|
|
327
|
+
c2 ^= 1
|
|
328
|
+
c4 ^= 1
|
|
329
|
+
if z0 > w0:
|
|
330
|
+
c3 ^= 1
|
|
331
|
+
c4 ^= 1
|
|
332
|
+
|
|
333
|
+
# Определяем индексы симплекса
|
|
334
|
+
i1 = 1 if (c1 != 0) else 0
|
|
335
|
+
j1 = 1 if (c2 != 0) else 0
|
|
336
|
+
k1 = 1 if (c3 != 0) else 0
|
|
337
|
+
l1 = 1 if (c4 != 0) else 0
|
|
338
|
+
|
|
339
|
+
i2 = 1 if (c1 ^ c2) != 0 else 0
|
|
340
|
+
j2 = 1 if (c2 ^ c3) != 0 else 0
|
|
341
|
+
k2 = 1 if (c3 ^ c4) != 0 else 0
|
|
342
|
+
l2 = 1 if (c4 ^ c1) != 0 else 0
|
|
343
|
+
|
|
344
|
+
i3 = 1 if (c1 ^ c2 ^ c3) != 0 else 0
|
|
345
|
+
j3 = 1 if (c2 ^ c3 ^ c4) != 0 else 0
|
|
346
|
+
k3 = 1 if (c3 ^ c4 ^ c1) != 0 else 0
|
|
347
|
+
l3 = 1 if (c4 ^ c1 ^ c2) != 0 else 0
|
|
348
|
+
|
|
349
|
+
i4 = 1
|
|
350
|
+
j4 = 1
|
|
351
|
+
k4 = 1
|
|
352
|
+
l4 = 1
|
|
353
|
+
|
|
354
|
+
# Координаты внутри симплекса
|
|
355
|
+
x1 = x0 - i1 + G4
|
|
356
|
+
y1 = y0 - j1 + G4
|
|
357
|
+
z1 = z0 - k1 + G4
|
|
358
|
+
w1 = w0 - l1 + G4
|
|
359
|
+
x2 = x0 - i2 + 2.0 * G4
|
|
360
|
+
y2 = y0 - j2 + 2.0 * G4
|
|
361
|
+
z2 = z0 - k2 + 2.0 * G4
|
|
362
|
+
w2 = w0 - l2 + 2.0 * G4
|
|
363
|
+
x3 = x0 - i3 + 3.0 * G4
|
|
364
|
+
y3 = y0 - j3 + 3.0 * G4
|
|
365
|
+
z3 = z0 - k3 + 3.0 * G4
|
|
366
|
+
w3 = w0 - l3 + 3.0 * G4
|
|
367
|
+
x4 = x0 - i4 + 4.0 * G4
|
|
368
|
+
y4 = y0 - j4 + 4.0 * G4
|
|
369
|
+
z4 = z0 - k4 + 4.0 * G4
|
|
370
|
+
w4 = w0 - l4 + 4.0 * G4
|
|
371
|
+
|
|
372
|
+
# Шаг 3: Хешируем углы симплекса
|
|
373
|
+
ii = i & 255
|
|
374
|
+
jj = j & 255
|
|
375
|
+
kk = k & 255
|
|
376
|
+
ll = l & 255
|
|
377
|
+
|
|
378
|
+
# Градиенты в пяти углах
|
|
379
|
+
gi0 = perm[ii + perm[jj + perm[kk + perm[ll]]]] % 32
|
|
380
|
+
gi1 = perm[ii + i1 + perm[jj + j1 + perm[kk + k1 + perm[ll + l1]]]] % 32
|
|
381
|
+
gi2 = perm[ii + i2 + perm[jj + j2 + perm[kk + k2 + perm[ll + l2]]]] % 32
|
|
382
|
+
gi3 = perm[ii + i3 + perm[jj + j3 + perm[kk + k3 + perm[ll + l3]]]] % 32
|
|
383
|
+
gi4 = perm[ii + 1 + perm[jj + 1 + perm[kk + 1 + perm[ll + 1]]]] % 32
|
|
384
|
+
|
|
385
|
+
# Шаг 4: Вычисляем вклад от каждого угла
|
|
386
|
+
t0 = 0.6 - x0 * x0 - y0 * y0 - z0 * z0 - w0 * w0
|
|
387
|
+
n0 = 0.0
|
|
388
|
+
if t0 > 0:
|
|
389
|
+
t0 *= t0
|
|
390
|
+
n0 = t0 * t0 * _dot4(_GRAD4[gi0], x0, y0, z0, w0)
|
|
391
|
+
|
|
392
|
+
t1 = 0.6 - x1 * x1 - y1 * y1 - z1 * z1 - w1 * w1
|
|
393
|
+
n1 = 0.0
|
|
394
|
+
if t1 > 0:
|
|
395
|
+
t1 *= t1
|
|
396
|
+
n1 = t1 * t1 * _dot4(_GRAD4[gi1], x1, y1, z1, w1)
|
|
397
|
+
|
|
398
|
+
t2 = 0.6 - x2 * x2 - y2 * y2 - z2 * z2 - w2 * w2
|
|
399
|
+
n2 = 0.0
|
|
400
|
+
if t2 > 0:
|
|
401
|
+
t2 *= t2
|
|
402
|
+
n2 = t2 * t2 * _dot4(_GRAD4[gi2], x2, y2, z2, w2)
|
|
403
|
+
|
|
404
|
+
t3 = 0.6 - x3 * x3 - y3 * y3 - z3 * z3 - w3 * w3
|
|
405
|
+
n3 = 0.0
|
|
406
|
+
if t3 > 0:
|
|
407
|
+
t3 *= t3
|
|
408
|
+
n3 = t3 * t3 * _dot4(_GRAD4[gi3], x3, y3, z3, w3)
|
|
409
|
+
|
|
410
|
+
t4 = 0.6 - x4 * x4 - y4 * y4 - z4 * z4 - w4 * w4
|
|
411
|
+
n4 = 0.0
|
|
412
|
+
if t4 > 0:
|
|
413
|
+
t4 *= t4
|
|
414
|
+
n4 = t4 * t4 * _dot4(_GRAD4[gi4], x4, y4, z4, w4)
|
|
415
|
+
|
|
416
|
+
# Шаг 5: Возвращаем результат
|
|
417
|
+
return 27.0 * (n0 + n1 + n2 + n3 + n4)
|
|
418
|
+
|
|
419
|
+
# ----------------------------------------------------------------------
|
|
420
|
+
# Векторизованные версии для работы с массивами
|
|
421
|
+
# ----------------------------------------------------------------------
|
|
422
|
+
|
|
423
|
+
@jit(nopython=True, parallel=True, cache=True)
|
|
424
|
+
def simplex_noise_2d_array(x: np.ndarray, y: np.ndarray,
|
|
425
|
+
perm: np.ndarray = _PERM_EXTENDED) -> np.ndarray:
|
|
426
|
+
"""Векторизованная версия 2D симплекс-шума"""
|
|
427
|
+
shape = x.shape
|
|
428
|
+
result = np.zeros(shape, dtype=np.float32)
|
|
429
|
+
|
|
430
|
+
for i in prange(shape[0]):
|
|
431
|
+
for j in range(shape[1]):
|
|
432
|
+
result[i, j] = simplex_noise_2d(x[i, j], y[i, j], perm)
|
|
433
|
+
|
|
434
|
+
return result
|
|
435
|
+
|
|
436
|
+
@jit(nopython=True, parallel=True, cache=True)
|
|
437
|
+
def simplex_noise_3d_array(x: np.ndarray, y: np.ndarray, z: np.ndarray,
|
|
438
|
+
perm: np.ndarray = _PERM_EXTENDED) -> np.ndarray:
|
|
439
|
+
"""Векторизованная версия 3D симплекс-шума"""
|
|
440
|
+
shape = x.shape
|
|
441
|
+
result = np.zeros(shape, dtype=np.float32)
|
|
442
|
+
|
|
443
|
+
for i in prange(shape[0]):
|
|
444
|
+
for j in range(shape[1]):
|
|
445
|
+
result[i, j] = simplex_noise_3d(x[i, j], y[i, j], z[i, j], perm)
|
|
446
|
+
|
|
447
|
+
return result
|
|
448
|
+
|
|
449
|
+
# ----------------------------------------------------------------------
|
|
450
|
+
# Класс для удобной работы с симплекс-шумом
|
|
451
|
+
# ----------------------------------------------------------------------
|
|
452
|
+
|
|
453
|
+
class SimplexNoise:
|
|
454
|
+
"""Удобный интерфейс для работы с симплекс-шумом"""
|
|
455
|
+
|
|
456
|
+
def __init__(self, seed: int = 42):
|
|
457
|
+
"""
|
|
458
|
+
Инициализация генератора шума
|
|
459
|
+
|
|
460
|
+
Args:
|
|
461
|
+
seed: Семя для генерации таблицы перестановок
|
|
462
|
+
"""
|
|
463
|
+
self.seed = seed
|
|
464
|
+
self.perm = self._generate_permutation(seed)
|
|
465
|
+
self.perm_extended = np.concatenate([self.perm, self.perm])
|
|
466
|
+
|
|
467
|
+
def _generate_permutation(self, seed: int) -> np.ndarray:
|
|
468
|
+
"""Генерация таблицы перестановок на основе seed"""
|
|
469
|
+
np.random.seed(seed)
|
|
470
|
+
perm = np.random.permutation(256).astype(np.uint8)
|
|
471
|
+
return perm
|
|
472
|
+
|
|
473
|
+
def noise_2d(self, x: Union[float, np.ndarray],
|
|
474
|
+
y: Union[float, np.ndarray]) -> Union[float, np.ndarray]:
|
|
475
|
+
"""2D симплекс-шум"""
|
|
476
|
+
if isinstance(x, np.ndarray) and isinstance(y, np.ndarray):
|
|
477
|
+
return simplex_noise_2d_array(x, y, self.perm_extended)
|
|
478
|
+
else:
|
|
479
|
+
return simplex_noise_2d(x, y, self.perm_extended)
|
|
480
|
+
|
|
481
|
+
def noise_3d(self, x: Union[float, np.ndarray],
|
|
482
|
+
y: Union[float, np.ndarray],
|
|
483
|
+
z: Union[float, np.ndarray]) -> Union[float, np.ndarray]:
|
|
484
|
+
"""3D симплекс-шум"""
|
|
485
|
+
if isinstance(x, np.ndarray) and isinstance(y, np.ndarray) and isinstance(z, np.ndarray):
|
|
486
|
+
if x.ndim <= 2:
|
|
487
|
+
return simplex_noise_3d_array(x, y, z, self.perm_extended)
|
|
488
|
+
result = np.zeros_like(x, dtype=np.float32)
|
|
489
|
+
it = np.nditer(x, flags=["multi_index"])
|
|
490
|
+
while not it.finished:
|
|
491
|
+
idx = it.multi_index
|
|
492
|
+
result[idx] = simplex_noise_3d(
|
|
493
|
+
float(x[idx]), float(y[idx]), float(z[idx]), self.perm_extended
|
|
494
|
+
)
|
|
495
|
+
it.iternext()
|
|
496
|
+
return result
|
|
497
|
+
return simplex_noise_3d(x, y, z, self.perm_extended)
|
|
498
|
+
|
|
499
|
+
def noise_4d(self, x: float, y: float, z: float, w: float) -> float:
|
|
500
|
+
"""4D симплекс-шум"""
|
|
501
|
+
return simplex_noise_4d(x, y, z, w, self.perm_extended)
|
|
502
|
+
|
|
503
|
+
def fractal_noise_2d(self, x: np.ndarray, y: np.ndarray,
|
|
504
|
+
octaves: int = 8, persistence: float = 0.5,
|
|
505
|
+
lacunarity: float = 2.0,
|
|
506
|
+
base_scale: float = 1.0) -> np.ndarray:
|
|
507
|
+
"""
|
|
508
|
+
Фрактальный шум (fBm) на основе симплекс-шума
|
|
509
|
+
|
|
510
|
+
Args:
|
|
511
|
+
x, y: Координатные сетки
|
|
512
|
+
octaves: Количество октав
|
|
513
|
+
persistence: Сохранение амплитуды между октавами
|
|
514
|
+
lacunarity: Умножение частоты между октавами
|
|
515
|
+
base_scale: Базовый масштаб
|
|
516
|
+
|
|
517
|
+
Returns:
|
|
518
|
+
Массив значений фрактального шума
|
|
519
|
+
"""
|
|
520
|
+
result = np.zeros_like(x, dtype=np.float32)
|
|
521
|
+
amplitude = 1.0
|
|
522
|
+
frequency = base_scale
|
|
523
|
+
|
|
524
|
+
for i in range(octaves):
|
|
525
|
+
# Генерируем шум для текущей октавы
|
|
526
|
+
nx = x * frequency
|
|
527
|
+
ny = y * frequency
|
|
528
|
+
|
|
529
|
+
noise = self.noise_2d(nx, ny)
|
|
530
|
+
result += amplitude * noise
|
|
531
|
+
|
|
532
|
+
# Подготовка для следующей октавы
|
|
533
|
+
amplitude *= persistence
|
|
534
|
+
frequency *= lacunarity
|
|
535
|
+
|
|
536
|
+
# Нормализация (примерная)
|
|
537
|
+
max_val = (1 - persistence ** octaves) / (1 - persistence)
|
|
538
|
+
return result / max_val if max_val > 0 else result
|
|
539
|
+
|
|
540
|
+
def fractal_noise_3d(self, x: np.ndarray, y: np.ndarray, z: np.ndarray,
|
|
541
|
+
octaves: int = 8, persistence: float = 0.5,
|
|
542
|
+
lacunarity: float = 2.0,
|
|
543
|
+
base_scale: float = 1.0) -> np.ndarray:
|
|
544
|
+
"""
|
|
545
|
+
Фрактальный 3D шум (fBm) на основе симплекс-шума
|
|
546
|
+
"""
|
|
547
|
+
result = np.zeros_like(x, dtype=np.float32)
|
|
548
|
+
amplitude = 1.0
|
|
549
|
+
frequency = base_scale
|
|
550
|
+
|
|
551
|
+
for _ in range(octaves):
|
|
552
|
+
nx = x * frequency
|
|
553
|
+
ny = y * frequency
|
|
554
|
+
nz = z * frequency
|
|
555
|
+
noise = self.noise_3d(nx, ny, nz)
|
|
556
|
+
result += amplitude * noise
|
|
557
|
+
amplitude *= persistence
|
|
558
|
+
frequency *= lacunarity
|
|
559
|
+
|
|
560
|
+
max_val = (1 - persistence ** octaves) / (1 - persistence)
|
|
561
|
+
return result / max_val if max_val > 0 else result
|
|
562
|
+
|
|
563
|
+
def ridged_noise_2d(self, x: np.ndarray, y: np.ndarray,
|
|
564
|
+
octaves: int = 8, persistence: float = 0.5,
|
|
565
|
+
lacunarity: float = 2.0,
|
|
566
|
+
base_scale: float = 1.0,
|
|
567
|
+
offset: float = 1.0,
|
|
568
|
+
gain: float = 2.0) -> np.ndarray:
|
|
569
|
+
"""
|
|
570
|
+
Риджид-шум (ridged multifractal) - для острых горных хребтов
|
|
571
|
+
|
|
572
|
+
Args:
|
|
573
|
+
x, y: Координатные сетки
|
|
574
|
+
octaves: Количество октав
|
|
575
|
+
persistence: Сохранение амплитуды
|
|
576
|
+
lacunarity: Умножение частоты
|
|
577
|
+
base_scale: Базовый масштаб
|
|
578
|
+
offset: Смещение для создания риджей
|
|
579
|
+
gain: Усиление сигнала
|
|
580
|
+
|
|
581
|
+
Returns:
|
|
582
|
+
Массив значений риджид-шума
|
|
583
|
+
"""
|
|
584
|
+
result = np.zeros_like(x, dtype=np.float32)
|
|
585
|
+
amplitude = 1.0
|
|
586
|
+
frequency = base_scale
|
|
587
|
+
weight = 1.0
|
|
588
|
+
|
|
589
|
+
for i in range(octaves):
|
|
590
|
+
# Генерируем шум для текущей октавы
|
|
591
|
+
nx = x * frequency
|
|
592
|
+
ny = y * frequency
|
|
593
|
+
|
|
594
|
+
noise = self.noise_2d(nx, ny)
|
|
595
|
+
|
|
596
|
+
# Делаем абсолютное значение и инвертируем для создания риджей
|
|
597
|
+
signal = offset - np.abs(noise)
|
|
598
|
+
signal *= signal
|
|
599
|
+
signal *= weight
|
|
600
|
+
|
|
601
|
+
weight = np.clip(signal * gain, 0.0, 1.0)
|
|
602
|
+
|
|
603
|
+
result += signal * amplitude
|
|
604
|
+
|
|
605
|
+
# Подготовка для следующей октавы
|
|
606
|
+
amplitude *= persistence
|
|
607
|
+
frequency *= lacunarity
|
|
608
|
+
|
|
609
|
+
return result / octaves
|
|
610
|
+
|
|
611
|
+
def billow_noise_2d(self, x: np.ndarray, y: np.ndarray,
|
|
612
|
+
octaves: int = 8, persistence: float = 0.5,
|
|
613
|
+
lacunarity: float = 2.0,
|
|
614
|
+
base_scale: float = 1.0) -> np.ndarray:
|
|
615
|
+
"""
|
|
616
|
+
Билоу-шум (billow noise) - пушистые облака
|
|
617
|
+
|
|
618
|
+
Args:
|
|
619
|
+
x, y: Координатные сетки
|
|
620
|
+
octaves: Количество октав
|
|
621
|
+
persistence: Сохранение амплитуды
|
|
622
|
+
lacunarity: Умножение частоты
|
|
623
|
+
base_scale: Базовый масштаб
|
|
624
|
+
|
|
625
|
+
Returns:
|
|
626
|
+
Массив значений билоу-шума
|
|
627
|
+
"""
|
|
628
|
+
result = np.zeros_like(x, dtype=np.float32)
|
|
629
|
+
amplitude = 1.0
|
|
630
|
+
frequency = base_scale
|
|
631
|
+
|
|
632
|
+
for i in range(octaves):
|
|
633
|
+
# Генерируем шум для текущей октавы
|
|
634
|
+
nx = x * frequency
|
|
635
|
+
ny = y * frequency
|
|
636
|
+
|
|
637
|
+
noise = self.noise_2d(nx, ny)
|
|
638
|
+
# Используем абсолютное значение для создания "пушистости"
|
|
639
|
+
noise = np.abs(noise)
|
|
640
|
+
|
|
641
|
+
result += amplitude * noise
|
|
642
|
+
|
|
643
|
+
# Подготовка для следующей октавы
|
|
644
|
+
amplitude *= persistence
|
|
645
|
+
frequency *= lacunarity
|
|
646
|
+
|
|
647
|
+
# Нормализация
|
|
648
|
+
max_val = (1 - persistence ** octaves) / (1 - persistence)
|
|
649
|
+
return result / max_val if max_val > 0 else result
|
|
650
|
+
|
|
651
|
+
# ----------------------------------------------------------------------
|
|
652
|
+
# Специализированные генераторы на основе симплекс-шума
|
|
653
|
+
# ----------------------------------------------------------------------
|
|
654
|
+
|
|
655
|
+
class SimplexTextureGenerator:
|
|
656
|
+
"""Генератор текстур на основе симплекс-шума"""
|
|
657
|
+
|
|
658
|
+
def __init__(self, seed: int = 42):
|
|
659
|
+
self.simplex = SimplexNoise(seed)
|
|
660
|
+
self.cache = {}
|
|
661
|
+
|
|
662
|
+
def generate_clouds(self, width: int, height: int,
|
|
663
|
+
scale: float = 0.01, time: float = 0.0) -> np.ndarray:
|
|
664
|
+
"""
|
|
665
|
+
Генерация облачной текстуры
|
|
666
|
+
|
|
667
|
+
Args:
|
|
668
|
+
width, height: Размеры текстуры
|
|
669
|
+
scale: Масштаб текстуры
|
|
670
|
+
time: Время для анимации
|
|
671
|
+
|
|
672
|
+
Returns:
|
|
673
|
+
Текстура в формате (height, width, 4) RGBA
|
|
674
|
+
"""
|
|
675
|
+
# Создаем координатную сетку
|
|
676
|
+
x = np.linspace(0, width * scale, width)
|
|
677
|
+
y = np.linspace(0, height * scale, height)
|
|
678
|
+
xx, yy = np.meshgrid(x, y)
|
|
679
|
+
|
|
680
|
+
if time > 0:
|
|
681
|
+
# Анимированные облака с использованием 3D шума
|
|
682
|
+
zz = np.ones_like(xx) * time * 0.1
|
|
683
|
+
base_noise = self.simplex.noise_3d(xx, yy, zz)
|
|
684
|
+
else:
|
|
685
|
+
# Статичные облака
|
|
686
|
+
base_noise = self.simplex.noise_2d(xx, yy)
|
|
687
|
+
|
|
688
|
+
# Фрактальный шум для детализации
|
|
689
|
+
fractal = self.simplex.fractal_noise_2d(
|
|
690
|
+
xx, yy, octaves=6, persistence=0.6,
|
|
691
|
+
lacunarity=2.0, base_scale=scale * 2
|
|
692
|
+
)
|
|
693
|
+
|
|
694
|
+
# Комбинируем шумы
|
|
695
|
+
clouds = base_noise * 0.7 + fractal * 0.3
|
|
696
|
+
|
|
697
|
+
# Нормализация к [0, 1]
|
|
698
|
+
clouds = (clouds + 1) * 0.5
|
|
699
|
+
|
|
700
|
+
# Создаем RGBA текстуру
|
|
701
|
+
texture = np.zeros((height, width, 4), dtype=np.float32)
|
|
702
|
+
|
|
703
|
+
# Белые облака с легкой голубизной
|
|
704
|
+
texture[..., 0] = clouds # R
|
|
705
|
+
texture[..., 1] = clouds # G
|
|
706
|
+
texture[..., 2] = clouds * 0.9 + 0.1 # B (легкая голубизна)
|
|
707
|
+
|
|
708
|
+
# Альфа-канал: более плотные облаки в центре
|
|
709
|
+
alpha = np.clip(clouds * 1.5 - 0.25, 0, 1)
|
|
710
|
+
texture[..., 3] = alpha
|
|
711
|
+
|
|
712
|
+
return texture
|
|
713
|
+
|
|
714
|
+
def generate_marble(self, width: int, height: int,
|
|
715
|
+
scale: float = 0.005, veins: int = 3) -> np.ndarray:
|
|
716
|
+
"""
|
|
717
|
+
Генерация мраморной текстуры
|
|
718
|
+
|
|
719
|
+
Args:
|
|
720
|
+
width, height: Размеры текстуры
|
|
721
|
+
scale: Масштаб текстуры
|
|
722
|
+
veins: Количество "вен" в мраморе
|
|
723
|
+
|
|
724
|
+
Returns:
|
|
725
|
+
Текстура в формате (height, width, 4) RGBA
|
|
726
|
+
"""
|
|
727
|
+
# Создаем координатную сетку
|
|
728
|
+
x = np.linspace(0, width * scale, width)
|
|
729
|
+
y = np.linspace(0, height * scale, height)
|
|
730
|
+
xx, yy = np.meshgrid(x, y)
|
|
731
|
+
|
|
732
|
+
# Базовый шум для структуры
|
|
733
|
+
base_noise = self.simplex.noise_2d(xx, yy)
|
|
734
|
+
|
|
735
|
+
# Создаем синусоидальные вены
|
|
736
|
+
marble_pattern = np.zeros_like(base_noise)
|
|
737
|
+
for i in range(veins):
|
|
738
|
+
angle = i * np.pi / veins
|
|
739
|
+
# Поворачиваем координаты
|
|
740
|
+
x_rot = xx * np.cos(angle) - yy * np.sin(angle)
|
|
741
|
+
# Синусоида для создания вен
|
|
742
|
+
vein = np.sin(x_rot * 10 + base_noise * 2) * 0.5 + 0.5
|
|
743
|
+
marble_pattern += vein
|
|
744
|
+
|
|
745
|
+
marble_pattern /= veins
|
|
746
|
+
|
|
747
|
+
# Добавляем фрактальную детализацию
|
|
748
|
+
detail = self.simplex.fractal_noise_2d(
|
|
749
|
+
xx, yy, octaves=4, persistence=0.5,
|
|
750
|
+
lacunarity=2.5, base_scale=scale * 10
|
|
751
|
+
)
|
|
752
|
+
marble_pattern = marble_pattern * 0.8 + detail * 0.2
|
|
753
|
+
|
|
754
|
+
# Создаем RGBA текстуру
|
|
755
|
+
texture = np.zeros((height, width, 4), dtype=np.float32)
|
|
756
|
+
|
|
757
|
+
# Цвета мрамора
|
|
758
|
+
base_color = np.array([0.92, 0.87, 0.82]) # Светлый мрамор
|
|
759
|
+
vein_color = np.array([0.75, 0.65, 0.55]) # Темные вены
|
|
760
|
+
|
|
761
|
+
# Интерполяция между цветами на основе паттерна
|
|
762
|
+
for i in range(3):
|
|
763
|
+
texture[..., i] = base_color[i] * (1 - marble_pattern) + vein_color[i] * marble_pattern
|
|
764
|
+
|
|
765
|
+
# Легкие вариации цвета
|
|
766
|
+
color_variation = self.simplex.noise_2d(xx * 0.1, yy * 0.1) * 0.1
|
|
767
|
+
texture[..., :3] += color_variation[..., np.newaxis] * 0.1
|
|
768
|
+
|
|
769
|
+
texture[..., 3] = 1.0 # Непрозрачный
|
|
770
|
+
|
|
771
|
+
return np.clip(texture, 0, 1)
|
|
772
|
+
|
|
773
|
+
def generate_wood(self, width: int, height: int,
|
|
774
|
+
scale: float = 0.01, rings: float = 20.0) -> np.ndarray:
|
|
775
|
+
"""
|
|
776
|
+
Генерация деревянной текстуры
|
|
777
|
+
|
|
778
|
+
Args:
|
|
779
|
+
width, height: Размеры текстуры
|
|
780
|
+
scale: Масштаб текстуры
|
|
781
|
+
rings: Количество годичных колец
|
|
782
|
+
|
|
783
|
+
Returns:
|
|
784
|
+
Текстура в формате (height, width, 4) RGBA
|
|
785
|
+
"""
|
|
786
|
+
# Создаем координатную сетку
|
|
787
|
+
x = np.linspace(-1, 1, width)
|
|
788
|
+
y = np.linspace(-1, 1, height)
|
|
789
|
+
xx, yy = np.meshgrid(x, y)
|
|
790
|
+
|
|
791
|
+
# Создаем кольцевую структуру (годичные кольца)
|
|
792
|
+
radius = np.sqrt(xx*xx + yy*yy) * rings
|
|
793
|
+
|
|
794
|
+
# Добавляем шум для реалистичности
|
|
795
|
+
noise = self.simplex.fractal_noise_2d(
|
|
796
|
+
xx, yy, octaves=5, persistence=0.6,
|
|
797
|
+
lacunarity=2.0, base_scale=scale * 5
|
|
798
|
+
)
|
|
799
|
+
|
|
800
|
+
# Комбинируем кольца с шумом
|
|
801
|
+
wood_pattern = np.sin(radius * 2 * np.pi + noise * 2) * 0.5 + 0.5
|
|
802
|
+
|
|
803
|
+
# Добавляем детали (поры дерева)
|
|
804
|
+
detail = self.simplex.noise_2d(xx * 50, yy * 50) * 0.1
|
|
805
|
+
wood_pattern += detail
|
|
806
|
+
|
|
807
|
+
# Создаем RGBA текстуру
|
|
808
|
+
texture = np.zeros((height, width, 4), dtype=np.float32)
|
|
809
|
+
|
|
810
|
+
# Цвета дерева
|
|
811
|
+
light_wood = np.array([0.7, 0.5, 0.3]) # Светлая древесина
|
|
812
|
+
dark_wood = np.array([0.4, 0.25, 0.1]) # Темная древесина
|
|
813
|
+
|
|
814
|
+
# Интерполяция между цветами
|
|
815
|
+
for i in range(3):
|
|
816
|
+
texture[..., i] = light_wood[i] * wood_pattern + dark_wood[i] * (1 - wood_pattern)
|
|
817
|
+
|
|
818
|
+
# Добавляем текстуру волокон
|
|
819
|
+
fiber = np.sin(xx * 100 + self.simplex.noise_2d(xx * 10, yy * 10)) * 0.05
|
|
820
|
+
texture[..., :3] += fiber[..., np.newaxis]
|
|
821
|
+
|
|
822
|
+
texture[..., 3] = 1.0 # Непрозрачный
|
|
823
|
+
|
|
824
|
+
return np.clip(texture, 0, 1)
|
|
825
|
+
|
|
826
|
+
def generate_lava(self, width: int, height: int,
|
|
827
|
+
scale: float = 0.01, time: float = 0.0) -> np.ndarray:
|
|
828
|
+
"""
|
|
829
|
+
Генерация текстуры лавы
|
|
830
|
+
|
|
831
|
+
Args:
|
|
832
|
+
width, height: Размеры текстуры
|
|
833
|
+
scale: Масштаб текстуры
|
|
834
|
+
time: Время для анимации
|
|
835
|
+
|
|
836
|
+
Returns:
|
|
837
|
+
Текстура в формате (height, width, 4) RGBA
|
|
838
|
+
"""
|
|
839
|
+
# Создаем координатную сетку
|
|
840
|
+
x = np.linspace(0, width * scale, width)
|
|
841
|
+
y = np.linspace(0, height * scale, height)
|
|
842
|
+
xx, yy = np.meshgrid(x, y)
|
|
843
|
+
|
|
844
|
+
# Анимированная текстура с использованием 3D шума
|
|
845
|
+
zz = np.ones_like(xx) * time * 0.2
|
|
846
|
+
|
|
847
|
+
# Базовый шум для лавы
|
|
848
|
+
if time > 0:
|
|
849
|
+
base_noise = self.simplex.noise_3d(xx, yy, zz)
|
|
850
|
+
else:
|
|
851
|
+
base_noise = self.simplex.noise_2d(xx, yy)
|
|
852
|
+
|
|
853
|
+
# Детализированный шум для текстуры
|
|
854
|
+
detail = self.simplex.fractal_noise_2d(
|
|
855
|
+
xx, yy, octaves=6, persistence=0.7,
|
|
856
|
+
lacunarity=1.8, base_scale=scale * 3
|
|
857
|
+
)
|
|
858
|
+
|
|
859
|
+
# Комбинируем шумы
|
|
860
|
+
lava = base_noise * 0.6 + detail * 0.4
|
|
861
|
+
|
|
862
|
+
# Нормализация
|
|
863
|
+
lava = (lava + 1) * 0.5
|
|
864
|
+
|
|
865
|
+
# Создаем RGBA текстуру
|
|
866
|
+
texture = np.zeros((height, width, 4), dtype=np.float32)
|
|
867
|
+
|
|
868
|
+
# Цвета лавы (от темно-красного до ярко-желтого)
|
|
869
|
+
# Используем нелинейное смешивание цветов
|
|
870
|
+
red = np.clip(lava * 1.5, 0, 1)
|
|
871
|
+
green = np.clip(lava * 0.8 - 0.2, 0, 1)
|
|
872
|
+
blue = np.clip(lava * 0.2 - 0.4, 0, 0.1)
|
|
873
|
+
|
|
874
|
+
texture[..., 0] = red # R
|
|
875
|
+
texture[..., 1] = green # G
|
|
876
|
+
texture[..., 2] = blue # B
|
|
877
|
+
|
|
878
|
+
# Яркость для свечения
|
|
879
|
+
brightness = np.power(lava, 2) # Квадрат для более ярких областей
|
|
880
|
+
|
|
881
|
+
# Альфа-канал с небольшими вариациями
|
|
882
|
+
texture[..., 3] = 0.9 + brightness * 0.1
|
|
883
|
+
|
|
884
|
+
return texture
|
|
885
|
+
|
|
886
|
+
def generate_terrain(self, width: int, height: int,
|
|
887
|
+
scale: float = 0.005, octaves: int = 6,
|
|
888
|
+
persistence: float = 0.5, lacunarity: float = 2.0
|
|
889
|
+
) -> np.ndarray:
|
|
890
|
+
"""
|
|
891
|
+
Генерация 2D текстуры рельефа
|
|
892
|
+
|
|
893
|
+
Args:
|
|
894
|
+
width, height: Размеры текстуры
|
|
895
|
+
scale: Масштаб текстуры
|
|
896
|
+
octaves: Количество октав
|
|
897
|
+
persistence: Сохранение амплитуды
|
|
898
|
+
lacunarity: Умножение частоты
|
|
899
|
+
|
|
900
|
+
Returns:
|
|
901
|
+
Текстура в формате (height, width, 4) RGBA
|
|
902
|
+
"""
|
|
903
|
+
x = np.linspace(0, width * scale, width)
|
|
904
|
+
y = np.linspace(0, height * scale, height)
|
|
905
|
+
xx, yy = np.meshgrid(x, y)
|
|
906
|
+
|
|
907
|
+
base = self.simplex.fractal_noise_2d(
|
|
908
|
+
xx, yy, octaves=octaves, persistence=persistence,
|
|
909
|
+
lacunarity=lacunarity, base_scale=1.0
|
|
910
|
+
)
|
|
911
|
+
ridged = self.simplex.ridged_noise_2d(
|
|
912
|
+
xx, yy, octaves=max(3, octaves // 2), persistence=0.6,
|
|
913
|
+
lacunarity=2.2, base_scale=1.5
|
|
914
|
+
)
|
|
915
|
+
|
|
916
|
+
heightmap = base * 0.7 + ridged * 0.3
|
|
917
|
+
heightmap = (heightmap - heightmap.min()) / (heightmap.max() - heightmap.min())
|
|
918
|
+
|
|
919
|
+
texture = np.zeros((height, width, 4), dtype=np.float32)
|
|
920
|
+
|
|
921
|
+
water = heightmap < 0.35
|
|
922
|
+
sand = (heightmap >= 0.35) & (heightmap < 0.45)
|
|
923
|
+
grass = (heightmap >= 0.45) & (heightmap < 0.7)
|
|
924
|
+
rock = (heightmap >= 0.7) & (heightmap < 0.85)
|
|
925
|
+
snow = heightmap >= 0.85
|
|
926
|
+
|
|
927
|
+
texture[water, :3] = np.array([0.10, 0.20, 0.50])
|
|
928
|
+
texture[sand, :3] = np.array([0.76, 0.70, 0.50])
|
|
929
|
+
texture[grass, :3] = np.array([0.20, 0.55, 0.25])
|
|
930
|
+
texture[rock, :3] = np.array([0.50, 0.50, 0.50])
|
|
931
|
+
texture[snow, :3] = np.array([0.92, 0.92, 0.96])
|
|
932
|
+
|
|
933
|
+
variation = self.simplex.noise_2d(xx * 0.2, yy * 0.2) * 0.05
|
|
934
|
+
texture[..., :3] = np.clip(texture[..., :3] + variation[..., np.newaxis], 0, 1)
|
|
935
|
+
texture[..., 3] = 1.0
|
|
936
|
+
|
|
937
|
+
return texture
|
|
938
|
+
|
|
939
|
+
def generate_grass(self, width: int, height: int,
|
|
940
|
+
scale: float = 0.02) -> np.ndarray:
|
|
941
|
+
"""
|
|
942
|
+
Генерация текстуры травы
|
|
943
|
+
|
|
944
|
+
Args:
|
|
945
|
+
width, height: Размеры текстуры
|
|
946
|
+
scale: Масштаб текстуры
|
|
947
|
+
|
|
948
|
+
Returns:
|
|
949
|
+
Текстура в формате (height, width, 4) RGBA
|
|
950
|
+
"""
|
|
951
|
+
x = np.linspace(0, width * scale, width)
|
|
952
|
+
y = np.linspace(0, height * scale, height)
|
|
953
|
+
xx, yy = np.meshgrid(x, y)
|
|
954
|
+
|
|
955
|
+
base = self.simplex.noise_2d(xx, yy)
|
|
956
|
+
detail = self.simplex.fractal_noise_2d(
|
|
957
|
+
xx, yy, octaves=4, persistence=0.6,
|
|
958
|
+
lacunarity=2.0, base_scale=scale * 5
|
|
959
|
+
)
|
|
960
|
+
grass = (base * 0.6 + detail * 0.4)
|
|
961
|
+
grass = (grass + 1) * 0.5
|
|
962
|
+
|
|
963
|
+
texture = np.zeros((height, width, 4), dtype=np.float32)
|
|
964
|
+
texture[..., 0] = np.clip(grass * 0.4, 0, 1)
|
|
965
|
+
texture[..., 1] = np.clip(grass * 0.8 + 0.2, 0, 1)
|
|
966
|
+
texture[..., 2] = np.clip(grass * 0.4, 0, 1)
|
|
967
|
+
texture[..., 3] = 1.0
|
|
968
|
+
|
|
969
|
+
return texture
|
|
970
|
+
|
|
971
|
+
# ----------------------------------------------------------------------
|
|
972
|
+
# Демонстрация и тестирование
|
|
973
|
+
# ----------------------------------------------------------------------
|
|
974
|
+
|
|
975
|
+
def benchmark_simplex_noise():
|
|
976
|
+
"""Бенчмарк производительности симплекс-шума"""
|
|
977
|
+
import time
|
|
978
|
+
|
|
979
|
+
print("Benchmarking Simplex Noise...")
|
|
980
|
+
print("=" * 50)
|
|
981
|
+
|
|
982
|
+
# Инициализация
|
|
983
|
+
simplex = SimplexNoise(seed=42)
|
|
984
|
+
|
|
985
|
+
# Тест 1: 2D шум на небольшом массиве
|
|
986
|
+
size_small = 256
|
|
987
|
+
x_small = np.random.randn(size_small, size_small)
|
|
988
|
+
y_small = np.random.randn(size_small, size_small)
|
|
989
|
+
|
|
990
|
+
start = time.time()
|
|
991
|
+
result = simplex.noise_2d(x_small, y_small)
|
|
992
|
+
elapsed = time.time() - start
|
|
993
|
+
|
|
994
|
+
print(f"2D Noise ({size_small}x{size_small}): {elapsed:.4f}s")
|
|
995
|
+
print(f" Mean: {result.mean():.4f}, Std: {result.std():.4f}")
|
|
996
|
+
|
|
997
|
+
# Тест 2: Фрактальный шум
|
|
998
|
+
start = time.time()
|
|
999
|
+
fractal = simplex.fractal_noise_2d(
|
|
1000
|
+
x_small, y_small, octaves=8, persistence=0.5,
|
|
1001
|
+
lacunarity=2.0, base_scale=1.0
|
|
1002
|
+
)
|
|
1003
|
+
elapsed = time.time() - start
|
|
1004
|
+
|
|
1005
|
+
print(f"Fractal Noise (8 octaves): {elapsed:.4f}s")
|
|
1006
|
+
print(f" Mean: {fractal.mean():.4f}, Std: {fractal.std():.4f}")
|
|
1007
|
+
|
|
1008
|
+
# Тест 3: Генерация текстур
|
|
1009
|
+
tex_gen = SimplexTextureGenerator(seed=42)
|
|
1010
|
+
|
|
1011
|
+
sizes = [(128, 128), (512, 512), (1024, 1024)]
|
|
1012
|
+
textures = ["clouds", "marble", "wood", "lava"]
|
|
1013
|
+
|
|
1014
|
+
print("\nTexture Generation Benchmark:")
|
|
1015
|
+
print("-" * 30)
|
|
1016
|
+
|
|
1017
|
+
for w, h in sizes:
|
|
1018
|
+
print(f"\nSize: {w}x{h}")
|
|
1019
|
+
for tex_type in textures:
|
|
1020
|
+
start = time.time()
|
|
1021
|
+
|
|
1022
|
+
if tex_type == "clouds":
|
|
1023
|
+
texture = tex_gen.generate_clouds(w, h)
|
|
1024
|
+
elif tex_type == "marble":
|
|
1025
|
+
texture = tex_gen.generate_marble(w, h)
|
|
1026
|
+
elif tex_type == "wood":
|
|
1027
|
+
texture = tex_gen.generate_wood(w, h)
|
|
1028
|
+
elif tex_type == "lava":
|
|
1029
|
+
texture = tex_gen.generate_lava(w, h)
|
|
1030
|
+
|
|
1031
|
+
elapsed = time.time() - start
|
|
1032
|
+
print(f" {tex_type:10s}: {elapsed:.4f}s")
|
|
1033
|
+
|
|
1034
|
+
def visualize_noise_patterns():
|
|
1035
|
+
"""Визуализация различных паттернов шума"""
|
|
1036
|
+
import matplotlib.pyplot as plt
|
|
1037
|
+
|
|
1038
|
+
print("Generating noise patterns visualization...")
|
|
1039
|
+
|
|
1040
|
+
# Создаем координатную сетку
|
|
1041
|
+
size = 512
|
|
1042
|
+
x = np.linspace(0, 10, size)
|
|
1043
|
+
y = np.linspace(0, 10, size)
|
|
1044
|
+
xx, yy = np.meshgrid(x, y)
|
|
1045
|
+
|
|
1046
|
+
# Инициализация генератора
|
|
1047
|
+
simplex = SimplexNoise(seed=42)
|
|
1048
|
+
|
|
1049
|
+
# Генерация различных типов шума
|
|
1050
|
+
patterns = {
|
|
1051
|
+
"Simplex 2D": simplex.noise_2d(xx, yy),
|
|
1052
|
+
"Fractal (fBm)": simplex.fractal_noise_2d(xx, yy, octaves=8),
|
|
1053
|
+
"Ridged": simplex.ridged_noise_2d(xx, yy, octaves=8),
|
|
1054
|
+
"Billow": simplex.billow_noise_2d(xx, yy, octaves=8)
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
# Визуализация
|
|
1058
|
+
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
|
|
1059
|
+
axes = axes.ravel()
|
|
1060
|
+
|
|
1061
|
+
for ax, (title, pattern) in zip(axes, patterns.items()):
|
|
1062
|
+
im = ax.imshow(pattern, cmap='viridis', origin='lower')
|
|
1063
|
+
ax.set_title(title, fontsize=14)
|
|
1064
|
+
ax.axis('off')
|
|
1065
|
+
plt.colorbar(im, ax=ax, shrink=0.8)
|
|
1066
|
+
|
|
1067
|
+
plt.tight_layout()
|
|
1068
|
+
plt.savefig('simplex_noise_patterns.png', dpi=150, bbox_inches='tight')
|
|
1069
|
+
plt.show()
|
|
1070
|
+
|
|
1071
|
+
print("Visualization saved as 'simplex_noise_patterns.png'")
|
|
1072
|
+
|
|
1073
|
+
if __name__ == "__main__":
|
|
1074
|
+
print("Simplex Noise Implementation")
|
|
1075
|
+
print("=" * 60)
|
|
1076
|
+
|
|
1077
|
+
# Запускаем бенчмарк
|
|
1078
|
+
benchmark_simplex_noise()
|
|
1079
|
+
|
|
1080
|
+
print("\n" + "=" * 60)
|
|
1081
|
+
print("Example usage:")
|
|
1082
|
+
|
|
1083
|
+
# Пример использования
|
|
1084
|
+
simplex = SimplexNoise(seed=123)
|
|
1085
|
+
|
|
1086
|
+
# Одиночная точка
|
|
1087
|
+
value = simplex.noise_2d(1.5, 2.3)
|
|
1088
|
+
print(f"\nNoise at (1.5, 2.3): {value:.6f}")
|
|
1089
|
+
|
|
1090
|
+
# Массив точек
|
|
1091
|
+
x = np.array([[0.0, 1.0], [2.0, 3.0]])
|
|
1092
|
+
y = np.array([[0.0, 0.5], [1.0, 1.5]])
|
|
1093
|
+
values = simplex.noise_2d(x, y)
|
|
1094
|
+
print(f"\nNoise array:\n{values}")
|
|
1095
|
+
|
|
1096
|
+
# Генератор текстур
|
|
1097
|
+
tex_gen = SimplexTextureGenerator(seed=456)
|
|
1098
|
+
|
|
1099
|
+
# Генерация небольшой текстуры облаков
|
|
1100
|
+
clouds = tex_gen.generate_clouds(64, 64)
|
|
1101
|
+
print(f"\nCloud texture shape: {clouds.shape}")
|
|
1102
|
+
print(f"Cloud texture range: [{clouds.min():.3f}, {clouds.max():.3f}]")
|
|
1103
|
+
|
|
1104
|
+
# 3D шум
|
|
1105
|
+
value_3d = simplex.noise_3d(1.0, 2.0, 3.0)
|
|
1106
|
+
print(f"\n3D noise at (1.0, 2.0, 3.0): {value_3d:.6f}")
|
|
1107
|
+
|
|
1108
|
+
# 4D шум
|
|
1109
|
+
value_4d = simplex.noise_4d(1.0, 2.0, 3.0, 4.0)
|
|
1110
|
+
print(f"4D noise at (1.0, 2.0, 3.0, 4.0): {value_4d:.6f}")
|
|
1111
|
+
|
|
1112
|
+
# Для визуализации (раскомментируйте при необходимости)
|
|
1113
|
+
# visualize_noise_patterns()
|