Skip to content

Instantly share code, notes, and snippets.

@peterhellberg
Created January 28, 2019 16:16
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save peterhellberg/972c15b774d4cb5de2baa64e5feccf41 to your computer and use it in GitHub Desktop.
Save peterhellberg/972c15b774d4cb5de2baa64e5feccf41 to your computer and use it in GitHub Desktop.
tinykaboom with gfx
package main
import (
"image"
"image/color"
"math"
"github.com/peterhellberg/gfx"
)
var (
sphereRadius = 1.5
noiseAmplitude = 0.2
)
func main() {
frame := render(640, 480, math.Pi/3.0)
gfx.SavePNG("/tmp/gfx-tinykaboom-step5.png", frame)
}
func render(width, height int, fov float64) *Frame {
frame := NewFrame(width, height)
fw, fh := float64(width), float64(height)
for j := 0; j < height; j++ {
for i := 0; i < width; i++ {
fi, fj := float64(i), float64(j)
dir := gfx.V3((fi+0.5)-fw/2, -(fj+0.5)+fh/2, -fh/(2*math.Tan(fov/2)))
if hit, ok := sphereTrace(gfx.V3(0, 0, 3), dir.Norm()); ok {
ld := gfx.V3(10, 10, 10).Sub(hit).Norm()
li := math.Max(0.4, ld.Dot(distanceFieldNormal(hit)))
frame.Buffer[i+j*width] = gfx.V3(1, 1, 1).Mul(li)
} else {
frame.Buffer[i+j*width] = gfx.V3(0.1, 0.1, 0.1)
}
}
}
return frame
}
func signedDistance(p gfx.Vec3) float64 {
d := math.Sin(16*p.X) * math.Sin(16*p.Y) * math.Sin(16*p.Z) * noiseAmplitude
return p.SqLen() - (sphereRadius + d)
}
func sphereTrace(orig, dir gfx.Vec3) (gfx.Vec3, bool) {
pos := orig
for i := 0; i < 128; i++ {
d := signedDistance(pos)
if d < 0 {
return pos, true
}
pos = pos.Add(dir.Mul(math.Max(d*0.1, 0.01)))
}
return pos, false
}
func distanceFieldNormal(pos gfx.Vec3) gfx.Vec3 {
const eps = 0.1
d := signedDistance(pos)
nx := signedDistance(pos.Add(gfx.V3(eps, 0, 0))) - d
ny := signedDistance(pos.Add(gfx.V3(0, eps, 0))) - d
nz := signedDistance(pos.Add(gfx.V3(0, 0, eps))) - d
return gfx.V3(nx, ny, nz).Norm()
}
type Frame struct {
Buffer []gfx.Vec3
bounds image.Rectangle
gfx.Float64Scaler
}
func NewFrame(w, h int) *Frame {
return &Frame{
Buffer: make([]gfx.Vec3, w*h),
bounds: gfx.IR(0, 0, w, h),
Float64Scaler: gfx.NewLinearScaler().Domain(0, 1).Range(0, 255),
}
}
func (f *Frame) At(x, y int) color.Color {
v := f.Buffer[x+y*f.bounds.Max.X]
return color.NRGBA{
uint8(f.ScaleFloat64(v.X)),
uint8(f.ScaleFloat64(v.Y)),
uint8(f.ScaleFloat64(v.Z)),
255,
}
}
func (f *Frame) Bounds() image.Rectangle {
return f.bounds
}
func (f *Frame) ColorModel() color.Model {
return color.NRGBAModel
}
@peterhellberg
Copy link
Author

peterhellberg commented Jan 28, 2019

@peterhellberg
Copy link
Author

peterhellberg commented Jan 28, 2019

Step 1: draw one sphere at the screen

package main

import (
	"image"
	"image/color"
	"math"

	"github.com/peterhellberg/gfx"
)

const sphereRadius = 0.47

func main() {
	var (
		fov    = math.Pi / 3.0
		width  = 640
		height = 480
		frame  = render(width, height, fov)
	)

	gfx.SavePNG("/tmp/gfx-tinykaboom-step1.png", frame)
}

func signedDistance(p gfx.Vec3) float64 {
	return p.Norm().Dot(gfx.V3(0, 0, sphereRadius))
}

func sphereTrace(orig, dir gfx.Vec3) (gfx.Vec3, bool) {
	pos := orig

	for i := 0; i < 128; i++ {
		d := signedDistance(pos)
		if d < 0 {
			return pos, true
		}

		pos = pos.Add(dir.Mul(math.Max(d*0.1, 0.01)))
	}

	return pos, false
}

func render(width, height int, fov float64) *Frame {
	frame := NewFrame(width, height)

	fw, fh := float64(width), float64(height)

	for j := 0; j < height; j++ {
		for i := 0; i < width; i++ {
			fi, fj := float64(i), float64(j)

			var dir gfx.Vec3

			dir.X = (fi + 0.5) - fw/2
			dir.Y = -(fj + 0.5) + fh/2
			dir.Z = -fh / (2 * math.Tan(fov/2))

			if _, ok := sphereTrace(gfx.V3(0, 0, 3), dir.Norm()); ok {
				frame.Buffer[i+j*width] = gfx.V3(1, 1, 1)
			} else {
				frame.Buffer[i+j*width] = gfx.V3(0.2, 0.7, 0.8)
			}

		}
	}

	return frame
}

type Frame struct {
	Buffer []gfx.Vec3
	bounds image.Rectangle
	gfx.Float64Scaler
}

func NewFrame(w, h int) *Frame {
	return &Frame{
		Buffer:        make([]gfx.Vec3, w*h),
		bounds:        gfx.IR(0, 0, w, h),
		Float64Scaler: gfx.NewLinearScaler().Domain(0, 1).Range(0, 255),
	}
}

