Cool perspective

This commit is contained in:
Carlos Sanchez 2024-07-24 18:42:44 -04:00
parent dab191f7ef
commit d3e27026ef
9 changed files with 6711 additions and 0 deletions

1
tinyrender1_perspective/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
tinyrender1

View File

@ -0,0 +1,5 @@
module tinyrender1
go 1.22.5
require github.com/udhos/gwob v1.0.0

View File

@ -0,0 +1,2 @@
github.com/udhos/gwob v1.0.0 h1:P9br9SLca7H5Hr/WuEcO9+ViUm+wlQnr4vxpPU6c6ww=
github.com/udhos/gwob v1.0.0/go.mod h1:xj4qGbkwL1sTPm1V17NfcIhkgG2rjfb8cUv9YIqE6DE=

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,58 @@
package main
import (
"fmt"
"strings"
)
// Convert rgb to uint
func Col2Uint(r, g, b byte) uint {
return (uint(r) << 16) | (uint(g) << 8) | uint(b)
}
// Convert uint to rgb (in that order)
func Uint2Col(col uint) (byte, byte, byte) {
return byte((col >> 16) & 0xFF), byte((col >> 8) & 0xFF), byte(col & 0xFF)
}
// Color is in ARGB (alpha not used right now)
type Framebuffer struct {
Data []uint
Width uint
Height uint
}
// Create a new framebuffer for the given width and height.
func NewFramebuffer(width uint, height uint) Framebuffer {
return Framebuffer{
Data: make([]uint, width*height),
Width: width,
Height: height,
}
}
// Sure hope this gets inlined...
func (fb *Framebuffer) Set(x uint, y uint, color uint) {
fb.Data[x+y*fb.Width] = color
}
func (fb *Framebuffer) SetSafe(x uint, y uint, color uint) {
if x >= fb.Width || y >= fb.Height {
return
}
fb.Data[x+y*fb.Width] = color
}
// Given some image data, return a string that is the ppm of it
func (fb *Framebuffer) ExportPPM() string {
var result strings.Builder
result.WriteString(fmt.Sprintf("P3\n%d %d\n255\n", fb.Width, fb.Height))
for y := range fb.Height {
for x := range fb.Width {
r, g, b := Uint2Col(fb.Data[x+y*fb.Width])
result.WriteString(fmt.Sprintf("%d %d %d\t", r, g, b))
}
result.WriteRune('\n')
}
return result.String()
}

View File

@ -0,0 +1,93 @@
package main
import (
"flag"
"fmt"
"log"
"os"
"runtime/pprof" // For performance profiling (unnecessary)
)
const (
Width = 512
Height = 512
FOV = 110
NearClip = 0.1
FarClip = 2 // Because the head is so small and close
ObjectFile = "head.obj"
Repeat = 1000
)
func must(err error) {
if err != nil {
panic(err)
}
}
// However flag works... idk
var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file")
func main() {
log.Printf("Program start")
// Little section for doing cpu profiling. I guess that's all you have to do?
flag.Parse()
if *cpuprofile != "" {
log.Printf("CPU profiling requested, write to %s", *cpuprofile)
f, err := os.Create(*cpuprofile)
must(err)
defer f.Close()
err = pprof.StartCPUProfile(f)
must(err)
defer pprof.StopCPUProfile()
}
fb := NewFramebuffer(Width, Height)
log.Printf("Loading obj %s", ObjectFile)
of, err := os.Open(ObjectFile)
must(err)
defer of.Close()
o, err := ParseObj(of)
must(err)
log.Printf("Running render")
halfwidth := float32(fb.Width / 2)
halfheight := float32(fb.Height / 2)
var projection Mat44f
var worldToCamera Mat44f
projection.SetProjection(FOV, NearClip, FarClip)
worldToCamera.SetIdentity()
worldToCamera.Set(2, 3, -1) // Get farther away from the face
var x [3]int
var y [3]int
var c [3]uint
var hi = int(fb.Height - 1)
for range Repeat {
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
// The ONLY difference between this and the non-perspective is this multiplication.
// Is this really all that's required?
fp := worldToCamera.MultiplyPoint3(f[i])
fp = projection.MultiplyPoint3(fp)
x[i] = int((fp.X + 1) * halfwidth)
y[i] = hi - int((fp.Y+1)*halfheight)
c[i] = Col2Uint(byte(0xFF*fp.Z), byte(0xFF*fp.Z), byte(0xFF*fp.Z))
}
Bresenham2(&fb, c[0], x[0], y[0], x[1], y[1])
Bresenham2(&fb, c[1], x[1], y[1], x[2], y[2])
Bresenham2(&fb, c[2], x[2], y[2], x[0], y[0])
}
}
log.Printf("Exporting ppm to stdout")
fmt.Print(fb.ExportPPM())
log.Printf("Program end")
}

