Skip to content

Commit

Permalink
Added a text raycaster.
Browse files Browse the repository at this point in the history
  • Loading branch information
salt-die committed Apr 6, 2024
1 parent 92a8f8c commit 1e84d84
Show file tree
Hide file tree
Showing 5 changed files with 597 additions and 59 deletions.
76 changes: 54 additions & 22 deletions examples/advanced/raycasting.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""A raycaster example that includes an animated texture."""
"""Showcase of batgrl's raycasters."""

import asyncio
from itertools import cycle, pairwise
Expand All @@ -8,15 +8,36 @@
import cv2
import numpy as np
from batgrl.app import App
from batgrl.gadgets.raycaster import Raycaster, RaycasterCamera, Sprite
from batgrl.colors import GREEN
from batgrl.gadgets.raycaster import Raycaster, RaycasterCamera, RgbaTexture, Sprite
from batgrl.gadgets.text_raycaster import TextRaycaster
from batgrl.gadgets.text_tools import cell
from batgrl.gadgets.texture_tools import read_texture
from batgrl.gadgets.video import Video
from batgrl.geometry import lerp

ASSETS = Path(__file__).parent.parent / "assets"
SPINNER = ASSETS / "spinner.gif"
CHECKER = ASSETS / "checkered.png"
SPRITE = ASSETS / "pixel_python.png"

def load_assets():
assets = Path(__file__).parent.parent / "assets"

yield assets / "spinner.gif"

checker = assets / "checkered.png"
yield read_texture(checker)

python_sprite = assets / "pixel_python.png"
yield read_texture(python_sprite)

wall = assets / "wall.txt"
yield np.array(
[[int(char) for char in line] for line in wall.read_text().splitlines()]
)

tree = assets / "tree.txt"
yield np.array([list(line) for line in tree.read_text().splitlines()])


