Added basic raycaster demo. I have no idea why I did this.
This commit is contained in:
parent
0c39c2dd57
commit
e9e1ccc35e
@ -7,7 +7,7 @@ import "image/color"
|
|||||||
// Pattern, color.Color, color.Model, and image.Image interfaces.
|
// Pattern, color.Color, color.Model, and image.Image interfaces.
|
||||||
type Uniform color.RGBA
|
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) {
|
func NewUniform (c color.Color) (uniform Uniform) {
|
||||||
r, g, b, a := c.RGBA()
|
r, g, b, a := c.RGBA()
|
||||||
uniform.R = uint8(r >> 8)
|
uniform.R = uint8(r >> 8)
|
||||||
@ -17,6 +17,19 @@ func NewUniform (c color.Color) (uniform Uniform) {
|
|||||||
return
|
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.
|
// ColorModel satisfies the image.Image interface.
|
||||||
func (uniform Uniform) ColorModel () (model color.Model) {
|
func (uniform Uniform) ColorModel () (model color.Model) {
|
||||||
return uniform
|
return uniform
|
||||||
|
71
examples/raycaster/game.go
Normal file
71
examples/raycaster/game.go
Normal file
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
41
examples/raycaster/main.go
Normal file
41
examples/raycaster/main.go
Normal file
@ -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()
|
||||||
|
}
|
90
examples/raycaster/ray.go
Normal file
90
examples/raycaster/ray.go
Normal file
@ -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))
|
||||||
|
}
|
153
examples/raycaster/raycaster.go
Normal file
153
examples/raycaster/raycaster.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user