From ac4fc8540d304c2bc0d99cd3acb5adff1bd89a87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tur=C3=A1nszki=20J=C3=A1nos?= Date: Sun, 28 Aug 2022 19:22:23 +0200 Subject: [PATCH] Sparse morph target and VRM expressions (#536) --- Editor/CMakeLists.txt | 1 + Editor/CameraWindow.cpp | 4 +- Editor/ComponentsWindow.cpp | 20 +- Editor/ComponentsWindow.h | 2 + Editor/Editor.cpp | 3 + Editor/Editor_SOURCE.vcxitems | 2 + Editor/Editor_SOURCE.vcxitems.filters | 2 + Editor/ExpressionWindow.cpp | 331 +++++++++++++++++ Editor/ExpressionWindow.h | 29 ++ Editor/IconDefinitions.h | 1 + Editor/ModelImporter_GLTF.cpp | 511 +++++++++++++++++++++----- Editor/OptionsWindow.cpp | 13 + Editor/OptionsWindow.h | 1 + WickedEngine/wiGUI.cpp | 10 + WickedEngine/wiGUI.h | 2 + WickedEngine/wiRenderer.cpp | 2 +- WickedEngine/wiScene.cpp | 259 ++++++++++++- WickedEngine/wiScene.h | 114 +++++- WickedEngine/wiScene_Serializers.cpp | 91 +++++ WickedEngine/wiVersion.cpp | 2 +- credits.txt | 1 + features.txt | 15 +- 22 files changed, 1306 insertions(+), 110 deletions(-) create mode 100644 Editor/ExpressionWindow.cpp create mode 100644 Editor/ExpressionWindow.h diff --git a/Editor/CMakeLists.txt b/Editor/CMakeLists.txt index d048851d7..ee3d51f96 100644 --- a/Editor/CMakeLists.txt +++ b/Editor/CMakeLists.txt @@ -34,6 +34,7 @@ set (SOURCE_FILES SoftBodyWindow.cpp ColliderWindow.cpp HierarchyWindow.cpp + ExpressionWindow.cpp OptionsWindow.cpp ComponentsWindow.cpp TerrainGenerator.cpp diff --git a/Editor/CameraWindow.cpp b/Editor/CameraWindow.cpp index 96b15a9db..fea03d046 100644 --- a/Editor/CameraWindow.cpp +++ b/Editor/CameraWindow.cpp @@ -361,6 +361,7 @@ void CameraWindow::ResizeLayout() y += padding; }; + add_fullwidth(resetButton); add(farPlaneSlider); add(nearPlaneSlider); add(fovSlider); @@ -371,12 +372,11 @@ void CameraWindow::ResizeLayout() add(movespeedSlider); add(rotationspeedSlider); add(accelerationSlider); - add(resetButton); add_right(fpsCheckBox); y += 20; - add(proxyButton); + add_fullwidth(proxyButton); add_right(followCheckBox); add(followSlider); diff --git a/Editor/ComponentsWindow.cpp b/Editor/ComponentsWindow.cpp index 4f71ace64..a91ca318b 100644 --- a/Editor/ComponentsWindow.cpp +++ b/Editor/ComponentsWindow.cpp @@ -38,6 +38,7 @@ void ComponentsWindow::Create(EditorComponent* _editor) colliderWnd.Create(editor); hierarchyWnd.Create(editor); cameraComponentWnd.Create(editor); + expressionWnd.Create(editor); newComponentCombo.Create("Add: "); @@ -46,12 +47,12 @@ void ComponentsWindow::Create(EditorComponent* _editor) newComponentCombo.selected_font.anim.typewriter.character_start = 1; newComponentCombo.SetTooltip("Add a component to the last selected entity."); newComponentCombo.SetInvalidSelectionText("..."); - newComponentCombo.AddItem("Name", 0); + newComponentCombo.AddItem("Name " ICON_NAME, 0); newComponentCombo.AddItem("Layer " ICON_LAYER, 1); newComponentCombo.AddItem("Hierarchy " ICON_HIERARCHY, 19); newComponentCombo.AddItem("Transform " ICON_TRANSFORM, 2); newComponentCombo.AddItem("Light " ICON_POINTLIGHT, 3); - newComponentCombo.AddItem("Matetial " ICON_MATERIAL, 4); + newComponentCombo.AddItem("Material " ICON_MATERIAL, 4); newComponentCombo.AddItem("Spring", 5); newComponentCombo.AddItem("Inverse Kinematics " ICON_IK, 6); newComponentCombo.AddItem("Sound " ICON_SOUND, 7); @@ -297,6 +298,7 @@ void ComponentsWindow::Create(EditorComponent* _editor) AddWidget(&colliderWnd); AddWidget(&hierarchyWnd); AddWidget(&cameraComponentWnd); + AddWidget(&expressionWnd); materialWnd.SetVisible(false); weatherWnd.SetVisible(false); @@ -321,6 +323,7 @@ void ComponentsWindow::Create(EditorComponent* _editor) colliderWnd.SetVisible(false); hierarchyWnd.SetVisible(false); cameraComponentWnd.SetVisible(false); + expressionWnd.SetVisible(false); SetSize(editor->optionsWnd.GetSize()); @@ -663,4 +666,17 @@ void ComponentsWindow::ResizeLayout() { scriptWnd.SetVisible(false); } + + if (scene.expressions.Contains(expressionWnd.entity)) + { + expressionWnd.SetVisible(true); + expressionWnd.SetPos(pos); + expressionWnd.SetSize(XMFLOAT2(width, expressionWnd.GetScale().y)); + pos.y += expressionWnd.GetSize().y; + pos.y += padding; + } + else + { + expressionWnd.SetVisible(false); + } } diff --git a/Editor/ComponentsWindow.h b/Editor/ComponentsWindow.h index f840dfa67..3d399d694 100644 --- a/Editor/ComponentsWindow.h +++ b/Editor/ComponentsWindow.h @@ -23,6 +23,7 @@ #include "ColliderWindow.h" #include "HierarchyWindow.h" #include "CameraComponentWindow.h" +#include "ExpressionWindow.h" class EditorComponent; @@ -59,4 +60,5 @@ public: ColliderWindow colliderWnd; HierarchyWindow hierarchyWnd; CameraComponentWindow cameraComponentWnd; + ExpressionWindow expressionWnd; }; diff --git a/Editor/Editor.cpp b/Editor/Editor.cpp index 421e68dbb..7c9db43dd 100644 --- a/Editor/Editor.cpp +++ b/Editor/Editor.cpp @@ -347,6 +347,7 @@ void EditorComponent::Load() componentsWnd.colliderWnd.SetEntity(INVALID_ENTITY); componentsWnd.hierarchyWnd.SetEntity(INVALID_ENTITY); componentsWnd.cameraComponentWnd.SetEntity(INVALID_ENTITY); + componentsWnd.expressionWnd.SetEntity(INVALID_ENTITY); optionsWnd.RefreshEntityTree(); ResetHistory(); @@ -1301,6 +1302,7 @@ void EditorComponent::Update(float dt) componentsWnd.colliderWnd.SetEntity(INVALID_ENTITY); componentsWnd.hierarchyWnd.SetEntity(INVALID_ENTITY); componentsWnd.cameraComponentWnd.SetEntity(INVALID_ENTITY); + componentsWnd.expressionWnd.SetEntity(INVALID_ENTITY); } else { @@ -1338,6 +1340,7 @@ void EditorComponent::Update(float dt) componentsWnd.colliderWnd.SetEntity(picked.entity); componentsWnd.hierarchyWnd.SetEntity(picked.entity); componentsWnd.cameraComponentWnd.SetEntity(picked.entity); + componentsWnd.expressionWnd.SetEntity(picked.entity); if (picked.subsetIndex >= 0) { diff --git a/Editor/Editor_SOURCE.vcxitems b/Editor/Editor_SOURCE.vcxitems index 5a272a35d..304630c49 100644 --- a/Editor/Editor_SOURCE.vcxitems +++ b/Editor/Editor_SOURCE.vcxitems @@ -23,6 +23,7 @@ + @@ -139,6 +140,7 @@ + diff --git a/Editor/Editor_SOURCE.vcxitems.filters b/Editor/Editor_SOURCE.vcxitems.filters index 5bad7a874..cde3855c3 100644 --- a/Editor/Editor_SOURCE.vcxitems.filters +++ b/Editor/Editor_SOURCE.vcxitems.filters @@ -82,6 +82,7 @@ + @@ -128,6 +129,7 @@ + diff --git a/Editor/ExpressionWindow.cpp b/Editor/ExpressionWindow.cpp new file mode 100644 index 000000000..8a1bc895d --- /dev/null +++ b/Editor/ExpressionWindow.cpp @@ -0,0 +1,331 @@ +#include "stdafx.h" +#include "ExpressionWindow.h" +#include "Editor.h" + +using namespace wi::ecs; +using namespace wi::scene; + +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, 500)); + + closeButton.SetTooltip("Delete ExpressionComponent"); + OnClose([=](wi::gui::EventArgs args) { + + wi::Archive& archive = editor->AdvanceHistory(); + archive << EditorComponent::HISTORYOP_COMPONENT_DATA; + editor->RecordEntity(archive, entity); + + editor->GetCurrentScene().expressions.Remove(entity); + + editor->RecordEntity(archive, entity); + + editor->optionsWnd.RefreshEntityTree(); + }); + + float x = 60; + float y = 4; + float hei = 20; + float step = hei + 2; + float wid = 220; + + blinkFrequencySlider.Create(0, 1, 0, 1000, "Blinks: "); + blinkFrequencySlider.SetTooltip("Specifies the number of blinks per second."); + blinkFrequencySlider.SetSize(XMFLOAT2(wid, hei)); + blinkFrequencySlider.OnSlide([=](wi::gui::EventArgs args) { + wi::scene::Scene& scene = editor->GetCurrentScene(); + ExpressionComponent* expression_mastering = scene.expressions.GetComponent(entity); + if (expression_mastering == nullptr) + return; + expression_mastering->blink_frequency = args.fValue; + }); + AddWidget(&blinkFrequencySlider); + + blinkLengthSlider.Create(0, 1, 0, 1000, "Blink Length: "); + blinkLengthSlider.SetSize(XMFLOAT2(wid, hei)); + blinkLengthSlider.OnSlide([=](wi::gui::EventArgs args) { + wi::scene::Scene& scene = editor->GetCurrentScene(); + ExpressionComponent* expression_mastering = scene.expressions.GetComponent(entity); + if (expression_mastering == nullptr) + return; + expression_mastering->blink_length = args.fValue; + }); + AddWidget(&blinkLengthSlider); + + blinkCountSlider.Create(1, 4, 2, 3, "Blink Count: "); + blinkCountSlider.SetSize(XMFLOAT2(wid, hei)); + blinkCountSlider.OnSlide([=](wi::gui::EventArgs args) { + wi::scene::Scene& scene = editor->GetCurrentScene(); + ExpressionComponent* expression_mastering = scene.expressions.GetComponent(entity); + if (expression_mastering == nullptr) + return; + expression_mastering->blink_count = args.iValue; + }); + AddWidget(&blinkCountSlider); + + lookFrequencySlider.Create(0, 1, 0, 1000, "Looks: "); + lookFrequencySlider.SetTooltip("Specifies the number of look-aways per second."); + lookFrequencySlider.SetSize(XMFLOAT2(wid, hei)); + lookFrequencySlider.OnSlide([=](wi::gui::EventArgs args) { + wi::scene::Scene& scene = editor->GetCurrentScene(); + ExpressionComponent* expression_mastering = scene.expressions.GetComponent(entity); + if (expression_mastering == nullptr) + return; + expression_mastering->look_frequency = args.fValue; + }); + AddWidget(&lookFrequencySlider); + + lookLengthSlider.Create(0, 1, 0, 1000, "Look Length: "); + lookLengthSlider.SetSize(XMFLOAT2(wid, hei)); + lookLengthSlider.OnSlide([=](wi::gui::EventArgs args) { + wi::scene::Scene& scene = editor->GetCurrentScene(); + ExpressionComponent* expression_mastering = scene.expressions.GetComponent(entity); + if (expression_mastering == nullptr) + return; + expression_mastering->look_length = args.fValue; + }); + AddWidget(&lookLengthSlider); + + expressionList.Create("Expressions: "); + expressionList.SetSize(XMFLOAT2(wid, 200)); + expressionList.SetPos(XMFLOAT2(4, y += step)); + expressionList.OnSelect([=](wi::gui::EventArgs args) { + wi::scene::Scene& scene = editor->GetCurrentScene(); + ExpressionComponent* expression_mastering = scene.expressions.GetComponent(entity); + if (expression_mastering == nullptr) + return; + if (args.iValue >= expression_mastering->expressions.size()) + return; + + const ExpressionComponent::Expression& expression = expression_mastering->expressions[args.iValue]; + binaryCheckBox.SetCheck(expression.IsBinary()); + weightSlider.SetValue(expression.weight); + overrideMouthCombo.SetSelectedByUserdataWithoutCallback((uint64_t)expression.override_mouth); + overrideBlinkCombo.SetSelectedByUserdataWithoutCallback((uint64_t)expression.override_blink); + overrideLookCombo.SetSelectedByUserdataWithoutCallback((uint64_t)expression.override_look); + }); + AddWidget(&expressionList); + + binaryCheckBox.Create("Binary: "); + binaryCheckBox.SetSize(XMFLOAT2(hei, hei)); + binaryCheckBox.OnClick([=](wi::gui::EventArgs args) { + wi::scene::Scene& scene = editor->GetCurrentScene(); + ExpressionComponent* expression_mastering = scene.expressions.GetComponent(entity); + if (expression_mastering == nullptr) + return; + + int count = expressionList.GetItemCount(); + for (int i = 0; i < count; ++i) + { + if (!expressionList.GetItem(i).selected) + continue; + if (i >= expression_mastering->expressions.size()) + return; + + ExpressionComponent::Expression& expression = expression_mastering->expressions[i]; + expression.SetBinary(args.bValue); + expression.SetDirty(); + } + }); + AddWidget(&binaryCheckBox); + + weightSlider.Create(0, 1, 0, 100000, "Weight: "); + weightSlider.SetSize(XMFLOAT2(wid, hei)); + weightSlider.SetPos(XMFLOAT2(x, y += step)); + weightSlider.OnSlide([&](wi::gui::EventArgs args) { + wi::scene::Scene& scene = editor->GetCurrentScene(); + ExpressionComponent* expression_mastering = scene.expressions.GetComponent(entity); + if (expression_mastering == nullptr) + return; + if (args.iValue >= expression_mastering->expressions.size()) + return; + + int count = expressionList.GetItemCount(); + for (int i = 0; i < count; ++i) + { + if (!expressionList.GetItem(i).selected) + continue; + if (i >= expression_mastering->expressions.size()) + return; + + ExpressionComponent::Expression& expression = expression_mastering->expressions[i]; + expression.weight = args.fValue; + expression.SetDirty(); + } + }); + AddWidget(&weightSlider); + + overrideMouthCombo.Create("Override Mouth: "); + overrideMouthCombo.SetTooltip("Lip sync override control"); + overrideMouthCombo.SetSize(XMFLOAT2(wid, hei)); + overrideMouthCombo.AddItem("None", (uint64_t)ExpressionComponent::Override::None); + overrideMouthCombo.AddItem("Block", (uint64_t)ExpressionComponent::Override::Block); + overrideMouthCombo.AddItem("Blend", (uint64_t)ExpressionComponent::Override::Blend); + overrideMouthCombo.OnSelect([=](wi::gui::EventArgs args) { + wi::scene::Scene& scene = editor->GetCurrentScene(); + ExpressionComponent* expression_mastering = scene.expressions.GetComponent(entity); + if (expression_mastering == nullptr) + return; + if (args.iValue >= expression_mastering->expressions.size()) + return; + + int count = expressionList.GetItemCount(); + for (int i = 0; i < count; ++i) + { + if (!expressionList.GetItem(i).selected) + continue; + if (i >= expression_mastering->expressions.size()) + return; + + ExpressionComponent::Expression& expression = expression_mastering->expressions[i]; + expression.override_mouth = (ExpressionComponent::Override)args.userdata; + expression.SetDirty(); + } + }); + AddWidget(&overrideMouthCombo); + + overrideBlinkCombo.Create("Override Blink: "); + overrideBlinkCombo.SetTooltip("Blink override control"); + overrideBlinkCombo.SetSize(XMFLOAT2(wid, hei)); + overrideBlinkCombo.AddItem("None", (uint64_t)ExpressionComponent::Override::None); + overrideBlinkCombo.AddItem("Block", (uint64_t)ExpressionComponent::Override::Block); + overrideBlinkCombo.AddItem("Blend", (uint64_t)ExpressionComponent::Override::Blend); + overrideBlinkCombo.OnSelect([=](wi::gui::EventArgs args) { + wi::scene::Scene& scene = editor->GetCurrentScene(); + ExpressionComponent* expression_mastering = scene.expressions.GetComponent(entity); + if (expression_mastering == nullptr) + return; + if (args.iValue >= expression_mastering->expressions.size()) + return; + + int count = expressionList.GetItemCount(); + for (int i = 0; i < count; ++i) + { + if (!expressionList.GetItem(i).selected) + continue; + if (i >= expression_mastering->expressions.size()) + return; + + ExpressionComponent::Expression& expression = expression_mastering->expressions[i]; + expression.override_blink = (ExpressionComponent::Override)args.userdata; + expression.SetDirty(); + } + }); + AddWidget(&overrideBlinkCombo); + + overrideLookCombo.Create("Override Look: "); + overrideLookCombo.SetTooltip("Look-away override control"); + overrideLookCombo.SetSize(XMFLOAT2(wid, hei)); + overrideLookCombo.AddItem("None", (uint64_t)ExpressionComponent::Override::None); + overrideLookCombo.AddItem("Block", (uint64_t)ExpressionComponent::Override::Block); + overrideLookCombo.AddItem("Blend", (uint64_t)ExpressionComponent::Override::Blend); + overrideLookCombo.OnSelect([=](wi::gui::EventArgs args) { + wi::scene::Scene& scene = editor->GetCurrentScene(); + ExpressionComponent* expression_mastering = scene.expressions.GetComponent(entity); + if (expression_mastering == nullptr) + return; + if (args.iValue >= expression_mastering->expressions.size()) + return; + + int count = expressionList.GetItemCount(); + for (int i = 0; i < count; ++i) + { + if (!expressionList.GetItem(i).selected) + continue; + if (i >= expression_mastering->expressions.size()) + return; + + ExpressionComponent::Expression& expression = expression_mastering->expressions[i]; + expression.override_look = (ExpressionComponent::Override)args.userdata; + expression.SetDirty(); + } + }); + AddWidget(&overrideLookCombo); + + + SetMinimized(true); + SetVisible(false); + + SetEntity(INVALID_ENTITY); +} + +void ExpressionWindow::SetEntity(Entity entity) +{ + if (this->entity == entity) + return; + + this->entity = entity; + + Scene& scene = editor->GetCurrentScene(); + + const ExpressionComponent* expression_mastering = scene.expressions.GetComponent(entity); + + if (expression_mastering != nullptr) + { + blinkFrequencySlider.SetValue(expression_mastering->blink_frequency); + blinkLengthSlider.SetValue(expression_mastering->blink_length); + blinkCountSlider.SetValue(expression_mastering->blink_count); + lookFrequencySlider.SetValue(expression_mastering->look_frequency); + lookLengthSlider.SetValue(expression_mastering->look_length); + + expressionList.ClearItems(); + for (const ExpressionComponent::Expression& expression : expression_mastering->expressions) + { + expressionList.AddItem(expression.name); + } + } +} + +void ExpressionWindow::ResizeLayout() +{ + wi::gui::Window::ResizeLayout(); + const float padding = 4; + const float width = GetWidgetAreaSize().x; + float y = padding; + float jump = 20; + + const float margin_left = 110; + const float margin_right = 45; + + auto add = [&](wi::gui::Widget& widget) { + if (!widget.IsVisible()) + return; + widget.SetPos(XMFLOAT2(margin_left, y)); + widget.SetSize(XMFLOAT2(width - margin_left - margin_right, widget.GetScale().y)); + y += widget.GetSize().y; + y += padding; + }; + auto add_right = [&](wi::gui::Widget& widget) { + if (!widget.IsVisible()) + return; + widget.SetPos(XMFLOAT2(width - margin_right - widget.GetSize().x, y)); + y += widget.GetSize().y; + y += padding; + }; + auto add_fullwidth = [&](wi::gui::Widget& widget) { + if (!widget.IsVisible()) + return; + const float margin_left = padding; + const float margin_right = padding; + widget.SetPos(XMFLOAT2(margin_left, y)); + widget.SetSize(XMFLOAT2(width - margin_left - margin_right, widget.GetScale().y)); + y += widget.GetSize().y; + y += padding; + }; + + add(blinkFrequencySlider); + add(blinkLengthSlider); + add(blinkCountSlider); + add(lookFrequencySlider); + add(lookLengthSlider); + add_fullwidth(expressionList); + add_right(binaryCheckBox); + add(weightSlider); + add(overrideMouthCombo); + add(overrideBlinkCombo); + add(overrideLookCombo); + +} diff --git a/Editor/ExpressionWindow.h b/Editor/ExpressionWindow.h new file mode 100644 index 000000000..aa21345e1 --- /dev/null +++ b/Editor/ExpressionWindow.h @@ -0,0 +1,29 @@ +#pragma once +#include "WickedEngine.h" + +class EditorComponent; + +class ExpressionWindow : public wi::gui::Window +{ +public: + void Create(EditorComponent* editor); + + EditorComponent* editor = nullptr; + wi::ecs::Entity entity = wi::ecs::INVALID_ENTITY; + void SetEntity(wi::ecs::Entity entity); + + wi::gui::Slider blinkFrequencySlider; + wi::gui::Slider blinkLengthSlider; + wi::gui::Slider blinkCountSlider; + wi::gui::Slider lookFrequencySlider; + wi::gui::Slider lookLengthSlider; + wi::gui::TreeList expressionList; + wi::gui::Slider weightSlider; + wi::gui::CheckBox binaryCheckBox; + wi::gui::ComboBox overrideMouthCombo; + wi::gui::ComboBox overrideBlinkCombo; + wi::gui::ComboBox overrideLookCombo; + + void ResizeLayout() override; +}; + diff --git a/Editor/IconDefinitions.h b/Editor/IconDefinitions.h index 57ae45eb7..c98db66bd 100644 --- a/Editor/IconDefinitions.h +++ b/Editor/IconDefinitions.h @@ -29,6 +29,7 @@ #define ICON_COLLIDER ICON_FA_CAPSULES #define ICON_SCRIPT ICON_FA_SCROLL #define ICON_HIERARCHY ICON_FA_ARROWS_DOWN_TO_PEOPLE +#define ICON_EXPRESSION ICON_FA_MASKS_THEATER #define ICON_TERRAIN ICON_FA_MOUNTAIN_SUN diff --git a/Editor/ModelImporter_GLTF.cpp b/Editor/ModelImporter_GLTF.cpp index f10dc7ba6..c9e6779b8 100644 --- a/Editor/ModelImporter_GLTF.cpp +++ b/Editor/ModelImporter_GLTF.cpp @@ -140,6 +140,7 @@ namespace tinygltf struct LoaderState { + std::string name; tinygltf::Model gltfModel; Scene* scene; wi::unordered_map entityMap; // node -> entity @@ -321,6 +322,7 @@ void ImportModel_GLTF(const std::string& fileName, Scene& scene) state.rootEntity = CreateEntity(); scene.transforms.Create(state.rootEntity); scene.names.Create(state.rootEntity) = name; + state.name = name; // Create materials: for (auto& x : state.gltfModel.materials) @@ -775,12 +777,6 @@ void ImportModel_GLTF(const std::string& fileName, Scene& scene) scene.Component_Attach(meshEntity, state.rootEntity); MeshComponent& mesh = *scene.meshes.GetComponent(meshEntity); - mesh.morph_targets.resize(x.weights.size()); - for (size_t i = 0; i < mesh.morph_targets.size(); i++) - { - mesh.morph_targets[i].weight = static_cast(x.weights[i]); - } - for (auto& prim : x.primitives) { assert(prim.indices >= 0); @@ -1216,109 +1212,143 @@ void ImportModel_GLTF(const std::string& fileName, Scene& scene) } } } + } - for (size_t i = 0; i < mesh.morph_targets.size(); i++) + + mesh.morph_targets.resize(prim.targets.size()); + for (size_t i = 0; i < prim.targets.size(); i++) + { + MeshComponent::MorphTarget& morph_target = mesh.morph_targets[i]; + for (auto& attr : prim.targets[i]) { - for (auto& attr : prim.targets[i]) + const std::string& attr_name = attr.first; + int attr_data = attr.second; + + const tinygltf::Accessor& accessor = state.gltfModel.accessors[attr_data]; + + if (!attr_name.compare("POSITION")) { - const std::string& attr_name = attr.first; - int attr_data = attr.second; - - const tinygltf::Accessor& accessor = state.gltfModel.accessors[attr_data]; - const tinygltf::BufferView& bufferView = state.gltfModel.bufferViews[accessor.bufferView]; - const tinygltf::Buffer& buffer = state.gltfModel.buffers[bufferView.buffer]; - - int stride = accessor.ByteStride(bufferView); - size_t vertexCount = accessor.count; - - const unsigned char* data = buffer.data.data() + accessor.byteOffset + bufferView.byteOffset; - - if (!attr_name.compare("POSITION")) + if (accessor.sparse.isSparse) { - mesh.morph_targets[i].vertex_positions.resize(vertexOffset + vertexCount); - for (size_t j = 0; j < vertexCount; ++j) - { - mesh.morph_targets[i].vertex_positions[vertexOffset + j] = ((XMFLOAT3*)data)[j]; - } + auto& sparse = accessor.sparse; + const tinygltf::BufferView& sparse_indices_view = state.gltfModel.bufferViews[sparse.indices.bufferView]; + const tinygltf::BufferView& sparse_values_view = state.gltfModel.bufferViews[sparse.values.bufferView]; + const tinygltf::Buffer& sparse_indices_buffer = state.gltfModel.buffers[sparse_indices_view.buffer]; + const tinygltf::Buffer& sparse_values_buffer = state.gltfModel.buffers[sparse_values_view.buffer]; + const uint8_t* sparse_indices_data = sparse_indices_buffer.data.data() + sparse.indices.byteOffset + sparse_indices_view.byteOffset; + const uint8_t* sparse_values_data = sparse_values_buffer.data.data() + sparse.values.byteOffset + sparse_values_view.byteOffset; + const size_t sparseOffset = morph_target.sparse_indices.size(); + morph_target.vertex_positions.resize(sparseOffset + sparse.count); + morph_target.sparse_indices.resize(sparseOffset + sparse.count); - if (accessor.sparse.isSparse) + switch (sparse.indices.componentType) { - auto& sparse = accessor.sparse; - const tinygltf::BufferView& sparse_indices_view = state.gltfModel.bufferViews[sparse.indices.bufferView]; - const tinygltf::BufferView& sparse_values_view = state.gltfModel.bufferViews[sparse.values.bufferView]; - const tinygltf::Buffer& sparse_indices_buffer = state.gltfModel.buffers[sparse_indices_view.buffer]; - const tinygltf::Buffer& sparse_values_buffer = state.gltfModel.buffers[sparse_values_view.buffer]; - const uint8_t* sparse_indices_data = sparse_indices_buffer.data.data() + sparse.indices.byteOffset + sparse_indices_view.byteOffset; - const uint8_t* sparse_values_data = sparse_values_buffer.data.data() + sparse.values.byteOffset + sparse_values_view.byteOffset; - switch (sparse.indices.componentType) + default: + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE: + for (int s = 0; s < sparse.count; ++s) { - default: - case TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE: - for (int s = 0; s < sparse.count; ++s) - { - mesh.morph_targets[i].vertex_positions[sparse_indices_data[s]] = ((const XMFLOAT3*)sparse_values_data)[s]; - } - break; - case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT: - for (int s = 0; s < sparse.count; ++s) - { - mesh.morph_targets[i].vertex_positions[((const uint16_t*)sparse_indices_data)[s]] = ((const XMFLOAT3*)sparse_values_data)[s]; - } - break; - case TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT: - for (int s = 0; s < sparse.count; ++s) - { - mesh.morph_targets[i].vertex_positions[((const uint32_t*)sparse_indices_data)[s]] = ((const XMFLOAT3*)sparse_values_data)[s]; - } - break; + morph_target.sparse_indices[sparseOffset + s] = vertexOffset + sparse_indices_data[s]; + morph_target.vertex_positions[sparseOffset + s] = ((const XMFLOAT3*)sparse_values_data)[s]; } + break; + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT: + for (int s = 0; s < sparse.count; ++s) + { + morph_target.sparse_indices[sparseOffset + s] = vertexOffset + ((const uint16_t*)sparse_indices_data)[s]; + morph_target.vertex_positions[sparseOffset + s] = ((const XMFLOAT3*)sparse_values_data)[s]; + } + break; + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT: + for (int s = 0; s < sparse.count; ++s) + { + morph_target.sparse_indices[sparseOffset + s] = vertexOffset + ((const uint32_t*)sparse_indices_data)[s]; + morph_target.vertex_positions[sparseOffset + s] = ((const XMFLOAT3*)sparse_values_data)[s]; + } + break; } } - else if (!attr_name.compare("NORMAL")) + else { - mesh.morph_targets[i].vertex_normals.resize(vertexOffset + vertexCount); + const tinygltf::BufferView& bufferView = state.gltfModel.bufferViews[accessor.bufferView]; + const tinygltf::Buffer& buffer = state.gltfModel.buffers[bufferView.buffer]; + + int stride = accessor.ByteStride(bufferView); + size_t vertexCount = accessor.count; + + const unsigned char* data = buffer.data.data() + accessor.byteOffset + bufferView.byteOffset; + + morph_target.vertex_positions.resize(vertexOffset + vertexCount); for (size_t j = 0; j < vertexCount; ++j) { - mesh.morph_targets[i].vertex_normals[vertexOffset + j] = ((XMFLOAT3*)data)[j]; + morph_target.vertex_positions[vertexOffset + j] = ((XMFLOAT3*)data)[j]; } + } + } + else if (!attr_name.compare("NORMAL")) + { + if (accessor.sparse.isSparse) + { + auto& sparse = accessor.sparse; + const tinygltf::BufferView& sparse_indices_view = state.gltfModel.bufferViews[sparse.indices.bufferView]; + const tinygltf::BufferView& sparse_values_view = state.gltfModel.bufferViews[sparse.values.bufferView]; + const tinygltf::Buffer& sparse_indices_buffer = state.gltfModel.buffers[sparse_indices_view.buffer]; + const tinygltf::Buffer& sparse_values_buffer = state.gltfModel.buffers[sparse_values_view.buffer]; + const uint8_t* sparse_indices_data = sparse_indices_buffer.data.data() + sparse.indices.byteOffset + sparse_indices_view.byteOffset; + const uint8_t* sparse_values_data = sparse_values_buffer.data.data() + sparse.values.byteOffset + sparse_values_view.byteOffset; + const size_t sparseOffset = morph_target.sparse_indices.size(); + morph_target.vertex_normals.resize(sparseOffset + sparse.count); + morph_target.sparse_indices.resize(sparseOffset + sparse.count); - if (accessor.sparse.isSparse) + switch (sparse.indices.componentType) { - auto& sparse = accessor.sparse; - const tinygltf::BufferView& sparse_indices_view = state.gltfModel.bufferViews[sparse.indices.bufferView]; - const tinygltf::BufferView& sparse_values_view = state.gltfModel.bufferViews[sparse.values.bufferView]; - const tinygltf::Buffer& sparse_indices_buffer = state.gltfModel.buffers[sparse_indices_view.buffer]; - const tinygltf::Buffer& sparse_values_buffer = state.gltfModel.buffers[sparse_values_view.buffer]; - const uint8_t* sparse_indices_data = sparse_indices_buffer.data.data() + sparse.indices.byteOffset + sparse_indices_view.byteOffset; - const uint8_t* sparse_values_data = sparse_values_buffer.data.data() + sparse.values.byteOffset + sparse_values_view.byteOffset; - switch (sparse.indices.componentType) + default: + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE: + for (int s = 0; s < sparse.count; ++s) { - default: - case TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE: - for (int s = 0; s < sparse.count; ++s) - { - mesh.morph_targets[i].vertex_normals[sparse_indices_data[s]] = ((const XMFLOAT3*)sparse_values_data)[s]; - } - break; - case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT: - for (int s = 0; s < sparse.count; ++s) - { - mesh.morph_targets[i].vertex_normals[((const uint16_t*)sparse_indices_data)[s]] = ((const XMFLOAT3*)sparse_values_data)[s]; - } - break; - case TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT: - for (int s = 0; s < sparse.count; ++s) - { - mesh.morph_targets[i].vertex_normals[((const uint32_t*)sparse_indices_data)[s]] = ((const XMFLOAT3*)sparse_values_data)[s]; - } - break; + morph_target.sparse_indices[sparseOffset + s] = vertexOffset + sparse_indices_data[s]; + morph_target.vertex_normals[sparseOffset + s] = ((const XMFLOAT3*)sparse_values_data)[s]; } + break; + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT: + for (int s = 0; s < sparse.count; ++s) + { + morph_target.sparse_indices[sparseOffset + s] = vertexOffset + ((const uint16_t*)sparse_indices_data)[s]; + morph_target.vertex_normals[sparseOffset + s] = ((const XMFLOAT3*)sparse_values_data)[s]; + } + break; + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT: + for (int s = 0; s < sparse.count; ++s) + { + morph_target.sparse_indices[sparseOffset + s] = vertexOffset + ((const uint32_t*)sparse_indices_data)[s]; + morph_target.vertex_normals[sparseOffset + s] = ((const XMFLOAT3*)sparse_values_data)[s]; + } + break; + } + } + else + { + const tinygltf::BufferView& bufferView = state.gltfModel.bufferViews[accessor.bufferView]; + const tinygltf::Buffer& buffer = state.gltfModel.buffers[bufferView.buffer]; + + int stride = accessor.ByteStride(bufferView); + size_t vertexCount = accessor.count; + + const unsigned char* data = buffer.data.data() + accessor.byteOffset + bufferView.byteOffset; + + morph_target.vertex_normals.resize(vertexOffset + vertexCount); + for (size_t j = 0; j < vertexCount; ++j) + { + morph_target.vertex_normals[vertexOffset + j] = ((XMFLOAT3*)data)[j]; } } } } } + } + for (size_t i = 0; i < x.weights.size(); i++) + { + mesh.morph_targets[i].weight = static_cast(x.weights[i]); } mesh.CreateRenderData(); @@ -1587,8 +1617,146 @@ void Import_Extension_VRM(LoaderState& state) TransformComponent& transform = *state.scene->transforms.GetComponent(state.rootEntity); transform.RotateRollPitchYaw(XMFLOAT3(0, XM_PI, 0)); + if (ext_vrm->second.Has("blendShapeMaster")) + { + // https://github.com/vrm-c/vrm-specification/tree/master/specification/0.0#vrm-extension-morph-setting-jsonextensionsvrmblendshapemaster + Entity entity = CreateEntity(); + ExpressionComponent& component = state.scene->expressions.Create(entity); + state.scene->Component_Attach(entity, state.rootEntity); + state.scene->names.Create(entity) = state.name + "_blendShapeMaster"; + + const auto& blendShapeMaster = ext_vrm->second.Get("blendShapeMaster"); + if (blendShapeMaster.Has("blendShapeGroups")) + { + const auto& blendShapeGroups = blendShapeMaster.Get("blendShapeGroups"); + for (size_t blendShapeGroup_index = 0; blendShapeGroup_index < blendShapeGroups.ArrayLen(); ++blendShapeGroup_index) + { + const auto& blendShapeGroup = blendShapeGroups.Get(int(blendShapeGroup_index)); + ExpressionComponent::Expression& expression = component.expressions.emplace_back(); + + if (blendShapeGroup.Has("name")) + { + const auto& value = blendShapeGroup.Get("name"); + expression.name = value.Get(); + } + if (blendShapeGroup.Has("presetName")) + { + const auto& value = blendShapeGroup.Get("presetName"); + std::string presetName = wi::helper::toUpper(value.Get()); + + if (!presetName.compare("JOY")) + { + expression.preset = ExpressionComponent::Preset::Happy; + } + else if (!presetName.compare("ANGRY")) + { + expression.preset = ExpressionComponent::Preset::Angry; + } + else if (!presetName.compare("SORROW")) + { + expression.preset = ExpressionComponent::Preset::Sad; + } + else if (!presetName.compare("FUN")) + { + expression.preset = ExpressionComponent::Preset::Relaxed; + } + else if (!presetName.compare("A")) + { + expression.preset = ExpressionComponent::Preset::Aa; + } + else if (!presetName.compare("I")) + { + expression.preset = ExpressionComponent::Preset::Ih; + } + else if (!presetName.compare("U")) + { + expression.preset = ExpressionComponent::Preset::Ou; + } + else if (!presetName.compare("E")) + { + expression.preset = ExpressionComponent::Preset::Ee; + } + else if (!presetName.compare("O")) + { + expression.preset = ExpressionComponent::Preset::Oh; + } + else if (!presetName.compare("BLINK")) + { + expression.preset = ExpressionComponent::Preset::Blink; + } + else if (!presetName.compare("BLINK_L")) + { + expression.preset = ExpressionComponent::Preset::BlinkLeft; + } + else if (!presetName.compare("BLINK_R")) + { + expression.preset = ExpressionComponent::Preset::BlinkRight; + } + else if (!presetName.compare("LOOKUP")) + { + expression.preset = ExpressionComponent::Preset::LookUp; + } + else if (!presetName.compare("LOOKDOWN")) + { + expression.preset = ExpressionComponent::Preset::LookDown; + } + else if (!presetName.compare("LOOKLEFT")) + { + expression.preset = ExpressionComponent::Preset::LookLeft; + } + else if (!presetName.compare("LOOKRIGHT")) + { + expression.preset = ExpressionComponent::Preset::LookRight; + } + else if (!presetName.compare("NEUTRAL")) + { + expression.preset = ExpressionComponent::Preset::Neutral; + } + + const size_t preset_index = (size_t)expression.preset; + if (preset_index < arraysize(component.presets)) + { + component.presets[preset_index] = (int)component.expressions.size() - 1; + } + } + if (blendShapeGroup.Has("isBinary")) + { + const auto& value = blendShapeGroup.Get("isBinary"); + expression.SetBinary(value.Get()); + } + if (blendShapeGroup.Has("binds")) + { + const auto& binds = blendShapeGroup.Get("binds"); + for (size_t bind_index = 0; bind_index < binds.ArrayLen(); ++bind_index) + { + const auto& bind = binds.Get(int(bind_index)); + ExpressionComponent::Expression::MorphTargetBinding& morph_target_binding = expression.morph_target_bindings.emplace_back(); + + if (bind.Has("mesh")) + { + const auto& value = bind.Get("mesh"); + morph_target_binding.meshID = state.scene->meshes.GetEntity(value.GetNumberAsInt()); + } + if (bind.Has("index")) + { + const auto& value = bind.Get("index"); + morph_target_binding.index = value.GetNumberAsInt(); + } + if (bind.Has("weight")) + { + const auto& value = bind.Get("weight"); + morph_target_binding.weight = float(value.GetNumberAsInt()) / 100.0f; + } + } + } + } + } + } + if (ext_vrm->second.Has("secondaryAnimation")) { + // https://github.com/vrm-c/vrm-specification/tree/master/specification/0.0#vrm-extension-spring-bone-settings-jsonextensionsvrmsecondaryanimation + const auto& secondaryAnimation = ext_vrm->second.Get("secondaryAnimation"); if (secondaryAnimation.Has("boneGroups")) { @@ -1721,9 +1889,182 @@ void Import_Extension_VRM(LoaderState& state) void Import_Extension_VRMC(LoaderState& state) { + auto ext_vrm = state.gltfModel.extensions.find("VRMC_vrm"); + if (ext_vrm != state.gltfModel.extensions.end()) + { + if (ext_vrm->second.Has("expressions")) + { + // https://github.com/vrm-c/vrm-specification/blob/master/specification/VRMC_vrm-1.0-beta/expressions.md#vrmc_vrmexpressions + Entity entity = CreateEntity(); + ExpressionComponent& component = state.scene->expressions.Create(entity); + state.scene->Component_Attach(entity, state.rootEntity); + state.scene->names.Create(entity) = state.name + "_expressions"; + + const auto& expressions = ext_vrm->second.Get("expressions"); + static const char* expression_types[] = { + "preset", + "custom", + }; + + for (auto& expression_type : expression_types) + { + if (expressions.Has(expression_type)) + { + const auto& names = expressions.Get(expression_type); + for (auto& name : names.Keys()) + { + const auto& vrm_expression = names.Get(name); + ExpressionComponent::Expression& expression = component.expressions.emplace_back(); + + if (!strcmp(expression_type, "preset")) + { + std::string presetName = wi::helper::toUpper(name); + if (!presetName.compare("HAPPY")) + { + expression.preset = ExpressionComponent::Preset::Happy; + } + else if (!presetName.compare("ANGRY")) + { + expression.preset = ExpressionComponent::Preset::Angry; + } + else if (!presetName.compare("SAD")) + { + expression.preset = ExpressionComponent::Preset::Sad; + } + else if (!presetName.compare("RELAXED")) + { + expression.preset = ExpressionComponent::Preset::Relaxed; + } + else if (!presetName.compare("SURPRISED")) + { + expression.preset = ExpressionComponent::Preset::Surprised; + } + else if (!presetName.compare("AA")) + { + expression.preset = ExpressionComponent::Preset::Aa; + } + else if (!presetName.compare("IH")) + { + expression.preset = ExpressionComponent::Preset::Ih; + } + else if (!presetName.compare("OU")) + { + expression.preset = ExpressionComponent::Preset::Ou; + } + else if (!presetName.compare("EE")) + { + expression.preset = ExpressionComponent::Preset::Ee; + } + else if (!presetName.compare("OH")) + { + expression.preset = ExpressionComponent::Preset::Oh; + } + else if (!presetName.compare("BLINK")) + { + expression.preset = ExpressionComponent::Preset::Blink; + } + else if (!presetName.compare("BLINKLEFT")) + { + expression.preset = ExpressionComponent::Preset::BlinkLeft; + } + else if (!presetName.compare("BLINKRIGHT")) + { + expression.preset = ExpressionComponent::Preset::BlinkRight; + } + else if (!presetName.compare("LOOKUP")) + { + expression.preset = ExpressionComponent::Preset::LookUp; + } + else if (!presetName.compare("LOOKDOWN")) + { + expression.preset = ExpressionComponent::Preset::LookDown; + } + else if (!presetName.compare("LOOKLEFT")) + { + expression.preset = ExpressionComponent::Preset::LookLeft; + } + else if (!presetName.compare("LOOKRIGHT")) + { + expression.preset = ExpressionComponent::Preset::LookRight; + } + else if (!presetName.compare("NEUTRAL")) + { + expression.preset = ExpressionComponent::Preset::Neutral; + } + + const size_t preset_index = (size_t)expression.preset; + if (preset_index < arraysize(component.presets)) + { + component.presets[preset_index] = (int)component.expressions.size() - 1; + } + } + expression.name = name; + + if (vrm_expression.Has("isBinary")) + { + const auto& value = vrm_expression.Get("isBinary"); + expression.SetBinary(value.Get()); + } + if (vrm_expression.Has("overrideMouth")) + { + const auto& value = vrm_expression.Get("overrideMouth"); + const std::string& override_enum = value.Get(); + if (!override_enum.compare("block")) + { + expression.override_mouth = ExpressionComponent::Override::Block; + } + if (!override_enum.compare("blend")) + { + expression.override_mouth = ExpressionComponent::Override::Blend; + } + } + if (vrm_expression.Has("morphTargetBinds")) + { + const auto& morpTargetBinds = vrm_expression.Get("morphTargetBinds"); + for (size_t morphTargetBind_index = 0; morphTargetBind_index < morpTargetBinds.ArrayLen(); ++morphTargetBind_index) + { + const auto& morphTargetBind = morpTargetBinds.Get(int(morphTargetBind_index)); + ExpressionComponent::Expression::MorphTargetBinding& morph_target_binding = expression.morph_target_bindings.emplace_back(); + + if (morphTargetBind.Has("node")) + { + const auto& value = morphTargetBind.Get("node"); + morph_target_binding.meshID = state.scene->meshes.GetEntity(state.gltfModel.nodes[value.GetNumberAsInt()].mesh); + } + if (morphTargetBind.Has("index")) + { + const auto& value = morphTargetBind.Get("index"); + morph_target_binding.index = value.GetNumberAsInt(); + } + if (morphTargetBind.Has("weight")) + { + const auto& value = morphTargetBind.Get("weight"); + morph_target_binding.weight = float(value.GetNumberAsDouble()); + } + } + } + //if (vrm_expression.Has("materialColorBinds")) + //{ + // const auto& materialColorBinds = vrm_expression.Get("materialColorBinds"); + // // TODO: find example model and implement + //} + //if (vrm_expression.Has("textureTransformBinds ")) + //{ + // const auto& textureTransformBinds = vrm_expression.Get("textureTransformBinds"); + // // TODO: find example model and implement + //} + + } + } + } + } + } + auto ext_vrmc_springbone = state.gltfModel.extensions.find("VRMC_springBone"); if (ext_vrmc_springbone != state.gltfModel.extensions.end()) { + // https://github.com/vrm-c/vrm-specification/tree/master/specification/VRMC_springBone-1.0-beta + // Colliders: if (ext_vrmc_springbone->second.Has("colliders")) { diff --git a/Editor/OptionsWindow.cpp b/Editor/OptionsWindow.cpp index e06920a80..1ae883cbc 100644 --- a/Editor/OptionsWindow.cpp +++ b/Editor/OptionsWindow.cpp @@ -307,6 +307,7 @@ void OptionsWindow::Create(EditorComponent* _editor) filterCombo.AddItem("Camera " ICON_CAMERA, (uint64_t)Filter::Camera); filterCombo.AddItem("Armature " ICON_ARMATURE, (uint64_t)Filter::Armature); filterCombo.AddItem("Script " ICON_SCRIPT, (uint64_t)Filter::Script); + filterCombo.AddItem("Expression " ICON_EXPRESSION, (uint64_t)Filter::Expression); filterCombo.SetTooltip("Apply filtering to the Entities"); filterCombo.OnSelect([&](wi::gui::EventArgs args) { filter = (Filter)args.userdata; @@ -884,6 +885,10 @@ void OptionsWindow::PushToEntityTree(wi::ecs::Entity entity, int level) { item.name += ICON_SCRIPT " "; } + if (scene.expressions.Contains(entity)) + { + item.name += ICON_EXPRESSION " "; + } if (entity == terragen.terrainEntity) { item.name += ICON_TERRAIN " "; @@ -1134,6 +1139,14 @@ void OptionsWindow::RefreshEntityTree() } } + if (has_flag(filter, Filter::Expression)) + { + for (size_t i = 0; i < scene.expressions.GetCount(); ++i) + { + PushToEntityTree(scene.expressions.GetEntity(i), 0); + } + } + entitytree_added_items.clear(); entitytree_opened_items.clear(); } diff --git a/Editor/OptionsWindow.h b/Editor/OptionsWindow.h index 437054c50..7178eeab9 100644 --- a/Editor/OptionsWindow.h +++ b/Editor/OptionsWindow.h @@ -55,6 +55,7 @@ public: Armature = 1 << 15, Collider = 1 << 16, Script = 1 << 17, + Expression = 1 << 18, All = ~0ull, } filter = Filter::All; diff --git a/WickedEngine/wiGUI.cpp b/WickedEngine/wiGUI.cpp index ddc8ae766..0fde93dcd 100644 --- a/WickedEngine/wiGUI.cpp +++ b/WickedEngine/wiGUI.cpp @@ -1627,6 +1627,10 @@ namespace wi::gui { this->value = value; } + void Slider::SetValue(int value) + { + this->value = float(value); + } float Slider::GetValue() const { return value; @@ -4435,6 +4439,12 @@ namespace wi::gui { items.push_back(item); } + void TreeList::AddItem(const std::string& name) + { + Item item; + item.name = name; + AddItem(item); + } void TreeList::ClearItems() { items.clear(); diff --git a/WickedEngine/wiGUI.h b/WickedEngine/wiGUI.h index 9a31f7f51..3bb663ad7 100644 --- a/WickedEngine/wiGUI.h +++ b/WickedEngine/wiGUI.h @@ -452,6 +452,7 @@ namespace wi::gui wi::Sprite sprites_knob[WIDGETSTATE_COUNT]; void SetValue(float value); + void SetValue(int value); float GetValue() const; void SetRange(float start, float end); @@ -692,6 +693,7 @@ namespace wi::gui void Create(const std::string& name); void AddItem(const Item& item); + void AddItem(const std::string& name); void ClearItems(); bool HasScrollbar() const; diff --git a/WickedEngine/wiRenderer.cpp b/WickedEngine/wiRenderer.cpp index 5c0bc1cc2..b4e6e1613 100644 --- a/WickedEngine/wiRenderer.cpp +++ b/WickedEngine/wiRenderer.cpp @@ -3839,7 +3839,7 @@ void UpdateRenderData( Entity entity = vis.scene->meshes.GetEntity(i); const MeshComponent& mesh = vis.scene->meshes[i]; - if (mesh.dirty_morph) + if (mesh.dirty_morph && !mesh.vertex_positions_morphed.empty()) { mesh.dirty_morph = false; GraphicsDevice::GPUAllocation allocation = device->AllocateGPU(mesh.vb_pos_nor_wind.size, cmd); diff --git a/WickedEngine/wiScene.cpp b/WickedEngine/wiScene.cpp index f13d8206e..d2e3d243c 100644 --- a/WickedEngine/wiScene.cpp +++ b/WickedEngine/wiScene.cpp @@ -1734,6 +1734,8 @@ namespace wi::scene } geometryArrayMapped = (ShaderGeometry*)geometryUploadBuffer[device->GetBufferIndex()].mapped_data; + RunExpressionUpdateSystem(ctx); + RunMeshUpdateSystem(ctx); RunMaterialUpdateSystem(ctx); @@ -3464,6 +3466,207 @@ namespace wi::scene }); } + void Scene::RunExpressionUpdateSystem(wi::jobsystem::context& ctx) + { + for (size_t i = 0; i < expressions.GetCount(); ++i) + { + ExpressionComponent& expression_mastering = expressions[i]; + + // Procedural blink: + expression_mastering.blink_timer += expression_mastering.blink_frequency * dt; + if (expression_mastering.blink_timer >= 1) + { + int blink = expression_mastering.presets[(int)ExpressionComponent::Preset::Blink]; + if (blink >= 0 && blink < expression_mastering.expressions.size()) + { + ExpressionComponent::Expression& expression = expression_mastering.expressions[blink]; + expression_mastering.blink_count = std::max(1, expression_mastering.blink_count); + float one_blink_length = expression_mastering.blink_length * expression_mastering.blink_frequency; + float all_blink_length = one_blink_length * (float)expression_mastering.blink_count; + float blink_index = std::floor(wi::math::Lerp(0, (float)expression_mastering.blink_count, (expression_mastering.blink_timer - 1) / all_blink_length)); + float blink_trim = 1 + one_blink_length * blink_index; + float blink_state = wi::math::InverseLerp(0, one_blink_length, expression_mastering.blink_timer - blink_trim); + if (blink_state < 0.5f) + { + // closing + expression.weight = wi::math::Lerp(0, 1, wi::math::saturate(blink_state * 2)); + } + else + { + // opening + expression.weight = wi::math::Lerp(1, 0, wi::math::saturate((blink_state - 0.5f) * 2)); + } + if (expression_mastering.blink_timer >= 1 + all_blink_length) + { + expression.weight = 0; + expression_mastering.blink_timer = 0; + } + expression.SetDirty(); + } + } + + // Procedural look: + if (expression_mastering.look_timer == 0) + { + // Roll new random look direction for next look away event: + float vertical = wi::random::GetRandom(-1.0f, 1.0f); + float horizontal = wi::random::GetRandom(-1.0f, 1.0f); + expression_mastering.look_weights[0] = wi::math::saturate(vertical); + expression_mastering.look_weights[1] = wi::math::saturate(-vertical); + expression_mastering.look_weights[2] = wi::math::saturate(horizontal); + expression_mastering.look_weights[3] = wi::math::saturate(-horizontal); + } + expression_mastering.look_timer += expression_mastering.look_frequency * dt; + if (expression_mastering.look_timer >= 1) + { + int looks[] = { + expression_mastering.presets[(int)ExpressionComponent::Preset::LookDown], + expression_mastering.presets[(int)ExpressionComponent::Preset::LookUp], + expression_mastering.presets[(int)ExpressionComponent::Preset::LookLeft], + expression_mastering.presets[(int)ExpressionComponent::Preset::LookRight], + }; + for (int idx = 0; idx= 0 && look < expression_mastering.expressions.size()) + { + ExpressionComponent::Expression& expression = expression_mastering.expressions[look]; + float look_state = wi::math::InverseLerp(0, expression_mastering.look_length * expression_mastering.look_frequency, expression_mastering.look_timer - 1); + if (look_state < 0.25f) + { + expression.weight = wi::math::Lerp(0, weight, wi::math::saturate(look_state * 4)); + } + else + { + expression.weight = wi::math::Lerp(weight, 0, wi::math::saturate((look_state - 0.75f) * 4)); + } + expression.SetDirty(); + } + } + if (expression_mastering.look_timer >= 1 + expression_mastering.look_length * expression_mastering.look_frequency) + { + expression_mastering.look_timer = 0; + } + } + + float overrideMouthBlend = 0; + float overrideBlinkBlend = 0; + float overrideLookBlend = 0; + + // Pass 1: reset targets that will be modified by expressions: + // Also accumulate override weights + for(ExpressionComponent::Expression& expression : expression_mastering.expressions) + { + const float blend = expression.IsBinary() ? (expression.weight > 0 ? 1 : 0) : expression.weight; + if (expression.override_mouth == ExpressionComponent::Override::Block) + { + overrideMouthBlend += 1; + } + if (expression.override_mouth == ExpressionComponent::Override::Blend) + { + overrideMouthBlend += blend; + } + if (expression.override_blink == ExpressionComponent::Override::Block) + { + overrideBlinkBlend += 1; + } + if (expression.override_blink == ExpressionComponent::Override::Blend) + { + overrideBlinkBlend += blend; + } + if (expression.override_look == ExpressionComponent::Override::Block) + { + overrideLookBlend += 1; + } + if (expression.override_look == ExpressionComponent::Override::Blend) + { + overrideLookBlend += blend; + } + + if (!expression.IsDirty()) + continue; + + for (const ExpressionComponent::Expression::MorphTargetBinding& morph_target_binding : expression.morph_target_bindings) + { + MeshComponent* mesh = meshes.GetComponent(morph_target_binding.meshID); + if (mesh != nullptr && (int)mesh->morph_targets.size() > morph_target_binding.index) + { + MeshComponent::MorphTarget& morph_target = mesh->morph_targets[morph_target_binding.index]; + if (morph_target.weight > 0) + { + morph_target.weight = 0; + } + } + } + } + + // Override weights are factored in: + const int mouths[] = { + 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 (int mouth : mouths) + { + if (mouth >= 0 && mouth < expression_mastering.expressions.size()) + { + ExpressionComponent::Expression& expression = expression_mastering.expressions[mouth]; + expression.weight *= 1 - wi::math::saturate(overrideMouthBlend); + } + } + const int blinks[] = { + expression_mastering.presets[(int)ExpressionComponent::Preset::Blink], + expression_mastering.presets[(int)ExpressionComponent::Preset::BlinkLeft], + expression_mastering.presets[(int)ExpressionComponent::Preset::BlinkRight], + }; + for (int blink : blinks) + { + if (blink >= 0 && blink < expression_mastering.expressions.size()) + { + ExpressionComponent::Expression& expression = expression_mastering.expressions[blink]; + expression.weight *= 1 - wi::math::saturate(overrideBlinkBlend); + } + } + const int looks[] = { + expression_mastering.presets[(int)ExpressionComponent::Preset::LookUp], + expression_mastering.presets[(int)ExpressionComponent::Preset::LookDown], + expression_mastering.presets[(int)ExpressionComponent::Preset::LookLeft], + expression_mastering.presets[(int)ExpressionComponent::Preset::LookRight], + }; + for (int look : looks) + { + if (look >= 0 && look < expression_mastering.expressions.size()) + { + ExpressionComponent::Expression& expression = expression_mastering.expressions[look]; + expression.weight *= 1 - wi::math::saturate(overrideLookBlend); + } + } + + // Pass 2: apply expressions: + for (ExpressionComponent::Expression& expression : expression_mastering.expressions) + { + if (!expression.IsDirty()) + continue; + + expression.SetDirty(false); + const float blend = expression.IsBinary() ? (expression.weight > 0 ? 1 : 0) : expression.weight; + + for (const ExpressionComponent::Expression::MorphTargetBinding& morph_target_binding : expression.morph_target_bindings) + { + MeshComponent* mesh = meshes.GetComponent(morph_target_binding.meshID); + if (mesh != nullptr && (int)mesh->morph_targets.size() > morph_target_binding.index) + { + MeshComponent::MorphTarget& morph_target = mesh->morph_targets[morph_target_binding.index]; + morph_target.weight = wi::math::Lerp(morph_target.weight, morph_target_binding.weight, blend); + mesh->dirty_morph = true; + } + } + } + } + } void Scene::RunColliderUpdateSystem(wi::jobsystem::context& ctx) { for (size_t i = 0; i < colliders.GetCount(); ++i) @@ -3925,25 +4128,53 @@ namespace wi::scene XMFLOAT3 _min = XMFLOAT3(std::numeric_limits::max(), std::numeric_limits::max(), std::numeric_limits::max()); XMFLOAT3 _max = XMFLOAT3(std::numeric_limits::lowest(), std::numeric_limits::lowest(), std::numeric_limits::lowest()); - for (size_t i = 0; i < mesh.vertex_positions.size(); ++i) - { - XMFLOAT3 pos = mesh.vertex_positions[i]; - XMFLOAT3 nor = mesh.vertex_normals.empty() ? XMFLOAT3(1, 1, 1) : mesh.vertex_normals[i]; - const uint8_t wind = mesh.vertex_windweights.empty() ? 0xFF : mesh.vertex_windweights[i]; + mesh.morph_temp_pos = mesh.vertex_positions; + mesh.morph_temp_nor = mesh.vertex_normals; - for (const MeshComponent::MorphTarget& morph : mesh.morph_targets) + for (const MeshComponent::MorphTarget& morph : mesh.morph_targets) + { + if (morph.weight <= 0) + continue; + if (morph.sparse_indices.empty()) { - pos.x += morph.weight * morph.vertex_positions[i].x; - pos.y += morph.weight * morph.vertex_positions[i].y; - pos.z += morph.weight * morph.vertex_positions[i].z; - - if (!morph.vertex_normals.empty()) + for (size_t i = 0; i < morph.vertex_positions.size(); ++i) { - nor.x += morph.weight * morph.vertex_normals[i].x; - nor.y += morph.weight * morph.vertex_normals[i].y; - nor.z += morph.weight * morph.vertex_normals[i].z; + mesh.morph_temp_pos[i].x += morph.weight * morph.vertex_positions[i].x; + mesh.morph_temp_pos[i].y += morph.weight * morph.vertex_positions[i].y; + mesh.morph_temp_pos[i].z += morph.weight * morph.vertex_positions[i].z; + + if (!morph.vertex_normals.empty()) + { + mesh.morph_temp_nor[i].x += morph.weight * morph.vertex_normals[i].x; + mesh.morph_temp_nor[i].y += morph.weight * morph.vertex_normals[i].y; + mesh.morph_temp_nor[i].z += morph.weight * morph.vertex_normals[i].z; + } } } + else + { + for (size_t i = 0; i < morph.sparse_indices.size(); ++i) + { + const uint32_t ind = morph.sparse_indices[i]; + mesh.morph_temp_pos[ind].x += morph.weight * morph.vertex_positions[i].x; + mesh.morph_temp_pos[ind].y += morph.weight * morph.vertex_positions[i].y; + mesh.morph_temp_pos[ind].z += morph.weight * morph.vertex_positions[i].z; + + if (!morph.vertex_normals.empty()) + { + mesh.morph_temp_nor[ind].x += morph.weight * morph.vertex_normals[i].x; + mesh.morph_temp_nor[ind].y += morph.weight * morph.vertex_normals[i].y; + mesh.morph_temp_nor[ind].z += morph.weight * morph.vertex_normals[i].z; + } + } + } + } + + for (size_t i = 0; i < mesh.morph_temp_pos.size(); ++i) + { + XMFLOAT3 pos = mesh.morph_temp_pos[i]; + XMFLOAT3 nor = mesh.morph_temp_nor.empty() ? XMFLOAT3(1, 1, 1) : mesh.morph_temp_nor[i]; + const uint8_t wind = mesh.vertex_windweights.empty() ? 0xFF : mesh.vertex_windweights[i]; XMStoreFloat3(&nor, XMVector3Normalize(XMLoadFloat3(&nor))); mesh.vertex_positions_morphed[i].FromFULL(pos, nor, wind); diff --git a/WickedEngine/wiScene.h b/WickedEngine/wiScene.h index ad9879e82..dad647c49 100644 --- a/WickedEngine/wiScene.h +++ b/WickedEngine/wiScene.h @@ -372,7 +372,8 @@ namespace wi::scene { wi::vector vertex_positions; wi::vector vertex_normals; - float weight; + wi::vector sparse_indices; // optional, these can be used to target vertices indirectly + float weight = 0; }; wi::vector morph_targets; @@ -608,6 +609,8 @@ namespace wi::scene // Non serialized attributes: wi::vector vertex_positions_morphed; + wi::vector morph_temp_pos; + wi::vector morph_temp_nor; }; @@ -1425,6 +1428,111 @@ namespace wi::scene void Serialize(wi::Archive& archive, wi::ecs::EntitySerializer& seri); }; + struct ExpressionComponent + { + enum FLAGS + { + EMPTY = 0, + }; + uint32_t _flags = EMPTY; + + // Preset expressions can have common behaviours assigned: + // https://github.com/vrm-c/vrm-specification/blob/bd205a6c3839993f2729e4e7c3a74af89877cfce/specification/VRMC_vrm-1.0-beta/expressions.md#preset-expressions + enum class Preset + { + // Emotions: + Happy, // Changed from joy + Angry, // anger + Sad, // Changed from sorrow + Relaxed, // Comfortable. Changed from fun + Surprised, // surprised. Added new in VRM 1.0 + + // Lip sync procedural: + // Procedural: A value that can be automatically generated by the system. + // - Analyze microphone input, generate from text, etc. + Aa, // aa + Ih, // i + Ou, // u + Ee, // eh + Oh, // oh + + // Blink procedural: + // Procedural: A value that can be automatically generated by the system. + // - Randomly blink, etc. + Blink, // close both eyelids + BlinkLeft, // Close the left eyelid + BlinkRight, // Close right eyelid + + // Gaze procedural: + // Procedural: A value that can be automatically generated by the system. + // - The VRM LookAt will generate a value for the gaze point from time to time (see LookAt Expression Type). + LookUp, // For models where the line of sight moves with Expression instead of bone. See eye control. + LookDown, // For models where the line of sight moves with Expression instead of bone. See eye control. + LookLeft, // For models whose line of sight moves with Expression instead of bone. See eye control. + LookRight, // For models where the line of sight moves with Expression instead of bone. See eye control. + + // Other: + Neutral, // left for backwards compatibility. + + Count, + }; + int presets[size_t(Preset::Count)] = { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 }; + + enum class Override + { + None, + Block, + Blend, + }; + + float blink_frequency = 0; // number of blinks per second + float blink_length = 0.1f; // blink's completion time in seconds + int blink_count = 2; + float look_frequency = 0; // number of lookAt changes per second + float look_length = 0.6f; // lookAt's completion time in seconds + + struct Expression + { + enum FLAGS + { + EMPTY = 0, + DIRTY = 1 << 0, + BINARY = 1 << 1, + }; + uint32_t _flags = EMPTY; + + std::string name; + float weight = 0; + + Preset preset = Preset::Count; + Override override_mouth = Override::None; + Override override_blink = Override::None; + Override override_look = Override::None; + + struct MorphTargetBinding + { + wi::ecs::Entity meshID = wi::ecs::INVALID_ENTITY; + int index = 0; + float weight = 0; + }; + wi::vector morph_target_bindings; + + constexpr bool IsDirty() const { return _flags & DIRTY; } + constexpr bool IsBinary() const { return _flags & BINARY; } + + constexpr void SetDirty(bool value = true) { if (value) { _flags |= DIRTY; } else { _flags &= ~DIRTY; } } + constexpr void SetBinary(bool value = true) { if (value) { _flags |= BINARY; } else { _flags &= ~BINARY; } } + }; + wi::vector expressions; + + // Non-serialized attributes: + float blink_timer = 0; + float look_timer = 0; + float look_weights[4] = {}; + + void Serialize(wi::Archive& archive, wi::ecs::EntitySerializer& seri); + }; + struct Scene { wi::ecs::ComponentLibrary componentLibrary; @@ -1434,7 +1542,7 @@ namespace wi::scene wi::ecs::ComponentManager& transforms = componentLibrary.Register("wi::scene::Scene::transforms"); wi::ecs::ComponentManager& hierarchy = componentLibrary.Register("wi::scene::Scene::hierarchy"); wi::ecs::ComponentManager& materials = componentLibrary.Register("wi::scene::Scene::materials", 1); // version = 1 - wi::ecs::ComponentManager& meshes = componentLibrary.Register("wi::scene::Scene::meshes"); + wi::ecs::ComponentManager& meshes = componentLibrary.Register("wi::scene::Scene::meshes", 1); // version = 1 wi::ecs::ComponentManager& impostors = componentLibrary.Register("wi::scene::Scene::impostors"); wi::ecs::ComponentManager& objects = componentLibrary.Register("wi::scene::Scene::objects"); wi::ecs::ComponentManager& aabb_objects = componentLibrary.Register("wi::scene::Scene::aabb_objects"); @@ -1459,6 +1567,7 @@ namespace wi::scene wi::ecs::ComponentManager& springs = componentLibrary.Register("wi::scene::Scene::springs", 1); // version = 1 wi::ecs::ComponentManager& colliders = componentLibrary.Register("wi::scene::Scene::colliders", 1); // version = 1 wi::ecs::ComponentManager& scripts = componentLibrary.Register("wi::scene::Scene::scripts"); + wi::ecs::ComponentManager& expressions = componentLibrary.Register("wi::scene::Scene::expressions"); // Non-serialized attributes: float dt = 0; @@ -1700,6 +1809,7 @@ namespace wi::scene void RunAnimationUpdateSystem(wi::jobsystem::context& ctx); void RunTransformUpdateSystem(wi::jobsystem::context& ctx); void RunHierarchyUpdateSystem(wi::jobsystem::context& ctx); + void RunExpressionUpdateSystem(wi::jobsystem::context& ctx); void RunColliderUpdateSystem(wi::jobsystem::context& ctx); void RunSpringUpdateSystem(wi::jobsystem::context& ctx); void RunInverseKinematicsUpdateSystem(wi::jobsystem::context& ctx); diff --git a/WickedEngine/wiScene_Serializers.cpp b/WickedEngine/wiScene_Serializers.cpp index 2e0bb96c9..297eaa08b 100644 --- a/WickedEngine/wiScene_Serializers.cpp +++ b/WickedEngine/wiScene_Serializers.cpp @@ -415,6 +415,10 @@ namespace wi::scene archive >> morph_targets[i].vertex_positions; archive >> morph_targets[i].vertex_normals; archive >> morph_targets[i].weight; + if (seri.GetVersion() >= 1) + { + archive >> morph_targets[i].sparse_indices; + } } } @@ -484,6 +488,10 @@ namespace wi::scene archive << morph_targets[i].vertex_positions; archive << morph_targets[i].vertex_normals; archive << morph_targets[i].weight; + if (seri.GetVersion() >= 1) + { + archive << morph_targets[i].sparse_indices; + } } } @@ -1461,6 +1469,89 @@ namespace wi::scene archive << relative_filename; } } + void ExpressionComponent::Serialize(wi::Archive& archive, EntitySerializer& seri) + { + if (archive.IsReadMode()) + { + archive >> _flags; + for (int& index : presets) + { + archive >> index; + } + archive >> blink_frequency; + archive >> blink_length; + archive >> blink_count; + archive >> look_frequency; + archive >> look_length; + + size_t expression_count = 0; + archive >> expression_count; + expressions.resize(expression_count); + for (size_t expression_index = 0; expression_index < expression_count; ++expression_index) + { + Expression& expression = expressions[expression_index]; + archive >> expression.name; + archive >> expression.weight; + + uint32_t value = 0; + archive >> value; + expression.preset = (Preset)value; + + archive >> value; + expression.override_mouth = (Override)value; + archive >> value; + expression.override_blink = (Override)value; + archive >> value; + expression.override_look = (Override)value; + + size_t count = 0; + archive >> count; + expression.morph_target_bindings.resize(count); + for (size_t i = 0; i < count; ++i) + { + SerializeEntity(archive, expression.morph_target_bindings[i].meshID, seri); + archive >> expression.morph_target_bindings[i].index; + archive >> expression.morph_target_bindings[i].weight; + } + + expression.SetDirty(); + } + } + else + { + archive << _flags; + for (int index : presets) + { + archive << index; + } + archive << blink_frequency; + archive << blink_length; + archive << blink_count; + archive << look_frequency; + archive << look_length; + + archive << expressions.size(); + for (size_t expression_index = 0; expression_index < expressions.size(); ++expression_index) + { + Expression& expression = expressions[expression_index]; + archive << expression.name; + archive << expression.weight; + + archive << (uint32_t)expression.preset; + archive << (uint32_t)expression.override_mouth; + archive << (uint32_t)expression.override_blink; + archive << (uint32_t)expression.override_look; + + archive << expression.morph_target_bindings.size(); + for (size_t i = 0; i < expression.morph_target_bindings.size(); ++i) + { + SerializeEntity(archive, expression.morph_target_bindings[i].meshID, seri); + archive << expression.morph_target_bindings[i].index; + archive << expression.morph_target_bindings[i].weight; + } + } + } + } void Scene::Serialize(wi::Archive& archive) { diff --git a/WickedEngine/wiVersion.cpp b/WickedEngine/wiVersion.cpp index 9bc942566..57916b57f 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 = 28; + const int revision = 29; const std::string version_string = std::to_string(major) + "." + std::to_string(minor) + "." + std::to_string(revision); diff --git a/credits.txt b/credits.txt index 8327bfe69..a0cf01c3f 100644 --- a/credits.txt +++ b/credits.txt @@ -33,6 +33,7 @@ Contributions: - Linux audio implementation - Linux controller implementation - Linux network implementation + - Lua property bindings support, improvements - Fixes - Maeve Garside https://github.com/MolassesLover - Linux package files diff --git a/features.txt b/features.txt index 2c4471490..10af7e152 100644 --- a/features.txt +++ b/features.txt @@ -8,7 +8,7 @@ Font rendering (True Type) Networking (UDP) 3D mesh rendering Skeletal animation -Morph target animation +Morph target animation (with sparse accessor) Physically based rendering Animated texturing Normal mapping @@ -74,7 +74,7 @@ Entity-Component System (Data oriented design) Lightmap baking (with GPU path tracing) Job system Inverse Kinematics -Springs +Springs, Colliders Variable Rate Shading Real time ray tracing: ambient occlusion, shadows, reflections (DXR and Vulkan raytracing) Screen Space Contact Shadows @@ -83,8 +83,9 @@ Surfel GI HDR display output Dynamic Diffuse Global Illumination (DDGI) Procedural terrain generator +Expressions -GLTF 2.0 extensions supported: +GLTF 2.0 - KHR extensions supported: KHR_materials_unlit KHR_materials_transmission KHR_materials_pbrSpecularGlossiness @@ -94,3 +95,11 @@ KHR_materials_specular KHR_materials_ior KHR_texture_basisu KHR_lights_punctual + +GLTF 2.0 - VRM 0.0 extensions supported: +VRM_secondaryAnimation +VRM_blendShapeMaster + +GLTF 2.0 - VRM 1.0 extensions supported: +VRMC_springBone +VRMC_vrm_expressions