Source code for blendersynth.blender.aov

"""Shader AOV manager"""

import bpy
from .nodes import tidy_tree, DeformedGeneratedTextureCoordinates, tree_add_socket
from ..utils import types
from ..utils import version
from typing import Union

ref_frames = ["CAMERA", "WORLD", "OBJECT"]

# Acceptable socket types for AOV colors & nodes
_socket_color_types = (bpy.types.NodeSocketVector, bpy.types.NodeSocketColor)
_socket_value_types = (bpy.types.NodeSocketFloat, bpy.types.NodeSocketInt)


# docs-special-members: __init__
# no-inherited-members


[docs] class AOV: """A generic Arbitrary Output Value. An AOV is a float or color value per-point that is calculated at the rendering stage, and can be output in a compositor to form an image. See `Blender docs <https://docs.blender.org/manual/en/latest/render/shader_nodes/output/aov.html>`_ for more info. """
[docs] def __init__(self, *, name=None, **kwargs): if name is None: name = self.__class__.__name__ self.name = name self._aov = None self._add_to_view_layer()
def _add_to_view_layer(self, view_layer=None): """Add aov to view layer if not already there""" if view_layer is None: view_layer = bpy.context.view_layer for aov in view_layer.aovs: if aov.name == self.name: raise ValueError("AOV `{}` already exists - each AOV must have a unique name".format(self.name)) aov = view_layer.aovs.add() aov.name = self.name self._aov = aov
[docs] def add_to_shader(self, shader_node_tree): """Add AOV to shader node tree""" # add_to_shader must return output socket to connect to node out_socket = self._add_to_shader(shader_node_tree) # check node socket is correct type AOV_TYPE = None if isinstance(out_socket, _socket_color_types): AOV_TYPE = "COLOR" elif isinstance(out_socket, _socket_value_types): AOV_TYPE = "VALUE" else: raise ValueError( f"Output of _add_to_layer must be in {_socket_color_types} if Color or {_socket_value_types} if value. Got: `{type(out_socket)}`" ) shader_aov_node = shader_node_tree.nodes.new("ShaderNodeOutputAOV") # https://projects.blender.org/blender/blender/commit/deb332601c2a5c5c41df21543c08ac1381ca4a0a if version.is_version_plus(4.2): shader_aov_node.aov_name = self.name else: shader_aov_node.name = self.name shader_node_tree.links.new(out_socket, shader_aov_node.inputs[AOV_TYPE.title()]) self._aov.type = AOV_TYPE tidy_tree(shader_node_tree)
def _add_to_shader(self, shader_node_tree) -> bpy.types.NodeSocket: raise NotImplementedError
[docs] def update(self): """Some AOVs need an update before rendering (to change certain node properties)""" return
def __str__(self): return self.name
[docs] class NormalsAOV(AOV):
[docs] def __init__( self, *, name: str = None, ref_frame: str = "CAMERA", order: str = "XYZ", polarity: Union[tuple, list] = (1, 1, 1), ): """Given a shader node tree, add surface normals as output. :param name: Name of AOV to add :param ref_frame: Reference frame to use for normals :param order: Order of components in RGB (default: XYZ) :param polarity: Polarity of XYZ (1 or -1) """ super().__init__(name=name) assert ref_frame in ref_frames, f"ref_frame must be one of {ref_frames}" self.ref_frame = ref_frame self.order = order self.polarity = polarity
def _add_to_shader(self, shader_node_tree): geom_node = shader_node_tree.nodes.new("ShaderNodeNewGeometry") vec_transform = shader_node_tree.nodes.new("ShaderNodeVectorTransform") map_range_node = shader_node_tree.nodes.new("ShaderNodeMapRange") vec_transform.vector_type = "NORMAL" vec_transform.convert_to = self.ref_frame # Set up mapping - with polarity map_range_node.data_type = "FLOAT_VECTOR" for i in range(3): map_range_node.inputs[7].default_value[i] = -self.polarity[i] map_range_node.inputs[8].default_value[i] = self.polarity[i] # Set up ordering sep_xyz_node = shader_node_tree.nodes.new("ShaderNodeSeparateXYZ") comb_xyz_node = shader_node_tree.nodes.new("ShaderNodeCombineXYZ") for i in range(3): shader_node_tree.links.new( sep_xyz_node.outputs[self.order[i]], comb_xyz_node.inputs["XYZ"[i]] ) # Make necessary connections for shader graph shader_node_tree.links.new( geom_node.outputs["True Normal"], vec_transform.inputs["Vector"] ) shader_node_tree.links.new( vec_transform.outputs["Vector"], map_range_node.inputs["Vector"] ) shader_node_tree.links.new( map_range_node.outputs["Vector"], sep_xyz_node.inputs["Vector"] ) return comb_xyz_node.outputs["Vector"]
[docs] class GeneratedAOV(AOV): """'Generated Texture Coordinates'. These are coordinates normalized to 0-1 for the object's undeformed bounding box, not taking into account deformation (pose, modifiers). See `Blender docs <https://docs.blender.org/manual/en/latest/render/shader_nodes/input/texture_coordinate.html>`_ for more info. """ def _add_to_shader(self, shader_node_tree): texcon_node = shader_node_tree.nodes.new("ShaderNodeTexCoord") return texcon_node.outputs["Generated"]
[docs] class DisplacementGeneratedAOV(AOV): """In the same co-ordinate space as :class:`GeneratedAOV`, give the displacement vector for each point under modifiers (e.g. pose). By default, this deformation is mapped from the range [-1 to 1] to [0 to 1], with 0.5 representing no deformation. Any values outside of this range are clamped. This can be modified through the input kwargs vmin and vmax. """
[docs] def __init__( self, *, name: str = None, mesh: types.Mesh = None, bbox_min: types.VectorLike = None, bbox_max: types.VectorLike = None, vmin: float = -1, vmax: float = 1, ): """Create AOV for displacement under modifiers. :param name: Name of AOV :param mesh: Mesh to calculate bounds for. Will be used to find bbox_min and bbox_max if given :param bbox_min: Minimum of bounding box. If not given, calculated from Mesh :param bbox_max: Maximum of bounding box. If not given, calculated from Mesh :param vmin: Minimum deformation to map to 0 :param vmax: Maximum value to map to 1 """ super().__init__(name=name) self.mesh = mesh self.bbox_min = bbox_min self.bbox_max = bbox_max self.vmin = vmin self.vmax = vmax assert mesh is not None or ( bbox_min is not None and bbox_max is not None ), "Either mesh or bbox_min and bbox_max must be given for DisplacementGeneratedAOV"
def _add_to_shader(self, shader_node_tree): self.deformed_coords = DeformedGeneratedTextureCoordinates( node_tree=shader_node_tree, mesh=self.mesh, bbox_min=self.bbox_min, bbox_max=self.bbox_max, ) self.generated_coords = shader_node_tree.nodes.new("ShaderNodeTexCoord") # subtract one from the other sub_node = shader_node_tree.nodes.new("ShaderNodeVectorMath") sub_node.operation = "SUBTRACT" # map result to range 0-1 map_range_node = shader_node_tree.nodes.new("ShaderNodeMapRange") map_range_node.data_type = "FLOAT_VECTOR" map_range_node.inputs[7].default_value = [self.vmin] * 3 map_range_node.inputs[8].default_value = [self.vmax] * 3 # link up nodes shader_node_tree.links.new( self.deformed_coords.outputs["Vector"], sub_node.inputs[0] ) shader_node_tree.links.new( self.generated_coords.outputs["Generated"], sub_node.inputs[1] ) shader_node_tree.links.new( sub_node.outputs["Vector"], map_range_node.inputs["Vector"] ) tidy_tree(shader_node_tree) return map_range_node.outputs["Vector"]
[docs] def set_bounds( self, mesh: types.Mesh = None, bbox_min: types.VectorLike = None, bbox_max: types.VectorLike = None, ): """Set bounds for DeformedGeneratedTextureCoordinates node group :param mesh: Mesh to calculate bounds for. Will be used to find bbox_min and bbox_max if given :param bbox_min: Minimum of bounding box. If not given, calculated from Mesh :param bbox_max: Maximum of bounding box. If not given, calculated from Mesh """ self.mesh = mesh self.bbox_min = bbox_min self.bbox_max = bbox_max self.node_group.register_bounds(mesh, bbox_min, bbox_max)
[docs] class UVAOV(AOV): """UV coordinates. See `Blender docs <https://docs.blender.org/manual/en/latest/editors/uv/index.html#editors-uv-index>`_ for more info.""" def _add_to_shader(self, shader_node_tree): texcon_node = shader_node_tree.nodes.new("ShaderNodeTexCoord") return texcon_node.outputs["UV"]
[docs] class ValueAOV(AOV): """A generic AOV that outputs a single value"""
[docs] def __init__(self, value: float = 0, name=None, **kwargs): """ :param value: Value to output :param name: Name of AOV :param kwargs: Additional kwargs to pass to :class:`AOV` """ super().__init__(name=name, **kwargs) self._value = 0 self.value_node = None self.value = value
@property def value(self): return self._value @value.setter def value(self, value): self._value = value if self.value_node is not None: self.value_node.outputs["Value"].default_value = value def _add_to_shader(self, shader_node_tree): self.value_node = shader_node_tree.nodes.new("ShaderNodeValue") self.value = self._value return self.value_node.outputs["Value"]
[docs] class AttrAOV(AOV): """Take an object attribute, and output it as an AOV""" attribute_type = None attribute_name = None def _add_to_shader(self, shader_node_tree): attr_node = shader_node_tree.nodes.new("ShaderNodeAttribute") attr_node.attribute_type = self.attribute_type attr_node.attribute_name = self.attribute_name return attr_node.outputs["Fac"]
[docs] class InstanceIDAOV(AttrAOV): """Instance ID - given to each object on creation. Output is an integer corresponding to the object's instance ID (0-indexed) """ attribute_type = "OBJECT" attribute_name = "instance_id"
[docs] class ClassIDAOV(AttrAOV): """Class ID - given to each object on creation. Output is an integer corresponding to the object's class ID (0-indexed) Class IDs can be manually set either when creating a Mesh, or by using the Mesh's :meth:`~blendersynth.blender.mesh.Mesh.set_class_id` method. If not set, will default to a different index from each primitive. """ attribute_type = "OBJECT" attribute_name = "class_id"
[docs] class AttrRGBAOV(AOV): """ For a given numerical attribute, outputs a color corresponding to the attribute's value. Object with attribute value `i` has HSV color `(i/N, 1, 1)`. `N` can be a property of the object to update. Runs :meth:`~update()` method to change the value of `N`, which is called before rendering. """ attribute_type = None attribute_name = None
[docs] def __init__(self, *, name=None): super().__init__(name=name) # Create Int Index -> HSV as a node group, so the 'num_objects' parameter can be edited centrally self.group = bpy.data.node_groups.new(name="IdxToHue", type="ShaderNodeTree") tree_add_socket(self.group, "NodeSocketFloat", "Index", "INPUT") tree_add_socket(self.group, "NodeSocketColor", "Color", "OUTPUT") self.input_node = self.group.nodes.new("NodeGroupInput") self.output_node = self.group.nodes.new("NodeGroupOutput") self.div_node = div_node = self.group.nodes.new( "ShaderNodeMath" ) # Need to keep reference so can update at runtime div_node.operation = "DIVIDE" div_node.use_clamp = True div_node.inputs[1].default_value = self.N hsv_node = self.group.nodes.new("ShaderNodeHueSaturation") hsv_node.inputs["Saturation"].default_value = 1 hsv_node.inputs["Value"].default_value = 1 hsv_node.inputs["Color"].default_value = (1, 0, 0, 1) # Red self.group.links.new(self.input_node.outputs["Index"], div_node.inputs[0]) self.group.links.new(div_node.outputs["Value"], hsv_node.inputs["Hue"]) self.group.links.new( hsv_node.outputs["Color"], self.output_node.inputs["Color"] ) tidy_tree(self.group)
def _add_to_shader(self, shader_node_tree): attr_node = shader_node_tree.nodes.new("ShaderNodeAttribute") attr_node.attribute_type = self.attribute_type attr_node.attribute_name = self.attribute_name # Create a group node for the Int Index -> HSV conversion group_node = shader_node_tree.nodes.new("ShaderNodeGroup") group_node.node_tree = self.group shader_node_tree.links.new(attr_node.outputs["Fac"], group_node.inputs["Index"]) return group_node.outputs["Color"]
[docs] def update(self): self.div_node.inputs[1].default_value = self.N
@property def N(self): return 0
[docs] class InstanceRGBAOV(AttrRGBAOV): """ :class:`~InstanceIDAOV` as an :class:`~AttrRGBAOV` for visualisation. Updates `N` at render time by reading the scene property 'NUM_MESHES' """ attribute_type = "OBJECT" attribute_name = "instance_id" @property def N(self): """Total number of meshes in the scene""" return bpy.context.scene.get("NUM_MESHES", 0) + 1
[docs] class ClassRGBAOV(AttrRGBAOV): """ :class:`~ClassIDAOV` but as an :class:`~AttrRGBAOV` for visualisation. Updates `N` at render time by reading the scene property `MAX_CLASSES` """ attribute_type = "OBJECT" attribute_name = "class_id" @property def N(self): """Update the divisor node with the current number of classes""" return bpy.context.scene.get("MAX_CLASSES", 0) + 1