Issues when trying to render to a custom render target

I definitely am losing my mind a bit here trying to render my game to a custom target so I can implement full screen effects. I have followed a few tutorials and checked everywhere, but I can’t seem to grasp what I’m doing wrong exactly

I have this render script and a render target correctly setup with a quad and a custom material, the projection seems to work fine except it doesn’t look good

--
-- projection that centers content with maintained aspect ratio and optional zoom
--
local function fixed_projection(near, far, zoom)
    local projected_width = render.get_window_width() / (zoom or 1)
    local projected_height = render.get_window_height() / (zoom or 1)
    local xoffset = -(projected_width - render.get_width()) / 2
    local yoffset = -(projected_height - render.get_height()) / 2
    return vmath.matrix4_orthographic(xoffset, xoffset + projected_width, yoffset, yoffset + projected_height, near, far)
end
--
-- projection that centers and fits content with maintained aspect ratio
--
local function fixed_fit_projection(near, far)
    local width = render.get_width()
    local height = render.get_height()
    local window_width = render.get_window_width()
    local window_height = render.get_window_height()
    local zoom = math.min(window_width / width, window_height / height)
    return fixed_projection(near, far, zoom)
end
--
-- projection that stretches content
--
local function stretch_projection(near, far)
    return vmath.matrix4_orthographic(0, render.get_width(), 0, render.get_height(), near, far)
end

local function get_projection(self)
    return self.projection_fn(self.near, self.far, self.zoom)
end

function init(self)
    self.tile_pred = render.predicate({ "tile" })
    self.gui_pred = render.predicate({ "gui" })
    self.text_pred = render.predicate({ "text" })
    self.particle_pred = render.predicate({ "particle" })

    self.clear_color = vmath.vector4(0, 0, 0, 0)
    self.clear_color.x = sys.get_config("render.clear_color_red", 0)
    self.clear_color.y = sys.get_config("render.clear_color_green", 0)
    self.clear_color.z = sys.get_config("render.clear_color_blue", 0)
    self.clear_color.w = sys.get_config("render.clear_color_alpha", 0)

    self.view = vmath.matrix4()

    -- default is stretch projection. copy from builtins and change for different projection
    -- or send a message to the render script to change projection:
    -- msg.post("@render:", "use_stretch_projection", { near = -1, far = 1 })
    -- msg.post("@render:", "use_fixed_projection", { near = -1, far = 1, zoom = 2 })
    -- msg.post("@render:", "use_fixed_fit_projection", { near = -1, far = 1 })
    self.near = -1
    self.far = 1
    self.projection_fn = fixed_projection

    -- Create a new render target.
    local color_params = { format = render.FORMAT_RGBA, width = render.get_window_width(), height = render.get_window_height() }
    local target_params = { [render.BUFFER_COLOR_BIT] = color_params }
    self.target = render.render_target("target", target_params)
    self.window_pred = render.predicate({ "postprocess" })
end

function update(self)
    local window_width = render.get_window_width()
    local window_height = render.get_window_height()
    if window_width == 0 or window_height == 0 then
        return
    end

    -- clear screen buffers
    --
    render.set_depth_mask(true)
    render.set_stencil_mask(0xff)
    render.clear({ [render.BUFFER_COLOR_BIT] = self.clear_color, [render.BUFFER_DEPTH_BIT] = 1, [render.BUFFER_STENCIL_BIT] = 0 })

    -- render world (sprites, tilemaps, particles etc)
    --
    local proj = get_projection(self)
    local frustum = proj * self.view

    render.set_viewport(0, 0, window_width, window_height)
    render.set_view(self.view)
    render.set_projection(proj)

    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)

    render.draw(self.tile_pred, { frustum = frustum })
    render.draw(self.particle_pred, { frustum = frustum })
    render.draw_debug3d()

    -- Set the render target to our target.
    render.set_render_target(self.target)
    -- Clear the render target with the games clear color.
    render.clear({ [render.BUFFER_COLOR_BIT] = self.clear_color, [render.BUFFER_DEPTH_BIT] = 1, [render.BUFFER_STENCIL_BIT] = 0 })
    -- Draw all objects with the "tile" tag in their material.
    render.draw(self.tile_pred, { frustum = frustum })
    -- Reset the render target.
    render.set_render_target(render.RENDER_TARGET_DEFAULT)

    -- Set the texture in slot 0 of the material to our render target.
    render.enable_texture(0, self.target, render.BUFFER_COLOR_BIT)
    -- Draw the window quad.
    render.draw(self.window_pred)
    -- Unset the texture slot.
    render.disable_texture(0, self.target)

    -- render GUI
    --
    local view_gui = vmath.matrix4()
    local proj_gui = vmath.matrix4_orthographic(0, window_width, 0, window_height, -1, 1)
    local frustum_gui = proj_gui * view_gui

    render.set_view(view_gui)
    render.set_projection(proj_gui)

    render.enable_state(render.STATE_STENCIL_TEST)
    render.draw(self.gui_pred, { frustum = frustum_gui })
    render.draw(self.text_pred, { frustum = frustum_gui })
    render.disable_state(render.STATE_STENCIL_TEST)
end

