2025-12-01

This commit is contained in:
2026-03-17 14:58:51 -06:00
parent 183e865f8b
commit 4b82b57113
6846 changed files with 954887 additions and 162606 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,12 @@
schema_version = "1.0.0"
id = "amzncharactertools"
version = "0.6.3"
name = "AMZNCharacterTools"
tagline = "AMZNCharacterTools"
maintainer = "Nathan Lindsay"
type = "add-on"
tags = ["3D View"]
blender_version_min = "4.5.0"
license = [
"SPDX:GPL-2.0-or-later",
]
+11 -66
View File
@@ -1,70 +1,15 @@
[3.73.41]
[3.76.15]
New/improved:
Compatibility: Blender 4.3: FBX export
Export: New "Actions Linker" menu, to link an action to another one. Useful if exporting RigA animation, that depends on RigB through constraints (such as two characters holding each other's hands).\nIf animations are interdependent, it is necessary to declare these links for correct animation export.
Export: New "One File per Actions List" setting, when enabling both "As Multiple Files" and "Actions Manager". This allows to export one file per Action List, including all actions in the list.
Export: Godot: New setting to comply with Godot Root Motion axes
Export Godot: Removed the "Root Motion" setting that bakes the c_traj animation to armature object, not relevant for Godot Root Motion.
Export: Actions Lists: Filtered actions in the actions selector, so that actions that are already part of the current list are removed from the search list
Export: GLTF: Exposed the "Start from frame 0" setting, in order to force the first frame of each action to be zero if enabled
Export: UE: Removed the extra orientation tweaks applied to the spine bones when exporting with Mannequin axes. It was offsetting the angles by a few degrees to improve the match in case of straight spine bones, but can be misleading. It is best to align the spine bones manually. It can still be enabled in the "Legacy" menu though.
Export: Actions Manager: New "Batch" feature to add multiple actions at once to a list, with filtering
Export: UI: Added the "Rename Bones from File" checkbox in the properties panel too. Was confusing to have it only in the menu located in file export browser
Export: Avoid operator when removing shape keys for faster performances
Export: FBX: New "Force Shape Keys Keying" setting. If disabled (default), non-driven or non-animated shape keys are now exported as static, without animation data
Rig: Spline IK: Changed "Parent External Bone to" setting, was limited to the tip bone before. Now applies to all bones of the chain, and to Deforming or Twist bones instead of controllers. Can still be parented to controllers if necessary, by directly parenting Spline reference bones to them.
Rig: Spline IK: New "Add Tail Bone" option. This adds an extra bone at the tip of the chain, that is not part of the main IK chain. Useful when needing to parent other limb to the tip, that must not be stretched or rotated with the main chain, such as rigging a spine for quadrupeds.
Rig: Spline IK: New "Preserve Shape" option, to preserve the current shape when changing the amount of the spline bones.
Rig: Spine: New "Preserve Shape" option, to preserve the current spine shape when changing the amount of bones
Rig: Leg: New Limb Option to skip the foot IK offset alignment when Match to Rig, allowing custom positioning
Rig: Spine: New "Parent Fallback" parameter in Limb Options, to set the default spine parent in case the root reference bone is not parented. By default parented to "c_traj". No parent if set to None.
Rig: Tail: New "Preserve Shape" option, to preserve the current shape when changing the amount of the tail bones
Rig: Spine, Tail, Spline IK: Removed "Update Transform" option, was confusing. Now, automatically update the bones transforms if the count was changed, using grid align or preserve shape if enabled.
Rig: New Bulge setting for arm Joint Fans, allowing to add extra bulge deformation when the fans are spreading/folding. The created constraint can be manually edited afterwards.
Rig: Apply Pose as Rest Pose: New "Apply Deformed Shape Keys" toggle, can be useful to speed up baking time in case deformations are not affecting shape keys
Rig: New Bulge setting for leg Joints Fans too
Rig: New Ankle Joints Fans
Rig: Leg: New "Foot Roll Break" option. If enabled, the foot will start rotating from the ball, then from the tip toes, when moving the foot roll cursor. Settings can be found in "Rig Main Properties".
Skin: Warning message in case of negative mesh scale when binding
Picker: The picker camera is now set automatically when clicking "Add Picker"
Picker: Support reverse spine bones
Rig Tools: New "Extract Root Motion" function that tracks and bakes the "c_traj" controller to the pelvis position, while maintaining it on the floor, and preserving the rig animation
Rig Tools: Extract Root Motion now supports Location Z with initial offset applied
Rig Tools: Improved Extract Root Motion performances, around 4x faster (before: 24 sec for 400 frames, after: 6 sec)
Rig Tools: Extract Root Motion: Added X and Y offsets
Remap: Added "Extract Root Motion" as an option when retargetting
Smart: Facial features are now togglable: Eyebrows, Eyes, Ears, Nose, Lips, Tongue, Teeth, Chin
Smart: Auto filter mesh objects when clicking "Get Selected Objects". This avoids throwing an error if non-mesh objects are selected.
Smart: UI: Moved the "Fingers" checkbox in the header of the Fingers tab to minimize redundancy
Version: Now supports the Blender 5 API, while maintaining backward-compatibility with older Blender versions. Rig, Remap, Export and Picker modules updated
Export: New "Reset Transforms" setting to reset controllers transforms between each actions when baking. Useful if some bones are not keyframes, avoiding persistent offsets shared across actions.
Rig: Eyebrows: Support of arbitrary amount of eyebrow bones
Rig: Eyebrows: Preserve shape when changing amount
Rig: Tongue: Support arbitrary amount of tongue bones
Remap: New "Bake Only Existing Keyframes" setting
Fixed:
Rig: The horse rig was containing unwanted actions data leftovers
Rig: Removed the "body_mid" collections coming from old armature presets
Rig: Facial: Error when disabling the mouth bones
Rig: Bottom bones had accidentally their Deform property turned off
Rig: Apply Pose as Rest Pose crashing in case of numerous shape keys
Rig: The Tail limb could have incorrect parent. No uses the default parent_retarget function
Rig: The Dog preset had a "Connected" second spine bone, leading ot issue
Rig: Thigh Joint Fans incorrectly parented to 3 Bones Leg Type 2
Rig: Custom shapes of reversed spine controllers could be offset
Rig: Kilt: When changing the amount of bones, the Z axis could be incorrectly oriented. Now calculates the curve normal vectors to align Z axes on.
Rig: Changed the arm, leg Stretch-Length minimum value to 0.1
Rig: Fixed the new Ankle Joint Fans bulge, was not evaluating the foot angle correctly, new helper foot bone added
Rig: Apply Pose as Rest Pose: Arm and leg stretch controller was not supported
Rig: Error when setting up secondary bones color group in pre-Blender 4 versions
Rig: Disabled "Extrapolate" from the c_foot_heel bone and cleared out of range mapping to avoid accidental rotation flip
Skin: Error when restoring current vertices weights if a temp object was deleted (Apply Shape Keys)
Export: Skinned objects having same name as export skeleton were clashing. It now throws a warning before exporting.
Export: Add batch actions to Action Manager could generate an error if an action was removed from file
Export: GLTF: Disabled the "export_apply" argument from the actual export, was preventing shape keys to export. Modifiers are pre-applied by AutoRigPro functions anyway, so no changes on the user side.
Export: Error when exporting actions list containing removed actions
Export: The exported skeleton was incorrectly named with an "_arpbob" suffix when the rig export name was the same as the armature name
Export: Apply Shape Keys was failing if the basis shape key was not named "Basis". Now, always refer to the shape key at index 0 as Basis
Export: Humanoid: Invalid fingers export when exporting more than 2 arms
Export: Shape keys could not apply and export correctly under certain circumstances, was missing "from_mix" set to false
Export: GLTF: Apply Modifiers was failing
Export: Displace modifiers were not scaled when exporting with Units x100
Rig Tools: Extract Root Motion quat_prev error
Smart: The arm/leg reference bones roll was not calculated. Was expecting the Match to Rig function to do it, but now that "Auto IK Roll" can be disabled, the roll must be aligned
Remap: Added further checks to evaluate if the source and target armature are really Armature objects, to avoid user error
QuickRig Compatibility: "Auto IK Roll" not applied in limb options
Rig: Lips corners mini not removed when disable the whole head
Export: Kilt: Master controller was not exported anymore
Unknown compatibility breakage: flush() function not recognized, likely due to a custom python environment
@@ -15,15 +15,13 @@
# 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 LICENCE BLOCK *****
import os, shutil, bpy
# ***** END GPL LICENCE BLOCK *****
bl_info = {
"name": "Auto-Rig Pro",
"author": "Artell",
"version": (3, 73, 41),
"version": (3, 76, 15),
"blender": (4, 2, 0),
"location": "3D View > Properties> Auto-Rig Pro",
"description": "Automatic rig generation based on reference bones and various tools",
@@ -33,7 +31,7 @@ bl_info = {
}
import bpy
import bpy, shutil
from bpy.app.handlers import persistent
from .src import auto_rig_prefs
from .src import rig_functions
@@ -49,14 +47,22 @@ from .src import utils
# gltf export specials
if bpy.app.version >= (4, 4, 0):
from .src.lib.animation import get_action_slot_idx
class glTF2ExportUserExtension:
export_action_only = ''
def __init__(self):
self.action = None
self.base_action = None
self.base_action_slot_idx = 0
def gather_actions_hook(self, blender_object, params, export_settings):
def gather_actions_hook(self, blender_object, params, export_settings):
# This hook collects exportable baked actions
# Filter actions
# Only filter ARP rigs
if not 'arp_rig_name' in blender_object:
@@ -65,6 +71,7 @@ class glTF2ExportUserExtension:
# convert string list with fancy separators to list
export_actions_names = []
sep = '|%%|'
print('export_action_only', self.export_action_only)
if sep in self.export_action_only:
for actname in self.export_action_only.split(sep):
export_actions_names.append(actname)
@@ -73,50 +80,103 @@ class glTF2ExportUserExtension:
print('Actions:', export_actions_names)
# collection actions
act_list = []
for act in params.blender_actions:
if len(act.keys()):
if "arp_baked_action" in act.keys():
if self.export_action_only == 'all_actions':# all
act_list.append(act)
elif self.export_action_only == act.name:# single action export
act_list.append(act)
elif len(export_actions_names):# actions list export
if act.name in export_actions_names:
act_list = []
if bpy.app.version >= (4, 4, 0):
# With version 4.4.0 and higher, params is an object that contains all needed data
for action_id in list(params.actions.keys()):
for act_id, act in enumerate(params.actions[action_id][:]):
if "arp_baked_action" in act.action.keys():
if self.export_action_only == 'all_actions':
pass # Do nothing, all actions are exported
elif len(export_actions_names) == 0 and self.export_action_only == params.actions[action_id][act_id].action.name:# single action
pass
elif len(export_actions_names) and params.actions[action_id][act_id].action.name in export_actions_names:# multiple actions
pass # Do nothing, this action is exported
else:
# We are going to remove this action from the list
params.actions[action_id].remove(act)
else:
params.actions[action_id].remove(act)
else:
# With version previous to 4.4.0, params has 3 fields: blender_actions, blender_tracks, action_on_type
for act in params.blender_actions:
if len(act.keys()):
if "arp_baked_action" in act.keys():
if self.export_action_only == 'all_actions':# all
act_list.append(act)
elif self.export_action_only == act.name:# single action export
act_list.append(act)
elif len(export_actions_names):# actions list export
if act.name in export_actions_names:
act_list.append(act)
params.blender_actions = act_list
for (k, v) in params.blender_tracks.items():
print('k', k)
print('v', v)
params.blender_tracks = {k:v for (k, v) in params.blender_tracks.items() if k in [act.name for act in params.blender_actions]}
params.action_on_type = {k:v for (k, v) in params.action_on_type.items() if k in [act.name for act in params.blender_actions]}
def animation_switch_loop_hook(self, blender_object, post, export_settings):
# Before looping on actions to export
# Store used action of original rig
# This hook ensures that the original rig active action is conserved after exporting, since it is switched during the export
# Store active action of original rig before looping through actions
if 'arp_rig_name' in blender_object and post is False:
original_rig = bpy.data.objects[blender_object['arp_rig_name']]
if original_rig.animation_data and original_rig.animation_data.action:
self.action = original_rig.animation_data.action
# save action
self.base_action = original_rig.animation_data.action
# save slot
if bpy.app.version >= (4,4,0):# backward-compatibility
self.base_action_slot_idx = get_action_slot_idx(self.base_action, original_rig.animation_data.action_slot)
# Restore initial action of the original rig
# After looping on actions to export
if 'arp_rig_name' in blender_object and post is True:
original_rig = bpy.data.objects[blender_object['arp_rig_name']]
if original_rig.animation_data:
original_rig.animation_data.action = self.action
self.action = None
# assign action
original_rig.animation_data.action = self.base_action
# assign slot
if bpy.app.version >= (4, 4, 0):
original_rig.animation_data.action_slot = self.base_action.slots[self.base_action_slot_idx]
self.base_action = None
self.base_action_slot_idx = 0
def post_animation_switch_hook(self, blender_object, blender_action, track_name, on_type, export_settings):
# When switching the exported rig, also switch the original rig (same action + "_%temp")
def post_animation_switch_hook(self, *args, **kwargs):
# This hook is necessary for shape keys export
# (if shape keys are driven by bones, then contained in the action data)
# When switching the exported rig action, make sure to also switch the action of the original rig
# the original action name and slot index are now stored in the 'arp_baked_action' property located on the baked action:
# ["arp_baked_action"] = action.name+'|||'+str(slot_idx)
# to be fetched with:
# act_name, slot_idx = blender_action["arp_baked_action"].split('|||')
if bpy.app.version >= (4, 4, 0):
blender_object, blender_action, slot, track_name, on_type, export_settings = args
else:
blender_object, blender_action, track_name, on_type, export_settings = args
if 'arp_rig_name' in blender_object:
original_rig = bpy.data.objects[blender_object['arp_rig_name']]
if original_rig.animation_data:
original_rig.animation_data.action = bpy.data.actions[blender_action.name + "_%temp"]
act_name, slot_idx = blender_action["arp_baked_action"].split('|||')
# assign action
original_rig.animation_data.action = bpy.data.actions[act_name]
# assign slot
if bpy.app.version >= (4, 4, 0):
original_rig.animation_data.action_slot = \
original_rig.animation_data.action.slots[int(slot_idx)]
def menu_func_export(self, context):
@@ -1,6 +1,6 @@
schema_version = "1.0.0"
id = "auto_rig_pro"
version = "3.73.41"
version = "3.76.15"
name = "Auto-Rig Pro"
tagline = "Automatic rig generation based on reference bones and various tools"
maintainer = "Artell <elartblender@gmail.com>"
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,870 @@
c_foot_ik.l%False%RELATIVE_CHAIN%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
Ankle_L
False
True
c_leg_pole.l
c_toes_ik.l%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
Toes_L
False
False
c_foot_ik.r%False%RELATIVE_CHAIN%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
Ankle_R
False
True
c_leg_pole.r
c_toes_ik.r%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
Toes_R
False
False
c_shoulder.l%True%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
Scapula_L
False
False
c_hand_fk.l%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
Wrist_L
False
False
c_arm_fk.l%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
Shoulder_L
False
False
c_forearm_fk.l%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
Elbow_L
False
False
c_shoulder.r%True%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
Scapula_R
False
False
c_hand_fk.r%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
Wrist_R
False
False
c_arm_fk.r%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
Shoulder_R
False
False
c_forearm_fk.r%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
Elbow_R
False
False
c_root_master.x%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
Root_M
True
False
c_spine_01.x%True%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
RootPart1_M
False
False
c_spine_02.x%True%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
RootPart2_M
False
False
c_spine_03.x%True%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
Spine1_M
False
False
c_spine_04.x%True%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
Spine1Part1_M
False
False
c_spine_05.x%True%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
Spine1Part2_M
False
False
c_spine_06.x%True%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
Chest_M
False
False
c_subneck_1.x%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
Neck_M
False
False
c_subneck_2.x%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
NeckPart1_M
False
False
c_neck.x%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
NeckPart2_M
False
False
c_head.x%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
Head_M
False
False
c_thumb1.l%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
ThumbFinger1_L
False
False
c_thumb2.l%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
ThumbFinger2_L
False
False
c_thumb3.l%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
ThumbFinger3_L
False
False
c_index1.l%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
IndexFinger1_L
False
False
c_index2.l%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
IndexFinger2_L
False
False
c_index3.l%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
IndexFinger3_L
False
False
c_middle1.l%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
MiddleFinger1_L
False
False
c_middle2.l%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
MiddleFinger2_L
False
False
c_middle3.l%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
MiddleFinger3_L
False
False
c_ring1.l%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
RingFinger1_L
False
False
c_ring2.l%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
RingFinger2_L
False
False
c_ring3.l%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
RingFinger3_L
False
False
c_pinky1.l%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
PinkyFinger1_L
False
False
c_pinky2.l%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
PinkyFinger2_L
False
False
c_pinky3.l%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
PinkyFinger3_L
False
False
c_thumb1.r%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
ThumbFinger1_R
False
False
c_thumb2.r%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
ThumbFinger2_R
False
False
c_thumb3.r%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
ThumbFinger3_R
False
False
c_index1.r%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
IndexFinger1_R
False
False
c_index2.r%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
IndexFinger2_R
False
False
c_index3.r%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
IndexFinger3_R
False
False
c_middle1.r%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
MiddleFinger1_R
False
False
c_middle2.r%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
MiddleFinger2_R
False
False
c_middle3.r%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
MiddleFinger3_R
False
False
c_ring1.r%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
RingFinger1_R
False
False
c_ring2.r%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
RingFinger2_R
False
False
c_ring3.r%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
RingFinger3_R
False
False
c_pinky1.r%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
PinkyFinger1_R
False
False
c_pinky2.r%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
PinkyFinger2_R
False
False
c_pinky3.r%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
PinkyFinger3_R
False
False
MiddleFinger4_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
MiddleFinger4_L
False
True
Cup_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
Cup_L
False
True
PinkyFinger4_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
PinkyFinger4_L
False
True
RingFinger4_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
RingFinger4_L
False
True
IndexFinger4_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
IndexFinger4_L
False
True
ThumbFinger4_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
ThumbFinger4_L
False
True
MiddleFinger4_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
MiddleFinger4_R
False
True
Cup_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
Cup_R
False
True
PinkyFinger4_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
PinkyFinger4_R
False
True
RingFinger4_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
RingFinger4_R
False
True
IndexFinger4_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
IndexFinger4_R
False
True
ThumbFinger4_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
ThumbFinger4_R
False
True
HipPart2_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
HipPart2_L
False
True
HipPart2_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
HipPart2_R
False
True
ShoulderPart2_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
ShoulderPart2_L
False
True
ShoulderPart2_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
ShoulderPart2_R
False
True
FaceJoint_M%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
FaceJoint_M
False
True
EyeJoint_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
EyeJoint_R
False
True
IrisJoint_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
IrisJoint_R
False
True
PupilJoint_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
PupilJoint_R
False
True
EyeJoint_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
EyeJoint_L
False
True
IrisJoint_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
IrisJoint_L
False
True
PupilJoint_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
PupilJoint_L
False
True
upperLidMain0_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
upperLidMain0_R
False
True
upperLidMain1_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
upperLidMain1_R
False
True
upperLidMain2_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
upperLidMain2_R
False
True
upperLidMain3_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
upperLidMain3_R
False
True
upperLidMain4_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
upperLidMain4_R
False
True
upperLidMain5_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
upperLidMain5_R
False
True
upperLidMain6_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
upperLidMain6_R
False
True
upperLidMain7_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
upperLidMain7_R
False
True
upperLidMain8_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
upperLidMain8_R
False
True
upperLidMain9_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
upperLidMain9_R
False
True
upperLidMain10_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
upperLidMain10_R
False
True
upperLidMain11_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
upperLidMain11_R
False
True
upperLidMain12_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
upperLidMain12_R
False
True
upperLidMain13_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
upperLidMain13_R
False
True
upperLidMain14_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
upperLidMain14_R
False
True
upperLidBaseJoint_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
upperLidBaseJoint_R
False
True
lowerLidMain1_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
lowerLidMain1_R
False
True
lowerLidMain2_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
lowerLidMain2_R
False
True
lowerLidMain3_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
lowerLidMain3_R
False
True
lowerLidMain4_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
lowerLidMain4_R
False
True
lowerLidMain5_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
lowerLidMain5_R
False
True
lowerLidMain6_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
lowerLidMain6_R
False
True
lowerLidMain7_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
lowerLidMain7_R
False
True
lowerLidMain8_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
lowerLidMain8_R
False
True
lowerLidMain9_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
lowerLidMain9_R
False
True
lowerLidMain10_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
lowerLidMain10_R
False
True
lowerLidMain11_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
lowerLidMain11_R
False
True
lowerLidMain12_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
lowerLidMain12_R
False
True
lowerLidMain13_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
lowerLidMain13_R
False
True
upperLidMain0_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
upperLidMain0_L
False
True
upperLidMain1_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
upperLidMain1_L
False
True
upperLidMain2_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
upperLidMain2_L
False
True
upperLidMain3_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
upperLidMain3_L
False
True
upperLidMain4_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
upperLidMain4_L
False
True
upperLidMain5_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
upperLidMain5_L
False
True
upperLidMain6_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
upperLidMain6_L
False
True
upperLidMain7_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
upperLidMain7_L
False
True
upperLidMain8_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
upperLidMain8_L
False
True
upperLidMain9_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
upperLidMain9_L
False
True
upperLidMain10_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
upperLidMain10_L
False
True
upperLidMain11_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
upperLidMain11_L
False
True
upperLidMain12_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
upperLidMain12_L
False
True
upperLidMain13_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
upperLidMain13_L
False
True
upperLidMain14_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
upperLidMain14_L
False
True
upperLidBaseJoint_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
upperLidBaseJoint_L
False
True
lowerLidMain1_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
lowerLidMain1_L
False
True
lowerLidMain2_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
lowerLidMain2_L
False
True
lowerLidMain3_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
lowerLidMain3_L
False
True
lowerLidMain4_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
lowerLidMain4_L
False
True
lowerLidMain5_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
lowerLidMain5_L
False
True
lowerLidMain6_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
lowerLidMain6_L
False
True
lowerLidMain7_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
lowerLidMain7_L
False
True
lowerLidMain8_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
lowerLidMain8_L
False
True
lowerLidMain9_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
lowerLidMain9_L
False
True
lowerLidMain10_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
lowerLidMain10_L
False
True
lowerLidMain11_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
lowerLidMain11_L
False
True
lowerLidMain12_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
lowerLidMain12_L
False
True
lowerLidMain13_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
lowerLidMain13_L
False
True
EyeBrowInnerJoint_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
EyeBrowInnerJoint_R
False
True
EyeBrowOuterJoint_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
EyeBrowOuterJoint_R
False
True
EyeBrowCenterJoint_M%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
EyeBrowCenterJoint_M
False
True
EyeBrowInnerJoint_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
EyeBrowInnerJoint_L
False
True
EyeBrowOuterJoint_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
EyeBrowOuterJoint_L
False
True
LipJoints_M%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
LipJoints_M
False
True
upperLipJoint0_M%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
upperLipJoint0_M
False
True
upperLipJoint1_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
upperLipJoint1_R
False
True
upperLipJoint1_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
upperLipJoint1_L
False
True
upperLipJoint2_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
upperLipJoint2_R
False
True
upperLipJoint2_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
upperLipJoint2_L
False
True
upperLipJoint3_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
upperLipJoint3_R
False
True
upperLipJoint3_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
upperLipJoint3_L
False
True
LipJoint_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
LipJoint_R
False
True
LipJoint_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
LipJoint_L
False
True
lowerLipJoint0_M%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
lowerLipJoint0_M
False
True
lowerLipJoint1_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
lowerLipJoint1_R
False
True
lowerLipJoint1_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
lowerLipJoint1_L
False
True
lowerLipJoint2_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
lowerLipJoint2_R
False
True
lowerLipJoint2_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
lowerLipJoint2_L
False
True
lowerLipJoint3_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
lowerLipJoint3_R
False
True
lowerLipJoint3_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
lowerLipJoint3_L
False
True
SmileBulgeJoint_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
SmileBulgeJoint_R
False
True
FrownBulgeJoint_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
FrownBulgeJoint_R
False
True
SmileBulgeJoint_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
SmileBulgeJoint_L
False
True
FrownBulgeJoint_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
FrownBulgeJoint_L
False
True
CheekRaiserJoint_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
CheekRaiserJoint_R
False
True
CheekRaiserJoint_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
CheekRaiserJoint_L
False
True
CheekJoint_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
CheekJoint_R
False
True
CheekJoint_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
CheekJoint_L
False
True
JawJoint_M%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
JawJoint_M
False
True
upperTeethJoint_M%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
upperTeethJoint_M
False
True
lowerTeethJoint_M%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
lowerTeethJoint_M
False
True
NoseJoint_M%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
NoseJoint_M
False
True
NoseCornerJoint_R%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
NoseCornerJoint_R
False
True
NoseCornerJoint_L%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
NoseCornerJoint_L
False
True
Tongue0Joint_M%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
Tongue0Joint_M
False
True
Tongue1Joint_M%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
Tongue1Joint_M
False
True
Tongue2Joint_M%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
Tongue2Joint_M
False
True
Tongue3Joint_M%False%ABSOLUTE%0.0,0.0,0.0%0.0,0.0,0.0%1.0%False%False%Y%
Tongue3Joint_M
False
True
File diff suppressed because it is too large Load Diff
@@ -23,7 +23,7 @@ thumb_ref_list = [j for i, j in thumb_ref_dict.items()]
thumb_control_dict = {'base':'c_thumb1_base', '1':'c_thumb1', '2':'c_thumb2', '3':'c_thumb3'}
thumb_control_list = [j for i, j in thumb_control_dict.items()]
thumb_intern_dict = {'base':'thumb1', 'bend_all':'thumb_bend_all', 'rot1':'c_thumb1_rot', 'rot2':'c_thumb2_rot', 'rot3':'c_thumb3_rot'}
thumb_intern_dict = {'base':'thumb1', 'bend_all':'thumb_bend_all', 'rot1':'thumb1_rot', 'rot2':'thumb2_rot', 'rot3':'thumb3_rot'}
thumb_intern_list = [j for i, j in thumb_intern_dict.items()]
# index
@@ -87,12 +87,13 @@ arm_bones_dict = {
'shoulder':{'control':'c_shoulder', 'deform':'shoulder', 'track_pole':'shoulder_track_pole', 'pole':'shoulder_pole'},
'arm':{'base':'arm', 'twist':'arm_twist', 'twist_twk':'arm_twist_twk', 'control_twist':'c_arm_twist_offset', 'stretch':'arm_stretch', 'control_fk':'c_arm_fk', 'fk':'arm_fk', 'ik':'arm_ik', 'control_ik':'c_arm_ik', 'ik_scale_fix':'arm_ik_nostr_scale_fix', 'ik_nostr':'arm_ik_nostr', 'secondary_00':'c_shoulder_bend','secondary_01':'c_arm_bend'},
'forearm':{'base':'forearm', 'control_fk':'c_forearm_fk', 'fk':'forearm_fk', 'ik':'forearm_ik', 'ik_nostr':'forearm_ik_nostr', 'stretch':'forearm_stretch', 'twist':'forearm_twist', 'secondary_00':'c_elbow_bend', 'secondary_01':'c_forearm_bend', 'secondary_02':'c_wrist_bend'},
'hand':{'deform':'hand', 'control_fk':'c_hand_fk', 'control_ik':'c_hand_ik', 'control_ik_offset':'c_hand_ik_offset', 'fk_scale_fix':'c_hand_fk_scale_fix', 'rot_twist':'hand_rot_twist', 'secondary_00':'hand_bend'},
'hand':{'deform':'hand', 'control_fk':'c_hand_fk', 'control_ik':'c_hand_ik', 'control_ik_offset':'c_hand_ik_offset', 'ik_pivot':'hand_ik_pivot', 'control_ik_pivot':'c_hand_ik_pivot', 'fk_scale_fix':'hand_fk_scale_fix', 'rot_twist':'hand_rot_twist', 'secondary_00':'hand_bend'},
'prepole':'arm_fk_pre_pole',
'fk_pole':'arm_fk_pole',
'control_pin':'c_stretch_arm_pin',
'control_stretch':'c_stretch_arm',
'control_pole_ik': 'c_arms_pole'}
'control_pole_ik': 'c_arms_pole',
'pole_line': 'arm_pole_line'}
arm_bones = []
for i, j in arm_bones_dict.items():
@@ -184,10 +185,11 @@ leg_bones_dict = {
'upthigh_helper': {'1': 'thigh_b_h', '2': 'thigh_b_loc'},
'thigh': {'base':'thigh', 'fk':'thigh_fk', 'ik':'thigh_ik', 'ik_nostr':'thigh_ik_nostr', 'control_fk':'c_thigh_fk', 'control_ik':'c_thigh_ik', 'twist':'thigh_twist', 'stretch':'thigh_stretch', 'secondary_00':'c_thigh_bend_contact', 'secondary_01':'c_thigh_bend_01', 'secondary_02':'c_thigh_bend_02'},
'calf':{'base':'leg', 'fk':'leg_fk', 'ik':'leg_ik', 'ik_nostr':'leg_ik_nostr', 'control_fk':'c_leg_fk', 'control_ik3':'c_leg_ik3', 'twist':'leg_twist', 'stretch':'leg_stretch', 'secondary_00':'c_knee_bend', 'secondary_01':'c_leg_bend_01', 'secondary_02':'c_leg_bend_02', 'secondary_03':'c_ankle_bend'},
'foot':{'fk':'foot_fk', 'control_fk':'c_foot_fk', 'snap_fk':'foot_snap_fk', 'ik':'foot_ik', 'ik_target':'foot_ik_target', 'control_ik':'c_foot_ik', 'deform':'foot', 'pole':'foot_pole', 'fk_scale_fix':'c_foot_fk_scale_fix', 'shape_override_fk':'c_p_foot_fk', 'shape_override_ik':'c_p_foot_ik', 'bank_01':'c_foot_bank_01', 'bank_02':'c_foot_bank_02', 'foot_heel':'c_foot_heel', 'control_reverse':'c_foot_01', 'pole_01':'foot_01_pole', 'roll':'c_foot_roll', 'control_roll':'c_foot_roll_cursor', 'secondary_00':'foot_bend', 'control_ik_offset':'c_foot_ik_offset', 'foot_fans_helper':'foot_fans_h'},
'toes':{'01': 'toes_01', '02': 'toes_02', '01_ik': 'toes_01_ik', 'control_fk': 'c_toes_fk', 'control_ik':'c_toes_ik', 'toes_track': 'c_toes_track', 'toes_end': 'c_toes_end', 'toes_end_01':'c_toes_end_01', 'control_pivot':'c_toes_pivot'},
'foot':{'fk':'foot_fk', 'control_fk':'c_foot_fk', 'snap_fk':'foot_snap_fk', 'ik':'foot_ik', 'ik_target':'foot_ik_target', 'control_ik':'c_foot_ik', 'deform':'foot', 'pole':'foot_pole', 'fk_scale_fix':'foot_fk_scale_fix', 'shape_override_fk':'c_p_foot_fk', 'shape_override_ik':'c_p_foot_ik', 'bank_01':'foot_bank_01', 'bank_02':'foot_bank_02', 'foot_heel':'foot_heel', 'control_reverse':'c_foot_01', 'pole_01':'foot_01_pole', 'roll':'foot_roll', 'control_roll':'c_foot_roll_cursor', 'secondary_00':'foot_bend', 'ik_pivot':'foot_ik_pivot', 'control_ik_pivot':'c_foot_ik_pivot','control_ik_offset':'c_foot_ik_offset', 'foot_fans_helper':'foot_fans_h'},
'toes':{'01': 'toes_01', '02': 'toes_02', '01_ik': 'toes_01_ik', 'control_fk': 'c_toes_fk', 'control_ik':'c_toes_ik', 'toes_track': 'toes_track', 'toes_end': 'toes_end', 'toes_end_01':'toes_end_01', 'control_pivot':'c_toes_pivot'},
'prepole':'leg_fk_pre_pole',
'control_pole_ik':'c_leg_pole',
'pole_line': 'leg_pole_line',
'fk_pole':'leg_fk_pole',
'control_stretch':'c_stretch_leg',
'control_pin':'c_stretch_leg_pin',
@@ -240,7 +242,7 @@ leg_deform = [
leg_bones_dict['upthigh2']]
leg_control = [
leg_bones_dict['upthigh'], leg_bones_dict['thigh']['control_fk'], leg_bones_dict['thigh']['control_ik'], leg_bones_dict['calf']['control_fk'], leg_bones_dict['toes_pinky1'], leg_bones_dict['toes_pinky2'], leg_bones_dict['toes_pinky3'], leg_bones_dict['toes_ring1'], leg_bones_dict['toes_ring2'], leg_bones_dict['toes_ring3'], leg_bones_dict['toes_middle1'], leg_bones_dict['toes_middle2'], leg_bones_dict['toes_middle3'], leg_bones_dict['toes_index1'], leg_bones_dict['toes_index2'], leg_bones_dict['toes_index3'], leg_bones_dict['toes_thumb1'], leg_bones_dict['toes_thumb2'], leg_bones_dict['foot']['control_fk'], leg_bones_dict['toes']['control_fk'], leg_bones_dict['control_stretch'], leg_bones_dict['calf']['secondary_03'], leg_bones_dict['calf']['secondary_02'], leg_bones_dict['calf']['secondary_01'], leg_bones_dict['calf']['secondary_00'], leg_bones_dict['thigh']['secondary_02'], leg_bones_dict['thigh']['secondary_01'], leg_bones_dict['thigh']['secondary_00'], leg_bones_dict['control_pin'], leg_bones_dict['foot']['control_ik'], leg_bones_dict['foot']['control_ik_offset'], leg_bones_dict['toes']['control_ik'], leg_bones_dict['foot']['control_reverse'], leg_bones_dict['foot']['control_roll'], leg_bones_dict['control_pole_ik']]
leg_bones_dict['upthigh'], leg_bones_dict['thigh']['control_fk'], leg_bones_dict['thigh']['control_ik'], leg_bones_dict['calf']['control_fk'], leg_bones_dict['toes_pinky1'], leg_bones_dict['toes_pinky2'], leg_bones_dict['toes_pinky3'], leg_bones_dict['toes_ring1'], leg_bones_dict['toes_ring2'], leg_bones_dict['toes_ring3'], leg_bones_dict['toes_middle1'], leg_bones_dict['toes_middle2'], leg_bones_dict['toes_middle3'], leg_bones_dict['toes_index1'], leg_bones_dict['toes_index2'], leg_bones_dict['toes_index3'], leg_bones_dict['toes_thumb1'], leg_bones_dict['toes_thumb2'], leg_bones_dict['foot']['control_fk'], leg_bones_dict['toes']['control_fk'], leg_bones_dict['control_stretch'], leg_bones_dict['calf']['secondary_03'], leg_bones_dict['calf']['secondary_02'], leg_bones_dict['calf']['secondary_01'], leg_bones_dict['calf']['secondary_00'], leg_bones_dict['thigh']['secondary_02'], leg_bones_dict['thigh']['secondary_01'], leg_bones_dict['thigh']['secondary_00'], leg_bones_dict['control_pin'], leg_bones_dict['foot']['control_ik'], leg_bones_dict['foot']['control_ik_offset'], leg_bones_dict['foot']['control_ik_pivot'],leg_bones_dict['toes']['control_ik'], leg_bones_dict['foot']['control_reverse'], leg_bones_dict['foot']['control_roll'], leg_bones_dict['control_pole_ik']]
leg_props = {'soft_ik': 'leg_softik', 'auto_ik_roll': 'leg_auto_ik_roll'}
@@ -275,7 +277,6 @@ def get_leg_toes_ikfk(leg_side, btype='ALL', no_side=False):
return list
def get_leg_joint_fans(leg_side, btype='ALL', no_side=False):
types = [btype]
if btype == 'ALL':
@@ -336,6 +337,7 @@ mouth_bones_ref_dict = {'lips_top_mid': 'lips_top_ref.x',
'lips_roll_top': 'lips_roll_top_ref.x',
'lips_roll_bot': 'lips_roll_bot_ref.x',
'lips_offset': 'lips_offset_ref.x',
'muzzle': 'muzzle_ref.x',
'jaw':'jaw_ref.x'
}
@@ -368,7 +370,8 @@ mouth_bones_dict = {
'c_lips_smile_offset': {'name':'c_lips_smile_offset', 'deform':False, 'control':False},
'c_lips_smile': {'name':'c_lips_smile', 'deform':True, 'control':True},
'c_lips_corner_mini': {'name':'c_lips_corner_mini', 'deform':True, 'control':True},
'c_lips_offset': {'name':'c_lips_offset.x', 'deform':False, 'control':True}
'c_lips_offset': {'name':'c_lips_offset.x', 'deform':False, 'control':True},
'c_muzzle': {'name':'c_muzzle.x', 'deform':False, 'control':True}
}
@@ -403,7 +406,7 @@ def get_variable_lips(head_side, btype='REFERENCE', no_side=False, levels=['top_
lips_list = []
for subtype in types:
for lip_id in range(1,32):
for lip_id in range(1, 32):
for _side in ['.l', '.r']:
for lvl in levels:
bname = ''
@@ -533,7 +536,6 @@ for i in teeth_ref_base:
teeth_ref.append(i+'.r')
# tongues
tongue_bones_ref_dict = {'tong_01':'tong_01_ref.x',
'tong_02':'tong_02_ref.x',
'tong_03':'tong_03_ref.x'
@@ -550,6 +552,33 @@ tongue_bones_dict = {'c_tong_01': {'name':'c_tong_01.x', 'deform':False, 'contro
tongue_ref = [j for i, j in tongue_bones_ref_dict.items()]
tongue_bones = [tongue_bones_dict[i]['name'] for i in tongue_bones_dict]
def get_tongues(side='.x', type='ALL', no_side=True, side_x=False):
list = []
ctrl = []
ref = []
deform = []
_side = '' if no_side else side
_sidex = '.x' if side_x else ''
for _i in range(1, 33):
stri = '%02d' % _i
if bpy.context.active_object.data.bones.get('tong_'+stri+'_ref'+side):
ctrl.append('c_tong_'+stri+_side+_sidex)
ref.append('tong_'+stri+'_ref'+_side+_sidex)
deform.append('tong_'+stri+_side+_sidex)
if type == 'ALL':
list = ctrl + ref + deform
elif type == 'CTRL':
list = ctrl
elif type == 'REF':
list = ref
elif type == 'DEF':
list = deform
elif type == 'NON_REF':
list = ctrl + deform
return list
# eyes
@@ -662,26 +691,24 @@ eyebrow_bones_left = [i+'.l' for i in eyebrow_bones] + [i+'.l' for i in eyebrow_
eyebrow_bones_right = [i+'.r' for i in eyebrow_bones] + [i+'.r' for i in eyebrow_ref]
def get_eyebrows(type='ALL', include_full=True):
def get_eyebrows(side='.l', type='ALL', include_full=True):
list = []
main_ctrl = []
ref = []
main_ctrl = [eyebrow_bones_dict['eyebrow_01_end']['name'],
eyebrow_bones_dict['eyebrow_01']['name'],
eyebrow_bones_dict['eyebrow_02']['name'],
eyebrow_bones_dict['eyebrow_03']['name']]
ref = [eyebrow_bones_ref_dict['eyebrow_01_end'],
eyebrow_bones_ref_dict['eyebrow_01'],
eyebrow_bones_ref_dict['eyebrow_02'],
eyebrow_bones_ref_dict['eyebrow_03']]
if side.endswith('.x'):
side = side[:-2]+'.l'# same count for left and right brows for now
for _i in range(0, 32):
stri = '%02d' % _i if _i > 0 else '01_end'
if bpy.context.active_object.data.bones.get('eyebrow_'+stri+'_ref'+side):
main_ctrl.append('c_eyebrow_'+stri)
ref.append('eyebrow_'+stri+'_ref')
master_ctrl = [eyebrow_bones_dict['eyebrow_full']['name']]
master_ctrl = [eyebrow_bones_dict['eyebrow_full']['name']]
master_ref = [eyebrow_bones_ref_dict['eyebrow_full']]
offsets = []
if type == 'ALL':
list = main_ctrl + ref
if include_full:
@@ -885,9 +912,6 @@ def get_spline_ik(rig, side):
return None
# Tail
tail_bones = ['c_tail_master']
#SMART FACIAL MARKERS
facial_markers = {'eyebrow_01_end.l': 15, 'eyebrow_01.l':16, 'eyebrow_02.l':17, 'eyebrow_03.l':18, 'eyebrow_01_end.r':40, 'eyebrow_01.r':41, 'eyebrow_02.r':42, 'eyebrow_03.r':43,
File diff suppressed because it is too large Load Diff
@@ -1,4 +1,4 @@
import bpy, os, platform, sys, ast
import bpy, os, platform, sys, ast, subprocess
def update_all_tab_names(self, context):
try:
@@ -79,11 +79,13 @@ class ARP_OT_save_prefs(bpy.types.Operator):
'arp_tab_name':get_prefs().arp_tab_name,
'arp_tools_tab_name':get_prefs().arp_tools_tab_name,
'beginner_mode': get_prefs().beginner_mode,
'xray_display': get_prefs().xray_display,
'custom_armatures_path': get_prefs().custom_armatures_path,
'custom_limb_path': get_prefs().custom_limb_path,
'rig_layers_path': get_prefs().rig_layers_path,
'remap_presets_path': get_prefs().remap_presets_path,
'ge_presets_path': get_prefs().ge_presets_path,
'ai_presets_path': get_prefs().ai_presets_path,
#'prefs_presets_path': get_prefs().prefs_presets_path,
'default_ikfk_arm':get_prefs().default_ikfk_arm,
'default_ikfk_leg': get_prefs().default_ikfk_leg,
@@ -104,6 +106,70 @@ class ARP_OT_save_prefs(bpy.types.Operator):
print(fp)
return {'FINISHED'}
def get_downloads_folder():
"""Get the default downloads folder path based on the operating system."""
system = platform.system().lower()
home = os.path.expanduser("~") # Gets the user's home directory
downloads = os.path.join(home, "Downloads")
# Ensure the path exists (optional, remove if you just want the path)
if not os.path.exists(downloads):
os.makedirs(downloads, exist_ok=True)
return downloads
def extract_zip(source_zip, destination_folder):
import zipfile, stat
os.makedirs(destination_folder, exist_ok=True)
print("Unzip...")
# Use unzip process instead of zipfile for better fidelity on Linux and Mac
if platform.system() in ["Darwin", "Linux"]:
if platform.system() == "Darwin":
result = subprocess.run(['xattr', source_zip], capture_output=True, text=True)
if 'com.apple.quarantine' in result.stdout:
subprocess.run(['xattr', '-d', 'com.apple.quarantine', source_zip], check=True)
result = subprocess.run(["unzip", "-o", source_zip, "-d", destination_folder], capture_output=True, text=True)
if result.returncode != 0:
raise Exception(f"unzip failed: {result.stderr}")
print(f"unzip output: {result.stdout}")
else:
# Fallback to zipfile for other platforms
with zipfile.ZipFile(source_zip, 'r') as zip_ref:
zip_ref.extractall(destination_folder)
return True, f"Successfully extracted {source_zip} to {destination_folder}"
class ARP_OT_install_ext(bpy.types.Operator):
"""Install external dependencies"""
bl_idname = 'arp.install_ext'
bl_label = 'Install Files'
filepath: bpy.props.StringProperty(subtype='FILE_PATH', default='name')
filename_ext = '.zip'
def invoke(self, context, event):
self.filepath = downloads_path = get_downloads_folder()
context.window_manager.fileselect_add(self)
return {'RUNNING_MODAL'}
def execute(self, context):
if not self.filepath.endswith('.zip'):
self.report({'ERROR'}, 'Select the zip file to install')
return {'FINISHED'}
print("Extract zip...")
extract_zip(self.filepath, get_prefs().ai_presets_path)
return {'FINISHED'}
class ARP_MT_arp_addon_preferences(bpy.types.AddonPreferences):
@@ -111,62 +177,123 @@ class ARP_MT_arp_addon_preferences(bpy.types.AddonPreferences):
arp_tab_name : bpy.props.StringProperty(name='Interface Tab', description='Name of the tab to display the interface in', default='ARP', update=update_all_tab_names)
arp_tools_tab_name : bpy.props.StringProperty(name='Tools Interface Tab', description='Name of the tab to display the tools (IK-FK snap...) interface in', default='Tool', update=update_all_tab_names)
beginner_mode: bpy.props.BoolProperty(name='Beginner Mode', default=True)
beginner_mode: bpy.props.BoolProperty(name='Beginner Mode', default=True, description='Show helper icons in the menus, to open contextual documentation links')
xray_display: bpy.props.BoolProperty(name='XRay Display', default=False, description='Always show the rig in X-Ray display/ In Front\nApplied when Match to Rig')
custom_armatures_path: bpy.props.StringProperty(name='Armatures', subtype='FILE_PATH', default=get_documents_path('Armatures Presets'), description='Path to store armature presets')
custom_limb_path: bpy.props.StringProperty(name='Limbs', subtype='FILE_PATH', default=get_documents_path('Custom Limbs'), description='Path to store custom limb presets')
rig_layers_path: bpy.props.StringProperty(name='Rig Layers', subtype='FILE_PATH', default=get_documents_path('Rig Layers'), description='Path to store rig layers presets')
remap_presets_path: bpy.props.StringProperty(name='Remap Presets', subtype='FILE_PATH', default=get_documents_path('Remap Presets'), description='Path to store remap presets')
ge_presets_path: bpy.props.StringProperty(name='Export Presets', subtype='FILE_PATH', default=get_documents_path('Game Engine Presets'), description='Path to store game engine export presets')
#prefs_presets_path: bpy.props.StringProperty(name='Preferences', subtype='FILE_PATH', default=get_documents_path('Preferences'), description='Path to store Auto-Rig Pro preferences')
ai_presets_path: bpy.props.StringProperty(name='AI', subtype='FILE_PATH', default=get_documents_path('AI'), description='Path to store AI executables')
parent_bound_objects: bpy.props.BoolProperty(default=True, description='Parent the objects to the armature when binding.\nTurning it off *is not recommended* as it can lead to issues, but it may be useful for some custom workflows')
default_ikfk_arm: bpy.props.EnumProperty(items=(('IK', 'IK', 'IK'), ('FK', 'FK', 'FK')), description='Default value for arms IK-FK switch', name='IK-FK Arms Default')
default_ikfk_leg: bpy.props.EnumProperty(items=(('IK', 'IK', 'IK'), ('FK', 'FK', 'FK')), description='Default value for legs IK-FK switch', name='IK-FK Legs Default')
default_head_lock: bpy.props.BoolProperty(default=True, name='Head Lock Default', description='Default value for the Head Lock switch')
remove_existing_arm_mods: bpy.props.BoolProperty(default=True, name='Remove Armature Modifiers', description='Remove existing armature modifiers when binding')
remove_existing_vgroups: bpy.props.BoolProperty(default=True, name='Remove Existing Vertex Groups', description='Remove existing vertex groups when binding')
remove_existing_arm_mods: bpy.props.BoolProperty(default=True, name='Remove Armature Modifiers', description='Remove any existing armature modifiers on meshes, when binding.\nIt is generally recommended to enable it, in order to avoid older modifiers to interfere with new ones')
remove_existing_vgroups: bpy.props.BoolProperty(default=True, name='Remove Existing Vertex Groups', description='Remove existing vertex groups when binding. It is generally recommended to enable it, in order to avoid older vertex groups to interfere with new ones')
rem_arm_mods_set: bpy.props.BoolProperty(default=False, description='Toggle to be executed the first time binding, to set default prefs')
rem_vgroups_set: bpy.props.BoolProperty(default=False, description='Toggle to be executed the first time binding, to set default prefs')
show_export_popup: bpy.props.BoolProperty(default=True, description='Show a popup notification on export completion')
arp_master_collec_name: bpy.props.StringProperty(default='', description='Group all Auto-Rig Pro armature collections under this collection. None if blank name\nApplied when Match to Rig')
def draw(self, context):
col = self.layout.column(align=True)
layout = self.layout
col = layout.column(align=True)
col.operator(ARP_OT_save_prefs.bl_idname, text='Save Preferences')
col.prop(self, 'beginner_mode', text='Beginner Mode (help buttons)')
col.separator()
col.label(text='Rig:')
col.prop(self, 'remove_existing_arm_mods', text='Remove Existing Armature Modifiers when Binding')
col.prop(self, 'remove_existing_vgroups', text='Remove Existing Vertex Groups when Binding')
col.separator()
col.prop(self, 'default_ikfk_arm', text='IK-FK Arms')
col.prop(self, 'default_ikfk_leg', text='IK-FK Legs')
col.prop(self, 'default_head_lock', text='Head Lock')
def show_rig_ui(panel):
if panel:
col = panel.column(align=True)
else:
col = layout.column(align=True)
col.label(text='Rig:')
col.prop(self, 'arp_master_collec_name', text='Master ARP Collec')
col.prop(self, 'xray_display', text='X-Ray Display')
col.prop(self, 'parent_bound_objects', text='Parent Objects when Binding')
col.prop(self, 'remove_existing_arm_mods', text='Remove Existing Armature Modifiers when Binding')
col.prop(self, 'remove_existing_vgroups', text='Remove Existing Vertex Groups when Binding')
col.separator()
col.prop(self, 'default_ikfk_arm', text='IK-FK Arms')
col.prop(self, 'default_ikfk_leg', text='IK-FK Legs')
col.prop(self, 'default_head_lock', text='Head Lock')
col.separator()
if bpy.app.version >= (4,1,0):
header_rig, panel_rig = layout.panel("arp_pref_ui_rig", default_closed=False)
header_rig.label(text="Rig")
if panel_rig:# None if collapsed
show_rig_ui(panel_rig)
else:
show_rig_ui(None)
col.separator()
col.separator()
col.label(text='Interface:')
col.prop(self, 'arp_tab_name', text='Main ARP Tab')
col.prop(self, 'arp_tools_tab_name', text='Tools Tab')
col.prop(self, 'show_export_popup', text='Show Popup when Export Finished')
col.separator()
col.separator()
col.label(text='Paths:')
#col.prop(self, 'prefs_presets_path')
col.prop(self, 'custom_armatures_path')
col.prop(self, 'custom_limb_path')
col.prop(self, 'rig_layers_path')
col.prop(self, 'remap_presets_path')
col.prop(self, 'ge_presets_path')
col.separator()
col.separator()
col.label(text='Special-Debug:', icon='ERROR')
col.prop(context.scene, 'arp_disable_smart_fx')
col.prop(context.scene, 'arp_debug_mode')
col.prop(context.scene, 'arp_debug_bind')
col.prop(context.scene, 'arp_experimental_mode')
def show_interface_ui(panel):
if panel:
col = panel.column(align=True)
else:
col = layout.column(align=True)
col.label(text='Interface:')
row = col.row(align=True)
row.prop(self, 'beginner_mode', text='Beginner Mode (help buttons)')
col.separator()
col.prop(self, 'arp_tab_name', text='Main ARP Tab')
col.prop(self, 'arp_tools_tab_name', text='Tools Tab')
col.prop(self, 'show_export_popup', text='Show Popup when Export Finished')
col.separator()
if bpy.app.version >= (4,1,0):
header_interface, panel_interface = layout.panel("arp_pref_ui_interface", default_closed=False)
header_interface.label(text="Interface")
if panel_interface:# None if collapsed
show_interface_ui(panel_interface)
else:
show_interface_ui(None)
def show_paths_ui(panel):
if panel:
col = panel.column(align=True)
else:
col = layout.column(align=True)
col.label(text='Paths:')
col.prop(self, 'custom_armatures_path')
col.prop(self, 'custom_limb_path')
col.prop(self, 'rig_layers_path')
col.prop(self, 'remap_presets_path')
col.prop(self, 'ge_presets_path')
col.prop(self, 'ai_presets_path')
col.operator('arp.install_ext', text='Install AI files...')
col.separator()
if bpy.app.version >= (4,1,0):
header_paths, panel_paths = layout.panel("arp_pref_ui_paths", default_closed=False)
header_paths.label(text="Paths")
if panel_paths:# None if collapsed
show_paths_ui(panel_paths)
else:
show_paths_ui(None)
def show_debug_ui(panel):
if panel:
col = panel.column()
else:
col = layout.column(align=True)
col.label(text='Debug:')
col.label(text='Special-Debug:', icon='ERROR')
col.prop(context.scene, 'arp_disable_smart_fx')
col.prop(context.scene, 'arp_debug_mode')
col.prop(context.scene, 'arp_debug_bind')
col.prop(context.scene, 'arp_experimental_mode')
if bpy.app.version >= (4,1,0):
header_debug, panel_debug = layout.panel("arp_pref_ui_debug", default_closed=True)
header_debug.label(text="Debug")
if panel_debug:# None if collapsed
show_debug_ui(panel_debug)
else:
show_debug_ui(None)
def load_arp_prefs():
@@ -214,6 +341,7 @@ def register():
try:
register_class(ARP_MT_arp_addon_preferences)
register_class(ARP_OT_save_prefs)
register_class(ARP_OT_install_ext)
except:
pass
@@ -225,6 +353,7 @@ def register():
bpy.types.Scene.arp_experimental_mode = bpy.props.BoolProperty(name='Experimental Mode', default=False, description = 'Enable experimental, unstable tools. Warning, can lead to errors. Use it at your own risks.', options={'HIDDEN'})
bpy.types.Scene.arp_disable_smart_fx = bpy.props.BoolProperty(name='Disable Smart FX', default=False, description='Disable Smart markers FX for debug purposes, such as Mac systems not supporting some graphics. Safe to use.', options={'HIDDEN'})
def unregister():
from bpy.utils import unregister_class
unregister_class(ARP_MT_arp_addon_preferences)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
{'verts': [(-0.09828728437423706, -0.09828728437423706, -0.09828728437423706), (-0.09828728437423706, -0.09828728437423706, 0.09828728437423706), (-0.09828728437423706, 0.09828728437423706, -0.09828728437423706), (-0.09828728437423706, 0.09828728437423706, 0.09828728437423706), (0.09828728437423706, -0.09828728437423706, -0.09828728437423706), (0.09828728437423706, -0.09828728437423706, 0.09828728437423706), (0.09828728437423706, 0.09828728437423706, -0.09828728437423706), (0.09828728437423706, 0.09828728437423706, 0.09828728437423706)], 'edges': [(2, 0), (0, 1), (1, 3), (3, 2), (6, 2), (3, 7), (7, 6), (4, 6), (7, 5), (5, 4), (0, 4), (5, 1)], 'faces': []}
@@ -0,0 +1 @@
{'verts': [(2.2649766151516815e-07, 0.15570558607578278, -7.078047481456906e-09), (-0.059585727751255035, 0.14385320246219635, -7.078047481456906e-09), (-0.11010024696588516, 0.11010047048330307, -7.0780492578137455e-09), (-0.14385297894477844, 0.05958595126867294, -7.0780492578137455e-09), (-0.15570536255836487, -6.806108121537591e-09, -7.078051922349005e-09), (-0.14385297894477844, -0.05958595871925354, -7.078052810527424e-09), (-0.11010024696588516, -0.11010047048330307, -7.078053698705844e-09), (-0.059585727751255035, -0.14385320246219635, -7.078055475062683e-09), (2.0298676872698707e-07, -0.15570558607578278, -7.078056363241103e-09), (0.059586141258478165, -0.14385321736335754, -7.078055475062683e-09), (0.11010067909955978, -0.11010051518678665, -7.078053698705844e-09), (0.14385342597961426, -0.059585969895124435, -7.078052810527424e-09), (0.15570582449436188, 1.856770626140758e-09, -7.078051922349005e-09), (0.14385342597961426, 0.05958597734570503, -7.0780492578137455e-09), (0.11010066419839859, 0.11010051518678665, -7.0780492578137455e-09), (0.05958610400557518, 0.14385323226451874, -7.078047481456906e-09), (2.2649766151516815e-07, -1.2993086784263141e-07, -0.15570560097694397), (-0.05958572402596474, -1.2993086784263141e-07, -0.14385321736335754), (-0.11010023206472397, -9.280776680498093e-08, -0.11010048538446426), (-0.14385296404361725, -5.1044271032196775e-08, -0.05958595499396324), (-0.15570534765720367, 6.084951285385701e-15, -2.7194335672220404e-10), (-0.14385296404361725, 5.1044271032196775e-08, 0.05958595499396324), (-0.11010023206472397, 9.280776680498093e-08, 0.11010045558214188), (-0.059585727751255035, 1.2993086784263141e-07, 0.14385318756103516), (2.0298676872698707e-07, 1.2993086784263141e-07, 0.15570557117462158), (0.059586141258478165, 1.2993086784263141e-07, 0.14385321736335754), (0.11010066419839859, 9.280776680498093e-08, 0.11010050028562546), (0.14385342597961426, 5.1044271032196775e-08, 0.05958596616983414), (0.1557057946920395, -1.6595322553699244e-15, -8.934822659512065e-09), (0.14385341107845306, -5.1044271032196775e-08, -0.05958598479628563), (0.110100656747818, -9.280776680498093e-08, -0.11010053008794785), (0.05958609655499458, -1.2993086784263141e-07, -0.14385324716567993), (2.0112997844989877e-07, 0.15570557117462158, -3.244572610583418e-08), (2.5433996597712394e-07, 0.14385318756103516, -0.05958598479628563), (3.007438635904691e-07, 0.11010044813156128, -0.11010050773620605), (3.471477612038143e-07, 0.05958591774106026, -0.14385321736335754), (3.5642852935779956e-07, -3.217376942643568e-08, -0.15570560097694397), (3.657092975117848e-07, -0.05958598107099533, -0.14385320246219635), (3.3786699304982903e-07, -0.11010048538446426, -0.11010046303272247), (3.007438635904691e-07, -0.14385320246219635, -0.059585943818092346), (2.518653445804375e-07, -0.15570557117462158, -5.22126430979597e-09), (1.9865532863150293e-07, -0.14385320246219635, 0.05958593636751175), (1.5225145943986718e-07, -0.11010048538446426, 0.11010045558214188), (1.0584753340481257e-07, -0.059585943818092346, 0.14385321736335754), (9.656679367253673e-08, 2.7224428933436684e-08, 0.15570557117462158), (8.728602551855147e-08, 0.059585995972156525, 0.14385317265987396), (1.1512832998050726e-07, 0.11010052263736725, 0.11010041832923889), (1.5225145943986718e-07, 0.14385323226451874, 0.059585850685834885)], 'edges': [(1, 0), (2, 1), (3, 2), (4, 3), (5, 4), (6, 5), (7, 6), (8, 7), (9, 8), (10, 9), (11, 10), (12, 11), (13, 12), (14, 13), (15, 14), (0, 15), (17, 16), (18, 17), (19, 18), (20, 19), (21, 20), (22, 21), (23, 22), (24, 23), (25, 24), (26, 25), (27, 26), (28, 27), (29, 28), (30, 29), (31, 30), (16, 31), (33, 32), (34, 33), (35, 34), (36, 35), (37, 36), (38, 37), (39, 38), (40, 39), (41, 40), (42, 41), (43, 42), (44, 43), (45, 44), (46, 45), (47, 46), (32, 47)], 'faces': []}
@@ -0,0 +1 @@
{'verts': [(2.2649766151516815e-07, 0.6557056903839111, 0.000808675482403487), (-0.059585727751255035, 0.6438533067703247, 0.000808675482403487), (-0.11010024696588516, 0.6101005673408508, 0.000808675482403487), (-0.14385297894477844, 0.5595860481262207, 0.000808675482403487), (-0.15570536255836487, 0.5000001192092896, 0.000808675482403487), (-0.14385297894477844, 0.440414160490036, 0.000808675482403487), (-0.11010024696588516, 0.3898996412754059, 0.000808675482403487), (-0.059585727751255035, 0.3561469316482544, 0.000808675482403487), (2.0298676872698707e-07, 0.34429454803466797, 0.000808675482403487), (0.059586141258478165, 0.356146901845932, 0.000808675482403487), (0.11010067909955978, 0.3898996114730835, 0.000808675482403487), (0.14385342597961426, 0.440414160490036, 0.000808675482403487), (0.15570582449436188, 0.5000001192092896, 0.000808675482403487), (0.14385342597961426, 0.5595861077308655, 0.000808675482403487), (0.11010066419839859, 0.6101006269454956, 0.000808675482403487), (0.05958610400557518, 0.6438533663749695, 0.000808675482403487), (2.2649766151516815e-07, 0.5, -0.15489688515663147), (-0.05958572402596474, 0.5, -0.14304450154304504), (-0.11010023206472397, 0.5, -0.10929182916879654), (-0.14385296404361725, 0.5000000596046448, -0.058777254074811935), (-0.15570534765720367, 0.5000001192092896, 0.000808675482403487), (-0.14385296404361725, 0.5000001788139343, 0.06039463356137276), (-0.11010023206472397, 0.5000002384185791, 0.1109091266989708), (-0.059585727751255035, 0.5000002384185791, 0.14466187357902527), (2.0298676872698707e-07, 0.5000002384185791, 0.1565142571926117), (0.059586141258478165, 0.5000002384185791, 0.14466190338134766), (0.11010066419839859, 0.5000002384185791, 0.11090918630361557), (0.14385342597961426, 0.5000001788139343, 0.06039463356137276), (0.1557057946920395, 0.5000001192092896, 0.000808675482403487), (0.14385341107845306, 0.5000000596046448, -0.05877731367945671), (0.110100656747818, 0.5, -0.10929182916879654), (0.05958609655499458, 0.5, -0.14304456114768982), (2.0112997844989877e-07, 0.6557056903839111, 0.0008086158777587116), (2.5433996597712394e-07, 0.6438533067703247, -0.05877731367945671), (3.007438635904691e-07, 0.6101005673408508, -0.10929182916879654), (3.471477612038143e-07, 0.5595860481262207, -0.14304450154304504), (3.5642852935779956e-07, 0.5000000596046448, -0.15489688515663147), (3.657092975117848e-07, 0.4404141306877136, -0.14304450154304504), (3.3786699304982903e-07, 0.3898996412754059, -0.10929176956415176), (3.007438635904691e-07, 0.3561469316482544, -0.058777254074811935), (2.518653445804375e-07, 0.34429454803466797, 0.000808675482403487), (1.9865532863150293e-07, 0.3561469316482544, 0.06039460375905037), (1.5225145943986718e-07, 0.3898996412754059, 0.1109091266989708), (1.0584753340481257e-07, 0.4404141902923584, 0.14466190338134766), (9.656679367253673e-08, 0.5000001192092896, 0.1565142571926117), (8.728602551855147e-08, 0.5595861077308655, 0.14466187357902527), (1.1512832998050726e-07, 0.6101006269454956, 0.11090909689664841), (1.5225145943986718e-07, 0.6438533663749695, 0.060394514352083206)], 'edges': [(1, 0), (2, 1), (3, 2), (4, 3), (5, 4), (6, 5), (7, 6), (8, 7), (9, 8), (10, 9), (11, 10), (12, 11), (13, 12), (14, 13), (15, 14), (0, 15), (17, 16), (18, 17), (19, 18), (20, 19), (21, 20), (22, 21), (23, 22), (24, 23), (25, 24), (26, 25), (27, 26), (28, 27), (29, 28), (30, 29), (31, 30), (16, 31), (33, 32), (34, 33), (35, 34), (36, 35), (37, 36), (38, 37), (39, 38), (40, 39), (41, 40), (42, 41), (43, 42), (44, 43), (45, 44), (46, 45), (47, 46), (32, 47)], 'faces': []}
@@ -0,0 +1 @@
{'verts': [(2.2649766151516815e-07, 1.1557058095932007, 0.000906810280866921), (-0.059585727751255035, 1.1438534259796143, 0.000906810280866921), (-0.11010024696588516, 1.1101007461547852, 0.000906810280866921), (-0.14385297894477844, 1.0595861673355103, 0.000906810280866921), (-0.15570536255836487, 1.000000238418579, 0.000906810280866921), (-0.14385297894477844, 0.940414309501648, 0.000906810280866921), (-0.11010024696588516, 0.889899730682373, 0.000906810280866921), (-0.059585727751255035, 0.856147050857544, 0.000906810280866921), (2.0298676872698707e-07, 0.8442946672439575, 0.000906810280866921), (0.059586141258478165, 0.856147050857544, 0.000906810280866921), (0.11010067909955978, 0.889899730682373, 0.000906810280866921), (0.14385342597961426, 0.940414309501648, 0.000906810280866921), (0.15570582449436188, 1.000000238418579, 0.000906810280866921), (0.14385342597961426, 1.0595862865447998, 0.000906810280866921), (0.11010066419839859, 1.1101007461547852, 0.000906810280866921), (0.05958610400557518, 1.1438534259796143, 0.000906810280866921), (2.2649766151516815e-07, 1.0000001192092896, -0.1547987461090088), (-0.05958572402596474, 1.0000001192092896, -0.14294636249542236), (-0.11010023206472397, 1.0000001192092896, -0.10919369757175446), (-0.14385296404361725, 1.000000238418579, -0.05867911875247955), (-0.15570534765720367, 1.000000238418579, 0.000906810280866921), (-0.14385296404361725, 1.000000238418579, 0.06049273908138275), (-0.11010023206472397, 1.0000003576278687, 0.11100725829601288), (-0.059585727751255035, 1.0000003576278687, 0.14476001262664795), (2.0298676872698707e-07, 1.0000003576278687, 0.15661239624023438), (0.059586141258478165, 1.0000003576278687, 0.14476001262664795), (0.11010066419839859, 1.0000003576278687, 0.11100731790065765), (0.14385342597961426, 1.000000238418579, 0.06049273908138275), (0.1557057946920395, 1.000000238418579, 0.000906810280866921), (0.14385341107845306, 1.000000238418579, -0.058679237961769104), (0.110100656747818, 1.0000001192092896, -0.10919369757175446), (0.05958609655499458, 1.0000001192092896, -0.14294636249542236), (2.0112997844989877e-07, 1.1557058095932007, 0.000906810280866921), (2.5433996597712394e-07, 1.1438534259796143, -0.058679237961769104), (3.007438635904691e-07, 1.1101007461547852, -0.10919369757175446), (3.471477612038143e-07, 1.0595861673355103, -0.14294636249542236), (3.5642852935779956e-07, 1.000000238418579, -0.1547987461090088), (3.657092975117848e-07, 0.9404142498970032, -0.14294636249542236), (3.3786699304982903e-07, 0.889899730682373, -0.10919369757175446), (3.007438635904691e-07, 0.856147050857544, -0.05867911875247955), (2.518653445804375e-07, 0.8442946672439575, 0.000906810280866921), (1.9865532863150293e-07, 0.856147050857544, 0.06049273908138275), (1.5225145943986718e-07, 0.889899730682373, 0.11100725829601288), (1.0584753340481257e-07, 0.940414309501648, 0.14476001262664795), (9.656679367253673e-08, 1.000000238418579, 0.15661239624023438), (8.728602551855147e-08, 1.0595862865447998, 0.14476001262664795), (1.1512832998050726e-07, 1.1101007461547852, 0.1110071986913681), (1.5225145943986718e-07, 1.1438534259796143, 0.0604926198720932)], 'edges': [(1, 0), (2, 1), (3, 2), (4, 3), (5, 4), (6, 5), (7, 6), (8, 7), (9, 8), (10, 9), (11, 10), (12, 11), (13, 12), (14, 13), (15, 14), (0, 15), (17, 16), (18, 17), (19, 18), (20, 19), (21, 20), (22, 21), (23, 22), (24, 23), (25, 24), (26, 25), (27, 26), (28, 27), (29, 28), (30, 29), (31, 30), (16, 31), (33, 32), (34, 33), (35, 34), (36, 35), (37, 36), (38, 37), (39, 38), (40, 39), (41, 40), (42, 41), (43, 42), (44, 43), (45, 44), (46, 45), (47, 46), (32, 47)], 'faces': []}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{'verts': [(-0.09828728437423706, 0.40223199129104614, -0.09828728437423706), (-0.09828728437423706, 0.40223199129104614, 0.09828728437423706), (-0.09828728437423706, 0.5988066792488098, -0.09828728437423706), (-0.09828728437423706, 0.5988066792488098, 0.09828728437423706), (0.09828728437423706, 0.40223199129104614, -0.09828728437423706), (0.09828728437423706, 0.40223199129104614, 0.09828728437423706), (0.09828728437423706, 0.5988066792488098, -0.09828728437423706), (0.09828728437423706, 0.5988066792488098, 0.09828728437423706)], 'edges': [(2, 0), (0, 1), (1, 3), (3, 2), (6, 2), (3, 7), (7, 6), (4, 6), (7, 5), (5, 4), (0, 4), (5, 1)], 'faces': []}
@@ -0,0 +1 @@
{'verts': [(-0.09828728437423706, 0.9076300859451294, -0.09828728437423706), (-0.09828728437423706, 0.9076300859451294, 0.09828728437423706), (-0.09828728437423706, 1.104204773902893, -0.09828728437423706), (-0.09828728437423706, 1.104204773902893, 0.09828728437423706), (0.09828728437423706, 0.9076300859451294, -0.09828728437423706), (0.09828728437423706, 0.9076300859451294, 0.09828728437423706), (0.09828728437423706, 1.104204773902893, -0.09828728437423706), (0.09828728437423706, 1.104204773902893, 0.09828728437423706), (0.00014829635620117188, 1.0, 0.0)], 'edges': [(2, 0), (0, 1), (1, 3), (3, 2), (6, 2), (3, 7), (7, 6), (4, 6), (7, 5), (5, 4), (0, 4), (5, 1)], 'faces': []}
@@ -224,6 +224,7 @@ def fbx_template_def_light(scene, settings, override_defaults=None, nbr_users=0)
b"CastLight": (True, "p_bool", False),
b"Color": ((1.0, 1.0, 1.0), "p_color", True),
b"Intensity": (100.0, "p_number", True), # Times 100 compared to Blender values...
b"Exposure" : (0.0, "p_number", True ),
b"DecayType": (2, "p_enum", False), # Quadratic.
b"DecayStart": (30.0 * gscale, "p_double", False),
b"CastShadows": (True, "p_bool", False),
@@ -611,11 +612,12 @@ def fbx_data_light_elements(root, lamp, scene_data):
light_key = scene_data.data_lights[lamp]
do_light = True
do_shadow = False
# NOTE: this was removed from lamps, always write black.
shadow_color = Vector((0.0, 0.0, 0.0))
if lamp.type not in {'HEMI'}:
do_light = True
do_shadow = lamp.use_shadow
shadow_color = lamp.shadow_color
# `shadow_color = lamp.shadow_color`: now removed.
light = elem_data_single_int64(root, b"NodeAttribute", get_fbx_uuid_from_key(light_key))
light.add_string(fbx_name_class(lamp.name.encode(), b"NodeAttribute"))
@@ -623,12 +625,20 @@ def fbx_data_light_elements(root, lamp, scene_data):
elem_data_single_int32(light, b"GeometryVersion", FBX_GEOMETRY_VERSION) # Sic...
intensity = lamp.energy * 100.0 * pow(2.0, lamp.exposure)
color = lamp.color.copy()
if lamp.use_temperature:
temperature_color = lamp.temperature_color
color[0] *= temperature_color[0]
color[1] *= temperature_color[1]
color[2] *= temperature_color[2]
tmpl = elem_props_template_init(scene_data.templates, b"Light")
props = elem_properties(light)
elem_props_template_set(tmpl, props, "p_enum", b"LightType", FBX_LIGHT_TYPES[lamp.type])
elem_props_template_set(tmpl, props, "p_bool", b"CastLight", do_light)
elem_props_template_set(tmpl, props, "p_color", b"Color", lamp.color)
elem_props_template_set(tmpl, props, "p_number", b"Intensity", lamp.energy * 100.0)
elem_props_template_set(tmpl, props, "p_color", b"Color", color)
elem_props_template_set(tmpl, props, "p_number", b"Intensity", intensity)
elem_props_template_set(tmpl, props, "p_enum", b"DecayType", FBX_LIGHT_DECAY_TYPES['INVERSE_SQUARE'])
elem_props_template_set(tmpl, props, "p_double", b"DecayStart", 25.0 * gscale) # 25 is old Blender default
elem_props_template_set(tmpl, props, "p_bool", b"CastShadows", do_shadow)
@@ -838,7 +848,16 @@ def fbx_data_mesh_shapes_elements(root, me_obj, me, scene_data, fbx_me_tmpl, fbx
mv_shape_verts_idx = shape_verts_idx.data
vg_idx = me_obj.bdata.vertex_groups[shape.vertex_group].index
for sk_idx, v_idx in enumerate(mv_shape_verts_idx):
curr_groups = []
for vg in vertices[v_idx].groups:
# weird bug, some vertices were reported with corrupted vgroups data (two times the same vertex group!).
# Ensure this does not happen
vgname = me_obj.bdata.vertex_groups[vg.group].name
if vgname in curr_groups:
continue
curr_groups.append(vgname)
if vg.group == vg_idx:
mv_shape_verts_weights[sk_idx] = vg.weight
break
@@ -931,8 +950,9 @@ def fbx_data_mesh_elements(root, me_obj, scene_data, done_meshes):
fbx_data_element_custom_properties(props, me)
# Subdivision levels. Take them from the first found subsurf modifier from the
# first object that has the mesh. Write crease information if the object has
# and subsurf modifier.
# first object that has the mesh. Always write crease information if present,
# if the modifier explicitly uses creases ("use_creases" setting) and mesh lacks them,
# still provide zeros (see TODO comment below)
write_crease = False
if scene_data.settings.use_subsurf:
last_subsurf = None
@@ -956,6 +976,7 @@ def fbx_data_mesh_elements(root, me_obj, scene_data, done_meshes):
elem_data_single_int32(geom, b"PropagateEdgeHardness", 0)
write_crease = last_subsurf.use_creases
write_crease = (write_crease or me.edge_creases)
elem_data_single_int32(geom, b"GeometryVersion", FBX_GEOMETRY_VERSION)
@@ -1091,7 +1112,7 @@ def fbx_data_mesh_elements(root, me_obj, scene_data, done_meshes):
# And now, layers!
# Smoothing.
if smooth_type in {'FACE', 'EDGE'}:
if smooth_type in {'FACE', 'EDGE', 'SMOOTH_GROUP'}:
ps_fbx_dtype = np.int32
_map = b""
if smooth_type == 'FACE':
@@ -1106,6 +1127,10 @@ def fbx_data_mesh_elements(root, me_obj, scene_data, done_meshes):
# The mesh has no "sharp_face" attribute, so every face is smooth.
t_ps = np.ones(len(me.polygons), dtype=ps_fbx_dtype)
_map = b"ByPolygon"
elif smooth_type == 'SMOOTH_GROUP':
smoothing_groups = me.calc_smooth_groups(use_bitflags=True, use_boundary_vertices_for_bitflags=True)[0]
t_ps = np.asarray(smoothing_groups, dtype=ps_fbx_dtype)
_map = b"ByPolygon"
else: # EDGE
_map = b"ByEdge"
if t_pvi_edge_indices.size:
@@ -1231,8 +1256,7 @@ def fbx_data_mesh_elements(root, me_obj, scene_data, done_meshes):
# normal_source = me.polygon_normals
# normal_mapping = b"ByPolygon"
case 'CORNER' | 'FACE':
# We have a mix of sharp/smooth edges/faces or custom split normals, so need to get normals from
# corners.
# We have a mix of sharp/smooth edges/faces or custom normals, so need to get normals from corners.
normal_source = me.corner_normals
normal_mapping = b"ByPolygonVertex"
case _:
@@ -1250,13 +1274,19 @@ def fbx_data_mesh_elements(root, me_obj, scene_data, done_meshes):
# FBX SDK documentation says that normals should use IndexToDirect.
elem_data_single_string(lay_nor, b"ReferenceInformationType", b"IndexToDirect")
# Tuple of unique sorted normals and then the index in the unique sorted normals of each normal in t_normal.
# Since we don't care about how the normals are sorted, only that they're unique, we can use the fast unique
# helper function.
t_normal, t_normal_idx = fast_first_axis_unique(t_normal.reshape(-1, 3), return_inverse=True)
# Workaround for Unity FBX import issue where the normals are considered invalid if any normals are
# deduplicated. See #123088.
if normal_mapping == b"ByVertice":
# Write every normal without any deduplication, so the indices array will be [0, 1, 2, ..., n].
t_normal_idx = np.arange(len(t_normal.reshape(-1, 3)), dtype=normal_idx_fbx_dtype)
else:
# Tuple of unique sorted normals and then the index in the unique sorted normals of each normal in t_normal.
# Since we don't care about how the normals are sorted, only that they're unique, we can use the fast unique
# helper function.
t_normal, t_normal_idx = fast_first_axis_unique(t_normal.reshape(-1, 3), return_inverse=True)
# Convert to the type for fbx
t_normal_idx = astype_view_signedness(t_normal_idx, normal_idx_fbx_dtype)
# Convert to the type for fbx
t_normal_idx = astype_view_signedness(t_normal_idx, normal_idx_fbx_dtype)
elem_data_single_float64_array(lay_nor, b"Normals", t_normal)
# Normal weights, no idea what it is.
@@ -1288,13 +1318,15 @@ def fbx_data_mesh_elements(root, me_obj, scene_data, done_meshes):
num_loops = len(me.loops)
t_ln = np.empty(num_loops * 3, dtype=normal_bl_dtype)
# t_lnw = np.zeros(len(me.loops), dtype=np.float64)
uv_names = [uvlayer.name for uvlayer in me.uv_layers]
# Annoying, `me.calc_tangent` errors in case there is no geometry...
if num_loops > 0:
for name in uv_names:
me.calc_tangents(uvmap=name)
for idx, uvlayer in enumerate(me.uv_layers):
name = uvlayer.name
# WARNING: Since tangent layers are recomputed inside the loop, do not directly iterate over the
# uvlayers. Instead, cache their keys (names), and use this cached data inside the loop to compute
# the tangent layers.
uvlayer_names = [uvl.name for uvl in me.uv_layers]
for idx, name in enumerate(uvlayer_names):
# Annoying, `me.calc_tangent` errors in case there is no geometry...
if num_loops > 0:
me.calc_tangents(uvmap=name)
# Loop bitangents (aka binormals).
# NOTE: this is not supported by importer currently.
me.loops.foreach_get("bitangent", t_ln)
@@ -1556,14 +1588,14 @@ def fbx_data_mesh_elements(root, me_obj, scene_data, done_meshes):
lay_tan = elem_empty(layer, b"LayerElement")
elem_data_single_string(lay_tan, b"Type", b"LayerElementTangent")
elem_data_single_int32(lay_tan, b"TypedIndex", 0)
if smooth_type in {'FACE', 'EDGE'}:
if smooth_type in {'FACE', 'EDGE', 'SMOOTH_GROUP'}:
lay_smooth = elem_empty(layer, b"LayerElement")
elem_data_single_string(lay_smooth, b"Type", b"LayerElementSmoothing")
elem_data_single_int32(lay_smooth, b"TypedIndex", 0)
if write_crease:
lay_smooth = elem_empty(layer, b"LayerElement")
elem_data_single_string(lay_smooth, b"Type", b"LayerElementEdgeCrease")
elem_data_single_int32(lay_smooth, b"TypedIndex", 0)
lay_crease = elem_empty(layer, b"LayerElement")
elem_data_single_string(lay_crease, b"Type", b"LayerElementEdgeCrease")
elem_data_single_int32(lay_crease, b"TypedIndex", 0)
if vcolnumber:
lay_vcol = elem_empty(layer, b"LayerElement")
elem_data_single_string(lay_vcol, b"Type", b"LayerElementColor")
@@ -1808,7 +1840,7 @@ def fbx_data_video_elements(root, vid, scene_data):
with open(filepath, 'br') as f:
elem_data_single_bytes(fbx_vid, b"Content", f.read())
except Exception as e:
print("WARNING: embedding file {} failed ({})".format(filepath, e))
print("WARNING: embedding file {:s} failed ({:s})".format(filepath, str(e)))
elem_data_single_bytes(fbx_vid, b"Content", b"")
msetts.embedded_set.add(filepath)
# Looks like we'd rather not write any 'Content' element in this case (see T44442).
@@ -1885,7 +1917,15 @@ def fbx_data_armature_elements(root, arm_obj, scene_data):
valid_idxs = set(bo_vg_idx.values())
vgroups = {vg.index: {} for vg in ob.vertex_groups}
for idx, v in enumerate(me.vertices):
curr_groups = []
for vg in v.groups:
# weird bug, some vertices were reported with corrupted vgroups data (two times the same vertex group!).
# Ensure this does not happen
vgname = ob.vertex_groups[vg.group].name
if vgname in curr_groups:
continue
curr_groups.append(vgname)
if (w := vg.weight) and (vg_idx := vg.group) in valid_idxs:
vgroups[vg_idx][idx] = w
@@ -1923,7 +1963,6 @@ def fbx_data_armature_elements(root, arm_obj, scene_data):
elif bpy.context.scene.arp_ge_force_rest_pose_export:
# Auto-Rig Pro implant to export the bind pose when there are no deformers (skeleton only)
# warning, UE doesn't import skeleton only. Unity does
print("Skeleton only, export bind pose anyway...")
mat_world_obj, mat_world_bones = fbx_data_bindpose_only(root, scene_data, armature=arm_obj, mat_world_armature=mat_world_arm, bones_list=bones)
@@ -2325,7 +2364,7 @@ def fbx_animations_do(scene_data, ref_id, f_start, f_end, start_zero, objects=No
depsgraph = scene_data.depsgraph
force_keying = scene_data.settings.bake_anim_use_all_bones
force_sek = scene_data.settings.bake_anim_force_startend_keying
force_sek_sk = scene_data.settings.bake_anim_force_startend_keying_sk
force_sek_sk = scene_data.settings.bake_anim_force_startend_keying_sk# Auto Rig Pro Implant
gscale = scene_data.settings.global_scale
if objects is not None:
@@ -2375,7 +2414,7 @@ def fbx_animations_do(scene_data, ref_id, f_start, f_end, start_zero, objects=No
is_animated = False
act_sk = me.shape_keys.animation_data.action
if act_sk:
fc = act_sk.fcurves.find(f'key_blocks["{shape.name}"].value')
fc = find_fcurve(act_sk, f'key_blocks["{shape.name}"].value')
is_animated = fc != None
if (is_animated or is_driven) or force_sek_sk:# Auto-Rig Pro Implant
@@ -2652,7 +2691,7 @@ def fbx_animations(scene_data):
# All actions.
if scene_data.settings.bake_anim_use_all_actions:
def validate_actions(act, path_resolve):
for fc in act.fcurves:
for fc in get_action_fcurves(act):
data_path = fc.data_path
if fc.array_index:
data_path = data_path + "[%d]" % fc.array_index
@@ -2746,7 +2785,7 @@ def fbx_animations(scene_data):
for pbo, mat in zip(ob.pose.bones, pbones_matrices):
pbo.matrix_basis = mat.copy()
ob.animation_data.action = org_act
bpy.data.objects.remove(ob_copy)
scene.frame_set(scene.frame_current, subframe=0.0)
@@ -2890,14 +2929,19 @@ def fbx_data_from_scene(scene, depsgraph, settings):
tmp_me = bpy.data.meshes.new_from_object(
ob_to_convert, preserve_all_data_layers=True, depsgraph=depsgraph)
# Usually the materials of the evaluated object will be the same, but modifiers, such as Geometry Nodes,
# can change the materials.
orig_mats = tuple(slot.material for slot in ob.material_slots)
eval_mats = tuple(slot.material.original if slot.material else None
for slot in ob_to_convert.material_slots)
# Usually the materials of the evaluated Object converted to a Mesh will be the same as the original
# Object, but modifiers, such as Geometry Nodes, can change the materials.
orig_mats = [slot.material for slot in ob.material_slots]
eval_mats = list(tmp_me.materials)
if orig_mats != eval_mats:
# Override the default behaviour of getting materials from ob_obj.bdata.material_slots.
ob_obj.override_materials = eval_mats
# An object-linked material slot replaces the material on the data at the slot's index. If applying
# modifiers changes the materials on the data, the object-linked material slot will replace the new
# material at the same index as before.
for i, slot in zip(range(len(eval_mats)), ob.material_slots):
if slot.link == 'OBJECT':
eval_mats[i] = slot.material
# Override the default behavior of getting materials from `ob_obj.bdata.material_slots`.
ob_obj.override_materials = tuple(eval_mats)
elif do_convert:
tmp_me = bpy.data.meshes.new_from_object(ob, preserve_all_data_layers=True, depsgraph=depsgraph)
elif do_copy:
@@ -2926,15 +2970,6 @@ def fbx_data_from_scene(scene, depsgraph, settings):
# store the object and corresponding tmp_me for renaming later
# must be done out of this loop, otherwise tmp_me names are incorrect
obj_tmp_meshes[ob] = tmp_me
'''
# since shape keys are removed after the mesh conversion with modifiers applied,
# then copy them back from the original to the converted object if no changes in the mesh topology were found
if ob_to_convert.data.shape_keys:
if len(ob_to_convert.data.vertices) == len(tmp_me.vertices):
temp_obj = bpy.data.objects.new("temp_obj_for_sk_transfer", tmp_me)# a temp object must be created to add shape keys
copy_shape_keys(ob_to_convert, temp_obj)
bpy.data.objects.remove(temp_obj)
'''
# Change armatures back.
for armature, pose_position in backup_pose_positions:
@@ -3284,7 +3319,7 @@ def fbx_data_from_scene(scene, depsgraph, settings):
idx = _objs_indices[ob_obj] = _objs_indices.get(ob_obj, -1) + 1
# XXX If a mesh has multiple material slots with the same material, they are combined into one slot.
# Even if duplicate materials were exported without combining them into one slot, keeping duplicate
# materials separated does not appear to be common behaviour of external software when importing FBX.
# materials separated does not appear to be common behavior of external software when importing FBX.
mesh_material_indices.setdefault(me, {})[ma] = idx
del _objs_indices
@@ -3717,7 +3752,6 @@ def save_single(operator, scene, depsgraph, filepath="",
).to_4x4()
bone_correction_matrix_inv = bone_correction_matrix.inverted()
media_settings = FBXExportSettingsMedia(
path_mode,
os.path.dirname(bpy.data.filepath), # base_src
@@ -3748,7 +3782,7 @@ def save_single(operator, scene, depsgraph, filepath="",
import bpy_extras.io_utils
print('\nFBX export starting... %r' % filepath)
start_time = time.process_time()
start_time = time.time()
# Generate some data about exported scene...
scene_data = fbx_data_from_scene(scene, depsgraph, settings)
@@ -3791,7 +3825,7 @@ def save_single(operator, scene, depsgraph, filepath="",
if not media_settings.embed_textures:
bpy_extras.io_utils.path_reference_copy(media_settings.copy_set)
print('export finished in %.4f sec.' % (time.process_time() - start_time))
print('export finished in %.4f sec.' % (time.time() - start_time))
return {'FINISHED'}
@@ -3836,11 +3870,13 @@ def defaults_unity3d():
"batch_mode": 'OFF',
}
def arp_save(operator, context,
filepath="",
use_selection=False,
use_visible=False,
use_active_collection=False,
collection="",
batch_mode='OFF',
use_batch_own_dir=False,
**kwargs
@@ -3861,13 +3897,23 @@ def arp_save(operator, context,
if batch_mode == 'OFF':
kwargs_mod = kwargs.copy()
source_collection = None
if use_active_collection:
if use_selection:
ctx_objects = tuple(obj
for obj in context.view_layer.active_layer_collection.collection.all_objects
if obj.select_get())
source_collection = context.view_layer.active_layer_collection.collection
elif collection:
local_collection = bpy.data.collections.get((collection, None))
if local_collection:
source_collection = local_collection
else:
ctx_objects = context.view_layer.active_layer_collection.collection.all_objects
operator.report({'ERROR'}, "Collection '%s' was not found" % collection)
return {'CANCELLED'}
if source_collection:
if use_selection:
ctx_objects = tuple(obj for obj in source_collection.all_objects if obj.select_get())
else:
ctx_objects = source_collection.all_objects
else:
if use_selection:
ctx_objects = context.selected_objects
@@ -3876,9 +3922,12 @@ def arp_save(operator, context,
if use_visible:
ctx_objects = tuple(obj for obj in ctx_objects if obj.visible_get())
# Sort exported objects by their names.
ctx_objects = sorted(ctx_objects, key=lambda ob: ob.name)
# Ensure no Objects are in Edit mode.
# Copy to a tuple for safety, to avoid the risk of modifying ctx_objects while iterating.
for obj in tuple(ctx_objects):
for obj in ctx_objects:
if not ensure_object_not_in_edit_mode(context, obj):
operator.report({'ERROR'}, "%s could not be set out of Edit Mode, so cannot be exported" % obj.name)
return {'CANCELLED'}
@@ -1944,5 +1944,5 @@ FBXImportSettings = namedtuple("FBXImportSettings", (
"use_custom_props", "use_custom_props_enum_as_string",
"nodal_material_wrap_map", "image_cache",
"ignore_leaf_bones", "force_connect_children", "automatic_bone_orientation", "bone_correction_matrix",
"use_prepost_rot", "colors_type",
"use_prepost_rot", "colors_type", "mtl_name_collision_mode",
))
@@ -247,6 +247,11 @@ class ARP_OT_export_fbx_wrap(bpy.types.Operator, ExportHelper):
description="Always add a keyframe at start and end of actions for animated channels",
default=True,
)
bake_anim_force_startend_keying_sk: BoolProperty(
name="Force Start/End Keying for Shape Keys",
description="Always add a keyframe at start and end of actions for animated channels",
default=False,
)
bake_anim_step: FloatProperty(
name="Sampling Rate",
description="How often to evaluate animated values (in frames)",
@@ -2320,6 +2320,7 @@ def fbx_animations_do(scene_data, ref_id, f_start, f_end, start_zero, objects=No
depsgraph = scene_data.depsgraph
force_keying = scene_data.settings.bake_anim_use_all_bones
force_sek = scene_data.settings.bake_anim_force_startend_keying
force_sek_sk = scene_data.settings.bake_anim_force_startend_keying_sk# Auto Rig Pro Implant
gscale = scene_data.settings.global_scale
if objects is not None:
@@ -2359,10 +2360,22 @@ def fbx_animations_do(scene_data, ref_id, f_start, f_end, start_zero, objects=No
if not me.shape_keys.use_relative:
continue
for shape, (channel_key, geom_key, _shape_verts_co, _shape_verts_idx) in shapes.items():
acnode = AnimationCurveNodeWrapper(channel_key, 'SHAPE_KEY', force_key, force_sek, (0.0,))
# Sooooo happy to have to twist again like a mad snake... Yes, we need to write those curves twice. :/
acnode.add_group(me_key, shape.name, shape.name, (shape.name,))
animdata_shapes[channel_key] = (acnode, me, shape)
# Auto-Rig Pro Implant
# Only export animation for driven or keyframed shapes
dr = me.shape_keys.animation_data.drivers.find(f'key_blocks["{shape.name}"].value')
is_driven = dr != None
is_animated = False
act_sk = me.shape_keys.animation_data.action
if act_sk:
fc = act_sk.fcurves.find(f'key_blocks["{shape.name}"].value')
is_animated = fc != None
if (is_animated or is_driven) or force_sek_sk:# Auto-Rig Pro Implant
acnode = AnimationCurveNodeWrapper(channel_key, 'SHAPE_KEY', force_key, force_sek, (0.0,))
# Sooooo happy to have to twist again like a mad snake... Yes, we need to write those curves twice. :/
acnode.add_group(me_key, shape.name, shape.name, (shape.name,))
animdata_shapes[channel_key] = (acnode, me, shape)
animdata_cameras = {}
for cam_obj, cam_key in scene_data.data_cameras.items():
@@ -3523,6 +3536,7 @@ def save_single(operator, scene, depsgraph, filepath="",
bake_anim_step=1.0,
bake_anim_simplify_factor=1.0,
bake_anim_force_startend_keying=True,
bake_anim_force_startend_keying_sk=False,
add_leaf_bones=False,
primary_bone_axis='Y',
secondary_bone_axis='X',
@@ -3609,7 +3623,7 @@ def save_single(operator, scene, depsgraph, filepath="",
bake_anim, bake_anim_use_all_bones, bake_anim_use_nla_strips, bake_anim_use_all_actions,
bake_anim_step, bake_anim_simplify_factor, bake_anim_force_startend_keying,
False, media_settings, use_custom_props, colors_type, prioritize_active_color,
shape_keys_baked_data_dict, export_action_only#Auto-Rig Pro implant
shape_keys_baked_data_dict, export_action_only, bake_anim_force_startend_keying_sk#Auto-Rig Pro implant
)
import bpy_extras.io_utils
@@ -1565,7 +1565,7 @@ FBXExportSettings = namedtuple("FBXExportSettings", (
"bake_anim", "bake_anim_use_all_bones", "bake_anim_use_nla_strips", "bake_anim_use_all_actions",
"bake_anim_step", "bake_anim_simplify_factor", "bake_anim_force_startend_keying",
"use_metadata", "media_settings", "use_custom_props", "colors_type", "prioritize_active_color",
"shape_keys_baked_data_dict", "export_action_only"# Auto-Rig Pro implant
"shape_keys_baked_data_dict", "export_action_only", "bake_anim_force_startend_keying_sk"# Auto-Rig Pro implant
))
# Helper container gathering some data we need multiple times:
@@ -5,15 +5,61 @@ from .version import *
from .sys_print import *
def create_action(actionname):
new_act = bpy.data.actions.new(actionname)
if bpy.app.version >= (5,0,0):
slot = new_act.slots.new('OBJECT', 'Slot 1')
actlay = new_act.layers.new('Layer')
actlay.strips.new(type='KEYFRAME')
return new_act
def assign_armature_action(armature, _action, _slot_idx=0):
if armature.animation_data:
armature.animation_data.action = _action
if bpy.app.version >= (4,4,0) and _action != None:
if len(_action.slots):# debug old files compatibility
armature.animation_data.action_slot = _action.slots[_slot_idx]
def get_action_slot_idx(act, act_slot):
for sloti, slot in enumerate(act.slots):
if slot == act_slot:
return sloti
def get_action_slot_name(act, act_slot_idx):
for sloti, slot in enumerate(act.slots):
if sloti == act_slot_idx:
return slot.name_display
def get_action_slot_frame_range(act, slot_idx):
cb = act.layers[0].strips[0].channelbag(act.slots[slot_idx])
min = float('inf')
max = -min
if cb:
for fc in cb.fcurves:
for kf in fc.keyframe_points:
kf_x = kf.co[0]
if kf_x < min:
min = kf_x
if kf_x > max:
max = kf_x
return min, max
def nla_exit_tweak():
active_obj = bpy.context.active_object
if active_obj.animation_data:
if active_obj.animation_data.use_tweak_mode:
print('NLA is in tweak mode, disable it')
active_action = active_obj.animation_data.action
#active_action = active_obj.animation_data.action
active_obj.animation_data.use_tweak_mode = False
# on exit, the active action is set to None. Bring it back
active_obj.animation_data.action = active_action
# Disable current action restore for now, buggy in some cases. To investigate later
# on exit, the active action is set to None. Bring it back
# active_obj.animation_data.action = active_action
return True
return False
@@ -102,7 +148,7 @@ def bake_anim(frame_start=0, frame_end=10, only_selected=False, bake_bones=True,
matrices_dict = {}
for pbone in armature.pose.bones:
if only_selected and not pbone.bone.select:
if only_selected and not is_pbone_selected(pbone):#pbone.bone.select:
continue
def_matrix = None
@@ -218,18 +264,20 @@ def bake_anim(frame_start=0, frame_end=10, only_selected=False, bake_bones=True,
_self.shape_keys_data[dict_entry] = sk.value
print_progress_bar("Baking phase 1", f-frame_start, frame_end-frame_start)
print_progress_bar("Baking", f-frame_start, frame_end-frame_start, start_percent=0, end_percent=90)
f += sampling_rate
f = round(f, 3)# round frame value because of decimals issues
print("")
#print("")
# set new action
action = None
if new_action:
action = bpy.data.actions.new(new_action_name)
#action = bpy.data.actions.new(new_action_name)
action = create_action(new_action_name)
anim_data = armature.animation_data_create()
anim_data.action = action
#anim_data.action = action
assign_armature_action(armature, action)
else:
action = armature.animation_data.action
@@ -241,17 +289,16 @@ def bake_anim(frame_start=0, frame_end=10, only_selected=False, bake_bones=True,
keyframes[fc_key].extend((frame, value))
# set transforms and store keyframes
# set transforms and store keyframes
if bake_bones:
bone_count = 0
total_bone_count = len(armature.pose.bones)
for pbone in armature.pose.bones:
bone_count += 1
print_progress_bar("Baking phase 2", bone_count, total_bone_count)
print_progress_bar("Baking", bone_count, total_bone_count, start_percent=90, end_percent=100)
if only_selected and not pbone.bone.select:
if only_selected and not is_pbone_selected(pbone):#pbone.bone.select:
continue
euler_prev = None
@@ -260,10 +307,12 @@ def bake_anim(frame_start=0, frame_end=10, only_selected=False, bake_bones=True,
for (f, matrix) in bones_data:
# optional, only keyframe given frames
if keyframes_dict:
if keyframes_dict and len(keyframes_dict):
if not pbone.name in keyframes_dict: continue
keyf_list = keyframes_dict[pbone.name]
if not f in keyf_list:
continue
if not f in keyf_list: continue
pbone.matrix_basis = matrix[pbone.name].copy()
@@ -307,13 +356,16 @@ def bake_anim(frame_start=0, frame_end=10, only_selected=False, bake_bones=True,
for fc_key, key_values in keyframes.items():
data_path, index = fc_key
fcurve = action.fcurves.find(data_path=data_path, index=index)
#fcurve = action.fcurves.find(data_path=data_path, index=index)
fcurve = find_fcurve(action, data_path, fc_index=index)
if new_action == False and fcurve:# for now always remove existing keyframes if overwriting current action, must be driven by constraints only
action.fcurves.remove(fcurve)
fcurve = action.fcurves.new(data_path, index=index, action_group=pbone.name)
#fcurve = action.fcurves.new(data_path, index=index, action_group=pbone.name)
fcurve = create_fcurve(action, data_path, fc_index=index, action_group=pbone.name)
if fcurve == None:
fcurve = action.fcurves.new(data_path, index=index, action_group=pbone.name)
#fcurve = action.fcurves.new(data_path, index=index, action_group=pbone.name)
fcurve = create_fcurve(action, data_path, fc_index=index, action_group=pbone.name)
# set keyframes points
num_keys = len(key_values) // 2
fcurve.keyframe_points.add(num_keys)
@@ -390,35 +442,39 @@ def bake_anim(frame_start=0, frame_end=10, only_selected=False, bake_bones=True,
print("\n")
def get_bone_keyframes_list(pb, act):
def get_bone_keyframes_list(pb, act, all_rot_modes=False, bonename=''):
# return a list containing all keyframes frames of the given pose bone
key_list = []
if pb: bonename = pb.name
# loc
for i in range(0,3):
fc = act.fcurves.find('pose.bones["'+pb.name+'"].location', index=i)
fc = get_action_fcurves(act, as_list=False).find('pose.bones["'+bonename+'"].location', index=i)
if fc:
for k in fc.keyframe_points:
if not k.co[0] in key_list:
key_list.append(k.co[0])
# rot
_range = 3
rot_path = 'rotation_euler'
if pb.rotation_mode == 'QUATERNION':
_range = 4
rot_path = 'rotation_quaternion'
for i in range(0,_range):# rot
fc = act.fcurves.find('pose.bones["'+pb.name+'"].'+rot_path, index=i)
if fc:
for k in fc.keyframe_points:
if not k.co[0] in key_list:
key_list.append(k.co[0])
rot_modes = []
if all_rot_modes:
rot_modes = ['rotation_euler', 'rotation_quaternion']
else:
rot_path = 'rotation_quaternion' if pb.rotation_mode == 'QUATERNION' else 'rotation_euler'
rot_modes.append(rot_path)
for rotmode in rot_modes:
_range = 3 if rotmode == 'rotation_euler' else 4
for i in range(0, _range):
fc = get_action_fcurves(act, as_list=False).find('pose.bones["'+bonename+'"].'+rotmode, index=i)
if fc:
for k in fc.keyframe_points:
if not k.co[0] in key_list:
key_list.append(k.co[0])
# scale
for i in range(0,3):
fc = act.fcurves.find('pose.bones["'+pb.name+'"].scale', index=i)
fc = get_action_fcurves(act, as_list=False).find('pose.bones["'+bonename+'"].scale', index=i)
if fc:
for k in fc.keyframe_points:
if not k.co[0] in key_list:
@@ -4,13 +4,12 @@ from .objects import *
class ARP_BonesData:
custom_bones_list = []
softlink_bones = []
const_interp_bones = []
armature_name = ''
#renamed_bones = {}
def init_values(self):
self.custom_bones_list = []
self.softlink_bones = []
#self.renamed_bones = {}
self.softlink_bones = []
self.const_interp_bones = []
def collect(self, arm_name):
@@ -37,12 +36,8 @@ class ARP_BonesData:
if "softlink" in b.keys():
if not b.name in self.softlink_bones:
self.softlink_bones.append(b.name)
add_stretch_bones(b)
#if 'rename' in b.keys():
# if not b.name in self.renamed_bones:
# self.renamed_bones[b.name] = b['rename']
add_stretch_bones(b)
if 'const_interp' in b.keys():
if not b.name in self.const_interp_bones:
self.const_interp_bones.append(b.name)
@@ -72,12 +67,8 @@ class ARP_BonesData:
if "softlink" in b.keys():
if not b.name in self.softlink_bones:
self.softlink_bones.append(b.name)
add_stretch_bones(b)
#if 'rename' in b.keys():
# if not b.name in self.renamed_bones:
# self.renamed_bones[b.name] = b['rename']
add_stretch_bones(b)
if 'const_interp' in b.keys():
if not b.name in self.const_interp_bones:
self.const_interp_bones.append(b.name)
@@ -121,7 +112,7 @@ def retarget_bone_side(bone_name, target_side, dupli_only=False):#"head.x", "_du
base_name = get_bone_base_name(bone_name)#'head'
new_name = ""
if dupli_only:# we only want to set the dupli target side and preserve the left/right/center end letters
if dupli_only:# we only want to set the dupli ID and preserve the left/right/center side suffix
current_side_letters = bone_name[-2:]#.l
dupli_side = target_side[:-2]#'_dupli_001' or ''
new_name = base_name+dupli_side+current_side_letters #'eyelid'+'_dupli_001'+'.l'
@@ -162,3 +153,17 @@ def duplicate(type=None):
bpy.ops.armature.duplicate_move(ARMATURE_OT_duplicate={}, TRANSFORM_OT_translate={"value": (0.0, 0.0, 0.0), "constraint_axis": (False, False, False),"orient_type": 'LOCAL', "mirror": False, "use_proportional_edit": False, "snap": False, "remove_on_cancel": False, "release_confirm": False})
elif type == "OBJECT":
bpy.ops.object.duplicate(linked=False, mode='TRANSLATION')
def get_bone_children(b):
children_list = []
children_list = get_bone_children_recur(b, list=children_list)
return children_list
def get_bone_children_recur(b, list=None):
if b.children:
for child in b.children:
list.append(child)
get_bone_children_recur(child, list=list)
return list
@@ -100,19 +100,19 @@ def create_edit_bone(bone_name, deform=False, tag=None):
return _b
def select_edit_bone(name, mode=1):
o = bpy.context.active_object
ebone = get_edit_bone(name)
def select_edit_bone(_name, mode=1):
_o = bpy.context.active_object
_ebone = get_edit_bone(_name)
if mode == 1:
o.data.bones.active = o.pose.bones[name].bone
_o.data.bones.active = _o.pose.bones[_name].bone
elif mode == 2:
o.data.edit_bones.active = o.data.edit_bones[name]
o.data.edit_bones.active.select = True
_o.data.edit_bones.active = _o.data.edit_bones[_name]
_o.data.edit_bones.active.select = True
ebone.select_head = True
ebone.select_tail = True
ebone.select = True
_ebone.select_head = True
_ebone.select_tail = True
_ebone.select = True
def delete_edit_bone(editbone):
@@ -10,7 +10,7 @@ def get_selected_pose_bones():
def get_pose_bone(name):
return bpy.context.active_object.pose.bones.get(name)
return bpy.context.active_object.pose.bones.get(name)
def get_custom_shape_scale_prop_name():
@@ -34,27 +34,33 @@ def set_custom_shape_scale(pbone, scale):
pbone.custom_shape_scale = scale
def scale_custom_shape(custom_shape, scale, origin='cog'):
cs_base_name = custom_shape.name
cs_scaled_name = cs_base_name+'_scaled_'+str(scale)
cs_base_scaled = get_object(cs_scaled_name)
def scale_custom_shape(custom_shape, scale, origin='cog', copy=True):
# scale the custom shape vertices and return the object
if copy:
cs_base_name = custom_shape.name
cs_scaled_name = cs_base_name+'_scaled_'+str(scale)
cs_base_scaled = get_object(cs_scaled_name)
if cs_base_scaled:
return cs_base_scaled
# make
cs_base_scaled = duplicate_object(new_name=cs_scaled_name, method='data', obj=custom_shape)
cs_base_scaled.data.name = cs_scaled_name
cog = Vector((0.0,0.0,0.0))
if cs_base_scaled:# if the scaled copy already exists, skip
return cs_base_scaled
# make
cs_base_scaled = duplicate_object(new_name=cs_scaled_name, method='data', obj=custom_shape)
cs_base_scaled.data.name = cs_scaled_name
else:
cs_base_scaled = custom_shape
# get centroid
cog = Vector((0.0,0.0,0.0))
if origin == 'cog':
for v in cs_base_scaled.data.vertices:
cog += v.co
cog = cog/len(cs_base_scaled.data.vertices)
elif origin == 'zero':
cog = Vector((0.0,0.0,0.0))
# scale verts
for v in cs_base_scaled.data.vertices:
scale_vec = cog - v.co
v.co = v.co + (scale_vec * (1-scale))
@@ -62,7 +68,9 @@ def scale_custom_shape(custom_shape, scale, origin='cog'):
return cs_base_scaled
def get_custom_shape_scale(pbone, uniform=True, as_list=False):
def get_custom_shape_scale(pbone, uniform=True, as_list=False):
# returns the custom shape scale setting values
if bpy.app.version >= (3,0,0):
if uniform:
# uniform scale
@@ -79,21 +87,55 @@ def get_custom_shape_scale(pbone, uniform=True, as_list=False):
# pre-Blender 3.0
else:
return pbone.custom_shape_scale
def get_custom_shape_translation(pbone, as_list=False):
if bpy.app.version >= (3,0,0):
if as_list:
return vector_to_list(pbone.custom_shape_translation)
else:
return pbone.custom_shape_translation
else:
return [0,0,0]
def get_custom_shape_rotation(pbone, as_list=False):
if bpy.app.version >= (3,0,0):
if as_list:
return vector_to_list(pbone.custom_shape_rotation_euler)
else:
return pbone.custom_shape_rotation_euler
else:
return [0,0,0]
def set_bone_custom_shape(pbone, cs_name):
cs = get_object(cs_name)
# set the bone custom shape object
# append first the object from the master file if not present in current file
_sel_rig = bpy.context.active_object
cs = None
# is the custom shape already there?
for _o in bpy.data.objects:
if _o.name.split('.')[0] == cs_name:# support multiple rigs per file, duplicate shapes
if _o.parent:
if _o.parent.parent:
if _o.parent.parent == _sel_rig.parent:# parented to the active rig's char_grp empty
cs = _o
break
if cs == None:
# load custom shape
append_from_arp(nodes=[cs_name], type='object')
cs = get_object(cs_name)
elif len(cs.users_collection) == 0:
# custom shape is found, but not in any collection. Fix it
cs_grp = None
for __o in bpy.context.scene.objects:
if __o.name.startswith('cs_grp') and __o.type == 'EMPTY':
if __o.name.startswith('cs_grp') and __o.type == 'EMPTY' and __o.parent == _sel_rig.parent:
cs_grp = __o
break
if cs_grp:
@@ -208,9 +250,12 @@ def get_bone_colors(bone_data, list=False):
return bone_data.color.palette
def set_bone_color(bone_data, bcolors):
def set_bone_color(bone_data, bcolors, assign_only_if_empty=False):
# Blender 4 and higher only
if assign_only_if_empty:# do not change color group if a group is already assigned
if bone_data.color.palette != 'DEFAULT':
return
if type(bcolors) == str:# set the color palette string
bone_data.color.palette = bcolors
else:# set the color lists
@@ -133,4 +133,50 @@ def search_layer_collection(layerColl, collName):
for layer in layerColl.children:
found = search_layer_collection(layer, collName)
if found:
return found
return found
def set_collection_viz(collection_name, show, set_render=False):
# set collection visibility
# returns true if a change was necessary
collection = bpy.data.collections.get(collection_name)
collection_has_switched = False
if collection:
if collection.hide_viewport == show:
collection.hide_viewport = not show
collection_has_switched = True
layer_col = search_layer_collection(bpy.context.view_layer.layer_collection, collection_name)
if layer_col:
if layer_col.hide_viewport == show:
layer_col.hide_viewport = not show
collection_has_switched = True
if set_render:
if collection.hide_render == show:
collection.hide_render = not show
collection_has_switched = True
if collection_has_switched:
return True
def get_obj_collections(obj):
# returns all collections that an object belongs to, including recursive parent collections
collections = set()
def find_parent_collections(collection):
for parent in bpy.data.collections:
if collection.name in parent.children.keys():
collections.add(parent)
find_parent_collections(parent)
# Find all direct collections the object belongs to
for collection in bpy.data.collections:
if obj.name in collection.objects.keys():
collections.add(collection)
find_parent_collections(collection)
return collections
@@ -4,6 +4,38 @@ from mathutils import *
from math import *
def restore_actions_cns_lib(dict):
for pbname in dict:
cns_dict = dict[pbname]
pb = get_pose_bone(pbname)
for cns_name in cns_dict:
lib_action_name = cns_dict[cns_name]
lib_action = None
for _act in bpy.data.actions:
if _act.name == lib_action_name and _act.library:
lib_action = _act
break
if lib_action:
cns = pb.constraints.get(cns_name)
cns.action = lib_action
#print("Replace with lib action:", pbname, lib_action_name)
def save_actions_cns_lib():
actions_cns_lib_dict = {}
for pb in bpy.context.active_object.pose.bones:
cnss = {}
for cns in pb.constraints:
if cns.type == "ACTION":
if cns.action and cns.action.library:
cnss[cns.name] = cns.action.name
actions_cns_lib_dict[pb.name] = cnss
return actions_cns_lib_dict
def enable_constraint(cns, value):
if bpy.app.version >= (3,0,0):
cns.enabled = value
@@ -3,6 +3,24 @@ from .objects import *
from .bone_pose import *
from .context import *
def remove_constraint_drivers(pb_name, cns_name):
_dp_cns = 'pose.bones["'+pb_name+'"].constraints["'+cns_name+'"].'
_to_del = []
# collect constraint drivers
for dra in bpy.context.active_object.animation_data.drivers:
if dra.data_path.startswith(_dp_cns):
_to_del.append((dra.data_path, dra.array_index))
# remove
for drdp in _to_del:
_dp, arridx = drdp[0], drdp[1]
drb = bpy.context.active_object.animation_data.drivers.find(_dp, index=arridx)
if drb:# maybe superfluous
bpy.context.active_object.animation_data.drivers.remove(drb)
print("Removed cns driver:", _dp, arridx)
def add_driver_to_prop(obj, dr_dp, tar_dp, array_idx=-1, exp="var", multi_var=False):
if obj.animation_data == None:
obj.animation_data_create()
@@ -3,6 +3,119 @@ from math import *
from mathutils import *
import numpy as np
def get_axis(matrix, axis):
if axis == 0:
return Vector((matrix[0][0], matrix[1][0], matrix[2][0])).normalized()
elif axis == 1:
return Vector((matrix[0][1], matrix[1][1], matrix[2][1])).normalized()
elif axis == 2:
return Vector((matrix[0][2], matrix[1][2], matrix[2][2])).normalized()
def lerp(a, b, t):
return a + t * (b - a)
def matrix_blend(matrix1, matrix2, blend_factor):
matrix1 = matrix1.to_4x4()
matrix2 = matrix2.to_4x4()
blend_factor = max(0.0, min(1.0, blend_factor))
result = Matrix()
for __i in range(4):
for __j in range(4):
result[__i][__j] = lerp(matrix1[__i][__j], matrix2[__i][__j], blend_factor)
return result
def damped_track(matrix, target, fac=1.0):
# Return the rotation matrix making Y axis looking at a given target point in space
# minimize rotations on other axes
curr_pos = matrix.to_translation()
current_y = Vector((matrix[0][1], matrix[1][1], matrix[2][1])).normalized()
desired_y = Vector(target - curr_pos).normalized()
dot = current_y.dot(desired_y)
if dot > 0.999999:
# Handle the case when vectors are very close or opposite
result = Matrix(matrix)
elif dot < -0.999999:
# Pointing opposite direction, pick current x_axis
up_axis = 0
up = Vector((matrix[0][up_axis], matrix[1][up_axis], matrix[2][up_axis])).normalized()
right = up.cross(desired_y).normalized()
up = desired_y.cross(right).normalized()
rotation_matrix = Matrix((
[up[0], desired_y[0], right[0], 0],
[up[1], desired_y[1], right[1], 0],
[up[2], desired_y[2], right[2], 0],
[0, 0, 0, 1]
))
result = rotation_matrix
else:
# Normal case: calculate minimal rotation
axis = current_y.cross(desired_y).normalized()
angle = current_y.angle(desired_y)
rotation = Matrix.Rotation(angle, 4, axis)
# Apply rotation to original matrix while preserving translation
result = rotation @ matrix
result.translation = curr_pos
if fac != 1.0:
return matrix_blend(matrix.copy(), result, fac)
else:
return result
def lookat_up(matrix, target, up_axis):
# Return the rotation matrix making Y axis looking at a given target point in space
# using a given up vector
eye = matrix.to_translation()
forward = Vector(target-eye).normalized()
up = up_axis.normalized()
right = up.cross(forward).normalized()
up = forward.cross(right).normalized()
rotation_matrix = Matrix((
[up[0], forward[0], right[0], 0],
[up[1], forward[1], right[1], 0],
[up[2], forward[2], right[2], 0],
[0, 0, 0, 1]
))
return rotation_matrix
def lookat_up_camera(matrix, target, up_axis):
# Return the rotation matrix making -Z axis looking at a given target point in space
# using Y as up vector
eye = matrix.to_translation()
forward = Vector(eye - target).normalized() # -Z direction, so reverse the vector
up = up_axis.normalized()
right = up.cross(forward).normalized()
up = forward.cross(right).normalized()
rotation_matrix = Matrix((
[right[0], up[0], forward[0], 0], # -forward for -Z
[right[1], up[1], forward[1], 0],
[right[2], up[2], forward[2], 0],
[0, 0, 0, 1]
))
return rotation_matrix
def compare_transform(transf1, transf2):
for i, j in enumerate(transf1):
if j == transf2[i]:
@@ -11,31 +124,78 @@ def compare_transform(transf1, transf2):
return False
return True
def catmull_rom_spline(p0, p1, p2, p3, num_points=20):
t = np.linspace(0, 1, num_points)
t2 = t**2
t3 = t**3
f0 = -0.5*t3 + t2 - 0.5*t
f1 = 1.5*t3 - 2.5*t2 + 1
f2 = -1.5*t3 + 2.0*t2 + 0.5*t
f3 = 0.5*t3 - 0.5*t2
return np.outer(f0, p0) + np.outer(f1, p1) + np.outer(f2, p2) + np.outer(f3, p3)
def generate_catrom_curve(points, num_points=20, closed=True):
points = np.array(points)
num_key_points = len(points)
curve = []
for i in range(num_key_points):
p0 = points[i - 1]
if not closed and i == 0:
p0 = points[i]
p1 = points[i]
p2 = points[(i + 1) % num_key_points]
if not closed and i >= num_key_points-1:
p2 = points[i]
p3 = points[(i + 2) % num_key_points]
if not closed and i >= num_key_points-2:
p3 = points[i]
segment = catmull_rom_spline(p0, p1, p2, p3, num_points)
curve.append(segment)
_pts = np.vstack(curve).tolist()
_pts.pop(len(_pts)-1)# remove double cyclic point
vec_list = [Vector((i[0], i[1], i[2])) for i in _pts]
return vec_list
def resample_curve(coords, length=1.0, amount=5, symmetrical=True, generate_normals=False):
def resample_curve(coords, length=1.0, amount=5, symmetrical=True, generate_normals=False, offset_first=True, keep_tip=False):
# resample a given set of points belonging to a curve
# only works by reduction
resampled_coords = []
dist_sum = 0.0
dist = length/amount
dist = length/(amount-1) if keep_tip else length/amount
for i, coord in enumerate(coords):
# special case, since we need symmetrical positioning,
# the first coord must be positioned half-distance
if len(resampled_coords) == 0 and symmetrical:
if coord == coords[0]:
if coord == coords[0]:# skip first
continue
p_prev = coords[i-1]
p_prev = coords[i-1]# average second
cur_dist = (coord-p_prev).magnitude
dist_sum += cur_dist
if dist_sum >= dist/2:
dist_sum = 0.0
resampled_coords.append(coord.copy())
resampled_coords.append(coord.copy())
else:
p_prev = coords[i-1]
if i == 0:
if offset_first:
p_prev = coords[i-1]
else:
resampled_coords.append(coord.copy())
continue
else:
p_prev = coords[i-1]
cur_dist = (coord-p_prev).magnitude
dist_sum += cur_dist
@@ -47,10 +207,8 @@ def resample_curve(coords, length=1.0, amount=5, symmetrical=True, generate_norm
# In case of precision error, the last coord did not fit in.
# Make sure to include it
#print('resampled_coords', len(resampled_coords), 'amount', amount)
# Make sure to include it
#print('resampled_coords', len(resampled_coords), 'amount', amount)
if len(resampled_coords) == amount - 1:
#print('Curve resampling error, add last coord as tip coord')
tip_coord = coords[len(coords)-2]
@@ -139,29 +297,27 @@ def generate_nurbs_curve(points, degree=3, num_points=100):
if len(points) < degree + 1:
raise ValueError("Number of points should be at least degree + 1.")
# Convert control points to numpy array
control_points = np.array(points)
# Calculate the number of knots needed for a closed curve
# calculate the number of knots needed for a closed curve
num_knots = len(control_points) + degree + 1
# Create a list of equally spaced parameter values for the control points
# create a list of equally spaced parameter values for the control points
parameter_values = np.linspace(0, 1, len(control_points))
# Compute the knot vector (closed curve)
# compute the knot vector (closed curve)
knots = np.zeros(num_knots)
knots[degree:-degree] = np.linspace(0, 1, num_knots - 2*degree)
knots[-degree:] = 1
# Evaluate the NURBS curve at 'num_points' points
# evaluate the NURBS curve at 'num_points' points
u_new = np.linspace(0, 1, num_points)
x = np.zeros(num_points)
y = np.zeros(num_points)
z = np.zeros(num_points)
for i in range(len(u_new)):
if i == len(u_new)-1:# the last one must be set manually, sigh
if i == len(u_new)-1:# the last one must be set manually
x[i] += control_points[len(points)-1, 0]
y[i] += control_points[len(points)-1, 1]
z[i] += control_points[len(points)-1, 2]
@@ -182,9 +338,26 @@ def generate_nurbs_curve(points, degree=3, num_points=100):
def signed_angle(vector_u, vector_v, normal):
normal = normal.normalized()
a = vector_u.angle(vector_v)
if vector_u.cross(vector_v).angle(normal) < 1:
a = -a
vector_u = vector_u.normalized()
vector_v = vector_v.normalized()
a = vector_u.angle(vector_v)
cross = vector_u.cross(vector_v)
# parallel vectors case
if cross.length < 1e-6:
if vector_u.dot(vector_v) > 0:
return 0.0
else:
arbitrary = vector_u.orthogonal()
if arbitrary.dot(normal) < 0:
return -a
else:
return a
else:
if cross.angle(normal) < 1:
a = -a
return a
@@ -1,5 +1,4 @@
import bpy, bmesh
from .objects import *
from .bone_data import *
from .version import *
@@ -34,6 +33,19 @@ def overwrite_vgroup(obj, vgroup, new_vgname):
vgroup.name = new_vgname
def export_mesh_data(mesh):
_verts = [(v.co[0], v.co[1], v.co[2]) for v in mesh.vertices]
_edges = [(edge.vertices[0], edge.vertices[1]) for edge in mesh.edges]
_faces = []
for face in mesh.polygons:
face_verts = []
for v in face.vertices:
face_verts.append(v)
_faces.append(face_verts)
return _verts, _edges, _faces
def create_mesh_data(mesh_name, verts, edges, faces):
# create an new mesh data given verts, edges and faces data
new_mesh = bpy.data.meshes.new(name=mesh_name)
@@ -42,11 +54,18 @@ def create_mesh_data(mesh_name, verts, edges, faces):
def create_object_mesh(obj_name, verts, edges, faces):
# create an new object with given mesh data
shape_mesh = create_mesh_data(obj_name, verts, edges, faces)
# create object
shape = bpy.data.objects.new(obj_name, shape_mesh)
return shape
def create_object_mesh_empty(obj_name):
_mesh = bpy.data.meshes.new(name=obj_name)
_obj = bpy.data.objects.new(name=obj_name, object_data=_mesh)
bpy.context.collection.objects.link(_obj)
return _obj
def transfer_shape_keys(source_obj, target_obj):
if source_obj == None or target_obj == None:
@@ -92,9 +111,10 @@ def transfer_shape_keys(source_obj, target_obj):
def transfer_shape_keys_deformed(source_obj, target_obj, apply_mods=False):
if source_obj == None or target_obj == None:
return
failed_sk = []
#print("Disable mods...")
# disable all non-armature modifiers to solve issues when baking the mesh
disabled_mod = {}
if apply_mods == False:
@@ -116,16 +136,17 @@ def transfer_shape_keys_deformed(source_obj, target_obj, apply_mods=False):
if source_obj.data.shape_keys == None:
return
source_shape_keys = source_obj.data.shape_keys.key_blocks
basis_index = 0
# pin the Basis key
#print("Pin Basis...")
source_obj.active_shape_key_index = basis_index
source_obj.show_only_shape_key = True
#print("Update Depsgraph...")
bpy.context.evaluated_depsgraph_get().update()
# store the vert coords in basis shape keys
#print("Store vert coords...")
mesh_baked = bmesh.new()
if bpy.app.version < (2,93,0):
mesh_baked.from_object(source_obj, bpy.context.evaluated_depsgraph_get(), deform=True, face_normals=False)
@@ -139,8 +160,10 @@ def transfer_shape_keys_deformed(source_obj, target_obj, apply_mods=False):
if 'mesh_baked' in locals():
del mesh_baked
source_shape_keys = source_obj.data.shape_keys.key_blocks
# store the vert coords in basis shape keys
#print("Iterate through shapes...")
for sk_index, sk in enumerate(source_shape_keys):
if sk_index == basis_index:
@@ -226,7 +249,7 @@ def transfer_shape_keys_deformed(source_obj, target_obj, apply_mods=False):
# restore disabled modifiers
for objname in disabled_mod:
ob = get_object(objname)
ob = bpy.data.objects.get(objname)
for modname in disabled_mod[objname]:
ob.modifiers[modname].show_viewport = True
@@ -377,13 +400,13 @@ def transfer_weight_verts(object=None, dict=None, list=None, target_group_name=N
for vert in object.data.vertices:
if len(vert.groups) == 0:
continue
for grp in vert.groups:
try:
grp_name = object.vertex_groups[grp.group].name
except:
continue
transfer_weight(object=object, vertice=vert, vertex_weight=grp.weight, group_name=grp_name, dict=dict, list=list, target_group_name=target_group_name, use_side=use_side)
# operate on a copy, creating vgroups in the same loop corrupts pointers on Mac OS
grps_dict = {object.vertex_groups[grp.group].name: grp.weight for grp in vert.groups if grp.group < len(object.vertex_groups)}# sic, there may be corrupted vgroup ID anyway
for grp_name in grps_dict:
weight = grps_dict[grp_name]
transfer_weight(object=object, vertice=vert, vertex_weight=weight, group_name=grp_name, dict=dict, list=list, target_group_name=target_group_name, use_side=use_side)
def transfer_weight(object=None, vertice=None, vertex_weight=None, group_name=None, dict=None, list=None, target_group_name=None, use_side=False):
@@ -419,6 +442,7 @@ def transfer_weight_prefix_verts(object=None, prefix='', tar_grp_base_name=''):
for vert in object.data.vertices:
if len(vert.groups) == 0:
continue
for grp in vert.groups:
try:
grp_name = object.vertex_groups[grp.group].name
@@ -436,7 +460,7 @@ def transfer_weight_prefix(object=None, vertice=None, vertex_weight=None, group_
object.vertex_groups[tar_group_name].add([vertice.index], vertex_weight, 'ADD')
def copy_vgroup(object=None, dict=None, use_side=False):
def copy_vgroup(object=None, dict=None, use_side=False, copy_method='OPERATOR'):
# dict = {'arm_stretch': ['c_arm_twist_offset'],...}
vgroups_names_copy = [i.name for i in object.vertex_groups]
@@ -458,10 +482,26 @@ def copy_vgroup(object=None, dict=None, use_side=False):
object.vertex_groups.remove(tar_grp)
# copy source group
object.vertex_groups.active_index = vg.index
bpy.ops.object.vertex_group_copy()
copy_idx = object.vertex_groups.active_index
copy_grp = object.vertex_groups[copy_idx]
copy_grp.name = tar_grp_name+side
if copy_method == 'OPERATOR':
bpy.ops.object.vertex_group_copy()
copy_idx = object.vertex_groups.active_index
copy_grp = object.vertex_groups[copy_idx]
copy_grp.name = tar_grp_name+side
elif copy_method == 'RAW':
copy_grp = object.vertex_groups.new(name=tar_grp_name+side)
for vert in object.data.vertices:
try:
weight = vg.weight(vert.index)
copy_grp.add([vert.index], weight, 'REPLACE')
except:
continue
def copy_vertex_group(obj, source_vg_name, new_vg_name):
source_vg = obj.vertex_groups.get(source_vg_name)
def multiply_weight(object=None, vertice=None, vertex_weight=None, group_name=None, dict=None):
@@ -495,4 +535,45 @@ def clamp_weights(object=None, vertice=None, vertex_weight=None, group_name=None
target_weight = grp.weight
def_weight = min(vertex_weight, target_weight)
object.vertex_groups[group_name].add([vertice.index], def_weight, 'REPLACE')
object.vertex_groups[group_name].add([vertice.index], def_weight, 'REPLACE')
def count_closed_mesh_elements(obj):
# count the amount of islands/closed elements contained in the same mesh object
if obj.type != 'MESH':
print("Cannot count mesh elements, not a mesh:", obj.name)
return 0
# Create a BMesh from the object's mesh data
bm = bmesh.new()
bm.from_mesh(obj.data)
# Find connected components
islands = []
visited_faces = set()
for face in bm.faces:
if face.index not in visited_faces:
# Perform a breadth-first search (BFS) to find all connected faces
island = set()
stack = [face]
while stack:
current_face = stack.pop()
if current_face.index not in visited_faces:
visited_faces.add(current_face.index)
island.add(current_face.index)
# Add neighboring faces to the stack
for edge in current_face.edges:
for linked_face in edge.link_faces:
if linked_face.index not in visited_faces:
stack.append(linked_face)
islands.append(island)
# Free the BMesh
bm.free()
# Count the islands
return len(islands)
@@ -1,7 +1,14 @@
import bpy, os
from .bone_edit import *
from .context import *
from .armature import *
from .collections import *
import bpy, os, bmesh, sys, ast
def create_empty(empty_name, empty_pos):
empty = bpy.data.objects.new(empty_name, None)
bpy.context.collection.objects.link(empty)
empty.location = empty_pos
def get_object_boundaries(obj):
@@ -37,93 +44,140 @@ def get_object_boundaries(obj):
return {'front':front, 'back':back, 'left':left, 'right':right, 'top':top, 'bottom':bottom}
def append_from_arp(nodes=None, type=None):
def append_from_arp(nodes=None, type=None, from_raw_file=False, new_name=None):
context = bpy.context
scene = context.scene
addon_directory = os.path.dirname(os.path.abspath(__file__))
addon_directory = os.path.dirname(addon_directory)
addon_directory = os.path.dirname(addon_directory)
filepath = addon_directory + "/armature_presets/" + "master.blend"
if type == "object":
# Clean the cs_ materials names (avoid .001, .002...)
for mat in bpy.data.materials:
if mat.name[:3] == "cs_":
if mat.name[-3:].isdigit() and bpy.data.materials.get(mat.name[:-4]) == None:
mat.name = mat.name[:-4]
# make a list of current custom shapes objects in the scene for removal later
cs_objects = [obj.name for obj in bpy.data.objects if obj.name.startswith('cs_')]
# Load the objects data in the file
with bpy.data.libraries.load(filepath, link=False) as (data_from, data_to):
data_to.objects = [name for name in data_from.objects if name in nodes]
if from_raw_file:
# get cs_grp
cs_grp = None
for _ob in bpy.context.scene.objects:
if _ob.name.startswith('cs_grp') and _ob.type == 'EMPTY':
cs_grp = _ob
print('FOUND cs_grp', cs_grp.name)
break
def _create_mesh_data(mesh_name, verts, edges, faces):
# create an new mesh data given verts, edges and faces data
new_mesh = bpy.data.meshes.new(name=mesh_name)
new_mesh.from_pydata(verts, edges, faces)
return new_mesh
# Add the objects in the scene
for obj in data_to.objects:
if obj:
# link
bpy.context.scene.collection.objects.link(obj)
def _create_object_mesh(obj_name, verts, edges, faces):
# create an new object with given mesh data
shape_mesh = _create_mesh_data(obj_name, verts, edges, faces)
shape = bpy.data.objects.new(obj_name, shape_mesh)
return shape
for cs_name in nodes:
filepath = addon_directory+'/src/cs/'+cs_name+'.py'
file = open(filepath, 'r') if sys.version_info >= (3, 11) else open(filepath, 'rU')
file_lines = file.readlines()
dict = ast.literal_eval(file_lines[0])
file.close()
new_cs_name = new_name if new_name != None else cs_name
cs_obj = _create_object_mesh(new_cs_name, dict['verts'], dict['edges'], dict['faces'])
# get cs_grp
cs_grp = None
for _ob in bpy.context.scene.objects:
if _ob.name.startswith('cs_grp') and _ob.type == 'EMPTY':
cs_grp = _ob
break
# apply existing scene material if exists
if len(obj.material_slots):
mat_name = obj.material_slots[0].name
found_mat = None
for mat in bpy.data.materials:
if mat.name == mat_name[:-4]: # substract .001, .002...
found_mat = mat.name
break
# assign existing material if already in file and delete the imported one
if found_mat:
obj.material_slots[0].material = bpy.data.materials[found_mat]
bpy.data.materials.remove(bpy.data.materials[mat_name], do_unlink=True)
# If we append a custom shape
if obj.name.startswith('cs_') or 'c_sphere' in obj.name:
if cs_grp:
# parent the custom shape
obj.parent = cs_grp
# assign to new collection
assigned_collections = []
for collec in cs_grp.users_collection:
collec.objects.link(obj)
assigned_collections.append(collec)
if len(assigned_collections):
# remove previous collections
for i in obj.users_collection:
if not i in assigned_collections:
i.objects.unlink(obj)
# and the scene collection
try:
bpy.context.scene.collection.objects.unlink(obj)
except:
pass
# If we append other objects,
# find added/useless custom shapes and delete them
# parent to cs_grp, collec
if cs_grp:
cs_obj.parent = cs_grp
for collec in cs_grp.users_collection:
collec.objects.link(cs_obj)
else:
for obj in bpy.data.objects:
if obj.name.startswith('cs_'):
if not obj.name in cs_objects:
bpy.data.objects.remove(obj, do_unlink=True)
bpy.context.scene.collection.objects.link(cs_obj)
else:
filepath = addon_directory + "/armature_presets/" + "cs.blend"
# Clean the cs_ materials names (avoid .001, .002...)
for mat in bpy.data.materials:
if mat.name.startswith("cs_"):
if mat.name[-3:].isdigit() and bpy.data.materials.get(mat.name[:-4]) == None:
mat.name = mat.name[:-4]
if 'obj' in locals():
del obj
# make a list of current custom shapes objects in the scene for removal later
cs_objects = [obj.name for obj in bpy.data.objects if obj.name.startswith('cs_')]
# Load the objects data in the file
with bpy.data.libraries.load(filepath, link=False) as (data_from, data_to):
data_to.objects = [name for name in data_from.objects if name in nodes]
# get cs_grp
cs_grp = None
for _ob in bpy.context.scene.objects:
if _ob.name.startswith('cs_grp') and _ob.type == 'EMPTY':
cs_grp = _ob
#print('FOUND cs_grp', cs_grp.name)
break
# Add the objects in the scene
for obj in data_to.objects:
if obj:
# link
bpy.context.scene.collection.objects.link(obj)
# apply existing scene material if exists
if len(obj.material_slots):
mat_name = obj.material_slots[0].name
found_mat = None
for mat in bpy.data.materials:
if mat.name == mat_name[:-4]: # substract .001, .002...
found_mat = mat.name
break
# assign existing material if already in file and delete the imported one
if found_mat:
obj.material_slots[0].material = bpy.data.materials[found_mat]
bpy.data.materials.remove(bpy.data.materials[mat_name], do_unlink=True)
# If we append a custom shape
if obj.name.startswith('cs_') or 'c_sphere' in obj.name:
if cs_grp:
# parent the custom shape
obj.parent = cs_grp
# assign to new collection
assigned_collections = []
for collec in cs_grp.users_collection:
try:
collec.objects.link(obj)
assigned_collections.append(collec)
except:# already in collec
pass
if len(assigned_collections):
# remove previous collections
for i in obj.users_collection:
if not i in assigned_collections:
i.objects.unlink(obj)
# and the scene collection
try:
bpy.context.scene.collection.objects.unlink(obj)
except:
pass
# If we append other objects,
# find added/useless custom shapes and delete them
else:
for obj in bpy.data.objects:
if obj.name.startswith('cs_'):
if not obj.name in cs_objects:
bpy.data.objects.remove(obj, do_unlink=True)
if 'obj' in locals():
del obj
if type == "text":
filepath = addon_directory + "/armature_presets/" + "master.blend"
# Load the objects data in the file
with bpy.data.libraries.load(filepath, link=False) as (data_from, data_to):
data_to.texts = [name for name in data_from.texts if name in nodes]
@@ -131,6 +185,8 @@ def append_from_arp(nodes=None, type=None):
bpy.context.evaluated_depsgraph_get().update()
if type == "font":
filepath = addon_directory + "/armature_presets/" + "master.blend"
# Load the data in the file
with bpy.data.libraries.load(filepath, link=False) as (data_from, data_to):
data_to.fonts = [name for name in data_from.fonts if name in nodes]
@@ -186,10 +242,31 @@ def delete_object(obj):
bpy.data.objects.remove(obj, do_unlink=True)
def set_active_object(object_name):
def set_active_object(object_name, force_display=False):
# set active/select object
# return the collections that were hidden and are now displayed, if force_display=True
bpy.context.view_layer.objects.active = bpy.data.objects[object_name]
enabled_collections = []
object_unhidden = False
if force_display:
# ensure the object is visible, and collections too
_obj_cols = get_obj_collections(bpy.data.objects.get(object_name))
for _obj_col in _obj_cols:
has_switched = set_collection_viz(_obj_col.name, True)
if has_switched:
enabled_collections.append(_obj_col.name)
# unhide object
if is_object_hidden(bpy.data.objects[object_name]):
unhide_object(bpy.data.objects[object_name])
object_unhidden = True
bpy.data.objects[object_name].select_set(state=True)
if force_display:
return enabled_collections, object_unhidden
def hide_object(obj_to_set):
try:# object may not be in current view layer
@@ -223,23 +300,38 @@ def unhide_object(obj_to_set):
def duplicate_object(new_name="", method='operator', obj=None):
if method == 'operator':
if method == 'operator':
try:
bpy.ops.object.duplicate(linked=False, mode='TRANSLATION')
except:
bpy.ops.object.duplicate('TRANSLATION', False)
if new_name != "":
bpy.context.active_object.name = new_name
elif method == 'data':
if obj:
obj_dupli = obj.copy()
_obj_dupli = obj.copy()
for col in obj.users_collection:
col.objects.link(obj_dupli)
obj_dupli.data = obj_dupli.data.copy()
obj_dupli.name = new_name
return obj_dupli
else:
print('Cannot duplicate object, not found')
col.objects.link(_obj_dupli)
_obj_dupli.data = _obj_dupli.data.copy()
if new_name != "":
_obj_dupli.name = new_name
return _obj_dupli
else:
print('Cannot duplicate object, not found')
elif method == 'raw':
if obj:
new_mesh = bpy.data.meshes.new(new_name)
bm = bmesh.new()
bm.from_mesh(obj.data)
_obj_dupli = bpy.data.objects.new(new_name, new_mesh)
bm.to_mesh(new_mesh)
bm.free()
for col in obj.users_collection:
col.objects.link(_obj_dupli)
new_mesh.update()
return _obj_dupli
def delete_children(passed_node, type):
@@ -1,9 +1,14 @@
import sys
def print_progress_bar(job_title, progress, length):
def print_progress_bar(job_title, progress, length, start_percent=0, end_percent=100):
if length != 0:
progress = int((progress * 100) / length)
else:
progress = 100
# optional remap
progress = start_percent + progress/(100 / (end_percent-start_percent))
sys.stdout.write("\r " + job_title + " %d%%" % progress)
sys.stdout.flush()
try:# unknown compatibility breakage with flush() in a rare case reported by a user
sys.stdout.flush()
except: pass
@@ -1,6 +1,15 @@
import bpy
from math import *
from mathutils import *
def propmatrix_to_matrix(arr):
if bpy.app.version >= (5,0,0):
return Matrix([arr[0:4], arr[4:8], arr[8:12], arr[12:16]])
else:
return Matrix(arr)
def vectorize3(list):
return Vector((list[0], list[1], list[2]))
@@ -1,5 +1,6 @@
import bpy
import addon_utils
import addon_utils
from bpy_extras import anim_utils
from .. import auto_rig_datas as ard
from .collections import *
from .armature import *
@@ -18,6 +19,18 @@ class ARP_blender_version:
blender_version = ARP_blender_version()
def is_pbone_selected(pb):
if bpy.app.version >= (5,0,0):
return pb.select
else: return pb.bone.select
def select_pb_db(pbone, state=True):
if bpy.app.version >= (5,0,0):
pbone.select = state
else: pbone.bone.select = state
def is_proxy(obj):
# proxy atttribute removed in Blender 3.3
if 'proxy' in dir(obj):
@@ -70,7 +83,7 @@ def convert_drivers_cs_to_xyz(armature):
# tag in prop
armature.data["arp_updated_3.0"] = True
print("Converted custom shape scale drivers to xyz")
#print("Converted custom shape scale drivers to xyz")
def convert_armature_layers_to_collection(armature):
@@ -90,7 +103,7 @@ def convert_armature_layers_to_collection(armature):
for col_name in col_names:#armature.data.collections_all:
_col = get_armature_collections(armature).get(col_name)
if _col == None:# debug...
print(" Collec is None, error when removing collection! Exit")
#print(" Collec is None, error when removing collection! Exit")
continue
# remove deprecated bones color groups
@@ -103,11 +116,13 @@ def convert_armature_layers_to_collection(armature):
_col.name = 'color_'+_col.name
# rename bones collections
print('Rename collections...')
#print('Rename collections...')
get_armature_collections(armature).update()
for i, _col in enumerate(get_armature_collections(armature)):
if _col == None:#Debug, for some reasons Mac throws a None collection when appending the Bird armature
continue
if _col.name.startswith('Layer '):
lidx = int(_col.name.split(' ')[1])-1
@@ -142,7 +157,7 @@ def convert_armature_layers_to_collection(armature):
for col_name in ard.layer_col_map:
if col_name[0].isupper():# only main collections with capital letters
if get_armature_collections(armature).get(col_name) == None:
print('Create collection', col_name)
#print('Create collection', col_name)
armature.data.collections.new(col_name)
sort_armature_collections(armature)
@@ -281,7 +296,7 @@ def invert_angle_with_blender_versions(angle=None, bone=False, axis=None):
if invert:
angle = -angle
return angle
return angle
def disable_bone_inherit_scale(editbone):
@@ -298,6 +313,50 @@ def enable_bone_inherit_scale(editbone):
editbone.use_inherit_scale = True
def find_fcurve(act, dp, slot_idx=0, fc_index=0):
if bpy.app.version >= (5,0,0):
cb = anim_utils.action_get_channelbag_for_slot(act, act.slots[slot_idx])
if cb == None: return None
return cb.fcurves.find(dp, index=fc_index)
else:
return act.fcurves.find(data_path=dp, index=fc_index)
def create_fcurve(act, dp, slot_idx=0, fc_index=0, action_group=''):
if bpy.app.version >= (5,0,0):
cb = anim_utils.action_ensure_channelbag_for_slot(act, act.slots[slot_idx])
return cb.fcurves.new(dp, index=fc_index, group_name=action_group)
else:
return act.fcurves.new(dp, index=fc_index, action_group=action_group)
def delete_fcurve(act, fc, slot_idx=0):
if bpy.app.version >= (5,0,0):
cb = anim_utils.action_ensure_channelbag_for_slot(act, act.slots[slot_idx])
cb.fcurves.remove(fc)
else:
act.fcurves.remove(fc)
def get_action_fcurves(act, slot_idx=0, as_list=True):
if bpy.app.version >= (4,4,0):
if bpy.app.version >= (5,0,0):
cb = anim_utils.action_ensure_channelbag_for_slot(act, act.slots[slot_idx])
else:
cb = act.layers[0].strips[0].channelbag(act.slots[slot_idx])
if cb:
if as_list:
return [_fc for _fc in cb.fcurves if _fc != None]# check for None curve, debug
else:
return cb.fcurves
else:
print("No fcurves found in this slot:", slot_idx)
return []
else:
return act.fcurves
def get_prefs():
if bpy.app.version >= (4,2,0):
return bpy.context.preferences.addons[__package__[:-8]].preferences
@@ -0,0 +1,127 @@
from mathutils import Matrix
matrix_coords_l = {'c_pinky1_base.l': Matrix(((-0.1321406364440918, 0.543448269367218, 0.8289769887924194, 0.4933325946331024),
(-0.9845015406608582, -0.16924019157886505, -0.04598342627286911, -0.10675017535686493),
(0.11530676484107971, -0.8222053647041321, 0.5573893189430237, 1.0162558555603027),
(0.0, 0.0, 0.0, 1.0))), 'c_pinky1.l': Matrix(((0.3489053249359131, 0.41421136260032654, 0.8406510353088379, 0.5218664407730103),
(-0.9321953058242798, 0.06120216101408005, 0.3567441999912262, -0.11563616245985031),
(0.0963180884718895, -0.9081208109855652, 0.40747979283332825, 0.9730858206748962),
(0.0, 0.0, 0.0, 1.0))), 'c_pinky2.l': Matrix(((0.4049581289291382, 0.5478314161300659, 0.7320449352264404, 0.5371885895729065),
(-0.894914984703064, 0.07334459573030472, 0.4401679039001465, -0.11337228119373322),
(0.1874462366104126, -0.8333672881126404, 0.5199640989303589, 0.9394932985305786),
(0.0, 0.0, 0.0, 1.0))), 'c_pinky3.l': Matrix(((0.4344693124294281, 0.6926425099372864, 0.5757458209991455, 0.5472332239151001),
(-0.8724089860916138, 0.16470646858215332, 0.4601897597312927, -0.11202748119831085),
(0.2239179015159607, -0.7022236585617065, 0.6758272647857666, 0.9242132306098938),
(0.0, 0.0, 0.0, 1.0))), 'c_ring1_base.l': Matrix(((-0.24063082039356232, 0.573573112487793, 0.7830138802528381, 0.4974002540111542),
(-0.9458432197570801, -0.3196682929992676, -0.05650714412331581, -0.11789528280496597),
(0.21789370477199554, -0.7542055249214172, 0.619432270526886, 1.0223618745803833),
(0.0, 0.0, 0.0, 1.0))), 'c_ring1.l': Matrix(((-0.05169704183936119, 0.4065243601799011, 0.9121761918067932, 0.5282756686210632),
(-0.9341012239456177, -0.34276989102363586, 0.09982069581747055, -0.1351030468940735),
(0.3532460331916809, -0.8469040393829346, 0.3974550664424896, 0.9817630052566528),
(0.0, 0.0, 0.0, 1.0))), 'c_ring2.l': Matrix(((0.07245957851409912, 0.6446757316589355, 0.7610143423080444, 0.5470014214515686),
(-0.9164383411407471, -0.25805044174194336, 0.3058597445487976, -0.15089207887649536),
(0.3935604691505432, -0.7195852994918823, 0.5721074342727661, 0.942751944065094),
(0.0, 0.0, 0.0, 1.0))), 'c_ring3.l': Matrix(((0.2263454794883728, 0.8068910241127014, 0.5456141233444214, 0.5622686743736267),
(-0.8494088053703308, -0.11064670979976654, 0.5160052180290222, -0.15700331330299377),
(0.47673046588897705, -0.5802450180053711, 0.6603360176086426, 0.9257106781005859),
(0.0, 0.0, 0.0, 1.0))), 'c_middle1_base.l': Matrix(((-0.41135188937187195, 0.5918521881103516, 0.6931815147399902, 0.49848878383636475),
(-0.8688518404960632, -0.4844593107700348, -0.10195799171924591, -0.12799011170864105),
(0.2754744291305542, -0.6442126035690308, 0.7135152816772461, 1.028594970703125),
(0.0, 0.0, 0.0, 1.0))), 'c_middle1.l': Matrix(((-0.29555466771125793, 0.41192948818206787, 0.8619521856307983, 0.5332725048065186),
(-0.860168993473053, -0.5072991251945496, -0.05250336974859238, -0.15646222233772278),
(0.4156401455402374, -0.756942093372345, 0.5042637586593628, 0.9907339215278625),
(0.0, 0.0, 0.0, 1.0))), 'c_middle2.l': Matrix(((-0.2989175617694855, 0.5045099854469299, 0.8100113868713379, 0.5524937510490417),
(-0.819093644618988, -0.5711621046066284, 0.053475141525268555, -0.18013334274291992),
(0.48962658643722534, -0.6474900841712952, 0.5839712619781494, 0.9554142355918884),
(0.0, 0.0, 0.0, 1.0))), 'c_middle3.l': Matrix(((0.03374910354614258, 0.6838750839233398, 0.728818416595459, 0.5666195154190063),
(-0.7648073434829712, -0.45176589488983154, 0.4593227207660675, -0.19612523913383484),
(0.6433745622634888, -0.5729071497917175, 0.5077859163284302, 0.9372851252555847),
(0.0, 0.0, 0.0, 1.0))), 'c_index1_base.l': Matrix(((-0.5358661413192749, 0.556251585483551, 0.6351626515388489, 0.4885375201702118),
(-0.7715515494346619, -0.6281226873397827, -0.10084662586450577, -0.13851317763328552),
(0.3428639769554138, -0.5441008806228638, 0.7657666206359863, 1.033432960510254),
(0.0, 0.0, 0.0, 1.0))), 'c_index1.l': Matrix(((-0.5446334481239319, 0.4174560010433197, 0.7273959517478943, 0.5232923626899719),
(-0.6641221046447754, -0.7443320751190186, -0.07008165121078491, -0.17775851488113403),
(0.5121680498123169, -0.5212484002113342, 0.6826301217079163, 0.9994372725486755),
(0.0, 0.0, 0.0, 1.0))), 'c_index2.l': Matrix(((-0.250714510679245, 0.43092647194862366, 0.8668590188026428, 0.5410243272781372),
(-0.7369382381439209, -0.665624737739563, 0.11775188148021698, -0.20937474071979523),
(0.6277450919151306, -0.6092994809150696, 0.484448105096817, 0.9772966504096985),
(0.0, 0.0, 0.0, 1.0))), 'c_index3.l': Matrix(((-0.42731034755706787, 0.5787864923477173, 0.6945587396621704, 0.5504752993583679),
(-0.7405635714530945, -0.6647520661354065, 0.09833458065986633, -0.2239731252193451),
(0.5186238884925842, -0.47234559059143066, 0.7126837968826294, 0.9639335870742798),
(0.0, 0.0, 0.0, 1.0))), 'c_thumb1_base.l': Matrix(((-0.742074191570282, 0.16673608124256134, -0.6492496132850647, 0.4722246825695038),
(0.06269612163305283, -0.9470593333244324, -0.3148776590824127, -0.14717695116996765),
(-0.6673793196678162, -0.2743678689002991, 0.6923345923423767, 1.0296953916549683),
(0.0, 0.0, 0.0, 1.0))), 'c_thumb1.l': Matrix(((-0.8646366000175476, -0.03816646337509155, -0.5009462237358093, 0.47222477197647095),
(0.11270685493946075, -0.9864310026168823, -0.11937803030014038, -0.14717693626880646),
(-0.48959267139434814, -0.15967853367328644, 0.857205867767334, 1.0296955108642578),
(0.0, 0.0, 0.0, 1.0))), 'c_thumb2.l': Matrix(((-0.9756026864051819, -0.10725779831409454, -0.19156037271022797, 0.4707222878932953),
(0.15552102029323578, -0.9534976482391357, -0.25817763805389404, -0.1860094517469406),
(-0.1549607515335083, -0.2816705107688904, 0.9469156265258789, 1.0234094858169556),
(0.0, 0.0, 0.0, 1.0))), 'c_thumb3.l': Matrix(((-0.8365055918693542, -0.21642035245895386, -0.5034093260765076, 0.46735304594039917),
(0.17929044365882874, -0.9762313961982727, 0.12176759541034698, -0.21596266329288483),
(-0.5177969336509705, 0.011602729558944702, 0.8554251194000244, 1.0145611763000488),
(0.0, 0.0, 0.0, 1.0)))}
matrix_coords_r = {'c_pinky1_base.r': Matrix(((-0.13214083015918732, -0.5434483885765076, -0.8289767503738403, -0.49333253502845764),
(0.9845014810562134, -0.1692400574684143, -0.04598373547196388, -0.10675013810396194),
(-0.11530639231204987, -0.8222052454948425, 0.557389497756958, 1.0162558555603027),
(0.0, 0.0, 0.0, 1.0))), 'c_pinky1.r': Matrix(((0.3489048182964325, -0.4142115116119385, -0.8406510353088379, -0.5218664407730103),
(0.9321953058242798, 0.061202313750982285, 0.356743723154068, -0.11563613265752792),
(-0.09631766378879547, -0.9081205725669861, 0.40748000144958496, 0.9730857014656067),
(0.0, 0.0, 0.0, 1.0))), 'c_pinky2.r': Matrix(((0.40495792031288147, -0.547831654548645, -0.7320449352264404, -0.537188708782196),
(0.8949152231216431, 0.07334486395120621, 0.4401676654815674, -0.11337225884199142),
(-0.18744593858718872, -0.833367109298706, 0.5199644565582275, 0.93949294090271),
(0.0, 0.0, 0.0, 1.0))), 'c_pinky3.r': Matrix(((0.4344694912433624, -0.6926425695419312, -0.5757454633712769, -0.547233521938324),
(0.8724088072776794, 0.1647067666053772, 0.4601900577545166, -0.11202739179134369),
(-0.2239178717136383, -0.7022233605384827, 0.6758275032043457, 0.9242128133773804),
(0.0, 0.0, 0.0, 1.0))), 'c_ring1_base.r': Matrix(((-0.24063074588775635, -0.5735732316970825, -0.7830137610435486, -0.4974001944065094),
(0.9458429217338562, -0.3196679651737213, -0.056507356464862823, -0.11789528280496597),
(-0.2178933024406433, -0.7542052865028381, 0.6194326281547546, 1.0223618745803833),
(0.0, 0.0, 0.0, 1.0))), 'c_ring1.r': Matrix(((-0.051697127521038055, -0.40652453899383545, -0.9121761322021484, -0.5282756090164185),
(0.9341010451316833, -0.34276944398880005, 0.09982036799192429, -0.13510306179523468),
(-0.35324567556381226, -0.8469040989875793, 0.39745569229125977, 0.9817630052566528),
(0.0, 0.0, 0.0, 1.0))), 'c_ring2.r': Matrix(((0.07245972752571106, -0.6446757912635803, -0.7610142230987549, -0.5470013618469238),
(0.9164387583732605, -0.2580502927303314, 0.3058595061302185, -0.15089204907417297),
(-0.39356034994125366, -0.7195854187011719, 0.5721078515052795, 0.9427520632743835),
(0.0, 0.0, 0.0, 1.0))), 'c_ring3.r': Matrix(((0.2263452708721161, -0.8068908452987671, -0.5456140041351318, -0.5622686147689819),
(0.8494092226028442, -0.11064684391021729, 0.516004741191864, -0.15700317919254303),
(-0.47673019766807556, -0.5802450180053711, 0.6603364944458008, 0.9257107377052307),
(0.0, 0.0, 0.0, 1.0))), 'c_middle1_base.r': Matrix(((-0.4113517999649048, -0.5918522477149963, -0.6931816339492798, -0.49848875403404236),
(0.8688518404960632, -0.48445942997932434, -0.10195799916982651, -0.12799015641212463),
(-0.27547430992126465, -0.6442127823829651, 0.7135154008865356, 1.028594970703125),
(0.0, 0.0, 0.0, 1.0))), 'c_middle1.r': Matrix(((-0.2955547571182251, -0.41192927956581116, -0.8619523048400879, -0.5332725644111633),
(0.860168993473053, -0.5072991251945496, -0.052503712475299835, -0.15646234154701233),
(-0.41563984751701355, -0.7569423317909241, 0.5042638778686523, 0.9907339215278625),
(0.0, 0.0, 0.0, 1.0))), 'c_middle2.r': Matrix(((-0.298917680978775, -0.5045096278190613, -0.8100114464759827, -0.5524936318397522),
(0.819093644618988, -0.5711621642112732, 0.053474873304367065, -0.18013350665569305),
(-0.48962661623954773, -0.6474902033805847, 0.5839712023735046, 0.9554141163825989),
(0.0, 0.0, 0.0, 1.0))), 'c_middle3.r': Matrix(((0.03374888375401497, -0.6838746070861816, -0.7288186550140381, -0.566619336605072),
(0.7648075819015503, -0.4517658054828644, 0.4593222737312317, -0.19612538814544678),
(-0.6433743834495544, -0.5729072093963623, 0.507785975933075, 0.9372850656509399),
(0.0, 0.0, 0.0, 1.0))), 'c_index1_base.r': Matrix(((-0.5358662605285645, -0.5562516450881958, -0.6351625323295593, -0.4885374903678894),
(0.7715515494346619, -0.6281226873397827, -0.10084672272205353, -0.13851313292980194),
(-0.3428637981414795, -0.5441008806228638, 0.7657667398452759, 1.033432960510254),
(0.0, 0.0, 0.0, 1.0))), 'c_index1.r': Matrix(((-0.5446333289146423, -0.4174560308456421, -0.7273959517478943, -0.5232923030853271),
(0.6641222238540649, -0.7443320751190186, -0.07008160650730133, -0.17775849997997284),
(-0.5121681690216064, -0.5212485194206238, 0.6826300024986267, 0.9994373321533203),
(0.0, 0.0, 0.0, 1.0))), 'c_index2.r': Matrix(((-0.25071465969085693, -0.43092650175094604, -0.8668591380119324, -0.5410242676734924),
(0.7369382977485657, -0.6656247973442078, 0.1177515834569931, -0.20937487483024597),
(-0.6277451515197754, -0.6092994213104248, 0.48444831371307373, 0.9772967100143433),
(0.0, 0.0, 0.0, 1.0))), 'c_index3.r': Matrix(((-0.42731064558029175, -0.5787865519523621, -0.69455885887146, -0.5504752993583679),
(0.7405636310577393, -0.6647522449493408, 0.09833402186632156, -0.22397328913211823),
(-0.5186238288879395, -0.47234559059143066, 0.7126840949058533, 0.9639335870742798),
(0.0, 0.0, 0.0, 1.0))), 'c_thumb1_base.r': Matrix(((-0.7420739531517029, -0.16673603653907776, 0.6492499709129333, -0.47222471237182617),
(-0.0626964196562767, -0.947059154510498, -0.31487780809402466, -0.14717699587345123),
(0.6673798561096191, -0.2743680477142334, 0.6923341155052185, 1.0296955108642578),
(0.0, 0.0, 0.0, 1.0))), 'c_thumb1.r': Matrix(((-0.8646363615989685, 0.038166627287864685, 0.5009466409683228, -0.4722246825695038),
(-0.11270713806152344, -0.9864309430122375, -0.11937820911407471, -0.14717702567577362),
(0.4895932376384735, -0.15967872738838196, 0.8572055697441101, 1.0296955108642578),
(0.0, 0.0, 0.0, 1.0))), 'c_thumb2.r': Matrix(((-0.9756026268005371, 0.10725804418325424, 0.19156040251255035, -0.4707222580909729),
(-0.1555212289094925, -0.9534975290298462, -0.25817784667015076, -0.18600955605506897),
(0.15496063232421875, -0.2816707491874695, 0.946915864944458, 1.0234094858169556),
(0.0, 0.0, 0.0, 1.0))), 'c_thumb3.r': Matrix(((-0.8365054726600647, 0.21642057597637177, 0.5034092664718628, -0.4673529863357544),
(-0.1792907863855362, -0.9762312769889832, 0.12176735699176788, -0.2159627228975296),
(0.5177968740463257, 0.011602476239204407, 0.8554254174232483, 1.0145611763000488),
(0.0, 0.0, 0.0, 1.0)))}
@@ -0,0 +1,141 @@
from mathutils import Matrix
hand_mat_l = Matrix(((-0.35261183977127075, 0.516721785068512, 0.7801688313484192, 0.4798411726951599),
(-0.8112916350364685, -0.5842888355255127, 0.020308198407292366, -0.11578542739152908),
(0.4663377106189728, -0.6257834434509277, 0.6252393126487732, 1.0348492860794067),
(0.0, 0.0, 0.0, 1.0)))
hand_mat_r = Matrix(((-0.3526119887828827, -0.516721785068512, -0.7801688313484192, -0.47984111309051514),
(0.8112916946411133, -0.5842887759208679, 0.020307956263422966, -0.11578543484210968),
(-0.4663374722003937, -0.6257835030555725, 0.6252394914627075, 1.0348492860794067),
(0.0, 0.0, 0.0, 1.0)))
matrix_coords_l = {'c_pinky1_base.l': Matrix(((-0.3588298559188843, 0.424203485250473, 0.8314400315284729, 0.49337443709373474),
(-0.8281897306442261, -0.5555429458618164, -0.07398698478937149, -0.10673553496599197),
(0.43051522970199585, -0.7151384949684143, 0.5506664514541626, 1.0163047313690186),
(0.0, 0.0, 0.0, 1.0))),
'c_pinky1.l': Matrix(((-0.05721341073513031, -0.9644581079483032, 0.25796768069267273, 0.5156472325325012),
(-0.8807923793792725, -0.0728902518749237, -0.4678591787815094, -0.1359044313430786),
(0.47003400325775146, -0.25398388504981995, -0.8453166484832764, 0.9787562489509583),
(0.0, 0.0, 0.0, 1.0))),
'c_pinky2.l': Matrix(((0.03900858014822006, 0.06956508010625839, -0.9968141913414001, 0.47997069358825684),
(-0.919760525226593, 0.39238572120666504, -0.008609596639871597, -0.13860078155994415),
(0.3905368149280548, 0.9171664714813232, 0.07928968966007233, 0.9693610668182373),
(0.0, 0.0, 0.0, 1.0))),
'c_pinky3.l': Matrix(((-0.012231852859258652, 0.9586328268051147, 0.28438279032707214, 0.4812462031841278),
(-0.9238848090171814, -0.11962065100669861, 0.363493949174881, -0.1314062774181366),
(0.3824753165245056, -0.25829076766967773, 0.8871296048164368, 0.9861776828765869),
(0.0, 0.0, 0.0, 1.0))),
'c_ring1_base.l': Matrix(((-0.39991500973701477, 0.47596868872642517, 0.7832762598991394, 0.49744197726249695),
(-0.8209208250045776, -0.5660759210586548, -0.07515108585357666, -0.1178804263472557),
(0.4076242744922638, -0.6730616688728333, 0.6171146631240845, 1.0224111080169678),
(0.0, 0.0, 0.0, 1.0))), 'c_ring1.l': Matrix(((-0.14413531124591827, -0.9160363078117371, 0.37430238723754883, 0.5230634212493896),
(-0.8027083277702332, -0.11296883970499039, -0.5855748057365417, -0.1483522206544876),
(0.5786923766136169, -0.38485753536224365, -0.7190269827842712, 0.9861801862716675),
(0.0, 0.0, 0.0, 1.0))), 'c_ring2.l': Matrix(((-0.10324987769126892, -0.19098883867263794, -0.9761467576026917, 0.48086801171302795),
(-0.8736379742622375, 0.48656898736953735, -0.002792816609144211, -0.15355591475963593),
(0.4754961431026459, 0.8525105118751526, -0.21709322929382324, 0.9684524536132812),
(0.0, 0.0, 0.0, 1.0))), 'c_ring3.l': Matrix(((-0.286427766084671, 0.9244988560676575, 0.2515174150466919, 0.47634512186050415),
(-0.848092257976532, -0.3667835295200348, 0.38237467408180237, -0.1420329213142395),
(0.44575732946395874, -0.10378727316856384, 0.8891169428825378, 0.9886415600776672),
(0.0, 0.0, 0.0, 1.0))), 'c_middle1_base.l': Matrix(((-0.49470439553260803, 0.5256193280220032, 0.6920921802520752, 0.4985300898551941),
(-0.7925525307655334, -0.5995886921882629, -0.11114682257175446, -0.1279752105474472),
(0.35654982924461365, -0.6035042405128479, 0.7132004499435425, 1.0286451578140259),
(0.0, 0.0, 0.0, 1.0))), 'c_middle1.l': Matrix(((-0.18281525373458862, -0.8775211572647095, 0.4433228373527527, 0.5294212102890015),
(-0.8266451358795166, -0.10689479857683182, -0.5524771809577942, -0.16321365535259247),
(0.5321995615959167, -0.46747225522994995, -0.7058566212654114, 0.9931765794754028),
(0.0, 0.0, 0.0, 1.0))), 'c_middle2.l': Matrix(((-0.09921594709157944, -0.1985192596912384, -0.9750621318817139, 0.48847508430480957),
(-0.8331379890441895, 0.5523718595504761, -0.027686286717653275, -0.16820143163204193),
(0.5440933704376221, 0.8096145987510681, -0.22019770741462708, 0.9713637232780457),
(0.0, 0.0, 0.0, 1.0))), 'c_middle3.l': Matrix(((-0.45292720198631287, 0.8585988879203796, 0.24013452231884003, 0.48291686177253723),
(-0.7653985619544983, -0.5125921964645386, 0.38912028074264526, -0.15273569524288177),
(0.45718955993652344, -0.007555872201919556, 0.8893374800682068, 0.9940320253372192),
(0.0, 0.0, 0.0, 1.0))), 'c_index1_base.l': Matrix(((-0.5358661413192749, 0.556251585483551, 0.6351626515388489, 0.4885786473751068),
(-0.7715515494346619, -0.6281226873397827, -0.10084662586450577, -0.1384982317686081),
(0.3428639769554138, -0.5441008806228638, 0.7657666206359863, 1.0334831476211548),
(0.0, 0.0, 0.0, 1.0))), 'c_index1.l': Matrix(((-0.3491906523704529, -0.8236109614372253, 0.4469125270843506, 0.5233335494995117),
(-0.7358909249305725, -0.054232120513916016, -0.6749246716499329, -0.17774353921413422),
(0.5801123976707458, -0.5645565986633301, -0.5871503949165344, 0.999487578868866),
(0.0, 0.0, 0.0, 1.0))), 'c_index2.l': Matrix(((-0.29383838176727295, -0.25366896390914917, -0.9215805530548096, 0.4883497357368469),
(-0.9045541882514954, 0.38540804386138916, 0.18232452869415283, -0.18004705011844635),
(0.30893459916114807, 0.8871937394142151, -0.34270498156547546, 0.9755074977874756),
(0.0, 0.0, 0.0, 1.0))), 'c_index3.l': Matrix(((-0.09548855572938919, 0.9815612435340881, 0.1655871868133545, 0.48278629779815674),
(-0.9254596829414368, -0.1488049030303955, 0.3483980894088745, -0.17159435153007507),
(0.36661431193351746, -0.11997638642787933, 0.9226047396659851, 0.9949652552604675),
(0.0, 0.0, 0.0, 1.0))), 'c_thumb1_base.l': Matrix(((-0.742074191570282, 0.16673608124256134, -0.6492496132850647, 0.47226616740226746),
(0.06269612163305283, -0.9470593333244324, -0.3148776590824127, -0.1471620798110962),
(-0.6673793196678162, -0.2743678689002991, 0.6923345923423767, 1.02974534034729),
(0.0, 0.0, 0.0, 1.0))), 'c_thumb1.l': Matrix(((-0.916347861289978, 0.030809633433818817, -0.3991962671279907, 0.47226616740226746),
(0.18215209245681763, -0.8557999730110168, -0.4841769337654114, -0.1471620798110962),
(-0.3565494418144226, -0.5163886547088623, 0.778598427772522, 1.02974534034729),
(0.0, 0.0, 0.0, 1.0))), 'c_thumb2.l': Matrix(((-0.9960366487503052, 0.006717206910252571, -0.08869118243455887, 0.47347909212112427),
(0.08558714389801025, -0.199024498462677, -0.9762499332427979, -0.1808519959449768),
(-0.024209430441260338, -0.9799714088439941, 0.19766077399253845, 1.009416937828064),
(0.0, 0.0, 0.0, 1.0))), 'c_thumb3.l': Matrix(((-0.8867418766021729, 0.45290955901145935, -0.0925314724445343, 0.4736900329589844),
(0.4336596429347992, 0.7457115054130554, -0.5058196783065796, -0.18710407614707947),
(-0.16008882224559784, -0.4886585474014282, -0.8576620221138, 0.9786321520805359),
(0.0, 0.0, 0.0, 1.0)))}
matrix_coords_r = {'c_pinky1_base.r': Matrix(((-0.3588300347328186, -0.4242035448551178, -0.831439733505249, -0.49333256483078003),
(0.8281898498535156, -0.5555428266525269, -0.07398723065853119, -0.10675002634525299),
(-0.4305148422718048, -0.7151386141777039, 0.5506665706634521, 1.0162558555603027),
(0.0, 0.0, 0.0, 1.0))), 'c_pinky1.r': Matrix(((-0.05721384286880493, 0.9644578099250793, -0.25796759128570557, -0.5156054496765137),
(0.8807926774024963, -0.07288983464241028, -0.4678589701652527, -0.13591890037059784),
(-0.4700336754322052, -0.2539840042591095, -0.8453166484832764, 0.9787073731422424),
(0.0, 0.0, 0.0, 1.0))), 'c_pinky2.r': Matrix(((0.03900822252035141, -0.06956491619348526, 0.9968144297599792, -0.47992897033691406),
(0.9197608232498169, 0.3923851549625397, -0.008609358221292496, -0.1386151909828186),
(-0.3905361294746399, 0.9171666502952576, 0.07928939163684845, 0.9693121910095215),
(0.0, 0.0, 0.0, 1.0))), 'c_pinky3.r': Matrix(((-0.012232892215251923, -0.9586328864097595, -0.28438252210617065, -0.48120447993278503),
(0.92388516664505, -0.11962135881185532, 0.3634931743144989, -0.13142070174217224),
(-0.3824746608734131, -0.25829029083251953, 0.8871298432350159, 0.9861287474632263),
(0.0, 0.0, 0.0, 1.0))), 'c_ring1_base.r': Matrix(((-0.39991509914398193, -0.4759686291217804, -0.7832764387130737, -0.49740028381347656),
(0.8209203481674194, -0.5660758018493652, -0.07515129446983337, -0.11789508163928986),
(-0.4076239764690399, -0.673061728477478, 0.617114782333374, 1.0223617553710938),
(0.0, 0.0, 0.0, 1.0))), 'c_ring1.r': Matrix(((-0.1441354602575302, 0.9160371422767639, -0.37430238723754883, -0.5230216979980469),
(0.802708089351654, -0.11296850442886353, -0.5855749249458313, -0.14836692810058594),
(-0.5786920189857483, -0.38485804200172424, -0.7190274596214294, 0.9861308336257935),
(0.0, 0.0, 0.0, 1.0))), 'c_ring2.r': Matrix(((-0.10325005650520325, 0.19098877906799316, 0.976146936416626, -0.4808262884616852),
(0.8736380934715271, 0.4865686595439911, -0.00279245525598526, -0.15357057750225067),
(-0.4754956364631653, 0.8525108695030212, -0.2170931100845337, 0.968403160572052),
(0.0, 0.0, 0.0, 1.0))), 'c_ring3.r': Matrix(((-0.28642746806144714, -0.9244990348815918, -0.25151753425598145, -0.4763032793998718),
(0.8480921983718872, -0.3667834401130676, 0.38237419724464417, -0.14204759895801544),
(-0.445756733417511, -0.10378794372081757, 0.8891172409057617, 0.9885924458503723),
(0.0, 0.0, 0.0, 1.0))), 'c_middle1_base.r': Matrix(((-0.49470436573028564, -0.5256195068359375, -0.6920923590660095, -0.4984886348247528),
(0.792552649974823, -0.5995886921882629, -0.11114684492349625, -0.12799014151096344),
(-0.3565496802330017, -0.6035044193267822, 0.7132005095481873, 1.0285948514938354),
(0.0, 0.0, 0.0, 1.0))), 'c_middle1.r': Matrix(((-0.18281537294387817, 0.8775213360786438, -0.4433228373527527, -0.5293797850608826),
(0.8266454935073853, -0.10689447820186615, -0.5524771809577942, -0.16322852671146393),
(-0.5321993827819824, -0.4674723744392395, -0.7058568000793457, 0.993126392364502),
(0.0, 0.0, 0.0, 1.0))), 'c_middle2.r': Matrix(((-0.09921589493751526, 0.19851899147033691, 0.9750621318817139, -0.4884335994720459),
(0.8331380486488342, 0.5523715019226074, -0.027686096727848053, -0.16821634769439697),
(-0.5440930128097534, 0.8096147179603577, -0.2201976180076599, 0.9713135957717896),
(0.0, 0.0, 0.0, 1.0))), 'c_middle3.r': Matrix(((-0.4529270827770233, -0.8585991263389587, -0.24013493955135345, -0.4828752875328064),
(0.7653987407684326, -0.5125921964645386, 0.3891199827194214, -0.15275052189826965),
(-0.45718950033187866, -0.007556170225143433, 0.8893375396728516, 0.9939819574356079),
(0.0, 0.0, 0.0, 1.0))), 'c_index1_base.r': Matrix(((-0.5358662605285645, -0.5562516450881958, -0.6351625323295593, -0.4885374903678894),
(0.7715515494346619, -0.6281226873397827, -0.10084672272205353, -0.13851310312747955),
(-0.3428637981414795, -0.5441008806228638, 0.7657667398452759, 1.033432960510254),
(0.0, 0.0, 0.0, 1.0))), 'c_index1.r': Matrix(((-0.3491906523704529, 0.8236109018325806, -0.44691264629364014, -0.5232923030853271),
(0.7358911037445068, -0.0542321503162384, -0.6749247312545776, -0.17775849997997284),
(-0.5801124572753906, -0.564556360244751, -0.5871505737304688, 0.9994373321533203),
(0.0, 0.0, 0.0, 1.0))), 'c_index2.r': Matrix(((-0.29383835196495056, 0.25366878509521484, 0.9215806722640991, -0.4883083999156952),
(0.9045540690422058, 0.3854084312915802, 0.18232448399066925, -0.18006213009357452),
(-0.3089349865913391, 0.8871937394142151, -0.342704713344574, 0.9754570126533508),
(0.0, 0.0, 0.0, 1.0))), 'c_index3.r': Matrix(((-0.09548826515674591, -0.9815613031387329, -0.1655874252319336, -0.48274490237236023),
(0.9254595637321472, -0.14880481362342834, 0.3483985364437103, -0.17160934209823608),
(-0.36661475896835327, -0.11997681856155396, 0.9226045608520508, 0.9949148893356323),
(0.0, 0.0, 0.0, 1.0))), 'c_thumb1_base.r': Matrix(((-0.7420739531517029, -0.16673603653907776, 0.6492499709129333, -0.47222471237182617),
(-0.0626964196562767, -0.947059154510498, -0.31487780809402466, -0.14717699587345123),
(0.6673798561096191, -0.2743680477142334, 0.6923341155052185, 1.0296955108642578),
(0.0, 0.0, 0.0, 1.0))), 'c_thumb1.r': Matrix(((-0.9163479208946228, -0.03080972284078598, 0.3991967439651489, -0.4722246825695038),
(-0.18215233087539673, -0.855799674987793, -0.4841769337654114, -0.14717702567577362),
(0.35655027627944946, -0.5163888931274414, 0.7785980105400085, 1.0296955108642578),
(0.0, 0.0, 0.0, 1.0))), 'c_thumb2.r': Matrix(((-0.9960367679595947, -0.0067172907292842865, 0.08869123458862305, -0.4734375476837158),
(-0.08558713644742966, -0.19902411103248596, -0.9762501120567322, -0.18086694180965424),
(0.024209478870034218, -0.9799717664718628, 0.19766059517860413, 1.0093668699264526),
(0.0, 0.0, 0.0, 1.0))), 'c_thumb3.r': Matrix(((-0.8867420554161072, -0.45290952920913696, 0.09253138303756714, -0.4736485183238983),
(-0.43365973234176636, 0.7457118630409241, -0.5058193206787109, -0.18711905181407928),
(0.16008873283863068, -0.48865842819213867, -0.8576623797416687, 0.978581964969635),
(0.0, 0.0, 0.0, 1.0)))}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
from mathutils import Vector, Euler, Matrix
coords={'pelvis': Vector((1.1648752433757181e-06, -0.03546452522277832, 9.705429077148438)), 'spine_01': Vector((2.2931205876375316e-06, -0.6347151398658752, 19.094528198242188)), 'spine_02': Vector((1.598883045517141e-06, 0.6980798244476318, 13.395744323730469)), 'spine_03': Vector((1.6606641111138742e-06, 1.0556304454803467, 13.992294311523438)), 'clavicle_l': Vector((14.954872131347656, 4.510254859924316, -2.2741546630859375)), 'upperarm_l': Vector((15.369140625, -0.2687036991119385, -1.2218170166015625)), 'lowerarm_l': Vector((20.141151428222656, -3.486912250518799, -1.384307861328125)), 'hand_l': Vector((10.576087951660156, -1.987648606300354, -0.7958221435546875)), 'index_01_l': Vector((4.158714294433594, -0.7536659240722656, -0.720947265625)), 'index_02_l': Vector((3.249267578125, -0.3236865997314453, -0.9248504638671875)), 'index_03_l': Vector((3.1864013671875, -0.4611630439758301, -1.073333740234375)), 'middle_01_l': Vector((4.579429626464844, -0.4014453887939453, -0.6331024169921875)), 'middle_02_l': Vector((3.5030288696289062, -0.391385555267334, -0.943206787109375)), 'middle_03_l': Vector((3.4647369384765625, -0.42128419876098633, -1.06402587890625)), 'pinky_01_l': Vector((3.498687744140625, 0.6987745761871338, -0.15118408203125)), 'pinky_02_l': Vector((2.8414535522460938, 0.5827560424804688, -0.70745849609375)), 'pinky_03_l': Vector((2.8664474487304688, 0.37882447242736816, -0.744293212890625)), 'ring_01_l': Vector((4.408515930175781, 0.2745676636695862, -0.3408203125)), 'ring_02_l': Vector((3.3953475952148438, 0.27488279342651367, -0.695098876953125)), 'ring_03_l': Vector((3.3896865844726562, 0.14387929439544678, -0.75921630859375)), 'thumb_01_l': Vector((2.0802688598632812, -2.6089048385620117, -1.9597320556640625)), 'thumb_02_l': Vector((2.58538818359375, -1.9712257385253906, -2.4354095458984375)), 'thumb_03_l': Vector((2.8783111572265625, -1.8077516555786133, -2.2245330810546875)), 'clavicle_r': Vector((-14.95479679107666, 4.510228633880615, -2.274139404296875)), 'upperarm_r': Vector((-15.369194030761719, -0.26871156692504883, -1.2218170166015625)), 'lowerarm_r': Vector((-20.141193389892578, -3.486935615539551, -1.384307861328125)), 'hand_r': Vector((-10.576080322265625, -1.987655758857727, -0.7958221435546875)), 'index_01_r': Vector((-4.158897399902344, -0.7537021636962891, -0.720977783203125)), 'index_02_r': Vector((-3.249267578125, -0.32368993759155273, -0.9248504638671875)), 'index_03_r': Vector((-3.1864089965820312, -0.4611630439758301, -1.0733184814453125)), 'middle_01_r': Vector((-4.579620361328125, -0.40146565437316895, -0.6331329345703125)), 'middle_02_r': Vector((-3.5030975341796875, -0.39139294624328613, -0.9432220458984375)), 'middle_03_r': Vector((-3.4648056030273438, -0.42128705978393555, -1.06402587890625)), 'pinky_01_r': Vector((-3.4987640380859375, 0.6987893581390381, -0.1511993408203125)), 'pinky_02_r': Vector((-2.84124755859375, 0.5827126502990723, -0.7073974609375)), 'pinky_03_r': Vector((-2.8662338256835938, 0.37879395484924316, -0.744232177734375)), 'ring_01_r': Vector((-4.408203125, 0.2745439410209656, -0.3408050537109375)), 'ring_02_r': Vector((-3.3953475952148438, 0.27488067746162415, -0.695098876953125)), 'ring_03_r': Vector((-3.3897171020507812, 0.14389744400978088, -0.7591552734375)), 'thumb_01_r': Vector((-2.0802078247070312, -2.608829975128174, -1.9596710205078125)), 'thumb_02_r': Vector((-2.58538818359375, -1.9712257385253906, -2.4354248046875)), 'thumb_03_r': Vector((-2.878326416015625, -1.8077640533447266, -2.2245025634765625)), 'neck_01': Vector((1.083126335288398e-06, -2.8042922019958496, 8.857421875)), 'head': Vector((1.1140700735268183e-06, 0.1489097774028778, 16.57891845703125)), 'thigh_l': Vector((3.5867919921875, -0.9371733665466309, -32.11991882324219)), 'calf_l': Vector((1.8110084533691406, 1.1467314958572388, -30.26090431213379)), 'foot_l': Vector((0.8927364349365234, -19.544645309448242, -1.1506776809692383)), 'thigh_r': Vector((-3.586806297302246, -0.9371740818023682, -32.120018005371094)), 'calf_r': Vector((-1.8110074996948242, 1.1467384099960327, -30.261043548583984)), 'foot_r': Vector((-0.8925895690917969, -19.54458999633789, -1.1506767272949219))}
File diff suppressed because it is too large Load Diff
+116 -10
View File
@@ -20,7 +20,7 @@
bl_info = {
"name": "BlenderKit Online Asset Library",
"author": "Vilem Duha, Petr Dlouhy, A. Gajdosik",
"version": (3, 16, 1, 250612), # X.Y.Z.yymmdd
"version": (3, 18, 0, 251121), # X.Y.Z.yymmdd
"blender": (3, 0, 0),
"location": "View3D > Properties > BlenderKit",
"description": "Boost your workflow with drag&drop assets from the community driven library.",
@@ -28,7 +28,7 @@ bl_info = {
"tracker_url": "https://github.com/BlenderKit/blenderkit/issues",
"category": "3D View",
}
VERSION = (3, 16, 1, 250612)
VERSION = (3, 18, 0, 251121)
import logging
import random
@@ -297,6 +297,11 @@ def asset_type_callback(self, context):
6,
),
]
# Only add addon option for Blender 4.2+
if bpy.app.version >= (4, 2, 0):
items.append(("ADDON", "Add-ons", "Find add-ons", "PLUGIN", 7))
else:
items = [
("MODEL", "Model", "Upload a model", "OBJECT_DATAMODE", 0),
@@ -314,6 +319,11 @@ def asset_type_callback(self, context):
),
]
# Only add addon option for Blender 4.2+
if bpy.app.version >= (4, 2, 0):
items.append(("ADDON", "Add-on", "Upload an addon", "PLUGIN", 7))
return items
@@ -380,6 +390,61 @@ class BlenderKitUIProps(PropertyGroup):
max=10,
update=search.search_update_delayed,
)
search_order_by: EnumProperty(
name="Order",
description="Search result order",
items=(
(
"default",
"Default",
"By default, the sorting algorithm changes dynamically based on search filters.",
),
("-created", "Newest", "Sort results from newest to oldest."),
("created", "Oldest", "Sort results from oldest to newest."),
(
"-bookmarks",
"▼ Bookmarks",
"Sort results from most bookmarked to least.",
),
(
"bookmarks",
"▲ Bookmarks",
"Sort results from least bookmarked to most.",
),
(
"-score",
"▼ Score",
"Sort results from highest asset score to the lowest.",
),
(
"score",
"▲ Score",
"Sort results from lowest asset score to the highest.",
),
(
"-working_hours",
"▼ Complexity",
"Sort results from most complex to the least.",
),
(
"working_hours",
"▲ Complexity",
"Sort results from least complex to the most.",
),
(
"-quality",
"▼ Quality",
"Sort results from highest quality rating to the lowest.",
),
(
"quality",
"▲ Quality",
"Sort results from lowest quality rating to the highest.",
),
),
default="default",
update=search.search_update,
)
search_license: EnumProperty(
name="License",
items=(
@@ -408,7 +473,7 @@ class BlenderKitUIProps(PropertyGroup):
)
search_blender_version_max: StringProperty(
name="Maximum version (excluding, lower than)",
default="4.99",
default="5.99",
description="Limit the assets by maximum version of Blender in which they were created, exluding the specified version and all newer versions from the search results. "
+ "Only assets created in LOWER THAN (< max) maximum version will be shown. Use semantic versioning format: X.Y.Z.\n\n"
+ "E.g.: exclude all Blender 4 assets by specifying 4, 4.0, or 4.0.0. Assets created in 3.6 and lower will be shown",
@@ -1109,6 +1174,19 @@ class BlenderKitGeoToolSearchProps(PropertyGroup, BlenderKitCommonSearchProps):
pass
class BlenderKitAddonSearchProps(PropertyGroup, BlenderKitCommonSearchProps):
search_installed: BoolProperty(
name="Installed Only",
description="Show only addons that are already installed in Blender",
default=False,
update=lambda self, context: (
search.refresh_search()
if context.window_manager.blenderkitUI.asset_type == "ADDON"
else None
),
)
class BlenderKitHDRUploadProps(PropertyGroup, BlenderKitCommonUploadProps):
texture_resolution_max: IntProperty(
name="Texture Resolution Max",
@@ -1927,7 +2005,11 @@ class BlenderKitAddonPreferences(AddonPreferences):
api_key: StringProperty(
name="BlenderKit API Key",
description="Your blenderkit API Key. Get it from your page on the website",
description=(
"Your unique API key authenticates downloads and requests inside the add-on. "
"No manual setup is required, the API Key is auto-filled at login and cleared at logout. "
"However, you can also paste the key from your profile settings on the BlenderKit website."
),
default="",
subtype="PASSWORD",
update=utils.api_key_property_updated,
@@ -1980,6 +2062,13 @@ class BlenderKitAddonPreferences(AddonPreferences):
update=utils.save_prefs,
)
sidebar_panels: BoolProperty(
name="Hide sidebar panels",
description="Hide BlenderKit sidebar panels (search, upload, and selected model functionality). This prevents upload and it's also the only place for import settings. Reenable this to access these features.",
default=False,
update=utils.save_prefs,
)
header_menu_fold: BoolProperty(
name="Header menu fold", default=False, update=ui_panels.update_header_menu_fold
)
@@ -2209,15 +2298,21 @@ In this case you should also set path to your system CA bundle containing proxy'
update=utils.save_prefs,
)
max_assetbar_rows: IntProperty(
name="Max Assetbar Rows",
description="max rows of assetbar in the 3D view",
default=1,
min=1,
maximized_assetbar_rows: IntProperty(
name="Maximized Assetbar Rows",
description="Maximum rows of assetbar in the 3D view when expanded",
default=4,
min=2,
max=20,
update=utils.save_prefs,
)
assetbar_expanded: BoolProperty(
name="Assetbar Expanded",
description="Whether the assetbar is currently expanded to show maximum rows",
default=False,
)
thumb_size: IntProperty(
name="Assetbar Thumbnail Size",
default=96,
@@ -2329,6 +2424,12 @@ In this case you should also set path to your system CA bundle containing proxy'
update=search.search_update,
) # In future we can subsets like sexualized, pornography or violence subset. And allow users choose what is part of NSFW.
temp_enabled_addons: StringProperty(
name="Temporarily Enabled Addons",
description="JSON string of temporarily enabled addon package IDs that should be disabled on next session",
default="[]",
)
def draw(self, context):
layout = self.layout
if self.api_key.strip() == "":
@@ -2362,9 +2463,10 @@ In this case you should also set path to your system CA bundle containing proxy'
gui_settings.label(text="GUI settings")
gui_settings.prop(self, "show_on_start")
gui_settings.prop(self, "thumb_size")
gui_settings.prop(self, "max_assetbar_rows")
gui_settings.prop(self, "maximized_assetbar_rows")
gui_settings.prop(self, "search_field_width")
gui_settings.prop(self, "search_in_header")
gui_settings.prop(self, "sidebar_panels")
gui_settings.prop(self, "show_VIEW3D_MT_blenderkit_model_properties")
gui_settings.prop(self, "tips_on_start")
gui_settings.prop(self, "announcements_on_start")
@@ -2434,6 +2536,7 @@ classes = (
BlenderKitBrushUploadProps,
BlenderKitGeoToolSearchProps,
BlenderKitNodeGroulUploadProps,
BlenderKitAddonSearchProps,
)
@@ -2500,6 +2603,9 @@ def register():
bpy.types.NodeTree.blenderkit = PointerProperty( # for uploads, not now...
type=BlenderKitNodeGroulUploadProps
)
bpy.types.WindowManager.blenderkit_addon = PointerProperty(
type=BlenderKitAddonSearchProps
)
if bpy.app.factory_startup is False:
user_preferences = bpy.context.preferences.addons[__package__].preferences
global_vars.PREFS = utils.get_preferences_as_dict()
@@ -0,0 +1,12 @@
include:
- "**/*.py"
exclude_dirs:
- "tests"
- ".venv"
- "__pycache__"
skips:
- "B404" # https://bandit.readthedocs.io/en/1.7.10/blacklists/blacklist_imports.html#b404-import-subprocess
- "B603" # https://bandit.readthedocs.io/en/1.7.10/plugins/b603_subprocess_without_shell_equals_true.html
- "B608" # https://bandit.readthedocs.io/en/1.7.10/plugins/b608_hardcoded_sql_expressions.html
+211 -86
View File
@@ -19,6 +19,7 @@
import logging
import uuid
from typing import Optional
import bpy
@@ -55,11 +56,34 @@ def append_brush(file_name, brushname=None, link=False, fake_user=True):
def append_nodegroup(
file_name, nodegroupname=None, link=False, fake_user=True, node_x=0, node_y=0
file_name,
nodegroupname=None,
link=False,
fake_user=True,
node_x=0,
node_y=0,
target_object=None,
nodegroup_mode="",
model_location=(0, 0, 0),
model_rotation=(0, 0, 0),
**kwargs,
):
"""Append selected node group. If nodegroupname is None, first node group is appended.
If node group with the same name is already in the scene, it is not appended again.
Try to look for a suitable node editor and insert the node group there, in the middle of the area.
Try to look for a suitable node editor and insert the node group there, or create/use modifier based on mode.
For geometry nodegroups, if no target object is provided, a target object will be created automatically.
Args:
file_name: Path to the .blend file containing the nodegroup
nodegroupname: Name of the nodegroup to append
link: Whether to link or append
fake_user: Whether to set fake user
node_x: X position for node placement in editor
node_y: Y position for node placement in editor
target_object: Target object for modifier mode (name string). If None and nodegroup is geometry type, a target object will be created
nodegroup_mode: How to add the nodegroup - "MODIFIER" for new modifier, "NODE" for node in editor, "" for default behavior
model_location: Location for the target object (used when creating new target)
model_rotation: Rotation for the target object (used when creating new target)
Returns:
tuple: (nodegroup, added_to_editor) - The nodegroup and whether it was added to an editor
@@ -76,6 +100,22 @@ def append_nodegroup(
nodegroup = bpy.data.node_groups[nodegroupname]
nodegroup.use_fake_user = fake_user
# Create target object automatically for geometry nodegroups when no target is provided
auto_created_target: Optional[bpy.types.Object] = None
if nodegroup.bl_rna.identifier == "GeometryNodeTree" and not target_object:
# Create a default mesh cube
bpy.ops.mesh.primitive_cube_add(
size=2, location=model_location, rotation=model_rotation
)
target_obj = bpy.context.active_object
target_obj.name = "GeometryNodeTarget"
target_object = target_obj.name
auto_created_target = target_obj
# Make sure it's selected and active
bpy.context.view_layer.objects.active = target_obj
target_obj.select_set(True)
# Mapping dict for node editor tree types to node group node types
sdict = {
"GeometryNodeTree": "GeometryNodeGroup",
@@ -86,25 +126,119 @@ def append_nodegroup(
# Get the nodegroup type
nodegroup_type = nodegroup.bl_rna.identifier
# Find a suitable node editor
# If no explicit mode is set, try to detect if we should add to an existing editor first
# This allows drag-drop into existing node editors to work properly
if not nodegroup_mode:
# Find a suitable node editor
for area in bpy.context.screen.areas:
if area.type != "NODE_EDITOR":
continue
if area.spaces.active.tree_type == nodegroup_type:
nt = area.spaces.active.edit_tree
if nt is None:
continue
# Add node to this editor
for n in nt.nodes:
n.select = False
node_type = sdict.get(nodegroup_type)
if node_type:
node = nt.nodes.new(node_type)
node.node_tree = nodegroup
node.location = (node_x, node_y)
node.select = True
nt.nodes.active = node
return (nodegroup, True)
# Handle modifier mode for geometry nodegroups
if nodegroup_mode == "MODIFIER" and target_object:
target_obj = bpy.data.objects.get(target_object)
if target_obj and nodegroup.bl_rna.identifier == "GeometryNodeTree":
# Create a new geometry nodes modifier with this nodegroup
gn_mod = target_obj.modifiers.new(name=nodegroup.name, type="NODES")
gn_mod.node_group = nodegroup
# Select the target object to make the change visible
bpy.context.view_layer.objects.active = target_obj
if target_obj not in bpy.context.selected_objects:
target_obj.select_set(True)
return (
nodegroup,
True,
) # Return True as we "added" it successfully to the modifier
# Handle node mode for geometry nodegroups with target object
# Create a modifier setup and then add the nodegroup as a node to the tree
if (
nodegroup_mode == "NODE"
and target_object
and nodegroup.bl_rna.identifier == "GeometryNodeTree"
):
target_obj = bpy.data.objects.get(target_object)
if target_obj:
# Select the target object to make it active
bpy.context.view_layer.objects.active = target_obj
if target_obj not in bpy.context.selected_objects:
target_obj.select_set(True)
# look for the geometry nodes modifier
gn_mod = None
for mod in target_obj.modifiers:
if mod.type == "NODES" and mod.node_group:
gn_mod = mod
break
if not gn_mod:
# create a new geometry nodes modifier
gn_mod = target_obj.modifiers.new(name="GeometryNodes", type="NODES")
if not gn_mod.node_group:
# create a new node group
bpy.ops.node.new_geometry_node_group_assign()
node_tree = gn_mod.node_group
if node_tree:
# Add the nodegroup as a node to the tree
group_node = node_tree.nodes.new("GeometryNodeGroup")
group_node.node_tree = nodegroup
group_node.location = (node_x, node_y)
group_node.select = True
node_tree.nodes.active = group_node
return (nodegroup, True)
# If not added yet through modes or if no mode specified, try to find any compatible editor
added_to_editor = False
# First try: exact match for tree type
# Try any compatible editor
for area in bpy.context.screen.areas:
if area.type != "NODE_EDITOR":
continue
if area.spaces.active.tree_type == nodegroup_type:
nt = area.spaces.active.edit_tree
if nt is None:
continue
nt = area.spaces.active.edit_tree
if nt is None:
continue
# Check if this editor type is compatible
if area.spaces.active.tree_type in sdict:
# Add node to this editor
for n in nt.nodes:
n.select = False
node_type = sdict.get(nodegroup_type)
node_type = sdict.get(area.spaces.active.tree_type)
if node_type:
# Check if nodegroup is compatible with this editor
# For example, don't add shader nodegroups to geometry node editor
if (
nodegroup_type == "ShaderNodeTree"
and area.spaces.active.tree_type != "ShaderNodeTree"
) or (
nodegroup_type == "GeometryNodeTree"
and area.spaces.active.tree_type != "GeometryNodeTree"
):
continue
node = nt.nodes.new(node_type)
node.node_tree = nodegroup
node.location = (node_x, node_y)
@@ -113,42 +247,20 @@ def append_nodegroup(
added_to_editor = True
break
# If not added yet, try any compatible editor
if not added_to_editor:
for area in bpy.context.screen.areas:
if area.type != "NODE_EDITOR":
continue
nt = area.spaces.active.edit_tree
if nt is None:
continue
# Check if this editor type is compatible
if area.spaces.active.tree_type in sdict:
# Add node to this editor
for n in nt.nodes:
n.select = False
node_type = sdict.get(area.spaces.active.tree_type)
if node_type:
# Check if nodegroup is compatible with this editor
# For example, don't add shader nodegroups to geometry node editor
if (
nodegroup_type == "ShaderNodeTree"
and area.spaces.active.tree_type != "ShaderNodeTree"
) or (
nodegroup_type == "GeometryNodeTree"
and area.spaces.active.tree_type != "GeometryNodeTree"
):
continue
node = nt.nodes.new(node_type)
node.node_tree = nodegroup
node.location = (node_x, node_y)
node.select = True
nt.nodes.active = node
added_to_editor = True
break
# Ensure automatically created targets receive the nodegroup as modifier
if auto_created_target:
gn_mod = None
for mod in auto_created_target.modifiers:
if mod.type == "NODES":
gn_mod = mod
break
if not gn_mod:
gn_mod = auto_created_target.modifiers.new(
name=nodegroup.name, type="NODES"
)
gn_mod.node_group = nodegroup
auto_created_target.select_set(True)
bpy.context.view_layer.objects.active = auto_created_target
return nodegroup, added_to_editor
@@ -246,15 +358,18 @@ def hdr_swap(name, hdr):
:return: None
"""
w = bpy.context.scene.world
if w:
if not w:
new_hdr_world(name, hdr)
if bpy.app.version < (5, 0, 0):
w.use_nodes = True
w.name = name
nt = w.node_tree
for n in nt.nodes:
if "ShaderNodeTexEnvironment" == n.bl_rna.identifier:
env_node = n
env_node.image = hdr
return
w.name = name
nt = w.node_tree
for n in nt.nodes:
if "ShaderNodeTexEnvironment" == n.bl_rna.identifier:
env_node = n
env_node.image = hdr
return
new_hdr_world(name, hdr)
@@ -266,7 +381,8 @@ def new_hdr_world(name, hdr):
:return: None
"""
w = bpy.data.worlds.new(name=name)
w.use_nodes = True
if bpy.app.version < (5, 0, 0):
w.use_nodes = True
bpy.context.scene.world = w
nt = w.node_tree
@@ -302,11 +418,11 @@ def load_HDR(file_name, name):
def link_collection(
file_name,
obnames=None,
obnames: Optional[list] = None,
location=(0, 0, 0),
link=False,
parent=None,
collection="",
link: bool = False,
parent: Optional[str] = None,
collection: str = "",
**kwargs,
):
"""link an instanced group - model type asset"""
@@ -314,7 +430,7 @@ def link_collection(
obnames = []
sel = utils.selection_get()
# Store the original active collection
orig_active_collection = bpy.context.view_layer.active_layer_collection
orig_active_collection = bpy.context.view_layer.active_layer_collection # type: ignore[union-attr]
# Activate target collection if specified
if collection:
@@ -322,10 +438,10 @@ def link_collection(
if target_collection:
# Find and activate the layer collection
layer_collection = find_layer_collection(
bpy.context.view_layer.layer_collection, collection
bpy.context.view_layer.layer_collection, collection # type: ignore[union-attr]
)
if layer_collection:
bpy.context.view_layer.active_layer_collection = layer_collection
bpy.context.view_layer.active_layer_collection = layer_collection # type: ignore[union-attr]
with bpy.data.libraries.load(file_name, link=link, relative=True) as (
data_from,
@@ -340,33 +456,33 @@ def link_collection(
rotation = kwargs["rotation"]
bpy.ops.object.empty_add(type="PLAIN_AXES", location=location, rotation=rotation)
main_object = bpy.context.view_layer.objects.active
main_object.instance_type = "COLLECTION"
main_object = bpy.context.view_layer.objects.active # type: ignore[union-attr]
main_object.instance_type = "COLLECTION" # type: ignore[union-attr]
if parent is not None:
main_object.parent = bpy.data.objects.get(parent)
if parent is not None and parent != "":
main_object.parent = bpy.data.objects.get(parent) # type: ignore[union-attr]
main_object.matrix_world.translation = location
main_object.matrix_world.translation = location # type: ignore[union-attr]
for col in bpy.data.collections:
if col.library is not None:
fp = bpy.path.abspath(col.library.filepath)
fp1 = bpy.path.abspath(file_name)
if fp == fp1:
main_object.instance_collection = col
main_object.instance_collection = col # type: ignore[union-attr]
break
# sometimes, the lib might already be without the actual link.
if not main_object.instance_collection and kwargs["name"]:
if not main_object.instance_collection and kwargs["name"]: # type: ignore[union-attr]
col = bpy.data.collections.get(kwargs["name"])
if col:
main_object.instance_collection = col
main_object.instance_collection = col # type: ignore[union-attr]
main_object.name = main_object.instance_collection.name
main_object.name = main_object.instance_collection.name # type: ignore[union-attr]
# Restore original active collection
if orig_active_collection:
bpy.context.view_layer.active_layer_collection = orig_active_collection
bpy.context.view_layer.active_layer_collection = orig_active_collection # type: ignore[union-attr]
utils.selection_set(sel)
return main_object, []
@@ -449,7 +565,13 @@ def append_particle_system(
def append_objects(
file_name, obnames=None, location=(0, 0, 0), link=False, collection="", **kwargs
file_name,
obnames: Optional[list] = None,
location=(0, 0, 0),
link: bool = False,
parent: Optional[str] = None,
collection: str = "",
**kwargs,
):
"""Append object into scene individually. 2 approaches based in definition of name argument.
TODO: really split this function into 2 functions: kwargs.get('name')==None and else.
@@ -461,7 +583,7 @@ def append_objects(
scene = bpy.context.scene
sel = utils.selection_get()
# Store the original active collection
orig_active_collection = bpy.context.view_layer.active_layer_collection
orig_active_collection = bpy.context.view_layer.active_layer_collection # type: ignore[union-attr]
# Activate target collection if specified
if collection:
@@ -469,10 +591,10 @@ def append_objects(
if target_collection:
# Find and activate the layer collection
layer_collection = find_layer_collection(
bpy.context.view_layer.layer_collection, collection
bpy.context.view_layer.layer_collection, collection # type: ignore[union-attr]
)
if layer_collection:
bpy.context.view_layer.active_layer_collection = layer_collection
bpy.context.view_layer.active_layer_collection = layer_collection # type: ignore[union-attr]
try:
bpy.ops.object.select_all(action="DESELECT")
@@ -486,6 +608,9 @@ def append_objects(
path = file_name + "/Collection"
collection_name = kwargs.get("name")
if collection_name is None:
bk_logger.warning("collection_name is None")
collection_name = ""
bpy.ops.wm.append(filename=collection_name, directory=path)
# fc = utils.get_fake_context(bpy.context, area_type='VIEW_3D')
@@ -496,13 +621,13 @@ def append_objects(
appended_collection = None
main_object = None
# get first at least one parent for sure
for ob in bpy.context.scene.objects:
for ob in bpy.context.scene.objects: # type: ignore[union-attr]
if ob.select_get():
if not ob.parent:
main_object = ob
ob.location = location
# do once again to ensure hidden objects are hidden
for ob in bpy.context.scene.objects:
for ob in bpy.context.scene.objects: # type: ignore[union-attr]
if ob.select_get():
return_obs.append(ob)
# check for object that should be hidden
@@ -521,14 +646,14 @@ def append_objects(
if kwargs.get("rotation"):
main_object.rotation_euler = kwargs["rotation"]
if kwargs.get("parent") is not None:
main_object.parent = bpy.data.objects[kwargs["parent"]]
if parent is not None and parent != "":
main_object.parent = bpy.data.objects[parent]
main_object.matrix_world.translation = location
# move objects that should be hidden to a sub collection
if len(to_hidden_collection) > 0 and appended_collection is not None:
hidden_collections = []
scene_collection = bpy.context.scene.collection
scene_collection = bpy.context.scene.collection # type: ignore[union-attr]
for ob in to_hidden_collection:
hide_collection = ob.users_collection[0]
@@ -577,7 +702,7 @@ def append_objects(
# Restore original active collection
if orig_active_collection:
bpy.context.view_layer.active_layer_collection = orig_active_collection
bpy.context.view_layer.active_layer_collection = orig_active_collection # type: ignore[union-attr]
utils.selection_set(sel)
# let collection also store info that it was created by BlenderKit, for purging reasons
@@ -618,7 +743,7 @@ def append_objects(
for obj in data_to.objects:
if obj is not None:
# if obj.name not in scene.objects:
scene.collection.objects.link(obj)
scene.collection.objects.link(obj) # type: ignore[union-attr]
if obj.parent is None:
obj.location = location
main_object = obj
@@ -637,11 +762,11 @@ def append_objects(
ob.hide_viewport = True
if kwargs.get("rotation") is not None:
main_object.rotation_euler = kwargs["rotation"]
main_object.rotation_euler = kwargs["rotation"] # type: ignore[union-attr]
if kwargs.get("parent") is not None:
main_object.parent = bpy.data.objects[kwargs["parent"]]
main_object.matrix_world.translation = location
if parent is not None and parent != "":
main_object.parent = bpy.data.objects[parent] # type: ignore[union-attr]
main_object.matrix_world.translation = location # type: ignore[union-attr]
try:
bpy.ops.object.select_all(action="DESELECT")
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -82,7 +82,7 @@ def get_texture_ui(tpath, iname):
def check_thumbnail(props, imgpath):
# TODO implement check if the file exists, if size is corect etc. needs some care
# TODO implement check if the file exists, if size is correct etc. needs some care
if imgpath == "":
props.has_thumbnail = False
return None
@@ -104,7 +104,7 @@ if __name__ == "__main__":
asset_data["files"][0]["file_name"] = file_name
if not has_url:
bg_blender.progress(
"couldn't download asset for thumnbail re-rendering"
"couldn't download asset for thumbnail re-rendering"
)
exit()
# download first, or rather make sure if it's already downloaded
@@ -163,7 +163,7 @@ if __name__ == "__main__":
asset_data["files"][0]["file_name"] = file_name
if has_url is not True:
bg_blender.progress(
"couldn't download asset for thumnbail re-rendering"
"couldn't download asset for thumbnail re-rendering"
)
bg_blender.progress("downloading asset")
fpath = bg_utils.download_asset_file(
@@ -31,6 +31,8 @@ from bpy.props import BoolProperty
from . import client_lib, client_tasks, datas, global_vars, reports, tasks_queue, utils
if bpy.app.version >= (4, 2, 0):
from . import override_extension_draw
CLIENT_ID = "IdFRwa3SGA8eMpzhRVFMg5Ts8sPK93xBjif93x0F"
REFRESH_RESERVE = 60 * 60 * 24 * 3 # 3 days
@@ -103,10 +105,9 @@ def clean_login_data():
preferences.api_key_timeout = 0
global_vars.BKIT_PROFILE = datas.MineProfile()
# Cleanup also the api key in the extensions repository setting and clean the cache
from . import override_extension_draw
override_extension_draw.ensure_repository(api_key="")
override_extension_draw.clear_repo_cache()
if bpy.app.version >= (4, 2, 0):
override_extension_draw.ensure_repository(api_key="")
override_extension_draw.clear_repo_cache()
def logout() -> None:
@@ -158,8 +159,6 @@ def write_tokens(auth_token, refresh_token, oauth_response):
preferences.api_key = auth_token # triggers api_key update function
# write token also to extensions repository setting and clear the cache
if bpy.app.version >= (4, 2, 0):
from . import override_extension_draw
override_extension_draw.ensure_repository(api_key=auth_token)
override_extension_draw.clear_repo_cache()
@@ -20,7 +20,7 @@
bl_info = {
"name": "BlenderKit Online Asset Library",
"author": "Vilem Duha, Petr Dlouhy, A. Gajdosik",
"version": (3, 15, 1, 250403), # X.Y.Z.yymmdd
"version": (3, 17, 0, 251008), # X.Y.Z.yymmdd
"blender": (3, 0, 0),
"location": "View3D > Properties > BlenderKit",
"description": "Boost your workflow with drag&drop assets from the community driven library.",
@@ -28,9 +28,10 @@ bl_info = {
"tracker_url": "https://github.com/BlenderKit/blenderkit/issues",
"category": "3D View",
}
VERSION = (3, 15, 1, 250403)
VERSION = (3, 17, 0, 251008)
import logging
import random
import sys
from importlib import reload
from os import path
@@ -279,52 +280,39 @@ def asset_type_callback(self, context):
items for Enum property, depending on the down_up property - BlenderKit is either in search or in upload mode.
"""
pcoll = icons.icon_collections["main"]
preferences = bpy.context.preferences.addons[__package__].preferences
if self.down_up == "SEARCH":
items = [
("MODEL", "Models", "Find models", "OBJECT_DATAMODE", 0),
("MATERIAL", "Materials", "Find materials", "MATERIAL", 2),
("SCENE", "Scenes", "Find scenes", "SCENE_DATA", 3),
("HDR", "HDRs", "Find HDRs", "WORLD", 4),
("BRUSH", "Brushes", "Find brushes", "BRUSH_DATA", 5),
("NODEGROUP", "Node Groups", "Find tools", "NODETREE", 6),
("MATERIAL", "Materials", "Find materials", "MATERIAL", 1),
("SCENE", "Scenes", "Find scenes", "SCENE_DATA", 2),
("HDR", "HDRs", "Find HDRs", "WORLD", 3),
("BRUSH", "Brushes", "Find brushes", "BRUSH_DATA", 4),
("NODEGROUP", "Node Groups", "Find tools", "NODETREE", 5),
(
"PRINTABLE",
"Printables",
"Find 3D printable models",
pcoll["asset_type_printable"].icon_id,
6,
),
]
# Add printable under experimental features
if preferences.experimental_features:
# Insert printable after MODEL (at index 1)
items.insert(
1,
(
"PRINTABLE",
"Printable",
"Find 3D printable models",
pcoll["asset_type_printable"].icon_id,
1,
),
)
else:
items = [
("MODEL", "Model", "Upload a model", "OBJECT_DATAMODE", 0),
("MATERIAL", "Material", "Upload a material", "MATERIAL", 2),
("SCENE", "Scene", "Upload a scene", "SCENE_DATA", 3),
("HDR", "HDR", "Upload a HDR", "WORLD", 4),
("BRUSH", "Brush", "Upload a brush", "BRUSH_DATA", 5),
("NODEGROUP", "Node Groups", "Upload a tool", "NODETREE", 6),
("MATERIAL", "Material", "Upload a material", "MATERIAL", 1),
("SCENE", "Scene", "Upload a scene", "SCENE_DATA", 2),
("HDR", "HDR", "Upload a HDR", "WORLD", 3),
("BRUSH", "Brush", "Upload a brush", "BRUSH_DATA", 4),
("NODEGROUP", "Node Groups", "Upload a tool", "NODETREE", 5),
(
"PRINTABLE",
"Printable",
"Upload a 3D printable model",
pcoll["asset_type_printable"].icon_id,
6,
),
]
# Add printable under experimental features
if preferences.experimental_features:
# Insert printable after MODEL (at index 1)
items.insert(
1,
(
"PRINTABLE",
"Printable",
"Upload a 3D printable model",
pcoll["asset_type_printable"].icon_id,
1,
),
)
return items
@@ -392,6 +380,61 @@ class BlenderKitUIProps(PropertyGroup):
max=10,
update=search.search_update_delayed,
)
search_order_by: EnumProperty(
name="Order",
description="Search result order",
items=(
(
"default",
"Default",
"By default, the sorting algorithm changes dynamically based on search filters.",
),
("-created", "Newest", "Sort results from newest to oldest."),
("created", "Oldest", "Sort results from oldest to newest."),
(
"-bookmarks",
"▼ Bookmarks",
"Sort results from most bookmarked to least.",
),
(
"bookmarks",
"▲ Bookmarks",
"Sort results from least bookmarked to most.",
),
(
"-score",
"▼ Score",
"Sort results from highest asset score to the lowest.",
),
(
"score",
"▲ Score",
"Sort results from lowest asset score to the highest.",
),
(
"-working_hours",
"▼ Complexity",
"Sort results from most complex to the least.",
),
(
"working_hours",
"▲ Complexity",
"Sort results from least complex to the most.",
),
(
"-quality",
"▼ Quality",
"Sort results from highest quality rating to the lowest.",
),
(
"quality",
"▲ Quality",
"Sort results from lowest quality rating to the highest.",
),
),
default="default",
update=search.search_update,
)
search_license: EnumProperty(
name="License",
items=(
@@ -420,7 +463,7 @@ class BlenderKitUIProps(PropertyGroup):
)
search_blender_version_max: StringProperty(
name="Maximum version (excluding, lower than)",
default="4.99",
default="5.99",
description="Limit the assets by maximum version of Blender in which they were created, exluding the specified version and all newer versions from the search results. "
+ "Only assets created in LOWER THAN (< max) maximum version will be shown. Use semantic versioning format: X.Y.Z.\n\n"
+ "E.g.: exclude all Blender 4 assets by specifying 4, 4.0, or 4.0.0. Assets created in 3.6 and lower will be shown",
@@ -938,12 +981,6 @@ class BlenderKitMaterialSearchProps(PropertyGroup, BlenderKitCommonSearchProps):
"aren't editable directly, but also don't increase your file size",
default="APPEND",
)
automap: BoolProperty(
name="Auto-Map",
description="reset object texture space and also add automatically a cube mapped UV "
"to the object. \n this allows most materials to apply instantly to any mesh",
default=True,
)
class BlenderKitMaterialUploadProps(PropertyGroup, BlenderKitCommonUploadProps):
@@ -1031,7 +1068,7 @@ class BlenderKitMaterialUploadProps(PropertyGroup, BlenderKitCommonUploadProps):
thumbnail_background_lightness: FloatProperty(
name="Thumbnail Background Lightness",
description="Set to make your material stand out with enough contrast",
default=0.9,
default=0.7,
min=0.00001,
max=1,
)
@@ -1271,15 +1308,23 @@ class BlenderKitModelUploadProps(PropertyGroup, BlenderKitCommonUploadProps):
thumbnail_background_lightness: FloatProperty(
name="Thumbnail Background Lightness",
description="Set to make your Model stand out",
default=1.0,
default=0.7,
min=0.01,
max=10,
)
# for printable models
thumbnail_material_color: FloatVectorProperty(
name="Thumbnail Material Color",
description="Color of the material for printable models",
default=(random.random(), random.random(), random.random()),
subtype="COLOR",
)
thumbnail_angle: EnumProperty(
name="Thumbnail Angle",
items=autothumb.thumbnail_angles,
default="DEFAULT",
default="ANGLE_1",
description="Thumbnailer angle",
)
@@ -1455,6 +1500,19 @@ class BlenderKitModelUploadProps(PropertyGroup, BlenderKitCommonUploadProps):
default=False,
)
# Add this new property for printable assets
photo_thumbnail: StringProperty(
name="Photo Thumbnail",
description="Photo of the 3D printed object (JPG or PNG, preferred size is 1024x1024 or higher)",
subtype="FILE_PATH",
default="",
)
photo_thumbnail_will_upload_on_website: BoolProperty(
name="I will upload photo on website",
description="True if the photo thumbnail will upload on the website\n please read upload tutorial for more information",
default=False,
)
class BlenderKitSceneUploadProps(PropertyGroup, BlenderKitCommonUploadProps):
style: EnumProperty(
@@ -1924,7 +1982,11 @@ class BlenderKitAddonPreferences(AddonPreferences):
api_key: StringProperty(
name="BlenderKit API Key",
description="Your blenderkit API Key. Get it from your page on the website",
description=(
"Your unique API key authenticates downloads and requests inside the add-on. "
"No manual setup is required, the API Key is auto-filled at login and cleared at logout. "
"However, you can also paste the key from your profile settings on the BlenderKit website."
),
default="",
subtype="PASSWORD",
update=utils.api_key_property_updated,
@@ -1977,6 +2039,13 @@ class BlenderKitAddonPreferences(AddonPreferences):
update=utils.save_prefs,
)
sidebar_panels: BoolProperty(
name="Hide sidebar panels",
description="Hide BlenderKit sidebar panels (search, upload, and selected model functionality). This prevents upload and it's also the only place for import settings. Reenable this to access these features.",
default=False,
update=utils.save_prefs,
)
header_menu_fold: BoolProperty(
name="Header menu fold", default=False, update=ui_panels.update_header_menu_fold
)
@@ -2056,6 +2125,16 @@ class BlenderKitAddonPreferences(AddonPreferences):
default="2048",
)
material_import_automap: BoolProperty(
name="Auto-Map",
description="Reset object texture space and also add automatically a cube mapped UV to the object.\n"
"This allows most materials to apply instantly to any mesh",
default=True,
update=utils.save_prefs,
)
# NETWORKING
ip_version: EnumProperty(
name="IP version",
items=(
@@ -2133,7 +2212,7 @@ class BlenderKitAddonPreferences(AddonPreferences):
name="Custom proxy address",
description="""Set custom HTTP proxy for HTTPS requests of add-on. This setting preceeds any system wide proxy settings. If left empty custom proxy will not be set.
If you use simple HTTP proxy, set in format http://ip:port, or http://username:password@ip:port if your HTTP proxy requires authentication. You have to specify the address with http:// prefix.
If you use simple HTTP proxy, set in format http://ip:port, or http://username:password@ip:port if your HTTP proxy requires authentication (make sure to escape special characters like #$%:^&*() etc. in username and password). You have to specify the address with http:// prefix.
HTTPS proxies are not supported! We wait for support in Python 3.11 and in aiohttp module. You can specify the HTTPS proxy with https:// prefix for hacking around and development purposes, but functionality cannot be guaranteed.
In this case you should also set path to your system CA bundle containing proxy's certificates in the field "Custom CA certificates path" below""",
@@ -2196,15 +2275,21 @@ In this case you should also set path to your system CA bundle containing proxy'
update=utils.save_prefs,
)
max_assetbar_rows: IntProperty(
name="Max Assetbar Rows",
description="max rows of assetbar in the 3D view",
default=1,
min=1,
maximized_assetbar_rows: IntProperty(
name="Maximized Assetbar Rows",
description="Maximum rows of assetbar in the 3D view when expanded",
default=4,
min=2,
max=20,
update=utils.save_prefs,
)
assetbar_expanded: BoolProperty(
name="Assetbar Expanded",
description="Whether the assetbar is currently expanded to show maximum rows",
default=False,
)
thumb_size: IntProperty(
name="Assetbar Thumbnail Size",
default=96,
@@ -2225,7 +2310,7 @@ In this case you should also set path to your system CA bundle containing proxy'
experimental_features: BoolProperty(
name="Enable experimental features",
description="""Enable experimental features of BlenderKit: \n - 3D printable assets (search and upload models optimized for 3D printing)""",
description="Enable experimental features of BlenderKit. Note: There are no experimental features in this version.",
default=False,
update=utils.save_prefs,
)
@@ -2349,9 +2434,10 @@ In this case you should also set path to your system CA bundle containing proxy'
gui_settings.label(text="GUI settings")
gui_settings.prop(self, "show_on_start")
gui_settings.prop(self, "thumb_size")
gui_settings.prop(self, "max_assetbar_rows")
gui_settings.prop(self, "maximized_assetbar_rows")
gui_settings.prop(self, "search_field_width")
gui_settings.prop(self, "search_in_header")
gui_settings.prop(self, "sidebar_panels")
gui_settings.prop(self, "show_VIEW3D_MT_blenderkit_model_properties")
gui_settings.prop(self, "tips_on_start")
gui_settings.prop(self, "announcements_on_start")
@@ -19,15 +19,27 @@
import logging
import uuid
from typing import Optional
import bpy
from . import utils
from . import utils, reports
bk_logger = logging.getLogger(__name__)
def find_layer_collection(layer_collection, collection_name):
"""Helper function to find a layer_collection by name"""
if layer_collection.collection.name == collection_name:
return layer_collection
for child in layer_collection.children:
result = find_layer_collection(child, collection_name)
if result:
return result
return None
def append_brush(file_name, brushname=None, link=False, fake_user=True):
"""append a brush"""
with bpy.data.libraries.load(file_name, link=link, relative=True) as (
@@ -43,10 +55,38 @@ def append_brush(file_name, brushname=None, link=False, fake_user=True):
return brush
def append_nodegroup(file_name, nodegroupname=None, link=False, fake_user=True):
def append_nodegroup(
file_name,
nodegroupname=None,
link=False,
fake_user=True,
node_x=0,
node_y=0,
target_object=None,
nodegroup_mode="",
model_location=(0, 0, 0),
model_rotation=(0, 0, 0),
**kwargs,
):
"""Append selected node group. If nodegroupname is None, first node group is appended.
If node group with the same name is already in the scene, it is not appended again.
Try to look for a suitable node editor and insert the node group there, in the middle of the area.
Try to look for a suitable node editor and insert the node group there, or create/use modifier based on mode.
For geometry nodegroups, if no target object is provided, a target object will be created automatically.
Args:
file_name: Path to the .blend file containing the nodegroup
nodegroupname: Name of the nodegroup to append
link: Whether to link or append
fake_user: Whether to set fake user
node_x: X position for node placement in editor
node_y: Y position for node placement in editor
target_object: Target object for modifier mode (name string). If None and nodegroup is geometry type, a target object will be created
nodegroup_mode: How to add the nodegroup - "MODIFIER" for new modifier, "NODE" for node in editor, "" for default behavior
model_location: Location for the target object (used when creating new target)
model_rotation: Rotation for the target object (used when creating new target)
Returns:
tuple: (nodegroup, added_to_editor) - The nodegroup and whether it was added to an editor
"""
with bpy.data.libraries.load(file_name, link=link, relative=True) as (
data_from,
@@ -59,35 +99,153 @@ def append_nodegroup(file_name, nodegroupname=None, link=False, fake_user=True):
nodegroupname = g
nodegroup = bpy.data.node_groups[nodegroupname]
nodegroup.use_fake_user = fake_user
# if there's an open node editor, let's find if it matches the type of the asset group and insert it
# in middle of the area.
# mapping dict for editor type to node group node types
# Create target object automatically for geometry nodegroups when no target is provided
if nodegroup.bl_rna.identifier == "GeometryNodeTree" and not target_object:
# Create a default mesh cube
bpy.ops.mesh.primitive_cube_add(
size=2, location=model_location, rotation=model_rotation
)
target_obj = bpy.context.active_object
target_obj.name = "GeometryNodeTarget"
target_object = target_obj.name
# Make sure it's selected and active
bpy.context.view_layer.objects.active = target_obj
target_obj.select_set(True)
# Mapping dict for node editor tree types to node group node types
sdict = {
"GeometryNodeTree": "GeometryNodeGroup",
"ShaderNodeTree": "ShaderNodeGroup",
"CompositorNodeTree": "CompositorNodeGroup",
}
# Look for a suitable node editor and insert the node group there, in the middle of the area.
# Get the nodegroup type
nodegroup_type = nodegroup.bl_rna.identifier
# If no explicit mode is set, try to detect if we should add to an existing editor first
# This allows drag-drop into existing node editors to work properly
if not nodegroup_mode:
# Find a suitable node editor
for area in bpy.context.screen.areas:
if area.type != "NODE_EDITOR":
continue
if area.spaces.active.tree_type == nodegroup_type:
nt = area.spaces.active.edit_tree
if nt is None:
continue
# Add node to this editor
for n in nt.nodes:
n.select = False
node_type = sdict.get(nodegroup_type)
if node_type:
node = nt.nodes.new(node_type)
node.node_tree = nodegroup
node.location = (node_x, node_y)
node.select = True
nt.nodes.active = node
return (nodegroup, True)
# Handle modifier mode for geometry nodegroups
if nodegroup_mode == "MODIFIER" and target_object:
target_obj = bpy.data.objects.get(target_object)
if target_obj and nodegroup.bl_rna.identifier == "GeometryNodeTree":
# Create a new geometry nodes modifier with this nodegroup
gn_mod = target_obj.modifiers.new(name=nodegroup.name, type="NODES")
gn_mod.node_group = nodegroup
# Select the target object to make the change visible
bpy.context.view_layer.objects.active = target_obj
if target_obj not in bpy.context.selected_objects:
target_obj.select_set(True)
return (
nodegroup,
True,
) # Return True as we "added" it successfully to the modifier
# Handle node mode for geometry nodegroups with target object
# Create a modifier setup and then add the nodegroup as a node to the tree
if (
nodegroup_mode == "NODE"
and target_object
and nodegroup.bl_rna.identifier == "GeometryNodeTree"
):
target_obj = bpy.data.objects.get(target_object)
if target_obj:
# Select the target object to make it active
bpy.context.view_layer.objects.active = target_obj
if target_obj not in bpy.context.selected_objects:
target_obj.select_set(True)
# look for the geometry nodes modifier
gn_mod = None
for mod in target_obj.modifiers:
if mod.type == "NODES" and mod.node_group:
gn_mod = mod
break
if not gn_mod:
# create a new geometry nodes modifier
gn_mod = target_obj.modifiers.new(name="GeometryNodes", type="NODES")
if not gn_mod.node_group:
# create a new node group
bpy.ops.node.new_geometry_node_group_assign()
node_tree = gn_mod.node_group
if node_tree:
# Add the nodegroup as a node to the tree
group_node = node_tree.nodes.new("GeometryNodeGroup")
group_node.node_tree = nodegroup
group_node.location = (node_x, node_y)
group_node.select = True
node_tree.nodes.active = group_node
return (nodegroup, True)
# If not added yet through modes or if no mode specified, try to find any compatible editor
added_to_editor = False
# Try any compatible editor
for area in bpy.context.screen.areas:
if area.type != "NODE_EDITOR":
continue
if area.spaces.active.tree_type != nodegroup.bl_rna.identifier:
continue
nt = area.spaces.active.edit_tree
if nt is None:
continue
# deselect all nodes
for n in nt.nodes:
n.select = False
node = nt.nodes.new(sdict[area.spaces.active.tree_type])
node.node_tree = nodegroup
area.spaces.active.node_tree = nodegroup
break
return nodegroup
# Check if this editor type is compatible
if area.spaces.active.tree_type in sdict:
# Add node to this editor
for n in nt.nodes:
n.select = False
node_type = sdict.get(area.spaces.active.tree_type)
if node_type:
# Check if nodegroup is compatible with this editor
# For example, don't add shader nodegroups to geometry node editor
if (
nodegroup_type == "ShaderNodeTree"
and area.spaces.active.tree_type != "ShaderNodeTree"
) or (
nodegroup_type == "GeometryNodeTree"
and area.spaces.active.tree_type != "GeometryNodeTree"
):
continue
node = nt.nodes.new(node_type)
node.node_tree = nodegroup
node.location = (node_x, node_y)
node.select = True
nt.nodes.active = node
added_to_editor = True
break
return nodegroup, added_to_editor
def append_material(file_name, matname=None, link=False, fake_user=True):
@@ -183,15 +341,18 @@ def hdr_swap(name, hdr):
:return: None
"""
w = bpy.context.scene.world
if w:
if not w:
new_hdr_world(name, hdr)
if bpy.app.version < (5, 0, 0):
w.use_nodes = True
w.name = name
nt = w.node_tree
for n in nt.nodes:
if "ShaderNodeTexEnvironment" == n.bl_rna.identifier:
env_node = n
env_node.image = hdr
return
w.name = name
nt = w.node_tree
for n in nt.nodes:
if "ShaderNodeTexEnvironment" == n.bl_rna.identifier:
env_node = n
env_node.image = hdr
return
new_hdr_world(name, hdr)
@@ -203,7 +364,8 @@ def new_hdr_world(name, hdr):
:return: None
"""
w = bpy.data.worlds.new(name=name)
w.use_nodes = True
if bpy.app.version < (5, 0, 0):
w.use_nodes = True
bpy.context.scene.world = w
nt = w.node_tree
@@ -238,16 +400,36 @@ def load_HDR(file_name, name):
def link_collection(
file_name, obnames=[], location=(0, 0, 0), link=False, parent=None, **kwargs
file_name,
obnames: Optional[list] = None,
location=(0, 0, 0),
link: bool = False,
parent: Optional[str] = None,
collection: str = "",
**kwargs,
):
"""link an instanced group - model type asset"""
if obnames is None:
obnames = []
sel = utils.selection_get()
# Store the original active collection
orig_active_collection = bpy.context.view_layer.active_layer_collection # type: ignore[union-attr]
# Activate target collection if specified
if collection:
target_collection = bpy.data.collections.get(collection)
if target_collection:
# Find and activate the layer collection
layer_collection = find_layer_collection(
bpy.context.view_layer.layer_collection, collection # type: ignore[union-attr]
)
if layer_collection:
bpy.context.view_layer.active_layer_collection = layer_collection # type: ignore[union-attr]
with bpy.data.libraries.load(file_name, link=link, relative=True) as (
data_from,
data_to,
):
scols = []
for col in data_from.collections:
if col == kwargs["name"]:
data_to.collections = [col]
@@ -257,46 +439,44 @@ def link_collection(
rotation = kwargs["rotation"]
bpy.ops.object.empty_add(type="PLAIN_AXES", location=location, rotation=rotation)
main_object = bpy.context.view_layer.objects.active
main_object.instance_type = "COLLECTION"
main_object = bpy.context.view_layer.objects.active # type: ignore[union-attr]
main_object.instance_type = "COLLECTION" # type: ignore[union-attr]
if parent is not None:
main_object.parent = bpy.data.objects.get(parent)
if parent is not None and parent != "":
main_object.parent = bpy.data.objects.get(parent) # type: ignore[union-attr]
main_object.matrix_world.translation = location
main_object.matrix_world.translation = location # type: ignore[union-attr]
for col in bpy.data.collections:
if col.library is not None:
fp = bpy.path.abspath(col.library.filepath)
fp1 = bpy.path.abspath(file_name)
if fp == fp1:
main_object.instance_collection = col
main_object.instance_collection = col # type: ignore[union-attr]
break
# sometimes, the lib might already be without the actual link.
if not main_object.instance_collection and kwargs["name"]:
if not main_object.instance_collection and kwargs["name"]: # type: ignore[union-attr]
col = bpy.data.collections.get(kwargs["name"])
if col:
main_object.instance_collection = col
main_object.instance_collection = col # type: ignore[union-attr]
main_object.name = main_object.instance_collection.name
main_object.name = main_object.instance_collection.name # type: ignore[union-attr]
# bpy.ops.wm.link(directory=file_name + "/Collection/", filename=kwargs['name'], link=link, instance_collections=True,
# autoselect=True)
# main_object = bpy.context.view_layer.objects.active
# if kwargs.get('rotation') is not None:
# main_object.rotation_euler = kwargs['rotation']
# main_object.location = location
# Restore original active collection
if orig_active_collection:
bpy.context.view_layer.active_layer_collection = orig_active_collection # type: ignore[union-attr]
utils.selection_set(sel)
return main_object, []
def append_particle_system(
file_name, obnames=[], location=(0, 0, 0), link=False, **kwargs
file_name, obnames=None, location=(0, 0, 0), link=False, **kwargs
):
"""link an instanced group - model type asset"""
if obnames is None:
obnames = []
pss = []
with bpy.data.libraries.load(file_name, link=link, relative=True) as (
data_from,
@@ -367,18 +547,53 @@ def append_particle_system(
return target_object, []
def append_objects(file_name, obnames=[], location=(0, 0, 0), link=False, **kwargs):
def append_objects(
file_name,
obnames: Optional[list] = None,
location=(0, 0, 0),
link: bool = False,
parent: Optional[str] = None,
collection: str = "",
**kwargs,
):
"""Append object into scene individually. 2 approaches based in definition of name argument.
TODO: really split this function into 2 functions: kwargs.get('name')==None and else.
"""
if obnames is None:
obnames = []
# simplified version of append
if kwargs.get("name"):
scene = bpy.context.scene
sel = utils.selection_get()
bpy.ops.object.select_all(action="DESELECT")
# Store the original active collection
orig_active_collection = bpy.context.view_layer.active_layer_collection # type: ignore[union-attr]
# Activate target collection if specified
if collection:
target_collection = bpy.data.collections.get(collection)
if target_collection:
# Find and activate the layer collection
layer_collection = find_layer_collection(
bpy.context.view_layer.layer_collection, collection # type: ignore[union-attr]
)
if layer_collection:
bpy.context.view_layer.active_layer_collection = layer_collection # type: ignore[union-attr]
try:
bpy.ops.object.select_all(action="DESELECT")
except Exception as e:
reports.add_report(
f"append_objects.1: {str(e)}",
3,
type="ERROR",
)
raise e
path = file_name + "/Collection"
collection_name = kwargs.get("name")
if collection_name is None:
bk_logger.warning("collection_name is None")
collection_name = ""
bpy.ops.wm.append(filename=collection_name, directory=path)
# fc = utils.get_fake_context(bpy.context, area_type='VIEW_3D')
@@ -386,22 +601,22 @@ def append_objects(file_name, obnames=[], location=(0, 0, 0), link=False, **kwar
return_obs = []
to_hidden_collection = []
collection = None
appended_collection = None
main_object = None
# get first at least one parent for sure
for ob in bpy.context.scene.objects:
for ob in bpy.context.scene.objects: # type: ignore[union-attr]
if ob.select_get():
if not ob.parent:
main_object = ob
ob.location = location
# do once again to ensure hidden objects are hidden
for ob in bpy.context.scene.objects:
for ob in bpy.context.scene.objects: # type: ignore[union-attr]
if ob.select_get():
return_obs.append(ob)
# check for object that should be hidden
if ob.users_collection[0].name == collection_name:
collection = ob.users_collection[0]
collection["is_blenderkit_asset"] = True
appended_collection = ob.users_collection[0]
appended_collection["is_blenderkit_asset"] = True
if not ob.parent:
main_object = ob
ob.location = location
@@ -414,14 +629,14 @@ def append_objects(file_name, obnames=[], location=(0, 0, 0), link=False, **kwar
if kwargs.get("rotation"):
main_object.rotation_euler = kwargs["rotation"]
if kwargs.get("parent") is not None:
main_object.parent = bpy.data.objects[kwargs["parent"]]
if parent is not None and parent != "":
main_object.parent = bpy.data.objects[parent]
main_object.matrix_world.translation = location
# move objects that should be hidden to a sub collection
if len(to_hidden_collection) > 0 and collection is not None:
if len(to_hidden_collection) > 0 and appended_collection is not None:
hidden_collections = []
scene_collection = bpy.context.scene.collection
scene_collection = bpy.context.scene.collection # type: ignore[union-attr]
for ob in to_hidden_collection:
hide_collection = ob.users_collection[0]
@@ -434,7 +649,11 @@ def append_objects(file_name, obnames=[], location=(0, 0, 0), link=False, **kwar
h_col = bpy.data.collections.get(hidden_collection_name)
if h_col is None:
h_col = bpy.data.collections.new(name=hidden_collection_name)
collection.children.link(h_col)
# If target collection is specified, make the hidden collection a child of target collection
if collection and bpy.data.collections.get(collection):
bpy.data.collections.get(collection).children.link(h_col)
else:
appended_collection.children.link(h_col)
utils.exclude_collection(hidden_collection_name)
ob.users_collection[0].objects.unlink(ob)
@@ -443,12 +662,31 @@ def append_objects(file_name, obnames=[], location=(0, 0, 0), link=False, **kwar
if hide_collection in hidden_collections:
continue
# All other collections are moved to be children of the model collection
bk_logger.info(f"{hide_collection}, {collection}")
utils.move_collection(hide_collection, collection)
bk_logger.info(f"{hide_collection}, {appended_collection}")
# If target collection is specified, move collections there instead
if collection and bpy.data.collections.get(collection):
utils.move_collection(
hide_collection, bpy.data.collections.get(collection)
)
else:
utils.move_collection(hide_collection, appended_collection)
utils.exclude_collection(hide_collection.name)
hidden_collections.append(hide_collection)
bpy.ops.object.select_all(action="DESELECT")
try:
bpy.ops.object.select_all(action="DESELECT")
except Exception as e:
reports.add_report(
f"append_objects.2: {str(e)}",
3,
type="ERROR",
)
raise e
# Restore original active collection
if orig_active_collection:
bpy.context.view_layer.active_layer_collection = orig_active_collection # type: ignore[union-attr]
utils.selection_set(sel)
# let collection also store info that it was created by BlenderKit, for purging reasons
@@ -471,7 +709,15 @@ def append_objects(file_name, obnames=[], location=(0, 0, 0), link=False, **kwar
# link them to scene
scene = bpy.context.scene
sel = utils.selection_get()
bpy.ops.object.select_all(action="DESELECT")
try:
bpy.ops.object.select_all(action="DESELECT")
except Exception as e:
reports.add_report(
f"append_objects.3: {str(e)}",
3,
type="ERROR",
)
raise e
return_obs = [] # this might not be needed, but better be sure to rewrite the list.
main_object = None
@@ -480,7 +726,7 @@ def append_objects(file_name, obnames=[], location=(0, 0, 0), link=False, **kwar
for obj in data_to.objects:
if obj is not None:
# if obj.name not in scene.objects:
scene.collection.objects.link(obj)
scene.collection.objects.link(obj) # type: ignore[union-attr]
if obj.parent is None:
obj.location = location
main_object = obj
@@ -499,13 +745,21 @@ def append_objects(file_name, obnames=[], location=(0, 0, 0), link=False, **kwar
ob.hide_viewport = True
if kwargs.get("rotation") is not None:
main_object.rotation_euler = kwargs["rotation"]
main_object.rotation_euler = kwargs["rotation"] # type: ignore[union-attr]
if kwargs.get("parent") is not None:
main_object.parent = bpy.data.objects[kwargs["parent"]]
main_object.matrix_world.translation = location
if parent is not None and parent != "":
main_object.parent = bpy.data.objects[parent] # type: ignore[union-attr]
main_object.matrix_world.translation = location # type: ignore[union-attr]
bpy.ops.object.select_all(action="DESELECT")
try:
bpy.ops.object.select_all(action="DESELECT")
except Exception as e:
reports.add_report(
f"append_objects.4: {str(e)}",
3,
type="ERROR",
)
raise e
utils.selection_set(sel)
return main_object, return_obs
@@ -30,7 +30,6 @@ from . import (
global_vars,
paths,
ratings_utils,
reports,
search,
ui,
ui_panels,
@@ -76,6 +75,27 @@ def modal_inside(self, context, event):
ui_props = bpy.context.window_manager.blenderkitUI
user_preferences = bpy.context.preferences.addons[__package__].preferences
# HANDLE PHOTO THUMBNAIL SWITCH
if hasattr(self, "needs_tooltip_update") and self.needs_tooltip_update:
self.needs_tooltip_update = False
sr = search.get_search_results()
if sr and self.active_index < len(sr):
asset_data = sr[self.active_index]
if asset_data["assetType"].lower() == "printable":
if self.show_photo_thumbnail:
photo_img = ui.get_full_photo_thumbnail(asset_data)
if photo_img:
self.tooltip_image.set_image(photo_img.filepath)
self.tooltip_image.set_image_colorspace("")
else:
self.tooltip_image.set_image(
paths.get_addon_thumbnail_path("thumbnail_notready.jpg")
)
else:
set_thumb_check(
self.tooltip_image, asset_data, thumb_type="thumbnail"
)
if ui_props.turn_off:
ui_props.turn_off = False
self.finish()
@@ -96,9 +116,15 @@ def modal_inside(self, context, event):
if sr is not None:
# this check runs more search, usefull especially for first search. Could be moved to a better place where the check
# doesn't run that often.
# Calculate current max rows based on expanded state
if user_preferences.assetbar_expanded:
current_max_rows = user_preferences.maximized_assetbar_rows
else:
current_max_rows = 1
if (
len(sr) - ui_props.scroll_offset
< (ui_props.wcount * user_preferences.max_assetbar_rows) + 15
< (ui_props.wcount * current_max_rows) + 15
):
self.search_more()
@@ -427,6 +453,13 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
default="Runs search and displays the asset bar at the same time"
)
show_photo_thumbnail: BoolProperty( # type: ignore[valid-type]
name="Show Photo Thumbnail",
description="Toggle between normal and photo thumbnail - use [ or ] to cycle through thumbnails. Currently used only for printables.",
default=False,
options={"SKIP_SAVE"},
)
@classmethod
def description(cls, context, properties):
return properties.tooltip
@@ -446,9 +479,6 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
self.tooltip_scale = 1.0
self.tooltip_height = self.tooltip_size
self.tooltip_width = self.tooltip_size
ui_props = bpy.context.window_manager.blenderkitUI
if ui_props.asset_type == "HDR":
self.tooltip_width = self.tooltip_size * 2
# total_size = tooltip# + 2 * self.margin
self.tooltip_panel = BL_UI_Drag_Panel(
0, 0, self.tooltip_width, self.tooltip_height
@@ -722,15 +752,19 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
self.max_hcount = math.floor(
max(region.width, context.window.width) / self.button_size
)
self.max_wcount = user_preferences.max_assetbar_rows
self.max_wcount = user_preferences.maximized_assetbar_rows
history_step = search.get_active_history_step()
search_results = history_step.get("search_results")
# we need to init all possible thumb previews in advance/
# self.hcount = user_preferences.max_assetbar_rows
# Calculate hcount based on expanded state
if search_results is not None and self.wcount > 0:
if user_preferences.assetbar_expanded:
max_rows = user_preferences.maximized_assetbar_rows
else:
max_rows = 1
self.hcount = min(
user_preferences.max_assetbar_rows,
max_rows,
math.ceil(len(search_results) / self.wcount),
)
self.hcount = max(self.hcount, 1)
@@ -768,6 +802,9 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
self.button_close.set_location(
self.bar_width - self.other_button_size, -self.other_button_size
)
self.button_expand.set_location(
self.bar_width - self.other_button_size, self.bar_height
)
# if hasattr(self, 'button_notifications'):
# self.button_notifications.set_location(self.bar_width - self.other_button_size * 2, -self.other_button_size)
self.button_scroll_up.set_location(self.bar_width, 0)
@@ -1026,6 +1063,25 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
self.widgets_panel.append(self.button_close)
# Expand/collapse button (positioned at bottom of assetbar)
self.button_expand = BL_UI_Button(
self.bar_width - self.other_button_size,
self.bar_height,
self.other_button_size,
self.other_button_size,
)
self.button_expand.bg_color = self.button_bg_color
self.button_expand.hover_bg_color = self.button_hover_color
self.button_expand.text = ""
self.button_expand.text_size = self.other_button_size * 0.8
self.button_expand.set_image_position((0, 0))
self.button_expand.set_image_size(
(self.other_button_size, self.other_button_size)
)
self.button_expand.set_mouse_down(self.toggle_expand)
self.widgets_panel.append(self.button_expand)
self.scroll_width = 30
self.button_scroll_down = BL_UI_Button(
-self.scroll_width, 0, self.scroll_width, self.bar_height
@@ -1069,9 +1125,14 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
)
self.history_back_button.bg_color = self.button_bg_color
self.history_back_button.hover_bg_color = self.button_hover_color
self.history_back_button.text = ""
self.history_back_button.text_size = button_size * 0.5
self.history_back_button.text_color = self.text_color
self.history_back_button.text = ""
icon_size = int(button_size * 0.6)
margin_lr = int((button_size - icon_size) / 2)
self.history_back_button.set_image(
paths.get_addon_thumbnail_path("history_back.png")
)
self.history_back_button.set_image_size((icon_size, icon_size))
self.history_back_button.set_image_position((margin_lr, margin_lr))
self.history_forward_button = BL_UI_Button(
margin * 2 + button_size,
@@ -1081,9 +1142,12 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
)
self.history_forward_button.bg_color = self.button_bg_color
self.history_forward_button.hover_bg_color = self.button_hover_color
self.history_forward_button.text = ""
self.history_forward_button.text_size = button_size * 0.5
self.history_forward_button.text_color = self.text_color
self.history_forward_button.text = ""
self.history_forward_button.set_image(
paths.get_addon_thumbnail_path("history_forward.png")
)
self.history_forward_button.set_image_size((icon_size, icon_size))
self.history_forward_button.set_image_position((margin_lr, margin_lr))
# Tab buttons
tabs = global_vars.TABS["tabs"]
@@ -1211,6 +1275,9 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
# Update tab icons
self.update_tab_icons()
# Update expand button icon
self.update_expand_button_icon()
def update_tab_icons(self):
"""Update tab icons based on the active history step's asset type"""
tabs = global_vars.TABS["tabs"]
@@ -1240,6 +1307,16 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
tab_button.set_image(icon_path)
tab_button.set_image_colorspace("")
def update_expand_button_icon(self):
"""Update expand button icon based on current expanded state."""
user_preferences = bpy.context.preferences.addons[__package__].preferences
if user_preferences.assetbar_expanded:
# Show up arrow when expanded (to collapse)
self.button_expand.text = ""
else:
# Show down arrow when collapsed (to expand)
self.button_expand.text = ""
def position_and_hide_buttons(self):
# position and layout buttons
sr = search.get_search_results()
@@ -1320,6 +1397,7 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
self.tooltip_base_size_pixels = 512
self.tooltip_scale = 1.0
self.bottom_panel_fraction = 0.15
self.needs_tooltip_update = False
self.update_ui_size(bpy.context)
# todo move all this to update UI size
@@ -1441,7 +1519,6 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
def update_tooltip_image(self, asset_id):
"""Update tootlip image when it finishes downloading and the downloaded image matches the active one."""
search_results = search.get_search_results()
if search_results is None:
return
@@ -1499,7 +1576,23 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
# self.tooltip = asset_data['tooltip']
ui_props = bpy.context.window_manager.blenderkitUI
ui_props.active_index = search_index # + self.scroll_offset
set_thumb_check(self.tooltip_image, asset_data, thumb_type="thumbnail")
# Update tooltip size based on asset type
if (
asset_data["assetType"].lower() == "printable"
and self.show_photo_thumbnail
):
photo_img = ui.get_full_photo_thumbnail(asset_data)
if photo_img:
self.tooltip_image.set_image(photo_img.filepath)
self.tooltip_image.set_image_colorspace("")
else:
self.tooltip_image.set_image(
paths.get_addon_thumbnail_path("thumbnail_notready.jpg")
)
else:
set_thumb_check(self.tooltip_image, asset_data, thumb_type="thumbnail")
get_tooltip_data(asset_data)
an = asset_data["displayName"]
max_name_length = 30
@@ -1615,7 +1708,7 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
now = time.time()
# avoid double click to download assets under panels, mainly category panel
if now - ui_panels.last_time_dropdown_active < 0.5:
if now - ui_panels.last_time_overlay_panel_active < 0.5:
return
# start drag drop
bpy.ops.view3d.asset_drag_drop(
@@ -1626,6 +1719,17 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
def cancel_press(self, widget):
self.finish()
def toggle_expand(self, widget):
"""Toggle the expanded state of the assetbar."""
user_preferences = bpy.context.preferences.addons[__package__].preferences
user_preferences.assetbar_expanded = not user_preferences.assetbar_expanded
# Update the button icon
self.update_expand_button_icon()
# Restart the asset bar to apply the new layout
self.restart_asset_bar()
def asset_menu(self, widget):
self.hide_tooltip()
bpy.ops.wm.blenderkit_asset_popup("INVOKE_DEFAULT")
@@ -1668,14 +1772,21 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
bookmark_button.set_image(img_fp)
def update_progress_bar(self, asset_button, asset_data):
if asset_data["downloaded"] > 0:
pb = asset_button.progress_bar
w = int(self.button_size * asset_data["downloaded"] / 100.0)
asset_button.progress_bar.width = w
asset_button.progress_bar.update(pb.x_screen, pb.y_screen)
asset_button.progress_bar.visible = True
else:
"""Update progress bar for an asset button."""
pb = asset_button.progress_bar
if pb is None:
return
if asset_data["downloaded"] == 0:
asset_button.progress_bar.visible = False
return
w = int(self.button_size * asset_data["downloaded"] / 100.0)
asset_button.progress_bar.width = w
asset_button.progress_bar.update(pb.x_screen, pb.y_screen)
asset_button.progress_bar.visible = True
if bpy.context.region is not None:
bpy.context.region.tag_redraw()
def update_validation_icon(self, asset_button, asset_data: dict):
if utils.profile_is_validator():
@@ -1853,6 +1964,30 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
search.search()
def handle_key_input(self, event):
# Check if enough time has passed since last popup/text input activity
# to prevent shortcuts from triggering while typing in text fields
now = time.time()
if now - ui_panels.last_time_overlay_panel_active < 0.5:
return False
# Shortcut: Toggle between normal and photo thumbnail
if event.type in {"ONE"}:
if self.show_photo_thumbnail == True:
self.show_photo_thumbnail = False
self.needs_tooltip_update = True
if event.type in {"TWO"}:
if self.show_photo_thumbnail == False:
self.show_photo_thumbnail = True
self.needs_tooltip_update = True
if (
event.type in {"LEFT_BRACKET", "RIGHT_BRACKET"}
and not event.shift
and self.active_index > -1
):
self.show_photo_thumbnail = not self.show_photo_thumbnail
self.needs_tooltip_update = True
return True
# Shortcut: Search by author
if event.type == "A":
self.search_by_author(self.active_index)
@@ -2166,6 +2301,49 @@ class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
)
def handle_bkclientjs_get_asset(task: search.client_tasks.Task):
"""Handle incoming bkclientjs/get_asset task after the user asked for download in online gallery. How it goes:
1. set search in the history
2. set the results in the history step
3. open the asset bar
We handle the task in asset_bar_op because we need access to the asset_bar_operator without circular import from search.
"""
bk_logger.info(f"handle_bkclientjs_get_asset: {task.result['asset_data']['name']}")
# Get asset data from task result
asset_data = task.result.get("asset_data")
if not asset_data:
bk_logger.error("No asset data found in task")
return
# Parse the asset data
parsed_asset_data = search.parse_result(asset_data)
if not parsed_asset_data:
bk_logger.error("Failed to parse asset data")
return
search.append_history_step(
search_keywords=f"asset_base_id:{asset_data['assetBaseId']}",
search_results=[parsed_asset_data],
asset_type=asset_data.get("assetType", "").upper(),
search_results_orig={"results": [asset_data], "count": 1},
)
# If asset bar is not open, try to open it
if asset_bar_operator is None:
try:
bpy.ops.view3d.run_assetbar_fix_context(keep_running=True, do_search=False) # type: ignore[attr-defined]
except Exception as e:
bk_logger.error(f"Failed to open asset bar: {e}")
return
# Force redraw of the region if asset bar exists
if asset_bar_operator and asset_bar_operator.area:
search.load_preview(parsed_asset_data)
asset_bar_operator.update_image(parsed_asset_data["assetBaseId"])
asset_bar_operator.area.tag_redraw()
BlenderKitAssetBarOperator.modal = asset_bar_modal # type: ignore[method-assign]
BlenderKitAssetBarOperator.invoke = asset_bar_invoke # type: ignore[method-assign]
@@ -20,12 +20,19 @@
import json
import logging
import os
import random
import subprocess
import tempfile
from pathlib import Path
import bpy
from bpy.props import BoolProperty, EnumProperty, FloatProperty, IntProperty
from bpy.props import (
BoolProperty,
EnumProperty,
FloatProperty,
IntProperty,
FloatVectorProperty,
)
from . import bg_blender, global_vars, paths, tasks_queue, utils, upload, search
@@ -41,7 +48,8 @@ thumbnail_resolutions = (
)
thumbnail_angles = (
("DEFAULT", "default", ""),
("ANGLE_1", "Angle 1", "Lower hanging camera angle"),
("ANGLE_2", "Angle 2", "Higher hanging camera angle"),
("FRONT", "front", ""),
("SIDE", "side", ""),
("TOP", "top", ""),
@@ -320,13 +328,17 @@ class GenerateThumbnailOperator(bpy.types.Operator):
return bpy.context.view_layer.objects.active is not None
def draw(self, context):
ob = bpy.context.active_object
while ob.parent is not None:
ob = ob.parent
ui_props = bpy.context.window_manager.blenderkitUI
asset_type = ui_props.asset_type
ob = utils.get_active_model()
props = ob.blenderkit
layout = self.layout
layout.label(text="thumbnailer settings")
layout.prop(props, "thumbnail_background_lightness")
# for printable models
if asset_type == "PRINTABLE":
layout.prop(props, "thumbnail_material_color")
layout.prop(props, "thumbnail_angle")
layout.prop(props, "thumbnail_snap_to")
layout.prop(props, "thumbnail_samples")
@@ -382,20 +394,27 @@ class GenerateThumbnailOperator(bpy.types.Operator):
obnames = []
for ob in obs:
obnames.append(ob.name)
# asset type can be model or printable
ui_props = bpy.context.window_manager.blenderkitUI
asset_type = ui_props.asset_type
args_dict = {
"type": "material",
"type": asset_type,
"asset_name": asset.name,
"filepath": filepath,
"thumbnail_path": thumb_path,
"tempdir": tempdir,
}
thumbnail_args = {
"type": "model",
"type": asset_type,
"models": str(obnames),
"thumbnail_angle": bkit.thumbnail_angle,
"thumbnail_snap_to": bkit.thumbnail_snap_to,
"thumbnail_background_lightness": bkit.thumbnail_background_lightness,
"thumbnail_material_color": (
bkit.thumbnail_material_color[0],
bkit.thumbnail_material_color[1],
bkit.thumbnail_material_color[2],
),
"thumbnail_resolution": bkit.thumbnail_resolution,
"thumbnail_samples": bkit.thumbnail_samples,
"thumbnail_denoising": bkit.thumbnail_denoising,
@@ -409,12 +428,6 @@ class GenerateThumbnailOperator(bpy.types.Operator):
def invoke(self, context, event):
wm = context.window_manager
# if bpy.data.filepath == '':
# ui_panels.ui_message(
# title="Can't render thumbnail",
# message="please save your file first")
#
# return {'FINISHED'}
return wm.invoke_props_dialog(self, width=400)
@@ -448,10 +461,17 @@ class ReGenerateThumbnailOperator(bpy.types.Operator):
max=10,
)
thumbnail_material_color: FloatVectorProperty(
name="Thumbnail Material Color",
description="Color of the material for printable models",
default=(random.random(), random.random(), random.random()),
subtype="COLOR",
)
thumbnail_angle: EnumProperty( # type: ignore[valid-type]
name="Thumbnail Angle",
items=thumbnail_angles,
default="DEFAULT",
default="ANGLE_1",
description="thumbnailer angle",
)
@@ -491,6 +511,9 @@ class ReGenerateThumbnailOperator(bpy.types.Operator):
layout.label(text="Server-side rendering may take several hours", icon="INFO")
layout.label(text="thumbnailer settings")
layout.prop(props, "thumbnail_background_lightness")
# for printable models
if self.asset_type == "PRINTABLE":
layout.prop(props, "thumbnail_material_color")
layout.prop(props, "thumbnail_angle")
layout.prop(props, "thumbnail_snap_to")
layout.prop(props, "thumbnail_samples")
@@ -503,17 +526,12 @@ class ReGenerateThumbnailOperator(bpy.types.Operator):
if not self.asset_index > -1:
return {"CANCELLED"}
# Get search results from history
history_step = search.get_active_history_step()
sr = history_step.get("search_results", [])
asset_data = sr[self.asset_index]
preferences = bpy.context.preferences.addons[__package__].preferences
if not self.render_locally:
# Use server-side thumbnail regeneration
success = upload.mark_for_thumbnail(
asset_id=asset_data["id"],
asset_id=self.asset_data["id"],
api_key=preferences.api_key,
use_gpu=preferences.thumbnail_use_gpu,
samples=self.thumbnail_samples,
@@ -536,13 +554,16 @@ class ReGenerateThumbnailOperator(bpy.types.Operator):
# Local thumbnail generation (original functionality)
tempdir = tempfile.mkdtemp()
an_slug = paths.slugify(asset_data["name"])
an_slug = paths.slugify(self.asset_data["name"])
thumb_path = os.path.join(tempdir, an_slug)
# asset type can be model or printable
ui_props = bpy.context.window_manager.blenderkitUI
self.asset_type = ui_props.asset_type
args_dict = {
"type": "material",
"asset_name": asset_data["name"],
"asset_data": asset_data,
"type": self.asset_type,
"asset_name": self.asset_data["name"],
"asset_data": self.asset_data,
# "filepath": filepath,
"thumbnail_path": thumb_path,
"tempdir": tempdir,
@@ -550,7 +571,7 @@ class ReGenerateThumbnailOperator(bpy.types.Operator):
"upload_after_render": True,
}
thumbnail_args = {
"type": "model",
"type": self.asset_type,
"thumbnail_angle": self.thumbnail_angle,
"thumbnail_snap_to": self.thumbnail_snap_to,
"thumbnail_background_lightness": self.thumbnail_background_lightness,
@@ -565,6 +586,11 @@ class ReGenerateThumbnailOperator(bpy.types.Operator):
def invoke(self, context, event):
wm = context.window_manager
# Get search results from history
history_step = search.get_active_history_step()
sr = history_step.get("search_results", [])
self.asset_data = sr[self.asset_index]
return wm.invoke_props_dialog(self, width=400)
@@ -21,6 +21,8 @@
import json
import math
import os
import random
import colorsys
import sys
from traceback import print_exc
@@ -110,6 +112,25 @@ def patch_imports(addon_module_name: str):
print(f"- Local repository {parts[1]} added")
def replace_materials(obs, material_name):
"""Replace all materials on objects with the specified material
Args:
obs: List of objects to process
material_name: Name of the material to apply to all objects
"""
material = bpy.data.materials.get(material_name)
if not material:
bg_blender.progress(f"Material {material_name} not found")
return
for ob in obs:
if ob.type == "MESH":
# Clear all material slots and add the specified material
ob.data.materials.clear()
ob.data.materials.append(material)
return material
if __name__ == "__main__":
try:
# args order must match the order in blenderkit/autothumb.py:get_thumbnailer_args()!
@@ -187,10 +208,11 @@ if __name__ == "__main__":
bpy.context.preferences.addons["cycles"].preferences.refresh_devices()
fdict = {
"DEFAULT": 1,
"FRONT": 2,
"SIDE": 3,
"TOP": 4,
"ANGLE_1": 1,
"ANGLE_2": 2,
"FRONT": 3,
"SIDE": 4,
"TOP": 5,
}
s = bpy.context.scene
s.frame_set(fdict[data["thumbnail_angle"]])
@@ -210,9 +232,41 @@ if __name__ == "__main__":
collection.hide_select = False
main_object.rotation_euler = (0, 0, 0)
# Add material replacement for printable assets
# works directly with the specific material that has a color node for input
if data.get("type") == "PRINTABLE":
material = replace_materials(allobs, "PrintableMaterial")
# Find the BaseColor node in this material
base_color_node = material.node_tree.nodes.get("BaseColor")
if base_color_node:
# randomize the color value, needs to be defined by random hue and saturation = 0.95, we need to convert it to RGB then
# random_color = (random.random(), 0.95, 0.5)
# # convert to RGB
# random_color = colorsys.hsv_to_rgb(
# random_color[0], random_color[1], random_color[2]
# )
random_color = data["thumbnail_material_color"]
base_color_node.outputs[0].default_value = (
random_color[0],
random_color[1],
random_color[2],
1,
)
# now let's make background color complementary to the material color
bpy.data.materials["bkit background"].node_tree.nodes[
"BaseColor"
].outputs["Color"].default_value = (
1 - random_color[0],
1 - random_color[1],
1 - random_color[2],
1,
)
bpy.data.materials["bkit background"].node_tree.nodes["Value"].outputs[
"Value"
].default_value = data["thumbnail_background_lightness"]
s.cycles.samples = data["thumbnail_samples"]
bpy.context.view_layer.cycles.use_denoising = data["thumbnail_denoising"]
bpy.context.view_layer.update()
@@ -237,11 +291,11 @@ if __name__ == "__main__":
bg_blender.progress("rendering thumbnail")
render_thumbnails()
if not data.get("upload_after_render") or not data.get("asset_data"):
bg_blender.progress(
"background autothumbnailer finished successfully (no upload)"
)
sys.exit(0)
bg_blender.progress("uploading thumbnail")
@@ -102,6 +102,11 @@ def clean_login_data():
preferences.api_key = ""
preferences.api_key_timeout = 0
global_vars.BKIT_PROFILE = datas.MineProfile()
# Cleanup also the api key in the extensions repository setting and clean the cache
from . import override_extension_draw
override_extension_draw.ensure_repository(api_key="")
override_extension_draw.clear_repo_cache()
def logout() -> None:
@@ -151,11 +156,12 @@ def write_tokens(auth_token, refresh_token, oauth_response):
preferences.login_attempt = False
preferences.api_key_refresh = refresh_token
preferences.api_key = auth_token # triggers api_key update function
# write token also to extensions repository setting
# write token also to extensions repository setting and clear the cache
if bpy.app.version >= (4, 2, 0):
from . import override_extension_draw
override_extension_draw.ensure_repository(api_key=auth_token)
override_extension_draw.clear_repo_cache()
#
@@ -1,17 +1,17 @@
{
"last_check": "2025-06-23 09:38:03.256719",
"backup_date": "",
"last_check": "2025-12-01 11:02:25.858363",
"backup_date": "October-27-2025",
"update_ready": true,
"ignore": false,
"just_restored": false,
"just_updated": false,
"version_text": {
"link": "https://github.com/BlenderKit/BlenderKit/releases/download/v3.16.1.250612/blenderkit-v3.16.1.250612.zip",
"link": "https://github.com/BlenderKit/BlenderKit/releases/download/v3.18.0.251121/blenderkit-v3.18.0.251121.zip",
"version": [
3,
16,
1,
250612
18,
0,
251121
]
}
}
@@ -159,8 +159,11 @@ class BL_UI_Button(BL_UI_Widget):
def draw_text(self, area_height):
font_id = 1
if bpy.app.version < (4, 0, 0):
if bpy.app.version < (3, 1, 0):
# Blender 3.0 requires size:int https://docs.blender.org/api/3.0/blf.html#blf.size
# but assetBar's search tab text is float - needs conversion in here
blf.size(font_id, int(self._text_size), 72)
elif bpy.app.version < (4, 0, 0):
blf.size(font_id, self._text_size, 72)
else:
blf.size(font_id, self._text_size)
@@ -204,7 +207,9 @@ class BL_UI_Button(BL_UI_Widget):
try:
self.mouse_down_func(self)
except Exception as e:
print(e)
import traceback
traceback.print_exc()
return True
@@ -1,3 +1,4 @@
import traceback
import bpy
from bpy.types import Operator
@@ -113,4 +114,4 @@ def draw_callback_px_separated(self, op, context):
for widget in self.widgets:
widget.draw()
except Exception as e:
print(e)
traceback.print_exc()
@@ -35,9 +35,9 @@ class BL_UI_Widget:
@bg_color.setter
def bg_color(self, value):
if value != self._bg_color:
bpy.context.region.tag_redraw()
self._bg_color = value
if bpy.context.region is not None:
bpy.context.region.tag_redraw()
@property
def visible(self):
@@ -2,7 +2,7 @@ schema_version = "1.0.0"
id = "blenderkit"
type = "add-on"
version = "3.15.1-250403" # X.Y.Z-YYYYMMDD, must have a dash instead of a dot
version = "3.17.0-251008" # X.Y.Z-YYYYMMDD, must have a dash instead of a dot
name = "BlenderKit Online Asset Library"
tagline = "Drag & drop of assets from the community driven library"
@@ -134,7 +134,7 @@ def handle_categories_task(task: client_tasks.Task):
) # TODO: do this in Client, just saving the file so next time it is updated even without internet
return
bk_logger.warning(task.message)
bk_logger.warning(f"Could not load categories: {task.message}")
if not os.path.exists(categories_filepath):
source_path = paths.get_addon_file(subpath="data" + os.sep + "categories.json")
try:

Some files were not shown because too many files have changed in this diff Show More