How to Make Simple Lighting in Defold
It seems this post had the effect I wanted it to. I've been working on this game again. While I could go over everything I've started doing, we'll skip that and just say that in this post we're going to be adding simple light maps to a 2D game in Defold by modifying the render script.
To add this feature to my current game in theory involves copying it from my other game with some slight adjustments. This post was supposed to walk me through modifying the render script so I didn't have to do it again, but this time, my new game uses a custom orthographic camera library, which has its own custom render script, so we're back in the same situation again of trying to figure out how to mix the functionality of both render scripts into one.
Let's try again
Add a Light Map
Graphic
- Try this one
- Add it to an atlas.
- Now you can add it to gameobjects like
/player#sprite_light
or/campfire#sprite_light
- Make sure to set the
light.material
we are going to create on the sprite.
Material
- Copy
sprite.material
,sprite.vp
, andsprite.fp
from your builtin materials directory into your custom materials directory. - Rename them to
light.material
, etc, or put them in alight
folder. - Set the tag on the material to
light
, which will refer to thelight
predicate in our render script.
Custom Render Script
- Define
/render/custom.render
and/render/custom.render_script
- Set the script to the render file
- Set the render file in the
game.project
Code
There really is no getting around this part. If you're using different libraries or features that involve a custom render script, you'll have to look at all of them and figure out how to consolidate it. Lucky for us, the orthographic library handles the cameras for us. Unlucky for us, we have to render the world and the lights to two separate targets. Then we combine them onto a quad, which we have not created yet. Another thing not to forget is that when the screen is resized, we have to resize the targets (update the state).
local camera = require "orthographic.camera"
camera.ORTHOGRAPHIC_RENDER_SCRIPT_USED = true
local IDENTITY = vmath.matrix4()
local function create_predicates(...)
local arg = {...}
local predicates = {}
for _, predicate_name in pairs(arg) do
predicates[predicate_name] = render.predicate({predicate_name})
end
return predicates
end
local function create_state()
local state = {}
local color = vmath.vector4(0, 0, 0, 0)
color.x = sys.get_config_number("render.clear_color_red", 0)
color.y = sys.get_config_number("render.clear_color_green", 0)
color.z = sys.get_config_number("render.clear_color_blue", 0)
color.w = sys.get_config_number("render.clear_color_alpha", 0)
state.color = color
state.clear_buffers = {
[render.BUFFER_COLOR_BIT] = color,
[render.BUFFER_DEPTH_BIT] = 1,
[render.BUFFER_STENCIL_BIT] = 0
}
state.render_targets = {}
local color_params = {
format = render.FORMAT_RGBA,
width = render.get_window_width(),
height = render.get_window_height(),
min_filter = render.FILTER_LINEAR,
mag_filter = render.FILTER_LINEAR,
u_wrap = render.WRAP_CLAMP_TO_EDGE,
v_wrap = render.WRAP_CLAMP_TO_EDGE
}
local parameters = {
[render.BUFFER_COLOR_BIT] = color_params
}
state.render_targets.world = render.render_target("world", parameters)
state.render_targets.lights = render.render_target("lights", parameters)
return state
end
function init(self)
self.predicates = create_predicates("tile", "gui", "debug_text", "particle", "light", "quad")
self.state = create_state()
end
local function clear_target(target, clear_color)
render.set_render_target(target)
render.set_depth_mask(true)
render.set_stencil_mask(0xff)
render.clear({
[render.BUFFER_COLOR_BIT] = clear_color,
[render.BUFFER_DEPTH_BIT] = 1,
[render.BUFFER_STENCIL_BIT] = 0
})
render.set_depth_mask(false)
end
local function begin_render_pass(blend_src, blend_dst)
render.enable_state(render.STATE_BLEND)
render.set_blend_func(blend_src or render.BLEND_SRC_ALPHA, blend_dst or render.BLEND_ONE_MINUS_SRC_ALPHA)
render.disable_state(render.STATE_DEPTH_TEST)
render.disable_state(render.STATE_CULL_FACE)
render.disable_state(render.STATE_STENCIL_TEST)
end
local function render_camera_passes(cameras, predicates, predicate_keys)
for _, camera_id in ipairs(cameras) do
local viewport = camera.get_viewport(camera_id)
render.set_viewport(viewport.x, viewport.y, viewport.z, viewport.w)
local view = camera.get_view(camera_id)
render.set_view(view)
local proj = camera.get_projection(camera_id)
render.set_projection(proj)
local frustum = proj * view
for _, key in ipairs(predicate_keys) do
render.draw(predicates[key], { frustum = frustum })
end
render.draw_debug3d()
end
end
local function render_target_pass(self, target, clear_color, predicate_keys)
clear_target(target, clear_color)
begin_render_pass()
local cameras = camera.get_cameras()
if #cameras > 0 then
render_camera_passes(cameras, self.predicates, predicate_keys)
end
render.set_render_target(render.RENDER_TARGET_DEFAULT)
end
local function render_world(self)
render_target_pass(self, self.state.render_targets.world, self.state.color, { "tile", "particle" })
end
local function render_lights(self)
render_target_pass(self, self.state.render_targets.lights, self.state.color, { "light" })
end
local function set_default_render_state()
render.set_view(IDENTITY)
render.set_projection(IDENTITY)
render.set_depth_mask(false)
render.disable_state(render.STATE_DEPTH_TEST)
render.disable_state(render.STATE_STENCIL_TEST)
render.enable_state(render.STATE_BLEND)
render.set_blend_func(render.BLEND_SRC_ALPHA, render.BLEND_ONE_MINUS_SRC_ALPHA)
render.disable_state(render.STATE_CULL_FACE)
end
local function render_targets_to_quad(self)
set_default_render_state()
render.enable_material(hash("light_quad"))
render.enable_texture(0, self.state.render_targets.world, render.BUFFER_COLOR0_BIT)
render.enable_texture(1, self.state.render_targets.lights, render.BUFFER_COLOR0_BIT)
render.draw(self.predicates.quad)
render.disable_texture(0)
render.disable_texture(1)
render.disable_material()
end
local function render_gui(self)
local window_width = render.get_window_width()
local window_height = render.get_window_height()
if window_width > 0 and window_height > 0 then
render.disable_state(render.STATE_DEPTH_TEST)
render.disable_state(render.STATE_CULL_FACE)
render.enable_state(render.STATE_STENCIL_TEST)
render.set_viewport(0, 0, window_width, window_height)
view = IDENTITY
render.set_view(view)
proj = vmath.matrix4_orthographic(0, window_width, 0, window_height, -1, 1)
render.set_projection(proj)
frustum = proj * view
render.draw(self.predicates.gui, {frustum = frustum})
render.draw(self.predicates.debug_text, {frustum = frustum})
render.disable_state(render.STATE_STENCIL_TEST)
end
end
function update(self)
render_world(self)
render_lights(self)
render_targets_to_quad(self)
render_gui(self)
end
local function update_state(self)
local width = render.get_window_width()
local height = render.get_window_height()
local color_params = {
format = render.FORMAT_RGBA,
width = width,
height = height,
min_filter = render.FILTER_LINEAR,
mag_filter = render.FILTER_LINEAR,
u_wrap = render.WRAP_CLAMP_TO_EDGE,
v_wrap = render.WRAP_CLAMP_TO_EDGE
}
local parameters = {
[render.BUFFER_COLOR_BIT] = color_params
}
self.state.render_targets.world = render.render_target("world", parameters)
self.state.render_targets.lights = render.render_target("lights", parameters)
end
local MSG_CLEAR_COLOR = hash("clear_color")
local MSG_WINDOW_RESIZED = hash("window_resized")
function on_message(self, message_id, message)
if message_id == MSG_CLEAR_COLOR then
self.state.color = message.color
elseif message_id == MSG_WINDOW_RESIZED then
update_state(self)
end
end
As long as you're coming straight from orthographic
camera, this should all work.
But not so fast, we need a quad.
Add a Quad
This a surface we are going to mix both render targets onto.
Configure Material
- Copy
model.material
,model.vp
, andmodel.fp
from your builtin materials directory into your custom materials directory. - Rename them to
quad.material
, etc, or put them in aquad
folder. - The tag on the material will be
quad
- You also need to define two textures on
quad.material
, one for each render target (the world and the lights)
samplers {
name: "tex0"
wrap_u: WRAP_MODE_CLAMP_TO_EDGE
wrap_v: WRAP_MODE_CLAMP_TO_EDGE
filter_min: FILTER_MODE_MIN_LINEAR
filter_mag: FILTER_MODE_MAG_LINEAR
}
samplers {
name: "tex1"
wrap_u: WRAP_MODE_CLAMP_TO_EDGE
wrap_v: WRAP_MODE_CLAMP_TO_EDGE
filter_min: FILTER_MODE_MIN_LINEAR
filter_mag: FILTER_MODE_MAG_LINEAR
}
Write Fragment Shader
In /shaders/quad/quad.fp
:
varying mediump vec2 var_texcoord0;
uniform lowp sampler2D tex0;
uniform lowp sampler2D tex1;
uniform lowp vec4 tint;
void main()
{
vec4 color_world = texture2D(tex0, var_texcoord0.xy);
vec4 color_light = texture2D(tex1, var_texcoord0.xy);
gl_FragColor = color_world * color_light;
}
This will probably get adjusted, for example this is where I add a factor to adjust how dark it is. This will be maximum darkness outside the light map.
Add Material as Render Resource
You can see where this goes on /render/custom.render
. Remember, you have to use the name from the render script, light_quad
.
render_resources {
name: "light_quad"
path: "/shaders/quad/quad.material"
}
Add the Material to a Game Object Model
- I just go to my
game.collection
, which is not the main collection. - I create a top level object in the collection and add a model component:
/quad#model
. - Set the quad material on the model.
Wrapping Up
It should just work.
The goal here was to lay out everything so that you can quickly add darkness and lighting by rendering the world and lights onto two different targets and then post processing them together. If you're lost, you can always rewatch the original video that gets you started solving this and figure it out all over again.
In case you couldn't tell, I'm adding dungeons and caves.