前言 最近接实习公司需求整Dynamic Bone,虽然因Dynamic Bone中节点距离恒定,布料模拟方面Magica Cloth效果远甚Dynamic Bone(然而性能消耗上也是),但Dynamic Bone的算法也算得上经典,也值得一写。
正文 DataStruct 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 public Transform m_Root = null ; public List<Transform> m_Roots = null ; public float m_UpdateRate = 60.0f ; public enum UpdateMode { Normal, AnimatePhysics, UnscaledTime, Default } public UpdateMode m_UpdateMode = UpdateMode.Default; [Range(0, 1) ] public float m_Damping = 0.1f ; public AnimationCurve m_DampingDistrib = null ; [Range(0, 1) ] public float m_Elasticity = 0.1f ; public AnimationCurve m_ElasticityDistrib = null ; [Range(0, 1) ] public float m_Stiffness = 0.1f ; public AnimationCurve m_StiffnessDistrib = null ; [Range(0, 1) ] public float m_Inert = 0 ; public AnimationCurve m_InertDistrib = null ; public float m_Friction = 0 ; public AnimationCurve m_FrictionDistrib = null ; public float m_Radius = 0 ; public AnimationCurve m_RadiusDistrib = null ; public float m_EndLength = 0 ; public Vector3 m_EndOffset = Vector3.zero; public Vector3 m_Gravity = Vector3.zero; public Vector3 m_Force = Vector3.zero; [Range(0, 1) ] public float m_BlendWeight = 1.0f ; public List<DynamicBoneColliderBase> m_Colliders = null ; public List<Transform> m_Exclusions = null ; public enum FreezeAxis { None, X, Y, Z } public FreezeAxis m_FreezeAxis = FreezeAxis.None; public bool m_DistantDisable = false ; public Transform m_ReferenceObject = null ; public float m_DistanceToObject = 20 ; [HideInInspector ] public bool m_Multithread = true ; Vector3 m_ObjectMove; Vector3 m_ObjectPrevPosition; float m_ObjectScale; float m_Time = 0 ; float m_Weight = 1.0f ; bool m_DistantDisabled = false ; int m_PreUpdateCount = 0 ; class Particle { public Transform m_Transform; public int m_ParentIndex; public int m_ChildCount; public float m_Damping; public float m_Elasticity; public float m_Stiffness; public float m_Inert; public float m_Friction; public float m_Radius; public float m_BoneLength; public bool m_isCollide; public bool m_TransformNotNull; public Vector3 m_Position; public Vector3 m_PrevPosition; public Vector3 m_EndOffset; public Vector3 m_InitLocalPosition; public Quaternion m_InitLocalRotation; public Vector3 m_TransformPosition; public Vector3 m_TransformLocalPosition; public Matrix4x4 m_TransformLocalToWorldMatrix; } class ParticleTree { public Transform m_Root; public Vector3 m_LocalGravity; public Matrix4x4 m_RootWorldToLocalMatrix; public float m_BoneTotalLength; public List<Particle> m_Particles = new List<Particle>(); public Vector3 m_RestGravity; } List<ParticleTree> m_ParticleTrees = new List<ParticleTree>(); float m_DeltaTime; List<DynamicBoneColliderBase> m_EffectiveColliders; #if ENABLE_MULTITHREAD bool m_WorkAdded = false ; static List<DynamicBone> s_PendingWorks = new List<DynamicBone>(); static List<DynamicBone> s_EffectiveWorks = new List<DynamicBone>(); static AutoResetEvent s_AllWorksDoneEvent; static int s_RemainWorkCount; static Semaphore s_WorkQueueSemaphore; static int s_WorkQueueIndex; #endif static int s_UpdateCount; static int s_PrepareFrame;
Start() 1 2 3 4 void Start (){ SetupParticles(); }
SetupParticles() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public void SetupParticles (){ m_ParticleTrees.Clear(); if (m_Root != null ) { AppendParticleTree(m_Root); } if (m_Roots != null ) { for (int i = 0 ; i < m_Roots.Count; ++i) { Transform root = m_Roots[i]; if (root == null ) continue ; if (m_ParticleTrees.Exists(x => x.m_Root == root)) continue ; AppendParticleTree(root); } } m_ObjectScale = Mathf.Abs(transform.lossyScale.x); m_ObjectPrevPosition = transform.position; m_ObjectMove = Vector3.zero; for (int i = 0 ; i < m_ParticleTrees.Count; ++i) { ParticleTree pt = m_ParticleTrees[i]; AppendParticles(pt, pt.m_Root, -1 , 0 ); } UpdateParameters(); }
AppendParticleTree() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 void AppendParticleTree (Transform root ){ if (root == null ) return ; var pt = new ParticleTree(); pt.m_Root = root; pt.m_RootWorldToLocalMatrix = root.worldToLocalMatrix; m_ParticleTrees.Add(pt); } void AppendParticles (ParticleTree pt, Transform b, int parentIndex, float boneLength ){ var p = new Particle(); p.m_Transform = b; p.m_TransformNotNull = b != null ; p.m_ParentIndex = parentIndex; if (b != null ) { p.m_Position = p.m_PrevPosition = b.position; p.m_InitLocalPosition = b.localPosition; p.m_InitLocalRotation = b.localRotation; } else { Transform pb = pt.m_Particles[parentIndex].m_Transform; if (m_EndLength > 0 ) { Transform ppb = pb.parent; if (ppb != null ) { p.m_EndOffset = pb.InverseTransformPoint((pb.position * 2 - ppb.position)) * m_EndLength; } else { p.m_EndOffset = new Vector3(m_EndLength, 0 , 0 ); } } else { p.m_EndOffset=pb.InverseTransformPoint(transform.TransformDirection(m_EndOffset)+pb.position); } p.m_Position = p.m_PrevPosition = pb.TransformPoint(p.m_EndOffset); p.m_InitLocalPosition = Vector3.zero; p.m_InitLocalRotation = Quaternion.identity; } if (parentIndex >= 0 ) { boneLength += (pt.m_Particles[parentIndex].m_Transform.position - p.m_Position).magnitude; p.m_BoneLength = boneLength; pt.m_BoneTotalLength = Mathf.Max(pt.m_BoneTotalLength, boneLength); ++pt.m_Particles[parentIndex].m_ChildCount; } int index = pt.m_Particles.Count; pt.m_Particles.Add(p); if (b != null ) { for (int i = 0 ; i < b.childCount; ++i) { Transform child = b.GetChild(i); bool exclude = false ; if (m_Exclusions != null ) { exclude = m_Exclusions.Contains(child); } if (!exclude) { AppendParticles(pt, child, index, boneLength); } else if (m_EndLength > 0 || m_EndOffset != Vector3.zero) { AppendParticles(pt, null , index, boneLength); } } if (b.childCount == 0 && (m_EndLength > 0 || m_EndOffset != Vector3.zero)) { AppendParticles(pt, null , index, boneLength); } } }
UpdateParameters() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 public void UpdateParameters (){ SetWeight(m_BlendWeight); for (int i = 0 ; i < m_ParticleTrees.Count; ++i) { UpdateParameters(m_ParticleTrees[i]); } } void UpdateParameters (ParticleTree pt ){ pt.m_LocalGravity=pt.m_RootWorldToLocalMatrix.MultiplyVector(m_Gravity).normalized*m_Gravity.magnitude; for (int i = 0 ; i < pt.m_Particles.Count; ++i) { Particle p = pt.m_Particles[i]; p.m_Damping = m_Damping; p.m_Elasticity = m_Elasticity; p.m_Stiffness = m_Stiffness; p.m_Inert = m_Inert; p.m_Friction = m_Friction; p.m_Radius = m_Radius; if (pt.m_BoneTotalLength > 0 ) { float a = p.m_BoneLength / pt.m_BoneTotalLength; if (m_DampingDistrib != null && m_DampingDistrib.keys.Length > 0 ) p.m_Damping *= m_DampingDistrib.Evaluate(a); if (m_ElasticityDistrib != null && m_ElasticityDistrib.keys.Length > 0 ) p.m_Elasticity *= m_ElasticityDistrib.Evaluate(a); if (m_StiffnessDistrib != null && m_StiffnessDistrib.keys.Length > 0 ) p.m_Stiffness *= m_StiffnessDistrib.Evaluate(a); if (m_InertDistrib != null && m_InertDistrib.keys.Length > 0 ) p.m_Inert *= m_InertDistrib.Evaluate(a); if (m_FrictionDistrib != null && m_FrictionDistrib.keys.Length > 0 ) p.m_Friction *= m_FrictionDistrib.Evaluate(a); if (m_RadiusDistrib != null && m_RadiusDistrib.keys.Length > 0 ) p.m_Radius *= m_RadiusDistrib.Evaluate(a); } p.m_Damping = Mathf.Clamp01(p.m_Damping); p.m_Elasticity = Mathf.Clamp01(p.m_Elasticity); p.m_Stiffness = Mathf.Clamp01(p.m_Stiffness); p.m_Inert = Mathf.Clamp01(p.m_Inert); p.m_Friction = Mathf.Clamp01(p.m_Friction); p.m_Radius = Mathf.Max(p.m_Radius, 0 ); } }
Update() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void Update () { if (m_UpdateMode != UpdateMode.AnimatePhysics) { PreUpdate(); } #if ENABLE_MULTITHREAD //如果开了多线程 if (m_PreUpdateCount > 0 && m_Multithread) { AddPendingWork(this ); m_WorkAdded = true ; } #endif ++s_UpdateCount; }
PreUpdate() 1 2 3 4 5 6 7 8 void PreUpdate (){ if (IsNeedUpdate()) { InitTransforms(); } ++m_PreUpdateCount; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 void InitTransforms () { for (int i = 0 ; i < m_ParticleTrees.Count; ++i) { InitTransforms(m_ParticleTrees[i]); } } void InitTransforms (ParticleTree pt ) { for (int i = 0 ; i < pt.m_Particles.Count; ++i) { Particle p = pt.m_Particles[i]; if (p.m_TransformNotNull) { p.m_Transform.localPosition = p.m_InitLocalPosition; p.m_Transform.localRotation = p.m_InitLocalRotation; } } }
AddPendingWork() 1 2 3 4 static void AddPendingWork (DynamicBone db ) { s_PendingWorks.Add(db); }
LateUpdate() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 void LateUpdate () { if (m_PreUpdateCount == 0 ) return ; if (s_UpdateCount > 0 ) { s_UpdateCount = 0 ; ++s_PrepareFrame; } SetWeight(m_BlendWeight); #if ENABLE_MULTITHREAD if (m_WorkAdded) { m_WorkAdded = false ; ExecuteWorks(); } else #endif { CheckDistance(); if (IsNeedUpdate()) { Prepare(); UpdateParticles(); ApplyParticlesToTransforms(); } } m_PreUpdateCount = 0 ; }
SetWeight() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public void SetWeight (float w ){ if (m_Weight != w) { if (w == 0 ) { InitTransforms(); } else if (m_Weight == 0 ) { ResetParticlesPosition(); } m_Weight = m_BlendWeight = w; } }
ResetParticlesPosition() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 void ResetParticlesPosition (){ for (int i = 0 ; i < m_ParticleTrees.Count; ++i) { ResetParticlesPosition(m_ParticleTrees[i]); } m_ObjectPrevPosition = transform.position; } void ResetParticlesPosition (ParticleTree pt ){ for (int i = 0 ; i < pt.m_Particles.Count; ++i) { Particle p = pt.m_Particles[i]; if (p.m_TransformNotNull) { p.m_Position = p.m_PrevPosition = p.m_Transform.position; } else { Transform pb = pt.m_Particles[p.m_ParentIndex].m_Transform; p.m_Position = p.m_PrevPosition = pb.TransformPoint(p.m_EndOffset); } p.m_isCollide = false ; } }
CheckDistance() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 void CheckDistance (){ if (!m_DistantDisable) return ; Transform rt = m_ReferenceObject; if (rt == null && Camera.main != null ) { rt = Camera.main.transform; } if (rt != null ) { float d2 = (rt.position - transform.position).sqrMagnitude; bool disable = d2 > m_DistanceToObject * m_DistanceToObject; if (disable != m_DistantDisabled) { if (!disable) { ResetParticlesPosition(); } m_DistantDisabled = disable; } } }
IsNeedUpdate() 1 2 3 4 bool IsNeedUpdate (){ return m_Weight > 0 && !(m_DistantDisable && m_DistantDisabled); }
Prepare() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 void Prepare () { m_DeltaTime = Time.deltaTime; #if UNITY_5_3_OR_NEWER if (m_UpdateMode == UpdateMode.UnscaledTime) { m_DeltaTime = Time.unscaledDeltaTime; } else if (m_UpdateMode == UpdateMode.AnimatePhysics) { m_DeltaTime = Time.fixedDeltaTime * m_PreUpdateCount; } #endif m_ObjectScale = Mathf.Abs(transform.lossyScale.x); m_ObjectMove = transform.position - m_ObjectPrevPosition; m_ObjectPrevPosition = transform.position; for (int i = 0 ; i < m_ParticleTrees.Count; ++i) { ParticleTree pt = m_ParticleTrees[i]; pt.m_RestGravity = pt.m_Root.TransformDirection(pt.m_LocalGravity); for (int j = 0 ; j < pt.m_Particles.Count; ++j) { Particle p = pt.m_Particles[j]; if (p.m_TransformNotNull) { p.m_TransformPosition = p.m_Transform.position; p.m_TransformLocalPosition = p.m_Transform.localPosition; p.m_TransformLocalToWorldMatrix = p.m_Transform.localToWorldMatrix; } } } if (m_EffectiveColliders != null ) { m_EffectiveColliders.Clear(); } if (m_Colliders != null ) { for (int i = 0 ; i < m_Colliders.Count; ++i) { DynamicBoneColliderBase c = m_Colliders[i]; if (c != null && c.enabled) { if (m_EffectiveColliders == null ) { m_EffectiveColliders = new List<DynamicBoneColliderBase>(); } m_EffectiveColliders.Add(c); if (c.PrepareFrame != s_PrepareFrame) { c.Prepare(); c.PrepareFrame = s_PrepareFrame; } } } } }
UpdateParticles() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 void UpdateParticles (){ if (m_ParticleTrees.Count <= 0 ) return ; int loop = 1 ; float timeVar = 1 ; float dt = m_DeltaTime; if (m_UpdateMode == UpdateMode.Default) { if (m_UpdateRate > 0 ) { timeVar = dt * m_UpdateRate; } } else { if (m_UpdateRate > 0 ) { float frameTime = 1.0f / m_UpdateRate; m_Time += dt; loop = 0 ; while (m_Time >= frameTime) { m_Time -= frameTime; if (++loop >= 3 ) { m_Time = 0 ; break ; } } } } if (loop > 0 ) { for (int i = 0 ; i < loop; ++i) { UpdateParticles1(timeVar, i); UpdateParticles2(timeVar); } } else { SkipUpdateParticles(); } }
UpdateParticles1() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 void UpdateParticles1 (float timeVar, int loopIndex ){ for (int i = 0 ; i < m_ParticleTrees.Count; ++i) { UpdateParticles1(m_ParticleTrees[i], timeVar, loopIndex); } } void UpdateParticles1 (ParticleTree pt, float timeVar, int loopIndex ){ Vector3 force = m_Gravity; Vector3 fdir = m_Gravity.normalized; Vector3 pf = fdir * Mathf.Max(Vector3.Dot(pt.m_RestGravity, fdir), 0 ); force -= pf; force = (force + m_Force) * (m_ObjectScale * timeVar); Vector3 objectMove = loopIndex == 0 ? m_ObjectMove : Vector3.zero; for (int i = 0 ; i < pt.m_Particles.Count; ++i) { Particle p = pt.m_Particles[i]; if (p.m_ParentIndex >= 0 ) { Vector3 v = p.m_Position - p.m_PrevPosition; Vector3 rmove = objectMove * p.m_Inert; p.m_PrevPosition = p.m_Position + rmove; float damping = p.m_Damping; if (p.m_isCollide) { damping += p.m_Friction; if (damping > 1 ) { damping = 1 ; } p.m_isCollide = false ; } p.m_Position += v * (1 - damping) + force + rmove; } else { p.m_PrevPosition = p.m_Position; p.m_Position = p.m_TransformPosition; } } }
UpdateParticles2() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 void UpdateParticles2 (float timeVar ){ for (int i = 0 ; i < m_ParticleTrees.Count; ++i) { UpdateParticles2(m_ParticleTrees[i], timeVar); } } void UpdateParticles2 (ParticleTree pt, float timeVar ){ var movePlane = new Plane(); for (int i = 1 ; i < pt.m_Particles.Count; ++i) { Particle p = pt.m_Particles[i]; Particle p0 = pt.m_Particles[p.m_ParentIndex]; float restLen; if (p.m_TransformNotNull) { restLen = (p0.m_TransformPosition - p.m_TransformPosition).magnitude; } else { restLen = p0.m_TransformLocalToWorldMatrix.MultiplyVector(p.m_EndOffset).magnitude; } float stiffness = Mathf.Lerp(1.0f , p.m_Stiffness, m_Weight); if (stiffness > 0 || p.m_Elasticity > 0 ) { Matrix4x4 m0 = p0.m_TransformLocalToWorldMatrix; m0.SetColumn(3 , p0.m_Position); Vector3 restPos; if (p.m_TransformNotNull) { restPos = m0.MultiplyPoint3x4(p.m_TransformLocalPosition); } else { restPos = m0.MultiplyPoint3x4(p.m_EndOffset); } Vector3 d = restPos - p.m_Position; p.m_Position += d * (p.m_Elasticity * timeVar); if (stiffness > 0 ) { d = restPos - p.m_Position; float len = d.magnitude; float maxlen = restLen * (1 - stiffness) * 2 ; if (len > maxlen) { p.m_Position += d * ((len - maxlen) / len); } } } if (m_EffectiveColliders != null ) { float particleRadius = p.m_Radius * m_ObjectScale; for (int j = 0 ; j < m_EffectiveColliders.Count; ++j) { DynamicBoneColliderBase c = m_EffectiveColliders[j]; p.m_isCollide |= c.Collide(ref p.m_Position, particleRadius); } } if (m_FreezeAxis != FreezeAxis.None) { Vector3 planeNormal=p0.m_TransformLocalToWorldMatrix.GetColumn((int )m_FreezeAxis-1 ).normalized; movePlane.SetNormalAndPosition(planeNormal, p0.m_Position); p.m_Position -= movePlane.normal * movePlane.GetDistanceToPoint(p.m_Position); } Vector3 dd = p0.m_Position - p.m_Position; float leng = dd.magnitude; if (leng > 0 ) { p.m_Position += dd * ((leng - restLen) / leng); } } }
ApplyParticlesToTransforms() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 void ApplyParticlesToTransforms () { Vector3 ax = Vector3.right; Vector3 ay = Vector3.up; Vector3 az = Vector3.forward; bool nx = false , ny = false , nz = false ; #if !UNITY_5_4_OR_NEWER Vector3 lossyScale = transform.lossyScale; if (lossyScale.x < 0 || lossyScale.y < 0 || lossyScale.z < 0 ) { Transform mirrorObject = transform; do { Vector3 ls = mirrorObject.localScale; nx = ls.x < 0 ; if (nx) ax = mirrorObject.right; ny = ls.y < 0 ; if (ny) ay = mirrorObject.up; nz = ls.z < 0 ; if (nz) az = mirrorObject.forward; if (nx || ny || nz) break ; mirrorObject = mirrorObject.parent; } while (mirrorObject != null ); } #endif for (int i = 0 ; i < m_ParticleTrees.Count; ++i) { ApplyParticlesToTransforms(m_ParticleTrees[i], ax, ay, az, nx, ny, nz); } } void ApplyParticlesToTransforms (ParticleTree pt,Vector3 ax,Vector3 ay,Vector3 az,bool nx,bool ny,bool nz ) { for (int i = 1 ; i < pt.m_Particles.Count; ++i) { Particle p = pt.m_Particles[i]; Particle p0 = pt.m_Particles[p.m_ParentIndex]; if (p0.m_ChildCount <= 1 ) { Vector3 localPos; if (p.m_TransformNotNull) { localPos = p.m_Transform.localPosition; } else { localPos = p.m_EndOffset; } Vector3 v0 = p0.m_Transform.TransformDirection(localPos); Vector3 v1 = p.m_Position - p0.m_Position; #if !UNITY_5_4_OR_NEWER if (nx) v1 = MirrorVector(v1, ax); if (ny) v1 = MirrorVector(v1, ay); if (nz) v1 = MirrorVector(v1, az); #endif Quaternion rot = Quaternion.FromToRotation(v0, v1); p0.m_Transform.rotation = rot * p0.m_Transform.rotation; } if (p.m_TransformNotNull) { p.m_Transform.position = p.m_Position; } } }
总结 该插件上次更新1.3.2版本还是一年多前,现在应该是入土了吧。解析Dynamic bone的文章前面已有许多(但没有新版),所以我务于全,展现了该脚本整个基本流程函数(其他未展现的流程函数基本也都调用前文出现过的子函数),希望对读者能有些许帮助。
总体而言,开始先构造了骨骼的链式结构,记录本地坐标,之后由根到尾一级一级的应用惯性,摩擦,外力,弹力,刚度处理更新位置。如果仍有疑惑,也可移步知乎其他文章。如核心代码的详解:DynamicBone(动态骨骼)源码赏析 ,模拟算法的讲解:动态骨骼Dynamic Bone算法详解 。