func (f *Frame) At(x, y int) color.Color {
	v := f.Buffer[x+y*f.bounds.Max.X]

	return color.NRGBA{
		uint8(f.ScaleFloat64(v.X)),
		uint8(f.ScaleFloat64(v.Y)),
		uint8(f.ScaleFloat64(v.Z)),
		255,
	}
}

func (f *Frame) Bounds() image.Rectangle {
	return f.bounds
}

func (f *Frame) ColorModel() color.Model {
	return color.NRGBAModel
}

gfx-tinykaboom-step1

@peterhellberg
Copy link
Author

peterhellberg commented Jan 28, 2019

Step 2: diffuse lighting

package main

import (
	"image"
	"image/color"
	"math"

	"github.com/peterhellberg/gfx"
)

const sphereRadius = 1.5

func main() {
	frame := render(640, 480, math.Pi/3.0)

	gfx.SavePNG("/tmp/gfx-tinykaboom-step2.png", frame)
}

func render(width, height int, fov float64) *Frame {
	frame := NewFrame(width, height)

	fw, fh := float64(width), float64(height)

	for j := 0; j < height; j++ {
		for i := 0; i < width; i++ {
			fi, fj := float64(i), float64(j)

			dir := gfx.V3((fi+0.5)-fw/2, -(fj+0.5)+fh/2, -fh/(2*math.Tan(fov/2)))

			if hit, ok := sphereTrace(gfx.V3(0, 0, 3), dir.Norm()); ok {
				ld := gfx.V3(10, 10, 10).Sub(hit).Norm()
				li := math.Max(0.4, ld.Dot(distanceFieldNormal(hit)))

				frame.Buffer[i+j*width] = gfx.V3(1, 1, 1).Mul(li)
			} else {
				frame.Buffer[i+j*width] = gfx.V3(0.2, 0.7, 0.8)
			}
		}
	}

	return frame
}

func signedDistance(p gfx.Vec3) float64 {
	return p.SqLen() - sphereRadius
}

func sphereTrace(orig, dir gfx.Vec3) (gfx.Vec3, bool) {
	pos := orig

	for i := 0; i < 128; i++ {
		d := signedDistance(pos)
		if d < 0 {
			return pos, true
		}

		pos = pos.Add(dir.Mul(math.Max(d*0.1, 0.01)))
	}

	return pos, false
}

func distanceFieldNormal(pos gfx.Vec3) gfx.Vec3 {
	const eps = 0.1

	d := signedDistance(pos)

	nx := signedDistance(pos.Add(gfx.V3(eps, 0, 0))) - d
	ny := signedDistance(pos.Add(gfx.V3(0, eps, 0))) - d
	nz := signedDistance(pos.Add(gfx.V3(0, 0, eps))) - d

	return gfx.V3(nx, ny, nz).Norm()
}

type Frame struct {
	Buffer []gfx.Vec3
	bounds image.Rectangle
	gfx.Float64Scaler
}

func NewFrame(w, h int) *Frame {
	return &Frame{
		Buffer:        make([]gfx.Vec3, w*h),
		bounds:        gfx.IR(0, 0, w, h),
		Float64Scaler: gfx.NewLinearScaler().Domain(0, 1).Range(0, 255),
	}
}

func (f *Frame) At(x, y int) color.Color {
	v := f.Buffer[x+y*f.bounds.Max.X]

	return color.NRGBA{
		uint8(f.ScaleFloat64(v.X)),
		uint8(f.ScaleFloat64(v.Y)),
		uint8(f.ScaleFloat64(v.Z)),
		255,
	}
}

func (f *Frame) Bounds() image.Rectangle {
	return f.bounds
}

func (f *Frame) ColorModel() color.Model {
	return color.NRGBAModel
}

gfx-tinykaboom-step2

@peterhellberg
Copy link
Author

peterhellberg commented Jan 28, 2019

Step 3: let us draw a pattern on our sphere

package main

import (
	"image"
	"image/color"
	"math"

	"github.com/peterhellberg/gfx"
)

const (
	sphereRadius   = 1.5
	noiseAmplitude = 0.2
)

func main() {
	frame := render(640, 480, math.Pi/3.0)

	gfx.SavePNG("/tmp/gfx-tinykaboom-step3.png", frame)
}

func render(width, height int, fov float64) *Frame {
	frame := NewFrame(width, height)

	fw, fh := float64(width), float64(height)

	for j := 0; j < height; j++ {
		for i := 0; i < width; i++ {
			fi, fj := float64(i), float64(j)

			dir := gfx.V3((fi+0.5)-fw/2, -(fj+0.5)+fh/2, -fh/(2*math.Tan(fov/2)))

			if hit, ok := sphereTrace(gfx.V3(0, 0, 3), dir.Norm()); ok {
				ld := gfx.V3(10, 10, 10).Sub(hit).Norm()
				li := math.Max(0.4, ld.Dot(distanceFieldNormal(hit)))

				d := (math.Sin(16*hit.X)*math.Sin(16*hit.Y)*math.Sin(16*hit.Z) + 1) / 2

				frame.Buffer[i+j*width] = gfx.V3(1, 1, 1).Mul(d * li)
			} else {
				frame.Buffer[i+j*width] = gfx.V3(0.1, 0.1, 0.1)
			}
		}
	}

	return frame
}

func signedDistance(p gfx.Vec3) float64 {
	return p.SqLen() - sphereRadius
}

func sphereTrace(orig, dir gfx.Vec3) (gfx.Vec3, bool) {
	pos := orig

	for i := 0; i < 128; i++ {
		d := signedDistance(pos)
		if d < 0 {
			return pos, true
		}

		pos = pos.Add(dir.Mul(math.Max(d*0.1, 0.01)))
	}

	return pos, false
}

func distanceFieldNormal(pos gfx.Vec3) gfx.Vec3 {
	const eps = 0.1

	d := signedDistance(pos)

	nx := signedDistance(pos.Add(gfx.V3(eps, 0, 0))) - d
	ny := signedDistance(pos.Add(gfx.V3(0, eps, 0))) - d
	nz := signedDistance(pos.Add(gfx.V3(0, 0, eps))) - d

	return gfx.V3(nx, ny, nz).Norm()
}

