""" This class allows the creation and management of lights in the scene. """
from typing import Union, Optional
import numpy as np
import bpy
from mathutils import Color
from blenderproc.python.types.EntityUtility import Entity
from blenderproc.python.utility.Utility import Utility, KeyFrame
[docs]
class Light(Entity):
"""
This class allows the creation and management of lights in the scene.
However, we advise to use emissive materials on objects to light a scene as these produce more realistic light
scenarios as the lighting does not directly start from a small point in space.
"""
def __init__(self, light_type: str = "POINT", name: str = "light", blender_obj: Optional[bpy.types.Object] = None):
"""
Constructs a new light if no blender_obj is given, else the params type and name are used to construct a new
light.
:param light_type: The initial type of light, can be one of [POINT, SUN, SPOT, AREA].
:param name: The name of the new light
:param blender_obj: A bpy.types.Light, this is then used instead of the type and name.
"""
if blender_obj is None:
# this creates a light object and sets is as the used entity inside the super class
light_data = bpy.data.lights.new(name=name, type=light_type)
light_obj = bpy.data.objects.new(name=name, object_data=light_data)
bpy.context.collection.objects.link(light_obj)
super().__init__(light_obj)
self.set_radius(0.25)
else:
super().__init__(blender_obj)
[docs]
def set_energy(self, energy: float, frame: Optional[int] = None):
""" Sets the energy of the light.
:param energy: The energy to set. If the type is SUN this value is interpreted as Watt per square meter,
otherwise it is interpreted as Watt.
:param frame: The frame number which the value should be set to. If None is given, the current
frame number is used.
"""
self.blender_obj.data.energy = energy
Utility.insert_keyframe(self.blender_obj.data, "energy", frame)
[docs]
def set_radius(self, radius: float, frame: Optional[int] = None):
""" Sets the radius / shadow_soft_size of the light.
:param radius: Light size for ray shadow sampling (Raytraced shadows).
:param frame: The frame number which the value should be set to. If None is given, the current
frame number is used.
"""
self.blender_obj.data.shadow_soft_size = radius
Utility.insert_keyframe(self.blender_obj.data, "shadow_soft_size", frame)
[docs]
def set_color(self, color: Union[list, Color], frame: Optional[int] = None):
""" Sets the color of the light.
:param color: The rgb color to set.
:param frame: The frame number which the value should be set to. If None is given, the current
frame number is used.
"""
self.blender_obj.data.color = color
Utility.insert_keyframe(self.blender_obj.data, "color", frame)
[docs]
def set_distance(self, distance: float, frame: Optional[int] = None):
""" Sets the falloff distance of the light = point where light is half the original intensity.
:param distance: The falloff distance to set.
:param frame: The frame number which the value should be set to. If None is given, the current
frame number is used.
"""
self.blender_obj.data.distance = distance
Utility.insert_keyframe(self.blender_obj.data, "distance", frame)
[docs]
def set_type(self, light_type: str, frame: Optional[int] = None):
""" Sets the type of the light.
:param light_type: The type to set, can be one of [POINT, SUN, SPOT, AREA].
:param frame: The frame number which the value should be set to. If None is given, the current
frame number is used.
"""
self.blender_obj.data.type = light_type
Utility.insert_keyframe(self.blender_obj.data, "type", frame)
[docs]
def setup_as_projector(self, pattern: np.ndarray, frame: Optional[int] = None):
r""" Sets a spotlight source as projector of a pattern image. Sets location and angle of projector to current
camera. Adjusts scale of pattern image to fit field-of-view of camera:
:math:`(0.5 + \frac{X}{Z \cdot F}, 0.5 + \frac{X}{Z \cdot F \cdot r}, 0)`
where $F$ is focal length and $r$ aspect ratio.
WARNING: This should be done after the camera parameters are set!
:param pattern: pattern image to be projected onto scene as np.ndarray.
:param frame: The frame number which the value should be set to. If None is given, the current
frame number is used.
"""
cam_ob = bpy.context.scene.camera
fov = cam_ob.data.angle # field of view of current camera in radians
focal_length = 2 * np.tan(fov / 2)
# Image aspect ratio = height / width
aspect_ratio = bpy.context.scene.render.resolution_y / bpy.context.scene.render.resolution_x
# Set location of light source to camera -- COPY TRANSFORMS
self.blender_obj.constraints.new('COPY_TRANSFORMS')
self.blender_obj.constraints['Copy Transforms'].target = cam_ob
# Setup nodes for projecting image
self.blender_obj.data.use_nodes = True
self.blender_obj.data.shadow_soft_size = 0
self.blender_obj.data.spot_size = 3.14159 # 180deg in rad
self.blender_obj.data.cycles.cast_shadow = False
nodes = self.blender_obj.data.node_tree.nodes
links = self.blender_obj.data.node_tree.links
node_ox = nodes.get('Emission')
image_data = bpy.data.images.new('pattern', width=pattern.shape[1], height=pattern.shape[0], alpha=True)
if pattern.dtype == np.uint8:
pattern = pattern / 255.0 # manual cast to range [0,1] to avoid integer casting issues below
image_data.pixels = pattern.ravel()
# Set Up Nodes
node_pattern = nodes.new(type="ShaderNodeTexImage") # Texture Image
node_pattern.label = 'Texture Image'
node_pattern.image = bpy.data.images['pattern']
node_pattern.extension = 'CLIP'
node_coord = nodes.new(type="ShaderNodeTexCoord") # Texture Coordinate
node_coord.label = 'Texture Coordinate'
f_value = nodes.new(type="ShaderNodeValue")
f_value.label = 'Focal Length'
f_value.outputs[0].default_value = focal_length
fr_value = nodes.new(type="ShaderNodeValue")
fr_value.label = 'Focal Length * Ratio'
fr_value.outputs[0].default_value = focal_length * aspect_ratio
divide1 = nodes.new(type="ShaderNodeMath")
divide1.label = 'X / ZF'
divide1.operation = 'DIVIDE'
divide2 = nodes.new(type="ShaderNodeMath")
divide2.label = 'Y / ZFr'
divide2.operation = 'DIVIDE'
multiply1 = nodes.new(type="ShaderNodeMath")
multiply1.label = 'Z * F'
multiply1.operation = 'MULTIPLY'
multiply2 = nodes.new(type="ShaderNodeMath")
multiply2.label = 'Z * Fr'
multiply2.operation = 'MULTIPLY'
center_image = nodes.new(type="ShaderNodeVectorMath")
center_image.operation = 'ADD'
center_image.label = 'Offset'
center_image.inputs[1].default_value[0] = 0.5
center_image.inputs[1].default_value[1] = 0.5
xyz_components = nodes.new(type="ShaderNodeSeparateXYZ")
combine_xyz = nodes.new(type="ShaderNodeCombineXYZ")
# Set Up Links
links.new(node_pattern.outputs["Color"], node_ox.inputs["Color"]) # Link Image Texture to Emission
links.new(node_coord.outputs["Normal"], xyz_components.inputs["Vector"])
# ZF
links.new(f_value.outputs[0], multiply1.inputs[1])
links.new(xyz_components.outputs["Z"], multiply1.inputs[0])
# ZFr
links.new(fr_value.outputs[0], multiply2.inputs[1])
links.new(xyz_components.outputs["Z"], multiply2.inputs[0])
# X / ZF
links.new(xyz_components.outputs["X"], divide1.inputs[0])
links.new(multiply1.outputs[0], divide1.inputs[1])
# Y / ZFr
links.new(xyz_components.outputs["Y"], divide2.inputs[0])
links.new(multiply2.outputs[0], divide2.inputs[1])
# Combine (X/ZF, Y/ZFr, 0)
links.new(divide1.outputs[0], combine_xyz.inputs["X"])
links.new(divide2.outputs[0], combine_xyz.inputs["Y"])
# Center image by offset
links.new(combine_xyz.outputs["Vector"], center_image.inputs[0])
# Link Mapping to Image Texture
links.new(center_image.outputs["Vector"], node_pattern.inputs["Vector"])
Utility.insert_keyframe(self.blender_obj.data, "use_projector", frame)
[docs]
def get_energy(self, frame: Optional[int] = None) -> float:
""" Returns the energy of the light.
:param frame: The frame number which the value should be set to. If None is given, the current
frame number is used.
:return: The energy at the specified frame.
"""
with KeyFrame(frame):
return self.blender_obj.data.energy
[docs]
def get_radius(self, frame: Optional[int] = None) -> float:
""" Returns the radius / shadow_soft_size of the light.
:param frame: The frame number which the value should be set to. If None is given, the current
frame number is used.
:return: The radius at the specified frame.
"""
with KeyFrame(frame):
return self.blender_obj.data.shadow_soft_size
[docs]
def get_color(self, frame: Optional[int] = None) -> Color:
""" Returns the RGB color of the light.
:param frame: The frame number which the value should be set to. If None is given, the current
frame number is used.
:return: The color at the specified frame.
"""
with KeyFrame(frame):
return self.blender_obj.data.color
[docs]
def get_distance(self, frame: Optional[int] = None) -> float:
""" Returns the falloff distance of the light (point where light is half the original intensity).
:param frame: The frame number which the value should be set to. If None is given, the current
frame number is used.
:return: The falloff distance at the specified frame.
"""
with KeyFrame(frame):
return self.blender_obj.data.distance
[docs]
def get_type(self, frame: Optional[int] = None) -> str:
""" Returns the type of the light.
:param frame: The frame number which the value should be set to. If None is given, the current
frame number is used.
:return: The type at the specified frame.
"""
with KeyFrame(frame):
return self.blender_obj.data.type