Why does the shader only read from the first colour buffer in a MRT?[solved]

I am currently trying to recreate the Deferred shading tutorial form Learnopengl in defold but it seems like only the first colour buffer is read from the MRT g_buffer by the lighting pass shader. Is this a defold limitation or am I getting something wrong?

Thanks in advance!

Render script:

local NR_LIGHTS = 32
local light_positions = {}
local light_colours = {}
local LQR = {} -- used to bypass the inability to pass structs to shaders

function init(self)
	local float_params = { -- position and normal
		format = graphics.TEXTURE_FORMAT_RGBA16F,
		width = render.get_window_width(),
		height = render.get_window_height(),
		min_filter = graphics.TEXTURE_FILTER_NEAREST,
		mag_filter = graphics.TEXTURE_FILTER_NEAREST,
		u_wrap = graphics.TEXTURE_WRAP_CLAMP_TO_EDGE,
		v_wrap = graphics.TEXTURE_WRAP_CLAMP_TO_EDGE
	}

	local albedo_spec_params = {
		format = graphics.TEXTURE_FORMAT_RGBA,
		width = render.get_window_width(),
		height = render.get_window_height(),
		min_filter = graphics.TEXTURE_FILTER_NEAREST,
		mag_filter = graphics.TEXTURE_FILTER_NEAREST,
		u_wrap = graphics.TEXTURE_WRAP_CLAMP_TO_EDGE,
		v_wrap = graphics.TEXTURE_WRAP_CLAMP_TO_EDGE
	}

	local blit_params = {
		format = graphics.TEXTURE_FORMAT_RGBA32F, -- needs to be precise
		width = render.get_window_width(),
		height = render.get_window_height(),
		min_filter = graphics.TEXTURE_FILTER_NEAREST,
		mag_filter = graphics.TEXTURE_FILTER_NEAREST,
		u_wrap = graphics.TEXTURE_WRAP_CLAMP_TO_EDGE,
		v_wrap = graphics.TEXTURE_WRAP_CLAMP_TO_EDGE
	}

	local depth_params = {
		format = graphics.TEXTURE_FORMAT_DEPTH,
		width = render.get_window_width(),
		height = render.get_window_height()
	}

	self.g_buffer = render.render_target({
		[graphics.BUFFER_TYPE_COLOR0_BIT] = float_params,
		[graphics.BUFFER_TYPE_COLOR1_BIT] = float_params,
		[graphics.BUFFER_TYPE_COLOR2_BIT] = albedo_spec_params,
		[graphics.BUFFER_TYPE_COLOR3_BIT] = blit_params,
		[graphics.BUFFER_TYPE_DEPTH_BIT] = depth_params
	})

	self.g_buffer_pred = render.predicate({"g_buffer"})
	self.deferred_shading_pred = render.predicate({"deferred_shading"})
	self.lightbox_pred = render.predicate({"lightbox"})

	self.ds_constants = render.constant_buffer()
	self.lb_constants = render.constant_buffer()

	-- transfer to controller script???
	math.randomseed(os.time())

	for i = 1, NR_LIGHTS do
		-- calculate slightly random offsets
		local x_pos = ((math.random(100) / 100) * 6 - 3)
		local y_pos = ((math.random(100) / 100) * 6 - 4)
		local z_pos = ((math.random(100) / 100) * 6 - 3)

		table.insert(light_positions, vmath.vector4(x_pos, y_pos, z_pos, 0))

		-- calculate random colour
		local r = ((math.random(200) / 100) + 0.5) -- between 0.5 and 1
		local g = ((math.random(200) / 100) + 0.5) -- between 0.5 and 1
		local b = ((math.random(200) / 100) + 0.5) -- between 0.5 and 1

		table.insert(light_colours, vmath.vector4(r, g, b, 0))
	end
end

local function clear_buffer()
	render.clear({[graphics.BUFFER_TYPE_COLOR0_BIT] = vmath.vector4(0, 0, 0, 1), [graphics.BUFFER_TYPE_DEPTH_BIT] = 1})
end

