This example shows how to make sprites respond to Defold Light components with a custom material in a basic setup.
The example shows how to utilise Light components (introduced in Defold 1.13.1) and materials to add a simple lighting to the sprites. The light illuminates every sprite that uses the lit material. This is a forward-lighting technique: each sprite calculates its lighting as it is drawn.
Read more about Light components in the manual.
The collection contains:
ambient_light game object with ambient light componentpoint_light game object with point light component and a follow_cursor scriptspot_light game object with spot light componentscene consisting of game objects with sprites for background and three stones.Sprite components of the scene elements all use lit_sprite.material,
which makes them respond to Light components.
The scene uses 3 lights:
0.5.
It provides the minimum illumination across the whole scene.3.0 and a range of 360.
Its game object starts at Z = 140, placing the light above the sprites on the positive Z axis.
The Point Light game object also contains light.script.
The script moves the light to the mouse cursor,
demonstrating that Light components follow their game object’s world transform automatically.3.0 and a range of 800,
outter cone angle - 50 and inner cone angle - 30.The engine collects light components data and exposes it to materials
whose shaders declare the built-in LightBuffer layout.
Ambient lights are accumulated separately as color × intensity.
Directional, Point, and Spot lights are stored in the light array with their world position or direction, color, intensity, range, and type-specific values. Read more about light components in the Lights manual.
A Point Light uses the position of its game object. Its effective range is the component’s Range multiplied by the smallest absolute axis of the game object’s world scale. Moving or uniformly scaling the game object therefore moves or resizes the illuminated area automatically without a need to change shader constants from Lua.
The light component itself does not select which sprites it affects. Any material that consumes the global light buffer can respond to it, while sprites using the regular built-in sprite material remain unchanged.
Sprite vertices are in world space. lit_sprite.vp uses the material’s view_proj
constant to project each vertex to the screen, just like a regular sprite shader.
It also uses mtx_view to pass the fragment position in view space to the fragment shader.
The built-in lighting functions operate in view space, so the surface position,
surface normal, and light position must all use the same coordinate space.
Passing mtx_view also keeps the material correct if the scene would later use a different camera.
lit_sprite.fp defines MAX_LIGHT_COUNT and includes /builtins/materials/lighting.glsl.
The include declares the engine-owned light buffer and provides two helpers used here:
ambient_light() returns the accumulated Ambient Light contribution.diffuse_lambert(normal, position) evaluates the active Directional, Point, and Spot lights up to MAX_LIGHT_COUNT.Sprites have positions and texture coordinates, but no normal vertex attribute.
This example treats every sprite as a flat surface facing positive Z
and transforms that fixed (0, 0, 1) direction into view space.
A Point Light must therefore have a positive Z offset:
if it were placed directly in the same plane as sprite, the light direction would be perpendicular
to the fixed normal and its Lambert contribution would be zero.
For a Point Light, the included shader calculates the contribution from three factors:
max(dot(normal, direction_to_light), 0).clamp(1 - distance / range, 0, 1)².The fragment shader adds the ambient and diffuse contributions, multiplies the sampled premultiplied sprite color by the result, and preserves the texture alpha.
This shader sets MAX_LIGHT_COUNT to 4.
The value is the maximum number of non-ambient lights this material evaluates.
It must not exceed [light] max_count in game.project.
This example leaves that project setting at its default of 64.
If more than four non-ambient lights are active, this shader evaluates only the first four.
Ambient lights do not occupy entries in the shader array because the engine combines them into one ambient value,
although every Ambient Light still counts as a Light component in the project limit.
light.script acquires input focus and handles mouse movement in on_input().
It copies the cursor’s X and Y coordinates to the Point Light game object
while preserving the positive Z offset required by the flat surface normal.
Light transforms are read by the engine after game object updates,
so the shader receives the new world position each frame.
No custom render script or render target is needed. The default renderer draws the sprites normally, and the engine binds the light buffer automatically before drawing a material that declares it.
This approach is useful for 2D games that need colored light and smooth distance falloff. It keeps the setup close to regulat sprite rendering and allows the same Light components to be shared with lit 3D materials.
MAX_LIGHT_COUNT,
so large scenes should choose the limit deliberately or use a more selective lighting design.For normal-mapped 2D art, replace the fixed normal with a normal sampled from a texture and transform it into the same coordinate space as the light calculations.
For shadows or large numbers of purely 2D lights, other techniques like e.g. a screen-space light-map pipeline may be more appropriate.
See the Defold manuals for more detail about materials, shaders, and the render pipeline.
The stone image is CC0 artwork from Kenney’s Platformer Art Extended pack.
#version 140
in highp vec4 position;
in mediump vec2 texcoord0;
out highp vec4 var_position;
out mediump vec2 var_texcoord0;
out highp mat4 var_view;
uniform vs_uniforms
{
highp mat4 view_proj;
highp mat4 mtx_view;
};
void main()
{
var_position = mtx_view * vec4(position.xyz, 1.0);
var_texcoord0 = texcoord0;
var_view = mtx_view;
gl_Position = view_proj * vec4(position.xyz, 1.0);
}
#version 140
in highp vec4 var_position;
in mediump vec2 var_texcoord0;
in highp mat4 var_view;
out vec4 out_fragColor;
uniform mediump sampler2D texture_sampler;
#define MAX_LIGHT_COUNT 4
#include "/builtins/materials/lighting.glsl"
void main()
{
vec4 color = texture(texture_sampler, var_texcoord0);
vec3 normal = normalize((var_view * vec4(0.0, 0.0, 1.0, 0.0)).xyz);
vec3 light = ambient_light() + diffuse_lambert(normal, var_position.xyz);
out_fragColor = vec4(color.rgb * light, color.a);
}
function init(self)
msg.post(".", "acquire_input_focus") -- <1>
end
function on_input(self, action_id, action)
if action_id == nil then
local position = go.get_position()
position.x = action.x
position.y = action.y
go.set_position(position) -- <2>
end
end
--[[
1. Acquire input focus so this script receives mouse movement.
2. Move the Point Light game object to the cursor while preserving its Z position.
]]