Chapter 1 and 2
This commit is contained in:
commit
5e0452ff48
18 changed files with 41905 additions and 0 deletions
1
.gitignore
vendored
Executable file
1
.gitignore
vendored
Executable file
|
|
@ -0,0 +1 @@
|
||||||
|
.idea/
|
||||||
8
README.md
Executable file
8
README.md
Executable file
|
|
@ -0,0 +1,8 @@
|
||||||
|
# The Ray Tracer Challenge with Go
|
||||||
|
|
||||||
|
This porject is similar to my other [ray tracer repository](https://github.com/ItsAMirko/TheRayTracerChallenge). I
|
||||||
|
wanted to learn a new programming language and thought that starting with
|
||||||
|
[The Ray Tracer Challenge](https://www.amazon.de/Ray-Tracer-Challenge-Test-Driven-Renderer/dp/1680502719) by
|
||||||
|
[Jamis Buck](https://twitter.com/jamis) might be a good idea.
|
||||||
|
|
||||||
|
Don't expect me to finish the complete book but let's see how far I get this time :D
|
||||||
37
chapters/chapter1/main.go
Executable file
37
chapters/chapter1/main.go
Executable file
|
|
@ -0,0 +1,37 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
. "raytracer/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Projectile struct {
|
||||||
|
Position Point
|
||||||
|
Velocity Vector
|
||||||
|
}
|
||||||
|
|
||||||
|
type Environment struct {
|
||||||
|
Gravity Vector
|
||||||
|
Wind Vector
|
||||||
|
}
|
||||||
|
|
||||||
|
func (proj *Projectile) tick(env Environment) {
|
||||||
|
proj.Position = proj.Position.AddVector(proj.Velocity)
|
||||||
|
proj.Velocity = proj.Velocity.AddVector(env.Gravity).AddVector(env.Wind)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
startPosition := CreatePoint(0, 1, 0)
|
||||||
|
startVelocity := CreateVector(1, 1, 0).Normalize()
|
||||||
|
gravity := CreateVector(0, -0.1, 0)
|
||||||
|
wind := CreateVector(-0.01, 0, 0)
|
||||||
|
|
||||||
|
projectile := Projectile{startPosition, startVelocity}
|
||||||
|
environment := Environment{gravity, wind}
|
||||||
|
|
||||||
|
for projectile.Position.Y() > 0 {
|
||||||
|
fmt.Printf("%.4f | %.4f\n", projectile.Position.X(), projectile.Position.Y())
|
||||||
|
|
||||||
|
projectile.tick(environment)
|
||||||
|
}
|
||||||
|
}
|
||||||
52
chapters/chapter2/main.go
Executable file
52
chapters/chapter2/main.go
Executable file
|
|
@ -0,0 +1,52 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"os"
|
||||||
|
. "raytracer/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Projectile struct {
|
||||||
|
Position Point
|
||||||
|
Velocity Vector
|
||||||
|
}
|
||||||
|
|
||||||
|
type Environment struct {
|
||||||
|
Gravity Vector
|
||||||
|
Wind Vector
|
||||||
|
}
|
||||||
|
|
||||||
|
func (proj *Projectile) tick(env Environment) {
|
||||||
|
proj.Position = proj.Position.AddVector(proj.Velocity)
|
||||||
|
proj.Velocity = proj.Velocity.AddVector(env.Gravity).AddVector(env.Wind)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
startPosition := CreatePoint(0, 1, 0)
|
||||||
|
startVelocity := CreateVector(1, 1.8, 0).Normalize().Multiply(8)
|
||||||
|
gravity := CreateVector(0, -0.05, 0)
|
||||||
|
wind := CreateVector(-0.005, 0, 0)
|
||||||
|
|
||||||
|
projectile := Projectile{startPosition, startVelocity}
|
||||||
|
environment := Environment{gravity, wind}
|
||||||
|
canvas := CreateCanvas(900, 500)
|
||||||
|
|
||||||
|
for projectile.Position.Y() > 0 {
|
||||||
|
canvas.SetPixel(uint64(math.Round(projectile.Position.X())),
|
||||||
|
uint64(500-math.Round(projectile.Position.Y())),
|
||||||
|
CreateColor(0, 1, 0))
|
||||||
|
|
||||||
|
projectile.tick(environment)
|
||||||
|
}
|
||||||
|
|
||||||
|
path, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.WriteFile(path+"/chapters/chapter2/test.ppm", []byte(canvas.ToPPM()), 0644)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
41003
chapters/chapter2/test.ppm
Executable file
41003
chapters/chapter2/test.ppm
Executable file
File diff suppressed because it is too large
Load diff
3
go.mod
Executable file
3
go.mod
Executable file
|
|
@ -0,0 +1,3 @@
|
||||||
|
module raytracer
|
||||||
|
|
||||||
|
go 1.17
|
||||||
53
model/canvas.go
Executable file
53
model/canvas.go
Executable file
|
|
@ -0,0 +1,53 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Canvas struct {
|
||||||
|
width uint64
|
||||||
|
height uint64
|
||||||
|
pixels [][]Color
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateCanvas(width uint64, height uint64) Canvas {
|
||||||
|
pixels := make([][]Color, width)
|
||||||
|
for i := uint64(0); i < width; i++ {
|
||||||
|
pixels[i] = make([]Color, height)
|
||||||
|
for j := uint64(0); j < height; j++ {
|
||||||
|
pixels[i][j] = CreateColor(0, 0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Canvas{width: width, height: height, pixels: pixels}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Canvas) SetPixel(x uint64, y uint64, color Color) {
|
||||||
|
if x >= c.width || y >= c.height {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.pixels[x][y] = color
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Canvas) ToPPM() string {
|
||||||
|
header := "P3\n" +
|
||||||
|
strconv.FormatInt(int64(c.width), 10) + " " +
|
||||||
|
strconv.FormatInt(int64(c.height), 10) +
|
||||||
|
"\n255\n"
|
||||||
|
|
||||||
|
body, line := "", ""
|
||||||
|
for j := uint64(0); j < c.height; j++ {
|
||||||
|
for i := uint64(0); i < c.width; i++ {
|
||||||
|
if len(line+c.pixels[i][j].ToHexString()) > 70 {
|
||||||
|
body = body + line + "\n"
|
||||||
|
line = ""
|
||||||
|
}
|
||||||
|
line = line + c.pixels[i][j].ToHexString() + " "
|
||||||
|
}
|
||||||
|
body = body + line + "\n"
|
||||||
|
line = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return header + body
|
||||||
|
}
|
||||||
92
model/canvas_test.go
Executable file
92
model/canvas_test.go
Executable file
|
|
@ -0,0 +1,92 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCanCreateACanvas(t *testing.T) {
|
||||||
|
canvas := CreateCanvas(10, 20)
|
||||||
|
|
||||||
|
if canvas.width != 10 {
|
||||||
|
t.Error("Expected for width:", 10, "Got:", canvas.width)
|
||||||
|
}
|
||||||
|
if canvas.height != 20 {
|
||||||
|
t.Error("Expected for height:", 20, "Got:", canvas.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := CreateColor(0, 0, 0)
|
||||||
|
var actual Color
|
||||||
|
var i, j uint64
|
||||||
|
|
||||||
|
for i = 0; i < 10; i++ {
|
||||||
|
for j = 0; j < 20; j++ {
|
||||||
|
actual = canvas.pixels[i][j]
|
||||||
|
if actual.EqualTo(expected) == false {
|
||||||
|
t.Error("Iteration:", i, "Expected:", expected, "Got:", actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCanSetAPixel(t *testing.T) {
|
||||||
|
canvas := CreateCanvas(1, 1)
|
||||||
|
|
||||||
|
expected := CreateColor(0, 0, 0)
|
||||||
|
canvas.SetPixel(0, 0, expected)
|
||||||
|
canvas.SetPixel(1, 1, expected)
|
||||||
|
actual := canvas.pixels[0][0]
|
||||||
|
|
||||||
|
if actual.EqualTo(expected) == false {
|
||||||
|
t.Error("Expected:", expected, "Got:", actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCanCreatePPMFileContent(t *testing.T) {
|
||||||
|
canvas := CreateCanvas(5, 3)
|
||||||
|
canvas.SetPixel(0, 0, CreateColor(1.5, 0, 0))
|
||||||
|
canvas.SetPixel(2, 1, CreateColor(0, 0.5, 0))
|
||||||
|
canvas.SetPixel(4, 2, CreateColor(-0.5, 0, 1))
|
||||||
|
|
||||||
|
expectedArray := []string{
|
||||||
|
"P3",
|
||||||
|
"5 3",
|
||||||
|
"255",
|
||||||
|
"255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ",
|
||||||
|
"0 0 0 0 0 0 0 128 0 0 0 0 0 0 0 ",
|
||||||
|
"0 0 0 0 0 0 0 0 0 0 0 0 0 0 255 ",
|
||||||
|
"",
|
||||||
|
}
|
||||||
|
actualArray := strings.Split(strings.ReplaceAll(canvas.ToPPM(), "\r\n", "\n"), "\n")
|
||||||
|
|
||||||
|
for index, actual := range actualArray {
|
||||||
|
expected := expectedArray[index]
|
||||||
|
if expected != actual {
|
||||||
|
t.Error("Index:", index, "Expected:", expected, "Got:", actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRespectsMaxCharsPerLineWhenCreationPPMFileContent(t *testing.T) {
|
||||||
|
canvas := CreateCanvas(10, 1)
|
||||||
|
for x := 0; x < 10; x++ {
|
||||||
|
canvas.SetPixel(uint64(x), 0, CreateColor(1, 0.66, 0.33))
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedArray := []string{
|
||||||
|
"P3",
|
||||||
|
"10 1",
|
||||||
|
"255",
|
||||||
|
"255 168 84 255 168 84 255 168 84 255 168 84 255 168 84 255 168 84 ",
|
||||||
|
"255 168 84 255 168 84 255 168 84 255 168 84 ",
|
||||||
|
"",
|
||||||
|
}
|
||||||
|
actualArray := strings.Split(strings.ReplaceAll(canvas.ToPPM(), "\r\n", "\n"), "\n")
|
||||||
|
|
||||||
|
for index, actual := range actualArray {
|
||||||
|
expected := expectedArray[index]
|
||||||
|
if expected != actual {
|
||||||
|
t.Error("Index:", index, "Expected:", expected, "Got:", actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
69
model/color.go
Executable file
69
model/color.go
Executable file
|
|
@ -0,0 +1,69 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Color struct {
|
||||||
|
red float64
|
||||||
|
green float64
|
||||||
|
blue float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateColor(red float64, green float64, blue float64) Color {
|
||||||
|
return Color{red: red, green: green, blue: blue}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Color) AddColor(c2 Color) Color {
|
||||||
|
return CreateColor(
|
||||||
|
c.red+c2.red,
|
||||||
|
c.green+c2.green,
|
||||||
|
c.blue+c2.blue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Color) SubtractColor(c2 Color) Color {
|
||||||
|
return CreateColor(
|
||||||
|
c.red-c2.red,
|
||||||
|
c.green-c2.green,
|
||||||
|
c.blue-c2.blue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Color) Multiply(multiplier float64) Color {
|
||||||
|
return CreateColor(
|
||||||
|
c.red*multiplier,
|
||||||
|
c.green*multiplier,
|
||||||
|
c.blue*multiplier)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Color) MultiplyColor(c2 Color) Color {
|
||||||
|
return CreateColor(
|
||||||
|
c.red*c2.red,
|
||||||
|
c.green*c2.green,
|
||||||
|
c.blue*c2.blue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Color) ToHexString() string {
|
||||||
|
red := calcHexValue(c.red)
|
||||||
|
green := calcHexValue(c.green)
|
||||||
|
blue := calcHexValue(c.blue)
|
||||||
|
|
||||||
|
return strconv.Itoa(red) + " " + strconv.Itoa(green) + " " + strconv.Itoa(blue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Color) EqualTo(c2 Color) bool {
|
||||||
|
return round(c.red, 8) == round(c2.red, 8) &&
|
||||||
|
round(c.green, 8) == round(c2.green, 8) &&
|
||||||
|
round(c.blue, 8) == round(c2.blue, 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
func calcHexValue(floatValue float64) int {
|
||||||
|
hex := math.Round(floatValue * 255)
|
||||||
|
if hex > 255 {
|
||||||
|
hex = 255
|
||||||
|
} else if hex < 0 {
|
||||||
|
hex = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return int(hex)
|
||||||
|
}
|
||||||
86
model/color_test.go
Executable file
86
model/color_test.go
Executable file
|
|
@ -0,0 +1,86 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCanCreateAColor(t *testing.T) {
|
||||||
|
color := CreateColor(0.1, 0.2, 0.3)
|
||||||
|
|
||||||
|
if color.red != 0.1 {
|
||||||
|
t.Error("Expected for red:", 0.1, "Got:", color.red)
|
||||||
|
}
|
||||||
|
if color.green != 0.2 {
|
||||||
|
t.Error("Expected for green:", 0.2, "Got:", color.green)
|
||||||
|
}
|
||||||
|
if color.blue != 0.3 {
|
||||||
|
t.Error("Expected for blue:", 0.3, "Got:", color.blue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCanAddUpTwoColors(t *testing.T) {
|
||||||
|
color1 := CreateColor(0.9, 0.6, 0.75)
|
||||||
|
color2 := CreateColor(0.7, 0.1, 0.25)
|
||||||
|
|
||||||
|
expected := CreateColor(1.6, 0.7, 1)
|
||||||
|
actual := color1.AddColor(color2)
|
||||||
|
|
||||||
|
if actual.EqualTo(expected) == false {
|
||||||
|
t.Error("Expected:", expected, "Got:", actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCanSubtractTwoColors(t *testing.T) {
|
||||||
|
color1 := CreateColor(0.9, 0.6, 0.75)
|
||||||
|
color2 := CreateColor(0.7, 0.1, 0.25)
|
||||||
|
|
||||||
|
expected := CreateColor(0.2, 0.5, 0.5)
|
||||||
|
actual := color1.SubtractColor(color2)
|
||||||
|
|
||||||
|
if actual.EqualTo(expected) == false {
|
||||||
|
t.Error("Expected:", expected, "Got:", actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCanMultiplyAColorByAScalar(t *testing.T) {
|
||||||
|
color1 := CreateColor(0.2, 0.3, 0.4)
|
||||||
|
scalar := 2.0
|
||||||
|
|
||||||
|
expected := CreateColor(0.4, 0.6, 0.8)
|
||||||
|
actual := color1.Multiply(scalar)
|
||||||
|
|
||||||
|
if actual.EqualTo(expected) == false {
|
||||||
|
t.Error("Expected:", expected, "Got:", actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCanMultiplyAColorByAColor(t *testing.T) {
|
||||||
|
color1 := CreateColor(1, 0.2, 0.4)
|
||||||
|
color2 := CreateColor(0.9, 1, 0.1)
|
||||||
|
|
||||||
|
expected := CreateColor(0.9, 0.2, 0.04)
|
||||||
|
actual := color1.MultiplyColor(color2)
|
||||||
|
|
||||||
|
if actual.EqualTo(expected) == false {
|
||||||
|
t.Error("Expected:", expected, "Got:", actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCanBeConvertedToHexString(t *testing.T) {
|
||||||
|
color1 := CreateColor(1, 0.5, 0)
|
||||||
|
color2 := CreateColor(-1, 0.75, 2)
|
||||||
|
|
||||||
|
expected1 := "255 128 0"
|
||||||
|
actual1 := color1.ToHexString()
|
||||||
|
|
||||||
|
if expected1 != actual1 {
|
||||||
|
t.Error("Expected:", expected1, "Got:", actual1)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected2 := "0 191 255"
|
||||||
|
actual2 := color2.ToHexString()
|
||||||
|
|
||||||
|
if expected2 != actual2 {
|
||||||
|
t.Error("Expected:", expected2, "Got:", actual2)
|
||||||
|
}
|
||||||
|
}
|
||||||
7
model/helper.go
Executable file
7
model/helper.go
Executable file
|
|
@ -0,0 +1,7 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import "math"
|
||||||
|
|
||||||
|
func round(valued float64, floatingPoints int) float64 {
|
||||||
|
return math.Round(valued*math.Pow10(floatingPoints)) / math.Pow10(floatingPoints)
|
||||||
|
}
|
||||||
30
model/helper_test.go
Executable file
30
model/helper_test.go
Executable file
|
|
@ -0,0 +1,30 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCanRoundFloatValue(t *testing.T) {
|
||||||
|
float := 1.23456789
|
||||||
|
|
||||||
|
var actual float64
|
||||||
|
var expectedValues [10]float64
|
||||||
|
expectedValues[0] = 1
|
||||||
|
expectedValues[1] = 1.2
|
||||||
|
expectedValues[2] = 1.23
|
||||||
|
expectedValues[3] = 1.235
|
||||||
|
expectedValues[4] = 1.2346
|
||||||
|
expectedValues[5] = 1.23457
|
||||||
|
expectedValues[6] = 1.234568
|
||||||
|
expectedValues[7] = 1.2345679
|
||||||
|
expectedValues[8] = 1.23456789
|
||||||
|
expectedValues[9] = 1.23456789
|
||||||
|
|
||||||
|
for i := 0; i <= 9; i++ {
|
||||||
|
actual = round(float, i)
|
||||||
|
|
||||||
|
if actual != expectedValues[i] {
|
||||||
|
t.Error("Iteration:", i, "Expected:", expectedValues[i], "Got:", actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
model/point.go
Executable file
28
model/point.go
Executable file
|
|
@ -0,0 +1,28 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
type Point struct {
|
||||||
|
*Tuple
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreatePoint(x float64, y float64, z float64) Point {
|
||||||
|
return Point{&Tuple{x: x, y: y, z: z, w: 1}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Point) SubtractPoint(p2 Point) Vector {
|
||||||
|
return CreateVector(p.x-p2.x, p.y-p2.y, p.z-p2.z)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Point) AddVector(v Vector) Point {
|
||||||
|
return CreatePoint(p.x+v.x, p.y+v.y, p.z+v.z)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Point) SubtractVector(v Vector) Point {
|
||||||
|
return CreatePoint(p.x-v.x, p.y-v.y, p.z-v.z)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Point) EqualTo(p2 Point) bool {
|
||||||
|
return round(p.x, 8) == round(p2.x, 8) &&
|
||||||
|
round(p.y, 8) == round(p2.y, 8) &&
|
||||||
|
round(p.z, 8) == round(p2.z, 8) &&
|
||||||
|
round(p.w, 8) == round(p2.w, 8)
|
||||||
|
}
|
||||||
58
model/point_test.go
Executable file
58
model/point_test.go
Executable file
|
|
@ -0,0 +1,58 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCanCreateAPoint(t *testing.T) {
|
||||||
|
point := CreatePoint(4, -4, 3)
|
||||||
|
|
||||||
|
if point.x != 4 {
|
||||||
|
t.Error("Expected for x:", 4, "Got:", point.x)
|
||||||
|
}
|
||||||
|
if point.y != -4 {
|
||||||
|
t.Error("Expected for y:", -4, "Got:", point.y)
|
||||||
|
}
|
||||||
|
if point.z != 3 {
|
||||||
|
t.Error("Expected for z:", 3, "Got:", point.z)
|
||||||
|
}
|
||||||
|
if point.IsPoint() != true {
|
||||||
|
t.Error("Expected a point")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCanSubtractTwoPoints(t *testing.T) {
|
||||||
|
point1 := CreatePoint(3, 2, 1)
|
||||||
|
point2 := CreatePoint(5, 6, 7)
|
||||||
|
|
||||||
|
expected := CreateVector(-2, -4, -6)
|
||||||
|
actual := point1.SubtractPoint(point2)
|
||||||
|
|
||||||
|
if actual.EqualTo(expected) == false {
|
||||||
|
t.Error("Expected:", expected, "Got:", actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCanAddAVectorFromAPoint(t *testing.T) {
|
||||||
|
point1 := CreatePoint(3, -2, 1)
|
||||||
|
vector := CreateVector(1, 1, -1)
|
||||||
|
|
||||||
|
expected := CreatePoint(4, -1, 0)
|
||||||
|
actual := point1.AddVector(vector)
|
||||||
|
|
||||||
|
if actual.EqualTo(expected) == false {
|
||||||
|
t.Error("Expected:", expected, "Got:", actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCanSubtractAVectorFromAPoint(t *testing.T) {
|
||||||
|
point1 := CreatePoint(3, 2, 1)
|
||||||
|
vector := CreateVector(5, 6, 7)
|
||||||
|
|
||||||
|
expected := CreatePoint(-2, -4, -6)
|
||||||
|
actual := point1.SubtractVector(vector)
|
||||||
|
|
||||||
|
if actual.EqualTo(expected) == false {
|
||||||
|
t.Error("Expected:", expected, "Got:", actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
71
model/tuple.go
Executable file
71
model/tuple.go
Executable file
|
|
@ -0,0 +1,71 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
type Tuple struct {
|
||||||
|
x float64
|
||||||
|
y float64
|
||||||
|
z float64
|
||||||
|
w float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Tuple) IsVector() bool {
|
||||||
|
return t.w == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Tuple) IsPoint() bool {
|
||||||
|
return t.w == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Tuple) X() float64 {
|
||||||
|
return t.x
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Tuple) Y() float64 {
|
||||||
|
return t.y
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Tuple) Z() float64 {
|
||||||
|
return t.z
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Tuple) W() float64 {
|
||||||
|
return t.w
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Tuple) AddTuple(t2 Tuple) Tuple {
|
||||||
|
return Tuple{
|
||||||
|
x: t.x + t2.x,
|
||||||
|
y: t.y + t2.y,
|
||||||
|
z: t.z + t2.z,
|
||||||
|
w: t.w + t2.w}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Tuple) Negate() Tuple {
|
||||||
|
return Tuple{
|
||||||
|
x: -t.x,
|
||||||
|
y: -t.y,
|
||||||
|
z: -t.z,
|
||||||
|
w: -t.w}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Tuple) Multiply(multiplier float64) Tuple {
|
||||||
|
return Tuple{
|
||||||
|
x: t.x * multiplier,
|
||||||
|
y: t.y * multiplier,
|
||||||
|
z: t.z * multiplier,
|
||||||
|
w: t.w * multiplier}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Tuple) Divide(divider float64) Tuple {
|
||||||
|
return Tuple{
|
||||||
|
x: t.x / divider,
|
||||||
|
y: t.y / divider,
|
||||||
|
z: t.z / divider,
|
||||||
|
w: t.w / divider}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Tuple) EqualTo(t2 Tuple) bool {
|
||||||
|
return round(t.x, 8) == round(t2.x, 8) &&
|
||||||
|
round(t.y, 8) == round(t2.y, 8) &&
|
||||||
|
round(t.z, 8) == round(t2.z, 8) &&
|
||||||
|
round(t.w, 8) == round(t2.w, 8)
|
||||||
|
}
|
||||||
93
model/tuple_test.go
Executable file
93
model/tuple_test.go
Executable file
|
|
@ -0,0 +1,93 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCanCreateATuple(t *testing.T) {
|
||||||
|
point := Tuple{x: 1, y: 2, z: 3, w: 4}
|
||||||
|
|
||||||
|
if point.X() != 1 {
|
||||||
|
t.Error("Expected for x:", 1, "Got:", point.x)
|
||||||
|
}
|
||||||
|
if point.Y() != 2 {
|
||||||
|
t.Error("Expected for y:", 2, "Got:", point.y)
|
||||||
|
}
|
||||||
|
if point.Z() != 3 {
|
||||||
|
t.Error("Expected for z:", 3, "Got:", point.z)
|
||||||
|
}
|
||||||
|
if point.W() != 4 {
|
||||||
|
t.Error("Expected for w:", 4, "Got:", point.w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTupleCanBeAPointOrVector(t *testing.T) {
|
||||||
|
vector := Tuple{x: 1, y: 2, z: 3, w: 0}
|
||||||
|
point := Tuple{x: 1, y: 2, z: 3, w: 1}
|
||||||
|
|
||||||
|
if vector.IsVector() != true {
|
||||||
|
t.Error("Expected tuple to be a vector")
|
||||||
|
}
|
||||||
|
if point.IsPoint() != true {
|
||||||
|
t.Error("Expected tuple to be a point")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCanAddUpTwoTuples(t *testing.T) {
|
||||||
|
tuple1 := Tuple{x: 3, y: -2, z: 5, w: 1}
|
||||||
|
tuple2 := Tuple{x: -2, y: 3, z: 1, w: 0}
|
||||||
|
|
||||||
|
expected := Tuple{x: 1, y: 1, z: 6, w: 1}
|
||||||
|
actual := tuple1.AddTuple(tuple2)
|
||||||
|
|
||||||
|
if actual.EqualTo(expected) == false {
|
||||||
|
t.Error("Expected:", expected, "Got:", actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCanNegateATuple(t *testing.T) {
|
||||||
|
tuple := Tuple{x: 1, y: -2, z: 3, w: -4}
|
||||||
|
|
||||||
|
expected := Tuple{x: -1, y: 2, z: -3, w: 4}
|
||||||
|
actual := tuple.Negate()
|
||||||
|
|
||||||
|
if actual.EqualTo(expected) == false {
|
||||||
|
t.Error("Expected:", expected, "Got:", actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCanMultiplyATupleByAScalar(t *testing.T) {
|
||||||
|
tuple1 := Tuple{x: 1, y: -2, z: 3, w: -4}
|
||||||
|
scalar := 3.5
|
||||||
|
|
||||||
|
expected := Tuple{x: 3.5, y: -7, z: 10.5, w: -14}
|
||||||
|
actual := tuple1.Multiply(scalar)
|
||||||
|
|
||||||
|
if actual.EqualTo(expected) == false {
|
||||||
|
t.Error("Expected:", expected, "Got:", actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCanMultiplyATupleByAFraction(t *testing.T) {
|
||||||
|
tuple := Tuple{x: 1, y: -2, z: 3, w: -4}
|
||||||
|
fraction := 0.5
|
||||||
|
|
||||||
|
expected := Tuple{x: 0.5, y: -1, z: 1.5, w: -2}
|
||||||
|
actual := tuple.Multiply(fraction)
|
||||||
|
|
||||||
|
if actual.EqualTo(expected) == false {
|
||||||
|
t.Error("Expected:", expected, "Got:", actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCanDivideATupleByAScalar(t *testing.T) {
|
||||||
|
tuple := Tuple{x: 1, y: -2, z: 3, w: -4}
|
||||||
|
scalar := 2.0
|
||||||
|
|
||||||
|
expected := Tuple{x: 0.5, y: -1, z: 1.5, w: -2}
|
||||||
|
actual := tuple.Divide(scalar)
|
||||||
|
|
||||||
|
if actual.EqualTo(expected) == false {
|
||||||
|
t.Error("Expected:", expected, "Got:", actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
58
model/vector.go
Executable file
58
model/vector.go
Executable file
|
|
@ -0,0 +1,58 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import "math"
|
||||||
|
|
||||||
|
type Vector struct {
|
||||||
|
*Tuple
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateVector(x float64, y float64, z float64) Vector {
|
||||||
|
return Vector{&Tuple{x: x, y: y, z: z, w: 0}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Vector) AddVector(v2 Vector) Vector {
|
||||||
|
return CreateVector(v.x+v2.x, v.y+v2.y, v.z+v2.z)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Vector) SubtractVector(v2 Vector) Vector {
|
||||||
|
return CreateVector(v.x-v2.x, v.y-v2.y, v.z-v2.z)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Vector) Negate() Vector {
|
||||||
|
return CreateVector(-v.x, -v.y, -v.z)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Vector) Multiply(multiplier float64) Vector {
|
||||||
|
return CreateVector(v.x*multiplier, v.y*multiplier, v.z*multiplier)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Vector) Divide(divider float64) Vector {
|
||||||
|
return CreateVector(v.x/divider, v.y/divider, v.z/divider)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Vector) Magnitude() float64 {
|
||||||
|
return math.Sqrt(v.x*v.x + v.y*v.y + v.z*v.z)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Vector) Normalize() Vector {
|
||||||
|
magnitude := v.Magnitude()
|
||||||
|
|
||||||
|
return CreateVector(v.x/magnitude, v.y/magnitude, v.z/magnitude)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Vector) Dot(v2 Vector) float64 {
|
||||||
|
return v.x*v2.x + v.y*v2.y + v.z*v2.z + v.w*v2.w
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Vector) Cross(v2 Vector) Vector {
|
||||||
|
return CreateVector(v.y*v2.z-v.z*v2.y,
|
||||||
|
v.z*v2.x-v.x*v2.z,
|
||||||
|
v.x*v2.y-v.y*v2.x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Vector) EqualTo(v2 Vector) bool {
|
||||||
|
return round(v.x, 8) == round(v2.x, 8) &&
|
||||||
|
round(v.y, 8) == round(v2.y, 8) &&
|
||||||
|
round(v.z, 8) == round(v2.z, 8) &&
|
||||||
|
round(v.w, 8) == round(v2.w, 8)
|
||||||
|
}
|
||||||
156
model/vector_test.go
Executable file
156
model/vector_test.go
Executable file
|
|
@ -0,0 +1,156 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCanCreateAVector(t *testing.T) {
|
||||||
|
vector := CreateVector(4, -4, 3)
|
||||||
|
|
||||||
|
if vector.x != 4 {
|
||||||
|
t.Error("Expected for x:", 4, "Got:", vector.x)
|
||||||
|
}
|
||||||
|
if vector.y != -4 {
|
||||||
|
t.Error("Expected for y:", -4, "Got:", vector.y)
|
||||||
|
}
|
||||||
|
if vector.z != 3 {
|
||||||
|
t.Error("Expected for z:", 3, "Got:", vector.z)
|
||||||
|
}
|
||||||
|
if vector.IsVector() != true {
|
||||||
|
t.Error("Expected a point")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCanAddAVectorToAVector(t *testing.T) {
|
||||||
|
vector1 := CreateVector(3, -2, 1)
|
||||||
|
vector2 := CreateVector(1, 1, -1)
|
||||||
|
|
||||||
|
expected := CreateVector(4, -1, 0)
|
||||||
|
actual := vector1.AddVector(vector2)
|
||||||
|
|
||||||
|
if actual.EqualTo(expected) == false {
|
||||||
|
t.Error("Expected:", expected, "Got:", actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCanSubtractTwoVectors(t *testing.T) {
|
||||||
|
vector1 := CreateVector(3, 2, 1)
|
||||||
|
vector2 := CreateVector(5, 6, 7)
|
||||||
|
|
||||||
|
expected := CreateVector(-2, -4, -6)
|
||||||
|
actual := vector1.SubtractVector(vector2)
|
||||||
|
|
||||||
|
if actual.EqualTo(expected) == false {
|
||||||
|
t.Error("Expected:", expected, "Got:", actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCanNegateAVector(t *testing.T) {
|
||||||
|
vector := CreateVector(1, -2, 3)
|
||||||
|
|
||||||
|
expected := CreateVector(-1, 2, -3)
|
||||||
|
actual := vector.Negate()
|
||||||
|
|
||||||
|
if actual.EqualTo(expected) == false {
|
||||||
|
t.Error("Expected:", expected, "Got:", actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCanMultiplyAVectorByAFraction(t *testing.T) {
|
||||||
|
vector := CreateVector(1, -2, 3)
|
||||||
|
fraction := 0.5
|
||||||
|
|
||||||
|
expected := CreateVector(0.5, -1, 1.5)
|
||||||
|
actual := vector.Multiply(fraction)
|
||||||
|
|
||||||
|
if actual.EqualTo(expected) == false {
|
||||||
|
t.Error("Expected:", expected, "Got:", actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCanDivideAVectorByAScalar(t *testing.T) {
|
||||||
|
vector := CreateVector(1, -2, 3)
|
||||||
|
scalar := 2.0
|
||||||
|
|
||||||
|
expected := CreateVector(0.5, -1, 1.5)
|
||||||
|
actual := vector.Divide(scalar)
|
||||||
|
|
||||||
|
if actual.EqualTo(expected) == false {
|
||||||
|
t.Error("Expected:", expected, "Got:", actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCanComputeTheMagnitude(t *testing.T) {
|
||||||
|
vector1 := CreateVector(1, 0, 0)
|
||||||
|
vector2 := CreateVector(0, 1, 0)
|
||||||
|
vector3 := CreateVector(0, 0, 1)
|
||||||
|
vector4 := CreateVector(1, 2, 3)
|
||||||
|
vector5 := CreateVector(-1, -2, -3)
|
||||||
|
|
||||||
|
if vector1.Magnitude() != 1 {
|
||||||
|
t.Error("Expected magnitude of:", 1, "Got:", vector1.Magnitude())
|
||||||
|
}
|
||||||
|
if vector2.Magnitude() != 1 {
|
||||||
|
t.Error("Expected magnitude of:", 1, "Got:", vector2.Magnitude())
|
||||||
|
}
|
||||||
|
if vector3.Magnitude() != 1 {
|
||||||
|
t.Error("Expected magnitude of:", 1, "Got:", vector3.Magnitude())
|
||||||
|
}
|
||||||
|
if vector4.Magnitude() != math.Sqrt(14) {
|
||||||
|
t.Error("Expected magnitude of:", math.Sqrt(14), "Got:", vector4.Magnitude())
|
||||||
|
}
|
||||||
|
if vector5.Magnitude() != math.Sqrt(14) {
|
||||||
|
t.Error("Expected magnitude of:", math.Sqrt(14), "Got:", vector5.Magnitude())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCanNormalizingAVector(t *testing.T) {
|
||||||
|
vector1 := CreateVector(4, 0, 0)
|
||||||
|
vector2 := CreateVector(1, 2, 3)
|
||||||
|
|
||||||
|
expected1 := CreateVector(1, 0, 0)
|
||||||
|
actual1 := vector1.Normalize()
|
||||||
|
|
||||||
|
if actual1.EqualTo(expected1) == false {
|
||||||
|
t.Error("Expected:", expected1, "Got:", actual1)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected2 := CreateVector(1/math.Sqrt(14), 2/math.Sqrt(14), 3/math.Sqrt(14))
|
||||||
|
actual2 := vector2.Normalize()
|
||||||
|
|
||||||
|
if actual2.EqualTo(expected2) == false {
|
||||||
|
t.Error("Expected:", expected2, "Got:", actual2)
|
||||||
|
}
|
||||||
|
if actual2.Magnitude() != 1 {
|
||||||
|
t.Error("Expected magnitude:", 1, "Got:", actual2.Magnitude())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCanCalculateDotProduct(t *testing.T) {
|
||||||
|
vector1 := CreateVector(1, 2, 3)
|
||||||
|
vector2 := CreateVector(2, 3, 4)
|
||||||
|
|
||||||
|
if vector1.Dot(vector2) != 20 {
|
||||||
|
t.Error("Expected dot product:", 20, "Got:", vector1.Dot(vector2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCanCalculateCrossProduct(t *testing.T) {
|
||||||
|
vector1 := CreateVector(1, 2, 3)
|
||||||
|
vector2 := CreateVector(2, 3, 4)
|
||||||
|
|
||||||
|
expected1 := CreateVector(-1, 2, -1)
|
||||||
|
actual1 := vector1.Cross(vector2)
|
||||||
|
|
||||||
|
if actual1.EqualTo(expected1) == false {
|
||||||
|
t.Error("Expected:", expected1, "Got:", actual1)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected2 := CreateVector(1, -2, 1)
|
||||||
|
actual2 := vector2.Cross(vector1)
|
||||||
|
|
||||||
|
if actual2.EqualTo(expected2) == false {
|
||||||
|
t.Error("Expected:", expected2, "Got:", actual2)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue