Skip to content

Dark scenes

Settings that keep a night scene dark, from the sky to the final pass.

night-marketA seller raises her lantern for a customer out of the rain; the potions are in focus and the market's lights blur into discs.
Lower the fire to see what the moon and the sky light on their own. The lamps and the fire light the scene, and the sky adds a little blue to the shadows.

The lighting defaults describe daylight. A pale blue sky lights every surface from above and shows in every reflection, and fog takes a pale grey-blue. In a night scene those values brighten the shadows and wash out the distance. Set the sky, the sun and the fog for the night, light the scene with lamps, and set the exposure for a dark picture:

import {
import bloom
bloom
,
import finish
finish
} from '@typegpu/kit';
const
const night: lighting.Lighting
night
=
import lighting
lighting
.
function create<{
readonly settings: {
readonly sunDirection: [-0.45, 0.75, -0.5];
readonly sunColor: [0.5, 0.65, 1];
readonly sunIntensity: 0.25;
readonly skyTop: [0.03, 0.06, 0.16];
readonly skyBottom: [0.02, 0.016, 0.014];
readonly fogColor: [0.03, 0.06, 0.13];
readonly fogDensity: 0.015;
readonly fogFalloff: 0.12;
};
readonly points: {
readonly table: table.Table<table.TableDef<"PointLight", {
position: Vec3f;
range: F32;
color: Vec3f;
intensity: F32;
}, {}>>;
};
}>(root: TgpuRoot, options?: ({
readonly settings: {
readonly sunDirection: [-0.45, 0.75, -0.5];
readonly sunColor: [0.5, 0.65, 1];
readonly sunIntensity: 0.25;
readonly skyTop: [0.03, 0.06, 0.16];
readonly skyBottom: [0.02, 0.016, 0.014];
readonly fogColor: [0.03, 0.06, 0.13];
readonly fogDensity: 0.015;
readonly fogFalloff: 0.12;
};
readonly points: {
readonly table: table.Table<table.TableDef<"PointLight", {
position: Vec3f;
range: F32;
color: Vec3f;
intensity: F32;
}, {}>>;
};
} & lighting.Options & {
...;
}) | undefined): lighting.Lighting

Creates lighting with a sun, sky and fog. It can also read point lights from a table or a function and cast shadows. Pipe its injection into each pipeline that uses it. Throws if points names no source or more than one, a light function has neither count nor candidates, a table comes with count, or candidates.ranges is anything other than a positive integer.

@injects ― hook the shading hooks its options name: reflect, ambient, fog, sunShadow and attenuation

@injects ― definition the sun, sky and fog, the shadows and the point lights, which shading reads

@binds ― 1 uniform: shading.shade, the sun, sky and fog

@binds ― 2 storage, 1 uniform: shading.shade with a table of point lights

@binds ― 3 storage, 1 uniform: shading.shade with a table of lights whose positions are on the device, the table's two parts

@binds ― 1 uniform: shading.shade with points.light or points.spot, plus what the function binds

@binds ― 0 storage: shading.shade with points.candidates, plus what their functions bind, 2 storage for a neighbour grid's range and slot

@binds ― 1 uniform: shading.shade with a shadow, the sun's light space and cascades, beside the depth array and its comparison sampler

@binds ― 1 uniform: shading.shade with pointShadow, each shadow's light, layers and views, beside their depth array and the comparison sampler

@owns ― 1 to 3 buffers, a sampler, targets, a cameras.layered, passes: the sun, sky and fog; with a shadow, its light space, the views of its cascades, a comparison sampler, the depth array and the passes that draw it; with pointShadow, the same for the lights' faces

@records ― shadow.record and pointShadow.record with what was added to them, before the passes that shade

@param ― options.shadow the sun's shadow, with a depth pass per cascade

@param ― options.pointShadow shadows that cast gives to lights of points.table, with a depth pass per face

@param ― options.points point lights from a table or a 'use gpu' function. Every lit point visits each slot up to the table's highest live one, or count with a function, unless candidates names the slots that may reach it

@returns ― lighting you destroy

