This cartridge is an example of the tline3d rotation algorithm!
I adapted this code from @freds72 's PICO-8 sprite rotation algorithm with tline(). They also helped me with some small edits via the Picotron Discord! You can check out freds' cart here: https://www.lexaloffle.com/bbs/?tid=37561
Here's the uncommented version to add to your project files (I'd recommend making a new file, such as rspr.lua, then including that file in your project to keep your own code clean):
-- rspr.lua
function rspr(sprite,cx,cy,sx,sy,rot)
sx = sx and sx or 1
sy = sy and sy or 1
rot = rot and rot or 0
local tex = get_spr(sprite)
local dx,dy = tex:width()*sx,tex:height()*sy
local quad = {
{x=0, y=0, u=0, v=0},
{x=dx, y=0, u=tex:width()-0.001, v=0},
{x=dx, y=dy, u=tex:width()-0.001, v=tex:height()-0.001},
{x=0, y=dy, u=0, v=tex:height()-0.001},
}
local c,s = cos(rot),-sin(rot)
local w,h = (dx-1)/2, (dy-1)/2
for _,v in pairs(quad) do
local x,y = v.x-w,v.y-h
v.x = c*x-s*y
v.y = s*x+c*y
end
tquad(quad, tex, cx, cy)
end
function tquad(coords,tex,dx,dy)
local screen_max = get_display():height()-1
local p0,spans = coords[#coords],{}
local x0,y0,u0,v0=p0.x+dx,p0.y+dy,p0.u,p0.v
for i=1,#coords do
local p1 = coords[i]
local x1,y1,u1,v1=p1.x+dx,p1.y+dy,p1.u,p1.v
local _x1,_y1,_u1,_v1=x1,y1,u1,v1
if(y0>y1) x0,y0,x1,y1,u0,v0,u1,v1=x1,y1,x0,y0,u1,v1,u0,v0
local dy=y1-y0
local dx,du,dv=(x1-x0)/dy,(u1-u0)/dy,(v1-v0)/dy
if(y0<0) x0-=y0*dx u0-=y0*du v0-=y0*dv y0=0
local cy0=ceil(y0)
local sy=cy0-y0
x0+=sy*dx
u0+=sy*du
v0+=sy*dv
for y=cy0,min(ceil(y1)-1,screen_max) do
local span=spans[y]
if span then tline3d(tex,span.x,y,x0,y,span.u,span.v,u0,v0)
else spans[y]={x=x0,u=u0,v=v0} end
x0+=dx
u0+=du
v0+=dv
end
x0,y0,u0,v0=_x1,_y1,_u1,_v1
end
end |
You don't need to call tquad yourself, rspr will do that for you. rpsr takes 6 arguments, the last three of which are optional:
- sprite: the spritesheet index to draw
- cx: the screen x-coordinate to render the sprite (the center of the sprite)
- cy: the screen y-coordinate to render the sprite (the center of the sprite)
- sx: (optional) the scale factor in the x-axis (defaults to 1)
- sy: (optional) the scale factor in the y-axis (defaults to 1)
- rot: (optional) the angle at which to rotate your sprite [0-1)
By no means is this code minified or code-golfed, so feel free to hack away at the token count if you wish!
In the comments below I'll drop a commented version that does a better job of explaining what each line does.
Current limitations:
- tline3d doesn't like bitmaps that don't have dimensions that are a power of 2. Be sure that your sprite has side-lengths that are a power of 2 (i.e. 8, 16, 32, 64, etc). This code can handle rectangles. If your sprite doesn't take up the entire space canvas, just use a transparency color to give it padding.
- draws from the CENTER of the sprite, not the top-left corner. Keep this in mind when rendering your sprites if you also need to handle collisions or other positional updates.
Here's the commented code for education purposes!
-- rspr - draw a rotated sprite
-- param: sprite - the number of the sprite to read from the spritesheet
-- param: cx - the x-coordinate on which to render the sprite (center of the sprite, not top-left)
-- param: cy - the y-coordinate on which to render the sprite (center of the sprite, not top-left)
-- param: sx - scale factor in the x direction
-- param: sy - scale factor in the y direction
-- param: rot - the angle to render the sprite [0-1)
function rspr(sprite,cx,cy,sx,sy,rot)
-- get default values
sx = sx and sx or 1
sy = sy and sy or 1
rot = rot and rot or 0
-- get sprite userdata and sprite dimensions
local tex = get_spr(sprite)
local dx,dy = tex:width()*sx,tex:height()*sy
-- create a polygon of 4 points that will represent the corners of the rendered sprite
-- we subtract an arbitrarily small amount from the texture to prevent artifacting (tline3d bug)
local quad = {
{x=0, y=0, u=0, v=0},
{x=dx, y=0, u=tex:width()-0.001, v=0},
{x=dx, y=dy, u=tex:width()-0.001, v=tex:height()-0.001},
{x=0, y=dy, u=0, v=tex:height()-0.001},
}
-- calculate cosine and sine, as well as the center point for the sprite
local c,s = cos(rot),-sin(rot)
local w,h = (dx-1)/2, (dy-1)/2
-- rotate each point in the quad
for _,v in pairs(quad) do
local x,y = v.x-w,v.y-h
v.x = c*x-s*y
v.y = s*x+c*y
end
-- render the quad to the display
tquad(quad, tex, cx, cy)
end
-- tquad - render a textured quadrilateral. based on @fred72's implementation here: https://www.lexaloffle.com/bbs/?tid=37561
-- param: coords - a table with 4 subtables. each subtable has values for x, y, u, v
-- param: tex - a bitmap userdata that represents the texture to render
-- param: dx, dy - the center of the quad on screen
function tquad(coords,tex,dx,dy)
-- what is the max y value that will get rendered? use this to cull unnecessary draw ops
local screen_max = get_display():height()-1
-- load the first set of points into the p0 table
local p0,spans = coords[#coords],{}
-- extract coordinates, offset by the desired position
local x0,y0,u0,v0=p0.x+dx,p0.y+dy,p0.u,p0.v
-- loop through the polygon's vertices
for i=1,#coords do
-- get the next vertex from the list
local p1 = coords[i]
-- extract coordinates, offset by desired position
local x1,y1,u1,v1=p1.x+dx,p1.y+dy,p1.u,p1.v
-- save those coordinate values to restore later
local _x1,_y1,_u1,_v1=x1,y1,u1,v1
-- swap coordinate pairs if they are out of order (with respect to y)
if(y0>y1) x0,y0,x1,y1,u0,v0,u1,v1=x1,y1,x0,y0,u1,v1,u0,v0
-- get the difference between y0 and y1
local dy=y1-y0
-- get the change in x,u,v based on the slope of y
local dx,du,dv=(x1-x0)/dy,(u1-u0)/dy,(v1-v0)/dy
-- cull our calculations to not go above y=0 to save CPU
if(y0<0) x0-=y0*dx u0-=y0*du v0-=y0*dv y0=0
-- snap y0 to a pixel value
local cy0=ceil(y0)
-- get the fractional difference between y0 and cy0
local sy=cy0-y0
-- adjust x,u,v by the fractional offset
x0+=sy*dx
u0+=sy*du
v0+=sy*dv
-- loop through all of the horizontal rows that we need to draw to make the rotated shape
-- we stop at screen_max if the sprite goes off screen to save CPU
for y=cy0,min(ceil(y1)-1,screen_max) do
-- a "span" is a row of pixels that we'll draw. attempt to get the span that we saved for this y-value from the list
local span=spans[y]
-- if we found a span, then draw a horizontal line across the span, sampling the texture from the sprite provided
if span then tline3d(tex,span.x,y,x0,y,span.u,span.v,u0,v0)
-- if we didn't find a span, add one to the list
else spans[y]={x=x0,u=u0,v=v0} end
-- add the change-in values to each coordinate based on the slope of y
x0+=dx
u0+=du
v0+=dv
end
-- restore coordinates to prepare for the next loop
x0,y0,u0,v0=_x1,_y1,_u1,_v1
end
end |
Since this has already been bumped, did you mean for quad to be a global variable? (line 8)
@edrato, I believe this code will work with the camera as well - wherever the sprite you're drawing is located in the game world (cx, cy), if that location is present within the camera's clipping rect, the sprite will be visible.
@Soupster, great point! No, that should not be a global. Thank you for pointing that out.
Hey! Thank you so much for this. I was still getting artifacting when using your function though, and the solution was to remove the - 0.001 and add the 0x100 flag to tline3d which skips rendering the last pixel.
Also, since this is quite a CPU-intensive operation, I decided to add a fallback to spr when no scaling or rotation needs to be done. That way we can just call this one function everywhere we need to render a sprite without worrying much about the performance implications.
Here's my fixed version:
--- Draw a sprite scaled and rotated
--- @param sprite integer - sprite index from spritesheet
--- @param cx number - center x-position of sprite
--- @param cy number - center y-position of sprite
--- @param sx number? - scale factor for width
--- @param sy number? - scale factor for height
--- @param rot number? - rotation [0-1), where 1 = 360 degrees
function draw_ex(sprite, cx, cy, sx, sy, rot)
sx = sx and sx or 1
sy = sy and sy or 1
rot = rot and rot or 0
local tex = get_spr(sprite)
if sx == 1 and sy == 1 and rot == 0 then
spr(sprite, cx - tex:width() / 2, cy - tex:height() / 2)
return
end
local dx, dy = tex:width() * sx, tex:height() * sy
local quad = {
{ x = 0, y = 0, u = 0, v = 0 },
{ x = dx, y = 0, u = tex:width(), v = 0 },
{ x = dx, y = dy, u = tex:width(), v = tex:height() },
{ x = 0, y = dy, u = 0, v = tex:height() },
}
local c, s = cos(rot), -sin(rot)
local w, h = (dx - 1) / 2, (dy - 1) / 2
for _, v in pairs(quad) do
local x, y = v.x - w, v.y - h
v.x = c * x - s * y
v.y = s * x + c * y
end
Sprite.tquad(quad, tex, cx, cy)
end
function tquad(coords, tex, dx, dy)
local screen_max = get_display():height() - 1
local p0, spans = coords[#coords], {}
local x0, y0, u0, v0 = p0.x + dx, p0.y + dy, p0.u, p0.v
for i = 1, #coords do
local p1 = coords[i]
local x1, y1, u1, v1 = p1.x + dx, p1.y + dy, p1.u, p1.v
local _x1, _y1, _u1, _v1 = x1, y1, u1, v1
if (y0 > y1) then x0, y0, x1, y1, u0, v0, u1, v1 = x1, y1, x0, y0, u1, v1, u0, v0 end
local dy = y1 - y0
local dx, du, dv = (x1 - x0) / dy, (u1 - u0) / dy, (v1 - v0) / dy
if (y0 < 0) then
x0 -= y0 * dx
u0 -= y0 * du
v0 -= y0 * dv
y0 = 0
end
local cy0 = ceil(y0)
local sy = cy0 - y0
x0 += sy * dx
u0 += sy * du
v0 += sy * dv
for y = cy0, min(ceil(y1) - 1, screen_max) do
local span = spans[y]
if span then
tline3d(tex, span.x, y, x0, y, span.u, span.v, u0, v0, 1, 1, 0x100)
else
spans[y] = { x = x0, u = u0, v = v0 }
end
x0 += dx
u0 += du
v0 += dv
end
x0, y0, u0, v0 = _x1, _y1, _u1, _v1
end
end |
I am working on a game that uses camera() and the OG version of tquad was causing my spite to not be displayed if my object's ( that i'm drawing the sprite on) xy was was outside of get_display():height().
I asked Grok3 for an implementation that did not require a limit and it came up with this. Working for me sofar, hope it helps someone else too.
function tquad(coords, tex, dx, dy)
-- Get current camera offsets
local cam_x, cam_y = camera()
-- Initialize variables
local p0, spans = coords[#coords], {}
local x0, y0, u0, v0 = p0.x + dx - cam_x, p0.y + dy - cam_y, p0.u, p0.v
for i = 1, #coords do
local p1 = coords[i]
local x1, y1, u1, v1 = p1.x + dx - cam_x, p1.y + dy - cam_y, p1.u, p1.v
local _x1, _y1, _u1, _v1 = x1, y1, u1, v1
-- Swap if y0 > y1 to ensure correct scanline order
if y0 > y1 then
x0, y0, x1, y1, u0, v0, u1, v1 = x1, y1, x0, y0, u1, v1, u0, v0
end
local dy = y1 - y0
local dx, du, dv = dy ~= 0 and (x1 - x0) / dy or 0, dy ~= 0 and (u1 - u0) / dy or 0, dy ~= 0 and (v1 - v0) / dy or 0
-- Adjust starting point if y0 is negative
if y0 < 0 then
x0 = x0 - y0 * dx
u0 = u0 - y0 * du
v0 = v0 - y0 * dv
y0 = 0
end
local cy0 = ceil(y0)
local sy = cy0 - y0
x0 = x0 + sy * dx
u0 = u0 + sy * du
v0 = v0 + sy * dv
-- Draw scanlines
for y = cy0, ceil(y1) - 1 do
local span = spans[y]
if span then
tline3d(tex, span.x, y, x0, y, span.u, span.v, u0, v0, 1, 1, 0x100)
else
spans[y] = { x = x0, u = u0, v = v0 }
end
x0 = x0 + dx
u0 = u0 + du
v0 = v0 + dv
end
x0, y0, u0, v0 = _x1, _y1, _u1, _v1
end
end |
[Please log in to post a comment]




