fractex 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1935 @@
1
+ # fractex/dynamic_textures_3d.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, complex128
10
+ import warnings
11
+ import math
12
+ import time
13
+ from dataclasses import dataclass, field
14
+ from enum import Enum
15
+ from collections import deque
16
+ import hashlib
17
+
18
+ # ----------------------------------------------------------------------
19
+ # Структуры данных для динамических текстур
20
+ # ----------------------------------------------------------------------
21
+
22
+ class DynamicTextureType(Enum):
23
+ """Типы динамических 3D текстур"""
24
+ LAVA_FLOW = 1 # Лавовые потоки
25
+ WATER_FLOW = 2 # Течение воды
26
+ SMOKE_PLUME = 3 # Дымовые шлейфы
27
+ FIRE = 4 # Огонь и пламя
28
+ CLOUD_DRIFT = 5 # Дрейф облаков
29
+ SEDIMENT = 6 # Осаждение/эрозия
30
+ DEFORMATION = 7 # Деформации материала
31
+ CHEMICAL_REACTION = 8 # Химические реакции
32
+ BIOLUMINESCENCE = 9 # Биолюминесценция
33
+ MAGMA_CHAMBER = 10 # Движение магмы
34
+
35
+ @dataclass
36
+ class DynamicTextureState:
37
+ """Состояние динамической текстуры в момент времени"""
38
+ time: float = 0.0
39
+ data: np.ndarray = None # (D, H, W, C)
40
+ velocity_field: Optional[np.ndarray] = None # Поле скоростей (D, H, W, 3)
41
+ temperature_field: Optional[np.ndarray] = None # Температурное поле
42
+ pressure_field: Optional[np.ndarray] = None # Поле давления
43
+ divergence_field: Optional[np.ndarray] = None # Дивергенция
44
+
45
+ def __post_init__(self):
46
+ if self.data is None:
47
+ raise ValueError("Data must be provided")
48
+
49
+ @property
50
+ def shape(self) -> Tuple[int, int, int, int]:
51
+ return self.data.shape
52
+
53
+ @property
54
+ def dimensions(self) -> Tuple[int, int, int]:
55
+ return self.shape[:3]
56
+
57
+ @dataclass
58
+ class PhysicsParameters:
59
+ """Физические параметры для симуляции"""
60
+ # Общие параметры
61
+ density: float = 1.0 # Плотность
62
+ viscosity: float = 0.1 # Вязкость
63
+ diffusion_rate: float = 0.01 # Коэффициент диффузии
64
+ time_step: float = 0.01 # Шаг по времени
65
+ gravity: Tuple[float, float, float] = (0.0, -9.8, 0.0)
66
+
67
+ # Температурные параметры
68
+ thermal_conductivity: float = 0.01
69
+ specific_heat: float = 1.0
70
+ temperature_decay: float = 0.99
71
+
72
+ # Параметры для конкретных типов
73
+ lava_viscosity: float = 100.0
74
+ water_viscosity: float = 0.001
75
+ smoke_buoyancy: float = 2.0
76
+ fire_temperature: float = 1000.0
77
+
78
+ # Пределы стабильности
79
+ max_velocity: float = 10.0
80
+ max_temperature: float = 2000.0
81
+ max_pressure: float = 100.0
82
+
83
+ def __post_init__(self):
84
+ self.gravity = np.array(self.gravity, dtype=np.float32)
85
+
86
+ # ----------------------------------------------------------------------
87
+ # Решатели физических уравнений (оптимизированные с Numba)
88
+ # ----------------------------------------------------------------------
89
+
90
+ class NavierStokesSolver3D:
91
+ """Решатель уравнений Навье-Стокса для жидкостей и газов"""
92
+
93
+ def __init__(self, dimensions: Tuple[int, int, int], params: PhysicsParameters):
94
+ self.dimensions = dimensions
95
+ self.params = params
96
+
97
+ # Поля
98
+ self.velocity = np.zeros((*dimensions, 3), dtype=np.float32) # (D, H, W, 3)
99
+ self.velocity_prev = np.zeros_like(self.velocity)
100
+ self.pressure = np.zeros(dimensions, dtype=np.float32)
101
+ self.divergence = np.zeros(dimensions, dtype=np.float32)
102
+
103
+ # Временные поля
104
+ self.temp_field = np.zeros(dimensions, dtype=np.float32)
105
+
106
+ # Предварительные вычисления
107
+ self._precompute_laplacian_kernel()
108
+
109
+ def _precompute_laplacian_kernel(self):
110
+ """Предварительное вычисление ядра лапласиана"""
111
+ self.laplacian_kernel = np.array([
112
+ [[0, 1, 0],
113
+ [1, -6, 1],
114
+ [0, 1, 0]],
115
+
116
+ [[1, 1, 1],
117
+ [1, -6, 1],
118
+ [1, 1, 1]],
119
+
120
+ [[0, 1, 0],
121
+ [1, -6, 1],
122
+ [0, 1, 0]]
123
+ ], dtype=np.float32) / 26.0
124
+
125
+ def step(self,
126
+ external_forces: Optional[np.ndarray] = None,
127
+ obstacles: Optional[np.ndarray] = None) -> np.ndarray:
128
+ """
129
+ Один шаг симуляции
130
+
131
+ Алгоритм:
132
+ 1. Добавление внешних сил
133
+ 2. Адвекция скорости
134
+ 3. Диффузия вязкости
135
+ 4. Проецирование (соленоидальность)
136
+ """
137
+ # 1. Добавление внешних сил (гравитация и т.д.)
138
+ self._add_external_forces(external_forces)
139
+
140
+ # 2. Адвекция скорости (перенос скорости полем скорости)
141
+ self._advect_velocity()
142
+
143
+ # 3. Диффузия вязкости
144
+ self._diffuse_viscosity()
145
+
146
+ # 4. Проецирование для обеспечения соленоидальности (div(u) = 0)
147
+ self._project()
148
+
149
+ # 5. Обработка препятствий
150
+ if obstacles is not None:
151
+ self._apply_obstacles(obstacles)
152
+
153
+ # 6. Стабилизация
154
+ self.velocity = np.nan_to_num(self.velocity, nan=0.0, posinf=0.0, neginf=0.0)
155
+ max_vel = self.params.max_velocity
156
+ if max_vel > 0:
157
+ self.velocity = np.clip(self.velocity, -max_vel, max_vel)
158
+
159
+ return self.velocity.copy()
160
+
161
+ def _add_external_forces(self, forces: Optional[np.ndarray]):
162
+ """Добавление внешних сил (гравитация, ветер, etc.)"""
163
+ if forces is not None:
164
+ self.velocity += forces * self.params.time_step
165
+ else:
166
+ # Добавляем гравитацию по умолчанию
167
+ for i in range(self.dimensions[0]):
168
+ for j in range(self.dimensions[1]):
169
+ for k in range(self.dimensions[2]):
170
+ self.velocity[i, j, k] += self.params.gravity * self.params.time_step
171
+
172
+ def _advect_velocity(self):
173
+ """Адвекция скорости (полулагранжевым методом)"""
174
+ dim_z, dim_y, dim_x = self.dimensions
175
+ self.velocity_prev = self.velocity.copy()
176
+ vel_new = np.zeros_like(self.velocity)
177
+
178
+ for i in prange(dim_z):
179
+ for j in range(dim_y):
180
+ for k in range(dim_x):
181
+ # Текущая скорость
182
+ vx, vy, vz = self.velocity[i, j, k]
183
+
184
+ # Координата предыдущего шага (обратное течение)
185
+ prev_i = i - vz * self.params.time_step * dim_z
186
+ prev_j = j - vy * self.params.time_step * dim_y
187
+ prev_k = k - vx * self.params.time_step * dim_x
188
+
189
+ # Обеспечиваем граничные условия (повторение)
190
+ prev_i = prev_i % dim_z
191
+ prev_j = prev_j % dim_y
192
+ prev_k = prev_k % dim_x
193
+
194
+ # Трилинейная интерполяция
195
+ i0 = int(np.floor(prev_i)) % dim_z
196
+ j0 = int(np.floor(prev_j)) % dim_y
197
+ k0 = int(np.floor(prev_k)) % dim_x
198
+
199
+ i1 = (i0 + 1) % dim_z
200
+ j1 = (j0 + 1) % dim_y
201
+ k1 = (k0 + 1) % dim_x
202
+
203
+ di = prev_i - i0
204
+ dj = prev_j - j0
205
+ dk = prev_k - k0
206
+
207
+ # Интерполяция по всем трем компонентам скорости
208
+ for comp in range(3):
209
+ c000 = self.velocity_prev[i0, j0, k0, comp]
210
+ c001 = self.velocity_prev[i0, j0, k1, comp]
211
+ c010 = self.velocity_prev[i0, j1, k0, comp]
212
+ c011 = self.velocity_prev[i0, j1, k1, comp]
213
+ c100 = self.velocity_prev[i1, j0, k0, comp]
214
+ c101 = self.velocity_prev[i1, j0, k1, comp]
215
+ c110 = self.velocity_prev[i1, j1, k0, comp]
216
+ c111 = self.velocity_prev[i1, j1, k1, comp]
217
+
218
+ c00 = c000 * (1 - dk) + c001 * dk
219
+ c01 = c010 * (1 - dk) + c011 * dk
220
+ c10 = c100 * (1 - dk) + c101 * dk
221
+ c11 = c110 * (1 - dk) + c111 * dk
222
+
223
+ c0 = c00 * (1 - dj) + c01 * dj
224
+ c1 = c10 * (1 - dj) + c11 * dj
225
+
226
+ vel_new[i, j, k, comp] = c0 * (1 - di) + c1 * di
227
+
228
+ self.velocity = vel_new
229
+
230
+ def _diffuse_viscosity(self):
231
+ """Диффузия вязкости (явная схема)"""
232
+ if self.params.viscosity <= 0:
233
+ return
234
+
235
+ dim_z, dim_y, dim_x = self.dimensions
236
+ dt = self.params.time_step
237
+ viscosity = self.params.viscosity
238
+ alpha = dt * viscosity
239
+
240
+ vel_new = np.zeros_like(self.velocity)
241
+
242
+ for comp in range(3):
243
+ for i in prange(dim_z):
244
+ for j in range(dim_y):
245
+ for k in range(dim_x):
246
+ # 7-точечный шаблон лапласиана
247
+ center = self.velocity[i, j, k, comp]
248
+
249
+ # Соседи
250
+ left = self.velocity[i, j, (k-1)%dim_x, comp]
251
+ right = self.velocity[i, j, (k+1)%dim_x, comp]
252
+ down = self.velocity[i, (j-1)%dim_y, k, comp]
253
+ up = self.velocity[i, (j+1)%dim_y, k, comp]
254
+ back = self.velocity[(i-1)%dim_z, j, k, comp]
255
+ front = self.velocity[(i+1)%dim_z, j, k, comp]
256
+
257
+ # Лапласиан
258
+ laplacian = (left + right + down + up + back + front - 6 * center)
259
+
260
+ # Обновление
261
+ vel_new[i, j, k, comp] = center + alpha * laplacian
262
+
263
+ self.velocity = vel_new
264
+
265
+ def _project(self):
266
+ """Проецирование для обеспечения соленоидальности"""
267
+ # Вычисление дивергенции
268
+ self._compute_divergence()
269
+
270
+ # Решение уравнения Пуассона для давления
271
+ self._solve_pressure_poisson()
272
+
273
+ # Вычитание градиента давления
274
+ self._subtract_pressure_gradient()
275
+
276
+ def _compute_divergence(self):
277
+ """Вычисление дивергенции поля скоростей"""
278
+ dim_z, dim_y, dim_x = self.dimensions
279
+
280
+ for i in prange(dim_z):
281
+ for j in range(dim_y):
282
+ for k in range(dim_x):
283
+ # Градиенты скорости
284
+ du_dx = (self.velocity[i, j, (k+1)%dim_x, 0] -
285
+ self.velocity[i, j, (k-1)%dim_x, 0]) / 2.0
286
+ dv_dy = (self.velocity[i, (j+1)%dim_y, k, 1] -
287
+ self.velocity[i, (j-1)%dim_y, k, 1]) / 2.0
288
+ dw_dz = (self.velocity[(i+1)%dim_z, j, k, 2] -
289
+ self.velocity[(i-1)%dim_z, j, k, 2]) / 2.0
290
+
291
+ self.divergence[i, j, k] = du_dx + dv_dy + dw_dz
292
+
293
+ def _solve_pressure_poisson(self, iterations: int = 20):
294
+ """Решение уравнения Пуассона методом Якоби"""
295
+ dim_z, dim_y, dim_x = self.dimensions
296
+ pressure_new = np.zeros_like(self.pressure)
297
+
298
+ for _ in range(iterations):
299
+ for i in prange(dim_z):
300
+ for j in range(dim_y):
301
+ for k in range(dim_x):
302
+ # Соседи давления
303
+ p_left = self.pressure[i, j, (k-1)%dim_x]
304
+ p_right = self.pressure[i, j, (k+1)%dim_x]
305
+ p_down = self.pressure[i, (j-1)%dim_y, k]
306
+ p_up = self.pressure[i, (j+1)%dim_y, k]
307
+ p_back = self.pressure[(i-1)%dim_z, j, k]
308
+ p_front = self.pressure[(i+1)%dim_z, j, k]
309
+
310
+ # Новое значение давления
311
+ pressure_new[i, j, k] = (p_left + p_right + p_down +
312
+ p_up + p_back + p_front -
313
+ self.divergence[i, j, k]) / 6.0
314
+
315
+ self.pressure = pressure_new.copy()
316
+
317
+ def _subtract_pressure_gradient(self):
318
+ """Вычитание градиента давления из скорости"""
319
+ dim_z, dim_y, dim_x = self.dimensions
320
+
321
+ for i in prange(dim_z):
322
+ for j in range(dim_y):
323
+ for k in range(dim_x):
324
+ # Градиенты давления
325
+ dp_dx = (self.pressure[i, j, (k+1)%dim_x] -
326
+ self.pressure[i, j, (k-1)%dim_x]) / 2.0
327
+ dp_dy = (self.pressure[i, (j+1)%dim_y, k] -
328
+ self.pressure[i, (j-1)%dim_y, k]) / 2.0
329
+ dp_dz = (self.pressure[(i+1)%dim_z, j, k] -
330
+ self.pressure[(i-1)%dim_z, j, k]) / 2.0
331
+
332
+ # Вычитаем градиент давления
333
+ self.velocity[i, j, k, 0] -= dp_dx
334
+ self.velocity[i, j, k, 1] -= dp_dy
335
+ self.velocity[i, j, k, 2] -= dp_dz
336
+
337
+ def _apply_obstacles(self, obstacles: np.ndarray):
338
+ """Применение граничных условий на препятствиях"""
339
+ dim_z, dim_y, dim_x = self.dimensions
340
+
341
+ for i in range(dim_z):
342
+ for j in range(dim_y):
343
+ for k in range(dim_x):
344
+ if obstacles[i, j, k] > 0.5:
345
+ # Обнуляем скорость в препятствиях
346
+ self.velocity[i, j, k] = 0.0
347
+
348
+ # Отражение скорости от соседей
349
+ if k > 0 and obstacles[i, j, k-1] < 0.5:
350
+ self.velocity[i, j, k-1, 0] = 0
351
+ if k < dim_x-1 and obstacles[i, j, k+1] < 0.5:
352
+ self.velocity[i, j, k+1, 0] = 0
353
+ if j > 0 and obstacles[i, j-1, k] < 0.5:
354
+ self.velocity[i, j-1, k, 1] = 0
355
+ if j < dim_y-1 and obstacles[i, j+1, k] < 0.5:
356
+ self.velocity[i, j+1, k, 1] = 0
357
+ if i > 0 and obstacles[i-1, j, k] < 0.5:
358
+ self.velocity[i-1, j, k, 2] = 0
359
+ if i < dim_z-1 and obstacles[i+1, j, k] < 0.5:
360
+ self.velocity[i+1, j, k, 2] = 0
361
+
362
+ # ----------------------------------------------------------------------
363
+ # Генераторы динамических текстур
364
+ # ----------------------------------------------------------------------
365
+
366
+ class DynamicTextureGenerator3D:
367
+ """Генератор динамических 3D текстур с физическим моделированием"""
368
+
369
+ def __init__(self,
370
+ dimensions: Tuple[int, int, int],
371
+ texture_type: DynamicTextureType,
372
+ physics_params: Optional[PhysicsParameters] = None,
373
+ seed: int = 42):
374
+
375
+ self.dimensions = dimensions
376
+ self.texture_type = texture_type
377
+ self.seed = seed
378
+ np.random.seed(seed)
379
+
380
+ # Параметры физики
381
+ if physics_params is None:
382
+ self.params = self._get_default_params(texture_type)
383
+ else:
384
+ self.params = physics_params
385
+
386
+ # Инициализация симуляторов
387
+ self.navier_stokes = NavierStokesSolver3D(dimensions, self.params)
388
+ self._init_fields()
389
+
390
+ # История состояний для оптимизации
391
+ self.state_history = deque(maxlen=10)
392
+ self.time = 0.0
393
+
394
+ # Кэш для оптимизации
395
+ self.cache = {}
396
+
397
+ def _get_default_params(self, texture_type: DynamicTextureType) -> PhysicsParameters:
398
+ """Получение параметров по умолчанию для типа текстуры"""
399
+ if texture_type == DynamicTextureType.LAVA_FLOW:
400
+ return PhysicsParameters(
401
+ density=2.8,
402
+ viscosity=100.0, # Высокая вязкость лавы
403
+ diffusion_rate=0.005,
404
+ time_step=0.005,
405
+ gravity=(0.0, -9.8, 0.0),
406
+ thermal_conductivity=0.02,
407
+ temperature_decay=0.995
408
+ )
409
+ elif texture_type == DynamicTextureType.WATER_FLOW:
410
+ return PhysicsParameters(
411
+ density=1.0,
412
+ viscosity=0.001, # Низкая вязкость воды
413
+ diffusion_rate=0.01,
414
+ time_step=0.01,
415
+ gravity=(0.0, -9.8, 0.0),
416
+ thermal_conductivity=0.001,
417
+ temperature_decay=0.99
418
+ )
419
+ elif texture_type == DynamicTextureType.SMOKE_PLUME:
420
+ return PhysicsParameters(
421
+ density=0.3, # Легкий дым
422
+ viscosity=0.01,
423
+ diffusion_rate=0.05,
424
+ time_step=0.02,
425
+ gravity=(0.0, 2.0, 0.0), # Подъемная сила
426
+ thermal_conductivity=0.1,
427
+ temperature_decay=0.95
428
+ )
429
+ elif texture_type == DynamicTextureType.FIRE:
430
+ return PhysicsParameters(
431
+ density=0.5,
432
+ viscosity=0.005,
433
+ diffusion_rate=0.1,
434
+ time_step=0.015,
435
+ gravity=(0.0, 1.5, 0.0), # Пламя поднимается
436
+ thermal_conductivity=0.2,
437
+ temperature_decay=0.9,
438
+ fire_temperature=1000.0
439
+ )
440
+ elif texture_type == DynamicTextureType.CLOUD_DRIFT:
441
+ return PhysicsParameters(
442
+ density=0.8,
443
+ viscosity=0.02,
444
+ diffusion_rate=0.02,
445
+ time_step=0.01,
446
+ gravity=(0.0, -0.5, 0.0), # Слабая гравитация
447
+ thermal_conductivity=0.01,
448
+ temperature_decay=0.98
449
+ )
450
+ else:
451
+ return PhysicsParameters()
452
+
453
+ def _init_fields(self):
454
+ """Инициализация полей в зависимости от типа текстуры"""
455
+ dim_z, dim_y, dim_x = self.dimensions
456
+
457
+ # Поле плотности/температуры
458
+ self.density_field = np.zeros(self.dimensions, dtype=np.float32)
459
+
460
+ # Поле цвета
461
+ self.color_field = np.zeros((*self.dimensions, 4), dtype=np.float32)
462
+
463
+ # Дополнительные поля в зависимости от типа
464
+ if self.texture_type == DynamicTextureType.LAVA_FLOW:
465
+ self._init_lava_fields()
466
+ elif self.texture_type == DynamicTextureType.WATER_FLOW:
467
+ self._init_water_fields()
468
+ elif self.texture_type == DynamicTextureType.SMOKE_PLUME:
469
+ self._init_smoke_fields()
470
+ elif self.texture_type == DynamicTextureType.FIRE:
471
+ self._init_fire_fields()
472
+ elif self.texture_type == DynamicTextureType.CLOUD_DRIFT:
473
+ self._init_cloud_fields()
474
+
475
+ # Инициализация препятствий
476
+ self.obstacles = self._generate_obstacles()
477
+
478
+ def _init_lava_fields(self):
479
+ """Инициализация полей для лавовых потоков"""
480
+ dim_z, dim_y, dim_x = self.dimensions
481
+
482
+ # Создаем источник лавы (горячая точка)
483
+ source_center = (dim_z//2, dim_y//4, dim_x//2)
484
+ source_radius = min(dim_x, dim_z) // 4
485
+
486
+ # Температурное поле
487
+ self.temperature_field = np.zeros(self.dimensions, dtype=np.float32)
488
+
489
+ for i in range(dim_z):
490
+ for j in range(dim_y):
491
+ for k in range(dim_x):
492
+ # Расстояние до центра источника
493
+ dz = i - source_center[0]
494
+ dy = j - source_center[1]
495
+ dx = k - source_center[2]
496
+ dist = np.sqrt(dx*dx + dy*dy + dz*dz)
497
+
498
+ if dist < source_radius:
499
+ # Горячая лава в источнике
500
+ temperature = 1200.0 * (1.0 - dist / source_radius)
501
+ self.temperature_field[i, j, k] = temperature
502
+ self.density_field[i, j, k] = 0.8
503
+
504
+ # Начальная скорость (вытекание из источника)
505
+ if dist > 0:
506
+ self.navier_stokes.velocity[i, j, k, 0] = dx / dist * 0.5
507
+ self.navier_stokes.velocity[i, j, k, 2] = dz / dist * 0.5
508
+
509
+ # Цветовое поле (лава)
510
+ self._update_lava_color()
511
+
512
+ def _init_water_fields(self):
513
+ """Инициализация полей для течения воды"""
514
+ dim_z, dim_y, dim_x = self.dimensions
515
+
516
+ # Водный резервуар (верхняя часть)
517
+ water_level = dim_y * 3 // 4
518
+
519
+ for i in range(dim_z):
520
+ for j in range(dim_y):
521
+ for k in range(dim_x):
522
+ if j > water_level:
523
+ self.density_field[i, j, k] = 0.9
524
+
525
+ # Течение (слева направо)
526
+ flow_strength = 1.0
527
+ for i in range(dim_z):
528
+ for j in range(dim_y):
529
+ for k in range(dim_x):
530
+ if self.density_field[i, j, k] > 0:
531
+ # Случайные возмущения
532
+ self.navier_stokes.velocity[i, j, k, 0] = flow_strength + np.random.randn() * 0.1
533
+ self.navier_stokes.velocity[i, j, k, 1] = np.random.randn() * 0.05
534
+
535
+ # Цвет воды
536
+ self._update_water_color()
537
+
538
+ def _init_smoke_fields(self):
539
+ """Инициализация полей для дымового шлейфа"""
540
+ dim_z, dim_y, dim_x = self.dimensions
541
+
542
+ # Источник дыма (внизу)
543
+ source_x = dim_x // 2
544
+ source_z = dim_z // 2
545
+
546
+ for i in range(dim_z):
547
+ for j in range(dim_y):
548
+ for k in range(dim_x):
549
+ # Расстояние до источника
550
+ dz = i - source_z
551
+ dx = k - source_x
552
+ dist = np.sqrt(dx*dx + dz*dz)
553
+
554
+ if dist < 5 and j < 10:
555
+ # Горячий дым
556
+ self.density_field[i, j, k] = 0.8
557
+
558
+ # Восходящий поток
559
+ self.navier_stokes.velocity[i, j, k, 1] = 2.0 + np.random.rand() * 0.5
560
+
561
+ # Температурное поле
562
+ self.temperature_field = np.zeros(self.dimensions, dtype=np.float32)
563
+ self.temperature_field[:, :10, :] = 500.0 # Горячий источник
564
+
565
+ # Цвет дыма
566
+ self._update_smoke_color()
567
+
568
+ def _init_fire_fields(self):
569
+ """Инициализация полей для огня"""
570
+ dim_z, dim_y, dim_x = self.dimensions
571
+
572
+ # Несколько источников огня
573
+ num_sources = 3
574
+ for _ in range(num_sources):
575
+ source_x = np.random.randint(dim_x//4, 3*dim_x//4)
576
+ source_z = np.random.randint(dim_z//4, 3*dim_z//4)
577
+
578
+ for i in range(max(0, source_z-2), min(dim_z, source_z+3)):
579
+ for j in range(5):
580
+ for k in range(max(0, source_x-2), min(dim_x, source_x+3)):
581
+ # Огонь
582
+ intensity = np.random.rand() * 0.8 + 0.2
583
+ self.density_field[i, j, k] = intensity
584
+
585
+ # Турбулентность
586
+ self.navier_stokes.velocity[i, j, k, 0] = np.random.randn() * 0.2
587
+ self.navier_stokes.velocity[i, j, k, 1] = 3.0 + np.random.rand() * 1.0
588
+ self.navier_stokes.velocity[i, j, k, 2] = np.random.randn() * 0.2
589
+
590
+ # Температурное поле
591
+ self.temperature_field = np.zeros(self.dimensions, dtype=np.float32)
592
+ self.temperature_field[:, :10, :] = self.params.fire_temperature
593
+
594
+ # Цвет огня
595
+ self._update_fire_color()
596
+
597
+ def _init_cloud_fields(self):
598
+ """Инициализация полей для облаков"""
599
+ dim_z, dim_y, dim_x = self.dimensions
600
+
601
+ # Создаем несколько облачных слоев
602
+ for layer in range(3):
603
+ layer_height = dim_y * (layer + 1) // 4
604
+
605
+ # Генерация облачной текстуры шумом
606
+ for i in range(dim_z):
607
+ for j in range(max(0, layer_height-5), min(dim_y, layer_height+5)):
608
+ for k in range(dim_x):
609
+ # Шум Перлина для облаков
610
+ noise = (np.sin(i*0.1 + layer*10) *
611
+ np.cos(k*0.1) *
612
+ np.sin(j*0.05 + layer*5))
613
+ noise = (noise + 1) * 0.5
614
+
615
+ if noise > 0.6:
616
+ self.density_field[i, j, k] = noise * 0.7
617
+
618
+ # Легкий ветер
619
+ wind_strength = 0.5
620
+ for i in range(dim_z):
621
+ for j in range(dim_y):
622
+ for k in range(dim_x):
623
+ if self.density_field[i, j, k] > 0:
624
+ self.navier_stokes.velocity[i, j, k, 0] = wind_strength
625
+
626
+ # Цвет облаков
627
+ self._update_cloud_color()
628
+
629
+ def _generate_obstacles(self) -> np.ndarray:
630
+ """Генерация препятствий для симуляции"""
631
+ dim_z, dim_y, dim_x = self.dimensions
632
+ obstacles = np.zeros(self.dimensions, dtype=np.float32)
633
+
634
+ if self.texture_type == DynamicTextureType.LAVA_FLOW:
635
+ # Камни и неровности для лавы
636
+ for _ in range(10):
637
+ center_x = np.random.randint(dim_x)
638
+ center_z = np.random.randint(dim_z)
639
+ center_y = np.random.randint(dim_y//2, dim_y)
640
+ radius = np.random.randint(3, 8)
641
+
642
+ for i in range(max(0, center_z-radius), min(dim_z, center_z+radius)):
643
+ for j in range(max(0, center_y-radius), min(dim_y, center_y+radius)):
644
+ for k in range(max(0, center_x-radius), min(dim_x, center_x+radius)):
645
+ dz = i - center_z
646
+ dy = j - center_y
647
+ dx = k - center_x
648
+ dist = np.sqrt(dx*dx + dy*dy + dz*dz)
649
+
650
+ if dist < radius:
651
+ obstacles[i, j, k] = 1.0
652
+
653
+ elif self.texture_type == DynamicTextureType.WATER_FLOW:
654
+ # Камни на дне реки
655
+ for _ in range(15):
656
+ center_x = np.random.randint(dim_x)
657
+ center_z = np.random.randint(dim_z)
658
+ center_y = dim_y - np.random.randint(5, 15)
659
+ radius = np.random.randint(2, 5)
660
+
661
+ for i in range(max(0, center_z-radius), min(dim_z, center_z+radius)):
662
+ for j in range(max(0, center_y-radius), min(dim_y, center_y+radius)):
663
+ for k in range(max(0, center_x-radius), min(dim_x, center_x+radius)):
664
+ dz = i - center_z
665
+ dy = j - center_y
666
+ dx = k - center_x
667
+ dist = np.sqrt(dx*dx + dy*dy + dz*dz)
668
+
669
+ if dist < radius:
670
+ obstacles[i, j, k] = 1.0
671
+
672
+ return obstacles
673
+
674
+ def _update_lava_color(self):
675
+ """Обновление цветового поля для лавы"""
676
+ dim_z, dim_y, dim_x = self.dimensions
677
+
678
+ for i in range(dim_z):
679
+ for j in range(dim_y):
680
+ for k in range(dim_x):
681
+ density = self.density_field[i, j, k]
682
+ temperature = self.temperature_field[i, j, k] if hasattr(self, 'temperature_field') else 1000.0
683
+
684
+ if density > 0:
685
+ # Цвет лавы в зависимости от температуры
686
+ # Холодная лава: темно-красная, горячая: ярко-желтая
687
+ t_norm = min(temperature / 1200.0, 1.0)
688
+
689
+ # Интерполяция между цветами
690
+ cold_color = np.array([0.6, 0.1, 0.0, 1.0]) # Темно-красный
691
+ hot_color = np.array([1.0, 0.8, 0.1, 1.0]) # Ярко-желтый
692
+
693
+ color = cold_color * (1 - t_norm) + hot_color * t_norm
694
+
695
+ # Яркость в зависимости от плотности
696
+ color[:3] *= density
697
+
698
+ self.color_field[i, j, k] = color
699
+ else:
700
+ self.color_field[i, j, k] = np.array([0.0, 0.0, 0.0, 0.0])
701
+
702
+ def _update_water_color(self):
703
+ """Обновление цветового поля для воды"""
704
+ dim_z, dim_y, dim_x = self.dimensions
705
+
706
+ # Цвет воды с глубиной
707
+ for i in range(dim_z):
708
+ for j in range(dim_y):
709
+ for k in range(dim_x):
710
+ density = self.density_field[i, j, k]
711
+
712
+ if density > 0:
713
+ # Глубина (чем ниже, тем темнее)
714
+ depth_factor = 1.0 - (j / dim_y)
715
+
716
+ # Базовый цвет воды
717
+ base_color = np.array([0.1, 0.3, 0.6, 0.8]) # Синий
718
+
719
+ # Темнее с глубиной
720
+ color = base_color * (0.7 + 0.3 * depth_factor)
721
+
722
+ # Прозрачность зависит от плотности
723
+ color[3] = 0.6 + density * 0.3
724
+
725
+ # Пузырьки (случайные яркие точки)
726
+ if np.random.rand() < 0.001:
727
+ color[:3] = np.array([1.0, 1.0, 1.0])
728
+ color[3] = 0.9
729
+
730
+ self.color_field[i, j, k] = color
731
+ else:
732
+ self.color_field[i, j, k] = np.array([0.0, 0.0, 0.0, 0.0])
733
+
734
+ def _update_smoke_color(self):
735
+ """Обновление цветового поля для дыма"""
736
+ dim_z, dim_y, dim_x = self.dimensions
737
+
738
+ for i in range(dim_z):
739
+ for j in range(dim_y):
740
+ for k in range(dim_x):
741
+ density = self.density_field[i, j, k]
742
+
743
+ if density > 0:
744
+ # Цвет дыма: от черного у источника к серому/белому
745
+ age_factor = min(j / dim_y, 1.0) # Чем выше, тем "старше" дым
746
+
747
+ # Интерполяция между черным и серым
748
+ young_color = np.array([0.1, 0.1, 0.1, 0.9]) # Черный дым
749
+ old_color = np.array([0.7, 0.7, 0.7, 0.3]) # Серый/белый дым
750
+
751
+ color = young_color * (1 - age_factor) + old_color * age_factor
752
+
753
+ # Интенсивность зависит от плотности
754
+ color[:3] *= density
755
+ color[3] *= density * 0.8
756
+
757
+ self.color_field[i, j, k] = color
758
+ else:
759
+ self.color_field[i, j, k] = np.array([0.0, 0.0, 0.0, 0.0])
760
+
761
+ def _update_fire_color(self):
762
+ """Обновление цветового поля для огня"""
763
+ dim_z, dim_y, dim_x = self.dimensions
764
+
765
+ for i in range(dim_z):
766
+ for j in range(dim_y):
767
+ for k in range(dim_x):
768
+ density = self.density_field[i, j, k]
769
+
770
+ if density > 0:
771
+ # Цвет огня: ядро - белое/желтое, края - красные
772
+ temperature = 1.0
773
+ if hasattr(self, 'temperature_field'):
774
+ temperature = min(self.temperature_field[i, j, k] / self.params.fire_temperature, 1.0)
775
+
776
+ # Три зоны цвета
777
+ if temperature > 0.8:
778
+ color = np.array([1.0, 1.0, 0.7, 0.9]) # Бело-желтый
779
+ elif temperature > 0.5:
780
+ color = np.array([1.0, 0.6, 0.1, 0.8]) # Оранжевый
781
+ else:
782
+ color = np.array([0.8, 0.2, 0.0, 0.7]) # Красный
783
+
784
+ # Интенсивность
785
+ intensity = density * temperature
786
+ color[:3] *= intensity
787
+ color[3] *= density
788
+
789
+ # Случайные мерцания
790
+ if np.random.rand() < 0.05:
791
+ color[:3] *= 1.2
792
+
793
+ self.color_field[i, j, k] = color
794
+ else:
795
+ self.color_field[i, j, k] = np.array([0.0, 0.0, 0.0, 0.0])
796
+
797
+ def _update_cloud_color(self):
798
+ """Обновление цветового поля для облаков"""
799
+ dim_z, dim_y, dim_x = self.dimensions
800
+
801
+ for i in range(dim_z):
802
+ for j in range(dim_y):
803
+ for k in range(dim_x):
804
+ density = self.density_field[i, j, k]
805
+
806
+ if density > 0:
807
+ # Цвет облаков: белый с легкими тенями
808
+ # Тени на нижней стороне облаков
809
+ shadow_factor = 1.0
810
+
811
+ # Проверяем плотность ниже (для теней)
812
+ if j > 0 and self.density_field[i, j-1, k] < density:
813
+ shadow_factor = 0.85
814
+
815
+ # Базовый белый цвет
816
+ color = np.array([0.95, 0.95, 0.98, density * 0.8])
817
+
818
+ # Применяем тени
819
+ color[:3] *= shadow_factor
820
+
821
+ # Легкий синий оттенок для высоких облаков
822
+ altitude_factor = j / dim_y
823
+ blue_tint = np.array([0.9, 0.95, 1.0, 1.0])
824
+ color = color * (1 - altitude_factor*0.3) + blue_tint * (altitude_factor*0.3)
825
+
826
+ self.color_field[i, j, k] = color
827
+ else:
828
+ self.color_field[i, j, k] = np.array([0.0, 0.0, 0.0, 0.0])
829
+
830
+ def update(self, dt: Optional[float] = None) -> DynamicTextureState:
831
+ """
832
+ Обновление динамической текстуры на один шаг
833
+
834
+ Args:
835
+ dt: Шаг по времени (если None, используется params.time_step)
836
+
837
+ Returns:
838
+ Состояние текстуры после обновления
839
+ """
840
+ if dt is None:
841
+ dt = self.params.time_step
842
+
843
+ self.time += dt
844
+
845
+ # 1. Обновление поля скоростей (Навье-Стокс)
846
+ external_forces = self._compute_external_forces()
847
+ velocity = self.navier_stokes.step(external_forces, self.obstacles)
848
+
849
+ # 2. Адвекция плотности
850
+ self._advect_density(velocity)
851
+
852
+ # 3. Диффузия плотности
853
+ self._diffuse_density()
854
+
855
+ # 4. Источники/стоки (в зависимости от типа)
856
+ self._apply_sources_and_sinks()
857
+
858
+ # 5. Обновление температуры (если есть)
859
+ if hasattr(self, 'temperature_field'):
860
+ self._update_temperature(velocity)
861
+
862
+ # 6. Обновление цвета
863
+ self._update_color_field()
864
+
865
+ # 7. Создание состояния
866
+ state = DynamicTextureState(
867
+ time=self.time,
868
+ data=self.color_field.copy(),
869
+ velocity_field=velocity.copy(),
870
+ temperature_field=self.temperature_field.copy() if hasattr(self, 'temperature_field') else None,
871
+ divergence_field=self.navier_stokes.divergence.copy()
872
+ )
873
+
874
+ # Сохраняем в историю
875
+ self.state_history.append(state)
876
+
877
+ return state
878
+
879
+ def _compute_external_forces(self) -> np.ndarray:
880
+ """Вычисление внешних сил в зависимости от типа текстуры"""
881
+ dim_z, dim_y, dim_x = self.dimensions
882
+ forces = np.zeros((dim_z, dim_y, dim_x, 3), dtype=np.float32)
883
+
884
+ if self.texture_type == DynamicTextureType.LAVA_FLOW:
885
+ # Гравитация + термическая конвекция
886
+ for i in range(dim_z):
887
+ for j in range(dim_y):
888
+ for k in range(dim_x):
889
+ # Горячая лава поднимается
890
+ if hasattr(self, 'temperature_field'):
891
+ temp = self.temperature_field[i, j, k]
892
+ buoyancy = (temp / 1000.0 - 1.0) * 2.0 # Подъемная сила
893
+ forces[i, j, k, 1] = self.params.gravity[1] + buoyancy
894
+ else:
895
+ forces[i, j, k] = self.params.gravity
896
+
897
+ elif self.texture_type == DynamicTextureType.SMOKE_PLUME:
898
+ # Сильная подъемная сила для дыма
899
+ for i in range(dim_z):
900
+ for j in range(dim_y):
901
+ for k in range(dim_x):
902
+ if self.density_field[i, j, k] > 0:
903
+ # Дым поднимается
904
+ forces[i, j, k, 1] = self.params.smoke_buoyancy
905
+
906
+ # Случайные турбулентности
907
+ forces[i, j, k, 0] += np.random.randn() * 0.1
908
+ forces[i, j, k, 2] += np.random.randn() * 0.1
909
+
910
+ elif self.texture_type == DynamicTextureType.FIRE:
911
+ # Огонь сильно поднимается + турбулентность
912
+ for i in range(dim_z):
913
+ for j in range(dim_y):
914
+ for k in range(dim_x):
915
+ if self.density_field[i, j, k] > 0:
916
+ # Интенсивная подъемная сила
917
+ buoyancy = 3.0 + np.random.rand() * 2.0
918
+ forces[i, j, k, 1] = buoyancy
919
+
920
+ # Вихревые движения
921
+ angle = self.time * 5.0 + i * 0.1 + k * 0.1
922
+ forces[i, j, k, 0] += np.sin(angle) * 0.5
923
+ forces[i, j, k, 2] += np.cos(angle) * 0.5
924
+
925
+ elif self.texture_type == DynamicTextureType.CLOUD_DRIFT:
926
+ # Легкий ветер + случайные движения
927
+ wind_strength = 0.3
928
+ for i in range(dim_z):
929
+ for j in range(dim_y):
930
+ for k in range(dim_x):
931
+ forces[i, j, k, 0] = wind_strength
932
+
933
+ # Слабые вертикальные движения
934
+ if self.density_field[i, j, k] > 0:
935
+ forces[i, j, k, 1] = np.random.randn() * 0.05
936
+
937
+ else:
938
+ # По умолчанию только гравитация
939
+ for i in range(dim_z):
940
+ for j in range(dim_y):
941
+ for k in range(dim_x):
942
+ forces[i, j, k] = self.params.gravity
943
+
944
+ return forces
945
+
946
+ def _advect_density(self, velocity: np.ndarray):
947
+ """Адвекция плотности полем скоростей (полулагранжевым методом)"""
948
+ dim_z, dim_y, dim_x = self.dimensions
949
+ density_new = np.zeros_like(self.density_field)
950
+
951
+ for i in prange(dim_z):
952
+ for j in range(dim_y):
953
+ for k in range(dim_x):
954
+ # Текущая скорость
955
+ vx, vy, vz = velocity[i, j, k]
956
+
957
+ # Координата предыдущего шага
958
+ prev_i = i - vz * self.params.time_step * dim_z
959
+ prev_j = j - vy * self.params.time_step * dim_y
960
+ prev_k = k - vx * self.params.time_step * dim_x
961
+
962
+ # Граничные условия
963
+ prev_i = max(0, min(dim_z-1, prev_i))
964
+ prev_j = max(0, min(dim_y-1, prev_j))
965
+ prev_k = max(0, min(dim_x-1, prev_k))
966
+
967
+ # Трилинейная интерполяция плотности
968
+ i0 = int(np.floor(prev_i))
969
+ j0 = int(np.floor(prev_j))
970
+ k0 = int(np.floor(prev_k))
971
+
972
+ i1 = min(i0 + 1, dim_z - 1)
973
+ j1 = min(j0 + 1, dim_y - 1)
974
+ k1 = min(k0 + 1, dim_x - 1)
975
+
976
+ di = prev_i - i0
977
+ dj = prev_j - j0
978
+ dk = prev_k - k0
979
+
980
+ c000 = self.density_field[i0, j0, k0]
981
+ c001 = self.density_field[i0, j0, k1]
982
+ c010 = self.density_field[i0, j1, k0]
983
+ c011 = self.density_field[i0, j1, k1]
984
+ c100 = self.density_field[i1, j0, k0]
985
+ c101 = self.density_field[i1, j0, k1]
986
+ c110 = self.density_field[i1, j1, k0]
987
+ c111 = self.density_field[i1, j1, k1]
988
+
989
+ c00 = c000 * (1 - dk) + c001 * dk
990
+ c01 = c010 * (1 - dk) + c011 * dk
991
+ c10 = c100 * (1 - dk) + c101 * dk
992
+ c11 = c110 * (1 - dk) + c111 * dk
993
+
994
+ c0 = c00 * (1 - dj) + c01 * dj
995
+ c1 = c10 * (1 - dj) + c11 * dj
996
+
997
+ density_new[i, j, k] = c0 * (1 - di) + c1 * di
998
+
999
+ self.density_field = density_new
1000
+
1001
+ def _diffuse_density(self):
1002
+ """Диффузия плотности"""
1003
+ if self.params.diffusion_rate <= 0:
1004
+ return
1005
+
1006
+ dim_z, dim_y, dim_x = self.dimensions
1007
+ diffusion = self.params.diffusion_rate
1008
+ dt = self.params.time_step
1009
+ alpha = dt * diffusion
1010
+
1011
+ density_new = np.zeros_like(self.density_field)
1012
+
1013
+ for i in prange(dim_z):
1014
+ for j in range(dim_y):
1015
+ for k in range(dim_x):
1016
+ center = self.density_field[i, j, k]
1017
+
1018
+ # Соседи
1019
+ left = self.density_field[i, j, (k-1)%dim_x]
1020
+ right = self.density_field[i, j, (k+1)%dim_x]
1021
+ down = self.density_field[i, (j-1)%dim_y, k]
1022
+ up = self.density_field[i, (j+1)%dim_y, k]
1023
+ back = self.density_field[(i-1)%dim_z, j, k]
1024
+ front = self.density_field[(i+1)%dim_z, j, k]
1025
+
1026
+ # Лапласиан
1027
+ laplacian = (left + right + down + up + back + front - 6 * center)
1028
+
1029
+ # Обновление
1030
+ density_new[i, j, k] = center + alpha * laplacian
1031
+
1032
+ self.density_field = np.clip(density_new, 0, 1)
1033
+
1034
+ def _apply_sources_and_sinks(self):
1035
+ """Применение источников и стоков в зависимости от типа"""
1036
+ dim_z, dim_y, dim_x = self.dimensions
1037
+
1038
+ if self.texture_type == DynamicTextureType.LAVA_FLOW:
1039
+ # Источник лавы продолжает извергаться
1040
+ source_center = (dim_z//2, dim_y//4, dim_x//2)
1041
+ source_radius = min(dim_x, dim_z) // 6
1042
+
1043
+ for i in range(max(0, source_center[0]-source_radius),
1044
+ min(dim_z, source_center[0]+source_radius)):
1045
+ for j in range(max(0, source_center[1]-source_radius),
1046
+ min(dim_y, source_center[1]+source_radius)):
1047
+ for k in range(max(0, source_center[2]-source_radius),
1048
+ min(dim_x, source_center[2]+source_radius)):
1049
+ dz = i - source_center[0]
1050
+ dy = j - source_center[1]
1051
+ dx = k - source_center[2]
1052
+ dist = np.sqrt(dx*dx + dy*dy + dz*dz)
1053
+
1054
+ if dist < source_radius:
1055
+ # Добавляем новую лаву
1056
+ self.density_field[i, j, k] = min(1.0, self.density_field[i, j, k] + 0.1)
1057
+
1058
+ if hasattr(self, 'temperature_field'):
1059
+ self.temperature_field[i, j, k] = 1200.0
1060
+
1061
+ elif self.texture_type == DynamicTextureType.SMOKE_PLUME:
1062
+ # Постоянный источник дыма
1063
+ source_x = dim_x // 2
1064
+ source_z = dim_z // 2
1065
+
1066
+ for i in range(max(0, source_z-3), min(dim_z, source_z+4)):
1067
+ for j in range(5):
1068
+ for k in range(max(0, source_x-3), min(dim_x, source_x+4)):
1069
+ dz = i - source_z
1070
+ dx = k - source_x
1071
+ dist = np.sqrt(dx*dx + dz*dz)
1072
+
1073
+ if dist < 3:
1074
+ # Новый дым
1075
+ self.density_field[i, j, k] = min(1.0, self.density_field[i, j, k] + 0.2)
1076
+
1077
+ if hasattr(self, 'temperature_field'):
1078
+ self.temperature_field[i, j, k] = 500.0
1079
+
1080
+ elif self.texture_type == DynamicTextureType.FIRE:
1081
+ # Постоянный источник огня
1082
+ for _ in range(2): # Несколько новых источников
1083
+ source_x = np.random.randint(dim_x//4, 3*dim_x//4)
1084
+ source_z = np.random.randint(dim_z//4, 3*dim_z//4)
1085
+
1086
+ for i in range(max(0, source_z-2), min(dim_z, source_z+3)):
1087
+ for j in range(5):
1088
+ for k in range(max(0, source_x-2), min(dim_x, source_x+3)):
1089
+ intensity = np.random.rand() * 0.5 + 0.3
1090
+ self.density_field[i, j, k] = min(1.0, self.density_field[i, j, k] + intensity * 0.1)
1091
+
1092
+ if hasattr(self, 'temperature_field'):
1093
+ self.temperature_field[i, j, k] = self.params.fire_temperature
1094
+
1095
+ # Общее затухание
1096
+ self.density_field *= 0.995
1097
+
1098
+ def _update_temperature(self, velocity: np.ndarray):
1099
+ """Обновление температурного поля"""
1100
+ dim_z, dim_y, dim_x = self.dimensions
1101
+
1102
+ # Адвекция температуры
1103
+ temp_new = np.zeros_like(self.temperature_field)
1104
+
1105
+ for i in range(dim_z):
1106
+ for j in range(dim_y):
1107
+ for k in range(dim_x):
1108
+ vx, vy, vz = velocity[i, j, k]
1109
+
1110
+ # Полулагранжевая адвекция
1111
+ prev_i = i - vz * self.params.time_step * dim_z
1112
+ prev_j = j - vy * self.params.time_step * dim_y
1113
+ prev_k = k - vx * self.params.time_step * dim_x
1114
+
1115
+ prev_i = max(0, min(dim_z-1, prev_i))
1116
+ prev_j = max(0, min(dim_y-1, prev_j))
1117
+ prev_k = max(0, min(dim_x-1, prev_k))
1118
+
1119
+ # Интерполяция
1120
+ i0 = int(np.floor(prev_i))
1121
+ j0 = int(np.floor(prev_j))
1122
+ k0 = int(np.floor(prev_k))
1123
+
1124
+ i1 = min(i0 + 1, dim_z - 1)
1125
+ j1 = min(j0 + 1, dim_y - 1)
1126
+ k1 = min(k0 + 1, dim_x - 1)
1127
+
1128
+ di = prev_i - i0
1129
+ dj = prev_j - j0
1130
+ dk = prev_k - k0
1131
+
1132
+ c000 = self.temperature_field[i0, j0, k0]
1133
+ c001 = self.temperature_field[i0, j0, k1]
1134
+ c010 = self.temperature_field[i0, j1, k0]
1135
+ c011 = self.temperature_field[i0, j1, k1]
1136
+ c100 = self.temperature_field[i1, j0, k0]
1137
+ c101 = self.temperature_field[i1, j0, k1]
1138
+ c110 = self.temperature_field[i1, j1, k0]
1139
+ c111 = self.temperature_field[i1, j1, k1]
1140
+
1141
+ c00 = c000 * (1 - dk) + c001 * dk
1142
+ c01 = c010 * (1 - dk) + c011 * dk
1143
+ c10 = c100 * (1 - dk) + c101 * dk
1144
+ c11 = c110 * (1 - dk) + c111 * dk
1145
+
1146
+ c0 = c00 * (1 - dj) + c01 * dj
1147
+ c1 = c10 * (1 - dj) + c11 * dj
1148
+
1149
+ temp_new[i, j, k] = c0 * (1 - di) + c1 * di
1150
+
1151
+ # Теплопроводность
1152
+ if self.params.thermal_conductivity > 0:
1153
+ for i in range(1, dim_z-1):
1154
+ for j in range(1, dim_y-1):
1155
+ for k in range(1, dim_x-1):
1156
+ laplacian = (temp_new[i-1, j, k] + temp_new[i+1, j, k] +
1157
+ temp_new[i, j-1, k] + temp_new[i, j+1, k] +
1158
+ temp_new[i, j, k-1] + temp_new[i, j, k+1] -
1159
+ 6 * temp_new[i, j, k])
1160
+
1161
+ temp_new[i, j, k] += self.params.thermal_conductivity * laplacian
1162
+
1163
+ # Охлаждение/затухание
1164
+ temp_new *= self.params.temperature_decay
1165
+
1166
+ # Ограничение температуры
1167
+ self.temperature_field = np.clip(temp_new, 0, self.params.max_temperature)
1168
+
1169
+ def _update_color_field(self):
1170
+ """Обновление цветового поля на основе текущего состояния"""
1171
+ if self.texture_type == DynamicTextureType.LAVA_FLOW:
1172
+ self._update_lava_color()
1173
+ elif self.texture_type == DynamicTextureType.WATER_FLOW:
1174
+ self._update_water_color()
1175
+ elif self.texture_type == DynamicTextureType.SMOKE_PLUME:
1176
+ self._update_smoke_color()
1177
+ elif self.texture_type == DynamicTextureType.FIRE:
1178
+ self._update_fire_color()
1179
+ elif self.texture_type == DynamicTextureType.CLOUD_DRIFT:
1180
+ self._update_cloud_color()
1181
+
1182
+ def get_state(self, time: Optional[float] = None) -> DynamicTextureState:
1183
+ """
1184
+ Получение состояния текстуры в определенное время
1185
+ (интерполяция между сохраненными состояниями)
1186
+ """
1187
+ if time is None:
1188
+ time = self.time
1189
+
1190
+ # Если запрашиваем текущее время
1191
+ if abs(time - self.time) < self.params.time_step * 0.5:
1192
+ return DynamicTextureState(
1193
+ time=self.time,
1194
+ data=self.color_field.copy(),
1195
+ velocity_field=self.navier_stokes.velocity.copy(),
1196
+ temperature_field=self.temperature_field.copy() if hasattr(self, 'temperature_field') else None
1197
+ )
1198
+
1199
+ # Ищем два ближайших состояния для интерполяции
1200
+ states = list(self.state_history)
1201
+ if len(states) < 2:
1202
+ return states[-1] if states else self.update(0)
1203
+
1204
+ # Сортируем по времени
1205
+ states.sort(key=lambda s: s.time)
1206
+
1207
+ # Находим состояния до и после запрашиваемого времени
1208
+ prev_state = None
1209
+ next_state = None
1210
+
1211
+ for state in states:
1212
+ if state.time <= time:
1213
+ prev_state = state
1214
+ else:
1215
+ next_state = state
1216
+ break
1217
+
1218
+ # Если время вне диапазона
1219
+ if prev_state is None:
1220
+ return next_state
1221
+ if next_state is None:
1222
+ return prev_state
1223
+
1224
+ # Линейная интерполяция
1225
+ t = (time - prev_state.time) / (next_state.time - prev_state.time)
1226
+
1227
+ # Интерполяция данных
1228
+ data_interp = prev_state.data * (1 - t) + next_state.data * t
1229
+
1230
+ # Создаем интерполированное состояние
1231
+ return DynamicTextureState(
1232
+ time=time,
1233
+ data=data_interp,
1234
+ velocity_field=prev_state.velocity_field if prev_state.velocity_field is not None else None,
1235
+ temperature_field=prev_state.temperature_field if prev_state.temperature_field is not None else None
1236
+ )
1237
+
1238
+ def reset(self):
1239
+ """Сброс симуляции в начальное состояние"""
1240
+ self._init_fields()
1241
+ self.time = 0.0
1242
+ self.state_history.clear()
1243
+ self.cache.clear()
1244
+
1245
+ # ----------------------------------------------------------------------
1246
+ # Система потоковых динамических текстур для больших миров
1247
+ # ----------------------------------------------------------------------
1248
+
1249
+ class StreamingDynamicTextures:
1250
+ """Управление множеством динамических текстур в потоковом режиме"""
1251
+
1252
+ def __init__(self,
1253
+ chunk_size: Tuple[int, int, int] = (32, 32, 32),
1254
+ max_active_chunks: int = 8,
1255
+ physics_params: Optional[PhysicsParameters] = None):
1256
+
1257
+ self.chunk_size = chunk_size
1258
+ self.max_active_chunks = max_active_chunks
1259
+ self.physics_params = physics_params or PhysicsParameters()
1260
+
1261
+ # Активные чанки
1262
+ self.active_chunks = {} # (cx, cy, cz) -> DynamicTextureGenerator3D
1263
+
1264
+ # Кэш состояний
1265
+ self.state_cache = {}
1266
+
1267
+ # Приоритетная очередь для обновления
1268
+ self.update_queue = []
1269
+
1270
+ # Статистика
1271
+ self.stats = {
1272
+ 'updates': 0,
1273
+ 'cache_hits': 0,
1274
+ 'cache_misses': 0,
1275
+ 'chunks_created': 0,
1276
+ 'chunks_evicted': 0
1277
+ }
1278
+
1279
+ def request_chunk(self,
1280
+ chunk_coords: Tuple[int, int, int],
1281
+ texture_type: DynamicTextureType,
1282
+ priority: float = 1.0) -> Optional[DynamicTextureState]:
1283
+ """
1284
+ Запрос состояния чанка динамической текстуры
1285
+
1286
+ Args:
1287
+ chunk_coords: Координаты чанка
1288
+ texture_type: Тип текстуры
1289
+ priority: Приоритет (выше = важнее)
1290
+
1291
+ Returns:
1292
+ Состояние чанка или None если не готово
1293
+ """
1294
+ chunk_key = (*chunk_coords, texture_type.value)
1295
+
1296
+ # Проверяем кэш
1297
+ if chunk_key in self.state_cache:
1298
+ self.stats['cache_hits'] += 1
1299
+ return self.state_cache[chunk_key]
1300
+
1301
+ self.stats['cache_misses'] += 1
1302
+
1303
+ # Проверяем активные чанки
1304
+ if chunk_coords in self.active_chunks:
1305
+ generator = self.active_chunks[chunk_coords]
1306
+
1307
+ # Обновляем приоритет
1308
+ self._update_priority(chunk_coords, priority)
1309
+
1310
+ # Получаем текущее состояние
1311
+ state = generator.get_state()
1312
+ self.state_cache[chunk_key] = state
1313
+
1314
+ return state
1315
+
1316
+ # Если не активно, создаем новый чанк
1317
+ if len(self.active_chunks) < self.max_active_chunks:
1318
+ self._create_chunk(chunk_coords, texture_type, priority)
1319
+
1320
+ return None
1321
+
1322
+ def update_all(self, dt: float):
1323
+ """Обновление всех активных чанков"""
1324
+ # Сортируем по приоритету
1325
+ sorted_chunks = sorted(self.update_queue, key=lambda x: x[0], reverse=True)
1326
+
1327
+ # Обновляем только верхние N чанков для производительности
1328
+ chunks_to_update = min(len(sorted_chunks), self.max_active_chunks // 2)
1329
+
1330
+ for i in range(chunks_to_update):
1331
+ priority, chunk_coords = sorted_chunks[i]
1332
+
1333
+ if chunk_coords in self.active_chunks:
1334
+ generator = self.active_chunks[chunk_coords]
1335
+ new_state = generator.update(dt)
1336
+
1337
+ # Обновляем кэш
1338
+ chunk_key = (*chunk_coords, generator.texture_type.value)
1339
+ self.state_cache[chunk_key] = new_state
1340
+
1341
+ self.stats['updates'] += 1
1342
+
1343
+ # Очистка устаревших данных
1344
+ self._cleanup_old_data()
1345
+
1346
+ def _create_chunk(self,
1347
+ chunk_coords: Tuple[int, int, int],
1348
+ texture_type: DynamicTextureType,
1349
+ priority: float):
1350
+ """Создание нового чанка динамической текстуры"""
1351
+ # Если достигнут лимит, вытесняем самый низкоприоритетный чанк
1352
+ if len(self.active_chunks) >= self.max_active_chunks:
1353
+ self._evict_lowest_priority_chunk()
1354
+
1355
+ # Создаем генератор
1356
+ generator = DynamicTextureGenerator3D(
1357
+ dimensions=self.chunk_size,
1358
+ texture_type=texture_type,
1359
+ physics_params=self.physics_params,
1360
+ seed=self._chunk_seed(chunk_coords, texture_type)
1361
+ )
1362
+
1363
+ # Инициализация в зависимости от соседних чанков
1364
+ self._initialize_from_neighbors(chunk_coords, generator)
1365
+
1366
+ # Добавляем в активные
1367
+ self.active_chunks[chunk_coords] = generator
1368
+ self._update_priority(chunk_coords, priority)
1369
+
1370
+ self.stats['chunks_created'] += 1
1371
+
1372
+ def _evict_lowest_priority_chunk(self):
1373
+ """Вытеснение чанка с самым низким приоритетом"""
1374
+ if not self.update_queue:
1375
+ return
1376
+
1377
+ # Находим чанк с минимальным приоритетом
1378
+ min_priority = float('inf')
1379
+ chunk_to_evict = None
1380
+
1381
+ for priority, chunk_coords in self.update_queue:
1382
+ if priority < min_priority:
1383
+ min_priority = priority
1384
+ chunk_to_evict = chunk_coords
1385
+
1386
+ if chunk_to_evict and chunk_to_evict in self.active_chunks:
1387
+ # Сохраняем финальное состояние в кэш
1388
+ generator = self.active_chunks[chunk_to_evict]
1389
+ final_state = generator.get_state()
1390
+ chunk_key = (*chunk_to_evict, generator.texture_type.value)
1391
+ self.state_cache[chunk_key] = final_state
1392
+
1393
+ # Удаляем из активных
1394
+ del self.active_chunks[chunk_to_evict]
1395
+
1396
+ # Удаляем из очереди обновлений
1397
+ self.update_queue = [(p, c) for p, c in self.update_queue if c != chunk_to_evict]
1398
+
1399
+ self.stats['chunks_evicted'] += 1
1400
+
1401
+ def _update_priority(self, chunk_coords: Tuple[int, int, int], new_priority: float):
1402
+ """Обновление приоритета чанка"""
1403
+ # Удаляем старый приоритет
1404
+ self.update_queue = [(p, c) for p, c in self.update_queue if c != chunk_coords]
1405
+
1406
+ # Добавляем новый
1407
+ self.update_queue.append((new_priority, chunk_coords))
1408
+
1409
+ def _chunk_seed(self, chunk_coords: Tuple[int, int, int], texture_type: DynamicTextureType) -> int:
1410
+ """Генерация seed для чанка"""
1411
+ seed_str = f"{chunk_coords[0]}_{chunk_coords[1]}_{chunk_coords[2]}_{texture_type.value}"
1412
+ return int(hashlib.md5(seed_str.encode()).hexdigest()[:8], 16) % (2**31)
1413
+
1414
+ def _initialize_from_neighbors(self,
1415
+ chunk_coords: Tuple[int, int, int],
1416
+ generator: DynamicTextureGenerator3D):
1417
+ """Инициализация чанка на основе соседних"""
1418
+ # Здесь могла бы быть логика передачи состояния между чанками
1419
+ # Например, течение воды из одного чанка в другой
1420
+
1421
+ # Пока просто оставляем стандартную инициализацию
1422
+ pass
1423
+
1424
+ def _cleanup_old_data(self):
1425
+ """Очистка устаревших данных из кэша"""
1426
+ max_cache_size = 100
1427
+
1428
+ if len(self.state_cache) > max_cache_size:
1429
+ # Удаляем самые старые записи
1430
+ keys_to_remove = list(self.state_cache.keys())[:len(self.state_cache) - max_cache_size]
1431
+ for key in keys_to_remove:
1432
+ del self.state_cache[key]
1433
+
1434
+ def get_stats(self) -> Dict:
1435
+ """Получение статистики системы"""
1436
+ return {
1437
+ **self.stats,
1438
+ 'active_chunks': len(self.active_chunks),
1439
+ 'cached_states': len(self.state_cache),
1440
+ 'queue_size': len(self.update_queue)
1441
+ }
1442
+
1443
+ # ----------------------------------------------------------------------
1444
+ # Визуализация динамических текстур
1445
+ # ----------------------------------------------------------------------
1446
+
1447
+ class DynamicTextureVisualizer:
1448
+ """Визуализатор для динамических 3D текстур"""
1449
+
1450
+ def __init__(self, render_method: str = "raycast"):
1451
+ self.render_method = render_method
1452
+
1453
+ def render_state(self,
1454
+ state: DynamicTextureState,
1455
+ camera_pos: Tuple[float, float, float] = (0.5, 0.5, 2.0),
1456
+ camera_target: Tuple[float, float, float] = (0.5, 0.5, 0.5),
1457
+ image_size: Tuple[int, int] = (256, 256)) -> np.ndarray:
1458
+ """
1459
+ Рендеринг состояния динамической текстуры
1460
+
1461
+ Args:
1462
+ state: Состояние текстуры
1463
+ camera_pos: Позиция камеры
1464
+ camera_target: Цель камеры
1465
+ image_size: Размер изображения
1466
+
1467
+ Returns:
1468
+ 2D изображение (H, W, 4) RGBA
1469
+ """
1470
+ if self.render_method == "raycast":
1471
+ return self._raycast_state(state, camera_pos, camera_target, image_size)
1472
+ elif self.render_method == "mip":
1473
+ return self._mip_state(state, image_size)
1474
+ elif self.render_method == "slice":
1475
+ return self._slice_state(state, image_size)
1476
+ else:
1477
+ raise ValueError(f"Unknown render method: {self.render_method}")
1478
+
1479
+ def _raycast_state(self,
1480
+ state: DynamicTextureState,
1481
+ camera_pos: Tuple[float, float, float],
1482
+ camera_target: Tuple[float, float, float],
1483
+ image_size: Tuple[int, int]) -> np.ndarray:
1484
+ """Рейкастинг для динамической текстуры"""
1485
+ width, height = image_size
1486
+ image = np.zeros((height, width, 4), dtype=np.float32)
1487
+
1488
+ # Базис камеры
1489
+ camera_dir = np.array(camera_target) - np.array(camera_pos)
1490
+ camera_dir = camera_dir / np.linalg.norm(camera_dir)
1491
+
1492
+ up = np.array([0.0, 1.0, 0.0])
1493
+ right = np.cross(camera_dir, up)
1494
+ right = right / np.linalg.norm(right)
1495
+ up = np.cross(right, camera_dir)
1496
+
1497
+ # FOV
1498
+ fov = 60.0
1499
+ aspect = width / height
1500
+ half_height = np.tan(np.radians(fov) / 2.0)
1501
+ half_width = aspect * half_height
1502
+
1503
+ dim_z, dim_y, dim_x, channels = state.shape
1504
+
1505
+ # Параметры рендеринга
1506
+ max_steps = 128
1507
+ step_size = 1.0 / max(dim_x, dim_y, dim_z)
1508
+
1509
+ for y in range(height):
1510
+ for x in range(width):
1511
+ # Направление луча
1512
+ u = (2.0 * x / width - 1.0) * half_width
1513
+ v = (1.0 - 2.0 * y / height) * half_height
1514
+
1515
+ ray_dir = camera_dir + u * right + v * up
1516
+ ray_dir = ray_dir / np.linalg.norm(ray_dir)
1517
+
1518
+ # Стартовая позиция
1519
+ ray_pos = np.array(camera_pos, dtype=np.float32)
1520
+
1521
+ # Интегрирование вдоль луча
1522
+ color = np.zeros(4, dtype=np.float32)
1523
+
1524
+ for step in range(max_steps):
1525
+ # Проверяем границы
1526
+ if (ray_pos[0] < 0 or ray_pos[0] >= 1 or
1527
+ ray_pos[1] < 0 or ray_pos[1] >= 1 or
1528
+ ray_pos[2] < 0 or ray_pos[2] >= 1):
1529
+ break
1530
+
1531
+ # Трилинейная интерполяция
1532
+ fx = ray_pos[0] * (dim_x - 1)
1533
+ fy = ray_pos[1] * (dim_y - 1)
1534
+ fz = ray_pos[2] * (dim_z - 1)
1535
+
1536
+ ix0 = int(np.floor(fx))
1537
+ iy0 = int(np.floor(fy))
1538
+ iz0 = int(np.floor(fz))
1539
+
1540
+ ix1 = min(ix0 + 1, dim_x - 1)
1541
+ iy1 = min(iy0 + 1, dim_y - 1)
1542
+ iz1 = min(iz0 + 1, dim_z - 1)
1543
+
1544
+ dx = fx - ix0
1545
+ dy = fy - iy0
1546
+ dz = fz - iz0
1547
+
1548
+ # Интерполяция для каждого канала
1549
+ sample = np.zeros(channels, dtype=np.float32)
1550
+
1551
+ for c in range(channels):
1552
+ c000 = state.data[iz0, iy0, ix0, c]
1553
+ c001 = state.data[iz0, iy0, ix1, c]
1554
+ c010 = state.data[iz0, iy1, ix0, c]
1555
+ c011 = state.data[iz0, iy1, ix1, c]
1556
+ c100 = state.data[iz1, iy0, ix0, c]
1557
+ c101 = state.data[iz1, iy0, ix1, c]
1558
+ c110 = state.data[iz1, iy1, ix0, c]
1559
+ c111 = state.data[iz1, iy1, ix1, c]
1560
+
1561
+ c00 = c000 * (1 - dx) + c001 * dx
1562
+ c01 = c010 * (1 - dx) + c011 * dx
1563
+ c10 = c100 * (1 - dx) + c101 * dx
1564
+ c11 = c110 * (1 - dx) + c111 * dx
1565
+
1566
+ c0 = c00 * (1 - dy) + c01 * dy
1567
+ c1 = c10 * (1 - dy) + c11 * dy
1568
+
1569
+ sample[c] = c0 * (1 - dz) + c1 * dz
1570
+
1571
+ # Фронтально-заднее смешивание
1572
+ alpha = sample[3]
1573
+ color = color + (1.0 - color[3]) * alpha * sample
1574
+
1575
+ # Если полностью непрозрачный
1576
+ if color[3] >= 0.99:
1577
+ break
1578
+
1579
+ # Двигаем луч
1580
+ ray_pos += ray_dir * step_size
1581
+
1582
+ image[y, x] = color
1583
+
1584
+ return np.clip(image, 0, 1)
1585
+
1586
+ def _mip_state(self, state: DynamicTextureState, image_size: Tuple[int, int]) -> np.ndarray:
1587
+ """MIP (Maximum Intensity Projection) рендеринг"""
1588
+ width, height = image_size
1589
+ dim_z, dim_y, dim_x, channels = state.shape
1590
+
1591
+ # Выбираем ось проекции (по умолчанию Z)
1592
+ axis = 'z'
1593
+
1594
+ if axis == 'x':
1595
+ mip = np.max(state.data, axis=2)
1596
+ elif axis == 'y':
1597
+ mip = np.max(state.data, axis=1)
1598
+ else: # 'z'
1599
+ mip = np.max(state.data, axis=0)
1600
+
1601
+ # Масштабируем до нужного размера
1602
+ from scipy import ndimage
1603
+
1604
+ if mip.shape[0] != height or mip.shape[1] != width:
1605
+ scale_y = height / mip.shape[0]
1606
+ scale_x = width / mip.shape[1]
1607
+
1608
+ mip_resized = np.zeros((height, width, channels), dtype=np.float32)
1609
+
1610
+ for c in range(channels):
1611
+ mip_resized[:, :, c] = ndimage.zoom(mip[:, :, c], (scale_y, scale_x), order=1)
1612
+
1613
+ mip = mip_resized
1614
+
1615
+ return mip
1616
+
1617
+ def _slice_state(self, state: DynamicTextureState, image_size: Tuple[int, int]) -> np.ndarray:
1618
+ """Рендеринг 2D среза"""
1619
+ width, height = image_size
1620
+ dim_z, dim_y, dim_x, channels = state.shape
1621
+
1622
+ # Серединный срез по оси Z
1623
+ slice_idx = dim_z // 2
1624
+ slice_data = state.data[slice_idx, :, :, :]
1625
+
1626
+ # Масштабируем
1627
+ from scipy import ndimage
1628
+
1629
+ if slice_data.shape[0] != height or slice_data.shape[1] != width:
1630
+ scale_y = height / slice_data.shape[0]
1631
+ scale_x = width / slice_data.shape[1]
1632
+
1633
+ slice_resized = np.zeros((height, width, channels), dtype=np.float32)
1634
+
1635
+ for c in range(channels):
1636
+ slice_resized[:, :, c] = ndimage.zoom(slice_data[:, :, c], (scale_y, scale_x), order=1)
1637
+
1638
+ slice_data = slice_resized
1639
+
1640
+ return slice_data
1641
+
1642
+ # ----------------------------------------------------------------------
1643
+ # Примеры использования
1644
+ # ----------------------------------------------------------------------
1645
+
1646
+ def example_lava_flow():
1647
+ """Пример лавового потока"""
1648
+
1649
+ print("Lava flow example...")
1650
+
1651
+ # Создаем генератор лавы
1652
+ generator = DynamicTextureGenerator3D(
1653
+ dimensions=(48, 48, 48),
1654
+ texture_type=DynamicTextureType.LAVA_FLOW,
1655
+ seed=42
1656
+ )
1657
+
1658
+ states = []
1659
+
1660
+ # Симуляция нескольких шагов
1661
+ print("Simulating lava flow...")
1662
+ for i in range(30):
1663
+ state = generator.update()
1664
+ states.append(state)
1665
+
1666
+ if i % 10 == 0:
1667
+ print(f" Step {i}, time: {state.time:.3f}")
1668
+
1669
+ print(f"Generated {len(states)} states")
1670
+
1671
+ # Визуализация
1672
+ visualizer = DynamicTextureVisualizer(render_method="slice")
1673
+
1674
+ # Рендерим последнее состояние
1675
+ last_state = states[-1]
1676
+ image = visualizer.render_state(
1677
+ last_state,
1678
+ camera_pos=(0.5, 0.5, 1.5),
1679
+ camera_target=(0.5, 0.5, 0.5),
1680
+ image_size=(512, 512)
1681
+ )
1682
+
1683
+ print(f"Rendered image shape: {image.shape}")
1684
+
1685
+ return states, image
1686
+
1687
+ def example_water_flow():
1688
+ """Пример течения воды"""
1689
+
1690
+ print("\nWater flow example...")
1691
+
1692
+ generator = DynamicTextureGenerator3D(
1693
+ dimensions=(64, 32, 64), # Шире, но ниже (как река)
1694
+ texture_type=DynamicTextureType.WATER_FLOW,
1695
+ seed=123
1696
+ )
1697
+
1698
+ states = []
1699
+
1700
+ # Симуляция
1701
+ print("Simulating water flow...")
1702
+ for i in range(50):
1703
+ state = generator.update()
1704
+ states.append(state)
1705
+
1706
+ print(f"Generated {len(states)} states")
1707
+
1708
+ # Анимация
1709
+ visualizer = DynamicTextureVisualizer(render_method="raycast")
1710
+
1711
+ # Рендерим несколько кадров
1712
+ frames = []
1713
+ for i in range(0, len(states), 5):
1714
+ frame = visualizer.render_state(
1715
+ states[i],
1716
+ camera_pos=(0.5, 0.7, 1.8),
1717
+ camera_target=(0.5, 0.3, 0.2),
1718
+ image_size=(256, 256)
1719
+ )
1720
+ frames.append(frame)
1721
+
1722
+ print(f"Rendered {len(frames)} frames")
1723
+
1724
+ return states, frames
1725
+
1726
+ def example_fire_simulation():
1727
+ """Пример симуляции огня"""
1728
+
1729
+ print("\nFire simulation example...")
1730
+
1731
+ generator = DynamicTextureGenerator3D(
1732
+ dimensions=(32, 48, 32),
1733
+ texture_type=DynamicTextureType.FIRE,
1734
+ seed=456
1735
+ )
1736
+
1737
+ states = []
1738
+
1739
+ # Быстрая симуляция огня
1740
+ print("Simulating fire...")
1741
+ for i in range(40):
1742
+ state = generator.update(dt=0.02) # Больший шаг для скорости
1743
+ states.append(state)
1744
+
1745
+ print(f"Generated {len(states)} states")
1746
+
1747
+ # Визуализация с подсветкой
1748
+ visualizer = DynamicTextureVisualizer(render_method="raycast")
1749
+
1750
+ images = []
1751
+ for i in range(0, len(states), 2):
1752
+ img = visualizer.render_state(
1753
+ states[i],
1754
+ camera_pos=(0.5, 0.3, 1.0),
1755
+ camera_target=(0.5, 0.2, 0.0),
1756
+ image_size=(256, 256)
1757
+ )
1758
+ images.append(img)
1759
+
1760
+ print(f"Rendered {len(images)} fire frames")
1761
+
1762
+ return states, images
1763
+
1764
+ def example_streaming_system():
1765
+ """Пример потоковой системы динамических текстур"""
1766
+
1767
+ print("\nStreaming dynamic textures system example...")
1768
+
1769
+ # Создаем потоковую систему
1770
+ streamer = StreamingDynamicTextures(
1771
+ chunk_size=(32, 32, 32),
1772
+ max_active_chunks=4,
1773
+ physics_params=PhysicsParameters(
1774
+ viscosity=0.01,
1775
+ diffusion_rate=0.02,
1776
+ time_step=0.01
1777
+ )
1778
+ )
1779
+
1780
+ # Запрашиваем несколько чанков с водой
1781
+ water_chunks = [(0, 0, 0), (1, 0, 0), (0, 1, 0)]
1782
+
1783
+ print("Requesting water chunks...")
1784
+ states = []
1785
+
1786
+ for coords in water_chunks:
1787
+ state = streamer.request_chunk(
1788
+ coords,
1789
+ DynamicTextureType.WATER_FLOW,
1790
+ priority=1.0
1791
+ )
1792
+
1793
+ if state is not None:
1794
+ states.append((coords, state))
1795
+ print(f" Chunk {coords}: ready")
1796
+ else:
1797
+ print(f" Chunk {coords}: generating...")
1798
+
1799
+ # Обновляем систему несколько раз
1800
+ print("\nUpdating streaming system...")
1801
+ for i in range(10):
1802
+ streamer.update_all(dt=0.01)
1803
+
1804
+ if i % 2 == 0:
1805
+ stats = streamer.get_stats()
1806
+ print(f" Step {i}: {stats['active_chunks']} active chunks")
1807
+
1808
+ # Проверяем чанки после обновления
1809
+ print("\nChecking chunks after updates...")
1810
+ for coords in water_chunks:
1811
+ state = streamer.request_chunk(coords, DynamicTextureType.WATER_FLOW)
1812
+ if state is not None:
1813
+ print(f" Chunk {coords}: ready (time: {state.time:.3f})")
1814
+
1815
+ stats = streamer.get_stats()
1816
+ print(f"\nFinal stats: {stats}")
1817
+
1818
+ return streamer, states
1819
+
1820
+ def example_cloud_drift():
1821
+ """Пример дрейфа облаков"""
1822
+
1823
+ print("\nCloud drift example...")
1824
+
1825
+ generator = DynamicTextureGenerator3D(
1826
+ dimensions=(64, 32, 64),
1827
+ texture_type=DynamicTextureType.CLOUD_DRIFT,
1828
+ seed=789
1829
+ )
1830
+
1831
+ states = []
1832
+
1833
+ print("Simulating cloud drift...")
1834
+ for i in range(60):
1835
+ state = generator.update()
1836
+ states.append(state)
1837
+
1838
+ print(f"Generated {len(states)} cloud states")
1839
+
1840
+ # Визуализация с неба
1841
+ visualizer = DynamicTextureVisualizer(render_method="raycast")
1842
+
1843
+ images = []
1844
+ for i in range(0, len(states), 3):
1845
+ img = visualizer.render_state(
1846
+ states[i],
1847
+ camera_pos=(0.5, 0.8, 1.5), # Смотрим сверху вниз
1848
+ camera_target=(0.5, 0.2, 0.5),
1849
+ image_size=(512, 256)
1850
+ )
1851
+ images.append(img)
1852
+
1853
+ print(f"Rendered {len(images)} cloud frames")
1854
+
1855
+ return states, images
1856
+
1857
+ if __name__ == "__main__":
1858
+ print("Dynamic 3D Textures System")
1859
+ print("=" * 60)
1860
+
1861
+ # Пример 1: Лавовые потоки
1862
+ lava_states, lava_image = example_lava_flow()
1863
+
1864
+ # Пример 2: Течение воды
1865
+ water_states, water_frames = example_water_flow()
1866
+
1867
+ # Пример 3: Огонь
1868
+ fire_states, fire_images = example_fire_simulation()
1869
+
1870
+ # Пример 4: Потоковая система
1871
+ streamer, streamed_states = example_streaming_system()
1872
+
1873
+ # Пример 5: Дрейф облаков
1874
+ cloud_states, cloud_images = example_cloud_drift()
1875
+
1876
+ print("\n" + "=" * 60)
1877
+ print("Dynamic 3D Textures Features:")
1878
+ print("-" * 40)
1879
+ print("1. Physics-based simulation (Navier-Stokes)")
1880
+ print("2. Multiple dynamic texture types:")
1881
+ print(" - Lava flows with temperature")
1882
+ print(" - Water flow with obstacles")
1883
+ print(" - Fire with turbulence")
1884
+ print(" - Smoke plumes with buoyancy")
1885
+ print(" - Cloud drift with wind")
1886
+ print("3. Streaming system for large worlds")
1887
+ print("4. Real-time visualization methods")
1888
+ print("5. Optimized with Numba JIT compilation")
1889
+
1890
+ print("\nPerformance considerations:")
1891
+ print("- Smaller volumes for real-time (32^3 - 64^3)")
1892
+ print("- Adjust time step for stability")
1893
+ print("- Use simplified physics when possible")
1894
+ print("- Implement level-of-detail for distant effects")
1895
+ print("- Consider GPU acceleration for production")
1896
+
1897
+ print("\nIntegration with game engine:")
1898
+ print("""
1899
+ # Пример интеграции
1900
+ class GameDynamicTextures:
1901
+ def __init__(self):
1902
+ self.streamer = StreamingDynamicTextures(
1903
+ chunk_size=(32, 32, 32),
1904
+ max_active_chunks=16
1905
+ )
1906
+
1907
+ def update(self, dt, player_position):
1908
+ # Обновляем чанки рядом с игроком
1909
+ player_chunk = self._world_to_chunk(player_position)
1910
+
1911
+ for dx in range(-2, 3):
1912
+ for dy in range(-1, 2):
1913
+ for dz in range(-2, 3):
1914
+ chunk_coords = (
1915
+ player_chunk[0] + dx,
1916
+ player_chunk[1] + dy,
1917
+ player_chunk[2] + dz
1918
+ )
1919
+
1920
+ # Запрашиваем чанк
1921
+ priority = 1.0 / (dx*dx + dy*dy + dz*dz + 1)
1922
+ state = self.streamer.request_chunk(
1923
+ chunk_coords,
1924
+ DynamicTextureType.WATER_FLOW,
1925
+ priority
1926
+ )
1927
+
1928
+ if state:
1929
+ self._render_chunk(chunk_coords, state)
1930
+
1931
+ # Обновляем симуляцию
1932
+ self.streamer.update_all(dt)
1933
+ """)
1934
+
1935
+ print("\nDynamic 3D textures system ready for interactive worlds!")