Skip to content

About

Procedural asteroids for games: a ~4k-triangle mesh whose detail lives in baked PBR maps, sculpted from a real impact history. Blender + numpy, MIT.

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

5 Commits

Folders and files

Repository files navigation

asteroid-generator

Generates photorealistic, game-ready asteroids: a low-poly mesh (~4k triangles) whose detail is carried by baked PBR texture maps.

python3 asteroid_gen.py --shape top_ridge --type C --seed 7 -o output/bennu_like.obj

Blender (4.3+; developed on 5.0) must be on PATH; the script relaunches itself inside Blender in background mode. Dependencies: numpy, scipy and Pillow in Blender's Python.

Examples

Each image is the generator's own preview render of the exported asset: about 4,000 triangles with 2048² maps and the tileable detail maps, under a single hard sun at 70° phase. To reproduce one, run its command from the repository root. The seed makes the result deterministic.

Eros-like S-type Eros-like: elongated stony body, crater-saturated
python3 asteroid_gen.py --shape potato --type S --seed 11 -o output/eros_like.obj
Bennu-like C-type Bennu/Ryugu-like: dark carbonaceous spinning-top rubble pile with boulders
python3 asteroid_gen.py --shape top_ridge --type C --seed 21 -o output/bennu_like.obj
Itokawa-like contact binary Itokawa-like: two-lobed contact binary with a regolith-filled neck
python3 asteroid_gen.py --shape contact_binary --type S --seed 22 -o output/itokawa_like.obj
Metal-rich fragment Metal-rich fragment: angular M-type collisional shard with exposed metal
python3 asteroid_gen.py --shape irregular --type M --seed 23 -o output/metal_fragment.obj
Phobos-like near-spherical body Phobos-like: near-spherical, heavily cratered stony body
python3 asteroid_gen.py --shape spherical --type S --seed 24 -o output/phobos_like.obj

Each run takes about 2.5–3 minutes on a 16-thread CPU and writes the OBJ/MTL, GLB, all maps and the preview next to the .obj.

Options

Flag Default Meaning
--shape potato potato (Eros), contact_binary (Itokawa), top_ridge (Bennu/Ryugu), irregular (Steins/Lutetia), spherical (Phobos/Vesta-like)
-t/--type S Spectral type: C carbonaceous (albedo ~0.04), S stony (~0.25), M metallic (~0.15 with exposed metal)
--poly-count 4000 Target triangle count of the exported mesh
--texture-res 2048 Map resolution
--sculpt-res 256 Voxel resolution of the high-detail sculpt (longest axis)
--detail-res 1024 Resolution of the tileable close-up detail maps (0 disables)
--craters / --boulders auto Override resolved crater/boulder counts (auto = crater saturation, rubble piles get many boulders)
--ao-samples 128 AO bake samples
--save-blend off Save a .blend with the high-detail sculpt and the asset (for re-baking)
--no-preview, --preview-res 1024 Preview render of the exported asset

Typical run time on a 16-thread CPU: ~3 min with defaults, ~1 min at --texture-res 1024 --detail-res 512.

How it works

  1. Shape: an implicit volume built from a Gaussian random sphere (Muinonen 1996): a spherical-harmonic log-radius with a power-law spectrum, combined with each archetype (smooth-union lobes, a bicone spin profile, fracture-plane facets, banana warp).
  2. History: craters and boulders are applied in time order as local CSG operations on the voxel grid, so later impacts erase earlier features and nothing folds or self-intersects.
    • Crater radii follow a cumulative power law (slope ~2.0–2.3) up to saturation. Profiles are shallow (d/D ≈ 0.08–0.15 for rubble piles, cf. Itokawa 0.08 ± 0.03). Rims are low, and the bowl and ejecta blanket decay as r⁻³. Craters degrade with age, and the youngest get bright, rayed ejecta.
    • Boulders are faceted convex polyhedra with a cumulative size slope of ~2.9 (Bennu), merged into the surface with a regolith fillet.
  3. Geophysics: self-gravity plus spin (near-critical for top shapes) is computed from the body's own mass. That gives true gravitational slopes and a geopotential. Steep slopes stay fresh (bright, as on Eros' crater walls), and gravitational lows collect smooth regolith ponds.
  4. Surfaces: the dense surface (~100–200k quads) is extracted with surface nets. The asset comes from a low-pass copy of the same volume, decimated to the triangle budget (closed 2-manifold, no self-intersections).
  5. UVs: compact charts (variational shape approximation), SLIM flattening, and a raster packer (≈60% texture-space use with ~40 islands and uniform texel density).
  6. Bake: Cycles projects the dense sculpt onto the asset (position, normal, AO, geology masks) into float, Non-Color buffers.
  7. Micro-relief and maps: below the voxel scale, thousands of small craters, cobbles and gravel, and self-affine rock/regolith roughness are evaluated per texel in 3D. Gradients are analytic, so the detail is seamless across UV islands. All maps are then synthesised from the bakes in numpy.

Outputs

File Encoding Notes
NAME.obj / .mtl Triangulated; MTL uses map_Kd, norm, map_Pr, map_Pm, disp
NAME.glb glTF 2.0 Base colour, normal (with tangents), shared ORM texture
_diffuse.png sRGB Base colour / albedo, no baked lighting or AO
_normal.png linear Tangent space, MikkTSpace, OpenGL (+Y). Flip green for DirectX/Unreal
_roughness.png, _metallic.png, _ao.png linear
_orm.png linear R = AO, G = roughness, B = metallic
_height.png 16-bit Displacement along the low-poly normal; height_min/max in the JSON give the scale
_cavity.png linear 0.5 = flat, > 0.5 concave (for dirt or edge-wear masks)
_detail_mask.png linear Base UVs: weight of the rock detail tile (0 = regolith tile)
_detail_{regolith,rock}_normal.png linear Tileable tangent-space detail normals (OpenGL +Y)
_detail_{regolith,rock}_albedo.png linear Tileable albedo modulation: base colour × 2 × value (0.5 = no change)
_maps.json Seed, height scale, UV stats, mean albedo, detail_uv_scale, conventions
_preview.png The exported asset, single hard sun at 70° phase, no fill light

Detail maps (close-ups)

A single 2048² map spread over a whole asteroid runs out of resolution at close range. The two tileable detail tiles continue the same micro-relief models (craters, angular cobbles, self-affine roughness) down to about 1/8 of a base texel. Each tile repeats every 128 base texels.

  • Sample the tiles at uv * detail_uv_scale (from the JSON; 16 at 2048²). They're aligned with the base UV axes, so they share the base tangent frame and need no extra tangents.
  • Blend regolith and rock with _detail_mask.png, sampled at the base UVs.
  • Fade the detail out with distance (or by mip level) so the repetition is never visible from afar.

glTF has no detail-map slot, so the GLB carries only the base maps. The generator's preview uses the detail maps, and the blend is a few lines in any engine:

vec3 detail_n = mix(texture(regolithN, uv * detailScale).xyz, texture(rockN, uv * detailScale).xyz, mask) * 2.0 - 1.0;
vec3 base_n   = texture(baseN, uv).xyz * 2.0 - 1.0;
float fade    = clamp(1.0 - viewDistance / detailFadeDistance, 0.0, 1.0);
vec3 n_ts     = normalize(vec3(base_n.xy + detail_n.xy * fade, base_n.z));   // UDN blend
vec3 albedo   = baseColor * mix(vec3(1.0), 2.0 * mix(texture(regolithA, uv * detailScale).rgb,
                                                       texture(rockA, uv * detailScale).rgb, mask), fade);

UV islands are rotated independently, so tile orientation changes across UV seams. That's invisible with this isotropic content. Object-space triplanar sampling of the same tiles also works if you'd rather avoid seams entirely.

Rendering tips for realism

Regolith doesn't scatter like Lambertian plaster. Asteroid discs show almost no limb darkening, and they brighten sharply near zero phase (the opposition surge). Most of the "looks real" effect comes from the light model, not the textures:

  • Use one hard directional light and no ambient or fill. Space has no sky light.
  • Use full roughness and low specular (the maps already do this).
  • If your engine lets you customise the diffuse term, blend Lambert with Lommel–Seeliger, as asteroid light-curve modelling does:
// mu0 = dot(N, L), mu = dot(N, V), both clamped to >= 0
float lommel_seeliger(float mu0, float mu) { return mu0 / max(mu0 + mu, 1e-4); }
vec3 regolith_diffuse(vec3 albedo, float mu0, float mu, float phase /*radians*/) {
    float ls  = lommel_seeliger(mu0, mu);
    float lam = mu0;
    // Mild opposition surge (shadow-hiding) below ~10 degrees phase
    float surge = 1.0 + 0.4 * exp(-phase / radians(4.0));
    return albedo * mix(lam, 2.0 * ls, 0.6) * surge;
}

Tests

python3 -m unittest tests.test_core       # pure numpy, ~2 s, no Blender
python3 -m unittest tests.test_generator  # end-to-end through Blender, ~1 min

License

MIT. See LICENSE.

About

Procedural asteroids for games: a ~4k-triangle mesh whose detail lives in baked PBR maps, sculpted from a real impact history. Blender + numpy, MIT.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages