diff --git a/examples/3d.cr b/examples/3d.cr index acd6c8f..8f8ecbb 100644 --- a/examples/3d.cr +++ b/examples/3d.cr @@ -111,22 +111,37 @@ class ThreeDee < PF::Game @depth_buffer, tri.color ) + + if tri.clipped + p1 = PF::Vector[tri.p1.x.to_i, tri.p1.y.to_i] + p2 = PF::Vector[tri.p2.x.to_i, tri.p2.y.to_i] + p3 = PF::Vector[tri.p3.x.to_i, tri.p3.y.to_i] + draw_triangle(p1, p2, p3, PF::Pixel.new(255, 255, 0)) + end end - tris = @projector.project(@model.tris) + tris = @projector.project(@model.tris, sort: true) tris.each do |tri| # Rasterize all triangles + p1 = PF::Vector[tri.p1.x.to_i, tri.p1.y.to_i] + p2 = PF::Vector[tri.p2.x.to_i, tri.p2.y.to_i] + p3 = PF::Vector[tri.p3.x.to_i, tri.p3.y.to_i] + 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], + p1, + p2, + p3, pixel: tri.color # buffer: @depth_buffer ) + + if tri.clipped + draw_triangle(p1, p2, p3, PF::Pixel.new(255, 0, 0)) + end end string = String.build do |io| - io << "Triangles: " << tris.size + cube_tris.size + io << "Triangles: " << cube_tris.size + tris.size io << "\nPosition: " io << "x: " << @camera.position.x.round(2) io << "y: " << @camera.position.y.round(2) diff --git a/src/3d/projector.cr b/src/3d/projector.cr index 2d67a60..297fd0c 100644 --- a/src/3d/projector.cr +++ b/src/3d/projector.cr @@ -6,18 +6,20 @@ module PF getter width : Int32 | Float64 getter height : Int32 | Float64 property near = 0.1 - property far = 1000.0 - getter fov = 90.0 + property far = 50.0 + getter fov = 70.0 property aspect_ratio : Float64? property camera : Camera property light : Vector3(Float64) = Vector3.new(0.0, 0.0, -1.0).normalized property mat_proj : Matrix(Float64, 16)? property clipping_plane_near : Vector3(Float64) - property near_plane_normal : Vector3(Float64) = Vector3.new(0.0, 0.0, 1.0) + property clipping_plane_far : Vector3(Float64) + @fov_rad : Float64? def initialize(@width, @height, @camera = Camera.new) @clipping_plane_near = Vector3.new(0.0, 0.0, @near) + @clipping_plane_far = Vector3.new(0.0, 0.0, @far) end def mat_proj @@ -58,42 +60,50 @@ module PF def project(tris : Array(Tri), camera = @camera, sort : Bool = false) mat_view = camera.view_matrix - # only draw triangles facing the camera + # Only draw triangles facing the camera tris = tris.select do |tri| tri.normal.dot(tri.p1 - camera.position) < 0.0 end - # Iterate tris to transform into view, project, and clip - 0.upto(tris.size - 1) do - tri = tris.pop + # Iterate tris to transform into + tris = tris.map do |tri| shade = (tri.normal.dot(light) + 1.0) / 2 # light should be normalized tri.color = tri.color * shade + tri * mat_view + end - tri *= mat_view + # Clip tris + { + {clipping_plane_near, Vector[0.0, 0.0, 1.0]}, + {clipping_plane_far, Vector[0.0, 0.0, -1.0]}, + }.each do |clip| + 0.upto(tris.size - 1) do + tri = tris.pop + tri.clip(plane: clip[0], plane_normal: clip[1]).each do |tri| + tris.unshift(tri) + end + end + end - # Clip against the near plane - tri.clip(plane: clipping_plane_near, plane_normal: near_plane_normal).each do |tri| - tri *= mat_proj + # Project the triangles + tris = tris.map do |tri| + z = tri.z + tri *= mat_proj + tri.z = z + tri.map_points do |point| # Invert the y axis - tri.p1.y = tri.p1.y * -1.0 - tri.p2.y = tri.p2.y * -1.0 - tri.p3.y = tri.p3.y * -1.0 + point.y *= -1.0 # scale into screen space - tri.p1 += 1.0 - tri.p2 += 1.0 - tri.p3 += 1.0 + point += 1.0 + point.x *= 0.5 * width + point.y *= 0.5 * height - tri.p1.x = tri.p1.x * 0.5 * width - tri.p1.y = tri.p1.y * 0.5 * height - tri.p2.x = tri.p2.x * 0.5 * width - tri.p2.y = tri.p2.y * 0.5 * height - tri.p3.x = tri.p3.x * 0.5 * width - tri.p3.y = tri.p3.y * 0.5 * height - - tris.unshift(tri) + point end + + tri end # sort triangles, no need to do this if using a depth buffer diff --git a/src/3d/tri.cr b/src/3d/tri.cr index 0658416..4487b14 100644 --- a/src/3d/tri.cr +++ b/src/3d/tri.cr @@ -12,14 +12,15 @@ module PF property t2 : Vector3(Float64) = Vector[0.0, 0.0, 0.0] property t3 : Vector3(Float64) = Vector[0.0, 0.0, 0.0] - property color : PF::Pixel + property color : Pixel + getter clipped : Bool = false setter normal : Vector3(Float64)? - def initialize(@p1 : Vector3(Float64), @p2 : Vector3(Float64), @p3 : Vector3(Float64), @color = PF::Pixel::White, @normal = nil) + def initialize(@p1 : Vector3(Float64), @p2 : Vector3(Float64), @p3 : Vector3(Float64), @color = PF::Pixel::White, @normal = nil, @clipped = false) end - def initialize(@p1, @p2, @p3, @t1, @t2, @t3, @color = PF::Pixel::White) + def initialize(@p1, @p2, @p3, @t1, @t2, @t3, @color = PF::Pixel::White, @clipped = false) end # Return the normal assuming clockwise pointing winding @@ -46,6 +47,18 @@ module PF (@p1.z + @p2.z + @p3.z) / 3.0 end + def z=(value : Float64) + @p1.z = value + @p2.z = value + @p3.z = value + end + + def map_points + @p1 = yield @p1 + @p2 = yield @p2 + @p3 = yield @p3 + end + # Multiply all points by a *Matrix*, returning a new *Tri* def *(mat : Matrix) # The transform function returns a w, which is the 4th component of the vertex @@ -119,7 +132,8 @@ module PF Tri.new( inside_points[0], clip_p1, clip_p2, inside_texts[0], int_t1, int_t2, - color: @color + color: @color, + clipped: true ), } end @@ -141,14 +155,16 @@ module PF Tri.new( inside_points[0], inside_points[1], clip_p1, inside_texts[0], inside_texts[1], int_t1, - color: @color + color: @color, + clipped: true ), # The second triangle will have the second inside point, the second intersection, then the first intersection # This order preserves clockwise winding Tri.new( inside_points[1], clip_p2, clip_p1, inside_texts[1], int_t2, int_t1, - color: @color + color: @color, + clipped: true ), } end