New lesson
This commit is contained in:
parent
0d12bc4156
commit
c12f5c9218
2
tinyrender2/.gitignore
vendored
Normal file
2
tinyrender2/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
tinyrender1
|
||||
render
|
3
tinyrender2/go.mod
Normal file
3
tinyrender2/go.mod
Normal file
@ -0,0 +1,3 @@
|
||||
module tinyrender2
|
||||
|
||||
go 1.22.5
|
0
tinyrender2/go.sum
Normal file
0
tinyrender2/go.sum
Normal file
6357
tinyrender2/head.obj
Normal file
6357
tinyrender2/head.obj
Normal file
File diff suppressed because it is too large
Load Diff
58
tinyrender2/image.go
Normal file
58
tinyrender2/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()
|
||||
}
|
86
tinyrender2/main.go
Normal file
86
tinyrender2/main.go
Normal file
@ -0,0 +1,86 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"runtime/pprof" // For performance profiling (unnecessary)
|
||||
)
|
||||
|
||||
const (
|
||||
Width = 512
|
||||
Height = 512
|
||||
ObjectFile = "head.obj"
|
||||
Repeat = 1_000
|
||||
)
|
||||
|
||||
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 x [3]int
|
||||
var y [3]int
|
||||
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
|
||||
x[i] = int((f[i].X + 1) * halfwidth)
|
||||
y[i] = hi - int((f[i].Y+1)*halfheight)
|
||||
}
|
||||
Bresenham2(&fb, 0xFFFFFF, x[0], y[0], x[1], y[1])
|
||||
Bresenham2(&fb, 0xFFFFFF, x[1], y[1], x[2], y[2])
|
||||
Bresenham2(&fb, 0xFFFFFF, x[2], y[2], x[0], y[0])
|
||||
}
|
||||
|
||||
// Just draw a simple line (a million times or something)
|
||||
// // LineDumb5(&fb, 0xFFFFFF, 100, 100, 350, 200)
|
||||
// // LineDumb5(&fb, 0xFF0000, 120, 100, 200, 350)
|
||||
// // LineDumb5(&fb, 0xFF0000, 350, 200, 100, 100) // backward first line
|
||||
// Bresenham2(&fb, 0xFFFFFF, 100, 100, 350, 200)
|
||||
// Bresenham2(&fb, 0xFF0000, 120, 100, 200, 350)
|
||||
// Bresenham2(&fb, 0xFF0000, 350, 200, 100, 100) // backward first line
|
||||
}
|
||||
|
||||
log.Printf("Exporting ppm to stdout")
|
||||
fmt.Print(fb.ExportPPM())
|
||||
|
||||
log.Printf("Program end")
|
||||
}
|
84
tinyrender2/obj.go
Normal file
84
tinyrender2/obj.go
Normal file
@ -0,0 +1,84 @@
|
||||
package main
|
||||
|
||||
// This reads obj files?
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Vec3f struct {
|
||||
X, Y, Z float32
|
||||
}
|
||||
|
||||
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
|
||||
}
|
235
tinyrender2/render.go
Normal file
235
tinyrender2/render.go
Normal file
@ -0,0 +1,235 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"math"
|
||||
)
|
||||
|
||||
// Draw a line using dumb
|
||||
func LineDumb(fb *Framebuffer, color uint, x0 uint, y0 uint, x1 uint, y1 uint) {
|
||||
var t float32
|
||||
for t = 0; t < 1; t += 0.01 {
|
||||
// Very simple interpolation between x and y
|
||||
x := x0 + uint(float32(x1-x0)*t)
|
||||
y := y0 + uint(float32(y1-y0)*t)
|
||||
fb.Set(x, y, color)
|
||||
}
|
||||
}
|
||||
|
||||
func LineDumb2(fb *Framebuffer, color uint, x0 uint, y0 uint, x1 uint, y1 uint) {
|
||||
for x := x0; x < x1; x++ {
|
||||
// For each pixel across, compute how far across we are and interpolate y
|
||||
t := float32(x-x0) / float32(x1-x0)
|
||||
y := uint(float32(y0)*(1-t) + float32(y1)*t)
|
||||
fb.Set(x, y, color)
|
||||
}
|
||||
}
|
||||
|
||||
func LineDumb3(fb *Framebuffer, color uint, x0 int, y0 int, x1 int, y1 int) {
|
||||
steep := false
|
||||
// This one makes sure that going pixel by pixel will not make holes in the
|
||||
// other direction by always moving in the less steep direction.
|
||||
if math.Abs(float64(x0-x1)) < math.Abs(float64(y0-y1)) {
|
||||
x0, y0 = y0, x0
|
||||
x1, y1 = y1, x1
|
||||
steep = true
|
||||
}
|
||||
// Don't let lines be invisible, always go left to right
|
||||
if x0 > x1 { // need to be left to right
|
||||
x0, x1 = x1, x0
|
||||
y0, y1 = y1, y0 // Why are we swapping these? I guess because we're swapping the points...
|
||||
}
|
||||
// Same as dumb2, going across pixel by pixel. But "x" here might actually be y
|
||||
// because of the inverted direction
|
||||
for x := x0; x <= x1; x++ {
|
||||
t := float32(x-x0) / float32(x1-x0)
|
||||
y := uint(float32(y0)*(1-t) + float32(y1)*t)
|
||||
if steep {
|
||||
fb.Set(y, uint(x), color)
|
||||
} else {
|
||||
fb.Set(uint(x), y, color)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func LineDumb4(fb *Framebuffer, color uint, x0 int, y0 int, x1 int, y1 int) {
|
||||
steep := false
|
||||
// This one makes sure that going pixel by pixel will not make holes in the
|
||||
// other direction by always moving in the less steep direction.
|
||||
if math.Abs(float64(x1-x0)) < math.Abs(float64(y1-y0)) {
|
||||
x0, y0 = y0, x0
|
||||
x1, y1 = y1, x1
|
||||
steep = true
|
||||
}
|
||||
// Don't let lines be invisible, always go left to right
|
||||
if x0 > x1 { // need to be left to right
|
||||
x0, x1 = x1, x0
|
||||
y0, y1 = y1, y0 // Why are we swapping these? I guess because we're swapping the points...
|
||||
}
|
||||
dx := x1 - x0 // These MUST be POST calcs
|
||||
dy := y1 - y0
|
||||
// Delta error or something? Basically, as we move in one direction, we get farther
|
||||
// and farther off course if we never move in the OTHER direction. This is the amount
|
||||
// of that per x direction.
|
||||
derror := math.Abs(float64(dy) / float64(dx))
|
||||
y := uint(y0)
|
||||
var err float64
|
||||
// Same as dumb2, going across pixel by pixel. But "x" here might actually be y
|
||||
// because of the inverted direction
|
||||
for x := x0; x <= x1; x++ {
|
||||
if steep {
|
||||
fb.Set(y, uint(x), color)
|
||||
} else {
|
||||
fb.Set(uint(x), y, color)
|
||||
}
|
||||
err += derror
|
||||
if err > 0.5 {
|
||||
if y1 > y0 {
|
||||
y += 1
|
||||
} else {
|
||||
y = y - 1
|
||||
}
|
||||
err -= 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For not-steep lines
|
||||
func BresenhamLow(fb *Framebuffer, color uint, x0 int, y0 int, x1 int, y1 int) {
|
||||
dx := x1 - x0
|
||||
dy := y1 - y0
|
||||
yi := 1 // This is the increment
|
||||
if dy < 0 { // Line moves in opposite vertical direction
|
||||
yi = -1
|
||||
dy = -dy
|
||||
}
|
||||
d := 2*dy - dx
|
||||
// Why minus dx? if slope is 0 then d is -dx. If slope is 1 then d = dy.
|
||||
// I don't know...
|
||||
y := y0
|
||||
x1u := uint(x1)
|
||||
for x := uint(x0); x <= x1u; x++ {
|
||||
fb.Set(x, uint(y), color)
|
||||
if d > 0 {
|
||||
y = y + yi
|
||||
d = d + 2*(dy-dx)
|
||||
} else {
|
||||
d = d + 2*dy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For steep lines
|
||||
func BresenhamHigh(fb *Framebuffer, color uint, x0 int, y0 int, x1 int, y1 int) {
|
||||
dx := x1 - x0
|
||||
dy := y1 - y0
|
||||
xi := 1 // This is the increment
|
||||
if dx < 0 { // Line moves in opposite vertical direction
|
||||
xi = -1
|
||||
dx = -dx
|
||||
}
|
||||
d := 2*dx - dy
|
||||
// Why minus dx? if slope is 0 then d is -dx. If slope is 1 then d = dy.
|
||||
// I don't know...
|
||||
x := x0
|
||||
y1u := uint(y1)
|
||||
for y := uint(y0); y <= y1u; y++ {
|
||||
fb.Set(uint(x), y, color)
|
||||
if d > 0 {
|
||||
x = x + xi
|
||||
d = d + 2*(dx-dy)
|
||||
} else {
|
||||
d = d + 2*dx
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Bresenham(fb *Framebuffer, color uint, x0 int, y0 int, x1 int, y1 int) {
|
||||
if math.Abs(float64(y1-y0)) < math.Abs(float64(x1-x0)) { // This is a flatter line
|
||||
if x0 > x1 { // This line is backwards
|
||||
BresenhamLow(fb, color, x1, y1, x0, y0)
|
||||
} else {
|
||||
BresenhamLow(fb, color, x0, y0, x1, y1)
|
||||
}
|
||||
} else { // This is a steep line
|
||||
if y0 > y1 { // This line is backwards
|
||||
BresenhamHigh(fb, color, x1, y1, x0, y0)
|
||||
} else {
|
||||
BresenhamHigh(fb, color, x0, y0, x1, y1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func LineDumb5(fb *Framebuffer, color uint, x0 int, y0 int, x1 int, y1 int) {
|
||||
steep := false
|
||||
// This one makes sure that going pixel by pixel will not make holes in the
|
||||
// other direction by always moving in the less steep direction.
|
||||
if math.Abs(float64(x1-x0)) < math.Abs(float64(y1-y0)) {
|
||||
x0, y0 = y0, x0
|
||||
x1, y1 = y1, x1
|
||||
steep = true
|
||||
}
|
||||
// Don't let lines be invisible, always go left to right
|
||||
if x0 > x1 { // need to be left to right
|
||||
x0, x1 = x1, x0
|
||||
y0, y1 = y1, y0 // Why are we swapping these? I guess because we're swapping the points...
|
||||
}
|
||||
dx := x1 - x0 // These MUST be POST calcs
|
||||
dy := y1 - y0
|
||||
derror2 := int(math.Abs(float64(dy))) * 2
|
||||
error2 := 0
|
||||
y := y0
|
||||
sy := 1
|
||||
if y1 <= y0 {
|
||||
sy = -1
|
||||
}
|
||||
// Same as dumb2, going across pixel by pixel. But "x" here might actually be y
|
||||
// because of the inverted direction
|
||||
for x := x0; x <= x1; x++ {
|
||||
if steep {
|
||||
fb.Set(uint(y), uint(x), color)
|
||||
} else {
|
||||
fb.Set(uint(x), uint(y), color)
|
||||
}
|
||||
error2 += derror2
|
||||
if error2 > dx {
|
||||
y = y + sy
|
||||
error2 -= dx * 2
|
||||
}
|
||||
}
|
||||
}
|
11
tinyrender2/run.sh
Executable file
11
tinyrender2/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 -o render
|
||||
echo "Running"
|
||||
./render "-cpuprofile=$1.prof" >"$1.ppm"
|
Loading…
Reference in New Issue
Block a user