First post here in the main thread, and it’s a safe one, if I’m being honest. If someone can figure out a better way to do fast, pixel based rendering on the Norns, feel free to ruin me.
The Problem
Norns drawing be slow af (when it comes to game and/or pixel stuff)
but why?
- right now we do all our drawing from Lua, which means our drawing lives in the single threaded event queue with everything else Lua on Norns.
- the norns platform still isn’t ready for easy, user-defined C behavior, but once we are there I’m hoping there will be a way to implement something low level for pixel stuff! There’s all sorts of fun stuff to be had with doing stuff like storing most of the background statically and doing a small amount of dynamic drawing to simulate lighting. I also think that the IRL non-linear effect of pixel brightness (a 15 outshines a 12 way more than a 6 outshines a 3) will allow for some clever collapsing of draw levels: think sprite pallette sets but even more primitive! Smooth animation for things like stars and waterfalls could be had with simple draws and pallette swaps! I should add support for storing/recalling zeroes cleverly so that we can have hidden pixels separate from nil pixels that can be switched on via a pallette swap.
The Solution
- don’t render everything as pixels! I already have some ideas for this, mostly around replacing “particle effects” with some rotating and scaling of drawing shapes. Pretty confident I can find some patterns that work and don’t clog up the Lua queue just to look good.
- use drawtables that sort pixel positions into groups based on brightness. This changes your drawing code from, at worst,
number of pixels * 3 separate calls to screen functions (level, pixel, fill for each pixel) to only number of levels * (pixels at that level + 2) calls (level, pixel_1… pixel_n, fill for each level). Below is the code to do the pixel array -> drawtable conversion and I recommend you use that! Pixel arrays (a table containing width // tilesize tables of height // tilesize numbers from 1-15, which represent the drawing levels) are still way more readable in the source than drawtables, so I would recommend just doing your conversions on the first load or init or something… Directly declared draw tables are really hard to parse back into an image in your head, at least for me!
- Right now I’m messing around with some drawing coroutines, but it’s probably going to work out being more of a head start on wrapping my head around solid world-drawing code in C. I can’t wait until there’s something robust for saying: “here is a simple representation of the world, please do the drawing thank you.” There’s also probably room for separating rendering backend+assets from game logic so that games could be ported to desktop platforms with a little Cairo surface at some point!
Some Code
woodplank-pixels = {
{5,3,3,5,3,5,5,3},
{1,1,1,1,1,1,1,1},
{5,3,3,5,5,3,2,3},
{1,1,1,1,1,1,1,1},
{5,3,5,3,5,3,3,3},
{1,1,1,1,1,1,1,1},
{5,3,3,5,3,3,5,5},
{1,1,1,1,1,1,2,1}
} -- see the crude woodgrain and knots?
function px_to_drawtable(p)
local t = {}
for j = 1,8 do
for k = 1,8 do
local z = p[j][k]
if t[z] == nil then t[z] = {} end
-- there are smarter ways to do the insert
-- than in nested loops,
-- but this function is only called once per tile
table.insert(t[z],vector(j,k))
end
end
return t
end
local x,y = 1,2 -- for coordinate addressing; in reality I use a vector module
function draw_tile(self)
local ox,oy = self.coords.x,self.coords.y
local dtab = self.dtab
for level,coords in pairs(dtab) do
screen.level(level)
for _,v in ipairs(coords) screen.pixel(x+ox-2,y+oy-2) end
screen.fill()
end
end
function init()
my_tile = {
coords = {64,64},
dtab = px_to_drawtable(woodplank-pixels),
draw = draw_tile
}
end
function redraw()
screen.clear()
my_tile:draw()
screen.update()
end