1
0
Fork 0

Initial commit

This commit is contained in:
Henrik Hautakoski 2025-06-07 20:26:00 +02:00
commit f26c478727
18 changed files with 621 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/game

5
go.mod Normal file
View file

@ -0,0 +1,5 @@
module github.com/pnx/go-raytracer
go 1.24.3
require github.com/veandco/go-sdl2 v0.4.40

2
go.sum Normal file
View file

@ -0,0 +1,2 @@
github.com/veandco/go-sdl2 v0.4.40 h1:fZv6wC3zz1Xt167P09gazawnpa0KY5LM7JAvKpX9d/U=
github.com/veandco/go-sdl2 v0.4.40/go.mod h1:OROqMhHD43nT4/i9crJukyVecjPNYYuCofep6SNiAjY=

65
graphics/sdl.go Normal file
View file

@ -0,0 +1,65 @@
package graphics
import (
"github.com/pnx/go-raytracer/math"
"github.com/veandco/go-sdl2/sdl"
)
type Context struct {
w, h int32
window *sdl.Window
renderer *sdl.Renderer
}
func (c Context) Width() int32 {
return c.w
}
func (c Context) Height() int32 {
return c.h
}
func (c Context) DrawLine(x1 int32, y1 int32, x2 int32, y2 int32, color math.Color) {
c.renderer.SetDrawColor(color.R, color.G, color.B, color.A)
c.renderer.DrawLine(x1, y1, x2, y2)
}
func (c Context) DrawRect(x, y, w, h int32, color math.Color) {
c.renderer.SetDrawColor(color.R, color.G, color.B, color.A)
c.renderer.FillRect(&sdl.Rect{
X: x,
Y: y,
W: w,
H: h,
})
}
func (c Context) Sync() {
c.renderer.Present()
}
func (c Context) Exit() {
c.window.Destroy()
c.renderer.Destroy()
sdl.Quit()
}
func Init(title string, w, h int32) (*Context, error) {
if err := sdl.Init(sdl.INIT_VIDEO); err != nil {
return nil, err
}
window, err := sdl.CreateWindow(title, sdl.WINDOWPOS_UNDEFINED, sdl.WINDOWPOS_UNDEFINED, w, h, sdl.WINDOW_SHOWN)
if err != nil {
return nil, err
}
renderer, err := sdl.CreateRenderer(window, -1, sdl.RENDERER_SOFTWARE)
if err != nil {
return nil, err
}
return &Context{
w: w,
h: h,
window: window,
renderer: renderer,
}, nil
}

97
main.go Normal file
View file

@ -0,0 +1,97 @@
package main
import (
"log"
stdmath "math"
"time"
"github.com/pnx/go-raytracer/graphics"
"github.com/pnx/go-raytracer/math"
"github.com/pnx/go-raytracer/render"
"github.com/pnx/go-raytracer/world"
"github.com/veandco/go-sdl2/sdl"
)
var (
gfxContext *graphics.Context
player Player
level *world.Level
startOfFrame time.Time
)
func loadLevel(newLevel *world.Level) {
level = newLevel
// Set player start position
start := level.PlayerStart()
player.Position = math.Position{
X: (float64(start.X) + 0.5) * tileSize,
Y: (float64(start.Y) + 0.5) * tileSize,
}
}
func update() bool {
for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() {
switch ev := event.(type) {
case *sdl.QuitEvent:
return true
case *sdl.KeyboardEvent:
if ev.Type == sdl.KEYDOWN {
switch ev.Keysym.Sym {
case sdl.K_ESCAPE:
return true
}
}
}
}
keys := sdl.GetKeyboardState()
if keys[sdl.SCANCODE_UP] != 0 {
player.MoveForward(level)
}
if keys[sdl.SCANCODE_DOWN] != 0 {
player.MoveBackward(level)
}
if keys[sdl.SCANCODE_LEFT] != 0 {
player.RotateLeft()
}
if keys[sdl.SCANCODE_RIGHT] != 0 {
player.RotateRight()
}
return false
}
func main() {
var err error
gfxContext, err = graphics.Init("Go Raycaster", 1024, 768)
if err != nil {
log.Fatal(err)
}
player = Player{
MoveSpd: 2,
RotSpd: 0.05,
}
loadLevel(&world.Level1)
for {
startOfFrame := time.Now()
if update() {
break
}
render.DrawScene(gfxContext, player.Transform, level, colorMap)
render.DrawMiniMap(gfxContext, player.Transform, level)
gfxContext.Sync()
elapsed := time.Since(startOfFrame)
sdl.Delay(uint32(stdmath.Max(16-float64(elapsed.Milliseconds()), 1)))
}
gfxContext.Exit()
}

