Source code for serge.blocks.lighting

"""Implements a lighting approach"""

import pygame
import math
import random
import serge.actor
import serge.common
import serge.engine
import serge.visual
import serge.geometry
import serge.blocks.utils
from serge.simplevecs import Vec2d
from serge.geometry import Line, Point


C_LOOP_NONE = 'none'
C_LOOP_PING_PONG = 'ping-pong'
C_LOOP_LOOP = 'loop'


[docs]class LightField(serge.actor.Actor): """Represents a field of light that will be drawn on top of the screen""" blend_mode = pygame.BLEND_RGBA_MULT particle_blend_mode = 0 fade_lights = True is_baked = False def __init__(self, tag, name, field_size_ratio, fade_amount=0.9): """Initialise the light field""" super(LightField, self).__init__(tag, name) screen_width, screen_height = serge.engine.CurrentEngine().getRenderer().getScreenSize() width, height = field_size_ratio * screen_width, field_size_ratio * screen_height self.visual = serge.visual.SurfaceDrawing(width, height) # self.fade_amount = fade_amount self.screen_width, self.screen_height = serge.engine.CurrentEngine().getRenderer().getScreenSize() # self.polygons = [] self.addPolygon(serge.geometry.Polygon( [[0, 0], [screen_width, 0], [screen_width, screen_height], [0, screen_height], [0, 0]] )) self.lights = [] self.visual.getSurface().fill((0, 0, 0)) self.ambient_light = None self.baked_light_field = None
[docs] def addPolygon(self, polygon): """Set the lines used for geometry""" self.polygons.append(polygon)
[docs] def addLight(self, light): """Add some lights to the field""" self.lights.append(light)
[docs] def addLights(self, lights): """Add a number of lights to the screen""" for light in lights: self.addLight(light)
[docs] def setAmbientLight(self, light): """Set an ambient light""" self.ambient_light = light
[docs] def clearAmbientLight(self): """Clear the ambient light""" self.ambient_light = None
[docs] def setBakedLightField(self, baked_lights): """Set the visual field of baked lights""" scaled_surface = pygame.transform.smoothscale(baked_lights, (self.width, self.height)) self.baked_light_field = scaled_surface
[docs] def clearBakedLightField(self): """Clear the baked lights""" self.baked_light_field = None
[docs] def addedToWorld(self, world): """The actor was added to the world""" super(LightField, self).addedToWorld(world) # self.world = world self.time = 0
[docs] def updateActor(self, interval, world): """Update the actor""" if not self.is_baked: self.updateLightField()
[docs] def updateLightField(self): """Update the overlal light field""" # # Light fade if self.fade_lights: self.visual.getSurface().fill(( (255 * self.fade_amount), (255 * self.fade_amount), (255 * self.fade_amount)), special_flags=pygame.BLEND_RGB_MULT ) else: self.visual.getSurface().fill((0, 0, 0)) # if self.ambient_light: self.visual.getSurface().fill( self.ambient_light, special_flags=pygame.BLEND_RGB_ADD, ) # # Randomly update lights for light in self.lights: light.drawTo(self) # if self.baked_light_field: self.visual.getSurface().blit( self.baked_light_field, (0, 0), special_flags=pygame.BLEND_RGBA_ADD, )
[docs] def renderTo(self, renderer, interval): """Render to a surface""" surface = renderer.getLayer(self.layer).getSurface() enlarged = pygame.transform.smoothscale(self.visual.getSurface(), (self.screen_width, self.screen_height)) surface.blit(enlarged, (0, 0), special_flags=self.blend_mode)
[docs] def addLightParticle(self, particle_sprite, x, y): """Add a light particle at screen coordinates x and y""" width, height = particle_sprite.get_size() px = x * self.width / self.screen_width - width / 2 py = y * self.height / self.screen_height - height / 2 self.visual.getSurface().blit(particle_sprite, (px, py), special_flags=self.particle_blend_mode)
[docs] def drawLines(self): """Draw the lines on the screen""" self.log.info('Drawing all the object lines to the screen') # world = serge.engine.CurrentEngine().getCurrentWorld() self.lines_display = serge.blocks.utils.addVisualActorToWorld( world, 'spike', 'lines-display', serge.visual.SurfaceDrawing(self.screen_width, self.screen_height), 'debug', (self.screen_width / 2, self.screen_height / 2), ) for item in self.polygons: for line in item.lines: pygame.draw.line( self.lines_display.visual.getSurface(), (0, 255, 0), (line.x1, line.y1), (line.x2, line.y2), )
[docs] def traceRay(self, ray, particle, distance): """Trace a ray to its destination""" lx, ly = ray.x1, ray.y1 # intersections = [] for polygon in self.polygons: for line in polygon.getLines(): intersection = ray.getIntersectingRay(line) if intersection: intersections.append((intersection.length, intersection, line)) # # Find the closest intersection if intersections: intersections.sort() end = intersections[0][1].getEndPoint() self.drawLightRay(Vec2d(lx, ly), Vec2d(end.x, end.y), particle, distance)
[docs] def drawLightRay(self, start, end, light_particle, distance_range): """Draw a light ray""" delta = end - start number_steps = int(min(delta.length, distance_range) / light_particle.step_size) pos = start step = delta.normalized() * light_particle.step_size # for i in range(number_steps): fraction = float(i) * light_particle.step_size / distance_range self.addLightParticle(light_particle.getSurfaceForDistanceFraction(fraction), pos.x, pos.y) pos += step
[docs]class Light(serge.common.Loggable): """A light that can be added to the display""" def __init__(self): """Initialise the light""" self.addLogger()
[docs] def drawTo(self, light_field): """Draw this light to a light field""" raise NotImplementedError
[docs]class DirectionalLight(Light): """Represents a light that shines in a direction""" def __init__(self, x, y, intensity, distance, angle, beam_width, particle, randomize_rays=False): """Initialise the particle""" super(DirectionalLight, self).__init__() # self.x = x self.y = y self.particle = particle self.intensity = intensity self.distance = distance self.angle = angle self.beam_width = beam_width self.randomize_rays = randomize_rays self.screen_width, self.screen_height = serge.engine.CurrentEngine().getRenderer().getScreenSize() self.particle.calculateParticleSteps(self.intensity)
[docs] def drawTo(self, light_field): """Draw this light to a light field""" min_angle, max_angle = self.angle - self.beam_width / 2.0, self.angle + self.beam_width / 2.0 rays_per_frame = max(1, int(float(self.beam_width) / self.particle.degrees_per_ray)) # for i in range(rays_per_frame): if self.randomize_rays: direction = random.uniform(min_angle, max_angle) else: direction = float(min_angle + (max_angle - min_angle) * i / rays_per_frame) light_ray = Line.fromRadial(self.x, self.y, direction, 10 * self.screen_width) light_field.traceRay(light_ray, self.particle, self.distance)
[docs] def setAngle(self, angle): """Set the angle of the light""" self.angle = angle
[docs] def setIntensity(self, intensity): """Set the light intensity""" self.intensity = intensity self.particle.calculateParticleSteps(intensity)
[docs]class Pointlight(Light): """A point of light""" def __init__(self, x, y, particle, intensity=1.0): """Initialise the light""" super(Pointlight, self).__init__() # self.x = x self.y = y self.particle = particle self.intensity = intensity
[docs] def drawTo(self, light_field): """Draw to the light field""" surface = self.particle.getSurfaceForDistanceFraction(0) width, height = surface.get_size() light_field.addLightParticle( surface, self.x - width / 2, self.y - height / 2)
[docs] def setIntensity(self, intensity): """Set the intensity""" self.intensity = intensity self.particle.calculateParticleSteps(intensity)
[docs]class LightParticle(serge.visual.Drawing): """A particle of light to draw on the screen""" def __init__(self, base_sprite_name, size_range=(1.0, 1.0), alpha_range=(1.0, 0.0), max_steps=15, step_size=20, degrees_per_ray=10): """Initialise the particle""" super(LightParticle, self).__init__() # self.size_range = size_range self.alpha_range = alpha_range self.max_steps = max_steps self.step_size = step_size self.degrees_per_ray = degrees_per_ray # self.base_sprite = serge.visual.Sprites.getItem(base_sprite_name) self.calculateParticleSteps()
[docs] def calculateParticleSteps(self, intensity=1.0): """Calculate the images for the particle steps""" # # Calculate images self.images = [] for i in range(self.max_steps): factor = float(i) / (self.max_steps - 1) size = factor * (self.size_range[1] - self.size_range[0]) + self.size_range[0] fade = intensity * max(0.0, min(1.0, factor * (self.alpha_range[1] - self.alpha_range[0]) + self.alpha_range[0])) self.base_sprite.setScale(size) surface = self.base_sprite.getSurface().copy() surface.fill((255, 255, 255, int(fade * 255)), special_flags=pygame.BLEND_RGBA_MULT) self.images.append(surface)
[docs] def getSurfaceForDistanceFraction(self, fraction): """Return the surface for the given step""" step = int(fraction * (len(self.images) - 1)) return self.images[step]
[docs]class GlowingPointLight(Pointlight): """A point light that varies its glow level""" def __init__(self, x, y, particle, intensity=1.0, min_intensity=0.0, max_intensity=1.0): """Initialise the light""" super(GlowingPointLight, self).__init__(x, y, particle, intensity) # self.min_intensity = min_intensity self.max_intensity = max_intensity
[docs] def updateLight(self, interval): """Update the light""" new_intensity = self.intensity * 0.9 + 0.1 * random.uniform(self.min_intensity, self.max_intensity) self.setIntensity(new_intensity)
[docs]class LightDolly(serge.actor.Actor): """A dolly to move a light along a path""" def __init__(self, name, light, path, path_traversal_time, spring_strength, damping, mass=1.0, loop=C_LOOP_NONE, face_velocity=False): """Initialise the dolly""" super(LightDolly, self).__init__('dolly', name) # self.path = path self.path_traversal_time = path_traversal_time self.spring_strength = spring_strength self.light = light self.damping = damping self.mass = mass self.loop = loop self.face_velocity = face_velocity # self.velocity = Vec2d(0, 0) self.elapsed_time = 0.0
[docs] def updateActor(self, interval, world): """Update the path motion""" self.elapsed_time += interval / 1000.0 if self.loop == C_LOOP_NONE: path_time = self.elapsed_time elif self.loop == C_LOOP_LOOP: path_time = self.elapsed_time % self.path_traversal_time elif self.loop == C_LOOP_PING_PONG: path_time = self.elapsed_time % (2 * self.path_traversal_time) if path_time >= self.path_traversal_time: path_time = self.path_traversal_time * 2.0 - path_time else: raise ValueError('Unknown loop type: %s' % self.loop) # end_point = self.path.getPointAtFraction(min(1.0, path_time / self.path_traversal_time)) # offset = Vec2d(*end_point) - Vec2d(self.light.x, self.light.y) force = self.spring_strength * offset - self.damping * self.velocity acceleration = force / self.mass # self.velocity += acceleration * interval / 1000.0 self.light.x += self.velocity.x * interval / 1000.0 self.light.y += self.velocity.y * interval / 1000.0 # # Face the way we are going if self.face_velocity: self.light.setAngle(math.degrees(self.velocity.angle))