diff --git a/examples/direct/direct.go b/examples/direct/direct.go index 8adc804..5d44648 100644 --- a/examples/direct/direct.go +++ b/examples/direct/direct.go @@ -11,6 +11,7 @@ import ( "github.com/hunterloftis/pbr/surface" ) +// The pathological case for indirect lighting: a small, very bright light at a large distance func main() { floor := surface.UnitCube().Move(0, -1, 0).Scale(100, 1, 100) halogen := material.Light(10000000, 10000000, 10000000) diff --git a/examples/moses/moses.go b/examples/moses/moses.go new file mode 100644 index 0000000..b83a7b3 --- /dev/null +++ b/examples/moses/moses.go @@ -0,0 +1,37 @@ +package main + +import ( + "fmt" + "os" + "os/signal" + "syscall" + + "github.com/hunterloftis/pbr" + "github.com/hunterloftis/pbr/material" + "github.com/hunterloftis/pbr/obj" + "github.com/hunterloftis/pbr/surface" +) + +func main() { + moses, err := obj.ReadFile("fixtures/models/moses/model.obj", false) + if err != nil { + panic(err) + } + key := surface.UnitSphere(material.Light(100000, 100000, 50000)).Move(-20, 10, 20).Scale(5, 5, 5) + fill := surface.UnitSphere(material.Light(20000, 20000, 50000)).Move(30, 10, 5).Scale(5, 5, 5) + back := surface.UnitSphere(material.Light(25000, 25000, 100000)).Move(-30, -5, -10).Scale(8, 8, 8) + scene := pbr.NewScene(moses...) + bounds, _ := scene.Info() + target := bounds.Center + scene.Add(key, fill, back) + cam := pbr.NewCamera(888, 500).MoveTo(0, -10, 50).LookAt(target, target) + render := pbr.NewRender(scene, cam) + interrupt := make(chan os.Signal, 2) + + fmt.Println("rendering moses.png (press Ctrl+C to finish)...") + signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM) + render.Start() + <-interrupt + render.Stop() + render.WritePngs("moses.png", "moses-heat.png", "moses-noise.png", 1) +} diff --git a/examples/shapes/shapes.go b/examples/shapes/shapes.go index 052098d..0d9eb56 100644 --- a/examples/shapes/shapes.go +++ b/examples/shapes/shapes.go @@ -3,7 +3,9 @@ package main import ( "fmt" "math" - "time" + "os" + "os/signal" + "syscall" "github.com/hunterloftis/pbr" "github.com/hunterloftis/pbr/geom" @@ -18,7 +20,7 @@ func main() { whitePlastic := material.Plastic(1, 1, 1, 0.2) bluePlastic := material.Plastic(0, 0, 0.9, 0) greenPlastic := material.Plastic(0, 0.9, 0, 0) - gold := material.Metal(1.022, 0.782, 0.344, 0.9, 0) + gold := material.Metal(1.022, 0.782, 0.344, 0.1, 1) greenGlass := material.Glass(0.2, 1, 0.1, 0.05) scene := pbr.NewScene() @@ -42,9 +44,11 @@ func main() { surface.UnitSphere(gold).Move(0.45, 0.05, -0.4).Scale(0.2, 0.2, 0.2), ) - fmt.Println("rendering shapes.png (15 mins)...") + interrupt := make(chan os.Signal, 2) + fmt.Println("rendering shapes.png (press Ctrl+C to finish)...") + signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM) render.Start() - time.Sleep(time.Minute * 15) + <-interrupt render.Stop() render.WritePngs("shapes.png", "shapes-heat.png", "shapes-noise.png", 1) } diff --git a/geom/direction.go b/geom/direction.go index d6c1005..b4d5d2b 100644 --- a/geom/direction.go +++ b/geom/direction.go @@ -52,6 +52,7 @@ func (a Direction) Scaled(n float64) Vector3 { } // Cross returns the cross product of unit vectors a and b. +// TODO: Has floating point error - acceptable? func (a Direction) Cross(b Direction) Direction { return Direction{a.Y*b.Z - a.Z*b.Y, a.Z*b.X - a.X*b.Z, a.X*b.Y - a.Y*b.X} } diff --git a/material/map.go b/material/map.go index 6de8014..d4143d9 100644 --- a/material/map.go +++ b/material/map.go @@ -95,9 +95,8 @@ func (m *Map) At(u, v float64) *Sample { y += height } r, g, b, a := m.d.Texture.At(x, y).RGBA() - // fmt.Println("rgb:", r, g, b, a) - // panic("ok") s.Color = rgb.Energy{float64(r), float64(g), float64(b)}.Amplified(1 / float64(a)) + // TODO: deal with metals; s.Fresnel = s.Fresnel.Blend(s.Color, s.Metal)? } return s } diff --git a/obj/scanner.go b/obj/scanner.go index e24fe2d..4041952 100644 --- a/obj/scanner.go +++ b/obj/scanner.go @@ -36,7 +36,7 @@ func NewScanner(r io.Reader) *Scanner { vn: make([]geom.Direction, 0), vt: make([]geom.Vector3, 0), lib: make(map[string]*material.Map), - mat: material.Plastic(1, 1, 1, 0.7), + mat: material.Default, } } diff --git a/sampler.go b/sampler.go index 987afb7..9dceedd 100644 --- a/sampler.go +++ b/sampler.go @@ -42,6 +42,8 @@ func (s *sampler) start(buffer *rgb.Framebuffer, in <-chan int, done chan<- samp }() } +// TODO: sample Specular reflections from direct light sources and weight results by their BSDF towards the light +// Or, better, sample lights directly in general and pass that through a unified BSDF func (s *sampler) tracePrimary(x, y int, rnd *rand.Rand) (energy rgb.Energy) { ray := s.camera.ray(float64(x), float64(y), rnd) hit := s.scene.Intersect(ray) @@ -84,8 +86,7 @@ func (s *sampler) traceIndirect(ray *geom.Ray3, depth int, signal rgb.Energy, rn normal, mat := hit.Surface.At(point) energy = energy.Merged(mat.Light, signal) dir, strength, diffused := mat.Bsdf(normal, ray.Dir, hit.Dist, rnd) - lights := s.scene.Lights() - if diffused && lights > 0 { + if lights := s.scene.Lights(); diffused && lights > 0 { direct, coverage := s.traceDirect(lights, point, normal, rnd) energy = energy.Merged(direct.Strength(mat.Color), signal) signal = signal.Amplified(1 - coverage) @@ -98,9 +99,8 @@ func (s *sampler) traceDirect(num int, point geom.Vector3, normal geom.Direction limit := int(math.Min(float64(s.direct), float64(num))) for i := 0; i < limit; i++ { light := s.scene.Light(rnd) - ray, solidAngle := light.Box().ShadowRay(point, rnd) - cos := ray.Dir.Cos(normal) - if cos <= 0 { + ray, solidAngle := light.Box().ShadowRay(point, normal, rnd) + if solidAngle <= 0 { break } coverage += solidAngle @@ -108,7 +108,7 @@ func (s *sampler) traceDirect(num int, point geom.Vector3, normal geom.Direction if !hit.Ok { break } - e := hit.Surface.Material().Emit().Amplified(solidAngle * cos / math.Pi) + e := hit.Surface.Material().Emit().Amplified(solidAngle / math.Pi) energy = energy.Plus(e) } return energy, coverage diff --git a/scene.go b/scene.go index f52e2ac..16070e2 100644 --- a/scene.go +++ b/scene.go @@ -48,7 +48,6 @@ func NewScene(surfaces ...surface.Surface) *Scene { func (s *Scene) Intersect(ray *geom.Ray3) surface.Hit { atomic.AddUint64(&s.rays, 1) return s.tree.Intersect(ray) - // return s.tree.IntersectSurfaces(ray, math.Inf(1)) } // Rays returns the total count of Ray/Scene intersections tested since the Scene was created. diff --git a/surface/box.go b/surface/box.go index 3d918af..e305780 100644 --- a/surface/box.go +++ b/surface/box.go @@ -95,15 +95,15 @@ func (b *Box) Contains(p geom.Vector3) bool { // chooses a random point within that disc, // and returns a Ray3 from the origin to the random point. // https://marine.rutgers.edu/dmcs/ms552/2009/solidangle.pdf -func (b *Box) ShadowRay(origin geom.Vector3, rnd *rand.Rand) (*geom.Ray3, float64) { - norm := origin.Minus(b.Center).Unit() - center2 := b.Center.Plus(norm.Scaled(-b.Radius)) - x, y := geom.RandPointInCircle(b.Radius, rnd) - right := norm.Cross(geom.Up) - up := right.Cross(norm) - point := center2.Plus(right.Scaled(x)).Plus(up.Scaled(y)) +func (b *Box) ShadowRay(origin geom.Vector3, normal geom.Direction, rnd *rand.Rand) (*geom.Ray3, float64) { + forward := origin.Minus(b.Center).Unit() + x, y := geom.RandPointInCircle(b.Radius, rnd) // TODO: push center back along "forward" axis, away from origin + right := forward.Cross(geom.Up) + up := right.Cross(forward) + point := b.Center.Plus(right.Scaled(x)).Plus(up.Scaled(y)) ray := geom.NewRay(origin, point.Minus(origin).Unit()) // TODO: this should be a convenience method - dist := center2.Minus(origin).Len() - weight := (b.Radius * b.Radius) / (2 * dist * dist) // cosine-weighted ratio of disc surface area to hemisphere surface area - return ray, weight + dist := b.Center.Minus(origin).Len() + cos := ray.Dir.Cos(normal) + solidAngle := cos * (b.Radius * b.Radius) / (2 * dist * dist) // cosine-weighted ratio of disc surface area to hemisphere surface area + return ray, solidAngle } diff --git a/surface/cube.go b/surface/cube.go index 25dfe42..4f742db 100644 --- a/surface/cube.go +++ b/surface/cube.go @@ -32,9 +32,9 @@ func (c *Cube) transform(m *geom.Matrix4) *Cube { c.Pos = c.Pos.Mult(m) min := c.Pos.MultPoint(geom.Vector3{}) max := c.Pos.MultPoint(geom.Vector3{}) - for x := -1.0; x <= 1; x += 2 { - for y := -1.0; y <= 1; y += 2 { - for z := -1.0; z <= 1; z += 2 { + for x := -0.5; x <= 0.5; x += 1 { + for y := -0.5; y <= 0.5; y += 1 { + for z := -0.5; z <= 0.5; z += 1 { pt := c.Pos.MultPoint(geom.Vector3{x, y, z}) min = min.Min(pt) max = max.Max(pt) diff --git a/surface/sphere.go b/surface/sphere.go index ef92e11..1f1fdd9 100644 --- a/surface/sphere.go +++ b/surface/sphere.go @@ -32,9 +32,9 @@ func (s *Sphere) transform(t *geom.Matrix4) *Sphere { s.Pos = s.Pos.Mult(t) min := s.Pos.MultPoint(geom.Vector3{}) max := s.Pos.MultPoint(geom.Vector3{}) - for x := -1.0; x <= 1; x += 2 { - for y := -1.0; y <= 1; y += 2 { - for z := -1.0; z <= 1; z += 2 { + for x := -0.5; x <= 0.5; x += 1 { + for y := -0.5; y <= 0.5; y += 1 { + for z := -0.5; z <= 0.5; z += 1 { pt := s.Pos.MultPoint(geom.Vector3{x, y, z}) min = min.Min(pt) max = max.Max(pt) @@ -71,6 +71,7 @@ func (s *Sphere) Box() *Box { // Intersect tests whether the sphere intersects a given ray. // http://tfpsly.free.fr/english/index.html?url=http://tfpsly.free.fr/english/3d/Raytracing.html +// TODO: http://kylehalladay.com/blog/tutorial/math/2013/12/24/Ray-Sphere-Intersection.html func (s *Sphere) Intersect(ray *geom.Ray3) Hit { if ok, _, _ := s.box.Check(ray); !ok { return Miss