type Frame struct {
	Buffer []gfx.Vec3
	bounds image.Rectangle
	gfx.Float64Scaler
}

func NewFrame(w, h int) *Frame {
	return &Frame{
		Buffer:        make([]gfx.Vec3, w*h),
		bounds:        gfx.IR(0, 0, w, h),
		Float64Scaler: gfx.NewLinearScaler().Domain(0, 1).Range(0, 255),
	}
}

func (f *Frame) At(x, y int) color.Color {
	v := f.Buffer[x+y*f.bounds.Max.X]

	return color.NRGBA{
		uint8(f.ScaleFloat64(v.X)),
		uint8(f.ScaleFloat64(v.Y)),
		uint8(f.ScaleFloat64(v.Z)),
		255,
	}
}

func (f *Frame) Bounds() image.Rectangle {
	return f.bounds
}

func (f *Frame) ColorModel() color.Model {
	return color.NRGBAModel
}

gfx-tinykaboom-step3

@peterhellberg
Copy link
Author

peterhellberg commented Jan 28, 2019

Step 4: displacement mapping

package main

import (
	"image"
	"image/color"
	"math"

	"github.com/peterhellberg/gfx"
)

const (
	sphereRadius   = 1.5
	noiseAmplitude = 0.2
)

func main() {
	frame := render(640, 480, math.Pi/3.0)

	gfx.SavePNG("/tmp/gfx-tinykaboom-step4.png", frame)
}

func render(width, height int, fov float64) *Frame {
	frame := NewFrame(width, height)

	fw, fh := float64(width), float64(height)

	for j := 0; j < height; j++ {
		for i := 0; i < width; i++ {
			fi, fj := float64(i), float64(j)

			dir := gfx.V3((fi+0.5)-fw/2, -(fj+0.5)+fh/2, -fh/(2*math.Tan(fov/2)))

			if hit, ok := sphereTrace(gfx.V3(0, 0, 3), dir.Norm()); ok {
				ld := gfx.V3(10, 10, 10).Sub(hit).Norm()
				li := math.Max(0.4, ld.Dot(distanceFieldNormal(hit)))

				frame.Buffer[i+j*width] = gfx.V3(1, 1, 1).Mul(li)
			} else {
				frame.Buffer[i+j*width] = gfx.V3(0.1, 0.1, 0.1)
			}
		}
	}

	return frame
}

func signedDistance(p gfx.Vec3) float64 {
	s := p.Norm().Mul(sphereRadius)
	d := math.Sin(16*s.X) * math.Sin(16*s.Y) * math.Sin(16*s.Z) * noiseAmplitude

	return p.SqLen() - sphereRadius + d
}

func sphereTrace(orig, dir gfx.Vec3) (gfx.Vec3, bool) {
	pos := orig

	for i := 0; i < 128; i++ {
		d := signedDistance(pos)
		if d < 0 {
			return pos, true
		}

		pos = pos.Add(dir.Mul(math.Max(d*0.1, 0.01)))
	}

	return pos, false
}

func distanceFieldNormal(pos gfx.Vec3) gfx.Vec3 {
	const eps = 0.1

	d := signedDistance(pos)

	nx := signedDistance(pos.Add(gfx.V3(eps, 0, 0))) - d
	ny := signedDistance(pos.Add(gfx.V3(0, eps, 0))) - d
	nz := signedDistance(pos.Add(gfx.V3(0, 0, eps))) - d

	return gfx.V3(nx, ny, nz).Norm()
}

type Frame struct {
	Buffer []gfx.Vec3
	bounds image.Rectangle
	gfx.Float64Scaler
}

func NewFrame(w, h int) *Frame {
	return &Frame{
		Buffer:        make([]gfx.Vec3, w*h),
		bounds:        gfx.IR(0, 0, w, h),
		Float64Scaler: gfx.NewLinearScaler().Domain(0, 1).Range(0, 255),
	}
}

func (f *Frame) At(x, y int) color.Color {
	v := f.Buffer[x+y*f.bounds.Max.X]

	return color.NRGBA{
		uint8(f.ScaleFloat64(v.X)),
		uint8(f.ScaleFloat64(v.Y)),
		uint8(f.ScaleFloat64(v.Z)),
		255,
	}
}

func (f *Frame) Bounds() image.Rectangle {
	return f.bounds
}

func (f *Frame) ColorModel() color.Model {
	return color.NRGBAModel
}

gfx-tinykaboom-step4

@peterhellberg
Copy link
Author

peterhellberg commented Jan 28, 2019

Step 5: one more implicit surface

package main

import (
	"image"
	"image/color"
	"math"

	"github.com/peterhellberg/gfx"
)

var (
	sphereRadius   = 1.0
	noiseAmplitude = 0.9
)

func main() {
	frame := render(640, 480, math.Pi/3.0)

	gfx.SavePNG("/tmp/gfx-tinykaboom-step5.png", frame)
}

func render(width, height int, fov float64) *Frame {
	frame := NewFrame(width, height)

	fw, fh := float64(width), float64(height)

	for j := 0; j < height; j++ {
		for i := 0; i < width; i++ {
			fi, fj := float64(i), float64(j)

			dir := gfx.V3((fi+0.5)-fw/2, -(fj+0.5)+fh/2, -fh/(2*math.Tan(fov/2)))

			if hit, ok := sphereTrace(gfx.V3(0, 0, 3), dir.Norm()); ok {
				ld := gfx.V3(10, 10, 10).Sub(hit).Norm()
				li := math.Max(0.4, ld.Dot(distanceFieldNormal(hit)))

				frame.Buffer[i+j*width] = gfx.V3(1, 1, 1).Mul(li)
			} else {
				frame.Buffer[i+j*width] = gfx.V3(0.1, 0.1, 0.1)
			}
		}
	}

	return frame
}

