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 | |
} |
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
}
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
}
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
}
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
}
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
}
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
}
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
}
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
}
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
}
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
Step 0: read the tinyraytracer article