Added some changes to the render script to add the constants and a snap toggle. You can see the impact the new material has by lowering the sharpening. You’ll need to add a key to toggle the snapping. I used F3 for testing it. If you disable the snapping while 1 is active you can see the jittery effects as you slowly scale the window.
I set the smoothing to 9.9 so it’s obvious that it works but it should be at 10.0 or higher. Maybe 20.0? Or less with some games maybe. When you scale the window with the snap toggled off and 2 active you can see where the nearest neighbor jittering is greatly smoothed out. You have to scale the window at pretty small increments to notice the jitter with this demo.
local WIDTH = 64
local HEIGHT = 64
local IDENTITY = vmath.matrix4()
local LOWREZ_PROJECTION = vmath.matrix4_orthographic(0, WIDTH, 0, HEIGHT, -1, 1)
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.lowrez_pred = render.predicate({"lowrez"})
self.controls_pred = render.predicate({"controls"})
self.view = IDENTITY
--self.projection = LOWREZ_PROJECTION
-- render target buffer parameters
local color_params = {
format = render.FORMAT_RGBA,
width = WIDTH,
height = HEIGHT,
min_filter = render.FILTER_NEAREST,
mag_filter = render.FILTER_NEAREST,
u_wrap = render.WRAP_CLAMP_TO_EDGE,
v_wrap = render.WRAP_CLAMP_TO_EDGE
}
local depth_params = {
format = render.FORMAT_DEPTH,
width = WIDTH,
height = HEIGHT,
u_wrap = render.WRAP_CLAMP_TO_EDGE,
v_wrap = render.WRAP_CLAMP_TO_EDGE
}
self.rt = render.render_target("lowrez", { [render.BUFFER_COLOR_BIT] = color_params, [render.BUFFER_DEPTH_BIT] = depth_params })
local clear_color = vmath.vector4(0, 0, 0, 0)
clear_color.x = sys.get_config("render.clear_color_red", 0)
clear_color.y = sys.get_config("render.clear_color_green", 0)
clear_color.z = sys.get_config("render.clear_color_blue", 0)
clear_color.w = sys.get_config("render.clear_color_alpha", 0)
self.clear_buffers = {
[render.BUFFER_COLOR_BIT] = clear_color,
[render.BUFFER_DEPTH_BIT] = 1,
[render.BUFFER_STENCIL_BIT] = 0
}
self.upscale_material = hash("lowrez")
self.scale_snap = true
end
local function clear(self, w, h)
-- clear
render.set_view(IDENTITY)
render.set_projection(vmath.matrix4_orthographic(0, w, 0, h, -1, 1))
render.set_depth_mask(true)
render.set_stencil_mask(0xff)
render.clear(self.clear_buffers)
end
local function draw_game(self)
clear(self, render.get_window_width(), render.get_window_height())
render.set_viewport(0, 0, WIDTH, HEIGHT)
-- draw world (sprites, tiles, pfx etc)
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.set_view(self.view)
render.set_projection(LOWREZ_PROJECTION)
render.draw(self.tile_pred)
render.draw(self.particle_pred)
render.draw_debug3d()
-- draw screen space gui
render.set_view(vmath.matrix4())
render.set_projection(LOWREZ_PROJECTION)
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
local function draw_upscaled(self)
-- calculate zoom
local window_width = render.get_window_width()
local window_height = render.get_window_height()
local zoom = math.min(window_width / WIDTH, window_height / HEIGHT)
if self.scale_snap then zoom = math.max(1, math.floor(zoom)) end
-- positioning
local width = WIDTH * zoom
local height = HEIGHT * zoom
local offsetx = (window_width - width) / 2
local offsety = (window_height - height) / 2
-- draw!
render.set_viewport(offsetx, offsety, width, height)
render.set_view(IDENTITY)
render.set_projection(IDENTITY)
render.enable_texture(0, self.rt, render.BUFFER_COLOR_BIT)
local constants = render.constant_buffer()
constants.sharpness = vmath.vector4(20.0, 1.0, 1.0, 1.0)
constants.texture_size = vmath.vector4(WIDTH, HEIGHT, 1.0, 1.0)
render.enable_material(self.upscale_material)
render.draw(self.lowrez_pred, constants)
render.disable_material()
render.disable_texture(0, self.rt)
end
local function draw_controls(self)
render.set_viewport(0, 0, render.get_window_width(), render.get_window_height())
render.set_view(IDENTITY)
render.set_projection(vmath.matrix4_orthographic(0, render.get_window_width(), 0, render.get_window_height(), -1, 1))
render.enable_state(render.STATE_STENCIL_TEST)
render.draw(self.controls_pred)
--render.draw(self.text_pred)
render.disable_state(render.STATE_STENCIL_TEST)
end
function update(self)
clear(self, render.get_window_width(), render.get_window_height())
render.enable_render_target(self.rt)
draw_game(self)
render.disable_render_target(self.rt)
draw_upscaled(self)
draw_controls(self)
end
function on_message(self, message_id, message)
if message_id == hash("clear_color") then
self.clear_buffers[render.BUFFER_COLOR_BIT] = message.color
elseif message_id == hash("set_view_projection") then
self.view = message.view
elseif message_id == hash("set_upscale_material") then
print("set_upscale_material", message.material)
self.upscale_material = message.material
elseif message_id == hash("toggle_scale_snap") then
if self.scale_snap then
self.scale_snap = false
else
self.scale_snap = true
end
end
end
function on_input(self, action_id, action)
if action_id and SET_MATERIAL_ACTIONS[action_id] then
if action.pressed then
print(SET_MATERIAL_ACTIONS[action_id])
msg.post("@render:", "set_upscale_material", { material = SET_MATERIAL_ACTIONS[action_id] })
end
elseif action_id and (action.pressed or action.repeated) then
self.actions[action_id] = true
end
if action_id == hash("key_f3") and action.released then
msg.post("@render:", "toggle_scale_snap")
end
end
A higher resolution than 64x64 may make the smoothness more noticeable as well. Celeste is 320x180.