func signedDistance(p gfx.Vec3) float64 {
	d := math.Sin(8*p.X) * math.Sin(8*p.Y) * math.Sin(8*p.Z) * noiseAmplitude

	return p.SqLen() - (sphereRadius + d)
}

func sphereTrace(orig, dir gfx.Vec3) (gfx.Vec3, bool) {
	pos := orig

	for i := 0; i < 256; i++ {
		d := signedDistance(pos)
		if d < 0 && pos.Z > 0 {
			return pos, true
		}

		pos = pos.Add(dir.Mul(math.Max(d*0.1, 0.01)))
	}

	return pos, false
}

func distanceFieldNormal(pos gfx.Vec3) gfx.Vec3 {
	const eps = 0.1

	d := signedDistance(pos)

	nx := signedDistance(pos.Add(gfx.V3(eps, 0, 0))) - d
	ny := signedDistance(pos.Add(gfx.V3(0, eps, 0))) - d
	nz := signedDistance(pos.Add(gfx.V3(0, 0, eps))) - d

	return gfx.V3(nx, ny, nz).Norm()
}

type Frame struct {
	Buffer []gfx.Vec3
	bounds image.Rectangle
	gfx.Float64Scaler
}

func NewFrame(w, h int) *Frame {
	return &Frame{
		Buffer:        make([]gfx.Vec3, w*h),
		bounds:        gfx.IR(0, 0, w, h),
		Float64Scaler: gfx.NewLinearScaler().Domain(0, 1).Range(0, 255),
	}
}

func (f *Frame) At(x, y int) color.Color {
	v := f.Buffer[x+y*f.bounds.Max.X]

	return color.NRGBA{
		uint8(f.ScaleFloat64(v.X)),
		uint8(f.ScaleFloat64(v.Y)),
		uint8(f.ScaleFloat64(v.Z)),
		255,
	}
}

func (f *Frame) Bounds() image.Rectangle {
	return f.bounds
}

func (f *Frame) ColorModel() color.Model {
	return color.NRGBAModel
}

gfx-tinykaboom-step5

Step 5: Animation

package main

import (
	"image"
	"image/color"
	"math"

	"github.com/peterhellberg/gfx"
)

var (
	sphereRadius   = 1.0
	noiseAmplitude = 0.0
)

func main() {
	a := &gfx.Animation{Delay: 25}

	for _, na := range []float64{
		.1, .2, .3, .4, .5, .6, .7, .6, .5, .4, .3, .2,
	} {
		noiseAmplitude = na

		frame := render(640, 480, math.Pi/3.0)

		p := gfx.NewPalettedImage(frame.Bounds(), gfx.PaletteEDG36[0:6])

		gfx.DrawSrc(p, p.Bounds(), frame, gfx.ZP)

		a.AddPalettedImage(p)
	}

	a.SaveGIF("/tmp/gfx-tinykaboom-step5-animation.gif")
}

func render(width, height int, fov float64) *Frame {
	frame := NewFrame(width, height)

	fw, fh := float64(width), float64(height)

	for j := 0; j < height; j++ {
		for i := 0; i < width; i++ {
			fi, fj := float64(i), float64(j)

			dir := gfx.V3((fi+0.5)-fw/2, -(fj+0.5)+fh/2, -fh/(2*math.Tan(fov/2)))

			if hit, ok := sphereTrace(gfx.V3(0, 0, 3), dir.Norm()); ok {
				ld := gfx.V3(10, 10, 10).Sub(hit).Norm()
				li := math.Max(0.4, ld.Dot(distanceFieldNormal(hit)))

				frame.Buffer[i+j*width] = gfx.V3(1, 1, 1).Mul(li)
			} else {
				frame.Buffer[i+j*width] = gfx.V3(0.1, 0.1, 0.1)
			}
		}
	}

	return frame
}

func signedDistance(p gfx.Vec3) float64 {
	d := math.Sin(8*p.X) * math.Sin(8*p.Y) * math.Sin(8*p.Z) * noiseAmplitude

	return p.SqLen() - (sphereRadius + d)
}

func sphereTrace(orig, dir gfx.Vec3) (gfx.Vec3, bool) {
	pos := orig

	for i := 0; i < 64; i++ {
		d := signedDistance(pos)

		if pos.Z <= 0 {
			return pos, false
		}

		if d < 0 {
			return pos, true
		}

		pos = pos.Add(dir.Mul(math.Max(d*0.1, 0.01)))
	}

	return pos, false
}

func distanceFieldNormal(pos gfx.Vec3) gfx.Vec3 {
	const eps = 0.1

	d := signedDistance(pos)

	nx := signedDistance(pos.Add(gfx.V3(eps, 0, 0))) - d
	ny := signedDistance(pos.Add(gfx.V3(0, eps, 0))) - d
	nz := signedDistance(pos.Add(gfx.V3(0, 0, eps))) - d

	return gfx.V3(nx, ny, nz).Norm()
}

type Frame struct {
	Buffer []gfx.Vec3
	bounds image.Rectangle
	gfx.Float64Scaler
}

func NewFrame(w, h int) *Frame {
	return &Frame{
		Buffer:        make([]gfx.Vec3, w*h),
		bounds:        gfx.IR(0, 0, w, h),
		Float64Scaler: gfx.NewLinearScaler().Domain(0, 1).Range(0, 255),
	}
}

func (f *Frame) At(x, y int) color.Color {
	v := f.Buffer[x+y*f.bounds.Max.X]

	return color.NRGBA{
		uint8(f.ScaleFloat64(v.X)),
		uint8(f.ScaleFloat64(v.Y)),
		uint8(f.ScaleFloat64(v.Z)),
		255,
	}
}

func (f *Frame) Bounds() image.Rectangle {
	return f.bounds
}

func (f *Frame) ColorModel() color.Model {
	return color.NRGBAModel
}

gfx-tinykaboom-step5-animation

