From b1086c978996e522f133b4c6bdc59c95016eec0a Mon Sep 17 00:00:00 2001 From: tslil clingman <> Date: Sun, 5 Sep 2021 11:43:38 -0400 Subject: Sprite rendering skeleton! Except that i'm not satisfied. It would appear that we're computing various distances more-or-less exactly in the sprite rendering process, but this is at odds with the walls and floors which -- by virtue of the linear interpolation garbage -- don't agree about where things are. The net result is that there are at least two fudge factors: - to account to for the incorrect height of the floors i shift the sprites up by 0.2 - to account for the incorrect wall distances i add 1 to the z_buffer comparison These fudges are ugly and empirical. Still to do: for more than one object we have to sort by distance first! --- src/main.zig | 24 ++++++++--- src/map.zig | 14 +++++- src/raycast.zig | 132 +++++++++++++++++++++++++++++++++++++++++++++----------- 3 files changed, 138 insertions(+), 32 deletions(-) (limited to 'src') diff --git a/src/main.zig b/src/main.zig index 2d12975..8129ae2 100644 --- a/src/main.zig +++ b/src/main.zig @@ -28,8 +28,8 @@ usingnamespace @import("raycast.zig"); usingnamespace @import("renderConstants.zig"); usingnamespace @import("map.zig"); -// TODO: is there anyway i can say fn renderWall : RenderWallFunction? -fn renderWall( +// TODO: is there anyway i can say fn renderSlice : RenderSliceFunction? +fn renderSlice( window: sf.graphics.RenderWindow, sprite: sf.graphics.Sprite, col: i32, // which column @@ -77,13 +77,15 @@ pub fn main() !void { var y: u32 = 0; while (y < 32) : (y += 1) { map.cells.items[y * 32 + 00] = Cell{ .height = MAX_HEIGHT }; - map.cells.items[y * 32 + 31] = Cell{ .height = MAX_HEIGHT }; + map.cells.items[y * 32 + 31] = Cell{ .height = MAX_HEIGHT, .wall_texture = 2 }; map.cells.items[31 * 32 + y] = Cell{ .height = MAX_HEIGHT }; map.cells.items[00 * 32 + y] = Cell{ .height = MAX_HEIGHT }; } - map.cells.items[y * 15 + 15] = Cell{ .height = MAX_HEIGHT, .wall_texture = 1 }; - map.cells.items[y * 15 + 14] = Cell{ .height = MAX_HEIGHT / 3, .wall_texture = 1, .floor_texture = 1 }; + map.cells.items[32 * 15 + 15] = Cell{ .height = MAX_HEIGHT, .wall_texture = 1 }; + map.cells.items[32 * 15 + 14] = Cell{ .height = MAX_HEIGHT / 3, .wall_texture = 1, .floor_texture = 1 }; + + try map.objects.append(Object{.pos_x = 14.5, .pos_y = 16.5, .texture = 0, .height = 1.5, .width = 1}); // Initialise SFML //-------------------------------------------------------------------------- @@ -117,7 +119,15 @@ pub fn main() !void { var walls_sprite = try sf.Sprite.createFromTexture(wall_textures); defer walls_sprite.destroy(); - // Load floor textures + // Load sprite textures + var object_textures = try sf.Texture.createFromFile("objects.png"); + defer object_textures.destroy(); + object_textures.setSmooth(false); + + var objects_sprite = try sf.Sprite.createFromTexture(object_textures); + defer objects_sprite.destroy(); + + // Load floor image var floors_image = try sf.Image.createFromFile("floors.png"); defer floors_image.destroy(); @@ -187,7 +197,7 @@ pub fn main() !void { window.draw(rendered_floors_sprite, null); // Finally the world - player.renderWorld(window, walls_sprite, map, renderWall); + player.renderWorld(window, walls_sprite, objects_sprite, renderSlice, map); window.display(); diff --git a/src/map.zig b/src/map.zig index 8093e21..92ca5be 100644 --- a/src/map.zig +++ b/src/map.zig @@ -20,6 +20,14 @@ const std = @import("std"); // Let's pretend that our units are meters pub const MAX_HEIGHT: f32 = 2.5; +pub const Object = struct { + height : f32, + width : f32, + texture : u8, + pos_x : f32, + pos_y : f32, +}; + pub const Cell = struct { height: f32, wall_texture: u8 = 0, @@ -34,9 +42,11 @@ pub const Map = struct { height: u32, size: f32 = 64, cells: std.ArrayList(Cell), + objects: std.ArrayList(Object), pub fn deinit(self: Map) void { self.cells.deinit(); + self.objects.deinit(); } pub fn new(width: u32, height: u32, alloc: *std.mem.Allocator) !Map { @@ -44,7 +54,9 @@ pub const Map = struct { try cells.ensureTotalCapacity(width * height); try cells.appendNTimes(Cell.floor, width * height); - return Map{ .width = width, .height = height, .cells = cells }; + var objects = std.ArrayList(Object).init(alloc); + + return Map{ .width = width, .height = height, .cells = cells, .objects = objects }; } pub fn inBounds(self: Map, x: i32, y: i32) bool { diff --git a/src/raycast.zig b/src/raycast.zig index 3df8cef..20597e9 100644 --- a/src/raycast.zig +++ b/src/raycast.zig @@ -26,7 +26,7 @@ const Colour = @import("sfml").graphics.Color; usingnamespace @import("map.zig"); usingnamespace @import("renderConstants.zig"); -pub const RenderWallFunction: type = fn ( +pub const RenderSliceFunction: type = fn ( window: RenderWindow, sprite: Sprite, col: i32, // which column we're in @@ -48,7 +48,7 @@ pub fn Player(PlaneWidth: f32, PlaneHeight: f32) type { acc_y: f32 = 0, fov: f32 = std.math.pi / 3.0, height: f32 = 1.8, // TODO - // z_buffer: std.BoundedArray(f32, PlaneWidth * PlaneHeight), + z_buffer: std.BoundedArray(f32, PlanePixels), const FOV: f32 = std.math.pi / 3.0; const PlanePixels = PlaneWidth * PlaneHeight; @@ -58,13 +58,13 @@ pub fn Player(PlaneWidth: f32, PlaneHeight: f32) type { // 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; + 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), + .z_buffer = try std.BoundedArray(f32, PlanePixels).fromSlice(&infs), }; } @@ -89,23 +89,105 @@ pub fn Player(PlaneWidth: f32, PlaneHeight: f32) type { self: *@This(), window: RenderWindow, walls_sprite: Sprite, + objects_sprite: Sprite, + renderSlice: RenderSliceFunction, map: Map, - // the abstract the rendering call - renderWall: RenderWallFunction, ) void { - // var i: usize = 0; - // while (i < self.z_buffer.len) : (i += 1) { - // self.z_buffer.set(i, std.math.inf(f32)); - // } - self.renderWalls(window, walls_sprite, map, renderWall); + // 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; + const proj_centre = PlaneWidth / 2 + PlaneDist * centre / perp_distance; + + const width = PlaneDist * obj.width / perp_distance; + const left = proj_centre - width / 2; + + // TODO: prune before this? + if (left + width < 0 or left >= PlaneWidth) continue; + + // TODO: Here's another fudge factor. I think this is something + // like the floors and walls aren't actually distance correct, + // so we have to toy with these exact calculations to fix it. + const height = PlaneDist * obj.height / perp_distance; + const top = PlaneHeight / 2 + PlaneDist * (obj.height + 0.2 - self.height) / 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)); + // TODO: what is this fudge factor? Is this again + // because distances aren't actually correct in the + // z_buffer but they are in this computation? + if (self.z_buffer.get(index) < perp_distance + 1) { + 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, - renderWall: RenderWallFunction, ) void { // This is a TERRIBLE hack: for whatever reason *linearly* // interpolating on the direction vectors gives @@ -181,13 +263,15 @@ pub fn Player(PlaneWidth: f32, PlaneHeight: f32) type { var cell = map.lookup(ipos_x, ipos_y); // project the top of the wall - const top = PlaneHeight / 2 + PlaneDist * (cell.height - self.height) / distance; + 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; + if (top > PlaneHeight) { + still_drawing = false; + } // compute the height of this wall const total_length = PlaneDist * cell.height / distance; @@ -206,14 +290,14 @@ pub fn Player(PlaneWidth: f32, PlaneHeight: f32) type { if ((horizontal_hit and sinra < 0) or (!horizontal_hit and cosra > 0)) texfrac = 1 - texfrac; // draw the wall - renderWall(window, walls_sprite, col, top, total_length, draw_frac, texfrac, cell.wall_texture); + 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, top); - // while (y > @floatToInt(i32, highest_point)) : (y -= 1) { - // const index = @intCast(usize, col * @floatToInt(i32, PlaneHeight) + y); - // self.z_buffer.set(index, distance); - // } + 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; } @@ -226,10 +310,10 @@ pub fn Player(PlaneWidth: f32, PlaneHeight: f32) type { // 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); + 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) { -- cgit v1.3.1