├── go.mod ├── .gitignore ├── images ├── cover.png ├── dof_06.png ├── dof_09.png ├── dof_12.png ├── fov_30.png ├── fov_60.png ├── fov_90.png ├── metal_f00.png ├── metal_f15.png ├── metal_f30.png ├── aperture_000.png ├── aperture_050.png ├── aperture_100.png ├── dielectric_i100.png ├── dielectric_i150.png ├── dielectric_i200.png ├── lambertian_black.png ├── lambertian_red.png └── lambertian_white.png ├── Makefile ├── ray.go ├── color.go ├── examples └── main.go ├── vector.go ├── hitter.go ├── README.md ├── material.go ├── camera.go ├── renderer.go └── LICENSE /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/y-taka-23/raytracing-go 2 | 3 | go 1.15 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.exe 2 | *.exe~ 3 | *.dll 4 | *.so 5 | *.dylib 6 | *.test 7 | *.out 8 | bin/ 9 | -------------------------------------------------------------------------------- /images/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y-taka-23/raytracing-go/HEAD/images/cover.png -------------------------------------------------------------------------------- /images/dof_06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y-taka-23/raytracing-go/HEAD/images/dof_06.png -------------------------------------------------------------------------------- /images/dof_09.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y-taka-23/raytracing-go/HEAD/images/dof_09.png -------------------------------------------------------------------------------- /images/dof_12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y-taka-23/raytracing-go/HEAD/images/dof_12.png -------------------------------------------------------------------------------- /images/fov_30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y-taka-23/raytracing-go/HEAD/images/fov_30.png -------------------------------------------------------------------------------- /images/fov_60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y-taka-23/raytracing-go/HEAD/images/fov_60.png -------------------------------------------------------------------------------- /images/fov_90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y-taka-23/raytracing-go/HEAD/images/fov_90.png -------------------------------------------------------------------------------- /images/metal_f00.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y-taka-23/raytracing-go/HEAD/images/metal_f00.png -------------------------------------------------------------------------------- /images/metal_f15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y-taka-23/raytracing-go/HEAD/images/metal_f15.png -------------------------------------------------------------------------------- /images/metal_f30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y-taka-23/raytracing-go/HEAD/images/metal_f30.png -------------------------------------------------------------------------------- /images/aperture_000.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y-taka-23/raytracing-go/HEAD/images/aperture_000.png -------------------------------------------------------------------------------- /images/aperture_050.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y-taka-23/raytracing-go/HEAD/images/aperture_050.png -------------------------------------------------------------------------------- /images/aperture_100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y-taka-23/raytracing-go/HEAD/images/aperture_100.png -------------------------------------------------------------------------------- /images/dielectric_i100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y-taka-23/raytracing-go/HEAD/images/dielectric_i100.png -------------------------------------------------------------------------------- /images/dielectric_i150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y-taka-23/raytracing-go/HEAD/images/dielectric_i150.png -------------------------------------------------------------------------------- /images/dielectric_i200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y-taka-23/raytracing-go/HEAD/images/dielectric_i200.png -------------------------------------------------------------------------------- /images/lambertian_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y-taka-23/raytracing-go/HEAD/images/lambertian_black.png -------------------------------------------------------------------------------- /images/lambertian_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y-taka-23/raytracing-go/HEAD/images/lambertian_red.png -------------------------------------------------------------------------------- /images/lambertian_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/y-taka-23/raytracing-go/HEAD/images/lambertian_white.png -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build examples clean 2 | 3 | build: 4 | go build 5 | 6 | examples: 7 | mkdir -p ./bin/ 8 | go build -o ./bin/example ./examples/main.go 9 | 10 | clean: 11 | rm -rf ./bin/ 12 | -------------------------------------------------------------------------------- /ray.go: -------------------------------------------------------------------------------- 1 | package raytracing 2 | 3 | type ray struct { 4 | origin Point 5 | direction Vector 6 | } 7 | 8 | func newRay(p Point, v Vector) ray { 9 | return ray{origin: p, direction: v} 10 | } 11 | 12 | func (r ray) at(t float64) Point { 13 | return origin().to(r.origin).add(r.direction.mul(t)).point() 14 | } 15 | -------------------------------------------------------------------------------- /color.go: -------------------------------------------------------------------------------- 1 | package raytracing 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "math" 7 | ) 8 | 9 | type Color Vector 10 | 11 | func NewColor(r, g, b float64) Color { 12 | return Color(NewVector(r, g, b)) 13 | } 14 | 15 | func writeColor(w io.Writer, c Color, samples int) { 16 | 17 | clamp := func(x, min, max float64) float64 { 18 | if x < min { 19 | return min 20 | } 21 | if x > max { 22 | return max 23 | } 24 | return x 25 | } 26 | 27 | r := int(255 * clamp(math.Sqrt(c.x/float64(samples)), 0, 0.999)) 28 | g := int(255 * clamp(math.Sqrt(c.y/float64(samples)), 0, 0.999)) 29 | b := int(255 * clamp(math.Sqrt(c.z/float64(samples)), 0, 0.999)) 30 | 31 | fmt.Fprintf(w, "%d %d %d\n", r, g, b) 32 | } 33 | -------------------------------------------------------------------------------- /examples/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/y-taka-23/raytracing-go" 7 | ) 8 | 9 | func main() { 10 | 11 | width, height := 384, 282 12 | 13 | scene := getScene() 14 | camera := getCamera(width, height) 15 | 16 | cfg := raytracing.NewRendererConfig(width, height) 17 | raytracing.NewRenderer(cfg).Render(scene, camera) 18 | } 19 | 20 | func getScene() raytracing.Scene { 21 | 22 | scene := raytracing.NewScene() 23 | 24 | scene.Add(raytracing.NewSphere( 25 | raytracing.NewPoint(0, -1000, 0), 1000, 26 | raytracing.NewLambertian(raytracing.NewColor(0.5, 0.5, 0.5)))) 27 | scene.Add(raytracing.NewSphere( 28 | raytracing.NewPoint(0, 1, 0), 1, 29 | raytracing.NewDielectric(1.5))) 30 | scene.Add(raytracing.NewSphere( 31 | raytracing.NewPoint(-4, 1, 0), 1, 32 | raytracing.NewLambertian(raytracing.NewColor(0.4, 0.2, 0.1)))) 33 | scene.Add(raytracing.NewSphere( 34 | raytracing.NewPoint(4, 1, 0), 1, 35 | raytracing.NewMetal(raytracing.NewColor(0.7, 0.6, 0.5), 0))) 36 | 37 | return *scene 38 | } 39 | 40 | func getCamera(width, height int) raytracing.Camera { 41 | 42 | cfg := raytracing.NewCameraConfig( 43 | float64(width)/float64(height), 44 | raytracing.WithPointOfView( 45 | raytracing.NewPoint(13, 2, 3), 46 | raytracing.NewPoint(2, 0.75, 0)), 47 | raytracing.WithVerticalFOV(math.Pi/9.0), 48 | raytracing.WithAperture(0.1), 49 | raytracing.WithFocusDistance(10.0), 50 | ) 51 | 52 | return raytracing.NewCamera(cfg) 53 | } 54 | -------------------------------------------------------------------------------- /vector.go: -------------------------------------------------------------------------------- 1 | package raytracing 2 | 3 | import "math" 4 | 5 | type Vector struct { 6 | x float64 7 | y float64 8 | z float64 9 | } 10 | 11 | func NewVector(x, y, z float64) Vector { 12 | return Vector{x: x, y: y, z: z} 13 | } 14 | 15 | func (v Vector) neg() Vector { 16 | return NewVector(-v.x, -v.y, -v.z) 17 | } 18 | 19 | func (v Vector) add(w Vector) Vector { 20 | return NewVector(v.x+w.x, v.y+w.y, v.z+w.z) 21 | } 22 | 23 | func (v Vector) sub(w Vector) Vector { 24 | return NewVector(v.x-w.x, v.y-w.y, v.z-w.z) 25 | } 26 | 27 | func (v Vector) mul(t float64) Vector { 28 | return NewVector(t*v.x, t*v.y, t*v.z) 29 | } 30 | 31 | func (v Vector) div(t float64) Vector { 32 | return NewVector(v.x/t, v.y/t, v.z/t) 33 | } 34 | 35 | func (v Vector) dot(w Vector) float64 { 36 | return v.x*w.x + v.y*w.y + v.z*w.z 37 | } 38 | 39 | func (v Vector) cross(w Vector) Vector { 40 | return NewVector(v.y*w.z-v.z*w.y, v.z*w.x-v.x*w.z, v.x*w.y-v.y*w.x) 41 | } 42 | 43 | func (v Vector) norm() float64 { 44 | return v.dot(v) 45 | } 46 | 47 | func (v Vector) length() float64 { 48 | return math.Sqrt(v.norm()) 49 | } 50 | 51 | func (v Vector) normalize() Vector { 52 | return v.div(v.length()) 53 | } 54 | 55 | type Point Vector 56 | 57 | func NewPoint(x, y, z float64) Point { 58 | return Point(NewVector(x, y, z)) 59 | } 60 | 61 | func origin() Point { 62 | return NewPoint(0, 0, 0) 63 | } 64 | 65 | func (p Point) to(q Point) Vector { 66 | return NewVector(q.x-p.x, q.y-p.y, q.z-p.z) 67 | } 68 | 69 | func (v Vector) point() Point { 70 | return Point(v) 71 | } 72 | -------------------------------------------------------------------------------- /hitter.go: -------------------------------------------------------------------------------- 1 | package raytracing 2 | 3 | import ( 4 | "math" 5 | ) 6 | 7 | type Hitter interface { 8 | hit(r ray, tMin, tMax float64) (hitRecord, bool) 9 | } 10 | 11 | type Scene struct { 12 | hitters []Hitter 13 | } 14 | 15 | func NewScene() *Scene { 16 | return &Scene{hitters: []Hitter{}} 17 | } 18 | 19 | func (s *Scene) Add(h Hitter) *Scene { 20 | s.hitters = append(s.hitters, h) 21 | return s 22 | } 23 | 24 | func (s Scene) hit(r ray, tMin, tMax float64) (hitRecord, bool) { 25 | hitAnything := false 26 | closestT := math.MaxFloat64 27 | var closest hitRecord 28 | for _, h := range s.hitters { 29 | if hr, ok := h.hit(r, tMin, tMax); ok { 30 | hitAnything = true 31 | if hr.t < closestT { 32 | closestT = hr.t 33 | closest = hr 34 | } 35 | } 36 | } 37 | return closest, hitAnything 38 | } 39 | 40 | type hitRecord struct { 41 | point Point 42 | normal Vector 43 | incident ray 44 | t float64 45 | material Material 46 | } 47 | 48 | func newHitRecord(p Point, v Vector, r ray, t float64, m Material) hitRecord { 49 | return hitRecord{point: p, normal: v, incident: r, t: t, material: m} 50 | } 51 | 52 | type sphere struct { 53 | center Point 54 | radius float64 55 | material Material 56 | } 57 | 58 | func NewSphere(p Point, r float64, m Material) Hitter { 59 | return sphere{center: p, radius: r, material: m} 60 | } 61 | 62 | func (s sphere) hit(r ray, tMin, tMax float64) (hitRecord, bool) { 63 | 64 | v := s.center.to(r.origin) 65 | a := r.direction.norm() 66 | b := 2 * r.direction.dot(v) 67 | c := v.norm() - s.radius*s.radius 68 | 69 | disc := b*b - 4*a*c 70 | if disc < 0 { 71 | return hitRecord{}, false 72 | } 73 | 74 | tClose := (-b - math.Sqrt(disc)) / (2 * a) 75 | if tMin < tClose && tClose < tMax { 76 | p := r.at(tClose) 77 | n := s.center.to(p).div(s.radius) 78 | return newHitRecord(p, n, r, tClose, s.material), true 79 | } 80 | 81 | tFar := (-b + math.Sqrt(disc)) / (2 * a) 82 | if tMin < tFar && tFar < tMax { 83 | p := r.at(tFar) 84 | n := s.center.to(p).div(s.radius) 85 | return newHitRecord(p, n, r, tFar, s.material), true 86 | } 87 | 88 | return hitRecord{}, false 89 | } 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ray Tracing in Go 2 | 3 | ![cover image](images/cover.png) 4 | 5 | A Go implementation of the book [Ray Tracing in One Weekend](https://raytracing.github.io/). The repository provides a library to describe and render your own scenes. For more detail, see [`examples/main.go`](examples/main.go). 6 | 7 | ## Getting Started 8 | 9 | ```shell 10 | git clone git@github.com:y-taka-23/raytracing-go.git 11 | cd raytracing-go 12 | make examples 13 | ./bin/example > example.ppm 14 | open example.ppm 15 | ``` 16 | 17 | ## Materials 18 | 19 | ### Lambertian 20 | 21 | |color|result| 22 | |:----:|:----:| 23 | |(0.8, 0.1, 0.1)|![red lambertian sphere](images/lambertian_red.png)| 24 | |(1.0, 1.0, 1.0)|![white lambertian sphere](images/lambertian_white.png)| 25 | |(0.0, 0.0, 0.0)|![black lambertian sphere](images/lambertian_black.png)| 26 | 27 | ### Metalic 28 | 29 | |fuzziness|result| 30 | |:----:|:----:| 31 | |0.0|![metalic sphere of fuzziness 0.0](images/metal_f00.png)| 32 | |0.15|![metalic sphere of fuzziness 0.15](images/metal_f15.png)| 33 | |0.3|![metalic sphere of fuzziness 0.3](images/metal_f30.png)| 34 | 35 | ### Dielectric 36 | 37 | |refractive index|result| 38 | |:----:|:----:| 39 | |1.0|![dielectric sphere of refractive index 1.0](images/dielectric_i100.png)| 40 | |1.5|![dielectric sphere of refractive index 1.5](images/dielectric_i150.png)| 41 | |2.0|![dielectric sphere of refractive index 2.0](images/dielectric_i200.png)| 42 | 43 | 44 | ## Camera Setting 45 | 46 | ### Angle of View 47 | 48 | |virtical angle (degree)|result| 49 | |:----:|:----:| 50 | |90|![result of the vertical angle in 90 degree](images/fov_90.png)| 51 | |60|![result of the vertical angle in 60 degree](images/fov_60.png)| 52 | |30|![result of the vertical angle in 30 degree](images/fov_30.png)| 53 | 54 | ### Aperture 55 | 56 | |aperture|result| 57 | |:----:|:----:| 58 | |0.0|![result of the aperture 0.0](images/aperture_000.png)| 59 | |0.5|![result of the aperture 0.5](images/aperture_050.png)| 60 | |1.0|![result of the aperture 1.0](images/aperture_100.png)| 61 | 62 | ### Depth of Field 63 | 64 | |focus distance|result| 65 | |:----:|:----:| 66 | |6|![result of the depth of field 6](images/dof_06.png)| 67 | |9|![result of the depth of field 9](images/dof_09.png)| 68 | |12|![result of the depth of field 12](images/dof_12.png)| 69 | 70 | ## Reference 71 | 72 | * Shirley, P. (2016). _Ray Tracing in One Weekend_ ([online edition available](https://raytracing.github.io/)) 73 | -------------------------------------------------------------------------------- /material.go: -------------------------------------------------------------------------------- 1 | package raytracing 2 | 3 | import ( 4 | "math" 5 | "math/rand" 6 | ) 7 | 8 | type Material interface { 9 | scatter(hr hitRecord) (ray, bool) 10 | attenuation() Color 11 | } 12 | 13 | type lambertian struct { 14 | albedo Color 15 | } 16 | 17 | func NewLambertian(c Color) Material { 18 | return lambertian{albedo: c} 19 | } 20 | 21 | func (l lambertian) scatter(hr hitRecord) (ray, bool) { 22 | phi := 2 * math.Pi * rand.Float64() 23 | z := 2*rand.Float64() - 1 24 | r := (1 - z*z) 25 | random := NewVector(r*math.Cos(phi), r*math.Sin(phi), z) 26 | return newRay(hr.point, hr.normal.add(random)), true 27 | } 28 | 29 | func (l lambertian) attenuation() Color { 30 | return l.albedo 31 | } 32 | 33 | type metal struct { 34 | albedo Color 35 | fuzziness float64 36 | } 37 | 38 | func NewMetal(c Color, f float64) Material { 39 | if f <= 0 { 40 | f = 0 41 | } 42 | if f >= 1 { 43 | f = 1 44 | } 45 | return metal{albedo: c, fuzziness: f} 46 | } 47 | 48 | func (m metal) scatter(hr hitRecord) (ray, bool) { 49 | f := randomInUnitSphere().mul(m.fuzziness) 50 | reflected := reflect(hr.incident.direction, hr.normal).add(f) 51 | if reflected.dot(hr.normal) < 0 { 52 | return ray{}, false 53 | } 54 | return newRay(hr.point, reflected), true 55 | } 56 | 57 | func randomInUnitSphere() Vector { 58 | x, y, z := 0.0, 0.0, 0.0 59 | for true { 60 | x = 2*rand.Float64() - 1 61 | y = 2*rand.Float64() - 1 62 | z = 2*rand.Float64() - 1 63 | if x*x+y*y+z*z < 1 { 64 | break 65 | } 66 | } 67 | return NewVector(x, y, z) 68 | } 69 | 70 | func (m metal) attenuation() Color { 71 | return m.albedo 72 | } 73 | 74 | type dielectric struct { 75 | refractiveIndex float64 76 | } 77 | 78 | func NewDielectric(idx float64) Material { 79 | return dielectric{refractiveIndex: idx} 80 | } 81 | 82 | func (d dielectric) scatter(hr hitRecord) (ray, bool) { 83 | 84 | var relIdx float64 85 | if hr.incident.direction.dot(hr.normal) < 0 { 86 | relIdx = 1.0 / d.refractiveIndex 87 | } else { 88 | relIdx = d.refractiveIndex 89 | } 90 | 91 | in := hr.incident.direction 92 | orthogonal := in.sub(hr.normal.mul(in.dot(hr.normal))).mul(relIdx) 93 | 94 | if in.norm() < orthogonal.norm() { 95 | reflected := reflect(hr.incident.direction, hr.normal) 96 | return newRay(hr.point, reflected), true 97 | } 98 | 99 | if rand.Float64() < schlick(in, hr.normal, relIdx) { 100 | reflected := reflect(hr.incident.direction, hr.normal) 101 | return newRay(hr.point, reflected), true 102 | } 103 | 104 | var parallel Vector 105 | if hr.incident.direction.dot(hr.normal) < 0 { 106 | parallel = hr.normal.mul(-math.Sqrt(in.norm() - orthogonal.norm())) 107 | } else { 108 | parallel = hr.normal.mul(math.Sqrt(in.norm() - orthogonal.norm())) 109 | } 110 | 111 | refracted := orthogonal.add(parallel) 112 | return newRay(hr.point, refracted), true 113 | } 114 | 115 | func (d dielectric) attenuation() Color { 116 | return NewColor(1, 1, 1) 117 | } 118 | 119 | func reflect(in Vector, normal Vector) Vector { 120 | parallel := normal.mul(in.neg().dot(normal)) 121 | return in.add(parallel.mul(2)) 122 | } 123 | 124 | func schlick(in, normal Vector, relIdx float64) float64 { 125 | cos := in.dot(normal) / in.length() 126 | if cos < 0 { 127 | cos = -cos 128 | } 129 | r := (relIdx - 1) / (relIdx + 1) 130 | r0 := r * r 131 | return r0 + (1-r0)*math.Pow(1-cos, 5) 132 | } 133 | -------------------------------------------------------------------------------- /camera.go: -------------------------------------------------------------------------------- 1 | package raytracing 2 | 3 | import ( 4 | "math" 5 | "math/rand" 6 | ) 7 | 8 | type Camera struct { 9 | lookFrom Point 10 | horizontal Vector 11 | vertical Vector 12 | toLowerLeftCorner Vector 13 | u Vector 14 | v Vector 15 | lensRadius float64 16 | } 17 | 18 | type CameraConfig struct { 19 | aspectRatio float64 20 | lookFrom Point 21 | lookAt Point 22 | viewUp Vector 23 | verticalFOV float64 24 | aperture float64 25 | focusDistance float64 26 | } 27 | 28 | type CameraConfigOption func(*CameraConfig) 29 | 30 | func NewCameraConfig(aspectRatio float64, opts ...CameraConfigOption) CameraConfig { 31 | cfg := CameraConfig{ 32 | aspectRatio: aspectRatio, 33 | lookFrom: origin(), 34 | lookAt: NewPoint(0, 0, -1), 35 | viewUp: NewVector(0, 1, 0), 36 | verticalFOV: math.Pi / 2.0, 37 | aperture: 0.0, 38 | focusDistance: 1.0, 39 | } 40 | 41 | for _, opt := range opts { 42 | opt(&cfg) 43 | } 44 | 45 | return cfg 46 | } 47 | 48 | func WithAspectRatio(ratio float64) CameraConfigOption { 49 | return func(cfg *CameraConfig) { 50 | cfg.aspectRatio = ratio 51 | } 52 | } 53 | 54 | func WithPointOfView(from, to Point) CameraConfigOption { 55 | return func(cfg *CameraConfig) { 56 | cfg.lookFrom = from 57 | cfg.lookAt = to 58 | } 59 | } 60 | 61 | func WithViewUp(vup Vector) CameraConfigOption { 62 | return func(cfg *CameraConfig) { 63 | cfg.viewUp = vup 64 | } 65 | } 66 | 67 | func WithVerticalFOV(rad float64) CameraConfigOption { 68 | return func(cfg *CameraConfig) { 69 | cfg.verticalFOV = rad 70 | } 71 | } 72 | 73 | func WithAperture(aperture float64) CameraConfigOption { 74 | return func(cfg *CameraConfig) { 75 | cfg.aperture = aperture 76 | } 77 | } 78 | 79 | func WithFocusDistance(dist float64) CameraConfigOption { 80 | return func(cfg *CameraConfig) { 81 | cfg.focusDistance = dist 82 | } 83 | } 84 | 85 | func NewCamera(cfg CameraConfig) Camera { 86 | 87 | h := math.Tan(cfg.verticalFOV / 2) 88 | viewportHeight := 2 * h 89 | viewportWidth := viewportHeight * cfg.aspectRatio 90 | 91 | w := cfg.lookAt.to(cfg.lookFrom).normalize() 92 | u := cfg.viewUp.cross(w).normalize() 93 | v := w.cross(u) 94 | 95 | horizontal := u.mul(cfg.focusDistance * viewportWidth) 96 | vertical := v.mul(cfg.focusDistance * viewportHeight) 97 | toLowerLeftCorner := w.neg().mul(cfg.focusDistance). 98 | sub(horizontal.div(2)). 99 | sub(vertical.div(2)) 100 | 101 | c := Camera{ 102 | lookFrom: cfg.lookFrom, 103 | horizontal: horizontal, 104 | vertical: vertical, 105 | toLowerLeftCorner: toLowerLeftCorner, 106 | u: u, 107 | v: v, 108 | lensRadius: cfg.aperture / 2.0, 109 | } 110 | 111 | return c 112 | } 113 | 114 | func (c Camera) castRay(s, t float64) ray { 115 | r := randomInUnitDisk().mul(c.lensRadius) 116 | offset := c.u.mul(r.x).add(c.v.mul(r.y)) 117 | return newRay( 118 | origin().to(c.lookFrom).add(offset).point(), 119 | c.toLowerLeftCorner. 120 | add(c.horizontal.mul(s)).add(c.vertical.mul(t)). 121 | sub(offset), 122 | ) 123 | } 124 | 125 | func randomInUnitDisk() Vector { 126 | x, y := 0.0, 0.0 127 | for true { 128 | x = 2*rand.Float64() - 1 129 | y = 2*rand.Float64() - 1 130 | if x*x+y*y < 1 { 131 | break 132 | } 133 | } 134 | return NewVector(x, y, 0.0) 135 | } 136 | -------------------------------------------------------------------------------- /renderer.go: -------------------------------------------------------------------------------- 1 | package raytracing 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "math" 7 | "math/rand" 8 | "os" 9 | "time" 10 | ) 11 | 12 | type Renderer struct { 13 | imageWidth int 14 | imageHeight int 15 | imageWriter io.Writer 16 | logWriter io.Writer 17 | samplesPerPixel int 18 | maxDepth int 19 | } 20 | 21 | type RendererConfig struct { 22 | imageWidth int 23 | imageHeight int 24 | imageWriter io.Writer 25 | logWriter io.Writer 26 | samplesPerPixel int 27 | maxDepth int 28 | } 29 | 30 | type RendererConfigOption func(*RendererConfig) 31 | 32 | func NewRendererConfig(width, height int, opts ...RendererConfigOption) RendererConfig { 33 | cfg := RendererConfig{ 34 | imageWidth: width, 35 | imageHeight: height, 36 | imageWriter: os.Stdout, 37 | logWriter: os.Stderr, 38 | samplesPerPixel: 100, 39 | maxDepth: 50, 40 | } 41 | 42 | for _, opt := range opts { 43 | opt(&cfg) 44 | } 45 | 46 | return cfg 47 | } 48 | 49 | func NewRenderer(cfg RendererConfig) Renderer { 50 | return Renderer{ 51 | imageWidth: cfg.imageWidth, 52 | imageHeight: cfg.imageHeight, 53 | imageWriter: cfg.imageWriter, 54 | logWriter: cfg.logWriter, 55 | samplesPerPixel: cfg.samplesPerPixel, 56 | maxDepth: cfg.maxDepth, 57 | } 58 | } 59 | 60 | func WithImageWriter(w io.Writer) RendererConfigOption { 61 | return func(cfg *RendererConfig) { 62 | cfg.imageWriter = w 63 | } 64 | } 65 | 66 | func WithLogWriter(w io.Writer) RendererConfigOption { 67 | return func(cfg *RendererConfig) { 68 | cfg.logWriter = w 69 | } 70 | } 71 | 72 | func WithSamplesPerPixel(samples int) RendererConfigOption { 73 | return func(cfg *RendererConfig) { 74 | cfg.samplesPerPixel = samples 75 | } 76 | } 77 | 78 | func WithMaxTraceDepth(depth int) RendererConfigOption { 79 | return func(cfg *RendererConfig) { 80 | cfg.maxDepth = depth 81 | } 82 | } 83 | 84 | func (r Renderer) Render(scene Scene, camera Camera) { 85 | 86 | rand.Seed(time.Now().UnixNano()) 87 | 88 | fmt.Fprintln(r.imageWriter, "P3") 89 | fmt.Fprintf(r.imageWriter, "%d %d\n", r.imageWidth, r.imageHeight) 90 | fmt.Fprintln(r.imageWriter, 255) 91 | 92 | for j := r.imageHeight - 1; j >= 0; j-- { 93 | for i := 0; i < r.imageWidth; i++ { 94 | fmt.Fprintf(r.logWriter, "\rScanlines remaining: %d", j) 95 | color := NewColor(0, 0, 0) 96 | for s := 0; s < r.samplesPerPixel; s++ { 97 | u := (float64(i) + rand.Float64()) / float64(r.imageWidth-1) 98 | v := (float64(j) + rand.Float64()) / float64(r.imageHeight-1) 99 | c := rayColor(camera.castRay(u, v), scene, r.maxDepth) 100 | color = NewColor(color.x+c.x, color.y+c.y, color.z+c.z) 101 | } 102 | writeColor(r.imageWriter, color, r.samplesPerPixel) 103 | } 104 | } 105 | fmt.Fprintln(r.logWriter, "\nDone.") 106 | } 107 | 108 | func rayColor(r ray, scene Scene, depth int) Color { 109 | 110 | if depth <= 0 { 111 | return NewColor(0, 0, 0) 112 | } 113 | 114 | hr, ok := scene.hit(r, 0.001, math.MaxFloat64) 115 | if !ok { 116 | return backgroundColor(r) 117 | } 118 | 119 | scattered, ok := hr.material.scatter(hr) 120 | if !ok { 121 | return NewColor(0, 0, 0) 122 | } 123 | att := hr.material.attenuation() 124 | c := rayColor(scattered, scene, depth-1) 125 | return NewColor(att.x*c.x, att.y*c.y, att.z*c.z) 126 | } 127 | 128 | func backgroundColor(r ray) Color { 129 | unit := r.direction.normalize() 130 | x := 0.5 * (unit.y + 1) 131 | return NewColor((1-x)*1.0+x*0.5, (1-x)*1.0+x*0.7, (1-x)*1.0+x*1.0) 132 | } 133 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------