diff --git a/Content/Documentation/ScriptingAPI-Documentation.md b/Content/Documentation/ScriptingAPI-Documentation.md index cfb9be605..2836e897c 100644 --- a/Content/Documentation/ScriptingAPI-Documentation.md +++ b/Content/Documentation/ScriptingAPI-Documentation.md @@ -1070,6 +1070,8 @@ Describes a Collider object. - GetWeight(int id) : float -- returns current weight of expression - SetPresetWeight(ExpressionPreset preset, float weight) -- Set a preset expression's weight. You can get access to preset values from ExpressionPreset table - GetPresetWeight(ExpressionPreset preset) : float -- returns current weight of preset expression +- SetForceTalkingEnabled(bool value) -- Force continuous talking animation, even if no voice is playing +- IsForceTalkingEnabled() : bool [outer] ExpressionPreset = { Happy = 0, diff --git a/Editor/ExpressionWindow.cpp b/Editor/ExpressionWindow.cpp index 4b3e49a3b..9534a0574 100644 --- a/Editor/ExpressionWindow.cpp +++ b/Editor/ExpressionWindow.cpp @@ -9,7 +9,7 @@ void ExpressionWindow::Create(EditorComponent* _editor) editor = _editor; wi::gui::Window::Create(ICON_EXPRESSION " Expression", wi::gui::Window::WindowControls::COLLAPSE | wi::gui::Window::WindowControls::CLOSE); - SetSize(XMFLOAT2(670, 550)); + SetSize(XMFLOAT2(670, 580)); closeButton.SetTooltip("Delete ExpressionComponent"); OnClose([=](wi::gui::EventArgs args) { @@ -36,6 +36,19 @@ void ExpressionWindow::Create(EditorComponent* _editor) infoLabel.SetText("Tip: If you also attach a Sound component to this entity, the mouth expression (if available) will be animated based on the sound playing."); AddWidget(&infoLabel); + talkCheckBox.Create("Force Talking: "); + talkCheckBox.SetTooltip("Force continuous talking animation, even if no voice is playing"); + talkCheckBox.SetSize(XMFLOAT2(hei, hei)); + talkCheckBox.OnClick([=](wi::gui::EventArgs args) { + wi::scene::Scene& scene = editor->GetCurrentScene(); + ExpressionComponent* expression_mastering = scene.expressions.GetComponent(entity); + if (expression_mastering == nullptr) + return; + + expression_mastering->SetForceTalkingEnabled(args.bValue); + }); + AddWidget(&talkCheckBox); + blinkFrequencySlider.Create(0, 1, 0, 1000, "Blinks: "); blinkFrequencySlider.SetTooltip("Specifies the number of blinks per second."); blinkFrequencySlider.SetSize(XMFLOAT2(wid, hei)); @@ -274,6 +287,7 @@ void ExpressionWindow::SetEntity(Entity entity) blinkCountSlider.SetValue(expression_mastering->blink_count); lookFrequencySlider.SetValue(expression_mastering->look_frequency); lookLengthSlider.SetValue(expression_mastering->look_length); + talkCheckBox.SetCheck(expression_mastering->IsForceTalkingEnabled()); expressionList.ClearItems(); for (const ExpressionComponent::Expression& expression : expression_mastering->expressions) @@ -321,6 +335,7 @@ void ExpressionWindow::ResizeLayout() }; add_fullwidth(infoLabel); + add_right(talkCheckBox); add(blinkFrequencySlider); add(blinkLengthSlider); add(blinkCountSlider); diff --git a/Editor/ExpressionWindow.h b/Editor/ExpressionWindow.h index 70a6ab2ee..ab5fd76ef 100644 --- a/Editor/ExpressionWindow.h +++ b/Editor/ExpressionWindow.h @@ -11,6 +11,7 @@ public: void SetEntity(wi::ecs::Entity entity); wi::gui::Label infoLabel; + wi::gui::CheckBox talkCheckBox; wi::gui::Slider blinkFrequencySlider; wi::gui::Slider blinkLengthSlider; wi::gui::Slider blinkCountSlider; diff --git a/WickedEngine/wiScene.cpp b/WickedEngine/wiScene.cpp index 54b5d3c39..4a44ad06f 100644 --- a/WickedEngine/wiScene.cpp +++ b/WickedEngine/wiScene.cpp @@ -29,6 +29,7 @@ namespace wi::scene void Scene::Update(float dt) { this->dt = dt; + time += dt; wi::jobsystem::context ctx; @@ -2261,44 +2262,104 @@ namespace wi::scene // Talking animation based on sound: const SoundComponent* sound = sounds.GetComponent(entity); - if(sound != nullptr && sound->soundResource.IsValid() && sound->IsPlaying()) + const bool voice_playing = sound != nullptr && sound->soundResource.IsValid() && sound->IsPlaying(); + if(voice_playing || expression_mastering.IsForceTalkingEnabled()) { - wi::audio::SampleInfo info = wi::audio::GetSampleInfo(&sound->soundResource.GetSound()); - uint32_t sample_frequency = info.sample_rate * info.channel_count; - uint64_t current_sample = wi::audio::GetTotalSamplesPlayed(&sound->soundinstance); - if (sound->IsLooped()) + ExpressionComponent::Preset unused_phonemes[4]; + int next = 0; + for (int phoneme = (int)ExpressionComponent::Preset::Aa; phoneme <= (int)ExpressionComponent::Preset::Oh; phoneme++) { - float total_time = float(current_sample) / float(info.sample_rate); - if (total_time > sound->soundinstance.loop_begin) + if (phoneme != (int)expression_mastering.talking_phoneme) // don't allow to select the current phoneme next { - float loop_length = sound->soundinstance.loop_length > 0 ? sound->soundinstance.loop_length : (float(info.sample_count) / float(sample_frequency)); - float loop_time = std::fmod(total_time - sound->soundinstance.loop_begin, loop_length); - current_sample = uint64_t(loop_time * info.sample_rate); + unused_phonemes[next++] = (ExpressionComponent::Preset)phoneme; + int mouth = expression_mastering.presets[(int)phoneme]; + ExpressionComponent::Expression& expression = expression_mastering.expressions[mouth]; + expression.weight = wi::math::Lerp(expression.weight, 0, 0.4f); // fade out unused + expression.SetDirty(); } } - current_sample *= info.channel_count; - current_sample = std::min(current_sample, info.sample_count); - - float voice = 0; - const int sample_count = 64; - for (int sam = 0; sam < sample_count; ++sam) - { - voice = std::max(voice, std::abs((float)info.samples[std::min(current_sample + sam, info.sample_count)] / 32768.0f)); - } - int mouth = expression_mastering.presets[(int)ExpressionComponent::Preset::Aa]; + int mouth = expression_mastering.presets[(int)expression_mastering.talking_phoneme]; ExpressionComponent::Expression& expression = expression_mastering.expressions[mouth]; - const float strength = 0.4f; - if (voice > 0.1f) + if (voice_playing) { - expression.weight = wi::math::Lerp(expression.weight, 1, strength); + // Take voice sample from audio: + wi::audio::SampleInfo info = wi::audio::GetSampleInfo(&sound->soundResource.GetSound()); + uint32_t sample_frequency = info.sample_rate * info.channel_count; + uint64_t current_sample = wi::audio::GetTotalSamplesPlayed(&sound->soundinstance); + if (sound->IsLooped()) + { + float total_time = float(current_sample) / float(info.sample_rate); + if (total_time > sound->soundinstance.loop_begin) + { + float loop_length = sound->soundinstance.loop_length > 0 ? sound->soundinstance.loop_length : (float(info.sample_count) / float(sample_frequency)); + float loop_time = std::fmod(total_time - sound->soundinstance.loop_begin, loop_length); + current_sample = uint64_t(loop_time * info.sample_rate); + } + } + current_sample *= info.channel_count; + current_sample = std::min(current_sample, info.sample_count); + + float voice = 0; + const int sample_count = 64; + for (int sam = 0; sam < sample_count; ++sam) + { + voice = std::max(voice, std::abs((float)info.samples[std::min(current_sample + sam, info.sample_count)] / 32768.0f)); + } + const float strength = 0.4f; + if (voice > 0.1f) + { + expression.weight = wi::math::Lerp(expression.weight, 1, strength); + } + else + { + expression.weight = wi::math::Lerp(expression.weight, 0, strength); + } } else { - expression.weight = wi::math::Lerp(expression.weight, 0, strength); + float wave = std::sin(time * 30) * 0.5f + 0.5f; + expression.weight = wave; } + + float prev_slope = expression_mastering.talking_weight_prev - expression_mastering.talking_weight_prev_prev; + float curr_slope = expression.weight - expression_mastering.talking_weight_prev; + expression_mastering.talking_weight_prev_prev = expression_mastering.talking_weight_prev; + expression_mastering.talking_weight_prev = expression.weight; + if (prev_slope < 0 && curr_slope > 0) + { + // New phoneme when voice slope valley is detected: + expression_mastering.talking_phoneme = unused_phonemes[wi::random::GetRandom(0, (int)arraysize(unused_phonemes) - 1)]; + } + expression.SetDirty(); } + else if (expression_mastering._flags & ExpressionComponent::TALKING_ENDED) + { + // When talking ended, smoothly blend out all phoneme expressions: + bool talking_active = false; + int phonemes[] = { + expression_mastering.presets[(int)ExpressionComponent::Preset::Aa], + expression_mastering.presets[(int)ExpressionComponent::Preset::Ih], + expression_mastering.presets[(int)ExpressionComponent::Preset::Ou], + expression_mastering.presets[(int)ExpressionComponent::Preset::Ee], + expression_mastering.presets[(int)ExpressionComponent::Preset::Oh], + }; + for (auto& phoneme : phonemes) + { + if (phoneme < 0) + continue; + auto& expression = expression_mastering.expressions[phoneme]; + expression.weight = wi::math::Lerp(expression.weight, 0, 0.4f); + expression.SetDirty(); + if (expression.weight > 0) + talking_active = true; + } + if (!talking_active) + { + expression_mastering._flags ^= ExpressionComponent::TALKING_ENDED; + } + } float overrideMouthBlend = 0; float overrideBlinkBlend = 0; @@ -2825,8 +2886,6 @@ namespace wi::scene // Springs: - static float time = 0; - time += dt; const XMVECTOR windDir = XMLoadFloat3(&weather.windDirection); if (springs.GetCount() > 0) diff --git a/WickedEngine/wiScene.h b/WickedEngine/wiScene.h index 99f232ff1..92b4e8e8f 100644 --- a/WickedEngine/wiScene.h +++ b/WickedEngine/wiScene.h @@ -63,6 +63,7 @@ namespace wi::scene }; uint32_t flags = EMPTY; + float time = 0; CameraComponent camera; // for LOD and 3D sound update std::shared_ptr physics_scene; wi::SpinLock locker; diff --git a/WickedEngine/wiScene_BindLua.cpp b/WickedEngine/wiScene_BindLua.cpp index 3585ccd97..a23152118 100644 --- a/WickedEngine/wiScene_BindLua.cpp +++ b/WickedEngine/wiScene_BindLua.cpp @@ -5605,6 +5605,8 @@ Luna::FunctionType ExpressionComponent_BindLua::met lunamethod(ExpressionComponent_BindLua, SetPresetWeight), lunamethod(ExpressionComponent_BindLua, GetWeight), lunamethod(ExpressionComponent_BindLua, GetPresetWeight), + lunamethod(ExpressionComponent_BindLua, SetForceTalkingEnabled), + lunamethod(ExpressionComponent_BindLua, IsForceTalkingEnabled), { NULL, NULL } }; Luna::PropertyType ExpressionComponent_BindLua::properties[] = { @@ -5720,6 +5722,24 @@ int ExpressionComponent_BindLua::GetPresetWeight(lua_State* L) } return 0; } +int ExpressionComponent_BindLua::SetForceTalkingEnabled(lua_State* L) +{ + int argc = wi::lua::SGetArgCount(L); + if (argc > 0) + { + component->SetForceTalkingEnabled(wi::lua::SGetBool(L, 1)); + } + else + { + wi::lua::SError(L, "SetForceTalkingEnabled(bool value) not enough arguments!"); + } + return 0; +} +int ExpressionComponent_BindLua::IsForceTalkingEnabled(lua_State* L) +{ + wi::lua::SSetBool(L, component->IsForceTalkingEnabled()); + return 1; +} diff --git a/WickedEngine/wiScene_BindLua.h b/WickedEngine/wiScene_BindLua.h index 48704ca26..62739e67f 100644 --- a/WickedEngine/wiScene_BindLua.h +++ b/WickedEngine/wiScene_BindLua.h @@ -1648,6 +1648,8 @@ namespace wi::lua::scene int SetPresetWeight(lua_State* L); int GetWeight(lua_State* L); int GetPresetWeight(lua_State* L); + int SetForceTalkingEnabled(lua_State* L); + int IsForceTalkingEnabled(lua_State* L); }; class HumanoidComponent_BindLua diff --git a/WickedEngine/wiScene_Components.h b/WickedEngine/wiScene_Components.h index 2300ad802..000939f09 100644 --- a/WickedEngine/wiScene_Components.h +++ b/WickedEngine/wiScene_Components.h @@ -1467,6 +1467,8 @@ namespace wi::scene enum FLAGS { EMPTY = 0, + FORCE_TALKING = 1 << 0, + TALKING_ENDED = 1 << 1, }; uint32_t _flags = EMPTY; @@ -1569,10 +1571,18 @@ namespace wi::scene }; wi::vector expressions; + constexpr bool IsForceTalkingEnabled() const { return _flags & FORCE_TALKING; } + + // Force continuous talking animation, even if no voice is playing + inline void SetForceTalkingEnabled(bool value = true) { if (value) { _flags |= FORCE_TALKING; } else { _flags &= ~FORCE_TALKING; _flags |= TALKING_ENDED; } } + // Non-serialized attributes: float blink_timer = 0; float look_timer = 0; float look_weights[4] = {}; + Preset talking_phoneme = Preset::Aa; + float talking_weight_prev_prev = 0; + float talking_weight_prev = 0; void Serialize(wi::Archive& archive, wi::ecs::EntitySerializer& seri); }; diff --git a/WickedEngine/wiVersion.cpp b/WickedEngine/wiVersion.cpp index 21358ac50..22d783d1c 100644 --- a/WickedEngine/wiVersion.cpp +++ b/WickedEngine/wiVersion.cpp @@ -9,7 +9,7 @@ namespace wi::version // minor features, major updates, breaking compatibility changes const int minor = 71; // minor bug fixes, alterations, refactors, updates - const int revision = 191; + const int revision = 192; const std::string version_string = std::to_string(major) + "." + std::to_string(minor) + "." + std::to_string(revision);