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