Skip to content

Shading & Lighting

How a planet's ground is coloured (Surfaces), how the tile materials, ocean and foliage are shaded, and how to light a planet so it reads as real.

Surfaces

A Surface is a planet's ground material, described as a short list of layers instead of a hand-built material graph. Each layer is a ground type (rock, soil, grass, sand, snow, ...) with a colour and the conditions where it appears: height, slope, latitude and biome channels. Metaplanets compiles the list into a material, M_Surface_<Name>, that you assign to the planet actor.

The Wildlands surface: tundra, scree and bare rock

Layers are applied bottom to top: the first layer is the base, and each later layer is painted over the ones before it where its conditions hold.

A surface can live in its own JSON file (Name.surface.json) or as a surface block in the planet's PlanetScript. This is the Rust planet's surface:

surface Rust {
    roughness = 0.8;
    // Rust-red regolith: the base everywhere.
    layer regolith { color = (0.46, 0.22, 0.11); color2 = (0.55, 0.29, 0.15); vary = 40000; bump = 0.1; bumpScale = 2.0; }
    // Bright dust provinces where biome z is high.
    layer dust     { color = (0.66, 0.42, 0.24); color2 = (0.72, 0.50, 0.30); vary = 80000; biome.z = (0.55, 1.0);
                     breakup = 0.4; breakupScale = 60000; opacity = 0.85; }
    // Dark basalt.
    layer basalt   { color = (0.17, 0.12, 0.10); color2 = (0.24, 0.17, 0.13); vary = 30000; biome.y = (0.15, 0.8); roughness = 0.75; }
    // Steep walls by slope.
    layer walls    { color = (0.40, 0.23, 0.14); vary = 2000; slope = (22, 50); slopeFade = 6; roughness = 0.85; }
    // Wet shore sediment just above the sea.
    layer shore    { color = (0.30, 0.17, 0.10); height = (-30, 6); heightFade = 4; opacity = 0.8; }
}

The same in JSON (from Docs/examples/surfaces/Temperate.surface.json):

{
  "name": "Temperate",
  "roughness": 0.85,
  "layers": [
    { "name": "bedrock", "color": [0.070, 0.064, 0.058], "color2": [0.125, 0.112, 0.098], "vary": 6,
      "roughness": 0.8, "bump": 0.15, "bumpScale": 1.6 },
    { "name": "grass", "color": [0.030, 0.060, 0.016], "color2": [0.070, 0.090, 0.025], "vary": 18, "roughness": 0.9,
      "slope": [0, 30], "slopeFade": 5, "height": [3, 2300], "heightFade": 200,
      "biome": { "channel": 1, "min": 0.05, "max": 0.45 }, "breakup": 0.45, "breakupScale": 35,
      "bump": 0.02, "bumpScale": 0.3 },
    { "name": "snow", "color": [0.80, 0.82, 0.86], "color2": [0.70, 0.74, 0.80], "vary": 10, "roughness": 0.55,
      "biome": { "channel": 0, "min": 0.15, "max": 0.55 }, "slope": [0, 48], "slopeFade": 8,
      "breakup": 0.35, "breakupScale": 25, "bump": 0.04, "bumpScale": 2.5 }
  ]
}

Build the material:

UnrealEditor-Cmd MyProject.uproject -run=MetaplanetCreateExamples -createsurface=C:/Planets/Temperate.surface.json -surfacedir=/Game/Planets/Surfaces

-createsurface also accepts a .ps file with a surface block, and several files separated by commas.

Surface settings

Top level:

Key Default Meaning
name Material name: M_Surface_<name>.
roughness 0.85 Default roughness for every layer.
specular 0.35 0–1.
param <Name> = <value>; (JSON "params") Runtime parameters; see below.
debug Show one input instead of the colour: biome, height, slope, rough, flat, tex_<layer> or orm_<layer>.

Per layer:

Key Default Meaning
color, color2 Two linear albedo colours, blended by noise.
vary 8 Size (m) of the colour variation between color and color2.
roughness inherits
opacity 1 How strongly the layer covers the ones below.
height, heightFade Height band in m above sea level, and its soft edge (50).
slope, slopeFade 0–90 Slope band in degrees, and its soft edge (5).
lat, latFade Latitude band in degrees, and its soft edge (1). Polar caps, tropical belts.
biome.x … biome.w (JSON "biome": {"channel", "min", "max"}) 0.3–0.7 Biome-channel range. Several conditions multiply. A range written high-to-low is a falling ramp.
breakup, breakupScale 0, 30 m Breaks the layer's edge into irregular patches (0–0.95).
bump, bumpScale Micro relief: a detail normal of bump metres at bumpScale metres wavelength. Typical: rock 0.1–0.2 m at 1–2 m, soil 0.03 at 0.5, grass 0.02 at 0.3.
texture A Texture Synth set name (T_<Set>_BaseColor, _Normal, _ORM), projected triplanar.
texScale 4 m Metres per texture repeat.
texStrength, texNormal, texColor 1, 1, 0 Overall, normal and colour strength of the texture.
heightBlend 0.5 if textured Blends layers by the texture's height, so grass fills the cracks between stones instead of a soft fade.

Textures and micro relief fade out by distance automatically, so the same surface works from orbit to the ground with no tiling visible from above.

Use real albedo values

Colours are linear albedo. Real ground is darker than most people expect: grass and forest 0.03–0.08, soil 0.05–0.1, rock 0.1–0.2, desert sand 0.3–0.4, fresh snow 0.8. If a planet looks pale, fix the exposure, not the colours.

Change a surface at runtime

Surfaces can declare parameters, and layer opacity and band ranges can be simple expressions of them:

surface Mars {
    param SeaLevel = 0;
    param CapN = 80;
    layer shore   { color = (0.30, 0.17, 0.10); height = (SeaLevel - 40, SeaLevel + 25); heightFade = 4; }
    layer polarIce { color = (0.80, 0.82, 0.86); lat = (CapN - 3 + 6 * biome_w, 95); latFade = 2; }
}

Each surface gets a Material Parameter Collection, MPC_<Surface>, next to its material. Set its scalars from Blueprint or C++ (Set Scalar Parameter Value on the collection) and every tile recolours in the same frame, with no terrain regeneration. Use it for seasons, shrinking ice caps, rising seas or a terraforming sequence.

Tile materials

If you build your own planet material instead of a Surface:

Input Notes
NormalMap, BiomeMap Per-tile textures provided by Metaplanets.
GlobalNormalMap, HasGlobalNormalMap A whole-planet normal cubemap for distant views (Bake Global Normal Map on the actor; 512 per face by default).
PlanetCenter World position of the planet centre.
MF_MetaplanetTileMorph Wire into World Position Offset. It morphs new tiles from the tile they replace, so there is no popping. Without it, LOD changes are hard swaps.
  • Normal Map Resolution (planet actor, Material): 64 per tile by default; 128 or more gives sharper shading from a distance at more memory.
  • Biome channels are stored per vertex (65 × 65 per tile on the finest lattice), so very sharp biome edges are softened at tile scale. Use breakup to make those edges natural.
  • After updating the plugin, re-create MF_MetaplanetTileMorph from your tile materials (-run=MetaplanetCreateExamples -createtilemorph) and rebuild your surfaces to pick up shading fixes.

Ocean shading

The planetary ocean is shaded by M_PlanetaryOcean:

  • Waves: three FFT cascades (1,024 m, 193 m and 29 m) of a wind sea plus swell, set by Sea State: Wind Speed (9 m/s), Fetch Km (300), Wind Direction, Swell Height (0.6 m), Swell Period (11 s), Choppiness (0.75), Whitecaps (1).
  • Sky: reflections and lighting come from the Metaplanets atmosphere's sea-level sky, so sunsets and the day/night line on the water match the sky exactly.
  • Light: a sharp sun glint, and a glow through back-lit wave crests.
  • Shore: water fades from wet sand to turquoise by its exact distance to the coast; waves break along the real coastline with bright foam at the crest that turns to lace behind; a thin swash line at the waterline.
  • Lakes and rivers: calm, rippled water with a current and white water on rapids.

Debug views: metaplanets.PlanetaryOcean.DebugView (8 = shore distance, 9 = wave height).

Foliage shading

  • Leaf cards (M_MetaplanetLeafCard): masked, two-sided, with alpha coverage kept across mips so canopies don't thin out with distance.
  • Bark (M_MetaplanetBark): moss on upward-facing bark, darkened under the canopy (CanopyAOBottom, CanopyHeightCm).
  • Crown AO: leaves deep inside or low in a crown see less sky (CrownInnerAO, CrownLowAO), which gives trees volume.
  • Impostors match the mesh trees: low specular (0.08) and high roughness (0.85), with crown darkening. See Foliage.

Lighting

A forest floor in sunlight with soft shadows

The sun

Use one Directional Light as the sun. The Metaplanets atmosphere, ocean and gas giants all find and follow it:

  • The sky, haze and clouds are lit by the directional light's own intensity and colour (Use Sun Light Intensity, on by default), so sky and terrain always share one sun.
  • The night side of a planet goes properly dark.
  • For a star's colour, set it on the directional light (or use a Star Color node in an Atmosphere Graph for the preview).

Sky light and ambient

Shadows need sky light, or they go black. The Sky Switch actor (with a sky preset, for example SP_EarthSky) sets this up: it swaps the level's Unreal sky for the Metaplanets atmosphere at runtime and adds a sky light that captures the sky.

Setting Default Notes
Ambient Intensity Scale (on the preset) 2 Sky-light strength. Use 1–2 with auto exposure; about 4 for manual, sun-metered exposure. 3+ tints grass blue under auto exposure.
metaplanets.Sky.AmbientScale Live override of the scale for tuning.

Global illumination and reflections

The example project uses Lumen with software ray tracing (hardware RT looked the same and cost 22–29% more GPU on land views), virtual shadow maps and mesh distance fields:

[/Script/Engine.RendererSettings]
r.DynamicGlobalIlluminationMethod=1
r.ReflectionMethod=1
r.Shadow.Virtual.Enable=1
r.GenerateMeshDistanceFields=True
r.Lumen.HardwareRayTracing=False
r.AllowStaticLighting=False
  • Trees and shrubs contribute to Lumen sky occlusion and distance-field AO (metaplanets.SpawnerDistanceFieldLighting 1), which grounds forests. Grass doesn't, to keep the cost down.
  • Terrain tiles have no distance fields, so there is no ground bounce under software Lumen. Outdoors, sun and sky dominate and this rarely shows.
  • Shade under a closed canopy can read slightly blue. Crown AO and the bark's canopy darkening counter it.

Keep meshes around the camera out of Lumen

Any mesh that surrounds the camera (a pawn's collision ball, a helmet, a cockpit shell) must have Affect Dynamic Indirect Lighting, Affect Distance Field Lighting and Cast Shadow turned off. Otherwise every Lumen ray starts inside it, bounce light takes its colour, and every shadow turns that colour. The shipped pawn is set up this way; for projects made before 2.2.0, run -run=MetaplanetCreateExamples -fixpawnlighting.

Before and after: blue shadows from a pawn mesh in Lumen, and neutral shadows once it's excluded

Shadows

  • Virtual shadow maps for everything. Foliage stops animating at its reach and ground scatter is cached static, so shadow pages stay cached.
  • Contact shadows on the sun: the shipped levels use a Contact Shadow Length of 0.03. Every spawned instance casts contact shadows.
  • Grass and ground scatter cast shadows by default. Limit them with metaplanets.Spawner.ScatterShadowDistanceMeters if blade shadows shimmer in the distance.
  • Cloud shadows: set on the AtmosphereComponent (Cloud Shadows: Extent Km 80, Strength 0.75).

Exposure

Exposure decides more about realism than any material:

  • Default auto exposure pulls every view to mid-grey. A dark forest (albedo 0.03–0.1) then reads pale and minty. That is exposure, not your colours: don't brighten the colours to compensate.
  • For ground-level shots, use manual exposure metered to the sun: EV100 = log2(sun lux / (1.2 × π)) − 1. Apply it as an exposure bias of −EV in manual mode with the physical camera off. A 10 lux sun gives about EV 0.4.
  • The atmosphere is exposed like lit geometry, so the sky stays consistent with the terrain under any exposure mode.
  • The example project enables Extend Default Luminance Range and uses Local Exposure contrast scales of 0.8 for highlights and shadows.

Checklist for realistic light

  • One directional light as the sun; the atmosphere follows it.
  • A Sky Switch (or a sky light capturing the sky) with Ambient Intensity Scale around 2.
  • Lumen, virtual shadow maps, mesh distance fields.
  • Contact Shadow Length 0.03 on the sun.
  • Manual or locked exposure for ground shots; auto exposure for flights from orbit to the ground.
  • Nothing around the camera in Lumen.
  • Real albedos in your surfaces.