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,1263 @@
|
|
|
1
|
+
# fractex/volume_scattering.py
|
|
2
|
+
"""
|
|
3
|
+
Система объемного рассеяния света (Volume Light Scattering)
|
|
4
|
+
Поддержка атмосферного рассеяния, подводного рассеяния, свечения частиц
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from typing import Dict, List, Tuple, Optional, Union, Callable
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from numba import jit, prange, vectorize, float32, float64, int32, int64
|
|
11
|
+
import warnings
|
|
12
|
+
import math
|
|
13
|
+
from enum import Enum
|
|
14
|
+
|
|
15
|
+
# ----------------------------------------------------------------------
|
|
16
|
+
# Константы и типы данных
|
|
17
|
+
# ----------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
class ScatteringType(Enum):
|
|
20
|
+
"""Типы рассеяния света"""
|
|
21
|
+
RAYLEIGH = 1 # Релеевское рассеяние (атмосфера, синее небо)
|
|
22
|
+
MIE = 2 # Рассеяние Ми (облака, туман, подводная взвесь)
|
|
23
|
+
HENYEY_GREENSTEIN = 3 # Анизотропное рассеяние (для объемных материалов)
|
|
24
|
+
ISOTROPIC = 4 # Изотропное рассеяние (равномерное во все стороны)
|
|
25
|
+
PHASE_FUNCTION = 5 # Кастомная фазовая функция
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class MediumProperties:
|
|
29
|
+
"""Свойства среды для рассеяния света"""
|
|
30
|
+
scattering_coefficient: float # Коэффициент рассеяния (σ_s)
|
|
31
|
+
absorption_coefficient: float # Коэффициент поглощения (σ_a)
|
|
32
|
+
extinction_coefficient: float = 0.0 # σ_t = σ_s + σ_a
|
|
33
|
+
scattering_albedo: float = 0.0 # ω = σ_s / σ_t
|
|
34
|
+
phase_function_g: float = 0.0 # Параметр асимметрии для HG (-1 до 1)
|
|
35
|
+
density: float = 1.0 # Плотность среды (0-1)
|
|
36
|
+
color: Tuple[float, float, float] = (1.0, 1.0, 1.0) # Цвет рассеяния
|
|
37
|
+
|
|
38
|
+
def __post_init__(self):
|
|
39
|
+
"""Вычисление производных параметров"""
|
|
40
|
+
self.extinction_coefficient = (self.scattering_coefficient +
|
|
41
|
+
self.absorption_coefficient)
|
|
42
|
+
if self.extinction_coefficient > 0:
|
|
43
|
+
self.scattering_albedo = (self.scattering_coefficient /
|
|
44
|
+
self.extinction_coefficient)
|
|
45
|
+
else:
|
|
46
|
+
self.scattering_albedo = 0.0
|
|
47
|
+
|
|
48
|
+
class LightSource:
|
|
49
|
+
"""Источник света для объемного рассеяния"""
|
|
50
|
+
|
|
51
|
+
def __init__(self,
|
|
52
|
+
position: Tuple[float, float, float] = (0, 0, 0),
|
|
53
|
+
direction: Tuple[float, float, float] = (0, -1, 0),
|
|
54
|
+
color: Tuple[float, float, float] = (1.0, 1.0, 1.0),
|
|
55
|
+
intensity: float = 1.0,
|
|
56
|
+
light_type: str = "directional"): # "directional", "point", "spot"
|
|
57
|
+
|
|
58
|
+
self.position = np.array(position, dtype=np.float32)
|
|
59
|
+
self.direction = np.array(direction, dtype=np.float32)
|
|
60
|
+
self.direction = self.direction / np.linalg.norm(self.direction)
|
|
61
|
+
self.color = np.array(color, dtype=np.float32)
|
|
62
|
+
self.intensity = intensity
|
|
63
|
+
self.light_type = light_type
|
|
64
|
+
|
|
65
|
+
# Для точечного и прожекторного света
|
|
66
|
+
self.radius = 0.0 # Радиус источника (для soft shadows)
|
|
67
|
+
self.attenuation = (1.0, 0.0, 0.0) # Постоянная, линейная, квадратичная
|
|
68
|
+
|
|
69
|
+
# ----------------------------------------------------------------------
|
|
70
|
+
# Фазовые функции рассеяния (оптимизированные с Numba)
|
|
71
|
+
# ----------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
@jit(nopython=True, cache=True)
|
|
74
|
+
def phase_function_isotropic(cos_theta: float) -> float:
|
|
75
|
+
"""Изотропная фазовая функция (рассеяние равномерно во все стороны)"""
|
|
76
|
+
return 1.0 / (4.0 * np.pi)
|
|
77
|
+
|
|
78
|
+
@jit(nopython=True, cache=True)
|
|
79
|
+
def phase_function_rayleigh(cos_theta: float) -> float:
|
|
80
|
+
"""Релеевская фазовая функция (рассеяние на малых частицах, атмосфера)"""
|
|
81
|
+
return (3.0 / (16.0 * np.pi)) * (1.0 + cos_theta * cos_theta)
|
|
82
|
+
|
|
83
|
+
@jit(nopython=True, cache=True)
|
|
84
|
+
def phase_function_henyey_greenstein(cos_theta: float, g: float) -> float:
|
|
85
|
+
"""
|
|
86
|
+
Фазовая функция Хеньи-Гринстейна для анизотропного рассеяния
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
cos_theta: Косинус угла между направлением луча и света
|
|
90
|
+
g: Параметр асимметрии (-1: назад, 0: изотропно, 1: вперед)
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Значение фазовой функции
|
|
94
|
+
"""
|
|
95
|
+
g2 = g * g
|
|
96
|
+
denominator = 1.0 + g2 - 2.0 * g * cos_theta
|
|
97
|
+
if denominator <= 0:
|
|
98
|
+
return 0.0
|
|
99
|
+
return (1.0 - g2) / (4.0 * np.pi * np.power(denominator, 1.5))
|
|
100
|
+
|
|
101
|
+
@jit(nopython=True, cache=True)
|
|
102
|
+
def phase_function_mie(cos_theta: float, g: float = 0.76) -> float:
|
|
103
|
+
"""Фазовая функция Ми (для крупных частиц, облаков)"""
|
|
104
|
+
# Используем HG как аппроксимацию для Ми
|
|
105
|
+
return phase_function_henyey_greenstein(cos_theta, g)
|
|
106
|
+
|
|
107
|
+
@jit(nopython=True, cache=True)
|
|
108
|
+
def schlick_phase_function(cos_theta: float, g: float) -> float:
|
|
109
|
+
"""
|
|
110
|
+
Аппроксимация Шлика для фазовой функции HG
|
|
111
|
+
Быстрее вычисляется, часто используется в real-time графике
|
|
112
|
+
"""
|
|
113
|
+
k = 1.55 * g - 0.55 * g * g * g
|
|
114
|
+
return (1.0 - k * k) / (4.0 * np.pi * (1.0 + k * cos_theta) * (1.0 + k * cos_theta))
|
|
115
|
+
|
|
116
|
+
# ----------------------------------------------------------------------
|
|
117
|
+
# Функции для расчета рассеяния в среде
|
|
118
|
+
# ----------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
@jit(nopython=True, cache=True)
|
|
121
|
+
def compute_optical_depth(density: np.ndarray,
|
|
122
|
+
extinction: float,
|
|
123
|
+
step_size: float) -> float:
|
|
124
|
+
"""
|
|
125
|
+
Вычисление оптической глубины (затухание света в среде)
|
|
126
|
+
|
|
127
|
+
τ = ∫ σ_t * ρ(x) dx
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
density: Плотность вдоль пути
|
|
131
|
+
extinction: Коэффициент экстинкции
|
|
132
|
+
step_size: Длина шага
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Оптическая глубина
|
|
136
|
+
"""
|
|
137
|
+
optical_depth = 0.0
|
|
138
|
+
for i in range(len(density)):
|
|
139
|
+
optical_depth += density[i] * extinction * step_size
|
|
140
|
+
return optical_depth
|
|
141
|
+
|
|
142
|
+
@jit(nopython=True, cache=True)
|
|
143
|
+
def transmittance(optical_depth: float) -> float:
|
|
144
|
+
"""
|
|
145
|
+
Пропускание (трансмиттанс) света через среду
|
|
146
|
+
|
|
147
|
+
T = exp(-τ)
|
|
148
|
+
"""
|
|
149
|
+
return np.exp(-optical_depth)
|
|
150
|
+
|
|
151
|
+
@jit(nopython=True, cache=True)
|
|
152
|
+
def in_scattering(source_radiance: float,
|
|
153
|
+
phase_function: float,
|
|
154
|
+
scattering_coef: float,
|
|
155
|
+
density: float,
|
|
156
|
+
transmittance: float,
|
|
157
|
+
step_size: float) -> float:
|
|
158
|
+
"""
|
|
159
|
+
Расчет ин-скеттеринга (вклада рассеянного света)
|
|
160
|
+
|
|
161
|
+
L_in = σ_s * ρ * P(θ) * L_source * T * Δx
|
|
162
|
+
"""
|
|
163
|
+
return scattering_coef * density * phase_function * source_radiance * transmittance * step_size
|
|
164
|
+
|
|
165
|
+
# ----------------------------------------------------------------------
|
|
166
|
+
# Класс объемного рассеяния для рендеринга
|
|
167
|
+
# ----------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
class VolumeScatteringRenderer:
|
|
170
|
+
"""Рендерер с учетом объемного рассеяния света"""
|
|
171
|
+
|
|
172
|
+
def __init__(self,
|
|
173
|
+
volume: 'VolumeTexture3D',
|
|
174
|
+
medium: MediumProperties,
|
|
175
|
+
light_sources: List[LightSource],
|
|
176
|
+
use_multiple_scattering: bool = False,
|
|
177
|
+
num_scattering_events: int = 2):
|
|
178
|
+
|
|
179
|
+
self.volume = volume
|
|
180
|
+
self.medium = medium
|
|
181
|
+
self.light_sources = light_sources
|
|
182
|
+
self.use_multiple_scattering = use_multiple_scattering
|
|
183
|
+
self.num_scattering_events = num_scattering_events
|
|
184
|
+
|
|
185
|
+
# Кэш для ускорения расчетов
|
|
186
|
+
self.density_cache = {}
|
|
187
|
+
self.transmittance_cache = {}
|
|
188
|
+
|
|
189
|
+
# Предварительные вычисления
|
|
190
|
+
self._precompute_phase_functions()
|
|
191
|
+
|
|
192
|
+
def _precompute_phase_functions(self):
|
|
193
|
+
"""Предварительное вычисление фазовых функций для ускорения"""
|
|
194
|
+
self.phase_functions = {
|
|
195
|
+
ScatteringType.ISOTROPIC: phase_function_isotropic,
|
|
196
|
+
ScatteringType.RAYLEIGH: phase_function_rayleigh,
|
|
197
|
+
ScatteringType.MIE: lambda cos: phase_function_mie(cos, self.medium.phase_function_g),
|
|
198
|
+
ScatteringType.HENYEY_GREENSTEIN: lambda cos: phase_function_henyey_greenstein(cos, self.medium.phase_function_g),
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
def render_single_scattering(self,
|
|
202
|
+
camera_pos: np.ndarray,
|
|
203
|
+
ray_dir: np.ndarray,
|
|
204
|
+
max_steps: int = 256,
|
|
205
|
+
step_size: float = 0.005) -> np.ndarray:
|
|
206
|
+
"""
|
|
207
|
+
Рендеринг с учетом однократного рассеяния (single scattering)
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
camera_pos: Позиция камеры
|
|
211
|
+
ray_dir: Направление луча (нормализованное)
|
|
212
|
+
max_steps: Максимальное количество шагов
|
|
213
|
+
step_size: Размер шага
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Цвет с учетом рассеяния (RGB)
|
|
217
|
+
"""
|
|
218
|
+
# Инициализация
|
|
219
|
+
accumulated_color = np.zeros(3, dtype=np.float32)
|
|
220
|
+
transmittance = 1.0
|
|
221
|
+
ray_pos = camera_pos.copy()
|
|
222
|
+
|
|
223
|
+
for step in range(max_steps):
|
|
224
|
+
# Проверяем границы объема
|
|
225
|
+
if not self._is_inside_volume(ray_pos):
|
|
226
|
+
break
|
|
227
|
+
|
|
228
|
+
# Получаем плотность в текущей точке
|
|
229
|
+
density = self._sample_density(ray_pos)
|
|
230
|
+
|
|
231
|
+
if density > 0:
|
|
232
|
+
# Вычисляем вклад от каждого источника света
|
|
233
|
+
for light in self.light_sources:
|
|
234
|
+
# Пропускание от точки до источника света
|
|
235
|
+
light_transmittance = self._compute_light_transmittance(ray_pos, light)
|
|
236
|
+
|
|
237
|
+
# Косинус угла между лучом и направлением к свету
|
|
238
|
+
light_dir = self._get_light_direction(ray_pos, light)
|
|
239
|
+
cos_theta = np.dot(ray_dir, light_dir)
|
|
240
|
+
|
|
241
|
+
# Фазовая функция
|
|
242
|
+
phase = self.phase_functions[ScatteringType.HENYEY_GREENSTEIN](cos_theta)
|
|
243
|
+
|
|
244
|
+
# Вклад рассеяния
|
|
245
|
+
scattering = in_scattering(
|
|
246
|
+
source_radiance=light.intensity,
|
|
247
|
+
phase_function=phase,
|
|
248
|
+
scattering_coef=self.medium.scattering_coefficient,
|
|
249
|
+
density=density,
|
|
250
|
+
transmittance=transmittance * light_transmittance,
|
|
251
|
+
step_size=step_size
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# Учитываем цвет источника и среды
|
|
255
|
+
scattering_color = scattering * light.color * self.medium.color
|
|
256
|
+
accumulated_color += scattering_color
|
|
257
|
+
|
|
258
|
+
# Обновляем пропускание
|
|
259
|
+
transmittance *= np.exp(-self.medium.extinction_coefficient * density * step_size)
|
|
260
|
+
|
|
261
|
+
# Продвигаем луч
|
|
262
|
+
ray_pos += ray_dir * step_size
|
|
263
|
+
|
|
264
|
+
# Если почти непрозрачно, останавливаемся
|
|
265
|
+
if transmittance < 0.01:
|
|
266
|
+
break
|
|
267
|
+
|
|
268
|
+
return np.clip(accumulated_color, 0, 1)
|
|
269
|
+
|
|
270
|
+
def render_multiple_scattering(self,
|
|
271
|
+
camera_pos: np.ndarray,
|
|
272
|
+
ray_dir: np.ndarray,
|
|
273
|
+
max_steps: int = 128,
|
|
274
|
+
step_size: float = 0.01) -> np.ndarray:
|
|
275
|
+
"""
|
|
276
|
+
Рендеринг с учетом многократного рассеяния (multiple scattering)
|
|
277
|
+
Использует упрощенный алгоритм для real-time
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
camera_pos: Позиция камеры
|
|
281
|
+
ray_dir: Направление луча
|
|
282
|
+
max_steps: Шаги для первичного луча
|
|
283
|
+
step_size: Размер шага
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
Цвет с учетом многократного рассеяния
|
|
287
|
+
"""
|
|
288
|
+
# Шаг 1: Однократное рассеяние (прямое освещение)
|
|
289
|
+
direct_scattering = self.render_single_scattering(
|
|
290
|
+
camera_pos, ray_dir, max_steps, step_size
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
if not self.use_multiple_scattering:
|
|
294
|
+
return direct_scattering
|
|
295
|
+
|
|
296
|
+
# Шаг 2: Многократное рассеяние (упрощенное)
|
|
297
|
+
# Используем аппроксимацию диффузного рассеяния
|
|
298
|
+
indirect_scattering = self._compute_indirect_scattering(
|
|
299
|
+
camera_pos, ray_dir, max_steps, step_size
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
# Комбинируем прямой и рассеянный свет
|
|
303
|
+
return np.clip(direct_scattering + indirect_scattering * 0.5, 0, 1)
|
|
304
|
+
|
|
305
|
+
def _compute_indirect_scattering(self,
|
|
306
|
+
camera_pos: np.ndarray,
|
|
307
|
+
ray_dir: np.ndarray,
|
|
308
|
+
max_steps: int,
|
|
309
|
+
step_size: float) -> np.ndarray:
|
|
310
|
+
"""
|
|
311
|
+
Упрощенное вычисление многократного рассеяния
|
|
312
|
+
(диффузная аппроксимация)
|
|
313
|
+
"""
|
|
314
|
+
accumulated_color = np.zeros(3, dtype=np.float32)
|
|
315
|
+
ray_pos = camera_pos.copy()
|
|
316
|
+
|
|
317
|
+
# Собираем плотности вдоль луча
|
|
318
|
+
densities = []
|
|
319
|
+
positions = []
|
|
320
|
+
|
|
321
|
+
for step in range(max_steps):
|
|
322
|
+
if not self._is_inside_volume(ray_pos):
|
|
323
|
+
break
|
|
324
|
+
|
|
325
|
+
density = self._sample_density(ray_pos)
|
|
326
|
+
if density > 0:
|
|
327
|
+
densities.append(density)
|
|
328
|
+
positions.append(ray_pos.copy())
|
|
329
|
+
|
|
330
|
+
ray_pos += ray_dir * step_size
|
|
331
|
+
|
|
332
|
+
if not densities:
|
|
333
|
+
return accumulated_color
|
|
334
|
+
|
|
335
|
+
# Для каждой точки с ненулевой плотностью
|
|
336
|
+
for i, (pos, density) in enumerate(zip(positions, densities)):
|
|
337
|
+
# Оцениваем рассеянный свет от соседних областей
|
|
338
|
+
# Упрощенная модель: считаем, что свет равномерно рассеивается
|
|
339
|
+
local_scattering = 0.0
|
|
340
|
+
|
|
341
|
+
# Смотрим на соседние точки
|
|
342
|
+
for j, (other_pos, other_density) in enumerate(zip(positions, densities)):
|
|
343
|
+
if i == j:
|
|
344
|
+
continue
|
|
345
|
+
|
|
346
|
+
# Расстояние между точками
|
|
347
|
+
dist = np.linalg.norm(pos - other_pos)
|
|
348
|
+
if dist < 0.1: # Только близкие точки
|
|
349
|
+
# Упрощенный вклад рассеяния
|
|
350
|
+
phase = phase_function_isotropic(1.0) # Изотропное
|
|
351
|
+
attenuation = np.exp(-self.medium.extinction_coefficient * dist)
|
|
352
|
+
local_scattering += (other_density * phase * attenuation)
|
|
353
|
+
|
|
354
|
+
# Нормализуем
|
|
355
|
+
if len(densities) > 1:
|
|
356
|
+
local_scattering /= (len(densities) - 1)
|
|
357
|
+
|
|
358
|
+
# Учитываем в общем цвете
|
|
359
|
+
scattering_strength = density * self.medium.scattering_coefficient
|
|
360
|
+
accumulated_color += scattering_strength * local_scattering * self.medium.color
|
|
361
|
+
|
|
362
|
+
return accumulated_color / len(densities)
|
|
363
|
+
|
|
364
|
+
def _sample_density(self, position: np.ndarray) -> float:
|
|
365
|
+
"""Выборка плотности из объемной текстуры"""
|
|
366
|
+
# Нормализуем координаты к [0, 1]
|
|
367
|
+
x = np.clip(position[0], 0, 1)
|
|
368
|
+
y = np.clip(position[1], 0, 1)
|
|
369
|
+
z = np.clip(position[2], 0, 1)
|
|
370
|
+
|
|
371
|
+
# Используем кэш для ускорения
|
|
372
|
+
cache_key = (int(x * 100), int(y * 100), int(z * 100))
|
|
373
|
+
if cache_key in self.density_cache:
|
|
374
|
+
return self.density_cache[cache_key]
|
|
375
|
+
|
|
376
|
+
# Трилинейная интерполяция
|
|
377
|
+
depth, height, width = self.volume.dimensions
|
|
378
|
+
|
|
379
|
+
fx = x * (width - 1)
|
|
380
|
+
fy = y * (height - 1)
|
|
381
|
+
fz = z * (depth - 1)
|
|
382
|
+
|
|
383
|
+
ix0 = int(np.floor(fx))
|
|
384
|
+
iy0 = int(np.floor(fy))
|
|
385
|
+
iz0 = int(np.floor(fz))
|
|
386
|
+
|
|
387
|
+
ix1 = min(ix0 + 1, width - 1)
|
|
388
|
+
iy1 = min(iy0 + 1, height - 1)
|
|
389
|
+
iz1 = min(iz0 + 1, depth - 1)
|
|
390
|
+
|
|
391
|
+
dx = fx - ix0
|
|
392
|
+
dy = fy - iy0
|
|
393
|
+
dz = fz - iz0
|
|
394
|
+
|
|
395
|
+
# Берем первый канал как плотность
|
|
396
|
+
c000 = self.volume.data[iz0, iy0, ix0, 0]
|
|
397
|
+
c001 = self.volume.data[iz0, iy0, ix1, 0]
|
|
398
|
+
c010 = self.volume.data[iz0, iy1, ix0, 0]
|
|
399
|
+
c011 = self.volume.data[iz0, iy1, ix1, 0]
|
|
400
|
+
c100 = self.volume.data[iz1, iy0, ix0, 0]
|
|
401
|
+
c101 = self.volume.data[iz1, iy0, ix1, 0]
|
|
402
|
+
c110 = self.volume.data[iz1, iy1, ix0, 0]
|
|
403
|
+
c111 = self.volume.data[iz1, iy1, ix1, 0]
|
|
404
|
+
|
|
405
|
+
# Трилинейная интерполяция
|
|
406
|
+
c00 = c000 * (1 - dx) + c001 * dx
|
|
407
|
+
c01 = c010 * (1 - dx) + c011 * dx
|
|
408
|
+
c10 = c100 * (1 - dx) + c101 * dx
|
|
409
|
+
c11 = c110 * (1 - dx) + c111 * dx
|
|
410
|
+
|
|
411
|
+
c0 = c00 * (1 - dy) + c01 * dy
|
|
412
|
+
c1 = c10 * (1 - dy) + c11 * dy
|
|
413
|
+
|
|
414
|
+
density = c0 * (1 - dz) + c1 * dz
|
|
415
|
+
|
|
416
|
+
# Кэшируем
|
|
417
|
+
self.density_cache[cache_key] = density
|
|
418
|
+
if len(self.density_cache) > 10000:
|
|
419
|
+
self.density_cache.pop(next(iter(self.density_cache)))
|
|
420
|
+
|
|
421
|
+
return density
|
|
422
|
+
|
|
423
|
+
def _is_inside_volume(self, position: np.ndarray) -> bool:
|
|
424
|
+
"""Проверка, находится ли точка внутри объема"""
|
|
425
|
+
return (0 <= position[0] <= 1 and
|
|
426
|
+
0 <= position[1] <= 1 and
|
|
427
|
+
0 <= position[2] <= 1)
|
|
428
|
+
|
|
429
|
+
def _get_light_direction(self, position: np.ndarray, light: LightSource) -> np.ndarray:
|
|
430
|
+
"""Получение направления к источнику света"""
|
|
431
|
+
if light.light_type == "directional":
|
|
432
|
+
return -light.direction # Источник бесконечно далеко
|
|
433
|
+
|
|
434
|
+
# Для точечного источника
|
|
435
|
+
light_dir = light.position - position
|
|
436
|
+
return light_dir / np.linalg.norm(light_dir)
|
|
437
|
+
|
|
438
|
+
def _compute_light_transmittance(self, position: np.ndarray, light: LightSource) -> float:
|
|
439
|
+
"""
|
|
440
|
+
Вычисление пропускания от точки до источника света
|
|
441
|
+
(shadow ray)
|
|
442
|
+
"""
|
|
443
|
+
if light.light_type == "directional":
|
|
444
|
+
# Для направленного света: луч в противоположном направлении
|
|
445
|
+
light_dir = -light.direction
|
|
446
|
+
ray_pos = position.copy()
|
|
447
|
+
optical_depth = 0.0
|
|
448
|
+
|
|
449
|
+
# Идем до границы объема
|
|
450
|
+
for _ in range(64): # Ограниченное количество шагов
|
|
451
|
+
ray_pos += light_dir * 0.01
|
|
452
|
+
if not self._is_inside_volume(ray_pos):
|
|
453
|
+
break
|
|
454
|
+
|
|
455
|
+
density = self._sample_density(ray_pos)
|
|
456
|
+
optical_depth += density * self.medium.extinction_coefficient * 0.01
|
|
457
|
+
|
|
458
|
+
else:
|
|
459
|
+
# Для точечного источника
|
|
460
|
+
light_dir = self._get_light_direction(position, light)
|
|
461
|
+
distance = np.linalg.norm(light.position - position)
|
|
462
|
+
ray_pos = position.copy()
|
|
463
|
+
step_size = distance / 64
|
|
464
|
+
optical_depth = 0.0
|
|
465
|
+
|
|
466
|
+
for i in range(64):
|
|
467
|
+
ray_pos += light_dir * step_size
|
|
468
|
+
if not self._is_inside_volume(ray_pos):
|
|
469
|
+
break
|
|
470
|
+
|
|
471
|
+
density = self._sample_density(ray_pos)
|
|
472
|
+
optical_depth += density * self.medium.extinction_coefficient * step_size
|
|
473
|
+
|
|
474
|
+
return transmittance(optical_depth)
|
|
475
|
+
|
|
476
|
+
def render_volumetric_light(self,
|
|
477
|
+
camera_pos: Tuple[float, float, float],
|
|
478
|
+
camera_target: Tuple[float, float, float],
|
|
479
|
+
image_size: Tuple[int, int],
|
|
480
|
+
max_steps: int = 128,
|
|
481
|
+
step_size: float = 0.01) -> np.ndarray:
|
|
482
|
+
"""
|
|
483
|
+
Рендеринг объема с учетом рассеяния света (вей освещения)
|
|
484
|
+
|
|
485
|
+
Args:
|
|
486
|
+
camera_pos: Позиция камеры
|
|
487
|
+
camera_target: Цель камеры
|
|
488
|
+
image_size: Размер выходного изображения
|
|
489
|
+
max_steps: Максимальное количество шагов луча
|
|
490
|
+
step_size: Размер шага
|
|
491
|
+
|
|
492
|
+
Returns:
|
|
493
|
+
2D изображение (H, W, 3) RGB
|
|
494
|
+
"""
|
|
495
|
+
width, height = image_size
|
|
496
|
+
image = np.zeros((height, width, 3), dtype=np.float32)
|
|
497
|
+
|
|
498
|
+
# Вычисляем базис камеры
|
|
499
|
+
camera_dir = np.array(camera_target) - np.array(camera_pos)
|
|
500
|
+
camera_dir = camera_dir / np.linalg.norm(camera_dir)
|
|
501
|
+
|
|
502
|
+
up = np.array([0.0, 1.0, 0.0])
|
|
503
|
+
right = np.cross(camera_dir, up)
|
|
504
|
+
right = right / np.linalg.norm(right)
|
|
505
|
+
up = np.cross(right, camera_dir)
|
|
506
|
+
|
|
507
|
+
# FOV
|
|
508
|
+
fov = 60.0
|
|
509
|
+
aspect = width / height
|
|
510
|
+
half_height = np.tan(np.radians(fov) / 2.0)
|
|
511
|
+
half_width = aspect * half_height
|
|
512
|
+
|
|
513
|
+
print(f"Rendering volumetric light {width}x{height}...")
|
|
514
|
+
|
|
515
|
+
# Для каждого пикселя
|
|
516
|
+
for y in prange(height):
|
|
517
|
+
for x in range(width):
|
|
518
|
+
# Вычисляем направление луча
|
|
519
|
+
u = (2.0 * x / width - 1.0) * half_width
|
|
520
|
+
v = (1.0 - 2.0 * y / height) * half_height
|
|
521
|
+
|
|
522
|
+
ray_dir = camera_dir + u * right + v * up
|
|
523
|
+
ray_dir = ray_dir / np.linalg.norm(ray_dir)
|
|
524
|
+
|
|
525
|
+
# Рендеринг с рассеянием
|
|
526
|
+
if self.use_multiple_scattering:
|
|
527
|
+
color = self.render_multiple_scattering(
|
|
528
|
+
np.array(camera_pos), ray_dir, max_steps, step_size
|
|
529
|
+
)
|
|
530
|
+
else:
|
|
531
|
+
color = self.render_single_scattering(
|
|
532
|
+
np.array(camera_pos), ray_dir, max_steps, step_size
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
image[y, x] = color
|
|
536
|
+
|
|
537
|
+
return np.clip(image, 0, 1)
|
|
538
|
+
|
|
539
|
+
# ----------------------------------------------------------------------
|
|
540
|
+
# Специализированные среды для разных эффектов
|
|
541
|
+
# ----------------------------------------------------------------------
|
|
542
|
+
|
|
543
|
+
class AtmosphereScattering:
|
|
544
|
+
"""Рассеяние в атмосфере (релеевское и ми)"""
|
|
545
|
+
|
|
546
|
+
# Константы для атмосферы Земли
|
|
547
|
+
RAYLEIGH_SCATTERING = np.array([5.8e-6, 1.35e-5, 3.31e-5]) # RGB коэффициенты
|
|
548
|
+
MIE_SCATTERING = 2e-5
|
|
549
|
+
RAYLEIGH_SCALE_HEIGHT = 8000.0 # метров
|
|
550
|
+
MIE_SCALE_HEIGHT = 1200.0 # метров
|
|
551
|
+
EARTH_RADIUS = 6371000.0 # метров
|
|
552
|
+
ATMOSPHERE_HEIGHT = 100000.0 # метров
|
|
553
|
+
|
|
554
|
+
def __init__(self):
|
|
555
|
+
self.sun_direction = np.array([0.0, 1.0, 0.0])
|
|
556
|
+
self.sun_intensity = 20.0
|
|
557
|
+
self.ground_albedo = 0.3
|
|
558
|
+
self.enable_ozone = True
|
|
559
|
+
|
|
560
|
+
def compute_atmosphere_scattering(self,
|
|
561
|
+
ray_origin: np.ndarray,
|
|
562
|
+
ray_direction: np.ndarray,
|
|
563
|
+
sun_direction: np.ndarray,
|
|
564
|
+
samples: int = 16) -> Tuple[np.ndarray, np.ndarray]:
|
|
565
|
+
"""
|
|
566
|
+
Вычисление рассеяния в атмосфере (упрощенная модель)
|
|
567
|
+
|
|
568
|
+
Returns:
|
|
569
|
+
(цвет рассеяния, цвет вторичного рассеяния)
|
|
570
|
+
"""
|
|
571
|
+
# Преобразуем в локальные координаты (высота над Землей)
|
|
572
|
+
ray_height = np.linalg.norm(ray_origin) - self.EARTH_RADIUS
|
|
573
|
+
ray_height = max(ray_height, 0.0)
|
|
574
|
+
|
|
575
|
+
# Инициализация
|
|
576
|
+
total_rayleigh = np.zeros(3, dtype=np.float32)
|
|
577
|
+
total_mie = np.zeros(3, dtype=np.float32)
|
|
578
|
+
optical_depth_rayleigh = 0.0
|
|
579
|
+
optical_depth_mie = 0.0
|
|
580
|
+
|
|
581
|
+
# Интегрируем вдоль луча
|
|
582
|
+
step_size = min(self.ATMOSPHERE_HEIGHT / samples, 1000.0)
|
|
583
|
+
current_pos = ray_origin.copy()
|
|
584
|
+
|
|
585
|
+
for i in range(samples):
|
|
586
|
+
# Высота текущей точки
|
|
587
|
+
height = np.linalg.norm(current_pos) - self.EARTH_RADIUS
|
|
588
|
+
|
|
589
|
+
if height < 0 or height > self.ATMOSPHERE_HEIGHT:
|
|
590
|
+
break
|
|
591
|
+
|
|
592
|
+
# Плотность на этой высоте (экспоненциальное убывание)
|
|
593
|
+
rayleigh_density = np.exp(-height / self.RAYLEIGH_SCALE_HEIGHT)
|
|
594
|
+
mie_density = np.exp(-height / self.MIE_SCALE_HEIGHT)
|
|
595
|
+
|
|
596
|
+
# Оптическая глубина
|
|
597
|
+
optical_depth_rayleigh += rayleigh_density * step_size
|
|
598
|
+
optical_depth_mie += mie_density * step_size
|
|
599
|
+
|
|
600
|
+
# Рассеяние к солнцу от этой точки
|
|
601
|
+
sun_transmittance_rayleigh = np.exp(-optical_depth_rayleigh * self.RAYLEIGH_SCATTERING)
|
|
602
|
+
sun_transmittance_mie = np.exp(-optical_depth_mie * self.MIE_SCATTERING)
|
|
603
|
+
|
|
604
|
+
# Фазовые функции
|
|
605
|
+
cos_theta = np.dot(ray_direction, sun_direction)
|
|
606
|
+
phase_rayleigh = phase_function_rayleigh(cos_theta)
|
|
607
|
+
phase_mie = phase_function_mie(cos_theta, g=0.76)
|
|
608
|
+
|
|
609
|
+
# Вклад рассеяния
|
|
610
|
+
light_path = sun_transmittance_rayleigh * sun_transmittance_mie
|
|
611
|
+
total_rayleigh += rayleigh_density * phase_rayleigh * light_path * step_size
|
|
612
|
+
total_mie += mie_density * phase_mie * light_path * step_size
|
|
613
|
+
|
|
614
|
+
# Двигаем луч
|
|
615
|
+
current_pos += ray_direction * step_size
|
|
616
|
+
|
|
617
|
+
# Учитываем интенсивность солнца
|
|
618
|
+
rayleigh_color = total_rayleigh * self.RAYLEIGH_SCATTERING * self.sun_intensity
|
|
619
|
+
mie_color = total_mie * self.MIE_SCATTERING * self.sun_intensity
|
|
620
|
+
|
|
621
|
+
# Цвет неба (комбинация релеевского и ми)
|
|
622
|
+
sky_color = rayleigh_color + mie_color
|
|
623
|
+
|
|
624
|
+
# Вторичное рассеяние (упрощенное)
|
|
625
|
+
secondary_scattering = sky_color * 0.5 * self.ground_albedo
|
|
626
|
+
|
|
627
|
+
return sky_color, secondary_scattering
|
|
628
|
+
|
|
629
|
+
def render_sky(self,
|
|
630
|
+
camera_pos: Tuple[float, float, float],
|
|
631
|
+
view_direction: Tuple[float, float, float],
|
|
632
|
+
sun_direction: Tuple[float, float, float],
|
|
633
|
+
image_size: Tuple[int, int]) -> np.ndarray:
|
|
634
|
+
"""
|
|
635
|
+
Рендеринг неба с атмосферным рассеянием
|
|
636
|
+
"""
|
|
637
|
+
width, height = image_size
|
|
638
|
+
image = np.zeros((height, width, 3), dtype=np.float32)
|
|
639
|
+
|
|
640
|
+
# Базис камеры
|
|
641
|
+
camera_dir = np.array(view_direction, dtype=np.float32)
|
|
642
|
+
camera_dir = camera_dir / np.linalg.norm(camera_dir)
|
|
643
|
+
|
|
644
|
+
up = np.array([0.0, 1.0, 0.0])
|
|
645
|
+
right = np.cross(camera_dir, up)
|
|
646
|
+
right = right / np.linalg.norm(right)
|
|
647
|
+
up = np.cross(right, camera_dir)
|
|
648
|
+
|
|
649
|
+
# FOV
|
|
650
|
+
fov = 90.0
|
|
651
|
+
aspect = width / height
|
|
652
|
+
half_height = np.tan(np.radians(fov) / 2.0)
|
|
653
|
+
half_width = aspect * half_height
|
|
654
|
+
|
|
655
|
+
# Позиция камеры в мировых координатах
|
|
656
|
+
camera_world_pos = np.array(camera_pos) + np.array([0, self.EARTH_RADIUS + 100, 0])
|
|
657
|
+
|
|
658
|
+
for y in range(height):
|
|
659
|
+
for x in range(width):
|
|
660
|
+
# Направление луча
|
|
661
|
+
u = (2.0 * x / width - 1.0) * half_width
|
|
662
|
+
v = (1.0 - 2.0 * y / height) * half_height
|
|
663
|
+
|
|
664
|
+
ray_dir = camera_dir + u * right + v * up
|
|
665
|
+
ray_dir = ray_dir / np.linalg.norm(ray_dir)
|
|
666
|
+
|
|
667
|
+
# Вычисляем рассеяние
|
|
668
|
+
sky_color, secondary = self.compute_atmosphere_scattering(
|
|
669
|
+
camera_world_pos, ray_dir, sun_direction
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
# Комбинируем
|
|
673
|
+
pixel_color = sky_color + secondary
|
|
674
|
+
|
|
675
|
+
# Добавляем солнце
|
|
676
|
+
sun_angle = np.dot(ray_dir, sun_direction)
|
|
677
|
+
if sun_angle > 0.9995:
|
|
678
|
+
# Диск солнца
|
|
679
|
+
sun_size = 0.01
|
|
680
|
+
if sun_angle > 1.0 - sun_size:
|
|
681
|
+
sun_intensity = 10.0
|
|
682
|
+
pixel_color = np.array([1.0, 0.9, 0.8]) * sun_intensity
|
|
683
|
+
|
|
684
|
+
image[y, x] = np.clip(pixel_color, 0, 1)
|
|
685
|
+
|
|
686
|
+
return image
|
|
687
|
+
|
|
688
|
+
class UnderwaterScattering:
|
|
689
|
+
"""Рассеяние света под водой"""
|
|
690
|
+
|
|
691
|
+
def __init__(self):
|
|
692
|
+
# Свойства воды
|
|
693
|
+
self.water_color = np.array([0.0, 0.4, 0.8]) # Синий цвет воды
|
|
694
|
+
self.scattering_coefficient = 0.1
|
|
695
|
+
self.absorption_coefficient = 0.05
|
|
696
|
+
self.density = 0.5
|
|
697
|
+
self.phase_function_g = 0.8 # Сильное рассеяние вперед
|
|
698
|
+
|
|
699
|
+
# Источники света
|
|
700
|
+
self.sun_direction = np.array([0.0, 1.0, 0.0])
|
|
701
|
+
self.sun_color = np.array([1.0, 0.9, 0.7])
|
|
702
|
+
self.ambient_light = np.array([0.1, 0.2, 0.3])
|
|
703
|
+
|
|
704
|
+
# Каустика
|
|
705
|
+
self.enable_caustics = True
|
|
706
|
+
self.caustics_intensity = 0.5
|
|
707
|
+
|
|
708
|
+
def render_underwater(self,
|
|
709
|
+
camera_pos: Tuple[float, float, float],
|
|
710
|
+
view_direction: Tuple[float, float, float],
|
|
711
|
+
water_surface_height: float = 0.0,
|
|
712
|
+
image_size: Tuple[int, int] = (256, 256),
|
|
713
|
+
max_depth: float = 100.0) -> np.ndarray:
|
|
714
|
+
"""
|
|
715
|
+
Рендеринг подводной сцены с учетом рассеяния
|
|
716
|
+
|
|
717
|
+
Args:
|
|
718
|
+
camera_pos: Позиция камеры (под водой)
|
|
719
|
+
view_direction: Направление взгляда
|
|
720
|
+
water_surface_height: Высота поверхности воды (Y координата)
|
|
721
|
+
image_size: Размер изображения
|
|
722
|
+
max_depth: Максимальная глубина видимости
|
|
723
|
+
|
|
724
|
+
Returns:
|
|
725
|
+
Подводное изображение с рассеянием
|
|
726
|
+
"""
|
|
727
|
+
width, height = image_size
|
|
728
|
+
image = np.zeros((height, width, 3), dtype=np.float32)
|
|
729
|
+
|
|
730
|
+
# Базис камеры
|
|
731
|
+
camera_dir = np.array(view_direction, dtype=np.float32)
|
|
732
|
+
camera_dir = camera_dir / np.linalg.norm(camera_dir)
|
|
733
|
+
|
|
734
|
+
up = np.array([0.0, 1.0, 0.0])
|
|
735
|
+
right = np.cross(camera_dir, up)
|
|
736
|
+
right = right / np.linalg.norm(right)
|
|
737
|
+
up = np.cross(right, camera_dir)
|
|
738
|
+
|
|
739
|
+
# FOV
|
|
740
|
+
fov = 70.0
|
|
741
|
+
aspect = width / height
|
|
742
|
+
half_height = np.tan(np.radians(fov) / 2.0)
|
|
743
|
+
half_width = aspect * half_height
|
|
744
|
+
|
|
745
|
+
camera_world_pos = np.array(camera_pos, dtype=np.float32)
|
|
746
|
+
|
|
747
|
+
print(f"Rendering underwater scene {width}x{height}...")
|
|
748
|
+
|
|
749
|
+
for y in range(height):
|
|
750
|
+
for x in range(width):
|
|
751
|
+
# Направление луча
|
|
752
|
+
u = (2.0 * x / width - 1.0) * half_width
|
|
753
|
+
v = (1.0 - 2.0 * y / height) * half_height
|
|
754
|
+
|
|
755
|
+
ray_dir = camera_dir + u * right + v * up
|
|
756
|
+
ray_dir = ray_dir / np.linalg.norm(ray_dir)
|
|
757
|
+
|
|
758
|
+
# Начинаем с позиции камеры
|
|
759
|
+
ray_pos = camera_world_pos.copy()
|
|
760
|
+
accumulated_color = np.zeros(3, dtype=np.float32)
|
|
761
|
+
transmittance = 1.0
|
|
762
|
+
|
|
763
|
+
# Интегрируем вдоль луча
|
|
764
|
+
step_size = 0.5
|
|
765
|
+
max_steps = int(max_depth / step_size)
|
|
766
|
+
|
|
767
|
+
for step in range(max_steps):
|
|
768
|
+
# Проверяем, не вышли ли мы из воды
|
|
769
|
+
if ray_pos[1] > water_surface_height:
|
|
770
|
+
# Мы на поверхности, добавляем цвет неба
|
|
771
|
+
sky_contribution = self._get_sky_light(ray_pos, ray_dir)
|
|
772
|
+
accumulated_color += transmittance * sky_contribution
|
|
773
|
+
break
|
|
774
|
+
|
|
775
|
+
# Расстояние от камеры
|
|
776
|
+
distance = step * step_size
|
|
777
|
+
|
|
778
|
+
# Плотность воды (может меняться с глубиной)
|
|
779
|
+
depth = water_surface_height - ray_pos[1]
|
|
780
|
+
density = self.density * (1.0 - np.exp(-depth / 10.0))
|
|
781
|
+
|
|
782
|
+
# Рассеяние от солнца
|
|
783
|
+
sun_direction_normalized = self.sun_direction / np.linalg.norm(self.sun_direction)
|
|
784
|
+
cos_theta = np.dot(ray_dir, sun_direction_normalized)
|
|
785
|
+
phase = phase_function_henyey_greenstein(cos_theta, self.phase_function_g)
|
|
786
|
+
|
|
787
|
+
# Затухание света от солнца до этой точки
|
|
788
|
+
# (упрощенно: учитываем только глубину)
|
|
789
|
+
sun_attenuation = np.exp(-depth * (self.scattering_coefficient + self.absorption_coefficient))
|
|
790
|
+
|
|
791
|
+
# Вклад рассеяния
|
|
792
|
+
scattering = (self.scattering_coefficient * density * phase *
|
|
793
|
+
self.sun_color * sun_attenuation * step_size)
|
|
794
|
+
|
|
795
|
+
# Учитываем цвет воды
|
|
796
|
+
scattering *= self.water_color
|
|
797
|
+
|
|
798
|
+
# Добавляем к накопленному цвету
|
|
799
|
+
accumulated_color += transmittance * scattering
|
|
800
|
+
|
|
801
|
+
# Обновляем пропускание
|
|
802
|
+
extinction = self.scattering_coefficient + self.absorption_coefficient
|
|
803
|
+
transmittance *= np.exp(-extinction * density * step_size)
|
|
804
|
+
|
|
805
|
+
# Двигаем луч
|
|
806
|
+
ray_pos += ray_dir * step_size
|
|
807
|
+
|
|
808
|
+
# Если почти непрозрачно, останавливаемся
|
|
809
|
+
if transmittance < 0.01:
|
|
810
|
+
break
|
|
811
|
+
|
|
812
|
+
# Добавляем каустику если включена
|
|
813
|
+
if self.enable_caustics and self.sun_direction[1] > 0:
|
|
814
|
+
caustics = self._compute_caustics(camera_world_pos, ray_dir, water_surface_height)
|
|
815
|
+
accumulated_color += caustics * self.caustics_intensity
|
|
816
|
+
|
|
817
|
+
# Добавляем ambient light
|
|
818
|
+
accumulated_color += self.ambient_light * (1.0 - transmittance)
|
|
819
|
+
|
|
820
|
+
image[y, x] = np.clip(accumulated_color, 0, 1)
|
|
821
|
+
|
|
822
|
+
return image
|
|
823
|
+
|
|
824
|
+
def _get_sky_light(self, position: np.ndarray, direction: np.ndarray) -> np.ndarray:
|
|
825
|
+
"""Получение цвета неба для подводного рендеринга"""
|
|
826
|
+
# Упрощенная модель: цвет неба зависит от направления
|
|
827
|
+
horizon_factor = max(0.0, direction[1]) # Чем выше смотрим, тем светлее
|
|
828
|
+
sky_color = np.array([0.5, 0.6, 0.8]) * (0.2 + 0.8 * horizon_factor)
|
|
829
|
+
return sky_color
|
|
830
|
+
|
|
831
|
+
def _compute_caustics(self,
|
|
832
|
+
camera_pos: np.ndarray,
|
|
833
|
+
view_dir: np.ndarray,
|
|
834
|
+
water_surface: float) -> np.ndarray:
|
|
835
|
+
"""
|
|
836
|
+
Вычисление каустики (игр света на дне под водой)
|
|
837
|
+
Упрощенная модель на основе шума
|
|
838
|
+
"""
|
|
839
|
+
# Точка пересечения с дном (упрощенно: плоскость на глубине 10м)
|
|
840
|
+
sea_floor_depth = 10.0
|
|
841
|
+
if view_dir[1] < -0.01: # Смотрим вниз
|
|
842
|
+
t = (camera_pos[1] + sea_floor_depth) / -view_dir[1]
|
|
843
|
+
floor_pos = camera_pos + view_dir * t
|
|
844
|
+
|
|
845
|
+
# Шум для каустики
|
|
846
|
+
noise = np.sin(floor_pos[0] * 10.0) * np.cos(floor_pos[2] * 10.0)
|
|
847
|
+
caustics = np.array([1.0, 1.0, 0.9]) * (noise * 0.5 + 0.5) * 0.3
|
|
848
|
+
|
|
849
|
+
# Затухание с глубиной
|
|
850
|
+
depth_factor = np.exp(-sea_floor_depth * 0.1)
|
|
851
|
+
return caustics * depth_factor
|
|
852
|
+
|
|
853
|
+
return np.zeros(3, dtype=np.float32)
|
|
854
|
+
|
|
855
|
+
# ----------------------------------------------------------------------
|
|
856
|
+
# Оптимизированные функции для real-time рендеринга
|
|
857
|
+
# ----------------------------------------------------------------------
|
|
858
|
+
|
|
859
|
+
@jit(nopython=True, parallel=True, cache=True)
|
|
860
|
+
def fast_volume_scattering_kernel(
|
|
861
|
+
width: int,
|
|
862
|
+
height: int,
|
|
863
|
+
camera_pos: np.ndarray,
|
|
864
|
+
camera_dir: np.ndarray,
|
|
865
|
+
right: np.ndarray,
|
|
866
|
+
up: np.ndarray,
|
|
867
|
+
half_width: float,
|
|
868
|
+
half_height: float,
|
|
869
|
+
light_dir: np.ndarray,
|
|
870
|
+
light_color: np.ndarray,
|
|
871
|
+
volume_data: np.ndarray,
|
|
872
|
+
scattering_coef: float,
|
|
873
|
+
absorption_coef: float,
|
|
874
|
+
phase_g: float,
|
|
875
|
+
max_steps: int,
|
|
876
|
+
step_size: float
|
|
877
|
+
) -> np.ndarray:
|
|
878
|
+
"""
|
|
879
|
+
Оптимизированное ядро для объемного рассеяния (работает на CPU)
|
|
880
|
+
|
|
881
|
+
Args:
|
|
882
|
+
Все параметры для рендеринга
|
|
883
|
+
|
|
884
|
+
Returns:
|
|
885
|
+
Изображение (H, W, 3)
|
|
886
|
+
"""
|
|
887
|
+
image = np.zeros((height, width, 3), dtype=np.float32)
|
|
888
|
+
extinction_coef = scattering_coef + absorption_coef
|
|
889
|
+
|
|
890
|
+
for y in prange(height):
|
|
891
|
+
for x in range(width):
|
|
892
|
+
# Направление луча
|
|
893
|
+
u = (2.0 * x / width - 1.0) * half_width
|
|
894
|
+
v = (1.0 - 2.0 * y / height) * half_height
|
|
895
|
+
|
|
896
|
+
ray_dir = camera_dir + u * right + v * up
|
|
897
|
+
ray_dir = ray_dir / np.linalg.norm(ray_dir)
|
|
898
|
+
|
|
899
|
+
# Рейкастинг
|
|
900
|
+
ray_pos = camera_pos.copy()
|
|
901
|
+
accumulated_color = np.zeros(3, dtype=np.float32)
|
|
902
|
+
transmittance = 1.0
|
|
903
|
+
|
|
904
|
+
for step in range(max_steps):
|
|
905
|
+
# Проверяем границы
|
|
906
|
+
if (ray_pos[0] < 0 or ray_pos[0] >= 1 or
|
|
907
|
+
ray_pos[1] < 0 or ray_pos[1] >= 1 or
|
|
908
|
+
ray_pos[2] < 0 or ray_pos[2] >= 1):
|
|
909
|
+
break
|
|
910
|
+
|
|
911
|
+
# Трилинейная интерполяция плотности
|
|
912
|
+
depth, vol_height, vol_width = volume_data.shape[:3]
|
|
913
|
+
|
|
914
|
+
fx = ray_pos[0] * (vol_width - 1)
|
|
915
|
+
fy = ray_pos[1] * (vol_height - 1)
|
|
916
|
+
fz = ray_pos[2] * (depth - 1)
|
|
917
|
+
|
|
918
|
+
ix0 = int(np.floor(fx))
|
|
919
|
+
iy0 = int(np.floor(fy))
|
|
920
|
+
iz0 = int(np.floor(fz))
|
|
921
|
+
|
|
922
|
+
ix1 = min(ix0 + 1, vol_width - 1)
|
|
923
|
+
iy1 = min(iy0 + 1, vol_height - 1)
|
|
924
|
+
iz1 = min(iz0 + 1, depth - 1)
|
|
925
|
+
|
|
926
|
+
dx = fx - ix0
|
|
927
|
+
dy = fy - iy0
|
|
928
|
+
dz = fz - iz0
|
|
929
|
+
|
|
930
|
+
# Берем плотность (первый канал)
|
|
931
|
+
c000 = volume_data[iz0, iy0, ix0, 0]
|
|
932
|
+
c001 = volume_data[iz0, iy0, ix1, 0]
|
|
933
|
+
c010 = volume_data[iz0, iy1, ix0, 0]
|
|
934
|
+
c011 = volume_data[iz0, iy1, ix1, 0]
|
|
935
|
+
c100 = volume_data[iz1, iy0, ix0, 0]
|
|
936
|
+
c101 = volume_data[iz1, iy0, ix1, 0]
|
|
937
|
+
c110 = volume_data[iz1, iy1, ix0, 0]
|
|
938
|
+
c111 = volume_data[iz1, iy1, ix1, 0]
|
|
939
|
+
|
|
940
|
+
c00 = c000 * (1 - dx) + c001 * dx
|
|
941
|
+
c01 = c010 * (1 - dx) + c011 * dx
|
|
942
|
+
c10 = c100 * (1 - dx) + c101 * dx
|
|
943
|
+
c11 = c110 * (1 - dx) + c111 * dx
|
|
944
|
+
|
|
945
|
+
c0 = c00 * (1 - dy) + c01 * dy
|
|
946
|
+
c1 = c10 * (1 - dy) + c11 * dy
|
|
947
|
+
|
|
948
|
+
density = c0 * (1 - dz) + c1 * dz
|
|
949
|
+
|
|
950
|
+
if density > 0:
|
|
951
|
+
# Фазовая функция
|
|
952
|
+
cos_theta = np.dot(ray_dir, light_dir)
|
|
953
|
+
phase = (1.0 - phase_g * phase_g) / \
|
|
954
|
+
(4.0 * np.pi * np.power(1.0 + phase_g * phase_g - 2.0 * phase_g * cos_theta, 1.5))
|
|
955
|
+
|
|
956
|
+
# Вклад рассеяния
|
|
957
|
+
scattering = (scattering_coef * density * phase *
|
|
958
|
+
light_color * transmittance * step_size)
|
|
959
|
+
|
|
960
|
+
accumulated_color += scattering
|
|
961
|
+
|
|
962
|
+
# Обновляем пропускание
|
|
963
|
+
transmittance *= np.exp(-extinction_coef * density * step_size)
|
|
964
|
+
|
|
965
|
+
# Продвигаем луч
|
|
966
|
+
ray_pos += ray_dir * step_size
|
|
967
|
+
|
|
968
|
+
# Ранний выход
|
|
969
|
+
if transmittance < 0.01:
|
|
970
|
+
break
|
|
971
|
+
|
|
972
|
+
image[y, x] = np.clip(accumulated_color, 0, 1)
|
|
973
|
+
|
|
974
|
+
return image
|
|
975
|
+
|
|
976
|
+
# ----------------------------------------------------------------------
|
|
977
|
+
# Примеры использования
|
|
978
|
+
# ----------------------------------------------------------------------
|
|
979
|
+
|
|
980
|
+
def example_atmospheric_scattering():
|
|
981
|
+
"""Пример атмосферного рассеяния (небо и облака)"""
|
|
982
|
+
|
|
983
|
+
print("Atmospheric scattering example...")
|
|
984
|
+
|
|
985
|
+
# Создаем объем облаков
|
|
986
|
+
from .volume_textures import VolumeTextureGenerator3D
|
|
987
|
+
generator = VolumeTextureGenerator3D(seed=42)
|
|
988
|
+
clouds = generator.generate_clouds_3d(
|
|
989
|
+
width=128, height=64, depth=128,
|
|
990
|
+
scale=0.02, density=0.3, detail=3
|
|
991
|
+
)
|
|
992
|
+
|
|
993
|
+
# Настройка среды (атмосфера с облаками)
|
|
994
|
+
medium = MediumProperties(
|
|
995
|
+
scattering_coefficient=0.1, # Рассеяние в облаках
|
|
996
|
+
absorption_coefficient=0.02, # Поглощение
|
|
997
|
+
phase_function_g=0.7, # Рассеяние вперед (облака)
|
|
998
|
+
density=1.0,
|
|
999
|
+
color=(1.0, 1.0, 1.0) # Белый свет
|
|
1000
|
+
)
|
|
1001
|
+
|
|
1002
|
+
# Источник света (солнце)
|
|
1003
|
+
sun_light = LightSource(
|
|
1004
|
+
direction=(0.3, 1.0, 0.2), # Солнце высоко
|
|
1005
|
+
color=(1.0, 0.9, 0.7), # Теплый солнечный свет
|
|
1006
|
+
intensity=2.0,
|
|
1007
|
+
light_type="directional"
|
|
1008
|
+
)
|
|
1009
|
+
|
|
1010
|
+
# Рендерер с рассеянием
|
|
1011
|
+
renderer = VolumeScatteringRenderer(
|
|
1012
|
+
volume=clouds,
|
|
1013
|
+
medium=medium,
|
|
1014
|
+
light_sources=[sun_light],
|
|
1015
|
+
use_multiple_scattering=True,
|
|
1016
|
+
num_scattering_events=2
|
|
1017
|
+
)
|
|
1018
|
+
|
|
1019
|
+
# Рендеринг
|
|
1020
|
+
camera_pos = (0.5, 0.5, 2.0)
|
|
1021
|
+
camera_target = (0.5, 0.5, 0.0)
|
|
1022
|
+
|
|
1023
|
+
image = renderer.render_volumetric_light(
|
|
1024
|
+
camera_pos=camera_pos,
|
|
1025
|
+
camera_target=camera_target,
|
|
1026
|
+
image_size=(512, 256),
|
|
1027
|
+
max_steps=128,
|
|
1028
|
+
step_size=0.01
|
|
1029
|
+
)
|
|
1030
|
+
|
|
1031
|
+
print(f"Rendered image shape: {image.shape}")
|
|
1032
|
+
|
|
1033
|
+
return image, clouds
|
|
1034
|
+
|
|
1035
|
+
def example_underwater_scene():
|
|
1036
|
+
"""Пример подводного рассеяния"""
|
|
1037
|
+
|
|
1038
|
+
print("\nUnderwater scattering example...")
|
|
1039
|
+
|
|
1040
|
+
# Создаем подводную среду (плотность воды с частицами)
|
|
1041
|
+
from .volume_textures import VolumeTextureGenerator3D
|
|
1042
|
+
generator = VolumeTextureGenerator3D(seed=123)
|
|
1043
|
+
|
|
1044
|
+
# Объем для плотности воды (силуэты водорослей, пузырей)
|
|
1045
|
+
water_volume = generator.generate_perlin_3d(
|
|
1046
|
+
width=96, height=96, depth=96,
|
|
1047
|
+
scale=0.05, octaves=3
|
|
1048
|
+
)
|
|
1049
|
+
|
|
1050
|
+
# Настройка подводной среды
|
|
1051
|
+
medium = MediumProperties(
|
|
1052
|
+
scattering_coefficient=0.15, # Сильное рассеяние в воде
|
|
1053
|
+
absorption_coefficient=0.1, # Поглощение синим светом меньше
|
|
1054
|
+
phase_function_g=0.8, # Очень сильное рассеяние вперед
|
|
1055
|
+
density=0.8,
|
|
1056
|
+
color=(0.1, 0.3, 0.6) # Синий цвет воды
|
|
1057
|
+
)
|
|
1058
|
+
|
|
1059
|
+
# Солнечный свет, проникающий через воду
|
|
1060
|
+
sun_light = LightSource(
|
|
1061
|
+
direction=(0.1, 1.0, 0.0), # Солнце над водой
|
|
1062
|
+
color=(0.7, 0.8, 1.0), # Голубоватый подводный свет
|
|
1063
|
+
intensity=1.5,
|
|
1064
|
+
light_type="directional"
|
|
1065
|
+
)
|
|
1066
|
+
|
|
1067
|
+
# Ambient light от рассеянного подводного света
|
|
1068
|
+
ambient_light = LightSource(
|
|
1069
|
+
direction=(0, 1, 0),
|
|
1070
|
+
color=(0.1, 0.2, 0.4),
|
|
1071
|
+
intensity=0.3,
|
|
1072
|
+
light_type="directional"
|
|
1073
|
+
)
|
|
1074
|
+
|
|
1075
|
+
# Рендерер
|
|
1076
|
+
renderer = VolumeScatteringRenderer(
|
|
1077
|
+
volume=water_volume,
|
|
1078
|
+
medium=medium,
|
|
1079
|
+
light_sources=[sun_light, ambient_light],
|
|
1080
|
+
use_multiple_scattering=False # Для производительности
|
|
1081
|
+
)
|
|
1082
|
+
|
|
1083
|
+
# Рендеринг под водой
|
|
1084
|
+
camera_pos = (0.5, 0.3, 0.5) # Камера под водой
|
|
1085
|
+
camera_target = (0.5, 0.2, 0.0) # Смотрим вниз
|
|
1086
|
+
|
|
1087
|
+
image = renderer.render_volumetric_light(
|
|
1088
|
+
camera_pos=camera_pos,
|
|
1089
|
+
camera_target=camera_target,
|
|
1090
|
+
image_size=(512, 256),
|
|
1091
|
+
max_steps=64, # Меньше шагов для производительности
|
|
1092
|
+
step_size=0.02
|
|
1093
|
+
)
|
|
1094
|
+
|
|
1095
|
+
print(f"Underwater image shape: {image.shape}")
|
|
1096
|
+
|
|
1097
|
+
return image, water_volume
|
|
1098
|
+
|
|
1099
|
+
def example_fast_volume_light():
|
|
1100
|
+
"""Пример быстрого объемного освещения для real-time"""
|
|
1101
|
+
|
|
1102
|
+
print("\nFast volume light example (real-time optimized)...")
|
|
1103
|
+
|
|
1104
|
+
# Создаем маленький объем для производительности
|
|
1105
|
+
from .volume_textures import VolumeTextureGenerator3D
|
|
1106
|
+
generator = VolumeTextureGenerator3D(seed=42)
|
|
1107
|
+
volume = generator.generate_clouds_3d(
|
|
1108
|
+
width=64, height=64, depth=64,
|
|
1109
|
+
scale=0.05, density=0.4, detail=2
|
|
1110
|
+
)
|
|
1111
|
+
|
|
1112
|
+
# Параметры камеры
|
|
1113
|
+
camera_pos = np.array([0.5, 0.5, 1.5], dtype=np.float32)
|
|
1114
|
+
camera_dir = np.array([0.0, 0.0, -1.0], dtype=np.float32)
|
|
1115
|
+
camera_dir = camera_dir / np.linalg.norm(camera_dir)
|
|
1116
|
+
|
|
1117
|
+
up = np.array([0.0, 1.0, 0.0], dtype=np.float32)
|
|
1118
|
+
right = np.cross(camera_dir, up)
|
|
1119
|
+
right = right / np.linalg.norm(right)
|
|
1120
|
+
up = np.cross(right, camera_dir)
|
|
1121
|
+
|
|
1122
|
+
# Параметры рассеяния
|
|
1123
|
+
width, height = 256, 256
|
|
1124
|
+
fov = 60.0
|
|
1125
|
+
aspect = width / height
|
|
1126
|
+
half_height = np.tan(np.radians(fov) / 2.0)
|
|
1127
|
+
half_width = aspect * half_height
|
|
1128
|
+
|
|
1129
|
+
light_dir = np.array([0.3, 1.0, 0.2], dtype=np.float32)
|
|
1130
|
+
light_dir = light_dir / np.linalg.norm(light_dir)
|
|
1131
|
+
light_color = np.array([1.0, 0.9, 0.7], dtype=np.float32)
|
|
1132
|
+
|
|
1133
|
+
scattering_coef = 0.1
|
|
1134
|
+
absorption_coef = 0.05
|
|
1135
|
+
phase_g = 0.7
|
|
1136
|
+
max_steps = 64
|
|
1137
|
+
step_size = 0.02
|
|
1138
|
+
|
|
1139
|
+
# Запускаем оптимизированное ядро
|
|
1140
|
+
image = fast_volume_scattering_kernel(
|
|
1141
|
+
width, height,
|
|
1142
|
+
camera_pos, camera_dir, right, up,
|
|
1143
|
+
half_width, half_height,
|
|
1144
|
+
light_dir, light_color,
|
|
1145
|
+
volume.data,
|
|
1146
|
+
scattering_coef, absorption_coef, phase_g,
|
|
1147
|
+
max_steps, step_size
|
|
1148
|
+
)
|
|
1149
|
+
|
|
1150
|
+
print(f"Fast volume light image shape: {image.shape}")
|
|
1151
|
+
|
|
1152
|
+
return image
|
|
1153
|
+
|
|
1154
|
+
def example_volumetric_fog():
|
|
1155
|
+
"""Пример объемного тумана с рассеянием"""
|
|
1156
|
+
|
|
1157
|
+
print("\nVolumetric fog example...")
|
|
1158
|
+
|
|
1159
|
+
# Создаем простой объем тумана (однородный с небольшими вариациями)
|
|
1160
|
+
from .volume_textures import VolumeTextureGenerator3D
|
|
1161
|
+
generator = VolumeTextureGenerator3D(seed=42)
|
|
1162
|
+
|
|
1163
|
+
# Генерируем однородный туман с небольшими вариациями
|
|
1164
|
+
fog_volume = generator.generate_perlin_3d(
|
|
1165
|
+
width=128, height=64, depth=128,
|
|
1166
|
+
scale=0.03, octaves=2
|
|
1167
|
+
)
|
|
1168
|
+
|
|
1169
|
+
# Делаем туман более однородным
|
|
1170
|
+
fog_data = fog_volume.data
|
|
1171
|
+
fog_density = np.clip(fog_data[..., 0] * 0.5 + 0.3, 0, 1) # Плотность 0.3-0.8
|
|
1172
|
+
fog_data[..., 0] = fog_density
|
|
1173
|
+
|
|
1174
|
+
# Настройка среды (туман)
|
|
1175
|
+
medium = MediumProperties(
|
|
1176
|
+
scattering_coefficient=0.08,
|
|
1177
|
+
absorption_coefficient=0.02,
|
|
1178
|
+
phase_function_g=0.2, # Слабое направленное рассеяние
|
|
1179
|
+
density=1.0,
|
|
1180
|
+
color=(0.9, 0.9, 0.9) # Сероватый туман
|
|
1181
|
+
)
|
|
1182
|
+
|
|
1183
|
+
# Несколько источников света (уличные фонари)
|
|
1184
|
+
lights = [
|
|
1185
|
+
LightSource(
|
|
1186
|
+
position=(0.3, 0.2, 0.3),
|
|
1187
|
+
color=(1.0, 0.9, 0.7),
|
|
1188
|
+
intensity=3.0,
|
|
1189
|
+
light_type="point"
|
|
1190
|
+
),
|
|
1191
|
+
LightSource(
|
|
1192
|
+
position=(0.7, 0.2, 0.7),
|
|
1193
|
+
color=(0.7, 0.9, 1.0),
|
|
1194
|
+
intensity=2.5,
|
|
1195
|
+
light_type="point"
|
|
1196
|
+
),
|
|
1197
|
+
LightSource(
|
|
1198
|
+
direction=(0.1, -1.0, 0.1), # Лунный свет
|
|
1199
|
+
color=(0.6, 0.7, 1.0),
|
|
1200
|
+
intensity=0.5,
|
|
1201
|
+
light_type="directional"
|
|
1202
|
+
)
|
|
1203
|
+
]
|
|
1204
|
+
|
|
1205
|
+
# Рендерер
|
|
1206
|
+
renderer = VolumeScatteringRenderer(
|
|
1207
|
+
volume=fog_volume,
|
|
1208
|
+
medium=medium,
|
|
1209
|
+
light_sources=lights,
|
|
1210
|
+
use_multiple_scattering=True
|
|
1211
|
+
)
|
|
1212
|
+
|
|
1213
|
+
# Рендеринг туманной сцены
|
|
1214
|
+
camera_pos = (0.5, 0.3, 1.0)
|
|
1215
|
+
camera_target = (0.5, 0.2, 0.0)
|
|
1216
|
+
|
|
1217
|
+
image = renderer.render_volumetric_light(
|
|
1218
|
+
camera_pos=camera_pos,
|
|
1219
|
+
camera_target=camera_target,
|
|
1220
|
+
image_size=(512, 256),
|
|
1221
|
+
max_steps=96,
|
|
1222
|
+
step_size=0.015
|
|
1223
|
+
)
|
|
1224
|
+
|
|
1225
|
+
print(f"Volumetric fog image shape: {image.shape}")
|
|
1226
|
+
|
|
1227
|
+
return image, fog_volume
|
|
1228
|
+
|
|
1229
|
+
if __name__ == "__main__":
|
|
1230
|
+
print("Volume Light Scattering System")
|
|
1231
|
+
print("=" * 60)
|
|
1232
|
+
|
|
1233
|
+
# Пример 1: Атмосферное рассеяние (облака)
|
|
1234
|
+
cloud_image, clouds = example_atmospheric_scattering()
|
|
1235
|
+
|
|
1236
|
+
# Пример 2: Подводное рассеяние
|
|
1237
|
+
underwater_image, water_volume = example_underwater_scene()
|
|
1238
|
+
|
|
1239
|
+
# Пример 3: Быстрое объемное освещение
|
|
1240
|
+
fast_image = example_fast_volume_light()
|
|
1241
|
+
|
|
1242
|
+
# Пример 4: Объемный туман
|
|
1243
|
+
fog_image, fog_volume = example_volumetric_fog()
|
|
1244
|
+
|
|
1245
|
+
print("\n" + "=" * 60)
|
|
1246
|
+
print("Volume Light Scattering Features:")
|
|
1247
|
+
print("-" * 40)
|
|
1248
|
+
print("1. Multiple scattering types: Rayleigh, Mie, Henyey-Greenstein")
|
|
1249
|
+
print("2. Atmospheric scattering for realistic skies")
|
|
1250
|
+
print("3. Underwater scattering with caustics")
|
|
1251
|
+
print("4. Volumetric fog and god rays")
|
|
1252
|
+
print("5. Single and multiple scattering support")
|
|
1253
|
+
print("6. Optimized kernels for real-time performance")
|
|
1254
|
+
print("7. Support for multiple light sources")
|
|
1255
|
+
|
|
1256
|
+
print("\nPerformance optimization tips:")
|
|
1257
|
+
print("- Use lower resolution volumes for real-time")
|
|
1258
|
+
print("- Reduce number of scattering events")
|
|
1259
|
+
print("- Use simplified phase functions (Schlick approximation)")
|
|
1260
|
+
print("- Implement level-of-detail for distant volumes")
|
|
1261
|
+
print("- Consider GPU acceleration for production use")
|
|
1262
|
+
|
|
1263
|
+
print("\nVolume light scattering system ready for realistic atmospheric effects!")
|