GLTF/GLB character imports puts skeleton not at root

I have a character in VRM file format exported from VRoid Studio (free software for creating anime style 3D characters). VRM is GLB that follows certain standards to make it more portable in VR apps like VR Chat. VRM documentation I took the VRM file and changed the file extension to GLB and tried to use it with Omniverse.

The character is a public sample VRoid Studio character (download: Sendagaya_Shibu.glb - Google Drive)

When I go to the Content window, right click on the GLB file and select “Convert to USD”, the Skeleton prim is put under Root/J_Bip_C_Hips0, not Root. This means when trying to retarget, it moves the hips to height zero. Ideally the Skeleton should be under Root.

I also imported the character’s GLB into Blender launched from the OV Launcher, then exported as FBX using all default settings (the materials were lost, but I was checking where the Skeleton was inserted, not the materials). Note that the Skeleton was added directly under “Root”.

So it appears the GLB/GLTF importer is putting the Skeleton at the wrong layer.

Note also in Blender “Root” is marked as a bone in the armature.

From Discord in case useful, I encountered this with another VRoid Studio character.

During retargeting, Mati suspects the Skeleton not being under Root (height 0) is causing the hips to be dragged down to ground level, resulting in the waist (hips) being stretched down to the ground, but the legs below and body above it is fine.

Thanks @alan.james.kent! Sharing this info with the dev team.

Do you think this will be looked at soonish? If not likely, then I will keep exploring other options like converting to OBJ or FBX as intermediate formats. I just did not want to waste time if the dev team were looking at it “soon(ish)”. (I know you have the new release coming out soon, so my guess is its all hands on deck to get that shipped, then if someone did fix it, it might be a while before it was made available.)

I had a bit of a browse of the code, but I think the relevant code is in libs/omniverse_asset_converter.dll , so I cannot see how it was implemented (and could not fix it if I did find the issue). The same function seems to deal with all supported file formats, so there is not a separate code path in the Python code that I can see per file type. So not much I can do I think other than try a different file format, or try some different libraries to read GLTF and write USD etc.

Oh, for amusement, I tried creating an override layer and moving the Skeleton layer above the hips layer. I did not expect it to work, and I was appropriately rewarded.

Next I am trying GLB->USD via the apose site, but it generates a very flat USD file, but I am probably just wasting time now.

1 Like

I’m still working on an answer, but I’m thinking if it’s a bug you’ll need to wait for a fix so best to move forward with Blender->OV or an intermediate format.

Hey Alan. How are you planning on animating the characters? Do you have created with this skeleton that you want to import or were you thinking of retargeting animations created with another skeleton?

I was trying the retargeting approach. It was just looking strange, but you made the comment the skeleton was in the wrong place, which makes sense. It looked like during retargeting it was pulling the hips down to the ground.

Note: GLTF->Blender->OBJ->USD seems to lose the bones. With FBX I have tried different settings but the result does not load with textures in Omniverse. But skeleton was at right layer.

So next idea is a “fix it up” python script that restructures the USD file. Load USD, reorganize layers, write it back out. I can compare the FBX and GLTF versions to work out the correct structure I hope. The skeleton was a bit interesting - its a flat list of tags and layer names (I think). I will probably have a go in USDA by hand first with a text editor, and if I get it right then try to automate it with a Python script. Good learning exercise. (I keep telling myself that.)

I need to fix other things anyway, so probably want such a script regardless. E.g. restructure the mesh layers to what audio2Face wants. I have a long list of topics to still work out in Omniverse. How to blend animation clips? (I don’t think the Sequencer supports this at present - you can only put one after the other.) How to combine procedural animation (e.g. “look at target”) with animation clips (including blending between them). I think everything is technically feasible - but some of it looks like a lot of work (probably too much for a hobby project).

The other “fun” I will have is Unity (where all my existing animation clips are) convert animation clips to “humanoid” animations. Rather than retargeting from one character to another, they convert animation clips to “humanoid” (generic), then retarget that to bone names. Each character has an “avatar description” which maps the bone names to the bones for that character. So its kinda the same, but different enough to make bringing them across… interesting. (It looks technically possible, just more work.) Fun times!

