From 89fe15e9368354aa0d1af7056de260178fe27fb8 Mon Sep 17 00:00:00 2001 From: Petr Ohlidal Date: Wed, 12 Oct 2022 04:35:07 +0200 Subject: [PATCH] Character: added bone blend masks. Example for the stock character.mesh (disables all upper body animation when running): ``` begin_bone_blend_mask anim_name "Run" bone_weight "hand.R" 0 bone_weight "hand.L" 0 bone_weight "forearm.R" 0 bone_weight "forearm.L" 0 bone_weight "upper_arm.R" 0 bone_weight "upper_arm.L" 0 bone_weight "shoulder.R" 0 bone_weight "shoulder.L" 0 bone_weight "head" 0 bone_weight "neck" 0 bone_weight "chest" 0 bone_weight "spine" 0 end_bone_blend_mask ``` --- resources/skeleton/config/classic.character | 25 ++++++++++- source/main/gfx/GfxCharacter.cpp | 42 +++++++++++++++++-- source/main/gfx/GfxCharacter.h | 1 + .../main/gui/panels/GUI_CharacterPoseUtil.cpp | 3 +- .../CharacterFileFormat.cpp | 34 ++++++++++++++- .../CharacterFileFormat.h | 16 ++++++- 6 files changed, 113 insertions(+), 8 deletions(-) diff --git a/resources/skeleton/config/classic.character b/resources/skeleton/config/classic.character index 3ced034006..9442395925 100644 --- a/resources/skeleton/config/classic.character +++ b/resources/skeleton/config/classic.character @@ -1,5 +1,8 @@ ; The classic character (aka RORbot) -; NOTE each anim is evaluated separately, there is no either-or relation, + +; This file format lets you compose game animations from one or more +; skeletal animations exported from a 3D modelling tool. +; NOTE each game-anim is evaluated separately, there is no either-or relation, ; so you must set each anim's conditions to avoid conflicts. ; For explanation of individual parameters, @@ -14,6 +17,24 @@ mesh_name "character.mesh" character_name "Classic RORBot" +begin_bone_blend_mask +; This is a dummy example of how bone blend masks can be done. +; bone_weight + anim_name "Run" + bone_weight "hand.R" 1 + bone_weight "hand.L" 1 + bone_weight "forearm.R" 1 + bone_weight "forearm.L" 1 + bone_weight "upper_arm.R" 1 + bone_weight "upper_arm.L" 1 + bone_weight "shoulder.R" 1 + bone_weight "shoulder.L" 1 + bone_weight "head" 1 + bone_weight "neck" 1 + bone_weight "chest" 1 + bone_weight "spine" 1 +end_bone_blend_mask + begin_animation game_description "driving" for_situation SITUATION_DRIVING @@ -160,6 +181,7 @@ begin_animation except_situation SITUATION_CUSTOM_MODE_01 anim_name "Spot_swim" playback_time_ratio 10.0 + anim_continuous false end_animation begin_animation @@ -169,4 +191,5 @@ begin_animation except_situation SITUATION_DRIVING anim_name "Driving" playback_time_ratio 2.0 + end_animation diff --git a/source/main/gfx/GfxCharacter.cpp b/source/main/gfx/GfxCharacter.cpp index e529a234cf..8a507864a3 100644 --- a/source/main/gfx/GfxCharacter.cpp +++ b/source/main/gfx/GfxCharacter.cpp @@ -63,10 +63,18 @@ GfxCharacter::GfxCharacter(Character* character) MaterialPtr mat2 = mat1->clone("tracks/" + xc_instance_name); entity->setMaterialName("tracks/" + xc_instance_name); - // setup animations - if (character->getCharacterDocument()->animblend_cumulative) + // setup animation blend + switch (character->getCharacterDocument()->force_animblend) { - entity->getSkeleton()->setBlendMode(ANIMBLEND_CUMULATIVE); + case ForceAnimBlend::CUMULATIVE: entity->getSkeleton()->setBlendMode(ANIMBLEND_CUMULATIVE); break; + case ForceAnimBlend::AVERAGE: entity->getSkeleton()->setBlendMode(ANIMBLEND_AVERAGE); break; + default:; // Keep the preset defined in .skeleton file + } + + // setup bone blend masks + for (BoneBlendMaskDef& mask_def : character->getCharacterDocument()->bone_blend_masks) + { + this->SetupBoneBlendMask(mask_def); } // setup diagnostic UI @@ -76,6 +84,34 @@ GfxCharacter::GfxCharacter(Character* character) } } +void GfxCharacter::SetupBoneBlendMask(BoneBlendMaskDef const& mask_def) +{ + Entity* entity = static_cast(xc_scenenode->getAttachedObject(0)); + + AnimationState* mask_created = nullptr; + try + { + AnimationState* anim = entity->getAnimationState(mask_def.anim_name); + if (mask_def.bone_weights.size() > 0) + { + anim->createBlendMask(entity->getSkeleton()->getNumBones()); + mask_created = anim; + for (BoneBlendMaskWeightDef const& def : mask_def.bone_weights) + { + Bone* bone = entity->getSkeleton()->getBone(def.bone_name); + anim->setBlendMaskEntry(bone->getHandle(), def.bone_weight); + } + } + } + catch (Exception& eeh) + { + App::GetConsole()->putMessage(Console::CONSOLE_MSGTYPE_INFO, Console::CONSOLE_SYSTEM_ERROR, + fmt::format("error setting up bone blend mask for animation '{}', message:{}", mask_def.anim_name, eeh.getFullDescription())); + if (mask_created) + mask_created->destroyBlendMask(); + } +} + RoR::GfxCharacter::~GfxCharacter() { Entity* ent = static_cast(xc_scenenode->getAttachedObject(0)); diff --git a/source/main/gfx/GfxCharacter.h b/source/main/gfx/GfxCharacter.h index 13f95b96f4..34201807d8 100644 --- a/source/main/gfx/GfxCharacter.h +++ b/source/main/gfx/GfxCharacter.h @@ -46,6 +46,7 @@ struct GfxCharacter void EnableAnim(Ogre::AnimationState* anim_state, float time); void UpdateAnimations(float dt); void EvaluateAnimDef(CharacterAnimDef const& def, float dt); + void SetupBoneBlendMask(BoneBlendMaskDef const& mask_def); Ogre::SceneNode* xc_scenenode; CharacterSB xc_simbuf; diff --git a/source/main/gui/panels/GUI_CharacterPoseUtil.cpp b/source/main/gui/panels/GUI_CharacterPoseUtil.cpp index 960996c1d0..e298f1c8f2 100644 --- a/source/main/gui/panels/GUI_CharacterPoseUtil.cpp +++ b/source/main/gui/panels/GUI_CharacterPoseUtil.cpp @@ -122,7 +122,8 @@ void CharacterPoseUtil::DrawAnimControls(Ogre::AnimationState* anim_state) // anim name line ImVec4 color = (anim_state->getEnabled()) ? ImGui::GetStyle().Colors[ImGuiCol_Text] : ImGui::GetStyle().Colors[ImGuiCol_TextDisabled]; - ImGui::TextColored(color, "'%s' (%.2f sec)", anim_state->getAnimationName().c_str(), anim_state->getLength()); + const char* uses_boneblendmask_text = anim_state->getBlendMask() ? ", uses bone blend mask!" : ""; + ImGui::TextColored(color, "'%s' (%.2f sec%s)", anim_state->getAnimationName().c_str(), anim_state->getLength(), uses_boneblendmask_text); if (m_manual_pose_active) { ImGui::SameLine(); diff --git a/source/main/resources/character_fileformat/CharacterFileFormat.cpp b/source/main/resources/character_fileformat/CharacterFileFormat.cpp index b9c8de586c..ec8fe9201b 100644 --- a/source/main/resources/character_fileformat/CharacterFileFormat.cpp +++ b/source/main/resources/character_fileformat/CharacterFileFormat.cpp @@ -120,8 +120,10 @@ void CharacterParser::TokenizeCurrentLine() // retval true = continue processing (false = stop) void CharacterParser::ProcessCurrentLine() { - if (!m_ctx.in_anim) + if (!m_ctx.in_anim && !m_ctx.in_bone_blend_mask) { + // Root level + if (StartsWith(m_cur_line, "character_name")) { m_def->character_name = GetParam(1); @@ -138,9 +140,15 @@ void CharacterParser::ProcessCurrentLine() { m_ctx.in_anim = true; } + else if (StartsWith(m_cur_line, "begin_bone_blend_mask")) + { + m_ctx.in_bone_blend_mask = true; + } } - else + else if (m_ctx.in_anim) { + // In '[begin/end]_animation' block. + if (StartsWith(m_cur_line, "end_animation")) { m_ctx.anim.game_id = (int)m_def->anims.size(); @@ -209,6 +217,28 @@ void CharacterParser::ProcessCurrentLine() m_ctx.anim.source_percentual = Ogre::StringConverter::parseBool(GetParam(1)); } } + else if (m_ctx.in_bone_blend_mask) + { + // In '[begin/end]_bone_blend_mask' block. + + if (StartsWith(m_cur_line, "end_bone_blend_mask")) + { + m_def->bone_blend_masks.push_back(m_ctx.bone_blend_mask); + m_ctx.bone_blend_mask = BoneBlendMaskDef(); + m_ctx.in_bone_blend_mask = false; + } + else if (StartsWith(m_cur_line, "anim_name")) + { + m_ctx.bone_blend_mask.anim_name = GetParam(1); + } + else if (StartsWith(m_cur_line, "bone_weight")) + { + BoneBlendMaskWeightDef def; + def.bone_name = GetParam(1); + def.bone_weight = Ogre::StringConverter::parseReal(GetParam(2)); + m_ctx.bone_blend_mask.bone_weights.push_back(def); + } + } } diff --git a/source/main/resources/character_fileformat/CharacterFileFormat.h b/source/main/resources/character_fileformat/CharacterFileFormat.h index 27ecce37f6..5a0f452b79 100644 --- a/source/main/resources/character_fileformat/CharacterFileFormat.h +++ b/source/main/resources/character_fileformat/CharacterFileFormat.h @@ -33,6 +33,18 @@ namespace RoR { /// @addtogroup Character /// @{ +struct BoneBlendMaskWeightDef //!< See `Ogre::AnimationState::setBlendMaskEntry()` +{ + std::string bone_name; + float bone_weight = 0.f; +}; + +struct BoneBlendMaskDef //!< Additional settings for a skeletal animation track exported from 3D modelling tool. +{ + std::string anim_name; //!< Name of the skeletal animation from OGRE's *.skeleton file. + std::vector bone_weights; +}; + struct CharacterAnimDef { std::string anim_name; //!< Name of the skeletal animation from OGRE's *.skeleton file. @@ -71,7 +83,7 @@ struct CharacterDocument std::string character_name; std::string mesh_name; std::vector anims; - std::vector skeletal_anim_opts; + std::vector bone_blend_masks; ForceAnimBlend force_animblend = ForceAnimBlend::NONE; //!< Should a specific `Ogre::SkeletonAnimationBlendMode` be forced, or should we keep what the .skeleton file defines? CharacterAnimDef* getAnimById(int id) @@ -104,7 +116,9 @@ class CharacterParser struct CharacterParserContext { CharacterAnimDef anim; + BoneBlendMaskDef bone_blend_mask; bool in_anim = false; + bool in_bone_blend_mask = false; } m_ctx; //!< Parser context CharacterDocumentPtr m_def;