function on_message(self, message_id, message)
    if message_id == hash("clear_color") then
        self.clear_color = message.color
    elseif message_id == hash("set_view_projection") then
        self.view = message.view
        self.projection = message.projection
    elseif message_id == hash("use_camera_projection") then
        self.projection_fn = function() return self.projection or vmath.matrix4() end
    elseif message_id == hash("use_stretch_projection") then
        self.near = message.near or -1
        self.far = message.far or 1
        self.projection_fn = stretch_projection
    elseif message_id == hash("use_fixed_projection") then
        self.near = message.near or -1
        self.far = message.far or 1
        self.zoom = message.zoom or 1
        self.projection_fn = fixed_projection
    elseif message_id == hash("use_fixed_fit_projection") then
        self.near = message.near or -1
        self.far = message.far or 1
        self.projection_fn = fixed_fit_projection
    end
end

This is how it looks in the engine vs running on the game

I don’t even know what “version” of the render_script this is at this point but every version I’ve made I got the same issue, it renders in this weird way almost like my GPU is dying

1 Like

I have this other version I was trying to make:

local camera = require "orthographic.camera"

local IDENTITY = vmath.matrix4()

local function create_render_target(self)
    local w = render.get_window_width()
    local h = render.get_window_height()
    if self.normal_rt then
        render.delete_render_target(self.normal_rt)
    end
    local color_params = {
        format     = render.FORMAT_RGBA,
        width      = w,
        height     = h,
        min_filter = render.FILTER_LINEAR,
        mag_filter = render.FILTER_LINEAR,
        u_wrap     = render.WRAP_CLAMP_TO_EDGE,
        v_wrap     = render.WRAP_CLAMP_TO_EDGE,
    }
    self.normal_rt = render.render_target("normal", { [render.BUFFER_COLOR_BIT] = color_params })
end

function init(self)
    self.tile_pred     = render.predicate({ "tile" })
    self.particle_pred = render.predicate({ "particle" })
    self.gui_pred      = render.predicate({ "gui" })
    self.text_pred     = render.predicate({ "text" })
    self.quad_pred     = render.predicate({ "postprocess" })

    self.clear_color   = vmath.vector4(
        sys.get_config_number("render.clear_color_red", 0.1),
        sys.get_config_number("render.clear_color_green", 0.3),
        sys.get_config_number("render.clear_color_blue", 0.7),
        sys.get_config_number("render.clear_color_alpha", 1.0)
    )

    self.width         = render.get_window_width()
    self.height        = render.get_window_height()

    create_render_target(self)
end

function update(self)
    local win_w = render.get_window_width()
    local win_h = render.get_window_height()

    if self.width ~= win_w or self.height ~= win_h then
        self.width  = win_w
        self.height = win_h
        create_render_target(self)
    end

    for _, cam_id in ipairs(camera.get_cameras()) do
        local vp         = camera.get_viewport(cam_id)
        local view       = camera.get_view(cam_id)
        local projection = camera.get_projection(cam_id)
        local frustum    = projection * view

        render.set_render_target(self.normal_rt)

        render.set_viewport(0, 0, win_w, win_h)
        render.set_depth_mask(false)
        render.disable_state(render.STATE_DEPTH_TEST)
        render.disable_state(render.STATE_STENCIL_TEST)
        render.disable_state(render.STATE_CULL_FACE)
        render.enable_state(render.STATE_BLEND)
        render.set_blend_func(render.BLEND_SRC_ALPHA, render.BLEND_ONE_MINUS_SRC_ALPHA)

        render.clear({ [render.BUFFER_COLOR_BIT] = self.clear_color })
        render.set_view(view)
        render.set_projection(projection)
        render.draw(self.tile_pred, { frustum = frustum })
        render.draw(self.particle_pred, { frustum = frustum })

        render.set_render_target(render.RENDER_TARGET_DEFAULT)

        render.set_viewport(vp.x, vp.y, vp.z, vp.w)
        render.clear({ [render.BUFFER_COLOR_BIT] = self.clear_color })
        render.set_view(IDENTITY)
        render.set_projection(IDENTITY)

        render.disable_state(render.STATE_BLEND)
        render.enable_texture(0, self.normal_rt, render.BUFFER_COLOR_BIT)
        render.draw(self.quad_pred)
        render.disable_texture(0, self.normal_rt)
        render.enable_state(render.STATE_BLEND)
    end

    render.set_viewport(0, 0, win_w, win_h)
    render.set_view(IDENTITY)
    render.set_projection(vmath.matrix4_orthographic(0, win_w, 0, win_h, -1, 1))
    render.enable_state(render.STATE_BLEND)
    render.set_blend_func(render.BLEND_SRC_ALPHA, render.BLEND_ONE_MINUS_SRC_ALPHA)
    render.enable_state(render.STATE_STENCIL_TEST)
    render.draw(self.gui_pred)
    render.draw(self.text_pred)
    render.disable_state(render.STATE_STENCIL_TEST)
end

function on_message(self, message_id, message)
    if message_id == hash("clear_color") then
        self.clear_color = message.color
    end
end

but the result is somehow even more weird?

Also forgot to mention I’m using the Orthographic Camera by britzl

1 Like

As we improve the built in camera solution there’s fewer and fewer reasons to use Orthographic. To rule out sources of problems you could try to use the standard camera component instead.

If that doesn’t work then please share a zip archive with a minimal project here.

2 Likes

I removed Orthographic from the project and with the default camera it now at least looks like it’s close rendering correctly:

this is the empty project I’m trying this on

Empty Project.zip (2.1 MB)