Skip to content

Instantly share code, notes, and snippets.

@peterhellberg
Created January 28, 2019 16:16
Show Gist options
  • 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

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