function update(self, dt)
	local w, h = render.get_window_width(), render.get_window_height()

	render.enable_state(graphics.STATE_DEPTH_TEST)


	---
	--- 1. geometry pass: render scene's geometry/color data into gbuffer
	---
	render.set_render_target(self.g_buffer)

	clear_buffer()

	render.set_viewport(0, 0, w, h)

	render.set_view(self.view)
	render.set_projection(self.projection) -- might need to use vmath.matrix4_perspective

	render.draw(self.g_buffer_pred)

	render.set_render_target(render.RENDER_TARGET_DEFAULT)


	---
	--- 2. lighting pass: calculate lighting by iterating over a screen filled quad pixel-by-pixel using the gbuffer's content.
	--- 
	render.disable_state(graphics.STATE_DEPTH_TEST)

	clear_buffer()

	render.set_viewport(0, 0, w, h)

	render.set_view(vmath.matrix4())
	render.set_projection(vmath.matrix4())

	render.enable_texture(0, self.g_buffer, graphics.BUFFER_TYPE_COLOR0_BIT)
	render.enable_texture(1, self.g_buffer, graphics.BUFFER_TYPE_COLOR1_BIT)
	render.enable_texture(2, self.g_buffer, graphics.BUFFER_TYPE_COLOR2_BIT)

	-- set uniforms
	self.ds_constants.light_positions = light_positions
	self.ds_constants.light_colours = light_colours

	for i = 1, NR_LIGHTS do
		-- update attenuation parameters and calculate radius
		local constant = 1 -- note that we don't send this to the shader, we assume it is always 1.0 (in our case)
		local linear = 0.7
		local quadratic = 1.8

		-- then calculate radius of light volume/sphere
		local max_brightness = math.max(math.max(light_colours[i].x, light_colours[i].y), light_colours[i].z)
		local radius = (-linear + math.sqrt(linear * linear - 4 * quadratic * (constant - (256/5) * max_brightness))) / (2 * quadratic)

		LQR[i] = vmath.vector4(linear, quadratic, radius, 0)
	end

	self.ds_constants.LQR = LQR

	self.ds_constants.view_pos = vmath.vector4(0, 0, 0, 5)

	render.draw(self.deferred_shading_pred, {constants = self.ds_constants})

	render.disable_texture(0)
	render.disable_texture(1)
	render.disable_texture(2)


	---
	--- 2.5. copy content of geometry's depth buffer to default framebuffer's depth buffer. Blitting.
	--- An extension can be written to access opengl for blitting but I will just create another colour
	--- buffer in the geometry pass to write the depth values to. The lightbox shader then reads from this buffer.
	--- 

	---
	--- 3. render lights on top of scene
	--- 
	render.set_view(self.view)
	render.set_projection(self.projection)

	render.enable_state(graphics.STATE_DEPTH_TEST)

	-- how do I set the uniforms since it iterates over a table.

	-- Work around for blitting. 
	render.enable_texture(0, self.g_buffer, graphics.BUFFER_TYPE_COLOR3_BIT)

	render.draw(self.lightbox_pred, {constants = self.lb_constants})

	render.disable_texture(0)
end

function on_message(self, message_id, message, sender)
	if message_id == hash("set_view_projection") then
		self.view = message.view
		self.projection = message.projection
	elseif message_id == hash("get_light_data") then
		msg.post(sender, "light_data", {positions = light_positions, colours = light_colours})
	end
end

g_buffer.fp

#version 330

layout (location = 0) out vec3 gPosition;
layout (location = 1) out vec3 gNormal;
layout (location = 2) out vec4 gAlbedoSpec;
layout (location = 3) out float blitDepth;

in vec2 TexCoords;
in vec3 FragPos;
in vec3 Normal;

uniform sampler2D texture_diffuse1;
uniform sampler2D texture_specular1;

/*void main()
{    
    // store the fragment position vector in the first gbuffer texture
    gPosition = FragPos;
    // also store the per-fragment normals into the gbuffer
    gNormal = normalize(Normal);
    // and the diffuse per-fragment color
    gAlbedoSpec.rgb = texture(texture_diffuse1, TexCoords).rgb;
    // store specular intensity in gAlbedoSpec's alpha component
    gAlbedoSpec.a = texture(texture_specular1, TexCoords).r;
    // store the depth value in blitDepth
    blitDepth = gl_FragCoord.z;
}*/

// debugger
void main()
{
    gPosition    = vec3(1.0, 0.0, 0.0); // should be red
    gNormal      = vec3(0.0, 1.0, 0.0); // should be green
    gAlbedoSpec  = vec4(0.0, 0.0, 1.0, 1.0); // should be blue
    blitDepth    = gl_FragCoord.z;
}

lightpass.fp

#version 330
out vec4 FragColor;

in vec2 TexCoords;

uniform sampler2D g_position;
uniform sampler2D g_normal;
uniform sampler2D g_albedo_spec;

struct Light {
    vec3 Position;
    vec3 Color;
    
    float Linear;
    float Quadratic;
    float Radius;
};

const int NR_LIGHTS = 32;
// Can't pass structs directly so I did a workaround.
//uniform Light lights[NR_LIGHTS];

Light lights[NR_LIGHTS];

uniform frags{
	vec4 light_positions[NR_LIGHTS];
	vec4 light_colours[NR_LIGHTS];
	vec4 LQR[NR_LIGHTS];

	vec4 view_pos;
};

