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