View File

@ -0,0 +1,143 @@
package main
// This reads obj files?
import (
"bufio"
"fmt"
"io"
"log"
"math"
"strings"
)
// These should probably go somewhere else but they're here because they're tied
// with the object format specification and I don't want a lot of granularity right now
type Vec3f struct {
X, Y, Z 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
// 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))
}
// 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
}
type Facef [3]Vec3f
type ObjModel struct {
Vertices []Vec3f
Faces []Facef
}
// Parse an obj file at the given reader. Only handles v and f right now
func ParseObj(reader io.Reader) (*ObjModel, error) {
result := ObjModel{
Vertices: make([]Vec3f, 0),
Faces: make([]Facef, 0),
}
breader := bufio.NewReader(reader)
done := false
for !done {
// Scan a line
line, err := breader.ReadString('\n')
if err != nil {
if err == io.EOF {
done = true
} else {
log.Printf("NOT EOF ERR?")
return nil, err
}
}
line = strings.Trim(line, " \t\n\r")
if len(line) == 0 {
continue
}
// Find the first "item", whatever that is. This also gets rid of comments
// since we just don't use lines that start with # (no handler
var t string
_, err = fmt.Sscan(line, &t)
if err != nil {
log.Printf("SSCANF ERR")
return nil, err
}
line = line[len(t):]
if t == "v" {
// Read a vertex, should be just three floats
var vertex Vec3f
_, err := fmt.Sscan(line, &vertex.X, &vertex.Y, &vertex.Z)
if err != nil {
return nil, err
}
result.Vertices = append(result.Vertices, vertex)
} else if t == "f" {
// Read a face; in our example, it's always three sets.
// For THIS example, we throw away those other values
var face Facef
var vi [3]int
var ti int
_, err := fmt.Sscanf(line, "%d/%d/%d %d/%d/%d %d/%d/%d",
&vi[0], &ti, &ti, &vi[1], &ti, &ti, &vi[2], &ti, &ti)
if err != nil {
return nil, err
}
for i := range 3 {
if vi[i] > len(result.Vertices) || vi[i] < 1 {
return nil, fmt.Errorf("Face vertex index out of bounds: %d", vi[i])
}
face[i] = result.Vertices[vi[i]-1]
}
result.Faces = append(result.Faces, face)
}
}
return &result, nil
}

View File

@ -0,0 +1,41 @@
package main
import (
"math"
)
func Bresenham2(fb *Framebuffer, color uint, x0 int, y0 int, x1 int, y1 int) {
dx := int(math.Abs(float64(x1 - x0)))
sx := -1
if x0 < x1 {
sx = 1
}
dy := -int(math.Abs(float64(y1 - y0)))
sy := -1
if y0 < y1 {
sy = 1
}
err := dx + dy
for {
fb.SetSafe(uint(x0), uint(y0), color)
if x0 == x1 && y0 == y1 {
break
}
e2 := 2 * err
if e2 >= dy {
if x0 == x1 {
break
}
err += dy
x0 += sx
}
if e2 <= dx {
if y0 == y1 {
break
}
err += dx
y0 += sy
}
}
}

11
tinyrender1_perspective/run.sh Executable file
View File

@ -0,0 +1,11 @@
#!/bin/sh
if [ $# -ne 1 ]; then
echo "You must pass the basename for the .prof and .ppm"
exit 1
fi
echo "Building"
go build
echo "Running"
./tinyrender1 "-cpuprofile=$1.prof" >"$1.ppm"