// 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 . 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; usingnamespace @import("map.zig"); usingnamespace @import("renderConstants.zig"); pub const RenderSliceFunction: type = fn ( window: RenderWindow, sprite: Sprite, col: i32, // which column we're in top: f32, // top of wall draw_frac: f32, length: f32, // length of slice to be drawn texfrac: f32, texture: u8, // which texture index ) void; pub fn Player(PlaneWidth: f32, PlaneHeight: f32) type { 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, fov: f32 = std.math.pi / 3.0, height: f32 = 1.8, // TODO z_buffer: std.BoundedArray(f32, PlanePixels), 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; // standing still at the given location, looking in direction ang, pub fn new(pos_x: f32, pos_y: f32, ang: f32) !@This() { const infs = [_]f32{std.math.inf(f32)} ** PlanePixels; return Player(PlaneWidth, PlaneHeight){ .pos_x = pos_x, .pos_y = pos_y, .ang = ang, // TODO: is there some clever way to avoid this long name? .z_buffer = try std.BoundedArray(f32, PlanePixels).fromSlice(&infs), }; } pub fn tick(self: *@This()) void { const dt = 1 / 30.0; const v_min = 0.8; const v_decay = 1.25; self.pos_x += self.vel_x * dt; self.pos_y += self.vel_y * dt; self.vel_x /= v_decay; if (std.math.fabs(self.vel_x) < v_min) self.vel_x = 0; self.vel_y /= v_decay; if (std.math.fabs(self.vel_y) < v_min) self.vel_y = 0; self.vel_x += self.acc_x * dt; self.vel_y += self.acc_y * dt; } pub fn renderWorld( self: *@This(), window: RenderWindow, walls_sprite: Sprite, objects_sprite: Sprite, renderSlice: RenderSliceFunction, map: Map, ) void { // reset the z_buffer var i: usize = 0; while (i < self.z_buffer.len) : (i += 1) { self.z_buffer.set(i, std.math.inf(f32)); } // then render all the walls and populate the z_buffer self.renderWalls(window, walls_sprite, renderSlice, map); // use the z_buffer to render sprites self.renderObjects(window, objects_sprite, renderSlice, map); } fn playerDistComp(self: @This(), lhs: Object, rhs: Object) bool { const lx = lhs.pos_x - self.pos_x; const ly = lhs.pos_y - self.pos_y; const rx = rhs.pos_x - self.pos_x; const ry = rhs.pos_y - self.pos_y; return (lx * lx + ly * ly < rx * rx + ry * ry); } // Assumes objects are sorted by proximity! fn renderObjects( self: @This(), window: RenderWindow, objects_sprite: Sprite, renderSlice: RenderSliceFunction, map: Map, ) void { //std.sort.sort(Object, map.objects.items, {}, self.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) / 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(i32, std.math.min(left + width, PlaneWidth)); var tex_frac: f32 = (start - left) / width; var col: i32 = @floatToInt(i32, start); const tex_frac_step = 1 / width; while (col < end) : ({ col += 1; tex_frac += tex_frac_step; }) { var bottom = std.math.min(top, PlaneHeight); while (bottom >= 0 and bottom >= top - height) : (bottom -= 1) { const index = @intCast(usize, col * @floatToInt(i32, PlaneHeight) + @floatToInt(i32, bottom)); if (self.z_buffer.get(index) < scaled_perp_distance) { bottom += 1; break; } } const draw_frac = std.math.clamp((top - bottom) / height, 0 , 1); renderSlice(window, objects_sprite, col, top, height, draw_frac, tex_frac, obj.texture); } } } fn renderWalls( self: *@This(), window: RenderWindow, walls_sprite: Sprite, renderSlice: RenderSliceFunction, map: Map, ) 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: i32 = 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 distance: f32 = 0; var still_drawing = true; var highest_point: f32 = 0; 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; } }) { var cell = map.lookup(ipos_x, ipos_y); // project the top of the wall var top = PlaneHeight / 2 + PlaneDist * (cell.height - self.height) / distance; // We have a wall to draw if it protrudes above what we have so far drawn if (top > highest_point) { // did we extend beyond the top of the plane? if (top > PlaneHeight) { still_drawing = false; } // compute the height of this wall const total_length = PlaneDist * cell.height / distance; // as well as the fraction we'll be drawing const draw_length = top - highest_point; const draw_frac = std.math.clamp(draw_length / total_length, 0, 1); // we need the raw Euclidean 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; // draw the wall renderSlice(window, walls_sprite, col, top, total_length, draw_frac, texfrac, cell.wall_texture); // record that there's a wall here in the z_buffer var y = @floatToInt(i32, std.math.min(top, PlaneHeight - 1)); while (y > @floatToInt(i32, highest_point)) : (y -= 1) { const index = @intCast(usize, col * @floatToInt(i32, PlaneHeight) + y); self.z_buffer.set(index, distance); } highest_point = top; } } } } pub fn renderFloorsToTexture(self: @This(), floors_image: Image, rendered_floors_texture: Texture, map: Map) !void { var pixels = [_]Colour{Colour.Black} ** (PlaneWidth * PlaneHeight / 2); // Again, another TERRIBLE hack: we do the same nasty linear // interpolation trick and for whatever reason the floors look fine. const cos_first = std.math.cos(self.ang + 0.5 * FOV); const sin_first = std.math.sin(self.ang + 0.5 * FOV); const cos_last = std.math.cos(self.ang - 0.5 * FOV); const sin_last = std.math.sin(self.ang - 0.5 * FOV); var row: usize = 0; while (row < PlaneHeight / 2) : (row += 1) { const row_dist = self.height * PlaneDist / @intToFloat(f32, row + 1); const dx_step = row_dist * (cos_last - cos_first) / PlaneWidth; const dy_step = row_dist * (sin_last - sin_first) / PlaneWidth; var col: usize = 0; var dx = row_dist * cos_first; var dy = row_dist * sin_first; while (col < PlaneWidth) : ({ col += 1; dx += dx_step; dy += dy_step; }) { const x = self.pos_x + dx; const y = self.pos_y + dy; const sx = std.math.modf(x); const sy = std.math.modf(y); const ix = @floatToInt(i32, sx.ipart); const iy = @floatToInt(i32, sy.ipart); if (map.inBounds(ix, iy)) { const tex = @as(c_uint, map.lookup(ix, iy).floor_texture); const toff = tex * @floatToInt(c_uint, 1 + TextureDim); const px = @floatToInt(c_uint, TextureDim * std.math.fabs(sx.fpart)); const py = @floatToInt(c_uint, TextureDim * std.math.fabs(sy.fpart)); const val = floors_image.getPixel(.{ .x = toff + px, .y = py }); pixels[row * @floatToInt(usize, PlaneWidth) + col] = val; } } } try rendered_floors_texture.updateFromPixels(&pixels, null); } }; }