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/examples/3d.py ADDED
@@ -0,0 +1,109 @@
1
+ import sys
2
+ from pathlib import Path
3
+ import numpy as np
4
+ import argparse
5
+ from dataclasses import dataclass
6
+
7
+ ROOT = Path(__file__).resolve().parents[2]
8
+ if str(ROOT) not in sys.path:
9
+ sys.path.insert(0, str(ROOT))
10
+ EXAMPLES_DIR = Path(__file__).resolve().parent
11
+ if str(EXAMPLES_DIR) not in sys.path:
12
+ sys.path.insert(0, str(EXAMPLES_DIR))
13
+
14
+ from fractex.volume_textures import (
15
+ VolumeTextureGenerator3D,
16
+ VolumeTextureBlender3D,
17
+ VolumeTextureRenderer,
18
+ VolumeTexture3D,
19
+ VolumeFormat,
20
+ )
21
+ from _output import save_ppm, save_mp4
22
+ from fractex.interactive import add_interactive_args, InteractiveConfig, run_interactive
23
+
24
+
25
+ @dataclass
26
+ class Player:
27
+ position: np.ndarray
28
+ look_at: np.ndarray
29
+
30
+
31
+ def main():
32
+ parser = argparse.ArgumentParser(description="3D volume example")
33
+ add_interactive_args(parser)
34
+ args = parser.parse_args()
35
+ game_time = 0.0
36
+ player = Player(
37
+ position=np.array([0.5, 0.5, 2.0]),
38
+ look_at=np.array([0.5, 0.5, 0.5])
39
+ )
40
+
41
+ # Создание объемного облачного неба
42
+ generator = VolumeTextureGenerator3D(seed=42)
43
+ cloud_volume = generator.generate_clouds_3d(
44
+ width=64, height=32, depth=64,
45
+ scale=0.02, density=0.4, animated=False, time=game_time
46
+ )
47
+
48
+ # Создание подземной пещеры с лавой
49
+ cave_noise = generator.generate_perlin_3d(
50
+ width=48, height=48, depth=48,
51
+ scale=0.03, octaves=3
52
+ )
53
+
54
+ lava_pockets = generator.generate_lava_3d(
55
+ width=48, height=48, depth=48,
56
+ scale=0.01, temperature=0.8, animated=False, time=game_time
57
+ )
58
+
59
+ # Смешивание пещеры и лавы
60
+ blender = VolumeTextureBlender3D()
61
+ cave_mask = (cave_noise.data > 0.7).astype(np.float32)
62
+ cave_rgba = VolumeTexture3D(
63
+ data=np.repeat(cave_noise.data, 4, axis=3),
64
+ format=VolumeFormat.RGBA_FLOAT,
65
+ voxel_size=cave_noise.voxel_size,
66
+ )
67
+ cave_with_lava = blender.blend(
68
+ cave_rgba, lava_pockets,
69
+ blend_mode="add",
70
+ blend_mask=cave_mask
71
+ )
72
+
73
+ # Рендеринг
74
+ renderer = VolumeTextureRenderer(cave_with_lava)
75
+ frames = []
76
+ for z in [2.2, 2.0, 1.8, 1.6, 1.4, 1.2]:
77
+ camera_pos = np.array([0.5, 0.5, z])
78
+ frame = renderer.render_raycast(
79
+ camera_pos=camera_pos,
80
+ camera_target=player.look_at,
81
+ image_size=(256, 144),
82
+ max_steps=64
83
+ )
84
+ frames.append(frame)
85
+
86
+ print("Cloud volume:", cloud_volume.data.shape)
87
+ print("Cave with lava:", cave_with_lava.data.shape)
88
+ print("Rendered frame:", frames[0].shape)
89
+ if float(frames[0].max()) == 0.0:
90
+ projection = cave_with_lava.data[..., 3].max(axis=0)
91
+ save_ppm(projection, EXAMPLES_DIR / "output" / "3d_raycast.ppm", stretch=True)
92
+ else:
93
+ save_ppm(frames[0], EXAMPLES_DIR / "output" / "3d_raycast.ppm", stretch=True)
94
+ save_mp4(frames, EXAMPLES_DIR / "output" / "3d_raycast.mp4", fps=8, stretch=True)
95
+
96
+ if args.interactive:
97
+ config = InteractiveConfig.from_args(args, title="3D Raycast (interactive)")
98
+ depth = cave_with_lava.data.shape[0]
99
+
100
+ def render_frame(t, w, h):
101
+ idx = int(abs(np.sin(t * 0.2)) * (depth - 1))
102
+ slice_img = cave_with_lava.data[idx]
103
+ return slice_img[..., :3]
104
+
105
+ run_interactive(render_frame, config)
106
+
107
+
108
+ if __name__ == "__main__":
109
+ main()
@@ -0,0 +1,113 @@
1
+ # Полный пайплайн: текстура + рассеяние
2
+ import sys
3
+ from pathlib import Path
4
+ import numpy as np
5
+ import argparse
6
+ from dataclasses import dataclass
7
+
8
+ ROOT = Path(__file__).resolve().parents[2]
9
+ if str(ROOT) not in sys.path:
10
+ sys.path.insert(0, str(ROOT))
11
+ EXAMPLES_DIR = Path(__file__).resolve().parent
12
+ if str(EXAMPLES_DIR) not in sys.path:
13
+ sys.path.insert(0, str(EXAMPLES_DIR))
14
+
15
+ from fractex.volume_textures import VolumeTextureGenerator3D
16
+ from fractex.volume_scattering import VolumeScatteringRenderer, MediumProperties, LightSource
17
+ from _output import save_ppm, save_mp4
18
+ from fractex.interactive import add_interactive_args, InteractiveConfig, run_interactive
19
+
20
+
21
+ @dataclass
22
+ class Player:
23
+ position: np.ndarray
24
+ look_at: np.ndarray
25
+
26
+
27
+ def blend_volume_with_scene(scene_image: np.ndarray, volume_image: np.ndarray, weight: float = 0.6) -> np.ndarray:
28
+ return np.clip(scene_image * (1.0 - weight) + volume_image * weight, 0, 1)
29
+
30
+
31
+ def main():
32
+ parser = argparse.ArgumentParser(description="3D integration example")
33
+ add_interactive_args(parser)
34
+ args = parser.parse_args()
35
+ # Генерация объемной текстуры облаков
36
+ generator = VolumeTextureGenerator3D(seed=42)
37
+ clouds_3d = generator.generate_clouds_3d(
38
+ width=64, height=32, depth=64,
39
+ scale=0.02, density=0.3, animated=False
40
+ )
41
+
42
+ # Настройка атмосферного рассеяния
43
+ atmosphere = MediumProperties(
44
+ scattering_coefficient=0.08,
45
+ absorption_coefficient=0.02,
46
+ phase_function_g=0.7,
47
+ density=1.0,
48
+ color=(1.0, 0.95, 0.9)
49
+ )
50
+
51
+ # Солнечный свет
52
+ sun = LightSource(
53
+ direction=np.array([0.2, 0.9, 0.1]),
54
+ color=(1.0, 0.95, 0.9),
55
+ intensity=1.0,
56
+ light_type="directional"
57
+ )
58
+
59
+ player = Player(
60
+ position=np.array([0.5, 0.5, -1.0]),
61
+ look_at=np.array([0.5, 0.5, 0.5])
62
+ )
63
+
64
+ frames = []
65
+ directions = [
66
+ np.array([0.3, 0.9, 0.1]),
67
+ np.array([0.1, 0.9, 0.3]),
68
+ np.array([-0.1, 0.9, 0.3]),
69
+ np.array([-0.3, 0.9, 0.1]),
70
+ np.array([0.0, 0.7, 0.7]),
71
+ np.array([0.0, 0.6, 0.8]),
72
+ ]
73
+ for direction in directions:
74
+ sun.direction = direction
75
+ renderer = VolumeScatteringRenderer(
76
+ volume=clouds_3d,
77
+ medium=atmosphere,
78
+ light_sources=[sun],
79
+ use_multiple_scattering=False
80
+ )
81
+ image = renderer.render_volumetric_light(
82
+ camera_pos=player.position,
83
+ camera_target=player.look_at,
84
+ image_size=(320, 180),
85
+ max_steps=48
86
+ )
87
+ scene_image = np.zeros_like(image)
88
+ final_image = blend_volume_with_scene(scene_image, image)
89
+ frames.append(final_image)
90
+
91
+ final_image = frames[0]
92
+ print("Rendered image:", final_image.shape, final_image.min(), final_image.max())
93
+ if float(final_image.max()) == 0.0:
94
+ density = clouds_3d.data[clouds_3d.data.shape[0] // 2, :, :, 3]
95
+ save_ppm(density, EXAMPLES_DIR / "output" / "3d_integration.ppm", stretch=True)
96
+ else:
97
+ save_ppm(final_image, EXAMPLES_DIR / "output" / "3d_integration.ppm", stretch=True)
98
+ save_mp4(frames, EXAMPLES_DIR / "output" / "3d_integration.mp4", fps=8, stretch=True)
99
+
100
+ if args.interactive:
101
+ config = InteractiveConfig.from_args(args, title="3D Integration (interactive)")
102
+ depth = clouds_3d.data.shape[0]
103
+
104
+ def render_frame(t, w, h):
105
+ idx = int(abs(np.sin(t * 0.2)) * (depth - 1))
106
+ density = clouds_3d.data[idx, :, :, 3]
107
+ return density
108
+
109
+ run_interactive(render_frame, config)
110
+
111
+
112
+ if __name__ == "__main__":
113
+ main()
@@ -0,0 +1,59 @@
1
+ # Создание hybrid материала: 2D текстура + 3D детали
2
+ import sys
3
+ from pathlib import Path
4
+ import argparse
5
+
6
+ ROOT = Path(__file__).resolve().parents[2]
7
+ if str(ROOT) not in sys.path:
8
+ sys.path.insert(0, str(ROOT))
9
+ EXAMPLES_DIR = Path(__file__).resolve().parent
10
+ if str(EXAMPLES_DIR) not in sys.path:
11
+ sys.path.insert(0, str(EXAMPLES_DIR))
12
+
13
+ from fractex.simplex_noise import SimplexTextureGenerator
14
+ from fractex.volume_textures import VolumeTextureGenerator3D
15
+ from _output import save_ppm
16
+ from fractex.interactive import add_interactive_args, InteractiveConfig, run_interactive
17
+
18
+
19
+ def main():
20
+ parser = argparse.ArgumentParser(description="3D integration 2D example")
21
+ add_interactive_args(parser)
22
+ args = parser.parse_args()
23
+
24
+ # 2D terrain текстура
25
+ tex_gen_2d = SimplexTextureGenerator(seed=42)
26
+ terrain_2d = tex_gen_2d.generate_terrain(64, 64)
27
+
28
+ # 3D детали (камни, трава)
29
+ tex_gen_3d = VolumeTextureGenerator3D(seed=42)
30
+ rocks_3d = tex_gen_3d.generate_rocks_3d(64, 64, 32)
31
+ grass_3d = tex_gen_3d.generate_grass_3d(64, 64, 16)
32
+
33
+ print("Terrain 2D:", terrain_2d.shape)
34
+ print("Rocks 3D:", rocks_3d.data.shape)
35
+ print("Grass 3D:", grass_3d.data.shape)
36
+
37
+ # Проекция 3D деталей на 2D terrain
38
+ # (демо — используем средний срез по глубине)
39
+ rocks_slice = rocks_3d.data[rocks_3d.data.shape[0] // 2, :, :, 3]
40
+ grass_slice = grass_3d.data[grass_3d.data.shape[0] // 2, :, :, 3]
41
+ combined = terrain_2d.copy()
42
+ combined[..., 1] = (combined[..., 1] * 0.7 + grass_slice * 0.3)
43
+ combined[..., 2] = (combined[..., 2] * 0.7 + rocks_slice * 0.3)
44
+
45
+ print("Combined terrain sample:", combined[0, 0])
46
+ save_ppm(combined, EXAMPLES_DIR / "output" / "3d_integration_2d.ppm")
47
+
48
+ if args.interactive:
49
+ config = InteractiveConfig.from_args(args, title="3D Integration 2D (interactive)")
50
+
51
+ def render_frame(t, w, h):
52
+ terrain = tex_gen_2d.generate_terrain(w, h)
53
+ return terrain[..., :3]
54
+
55
+ run_interactive(render_frame, config)
56
+
57
+
58
+ if __name__ == "__main__":
59
+ main()
@@ -0,0 +1,34 @@
1
+ """Example modules for Fractex."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import runpy
6
+ import sys
7
+ from typing import List, Optional
8
+
9
+
10
+ def list_examples() -> List[str]:
11
+ return [
12
+ "splash",
13
+ "custom_pattern",
14
+ "architecture_pattern",
15
+ "composite_material",
16
+ "crystal_cave",
17
+ "integration",
18
+ "terrain",
19
+ "3d_integration_2d",
20
+ "3d_integration",
21
+ "3d",
22
+ "underwater",
23
+ "underwater_volkano",
24
+ "game_texture",
25
+ ]
26
+
27
+
28
+ def run_example(name: str, args: Optional[List[str]] = None) -> None:
29
+ """Run an example module by name."""
30
+ if name not in list_examples():
31
+ raise ValueError(f"Unknown example '{name}'.")
32
+ module = f"fractex.examples.{name}"
33
+ sys.argv = [module] + (args or [])
34
+ runpy.run_module(module, run_name="__main__")
@@ -0,0 +1,115 @@
1
+ """Small helpers for saving example outputs without extra deps."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Iterable, Optional
7
+
8
+ import numpy as np
9
+
10
+
11
+ def _ensure_rgb(image: np.ndarray) -> np.ndarray:
12
+ if image.ndim == 2:
13
+ image = np.repeat(image[:, :, None], 3, axis=2)
14
+ if image.shape[2] == 1:
15
+ image = np.repeat(image, 3, axis=2)
16
+ if image.shape[2] >= 3:
17
+ return image[:, :, :3]
18
+ raise ValueError("Unsupported image shape for RGB conversion.")
19
+
20
+
21
+ def _normalize_to_uint8(image: np.ndarray, stretch: bool) -> np.ndarray:
22
+ img = image.astype(np.float32)
23
+ if img.size == 0:
24
+ return img.astype(np.uint8)
25
+ min_val = float(img.min())
26
+ max_val = float(img.max())
27
+ if stretch and max_val > min_val:
28
+ img = (img - min_val) / (max_val - min_val)
29
+ elif min_val < 0.0 or max_val > 1.0:
30
+ if max_val > min_val:
31
+ img = (img - min_val) / (max_val - min_val)
32
+ img = np.clip(img, 0.0, 1.0)
33
+ return (img * 255.0 + 0.5).astype(np.uint8)
34
+
35
+
36
+ def save_ppm(image: np.ndarray, path: Path, stretch: bool = False) -> None:
37
+ """Save an image to binary PPM (P6) format."""
38
+ path.parent.mkdir(parents=True, exist_ok=True)
39
+ rgb = _ensure_rgb(image)
40
+ data = _normalize_to_uint8(rgb, stretch=stretch)
41
+ height, width, _ = data.shape
42
+ header = f"P6\n{width} {height}\n255\n".encode("ascii")
43
+ with path.open("wb") as f:
44
+ f.write(header)
45
+ f.write(data.tobytes())
46
+
47
+
48
+ def save_volume_slice(
49
+ volume: np.ndarray,
50
+ path: Path,
51
+ axis: int = 0,
52
+ index: Optional[int] = None,
53
+ stretch: bool = False,
54
+ ) -> None:
55
+ """Save a middle slice from a 3D/4D volume."""
56
+ if volume.ndim == 3:
57
+ data = volume
58
+ elif volume.ndim == 4:
59
+ data = volume
60
+ else:
61
+ raise ValueError("Volume must be 3D or 4D.")
62
+
63
+ if index is None:
64
+ index = data.shape[axis] // 2
65
+ if axis == 0:
66
+ slice_img = data[index]
67
+ elif axis == 1:
68
+ slice_img = data[:, index, :]
69
+ else:
70
+ slice_img = data[:, :, index]
71
+
72
+ save_ppm(slice_img, path, stretch=stretch)
73
+
74
+
75
+ def save_ppm_sequence(
76
+ frames: Iterable[np.ndarray],
77
+ directory: Path,
78
+ prefix: str = "frame",
79
+ stretch: bool = False,
80
+ ) -> None:
81
+ """Save a sequence of frames as numbered PPM files."""
82
+ directory.mkdir(parents=True, exist_ok=True)
83
+ for i, frame in enumerate(frames):
84
+ save_ppm(frame, directory / f"{prefix}_{i:03d}.ppm", stretch=stretch)
85
+
86
+
87
+ def save_mp4(
88
+ frames: Iterable[np.ndarray],
89
+ path: Path,
90
+ fps: int = 24,
91
+ stretch: bool = False,
92
+ macro_block_size: int = 1,
93
+ ) -> None:
94
+ """Save frames to MP4 if imageio is available."""
95
+ try:
96
+ import imageio
97
+ use_v3 = hasattr(imageio, "v3")
98
+ except Exception:
99
+ print("imageio is not available; skipping mp4 export.")
100
+ return
101
+
102
+ path.parent.mkdir(parents=True, exist_ok=True)
103
+ prepared = []
104
+ for frame in frames:
105
+ rgb = _ensure_rgb(frame)
106
+ data = _normalize_to_uint8(rgb, stretch=stretch)
107
+ prepared.append(data)
108
+
109
+ if use_v3:
110
+ imageio.v3.imwrite(path, prepared, fps=fps, macro_block_size=macro_block_size)
111
+ else:
112
+ imageio.mimsave(path, prepared, fps=fps, macro_block_size=macro_block_size)
113
+
114
+
115
+ """Small helpers for saving example outputs without extra deps."""
@@ -0,0 +1,61 @@
1
+ # Готические витражи
2
+ import sys
3
+ from pathlib import Path
4
+ import argparse
5
+
6
+ ROOT = Path(__file__).resolve().parents[2]
7
+ if str(ROOT) not in sys.path:
8
+ sys.path.insert(0, str(ROOT))
9
+ EXAMPLES_DIR = Path(__file__).resolve().parent
10
+ if str(EXAMPLES_DIR) not in sys.path:
11
+ sys.path.insert(0, str(EXAMPLES_DIR))
12
+
13
+ from fractex.geometric_patterns_3d import (
14
+ CompositePatternGenerator3D,
15
+ GeometricPattern3D,
16
+ PatternParameters,
17
+ )
18
+ from _output import save_volume_slice
19
+ from fractex.interactive import add_interactive_args, InteractiveConfig, run_interactive
20
+
21
+
22
+ def main():
23
+ parser = argparse.ArgumentParser(description="Architecture pattern example")
24
+ add_interactive_args(parser)
25
+ args = parser.parse_args()
26
+ stained_glass = CompositePatternGenerator3D(seed=42)
27
+
28
+ gothic_pattern = stained_glass.generate_composite(
29
+ pattern_types=[
30
+ GeometricPattern3D.HONEYCOMB,
31
+ GeometricPattern3D.DIAMOND_STRUCTURE,
32
+ GeometricPattern3D.CRYSTAL_LATTICE,
33
+ ],
34
+ dimensions=(64, 64, 64),
35
+ params_list=[
36
+ PatternParameters(cell_size=0.2, wall_thickness=0.05),
37
+ PatternParameters(scale=2.0, thickness=0.03),
38
+ PatternParameters(crystal_type="cubic", scale=0.5, thickness=0.01),
39
+ ],
40
+ )
41
+
42
+ print("Gothic pattern:", gothic_pattern.shape)
43
+ save_volume_slice(
44
+ gothic_pattern,
45
+ EXAMPLES_DIR / "output" / "architecture_pattern_slice.ppm",
46
+ stretch=True,
47
+ )
48
+
49
+ if args.interactive:
50
+ config = InteractiveConfig.from_args(args, title="Architecture Pattern (interactive)")
51
+ depth = gothic_pattern.shape[0]
52
+
53
+ def render_frame(t, w, h):
54
+ idx = int(abs(np.sin(t * 0.2)) * (depth - 1))
55
+ return gothic_pattern[idx, :, :, :3]
56
+
57
+ run_interactive(render_frame, config)
58
+
59
+
60
+ if __name__ == "__main__":
61
+ main()
@@ -0,0 +1,54 @@
1
+ # Динамическая атмосфера с изменяющимся временем дня
2
+ import sys
3
+ from pathlib import Path
4
+ import math
5
+ import numpy as np
6
+
7
+ ROOT = Path(__file__).resolve().parents[2]
8
+ if str(ROOT) not in sys.path:
9
+ sys.path.insert(0, str(ROOT))
10
+
11
+
12
+ class DynamicAtmosphere:
13
+ def __init__(self):
14
+ self.sun_direction = np.array([0.0, 1.0, 0.0])
15
+ self.sun_intensity = 1.0
16
+ self.scattering_coefficient = 0.1
17
+ self.phase_function_g = 0.7
18
+ self.sun_color = (1.0, 0.95, 0.9)
19
+
20
+ def update_time_of_day(self, time_of_day: float):
21
+ # time_of_day: 0.0 (полночь) до 1.0 (полночь следующего дня)
22
+ sun_angle = time_of_day * 2 * math.pi
23
+ self.sun_direction = np.array([
24
+ math.sin(sun_angle),
25
+ math.cos(sun_angle),
26
+ 0.0
27
+ ])
28
+
29
+ # Цвет солнца
30
+ if 0.2 < time_of_day < 0.3:
31
+ self.sun_color = (1.0, 0.5, 0.3)
32
+ elif 0.7 < time_of_day < 0.8:
33
+ self.sun_color = (1.0, 0.4, 0.2)
34
+ else:
35
+ self.sun_color = (1.0, 0.95, 0.9)
36
+
37
+ # Интенсивность
38
+ sun_height = self.sun_direction[1]
39
+ self.sun_intensity = max(0.1, sun_height * 2)
40
+
41
+ # Параметры рассеяния
42
+ if time_of_day < 0.25 or time_of_day > 0.75:
43
+ self.scattering_coefficient = 0.03
44
+ self.phase_function_g = 0.3
45
+ else:
46
+ self.scattering_coefficient = 0.1
47
+ self.phase_function_g = 0.7
48
+
49
+
50
+ if __name__ == "__main__":
51
+ atmosphere = DynamicAtmosphere()
52
+ for t in [0.0, 0.25, 0.5, 0.75]:
53
+ atmosphere.update_time_of_day(t)
54
+ print(f"time={t:.2f} sun_dir={atmosphere.sun_direction} intensity={atmosphere.sun_intensity:.2f}")
@@ -0,0 +1,63 @@
1
+ # Создание сложных гибридных материалов
2
+ import sys
3
+ from pathlib import Path
4
+ import argparse
5
+
6
+ ROOT = Path(__file__).resolve().parents[2]
7
+ if str(ROOT) not in sys.path:
8
+ sys.path.insert(0, str(ROOT))
9
+ EXAMPLES_DIR = Path(__file__).resolve().parent
10
+ if str(EXAMPLES_DIR) not in sys.path:
11
+ sys.path.insert(0, str(EXAMPLES_DIR))
12
+
13
+ from fractex.geometric_patterns_3d import (
14
+ CompositePatternGenerator3D,
15
+ GeometricPattern3D,
16
+ PatternParameters,
17
+ )
18
+ from _output import save_volume_slice
19
+ from fractex.interactive import add_interactive_args, InteractiveConfig, run_interactive
20
+
21
+
22
+ def main():
23
+ parser = argparse.ArgumentParser(description="Composite material example")
24
+ add_interactive_args(parser)
25
+ args = parser.parse_args()
26
+ composite = CompositePatternGenerator3D(seed=42)
27
+
28
+ # Кристаллическая основа с гироидными каналами
29
+ hybrid_material = composite.generate_composite(
30
+ pattern_types=[
31
+ GeometricPattern3D.CRYSTAL_LATTICE,
32
+ GeometricPattern3D.GYROID,
33
+ GeometricPattern3D.DIAMOND_STRUCTURE,
34
+ ],
35
+ dimensions=(64, 64, 64),
36
+ params_list=[
37
+ PatternParameters(crystal_type="diamond", thickness=0.05),
38
+ PatternParameters(scale=4.0, surface_threshold=0.2),
39
+ PatternParameters(scale=2.0, thickness=0.02),
40
+ ],
41
+ blend_mode="add",
42
+ )
43
+
44
+ print("Hybrid material:", hybrid_material.shape)
45
+ save_volume_slice(
46
+ hybrid_material,
47
+ EXAMPLES_DIR / "output" / "composite_material_slice.ppm",
48
+ stretch=True,
49
+ )
50
+
51
+ if args.interactive:
52
+ config = InteractiveConfig.from_args(args, title="Composite Material (interactive)")
53
+ depth = hybrid_material.shape[0]
54
+
55
+ def render_frame(t, w, h):
56
+ idx = int(abs(np.sin(t * 0.2)) * (depth - 1))
57
+ return hybrid_material[idx, :, :, :3]
58
+
59
+ run_interactive(render_frame, config)
60
+
61
+
62
+ if __name__ == "__main__":
63
+ main()
@@ -0,0 +1,61 @@
1
+ # Кристаллическая пещера
2
+ import sys
3
+ from pathlib import Path
4
+ import argparse
5
+ import numpy as np
6
+
7
+ ROOT = Path(__file__).resolve().parents[2]
8
+ if str(ROOT) not in sys.path:
9
+ sys.path.insert(0, str(ROOT))
10
+ EXAMPLES_DIR = Path(__file__).resolve().parent
11
+ if str(EXAMPLES_DIR) not in sys.path:
12
+ sys.path.insert(0, str(EXAMPLES_DIR))
13
+
14
+ from fractex.geometric_patterns_3d import (
15
+ CompositePatternGenerator3D,
16
+ GeometricPattern3D,
17
+ PatternParameters,
18
+ )
19
+ from _output import save_volume_slice
20
+ from fractex.interactive import add_interactive_args, InteractiveConfig, run_interactive
21
+
22
+
23
+ def main():
24
+ parser = argparse.ArgumentParser(description="Crystal cave example")
25
+ add_interactive_args(parser)
26
+ args = parser.parse_args()
27
+ world_seed = 123
28
+ crystal_cave = CompositePatternGenerator3D(seed=world_seed)
29
+
30
+ cave_pattern = crystal_cave.generate_layered_pattern(
31
+ pattern_layers=[
32
+ (GeometricPattern3D.CRYSTAL_LATTICE,
33
+ PatternParameters(crystal_type="hexagonal", scale=1.5), 0.8),
34
+ (GeometricPattern3D.DIAMOND_STRUCTURE,
35
+ PatternParameters(scale=3.0, thickness=0.02), 0.4),
36
+ (GeometricPattern3D.LAVA_LAMPS,
37
+ PatternParameters(surface_isolevel=0.3), 0.6),
38
+ ],
39
+ dimensions=(64, 64, 64),
40
+ )
41
+
42
+ print("Cave pattern:", cave_pattern.shape)
43
+ save_volume_slice(
44
+ cave_pattern,
45
+ EXAMPLES_DIR / "output" / "crystal_cave_slice.ppm",
46
+ stretch=True,
47
+ )
48
+
49
+ if args.interactive:
50
+ config = InteractiveConfig.from_args(args, title="Crystal Cave (interactive)")
51
+ depth = cave_pattern.shape[0]
52
+
53
+ def render_frame(t, w, h):
54
+ idx = int(abs(np.sin(t * 0.2)) * (depth - 1))
55
+ return cave_pattern[idx, :, :, :3]
56
+
57
+ run_interactive(render_frame, config)
58
+
59
+
60
+ if __name__ == "__main__":
61
+ main()