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