6
makefile Normal file
View file

@ -0,0 +1,6 @@
GO=go
.PHONY: game
game:
$(GO) build -o $@ .

23
math/color.go Normal file
View file

@ -0,0 +1,23 @@
package math
type Color struct {
R, G, B, A uint8
}
func (c Color) Shade(value uint8) Color {
return Color{
R: c.R / value,
G: c.G / value,
B: c.B / value,
A: c.A,
}
}
func (c Color) Sub(value uint8) Color {
return Color{
R: c.R - value,
G: c.G - value,
B: c.B - value,
A: c.A,
}
}

7
math/degree.go Normal file
View file

@ -0,0 +1,7 @@
package math
import "math"
func DecToRad(deg float64) float64 {
return deg * (math.Pi / 180)
}

26
math/direction.go Normal file
View file

@ -0,0 +1,26 @@
package math
import (
stdmath "math"
)
type Direction float64
func (d *Direction) Set(v float64) {
*d = Direction(v)
}
func (d Direction) Get() float64 {
return float64(d)
}
func (d Direction) ForwardVector() Vec2f {
return Vec2f{
X: stdmath.Cos(float64(d)),
Y: stdmath.Sin(float64(d)),
}
}
func (d *Direction) Rotate(delta float64) {
*d += Direction(delta)
}

11
math/position.go Normal file
View file

@ -0,0 +1,11 @@
package math
type Position Vec2f
func (p *Position) Set(v Vec2f) {
*p = Position(v)
}
func (p Position) Get() Vec2f {
return Vec2f(p)
}

6
math/transform.go Normal file
View file

@ -0,0 +1,6 @@
package math
type Transform struct {
Position
Direction
}

82
math/vec2.go Normal file
View file

@ -0,0 +1,82 @@
package math
import stdmath "math"
type UnsignedInteger interface {
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
}
type SignedInteger interface {
~int | ~int8 | ~int16 | ~int32 | ~int64
}
type Integer interface {
UnsignedInteger | SignedInteger
}
type Float interface {
~float32 | ~float64
}
type Number interface {
Integer | Float
}
type Vec2[T Number] struct {
X, Y T
}
type (
Vec2i = Vec2[int]
Vec2i32 = Vec2[int32]
Vec2f = Vec2[float64]
Vec2u8 = Vec2[uint8]
)
func (v Vec2[T]) Add(x, y T) Vec2[T] {
return Vec2[T]{
X: v.X + x,
Y: v.Y + y,
}
}
func (v Vec2[T]) AddS(s T) Vec2[T] {
return v.Add(s, s)
}
func (v Vec2[T]) AddVec(other Vec2[T]) Vec2[T] {
return Vec2[T]{
X: v.X + other.X,
Y: v.Y + other.Y,
}
}
func (v Vec2[T]) Mul(x, y T) Vec2[T] {
return Vec2[T]{
X: v.X * x,
Y: v.Y * y,
}
}
func (v Vec2[T]) MulVec(other Vec2[T]) Vec2[T] {
return v.Mul(other.X, other.Y)
}
func (v Vec2[T]) Scale(f T) Vec2[T] {
return v.Mul(f, f)
}
func (v Vec2[T]) Length() float64 {
return stdmath.Sqrt(float64(v.X*v.X) + float64(v.Y*v.Y))
}
func (v Vec2[T]) Normalize() Vec2[T] {
l := v.Length()
if l <= 0.0 {
return Vec2[T]{}
}
return Vec2[T]{
X: T(float64(v.X) / l),
Y: T(float64(v.Y) / l),
}
}

38
player.go Normal file
View file