void set_light_data()
{
	for(int i = 0; i < NR_LIGHTS; ++i)
	{
		Light light;
		light.Position = light_positions[i].xyz;
		light.Color = light_colours[i].xyz;
		light.Linear = LQR[i].x;
		light.Quadratic = LQR[i].y;
		light.Radius = LQR[i].z;

		lights[i] = light;
	}
}

/*void main()
{             
    // retrieve data from gbuffer
    vec3 FragPos = texture(g_position, TexCoords).rgb;
    vec3 Normal = texture(g_normal, TexCoords).rgb;
    vec3 Diffuse = texture(g_albedo_spec, TexCoords).rgb;
    float Specular = texture(g_albedo_spec, TexCoords).a;
    
    // then calculate lighting as usual
    vec3 lighting  = Diffuse * 0.1; // hard-coded ambient component
    vec3 viewDir  = normalize(view_pos.xyz - FragPos);

	set_light_data();

    for(int i = 0; i < NR_LIGHTS; ++i)
    {
        // calculate distance between light source and current fragment
        float distance = length(lights[i].Position - FragPos);
        if(distance < lights[i].Radius)
        {
            // diffuse
            vec3 lightDir = normalize(lights[i].Position - FragPos);
            vec3 diffuse = max(dot(Normal, lightDir), 0.0) * Diffuse * lights[i].Color;
            // specular
            vec3 halfwayDir = normalize(lightDir + viewDir);  
            float spec = pow(max(dot(Normal, halfwayDir), 0.0), 16.0);
            vec3 specular = lights[i].Color * spec * Specular;
            // attenuation
            float attenuation = 1.0 / (1.0 + lights[i].Linear * distance + lights[i].Quadratic * distance * distance);
            diffuse *= attenuation;
            specular *= attenuation;
            lighting += diffuse + specular;
        }
    }    
    FragColor = vec4(lighting, 1.0);
}*/

//debugger
void main()
{
    // switching between g_position, g_normal and g_albedo_spec produces the same red colour. See below.
    FragColor = vec4(texture(g_albedo_spec, TexCoords).rgb, 1.0);
}


Result:

@Pawel didn’t you do a deferred rendering example at some point recently?

I don’t know if this is correct, but notice how the multitargeting output is made in this example (no layout info ).

Also, when testing a shader, keep in mind that the compiler or driver may optimize and remove unused textures from the shader (there may be an error with the order of setting by number in the render script; it is better to set by name).

3 Likes

:sweat_smile:

I’m using version 140 and I’m just using unforms for textures in my gbuffer.fp for g-buffer pass:

uniform sampler2D albedo;
uniform sampler2D normal;
uniform sampler2D ao;
uniform sampler2D roughness;
uniform sampler2D metallic;

And just like that, when you use then in your render script textures:

	render.enable_texture(0, self.g_buffer, graphics.BUFFER_TYPE_COLOR0_BIT)
	render.enable_texture(1, self.g_buffer, graphics.BUFFER_TYPE_COLOR1_BIT)
	render.enable_texture(2, self.g_buffer, graphics.BUFFER_TYPE_COLOR2_BIT)

to draw to your gbuffer render target with the material with such a fp you should have all of them in it. You definitely should be able to draw up to 4 textures to the MRT.

I used multitargeting for the Bloom example and it worked fine. I do not understand why it is a problem now. I tried the method but no change.

Can you share either this project or a repro?

Deferred Shading.zip (40.8 KB)

Backpack model download link (too large to upload)

1 Like

I think the issue may stem from passing tables to lightpass.fp using render.constant_buffer(). The docs show that render.constant_buffer() can be used to pass tables though. What is the proper way to pass tables to shaders in defold?

The fix is set the textures by name instead of number:

render.enable_texture("g_position", self.g_buffer, graphics.BUFFER_TYPE_COLOR0_BIT)
render.enable_texture("g_normal", self.g_buffer, graphics.BUFFER_TYPE_COLOR1_BIT)
render.enable_texture("g_albedo_spec", self.g_buffer, graphics.BUFFER_TYPE_COLOR2_BIT)

instead of

render.enable_texture(0, self.g_buffer, graphics.BUFFER_TYPE_COLOR0_BIT)
render.enable_texture(1, self.g_buffer, graphics.BUFFER_TYPE_COLOR1_BIT)
render.enable_texture(2, self.g_buffer, graphics.BUFFER_TYPE_COLOR2_BIT)
3 Likes

I can’t download it, access denied (?). Could you please send it to pawel@defold.se?

It should work with the texture unit, maybe it’s a bug :thinking:

I’ve sent it. There’s also another problem with the backpacks not translating and scaling properly.