@peterhellberg
Copy link
Author

peterhellberg commented Jan 28, 2019

Step 6: pseudorandom noise

package main

import (
	"image"
	"image/color"
	"math"

	"github.com/peterhellberg/gfx"
)

var (
	sphereRadius   = 1.5
	noiseAmplitude = 1.0
	simplex        = gfx.NewSimplexNoise(0)
)

func main() {
	frame := render(640, 480, math.Pi/3.0)

	gfx.SavePNG("/tmp/gfx-tinykaboom-step6.png", frame)
}

func render(width, height int, fov float64) *Frame {
	frame := NewFrame(width, height)

	fw, fh := float64(width), float64(height)

	for j := 0; j < height; j++ {
		for i := 0; i < width; i++ {
			fi, fj := float64(i), float64(j)

			dir := gfx.V3((fi+0.5)-fw/2, -(fj+0.5)+fh/2, -fh/(2*math.Tan(fov/2)))

			if hit, ok := sphereTrace(gfx.V3(0, 0, 3), dir.Norm()); ok {
				ld := gfx.V3(10, 10, 10).Sub(hit).Norm()
				li := math.Max(0.4, ld.Dot(distanceFieldNormal(hit)))

				frame.Buffer[i+j*width] = gfx.V3(1, 1, 1).Mul(li)
			} else {
				frame.Buffer[i+j*width] = gfx.V3(0.1, 0.1, 0.1)
			}
		}
	}

	return frame
}

func rotate(v gfx.Vec3) gfx.Vec3 {
	return gfx.V3(
		gfx.V3(0.00, 0.80, 0.60).Dot(v),
		gfx.V3(-0.80, 0.36, -0.48).Dot(v),
		gfx.V3(-0.60, -0.48, 0.64).Dot(v),
	)
}

func fractalBrownianMotion(x gfx.Vec3) float64 {
	p := rotate(x)
	f := 0.0

	f += 0.5000 * simplex.Noise3D(p.X, p.Y, p.Z)
	p = p.Mul(2.32)

	f += 0.2500 * simplex.Noise3D(p.X, p.Y, p.Z)
	p = p.Mul(3.03)

	f += 0.1250 * simplex.Noise3D(p.X, p.Y, p.Z)
	p = p.Mul(2.61)

	f += 0.0625 * simplex.Noise3D(p.X, p.Y, p.Z)

	return f / 0.9375
}

func signedDistance(p gfx.Vec3) float64 {
	d := -fractalBrownianMotion(p.Mul(3.4)) * noiseAmplitude

	return p.SqLen() - (sphereRadius + d)
}

func sphereTrace(orig, dir gfx.Vec3) (gfx.Vec3, bool) {
	pos := orig

	for i := 0; i < 256; i++ {
		d := signedDistance(pos)
		if d < 0 && pos.Z > 0 {
			return pos, true
		}

		pos = pos.Add(dir.Mul(math.Max(d*0.1, 0.01)))
	}

	return pos, false
}

func distanceFieldNormal(pos gfx.Vec3) gfx.Vec3 {
	const eps = 0.1

	d := signedDistance(pos)

	nx := signedDistance(pos.Add(gfx.V3(eps, 0, 0))) - d
	ny := signedDistance(pos.Add(gfx.V3(0, eps, 0))) - d
	nz := signedDistance(pos.Add(gfx.V3(0, 0, eps))) - d

	return gfx.V3(nx, ny, nz).Norm()
}

type Frame struct {
	Buffer []gfx.Vec3
	bounds image.Rectangle
	gfx.Float64Scaler
}

func NewFrame(w, h int) *Frame {
	return &Frame{
		Buffer:        make([]gfx.Vec3, w*h),
		bounds:        gfx.IR(0, 0, w, h),
		Float64Scaler: gfx.NewLinearScaler().Domain(0, 1).Range(0, 255),
	}
}

func (f *Frame) At(x, y int) color.Color {
	v := f.Buffer[x+y*f.bounds.Max.X]

	return color.NRGBA{
		uint8(f.ScaleFloat64(v.X)),
		uint8(f.ScaleFloat64(v.Y)),
		uint8(f.ScaleFloat64(v.Z)),
		255,
	}
}

func (f *Frame) Bounds() image.Rectangle {
	return f.bounds
}

func (f *Frame) ColorModel() color.Model {
	return color.NRGBAModel
}

gfx-tinykaboom-step6

@peterhellberg
Copy link
Author

Step 7: fire colors

package main

import (
	"image"
	"image/color"
	"math"

	"github.com/peterhellberg/gfx"
)

var (
	sphereRadius   = 1.5
	noiseAmplitude = 1.5
	simplex        = gfx.NewSimplexNoise(42)
)

func main() {
	frame := render(1920, 1080, math.Pi/3.0)

	gfx.SavePNG("/tmp/gfx-tinykaboom-step7.png", frame)
}

func render(width, height int, fov float64) *Frame {
	frame := NewFrame(width, height)

	fw, fh := float64(width), float64(height)

	for j := 0; j < height; j++ {
		for i := 0; i < width; i++ {
			fi, fj := float64(i), float64(j)

			dir := gfx.V3((fi+0.5)-fw/2, -(fj+0.5)+fh/2, -fh/(2*math.Tan(fov/2)))

			if hit, ok := sphereTrace(gfx.V3(0, 0, 3), dir.Norm()); ok {
				noiseLevel := (sphereRadius - hit.SqLen()) / noiseAmplitude

				ld := gfx.V3(10, 10, 10).Sub(hit).Norm()
				li := math.Max(0.4, ld.Dot(distanceFieldNormal(hit)))

				frame.Buffer[i+j*width] = paletteFire((-.2 + noiseLevel) * 2).Mul(li)
			} else {
				frame.Buffer[i+j*width] = gfx.V3(0.1, 0.1, 0.1)
			}
		}
	}

	return frame
}

