pixelfaucet/examples/cube.cr

373 lines
8.2 KiB
Crystal
Raw Normal View History

2021-11-30 23:05:07 -05:00
require "../src/game"
require "../src/controller"
require "../src/sprite"
require "../src/sprite/vector_sprite"
require "../src/pixel"
require "../src/point"
struct Vec3d(T)
property x : T
property y : T
property z : T
2021-12-12 23:42:58 -05:00
property w : T
2021-11-30 23:05:07 -05:00
2021-12-12 23:42:58 -05:00
def initialize(@x : T, @y : T, @z : T, @w = T.new(1))
2021-11-30 23:05:07 -05:00
end
def +(other : T)
Vec3d.new(@x + other, @y + other, @z + other)
end
def +(other : Vec3d)
Vec3d.new(@x + other.x, @y + other.y, @z + other.z)
end
def -(other : T)
Vec3d.new(@x - other, @y - other, @z - other)
end
def -(other : Vec3d)
Vec3d.new(@x - other.x, @y - other.y, @z - other.z)
end
2021-11-30 23:05:07 -05:00
def *(matrix : Mat4)
vec = Vec3d.new(
@x * matrix[0, 0] + @y * matrix[1, 0] + @z * matrix[2, 0] + matrix[3, 0],
@x * matrix[0, 1] + @y * matrix[1, 1] + @z * matrix[2, 1] + matrix[3, 1],
@x * matrix[0, 2] + @y * matrix[1, 2] + @z * matrix[2, 2] + matrix[3, 2]
)
w = @x * matrix[0, 3] + @y * matrix[1, 3] + @z * matrix[2, 3] + matrix[3, 3]
vec /= w unless w == 0.0
vec
end
def *(other : Vec3d)
Vec3d.new(@x * other.x, @y * other.y, @z * other.z)
end
def *(other : T)
Vec3d.new(@x * other, @y * other, @z * other)
end
def /(other : Vec3d)
Vec3d.new(@x / other.x, @y / other.y, @z / other.z)
end
2021-11-30 23:05:07 -05:00
def /(other : T)
Vec3d.new(@x / other, @y / other, @z / other)
end
def cross_product(other : Vec3d)
Vec3d.new(
x: @y * other.z - @z * other.y,
y: @z * other.x - @x * other.z,
z: @x * other.y - @y * other.x
)
end
2021-11-30 23:05:07 -05:00
2021-12-12 21:33:10 -05:00
# Geth the length using pythagorean
def length
Math.sqrt(@x * @x + @y * @y + @z * @z)
end
def normalized
2021-12-12 21:33:10 -05:00
l = length
Vec3d.new(@x / l, @y / l, @z / l)
end
# Returns the dot product
def dot(other : Vec3d)
@x * other.x + @y * other.y + @z * other.z
2021-11-30 23:05:07 -05:00
end
end
struct Mat4
2021-12-07 00:34:45 -05:00
alias T = Float64
alias RowT = Tuple(T, T, T, T)
property matrix = Slice(T).new(4*4, 0.0)
2021-11-30 23:05:07 -05:00
def index(x : Int, y : Int)
y * 4 + x
end
2021-12-07 00:34:45 -05:00
def set(value : Tuple(RowT, RowT, RowT, RowT))
2021-12-07 21:43:37 -05:00
{% for y in (0..3) %}
{% for x in (0..3) %}
self[{{x}},{{y}}] = value[{{x}}][{{y}}]
2021-12-07 00:34:45 -05:00
{% end %}
{% end %}
end
2021-11-30 23:05:07 -05:00
def [](x : Int, y : Int)
self[index(x, y)]
end
def []=(x : Int, y : Int, value : Float64)
self[index(x, y)] = value
end
def [](index)
@matrix[index]
end
def []=(index, value)
@matrix[index] = value
end
end
struct Tri
property p1 : Vec3d(Float64)
property p2 : Vec3d(Float64)
property p3 : Vec3d(Float64)
@normal : Vec3d(Float64)?
def initialize(@p1 : Vec3d(Float64), @p2 : Vec3d(Float64), @p3 : Vec3d(Float64))
end
def initialize(p1x : Float64, p1y : Float64, p1z : Float64, p2x : Float64, p2y : Float64, p2z : Float64, p3x : Float64, p3y : Float64, p3z : Float64)
@p1 = Vec3d(Float64).new(p1x, p1y, p1z)
@p2 = Vec3d(Float64).new(p2x, p2y, p2z)
@p3 = Vec3d(Float64).new(p3x, p3y, p3z)
end
2021-12-07 21:43:37 -05:00
# Return the normal assuming clockwise pointing winding
def normal
line1 = @p2 - @p1
line2 = @p3 - @p1
@normal ||= line1.cross_product(line2).normalized
end
2021-12-07 00:34:45 -05:00
2021-12-07 21:43:37 -05:00
# Get the average x value
def x
(@p1.x + @p2.x + @p3.x) / 3.0
end
# Get the average y value
def y
(@p1.y + @p2.y + @p3.y) / 3.0
end
# Get the average z value
2021-12-07 00:34:45 -05:00
def z
(@p1.z + @p2.z + @p3.z) / 3.0
end
2021-12-07 21:43:37 -05:00
# Multiply all points by a Mat4, returning a new Tri
def *(mat : Mat4)
Tri.new(@p1 * mat, @p2 * mat, @p3 * mat)
end
2021-12-07 00:34:45 -05:00
end
class Mesh
property tris = [] of Tri
def initialize(@tris)
end
def self.load(path)
verticies = [] of Vec3d(Float64)
tris = [] of Tri
2021-12-07 21:43:37 -05:00
line_no = 0
2021-12-07 00:34:45 -05:00
File.open(path) do |file|
file.each_line do |line|
2021-12-07 21:43:37 -05:00
line_no += 1
next if line =~ /^\s*$/
parts = line.split(/\s+/)
case parts[0]
when "v"
2021-12-07 00:34:45 -05:00
verticies << Vec3d.new(parts[1].to_f64, parts[2].to_f64, parts[3].to_f64)
2021-12-07 21:43:37 -05:00
when "f"
face_verts = [] of Vec3d(Float64)
parts[1..3].each do |part|
face = part.split('/')
face_verts << verticies[face[0].to_i - 1]
end
tris << Tri.new(face_verts[0], face_verts[1], face_verts[2])
2021-12-07 00:34:45 -05:00
end
end
end
new(tris)
end
end
2021-12-07 21:43:37 -05:00
class Model
2021-12-07 00:34:45 -05:00
property mesh : Mesh
2021-11-30 23:05:07 -05:00
property position = Vec3d(Float64).new(0.0, 0.0, 0.0)
property rotation = Vec3d(Float64).new(0.0, 0.0, 0.0)
2021-12-07 00:34:45 -05:00
@mat_rx = Mat4.new
@mat_ry = Mat4.new
@mat_rz = Mat4.new
2021-12-12 23:42:58 -05:00
@mat_translation = Mat4.new
2021-12-07 00:34:45 -05:00
2021-12-07 21:43:37 -05:00
def initialize(obj : String)
@mesh = Mesh.load(obj)
2021-11-30 23:05:07 -05:00
end
def update(dt : Float64)
2021-12-07 21:43:37 -05:00
cox = Math.cos(@rotation.x)
sox = Math.sin(@rotation.x)
coy = Math.cos(@rotation.y)
soy = Math.sin(@rotation.y)
2021-12-12 23:42:58 -05:00
coz = Math.cos(@rotation.z)
siz = Math.sin(@rotation.z)
2021-12-07 21:43:37 -05:00
@mat_rx.set({
{1.0, 0.0, 0.0, 0.0},
{0.0, cox, sox, 0.0},
{0.0, -sox, cox, 0.0},
{0.0, 0.0, 0.0, 1.0},
})
@mat_ry.set({
{coy, 0.0, soy, 0.0},
{0.0, 1.0, 0.0, 0.0},
{-soy, 0.0, coy, 0.0},
{0.0, 0.0, 0.0, 1.0},
})
2021-12-12 23:42:58 -05:00
@mat_rz.set({
{coz, siz, 0.0, 0.0},
{-siz, coz, 0.0, 0.0},
{0.0, 0.0, 1.0, 0.0},
{0.0, 0.0, 0.0, 1.0},
})
@mat_translation.set({
{1.0, 0.0, 0.0, 0.0},
{0.0, 1.0, 0.0, 0.0},
{0.0, 0.0, 1.0, 0.0},
{@position.x, @position.y, @position.z, 1.0},
})
2021-11-30 23:05:07 -05:00
end
2021-12-07 00:34:45 -05:00
def draw(engine : PF::Game, mat_proj, camera, light)
# Translation and rotation
tris = @mesh.tris.map do |tri|
2021-12-07 21:43:37 -05:00
tri *= @mat_rx
tri *= @mat_ry
tri *= @mat_rz
2021-12-12 23:42:58 -05:00
tri *= @mat_translation
2021-12-07 00:34:45 -05:00
tri
end
# only draw triangles facing the camera
tris = tris.select do |tri|
tri.normal.dot(tri.p1 - camera) < 0
end
# sort triangles
2021-12-12 23:42:58 -05:00
tris = tris.sort { |a, b| a.z <=> b.z }
2021-12-07 00:34:45 -05:00
tris.each do |tri|
2021-12-12 23:42:58 -05:00
shade : UInt8 = (tri.normal.dot(light).abs * 255.0).clamp(0.0..255.0).to_u8
2021-12-07 00:34:45 -05:00
tri.p1 *= mat_proj
tri.p2 *= mat_proj
tri.p3 *= mat_proj
tri.p1 += 1.0
tri.p2 += 1.0
tri.p3 += 1.0
tri.p1 *= 0.5 * engine.width
tri.p2 *= 0.5 * engine.width
tri.p3 *= 0.5 * engine.width
engine.fill_triangle(
PF::Point.new(tri.p1.x.to_i, tri.p1.y.to_i),
PF::Point.new(tri.p2.x.to_i, tri.p2.y.to_i),
PF::Point.new(tri.p3.x.to_i, tri.p3.y.to_i),
pixel: PF::Pixel.new(shade, shade, shade, 255)
)
2021-11-30 23:05:07 -05:00
end
end
end
class CubeGame < PF::Game
2021-12-07 21:43:37 -05:00
@cube : Model
2021-11-30 23:05:07 -05:00
@paused = false
2021-11-30 23:05:07 -05:00
@aspect_ratio : Float64
@fov : Float64
@fov_rad : Float64
@near : Float64
@far : Float64
@camera : Vec3d(Float64)
2021-12-07 00:34:45 -05:00
@light : Vec3d(Float64) = Vec3d.new(0.0, 0.0, -1.0).normalized
2021-11-30 23:05:07 -05:00
@mat_proj : Mat4
2021-12-07 00:34:45 -05:00
@speed = 3.0
2021-11-30 23:05:07 -05:00
def initialize(@width, @height, @scale)
super(@width, @height, @scale)
2021-12-07 21:43:37 -05:00
@cube = Model.new("examples/cube.obj")
2021-12-12 23:42:58 -05:00
@cube.position.z = @cube.position.z - 3.0
2021-11-30 23:05:07 -05:00
@controller = PF::Controller(LibSDL::Keycode).new({
LibSDL::Keycode::RIGHT => "Rotate Right",
LibSDL::Keycode::LEFT => "Rotate Left",
2021-12-07 00:34:45 -05:00
LibSDL::Keycode::UP => "Rotate Up",
LibSDL::Keycode::DOWN => "Rotate Down",
2021-11-30 23:05:07 -05:00
LibSDL::Keycode::SPACE => "Pause",
})
@near = 0.1
@far = 1000.0
@fov = 90.0
@aspect_ratio = @height / @width
@fov_rad = 1.0 / Math.tan(@fov * 0.5 / 180.0 * Math::PI)
@camera = Vec3d.new(0.0, 0.0, 0.0)
2021-11-30 23:05:07 -05:00
@mat_proj = Mat4.new
@mat_proj[0, 0] = @aspect_ratio * @fov_rad
@mat_proj[1, 1] = @fov_rad
@mat_proj[2, 2] = @far / (@far - @near)
@mat_proj[3, 2] = (-@far * @near) / (@far - @near)
@mat_proj[2, 3] = 1.0
@mat_proj[3, 3] = 0.0
@theta = 0.0
end
def update(dt)
@paused = !@paused if @controller.pressed?("Pause")
2021-12-07 00:34:45 -05:00
if @controller.action?("Rotate Right")
2021-12-12 21:33:10 -05:00
@cube.rotation.x = @cube.rotation.x + @speed * dt
2021-12-07 00:34:45 -05:00
end
if @controller.action?("Rotate Left")
2021-12-12 21:33:10 -05:00
@cube.rotation.x = @cube.rotation.x - @speed * dt
2021-12-07 00:34:45 -05:00
end
if @controller.action?("Rotate Up")
2021-12-12 21:33:10 -05:00
@cube.rotation.z = @cube.rotation.z - @speed * dt
2021-12-07 00:34:45 -05:00
end
if @controller.action?("Rotate Down")
2021-12-12 21:33:10 -05:00
@cube.rotation.z = @cube.rotation.z + @speed * dt
2021-12-07 00:34:45 -05:00
end
2021-11-30 23:05:07 -05:00
unless @paused
2021-12-12 21:33:10 -05:00
@cube.rotation.y = @cube.rotation.y + 1.0 * dt
2021-11-30 23:05:07 -05:00
end
2021-12-07 00:34:45 -05:00
@cube.update(dt)
2021-11-30 23:05:07 -05:00
end
def draw
clear(0, 0, 100)
2021-12-07 00:34:45 -05:00
@cube.draw(self, @mat_proj, @camera, @light)
2021-11-30 23:05:07 -05:00
end
end
2021-12-12 21:33:10 -05:00
engine = CubeGame.new(400, 300, 2)
2021-11-30 23:05:07 -05:00
engine.run!