workshop rework
This commit is contained in:
1 parent
a3a80dfa16
commit
fb39e290c0
115 files changed
+1865
-497
No files matched your search
@@ -0,0 +1,159 @@
|
||||
"""Run inside Unreal (Python console / Execute Python Script).
|
||||
|
||||
Reads terrain_config.json (next to this script) and:
|
||||
1. builds a heightfield mesh with GeometryScript (fractal value noise),
|
||||
bakes it to a StaticMesh, and drops it into the level;
|
||||
2. scatters tree/rock StaticMeshActors across it per "scatter_layers",
|
||||
using the same noise field so foliage height always matches the
|
||||
ground beneath it (no raycasting needed).
|
||||
|
||||
Run import_environment_assets.py first so the mesh paths referenced in
|
||||
terrain_config.json actually exist.
|
||||
|
||||
Set the TERRAIN_CONFIG env var to point at a different config file, e.g. to
|
||||
run terrain_config_variants_demo.json without touching the default config.
|
||||
|
||||
See generate_terrain_from_heightmap.py for the same pipeline driven by a real
|
||||
heightmap image instead of procedural noise - both share terrain_common.py.
|
||||
"""
|
||||
import os, json, math, random, unreal
|
||||
import terrain_common as tc
|
||||
|
||||
CONFIG_PATH = os.environ.get(
|
||||
"TERRAIN_CONFIG",
|
||||
os.path.join(os.path.dirname(os.path.abspath(__file__)), "terrain_config_variants_demo.json"),
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fractal value noise (pure Python, no engine dependency) -------------------
|
||||
# ---------------------------------------------------------------------------
|
||||
class ValueNoise2D:
|
||||
"""Seeded, smooth, tileable-enough value noise for terrain-style height fields."""
|
||||
|
||||
def __init__(self, seed):
|
||||
rng = random.Random(seed)
|
||||
table = list(range(256))
|
||||
rng.shuffle(table)
|
||||
self._perm = table + table # avoid overflow when indexing i+1
|
||||
|
||||
@staticmethod
|
||||
def _fade(t):
|
||||
return t * t * t * (t * (t * 6 - 15) + 10)
|
||||
|
||||
@staticmethod
|
||||
def _lerp(a, b, t):
|
||||
return a + t * (b - a)
|
||||
|
||||
def _gradient(self, ix, iy):
|
||||
h = self._perm[(self._perm[ix & 255] + iy) & 255]
|
||||
angle = (h / 255.0) * 2.0 * math.pi
|
||||
return math.cos(angle), math.sin(angle)
|
||||
|
||||
def sample(self, x, y):
|
||||
x0, y0 = math.floor(x), math.floor(y)
|
||||
x1, y1 = x0 + 1, y0 + 1
|
||||
sx, sy = x - x0, y - y0
|
||||
|
||||
def dot_grid(ix, iy, px, py):
|
||||
gx, gy = self._gradient(ix, iy)
|
||||
return gx * (px - ix) + gy * (py - iy)
|
||||
|
||||
n00 = dot_grid(x0, y0, x, y)
|
||||
n10 = dot_grid(x1, y0, x, y)
|
||||
n01 = dot_grid(x0, y1, x, y)
|
||||
n11 = dot_grid(x1, y1, x, y)
|
||||
|
||||
u, v = self._fade(sx), self._fade(sy)
|
||||
return self._lerp(self._lerp(n00, n10, u), self._lerp(n01, n11, u), v)
|
||||
|
||||
|
||||
class FractalHeightField:
|
||||
def __init__(self, noise_cfg, height_scale):
|
||||
self.height_scale = height_scale
|
||||
self.frequency = noise_cfg["frequency"]
|
||||
self.octaves = noise_cfg["octaves"]
|
||||
self.lacunarity = noise_cfg["lacunarity"]
|
||||
self.gain = noise_cfg["gain"]
|
||||
self.noise = ValueNoise2D(noise_cfg["seed"])
|
||||
|
||||
def height(self, x, y):
|
||||
amplitude, frequency, total, max_amp = 1.0, self.frequency, 0.0, 0.0
|
||||
for _ in range(self.octaves):
|
||||
total += amplitude * self.noise.sample(x * frequency, y * frequency)
|
||||
max_amp += amplitude
|
||||
amplitude *= self.gain
|
||||
frequency *= self.lacunarity
|
||||
return (total / max_amp) * self.height_scale
|
||||
|
||||
def normal(self, x, y, step=200.0):
|
||||
return tc.finite_difference_normal(self.height, x, y, step)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
def generate_tile(config, terrain_cfg, tile_label):
|
||||
"""Builds one terrain + its scatter layers + its explicit placements, all
|
||||
under a dedicated Outliner folder so multiple tiles can coexist in one level.
|
||||
"""
|
||||
field = FractalHeightField(terrain_cfg["noise"], terrain_cfg["height_scale"])
|
||||
base_folder = f"ProceduralTerrain/{tile_label}"
|
||||
|
||||
print(f"[{tile_label}] building terrain mesh (seed={terrain_cfg['noise']['seed']})...")
|
||||
static_mesh, bounds = tc.build_terrain_mesh(terrain_cfg, field)
|
||||
|
||||
loc = terrain_cfg["location"]
|
||||
origin = unreal.Vector(loc[0], loc[1], loc[2])
|
||||
tc.spawn_terrain_actor(static_mesh, origin, base_folder, f"Terrain_{tile_label}")
|
||||
|
||||
for layer_cfg in config.get("scatter_layers", []):
|
||||
tc.scatter_layer(layer_cfg, field, bounds, origin, base_folder)
|
||||
|
||||
explicit_entries = config.get("explicit_placements", [])
|
||||
if explicit_entries:
|
||||
tc.place_explicit(explicit_entries, field, origin, base_folder)
|
||||
|
||||
|
||||
def main():
|
||||
with open(CONFIG_PATH, "r") as f:
|
||||
config = json.load(f)
|
||||
|
||||
base_terrain_cfg = config["terrain"]
|
||||
variants = config.get("variants")
|
||||
|
||||
if not variants:
|
||||
# Single-tile mode: unchanged behavior for existing configs.
|
||||
generate_tile(config, base_terrain_cfg, "Main")
|
||||
print("Done.")
|
||||
return
|
||||
|
||||
# Variant mode: one JSON list of seeds becomes N independent, deterministic
|
||||
# tiles laid out side by side in a single run - each a full reproducible
|
||||
# world driven by nothing but that seed. There's no "batch-produce N baked,
|
||||
# comparable layouts from one data file" workflow in the Foliage tool or a
|
||||
# PCG graph; you'd re-author or re-seed each one by hand, one at a time.
|
||||
gap = base_terrain_cfg["size_x"] * 1.2
|
||||
base_seed = base_terrain_cfg["noise"]["seed"]
|
||||
base_layer_seeds = {layer["name"]: layer.get("seed", 0) for layer in config.get("scatter_layers", [])}
|
||||
|
||||
for i, seed_offset in enumerate(variants):
|
||||
tile_label = f"Variant_{seed_offset}"
|
||||
terrain_cfg = json.loads(json.dumps(base_terrain_cfg)) # cheap deep copy
|
||||
terrain_cfg["noise"]["seed"] = base_seed + seed_offset
|
||||
terrain_cfg["asset_name"] = f"{base_terrain_cfg['asset_name']}_{tile_label}"
|
||||
terrain_cfg["location"] = [
|
||||
base_terrain_cfg["location"][0] + i * gap,
|
||||
base_terrain_cfg["location"][1],
|
||||
base_terrain_cfg["location"][2],
|
||||
]
|
||||
|
||||
tile_config = json.loads(json.dumps(config))
|
||||
tile_config["terrain"] = terrain_cfg
|
||||
for layer_cfg in tile_config.get("scatter_layers", []):
|
||||
layer_cfg["seed"] = base_layer_seeds.get(layer_cfg["name"], 0) + seed_offset
|
||||
|
||||
generate_tile(tile_config, terrain_cfg, tile_label)
|
||||
|
||||
print(f"Done: {len(variants)} variant(s) generated side by side.")
|
||||
|
||||
|
||||
main()
|
||||
Reference in new issue
Block a user