DQ skinning for Unity
Dual Quaternion skinning implementation with bulging compensation
DualQuaternionSkinner.cs
1 using UnityEngine;
2 
10 [RequireComponent(typeof(MeshFilter))]
11 public class DualQuaternionSkinner : MonoBehaviour
12 {
17  public Vector3 boneOrientationVector = Vector3.up;
18 
19  bool viewFrustrumCulling = true;
20 
21  struct VertexInfo
22  {
23  // could use float3 instead of float4 but NVidia says structures not aligned to 128 bits are slow
24  // https://developer.nvidia.com/content/understanding-structured-buffer-performance
25 
26  public Vector4 position;
27  public Vector4 normal;
28  public Vector4 tangent;
29 
30  public int boneIndex0;
31  public int boneIndex1;
32  public int boneIndex2;
33  public int boneIndex3;
34 
35  public float weight0;
36  public float weight1;
37  public float weight2;
38  public float weight3;
39 
40  public float compensation_coef;
41  }
42 
43  struct MorphDelta
44  {
45  // could use float3 instead of float4 but NVidia says structures not aligned to 128 bits are slow
46  // https://developer.nvidia.com/content/understanding-structured-buffer-performance
47 
48  public Vector4 position;
49  public Vector4 normal;
50  public Vector4 tangent;
51  }
52 
53  struct DualQuaternion
54  {
55  public Quaternion rotationQuaternion;
56  public Vector4 position;
57  }
58 
59  const int numthreads = 1024; // must be same in compute shader code
60  const int textureWidth = 1024; // no need to adjust compute shaders
61 
65  [Range(0,1)]
66  public float bulgeCompensation = 0;
67 
68  public ComputeShader shaderComputeBoneDQ;
69  public ComputeShader shaderDQBlend;
70  public ComputeShader shaderApplyMorph;
71 
75  public bool started { get; private set; } = false;
76 
77  Matrix4x4[] poseMatrices;
78 
79  ComputeBuffer bufPoseMatrices;
80  ComputeBuffer bufSkinnedDq;
81  ComputeBuffer bufBindDq;
82 
83  ComputeBuffer bufVertInfo;
84  ComputeBuffer bufMorphTemp_1;
85  ComputeBuffer bufMorphTemp_2;
86 
87  ComputeBuffer bufBoneDirections;
88 
89  ComputeBuffer[] arrBufMorphDeltas;
90 
91  float[] morphWeights;
92 
93  MeshFilter mf
94  {
95  get
96  {
97  if (this._mf == null)
98  {
99  this._mf = this.GetComponent<MeshFilter>();
100  }
101 
102  return this._mf;
103  }
104  }
105  MeshFilter _mf;
106 
107  MeshRenderer mr
108  {
109  get
110  {
111  if (this._mr == null)
112  {
113  this._mr = this.GetComponent<MeshRenderer>();
114  if (this._mr == null)
115  {
116  this._mr = this.gameObject.AddComponent<MeshRenderer>();
117  }
118  }
119 
120  return this._mr;
121  }
122  }
123  MeshRenderer _mr;
124 
125  SkinnedMeshRenderer smr
126  {
127  get
128  {
129  if (this._smr == null)
130  {
131  this._smr = this.GetComponent<SkinnedMeshRenderer>();
132  }
133 
134  return this._smr;
135  }
136  }
137  SkinnedMeshRenderer _smr;
138 
139  MaterialPropertyBlock materialPropertyBlock;
140 
141  Transform[] bones;
142  Matrix4x4[] bindPoses;
143 
144  /*
145  Vulkan and OpenGL only support ComputeBuffer in compute shaders
146  passing data to the vertex and fragment shaders is done through RenderTextures
147 
148  using ComputeBuffers would improve the efficiency slightly but it would only work with Dx11
149 
150  layout is as such:
151  rtSkinnedData_1 float4 vertex.xyz, normal.x
152  rtSkinnedData_2 float4 normal.yz, tangent.xy
153  rtSkinnedData_3 float2 tangent.zw
154  */
155  RenderTexture rtSkinnedData_1;
156  RenderTexture rtSkinnedData_2;
157  RenderTexture rtSkinnedData_3;
158 
159  int kernelHandleComputeBoneDQ;
160  int kernelHandleDQBlend;
161  int kernelHandleApplyMorph;
162 
167  public void SetViewFrustrumCulling(bool viewFrustrumculling)
168  {
169  if (this.viewFrustrumCulling == viewFrustrumculling)
170  return;
171 
172  this.viewFrustrumCulling = viewFrustrumculling;
173 
174  if (this.started == true)
175  UpdateViewFrustrumCulling();
176  }
177 
183  {
184  return this.viewFrustrumCulling;
185  }
186 
187  void UpdateViewFrustrumCulling()
188  {
189  if (this.viewFrustrumCulling)
190  this.mf.sharedMesh.bounds = this.smr.localBounds;
191  else
192  this.mf.sharedMesh.bounds = new Bounds(Vector3.zero, Vector3.one * 100000000);
193  }
194 
201  public float[] GetBlendShapeWeights()
202  {
203  float[] weights = new float[this.morphWeights.Length];
204  for (int i = 0; i < weights.Length; i++)
205  {
206  weights[i] = this.morphWeights[i];
207  }
208 
209  return weights;
210  }
211 
218  public void SetBlendShapeWeights(float[] weights)
219  {
220  if (weights.Length != this.morphWeights.Length)
221  {
222  throw new System.ArgumentException(
223  "An array of weights must contain the number of elements " +
224  $"equal to the number of available blendshapes. Currently " +
225  $"{this.morphWeights.Length} blendshapes ara available but {weights.Length} weights were passed."
226  );
227  }
228 
229  for (int i = 0; i < weights.Length; i++)
230  {
231  this.morphWeights[i] = weights[i];
232  }
233 
234  this.ApplyMorphs();
235  }
236 
244  public void SetBlendShapeWeight(int index, float weight)
245  {
246  if (this.started == false)
247  {
248  this.GetComponent<SkinnedMeshRenderer>().SetBlendShapeWeight(index, weight);
249  return;
250  }
251 
252  if (index < 0 || index >= this.morphWeights.Length)
253  {
254  throw new System.IndexOutOfRangeException("Blend shape index out of range");
255  }
256 
257  this.morphWeights[index] = weight;
258 
259  this.ApplyMorphs();
260  }
261 
269  public float GetBlendShapeWeight(int index)
270  {
271  if (this.started == false)
272  {
273  return this.GetComponent<SkinnedMeshRenderer>().GetBlendShapeWeight(index);
274  }
275 
276  if (index < 0 || index >= this.morphWeights.Length)
277  {
278  throw new System.IndexOutOfRangeException("Blend shape index out of range");
279  }
280 
281  return this.morphWeights[index];
282  }
283 
290  public Mesh mesh
291  {
292  get
293  {
294  if (this.started == false)
295  {
296  return this.smr.sharedMesh;
297  }
298 
299  return this.mf.sharedMesh;
300  }
301  }
302 
307  {
308  if (this.started == false)
309  {
310  return;
311  }
312 
313  var vertInfos = new VertexInfo[this.mf.sharedMesh.vertexCount];
314  this.bufVertInfo.GetData(vertInfos);
315 
316  for (int i = 0; i < vertInfos.Length; i++)
317  {
318  Matrix4x4 bindPose = this.bindPoses[vertInfos[i].boneIndex0].inverse;
319  Quaternion boneBindRotation = bindPose.ExtractRotation();
320  Vector3 boneDirection = boneBindRotation * this.boneOrientationVector; // ToDo figure out bone orientation
321  Vector3 bonePosition = bindPose.ExtractPosition();
322  Vector3 toBone = bonePosition - (Vector3)vertInfos[i].position;
323 
324  vertInfos[i].compensation_coef = Vector3.Cross(toBone, boneDirection).magnitude;
325  }
326 
327  this.bufVertInfo.SetData(vertInfos);
328  this.ApplyMorphs();
329  }
330 
331  int GetVertexTextureHeight(int vertexCount, int textureWidth)
332  {
333  int textureHeight = this.mf.sharedMesh.vertexCount / textureWidth;
334  if (this.mf.sharedMesh.vertexCount % textureWidth != 0)
335  {
336  textureHeight++;
337  }
338  return textureHeight;
339  }
340 
341  void GrabMeshFromSkinnedMeshRenderer()
342  {
343  this.ReleaseBuffers();
344 
345  this.mf.sharedMesh = this.smr.sharedMesh;
346  this.bindPoses = this.mf.sharedMesh.bindposes;
347 
348  this.arrBufMorphDeltas = new ComputeBuffer[this.mf.sharedMesh.blendShapeCount];
349 
350  this.morphWeights = new float[this.mf.sharedMesh.blendShapeCount];
351 
352  var deltaVertices = new Vector3[this.mf.sharedMesh.vertexCount];
353  var deltaNormals = new Vector3[this.mf.sharedMesh.vertexCount];
354  var deltaTangents = new Vector3[this.mf.sharedMesh.vertexCount];
355 
356  var deltaVertInfos = new MorphDelta[this.mf.sharedMesh.vertexCount];
357 
358  for (int i = 0; i < this.mf.sharedMesh.blendShapeCount; i++)
359  {
360  this.mf.sharedMesh.GetBlendShapeFrameVertices(i, 0, deltaVertices, deltaNormals, deltaTangents);
361 
362  this.arrBufMorphDeltas[i] = new ComputeBuffer(this.mf.sharedMesh.vertexCount, sizeof(float) * 12);
363 
364  for (int k = 0; k < this.mf.sharedMesh.vertexCount; k++)
365  {
366  deltaVertInfos[k].position = deltaVertices != null ? deltaVertices[k] : Vector3.zero;
367  deltaVertInfos[k].normal = deltaNormals != null ? deltaNormals[k] : Vector3.zero;
368  deltaVertInfos[k].tangent = deltaTangents != null ? deltaTangents[k] : Vector3.zero;
369  }
370 
371  this.arrBufMorphDeltas[i].SetData(deltaVertInfos);
372  }
373 
374  Material[] materials = this.smr.sharedMaterials;
375  for (int i = 0; i < materials.Length; i++)
376  {
377  materials[i].SetInt("_DoSkinning", 1);
378  }
379  this.mr.materials = materials;
380 
381  this.shaderDQBlend.SetInt("textureWidth", textureWidth);
382 
383  this.poseMatrices = new Matrix4x4[this.mf.sharedMesh.bindposes.Length];
384 
385  // initiate textures and buffers
386 
387  int textureHeight = GetVertexTextureHeight(this.mf.sharedMesh.vertexCount, textureWidth);
388 
389  this.rtSkinnedData_1 = new RenderTexture(textureWidth, textureHeight, 0, RenderTextureFormat.ARGBFloat)
390  {
391  filterMode = FilterMode.Point,
392  enableRandomWrite = true
393  };
394  this.rtSkinnedData_1.Create();
395  this.shaderDQBlend.SetTexture(this.kernelHandleDQBlend, "skinned_data_1", this.rtSkinnedData_1);
396 
397  this.rtSkinnedData_2 = new RenderTexture(textureWidth, textureHeight, 0, RenderTextureFormat.ARGBFloat)
398  {
399  filterMode = FilterMode.Point,
400  enableRandomWrite = true
401  };
402  this.rtSkinnedData_2.Create();
403  this.shaderDQBlend.SetTexture(this.kernelHandleDQBlend, "skinned_data_2", this.rtSkinnedData_2);
404 
405  this.rtSkinnedData_3 = new RenderTexture(textureWidth, textureHeight, 0, RenderTextureFormat.RGFloat)
406  {
407  filterMode = FilterMode.Point,
408  enableRandomWrite = true
409  };
410  this.rtSkinnedData_3.Create();
411  this.shaderDQBlend.SetTexture(this.kernelHandleDQBlend, "skinned_data_3", this.rtSkinnedData_3);
412 
413  this.bufPoseMatrices = new ComputeBuffer(this.mf.sharedMesh.bindposes.Length, sizeof(float) * 16);
414  this.shaderComputeBoneDQ.SetBuffer(this.kernelHandleComputeBoneDQ, "pose_matrices", this.bufPoseMatrices);
415 
416  this.bufSkinnedDq = new ComputeBuffer(this.mf.sharedMesh.bindposes.Length, sizeof(float) * 8);
417  this.shaderComputeBoneDQ.SetBuffer(this.kernelHandleComputeBoneDQ, "skinned_dual_quaternions", this.bufSkinnedDq);
418  this.shaderDQBlend.SetBuffer(this.kernelHandleDQBlend, "skinned_dual_quaternions", this.bufSkinnedDq);
419 
420  this.bufBoneDirections = new ComputeBuffer(this.mf.sharedMesh.bindposes.Length, sizeof(float) * 4);
421  this.shaderComputeBoneDQ.SetBuffer(this.kernelHandleComputeBoneDQ, "bone_directions", this.bufBoneDirections);
422  this.shaderDQBlend.SetBuffer(this.kernelHandleDQBlend, "bone_directions", this.bufBoneDirections);
423 
424  this.bufVertInfo = new ComputeBuffer(this.mf.sharedMesh.vertexCount, sizeof(float) * 16 + sizeof(int) * 4 + sizeof(float));
425  var vertInfos = new VertexInfo[this.mf.sharedMesh.vertexCount];
426  Vector3[] vertices = this.mf.sharedMesh.vertices;
427  Vector3[] normals = this.mf.sharedMesh.normals;
428  Vector4[] tangents = this.mf.sharedMesh.tangents;
429  BoneWeight[] boneWeights = this.mf.sharedMesh.boneWeights;
430  for (int i = 0; i < vertInfos.Length; i++)
431  {
432  vertInfos[i].position = vertices[i];
433 
434  vertInfos[i].boneIndex0 = boneWeights[i].boneIndex0;
435  vertInfos[i].boneIndex1 = boneWeights[i].boneIndex1;
436  vertInfos[i].boneIndex2 = boneWeights[i].boneIndex2;
437  vertInfos[i].boneIndex3 = boneWeights[i].boneIndex3;
438 
439  vertInfos[i].weight0 = boneWeights[i].weight0;
440  vertInfos[i].weight1 = boneWeights[i].weight1;
441  vertInfos[i].weight2 = boneWeights[i].weight2;
442  vertInfos[i].weight3 = boneWeights[i].weight3;
443 
444  // determine per-vertex compensation coef
445 
446  Matrix4x4 bindPose = this.bindPoses[vertInfos[i].boneIndex0].inverse;
447  Quaternion boneBindRotation = bindPose.ExtractRotation();
448  Vector3 boneDirection = boneBindRotation * this.boneOrientationVector; // ToDo figure out bone orientation
449  Vector3 bonePosition = bindPose.ExtractPosition();
450  Vector3 toBone = bonePosition - (Vector3)vertInfos[i].position;
451 
452  vertInfos[i].compensation_coef = Vector3.Cross(toBone, boneDirection).magnitude;
453  }
454 
455  if (normals.Length > 0)
456  {
457  for (int i = 0; i < vertInfos.Length; i++)
458  {
459  vertInfos[i].normal = normals[i];
460  }
461  }
462 
463  if (tangents.Length > 0)
464  {
465  for (int i = 0; i < vertInfos.Length; i++)
466  {
467  vertInfos[i].tangent = tangents[i];
468  }
469  }
470 
471  this.bufVertInfo.SetData(vertInfos);
472  this.shaderDQBlend.SetBuffer(this.kernelHandleDQBlend, "vertex_infos", this.bufVertInfo);
473 
474  this.bufMorphTemp_1 = new ComputeBuffer(this.mf.sharedMesh.vertexCount, sizeof(float) * 16 + sizeof(int) * 4 + sizeof(float));
475  this.bufMorphTemp_2 = new ComputeBuffer(this.mf.sharedMesh.vertexCount, sizeof(float) * 16 + sizeof(int) * 4 + sizeof(float));
476 
477  // bind DQ buffer
478 
479  Matrix4x4[] bindPoses = this.mf.sharedMesh.bindposes;
480  var bindDqs = new DualQuaternion[bindPoses.Length];
481  for (int i = 0; i < bindPoses.Length; i++)
482  {
483  bindDqs[i].rotationQuaternion = bindPoses[i].ExtractRotation();
484  bindDqs[i].position = bindPoses[i].ExtractPosition();
485  }
486 
487  this.bufBindDq = new ComputeBuffer(bindDqs.Length, sizeof(float) * 8);
488  this.bufBindDq.SetData(bindDqs);
489  this.shaderComputeBoneDQ.SetBuffer(this.kernelHandleComputeBoneDQ, "bind_dual_quaternions", this.bufBindDq);
490 
491  this.UpdateViewFrustrumCulling();
492  this.ApplyMorphs();
493  }
494 
495  void ApplyMorphs()
496  {
497  ComputeBuffer bufMorphedVertexInfos = this.GetMorphedVertexInfos(
498  this.bufVertInfo,
499  ref this.bufMorphTemp_1,
500  ref this.bufMorphTemp_2,
501  this.arrBufMorphDeltas,
502  this.morphWeights
503  );
504 
505  this.shaderDQBlend.SetBuffer(this.kernelHandleDQBlend, "vertex_infos", bufMorphedVertexInfos);
506  }
507 
508  ComputeBuffer GetMorphedVertexInfos(ComputeBuffer bufOriginal, ref ComputeBuffer bufTemp_1, ref ComputeBuffer bufTemp_2, ComputeBuffer[] arrBufDelta, float[] weights)
509  {
510  ComputeBuffer bufSource = bufOriginal;
511 
512  for (int i = 0; i < weights.Length; i++)
513  {
514  if (weights[i] == 0)
515  {
516  continue;
517  }
518 
519  if (arrBufDelta[i] == null)
520  {
521  throw new System.NullReferenceException();
522  }
523 
524  this.shaderApplyMorph.SetBuffer(this.kernelHandleApplyMorph, "source", bufSource);
525  this.shaderApplyMorph.SetBuffer(this.kernelHandleApplyMorph, "target", bufTemp_1);
526  this.shaderApplyMorph.SetBuffer(this.kernelHandleApplyMorph, "delta", arrBufDelta[i]);
527  this.shaderApplyMorph.SetFloat("weight", weights[i] / 100f);
528 
529  int numThreadGroups = bufSource.count / numthreads;
530  if (bufSource.count % numthreads != 0)
531  {
532  numThreadGroups++;
533  }
534 
535  this.shaderApplyMorph.Dispatch(this.kernelHandleApplyMorph, numThreadGroups, 1, 1);
536 
537  bufSource = bufTemp_1;
538  bufTemp_1 = bufTemp_2;
539  bufTemp_2 = bufSource;
540  }
541 
542  return bufSource;
543  }
544 
545  void ReleaseBuffers()
546  {
547  this.bufBindDq?.Release();
548  this.bufPoseMatrices?.Release();
549  this.bufSkinnedDq?.Release();
550 
551  this.bufVertInfo?.Release();
552  this.bufMorphTemp_1?.Release();
553  this.bufMorphTemp_2?.Release();
554 
555  this.bufBoneDirections?.Release();
556 
557  if (this.arrBufMorphDeltas != null)
558  {
559  for (int i = 0; i < this.arrBufMorphDeltas.Length; i++)
560  {
561  this.arrBufMorphDeltas[i]?.Release();
562  }
563  }
564  }
565  void OnDestroy()
566  {
567  this.ReleaseBuffers();
568  }
569 
570  // Use this for initialization
571  void Start()
572  {
573  this.materialPropertyBlock = new MaterialPropertyBlock();
574 
575  this.shaderComputeBoneDQ = (ComputeShader)Instantiate(this.shaderComputeBoneDQ); // bug workaround
576  this.shaderDQBlend = (ComputeShader)Instantiate(this.shaderDQBlend); // bug workaround
577  this.shaderApplyMorph = (ComputeShader)Instantiate(this.shaderApplyMorph); // bug workaround
578 
579  this.kernelHandleComputeBoneDQ = this.shaderComputeBoneDQ.FindKernel("CSMain");
580  this.kernelHandleDQBlend = this.shaderDQBlend.FindKernel("CSMain");
581  this.kernelHandleApplyMorph = this.shaderApplyMorph.FindKernel("CSMain");
582 
583  this.bones = this.smr.bones;
584 
585  this.started = true;
586  this.GrabMeshFromSkinnedMeshRenderer();
587 
588  for (int i = 0; i < this.morphWeights.Length; i++)
589  {
590  this.morphWeights[i] = this.smr.GetBlendShapeWeight(i);
591  }
592 
593  this.smr.enabled = false;
594  }
595 
596  // Update is called once per frame
597  void Update()
598  {
599  if (this.mr.isVisible == false)
600  {
601  return;
602  }
603 
604  this.mf.sharedMesh.MarkDynamic(); // once or every frame? idk.
605  // at least it does not affect performance
606 
607  for (int i = 0; i < this.bones.Length; i++)
608  {
609  this.poseMatrices[i] = this.bones[i].localToWorldMatrix;
610  }
611 
612  this.bufPoseMatrices.SetData(this.poseMatrices);
613 
614  // Calculate blended quaternions
615 
616  int numThreadGroups = this.bones.Length / numthreads;
617  numThreadGroups += this.bones.Length % numthreads == 0 ? 0 : 1;
618 
619  this.shaderComputeBoneDQ.SetVector("boneOrientation", this.boneOrientationVector);
620  this.shaderComputeBoneDQ.SetMatrix(
621  "self_matrix",
622  this.transform.worldToLocalMatrix
623  );
624  this.shaderComputeBoneDQ.Dispatch(this.kernelHandleComputeBoneDQ, numThreadGroups, 1, 1);
625 
626  numThreadGroups = this.mf.sharedMesh.vertexCount / numthreads;
627  numThreadGroups += this.mf.sharedMesh.vertexCount % numthreads == 0 ? 0 : 1;
628 
629  this.shaderDQBlend.SetFloat("compensation_coef", this.bulgeCompensation);
630  this.shaderDQBlend.Dispatch(this.kernelHandleDQBlend, numThreadGroups, 1, 1);
631 
632  this.materialPropertyBlock.SetTexture("skinned_data_1", this.rtSkinnedData_1);
633  this.materialPropertyBlock.SetTexture("skinned_data_2", this.rtSkinnedData_2);
634  this.materialPropertyBlock.SetTexture("skinned_data_3", this.rtSkinnedData_3);
635  this.materialPropertyBlock.SetInt("skinned_tex_height", GetVertexTextureHeight(this.mf.sharedMesh.vertexCount, textureWidth));
636  this.materialPropertyBlock.SetInt("skinned_tex_width", textureWidth);
637 
638  this.mr.SetPropertyBlock(this.materialPropertyBlock);
639  }
640 }
DualQuaternionSkinner
Replaces Unity's default linear skinning with DQ skinning
Definition: DualQuaternionSkinner.cs:12
DualQuaternionSkinner.SetBlendShapeWeight
void SetBlendShapeWeight(int index, float weight)
Set weight for the blend shape with given index. Default range is 0-100. It is possible to apply ne...
Definition: DualQuaternionSkinner.cs:244
DualQuaternionSkinner.bulgeCompensation
float bulgeCompensation
Adjusts the amount of bulge-compensation.
Definition: DualQuaternionSkinner.cs:66
DualQuaternionSkinner.SetBlendShapeWeights
void SetBlendShapeWeights(float[] weights)
Applies blend shape weights from the given array. Default range is 0-100. It is possible to apply n...
Definition: DualQuaternionSkinner.cs:218
DualQuaternionSkinner.GetBlendShapeWeight
float GetBlendShapeWeight(int index)
Returns currently applied weight for the blend shape with given index. Default range is 0-100....
Definition: DualQuaternionSkinner.cs:269
DualQuaternionSkinner.started
bool started
Indicates whether DualQuaternionSkinner is currently active.
Definition: DualQuaternionSkinner.cs:75
DualQuaternionSkinner.mesh
Mesh mesh
UnityEngine.Mesh that is currently being rendered.
Definition: DualQuaternionSkinner.cs:291
DualQuaternionSkinner.UpdatePerVertexCompensationCoef
void UpdatePerVertexCompensationCoef()
If the value of boneOrientationVector was changed while DualQuaternionSkinner is active (started == t...
Definition: DualQuaternionSkinner.cs:306
DualQuaternionSkinner.GetViewFrustrumCulling
bool GetViewFrustrumCulling()
Returns current state of view frustrum culling.
Definition: DualQuaternionSkinner.cs:182
DualQuaternionSkinner.GetBlendShapeWeights
float[] GetBlendShapeWeights()
Returns an array of currently applied blend shape weights. Default range is 0-100....
Definition: DualQuaternionSkinner.cs:201
DualQuaternionSkinner.boneOrientationVector
Vector3 boneOrientationVector
Bone orientation is required for bulge-compensation. Do not set directly, use custom editor instead.
Definition: DualQuaternionSkinner.cs:17
DualQuaternionSkinner.SetViewFrustrumCulling
void SetViewFrustrumCulling(bool viewFrustrumculling)
Enable or disable view frustrum culling. When moving the bones manually to test the script,...
Definition: DualQuaternionSkinner.cs:167