aboutsummaryrefslogtreecommitdiff
path: root/tools/terrain_map_preview_renderer.py
diff options
context:
space:
mode:
Diffstat (limited to 'tools/terrain_map_preview_renderer.py')
-rw-r--r--tools/terrain_map_preview_renderer.py679
1 files changed, 0 insertions, 679 deletions
diff --git a/tools/terrain_map_preview_renderer.py b/tools/terrain_map_preview_renderer.py
deleted file mode 100644
index 86d72d7..0000000
--- a/tools/terrain_map_preview_renderer.py
+++ /dev/null
@@ -1,679 +0,0 @@
-#!/usr/bin/env python3
-"""
-Software 3D renderer for terrain Land.msh + Land.map overlay.
-
-Output format: binary PPM (P6), dependency-free.
-"""
-
-from __future__ import annotations
-
-import argparse
-import math
-import struct
-from pathlib import Path
-from typing import Any
-
-import archive_roundtrip_validator as arv
-
-MAGIC_NRES = b"NRes"
-
-
-def _entry_payload(blob: bytes, entry: dict[str, Any]) -> bytes:
- start = int(entry["data_offset"])
- end = start + int(entry["size"])
- return blob[start:end]
-
-
-def _parse_nres(blob: bytes, source: str) -> dict[str, Any]:
- if blob[:4] != MAGIC_NRES:
- raise RuntimeError(f"{source}: not an NRes payload")
- return arv.parse_nres(blob, source=source)
-
-
-def _by_type(entries: list[dict[str, Any]]) -> dict[int, list[dict[str, Any]]]:
- out: dict[int, list[dict[str, Any]]] = {}
- for row in entries:
- out.setdefault(int(row["type_id"]), []).append(row)
- return out
-
-
-def _get_single(by_type: dict[int, list[dict[str, Any]]], type_id: int, label: str) -> dict[str, Any]:
- rows = by_type.get(type_id, [])
- if not rows:
- raise RuntimeError(f"missing resource type {type_id} ({label})")
- return rows[0]
-
-
-def _downsample_faces(
- faces: list[tuple[int, int, int]],
- max_faces: int,
-) -> list[tuple[int, int, int]]:
- if max_faces <= 0 or len(faces) <= max_faces:
- return faces
- step = len(faces) / max_faces
- out: list[tuple[int, int, int]] = []
- pos = 0.0
- while len(out) < max_faces and int(pos) < len(faces):
- out.append(faces[int(pos)])
- pos += step
- return out
-
-
-def load_terrain_msh(
- path: Path,
- *,
- max_faces: int,
-) -> tuple[list[tuple[float, float, float]], list[tuple[int, int, int]], dict[str, int]]:
- blob = path.read_bytes()
- parsed = _parse_nres(blob, str(path))
- by_type = _by_type(parsed["entries"])
-
- res3 = _get_single(by_type, 3, "positions")
- res21 = _get_single(by_type, 21, "terrain faces")
-
- pos_blob = _entry_payload(blob, res3)
- if len(pos_blob) % 12 != 0:
- raise RuntimeError(f"{path}: type 3 payload size is not divisible by 12")
- vertex_count = len(pos_blob) // 12
- positions = [struct.unpack_from("<3f", pos_blob, i * 12) for i in range(vertex_count)]
-
- face_blob = _entry_payload(blob, res21)
- if len(face_blob) % 28 != 0:
- raise RuntimeError(f"{path}: type 21 payload size is not divisible by 28")
- all_faces: list[tuple[int, int, int]] = []
- raw_face_count = len(face_blob) // 28
- dropped = 0
- for i in range(raw_face_count):
- off = i * 28
- i0, i1, i2 = struct.unpack_from("<HHH", face_blob, off + 8)
- if i0 >= vertex_count or i1 >= vertex_count or i2 >= vertex_count:
- dropped += 1
- continue
- all_faces.append((i0, i1, i2))
-
- faces = _downsample_faces(all_faces, max_faces)
- meta = {
- "vertex_count": vertex_count,
- "face_count_raw": raw_face_count,
- "face_count_valid": len(all_faces),
- "face_count_rendered": len(faces),
- "face_dropped_invalid": dropped,
- }
- return positions, faces, meta
-
-
-def load_areal_map(path: Path) -> tuple[list[dict[str, Any]], dict[str, int]]:
- blob = path.read_bytes()
- parsed = _parse_nres(blob, str(path))
- by_type = _by_type(parsed["entries"])
- chunk = _get_single(by_type, 12, "ArealMapGeometry")
-
- payload = _entry_payload(blob, chunk)
- areal_count = int(chunk["attr1"])
- ptr = 0
- areals: list[dict[str, Any]] = []
- for idx in range(areal_count):
- if ptr + 56 > len(payload):
- raise RuntimeError(f"{path}: truncated areal header at index={idx}")
- class_id = struct.unpack_from("<I", payload, ptr + 40)[0]
- vertex_count, poly_count = struct.unpack_from("<II", payload, ptr + 48)
- verts_off = ptr + 56
- verts_size = 12 * vertex_count
- if verts_off + verts_size > len(payload):
- raise RuntimeError(f"{path}: areal[{idx}] vertices out of bounds")
- verts = [struct.unpack_from("<3f", payload, verts_off + 12 * i) for i in range(vertex_count)]
-
- links_off = verts_off + verts_size
- links_size = 8 * (vertex_count + 3 * poly_count)
- p = links_off + links_size
- for _ in range(poly_count):
- if p + 4 > len(payload):
- raise RuntimeError(f"{path}: areal[{idx}] poly header out of bounds")
- n = struct.unpack_from("<I", payload, p)[0]
- p += 4 * (3 * n + 1)
- if p > len(payload):
- raise RuntimeError(f"{path}: areal[{idx}] poly data out of bounds")
-
- areals.append(
- {
- "index": idx,
- "class_id": class_id,
- "vertices": verts,
- }
- )
- ptr = p
-
- if ptr + 8 > len(payload):
- raise RuntimeError(f"{path}: missing cells section")
- cells_x, cells_y = struct.unpack_from("<II", payload, ptr)
- ptr += 8
- for _x in range(cells_x):
- for _y in range(cells_y):
- if ptr + 2 > len(payload):
- raise RuntimeError(f"{path}: cells section truncated")
- hit_count = struct.unpack_from("<H", payload, ptr)[0]
- ptr += 2 + 2 * hit_count
- if ptr > len(payload):
- raise RuntimeError(f"{path}: cells section out of bounds")
- if ptr != len(payload):
- raise RuntimeError(f"{path}: trailing bytes in chunk12 parse ({len(payload) - ptr})")
-
- meta = {
- "areal_count": areal_count,
- "cells_x": cells_x,
- "cells_y": cells_y,
- }
- return areals, meta
-
-
-def _color_for_class(class_id: int) -> tuple[int, int, int]:
- x = (class_id * 1103515245 + 12345) & 0x7FFFFFFF
- r = 60 + (x & 0x7F)
- g = 60 + ((x >> 7) & 0x7F)
- b = 60 + ((x >> 14) & 0x7F)
- return r, g, b
-
-
-def _write_ppm(path: Path, width: int, height: int, rgb: bytearray) -> None:
- path.parent.mkdir(parents=True, exist_ok=True)
- with path.open("wb") as handle:
- handle.write(f"P6\n{width} {height}\n255\n".encode("ascii"))
- handle.write(rgb)
-
-
-def _write_obj(
- path: Path,
- terrain_positions: list[tuple[float, float, float]],
- terrain_faces: list[tuple[int, int, int]],
- areals: list[dict[str, Any]],
- *,
- include_areals: bool,
-) -> None:
- path.parent.mkdir(parents=True, exist_ok=True)
- with path.open("w", encoding="utf-8", newline="\n") as out:
- out.write("# Exported by terrain_map_preview_renderer.py\n")
- out.write("o terrain\n")
- for x, y, z in terrain_positions:
- out.write(f"v {x:.9g} {y:.9g} {z:.9g}\n")
- for i0, i1, i2 in terrain_faces:
- # OBJ indices are 1-based.
- out.write(f"f {i0 + 1} {i1 + 1} {i2 + 1}\n")
-
- if include_areals and areals:
- base = len(terrain_positions)
- area_vertex_counts: list[int] = []
- out.write("o areal_edges\n")
- for area in areals:
- verts = area["vertices"]
- area_vertex_counts.append(len(verts))
- for x, y, z in verts:
- out.write(f"v {x:.9g} {y:.9g} {z:.9g}\n")
-
- ptr = base
- for area_idx, area in enumerate(areals):
- cnt = area_vertex_counts[area_idx]
- if cnt < 2:
- ptr += cnt
- continue
- # closed polyline.
- line = [str(ptr + i + 1) for i in range(cnt)]
- line.append(str(ptr + 1))
- out.write("l " + " ".join(line) + "\n")
- ptr += cnt
-
-
-def _render_scene(
- terrain_positions: list[tuple[float, float, float]],
- terrain_faces: list[tuple[int, int, int]],
- areals: list[dict[str, Any]],
- *,
- width: int,
- height: int,
- yaw_deg: float,
- pitch_deg: float,
- wireframe: bool,
- areal_overlay: bool,
-) -> bytearray:
- all_positions = list(terrain_positions)
- if areal_overlay:
- for area in areals:
- all_positions.extend(area["vertices"])
- if not all_positions:
- raise RuntimeError("scene is empty")
-
- xs = [p[0] for p in all_positions]
- ys = [p[1] for p in all_positions]
- zs = [p[2] for p in all_positions]
- cx = (min(xs) + max(xs)) * 0.5
- cy = (min(ys) + max(ys)) * 0.5
- cz = (min(zs) + max(zs)) * 0.5
- span = max(max(xs) - min(xs), max(ys) - min(ys), max(zs) - min(zs))
- radius = max(span * 0.5, 1e-3)
-
- yaw = math.radians(yaw_deg)
- pitch = math.radians(pitch_deg)
- cyaw = math.cos(yaw)
- syaw = math.sin(yaw)
- cpitch = math.cos(pitch)
- spitch = math.sin(pitch)
- camera_dist = radius * 3.2
- scale = min(width, height) * 0.96
-
- # Terrain transform cache.
- vx: list[float] = []
- vy: list[float] = []
- vz: list[float] = []
- sx: list[float] = []
- sy: list[float] = []
- for x, y, z in terrain_positions:
- x0 = x - cx
- y0 = y - cy
- z0 = z - cz
- x1 = cyaw * x0 + syaw * z0
- z1 = -syaw * x0 + cyaw * z0
- y2 = cpitch * y0 - spitch * z1
- z2 = spitch * y0 + cpitch * z1 + camera_dist
- if z2 < 1e-3:
- z2 = 1e-3
- vx.append(x1)
- vy.append(y2)
- vz.append(z2)
- sx.append(width * 0.5 + (x1 / z2) * scale)
- sy.append(height * 0.5 - (y2 / z2) * scale)
-
- def project_point(x: float, y: float, z: float) -> tuple[float, float, float]:
- x0 = x - cx
- y0 = y - cy
- z0 = z - cz
- x1 = cyaw * x0 + syaw * z0
- z1 = -syaw * x0 + cyaw * z0
- y2 = cpitch * y0 - spitch * z1
- z2 = spitch * y0 + cpitch * z1 + camera_dist
- if z2 < 1e-3:
- z2 = 1e-3
- px = width * 0.5 + (x1 / z2) * scale
- py = height * 0.5 - (y2 / z2) * scale
- return px, py, z2
-
- rgb = bytearray([14, 16, 20] * (width * height))
- zbuf = [float("inf")] * (width * height)
- light_dir = (0.35, 0.45, 1.0)
- l_len = math.sqrt(light_dir[0] ** 2 + light_dir[1] ** 2 + light_dir[2] ** 2)
- light = (light_dir[0] / l_len, light_dir[1] / l_len, light_dir[2] / l_len)
-
- def edge(ax: float, ay: float, bx: float, by: float, px: float, py: float) -> float:
- return (px - ax) * (by - ay) - (py - ay) * (bx - ax)
-
- for i0, i1, i2 in terrain_faces:
- x0 = sx[i0]
- y0 = sy[i0]
- x1 = sx[i1]
- y1 = sy[i1]
- x2 = sx[i2]
- y2 = sy[i2]
- area = edge(x0, y0, x1, y1, x2, y2)
- if area == 0.0:
- continue
-
- ux = vx[i1] - vx[i0]
- uy = vy[i1] - vy[i0]
- uz = vz[i1] - vz[i0]
- wx = vx[i2] - vx[i0]
- wy = vy[i2] - vy[i0]
- wz = vz[i2] - vz[i0]
- nx = uy * wz - uz * wy
- ny = uz * wx - ux * wz
- nz = ux * wy - uy * wx
- n_len = math.sqrt(nx * nx + ny * ny + nz * nz)
- if n_len > 0.0:
- nx /= n_len
- ny /= n_len
- nz /= n_len
- intensity = nx * light[0] + ny * light[1] + nz * light[2]
- if intensity < 0.0:
- intensity = 0.0
- shade = int(45 + 185 * intensity)
- color = (min(255, shade + 6), min(255, shade + 14), min(255, shade + 28))
-
- minx = int(max(0, math.floor(min(x0, x1, x2))))
- maxx = int(min(width - 1, math.ceil(max(x0, x1, x2))))
- miny = int(max(0, math.floor(min(y0, y1, y2))))
- maxy = int(min(height - 1, math.ceil(max(y0, y1, y2))))
- if minx > maxx or miny > maxy:
- continue
-
- z0 = vz[i0]
- z1 = vz[i1]
- z2 = vz[i2]
- inv_area = 1.0 / area
- for py in range(miny, maxy + 1):
- fy = py + 0.5
- row = py * width
- for px in range(minx, maxx + 1):
- fx = px + 0.5
- w0 = edge(x1, y1, x2, y2, fx, fy)
- w1 = edge(x2, y2, x0, y0, fx, fy)
- w2 = edge(x0, y0, x1, y1, fx, fy)
- if area > 0:
- if w0 < 0 or w1 < 0 or w2 < 0:
- continue
- else:
- if w0 > 0 or w1 > 0 or w2 > 0:
- continue
- bz0 = w0 * inv_area
- bz1 = w1 * inv_area
- bz2 = w2 * inv_area
- depth = bz0 * z0 + bz1 * z1 + bz2 * z2
- idx = row + px
- if depth >= zbuf[idx]:
- continue
- zbuf[idx] = depth
- p = idx * 3
- rgb[p + 0] = color[0]
- rgb[p + 1] = color[1]
- rgb[p + 2] = color[2]
-
- def draw_line(
- xa: float,
- ya: float,
- xb: float,
- yb: float,
- color: tuple[int, int, int],
- ) -> None:
- x0i = int(round(xa))
- y0i = int(round(ya))
- x1i = int(round(xb))
- y1i = int(round(yb))
- dx = abs(x1i - x0i)
- sx_step = 1 if x0i < x1i else -1
- dy = -abs(y1i - y0i)
- sy_step = 1 if y0i < y1i else -1
- err = dx + dy
- x = x0i
- y = y0i
- while True:
- if 0 <= x < width and 0 <= y < height:
- p = (y * width + x) * 3
- rgb[p + 0] = color[0]
- rgb[p + 1] = color[1]
- rgb[p + 2] = color[2]
- if x == x1i and y == y1i:
- break
- e2 = 2 * err
- if e2 >= dy:
- err += dy
- x += sx_step
- if e2 <= dx:
- err += dx
- y += sy_step
-
- if wireframe:
- wf = (225, 232, 246)
- for i0, i1, i2 in terrain_faces:
- draw_line(sx[i0], sy[i0], sx[i1], sy[i1], wf)
- draw_line(sx[i1], sy[i1], sx[i2], sy[i2], wf)
- draw_line(sx[i2], sy[i2], sx[i0], sy[i0], wf)
-
- if areal_overlay:
- for area in areals:
- verts = area["vertices"]
- if len(verts) < 2:
- continue
- color = _color_for_class(int(area["class_id"]))
- projected = [project_point(x, y, z + 0.35) for x, y, z in verts]
- for i in range(len(projected)):
- x0, y0, _ = projected[i]
- x1, y1, _ = projected[(i + 1) % len(projected)]
- draw_line(x0, y0, x1, y1, color)
-
- return rgb
-
-
-def cmd_render(args: argparse.Namespace) -> int:
- msh_path = Path(args.land_msh).resolve()
- map_path = Path(args.land_map).resolve() if args.land_map else None
- output_path = Path(args.output).resolve()
-
- positions, faces, terrain_meta = load_terrain_msh(msh_path, max_faces=int(args.max_faces))
- areals: list[dict[str, Any]] = []
- map_meta: dict[str, int] = {"areal_count": 0, "cells_x": 0, "cells_y": 0}
- if map_path:
- areals, map_meta = load_areal_map(map_path)
-
- rgb = _render_scene(
- positions,
- faces,
- areals,
- width=int(args.width),
- height=int(args.height),
- yaw_deg=float(args.yaw),
- pitch_deg=float(args.pitch),
- wireframe=bool(args.wireframe),
- areal_overlay=bool(args.overlay_areals),
- )
- _write_ppm(output_path, int(args.width), int(args.height), rgb)
-
- print(f"Rendered terrain : {msh_path}")
- if map_path:
- print(f"Areal overlay : {map_path}")
- print(f"Output : {output_path}")
- print(
- "Terrain geometry : "
- f"vertices={terrain_meta['vertex_count']}, "
- f"faces={terrain_meta['face_count_rendered']}/{terrain_meta['face_count_valid']} "
- f"(raw={terrain_meta['face_count_raw']}, dropped={terrain_meta['face_dropped_invalid']})"
- )
- if map_path:
- print(
- "Areal map : "
- f"areals={map_meta['areal_count']}, cells={map_meta['cells_x']}x{map_meta['cells_y']}"
- )
- return 0
-
-
-def cmd_export_obj(args: argparse.Namespace) -> int:
- msh_path = Path(args.land_msh).resolve()
- map_path = Path(args.land_map).resolve() if args.land_map else None
- output_path = Path(args.output).resolve()
-
- positions, faces, terrain_meta = load_terrain_msh(msh_path, max_faces=int(args.max_faces))
- areals: list[dict[str, Any]] = []
- if map_path and bool(args.include_areals):
- areals, _ = load_areal_map(map_path)
-
- _write_obj(
- output_path,
- positions,
- faces,
- areals,
- include_areals=bool(args.include_areals),
- )
-
- areal_vertices = sum(len(a["vertices"]) for a in areals)
- print(f"Terrain source : {msh_path}")
- if map_path:
- print(f"Areal source : {map_path}")
- print(f"OBJ output : {output_path}")
- print(
- "Terrain geometry : "
- f"vertices={terrain_meta['vertex_count']}, "
- f"faces={terrain_meta['face_count_rendered']}/{terrain_meta['face_count_valid']}"
- )
- if bool(args.include_areals):
- print(f"Areal edges : areals={len(areals)}, extra_vertices={areal_vertices}")
- return 0
-
-
-def cmd_render_turntable(args: argparse.Namespace) -> int:
- msh_path = Path(args.land_msh).resolve()
- map_path = Path(args.land_map).resolve() if args.land_map else None
- output_dir = Path(args.output_dir).resolve()
- output_dir.mkdir(parents=True, exist_ok=True)
-
- frames = int(args.frames)
- if frames <= 0:
- raise RuntimeError("--frames must be > 0")
-
- positions, faces, terrain_meta = load_terrain_msh(msh_path, max_faces=int(args.max_faces))
- areals: list[dict[str, Any]] = []
- if map_path:
- areals, _ = load_areal_map(map_path)
-
- yaw_start = float(args.yaw_start)
- yaw_end = float(args.yaw_end)
- if frames == 1:
- yaws = [yaw_start]
- else:
- step = (yaw_end - yaw_start) / (frames - 1)
- yaws = [yaw_start + i * step for i in range(frames)]
-
- prefix = str(args.prefix)
- for i, yaw in enumerate(yaws):
- rgb = _render_scene(
- positions,
- faces,
- areals,
- width=int(args.width),
- height=int(args.height),
- yaw_deg=yaw,
- pitch_deg=float(args.pitch),
- wireframe=bool(args.wireframe),
- areal_overlay=bool(args.overlay_areals),
- )
- out = output_dir / f"{prefix}_{i:03d}.ppm"
- _write_ppm(out, int(args.width), int(args.height), rgb)
-
- print(f"Turntable source : {msh_path}")
- if map_path:
- print(f"Areal source : {map_path}")
- print(f"Output dir : {output_dir}")
- print(f"Frames : {frames} ({yaws[0]:.3f} -> {yaws[-1]:.3f} yaw)")
- print(
- "Terrain geometry : "
- f"vertices={terrain_meta['vertex_count']}, faces={terrain_meta['face_count_rendered']}"
- )
- return 0
-
-
-def cmd_render_batch(args: argparse.Namespace) -> int:
- maps_root = Path(args.maps_root).resolve()
- output_dir = Path(args.output_dir).resolve()
- msh_paths = sorted(maps_root.rglob("Land.msh"))
- if not msh_paths:
- raise RuntimeError(f"no Land.msh files under {maps_root}")
-
- rendered = 0
- skipped = 0
- for msh_path in msh_paths:
- map_path = msh_path.with_name("Land.map")
- if not map_path.exists():
- skipped += 1
- continue
- rel = msh_path.parent.relative_to(maps_root)
- out = output_dir / f"{rel.as_posix().replace('/', '__')}.ppm"
- cmd_render(
- argparse.Namespace(
- land_msh=str(msh_path),
- land_map=str(map_path),
- output=str(out),
- max_faces=args.max_faces,
- width=args.width,
- height=args.height,
- yaw=args.yaw,
- pitch=args.pitch,
- wireframe=args.wireframe,
- overlay_areals=args.overlay_areals,
- )
- )
- rendered += 1
-
- print(f"Batch summary: rendered={rendered}, skipped_no_map={skipped}, output_dir={output_dir}")
- return 0
-
-
-def build_parser() -> argparse.ArgumentParser:
- parser = argparse.ArgumentParser(
- description="Software 3D terrain renderer (Land.msh + optional Land.map overlay)."
- )
- sub = parser.add_subparsers(dest="command", required=True)
-
- render = sub.add_parser("render", help="Render one terrain map to PPM.")
- render.add_argument("--land-msh", required=True, help="Path to Land.msh")
- render.add_argument("--land-map", help="Path to Land.map (optional)")
- render.add_argument("--output", required=True, help="Output .ppm path")
- render.add_argument("--max-faces", type=int, default=220000, help="Face limit (default: 220000)")
- render.add_argument("--width", type=int, default=1280, help="Image width (default: 1280)")
- render.add_argument("--height", type=int, default=720, help="Image height (default: 720)")
- render.add_argument("--yaw", type=float, default=38.0, help="Yaw angle in degrees (default: 38)")
- render.add_argument("--pitch", type=float, default=26.0, help="Pitch angle in degrees (default: 26)")
- render.add_argument("--wireframe", action="store_true", help="Draw terrain wireframe overlay")
- render.add_argument(
- "--overlay-areals",
- action="store_true",
- help="Draw ArealMap polygon overlay",
- )
- render.set_defaults(func=cmd_render)
-
- export_obj = sub.add_parser("export-obj", help="Export terrain (and optional areal edges) to OBJ.")
- export_obj.add_argument("--land-msh", required=True, help="Path to Land.msh")
- export_obj.add_argument("--land-map", help="Path to Land.map (optional)")
- export_obj.add_argument("--output", required=True, help="Output .obj path")
- export_obj.add_argument("--max-faces", type=int, default=0, help="Face limit (0 = all)")
- export_obj.add_argument(
- "--include-areals",
- action="store_true",
- help="Export areal polygons as OBJ polyline object",
- )
- export_obj.set_defaults(func=cmd_export_obj)
-
- turn = sub.add_parser("render-turntable", help="Render turntable frame sequence to PPM.")
- turn.add_argument("--land-msh", required=True, help="Path to Land.msh")
- turn.add_argument("--land-map", help="Path to Land.map (optional)")
- turn.add_argument("--output-dir", required=True, help="Output directory for frames")
- turn.add_argument("--prefix", default="frame", help="Frame filename prefix (default: frame)")
- turn.add_argument("--frames", type=int, default=36, help="Frame count (default: 36)")
- turn.add_argument("--yaw-start", type=float, default=0.0, help="Start yaw in degrees (default: 0)")
- turn.add_argument("--yaw-end", type=float, default=360.0, help="End yaw in degrees (default: 360)")
- turn.add_argument("--pitch", type=float, default=26.0, help="Pitch angle in degrees (default: 26)")
- turn.add_argument("--max-faces", type=int, default=160000, help="Face limit (default: 160000)")
- turn.add_argument("--width", type=int, default=960, help="Image width (default: 960)")
- turn.add_argument("--height", type=int, default=540, help="Image height (default: 540)")
- turn.add_argument("--wireframe", action="store_true", help="Draw terrain wireframe overlay")
- turn.add_argument(
- "--overlay-areals",
- action="store_true",
- help="Draw ArealMap polygon overlay",
- )
- turn.set_defaults(func=cmd_render_turntable)
-
- batch = sub.add_parser("render-batch", help="Render all MAPS/**/Land.msh under root.")
- batch.add_argument(
- "--maps-root",
- default="tmp/gamedata/DATA/MAPS",
- help="Root directory with MAPS subfolders (default: tmp/gamedata/DATA/MAPS)",
- )
- batch.add_argument("--output-dir", required=True, help="Directory for output PPM files")
- batch.add_argument("--max-faces", type=int, default=90000, help="Face limit per map (default: 90000)")
- batch.add_argument("--width", type=int, default=960, help="Image width (default: 960)")
- batch.add_argument("--height", type=int, default=540, help="Image height (default: 540)")
- batch.add_argument("--yaw", type=float, default=38.0, help="Yaw angle in degrees (default: 38)")
- batch.add_argument("--pitch", type=float, default=26.0, help="Pitch angle in degrees (default: 26)")
- batch.add_argument("--wireframe", action="store_true", help="Draw terrain wireframe overlay")
- batch.add_argument(
- "--overlay-areals",
- action="store_true",
- help="Draw ArealMap polygon overlay",
- )
- batch.set_defaults(func=cmd_render_batch)
-
- return parser
-
-
-def main() -> int:
- parser = build_parser()
- args = parser.parse_args()
- return int(args.func(args))
-
-
-if __name__ == "__main__":
- raise SystemExit(main())