diff options
| -rw-r--r-- | floors.png | bin | 0 -> 980 bytes | |||
| -rw-r--r-- | src/main.zig | 105 | ||||
| -rw-r--r-- | src/raycast.zig | 323 | ||||
| -rw-r--r-- | src/renderConstants.zig | 17 |
4 files changed, 265 insertions, 180 deletions
diff --git a/floors.png b/floors.png Binary files differnew file mode 100644 index 0000000..cf3892a --- /dev/null +++ b/floors.png diff --git a/src/main.zig b/src/main.zig index aef559f..2d12975 100644 --- a/src/main.zig +++ b/src/main.zig @@ -39,21 +39,19 @@ fn renderWall( texfrac: f32, // how far along the texture texture: u8, // which texture index ) void { - const hFact = screenWidth / planeWidth; - const vFact = screenHeight / planeHeight; // we need ceil here so that we draw always to or past the edge of the screen - const draw_height = @floatToInt(c_int, std.math.ceil(draw_frac * wallTextureDim)); - const total_length = total_height * vFact; + const draw_height = @floatToInt(c_int, std.math.ceil(draw_frac * TextureDim)); + const total_length = total_height * VFact; - const xpos = @intToFloat(f32, col) * hFact; - const ypos = (screenHeight + vFact * planeHeight) / 2 - top * vFact; + const xpos = @intToFloat(f32, col) * HFact; + const ypos = ScreenHeight / 2 + (PlaneHeight / 2 - top) * VFact; - const tind = @as(c_int, texture) * @floatToInt(c_int, 1 + wallTextureDim); - const left = tind + @floatToInt(c_int, texfrac * wallTextureDim); + const tind = @as(c_int, texture) * @floatToInt(c_int, 1 + TextureDim); + const left = tind + @floatToInt(c_int, texfrac * TextureDim); sprite.setPosition(.{ .x = xpos, .y = ypos }); - sprite.setScale(.{ .x = hFact, .y = total_length / wallTextureDim }); + sprite.setScale(.{ .x = HFact, .y = total_length / TextureDim }); sprite.setTextureRect(.{ .top = 0, .left = left, .width = 1, .height = draw_height }); window.draw(sprite, null); } @@ -64,9 +62,11 @@ pub fn main() !void { // Setup player and map //-------------------------------------------------------------------------- - var player = Player.new(15.75, 1.5, // pos - PI / 2.0, // ang - @intToFloat(f32, planeWidth), @intToFloat(f32, planeHeight)); + var player = try Player(PlaneWidth, PlaneHeight).new( + 15, // x coord + 1.5, // y coord + PI / 2.0, // angle + ); var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); @@ -83,45 +83,53 @@ pub fn main() !void { } map.cells.items[y * 15 + 15] = Cell{ .height = MAX_HEIGHT, .wall_texture = 1 }; - map.cells.items[y * 15 + 14] = Cell{ .height = MAX_HEIGHT / 1.5, .wall_texture = 1 }; + map.cells.items[y * 15 + 14] = Cell{ .height = MAX_HEIGHT / 3, .wall_texture = 1, .floor_texture = 1 }; // Initialise SFML //-------------------------------------------------------------------------- - var window = try sf.RenderWindow.create(.{ .x = screenWidth, .y = screenHeight }, 32, "zirc", sf.window.Style.none); + var window = try sf.RenderWindow.create(.{ .x = ScreenWidth, .y = ScreenHeight }, 32, "zirc", sf.window.Style.none); defer window.destroy(); - window.setFramerateLimit(30); - - var backRect = try sf.RectangleShape.create(.{ .x = screenWidth, .y = screenHeight / 2 }); - defer backRect.destroy(); - - backRect.setPosition(.{ .x = 0, .y = screenHeight / 2 }); - backRect.setFillColor(sf.Color.fromRGB(50, 50, 50)); + window.setFramerateLimit(60); // Load the background skybox, really `skycylinder', and assign it to a // sprite -- it's set to repeating so that it goes on forever. We need to - // scale the sprite so that drawing 1/4 of backTextureWidth fills the screen - // horizontally, and drawing backTextureHeight fills half of the screen + // scale the sprite so that drawing 1/4 of backTextureWidth fills the Screen + // horizontally, and drawing backTextureHeight fills half of the Screen // vertically. - var backTexture = try sf.Texture.createFromFile("back.png"); - defer backTexture.destroy(); + var back_texture = try sf.Texture.createFromFile("back.png"); + defer back_texture.destroy(); + + back_texture.setSmooth(false); // looks worse => better! + back_texture.setRepeated(true); + + var back_sprite = try sf.Sprite.createFromTexture(back_texture); + defer back_sprite.destroy(); - backTexture.setSmooth(false); // looks worse => better! - backTexture.setRepeated(true); + back_sprite.setScale(.{ .x = 4.0 * ScreenWidth / BackTextureWidth, .y = ScreenHeight / (2 * BackTextureHeight) }); - var backSprite = try sf.Sprite.createFromTexture(backTexture); - defer backSprite.destroy(); + // Load wall textures + var wall_textures = try sf.Texture.createFromFile("walls.png"); + defer wall_textures.destroy(); + wall_textures.setSmooth(false); // looks worse => better! - backSprite.setScale(.{ .x = 4.0 * screenWidth / backTextureWidth, .y = screenHeight / (2 * backTextureHeight) }); + var walls_sprite = try sf.Sprite.createFromTexture(wall_textures); + defer walls_sprite.destroy(); - // Load brick texture! - var wallTexture = try sf.Texture.createFromFile("walls.png"); - defer wallTexture.destroy(); - wallTexture.setSmooth(false); // looks worse => better! + // Load floor textures + var floors_image = try sf.Image.createFromFile("floors.png"); + defer floors_image.destroy(); - var wallSprite = try sf.Sprite.createFromTexture(wallTexture); - defer wallSprite.destroy(); + var rendered_floors_texture = try sf.Texture.create(.{ .x = PlaneWidth, .y = PlaneHeight / 2 }); + defer rendered_floors_texture.destroy(); + rendered_floors_texture.setSmooth(false); + + var rendered_floors_sprite = try sf.Sprite.createFromTexture(rendered_floors_texture); + defer rendered_floors_sprite.destroy(); + + rendered_floors_sprite.setPosition(.{ .x = 0, .y = ScreenHeight / 2 }); + rendered_floors_sprite.setScale(.{ .x = HFact, .y = VFact }); // Start your engines! //-------------------------------------------------------------------------- @@ -133,6 +141,9 @@ pub fn main() !void { if (isKeyPressed(.Escape)) window.close(); + if (isKeyPressed(.C)) player.height -= 0.1; + if (isKeyPressed(.V)) player.height += 0.1; + const rotation = 2.0 * PI / 100.0; if (isKeyPressed(.R)) player.ang += rotation; if (isKeyPressed(.T)) player.ang -= rotation; @@ -159,18 +170,24 @@ pub fn main() !void { window.clear(sf.Color.fromRGB(100, 100, 100)); - const back_scroll = @floatToInt(i32, -backTextureWidth * player.ang / (2 * PI)); - backSprite.setTextureRect(sf.IntRect{ + // First the background + const back_scroll = @floatToInt(i32, -BackTextureWidth * player.ang / (2 * PI)); + back_sprite.setTextureRect(sf.IntRect{ .left = back_scroll, // fix location of texture in the sky .top = 0, - // scaling ensures that the next two paint it across the width of - // the screen, and for half the height of the screen - .width = backTextureWidth / 4.0, // 1/4 of texture per screen - .height = backTextureHeight, // full height of texture visible + // scaling ensures that the next two dimensions paint it across the + // width of the Screen, and for half the height of the screen + .width = BackTextureWidth / 4.0, // 1/4 of texture per screen + .height = BackTextureHeight, // full height of texture visible }); - window.draw(backSprite, null); + window.draw(back_sprite, null); + + // Then render the walls + try player.renderFloorsToTexture(floors_image, rendered_floors_texture, map); + window.draw(rendered_floors_sprite, null); - player.renderMapUsing(window, wallSprite, map, renderWall); + // Finally the world + player.renderWorld(window, walls_sprite, map, renderWall); window.display(); diff --git a/src/raycast.zig b/src/raycast.zig index 608bb10..aca7db0 100644 --- a/src/raycast.zig +++ b/src/raycast.zig @@ -19,8 +19,12 @@ 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 RenderWallFunction: type = fn ( window: RenderWindow, @@ -33,165 +37,226 @@ pub const RenderWallFunction: type = fn ( texture: u8, // which texture index ) void; -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, - fov: f32 = std.math.pi / 3.0, - height: f32 = 1.7, // TODO +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, PlaneWidth * PlaneHeight), - plane_height: f32, - plane_width: i32, - plane_dist: f32, + 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 PlaneDist = PlaneWidth / (2 * std.math.tan(FOV / 2)); - pub fn new(pos_x: f32, pos_y: f32, ang: f32, plane_width: i32, plane_height: i32) Player { - const fov: f32 = std.math.pi / 3.0; - return Player{ - // standing still at the given location, looking in direction ang, - .pos_x = pos_x, - .pos_y = pos_y, - .ang = ang, - // plane of projection - .plane_width = plane_width, - .plane_height = @intToFloat(f32, plane_height), - // given the desired width of the image, how far away must - // the projection plane be from the camera? - .plane_dist = @intToFloat(f32, plane_width) / (2 * std.math.tan(fov / 2)), - }; - } + // 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: *Player) void { - const dt = 1 / 30.0; - const v_min = 0.8; - const v_decay = 1.25; + 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.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 /= 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; - } + self.vel_x += self.acc_x * dt; + self.vel_y += self.acc_y * dt; + } - pub fn renderMapUsing( - self: Player, - window: RenderWindow, - wallSprite: Sprite, - map: Map, - // the abstract the rendering call - renderWall: RenderWallFunction, - ) void { - self.renderWalls(window, wallSprite, map, renderWall); - } + pub fn renderWorld( + self: *@This(), + window: RenderWindow, + walls_sprite: Sprite, + 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); + } - fn renderWalls(self: Player, window: RenderWindow, wallSprite: Sprite, map: Map, renderWall: RenderWallFunction) void { - const floor = std.math.floor; + fn renderWalls( + self: *@This(), + window: RenderWindow, + walls_sprite: Sprite, + map: Map, + renderWall: RenderWallFunction, + ) void { + const floor = std.math.floor; - var col: i32 = 0; - while (col < self.plane_width) : (col += 1) { - const horiz_frac = @intToFloat(f32, col) / (@intToFloat(f32, self.plane_width) - 1); + var col: i32 = 0; + var ra: f32 = 0.5 * FOV + self.ang; + const ra_step = FOV / PlaneWidth; + while (col < PlaneWidth) : ({ + col += 1; + ra -= ra_step; + }) { + const cosra = std.math.cos(ra); + const sinra = std.math.sin(ra); - const ra = (0.5 - horiz_frac) * self.fov + self.ang; + // 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. + const dy_for_x_step = std.math.fabs(1 / cosra); + const dx_for_y_step = std.math.fabs(1 / sinra); - const cosra = std.math.cos(ra); - const sinra = std.math.sin(ra); + var step_x: i32 = -1; + var step_y: i32 = -1; - // 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. - const dy_for_x_step = std.math.fabs(1 / cosra); - const dx_for_y_step = std.math.fabs(1 / sinra); + var dist_x: f32 = undefined; + var dist_y: f32 = undefined; - var step_x: i32 = -1; - var step_y: i32 = -1; + var ipos_x: i32 = @floatToInt(i32, floor(self.pos_x)); + var ipos_y: i32 = @floatToInt(i32, floor(self.pos_y)); - var dist_x: f32 = undefined; - var dist_y: f32 = undefined; + // 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; + } - var ipos_x: i32 = @floatToInt(i32, floor(self.pos_x)); - var ipos_y: i32 = @floatToInt(i32, floor(self.pos_y)); + 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; + } - // 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; - } + 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); - 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; - } + // the correct distance is the shortest distance from the plane + // of projection to the point, that is, perpendicular distance + const perp_distance = distance * std.math.cos(self.ang - ra); - var distance: f32 = 0; - var still_drawing = true; - var highest_point: f32 = 0; - var horizontal_hit: bool = undefined; - while (still_drawing) { + // project the top of the wall + const top = PlaneHeight / 2 + PlaneDist * (cell.height - self.height) / perp_distance; - // 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; - } + // 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 / perp_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; - if (!map.inBounds(ipos_x, ipos_y)) break; + // draw the wall + renderWall(window, walls_sprite, col, top, total_length, draw_frac, texfrac, cell.wall_texture); - var cell = map.lookup(ipos_x, ipos_y); + // 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, perp_distance); + // } - // the correct distance is the shortest distance from the plane - // of projection to the point, that is, perpendicular distance - const perp_distance = distance * std.math.cos(self.ang - ra); + highest_point = top; + } + } + } + } - // project the top of the wall - const top = self.plane_height / 2 + self.plane_dist * (cell.height - self.height) / perp_distance; + pub fn renderFloorsToTexture(self: @This(), floors_image: Image, rendered_floors_texture: Texture, map: Map) !void { + var pixels = [_]Colour{Colour.Black} ** (PlaneWidth * PlaneHeight / 2); - // We have a wall to draw if it protrudes above what we have so far drawn - if (top > highest_point) { + const ang_step = FOV / PlaneWidth; + var row: usize = 0; + while (row < PlaneHeight / 2) : (row += 1) { + const row_dist = self.height * PlaneDist / @intToFloat(f32, row + 1); - // did we extend beyond the top of the plane? - if (top > self.plane_height) still_drawing = false; + var col: usize = 0; + var ang = 0.5 * FOV + self.ang; + var ang_diff: f32 = 0.5 * FOV; - // compute the height of this wall - const total_length = self.plane_dist * cell.height / perp_distance; + while (col < PlaneWidth) : ({ + col += 1; + ang -= ang_step; + ang_diff -= ang_step; + }) { + const perp_dist = row_dist / std.math.cos(ang_diff); + const x = self.pos_x + perp_dist * std.math.cos(ang); + const y = self.pos_y + perp_dist * std.math.sin(ang); - // as well as the fraction we'll be drawing - const draw_frac = std.math.min(1, (top - highest_point) / total_length); + const sx = std.math.modf(x); + const sy = std.math.modf(y); - // 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; + const ix = @floatToInt(i32, sx.ipart); + const iy = @floatToInt(i32, sy.ipart); - renderWall(window, wallSprite, col, top, total_length, draw_frac, texfrac, cell.wall_texture); + 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)); - highest_point = top; + const val = floors_image.getPixel(.{ .x = toff + px, .y = py }); + + pixels[row * @floatToInt(usize, PlaneWidth) + col] = val; + } } } + + try rendered_floors_texture.updateFromPixels(&pixels, null); } - } -}; + }; +} diff --git a/src/renderConstants.zig b/src/renderConstants.zig index 2dbadee..7c193a7 100644 --- a/src/renderConstants.zig +++ b/src/renderConstants.zig @@ -14,13 +14,16 @@ // // You should have received a copy of the GNU General Public License // along with this program. If not, see <https://www.gnu.org/licenses/>. -pub const screenWidth: f32 = 1024; -pub const screenHeight: f32 = 768; +pub const ScreenWidth: f32 = 1024; +pub const ScreenHeight: f32 = 768; -pub const planeWidth = 320; -pub const planeHeight = 240; +pub const PlaneWidth = 320; +pub const PlaneHeight = 240; -pub const backTextureWidth: f32 = 1280; -pub const backTextureHeight: f32 = 240; +pub const HFact = ScreenWidth / PlaneWidth; +pub const VFact = ScreenHeight / PlaneHeight; -pub const wallTextureDim: f32 = 32.0; +pub const BackTextureWidth: f32 = 1280; +pub const BackTextureHeight: f32 = 240; + +pub const TextureDim: f32 = 32.0; |
