Cool perspective
This commit is contained in:
parent
dab191f7ef
commit
d3e27026ef
1
tinyrender1_perspective/.gitignore
vendored
Normal file
1
tinyrender1_perspective/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
tinyrender1
|
5
tinyrender1_perspective/go.mod
Normal file
5
tinyrender1_perspective/go.mod
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
module tinyrender1
|
||||||
|
|
||||||
|
go 1.22.5
|
||||||
|
|
||||||
|
require github.com/udhos/gwob v1.0.0
|
2
tinyrender1_perspective/go.sum
Normal file
2
tinyrender1_perspective/go.sum
Normal 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=
|
6357
tinyrender1_perspective/head.obj
Normal file
6357
tinyrender1_perspective/head.obj
Normal file
File diff suppressed because it is too large
Load Diff
58
tinyrender1_perspective/image.go
Normal file
58
tinyrender1_perspective/image.go
Normal 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()
|
||||||
|
}
|
93
tinyrender1_perspective/main.go
Normal file
93
tinyrender1_perspective/main.go
Normal 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")
|
||||||
|
}
|
143
tinyrender1_perspective/obj.go
Normal file
143
tinyrender1_perspective/obj.go
Normal 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
|
||||||
|
}
|
41
tinyrender1_perspective/render.go
Normal file
41
tinyrender1_perspective/render.go
Normal 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
11
tinyrender1_perspective/run.sh
Executable 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"
|
Loading…
Reference in New Issue
Block a user