aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/main.zig52
-rw-r--r--src/player.zig538
-rw-r--r--src/render.zig456
3 files changed, 548 insertions, 498 deletions
diff --git a/src/main.zig b/src/main.zig
index 3c425ba..c3647b4 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -25,7 +25,8 @@ const isKeyPressed = sf.window.keyboard.isKeyPressed;
const std = @import("std");
const constants = @import("constants.zig");
-const raycast = @import("player.zig");
+const player = @import("player.zig");
+const render = @import("render.zig");
const level = @import("level.zig");
pub fn main() !void {
@@ -34,7 +35,9 @@ pub fn main() !void {
// Setup player and map
//--------------------------------------------------------------------------
- var player = raycast.Player(constants.PlaneWidth, constants.PlaneHeight).new(
+ var renderer = render.Renderer(constants.PlaneWidth, constants.PlaneHeight).new();
+
+ var plyr = player.Player.new(
7, // x coord
1.5, // y coord
PI / 2.0, // angle
@@ -131,44 +134,44 @@ pub fn main() !void {
}
if (isKeyPressed(.Escape)) window.close();
- if (isKeyPressed(.C)) player.height -= 0.1;
- if (isKeyPressed(.V)) player.height += 0.1;
+ if (isKeyPressed(.C)) plyr.height -= 0.1;
+ if (isKeyPressed(.V)) plyr.height += 0.1;
const rotation = 2.0 * PI / 200.0;
- if (isKeyPressed(.Left)) player.ang += rotation;
- if (isKeyPressed(.Right)) player.ang -= rotation;
+ if (isKeyPressed(.Left)) plyr.ang += rotation;
+ if (isKeyPressed(.Right)) plyr.ang -= rotation;
- const cos = std.math.cos(player.ang);
- const sin = std.math.sin(player.ang);
+ const cos = std.math.cos(plyr.ang);
+ const sin = std.math.sin(plyr.ang);
- player.acc_x = 0;
- player.acc_y = 0;
+ plyr.acc_x = 0;
+ plyr.acc_y = 0;
const acc_mag = 30;
if (isKeyPressed(.R)) {
- player.acc_x -= sin;
- player.acc_y += cos;
+ plyr.acc_x -= sin;
+ plyr.acc_y += cos;
}
if (isKeyPressed(.T)) {
- player.acc_x += sin;
- player.acc_y -= cos;
+ plyr.acc_x += sin;
+ plyr.acc_y -= cos;
}
if (isKeyPressed(.Up) or isKeyPressed(.F)) {
- player.acc_x += cos;
- player.acc_y += sin;
+ plyr.acc_x += cos;
+ plyr.acc_y += sin;
}
if (isKeyPressed(.Down) or isKeyPressed(.S)) {
- player.acc_x -= cos;
- player.acc_y -= sin;
+ plyr.acc_x -= cos;
+ plyr.acc_y -= sin;
}
// normalise
- const a = std.math.sqrt(player.acc_x * player.acc_x + player.acc_y * player.acc_y);
+ const a = std.math.sqrt(plyr.acc_x * plyr.acc_x + plyr.acc_y * plyr.acc_y);
if (a > 0) {
- player.acc_x *= acc_mag / a;
- player.acc_y *= acc_mag / a;
+ plyr.acc_x *= acc_mag / a;
+ plyr.acc_y *= acc_mag / a;
}
// First the background
- const back_scroll = @floatToInt(i32, -constants.BackTextureWidth * player.ang / (2 * PI));
+ const back_scroll = @floatToInt(i32, -constants.BackTextureWidth * plyr.ang / (2 * PI));
back_sprite.setTextureRect(sf.IntRect{
.left = back_scroll, // fix location of texture in the sky
.top = 0,
@@ -179,7 +182,8 @@ pub fn main() !void {
});
window.draw(back_sprite, null);
- try player.renderWorld(
+ try renderer.renderWorld(
+ plyr,
window,
objects_image,
walls_image,
@@ -191,6 +195,6 @@ pub fn main() !void {
window.display();
- player.tick(map);
+ plyr.tick(map);
}
}
diff --git a/src/player.zig b/src/player.zig
index 7a94b7b..258b924 100644
--- a/src/player.zig
+++ b/src/player.zig
@@ -26,490 +26,80 @@ const Colour = @import("sfml").graphics.Color;
const level = @import("level.zig");
const constants = @import("constants.zig");
-fn playerDistComp(pos: [2]f32, lhs: level.Object, rhs: level.Object) bool {
- const lx = lhs.pos_x - pos[0];
- const ly = lhs.pos_y - pos[1];
- const rx = rhs.pos_x - pos[0];
- const ry = rhs.pos_y - pos[1];
+pub const Player = struct {
+ pos_x: f32,
+ pos_y: f32,
+ ang: f32,
+ vel_x: f32 = 0,
+ vel_y: f32 = 0,
+ acc_x: f32 = 0,
+ acc_y: f32 = 0,
+ height: f32 = 2.0 * (1.8 / 2.5), // TODO
+ anim_step: f32 = 0,
- return (lx * lx + ly * ly > rx * rx + ry * ry);
-}
+ // standing still at the given location, looking in direction ang
+ pub fn new(pos_x: f32, pos_y: f32, ang: f32) @This() {
+ return Player{
+ .pos_x = pos_x,
+ .pos_y = pos_y,
+ .ang = ang,
+ };
+ }
-fn fasterColourBlend(onto: Colour, from: Colour) Colour {
- const af: u16 = from.a;
- const of: u16 = onto.a;
+ pub fn tick(self: *Player, map: level.Map) void {
+ const dt = 1 / 30.0;
+ const v_min = 0.8;
+ const v_decay = 1 / 1.25;
- const ablend: u16 = @divTrunc(of * (255 - af), 255);
- const na: u16 = af + ablend;
- if (na == 0) return Colour.Black;
+ var next_x = self.pos_x + self.vel_x * dt;
+ var next_y = self.pos_y + self.vel_y * dt;
- const rf: u16 = from.r;
- const ro: u16 = onto.r;
- const gf: u16 = from.g;
- const go: u16 = onto.g;
- const bf: u16 = from.b;
- const bo: u16 = onto.b;
+ // Collision detection
+ const min_dist: f32 = 0.1;
+ const fx = std.math.floor(self.pos_x);
+ const fy = std.math.floor(self.pos_y);
+ const ix = @floatToInt(i32, fx);
+ const iy = @floatToInt(i32, fy);
+ const nix = @floatToInt(i32, std.math.floor(next_x + if (self.vel_x > 0) min_dist else -min_dist));
+ const niy = @floatToInt(i32, std.math.floor(next_y + if (self.vel_y > 0) min_dist else -min_dist));
- // The most accurate i've found is
- const nr = @divTrunc(ro * ablend + rf * af, na);
- const ng = @divTrunc(go * ablend + gf * af, na);
- const nb = @divTrunc(bo * ablend + bf * af, na);
-
- // These computations are incorrect, but faster
- // const nr = (af * rf + (255 - af) * ro) / 255;
- // const ng = (af * gf + (255 - af) * go) / 255;
- // const nb = (af * bf + (255 - af) * bo) / 255;
-
- return Colour{
- .a = @intCast(u8, na),
- .r = @intCast(u8, nr),
- .g = @intCast(u8, ng),
- .b = @intCast(u8, nb),
- };
-}
-
-pub fn Player(PlaneWidth: f32, PlaneHeight: f32) type {
- const FOV: f32 = std.math.pi / 3.0;
- const PlanePixels = PlaneWidth * PlaneHeight;
- // given the desired width of the image, how far away must
- // the projection plane be from the camera?
- const FOV_SCALE = 2 * std.math.tan(FOV / 2);
- const PlaneDist = PlaneWidth / FOV_SCALE;
-
- return struct {
- pos_x: f32,
- pos_y: f32,
- ang: f32,
- vel_x: f32 = 0,
- vel_y: f32 = 0,
- acc_x: f32 = 0,
- acc_y: f32 = 0,
- height: f32 = 2.0 * (1.8 / 2.5), // TODO
- z_buffer: [PlanePixels]f32,
- anim_step: f32 = 0,
-
- // standing still at the given location, looking in direction ang
- pub fn new(pos_x: f32, pos_y: f32, ang: f32) @This() {
- return Player(PlaneWidth, PlaneHeight){
- .pos_x = pos_x,
- .pos_y = pos_y,
- .ang = ang,
- .z_buffer = [_]f32{std.math.inf(f32)} ** PlanePixels,
- };
- }
-
- pub fn tick(self: *@This(), map: level.Map) void {
- const dt = 1 / 30.0;
- const v_min = 0.8;
- const v_decay = 1 / 1.25;
-
- var next_x = self.pos_x + self.vel_x * dt;
- var next_y = self.pos_y + self.vel_y * dt;
-
- // Collision detection
- const min_dist: f32 = 0.1;
- const fx = std.math.floor(self.pos_x);
- const fy = std.math.floor(self.pos_y);
- const ix = @floatToInt(i32, fx);
- const iy = @floatToInt(i32, fy);
- const nix = @floatToInt(i32, std.math.floor(next_x + if (self.vel_x > 0) min_dist else -min_dist));
- const niy = @floatToInt(i32, std.math.floor(next_y + if (self.vel_y > 0) min_dist else -min_dist));
-
- if (!map.inBounds(nix, iy) or
- blk: {
- const cell = map.lookup(nix, iy);
- if (cell.floor_height > 0) break :blk true;
- if (cell.ceiling_height < self.height) break :blk true;
- break :blk false;
- }) {
- next_x = fx + if (self.vel_x > 0) 1 - min_dist else min_dist;
- self.vel_x = 0;
- self.acc_x = 0;
- }
-
- if (!map.inBounds(ix, niy) or map.lookup(ix, niy).floor_height > 0) {
- next_y = fy + if (self.vel_y > 0) 1 - min_dist else min_dist;
- self.vel_y = 0;
- self.acc_y = 0;
- }
-
- // Update position
- self.pos_x = next_x;
- self.pos_y = next_y;
-
- // Update velocity
- self.vel_x += self.acc_x * dt;
- self.vel_y += self.acc_y * dt;
-
- const vd = v_decay * std.math.sqrt(self.vel_x * self.vel_x + self.vel_y * self.vel_y);
- if (vd < v_min) {
- self.vel_x = 0;
- self.vel_y = 0;
- } else {
- // If we're moving update our animation state
- self.anim_step += 1;
- self.height -= std.math.sin(self.anim_step / 10 * std.math.pi) * 0.015;
-
- self.vel_x *= v_decay;
- self.vel_y *= v_decay;
- }
+ if (!map.inBounds(nix, iy) or
+ blk: {
+ const cell = map.lookup(nix, iy);
+ if (cell.floor_height > 0) break :blk true;
+ if (cell.ceiling_height < self.height) break :blk true;
+ break :blk false;
+ }) {
+ next_x = fx + if (self.vel_x > 0) 1 - min_dist else min_dist;
+ self.vel_x = 0;
+ self.acc_x = 0;
}
- pub fn renderWorld(
- self: *@This(),
- window: RenderWindow,
- objects_image: Image,
- walls_image: Image,
- surfaces_image: Image,
- rendered_surfaces_texture: Texture,
- rendered_surfaces_sprite: Sprite,
- map: level.Map,
- ) !void {
- // Fist reset the z_buffer
- var i: usize = 0;
- while (i < self.z_buffer.len) : (i += 1) {
- self.z_buffer[i] = std.math.inf(f32);
- }
-
- var pixels = [_]Colour{Colour.Transparent} ** (PlaneWidth * PlaneHeight);
-
- // Draw all vertical and horizontal surfaces, and populate the z-buffer
- self.renderCells(walls_image, surfaces_image, map, &pixels);
-
- // use the z_buffer to render sprites
- self.renderObjects(objects_image, map, &pixels);
-
- try rendered_surfaces_texture.updateFromPixels(&pixels, null);
- window.draw(rendered_surfaces_sprite, null);
- }
-
- fn renderObjects(
- self: @This(),
- objects_image: Image,
- map: level.Map,
- pixels: []Colour,
- ) void {
- std.sort.sort(level.Object, map.objects.items,
- // Wow, context with an arbitrary type! No macros, just
- // Zig all the way down!
- [2]f32{ self.pos_x, self.pos_y }, playerDistComp);
-
- const self_cos = std.math.cos(self.ang);
- const self_sin = std.math.sin(self.ang);
-
- for (map.objects.items) |obj| {
- const ox = obj.pos_x - self.pos_x;
- const oy = obj.pos_y - self.pos_y;
-
- // We compute the two coordinates of rotating by -self.ang, the
- // first of which gives the perpendicular distance to the plane
- // of projection, and the second of which gives the
- // (unprojected) centre of the object.
- const perp_distance = self_cos * ox + self_sin * oy;
- const centre = self_sin * ox - self_cos * oy;
-
- // NOTE: in the below we have applied the magic scaling factor
- // of FOV_SCALE. I don't understand how this compensates for the
- // linear interpolation incorrectness we do elsewhere, but
- // somehow it scales the *correct* values we compute above into
- // whatever agrees with the wall and floor rendering voodoo.
-
- // This quantity is independent of FOV_SCALE because it enters
- // both via centre and perp_distance
- const proj_centre = PlaneWidth / 2 + PlaneDist * centre / perp_distance;
-
- // Here's the magic adjustment
- const scaled_perp_distance = FOV_SCALE * perp_distance;
- const width = PlaneDist * obj.width / scaled_perp_distance;
- const left = proj_centre - width / 2;
-
- // TODO: prune before this?
- if (left + width < 0 or left >= PlaneWidth) continue;
-
- const height = PlaneDist * obj.height / scaled_perp_distance;
- const top = PlaneHeight / 2 + PlaneDist * (obj.height - self.height + obj.pos_z) / scaled_perp_distance;
-
- // TODO: likewise?
- if (top < 0 or top - height >= PlaneHeight) continue;
-
- // Something is on the screen, let's draw it!
- const start = std.math.max(0, left);
- const end = @floatToInt(usize, std.math.min(left + width, PlaneWidth - 1));
-
- const inv_height = 1 / height;
-
- var tex_frac: f32 = std.math.clamp((start - left) / width, 0, 1);
- var col: usize = @floatToInt(usize, start);
- const tex_frac_step = 1 / width;
- while (col < end) : ({
- col += 1;
- tex_frac += tex_frac_step;
- }) {
- var bottom = std.math.min(top, PlaneHeight);
- var pix_y = @floatToInt(usize, std.math.ceil(std.math.max(PlaneHeight - bottom - 1, 0)));
- var texel_y = (top - bottom) / height;
- while (pix_y < PlaneHeight and bottom >= top - height) : ({
- bottom -= 1;
- pix_y += 1;
- texel_y += inv_height;
- }) {
- const index = col * @floatToInt(usize, PlaneHeight) + @floatToInt(usize, bottom);
- if (self.z_buffer[index] > scaled_perp_distance) {
- const tx = @floatToInt(c_uint, tex_frac * (constants.TextureDim - 1));
- const toff = obj.texture * @floatToInt(c_uint, constants.TextureDim);
- const ty = @floatToInt(c_uint, texel_y * (constants.TextureDim - 1));
- const texel = objects_image.getPixel(.{ .x = toff + tx, .y = ty });
- const pix_index = @floatToInt(usize, PlaneWidth) * pix_y + col;
- // TODO: Decide whether being accurate is as important as being fast
- pixels[pix_index] = fasterColourBlend(pixels[pix_index], texel);
- }
- }
- }
- }
+ if (!map.inBounds(ix, niy) or map.lookup(ix, niy).floor_height > 0) {
+ next_y = fy + if (self.vel_y > 0) 1 - min_dist else min_dist;
+ self.vel_y = 0;
+ self.acc_y = 0;
}
- fn renderCells(
- self: *@This(),
- walls_image: Image,
- surfaces_image: Image,
- map: level.Map,
- pixels: []Colour,
- ) void {
- // This is a TERRIBLE hack: for whatever reason *linearly*
- // interpolating on the direction vectors gives
- // perspective-correct-seeming walls!
- const cos_first = std.math.cos(self.ang + 0.5 * FOV);
- const cos_last = std.math.cos(self.ang - 0.5 * FOV);
- const sin_first = std.math.sin(self.ang + 0.5 * FOV);
- const sin_last = std.math.sin(self.ang - 0.5 * FOV);
-
- const cos_step = (cos_last - cos_first) / PlaneWidth;
- const sin_step = (sin_last - sin_first) / PlaneWidth;
-
- var col: u16 = 0;
- var cosra = cos_first;
- var sinra = sin_first;
- while (col < PlaneWidth) : ({
- col += 1;
- cosra += cos_step;
- sinra += sin_step;
- }) {
- // Observe that sqrt(1+tan^2) = abs(1/cos) sqrt(cos^2+sin^2) =
- // abs(1/cos). Similarly so for cot, hence we obtain the
- // following lengths for the hypotenuses assuming that x
- // (respectively y) are unit length and the angle is ra. This
- // for whatever reasons still works when we linearly interpolate
- // on cos and sin!
- const dy_for_x_step = std.math.fabs(1 / cosra);
- const dx_for_y_step = std.math.fabs(1 / sinra);
-
- var step_x: i32 = -1;
- var step_y: i32 = -1;
-
- var dist_x: f32 = undefined;
- var dist_y: f32 = undefined;
-
- var ipos_x: i32 = @floatToInt(i32, std.math.floor(self.pos_x));
- var ipos_y: i32 = @floatToInt(i32, std.math.floor(self.pos_y));
-
- // looking right
- if (cosra >= 0) {
- step_x = 1;
- // assuming unit size grid cells
- dist_y = (@intToFloat(f32, ipos_x) + 1 - self.pos_x) * dy_for_x_step;
- } else {
- dist_y = (self.pos_x - @intToFloat(f32, ipos_x)) * dy_for_x_step;
- }
-
- if (sinra >= 0) {
- step_y = 1;
- dist_x = (@intToFloat(f32, ipos_y) + 1 - self.pos_y) * dx_for_y_step;
- } else {
- dist_x = (self.pos_y - @intToFloat(f32, ipos_y)) * dx_for_y_step;
- }
-
- var top_of_floor: f32 = undefined;
- var bottom_of_ceiling: f32 = undefined;
- var distance: f32 = 0;
- var still_drawing = true;
- var highest_drawn: f32 = 0;
- var lowest_drawn: f32 = PlaneHeight - 1;
- var horizontal_hit: bool = undefined;
- while (still_drawing and map.inBounds(ipos_x, ipos_y)) : ({
- // Find the next cell on our path
- if (dist_y < dist_x) {
- horizontal_hit = false;
- distance = dist_y;
- dist_y += dy_for_x_step;
- ipos_x += step_x;
- } else {
- horizontal_hit = true;
- distance = dist_x;
- dist_x += dx_for_y_step;
- ipos_y += step_y;
- }
- }) {
- const cell = map.lookup(ipos_x, ipos_y);
-
- // Are we drawing vertical surfaces?
- if (cell.floor_height > 0 or cell.draw_down) {
- // project the top of the bottom and the bottom of the top
- top_of_floor = PlaneHeight / 2 + PlaneDist * (cell.floor_height - self.height) / distance;
- bottom_of_ceiling = PlaneHeight / 2 + PlaneDist * (cell.ceiling_height - self.height) / distance;
-
- const draw_lower = top_of_floor > highest_drawn;
- const draw_upper = cell.draw_down and bottom_of_ceiling < lowest_drawn;
-
- // Are we able to see any vertical faces?
- if (draw_upper or draw_lower) {
- // we need the distance to calculate the fractional
- // part of the relevant coordinate for texture
- // mapping of the walls
- const hit_coordinate = if (horizontal_hit) distance * cosra + self.pos_x else distance * sinra + self.pos_y;
- var texfrac = std.math.modf(hit_coordinate).fpart;
-
- // we also want to be sure that we're consistently orienting
- // textures, in this case clockwise
- if ((horizontal_hit and sinra < 0) or (!horizontal_hit and cosra > 0)) texfrac = 1 - texfrac;
- const texstrip = @floatToInt(c_uint, (constants.TextureDim - 1) * texfrac);
-
- // height of a unit-height wall at this distance
- const nominal_length = PlaneDist / distance;
- const inv_nom_len = distance / PlaneDist;
- // used for texel indexing
-
- const td = @floatToInt(c_uint, constants.TextureDim);
- if (draw_lower) {
- // which texture index?
- const t_lower_off = cell.lower_texture * td;
- // Note the bizarre rounding we have to do to avoid artifacts
- const constrained_top = std.math.min(std.math.floor(top_of_floor), std.math.ceil(lowest_drawn));
- const stop = @floatToInt(i32, highest_drawn);
- var zb_y = @floatToInt(i32, constrained_top);
- var pix_y = @floatToInt(usize, std.math.ceil(std.math.max(PlaneHeight - constrained_top, 0)));
- var texel_y = (top_of_floor - constrained_top) / nominal_length;
-
- // now we have what we need to draw the face,
- // and update the z-buffer
- while (zb_y > stop) : ({
- zb_y -= 1;
- pix_y += 1;
- texel_y += inv_nom_len;
- }) {
- const ty = @floatToInt(c_uint, std.math.modf(texel_y).fpart * constants.TextureDim);
- const texel = walls_image.getPixel(.{ .x = t_lower_off + texstrip, .y = ty });
-
- const pix_index = pix_y * @floatToInt(usize, PlaneWidth) + col;
- pixels[pix_index] = texel;
-
- const index = @intCast(usize, col * @floatToInt(i32, PlaneHeight) + zb_y);
- self.z_buffer[index] = distance;
- }
- highest_drawn = top_of_floor;
- }
-
- if (draw_upper) {
- const proj_default_end = PlaneHeight / 2 + PlaneDist * (level.Cell.DEFAULT_HEIGHT - self.height) / distance;
- const stop = @floatToInt(i32, std.math.min(lowest_drawn, proj_default_end));
- const t_upper_off = cell.upper_texture * td;
- const constrained_bottom = std.math.max(bottom_of_ceiling, highest_drawn);
- var zb_y = @floatToInt(i32, constrained_bottom);
- var pix_y = @floatToInt(usize, std.math.ceil(std.math.max(PlaneHeight - constrained_bottom - 1, 0)));
- var texel_y: f32 = 0;
- while (zb_y < stop) : ({
- zb_y += 1;
- pix_y -= 1;
- texel_y += inv_nom_len;
- }) {
- const ty = @floatToInt(c_uint, (1 - std.math.modf(texel_y).fpart) * (constants.TextureDim - 1));
- const texel = walls_image.getPixel(.{ .x = t_upper_off + texstrip, .y = ty });
-
- const pix_index = pix_y * @floatToInt(usize, PlaneWidth) + col;
- pixels[pix_index] = texel;
-
- const index = @intCast(usize, col * @floatToInt(i32, PlaneHeight) + zb_y);
- self.z_buffer[index] = distance;
- }
- lowest_drawn = bottom_of_ceiling;
- }
- }
- }
-
- // do we potentially draw floor for this cell?
- if (highest_drawn < PlaneHeight / 2 or (cell.draw_down and lowest_drawn > PlaneHeight / 2)) {
- if (dist_y < dist_x) {
- distance = dist_y;
- } else {
- distance = dist_x;
- }
-
- // Note: next_top can never exceed PlaneHeight / 2 in
- // the body of the next block. If the wall is taller
- // than us the back edge is lower than the front one so
- // this check will fail as we just drew it (or higher
- // than it). If the wall is shorter then the back edge
- // is at most the horizon. Similarly so for next_bottom
- const next_top = PlaneHeight / 2 + PlaneDist * (cell.floor_height - self.height) / distance;
- const next_bottom = PlaneHeight / 2 + PlaneDist * (cell.ceiling_height - self.height) / distance;
-
- // draw floor?
- if (next_top > highest_drawn) {
- top_of_floor = std.math.floor(highest_drawn);
- const thresh = std.math.min(std.math.min(next_top, lowest_drawn), PlaneHeight / 2) - 1;
- while (top_of_floor <= thresh) : (top_of_floor += 1) {
- const row_dist = (self.height - cell.floor_height) * PlaneDist / (PlaneHeight / 2 - top_of_floor);
- const ptop = @floatToInt(usize, top_of_floor);
- const itop = @floatToInt(usize, PlaneHeight) - ptop - 1;
-
- // draw the correct pixel
- const sx = std.math.modf(self.pos_x + row_dist * cosra);
- const sy = std.math.modf(self.pos_y + row_dist * sinra);
- const toff = cell.floor_texture * @floatToInt(c_uint, constants.TextureDim);
- const px = @floatToInt(c_uint, constants.TextureDim * std.math.fabs(sx.fpart));
- const py = @floatToInt(c_uint, constants.TextureDim * std.math.fabs(sy.fpart));
- const val = surfaces_image.getPixel(.{ .x = toff + px, .y = py });
- pixels[itop * @floatToInt(usize, PlaneWidth) + col] = val;
-
- // record in the z_buffer only if we're above the floor!
- if (cell.floor_height > 0) {
- const index = col * @floatToInt(usize, PlaneHeight) + ptop;
- self.z_buffer[index] = row_dist;
- }
- }
- highest_drawn = next_top;
- }
+ // Update position
+ self.pos_x = next_x;
+ self.pos_y = next_y;
- // draw ceiling?
- if (cell.draw_down and next_bottom < lowest_drawn) {
- bottom_of_ceiling = std.math.ceil(lowest_drawn);
- const thresh = std.math.max(std.math.max(next_bottom, highest_drawn), PlaneHeight / 2) - 1;
- while (bottom_of_ceiling > thresh) : (bottom_of_ceiling -= 1) {
- const row_dist = (cell.ceiling_height - self.height) * PlaneDist / (bottom_of_ceiling - PlaneHeight / 2);
- const ptop = @floatToInt(usize, bottom_of_ceiling);
- const itop = @floatToInt(usize, PlaneHeight) - ptop;
+ // Update velocity
+ self.vel_x += self.acc_x * dt;
+ self.vel_y += self.acc_y * dt;
- const sx = std.math.modf(self.pos_x + row_dist * cosra);
- const sy = std.math.modf(self.pos_y + row_dist * sinra);
- const toff = cell.ceiling_texture * @floatToInt(c_uint, constants.TextureDim);
- const px = @floatToInt(c_uint, constants.TextureDim * std.math.fabs(sx.fpart));
- const py = @floatToInt(c_uint, constants.TextureDim * std.math.fabs(sy.fpart));
- const val = surfaces_image.getPixel(.{ .x = toff + px, .y = py });
- pixels[itop * @floatToInt(usize, PlaneWidth) + col] = val;
+ const vd = v_decay * std.math.sqrt(self.vel_x * self.vel_x + self.vel_y * self.vel_y);
+ if (vd < v_min) {
+ self.vel_x = 0;
+ self.vel_y = 0;
+ } else {
+ // If we're moving update our animation state
+ self.anim_step += 1;
+ self.height -= std.math.sin(self.anim_step / 10 * std.math.pi) * 0.015;
- if (cell.draw_down) {
- const index = col * @floatToInt(usize, PlaneHeight) + ptop;
- self.z_buffer[index] = row_dist;
- }
- }
- lowest_drawn = next_bottom;
- }
- }
- }
- // Have we filled this column?
- if (top_of_floor > lowest_drawn or bottom_of_ceiling < highest_drawn) {
- still_drawing = false;
- }
- }
+ self.vel_x *= v_decay;
+ self.vel_y *= v_decay;
}
- };
-}
+ }
+};
diff --git a/src/render.zig b/src/render.zig
new file mode 100644
index 0000000..12af775
--- /dev/null
+++ b/src/render.zig
@@ -0,0 +1,456 @@
+// This file is part of ZiRC
+//
+// Copyright (C) 2021, tslil clingman
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+const std = @import("std");
+
+const RenderWindow = @import("sfml").graphics.RenderWindow;
+const Sprite = @import("sfml").graphics.Sprite;
+const Texture = @import("sfml").graphics.Texture;
+const Image = @import("sfml").graphics.Image;
+const Colour = @import("sfml").graphics.Color;
+
+const level = @import("level.zig");
+const constants = @import("constants.zig");
+const player = @import("player.zig");
+
+fn playerDistComp(pos: [2]f32, lhs: level.Object, rhs: level.Object) bool {
+ const lx = lhs.pos_x - pos[0];
+ const ly = lhs.pos_y - pos[1];
+ const rx = rhs.pos_x - pos[0];
+ const ry = rhs.pos_y - pos[1];
+
+ return (lx * lx + ly * ly > rx * rx + ry * ry);
+}
+
+fn fasterColourBlend(onto: Colour, from: Colour) Colour {
+ const af: u16 = from.a;
+ const of: u16 = onto.a;
+
+ const ablend: u16 = @divTrunc(of * (255 - af), 255);
+ const na: u16 = af + ablend;
+ if (na == 0) return Colour.Black;
+
+ const rf: u16 = from.r;
+ const ro: u16 = onto.r;
+ const gf: u16 = from.g;
+ const go: u16 = onto.g;
+ const bf: u16 = from.b;
+ const bo: u16 = onto.b;
+
+ // The most accurate i've found is
+ const nr = @divTrunc(ro * ablend + rf * af, na);
+ const ng = @divTrunc(go * ablend + gf * af, na);
+ const nb = @divTrunc(bo * ablend + bf * af, na);
+
+ // These computations are incorrect, but faster
+ // const nr = (af * rf + (255 - af) * ro) / 255;
+ // const ng = (af * gf + (255 - af) * go) / 255;
+ // const nb = (af * bf + (255 - af) * bo) / 255;
+
+ return Colour{
+ .a = @intCast(u8, na),
+ .r = @intCast(u8, nr),
+ .g = @intCast(u8, ng),
+ .b = @intCast(u8, nb),
+ };
+}
+
+pub fn Renderer(PlaneWidth: f32, PlaneHeight: f32) type {
+ const FOV: f32 = std.math.pi / 3.0;
+ const PlanePixels = PlaneWidth * PlaneHeight;
+ // given the desired width of the image, how far away must
+ // the projection plane be from the camera?
+ const FOV_SCALE = 2 * std.math.tan(FOV / 2);
+ const PlaneDist = PlaneWidth / FOV_SCALE;
+
+ return struct {
+ z_buffer: [PlanePixels]f32,
+
+ pub fn new() @This() {
+ return Renderer(PlaneWidth, PlaneHeight){
+ .z_buffer = [_]f32{std.math.inf(f32)} ** PlanePixels,
+ };
+ }
+
+ pub fn renderWorld(
+ self: *@This(),
+ plyr: player.Player,
+ window: RenderWindow,
+ objects_image: Image,
+ walls_image: Image,
+ surfaces_image: Image,
+ rendered_surfaces_texture: Texture,
+ rendered_surfaces_sprite: Sprite,
+ map: level.Map,
+ ) !void {
+ // Fist reset the z_buffer
+ var i: usize = 0;
+ while (i < self.z_buffer.len) : (i += 1) {
+ self.z_buffer[i] = std.math.inf(f32);
+ }
+
+ var pixels = [_]Colour{Colour.Transparent} ** (PlaneWidth * PlaneHeight);
+
+ // Draw all vertical and horizontal surfaces, and populate the z-buffer
+ self.renderCells(plyr, walls_image, surfaces_image, map, &pixels);
+
+ // use the z_buffer to render sprites
+ self.renderObjects(plyr, objects_image, map, &pixels);
+
+ try rendered_surfaces_texture.updateFromPixels(&pixels, null);
+ window.draw(rendered_surfaces_sprite, null);
+ }
+
+ fn renderObjects(
+ self: @This(),
+ plyr: player.Player,
+ objects_image: Image,
+ map: level.Map,
+ pixels: []Colour,
+ ) void {
+ const ppos_x = plyr.pos_x;
+ const ppos_y = plyr.pos_y;
+ const pheight = plyr.height;
+ const pcos = std.math.cos(plyr.ang);
+ const psin = std.math.sin(plyr.ang);
+
+ std.sort.sort(level.Object, map.objects.items,
+ // Wow, context with an arbitrary type! No macros, just
+ // Zig all the way down!
+ [2]f32{ ppos_x, ppos_y }, playerDistComp);
+
+ for (map.objects.items) |obj| {
+ const ox = obj.pos_x - ppos_x;
+ const oy = obj.pos_y - ppos_y;
+
+ // We compute the two coordinates of rotating by -self.ang, the
+ // first of which gives the perpendicular distance to the plane
+ // of projection, and the second of which gives the
+ // (unprojected) centre of the object.
+ const perp_distance = pcos * ox + psin * oy;
+ const centre = psin * ox - pcos * oy;
+
+ // NOTE: in the below we have applied the magic scaling factor
+ // of FOV_SCALE. I don't understand how this compensates for the
+ // linear interpolation incorrectness we do elsewhere, but
+ // somehow it scales the *correct* values we compute above into
+ // whatever agrees with the wall and floor rendering voodoo.
+
+ // This quantity is independent of FOV_SCALE because it enters
+ // both via centre and perp_distance
+ const proj_centre = PlaneWidth / 2 + PlaneDist * centre / perp_distance;
+
+ // Here's the magic adjustment
+ const scaled_perp_distance = FOV_SCALE * perp_distance;
+ const width = PlaneDist * obj.width / scaled_perp_distance;
+ const left = proj_centre - width / 2;
+
+ // TODO: prune before this?
+ if (left + width < 0 or left >= PlaneWidth) continue;
+
+ const height = PlaneDist * obj.height / scaled_perp_distance;
+ const top = PlaneHeight / 2 + PlaneDist * (obj.height - pheight + obj.pos_z) / scaled_perp_distance;
+
+ // TODO: likewise?
+ if (top < 0 or top - height >= PlaneHeight) continue;
+
+ // Something is on the screen, let's draw it!
+ const start = std.math.max(0, left);
+ const end = @floatToInt(usize, std.math.min(left + width, PlaneWidth - 1));
+
+ const inv_height = 1 / height;
+
+ var tex_frac: f32 = std.math.clamp((start - left) / width, 0, 1);
+ var col: usize = @floatToInt(usize, start);
+ const tex_frac_step = 1 / width;
+ while (col < end) : ({
+ col += 1;
+ tex_frac += tex_frac_step;
+ }) {
+ var bottom = std.math.min(top, PlaneHeight);
+ var pix_y = @floatToInt(usize, std.math.ceil(std.math.max(PlaneHeight - bottom - 1, 0)));
+ var texel_y = (top - bottom) / height;
+ while (pix_y < PlaneHeight and bottom >= top - height) : ({
+ bottom -= 1;
+ pix_y += 1;
+ texel_y += inv_height;
+ }) {
+ const index = col * @floatToInt(usize, PlaneHeight) + @floatToInt(usize, bottom);
+ if (self.z_buffer[index] > scaled_perp_distance) {
+ const tx = @floatToInt(c_uint, tex_frac * constants.TextureDim);
+ const toff = obj.texture * @floatToInt(c_uint, constants.TextureDim);
+ const ty = @floatToInt(c_uint, texel_y * constants.TextureDim);
+ const texel = objects_image.getPixel(.{ .x = toff + tx, .y = ty });
+ const pix_index = @floatToInt(usize, PlaneWidth) * pix_y + col;
+ // TODO: Decide whether being accurate is as important as being fast
+ pixels[pix_index] = fasterColourBlend(pixels[pix_index], texel);
+ }
+ }
+ }
+ }
+ }
+
+ fn renderCells(
+ self: *@This(),
+ plyr: player.Player,
+ walls_image: Image,
+ surfaces_image: Image,
+ map: level.Map,
+ pixels: []Colour,
+ ) void {
+ // This is a TERRIBLE hack: for whatever reason *linearly*
+ // interpolating on the direction vectors gives
+ // perspective-correct-seeming walls!
+ const cos_first = std.math.cos(plyr.ang + 0.5 * FOV);
+ const cos_last = std.math.cos(plyr.ang - 0.5 * FOV);
+ const sin_first = std.math.sin(plyr.ang + 0.5 * FOV);
+ const sin_last = std.math.sin(plyr.ang - 0.5 * FOV);
+
+ const cos_step = (cos_last - cos_first) / PlaneWidth;
+ const sin_step = (sin_last - sin_first) / PlaneWidth;
+
+ const ppos_x = plyr.pos_x;
+ const ppos_y = plyr.pos_y;
+ const pheight = plyr.height;
+
+ var col: u16 = 0;
+ var cosra = cos_first;
+ var sinra = sin_first;
+ while (col < PlaneWidth) : ({
+ col += 1;
+ cosra += cos_step;
+ sinra += sin_step;
+ }) {
+ // Observe that sqrt(1+tan^2) = abs(1/cos) sqrt(cos^2+sin^2) =
+ // abs(1/cos). Similarly so for cot, hence we obtain the
+ // following lengths for the hypotenuses assuming that x
+ // (respectively y) are unit length and the angle is ra. This
+ // for whatever reasons still works when we linearly interpolate
+ // on cos and sin!
+ const dy_for_x_step = std.math.fabs(1 / cosra);
+ const dx_for_y_step = std.math.fabs(1 / sinra);
+
+ var step_x: i32 = -1;
+ var step_y: i32 = -1;
+
+ var dist_x: f32 = undefined;
+ var dist_y: f32 = undefined;
+
+ var ipos_x: i32 = @floatToInt(i32, std.math.floor(ppos_x));
+ var ipos_y: i32 = @floatToInt(i32, std.math.floor(ppos_y));
+
+ // looking right
+ if (cosra >= 0) {
+ step_x = 1;
+ // assuming unit size grid cells
+ dist_y = (@intToFloat(f32, ipos_x) + 1 - ppos_x) * dy_for_x_step;
+ } else {
+ dist_y = (ppos_x - @intToFloat(f32, ipos_x)) * dy_for_x_step;
+ }
+
+ if (sinra >= 0) {
+ step_y = 1;
+ dist_x = (@intToFloat(f32, ipos_y) + 1 - ppos_y) * dx_for_y_step;
+ } else {
+ dist_x = (ppos_y - @intToFloat(f32, ipos_y)) * dx_for_y_step;
+ }
+
+ var top_of_floor: f32 = undefined;
+ var bottom_of_ceiling: f32 = undefined;
+ var distance: f32 = 0;
+ var still_drawing = true;
+ var highest_drawn: f32 = 0;
+ var lowest_drawn: f32 = PlaneHeight - 1;
+ var horizontal_hit: bool = undefined;
+ while (still_drawing and map.inBounds(ipos_x, ipos_y)) : ({
+ // Find the next cell on our path
+ if (dist_y < dist_x) {
+ horizontal_hit = false;
+ distance = dist_y;
+ dist_y += dy_for_x_step;
+ ipos_x += step_x;
+ } else {
+ horizontal_hit = true;
+ distance = dist_x;
+ dist_x += dx_for_y_step;
+ ipos_y += step_y;
+ }
+ }) {
+ const cell = map.lookup(ipos_x, ipos_y);
+
+ // Are we drawing vertical surfaces?
+ if (cell.floor_height > 0 or cell.draw_down) {
+ // project the top of the bottom and the bottom of the top
+ top_of_floor = PlaneHeight / 2 + PlaneDist * (cell.floor_height - pheight) / distance;
+ bottom_of_ceiling = PlaneHeight / 2 + PlaneDist * (cell.ceiling_height - pheight) / distance;
+
+ const draw_lower = top_of_floor > highest_drawn;
+ const draw_upper = cell.draw_down and bottom_of_ceiling < lowest_drawn;
+
+ // Are we able to see any vertical faces?
+ if (draw_upper or draw_lower) {
+ // we need the distance to calculate the fractional
+ // part of the relevant coordinate for texture
+ // mapping of the walls
+ const hit_coordinate = if (horizontal_hit) distance * cosra + ppos_x else distance * sinra + ppos_y;
+ var texfrac = std.math.modf(hit_coordinate).fpart;
+
+ // we also want to be sure that we're consistently orienting
+ // textures, in this case clockwise
+ if ((horizontal_hit and sinra < 0) or (!horizontal_hit and cosra > 0)) texfrac = 1 - texfrac;
+ const texstrip = @floatToInt(c_uint, (constants.TextureDim - 1) * texfrac);
+
+ // height of a unit-height wall at this distance
+ const nominal_length = PlaneDist / distance;
+ const inv_nom_len = distance / PlaneDist;
+ // used for texel indexing
+
+ const td = @floatToInt(c_uint, constants.TextureDim);
+ if (draw_lower) {
+ // which texture index?
+ const t_lower_off = cell.lower_texture * td;
+ // Note the bizarre rounding we have to do to avoid artifacts
+ const constrained_top = std.math.min(std.math.floor(top_of_floor), std.math.ceil(lowest_drawn));
+ const stop = @floatToInt(i32, highest_drawn);
+ var zb_y = @floatToInt(i32, constrained_top);
+ var pix_y = @floatToInt(usize, std.math.ceil(std.math.max(PlaneHeight - constrained_top - 1, 0)));
+ var texel_y = (top_of_floor - constrained_top) / nominal_length;
+
+ // now we have what we need to draw the face,
+ // and update the z-buffer
+ while (zb_y > stop) : ({
+ zb_y -= 1;
+ pix_y += 1;
+ texel_y += inv_nom_len;
+ }) {
+ const ty = @floatToInt(c_uint, std.math.modf(texel_y).fpart * constants.TextureDim);
+ const texel = walls_image.getPixel(.{ .x = t_lower_off + texstrip, .y = ty });
+
+ const pix_index = pix_y * @floatToInt(usize, PlaneWidth) + col;
+ pixels[pix_index] = texel;
+
+ const index = @intCast(usize, col * @floatToInt(i32, PlaneHeight) + zb_y);
+ self.z_buffer[index] = distance;
+ }
+ highest_drawn = top_of_floor;
+ }
+
+ if (draw_upper) {
+ const proj_default_end = PlaneHeight / 2 + PlaneDist * (level.Cell.DEFAULT_HEIGHT - pheight) / distance;
+ const stop = @floatToInt(i32, std.math.min(lowest_drawn, proj_default_end));
+ const t_upper_off = cell.upper_texture * td;
+ const constrained_bottom = std.math.max(bottom_of_ceiling, highest_drawn);
+ var zb_y = @floatToInt(i32, constrained_bottom);
+ var pix_y = @floatToInt(usize, std.math.ceil(std.math.max(PlaneHeight - constrained_bottom - 1, 0)));
+ var texel_y: f32 = 0;
+ while (zb_y < stop) : ({
+ zb_y += 1;
+ pix_y -= 1;
+ texel_y += inv_nom_len;
+ }) {
+ const ty = @floatToInt(c_uint, (1 - std.math.modf(texel_y).fpart) * (constants.TextureDim - 1));
+ const texel = walls_image.getPixel(.{ .x = t_upper_off + texstrip, .y = ty });
+
+ const pix_index = pix_y * @floatToInt(usize, PlaneWidth) + col;
+ pixels[pix_index] = texel;
+
+ const index = @intCast(usize, col * @floatToInt(i32, PlaneHeight) + zb_y);
+ self.z_buffer[index] = distance;
+ }
+ lowest_drawn = bottom_of_ceiling;
+ }
+ }
+ }
+
+ // do we potentially draw floor for this cell?
+ if (highest_drawn < PlaneHeight / 2 or (cell.draw_down and lowest_drawn > PlaneHeight / 2)) {
+ if (dist_y < dist_x) {
+ distance = dist_y;
+ } else {
+ distance = dist_x;
+ }
+
+ // Note: next_top can never exceed PlaneHeight / 2 in
+ // the body of the next block. If the wall is taller
+ // than us the back edge is lower than the front one so
+ // this check will fail as we just drew it (or higher
+ // than it). If the wall is shorter then the back edge
+ // is at most the horizon. Similarly so for next_bottom
+ const next_top = PlaneHeight / 2 + PlaneDist * (cell.floor_height - pheight) / distance;
+ const next_bottom = PlaneHeight / 2 + PlaneDist * (cell.ceiling_height - pheight) / distance;
+
+ // draw floor?
+ if (next_top > highest_drawn) {
+ top_of_floor = std.math.ceil(highest_drawn);
+ const thresh = std.math.min(std.math.min(next_top, lowest_drawn), PlaneHeight / 2 - 1);
+ while (top_of_floor < thresh) : (top_of_floor += 1) {
+ const row_dist = (pheight - cell.floor_height) * PlaneDist / (PlaneHeight / 2 - top_of_floor);
+ const ptop = @floatToInt(usize, top_of_floor);
+ const itop = @floatToInt(usize, PlaneHeight) - ptop - 1;
+
+ // draw the correct pixel
+ const sx = std.math.modf(ppos_x + row_dist * cosra);
+ const sy = std.math.modf(ppos_y + row_dist * sinra);
+ const toff = cell.floor_texture * @floatToInt(c_uint, constants.TextureDim);
+ const px = @floatToInt(c_uint, constants.TextureDim * std.math.fabs(sx.fpart));
+ const py = @floatToInt(c_uint, constants.TextureDim * std.math.fabs(sy.fpart));
+ const val = surfaces_image.getPixel(.{ .x = toff + px, .y = py });
+ pixels[itop * @floatToInt(usize, PlaneWidth) + col] = val;
+
+ // record in the z_buffer only if we're above the floor!
+ if (cell.floor_height > 0) {
+ const index = col * @floatToInt(usize, PlaneHeight) + ptop;
+ self.z_buffer[index] = row_dist;
+ }
+ }
+ highest_drawn = next_top;
+ }
+
+ // draw ceiling?
+ if (cell.draw_down and next_bottom < lowest_drawn) {
+ bottom_of_ceiling = std.math.ceil(lowest_drawn);
+ const thresh = std.math.max(std.math.max(next_bottom, highest_drawn), PlaneHeight / 2 - 1);
+ while (bottom_of_ceiling > thresh) : (bottom_of_ceiling -= 1) {
+ const row_dist = (cell.ceiling_height - pheight) * PlaneDist / (bottom_of_ceiling - PlaneHeight / 2);
+ const ptop = @floatToInt(usize, bottom_of_ceiling);
+ const itop = @floatToInt(usize, PlaneHeight) - ptop;
+
+ const sx = std.math.modf(ppos_x + row_dist * cosra);
+ const sy = std.math.modf(ppos_y + row_dist * sinra);
+ const toff = cell.ceiling_texture * @floatToInt(c_uint, constants.TextureDim);
+ const px = @floatToInt(c_uint, constants.TextureDim * std.math.fabs(sx.fpart));
+ const py = @floatToInt(c_uint, constants.TextureDim * std.math.fabs(sy.fpart));
+ const val = surfaces_image.getPixel(.{ .x = toff + px, .y = py });
+ pixels[itop * @floatToInt(usize, PlaneWidth) + col] = val;
+
+ if (cell.draw_down) {
+ const index = col * @floatToInt(usize, PlaneHeight) + ptop;
+ self.z_buffer[index] = row_dist;
+ }
+ }
+ lowest_drawn = next_bottom;
+ }
+ }
+ }
+ // Have we filled this column?
+ if (top_of_floor > lowest_drawn or bottom_of_ceiling < highest_drawn) {
+ still_drawing = false;
+ }
+ }
+ }
+ };
+}