Merge branch 'master' into xr-controller-support
This commit is contained in:
commit
ece0feb7df
|
@ -1,4 +1,4 @@
|
|||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
|
|
@ -976,16 +976,19 @@ class BlenderKitBrushSearchProps(PropertyGroup, BlenderKitCommonSearchProps):
|
|||
class BlenderKitHDRUploadProps(PropertyGroup, BlenderKitCommonUploadProps):
|
||||
texture_resolution_max: IntProperty(name="Texture Resolution Max", description="texture resolution maximum",
|
||||
default=0)
|
||||
evs_cap: IntProperty(name="EV cap", description="EVs dynamic range",
|
||||
default=0)
|
||||
true_hdr: BoolProperty(name="Real HDR", description="Image has High dynamic range.",default=False)
|
||||
|
||||
|
||||
class BlenderKitBrushUploadProps(PropertyGroup, BlenderKitCommonUploadProps):
|
||||
mode: EnumProperty(
|
||||
name="Mode",
|
||||
items=(
|
||||
('IMAGE', 'Texture paint', "Texture brush"),
|
||||
('SCULPT', 'Sculpt', 'Sculpt brush'),
|
||||
('VERTEX', 'Vertex paint', 'Vertex paint brush'),
|
||||
('WEIGHT', 'Weight paint', 'Weight paint brush'),
|
||||
("IMAGE", "Texture paint", "Texture brush"),
|
||||
("SCULPT", "Sculpt", "Sculpt brush"),
|
||||
("VERTEX", "Vertex paint", "Vertex paint brush"),
|
||||
("WEIGHT", "Weight paint", "Weight paint brush"),
|
||||
),
|
||||
description="Mode where the brush works",
|
||||
default="SCULPT",
|
||||
|
@ -1514,6 +1517,13 @@ class BlenderKitHDRSearchProps(PropertyGroup, BlenderKitCommonSearchProps):
|
|||
update=search.search_update
|
||||
)
|
||||
|
||||
true_hdr: BoolProperty(
|
||||
name='Real HDRs only',
|
||||
description='Search only for real HDRs, this means images that have a range higher than 0-1 in their pixels.',
|
||||
default=True,
|
||||
update=search.search_update
|
||||
)
|
||||
|
||||
|
||||
class BlenderKitSceneSearchProps(PropertyGroup, BlenderKitCommonSearchProps):
|
||||
search_keywords: StringProperty(
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
|
||||
from blenderkit import paths, append_link, utils, ui, colors, tasks_queue, rerequests, resolutions, ui_panels
|
||||
from blenderkit import paths, append_link, utils, ui, colors, tasks_queue, rerequests, resolutions, ui_panels, search
|
||||
|
||||
import threading
|
||||
import time
|
||||
|
@ -693,7 +693,11 @@ def delete_unfinished_file(file_name):
|
|||
|
||||
def download_asset_file(asset_data, resolution='blend', api_key = ''):
|
||||
# this is a simple non-threaded way to download files for background resolution genenration tool
|
||||
file_name = paths.get_download_filepaths(asset_data, resolution)[0] # prefer global dir if possible.
|
||||
file_names = paths.get_download_filepaths(asset_data, resolution) # prefer global dir if possible.
|
||||
if len(file_names) == 0:
|
||||
return None
|
||||
|
||||
file_name = file_names[0]
|
||||
|
||||
if check_existing(asset_data, resolution=resolution):
|
||||
# this sends the thread for processing, where another check should occur, since the file might be corrupted.
|
||||
|
@ -704,6 +708,7 @@ def download_asset_file(asset_data, resolution='blend', api_key = ''):
|
|||
|
||||
with open(file_name, "wb") as f:
|
||||
print("Downloading %s" % file_name)
|
||||
headers = utils.get_headers(api_key)
|
||||
res_file_info, resolution = paths.get_res_file(asset_data, resolution)
|
||||
response = requests.get(res_file_info['url'], stream=True)
|
||||
total_length = response.headers.get('Content-Length')
|
||||
|
@ -1308,12 +1313,23 @@ class BlenderkitDownloadOperator(bpy.types.Operator):
|
|||
# or from the scene.
|
||||
asset_base_id = self.asset_base_id
|
||||
|
||||
au = s.get('assets used')
|
||||
if au == None:
|
||||
s['assets used'] = {}
|
||||
if asset_base_id in s.get('assets used'):
|
||||
# already used assets have already download link and especially file link.
|
||||
asset_data = s['assets used'][asset_base_id].to_dict()
|
||||
au = s.get('assets used')
|
||||
if au == None:
|
||||
s['assets used'] = {}
|
||||
if asset_base_id in s.get('assets used'):
|
||||
# already used assets have already download link and especially file link.
|
||||
asset_data = s['assets used'][asset_base_id].to_dict()
|
||||
else:
|
||||
#when not in scene nor in search results, we need to get it from the server
|
||||
params = {
|
||||
'asset_base_id': self.asset_base_id
|
||||
}
|
||||
preferences = bpy.context.preferences.addons['blenderkit'].preferences
|
||||
|
||||
results = search.get_search_simple(params, page_size=1, max_results=1,
|
||||
api_key=preferences.api_key)
|
||||
asset_data = search.parse_result(results[0])
|
||||
|
||||
return asset_data
|
||||
|
||||
def execute(self, context):
|
||||
|
|
|
@ -2,6 +2,7 @@ import bpy
|
|||
import os
|
||||
import time
|
||||
|
||||
|
||||
def get_orig_render_settings():
|
||||
rs = bpy.context.scene.render
|
||||
ims = rs.image_settings
|
||||
|
@ -33,7 +34,8 @@ def set_orig_render_settings(orig_settings):
|
|||
vs.view_transform = orig_settings['view_transform']
|
||||
|
||||
|
||||
def img_save_as(img, filepath='//', file_format='JPEG', quality=90, color_mode='RGB', compression=15, view_transform = 'Raw', exr_codec = 'DWAA'):
|
||||
def img_save_as(img, filepath='//', file_format='JPEG', quality=90, color_mode='RGB', compression=15,
|
||||
view_transform='Raw', exr_codec='DWAA'):
|
||||
'''Uses Blender 'save render' to save images - BLender isn't really able so save images with other methods correctly.'''
|
||||
|
||||
ors = get_orig_render_settings()
|
||||
|
@ -49,11 +51,11 @@ def img_save_as(img, filepath='//', file_format='JPEG', quality=90, color_mode='
|
|||
ims.exr_codec = exr_codec
|
||||
vs.view_transform = view_transform
|
||||
|
||||
|
||||
img.save_render(filepath=bpy.path.abspath(filepath), scene=bpy.context.scene)
|
||||
|
||||
set_orig_render_settings(ors)
|
||||
|
||||
|
||||
def set_colorspace(img, colorspace):
|
||||
'''sets image colorspace, but does so in a try statement, because some people might actually replace the default
|
||||
colorspace settings, and it literally can't be guessed what these people use, even if it will mostly be the filmic addon.
|
||||
|
@ -66,11 +68,22 @@ def set_colorspace(img, colorspace):
|
|||
except:
|
||||
print(f'Colorspace {colorspace} not found.')
|
||||
|
||||
def analyze_image_is_true_hdr(image):
|
||||
import numpy
|
||||
scene = bpy.context.scene
|
||||
ui_props = scene.blenderkitUI
|
||||
size = image.size
|
||||
imageWidth = size[0]
|
||||
imageHeight = size[1]
|
||||
tempBuffer = numpy.empty(imageWidth * imageHeight * 4, dtype=numpy.float32)
|
||||
image.pixels.foreach_get(tempBuffer)
|
||||
image.blenderkit.true_hdr = numpy.amax(tempBuffer) > 1.05
|
||||
|
||||
def generate_hdr_thumbnail():
|
||||
import numpy
|
||||
scene = bpy.context.scene
|
||||
ui_props = scene.blenderkitUI
|
||||
hdr_image = ui_props.hdr_upload_image#bpy.data.images.get(ui_props.hdr_upload_image)
|
||||
hdr_image = ui_props.hdr_upload_image # bpy.data.images.get(ui_props.hdr_upload_image)
|
||||
|
||||
base, ext = os.path.splitext(hdr_image.filepath)
|
||||
thumb_path = base + '.jpg'
|
||||
|
@ -90,6 +103,8 @@ def generate_hdr_thumbnail():
|
|||
|
||||
hdr_image.pixels.foreach_get(tempBuffer)
|
||||
|
||||
hdr_image.blenderkit.true_hdr = numpy.amax(tempBuffer) > 1.05
|
||||
|
||||
inew.filepath = thumb_path
|
||||
set_colorspace(inew, 'Linear')
|
||||
inew.pixels.foreach_set(tempBuffer)
|
||||
|
@ -103,29 +118,31 @@ def generate_hdr_thumbnail():
|
|||
|
||||
def find_color_mode(image):
|
||||
if not isinstance(image, bpy.types.Image):
|
||||
raise(TypeError)
|
||||
raise (TypeError)
|
||||
else:
|
||||
depth_mapping = {
|
||||
8: 'BW',
|
||||
24: 'RGB',
|
||||
32: 'RGBA',#can also be bw.. but image.channels doesn't work.
|
||||
32: 'RGBA', # can also be bw.. but image.channels doesn't work.
|
||||
96: 'RGB',
|
||||
128: 'RGBA',
|
||||
}
|
||||
return depth_mapping.get(image.depth,'RGB')
|
||||
return depth_mapping.get(image.depth, 'RGB')
|
||||
|
||||
|
||||
def find_image_depth(image):
|
||||
if not isinstance(image, bpy.types.Image):
|
||||
raise(TypeError)
|
||||
raise (TypeError)
|
||||
else:
|
||||
depth_mapping = {
|
||||
8: '8',
|
||||
24: '8',
|
||||
32: '8',#can also be bw.. but image.channels doesn't work.
|
||||
32: '8', # can also be bw.. but image.channels doesn't work.
|
||||
96: '16',
|
||||
128: '16',
|
||||
}
|
||||
return depth_mapping.get(image.depth,'8')
|
||||
return depth_mapping.get(image.depth, '8')
|
||||
|
||||
|
||||
def can_erase_alpha(na):
|
||||
alpha = na[3::4]
|
||||
|
@ -148,6 +165,7 @@ def is_image_black(na):
|
|||
print('image can have alpha channel dropped')
|
||||
return rgbsum == 0
|
||||
|
||||
|
||||
def is_image_bw(na):
|
||||
r = na[::4]
|
||||
g = na[1::4]
|
||||
|
@ -186,7 +204,8 @@ def numpytoimage(a, iname, width=0, height=0, channels=3):
|
|||
if image.name[:len(iname)] == iname and image.size[0] == width and image.size[1] == height:
|
||||
i = image
|
||||
if i is None:
|
||||
i = bpy.data.images.new(iname, width, height, alpha=False, float_buffer=False, stereo3d=False, is_data=False, tiled=False)
|
||||
i = bpy.data.images.new(iname, width, height, alpha=False, float_buffer=False, stereo3d=False, is_data=False,
|
||||
tiled=False)
|
||||
|
||||
# dropping this re-shaping code - just doing flat array for speed and simplicity
|
||||
# d = a.shape[0] * a.shape[1]
|
||||
|
@ -220,6 +239,7 @@ def imagetonumpy_flat(i):
|
|||
# print('\ntime of image to numpy ' + str(time.time() - t))
|
||||
return na
|
||||
|
||||
|
||||
def imagetonumpy(i):
|
||||
t = time.time()
|
||||
|
||||
|
@ -273,18 +293,19 @@ def get_rgb_mean(i):
|
|||
# return(rmedian,gmedian, bmedian)
|
||||
return (rmean, gmean, bmean)
|
||||
|
||||
|
||||
def check_nmap_mean_ok(i):
|
||||
'''checks if normal map values are in standard range.'''
|
||||
|
||||
rmean,gmean,bmean = get_rgb_mean(i)
|
||||
rmean, gmean, bmean = get_rgb_mean(i)
|
||||
|
||||
#we could/should also check blue, but some ogl substance exports have 0-1, while 90% nmaps have 0.5 - 1.
|
||||
nmap_ok = 0.45< rmean < 0.55 and .45 < gmean < .55
|
||||
# we could/should also check blue, but some ogl substance exports have 0-1, while 90% nmaps have 0.5 - 1.
|
||||
nmap_ok = 0.45 < rmean < 0.55 and .45 < gmean < .55
|
||||
|
||||
return nmap_ok
|
||||
|
||||
|
||||
def check_nmap_ogl_vs_dx(i, mask = None, generated_test_images = False):
|
||||
def check_nmap_ogl_vs_dx(i, mask=None, generated_test_images=False):
|
||||
'''
|
||||
checks if normal map is directX or OpenGL.
|
||||
Returns - String value - DirectX and OpenGL
|
||||
|
@ -293,8 +314,6 @@ def check_nmap_ogl_vs_dx(i, mask = None, generated_test_images = False):
|
|||
width = i.size[0]
|
||||
height = i.size[1]
|
||||
|
||||
|
||||
|
||||
rmean, gmean, bmean = get_rgb_mean(i)
|
||||
|
||||
na = imagetonumpy(i)
|
||||
|
@ -306,8 +325,8 @@ def check_nmap_ogl_vs_dx(i, mask = None, generated_test_images = False):
|
|||
green_y_comparison = numpy.zeros((width, height), numpy.float32)
|
||||
|
||||
if generated_test_images:
|
||||
red_x_comparison_img = numpy.empty((width, height, 4), numpy.float32) #images for debugging purposes
|
||||
green_y_comparison_img = numpy.empty((width, height, 4), numpy.float32)#images for debugging purposes
|
||||
red_x_comparison_img = numpy.empty((width, height, 4), numpy.float32) # images for debugging purposes
|
||||
green_y_comparison_img = numpy.empty((width, height, 4), numpy.float32) # images for debugging purposes
|
||||
|
||||
ogl = numpy.zeros((width, height), numpy.float32)
|
||||
dx = numpy.zeros((width, height), numpy.float32)
|
||||
|
@ -318,21 +337,21 @@ def check_nmap_ogl_vs_dx(i, mask = None, generated_test_images = False):
|
|||
|
||||
for y in range(0, height):
|
||||
for x in range(0, width):
|
||||
#try to mask with UV mask image
|
||||
if mask is None or mask[x,y,3]>0:
|
||||
# try to mask with UV mask image
|
||||
if mask is None or mask[x, y, 3] > 0:
|
||||
|
||||
last_height_x = ogl[max(x - 1, 0), min(y, height - 1)]
|
||||
last_height_y = ogl[max(x,0), min(y - 1,height-1)]
|
||||
last_height_y = ogl[max(x, 0), min(y - 1, height - 1)]
|
||||
|
||||
diff_x = ((na[x, y, 0] - rmean) / ((na[x, y, 2] - 0.5)))
|
||||
diff_y = ((na[x, y, 1] - gmean) / ((na[x, y, 2] - 0.5)))
|
||||
calc_height = (last_height_x + last_height_y) \
|
||||
- diff_x - diff_y
|
||||
calc_height = calc_height /2
|
||||
- diff_x - diff_y
|
||||
calc_height = calc_height / 2
|
||||
ogl[x, y] = calc_height
|
||||
if generated_test_images:
|
||||
rgb = calc_height *.1 +.5
|
||||
ogl_img[x,y] = [rgb,rgb,rgb,1]
|
||||
rgb = calc_height * .1 + .5
|
||||
ogl_img[x, y] = [rgb, rgb, rgb, 1]
|
||||
|
||||
# green channel
|
||||
last_height_x = dx[max(x - 1, 0), min(y, height - 1)]
|
||||
|
@ -348,7 +367,6 @@ def check_nmap_ogl_vs_dx(i, mask = None, generated_test_images = False):
|
|||
rgb = calc_height * .1 + .5
|
||||
dx_img[x, y] = [rgb, rgb, rgb, 1]
|
||||
|
||||
|
||||
ogl_std = ogl.std()
|
||||
dx_std = dx.std()
|
||||
|
||||
|
@ -362,7 +380,6 @@ def check_nmap_ogl_vs_dx(i, mask = None, generated_test_images = False):
|
|||
else:
|
||||
print('this is probably an OpenGL texture')
|
||||
|
||||
|
||||
if generated_test_images:
|
||||
# red_x_comparison_img = red_x_comparison_img.swapaxes(0,1)
|
||||
# red_x_comparison_img = red_x_comparison_img.flatten()
|
||||
|
@ -383,9 +400,10 @@ def check_nmap_ogl_vs_dx(i, mask = None, generated_test_images = False):
|
|||
numpytoimage(dx_img, 'DirectX', width=width, height=height, channels=1)
|
||||
|
||||
if abs(ogl_std) > abs(dx_std):
|
||||
return 'DirectX'
|
||||
return 'DirectX'
|
||||
return 'OpenGL'
|
||||
|
||||
|
||||
def make_possible_reductions_on_image(teximage, input_filepath, do_reductions=False, do_downscale=False):
|
||||
'''checks the image and saves it to drive with possibly reduced channels.
|
||||
Also can remove the image from the asset if the image is pure black
|
||||
|
@ -396,7 +414,7 @@ def make_possible_reductions_on_image(teximage, input_filepath, do_reductions=Fa
|
|||
'''
|
||||
colorspace = teximage.colorspace_settings.name
|
||||
teximage.colorspace_settings.name = 'Non-Color'
|
||||
#teximage.colorspace_settings.name = 'sRGB' color correction mambo jambo.
|
||||
# teximage.colorspace_settings.name = 'sRGB' color correction mambo jambo.
|
||||
|
||||
JPEG_QUALITY = 90
|
||||
# is_image_black(na)
|
||||
|
@ -429,7 +447,7 @@ def make_possible_reductions_on_image(teximage, input_filepath, do_reductions=Fa
|
|||
image_depth = find_image_depth(teximage)
|
||||
|
||||
ims.color_mode = find_color_mode(teximage)
|
||||
#image_depth = str(max(min(int(teximage.depth / 3), 16), 8))
|
||||
# image_depth = str(max(min(int(teximage.depth / 3), 16), 8))
|
||||
print('resulting depth set to:', image_depth)
|
||||
|
||||
fp = input_filepath
|
||||
|
@ -469,8 +487,6 @@ def make_possible_reductions_on_image(teximage, input_filepath, do_reductions=Fa
|
|||
if do_downscale:
|
||||
downscale(teximage)
|
||||
|
||||
|
||||
|
||||
# it's actually very important not to try to change the image filepath and packed file filepath before saving,
|
||||
# blender tries to re-pack the image after writing to image.packed_image.filepath and reverts any changes.
|
||||
teximage.save_render(filepath=bpy.path.abspath(fp), scene=bpy.context.scene)
|
||||
|
@ -486,4 +502,4 @@ def make_possible_reductions_on_image(teximage, input_filepath, do_reductions=Fa
|
|||
ims.quality = orig_quality
|
||||
ims.color_mode = orig_color_mode
|
||||
ims.compression = orig_compression
|
||||
ims.color_depth = orig_depth
|
||||
ims.color_depth = orig_depth
|
||||
|
|
|
@ -310,7 +310,6 @@ def get_download_filepaths(asset_data, resolution='blend', can_return_others = F
|
|||
'''Get all possible paths of the asset and resolution. Usually global and local directory.'''
|
||||
dirs = get_download_dirs(asset_data['assetType'])
|
||||
res_file, resolution = get_res_file(asset_data, resolution, find_closest_with_url = can_return_others)
|
||||
|
||||
name_slug = slugify(asset_data['name'])
|
||||
asset_folder_name = f"{name_slug}_{asset_data['id']}"
|
||||
|
||||
|
|
|
@ -448,7 +448,7 @@ def search_timer():
|
|||
headers = utils.get_headers(api_key)
|
||||
if utils.profile_is_validator():
|
||||
for r in rdata['results']:
|
||||
if ratings_utils.get_rating_local(asset_data['id']) is None:
|
||||
if ratings_utils.get_rating_local(r['id']) is None:
|
||||
rating_thread = threading.Thread(target=ratings_utils.get_rating, args=([r['id'], headers]),
|
||||
daemon=True)
|
||||
rating_thread.start()
|
||||
|
@ -1133,9 +1133,12 @@ def build_query_HDR():
|
|||
props = bpy.context.window_manager.blenderkit_HDR
|
||||
query = {
|
||||
"asset_type": 'hdr',
|
||||
|
||||
# "engine": props.search_engine,
|
||||
# "adult": props.search_adult,
|
||||
}
|
||||
if props.true_hdr:
|
||||
query["trueHDR"] = props.true_hdr
|
||||
build_query_common(query, props)
|
||||
return query
|
||||
|
||||
|
@ -1283,6 +1286,8 @@ def get_search_simple(parameters, filepath=None, page_size=100, max_results=1000
|
|||
requeststring += f'+{p}:{parameters[p]}'
|
||||
|
||||
requeststring += '&page_size=' + str(page_size)
|
||||
requeststring += '&dict_parameters=1'
|
||||
|
||||
bk_logger.debug(requeststring)
|
||||
response = rerequests.get(requeststring, headers=headers) # , params = rparameters)
|
||||
# print(response.json())
|
||||
|
@ -1425,6 +1430,8 @@ def update_filters():
|
|||
sprops.search_polycount
|
||||
elif ui_props.asset_type == 'MATERIAL':
|
||||
sprops.use_filters = fcommon
|
||||
elif ui_props.asset_type == 'HDR':
|
||||
sprops.use_filters = sprops.true_hdr
|
||||
|
||||
|
||||
def search_update(self, context):
|
||||
|
@ -1491,7 +1498,8 @@ class SearchOperator(Operator):
|
|||
|
||||
own: BoolProperty(name="own assets only",
|
||||
description="Find all own assets",
|
||||
default=False)
|
||||
default=False,
|
||||
options={'SKIP_SAVE'})
|
||||
|
||||
category: StringProperty(
|
||||
name="category",
|
||||
|
|
|
@ -870,6 +870,32 @@ class VIEW3D_PT_blenderkit_advanced_material_search(Panel):
|
|||
row.prop(props, "search_file_size_max", text='Max')
|
||||
layout.prop(props, "quality_limit", slider=True)
|
||||
|
||||
class VIEW3D_PT_blenderkit_advanced_HDR_search(Panel):
|
||||
bl_category = "BlenderKit"
|
||||
bl_idname = "VIEW3D_PT_blenderkit_advanced_HDR_search"
|
||||
bl_parent_id = "VIEW3D_PT_blenderkit_unified"
|
||||
bl_space_type = 'VIEW_3D'
|
||||
bl_region_type = 'UI'
|
||||
bl_label = "Search filters"
|
||||
bl_options = {'DEFAULT_CLOSED'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
s = context.scene
|
||||
ui_props = s.blenderkitUI
|
||||
return ui_props.down_up == 'SEARCH' and ui_props.asset_type == 'HDR'
|
||||
|
||||
def draw(self, context):
|
||||
wm = context.window_manager
|
||||
props = wm.blenderkit_HDR
|
||||
layout = self.layout
|
||||
layout.separator()
|
||||
|
||||
layout.prop(props, "own_only")
|
||||
layout.prop(props, "true_hdr")
|
||||
|
||||
|
||||
|
||||
class VIEW3D_PT_blenderkit_categories(Panel):
|
||||
bl_category = "BlenderKit"
|
||||
bl_idname = "VIEW3D_PT_blenderkit_categories"
|
||||
|
@ -891,6 +917,7 @@ class VIEW3D_PT_blenderkit_categories(Panel):
|
|||
def draw(self, context):
|
||||
draw_panel_categories(self, context)
|
||||
|
||||
|
||||
def draw_scene_import_settings(self, context):
|
||||
wm = bpy.context.window_manager
|
||||
props = wm.blenderkit_scene
|
||||
|
@ -945,7 +972,7 @@ class VIEW3D_PT_blenderkit_import_settings(Panel):
|
|||
|
||||
row.prop(props, 'append_method', expand=True, icon_only=False)
|
||||
if ui_props.asset_type == 'SCENE':
|
||||
draw_scene_import_settings(self,context)
|
||||
draw_scene_import_settings(self, context)
|
||||
|
||||
if ui_props.asset_type == 'HDR':
|
||||
props = wm.blenderkit_HDR
|
||||
|
@ -1149,35 +1176,35 @@ class BlenderKitWelcomeOperator(bpy.types.Operator):
|
|||
print('running search no')
|
||||
ui_props = bpy.context.scene.blenderkitUI
|
||||
random_searches = [
|
||||
('MATERIAL','ice'),
|
||||
('MODEL','car'),
|
||||
('MODEL','vase'),
|
||||
('MODEL','grass'),
|
||||
('MODEL','plant'),
|
||||
('MODEL','man'),
|
||||
('MATERIAL','metal'),
|
||||
('MATERIAL','wood'),
|
||||
('MATERIAL','floor'),
|
||||
('MATERIAL','bricks'),
|
||||
('MATERIAL', 'ice'),
|
||||
('MODEL', 'car'),
|
||||
('MODEL', 'vase'),
|
||||
('MODEL', 'grass'),
|
||||
('MODEL', 'plant'),
|
||||
('MODEL', 'man'),
|
||||
('MATERIAL', 'metal'),
|
||||
('MATERIAL', 'wood'),
|
||||
('MATERIAL', 'floor'),
|
||||
('MATERIAL', 'bricks'),
|
||||
]
|
||||
random_search = random.choice(random_searches)
|
||||
ui_props.asset_type = random_search[0]
|
||||
|
||||
bpy.context.window_manager.blenderkit_mat.search_keywords = ''#random_search[1]
|
||||
bpy.context.window_manager.blenderkit_mat.search_keywords = '+is_free:true+score_gte:1000+order:-created'#random_search[1]
|
||||
bpy.context.window_manager.blenderkit_mat.search_keywords = '' # random_search[1]
|
||||
bpy.context.window_manager.blenderkit_mat.search_keywords = '+is_free:true+score_gte:1000+order:-created' # random_search[1]
|
||||
# search.search()
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
wm = bpy.context.window_manager
|
||||
img = utils.get_thumbnail('intro.jpg')
|
||||
utils.img_to_preview(img, copy_original = True)
|
||||
utils.img_to_preview(img, copy_original=True)
|
||||
self.img = img
|
||||
w, a, r = utils.get_largest_area(area_type='VIEW_3D')
|
||||
if a is not None:
|
||||
a.spaces.active.show_region_ui = True
|
||||
|
||||
return wm.invoke_props_dialog(self, width = 500)
|
||||
return wm.invoke_props_dialog(self, width=500)
|
||||
|
||||
|
||||
def draw_asset_context_menu(layout, context, asset_data, from_panel=False):
|
||||
|
@ -1396,8 +1423,8 @@ class OBJECT_MT_blenderkit_asset_menu(bpy.types.Menu):
|
|||
|
||||
def numeric_to_str(s):
|
||||
if s:
|
||||
if s<1:
|
||||
s = str(round(s,1))
|
||||
if s < 1:
|
||||
s = str(round(s, 1))
|
||||
else:
|
||||
s = str(round(s))
|
||||
else:
|
||||
|
@ -1405,14 +1432,32 @@ def numeric_to_str(s):
|
|||
return s
|
||||
|
||||
|
||||
def push_op_left(layout, strength =3):
|
||||
def push_op_left(layout, strength=3):
|
||||
for a in range(0, strength):
|
||||
layout.label(text='')
|
||||
|
||||
|
||||
def label_or_url(layout, text='', tooltip='', url='', icon_value=None, icon=None):
|
||||
def label_or_url_or_operator(layout, text='', tooltip='', url='', operator=None, operator_kwargs={}, icon_value=None,
|
||||
icon=None):
|
||||
'''automatically switch between different layout options for linking or tooltips'''
|
||||
layout.emboss = 'NONE'
|
||||
|
||||
if operator is not None:
|
||||
if icon:
|
||||
op = layout.operator(operator, text=text, icon=icon)
|
||||
elif icon_value:
|
||||
op = layout.operator(operator, text=text, icon_value=icon_value)
|
||||
else:
|
||||
op = layout.operator(operator, text=text)
|
||||
for kwarg in operator_kwargs.keys():
|
||||
if type(operator_kwargs[kwarg]) == str:
|
||||
quoatation = "'"
|
||||
else:
|
||||
quoatation = ""
|
||||
exec(f"op.{kwarg} = {quoatation}{operator_kwargs[kwarg]}{quoatation}")
|
||||
push_op_left(layout, strength=2)
|
||||
|
||||
return
|
||||
if url != '':
|
||||
if icon:
|
||||
op = layout.operator('wm.blenderkit_url', text=text, icon=icon)
|
||||
|
@ -1422,7 +1467,7 @@ def label_or_url(layout, text='', tooltip='', url='', icon_value=None, icon=None
|
|||
op = layout.operator('wm.blenderkit_url', text=text)
|
||||
op.url = url
|
||||
op.tooltip = tooltip
|
||||
push_op_left(layout, strength = 5)
|
||||
push_op_left(layout, strength=5)
|
||||
|
||||
return
|
||||
if tooltip != '':
|
||||
|
@ -1435,7 +1480,7 @@ def label_or_url(layout, text='', tooltip='', url='', icon_value=None, icon=None
|
|||
op.tooltip = tooltip
|
||||
|
||||
# these are here to move the text to left, since operators can only center text by default
|
||||
push_op_left(layout, strength = 3)
|
||||
push_op_left(layout, strength=3)
|
||||
return
|
||||
if icon:
|
||||
layout.label(text=text, icon=icon)
|
||||
|
@ -1460,7 +1505,8 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingsProperties):
|
|||
# layout = layout.column()
|
||||
draw_asset_context_menu(layout, context, self.asset_data, from_panel=False)
|
||||
|
||||
def draw_property(self, layout, left, right, icon=None, icon_value=None, url='', tooltip=''):
|
||||
def draw_property(self, layout, left, right, icon=None, icon_value=None, url='', tooltip='', operator=None,
|
||||
operator_kwargs={}):
|
||||
right = str(right)
|
||||
row = layout.row()
|
||||
split = row.split(factor=0.35)
|
||||
|
@ -1471,7 +1517,8 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingsProperties):
|
|||
# split for questionmark:
|
||||
if url != '':
|
||||
split = split.split(factor=0.6)
|
||||
label_or_url(split, text=right, tooltip=tooltip, url=url, icon_value=icon_value, icon=icon)
|
||||
label_or_url_or_operator(split, text=right, tooltip=tooltip, url=url, operator=operator,
|
||||
operator_kwargs=operator_kwargs, icon_value=icon_value, icon=icon)
|
||||
# additional questionmark icon where it's important?
|
||||
if url != '':
|
||||
split = split.split()
|
||||
|
@ -1479,7 +1526,7 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingsProperties):
|
|||
op.url = url
|
||||
op.tooltip = tooltip
|
||||
|
||||
def draw_asset_parameter(self, layout, key='', pretext=''):
|
||||
def draw_asset_parameter(self, layout, key='', pretext='', do_search=False):
|
||||
parameter = utils.get_param(self.asset_data, key)
|
||||
if parameter == None:
|
||||
return
|
||||
|
@ -1487,7 +1534,15 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingsProperties):
|
|||
parameter = f"{parameter:,d}"
|
||||
elif type(parameter) == float:
|
||||
parameter = f"{parameter:,.1f}"
|
||||
self.draw_property(layout, pretext, parameter)
|
||||
if do_search:
|
||||
kwargs = {
|
||||
'esc': True,
|
||||
'keywords': f'+{key}:{parameter}',
|
||||
'tooltip': f'search by {parameter}',
|
||||
}
|
||||
self.draw_property(layout, pretext, parameter, operator='view3d.blenderkit_search', operator_kwargs=kwargs)
|
||||
else:
|
||||
self.draw_property(layout, pretext, parameter)
|
||||
|
||||
def draw_description(self, layout, width=250):
|
||||
if len(self.asset_data['description']) > 0:
|
||||
|
@ -1495,7 +1550,7 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingsProperties):
|
|||
box.scale_y = 0.4
|
||||
box.label(text='Description')
|
||||
box.separator()
|
||||
link_more = utils.label_multiline(box, self.asset_data['description'], width=width, max_lines = 10)
|
||||
link_more = utils.label_multiline(box, self.asset_data['description'], width=width, max_lines=10)
|
||||
if link_more:
|
||||
row = box.row()
|
||||
row.scale_y = 2
|
||||
|
@ -1588,9 +1643,10 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingsProperties):
|
|||
resolutions = resolutions.replace('_', '.')
|
||||
self.draw_property(box, 'Generated', resolutions)
|
||||
|
||||
self.draw_asset_parameter(box, key='designer', pretext='Designer')
|
||||
self.draw_asset_parameter(box, key='manufacturer', pretext='Manufacturer') # TODO make them clickable!
|
||||
self.draw_asset_parameter(box, key='designCollection', pretext='Collection')
|
||||
self.draw_asset_parameter(box, key='designer', pretext='Designer', do_search=True)
|
||||
self.draw_asset_parameter(box, key='manufacturer', pretext='Manufacturer',
|
||||
do_search=True)
|
||||
self.draw_asset_parameter(box, key='designCollection', pretext='Collection', do_search=True)
|
||||
self.draw_asset_parameter(box, key='designVariant', pretext='Variant')
|
||||
self.draw_asset_parameter(box, key='designYear', pretext='Design year')
|
||||
|
||||
|
@ -1674,7 +1730,7 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingsProperties):
|
|||
self.draw_property(box, 'Created', date)
|
||||
if utils.asset_from_newer_blender_version(self.asset_data):
|
||||
# row = box.row()
|
||||
box.alert=True
|
||||
box.alert = True
|
||||
self.draw_property(box,
|
||||
'Blender version',
|
||||
self.asset_data['sourceAppVersion'],
|
||||
|
@ -1751,7 +1807,6 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingsProperties):
|
|||
box_thumbnail.scale_y = .4
|
||||
box_thumbnail.template_icon(icon_value=self.img.preview.icon_id, scale=width * .12)
|
||||
|
||||
|
||||
# op = row.operator('view3d.asset_drag_drop', text='Drag & Drop from here', depress=True)
|
||||
# From here on, only ratings are drawn, which won't be displayed for private assets from now on.
|
||||
|
||||
|
@ -1798,7 +1853,8 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingsProperties):
|
|||
if rcount <= show_rating_prompt_threshold:
|
||||
box_thumbnail.alert = True
|
||||
box_thumbnail.label(text=f"")
|
||||
box_thumbnail.label(text=f"This asset has only {rcount} rating{'' if rcount == 1 else 's'}, please rate.")
|
||||
box_thumbnail.label(
|
||||
text=f"This asset has only {rcount} rating{'' if rcount == 1 else 's'}, please rate.")
|
||||
# box_thumbnail.label(text=f"Please rate this asset.")
|
||||
|
||||
row = box_thumbnail.row()
|
||||
|
@ -1841,29 +1897,17 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingsProperties):
|
|||
|
||||
# define enum flags
|
||||
|
||||
|
||||
|
||||
|
||||
def draw(self, context):
|
||||
ui_props = context.scene.blenderkitUI
|
||||
|
||||
sr = bpy.context.window_manager['search results']
|
||||
asset_data = sr[ui_props.active_index]
|
||||
self.asset_data = asset_data
|
||||
layout = self.layout
|
||||
# top draggabe bar with name of the asset
|
||||
top_row = layout.row()
|
||||
top_drag_bar = top_row.box()
|
||||
bcats = bpy.context.window_manager['bkit_categories']
|
||||
def draw_titlebar(self, context, layout):
|
||||
top_drag_bar = layout.box()
|
||||
bcats = bpy.context.window_manager['bkit_categories']
|
||||
|
||||
cat_path = categories.get_category_path(bcats,
|
||||
self.asset_data['category'])[1:]
|
||||
|
||||
|
||||
cat_path_names = categories.get_category_name_path(bcats,
|
||||
self.asset_data['category'])[1:]
|
||||
self.asset_data['category'])[1:]
|
||||
|
||||
aname = asset_data['displayName']
|
||||
aname = self.asset_data['displayName']
|
||||
aname = aname[0].upper() + aname[1:]
|
||||
|
||||
if 1:
|
||||
|
@ -1873,7 +1917,7 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingsProperties):
|
|||
# name_row = name_row.row()
|
||||
for i, c in enumerate(cat_path):
|
||||
cat_name = cat_path_names[i]
|
||||
op = name_row.operator('view3d.blenderkit_asset_bar', text=cat_name + ' >', emboss=False)
|
||||
op = name_row.operator('view3d.blenderkit_asset_bar', text=cat_name + ' >', emboss=True)
|
||||
op.do_search = True
|
||||
op.keep_running = True
|
||||
op.tooltip = f"Browse {cat_name} category"
|
||||
|
@ -1881,23 +1925,16 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingsProperties):
|
|||
# name_row.label(text='>')
|
||||
|
||||
name_row.label(text=aname)
|
||||
push_op_left(name_row, strength = 3)
|
||||
op = name_row.operator('view3d.close_popup_button', text='', icon = 'CANCEL')
|
||||
|
||||
# for i,c in enumerate(cat_path_names):
|
||||
# cat_path_names[i] = c.capitalize()
|
||||
# cat_path_names_string = ' > '.join(cat_path_names)
|
||||
# # box.label(text=cat_path)
|
||||
#
|
||||
#
|
||||
#
|
||||
#
|
||||
# # name_row.label(text=' ')
|
||||
# top_drag_bar.label(text=f'{cat_path_names_string} > {aname}')
|
||||
push_op_left(name_row, strength=3)
|
||||
op = name_row.operator('view3d.close_popup_button', text='', icon='CANCEL')
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
# top draggable bar with name of the asset
|
||||
top_row = layout.row()
|
||||
self.draw_titlebar(context, top_row)
|
||||
# left side
|
||||
row = layout.row(align=True)
|
||||
|
||||
split_ratio = 0.45
|
||||
split_left = row.split(factor=split_ratio)
|
||||
left_column = split_left.column()
|
||||
|
@ -1907,7 +1944,7 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingsProperties):
|
|||
split_right = split_left.split()
|
||||
self.draw_menu_desc_author(context, split_right, width=int(self.width * (1 - split_ratio)))
|
||||
|
||||
if not utils.user_is_owner(asset_data=asset_data):
|
||||
if not utils.user_is_owner(asset_data=self.asset_data):
|
||||
# Draw ratings, but not for owners of assets - doesn't make sense.
|
||||
ratings_box = layout.box()
|
||||
ratings.draw_ratings_menu(self, context, ratings_box)
|
||||
|
@ -1923,8 +1960,10 @@ class AssetPopupCard(bpy.types.Operator, ratings_utils.RatingsProperties):
|
|||
ui_props.draw_tooltip = False
|
||||
sr = bpy.context.window_manager['search results']
|
||||
asset_data = sr[ui_props.active_index]
|
||||
self.asset_data = asset_data
|
||||
|
||||
self.img = ui.get_large_thumbnail_image(asset_data)
|
||||
utils.img_to_preview(self.img, copy_original = True)
|
||||
utils.img_to_preview(self.img, copy_original=True)
|
||||
|
||||
self.asset_type = asset_data['assetType']
|
||||
self.asset_id = asset_data['id']
|
||||
|
@ -1988,6 +2027,7 @@ class SetCategoryOperator(bpy.types.Operator):
|
|||
bpy.context.window_manager['active_category'][self.asset_type] = acat
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class ClosePopupButton(bpy.types.Operator):
|
||||
"""Visit subcategory"""
|
||||
bl_idname = "view3d.close_popup_button"
|
||||
|
@ -2001,26 +2041,25 @@ class ClosePopupButton(bpy.types.Operator):
|
|||
def win_close(self):
|
||||
VK_ESCAPE = 0x1B
|
||||
ctypes.windll.user32.keybd_event(VK_ESCAPE)
|
||||
print('hit escape')
|
||||
return True
|
||||
|
||||
def mouse_trick(self,context,x,y):
|
||||
def mouse_trick(self, context, x, y):
|
||||
# import time
|
||||
context.area.tag_redraw()
|
||||
w = context.window
|
||||
w.cursor_warp(w.x+15,w.y+w.height-15);
|
||||
w.cursor_warp(w.x + 15, w.y + w.height - 15);
|
||||
# time.sleep(.12)
|
||||
w.cursor_warp(x,y);
|
||||
w.cursor_warp(x, y);
|
||||
context.area.tag_redraw()
|
||||
|
||||
|
||||
def invoke(self, context, event):
|
||||
if platform.system() == 'Windows':
|
||||
self.win_close()
|
||||
else:
|
||||
self.mouse_trick(context,event.mouse_x, event.mouse_y)
|
||||
self.mouse_trick(context, event.mouse_x, event.mouse_y)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class UrlPopupDialog(bpy.types.Operator):
|
||||
"""Generate Cycles thumbnail for model assets"""
|
||||
bl_idname = "wm.blenderkit_url_dialog"
|
||||
|
@ -2221,7 +2260,7 @@ def header_search_draw(self, context):
|
|||
layout.prop(ui_props, "asset_type", expand=True, icon_only=True, text='', icon='URL')
|
||||
layout.prop(props, "search_keywords", text="", icon='VIEWZOOM')
|
||||
draw_assetbar_show_hide(layout, props)
|
||||
layout.popover(panel="VIEW3D_PT_blenderkit_categories", text="", icon = 'OUTLINER')
|
||||
layout.popover(panel="VIEW3D_PT_blenderkit_categories", text="", icon='OUTLINER')
|
||||
|
||||
pcoll = icons.icon_collections["main"]
|
||||
|
||||
|
@ -2230,11 +2269,13 @@ def header_search_draw(self, context):
|
|||
else:
|
||||
icon_id = pcoll['filter'].icon_id
|
||||
|
||||
if ui_props.asset_type=='MODEL':
|
||||
layout.popover(panel="VIEW3D_PT_blenderkit_advanced_model_search", text="", icon_value = icon_id)
|
||||
if ui_props.asset_type == 'MODEL':
|
||||
layout.popover(panel="VIEW3D_PT_blenderkit_advanced_model_search", text="", icon_value=icon_id)
|
||||
|
||||
elif ui_props.asset_type=='MATERIAL':
|
||||
layout.popover(panel="VIEW3D_PT_blenderkit_advanced_material_search", text="", icon_value = icon_id)
|
||||
elif ui_props.asset_type == 'MATERIAL':
|
||||
layout.popover(panel="VIEW3D_PT_blenderkit_advanced_material_search", text="", icon_value=icon_id)
|
||||
elif ui_props.asset_type == 'HDR':
|
||||
layout.popover(panel="VIEW3D_PT_blenderkit_advanced_HDR_search", text="", icon_value=icon_id)
|
||||
|
||||
|
||||
def ui_message(title, message):
|
||||
|
@ -2256,6 +2297,7 @@ classes = (
|
|||
VIEW3D_PT_blenderkit_unified,
|
||||
VIEW3D_PT_blenderkit_advanced_model_search,
|
||||
VIEW3D_PT_blenderkit_advanced_material_search,
|
||||
VIEW3D_PT_blenderkit_advanced_HDR_search,
|
||||
VIEW3D_PT_blenderkit_categories,
|
||||
VIEW3D_PT_blenderkit_import_settings,
|
||||
VIEW3D_PT_blenderkit_model_properties,
|
||||
|
|
|
@ -63,7 +63,6 @@ def get_app_version():
|
|||
return '%i.%i.%i' % (ver[0], ver[1], ver[2])
|
||||
|
||||
|
||||
|
||||
def add_version(data):
|
||||
app_version = get_app_version()
|
||||
addon_version = version_checker.get_addon_version()
|
||||
|
@ -444,6 +443,9 @@ def get_upload_data(caller=None, context=None, asset_type=None):
|
|||
return None, None
|
||||
|
||||
props = image.blenderkit
|
||||
|
||||
image_utils.analyze_image_is_true_hdr(image)
|
||||
|
||||
# props.name = brush.name
|
||||
base, ext = os.path.splitext(image.filepath)
|
||||
thumb_path = base + '.jpg'
|
||||
|
@ -460,8 +462,8 @@ def get_upload_data(caller=None, context=None, asset_type=None):
|
|||
# mat analytics happen here, since they don't take up any time...
|
||||
|
||||
upload_params = {
|
||||
"textureResolutionMax": props.texture_resolution_max
|
||||
|
||||
"textureResolutionMax": props.texture_resolution_max,
|
||||
"trueHDR": props.true_hdr
|
||||
}
|
||||
|
||||
upload_data = {
|
||||
|
@ -660,11 +662,8 @@ class FastMetadata(bpy.types.Operator):
|
|||
update=update_free_full
|
||||
)
|
||||
|
||||
|
||||
####################
|
||||
|
||||
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
scene = bpy.context.scene
|
||||
|
@ -729,7 +728,7 @@ class FastMetadata(bpy.types.Operator):
|
|||
asset_data = dict(sr[ui_props.active_index])
|
||||
else:
|
||||
|
||||
active_asset = utils.get_active_asset_by_type(asset_type = self.asset_type)
|
||||
active_asset = utils.get_active_asset_by_type(asset_type=self.asset_type)
|
||||
asset_data = active_asset.get('asset_data')
|
||||
|
||||
if not can_edit_asset(asset_data=asset_data):
|
||||
|
@ -1081,6 +1080,9 @@ def start_upload(self, context, asset_type, reupload, upload_set):
|
|||
if 'THUMBNAIL' in upload_set:
|
||||
if asset_type == 'HDR':
|
||||
image_utils.generate_hdr_thumbnail()
|
||||
# get upload data because the image utils function sets true_hdr
|
||||
export_data, upload_data = get_upload_data(caller=self, context=context, asset_type=asset_type)
|
||||
|
||||
elif not os.path.exists(export_data["thumbnail_path"]):
|
||||
props.upload_state = 'Thumbnail not found'
|
||||
props.uploading = False
|
||||
|
@ -1214,9 +1216,12 @@ class UploadOperator(Operator):
|
|||
layout.prop(self, 'thumbnail')
|
||||
|
||||
if props.asset_base_id != '' and not self.reupload:
|
||||
layout.label(text="Really upload as new? ")
|
||||
layout.label(text="Do this only when you create a new asset from an old one.")
|
||||
layout.label(text="For updates of thumbnail or model use reupload.")
|
||||
utils.label_multiline(layout, text="Really upload as new?\n"
|
||||
"Do this only when you create\n"
|
||||
"a new asset from an old one.\n"
|
||||
"For updates of thumbnail or model use reupload.\n",
|
||||
width=400, icon='ERROR')
|
||||
|
||||
|
||||
if props.is_private == 'PUBLIC':
|
||||
if self.asset_type == 'MODEL':
|
||||
|
@ -1229,6 +1234,22 @@ class UploadOperator(Operator):
|
|||
'- Check if it has all textures and renders as expected\n'
|
||||
'- Check if it has correct size in world units (for models)'
|
||||
, width=400)
|
||||
elif self.asset_type == 'HDR':
|
||||
if not props.true_hdr:
|
||||
utils.label_multiline(layout, text="This image isn't HDR,\n"
|
||||
"It has a low dynamic range.\n"
|
||||
"BlenderKit library accepts 360 degree images\n"
|
||||
"however the default filter setting for search\n"
|
||||
"is to show only true HDR images\n"
|
||||
, icon='ERROR', width=400)
|
||||
|
||||
utils.label_multiline(layout, text='You marked the asset as public.\n'
|
||||
'This means it will be validated by our team.\n\n'
|
||||
'Please test your upload after it finishes:\n'
|
||||
'- Open a new file\n'
|
||||
'- Find the asset and download it\n'
|
||||
'- Check if it works as expected\n'
|
||||
, width=400)
|
||||
else:
|
||||
utils.label_multiline(layout, text='You marked the asset as public.\n'
|
||||
'This means it will be validated by our team.\n\n'
|
||||
|
@ -1239,12 +1260,17 @@ class UploadOperator(Operator):
|
|||
, width=400)
|
||||
|
||||
def invoke(self, context, event):
|
||||
props = utils.get_upload_props()
|
||||
|
||||
if not utils.user_logged_in():
|
||||
ui_panels.draw_not_logged_in(self, message='To upload assets you need to login/signup.')
|
||||
return {'CANCELLED'}
|
||||
|
||||
if self.asset_type == 'HDR':
|
||||
props = utils.get_upload_props()
|
||||
# getting upload data for images ensures true_hdr check so users can be informed about their handling
|
||||
# simple 360 photos or renders with LDR are hidden by default..
|
||||
export_data, upload_data = get_upload_data(asset_type='HDR')
|
||||
|
||||
# if props.is_private == 'PUBLIC':
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
# else:
|
||||
|
|
|
@ -838,10 +838,11 @@ def user_is_owner(asset_data=None):
|
|||
def asset_from_newer_blender_version(asset_data):
|
||||
bver = bpy.app.version
|
||||
aver = asset_data['sourceAppVersion'].split('.')
|
||||
# print(aver,bver)
|
||||
#print(aver,bver)
|
||||
bver_f = bver[0] + bver[1] * .01 + bver[2] * .0001
|
||||
aver_f = int(aver[0]) + int(aver[1]) * .01 + int(aver[2]) * .0001
|
||||
return aver_f>bver_f
|
||||
if len(aver)>=3:
|
||||
aver_f = int(aver[0]) + int(aver[1]) * .01 + int(aver[2]) * .0001
|
||||
return aver_f>bver_f
|
||||
|
||||
def guard_from_crash():
|
||||
'''
|
||||
|
|
|
@ -333,7 +333,7 @@ def cancel_cage(self):
|
|||
self.gp_obj.grease_pencil_modifiers.remove(mod)
|
||||
else:
|
||||
print(f'tmp_lattice modifier not found to remove on {self.gp_obj.name}')
|
||||
|
||||
|
||||
for ob in self.other_gp:
|
||||
mod = ob.grease_pencil_modifiers.get('tmp_lattice')
|
||||
if mod:
|
||||
|
@ -586,7 +586,7 @@ valid:Spacebar/Enter, cancel:Del/Backspace/Tab/Ctrl+T"
|
|||
## silent return
|
||||
return {'CANCELLED'}
|
||||
|
||||
|
||||
|
||||
# bpy.ops.ed.undo_push(message="Box deform step")#don't work as expected (+ might be obsolete)
|
||||
# https://developer.blender.org/D6147 <- undo forget
|
||||
|
||||
|
|
|
@ -89,7 +89,7 @@ class GreasePencilAddonPrefs(bpy.types.AddonPreferences):
|
|||
name = "Use Hud",
|
||||
description = "Display angle lines and angle value as text on viewport",
|
||||
default = False)
|
||||
|
||||
|
||||
canvas_use_view_center: BoolProperty(
|
||||
name = "Rotate From View Center In Camera",
|
||||
description = "Rotate from view center in camera view, Else rotate from camera center",
|
||||
|
|
|
@ -136,7 +136,7 @@ class RC_OT_RotateCanvas(bpy.types.Operator):
|
|||
|
||||
## area deformation restore
|
||||
new_cam_offset = mathutils.Vector((new_cam_offset[0], new_cam_offset[1] * self.ratio_inv))
|
||||
|
||||
|
||||
context.space_data.region_3d.view_camera_offset = new_cam_offset
|
||||
|
||||
else: # free view
|
||||
|
@ -187,7 +187,7 @@ class RC_OT_RotateCanvas(bpy.types.Operator):
|
|||
# CORRECT UI OVERLAP FROM HEADER TOOLBAR
|
||||
regs = context.area.regions
|
||||
if context.preferences.system.use_region_overlap:
|
||||
w = context.area.width
|
||||
w = context.area.width
|
||||
# minus tool header
|
||||
h = context.area.height - regs[0].height
|
||||
else:
|
||||
|
@ -195,9 +195,9 @@ class RC_OT_RotateCanvas(bpy.types.Operator):
|
|||
w = context.area.width - regs[2].width - regs[3].width
|
||||
# minus tool header + header
|
||||
h = context.area.height - regs[0].height - regs[1].height
|
||||
|
||||
|
||||
self.ratio = h / w
|
||||
self.ratio_inv = w / h
|
||||
self.ratio_inv = w / h
|
||||
|
||||
if self.in_cam:
|
||||
# Get camera from scene
|
||||
|
@ -207,8 +207,8 @@ class RC_OT_RotateCanvas(bpy.types.Operator):
|
|||
if self.cam.lock_rotation[:] != (False, False, False):
|
||||
self.report({'WARNING'}, 'Camera rotation is locked')
|
||||
return {'CANCELLED'}
|
||||
|
||||
if self.use_view_center:
|
||||
|
||||
if self.use_view_center:
|
||||
self.center = mathutils.Vector((w/2, h/2))
|
||||
else:
|
||||
self.center = self.get_center_view(context, self.cam)
|
||||
|
@ -220,7 +220,7 @@ class RC_OT_RotateCanvas(bpy.types.Operator):
|
|||
# store camera matrix world
|
||||
self.cam_matrix = self.cam.matrix_world.copy()
|
||||
# self.cam_init_euler = self.cam.rotation_euler.copy()
|
||||
|
||||
|
||||
## initialize current view_offset in camera
|
||||
self.view_cam_offset = mathutils.Vector(context.space_data.region_3d.view_camera_offset)
|
||||
|
||||
|
|
|
@ -211,7 +211,7 @@ class GPTS_OT_time_scrub(bpy.types.Operator):
|
|||
else:
|
||||
self.init_index = 0
|
||||
self.init_frame = self.new_frame = self.pos[0]
|
||||
|
||||
|
||||
# del active_pos
|
||||
self.index_limit = len(self.pos) - 1
|
||||
|
||||
|
@ -311,14 +311,14 @@ class GPTS_OT_time_scrub(bpy.types.Operator):
|
|||
shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR') # initiate shader
|
||||
self.batch_timeline = batch_for_shader(
|
||||
shader, 'LINES', {"pos": self.hud_lines})
|
||||
|
||||
|
||||
if self.rolling_mode:
|
||||
current_id = self.pos.index(self.new_frame)
|
||||
# Add init_frame to "cancel" it in later UI code
|
||||
ui_key_pos = [i - current_id + self.init_frame for i, _f in enumerate(self.pos[:-2])]
|
||||
else:
|
||||
ui_key_pos = self.pos[:-2]
|
||||
|
||||
|
||||
|
||||
# keyframe display
|
||||
if self.keyframe_aspect == 'LINE':
|
||||
|
@ -716,7 +716,7 @@ def draw_ts_pref(prefs, layout):
|
|||
snap_text = 'Disable keyframes snap: '
|
||||
else:
|
||||
snap_text = 'Keyframes snap: '
|
||||
|
||||
|
||||
snap_text += 'Left Mouse' if prefs.keycode == 'RIGHTMOUSE' else 'Right Mouse'
|
||||
if not prefs.use_ctrl:
|
||||
snap_text += ' or Ctrl'
|
||||
|
@ -724,7 +724,7 @@ def draw_ts_pref(prefs, layout):
|
|||
snap_text += ' or Shift'
|
||||
if not prefs.use_alt:
|
||||
snap_text += ' or Alt'
|
||||
|
||||
|
||||
if prefs.rolling_mode:
|
||||
snap_text = 'Gap-less mode (always snap)'
|
||||
|
||||
|
|
|
@ -177,7 +177,7 @@ class ExportGLTF2_Base:
|
|||
name='Keep original',
|
||||
description=('Keep original textures files if possible. '
|
||||
'WARNING: if you use more than one texture, '
|
||||
'where pbr standard requires only one, only one texture will be used.'
|
||||
'where pbr standard requires only one, only one texture will be used. '
|
||||
'This can lead to unexpected results'
|
||||
),
|
||||
default=False,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
|
|
@ -1893,7 +1893,7 @@ class NWPreviewNode(Operator, NWBase):
|
|||
# Exit early
|
||||
if not valid:
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
delete_sockets = []
|
||||
|
||||
# Scan through all nodes in tree including nodes inside of groups to find viewer sockets
|
||||
|
@ -1926,7 +1926,7 @@ class NWPreviewNode(Operator, NWBase):
|
|||
if out_i is None:
|
||||
return {'FINISHED'}
|
||||
socket_type = 'GEOMETRY'
|
||||
# Find an input socket of the output of type geometry
|
||||
# Find an input socket of the output of type geometry
|
||||
geometryoutindex = None
|
||||
for i,inp in enumerate(geometryoutput.inputs):
|
||||
if inp.type == socket_type:
|
||||
|
@ -2430,13 +2430,13 @@ class NWMergeNodes(Operator, NWBase):
|
|||
# Check if the link connects to a node that is in selected_nodes
|
||||
# If not, then check recursively for each link in the nodes outputs.
|
||||
# If yes, return True. If the recursion stops without finding a node
|
||||
# in selected_nodes, it returns False. The depth is used to prevent
|
||||
# in selected_nodes, it returns False. The depth is used to prevent
|
||||
# getting stuck in a loop because of an already present cycle.
|
||||
@staticmethod
|
||||
def link_creates_cycle(link, selected_nodes, depth=0)->bool:
|
||||
if depth > 255:
|
||||
# We're stuck in a cycle, but that cycle was already present,
|
||||
# so we return False.
|
||||
# so we return False.
|
||||
# NOTE: The number 255 is arbitrary, but seems to work well.
|
||||
return False
|
||||
node = link.to_node
|
||||
|
@ -2451,7 +2451,7 @@ class NWMergeNodes(Operator, NWBase):
|
|||
return True
|
||||
# None of the outputs found a node in selected_nodes, so there is no cycle.
|
||||
return False
|
||||
|
||||
|
||||
# Merge the nodes in `nodes_list` with a node of type `node_name` that has a multi_input socket.
|
||||
# The parameters `socket_indices` gives the indices of the node sockets in the order that they should
|
||||
# be connected. The last one is assumed to be a multi input socket.
|
||||
|
@ -2597,7 +2597,7 @@ class NWMergeNodes(Operator, NWBase):
|
|||
# get maximum loc_x
|
||||
loc_x = nodes_list[0][1] + nodes_list[0][3] + 70
|
||||
nodes_list.sort(key=lambda k: k[2], reverse=True)
|
||||
|
||||
|
||||
# Change the node type for math nodes in a geometry node tree.
|
||||
if tree_type == 'GEOMETRY':
|
||||
if nodes_list is selected_math or nodes_list is selected_vector:
|
||||
|
@ -2709,7 +2709,7 @@ class NWMergeNodes(Operator, NWBase):
|
|||
add.location = loc_x, loc_y
|
||||
loc_y += offset_y
|
||||
add.select = True
|
||||
|
||||
|
||||
# This has already been handled separately
|
||||
if was_multi:
|
||||
continue
|
||||
|
@ -2721,7 +2721,7 @@ class NWMergeNodes(Operator, NWBase):
|
|||
last_add = nodes[count_before]
|
||||
# Create list of invalid indexes.
|
||||
invalid_nodes = [nodes[n[0]] for n in (selected_mix + selected_math + selected_shader + selected_z + selected_geometry)]
|
||||
|
||||
|
||||
# Special case:
|
||||
# Two nodes were selected and first selected has no output links, second selected has output links.
|
||||
# Then add links from last add to all links 'to_socket' of out links of second selected.
|
||||
|
@ -4385,7 +4385,7 @@ class NWConnectionListOutputs(Menu, NWBase):
|
|||
n1 = nodes[context.scene.NWLazySource]
|
||||
index=0
|
||||
for o in n1.outputs:
|
||||
# Only show sockets that are exposed.
|
||||
# Only show sockets that are exposed.
|
||||
if o.enabled:
|
||||
layout.operator(NWCallInputsMenu.bl_idname, text=o.name, icon="RADIOBUT_OFF").from_socket=index
|
||||
index+=1
|
||||
|
@ -4406,7 +4406,7 @@ class NWConnectionListInputs(Menu, NWBase):
|
|||
# Only show sockets that are exposed.
|
||||
# This prevents, for example, the scale value socket
|
||||
# of the vector math node being added to the list when
|
||||
# the mode is not 'SCALE'.
|
||||
# the mode is not 'SCALE'.
|
||||
if i.enabled:
|
||||
op = layout.operator(NWMakeLink.bl_idname, text=i.name, icon="FORWARD")
|
||||
op.from_socket = context.scene.NWSourceSocket
|
||||
|
|
|
@ -83,7 +83,7 @@ class SceneProperties(PropertyGroup):
|
|||
name="Data Layers",
|
||||
description=(
|
||||
"Export normals, UVs, vertex colors and materials for formats that support it "
|
||||
"significantly increasing filesize"
|
||||
"significantly increasing file size"
|
||||
),
|
||||
)
|
||||
export_path: StringProperty(
|
||||
|
|
|
@ -21,34 +21,11 @@ Pose Library - functions.
|
|||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, List, Set, cast, Iterable
|
||||
from typing import Any, List, Iterable
|
||||
|
||||
Datablock = Any
|
||||
|
||||
import bpy
|
||||
from bpy.types import (
|
||||
Context,
|
||||
)
|
||||
|
||||
|
||||
def asset_mark(context: Context, datablock: Any) -> Set[str]:
|
||||
asset_mark_ctx = {
|
||||
**context.copy(),
|
||||
"id": datablock,
|
||||
}
|
||||
return cast(Set[str], bpy.ops.asset.mark(asset_mark_ctx))
|
||||
|
||||
|
||||
def asset_clear(context: Context, datablock: Any) -> Set[str]:
|
||||
asset_clear_ctx = {
|
||||
**context.copy(),
|
||||
"id": datablock,
|
||||
}
|
||||
result = bpy.ops.asset.clear(asset_clear_ctx)
|
||||
assert isinstance(result, set)
|
||||
if "FINISHED" in result:
|
||||
datablock.use_fake_user = False
|
||||
return result
|
||||
|
||||
|
||||
def load_assets_from(filepath: Path) -> List[Datablock]:
|
||||
|
|
|
@ -41,7 +41,12 @@ class VIEW3D_PT_pose_library(Panel):
|
|||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
return context.preferences.experimental.use_asset_browser
|
||||
exp_prefs = context.preferences.experimental
|
||||
try:
|
||||
return exp_prefs.use_asset_browser
|
||||
except AttributeError:
|
||||
# The 'use_asset_browser' experimental option was removed from Blender.
|
||||
return True
|
||||
|
||||
def draw(self, context: Context) -> None:
|
||||
layout = self.layout
|
||||
|
@ -172,7 +177,12 @@ class DOPESHEET_PT_asset_panel(Panel):
|
|||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
return context.preferences.experimental.use_asset_browser
|
||||
exp_prefs = context.preferences.experimental
|
||||
try:
|
||||
return exp_prefs.use_asset_browser
|
||||
except AttributeError:
|
||||
# The 'use_asset_browser' experimental option was removed from Blender.
|
||||
return True
|
||||
|
||||
def draw(self, context: Context) -> None:
|
||||
layout = self.layout
|
||||
|
|
|
@ -216,20 +216,25 @@ class POSELIB_OT_copy_as_asset(PoseAssetCreator, Operator):
|
|||
|
||||
filepath = self.save_datablock(asset)
|
||||
|
||||
functions.asset_clear(context, asset)
|
||||
if asset.users > 0:
|
||||
self.report({"ERROR"}, "Whaaaat who is using our brand new asset?")
|
||||
return {"FINISHED"}
|
||||
|
||||
bpy.data.actions.remove(asset)
|
||||
|
||||
context.window_manager.clipboard = "%s%s" % (
|
||||
self.CLIPBOARD_ASSET_MARKER,
|
||||
filepath,
|
||||
)
|
||||
|
||||
asset_browser.tag_redraw(context.screen)
|
||||
self.report({"INFO"}, "Pose Asset copied, use Paste As New Asset in any Asset Browser to paste")
|
||||
|
||||
# The asset has been saved to disk, so to clean up it has to loose its asset & fake user status.
|
||||
asset.asset_clear()
|
||||
asset.use_fake_user = False
|
||||
|
||||
# The asset can be removed from the main DB, as it was purely created to
|
||||
# be stored to disk, and not to be used in this file.
|
||||
if asset.users > 0:
|
||||
# This should never happen, and indicates a bug in the code. Having a warning about it is nice,
|
||||
# but it shouldn't stand in the way of actually cleaning up the meant-to-be-temporary datablock.
|
||||
self.report({"WARNING"}, "Unexpected non-zero user count for the asset, please report this as a bug")
|
||||
|
||||
bpy.data.actions.remove(asset)
|
||||
return {"FINISHED"}
|
||||
|
||||
def save_datablock(self, action: Action) -> Path:
|
||||
|
|
|
@ -305,7 +305,7 @@ def create_pose_asset(
|
|||
) -> Optional[Action]:
|
||||
"""Create a single-frame Action containing only the pose of the given bones.
|
||||
|
||||
DOES mark as asset, DOES NOT add asset metadata.
|
||||
DOES mark as asset, DOES NOT configure asset metadata.
|
||||
"""
|
||||
|
||||
creator = PoseActionCreator(params)
|
||||
|
@ -313,7 +313,7 @@ def create_pose_asset(
|
|||
if pose_action is None:
|
||||
return None
|
||||
|
||||
functions.asset_mark(context, pose_action)
|
||||
pose_action.asset_mark()
|
||||
return pose_action
|
||||
|
||||
|
||||
|
|
76
real_snow.py
76
real_snow.py
|
@ -19,12 +19,12 @@
|
|||
bl_info = {
|
||||
"name": "Real Snow",
|
||||
"description": "Generate snow mesh",
|
||||
"author": "Wolf <wolf.art3d@gmail.com>",
|
||||
"version": (1, 1),
|
||||
"author": "Marco Pavanello, Drew Perttula",
|
||||
"version": (1, 2),
|
||||
"blender": (2, 83, 0),
|
||||
"location": "View 3D > Properties Panel",
|
||||
"doc_url": "https://github.com/macio97/Real-Snow",
|
||||
"tracker_url": "https://github.com/macio97/Real-Snow/issues",
|
||||
"doc_url": "https://github.com/marcopavanello/real-snow",
|
||||
"tracker_url": "https://github.com/marcopavanello/real-snow/issues",
|
||||
"support": "COMMUNITY",
|
||||
"category": "Object",
|
||||
}
|
||||
|
@ -86,17 +86,17 @@ class SNOW_OT_Create(Operator):
|
|||
height = context.scene.snow.height
|
||||
vertices = context.scene.snow.vertices
|
||||
|
||||
# get list of selected objects except non-mesh objects
|
||||
# Get a list of selected objects, except non-mesh objects
|
||||
input_objects = [obj for obj in context.selected_objects if obj.type == 'MESH']
|
||||
snow_list = []
|
||||
# start UI progress bar
|
||||
# Start UI progress bar
|
||||
length = len(input_objects)
|
||||
context.window_manager.progress_begin(0, 10)
|
||||
timer=0
|
||||
timer = 0
|
||||
for obj in input_objects:
|
||||
# timer
|
||||
# Timer
|
||||
context.window_manager.progress_update(timer)
|
||||
# duplicate mesh
|
||||
# Duplicate mesh
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
obj.select_set(True)
|
||||
context.view_layer.objects.active = obj
|
||||
|
@ -113,14 +113,14 @@ class SNOW_OT_Create(Operator):
|
|||
bm_copy = bm_orig.copy()
|
||||
bm_copy.transform(obj.matrix_world)
|
||||
bm_copy.normal_update()
|
||||
# get faces data
|
||||
# Get faces data
|
||||
delete_faces(vertices, bm_copy, snow_object)
|
||||
ballobj = add_metaballs(context, height, snow_object)
|
||||
context.view_layer.objects.active = snow_object
|
||||
surface_area = area(snow_object)
|
||||
snow = add_particles(context, surface_area, height, coverage, snow_object, ballobj)
|
||||
add_modifiers(snow)
|
||||
# place inside collection
|
||||
# Place inside collection
|
||||
context.view_layer.active_layer_collection = context.view_layer.layer_collection
|
||||
if "Snow" not in context.scene.collection.children:
|
||||
coll = bpy.data.collections.new("Snow")
|
||||
|
@ -130,17 +130,17 @@ class SNOW_OT_Create(Operator):
|
|||
coll.objects.link(snow)
|
||||
context.view_layer.layer_collection.collection.objects.unlink(snow)
|
||||
add_material(snow)
|
||||
# parent with object
|
||||
# Parent with object
|
||||
snow.parent = obj
|
||||
snow.matrix_parent_inverse = obj.matrix_world.inverted()
|
||||
# add snow to list
|
||||
# Add snow to list
|
||||
snow_list.append(snow)
|
||||
# update progress bar
|
||||
# Update progress bar
|
||||
timer += 0.1 / length
|
||||
# select created snow meshes
|
||||
# Select created snow meshes
|
||||
for s in snow_list:
|
||||
s.select_set(True)
|
||||
# end progress bar
|
||||
# End progress bar
|
||||
context.window_manager.progress_end()
|
||||
|
||||
return {'FINISHED'}
|
||||
|
@ -148,7 +148,7 @@ class SNOW_OT_Create(Operator):
|
|||
|
||||
def add_modifiers(snow):
|
||||
bpy.ops.object.transform_apply(location=False, scale=True, rotation=False)
|
||||
# decimate the mesh to get rid of some visual artifacts
|
||||
# Decimate the mesh to get rid of some visual artifacts
|
||||
snow.modifiers.new("Decimate", 'DECIMATE')
|
||||
snow.modifiers["Decimate"].ratio = 0.5
|
||||
snow.modifiers.new("Subdiv", "SUBSURF")
|
||||
|
@ -158,21 +158,21 @@ def add_modifiers(snow):
|
|||
|
||||
|
||||
def add_particles(context, surface_area: float, height: float, coverage: float, snow_object: bpy.types.Object, ballobj: bpy.types.Object):
|
||||
# approximate the number of particles to be emitted
|
||||
number = int(surface_area*50*(height**-2)*((coverage/100)**2))
|
||||
# Approximate the number of particles to be emitted
|
||||
number = int(surface_area * 50 * (height ** -2) * ((coverage / 100) ** 2))
|
||||
bpy.ops.object.particle_system_add()
|
||||
particles = snow_object.particle_systems[0]
|
||||
psettings = particles.settings
|
||||
psettings.type = 'HAIR'
|
||||
psettings.render_type = 'OBJECT'
|
||||
# generate random number for seed
|
||||
# Generate random number for seed
|
||||
random_seed = random.randint(0, 1000)
|
||||
particles.seed = random_seed
|
||||
# set particles object
|
||||
# Set particles object
|
||||
psettings.particle_size = height
|
||||
psettings.instance_object = ballobj
|
||||
psettings.count = number
|
||||
# convert particles to mesh
|
||||
# Convert particles to mesh
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
context.view_layer.objects.active = ballobj
|
||||
ballobj.select_set(True)
|
||||
|
@ -192,8 +192,8 @@ def add_metaballs(context, height: float, snow_object: bpy.types.Object) -> bpy.
|
|||
ball = bpy.data.metaballs.new(ball_name)
|
||||
ballobj = bpy.data.objects.new(ball_name, ball)
|
||||
bpy.context.scene.collection.objects.link(ballobj)
|
||||
# these settings have proven to work on a large amount of scenarios
|
||||
ball.resolution = 0.7*height+0.3
|
||||
# These settings have proven to work on a large amount of scenarios
|
||||
ball.resolution = 0.7 * height + 0.3
|
||||
ball.threshold = 1.3
|
||||
element = ball.elements.new()
|
||||
element.radius = 1.5
|
||||
|
@ -203,22 +203,22 @@ def add_metaballs(context, height: float, snow_object: bpy.types.Object) -> bpy.
|
|||
|
||||
|
||||
def delete_faces(vertices, bm_copy, snow_object: bpy.types.Object):
|
||||
# find upper faces
|
||||
# Find upper faces
|
||||
if vertices:
|
||||
selected_faces = [face.index for face in bm_copy.faces if face.select]
|
||||
# based on a certain angle, find all faces not pointing up
|
||||
down_faces = [face.index for face in bm_copy.faces if Vector((0, 0, -1.0)).angle(face.normal, 4.0) < (math.pi/2.0+0.5)]
|
||||
selected_faces = set(face.index for face in bm_copy.faces if face.select)
|
||||
# Based on a certain angle, find all faces not pointing up
|
||||
down_faces = set(face.index for face in bm_copy.faces if Vector((0, 0, -1.0)).angle(face.normal, 4.0) < (math.pi / 2.0 + 0.5))
|
||||
bm_copy.free()
|
||||
bpy.ops.mesh.select_all(action='DESELECT')
|
||||
# select upper faces
|
||||
# Select upper faces
|
||||
mesh = bmesh.from_edit_mesh(snow_object.data)
|
||||
for face in mesh.faces:
|
||||
if vertices:
|
||||
if not face.index in selected_faces:
|
||||
if face.index not in selected_faces:
|
||||
face.select = True
|
||||
if face.index in down_faces:
|
||||
face.select = True
|
||||
# delete unneccessary faces
|
||||
# Delete unnecessary faces
|
||||
faces_select = [face for face in mesh.faces if face.select]
|
||||
bmesh.ops.delete(mesh, geom=faces_select, context='FACES_KEEP_BOUNDARY')
|
||||
mesh.free()
|
||||
|
@ -236,16 +236,16 @@ def area(obj: bpy.types.Object) -> float:
|
|||
|
||||
def add_material(obj: bpy.types.Object):
|
||||
mat_name = "Snow"
|
||||
# if material doesn't exist, create it
|
||||
# If material doesn't exist, create it
|
||||
if mat_name in bpy.data.materials:
|
||||
bpy.data.materials[mat_name].name = mat_name+".001"
|
||||
mat = bpy.data.materials.new(mat_name)
|
||||
mat.use_nodes = True
|
||||
nodes = mat.node_tree.nodes
|
||||
# delete all nodes
|
||||
# Delete all nodes
|
||||
for node in nodes:
|
||||
nodes.remove(node)
|
||||
# add nodes
|
||||
# Add nodes
|
||||
output = nodes.new('ShaderNodeOutputMaterial')
|
||||
principled = nodes.new('ShaderNodeBsdfPrincipled')
|
||||
vec_math = nodes.new('ShaderNodeVectorMath')
|
||||
|
@ -265,7 +265,7 @@ def add_material(obj: bpy.types.Object):
|
|||
noise3 = nodes.new('ShaderNodeTexNoise')
|
||||
mapping = nodes.new('ShaderNodeMapping')
|
||||
coord = nodes.new('ShaderNodeTexCoord')
|
||||
# change location
|
||||
# Change location
|
||||
output.location = (100, 0)
|
||||
principled.location = (-200, 500)
|
||||
vec_math.location = (-400, 400)
|
||||
|
@ -285,7 +285,7 @@ def add_material(obj: bpy.types.Object):
|
|||
noise3.location = (-1500, -400)
|
||||
mapping.location = (-1700, 0)
|
||||
coord.location = (-1900, 0)
|
||||
# change node parameters
|
||||
# Change node parameters
|
||||
principled.distribution = "MULTI_GGX"
|
||||
principled.subsurface_method = "RANDOM_WALK"
|
||||
principled.inputs[0].default_value[0] = 0.904
|
||||
|
@ -332,7 +332,7 @@ def add_material(obj: bpy.types.Object):
|
|||
mapping.inputs[3].default_value[0] = 12
|
||||
mapping.inputs[3].default_value[1] = 12
|
||||
mapping.inputs[3].default_value[2] = 12
|
||||
# link nodes
|
||||
# Link nodes
|
||||
link = mat.node_tree.links
|
||||
link.new(principled.outputs[0], output.inputs[0])
|
||||
link.new(vec_math.outputs[0], principled.inputs[2])
|
||||
|
@ -355,7 +355,7 @@ def add_material(obj: bpy.types.Object):
|
|||
link.new(mapping.outputs[0], noise2.inputs[0])
|
||||
link.new(mapping.outputs[0], noise3.inputs[0])
|
||||
link.new(coord.outputs[3], mapping.inputs[0])
|
||||
# set displacement and add material
|
||||
# Set displacement and add material
|
||||
mat.cycles.displacement_method = "DISPLACEMENT"
|
||||
obj.data.materials.append(mat)
|
||||
|
||||
|
|
|
@ -0,0 +1,206 @@
|
|||
# ====================== BEGIN GPL LICENSE BLOCK ======================
|
||||
#
|
||||
# This program 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 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program 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 this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ======================= END GPL LICENSE BLOCK ========================
|
||||
|
||||
# <pep8 compliant>
|
||||
|
||||
import bpy
|
||||
import math
|
||||
|
||||
from itertools import count
|
||||
|
||||
from ...utils.naming import make_derived_name
|
||||
from ...utils.bones import flip_bone, copy_bone_position
|
||||
from ...utils.layers import ControlLayersOption
|
||||
from ...utils.misc import map_list
|
||||
|
||||
from ...base_rig import stage
|
||||
|
||||
from ..chain_rigs import TweakChainRig
|
||||
from ..widgets import create_jaw_widget
|
||||
|
||||
|
||||
class Rig(TweakChainRig):
|
||||
"""Basic tongue from the original PitchiPoy face rig."""
|
||||
|
||||
min_chain_length = 3
|
||||
|
||||
def initialize(self):
|
||||
super().initialize()
|
||||
|
||||
self.bbone_segments = self.params.bbones
|
||||
|
||||
####################################################
|
||||
# BONES
|
||||
#
|
||||
# ctrl:
|
||||
# master:
|
||||
# Master control.
|
||||
# mch:
|
||||
# follow[]:
|
||||
# Partial follow master bones.
|
||||
#
|
||||
####################################################
|
||||
|
||||
####################################################
|
||||
# Control chain
|
||||
|
||||
@stage.generate_bones
|
||||
def make_control_chain(self):
|
||||
org = self.bones.org[0]
|
||||
name = self.copy_bone(org, make_derived_name(org, 'ctrl'), parent=True)
|
||||
flip_bone(self.obj, name)
|
||||
self.bones.ctrl.master = name
|
||||
|
||||
@stage.parent_bones
|
||||
def parent_control_chain(self):
|
||||
pass
|
||||
|
||||
@stage.configure_bones
|
||||
def configure_control_chain(self):
|
||||
master = self.bones.ctrl.master
|
||||
|
||||
self.copy_bone_properties(self.bones.org[0], master)
|
||||
|
||||
ControlLayersOption.SKIN_PRIMARY.assign(self.params, self.obj, [master])
|
||||
|
||||
@stage.generate_widgets
|
||||
def make_control_widgets(self):
|
||||
create_jaw_widget(self.obj, self.bones.ctrl.master)
|
||||
|
||||
####################################################
|
||||
# Mechanism chain
|
||||
|
||||
@stage.generate_bones
|
||||
def make_follow_chain(self):
|
||||
self.bones.mch.follow = map_list(self.make_mch_follow_bone, count(1), self.bones.org[1:])
|
||||
|
||||
def make_mch_follow_bone(self, i, org):
|
||||
name = self.copy_bone(org, make_derived_name(org, 'mch'))
|
||||
copy_bone_position(self.obj, self.base_bone, name)
|
||||
flip_bone(self.obj, name)
|
||||
return name
|
||||
|
||||
@stage.parent_bones
|
||||
def parent_follow_chain(self):
|
||||
for mch in self.bones.mch.follow:
|
||||
self.set_bone_parent(mch, self.rig_parent_bone)
|
||||
|
||||
@stage.rig_bones
|
||||
def rig_follow_chain(self):
|
||||
master = self.bones.ctrl.master
|
||||
num_orgs = len(self.bones.org)
|
||||
|
||||
for i, mch in enumerate(self.bones.mch.follow):
|
||||
self.make_constraint(mch, 'COPY_TRANSFORMS', master, influence=1-(1+i)/num_orgs)
|
||||
|
||||
####################################################
|
||||
# Tweak chain
|
||||
|
||||
@stage.parent_bones
|
||||
def parent_tweak_chain(self):
|
||||
ctrl = self.bones.ctrl
|
||||
parents = [ctrl.master, *self.bones.mch.follow, self.rig_parent_bone]
|
||||
for tweak, main in zip(ctrl.tweak, parents):
|
||||
self.set_bone_parent(tweak, main)
|
||||
|
||||
####################################################
|
||||
# SETTINGS
|
||||
|
||||
@classmethod
|
||||
def add_parameters(self, params):
|
||||
params.bbones = bpy.props.IntProperty(
|
||||
name='B-Bone Segments',
|
||||
default=10,
|
||||
min=1,
|
||||
description='Number of B-Bone segments'
|
||||
)
|
||||
|
||||
ControlLayersOption.SKIN_PRIMARY.add_parameters(params)
|
||||
|
||||
@classmethod
|
||||
def parameters_ui(self, layout, params):
|
||||
layout.prop(params, 'bbones')
|
||||
|
||||
ControlLayersOption.SKIN_PRIMARY.parameters_ui(layout, params)
|
||||
|
||||
|
||||
def create_sample(obj):
|
||||
# generated by rigify.utils.write_metarig
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
arm = obj.data
|
||||
|
||||
bones = {}
|
||||
|
||||
bone = arm.edit_bones.new('tongue')
|
||||
bone.head = 0.0000, 0.0000, 0.0000
|
||||
bone.tail = 0.0000, 0.0161, 0.0074
|
||||
bone.roll = 0.0000
|
||||
bone.use_connect = False
|
||||
bones['tongue'] = bone.name
|
||||
bone = arm.edit_bones.new('tongue.001')
|
||||
bone.head = 0.0000, 0.0161, 0.0074
|
||||
bone.tail = 0.0000, 0.0375, 0.0091
|
||||
bone.roll = 0.0000
|
||||
bone.use_connect = True
|
||||
bone.parent = arm.edit_bones[bones['tongue']]
|
||||
bones['tongue.001'] = bone.name
|
||||
bone = arm.edit_bones.new('tongue.002')
|
||||
bone.head = 0.0000, 0.0375, 0.0091
|
||||
bone.tail = 0.0000, 0.0605, -0.0029
|
||||
bone.roll = 0.0000
|
||||
bone.use_connect = True
|
||||
bone.parent = arm.edit_bones[bones['tongue.001']]
|
||||
bones['tongue.002'] = bone.name
|
||||
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
pbone = obj.pose.bones[bones['tongue']]
|
||||
pbone.rigify_type = 'face.basic_tongue'
|
||||
pbone.lock_location = (False, False, False)
|
||||
pbone.lock_rotation = (False, False, False)
|
||||
pbone.lock_rotation_w = False
|
||||
pbone.lock_scale = (False, False, False)
|
||||
pbone.rotation_mode = 'QUATERNION'
|
||||
pbone = obj.pose.bones[bones['tongue.001']]
|
||||
pbone.rigify_type = ''
|
||||
pbone.lock_location = (False, False, False)
|
||||
pbone.lock_rotation = (False, False, False)
|
||||
pbone.lock_rotation_w = False
|
||||
pbone.lock_scale = (False, False, False)
|
||||
pbone.rotation_mode = 'QUATERNION'
|
||||
pbone = obj.pose.bones[bones['tongue.002']]
|
||||
pbone.rigify_type = ''
|
||||
pbone.lock_location = (False, False, False)
|
||||
pbone.lock_rotation = (False, False, False)
|
||||
pbone.lock_rotation_w = False
|
||||
pbone.lock_scale = (False, False, False)
|
||||
pbone.rotation_mode = 'QUATERNION'
|
||||
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
for bone in arm.edit_bones:
|
||||
bone.select = False
|
||||
bone.select_head = False
|
||||
bone.select_tail = False
|
||||
for b in bones:
|
||||
bone = arm.edit_bones[bones[b]]
|
||||
bone.select = True
|
||||
bone.select_head = True
|
||||
bone.select_tail = True
|
||||
bone.bbone_x = bone.bbone_z = bone.length * 0.05
|
||||
arm.edit_bones.active = bone
|
||||
|
||||
return bones
|
|
@ -0,0 +1,825 @@
|
|||
# ====================== BEGIN GPL LICENSE BLOCK ======================
|
||||
#
|
||||
# This program 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 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program 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 this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ======================= END GPL LICENSE BLOCK ========================
|
||||
|
||||
# <pep8 compliant>
|
||||
|
||||
import bpy
|
||||
import math
|
||||
import functools
|
||||
import mathutils
|
||||
|
||||
from itertools import count
|
||||
from mathutils import Vector, Matrix
|
||||
|
||||
from ...utils.naming import make_derived_name, mirror_name, change_name_side, Side, SideZ
|
||||
from ...utils.bones import align_bone_z_axis, put_bone
|
||||
from ...utils.widgets import (widget_generator, generate_circle_geometry,
|
||||
generate_circle_hull_geometry)
|
||||
from ...utils.widgets_basic import create_circle_widget
|
||||
from ...utils.switch_parent import SwitchParentBuilder
|
||||
from ...utils.misc import map_list, matrix_from_axis_pair, LazyRef
|
||||
|
||||
from ...base_rig import stage, RigComponent
|
||||
|
||||
from ..skin.skin_nodes import ControlBoneNode
|
||||
from ..skin.skin_parents import ControlBoneParentOffset
|
||||
from ..skin.skin_rigs import BaseSkinRig
|
||||
|
||||
from ..skin.basic_chain import Rig as BasicChainRig
|
||||
|
||||
|
||||
class Rig(BaseSkinRig):
|
||||
"""
|
||||
Eye rig that manages two child eyelid chains. The chains must
|
||||
connect at their ends using T/B symmetry.
|
||||
"""
|
||||
|
||||
def find_org_bones(self, bone):
|
||||
return bone.name
|
||||
|
||||
cluster_control = None
|
||||
|
||||
def initialize(self):
|
||||
super().initialize()
|
||||
|
||||
bone = self.get_bone(self.base_bone)
|
||||
self.center = bone.head
|
||||
self.axis = bone.vector
|
||||
|
||||
self.eye_corner_nodes = []
|
||||
self.eye_corner_matrix = None
|
||||
|
||||
# Create the cluster control (it will assign self.cluster_control)
|
||||
if not self.cluster_control:
|
||||
self.create_cluster_control()
|
||||
|
||||
self.init_child_chains()
|
||||
|
||||
def create_cluster_control(self):
|
||||
return EyeClusterControl(self)
|
||||
|
||||
####################################################
|
||||
# UTILITIES
|
||||
|
||||
def is_eye_control_node(self, node):
|
||||
return node.rig in self.child_chains and node.is_master_node
|
||||
|
||||
def is_eye_corner_node(self, node):
|
||||
# Corners are nodes where the two T and B chains merge
|
||||
sides = set(n.name_split.side_z for n in node.get_merged_siblings())
|
||||
return {SideZ.BOTTOM, SideZ.TOP}.issubset(sides)
|
||||
|
||||
def init_eye_corner_space(self):
|
||||
"""Initialize the coordinate space of the eye based on two corners."""
|
||||
if self.eye_corner_matrix:
|
||||
return
|
||||
|
||||
if len(self.eye_corner_nodes) != 2:
|
||||
self.raise_error('Expected 2 eye corners, but found {}', len(self.eye_corner_nodes))
|
||||
|
||||
# Build a coordinate space with XY plane based on center and two corners,
|
||||
# and Y axis oriented as close to the eye axis as possible.
|
||||
vecs = [(node.point - self.center).normalized() for node in self.eye_corner_nodes]
|
||||
normal = vecs[0].cross(vecs[1])
|
||||
space_axis = self.axis - self.axis.project(normal)
|
||||
|
||||
matrix = matrix_from_axis_pair(space_axis, normal, 'z').to_4x4()
|
||||
matrix.translation = self.center
|
||||
self.eye_corner_matrix = matrix.inverted()
|
||||
|
||||
# Compute signed angles from space_axis to the eye corners
|
||||
amin, amax = self.eye_corner_range = list(
|
||||
sorted(map(self.get_eye_corner_angle, self.eye_corner_nodes)))
|
||||
|
||||
if not (amin <= 0 <= amax):
|
||||
self.raise_error('Bad relative angles of eye corners: {}..{}',
|
||||
math.degrees(amin), math.degrees(amax))
|
||||
|
||||
def get_eye_corner_angle(self, node):
|
||||
"""Compute a signed Z rotation angle from the eye axis to the node."""
|
||||
pt = self.eye_corner_matrix @ node.point
|
||||
return math.atan2(pt.x, pt.y)
|
||||
|
||||
def get_master_control_position(self):
|
||||
"""Compute suitable position for the master control."""
|
||||
self.init_eye_corner_space()
|
||||
|
||||
# Place the control between the two corners on the eye axis
|
||||
pcorners = [node.point for node in self.eye_corner_nodes]
|
||||
|
||||
point, _ = mathutils.geometry.intersect_line_line(
|
||||
self.center, self.center + self.axis, pcorners[0], pcorners[1]
|
||||
)
|
||||
return point
|
||||
|
||||
def get_lid_follow_influence(self, node):
|
||||
"""Compute the influence factor of the eye movement on this eyelid control node."""
|
||||
self.init_eye_corner_space()
|
||||
|
||||
# Interpolate from axis to corners based on Z angle
|
||||
angle = self.get_eye_corner_angle(node)
|
||||
amin, amax = self.eye_corner_range
|
||||
|
||||
if amin < angle < 0:
|
||||
return 1 - min(1, angle/amin) ** 2
|
||||
elif 0 < angle < amax:
|
||||
return 1 - min(1, angle/amax) ** 2
|
||||
else:
|
||||
return 0
|
||||
|
||||
####################################################
|
||||
# BONES
|
||||
#
|
||||
# ctrl:
|
||||
# master:
|
||||
# Parent control for moving the whole eye.
|
||||
# target:
|
||||
# Individual target this eye aims for.
|
||||
# mch:
|
||||
# master:
|
||||
# Bone that rotates to track ctrl.target.
|
||||
# track:
|
||||
# Bone that translates to follow mch.master tail.
|
||||
# deform:
|
||||
# master:
|
||||
# Deform mirror of ctrl.master.
|
||||
# eye:
|
||||
# Deform bone that rotates with mch.master.
|
||||
# iris:
|
||||
# Iris deform bone at master tail that scales with ctrl.target
|
||||
#
|
||||
####################################################
|
||||
|
||||
####################################################
|
||||
# CHILD CHAINS
|
||||
|
||||
def init_child_chains(self):
|
||||
self.child_chains = [rig for rig in self.rigify_children if isinstance(rig, BasicChainRig)]
|
||||
|
||||
# Inject a component twisting handles to the eye radius
|
||||
for child in self.child_chains:
|
||||
self.patch_chain(child)
|
||||
|
||||
def patch_chain(self, child):
|
||||
return EyelidChainPatch(child, self)
|
||||
|
||||
####################################################
|
||||
# CONTROL NODES
|
||||
|
||||
def extend_control_node_parent(self, parent, node):
|
||||
if self.is_eye_control_node(node):
|
||||
if self.is_eye_corner_node(node):
|
||||
# Remember corners for later computations
|
||||
assert not self.eye_corner_matrix
|
||||
self.eye_corner_nodes.append(node)
|
||||
else:
|
||||
# Non-corners get extra motion applied to them
|
||||
return self.extend_mid_node_parent(parent, node)
|
||||
|
||||
return parent
|
||||
|
||||
def extend_mid_node_parent(self, parent, node):
|
||||
parent = ControlBoneParentOffset(self, node, parent)
|
||||
|
||||
# Add movement of the eye to the eyelid controls
|
||||
parent.add_copy_local_location(
|
||||
LazyRef(self.bones.mch, 'track'),
|
||||
influence=LazyRef(self.get_lid_follow_influence, node)
|
||||
)
|
||||
|
||||
# If Limit Distance on the control can be disabled, add another one to the mch
|
||||
if self.params.eyelid_detach_option:
|
||||
parent.add_limit_distance(
|
||||
self.bones.org,
|
||||
distance=(node.point - self.center).length,
|
||||
limit_mode='LIMITDIST_ONSURFACE', use_transform_limit=True,
|
||||
# Use custom space to accomodate scaling
|
||||
space='CUSTOM', space_object=self.obj, space_subtarget=self.bones.org,
|
||||
# Don't allow reordering this limit and subsequent offsets
|
||||
ensure_order=True,
|
||||
)
|
||||
|
||||
return parent
|
||||
|
||||
def extend_control_node_rig(self, node):
|
||||
if self.is_eye_control_node(node):
|
||||
# Add Limit Distance to enforce following the surface of the eye to the control
|
||||
con = self.make_constraint(
|
||||
node.control_bone, 'LIMIT_DISTANCE', self.bones.org,
|
||||
distance=(node.point - self.center).length,
|
||||
limit_mode='LIMITDIST_ONSURFACE', use_transform_limit=True,
|
||||
# Use custom space to accomodate scaling
|
||||
space='CUSTOM', space_object=self.obj, space_subtarget=self.bones.org,
|
||||
)
|
||||
|
||||
if self.params.eyelid_detach_option:
|
||||
self.make_driver(con, 'influence',
|
||||
variables=[(self.bones.ctrl.target, 'lid_attach')])
|
||||
|
||||
####################################################
|
||||
# SCRIPT
|
||||
|
||||
@stage.configure_bones
|
||||
def configure_script_panels(self):
|
||||
ctrl = self.bones.ctrl
|
||||
|
||||
controls = sum((chain.get_all_controls() for chain in self.child_chains), ctrl.flatten())
|
||||
panel = self.script.panel_with_selected_check(self, controls)
|
||||
|
||||
self.add_custom_properties()
|
||||
self.add_ui_sliders(panel)
|
||||
|
||||
def add_custom_properties(self):
|
||||
target = self.bones.ctrl.target
|
||||
|
||||
if self.params.eyelid_follow_split:
|
||||
self.make_property(
|
||||
target, 'lid_follow', list(self.params.eyelid_follow_default),
|
||||
description='Eylids follow eye movement (X and Z)'
|
||||
)
|
||||
else:
|
||||
self.make_property(target, 'lid_follow', 1.0,
|
||||
description='Eylids follow eye movement')
|
||||
|
||||
if self.params.eyelid_detach_option:
|
||||
self.make_property(target, 'lid_attach', 1.0,
|
||||
description='Eylids follow eye surface')
|
||||
|
||||
def add_ui_sliders(self, panel, *, add_name=False):
|
||||
target = self.bones.ctrl.target
|
||||
|
||||
name_tail = f' ({target})' if add_name else ''
|
||||
follow_text = f'Eyelids Follow{name_tail}'
|
||||
|
||||
if self.params.eyelid_follow_split:
|
||||
row = panel.split(factor=0.66, align=True)
|
||||
row.custom_prop(target, 'lid_follow', index=0, text=follow_text, slider=True)
|
||||
row.custom_prop(target, 'lid_follow', index=1, text='', slider=True)
|
||||
else:
|
||||
panel.custom_prop(target, 'lid_follow', text=follow_text, slider=True)
|
||||
|
||||
if self.params.eyelid_detach_option:
|
||||
panel.custom_prop(
|
||||
target, 'lid_attach', text=f'Eyelids Attached{name_tail}', slider=True)
|
||||
|
||||
####################################################
|
||||
# Master control
|
||||
|
||||
@stage.generate_bones
|
||||
def make_master_control(self):
|
||||
org = self.bones.org
|
||||
name = self.copy_bone(org, make_derived_name(org, 'ctrl', '_master'), parent=True)
|
||||
put_bone(self.obj, name, self.get_master_control_position())
|
||||
self.bones.ctrl.master = name
|
||||
|
||||
@stage.configure_bones
|
||||
def configure_master_control(self):
|
||||
self.copy_bone_properties(self.bones.org, self.bones.ctrl.master)
|
||||
|
||||
@stage.generate_widgets
|
||||
def make_master_control_widget(self):
|
||||
ctrl = self.bones.ctrl.master
|
||||
create_circle_widget(self.obj, ctrl, radius=1, head_tail=0.25)
|
||||
|
||||
####################################################
|
||||
# Tracking MCH
|
||||
|
||||
@stage.generate_bones
|
||||
def make_mch_track_bones(self):
|
||||
org = self.bones.org
|
||||
mch = self.bones.mch
|
||||
|
||||
mch.master = self.copy_bone(org, make_derived_name(org, 'mch'))
|
||||
mch.track = self.copy_bone(org, make_derived_name(org, 'mch', '_track'), scale=1/4)
|
||||
|
||||
put_bone(self.obj, mch.track, self.get_bone(org).tail)
|
||||
|
||||
@stage.parent_bones
|
||||
def parent_mch_track_bones(self):
|
||||
mch = self.bones.mch
|
||||
ctrl = self.bones.ctrl
|
||||
self.set_bone_parent(mch.master, ctrl.master)
|
||||
self.set_bone_parent(mch.track, ctrl.master)
|
||||
|
||||
@stage.rig_bones
|
||||
def rig_mch_track_bones(self):
|
||||
mch = self.bones.mch
|
||||
ctrl = self.bones.ctrl
|
||||
|
||||
# Rotationally track the target bone in mch.master
|
||||
self.make_constraint(mch.master, 'DAMPED_TRACK', ctrl.target)
|
||||
|
||||
# Translate to track the tail of mch.master in mch.track. Its local
|
||||
# location is then copied to the control nodes.
|
||||
# Two constraints are used to provide different X and Z influence values.
|
||||
con_x = self.make_constraint(
|
||||
mch.track, 'COPY_LOCATION', mch.master, head_tail=1, name='lid_follow_x',
|
||||
use_xyz=(True, False, False),
|
||||
space='CUSTOM', space_object=self.obj, space_subtarget=self.bones.org,
|
||||
)
|
||||
|
||||
con_z = self.make_constraint(
|
||||
mch.track, 'COPY_LOCATION', mch.master, head_tail=1, name='lid_follow_z',
|
||||
use_xyz=(False, False, True),
|
||||
space='CUSTOM', space_object=self.obj, space_subtarget=self.bones.org,
|
||||
)
|
||||
|
||||
# Apply follow slider influence(s)
|
||||
if self.params.eyelid_follow_split:
|
||||
self.make_driver(con_x, 'influence', variables=[(ctrl.target, 'lid_follow', 0)])
|
||||
self.make_driver(con_z, 'influence', variables=[(ctrl.target, 'lid_follow', 1)])
|
||||
else:
|
||||
factor = self.params.eyelid_follow_default
|
||||
|
||||
self.make_driver(
|
||||
con_x, 'influence', expression=f'var*{factor[0]}',
|
||||
variables=[(ctrl.target, 'lid_follow')]
|
||||
)
|
||||
self.make_driver(
|
||||
con_z, 'influence', expression=f'var*{factor[1]}',
|
||||
variables=[(ctrl.target, 'lid_follow')]
|
||||
)
|
||||
|
||||
####################################################
|
||||
# ORG bone
|
||||
|
||||
@stage.parent_bones
|
||||
def parent_org_chain(self):
|
||||
self.set_bone_parent(self.bones.org, self.bones.ctrl.master, inherit_scale='FULL')
|
||||
|
||||
####################################################
|
||||
# Deform bones
|
||||
|
||||
@stage.generate_bones
|
||||
def make_deform_bone(self):
|
||||
org = self.bones.org
|
||||
deform = self.bones.deform
|
||||
deform.master = self.copy_bone(org, make_derived_name(org, 'def', '_master'), scale=3/2)
|
||||
|
||||
if self.params.make_deform:
|
||||
deform.eye = self.copy_bone(org, make_derived_name(org, 'def'))
|
||||
deform.iris = self.copy_bone(org, make_derived_name(org, 'def', '_iris'), scale=1/2)
|
||||
put_bone(self.obj, deform.iris, self.get_bone(org).tail)
|
||||
|
||||
@stage.parent_bones
|
||||
def parent_deform_chain(self):
|
||||
deform = self.bones.deform
|
||||
self.set_bone_parent(deform.master, self.bones.org)
|
||||
|
||||
if self.params.make_deform:
|
||||
self.set_bone_parent(deform.eye, self.bones.mch.master)
|
||||
self.set_bone_parent(deform.iris, deform.eye)
|
||||
|
||||
@stage.rig_bones
|
||||
def rig_deform_chain(self):
|
||||
if self.params.make_deform:
|
||||
# Copy XZ local scale from the eye target control
|
||||
self.make_constraint(
|
||||
self.bones.deform.iris, 'COPY_SCALE', self.bones.ctrl.target,
|
||||
owner_space='LOCAL', target_space='LOCAL_OWNER_ORIENT', use_y=False,
|
||||
)
|
||||
|
||||
####################################################
|
||||
# SETTINGS
|
||||
|
||||
@classmethod
|
||||
def add_parameters(self, params):
|
||||
params.make_deform = bpy.props.BoolProperty(
|
||||
name="Deform",
|
||||
default=True,
|
||||
description="Create a deform bone for the copy"
|
||||
)
|
||||
|
||||
params.eyelid_detach_option = bpy.props.BoolProperty(
|
||||
name="Eyelid Detach Option",
|
||||
default=False,
|
||||
description="Create an option to detach eyelids from the eye surface"
|
||||
)
|
||||
|
||||
params.eyelid_follow_split = bpy.props.BoolProperty(
|
||||
name="Split Eyelid Follow Slider",
|
||||
default=False,
|
||||
description="Create separate eyelid follow influence sliders for X and Z"
|
||||
)
|
||||
|
||||
params.eyelid_follow_default = bpy.props.FloatVectorProperty(
|
||||
size=2,
|
||||
name="Eyelids Follow Default",
|
||||
default=(0.2, 0.7), min=0, max=1,
|
||||
description="Default setting for the Eyelids Follow sliders (X and Z)",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def parameters_ui(self, layout, params):
|
||||
col = layout.column()
|
||||
col.prop(params, "make_deform", text="Eyball And Iris Deforms")
|
||||
col.prop(params, "eyelid_detach_option")
|
||||
|
||||
col.prop(params, "eyelid_follow_split")
|
||||
|
||||
row = col.row(align=True)
|
||||
row.prop(params, "eyelid_follow_default", index=0, text="Follow X", slider=True)
|
||||
row.prop(params, "eyelid_follow_default", index=1, text="Follow Z", slider=True)
|
||||
|
||||
|
||||
class EyelidChainPatch(RigComponent):
|
||||
"""Component injected into child chains to twist handles aiming Z axis at the eye center."""
|
||||
|
||||
rigify_sub_object_run_late = True
|
||||
|
||||
def __init__(self, owner, eye):
|
||||
super().__init__(owner)
|
||||
|
||||
self.eye = eye
|
||||
self.owner.use_pre_handles = True
|
||||
|
||||
def align_bone(self, name):
|
||||
"""Align bone rest orientation to aim Z axis at the eye center."""
|
||||
align_bone_z_axis(self.obj, name, self.eye.center - self.get_bone(name).head)
|
||||
|
||||
def prepare_bones(self):
|
||||
for org in self.owner.bones.org:
|
||||
self.align_bone(org)
|
||||
|
||||
def generate_bones(self):
|
||||
if self.owner.use_bbones:
|
||||
mch = self.owner.bones.mch
|
||||
for pre in [*mch.handles_pre, *mch.handles]:
|
||||
self.align_bone(pre)
|
||||
|
||||
def rig_bones(self):
|
||||
if self.owner.use_bbones:
|
||||
for pre, node in zip(self.owner.bones.mch.handles_pre, self.owner.control_nodes):
|
||||
self.make_constraint(pre, 'COPY_LOCATION', node.control_bone, name='locate_cur')
|
||||
self.make_constraint(
|
||||
pre, 'LOCKED_TRACK', self.eye.bones.org, name='track_center',
|
||||
track_axis='TRACK_Z', lock_axis='LOCK_Y',
|
||||
)
|
||||
|
||||
|
||||
class EyeClusterControl(RigComponent):
|
||||
"""Component generating a common control for an eye cluster."""
|
||||
|
||||
def __init__(self, owner):
|
||||
super().__init__(owner)
|
||||
|
||||
self.find_cluster_rigs()
|
||||
|
||||
def find_cluster_rigs(self):
|
||||
"""Find and register all other eyes that belong to this cluster."""
|
||||
owner = self.owner
|
||||
|
||||
owner.cluster_control = self
|
||||
self.rig_list = [owner]
|
||||
|
||||
# Collect all sibling eye rigs
|
||||
parent_rig = owner.rigify_parent
|
||||
if parent_rig:
|
||||
for rig in parent_rig.rigify_children:
|
||||
if isinstance(rig, Rig) and rig != owner:
|
||||
rig.cluster_control = self
|
||||
self.rig_list.append(rig)
|
||||
|
||||
self.rig_count = len(self.rig_list)
|
||||
|
||||
####################################################
|
||||
# UTILITIES
|
||||
|
||||
def find_cluster_position(self):
|
||||
"""Compute the eye cluster control position and orientation."""
|
||||
|
||||
# Average location and Y axis of all the eyes
|
||||
axis = Vector((0, 0, 0))
|
||||
center = Vector((0, 0, 0))
|
||||
length = 0
|
||||
|
||||
for rig in self.rig_list:
|
||||
bone = self.get_bone(rig.base_bone)
|
||||
axis += bone.y_axis
|
||||
center += bone.head
|
||||
length += bone.length
|
||||
|
||||
axis /= self.rig_count
|
||||
center /= self.rig_count
|
||||
length /= self.rig_count
|
||||
|
||||
# Create the matrix from the average Y and world Z
|
||||
matrix = matrix_from_axis_pair((0, 0, 1), axis, 'z').to_4x4()
|
||||
matrix.translation = center + axis * length * 5
|
||||
|
||||
self.size = length * 3 / 4
|
||||
self.matrix = matrix
|
||||
self.inv_matrix = matrix.inverted()
|
||||
|
||||
def project_rig_control(self, rig):
|
||||
"""Intersect the given eye Y axis with the cluster plane, returns (x,y,0)."""
|
||||
bone = self.get_bone(rig.base_bone)
|
||||
|
||||
head = self.inv_matrix @ bone.head
|
||||
tail = self.inv_matrix @ bone.tail
|
||||
axis = tail - head
|
||||
|
||||
return head + axis * (-head.z / axis.z)
|
||||
|
||||
def get_common_rig_name(self):
|
||||
"""Choose a name for the cluster control based on the members."""
|
||||
names = set(rig.base_bone for rig in self.rig_list)
|
||||
name = min(names)
|
||||
|
||||
if mirror_name(name) in names:
|
||||
return change_name_side(name, side=Side.MIDDLE)
|
||||
|
||||
return name
|
||||
|
||||
def get_rig_control_matrix(self, rig):
|
||||
"""Compute a matrix for an individual eye sub-control."""
|
||||
matrix = self.matrix.copy()
|
||||
matrix.translation = self.matrix @ self.rig_points[rig]
|
||||
return matrix
|
||||
|
||||
def get_master_control_layers(self):
|
||||
"""Combine layers of all eyes for the cluster control."""
|
||||
all_layers = [list(self.get_bone(rig.base_bone).layers) for rig in self.rig_list]
|
||||
return [any(items) for items in zip(*all_layers)]
|
||||
|
||||
def get_all_rig_control_bones(self):
|
||||
"""Make a list of all control bones of all clustered eyes."""
|
||||
return list(set(sum((rig.bones.ctrl.flatten() for rig in self.rig_list), [self.master_bone])))
|
||||
|
||||
####################################################
|
||||
# STAGES
|
||||
|
||||
def initialize(self):
|
||||
self.find_cluster_position()
|
||||
self.rig_points = {rig: self.project_rig_control(rig) for rig in self.rig_list}
|
||||
|
||||
def generate_bones(self):
|
||||
if self.rig_count > 1:
|
||||
self.master_bone = self.make_master_control()
|
||||
self.child_bones = []
|
||||
|
||||
for rig in self.rig_list:
|
||||
rig.bones.ctrl.target = child = self.make_child_control(rig)
|
||||
self.child_bones.append(child)
|
||||
else:
|
||||
self.master_bone = self.make_child_control(self.rig_list[0])
|
||||
self.child_bones = [self.master_bone]
|
||||
self.owner.bones.ctrl.target = self.master_bone
|
||||
|
||||
self.build_parent_switch()
|
||||
|
||||
def make_master_control(self):
|
||||
name = self.new_bone(make_derived_name(self.get_common_rig_name(), 'ctrl', '_common'))
|
||||
bone = self.get_bone(name)
|
||||
bone.matrix = self.matrix
|
||||
bone.length = self.size
|
||||
bone.layers = self.get_master_control_layers()
|
||||
return name
|
||||
|
||||
def make_child_control(self, rig):
|
||||
name = rig.copy_bone(
|
||||
rig.base_bone, make_derived_name(rig.base_bone, 'ctrl'), length=self.size)
|
||||
self.get_bone(name).matrix = self.get_rig_control_matrix(rig)
|
||||
return name
|
||||
|
||||
def build_parent_switch(self):
|
||||
pbuilder = SwitchParentBuilder(self.owner.generator)
|
||||
|
||||
org_parent = self.owner.rig_parent_bone
|
||||
parents = [org_parent] if org_parent else []
|
||||
|
||||
pbuilder.build_child(
|
||||
self.owner, self.master_bone,
|
||||
prop_name=f'Parent ({self.master_bone})',
|
||||
extra_parents=parents, select_parent=org_parent,
|
||||
controls=self.get_all_rig_control_bones
|
||||
)
|
||||
|
||||
def parent_bones(self):
|
||||
if self.rig_count > 1:
|
||||
for child in self.child_bones:
|
||||
self.set_bone_parent(child, self.master_bone)
|
||||
|
||||
def configure_bones(self):
|
||||
for child in self.child_bones:
|
||||
bone = self.get_bone(child)
|
||||
bone.lock_rotation = (True, True, True)
|
||||
bone.lock_rotation_w = True
|
||||
|
||||
# When the cluster master control is selected, show sliders for all eyes
|
||||
if self.rig_count > 1:
|
||||
panel = self.owner.script.panel_with_selected_check(self.owner, [self.master_bone])
|
||||
|
||||
for rig in self.rig_list:
|
||||
rig.add_ui_sliders(panel, add_name=True)
|
||||
|
||||
def generate_widgets(self):
|
||||
for child in self.child_bones:
|
||||
create_eye_widget(self.obj, child)
|
||||
|
||||
if self.rig_count > 1:
|
||||
pt2d = [p.to_2d() / self.size for p in self.rig_points.values()]
|
||||
create_eye_cluster_widget(self.obj, self.master_bone, points=pt2d)
|
||||
|
||||
|
||||
@widget_generator
|
||||
def create_eye_widget(geom, *, size=1):
|
||||
generate_circle_geometry(geom, Vector((0, 0, 0)), size/2)
|
||||
|
||||
|
||||
@widget_generator
|
||||
def create_eye_cluster_widget(geom, *, size=1, points):
|
||||
hpoints = [points[i] for i in mathutils.geometry.convex_hull_2d(points)]
|
||||
|
||||
generate_circle_hull_geometry(geom, hpoints, size*0.75, size*0.6)
|
||||
generate_circle_hull_geometry(geom, hpoints, size, size*0.85)
|
||||
|
||||
|
||||
def create_sample(obj):
|
||||
# generated by rigify.utils.write_metarig
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
arm = obj.data
|
||||
|
||||
bones = {}
|
||||
|
||||
bone = arm.edit_bones.new('eye.L')
|
||||
bone.head = 0.0000, 0.0000, 0.0000
|
||||
bone.tail = 0.0000, -0.0125, 0.0000
|
||||
bone.roll = 0.0000
|
||||
bone.use_connect = False
|
||||
bones['eye.L'] = bone.name
|
||||
bone = arm.edit_bones.new('lid1.T.L')
|
||||
bone.head = 0.0155, -0.0006, -0.0003
|
||||
bone.tail = 0.0114, -0.0099, 0.0029
|
||||
bone.roll = 2.9453
|
||||
bone.use_connect = False
|
||||
bone.parent = arm.edit_bones[bones['eye.L']]
|
||||
bones['lid1.T.L'] = bone.name
|
||||
bone = arm.edit_bones.new('lid1.B.L')
|
||||
bone.head = 0.0155, -0.0006, -0.0003
|
||||
bone.tail = 0.0112, -0.0095, -0.0039
|
||||
bone.roll = -0.0621
|
||||
bone.use_connect = False
|
||||
bone.parent = arm.edit_bones[bones['eye.L']]
|
||||
bones['lid1.B.L'] = bone.name
|
||||
bone = arm.edit_bones.new('lid2.T.L')
|
||||
bone.head = 0.0114, -0.0099, 0.0029
|
||||
bone.tail = 0.0034, -0.0149, 0.0040
|
||||
bone.roll = 2.1070
|
||||
bone.use_connect = True
|
||||
bone.parent = arm.edit_bones[bones['lid1.T.L']]
|
||||
bones['lid2.T.L'] = bone.name
|
||||
bone = arm.edit_bones.new('lid2.B.L')
|
||||
bone.head = 0.0112, -0.0095, -0.0039
|
||||
bone.tail = 0.0029, -0.0140, -0.0057
|
||||
bone.roll = 0.8337
|
||||
bone.use_connect = True
|
||||
bone.parent = arm.edit_bones[bones['lid1.B.L']]
|
||||
bones['lid2.B.L'] = bone.name
|
||||
bone = arm.edit_bones.new('lid3.T.L')
|
||||
bone.head = 0.0034, -0.0149, 0.0040
|
||||
bone.tail = -0.0046, -0.0157, 0.0026
|
||||
bone.roll = 1.7002
|
||||
bone.use_connect = True
|
||||
bone.parent = arm.edit_bones[bones['lid2.T.L']]
|
||||
bones['lid3.T.L'] = bone.name
|
||||
bone = arm.edit_bones.new('lid3.B.L')
|
||||
bone.head = 0.0029, -0.0140, -0.0057
|
||||
bone.tail = -0.0041, -0.0145, -0.0057
|
||||
bone.roll = 1.0671
|
||||
bone.use_connect = True
|
||||
bone.parent = arm.edit_bones[bones['lid2.B.L']]
|
||||
bones['lid3.B.L'] = bone.name
|
||||
bone = arm.edit_bones.new('lid4.T.L')
|
||||
bone.head = -0.0046, -0.0157, 0.0026
|
||||
bone.tail = -0.0123, -0.0140, -0.0049
|
||||
bone.roll = 1.0850
|
||||
bone.use_connect = True
|
||||
bone.parent = arm.edit_bones[bones['lid3.T.L']]
|
||||
bones['lid4.T.L'] = bone.name
|
||||
bone = arm.edit_bones.new('lid4.B.L')
|
||||
bone.head = -0.0041, -0.0145, -0.0057
|
||||
bone.tail = -0.0123, -0.0140, -0.0049
|
||||
bone.roll = 1.1667
|
||||
bone.use_connect = True
|
||||
bone.parent = arm.edit_bones[bones['lid3.B.L']]
|
||||
bones['lid4.B.L'] = bone.name
|
||||
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
pbone = obj.pose.bones[bones['eye.L']]
|
||||
pbone.rigify_type = 'face.skin_eye'
|
||||
pbone.lock_location = (False, False, False)
|
||||
pbone.lock_rotation = (False, False, False)
|
||||
pbone.lock_rotation_w = False
|
||||
pbone.lock_scale = (False, False, False)
|
||||
pbone.rotation_mode = 'QUATERNION'
|
||||
pbone = obj.pose.bones[bones['lid1.T.L']]
|
||||
pbone.rigify_type = 'skin.stretchy_chain'
|
||||
pbone.lock_location = (False, False, False)
|
||||
pbone.lock_rotation = (False, False, False)
|
||||
pbone.lock_rotation_w = False
|
||||
pbone.lock_scale = (False, False, False)
|
||||
pbone.rotation_mode = 'QUATERNION'
|
||||
try:
|
||||
pbone.rigify_parameters.skin_chain_pivot_pos = 2
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
pbone.rigify_parameters.bbones = 5
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
pbone.rigify_parameters.skin_chain_connect_mirror = [False, False]
|
||||
except AttributeError:
|
||||
pass
|
||||
pbone = obj.pose.bones[bones['lid1.B.L']]
|
||||
pbone.rigify_type = 'skin.stretchy_chain'
|
||||
pbone.lock_location = (False, False, False)
|
||||
pbone.lock_rotation = (False, False, False)
|
||||
pbone.lock_rotation_w = False
|
||||
pbone.lock_scale = (False, False, False)
|
||||
pbone.rotation_mode = 'QUATERNION'
|
||||
try:
|
||||
pbone.rigify_parameters.skin_chain_pivot_pos = 2
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
pbone.rigify_parameters.bbones = 5
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
pbone.rigify_parameters.skin_chain_connect_mirror = [False, False]
|
||||
except AttributeError:
|
||||
pass
|
||||
pbone = obj.pose.bones[bones['lid2.T.L']]
|
||||
pbone.rigify_type = ''
|
||||
pbone.lock_location = (False, False, False)
|
||||
pbone.lock_rotation = (False, False, False)
|
||||
pbone.lock_rotation_w = False
|
||||
pbone.lock_scale = (False, False, False)
|
||||
pbone.rotation_mode = 'QUATERNION'
|
||||
pbone = obj.pose.bones[bones['lid2.B.L']]
|
||||
pbone.rigify_type = ''
|
||||
pbone.lock_location = (False, False, False)
|
||||
pbone.lock_rotation = (False, False, False)
|
||||
pbone.lock_rotation_w = False
|
||||
pbone.lock_scale = (False, False, False)
|
||||
pbone.rotation_mode = 'QUATERNION'
|
||||
pbone = obj.pose.bones[bones['lid3.T.L']]
|
||||
pbone.rigify_type = ''
|
||||
pbone.lock_location = (False, False, False)
|
||||
pbone.lock_rotation = (False, False, False)
|
||||
pbone.lock_rotation_w = False
|
||||
pbone.lock_scale = (False, False, False)
|
||||
pbone.rotation_mode = 'QUATERNION'
|
||||
pbone = obj.pose.bones[bones['lid3.B.L']]
|
||||
pbone.rigify_type = ''
|
||||
pbone.lock_location = (False, False, False)
|
||||
pbone.lock_rotation = (False, False, False)
|
||||
pbone.lock_rotation_w = False
|
||||
pbone.lock_scale = (False, False, False)
|
||||
pbone.rotation_mode = 'QUATERNION'
|
||||
pbone = obj.pose.bones[bones['lid4.T.L']]
|
||||
pbone.rigify_type = ''
|
||||
pbone.lock_location = (False, False, False)
|
||||
pbone.lock_rotation = (False, False, False)
|
||||
pbone.lock_rotation_w = False
|
||||
pbone.lock_scale = (False, False, False)
|
||||
pbone.rotation_mode = 'QUATERNION'
|
||||
pbone = obj.pose.bones[bones['lid4.B.L']]
|
||||
pbone.rigify_type = ''
|
||||
pbone.lock_location = (False, False, False)
|
||||
pbone.lock_rotation = (False, False, False)
|
||||
pbone.lock_rotation_w = False
|
||||
pbone.lock_scale = (False, False, False)
|
||||
pbone.rotation_mode = 'QUATERNION'
|
||||
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
for bone in arm.edit_bones:
|
||||
bone.select = False
|
||||
bone.select_head = False
|
||||
bone.select_tail = False
|
||||
for b in bones:
|
||||
bone = arm.edit_bones[bones[b]]
|
||||
bone.select = True
|
||||
bone.select_head = True
|
||||
bone.select_tail = True
|
||||
bone.bbone_x = bone.bbone_z = bone.length * 0.05
|
||||
arm.edit_bones.active = bone
|
||||
|
||||
return bones
|
|
@ -0,0 +1,862 @@
|
|||
# ====================== BEGIN GPL LICENSE BLOCK ======================
|
||||
#
|
||||
# This program 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 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program 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 this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ======================= END GPL LICENSE BLOCK ========================
|
||||
|
||||
# <pep8 compliant>
|
||||
|
||||
import bpy
|
||||
import math
|
||||
|
||||
from itertools import count, repeat
|
||||
from mathutils import Vector, Matrix
|
||||
from bl_math import clamp
|
||||
|
||||
from ...utils.naming import make_derived_name, Side, SideZ, get_name_side_z
|
||||
from ...utils.bones import align_bone_z_axis, put_bone
|
||||
from ...utils.misc import map_list, matrix_from_axis_pair, LazyRef
|
||||
from ...utils.widgets_basic import create_circle_widget
|
||||
|
||||
from ...base_rig import stage, RigComponent
|
||||
|
||||
from ..skin.skin_nodes import ControlBoneNode
|
||||
from ..skin.skin_parents import ControlBoneParentOrg, ControlBoneParentArmature
|
||||
from ..skin.skin_rigs import BaseSkinRig
|
||||
|
||||
from ..skin.basic_chain import Rig as BasicChainRig
|
||||
|
||||
from ..widgets import create_jaw_widget
|
||||
|
||||
|
||||
class Rig(BaseSkinRig):
|
||||
"""
|
||||
Jaw rig that manages loops of four mouth chains each. The chains
|
||||
must connect together at their ends using L/R and T/B symmetry.
|
||||
"""
|
||||
|
||||
def find_org_bones(self, bone):
|
||||
return bone.name
|
||||
|
||||
def initialize(self):
|
||||
super().initialize()
|
||||
|
||||
self.mouth_orientation = self.get_mouth_orientation()
|
||||
self.chain_to_layer = None
|
||||
|
||||
self.init_child_chains()
|
||||
|
||||
####################################################
|
||||
# UTILITIES
|
||||
|
||||
def get_mouth_orientation(self):
|
||||
jaw_axis = self.get_bone(self.base_bone).y_axis.copy()
|
||||
jaw_axis[2] = 0
|
||||
|
||||
return matrix_from_axis_pair(jaw_axis, (0, 0, 1), 'z').to_quaternion()
|
||||
|
||||
def is_corner_node(self, node):
|
||||
# Corners are nodes where two T/B or L/R chains meet.
|
||||
siblings = [n for n in node.get_merged_siblings() if n.rig in self.child_chains]
|
||||
|
||||
sides_x = set(n.name_split.side for n in siblings)
|
||||
sides_z = set(n.name_split.side_z for n in siblings)
|
||||
|
||||
if {SideZ.BOTTOM, SideZ.TOP}.issubset(sides_z):
|
||||
if Side.LEFT in sides_x:
|
||||
return Side.LEFT
|
||||
else:
|
||||
return Side.RIGHT
|
||||
|
||||
if {Side.LEFT, Side.RIGHT}.issubset(sides_x):
|
||||
if SideZ.TOP in sides_z:
|
||||
return SideZ.TOP
|
||||
else:
|
||||
return SideZ.BOTTOM
|
||||
|
||||
return None
|
||||
|
||||
####################################################
|
||||
# BONES
|
||||
#
|
||||
# ctrl:
|
||||
# master:
|
||||
# Main jaw open control.
|
||||
# mouth:
|
||||
# Main control for adjusting mouth position and scale.
|
||||
# mch:
|
||||
# lock:
|
||||
# Jaw master mirror for the locked mouth.
|
||||
# top[]:
|
||||
# Jaw master mirrors for the loop top.
|
||||
# bottom[]:
|
||||
# Jaw master mirrors for the loop bottom.
|
||||
# middle[]:
|
||||
# Middle position between top[] and bottom[].
|
||||
# mouth_parent = middle[0]:
|
||||
# Parent for ctrl.mouth, mouth_layers and *_in
|
||||
# mouth_layers[]:
|
||||
# Apply fade out of ctrl.mouth motion for outer loops.
|
||||
# top_out[], bottom_out[], middle_out[]:
|
||||
# Combine mouth and jaw motions via Copy Custom to Local.
|
||||
# deform:
|
||||
# master:
|
||||
# Deform mirror of ctrl.master.
|
||||
#
|
||||
####################################################
|
||||
|
||||
####################################################
|
||||
# CHILD CHAINS
|
||||
|
||||
def init_child_chains(self):
|
||||
self.child_chains = [
|
||||
rig
|
||||
for rig in self.rigify_children
|
||||
if isinstance(rig, BasicChainRig) and get_name_side_z(rig.base_bone) != SideZ.MIDDLE
|
||||
]
|
||||
|
||||
self.corners = {Side.LEFT: [], Side.RIGHT: [], SideZ.TOP: [], SideZ.BOTTOM: []}
|
||||
|
||||
def arrange_child_chains(self):
|
||||
"""Sort child chains into their corresponding mouth loops."""
|
||||
if self.chain_to_layer is not None:
|
||||
return
|
||||
|
||||
# Index child node corners
|
||||
for child in self.child_chains:
|
||||
for node in child.control_nodes:
|
||||
corner = self.is_corner_node(node)
|
||||
if corner:
|
||||
if node.merged_master not in self.corners[corner]:
|
||||
self.corners[corner].append(node.merged_master)
|
||||
|
||||
self.num_layers = len(self.corners[SideZ.TOP])
|
||||
|
||||
for k, v in self.corners.items():
|
||||
if len(v) == 0:
|
||||
self.raise_error("Could not find all mouth corners")
|
||||
if len(v) != self.num_layers:
|
||||
self.raise_error(
|
||||
"Mouth corner counts differ: {} vs {}",
|
||||
[n.name for n in v], [n.name for n in self.corners[SideZ.TOP]]
|
||||
)
|
||||
|
||||
# Find inner top/bottom corners
|
||||
anchor = self.corners[SideZ.BOTTOM][0].point
|
||||
inner_top = min(self.corners[SideZ.TOP], key=lambda p: (p.point - anchor).length)
|
||||
|
||||
anchor = inner_top.point
|
||||
inner_bottom = min(self.corners[SideZ.BOTTOM], key=lambda p: (p.point - anchor).length)
|
||||
|
||||
# Compute the mouth space
|
||||
self.mouth_center = center = (inner_top.point + inner_bottom.point) / 2
|
||||
|
||||
matrix = self.mouth_orientation.to_matrix().to_4x4()
|
||||
matrix.translation = center
|
||||
self.mouth_space = matrix
|
||||
self.to_mouth_space = matrix.inverted()
|
||||
|
||||
# Build a mapping of child chain to layer (i.e. sort multiple mouth loops)
|
||||
self.chain_to_layer = {}
|
||||
self.chains_by_side = {}
|
||||
|
||||
for k, v in list(self.corners.items()):
|
||||
self.corners[k] = ordered = sorted(v, key=lambda p: (p.point - center).length)
|
||||
|
||||
chain_set = set()
|
||||
|
||||
for i, node in enumerate(ordered):
|
||||
for sibling in node.get_merged_siblings():
|
||||
if sibling.rig in self.child_chains:
|
||||
cur_layer = self.chain_to_layer.get(sibling.rig)
|
||||
|
||||
if cur_layer is not None and cur_layer != i:
|
||||
self.raise_error(
|
||||
"Conflicting mouth chain layer on {}: {} and {}", sibling.rig.base_bone, i, cur_layer)
|
||||
|
||||
self.chain_to_layer[sibling.rig] = i
|
||||
chain_set.add(sibling.rig)
|
||||
|
||||
self.chains_by_side[k] = chain_set
|
||||
|
||||
for child in self.child_chains:
|
||||
if child not in self.chain_to_layer:
|
||||
self.raise_error("Could not determine chain layer on {}", child.base_bone)
|
||||
|
||||
if not self.chains_by_side[Side.LEFT].isdisjoint(self.chains_by_side[Side.RIGHT]):
|
||||
self.raise_error("Left/right conflict in mouth")
|
||||
if not self.chains_by_side[SideZ.TOP].isdisjoint(self.chains_by_side[SideZ.BOTTOM]):
|
||||
self.raise_error("Top/bottom conflict in mouth")
|
||||
|
||||
# Find left/right direction
|
||||
pt = self.to_mouth_space @ self.corners[Side.LEFT][0].point
|
||||
|
||||
self.left_sign = 1 if pt.x > 0 else -1
|
||||
|
||||
for node in self.corners[Side.LEFT]:
|
||||
if (self.to_mouth_space @ node.point).x * self.left_sign <= 0:
|
||||
self.raise_error("Bad left corner location: {}", node.name)
|
||||
|
||||
for node in self.corners[Side.RIGHT]:
|
||||
if (self.to_mouth_space @ node.point).x * self.left_sign >= 0:
|
||||
self.raise_error("Bad right corner location: {}", node.name)
|
||||
|
||||
# Find layer loop widths
|
||||
self.layer_width = [
|
||||
(self.corners[Side.LEFT][i].point - self.corners[Side.RIGHT][i].point).length
|
||||
for i in range(self.num_layers)
|
||||
]
|
||||
|
||||
def position_mouth_bone(self, name, scale):
|
||||
self.arrange_child_chains()
|
||||
|
||||
bone = self.get_bone(name)
|
||||
bone.matrix = self.mouth_space
|
||||
bone.length = self.layer_width[0] * scale
|
||||
|
||||
####################################################
|
||||
# CONTROL NODES
|
||||
|
||||
def get_node_parent_bones(self, node):
|
||||
"""Get parent bones and their armature weights for the given control node."""
|
||||
self.arrange_child_chains()
|
||||
|
||||
# Choose correct layer bones
|
||||
layer = self.chain_to_layer[node.rig]
|
||||
|
||||
top_mch = LazyRef(self.bones.mch, 'top_out', layer)
|
||||
bottom_mch = LazyRef(self.bones.mch, 'bottom_out', layer)
|
||||
middle_mch = LazyRef(self.bones.mch, 'middle_out', layer)
|
||||
|
||||
# Corners have one input
|
||||
corner = self.is_corner_node(node)
|
||||
if corner:
|
||||
if corner == SideZ.TOP:
|
||||
return [top_mch]
|
||||
elif corner == SideZ.BOTTOM:
|
||||
return [bottom_mch]
|
||||
else:
|
||||
return [middle_mch]
|
||||
|
||||
# Otherwise blend two
|
||||
if node.rig in self.chains_by_side[SideZ.TOP]:
|
||||
side_mch = top_mch
|
||||
else:
|
||||
side_mch = bottom_mch
|
||||
|
||||
pt_x = (self.to_mouth_space @ node.point).x
|
||||
side = Side.LEFT if pt_x * self.left_sign >= 0 else Side.RIGHT
|
||||
|
||||
corner_x = (self.to_mouth_space @ self.corners[side][layer].point).x
|
||||
factor = math.sqrt(1 - clamp(pt_x / corner_x) ** 2)
|
||||
|
||||
return [(side_mch, factor), (middle_mch, 1-factor)]
|
||||
|
||||
def get_parent_for_name(self, name, parent_bone):
|
||||
"""Get single replacement parent for the given child bone."""
|
||||
if parent_bone == self.base_bone:
|
||||
side = get_name_side_z(name)
|
||||
if side == SideZ.TOP:
|
||||
return LazyRef(self.bones.mch, 'top', -1)
|
||||
if side == SideZ.BOTTOM:
|
||||
return LazyRef(self.bones.mch, 'bottom', -1)
|
||||
|
||||
return parent_bone
|
||||
|
||||
def get_child_chain_parent(self, rig, parent_bone):
|
||||
return self.get_parent_for_name(rig.base_bone, parent_bone)
|
||||
|
||||
def build_control_node_parent(self, node, parent_bone):
|
||||
if node.rig in self.child_chains:
|
||||
return ControlBoneParentArmature(
|
||||
self, node,
|
||||
bones=self.get_node_parent_bones(node),
|
||||
orientation=self.mouth_orientation,
|
||||
copy_scale=LazyRef(self.bones.mch, 'mouth_parent'),
|
||||
)
|
||||
|
||||
return ControlBoneParentOrg(self.get_parent_for_name(node.name, parent_bone))
|
||||
|
||||
####################################################
|
||||
# Master control
|
||||
|
||||
@stage.generate_bones
|
||||
def make_master_control(self):
|
||||
org = self.bones.org
|
||||
name = self.copy_bone(org, make_derived_name(org, 'ctrl'), parent=True)
|
||||
self.bones.ctrl.master = name
|
||||
|
||||
@stage.configure_bones
|
||||
def configure_master_control(self):
|
||||
self.copy_bone_properties(self.bones.org, self.bones.ctrl.master)
|
||||
|
||||
self.get_bone(self.bones.ctrl.master).lock_scale = (True, True, True)
|
||||
|
||||
@stage.generate_widgets
|
||||
def make_master_control_widget(self):
|
||||
ctrl = self.bones.ctrl.master
|
||||
create_jaw_widget(self.obj, ctrl)
|
||||
|
||||
####################################################
|
||||
# Mouth control
|
||||
|
||||
@stage.generate_bones
|
||||
def make_mouth_control(self):
|
||||
org = self.bones.org
|
||||
name = self.copy_bone(org, make_derived_name(org, 'ctrl', '_mouth'))
|
||||
self.position_mouth_bone(name, 1)
|
||||
self.bones.ctrl.mouth = name
|
||||
|
||||
@stage.parent_bones
|
||||
def parent_mouth_control(self):
|
||||
self.set_bone_parent(self.bones.ctrl.mouth, self.bones.mch.mouth_parent)
|
||||
|
||||
@stage.configure_bones
|
||||
def configure_mouth_control(self):
|
||||
pass
|
||||
|
||||
@stage.generate_widgets
|
||||
def make_mouth_control_widget(self):
|
||||
ctrl = self.bones.ctrl.mouth
|
||||
|
||||
width = (self.corners[Side.LEFT][0].point - self.corners[Side.RIGHT][0].point).length
|
||||
height = (self.corners[SideZ.TOP][0].point - self.corners[SideZ.BOTTOM][0].point).length
|
||||
back = (self.corners[Side.LEFT][0].point + self.corners[Side.RIGHT][0].point) / 2
|
||||
front = (self.corners[SideZ.TOP][0].point + self.corners[SideZ.BOTTOM][0].point) / 2
|
||||
depth = (front - back).length
|
||||
|
||||
create_circle_widget(
|
||||
self.obj, ctrl,
|
||||
radius=0.2 + 0.5 * (height / width), radius_x=0.7,
|
||||
head_tail=0.2, head_tail_x=0.2 - (depth / width)
|
||||
)
|
||||
|
||||
####################################################
|
||||
# Jaw Motion MCH
|
||||
|
||||
@stage.generate_bones
|
||||
def make_mch_lock_bones(self):
|
||||
org = self.bones.org
|
||||
mch = self.bones.mch
|
||||
|
||||
self.arrange_child_chains()
|
||||
|
||||
mch.lock = self.copy_bone(
|
||||
org, make_derived_name(org, 'mch', '_lock'), scale=1/2, parent=True)
|
||||
|
||||
mch.top = map_list(self.make_mch_top_bone, range(self.num_layers), repeat(org))
|
||||
mch.bottom = map_list(self.make_mch_bottom_bone, range(self.num_layers), repeat(org))
|
||||
mch.middle = map_list(self.make_mch_middle_bone, range(self.num_layers), repeat(org))
|
||||
|
||||
mch.mouth_parent = mch.middle[0]
|
||||
|
||||
def make_mch_top_bone(self, i, org):
|
||||
return self.copy_bone(org, make_derived_name(org, 'mch', '_top'), scale=1/4, parent=True)
|
||||
|
||||
def make_mch_bottom_bone(self, i, org):
|
||||
return self.copy_bone(org, make_derived_name(org, 'mch', '_bottom'), scale=1/3, parent=True)
|
||||
|
||||
def make_mch_middle_bone(self, i, org):
|
||||
return self.copy_bone(org, make_derived_name(org, 'mch', '_middle'), scale=2/3, parent=True)
|
||||
|
||||
@stage.parent_bones
|
||||
def parent_mch_lock_bones(self):
|
||||
mch = self.bones.mch
|
||||
ctrl = self.bones.ctrl
|
||||
|
||||
for mid, top in zip(mch.middle, mch.top):
|
||||
self.set_bone_parent(mid, top)
|
||||
|
||||
for bottom in mch.bottom[1:]:
|
||||
self.set_bone_parent(bottom, ctrl.master)
|
||||
|
||||
@stage.configure_bones
|
||||
def configure_mch_lock_bones(self):
|
||||
ctrl = self.bones.ctrl
|
||||
|
||||
panel = self.script.panel_with_selected_check(self, [ctrl.master, ctrl.mouth])
|
||||
|
||||
self.make_property(ctrl.master, 'mouth_lock', 0.0, description='Mouth is locked closed')
|
||||
panel.custom_prop(ctrl.master, 'mouth_lock', text='Mouth Lock', slider=True)
|
||||
|
||||
@stage.rig_bones
|
||||
def rig_mch_track_bones(self):
|
||||
mch = self.bones.mch
|
||||
ctrl = self.bones.ctrl
|
||||
|
||||
# Lock position follows jaw master with configured influence
|
||||
self.make_constraint(
|
||||
mch.lock, 'COPY_TRANSFORMS', ctrl.master,
|
||||
influence=self.params.jaw_locked_influence,
|
||||
)
|
||||
|
||||
# Innermost top bone follows lock position according to slider
|
||||
con = self.make_constraint(mch.top[0], 'COPY_TRANSFORMS', mch.lock)
|
||||
self.make_driver(con, 'influence', variables=[(ctrl.master, 'mouth_lock')])
|
||||
|
||||
# Innermost bottom bone follows jaw master with configured influence, and then lock
|
||||
self.make_constraint(
|
||||
mch.bottom[0], 'COPY_TRANSFORMS', ctrl.master,
|
||||
influence=self.params.jaw_mouth_influence,
|
||||
)
|
||||
|
||||
con = self.make_constraint(mch.bottom[0], 'COPY_TRANSFORMS', mch.lock)
|
||||
self.make_driver(con, 'influence', variables=[(ctrl.master, 'mouth_lock')])
|
||||
|
||||
# Outer layer bones interpolate toward innermost based on influence decay
|
||||
coeff = self.params.jaw_secondary_influence
|
||||
|
||||
for i, name in enumerate(mch.top[1:]):
|
||||
self.make_constraint(name, 'COPY_TRANSFORMS', mch.top[0], influence=coeff ** (1+i))
|
||||
|
||||
for i, name in enumerate(mch.bottom[1:]):
|
||||
self.make_constraint(name, 'COPY_TRANSFORMS', mch.bottom[0], influence=coeff ** (1+i))
|
||||
|
||||
# Middle bones interpolate the middle between top and bottom
|
||||
for mid, bottom in zip(mch.middle, mch.bottom):
|
||||
self.make_constraint(mid, 'COPY_TRANSFORMS', bottom, influence=0.5)
|
||||
|
||||
####################################################
|
||||
# Mouth MCH
|
||||
|
||||
@stage.generate_bones
|
||||
def make_mch_mouth_bones(self):
|
||||
mch = self.bones.mch
|
||||
|
||||
mch.mouth_layers = map_list(self.make_mch_mouth_bone,
|
||||
range(1, self.num_layers), repeat('_mouth_layer'), repeat(0.6))
|
||||
|
||||
mch.top_out = map_list(self.make_mch_mouth_inout_bone,
|
||||
range(self.num_layers), repeat('_top_out'), repeat(0.4))
|
||||
mch.bottom_out = map_list(self.make_mch_mouth_inout_bone,
|
||||
range(self.num_layers), repeat('_bottom_out'), repeat(0.35))
|
||||
mch.middle_out = map_list(self.make_mch_mouth_inout_bone,
|
||||
range(self.num_layers), repeat('_middle_out'), repeat(0.3))
|
||||
|
||||
def make_mch_mouth_bone(self, i, suffix, size):
|
||||
name = self.copy_bone(self.bones.org, make_derived_name(self.bones.org, 'mch', suffix))
|
||||
self.position_mouth_bone(name, size)
|
||||
return name
|
||||
|
||||
def make_mch_mouth_inout_bone(self, i, suffix, size):
|
||||
return self.copy_bone(self.bones.org, make_derived_name(self.bones.org, 'mch', suffix), scale=size)
|
||||
|
||||
@stage.parent_bones
|
||||
def parent_mch_mouth_bones(self):
|
||||
mch = self.bones.mch
|
||||
layers = [self.bones.ctrl.mouth, *mch.mouth_layers]
|
||||
|
||||
for name in mch.mouth_layers:
|
||||
self.set_bone_parent(name, mch.mouth_parent)
|
||||
|
||||
for name_list in [mch.top_out, mch.bottom_out, mch.middle_out]:
|
||||
for name, parent in zip(name_list, layers):
|
||||
self.set_bone_parent(name, parent)
|
||||
|
||||
@stage.rig_bones
|
||||
def rig_mch_mouth_bones(self):
|
||||
mch = self.bones.mch
|
||||
ctrl = self.bones.ctrl.mouth
|
||||
|
||||
# Mouth influence fade out
|
||||
for i, name in enumerate(mch.mouth_layers):
|
||||
self.rig_mch_mouth_layer_bone(i+1, name, ctrl)
|
||||
|
||||
# Transfer and combine jaw motion with mouth
|
||||
all_jaw = mch.top + mch.bottom + mch.middle
|
||||
all_out = mch.top_out + mch.bottom_out + mch.middle_out
|
||||
|
||||
for dest, src in zip(all_out, all_jaw):
|
||||
self.make_constraint(
|
||||
dest, 'COPY_TRANSFORMS', src,
|
||||
owner_space='LOCAL', target_space='CUSTOM',
|
||||
space_object=self.obj, space_subtarget=mch.mouth_parent,
|
||||
)
|
||||
|
||||
def rig_mch_mouth_layer_bone(self, i, mch, ctrl):
|
||||
# Fade location and rotation based on influence decay
|
||||
inf = self.params.jaw_secondary_influence ** i
|
||||
|
||||
self.make_constraint(mch, 'COPY_LOCATION', ctrl, influence=inf)
|
||||
self.make_constraint(mch, 'COPY_ROTATION', ctrl, influence=inf)
|
||||
|
||||
# For scale, additionally take radius into account
|
||||
inf_scale = inf * self.layer_width[0] / self.layer_width[i]
|
||||
|
||||
self.make_constraint(mch, 'COPY_SCALE', ctrl, influence=inf_scale)
|
||||
|
||||
####################################################
|
||||
# ORG bone
|
||||
|
||||
@stage.parent_bones
|
||||
def parent_org_chain(self):
|
||||
self.set_bone_parent(self.bones.org, self.bones.ctrl.master, inherit_scale='FULL')
|
||||
|
||||
####################################################
|
||||
# Deform bones
|
||||
|
||||
@stage.generate_bones
|
||||
def make_deform_bone(self):
|
||||
org = self.bones.org
|
||||
deform = self.bones.deform
|
||||
self.bones.deform.master = self.copy_bone(org, make_derived_name(org, 'def'))
|
||||
|
||||
@stage.parent_bones
|
||||
def parent_deform_chain(self):
|
||||
deform = self.bones.deform
|
||||
self.set_bone_parent(deform.master, self.bones.org)
|
||||
|
||||
####################################################
|
||||
# SETTINGS
|
||||
|
||||
@classmethod
|
||||
def add_parameters(self, params):
|
||||
params.jaw_mouth_influence = bpy.props.FloatProperty(
|
||||
name="Bottom Lip Influence",
|
||||
default=0.5, min=0, max=1,
|
||||
description="Influence of the jaw on the bottom lip chains"
|
||||
)
|
||||
|
||||
params.jaw_locked_influence = bpy.props.FloatProperty(
|
||||
name="Locked Influence",
|
||||
default=0.2, min=0, max=1,
|
||||
description="Influence of the jaw on the locked mouth"
|
||||
)
|
||||
|
||||
params.jaw_secondary_influence = bpy.props.FloatProperty(
|
||||
name="Secondary Influence Falloff",
|
||||
default=0.5, min=0, max=1,
|
||||
description="Reduction factor for each level of secondary mouth loops"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def parameters_ui(self, layout, params):
|
||||
layout.prop(params, "jaw_mouth_influence", slider=True)
|
||||
layout.prop(params, "jaw_locked_influence", slider=True)
|
||||
layout.prop(params, "jaw_secondary_influence", slider=True)
|
||||
|
||||
|
||||
def create_sample(obj):
|
||||
# generated by rigify.utils.write_metarig
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
arm = obj.data
|
||||
|
||||
bones = {}
|
||||
|
||||
bone = arm.edit_bones.new('jaw')
|
||||
bone.head = 0.0000, 0.0000, 0.0000
|
||||
bone.tail = 0.0000, -0.0585, -0.0489
|
||||
bone.roll = 0.0000
|
||||
bone.use_connect = False
|
||||
bones['jaw'] = bone.name
|
||||
bone = arm.edit_bones.new('teeth.T')
|
||||
bone.head = 0.0000, -0.0589, 0.0080
|
||||
bone.tail = 0.0000, -0.0283, 0.0080
|
||||
bone.roll = 0.0000
|
||||
bone.use_connect = False
|
||||
bones['teeth.T'] = bone.name
|
||||
bone = arm.edit_bones.new('lip.T.L')
|
||||
bone.head = -0.0000, -0.0684, 0.0030
|
||||
bone.tail = 0.0105, -0.0655, 0.0033
|
||||
bone.roll = -0.0000
|
||||
bone.use_connect = False
|
||||
bone.parent = arm.edit_bones[bones['jaw']]
|
||||
bones['lip.T.L'] = bone.name
|
||||
bone = arm.edit_bones.new('lip.B.L')
|
||||
bone.head = -0.0000, -0.0655, -0.0078
|
||||
bone.tail = 0.0107, -0.0625, -0.0053
|
||||
bone.roll = -0.0551
|
||||
bone.use_connect = False
|
||||
bone.parent = arm.edit_bones[bones['jaw']]
|
||||
bones['lip.B.L'] = bone.name
|
||||
bone = arm.edit_bones.new('lip.T.R')
|
||||
bone.head = 0.0000, -0.0684, 0.0030
|
||||
bone.tail = -0.0105, -0.0655, 0.0033
|
||||
bone.roll = 0.0000
|
||||
bone.use_connect = False
|
||||
bone.parent = arm.edit_bones[bones['jaw']]
|
||||
bones['lip.T.R'] = bone.name
|
||||
bone = arm.edit_bones.new('lip.B.R')
|
||||
bone.head = 0.0000, -0.0655, -0.0078
|
||||
bone.tail = -0.0107, -0.0625, -0.0053
|
||||
bone.roll = 0.0551
|
||||
bone.use_connect = False
|
||||
bone.parent = arm.edit_bones[bones['jaw']]
|
||||
bones['lip.B.R'] = bone.name
|
||||
bone = arm.edit_bones.new('teeth.B')
|
||||
bone.head = 0.0000, -0.0543, -0.0136
|
||||
bone.tail = 0.0000, -0.0237, -0.0136
|
||||
bone.roll = 0.0000
|
||||
bone.use_connect = False
|
||||
bone.parent = arm.edit_bones[bones['jaw']]
|
||||
bones['teeth.B'] = bone.name
|
||||
bone = arm.edit_bones.new('lip1.T.L')
|
||||
bone.head = 0.0105, -0.0655, 0.0033
|
||||
bone.tail = 0.0193, -0.0586, 0.0007
|
||||
bone.roll = -0.0257
|
||||
bone.use_connect = True
|
||||
bone.parent = arm.edit_bones[bones['lip.T.L']]
|
||||
bones['lip1.T.L'] = bone.name
|
||||
bone = arm.edit_bones.new('lip1.B.L')
|
||||
bone.head = 0.0107, -0.0625, -0.0053
|
||||
bone.tail = 0.0194, -0.0573, -0.0029
|
||||
bone.roll = 0.0716
|
||||
bone.use_connect = True
|
||||
bone.parent = arm.edit_bones[bones['lip.B.L']]
|
||||
bones['lip1.B.L'] = bone.name
|
||||
bone = arm.edit_bones.new('lip1.T.R')
|
||||
bone.head = -0.0105, -0.0655, 0.0033
|
||||
bone.tail = -0.0193, -0.0586, 0.0007
|
||||
bone.roll = 0.0257
|
||||
bone.use_connect = True
|
||||
bone.parent = arm.edit_bones[bones['lip.T.R']]
|
||||
bones['lip1.T.R'] = bone.name
|
||||
bone = arm.edit_bones.new('lip1.B.R')
|
||||
bone.head = -0.0107, -0.0625, -0.0053
|
||||
bone.tail = -0.0194, -0.0573, -0.0029
|
||||
bone.roll = -0.0716
|
||||
bone.use_connect = True
|
||||
bone.parent = arm.edit_bones[bones['lip.B.R']]
|
||||
bones['lip1.B.R'] = bone.name
|
||||
bone = arm.edit_bones.new('lip2.T.L')
|
||||
bone.head = 0.0193, -0.0586, 0.0007
|
||||
bone.tail = 0.0236, -0.0539, -0.0014
|
||||
bone.roll = 0.0324
|
||||
bone.use_connect = True
|
||||
bone.parent = arm.edit_bones[bones['lip1.T.L']]
|
||||
bones['lip2.T.L'] = bone.name
|
||||
bone = arm.edit_bones.new('lip2.B.L')
|
||||
bone.head = 0.0194, -0.0573, -0.0029
|
||||
bone.tail = 0.0236, -0.0539, -0.0014
|
||||
bone.roll = 0.0467
|
||||
bone.use_connect = True
|
||||
bone.parent = arm.edit_bones[bones['lip1.B.L']]
|
||||
bones['lip2.B.L'] = bone.name
|
||||
bone = arm.edit_bones.new('lip2.T.R')
|
||||
bone.head = -0.0193, -0.0586, 0.0007
|
||||
bone.tail = -0.0236, -0.0539, -0.0014
|
||||
bone.roll = -0.0324
|
||||
bone.use_connect = True
|
||||
bone.parent = arm.edit_bones[bones['lip1.T.R']]
|
||||
bones['lip2.T.R'] = bone.name
|
||||
bone = arm.edit_bones.new('lip2.B.R')
|
||||
bone.head = -0.0194, -0.0573, -0.0029
|
||||
bone.tail = -0.0236, -0.0539, -0.0014
|
||||
bone.roll = -0.0467
|
||||
bone.use_connect = True
|
||||
bone.parent = arm.edit_bones[bones['lip1.B.R']]
|
||||
bones['lip2.B.R'] = bone.name
|
||||
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
pbone = obj.pose.bones[bones['jaw']]
|
||||
pbone.rigify_type = 'face.skin_jaw'
|
||||
pbone.lock_location = (False, False, False)
|
||||
pbone.lock_rotation = (False, False, False)
|
||||
pbone.lock_rotation_w = False
|
||||
pbone.lock_scale = (False, False, False)
|
||||
pbone.rotation_mode = 'QUATERNION'
|
||||
pbone = obj.pose.bones[bones['teeth.T']]
|
||||
pbone.rigify_type = 'basic.super_copy'
|
||||
pbone.lock_location = (False, False, False)
|
||||
pbone.lock_rotation = (False, False, False)
|
||||
pbone.lock_rotation_w = False
|
||||
pbone.lock_scale = (False, False, False)
|
||||
pbone.rotation_mode = 'QUATERNION'
|
||||
try:
|
||||
pbone.rigify_parameters.make_deform = False
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
pbone.rigify_parameters.super_copy_widget_type = "teeth"
|
||||
except AttributeError:
|
||||
pass
|
||||
pbone = obj.pose.bones[bones['lip.T.L']]
|
||||
pbone.rigify_type = 'skin.stretchy_chain'
|
||||
pbone.lock_location = (False, False, False)
|
||||
pbone.lock_rotation = (False, False, False)
|
||||
pbone.lock_rotation_w = False
|
||||
pbone.lock_scale = (False, False, False)
|
||||
pbone.rotation_mode = 'QUATERNION'
|
||||
try:
|
||||
pbone.rigify_parameters.bbones = 3
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
pbone.rigify_parameters.skin_chain_falloff_spherical = [True, False, True]
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
pbone.rigify_parameters.skin_chain_falloff = [0.5, 1.0, -0.5]
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
pbone.rigify_parameters.skin_chain_connect_mirror = [True, False]
|
||||
except AttributeError:
|
||||
pass
|
||||
pbone = obj.pose.bones[bones['lip.B.L']]
|
||||
pbone.rigify_type = 'skin.stretchy_chain'
|
||||
pbone.lock_location = (False, False, False)
|
||||
pbone.lock_rotation = (False, False, False)
|
||||
pbone.lock_rotation_w = False
|
||||
pbone.lock_scale = (False, False, False)
|
||||
pbone.rotation_mode = 'QUATERNION'
|
||||
try:
|
||||
pbone.rigify_parameters.bbones = 3
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
pbone.rigify_parameters.skin_chain_falloff_spherical = [True, False, True]
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
pbone.rigify_parameters.skin_chain_falloff = [0.5, 1.0, -0.5]
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
pbone.rigify_parameters.skin_chain_connect_mirror = [True, False]
|
||||
except AttributeError:
|
||||
pass
|
||||
pbone = obj.pose.bones[bones['lip.T.R']]
|
||||
pbone.rigify_type = 'skin.stretchy_chain'
|
||||
pbone.lock_location = (False, False, False)
|
||||
pbone.lock_rotation = (False, False, False)
|
||||
pbone.lock_rotation_w = False
|
||||
pbone.lock_scale = (False, False, False)
|
||||
pbone.rotation_mode = 'QUATERNION'
|
||||
try:
|
||||
pbone.rigify_parameters.bbones = 3
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
pbone.rigify_parameters.skin_chain_falloff_spherical = [True, False, True]
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
pbone.rigify_parameters.skin_chain_falloff = [0.5, 1.0, -0.5]
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
pbone.rigify_parameters.skin_chain_connect_mirror = [True, False]
|
||||
except AttributeError:
|
||||
pass
|
||||
pbone = obj.pose.bones[bones['lip.B.R']]
|
||||
pbone.rigify_type = 'skin.stretchy_chain'
|
||||
pbone.lock_location = (False, False, False)
|
||||
pbone.lock_rotation = (False, False, False)
|
||||
pbone.lock_rotation_w = False
|
||||
pbone.lock_scale = (False, False, False)
|
||||
pbone.rotation_mode = 'QUATERNION'
|
||||
try:
|
||||
pbone.rigify_parameters.bbones = 3
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
pbone.rigify_parameters.skin_chain_falloff_spherical = [True, False, True]
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
pbone.rigify_parameters.skin_chain_falloff = [0.5, 1.0, -0.5]
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
pbone.rigify_parameters.skin_chain_connect_mirror = [True, False]
|
||||
except AttributeError:
|
||||
pass
|
||||
pbone = obj.pose.bones[bones['teeth.B']]
|
||||
pbone.rigify_type = 'basic.super_copy'
|
||||
pbone.lock_location = (False, False, False)
|
||||
pbone.lock_rotation = (False, False, False)
|
||||
pbone.lock_rotation_w = False
|
||||
pbone.lock_scale = (False, False, False)
|
||||
pbone.rotation_mode = 'QUATERNION'
|
||||
try:
|
||||
pbone.rigify_parameters.super_copy_widget_type = "teeth"
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
pbone.rigify_parameters.make_deform = False
|
||||
except AttributeError:
|
||||
pass
|
||||
pbone = obj.pose.bones[bones['lip1.T.L']]
|
||||
pbone.rigify_type = ''
|
||||
pbone.lock_location = (False, False, False)
|
||||
pbone.lock_rotation = (False, False, False)
|
||||
pbone.lock_rotation_w = False
|
||||
pbone.lock_scale = (False, False, False)
|
||||
pbone.rotation_mode = 'QUATERNION'
|
||||
pbone = obj.pose.bones[bones['lip1.B.L']]
|
||||
pbone.rigify_type = ''
|
||||
pbone.lock_location = (False, False, False)
|
||||
pbone.lock_rotation = (False, False, False)
|
||||
pbone.lock_rotation_w = False
|
||||
pbone.lock_scale = (False, False, False)
|
||||
pbone.rotation_mode = 'QUATERNION'
|
||||
pbone = obj.pose.bones[bones['lip1.T.R']]
|
||||
pbone.rigify_type = ''
|
||||
pbone.lock_location = (False, False, False)
|
||||
pbone.lock_rotation = (False, False, False)
|
||||
pbone.lock_rotation_w = False
|
||||
pbone.lock_scale = (False, False, False)
|
||||
pbone.rotation_mode = 'QUATERNION'
|
||||
pbone = obj.pose.bones[bones['lip1.B.R']]
|
||||
pbone.rigify_type = ''
|
||||
pbone.lock_location = (False, False, False)
|
||||
pbone.lock_rotation = (False, False, False)
|
||||
pbone.lock_rotation_w = False
|
||||
pbone.lock_scale = (False, False, False)
|
||||
pbone.rotation_mode = 'QUATERNION'
|
||||
pbone = obj.pose.bones[bones['lip2.T.L']]
|
||||
pbone.rigify_type = ''
|
||||
pbone.lock_location = (False, False, False)
|
||||
pbone.lock_rotation = (False, False, False)
|
||||
pbone.lock_rotation_w = False
|
||||
pbone.lock_scale = (False, False, False)
|
||||
pbone.rotation_mode = 'QUATERNION'
|
||||
pbone = obj.pose.bones[bones['lip2.B.L']]
|
||||
pbone.rigify_type = ''
|
||||
pbone.lock_location = (False, False, False)
|
||||
pbone.lock_rotation = (False, False, False)
|
||||
pbone.lock_rotation_w = False
|
||||
pbone.lock_scale = (False, False, False)
|
||||
pbone.rotation_mode = 'QUATERNION'
|
||||
pbone = obj.pose.bones[bones['lip2.T.R']]
|
||||
pbone.rigify_type = ''
|
||||
pbone.lock_location = (False, False, False)
|
||||
pbone.lock_rotation = (False, False, False)
|
||||
pbone.lock_rotation_w = False
|
||||
pbone.lock_scale = (False, False, False)
|
||||
pbone.rotation_mode = 'QUATERNION'
|
||||
pbone = obj.pose.bones[bones['lip2.B.R']]
|
||||
pbone.rigify_type = ''
|
||||
pbone.lock_location = (False, False, False)
|
||||
pbone.lock_rotation = (False, False, False)
|
||||
pbone.lock_rotation_w = False
|
||||
pbone.lock_scale = (False, False, False)
|
||||
pbone.rotation_mode = 'QUATERNION'
|
||||
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
for bone in arm.edit_bones:
|
||||
bone.select = False
|
||||
bone.select_head = False
|
||||
bone.select_tail = False
|
||||
for b in bones:
|
||||
bone = arm.edit_bones[bones[b]]
|
||||
bone.select = True
|
||||
bone.select_head = True
|
||||
bone.select_tail = True
|
||||
bone.bbone_x = bone.bbone_z = bone.length * 0.05
|
||||
arm.edit_bones.active = bone
|
||||
|
||||
return bones
|
|
@ -0,0 +1,142 @@
|
|||
# ====================== BEGIN GPL LICENSE BLOCK ======================
|
||||
#
|
||||
# This program 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 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program 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 this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ======================= END GPL LICENSE BLOCK ========================
|
||||
|
||||
# <pep8 compliant>
|
||||
|
||||
import bpy
|
||||
|
||||
from ...utils.naming import make_derived_name
|
||||
from ...utils.widgets import layout_widget_dropdown, create_registered_widget
|
||||
from ...utils.mechanism import move_all_constraints
|
||||
|
||||
from ...base_rig import stage
|
||||
|
||||
from .skin_nodes import ControlBoneNode, ControlNodeIcon, ControlNodeEnd
|
||||
from .skin_rigs import BaseSkinChainRigWithRotationOption
|
||||
|
||||
from ..basic.raw_copy import RelinkConstraintsMixin
|
||||
|
||||
|
||||
class Rig(BaseSkinChainRigWithRotationOption, RelinkConstraintsMixin):
|
||||
"""Custom skin control node."""
|
||||
|
||||
chain_priority = 20
|
||||
|
||||
def find_org_bones(self, bone):
|
||||
return bone.name
|
||||
|
||||
def initialize(self):
|
||||
super().initialize()
|
||||
|
||||
self.make_deform = self.params.make_extra_deform
|
||||
|
||||
####################################################
|
||||
# CONTROL NODES
|
||||
|
||||
@stage.initialize
|
||||
def init_control_nodes(self):
|
||||
org = self.bones.org
|
||||
name = make_derived_name(org, 'ctrl')
|
||||
|
||||
self.control_node = node = ControlBoneNode(
|
||||
self, org, name, icon=ControlNodeIcon.CUSTOM, chain_end=ControlNodeEnd.START)
|
||||
|
||||
node.hide_control = self.params.skin_anchor_hide
|
||||
|
||||
def make_control_node_widget(self, node):
|
||||
create_registered_widget(self.obj, node.control_bone,
|
||||
self.params.pivot_master_widget_type or 'cube')
|
||||
|
||||
def extend_control_node_rig(self, node):
|
||||
if node.rig == self:
|
||||
org = self.bones.org
|
||||
|
||||
self.copy_bone_properties(org, node.control_bone)
|
||||
|
||||
self.relink_bone_constraints(org)
|
||||
|
||||
move_all_constraints(self.obj, org, node.control_bone)
|
||||
|
||||
##############################
|
||||
# ORG chain
|
||||
|
||||
@stage.parent_bones
|
||||
def parent_org_chain(self):
|
||||
self.set_bone_parent(self.bones.org, self.control_node.control_bone)
|
||||
|
||||
##############################
|
||||
# Deform bone
|
||||
|
||||
@stage.generate_bones
|
||||
def make_deform_bone(self):
|
||||
if self.make_deform:
|
||||
self.bones.deform = self.copy_bone(
|
||||
self.bones.org, make_derived_name(self.bones.org, 'def'))
|
||||
|
||||
@stage.parent_bones
|
||||
def parent_deform_chain(self):
|
||||
if self.make_deform:
|
||||
self.set_bone_parent(self.bones.deform, self.bones.org)
|
||||
|
||||
####################################################
|
||||
# SETTINGS
|
||||
|
||||
@classmethod
|
||||
def add_parameters(self, params):
|
||||
params.make_extra_deform = bpy.props.BoolProperty(
|
||||
name="Extra Deform",
|
||||
default=False,
|
||||
description="Create an optional deform bone"
|
||||
)
|
||||
|
||||
params.skin_anchor_hide = bpy.props.BoolProperty(
|
||||
name='Suppress Control',
|
||||
default=False,
|
||||
description='Make the control bone a mechanism bone invisible to the user and only affected by constraints'
|
||||
)
|
||||
|
||||
params.pivot_master_widget_type = bpy.props.StringProperty(
|
||||
name="Widget Type",
|
||||
default='cube',
|
||||
description="Choose the type of the widget to create"
|
||||
)
|
||||
|
||||
self.add_relink_constraints_params(params)
|
||||
|
||||
super().add_parameters(params)
|
||||
|
||||
@classmethod
|
||||
def parameters_ui(self, layout, params):
|
||||
col = layout.column()
|
||||
col.prop(params, "make_extra_deform", text='Generate Deform Bone')
|
||||
col.prop(params, "skin_anchor_hide")
|
||||
|
||||
row = layout.row()
|
||||
row.active = not params.skin_anchor_hide
|
||||
layout_widget_dropdown(row, params, "pivot_master_widget_type")
|
||||
|
||||
layout.prop(params, "relink_constraints")
|
||||
|
||||
layout.label(text="All constraints are moved to the control bone.", icon='INFO')
|
||||
|
||||
super().parameters_ui(layout, params)
|
||||
|
||||
|
||||
def create_sample(obj):
|
||||
from rigify.rigs.basic.super_copy import create_sample as inner
|
||||
obj.pose.bones[inner(obj)["Bone"]].rigify_type = 'skin.anchor'
|
|
@ -0,0 +1,520 @@
|
|||
# ====================== BEGIN GPL LICENSE BLOCK ======================
|
||||
#
|
||||
# This program 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 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program 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 this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ======================= END GPL LICENSE BLOCK ========================
|
||||
|
||||
# <pep8 compliant>
|
||||
|
||||
import bpy
|
||||
import math
|
||||
|
||||
from itertools import count, repeat
|
||||
from mathutils import Vector, Matrix, Quaternion
|
||||
|
||||
from math import acos
|
||||
from bl_math import smoothstep
|
||||
|
||||
from ...utils.rig import connected_children_names, rig_is_child
|
||||
from ...utils.layers import ControlLayersOption
|
||||
from ...utils.naming import make_derived_name
|
||||
from ...utils.bones import align_bone_orientation, align_bone_to_axis, align_bone_roll
|
||||
from ...utils.mechanism import driver_var_distance
|
||||
from ...utils.widgets_basic import create_cube_widget, create_sphere_widget
|
||||
from ...utils.misc import map_list, matrix_from_axis_roll
|
||||
|
||||
from ...base_rig import stage
|
||||
|
||||
from .skin_nodes import ControlBoneNode, ControlNodeEnd
|
||||
from .skin_rigs import BaseSkinChainRigWithRotationOption, get_bone_quaternion
|
||||
|
||||
|
||||
class Rig(BaseSkinChainRigWithRotationOption):
|
||||
"""
|
||||
Base deform rig of the skin system, implementing a B-Bone chain without
|
||||
any automation on the control nodes.
|
||||
"""
|
||||
|
||||
chain_priority = None
|
||||
|
||||
def find_org_bones(self, bone):
|
||||
return [bone.name] + connected_children_names(self.obj, bone.name)
|
||||
|
||||
def initialize(self):
|
||||
super().initialize()
|
||||
|
||||
self.bbone_segments = self.params.bbones
|
||||
self.use_bbones = self.bbone_segments > 1
|
||||
self.use_connect_mirror = self.params.skin_chain_connect_mirror
|
||||
self.use_connect_ends = self.params.skin_chain_connect_ends
|
||||
self.use_scale = any(self.params.skin_chain_use_scale)
|
||||
self.use_reparent_handles = self.params.skin_chain_use_reparent
|
||||
|
||||
orgs = self.bones.org
|
||||
|
||||
self.num_orgs = len(orgs)
|
||||
self.length = sum([self.get_bone(b).length for b in orgs]) / len(orgs)
|
||||
|
||||
####################################################
|
||||
# OVERRIDES
|
||||
|
||||
def get_control_node_rotation(self, node):
|
||||
"""Compute the chain-aligned control orientation."""
|
||||
orgs = self.bones.org
|
||||
|
||||
# Average the adjoining org bone orientations
|
||||
bones = orgs[max(0, node.index-1):node.index+1]
|
||||
quats = [get_bone_quaternion(self.obj, name) for name in bones]
|
||||
result = sum(quats, Quaternion((0, 0, 0, 0))).normalized()
|
||||
|
||||
# For end bones, align to the connected chain tangent
|
||||
if node.index in (0, self.num_orgs):
|
||||
chain = self.get_node_chain_with_mirror()
|
||||
nprev = chain[node.index]
|
||||
nnext = chain[node.index+2]
|
||||
|
||||
if nprev and nnext:
|
||||
# Apply only swing to preserve roll; tgt roll thus doesn't matter
|
||||
tgt = matrix_from_axis_roll(nnext.point - nprev.point, 0).to_quaternion()
|
||||
swing, _ = (result.inverted() @ tgt).to_swing_twist('Y')
|
||||
result = result @ swing
|
||||
|
||||
return result
|
||||
|
||||
def get_all_controls(self):
|
||||
return [node.control_bone for node in self.control_nodes]
|
||||
|
||||
####################################################
|
||||
# BONES
|
||||
#
|
||||
# mch:
|
||||
# handles[]
|
||||
# Final B-Bone handles.
|
||||
# handles_pre[] (optional, may be copy of handles[])
|
||||
# Mechanism bones that emulate Auto handle behavior.
|
||||
# deform[]:
|
||||
# Deformation B-Bones.
|
||||
#
|
||||
####################################################
|
||||
|
||||
####################################################
|
||||
# CONTROL NODES
|
||||
|
||||
@stage.initialize
|
||||
def init_control_nodes(self):
|
||||
orgs = self.bones.org
|
||||
|
||||
self.control_nodes = nodes = [
|
||||
# Bone head nodes
|
||||
*map_list(self.make_control_node, count(0), orgs, repeat(False)),
|
||||
# Tail of the final bone
|
||||
self.make_control_node(len(orgs), orgs[-1], True),
|
||||
]
|
||||
|
||||
self.control_node_chain = None
|
||||
|
||||
nodes[0].chain_end_neighbor = nodes[1]
|
||||
nodes[-1].chain_end_neighbor = nodes[-2]
|
||||
|
||||
def make_control_node(self, i, org, is_end):
|
||||
bone = self.get_bone(org)
|
||||
name = make_derived_name(org, 'ctrl', '_end' if is_end else '')
|
||||
pos = bone.tail if is_end else bone.head
|
||||
|
||||
if i == 0:
|
||||
chain_end = ControlNodeEnd.START
|
||||
elif is_end:
|
||||
chain_end = ControlNodeEnd.END
|
||||
else:
|
||||
chain_end = ControlNodeEnd.MIDDLE
|
||||
|
||||
return ControlBoneNode(
|
||||
self, org, name, point=pos, size=self.length/3, index=i,
|
||||
allow_scale=self.use_scale, needs_reparent=self.use_reparent_handles,
|
||||
chain_end=chain_end,
|
||||
)
|
||||
|
||||
def make_control_node_widget(self, node):
|
||||
create_sphere_widget(self.obj, node.control_bone)
|
||||
|
||||
####################################################
|
||||
# B-Bone handle MCH
|
||||
|
||||
# Generate two layers of handle bones, 'pre' for the auto handle mechanism,
|
||||
# and final handles combining that with user transformation. This flag may
|
||||
# be enabled by parent controller rigs when needed in order to be able to
|
||||
# inject more automatic handle positioning mechanisms.
|
||||
use_pre_handles = False
|
||||
|
||||
def get_connected_node(self, node):
|
||||
"""Find which other chain to connect this chain to at this node."""
|
||||
is_end = 1 if node.index != 0 else 0
|
||||
corner = self.params.skin_chain_connect_sharp_angle[is_end]
|
||||
|
||||
# First try merge through mirror
|
||||
if self.use_connect_mirror[is_end]:
|
||||
mirror = node.get_best_mirror()
|
||||
|
||||
if mirror and mirror.chain_end_neighbor and isinstance(mirror.rig, Rig):
|
||||
# Connect the same chain end
|
||||
s_is_end = 1 if mirror.index != 0 else 0
|
||||
|
||||
if is_end == s_is_end and mirror.rig.use_connect_mirror[is_end]:
|
||||
mirror_corner = mirror.rig.params.skin_chain_connect_sharp_angle[is_end]
|
||||
|
||||
return mirror, mirror.chain_end_neighbor, (corner + mirror_corner)/2
|
||||
|
||||
# Then try connecting ends
|
||||
if self.use_connect_ends[is_end]:
|
||||
# Find chains that want to connect ends at this node group
|
||||
groups = ([], [])
|
||||
|
||||
for sibling in node.get_merged_siblings():
|
||||
if isinstance(sibling.rig, Rig) and sibling.chain_end_neighbor:
|
||||
s_is_end = 1 if sibling.index != 0 else 0
|
||||
|
||||
if sibling.rig.use_connect_ends[s_is_end]:
|
||||
groups[s_is_end].append(sibling)
|
||||
|
||||
# Only connect if the pairing is unambiguous
|
||||
if len(groups[0]) == 1 and len(groups[1]) == 1:
|
||||
assert node == groups[is_end][0]
|
||||
|
||||
link = groups[1 - is_end][0]
|
||||
link_corner = link.rig.params.skin_chain_connect_sharp_angle[1 - is_end]
|
||||
|
||||
return link, link.chain_end_neighbor, (corner + link_corner)/2
|
||||
|
||||
return None, None, 0
|
||||
|
||||
def get_node_chain_with_mirror(self):
|
||||
"""Get node chain with connected node extensions at the ends."""
|
||||
if self.control_node_chain is not None:
|
||||
return self.control_node_chain
|
||||
|
||||
nodes = self.control_nodes
|
||||
prev_link, self.prev_node, self.prev_corner = self.get_connected_node(nodes[0])
|
||||
next_link, self.next_node, self.next_corner = self.get_connected_node(nodes[-1])
|
||||
|
||||
self.control_node_chain = [self.prev_node, *nodes, self.next_node]
|
||||
|
||||
# Optimize connect next by sharing last handle mch
|
||||
if next_link and next_link.index == 0:
|
||||
self.next_chain_rig = next_link.rig
|
||||
else:
|
||||
self.next_chain_rig = None
|
||||
|
||||
return self.control_node_chain
|
||||
|
||||
def get_all_mch_handles(self):
|
||||
if self.next_chain_rig:
|
||||
return self.bones.mch.handles + [self.next_chain_rig.bones.mch.handles[0]]
|
||||
else:
|
||||
return self.bones.mch.handles
|
||||
|
||||
def get_all_mch_handles_pre(self):
|
||||
if self.next_chain_rig:
|
||||
return self.bones.mch.handles_pre + [self.next_chain_rig.bones.mch.handles_pre[0]]
|
||||
else:
|
||||
return self.bones.mch.handles_pre
|
||||
|
||||
@stage.generate_bones
|
||||
def make_mch_handle_bones(self):
|
||||
if self.use_bbones:
|
||||
mch = self.bones.mch
|
||||
chain = self.get_node_chain_with_mirror()
|
||||
|
||||
# If the last handle mch will be shared, drop it from chain
|
||||
if self.next_chain_rig:
|
||||
chain = chain[0:-1]
|
||||
|
||||
mch.handles = map_list(self.make_mch_handle_bone, count(0),
|
||||
chain, chain[1:], chain[2:])
|
||||
|
||||
if self.use_pre_handles:
|
||||
mch.handles_pre = map_list(self.make_mch_pre_handle_bone, count(0), mch.handles)
|
||||
else:
|
||||
mch.handles_pre = mch.handles
|
||||
|
||||
def make_mch_handle_bone(self, i, prev_node, node, next_node):
|
||||
name = self.copy_bone(node.org, make_derived_name(node.name, 'mch', '_handle'))
|
||||
|
||||
hstart = prev_node or node
|
||||
hend = next_node or node
|
||||
haxis = (hend.point - hstart.point).normalized()
|
||||
|
||||
bone = self.get_bone(name)
|
||||
bone.tail = bone.head + haxis * self.length * 3/4
|
||||
|
||||
align_bone_roll(self.obj, name, node.org)
|
||||
return name
|
||||
|
||||
def make_mch_pre_handle_bone(self, i, handle):
|
||||
return self.copy_bone(handle, make_derived_name(handle, 'mch', '_pre'))
|
||||
|
||||
@stage.parent_bones
|
||||
def parent_mch_handle_bones(self):
|
||||
if self.use_bbones:
|
||||
mch = self.bones.mch
|
||||
|
||||
if self.use_pre_handles:
|
||||
for pre in mch.handles_pre:
|
||||
self.set_bone_parent(pre, self.rig_parent_bone, inherit_scale='AVERAGE')
|
||||
|
||||
for handle in mch.handles:
|
||||
self.set_bone_parent(handle, self.rig_parent_bone, inherit_scale='AVERAGE')
|
||||
|
||||
@stage.rig_bones
|
||||
def rig_mch_handle_bones(self):
|
||||
if self.use_bbones:
|
||||
mch = self.bones.mch
|
||||
chain = self.get_node_chain_with_mirror()
|
||||
|
||||
# Rig Auto-handle emulation (on pre handles)
|
||||
for args in zip(count(0), mch.handles_pre, chain, chain[1:], chain[2:]):
|
||||
self.rig_mch_handle_auto(*args)
|
||||
|
||||
# Apply user transformation to the final handles
|
||||
for args in zip(count(0), mch.handles, chain, chain[1:], chain[2:], mch.handles_pre):
|
||||
self.rig_mch_handle_user(*args)
|
||||
|
||||
def rig_mch_handle_auto(self, i, mch, prev_node, node, next_node):
|
||||
hstart = prev_node or node
|
||||
hend = next_node or node
|
||||
|
||||
# Emulate auto handle
|
||||
self.make_constraint(mch, 'COPY_LOCATION', hstart.control_bone, name='locate_prev')
|
||||
self.make_constraint(mch, 'DAMPED_TRACK', hend.control_bone, name='track_next')
|
||||
|
||||
def rig_mch_handle_user(self, i, mch, prev_node, node, next_node, pre):
|
||||
# Copy from the pre handle if used. Before Full is used to allow
|
||||
# drivers on local transform channels to still work.
|
||||
if pre != mch:
|
||||
self.make_constraint(
|
||||
mch, 'COPY_TRANSFORMS', pre, name='copy_pre',
|
||||
space='LOCAL', mix_mode='BEFORE_FULL',
|
||||
)
|
||||
|
||||
# Apply user rotation and scale.
|
||||
# If the node belongs to a parent of this rig, there is a good chance this
|
||||
# may cause weird double transformation, so skip it in that case.
|
||||
if not rig_is_child(self, node.merged_master.rig, strict=True):
|
||||
input_bone = node.reparent_bone if self.use_reparent_handles else node.control_bone
|
||||
|
||||
self.make_constraint(
|
||||
mch, 'COPY_TRANSFORMS', input_bone, name='copy_user',
|
||||
target_space='LOCAL_OWNER_ORIENT', owner_space='LOCAL',
|
||||
mix_mode='BEFORE_FULL',
|
||||
)
|
||||
|
||||
# Remove any shear created by the previous steps
|
||||
self.make_constraint(mch, 'LIMIT_ROTATION', name='remove_shear')
|
||||
|
||||
##############################
|
||||
# ORG chain
|
||||
|
||||
@stage.parent_bones
|
||||
def parent_org_chain(self):
|
||||
orgs = self.bones.org
|
||||
self.set_bone_parent(orgs[0], self.rig_parent_bone, inherit_scale='AVERAGE')
|
||||
self.parent_bone_chain(orgs, use_connect=True, inherit_scale='AVERAGE')
|
||||
|
||||
@stage.rig_bones
|
||||
def rig_org_chain(self):
|
||||
for args in zip(count(0), self.bones.org, self.control_nodes, self.control_nodes[1:]):
|
||||
self.rig_org_bone(*args)
|
||||
|
||||
def rig_org_bone(self, i, org, node, next_node):
|
||||
if i == 0:
|
||||
self.make_constraint(org, 'COPY_LOCATION', node.control_bone)
|
||||
|
||||
self.make_constraint(org, 'STRETCH_TO', next_node.control_bone, keep_axis='SWING_Y')
|
||||
|
||||
##############################
|
||||
# Deform chain
|
||||
|
||||
@stage.generate_bones
|
||||
def make_deform_chain(self):
|
||||
self.bones.deform = map_list(self.make_deform_bone, count(0), self.bones.org)
|
||||
|
||||
def make_deform_bone(self, i, org):
|
||||
name = self.copy_bone(org, make_derived_name(org, 'def'), bbone=True)
|
||||
self.get_bone(name).bbone_segments = self.bbone_segments
|
||||
return name
|
||||
|
||||
@stage.parent_bones
|
||||
def parent_deform_chain(self):
|
||||
deform = self.bones.deform
|
||||
|
||||
self.set_bone_parent(deform[0], self.rig_parent_bone, inherit_scale='AVERAGE')
|
||||
self.parent_bone_chain(deform, use_connect=True, inherit_scale='AVERAGE')
|
||||
|
||||
if self.use_bbones:
|
||||
handles = self.get_all_mch_handles()
|
||||
|
||||
for name, start_handle, end_handle in zip(deform, handles, handles[1:]):
|
||||
bone = self.get_bone(name)
|
||||
bone.bbone_handle_type_start = 'TANGENT'
|
||||
bone.bbone_custom_handle_start = self.get_bone(start_handle)
|
||||
bone.bbone_handle_type_end = 'TANGENT'
|
||||
bone.bbone_custom_handle_end = self.get_bone(end_handle)
|
||||
|
||||
if self.use_scale:
|
||||
bone.bbone_handle_use_scale_start = self.params.skin_chain_use_scale[0:3]
|
||||
bone.bbone_handle_use_scale_end = self.params.skin_chain_use_scale[0:3]
|
||||
|
||||
bone.bbone_handle_use_ease_start = self.params.skin_chain_use_scale[3]
|
||||
bone.bbone_handle_use_ease_end = self.params.skin_chain_use_scale[3]
|
||||
|
||||
@stage.rig_bones
|
||||
def rig_deform_chain(self):
|
||||
for args in zip(count(0), self.bones.deform, self.bones.org):
|
||||
self.rig_deform_bone(*args)
|
||||
|
||||
def rig_deform_bone(self, i, deform, org):
|
||||
self.make_constraint(deform, 'COPY_TRANSFORMS', org)
|
||||
|
||||
if self.use_bbones:
|
||||
if i == 0 and self.prev_corner > 1e-3:
|
||||
self.make_corner_driver(
|
||||
deform, 'bbone_easein', self.control_nodes[0], self.control_nodes[1], self.prev_node, self.prev_corner)
|
||||
|
||||
elif i == self.num_orgs-1 and self.next_corner > 1e-3:
|
||||
self.make_corner_driver(
|
||||
deform, 'bbone_easeout', self.control_nodes[-1], self.control_nodes[-2], self.next_node, self.next_corner)
|
||||
|
||||
def make_corner_driver(self, bbone, field, corner_node, next_node1, next_node2, angle):
|
||||
"""
|
||||
Create a driver adjusting B-Bone Ease based on the angle between controls,
|
||||
gradually making the corner sharper when the angle drops below the threshold.
|
||||
"""
|
||||
pbone = self.get_bone(bbone)
|
||||
|
||||
a = (corner_node.point - next_node1.point).length
|
||||
b = (corner_node.point - next_node2.point).length
|
||||
c = (next_node1.point - next_node2.point).length
|
||||
|
||||
varmap = {
|
||||
'a': driver_var_distance(self.obj, bone1=corner_node.control_bone, bone2=next_node1.control_bone),
|
||||
'b': driver_var_distance(self.obj, bone1=corner_node.control_bone, bone2=next_node2.control_bone),
|
||||
'c': driver_var_distance(self.obj, bone1=next_node1.control_bone, bone2=next_node2.control_bone),
|
||||
}
|
||||
|
||||
# Compute and set the ease in rest pose
|
||||
initval = -1+2*smoothstep(-1, 1, acos((a*a+b*b-c*c)/max(2*a*b, 1e-10))/angle)
|
||||
|
||||
setattr(pbone.bone, field, initval)
|
||||
|
||||
# Create the actual driver
|
||||
self.make_driver(
|
||||
pbone, field,
|
||||
expression='%f+2*smoothstep(-1,1,acos((a*a+b*b-c*c)/max(2*a*b,1e-10))/%f)' % (-1-initval, angle),
|
||||
variables=varmap
|
||||
)
|
||||
|
||||
####################################################
|
||||
# SETTINGS
|
||||
|
||||
@classmethod
|
||||
def add_parameters(self, params):
|
||||
params.bbones = bpy.props.IntProperty(
|
||||
name='B-Bone Segments',
|
||||
default=10,
|
||||
min=1,
|
||||
description='Number of B-Bone segments'
|
||||
)
|
||||
|
||||
params.skin_chain_use_reparent = bpy.props.BoolProperty(
|
||||
name='Merge Parent Rotation And Scale',
|
||||
default=False,
|
||||
description='When controls are merged into ones owned by other chains, include ' +
|
||||
'parent-induced rotation/scale difference into handle motion. Otherwise ' +
|
||||
'only local motion of the control bone is used',
|
||||
)
|
||||
|
||||
params.skin_chain_use_scale = bpy.props.BoolVectorProperty(
|
||||
size=4,
|
||||
name='Use Handle Scale',
|
||||
default=(False, False, False, False),
|
||||
description='Use control scaling to scale the B-Bone'
|
||||
)
|
||||
|
||||
params.skin_chain_connect_mirror = bpy.props.BoolVectorProperty(
|
||||
size=2,
|
||||
name='Connect With Mirror',
|
||||
default=(True, True),
|
||||
description='Create a smooth B-Bone transition if an end of the chain meets its mirror'
|
||||
)
|
||||
|
||||
params.skin_chain_connect_sharp_angle = bpy.props.FloatVectorProperty(
|
||||
size=2,
|
||||
name='Sharpen Corner',
|
||||
default=(0, 0),
|
||||
min=0,
|
||||
max=math.pi,
|
||||
description='Create a mechanism to sharpen a connected corner when the angle is below this value',
|
||||
unit='ROTATION',
|
||||
)
|
||||
|
||||
params.skin_chain_connect_ends = bpy.props.BoolVectorProperty(
|
||||
size=2,
|
||||
name='Connect Matching Ends',
|
||||
default=(False, False),
|
||||
description='Create a smooth B-Bone transition if an end of the chain meets another chain going in the same direction'
|
||||
)
|
||||
|
||||
super().add_parameters(params)
|
||||
|
||||
@classmethod
|
||||
def parameters_ui(self, layout, params):
|
||||
layout.prop(params, "bbones")
|
||||
|
||||
col = layout.column()
|
||||
col.active = params.bbones > 1
|
||||
|
||||
col.prop(params, "skin_chain_use_reparent")
|
||||
|
||||
row = col.split(factor=0.3)
|
||||
row.label(text="Use Scale:")
|
||||
row = row.row(align=True)
|
||||
row.prop(params, "skin_chain_use_scale", index=0, text="X", toggle=True)
|
||||
row.prop(params, "skin_chain_use_scale", index=1, text="Y", toggle=True)
|
||||
row.prop(params, "skin_chain_use_scale", index=2, text="Z", toggle=True)
|
||||
row.prop(params, "skin_chain_use_scale", index=3, text="Ease", toggle=True)
|
||||
|
||||
row = col.split(factor=0.3)
|
||||
row.label(text="Connect Mirror:")
|
||||
row = row.row(align=True)
|
||||
row.prop(params, "skin_chain_connect_mirror", index=0, text="Start", toggle=True)
|
||||
row.prop(params, "skin_chain_connect_mirror", index=1, text="End", toggle=True)
|
||||
|
||||
row = col.split(factor=0.3)
|
||||
row.label(text="Connect Next:")
|
||||
row = row.row(align=True)
|
||||
row.prop(params, "skin_chain_connect_ends", index=0, text="Start", toggle=True)
|
||||
row.prop(params, "skin_chain_connect_ends", index=1, text="End", toggle=True)
|
||||
|
||||
row = col.split(factor=0.3)
|
||||
row.label(text="Sharpen:")
|
||||
row = row.row(align=True)
|
||||
row.prop(params, "skin_chain_connect_sharp_angle", index=0, text="Start")
|
||||
row.prop(params, "skin_chain_connect_sharp_angle", index=1, text="End")
|
||||
|
||||
super().parameters_ui(layout, params)
|
||||
|
||||
|
||||
def create_sample(obj):
|
||||
from rigify.rigs.basic.copy_chain import create_sample as inner
|
||||
obj.pose.bones[inner(obj)["bone.01"]].rigify_type = 'skin.basic_chain'
|
|
@ -0,0 +1,321 @@
|
|||
# ====================== BEGIN GPL LICENSE BLOCK ======================
|
||||
#
|
||||
# This program 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 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program 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 this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ======================= END GPL LICENSE BLOCK ========================
|
||||
|
||||
# <pep8 compliant>
|
||||
|
||||
import bpy
|
||||
|
||||
from ...utils.naming import make_derived_name
|
||||
from ...utils.widgets_basic import create_cube_widget
|
||||
from ...utils.mechanism import move_all_constraints
|
||||
|
||||
from ...base_rig import stage
|
||||
from ...base_generate import SubstitutionRig
|
||||
|
||||
from .skin_nodes import ControlQueryNode
|
||||
from .skin_rigs import BaseSkinRig
|
||||
|
||||
from ..basic.raw_copy import RelinkConstraintsMixin
|
||||
|
||||
from .basic_chain import Rig as BasicChainRig
|
||||
|
||||
|
||||
class Rig(SubstitutionRig):
|
||||
"""Skin rig component that injects constraints into a control generated by other rigs."""
|
||||
|
||||
def substitute(self):
|
||||
# Deformation is implemented by inheriting from the chain rig, so
|
||||
# enabling it requires switching between two different classes.
|
||||
if self.params.skin_glue_head_mode == 'BRIDGE':
|
||||
return [self.instantiate_rig(BridgeGlueRig, self.base_bone)]
|
||||
else:
|
||||
return [self.instantiate_rig(SimpleGlueRig, self.base_bone)]
|
||||
|
||||
|
||||
def add_parameters(params):
|
||||
SimpleGlueRig.add_parameters(params)
|
||||
BridgeGlueRig.add_parameters(params)
|
||||
|
||||
|
||||
def parameters_ui(layout, params):
|
||||
if params.skin_glue_head_mode == 'BRIDGE':
|
||||
BridgeGlueRig.parameters_ui(layout, params)
|
||||
else:
|
||||
SimpleGlueRig.parameters_ui(layout, params)
|
||||
|
||||
|
||||
class BaseGlueRig(BaseSkinRig, RelinkConstraintsMixin):
|
||||
"""Base class for the glue rigs."""
|
||||
|
||||
def initialize(self):
|
||||
super().initialize()
|
||||
|
||||
self.glue_head_mode = self.params.skin_glue_head_mode
|
||||
|
||||
self.glue_use_tail = self.params.relink_constraints and self.params.skin_glue_use_tail
|
||||
self.relink_unmarked_constraints = self.glue_use_tail
|
||||
|
||||
####################################################
|
||||
# QUERY NODES
|
||||
|
||||
@stage.initialize
|
||||
def init_glue_nodes(self):
|
||||
bone = self.get_bone(self.base_bone)
|
||||
|
||||
self.head_constraint_node = ControlQueryNode(
|
||||
self, self.base_bone, point=bone.head
|
||||
)
|
||||
|
||||
if self.glue_use_tail:
|
||||
self.tail_position_node = PositionQueryNode(
|
||||
self, self.base_bone, point=bone.tail,
|
||||
needs_reparent=self.params.skin_glue_tail_reparent,
|
||||
)
|
||||
|
||||
####################################################
|
||||
# GLUE CONSTRAINTS
|
||||
|
||||
def rig_glue_constraints(self):
|
||||
org = self.base_bone
|
||||
ctrl = self.head_constraint_node.control_bone
|
||||
|
||||
self.relink_bone_constraints(org)
|
||||
|
||||
# Add the built-in constraint
|
||||
if self.glue_use_tail:
|
||||
target = self.tail_position_node.output_bone
|
||||
add_mode = self.params.skin_glue_add_constraint
|
||||
inf = self.params.skin_glue_add_constraint_influence
|
||||
|
||||
if add_mode == 'COPY_LOCATION':
|
||||
self.make_constraint(
|
||||
ctrl, 'COPY_LOCATION', target, insert_index=0,
|
||||
owner_space='LOCAL', target_space='LOCAL',
|
||||
use_offset=True, influence=inf
|
||||
)
|
||||
elif add_mode == 'COPY_LOCATION_OWNER':
|
||||
self.make_constraint(
|
||||
ctrl, 'COPY_LOCATION', target, insert_index=0,
|
||||
owner_space='LOCAL', target_space='LOCAL_OWNER_ORIENT',
|
||||
use_offset=True, influence=inf
|
||||
)
|
||||
|
||||
move_all_constraints(self.obj, org, ctrl)
|
||||
|
||||
def find_relink_target(self, spec, old_target):
|
||||
if self.glue_use_tail and (spec == 'TARGET' or spec == '' == old_target):
|
||||
return self.tail_position_node.output_bone
|
||||
|
||||
return super().find_relink_target(spec, old_target)
|
||||
|
||||
####################################################
|
||||
# SETTINGS
|
||||
|
||||
@classmethod
|
||||
def add_parameters(self, params):
|
||||
params.skin_glue_head_mode = bpy.props.EnumProperty(
|
||||
name='Glue Mode',
|
||||
items=[('CHILD', 'Child Of Control',
|
||||
"The glue bone becomes a child of the control bone"),
|
||||
('MIRROR', 'Mirror Of Control',
|
||||
"The glue bone becomes a sibling of the control bone with Copy Transforms"),
|
||||
('REPARENT', 'Mirror With Parents',
|
||||
"The glue bone keeps its parent, but uses Copy Transforms to group both local and parent induced motion of the control into local space"),
|
||||
('BRIDGE', 'Deformation Bridge',
|
||||
"Other than adding glue constraints to the control, the rig acts as a one segment basic deform chain")],
|
||||
default='CHILD',
|
||||
description="Specifies how the glue bone is rigged to the control at the bone head location",
|
||||
)
|
||||
|
||||
params.skin_glue_use_tail = bpy.props.BoolProperty(
|
||||
name='Use Tail Target',
|
||||
default=False,
|
||||
description='Find the control at the bone tail location and use it to relink TARGET or any constraints without an assigned subtarget or relink spec'
|
||||
)
|
||||
|
||||
params.skin_glue_tail_reparent = bpy.props.BoolProperty(
|
||||
name='Target Local With Parents',
|
||||
default=False,
|
||||
description='Include transformations induced by target parents into target local space'
|
||||
)
|
||||
|
||||
params.skin_glue_add_constraint = bpy.props.EnumProperty(
|
||||
name='Add Constraint',
|
||||
items=[('NONE', 'No New Constraint',
|
||||
"Don't add new constraints"),
|
||||
('COPY_LOCATION', 'Copy Location (Local)',
|
||||
"Add a constraint to copy Local Location with Offset. If the owner and target control " +
|
||||
"rest orientations are different, the global movement direction will change accordingly"),
|
||||
('COPY_LOCATION_OWNER', 'Copy Location (Local, Owner Orientation)',
|
||||
"Add a constraint to copy Local Location (Owner Orientation) with Offset. Even if the owner and " +
|
||||
"target controls have different rest orientations, the global movement direction would be the same")],
|
||||
default='NONE',
|
||||
description="Add one of the common constraints linking the control to the tail target",
|
||||
)
|
||||
|
||||
params.skin_glue_add_constraint_influence = bpy.props.FloatProperty(
|
||||
name="Influence",
|
||||
default=1.0, min=0, max=1,
|
||||
description="Influence of the added constraint",
|
||||
)
|
||||
|
||||
self.add_relink_constraints_params(params)
|
||||
|
||||
super().add_parameters(params)
|
||||
|
||||
@classmethod
|
||||
def parameters_ui(self, layout, params):
|
||||
layout.prop(params, "skin_glue_head_mode")
|
||||
layout.prop(params, "relink_constraints")
|
||||
|
||||
if params.relink_constraints:
|
||||
col = layout.column()
|
||||
col.prop(params, "skin_glue_use_tail")
|
||||
|
||||
col2 = col.column()
|
||||
col2.active = params.skin_glue_use_tail
|
||||
col2.prop(params, "skin_glue_tail_reparent")
|
||||
|
||||
col = layout.column()
|
||||
col.active = params.skin_glue_use_tail
|
||||
col.prop(params, "skin_glue_add_constraint", text="Add")
|
||||
|
||||
col3 = col.column()
|
||||
col3.active = params.skin_glue_add_constraint != 'NONE'
|
||||
col3.prop(params, "skin_glue_add_constraint_influence", slider=True)
|
||||
|
||||
layout.label(text="All constraints are moved to the control bone.", icon='INFO')
|
||||
|
||||
super().parameters_ui(layout, params)
|
||||
|
||||
|
||||
class SimpleGlueRig(BaseGlueRig):
|
||||
"""Normal glue rig that only does glue."""
|
||||
|
||||
def find_org_bones(self, bone):
|
||||
return bone.name
|
||||
|
||||
####################################################
|
||||
# QUERY NODES
|
||||
|
||||
@stage.initialize
|
||||
def init_glue_nodes(self):
|
||||
super().init_glue_nodes()
|
||||
|
||||
bone = self.get_bone(self.base_bone)
|
||||
|
||||
self.head_position_node = PositionQueryNode(
|
||||
self, self.base_bone, point=bone.head,
|
||||
rig_org=self.glue_head_mode != 'CHILD',
|
||||
needs_reparent=self.glue_head_mode == 'REPARENT',
|
||||
)
|
||||
|
||||
##############################
|
||||
# ORG chain
|
||||
|
||||
@stage.parent_bones
|
||||
def parent_org_bone(self):
|
||||
if self.glue_head_mode == 'CHILD':
|
||||
self.set_bone_parent(self.bones.org, self.head_position_node.output_bone)
|
||||
|
||||
@stage.rig_bones
|
||||
def rig_org_bone(self):
|
||||
# This executes before head_position_node owned a by generator plugin
|
||||
self.rig_glue_constraints()
|
||||
|
||||
|
||||
class BridgeGlueRig(BaseGlueRig, BasicChainRig):
|
||||
"""Glue rig that also behaves like a deformation chain rig."""
|
||||
|
||||
def find_org_bones(self, bone):
|
||||
# Still only bind to one bone
|
||||
return [bone.name]
|
||||
|
||||
# Assign lowest priority
|
||||
chain_priority = -20
|
||||
|
||||
# Orientation is irrelevant since controls should be merged into others
|
||||
use_skin_control_orientation_bone = False
|
||||
|
||||
####################################################
|
||||
# QUERY NODES
|
||||
|
||||
@stage.prepare_bones
|
||||
def prepare_glue_nodes(self):
|
||||
# Verify that all nodes of the chain have been merged into others
|
||||
for node in self.control_nodes:
|
||||
if node.is_master_node:
|
||||
self.raise_error('glue control {} was not merged', node.name)
|
||||
|
||||
##############################
|
||||
# ORG chain
|
||||
|
||||
@stage.rig_bones
|
||||
def rig_org_chain(self):
|
||||
# Move the user constraints away before the chain adds new ones
|
||||
self.rig_glue_constraints()
|
||||
|
||||
super().rig_org_chain()
|
||||
|
||||
|
||||
class PositionQueryNode(ControlQueryNode):
|
||||
"""Finds the position of the highest layer control and rig reparent and/or org bone"""
|
||||
|
||||
def __init__(self, rig, org, *, point=None, needs_reparent=False, rig_org=False):
|
||||
super().__init__(rig, org, point=point, find_highest_layer=True)
|
||||
|
||||
self.needs_reparent = needs_reparent
|
||||
self.rig_org = rig_org
|
||||
|
||||
@property
|
||||
def output_bone(self):
|
||||
if self.rig_org:
|
||||
return self.org
|
||||
elif self.needs_reparent:
|
||||
return self.reparent_bone
|
||||
else:
|
||||
return self.control_bone
|
||||
|
||||
def initialize(self):
|
||||
if self.needs_reparent:
|
||||
parent = self.build_parent()
|
||||
|
||||
if not self.rig_org:
|
||||
self.merged_master.request_reparent(parent)
|
||||
|
||||
def parent_bones(self):
|
||||
if self.rig_org:
|
||||
if self.needs_reparent:
|
||||
parent = self.node_parent.output_bone
|
||||
else:
|
||||
parent = self.get_bone_parent(self.control_bone)
|
||||
|
||||
self.set_bone_parent(self.org, parent, inherit_scale='AVERAGE')
|
||||
|
||||
def apply_bones(self):
|
||||
if self.rig_org:
|
||||
self.get_bone(self.org).matrix = self.merged_master.matrix
|
||||
|
||||
def rig_bones(self):
|
||||
if self.rig_org:
|
||||
self.make_constraint(self.org, 'COPY_TRANSFORMS', self.control_bone)
|
||||
|
||||
|
||||
def create_sample(obj):
|
||||
from rigify.rigs.basic.super_copy import create_sample as inner
|
||||
obj.pose.bones[inner(obj)["Bone"]].rigify_type = 'skin.glue'
|
|
@ -0,0 +1,520 @@
|
|||
# ====================== BEGIN GPL LICENSE BLOCK ======================
|
||||
#
|
||||
# This program 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 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program 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 this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ======================= END GPL LICENSE BLOCK ========================
|
||||
|
||||
# <pep8 compliant>
|
||||
|
||||
import bpy
|
||||
import enum
|
||||
|
||||
from mathutils import Vector, Quaternion
|
||||
|
||||
from ...utils.layers import set_bone_layers
|
||||
from ...utils.naming import NameSides, make_derived_name, get_name_base_and_sides, change_name_side, Side, SideZ
|
||||
from ...utils.bones import BoneUtilityMixin, set_bone_widget_transform
|
||||
from ...utils.widgets_basic import create_cube_widget, create_sphere_widget
|
||||
from ...utils.mechanism import MechanismUtilityMixin
|
||||
from ...utils.rig import get_parent_rigs
|
||||
|
||||
from ...utils.node_merger import MainMergeNode, QueryMergeNode
|
||||
|
||||
from .skin_parents import ControlBoneParentLayer, ControlBoneWeakParentLayer
|
||||
from .skin_rigs import BaseSkinRig, BaseSkinChainRig
|
||||
|
||||
|
||||
class ControlNodeLayer(enum.IntEnum):
|
||||
FREE = 0
|
||||
MIDDLE_PIVOT = 10
|
||||
TWEAK = 20
|
||||
|
||||
|
||||
class ControlNodeIcon(enum.IntEnum):
|
||||
TWEAK = 0
|
||||
MIDDLE_PIVOT = 1
|
||||
FREE = 2
|
||||
CUSTOM = 3
|
||||
|
||||
|
||||
class ControlNodeEnd(enum.IntEnum):
|
||||
START = -1
|
||||
MIDDLE = 0
|
||||
END = 1
|
||||
|
||||
|
||||
class BaseSkinNode(MechanismUtilityMixin, BoneUtilityMixin):
|
||||
"""Base class for skin control and query nodes."""
|
||||
|
||||
node_parent_built = False
|
||||
|
||||
def do_build_parent(self):
|
||||
"""Create and intern the parent mechanism generator."""
|
||||
assert self.rig.generator.stage == 'initialize'
|
||||
|
||||
result = self.rig.build_own_control_node_parent(self)
|
||||
parents = self.rig.get_all_parent_skin_rigs()
|
||||
|
||||
for rig in reversed(parents):
|
||||
result = rig.extend_control_node_parent(result, self)
|
||||
|
||||
for rig in parents:
|
||||
result = rig.extend_control_node_parent_post(result, self)
|
||||
|
||||
result = self.merged_master.intern_parent(self, result)
|
||||
result.is_parent_frozen = True
|
||||
return result
|
||||
|
||||
def build_parent(self, use=True):
|
||||
"""Create and activate if needed the parent mechanism for this node."""
|
||||
if not self.node_parent_built:
|
||||
self.node_parent = self.do_build_parent()
|
||||
self.node_parent_built = True
|
||||
|
||||
if use:
|
||||
self.merged_master.register_use_parent(self.node_parent)
|
||||
|
||||
return self.node_parent
|
||||
|
||||
@property
|
||||
def control_bone(self):
|
||||
"""The generated control bone."""
|
||||
return self.merged_master._control_bone
|
||||
|
||||
@property
|
||||
def reparent_bone(self):
|
||||
"""The generated reparent bone for this node's parent mechanism."""
|
||||
return self.merged_master.get_reparent_bone(self.node_parent)
|
||||
|
||||
|
||||
class ControlBoneNode(MainMergeNode, BaseSkinNode):
|
||||
"""Node representing controls of skin chain rigs."""
|
||||
|
||||
merge_domain = 'ControlNetNode'
|
||||
|
||||
def __init__(
|
||||
self, rig, org, name, *, point=None, size=None,
|
||||
needs_parent=False, needs_reparent=False, allow_scale=False,
|
||||
chain_end=ControlNodeEnd.MIDDLE,
|
||||
layer=ControlNodeLayer.FREE, index=None, icon=ControlNodeIcon.TWEAK,
|
||||
):
|
||||
assert isinstance(rig, BaseSkinChainRig)
|
||||
|
||||
super().__init__(rig, name, point or rig.get_bone(org).head)
|
||||
|
||||
self.org = org
|
||||
|
||||
self.name_split = get_name_base_and_sides(name)
|
||||
|
||||
self.name_merged = None
|
||||
self.name_merged_split = None
|
||||
|
||||
self.size = size or rig.get_bone(org).length
|
||||
self.layer = layer
|
||||
self.icon = icon
|
||||
self.rotation = None
|
||||
self.chain_end = chain_end
|
||||
|
||||
# Create the parent mechanism even if not master
|
||||
self.node_needs_parent = needs_parent
|
||||
# If this node's own parent mechanism differs from master, generate a conversion bone
|
||||
self.node_needs_reparent = needs_reparent
|
||||
|
||||
# Generate the control as a MCH bone to hide it from the user
|
||||
self.hide_control = False
|
||||
# Unlock scale channels
|
||||
self.allow_scale = allow_scale
|
||||
|
||||
# For use by the owner rig: index in chain
|
||||
self.index = index
|
||||
# If this node is the end of a chain, points to the next one
|
||||
self.chain_end_neighbor = None
|
||||
|
||||
def can_merge_into(self, other):
|
||||
# Only merge up the layers (towards more mechanism)
|
||||
dprio = self.rig.chain_priority - other.rig.chain_priority
|
||||
return (
|
||||
dprio <= 0 and
|
||||
(self.layer <= other.layer or dprio < 0) and
|
||||
super().can_merge_into(other)
|
||||
)
|
||||
|
||||
def get_merge_priority(self, other):
|
||||
# Prefer higher and closest layer
|
||||
if self.layer <= other.layer:
|
||||
return -abs(self.layer - other.layer)
|
||||
else:
|
||||
return -abs(self.layer - other.layer) - 100
|
||||
|
||||
def is_better_cluster(self, other):
|
||||
"""Check if the current bone is preferrable as master when choosing of same sized groups."""
|
||||
|
||||
# Prefer bones that have strictly more parents
|
||||
my_parents = list(reversed(get_parent_rigs(self.rig.rigify_parent)))
|
||||
other_parents = list(reversed(get_parent_rigs(other.rig.rigify_parent)))
|
||||
|
||||
if len(my_parents) > len(other_parents) and my_parents[0:len(other_parents)] == other_parents:
|
||||
return True
|
||||
if len(other_parents) > len(my_parents) and other_parents[0:len(other_parents)] == my_parents:
|
||||
return False
|
||||
|
||||
# Prefer side chains
|
||||
side_x_my, side_z_my = map(abs, self.name_split[1:])
|
||||
side_x_other, side_z_other = map(abs, other.name_split[1:])
|
||||
|
||||
if ((side_x_my < side_x_other and side_z_my <= side_z_other) or
|
||||
(side_x_my <= side_x_other and side_z_my < side_z_other)):
|
||||
return False
|
||||
if ((side_x_my > side_x_other and side_z_my >= side_z_other) or
|
||||
(side_x_my >= side_x_other and side_z_my > side_z_other)):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def merge_done(self):
|
||||
if self.is_master_node:
|
||||
self.parent_subrig_cache = []
|
||||
self.parent_subrig_names = {}
|
||||
self.reparent_requests = []
|
||||
self.used_parents = {}
|
||||
|
||||
super().merge_done()
|
||||
|
||||
self.find_mirror_siblings()
|
||||
|
||||
def find_mirror_siblings(self):
|
||||
"""Find merged nodes that have their names in mirror symmetry with this one."""
|
||||
|
||||
self.mirror_siblings = {}
|
||||
self.mirror_sides_x = set()
|
||||
self.mirror_sides_z = set()
|
||||
|
||||
for node in self.get_merged_siblings():
|
||||
if node.name_split.base == self.name_split.base:
|
||||
self.mirror_siblings[node.name_split] = node
|
||||
self.mirror_sides_x.add(node.name_split.side)
|
||||
self.mirror_sides_z.add(node.name_split.side_z)
|
||||
|
||||
assert self.mirror_siblings[self.name_split] is self
|
||||
|
||||
# Remove sides that merged with a mirror from the name
|
||||
side_x = Side.MIDDLE if len(self.mirror_sides_x) > 1 else self.name_split.side
|
||||
side_z = SideZ.MIDDLE if len(self.mirror_sides_z) > 1 else self.name_split.side_z
|
||||
|
||||
self.name_merged = change_name_side(self.name, side=side_x, side_z=side_z)
|
||||
self.name_merged_split = NameSides(self.name_split.base, side_x, side_z)
|
||||
|
||||
def get_best_mirror(self):
|
||||
"""Find best mirror sibling for connecting via mirror."""
|
||||
|
||||
base, side, sidez = self.name_split
|
||||
|
||||
for flip in [(base, -side, -sidez), (base, -side, sidez), (base, side, -sidez)]:
|
||||
mirror = self.mirror_siblings.get(flip, None)
|
||||
if mirror and mirror is not self:
|
||||
return mirror
|
||||
|
||||
return None
|
||||
|
||||
def intern_parent(self, node, parent):
|
||||
"""De-duplicate the parent layer chain within this merge group."""
|
||||
|
||||
# Quick check for the same object
|
||||
if id(parent) in self.parent_subrig_names:
|
||||
return parent
|
||||
|
||||
# Find if an identical parent is already in the cache
|
||||
cache = self.parent_subrig_cache
|
||||
|
||||
for previous in cache:
|
||||
if previous == parent:
|
||||
previous.is_parent_frozen = True
|
||||
return previous
|
||||
|
||||
# Add to cache and intern the layer parent if exists
|
||||
cache.append(parent)
|
||||
|
||||
self.parent_subrig_names[id(parent)] = node.name
|
||||
|
||||
if isinstance(parent, ControlBoneParentLayer):
|
||||
parent.parent = self.intern_parent(node, parent.parent)
|
||||
|
||||
return parent
|
||||
|
||||
def register_use_parent(self, parent):
|
||||
"""Activate this parent mechanism generator."""
|
||||
self.used_parents[id(parent)] = parent
|
||||
|
||||
def request_reparent(self, parent):
|
||||
"""Request a reparent bone to be generated for this parent mechanism."""
|
||||
requests = self.reparent_requests
|
||||
|
||||
if parent not in requests:
|
||||
# If the actual reparent would be generated, weak parent will be needed.
|
||||
if self.has_weak_parent and not self.use_weak_parent:
|
||||
if self.use_mix_parent or parent != self.node_parent:
|
||||
self.use_weak_parent = True
|
||||
|
||||
for weak_parent in self.node_parent_list_weak:
|
||||
self.register_use_parent(weak_parent)
|
||||
|
||||
self.register_use_parent(parent)
|
||||
requests.append(parent)
|
||||
|
||||
def get_reparent_bone(self, parent):
|
||||
"""Returns the generated reparent bone for this parent mechanism."""
|
||||
return self.reparent_bones[id(parent)]
|
||||
|
||||
def get_rotation(self):
|
||||
"""Returns the orientation quaternion provided for this node by parents."""
|
||||
if self.rotation is None:
|
||||
self.rotation = self.rig.get_final_control_node_rotation(self)
|
||||
|
||||
return self.rotation
|
||||
|
||||
def initialize(self):
|
||||
if self.is_master_node:
|
||||
sibling_list = self.get_merged_siblings()
|
||||
mirror_sibling_list = self.mirror_siblings.values()
|
||||
|
||||
# Compute size
|
||||
best = max(sibling_list, key=lambda n: n.icon)
|
||||
best_mirror = best.mirror_siblings.values()
|
||||
|
||||
self.size = sum(node.size for node in best_mirror) / len(best_mirror)
|
||||
|
||||
# Compute orientation
|
||||
self.rotation = sum(
|
||||
(node.get_rotation() for node in mirror_sibling_list),
|
||||
Quaternion((0, 0, 0, 0))
|
||||
).normalized()
|
||||
|
||||
self.matrix = self.rotation.to_matrix().to_4x4()
|
||||
self.matrix.translation = self.point
|
||||
|
||||
# Create parents and decide if mix would be needed
|
||||
parent_list = [node.build_parent(use=False) for node in mirror_sibling_list]
|
||||
|
||||
if all(parent == self.node_parent for parent in parent_list):
|
||||
self.use_mix_parent = False
|
||||
parent_list = [self.node_parent]
|
||||
else:
|
||||
self.use_mix_parent = True
|
||||
|
||||
# Prepare parenting without weak layers
|
||||
self.use_weak_parent = False
|
||||
self.node_parent_list_weak = parent_list
|
||||
|
||||
self.node_parent_list = [ControlBoneWeakParentLayer.strip(p) for p in parent_list]
|
||||
self.has_weak_parent = any((p is not pw)
|
||||
for p, pw in zip(self.node_parent_list, parent_list))
|
||||
|
||||
for parent in self.node_parent_list:
|
||||
self.register_use_parent(parent)
|
||||
|
||||
# All nodes
|
||||
if self.node_needs_parent or self.node_needs_reparent:
|
||||
parent = self.build_parent()
|
||||
if self.node_needs_reparent:
|
||||
self.merged_master.request_reparent(parent)
|
||||
|
||||
def prepare_bones(self):
|
||||
# Activate parent components once all reparents are registered
|
||||
if self.is_master_node:
|
||||
for parent in self.used_parents.values():
|
||||
parent.enable_component()
|
||||
|
||||
self.used_parents = None
|
||||
|
||||
def make_bone(self, name, scale, *, rig=None, orientation=None):
|
||||
"""
|
||||
Creates a bone associated with this node, using the appropriate
|
||||
orientation, location and size.
|
||||
"""
|
||||
name = (rig or self).copy_bone(self.org, name)
|
||||
|
||||
if orientation is not None:
|
||||
matrix = orientation.to_matrix().to_4x4()
|
||||
matrix.translation = self.merged_master.point
|
||||
else:
|
||||
matrix = self.merged_master.matrix
|
||||
|
||||
bone = self.get_bone(name)
|
||||
bone.matrix = matrix
|
||||
bone.length = self.merged_master.size * scale
|
||||
|
||||
return name
|
||||
|
||||
def find_master_name_node(self):
|
||||
"""Find which node to name the control bone from."""
|
||||
|
||||
# Chain end nodes have sub-par names, so try to find another chain
|
||||
if self.chain_end == ControlNodeEnd.END:
|
||||
# Choose possible other nodes so that it doesn't lose mirror tags
|
||||
siblings = [
|
||||
node for node in self.get_merged_siblings()
|
||||
if self.mirror_sides_x.issubset(node.mirror_sides_x)
|
||||
and self.mirror_sides_z.issubset(node.mirror_sides_z)
|
||||
]
|
||||
|
||||
# Prefer chain start, then middle nodes
|
||||
candidates = [node for node in siblings if node.chain_end == ControlNodeEnd.START]
|
||||
|
||||
if not candidates:
|
||||
candidates = [node for node in siblings if node.chain_end == ControlNodeEnd.MIDDLE]
|
||||
|
||||
# Choose based on priority and name alphabetical order
|
||||
if candidates:
|
||||
return min(candidates, key=lambda c: (-c.rig.chain_priority, c.name_merged))
|
||||
|
||||
return self
|
||||
|
||||
def generate_bones(self):
|
||||
if self.is_master_node:
|
||||
# Make control bone
|
||||
self._control_bone = self.make_master_bone()
|
||||
|
||||
# Make weak parent bone
|
||||
if self.use_weak_parent:
|
||||
self.weak_parent_bone = self.make_bone(
|
||||
make_derived_name(self._control_bone, 'mch', '_weak_parent'), 1/2)
|
||||
|
||||
# Make mix parent if needed
|
||||
self.reparent_bones = {}
|
||||
|
||||
if self.use_mix_parent:
|
||||
self.mix_parent_bone = self.make_bone(
|
||||
make_derived_name(self._control_bone, 'mch', '_mix_parent'), 1/2)
|
||||
else:
|
||||
self.reparent_bones[id(self.node_parent)] = self._control_bone
|
||||
|
||||
# Make requested reparents
|
||||
self.reparent_bones_fake = set(self.reparent_bones.values())
|
||||
|
||||
for parent in self.reparent_requests:
|
||||
if id(parent) not in self.reparent_bones:
|
||||
parent_name = self.parent_subrig_names[id(parent)]
|
||||
bone = self.make_bone(make_derived_name(parent_name, 'mch', '_reparent'), 1/3)
|
||||
self.reparent_bones[id(parent)] = bone
|
||||
|
||||
def make_master_bone(self):
|
||||
choice = self.find_master_name_node()
|
||||
name = choice.name_merged
|
||||
|
||||
if self.hide_control:
|
||||
name = make_derived_name(name, 'mch')
|
||||
|
||||
return choice.make_bone(name, 1)
|
||||
|
||||
def parent_bones(self):
|
||||
if self.is_master_node:
|
||||
if self.use_mix_parent:
|
||||
self.set_bone_parent(self._control_bone, self.mix_parent_bone,
|
||||
inherit_scale='AVERAGE')
|
||||
self.rig.generator.disable_auto_parent(self.mix_parent_bone)
|
||||
else:
|
||||
self.set_bone_parent(self._control_bone, self.node_parent_list[0].output_bone,
|
||||
inherit_scale='AVERAGE')
|
||||
|
||||
if self.use_weak_parent:
|
||||
if self.use_mix_parent:
|
||||
self.rig.generator.disable_auto_parent(self.weak_parent_bone)
|
||||
else:
|
||||
parent = self.node_parent_list_weak[0]
|
||||
self.set_bone_parent(self.weak_parent_bone, parent.output_bone,
|
||||
inherit_scale=parent.inherit_scale_mode)
|
||||
|
||||
for parent in self.reparent_requests:
|
||||
bone = self.reparent_bones[id(parent)]
|
||||
if bone not in self.reparent_bones_fake:
|
||||
self.set_bone_parent(bone, parent.output_bone, inherit_scale='AVERAGE')
|
||||
|
||||
def configure_bones(self):
|
||||
if self.is_master_node:
|
||||
if not any(node.allow_scale for node in self.get_merged_siblings()):
|
||||
self.get_bone(self.control_bone).lock_scale = (True, True, True)
|
||||
|
||||
layers = self.rig.get_control_node_layers(self)
|
||||
if layers:
|
||||
bone = self.get_bone(self.control_bone).bone
|
||||
set_bone_layers(bone, layers, not self.is_master_node)
|
||||
|
||||
def rig_bones(self):
|
||||
if self.is_master_node:
|
||||
# Rig the mixed parent
|
||||
if self.use_mix_parent:
|
||||
targets = [parent.output_bone for parent in self.node_parent_list]
|
||||
self.make_constraint(self.mix_parent_bone, 'ARMATURE',
|
||||
targets=targets, use_deform_preserve_volume=True)
|
||||
|
||||
# Invoke parent rig callbacks
|
||||
for rig in reversed(self.rig.get_all_parent_skin_rigs()):
|
||||
rig.extend_control_node_rig(self)
|
||||
|
||||
# Rig reparent bones
|
||||
reparent_source = self.control_bone
|
||||
|
||||
if self.use_weak_parent:
|
||||
reparent_source = self.weak_parent_bone
|
||||
|
||||
self.make_constraint(reparent_source, 'COPY_TRANSFORMS',
|
||||
self.control_bone, space='LOCAL')
|
||||
|
||||
if self.use_mix_parent:
|
||||
targets = [parent.output_bone for parent in self.node_parent_list_weak]
|
||||
self.make_constraint(self.weak_parent_bone, 'ARMATURE',
|
||||
targets=targets, use_deform_preserve_volume=True)
|
||||
|
||||
set_bone_widget_transform(self.obj, self.control_bone, reparent_source)
|
||||
|
||||
for parent in self.reparent_requests:
|
||||
bone = self.reparent_bones[id(parent)]
|
||||
if bone not in self.reparent_bones_fake:
|
||||
self.make_constraint(bone, 'COPY_TRANSFORMS', reparent_source)
|
||||
|
||||
def generate_widgets(self):
|
||||
if self.is_master_node:
|
||||
best = max(self.get_merged_siblings(), key=lambda n: n.icon)
|
||||
|
||||
if best.icon == ControlNodeIcon.TWEAK:
|
||||
create_sphere_widget(self.obj, self.control_bone)
|
||||
elif best.icon in (ControlNodeIcon.MIDDLE_PIVOT, ControlNodeIcon.FREE):
|
||||
create_cube_widget(self.obj, self.control_bone)
|
||||
else:
|
||||
best.rig.make_control_node_widget(best)
|
||||
|
||||
|
||||
class ControlQueryNode(QueryMergeNode, BaseSkinNode):
|
||||
"""Node representing controls of skin chain rigs."""
|
||||
|
||||
merge_domain = 'ControlNetNode'
|
||||
|
||||
def __init__(self, rig, org, *, name=None, point=None, find_highest_layer=False):
|
||||
assert isinstance(rig, BaseSkinRig)
|
||||
|
||||
super().__init__(rig, name or org, point or rig.get_bone(org).head)
|
||||
|
||||
self.org = org
|
||||
self.find_highest_layer = find_highest_layer
|
||||
|
||||
def can_merge_into(self, other):
|
||||
return True
|
||||
|
||||
def get_merge_priority(self, other):
|
||||
return other.layer if self.find_highest_layer else -other.layer
|
||||
|
||||
@property
|
||||
def merged_master(self):
|
||||
return self.matched_nodes[0]
|
|
@ -0,0 +1,395 @@
|
|||
# ====================== BEGIN GPL LICENSE BLOCK ======================
|
||||
#
|
||||
# This program 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 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program 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 this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ======================= END GPL LICENSE BLOCK ========================
|
||||
|
||||
# <pep8 compliant>
|
||||
|
||||
import bpy
|
||||
|
||||
from itertools import count
|
||||
from string import Template
|
||||
|
||||
from ...utils.naming import make_derived_name
|
||||
from ...utils.misc import force_lazy, LazyRef
|
||||
|
||||
from ...base_rig import LazyRigComponent, stage
|
||||
|
||||
|
||||
class ControlBoneParentBase(LazyRigComponent):
|
||||
"""
|
||||
Base class for components that generate parent mechanisms for skin controls.
|
||||
The generated parent bone is accessible through the output_bone field or property.
|
||||
"""
|
||||
|
||||
# Run this component after the @stage methods of the owner node and its slave nodes
|
||||
rigify_sub_object_run_late = True
|
||||
|
||||
# This generator's output bone cannot be modified by generators layered on top.
|
||||
# Otherwise they may optimize bone count by adding more constraints in place.
|
||||
# (This generally signals the bone is shared between multiple users.)
|
||||
is_parent_frozen = False
|
||||
|
||||
def __init__(self, rig, node):
|
||||
super().__init__(node)
|
||||
|
||||
# Rig that provides this parent mechanism.
|
||||
self.rig = rig
|
||||
# Control node that the mechanism is provided for
|
||||
self.node = node
|
||||
|
||||
def __eq__(self, other):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class ControlBoneParentOrg:
|
||||
"""Control node parent generator wrapping a single ORG bone."""
|
||||
|
||||
is_parent_frozen = True
|
||||
|
||||
def __init__(self, org):
|
||||
self._output_bone = org
|
||||
|
||||
@property
|
||||
def output_bone(self):
|
||||
return force_lazy(self._output_bone)
|
||||
|
||||
def enable_component(self):
|
||||
pass
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, ControlBoneParentOrg) and self._output_bone == other._output_bone
|
||||
|
||||
|
||||
class ControlBoneParentArmature(ControlBoneParentBase):
|
||||
"""Control node parent generator using the Armature constraint to parent the bone."""
|
||||
|
||||
def __init__(self, rig, node, *, bones, orientation=None, copy_scale=None, copy_rotation=None):
|
||||
super().__init__(rig, node)
|
||||
|
||||
# List of Armature constraint target specs for make_constraint (lazy).
|
||||
self.bones = bones
|
||||
# Orientation quaternion for the bone (lazy)
|
||||
self.orientation = orientation
|
||||
# Bone to copy scale from (lazy)
|
||||
self.copy_scale = copy_scale
|
||||
# Bone to copy rotation from (lazy)
|
||||
self.copy_rotation = copy_rotation
|
||||
|
||||
if copy_scale or copy_rotation:
|
||||
self.is_parent_frozen = True
|
||||
|
||||
def __eq__(self, other):
|
||||
return (
|
||||
isinstance(other, ControlBoneParentArmature) and
|
||||
self.node.point == other.node.point and
|
||||
self.orientation == other.orientation and
|
||||
self.bones == other.bones and
|
||||
self.copy_scale == other.copy_scale and
|
||||
self.copy_rotation == other.copy_rotation
|
||||
)
|
||||
|
||||
def generate_bones(self):
|
||||
self.output_bone = self.node.make_bone(
|
||||
make_derived_name(self.node.name, 'mch', '_arm'), 1/4, rig=self.rig)
|
||||
|
||||
self.rig.generator.disable_auto_parent(self.output_bone)
|
||||
|
||||
if self.orientation:
|
||||
matrix = force_lazy(self.orientation).to_matrix().to_4x4()
|
||||
matrix.translation = self.node.point
|
||||
self.get_bone(self.output_bone).matrix = matrix
|
||||
|
||||
def parent_bones(self):
|
||||
self.targets = force_lazy(self.bones)
|
||||
|
||||
assert len(self.targets) > 0
|
||||
|
||||
# Single target can be simplified to parenting
|
||||
if len(self.targets) == 1:
|
||||
target = force_lazy(self.targets[0])
|
||||
if isinstance(target, tuple):
|
||||
target = target[0]
|
||||
|
||||
self.set_bone_parent(
|
||||
self.output_bone, target,
|
||||
inherit_scale='NONE' if self.copy_scale else 'FIX_SHEAR'
|
||||
)
|
||||
|
||||
def rig_bones(self):
|
||||
# Multiple targets use the Armature constraint
|
||||
if len(self.targets) > 1:
|
||||
self.make_constraint(
|
||||
self.output_bone, 'ARMATURE', targets=self.targets,
|
||||
use_deform_preserve_volume=True
|
||||
)
|
||||
|
||||
self.make_constraint(self.output_bone, 'LIMIT_ROTATION')
|
||||
|
||||
if self.copy_rotation:
|
||||
self.make_constraint(self.output_bone, 'COPY_ROTATION', self.copy_rotation)
|
||||
if self.copy_scale:
|
||||
self.make_constraint(self.output_bone, 'COPY_SCALE', self.copy_scale)
|
||||
|
||||
|
||||
class ControlBoneParentLayer(ControlBoneParentBase):
|
||||
"""Base class for parent generators that build on top of another mechanism."""
|
||||
|
||||
def __init__(self, rig, node, parent):
|
||||
super().__init__(rig, node)
|
||||
self.parent = parent
|
||||
|
||||
def enable_component(self):
|
||||
self.parent.enable_component()
|
||||
super().enable_component()
|
||||
|
||||
|
||||
class ControlBoneWeakParentLayer(ControlBoneParentLayer):
|
||||
"""
|
||||
Base class for layered parent generator that is only used for the reparent source.
|
||||
I.e. it doesn't affect the control for its owner rig, but only for other rigs
|
||||
that have controls merged into this one.
|
||||
"""
|
||||
|
||||
# Inherit mode used to parent the pseudo-control to the output of this generator.
|
||||
inherit_scale_mode = 'AVERAGE'
|
||||
|
||||
@staticmethod
|
||||
def strip(parent):
|
||||
while isinstance(parent, ControlBoneWeakParentLayer):
|
||||
parent = parent.parent
|
||||
|
||||
return parent
|
||||
|
||||
|
||||
class ControlBoneParentOffset(ControlBoneParentLayer):
|
||||
"""
|
||||
Parent mechanism generator that offsets the control's location.
|
||||
|
||||
Supports Copy Transforms (Local) constraints and location drivers.
|
||||
Multiple offsets can be accumulated in the same generator, which
|
||||
will automatically create as many bones as needed.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def wrap(cls, owner, parent, node, *constructor_args):
|
||||
return cls(owner, node, parent, *constructor_args)
|
||||
|
||||
def __init__(self, rig, node, parent):
|
||||
super().__init__(rig, node, parent)
|
||||
self.copy_local = {}
|
||||
self.add_local = {}
|
||||
self.add_orientations = {}
|
||||
self.limit_distance = []
|
||||
|
||||
def enable_component(self):
|
||||
# Automatically merge an unfrozen sequence of this generator instances
|
||||
while isinstance(self.parent, ControlBoneParentOffset) and not self.parent.is_parent_frozen:
|
||||
self.prepend_contents(self.parent)
|
||||
self.parent = self.parent.parent
|
||||
|
||||
super().enable_component()
|
||||
|
||||
def prepend_contents(self, other):
|
||||
"""Merge all offsets stored in the other generator into the current one."""
|
||||
for key, val in other.copy_local.items():
|
||||
if key not in self.copy_local:
|
||||
self.copy_local[key] = val
|
||||
else:
|
||||
inf, expr, cbs = val
|
||||
inf0, expr0, cbs0 = self.copy_local[key]
|
||||
self.copy_local[key] = [inf+inf0, expr+expr0, cbs+cbs0]
|
||||
|
||||
for key, val in other.add_orientations.items():
|
||||
if key not in self.add_orientations:
|
||||
self.add_orientations[key] = val
|
||||
|
||||
for key, val in other.add_local.items():
|
||||
if key not in self.add_local:
|
||||
self.add_local[key] = val
|
||||
else:
|
||||
ot0, ot1, ot2 = val
|
||||
my0, my1, my2 = self.add_local[key]
|
||||
self.add_local[key] = (ot0+my0, ot1+my1, ot2+my2)
|
||||
|
||||
self.limit_distance = other.limit_distance + self.limit_distance
|
||||
|
||||
def add_copy_local_location(self, target, *, influence=1, influence_expr=None, influence_vars={}):
|
||||
"""
|
||||
Add a Copy Location (Local, Owner Orientation) offset.
|
||||
The influence may be specified as a (lazy) constant, or a driver expression
|
||||
with variables (using the same $var syntax as add_location_driver).
|
||||
"""
|
||||
if target not in self.copy_local:
|
||||
self.copy_local[target] = [0, [], []]
|
||||
|
||||
if influence_expr:
|
||||
self.copy_local[target][1].append((influence_expr, influence_vars))
|
||||
elif callable(influence):
|
||||
self.copy_local[target][2].append(influence)
|
||||
else:
|
||||
self.copy_local[target][0] += influence
|
||||
|
||||
def add_location_driver(self, orientation, index, expression, variables):
|
||||
"""
|
||||
Add a driver offsetting along the specified axis in the given Quaternion orientation.
|
||||
The variables may have to be renamed due to conflicts between multiple add requests,
|
||||
so the expression should use the $var syntax of Template to reference them.
|
||||
"""
|
||||
assert isinstance(variables, dict)
|
||||
|
||||
key = tuple(round(x*10000) for x in orientation)
|
||||
|
||||
if key not in self.add_local:
|
||||
self.add_orientations[key] = orientation
|
||||
self.add_local[key] = ([], [], [])
|
||||
|
||||
self.add_local[key][index].append((expression, variables))
|
||||
|
||||
def add_limit_distance(self, target, *, ensure_order=False, **kwargs):
|
||||
"""Add a limit distance constraint with the given make_constraint arguments."""
|
||||
self.limit_distance.append((target, kwargs))
|
||||
|
||||
# Prevent merging from reordering this limit
|
||||
if ensure_order:
|
||||
self.is_parent_frozen = True
|
||||
|
||||
def __eq__(self, other):
|
||||
return (
|
||||
isinstance(other, ControlBoneParentOffset) and
|
||||
self.parent == other.parent and
|
||||
self.copy_local == other.copy_local and
|
||||
self.add_local == other.add_local and
|
||||
self.limit_distance == other.limit_distance
|
||||
)
|
||||
|
||||
@property
|
||||
def output_bone(self):
|
||||
return self.mch_bones[-1] if self.mch_bones else self.parent.output_bone
|
||||
|
||||
def generate_bones(self):
|
||||
self.mch_bones = []
|
||||
self.reuse_mch = False
|
||||
|
||||
if self.copy_local or self.add_local or self.limit_distance:
|
||||
mch_name = make_derived_name(self.node.name, 'mch', '_poffset')
|
||||
|
||||
if self.add_local:
|
||||
# Generate a bone for every distinct orientation used for the drivers
|
||||
for key in self.add_local:
|
||||
self.mch_bones.append(self.node.make_bone(
|
||||
mch_name, 1/4, rig=self.rig, orientation=self.add_orientations[key]))
|
||||
else:
|
||||
# Try piggybacking on the parent bone if allowed
|
||||
if not self.parent.is_parent_frozen:
|
||||
bone = self.get_bone(self.parent.output_bone)
|
||||
if (bone.head - self.node.point).length < 1e-5:
|
||||
self.reuse_mch = True
|
||||
self.mch_bones = [bone.name]
|
||||
return
|
||||
|
||||
self.mch_bones.append(self.node.make_bone(mch_name, 1/4, rig=self.rig))
|
||||
|
||||
def parent_bones(self):
|
||||
if self.mch_bones:
|
||||
if not self.reuse_mch:
|
||||
self.rig.set_bone_parent(self.mch_bones[0], self.parent.output_bone)
|
||||
|
||||
self.rig.parent_bone_chain(self.mch_bones, use_connect=False)
|
||||
|
||||
def compile_driver(self, items):
|
||||
variables = {}
|
||||
expressions = []
|
||||
|
||||
# Loop through all expressions and combine the variable maps.
|
||||
for expr, varset in items:
|
||||
template = Template(expr)
|
||||
varmap = {}
|
||||
|
||||
# Check that all variables are present
|
||||
try:
|
||||
template.substitute({k: '' for k in varset})
|
||||
except Exception as e:
|
||||
self.rig.raise_error('Invalid driver expression: {}\nError: {}', expr, e)
|
||||
|
||||
# Merge variables
|
||||
for name, desc in varset.items():
|
||||
# Check if the variable is used.
|
||||
try:
|
||||
template.substitute({k: '' for k in varset if k != name})
|
||||
continue
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# Descriptors may not be hashable, so linear search
|
||||
for vn, vdesc in variables.items():
|
||||
if vdesc == desc:
|
||||
varmap[name] = vn
|
||||
break
|
||||
else:
|
||||
# Find an unique name for the new variable and add to map
|
||||
new_name = name
|
||||
if new_name in variables:
|
||||
for i in count(1):
|
||||
new_name = '%s_%d' % (name, i)
|
||||
if new_name not in variables:
|
||||
break
|
||||
|
||||
variables[new_name] = desc
|
||||
varmap[name] = new_name
|
||||
|
||||
# Substitute the new names into the expression
|
||||
expressions.append(template.substitute(varmap))
|
||||
|
||||
# Add all expressions together
|
||||
if len(expressions) > 1:
|
||||
final_expr = '+'.join('('+expr+')' for expr in expressions)
|
||||
else:
|
||||
final_expr = expressions[0]
|
||||
|
||||
return final_expr, variables
|
||||
|
||||
def rig_bones(self):
|
||||
# Emit the Copy Location constraints
|
||||
if self.copy_local:
|
||||
mch = self.mch_bones[0]
|
||||
for target, (influence, drivers, lazyinf) in self.copy_local.items():
|
||||
influence += sum(map(force_lazy, lazyinf))
|
||||
|
||||
con = self.make_constraint(
|
||||
mch, 'COPY_LOCATION', target, use_offset=True,
|
||||
target_space='LOCAL_OWNER_ORIENT', owner_space='LOCAL', influence=influence,
|
||||
)
|
||||
|
||||
if drivers:
|
||||
if influence > 0:
|
||||
drivers.append((str(influence), {}))
|
||||
|
||||
expr, variables = self.compile_driver(drivers)
|
||||
self.make_driver(con, 'influence', expression=expr, variables=variables)
|
||||
|
||||
# Add the direct offset drivers
|
||||
if self.add_local:
|
||||
for mch, (key, specs) in zip(self.mch_bones, self.add_local.items()):
|
||||
for index, vals in enumerate(specs):
|
||||
if vals:
|
||||
expr, variables = self.compile_driver(vals)
|
||||
self.make_driver(mch, 'location', index=index,
|
||||
expression=expr, variables=variables)
|
||||
|
||||
# Add the limit distance constraints
|
||||
for target, kwargs in self.limit_distance:
|
||||
self.make_constraint(self.mch_bones[-1], 'LIMIT_DISTANCE', target, **kwargs)
|
|
@ -0,0 +1,241 @@
|
|||
# ====================== BEGIN GPL LICENSE BLOCK ======================
|
||||
#
|
||||
# This program 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 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program 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 this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ======================= END GPL LICENSE BLOCK ========================
|
||||
|
||||
# <pep8 compliant>
|
||||
|
||||
import bpy
|
||||
|
||||
from ...utils.naming import make_derived_name
|
||||
from ...utils.misc import force_lazy, LazyRef
|
||||
|
||||
from ...base_rig import BaseRig, stage
|
||||
|
||||
from .skin_parents import ControlBoneParentOrg
|
||||
|
||||
|
||||
class BaseSkinRig(BaseRig):
|
||||
"""
|
||||
Base type for all rigs involved in the skin system.
|
||||
This includes chain rigs and the parent provider rigs.
|
||||
"""
|
||||
|
||||
def initialize(self):
|
||||
self.rig_parent_bone = self.get_bone_parent(self.base_bone)
|
||||
|
||||
##########################
|
||||
# Utilities
|
||||
|
||||
def get_parent_skin_rig(self):
|
||||
"""Find the closest BaseSkinRig parent."""
|
||||
parent = self.rigify_parent
|
||||
|
||||
while parent:
|
||||
if isinstance(parent, BaseSkinRig):
|
||||
return parent
|
||||
parent = parent.rigify_parent
|
||||
|
||||
return None
|
||||
|
||||
def get_all_parent_skin_rigs(self):
|
||||
"""Get a list of all BaseSkinRig parents, starting with this rig."""
|
||||
items = []
|
||||
current = self
|
||||
while current:
|
||||
items.append(current)
|
||||
current = current.get_parent_skin_rig()
|
||||
return items
|
||||
|
||||
def get_child_chain_parent_next(self, rig):
|
||||
"""
|
||||
Retrieves the parent bone for the child chain rig
|
||||
as determined by the parent skin rig.
|
||||
"""
|
||||
if isinstance(self.rigify_parent, BaseSkinRig):
|
||||
return self.rigify_parent.get_child_chain_parent(rig, self.rig_parent_bone)
|
||||
else:
|
||||
return self.rig_parent_bone
|
||||
|
||||
def build_control_node_parent_next(self, node):
|
||||
"""
|
||||
Retrieves the parent mechanism generator for the child control node
|
||||
as determined by the parent skin rig.
|
||||
"""
|
||||
if isinstance(self.rigify_parent, BaseSkinRig):
|
||||
return self.rigify_parent.build_control_node_parent(node, self.rig_parent_bone)
|
||||
else:
|
||||
return ControlBoneParentOrg(self.rig_parent_bone)
|
||||
|
||||
##########################
|
||||
# Methods to override
|
||||
|
||||
def get_child_chain_parent(self, rig, parent_bone):
|
||||
"""
|
||||
Returns the (lazy) parent bone to use for the given child chain rig.
|
||||
The parent_bone argument specifies the actual parent bone from caller.
|
||||
"""
|
||||
return parent_bone
|
||||
|
||||
def build_control_node_parent(self, node, parent_bone):
|
||||
"""
|
||||
Returns the parent mechanism generator for the child control node.
|
||||
The parent_bone argument specifies the actual parent bone from caller.
|
||||
Called during the initialize stage.
|
||||
"""
|
||||
return ControlBoneParentOrg(self.get_child_chain_parent(node.rig, parent_bone))
|
||||
|
||||
def build_own_control_node_parent(self, node):
|
||||
"""
|
||||
Returns the parent mechanism generator for nodes directly owned by this rig.
|
||||
Called during the initialize stage.
|
||||
"""
|
||||
return self.build_control_node_parent_next(node)
|
||||
|
||||
def extend_control_node_parent(self, parent, node):
|
||||
"""
|
||||
First callback pass of adjustments to the parent mechanism generator for the given node.
|
||||
Called for all BaseSkinRig parents in parent to child order during the initialize stage.
|
||||
"""
|
||||
return parent
|
||||
|
||||
def extend_control_node_parent_post(self, parent, node):
|
||||
"""
|
||||
Second callback pass of adjustments to the parent mechanism generator for the given node.
|
||||
Called for all BaseSkinRig parents in child to parent order during the initialize stage.
|
||||
"""
|
||||
return parent
|
||||
|
||||
def extend_control_node_rig(self, node):
|
||||
"""
|
||||
A callback pass for adding constraints directly to the generated control.
|
||||
Called for all BaseSkinRig parents in parent to child order during the rig stage.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def get_bone_quaternion(obj, bone):
|
||||
return obj.pose.bones[bone].bone.matrix_local.to_quaternion()
|
||||
|
||||
|
||||
class BaseSkinChainRig(BaseSkinRig):
|
||||
"""
|
||||
Base type for all skin rigs that can own control nodes, rather than
|
||||
only modifying nodes of their children or other rigs.
|
||||
"""
|
||||
|
||||
chain_priority = 0
|
||||
|
||||
def initialize(self):
|
||||
super().initialize()
|
||||
|
||||
if type(self).chain_priority is None:
|
||||
self.chain_priority = self.params.skin_chain_priority
|
||||
|
||||
def parent_bones(self):
|
||||
self.rig_parent_bone = force_lazy(self.get_child_chain_parent_next(self))
|
||||
|
||||
def get_final_control_node_rotation(self, node):
|
||||
"""Returns the orientation to use for the given control node owned by this rig."""
|
||||
return self.get_control_node_rotation(node)
|
||||
|
||||
##########################
|
||||
# Methods to override
|
||||
|
||||
def get_control_node_rotation(self, node):
|
||||
"""
|
||||
Returns the rig-specific orientation to use for the given control node of this rig,
|
||||
if not overridden by the Orientation Bone option.
|
||||
"""
|
||||
return get_bone_quaternion(self.obj, self.base_bone)
|
||||
|
||||
def get_control_node_layers(self, node):
|
||||
"""Returns the armature layers to use for the given control node owned by this rig."""
|
||||
return self.get_bone(self.base_bone).bone.layers
|
||||
|
||||
def make_control_node_widget(self, node):
|
||||
"""Called to generate the widget for nodes with ControlNodeIcon.CUSTOM."""
|
||||
raise NotImplementedError()
|
||||
|
||||
##########################
|
||||
# UI
|
||||
|
||||
@classmethod
|
||||
def add_parameters(self, params):
|
||||
params.skin_chain_priority = bpy.props.IntProperty(
|
||||
name='Chain Priority',
|
||||
min=-10, max=10, default=0,
|
||||
description='When merging controls, chains with higher priority always win'
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def parameters_ui(self, layout, params):
|
||||
if self.chain_priority is None:
|
||||
layout.prop(params, "skin_chain_priority")
|
||||
|
||||
|
||||
class BaseSkinChainRigWithRotationOption(BaseSkinChainRig):
|
||||
"""
|
||||
Skin chain rig with an option to override the orientation to use
|
||||
for controls via specifying an arbitrary template bone.
|
||||
"""
|
||||
|
||||
use_skin_control_orientation_bone = True
|
||||
|
||||
def get_final_control_node_rotation(self, node):
|
||||
bone_name = self.params.skin_control_orientation_bone
|
||||
|
||||
if bone_name and self.use_skin_control_orientation_bone:
|
||||
# Retrieve the orientation from the specified ORG bone
|
||||
try:
|
||||
org_name = make_derived_name(bone_name, 'org')
|
||||
|
||||
if org_name not in self.obj.pose.bones:
|
||||
org_name = bone_name
|
||||
|
||||
return get_bone_quaternion(self.obj, org_name)
|
||||
|
||||
except KeyError:
|
||||
self.raise_error('Could not find orientation bone {}', bone_name)
|
||||
|
||||
else:
|
||||
# Use the rig-specific orientation
|
||||
return self.get_control_node_rotation(node)
|
||||
|
||||
@classmethod
|
||||
def add_parameters(self, params):
|
||||
params.skin_control_orientation_bone = bpy.props.StringProperty(
|
||||
name="Orientation Bone",
|
||||
description="If set, control orientation is taken from the specified bone",
|
||||
)
|
||||
|
||||
super().add_parameters(params)
|
||||
|
||||
@classmethod
|
||||
def parameters_ui(self, layout, params):
|
||||
if self.use_skin_control_orientation_bone:
|
||||
from rigify.operators.copy_mirror_parameters import make_copy_parameter_button
|
||||
|
||||
row = layout.row()
|
||||
row.prop_search(params, "skin_control_orientation_bone",
|
||||
bpy.context.active_object.pose, "bones", text="Orientation")
|
||||
|
||||
make_copy_parameter_button(
|
||||
row, "skin_control_orientation_bone", mirror_bone=True,
|
||||
base_class=BaseSkinChainRigWithRotationOption
|
||||
)
|
||||
|
||||
super().parameters_ui(layout, params)
|
|
@ -0,0 +1,422 @@
|
|||
# ====================== BEGIN GPL LICENSE BLOCK ======================
|
||||
#
|
||||
# This program 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 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program 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 this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ======================= END GPL LICENSE BLOCK ========================
|
||||
|
||||
# <pep8 compliant>
|
||||
|
||||
import bpy
|
||||
import enum
|
||||
|
||||
from itertools import count, repeat
|
||||
from mathutils import Vector, Matrix
|
||||
from bl_math import clamp
|
||||
|
||||
from ...utils.rig import connected_children_names
|
||||
from ...utils.layers import ControlLayersOption
|
||||
from ...utils.naming import make_derived_name
|
||||
from ...utils.bones import align_bone_orientation, align_bone_to_axis, align_bone_roll
|
||||
from ...utils.misc import map_list, LazyRef
|
||||
from ...utils.mechanism import driver_var_transform
|
||||
|
||||
from ...base_rig import stage
|
||||
|
||||
from .skin_nodes import ControlBoneNode, ControlNodeLayer, ControlNodeIcon
|
||||
from .skin_parents import ControlBoneWeakParentLayer, ControlBoneParentOffset
|
||||
|
||||
from .basic_chain import Rig as BasicChainRig
|
||||
|
||||
|
||||
class Control(enum.IntEnum):
|
||||
START = 0
|
||||
MIDDLE = 1
|
||||
END = 2
|
||||
|
||||
|
||||
class Rig(BasicChainRig):
|
||||
"""
|
||||
Skin chain that propagates motion of its end and middle controls, resulting in
|
||||
stretching the whole chain rather than just immediately connected chain segments.
|
||||
"""
|
||||
|
||||
min_chain_length = 2
|
||||
|
||||
def initialize(self):
|
||||
if len(self.bones.org) < self.min_chain_length:
|
||||
self.raise_error(
|
||||
"Input to rig type must be a chain of {} or more bones.", self.min_chain_length)
|
||||
|
||||
super().initialize()
|
||||
|
||||
orgs = self.bones.org
|
||||
|
||||
# Check the middle pivot location
|
||||
self.pivot_pos = self.params.skin_chain_pivot_pos
|
||||
|
||||
if not (0 <= self.pivot_pos < len(orgs)):
|
||||
self.raise_error('Invalid middle control position: {}', self.pivot_pos)
|
||||
|
||||
# Compute cumulative chain lengths from the start
|
||||
bone_lengths = [self.get_bone(org).length for org in orgs]
|
||||
|
||||
self.chain_lengths = [sum(bone_lengths[0:i]) for i in range(len(orgs)+1)]
|
||||
|
||||
# Compute the chain start to end direction vector
|
||||
if not self.params.skin_chain_falloff_length:
|
||||
self.pivot_base = self.get_bone(orgs[0]).head
|
||||
self.pivot_vector = self.get_bone(orgs[-1]).tail - self.pivot_base
|
||||
self.pivot_length = self.pivot_vector.length
|
||||
self.pivot_vector.normalize()
|
||||
|
||||
# Compute the position of the middle pivot within the chain
|
||||
if self.pivot_pos:
|
||||
pivot_point = self.get_bone(orgs[self.pivot_pos]).head
|
||||
self.middle_pivot_factor = self.get_pivot_projection(pivot_point, self.pivot_pos)
|
||||
|
||||
####################################################
|
||||
# UTILITIES
|
||||
|
||||
def get_pivot_projection(self, pos, index):
|
||||
"""Compute the interpolation factor within the chain for a control at pos and index."""
|
||||
if self.params.skin_chain_falloff_length:
|
||||
# Position along the length of the chain
|
||||
return self.chain_lengths[index] / self.chain_lengths[-1]
|
||||
else:
|
||||
# Position projected on the line connecting chain ends
|
||||
return clamp((pos - self.pivot_base).dot(self.pivot_vector) / self.pivot_length)
|
||||
|
||||
def use_falloff_curve(self, idx):
|
||||
"""Check if the given Control has any influence on other nodes."""
|
||||
return self.params.skin_chain_falloff[idx] > -10
|
||||
|
||||
def apply_falloff_curve(self, factor, idx):
|
||||
"""Compute the falloff weight at position factor for the given Control."""
|
||||
weight = self.params.skin_chain_falloff[idx]
|
||||
|
||||
if self.params.skin_chain_falloff_spherical[idx]:
|
||||
# circular falloff
|
||||
if weight >= 0:
|
||||
p = 2 ** weight
|
||||
return (1 - (1 - factor) ** p) ** (1/p)
|
||||
else:
|
||||
p = 2 ** -weight
|
||||
return 1 - (1 - factor ** p) ** (1/p)
|
||||
else:
|
||||
# parabolic falloff
|
||||
return 1 - (1 - factor) ** (2 ** weight)
|
||||
|
||||
####################################################
|
||||
# CONTROL NODES
|
||||
|
||||
def make_control_node(self, i, org, is_end):
|
||||
node = super().make_control_node(i, org, is_end)
|
||||
|
||||
# Chain end control nodes
|
||||
if i == 0 or i == self.num_orgs:
|
||||
node.layer = ControlNodeLayer.FREE
|
||||
node.icon = ControlNodeIcon.FREE
|
||||
if i == 0:
|
||||
node.node_needs_reparent = self.use_falloff_curve(Control.START)
|
||||
else:
|
||||
node.node_needs_reparent = self.use_falloff_curve(Control.END)
|
||||
# Middle pivot control node
|
||||
elif i == self.pivot_pos:
|
||||
node.layer = ControlNodeLayer.MIDDLE_PIVOT
|
||||
node.icon = ControlNodeIcon.MIDDLE_PIVOT
|
||||
node.node_needs_reparent = self.use_falloff_curve(Control.MIDDLE)
|
||||
# Other (tweak) control nodes
|
||||
else:
|
||||
node.layer = ControlNodeLayer.TWEAK
|
||||
node.icon = ControlNodeIcon.TWEAK
|
||||
|
||||
return node
|
||||
|
||||
def extend_control_node_parent(self, parent, node):
|
||||
if node.rig != self or node.index in (0, self.num_orgs):
|
||||
return parent
|
||||
|
||||
parent = ControlBoneParentOffset(self, node, parent)
|
||||
|
||||
# Add offsets from the end controls to other nodes
|
||||
factor = self.get_pivot_projection(node.point, node.index)
|
||||
|
||||
if self.use_falloff_curve(Control.START):
|
||||
parent.add_copy_local_location(
|
||||
LazyRef(self.control_nodes[0], 'reparent_bone'),
|
||||
influence=self.apply_falloff_curve(1 - factor, Control.START),
|
||||
)
|
||||
|
||||
if self.use_falloff_curve(Control.END):
|
||||
parent.add_copy_local_location(
|
||||
LazyRef(self.control_nodes[-1], 'reparent_bone'),
|
||||
influence=self.apply_falloff_curve(factor, Control.END),
|
||||
)
|
||||
|
||||
# Add offset from the middle pivot
|
||||
if self.pivot_pos and node.index != self.pivot_pos:
|
||||
if self.use_falloff_curve(Control.MIDDLE):
|
||||
if node.index < self.pivot_pos:
|
||||
factor = factor / self.middle_pivot_factor
|
||||
else:
|
||||
factor = (1 - factor) / (1 - self.middle_pivot_factor)
|
||||
|
||||
parent.add_copy_local_location(
|
||||
LazyRef(self.control_nodes[self.pivot_pos], 'reparent_bone'),
|
||||
influence=self.apply_falloff_curve(clamp(factor), Control.MIDDLE),
|
||||
)
|
||||
|
||||
# If Propagate To Controls is set, add an extra wrapper for twist/scale
|
||||
if node.index != self.pivot_pos and self.params.skin_chain_falloff_to_controls:
|
||||
if self.params.skin_chain_falloff_twist or self.params.skin_chain_falloff_scale:
|
||||
parent = ControlBoneChainPropagate(self, node, parent)
|
||||
|
||||
return parent
|
||||
|
||||
def get_control_node_layers(self, node):
|
||||
layers = None
|
||||
|
||||
# Secondary Layers used for the middle pivot
|
||||
if self.pivot_pos and node.index == self.pivot_pos:
|
||||
layers = ControlLayersOption.SKIN_SECONDARY.get(self.params)
|
||||
|
||||
# Primary Layers used for the end controls, and middle if secondary not set
|
||||
if not layers and node.index in (0, self.num_orgs, self.pivot_pos):
|
||||
layers = ControlLayersOption.SKIN_PRIMARY.get(self.params)
|
||||
|
||||
return layers or super().get_control_node_layers(node)
|
||||
|
||||
####################################################
|
||||
# B-Bone handle MCH
|
||||
|
||||
def rig_mch_handle_user(self, i, mch, prev_node, node, next_node, pre):
|
||||
super().rig_mch_handle_user(i, mch, prev_node, node, next_node, pre)
|
||||
|
||||
self.rig_propagate(mch, node)
|
||||
|
||||
def rig_propagate(self, mch, node):
|
||||
# Interpolate chain twist and/or scale between pivots
|
||||
if node.index not in (0, self.num_orgs, self.pivot_pos):
|
||||
index1, index2, factor = self.get_propagate_spec(node)
|
||||
|
||||
if self.params.skin_chain_falloff_twist:
|
||||
self.rig_propagate_twist(mch, index1, index2, factor)
|
||||
|
||||
if self.use_scale and self.params.skin_chain_falloff_scale:
|
||||
self.rig_propagate_scale(mch, index1, index2, factor)
|
||||
|
||||
def get_propagate_spec(self, node):
|
||||
"""Compute source handle indices and factor for propagating scale and twist to node."""
|
||||
index1 = 0
|
||||
index2 = self.num_orgs
|
||||
|
||||
len_cur = self.chain_lengths[node.index]
|
||||
len_end = self.chain_lengths[-1]
|
||||
|
||||
if self.pivot_pos:
|
||||
len_pivot = self.chain_lengths[self.pivot_pos]
|
||||
|
||||
if node.index < self.pivot_pos:
|
||||
factor = len_cur / len_pivot
|
||||
index2 = self.pivot_pos
|
||||
else:
|
||||
factor = (len_cur - len_pivot) / (len_end - len_pivot)
|
||||
index1 = self.pivot_pos
|
||||
else:
|
||||
factor = len_cur / len_end
|
||||
|
||||
return index1, index2, factor
|
||||
|
||||
def rig_propagate_twist(self, mch, index1, index2, factor):
|
||||
handles = self.get_all_mch_handles()
|
||||
handles_pre = self.get_all_mch_handles_pre()
|
||||
|
||||
# Get Y Twist rotation of the input handles
|
||||
variables = {
|
||||
'y1': driver_var_transform(
|
||||
self.obj, handles[index1], type='ROT_Y',
|
||||
space='LOCAL', rotation_mode='SWING_TWIST_Y'
|
||||
),
|
||||
'y2': driver_var_transform(
|
||||
self.obj, handles[index2], type='ROT_Y',
|
||||
space='LOCAL', rotation_mode='SWING_TWIST_Y'
|
||||
),
|
||||
}
|
||||
|
||||
# If pre handles are used, exclude the pre-handle twist,
|
||||
# since it is caused by mechanisms and not user animation.
|
||||
if handles_pre[index1] != handles[index1]:
|
||||
variables['p1'] = driver_var_transform(
|
||||
self.obj, handles_pre[index1], type='ROT_Y',
|
||||
space='LOCAL', rotation_mode='SWING_TWIST_Y'
|
||||
)
|
||||
expr1 = 'y1-p1'
|
||||
else:
|
||||
expr1 = 'y1'
|
||||
|
||||
if handles_pre[index2] != handles[index2]:
|
||||
variables['p2'] = driver_var_transform(
|
||||
self.obj, handles_pre[index2], type='ROT_Y',
|
||||
space='LOCAL', rotation_mode='SWING_TWIST_Y'
|
||||
)
|
||||
expr2 = 'y2-p2'
|
||||
else:
|
||||
expr2 = 'y2'
|
||||
|
||||
# Create the driver for Y Euler Rotation
|
||||
bone = self.get_bone(mch)
|
||||
bone.rotation_mode = 'YXZ'
|
||||
|
||||
self.make_driver(
|
||||
bone, 'rotation_euler', index=1,
|
||||
expression=f'lerp({expr1},{expr2},{clamp(factor)})',
|
||||
variables=variables
|
||||
)
|
||||
|
||||
def rig_propagate_scale(self, mch, index1, index2, factor, use_y=False):
|
||||
handles = self.get_all_mch_handles()
|
||||
|
||||
self.make_constraint(
|
||||
mch, 'COPY_SCALE', handles[index1], space='LOCAL',
|
||||
use_x=True, use_y=use_y, use_z=True,
|
||||
use_offset=True, power=clamp(1-factor)
|
||||
)
|
||||
self.make_constraint(
|
||||
mch, 'COPY_SCALE', handles[index2], space='LOCAL',
|
||||
use_x=True, use_y=use_y, use_z=True,
|
||||
use_offset=True, power=clamp(factor)
|
||||
)
|
||||
|
||||
####################################################
|
||||
# SETTINGS
|
||||
|
||||
@classmethod
|
||||
def add_parameters(self, params):
|
||||
params.skin_chain_pivot_pos = bpy.props.IntProperty(
|
||||
name='Middle Control Position',
|
||||
default=0,
|
||||
min=0,
|
||||
description='Position of the middle control, disabled if zero'
|
||||
)
|
||||
|
||||
params.skin_chain_falloff_spherical = bpy.props.BoolVectorProperty(
|
||||
size=3,
|
||||
name='Spherical Falloff',
|
||||
default=(False, False, False),
|
||||
description='Falloff curve tries to form a circle at +1 instead of a parabola',
|
||||
)
|
||||
|
||||
params.skin_chain_falloff = bpy.props.FloatVectorProperty(
|
||||
size=3,
|
||||
name='Control Falloff',
|
||||
default=(0.0, 1.0, 0.0),
|
||||
soft_min=-2, min=-10, soft_max=2,
|
||||
description='Falloff curve coefficient: 0 is linear, and higher value is wider influence. Set to -10 to disable influence completely',
|
||||
)
|
||||
|
||||
params.skin_chain_falloff_length = bpy.props.BoolProperty(
|
||||
name='Falloff Along Chain Curve',
|
||||
default=False,
|
||||
description='Falloff is computed along the curve of the chain, instead of projecting on the axis connecting the start and end points',
|
||||
)
|
||||
|
||||
params.skin_chain_falloff_twist = bpy.props.BoolProperty(
|
||||
name='Propagate Twist',
|
||||
default=True,
|
||||
description='Propagate twist from main controls throughout the chain',
|
||||
)
|
||||
|
||||
params.skin_chain_falloff_scale = bpy.props.BoolProperty(
|
||||
name='Propagate Scale',
|
||||
default=False,
|
||||
description='Propagate scale from main controls throughout the chain',
|
||||
)
|
||||
|
||||
params.skin_chain_falloff_to_controls = bpy.props.BoolProperty(
|
||||
name='Propagate To Controls',
|
||||
default=False,
|
||||
description='Expose scale and/or twist propagated to tweak controls to be seen as ' +
|
||||
'parent motion by glue or other chains using Merge Parent Rotation And ' +
|
||||
'Scale. Otherwise it is only propagated internally within this chain',
|
||||
)
|
||||
|
||||
ControlLayersOption.SKIN_PRIMARY.add_parameters(params)
|
||||
ControlLayersOption.SKIN_SECONDARY.add_parameters(params)
|
||||
|
||||
super().add_parameters(params)
|
||||
|
||||
@classmethod
|
||||
def parameters_ui(self, layout, params):
|
||||
layout.prop(params, "skin_chain_pivot_pos")
|
||||
|
||||
col = layout.column(align=True)
|
||||
|
||||
row = col.row(align=True)
|
||||
row.label(text="Falloff:")
|
||||
|
||||
for i in range(3):
|
||||
row2 = row.row(align=True)
|
||||
row2.active = i != 1 or params.skin_chain_pivot_pos > 0
|
||||
row2.prop(params, "skin_chain_falloff", text="", index=i)
|
||||
row2.prop(params, "skin_chain_falloff_spherical", text="", icon='SPHERECURVE', index=i)
|
||||
|
||||
col.prop(params, "skin_chain_falloff_length")
|
||||
|
||||
row = col.split(factor=0.25)
|
||||
row.label(text="Propagate:")
|
||||
row = row.row(align=True)
|
||||
row.prop(params, "skin_chain_falloff_twist", text="Twist", toggle=True)
|
||||
row.prop(params, "skin_chain_falloff_scale", text="Scale", toggle=True)
|
||||
row.prop(params, "skin_chain_falloff_to_controls", text="To Controls", toggle=True)
|
||||
|
||||
ControlLayersOption.SKIN_PRIMARY.parameters_ui(layout, params)
|
||||
|
||||
if params.skin_chain_pivot_pos > 0:
|
||||
ControlLayersOption.SKIN_SECONDARY.parameters_ui(layout, params)
|
||||
|
||||
super().parameters_ui(layout, params)
|
||||
|
||||
|
||||
class ControlBoneChainPropagate(ControlBoneWeakParentLayer):
|
||||
"""
|
||||
Parent mechanism generator that propagates chain twist/scale
|
||||
to the reparent system, if Propagate To Controls is used.
|
||||
"""
|
||||
inherit_scale_mode = 'FULL'
|
||||
|
||||
def __eq__(self, other):
|
||||
return (
|
||||
isinstance(other, ControlBoneChainPropagate) and
|
||||
self.parent == other.parent and
|
||||
self.rig == other.rig and
|
||||
self.node.index == other.node.index
|
||||
)
|
||||
|
||||
def generate_bones(self):
|
||||
# The parent bone is based on the handle and aligned appropriately.
|
||||
handle = self.rig.bones.mch.handles[self.node.index]
|
||||
self.output_bone = self.copy_bone(handle, make_derived_name(handle, 'mch', '_parent'))
|
||||
|
||||
def parent_bones(self):
|
||||
self.set_bone_parent(self.output_bone, self.parent.output_bone, inherit_scale='AVERAGE')
|
||||
|
||||
def rig_bones(self):
|
||||
# Add the twist/scale propagation rigging to the bone like the handle.
|
||||
self.rig.rig_propagate(self.output_bone, self.node)
|
||||
|
||||
|
||||
def create_sample(obj):
|
||||
from rigify.rigs.basic.copy_chain import create_sample as inner
|
||||
obj.pose.bones[inner(obj)["bone.01"]].rigify_type = 'skin.stretchy_chain'
|
|
@ -0,0 +1,148 @@
|
|||
# ====================== BEGIN GPL LICENSE BLOCK ======================
|
||||
#
|
||||
# This program 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 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program 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 this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ======================= END GPL LICENSE BLOCK ========================
|
||||
|
||||
# <pep8 compliant>
|
||||
|
||||
import bpy
|
||||
import math
|
||||
|
||||
from itertools import count, repeat
|
||||
from mathutils import Vector, Matrix
|
||||
|
||||
from ....utils.naming import make_derived_name
|
||||
from ....utils.widgets_basic import create_cube_widget
|
||||
from ....utils.misc import LazyRef
|
||||
|
||||
from ....base_rig import stage
|
||||
|
||||
from ..skin_parents import ControlBoneParentArmature
|
||||
from ..skin_rigs import BaseSkinRig
|
||||
|
||||
|
||||
class Rig(BaseSkinRig):
|
||||
"""
|
||||
This rig transforms its child nodes' locations, but keeps
|
||||
their rotation and scale stable. This demonstrates implementing
|
||||
a basic parent controller rig.
|
||||
"""
|
||||
|
||||
def find_org_bones(self, bone):
|
||||
return bone.name
|
||||
|
||||
def initialize(self):
|
||||
super().initialize()
|
||||
|
||||
self.make_control = self.params.make_control
|
||||
|
||||
# Choose the parent bone for the child nodes
|
||||
if self.make_control:
|
||||
self.input_ref = LazyRef(self.bones.ctrl, 'master')
|
||||
else:
|
||||
self.input_ref = self.base_bone
|
||||
|
||||
# Retrieve the orientation of the control
|
||||
matrix = self.get_bone(self.base_bone).bone.matrix_local
|
||||
|
||||
self.transform_orientation = matrix.to_quaternion()
|
||||
|
||||
####################################################
|
||||
# Control Nodes
|
||||
|
||||
def build_control_node_parent(self, node, parent_bone):
|
||||
# Parent nodes to the control bone, but isolate rotation and scale
|
||||
return ControlBoneParentArmature(
|
||||
self, node, bones=[self.input_ref],
|
||||
orientation=self.transform_orientation,
|
||||
copy_scale=LazyRef(self.bones.mch, 'template'),
|
||||
copy_rotation=LazyRef(self.bones.mch, 'template'),
|
||||
)
|
||||
|
||||
def get_child_chain_parent(self, rig, parent_bone):
|
||||
# Forward child chain parenting to the next rig, so that
|
||||
# only control nodes are affected by this one.
|
||||
return self.get_child_chain_parent_next(rig)
|
||||
|
||||
####################################################
|
||||
# BONES
|
||||
#
|
||||
# ctrl:
|
||||
# master
|
||||
# Master control
|
||||
# mch:
|
||||
# template
|
||||
# Bone used to lock rotation and scale of child nodes.
|
||||
#
|
||||
####################################################
|
||||
|
||||
####################################################
|
||||
# Master control
|
||||
|
||||
@stage.generate_bones
|
||||
def make_master_control(self):
|
||||
if self.make_control:
|
||||
self.bones.ctrl.master = self.copy_bone(
|
||||
self.bones.org, make_derived_name(self.bones.org, 'ctrl'), parent=True)
|
||||
|
||||
@stage.configure_bones
|
||||
def configure_master_control(self):
|
||||
if self.make_control:
|
||||
self.copy_bone_properties(self.bones.org, self.bones.ctrl.master)
|
||||
|
||||
@stage.generate_widgets
|
||||
def make_master_control_widget(self):
|
||||
if self.make_control:
|
||||
create_cube_widget(self.obj, self.bones.ctrl.master)
|
||||
|
||||
####################################################
|
||||
# Template MCH
|
||||
|
||||
@stage.generate_bones
|
||||
def make_mch_template_bone(self):
|
||||
self.bones.mch.template = self.copy_bone(
|
||||
self.bones.org, make_derived_name(self.bones.org, 'mch', '_orient'), parent=True)
|
||||
|
||||
@stage.parent_bones
|
||||
def parent_mch_template_bone(self):
|
||||
self.set_bone_parent(self.bones.mch.template, self.get_child_chain_parent_next(self))
|
||||
|
||||
####################################################
|
||||
# ORG bone
|
||||
|
||||
@stage.rig_bones
|
||||
def rig_org_bone(self):
|
||||
pass
|
||||
|
||||
####################################################
|
||||
# SETTINGS
|
||||
|
||||
@classmethod
|
||||
def add_parameters(self, params):
|
||||
params.make_control = bpy.props.BoolProperty(
|
||||
name="Control",
|
||||
default=True,
|
||||
description="Create a control bone for the copy"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def parameters_ui(self, layout, params):
|
||||
layout.prop(params, "make_control", text="Generate Control")
|
||||
|
||||
|
||||
def create_sample(obj):
|
||||
from rigify.rigs.basic.super_copy import create_sample as inner
|
||||
obj.pose.bones[inner(obj)["Bone"]].rigify_type = 'skin.transform.basic'
|
|
@ -928,7 +928,7 @@ class VIEW3D_MT_rigify(bpy.types.Menu):
|
|||
layout = self.layout
|
||||
|
||||
layout.operator(Generate.bl_idname, text="Generate")
|
||||
|
||||
|
||||
if context.mode == 'EDIT_ARMATURE':
|
||||
layout.separator()
|
||||
layout.operator(Sample.bl_idname)
|
||||
|
|
|
@ -160,3 +160,16 @@ ControlLayersOption.TWEAK = ControlLayersOption('tweak', description="Layers for
|
|||
# Layer parameters used by the super_face rig.
|
||||
ControlLayersOption.FACE_PRIMARY = ControlLayersOption('primary', description="Layers for the primary controls to be on")
|
||||
ControlLayersOption.FACE_SECONDARY = ControlLayersOption('secondary', description="Layers for the secondary controls to be on")
|
||||
|
||||
# Layer parameters used by the skin rigs
|
||||
ControlLayersOption.SKIN_PRIMARY = ControlLayersOption(
|
||||
'skin_primary', toggle_default=False,
|
||||
toggle_name="Primary Control Layers",
|
||||
description="Layers for the primary controls to be on",
|
||||
)
|
||||
|
||||
ControlLayersOption.SKIN_SECONDARY = ControlLayersOption(
|
||||
'skin_secondary', toggle_default=False,
|
||||
toggle_name="Secondary Control Layers",
|
||||
description="Layers for the secondary controls to be on",
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue