├── .gitignore ├── LICENSE.md ├── README.md ├── sample ├── output.png └── reflections.png └── src ├── godray ├── algebra.go ├── camera.go ├── color.go ├── geometry.go ├── light.go ├── material.go └── screen.go └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | *.swp 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jack Huang, Paul Mou 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # godray 2 | 3 | Multiple lights + shadows 4 | ![](https://github.com/LanJian/godray/raw/master/sample/output.png) 5 | 6 | Reflections 7 | ![](https://github.com/LanJian/godray/raw/master/sample/reflections.png) 8 | 9 | List of Features 10 | * Sphere 11 | * Phong lighting model 12 | * Shadow 13 | * Reflection 14 | 15 | To run without compilation 16 | 17 | go run src/main.go 18 | -------------------------------------------------------------------------------- /sample/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LanJian/godray/a68d33c1f734fe9c54e4fe21ec188a059ec812ca/sample/output.png -------------------------------------------------------------------------------- /sample/reflections.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LanJian/godray/a68d33c1f734fe9c54e4fe21ec188a059ec812ca/sample/reflections.png -------------------------------------------------------------------------------- /src/godray/algebra.go: -------------------------------------------------------------------------------- 1 | package godray 2 | 3 | import "math" 4 | 5 | type Vector struct { 6 | X, Y, Z float64 7 | } 8 | 9 | type Point struct { 10 | X, Y, Z float64 11 | } 12 | 13 | type Ray struct { 14 | P *Point 15 | V *Vector 16 | } 17 | 18 | func (a *Vector) Add(b *Vector) *Vector { 19 | return &Vector{a.X + b.X, a.Y + b.Y, a.Z + b.Z} 20 | } 21 | 22 | func (a *Point) Add(b *Vector) *Point { 23 | return &Point{a.X + b.X, a.Y + b.Y, a.Z + b.Z} 24 | } 25 | 26 | func (a *Point) Subtract(b *Point) *Vector { 27 | return &Vector{a.X - b.X, a.Y - b.Y, a.Z - b.Z} 28 | } 29 | 30 | func (a *Vector) Subtract(b *Vector) *Vector { 31 | return a.Add(b.Scale(-1)) 32 | } 33 | 34 | func (a *Vector) Scale(s float64) *Vector { 35 | return &Vector{a.X * s, a.Y * s, a.Z * s} 36 | } 37 | 38 | func (a *Vector) Dot(b *Vector) float64 { 39 | return a.X*b.X + a.Y*b.Y + a.Z*b.Z 40 | } 41 | 42 | func (a *Vector) Cross(b *Vector) *Vector { 43 | return &Vector{ 44 | a.Y*b.Z - a.Z*b.Y, 45 | a.Z*b.X - a.X*b.Z, 46 | a.X*b.Y - a.Y*b.X, 47 | } 48 | } 49 | 50 | func (a *Vector) Normalize() *Vector { 51 | m := a.Magnitude() 52 | return &Vector{a.X / m, a.Y / m, a.Z / m} 53 | } 54 | 55 | func (a *Vector) Magnitude() float64 { 56 | return math.Sqrt(a.X*a.X + a.Y*a.Y + a.Z*a.Z) 57 | } 58 | -------------------------------------------------------------------------------- /src/godray/camera.go: -------------------------------------------------------------------------------- 1 | package godray 2 | 3 | import "math" 4 | 5 | type camera struct { 6 | Position *Point 7 | View *Vector 8 | Up *Vector 9 | } 10 | 11 | func NewCamera(position *Point, view *Vector, up *Vector) *camera { 12 | return &camera{position, view.Normalize(), up.Normalize()} 13 | } 14 | 15 | func (c *camera) side() *Vector { 16 | return c.View.Cross(c.Up) 17 | } 18 | 19 | func (c *camera) GetRayTo(screen *Screen, u int, v int) *Ray { 20 | w, h := float64(screen.Width), float64(screen.Height) 21 | fovy := float64(screen.Fov) 22 | 23 | d := (h / 2) / math.Tan(fovy/2) 24 | sideVector := c.side().Scale(float64(u) - w/2) 25 | upVector := c.Up.Scale(h/2 - float64(v)) 26 | viewVector := c.View.Scale(d) 27 | 28 | return &Ray{ 29 | c.Position, 30 | sideVector.Add(upVector).Add(viewVector).Normalize(), 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/godray/color.go: -------------------------------------------------------------------------------- 1 | package godray 2 | 3 | import ( 4 | "image/color" 5 | "math" 6 | ) 7 | 8 | var ( 9 | Red = &Color{color.RGBA{255, 0, 0, 255}} 10 | Green = &Color{color.RGBA{0, 255, 0, 255}} 11 | Blue = &Color{color.RGBA{0, 0, 255, 255}} 12 | White = &Color{color.RGBA{255, 255, 255, 255}} 13 | Black = &Color{color.RGBA{0, 0, 0, 255}} 14 | ) 15 | 16 | type ColorAlgebra interface { 17 | Multiply(c color.Color) color.Color 18 | Scale(factor float64) color.Color 19 | } 20 | 21 | type Color struct { 22 | color.RGBA 23 | } 24 | 25 | func multiply(a, b float64) uint8 { 26 | return clamp(0, 255, a*b/255) 27 | } 28 | 29 | func clamp(min, max, num float64) uint8 { 30 | return uint8(math.Min(math.Max(num, min), max)) 31 | } 32 | 33 | func (c1 Color) Multiply(c2 *Color) *Color { 34 | r1, g1, b1 := c1.R, c1.G, c1.B 35 | r2, g2, b2 := c2.R, c2.G, c2.B 36 | 37 | return &Color{ 38 | color.RGBA{ 39 | multiply(float64(r1), float64(r2)), 40 | multiply(float64(g1), float64(g2)), 41 | multiply(float64(b1), float64(b2)), 42 | 255, 43 | }, 44 | } 45 | } 46 | 47 | func (c1 Color) Add(c2 *Color) *Color { 48 | r1, g1, b1 := c1.R, c1.G, c1.B 49 | r2, g2, b2 := c2.R, c2.G, c2.B 50 | 51 | return &Color{ 52 | color.RGBA{ 53 | clamp(0, 255, float64(r1)+float64(r2)), 54 | clamp(0, 255, float64(g1)+float64(g2)), 55 | clamp(0, 255, float64(b1)+float64(b2)), 56 | 255, 57 | }, 58 | } 59 | } 60 | 61 | func (c1 Color) Scale(scalar float64) *Color { 62 | r1, g1, b1 := c1.R, c1.G, c1.G 63 | 64 | return &Color{ 65 | color.RGBA{ 66 | clamp(0, 255, float64(r1)*scalar), 67 | clamp(0, 255, float64(g1)*scalar), 68 | clamp(0, 255, float64(b1)*scalar), 69 | 255, 70 | }, 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/godray/geometry.go: -------------------------------------------------------------------------------- 1 | package godray 2 | 3 | import ( 4 | "math" 5 | ) 6 | 7 | const epsilon = 1e-4 8 | 9 | type Object interface { 10 | Intersect(*Ray) (*Point, float64, *Vector) 11 | Material() *Material 12 | SetMaterial(*Material) 13 | Normal(*Point) *Vector 14 | } 15 | 16 | type Sphere struct { 17 | Center *Point 18 | Radius float64 19 | material *Material 20 | } 21 | 22 | type Intersection struct { 23 | Point *Point 24 | Distance float64 25 | Normal *Vector 26 | } 27 | 28 | func NewSphere(center *Point, radius float64, material *Material) Sphere { 29 | return Sphere{center, radius, material} 30 | } 31 | 32 | func (s Sphere) Material() *Material { 33 | return s.material 34 | } 35 | 36 | func (s Sphere) SetMaterial(material *Material) { 37 | s.material = material 38 | } 39 | 40 | func (s Sphere) Normal(p *Point) *Vector { 41 | return p.Subtract(s.Center).Normalize() 42 | } 43 | 44 | func (s Sphere) Intersect(r *Ray) (*Point, float64, *Vector) { 45 | l := r.V.Normalize() 46 | o := r.P 47 | 48 | leftSide := l.Dot(o.Subtract(s.Center)) 49 | 50 | diff := r.P.Subtract(s.Center) 51 | sqrtSum := (leftSide * leftSide) - diff.Dot(diff) + s.Radius*s.Radius 52 | 53 | if sqrtSum == 0.0 { 54 | d := -1 * leftSide 55 | intersectPoint := o.Add(r.V.Scale(d)) 56 | 57 | if d > epsilon { 58 | return intersectPoint, d, s.Normal(intersectPoint) 59 | } 60 | } else if sqrtSum > 0 { 61 | d1 := -1*leftSide + math.Sqrt(sqrtSum) 62 | d2 := -1*leftSide - math.Sqrt(sqrtSum) 63 | 64 | pt1 := o.Add(r.V.Normalize().Scale(d1)) 65 | pt2 := o.Add(r.V.Normalize().Scale(d2)) 66 | 67 | if r.P.Subtract(pt1).Magnitude()-r.P.Subtract(pt2).Magnitude() >= 0 { 68 | if d2 > epsilon { 69 | return pt2, d2, s.Normal(pt2) 70 | } 71 | } else if d1 > epsilon { 72 | return pt1, d1, s.Normal(pt1) 73 | } 74 | } 75 | 76 | return nil, math.MaxFloat64, nil 77 | } 78 | -------------------------------------------------------------------------------- /src/godray/light.go: -------------------------------------------------------------------------------- 1 | package godray 2 | 3 | type Light struct { 4 | Position *Point 5 | Ambient *Color 6 | Diffuse *Color 7 | Specular *Color 8 | } 9 | -------------------------------------------------------------------------------- /src/godray/material.go: -------------------------------------------------------------------------------- 1 | package godray 2 | 3 | type Material struct { 4 | Ambient *Color 5 | Diffuse *Color 6 | Specular *Color 7 | Shininess float64 8 | Reflectivity float64 9 | } 10 | -------------------------------------------------------------------------------- /src/godray/screen.go: -------------------------------------------------------------------------------- 1 | package godray 2 | 3 | type Screen struct { 4 | Width, Height int 5 | Fov float64 6 | } 7 | -------------------------------------------------------------------------------- /src/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import . "./godray" 4 | 5 | import ( 6 | "fmt" 7 | "image" 8 | "image/color" 9 | "image/draw" 10 | "image/png" 11 | "math" 12 | "os" 13 | ) 14 | 15 | var i *Vector = &Vector{1, 0, 0} 16 | var j *Vector = &Vector{0, 1, 0} 17 | var k *Vector = &Vector{0, 0, 1} 18 | var o *Point = &Point{0, 0, 0} 19 | 20 | const MAX_DEPTH int = 10 21 | 22 | func getClosestIntersection(ray *Ray, objects []Object) (*Intersection, Object) { 23 | intersections := make([]*Intersection, len(objects)) 24 | 25 | for i, object := range objects { 26 | point, dist, n := object.Intersect(ray) 27 | intersections[i] = &Intersection{point, dist, n} 28 | } 29 | 30 | var closestIntersection *Intersection 31 | var closestObject Object 32 | for i, intersection := range intersections { 33 | if closestIntersection == nil || 34 | intersection.Distance < closestIntersection.Distance { 35 | closestIntersection = intersection 36 | closestObject = objects[i] 37 | } 38 | } 39 | 40 | return closestIntersection, closestObject 41 | } 42 | 43 | func raytrace(ray *Ray, lights []*Light, objects []Object, depth int) *Color { 44 | var ambientTerm *Color = Black 45 | var diffuseTerm *Color = Black 46 | var specularTerm *Color = Black 47 | 48 | if depth <= 0 { 49 | return nil 50 | } 51 | 52 | closestIntersection, closestObject := getClosestIntersection(ray, objects) 53 | intersection := closestIntersection.Point 54 | n := closestIntersection.Normal 55 | 56 | vv := ray.V.Normalize().Scale(-1) 57 | 58 | // Phong equaion calculation 59 | // for details, please refer to https://en.wikipedia.org/wiki/Phong_reflection_model 60 | if intersection != nil { 61 | for _, light := range lights { 62 | ambientTerm = ambientTerm.Add(closestObject.Material().Ambient. 63 | Multiply(light.Ambient)) 64 | 65 | l := light.Position.Subtract(intersection).Normalize() 66 | 67 | rayToLight := &Ray{ 68 | intersection, 69 | l, 70 | } 71 | 72 | obstruction, _ := getClosestIntersection(rayToLight, objects) 73 | // shadow calculation - if obstructed, we skip coloring. 74 | if obstruction.Point != nil { 75 | continue 76 | } 77 | 78 | r := n.Scale(2 * l.Dot(n)).Subtract(l) 79 | 80 | if l.Dot(n) > 0 { 81 | diffuseTerm = diffuseTerm.Add(light.Diffuse.Scale(l.Dot(n)). 82 | Multiply(closestObject.Material().Diffuse)) 83 | } 84 | 85 | if r.Dot(vv) > 0 { 86 | specularTerm = specularTerm.Add(light.Specular. 87 | Scale(math.Pow(r.Dot(vv), closestObject.Material().Shininess)). 88 | Multiply(closestObject.Material().Specular)) 89 | } 90 | } 91 | 92 | // recurse 93 | newRay := &Ray{intersection, n.Scale(vv.Dot(n)).Scale(2).Subtract(vv).Normalize()} 94 | // reflection calculation - treating the reflected color as a light source 95 | reflectedColor := raytrace(newRay, lights, objects, depth-1) 96 | 97 | if reflectedColor != nil { 98 | if newRay.V.Dot(n) > 0 { 99 | diffuseTerm = diffuseTerm.Add(reflectedColor.Scale(newRay.V.Dot(n)). 100 | Multiply(closestObject.Material().Diffuse).Scale(closestObject.Material().Reflectivity)) 101 | } 102 | 103 | if newRay.V.Dot(vv) > 0 { 104 | specularTerm = specularTerm.Add(reflectedColor. 105 | Scale(math.Pow(newRay.V.Dot(vv), closestObject.Material().Shininess)). 106 | Multiply(closestObject.Material().Specular).Scale(closestObject.Material().Reflectivity)) 107 | } 108 | } 109 | 110 | return ambientTerm.Add(diffuseTerm).Add(specularTerm) 111 | } 112 | 113 | return nil 114 | } 115 | 116 | func main() { 117 | eye := o 118 | camera := NewCamera(eye.Add(k.Scale(10)), k.Scale(-1), j) 119 | screen := &Screen{Width: 800, Height: 600, Fov: 45} 120 | 121 | // lights 122 | lights := []*Light{ 123 | &Light{ 124 | &Point{10, 4, 2}, 125 | White.Scale(0.1), 126 | White, 127 | White, 128 | }, 129 | } 130 | 131 | // objects 132 | objects := []Object{ 133 | NewSphere(&Point{5, 0, -2}, 2, &Material{ 134 | &Color{color.RGBA{250, 60, 60, 255}}, 135 | &Color{color.RGBA{250, 60, 60, 255}}, 136 | White, 137 | 20, 138 | 0.4, 139 | }), 140 | NewSphere(&Point{-2, 2, -4}, 3, &Material{ 141 | &Color{color.RGBA{60, 250, 60, 255}}, 142 | &Color{color.RGBA{60, 250, 60, 255}}, 143 | White, 144 | 50, 145 | 0.9, 146 | }), 147 | NewSphere(&Point{0, -10, -4}, 8, &Material{ 148 | Blue, 149 | &Color{color.RGBA{100, 100, 200, 255}}, 150 | White, 151 | 50, 152 | 0.9, 153 | }), 154 | NewSphere(&Point{0, 4, 0}, 1, &Material{ 155 | &Color{color.RGBA{250, 225, 150, 255}}, 156 | &Color{color.RGBA{250, 225, 150, 255}}, 157 | White, 158 | 30, 159 | 0.6, 160 | }), 161 | } 162 | 163 | // Uncomment these to test the rays and intersection. 164 | // miss := &Ray{&Point{0, 5, 0}, &Vector{0, 0, -4}} 165 | // _, _, n := sphere.Intersect(hit) 166 | // intersection, t := sphere.Intersect(miss) 167 | // fmt.Println(n) 168 | 169 | out, err := os.Create("./output.png") 170 | if err != nil { 171 | os.Exit(1) 172 | } 173 | 174 | imgRect := image.Rect(0, 0, screen.Width, screen.Height) 175 | img := image.NewRGBA(imgRect) 176 | draw.Draw(img, img.Bounds(), &image.Uniform{color.Black}, image.ZP, draw.Src) 177 | 178 | for u := 0; u < screen.Width; u++ { 179 | for v := 0; v < screen.Height; v++ { 180 | ray := camera.GetRayTo(screen, u, v) 181 | illumination := raytrace(ray, lights, objects, MAX_DEPTH) 182 | if illumination == nil { 183 | illumination = Black 184 | } 185 | 186 | fill := &image.Uniform{color.RGBA{ 187 | illumination.R, 188 | illumination.G, 189 | illumination.B, 190 | illumination.A, 191 | }} 192 | 193 | draw.Draw(img, image.Rect(u, v, u+1, v+1), fill, image.ZP, draw.Src) 194 | } 195 | } 196 | 197 | err = png.Encode(out, img) 198 | if err != nil { 199 | fmt.Println(err) 200 | os.Exit(1) 201 | } 202 | } 203 | --------------------------------------------------------------------------------