create
(
const root: TgpuRoot
root
, {
settings: {
readonly sunDirection: [-0.45, 0.75, -0.5];
readonly sunColor: [0.5, 0.65, 1];
readonly sunIntensity: 0.25;
readonly skyTop: [0.03, 0.06, 0.16];
readonly skyBottom: [0.02, 0.016, 0.014];
readonly fogColor: [0.03, 0.06, 0.13];
readonly fogDensity: 0.015;
readonly fogFalloff: 0.12;
} & ((Partial<InferInput<WgslStruct<{
sunDirection: Vec3f;
sunIntensity: F32;
sunColor: Vec3f;
skyIntensity: F32;
skyTop: Vec3f;
fogDensity: F32;
skyBottom: Vec3f;
fogFalloff: F32;
fogColor: Vec3f;
skyReflection: F32;
fogStart: F32;
}>>> | undefined) & {
...;
})

Sets initial sun, sky and fog values over defaults. Default defaults, which has no fog.

settings
: {
sunDirection: [-0.45, 0.75, -0.5]
sunDirection
: [-0.45, 0.75, -0.5], // the moon
sunColor: [0.5, 0.65, 1]
sunColor
: [0.5, 0.65, 1],
sunIntensity: 0.25
sunIntensity
: 0.25,
skyTop: [0.03, 0.06, 0.16]
skyTop
: [0.03, 0.06, 0.16],
skyBottom: [0.02, 0.016, 0.014]
skyBottom
: [0.02, 0.016, 0.014],
fogColor: [0.03, 0.06, 0.13]
fogColor
: [0.03, 0.06, 0.13],
fogDensity: 0.015
fogDensity
: 0.015,
fogFalloff: 0.12
fogFalloff
: 0.12,
},
points: {
readonly table: table.Table<table.TableDef<"PointLight", {
position: Vec3f;
range: F32;
color: Vec3f;
intensity: F32;
}, {}>>;
} & lighting.TablePoints & {
readonly table: table.Table<table.TableDef<"PointLight", {
position: Vec3f;
range: F32;
color: Vec3f;
intensity: F32;
}, {}>>;
}

Enables point lights from a table or a function. Default none.

points
: {
table: table.Table<table.TableDef<"PointLight", {
position: Vec3f;
range: F32;
color: Vec3f;
intensity: F32;
}, {}>> & lighting.LightTable

Supplies a table of lighting.PointLight, MovingPointLight, SpotLight or MovingSpotLight. Its fields start at zero, so give it defaults such as lighting.WHITE. Flush and destroy it yourself. Default none, lights from light or spot.

table
:
const lamps: table.Table<table.TableDef<"PointLight", {
position: Vec3f;
range: F32;
color: Vec3f;
intensity: F32;
}, {}>>
lamps
},
});
const
const source: () => targets.Texture<{
readonly format: "rgba16float";
readonly sampled: true;
}>
source
= () =>
const screen: targets.Targets<{
readonly hdr: {
readonly format: "rgba16float";
readonly sampled: true;
};
}>
screen
.
Targets<{ readonly hdr: { readonly format: "rgba16float"; readonly sampled: true; }; }>.textures: {
readonly hdr: targets.Texture<{
readonly format: "rgba16float";
readonly sampled: true;
}>;
}

Provides the current textures. A size change replaces them. Throws after destroy.

textures
.
hdr: targets.Texture<{
readonly format: "rgba16float";
readonly sampled: true;
}>
hdr
;
const
const glow: bloom.Bloom
glow
=
import bloom
bloom
.
function create(root: TgpuRoot, options: bloom.Options): bloom.Bloom

Creates bloom from a chain of half-size textures. Each level downsamples with 13 taps, then a tent filter adds it to the next larger level. The result is mip 0 of output, at half the source size. With region, passes draw and sample only that region; changing the region allocates nothing.

@allocates ― on resize: a new chain and the bind groups of its passes; so does a new source

@owns ― targets, a sampler, 3 pipelines, 2 bind groups and a buffer per level: the chain of glow levels, the settings, a group for each pass, and the region each pass reads

@records ― record after whatever wrote the source; then output holds the glow.

@param ― options.source the HDR picture, read at every record

@param ― options.region the part of the source that holds the picture, read at every record

@returns ― a bloom you record every frame and destroy

create
(
const root: TgpuRoot
root
, {
Options.source: finish.Rendered | (() => finish.FloatTexture)

Provides the HDR picture: a rendered picture, such as aa, or a function returning a texture, such as () => screen.textures.hdr. record reads it on every call, so the texture can be rebuilt.

source
,
Options.settings?: Partial<InferInput<WgslStruct<{
threshold: F32;
knee: F32;
spread: F32;
}>>> | undefined

Sets initial values over defaults. Default defaults, with threshold 1, knee 0.5 and spread 1.

settings
: {
threshold: number
threshold
: 1.2 } });
const
const final: finish.Finish
final
=
import finish
finish
.
function create(root: TgpuRoot, options: finish.Options): finish.Finish

Creates the final pass of a frame. It reads the source, adds glow, tone maps, grades, applies the vignette, encodes and dithers. It draws one triangle into the caller pass.

@allocates ― on resize: a bind group for a new source or glow

@owns ― 2 buffers, a sampler, a pipeline, 2 bind groups: the settings, the regions it reads, and a group each for the source and the glow

@records ― record into a pass of its own on the view you give, usually the canvas's, or draw into the caller's pass, after the bloom's record.

@param ― options.source the picture, rendered or a texture, read at every draw

@param ― options.region the part of the source that holds the picture, read at every draw

@param ― options.glow a bloom, or a glow of your own, added before tone mapping

@param ― options.exposure an adapted exposure, read on the GPU

@returns ― a final pass you draw every frame and destroy

