354 lines
7.5 KiB
Go
354 lines
7.5 KiB
Go
package raycast
|
|
|
|
import (
|
|
"image/color"
|
|
"math"
|
|
"sort"
|
|
|
|
"github.com/atridad/LilGuy/internal/world"
|
|
"github.com/hajimehoshi/ebiten/v2"
|
|
)
|
|
|
|
// Line represents a line segment
|
|
type Line struct {
|
|
X1, Y1, X2, Y2 float64
|
|
}
|
|
|
|
// angle returns the angle of the line
|
|
func (l *Line) angle() float64 {
|
|
return math.Atan2(l.Y2-l.Y1, l.X2-l.X1)
|
|
}
|
|
|
|
// LightSource represents a light that casts rays
|
|
type LightSource struct {
|
|
X, Y float64
|
|
Radius float64
|
|
Color color.RGBA
|
|
Intensity float64
|
|
}
|
|
|
|
type System struct {
|
|
lights []*LightSource
|
|
shadowImage *ebiten.Image
|
|
triangleImage *ebiten.Image
|
|
screenWidth int
|
|
screenHeight int
|
|
enabled bool
|
|
shadowIntensity float64
|
|
}
|
|
|
|
func NewSystem(screenWidth, screenHeight int) *System {
|
|
shadowImage := ebiten.NewImage(screenWidth, screenHeight)
|
|
triangleImage := ebiten.NewImage(screenWidth, screenHeight)
|
|
triangleImage.Fill(color.White)
|
|
|
|
return &System{
|
|
lights: make([]*LightSource, 0),
|
|
shadowImage: shadowImage,
|
|
triangleImage: triangleImage,
|
|
screenWidth: screenWidth,
|
|
screenHeight: screenHeight,
|
|
enabled: true,
|
|
shadowIntensity: 0.7,
|
|
}
|
|
}
|
|
|
|
func (s *System) AddLight(light *LightSource) {
|
|
s.lights = append(s.lights, light)
|
|
}
|
|
|
|
func (s *System) ClearLights() {
|
|
s.lights = s.lights[:0]
|
|
}
|
|
|
|
func (s *System) SetEnabled(enabled bool) {
|
|
s.enabled = enabled
|
|
}
|
|
|
|
func (s *System) IsEnabled() bool {
|
|
return s.enabled
|
|
}
|
|
|
|
func (s *System) SetShadowIntensity(intensity float64) {
|
|
s.shadowIntensity = intensity
|
|
}
|
|
|
|
func worldToLines(w *world.World) []Line {
|
|
var lines []Line
|
|
|
|
for _, surface := range w.Surfaces {
|
|
if !surface.IsSolid() {
|
|
continue
|
|
}
|
|
|
|
x, y, width, height := surface.Bounds()
|
|
|
|
lines = append(lines,
|
|
Line{x, y, x + width, y},
|
|
Line{x + width, y, x + width, y + height},
|
|
Line{x + width, y + height, x, y + height},
|
|
Line{x, y + height, x, y},
|
|
)
|
|
}
|
|
|
|
return lines
|
|
}
|
|
|
|
// intersection calculates the intersection of two lines
|
|
func intersection(l1, l2 Line) (float64, float64, bool) {
|
|
denom := (l1.X1-l1.X2)*(l2.Y1-l2.Y2) - (l1.Y1-l1.Y2)*(l2.X1-l2.X2)
|
|
tNum := (l1.X1-l2.X1)*(l2.Y1-l2.Y2) - (l1.Y1-l2.Y1)*(l2.X1-l2.X2)
|
|
uNum := -((l1.X1-l1.X2)*(l1.Y1-l2.Y1) - (l1.Y1-l1.Y2)*(l1.X1-l2.X1))
|
|
|
|
if denom == 0 {
|
|
return 0, 0, false
|
|
}
|
|
|
|
t := tNum / denom
|
|
if t > 1 || t < 0 {
|
|
return 0, 0, false
|
|
}
|
|
|
|
u := uNum / denom
|
|
if u > 1 || u < 0 {
|
|
return 0, 0, false
|
|
}
|
|
|
|
x := l1.X1 + t*(l1.X2-l1.X1)
|
|
y := l1.Y1 + t*(l1.Y2-l1.Y1)
|
|
return x, y, true
|
|
}
|
|
|
|
// newRay creates a ray from a point at a given angle
|
|
func newRay(x, y, length, angle float64) Line {
|
|
return Line{
|
|
X1: x,
|
|
Y1: y,
|
|
X2: x + length*math.Cos(angle),
|
|
Y2: y + length*math.Sin(angle),
|
|
}
|
|
}
|
|
|
|
// getLinePoints extracts unique points from lines
|
|
func getLinePoints(lines []Line) [][2]float64 {
|
|
pointMap := make(map[[2]float64]bool)
|
|
|
|
for _, line := range lines {
|
|
pointMap[[2]float64{line.X1, line.Y1}] = true
|
|
pointMap[[2]float64{line.X2, line.Y2}] = true
|
|
}
|
|
|
|
points := make([][2]float64, 0, len(pointMap))
|
|
for p := range pointMap {
|
|
points = append(points, p)
|
|
}
|
|
|
|
return points
|
|
}
|
|
|
|
func castRays(cx, cy float64, lines []Line) []Line {
|
|
const rayLength = 2000.0
|
|
|
|
var rays []Line
|
|
points := getLinePoints(lines)
|
|
|
|
for _, p := range points {
|
|
l := Line{cx, cy, p[0], p[1]}
|
|
angle := l.angle()
|
|
|
|
for _, offset := range []float64{-0.001, 0.001} {
|
|
var intersections [][2]float64
|
|
ray := newRay(cx, cy, rayLength, angle+offset)
|
|
|
|
for _, wall := range lines {
|
|
if px, py, ok := intersection(ray, wall); ok {
|
|
intersections = append(intersections, [2]float64{px, py})
|
|
}
|
|
}
|
|
|
|
if len(intersections) > 0 {
|
|
minDist := math.Inf(1)
|
|
minIdx := -1
|
|
|
|
for i, point := range intersections {
|
|
dist := (cx-point[0])*(cx-point[0]) + (cy-point[1])*(cy-point[1])
|
|
if dist < minDist {
|
|
minDist = dist
|
|
minIdx = i
|
|
}
|
|
}
|
|
|
|
if minIdx >= 0 {
|
|
rays = append(rays, Line{cx, cy, intersections[minIdx][0], intersections[minIdx][1]})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
sort.Slice(rays, func(i, j int) bool {
|
|
return rays[i].angle() < rays[j].angle()
|
|
})
|
|
|
|
return rays
|
|
}
|
|
|
|
// rayVertices creates vertices for a triangle between rays
|
|
func rayVertices(x1, y1, x2, y2, x3, y3 float64) []ebiten.Vertex {
|
|
return []ebiten.Vertex{
|
|
{DstX: float32(x1), DstY: float32(y1), SrcX: 0, SrcY: 0, ColorR: 1, ColorG: 1, ColorB: 1, ColorA: 1},
|
|
{DstX: float32(x2), DstY: float32(y2), SrcX: 0, SrcY: 0, ColorR: 1, ColorG: 1, ColorB: 1, ColorA: 1},
|
|
{DstX: float32(x3), DstY: float32(y3), SrcX: 0, SrcY: 0, ColorR: 1, ColorG: 1, ColorB: 1, ColorA: 1},
|
|
}
|
|
}
|
|
|
|
func (s *System) Draw(screen *ebiten.Image, w *world.World) {
|
|
if !s.enabled || len(s.lights) == 0 {
|
|
return
|
|
}
|
|
|
|
lines := worldToLines(w)
|
|
|
|
sw := float64(s.screenWidth)
|
|
sh := float64(s.screenHeight)
|
|
lines = append(lines,
|
|
Line{0, 0, sw, 0},
|
|
Line{sw, 0, sw, sh},
|
|
Line{sw, sh, 0, sh},
|
|
Line{0, sh, 0, 0},
|
|
)
|
|
|
|
s.shadowImage.Fill(color.RGBA{0, 0, 0, 180})
|
|
|
|
for _, light := range s.lights {
|
|
rays := castRays(light.X, light.Y, lines)
|
|
|
|
opt := &ebiten.DrawTrianglesOptions{}
|
|
opt.Blend = ebiten.BlendSourceOut
|
|
|
|
for i, ray := range rays {
|
|
nextRay := rays[(i+1)%len(rays)]
|
|
|
|
v := rayVertices(light.X, light.Y, nextRay.X2, nextRay.Y2, ray.X2, ray.Y2)
|
|
s.shadowImage.DrawTriangles(v, []uint16{0, 1, 2}, s.triangleImage, opt)
|
|
}
|
|
}
|
|
|
|
shadowOpt := &ebiten.DrawImageOptions{}
|
|
shadowOpt.ColorScale.ScaleAlpha(float32(s.shadowIntensity))
|
|
screen.DrawImage(s.shadowImage, shadowOpt)
|
|
}
|
|
|
|
func (s *System) DrawDebug(screen *ebiten.Image, w *world.World) {
|
|
if !s.enabled || len(s.lights) == 0 {
|
|
return
|
|
}
|
|
|
|
lines := worldToLines(w)
|
|
|
|
sw := float64(s.screenWidth)
|
|
sh := float64(s.screenHeight)
|
|
lines = append(lines,
|
|
Line{0, 0, sw, 0},
|
|
Line{sw, 0, sw, sh},
|
|
Line{sw, sh, 0, sh},
|
|
Line{0, sh, 0, 0},
|
|
)
|
|
|
|
for _, light := range s.lights {
|
|
rays := castRays(light.X, light.Y, lines)
|
|
|
|
for _, ray := range rays {
|
|
drawLine(screen, ray, color.RGBA{255, 255, 0, 150})
|
|
}
|
|
|
|
drawCircle(screen, light.X, light.Y, 5, color.RGBA{255, 200, 100, 255})
|
|
}
|
|
}
|
|
|
|
// Helper function to draw a line
|
|
func drawLine(screen *ebiten.Image, l Line, c color.RGBA) {
|
|
x1, y1 := int(l.X1), int(l.Y1)
|
|
x2, y2 := int(l.X2), int(l.Y2)
|
|
|
|
dx := abs(x2 - x1)
|
|
dy := abs(y2 - y1)
|
|
sx := -1
|
|
if x1 < x2 {
|
|
sx = 1
|
|
}
|
|
sy := -1
|
|
if y1 < y2 {
|
|
sy = 1
|
|
}
|
|
err := dx - dy
|
|
|
|
for {
|
|
if x1 >= 0 && x1 < screen.Bounds().Dx() && y1 >= 0 && y1 < screen.Bounds().Dy() {
|
|
screen.Set(x1, y1, c)
|
|
}
|
|
|
|
if x1 == x2 && y1 == y2 {
|
|
break
|
|
}
|
|
|
|
e2 := 2 * err
|
|
if e2 > -dy {
|
|
err -= dy
|
|
x1 += sx
|
|
}
|
|
if e2 < dx {
|
|
err += dx
|
|
y1 += sy
|
|
}
|
|
}
|
|
}
|
|
|
|
// Helper function to draw a circle
|
|
func drawCircle(screen *ebiten.Image, cx, cy, radius float64, c color.RGBA) {
|
|
for angle := 0.0; angle < 2*math.Pi; angle += 0.1 {
|
|
x := int(cx + radius*math.Cos(angle))
|
|
y := int(cy + radius*math.Sin(angle))
|
|
if x >= 0 && x < screen.Bounds().Dx() && y >= 0 && y < screen.Bounds().Dy() {
|
|
screen.Set(x, y, c)
|
|
}
|
|
}
|
|
}
|
|
|
|
func abs(x int) int {
|
|
if x < 0 {
|
|
return -x
|
|
}
|
|
return x
|
|
}
|
|
|
|
func (s *System) IsPointInShadow(x, y float64, w *world.World) bool {
|
|
if !s.enabled || len(s.lights) == 0 {
|
|
return false
|
|
}
|
|
|
|
lines := worldToLines(w)
|
|
|
|
for _, light := range s.lights {
|
|
rayToPoint := Line{light.X, light.Y, x, y}
|
|
distanceToPoint := math.Sqrt((x-light.X)*(x-light.X) + (y-light.Y)*(y-light.Y))
|
|
|
|
blocked := false
|
|
for _, wall := range lines {
|
|
if ix, iy, ok := intersection(rayToPoint, wall); ok {
|
|
distanceToIntersection := math.Sqrt((ix-light.X)*(ix-light.X) + (iy-light.Y)*(iy-light.Y))
|
|
|
|
if distanceToIntersection < distanceToPoint-10.0 {
|
|
blocked = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if !blocked {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|