Skip to content

Commit cca7e0d

Browse files
author
Roberto De Ioris
authored
Update SnippetsForStaticAndSkeletalMeshes.md
1 parent 343c68d commit cca7e0d

File tree

1 file changed

+165
-2
lines changed

1 file changed

+165
-2
lines changed

tutorials/SnippetsForStaticAndSkeletalMeshes.md

Lines changed: 165 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -854,6 +854,169 @@ Note, that this example use the original skeleton for the mesh. For real-world c
854854

855855
## SkeletalMesh: Building from Collada
856856

857+
There is already a tutorial for importing collada files as static meshes (https://github.com/20tab/UnrealEnginePython/blob/master/tutorials/WritingAColladaFactoryWithPython.md). This snippet shows how to import skeletal meshes. You can combine both to build a full-featured importer.
858+
859+
The main topic here is how to deal with matrices. Collada files expose bone infos as 4x4 column major matrices. FTransform objects can be built by passing them a 4x4 matrix in the UE4 convention (row-major). Collada obviously follows the OpenGL conventions (column-major), so we need to transpose all matrices. The pycollada modules returns matrices as numpy arrays, so in addition to transposing we need to flatten them (FTransform expects a simple iterator of 16 float elements).
860+
861+
Remember to install the pycollada module:
862+
863+
```
864+
pip install pycollada
865+
```
866+
867+
Here is the code, note that this time we do not save assets. All of the objects are transient (storing them is left as exercise).
868+
869+
Check how we need to fix UVs too, as UE4 do not use the OpenGL convention (damn, this is starting to become annoying ;) of texcoords origin on the left-bottom.
870+
871+
```python
872+
import unreal_engine as ue
873+
from unreal_engine.classes import Skeleton, SkeletalMesh
874+
from unreal_engine import FTransform, FSoftSkinVertex, FVector, FRotator, FQuat
875+
876+
from collada import Collada
877+
import numpy
878+
879+
class ColladaLoader:
880+
881+
def __init__(self, filename):
882+
self.dae = Collada(filename)
883+
self.controller = self.dae.controllers[0]
884+
# while i love quaternions, building them from euler rotations
885+
# looks generally easier to the reader...
886+
# this rotation is required for fixing the inverted Z axis on
887+
# bone positions. As bind pose should have rotations set to 0
888+
# we will apply this quaternion directly to bone positions
889+
self.base_quaternion = FRotator(0, 180, 180).quaternion()
890+
self.skeleton = self.get_skeleton()
891+
self.mesh = self.get_mesh(self.controller.geometry)
892+
893+
def get_skeleton(self):
894+
# this mapping will allows us to fast retrieve a bone index, given its name
895+
# instead of relying on UE4 api
896+
self.bone_mapping = {}
897+
for scene in self.dae.scenes:
898+
# find the first node of type 'JOINT'
899+
for node in scene.nodes:
900+
if node.xmlnode.attrib.get('type', None) == 'JOINT':
901+
self.skeleton = Skeleton()
902+
self.traverse_hierarchy(node, FTransform(), -1)
903+
return self.skeleton
904+
905+
def traverse_hierarchy(self, node, parent_transform, parent_id):
906+
transform = FTransform(self.controller.joint_matrices[node.id].transpose().flatten())
907+
v0 = transform.translation
908+
q0 = transform.quaternion
909+
910+
# fix axis from OpenGL to UE4 (note the quaternion multiplication)
911+
transform.translation = FVector(v0.z, v0.x, v0.y) * self.base_quaternion
912+
transform.quaternion = FQuat(q0[2], q0[0] * -1, q0[1] * -1, q0[3])
913+
914+
relative_transform = transform * parent_transform.inverse()
915+
916+
parent_id = self.skeleton.skeleton_add_bone(node.id, parent_id, relative_transform)
917+
# used for fast joint mapping
918+
self.bone_mapping[node.id] = parent_id
919+
for child in node.children:
920+
if child.xmlnode.attrib.get('type', None) == 'JOINT':
921+
self.traverse_hierarchy(child, transform, parent_id)
922+
923+
def get_mesh(self, geometry):
924+
print(self.bone_mapping)
925+
self.mesh = SkeletalMesh()
926+
self.mesh.skeletal_mesh_set_skeleton(self.skeleton)
927+
triset = geometry.primitives[0]
928+
# the numpy.ravel function, completely flatten an array
929+
vertices = numpy.ravel(triset.vertex[triset.vertex_index])
930+
uvs = numpy.ravel(triset.texcoordset[0][triset.texcoord_indexset[0]])
931+
normals = numpy.ravel(triset.normal[triset.normal_index])
932+
# currently pycollada has no shortcuts for bones list
933+
bones = []
934+
weights = []
935+
for index in triset.vertex_index:
936+
influence_bones = []
937+
influence_weights = []
938+
for influence in self.controller.index[index]:
939+
influence_bones.append(self.bone_mapping[self.controller.weight_joints[influence[0]]])
940+
weight_value = int(self.controller.weights[influence[1]] * 255)
941+
# hack for avoiding required bones to be skipped
942+
# most of file weights will result in 254 instead of 255 (this is caused by float instability)
943+
if weight_value == 0:
944+
weight_value = 1
945+
influence_weights.append(weight_value)
946+
bones.append(influence_bones)
947+
weights.append(influence_weights)
948+
soft_vertices = self.build_soft_vertices(vertices, uvs, normals, bones, weights)
949+
self.mesh.skeletal_mesh_build_lod(soft_vertices)
950+
return self.mesh
951+
952+
def build_soft_vertices(self, vertices, uvs, normals, bones, weights):
953+
soft_vertices = []
954+
for i in range(0, len(vertices), 3):
955+
v = FSoftSkinVertex()
956+
xv, yv, zv = vertices[i], vertices[i+1], vertices[i+2]
957+
# invert forward
958+
v.position = FVector(zv * -1, xv, yv)
959+
xn, yn, zn = normals[i], normals[i+1], normals[i+2]
960+
# invert forward
961+
v.tangent_z = FVector(zn * -1, xn, yn)
962+
963+
# get uv index
964+
uv_index = int(i / 3 * 2)
965+
# fix uvs from 0 on bottom to 0 on top
966+
v.uvs = [(uvs[uv_index], 1 - uvs[uv_index+1])]
967+
968+
# get joint index
969+
joint_index = int(i / 3)
970+
# fix a special condition where the first bone has zero weight
971+
# but there are multiple influences. This is required as UE4 automatically
972+
# modify the first bone if weight normalization fails
973+
new_best_index = weights[joint_index].index(max(weights[joint_index]))
974+
if new_best_index > 0:
975+
first_bone = bones[joint_index][new_best_index]
976+
first_weight = weights[joint_index][new_best_index]
977+
bones[joint_index][new_best_index] = bones[joint_index][0]
978+
weights[joint_index][new_best_index] = weights[joint_index][0]
979+
bones[joint_index][0] = first_bone
980+
weights[joint_index][0] = first_weight
981+
v.influence_bones = bones[joint_index]
982+
v.influence_weights = weights[joint_index]
983+
soft_vertices.append(v)
984+
985+
return soft_vertices
986+
987+
filename = ue.open_file_dialog('Choose a Collada file', '', '', 'Collada|*.dae;')[0]
988+
loader = ColladaLoader(filename)
989+
ue.open_editor_for_asset(loader.skeleton)
990+
```
991+
992+
A note about joints management in UE4.
993+
994+
Unreal expects each vertex weights to be normalized. It means their sum must be 255 (maximum value for FSoftSkinVertex). If the sum
995+
is not 255, UE4 will automatically add the required weight to the first bone influence.
996+
997+
This behaviour could lead to annoying errors generated by float errors (Collada exposes bone weights as float value in the range 0..1).
998+
999+
As an example, most of the vertices will result in a total weight of 254 (instead of 255), and (more bad), Collada does not enforce the first influence to be the one with the highest value.
1000+
1001+
For both reasons we add 1 to 0 weights (collada files does not report bone influences with a weight of 0, so we are safe) and, more important, we reorder bone influences in a way that the first one is always the one with the higher weight (more infos in the code comments):
1002+
1003+
```python
1004+
new_best_index = weights[joint_index].index(max(weights[joint_index]))
1005+
if new_best_index > 0:
1006+
first_bone = bones[joint_index][new_best_index]
1007+
first_weight = weights[joint_index][new_best_index]
1008+
bones[joint_index][new_best_index] = bones[joint_index][0]
1009+
weights[joint_index][new_best_index] = weights[joint_index][0]
1010+
bones[joint_index][0] = first_bone
1011+
weights[joint_index][0] = first_weight
1012+
v.influence_bones = bones[joint_index]
1013+
v.influence_weights = weights[joint_index]
1014+
```
1015+
1016+
If all goes well you wll end with your model correctly loaded (the screenshot shows a mixamo-exported model):
1017+
1018+
![Collada](https://github.com/20tab/UnrealEnginePython/blob/master/tutorials/SnippetsForStaticAndSkeletalMeshes_Assets/collada.PNG)
1019+
8571020
## SkeletalMesh: Morph Targets
8581021

8591022
Morph Targets (or Blend Shapes), are a simple form of animation where a specific vertex (and eventually its normal) is translated to a new position. By interpolating the transition from the default position to the new one defined by the morph target, you can generate an animation. Morph Targets are common in facial animations or, more generally, whenever an object must be heavily deformed.
@@ -1046,8 +1209,8 @@ This method is how we build the skeleton:
10461209
quat = FQuat(bone['rotq'][2], bone['rotq'][0] * -1,
10471210
bone['rotq'][1] * -1, bone['rotq'][3])
10481211
elif 'rot' in bone:
1049-
quat = FRotator(bone['rot'][2], bone['rot'][0] * -
1050-
1, bome['rot'][1] * -1).quaternion()
1212+
quat = FRotator(bone['rot'][2], bone['rot'][0] - 180
1213+
, bone['rot'][1] - 180).quaternion()
10511214
pos = FVector(bone['pos'][2] * -1, bone['pos'][0],
10521215
bone['pos'][1]) * self.scale
10531216
# always set parent+1 as we added the root bone before

0 commit comments

Comments
 (0)