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
| Component | What it imitates | Use it for |
|---|---|---|
DirectionalLight | Distant sun — parallel rays across the whole scene | Key sunlight on Skyforge exterior decks; one per scene is usually enough. |
PointLight | Bare bulb — shines outward in all directions from a point | Lamps, consoles, explosion flashes, warning beacons. |
SpotLight | Flashlight — a cone in one direction | Searchlights, 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
}
}
| Field | Meaning |
|---|---|
color | RGB of the emitted light, each 0 to 1. |
intensity | Brightness multiplier. Start near 1 and adjust. |
range | How far the light reaches, in world units. Point and spot only. |
casts_shadows | Enables directional shadows. Point/spot shadow casting is not implemented. |
inner_angle / outer_angle | Angles 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_texturereferences an equirectangular sky texture asset.ambient_intensitymultipliesambient_color, independently of the sky image.- Place one
Environment3Din 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
}
}
bloomis the glow strength;bloom_thresholdis the brightness cutoff.saturationandcontrastare numeric fields directly on the stack. Attach the stack to the camera entity.vignettedarkens 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)
| Call | Arguments | Returns |
|---|---|---|
dent | id and an impact table containing world-space point, inward direction, radius and depth. | Boolean accepted, error string. |
impact | id, contact record with point_x/y/z, normal_x/y/z, impact_energy; optional material overrides. | Boolean accepted, error string, dent depth. |
reset | id | Boolean 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_rayreturns origin/direction vectors, not a physics hit. It returns nil if the camera or transform cannot be resolved.world_to_screenreturns a Vec2 table or nil outside near/far depth. Offscreen X/Y coordinates are not clipped.screen_to_worldreturns 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)
| Call | Arguments | Returns |
|---|---|---|
get_position / get_rotation / get_scale | id | Three local-space scalars. Each coordinate is nil when the entity or component is missing. |
set_position / set_rotation / set_scale | id plus three scalars | Boolean success. Writes local-space values; rotation is Euler radians. |
add_position | id, delta dx, dy, dz | Boolean success. Offsets the current local position. |
forward / right / up | id | Three world-space unit-vector components, or nil per component when missing. |
look_at | id, target tx, ty, tz in world space | Boolean 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
directionto[0, 0, 1]to matchlook_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
DirectionalLightfor the key sun, oneEnvironment3Dfor the sky and ambient. PointLightandSpotLightonly where the design calls for them.- One
PostProcessStackwith restrained settings. - One camera; conversions always take the real viewport size.
Transform3D.look_atfor 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.