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. --- src/player.zig | 239 +++++++++++++++++++++++++++++---------------------------- 1 file changed, 122 insertions(+), 117 deletions(-) (limited to 'src/player.zig') 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; + } } } }; -- cgit v1.3.1