Byte Engine Docs

Physically based lighting

Create directional, point, and cone lights with physical color and intensity units.

Use physical light units when you want lighting that responds predictably to distance and material properties.

Byte Engine provides three analytic light shapes. Point and cone lights can also use a baked IES profile when you need a measured angular intensity distribution:

LightBest intensity unitTypical use
DirectionalLightIlluminance in luxSunlight or another source that is effectively infinitely far away
PointLightLuminous intensity in candela or total flux in lumensBare bulbs and other omnidirectional sources
ConeLightLuminous intensity in candela or total flux in lumensSpotlights, flashlights, and focused fixtures

One world-space distance unit is one meter. Keep your scene at that scale so inverse-square attenuation and distance-based conversions remain meaningful.

Create a light

Choose a color, choose an intensity, and create the light. Then submit it through DefaultWorld::light_factory_mut so the active rendering pipeline can use it.

use byte_engine::{
    gameplay::world::DefaultWorld,
    math::Vector3,
    rendering::{DirectionalLight, LightColor, PhotometricIntensity},
};

let mut world = DefaultWorld::new();
let sunlight = DirectionalLight::new(
    Vector3::new(0.2, -1.0, 0.3),
    LightColor::TemperatureKelvin(5_778.0),
    PhotometricIntensity::Illuminance {
        lux: 100_000.0,
        measurement_distance_m: 1.0,
    },
)?;

world.light_factory_mut().create(sunlight.into());
# Ok::<(), byte_engine::rendering::PhotometricError>(())

Light constructors return PhotometricError when a color, magnitude, distance, or area is zero, negative, or not finite. A Kelvin value must be from 1,000 K through 40,000 K.

Choose a color

Use LightColor::TemperatureKelvin for a blackbody-style illuminant such as daylight, a tungsten lamp, or a warm practical light. Byte Engine converts the temperature through CIE chromaticity to linear sRGB.

Use LightColor::LinearSrgb for an authored color:

let color = LightColor::LinearSrgb(Vector3::new(1.0, 0.15, 0.02));

The RGB values describe chromaticity, not brightness. Byte Engine normalizes the color to unit photopic luminance. Set brightness with PhotometricIntensity; increasing the RGB values alone does not make the light brighter. RGB components must be nonnegative and the color must have nonzero luminance.

Choose an intensity

You can use every PhotometricIntensity variant with every light shape. Include the reference distance or area carried by the variant so Byte Engine can make a deterministic conversion.

Lux

Lux measures illuminance arriving at a surface. It is the most direct choice for a directional light. For a point or cone light, measurement_distance_m says where the requested illuminance must be reached.

let intensity = PhotometricIntensity::Illuminance {
    lux: 500.0,
    measurement_distance_m: 2.0,
};

For local lights, Byte Engine converts lux to candela with I = E × d². A light that produces 500 lux at 2 meters therefore has an intensity of 2,000 candela. The measurement distance is validated but does not change an already directional illuminance field.

Candela

Candela measures luminous intensity in one direction. It maps directly to point and cone lights. For a directional light, reference_distance_m defines the plane where the equivalent illuminance is measured.

let intensity = PhotometricIntensity::LuminousIntensity {
    candela: 1_200.0,
    reference_distance_m: 1.0,
};

A directional light converts this value with E = I / d². Local lights keep the candela value; their reference distance is validated but does not change it.

Lumens

Lumens measure total luminous flux. A point light distributes its lumens uniformly over steradians, so Byte Engine uses I = Φ / 4π.

A cone light distributes its lumens through its inner-to-outer soft cone. Byte Engine uses the same linear angular falloff on the CPU and GPU:

cone factor = clamp((cosine − cos(outer)) / (cos(inner) − cos(outer)), 0, 1)

Its effective solid angle is:

Ω = π × (2 − cos(inner half-angle) − cos(outer half-angle))
I = Φ / Ω

For a directional light, supply the beam area at the reference plane and Byte Engine uses E = Φ / A.

let intensity = PhotometricIntensity::LuminousFlux {
    lumens: 1_600.0,
    directional_beam_area_m2: 4.0,
};

directional_beam_area_m2 is required and validated for every light. Point and cone lights do not use it because their emission shape already defines how the flux is distributed.

Nits

Nits, or candela per square meter, measure luminance. Use this option when you know the luminance and projected area of the emitting surface.

let intensity = PhotometricIntensity::Luminance {
    nits: 300.0,
    projected_area_m2: 0.02,
    reference_distance_m: 2.0,
};

Point and cone lights use the uniform-emitter approximation I = L × A. Directional lights use E = L × A / d². Analytic lights do not retain the physical emitter shape after this conversion.

Create point and cone lights

The following point light emits 800 lumens uniformly:

use byte_engine::rendering::{LightColor, PhotometricIntensity, PointLight};

