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.
Properties
Section titled “Properties”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.
| Property | Type | Required | Description |
|---|---|---|---|
$type | "dh.look" | no | Type identifier |
name | string | no | Display name |
description | string | no | One line on what the look reproduces |
exposure | float | no | Linear multiplier applied to scene radiance before the curve. Must be > 0 |
tonemap | string | no | Curve name: reinhardWhite, aces, reinhard, hable or none. Matched case-insensitively |
hable | object | no | The coefficients of the hable curve; see The hable block. Read only while the world resolves through that curve |
bloom | bool | no | Whether bright areas bloom at all |
bloomIntensity | float | no | Linear multiplier the resolved bloom pyramid is added back with. Must be ≥ 0 |
bloomThreshold | float | no | Linear radiance above which a pixel blooms, tested on its brightest channel. Must be ≥ 0 |
bloomSoftKnee | float | no | Width of the quadratic knee below the threshold, as a fraction of it. 0–1 |
bloomDiffusion | float | no | How far the bloom spreads, in pyramid levels rather than pixels. 1–10 |
bloomAnamorphic | float | no | Aspect distortion of the glow. -1–1; 0 is round, negative stretches vertically |
halfLambert | float | no | Diffuse wrap. 0–1: 0 is a hard Lambert terminator, 1 a full Source-style wrap |
bakedWrap | float | no | Diffuse 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 |
falloff | string | no | Default point/spot distance curve: physical or unity |
The type is inferred from the file extension, so
$typeis 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.
The hable block
Section titled “The hable block”"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.
| Field | Type | Required | Meaning |
|---|---|---|---|
shoulderStrength | float | no | The curve’s A: how strongly the shoulder bends. 0–16; Hable’s value is 0.15 |
linearStrength | float | no | B, the slope of the straight middle. 0–16; 0.50 |
linearAngle | float | no | C, the angle of the straight middle. 0–16; 0.10 |
toeStrength | float | no | D, how strongly the toe bends. 0.001–16; 0.20 |
toeNumerator | float | no | E, the toe’s numerator. 0–16; 0.02 |
toeDenominator | float | no | F, the toe’s denominator. 0.001–16; 0.30 |
whitePoint | float | no | The 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.
{ "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:
| Where | Hable 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 |
Full Example
Section titled “Full Example”{ "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"}Adopting a look
Section titled “Adopting a look”A map’s top-level look field takes either a built-in slug or a reference to a .dh-look asset:
// 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-wide default
Section titled “A pallet-wide default”A pallet manifest can name a look of its own, and every map in the pallet picks it up:
{ "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.
Precedence
Section titled “Precedence”A look is a seed, never a lock. From weakest to strongest:
| # | Layer | Where it is authored |
|---|---|---|
| 1 | Engine defaults | The engine itself; whatever a look leaves unset falls through to here |
| 2 | Pallet default look | pallet.dh → look |
| 3 | Map’s look | the .dh-map’s look field (a slug or a .dh-look) |
| 4 | Map’s explicit fields | the .dh-map’s own render and lighting blocks |
| 5 | Live console edit | a 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.
Hot rebuild
Section titled “Hot rebuild”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.