Created
January 28, 2019 16:16
-
-
Save peterhellberg/972c15b774d4cb5de2baa64e5feccf41 to your computer and use it in GitHub Desktop.
tinykaboom with gfx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
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
}
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Optimized