func paletteFire(d float64) gfx.Vec3 {
	var (
		yellow   = gfx.V3(1.7, 1.3, 1.0) // note that the color is "hot", i.e. has components >1
		orange   = gfx.V3(1.0, 0.6, 0.0)
		red      = gfx.V3(1.0, 0.0, 0.0)
		darkgray = gfx.V3(0.2, 0.2, 0.2)
		gray     = gfx.V3(0.4, 0.4, 0.4)
	)

	x := math.Max(0, math.Min(1, d))

	switch {
	case x < 0.25:
		return gray.Lerp(darkgray, x*4)
	case x < 0.5:
		return darkgray.Lerp(red, x*4-1)
	case x < 0.75:
		return red.Lerp(orange, x*4-2)
	default:
		return orange.Lerp(yellow, x*4-3)
	}
}

func rotate(v gfx.Vec3) gfx.Vec3 {
	return gfx.V3(
		gfx.V3(0.00, 0.80, 0.60).Dot(v),
		gfx.V3(-0.80, 0.36, -0.48).Dot(v),
		gfx.V3(-0.60, -0.48, 0.64).Dot(v),
	)
}

func fractalBrownianMotion(x gfx.Vec3, f float64) float64 {
	p := rotate(x)

	f += 0.5000 * simplex.Noise3D(p.X, p.Y, p.Z)
	p = p.Mul(2.32)

	f += 0.2500 * simplex.Noise3D(p.X, p.Y, p.Z)
	p = p.Mul(3.03)

	f += 0.1250 * simplex.Noise3D(p.X, p.Y, p.Z)
	p = p.Mul(2.61)

	f += 0.0625 * simplex.Noise3D(p.X, p.Y, p.Z)

	return f
}

func signedDistance(p gfx.Vec3) float64 {
	d := -fractalBrownianMotion(p.Mul(1.4), 0.5) * noiseAmplitude

	return p.SqLen() - (sphereRadius + d)
}

func sphereTrace(orig, dir gfx.Vec3) (gfx.Vec3, bool) {
	pos := orig

	for i := 0; i < 256; i++ {
		d := signedDistance(pos)
		if d < 0 && pos.Z > 0 {
			return pos, true
		}

		pos = pos.Add(dir.Mul(math.Max(d*0.1, 0.01)))
	}

	return pos, false
}

func distanceFieldNormal(pos gfx.Vec3) gfx.Vec3 {
	const eps = 0.1

	d := signedDistance(pos)

	nx := signedDistance(pos.Add(gfx.V3(eps, 0, 0))) - d
	ny := signedDistance(pos.Add(gfx.V3(0, eps, 0))) - d
	nz := signedDistance(pos.Add(gfx.V3(0, 0, eps))) - d

	return gfx.V3(nx, ny, nz).Norm()
}

type Frame struct {
	Buffer []gfx.Vec3
	bounds image.Rectangle
	gfx.Float64Scaler
}

func NewFrame(w, h int) *Frame {
	return &Frame{
		Buffer:        make([]gfx.Vec3, w*h),
		bounds:        gfx.IR(0, 0, w, h),
		Float64Scaler: gfx.NewLinearScaler().Domain(0, 1).Range(0, 255),
	}
}

func (f *Frame) At(x, y int) color.Color {
	v := f.Buffer[x+y*f.bounds.Max.X]

	return color.NRGBA{
		uint8(f.ScaleFloat64(gfx.Clamp(v.X, 0, 1))),
		uint8(f.ScaleFloat64(gfx.Clamp(v.Y, 0, 1))),
		uint8(f.ScaleFloat64(gfx.Clamp(v.Z, 0, 1))),
		255,
	}
}

func (f *Frame) Bounds() image.Rectangle {
	return f.bounds
}

func (f *Frame) ColorModel() color.Model {
	return color.NRGBAModel
}

gfx-tinykaboom-step7

@peterhellberg
Copy link
Author

3840x2160

gfx-tinykaboom-3840x2160

@peterhellberg
Copy link
Author

peterhellberg commented Jan 28, 2019

gfx-tinykaboom-trace

@peterhellberg
Copy link
Author

gfx-tinykaboom-trace-sphere-radius

@peterhellberg
Copy link
Author

Optimized

package main

import (
	"image"
	"image/color"
	"math"

	"github.com/peterhellberg/gfx"
)

var (
	sphereRadius   = 1.5
	noiseAmplitude = 1.5
	simplex        = gfx.NewSimplexNoise(42)

	bg   = gfx.V3(0.1, 0.1, 0.1)
	orig = gfx.V3(0, 0, 3)
	lv   = gfx.V3(10, 10, 10)
)

func main() {
	frame := render(1920, 1080, math.Pi/3.0)

	gfx.SavePNG("/tmp/gfx-tinykaboom-step7-optimized.png", frame)
}

func render(width, height int, fov float64) *Frame {
	var (
		frame    = NewFrame(width, height)
		fw, fh   = float64(width), float64(height)
		fw2, fh2 = fw / 2, fh / 2
		fhtf2    = -fh / (2 * math.Tan(fov/2))
	)

	for j := 0; j < height; j++ {
		for i := 0; i < width; i++ {
			fi, fj := float64(i), float64(j)

			dir := gfx.V3((fi+0.5)-fw2, -(fj+0.5)+fh2, fhtf2).Norm()

			if hit, ok := sphereTrace(orig, dir); ok {
				noiseLevel := (sphereRadius - hit.SqLen()) / noiseAmplitude

				li := math.Max(0.4, lv.Sub(hit).Norm().Dot(distanceFieldNormal(hit)))

				frame.Buffer[i+j*width] = paletteFire((-0.2 + noiseLevel) * 2).Mul(li)
			} else {
				frame.Buffer[i+j*width] = bg
			}
		}
	}

	return frame
}

