Skip to content

dh.look

Extension: .dh-look
Type ID: dh.look

A look is a named bundle of art direction a map adopts instead of transcribing numbers: the tonemap curve, the exposure, the bloom shape, the diffuse wrap and the light falloff. The engine ships four built-in looks by slug, and a .dh-look file is how you author your own — a house look you write once and every map in the pallet inherits.

A look carries the response chain and nothing else. It never touches your sun direction, your sun or ambient colors, or the intensity scales, because those describe a particular scene rather than a reusable look. A map ported with its source engine’s ambient values keeps them.

Every field is optional, and an omitted field means “this look does not speak to that setting” — it falls through to the engine default rather than being pinned. That is what makes a look composable.

PropertyTypeRequiredDescription
$type"dh.look"noType identifier
namestringnoDisplay name
descriptionstringnoOne line on what the look reproduces
exposurefloatnoLinear multiplier applied to scene radiance before the curve. Must be > 0
tonemapstringnoCurve name: reinhardWhite, aces, reinhard, hable or none. Matched case-insensitively
hableobjectnoThe coefficients of the hable curve; see The hable block. Read only while the world resolves through that curve
bloomboolnoWhether bright areas bloom at all
bloomIntensityfloatnoLinear multiplier the resolved bloom pyramid is added back with. Must be ≥ 0
bloomThresholdfloatnoLinear radiance above which a pixel blooms, tested on its brightest channel. Must be ≥ 0
bloomSoftKneefloatnoWidth of the quadratic knee below the threshold, as a fraction of it. 0–1
bloomDiffusionfloatnoHow far the bloom spreads, in pyramid levels rather than pixels. 1–10
bloomAnamorphicfloatnoAspect distortion of the glow. -1–1; 0 is round, negative stretches vertically
halfLambertfloatnoDiffuse wrap. 0–1: 0 is a hard Lambert terminator, 1 a full Source-style wrap
bakedWrapfloatnoDiffuse wrap on baked light, applied as the lightmap’s MonoSH is evaluated. 0–1: 0 (the default) is Bakery-exact Lambert; 1 lifts L0 by the half-Lambert kernel’s harmonics. Changing it never needs a rebake, and realtime lights keep halfLambert
falloffstringnoDefault point/spot distance curve: physical or unity

The type is inferred from the file extension, so $type is not needed in source files. The compiler adds it automatically during builds.

A value outside the range above is a compile error, not a silent clamp — and it is the same error, from the same validator, that the equivalent field in a map’s own render or lighting block would raise.

"tonemap": "hable" selects John Hable’s filmic curve (the Uncharted 2 curve): a toe, a straight middle and a shoulder that reaches exactly white at the white point. With no hable block it is the classic curve, Hable’s published coefficients. A block states only the fields it wants to change.

FieldTypeRequiredMeaning
shoulderStrengthfloatnoThe curve’s A: how strongly the shoulder bends. 0–16; Hable’s value is 0.15
linearStrengthfloatnoB, the slope of the straight middle. 0–16; 0.50
linearAnglefloatnoC, the angle of the straight middle. 0–16; 0.10
toeStrengthfloatnoD, how strongly the toe bends. 0.001–16; 0.20
toeNumeratorfloatnoE, the toe’s numerator. 0–16; 0.02
toeDenominatorfloatnoF, the toe’s denominator. 0.001–16; 0.30
whitePointfloatnoThe exposed linear radiance that resolves to white; anything brighter clamps there. 0.01–4096; 11.2

The curve is (x(A·x + C·B) + D·E) / (x(A·x + B) + D·F) − E/F. The engine clamps the exposed radiance at whitePoint, runs it through the curve and divides by the curve at whitePoint, so white is always exactly 1.

This is also Source 2’s own resolve. Source 2 multiplies the exposed color by 2.8 before the curve and clamps at the white point scaled alike; both are a plain multiplier, so a Source 2 map states exposure = 2^bias · 2.8 and whitePoint = W · 2.8 and needs no special case in the engine. Measured against reinhardWhite, Hable darkens shadows, which is why it is a choice and never the default.

looks/punchy.dh-look
{
"tonemap": "hable",
"exposure": 2.0,
"hable": { "shoulderStrength": 0.22, "linearStrength": 0.30, "whitePoint": 6.0 }
}

The curve is resolved by the shared engine client, so every place that draws the world gets the same picture:

WhereHable curve
Engine, Windows desktop✅
Engine, iOS and Steam Frame✅ the shared client resolves it
dh render and captures✅ the same resolve
Runtime game mods (BONELAB, Schedule I and the rest)❔ not checked; they draw through the game’s own camera
looks/night.dh-look
{
"name": "Night",
"description": "Cold, low-key interiors: ACES off, wide soft bloom, full diffuse wrap.",
"exposure": 1.4,
"tonemap": "reinhardWhite",
"bloom": true,
"bloomIntensity": 0.45,
"bloomThreshold": 1.1,
"bloomSoftKnee": 0.6,
"bloomDiffusion": 8,
"bloomAnamorphic": -0.24,
"halfLambert": 1.0,
"falloff": "physical"
}

A map’s top-level look field takes either a built-in slug or a reference to a .dh-look asset:

maps/lobby.dh-map
// A built-in slug...
"look": "unity"
// ...a look in this pallet...
"look": "looks/night.dh-look"
// ...or one from a dependency.
"look": "dev.example.looks:looks/night.dh-look"

The built-in slugs are neutral, unity, source and filmic — see the map’s look table for what each reproduces.

filmic names the hable curve with Hable’s published coefficients stated in full, and sets no exposure.

Cross-pallet references follow the ordinary resolution rules: the core pallet is always available, and any other pallet ID must be a declared dependency in your pallet.dh.

A pallet manifest can name a look of its own, and every map in the pallet picks it up:

pallet.dh
{
"id": "dev.example.maps.harbor",
"name": "Night City",
"version": "0.0.1",
"look": "looks/night.dh-look"
}

This is the weakest authored layer, so it never fights a map that names its own. It is for the case a pallet’s maps all share a house look: state it once, in one place, rather than repeating it in every .dh-map.

A look is a seed, never a lock. From weakest to strongest:

#LayerWhere it is authored
1Engine defaultsThe engine itself; whatever a look leaves unset falls through to here
2Pallet default lookpallet.dh → look
3Map’s lookthe .dh-map’s look field (a slug or a .dh-look)
4Map’s explicit fieldsthe .dh-map’s own render and lighting blocks
5Live console edita stored world.render.* / world.lighting.* value

So a pallet whose default look is night, containing a map that adopts unity and then sets render.exposure, renders at that exposure, with the Unity curve and bloom, and picks up night only for fields both of the others leave unset. Nothing is ever written back to the map, and a console edit is a live override on top rather than an edit of the content.

Like materials, look definitions are re-read on every rebuild rather than cached against a checksum, so editing a .dh-look and recompiling applies on the next map load without restarting the engine.