I really should get a scene working with the retargeting approach. I have a library of animation clips from many different original characters collected over time. So I have to wrap my head around what to do in Omniverse. Maybe convert all animation clips to a standard character, then retarget that one character to everyone else. Retargeting many source character animations to many target characters feels inefficient.

But I also have to work out hair bounces, hair in wind, cloth simulation for clothes, texture tiling for different skins, facial expressions, … it’s big enough a job that the logical part of my brain tells me “give up now, you will never finish it”. So this is currently more a learning exercise.

Just an update, I made some progress with an extension to move the Skeleton node up a level. See https://gist.github.com/alankent/095a24321a03b1407e1ff79fc7b8b7fb

The code moves the Skeleton up a level, then adds “Root” to the bone names. But I think something needs to happen to the meshes as well so they stay connected to the bones.

I moved the Skeleton node up a level, inserted a new root bone at the head of the Skeleton joints array, added in “Root/” to all the joint prim paths, etc., but the meshes are no longer connected to the bones (if you rotate bones, the meshes no longer move with the bones). Sharing current version in case anyone had any advice. I assume I need to patch up some properties of Mesh nodes due to the skeleton changes? But its hard to work out as not sure which properties I need to recompute (and what they all mean).

Just to combine the threads, I got a workaround going by using USD to combine bones out of the blender export and materials out of the GLB conversion. Not ideal, but got a character to pose at least. VRoid Studio, meet NVIDIA Omniverse – Extra Ordinary, the Series

1 Like

Another follow-up question from the team. What was your experience bringing the characters in from Unity via the connector? Did that not resolve the “no root join” situation automatically?

Let me go through it carefully again to take detailed notes. I augment the characters extensively inside Unity, adding Unity extensions for cloth dynamics etc. I will try using the raw characters.

I tried bringing the VRoid Studio character across via the Omniverse Connector. Unfortunately the connector does not export the skeleton - only the meshes.

There is a Root node, but no Skeleton.

The textures don’t seem to support alpha either.

Ideally they should be more like:

image

Using GLB to USD converter, it does better, understands opacity - but trying with another character it has the same problem. The root is one layer too low in the stage hierarchy.

Oh, in case of any interest, I tried to create a avatar using “Ready Player Me”. I think it also generates VRM files (GLB for sure). But this one has no root bone, so would be a pain to animate.

Note there is something funny with the rendering in Omniverse. In Blender it looks good:

The same GLB file imported into OV has some texture problems on the face, hair, and hands.

I uploaded the GLB file if you want to look yourself ready-player-me-1.glb - Google Drive

Just wondering if any news? I tried with another Ready Player Me character - it preserved the textures better for this particular model, but dropped all the blendshapes during the import (needed by audio2face). I thought I would check if likely to be fixed any time soon.

Hi @alan.james.kent. I rechecked the UsdSkel hierarchy issue and this looks to be fixed in USD Composer 2023.2+. Here’s what it looks like for me now and Convert/Import:

Can you verify on your end?

For the Ready Player Me character, I think the issue is that the character has multiple textures with the name baseColor and on conversion, we’re writing that out to baseColor.png resulting in the textures files getting clobbered. The last output texture wins and all the materials that were referencing unique “baseColor” textures are now all reading from the same one. I’ve created an internal ticket for this: OM-114002.

Thanks for reporting!

2 Likes

Yes, the Root bone is now present (yay!).

Other outstanding issues with importing VRM (renamed to .glb) files, in case of interest:

  • Blend shapes are discarded - VRoid Studio characters don’t have ARKit blendshapes unfortunately, but ReadyPlayer.me characters do, which should then work with audio2face if preserved. (Having generated them myself in my own code, I don’t think it’s a big job. Blend shapes are not that complicated. No maths, just copying the data structures across.)
  • Some material improvements. I don’t understand materials very well, but trial an error I got visual improvements. This is using the gltf/pbr.mdl shaders.

Three.js to USDA code snippet to work out alpha mode more reliably.

  const alphaMode = material.transparent ? 2 : (material.alphaTest > 0) ? 1 : 0
  await os.println(`int inputs:alpha_mode = ${alphaMode} (`)

That change, plus adjusting “Emissive Strength” (from 10000 to 130) and Metalic “Roughness Factor” to more like 0.9 to fix hair sheen, I instead get: