precomputed shade algo, viewport basic render
This commit is contained in:
281
engine/fov/precomputed_shade/precomputed_shade.go
Normal file
281
engine/fov/precomputed_shade/precomputed_shade.go
Normal file
@ -0,0 +1,281 @@
|
||||
package precomputed_shade
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"lab.zaar.be/thefish/alchemyst-go/engine/fov/basic"
|
||||
"lab.zaar.be/thefish/alchemyst-go/engine/gamemap"
|
||||
"lab.zaar.be/thefish/alchemyst-go/engine/types"
|
||||
"math"
|
||||
"sort"
|
||||
)
|
||||
|
||||
/*
|
||||
Why am I here? Well, I just don't know what to call it - I'm sure it's an established method, and I'm aware there are probably optimisations to be had. I thought if I roughed out the algorithm here, the r/roguelikedev community would surely be able to help! I haven't included optimisations here, but if anyone wants them I got 'em :)
|
||||
Method
|
||||
Beforehand
|
||||
|
||||
List the cells in your largest-possible FOV, storing X and Y values relative to the center.
|
||||
|
||||
Store the distance from the center for each cell, and sort the list by this in ascending order.
|
||||
|
||||
Store the range of angles occludedAngles by each cell in this list, in clockwise order as absolute integers only.
|
||||
|
||||
Create a 360-char string of 0s called EmptyShade, and a 360-char string of 1s called FullShade
|
||||
|
||||
Runtime
|
||||
|
||||
Store two strings – CurrentShade and NextShade
|
||||
|
||||
Set CurrentShade to EmptyShade to start.
|
||||
|
||||
While CurrentShade =/= FullShade: step through the Cell List:
|
||||
|
||||
If the distance to the current cell is not equal to the previous distance checked then replace the contents of the
|
||||
CurrentShade variable with the contents of the NextShade variable.
|
||||
|
||||
If the tested cell is opaque – for each angle in the range occludedAngles by the cell, place a 1 at the position
|
||||
determined by angle%360 in the NextShade string.
|
||||
|
||||
For each angle in the range occludedAngles by the cell, add 1 to the shade value for that cell for each 0 encountered
|
||||
at the position determined by angle%360 in the CurrentShade string.
|
||||
|
||||
Notes
|
||||
Benefits
|
||||
|
||||
No messing around with octants
|
||||
|
||||
Highly efficient - each cell is only visited once, and checks within that cell are rare. It performs as fast as any
|
||||
other LOS I've tried but with more options
|
||||
|
||||
Human-readable - code and output are highly legible, making it very easy to work with
|
||||
|
||||
Flexible - I'm using it for FOV, LOS, renderer, and lighting. Each process is calling the same function, within which
|
||||
flags control how much data is evaluated and output. It only uses the data it needs to in the context where
|
||||
its needed – so monsters that need a list of things they can see only check if a cell is visible or not, and don’t
|
||||
bother calculating how much visibility they have there. This cuts processing dramatically.
|
||||
|
||||
Other links
|
||||
|
||||
cfov by Konstantin Stupnik on RogueTemple
|
||||
|
||||
Pre-Computed Visiblity Trees on RogueBasin
|
||||
|
||||
/r/roguelikedev FAQ Friday on FOV which kicked off this train of thought
|
||||
|
||||
/u/pnjeffries on his FOV algorithm which inspired this one
|
||||
|
||||
Adam Milazzo's FOV Method Roundup where a similar method described as 'permissive' is detailed
|
||||
*/
|
||||
|
||||
var errNotFoundCell = errors.New("Cell not found")
|
||||
var errOutOfBounds = errors.New("Cell out of bounds")
|
||||
|
||||
type Cell struct {
|
||||
types.Coords
|
||||
distance float64
|
||||
occludedAngles []int //indexes of cells in CellList
|
||||
shade int //shade value
|
||||
}
|
||||
|
||||
type CellList []*Cell
|
||||
|
||||
type DistanceSorter CellList
|
||||
|
||||
func (a DistanceSorter) Len() int { return len(a) }
|
||||
func (a DistanceSorter) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a DistanceSorter) Less(i, j int) bool { return a[i].distance < a[j].distance }
|
||||
|
||||
type precomputedShade struct {
|
||||
initCoords types.Coords
|
||||
MaxTorchRadius int
|
||||
CellList CellList
|
||||
FovMap [][]int
|
||||
LightWalls bool
|
||||
}
|
||||
|
||||
func NewPrecomputedShade(maxTorchRadius int) *precomputedShade {
|
||||
result := &precomputedShade{MaxTorchRadius: maxTorchRadius, LightWalls: true}
|
||||
result.PrecomputeFovMap()
|
||||
return result
|
||||
}
|
||||
|
||||
func (ps *precomputedShade) FindByCoords(c types.Coords) (int, *Cell, error) {
|
||||
for i := range ps.CellList {
|
||||
if ps.CellList[i].Coords == c {
|
||||
// Found!
|
||||
return i, ps.CellList[i], nil
|
||||
}
|
||||
}
|
||||
return 0, &Cell{}, errNotFoundCell
|
||||
}
|
||||
|
||||
func (ps *precomputedShade) IsInFov(coords types.Coords) bool {
|
||||
rc := ps.fromLevelCoords(coords)
|
||||
_, cell, err := ps.FindByCoords(rc)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return cell.shade > 0
|
||||
}
|
||||
|
||||
func (ps *precomputedShade) SetLightWalls(value bool) {
|
||||
ps.LightWalls = value
|
||||
}
|
||||
|
||||
func (ps *precomputedShade) Init() {
|
||||
ps.PrecomputeFovMap()
|
||||
}
|
||||
|
||||
func (ps *precomputedShade) PrecomputeFovMap() {
|
||||
max := ps.MaxTorchRadius
|
||||
minusMax := (-1) * max
|
||||
zeroCoords := types.Coords{0, 0}
|
||||
var x, y int
|
||||
//fill list
|
||||
for x = minusMax; x < max+1; x++ {
|
||||
for y = minusMax; y < max+1; y++ {
|
||||
if x == 0 && y == 0 {
|
||||
continue;
|
||||
}
|
||||
iterCoords := types.Coords{x, y}
|
||||
distance := zeroCoords.DistanceTo(iterCoords)
|
||||
if distance <= float64(max) {
|
||||
ps.CellList = append(ps.CellList, &Cell{iterCoords, distance, nil, 0})
|
||||
}
|
||||
}
|
||||
}
|
||||
//Do not change cell order after this!
|
||||
sort.Sort(DistanceSorter(ps.CellList))
|
||||
//debug
|
||||
//for _, cell := range ps.CellList {
|
||||
// fmt.Printf("\n coords: %v, distance: %f, len_occl: %d", cell.Coords, cell.distance, len(cell.occludedAngles))
|
||||
//}
|
||||
|
||||
//Bresanham lines / Raycast
|
||||
var lineX, lineY float64
|
||||
for i := 0; i < 360; i++ {
|
||||
dx := math.Sin(float64(i) / (float64(180) / math.Pi))
|
||||
dy := math.Cos(float64(i) / (float64(180) / math.Pi))
|
||||
|
||||
lineX = 0
|
||||
lineY = 0
|
||||
for j := 0; j < max; j++ {
|
||||
lineX -= dx
|
||||
lineY -= dy
|
||||
|
||||
roundedX := int(basic.Round(lineX))
|
||||
roundedY := int(basic.Round(lineY))
|
||||
|
||||
_, cell, err := ps.FindByCoords(types.Coords{roundedX, roundedY})
|
||||
|
||||
if err != nil {
|
||||
//inexistent coord found
|
||||
break;
|
||||
}
|
||||
cell.occludedAngles = unique(append(cell.occludedAngles, i))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//for _, cell := range ps.CellList {
|
||||
// fmt.Printf("\n coords: %v, distance: %f, len_occl: %d", cell.Coords, cell.distance, len(cell.occludedAngles))
|
||||
//}
|
||||
}
|
||||
|
||||
func (ps *precomputedShade) recalc(level *gamemap.Level, initCoords types.Coords, radius int) {
|
||||
|
||||
ps.initCoords = initCoords
|
||||
|
||||
if radius > ps.MaxTorchRadius {
|
||||
radius = ps.MaxTorchRadius //fixme
|
||||
}
|
||||
|
||||
level.Tiles[initCoords.X][initCoords.Y].Visible = true
|
||||
|
||||
var fullShade = make([]byte, 360)
|
||||
for i := range fullShade {
|
||||
fullShade[i] = 1
|
||||
}
|
||||
var emptyShade = make([]byte, 360)
|
||||
currentShade := emptyShade
|
||||
nextShade := emptyShade
|
||||
|
||||
i := 0
|
||||
prevDistance := 0.0
|
||||
for !bytes.Equal(currentShade, fullShade) {
|
||||
if (i == len(ps.CellList)-1) {
|
||||
break
|
||||
}
|
||||
cell := ps.CellList[i]
|
||||
i++
|
||||
if cell.distance != prevDistance {
|
||||
currentShade = nextShade
|
||||
}
|
||||
|
||||
if cell.distance > float64(radius) {
|
||||
break
|
||||
}
|
||||
|
||||
lc, err := ps.toLevelCoords(level, initCoords, cell.Coords)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
//fmt.Printf("\n level coords: %v", lc)
|
||||
if level.Tiles[lc.X][lc.Y].BlocksSight {
|
||||
level.Tiles[lc.X][lc.Y].Visible = true
|
||||
for _, angle := range cell.occludedAngles {
|
||||
nextShade[angle] = 1
|
||||
}
|
||||
}
|
||||
|
||||
for _, angle := range cell.occludedAngles {
|
||||
if currentShade[angle] == 0 {
|
||||
cell.shade = cell.shade + 1
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ps *precomputedShade) ComputeFov(level *gamemap.Level, initCoords types.Coords, radius int) {
|
||||
|
||||
ps.recalc(level, initCoords, radius)
|
||||
|
||||
for _, cell := range ps.CellList {
|
||||
//fmt.Printf("\n coords: %v, distance: %f, shade: %d", cell.Coords, cell.distance, cell.shade)
|
||||
if cell.shade > 0 {
|
||||
cs, err := ps.toLevelCoords(level, initCoords, cell.Coords)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
level.Tiles[cs.X][cs.Y].Visible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ps *precomputedShade) toLevelCoords(level *gamemap.Level, initCoords, relativeCoords types.Coords) (types.Coords, error) {
|
||||
realCoords := types.Coords{initCoords.X + relativeCoords.X, initCoords.Y + relativeCoords.Y}
|
||||
if !level.InBounds(realCoords) {
|
||||
return types.Coords{}, errOutOfBounds
|
||||
}
|
||||
return realCoords, nil
|
||||
}
|
||||
|
||||
func (ps *precomputedShade) fromLevelCoords(lc types.Coords) types.Coords {
|
||||
relativeCoords := types.Coords{lc.X - ps.initCoords.X, lc.Y - ps.initCoords.Y}
|
||||
return relativeCoords
|
||||
}
|
||||
|
||||
func unique(intSlice []int) []int {
|
||||
keys := make(map[int]bool)
|
||||
list := []int{}
|
||||
for _, entry := range intSlice {
|
||||
if _, value := keys[entry]; !value {
|
||||
keys[entry] = true
|
||||
list = append(list, entry)
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
85
engine/fov/precomputed_shade/precomputed_shade_test.go
Normal file
85
engine/fov/precomputed_shade/precomputed_shade_test.go
Normal file
@ -0,0 +1,85 @@
|
||||
package precomputed_shade
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"lab.zaar.be/thefish/alchemyst-go/engine/gamemap"
|
||||
"lab.zaar.be/thefish/alchemyst-go/engine/types"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPsDistance(t *testing.T) {
|
||||
iterCoords := types.Coords{0, 0}
|
||||
|
||||
fmt.Printf("\n dto: \t %v", iterCoords.DistanceTo(types.Coords{0, 1}))
|
||||
fmt.Printf("\n dto: \t %v", iterCoords.DistanceTo(types.Coords{0, 5}))
|
||||
fmt.Printf("\n dto: \t %v", iterCoords.DistanceTo(types.Coords{3, 3}))
|
||||
fmt.Printf("\n dto: \t %v", iterCoords.DistanceTo(types.Coords{100, 0}))
|
||||
}
|
||||
|
||||
func TestPrecompShade(t *testing.T) {
|
||||
ppFov := NewPrecomputedShade(15)
|
||||
_ = ppFov
|
||||
t.Log("ok")
|
||||
|
||||
//level := gamemap.NewLevel(util.ClientCtx{}, "test", 1)
|
||||
|
||||
level := &gamemap.Level{
|
||||
Name: "test1",
|
||||
Depth: 1,
|
||||
Rect: types.NewRect(0, 0, 20, 20),
|
||||
}
|
||||
|
||||
level.Tiles = make([][]*gamemap.Tile, level.W)
|
||||
for i := range level.Tiles {
|
||||
level.Tiles[i] = make([]*gamemap.Tile, level.H)
|
||||
}
|
||||
|
||||
var tile func() (*gamemap.Tile)
|
||||
|
||||
for x := 0; x < level.W; x++ {
|
||||
for y := 0; y < level.H; y++ {
|
||||
if x == 0 || y == 0 || x == (level.W-1) || y == (level.H-1) {
|
||||
tile = gamemap.NewWall
|
||||
} else {
|
||||
tile = gamemap.NewFloor
|
||||
}
|
||||
level.Tiles[x][y] = tile()
|
||||
}
|
||||
}
|
||||
|
||||
playerCoords := types.Coords{10, 10}
|
||||
|
||||
level.Tiles[8][12] = gamemap.NewWall()
|
||||
level.Tiles[10][8] = gamemap.NewWall()
|
||||
|
||||
level.Tiles[7][9] = gamemap.NewWall()
|
||||
level.Tiles[7][11] = gamemap.NewWall()
|
||||
level.Tiles[5][10] = gamemap.NewWall()
|
||||
|
||||
level.Tiles[10][11] = gamemap.NewWall()
|
||||
level.Tiles[11][10] = gamemap.NewWall()
|
||||
|
||||
ppFov.ComputeFov(level, playerCoords, 15)
|
||||
|
||||
fmt.Printf("\n\n")
|
||||
|
||||
var render = func(x, y int) string {
|
||||
if playerCoords.X == x && playerCoords.Y == y {
|
||||
return "@"
|
||||
}
|
||||
result := level.Tiles[x][y].Char
|
||||
if !level.Tiles[x][y].Visible {
|
||||
result = "?"
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
for y := 0; y < level.H; y++ {
|
||||
for x := 0; x < level.W; x++ {
|
||||
|
||||
fmt.Printf("%s", render(x, y))
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user