tommate Posted January 14 Posted January 14 Hello everyone! I am currently trying to expand my animation workflow between Unigine and Blender. I am using Character Creator 4 and iClone 8 for creating and animating characters, import them into Blender for some touch ups or changes in the model/animation and then exporting them to unigine. So far, this worked great! But I only did skeletal animations till now. I am currently trying to add shape key animations for face experssions. But it doesnt seem to work when importing them to Unigine. And I dont know if this is an issue on my side, an issue with Blender or with Unigine. I tried using fbx or gltf exports in case it was a limitation of fbx. Both basically have the same outcome: I get the skeletal animation but not the morph target animation. I do get the morph target sliders inside the mesh nodes. And they do work, but I still need the animation for them that I have in Blender. Is there a way to make this work? Thanks in advance! note: I added the Blend file with the mesh and animation from CC4/iC8. morph_target_test.blend
silent Posted January 14 Posted January 14 Hi Tom, You did everything correctly. However, at the moment there is no support for keyframe animations, including shape keys. Our team has been working internally for some time on a new animation system that will allow parsing and reading such data, as well as creating animations directly in the Editor. Development is still ongoing, but we expect that part of this functionality will be released with the 2.21 SDK update, which is planned for early spring. In version 2.20, blend shapes need to be animated manually via code. To do this, keyframes typically need to be exported from Blender first. I believe we have used this approach in some of our recent internal projects. I would need to check with my team first to see whether we can share the Blender export script and a few code snippets for you to review. Thank you! How to submit a good bug report --- FTP server for test scenes and user uploads: ftp://files.unigine.com user: upload password: 6xYkd6vLYWjpW6SN
tommate Posted January 15 Author Posted January 15 Ah I see, thats a bummer. But anyway, thank you for the help! I will keep an I out for the future updates.
silent Posted January 15 Posted January 15 Found script for keyframes export :) As for the code, the main concept you can get from the snippet here (from a different content): Spoiler #include "AppWorldLogic.h" #include <UnigineWorld.h> #include <UnigineGame.h> #include <UnigineJson.h> #include <UnigineObjects.h> using namespace Unigine; using namespace Unigine::Math; int AppWorldLogic::init() { // Get skinned mesh node skinned = checked_ptr_cast<ObjectMeshSkinned>(World::getNodeByName("CC_Base_Body")); if (!skinned) return 0; // Enable all morph targets for all surfaces and initialize weights: // target 0 = bind pose (base), others = 0 const int num_surfaces = skinned->getNumSurfaces(); for (int surface = 0; surface < num_surfaces; ++surface) { const int num_targets = skinned->getNumSurfaceTargets(surface); // Enable targets and set initial weights for (int t = 0; t < num_targets; ++t) { skinned->setSurfaceTargetEnabled(surface, t, true); skinned->setSurfaceTargetWeight(surface, t, (t == 0) ? 1.0f : 0.0f); } } // Load animation weights from JSON JsonPtr json = Json::create(); if (!json->load("speech/Garage_4.json")) return 0; num_frames = json->getChild("numFrames")->getInt(); auto nm = json->getChild("facsNames"); JsonPtr weights = json->getChild("weightMat"); // Map mesh target names -> indices in facsNames // (assumes targets are consistent across surfaces, using surface 0 as reference) const int ref_surface = 0; const int num_targets_ref = skinned->getNumSurfaceTargets(ref_surface); int key_indices[200]; const int max_keys = Math::min(num_targets_ref - 1, 200); for (int i = 1; i < num_targets_ref && (i - 1) < 200; ++i) { String name(skinned->getSurfaceTargetName(ref_surface, i)); if (name.size() > 0) name = String::substr(name, 0, name.size() - 1); key_indices[i - 1] = -1; for (int j = 0; j < nm->getNumChildren(); ++j) { JsonPtr key = nm->getChild(j); String s = key->getString(); if (String::equal(s, name)) { key_indices[i - 1] = j; break; } } } // Build per-frame weights table (for targets 1..N-1) keys.clear(); keys.reserve(weights->getNumChildren()); for (int i = 0; i < weights->getNumChildren(); ++i) { JsonPtr fr = weights->getChild(i); Vector<float> ws; ws.reserve(max_keys); for (int t = 1; t < num_targets_ref && (t - 1) < 200; ++t) { const int ind = key_indices[t - 1]; float w = 0.0f; if (ind >= 0) w = fr->getChild(ind)->getNumber(); ws.append(w); } keys.append(ws); } return 1; } int AppWorldLogic::update() { if (!skinned || num_frames <= 0 || keys.size() == 0) return 1; const int frame = Math::mod(Game::getTime() * 60.0f, num_frames); const Vector<float> &ws = keys.get(frame); const int num_surfaces = skinned->getNumSurfaces(); for (int surface = 0; surface < num_surfaces; ++surface) { const int num_targets = skinned->getNumSurfaceTargets(surface); float total_weight = 0.0f; // Apply weights to targets 1..N-1 (limited by ws size) const int count = Math::min(num_targets - 1, ws.size()); for (int t = 1; t <= count; ++t) { const float w = ws[t - 1]; total_weight += w; skinned->setSurfaceTargetEnabled(surface, t, true); skinned->setSurfaceTargetWeight(surface, t, w); } // Base target (0) gets the remainder skinned->setSurfaceTargetEnabled(surface, 0, true); skinned->setSurfaceTargetWeight(surface, 0, 1.0f - total_weight); } return 1; } Thank you! export_shape_key_anim.py How to submit a good bug report --- FTP server for test scenes and user uploads: ftp://files.unigine.com user: upload password: 6xYkd6vLYWjpW6SN
tommate Posted February 12 Author Posted February 12 Hey, sorry for leaving this so long on read. The script helped a lot, and I managed to do what I need. Thank you very much! 1
Recommended Posts