286 lines
9.6 KiB
Python
286 lines
9.6 KiB
Python
import bpy
|
|
import os
|
|
from typing import Tuple
|
|
|
|
|
|
bl_info = {
|
|
"name": "BToon",
|
|
"author": "Yuki Koyama",
|
|
"version": (0, 1),
|
|
"blender": (2, 83, 0),
|
|
"location": "View3D > Object > BToon Utilities",
|
|
"description": "Toon shading utilities",
|
|
"warning": "",
|
|
"support": "TESTING",
|
|
"wiki_url": "https://github.com/yuki-koyama/btoon",
|
|
"tracker_url": "https://github.com/yuki-koyama/btoon/issues",
|
|
"category": "Material"
|
|
}
|
|
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Copied from blender-cli-rendering:
|
|
# https://github.com/yuki-koyama/blender-cli-rendering
|
|
# ------------------------------------------------------------------------------
|
|
def clean_nodes(nodes: bpy.types.Nodes) -> None:
|
|
for node in nodes:
|
|
nodes.remove(node)
|
|
|
|
|
|
def add_material(name: str = "Material", use_nodes: bool = False, make_node_tree_empty: bool = False) -> bpy.types.Material:
|
|
'''
|
|
https://docs.blender.org/api/current/bpy.types.BlendDataMaterials.html
|
|
https://docs.blender.org/api/current/bpy.types.Material.html
|
|
'''
|
|
|
|
# TODO: Check whether the name is already used or not
|
|
|
|
material = bpy.data.materials.new(name)
|
|
material.use_nodes = use_nodes
|
|
|
|
if use_nodes and make_node_tree_empty:
|
|
clean_nodes(material.node_tree.nodes)
|
|
|
|
return material
|
|
|
|
|
|
# https://docs.blender.org/api/current/bpy.types.SolidifyModifier.html
|
|
def add_solidify_modifier(mesh_object: bpy.types.Object,
|
|
thickness: float = 0.01,
|
|
flip_normal: bool = False,
|
|
fill_rim: bool = True,
|
|
material_index_offset: int = 0,
|
|
shell_vertex_group: str = "",
|
|
rim_vertex_group: str = "") -> None:
|
|
|
|
modifier: bpy.types.SolidifyModifier = mesh_object.modifiers.new(name="Solidify", type='SOLIDIFY')
|
|
|
|
modifier.material_offset = material_index_offset
|
|
modifier.thickness = thickness
|
|
modifier.use_flip_normals = flip_normal
|
|
modifier.use_rim = fill_rim
|
|
|
|
# TODO: Check whether shell_vertex_group is either empty or defined
|
|
# TODO: Check whether rim_vertex_group is either empty or defined
|
|
|
|
modifier.shell_vertex_group = shell_vertex_group
|
|
modifier.rim_vertex_group = rim_vertex_group
|
|
|
|
|
|
def add_displace_modifier(mesh_object: bpy.types.Object,
|
|
texture_name: str,
|
|
vertex_group: str = "",
|
|
mid_level: float = 0.5,
|
|
strength: float = 1.0) -> None:
|
|
'''
|
|
https://docs.blender.org/api/current/bpy.types.DisplaceModifier.html
|
|
'''
|
|
|
|
modifier = mesh_object.modifiers.new(name="Displace", type='DISPLACE')
|
|
|
|
modifier.mid_level = mid_level
|
|
modifier.strength = strength
|
|
|
|
# TODO: Check whether texture_name is properly defined
|
|
modifier.texture = bpy.data.textures[texture_name]
|
|
|
|
# TODO: Check whether vertex_group is either empty or defined
|
|
modifier.vertex_group = vertex_group
|
|
|
|
|
|
# https://docs.blender.org/api/current/bpy.types.VertexGroups.html
|
|
# https://docs.blender.org/api/current/bpy.types.VertexGroup.html
|
|
def add_vertex_group(mesh_object: bpy.types.Object, name: str = "Group") -> bpy.types.VertexGroup:
|
|
|
|
# TODO: Check whether the object has a mesh data
|
|
# TODO: Check whether the object already has a vertex group with the specified name
|
|
|
|
vertex_group = mesh_object.vertex_groups.new(name=name)
|
|
|
|
return vertex_group
|
|
|
|
|
|
def build_emission_nodes(node_tree: bpy.types.NodeTree,
|
|
color: Tuple[float, float, float] = (0.0, 0.0, 0.0),
|
|
strength: float = 1.0) -> None:
|
|
'''
|
|
https://docs.blender.org/api/current/bpy.types.ShaderNodeEmission.html
|
|
'''
|
|
output_node = node_tree.nodes.new(type='ShaderNodeOutputMaterial')
|
|
emission_node = node_tree.nodes.new(type='ShaderNodeEmission')
|
|
|
|
output_node.location[0] += 100.0
|
|
emission_node.location[0] -= 100.0
|
|
|
|
emission_node.inputs["Color"].default_value = color + (1.0, )
|
|
emission_node.inputs["Strength"].default_value = strength
|
|
|
|
node_tree.links.new(emission_node.outputs['Emission'], output_node.inputs['Surface'])
|
|
|
|
|
|
def add_clouds_texture(name: str = "Clouds Texture",
|
|
size: float = 0.25,
|
|
depth: int = 2,
|
|
nabla: float = 0.025,
|
|
brightness: float = 1.0,
|
|
contrast: float = 1.0) -> bpy.types.CloudsTexture:
|
|
'''
|
|
https://docs.blender.org/api/current/bpy.types.BlendDataTextures.html
|
|
https://docs.blender.org/api/current/bpy.types.Texture.html
|
|
https://docs.blender.org/api/current/bpy.types.CloudsTexture.html
|
|
'''
|
|
|
|
# TODO: Check whether the name is already used or not
|
|
|
|
tex = bpy.data.textures.new(name, type='CLOUDS')
|
|
|
|
tex.noise_scale = size
|
|
tex.noise_depth = depth
|
|
tex.nabla = nabla
|
|
|
|
tex.intensity = brightness
|
|
tex.contrast = contrast
|
|
|
|
return tex
|
|
|
|
|
|
def append_material(blend_file_path: str, material_name: str) -> bool:
|
|
'''
|
|
https://docs.blender.org/api/current/bpy.types.BlendDataLibraries.html
|
|
'''
|
|
|
|
# Load the library file
|
|
with bpy.data.libraries.load(blend_file_path, link=False) as (data_from, data_to):
|
|
# Check whether the specified material exists in the blend file
|
|
if material_name in data_from.materials:
|
|
# Append the material and return True
|
|
data_to.materials = [material_name]
|
|
return True
|
|
else:
|
|
# If the material is not found, return False without doing anything
|
|
return False
|
|
|
|
# TODO: Handle the exception of not being able to load the library file
|
|
# TODO: Remove the linked library from byp.data.libraries
|
|
# ------------------------------------------------------------------------------
|
|
|
|
|
|
class BTOON_OP_set_contours(bpy.types.Operator):
|
|
|
|
bl_idname = "btoon.set_contours"
|
|
bl_label = "Set Contours"
|
|
bl_description = "Set contours to the selected objects"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
def execute(self, context: bpy.types.Context):
|
|
|
|
contour_group_name = "BToon Contour"
|
|
contour_noise_name = "BToon Contour Displace Noise"
|
|
mat_name = "BToon Contour"
|
|
|
|
if not context.selected_objects:
|
|
self.report({'WARNING'}, "BToon: No objects are selected.")
|
|
|
|
return {'FINISHED'}
|
|
|
|
if mat_name in bpy.data.materials:
|
|
self.report({'INFO'}, "BToon: The material append process is skipped since {} already exists.".format(mat_name))
|
|
else:
|
|
blend_file_path = os.path.dirname(os.path.abspath(__file__)) + "/library.blend"
|
|
append_material(blend_file_path, mat_name)
|
|
|
|
assert mat_name in bpy.data.materials
|
|
|
|
self.report({'INFO'}, "BToon: A material named {} is appended.".format(mat_name))
|
|
|
|
mat = bpy.data.materials[mat_name]
|
|
|
|
if not contour_noise_name in bpy.data.textures:
|
|
add_clouds_texture(contour_noise_name, brightness=0.5)
|
|
|
|
for object in context.selected_objects:
|
|
add_vertex_group(object, contour_group_name)
|
|
add_solidify_modifier(object, -0.01, True, False, 1, shell_vertex_group=contour_group_name)
|
|
add_displace_modifier(object, texture_name=contour_noise_name, vertex_group=contour_group_name, mid_level=0.0, strength=-0.05)
|
|
|
|
object.data.materials.append(mat)
|
|
|
|
self.report({'INFO'}, "BToon: The contour for {} is set.".format(object.name))
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
class BTOON_OP_append_skin_material(bpy.types.Operator):
|
|
|
|
bl_idname = "btoon.append_skin_material"
|
|
bl_label = "Append Skin Material"
|
|
bl_description = "Append the skin material to the selected objects"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
def execute(self, context: bpy.types.Context):
|
|
|
|
mat_name = "BToon Skin"
|
|
|
|
if not context.selected_objects:
|
|
self.report({'WARNING'}, "BToon: No objects are selected.")
|
|
|
|
return {'FINISHED'}
|
|
|
|
if mat_name in bpy.data.materials:
|
|
self.report({'INFO'}, "BToon: The material append process is skipped since {} already exists.".format(mat_name))
|
|
else:
|
|
blend_file_path = os.path.dirname(os.path.abspath(__file__)) + "/library.blend"
|
|
append_material(blend_file_path, mat_name)
|
|
|
|
assert mat_name in bpy.data.materials
|
|
|
|
self.report({'INFO'}, "BToon: A material named {} is appended.".format(mat_name))
|
|
|
|
mat = bpy.data.materials[mat_name]
|
|
|
|
for object in context.selected_objects:
|
|
object.data.materials.append(mat)
|
|
|
|
self.report({'INFO'}, "BToon: The skin material is appended to {}.".format(object.name))
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
op_classes = [
|
|
BTOON_OP_set_contours,
|
|
BTOON_OP_append_skin_material,
|
|
]
|
|
|
|
|
|
class BTOON_MT_show_menu(bpy.types.Menu):
|
|
bl_idname = "BTOON_MT_show_menu"
|
|
bl_label = "BToon Utilities"
|
|
bl_description = "Toon shading utilities"
|
|
|
|
def draw(self, context: bpy.types.Context) -> None:
|
|
for op_class in op_classes:
|
|
self.layout.operator(op_class.bl_idname)
|
|
|
|
|
|
def menu_func(self, context: bpy.types.Context) -> None:
|
|
self.layout.separator()
|
|
self.layout.menu(BTOON_MT_show_menu.bl_idname)
|
|
|
|
|
|
def register():
|
|
bpy.utils.register_class(BTOON_MT_show_menu)
|
|
for op_class in op_classes:
|
|
bpy.utils.register_class(op_class)
|
|
bpy.types.VIEW3D_MT_object.append(menu_func)
|
|
|
|
|
|
def unregister():
|
|
bpy.utils.unregister_class(BTOON_MT_show_menu)
|
|
for op_class in op_classes:
|
|
bpy.utils.unregister_class(op_class)
|
|
bpy.types.VIEW3D_MT_object.remove(menu_func)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
register()
|