Files
HelloVulkan/App/editor/InspectorPanel.cpp
T
atdunbg 6c81bfd70d physics: rigid body drive API (force/impulse/torque/velocity)
PhysicsWorld facade methods addressed by entity bits via body records:
AddForce/AddImpulse/AddTorque (dynamic bodies only, auto-wake), Set/
GetLinearVelocity/SetAngularVelocity (dynamic + kinematic). Inspector
RigidBody block gains impulse debug controls for manual response tests.
Transient by design — nothing serialized.
2026-10-03 16:10:22 +08:00

327 lines
12 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// InspectorPanel.cpp:组件编辑实现。
//
// Transform 编辑必须走 Set* 入口(标脏 → 帧循环统一 Compose 重算
// world),直接改私有成员是编译错误——面板天然遵守引擎约束。
//
#include "InspectorPanel.h"
#include "Engine/ECS/Components.h"
#include "Engine/Renderer/Model.h"
#include "Engine/Physics/PhysicsWorld.h" // Apply Impulse 调试(GetPhysics 门面)
#include <imgui.h>
#include <cstdio>
#include <vector>
namespace
{
// InputText 需要 char 缓冲;提交时写回 std::string
bool InputTextString(const char* label, std::string& str)
{
char buf[256];
std::snprintf(buf, sizeof(buf), "%s", str.c_str());
if (ImGui::InputText(label, buf, sizeof(buf)))
{
str = buf;
return true;
}
return false;
}
// UE 风变换行:label + 三轴色 DragFloat(轴色对齐 Gizmo:X 红 /
// Y 绿 / Z 蓝)。拖拽粗调;Ctrl+Click 进输入框键入精确值。
// 任一轴变化返回 true
bool DragVec3Row(const char* label, glm::vec3& v, float speed,
float mn = -3.4e38f, float mx = 3.4e38f)
{
ImGui::TextUnformatted(label);
ImGui::SameLine(72.0f); // 固定标签列宽 → 三行输入框对齐
ImGui::PushID(label);
constexpr ImVec4 axisCol[3] = {
{0.72f, 0.24f, 0.24f, 1.00f}, // X 红
{0.30f, 0.68f, 0.34f, 1.00f}, // Y 绿
{0.24f, 0.40f, 0.88f, 1.00f}, // Z 蓝
};
const float boxW =
(ImGui::GetContentRegionAvail().x
- 2.0f * ImGui::GetStyle().ItemInnerSpacing.x) / 3.0f;
bool changed = false;
for (int i = 0; i < 3; ++i)
{
ImGui::PushStyleColor(ImGuiCol_FrameBg, axisCol[i]);
ImGui::SetNextItemWidth(boxW);
changed |= ImGui::DragFloat(
i == 0 ? "##x" : i == 1 ? "##y" : "##z",
&v[i], speed, mn, mx, "%.2f");
ImGui::PopStyleColor();
if (i < 2) ImGui::SameLine();
}
ImGui::PopID();
return changed;
}
}
void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
{
ImGui::Begin("Inspector");
auto& reg = ctx.scene->GetRegistry();
if (selected == entt::null || !reg.valid(selected))
{
ImGui::TextDisabled("(no entity selected)");
ImGui::End();
return;
}
// ---- Name ----
if (auto* name = reg.try_get<Engine::ECS::NameComponent>(selected))
{
InputTextString("Name", name->name);
}
ImGui::Separator();
// ---- Add Component ----
if (ImGui::Button("Add Component"))
{
ImGui::OpenPopup("add_component");
}
if (ImGui::BeginPopup("add_component"))
{
if (!reg.all_of<Engine::ECS::LightComponent>(selected) &&
ImGui::MenuItem("Light"))
{
reg.emplace<Engine::ECS::LightComponent>(selected);
}
if (!reg.all_of<Engine::ECS::CameraComponent>(selected) &&
ImGui::MenuItem("Camera"))
{
reg.emplace<Engine::ECS::CameraComponent>(selected);
}
if (!reg.all_of<Engine::ECS::AnimationComponent>(selected) &&
ImGui::MenuItem("Animation"))
{
reg.emplace<Engine::ECS::AnimationComponent>(selected);
}
if (!reg.all_of<Engine::ECS::NameComponent>(selected) &&
ImGui::MenuItem("Name"))
{
reg.emplace<Engine::ECS::NameComponent>(selected);
}
if (!reg.all_of<Engine::ECS::RigidBodyComponent>(selected) &&
ImGui::MenuItem("RigidBody"))
{
reg.emplace<Engine::ECS::RigidBodyComponent>(selected);
}
ImGui::EndPopup();
}
ImGui::Separator();
// ---- Transform ----
if (auto* t = reg.try_get<Engine::ECS::TransformComponent>(selected))
{
if (ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen))
{
glm::vec3 pos = t->GetPosition();
if (DragVec3Row("position", pos, 0.05f))
{
t->SetPosition(pos);
}
glm::vec3 rot = t->GetRotation(); // 派生欧拉(度)
if (DragVec3Row("rotation", rot, 0.5f))
{
t->SetRotation(rot);
}
glm::vec3 scl = t->GetScale();
if (DragVec3Row("scale", scl, 0.02f, 0.001f, 100.0f))
{
t->SetScale(scl);
}
}
}
// ---- MeshRenderer ----
if (auto* mr = reg.try_get<Engine::ECS::MeshRendererComponent>(selected))
{
if (ImGui::CollapsingHeader("MeshRenderer",
ImGuiTreeNodeFlags_DefaultOpen))
{
if (const Engine::Model* model = ctx.assets->GetModel(mr->model);
model != nullptr)
{
const auto& clips = model->GetClips();
ImGui::Text("model: valid (%u clips, %u bones)",
static_cast<unsigned>(clips.size()),
static_cast<unsigned>(model->GetBoneCount()));
ImGui::TextDisabled("(mesh asset is read-only here)");
}
else
{
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
"model: INVALID (dangling ref)");
}
}
}
// ---- Light ----
if (auto* light = reg.try_get<Engine::ECS::LightComponent>(selected))
{
if (ImGui::CollapsingHeader("Light", ImGuiTreeNodeFlags_DefaultOpen))
{
int type = static_cast<int>(light->type);
if (ImGui::Combo("type", &type, "Directional\0Point\0"))
{
light->type = static_cast<Engine::ECS::LightComponent::Type>(type);
}
ImGui::ColorEdit3("color", &light->color.x);
ImGui::DragFloat("intensity", &light->color.a, 0.05f, 0.0f, 100.0f);
if (light->type == Engine::ECS::LightComponent::Type::Directional)
{
ImGui::SliderFloat3("direction", &light->direction.x, -1.0f, 1.0f);
ImGui::Checkbox("castShadow", &light->castShadow);
}
else
{
ImGui::DragFloat("radius", &light->radius, 0.1f, 0.0f, 100.0f);
ImGui::TextDisabled("(position = entity Transform)");
}
}
}
// ---- Camera ----
if (auto* cam = reg.try_get<Engine::ECS::CameraComponent>(selected))
{
if (ImGui::CollapsingHeader("Camera", ImGuiTreeNodeFlags_DefaultOpen))
{
if (ImGui::Checkbox("primary", &cam->primary) && cam->primary)
{
// primary 互斥:启用本相机时清掉其余实体的 primary
//(引擎取第一个 primary 作为游戏形态渲染视角)
reg.view<Engine::ECS::CameraComponent>().each(
[&](entt::entity e, Engine::ECS::CameraComponent& c)
{
if (e != selected)
{
c.primary = false;
}
});
}
// 对齐跳转:把本实体位姿复制给编辑器相机,Scene view 立即
// 从该位置观看(只取位姿,不接管渲染——渲染源分派在
// SceneRenderer,编辑器形态恒用编辑器相机)
if (ImGui::Button("Align Viewport to Camera") && ctx.editorCamera)
{
if (auto* tr = reg.try_get<Engine::ECS::TransformComponent>(
selected))
{
ctx.editorCamera->AlignTo(*tr);
}
}
}
}
// ---- RigidBody(Jolt 刚体:动态体 Transform 由物理每帧写回)----
// type/collider/mass 变更 → PhysicsWorld 重建 body;其余参数即时生效
if (auto* rb = reg.try_get<Engine::ECS::RigidBodyComponent>(selected))
{
if (ImGui::CollapsingHeader("RigidBody", ImGuiTreeNodeFlags_DefaultOpen))
{
int type = static_cast<int>(rb->type);
if (ImGui::Combo("type", &type, "Static\0Dynamic\0Kinematic\0"))
{
rb->type = static_cast<Engine::ECS::RigidBodyComponent::BodyType>(type);
}
int collider = static_cast<int>(rb->collider);
if (ImGui::Combo("collider", &collider, "Box\0Sphere\0Mesh\0"))
{
rb->collider = static_cast<Engine::ECS::RigidBodyComponent::ColliderType>(collider);
}
if (rb->collider == Engine::ECS::RigidBodyComponent::ColliderType::Box)
{
ImGui::DragFloat3("halfExtents", &rb->boxHalfExtents.x, 0.05f, 0.01f, 500.0f);
}
else if (rb->collider == Engine::ECS::RigidBodyComponent::ColliderType::Sphere)
{
ImGui::DragFloat("radius", &rb->sphereRadius, 0.05f, 0.01f, 500.0f);
}
else
{
// Mesh:跟随 MeshRenderer 视觉网格(Static=精确网格,
// Dynamic=凸包近似),无尺寸可调
ImGui::TextDisabled("follows MeshRenderer geometry");
}
ImGui::DragFloat("mass", &rb->mass, 0.1f, 0.1f, 1000.0f, "%.1f");
ImGui::SliderFloat("gravityScale", &rb->gravityScale, -1.0f, 5.0f);
ImGui::SliderFloat("friction", &rb->friction, 0.0f, 2.0f);
ImGui::SliderFloat("restitution", &rb->restitution, 0.0f, 1.0f);
ImGui::Checkbox("isSensor", &rb->isSensor);
if (rb->bodyValid &&
rb->type == Engine::ECS::RigidBodyComponent::BodyType::Dynamic)
{
ImGui::Text("vel: %.2f, %.2f, %.2f m/s",
rb->velocity.x, rb->velocity.y, rb->velocity.z);
// 驱动调试(冲量瞬态不序列化):直接推一把看响应
static glm::vec3 s_debugImpulse{0.0f, 5.0f, 0.0f};
ImGui::DragFloat3("impulse", &s_debugImpulse.x, 0.1f);
if (ImGui::Button("Apply Impulse"))
{
ctx.app->GetPhysics().AddImpulse(
static_cast<uint32_t>(entt::to_integral(selected)),
s_debugImpulse);
}
}
}
}
// ---- Animation ----
if (auto* anim = reg.try_get<Engine::ECS::AnimationComponent>(selected))
{
if (ImGui::CollapsingHeader("Animation",
ImGuiTreeNodeFlags_DefaultOpen))
{
const auto* mr = reg.try_get<Engine::ECS::MeshRendererComponent>(selected);
const Engine::Model* model =
mr ? ctx.assets->GetModel(mr->model) : nullptr;
if (model != nullptr && model->HasAnimations())
{
const auto& clips = model->GetClips();
std::vector<const char*> clipNames;
clipNames.reserve(clips.size());
for (const auto& c : clips)
{
clipNames.push_back(c.name.c_str());
}
int cur = anim->clipIndex;
if (ImGui::Combo("clip", &cur, clipNames.data(),
static_cast<int>(clipNames.size())))
{
anim->Play(cur); // 触发 crossfade(fade 秒)
}
ImGui::Checkbox("playing", &anim->playing);
ImGui::SameLine();
ImGui::Checkbox("loop", &anim->loop);
ImGui::SliderFloat("speed", &anim->speed, -2.0f, 2.0f);
ImGui::SliderFloat("fade", &anim->fadeDuration, 0.0f, 1.0f);
if (anim->clipIndex >= 0 &&
anim->clipIndex < static_cast<int>(clips.size()))
{
ImGui::Text("t: %.2f / %.2f s", anim->time,
clips[anim->clipIndex].duration);
}
}
else
{
ImGui::TextDisabled("(no animation clips on model)");
}
}
}
ImGui::End();
}