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.