fractex 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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!")