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/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!")
|