1. This site uses cookies. By continuing to use this site, you are agreeing to our use of cookies. Learn More.
  2. Anuncie Aqui
    Anuncie aqui você Também: fdantas@4each.com.br

[Python] How should I go about generating a height/elevation map for a procedurally generated...

Discussão em 'Python' iniciado por Stack, Setembro 17, 2025.

  1. Stack

    Stack Membro Participativo

    I'm currently working on a personal project on making a procedural map generator. The way it currently is working is not the most intuitive, it's three different scripts all importing from each other, the current algorithm is:

    1. Continent Generation:
    • Generate ~70 randomly scaled circles and places them starting from the biggest one, the subsequent circles are placed randomly along the edges of the blob.
    • Once that's done, It averages out the edges to smooth it out and goes through a simplex noise pass and a perlin noise pass to rough up the edges.
    1. Continent placement.
    • Imports the generated result from the continent generation script and places bounding ellipses to approximate the size.
    • Places a main continent randomly, biased against the center of the map.
    • Calculates the remaining amount of space
    • Finds where there is the biggest empty space and places the remaining continents.

    I'm currently trying to make another script to take the result from script the Continent Placement script to try and generate elevation and color to render it, but I don't really know where to go from here. Heres the MapGen code that I currently have:

    import random
    import math
    import numpy as np
    import matplotlib.pyplot as plt
    from shapely.geometry import Point, Polygon
    from shapely.affinity import scale as shp_scale, rotate as shp_rotate, translate as shp_translate

    try:
    from tqdm import tqdm
    except Exception:
    tqdm = lambda x: x # fallback if tqdm missing

    # Optional continent generator fallback
    try:
    from ContGen import generate_continent_simple
    except Exception:
    def generate_continent_simple(seed=None):
    if seed is not None:
    random.seed(seed)
    pts = [
    (math.cos(ang) * (40 + random.random() * 30) + random.uniform(-5, 5),
    math.sin(ang) * (40 + random.random() * 30) + random.uniform(-5, 5))
    for ang in np.linspace(0, 2 * math.pi, 20, endpoint=False)
    ]
    return Polygon(pts).buffer(0)

    # ------------------
    # Defaults
    # ------------------
    DEFAULT_PARAMS = {
    "RANDOM_SEED": 123453,
    "MAP_WIDTH": 800,
    "MAP_HEIGHT": 600,
    "PADDING": 6.0,
    "CONTINENTS_SUM": 4,
    "MAIN_FIXED_AXIS": 500,
    "POST_PLACEMENT_SCALING": 0.92,
    "MAX_ATTEMPTS": 250,
    "DEBUG_MODE": False,
    "ANIMATION_DELAY": 0.01,
    "ELLIPSE_RESOLUTION": 64,
    "OPENSEARCH_STEP": 60,
    "OPEN_CANDIDATES": 12,
    "MAXFIT_BINARY_ITERS": 28,
    "MIN_ACCEPT_HALF_AXIS": 2.0,
    }

    _unit_circle_cache = {}

    # ------------------
    # Geometry helpers
    # ------------------
    def _get_unit_circle(res=64):
    if res not in _unit_circle_cache:
    _unit_circle_cache[res] = Point(0, 0).buffer(1.0, resolution=res)
    return _unit_circle_cache[res]

    def normalize_center(cx, cy, w, h):
    return (cx % w, cy % h)

    def wrapped_copies(geom, w, h):
    return [shp_translate(geom, dx, dy) if (dx or dy) else geom
    for dx in (-w, 0, w) for dy in (-h, 0, h)]

    def ellipse_crosses_axes(ellipse, w, h):
    minx, miny, maxx, maxy = ellipse.bounds
    return (minx < 0 or maxx > w), (miny < 0 or maxy > h)

    def crosses_both_axes(ellipse, w, h):
    cx, cy = ellipse_crosses_axes(ellipse, w, h)
    return cx and cy

    def build_ellipse_polygon(cx, cy, a, b, angle, res=64):
    return shp_translate(
    shp_rotate(shp_scale(_get_unit_circle(res), xfact=a, yfact=b, origin=(0, 0)),
    angle, origin=(0, 0), use_radians=False),
    xoff=cx, yoff=cy)

    # ------------------
    # Fit ellipse to polygon
    # ------------------
    def fit_oriented_ellipse(poly):
    coords = np.array(poly.exterior.coords)
    if len(coords) > 1 and np.allclose(coords[0], coords[-1]):
    coords = coords[:-1]
    centroid = coords.mean(axis=0)
    pts = coords - centroid

    if len(pts) < 3:
    minx, miny, maxx, maxy = poly.bounds
    cx, cy = (minx + maxx) / 2, (miny + maxy) / 2
    a, b = max((maxx - minx) / 2, 1e-6), max((maxy - miny) / 2, 1e-6)
    return cx, cy, a, b, 0.0, math.pi * a * b

    cov = np.cov(pts.T)
    eigvals, eigvecs = np.linalg.eigh(cov)
    v1 = eigvecs[:, eigvals.argsort()[::-1][0]]
    v2 = eigvecs[:, eigvals.argsort()[::-1][1]]
    a, b = max(abs(pts.dot(v1)).max(), 1e-6), max(abs(pts.dot(v2)).max(), 1e-6)
    a *= 1.03; b *= 1.03
    angle = math.degrees(math.atan2(v1[1], v1[0]))
    return centroid[0], centroid[1], a, b, angle, math.pi * a * b

    # ------------------
    # Candidate helpers
    # ------------------
    def biased_random_position(w, h, margin=0.35):
    cx, cy, max_dist, attempts = w / 2, h / 2, math.hypot(w/2, h/2), 0
    while True:
    attempts += 1
    x, y = random.uniform(0, w), random.uniform(0, h)
    if random.random() < (math.hypot(x - cx, y - cy) / max_dist) ** (1.0 + margin) or attempts > 500:
    return x, y

    def find_open_centers(placed, w, h, pad, step, top_k):
    xs, ys = np.arange(step//2, w, step), np.arange(step//2, h, step)
    candidates = []
    for x in xs:
    for y in ys:
    pt, min_d = Point(x, y), float('inf')
    for e in placed:
    for dx in (-w, 0, w):
    for dy in (-h, 0, h):
    d = pt.distance(shp_translate(e, dx, dy) if (dx or dy) else e)
    if d < min_d:
    min_d = d
    if min_d <= 0:
    break
    if min_d <= 0: break
    if min_d <= 0: break
    avail = max(0.0, min_d - pad)
    if avail > 1.0:
    candidates.append((avail, (x, y)))
    return [c[1] for c in sorted(candidates, key=lambda t: t[0], reverse=True)[:top_k]]

    def torus_intersects(candidate, placed, w, h, pad):
    cand_buffers = [g.buffer(pad) for g in wrapped_copies(candidate, w, h)]
    for p in placed:
    buf = p.buffer(pad)
    if any(cb.intersects(buf) for cb in cand_buffers):
    return True
    return False

    # ------------------
    # Scale fitting
    # ------------------
    def compute_max_scale_for_center(a0, b0, angle, center, placed, w, h, pad, iters, s_cap, min_axis):
    cx, cy = center
    s_high = min(s_cap, max(w, h) / max(a0, b0)) if (s_cap and max(a0, b0) > 0) else max(w, h) / max(a0, b0)
    if s_high <= 0: return 0.0
    low, high = 0.0, s_high

    for _ in range(iters):
    mid = (low + high) / 2.0
    a_mid, b_mid = a0 * mid, b0 * mid
    if a_mid < min_axis or b_mid < min_axis:
    low = mid; continue
    nx, ny = normalize_center(cx, cy, w, h)
    if crosses_both_axes(build_ellipse_polygon(nx, ny, a_mid, b_mid, angle), w, h):
    high = mid; continue
    ell_mid = build_ellipse_polygon(cx, cy, a_mid, b_mid, angle)
    if torus_intersects(ell_mid, placed, w, h, pad):
    high = mid
    else:
    low = mid
    return low

    # ------------------
    # Placement core
    # ------------------
    def place_ellipses_with_retries(polys, fits, params=None, debug_ax=None):
    params = {**DEFAULT_PARAMS, **(params or {})}
    w, h = int(params["MAP_WIDTH"]), int(params["MAP_HEIGHT"])
    pad, res = float(params["PADDING"]), int(params["ELLIPSE_RESOLUTION"])
    max_attempts = int(params["MAX_ATTEMPTS"])
    op_step, op_k = int(params["OPENSEARCH_STEP"]), int(params["OPEN_CANDIDATES"])
    bin_iters, min_axis = int(params["MAXFIT_BINARY_ITERS"]), float(params["MIN_ACCEPT_HALF_AXIS"])

    if params["RANDOM_SEED"] is not None:
    random.seed(params["RANDOM_SEED"]); np.random.seed(params["RANDOM_SEED"])

    if not polys: return []

    results, placed = [], []

    # --- place main continent
    main_idx = random.randrange(len(polys))
    main_poly, main_fit = polys[main_idx], fits[main_idx]
    a0, b0, area0 = main_fit[2], main_fit[3], max(main_fit[5], 1.0)

    if params["MAIN_FIXED_AXIS"]:
    s_main = (params["MAIN_FIXED_AXIS"] / 2) / max(a0, b0)
    else:
    target_area = w * h * 0.4
    s_main = math.sqrt(target_area / area0)

    a_main, b_main = a0 * s_main, b0 * s_main

    for _ in range(max_attempts):
    rot = random.uniform(0, 360)
    cx, cy = biased_random_position(w, h)
    ell = build_ellipse_polygon(cx, cy, a_main, b_main, rot, res)
    nx, ny = normalize_center(cx, cy, w, h)
    if not crosses_both_axes(build_ellipse_polygon(nx, ny, a_main, b_main, rot, res), w, h) and not torus_intersects(ell, placed, w, h, pad):
    ell_canon = build_ellipse_polygon(nx, ny, a_main, b_main, rot, res)
    placed.append(ell_canon)
    results.append({"orig_index": main_idx, "orig_poly": main_poly, "orig_fit": main_fit,
    "ellipse": ell_canon, "scale": s_main, "angle": rot, "center": (nx, ny)})
    break
    else:
    raise RuntimeError("Could not place main continent.")

    main_max = max((results[0]["ellipse"].bounds[2] - results[0]["ellipse"].bounds[0]) / 2,
    (results[0]["ellipse"].bounds[3] - results[0]["ellipse"].bounds[1]) / 2)

    # --- place remaining
    for orig_idx, poly, fit in zip(
    [i for i in range(len(polys)) if i != main_idx],
    [p for i, p in enumerate(polys) if i != main_idx],
    [f for i, f in enumerate(fits) if i != main_idx]):

    a0, b0 = fit[2], fit[3]
    s_cap = main_max / max(a0, b0) if max(a0, b0) > 0 else None

    candidates = find_open_centers(placed, w, h, pad, op_step, op_k) or \
    [(random.uniform(0, w), random.uniform(0, h)) for _ in range(6)]

    best = None
    for center in candidates:
    for rot in [fit[4] % 360, (fit[4] + 90) % 360, random.uniform(0, 360)]:
    s_max = compute_max_scale_for_center(a0, b0, rot, center, placed, w, h, pad, bin_iters, s_cap, min_axis)
    if s_max <= 0: continue
    nx, ny = normalize_center(*center, w, h)
    ell = build_ellipse_polygon(nx, ny, a0 * s_max, b0 * s_max, rot, res)
    if not crosses_both_axes(ell, w, h) and (not best or ell.area > best["ellipse"].area):
    best = {"orig_index": orig_idx, "orig_poly": poly, "orig_fit": fit,
    "ellipse": ell, "scale": s_max, "angle": rot, "center": (nx, ny)}

    if best:
    placed.append(best["ellipse"])
    results.append(best)

    # --- post placement scaling
    if 0 < params["POST_PLACEMENT_SCALING"] < 1:
    for item in results:
    s = item["scale"] * params["POST_PLACEMENT_SCALING"]
    item["scale"] = s
    item["ellipse"] = shp_scale(item["ellipse"], xfact=params["POST_PLACEMENT_SCALING"],
    yfact=params["POST_PLACEMENT_SCALING"], origin='center')
    return results

    # ------------------
    # Transform + draw
    # ------------------
    def apply_transform_and_draw(placements, params=None, show=True):
    params = {**DEFAULT_PARAMS, **(params or {})}
    w, h = int(params["MAP_WIDTH"]), int(params["MAP_HEIGHT"])
    rect = Polygon([(0,0),(w,0),(w,h),(0,h)])

    final_polys = []
    for item in placements:
    poly, fit, s, ang, (cx, cy) = item["orig_poly"], item["orig_fit"], item["scale"], item["angle"], item["center"]
    transformed = shp_scale(poly, xfact=s, yfact=s, origin='center')
    if fit: transformed = shp_rotate(transformed, ang - fit[4], origin='center', use_radians=False)
    transformed = shp_translate(transformed, xoff=cx - transformed.centroid.x, yoff=cy - transformed.centroid.y)
    final_polys.append(transformed)

    if show:
    fig, ax = plt.subplots(figsize=(12, 8))
    ax.set_xlim(0, w); ax.set_ylim(0, h); ax.set_aspect('equal')
    bx, by = rect.exterior.xy; ax.plot(bx, by, color='black', linewidth=2)
    for item in placements:
    for copy in wrapped_copies(item["ellipse"], w, h):
    if copy.intersects(rect):
    ex, ey = copy.exterior.xy; ax.plot(ex, ey, '--', color='gray', alpha=0.6)
    for poly in final_polys:
    for copy in wrapped_copies(poly, w, h):
    if copy.intersects(rect):
    x, y = copy.exterior.xy; ax.fill(x, y, alpha=0.75, color='green')
    plt.show()
    return final_polys

    # ------------------
    # Public API
    # ------------------
    def generate_map(params=None, show=True):
    params = {**DEFAULT_PARAMS, **(params or {})}
    if params["RANDOM_SEED"] is not None:
    random.seed(params["RANDOM_SEED"]); np.random.seed(params["RANDOM_SEED"])

    cnt = int(params["CONTINENTS_SUM"])
    raw = [generate_continent_simple(seed=random.randint(0, 10**9)) for _ in range(cnt)]
    fits = [fit_oriented_ellipse(p) for p in raw]
    placements = place_ellipses_with_retries(raw, fits, params)
    return placements, apply_transform_and_draw(placements, params, show)

    # ------------------
    # Demo
    # ------------------
    def main():
    params = {**DEFAULT_PARAMS, "DEBUG_MODE": True, "RANDOM_SEED": 42}
    placements, _ = generate_map(params, show=True)
    print(f"Placed {len(placements)} continents.")

    if __name__ == "__main__":
    main()


    I tried making the heightmap/elevation levels for coloring using perlin noise, but it ended up looking like it was just slapped on top of the map, and didn't really look right, I figured if I wanted it to look good the elevation map needs to follow the main topology and outlines of the simple map. Does anyone have any Ideas?

    Continue reading...

Compartilhe esta Página