DEMIENGINEDOCUMENTATION Everything is free
3D gameplay · Game systems

Lighting, environment, and screen/world conversion.

Lights & camera

Lights make 3D art readable; the camera decides what the player sees and where a click lands in the world. This page covers the three light types, the environment and post effects, cosmetic denting, camera conversions, and transform directions.

Why lights exist

Without lights, a 3D scene is flat grey: triangles have no shade, no highlight, no sense of depth. A light is a component that tells the renderer where illumination comes from. Three types cover almost every scene, each matching a real-world source.

The three light types

ComponentWhat it imitatesUse it for
DirectionalLightDistant sun — parallel rays across the whole sceneKey sunlight on Skyforge exterior decks; one per scene is usually enough.
PointLightBare bulb — shines outward in all directions from a pointLamps, consoles, explosion flashes, warning beacons.
SpotLightFlashlight — a cone in one directionSearchlights, doorways, hangar bay floods, aiming lights.
{
  "DirectionalLight": {
    "color": [1, 0.95, 0.85],
    "intensity": 1.2,
    "casts_shadows": true
  }
}

{
  "PointLight": {
    "color": [0.4, 0.8, 1],
    "intensity": 2.0,
    "range": 8
  }
}

{
  "SpotLight": {
    "color": [1, 1, 1],
    "intensity": 3.0,
    "range": 20,
    "inner_angle": 15,
    "outer_angle": 35
  }
}
FieldMeaning
colorRGB of the emitted light, each 0 to 1.
intensityBrightness multiplier. Start near 1 and adjust.
rangeHow far the light reaches, in world units. Point and spot only.
casts_shadowsEnables directional shadows. Point/spot shadow casting is not implemented.
inner_angle / outer_angleAngles in degrees from the spotlight axis. Full brightness inside the inner cone, fading at the outer cone.

The built-in lighting path uses dynamically sized light data rather than a fixed four-point/four-spot array. All enabled, mask-matching lights contribute. The first matching directional light is the primary sun and can cast cascaded shadows; additional directional lights contribute illumination without shadows. Point and spot shadows remain unimplemented. Point/spot lights use Transform3D for position; both sun and spot directions follow transform rotation and parenting. GPU texture capacity and rendering cost still apply: more overlapping lights increase fragment work. Exceeding storage capacity reports an error rather than silently dropping lights.

Environment3D

Environment3D controls the panorama backdrop, ambient color/intensity, fog, anti-aliasing and shadow quality. A sky texture does not by itself provide reflection probes or physically based image lighting.

{
  "Environment3D": {
    "sky_texture": "asset://textures/orbit_sky",
    "ambient_intensity": 0.5,
    "shadow_cascades": 4,
    "shadow_resolution": 1024
  }
}
  • sky_texture references an equirectangular sky texture asset.
  • ambient_intensity multiplies ambient_color, independently of the sky image.
  • Place one Environment3D in the scene. Outdoors in Skyforge uses an orbital panorama; interiors can use a darker studio image.

PostProcessStack

For shadow distance, cascades, filtering, bias and their costs, see the directional shadow settings. Cascades improve near-camera detail without shrinking the whole scene's shadow range.

Post-processing is the final grade applied to the whole rendered image after the scene is drawn — bloom on neon signs, color grading for a cold station mood, vignette to focus the center.

{
  "PostProcessStack": {
    "bloom": 0.4,
    "bloom_threshold": 0.9,
    "saturation": 1.05,
    "contrast": 1.1,
    "vignette": 0.15
  }
}
  • bloom is the glow strength; bloom_threshold is the brightness cutoff.
  • saturation and contrast are numeric fields directly on the stack. Attach the stack to the camera entity.
  • vignette darkens the corners, 0 to 1.
  • Keep effects subtle — a heavy grade fights readability of models and UI.

Denting meshes

Some impact should leave a mark without splitting an object apart. Dentable3D opts a mesh into cosmetic deformation: hull plating that bows where a drone struck, cargo pods that bruise on hard landings.

{
  "Dentable3D": {}
}

Drive dents from Lua with demi.mesh.deformation:

local Deform = require("demi.mesh.deformation")

local accepted, error = Deform.dent(id, {
    point = {x = x, y = y, z = z},
    direction = {x = 0, y = 0, z = -1},
    radius = 0.32, depth = 0.08,
})
local accepted, error, depth = Deform.impact(id, contact)
Deform.reset(id)
CallArgumentsReturns
dentid and an impact table containing world-space point, inward direction, radius and depth.Boolean accepted, error string.
impactid, contact record with point_x/y/z, normal_x/y/z, impact_energy; optional material overrides.Boolean accepted, error string, dent depth.
resetidBoolean success. Restores this instance's original visual mesh.

