1

I am currently working on a small project in PICO-8 (that's why the code is in Lua and I am rendering to a 128x128 screen), where I tried re-creating a pseudo-3D environment similar to the Mode-7 rendering on the SNES. I managed to get the camera rendering working with help from this video by javidx9. It looks like this:

--camera
function init_camera()
    cam = {}
    cam.dist = 2.5
    cam.x = ball.x+cam.dist
    cam.y = ball.y
    cam.a = 0
    cam.dx = 0
    cam.dy = 0
    cam.near = 0.125
    cam.far = 2
    cam.fovhalf = -0.6
    cam.spd = 0.01
    
    sky_offset = 0
    sky_speed = 0.01
end

function draw_camera()
    far_x1 = cam.x + cos(cam.a - cam.fovhalf) * cam.far
    far_y1 = cam.y + sin(cam.a - cam.fovhalf) * cam.far
    
    far_x2 = cam.x + cos(cam.a + cam.fovhalf) * cam.far
    far_y2 = cam.y + sin(cam.a + cam.fovhalf) * cam.far
    
    near_x1 = cam.x + cos(cam.a - cam.fovhalf) * cam.near
    near_y1 = cam.y + sin(cam.a - cam.fovhalf) * cam.near
    
    near_x2 = cam.x + cos(cam.a + cam.fovhalf) * cam.near
    near_y2 = cam.y + sin(cam.a + cam.fovhalf) * cam.near

    for y=1,64 do
        sample_depth = y / 64
        
        start_x = (far_x1 - near_x1) / sample_depth + near_x1
        start_y = (far_y1 - near_y1) / sample_depth + near_y1
        end_x = (far_x2 - near_x2) / sample_depth + near_x2
        end_y = (far_y2 - near_y1) / sample_depth + near_y2
        
        --floor
        poke(0x5f38, 0)
        poke(0x5f39, 0)
        tline(0,y+64,128,y+64,
        start_x+origin.x,start_y+origin.y,
        (end_x-start_x)/128,(end_y-start_y)/128)
    
        --sky
        poke(0x5f38, 8)
        poke(0x5f39, 8)
        tline(0,64-y,128,64-y,
        start_x+8+sky_offset,start_y+8+sky_offset,
        (end_x-start_x)/128,(end_y-start_y)/128)
    end
    
    sky_offset = (sky_offset + sky_speed)%8
end

The relevant parts are the calculations of the "near" and "far" points, which define the view frustrum and thus the camera's perspective.

Now the part that is causing problems is the rendering of sprites in this pseudo 3D environment. It takes the (x,y) coordinates of the sprite (defining the 2D coordinates where the sprite is "standing on the floor") and looks like this:

theta = atan2(flag.x - cam.x, sprite.y - sprite.y)
cam_angle = (theta - cam.a + 0.5) % 1
    
spr_dist = dist(sprite.x,sprite.y,sprite.z,cam.x,cam.y,cam.far)
norm_depth = cos(cam_angle) * (spr_dist-cam.far) / (spr_dist+cam.dist)
    
draw_x = 64 - 64 * sin(cam_angle) / cam.fovhalf
draw_y = 128 - 64 * norm_depth

"cam_angle" describes the relative angle between the cameras direction and the sprite. This means it's 0 (or 1) when the camera is directly facing the sprite, and 0.5 when it is facing directly away. From debugging I am 99% sure that this angle is correct, so the fault seems to be in the rest.

I have been at this for a few days now and still it's not looking quite right, with both the on-screen height and width always being slightly off by a mysterious unknown factor. I know it's not a super serious problem, bu I would be very grateful if someone who has experience with camera projections and 3D rendering (and is better at math than me, welp) could help me out here. Thank you!

The main approach is still to position the sprite on this "virtual circle" around the camera via the sin() and cos() of the angle between the camera and the sprite. But the problem is that the raw sin() and cos() do not work because of the camera' skewed perspective. The rendering of the floor is scaled by distance (i.e. divided by the sample depth, more details in the linked video at around 15:00). I have tried to apply a similar method to the sprite positioning by calculating the "norm_depth", which should go towards 1 as the distance between the sprite and the camera approaches infinity, meaning the sprite is "on the horizon". Similarly, with a depth of 0 it should appear at the bottom of the screen.

This behavior of "norm_depth" is roughly working, being 0 at the bottom and 1 at the horizon, but in between it is off by a factor. I've tried accounting for the camera's positioning ("cam.far" is the virtual height of the camera in the world) and the frustrum, but none of my formulas have given the desired result.

0