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.
Another nitpick: You can use for v in all() do
instead of for _,v in pairs() do
[Please log in to post a comment]