Hacky light rendering limited by distance and angle.
[tabakrolletjie.git] / tabakrolletjie / lights.py
1 """ May it be a light for you in dark places, when all other lights go out.
2 """
3
4 import pymunk
5 import pymunk.pygame_util
6 import pygame.display
7 import pygame.draw
8
9 from .constants import (
10     SCREEN_SIZE, LIGHT_CATEGORY, FITTINGS_CATEGORY)
11 from .utils import debug_timer
12
13 LIGHT_FILTER = pymunk.ShapeFilter(
14     mask=pymunk.ShapeFilter.ALL_MASKS ^ (
15         LIGHT_CATEGORY | FITTINGS_CATEGORY),
16     categories=LIGHT_CATEGORY)
17
18 FITTINGS_FILTER = pymunk.ShapeFilter(
19     mask=pymunk.ShapeFilter.ALL_MASKS ^ (
20         LIGHT_CATEGORY | FITTINGS_CATEGORY),
21     categories=FITTINGS_CATEGORY)
22
23
24 def screen_rays(pos):
25     """ An iterable that returns ordered rays from pos to the edge of the
26         screen, starting with the edge point (0, 0) and continuing clockwise
27         in pymunk coordinates.
28     """
29     w, h = SCREEN_SIZE
30     left, right, bottom, top = 0, w, 0, h
31     step = 1
32     for y in range(0, h, step):
33         yield pymunk.Vec2d(left, y)
34     for x in range(0, w, step):
35         yield pymunk.Vec2d(x, top)
36     for y in range(top, -1, -step):
37         yield pymunk.Vec2d(right, y)
38     for x in range(right, -1, -step):
39         yield pymunk.Vec2d(x, bottom)
40
41
42 @debug_timer("lights.calculate_ray_polys")
43 def calculate_ray_polys(space, body, position):
44     position = pymunk.Vec2d(position)
45     vertices = [position]
46     ray_polys = []
47     for ray in screen_rays(position):
48         info = space.segment_query_first(position, ray, 1, LIGHT_FILTER)
49         point = ray if info is None else info.point
50         vertices.append(point)
51         if len(vertices) > 3:
52             trial_poly = pymunk.Poly(None, vertices)
53             trial_poly.update(pymunk.Transform.identity())
54             query_prev = trial_poly.point_query(vertices[-2])
55             query_pos = trial_poly.point_query(position)
56             if query_prev.distance < -0.01 or query_pos.distance < -0.01:
57                 new_poly = pymunk.Poly(body, vertices[:-1])
58                 vertices = [position, vertices[-1]]
59                 ray_polys.append(new_poly)
60             else:
61                 vertices = trial_poly.get_vertices() + [point]
62     if len(vertices) > 2:
63         ray_polys.append(pymunk.Poly(body, vertices))
64     return ray_polys
65
66
67 class LightManager(object):
68     """ Manages a set of lights. """
69
70     def __init__(self, space, gamestate):
71         self._space = space
72         self._lights = [
73             BaseLight.load(cfg) for cfg in gamestate.station["lights"]]
74         for light in self._lights:
75             light.add(self._space)
76
77     def toggle_nearest(self, *args, **kw):
78         light = self.nearest(*args, **kw)
79         if light:
80             light.toggle()
81
82     def nearest(self, pos, surfpos=False, max_distance=1.0):
83         if surfpos:
84             surface = pygame.display.get_surface()
85             pos = pymunk.pygame_util.from_pygame(pos, surface)
86         point_info = self._space.point_query_nearest(
87             pos, max_distance, pymunk.ShapeFilter(mask=FITTINGS_CATEGORY))
88         if point_info is not None:
89             return point_info.shape.body.light
90         return None
91
92     def lit_by(self, pos, surfpos=False, max_distance=0.0):
93         if surfpos:
94             surface = pygame.display.get_surface()
95             pos = pymunk.pygame_util.from_pygame(pos, surface)
96         point_info_list = self._space.point_query(
97             pos, max_distance, pymunk.ShapeFilter(mask=LIGHT_CATEGORY))
98         lights = [p.shape.body.light for p in point_info_list]
99         return [light for light in lights if light.on]
100
101     def render_light(self, surface):
102         for light in self._lights:
103             light.render_light(surface)
104
105     def render_fittings(self, surface):
106         for light in self._lights:
107             light.render_fitting(surface)
108
109
110 class BaseLight(object):
111     """ Common light functionality. """
112
113     COLOURS = {
114         "red": (255, 0, 0),
115         "green": (0, 255, 0),
116         "blue": (0, 255, 255),
117         "yellow": (255, 255, 0),
118         "white": (255, 255, 255),
119     }
120
121     def __init__(
122             self, colour, position, intensity=1.0,
123             radius_limits=(None, None), angle_limits=(None, None)):
124         self.colour = colour
125         self.position = pymunk.Vec2d(position)
126         self.on = True
127         self.intensity = intensity
128         self.radius_limits = radius_limits
129         self.angle_limits = angle_limits
130         self.body = pymunk.Body(0, 0, pymunk.body.Body.STATIC)
131         self.fitting = pymunk.Circle(self.body, 10.0, self.position)
132         self.body.light = self
133
134     @classmethod
135     def load(cls, config):
136         kw = config.copy()
137         light_type = kw.pop("type")
138         [light_class] = [
139             c for c in cls.__subclasses__()
140             if c.__name__.lower() == light_type]
141         return light_class(**kw)
142
143     def add(self, space):
144         if self.body.space is not None:
145             space.remove(self.body, *self.body.shapes)
146         shapes = self.shapes_for_ray_polys(
147             calculate_ray_polys(space, self.body, self.position))
148         for shape in shapes:
149             shape.filter = LIGHT_FILTER
150         self.fitting.filter = FITTINGS_FILTER
151         space.add(self.body, self.fitting, *shapes)
152
153     def shapes_for_ray_polys(self, ray_polys):
154         return ray_polys
155
156     def toggle(self):
157         self.on = not self.on
158
159     def render_light(self, surface):
160         if not self.on:
161             return
162
163         raypoly_mask = surface.copy()
164         white, black = (255, 255, 255, 255), (0, 0, 0, 0)
165         raypoly_mask.fill(black)
166         for shape in self.body.shapes:
167             if shape is self.fitting:
168                 continue
169             pygame_poly = [
170                 pymunk.pygame_util.to_pygame(v, surface) for v in
171                 shape.get_vertices()]
172             pygame.draw.polygon(raypoly_mask, white, pygame_poly, 0)
173             pygame.draw.aalines(raypoly_mask, white, True, pygame_poly, 1)
174
175         limits_mask = surface.copy()
176         limits_mask.fill(black)
177         centre = pymunk.pygame_util.to_pygame(self.position, surface)
178         max_radius = self.radius_limits[1] or 50.0
179         box = (centre[0] - max_radius, centre[1] - max_radius,
180                max_radius * 2, max_radius * 2)
181         width = max_radius - (self.radius_limits[0] or 0)
182         box2 = (box[0] + 1,) + tuple(box[1:])
183         box3 = (box[0] + 2,) + tuple(box[1:])
184         import math
185         start_angle = (self.angle_limits[0] or 0.0) * (math.pi / 180.0)
186         end_angle = (self.angle_limits[1] or 360.0) * (math.pi / 180.0)
187         pygame.draw.arc(
188             limits_mask, white, box, start_angle, end_angle, int(width))
189         pygame.draw.arc(
190             limits_mask, white, box2, start_angle, end_angle, int(width))
191         pygame.draw.arc(
192             limits_mask, white, box3, start_angle, end_angle, int(width))
193
194         import pygame.locals as pgl
195         raypoly_mask.blit(limits_mask, (0, 0), None, pgl.BLEND_RGBA_MIN)
196         raypoly_mask.set_colorkey(black)
197
198         light_colour = self.COLOURS[self.colour]
199         overlay = surface.copy()
200         overlay.fill(light_colour)
201         raypoly_mask.blit(overlay, (0, 0), None, pgl.BLEND_RGBA_MULT)
202
203         mask2 = surface.copy()
204         mask2.set_alpha(255)
205         mask2.blit(raypoly_mask, (0, 0), None)
206
207         mask2.set_alpha(int(255 * self.intensity))
208
209         surface.blit(mask2, (0, 0), None)
210
211     def render_fitting(self, surface):
212         pygame.draw.circle(
213             surface, (255, 255, 0),
214             pymunk.pygame_util.to_pygame(self.fitting.offset, surface),
215             int(self.fitting.radius))
216
217
218 class SpotLight(BaseLight):
219     def __init__(self, **kw):
220         kw.pop("direction", None)
221         kw.pop("spread", None)
222         super(SpotLight, self).__init__(**kw)