Denting is visual only. Pair it with physics contact points for placement and with Destruction when the object should eventually break.

Camera3D conversions

Pass viewport-local pixels with a top-left origin, +Y down, and the actual viewport width and height. The functions account for the camera's target offset, up axis, projection and transform hierarchy.

local Camera3D = require("demi.camera3d")
local Physics = require("demi.physics.query3d")

local ray = Camera3D.screen_ray(camera_id, mx, my, view_w, view_h)
if ray then
    local origin, direction = ray.origin, ray.direction
    local hit = Physics.raycast(
        origin.x, origin.y, origin.z,
        direction.x, direction.y, direction.z, 100)
    if hit then
        -- hit.entity_id, hit.point, hit.normal, hit.distance
    end
end

local screen = Camera3D.world_to_screen(camera_id, px, py, pz, view_w, view_h)
if screen then
    -- screen.x and screen.y are label coordinates.
end

local wx, wy, wz = Camera3D.screen_to_world(
    camera_id, mx, my, view_w, view_h, 10)
-- World point 10 metres along the normalized screen ray.
  • screen_ray returns origin/direction vectors, not a physics hit. It returns nil if the camera or transform cannot be resolved.
  • world_to_screen returns a Vec2 table or nil outside near/far depth. Offscreen X/Y coordinates are not clipped.
  • screen_to_world returns three world-space scalars. Its final distance argument is measured along the ray, not along the camera's forward axis.
  • Physics.raycast's optional final argument is an ignored entity ID, not a layer filter.

FOV is in degrees. orthographic_size is the full visible height in world units.

Transform3D directions

Cameras, lights, and characters constantly need “which way am I facing?”. demi.transform3d answers in world space:

local T = require("demi.transform3d")

local x, y, z = T.get_position(id)
local rx, ry, rz = T.get_rotation(id)
local sx, sy, sz = T.get_scale(id)

T.set_position(id, x, y, z)
T.set_rotation(id, rx, ry, rz)     -- Euler radians
T.set_scale(id, sx, sy, sz)
T.add_position(id, dx, dy, dz)

local fx, fy, fz = T.forward(id)
local rxx, ryy, rzz = T.right(id)
local ux, uy, uz = T.up(id)

T.look_at(id, tx, ty, tz)
CallArgumentsReturns
get_position / get_rotation / get_scaleidThree local-space scalars. Each coordinate is nil when the entity or component is missing.
set_position / set_rotation / set_scaleid plus three scalarsBoolean success. Writes local-space values; rotation is Euler radians.
add_positionid, delta dx, dy, dzBoolean success. Offsets the current local position.
forward / right / upidThree world-space unit-vector components, or nil per component when missing.
look_atid, target tx, ty, tz in world spaceBoolean success. Aims the entity's +Z axis at the world point, compensating for its parent transform.

A spotlight that tracks the player:

local T = require("demi.transform3d")

local px, py, pz = T.get_position(player_id)
if px then T.look_at(spot_id, px, py, pz) end
  • This example assumes the player is a scene root, so its local position is also its world position.
  • Set the spotlight's local direction to [0, 0, 1] to match look_at's +Z axis; its default is -Z.
  • Hierarchy is respected, so a spotlight parented to a moving gantry still aims correctly.

Getters return separate numbers and nil per coordinate when the entity or component is missing — guard before arithmetic.

Scene setup checklist

  • One DirectionalLight for the key sun, one Environment3D for the sky and ambient.
  • PointLight and SpotLight only where the design calls for them.
  • One PostProcessStack with restrained settings.
  • One camera; conversions always take the real viewport size.
  • Transform3D.look_at for aiming, world vectors for facing checks.

Create and edit lighting in the editor

Open Create from entity preset (the prefab icon) in Hierarchy. Basic Lighting creates an environment and a shadow-casting sun together; Point Light and Spot Light create movable local lights with a Transform. Select a light using its Hierarchy row or the sun-shaped viewport marker. Use Move for a local light's position and Rotate to aim a spot or sun. Moving a directional sun changes its authoring marker, not its illumination direction.

For a desk lamp, parent a point or spot light under the lamp entity, then create a prefab from that hierarchy. Each instance has its own light. Use View → Studio preview lighting only for neutral inspection; disable it to see the authored lighting. Older sun entities without Transform offer Enable light gizmo in Inspector.