Lighting experiments
This commit is contained in:
353
internal/raycast/raycast.go
Normal file
353
internal/raycast/raycast.go
Normal file
@@ -0,0 +1,353 @@
|
||||
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-5.0 {
|
||||
blocked = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !blocked {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user