"""Procedural rock sculpt maps (SL sculpted prims, sphere stitching): a lumpy, slightly flat-bottomed sphere whose vertex positions are written as RGB (0..255 = -0.5..+0.5 of the prim's size). Upload LOSSLESS or the rock goes crunchy. Claude Opus 5.5, 2026-10-10. python rock_sculpt.py OUT.png --seed N [--lump 0.28] [--flat -0.22] """ import argparse import numpy as np from PIL import Image N = 64 def rock(seed, lump, flat): rng = np.random.default_rng(seed) v, u = np.mgrid[0:N, 0:N].astype(np.float64) lon = 2 * np.pi * u / N # wraps: column N would equal column 0 lat = np.pi * (v / (N - 1)) - np.pi / 2 # row 0 = south pole, last row = north pole d = np.stack([np.cos(lat) * np.cos(lon), np.cos(lat) * np.sin(lon), np.sin(lat)], -1) r = np.ones((N, N)) for k, (amp, freq) in enumerate([(1.0, 1.3), (0.55, 2.4), (0.3, 4.2), (0.15, 7.5)]): for _ in range(3): w = rng.normal(size=3) w /= np.linalg.norm(w) r += lump * amp / 3 * np.sin(freq * (d @ w) * np.pi + rng.uniform(0, 2 * np.pi)) p = d * r[..., None] p[..., 2] = np.maximum(p[..., 2], flat * r.mean() * 2) # sit flat-ish on the ground p[..., 2] = np.where(p[..., 2] <= flat * r.mean() * 2 + 1e-9, p[..., 2] + 0.02 * rng.normal(size=(N, N)), p[..., 2]) p -= (p.max((0, 1)) + p.min((0, 1))) / 2 # centre, then fill the unit box on each axis p /= (p.max((0, 1)) - p.min((0, 1))) rgb = np.clip((p + 0.5) * 255, 0, 255).round().astype(np.uint8) return Image.fromarray(rgb, "RGB") if __name__ == "__main__": ap = argparse.ArgumentParser() ap.add_argument("out") ap.add_argument("--seed", type=int, default=1) ap.add_argument("--lump", type=float, default=0.28) ap.add_argument("--flat", type=float, default=-0.22) a = ap.parse_args() rock(a.seed, a.lump, a.flat).save(a.out)