SPINNER, CHECKER, PYTHON, WALL, TREE = load_assets()
MAP = np.array(
[
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
Expand All @@ -34,7 +55,7 @@
)


class VideoTexture(Video):
class VideoTexture(Video, RgbaTexture):
"""A video player that implements the `RgbaTexture` protocol."""

def __init__(self, source):
Expand Down Expand Up @@ -63,50 +84,61 @@ async def on_start(self):
video.play()
camera = RaycasterCamera(pos=points[0], theta=angles[0])
raycaster = Raycaster(
map=MAP,
caster_map=MAP,
camera=camera,
wall_textures=[video],
sprites=[Sprite(pos=points[i], texture_idx=0) for i in range(4)],
sprite_textures=[read_texture(SPRITE)],
floor=read_texture(CHECKER),
size_hint={"height_hint": 1.0, "width_hint": 1.0},
sprite_textures=[PYTHON],
floor=CHECKER,
size_hint={"height_hint": 1.0, "width_hint": 0.5},
)
text_raycaster = TextRaycaster(
caster_map=MAP,
camera=camera,
wall_textures=[WALL],
sprites=[Sprite(pos=points[i], texture_idx=0) for i in range(4)],
sprite_textures=[TREE],
default_cell=cell(fg_color=GREEN),
size_hint={"height_hint": 1.0, "width_hint": 0.5},
pos_hint={"x_hint": 0.5, "anchor": "left"},
)
self.add_gadget(raycaster)
self.add_gadgets(raycaster, text_raycaster)

turn_duration = 0.75
move_duration = 1.5
last_time = monotonic()
elapsed = 0

TURN_DURATION = 0.75
MOVE_DURATION = 1.5

for i, j in pairwise(cycle(range(4))):
u, v = angles[i], angles[j]
if v > u:
v -= 2 * np.pi

while elapsed < TURN_DURATION:
while elapsed < turn_duration:
current_time = monotonic()
elapsed += current_time - last_time
last_time = current_time
camera.theta = lerp(u, v, elapsed / TURN_DURATION)
camera.theta = lerp(u, v, elapsed / turn_duration)
raycaster.cast_rays()
text_raycaster.cast_rays()
await asyncio.sleep(0)

camera.theta = v
elapsed -= TURN_DURATION
elapsed -= turn_duration

while elapsed < MOVE_DURATION:
while elapsed < move_duration:
current_time = monotonic()
elapsed += current_time - last_time
last_time = current_time

camera.pos = lerp(points[i], points[j], elapsed / MOVE_DURATION)
camera.pos = lerp(points[i], points[j], elapsed / move_duration)
raycaster.cast_rays()
text_raycaster.cast_rays()
await asyncio.sleep(0)

camera.pos = points[j]
elapsed -= MOVE_DURATION
elapsed -= move_duration


if __name__ == "__main__":
RaycasterApp(title="Raycaster Example").run()
RaycasterApp(title="Raycasting Example").run()
28 changes: 28 additions & 0 deletions examples/assets/tree.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
00000000000000000000000000000000000000000000.000000000
0000000000000000000000000000000000.000000000;000000000
00000.00000000000000.00000000000000;%00000;;0000000000
0000000,00000000000,0000000000000000:;%00%;00000000000
00000000:000000000;0000000000000000000:;%;'00000.,0000
,.00000000%;00000%;000000000000;00000000%;'0000,;00000
0;0000000;%;00%%;00000000,00000%;0000;%;0000,%'0000000
00%;0000000%;%;000000,00;0000000%;00;%;000,%;'00000000
000;%;000000%;00000000;%;00000000%0;%;00,%;'0000000000
0000`%;.00000;%;00000%;'000000000`;%%;.%;'000000000000
00000`:;%.0000;%%.0%@;00000000%;0;@%;%'000000000000000
00000000`:%;.00:;bd%;0000000000%;@%;'00000000000000000
0000000000`@%:.00:;%.000000000;@@%;'000000000000000000
000000000000`@%.00`;@%.000000;@@%;00000000000000000000
00000000000000`@%%.0`@%%0000;@@%;000000000000000000000
0000000000000000;@%.0:@%%00%@@%;0000000000000000000000
000000000000000000%@bd%%%bd%%:;00000000000000000000000
00000000000000000000#@%%%%%:;;000000000000000000000000
00000000000000000000%@@%%%::;0000000000000000000000000
00000000000000000000%@@@%(o);00.0'00000000000000000000
00000000000000000000%@@@o%;:(.,'0000000000000000000000
0000000000000000`..0%@@@o%::;0000000000000000000000000
0000000000000000000`)@@@o%::;0000000000000000000000000
00000000000000000000%@@(o)::;0000000000000000000000000
0000000000000000000.%@@@@%::;0000000000000000000000000
0000000000000000000;%@@@@%::;.000000000000000000000000
000000000000000000;%@@@@%%:;;;.00000000000000000000000
00000000000000...;%@@@@@%%:;;;;,..00000000000000000000
21 changes: 21 additions & 0 deletions examples/assets/wall.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
55555555555555555555555555555
55555555555555555555555555555
55555555000000000000055555555
55555500000000000000000555555
55555500055555555555000555555
55555500055555555555000555555
55555500055555555555000555555
55555500055555555555000555555
55555555555555555555555555555
55555500055555555555000555555
55555500055555555555000555555
55555500055555555555000555555
55555500055555555555000555555
55555555000000000000055555555
55555555550000000005555555555
55555555555555555555555555555
55555555555555555555555555555
55555555555555555555555555555
55555555555555555555555555555
55555555555555555555555555555
55555555555555555555555555555
82 changes: 45 additions & 37 deletions src/batgrl/gadgets/raycaster.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,28 +146,28 @@ class Raycaster(Graphics):
Parameters
----------
map : NDArray[np.ushort]
An array-like with non-zero entries n indicating walls with texture
`wall_textures[n - 1]`.
caster_map : NDArray[np.ushort]
The raycaster map.
camera : RaycasterCamera
A view in the map.
The raycaster camera.
wall_textures : List[RgbaTexture]
Textures for walls.
light_wall_textures : list[RgbaTexture] | None, default: None
If provided, walls north/south face will use textures in
:attr:`light_wall_textures` instead of :attr:`wall_textures`.
Optional wall textures for north/south facing walls.
sprites : list[Sprite] | None, default: None
List of sprites.
A list of sprites.
sprite_textures : list[RgbaTexture] | None, default: None
Textures for sprites.
ceiling : RgbaTexture | None, default: None
Ceiling texture.
Optional ceiling texture.
ceiling_color : Color, default: BLACK
Color of ceiling if no ceiling texture.
floor : RgbaTexture | None, default: None
Floor texture.
Optional floor texture.
floor_color : Color, default: BLACK
Color of floor if no floor texture.
max_hops : int, default: 20
Determines how far rays are cast.
default_color : AColor, default: AColor(0, 0, 0, 0)
Default texture color.
alpha : float, default: 1.0
Expand All @@ -193,27 +193,28 @@ class Raycaster(Graphics):
Attributes
----------
map : NDArray[np.ushort]
An array-like with non-zero entries n indicating walls with texture
`wall_textures[n - 1]`.
caster_map : NDArray[np.ushort]
The raycaster map.
camera : RaycasterCamera
A view in the map.
The raycaster camera.
wall_textures : List[RgbaTexture]
East/west-faced walls' textures.
Textures for walls.
light_wall_textures : list[RgbaTexture]
North/south-faced walls' textures.
Wall textures for north/sourth facing walls.
sprites : list[Sprite]
List of sprites.
A list of sprites.
sprite_textures : list[RgbaTexture]
Textures for sprites.
ceiling : RgbaTexture | None
Ceiling texture.
The ceiling texture.
ceiling_color : Color
Color of ceiling if no ceiling texture.
floor : RgbaTexture
Floor texture.
The floor texture.
floor_color : Color
Color of floor if no floor texture.
max_hops : int
Determines how far rays are cast.
texture : NDArray[np.uint8]
uint8 RGBA color array.
default_color : AColor
Expand Down Expand Up @@ -325,12 +326,10 @@ class Raycaster(Graphics):
Remove this gadget and recursively remove all its children.
"""

HOPS = 20 # How far rays are cast.

def __init__(
self,
*,
map: NDArray[np.ushort],
caster_map: NDArray[np.ushort],
camera: RaycasterCamera,
wall_textures: list[RgbaTexture] | None,
light_wall_textures: list[RgbaTexture] | None = None,
Expand All @@ -340,6 +339,7 @@ def __init__(
ceiling_color: Color = BLACK,
floor: RgbaTexture | None = None,
floor_color: Color = BLACK,
max_hops: int = 20,
default_color: AColor = TRANSPARENT,
alpha: float = 1.0,
interpolation: Interpolation = "linear",
Expand All @@ -363,17 +363,28 @@ def __init__(
is_visible=is_visible,
is_enabled=is_enabled,
)

self.map = map
self.caster_map = caster_map
"""The raycaster map."""
self.camera = camera
"""The raycaster camera."""
self.wall_textures = wall_textures
"""Textures for walls."""
self.light_wall_textures = light_wall_textures or wall_textures
"""Optional wall textures for north/south facing walls."""
self.sprites = sprites
"""A list of sprites."""
self.sprite_textures = sprite_textures
"""Textures for sprites."""
self.ceiling = ceiling
"""Optional ceiling texture."""
self.ceiling_color = ceiling_color
"""Color of ceiling if no ceiling texture."""
self.floor = floor
"""Optional floor texture."""
self.floor_color = floor_color
"""Color of floor if no floor texture."""
self.max_hops = max_hops
"""Determines how far rays are cast."""

# Buffers
self._pos_int = np.zeros((2,), dtype=int)
Expand Down Expand Up @@ -440,7 +451,7 @@ def _cast_ray(self, column):
"""Cast a ray for a given column of the screen."""
camera = self.camera
camera_pos = camera.pos
map = self.map
caster_map = self.caster_map

ray_pos = self._pos_int
ray_pos[:] = camera_pos
Expand All @@ -450,33 +461,31 @@ def _cast_ray(self, column):
step = self._steps[column]
sides = self._sides[column]

# Casting #
for _ in range(self.HOPS):
# Cast a ray until we hit a wall or hit max_hops
for _ in range(self.max_hops):
side = 0 if sides[0] < sides[1] else 1
sides[side] += delta[side]
ray_pos[side] += step[side]

if texture_index := map[tuple(ray_pos)]:
if texture_index := caster_map[tuple(ray_pos)]:
# Distance from wall to camera plane.
# Note that distance of wall to camera is not used
# as it would result in a "fish-eye" effect.
distance = (
ray_pos[side] - camera_pos[side] + (0 if step[side] == 1 else 1)
) / ray_angle[side]
break

else: # No walls in range.
distance = 1000 # 1000 == infinity, roughly
distance = 10000

self._column_distances[column] = distance

# Rendering #
texture = self.texture[:, ::-1]
height = texture.shape[0]

column_height = (
int(height / distance) if distance else 1000
) # 1000 == infinity, roughly
column_height = int(height / distance) if distance else 10000
if column_height == 0:
return

# Start and end y-coordinates of column.
half_height = height >> 1
Expand Down Expand Up @@ -614,14 +623,13 @@ def _cast_sprites(self):
tex_height, tex_width, _ = sprite_tex.shape

clip_y = (sprite_height - h) / 2
np.add(rows, clip_y, out=rows)
np.multiply(rows, tex_height / sprite_height, out=rows)
rows += clip_y
rows *= tex_height / sprite_height
np.clip(rows, 0, None, out=rows)

clip_x = sprite_x - sprite_width / 2
tex_xs = columns - clip_x
np.multiply(tex_xs, tex_width, out=tex_xs)
np.divide(tex_xs, sprite_width, out=tex_xs)
tex_xs *= tex_width / sprite_width

sprite_rect = sprite_tex[rows.astype(int)][:, tex_xs.astype(int)]
dst = texture[start_y:end_y, columns, :3]
Expand Down
Loading

0 comments on commit 1e84d84

Please sign in to comment.