From 44935fcbcc46ed773849e8af6377543e02352e87 Mon Sep 17 00:00:00 2001 From: Alex Clink Date: Tue, 29 Mar 2022 22:59:02 -0400 Subject: [PATCH] Refactor fill_triangle, add textured example --- examples/3d.cr | 72 +++++++---- src/sprite/fill_triangle.cr | 243 ++++++++++++++++++++---------------- 2 files changed, 187 insertions(+), 128 deletions(-) diff --git a/examples/3d.cr b/examples/3d.cr index 9d65294..acd6c8f 100644 --- a/examples/3d.cr +++ b/examples/3d.cr @@ -3,6 +3,7 @@ require "../src/controller" require "../src/sprite" require "../src/pixel" require "../src/vector" +require "../src/sprite" require "../src/3d/*" @@ -10,19 +11,25 @@ class ThreeDee < PF::Game @projector : PF::Projector @camera : PF::Camera @paused = false - @speed = 5.0 + @speed = 10.0 @controller : PF::Controller(PF::Keys) + @depth_buffer : PF::DepthBuffer def initialize(*args, **kwargs) super @projector = PF::Projector.new(width, height) + @depth_buffer = PF::DepthBuffer.new(width, height) + @camera = @projector.camera - # @model = PF::Mesh.load_obj("./assets/cube.obj") + @model = PF::Mesh.load_obj("./assets/pixelfaucet.obj") - @texture = PF::Sprite.new("./assets/bricks.png") @model.position.z = @model.position.z + 2.0 + @cube_model = PF::Mesh.load_obj("./assets/cube.obj") + @cube_model.position.z = @cube_model.position.z + 2.5 + @sprite = PF::Sprite.new("./assets/bricks.png") + @controller = PF::Controller(PF::Keys).new({ PF::Keys::RIGHT => "Rotate Right", PF::Keys::LEFT => "Rotate Left", @@ -60,22 +67,23 @@ class ThreeDee < PF::Game @camera.position.y = @camera.position.y - @speed * dt end - # Controll the camera pitch instead of elevation - + # Control the camera pitch instead of elevation - + # TODO: this needs to account for where the camera is pointing # if @controller.held?("Up") - # @camera.pitch = @camera.pitch + (@speed / 2) * dt + # @camera.pitch = @camera.pitch + (@speed / 5) * dt # end - + # # if @controller.held?("Down") - # @camera.pitch = @camera.pitch - (@speed / 2) * dt + # @camera.pitch = @camera.pitch - (@speed / 5) * dt # end if @controller.held?("Rotate Left") - @camera.yaw = @camera.yaw - (@speed / 2) * dt + @camera.yaw = @camera.yaw - (@speed / 3) * dt end if @controller.held?("Rotate Right") - @camera.yaw = @camera.yaw + (@speed / 2) * dt + @camera.yaw = @camera.yaw + (@speed / 3) * dt end if @controller.held?("Forward") @@ -86,33 +94,53 @@ class ThreeDee < PF::Game @camera.position = @camera.position - (forward * @speed * dt) end - @model.rotation.x = @model.rotation.x + 3.0 * dt + @model.rotation.x = @model.rotation.x + 1.0 * dt end def draw - clear(25, 50, 25) - tris = @projector.project(@model.tris) - draw_string("Triangles: #{tris.size}", 3, 3) + # clear(25, 50, 25) + clear + @depth_buffer.clear + cube_tris = @projector.project(@cube_model.tris) + cube_tris.each do |tri| + fill_triangle( + tri.p1.to_i, tri.p2.to_i, tri.p3.to_i, # Points + tri.t1, tri.t2, tri.t3, # Texture Points + @sprite, + @depth_buffer, + tri.color + ) + end + + tris = @projector.project(@model.tris) tris.each do |tri| # Rasterize all triangles + fill_triangle( PF::Vector[tri.p1.x.to_i, tri.p1.y.to_i], PF::Vector[tri.p2.x.to_i, tri.p2.y.to_i], PF::Vector[tri.p3.x.to_i, tri.p3.y.to_i], - pixel: tri.color + pixel: tri.color # buffer: @depth_buffer ) - - # draw_triangle( - # PF::Vector[tri.p1.x.to_i, tri.p1.y.to_i], - # PF::Vector[tri.p2.x.to_i, tri.p2.y.to_i], - # PF::Vector[tri.p3.x.to_i, tri.p3.y.to_i], - # pixel: PF::Pixel.blue - # ) end + + string = String.build do |io| + io << "Triangles: " << tris.size + cube_tris.size + io << "\nPosition: " + io << "x: " << @camera.position.x.round(2) + io << "y: " << @camera.position.y.round(2) + io << "z: " << @camera.position.z.round(2) + io << "\nRotation: " + io << "x: " << @camera.rotation.x.round(2) + io << "y: " << @camera.rotation.y.round(2) + io << "z: " << @camera.rotation.z.round(2) + end + + draw_string(string, 3, 3) end end # engine = ThreeDee.new(256, 240, 4) -engine = ThreeDee.new(640, 480, 2) +engine = ThreeDee.new(256 * 2, 240 * 2, 2) engine.run! diff --git a/src/sprite/fill_triangle.cr b/src/sprite/fill_triangle.cr index c200c42..3179db6 100644 --- a/src/sprite/fill_triangle.cr +++ b/src/sprite/fill_triangle.cr @@ -2,12 +2,25 @@ require "../line" module PF class Sprite - # Draw a filled in triangle - def fill_triangle(p1 : Vector2, p2 : Vector2, p3 : Vector2, pixel : Pixel = Pixel.new) + private def sort_verticies(p1 : Vector2, p2 : Vector2, p3 : Vector2) # Sort points from top to bottom p1, p2 = p2, p1 if p2.y < p1.y p1, p3 = p3, p1 if p3.y < p1.y p2, p3 = p3, p2 if p3.y < p2.y + {p1, p2, p3} + end + + private def sort_verticies(p1 : Vector3, p2 : Vector3, p3 : Vector3, t1 : Vector3, t2 : Vector3, t3 : Vector3) + # Sort points from top to bottom + p1, p2, t1, t2 = p2, p1, t2, t1 if p2.y < p1.y + p1, p3, t1, t3 = p3, p1, t3, t1 if p3.y < p1.y + p2, p3, t2, t3 = p3, p2, t3, t2 if p3.y < p2.y + {p1, p2, p3, t1, t2, t3} + end + + # Draw a filled in triangle + def fill_triangle(p1 : Vector2, p2 : Vector2, p3 : Vector2, pixel : Pixel = Pixel.new) + p1, p2, p3 = sort_verticies(p1, p2, p3) # sort left and right edges by run / rise line_left = PF::Line.new(p1, p2) @@ -21,36 +34,46 @@ module PF slope_left = line_left.slope slope_right = line_right.slope - c = p1.y # offset - height = p3.y - p1.y - mid = p2.y - p1.y + offset = p1.y # height offset from 0 + height = p3.y - p1.y # height of the triangle + mid = p2.y - p1.y # where the flat bottom triangle ends - 0.upto(height) do |y| - if slope_left == 0 - # When there is no rise, set the x value directly - x_left = line_left.p2.x - else - x_left = ((y - (line_left.p1.y - p1.y)) / slope_left).round.to_i + line_left.p1.x - end + start = 0 + fin = mid - if slope_right == 0 - x_right = line_right.p2.x - else - x_right = ((y - (line_right.p1.y - p1.y)) / slope_right).round.to_i + line_right.p1.x - end - - x_left.upto(x_right) do |x| - draw_point(x, y + c, pixel) - end - - if y == mid - if line_left.p2 == p2 - line_left = PF::Line.new(p2, p3) - slope_left = line_left.slope + # Draw the triangle in two halfs + # 0 - Flat bottom triangle + # 1 - Flat top triangle + 2.times do |half| + start.upto(fin) do |y| + if slope_left == 0 + # When there is no rise, set the x value directly + x_left = line_left.p2.x else - line_right = PF::Line.new(p2, p3) - slope_right = line_right.slope + x_left = ((y - (line_left.p1.y - p1.y)) / slope_left).round.to_i + line_left.p1.x end + + if slope_right == 0 + x_right = line_right.p2.x + else + x_right = ((y - (line_right.p1.y - p1.y)) / slope_right).round.to_i + line_right.p1.x + end + + x_left.upto(x_right) do |x| + draw_point(x, y + offset, pixel) + end + end + + start = fin + 1 + fin = height + + # Depending on which point is the middle + if line_left.p2 == p2 + line_left = PF::Line.new(p2, p3) + slope_left = line_left.slope + else + line_right = PF::Line.new(p2, p3) + slope_right = line_right.slope end end end @@ -61,11 +84,11 @@ module PF end # Draw a textured triangle - def fill_triangle(p1 : Vector2, p2 : Vector2, p3 : Vector2, t1 : Vector3, t2 : Vector3, t3 : Vector3, sprite : Sprite, buffer : DepthBuffer, color : Pixel = Pixel.white) - # Sort points from top to bottom - p1, p2, t1, t2 = p2, p1, t2, t1 if p2.y < p1.y - p1, p3, t1, t3 = p3, p1, t3, t1 if p3.y < p1.y - p2, p3, t2, t3 = p3, p2, t3, t2 if p3.y < p2.y + def fill_triangle(p1 : Vector3, p2 : Vector3, p3 : Vector3, t1 : Vector3, t2 : Vector3, t3 : Vector3, sprite : Sprite, buffer : DepthBuffer, color : Pixel = Pixel::White) + p1, p2, p3, t1, t2, t3 = sort_verticies(p1, p2, p3, t1, t2, t3) + + # z = (p1.z + p2.z + p3.z) // 3 + z = p1.z # Create lines starting at p1 to the other lower points line_left = PF::Line.new(p1, p2) @@ -92,95 +115,103 @@ module PF height = p3.y - p1.y # triangle height mid = p2.y - p1.y # where the shorter line ends - # Starting at 0, up to the height, draw scanlines - 0.upto(height) do |y| - # Get the normalized t value for this height level - ty = height > 0 ? y / height : 0.0 + start = 0 + fin = mid - # Check if the slope is 0, this would cause a divide by 0 - if slope_left == 0 - # When there is no rise, set the x value directly - x_left = line_left.p2.x - else - x_left = ((y - (line_left.p1.y - p1.y)) / slope_left).round.to_i + line_left.p1.x - end - - if slope_right == 0 - x_right = line_right.p2.x - t_right = tl_right.p2.x - else - x_right = ((y - (line_right.p1.y - p1.y)) / slope_right).round.to_i + line_right.p1.x - end - - # Get the normalized t value for this height level - ty = height > 0 ? y / height : 0.0 - - # LERP both texture edges at the y position to create a new line - tyl = - if switch_left - # Line left is the 2 part segment - if y <= mid - # still in the first segment (percent over the midpoint) - mid == 0 ? 0.0 : y / mid - else - # in the second part, pecentage of middle to end - height == 0 ? 0.0 : (y - mid) / (height - mid) - end + # Draw the triangle in two halfs + # 0 - Flat bottom triangle + # 1 - Flat top triangle + 2.times do |half| + start.upto(fin) do |y| + # Check if the slope is 0, this would cause a divide by 0 + if slope_left == 0 + # When there is no rise, set the x value directly + x_left = line_left.p2.x else - height == 0 ? 0.0 : y / height + x_left = ((y - (line_left.p1.y - p1.y)) / slope_left).round.to_i + line_left.p1.x end - tyr = - unless switch_left - if y <= mid - mid == 0 ? 1.0 : y / mid - else - height == 0 ? 1.0 : (y - mid) / (height - mid) - end + if slope_right == 0 + x_right = line_right.p2.x + t_right = tl_right.p2.x else - height == 0 ? 1.0 : y / height + x_right = ((y - (line_right.p1.y - p1.y)) / slope_right).round.to_i + line_right.p1.x end - texture_line = PF::Line.new(tl_left.lerp(tyl), tl_right.lerp(tyr)) + # Get the normalized t value for this height level + ty = height > 0 ? y / height : 0.0 - # Get the width of the scan line - scan_size = x_right - x_left + # LERP both texture edges at the y position to create a new line + tyl = + if switch_left + # Line left is the 2 part segment + if half == 0 + # still in the first segment (percent over the midpoint) + mid == 0 ? 0.0 : y / mid + else + # in the second part, pecentage of middle to end + height == 0 ? 0.0 : (y - mid) / (height - mid) + end + else + height == 0 ? 0.0 : y / height + end - x_left.upto(x_right) do |x| - # LERP the line between the texture edges - t = scan_size == 0 ? 0.0 : (x - x_left) / scan_size - texture_point = texture_line.lerp(t) + tyr = + unless switch_left + if half == 0 + mid == 0 ? 1.0 : y / mid + else + height == 0 ? 1.0 : (y - mid) / (height - mid) + end + else + height == 0 ? 1.0 : y / height + end - if texture_point.z >= buffer[x, y + c] - buffer[x, y + c] = texture_point.z - # Get the x and y of the texture coords, divide by z for perspective, then - # multiply the point by the size of the sprite to get the final texture point - sample_point = ((Vector[texture_point.x, texture_point.y] / texture_point.z) * sprite.size) - # Invert the y axis for the sprite - sample_point.y = sprite.height - sample_point.y - # sample_point = sample_point / texture_point.z if texture_point.z != 0 - pixel = sprite.sample((sample_point + 0.5).to_i) + texture_line = PF::Line.new(tl_left.lerp(tyl), tl_right.lerp(tyr)) - # Blend the pixel sample with the provided color - pixel.r = (pixel.r * (color.r / 255)).to_u8 - pixel.g = (pixel.g * (color.g / 255)).to_u8 - pixel.b = (pixel.b * (color.b / 255)).to_u8 + # Get the width of the scan line + scan_size = x_right - x_left - draw_point(x, y + c, pixel) + x_left.upto(x_right) do |x| + # LERP the line between the texture edges + t = scan_size == 0 ? 0.0 : (x - x_left) / scan_size + texture_point = texture_line.lerp(t) + + if texture_point.z > buffer[x, y + c] + buffer[x, y + c] = texture_point.z + # Get the x and y of the texture coords, divide by z for perspective, then + # multiply the point by the size of the sprite to get the final texture point + sample_point = ((Vector[texture_point.x, texture_point.y] / texture_point.z) * sprite.size) + # Invert the y axis for the sprite + sample_point.y = sprite.height - sample_point.y + sample_point %= sprite.size + + pixel = sprite.sample((sample_point).to_i) + + # Blend the pixel sample with the provided color + pixel = pixel.darken(color) + + # Darken by distance + d = (((50.0 - z) / 100.0) + 0.5).clamp(0.0..1.0) + pixel *= d + + draw_point(x, y + c, pixel) + end end end + start = fin + 1 + fin = height + # Once we hit the point where a line changes, we need a new slope for that line - if y == mid - if switch_left - line_left = PF::Line.new(p2, p3) - tl_left = PF::Line.new(t2, t3) - slope_left = line_left.slope - else - line_right = PF::Line.new(p2, p3) - tl_right = PF::Line.new(t2, t3) - slope_right = line_right.slope - end + if switch_left + line_left = PF::Line.new(p2, p3) + tl_left = PF::Line.new(t2, t3) + slope_left = line_left.slope + else + line_right = PF::Line.new(p2, p3) + tl_right = PF::Line.new(t2, t3) + slope_right = line_right.slope end end end