diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/main.zig | 52 | ||||
| -rw-r--r-- | src/player.zig | 538 | ||||
| -rw-r--r-- | src/render.zig | 456 |
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; + } + } + } + }; +} |
