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 ADDED
@@ -0,0 +1,1585 @@
1
+ # fractex/volume_textures.py
2
+ """
3
+ Полная система для работы с 3D текстурами (объемными текстурами)
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, int32, int64
10
+ import warnings
11
+ import time
12
+ import threading
13
+ from queue import Queue, PriorityQueue
14
+ from dataclasses import dataclass
15
+ from enum import Enum
16
+ import hashlib
17
+ import zlib
18
+
19
+ # ----------------------------------------------------------------------
20
+ # Базовые структуры данных для 3D текстур
21
+ # ----------------------------------------------------------------------
22
+
23
+ class VolumeFormat(Enum):
24
+ """Форматы объемных текстур"""
25
+ GRAYSCALE = 1 # 1 канал: плотность/прозрачность
26
+ GRAYSCALE_ALPHA = 2 # 2 канала: плотность + альфа
27
+ RGB = 3 # 3 канала: цвет
28
+ RGBA = 4 # 4 канала: цвет + альфа
29
+ RGBA_FLOAT = 5 # 4 канала с float32 точностью
30
+ DENSITY = 6 # 1 канал плотности (для облаков, дыма)
31
+ MATERIAL_ID = 7 # 1 канал ID материала
32
+
33
+ @dataclass
34
+ class VolumeTexture3D:
35
+ """Структура для хранения 3D текстуры"""
36
+ data: np.ndarray # Форма: (depth, height, width, channels)
37
+ format: VolumeFormat # Формат данных
38
+ voxel_size: Tuple[float, float, float] = (1.0, 1.0, 1.0) # Размер вокселя
39
+ world_origin: Tuple[float, float, float] = (0.0, 0.0, 0.0) # Начало координат
40
+ compression: Optional[str] = None # Тип сжатия
41
+
42
+ def __post_init__(self):
43
+ """Проверка и нормализация данных"""
44
+ if self.data.ndim != 4:
45
+ raise ValueError(f"Volume data must be 4D (depth, height, width, channels), got {self.data.ndim}D")
46
+
47
+ # Нормализация в [0, 1] если данные не float
48
+ if self.data.dtype != np.float32 and self.data.dtype != np.float64:
49
+ if self.data.dtype in [np.uint8, np.uint16, np.uint32]:
50
+ max_val = np.iinfo(self.data.dtype).max
51
+ self.data = self.data.astype(np.float32) / max_val
52
+ else:
53
+ self.data = self.data.astype(np.float32)
54
+
55
+ @property
56
+ def shape(self) -> Tuple[int, int, int, int]:
57
+ """Возвращает форму данных (D, H, W, C)"""
58
+ return self.data.shape
59
+
60
+ @property
61
+ def size_bytes(self) -> int:
62
+ """Размер в байтах"""
63
+ return self.data.nbytes
64
+
65
+ @property
66
+ def dimensions(self) -> Tuple[int, int, int]:
67
+ """Возвращает 3D размеры (без каналов)"""
68
+ return self.shape[:3]
69
+
70
+ @property
71
+ def channels(self) -> int:
72
+ """Количество каналов"""
73
+ return self.shape[3]
74
+
75
+ def sample(self, x: float, y: float, z: float,
76
+ wrap_mode: str = "repeat") -> np.ndarray:
77
+ """
78
+ Трилинейная интерполяция в точке (x, y, z)
79
+ Координаты нормализованы к [0, 1]
80
+ """
81
+ return sample_volume_trilinear(self.data, x, y, z, wrap_mode)
82
+
83
+ def get_slice(self, axis: str, index: int) -> np.ndarray:
84
+ """
85
+ Получение 2D среза из 3D текстуры
86
+
87
+ Args:
88
+ axis: 'x', 'y', или 'z'
89
+ index: Индекс среза (0 - размер-1)
90
+ """
91
+ if axis == 'x':
92
+ if index < 0 or index >= self.shape[2]:
93
+ raise IndexError(f"X index {index} out of bounds [0, {self.shape[2]-1}]")
94
+ return self.data[:, :, index, :]
95
+ elif axis == 'y':
96
+ if index < 0 or index >= self.shape[1]:
97
+ raise IndexError(f"Y index {index} out of bounds [0, {self.shape[1]-1}]")
98
+ return self.data[:, index, :, :]
99
+ elif axis == 'z':
100
+ if index < 0 or index >= self.shape[0]:
101
+ raise IndexError(f"Z index {index} out of bounds [0, {self.shape[0]-1}]")
102
+ return self.data[index, :, :, :]
103
+ else:
104
+ raise ValueError(f"Axis must be 'x', 'y', or 'z', got {axis}")
105
+
106
+ def compress(self, method: str = "zlib", level: int = 6) -> bytes:
107
+ """Сжатие данных текстуры"""
108
+ if method == "zlib":
109
+ return zlib.compress(self.data.tobytes(), level=level)
110
+ elif method == "lz4":
111
+ try:
112
+ import lz4.frame
113
+ return lz4.frame.compress(self.data.tobytes())
114
+ except ImportError:
115
+ warnings.warn("lz4 not installed, falling back to zlib")
116
+ return zlib.compress(self.data.tobytes(), level=level)
117
+ else:
118
+ raise ValueError(f"Unknown compression method: {method}")
119
+
120
+ @classmethod
121
+ def decompress(cls, compressed_data: bytes, shape: Tuple[int, int, int, int],
122
+ dtype: np.dtype = np.float32, method: str = "zlib") -> 'VolumeTexture3D':
123
+ """Восстановление из сжатых данных"""
124
+ if method == "zlib":
125
+ data_bytes = zlib.decompress(compressed_data)
126
+ elif method == "lz4":
127
+ try:
128
+ import lz4.frame
129
+ data_bytes = lz4.frame.decompress(compressed_data)
130
+ except ImportError:
131
+ raise ImportError("lz4 required for lz4 decompression")
132
+ else:
133
+ raise ValueError(f"Unknown compression method: {method}")
134
+
135
+ data = np.frombuffer(data_bytes, dtype=dtype).reshape(shape)
136
+ return cls(data=data, format=VolumeFormat.RGBA_FLOAT)
137
+
138
+ # ----------------------------------------------------------------------
139
+ # Функции трилинейной интерполяции (оптимизированные с Numba)
140
+ # ----------------------------------------------------------------------
141
+
142
+ @jit(nopython=True, cache=True)
143
+ def sample_volume_trilinear(volume: np.ndarray,
144
+ x: float, y: float, z: float,
145
+ wrap_mode: str = "repeat") -> np.ndarray:
146
+ """
147
+ Трилинейная интерполяция в 3D текстуре
148
+
149
+ Args:
150
+ volume: 4D массив (D, H, W, C)
151
+ x, y, z: Нормализованные координаты [0, 1]
152
+ wrap_mode: Режим заворачивания координат: "repeat", "clamp", "mirror"
153
+
154
+ Returns:
155
+ Интерполированное значение (C,)
156
+ """
157
+ depth, height, width, channels = volume.shape
158
+
159
+ # Обрабатываем режим заворачивания
160
+ if wrap_mode == "repeat":
161
+ x = x - np.floor(x)
162
+ y = y - np.floor(y)
163
+ z = z - np.floor(z)
164
+ elif wrap_mode == "clamp":
165
+ x = np.clip(x, 0.0, 1.0)
166
+ y = np.clip(y, 0.0, 1.0)
167
+ z = np.clip(z, 0.0, 1.0)
168
+ elif wrap_mode == "mirror":
169
+ x = np.abs(1.0 - np.abs(1.0 - x * 2.0)) / 2.0
170
+ y = np.abs(1.0 - np.abs(1.0 - y * 2.0)) / 2.0
171
+ z = np.abs(1.0 - np.abs(1.0 - z * 2.0)) / 2.0
172
+ else:
173
+ x = np.clip(x, 0.0, 1.0)
174
+ y = np.clip(y, 0.0, 1.0)
175
+ z = np.clip(z, 0.0, 1.0)
176
+
177
+ # Переводим в координаты текстуры
178
+ fx = x * (width - 1)
179
+ fy = y * (height - 1)
180
+ fz = z * (depth - 1)
181
+
182
+ # Целочисленные координаты
183
+ ix0 = int(np.floor(fx))
184
+ iy0 = int(np.floor(fy))
185
+ iz0 = int(np.floor(fz))
186
+
187
+ # Проверяем границы
188
+ if ix0 < 0 or ix0 >= width or iy0 < 0 or iy0 >= height or iz0 < 0 or iz0 >= depth:
189
+ return np.zeros(channels, dtype=volume.dtype)
190
+
191
+ # Дробные части
192
+ dx = fx - ix0
193
+ dy = fy - iy0
194
+ dz = fz - iz0
195
+
196
+ # Следующие координаты с проверкой границ
197
+ ix1 = min(ix0 + 1, width - 1)
198
+ iy1 = min(iy0 + 1, height - 1)
199
+ iz1 = min(iz0 + 1, depth - 1)
200
+
201
+ # Интерполяция по X
202
+ c000 = volume[iz0, iy0, ix0, :]
203
+ c001 = volume[iz0, iy0, ix1, :]
204
+ c010 = volume[iz0, iy1, ix0, :]
205
+ c011 = volume[iz0, iy1, ix1, :]
206
+ c100 = volume[iz1, iy0, ix0, :]
207
+ c101 = volume[iz1, iy0, ix1, :]
208
+ c110 = volume[iz1, iy1, ix0, :]
209
+ c111 = volume[iz1, iy1, ix1, :]
210
+
211
+ # Линейная интерполяция по X
212
+ c00 = c000 * (1 - dx) + c001 * dx
213
+ c01 = c010 * (1 - dx) + c011 * dx
214
+ c10 = c100 * (1 - dx) + c101 * dx
215
+ c11 = c110 * (1 - dx) + c111 * dx
216
+
217
+ # Линейная интерполяция по Y
218
+ c0 = c00 * (1 - dy) + c01 * dy
219
+ c1 = c10 * (1 - dy) + c11 * dy
220
+
221
+ # Линейная интерполяция по Z
222
+ result = c0 * (1 - dz) + c1 * dz
223
+
224
+ return result
225
+
226
+ @jit(nopython=True, parallel=True, cache=True)
227
+ def sample_volume_trilinear_batch(volume: np.ndarray,
228
+ coords: np.ndarray,
229
+ wrap_mode: str = "repeat") -> np.ndarray:
230
+ """
231
+ Пакетная трилинейная интерполяция для множества точек
232
+
233
+ Args:
234
+ volume: 4D массив (D, H, W, C)
235
+ coords: Массив координат (N, 3) в [0, 1]
236
+ wrap_mode: Режим заворачивания координат
237
+
238
+ Returns:
239
+ Массив интерполированных значений (N, C)
240
+ """
241
+ depth, height, width, channels = volume.shape
242
+ n_points = coords.shape[0]
243
+ result = np.zeros((n_points, channels), dtype=volume.dtype)
244
+
245
+ for i in prange(n_points):
246
+ x, y, z = coords[i, 0], coords[i, 1], coords[i, 2]
247
+
248
+ # Обрабатываем режим заворачивания
249
+ if wrap_mode == "repeat":
250
+ x = x - np.floor(x)
251
+ y = y - np.floor(y)
252
+ z = z - np.floor(z)
253
+ elif wrap_mode == "clamp":
254
+ x = max(0.0, min(1.0, x))
255
+ y = max(0.0, min(1.0, y))
256
+ z = max(0.0, min(1.0, z))
257
+
258
+ # Переводим в координаты текстуры
259
+ fx = x * (width - 1)
260
+ fy = y * (height - 1)
261
+ fz = z * (depth - 1)
262
+
263
+ # Целочисленные координаты
264
+ ix0 = int(np.floor(fx))
265
+ iy0 = int(np.floor(fy))
266
+ iz0 = int(np.floor(fz))
267
+
268
+ # Проверяем границы
269
+ if ix0 < 0 or ix0 >= width or iy0 < 0 or iy0 >= height or iz0 < 0 or iz0 >= depth:
270
+ continue
271
+
272
+ # Дробные части
273
+ dx = fx - ix0
274
+ dy = fy - iy0
275
+ dz = fz - iz0
276
+
277
+ # Следующие координаты с проверкой границ
278
+ ix1 = ix0 + 1 if ix0 < width - 1 else ix0
279
+ iy1 = iy0 + 1 if iy0 < height - 1 else iy0
280
+ iz1 = iz0 + 1 if iz0 < depth - 1 else iz0
281
+
282
+ # Берем значения из текстуры
283
+ c000 = volume[iz0, iy0, ix0, :]
284
+ c001 = volume[iz0, iy0, ix1, :]
285
+ c010 = volume[iz0, iy1, ix0, :]
286
+ c011 = volume[iz0, iy1, ix1, :]
287
+ c100 = volume[iz1, iy0, ix0, :]
288
+ c101 = volume[iz1, iy0, ix1, :]
289
+ c110 = volume[iz1, iy1, ix0, :]
290
+ c111 = volume[iz1, iy1, ix1, :]
291
+
292
+ # Линейная интерполяция по X
293
+ c00 = c000 * (1 - dx) + c001 * dx
294
+ c01 = c010 * (1 - dx) + c011 * dx
295
+ c10 = c100 * (1 - dx) + c101 * dx
296
+ c11 = c110 * (1 - dx) + c111 * dx
297
+
298
+ # Линейная интерполяция по Y
299
+ c0 = c00 * (1 - dy) + c01 * dy
300
+ c1 = c10 * (1 - dy) + c11 * dy
301
+
302
+ # Линейная интерполяция по Z
303
+ result[i, :] = c0 * (1 - dz) + c1 * dz
304
+
305
+ return result
306
+
307
+ # ----------------------------------------------------------------------
308
+ # Генераторы 3D текстур на основе шума
309
+ # ----------------------------------------------------------------------
310
+
311
+ class VolumeTextureGenerator3D:
312
+ """Генератор 3D текстур на основе процедурного шума"""
313
+
314
+ def __init__(self, seed: int = 42):
315
+ self.seed = seed
316
+ self.cache = {}
317
+ try:
318
+ from .simplex_noise import SimplexNoise
319
+ self.noise = SimplexNoise(seed)
320
+ except ImportError:
321
+ warnings.warn("SimplexNoise not available, using fallback")
322
+ self.noise = None
323
+
324
+ def generate_clouds_3d(self,
325
+ width: int = 64,
326
+ height: int = 64,
327
+ depth: int = 64,
328
+ scale: float = 0.05,
329
+ density: float = 0.5,
330
+ detail: int = 3,
331
+ animated: bool = False,
332
+ time: float = 0.0) -> VolumeTexture3D:
333
+ """
334
+ Генерация 3D текстуры облаков
335
+
336
+ Args:
337
+ width, height, depth: Размеры 3D текстуры
338
+ scale: Масштаб шума
339
+ density: Плотность облаков (0-1)
340
+ detail: Детализация (количество октав)
341
+ animated: Анимированная текстура (использует 4D шум)
342
+ time: Время для анимации
343
+
344
+ Returns:
345
+ VolumeTexture3D с облачной текстурой
346
+ """
347
+ print(f"Generating 3D cloud texture {width}x{height}x{depth}...")
348
+
349
+ # Создаем координатную сетку
350
+ x = np.linspace(0, width * scale, width)
351
+ y = np.linspace(0, height * scale, height)
352
+ z = np.linspace(0, depth * scale, depth)
353
+
354
+ xx, yy, zz = np.meshgrid(x, y, z, indexing='ij')
355
+
356
+ # Генерируем шум
357
+ if self.noise is None:
358
+ # Fallback: простой шум
359
+ noise_data = np.sin(xx * 0.1) * np.cos(yy * 0.1) * np.sin(zz * 0.1)
360
+ else:
361
+ if animated and time > 0:
362
+ # Анимированные облака с 4D шумом
363
+ noise_data = np.zeros((depth, height, width), dtype=np.float32)
364
+ for i in range(depth):
365
+ # Используем z-координату + время как четвертое измерение
366
+ w = zz[i, :, :] * 0.1 + time * 0.1
367
+ noise_slice = self.noise.noise_4d(xx[i, :, :], yy[i, :, :],
368
+ zz[i, :, :], w)
369
+ noise_data[i, :, :] = noise_slice
370
+ else:
371
+ # Статичные облаки с 3D шумом
372
+ noise_data = self.noise.noise_3d(xx, yy, zz)
373
+
374
+ # Фрактальный шум для детализации
375
+ if detail > 1 and self.noise is not None:
376
+ fractal = np.zeros_like(noise_data)
377
+ amplitude = 1.0
378
+ frequency = scale * 2
379
+
380
+ for i in range(detail):
381
+ nx = xx * frequency
382
+ ny = yy * frequency
383
+ nz = zz * frequency
384
+
385
+ if animated and time > 0:
386
+ # Анимированный детальный шум
387
+ octave_noise = np.zeros_like(noise_data)
388
+ for j in range(depth):
389
+ w = nz[j, :, :] * 0.1 + time * 0.1 + i * 10
390
+ octave_slice = self.noise.noise_4d(nx[j, :, :], ny[j, :, :],
391
+ nz[j, :, :], w)
392
+ octave_noise[j, :, :] = octave_slice
393
+ else:
394
+ octave_noise = self.noise.noise_3d(nx, ny, nz)
395
+
396
+ fractal += amplitude * octave_noise
397
+ amplitude *= 0.5
398
+ frequency *= 2.0
399
+
400
+ noise_data = noise_data * 0.7 + fractal * 0.3
401
+
402
+ # Нормализация и применение плотности
403
+ noise_data = (noise_data - noise_data.min()) / (noise_data.max() - noise_data.min())
404
+
405
+ # Формируем плотность облаков
406
+ density_map = np.clip(noise_data * 2.0 - (1.0 - density), 0, 1)
407
+
408
+ # Создаем RGBA текстуру
409
+ texture_data = np.zeros((depth, height, width, 4), dtype=np.float32)
410
+
411
+ # Белый цвет для облаков
412
+ texture_data[..., 0] = 1.0 # R
413
+ texture_data[..., 1] = 1.0 # G
414
+ texture_data[..., 2] = 1.0 # B
415
+ texture_data[..., 3] = density_map # Альфа = плотность
416
+
417
+ return VolumeTexture3D(
418
+ data=texture_data,
419
+ format=VolumeFormat.RGBA_FLOAT,
420
+ voxel_size=(1.0/width, 1.0/height, 1.0/depth)
421
+ )
422
+
423
+ def generate_marble_3d(self,
424
+ width: int = 64,
425
+ height: int = 64,
426
+ depth: int = 64,
427
+ scale: float = 0.02,
428
+ vein_strength: float = 0.8,
429
+ vein_frequency: float = 5.0) -> VolumeTexture3D:
430
+ """
431
+ Генерация 3D мраморной текстуры
432
+
433
+ Args:
434
+ width, height, depth: Размеры 3D текстуры
435
+ scale: Масштаб шума
436
+ vein_strength: Сила прожилок (0-1)
437
+ vein_frequency: Частота прожилок
438
+
439
+ Returns:
440
+ VolumeTexture3D с мраморной текстурой
441
+ """
442
+ print(f"Generating 3D marble texture {width}x{height}x{depth}...")
443
+
444
+ # Создаем координатную сетку
445
+ x = np.linspace(0, width * scale, width)
446
+ y = np.linspace(0, height * scale, height)
447
+ z = np.linspace(0, depth * scale, depth)
448
+
449
+ xx, yy, zz = np.meshgrid(x, y, z, indexing='ij')
450
+
451
+ # Базовый шум для структуры
452
+ if self.noise is None:
453
+ base_noise = np.sin(xx * 0.5) * np.cos(yy * 0.5) * np.sin(zz * 0.5)
454
+ else:
455
+ base_noise = self.noise.noise_3d(xx, yy, zz)
456
+
457
+ # Создаем синусоидальные прожилки в 3D
458
+ # Используем синус от расстояния до центра по разным осям
459
+ marble_pattern = np.zeros_like(base_noise)
460
+
461
+ # Несколько направлений прожилок
462
+ directions = [
463
+ (1.0, 0.5, 0.3),
464
+ (0.3, 1.0, 0.5),
465
+ (0.5, 0.3, 1.0)
466
+ ]
467
+
468
+ for dir_x, dir_y, dir_z in directions:
469
+ # Проекция на направление
470
+ projection = xx * dir_x + yy * dir_y + zz * dir_z
471
+
472
+ # Синусоидальные прожилки
473
+ veins = np.sin(projection * vein_frequency + base_noise * 3) * 0.5 + 0.5
474
+ marble_pattern += veins
475
+
476
+ marble_pattern /= len(directions)
477
+
478
+ # Добавляем детали
479
+ if self.noise is not None:
480
+ detail = self.noise.fractal_noise_3d(
481
+ xx, yy, zz, octaves=3, persistence=0.5,
482
+ lacunarity=2.0, base_scale=scale * 5
483
+ )
484
+ marble_pattern = marble_pattern * 0.8 + detail * 0.2
485
+
486
+ # Нормализация
487
+ marble_pattern = (marble_pattern - marble_pattern.min()) / \
488
+ (marble_pattern.max() - marble_pattern.min())
489
+
490
+ # Создаем RGBA текстуру
491
+ texture_data = np.zeros((depth, height, width, 4), dtype=np.float32)
492
+
493
+ # Цвета мрамора
494
+ base_color = np.array([0.92, 0.87, 0.82]) # Светлый мрамор
495
+ vein_color = np.array([0.75, 0.65, 0.55]) # Темные прожилки
496
+
497
+ # Интерполяция между цветами
498
+ for i in range(3):
499
+ texture_data[..., i] = base_color[i] * (1 - marble_pattern) + \
500
+ vein_color[i] * marble_pattern
501
+
502
+ # Непрозрачный
503
+ texture_data[..., 3] = 1.0
504
+
505
+ return VolumeTexture3D(
506
+ data=texture_data,
507
+ format=VolumeFormat.RGBA_FLOAT,
508
+ voxel_size=(1.0/width, 1.0/height, 1.0/depth)
509
+ )
510
+
511
+ def generate_wood_3d(self,
512
+ width: int = 64,
513
+ height: int = 64,
514
+ depth: int = 64,
515
+ scale: float = 0.03,
516
+ ring_frequency: float = 10.0) -> VolumeTexture3D:
517
+ """
518
+ Генерация 3D деревянной текстуры
519
+
520
+ Args:
521
+ width, height, depth: Размеры 3D текстуры
522
+ scale: Масштаб текстуры
523
+ ring_frequency: Частота годичных колец
524
+
525
+ Returns:
526
+ VolumeTexture3D с деревянной текстурой
527
+ """
528
+ print(f"Generating 3D wood texture {width}x{height}x{depth}...")
529
+
530
+ # Создаем координатную сетку от -1 до 1
531
+ x = np.linspace(-1, 1, width)
532
+ y = np.linspace(-1, 1, height)
533
+ z = np.linspace(-1, 1, depth)
534
+
535
+ xx, yy, zz = np.meshgrid(x, y, z, indexing='ij')
536
+
537
+ # Создаем цилиндрические координаты для древесных колец
538
+ # Используем расстояние от центральной оси (например, ось Z)
539
+ radius = np.sqrt(xx*xx + yy*yy) * ring_frequency
540
+
541
+ # Добавляем шум для реалистичности
542
+ if self.noise is None:
543
+ noise = np.sin(xx * 5) * np.cos(yy * 5) * np.sin(zz * 3) * 0.2
544
+ else:
545
+ noise = self.noise.noise_3d(xx * 5, yy * 5, zz * 3) * 0.2
546
+
547
+ # Кольца плюс шум
548
+ wood_pattern = np.sin(radius * 2 * np.pi + noise * 2) * 0.5 + 0.5
549
+
550
+ # Добавляем волокна вдоль оси Z
551
+ fiber = np.sin(zz * 20 + self.noise.noise_3d(xx * 10, yy * 10, zz) * 3) * 0.1
552
+ wood_pattern += fiber
553
+
554
+ # Нормализация
555
+ wood_pattern = np.clip(wood_pattern, 0, 1)
556
+
557
+ # Создаем RGBA текстуру
558
+ texture_data = np.zeros((depth, height, width, 4), dtype=np.float32)
559
+
560
+ # Цвета дерева
561
+ light_wood = np.array([0.7, 0.5, 0.3]) # Светлая древесина
562
+ dark_wood = np.array([0.4, 0.25, 0.1]) # Темная древесина
563
+
564
+ # Интерполяция между цветами
565
+ for i in range(3):
566
+ texture_data[..., i] = light_wood[i] * wood_pattern + \
567
+ dark_wood[i] * (1 - wood_pattern)
568
+
569
+ # Добавляем вариации цвета на основе шума
570
+ color_variation = np.zeros_like(texture_data[..., :3])
571
+ if self.noise is not None:
572
+ for i in range(3):
573
+ color_noise = self.noise.noise_3d(xx * 2, yy * 2, zz * 2)
574
+ color_variation[..., i] = color_noise * 0.1
575
+
576
+ texture_data[..., :3] += color_variation
577
+ texture_data[..., 3] = 1.0 # Непрозрачный
578
+
579
+ return VolumeTexture3D(
580
+ data=np.clip(texture_data, 0, 1),
581
+ format=VolumeFormat.RGBA_FLOAT,
582
+ voxel_size=(1.0/width, 1.0/height, 1.0/depth)
583
+ )
584
+
585
+ def generate_perlin_3d(self,
586
+ width: int = 64,
587
+ height: int = 64,
588
+ depth: int = 64,
589
+ scale: float = 0.05,
590
+ octaves: int = 4) -> VolumeTexture3D:
591
+ """
592
+ Генерация 3D текстуры на основе фрактального шума Перлина
593
+
594
+ Args:
595
+ width, height, depth: Размеры 3D текстуры
596
+ scale: Масштаб шума
597
+ octaves: Количество октав
598
+
599
+ Returns:
600
+ VolumeTexture3D с текстурой шума
601
+ """
602
+ if self.noise is None:
603
+ raise ValueError("Noise generator not available")
604
+
605
+ print(f"Generating 3D Perlin noise texture {width}x{height}x{depth}...")
606
+
607
+ # Создаем координатную сетку
608
+ x = np.linspace(0, width * scale, width)
609
+ y = np.linspace(0, height * scale, height)
610
+ z = np.linspace(0, depth * scale, depth)
611
+
612
+ xx, yy, zz = np.meshgrid(x, y, z, indexing='ij')
613
+
614
+ # Генерируем фрактальный шум
615
+ noise_data = np.zeros_like(xx, dtype=np.float32)
616
+ amplitude = 1.0
617
+ frequency = scale
618
+
619
+ for i in range(octaves):
620
+ nx = xx * frequency
621
+ ny = yy * frequency
622
+ nz = zz * frequency
623
+
624
+ octave_noise = self.noise.noise_3d(nx, ny, nz)
625
+ noise_data += amplitude * octave_noise
626
+
627
+ amplitude *= 0.5
628
+ frequency *= 2.0
629
+
630
+ # Нормализация к [0, 1]
631
+ noise_data = (noise_data - noise_data.min()) / (noise_data.max() - noise_data.min())
632
+ noise_data = np.transpose(noise_data, (2, 1, 0))
633
+
634
+ # Создаем grayscale текстуру
635
+ texture_data = np.zeros((depth, height, width, 1), dtype=np.float32)
636
+ texture_data[..., 0] = noise_data
637
+
638
+ return VolumeTexture3D(
639
+ data=texture_data,
640
+ format=VolumeFormat.GRAYSCALE,
641
+ voxel_size=(1.0/width, 1.0/height, 1.0/depth)
642
+ )
643
+
644
+ def generate_lava_3d(self,
645
+ width: int = 64,
646
+ height: int = 64,
647
+ depth: int = 64,
648
+ scale: float = 0.03,
649
+ temperature: float = 0.7,
650
+ animated: bool = False,
651
+ time: float = 0.0) -> VolumeTexture3D:
652
+ """
653
+ Генерация 3D текстуры лавы
654
+
655
+ Args:
656
+ width, height, depth: Размеры 3D текстуры
657
+ scale: Масштаб шума
658
+ temperature: Температура (влияет на цвета)
659
+ animated: Анимированная текстура
660
+ time: Время для анимации
661
+
662
+ Returns:
663
+ VolumeTexture3D с текстурой лавы
664
+ """
665
+ print(f"Generating 3D lava texture {width}x{height}x{depth}...")
666
+
667
+ # Создаем координатную сетку
668
+ x = np.linspace(0, width * scale, width)
669
+ y = np.linspace(0, height * scale, height)
670
+ z = np.linspace(0, depth * scale, depth)
671
+
672
+ xx, yy, zz = np.meshgrid(x, y, z, indexing='ij')
673
+
674
+ # Базовый шум
675
+ if self.noise is None:
676
+ base_noise = np.sin(xx * 0.5) * np.cos(yy * 0.5) * np.sin(zz * 0.5)
677
+ else:
678
+ if animated and time > 0:
679
+ # Анимированная лава с 4D шумом
680
+ base_noise = np.zeros_like(xx, dtype=np.float32)
681
+ for i in range(depth):
682
+ w = zz[:, :, i] * 0.1 + time * 0.2
683
+ noise_slice = self.noise.noise_4d(
684
+ xx[:, :, i],
685
+ yy[:, :, i],
686
+ zz[:, :, i],
687
+ w,
688
+ )
689
+ base_noise[:, :, i] = noise_slice
690
+ else:
691
+ base_noise = self.noise.noise_3d(xx, yy, zz)
692
+
693
+ # Детализированный шум для текстуры
694
+ if self.noise is not None:
695
+ detail = self.noise.fractal_noise_3d(
696
+ xx, yy, zz, octaves=5, persistence=0.6,
697
+ lacunarity=1.8, base_scale=scale * 3
698
+ )
699
+ lava = base_noise * 0.6 + detail * 0.4
700
+ else:
701
+ lava = base_noise
702
+
703
+ # Нормализация
704
+ lava = (lava - lava.min()) / (lava.max() - lava.min())
705
+
706
+ # Создаем RGBA текстуру
707
+ texture_data = np.zeros((depth, height, width, 4), dtype=np.float32)
708
+
709
+ # Цвета лавы на основе температуры
710
+ # Холодная лава: темно-красная, горячая: ярко-желтая
711
+ hot_color = np.array([1.0, 0.8, 0.1]) # Ярко-желтый
712
+ cold_color = np.array([0.6, 0.1, 0.0]) # Темно-красный
713
+
714
+ # Смешиваем цвета на основе значения шума и температуры
715
+ for i in range(3):
716
+ texture_data[..., i] = cold_color[i] * (1 - lava * temperature) + \
717
+ hot_color[i] * lava * temperature
718
+
719
+ # Яркость для эффекта свечения
720
+ brightness = np.power(lava, 2) * temperature
721
+
722
+ # Альфа-канал с вариациями
723
+ texture_data[..., 3] = 0.9 + brightness * 0.2
724
+
725
+ # Добавляем шум к альфа-каналу для эффекта пузырей
726
+ if self.noise is not None:
727
+ bubble_noise = self.noise.noise_3d(xx * 10, yy * 10, zz * 10) * 0.1
728
+ texture_data[..., 3] += bubble_noise
729
+
730
+ return VolumeTexture3D(
731
+ data=np.clip(texture_data, 0, 1),
732
+ format=VolumeFormat.RGBA_FLOAT,
733
+ voxel_size=(1.0/width, 1.0/height, 1.0/depth)
734
+ )
735
+
736
+ def generate_rocks_3d(self,
737
+ width: int = 64,
738
+ height: int = 64,
739
+ depth: int = 64,
740
+ scale: float = 0.08,
741
+ hardness: float = 0.6) -> VolumeTexture3D:
742
+ """Генерация 3D текстуры камня"""
743
+ print(f"Generating 3D rock texture {width}x{height}x{depth}...")
744
+
745
+ x = np.linspace(0, width * scale, width)
746
+ y = np.linspace(0, height * scale, height)
747
+ z = np.linspace(0, depth * scale, depth)
748
+ xx, yy, zz = np.meshgrid(x, y, z, indexing='ij')
749
+
750
+ if self.noise is None:
751
+ noise_data = np.sin(xx * 0.3) * np.cos(yy * 0.3) * np.sin(zz * 0.3)
752
+ else:
753
+ noise_data = np.zeros_like(xx, dtype=np.float32)
754
+ amplitude = 1.0
755
+ frequency = 1.0
756
+ for _ in range(4):
757
+ noise_data += amplitude * self.noise.noise_3d(
758
+ xx * frequency, yy * frequency, zz * frequency
759
+ )
760
+ amplitude *= 0.5
761
+ frequency *= 2.0
762
+
763
+ noise_data = (noise_data - noise_data.min()) / (noise_data.max() - noise_data.min())
764
+ noise_data = np.transpose(noise_data, (2, 1, 0))
765
+
766
+ density_map = np.clip((noise_data - (1.0 - hardness)) * 3.0, 0.0, 1.0)
767
+
768
+ texture_data = np.zeros((depth, height, width, 4), dtype=np.float32)
769
+ rock_color = np.array([0.45, 0.45, 0.48])
770
+ texture_data[..., :3] = rock_color
771
+ texture_data[..., 3] = density_map
772
+
773
+ return VolumeTexture3D(
774
+ data=np.clip(texture_data, 0, 1),
775
+ format=VolumeFormat.RGBA_FLOAT,
776
+ voxel_size=(1.0/width, 1.0/height, 1.0/depth)
777
+ )
778
+
779
+ def generate_grass_3d(self,
780
+ width: int = 64,
781
+ height: int = 64,
782
+ depth: int = 64,
783
+ scale: float = 0.1,
784
+ density: float = 0.5) -> VolumeTexture3D:
785
+ """Генерация 3D текстуры травы"""
786
+ print(f"Generating 3D grass texture {width}x{height}x{depth}...")
787
+
788
+ x = np.linspace(0, width * scale, width)
789
+ y = np.linspace(0, height * scale, height)
790
+ z = np.linspace(0, depth * scale, depth)
791
+ xx, yy, zz = np.meshgrid(x, y, z, indexing='ij')
792
+
793
+ if self.noise is None:
794
+ noise_data = np.sin(xx * 0.5) * np.cos(yy * 0.5) * np.sin(zz * 0.5)
795
+ else:
796
+ noise_data = self.noise.noise_3d(xx, yy, zz)
797
+
798
+ noise_data = (noise_data - noise_data.min()) / (noise_data.max() - noise_data.min())
799
+ noise_data = np.transpose(noise_data, (2, 1, 0))
800
+
801
+ density_map = np.clip(noise_data * 1.5 - (1.0 - density), 0.0, 1.0)
802
+
803
+ texture_data = np.zeros((depth, height, width, 4), dtype=np.float32)
804
+ grass_color = np.array([0.15, 0.50, 0.20])
805
+ texture_data[..., :3] = grass_color
806
+ texture_data[..., 3] = density_map
807
+
808
+ return VolumeTexture3D(
809
+ data=np.clip(texture_data, 0, 1),
810
+ format=VolumeFormat.RGBA_FLOAT,
811
+ voxel_size=(1.0/width, 1.0/height, 1.0/depth)
812
+ )
813
+
814
+ def generate_material_id_3d(self,
815
+ width: int = 64,
816
+ height: int = 64,
817
+ depth: int = 64,
818
+ num_materials: int = 5) -> VolumeTexture3D:
819
+ """
820
+ Генерация 3D текстуры с ID материалов (для воксельных миров)
821
+
822
+ Args:
823
+ width, height, depth: Размеры 3D текстуры
824
+ num_materials: Количество различных материалов
825
+
826
+ Returns:
827
+ VolumeTexture3D с ID материалов
828
+ """
829
+ print(f"Generating 3D material ID texture {width}x{height}x{depth}...")
830
+
831
+ # Создаем случайное распределение материалов
832
+ np.random.seed(self.seed)
833
+
834
+ # Простой шум для распределения
835
+ x = np.linspace(0, 10, width)
836
+ y = np.linspace(0, 10, height)
837
+ z = np.linspace(0, 10, depth)
838
+
839
+ xx, yy, zz = np.meshgrid(x, y, z, indexing='ij')
840
+
841
+ if self.noise is None:
842
+ # Простой паттерн
843
+ noise = np.sin(xx) * np.cos(yy) * np.sin(zz)
844
+ else:
845
+ # Используем фрактальный шум для естественного распределения
846
+ noise = self.noise.fractal_noise_3d(
847
+ xx, yy, zz, octaves=3, persistence=0.5,
848
+ lacunarity=2.0, base_scale=0.1
849
+ )
850
+
851
+ # Нормализуем и квантуем на num_materials материалов
852
+ noise_normalized = (noise - noise.min()) / (noise.max() - noise.min())
853
+ material_ids = (noise_normalized * (num_materials - 1)).astype(np.uint8)
854
+
855
+ # Создаем текстуру с одним каналом
856
+ texture_data = np.zeros((depth, height, width, 1), dtype=np.uint8)
857
+ texture_data[..., 0] = material_ids
858
+
859
+ return VolumeTexture3D(
860
+ data=texture_data,
861
+ format=VolumeFormat.MATERIAL_ID,
862
+ voxel_size=(1.0/width, 1.0/height, 1.0/depth)
863
+ )
864
+
865
+ # ----------------------------------------------------------------------
866
+ # Система кэширования и потоковой загрузки 3D текстур
867
+ # ----------------------------------------------------------------------
868
+
869
+ class VolumeTextureCache:
870
+ """Кэш для 3D текстур с LRU политикой и сжатием"""
871
+
872
+ def __init__(self, max_size_mb: int = 1024, use_compression: bool = True):
873
+ self.max_size_bytes = max_size_mb * 1024 * 1024
874
+ self.use_compression = use_compression
875
+ self.cache = {} # key -> (texture, timestamp, size_bytes)
876
+ self.lru_queue = []
877
+ self.current_size_bytes = 0
878
+ self.hits = 0
879
+ self.misses = 0
880
+ self.compression_ratio = 0.0
881
+
882
+ def get(self, key: str) -> Optional[VolumeTexture3D]:
883
+ """Получение текстуры из кэша"""
884
+ if key in self.cache:
885
+ # Обновляем LRU
886
+ self.lru_queue.remove(key)
887
+ self.lru_queue.append(key)
888
+
889
+ texture, _, _ = self.cache[key]
890
+ self.hits += 1
891
+ return texture
892
+
893
+ self.misses += 1
894
+ return None
895
+
896
+ def put(self, key: str, texture: VolumeTexture3D):
897
+ """Добавление текстуры в кэш"""
898
+ size_bytes = texture.size_bytes
899
+
900
+ # Сжатие если нужно
901
+ if self.use_compression and texture.compression is None:
902
+ try:
903
+ compressed_data = texture.compress(method="zlib")
904
+ compressed_size = len(compressed_data)
905
+
906
+ # Создаем сжатую версию
907
+ compressed_texture = VolumeTexture3D(
908
+ data=texture.data, # Оригинальные данные все еще в памяти
909
+ format=texture.format,
910
+ voxel_size=texture.voxel_size,
911
+ world_origin=texture.world_origin,
912
+ compression="zlib"
913
+ )
914
+ texture = compressed_texture
915
+ size_bytes = compressed_size
916
+
917
+ if texture.size_bytes > 0:
918
+ self.compression_ratio = compressed_size / texture.size_bytes
919
+ except Exception as e:
920
+ warnings.warn(f"Compression failed: {e}")
921
+
922
+ # Проверяем, поместится ли
923
+ while self.current_size_bytes + size_bytes > self.max_size_bytes and self.lru_queue:
924
+ # Удаляем самый старый элемент
925
+ oldest_key = self.lru_queue.pop(0)
926
+ _, _, old_size = self.cache[oldest_key]
927
+ self.current_size_bytes -= old_size
928
+ del self.cache[oldest_key]
929
+
930
+ # Добавляем
931
+ self.cache[key] = (texture, time.time(), size_bytes)
932
+ self.lru_queue.append(key)
933
+ self.current_size_bytes += size_bytes
934
+
935
+ def clear(self):
936
+ """Очистка кэша"""
937
+ self.cache.clear()
938
+ self.lru_queue.clear()
939
+ self.current_size_bytes = 0
940
+ self.hits = 0
941
+ self.misses = 0
942
+
943
+ def stats(self) -> Dict:
944
+ """Статистика кэша"""
945
+ return {
946
+ "size_bytes": self.current_size_bytes,
947
+ "size_mb": self.current_size_bytes / (1024 * 1024),
948
+ "max_size_mb": self.max_size_bytes / (1024 * 1024),
949
+ "items": len(self.cache),
950
+ "hits": self.hits,
951
+ "misses": self.misses,
952
+ "hit_ratio": self.hits / max(self.hits + self.misses, 1),
953
+ "compression_ratio": self.compression_ratio
954
+ }
955
+
956
+ # ----------------------------------------------------------------------
957
+ # Система смешивания 3D текстур
958
+ # ----------------------------------------------------------------------
959
+
960
+ class VolumeTextureBlender3D:
961
+ """Система смешивания 3D текстур"""
962
+
963
+ def __init__(self):
964
+ self.blend_modes_3d = {
965
+ "add": self._blend_3d_add,
966
+ "multiply": self._blend_3d_multiply,
967
+ "overlay": self._blend_3d_overlay,
968
+ "screen": self._blend_3d_screen,
969
+ "max": self._blend_3d_max,
970
+ "min": self._blend_3d_min,
971
+ "lerp": self._blend_3d_lerp,
972
+ "height_blend": self._blend_3d_height_based,
973
+ }
974
+
975
+ def blend(self,
976
+ volume_a: VolumeTexture3D,
977
+ volume_b: VolumeTexture3D,
978
+ blend_mode: str = "lerp",
979
+ blend_mask: Optional[np.ndarray] = None,
980
+ **kwargs) -> VolumeTexture3D:
981
+ """
982
+ Смешивание двух 3D текстур
983
+
984
+ Args:
985
+ volume_a, volume_b: 3D текстуры для смешивания
986
+ blend_mode: Режим смешивания
987
+ blend_mask: 3D маска смешивания (D, H, W) или (D, H, W, 1)
988
+ **kwargs: Дополнительные параметры
989
+
990
+ Returns:
991
+ Новая смешанная 3D текстура
992
+ """
993
+ # Проверка размеров
994
+ if volume_a.shape != volume_b.shape:
995
+ raise ValueError(f"Volume shapes must match: {volume_a.shape} != {volume_b.shape}")
996
+
997
+ # Проверка маски
998
+ if blend_mask is not None:
999
+ if blend_mask.ndim == 3:
1000
+ blend_mask = blend_mask[..., np.newaxis]
1001
+ if blend_mask.shape[:3] != volume_a.shape[:3]:
1002
+ raise ValueError(f"Mask shape {blend_mask.shape[:3]} doesn't match volume shape {volume_a.shape[:3]}")
1003
+
1004
+ # Выбор режима смешивания
1005
+ if blend_mode not in self.blend_modes_3d:
1006
+ raise ValueError(f"Unknown blend mode: {blend_mode}")
1007
+
1008
+ blend_func = self.blend_modes_3d[blend_mode]
1009
+
1010
+ # Смешивание
1011
+ result_data = blend_func(volume_a.data, volume_b.data, **kwargs)
1012
+
1013
+ # Применение маски если есть
1014
+ if blend_mask is not None:
1015
+ # Линейная интерполяция на основе маски
1016
+ result_data = volume_a.data * (1 - blend_mask) + result_data * blend_mask
1017
+
1018
+ return VolumeTexture3D(
1019
+ data=result_data,
1020
+ format=volume_a.format,
1021
+ voxel_size=volume_a.voxel_size,
1022
+ world_origin=volume_a.world_origin
1023
+ )
1024
+
1025
+ def blend_multiple(self,
1026
+ volumes: List[VolumeTexture3D],
1027
+ blend_modes: List[str],
1028
+ opacities: List[float]) -> VolumeTexture3D:
1029
+ """
1030
+ Последовательное смешивание нескольких 3D текстур
1031
+ """
1032
+ if len(volumes) < 2:
1033
+ return volumes[0] if volumes else None
1034
+
1035
+ result = volumes[0]
1036
+
1037
+ for i in range(1, len(volumes)):
1038
+ # Создаем маску на основе прозрачности
1039
+ opacity = opacities[i-1] if i-1 < len(opacities) else 1.0
1040
+ blend_mode = blend_modes[i-1] if i-1 < len(blend_modes) else "lerp"
1041
+
1042
+ if opacity < 1.0:
1043
+ mask = np.full(volumes[i].shape[:3], opacity, dtype=np.float32)
1044
+ result = self.blend(result, volumes[i], blend_mode, mask)
1045
+ else:
1046
+ result = self.blend(result, volumes[i], blend_mode)
1047
+
1048
+ return result
1049
+
1050
+ def _blend_3d_add(self, a: np.ndarray, b: np.ndarray, **kwargs) -> np.ndarray:
1051
+ """Сложение"""
1052
+ return np.clip(a + b, 0, 1)
1053
+
1054
+ def _blend_3d_multiply(self, a: np.ndarray, b: np.ndarray, **kwargs) -> np.ndarray:
1055
+ """Умножение"""
1056
+ return a * b
1057
+
1058
+ def _blend_3d_overlay(self, a: np.ndarray, b: np.ndarray, **kwargs) -> np.ndarray:
1059
+ """Overlay для 3D"""
1060
+ # Аналогично 2D overlay, но для каждого вокселя
1061
+ result = np.zeros_like(a)
1062
+
1063
+ # Для каждого канала
1064
+ for c in range(a.shape[3]):
1065
+ a_channel = a[..., c]
1066
+ b_channel = b[..., c]
1067
+
1068
+ mask = a_channel < 0.5
1069
+ result_channel = np.zeros_like(a_channel)
1070
+
1071
+ result_channel[mask] = 2 * a_channel[mask] * b_channel[mask]
1072
+ result_channel[~mask] = 1 - 2 * (1 - a_channel[~mask]) * (1 - b_channel[~mask])
1073
+
1074
+ result[..., c] = result_channel
1075
+
1076
+ return result
1077
+
1078
+ def _blend_3d_screen(self, a: np.ndarray, b: np.ndarray, **kwargs) -> np.ndarray:
1079
+ """Screen для 3D"""
1080
+ return 1 - (1 - a) * (1 - b)
1081
+
1082
+ def _blend_3d_max(self, a: np.ndarray, b: np.ndarray, **kwargs) -> np.ndarray:
1083
+ """Максимум"""
1084
+ return np.maximum(a, b)
1085
+
1086
+ def _blend_3d_min(self, a: np.ndarray, b: np.ndarray, **kwargs) -> np.ndarray:
1087
+ """Минимум"""
1088
+ return np.minimum(a, b)
1089
+
1090
+ def _blend_3d_lerp(self, a: np.ndarray, b: np.ndarray, t: float = 0.5, **kwargs) -> np.ndarray:
1091
+ """Линейная интерполяция"""
1092
+ return a * (1 - t) + b * t
1093
+
1094
+ def _blend_3d_height_based(self, a: np.ndarray, b: np.ndarray,
1095
+ height_map: np.ndarray, **kwargs) -> np.ndarray:
1096
+ """Смешивание на основе высоты (по оси Y)"""
1097
+ if height_map.ndim == 3:
1098
+ height_map = height_map[..., np.newaxis]
1099
+
1100
+ # Нормализуем карту высот
1101
+ height_norm = (height_map - height_map.min()) / (height_map.max() - height_map.min())
1102
+
1103
+ # Параметры перехода
1104
+ low_threshold = kwargs.get('low_threshold', 0.3)
1105
+ high_threshold = kwargs.get('high_threshold', 0.7)
1106
+ transition = kwargs.get('transition', 0.1)
1107
+
1108
+ # Создаем маску смешивания
1109
+ blend_mask = np.zeros_like(height_norm)
1110
+
1111
+ # Для каждого вокселя
1112
+ for i in range(a.shape[0]): # По глубине
1113
+ for j in range(a.shape[1]): # По высоте
1114
+ for k in range(a.shape[2]): # По ширине
1115
+ height = height_norm[i, j, k, 0]
1116
+
1117
+ if height <= low_threshold - transition/2:
1118
+ blend_mask[i, j, k, 0] = 0
1119
+ elif height >= high_threshold + transition/2:
1120
+ blend_mask[i, j, k, 0] = 1
1121
+ elif height <= low_threshold + transition/2:
1122
+ t = (height - (low_threshold - transition/2)) / transition
1123
+ blend_mask[i, j, k, 0] = t
1124
+ elif height >= high_threshold - transition/2:
1125
+ t = 1 - (height - (high_threshold - transition/2)) / transition
1126
+ blend_mask[i, j, k, 0] = t
1127
+ else:
1128
+ blend_mask[i, j, k, 0] = 1
1129
+
1130
+ return a * (1 - blend_mask) + b * blend_mask
1131
+
1132
+ # ----------------------------------------------------------------------
1133
+ # Система потоковой генерации 3D текстур (chunk-based)
1134
+ # ----------------------------------------------------------------------
1135
+
1136
+ class VolumeTextureStreamer:
1137
+ """Потоковая генерация больших 3D текстур по чанкам"""
1138
+
1139
+ def __init__(self, generator: VolumeTextureGenerator3D,
1140
+ chunk_size: Tuple[int, int, int] = (32, 32, 32),
1141
+ cache_size_mb: int = 512):
1142
+ self.generator = generator
1143
+ self.chunk_size = chunk_size
1144
+ self.cache = VolumeTextureCache(max_size_mb=cache_size_mb)
1145
+ self.worker_threads = []
1146
+ self.task_queue = PriorityQueue()
1147
+ self.result_queue = Queue()
1148
+ self.running = False
1149
+
1150
+ def start_workers(self, num_workers: int = 4):
1151
+ """Запуск рабочих потоков"""
1152
+ self.running = True
1153
+ for i in range(num_workers):
1154
+ thread = threading.Thread(target=self._worker_loop, daemon=True)
1155
+ thread.start()
1156
+ self.worker_threads.append(thread)
1157
+
1158
+ def stop_workers(self):
1159
+ """Остановка рабочих потоков"""
1160
+ self.running = False
1161
+ for _ in range(len(self.worker_threads)):
1162
+ self.task_queue.put((999, None)) # Sentinel
1163
+
1164
+ for thread in self.worker_threads:
1165
+ thread.join(timeout=5.0)
1166
+ self.worker_threads.clear()
1167
+
1168
+ def request_chunk(self, chunk_coords: Tuple[int, int, int],
1169
+ texture_type: str = "clouds",
1170
+ priority: int = 0,
1171
+ **kwargs) -> Optional[VolumeTexture3D]:
1172
+ """
1173
+ Запрос чанка 3D текстуры
1174
+
1175
+ Args:
1176
+ chunk_coords: Координаты чанка (cx, cy, cz)
1177
+ texture_type: Тип текстуры
1178
+ priority: Приоритет (меньше = выше приоритет)
1179
+ **kwargs: Параметры генерации
1180
+
1181
+ Returns:
1182
+ Чанк текстуры или None если еще не сгенерирован
1183
+ """
1184
+ chunk_key = self._get_chunk_key(chunk_coords, texture_type, kwargs)
1185
+
1186
+ # Проверяем кэш
1187
+ cached = self.cache.get(chunk_key)
1188
+ if cached is not None:
1189
+ return cached
1190
+
1191
+ # Добавляем в очередь задач если не в процессе генерации
1192
+ task = (priority, (chunk_coords, texture_type, kwargs, chunk_key))
1193
+ self.task_queue.put(task)
1194
+
1195
+ return None
1196
+
1197
+ def get_available_chunks(self) -> List[Tuple[int, int, int]]:
1198
+ """Получение списка готовых чанков"""
1199
+ # Проверяем очередь результатов
1200
+ available_chunks = []
1201
+ while not self.result_queue.empty():
1202
+ chunk_coords, texture = self.result_queue.get()
1203
+ available_chunks.append(chunk_coords)
1204
+
1205
+ return available_chunks
1206
+
1207
+ def _worker_loop(self):
1208
+ """Цикл рабочего потока"""
1209
+ while self.running:
1210
+ try:
1211
+ priority, task = self.task_queue.get(timeout=0.1)
1212
+ if task is None: # Sentinel
1213
+ break
1214
+
1215
+ chunk_coords, texture_type, kwargs, chunk_key = task
1216
+
1217
+ # Генерация чанка
1218
+ chunk = self._generate_chunk(chunk_coords, texture_type, **kwargs)
1219
+
1220
+ # Кэширование
1221
+ self.cache.put(chunk_key, chunk)
1222
+
1223
+ # Добавление в очередь результатов
1224
+ self.result_queue.put((chunk_coords, chunk))
1225
+
1226
+ self.task_queue.task_done()
1227
+
1228
+ except Exception as e:
1229
+ warnings.warn(f"Worker thread error: {e}")
1230
+
1231
+ def _generate_chunk(self, chunk_coords: Tuple[int, int, int],
1232
+ texture_type: str, **kwargs) -> VolumeTexture3D:
1233
+ """Генерация одного чанка"""
1234
+ cx, cy, cz = chunk_coords
1235
+ chunk_w, chunk_h, chunk_d = self.chunk_size
1236
+
1237
+ # Вычисляем мировые координаты чанка
1238
+ world_x = cx * chunk_w
1239
+ world_y = cy * chunk_h
1240
+ world_z = cz * chunk_d
1241
+
1242
+ # Вызываем соответствующий генератор
1243
+ gen_method = getattr(self.generator, f"generate_{texture_type}_3d", None)
1244
+ if gen_method is None:
1245
+ # По умолчанию облака
1246
+ gen_method = self.generator.generate_clouds_3d
1247
+
1248
+ # Генерируем чанк
1249
+ chunk = gen_method(
1250
+ width=chunk_w,
1251
+ height=chunk_h,
1252
+ depth=chunk_d,
1253
+ **kwargs
1254
+ )
1255
+
1256
+ # Обновляем мировые координаты
1257
+ chunk.world_origin = (world_x, world_y, world_z)
1258
+
1259
+ return chunk
1260
+
1261
+ def _get_chunk_key(self, chunk_coords: Tuple[int, int, int],
1262
+ texture_type: str, params: Dict) -> str:
1263
+ """Создание уникального ключа для чанка"""
1264
+ # Хешируем параметры для создания ключа
1265
+ params_str = str(sorted(params.items()))
1266
+ hash_obj = hashlib.md5(f"{chunk_coords}_{texture_type}_{params_str}".encode())
1267
+ return hash_obj.hexdigest()
1268
+
1269
+ # ----------------------------------------------------------------------
1270
+ # Рендерер для визуализации 3D текстур
1271
+ # ----------------------------------------------------------------------
1272
+
1273
+ class VolumeTextureRenderer:
1274
+ """Простой рейкастинг-рендерер для 3D текстур"""
1275
+
1276
+ def __init__(self,
1277
+ volume: VolumeTexture3D,
1278
+ light_direction: Tuple[float, float, float] = (0.5, 1.0, 0.5)):
1279
+ self.volume = volume
1280
+ self.light_direction = np.array(light_direction, dtype=np.float32)
1281
+ self.light_direction = self.light_direction / np.linalg.norm(self.light_direction)
1282
+ self.transfer_function = self._default_transfer_function()
1283
+
1284
+ def _default_transfer_function(self) -> Callable[[np.ndarray], np.ndarray]:
1285
+ """Функция передачи по умолчанию (значение плотности -> цвет)"""
1286
+ def transfer(density: np.ndarray) -> np.ndarray:
1287
+ # Простая функция: плотность -> оттенок серого
1288
+ color = np.zeros((*density.shape, 4), dtype=np.float32)
1289
+ color[..., 0] = density # R
1290
+ color[..., 1] = density # G
1291
+ color[..., 2] = density # B
1292
+ color[..., 3] = density # A
1293
+ return color
1294
+
1295
+ return transfer
1296
+
1297
+ def render_slice(self, axis: str = 'z', index: int = 0) -> np.ndarray:
1298
+ """Рендеринг 2D среза из 3D текстуры"""
1299
+ slice_data = self.volume.get_slice(axis, index)
1300
+
1301
+ # Если нужно, применяем функцию передачи
1302
+ if self.volume.format in [VolumeFormat.GRAYSCALE, VolumeFormat.DENSITY]:
1303
+ # Одноканальные данные -> RGB
1304
+ if slice_data.shape[-1] == 1:
1305
+ slice_rgb = np.zeros((*slice_data.shape[:2], 4), dtype=np.float32)
1306
+ slice_rgb[..., 0] = slice_data[..., 0] # R
1307
+ slice_rgb[..., 1] = slice_data[..., 0] # G
1308
+ slice_rgb[..., 2] = slice_data[..., 0] # B
1309
+ slice_rgb[..., 3] = 1.0 # A
1310
+ return slice_rgb
1311
+
1312
+ return slice_data
1313
+
1314
+ def render_raycast(self,
1315
+ camera_pos: Tuple[float, float, float] = (0.5, 0.5, 2.0),
1316
+ camera_target: Tuple[float, float, float] = (0.5, 0.5, 0.5),
1317
+ image_size: Tuple[int, int] = (256, 256),
1318
+ max_steps: int = 256,
1319
+ step_size: float = 0.005) -> np.ndarray:
1320
+ """
1321
+ Простой рейкастинг для визуализации объема
1322
+
1323
+ Args:
1324
+ camera_pos: Позиция камеры
1325
+ camera_target: Цель камеры
1326
+ image_size: Размер выходного изображения
1327
+ max_steps: Максимальное количество шагов луча
1328
+ step_size: Размер шага
1329
+
1330
+ Returns:
1331
+ 2D изображение (H, W, 4) RGBA
1332
+ """
1333
+ print(f"Raycasting volume {self.volume.shape}...")
1334
+
1335
+ width, height = image_size
1336
+ image = np.zeros((height, width, 4), dtype=np.float32)
1337
+
1338
+ # Вычисляем направление взгляда
1339
+ camera_dir = np.array(camera_target) - np.array(camera_pos)
1340
+ camera_dir = camera_dir / np.linalg.norm(camera_dir)
1341
+
1342
+ # Вычисляем базис камеры
1343
+ up = np.array([0.0, 1.0, 0.0])
1344
+ right = np.cross(camera_dir, up)
1345
+ right = right / np.linalg.norm(right)
1346
+ up = np.cross(right, camera_dir)
1347
+
1348
+ # FOV
1349
+ fov = 60.0
1350
+ aspect = width / height
1351
+ half_height = np.tan(np.radians(fov) / 2.0)
1352
+ half_width = aspect * half_height
1353
+
1354
+ # Для каждого пикселя
1355
+ for y in range(height):
1356
+ for x in range(width):
1357
+ # Вычисляем направление луча
1358
+ u = (2.0 * x / width - 1.0) * half_width
1359
+ v = (1.0 - 2.0 * y / height) * half_height
1360
+
1361
+ ray_dir = camera_dir + u * right + v * up
1362
+ ray_dir = ray_dir / np.linalg.norm(ray_dir)
1363
+
1364
+ # Стартовая позиция
1365
+ ray_pos = np.array(camera_pos, dtype=np.float32)
1366
+
1367
+ # Интегрирование вдоль луча
1368
+ color = np.zeros(4, dtype=np.float32)
1369
+
1370
+ for step in range(max_steps):
1371
+ # Проверяем границы объема
1372
+ if (ray_pos[0] < 0 or ray_pos[0] >= 1 or
1373
+ ray_pos[1] < 0 or ray_pos[1] >= 1 or
1374
+ ray_pos[2] < 0 or ray_pos[2] >= 1):
1375
+ break
1376
+
1377
+ # Выборка из объема
1378
+ sample = self.volume.sample(ray_pos[0], ray_pos[1], ray_pos[2])
1379
+
1380
+ # Фронтально-заднее смешивание
1381
+ alpha = sample[3] if len(sample) >= 4 else sample[0]
1382
+ color = color + (1.0 - color[3]) * alpha * np.append(sample[:3], alpha)
1383
+
1384
+ # Если полностью непрозрачный, останавливаемся
1385
+ if color[3] >= 0.99:
1386
+ break
1387
+
1388
+ # Двигаем луч
1389
+ ray_pos += ray_dir * step_size
1390
+
1391
+ image[y, x] = color
1392
+
1393
+ return np.clip(image, 0, 1)
1394
+
1395
+ def render_mip(self, axis: str = 'z') -> np.ndarray:
1396
+ """
1397
+ Рендеринг MIP (Maximum Intensity Projection) - используется в медицине
1398
+
1399
+ Args:
1400
+ axis: Ось проекции ('x', 'y', или 'z')
1401
+
1402
+ Returns:
1403
+ 2D изображение MIP
1404
+ """
1405
+ volume_data = self.volume.data
1406
+
1407
+ if axis == 'x':
1408
+ # Проекция вдоль оси X
1409
+ mip = np.max(volume_data, axis=2)
1410
+ elif axis == 'y':
1411
+ # Проекция вдоль оси Y
1412
+ mip = np.max(volume_data, axis=1)
1413
+ elif axis == 'z':
1414
+ # Проекция вдоль оси Z
1415
+ mip = np.max(volume_data, axis=0)
1416
+ else:
1417
+ raise ValueError(f"Invalid axis: {axis}")
1418
+
1419
+ # Если одноканальный, конвертируем в RGB
1420
+ if mip.ndim == 2 or mip.shape[-1] == 1:
1421
+ mip_rgb = np.zeros((*mip.shape[:2], 4), dtype=np.float32)
1422
+ if mip.ndim == 3:
1423
+ mip_rgb[..., 0] = mip[..., 0]
1424
+ mip_rgb[..., 1] = mip[..., 0]
1425
+ mip_rgb[..., 2] = mip[..., 0]
1426
+ else:
1427
+ mip_rgb[..., 0] = mip
1428
+ mip_rgb[..., 1] = mip
1429
+ mip_rgb[..., 2] = mip
1430
+ mip_rgb[..., 3] = 1.0
1431
+ return mip_rgb
1432
+
1433
+ return mip
1434
+
1435
+ # ----------------------------------------------------------------------
1436
+ # Примеры использования
1437
+ # ----------------------------------------------------------------------
1438
+
1439
+ def example_3d_clouds():
1440
+ """Пример создания и визуализации 3D облаков"""
1441
+
1442
+ print("Generating 3D cloud texture example...")
1443
+
1444
+ # Создаем генератор
1445
+ generator = VolumeTextureGenerator3D(seed=42)
1446
+
1447
+ # Генерируем 3D облака
1448
+ clouds_3d = generator.generate_clouds_3d(
1449
+ width=64, height=64, depth=64,
1450
+ scale=0.05, density=0.4, detail=3
1451
+ )
1452
+
1453
+ print(f"3D Cloud texture shape: {clouds_3d.shape}")
1454
+ print(f"Size: {clouds_3d.size_bytes / (1024*1024):.2f} MB")
1455
+
1456
+ # Рендерим срезы
1457
+ renderer = VolumeTextureRenderer(clouds_3d)
1458
+
1459
+ # Срез по оси Z
1460
+ slice_z = renderer.render_slice('z', 32)
1461
+ print(f"Slice shape: {slice_z.shape}")
1462
+
1463
+ # MIP проекция
1464
+ mip = renderer.render_mip('z')
1465
+ print(f"MIP shape: {mip.shape}")
1466
+
1467
+ return clouds_3d, slice_z, mip
1468
+
1469
+ def example_3d_marble():
1470
+ """Пример создания 3D мраморной текстуры"""
1471
+
1472
+ print("\nGenerating 3D marble texture example...")
1473
+
1474
+ generator = VolumeTextureGenerator3D(seed=123)
1475
+
1476
+ marble_3d = generator.generate_marble_3d(
1477
+ width=48, height=48, depth=48,
1478
+ scale=0.03, vein_strength=0.7, vein_frequency=8.0
1479
+ )
1480
+
1481
+ print(f"3D Marble texture shape: {marble_3d.shape}")
1482
+
1483
+ # Создаем еще один слой для смешивания
1484
+ clouds_3d = generator.generate_clouds_3d(
1485
+ width=48, height=48, depth=48,
1486
+ scale=0.02, density=0.3, detail=2
1487
+ )
1488
+
1489
+ # Смешиваем текстуры
1490
+ blender = VolumeTextureBlender3D()
1491
+
1492
+ # Создаем маску смешивания (градиент по высоте)
1493
+ height, width, depth = marble_3d.shape[:3]
1494
+ y_coords = np.linspace(0, 1, height)
1495
+ mask_3d = np.zeros((depth, height, width, 1), dtype=np.float32)
1496
+
1497
+ for i in range(depth):
1498
+ for j in range(height):
1499
+ mask_3d[i, j, :, 0] = y_coords[j] # Градиент по Y
1500
+
1501
+ blended = blender.blend(
1502
+ marble_3d, clouds_3d,
1503
+ blend_mode="lerp",
1504
+ blend_mask=mask_3d
1505
+ )
1506
+
1507
+ print(f"Blended texture shape: {blended.shape}")
1508
+
1509
+ return marble_3d, blended
1510
+
1511
+ def example_streaming_3d_textures():
1512
+ """Пример потоковой генерации больших 3D текстур"""
1513
+
1514
+ print("\nStreaming 3D texture generation example...")
1515
+
1516
+ generator = VolumeTextureGenerator3D(seed=42)
1517
+ streamer = VolumeTextureStreamer(
1518
+ generator=generator,
1519
+ chunk_size=(16, 16, 16),
1520
+ cache_size_mb=256
1521
+ )
1522
+
1523
+ # Запускаем рабочие потоки
1524
+ streamer.start_workers(num_workers=2)
1525
+
1526
+ # Запрашиваем несколько чанков
1527
+ chunks = []
1528
+ for cz in range(2):
1529
+ for cy in range(2):
1530
+ for cx in range(2):
1531
+ chunk = streamer.request_chunk(
1532
+ chunk_coords=(cx, cy, cz),
1533
+ texture_type="clouds",
1534
+ priority=cx + cy + cz,
1535
+ scale=0.1, density=0.5
1536
+ )
1537
+ if chunk is not None:
1538
+ chunks.append(((cx, cy, cz), chunk))
1539
+
1540
+ # Ждем генерации
1541
+ time.sleep(2.0)
1542
+
1543
+ # Проверяем готовые чанки
1544
+ available = streamer.get_available_chunks()
1545
+ print(f"Available chunks: {len(available)}")
1546
+
1547
+ # Останавливаем workers
1548
+ streamer.stop_workers()
1549
+
1550
+ # Статистика кэша
1551
+ stats = streamer.cache.stats()
1552
+ print(f"Cache stats: {stats}")
1553
+
1554
+ return streamer
1555
+
1556
+ if __name__ == "__main__":
1557
+ print("3D Texture System")
1558
+ print("=" * 60)
1559
+
1560
+ # Пример 1: 3D облака
1561
+ clouds_3d, cloud_slice, cloud_mip = example_3d_clouds()
1562
+
1563
+ # Пример 2: 3D мрамор и смешивание
1564
+ marble_3d, blended_3d = example_3d_marble()
1565
+
1566
+ # Пример 3: Потоковая генерация
1567
+ streamer = example_streaming_3d_textures()
1568
+
1569
+ print("\n" + "=" * 60)
1570
+ print("3D Texture System Features:")
1571
+ print("-" * 40)
1572
+ print("1. Multiple 3D texture types: clouds, marble, wood, lava, perlin noise")
1573
+ print("2. Volume texture cache with compression")
1574
+ print("3. 3D texture blending with multiple blend modes")
1575
+ print("4. Streaming generation for large volumes")
1576
+ print("5. Raycasting and MIP rendering")
1577
+ print("6. Trilinear interpolation for smooth sampling")
1578
+
1579
+ print("\nPerformance tips:")
1580
+ print("- Use smaller volumes for real-time applications (64^3 or less)")
1581
+ print("- Enable compression for texture cache")
1582
+ print("- Use streaming for large worlds")
1583
+ print("- Consider GPU acceleration for raycasting")
1584
+
1585
+ print("\n3D texture system ready for volumetric rendering!")