feat: Add "simple" inspector to the imgui panels
This commit is contained in:
@@ -12,6 +12,8 @@ set(SRC_FILES
|
|||||||
|
|
||||||
"src/Components/Physics/Rigidbody.cpp"
|
"src/Components/Physics/Rigidbody.cpp"
|
||||||
"src/Components/Physics/BoxCollider.cpp"
|
"src/Components/Physics/BoxCollider.cpp"
|
||||||
|
"src/Components/Physics/CapsuleCollider.cpp"
|
||||||
|
"src/Components/Physics/Collider.cpp"
|
||||||
"src/Components/Physics/SphereCollider.cpp"
|
"src/Components/Physics/SphereCollider.cpp"
|
||||||
|
|
||||||
"src/Graphics/BindlessSetManager.cpp"
|
"src/Graphics/BindlessSetManager.cpp"
|
||||||
@@ -64,6 +66,7 @@ set(SRC_FILES
|
|||||||
"src/Serialization/ComponentRegistry.cpp"
|
"src/Serialization/ComponentRegistry.cpp"
|
||||||
|
|
||||||
"src/Util/DeltaTime.cpp"
|
"src/Util/DeltaTime.cpp"
|
||||||
|
"src/Util/ImGuiUtils.cpp"
|
||||||
|
|
||||||
"src/Physics/PhysicsWorld.cpp"
|
"src/Physics/PhysicsWorld.cpp"
|
||||||
"src/Physics/SimplePhysicsWorld.cpp"
|
"src/Physics/SimplePhysicsWorld.cpp"
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ public:
|
|||||||
nlohmann::json Serialize() const override;
|
nlohmann::json Serialize() const override;
|
||||||
void Deserialize(const nlohmann::json& data) override;
|
void Deserialize(const nlohmann::json& data) override;
|
||||||
void ResolveReferences(const ObjectMap& objects) override;
|
void ResolveReferences(const ObjectMap& objects) override;
|
||||||
|
void ImGuiInspector() override;
|
||||||
|
|
||||||
void SetMeshID(MeshID id);
|
void SetMeshID(MeshID id);
|
||||||
MeshID GetMeshID() const { return meshID; }
|
MeshID GetMeshID() const { return meshID; }
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ public:
|
|||||||
|
|
||||||
nlohmann::json Serialize() const override;
|
nlohmann::json Serialize() const override;
|
||||||
void Deserialize(const nlohmann::json& data) override;
|
void Deserialize(const nlohmann::json& data) override;
|
||||||
|
void ImGuiInspector() override;
|
||||||
|
|
||||||
// optional setters
|
// optional setters
|
||||||
void SetRadius(float r) { m_Radius = r; }
|
void SetRadius(float r) { m_Radius = r; }
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ public:
|
|||||||
|
|
||||||
nlohmann::json Serialize() const override;
|
nlohmann::json Serialize() const override;
|
||||||
void Deserialize(const nlohmann::json& data) override;
|
void Deserialize(const nlohmann::json& data) override;
|
||||||
|
void ImGuiInspector() override;
|
||||||
|
|
||||||
std::string GetTypeName() const override
|
std::string GetTypeName() const override
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ public:
|
|||||||
return "CapsuleCollider";
|
return "CapsuleCollider";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ImGuiInspector() override;
|
||||||
|
|
||||||
nlohmann::json Serialize() const override {
|
nlohmann::json Serialize() const override {
|
||||||
auto data = SerializeCollider();
|
auto data = SerializeCollider();
|
||||||
data["radius"] = m_Radius;
|
data["radius"] = m_Radius;
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ public:
|
|||||||
|
|
||||||
~Collider() override = default;
|
~Collider() override = default;
|
||||||
|
|
||||||
|
void ImGuiInspector() override;
|
||||||
|
|
||||||
[[nodiscard]] virtual PhysicsShapeDesc BuildPhysicsShape() const = 0;
|
[[nodiscard]] virtual PhysicsShapeDesc BuildPhysicsShape() const = 0;
|
||||||
|
|
||||||
[[nodiscard]] bool IsTrigger() const { return m_IsTrigger; }
|
[[nodiscard]] bool IsTrigger() const { return m_IsTrigger; }
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ public:
|
|||||||
|
|
||||||
nlohmann::json Serialize() const override;
|
nlohmann::json Serialize() const override;
|
||||||
void Deserialize(const nlohmann::json& data) override;
|
void Deserialize(const nlohmann::json& data) override;
|
||||||
|
void ImGuiInspector() override;
|
||||||
|
|
||||||
std::string GetTypeName() const override
|
std::string GetTypeName() const override
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ public:
|
|||||||
|
|
||||||
nlohmann::json Serialize() const override;
|
nlohmann::json Serialize() const override;
|
||||||
void Deserialize(const nlohmann::json& data) override;
|
void Deserialize(const nlohmann::json& data) override;
|
||||||
|
void ImGuiInspector() override;
|
||||||
std::string GetTypeName() const override
|
std::string GetTypeName() const override
|
||||||
{
|
{
|
||||||
return "SphereCollider";
|
return "SphereCollider";
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ public:
|
|||||||
|
|
||||||
nlohmann::json Serialize() const override;
|
nlohmann::json Serialize() const override;
|
||||||
void Deserialize(const nlohmann::json& data) override;
|
void Deserialize(const nlohmann::json& data) override;
|
||||||
|
void ImGuiInspector() override;
|
||||||
|
|
||||||
void SetDistance(float distance);
|
void SetDistance(float distance);
|
||||||
void SetSpeed(float speed) { m_Speed = speed; }
|
void SetSpeed(float speed) { m_Speed = speed; }
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ public:
|
|||||||
|
|
||||||
nlohmann::json Serialize() const override;
|
nlohmann::json Serialize() const override;
|
||||||
void Deserialize(const nlohmann::json& data) override;
|
void Deserialize(const nlohmann::json& data) override;
|
||||||
|
void ImGuiInspector() override;
|
||||||
|
|
||||||
void SetAxis(const glm::vec3& axis) {
|
void SetAxis(const glm::vec3& axis) {
|
||||||
m_Axis = glm::dot(axis, axis) > 0.000001f
|
m_Axis = glm::dot(axis, axis) > 0.000001f
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ public:
|
|||||||
virtual void LateUpdate(float dt);
|
virtual void LateUpdate(float dt);
|
||||||
virtual void FixedUpdate(float fixedDt);
|
virtual void FixedUpdate(float fixedDt);
|
||||||
|
|
||||||
|
// Called by the editor inspector while this component is selected. A
|
||||||
|
// component owns the controls for its editable runtime properties.
|
||||||
virtual void ImGuiInspector();
|
virtual void ImGuiInspector();
|
||||||
virtual void ImGuiRender();
|
virtual void ImGuiRender();
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
|
|
||||||
#include <destrum/Event.h>
|
#include <destrum/Event.h>
|
||||||
|
#include <destrum/ObjectModel/ObjectId.h>
|
||||||
#include <destrum/Scene/SceneManager.h>
|
#include <destrum/Scene/SceneManager.h>
|
||||||
|
|
||||||
#include "destrum/Physics/JoltPhysicsWorld.h"
|
#include "destrum/Physics/JoltPhysicsWorld.h"
|
||||||
@@ -120,6 +121,7 @@ private:
|
|||||||
|
|
||||||
//Imgui vars
|
//Imgui vars
|
||||||
bool m_ShowDemoWindow{false};
|
bool m_ShowDemoWindow{false};
|
||||||
|
ObjectId m_SelectedObjectId{InvalidObjectId};
|
||||||
|
|
||||||
std::function<void()> m_registerBindings;
|
std::function<void()> m_registerBindings;
|
||||||
std::function<void()> m_unregisterBindings;
|
std::function<void()> m_unregisterBindings;
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#ifndef DESTRUM_IMGUIUTILS_H
|
||||||
|
#define DESTRUM_IMGUIUTILS_H
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
|
||||||
|
#include <destrum/ObjectModel/ObjectId.h>
|
||||||
|
|
||||||
|
class GameObject;
|
||||||
|
|
||||||
|
namespace ImGuiUtils {
|
||||||
|
[[nodiscard]] bool DrawVec3Control(
|
||||||
|
const char* label,
|
||||||
|
glm::vec3& value,
|
||||||
|
float speed = 0.1f);
|
||||||
|
|
||||||
|
void RenderSceneInspector(
|
||||||
|
std::string_view sceneName,
|
||||||
|
const std::vector<std::shared_ptr<GameObject>>& objects,
|
||||||
|
const std::vector<std::shared_ptr<GameObject>>& pendingAdditions,
|
||||||
|
ObjectId& selectedObjectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // DESTRUM_IMGUIUTILS_H
|
||||||
@@ -194,6 +194,8 @@ void App::run()
|
|||||||
|
|
||||||
{
|
{
|
||||||
ZoneScopedN("Debug ImGui");
|
ZoneScopedN("Debug ImGui");
|
||||||
|
SceneManager::GetInstance().RenderImgui();
|
||||||
|
|
||||||
ImGui::Begin("Debug");
|
ImGui::Begin("Debug");
|
||||||
ImGui::Text("FPS: %.2f", avgFPS);
|
ImGui::Text("FPS: %.2f", avgFPS);
|
||||||
ImGui::End();
|
ImGui::End();
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include <glm/gtc/matrix_transform.hpp>
|
#include <glm/gtc/matrix_transform.hpp>
|
||||||
#include <glm/gtc/quaternion.hpp>
|
#include <glm/gtc/quaternion.hpp>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <imgui.h>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
|
||||||
@@ -327,24 +328,58 @@ void Animator::Update(float dt) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Animator::ImGuiInspector() {
|
void Animator::ImGuiInspector() {
|
||||||
// ImGui::Text("Clip: %s", m_currentClipName.empty() ? "(none)" : m_currentClipName.c_str());
|
if (m_skeletonAsset.empty()) {
|
||||||
// ImGui::Text("Time: %.2f", m_current.time);
|
ImGui::TextDisabled("Skeleton: <none>");
|
||||||
// ImGui::Text("Blend: %.2f", m_blendT);
|
} else {
|
||||||
//
|
ImGui::TextWrapped("Skeleton: %s", m_skeletonAsset.path.c_str());
|
||||||
// if (!isPlaying()) {
|
if (!m_skeletonAsset.subresource.empty()) {
|
||||||
// ImGui::BeginDisabled();
|
ImGui::TextWrapped(
|
||||||
// ImGui::Button("Stop");
|
"Skeleton subresource: %s",
|
||||||
// ImGui::EndDisabled();
|
m_skeletonAsset.subresource.c_str());
|
||||||
// } else if (ImGui::Button("Stop")) {
|
}
|
||||||
// stop();
|
}
|
||||||
// }
|
ImGui::Text("Joints: %zu", m_skeleton.joints.size());
|
||||||
//
|
|
||||||
// ImGui::Separator();
|
if (!m_referencesResolved) {
|
||||||
// ImGui::Text("Clips:");
|
ImGui::TextDisabled("Asset references are waiting to be resolved.");
|
||||||
// for (auto& [name, _] : m_clips) {
|
}
|
||||||
// if (ImGui::Selectable(name.c_str(), name == m_currentClipName))
|
|
||||||
// play(name);
|
if (m_current.clip == nullptr) {
|
||||||
// }
|
ImGui::TextDisabled("Playback: stopped");
|
||||||
|
} else {
|
||||||
|
ImGui::Text("Playing: %s", m_currentClipName.c_str());
|
||||||
|
float speed = m_current.speed;
|
||||||
|
if (ImGui::DragFloat("Playback speed", &speed, 0.05f)) {
|
||||||
|
m_current.speed = speed;
|
||||||
|
}
|
||||||
|
ImGui::Text("Time: %.2f / %.2f s", m_current.time, m_current.clip->duration);
|
||||||
|
ImGui::Text("Blend: %.2f", m_blendT);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool wasPlaying = isPlaying();
|
||||||
|
if (!wasPlaying) {
|
||||||
|
ImGui::BeginDisabled();
|
||||||
|
}
|
||||||
|
if (ImGui::Button("Stop")) {
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
if (!wasPlaying) {
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::Separator();
|
||||||
|
ImGui::Text("Clips: %zu", m_clips.size());
|
||||||
|
for (const std::string& name : m_clipOrder) {
|
||||||
|
if (!m_clips.contains(name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::PushID(name.c_str());
|
||||||
|
if (ImGui::Selectable(name.c_str(), name == m_currentClipName)) {
|
||||||
|
play(name, 0.15f);
|
||||||
|
}
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Animator::addClip(std::shared_ptr<SkeletalAnimation> clip) {
|
void Animator::addClip(std::shared_ptr<SkeletalAnimation> clip) {
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
#include "destrum/ObjectModel/GameObject.h"
|
#include "destrum/ObjectModel/GameObject.h"
|
||||||
#include "destrum/Util/GameState.h"
|
#include "destrum/Util/GameState.h"
|
||||||
|
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
|
|
||||||
MeshRendererComponent::MeshRendererComponent(GameObject& parent): Component(parent, "MeshRendererComponent") {
|
MeshRendererComponent::MeshRendererComponent(GameObject& parent): Component(parent, "MeshRendererComponent") {
|
||||||
@@ -139,6 +141,30 @@ void MeshRendererComponent::ResolveReferences(const ObjectMap&) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MeshRendererComponent::ImGuiInspector() {
|
||||||
|
if (meshID == NULL_MESH_ID) {
|
||||||
|
ImGui::TextDisabled("Mesh: <none>");
|
||||||
|
} else {
|
||||||
|
ImGui::Text("Mesh ID: %zu", meshID);
|
||||||
|
ImGui::TextWrapped(
|
||||||
|
"Mesh key: %s",
|
||||||
|
meshKey.empty() ? "<not resolved>" : meshKey.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (materialID == NULL_MATERIAL_ID) {
|
||||||
|
ImGui::TextDisabled("Material: <none>");
|
||||||
|
} else {
|
||||||
|
ImGui::Text("Material ID: %u", materialID);
|
||||||
|
ImGui::TextWrapped(
|
||||||
|
"Material key: %s",
|
||||||
|
materialKey.empty() ? "<not resolved>" : materialKey.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::Text(
|
||||||
|
"Skinning: %s",
|
||||||
|
m_skinnedMesh != nullptr ? "active" : "static");
|
||||||
|
}
|
||||||
|
|
||||||
void MeshRendererComponent::SetMeshID(MeshID id) {
|
void MeshRendererComponent::SetMeshID(MeshID id) {
|
||||||
meshID = id;
|
meshID = id;
|
||||||
meshKey.clear();
|
meshKey.clear();
|
||||||
|
|||||||
@@ -9,6 +9,11 @@
|
|||||||
#include "destrum/Components/MeshRendererComponent.h"
|
#include "destrum/Components/MeshRendererComponent.h"
|
||||||
#include "destrum/Util/GameState.h"
|
#include "destrum/Util/GameState.h"
|
||||||
#include "destrum/Util/DeltaTime.h"
|
#include "destrum/Util/DeltaTime.h"
|
||||||
|
#include "destrum/Util/ImGuiUtils.h"
|
||||||
|
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
static glm::vec3 RandomUnitVector(std::mt19937& rng)
|
static glm::vec3 RandomUnitVector(std::mt19937& rng)
|
||||||
{
|
{
|
||||||
@@ -175,3 +180,64 @@ void OrbitAndSpin::Deserialize(const nlohmann::json& data) {
|
|||||||
}
|
}
|
||||||
BuildOrbitBasis();
|
BuildOrbitBasis();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void OrbitAndSpin::ImGuiInspector() {
|
||||||
|
float radius = m_Radius;
|
||||||
|
if (ImGui::DragFloat("Radius", &radius, 0.05f, 0.0f)) {
|
||||||
|
SetRadius(radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
glm::vec3 center = m_Center;
|
||||||
|
if (ImGuiUtils::DrawVec3Control("Center", center)) {
|
||||||
|
SetCenter(center);
|
||||||
|
}
|
||||||
|
|
||||||
|
glm::vec3 orbitAxis = m_OrbitAxis;
|
||||||
|
if (ImGuiUtils::DrawVec3Control("Orbit axis", orbitAxis)) {
|
||||||
|
if (glm::dot(orbitAxis, orbitAxis) >= 1e-8f) {
|
||||||
|
m_OrbitAxis = glm::normalize(orbitAxis);
|
||||||
|
BuildOrbitBasis();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float orbitSpeed = m_OrbitSpeed;
|
||||||
|
if (ImGui::DragFloat("Orbit speed", &orbitSpeed, 0.05f)) {
|
||||||
|
m_OrbitSpeed = orbitSpeed;
|
||||||
|
}
|
||||||
|
ImGui::DragFloat("Orbit phase", &m_OrbitPhase, 0.05f);
|
||||||
|
ImGui::Text("Orbit angle: %.2f rad", m_OrbitAngle);
|
||||||
|
|
||||||
|
float growSpeed = m_GrowSpeed;
|
||||||
|
if (ImGui::DragFloat("Grow speed", &growSpeed, 0.05f, 0.0f)) {
|
||||||
|
m_GrowSpeed = growSpeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
float growMin = m_GrowMin;
|
||||||
|
if (ImGui::DragFloat("Grow min", &growMin, 0.01f, 0.0f)) {
|
||||||
|
m_GrowMin = std::min(growMin, m_GrowMax);
|
||||||
|
}
|
||||||
|
|
||||||
|
float growMax = m_GrowMax;
|
||||||
|
if (ImGui::DragFloat("Grow max", &growMax, 0.01f, 0.0f)) {
|
||||||
|
m_GrowMax = std::max(growMax, m_GrowMin);
|
||||||
|
}
|
||||||
|
|
||||||
|
glm::vec3 baseScale = m_BaseScale;
|
||||||
|
if (ImGuiUtils::DrawVec3Control("Base scale", baseScale, 0.05f)) {
|
||||||
|
m_BaseScale = baseScale;
|
||||||
|
}
|
||||||
|
|
||||||
|
glm::vec3 spinAxis = m_SpinAxis;
|
||||||
|
if (ImGuiUtils::DrawVec3Control("Spin axis", spinAxis)) {
|
||||||
|
if (glm::dot(spinAxis, spinAxis) >= 1e-8f) {
|
||||||
|
m_SpinAxis = glm::normalize(spinAxis);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float spinSpeed = m_SpinSpeed;
|
||||||
|
if (ImGui::DragFloat("Spin speed", &spinSpeed, 0.05f)) {
|
||||||
|
m_SpinSpeed = spinSpeed;
|
||||||
|
}
|
||||||
|
ImGui::Text("Grow phase: %.2f rad", m_GrowPhase);
|
||||||
|
ImGui::Text("Material ID: %u", m_MaterialID);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
#include <destrum/Components/Physics/BoxCollider.h>
|
#include <destrum/Components/Physics/BoxCollider.h>
|
||||||
|
|
||||||
|
#include <destrum/Util/ImGuiUtils.h>
|
||||||
|
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
nlohmann::json BoxCollider::Serialize() const {
|
nlohmann::json BoxCollider::Serialize() const {
|
||||||
auto data = SerializeCollider();
|
auto data = SerializeCollider();
|
||||||
data["halfExtents"] = {m_HalfExtents.x, m_HalfExtents.y, m_HalfExtents.z};
|
data["halfExtents"] = {m_HalfExtents.x, m_HalfExtents.y, m_HalfExtents.z};
|
||||||
@@ -17,3 +21,12 @@ void BoxCollider::Deserialize(const nlohmann::json& data) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void BoxCollider::ImGuiInspector() {
|
||||||
|
Collider::ImGuiInspector();
|
||||||
|
|
||||||
|
glm::vec3 halfExtents = m_HalfExtents;
|
||||||
|
if (ImGuiUtils::DrawVec3Control("Half extents", halfExtents)) {
|
||||||
|
SetHalfExtents(halfExtents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
#include <destrum/Components/Physics/CapsuleCollider.h>
|
||||||
|
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
|
void CapsuleCollider::ImGuiInspector() {
|
||||||
|
Collider::ImGuiInspector();
|
||||||
|
|
||||||
|
float radius = m_Radius;
|
||||||
|
if (ImGui::DragFloat("Radius", &radius, 0.05f, 0.0001f)) {
|
||||||
|
SetRadius(radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
float height = m_Height;
|
||||||
|
if (ImGui::DragFloat("Height", &height, 0.05f, 0.0001f)) {
|
||||||
|
SetHeight(height);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
#include <destrum/Components/Physics/Collider.h>
|
||||||
|
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
|
#include <destrum/Util/ImGuiUtils.h>
|
||||||
|
|
||||||
|
void Collider::ImGuiInspector() {
|
||||||
|
bool isTrigger = m_IsTrigger;
|
||||||
|
if (ImGui::Checkbox("Is trigger", &isTrigger)) {
|
||||||
|
SetTrigger(isTrigger);
|
||||||
|
}
|
||||||
|
|
||||||
|
glm::vec3 centerOffset = m_CenterOffset;
|
||||||
|
if (ImGuiUtils::DrawVec3Control("Center offset", centerOffset)) {
|
||||||
|
SetCenterOffset(centerOffset);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@
|
|||||||
#include <destrum/ObjectModel/GameObject.h>
|
#include <destrum/ObjectModel/GameObject.h>
|
||||||
#include <destrum/Scene/Scene.h>
|
#include <destrum/Scene/Scene.h>
|
||||||
|
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
[[nodiscard]] const char* RigidbodyTypeName(RigidbodyType type) {
|
[[nodiscard]] const char* RigidbodyTypeName(RigidbodyType type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -128,3 +130,48 @@ void Rigidbody::Deserialize(const nlohmann::json& data) {
|
|||||||
m_AllowSleep = data.at("allowSleep").get<bool>();
|
m_AllowSleep = data.at("allowSleep").get<bool>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Rigidbody::ImGuiInspector() {
|
||||||
|
static const char* typeNames[] = {"Static", "Dynamic", "Kinematic"};
|
||||||
|
int type = static_cast<int>(m_Type);
|
||||||
|
if (ImGui::Combo("Body type", &type, typeNames, IM_ARRAYSIZE(typeNames))) {
|
||||||
|
SetType(static_cast<RigidbodyType>(type));
|
||||||
|
}
|
||||||
|
|
||||||
|
float mass = m_Mass;
|
||||||
|
if (ImGui::DragFloat("Mass", &mass, 0.1f, 0.0001f)) {
|
||||||
|
SetMass(mass);
|
||||||
|
}
|
||||||
|
|
||||||
|
float friction = m_Friction;
|
||||||
|
if (ImGui::DragFloat("Friction", &friction, 0.01f, 0.0f)) {
|
||||||
|
SetFriction(friction);
|
||||||
|
}
|
||||||
|
|
||||||
|
float restitution = m_Restitution;
|
||||||
|
if (ImGui::SliderFloat("Restitution", &restitution, 0.0f, 1.0f)) {
|
||||||
|
SetRestitution(restitution);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool useGravity = m_UseGravity;
|
||||||
|
if (ImGui::Checkbox("Use gravity", &useGravity)) {
|
||||||
|
SetUseGravity(useGravity);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool allowSleep = m_AllowSleep;
|
||||||
|
if (ImGui::Checkbox("Allow sleep", &allowSleep)) {
|
||||||
|
SetAllowSleep(allowSleep);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (HasPhysicsBody()) {
|
||||||
|
const glm::vec3 velocity = GetLinearVelocity();
|
||||||
|
ImGui::Text(
|
||||||
|
"Linear velocity: %.2f, %.2f, %.2f",
|
||||||
|
velocity.x,
|
||||||
|
velocity.y,
|
||||||
|
velocity.z);
|
||||||
|
ImGui::Text("Physics body: %u", m_Body.id);
|
||||||
|
} else {
|
||||||
|
ImGui::TextDisabled("Physics body: not registered");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#include <destrum/Components/Physics/SphereCollider.h>
|
#include <destrum/Components/Physics/SphereCollider.h>
|
||||||
|
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
nlohmann::json SphereCollider::Serialize() const {
|
nlohmann::json SphereCollider::Serialize() const {
|
||||||
auto data = SerializeCollider();
|
auto data = SerializeCollider();
|
||||||
data["radius"] = m_Radius;
|
data["radius"] = m_Radius;
|
||||||
@@ -12,3 +14,12 @@ void SphereCollider::Deserialize(const nlohmann::json& data) {
|
|||||||
SetRadius(data.at("radius").get<float>());
|
SetRadius(data.at("radius").get<float>());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SphereCollider::ImGuiInspector() {
|
||||||
|
Collider::ImGuiInspector();
|
||||||
|
|
||||||
|
float radius = m_Radius;
|
||||||
|
if (ImGui::DragFloat("Radius", &radius, 0.05f, 0.0001f)) {
|
||||||
|
SetRadius(radius);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
#include <glm/gtc/quaternion.hpp> // glm::quat, glm::angleAxis
|
#include <glm/gtc/quaternion.hpp> // glm::quat, glm::angleAxis
|
||||||
#include <glm/gtx/quaternion.hpp> // operator*(quat, vec3)
|
#include <glm/gtx/quaternion.hpp> // operator*(quat, vec3)
|
||||||
#include <destrum/Util/DeltaTime.h>
|
#include <destrum/Util/DeltaTime.h>
|
||||||
|
#include <destrum/Util/ImGuiUtils.h>
|
||||||
|
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
glm::vec3 Rotator::MakePerpendicularUnitVector(const glm::vec3& axis)
|
glm::vec3 Rotator::MakePerpendicularUnitVector(const glm::vec3& axis)
|
||||||
{
|
{
|
||||||
@@ -128,3 +131,27 @@ void Rotator::Deserialize(const nlohmann::json& data) {
|
|||||||
m_InitialOffset = {value.at(0).get<float>(), value.at(1).get<float>(), value.at(2).get<float>()};
|
m_InitialOffset = {value.at(0).get<float>(), value.at(1).get<float>(), value.at(2).get<float>()};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Rotator::ImGuiInspector() {
|
||||||
|
float distance = m_Distance;
|
||||||
|
if (ImGui::DragFloat("Distance", &distance, 0.05f, 0.0f)) {
|
||||||
|
SetDistance(distance);
|
||||||
|
}
|
||||||
|
|
||||||
|
float speed = m_Speed;
|
||||||
|
if (ImGui::DragFloat("Speed", &speed, 0.05f)) {
|
||||||
|
SetSpeed(speed);
|
||||||
|
}
|
||||||
|
|
||||||
|
glm::vec3 pivot = m_Pivot;
|
||||||
|
if (ImGuiUtils::DrawVec3Control("Pivot", pivot)) {
|
||||||
|
SetPivotPosition(pivot);
|
||||||
|
}
|
||||||
|
|
||||||
|
glm::vec3 axis = m_Axis;
|
||||||
|
if (ImGuiUtils::DrawVec3Control("Axis", axis)) {
|
||||||
|
SetAxis(axis);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::Text("Current angle: %.2f rad", m_CurrentAngle);
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
|
|
||||||
#include "destrum/ObjectModel/Transform.h"
|
#include "destrum/ObjectModel/Transform.h"
|
||||||
#include "destrum/Util/DeltaTime.h"
|
#include "destrum/Util/DeltaTime.h"
|
||||||
|
#include "destrum/Util/ImGuiUtils.h"
|
||||||
|
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
void Spinner::Update(float dt)
|
void Spinner::Update(float dt)
|
||||||
{
|
{
|
||||||
@@ -33,3 +36,17 @@ void Spinner::Deserialize(const nlohmann::json& data) {
|
|||||||
m_Angle = data.at("angle").get<float>();
|
m_Angle = data.at("angle").get<float>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Spinner::ImGuiInspector() {
|
||||||
|
glm::vec3 axis = m_Axis;
|
||||||
|
if (ImGuiUtils::DrawVec3Control("Axis", axis)) {
|
||||||
|
SetAxis(axis);
|
||||||
|
}
|
||||||
|
|
||||||
|
float speed = m_Speed;
|
||||||
|
if (ImGui::DragFloat("Speed", &speed, 0.05f)) {
|
||||||
|
SetSpeed(speed);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::Text("Current angle: %.2f rad", m_Angle);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#include <destrum/Scene/Scene.h>
|
#include <destrum/Scene/Scene.h>
|
||||||
#include <destrum/ObjectModel/GameObject.h>
|
#include <destrum/ObjectModel/GameObject.h>
|
||||||
#include <destrum/Util/DeltaTime.h>
|
#include <destrum/Util/ImGuiUtils.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
@@ -160,6 +160,11 @@ void Scene::Render(const RenderContext& ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Scene::RenderImgui() {
|
void Scene::RenderImgui() {
|
||||||
|
ImGuiUtils::RenderSceneInspector(
|
||||||
|
m_name,
|
||||||
|
m_objects,
|
||||||
|
m_pendingAdditions,
|
||||||
|
m_SelectedObjectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Scene::CleanupDestroyedGameObjects() {
|
void Scene::CleanupDestroyedGameObjects() {
|
||||||
|
|||||||
@@ -0,0 +1,302 @@
|
|||||||
|
#include <destrum/Util/ImGuiUtils.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cfloat>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_set>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
|
#include <glm/gtc/type_ptr.hpp>
|
||||||
|
#include <glm/gtx/quaternion.hpp>
|
||||||
|
|
||||||
|
#include <destrum/ObjectModel/Component.h>
|
||||||
|
#include <destrum/ObjectModel/GameObject.h>
|
||||||
|
#include <destrum/ObjectModel/Transform.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
[[nodiscard]] bool IsInspectableObject(const GameObject* object) {
|
||||||
|
return object != nullptr && !object->IsBeingDestroyed();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool HasInspectableChildren(const GameObject& object) {
|
||||||
|
return std::ranges::any_of(object.GetTransform().GetChildren(),
|
||||||
|
[](const Transform* child) {
|
||||||
|
return child != nullptr && IsInspectableObject(child->GetOwner());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawGameObjectNode(
|
||||||
|
GameObject& object,
|
||||||
|
ObjectId& selectedObjectId,
|
||||||
|
std::unordered_set<const GameObject*>& visited,
|
||||||
|
bool defaultOpen = false) {
|
||||||
|
if (!IsInspectableObject(&object) || !visited.insert(&object).second) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool hasChildren = HasInspectableChildren(object);
|
||||||
|
ImGuiTreeNodeFlags treeFlags = ImGuiTreeNodeFlags_SpanAvailWidth;
|
||||||
|
if (object.GetId() == selectedObjectId) {
|
||||||
|
treeFlags |= ImGuiTreeNodeFlags_Selected;
|
||||||
|
}
|
||||||
|
if (defaultOpen) {
|
||||||
|
treeFlags |= ImGuiTreeNodeFlags_DefaultOpen;
|
||||||
|
}
|
||||||
|
if (!hasChildren) {
|
||||||
|
treeFlags |= ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string displayName = object.GetName().empty()
|
||||||
|
? "<unnamed>"
|
||||||
|
: object.GetName();
|
||||||
|
const bool isOpen = ImGui::TreeNodeEx(
|
||||||
|
static_cast<const void*>(&object),
|
||||||
|
treeFlags,
|
||||||
|
"%s",
|
||||||
|
displayName.c_str());
|
||||||
|
|
||||||
|
if (ImGui::IsItemClicked()) {
|
||||||
|
selectedObjectId = object.GetId();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!object.IsActive()) {
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::TextDisabled("[inactive]");
|
||||||
|
} else if (!object.IsActiveInHierarchy()) {
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::TextDisabled("[parent inactive]");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOpen && hasChildren) {
|
||||||
|
for (Transform* childTransform : object.GetTransform().GetChildren()) {
|
||||||
|
if (childTransform == nullptr) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
GameObject* child = childTransform->GetOwner();
|
||||||
|
if (IsInspectableObject(child)) {
|
||||||
|
DrawGameObjectNode(*child, selectedObjectId, visited);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::TreePop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] GameObject* FindSelectedObject(
|
||||||
|
const std::vector<GameObject*>& objects,
|
||||||
|
ObjectId selectedObjectId) {
|
||||||
|
const auto it = std::find_if(
|
||||||
|
objects.begin(),
|
||||||
|
objects.end(),
|
||||||
|
[selectedObjectId](const GameObject* object) {
|
||||||
|
return object != nullptr && object->GetId() == selectedObjectId;
|
||||||
|
});
|
||||||
|
return it == objects.end() ? nullptr : *it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace ImGuiUtils {
|
||||||
|
bool DrawVec3Control(const char* label, glm::vec3& value, float speed) {
|
||||||
|
if (label == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::PushID(label);
|
||||||
|
ImGui::AlignTextToFramePadding();
|
||||||
|
ImGui::TextUnformatted(label);
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::SetNextItemWidth(-FLT_MIN);
|
||||||
|
const bool changed = ImGui::DragFloat3(
|
||||||
|
"##value",
|
||||||
|
glm::value_ptr(value),
|
||||||
|
speed);
|
||||||
|
ImGui::PopID();
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderSceneInspector(
|
||||||
|
std::string_view sceneName,
|
||||||
|
const std::vector<std::shared_ptr<GameObject>>& objectsSource,
|
||||||
|
const std::vector<std::shared_ptr<GameObject>>& pendingAdditions,
|
||||||
|
ObjectId& selectedObjectId) {
|
||||||
|
std::vector<GameObject*> objects;
|
||||||
|
objects.reserve(objectsSource.size() + pendingAdditions.size());
|
||||||
|
|
||||||
|
const auto appendInspectableObjects = [&objects](const auto& source) {
|
||||||
|
for (const auto& object : source) {
|
||||||
|
if (IsInspectableObject(object.get())) {
|
||||||
|
objects.push_back(object.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
appendInspectableObjects(objectsSource);
|
||||||
|
appendInspectableObjects(pendingAdditions);
|
||||||
|
|
||||||
|
std::vector<GameObject*> roots;
|
||||||
|
roots.reserve(objects.size());
|
||||||
|
for (GameObject* object : objects) {
|
||||||
|
Transform* parent = object->GetTransform().GetParent();
|
||||||
|
if (parent == nullptr || !IsInspectableObject(parent->GetOwner())) {
|
||||||
|
roots.push_back(object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (FindSelectedObject(objects, selectedObjectId) == nullptr) {
|
||||||
|
selectedObjectId = InvalidObjectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SetNextWindowSize(ImVec2(640.0f, 520.0f), ImGuiCond_FirstUseEver);
|
||||||
|
if (!ImGui::Begin("Inspector")) {
|
||||||
|
ImGui::End();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string displaySceneName = sceneName.empty()
|
||||||
|
? "<unnamed>"
|
||||||
|
: std::string{sceneName};
|
||||||
|
ImGui::Text("Scene: %s", displaySceneName.c_str());
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::TextDisabled("%zu GameObjects | %zu roots", objects.size(), roots.size());
|
||||||
|
ImGui::Separator();
|
||||||
|
|
||||||
|
const float availableWidth = ImGui::GetContentRegionAvail().x;
|
||||||
|
const float hierarchyWidth = std::max(180.0f, availableWidth * 0.45f);
|
||||||
|
|
||||||
|
if (ImGui::BeginChild(
|
||||||
|
"SceneHierarchy",
|
||||||
|
ImVec2(hierarchyWidth, 0.0f),
|
||||||
|
ImGuiChildFlags_Borders)) {
|
||||||
|
std::unordered_set<const GameObject*> visited;
|
||||||
|
visited.reserve(objects.size());
|
||||||
|
for (GameObject* root : roots) {
|
||||||
|
DrawGameObjectNode(*root, selectedObjectId, visited, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep malformed or partially loaded hierarchies visible instead of
|
||||||
|
// silently dropping objects whose parent is not in this list.
|
||||||
|
bool hasUnvisitedObjects = false;
|
||||||
|
for (GameObject* object : objects) {
|
||||||
|
if (!visited.contains(object)) {
|
||||||
|
if (!hasUnvisitedObjects) {
|
||||||
|
ImGui::Separator();
|
||||||
|
ImGui::TextDisabled("OTHER OBJECTS");
|
||||||
|
hasUnvisitedObjects = true;
|
||||||
|
}
|
||||||
|
DrawGameObjectNode(*object, selectedObjectId, visited);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (objects.empty()) {
|
||||||
|
ImGui::TextDisabled("This scene has no GameObjects.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
|
||||||
|
GameObject* selectedObject = FindSelectedObject(objects, selectedObjectId);
|
||||||
|
if (selectedObject == nullptr) {
|
||||||
|
selectedObjectId = InvalidObjectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SameLine();
|
||||||
|
|
||||||
|
if (ImGui::BeginChild(
|
||||||
|
"ObjectInspector",
|
||||||
|
ImVec2(0.0f, 0.0f),
|
||||||
|
ImGuiChildFlags_Borders)) {
|
||||||
|
if (selectedObject == nullptr) {
|
||||||
|
ImGui::TextDisabled("Select a GameObject to inspect it.");
|
||||||
|
} else {
|
||||||
|
const std::string displayName = selectedObject->GetName().empty()
|
||||||
|
? "<unnamed>"
|
||||||
|
: selectedObject->GetName();
|
||||||
|
ImGui::TextUnformatted(displayName.c_str());
|
||||||
|
ImGui::TextDisabled(
|
||||||
|
"GameObject | ID %llu",
|
||||||
|
static_cast<unsigned long long>(selectedObject->GetId()));
|
||||||
|
ImGui::Separator();
|
||||||
|
|
||||||
|
bool active = selectedObject->IsActive();
|
||||||
|
if (ImGui::Checkbox("Active", &active)) {
|
||||||
|
selectedObject->SetActive(active);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Transform* parent = selectedObject->GetTransform().GetParent();
|
||||||
|
if (parent != nullptr && parent->GetOwner() != nullptr) {
|
||||||
|
ImGui::Text("Parent: %s", parent->GetOwner()->GetName().c_str());
|
||||||
|
} else {
|
||||||
|
ImGui::Text("Parent: <scene root>");
|
||||||
|
}
|
||||||
|
ImGui::Text("Children: %d", selectedObject->GetTransform().GetChildCount());
|
||||||
|
|
||||||
|
if (ImGui::CollapsingHeader(
|
||||||
|
"Transform",
|
||||||
|
ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||||
|
Transform& transform = selectedObject->GetTransform();
|
||||||
|
bool transformChanged = false;
|
||||||
|
|
||||||
|
glm::vec3 localPosition = transform.GetLocalPosition();
|
||||||
|
if (DrawVec3Control("Position", localPosition, 0.05f)) {
|
||||||
|
transform.SetLocalPosition(localPosition);
|
||||||
|
transformChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
glm::vec3 localRotation = glm::degrees(
|
||||||
|
glm::eulerAngles(transform.GetLocalRotation()));
|
||||||
|
if (DrawVec3Control("Rotation", localRotation, 0.5f)) {
|
||||||
|
transform.SetLocalRotation(localRotation);
|
||||||
|
transformChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
glm::vec3 localScale = transform.GetLocalScale();
|
||||||
|
if (DrawVec3Control("Scale", localScale, 0.05f)) {
|
||||||
|
transform.SetLocalScale(localScale);
|
||||||
|
transformChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transformChanged) {
|
||||||
|
selectedObject->RefreshPhysics();
|
||||||
|
}
|
||||||
|
|
||||||
|
const glm::vec3& worldPosition = transform.GetWorldPosition();
|
||||||
|
ImGui::Text(
|
||||||
|
"World position: %.2f, %.2f, %.2f",
|
||||||
|
worldPosition.x,
|
||||||
|
worldPosition.y,
|
||||||
|
worldPosition.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui::CollapsingHeader(
|
||||||
|
"Components",
|
||||||
|
ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||||
|
bool hasComponents = false;
|
||||||
|
for (const auto& component : selectedObject->GetComponents()) {
|
||||||
|
if (component == nullptr || component->IsBeingDestroyed()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasComponents = true;
|
||||||
|
ImGui::PushID(component.get());
|
||||||
|
const std::string typeName = component->GetTypeName();
|
||||||
|
if (ImGui::CollapsingHeader(typeName.c_str())) {
|
||||||
|
bool enabled = component->isEnabled();
|
||||||
|
if (ImGui::Checkbox("Enabled", &enabled)) {
|
||||||
|
component->SetEnabled(enabled);
|
||||||
|
}
|
||||||
|
component->ImGuiInspector();
|
||||||
|
}
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
if (!hasComponents) {
|
||||||
|
ImGui::TextDisabled("No components.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
|
||||||
|
ImGui::End();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user