@ -0,0 +1,38 @@
package main
import (
"github.com/pnx/go-raytracer/math"
"github.com/pnx/go-raytracer/world"
)
type Player struct {
math.Transform
MoveSpd float64
RotSpd float64
}
func (p *Player) Move(delta float64, level *world.Level) {
deltaVec := p.ForwardVector().Scale(delta)
newPos := p.Position.Get().AddVec(deltaVec)
// make sure we don't move past walls.
if !level.Wall(int(newPos.X)/world.TileSize, int(newPos.Y)/world.TileSize) {
p.Position.Set(newPos)
}
}
func (p *Player) MoveForward(level *world.Level) {
p.Move(p.MoveSpd, level)
}
func (p *Player) MoveBackward(level *world.Level) {
p.Move(-p.MoveSpd, level)
}
func (p *Player) RotateLeft() {
p.Rotate(-p.RotSpd)
}
func (p *Player) RotateRight() {
p.Rotate(p.RotSpd)
}

47
render/engine.go Normal file
View file

@ -0,0 +1,47 @@
package render
import (
"github.com/pnx/go-raytracer/graphics"
"github.com/pnx/go-raytracer/math"
"github.com/pnx/go-raytracer/world"
)
func DrawColumn(ctx *graphics.Context, x int32, wall_h int32, color math.Color) {
if wall_h < ctx.Height() {
y1 := (ctx.Height() - wall_h) / 2
y2 := (ctx.Height() + wall_h) / 2
// Top
// renderer.SetDrawColor(90, 90, 0, 255)
ctx.DrawLine(x, 0, int32(x), y1, math.Color{R: 90, G: 90, B: 0, A: 255})
// // Middle
// renderer.SetDrawColor(color.R, color.G, color.B, color.A)
ctx.DrawLine(x, y1, int32(x), y2, color)
// Bottom
if y2 < ctx.Height() {
// renderer.SetDrawColor(50, 50, 50, 255)
ctx.DrawLine(x, y2, int32(x), ctx.Height(), math.Color{R: 50, G: 50, B: 50, A: 255})
}
return
}
// ctx.SetDrawColor(color.R, color.G, color.B, color.A)
ctx.DrawLine(x, 0, int32(x), ctx.Height(), color)
}
func DrawScene(ctx *graphics.Context, camera math.Transform, level *world.Level, colorMap []math.Color) {
for x := range ctx.Width() {
result := CastRay(camera, level, int(x), int(ctx.Width()))
lineHeight := int(float64(ctx.Height()) / result.Distance)
color := colorMap[level.Cell(int(result.Cell.X), int(result.Cell.Y))-1]
if result.Side > 0 {
color = color.Shade(2)
}
DrawColumn(ctx, int32(x), int32(lineHeight), color)
}
}

36
render/minimap.go Normal file
View file

@ -0,0 +1,36 @@
package render
import (
"github.com/pnx/go-raytracer/graphics"
"github.com/pnx/go-raytracer/math"
"github.com/pnx/go-raytracer/world"
)
func DrawMiniMap(ctx *graphics.Context, camera math.Transform, level *world.Level) {
const tileSize = 32
const scale = float64(tileSize) / float64(world.TileSize)
// offset := math.Vec2i32{X: 25, Y: 25}
// size := math.Vec2i32{X: 200, Y: 100}
ctx.DrawRect(0, 0, int32(level.W*tileSize)+3, int32(level.H*tileSize)+3, math.Color{})
for y := range level.H {
for x := range level.W {
if level.Wall(x, y) {
ctx.DrawRect(int32(x*tileSize), int32(y*tileSize), tileSize, tileSize, math.Color{R: 255, G: 255, B: 255, A: 255})
}
}
}
// Camera Position
cPos := camera.Position.Get().Scale(scale)
ctx.DrawRect(int32(cPos.X-2), int32(cPos.Y-2), 4, 4, math.Color{R: 255, G: 0, B: 255, A: 255})
// Rays
for x := range ctx.Width() {
hit := CastRay(camera, level, int(x), int(ctx.Width()))
end := camera.Position.Get().AddVec(hit.Pos.Scale(world.TileSize)).Scale(scale)
ctx.DrawLine(int32(cPos.X), int32(cPos.Y), int32(end.X), int32(end.Y), math.Color{R: 150, G: 150, B: 0, A: 255})
}
}