var (
	yellow   = gfx.V3(1.7, 1.3, 1.0) // note that the color is "hot", i.e. has components >1
	orange   = gfx.V3(1.0, 0.6, 0.0)
	red      = gfx.V3(1.0, 0.0, 0.0)
	darkgray = gfx.V3(0.2, 0.2, 0.2)
	gray     = gfx.V3(0.4, 0.4, 0.4)
)

func paletteFire(d float64) gfx.Vec3 {
	x := math.Max(0, math.Min(1, d))

	switch {
	case x < 0.25:
		return gray.Lerp(darkgray, x*4)
	case x < 0.5:
		return darkgray.Lerp(red, x*4-1)
	case x < 0.75:
		return red.Lerp(orange, x*4-2)
	default:
		return orange.Lerp(yellow, x*4-3)
	}
}

func rotate(v gfx.Vec3) gfx.Vec3 {
	return gfx.V3(
		gfx.V3(0.00, 0.80, 0.60).Dot(v),
		gfx.V3(-0.80, 0.36, -0.48).Dot(v),
		gfx.V3(-0.60, -0.48, 0.64).Dot(v),
	)
}

func fractalBrownianMotion(x gfx.Vec3, f float64) float64 {
	p := rotate(x)

	f += 0.5000 * simplex.Noise3D(p.X, p.Y, p.Z)

	p = p.Mul(2.32)

	f += 0.2500 * simplex.Noise3D(p.X, p.Y, p.Z)
	p = p.Mul(3.03)

	f += 0.1250 * simplex.Noise3D(p.X, p.Y, p.Z)
	p = p.Mul(2.61)

	f += 0.0625 * simplex.Noise3D(p.X, p.Y, p.Z)

	return f
}

func signedDistance(p gfx.Vec3) float64 {
	d := -fractalBrownianMotion(p.Mul(1.4), 0.5) * noiseAmplitude

	return p.SqLen() - (sphereRadius + d)
}

func sphereTrace(orig, dir gfx.Vec3) (gfx.Vec3, bool) {
	if orig.SqLen()-math.Pow(orig.Dot(dir), 2) > math.Pow(sphereRadius, 2) {
		return orig, false
	}

	pos := orig

	for i := 0; i < 64; i++ {
		d := signedDistance(pos)
		if d < 0 && pos.Z > 0 {
			return pos, true
		}

		pos = pos.Add(dir.Mul(math.Max(d*0.1, 0.01)))
	}

	return pos, false
}

const eps = 0.1

var (
	epsX = gfx.V3(eps, 0, 0)
	epsY = gfx.V3(0, eps, 0)
	epsZ = gfx.V3(0, 0, eps)
)

func distanceFieldNormal(pos gfx.Vec3) gfx.Vec3 {
	d := signedDistance(pos)

	return gfx.V3(
		signedDistance(pos.Add(epsX))-d,
		signedDistance(pos.Add(epsY))-d,
		signedDistance(pos.Add(epsZ))-d,
	).Norm()
}

type Frame struct {
	Buffer []gfx.Vec3
	bounds image.Rectangle
	gfx.Float64Scaler
}

func NewFrame(w, h int) *Frame {
	return &Frame{
		Buffer:        make([]gfx.Vec3, w*h),
		bounds:        gfx.IR(0, 0, w, h),
		Float64Scaler: gfx.NewLinearScaler().Domain(0, 1).Range(0, 255),
	}
}

func (f *Frame) At(x, y int) color.Color {
	v := f.Buffer[x+y*f.bounds.Max.X]

	return color.NRGBA{
		uint8(f.ScaleFloat64(gfx.Clamp(v.X, 0, 1))),
		uint8(f.ScaleFloat64(gfx.Clamp(v.Y, 0, 1))),
		uint8(f.ScaleFloat64(gfx.Clamp(v.Z, 0, 1))),
		255,
	}
}

func (f *Frame) Bounds() image.Rectangle {
	return f.bounds
}

func (f *Frame) ColorModel() color.Model {
	return color.NRGBAModel
}

@peterhellberg
Copy link
Author

Ebiten version: https://jsgo.io/9c895916adc7fa50128f9695bd697dab2ea88537

package main

import (
	"image"
	"image/color"
	"math"

	"github.com/hajimehoshi/ebiten"
	"github.com/hajimehoshi/ebiten/ebitenutil"
	"github.com/peterhellberg/gfx"
)

var (
	simplex = gfx.NewSimplexNoise(42)

	bg   = gfx.V3(0.1, 0.1, 0.1)
	orig = gfx.V3(0, 0, 3)
	lv   = gfx.V3(10, 10, 10)
)

var (
	yellow   = gfx.V3(1.7, 1.3, 1.0) // note that the color is "hot", i.e. has components >1
	orange   = gfx.V3(1.0, 0.6, 0.0)
	red      = gfx.V3(1.0, 0.0, 0.0)
	darkgray = gfx.V3(0.2, 0.2, 0.2)
	gray     = gfx.V3(0.4, 0.4, 0.4)
)

func main() {

	s := &State{
		width:  192,
		height: 108,
		fov:    math.Pi / 3.0,
		sr:     1.5,
		na:     1.5,
	}

	s.render()

	ebiten.SetFullscreen(true)
	ebiten.Run(s.loop, s.width, s.height, 10, "Ebiten GFX Tinyboom")
}

type State struct {
	width  int
	height int
	fov    float64
	sr     float64
	na     float64
	dirty  bool
	frame  *Frame
}

func (s *State) render() {
	s.frame = render(s.width, s.height, s.fov, s.sr, s.na)
	s.dirty = false
}

