Files
blender-portable-repo/scripts/addons/cc_blender_tools-2_3_3/sculpting.py
T
2026-03-17 15:25:32 -06:00

1112 lines
49 KiB
Python

# Copyright (C) 2021 Victor Soupday
# This file is part of CC/iC Blender Tools <https://github.com/soupday/cc_blender_tools>
#
# CC/iC Blender Tools is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# CC/iC Blender Tools is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with CC/iC Blender Tools. If not, see <https://www.gnu.org/licenses/>.
import os
import bpy
import mathutils
from . import nodeutils, imageutils, meshutils, geom, materials, bake, modifiers, lib, utils, params, vars
LAYER_TARGET_SCULPT = "BODY"
LAYER_TARGET_DETAIL = "DETAIL"
BAKE_TYPE_NORMALS = "NORMALS"
BAKE_TYPE_DISPLACEMENT = "DISPLACEMENT"
BAKE_TYPE_AO = "AO"
LAYER_MIX_SUFFIX = "Layer_Mix"
BAKE_NORMAL_SUFFIX = "Bake_Normal"
BAKE_DISPLACEMENT_SUFFIX = "Bake_Displacement"
BAKE_AO_SUFFIX = "Bake_AO"
LAYER_NORMAL_SUFFIX = "Layer_Normal"
LAYER_DISPLACEMENT_SUFFIX = "Layer_Displacement"
LAYER_AO_SUFFIX = "Layer_AO"
BAKE_FOLDER = "Sculpt Bake"
SKINGEN_FOLDER = "Skingen"
def set_multi_res_level(obj, view_level = -1, sculpt_level = -1, render_level = -1):
if obj:
mod : bpy.types.MultiresModifier
mod = modifiers.get_object_modifier(obj, modifiers.MOD_MULTIRES, modifiers.MOD_MULTIRES_NAME)
if mod:
utils.log_info(f"Setting Multi-res modifier to levels: {view_level}/{sculpt_level}/{render_level}")
if view_level >= 0:
mod.levels = max(0, min(view_level, mod.total_levels))
if sculpt_level >= 0:
mod.sculpt_levels = max(0, min(sculpt_level, mod.total_levels))
if render_level >= 0:
mod.render_levels = max(0, min(render_level, mod.total_levels))
def apply_multi_res_shape(body):
# applying base shape distorts the displacement maps
# so it must be done after the displacement map is baked, and it must be final,
# displacement map masks will no longer work after this
if utils.object_mode() and utils.set_only_active_object(body):
# removing all shape keys
utils.log_info("Removing all shape keys")
try:
bpy.ops.object.shape_key_remove(all=True)
except:
pass
# applying base shape
mod = modifiers.get_object_modifier(body, modifiers.MOD_MULTIRES, modifiers.MOD_MULTIRES_NAME)
if mod and utils.set_only_active_object(body):
utils.log_info("Applying base shape")
if utils.B500():
# apply_heuristic=True applies base assuming it will be used subdivided
# apply_heuristic=False applies base to level 0
bpy.ops.object.multires_base_apply(modifier=mod.name, apply_heuristic=False)
else:
bpy.ops.object.multires_base_apply(modifier=mod.name)
def displacement_map_func(value):
return abs(value - 0.5)
def copy_base_shape(multi_res_object, source_body_obj, layer_target, by_vertex_group = False):
utils.log_info("Copying shape to source body.")
if by_vertex_group:
# generate vertex weights for mesh copy
for mat in multi_res_object.data.materials:
displacement_map = nodeutils.get_node_by_id_and_type(mat.node_tree.nodes,
f"{layer_target}_{BAKE_DISPLACEMENT_SUFFIX}",
"TEX_IMAGE")
geom.map_image_to_vertex_weights(multi_res_object, mat, displacement_map.image,
"DISPLACEMENT_MASKED", displacement_map_func)
# copy to source body using vertex weights as a copy mask
geom.copy_vert_positions_by_uv_id(multi_res_object, source_body_obj, accuracy = 5,
vertex_group = "DISPLACEMENT_MASKED", threshold = 0.0038,
flatten_udim=True)
else:
# copy to source body
geom.copy_vert_positions_by_uv_id(multi_res_object, source_body_obj, accuracy = 5,
flatten_udim=True)
return
def do_multires_bake(context, chr_cache, multires_mesh, layer_target, apply_shape=False, source_body=None):
prefs = vars.prefs()
utils.log_info(f"Begin Multi-Res Bake: Layer = {layer_target}")
utils.log_indent()
if utils.B292():
bpy.context.scene.render.bake.target = 'IMAGE_TEXTURES'
# store object render visibility state
rv_state = utils.store_render_visibility_state()
# prep for baking directly onto body mesh surface
bake_state = bake.prep_bake(context, samples=32, make_surface=False)
# AO Baking (full res on body mesh)
select_bake_images(multires_mesh, BAKE_TYPE_AO, layer_target)
ao_body = utils.duplicate_object(multires_mesh)
ao_body.name = multires_mesh.name + "_AOBAKE"
materials.normalize_udim_uvs(ao_body)
utils.set_only_render_visible(ao_body)
utils.object_mode_to(ao_body)
utils.set_only_active_object(ao_body)
set_multi_res_level(ao_body, view_level=9, sculpt_level=9, render_level=9)
utils.log_info(f"Baking {layer_target} AO...")
if utils.B500():
bpy.context.scene.render.bake.use_multires = False
else:
bpy.context.scene.render.use_bake_multires = False
# *cycles* bake type to AO
bpy.context.scene.cycles.bake_type = "AO"
if prefs.bake_use_gpu:
bake.set_cycles_samples(context, samples=2048, adaptive_samples=0.1, time_limit=15, use_gpu=True)
else:
bake.set_cycles_samples(context, samples=16, time_limit=30, use_gpu=False)
bpy.ops.object.bake(type="AO")
utils.delete_mesh_object(ao_body)
# Displacement Baking
select_bake_images(multires_mesh, BAKE_TYPE_DISPLACEMENT, layer_target)
if utils.B500():
bpy.context.scene.render.bake.use_multires = True
else:
bpy.context.scene.render.use_bake_multires = True
bake.set_cycles_samples(context, samples=2)
# copy the body for displacement baking
utils.log_info("Duplicating body for displacement baking")
utils.unhide(multires_mesh)
disp_body = utils.duplicate_object(multires_mesh)
disp_body.name = multires_mesh.name + "_DISPBAKE"
materials.normalize_udim_uvs(disp_body)
# displacement masks *will not* bake if multiple overlapping materials in the mesh,
# so split by materials and bake each separately.
utils.log_info(f"Baking {layer_target} displacement...")
utils.clear_selected_objects()
utils.set_only_active_object(disp_body)
utils.edit_mode_to(disp_body)
bpy.ops.mesh.separate(type='MATERIAL')
objects = bpy.context.selected_objects.copy()
for obj in objects:
utils.set_only_render_visible(obj)
utils.object_mode_to(obj)
utils.set_only_active_object(obj)
# copying or splitting the mesh resets the multi-res levels...
set_multi_res_level(obj, view_level=0, sculpt_level=9, render_level=9)
# bake the displacement mask
utils.log_info(f"Baking {layer_target} sub displacement {obj.name}")
if utils.B500():
bpy.context.scene.render.bake.type = BAKE_TYPE_DISPLACEMENT
else:
bpy.context.scene.render.bake_type = BAKE_TYPE_DISPLACEMENT
bpy.ops.object.bake_image()
utils.delete_mesh_object(obj)
# Normal Baking
select_bake_images(multires_mesh, BAKE_TYPE_NORMALS, layer_target)
# copy the body for normal baking
utils.set_only_render_visible(multires_mesh)
utils.log_info("Duplicating body for normal baking")
norm_body = utils.duplicate_object(multires_mesh)
norm_body.name = multires_mesh.name + "_NORMBAKE"
materials.normalize_udim_uvs(norm_body)
utils.set_only_render_visible(norm_body)
utils.object_mode_to(norm_body)
utils.set_only_active_object(norm_body)
apply_multi_res_shape(norm_body)
# set multi-res levels for normal baking
utils.log_info("Setting multi-res levels for baking")
set_multi_res_level(norm_body, view_level=0, sculpt_level=9, render_level=9)
# bake the normals
utils.log_info(f"Baking {layer_target} normals...")
if utils.B500():
bpy.context.scene.render.bake.type = BAKE_TYPE_NORMALS
else:
bpy.context.scene.render.bake_type = BAKE_TYPE_NORMALS
bpy.ops.object.bake_image()
utils.log_recess()
utils.log_info("Baking complete!")
if layer_target == LAYER_TARGET_SCULPT and apply_shape and source_body:
utils.log_info("Transfering sculpt base shape to source body...")
utils.unhide(multires_mesh)
utils.unhide(source_body)
if norm_body and source_body:
copy_base_shape(norm_body, source_body, layer_target, True)
# if there is a detail sculpt body, update that with the new base shape too
detail_body = chr_cache.get_detail_body(context_object=source_body)
if detail_body:
# the base shape has only been applied to the norm_body so far ...
copy_base_shape(norm_body, detail_body, layer_target, True)
utils.delete_mesh_object(norm_body)
# restore render engine
bake.post_bake(context, bake_state)
# restore object render visibilty state
utils.restore_render_visibility_state(rv_state)
def save_skin_gen_bake(chr_cache, body, layer_target):
base_dir = utils.local_path()
if not base_dir:
base_dir = chr_cache.get_import_dir()
bake_dir = os.path.join(base_dir, BAKE_FOLDER)
utils.log_info(f"Texture save path: {bake_dir}")
os.makedirs(bake_dir, exist_ok=True)
character_name = chr_cache.character_name
if body:
for mat in body.data.materials:
normal_image_name = f"{character_name}_{mat.name}_{layer_target}_{BAKE_NORMAL_SUFFIX}"
displacement_image_name = f"{character_name}_{mat.name}_{layer_target}_{BAKE_DISPLACEMENT_SUFFIX}"
ao_image_name = f"{character_name}_{mat.name}_{layer_target}_{BAKE_AO_SUFFIX}"
if normal_image_name in bpy.data.images:
normal_image = bpy.data.images[normal_image_name]
if displacement_image_name in bpy.data.images:
displacement_image = bpy.data.images[displacement_image_name]
if ao_image_name in bpy.data.images:
ao_image = bpy.data.images[ao_image_name]
images = [
[normal_image, normal_image_name, 'PNG', '8'],
[ao_image, ao_image_name, 'PNG', '8'],
[displacement_image, displacement_image_name, 'PNG', '16'],
]
image : bpy.types.Image
for image, image_name, file_format, color_depth in images:
if image:
image_file = image_name + ".png"
image_path = os.path.normpath(os.path.join(bake_dir, image_file))
if image_path:
imageutils.save_scene_image(image, image_path, file_format, color_depth)
utils.log_info(f"Saved baked Image: {image_path}")
def select_bake_images(body, bake_type, layer_target):
if body:
for mat in body.data.materials:
nodes = mat.node_tree.nodes
for node in nodes:
node.select = False
if bake_type == BAKE_TYPE_NORMALS:
bake_node_name = f"{layer_target}_{BAKE_NORMAL_SUFFIX}"
elif bake_type == BAKE_TYPE_AO:
bake_node_name = f"{layer_target}_{BAKE_AO_SUFFIX}"
else:
bake_node_name = f"{layer_target}_{BAKE_DISPLACEMENT_SUFFIX}"
bake_node = nodeutils.find_node_by_type_and_keywords(nodes, "TEX_IMAGE", bake_node_name)
if bake_node:
utils.log_info(f"Selecting image {bake_node.name} for bake.")
bake_node.select = True
nodes.active = bake_node
else:
utils.log_error(f"Could not find image node: {bake_node_name}!")
def has_overlay_nodes(body, layer_target):
mix_node_name = f"{layer_target}_{LAYER_MIX_SUFFIX}"
if body:
for mat in body.data.materials:
nodes = mat.node_tree.nodes
mix_node = nodeutils.find_node_by_type_and_keywords(nodes, "GROUP", mix_node_name)
if mix_node:
return True
return False
def has_body_multires_mod(body):
if body:
mod = modifiers.get_object_modifier(body, modifiers.MOD_MULTIRES, modifiers.MOD_MULTIRES_NAME)
if mod:
return True
return False
def export_skingen(context, chr_cache, layer_target, export_path):
export_dir, export_file = os.path.split(export_path)
export_name, export_ext = os.path.splitext(export_file)
utils.log_info(f"Texture save path: {export_dir}")
os.makedirs(export_dir, exist_ok=True)
source_obj = utils.get_context_mesh(context)
if layer_target == LAYER_TARGET_DETAIL:
channel_id = "Detail"
else:
channel_id = "Body"
if source_obj:
mix_node_name = f"{layer_target}_{LAYER_MIX_SUFFIX}"
for mat in source_obj.data.materials:
nodes = mat.node_tree.nodes
links = mat.node_tree.links
mix_node = nodeutils.find_node_by_type_and_keywords(nodes, "GROUP", mix_node_name)
if mix_node:
bake.bake_node_socket_output(context, mix_node, "Normal Layer", mat, channel_id + " Normal", export_dir,
name_prefix = export_name, exact_name=True, underscores=False)
bake.bake_node_socket_output(context, mix_node, "AO Layer", mat, channel_id + " AO", export_dir,
name_prefix = export_name, exact_name=True, underscores=False)
bake.bake_node_socket_output(context, mix_node, "Mask", mat, channel_id + " Mask", export_dir,
name_prefix = export_name, exact_name=True, underscores=False)
def update_layer_nodes(context, chr_cache, layer_target, socket, value):
context = vars.get_context(context)
source_obj = utils.get_context_mesh(context)
if chr_cache and source_obj:
mix_node_name = f"{layer_target}_{LAYER_MIX_SUFFIX}"
for mat in source_obj.data.materials:
nodes = mat.node_tree.nodes
mix_node = nodeutils.find_node_by_type_and_keywords(nodes, "GROUP", mix_node_name)
nodeutils.set_node_input_value(mix_node, socket, value)
def get_bake_dir(chr_cache):
base_dir = utils.local_path()
if not base_dir:
base_dir = chr_cache.get_import_dir()
bake_dir = os.path.join(base_dir, BAKE_FOLDER)
utils.log_info(f"Texture save path: {bake_dir}")
os.makedirs(bake_dir, exist_ok=True)
return bake_dir
def setup_bake_nodes(context, chr_cache, multires_mesh, layer_target):
prefs = vars.prefs()
bake_dir = get_bake_dir(chr_cache)
for mat in multires_mesh.data.materials:
nodes = mat.node_tree.nodes
links = mat.node_tree.links
utils.log_info(f"Setting up {layer_target} bake and layer nodes for {mat.name}")
mat_cache = chr_cache.get_material_cache(mat)
shader_name = params.get_shader_name(mat_cache)
bsdf_node, shader_node, mixer_node = nodeutils.get_shader_nodes(mat, shader_name)
# base the image name on the character name
character_name = chr_cache.character_name
mix_node_name = f"{layer_target}_{LAYER_MIX_SUFFIX}"
sculpt_mix_node_name = f"{LAYER_TARGET_SCULPT}_{LAYER_MIX_SUFFIX}"
detail_mix_node_name = f"{LAYER_TARGET_DETAIL}_{LAYER_MIX_SUFFIX}"
normal_image_name = f"{character_name}_{mat.name}_{layer_target}_{BAKE_NORMAL_SUFFIX}"
ao_image_name = f"{character_name}_{mat.name}_{layer_target}_{BAKE_AO_SUFFIX}"
displacement_image_name = f"{character_name}_{mat.name}_{layer_target}_{BAKE_DISPLACEMENT_SUFFIX}"
normal_bake_node_name = f"{layer_target}_{BAKE_NORMAL_SUFFIX}"
ao_bake_node_name = f"{layer_target}_{BAKE_AO_SUFFIX}"
displacement_bake_node_name = f"{layer_target}_{BAKE_DISPLACEMENT_SUFFIX}"
normal_layer_node_name = f"{layer_target}_{LAYER_NORMAL_SUFFIX}"
ao_layer_node_name = f"{layer_target}_{LAYER_AO_SUFFIX}"
displacement_layer_node_name = f"{layer_target}_{LAYER_DISPLACEMENT_SUFFIX}"
normal_image_file = normal_image_name + ".png"
normal_image_path = os.path.normpath(os.path.join(bake_dir, normal_image_file))
ao_image_file = ao_image_name + ".png"
ao_image_path = os.path.normpath(os.path.join(bake_dir, ao_image_file))
displacement_image_file = displacement_image_name + ".png"
displacement_image_path = os.path.normpath(os.path.join(bake_dir, displacement_image_file))
delta = 0
if layer_target == LAYER_TARGET_DETAIL:
delta = 600
if layer_target == LAYER_TARGET_DETAIL:
normal_image = imageutils.get_custom_image(normal_image_name, int(prefs.detail_normal_bake_size), alpha = False, data = True, path = normal_image_path)
ao_image = imageutils.get_custom_image(ao_image_name, int(0.5 * int(prefs.detail_normal_bake_size)), alpha = False, data = True, path = ao_image_path)
displacement_image = imageutils.get_custom_image(displacement_image_name, int(prefs.detail_normal_bake_size), alpha = False, data = True, float = True, path = displacement_image_path)
else:
normal_image = imageutils.get_custom_image(normal_image_name, int(prefs.body_normal_bake_size), alpha = False, data = True, path = normal_image_path)
ao_image = imageutils.get_custom_image(ao_image_name, int(0.5 * int(prefs.body_normal_bake_size)), alpha = False, data = True, path = ao_image_path)
displacement_image = imageutils.get_custom_image(displacement_image_name, int(prefs.body_normal_bake_size), alpha = False, data = True, float = True, path = displacement_image_path)
normal_tex_node = nodeutils.find_node_by_type_and_keywords(nodes, "TEX_IMAGE", "(NORMAL)")
ao_tex_node = nodeutils.find_node_by_type_and_keywords(nodes, "TEX_IMAGE", "(AO)")
ref_location = mathutils.Vector((-1600, -1100))
normal_bake_node = nodeutils.create_custom_image_node(nodes, normal_bake_node_name, normal_image, location = (1000 + delta, -1000))
ao_bake_node = nodeutils.create_custom_image_node(nodes, ao_bake_node_name, ao_image, location = (1000 + delta, -1300))
displacement_bake_node = nodeutils.create_custom_image_node(nodes, displacement_bake_node_name, displacement_image,
location = (1000 + delta, -1600))
normal_layer_node = nodeutils.create_custom_image_node(nodes, normal_layer_node_name, normal_image,
location = ref_location + mathutils.Vector((delta, -1200)))
ao_layer_node = nodeutils.create_custom_image_node(nodes, ao_layer_node_name, ao_image,
location = ref_location + mathutils.Vector((delta, -1500)))
displacement_layer_node = nodeutils.create_custom_image_node(nodes, displacement_layer_node_name, displacement_image,
location = ref_location + mathutils.Vector((delta, -1800)))
# find or create the layer mix group
mix_node = nodeutils.find_node_by_type_and_keywords(nodes, "GROUP", mix_node_name)
if not mix_node:
mix_group = lib.get_node_group("rl_tex_mod_normal_ao_blend")
mix_node = nodeutils.make_node_group_node(nodes, mix_group, "Normal Blend", mix_node_name)
if layer_target == LAYER_TARGET_DETAIL:
chr_cache.detail_normal_strength = 1.0
chr_cache.detail_ao_strength = 0.5
chr_cache.detail_normal_definition = 15.0
chr_cache.detail_mix_mode = "OVERLAY"
elif layer_target == LAYER_TARGET_SCULPT:
chr_cache.body_normal_strength = 1.0
chr_cache.body_ao_strength = 0.5
chr_cache.body_normal_definition = 15.0
chr_cache.body_mix_mode = "OVERLAY"
if layer_target == LAYER_TARGET_DETAIL:
nodeutils.set_node_input_value(mix_node, "Normal Strength", chr_cache.detail_normal_strength)
nodeutils.set_node_input_value(mix_node, "AO Strength", chr_cache.detail_ao_strength)
nodeutils.set_node_input_value(mix_node, "Definition", chr_cache.detail_normal_definition)
nodeutils.set_node_input_value(mix_node, "Mix Mode", 0.0)
elif layer_target == LAYER_TARGET_SCULPT:
nodeutils.set_node_input_value(mix_node, "Normal Strength", chr_cache.body_normal_strength)
nodeutils.set_node_input_value(mix_node, "AO Strength", chr_cache.body_ao_strength)
nodeutils.set_node_input_value(mix_node, "Definition", chr_cache.body_normal_definition)
nodeutils.set_node_input_value(mix_node, "Mix Mode", 0.0)
mix_node.location = ref_location + mathutils.Vector((300 + delta, -1200))
# if connecting the detail layer and there is also a sculpt layer, connect the normal input from the sculpt layer instead
sculpt_mix_node = nodeutils.find_node_by_type_and_keywords(nodes, "GROUP", sculpt_mix_node_name)
if layer_target == LAYER_TARGET_DETAIL and sculpt_mix_node:
nodeutils.link_nodes(links, sculpt_mix_node, "Color", mix_node, "Color1")
nodeutils.link_nodes(links, sculpt_mix_node, "AO", mix_node, "AO1")
else:
nodeutils.link_nodes(links, normal_tex_node, "Color", mix_node, "Color1")
nodeutils.link_nodes(links, ao_tex_node, "Color", mix_node, "AO1")
nodeutils.link_nodes(links, normal_layer_node, "Color", mix_node, "Color2")
nodeutils.link_nodes(links, ao_layer_node, "Color", mix_node, "AO2")
nodeutils.link_nodes(links, displacement_layer_node, "Color", mix_node, "Displacement Mask")
# if connecting the sculpt layer and there is also a detail layer, connect the normal output from the sculpt layer to detail layer input
detail_mix_node = nodeutils.find_node_by_type_and_keywords(nodes, "GROUP", detail_mix_node_name)
if layer_target == LAYER_TARGET_SCULPT and detail_mix_node:
nodeutils.link_nodes(links, mix_node, "Color", detail_mix_node, "Color1")
nodeutils.link_nodes(links, mix_node, "AO", detail_mix_node, "AO1")
else:
nodeutils.link_nodes(links, mix_node, "Color", shader_node, "Normal Map")
nodeutils.link_nodes(links, mix_node, "AO", shader_node, "AO Map")
# disconnect the normals to the bsdf node (so they don't get included in the bake)
nodeutils.unlink_node_input(links, bsdf_node, "Normal")
def finish_bake(chr_cache, detail_body, layer_target):
if detail_body:
for mat in detail_body.data.materials:
utils.log_info(f"Finalizing bake node setup for {mat.name}")
nodes = mat.node_tree.nodes
links = mat.node_tree.links
mat_cache = chr_cache.get_material_cache(mat)
shader_name = params.get_shader_name(mat_cache)
bsdf_node, shader_node, mixer_node = nodeutils.get_shader_nodes(mat, shader_name)
nodeutils.link_nodes(links, shader_node, "Normal", bsdf_node, "Normal")
def remove_bake_nodes(context, chr_cache, layer_target, multires_mesh):
if not utils.object_exists_is_mesh(multires_mesh):
utils.log_error("Multires mesh not found!")
return
for mat in multires_mesh.data.materials:
nodes = mat.node_tree.nodes
links = mat.node_tree.links
utils.log_info(f"Removing {layer_target} bake and layer nodes for {mat.name}")
mix_node_name = f"{layer_target}_{LAYER_MIX_SUFFIX}"
normal_bake_node_name = f"{layer_target}_{BAKE_NORMAL_SUFFIX}"
ao_bake_node_name = f"{layer_target}_{BAKE_AO_SUFFIX}"
displacement_bake_node_name = f"{layer_target}_{BAKE_DISPLACEMENT_SUFFIX}"
normal_layer_node_name = f"{layer_target}_{LAYER_NORMAL_SUFFIX}"
ao_layer_node_name = f"{layer_target}_{LAYER_AO_SUFFIX}"
displacement_layer_node_name = f"{layer_target}_{LAYER_DISPLACEMENT_SUFFIX}"
# remove the mix layer
mix_node = nodeutils.find_node_by_type_and_keywords(nodes, "GROUP", mix_node_name)
normal_to_node, normal_to_socket = nodeutils.get_node_and_socket_connected_to_output(mix_node, "Color")
normal_from_node, normal_from_socket = nodeutils.get_node_and_socket_connected_to_input(mix_node, "Color1")
ao_to_node, ao_to_socket = nodeutils.get_node_and_socket_connected_to_output(mix_node, "AO")
ao_from_node, ao_from_socket = nodeutils.get_node_and_socket_connected_to_input(mix_node, "AO1")
if mix_node:
nodes.remove(mix_node)
if normal_from_socket and normal_to_socket:
nodeutils.link_nodes(links, normal_from_node, normal_from_socket, normal_to_node, normal_to_socket)
if ao_from_socket and ao_to_socket:
nodeutils.link_nodes(links, ao_from_node, ao_from_socket, ao_to_node, ao_to_socket)
# remove the image nodes
for node_name in [normal_bake_node_name, normal_layer_node_name,
ao_bake_node_name, ao_layer_node_name,
displacement_bake_node_name, displacement_layer_node_name]:
node = nodeutils.find_node_by_type_and_keywords(nodes, "TEX_IMAGE", node_name)
if node:
nodes.remove(node)
def flatten_bake_layers(context, chr_cache):
prefs = vars.prefs()
bake_dir = get_bake_dir(chr_cache)
prefix = chr_cache.character_name + "_Flattened"
context_obj = utils.get_context_mesh(context)
if context_obj:
for mat in context_obj.data.materials:
nodes = mat.node_tree.nodes
links = mat.node_tree.links
sculpt_mix_node_name = f"{LAYER_TARGET_SCULPT}_{LAYER_MIX_SUFFIX}"
detail_mix_node_name = f"{LAYER_TARGET_DETAIL}_{LAYER_MIX_SUFFIX}"
sculpt_mix_node = nodeutils.find_node_by_type_and_keywords(nodes, "GROUP", sculpt_mix_node_name)
detail_mix_node = nodeutils.find_node_by_type_and_keywords(nodes, "GROUP", detail_mix_node_name)
mix_node = detail_mix_node if detail_mix_node else sculpt_mix_node
if mix_node:
# bake the full layer outputs
flattened_normal_image = bake.bake_node_socket_output(context, mix_node, "Color", mat, "Normal", bake_dir,
name_prefix=prefix, exact_name=True,
underscores=True, unique_name=True)
flattened_ao_image = bake.bake_node_socket_output(context, mix_node, "AO", mat, "AO", bake_dir,
name_prefix=prefix, exact_name=True,
underscores=True, unique_name=True)
normal_image_node = None
ao_image_node = None
# determine the source Normal and AO image nodes.
if (sculpt_mix_node and detail_mix_node) or sculpt_mix_node:
normal_image_node = nodeutils.get_node_connected_to_input(sculpt_mix_node, "Color1")
ao_image_node = nodeutils.get_node_connected_to_input(sculpt_mix_node, "AO1")
elif detail_mix_node:
normal_image_node = nodeutils.get_node_connected_to_input(detail_mix_node, "Color1")
ao_image_node = nodeutils.get_node_connected_to_input(detail_mix_node, "AO1")
# change the Normal image, or create a new Normal node
if normal_image_node and normal_image_node.type == "TEX_IMAGE":
normal_image_node.image = flattened_normal_image
else:
normal_node_name = f"FLATTENED_{LAYER_NORMAL_SUFFIX}"
normal_image_node = nodeutils.create_custom_image_node(nodes, normal_node_name, flattened_normal_image,
location=mathutils.Vector((-500, -1200)))
# change the AO image, or create a new AO node
if ao_image_node and ao_image_node.type == "TEX_IMAGE":
ao_image_node.image = flattened_ao_image
else:
ao_node_name = f"FLATTENED_{LAYER_AO_SUFFIX}"
ao_image_node = nodeutils.create_custom_image_node(nodes, ao_node_name, flattened_ao_image,
location=mathutils.Vector((-500, -1500)))
# reconnect the image nodes to the sculpt layer mix nodes
# (these will be reconnected when cleaning up the sculpt layers)
if (sculpt_mix_node and detail_mix_node) or sculpt_mix_node:
nodeutils.link_nodes(links, normal_image_node, "Color", sculpt_mix_node, "Color1")
nodeutils.link_nodes(links, ao_image_node, "Color", sculpt_mix_node, "AO1")
elif detail_mix_node:
nodeutils.link_nodes(links, normal_image_node, "Color", detail_mix_node, "Color1")
nodeutils.link_nodes(links, ao_image_node, "Color", detail_mix_node, "AO1")
clean_multires_sculpt(context, chr_cache, LAYER_TARGET_DETAIL)
clean_multires_sculpt(context, chr_cache, LAYER_TARGET_SCULPT)
def get_layer_target_mesh(context, chr_cache, layer_target):
mesh = None
context_object = utils.get_context_mesh(context)
if layer_target == LAYER_TARGET_DETAIL:
mesh = chr_cache.get_detail_body(context_object=context_object)
elif layer_target == LAYER_TARGET_SCULPT:
mesh = chr_cache.get_sculpt_body(context_object=context_object)
return mesh
def set_layer_target_mesh(chr_cache, layer_target, mesh):
if layer_target == LAYER_TARGET_DETAIL:
chr_cache.set_detail_body(mesh)
elif layer_target == LAYER_TARGET_SCULPT:
chr_cache.set_sculpt_body(mesh)
return mesh
def set_sculpt_source(multires_mesh, layer_target, source_object):
prop_name = f"rl_multires_{layer_target}"
if utils.object_exists_is_mesh(source_object) and utils.object_exists_is_mesh(multires_mesh):
multires_mesh[prop_name] = source_object.name
def bake_multires_sculpt(context, chr_cache, layer_target, apply_shape=False):
multires_mesh = get_layer_target_mesh(context, chr_cache, layer_target)
multires_source = chr_cache.get_sculpt_source(multires_mesh, layer_target)
utils.unhide(multires_mesh)
# make sure to go into object mode otherwise the sculpt is not applied.
if utils.object_mode_to(multires_mesh):
setup_bake_nodes(context, chr_cache, multires_mesh, layer_target)
do_multires_bake(context, chr_cache, multires_mesh, layer_target, apply_shape=apply_shape, source_body=multires_source)
save_skin_gen_bake(chr_cache, multires_mesh, layer_target)
finish_bake(chr_cache, multires_mesh, layer_target)
end_multires_sculpting(context, chr_cache, layer_target, show_baked = True)
def set_hide_character(chr_cache, hide):
arm = chr_cache.get_armature()
for obj in chr_cache.get_all_objects(include_armature=True,
include_children=True):
if not hide and chr_cache.is_sculpt_object(obj):
# always hide the sculpt objects
utils.hide(obj, True)
else:
utils.hide(obj, hide)
utils.hide(arm, hide)
def begin_multires_sculpting(context, chr_cache, layer_target):
# get the context sculpt target
multi_res_mesh = get_layer_target_mesh(context, chr_cache, layer_target)
# update the last used sculpt target
set_layer_target_mesh(chr_cache, layer_target, multi_res_mesh)
# begin
if utils.object_exists_is_mesh(multi_res_mesh):
set_hide_character(chr_cache, True)
utils.unhide(multi_res_mesh)
utils.object_mode()
utils.set_only_active_object(multi_res_mesh)
#bpy.ops.view3d.view_selected()
# TODO mute the detail normal mix nodes (so the normal overlay isn't shown when sculpting)
utils.set_mode("SCULPT")
shading = utils.get_view_3d_shading(context)
if shading:
try:
shading.type = 'SOLID'
shading.light = 'MATCAP'
shading.studio_light = 'basic_1.exr'
shading.show_cavity = True
except: ...
def end_multires_sculpting(context, chr_cache, layer_target, multires_mesh=None, show_baked = False):
if not multires_mesh:
multires_mesh = get_layer_target_mesh(context, chr_cache, layer_target)
if utils.object_exists_is_mesh(multires_mesh):
sculpt_source = chr_cache.get_sculpt_source(multires_mesh, layer_target)
body_objects = chr_cache.get_objects_of_type(LAYER_TARGET_SCULPT)
if show_baked:
set_multi_res_level(multires_mesh, view_level=0)
else:
set_multi_res_level(multires_mesh, view_level=9)
utils.object_mode()
set_hide_character(chr_cache, False)
utils.hide(multires_mesh)
if sculpt_source:
utils.set_only_active_object(sculpt_source)
else:
utils.try_select_objects(body_objects)
shading = utils.get_view_3d_shading(context)
if shading:
shading.type = 'MATERIAL'
def clean_multires_sculpt(context, chr_cache, layer_target):
multires_mesh = get_layer_target_mesh(context, chr_cache, layer_target)
if multires_mesh:
end_multires_sculpting(context, chr_cache, layer_target, multires_mesh=multires_mesh, show_baked = True)
remove_bake_nodes(context, chr_cache, layer_target, multires_mesh)
remove_multires_body(context, chr_cache, layer_target, multires_mesh)
def remove_multires_body(context, chr_cache, layer_target, multires_mesh):
if multires_mesh:
utils.delete_mesh_object(multires_mesh)
if layer_target == LAYER_TARGET_DETAIL and chr_cache.detail_multires_body == multires_mesh:
chr_cache.set_detail_body(None)
elif layer_target == LAYER_TARGET_SCULPT and chr_cache.sculpt_multires_body == multires_mesh:
chr_cache.set_sculpt_body(None)
def hide_body_parts(chr_cache):
prefs = vars.prefs()
body_objects = chr_cache.get_objects_of_type("BODY")
for body in body_objects:
hide_slots = []
for i in range(0, len(body.material_slots)):
slot = body.material_slots[i]
mat = slot.material
if mat:
mat_cache = chr_cache.get_material_cache(mat)
# hide eyelashes and nails
if (mat_cache.material_type == "NAILS" or
mat_cache.material_type == "EYELASH"):
hide_slots.append(i)
utils.object_mode()
utils.clear_selected_objects()
if utils.edit_mode_to(body):
bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='FACE')
bpy.ops.mesh.select_all(action='DESELECT')
for slot_index in hide_slots:
bpy.context.object.active_material_index = slot_index
bpy.ops.object.material_slot_select()
bpy.ops.mesh.hide(unselected=False)
utils.object_mode()
def add_multires_mesh(context, chr_cache, layer_target, sub_target = "ALL"):
prefs = vars.prefs()
context = vars.get_context(context=context)
# duplicate the body
context_object = utils.get_context_mesh(context)
body_cache = chr_cache.get_body_cache()
body_id = body_cache.object_id if body_cache else None
body_objects = chr_cache.get_objects_of_type("BODY")
cache_objects = chr_cache.get_cache_objects()
is_body_type = body_id and utils.get_rl_object_id(context_object) == body_id
multires_mesh = None
multires_source = None
# if the body object has been split, assume the user wants to keep their split
# for sculpting, only split the object when the full body is present.
if len(body_objects) == 1 and is_body_type:
body = body_objects[0]
multires_mesh = utils.duplicate_object(body)
multires_source = body
# split the objects by material
utils.clear_selected_objects()
utils.edit_mode_to(multires_mesh)
bpy.ops.mesh.separate(type='MATERIAL')
objects = context.selected_objects.copy()
rejoin = []
# delete the material parts not wanted by the sculpt target
for obj in objects:
if len(obj.material_slots) > 0:
mat = obj.material_slots[0].material
mat_cache = chr_cache.get_material_cache(mat)
remove = False
if mat and mat_cache:
# always remove eyelashes and nails
if (mat_cache.material_type == "NAILS" or
mat_cache.material_type == "EYELASH"):
remove = True
if sub_target == "BODY":
# remove head
if mat_cache.material_type == "SKIN_HEAD":
remove = True
elif sub_target == "HEAD":
# remove everything but head
if mat_cache.material_type != "SKIN_HEAD":
remove = True
if remove:
utils.delete_mesh_object(obj)
else:
rejoin.append(obj)
# rejoin the remaining objects
utils.try_select_objects(rejoin, True)
multires_mesh = rejoin[0]
utils.set_active_object(multires_mesh)
bpy.ops.object.join()
else:
if context_object in cache_objects:
body = context_object
multires_mesh = utils.duplicate_object(body)
multires_source = body
if multires_mesh and utils.set_only_active_object(multires_mesh):
# remove doubles
if utils.edit_mode_to(multires_mesh):
bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='VERT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.mesh.remove_doubles()
if utils.object_mode_to(multires_mesh):
# remove all modifiers
multires_mesh.modifiers.clear()
# remove all shapekeys
if utils.object_has_shape_keys(multires_mesh):
bpy.ops.object.shape_key_remove(all=True)
# unparent and keep transform
#bpy.ops.object.parent_clear(type = "CLEAR_KEEP_TRANSFORM")
if layer_target == LAYER_TARGET_DETAIL:
sculpt_level = prefs.detail_multires_level
elif layer_target == LAYER_TARGET_SCULPT:
sculpt_level = prefs.sculpt_multires_level
else:
sculpt_level = 2
# add multi-res modifier
modifiers.add_multi_res_modifier(multires_mesh, sculpt_level, use_custom_normals=True, quality=6)
# store the references
set_layer_target_mesh(chr_cache, layer_target, multires_mesh)
if layer_target == LAYER_TARGET_DETAIL:
chr_cache.detail_sculpt_sub_target = sub_target
multires_mesh.name = body.name + "_" + layer_target
set_sculpt_source(multires_mesh, layer_target, multires_source)
return multires_mesh
def setup_multires_sculpt(context, chr_cache, layer_target):
props = vars.props()
prefs = vars.prefs()
if chr_cache:
multires_mesh = get_layer_target_mesh(context, chr_cache, layer_target)
if layer_target == LAYER_TARGET_DETAIL:
detail_sculpt_sub_target = chr_cache.detail_sculpt_sub_target
if multires_mesh and detail_sculpt_sub_target == prefs.detail_sculpt_sub_target:
begin_multires_sculpting(context, chr_cache, layer_target)
elif multires_mesh and detail_sculpt_sub_target != prefs.detail_sculpt_sub_target:
remove_multires_body(context, chr_cache, layer_target, multires_mesh)
multires_mesh = add_multires_mesh(context, chr_cache, layer_target, prefs.detail_sculpt_sub_target)
begin_multires_sculpting(context, chr_cache, layer_target)
elif multires_mesh is None:
multires_mesh = add_multires_mesh(context, chr_cache, layer_target, prefs.detail_sculpt_sub_target)
begin_multires_sculpting(context, chr_cache, layer_target)
elif layer_target == LAYER_TARGET_SCULPT:
if multires_mesh:
begin_multires_sculpting(context, chr_cache, layer_target)
else:
multires_mesh = add_multires_mesh(context, chr_cache, layer_target)
begin_multires_sculpting(context, chr_cache, layer_target)
class CC3OperatorSculpt(bpy.types.Operator):
"""Sculpt Functions"""
bl_idname = "cc3.sculpting"
bl_label = "Sculpting Functions"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
param: bpy.props.StringProperty(
name = "param",
default = ""
)
def execute(self, context):
props = vars.props()
prefs = vars.prefs()
chr_cache = props.get_context_character_cache(context)
if self.param == "DETAIL_SETUP":
setup_multires_sculpt(context, chr_cache, LAYER_TARGET_DETAIL)
elif self.param == "DETAIL_BEGIN":
begin_multires_sculpting(context, chr_cache, LAYER_TARGET_DETAIL)
elif self.param == "DETAIL_END":
end_multires_sculpting(context, chr_cache, LAYER_TARGET_DETAIL)
elif self.param == "DETAIL_BAKE":
bake_multires_sculpt(context, chr_cache, LAYER_TARGET_DETAIL)
elif self.param == "DETAIL_CLEAN":
clean_multires_sculpt(context, chr_cache, LAYER_TARGET_DETAIL)
if self.param == "BODY_SETUP":
setup_multires_sculpt(context, chr_cache, LAYER_TARGET_SCULPT)
elif self.param == "BODY_BEGIN":
begin_multires_sculpting(context, chr_cache, LAYER_TARGET_SCULPT)
elif self.param == "BODY_END":
end_multires_sculpting(context, chr_cache, LAYER_TARGET_SCULPT)
elif self.param == "BODY_BAKE":
if chr_cache.multires_bake_apply:
bake_multires_sculpt(context, chr_cache, LAYER_TARGET_SCULPT,
apply_shape=True)
else:
bake_multires_sculpt(context, chr_cache, LAYER_TARGET_SCULPT)
elif self.param == "BODY_CLEAN":
clean_multires_sculpt(context, chr_cache, LAYER_TARGET_SCULPT)
elif self.param == "FLATTEN_LAYERS":
flatten_bake_layers(context, chr_cache)
pass
elif self.param == "RESET_FROM_SOURCE":
pass
elif self.param == "STORE_LASH":
meshutils.store_lash_data(chr_cache)
elif self.param == "FIX_LASH":
meshutils.restore_lash_data(chr_cache)
return {"FINISHED"}
@classmethod
def description(cls, context, properties):
if properties.param == "DETAIL_SETUP":
return "Set up and begin detail sculpting for the character.\n\n" \
"Detail sculpting is done on a reduced copy of the character and sculpted normals are baked back and overlayed on the original character.\n\n" \
"Note: This does not make any changes to the mesh of the original character.\n\n" \
"Warning: It is very important that you *do not* apply the base shape yourself in the multi-res modifier"
elif properties.param == "DETAIL_BEGIN":
return "Resume detail sculpting for the character.\n\n" \
"Detail sculpting is done on a reduced copy of the character and sculpted normals are baked back and overlayed on the original character.\n\n" \
"Note: This does not make any changes to the mesh of the original character.\n\n" \
"Warning: It is very important that you *do not* apply the base shape yourself in the multi-res modifier"
elif properties.param == "DETAIL_END":
return "Stop detail sculpting and return to the original character"
elif properties.param == "DETAIL_BAKE":
return "Bake the detail sculpt normals and masks and overlay on the original character.\n\n" \
"The original character's mesh is *not* altered.\n\n" \
"The normal overlays are masked to show only the areas that have been sculpted on, so minor changes due to multi-res subdivision should not cause any additional distortion.\n\n" \
"Once baked, the detail normals can be exported as a separate layer for Skin-Gen in Charactrer Creator"
elif properties.param == "DETAIL_CLEAN":
return "Removes the detail sculpt and normal layers"
elif properties.param == "BODY_SETUP":
return "Set up and begin full body sculpting for the character.\n\n" \
"Body sculpting is done on a reduced copy of the character (Only the Head, Body, Arms and Legs) and sculpted normals are baked back and overlayed on the original character.\n\n" \
"Note: This does not make any changes to the mesh of the original character.\n\n" \
"Warning: It is very important that you *do not* apply the base shape yourself in the multi-res modifier"
elif properties.param == "BODY_BEGIN":
return "Resume full body sculpting for the character.\n\n" \
"Body sculpting is done on a reduced copy of the character (Only the Head, Body, Arms and Legs) and sculpted normals are baked back and overlayed on the original character.\n\n" \
"Note: This does not make any changes to the mesh of the original character.\n\n" \
"Warning: It is very important that you *do not* apply the base shape yourself in the multi-res modifier"
elif properties.param == "BODY_END":
return "Stop body sculpting and return to the original character"
elif properties.param == "BODY_BAKE":
return "Bake the body sculpt normals and masks and overlay on the original character.\n\n" \
"Optionally, the multi-res base shape can by copied back to the original character, in a way that does not destroy the shape-keys.\n\n" \
"The normal overlays are masked to show only the areas that have been sculpted on, so minor changes due to multi-res subdivision should not cause any additional distortion.\n\n" \
"Once baked, the body normals can be exported as a separate layer for Skin-Gen in Charactrer Creator"
elif properties.param == "BODY_CLEAN":
return "Removes the body sculpt and normal layers"
return ""
class CC3OperatorSculptExport(bpy.types.Operator):
"""Export Sculpt Layers"""
bl_idname = "cc3.sculpt_export"
bl_label = "Export Layer"
bl_options = {"REGISTER"}
filepath: bpy.props.StringProperty(
name="File Path",
description="Filepath used for exporting the layers",
maxlen=1024,
subtype='FILE_PATH',
)
filename_ext = ".png" # ExportHelper mixin class uses this
filter_glob: bpy.props.StringProperty(
default="*.png;*.jpg",
options={"HIDDEN"},
)
param: bpy.props.StringProperty(
name = "param",
default = "",
options={"HIDDEN"}
)
def execute(self, context):
props = vars.props()
prefs = vars.prefs()
chr_cache = props.get_context_character_cache(context)
if self.param == "DETAIL_SKINGEN":
export_skingen(context, chr_cache, LAYER_TARGET_DETAIL, self.filepath)
elif self.param == "BODY_SKINGEN":
export_skingen(context, chr_cache, LAYER_TARGET_SCULPT, self.filepath)
return {"FINISHED"}
def invoke(self, context, event):
prefs = vars.prefs()
props = vars.props()
chr_cache = props.get_context_character_cache(context)
export_format = "png"
# determine default file name
if not self.filepath:
default_file_path = context.blend_data.filepath
if not default_file_path:
if self.param == "DETAIL_SKINGEN":
default_file_path = "detail_layer"
else:
default_file_path = "body_layer"
else:
default_file_path = os.path.splitext(default_file_path)[0]
self.filepath = default_file_path + self.filename_ext
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
def check(self, context):
change_ext = False
filepath = self.filepath
if os.path.basename(filepath):
base, ext = os.path.splitext(filepath)
if ext != self.filename_ext:
filepath = bpy.path.ensure_ext(base, self.filename_ext)
else:
filepath = bpy.path.ensure_ext(filepath, self.filename_ext)
if filepath != self.filepath:
self.filepath = filepath
change_ext = True
return change_ext
@classmethod
def description(cls, context, properties):
if properties.param == "DETAIL_SKINGEN":
return "Export the detail sculpt layer normal maps and masks"
elif properties.param == "BODY_SKINGEN":
return "Export the body sculpt normal normal maps and masks"
return ""