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/interactive.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Helpers for interactive rendering with adaptive quality."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Callable, Optional, Tuple
|
|
7
|
+
import itertools
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def add_interactive_args(parser) -> None:
|
|
14
|
+
parser.add_argument("--interactive", action="store_true", help="Run interactive view")
|
|
15
|
+
parser.add_argument("--scale", type=float, default=1.0, help="Display scale multiplier")
|
|
16
|
+
parser.add_argument("--fps", type=float, default=30.0, help="Target FPS")
|
|
17
|
+
parser.add_argument("--width", type=int, default=None, help="Override width")
|
|
18
|
+
parser.add_argument("--height", type=int, default=None, help="Override height")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def add_preset_arg(parser, presets, default: Optional[str] = None, dest: str = "preset") -> None:
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"--preset",
|
|
24
|
+
choices=presets,
|
|
25
|
+
default=default,
|
|
26
|
+
dest=dest,
|
|
27
|
+
help="Preset name",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def resolve_preset(value: Optional[str], presets, fallback: Optional[str] = None) -> Optional[str]:
|
|
32
|
+
if value is None:
|
|
33
|
+
return fallback
|
|
34
|
+
if value not in presets:
|
|
35
|
+
raise ValueError(f"Unknown preset '{value}'. Available: {', '.join(presets)}")
|
|
36
|
+
return value
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _ensure_rgb(image: np.ndarray) -> np.ndarray:
|
|
40
|
+
if image.ndim == 2:
|
|
41
|
+
image = np.repeat(image[:, :, None], 3, axis=2)
|
|
42
|
+
if image.shape[2] == 1:
|
|
43
|
+
image = np.repeat(image, 3, axis=2)
|
|
44
|
+
if image.shape[2] >= 3:
|
|
45
|
+
return image[:, :, :3]
|
|
46
|
+
raise ValueError("Unsupported image shape for RGB conversion.")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def resize_nearest(image: np.ndarray, out_w: int, out_h: int) -> np.ndarray:
|
|
50
|
+
if image.ndim == 2:
|
|
51
|
+
image = image[:, :, None]
|
|
52
|
+
in_h, in_w, channels = image.shape
|
|
53
|
+
if in_w == out_w and in_h == out_h:
|
|
54
|
+
return image
|
|
55
|
+
scale_x = max(1, out_w // in_w)
|
|
56
|
+
scale_y = max(1, out_h // in_h)
|
|
57
|
+
up = np.repeat(np.repeat(image, scale_y, axis=0), scale_x, axis=1)
|
|
58
|
+
up = up[:out_h, :out_w, :]
|
|
59
|
+
return up if channels > 1 else up[:, :, 0]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_screen_size(default: Tuple[int, int] = (1920, 1080)) -> Tuple[int, int]:
|
|
63
|
+
try:
|
|
64
|
+
import tkinter as tk
|
|
65
|
+
root = tk.Tk()
|
|
66
|
+
root.withdraw()
|
|
67
|
+
screen_width = root.winfo_screenwidth()
|
|
68
|
+
screen_height = root.winfo_screenheight()
|
|
69
|
+
root.destroy()
|
|
70
|
+
return int(screen_width), int(screen_height)
|
|
71
|
+
except Exception:
|
|
72
|
+
return default
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class InteractiveConfig:
|
|
77
|
+
title: str = "Fractex"
|
|
78
|
+
target_fps: float = 30.0
|
|
79
|
+
scale: float = 1.0
|
|
80
|
+
width: Optional[int] = None
|
|
81
|
+
height: Optional[int] = None
|
|
82
|
+
min_scale: float = 0.4
|
|
83
|
+
max_scale: float = 1.0
|
|
84
|
+
min_render: int = 64
|
|
85
|
+
|
|
86
|
+
@classmethod
|
|
87
|
+
def from_args(cls, args, title: Optional[str] = None) -> "InteractiveConfig":
|
|
88
|
+
return cls(
|
|
89
|
+
title=title or "Fractex",
|
|
90
|
+
target_fps=max(1.0, getattr(args, "fps", 30.0)),
|
|
91
|
+
scale=max(0.1, getattr(args, "scale", 1.0)),
|
|
92
|
+
width=getattr(args, "width", None),
|
|
93
|
+
height=getattr(args, "height", None),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def run_interactive(
|
|
98
|
+
render_frame: Callable[[float, int, int], np.ndarray],
|
|
99
|
+
config: InteractiveConfig,
|
|
100
|
+
) -> None:
|
|
101
|
+
try:
|
|
102
|
+
import matplotlib.pyplot as plt
|
|
103
|
+
from matplotlib.animation import FuncAnimation
|
|
104
|
+
except Exception:
|
|
105
|
+
print("matplotlib is not available; cannot display interactive output.")
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
screen_w, screen_h = get_screen_size()
|
|
109
|
+
width = config.width or int(screen_w * config.scale)
|
|
110
|
+
height = config.height or int(screen_h * config.scale)
|
|
111
|
+
width = max(config.min_render, width)
|
|
112
|
+
height = max(config.min_render, height)
|
|
113
|
+
|
|
114
|
+
fig, ax = plt.subplots()
|
|
115
|
+
dpi = fig.get_dpi()
|
|
116
|
+
fig.set_size_inches(width / dpi, height / dpi)
|
|
117
|
+
ax.axis("off")
|
|
118
|
+
ax.set_title(config.title)
|
|
119
|
+
fig.subplots_adjust(left=0, right=1, top=1, bottom=0)
|
|
120
|
+
|
|
121
|
+
target_ms = 1000.0 / max(1.0, config.target_fps)
|
|
122
|
+
render_scale = 1.0
|
|
123
|
+
last_time = time.perf_counter()
|
|
124
|
+
ema_ms = target_ms
|
|
125
|
+
|
|
126
|
+
frame0 = render_frame(0.0, width, height)
|
|
127
|
+
im = ax.imshow(_ensure_rgb(frame0), animated=True, aspect="auto")
|
|
128
|
+
|
|
129
|
+
def update(frame):
|
|
130
|
+
nonlocal render_scale, last_time, ema_ms
|
|
131
|
+
now = time.perf_counter()
|
|
132
|
+
dt_ms = (now - last_time) * 1000.0
|
|
133
|
+
last_time = now
|
|
134
|
+
ema_ms = ema_ms * 0.9 + dt_ms * 0.1
|
|
135
|
+
|
|
136
|
+
if ema_ms > target_ms * 1.1:
|
|
137
|
+
render_scale = max(config.min_scale, render_scale * 0.9)
|
|
138
|
+
elif ema_ms < target_ms * 0.9:
|
|
139
|
+
render_scale = min(config.max_scale, render_scale * 1.05)
|
|
140
|
+
|
|
141
|
+
render_w = max(config.min_render, int(width * render_scale))
|
|
142
|
+
render_h = max(config.min_render, int(height * render_scale))
|
|
143
|
+
t = frame / 10.0
|
|
144
|
+
frame_img = render_frame(t, render_w, render_h)
|
|
145
|
+
frame_img = resize_nearest(frame_img, width, height)
|
|
146
|
+
im.set_array(_ensure_rgb(frame_img))
|
|
147
|
+
return (im,)
|
|
148
|
+
|
|
149
|
+
anim = FuncAnimation(
|
|
150
|
+
fig,
|
|
151
|
+
update,
|
|
152
|
+
frames=itertools.count(),
|
|
153
|
+
interval=0,
|
|
154
|
+
blit=True,
|
|
155
|
+
repeat=True,
|
|
156
|
+
cache_frame_data=False,
|
|
157
|
+
)
|
|
158
|
+
plt.show(block=True)
|