Skip to content

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, and sprite.fp from your builtin materials directory into your custom materials directory.
  • Rename them to light.material, etc, or put them in a light folder.
  • Set the tag on the material to light, which will refer to the light 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, and model.fp from your builtin materials directory into your custom materials directory.
  • Rename them to quad.material, etc, or put them in a quad 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.