create
(
const root: TgpuRoot
root
, {
Options.format: GPUTextureFormat

Sets the output format, such as the canvas format.

format
:
const surface: web.Surface
surface
.
Surface.format: GPUTextureFormat
format
,
Options.source: finish.Rendered | (() => finish.FloatTexture)

Provides the picture at any size: a rendered picture, such as aa or blur, or a function returning a texture, such as () => screen.textures.hdr. draw reads it on every call, so the texture can be rebuilt.

source
,
Options.glow?: finish.Rendered | undefined

Adds a bloom, or a glow of your own such as an emission target, before tone mapping. draw reads its output and region on every call. Default none.

glow
,
Options.settings?: Partial<InferInput<WgslStruct<{
exposure: F32;
vignette: F32;
dither: F32;
glowIntensity: F32;
}>>> | undefined

Sets initial values over defaults. Default defaults, with exposure 1, vignette 0.35, dither 1 and glowIntensity 0.12.

settings
: {
exposure: number
exposure
: 1.2 },
});
Setting At night
skyTop, skyBottom Dark blue above, near black below. They light every surface and fill every reflection.
sunColor, sunIntensity A pale blue moon from 0.05 to 0.25.
fogColor The sky’s colour near the horizon. A pale fog brightens a dark scene at any density.
bloom threshold 0.8 to 1.2. Raise it until lit surfaces stop glowing and only lamps, flames and sparks do.
finish exposure 1.2, or an adapted exposure.

Point lights provide most of the light at night. Give a lantern or a fire an intensity near 8 and a range near 11, and small lamps an intensity from 0.5 to 3. See point lights and their shadows.

An adapted exposure aims the picture’s average at key. The default, 0.18, is a daylit scene’s average. It brightens a night scene to the same average and lifts its shadows. Set key to the average the scene is lit for:

import {
import exposure
exposure
,
import finish
finish
} from '@typegpu/kit';
const
const eye: exposure.Exposure
eye
=
import exposure
exposure
.
function create(root: TgpuRoot, options: exposure.Options): exposure.Exposure

Creates an exposure that adapts to the picture on the GPU. Each record averages the log luminance of 4096 filtered samples over the source in one workgroup. It then eases the exposure towards key divided by that average, within lowest and highest. The exposure stays in a GPU buffer, and nothing is read back.

@allocates ― on resize: a bind group for a new source

@owns ― 3 buffers, a sampler, a pipeline and a bind group: the settings, the frame's region and time, the exposure, and a group for the source

@injects ― per-instance exposure.current, for the final pass or shaders of your own

@binds ― 1 storage: exposure.current, read-only

@records ― record after whatever wrote the source, before the final pass that reads it

@param ― options.source the linear picture, read at every record

@param ― options.region the part of the source that holds the picture, read at every record

@returns ― an exposure you record every frame and destroy

create
(
const root: TgpuRoot
root
, {
Options.source: (() => finish.FloatTexture) | finish.Rendered

Provides the linear picture to measure, usually the final pass's source: a rendered picture, such as aa, or a function returning a texture. record reads it on every call, so the texture can be rebuilt.

source
,
Options.settings?: Partial<InferInput<WgslStruct<{
key: F32;
lowest: F32;
highest: F32;
rise: F32;
fall: F32;
}>>> | undefined

Sets initial values over defaults. Default defaults, with key 0.18, lowest 1/16, highest 16, rise 1 and fall 3.

settings
: {
key: number
key
: 0.04 } });
const
const final: finish.Finish
final
=
import finish
finish
.
function create(root: TgpuRoot, options: finish.Options): finish.Finish

Creates the final pass of a frame. It reads the source, adds glow, tone maps, grades, applies the vignette, encodes and dithers. It draws one triangle into the caller pass.

@allocates ― on resize: a bind group for a new source or glow

@owns ― 2 buffers, a sampler, a pipeline, 2 bind groups: the settings, the regions it reads, and a group each for the source and the glow

@records ― record into a pass of its own on the view you give, usually the canvas's, or draw into the caller's pass, after the bloom's record.

@param ― options.source the picture, rendered or a texture, read at every draw

@param ― options.region the part of the source that holds the picture, read at every draw

@param ― options.glow a bloom, or a glow of your own, added before tone mapping

@param ― options.exposure an adapted exposure, read on the GPU

@returns ― a final pass you draw every frame and destroy

create
(
const root: TgpuRoot
root
, {
Options.format: GPUTextureFormat

Sets the output format, such as the canvas format.

format
:
const surface: web.Surface
surface
.
Surface.format: GPUTextureFormat
format
,
Options.source: finish.Rendered | (() => finish.FloatTexture)

Provides the picture at any size: a rendered picture, such as aa or blur, or a function returning a texture, such as () => screen.textures.hdr. draw reads it on every call, so the texture can be rebuilt.

source
,
Options.exposure?: Pick<exposure.Exposure, "inject"> | undefined

Multiplies settings.exposure by an exposure adapted to the picture on the GPU, such as one from exposure.create recorded before this pass. Default none.

exposure
:
const eye: exposure.Exposure
eye
});
  • A street lit by lamps and the moon averages about 0.04. A night sky lit by fireworks averages about 0.007.
  • highest caps how far the exposure rises in a dark moment, such as a street whose fire went out. Set it a little above the exposure the scene looks right at.
  • A flash brightens the average, and the exposure falls at fall to dim the picture around it.