2140 lines
77 KiB
Python
2140 lines
77 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 bpy, bmesh
|
|
import os, math, random
|
|
from mathutils import Vector
|
|
from . import springbones, modifiers, geom, utils, jsonutils, bones, meshutils, vars
|
|
|
|
|
|
STROKE_JOIN_THRESHOLD = 1.0 / 100.0 # 1cm
|
|
BONE_SMOOTH_LEVEL_CUSTOM_PROP = "rl_generated_smoothing_level"
|
|
|
|
def begin_hair_sculpt(chr_cache):
|
|
return
|
|
|
|
|
|
def end_hair_sculpt(chr_cache):
|
|
return
|
|
|
|
|
|
def find_obj_cache(chr_cache, obj):
|
|
if chr_cache and obj and obj.type == "MESH":
|
|
# try to find directly
|
|
obj_cache = chr_cache.get_object_cache(obj)
|
|
if obj_cache:
|
|
return obj_cache
|
|
# obj might be part of a split or a copy from original character object
|
|
# so will have the same name but with duplication suffixes
|
|
possible = []
|
|
source_name = utils.strip_name(obj.name)
|
|
for obj_cache in chr_cache.object_cache:
|
|
if obj_cache.is_mesh() and obj_cache.source_name == source_name:
|
|
possible.append(obj_cache)
|
|
# if only one possibility return that
|
|
if possible and len(possible) == 1:
|
|
return possible[0]
|
|
# try to find the correct object cache by matching the materials
|
|
# try matching all the materials first
|
|
for obj_cache in possible:
|
|
o = obj_cache.get_object()
|
|
if o:
|
|
found = True
|
|
for mat in obj.data.materials:
|
|
if mat not in o.data.materials:
|
|
found = False
|
|
if found:
|
|
return obj_cache
|
|
# then try just matching any
|
|
for obj_cache in possible:
|
|
o = obj_cache.get_object()
|
|
if o:
|
|
found = True
|
|
for mat in obj.data.materials:
|
|
if mat in o.data.materials:
|
|
return obj_cache
|
|
return None
|
|
|
|
|
|
def clear_particle_systems(obj):
|
|
if utils.object_mode() and utils.set_only_active_object(obj):
|
|
for i in range(0, len(obj.particle_systems)):
|
|
bpy.ops.object.particle_system_remove()
|
|
return True
|
|
return False
|
|
|
|
|
|
def convert_hair_group_to_particle_systems(obj, curves):
|
|
if clear_particle_systems(obj):
|
|
for c in curves:
|
|
if utils.set_only_active_object(c):
|
|
bpy.ops.curves.convert_to_particle_system()
|
|
|
|
|
|
def export_blender_hair(op, chr_cache, objects, base_path):
|
|
props = vars.props()
|
|
prefs = vars.prefs()
|
|
|
|
utils.expand_with_child_objects(objects)
|
|
|
|
folder, name = os.path.split(base_path)
|
|
file, ext = os.path.splitext(name)
|
|
|
|
parents = []
|
|
for obj in objects:
|
|
if obj.type == "CURVES":
|
|
if obj.parent:
|
|
if obj.parent not in parents:
|
|
parents.append(obj.parent)
|
|
else:
|
|
op.report({'ERROR'}, f"Curve: {obj.data.name} has no parent!")
|
|
|
|
json_data = { "Hair": { "Objects": { } } }
|
|
export_id = 0
|
|
|
|
for parent in parents:
|
|
|
|
groups = {}
|
|
|
|
obj_cache = find_obj_cache(chr_cache, parent)
|
|
|
|
if obj_cache:
|
|
|
|
parent_name = utils.determine_object_export_name(chr_cache, parent, obj_cache)
|
|
|
|
json_data["Hair"]["Objects"][parent_name] = { "Groups": {} }
|
|
|
|
if props.hair_export_group_by == "CURVE":
|
|
for obj in objects:
|
|
if obj.type == "CURVES" and obj.parent == parent:
|
|
group = [obj]
|
|
name = obj.data.name
|
|
groups[name] = group
|
|
utils.log_info(f"Group: {name}, Object: {obj.data.name}")
|
|
|
|
elif props.hair_export_group_by == "NAME":
|
|
for obj in objects:
|
|
if obj.type == "CURVES" and obj.parent == parent:
|
|
name = utils.strip_name(obj.data.name)
|
|
if name not in groups.keys():
|
|
groups[name] = []
|
|
groups[name].append(obj)
|
|
utils.log_info(f"Group: {name}, Object: {obj.data.name}")
|
|
|
|
else: #props.hair_export_group_by == "NONE":
|
|
if "Hair" not in groups.keys():
|
|
groups["Hair"] = []
|
|
for obj in objects:
|
|
if obj.type == "CURVES" and obj.parent == parent:
|
|
groups["Hair"].append(obj)
|
|
utils.log_info(f"Group: Hair, Object: {obj.data.name}")
|
|
|
|
for group_name in groups.keys():
|
|
file_name = f"{file}_{export_id}.abc"
|
|
file_path = os.path.join(folder, file_name)
|
|
export_id += 1
|
|
|
|
convert_hair_group_to_particle_systems(parent, groups[group_name])
|
|
|
|
utils.try_select_objects(groups[group_name], True)
|
|
utils.set_active_object(parent)
|
|
|
|
json_data["Hair"]["Objects"][parent_name]["Groups"][group_name] = { "File": file_name }
|
|
|
|
bpy.ops.wm.alembic_export(
|
|
filepath=file_path,
|
|
check_existing=False,
|
|
global_scale=100.0,
|
|
start=1, end=1,
|
|
use_instancing = False,
|
|
selected=True,
|
|
visible_objects_only=True,
|
|
evaluation_mode = "RENDER",
|
|
packuv=False,
|
|
export_hair=True,
|
|
export_particles=True)
|
|
|
|
clear_particle_systems(parent)
|
|
|
|
else:
|
|
op.report({'ERROR'}, f"Unable to find source mesh object in character for: {parent.name}!")
|
|
|
|
new_json_path = os.path.join(folder, file + ".json")
|
|
jsonutils.write_json(json_data, new_json_path)
|
|
|
|
utils.try_select_objects(objects, True)
|
|
|
|
|
|
def create_curve():
|
|
curve = bpy.data.curves.new("Hair Curve", type="CURVE")
|
|
curve.dimensions = "3D"
|
|
obj = bpy.data.objects.new("Hair Curve", curve)
|
|
bpy.context.collection.objects.link(obj)
|
|
return curve
|
|
|
|
|
|
def create_hair_curves():
|
|
curves = bpy.data.hair_curves.new("Hair Curves")
|
|
obj = bpy.data.objects.new("Hair Curves", curves)
|
|
bpy.context.collection.objects.link(obj)
|
|
return curves
|
|
|
|
|
|
def add_poly_spline(points, curve):
|
|
"""Create a poly curve from a list of Vectors
|
|
"""
|
|
spline : bpy.types.Spline = curve.splines.new("POLY")
|
|
spline.points.add(len(points) - 1)
|
|
for i in range(0, len(points)):
|
|
co = points[i]
|
|
spline.points[i].co = (co.x, co.y, co.z, 1.0)
|
|
|
|
|
|
def card_dir_from_uv_map(card_dirs, uv_map):
|
|
# analyse uv bounds
|
|
uv_min, uv_max = geom.get_uv_bounds(uv_map)
|
|
uv_extent = uv_max - uv_min
|
|
if abs(uv_extent.x) < 0.0001 or abs(uv_extent.y < 0.0001):
|
|
return card_dirs["SQUARE"]
|
|
uv_aspect = uv_extent.x / uv_extent.y
|
|
|
|
# only deal with vertical or horizontal cards
|
|
# squarish cards are patches of hair that shouldn't be weighted
|
|
if uv_aspect >= 2.0:
|
|
uv_orient = "HORIZONTAL"
|
|
elif uv_aspect <= 0.5:
|
|
uv_orient = "VERTICAL"
|
|
else:
|
|
uv_orient = "SQUARE"
|
|
return card_dirs[uv_orient]
|
|
|
|
|
|
def parse_loop(bm, edge_index, edges_left, loop, edge_map):
|
|
"""Returns a set of vertex indices in the edge loop
|
|
"""
|
|
if edge_index in edges_left:
|
|
edges_left.remove(edge_index)
|
|
edge = bm.edges[edge_index]
|
|
loop.add(edge.verts[0].index)
|
|
loop.add(edge.verts[1].index)
|
|
if edge.index in edge_map:
|
|
for ce in edge_map[edge.index]:
|
|
parse_loop(bm, ce, edges_left, loop, edge_map)
|
|
|
|
|
|
def sort_func_u(vert_uv_pair):
|
|
return vert_uv_pair[-1].x
|
|
|
|
|
|
def sort_func_v(vert_uv_pair):
|
|
return vert_uv_pair[-1].y
|
|
|
|
|
|
def sort_verts_by_uv(obj, bm, loop, uv_map, dir):
|
|
sorted = []
|
|
for vert in loop:
|
|
uv = uv_map[vert]
|
|
sorted.append([vert, uv])
|
|
if dir.x > 0:
|
|
sorted.sort(reverse=False, key=sort_func_u)
|
|
elif dir.x < 0:
|
|
sorted.sort(reverse=True, key=sort_func_u)
|
|
elif dir.y > 0:
|
|
sorted.sort(reverse=False, key=sort_func_v)
|
|
else:
|
|
sorted.sort(reverse=True, key=sort_func_v)
|
|
return [ obj.matrix_world @ bm.verts[v].co for v, uv in sorted]
|
|
|
|
|
|
def get_ordered_coordinate_loops(obj, bm, edges, dir, uv_map, edge_map):
|
|
edges_left = set(edges)
|
|
loops = []
|
|
|
|
# separate edges into vertex loops
|
|
while len(edges_left) > 0:
|
|
loop = set()
|
|
edge_index = list(edges_left)[0]
|
|
parse_loop(bm, edge_index, edges_left, loop, edge_map)
|
|
sorted = sort_verts_by_uv(obj, bm, loop, uv_map, dir)
|
|
loops.append(sorted)
|
|
|
|
return loops
|
|
|
|
|
|
def get_vert_loops(obj, bm, edges, edge_map):
|
|
edges_left = set(edges)
|
|
vert_loops = []
|
|
|
|
# separate edges into vertex loops
|
|
while len(edges_left) > 0:
|
|
loop = set()
|
|
edge_index = list(edges_left)[0]
|
|
parse_loop(bm, edge_index, edges_left, loop, edge_map)
|
|
verts = [ index for index in loop]
|
|
vert_loops.append(verts)
|
|
|
|
return vert_loops
|
|
|
|
|
|
def merge_length_coordinate_loops(loops):
|
|
|
|
size = len(loops[0])
|
|
|
|
for merged in loops:
|
|
if len(merged) != size:
|
|
return None
|
|
|
|
num = len(loops)
|
|
merged = []
|
|
|
|
for i in range(0, size):
|
|
co = Vector((0,0,0))
|
|
for l in range(0, num):
|
|
co += loops[l][i]
|
|
co /= num
|
|
merged.append(co)
|
|
|
|
return merged
|
|
|
|
|
|
def sort_lateral_card(obj, bm, loops, uv_map, dir):
|
|
|
|
sorted = []
|
|
card = {}
|
|
|
|
for loop in loops:
|
|
co = Vector((0,0,0))
|
|
uv = Vector((0,0))
|
|
count = 0
|
|
for vert_index in loop:
|
|
co += obj.matrix_world @ bm.verts[vert_index].co
|
|
uv += uv_map[vert_index]
|
|
count += 1
|
|
co /= count
|
|
uv /= count
|
|
sorted.append([co, loop, uv])
|
|
|
|
if dir.x > 0:
|
|
sorted.sort(reverse=False, key=sort_func_u)
|
|
elif dir.x < 0:
|
|
sorted.sort(reverse=True, key=sort_func_u)
|
|
elif dir.y > 0:
|
|
sorted.sort(reverse=False, key=sort_func_v)
|
|
else:
|
|
sorted.sort(reverse=True, key=sort_func_v)
|
|
|
|
card["median"] = [ co for co, loop, uv in sorted ]
|
|
card["loops"] = [ loop for co, loop, uv in sorted ]
|
|
return card
|
|
|
|
|
|
def grid_to_loops(obj, bm, island, card_dirs, one_loop_per_card):
|
|
props = vars.props()
|
|
|
|
# each island has a unique UV map
|
|
uv_map = geom.get_uv_island_map(bm, 0, island)
|
|
|
|
card_dir = card_dir_from_uv_map(card_dirs, uv_map)
|
|
|
|
# get all edges aligned with the card dir in the island
|
|
edges = geom.get_uv_aligned_edges(bm, island, card_dir, uv_map, dir_threshold=props.hair_card_dir_threshold)
|
|
utils.log_info(f"{len(edges)} aligned edges.")
|
|
|
|
# map connected edges
|
|
edge_map = geom.get_linked_edge_map(bm, edges)
|
|
|
|
# separate into ordered vertex loops
|
|
loops = get_ordered_coordinate_loops(obj, bm, edges, card_dir, uv_map, edge_map)
|
|
|
|
utils.log_info(f"{len(loops)} ordered loops.")
|
|
|
|
# (merge and) generate poly curves
|
|
if one_loop_per_card:
|
|
loop = merge_length_coordinate_loops(loops)
|
|
if loop:
|
|
return [loop]
|
|
else:
|
|
utils.log_info("Loops have differing lengths, grid extraction failed.")
|
|
return None
|
|
else:
|
|
return loops
|
|
|
|
return True
|
|
|
|
|
|
def get_vert_loop_to(obj, bm, from_index, to_index, edges, reverse = False):
|
|
verts = []
|
|
co_loop = []
|
|
following = False
|
|
for edge_index in edges:
|
|
if edge_index == from_index:
|
|
following = True
|
|
if edge_index == to_index:
|
|
following = False
|
|
if following:
|
|
edge = bm.edges[edge_index]
|
|
for vert in edge.verts:
|
|
if vert.index not in verts:
|
|
verts.append(vert.index)
|
|
if reverse:
|
|
co_loop.insert(0, obj.matrix_world @ vert.co)
|
|
else:
|
|
co_loop.append(obj.matrix_world @ vert.co)
|
|
return verts, co_loop
|
|
|
|
|
|
def sort_boundary_edges(bm, edges : set, start_index):
|
|
edge = bm.edges[start_index]
|
|
vert = edge.verts[1]
|
|
edge_loop = [start_index]
|
|
edges.remove(start_index)
|
|
following = True
|
|
while following:
|
|
following = False
|
|
for next_edge in vert.link_edges:
|
|
if next_edge != edge and next_edge.index in edges:
|
|
edge_loop.append(next_edge.index)
|
|
edges.remove(next_edge.index)
|
|
for next_vert in next_edge.verts:
|
|
if next_vert.index != vert.index:
|
|
edge = next_edge
|
|
vert = next_vert
|
|
following = True
|
|
break
|
|
if following:
|
|
break
|
|
return edge_loop
|
|
|
|
|
|
def split_boundary_loops(obj, bm, boundary_edges, card_dir, uv_map):
|
|
edge : bmesh.types.BMEdge
|
|
min_proj = math.inf
|
|
max_proj = -math.inf
|
|
list_edges = list(boundary_edges)
|
|
min_edge_index = list_edges[0]
|
|
max_edge_index = list_edges[-1]
|
|
for edge_index in boundary_edges:
|
|
edge = bm.edges[edge_index]
|
|
vert = edge.verts[0]
|
|
uv = uv_map[vert.index]
|
|
proj = -card_dir.dot(uv)
|
|
if proj < min_proj:
|
|
min_proj = proj
|
|
min_edge_index = edge.index
|
|
if proj > max_proj:
|
|
max_proj = proj
|
|
max_edge_index = edge.index
|
|
# sort the boundary edges in order, starting from max_edge (the top most UV edge)
|
|
num_edges = len(boundary_edges)
|
|
edge_loop = sort_boundary_edges(bm, boundary_edges, max_edge_index)
|
|
# return nothing if the sorted boundary edge does not contain all the edges
|
|
# (i.e. there are breaks in the edges)
|
|
if len(edge_loop) != num_edges:
|
|
utils.log_info(f"Unable to sort boundary edges: {len(edge_loop)} != {num_edges}")
|
|
return None, None
|
|
# extract the left and right coordinate loops
|
|
left_verts, left_coords = get_vert_loop_to(obj, bm, max_edge_index, min_edge_index, edge_loop)
|
|
right_verts, right_coords = get_vert_loop_to(obj, bm, min_edge_index, max_edge_index, edge_loop, reverse=True)
|
|
# need to reverse the order of one of these... but which one
|
|
|
|
return left_coords, right_coords
|
|
|
|
|
|
def get_projection_on_loop(loop, co):
|
|
p0 = loop[0]
|
|
min_distance = math.inf
|
|
projected_point = p0
|
|
projected_length = 0.0
|
|
length = 0.0
|
|
for i in range(1, len(loop)):
|
|
p1 = loop[i]
|
|
segment_length = (p1 - p0).length
|
|
dist, fac = distance_from_line(co, p0, p1)
|
|
if dist < min_distance:
|
|
min_distance = dist
|
|
projected_point = p0 * (1.0 - fac) + p1 * fac
|
|
projected_length = length + segment_length * fac
|
|
length += segment_length
|
|
p0 = p1
|
|
return projected_point, projected_length
|
|
|
|
|
|
def proj_loop_sort_func(co_len_pair):
|
|
return co_len_pair[1]
|
|
|
|
|
|
def project_boundary_loop(src_loop, dst_loop):
|
|
"""Projects the source loop onto the destination loop."""
|
|
sort_points = []
|
|
# add the original points & lengths
|
|
for i in range(0, len(dst_loop)):
|
|
sort_points.append([dst_loop[i], loop_length(dst_loop, i)])
|
|
# add the projected points & lengths
|
|
for co in src_loop:
|
|
projected_point, projected_length = get_projection_on_loop(dst_loop, co)
|
|
sort_points.append([projected_point, projected_length])
|
|
# sort by length
|
|
sort_points.sort(key=proj_loop_sort_func)
|
|
# return the coordinate loop
|
|
loop = [ pair[0] for pair in sort_points ]
|
|
return loop
|
|
|
|
|
|
def mesh_to_loops(obj, bm, island, card_dirs):
|
|
props = vars.props()
|
|
|
|
# each island has a unique UV map
|
|
uv_map = geom.get_uv_island_map(bm, 0, island)
|
|
|
|
card_dir = card_dir_from_uv_map(card_dirs, uv_map)
|
|
|
|
# find the boundary edges
|
|
boundary_edges = geom.get_boundary_edges(bm, island)
|
|
|
|
# check for minimum bound edges
|
|
if len(boundary_edges) < 4:
|
|
return None
|
|
|
|
# the top most UV in the boundary edge is the start of the left hand side
|
|
# the bottom most UV in the boundary edge is the end of the right hand side
|
|
# split the boundary edge into two loops left and right
|
|
left_loop, right_loop = split_boundary_loops(obj, bm, boundary_edges, card_dir, uv_map)
|
|
|
|
if left_loop and right_loop:
|
|
|
|
# project each vertex in loop_left into loop_right, order by projected length
|
|
projected_right_loop = project_boundary_loop(left_loop, right_loop)
|
|
|
|
# project each vertex in loop_right into loop_left, order by projected length
|
|
projected_left_loop = project_boundary_loop(right_loop, left_loop)
|
|
|
|
# the two loops should now be the same length and each index in the loops represents
|
|
# a point in one loop and/or it's projection in the other
|
|
# now average the loops into one loop representing the mesh hair card
|
|
loop = merge_length_coordinate_loops([projected_left_loop, projected_right_loop])
|
|
|
|
if loop:
|
|
return [loop]
|
|
|
|
utils.log_info("Loops have differing lengths or breaks, mesh extraction failed.")
|
|
return None
|
|
|
|
|
|
def selected_cards_to_length_loops(chr_cache, obj, card_dirs, one_loop_per_card = True, card_selection_mode = "SELECTED"):
|
|
prefs = vars.prefs()
|
|
props = vars.props()
|
|
|
|
# select linked and set to edge mode
|
|
utils.edit_mode_to(obj, only_this=True)
|
|
if card_selection_mode == "ALL":
|
|
bpy.ops.mesh.select_all(action='SELECT')
|
|
else:
|
|
bpy.ops.mesh.select_linked(delimit={'UV'})
|
|
bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='FACE')
|
|
|
|
# object mode to save edit changes
|
|
utils.object_mode_to(obj)
|
|
|
|
deselect_invalid_materials(chr_cache, obj)
|
|
|
|
# get the bmesh
|
|
mesh = obj.data
|
|
bm = geom.get_bmesh(mesh)
|
|
|
|
# get lists of the faces in each selected island
|
|
islands = geom.get_uv_islands(bm, 0, use_selected=True)
|
|
|
|
utils.log_info(f"{len(islands)} islands selected.")
|
|
|
|
all_loops = []
|
|
cards = []
|
|
|
|
for island in islands:
|
|
|
|
utils.log_info(f"Processing island, faces: {len(island)}")
|
|
utils.log_indent()
|
|
|
|
is_grid = geom.is_island_grid(bm, island)
|
|
loops = None
|
|
if is_grid:
|
|
loops = grid_to_loops(obj, bm, island, card_dirs, one_loop_per_card)
|
|
|
|
if not loops:
|
|
is_grid = False
|
|
loops = mesh_to_loops(obj, bm, island, card_dirs)
|
|
|
|
if is_grid:
|
|
utils.log_info("Grid")
|
|
else:
|
|
utils.log_info("Polymesh")
|
|
|
|
face : bmesh.types.BMFace
|
|
verts = set()
|
|
for face_index in island:
|
|
face = bm.faces[face_index]
|
|
for vert in face.verts:
|
|
verts.add(vert.index)
|
|
|
|
card = { "verts": verts, "loops": loops }
|
|
cards.append(card)
|
|
|
|
utils.log_recess()
|
|
|
|
return cards, bm
|
|
|
|
|
|
def debug_loop(loop):
|
|
curve = create_curve()
|
|
add_poly_spline(loop, curve)
|
|
|
|
|
|
def selected_cards_to_curves(chr_cache, obj, card_dirs, one_loop_per_card = True):
|
|
curve = create_curve()
|
|
cards, bm = selected_cards_to_length_loops(chr_cache, obj, card_dirs, one_loop_per_card)
|
|
for card in cards:
|
|
loops = card["loops"]
|
|
for loop in loops:
|
|
add_poly_spline(loop, curve)
|
|
|
|
# TODO
|
|
# Put the curve object to the same scale as the body mesh
|
|
# With roots above the scalp plant the root of the curves into the scalp? (within tolerance)
|
|
# or add an new root point on the scalp...
|
|
# With roots below the scalp, crop the loop
|
|
# convert to curves
|
|
# set curve render subdivision to at least 2
|
|
# snap curves to surface
|
|
|
|
|
|
def loop_length(loop, index = -1):
|
|
if index == -1:
|
|
index = len(loop) - 1
|
|
p0 = loop[0]
|
|
d = 0
|
|
for i in range(1, index + 1):
|
|
p1 = loop[i]
|
|
d += (p1 - p0).length
|
|
p0 = p1
|
|
return d
|
|
|
|
|
|
def eval_loop_at(loop, length, fac):
|
|
p0 = loop[0]
|
|
f0 = 0
|
|
for i in range(1, len(loop)):
|
|
p1 = loop[i]
|
|
v = p1 - p0
|
|
fl = v.length / length
|
|
f1 = f0 + fl
|
|
if fl > 0 and fac <= f1 and fac >= f0:
|
|
df = fac - f0
|
|
return p0 + v * (df / fl)
|
|
f0 = f1
|
|
p0 = p1
|
|
f1 += fl
|
|
return p0
|
|
|
|
|
|
def is_on_loop(co, loop, threshold = 0.001):
|
|
"""Is the coordinate on the loop.
|
|
(All coordintes should be in world space)"""
|
|
p0 = loop[0]
|
|
min_distance = threshold + 1.0
|
|
for i in range(1, len(loop)):
|
|
p1 = loop[i]
|
|
dist, fac = distance_from_line(co, p0, p1)
|
|
if dist < min_distance:
|
|
min_distance = dist
|
|
if min_distance < threshold:
|
|
return True
|
|
p0 = p1
|
|
return min_distance < threshold
|
|
|
|
|
|
def clear_hair_bone_weights(chr_cache, arm, objects, card_mode, bone_mode, parent_mode):
|
|
utils.object_mode_to(arm)
|
|
|
|
bone_chain_defs = get_bone_chain_defs(chr_cache, arm, bone_mode, parent_mode)
|
|
|
|
hair_bones = []
|
|
for bone_chain in bone_chain_defs:
|
|
for bone_def in bone_chain:
|
|
hair_bones.append(bone_def["name"])
|
|
|
|
if not objects:
|
|
objects = meshutils.get_child_objects_with_vertex_groups(arm, hair_bones)
|
|
|
|
for obj in objects:
|
|
remove_hair_bone_weights(obj, hair_bones, card_mode)
|
|
|
|
arm.data.pose_position = "POSE"
|
|
utils.pose_mode_to(arm)
|
|
|
|
|
|
def remove_hair_bones(chr_cache, arm, bone_mode, parent_mode):
|
|
utils.object_mode_to(arm)
|
|
|
|
hair_bones = []
|
|
bone_chain_defs = get_bone_chain_defs(chr_cache, arm, bone_mode, parent_mode)
|
|
for bone_chain in bone_chain_defs:
|
|
for bone_def in bone_chain:
|
|
hair_bones.append(bone_def["name"])
|
|
|
|
# remove the bones in edit mode
|
|
if hair_bones and utils.edit_mode_to(arm, True):
|
|
for bone_name in hair_bones:
|
|
arm.data.edit_bones.remove(arm.data.edit_bones[bone_name])
|
|
|
|
# remove the spring rig if there are no child bones left
|
|
if utils.edit_mode_to(arm):
|
|
spring_rig = springbones.get_spring_rig(chr_cache, arm, parent_mode, mode = "EDIT")
|
|
if spring_rig and not spring_rig.children:
|
|
arm.data.edit_bones.remove(spring_rig)
|
|
|
|
#use all mesh objects in the character with matching vertex groups
|
|
objects = meshutils.get_child_objects_with_vertex_groups(arm, hair_bones)
|
|
|
|
#remove the weights from the character meshes
|
|
for obj in objects:
|
|
remove_hair_bone_weights(obj, hair_bones, "ALL")
|
|
|
|
utils.object_mode_to(arm)
|
|
|
|
|
|
def rename_hair_bones(chr_cache, arm, base_name, parent_mode):
|
|
utils.object_mode_to(arm)
|
|
|
|
bone_remap = {}
|
|
bone_chain_defs = None
|
|
|
|
hair_bones = []
|
|
bone_chain_defs = get_bone_chain_defs(chr_cache, arm, "SELECTED", parent_mode)
|
|
for bone_chain in bone_chain_defs:
|
|
for bone_def in bone_chain:
|
|
hair_bones.append(bone_def["name"])
|
|
|
|
utils.edit_mode_to(arm)
|
|
loop_index = 1
|
|
for bone_chain in bone_chain_defs:
|
|
loop_index = find_unused_hair_bone_index(arm, loop_index, base_name)
|
|
chain_index = 0
|
|
for bone_def in bone_chain:
|
|
old_name = bone_def["name"]
|
|
new_name = f"{base_name}_{loop_index}_{chain_index}"
|
|
edit_bone : bpy.types.EditBone
|
|
edit_bone = arm.data.edit_bones[old_name]
|
|
edit_bone.name = new_name
|
|
edit_bone.select = True
|
|
bone_remap[old_name] = edit_bone.name
|
|
chain_index += 1
|
|
|
|
utils.object_mode_to(arm)
|
|
|
|
#if no objects selected, use all mesh objects in the character with matching vertex groups
|
|
objects = meshutils.get_child_objects_with_vertex_groups(arm, hair_bones)
|
|
|
|
# now rename the vertex groups in all the objects...
|
|
for obj in objects:
|
|
for vg in obj.vertex_groups:
|
|
if vg.name in bone_remap:
|
|
vg.name = bone_remap[vg.name]
|
|
|
|
|
|
def contains_hair_bone_chain(arm, loop_index, prefix):
|
|
"""Edit mode"""
|
|
for bone in arm.data.edit_bones:
|
|
if bone.name.startswith(f"{prefix}_{loop_index}_"):
|
|
return True
|
|
return False
|
|
|
|
|
|
def find_unused_hair_bone_index(arm, loop_index, prefix):
|
|
"""Edit mode"""
|
|
while contains_hair_bone_chain(arm, loop_index, prefix):
|
|
loop_index += 1
|
|
return loop_index
|
|
|
|
|
|
def is_nearby_bone(arm, world_pos):
|
|
"""Edit mode"""
|
|
for edit_bone in arm.data.edit_bones:
|
|
length = (world_pos - arm.matrix_world @ edit_bone.head).length
|
|
if length < 0.01:
|
|
return True
|
|
return False
|
|
|
|
|
|
def custom_bone(chr_cache, arm, parent_mode, loop_index, bone_length, new_bones):
|
|
"""Must be in edit mode on the armature."""
|
|
|
|
props = vars.props()
|
|
|
|
hair_rig = springbones.get_spring_rig(chr_cache, arm, parent_mode, create_if_missing=True, mode = "EDIT")
|
|
|
|
hair_bone_prefix = props.hair_rig_group_name
|
|
|
|
if hair_rig:
|
|
|
|
parent_bone = hair_rig
|
|
|
|
bone_name = f"{hair_bone_prefix}_{loop_index}_0"
|
|
edit_bone : bpy.types.EditBone = bones.new_edit_bone(arm, bone_name, parent_bone.name)
|
|
new_bones.append(bone_name)
|
|
edit_bone.select = True
|
|
edit_bone.select_head = True
|
|
edit_bone.select_tail = True
|
|
world_origin = arm.matrix_world @ hair_rig.head
|
|
world_pos = world_origin + Vector((0, 0.05, 0.15))
|
|
while is_nearby_bone(arm, world_pos):
|
|
world_pos += Vector((0, 0.0175, 0))
|
|
world_head = world_pos
|
|
world_tail = world_pos + Vector((0, 0, bone_length))
|
|
edit_bone.head = arm.matrix_world.inverted() @ world_head
|
|
edit_bone.tail = arm.matrix_world.inverted() @ world_tail
|
|
bone_z = (((world_head + world_tail) * 0.5) - world_origin).normalized()
|
|
edit_bone.align_roll(bone_z)
|
|
# set bone layer to 25, so we can show only the added hair bones 'in front'
|
|
bones.set_bone_collection(arm, edit_bone, "Spring (Edit)", None, vars.SPRING_EDIT_LAYER)
|
|
# don't directly connect first bone in a chain
|
|
edit_bone.use_connect = False
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def get_linked_bones(edit_bone, bone_list):
|
|
if edit_bone.name not in bone_list:
|
|
bone_list.append(edit_bone.name)
|
|
for child_bone in edit_bone.children:
|
|
get_linked_bones(child_bone, bone_list)
|
|
return bone_list
|
|
|
|
|
|
def bone_chains_match(arm, bone_list_a, bone_list_b, tolerance = 0.001):
|
|
|
|
tolerance /= ((arm.scale[0] + arm.scale[1] + arm.scale[2]) / 3.0)
|
|
|
|
for bone_name_a in bone_list_a:
|
|
edit_bone_a = arm.data.edit_bones[bone_name_a]
|
|
has_match = False
|
|
for bone_name_b in bone_list_b:
|
|
edit_bone_b = arm.data.edit_bones[bone_name_b]
|
|
delta = (edit_bone_a.head - edit_bone_b.head).length + (edit_bone_a.tail - edit_bone_b.tail).length
|
|
if (delta < tolerance):
|
|
has_match = True
|
|
if not has_match:
|
|
return False
|
|
return True
|
|
|
|
|
|
def bone_chain_matches_loop(arm, bone_list, loop, threshold = 0.001):
|
|
for bone_name in bone_list:
|
|
if bone_name in arm.data.edit_bones:
|
|
edit_bone = arm.data.edit_bones[bone_name]
|
|
if not is_on_loop(arm.matrix_world @ edit_bone.head, loop, threshold):
|
|
return False
|
|
if not is_on_loop(arm.matrix_world @ edit_bone.tail, loop, threshold):
|
|
return False
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
|
|
def remove_existing_loop_bones(chr_cache, arm, smoothed_loops):
|
|
"""Removes any bone chains in the hair rig that align with the loops"""
|
|
|
|
props = vars.props()
|
|
bone_selection_mode = props.hair_rig_bind_bone_mode
|
|
|
|
if bone_selection_mode == "SELECTED":
|
|
# select all linked bones
|
|
utils.edit_mode_to(arm)
|
|
bpy.ops.armature.select_linked()
|
|
utils.object_mode_to(arm)
|
|
|
|
utils.edit_mode_to(arm)
|
|
|
|
hair_rigs = springbones.get_spring_rigs(chr_cache, arm, ["HEAD", "JAW"], mode="EDIT")
|
|
|
|
remove_bone_list = []
|
|
remove_loop_set_list = []
|
|
removed_roots = []
|
|
|
|
for parent_mode in hair_rigs:
|
|
|
|
hair_rig = hair_rigs[parent_mode]["bone"]
|
|
if hair_rig:
|
|
|
|
for chain_root in hair_rig.children:
|
|
chain_root: bpy.types.EditBone
|
|
if chain_root not in removed_roots:
|
|
chain_bones = get_linked_bones(chain_root, [])
|
|
for smoothed_loop_set in smoothed_loops:
|
|
bone_smooth_level = 0
|
|
if BONE_SMOOTH_LEVEL_CUSTOM_PROP in chain_root:
|
|
bone_smooth_level = chain_root[BONE_SMOOTH_LEVEL_CUSTOM_PROP]
|
|
bone_smooth_loop = smoothed_loop_set[bone_smooth_level]
|
|
# compare the bone chain with the loop at it's generated smoothing level
|
|
if bone_chain_matches_loop(arm, chain_bones, bone_smooth_loop, 0.001):
|
|
remove_bones = False
|
|
remove_loop = False
|
|
if bone_selection_mode == "SELECTED":
|
|
if bones.get_bone_selected(arm, chain_root):
|
|
# if the chain is selected, then it is to be replaced, so remove it.
|
|
remove_bones = True
|
|
else:
|
|
# otherwise remove the loop, so it won't generate new bones over the existing bones.
|
|
remove_loop = True
|
|
else:
|
|
remove_bones = True
|
|
|
|
if remove_bones:
|
|
utils.log_info(f"Existing bone chain starting: {chain_root.name} is to be re-generated.")
|
|
remove_bone_list.extend(chain_bones)
|
|
removed_roots.append(chain_root)
|
|
if remove_loop:
|
|
utils.log_info(f"Existing bone chain starting: {chain_root.name} will not be replaced.")
|
|
remove_loop_set_list.append(smoothed_loop_set)
|
|
|
|
if remove_bone_list:
|
|
for bone_name in remove_bone_list:
|
|
if bone_name in arm.data.edit_bones:
|
|
utils.log_info(f"Removing bone on generating loop: {bone_name}")
|
|
arm.data.edit_bones.remove(arm.data.edit_bones[bone_name])
|
|
else:
|
|
utils.log_info(f"Already deleted: {bone_name} ?")
|
|
|
|
if remove_loop_set_list:
|
|
for smoothed_loop_set in remove_loop_set_list:
|
|
if smoothed_loop_set in smoothed_loops:
|
|
smoothed_loops.remove(smoothed_loop_set)
|
|
utils.log_info(f"Removing loop from generation list")
|
|
|
|
return
|
|
|
|
|
|
def remove_duplicate_bones(chr_cache, arm):
|
|
"""Remove any duplicate bone chains"""
|
|
|
|
remove_list = []
|
|
removed_roots = []
|
|
|
|
utils.edit_mode_to(arm)
|
|
|
|
hair_rigs = springbones.get_spring_rigs(chr_cache, arm, ["HEAD", "JAW"], mode = "EDIT")
|
|
|
|
for parent_mode in hair_rigs:
|
|
|
|
hair_rig = hair_rigs[parent_mode]["bone"]
|
|
if hair_rig:
|
|
|
|
for chain_root in hair_rig.children:
|
|
if chain_root not in removed_roots:
|
|
chain_bones = get_linked_bones(chain_root, [])
|
|
for i in range(len(hair_rig.children)-1, 0, -1):
|
|
test_chain_root = hair_rig.children[i]
|
|
if test_chain_root not in removed_roots:
|
|
test_chain_bones = get_linked_bones(test_chain_root, [])
|
|
if chain_root == test_chain_root:
|
|
break
|
|
if bone_chains_match(arm, test_chain_bones, chain_bones, 0.001):
|
|
remove_list.extend(test_chain_bones)
|
|
removed_roots.append(test_chain_root)
|
|
|
|
if remove_list:
|
|
for bone_name in remove_list:
|
|
if bone_name in arm.data.edit_bones:
|
|
utils.log_info(f"Removing duplicate bone: {bone_name}")
|
|
arm.data.edit_bones.remove(arm.data.edit_bones[bone_name])
|
|
else:
|
|
utils.log_info(f"Already deleted: {bone_name} ?")
|
|
|
|
# object mode to save changes to edit bones
|
|
utils.object_mode_to(arm)
|
|
|
|
return
|
|
|
|
|
|
def loop_to_bones(chr_cache, arm, parent_mode, loop, loop_index, bone_length,
|
|
skip_length, trunc_length, smooth_level, new_bones):
|
|
"""Generate hair rig bones from vertex loops. Must be in edit mode on armature."""
|
|
|
|
props = vars.props()
|
|
|
|
if len(loop) < 2:
|
|
return False
|
|
|
|
length = loop_length(loop)
|
|
|
|
# maximum skip length of half the length
|
|
skip_length = min(skip_length, length / 2.0)
|
|
# maximum trunc length of half the remaining length
|
|
trunc_length = min(trunc_length, (length - skip_length) / 2.0)
|
|
|
|
# skip zero length loops
|
|
if length < 0.001:
|
|
return False
|
|
|
|
segments = max(1, round((length - skip_length - trunc_length) / bone_length))
|
|
|
|
fac = skip_length / length
|
|
max_fac = (length - trunc_length) / length
|
|
df = (max_fac - fac) / segments
|
|
chain = []
|
|
first = True
|
|
|
|
hair_rig = springbones.get_spring_rig(chr_cache, arm, parent_mode, create_if_missing=True, mode = "EDIT")
|
|
|
|
hair_bone_prefix = props.hair_rig_group_name
|
|
|
|
if hair_rig:
|
|
|
|
parent_bone = hair_rig
|
|
|
|
for s in range(0, segments):
|
|
bone_name = f"{hair_bone_prefix}_{loop_index}_{s}"
|
|
edit_bone : bpy.types.EditBone = bones.new_edit_bone(arm, bone_name, parent_bone.name)
|
|
edit_bone[BONE_SMOOTH_LEVEL_CUSTOM_PROP] = smooth_level
|
|
new_bones.append(bone_name)
|
|
edit_bone.select = True
|
|
edit_bone.select_head = True
|
|
edit_bone.select_tail = True
|
|
world_head = eval_loop_at(loop, length, fac)
|
|
world_tail = eval_loop_at(loop, length, fac + df)
|
|
edit_bone.head = arm.matrix_world.inverted() @ world_head
|
|
edit_bone.tail = arm.matrix_world.inverted() @ world_tail
|
|
world_origin = arm.matrix_world @ hair_rig.head
|
|
bone_z = (((world_head + world_tail) * 0.5) - world_origin).normalized()
|
|
edit_bone.align_roll(bone_z)
|
|
parent_bone = edit_bone
|
|
# set bone layer to 25, so we can show only the added hair bones 'in front'
|
|
bones.set_bone_collection(arm, edit_bone, "Spring (Edit)", None, vars.SPRING_EDIT_LAYER)
|
|
chain.append(bone_name)
|
|
if first:
|
|
edit_bone.use_connect = False
|
|
first = False
|
|
else:
|
|
edit_bone.use_connect = True
|
|
first = False
|
|
fac += df
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def get_smoothed_loops_set(loops):
|
|
smoothed_loops_set = []
|
|
for loop in loops:
|
|
smoothed_loops_set.append(generate_smoothed_loop_levels(loop))
|
|
return smoothed_loops_set
|
|
|
|
|
|
def selected_cards_to_bones(chr_cache, arm, obj, parent_mode, card_dirs,
|
|
one_loop_per_card = True, bone_length = 0.075,
|
|
skip_length = 0.075, trunc_length = 0.0, smooth_level = 0):
|
|
"""Lengths in world space units (m)."""
|
|
|
|
props = vars.props()
|
|
|
|
mode_selection = utils.store_mode_selection_state()
|
|
arm_pose = set_rest_pose(arm)
|
|
|
|
springbones.realign_spring_bones_axis(chr_cache, arm)
|
|
|
|
springbones.show_spring_bone_edit_layer(chr_cache, arm, True)
|
|
|
|
hair_bone_prefix = props.hair_rig_group_name
|
|
|
|
# check anchor bone exists...
|
|
anchor_bone_name = springbones.get_spring_anchor_name(chr_cache, arm, parent_mode)
|
|
anchor_bone = bones.get_pose_bone(arm, anchor_bone_name)
|
|
if anchor_bone:
|
|
cards, bm = selected_cards_to_length_loops(chr_cache, obj, card_dirs, one_loop_per_card)
|
|
utils.edit_mode_to(arm)
|
|
smoothed_loops_set = []
|
|
for card in cards:
|
|
loops = card["loops"]
|
|
if loops:
|
|
card_smoothed_loops_set = get_smoothed_loops_set(loops)
|
|
smoothed_loops_set.extend(card_smoothed_loops_set)
|
|
remove_existing_loop_bones(chr_cache, arm, smoothed_loops_set)
|
|
for edit_bone in arm.data.edit_bones:
|
|
edit_bone.select_head = False
|
|
edit_bone.select_tail = False
|
|
edit_bone.select = False
|
|
loop_index = 1
|
|
new_bones = []
|
|
for smoothed_loop in smoothed_loops_set:
|
|
loop = smoothed_loop[smooth_level]
|
|
loop_index = find_unused_hair_bone_index(arm, loop_index, hair_bone_prefix)
|
|
if loop_to_bones(chr_cache, arm, parent_mode, loop, loop_index,
|
|
bone_length, skip_length, trunc_length, smooth_level, new_bones):
|
|
loop_index += 1
|
|
|
|
remove_duplicate_bones(chr_cache, arm)
|
|
|
|
utils.object_mode_to(arm)
|
|
|
|
restore_pose(arm, arm_pose)
|
|
utils.restore_mode_selection_state(mode_selection)
|
|
utils.try_select_object(arm)
|
|
|
|
|
|
def get_hair_cards_lateral(chr_cache, obj, card_dirs, card_selection_mode):
|
|
prefs = vars.prefs()
|
|
props = vars.props()
|
|
|
|
# select linked and set to edge mode
|
|
utils.edit_mode_to(obj, only_this=True)
|
|
if card_selection_mode == "ALL":
|
|
bpy.ops.mesh.select_all(action='SELECT')
|
|
else:
|
|
bpy.ops.mesh.select_linked(delimit={'UV'})
|
|
|
|
bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='FACE')
|
|
|
|
# object mode to save edit changes
|
|
utils.object_mode_to(obj)
|
|
|
|
deselect_invalid_materials(chr_cache, obj)
|
|
|
|
# get the bmesh
|
|
mesh = obj.data
|
|
bm = geom.get_bmesh(mesh)
|
|
|
|
# get lists of the faces in each selected island
|
|
islands = geom.get_uv_islands(bm, 0, use_selected=True)
|
|
|
|
utils.log_info(f"{len(islands)} islands selected.")
|
|
|
|
cards = []
|
|
|
|
for island in islands:
|
|
|
|
utils.log_info(f"Processing island, faces: {len(island)}")
|
|
utils.log_indent()
|
|
|
|
# each island has a unique UV map
|
|
uv_map = geom.get_uv_island_map(bm, 0, island)
|
|
|
|
card_dir = card_dir_from_uv_map(card_dirs, uv_map)
|
|
|
|
# get all edges NOT aligned with the card dir in the island, i.e. the lateral edges
|
|
edges = geom.get_uv_aligned_edges(bm, island, card_dir, uv_map,
|
|
get_non_aligned=True, dir_threshold=props.hair_card_dir_threshold)
|
|
|
|
utils.log_info(f"{len(edges)} non-aligned edges.")
|
|
|
|
# map connected edges
|
|
edge_map = geom.get_linked_edge_map(bm, edges)
|
|
|
|
# separate into lateral vertex loops
|
|
vert_loops = get_vert_loops(obj, bm, edges, edge_map)
|
|
|
|
utils.log_info(f"{len(vert_loops)} lateral loops.")
|
|
|
|
# generate hair card info
|
|
# a median coordinate loop representing the median positions of the hair card
|
|
card = sort_lateral_card(obj, bm, vert_loops, uv_map, card_dir)
|
|
cards.append(card)
|
|
|
|
utils.log_recess()
|
|
|
|
return bm, cards
|
|
|
|
|
|
def distance_from_line(co, start, end):
|
|
"""Returns the distance from the line and where along the line it is closest."""
|
|
line = end - start
|
|
dir = line.normalized()
|
|
length = line.length
|
|
from_start : Vector = co - start
|
|
from_end : Vector = co - end
|
|
if line.dot(from_start) <= 0:
|
|
return (co - start).length, 0.0
|
|
elif line.dot(from_end) >= 0:
|
|
return (co - end).length, 1.0
|
|
else:
|
|
return (line.cross(from_start) / length).length, min(1.0, max(0.0, dir.dot(from_start) / length))
|
|
|
|
|
|
def get_distance_to_bone_def(bone_def, co : Vector):
|
|
#bone_def = { "name": pose_bone.name, "head": head, "tail": tail, "line": line, "dir": dir }
|
|
head : Vector = bone_def["head"]
|
|
tail : Vector = bone_def["tail"]
|
|
return distance_from_line(co, head, tail)
|
|
|
|
|
|
def get_closest_bone_def(bone_chain, co, max_radius):
|
|
least_distance = max_radius * 2.0
|
|
least_bone_def = bone_chain[0]
|
|
least_fac = 0
|
|
for bone_def in bone_chain:
|
|
d, f = get_distance_to_bone_def(bone_def, co)
|
|
if d < least_distance:
|
|
least_distance = d
|
|
least_bone_def = bone_def
|
|
least_fac = f
|
|
return least_bone_def, least_distance, least_fac
|
|
|
|
|
|
def get_weighted_bone_distance(bone_chain, max_radius, median_loop, median_length):
|
|
weighted_distance = 0
|
|
co_length = 0
|
|
last_co = median_loop[0]
|
|
for co in median_loop:
|
|
co_length += (co - last_co).length
|
|
factor = co_length / median_length
|
|
bone_def, distance, fac = get_closest_bone_def(bone_chain, co, max_radius)
|
|
weighted_distance += distance * factor * 2.0
|
|
return weighted_distance / len(median_loop)
|
|
|
|
|
|
def weight_card_to_bones(obj, bm : bmesh.types.BMesh, card, sorted_bones, max_radius, max_bones, max_weight,
|
|
curve, variance):
|
|
props = vars.props()
|
|
CC4_SPRING_RIG = props.hair_rig_target == "CC4"
|
|
|
|
bm.verts.layers.deform.verify()
|
|
# vertex weights are in the deform layer of the BMesh verts
|
|
dl = bm.verts.layers.deform.active
|
|
|
|
card_loop = card["loops"][0]
|
|
card_loop_length = loop_length(card_loop)
|
|
|
|
if len(sorted_bones) < max_bones:
|
|
max_bones = len(sorted_bones)
|
|
|
|
min_weight = 0.01 if CC4_SPRING_RIG else 0.0
|
|
acc_root_weight = (1.0 - max_weight) / max_bones
|
|
bone_weight_variance_mods = []
|
|
for i in range(0, max_bones):
|
|
bone_weight_variance_mods.append(random.uniform(max_weight * (1 - variance), max_weight))
|
|
|
|
first_bone_groups = []
|
|
if CC4_SPRING_RIG:
|
|
for b in range(0, max_bones):
|
|
bone_chain = sorted_bones[b]["bones"]
|
|
bone_def = bone_chain[0]
|
|
bone_name = bone_def["name"]
|
|
vg = meshutils.add_vertex_group(obj, bone_name)
|
|
first_bone_groups.append(vg)
|
|
|
|
for vert_index in card["verts"]:
|
|
vertex : bmesh.types.BMVert = bm.verts[vert_index]
|
|
if vertex.is_valid:
|
|
co = obj.matrix_world @ vertex.co
|
|
proj_point, proj_length = get_projection_on_loop(card_loop, co)
|
|
card_length_fac = math.pow(proj_length / card_loop_length, curve)
|
|
|
|
for b in range(0, max_bones):
|
|
bone_chain = sorted_bones[b]["bones"]
|
|
bone_def, bone_distance, bone_fac = get_closest_bone_def(bone_chain, co, max_radius)
|
|
|
|
weight_distance = min(max_radius, max(0, max_radius - bone_distance))
|
|
weight = bone_weight_variance_mods[b] * (weight_distance / max_radius) / max_bones
|
|
|
|
# bone_fac is used to scale the weights on the very first bone in the chain, from 0 to 1
|
|
# (unless it's for a CC4 accessory)
|
|
if CC4_SPRING_RIG:
|
|
bone_fac = 1.0
|
|
elif bone_def != bone_chain[0]:
|
|
bone_fac = 1.0
|
|
|
|
weight *= max(0, min(bone_fac, card_length_fac))
|
|
weight = max(min_weight, weight)
|
|
|
|
bone_name = bone_def["name"]
|
|
vg = meshutils.add_vertex_group(obj, bone_name)
|
|
if vg:
|
|
vertex[dl][vg.index] = weight
|
|
# if the weight's are scaled back, they need to be scaled back
|
|
# against the root bone's weights, unless this is for the root bone
|
|
# in which case we need to add the root weight
|
|
if CC4_SPRING_RIG:
|
|
first_vg = first_bone_groups[b]
|
|
if vg.index != first_vg.index:
|
|
vertex[dl][first_vg.index] = acc_root_weight
|
|
else:
|
|
vertex[dl][first_vg.index] = weight + acc_root_weight
|
|
|
|
|
|
def sort_func_weighted_distance(bone_weight_distance):
|
|
return bone_weight_distance["distance"]
|
|
|
|
|
|
def assign_bones(obj, bm, cards, bone_chains, max_radius, max_bones, max_weight, curve, variance):
|
|
for i, card in enumerate(cards):
|
|
loops = card["loops"]
|
|
if loops:
|
|
card_loop = loops[0]
|
|
card_loop_length = loop_length(card_loop)
|
|
sorted_bones = []
|
|
for bone_chain in bone_chains:
|
|
weighted_distance = get_weighted_bone_distance(bone_chain, max_radius, card_loop, card_loop_length)
|
|
bone_weight_distance = { "distance": weighted_distance, "bones": bone_chain }
|
|
sorted_bones.append(bone_weight_distance)
|
|
sorted_bones.sort(reverse=False, key=sort_func_weighted_distance)
|
|
weight_card_to_bones(obj, bm, card, sorted_bones, max_radius, max_bones, max_weight, curve, variance)
|
|
|
|
|
|
def remove_hair_bone_weights(obj, hair_bone_list, card_mode):
|
|
"""Remove vertex groups for the given bones"""
|
|
|
|
if card_mode == "ALL":
|
|
# remove all hair_bone_list vertex groups from object
|
|
utils.object_mode_to(obj)
|
|
vg : bpy.types.VertexGroup
|
|
for vg in obj.vertex_groups:
|
|
if vg.name in hair_bone_list:
|
|
meshutils.remove_vertex_group(obj, vg.name)
|
|
utils.edit_mode_to(obj)
|
|
utils.object_mode_to(obj)
|
|
|
|
else:
|
|
# select linked and set to edge mode
|
|
utils.edit_mode_to(obj, only_this=True)
|
|
bpy.ops.mesh.select_linked(delimit={'UV'})
|
|
bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='VERT')
|
|
# object mode to save edit changes
|
|
utils.object_mode_to(obj)
|
|
# remove weights from selected verts
|
|
geom.remove_vertex_groups_from_selected(obj, hair_bone_list)
|
|
|
|
|
|
def reset_weights_to_bones(obj, bone_names, card_mode):
|
|
|
|
if card_mode == "ALL":
|
|
utils.object_mode_to(obj)
|
|
while obj.vertex_groups:
|
|
obj.vertex_groups.remove(obj.vertex_groups[0])
|
|
for bone_name in bone_names:
|
|
meshutils.add_vertex_group(obj, bone_name)
|
|
meshutils.set_vertex_group(obj, bone_name, 1.0 / len(bone_names))
|
|
utils.edit_mode_to(obj)
|
|
utils.object_mode_to(obj)
|
|
|
|
else:
|
|
# select linked and set to edge mode
|
|
utils.edit_mode_to(obj, only_this=True)
|
|
bpy.ops.mesh.select_linked(delimit={'UV'})
|
|
bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='VERT')
|
|
# object mode to save edit changes
|
|
utils.object_mode_to(obj)
|
|
# remove weights from selected verts
|
|
bone_list = [ vg.name for vg in obj.vertex_groups if vg.name != bone_names ]
|
|
geom.remove_vertex_groups_from_selected(obj, bone_list)
|
|
geom.add_vertex_groups_to_selected(obj, bone_names, 1.0 / len(bone_names))
|
|
|
|
|
|
def scale_existing_weights(obj, bm, scale, exclude_bone_names: list = None):
|
|
bm.verts.ensure_lookup_table()
|
|
bm.verts.layers.deform.verify()
|
|
dl = bm.verts.layers.deform.active
|
|
|
|
min_weight = 1.0
|
|
max_weight = 0.0
|
|
for vert in bm.verts:
|
|
weight = 0
|
|
for vg in obj.vertex_groups:
|
|
if exclude_bone_names and vg.name in exclude_bone_names:
|
|
continue
|
|
if vg.index in vert[dl].keys():
|
|
weight += vert[dl][vg.index]
|
|
min_weight = min(weight, min_weight)
|
|
max_weight = max(weight, max_weight)
|
|
|
|
if max_weight < 0.00001:
|
|
# nothing left to scale
|
|
return
|
|
|
|
normalizing_scale = 1.0 / max_weight
|
|
for vg in obj.vertex_groups:
|
|
if exclude_bone_names and vg.name in exclude_bone_names:
|
|
continue
|
|
for vert in bm.verts:
|
|
if vg.index in vert[dl].keys():
|
|
vert[dl][vg.index] *= normalizing_scale * scale
|
|
|
|
|
|
|
|
def add_bone_chain_def(arm, edit_bone : bpy.types.EditBone, chain):
|
|
|
|
if edit_bone.children and len(edit_bone.children) > 1:
|
|
return False
|
|
|
|
head = arm.matrix_world @ edit_bone.head
|
|
tail = arm.matrix_world @ edit_bone.tail
|
|
line = tail - head
|
|
dir = line.normalized()
|
|
|
|
# extend the last bone def in the chain to ensure full overlap with hair mesh
|
|
if not edit_bone.children:
|
|
line *= 4
|
|
tail = head + line
|
|
|
|
bone_def = { "name": edit_bone.name, "head": head, "tail": tail, "line": line, "dir": dir, "length": line.length }
|
|
chain.append(bone_def)
|
|
|
|
if edit_bone.children and len(edit_bone.children) == 1:
|
|
return add_bone_chain_def(arm, edit_bone.children[0], chain)
|
|
|
|
return True
|
|
|
|
|
|
def get_bone_chain_defs(chr_cache, arm, bone_selection_mode, parent_mode):
|
|
"""Get each bone chain from the spring bone rig."""
|
|
|
|
utils.edit_mode_to(arm)
|
|
|
|
if bone_selection_mode == "SELECTED":
|
|
# select all linked bones
|
|
utils.edit_mode_to(arm)
|
|
bpy.ops.armature.select_linked()
|
|
utils.object_mode_to(arm)
|
|
utils.edit_mode_to(arm)
|
|
|
|
# NOTE: remember edit bones do not survive mode changes...
|
|
bone_chains = []
|
|
|
|
hair_rig = springbones.get_spring_rig(chr_cache, arm, parent_mode, mode = "EDIT")
|
|
if hair_rig:
|
|
for child_bone in hair_rig.children:
|
|
if bones.get_bone_selected(arm, child_bone.name) or bone_selection_mode == "ALL":
|
|
chain = []
|
|
if not add_bone_chain_def(arm, child_bone, chain):
|
|
continue
|
|
bone_chains.append(chain)
|
|
|
|
utils.object_mode_to(arm)
|
|
|
|
return bone_chains
|
|
|
|
|
|
def add_child_spring_bone_names(bone, names):
|
|
for child in bone.children:
|
|
names.append(child.name)
|
|
add_child_spring_bone_names(child, names)
|
|
|
|
|
|
def get_all_spring_bone_names(chr_cache, arm):
|
|
bone_names = []
|
|
spring_rigs = springbones.get_spring_rigs(chr_cache, arm)
|
|
for parent_mode in spring_rigs:
|
|
spring_rig_def = spring_rigs[parent_mode]
|
|
spring_root = spring_rig_def["bone"]
|
|
add_child_spring_bone_names(spring_root, bone_names)
|
|
return bone_names
|
|
|
|
|
|
def smooth_hair_bone_weights(arm, obj, bone_chains, iterations):
|
|
props = vars.props()
|
|
|
|
if iterations == 0:
|
|
return
|
|
|
|
bones.select_all_bones(arm, False)
|
|
|
|
# select all the bones involved
|
|
for bone_chain in bone_chains:
|
|
for bone_def in bone_chain:
|
|
bone_name = bone_def["name"]
|
|
if bone_name in arm.data.bones:
|
|
bones.select_bone(arm, bone_name, True)
|
|
|
|
# Note: BONE_SELECT group select mode is only available if the armature is also selected with the active mesh
|
|
# (otherwise it doesn't even exist as an enum option)
|
|
utils.object_mode()
|
|
utils.try_select_objects([arm, obj], True)
|
|
utils.set_active_object(obj)
|
|
utils.set_mode("WEIGHT_PAINT")
|
|
try:
|
|
bpy.ops.object.vertex_group_smooth(group_select_mode='BONE_SELECT', factor = 1.0, repeat = iterations)
|
|
except:
|
|
utils.log_error("Unable to smooth spring bone vertex groups: No armature modifier on hair mesh?")
|
|
utils.object_mode_to(obj)
|
|
|
|
# for CC4 rigs, lock rotation and position of the first bone in each chain
|
|
for bone_chain in bone_chains:
|
|
bone_def = bone_chain[0]
|
|
bone_name = bone_def["name"]
|
|
if bone_name in arm.data.bones:
|
|
bone : bpy.types.Bone = arm.data.bones[bone_name]
|
|
pose_bone : bpy.types.PoseBone = arm.pose.bones[bone_name]
|
|
if props.hair_rig_target == "CC4":
|
|
pose_bone.lock_location = [True, True, True]
|
|
pose_bone.lock_rotation = [True, True, True]
|
|
pose_bone.lock_scale = [True, True, True]
|
|
pose_bone.lock_rotation_w = True
|
|
else:
|
|
pose_bone.lock_location = [False, False, False]
|
|
pose_bone.lock_rotation = [False, False, False]
|
|
pose_bone.lock_rotation_w = False
|
|
pose_bone.lock_scale = [False, False, False]
|
|
|
|
|
|
|
|
def find_stroke_set_root(stroke_set, stroke, done : list):
|
|
done.append(stroke)
|
|
next_strokes, prev_strokes = stroke_set[stroke]
|
|
if not prev_strokes:
|
|
return stroke
|
|
elif prev_strokes not in done:
|
|
return find_stroke_set_root(stroke_set, prev_strokes[0], done)
|
|
else:
|
|
return None
|
|
|
|
|
|
def combine_strokes(strokes):
|
|
stroke_set = {}
|
|
|
|
for stroke in strokes:
|
|
# if the last position is near the first position of another stroke...
|
|
first = stroke.points[0].co
|
|
last = stroke.points[-1].co
|
|
next_strokes = []
|
|
prev_strokes = []
|
|
stroke_set[stroke] = [next_strokes, prev_strokes]
|
|
for s in strokes:
|
|
if s != stroke:
|
|
if (s.points[0].co - last).length < STROKE_JOIN_THRESHOLD:
|
|
next_strokes.append(s)
|
|
if (s.points[-1].co - first).length < STROKE_JOIN_THRESHOLD:
|
|
prev_strokes.append(s)
|
|
|
|
stroke_roots = set()
|
|
for stroke in strokes:
|
|
root = find_stroke_set_root(stroke_set, stroke, [])
|
|
if root:
|
|
stroke_roots.add(root)
|
|
|
|
return stroke_set, stroke_roots
|
|
|
|
|
|
def stroke_root_to_loop(stroke_set, stroke, loop : list):
|
|
next_strokes, prev_strokes = stroke_set[stroke]
|
|
for p in stroke.points:
|
|
loop.append(p.co)
|
|
if next_strokes:
|
|
stroke_root_to_loop(stroke_set, next_strokes[0], loop)
|
|
|
|
|
|
def subdivide_loop(loop):
|
|
subd = []
|
|
for i in range(0, len(loop) - 1):
|
|
l0 = loop[i]
|
|
l2 = loop[i + 1]
|
|
l1 = (l0 + l2) * 0.5
|
|
subd.append(l0)
|
|
subd.append(l1)
|
|
subd.append(loop[-1])
|
|
loop.clear()
|
|
for co in subd:
|
|
loop.append(co)
|
|
|
|
|
|
def generate_smoothed_loop_levels(loop, strength = 1.0, max_iterations = 10):
|
|
"""Returns a dictionary { iteration_level: smoothed_loop, ... } of loops smoothed by iteration level (0 to max_iterations+1)"""
|
|
smoothed_levels = {}
|
|
for i in range(0, max_iterations + 1):
|
|
smooth_level = loop.copy()
|
|
smoothed_levels[i] = smooth_level
|
|
if i > 0:
|
|
for l in range(1, len(loop)-1):
|
|
smoothed = (loop[l - 1] + loop[l] + loop[l + 1]) / 3.0
|
|
original = loop[l]
|
|
smooth_level[l] = (smoothed - original) * strength + original
|
|
loop = smooth_level
|
|
return smoothed_levels
|
|
|
|
|
|
def grease_pencil_to_length_loops(bone_length):
|
|
current_frame = bpy.context.scene.frame_current
|
|
|
|
grease_pencil_layer = get_active_grease_pencil_layer()
|
|
if not grease_pencil_layer:
|
|
return
|
|
|
|
frame = grease_pencil_layer.active_frame
|
|
stroke_set, stroke_roots = combine_strokes(frame.strokes)
|
|
|
|
loops = []
|
|
for root in stroke_roots:
|
|
loop = []
|
|
stroke_root_to_loop(stroke_set, root, loop)
|
|
if len(loop) > 1 and loop_length(loop) >= bone_length / 2:
|
|
while(len(loop) < 25):
|
|
subdivide_loop(loop)
|
|
loops.append(loop)
|
|
|
|
return loops
|
|
|
|
|
|
def grease_pencil_to_bones(chr_cache, arm, parent_mode, bone_length = 0.05,
|
|
skip_length = 0.0, trunc_length = 0.0, smooth_level = 0):
|
|
props = vars.props()
|
|
|
|
grease_pencil_layer = get_active_grease_pencil_layer()
|
|
if not grease_pencil_layer:
|
|
return
|
|
|
|
# turn off grease pencil on current object / mode (including object mode)
|
|
# (this is expected to be object mode on a hair mesh)
|
|
tool_idname = utils.get_current_tool_idname(bpy.context)
|
|
if "builtin.annotate" in tool_idname:
|
|
bpy.ops.wm.tool_set_by_id(name="builtin.select_box")
|
|
mode = utils.get_mode()
|
|
if mode != "OBJECT":
|
|
utils.object_mode()
|
|
bpy.ops.wm.tool_set_by_id(name="builtin.select_box")
|
|
|
|
#mode_selection = utils.store_mode_selection_state()
|
|
arm_pose = set_rest_pose(arm)
|
|
|
|
springbones.realign_spring_bones_axis(chr_cache, arm)
|
|
|
|
springbones.show_spring_bone_edit_layer(chr_cache, arm, True)
|
|
|
|
hair_bone_prefix = props.hair_rig_group_name
|
|
|
|
# check root bone exists...
|
|
anchor_bone_name = springbones.get_spring_anchor_name(chr_cache, arm, parent_mode)
|
|
anchor_bone = bones.get_pose_bone(arm, anchor_bone_name)
|
|
|
|
if anchor_bone:
|
|
loops = grease_pencil_to_length_loops(bone_length)
|
|
utils.edit_mode_to(arm)
|
|
smoothed_loops_set = get_smoothed_loops_set(loops)
|
|
remove_existing_loop_bones(chr_cache, arm, smoothed_loops_set)
|
|
for edit_bone in arm.data.edit_bones:
|
|
edit_bone.select_head = False
|
|
edit_bone.select_tail = False
|
|
edit_bone.select = False
|
|
loop_index = 1
|
|
new_bones = []
|
|
for smoothed_loop in smoothed_loops_set:
|
|
loop = smoothed_loop[smooth_level]
|
|
loop_index = find_unused_hair_bone_index(arm, loop_index, hair_bone_prefix)
|
|
if loop_to_bones(chr_cache, arm, parent_mode, loop, loop_index,
|
|
bone_length, skip_length, trunc_length, smooth_level, new_bones):
|
|
loop_index += 1
|
|
|
|
remove_duplicate_bones(chr_cache, arm)
|
|
|
|
utils.object_mode_to(arm)
|
|
# turn OFF grease pencil on armature : object mode
|
|
bpy.ops.wm.tool_set_by_id(name="builtin.select_box")
|
|
|
|
restore_pose(arm, arm_pose)
|
|
#utils.restore_mode_selection_state(mode_selection)
|
|
utils.edit_mode_to(arm)
|
|
# turn ON grease pencil on armature : edit mode
|
|
# (hopefully at this point grease pencil will only be on the armature in edit mode)
|
|
bpy.ops.wm.tool_set_by_id(name="builtin.annotate")
|
|
|
|
|
|
def get_active_grease_pencil_layer():
|
|
#current_frame = bpy.context.scene.frame_current
|
|
#note_layer = bpy.data.grease_pencils['Annotations'].layers.active
|
|
#frame = note_layer.active_frame
|
|
try:
|
|
layer_index = bpy.context.scene.grease_pencil.layers.active_index
|
|
layer = bpy.context.scene.grease_pencil.layers[layer_index]
|
|
return layer
|
|
except:
|
|
try:
|
|
return bpy.context.scene.grease_pencil.layers.active
|
|
except:
|
|
return None
|
|
|
|
|
|
def clear_grease_pencil():
|
|
active_layer = get_active_grease_pencil_layer()
|
|
if active_layer:
|
|
active_frame = active_layer.active_frame
|
|
try:
|
|
active_layer.frames.remove(active_frame)
|
|
except:
|
|
try:
|
|
active_layer.active_frame.clear()
|
|
except:
|
|
utils.log_error("Unable to remove active grease pencil frame!")
|
|
|
|
|
|
def add_custom_bone(chr_cache, arm, parent_mode, bone_length = 0.05, skip_length = 0.0):
|
|
props = vars.props()
|
|
|
|
arm_pose = set_rest_pose(arm)
|
|
|
|
springbones.realign_spring_bones_axis(chr_cache, arm)
|
|
|
|
springbones.show_spring_bone_edit_layer(chr_cache, arm, True)
|
|
|
|
hair_bone_prefix = props.hair_rig_group_name
|
|
|
|
# check root bone exists...
|
|
anchor_bone_name = springbones.get_spring_anchor_name(chr_cache, arm, parent_mode)
|
|
anchor_bone = bones.get_pose_bone(arm, anchor_bone_name)
|
|
|
|
if anchor_bone:
|
|
utils.edit_mode_to(arm)
|
|
bones.select_all_bones(arm, False)
|
|
loop_index = 1
|
|
new_bones = []
|
|
loop_index = find_unused_hair_bone_index(arm, loop_index, hair_bone_prefix)
|
|
if custom_bone(chr_cache, arm, parent_mode, loop_index, bone_length, new_bones):
|
|
loop_index += 1
|
|
|
|
utils.object_mode_to(arm)
|
|
restore_pose(arm, arm_pose)
|
|
|
|
remove_duplicate_bones(chr_cache, arm)
|
|
|
|
utils.edit_mode_to(arm)
|
|
|
|
|
|
def bind_to_first_bones(chr_cache, arm, objects, card_mode, parent_mode):
|
|
bone_chains = get_bone_chain_defs(chr_cache, arm, "ALL", parent_mode)
|
|
first_bones = []
|
|
for chain in bone_chains:
|
|
if chain:
|
|
first_bones.append(chain[0]["name"])
|
|
for obj in objects:
|
|
reset_weights_to_bones(obj, first_bones, card_mode)
|
|
|
|
|
|
def bind_cards_to_bones(chr_cache, arm, objects, card_dirs,
|
|
max_radius, max_bones, max_weight,
|
|
curve, variance, existing_scale,
|
|
card_mode, bone_mode, smoothing, parent_mode):
|
|
|
|
utils.object_mode_to(arm)
|
|
set_rest_pose(arm)
|
|
remove_duplicate_bones(chr_cache, arm)
|
|
|
|
springbones.realign_spring_bones_axis(chr_cache, arm)
|
|
|
|
bone_chain_defs = get_bone_chain_defs(chr_cache, arm, bone_mode, parent_mode)
|
|
all_spring_bone_names = get_all_spring_bone_names(chr_cache, arm)
|
|
|
|
if bone_chain_defs:
|
|
|
|
hair_bones = []
|
|
for bone_chain in bone_chain_defs:
|
|
for bone_def in bone_chain:
|
|
hair_bones.append(bone_def["name"])
|
|
|
|
# if no meshes selected, get a list of all hair objects
|
|
if not objects:
|
|
objects = []
|
|
chr_objects = chr_cache.get_cache_objects()
|
|
for obj in chr_objects:
|
|
obj_cache = chr_cache.get_object_cache(obj)
|
|
if obj_cache and not obj_cache.disabled and obj not in objects:
|
|
for mat in obj.data.materials:
|
|
mat_cache = chr_cache.get_material_cache(mat)
|
|
if mat_cache and mat_cache.material_type == "HAIR":
|
|
objects.append(obj)
|
|
break
|
|
|
|
for obj in objects:
|
|
# ensure an armature modifier with this armature (otherwise weight smooth fails)
|
|
arm_mod = modifiers.get_armature_modifier(obj, create=True, armature=arm)
|
|
#
|
|
remove_hair_bone_weights(obj, hair_bones, card_mode)
|
|
cards, bm = selected_cards_to_length_loops(chr_cache, obj, card_dirs,
|
|
one_loop_per_card=True, card_selection_mode=card_mode)
|
|
scale_existing_weights(obj, bm, existing_scale, all_spring_bone_names)
|
|
assign_bones(obj, bm, cards, bone_chain_defs, max_radius, max_bones, max_weight, curve, variance)
|
|
|
|
bm.to_mesh(obj.data)
|
|
geom.clean_empty_vertex_groups(obj, bm)
|
|
|
|
smooth_hair_bone_weights(arm, obj, bone_chain_defs, smoothing)
|
|
|
|
else:
|
|
|
|
utils.log_error("No bones selected!")
|
|
|
|
arm.data.pose_position = "POSE"
|
|
utils.pose_mode_to(arm)
|
|
|
|
|
|
def deselect_invalid_materials(chr_cache, obj):
|
|
"""Mesh polygon selection only works in OBJECT mode"""
|
|
if utils.object_exists_is_mesh(obj):
|
|
for slot in obj.material_slots:
|
|
mat = slot.material
|
|
if mat is None: continue
|
|
mat_cache = chr_cache.get_material_cache(mat)
|
|
if mat_cache:
|
|
if mat_cache.material_type == "SCALP":
|
|
meshutils.select_material_faces(obj, mat, False)
|
|
|
|
|
|
def set_rest_pose(arm):
|
|
arm_pose = arm.data.pose_position
|
|
arm.data.pose_position = "REST"
|
|
return arm_pose
|
|
|
|
|
|
def restore_pose(arm, arm_pose):
|
|
arm.data.pose_position = arm_pose
|
|
|
|
|
|
class CC3OperatorHair(bpy.types.Operator):
|
|
"""Hair Spring Rigging"""
|
|
bl_idname = "cc3.hair"
|
|
bl_label = "Hair Spring Rigging"
|
|
#bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
|
|
|
param: bpy.props.StringProperty(
|
|
name = "param",
|
|
default = ""
|
|
)
|
|
|
|
def execute(self, context):
|
|
props = vars.props()
|
|
prefs = vars.prefs()
|
|
|
|
mode_selection = utils.store_mode_selection_state()
|
|
|
|
chr_cache = props.get_context_character_cache(context)
|
|
|
|
if not chr_cache:
|
|
self.report({"ERROR"}, "No current character!")
|
|
return {"FINISHED"}
|
|
|
|
arm = chr_cache.get_armature()
|
|
hair_mesh = utils.get_selected_mesh()
|
|
|
|
if self.param == "CARDS_TO_CURVES":
|
|
|
|
if hair_mesh:
|
|
selected_cards_to_curves(chr_cache, utils.get_active_object(),
|
|
props.hair_dir_vectors(),
|
|
one_loop_per_card = props.hair_curve_merge_loops == "MERGE")
|
|
|
|
if self.param == "ADD_BONES":
|
|
|
|
if arm and hair_mesh:
|
|
utils.unhide(arm)
|
|
selected_cards_to_bones(chr_cache, arm,
|
|
hair_mesh,
|
|
props.hair_rig_bone_root,
|
|
props.hair_dir_vectors(),
|
|
one_loop_per_card = True,
|
|
bone_length = props.hair_rig_bone_length / 100.0,
|
|
skip_length = props.hair_rig_bind_skip_length / 100.0,
|
|
trunc_length = props.hair_rig_bind_trunc_length / 100.0,
|
|
smooth_level = props.hair_rig_bone_smoothing)
|
|
else:
|
|
self.report({"ERROR"}, "Active Object must be a mesh!")
|
|
|
|
if self.param == "ADD_BONES_GREASE":
|
|
|
|
if arm:
|
|
utils.unhide(arm)
|
|
grease_pencil_to_bones(chr_cache, arm, props.hair_rig_bone_root,
|
|
bone_length = props.hair_rig_bone_length / 100.0,
|
|
skip_length = props.hair_rig_bind_skip_length / 100.0,
|
|
trunc_length = props.hair_rig_bind_trunc_length / 100.0,
|
|
smooth_level = props.hair_rig_bone_smoothing)
|
|
else:
|
|
self.report({"ERROR"}, "Active Object be part of the character!")
|
|
|
|
if self.param == "ADD_BONES_CUSTOM":
|
|
|
|
if arm:
|
|
utils.unhide(arm)
|
|
add_custom_bone(chr_cache, arm, props.hair_rig_bone_root,
|
|
bone_length = props.hair_rig_bone_length / 100.0)
|
|
|
|
else:
|
|
self.report({"ERROR"}, "Active Object be part of the character!")
|
|
|
|
if self.param == "REMOVE_HAIR_BONES":
|
|
|
|
if arm:
|
|
utils.unhide(arm)
|
|
remove_hair_bones(chr_cache, arm,
|
|
props.hair_rig_bind_bone_mode,
|
|
props.hair_rig_bone_root)
|
|
|
|
utils.restore_mode_selection_state(mode_selection)
|
|
|
|
if self.param == "RESET_ACCESSORY_WEIGHTS":
|
|
|
|
objects = utils.get_selected_meshes(context)
|
|
if arm and objects:
|
|
bind_to_first_bones(chr_cache, arm, objects, props.hair_rig_bind_card_mode, props.hair_rig_bone_root)
|
|
|
|
|
|
if self.param == "BIND_TO_BONES":
|
|
|
|
objects = utils.get_selected_meshes(context)
|
|
|
|
seed = props.hair_rig_bind_seed
|
|
random.seed(seed)
|
|
|
|
existing_scale = props.hair_rig_bind_existing_scale if props.hair_rig_target != "CC4" else 0.0
|
|
|
|
if arm and objects:
|
|
utils.unhide(arm)
|
|
bind_cards_to_bones(chr_cache, arm,
|
|
objects,
|
|
props.hair_dir_vectors(),
|
|
props.hair_rig_bind_bone_radius / 100.0,
|
|
props.hair_rig_bind_bone_count,
|
|
props.hair_rig_bind_bone_weight,
|
|
props.hair_rig_bind_weight_curve,
|
|
props.hair_rig_bind_bone_variance,
|
|
existing_scale,
|
|
props.hair_rig_bind_card_mode,
|
|
props.hair_rig_bind_bone_mode,
|
|
props.hair_rig_bind_smoothing,
|
|
props.hair_rig_bone_root)
|
|
else:
|
|
self.report({"ERROR"}, "Selected Object(s) to bind must be Meshes!")
|
|
|
|
if self.param == "CLEAR_WEIGHTS":
|
|
|
|
objects = utils.get_selected_meshes(context)
|
|
|
|
if arm:
|
|
utils.unhide(arm)
|
|
clear_hair_bone_weights(chr_cache, arm, objects,
|
|
props.hair_rig_bind_card_mode,
|
|
props.hair_rig_bind_bone_mode,
|
|
props.hair_rig_bone_root)
|
|
utils.restore_mode_selection_state(mode_selection)
|
|
|
|
if self.param == "CLEAR_GREASE_PENCIL":
|
|
|
|
clear_grease_pencil()
|
|
|
|
if self.param == "MAKE_ACCESSORY":
|
|
|
|
objects = utils.get_selected_meshes(context)
|
|
|
|
if arm and objects:
|
|
utils.unhide(arm)
|
|
accessory_name = springbones.convert_spring_rig_to_accessory(chr_cache, arm, objects, props.hair_rig_bone_root)
|
|
if accessory_name:
|
|
self.report({'INFO'}, f"Accesssory: {accessory_name} created!")
|
|
|
|
|
|
if self.param == "GROUP_NAME_BONES":
|
|
|
|
group_name = props.hair_rig_group_name
|
|
parent_mode = props.hair_rig_bone_root
|
|
|
|
objects = utils.get_selected_meshes(context)
|
|
if arm:
|
|
utils.unhide(arm)
|
|
rename_hair_bones(chr_cache, arm, group_name, parent_mode)
|
|
|
|
utils.restore_mode_selection_state(mode_selection)
|
|
|
|
if self.param == "SPRING_BONES_TOGGLE":
|
|
if arm:
|
|
springbones.show_spring_bone_edit_layer(chr_cache, arm, False)
|
|
|
|
if self.param == "SPRING_BONES_SHOW":
|
|
if arm:
|
|
springbones.show_spring_bone_edit_layer(chr_cache, arm, True)
|
|
|
|
if self.param == "ARMATURE_SHOW_POSE":
|
|
if arm:
|
|
arm.data.pose_position = "POSE"
|
|
|
|
if self.param == "ARMATURE_SHOW_REST":
|
|
if arm:
|
|
arm.data.pose_position = "REST"
|
|
|
|
if self.param == "CYCLE_BONE_STYLE":
|
|
if arm:
|
|
if arm.data.display_type == 'WIRE':
|
|
arm.data.display_type = 'OCTAHEDRAL'
|
|
arm.display_type = 'SOLID'
|
|
elif arm.data.display_type == 'OCTAHEDRAL' and arm.display_type == 'SOLID':
|
|
arm.data.display_type = 'OCTAHEDRAL'
|
|
arm.display_type = 'WIRE'
|
|
elif arm.data.display_type == 'OCTAHEDRAL' and arm.display_type == 'WIRE':
|
|
arm.data.display_type = 'STICK'
|
|
arm.display_type = 'SOLID'
|
|
elif arm.data.display_type == 'STICK':
|
|
arm.data.display_type = 'WIRE'
|
|
arm.display_type = 'SOLID'
|
|
else:
|
|
arm.data.display_type = 'OCTAHEDRAL'
|
|
arm.display_type = 'SOLID'
|
|
|
|
if self.param == "TOGGLE_GREASE_PENCIL":
|
|
tool_idname = utils.get_current_tool_idname(context)
|
|
if "builtin.annotate" in tool_idname:
|
|
mode = utils.get_mode()
|
|
if mode != "OBJECT":
|
|
utils.object_mode()
|
|
bpy.ops.wm.tool_set_by_id(name="builtin.select_box")
|
|
utils.set_mode(mode)
|
|
bpy.ops.wm.tool_set_by_id(name="builtin.select_box")
|
|
if arm:
|
|
arm.data.pose_position = "POSE"
|
|
else:
|
|
mode = utils.get_mode()
|
|
if mode != "OBJECT":
|
|
utils.object_mode()
|
|
bpy.ops.wm.tool_set_by_id(name="builtin.annotate")
|
|
utils.set_mode(mode)
|
|
bpy.ops.wm.tool_set_by_id(name="builtin.annotate")
|
|
bpy.context.scene.tool_settings.annotation_stroke_placement_view3d = 'SURFACE'
|
|
try:
|
|
props = bpy.context.workspace.tools["builtin.annotate"].operator_properties("gpencil.annotate")
|
|
props.use_stabilizer = True
|
|
except:
|
|
pass
|
|
# only use rest position to draw grease pencil on surface of hair
|
|
if arm:
|
|
arm.data.pose_position = "REST"
|
|
|
|
return {"FINISHED"}
|
|
|
|
@classmethod
|
|
def description(cls, context, properties):
|
|
props = vars.props()
|
|
|
|
if properties.param == "ADD_BONES":
|
|
return "Add bones to the hair rig, generated from the selected hair cards in the active mesh"
|
|
elif properties.param == "ADD_BONES_CUSTOM":
|
|
return "Add a single custom bone to the hair rig"
|
|
elif properties.param == "ADD_BONES_GREASE":
|
|
return "Add bones generated from grease pencil lines drawn in the current annotation layer.\n\n" \
|
|
"Note: For best results draw lines onto the hair in Surface placement mode."
|
|
elif properties.param == "REMOVE_HAIR_BONES":
|
|
if props.hair_rig_bind_bone_mode == "ALL":
|
|
return "Remove all bones from the hair rig.\n\n" \
|
|
"All associated vertex weights will also be removed from the hair meshes"
|
|
else:
|
|
return "Remove only the selected bones from the hair rig.\n\n" \
|
|
"The vertex weights for the removed bones will also be removed from the hair meshes\n\n" \
|
|
"Note: Selecting any bone in a chain will use the entire chain of bones"
|
|
elif properties.param == "BIND_TO_BONES":
|
|
if props.hair_rig_bind_card_mode == "ALL":
|
|
if props.hair_rig_bind_bone_mode == "ALL":
|
|
return "Bind the selected hair meshes to all of the hair rig bones.\n\n" \
|
|
"If no meshes are selected then *all* meshes in the character will be considered"
|
|
else:
|
|
return "Bind the selected hair meshes to only the selected hair rig bones.\n\n" \
|
|
"If no meshes are selected then *all* meshes in the character will be considered.\n\n" \
|
|
"Note: Selecting any bone in a chain will use the entire chain of bones"
|
|
else:
|
|
if props.hair_rig_bind_bone_mode == "ALL":
|
|
return "Bind only the selected hair cards in the selected hair meshes to all of the hair rig bones.\n\n" \
|
|
"If no meshes are selected then *all* meshes in the character will be considered.\n\n" \
|
|
"Note: Selecting any part of a hair card will use the entire card island"
|
|
else:
|
|
return "Bind only the selected hair cards in the selected hair meshes to only the selected hair rig bones.\n\n" \
|
|
"If no meshes are selected then *all* meshes in the character will be considered.\n\n" \
|
|
"Note: Selecting any bone in a chain will use the entire chain of bones and selecting any part of a hair card will select the whole har card island"
|
|
elif properties.param == "CLEAR_WEIGHTS":
|
|
if props.hair_rig_bind_card_mode == "ALL":
|
|
if props.hair_rig_bind_bone_mode == "ALL":
|
|
return "Clear all the hair rig bone vertex weights from the selected hair meshes.\n\n" \
|
|
"If no meshes are selected then *all* meshes in the character will be considered"
|
|
else:
|
|
return "Clear only the selected hair rig bone vertex weights from the selected hair meshes.\n\n" \
|
|
"If no meshes are selected then *all* meshes in the character will be considered.\n\n" \
|
|
"Note: Selecting any bone in a chain will use the entire chain of bones"
|
|
else:
|
|
if props.hair_rig_bind_bone_mode == "ALL":
|
|
return "Clear all the hair rig bone vertex weights from only the selected hair cards in the selected meshes.\n\n" \
|
|
"If no meshes are selected then *all* meshes in the character will be considered.\n\n" \
|
|
"Note: Selecting any part of a hair card will select the whole har card island"
|
|
else:
|
|
return "Clear only the selected hair rig bone vertex weights from only the selected hair cards in the selected meshes.\n\n" \
|
|
"If no meshes are selected then *all* meshes in the character will be considered.\n\n" \
|
|
"Note: Selecting any bone in a chain will use the entire chain of bones and selecting any part of a hair card will select the whole har card island"
|
|
|
|
elif properties.param == "CLEAR_GREASE_PENCIL":
|
|
return "Remove all grease pencil lines from the current annotation layer"
|
|
elif properties.param == "CARDS_TO_CURVES":
|
|
return "Convert all the hair cards into curves"
|
|
elif properties.param == "RESET_ACCESSORY_WEIGHTS":
|
|
return "Resets the weights on the mesh to the base weights needed by an accesssory, i.e. weighted only to the first bone in each bone chain.\n" \
|
|
"Useful for setting a base weighting for further selective mesh binding"
|
|
elif properties.param == "MAKE_ACCESSORY":
|
|
return "Converts the hair spring rig and selected objects into an accessory named after the active object.\n" \
|
|
"Accessories are categorized by:\n" \
|
|
" 1. A bone representing the accessory parented to a CC Base bone.\n" \
|
|
" 2. Child accessory deformation bone(s) parented to the accessory bone in 1.\n" \
|
|
" 3. Object(s) with vertex weights ONLY to these accessory deformation bones in 2.\n" \
|
|
" 4. All vertices in the accessory must be weighted"
|
|
|
|
elif properties.param == "GROUP_NAME_BONES":
|
|
return "Rename the bones in the selected chain so they all belong to the same group name"
|
|
|
|
elif properties.param == "TOGGLE_GREASE_PENCIL":
|
|
return "Quick toggle grease pencil mode with surface draw and stabilze stroke"
|
|
|
|
elif properties.param == "CYCLE_BONE_STYLE":
|
|
return "Cycle through armature bone styles"
|
|
|
|
return ""
|
|
|
|
|
|
class CC3ExportHair(bpy.types.Operator):
|
|
"""Export Hair Curves"""
|
|
bl_idname = "cc3.export_hair"
|
|
bl_label = "Export Hair"
|
|
bl_options = {"REGISTER"}
|
|
|
|
filepath: bpy.props.StringProperty(
|
|
name="File Path",
|
|
description="Base filepath used for exporting the hair curves",
|
|
maxlen=1024,
|
|
subtype='FILE_PATH',
|
|
)
|
|
|
|
filename_ext = "" # ExportHelper mixin class uses this
|
|
|
|
#filter_glob: bpy.props.StringProperty(
|
|
# default="*.fbx;*.obj;*.blend",
|
|
# options={"HIDDEN"},
|
|
# )
|
|
|
|
def execute(self, context):
|
|
props = vars.props()
|
|
prefs = vars.prefs()
|
|
|
|
objects = bpy.context.selected_objects.copy()
|
|
chr_cache = props.get_character_cache_from_objects(objects, True)
|
|
|
|
export_blender_hair(self, chr_cache, objects, self.filepath)
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
def invoke(self, context, event):
|
|
context.window_manager.fileselect_add(self)
|
|
return {"RUNNING_MODAL"}
|
|
|
|
|
|
@classmethod
|
|
def description(cls, context, properties):
|
|
return "Export the hair curves to Alembic."
|