1493 lines
60 KiB
Python
1493 lines
60 KiB
Python
# ##### BEGIN GPL LICENSE BLOCK #####
|
|
#
|
|
# This program is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU General Public License
|
|
# as published by the Free Software Foundation; either version 3
|
|
# of the License, or (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software Foundation,
|
|
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
#
|
|
# ##### END GPL LICENSE BLOCK #####
|
|
|
|
# <pep8 compliant>
|
|
|
|
import bpy
|
|
import math
|
|
import bpy_extras
|
|
import mathutils
|
|
import re
|
|
from math import inf
|
|
from rna_prop_ui import rna_idprop_ui_create
|
|
|
|
DEFAULT_VISIBLE_LAYER = 'CarRig_Default_Ctrls' # New Setting for Blender4.0
|
|
CUSTOM_SHAPE_LAYER = 'CarRig_Custom_Ctrls' # 13
|
|
MCH_BONE_EXTENSION_LAYER = 'CarRig_MCH_Bone_Ext' # 14
|
|
DEF_BONE_LAYER = 'CarRig_Def_Bone' # 15
|
|
MCH_BONE_LAYER = 'CarRig_MCH_Bone' # 31
|
|
|
|
|
|
def deselect_edit_bones(ob):
|
|
for b in ob.data.edit_bones:
|
|
b.select = False
|
|
b.select_head = False
|
|
b.select_tail = False
|
|
|
|
|
|
def create_constraint_influence_driver(ob, cns, driver_data_path, base_influence=1.0):
|
|
fcurve = cns.driver_add('influence')
|
|
drv = fcurve.driver
|
|
drv.type = 'AVERAGE'
|
|
var = drv.variables.new()
|
|
var.name = 'influence'
|
|
var.type = 'SINGLE_PROP'
|
|
|
|
targ = var.targets[0]
|
|
targ.id_type = 'OBJECT'
|
|
targ.id = ob
|
|
targ.data_path = driver_data_path
|
|
|
|
if base_influence != 1.0:
|
|
fmod = fcurve.modifiers[0]
|
|
fmod.mode = 'POLYNOMIAL'
|
|
fmod.poly_order = 1
|
|
fmod.coefficients = (0, base_influence)
|
|
|
|
|
|
def create_rotation_euler_x_driver(ob, bone, driver_data_path):
|
|
fcurve = bone.driver_add('rotation_euler', 0)
|
|
drv = fcurve.driver
|
|
drv.type = 'AVERAGE'
|
|
var = drv.variables.new()
|
|
var.name = 'rotationAngle'
|
|
var.type = 'SINGLE_PROP'
|
|
|
|
targ = var.targets[0]
|
|
targ.id_type = 'OBJECT'
|
|
targ.id = ob
|
|
targ.data_path = driver_data_path
|
|
|
|
|
|
def create_translation_x_driver(ob, bone, driver_data_path):
|
|
fcurve = bone.driver_add('location', 0)
|
|
drv = fcurve.driver
|
|
drv.type = 'AVERAGE'
|
|
var = drv.variables.new()
|
|
var.name = 'rotationAngle'
|
|
var.type = 'SINGLE_PROP'
|
|
|
|
targ = var.targets[0]
|
|
targ.id_type = 'OBJECT'
|
|
targ.id = ob
|
|
targ.data_path = driver_data_path
|
|
|
|
|
|
def create_bone_group(pose, group_name, color_set, bone_names):
|
|
# Here Only set color
|
|
for bone_name in bone_names:
|
|
bone = pose.bones.get(bone_name)
|
|
if bone is not None:
|
|
bone.color.palette = color_set
|
|
|
|
|
|
def name_range(prefix, nb=1000):
|
|
if nb > 0:
|
|
yield prefix
|
|
for i in range(1, nb):
|
|
yield '%s.%03d' % (prefix, i)
|
|
|
|
|
|
def get_widget(name):
|
|
widget = bpy.data.objects.get(name)
|
|
if widget is None:
|
|
from . import widgets
|
|
widgets.create()
|
|
widget = bpy.data.objects.get(name)
|
|
return widget
|
|
|
|
|
|
def define_custom_property(target, name, value, description=None, overridable=True):
|
|
rna_idprop_ui_create(target, name, default=value, description=description, overridable=overridable, min=-inf,
|
|
max=inf)
|
|
|
|
|
|
def dispatch_bones_to_armature_layers(ob):
|
|
'''Bone Collections were introduced in Blender 4.0 as replacement of Armature Layers and Bone Groups.'''
|
|
amt = bpy.context.object.data
|
|
default_visible_layer = amt.collections.new(name=DEFAULT_VISIBLE_LAYER)
|
|
custom_shape_layer = amt.collections.new(name=CUSTOM_SHAPE_LAYER) # 13
|
|
def_bone_layer = amt.collections.new(name=DEF_BONE_LAYER) # 15
|
|
mch_bone_layer = amt.collections.new(name=MCH_BONE_LAYER) # 31
|
|
mch_bone_extension_layer = amt.collections.new(name=MCH_BONE_EXTENSION_LAYER) # 14
|
|
|
|
# set visibility
|
|
custom_shape_layer.is_visible = False
|
|
def_bone_layer.is_visible = False
|
|
mch_bone_extension_layer.is_visible = False
|
|
mch_bone_layer.is_visible = False
|
|
|
|
# get MCH bone
|
|
re_mch_bone = re.compile(r'^MCH-Wheel(Brake)?\.(Ft|Bk)\.[LR](\.\d+)?$')
|
|
|
|
for b in ob.data.bones:
|
|
if b.name.startswith('DEF-'):
|
|
def_bone_layer.assign(b)
|
|
elif b.name.startswith('MCH-'):
|
|
mch_bone_layer.assign(b)
|
|
if b.name in ('MCH-Body', 'MCH-Steering') or re_mch_bone.match(b.name):
|
|
mch_bone_extension_layer.assign(b)
|
|
else:
|
|
default_visible_layer.assign(b)
|
|
pass
|
|
|
|
for b in ob.pose.bones:
|
|
if b.custom_shape:
|
|
if b.custom_shape_transform:
|
|
ob.pose.bones[b.custom_shape_transform.name].custom_shape = b.custom_shape
|
|
custom_shape_layer.assign(ob.pose.bones[b.custom_shape_transform.name])
|
|
# ob.data.bones[b.custom_shape_transform.name].layers = shape_bone_layers
|
|
else:
|
|
default_visible_layer.assign(ob.data.bones[b.name])
|
|
# ob.data.bones[b.name].layers[CUSTOM_SHAPE_LAYER] = True
|
|
|
|
# Remove custom shape from default layer
|
|
for bone in custom_shape_layer.bones:
|
|
default_visible_layer.unassign(bone)
|
|
|
|
|
|
class NameSuffix(object):
|
|
|
|
def __init__(self, position, side, index=0):
|
|
self.position = position
|
|
self.side = side
|
|
self.index = index
|
|
if index == 0:
|
|
self.value = '%s.%s' % (position, side)
|
|
else:
|
|
self.value = '%s.%s.%03d' % (position, side, index)
|
|
|
|
def name(self, base_name=None):
|
|
return '%s.%s' % (base_name, self.value) if base_name else self.value
|
|
|
|
@property
|
|
def is_front(self):
|
|
return self.position == 'Ft'
|
|
|
|
@property
|
|
def is_left(self):
|
|
return self.side == 'L'
|
|
|
|
@property
|
|
def is_first(self):
|
|
return self.index == 0
|
|
|
|
def __str__(self):
|
|
return self.value
|
|
|
|
|
|
class BoundingBox(object):
|
|
|
|
def __init__(self, armature, bone_name):
|
|
objs = [o for o in armature.children if o.parent_bone == bone_name]
|
|
bone = armature.data.bones[bone_name]
|
|
self.__center = bone.head.copy()
|
|
if not objs:
|
|
self.__xyz = [bone.head.x - bone.length / 2, bone.head.x + bone.length / 2, bone.head.y - bone.length,
|
|
bone.head.y + bone.length, .0, bone.head.z * 2]
|
|
else:
|
|
self.__xyz = [inf, -inf, inf, -inf, inf, -inf]
|
|
self.__compute(mathutils.Matrix(), *objs)
|
|
|
|
def __compute(self, pmatrix, *objs):
|
|
for obj in objs:
|
|
omatrix = pmatrix @ obj.matrix_world
|
|
if obj.instance_type == 'COLLECTION':
|
|
self.__compute(omatrix, *obj.instance_collection.all_objects)
|
|
elif obj.bound_box:
|
|
for p in obj.bound_box:
|
|
world_p = omatrix @ mathutils.Vector(p)
|
|
self.__xyz[0] = min(world_p.x, self.__xyz[0])
|
|
self.__xyz[1] = max(world_p.x, self.__xyz[1])
|
|
self.__xyz[2] = min(world_p.y, self.__xyz[2])
|
|
self.__xyz[3] = max(world_p.y, self.__xyz[3])
|
|
self.__xyz[4] = min(world_p.z, self.__xyz[4])
|
|
self.__xyz[5] = max(world_p.z, self.__xyz[5])
|
|
self.__compute(pmatrix, *obj.children)
|
|
|
|
@property
|
|
def center(self):
|
|
return self.__center
|
|
|
|
@property
|
|
def box_center(self):
|
|
return mathutils.Vector((self.max_x + self.min_x, self.max_y + self.min_y, self.max_z + self.min_z)) / 2
|
|
|
|
@property
|
|
def min_x(self):
|
|
return self.__xyz[0]
|
|
|
|
@property
|
|
def max_x(self):
|
|
return self.__xyz[1]
|
|
|
|
@property
|
|
def min_y(self):
|
|
return self.__xyz[2]
|
|
|
|
@property
|
|
def max_y(self):
|
|
return self.__xyz[3]
|
|
|
|
@property
|
|
def min_z(self):
|
|
return self.__xyz[4]
|
|
|
|
@property
|
|
def max_z(self):
|
|
return self.__xyz[5]
|
|
|
|
@property
|
|
def width(self):
|
|
return abs(self.__xyz[0] - self.__xyz[1])
|
|
|
|
@property
|
|
def length(self):
|
|
return abs(self.__xyz[2] - self.__xyz[3])
|
|
|
|
@property
|
|
def height(self):
|
|
return abs(self.__xyz[4] - self.__xyz[5])
|
|
|
|
|
|
class WheelBoundingBox(BoundingBox):
|
|
|
|
def __init__(self, armature, bone_name, side):
|
|
super().__init__(armature, bone_name)
|
|
self.side = side
|
|
|
|
def compute_outer_x(self, delta=0):
|
|
if self.side == 'L':
|
|
return self.max_x + delta
|
|
else:
|
|
return self.min_x - delta
|
|
|
|
|
|
class WheelsDimension(object):
|
|
|
|
def __init__(self, armature, position, side_position, default):
|
|
self.default = default
|
|
self.position = position
|
|
self.side_position = side_position
|
|
self.wheels = []
|
|
wheel_bones = (armature.data.edit_bones.get(name) for name in
|
|
name_range('DEF-Wheel.%s.%s' % (self.position, self.side_position)))
|
|
for wheel_bone in wheel_bones:
|
|
if wheel_bone is None:
|
|
break
|
|
self.wheels.append(WheelBoundingBox(armature, wheel_bone.name, side_position))
|
|
|
|
def name_suffixes(self):
|
|
for i in range(len(self.wheels)):
|
|
yield NameSuffix(self.position, self.side_position, i)
|
|
|
|
def names(self, base_name=None):
|
|
for name_suffix in name_range('%s.%s' % (self.position, self.side_position), self.nb):
|
|
yield '%s.%s' % (base_name, name_suffix) if base_name else name_suffix
|
|
|
|
def name(self, base_name=None):
|
|
suffix = '%s.%s' % (self.position, self.side_position)
|
|
return '%s.%s' % (base_name, suffix) if base_name else suffix
|
|
|
|
@property
|
|
def nb(self):
|
|
return len(self.wheels)
|
|
|
|
@property
|
|
def min_position(self):
|
|
if self.nb == 0:
|
|
return self.default
|
|
return min(self.wheels, key=lambda w: w.center.y).center
|
|
|
|
@property
|
|
def max_position(self):
|
|
if self.nb == 0:
|
|
return self.default
|
|
return max(self.wheels, key=lambda w: w.center.y).center
|
|
|
|
@property
|
|
def medium_position(self):
|
|
if self.nb == 0:
|
|
return self.min_position
|
|
return (self.min_position + self.max_position) / 2.0
|
|
|
|
def compute_outer_x(self, delta=0):
|
|
if self.side_position == 'L':
|
|
x = max(map(lambda w: w.max_x, self.wheels))
|
|
x += delta
|
|
else:
|
|
x = min(map(lambda w: w.min_x, self.wheels))
|
|
x -= delta
|
|
return x
|
|
|
|
@property
|
|
def outer_z(self):
|
|
return max(map(lambda w: w.max_z, self.wheels))
|
|
|
|
@property
|
|
def outer_front(self):
|
|
return min(map(lambda w: w.min_y, self.wheels))
|
|
|
|
@property
|
|
def outer_back(self):
|
|
return max(map(lambda w: w.max_y, self.wheels))
|
|
|
|
|
|
class CarDimension(object):
|
|
|
|
def __init__(self, armature):
|
|
body = armature.data.edit_bones['DEF-Body']
|
|
self.bb_body = BoundingBox(armature, 'DEF-Body')
|
|
self.wheels_front_left = WheelsDimension(armature, 'Ft', 'L', default=body.head)
|
|
self.wheels_front_right = WheelsDimension(armature, 'Ft', 'R', default=body.head)
|
|
self.wheels_back_left = WheelsDimension(armature, 'Bk', 'L', default=body.tail)
|
|
self.wheels_back_right = WheelsDimension(armature, 'Bk', 'R', default=body.tail)
|
|
|
|
@property
|
|
def body_center(self):
|
|
return self.bb_body.center
|
|
|
|
@property
|
|
def car_center(self):
|
|
center = self.bb_body.box_center.copy()
|
|
center.y = (self.max_y + self.min_y) / 2
|
|
return center
|
|
|
|
@property
|
|
def width(self):
|
|
return max([self.bb_body.width] + [abs(w.compute_outer_x() - self.bb_body.center.x) * 2 for w in
|
|
self.wheels_dimensions])
|
|
|
|
@property
|
|
def height(self):
|
|
return max([self.bb_body.max_z] + [w.outer_z for w in self.wheels_dimensions])
|
|
|
|
@property
|
|
def length(self):
|
|
return abs(self.max_y - self.min_y)
|
|
|
|
@property
|
|
def min_y(self):
|
|
return min([self.bb_body.min_y] + [w.outer_front for w in self.wheels_dimensions])
|
|
|
|
@property
|
|
def max_y(self):
|
|
return max([self.bb_body.max_y] + [w.outer_back for w in self.wheels_dimensions])
|
|
|
|
@property
|
|
def wheels_front_position(self):
|
|
position = (self.wheels_front_left.min_position + self.wheels_front_right.min_position) / 2
|
|
position.x = self.bb_body.center.x
|
|
return position
|
|
|
|
@property
|
|
def wheels_back_position(self):
|
|
position = (self.wheels_back_left.max_position + self.wheels_back_right.max_position) / 2
|
|
position.x = self.bb_body.center.x
|
|
return position
|
|
|
|
@property
|
|
def suspension_front_position(self):
|
|
position = (self.wheels_front_left.medium_position + self.wheels_front_right.medium_position) / 2
|
|
position.x = self.bb_body.center.x
|
|
return position
|
|
|
|
@property
|
|
def suspension_back_position(self):
|
|
position = (self.wheels_back_left.medium_position + self.wheels_back_right.medium_position) / 2
|
|
position.x = self.bb_body.center.x
|
|
return position
|
|
|
|
@property
|
|
def has_wheels(self):
|
|
return self.has_front_wheels or self.has_back_wheels
|
|
|
|
@property
|
|
def has_front_wheels(self):
|
|
return self.nb_front_wheels > 0
|
|
|
|
@property
|
|
def has_back_wheels(self):
|
|
return self.nb_back_wheels > 0
|
|
|
|
@property
|
|
def nb_front_wheels(self):
|
|
return max(self.wheels_front_left.nb, self.wheels_front_right.nb)
|
|
|
|
@property
|
|
def nb_back_wheels(self):
|
|
return max(self.wheels_back_left.nb, self.wheels_back_right.nb)
|
|
|
|
@property
|
|
def wheels_dimensions(self):
|
|
return filter(lambda w: w.nb,
|
|
(self.wheels_front_left, self.wheels_front_right, self.wheels_back_left, self.wheels_back_right))
|
|
|
|
|
|
def create_wheel_brake_bone(wheel_brake, parent_bone, wheel_bone):
|
|
wheel_brake.use_deform = False
|
|
wheel_brake.parent = parent_bone
|
|
wheel_brake.head = wheel_bone.head
|
|
wheel_brake.tail = wheel_bone.tail
|
|
|
|
|
|
def generate_constraint_on_wheel_brake_bone(wheel_brake_pose_bone, wheel_pose_bone):
|
|
wheel_brake_pose_bone.lock_location = (True, True, True)
|
|
wheel_brake_pose_bone.lock_rotation = (True, True, True)
|
|
wheel_brake_pose_bone.lock_rotation_w = True
|
|
wheel_brake_pose_bone.lock_scale = (True, False, False)
|
|
wheel_brake_pose_bone.custom_shape = get_widget('WGT-CarRig.WheelBrake')
|
|
wheel_brake_pose_bone.bone.show_wire = True
|
|
amt = bpy.context.object.data
|
|
groups = amt.collections
|
|
for group in groups:
|
|
for bone in group.bones:
|
|
if bone.name == wheel_pose_bone.anme:
|
|
group.assign(wheel_brake_pose_bone)
|
|
|
|
cns = wheel_brake_pose_bone.constraints.new('LIMIT_SCALE')
|
|
cns.name = 'Brakes'
|
|
cns.use_transform_limit = True
|
|
cns.owner_space = 'LOCAL'
|
|
cns.use_max_x = True
|
|
cns.use_min_x = True
|
|
cns.min_x = 1.0
|
|
cns.max_x = 1.0
|
|
cns.use_max_y = True
|
|
cns.use_min_y = True
|
|
cns.min_y = .5
|
|
cns.max_y = 1.0
|
|
cns.use_max_z = True
|
|
cns.use_min_z = True
|
|
cns.min_z = .5
|
|
cns.max_z = 1.0
|
|
|
|
|
|
class ArmatureGenerator(object):
|
|
|
|
def __init__(self, ob):
|
|
self.ob = ob
|
|
|
|
def generate(self, scene, adjust_origin):
|
|
define_custom_property(self.ob,
|
|
name='wheels_on_y_axis',
|
|
value=False,
|
|
description="Activate wheels rotation when moving the root bone along the Y axis")
|
|
define_custom_property(self.ob,
|
|
name='suspension_factor',
|
|
value=.5,
|
|
description="Influence of the dampers over the pitch of the body")
|
|
define_custom_property(self.ob,
|
|
name='suspension_rolling_factor',
|
|
value=.5,
|
|
description="Influence of the dampers over the roll of the body")
|
|
|
|
location = self.ob.location.copy()
|
|
self.ob.location = (0, 0, 0)
|
|
try:
|
|
bpy.ops.object.mode_set(mode='EDIT')
|
|
self.dimension = CarDimension(self.ob)
|
|
self.generate_animation_rig()
|
|
self.ob.data['Car Rig'] = True
|
|
deselect_edit_bones(self.ob)
|
|
|
|
if adjust_origin:
|
|
bpy.ops.object.mode_set(mode='OBJECT')
|
|
self.set_origin(scene)
|
|
|
|
bpy.ops.object.mode_set(mode='POSE')
|
|
self.generate_constraints_on_rig()
|
|
self.ob.display_type = 'WIRE'
|
|
|
|
self.generate_bone_groups()
|
|
dispatch_bones_to_armature_layers(self.ob)
|
|
finally:
|
|
self.ob.location += location
|
|
|
|
def generate_animation_rig(self):
|
|
amt = self.ob.data
|
|
|
|
body = amt.edit_bones['DEF-Body']
|
|
root = amt.edit_bones.new('Root')
|
|
if self.dimension.has_back_wheels:
|
|
root.head = self.dimension.wheels_back_position
|
|
elif self.dimension.has_front_wheels:
|
|
root.head = self.dimension.wheels_front_position
|
|
else:
|
|
root.head = self.dimension.body_center
|
|
root.head.z = 0
|
|
root.tail = root.head
|
|
root.tail.y += max(self.dimension.length / 1.95, self.dimension.width * 1.1)
|
|
root.use_deform = False
|
|
|
|
shape_root = amt.edit_bones.new('SHP-Root')
|
|
shape_root.head = self.dimension.car_center
|
|
shape_root.head.z = 0.01
|
|
shape_root.tail = shape_root.head
|
|
shape_root.tail.y += root.length
|
|
shape_root.use_deform = False
|
|
shape_root.parent = root
|
|
|
|
drift = amt.edit_bones.new('Drift')
|
|
drift.head = self.dimension.wheels_front_position
|
|
drift.head.z = self.dimension.wheels_back_position.z
|
|
drift.tail = drift.head
|
|
drift.tail.y -= self.dimension.width * .95
|
|
drift.roll = math.pi
|
|
drift.use_deform = False
|
|
drift.parent = root
|
|
base_bone_parent = drift
|
|
|
|
if self.dimension.has_front_wheels:
|
|
groundsensor_axle_front = amt.edit_bones.new('GroundSensor.Axle.Ft')
|
|
groundsensor_axle_front.head = self.dimension.wheels_front_position
|
|
groundsensor_axle_front.tail = groundsensor_axle_front.head
|
|
groundsensor_axle_front.tail.y += self.dimension.length / 16
|
|
groundsensor_axle_front.parent = root
|
|
|
|
shp_groundsensor_axle_front = amt.edit_bones.new('SHP-GroundSensor.Axle.Ft')
|
|
shp_groundsensor_axle_front.head = groundsensor_axle_front.head
|
|
shp_groundsensor_axle_front.tail = groundsensor_axle_front.tail
|
|
shp_groundsensor_axle_front.head.z = shp_groundsensor_axle_front.tail.z = 0.001
|
|
shp_groundsensor_axle_front.parent = groundsensor_axle_front
|
|
|
|
mch_root_axle_front = amt.edit_bones.new('MCH-Root.Axle.Ft')
|
|
mch_root_axle_front.head = self.dimension.wheels_front_position
|
|
mch_root_axle_front.head.z = 0.001
|
|
mch_root_axle_front.tail = mch_root_axle_front.head
|
|
mch_root_axle_front.tail.y += self.dimension.length / 6
|
|
mch_root_axle_front.parent = groundsensor_axle_front
|
|
if not self.dimension.has_back_wheels:
|
|
drift.parent = mch_root_axle_front
|
|
|
|
if self.dimension.has_back_wheels:
|
|
groundsensor_axle_back = amt.edit_bones.new('GroundSensor.Axle.Bk')
|
|
groundsensor_axle_back.head = self.dimension.wheels_back_position
|
|
groundsensor_axle_back.tail = groundsensor_axle_back.head
|
|
groundsensor_axle_back.tail.y += self.dimension.length / 16
|
|
groundsensor_axle_back.parent = drift
|
|
|
|
shp_groundsensor_axle_back = amt.edit_bones.new('SHP-GroundSensor.Axle.Bk')
|
|
shp_groundsensor_axle_back.head = groundsensor_axle_back.head
|
|
shp_groundsensor_axle_back.tail = groundsensor_axle_back.tail
|
|
shp_groundsensor_axle_back.head.z = shp_groundsensor_axle_back.tail.z = 0.001
|
|
shp_groundsensor_axle_back.parent = groundsensor_axle_back
|
|
|
|
mch_root_axle_back = amt.edit_bones.new('MCH-Root.Axle.Bk')
|
|
mch_root_axle_back.head = self.dimension.wheels_back_position
|
|
mch_root_axle_back.head.z = 0
|
|
mch_root_axle_back.tail = mch_root_axle_back.head
|
|
mch_root_axle_back.tail.y += self.dimension.length / 6
|
|
mch_root_axle_back.parent = groundsensor_axle_back
|
|
base_bone_parent = mch_root_axle_back
|
|
|
|
shape_drift = amt.edit_bones.new('SHP-Drift')
|
|
shape_drift.head = self.dimension.body_center
|
|
shape_drift.head.y = self.dimension.max_y + drift.length * .2
|
|
shape_drift.head.z = self.dimension.wheels_back_position.z
|
|
shape_drift.tail = shape_drift.head
|
|
shape_drift.tail.y += drift.length
|
|
shape_drift.use_deform = False
|
|
shape_drift.parent = base_bone_parent
|
|
|
|
for wheel_dimension in self.dimension.wheels_dimensions:
|
|
for name_suffix, wheel_bounding_box in zip(wheel_dimension.name_suffixes(), wheel_dimension.wheels):
|
|
self.generate_animation_wheel_bones(name_suffix, wheel_bounding_box, base_bone_parent)
|
|
self.generate_wheel_damper(wheel_dimension, base_bone_parent)
|
|
|
|
if self.dimension.has_front_wheels:
|
|
wheel_ft_r = amt.edit_bones.get('DEF-Wheel.Ft.R')
|
|
wheelFtL = amt.edit_bones.get('DEF-Wheel.Ft.L')
|
|
|
|
axis_ft = amt.edit_bones.new('MCH-Axis.Ft')
|
|
axis_ft.head = wheel_ft_r.head
|
|
axis_ft.tail = wheelFtL.head
|
|
axis_ft.use_deform = False
|
|
axis_ft.parent = base_bone_parent
|
|
|
|
mch_steering = amt.edit_bones.new('MCH-Steering')
|
|
mch_steering.head = self.dimension.wheels_front_position
|
|
mch_steering.tail = self.dimension.wheels_front_position
|
|
mch_steering.tail.y += self.dimension.width / 2
|
|
mch_steering.use_deform = False
|
|
mch_steering.parent = groundsensor_axle_front if groundsensor_axle_front else root
|
|
|
|
steering_rotation = amt.edit_bones.new('MCH-Steering.rotation')
|
|
steering_rotation.head = mch_steering.head
|
|
steering_rotation.tail = mch_steering.tail
|
|
steering_rotation.tail.y += 1
|
|
steering_rotation.use_deform = False
|
|
|
|
steering = amt.edit_bones.new('Steering')
|
|
steering.head = steering_rotation.head
|
|
steering.head.y = self.dimension.min_y - 4 * wheelFtL.length
|
|
steering.tail = steering.head
|
|
steering.tail.y -= self.dimension.width / 2
|
|
steering.use_deform = False
|
|
steering.parent = steering_rotation
|
|
|
|
if self.dimension.has_back_wheels:
|
|
wheel_bk_r = amt.edit_bones.get('DEF-Wheel.Bk.R')
|
|
wheel_bk_l = amt.edit_bones.get('DEF-Wheel.Bk.L')
|
|
|
|
axisBk = amt.edit_bones.new('MCH-Axis.Bk')
|
|
axisBk.head = wheel_bk_r.head
|
|
axisBk.tail = wheel_bk_l.head
|
|
axisBk.use_deform = False
|
|
axisBk.parent = base_bone_parent
|
|
|
|
suspension_bk = amt.edit_bones.new('MCH-Suspension.Bk')
|
|
suspension_bk.head = self.dimension.suspension_back_position
|
|
suspension_bk.tail = self.dimension.suspension_back_position
|
|
suspension_bk.tail.y += 2
|
|
suspension_bk.use_deform = False
|
|
suspension_bk.parent = base_bone_parent
|
|
|
|
suspension_ft = amt.edit_bones.new('MCH-Suspension.Ft')
|
|
suspension_ft.head = self.dimension.suspension_front_position
|
|
align_vector = suspension_bk.head - suspension_ft.head
|
|
align_vector.magnitude = 2
|
|
suspension_ft.tail = self.dimension.suspension_front_position + align_vector
|
|
suspension_ft.use_deform = False
|
|
suspension_ft.parent = base_bone_parent
|
|
|
|
axis = amt.edit_bones.new('MCH-Axis')
|
|
axis.head = suspension_ft.head
|
|
axis.tail = suspension_bk.head
|
|
axis.use_deform = False
|
|
axis.parent = suspension_ft
|
|
|
|
mch_body = amt.edit_bones.new('MCH-Body')
|
|
mch_body.head = body.head
|
|
mch_body.tail = body.tail
|
|
mch_body.tail.y += 1
|
|
mch_body.use_deform = False
|
|
mch_body.parent = axis
|
|
|
|
suspension = amt.edit_bones.new('Suspension')
|
|
suspension.head = self.dimension.body_center
|
|
suspension.head.z = self.dimension.height + self.dimension.width * .25
|
|
suspension.tail = suspension.head
|
|
suspension.tail.y += root.length * .5
|
|
suspension.use_deform = False
|
|
suspension.parent = axis
|
|
|
|
def generate_animation_wheel_bones(self, name_suffix, wheel_bounding_box, parent_bone):
|
|
amt = self.ob.data
|
|
|
|
def_wheel_bone = amt.edit_bones.get(name_suffix.name('DEF-Wheel'))
|
|
|
|
if def_wheel_bone is None:
|
|
return
|
|
|
|
ground_sensor = amt.edit_bones.new(name_suffix.name('GroundSensor'))
|
|
ground_sensor.head = wheel_bounding_box.box_center
|
|
ground_sensor.head.z = def_wheel_bone.head.z
|
|
ground_sensor.tail = ground_sensor.head
|
|
ground_sensor.tail.y += max(max(wheel_bounding_box.height, ground_sensor.head.z) / 2.5,
|
|
wheel_bounding_box.width * 1.02)
|
|
ground_sensor.use_deform = False
|
|
ground_sensor.parent = parent_bone
|
|
|
|
shp_ground_sensor = amt.edit_bones.new(name_suffix.name('SHP-GroundSensor'))
|
|
shp_ground_sensor.head = ground_sensor.head
|
|
shp_ground_sensor.tail = ground_sensor.tail
|
|
shp_ground_sensor.head.z = shp_ground_sensor.tail.z = .001
|
|
shp_ground_sensor.use_deform = False
|
|
shp_ground_sensor.parent = ground_sensor
|
|
|
|
mch_wheel = amt.edit_bones.new(name_suffix.name('MCH-Wheel'))
|
|
mch_wheel.head = def_wheel_bone.head
|
|
mch_wheel.tail = def_wheel_bone.tail
|
|
mch_wheel.tail.y += .5
|
|
mch_wheel.use_deform = False
|
|
mch_wheel.parent = ground_sensor
|
|
|
|
define_custom_property(self.ob,
|
|
name=name_suffix.name('Wheel.rotation'),
|
|
value=.0,
|
|
description="Animation property for wheel spinning")
|
|
mch_wheel_rotation = amt.edit_bones.new(name_suffix.name('MCH-Wheel.rotation'))
|
|
mch_wheel_rotation.head = def_wheel_bone.head
|
|
mch_wheel_rotation.tail = def_wheel_bone.head
|
|
mch_wheel_rotation.tail.y += mch_wheel_rotation.tail.z
|
|
mch_wheel_rotation.use_deform = False
|
|
|
|
def_wheel_brake_bone = amt.edit_bones.get(name_suffix.name('DEF-WheelBrake'))
|
|
if def_wheel_brake_bone is not None:
|
|
mch_wheel = amt.edit_bones.new(name_suffix.name('MCH-WheelBrake'))
|
|
mch_wheel.head = def_wheel_brake_bone.head
|
|
mch_wheel.tail = def_wheel_brake_bone.tail
|
|
mch_wheel.tail.y += .5
|
|
mch_wheel.use_deform = False
|
|
mch_wheel.parent = ground_sensor
|
|
|
|
wheel = amt.edit_bones.new(name_suffix.name('Wheel'))
|
|
wheel.use_deform = False
|
|
wheel.parent = ground_sensor
|
|
wheel.head = def_wheel_bone.head
|
|
wheel.head.x = wheel_bounding_box.compute_outer_x(wheel_bounding_box.length * .05)
|
|
wheel.tail = wheel.head
|
|
wheel.tail.y += wheel.tail.z * .9
|
|
|
|
if name_suffix.is_left and name_suffix.is_first:
|
|
wheel_brake = amt.edit_bones.new(name_suffix.name('WheelBrake'))
|
|
create_wheel_brake_bone(wheel_brake, mch_wheel, wheel)
|
|
|
|
def generate_wheel_damper(self, wheel_dimension, parent_bone):
|
|
amt = self.ob.data
|
|
|
|
if wheel_dimension.nb == 1:
|
|
wheel_damper_parent = amt.edit_bones[wheel_dimension.name('GroundSensor')]
|
|
else:
|
|
wheel_damper_parent = amt.edit_bones.new(wheel_dimension.name('MCH-GroundSensor'))
|
|
wheel_damper_parent.head = wheel_dimension.medium_position
|
|
wheel_damper_parent.tail = wheel_dimension.medium_position
|
|
wheel_damper_parent.tail.y += 1.0
|
|
wheel_damper_parent.head.z = 0
|
|
wheel_damper_parent.tail.z = 0
|
|
wheel_damper_parent.use_deform = False
|
|
wheel_damper_parent.parent = parent_bone
|
|
|
|
wheel_damper = amt.edit_bones.new(wheel_dimension.name('WheelDamper'))
|
|
wheel_damper.head = wheel_dimension.medium_position
|
|
wheel_damper_scale_ratio = abs(wheel_damper.head.z)
|
|
wheel_damper.head.x = wheel_dimension.compute_outer_x(wheel_damper_scale_ratio * .25)
|
|
wheel_damper.head.z *= 1.5
|
|
wheel_damper.tail = wheel_damper.head
|
|
wheel_damper.tail.y += wheel_damper_scale_ratio
|
|
wheel_damper.use_deform = False
|
|
wheel_damper.parent = wheel_damper_parent
|
|
|
|
mch_wheel_damper = amt.edit_bones.new(wheel_dimension.name('MCH-WheelDamper'))
|
|
mch_wheel_damper.head = wheel_dimension.medium_position
|
|
mch_wheel_damper.tail = wheel_dimension.medium_position
|
|
mch_wheel_damper.tail.y += 2
|
|
mch_wheel_damper.use_deform = False
|
|
mch_wheel_damper.parent = wheel_damper
|
|
|
|
def generate_constraints_on_rig(self):
|
|
pose = self.ob.pose
|
|
amt = self.ob.data
|
|
|
|
for b in pose.bones:
|
|
if b.name.startswith('DEF-') or b.name.startswith('MCH-') or b.name.startswith('SHP-'):
|
|
b.lock_location = (True, True, True)
|
|
b.lock_rotation = (True, True, True)
|
|
b.lock_scale = (True, True, True)
|
|
b.lock_rotation_w = True
|
|
|
|
for wheel_dimension in self.dimension.wheels_dimensions:
|
|
for name_suffix in wheel_dimension.name_suffixes():
|
|
self.generate_constraints_on_wheel_bones(name_suffix)
|
|
self.generate_constraints_on_wheel_damper(wheel_dimension)
|
|
|
|
self.generate_constraints_on_axle_bones('Ft')
|
|
self.generate_constraints_on_axle_bones('Bk')
|
|
|
|
mch_axis = pose.bones.get('MCH-Axis')
|
|
if mch_axis is not None:
|
|
for axis_pos, influence in (('Ft', 1), ('Bk', .5)):
|
|
subtarget = 'MCH-Axis.%s' % axis_pos
|
|
if subtarget in pose.bones:
|
|
cns = mch_axis.constraints.new('TRANSFORM')
|
|
cns.name = 'Rotation from %s' % subtarget
|
|
cns.target = self.ob
|
|
cns.subtarget = subtarget
|
|
cns.map_from = 'ROTATION'
|
|
cns.from_min_x_rot = math.radians(-180)
|
|
cns.from_max_x_rot = math.radians(180)
|
|
cns.map_to_y_from = 'X'
|
|
cns.map_to = 'ROTATION'
|
|
cns.to_min_y_rot = math.radians(180)
|
|
cns.to_max_y_rot = math.radians(-180)
|
|
cns.owner_space = 'LOCAL'
|
|
cns.target_space = 'LOCAL'
|
|
create_constraint_influence_driver(self.ob, cns, '["suspension_rolling_factor"]',
|
|
base_influence=influence)
|
|
|
|
root = pose.bones['Root']
|
|
root.lock_scale = (True, True, True)
|
|
root.custom_shape = get_widget('WGT-CarRig.Root')
|
|
root.custom_shape_transform = pose.bones['SHP-Root']
|
|
root.bone.show_wire = True
|
|
|
|
for ground_sensor_axle_name in ('GroundSensor.Axle.Ft', 'GroundSensor.Axle.Bk'):
|
|
ground_sensor_axle = pose.bones.get(ground_sensor_axle_name)
|
|
if ground_sensor_axle:
|
|
ground_sensor_axle.lock_location = (True, True, False)
|
|
ground_sensor_axle.lock_rotation = (True, True, True)
|
|
ground_sensor_axle.lock_scale = (True, True, True)
|
|
ground_sensor_axle.custom_shape = get_widget('WGT-CarRig.GroundSensor.Axle')
|
|
ground_sensor_axle.lock_rotation_w = True
|
|
ground_sensor_axle.custom_shape_transform = pose.bones['SHP-%s' % ground_sensor_axle.name]
|
|
ground_sensor_axle.bone.show_wire = True
|
|
self.generate_ground_projection_constraint(ground_sensor_axle)
|
|
|
|
if ground_sensor_axle.name == 'GroundSensor.Axle.Ft' and 'GroundSensor.Axle.Bk' in pose.bones:
|
|
cns = ground_sensor_axle.constraints.new('LIMIT_DISTANCE')
|
|
cns.name = 'Limit distance from Root'
|
|
cns.limit_mode = 'LIMITDIST_ONSURFACE'
|
|
cns.target = self.ob
|
|
cns.subtarget = 'GroundSensor.Axle.Bk'
|
|
cns.use_transform_limit = True
|
|
cns.owner_space = 'POSE'
|
|
cns.target_space = 'POSE'
|
|
|
|
mch_root_axle_front = pose.bones.get('MCH-Root.Axle.Ft')
|
|
mch_root_axle_back = pose.bones.get('MCH-Root.Axle.Bk')
|
|
if mch_root_axle_front and mch_root_axle_back:
|
|
cns = mch_root_axle_back.constraints.new('DAMPED_TRACK')
|
|
cns.name = 'Track front axle'
|
|
cns.target = self.ob
|
|
cns.subtarget = mch_root_axle_front.name
|
|
cns.track_axis = 'TRACK_NEGATIVE_Y'
|
|
|
|
drift = pose.bones['Drift']
|
|
drift.lock_location = (True, True, True)
|
|
drift.lock_rotation = (True, True, False)
|
|
drift.lock_scale = (True, True, True)
|
|
drift.rotation_mode = 'ZYX'
|
|
drift.custom_shape = get_widget('WGT-CarRig.DriftHandle')
|
|
drift.custom_shape_transform = pose.bones['SHP-Drift']
|
|
drift.bone.show_wire = True
|
|
|
|
suspension = pose.bones['Suspension']
|
|
suspension.lock_rotation = (True, True, True)
|
|
suspension.lock_scale = (True, True, True)
|
|
suspension.lock_rotation_w = True
|
|
suspension.custom_shape = get_widget('WGT-CarRig.Suspension')
|
|
suspension.bone.show_wire = True
|
|
|
|
steering = pose.bones.get('Steering')
|
|
if steering is not None:
|
|
steering.lock_location = (False, True, True)
|
|
steering.lock_rotation = (True, True, True)
|
|
steering.lock_scale = (True, True, True)
|
|
steering.lock_rotation_w = True
|
|
steering.custom_shape = get_widget('WGT-CarRig.Steering')
|
|
steering.bone.show_wire = True
|
|
|
|
mch_steering_rotation = pose.bones['MCH-Steering.rotation']
|
|
mch_steering_rotation.rotation_mode = 'QUATERNION'
|
|
define_custom_property(self.ob,
|
|
name='Steering.rotation',
|
|
value=.0,
|
|
description="Animation property for steering")
|
|
create_translation_x_driver(self.ob, mch_steering_rotation, '["Steering.rotation"]')
|
|
|
|
if mch_root_axle_back:
|
|
cns = mch_steering_rotation.constraints.new('COPY_ROTATION')
|
|
cns.name = 'Copy back axle rotation'
|
|
cns.target = self.ob
|
|
cns.subtarget = mch_root_axle_back.name
|
|
cns.use_x = True
|
|
cns.use_y = False
|
|
cns.use_z = False
|
|
cns.owner_space = 'LOCAL'
|
|
cns.target_space = 'LOCAL'
|
|
|
|
self.generate_childof_constraint(mch_steering_rotation,
|
|
mch_root_axle_front if mch_root_axle_front else root)
|
|
|
|
mch_steering = pose.bones['MCH-Steering']
|
|
cns = mch_steering.constraints.new('DAMPED_TRACK')
|
|
cns.name = 'Track steering bone'
|
|
cns.target = self.ob
|
|
cns.subtarget = 'Steering'
|
|
cns.track_axis = 'TRACK_NEGATIVE_Y'
|
|
|
|
cns = mch_steering.constraints.new('COPY_ROTATION')
|
|
cns.name = 'Drift counter animation'
|
|
cns.target = self.ob
|
|
cns.subtarget = 'Drift'
|
|
cns.use_x = False
|
|
cns.use_y = False
|
|
cns.use_z = True
|
|
cns.use_offset = True
|
|
cns.owner_space = 'LOCAL'
|
|
cns.target_space = 'LOCAL'
|
|
|
|
mch_body = self.ob.pose.bones['MCH-Body']
|
|
cns = mch_body.constraints.new('TRANSFORM')
|
|
cns.name = 'Suspension on rollover'
|
|
cns.target = self.ob
|
|
cns.subtarget = 'Suspension'
|
|
cns.map_from = 'LOCATION'
|
|
cns.from_min_x = -2
|
|
cns.from_max_x = 2
|
|
cns.from_min_y = -2
|
|
cns.from_max_y = 2
|
|
cns.map_to_x_from = 'Y'
|
|
cns.map_to_y_from = 'X'
|
|
cns.map_to = 'ROTATION'
|
|
cns.to_min_x_rot = math.radians(6)
|
|
cns.to_max_x_rot = math.radians(-6)
|
|
cns.to_min_y_rot = math.radians(-7)
|
|
cns.to_max_y_rot = math.radians(7)
|
|
cns.owner_space = 'LOCAL'
|
|
cns.target_space = 'LOCAL'
|
|
|
|
cns = mch_body.constraints.new('TRANSFORM')
|
|
cns.name = 'Suspension on vertical'
|
|
cns.target = self.ob
|
|
cns.subtarget = 'Suspension'
|
|
cns.map_from = 'LOCATION'
|
|
cns.from_min_z = -0.5
|
|
cns.from_max_z = 0.5
|
|
cns.map_to_z_from = 'Z'
|
|
cns.map_to = 'LOCATION'
|
|
cns.to_min_z = -0.1
|
|
cns.to_max_z = 0.1
|
|
cns.owner_space = 'LOCAL'
|
|
cns.target_space = 'LOCAL'
|
|
|
|
body = self.ob.pose.bones['DEF-Body']
|
|
cns = body.constraints.new('COPY_TRANSFORMS')
|
|
cns.target = self.ob
|
|
cns.subtarget = 'MCH-Body'
|
|
|
|
def generate_ground_projection_constraint(self, bone):
|
|
cns = bone.constraints.new('SHRINKWRAP')
|
|
cns.name = 'Ground projection'
|
|
cns.shrinkwrap_type = 'NEAREST_SURFACE'
|
|
cns.project_axis_space = 'LOCAL'
|
|
cns.project_axis = 'NEG_Z'
|
|
cns.distance = abs(bone.head.z)
|
|
|
|
def generate_childof_constraint(self, child, parent):
|
|
cns = child.constraints.new('CHILD_OF')
|
|
cns.target = self.ob
|
|
cns.subtarget = parent.name
|
|
cns.inverse_matrix = self.ob.data.bones[parent.name].matrix_local.inverted()
|
|
cns.use_location_x = True
|
|
cns.use_location_y = True
|
|
cns.use_location_z = True
|
|
cns.use_rotation_x = True
|
|
cns.use_rotation_y = True
|
|
cns.use_rotation_z = True
|
|
return cns
|
|
|
|
def generate_constraints_on_axle_bones(self, position):
|
|
pose = self.ob.pose
|
|
|
|
subtarget = 'MCH-Axis.%s' % position
|
|
if subtarget in pose.bones:
|
|
mch_suspension = pose.bones['MCH-Suspension.%s' % position]
|
|
cns = mch_suspension.constraints.new('COPY_LOCATION')
|
|
cns.name = 'Location from %s' % subtarget
|
|
cns.target = self.ob
|
|
cns.subtarget = subtarget
|
|
cns.head_tail = .5
|
|
cns.use_x = False
|
|
cns.use_y = False
|
|
cns.use_z = True
|
|
cns.owner_space = 'WORLD'
|
|
cns.target_space = 'WORLD'
|
|
create_constraint_influence_driver(self.ob, cns, '["suspension_factor"]')
|
|
|
|
if position == 'Ft':
|
|
cns = mch_suspension.constraints.new('DAMPED_TRACK')
|
|
cns.name = 'Track suspension back'
|
|
cns.target = self.ob
|
|
cns.subtarget = 'MCH-Suspension.Bk'
|
|
cns.track_axis = 'TRACK_Y'
|
|
|
|
mch_axis = pose.bones.get('MCH-Axis.%s' % position)
|
|
if mch_axis is not None:
|
|
cns = mch_axis.constraints.new('COPY_LOCATION')
|
|
cns.name = 'Copy location from right wheel'
|
|
cns.target = self.ob
|
|
cns.subtarget = 'MCH-WheelDamper.%s.R' % position
|
|
cns.use_x = True
|
|
cns.use_y = True
|
|
cns.use_z = True
|
|
cns.owner_space = 'WORLD'
|
|
cns.target_space = 'WORLD'
|
|
|
|
mch_axis = pose.bones['MCH-Axis.%s' % position]
|
|
cns = mch_axis.constraints.new('DAMPED_TRACK')
|
|
cns.name = 'Track Left Wheel'
|
|
cns.target = self.ob
|
|
cns.subtarget = 'MCH-WheelDamper.%s.L' % position
|
|
cns.track_axis = 'TRACK_Y'
|
|
|
|
def generate_constraints_on_wheel_bones(self, name_suffix):
|
|
pose = self.ob.pose
|
|
|
|
def_wheel = pose.bones.get(name_suffix.name('DEF-Wheel'))
|
|
if def_wheel is None:
|
|
return
|
|
|
|
cns = def_wheel.constraints.new('COPY_TRANSFORMS')
|
|
cns.target = self.ob
|
|
cns.subtarget = name_suffix.name('MCH-Wheel')
|
|
|
|
def_wheel_brake = pose.bones.get(name_suffix.name('DEF-WheelBrake'))
|
|
if def_wheel_brake is not None:
|
|
cns = def_wheel_brake.constraints.new('COPY_TRANSFORMS')
|
|
cns.target = self.ob
|
|
cns.subtarget = name_suffix.name('MCH-WheelBrake')
|
|
|
|
ground_sensor = pose.bones[name_suffix.name('GroundSensor')]
|
|
ground_sensor.lock_location = (True, True, False)
|
|
ground_sensor.lock_rotation = (True, True, True)
|
|
ground_sensor.lock_rotation_w = True
|
|
ground_sensor.lock_scale = (True, True, True)
|
|
ground_sensor.custom_shape = get_widget('WGT-CarRig.GroundSensor')
|
|
ground_sensor.custom_shape_transform = pose.bones['SHP-%s' % ground_sensor.name]
|
|
ground_sensor.bone.show_wire = True
|
|
|
|
if name_suffix.is_front:
|
|
cns = ground_sensor.constraints.new('COPY_ROTATION')
|
|
cns.name = 'Steering rotation'
|
|
cns.target = self.ob
|
|
cns.subtarget = 'MCH-Steering'
|
|
cns.use_x = False
|
|
cns.use_y = False
|
|
cns.use_z = True
|
|
cns.owner_space = 'LOCAL'
|
|
cns.target_space = 'LOCAL'
|
|
|
|
self.generate_ground_projection_constraint(ground_sensor)
|
|
|
|
cns = ground_sensor.constraints.new('LIMIT_LOCATION')
|
|
cns.name = 'Ground projection limitation'
|
|
cns.use_transform_limit = True
|
|
cns.owner_space = 'LOCAL'
|
|
cns.use_max_x = True
|
|
cns.use_min_x = True
|
|
cns.min_x = 0
|
|
cns.max_x = 0
|
|
cns.use_max_y = True
|
|
cns.use_min_y = True
|
|
cns.min_y = 0
|
|
cns.max_y = 0
|
|
cns.use_max_z = True
|
|
cns.use_min_z = True
|
|
cns.min_z = -.2
|
|
cns.max_z = .2
|
|
|
|
wheel = pose.bones.get(name_suffix.name('Wheel'))
|
|
wheel.rotation_mode = "XYZ"
|
|
wheel.lock_location = (True, True, True)
|
|
wheel.lock_rotation = (False, True, True)
|
|
wheel.lock_scale = (True, True, True)
|
|
wheel.custom_shape = get_widget('WGT-CarRig.Wheel')
|
|
wheel.bone.show_wire = True
|
|
|
|
wheel_brake = pose.bones.get(name_suffix.name('WheelBrake'))
|
|
if wheel_brake:
|
|
generate_constraint_on_wheel_brake_bone(wheel_brake, wheel)
|
|
|
|
mch_wheel = pose.bones[name_suffix.name('MCH-Wheel')]
|
|
mch_wheel.rotation_mode = "XYZ"
|
|
|
|
cns = mch_wheel.constraints.new('COPY_ROTATION')
|
|
cns.name = 'Bake animation wheels'
|
|
cns.target = self.ob
|
|
cns.subtarget = name_suffix.name('MCH-Wheel.rotation')
|
|
cns.use_x = True
|
|
cns.use_y = False
|
|
cns.use_z = False
|
|
cns.use_offset = False
|
|
cns.owner_space = 'POSE'
|
|
cns.target_space = 'POSE'
|
|
|
|
cns = mch_wheel.constraints.new('TRANSFORM')
|
|
cns.name = 'Wheel rotation along Y axis'
|
|
cns.target = self.ob
|
|
cns.subtarget = 'Root'
|
|
cns.use_motion_extrapolate = True
|
|
cns.map_from = 'LOCATION'
|
|
cns.from_min_y = - math.pi * abs(mch_wheel.head.z if mch_wheel.head.z != 0 else 1)
|
|
cns.from_max_y = - cns.from_min_y
|
|
cns.map_to_x_from = 'Y'
|
|
cns.map_to = 'ROTATION'
|
|
cns.to_min_x_rot = math.pi
|
|
cns.to_max_x_rot = -math.pi
|
|
cns.owner_space = 'LOCAL'
|
|
cns.target_space = 'LOCAL'
|
|
|
|
create_constraint_influence_driver(self.ob, cns, '["wheels_on_y_axis"]')
|
|
|
|
cns = mch_wheel.constraints.new('COPY_ROTATION')
|
|
cns.name = 'Animation wheels'
|
|
cns.target = self.ob
|
|
cns.subtarget = wheel.name
|
|
cns.use_x = True
|
|
cns.use_y = False
|
|
cns.use_z = False
|
|
cns.use_offset = True
|
|
cns.owner_space = 'LOCAL'
|
|
cns.target_space = 'LOCAL'
|
|
|
|
mch_wheel_rotation = pose.bones[name_suffix.name('MCH-Wheel.rotation')]
|
|
mch_wheel_rotation.rotation_mode = "XYZ"
|
|
self.generate_childof_constraint(mch_wheel_rotation, ground_sensor)
|
|
create_rotation_euler_x_driver(self.ob, mch_wheel_rotation, '["%s"]' % name_suffix.name('Wheel.rotation'))
|
|
|
|
def generate_constraints_on_wheel_damper(self, wheel_dimension):
|
|
pose = self.ob.pose
|
|
|
|
wheel_damper = pose.bones.get(wheel_dimension.name('WheelDamper'))
|
|
if wheel_damper is not None:
|
|
wheel_damper.lock_location = (True, True, False)
|
|
wheel_damper.lock_rotation = (True, True, True)
|
|
wheel_damper.lock_rotation_w = True
|
|
wheel_damper.lock_scale = (True, True, True)
|
|
wheel_damper.custom_shape = get_widget('WGT-CarRig.WheelDamper')
|
|
wheel_damper.bone.show_wire = True
|
|
|
|
mch_ground_sensor = pose.bones.get(wheel_dimension.name('MCH-GroundSensor'))
|
|
if mch_ground_sensor is not None:
|
|
fcurve = mch_ground_sensor.driver_add('location', 2)
|
|
drv = fcurve.driver
|
|
drv.type = 'MAX'
|
|
|
|
for i, ground_sensor_name in enumerate(wheel_dimension.names('GroundSensor')):
|
|
if ground_sensor_name in pose.bones:
|
|
var = drv.variables.new()
|
|
var.name = 'groundSensor%03d' % i
|
|
var.type = 'TRANSFORMS'
|
|
|
|
targ = var.targets[0]
|
|
targ.id = self.ob
|
|
targ.bone_target = ground_sensor_name
|
|
targ.transform_space = 'LOCAL_SPACE'
|
|
targ.transform_type = 'LOC_Z'
|
|
|
|
def generate_bone_groups(self):
|
|
pose = self.ob.pose
|
|
create_bone_group(pose, 'Direction', color_set='THEME04', bone_names=('Root', 'Drift', 'SHP-Root', 'SHP-Drift'))
|
|
create_bone_group(pose, 'Suspension', color_set='THEME09', bone_names=(
|
|
'Suspension', 'WheelDamper.Ft.L', 'WheelDamper.Ft.R', 'WheelDamper.Bk.L', 'WheelDamper.Bk.R'))
|
|
|
|
wheel_widgets = ('Steering',)
|
|
for wheel_dimension in self.dimension.wheels_dimensions:
|
|
wheel_widgets += tuple(wheel_dimension.names('Wheel'))
|
|
wheel_widgets += tuple(wheel_dimension.names('WheelBrake'))
|
|
create_bone_group(pose, 'Wheel', color_set='THEME03', bone_names=wheel_widgets)
|
|
|
|
ground_sensor_names = (
|
|
'GroundSensor.Axle.Ft', 'GroundSensor.Axle.Bk', 'SHP-GroundSensor.Axle.Ft', 'SHP-GroundSensor.Axle.Bk')
|
|
for wheel_dimension in self.dimension.wheels_dimensions:
|
|
ground_sensor_names += tuple(wheel_dimension.names('GroundSensor'))
|
|
ground_sensor_names += tuple("SHP-%s" % i for i in ground_sensor_names)
|
|
create_bone_group(pose, 'GroundSensor', color_set='THEME02', bone_names=ground_sensor_names)
|
|
|
|
def set_origin(self, scene):
|
|
object_location = self.ob.location[:]
|
|
root = self.ob.data.bones.get('Root')
|
|
if root:
|
|
cursor_location = scene.cursor.location[:]
|
|
scene.cursor.location = root.head
|
|
try:
|
|
bpy.ops.object.origin_set(type='ORIGIN_CURSOR')
|
|
finally:
|
|
scene.cursor.location = cursor_location
|
|
self.ob.location = object_location
|
|
|
|
|
|
class OBJECT_OT_armatureCarDeformationRig(bpy.types.Operator):
|
|
bl_idname = "object.armature_car_deformation_rig"
|
|
bl_label = "Add car deformation rig"
|
|
bl_description = "Creates the base rig for a car."
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
body_pos_delta: bpy.props.FloatVectorProperty(name='Delta Location',
|
|
description='Extra translation added to location of the car body',
|
|
size=3,
|
|
default=(0, 0, 0),
|
|
subtype='TRANSLATION')
|
|
|
|
nb_front_wheels_pairs: bpy.props.IntProperty(name='Pairs',
|
|
description='Number of front wheels pairs',
|
|
default=1,
|
|
min=0)
|
|
|
|
front_wheel_pos_delta: bpy.props.FloatVectorProperty(name='Delta Location',
|
|
description='Extra translation added to location of the front wheels',
|
|
size=3,
|
|
default=(0, 0, 0),
|
|
subtype='TRANSLATION')
|
|
|
|
nb_back_wheels_pairs: bpy.props.IntProperty(name='Pairs',
|
|
description='Number of back wheels pairs',
|
|
default=1,
|
|
min=0)
|
|
|
|
back_wheel_pos_delta: bpy.props.FloatVectorProperty(name='Delta Location',
|
|
description='Extra translation added to location of the back wheels',
|
|
size=3,
|
|
default=(0, 0, 0),
|
|
subtype='TRANSLATION')
|
|
|
|
nb_front_wheel_brakes_pairs: bpy.props.IntProperty(name='Front Pairs',
|
|
description='Number of front wheel brakes pairs',
|
|
default=0,
|
|
min=0)
|
|
|
|
front_wheel_brakes_pos_delta: bpy.props.FloatProperty(name='Front Delta Location',
|
|
description='Extra translation added to location of the front brakes',
|
|
default=0)
|
|
|
|
nb_back_wheel_brakes_pairs: bpy.props.IntProperty(name='Back Pairs',
|
|
description='Number of back wheel brakes pairs',
|
|
default=0,
|
|
min=0)
|
|
|
|
back_wheel_brakes_pos_delta: bpy.props.FloatProperty(name='Back Delta Location',
|
|
description='Extra translation added to location of the back brakes',
|
|
default=0)
|
|
|
|
def draw(self, context):
|
|
self.layout.use_property_split = True
|
|
self.layout.use_property_decorate = False
|
|
self.layout.label(text='Body')
|
|
layout = self.layout.box()
|
|
layout.prop(self, 'body_pos_delta')
|
|
self.layout.label(text='Front wheels')
|
|
layout = self.layout.box()
|
|
layout.prop(self, 'nb_front_wheels_pairs')
|
|
layout.prop(self, 'front_wheel_pos_delta')
|
|
self.layout.label(text='Back wheels')
|
|
layout = self.layout.box()
|
|
layout.prop(self, 'nb_back_wheels_pairs')
|
|
layout.prop(self, 'back_wheel_pos_delta')
|
|
self.layout.label(text='Brakes')
|
|
layout = self.layout.box()
|
|
layout.prop(self, 'nb_front_wheel_brakes_pairs')
|
|
layout.prop(self, 'front_wheel_brakes_pos_delta')
|
|
layout.prop(self, 'nb_back_wheel_brakes_pairs')
|
|
layout.prop(self, 'back_wheel_brakes_pos_delta')
|
|
|
|
def invoke(self, context, event):
|
|
self.bones_position = {
|
|
'Body': mathutils.Vector((0.0, 0, .8)),
|
|
'Wheel.Ft.L': mathutils.Vector((0.9, -2, .5)),
|
|
'Wheel.Ft.R': mathutils.Vector((-.9, -2, .5)),
|
|
'Wheel.Bk.L': mathutils.Vector((0.9, 2, .5)),
|
|
'Wheel.Bk.R': mathutils.Vector((-.9, 2, .5)),
|
|
'WheelBrake.Ft.L': mathutils.Vector((0.8, -2, .5)),
|
|
'WheelBrake.Ft.R': mathutils.Vector((-.8, -2, .5)),
|
|
'WheelBrake.Bk.L': mathutils.Vector((0.8, 2, .5)),
|
|
'WheelBrake.Bk.R': mathutils.Vector((-.8, 2, .5))
|
|
}
|
|
self.target_objects_name = {}
|
|
|
|
has_body_target = self._find_target_object(context, 'Body')
|
|
|
|
nb_wheels_ft_l = self._find_target_object_for_wheels(context, 'Wheel.Ft.L')
|
|
nb_wheels_ft_r = self._find_target_object_for_wheels(context, 'Wheel.Ft.R')
|
|
nb_wheels_bk_l = self._find_target_object_for_wheels(context, 'Wheel.Bk.L')
|
|
nb_wheels_bk_r = self._find_target_object_for_wheels(context, 'Wheel.Bk.R')
|
|
|
|
nb_wheel_brakes_ft_l = self._find_target_object_for_wheels(context, 'WheelBrake.Ft.L')
|
|
nb_wheel_brakes_ft_r = self._find_target_object_for_wheels(context, 'WheelBrake.Ft.R')
|
|
nb_wheel_brakes_bk_l = self._find_target_object_for_wheels(context, 'WheelBrake.Bk.L')
|
|
nb_wheel_brakes_bk_r = self._find_target_object_for_wheels(context, 'WheelBrake.Bk.R')
|
|
|
|
self.nb_front_wheels_pairs = max(nb_wheels_ft_l, nb_wheels_ft_r)
|
|
self.nb_back_wheels_pairs = max(nb_wheels_bk_l, nb_wheels_bk_r)
|
|
self.nb_front_wheel_brakes_pairs = max(nb_wheel_brakes_ft_l, nb_wheel_brakes_ft_r)
|
|
self.nb_back_wheel_brakes_pairs = max(nb_wheel_brakes_bk_l, nb_wheel_brakes_bk_r)
|
|
|
|
# if no target object has been found for body, we assume it may have no
|
|
# target object for front and back wheels either.
|
|
if not has_body_target:
|
|
self.nb_front_wheels_pairs = max(1, self.nb_front_wheels_pairs)
|
|
self.nb_back_wheels_pairs = max(1, self.nb_back_wheels_pairs)
|
|
|
|
return self.execute(context)
|
|
|
|
def _find_target_object_for_wheels(self, context, suffix_name):
|
|
for count, name in enumerate(name_range(suffix_name)):
|
|
if not self._find_target_object(context, name):
|
|
return count
|
|
|
|
def _find_target_object(self, context, name):
|
|
escaped_name = re.escape(name).replace(r'\.', r'[\.-_ ]')
|
|
pattern = re.compile(f"^.*{escaped_name}$", re.IGNORECASE)
|
|
for obj in context.selected_objects:
|
|
if pattern.match(obj.name):
|
|
self.target_objects_name[name] = obj.name
|
|
self.bones_position[name] = obj.location.copy()
|
|
return True
|
|
return False
|
|
|
|
def execute(self, context):
|
|
"""Creates the meta rig with basic bones"""
|
|
amt = bpy.data.armatures.new('Car Rig Data')
|
|
amt['Car Rig'] = False
|
|
|
|
rig = bpy_extras.object_utils.object_data_add(context, amt, name='Car Rig')
|
|
|
|
# TODO: cannot edit new object added to a hidden collection
|
|
# Could be a better fix (steal code from other addons).
|
|
try:
|
|
bpy.ops.object.mode_set(mode='EDIT')
|
|
except TypeError:
|
|
self.report({'ERROR'},
|
|
"Cannot edit the new armature! Please make sure the active collection is visible and editable")
|
|
return {'CANCELLED'}
|
|
|
|
self._create_bone(rig, 'Body', delta_pos=self.body_pos_delta)
|
|
|
|
self._create_wheel_bones(rig, 'Wheel.Ft.L', self.nb_front_wheels_pairs, self.front_wheel_pos_delta)
|
|
self._create_wheel_bones(rig, 'Wheel.Ft.R', self.nb_front_wheels_pairs,
|
|
self.front_wheel_pos_delta.reflect(mathutils.Vector((1, 0, 0))))
|
|
self._create_wheel_bones(rig, 'Wheel.Bk.L', self.nb_back_wheels_pairs, self.back_wheel_pos_delta)
|
|
self._create_wheel_bones(rig, 'Wheel.Bk.R', self.nb_back_wheels_pairs,
|
|
self.back_wheel_pos_delta.reflect(mathutils.Vector((1, 0, 0))))
|
|
|
|
front_wheel_brakes_delta_pos = self.front_wheel_pos_delta.copy()
|
|
front_wheel_brakes_delta_pos.x = self.front_wheel_brakes_pos_delta
|
|
self._create_wheel_bones(rig, 'WheelBrake.Ft.L', self.nb_front_wheel_brakes_pairs, front_wheel_brakes_delta_pos)
|
|
self._create_wheel_bones(rig, 'WheelBrake.Ft.R', self.nb_front_wheel_brakes_pairs,
|
|
front_wheel_brakes_delta_pos.reflect(mathutils.Vector((1, 0, 0))))
|
|
back_wheel_brakes_delta_pos = self.back_wheel_pos_delta.copy()
|
|
back_wheel_brakes_delta_pos.x = self.back_wheel_brakes_pos_delta
|
|
self._create_wheel_bones(rig, 'WheelBrake.Bk.L', self.nb_back_wheel_brakes_pairs, back_wheel_brakes_delta_pos)
|
|
self._create_wheel_bones(rig, 'WheelBrake.Bk.R', self.nb_back_wheel_brakes_pairs,
|
|
back_wheel_brakes_delta_pos.reflect(mathutils.Vector((1, 0, 0))))
|
|
|
|
deselect_edit_bones(rig)
|
|
|
|
bpy.ops.object.mode_set(mode='OBJECT')
|
|
|
|
return {'FINISHED'}
|
|
|
|
def _create_bone(self, rig, name, delta_pos):
|
|
b = rig.data.edit_bones.new('DEF-' + name)
|
|
|
|
b.head = self.bones_position[name] + delta_pos
|
|
b.tail = b.head
|
|
if name == 'Body':
|
|
b.tail.y += b.tail.z * 4
|
|
else:
|
|
b.tail.y += b.tail.z
|
|
|
|
target_obj_name = self.target_objects_name.get(name)
|
|
if target_obj_name is not None and target_obj_name in bpy.context.scene.objects:
|
|
target_obj = bpy.context.scene.objects[target_obj_name]
|
|
if name == 'Body':
|
|
b.tail = b.head
|
|
b.tail.y += target_obj.dimensions[1] / 2 if target_obj.dimensions and target_obj.dimensions[
|
|
0] != 0 else 1
|
|
target_obj.parent = rig
|
|
target_obj.parent_bone = b.name
|
|
target_obj.parent_type = 'BONE'
|
|
target_obj.location += rig.matrix_world.to_translation()
|
|
target_obj.matrix_parent_inverse = (rig.matrix_world @ mathutils.Matrix.Translation(b.tail)).inverted()
|
|
|
|
return b
|
|
|
|
def _create_wheel_bones(self, rig, base_wheel_name, nb_wheels, delta_pos):
|
|
for wheel_name in name_range(base_wheel_name, nb_wheels):
|
|
if wheel_name not in self.bones_position:
|
|
wheel_position = previous_wheel_default_pos.copy()
|
|
wheel_position.y += abs(previous_wheel.head.z * 2.2)
|
|
self.bones_position[wheel_name] = wheel_position
|
|
previous_wheel = self._create_bone(rig, wheel_name, delta_pos)
|
|
previous_wheel_default_pos = self.bones_position[wheel_name]
|
|
|
|
|
|
class POSE_OT_carAnimationRigGenerate(bpy.types.Operator):
|
|
bl_idname = "pose.car_animation_rig_generate"
|
|
bl_label = "Generate car animation rig"
|
|
bl_description = "Creates the complete armature for animating the car."
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
adjust_origin: bpy.props.BoolProperty(name='Move origin',
|
|
description='Set origin of the armature at the same location as the root bone',
|
|
default=True)
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return context.object is not None and context.object.data is not None and 'Car Rig' in context.object.data
|
|
|
|
def draw(self, context):
|
|
self.layout.use_property_split = True
|
|
self.layout.use_property_decorate = False
|
|
self.layout.prop(self, 'adjust_origin')
|
|
|
|
def execute(self, context):
|
|
if context.object.data['Car Rig']:
|
|
self.report({'INFO'}, 'Rig already generated')
|
|
return {"CANCELLED"}
|
|
|
|
if 'DEF-Body' not in context.object.data.bones:
|
|
self.report({'ERROR'}, 'No bone named DEF-Body. This is not a valid armature!')
|
|
return {"CANCELLED"}
|
|
|
|
armature_generator = ArmatureGenerator(context.object)
|
|
armature_generator.generate(context.scene, self.adjust_origin)
|
|
return {"FINISHED"}
|
|
|
|
|
|
class POSE_OT_carAnimationAddBrakeWheelBones(bpy.types.Operator):
|
|
bl_idname = "pose.car_animation_add_brake_wheel_bones"
|
|
bl_label = "Add missing brake wheel bones"
|
|
bl_description = "Generates missing brake wheel bones for each selected wheel widget."
|
|
bl_options = {'UNDO'}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return context.object.mode == 'POSE' and \
|
|
context.object is not None and context.object.data is not None and \
|
|
context.object.data.get('Car Rig')
|
|
|
|
def execute(self, context):
|
|
mode = context.object.mode
|
|
re_wheel_bone_name = re.compile(r'^Wheel\.(Ft|Bk)\.([LR])(\.\d+)?$')
|
|
for pose_bone in context.selected_pose_bones:
|
|
matcher = re_wheel_bone_name.match(pose_bone.name)
|
|
if matcher:
|
|
wheelbrake_name = 'WheelBrake.%s.%s%s' % matcher.groups(default='')
|
|
parent_name = 'MCH-Wheel.%s.%s%s' % matcher.groups(default='')
|
|
self.create_wheelbrake_bone(context, pose_bone, wheelbrake_name, parent_name)
|
|
bpy.ops.object.mode_set(mode=mode)
|
|
return {"FINISHED"}
|
|
|
|
def create_wheelbrake_bone(self, context, wheel_pose_bone, name, parent_name):
|
|
obj = context.object
|
|
amt = context.object.data
|
|
if name not in amt.bones and parent_name in amt.bones:
|
|
bpy.ops.object.mode_set(mode='EDIT')
|
|
create_wheel_brake_bone(amt.edit_bones.new(name), amt.edit_bones[parent_name],
|
|
amt.edit_bones[wheel_pose_bone.name])
|
|
bpy.ops.object.mode_set(mode='POSE')
|
|
generate_constraint_on_wheel_brake_bone(obj.pose.bones[name], wheel_pose_bone)
|
|
|
|
|
|
def register():
|
|
bpy.utils.register_class(POSE_OT_carAnimationRigGenerate)
|
|
bpy.utils.register_class(OBJECT_OT_armatureCarDeformationRig)
|
|
bpy.utils.register_class(POSE_OT_carAnimationAddBrakeWheelBones)
|
|
|
|
|
|
def unregister():
|
|
bpy.utils.unregister_class(POSE_OT_carAnimationAddBrakeWheelBones)
|
|
bpy.utils.unregister_class(OBJECT_OT_armatureCarDeformationRig)
|
|
bpy.utils.unregister_class(POSE_OT_carAnimationRigGenerate)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
register()
|