func (s *State) loop(screen *ebiten.Image) error {
	if ebiten.IsKeyPressed(ebiten.KeyEscape) || ebiten.IsKeyPressed(ebiten.KeyQ) {
		return gfx.ErrDone
	}

	switch {
	case ebiten.IsKeyPressed(ebiten.KeyUp):
		s.na -= 0.02
		s.dirty = true
	case ebiten.IsKeyPressed(ebiten.KeyDown):
		s.na += 0.02
		s.dirty = true
	case ebiten.IsKeyPressed(ebiten.KeyRight):
		s.sr += 0.02
		s.dirty = true
	case ebiten.IsKeyPressed(ebiten.KeyLeft):
		s.sr -= 0.02
		s.dirty = true
	}

	if ebiten.IsDrawingSkipped() {
		return nil
	}

	if s.dirty {
		gfx.Dump(s)
		s.render()
	}

	screen.ReplacePixels(s.frame.Pix)

	return ebitenutil.DebugPrint(screen, gfx.Sprintf("r:%.02f a:%.02f", s.sr, s.na))
}

func render(width, height int, fov, sr, na float64) *Frame {
	var (
		frame    = NewFrame(width, height)
		fw, fh   = float64(width), float64(height)
		fw2, fh2 = fw / 2, fh / 2
		fhtf2    = -fh / (2 * math.Tan(fov/2))
	)

	for j := 0; j < height; j++ {
		for i := 0; i < width; i++ {
			fi, fj := float64(i), float64(j)

			dir := gfx.V3((fi+0.5)-fw2, -(fj+0.5)+fh2, fhtf2).Norm()

			if hit, ok := sphereTrace(orig, dir, sr, na); ok {
				noiseLevel := (sr - hit.SqLen()) / na

				li := math.Max(0.4, lv.Sub(hit).Norm().Dot(distanceFieldNormal(hit, sr, na)))

				c := paletteFire((-0.2 + noiseLevel) * 2).Mul(li)

				frame.Buffer[i+j*width] = gfx.V3(
					gfx.Clamp(c.X, 0, 1),
					gfx.Clamp(c.Y, 0, 1),
					gfx.Clamp(c.Z, 0, 1),
				)
			} else {
				frame.Buffer[i+j*width] = bg
			}
		}
	}

	f := gfx.NewImage(width, height)

	gfx.DrawSrc(f, f.Bounds(), frame, gfx.ZP)

	frame.Pix = f.Pix

	return frame
}

func paletteFire(d float64) gfx.Vec3 {
	x := math.Max(0, math.Min(1, d))

	switch {
	case x < 0.25:
		return gray.Lerp(darkgray, x*4)
	case x < 0.5:
		return darkgray.Lerp(red, x*4-1)
	case x < 0.75:
		return red.Lerp(orange, x*4-2)
	default:
		return orange.Lerp(yellow, x*4-3)
	}
}

func rotate(v gfx.Vec3) gfx.Vec3 {
	return gfx.V3(
		gfx.V3(0.00, 0.80, 0.60).Dot(v),
		gfx.V3(-0.80, 0.36, -0.48).Dot(v),
		gfx.V3(-0.60, -0.48, 0.64).Dot(v),
	)
}

func fractalBrownianMotion(x gfx.Vec3, f float64) float64 {
	p := rotate(x)

	f += 0.5000 * simplex.Noise3D(p.X, p.Y, p.Z)

	p = p.Mul(2.32)

	f += 0.2500 * simplex.Noise3D(p.X, p.Y, p.Z)
	p = p.Mul(3.03)

	f += 0.1250 * simplex.Noise3D(p.X, p.Y, p.Z)
	p = p.Mul(2.61)

	f += 0.0625 * simplex.Noise3D(p.X, p.Y, p.Z)

	return f
}

func signedDistance(p gfx.Vec3, sr, na float64) float64 {
	d := -fractalBrownianMotion(p.Mul(1.4), 0.5) * na

	return p.SqLen() - (sr + d)
}

func sphereTrace(orig, dir gfx.Vec3, sr, na float64) (gfx.Vec3, bool) {
	if orig.SqLen()-math.Pow(orig.Dot(dir), 2) > math.Pow(sr, 2) {
		return orig, false
	}

	pos := orig

	for i := 0; i < 64; i++ {
		d := signedDistance(pos, sr, na)
		if d < 0 && pos.Z > 0 {
			return pos, true
		}

		pos = pos.Add(dir.Mul(math.Max(d*0.1, 0.01)))
	}

	return pos, false
}

const eps = 0.1

var (
	epsX = gfx.V3(eps, 0, 0)
	epsY = gfx.V3(0, eps, 0)
	epsZ = gfx.V3(0, 0, eps)
)

func distanceFieldNormal(pos gfx.Vec3, sr, na float64) gfx.Vec3 {
	d := signedDistance(pos, sr, na)

	return gfx.V3(
		signedDistance(pos.Add(epsX), sr, na)-d,
		signedDistance(pos.Add(epsY), sr, na)-d,
		signedDistance(pos.Add(epsZ), sr, na)-d,
	).Norm()
}

type Frame struct {
	Buffer []gfx.Vec3
	Pix    []uint8
	bounds image.Rectangle
}

func NewFrame(w, h int) *Frame {
	return &Frame{
		Buffer: make([]gfx.Vec3, w*h),
		Pix:    make([]uint8, w*h*4),
		bounds: gfx.IR(0, 0, w, h),
	}
}

func (f *Frame) At(x, y int) color.Color {
	v := f.Buffer[x+y*f.bounds.Max.X]

	return color.NRGBA{uint8(v.X * 255), uint8(v.Y * 255), uint8(v.Z * 255), 255}
}

func (f *Frame) Bounds() image.Rectangle {
	return f.bounds
}

func (f *Frame) ColorModel() color.Model {
	return color.NRGBAModel
}

@peterhellberg
Copy link
Author

r 1.50 a 1.50 r 3.12 a 3.96 r 1.46 a -9.80
screen shot 2019-01-28 at 23 30 19 screen shot 2019-01-28 at 23 31 14 screen shot 2019-01-28 at 23 32 06
r 6.44 a -0.02 r 2.10 a -1.20 r 6.66 a 6.66
screen shot 2019-01-28 at 23 33 11 screen shot 2019-01-28 at 23 34 53 screen shot 2019-01-28 at 23 47 59

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment