1112 lines
49 KiB
Python
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 "" |