From d4f2376bb0ea219b79dac3f4f37b77342ca6ef4a Mon Sep 17 00:00:00 2001 From: tslil clingman <> Date: Thu, 7 Oct 2021 11:25:09 -0400 Subject: Variable height ceilings! Plus bonus graphical artefacts around the edges of such! In the end the majority of this code was rather easy: take the upwards floor drawing code and reflect it. However, there were many edge-cases and plenty of off-by-one pixels and rounding issues that didn't quite work the same way going down as they do going up. Also the object drawing code used to take advantage of the fact that the z-buffer was painted bottom-up only, so that required change too. --- README.md | 9 +- screenshot.png | Bin 42650 -> 97917 bytes src/constants.zig | 2 - src/level.zig | 11 ++- src/main.zig | 36 +++++--- src/player.zig | 239 ++++++++++++++++++++++++++++-------------------------- walls.png | Bin 568 -> 677 bytes 7 files changed, 162 insertions(+), 135 deletions(-) diff --git a/README.md b/README.md index fff984c..25bd71f 100644 --- a/README.md +++ b/README.md @@ -18,10 +18,13 @@ In particular alpha blending is implemented in software. This was the result of - [X] Static sprite objects - [X] Skyclinder* - [X] No overdraw (without sprites, skyclinder) -- [ ] Variable height ceilings -- [ ] Walls extending from ceiling instead of floor +- [X] Variable height ceilings +- [X] Walls extending from ceiling instead of floor - [ ] Sprites look different from different directions - [ ] Animated textures +- [ ] Fluid-like floors +- [ ] Procedurally generated textures + *It's topologically a cylinder, not a box; hardware rendered for now @@ -37,4 +40,4 @@ The default keybinds are FRST for Forward, Left Strafe, Backward, Right Strafe a The code is GPLv3+, the artwork is CC-BY-SA 4.0 -I make use of the zig-sfml library, which is Copyright (C) 2021 Guigui220D under the terms of the MIT license. For details see `src/sfml/LICENCE` \ No newline at end of file +I make use of the zig-sfml library, which is Copyright (C) 2021 Guigui220D under the terms of the MIT license. For details see `src/sfml/LICENCE` diff --git a/screenshot.png b/screenshot.png index c5c6444..6026f7f 100644 Binary files a/screenshot.png and b/screenshot.png differ diff --git a/src/constants.zig b/src/constants.zig index d363af5..450155f 100644 --- a/src/constants.zig +++ b/src/constants.zig @@ -28,5 +28,3 @@ pub const BackTextureWidth: f32 = 1280; pub const BackTextureHeight: f32 = 240; pub const TextureDim: f32 = 32.0; - -pub const MAX_HEIGHT: f32 = 2; diff --git a/src/level.zig b/src/level.zig index 1d61a02..a5d7330 100644 --- a/src/level.zig +++ b/src/level.zig @@ -27,12 +27,17 @@ pub const Object = struct { }; pub const Cell = struct { - height: f32, - wall_texture: u8 = 0, + floor_height: f32 = 0, + ceiling_height : f32 = DEFAULT_HEIGHT, + draw_down: bool = false, + lower_texture: u8 = 0, + upper_texture: u8 = 0, floor_texture: u8 = 0, ceiling_texture: u8 = 2, - const floor = Cell{ .height = 0 }; + pub const floor = Cell{ }; + + pub const DEFAULT_HEIGHT : f32 = 4; }; pub const Map = struct { diff --git a/src/main.zig b/src/main.zig index 5754793..d76e424 100644 --- a/src/main.zig +++ b/src/main.zig @@ -48,18 +48,34 @@ pub fn main() !void { var y: u32 = 0; while (y < 16) : (y += 1) { - map.cells.items[y * 16 + 00] = level.Cell{ .height = constants.MAX_HEIGHT }; - map.cells.items[y * 16 + 15] = level.Cell{ .height = constants.MAX_HEIGHT, .wall_texture = 2 }; - map.cells.items[15 * 16 + y] = level.Cell{ .height = constants.MAX_HEIGHT }; - map.cells.items[00 * 16 + y] = level.Cell{ .height = constants.MAX_HEIGHT }; + map.cells.items[y * 16 + 00] = level.Cell{ .floor_height = level.Cell.DEFAULT_HEIGHT }; + map.cells.items[y * 16 + 15] = level.Cell{ .floor_height = level.Cell.DEFAULT_HEIGHT, .lower_texture = 2 }; + map.cells.items[15 * 16 + y] = level.Cell{ .floor_height = level.Cell.DEFAULT_HEIGHT }; + map.cells.items[00 * 16 + y] = level.Cell{ .floor_height = level.Cell.DEFAULT_HEIGHT }; + + if (y < 15) { + map.cells.items[y * 16 + 01] = level.Cell{ + .ceiling_height = 3, + .upper_texture = 3, + .draw_down = true, + }; + } } - map.cells.items[16 * 7 + 7] = level.Cell{ .height = constants.MAX_HEIGHT, .wall_texture = 1 }; - map.cells.items[16 * 7 + 6] = level.Cell{ .height = constants.MAX_HEIGHT / 4, .wall_texture = 1, .floor_texture = 1 }; - - try map.objects.append(level.Object{ .pos_x = 5.5, .pos_y = 7.5, .pos_z = 1, .texture = 2, .height = 1, .width = 1 }); - try map.objects.append(level.Object{ .pos_x = 4, .pos_y = 8.5, .texture = 1, .height = 1, .width = 1 }); - try map.objects.append(level.Object{ .pos_x = 6.5, .pos_y = 8.5, .texture = 0, .height = 1, .width = 1 }); + map.cells.items[16 * 7 + 7] = level.Cell{ .floor_height = level.Cell.DEFAULT_HEIGHT, .lower_texture = 1 }; + map.cells.items[16 * 7 + 6] = level.Cell{ + .floor_height = 0.5, + .ceiling_height = 2, + .lower_texture = 1, + .upper_texture = 1, + .floor_texture = 1, + .ceiling_texture = 1, + .draw_down = true, + }; + + try map.objects.append(level.Object{ .pos_x = 1.5, .pos_y = 7.5, .pos_z = 2, .texture = 2, .height = 1, .width = 1 }); + try map.objects.append(level.Object{ .pos_x = 4, .pos_y = 8.5, .texture = 0, .height = 1, .width = 1 }); + try map.objects.append(level.Object{ .pos_x = 6, .pos_y = 8.5, .texture = 1, .height = 1, .width = 1 }); // Initialise SFML //-------------------------------------------------------------------------- diff --git a/src/player.zig b/src/player.zig index f6b82b1..c8fc4df 100644 --- a/src/player.zig +++ b/src/player.zig @@ -115,13 +115,19 @@ pub fn Player(PlaneWidth: f32, PlaneHeight: f32) type { 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 map.lookup(nix, iy).height > 0) { + 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).height > 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; @@ -167,13 +173,9 @@ pub fn Player(PlaneWidth: f32, PlaneHeight: f32) type { var pixels = [_]Colour{Colour.Transparent} ** (PlaneWidth * PlaneHeight); - // then draw all the walls and populate the z_buffer, while also - // rendering the surfaces below the horizon to the pixel array + // Draw all vertical and horizontal surfaces, and populate the z-buffer self.renderCells(walls_image, surfaces_image, map, &pixels); - // then render the ceilings to our pixel array - self.renderCeilingsToTexture(surfaces_image, map, &pixels); - // use the z_buffer to render sprites self.renderObjects(objects_image, map, &pixels); @@ -244,7 +246,7 @@ pub fn Player(PlaneWidth: f32, PlaneHeight: f32) type { 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, 0))); + 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; @@ -252,9 +254,7 @@ pub fn Player(PlaneWidth: f32, PlaneHeight: f32) type { texel_y += inv_height; }) { const index = col * @floatToInt(usize, PlaneHeight) + @floatToInt(usize, bottom); - if (self.z_buffer[index] < scaled_perp_distance) { - break; - } else { + 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); @@ -328,10 +328,12 @@ pub fn Player(PlaneWidth: f32, PlaneHeight: f32) type { dist_x = (self.pos_y - @intToFloat(f32, ipos_y)) * dx_for_y_step; } - var top: f32 = undefined; + var top_of_floor: f32 = undefined; + var bottom_of_ceiling: f32 = undefined; var distance: f32 = 0; var still_drawing = true; - var highest_point: f32 = 0; + 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 @@ -349,19 +351,17 @@ pub fn Player(PlaneWidth: f32, PlaneHeight: f32) type { }) { const cell = map.lookup(ipos_x, ipos_y); - // Is there a wall? - if (cell.height > 0) { - // project the top of the wall - top = PlaneHeight / 2 + PlaneDist * (cell.height - self.height) / distance; + // 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; - // Does the wall extend above what we've draw? - if (top > highest_point) { - - // If we reach the top we have to stop! - if (top > PlaneHeight) { - still_drawing = false; - } + 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 @@ -374,60 +374,90 @@ pub fn Player(PlaneWidth: f32, PlaneHeight: f32) type { if (texfrac == 1) texfrac = 0.9999; // i think this caused a crash at one point const texstrip = @floatToInt(c_uint, constants.TextureDim * texfrac); - // which texture index? - const toff = cell.wall_texture * @floatToInt(c_uint, constants.TextureDim); - // 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; + // now we have what we need to draw the face, and + // update the z-buffer + const constrained_top = std.math.min(top_of_floor, lowest_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; + while (zb_y > @floatToInt(i32, highest_drawn)) : ({ + zb_y -= 1; + pix_y += 1; + texel_y += inv_nom_len; + }) { + const ty = @floatToInt(c_uint, std.math.modf(texel_y).fpart * (constants.TextureDim - 1)); + 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; + } - // now we have what we need to draw the wall, and - // update the z-buffer - const constrained_top = std.math.min(top, PlaneHeight - 1); - var zb_y = @floatToInt(i32, constrained_top); - var pix_y = @floatToInt(usize, std.math.ceil(std.math.max(PlaneHeight - top, 0))); - var texel_y = (top - constrained_top) / nominal_length; - while (zb_y > @floatToInt(i32, highest_point)) : ({ - 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 = toff + 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; + 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; } - highest_point = top; } } - // do we potentially draw the top of this cell? - if (highest_point < PlaneHeight / 2) { + // 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. - const next_top = PlaneHeight / 2 + PlaneDist * (cell.height - self.height) / distance; - - // only if we can see some part of it - if (next_top > highest_point) { - top = highest_point; - while (top <= next_top and top < PlaneHeight / 2) : (top += 1) { - const row_dist = (self.height - cell.height) * PlaneDist / (PlaneHeight / 2 - top); - const ptop = @floatToInt(usize, top + 1); - const itop = @floatToInt(usize, PlaneHeight) - ptop; + // 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); @@ -439,69 +469,44 @@ pub fn Player(PlaneWidth: f32, PlaneHeight: f32) type { pixels[itop * @floatToInt(usize, PlaneWidth) + col] = val; // record in the z_buffer only if we're above the floor! - if (cell.height > 0) { + if (cell.floor_height > 0) { const index = col * @floatToInt(usize, PlaneHeight) + ptop; self.z_buffer[index] = row_dist; } } - highest_point = next_top; + highest_drawn = next_top; } - } - } - } - } - fn renderCeilingsToTexture( - self: @This(), - surfaces_image: Image, - map: level.Map, - pixels: []Colour, - ) void { - // 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 frow = (PlaneHeight / 2 - @intToFloat(f32, row)); - const row_dist = (constants.MAX_HEIGHT - self.height) * PlaneDist / frow; - - 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); - - const index = col * @floatToInt(usize, PlaneHeight) + @floatToInt(usize, PlaneHeight - 1) - row; - if (map.inBounds(ix, iy) and row_dist < self.z_buffer[index]) { - const cell = map.lookup(ix, iy); - 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)); + // 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; - const val = surfaces_image.getPixel(.{ .x = toff + px, .y = py }); + 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; - pixels[row * @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; + } } } }; diff --git a/walls.png b/walls.png index a4ff902..ca89c86 100644 Binary files a/walls.png and b/walls.png differ -- cgit v1.3.1