import bpy
from .bsyn_object import BsynObject
from .nodes import tidy_tree
from ..utils import io
from ..utils import version
def _new_shader_node(node_tree, node_type):
shader_node = node_tree.nodes.new(type=node_type)
if node_type == "ShaderNodeBsdfPrincipled":
spec_key = "Specular IOR Level" if version.is_version_plus(4) else "Specular"
shader_node.inputs[spec_key].default_value = 0
return shader_node
[docs]
class Material(BsynObject):
"""BlenderSynth Material class. Will always be a node material."""
def __init__(
self, name="NewMaterial", shader_type="ShaderNodeBsdfPrincipled", mat=None
):
if mat is None:
mat = bpy.data.materials.new(name)
mat.use_nodes = True
mat.node_tree.nodes.clear()
self.shader = _new_shader_node(mat.node_tree, shader_type)
self.output = mat.node_tree.nodes.new(type="ShaderNodeOutputMaterial")
mat.node_tree.links.new(
self.shader.outputs[0], self.output.inputs["Surface"]
)
else:
self.shader = None
self.output = mat.node_tree.nodes["Material Output"]
mat.use_nodes = True
self._object = mat
self.node_tree = mat.node_tree
self.nodes = self.node_tree.nodes
self.links = self.node_tree.links
self._image_nodes = {} # dict of shader input socket : image node
# set-up scaling
self._add_scaling_node()
self._scale = 1
tidy_tree(self.node_tree)
[docs]
@classmethod
def from_image(
cls,
image_loc,
name="ImageMaterials",
shader_type="ShaderNodeBsdfPrincipled",
use_global_scale=True,
) -> "Material":
"""
Create a new material from an image texture.
:param image_loc: Location of the image
:param name: Name of the material
:param shader_type: Shader type
:param use_global_scale: Flag to scale the image texture node by the global scale
:return: New Material instance
"""
mat = cls(name, shader_type=shader_type) # new instance
mat.add_source(
image_loc, input_name="Base Color", use_global_scale=use_global_scale
)
return mat
[docs]
def add_source(
self,
image_loc: str,
input_name: str = "Base Color",
use_global_scale: bool = True,
allow_duplicate: bool = False,
):
"""
Add an image texture node to the material node tree, and connect it to the specified input of the Shader.
:param image_loc: Image location
:param input_name: Input socket name of the Shader to connect the image texture node to
:param use_global_scale: Flag to scale the image texture node by the global scale
:param allow_duplicate: Flag to allow duplicate image texture nodes (by default, a node to the same input will overwrite any previous)
:return:
"""
# Get the nodes in the material node tree
nodes = self.obj.node_tree.nodes
links = self.obj.node_tree.links
# Find the Principled BSDF
principled = next(
(node for node in nodes if node.type == "BSDF_PRINCIPLED"), None
)
if not principled:
raise ValueError(
"add_source requires a Principled BSDF node in the material node tree"
)
if input_name in self._image_nodes and not allow_duplicate:
tex_image_node = self._image_nodes[input_name]
else:
tex_image_node = nodes.new("ShaderNodeTexImage")
tex_image_node.image = io.load_image(image_loc)
self._image_nodes[input_name] = tex_image_node
if input_name == "Displacement":
# Connect to the 'Displacement' socket of the output node
links.new(
tex_image_node.outputs["Color"], self.output.inputs["Displacement"]
)
else:
# Connect the image texture node to the specified input of the Principled BSDF
links.new(tex_image_node.outputs["Color"], principled.inputs[input_name])
if use_global_scale:
links.new(self._texture_scaling_socket, tex_image_node.inputs["Vector"])
if "color" not in input_name.lower():
tex_image_node.image.colorspace_settings.name = "Non-Color"
tidy_tree(self.node_tree)
@classmethod
def from_blender_material(cls, blender_mat: bpy.types.Material):
if not isinstance(blender_mat, bpy.types.Material):
raise ValueError("Invalid Blender material")
# Create a new Material instance
mat = cls(blender_mat.name, mat=blender_mat)
# Find the shader and output nodes in the copied node tree
mat.shader = next(
(node for node in mat.nodes if node.type == "BSDF_PRINCIPLED"), None
)
mat.output = next(
(node for node in mat.nodes if node.type == "OUTPUT_MATERIAL"), None
)
return mat
def _add_scaling_node(self):
"""Add scaling node to the material node tree"""
self.scale_node = self.nodes.new("ShaderNodeValue")
tex_coord_node = self.nodes.new("ShaderNodeTexCoord")
mapping_node = self.nodes.new("ShaderNodeMapping")
self.links.new(self.scale_node.outputs[0], mapping_node.inputs["Scale"])
self.links.new(tex_coord_node.outputs["UV"], mapping_node.inputs["Vector"])
self._texture_scaling_socket = mapping_node.outputs["Vector"]
self.scale = 1 # set default scale
[docs]
def set_bdsf_property(self, key: str, value: float):
"""Set the property of the BSDF node"""
if self.shader.type != "BSDF_PRINCIPLED":
raise ValueError("Shader is not a Principled BSDF")
if key not in self.shader.inputs:
raise ValueError(f"Invalid BDSF shader property `{key}`")
self.shader.inputs[key].default_value = value
@property
def scale(self):
return self._scale
@scale.setter
def scale(self, value):
self._scale = value
self.scale_node.outputs[0].default_value = value
@property
def name(self):
return self.obj.name