fractex 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fractex/3d.py +1585 -0
- fractex/__init__.py +38 -0
- fractex/advanced.py +170 -0
- fractex/cli.py +81 -0
- fractex/core.py +508 -0
- fractex/dynamic_textures_3d.py +1935 -0
- fractex/examples/3d.py +109 -0
- fractex/examples/3d_integration.py +113 -0
- fractex/examples/3d_integration_2d.py +59 -0
- fractex/examples/__init__.py +34 -0
- fractex/examples/_output.py +115 -0
- fractex/examples/architecture_pattern.py +61 -0
- fractex/examples/atmosphere.py +54 -0
- fractex/examples/composite_material.py +63 -0
- fractex/examples/crystal_cave.py +61 -0
- fractex/examples/custom_pattern.py +114 -0
- fractex/examples/game_integration.py +86 -0
- fractex/examples/game_texture.py +178 -0
- fractex/examples/integration.py +102 -0
- fractex/examples/physic_integration.py +70 -0
- fractex/examples/splash.py +159 -0
- fractex/examples/terrain.py +76 -0
- fractex/examples/underwater.py +94 -0
- fractex/examples/underwater_volkano.py +112 -0
- fractex/geometric_patterns_3d.py +2372 -0
- fractex/interactive.py +158 -0
- fractex/simplex_noise.py +1113 -0
- fractex/texture_blending.py +1377 -0
- fractex/volume_scattering.py +1263 -0
- fractex/volume_textures.py +8 -0
- fractex-0.1.0.dist-info/METADATA +100 -0
- fractex-0.1.0.dist-info/RECORD +36 -0
- fractex-0.1.0.dist-info/WHEEL +5 -0
- fractex-0.1.0.dist-info/entry_points.txt +2 -0
- fractex-0.1.0.dist-info/licenses/LICENSE +21 -0
- fractex-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Создание собственных паттернов
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
import numpy as np
|
|
6
|
+
import argparse
|
|
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 _output import save_volume_slice
|
|
16
|
+
from fractex.interactive import add_interactive_args, InteractiveConfig, run_interactive
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class CustomPatternParams:
|
|
21
|
+
scale: float = 10.0
|
|
22
|
+
surface_threshold: float = 0.5
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CustomPatternGenerator:
|
|
26
|
+
def generate_custom_pattern(self, dimensions, params: CustomPatternParams):
|
|
27
|
+
depth, height, width = dimensions
|
|
28
|
+
texture = np.zeros((depth, height, width, 4), dtype=np.float32)
|
|
29
|
+
|
|
30
|
+
# Пользовательская логика
|
|
31
|
+
for i in range(depth):
|
|
32
|
+
for j in range(height):
|
|
33
|
+
for k in range(width):
|
|
34
|
+
value = self.custom_math_function(i, j, k, params)
|
|
35
|
+
|
|
36
|
+
if self.is_on_surface(value, params):
|
|
37
|
+
color = self.calculate_color(i, j, k, value)
|
|
38
|
+
texture[i, j, k] = color
|
|
39
|
+
|
|
40
|
+
return texture
|
|
41
|
+
|
|
42
|
+
def custom_math_function(self, x, y, z, params: CustomPatternParams):
|
|
43
|
+
# Пример: гиперболический параболоид
|
|
44
|
+
return (x / params.scale) ** 2 - (y / params.scale) ** 2 - z / params.scale
|
|
45
|
+
|
|
46
|
+
def is_on_surface(self, value: float, params: CustomPatternParams) -> bool:
|
|
47
|
+
return abs(value) < params.surface_threshold
|
|
48
|
+
|
|
49
|
+
def calculate_color(self, x, y, z, value: float) -> np.ndarray:
|
|
50
|
+
return np.array([
|
|
51
|
+
0.4 + 0.6 * np.clip(value, -1, 1),
|
|
52
|
+
0.6,
|
|
53
|
+
0.8,
|
|
54
|
+
1.0,
|
|
55
|
+
], dtype=np.float32)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
if __name__ == "__main__":
|
|
59
|
+
parser = argparse.ArgumentParser(description="Custom pattern example")
|
|
60
|
+
add_interactive_args(parser)
|
|
61
|
+
parser.add_argument("--speed", type=float, default=1.0)
|
|
62
|
+
args = parser.parse_args()
|
|
63
|
+
|
|
64
|
+
generator = CustomPatternGenerator()
|
|
65
|
+
params = CustomPatternParams(scale=8.0, surface_threshold=0.4)
|
|
66
|
+
texture = generator.generate_custom_pattern((16, 16, 16), params)
|
|
67
|
+
print("Custom pattern:", texture.shape, texture.min(), texture.max())
|
|
68
|
+
save_volume_slice(
|
|
69
|
+
texture,
|
|
70
|
+
EXAMPLES_DIR / "output" / "custom_pattern_slice.ppm",
|
|
71
|
+
stretch=True,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if args.interactive:
|
|
75
|
+
config = InteractiveConfig.from_args(args, title="Custom Pattern (interactive)")
|
|
76
|
+
|
|
77
|
+
def render_frame(t, w, h):
|
|
78
|
+
speed = max(0.1, args.speed)
|
|
79
|
+
tt = t * speed * 3.0
|
|
80
|
+
zoom = np.exp(tt * 0.03)
|
|
81
|
+
zoom = min(zoom, 200.0)
|
|
82
|
+
depth = (tt * 1.5) % (params.scale * 6.0)
|
|
83
|
+
drift = np.exp(-tt * 0.02)
|
|
84
|
+
x0 = np.sin(tt * 0.4) * 3.0 * drift
|
|
85
|
+
y0 = np.cos(tt * 0.35) * 3.0 * drift
|
|
86
|
+
|
|
87
|
+
x = np.linspace(-1.0, 1.0, w) / zoom + x0
|
|
88
|
+
y = np.linspace(-1.0, 1.0, h) / zoom + y0
|
|
89
|
+
xx, yy = np.meshgrid(x, y)
|
|
90
|
+
|
|
91
|
+
angle = t * 0.02
|
|
92
|
+
cos_a, sin_a = np.cos(angle), np.sin(angle)
|
|
93
|
+
xr = xx * cos_a - yy * sin_a
|
|
94
|
+
yr = xx * sin_a + yy * cos_a
|
|
95
|
+
|
|
96
|
+
value = (xr / params.scale) ** 2 - (yr / params.scale) ** 2 - depth / params.scale
|
|
97
|
+
value2 = np.sin(xr * 3.0 + t * 0.5) + np.cos(yr * 2.0 - t * 0.3)
|
|
98
|
+
combined = value + 0.35 * value2
|
|
99
|
+
mask = 0.1 + 0.9 * np.exp(-np.abs(combined) * 1.6)
|
|
100
|
+
|
|
101
|
+
radius = np.sqrt(xr * xr + yr * yr)
|
|
102
|
+
vignette = np.clip(1.2 - radius * 0.8, 0.0, 1.0)
|
|
103
|
+
|
|
104
|
+
rgb = np.zeros((h, w, 3), dtype=np.float32)
|
|
105
|
+
color = np.clip(0.4 + 0.6 * np.tanh(combined), 0, 1)
|
|
106
|
+
ripple = 0.5 + 0.5 * np.sin(t * 0.6 + (xr + yr) * 2.0)
|
|
107
|
+
palette_shift = 0.5 + 0.5 * np.sin(t * 0.12)
|
|
108
|
+
rgb[..., 0] = (color * 0.7 + ripple * 0.3) * mask
|
|
109
|
+
rgb[..., 1] = (0.45 + 0.45 * ripple + 0.1 * palette_shift) * mask
|
|
110
|
+
rgb[..., 2] = (0.65 + 0.3 * np.cos(t * 0.4) + 0.1 * palette_shift) * mask
|
|
111
|
+
rgb = np.clip(rgb * vignette[..., None], 0, 1)
|
|
112
|
+
return rgb
|
|
113
|
+
|
|
114
|
+
run_interactive(render_frame, config)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Полная интеграция с Unity/Unreal
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import math
|
|
5
|
+
from typing import Tuple, List
|
|
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
|
+
from fractex.dynamic_textures_3d import StreamingDynamicTextures, DynamicTextureType
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class GameDynamicTexturesManager:
|
|
15
|
+
def __init__(self, world_size: Tuple[int, int, int]):
|
|
16
|
+
# Разделяем мир на чанки
|
|
17
|
+
self.chunk_size = (32, 32, 32)
|
|
18
|
+
self.num_chunks = (
|
|
19
|
+
world_size[0] // self.chunk_size[0],
|
|
20
|
+
world_size[1] // self.chunk_size[1],
|
|
21
|
+
world_size[2] // self.chunk_size[2],
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
self.streamer = StreamingDynamicTextures(
|
|
25
|
+
chunk_size=self.chunk_size,
|
|
26
|
+
max_active_chunks=8
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
self.render_cache = {}
|
|
30
|
+
|
|
31
|
+
def update(self, dt: float, player_position: Tuple[float, float, float]):
|
|
32
|
+
visible_chunks = self._get_visible_chunks(player_position)
|
|
33
|
+
|
|
34
|
+
for chunk_coords in visible_chunks:
|
|
35
|
+
distance = self._distance_to_chunk(player_position, chunk_coords)
|
|
36
|
+
priority = 1.0 / (distance + 1.0)
|
|
37
|
+
|
|
38
|
+
state = self.streamer.request_chunk(
|
|
39
|
+
chunk_coords,
|
|
40
|
+
DynamicTextureType.WATER_FLOW,
|
|
41
|
+
priority
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
if state:
|
|
45
|
+
self._render_or_update_chunk(chunk_coords, state)
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
self.streamer.update_all(dt)
|
|
49
|
+
except Exception as exc:
|
|
50
|
+
print(f"Simulation update skipped: {exc}")
|
|
51
|
+
|
|
52
|
+
def _get_visible_chunks(self, player_position: Tuple[float, float, float]) -> List[Tuple[int, int, int]]:
|
|
53
|
+
"""Получение чанков в поле зрения игрока (упрощенно)"""
|
|
54
|
+
px, py, pz = player_position
|
|
55
|
+
cx = int(px // self.chunk_size[0])
|
|
56
|
+
cy = int(py // self.chunk_size[1])
|
|
57
|
+
cz = int(pz // self.chunk_size[2])
|
|
58
|
+
|
|
59
|
+
visible = []
|
|
60
|
+
for dx in [-1, 0, 1]:
|
|
61
|
+
for dy in [-1, 0, 1]:
|
|
62
|
+
for dz in [-1, 0, 1]:
|
|
63
|
+
nx, ny, nz = cx + dx, cy + dy, cz + dz
|
|
64
|
+
if 0 <= nx < self.num_chunks[0] and 0 <= ny < self.num_chunks[1] and 0 <= nz < self.num_chunks[2]:
|
|
65
|
+
visible.append((nx, ny, nz))
|
|
66
|
+
|
|
67
|
+
return visible
|
|
68
|
+
|
|
69
|
+
def _distance_to_chunk(self, player_position: Tuple[float, float, float], chunk_coords: Tuple[int, int, int]) -> float:
|
|
70
|
+
px, py, pz = player_position
|
|
71
|
+
cx, cy, cz = chunk_coords
|
|
72
|
+
center = (
|
|
73
|
+
(cx + 0.5) * self.chunk_size[0],
|
|
74
|
+
(cy + 0.5) * self.chunk_size[1],
|
|
75
|
+
(cz + 0.5) * self.chunk_size[2],
|
|
76
|
+
)
|
|
77
|
+
return math.dist(player_position, center)
|
|
78
|
+
|
|
79
|
+
def _render_or_update_chunk(self, chunk_coords: Tuple[int, int, int], state):
|
|
80
|
+
self.render_cache[chunk_coords] = state.data.mean()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
if __name__ == "__main__":
|
|
84
|
+
manager = GameDynamicTexturesManager(world_size=(96, 96, 96))
|
|
85
|
+
manager.update(dt=0.016, player_position=(48.0, 48.0, 48.0))
|
|
86
|
+
print("Render cache size:", len(manager.render_cache))
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# fractex/examples/game_texture.py
|
|
2
|
+
"""
|
|
3
|
+
Пример интеграции в игровой движок
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from queue import Queue
|
|
9
|
+
import threading
|
|
10
|
+
import argparse
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
ROOT = Path(__file__).resolve().parents[2]
|
|
14
|
+
if str(ROOT) not in sys.path:
|
|
15
|
+
sys.path.insert(0, str(ROOT))
|
|
16
|
+
EXAMPLES_DIR = Path(__file__).resolve().parent
|
|
17
|
+
if str(EXAMPLES_DIR) not in sys.path:
|
|
18
|
+
sys.path.insert(0, str(EXAMPLES_DIR))
|
|
19
|
+
|
|
20
|
+
from fractex import FractalParams, FractalGenerator, InfiniteTexture, TextureStreamer
|
|
21
|
+
from _output import save_ppm
|
|
22
|
+
from fractex.interactive import add_interactive_args, InteractiveConfig, run_interactive
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class GameTextureSystem:
|
|
26
|
+
"""Система текстур для игры с бесконечной детализацией"""
|
|
27
|
+
|
|
28
|
+
def __init__(self):
|
|
29
|
+
self.textures = {}
|
|
30
|
+
self.streamers = {}
|
|
31
|
+
self.worker_threads = []
|
|
32
|
+
self.task_queue = Queue()
|
|
33
|
+
|
|
34
|
+
# Запускаем рабочие потоки для генерации текстур
|
|
35
|
+
for i in range(4): # 4 потока
|
|
36
|
+
thread = threading.Thread(target=self._texture_worker)
|
|
37
|
+
thread.daemon = True
|
|
38
|
+
thread.start()
|
|
39
|
+
self.worker_threads.append(thread)
|
|
40
|
+
|
|
41
|
+
def register_texture(self, name, params, texture_type="procedural"):
|
|
42
|
+
"""Регистрация новой текстуры"""
|
|
43
|
+
generator = FractalGenerator(params)
|
|
44
|
+
texture = InfiniteTexture(generator, texture_type)
|
|
45
|
+
streamer = TextureStreamer(texture)
|
|
46
|
+
|
|
47
|
+
self.textures[name] = texture
|
|
48
|
+
self.streamers[name] = streamer
|
|
49
|
+
|
|
50
|
+
return texture
|
|
51
|
+
|
|
52
|
+
def request_texture_tiles(self, name, viewport, camera_zoom):
|
|
53
|
+
"""Запрос тайлов текстуры для вьюпорта"""
|
|
54
|
+
streamer = self.streamers[name]
|
|
55
|
+
|
|
56
|
+
# Определяем необходимые тайлы и уровни детализации
|
|
57
|
+
tiles_needed = self._calculate_tiles_needed(viewport, camera_zoom)
|
|
58
|
+
|
|
59
|
+
# Запрашиваем все необходимые тайлы
|
|
60
|
+
results = {}
|
|
61
|
+
for tile_info in tiles_needed:
|
|
62
|
+
tile_x, tile_y, lod = tile_info
|
|
63
|
+
|
|
64
|
+
# Проверяем кэш
|
|
65
|
+
tile = streamer.request_tile(tile_x, tile_y, lod)
|
|
66
|
+
results[(tile_x, tile_y, lod)] = tile
|
|
67
|
+
|
|
68
|
+
return results
|
|
69
|
+
|
|
70
|
+
def _calculate_tiles_needed(self, viewport, zoom):
|
|
71
|
+
"""Расчет необходимых тайлов на основе положения камеры"""
|
|
72
|
+
tiles = []
|
|
73
|
+
|
|
74
|
+
# Преобразуем вьюпорт в тайловые координаты
|
|
75
|
+
min_x, min_y = viewport['min']
|
|
76
|
+
max_x, max_y = viewport['max']
|
|
77
|
+
|
|
78
|
+
# Разные LOD для разных расстояний (MIP-маппинг)
|
|
79
|
+
base_tile_size = 256
|
|
80
|
+
for lod in range(4): # 4 уровня детализации
|
|
81
|
+
tile_scale = 2 ** lod
|
|
82
|
+
effective_tile_size = base_tile_size / tile_scale
|
|
83
|
+
|
|
84
|
+
start_tile_x = int(min_x // effective_tile_size)
|
|
85
|
+
start_tile_y = int(min_y // effective_tile_size)
|
|
86
|
+
end_tile_x = int(max_x // effective_tile_size) + 1
|
|
87
|
+
end_tile_y = int(max_y // effective_tile_size) + 1
|
|
88
|
+
|
|
89
|
+
for tx in range(start_tile_x, end_tile_x):
|
|
90
|
+
for ty in range(start_tile_y, end_tile_y):
|
|
91
|
+
tiles.append((tx, ty, lod))
|
|
92
|
+
|
|
93
|
+
return tiles
|
|
94
|
+
|
|
95
|
+
def _texture_worker(self):
|
|
96
|
+
"""Рабочий поток для генерации текстур"""
|
|
97
|
+
while True:
|
|
98
|
+
try:
|
|
99
|
+
task = self.task_queue.get()
|
|
100
|
+
if task is None:
|
|
101
|
+
break
|
|
102
|
+
|
|
103
|
+
texture_name, tile_x, tile_y, lod = task
|
|
104
|
+
streamer = self.streamers[texture_name]
|
|
105
|
+
streamer.request_tile(tile_x, tile_y, lod)
|
|
106
|
+
|
|
107
|
+
self.task_queue.task_done()
|
|
108
|
+
except:
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
# Демонстрация
|
|
112
|
+
def demo_terrain_texture():
|
|
113
|
+
"""Создание бесконечно детализируемой текстуры terrain"""
|
|
114
|
+
|
|
115
|
+
# Параметры для terrain текстуры
|
|
116
|
+
terrain_params = FractalParams(
|
|
117
|
+
seed=42,
|
|
118
|
+
base_scale=0.005,
|
|
119
|
+
detail_level=4.0,
|
|
120
|
+
persistence=0.55,
|
|
121
|
+
lacunarity=2.1,
|
|
122
|
+
octaves=16,
|
|
123
|
+
fractal_dimension=2.7
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Создаем систему текстур
|
|
127
|
+
texture_system = GameTextureSystem()
|
|
128
|
+
|
|
129
|
+
# Регистрируем различные текстуры
|
|
130
|
+
texture_system.register_texture("terrain", terrain_params, "stone")
|
|
131
|
+
texture_system.register_texture("clouds",
|
|
132
|
+
FractalParams(seed=123, persistence=0.7, octaves=8), "clouds")
|
|
133
|
+
texture_system.register_texture("water",
|
|
134
|
+
FractalParams(seed=456, base_scale=0.002, persistence=0.4), "water")
|
|
135
|
+
|
|
136
|
+
# Имитация запросов от игрового движка
|
|
137
|
+
viewport = {'min': (0, 0), 'max': (1024, 1024)}
|
|
138
|
+
zoom_levels = [1.0, 2.0, 4.0, 8.0]
|
|
139
|
+
|
|
140
|
+
for zoom in zoom_levels:
|
|
141
|
+
print(f"\nGenerating texture tiles at zoom {zoom}x...")
|
|
142
|
+
|
|
143
|
+
terrain_tiles = texture_system.request_texture_tiles(
|
|
144
|
+
"terrain", viewport, zoom
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
print(f"Generated {len(terrain_tiles)} terrain tiles")
|
|
148
|
+
|
|
149
|
+
# Здесь можно визуализировать или сохранить тайлы
|
|
150
|
+
for (tx, ty, lod), tile in list(terrain_tiles.items())[:3]:
|
|
151
|
+
print(f" Tile ({tx},{ty}) LOD {lod}: {tile.shape}")
|
|
152
|
+
|
|
153
|
+
if terrain_tiles:
|
|
154
|
+
first_tile = next(iter(terrain_tiles.values()))
|
|
155
|
+
save_ppm(first_tile, EXAMPLES_DIR / "output" / f"game_texture_tile_{zoom:.1f}x.ppm")
|
|
156
|
+
|
|
157
|
+
return texture_system
|
|
158
|
+
|
|
159
|
+
if __name__ == "__main__":
|
|
160
|
+
parser = argparse.ArgumentParser(description="Game texture example")
|
|
161
|
+
add_interactive_args(parser)
|
|
162
|
+
args = parser.parse_args()
|
|
163
|
+
|
|
164
|
+
# Запускаем демо
|
|
165
|
+
system = demo_terrain_texture()
|
|
166
|
+
print("\nTexture system ready!")
|
|
167
|
+
|
|
168
|
+
if args.interactive:
|
|
169
|
+
config = InteractiveConfig.from_args(args, title="Game Texture (interactive)")
|
|
170
|
+
terrain_params = FractalParams(seed=42, base_scale=0.005, detail_level=3.0)
|
|
171
|
+
generator = FractalGenerator(terrain_params)
|
|
172
|
+
texture = InfiniteTexture(generator, "stone")
|
|
173
|
+
|
|
174
|
+
def render_frame(t, w, h):
|
|
175
|
+
zoom = 1.0 + 0.5 * (1 + np.sin(t * 0.2))
|
|
176
|
+
return texture.generate_tile(0, 0, w, h, zoom=zoom)[..., :3]
|
|
177
|
+
|
|
178
|
+
run_interactive(render_frame, config)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import numpy as np
|
|
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.texture_blending import TextureBlender
|
|
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="Texture blending example")
|
|
21
|
+
add_interactive_args(parser)
|
|
22
|
+
args = parser.parse_args()
|
|
23
|
+
# Генерация текстур
|
|
24
|
+
tex_gen = SimplexTextureGenerator(seed=42)
|
|
25
|
+
size = 256
|
|
26
|
+
clouds = tex_gen.generate_clouds(size, size)
|
|
27
|
+
rock = tex_gen.generate_marble(size, size)
|
|
28
|
+
grass = tex_gen.generate_grass(size, size)
|
|
29
|
+
|
|
30
|
+
height_map = rock[..., 0]
|
|
31
|
+
cloud_mask = clouds[..., 3]
|
|
32
|
+
|
|
33
|
+
blender = TextureBlender()
|
|
34
|
+
result = blender.blend_layer_stack(
|
|
35
|
+
base_texture=rock,
|
|
36
|
+
layers=[
|
|
37
|
+
{
|
|
38
|
+
"texture": grass,
|
|
39
|
+
"blend_mode": "overlay",
|
|
40
|
+
"opacity": 0.7,
|
|
41
|
+
"mask_params": {
|
|
42
|
+
"mask_type": "height_based",
|
|
43
|
+
"parameters": {
|
|
44
|
+
"height_map": height_map,
|
|
45
|
+
"min_height": 0.3,
|
|
46
|
+
"max_height": 0.6,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"texture": clouds,
|
|
52
|
+
"blend_mode": "screen",
|
|
53
|
+
"opacity": 0.3,
|
|
54
|
+
"mask": cloud_mask,
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
print("Blended texture:", result.shape, result.min(), result.max())
|
|
60
|
+
save_ppm(result, EXAMPLES_DIR / "output" / "integration_blend.ppm")
|
|
61
|
+
|
|
62
|
+
if args.interactive:
|
|
63
|
+
config = InteractiveConfig.from_args(args, title="Integration Blend (interactive)")
|
|
64
|
+
|
|
65
|
+
def render_frame(t, w, h):
|
|
66
|
+
tex_gen = SimplexTextureGenerator(seed=42)
|
|
67
|
+
clouds = tex_gen.generate_clouds(w, h, scale=0.01 + 0.003 * np.sin(t * 0.1))
|
|
68
|
+
rock = tex_gen.generate_marble(w, h, scale=0.005 + 0.002 * np.cos(t * 0.07))
|
|
69
|
+
grass = tex_gen.generate_grass(w, h, scale=0.02 + 0.01 * np.sin(t * 0.08))
|
|
70
|
+
height_map = rock[..., 0]
|
|
71
|
+
cloud_mask = clouds[..., 3]
|
|
72
|
+
blender = TextureBlender()
|
|
73
|
+
return blender.blend_layer_stack(
|
|
74
|
+
base_texture=rock,
|
|
75
|
+
layers=[
|
|
76
|
+
{
|
|
77
|
+
"texture": grass,
|
|
78
|
+
"blend_mode": "overlay",
|
|
79
|
+
"opacity": 0.7,
|
|
80
|
+
"mask_params": {
|
|
81
|
+
"mask_type": "height_based",
|
|
82
|
+
"parameters": {
|
|
83
|
+
"height_map": height_map,
|
|
84
|
+
"min_height": 0.3,
|
|
85
|
+
"max_height": 0.6,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"texture": clouds,
|
|
91
|
+
"blend_mode": "screen",
|
|
92
|
+
"opacity": 0.3,
|
|
93
|
+
"mask": cloud_mask,
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
)[..., :3]
|
|
97
|
+
|
|
98
|
+
run_interactive(render_frame, config)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
if __name__ == "__main__":
|
|
102
|
+
main()
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Геометрические паттерны для разрушаемых материалов
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
ROOT = Path(__file__).resolve().parents[2]
|
|
7
|
+
if str(ROOT) not in sys.path:
|
|
8
|
+
sys.path.insert(0, str(ROOT))
|
|
9
|
+
|
|
10
|
+
from fractex.geometric_patterns_3d import GeometricPatternGenerator3D, GeometricPattern3D, PatternParameters
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DestructibleMaterial:
|
|
14
|
+
def __init__(self, pattern_type, params):
|
|
15
|
+
self.pattern_generator = GeometricPatternGenerator3D()
|
|
16
|
+
self.base_pattern = self.pattern_generator.generate_pattern(
|
|
17
|
+
pattern_type, (32, 32, 32), params
|
|
18
|
+
)
|
|
19
|
+
self.stress_field = np.zeros((32, 32, 32), dtype=np.float32)
|
|
20
|
+
|
|
21
|
+
def apply_stress(self, position, force):
|
|
22
|
+
stress_point = self.world_to_voxel(position)
|
|
23
|
+
self.propagate_stress(stress_point, force)
|
|
24
|
+
fracture_points = self.find_fracture_points()
|
|
25
|
+
self.modify_pattern_for_fractures(fracture_points)
|
|
26
|
+
|
|
27
|
+
def propagate_stress(self, point, force, depth: int = 0):
|
|
28
|
+
if depth > 6 or force < 0.5:
|
|
29
|
+
return
|
|
30
|
+
for direction in self.get_structure_directions(point):
|
|
31
|
+
self.stress_field[tuple(point)] += force
|
|
32
|
+
next_point = point + direction
|
|
33
|
+
if self.is_connected(point, next_point):
|
|
34
|
+
self.propagate_stress(next_point, force * 0.7, depth + 1)
|
|
35
|
+
|
|
36
|
+
def world_to_voxel(self, position):
|
|
37
|
+
pos = np.clip(np.array(position, dtype=int), 0, 31)
|
|
38
|
+
return pos
|
|
39
|
+
|
|
40
|
+
def get_structure_directions(self, point):
|
|
41
|
+
return [
|
|
42
|
+
np.array([1, 0, 0]),
|
|
43
|
+
np.array([-1, 0, 0]),
|
|
44
|
+
np.array([0, 1, 0]),
|
|
45
|
+
np.array([0, -1, 0]),
|
|
46
|
+
np.array([0, 0, 1]),
|
|
47
|
+
np.array([0, 0, -1]),
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
def is_connected(self, point_a, point_b):
|
|
51
|
+
if np.any(point_b < 0) or np.any(point_b >= 32):
|
|
52
|
+
return False
|
|
53
|
+
return True
|
|
54
|
+
|
|
55
|
+
def find_fracture_points(self):
|
|
56
|
+
threshold = self.stress_field.mean() + self.stress_field.std()
|
|
57
|
+
return np.argwhere(self.stress_field > threshold)
|
|
58
|
+
|
|
59
|
+
def modify_pattern_for_fractures(self, fracture_points):
|
|
60
|
+
for p in fracture_points[:50]:
|
|
61
|
+
self.base_pattern[tuple(p)] = 0
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
if __name__ == "__main__":
|
|
65
|
+
material = DestructibleMaterial(
|
|
66
|
+
GeometricPattern3D.CRYSTAL_LATTICE,
|
|
67
|
+
PatternParameters(scale=1.0, thickness=0.05),
|
|
68
|
+
)
|
|
69
|
+
material.apply_stress((10, 10, 10), force=5.0)
|
|
70
|
+
print("Fracture points:", material.find_fracture_points().shape[0])
|