Log In  


Cart #tline3drot_example-0 | 2024-04-13 | Embed ▽ | License: CC4-BY-NC-SA
24

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.
24


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

if the camera follows the player, how i can make it works?


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


[Please log in to post a comment]