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.
@@ -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()