Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPeter Kim <pk15950@gmail.com>2021-08-13 01:05:15 +0300
committerPeter Kim <pk15950@gmail.com>2021-08-13 01:05:15 +0300
commitece0feb7df7cb06fcb82514300f5f8d72f082654 (patch)
treec1c7de094004eb7dde9270bb067b841c7a86a1be
parent26fb63e5636836de6c4df26edab9e39dcd2d46b6 (diff)
parent31abe549839bdb30ce0926142bcbf8a3d9236067 (diff)
Merge branch 'master' into xr-controller-support
-rw-r--r--archimesh/achm_venetian_maker.py2
-rw-r--r--blenderkit/__init__.py18
-rw-r--r--blenderkit/download.py32
-rw-r--r--blenderkit/image_utils.py82
-rw-r--r--blenderkit/paths.py1
-rw-r--r--blenderkit/search.py12
-rw-r--r--blenderkit/ui_panels.py196
-rw-r--r--blenderkit/upload.py48
-rw-r--r--blenderkit/utils.py7
-rw-r--r--greasepencil_tools/box_deform.py4
-rw-r--r--greasepencil_tools/prefs.py2
-rw-r--r--greasepencil_tools/rotate_canvas.py14
-rw-r--r--greasepencil_tools/timeline_scrub.py10
-rwxr-xr-xio_scene_gltf2/__init__.py2
-rw-r--r--mesh_tools/mesh_edges_floor_plan.py2
-rw-r--r--node_wrangler.py20
-rw-r--r--object_print3d_utils/__init__.py2
-rw-r--r--pose_library/functions.py25
-rw-r--r--pose_library/gui.py14
-rw-r--r--pose_library/operators.py21
-rw-r--r--pose_library/pose_creation.py4
-rw-r--r--real_snow.py76
-rw-r--r--rigify/rigs/face/basic_tongue.py206
-rw-r--r--rigify/rigs/face/skin_eye.py825
-rw-r--r--rigify/rigs/face/skin_jaw.py862
-rw-r--r--rigify/rigs/skin/anchor.py142
-rw-r--r--rigify/rigs/skin/basic_chain.py520
-rw-r--r--rigify/rigs/skin/glue.py321
-rw-r--r--rigify/rigs/skin/skin_nodes.py520
-rw-r--r--rigify/rigs/skin/skin_parents.py395
-rw-r--r--rigify/rigs/skin/skin_rigs.py241
-rw-r--r--rigify/rigs/skin/stretchy_chain.py422
-rw-r--r--rigify/rigs/skin/transform/basic.py148
-rw-r--r--rigify/ui.py2
-rw-r--r--rigify/utils/layers.py13
35 files changed, 4968 insertions, 243 deletions
diff --git a/archimesh/achm_venetian_maker.py b/archimesh/achm_venetian_maker.py
index 4f7a35b2..da90ab55 100644
--- a/archimesh/achm_venetian_maker.py
+++ b/archimesh/achm_venetian_maker.py
@@ -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
diff --git a/blenderkit/__init__.py b/blenderkit/__init__.py
index 547060ac..d22f383b 100644
--- a/blenderkit/__init__.py
+++ b/blenderkit/__init__.py
@@ -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(
diff --git a/blenderkit/download.py b/blenderkit/download.py
index d0b6cc98..2d5343b3 100644
--- a/blenderkit/download.py
+++ b/blenderkit/download.py
@@ -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):
diff --git a/blenderkit/image_utils.py b/blenderkit/image_utils.py
index 00c61917..4c09b06a 100644
--- a/blenderkit/image_utils.py
+++ b/blenderkit/image_utils.py
@@ -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 \ No newline at end of file
+ ims.color_depth = orig_depth
diff --git a/blenderkit/paths.py b/blenderkit/paths.py
index e074d966..08a4e492 100644
--- a/blenderkit/paths.py
+++ b/blenderkit/paths.py
@@ -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']}"
diff --git a/blenderkit/search.py b/blenderkit/search.py
index ac03080c..c386f588 100644
--- a/blenderkit/search.py
+++ b/blenderkit/search.py
@@ -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",
diff --git a/blenderkit/ui_panels.py b/blenderkit/ui_panels.py
index 67f42148..e2b8d700 100644
--- a/blenderkit/ui_panels.py
+++ b/blenderkit/ui_panels.py
@@ -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,
diff --git a/blenderkit/upload.py b/blenderkit/upload.py
index c913c714..bf207293 100644
--- a/blenderkit/upload.py
+++ b/blenderkit/upload.py
@@ -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:
diff --git a/blenderkit/utils.py b/blenderkit/utils.py
index afaf747e..096688be 100644
--- a/blenderkit/utils.py
+++ b/blenderkit/utils.py
@@ -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():
'''
diff --git a/greasepencil_tools/box_deform.py b/greasepencil_tools/box_deform.py
index a9a47ce2..6fa866ec 100644
--- a/greasepencil_tools/box_deform.py
+++ b/greasepencil_tools/box_deform.py
@@ -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
diff --git a/greasepencil_tools/prefs.py b/greasepencil_tools/prefs.py
index 929197d5..11d3d0be 100644
--- a/greasepencil_tools/prefs.py
+++ b/greasepencil_tools/prefs.py
@@ -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",
diff --git a/greasepencil_tools/rotate_canvas.py b/greasepencil_tools/rotate_canvas.py
index 0b46299d..36da5ee8 100644
--- a/greasepencil_tools/rotate_canvas.py
+++ b/greasepencil_tools/rotate_canvas.py
@@ -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)
diff --git a/greasepencil_tools/timeline_scrub.py b/greasepencil_tools/timeline_scrub.py
index 75e2cef4..2a745f5f 100644
--- a/greasepencil_tools/timeline_scrub.py
+++ b/greasepencil_tools/timeline_scrub.py
@@ -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)'
diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py
index 92126ac9..ccb4517d 100755
--- a/io_scene_gltf2/__init__.py
+++ b/io_scene_gltf2/__init__.py
@@ -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,
diff --git a/mesh_tools/mesh_edges_floor_plan.py b/mesh_tools/mesh_edges_floor_plan.py
index a654a4c3..ae4aaa65 100644
--- a/mesh_tools/mesh_edges_floor_plan.py
+++ b/mesh_tools/mesh_edges_floor_plan.py
@@ -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
diff --git a/node_wrangler.py b/node_wrangler.py
index f9008fbb..09d9b06f 100644
--- a/node_wrangler.py
+++ b/node_wrangler.py
@@ -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
diff --git a/object_print3d_utils/__init__.py b/object_print3d_utils/__init__.py
index 01f8a1fa..80ddb64e 100644
--- a/object_print3d_utils/__init__.py
+++ b/object_print3d_utils/__init__.py
@@ -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(
diff --git a/pose_library/functions.py b/pose_library/functions.py
index bb32e669..d2e210a7 100644
--- a/pose_library/functions.py
+++ b/pose_library/functions.py
@@ -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]:
diff --git a/pose_library/gui.py b/pose_library/gui.py
index 5ac6a934..a2f04a22 100644
--- a/pose_library/gui.py
+++ b/pose_library/gui.py
@@ -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
diff --git a/pose_library/operators.py b/pose_library/operators.py
index 959c9f1a..c0c8b332 100644
--- a/pose_library/operators.py
+++ b/pose_library/operators.py
@@ -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:
diff --git a/pose_library/pose_creation.py b/pose_library/pose_creation.py
index 79efcae4..ac08b776 100644
--- a/pose_library/pose_creation.py
+++ b/pose_library/pose_creation.py
@@ -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
diff --git a/real_snow.py b/real_snow.py
index f1091b2d..8f1b9ffd 100644
--- a/real_snow.py
+++ b/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)
diff --git a/rigify/rigs/face/basic_tongue.py b/rigify/rigs/face/basic_tongue.py
new file mode 100644
index 00000000..380e14df
--- /dev/null
+++ b/rigify/rigs/face/basic_tongue.py
@@ -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
diff --git a/rigify/rigs/face/skin_eye.py b/rigify/rigs/face/skin_eye.py
new file mode 100644
index 00000000..498a90c4
--- /dev/null
+++ b/rigify/rigs/face/skin_eye.py
@@ -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
diff --git a/rigify/rigs/face/skin_jaw.py b/rigify/rigs/face/skin_jaw.py
new file mode 100644
index 00000000..6829818c
--- /dev/null
+++ b/rigify/rigs/face/skin_jaw.py
@@ -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
diff --git a/rigify/rigs/skin/anchor.py b/rigify/rigs/skin/anchor.py
new file mode 100644
index 00000000..0392761f
--- /dev/null
+++ b/rigify/rigs/skin/anchor.py
@@ -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'
diff --git a/rigify/rigs/skin/basic_chain.py b/rigify/rigs/skin/basic_chain.py
new file mode 100644
index 00000000..b2cac8a6
--- /dev/null
+++ b/rigify/rigs/skin/basic_chain.py
@@ -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'
diff --git a/rigify/rigs/skin/glue.py b/rigify/rigs/skin/glue.py
new file mode 100644
index 00000000..2fffc885
--- /dev/null
+++ b/rigify/rigs/skin/glue.py
@@ -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'
diff --git a/rigify/rigs/skin/skin_nodes.py b/rigify/rigs/skin/skin_nodes.py
new file mode 100644
index 00000000..2fd04f9d
--- /dev/null
+++ b/rigify/rigs/skin/skin_nodes.py
@@ -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]
diff --git a/rigify/rigs/skin/skin_parents.py b/rigify/rigs/skin/skin_parents.py
new file mode 100644
index 00000000..0cfaec36
--- /dev/null
+++ b/rigify/rigs/skin/skin_parents.py
@@ -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)
diff --git a/rigify/rigs/skin/skin_rigs.py b/rigify/rigs/skin/skin_rigs.py
new file mode 100644
index 00000000..a4bc361e
--- /dev/null
+++ b/rigify/rigs/skin/skin_rigs.py
@@ -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)
diff --git a/rigify/rigs/skin/stretchy_chain.py b/rigify/rigs/skin/stretchy_chain.py
new file mode 100644
index 00000000..ac3d7784
--- /dev/null
+++ b/rigify/rigs/skin/stretchy_chain.py
@@ -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'
diff --git a/rigify/rigs/skin/transform/basic.py b/rigify/rigs/skin/transform/basic.py
new file mode 100644
index 00000000..2069615a
--- /dev/null
+++ b/rigify/rigs/skin/transform/basic.py
@@ -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'
diff --git a/rigify/ui.py b/rigify/ui.py
index 6b1e6e4c..a268a196 100644
--- a/rigify/ui.py
+++ b/rigify/ui.py
@@ -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)
diff --git a/rigify/utils/layers.py b/rigify/utils/layers.py
index 1f65863d..bc5a8c56 100644
--- a/rigify/utils/layers.py
+++ b/rigify/utils/layers.py
@@ -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",
+)