From 1345fb8fb2a3ff546e7243f916e7fe38f3f041c8 Mon Sep 17 00:00:00 2001 From: Carlos Sanchez Date: Sun, 28 Jul 2024 22:28:35 -0400 Subject: [PATCH] Working re-implemented semi-movement --- tinyrender4/.gitignore | 1 + tinyrender4/animation.sh | 24 +++++++++++++ tinyrender4/main.go | 26 +++++++++++--- tinyrender4/obj.go | 76 ++++++++++++++++++++++++++++++++++++++++ tinyrender4/render.go | 3 ++ 5 files changed, 125 insertions(+), 5 deletions(-) create mode 100755 tinyrender4/animation.sh diff --git a/tinyrender4/.gitignore b/tinyrender4/.gitignore index 1c34034..4292a60 100644 --- a/tinyrender4/.gitignore +++ b/tinyrender4/.gitignore @@ -1,2 +1,3 @@ tinyrender1 +tinyrender4 render diff --git a/tinyrender4/animation.sh b/tinyrender4/animation.sh new file mode 100755 index 0000000..4b5dd1a --- /dev/null +++ b/tinyrender4/animation.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +if [ $# -ne 1 ]; then + echo "You must pass the basename for the animation folder" + exit 1 +fi + +echo "Building" +go build -o render +mkdir -p $1 + +echo "Running" + +frame=0 +for x in $(seq -3 0.1 3); do + ff=$(printf "%03d" $frame) + ./render "-repeat=1" "-xofs=$x" "-zofs=-1.8" "-fov=70" "-p6file=$1/$ff.ppm" + frame=$((frame + 1)) +done + +echo "Converting animation" + +cd $1 +convert -delay 5 -loop 0 *.ppm -resize 256x256 anim.gif diff --git a/tinyrender4/main.go b/tinyrender4/main.go index fc461b3..5de7c1f 100644 --- a/tinyrender4/main.go +++ b/tinyrender4/main.go @@ -16,9 +16,10 @@ import ( const ( Width = 512 Height = 512 + NearClip = 0.1 + FarClip = 5 // Because the head is so small and close ObjectFile = "head.obj" TextureFile = "head.jpg" - Repeat = 500 ) func must(err error) { @@ -31,6 +32,10 @@ func must(err error) { var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file") var dozbuf = flag.Bool("zbuffer", false, "Write zbuffer instead of image") var p6file = flag.String("p6file", "", "Output binary ppm to given file instead") +var fov = flag.Float64("fov", 90, "Horizontal FOV in degrees") +var xofs = flag.Float64("xofs", 0, "Offset image by x") +var zofs = flag.Float64("zofs", -1.5, "Offset image by z (should be negative)") +var repeat = flag.Int("repeat", 60, "Amount of times to repeat render") // var zcuthigh = flag.Float64("zcuthigh", math.MaxFloat32, "High cutoff for z (values above this will be removed)") // var zcutlow = flag.Float64("zcutlow", -math.MaxFloat32, "Low cutoff for z (values below are removed)") @@ -71,22 +76,33 @@ func main() { light := Vec3f{0, 0, -1} + var projection Mat44f + var worldToCamera Mat44f + projection.SetProjection(float32(*fov), NearClip, FarClip) + worldToCamera.SetTranslation(float32(*xofs), 0, float32(*zofs)) + + // Premultiply all the translation/etc matrices. Why do we do world to camera THEN + // projection? I guess that makes sense actually, oops... projection is the last step. + screenmat := worldToCamera.Multiply(&projection) + halfwidth := float32(fb.Width / 2) halfheight := float32(fb.Height / 2) var sc [3]Vertex var hi = float32(fb.Height - 1) - for range Repeat { + for range *repeat { fb.ResetZBuffer() for _, f := range o.Faces { // Precompute perspective for vertices to save time. Notice Z // is not considered: is this orthographic projection? Yeah probably... for i := range 3 { // Triangles, bro + fp := screenmat.MultiplyPoint3(f[i].Pos) sc[i] = f[i] - sc[i].Pos.X = (f[i].Pos.X + 1) * halfwidth - sc[i].Pos.Y = hi - (f[i].Pos.Y+1)*halfheight + sc[i].Pos.X = (fp.X + 1) * halfwidth + sc[i].Pos.Y = hi - (fp.Y+1)*halfheight + sc[i].Pos.Z = fp.Z // NOTE: WE USE NEGATIVE Z BECAUSE IT'S SUPPOSED TO BE DISTANCE! AS-IS, CLOSER // POINTS HAVE HIGHER Z VLAUES - sc[i].Pos.Z = -f[i].Pos.Z // Pull Z value directly. This is fine, our z-buffer is currently float32 + // sc[i].Pos.Z = -fp.Z // Pull Z value directly. This is fine, our z-buffer is currently float32 } l1 := f[2].Pos.Sub(f[0].Pos) diff --git a/tinyrender4/obj.go b/tinyrender4/obj.go index 12bb1c0..2ba8970 100644 --- a/tinyrender4/obj.go +++ b/tinyrender4/obj.go @@ -22,6 +22,82 @@ type Vec2f struct { X, Y float32 } +// A ROW MAJOR matrix +type Mat44f [16]float32 + +func (m *Mat44f) Set(x int, y int, val float32) { + m[x+y*4] = val +} +func (m *Mat44f) Get(x int, y int) float32 { + return m[x+y*4] +} +func (m *Mat44f) ZeroFill() { + for i := range m { + m[i] = 0 + } +} +func (m *Mat44f) SetIdentity() { + m.ZeroFill() + for i := range 4 { + m.Set(i, i, 1) + } +} + +// Compute the projection matrix, filling the given matrix. FOV is in degrees +func (m *Mat44f) SetProjection(fov float32, near float32, far float32) { + // Projection matrix is (ROW MAJOR!) + // S 0 0 0 + // 0 S 0 0 + // 0 0 -f/(f-n) -1 + // 0 0 -fn/(f-n) 0 + // where S (scale) is 1 / tan(fov / 2) (assuming fov is radians) + m.ZeroFill() + scale := float32(1 / math.Tan(float64(fov)*0.5*math.Pi/180)) + m.Set(0, 0, scale) + m.Set(1, 1, scale) + m.Set(2, 2, -far/(far-near)) + m.Set(3, 2, -1) + m.Set(2, 3, -far*near/(far-near)) +} + +func (m *Mat44f) SetTranslation(x, y, z float32) { + m.SetIdentity() + m.Set(0, 3, x) // Let user decide how to offset x + m.Set(1, 3, y) // Let user decide how to offset x + m.Set(2, 3, z) // Get farther away from the face (user) +} + +// Multiply the given point by our vector. Remember this is row-major order +func (m *Mat44f) MultiplyPoint3(p Vec3f) Vec3f { + var out Vec3f + // We hope very much that Go will optimize the function calls for us, + // along with computing the constants. + out.X = p.X*m.Get(0, 0) + p.Y*m.Get(0, 1) + p.Z*m.Get(0, 2) + m.Get(0, 3) + out.Y = p.X*m.Get(1, 0) + p.Y*m.Get(1, 1) + p.Z*m.Get(1, 2) + m.Get(1, 3) + out.Z = p.X*m.Get(2, 0) + p.Y*m.Get(2, 1) + p.Z*m.Get(2, 2) + m.Get(2, 3) + w := p.X*m.Get(3, 0) + p.Y*m.Get(3, 1) + p.Z*m.Get(3, 2) + m.Get(3, 3) + if w != 1 { + out.X /= w + out.Y /= w + out.Z /= w + } + return out +} + +// Multiply two 4x4 matrices together (not optimized) +func (m *Mat44f) Multiply(m2 *Mat44f) Mat44f { + var result Mat44f + // This is the x and y of our resulting matrix + for y := 0; y < 4; y++ { + for x := 0; x < 4; x++ { + for i := 0; i < 4; i++ { + result[x+y*4] += m[i+y*4] * m2[x+i*4] + } + } + } + return result +} + // A single vertex generally has multiple items associated with it // when it's part of a face. type Vertex struct { diff --git a/tinyrender4/render.go b/tinyrender4/render.go index 284ed12..6216fc1 100644 --- a/tinyrender4/render.go +++ b/tinyrender4/render.go @@ -275,6 +275,9 @@ func Triangle3t(fb *Framebuffer, texture *Framebuffer, intensity float32, v0v Ve v1 := v1v.Pos.ToVec2i() v2 := v2v.Pos.ToVec2i() boundsTL, boundsBR := ComputeBoundingBox(v0, v1, v2) + if boundsBR.Y < 0 || boundsBR.X < 0 || boundsTL.X >= int(fb.Width) || boundsTL.Y >= int(fb.Height) { + return + } if boundsTL.Y < 0 { boundsTL.Y = 0 }