85
render/raycaster.go Normal file
View file

@ -0,0 +1,85 @@
package render
import (
stdmath "math"
"github.com/pnx/go-raytracer/math"
"github.com/pnx/go-raytracer/world"
)
type RayHit struct {
Cell math.Vec2u8
Side int
Distance float64
Pos math.Vec2f
}
func CastRay(camera math.Transform, level *world.Level, x int, max_rays int) RayHit {
angle := camera.Direction.Get()
cameraX := 2*float64(x)/float64(max_rays) - 1
rayDirX := stdmath.Cos(angle) + cameraX*stdmath.Cos(angle+stdmath.Pi/2)
rayDirY := stdmath.Sin(angle) + cameraX*stdmath.Sin(angle+stdmath.Pi/2)
mapX := int(camera.X) / world.TileSize
mapY := int(camera.Y) / world.TileSize
deltaDistX := stdmath.Abs(1 / rayDirX)
deltaDistY := stdmath.Abs(1 / rayDirY)
var stepX, stepY int
var sideDistX, sideDistY float64
if rayDirX < 0 {
stepX = -1
sideDistX = (float64(camera.X)/world.TileSize - float64(mapX)) * deltaDistX
} else {
stepX = 1
sideDistX = (float64(mapX+1) - float64(camera.X)/world.TileSize) * deltaDistX
}
if rayDirY < 0 {
stepY = -1
sideDistY = (float64(camera.Y)/world.TileSize - float64(mapY)) * deltaDistY
} else {
stepY = 1
sideDistY = (float64(mapY+1) - float64(camera.Y)/world.TileSize) * deltaDistY
}
// DDA loop
var side int // 0 = x, 1 = y
for {
if sideDistX < sideDistY {
sideDistX += deltaDistX
mapX += stepX
side = 0
} else {
sideDistY += deltaDistY
mapY += stepY
side = 1
}
if mapX < 0 || mapX >= level.W || mapY < 0 || mapY >= level.H {
break // out of bounds
}
if level.Wall(mapX, mapY) {
break
}
}
// Calculate distance to wall
var perpWallDist float64
if side == 0 {
perpWallDist = (float64(mapX) - float64(camera.X)/world.TileSize + float64(1-stepX)/2) / rayDirX
} else {
perpWallDist = (float64(mapY) - float64(camera.Y)/world.TileSize + float64(1-stepY)/2) / rayDirY
}
return RayHit{
Cell: math.Vec2u8{X: uint8(mapX), Y: uint8(mapY)},
Side: side,
Distance: perpWallDist,
Pos: math.Vec2f{
X: rayDirX * perpWallDist,
Y: rayDirY * perpWallDist,
},
}
}

42
world/level.go Normal file
View file

@ -0,0 +1,42 @@
package world
import "github.com/pnx/go-raytracer/math"
const TileSize = 64
type Special byte
const (
Empty Special = 0
PlayerStart Special = 'S'
)
type Level struct {
W, H int
Grid []byte
Specials []Special
}
func (m Level) Wall(x, y int) bool {
return m.Grid[(y*m.W)+x] > 0
}
func (m Level) Cell(x, y int) byte {
return m.Grid[(y*m.W)+x]
}
func (m Level) PosToCell(x, y float64) (int, int) {
return int(x) / TileSize, int(y) / TileSize
}
func (m Level) PlayerStart() math.Vec2[int] {
for k, v := range m.Specials {
if v == PlayerStart {
return math.Vec2[int]{
X: k % m.W,
Y: k / m.W,
}
}
}
return math.Vec2[int]{X: -1, Y: -1}
}

42
world/level1.go Normal file
View file

@ -0,0 +1,42 @@
package world
var Level1 = Level{
W: 16,
H: 16,
Grid: []uint8{
2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 2, 2, 0, 1,
2, 2, 2, 0, 2, 2, 2, 2, 0, 4, 4, 0, 0, 0, 0, 1,
3, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1,
3, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1,
3, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
3, 0, 0, 0, 3, 0, 0, 0, 0, 0, 4, 0, 3, 0, 0, 1,
3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 1, 0, 2, 0, 0, 1,
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
},
Specials: []Special{
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 'S', 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
},
}