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
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()
|