Source code for arcade.future.light.lights

from array import array
from collections.abc import Iterator, Sequence

from arcade import gl
from arcade.color import WHITE
from arcade.future.texture_render_target import RenderTargetTexture
from arcade.types import RGBOrA255


[docs] class Light: """ Create a Light. Note: It's important to separate lights that don't change properties and static ones with the ``usage`` parameter. Args: center_x: X position of the light center_y: Y position of the light radius: Radius of the light color: Color of the light mode: 'hard' or 'soft' light """ HARD = 1.0 SOFT = 0.0 def __init__( self, center_x: float, center_y: float, radius: float = 50.0, color: RGBOrA255 = WHITE, mode: str = "hard", ): if not (isinstance(color, tuple) or isinstance(color, list)): raise ValueError( "Color must be a 3-4 element Tuple or List with red-green-blue " " and optionally an alpha." ) if not isinstance(mode, str) or not (mode == "soft" or mode == "hard"): raise ValueError("Mode must be set to either 'soft' or 'hard'.") self._center_x = center_x self._center_y = center_y self._radius = radius self._attenuation = Light.HARD if mode == "hard" else Light.SOFT self._color = color[:3] self._light_layer: LightLayer | None = None if len(self._color) != 3: raise ValueError( "Color must be a 3-4 element Tuple or List with red-green-blue " "and optionally an alpha." ) @property def position(self) -> tuple[float, float]: """Get or set the light position""" return self._center_x, self._center_y @position.setter def position(self, value): if self._light_layer: self._light_layer._rebuild = True self._center_x, self._center_y = value @property def radius(self) -> float: """Get or set the light size""" return self._radius @radius.setter def radius(self, value): if self._light_layer: self._light_layer._rebuild = True self._radius = value
[docs] class LightLayer(RenderTargetTexture): """ Create a LightLayer The size of a layer should ideally be of the same size and the screen. Args: width: Width of light layer height: Height of light layer """ def __init__(self, width: int, height: int): super().__init__(width, height) self._lights: list[Light] = [] self._prev_target = None self._rebuild = False self._stride = 28 self._buffer = self.ctx.buffer(reserve=self._stride * 100) # fmt: off vertex_data = array('f', [ -1.0, +1.0, 0.0, 1.0, -1.0, -1.0, 0.0, 0.0, +1.0, +1.0, 1.0, 1.0, +1.0, -1.0, 1.0, 0.0, ]) # fmt: on self._vao = self.ctx.geometry( [ gl.BufferDescription( self.ctx.buffer(data=vertex_data), "2f 2f", ["in_vert", "in_uv"], ), gl.BufferDescription( self._buffer, "2f 1f 1f 3f", [ "in_instance_position", "in_instance_radius", "in_instance_attenuation", "in_instance_color", ], instanced=True, ), ] ) self._light_program = self.ctx.load_program( vertex_shader=":system:shaders/lights/point_lights_vs.glsl", fragment_shader=":system:shaders/lights/point_lights_fs.glsl", ) self._combine_program = self.ctx.load_program( vertex_shader=":system:shaders/lights/combine_vs.glsl", fragment_shader=":system:shaders/lights/combine_fs.glsl", ) # NOTE: Diffuse buffer created in parent self._light_buffer = self.ctx.framebuffer( color_attachments=self.ctx.texture((width, height), components=3) ) @property def diffuse_texture(self): """The diffuse texture""" return self.texture @property def light_texture(self): """The light texture""" return self._light_buffer.color_attachments[0]
[docs] def resize(self, width, height): """Resize the light layer""" super().resize(width, height) self._light_buffer = self.ctx.framebuffer( color_attachments=self.ctx.texture((width, height), components=3) )
[docs] def clear(self): """Clear the light layer""" super().clear() self._light_buffer.clear()
[docs] def add(self, light: Light): """Add a Light to the layer""" self._lights.append(light) light._light_layer = self self._rebuild = True
[docs] def extend(self, lights: Sequence[Light]): """Add a list of lights to the layer""" for light in lights: self.add(light)
[docs] def remove(self, light: Light): """Remove a light to the layer""" self._lights.remove(light) light._light_layer = None self._rebuild = True
[docs] def __len__(self) -> int: """Number of lights""" return len(self._lights)
[docs] def __iter__(self) -> Iterator[Light]: """Return an iterable object of lights""" return iter(self._lights)
def __getitem__(self, i) -> Light: return self._lights[i] def __enter__(self): self._prev_target = self.ctx.active_framebuffer self._fbo.use() self._fbo.clear(color=self._background_color) return self def __exit__(self, exc_type, exc_val, exc_tb): self._prev_target.use()
[docs] def draw( self, position: tuple[float, float] = (0, 0), target=None, ambient_color: RGBOrA255 = (64, 64, 64), ): """Draw the lights Args: position: Position offset (scrolling) target: The window or framebuffer we want to render to (default is window) ambient_color: The ambient light color """ if target is None: target = self.window # Re-build light data if needed if self._rebuild and len(self._lights) > 0: data: list[float] = [] for light in self._lights: data.extend(light.position) data.append(light.radius) data.append(light._attenuation) data.extend(light._color) while self._buffer.size < len(data) * self._stride: self._buffer.orphan(double=True) self._buffer.write(data=array("f", data)) self._rebuild = False # Render to light buffer self._light_buffer.use() self._light_buffer.clear() if len(self._lights) > 0: self._light_program["offset"] = position self.ctx.enable(self.ctx.BLEND) self.ctx.blend_func = self.ctx.BLEND_ADDITIVE self._vao.render( self._light_program, mode=self.ctx.TRIANGLE_STRIP, instances=len(self._lights) ) self.ctx.blend_func = self.ctx.BLEND_DEFAULT # Combine pass target.use() self._combine_program["diffuse_buffer"] = 0 self._combine_program["light_buffer"] = 1 self._combine_program["ambient"] = ambient_color[:3] self._fbo.color_attachments[0].use(0) self._light_buffer.color_attachments[0].use(1) self._quad_fs.render(self._combine_program)