let bulb = PointLight::new(
    Vector3::new(0.0, 2.4, 0.0),
    LightColor::TemperatureKelvin(2_700.0),
    PhotometricIntensity::LuminousFlux {
        lumens: 800.0,
        directional_beam_area_m2: 1.0,
    },
)?;

The following cone light emits 1,600 lumens through a soft 20° to 30° half-angle range:

use byte_engine::rendering::{ConeLight, LightColor, PhotometricIntensity};

let spotlight = ConeLight::new(
    Vector3::new(0.0, 3.0, 0.0),
    Vector3::new(0.0, -1.0, 0.0),
    LightColor::TemperatureKelvin(3_200.0),
    PhotometricIntensity::LuminousFlux {
        lumens: 1_600.0,
        directional_beam_area_m2: 1.0,
    },
    20.0_f32.to_radians(),
    30.0_f32.to_radians(),
)?;

Cone angles are half angles measured from the light direction. The renderer automatically uses a 0.1 m near plane and a neutral exposure scale of 1. It extends the far plane until peak exposure-weighted illumination falls below 0.125 lux. These values control only the cone shadow view; they don't limit lighting attenuation. Override either endpoint with with_shadow_near or with_shadow_far, or override both with with_shadow_range. A cone with an outer half angle of 90° or more remains a valid light but cannot use one perspective shadow map.

Use an IES profile

Use an IES profile when you have a fixture's measured .ies file and want its real angular intensity pattern. Bake the source into your resource database, then pass its resource ID to PointLight::new_ies or ConeLight::new_ies.

The IES file supplies the peak candela and the relative intensity in every direction. LightColor only tints that measured light; do not pass a separate PhotometricIntensity value. Set dimmer to a linear fraction from 0.0 for off through 1.0 for the profile's measured output. For example, 0.01 runs the measured fixture at 1% output. The renderer applies the same dimmed intensity to shading, automatic shadow coverage, and shadow priority.

The renderer uses a dimmed unit-luminance fallback while the profile uploads asynchronously. If the resource is missing or is not a usable baked IES image, the visibility pipeline logs an error and the fallback remains active.

Use an Orientation to place the profile:

  • Local +Z is the emission axis.
  • Local +X is the IES C0 plane.
  • Local +Y is the IES C90 plane.

Keep the orientation when you animate a fixture, because its C0 plane defines the rotation of an asymmetric profile. Use orientation_from_direction only when a canonical zero-roll frame is enough.

use byte_engine::{
    math::{Orientation, Point},
    rendering::{LightColor, PointLight},
};

let office_fixture = PointLight::new_ies(
    Point::new(0.0, 2.4, 0.0),
    Orientation::identity(),
    LightColor::Kelvin(4_000.0),
    0.5,
    "lights/office.ies",
)?;

For a cone fixture, use ConeLight::new_ies with the same position, orientation, color, dimmer, and resource ID, followed by its inner and outer half angles. After the profile reaches the GPU, its dimmed calibrated peak candela also controls automatic shadow coverage and local-shadow priority.

Control local-light shadows

Point lights use a cube shadow map, so each selected point light renders six 90-degree shadow views. The renderer uses a 0.1 m near plane and extends the automatic far plane until the peak exposure-weighted illumination falls below 0.125 lux. This range controls shadow coverage only; it does not limit inverse-square lighting. Use with_shadow_near, with_shadow_far, or with_shadow_range on PointLight when your scene needs explicit coverage.

The visibility pipeline reuses up to four cone maps and four point-light cube maps per sink by default. Set render.cone-shadow-map-pool.capacity or render.point-shadow-map-pool.capacity at application startup to change either limit. A point-shadow capacity uses one cube map per light; each cube map has six 1024 by 1024 depth faces. For each pool, the renderer ranks visible lights by their projected sink coverage. It first reserves a slot for each sink's most important light when capacity allows, then continues through each sink's next-ranked light in sink order. The sinks reached before a partial final round fills the pool receive the remaining slots. Other visible local lights stay lit but do not cast shadows.

Understand the rendering result

Byte Engine resolves authoring units on the CPU. The GPU receives RGB lux for directional lights and RGB candela for point and cone lights. Local lights use inverse-square attenuation, and the PBR material evaluation applies diffuse albedo / π and specular BRDF terms.

For a local light, the incident RGB quantity at a surface is candela / distance², multiplied by the cone factor when applicable. For a directional light, the incident RGB quantity is its lux value directly. Shadows and material BRDF terms then reduce or redistribute that incident light; they do not change the authored unit conversion.

Camera exposure not available

The current camera doesn't apply photographic exposure. Don't treat its aperture field as a lighting exposure control.

Bright real-world values can look clipped or overly bright through AGX or ACES tone mapping. Keep physical values in your scene instead of compensating with arbitrary light scaling.

Physical calibration limits

Environment maps and emissive materials don't have a unified physical calibration with analytic lights. Radiometric units such as watts and watts per square meter aren't supported.

Rust API

After you create a light, submit it through DefaultWorld::light_factory_mut.

On this page