From e9e1ccc35e846e5da75ad50deede4ab161c7782b Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Mon, 20 Feb 2023 01:52:50 -0500 Subject: [PATCH] Added basic raycaster demo. I have no idea why I did this. --- artist/uniform.go | 15 +++- examples/raycaster/game.go | 71 +++++++++++++++ examples/raycaster/main.go | 41 +++++++++ examples/raycaster/ray.go | 90 +++++++++++++++++++ examples/raycaster/raycaster.go | 153 ++++++++++++++++++++++++++++++++ 5 files changed, 369 insertions(+), 1 deletion(-) create mode 100644 examples/raycaster/game.go create mode 100644 examples/raycaster/main.go create mode 100644 examples/raycaster/ray.go create mode 100644 examples/raycaster/raycaster.go diff --git a/artist/uniform.go b/artist/uniform.go index 4fa3013..74c1d0b 100644 --- a/artist/uniform.go +++ b/artist/uniform.go @@ -7,7 +7,7 @@ import "image/color" // Pattern, color.Color, color.Model, and image.Image interfaces. type Uniform color.RGBA -// NewUniform returns a new Uniform image of the given color. +// NewUniform returns a new Uniform pattern of the given color. func NewUniform (c color.Color) (uniform Uniform) { r, g, b, a := c.RGBA() uniform.R = uint8(r >> 8) @@ -17,6 +17,19 @@ func NewUniform (c color.Color) (uniform Uniform) { return } +func hex (color uint32) (c color.RGBA) { + c.A = uint8(color) + c.B = uint8(color >> 8) + c.G = uint8(color >> 16) + c.R = uint8(color >> 24) + return +} + +// Uhex creates a new Uniform pattern from an RGBA integer value. +func Uhex (color uint32) (uniform Uniform) { + return NewUniform(hex(color)) +} + // ColorModel satisfies the image.Image interface. func (uniform Uniform) ColorModel () (model color.Model) { return uniform diff --git a/examples/raycaster/game.go b/examples/raycaster/game.go new file mode 100644 index 0000000..ec6eb46 --- /dev/null +++ b/examples/raycaster/game.go @@ -0,0 +1,71 @@ +package main + +import "time" +import "git.tebibyte.media/sashakoshka/tomo" +import "git.tebibyte.media/sashakoshka/tomo/canvas" + +type Game struct { + *Raycaster + running bool + tickChan <- chan time.Time + stopChan chan bool + + controlState ControlState +} + +func NewGame (world World) (game *Game) { + game = &Game { + Raycaster: NewRaycaster(world), + stopChan: make(chan bool), + } + game.Raycaster.OnControlStateChange (func (state ControlState) { + game.controlState = state + }) + return +} + +func (game *Game) DrawTo (canvas canvas.Canvas) { + if canvas == nil { + game.stopChan <- true + } else if !game.running { + game.running = true + go game.run() + } + game.Raycaster.DrawTo(canvas) +} + +func (game *Game) tick () { + if game.controlState.WalkForward { + game.Walk(0.1) + } + if game.controlState.WalkBackward { + game.Walk(-0.1) + } + if game.controlState.StrafeLeft { + game.Strafe(-0.1) + } + if game.controlState.StrafeRight { + game.Strafe(0.1) + } + if game.controlState.LookLeft { + game.Rotate(-0.1) + } + if game.controlState.LookRight { + game.Rotate(0.1) + } + + tomo.Do(game.Draw) +} + +func (game *Game) run () { + ticker := time.NewTicker(time.Second / 30) + game.tickChan = ticker.C + for game.running { + select { + case <- game.tickChan: + game.tick() + case <- game.stopChan: + ticker.Stop() + } + } +} diff --git a/examples/raycaster/main.go b/examples/raycaster/main.go new file mode 100644 index 0000000..79829dd --- /dev/null +++ b/examples/raycaster/main.go @@ -0,0 +1,41 @@ +package main + +import "git.tebibyte.media/sashakoshka/tomo" +import "git.tebibyte.media/sashakoshka/tomo/layouts/basic" +import "git.tebibyte.media/sashakoshka/tomo/elements/basic" +import _ "git.tebibyte.media/sashakoshka/tomo/backends/x" + +func main () { + tomo.Run(run) +} + +func run () { + window, _ := tomo.NewWindow(640, 480) + window.SetTitle("Raycaster") + + container := basicElements.NewContainer(basicLayouts.Vertical { true, true }) + window.Adopt(container) + + game := NewGame (DefaultWorld { + Data: []int { + 1,1,1,1,1,1,1,1,1,1, + 1,0,0,0,0,0,0,0,0,1, + 1,0,0,0,0,0,0,0,0,1, + 1,0,0,1,1,0,1,0,0,1, + 1,0,0,1,0,0,1,0,0,1, + 1,0,0,1,0,0,1,0,0,1, + 1,0,0,1,0,1,1,0,0,1, + 1,0,0,0,0,0,0,0,0,1, + 1,0,0,0,0,0,0,0,0,1, + 1,1,1,1,1,1,1,1,1,1, + }, + Stride: 10, + }) + + container.Adopt(basicElements.NewLabel("Explore a 3D world!", false), false) + container.Adopt(game, true) + game.Focus() + + window.OnClose(tomo.Stop) + window.Show() +} diff --git a/examples/raycaster/ray.go b/examples/raycaster/ray.go new file mode 100644 index 0000000..d9ff36f --- /dev/null +++ b/examples/raycaster/ray.go @@ -0,0 +1,90 @@ +package main + +import "math" +import "image" + +type World interface { + At (image.Point) int +} + +type DefaultWorld struct { + Data []int + Stride int +} + +func (world DefaultWorld) At (position image.Point) int { + if position.X < 0 { return 0 } + if position.Y < 0 { return 0 } + if position.X >= world.Stride { return 0 } + index := position.X + position.Y * world.Stride + if index >= len(world.Data) { return 0 } + return world.Data[index] +} + +type Camera struct { + X, Y float64 + Angle float64 + Fov float64 +} + +func (camera *Camera) Point () (image.Point) { + return image.Pt(int(camera.X), int(camera.Y)) +} + +func (camera *Camera) Rotate (by float64) { + camera.Angle += by + if camera.Angle < 0 { camera.Angle += math.Pi * 2 } + if camera.Angle > math.Pi * 2 { camera.Angle = 0 } +} + +func (camera *Camera) Walk (by float64) { + dx, dy := camera.Delta() + camera.X += dx * by + camera.Y += dy * by +} + +func (camera *Camera) Strafe (by float64) { + dx, dy := camera.OffsetDelta() + camera.X += dx * by + camera.Y += dy * by +} + +func (camera *Camera) Delta () (x float64, y float64) { + return math.Cos(camera.Angle), math.Sin(camera.Angle) +} + +func (camera *Camera) OffsetDelta () (x float64, y float64) { + offset := math.Pi / 2 + return math.Cos(camera.Angle + offset), math.Sin(camera.Angle + offset) +} + +type Ray struct { + X, Y float64 + Angle float64 + Precision int +} + +func (ray *Ray) Cast (world World, max int) (distance float64) { + precision := 64 + + dX := math.Cos(ray.Angle) / float64(precision) + dY := math.Sin(ray.Angle) / float64(precision) + origX, origY := ray.X, ray.Y + + wall := 0 + depth := 0 + for wall == 0 && depth < max * precision { + ray.X += dX + ray.Y += dY + wall = world.At(ray.Point()) + depth ++ + } + + distanceX := origX - ray.X + distanceY := origY - ray.Y + return math.Sqrt(distanceX * distanceX + distanceY * distanceY) +} + +func (ray *Ray) Point () (image.Point) { + return image.Pt(int(ray.X), int(ray.Y)) +} diff --git a/examples/raycaster/raycaster.go b/examples/raycaster/raycaster.go new file mode 100644 index 0000000..789f767 --- /dev/null +++ b/examples/raycaster/raycaster.go @@ -0,0 +1,153 @@ +package main + +import "math" +import "image/color" +import "git.tebibyte.media/sashakoshka/tomo/input" +import "git.tebibyte.media/sashakoshka/tomo/config" +import "git.tebibyte.media/sashakoshka/tomo/artist" +import "git.tebibyte.media/sashakoshka/tomo/elements/core" + +type ControlState struct { + WalkForward bool + WalkBackward bool + StrafeLeft bool + StrafeRight bool + LookLeft bool + LookRight bool +} + +type Raycaster struct { + *core.Core + *core.FocusableCore + core core.CoreControl + focusableControl core.FocusableCoreControl + config config.Wrapped + + Camera + controlState ControlState + world World + onControlStateChange func (ControlState) +} + +func NewRaycaster (world World) (element *Raycaster) { + element = &Raycaster { + Camera: Camera { + X: 2, + Y: 2, + Angle: 1, + Fov: 1, + }, + world: world, + } + element.Core, element.core = core.NewCore(element.drawAll) + element.FocusableCore, + element.focusableControl = core.NewFocusableCore(element.Draw) + element.core.SetMinimumSize(64, 64) + return +} + +func (element *Raycaster) OnControlStateChange (callback func (ControlState)) { + element.onControlStateChange = callback +} + +func (element *Raycaster) Draw () { + if element.core.HasImage() { + element.drawAll() + element.core.DamageAll() + } +} + +func (element *Raycaster) HandleMouseDown (x, y int, button input.Button) { + if !element.Focused() { element.Focus() } +} + +func (element *Raycaster) HandleMouseUp (x, y int, button input.Button) { } +func (element *Raycaster) HandleMouseMove (x, y int) { } +func (element *Raycaster) HandleMouseScroll (x, y int, deltaX, deltaY float64) { } + +func (element *Raycaster) HandleKeyDown (key input.Key, modifiers input.Modifiers) { + switch key { + case input.KeyLeft: element.controlState.LookLeft = true + case input.KeyRight: element.controlState.LookRight = true + case 'a', 'A': element.controlState.StrafeLeft = true + case 'd', 'D': element.controlState.StrafeRight = true + case 'w', 'W': element.controlState.WalkForward = true + case 's', 'S': element.controlState.WalkBackward = true + default: return + } + + if element.onControlStateChange != nil { + element.onControlStateChange(element.controlState) + } +} + +func (element *Raycaster) HandleKeyUp(key input.Key, modifiers input.Modifiers) { + switch key { + case input.KeyLeft: element.controlState.LookLeft = false + case input.KeyRight: element.controlState.LookRight = false + case 'a', 'A': element.controlState.StrafeLeft = false + case 'd', 'D': element.controlState.StrafeRight = false + case 'w', 'W': element.controlState.WalkForward = false + case 's', 'S': element.controlState.WalkBackward = false + default: return + } + + if element.onControlStateChange != nil { + element.onControlStateChange(element.controlState) + } +} + +func (element *Raycaster) drawAll () { + bounds := element.Bounds() + // artist.FillRectangle(element.core, artist.Uhex(0x000000FF), bounds) + width := bounds.Dx() + height := bounds.Dy() + + ray := Ray { + Angle: element.Camera.Angle - element.Camera.Fov / 2, + Precision: 64, + } + + for x := 0; x < width; x ++ { + ray.X = element.Camera.X + ray.Y = element.Camera.Y + + distance := ray.Cast(element.world, 8) + distanceFac := float64(distance) / 8 + distance *= math.Cos(ray.Angle - element.Camera.Angle) + + wallHeight := height + if distance > 0 { + wallHeight = int((float64(height) / 2.0) / float64(distance)) + } + + ceilingColor := color.RGBA { 0x00, 0x00, 0x00, 0xFF } + wallColor := color.RGBA { 0xCC, 0x33, 0x22, 0xFF } + floorColor := color.RGBA { 0x11, 0x50, 0x22, 0xFF } + + // fmt.Println(float64(distance) / 32) + + wallColor = artist.LerpRGBA(wallColor, ceilingColor, distanceFac) + + // draw + data, stride := element.core.Buffer() + wallStart := height / 2 - wallHeight + bounds.Min.Y + wallEnd := height / 2 + wallHeight + bounds.Min.Y + if wallStart < 0 { wallStart = 0 } + if wallEnd > bounds.Max.Y { wallEnd = bounds.Max.Y } + for y := bounds.Min.Y; y < wallStart; y ++ { + data[y * stride + x + bounds.Min.X] = ceilingColor + } + for y := wallStart; y < wallEnd; y ++ { + data[y * stride + x + bounds.Min.X] = wallColor + } + for y := wallEnd; y < bounds.Max.Y; y ++ { + floorFac := float64(y - (height / 2)) / float64(height / 2) + data[y * stride + x + bounds.Min.X] = + artist.LerpRGBA(ceilingColor, floorColor, floorFac) + } + + // increment angle + ray.Angle += element.Camera.Fov / float64(width) + } +}