Editor: group Details parameters and reorganize Stats panel

Details: multi-parameter components (RigidBody/Joint/CharacterController/
Animation) now use SeparatorText category headers in UE Details style;
all parameter labels normalized to Pascal Case.

Stats: rearranged into View / Scene / Rendering / Physics / Performance
collapsing groups. Clear Color moved to Scene, Debug View to Rendering,
GPU Timings + Frame merged into Performance; bare top-level widgets and
the now-obsolete Environment group removed.
This commit is contained in:
2026-10-05 01:20:00 +08:00
parent 432089747f
commit 5ca5ac2a1e
2 changed files with 141 additions and 123 deletions
+87 -63
View File
@@ -263,7 +263,7 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
}
}
glm::vec3 pos = t->GetPosition();
if (DragVec3Row("position", pos, 0.05f))
if (DragVec3Row("Position", pos, 0.05f))
{
t->SetPosition(pos);
}
@@ -315,7 +315,7 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
glm::vec3 rot = s_RotMirror;
// "%.1f" = UE Details 默认显示精度。mirror 值直显(无 wrap、
// 无限累计);Ctrl+Click 手输全精度写入
if (DragVec3Row("rotation", rot, 0.5f, -3.4e38f, 3.4e38f,
if (DragVec3Row("Rotation", rot, 0.5f, -3.4e38f, 3.4e38f,
"%.1f", &editingRot))
{
t->SetRotation(rot);
@@ -323,7 +323,7 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
}
s_RotEditingLast = editingRot;
glm::vec3 scl = t->GetScale();
if (DragVec3Row("scale", scl, 0.02f, 0.001f, 100.0f))
if (DragVec3Row("Scale", scl, 0.02f, 0.001f, 100.0f))
{
t->SetScale(scl);
}
@@ -426,20 +426,20 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
ImGui::BeginDisabled(true);
}
int type = static_cast<int>(light->type);
if (ImGui::Combo("type", &type, "Directional\0Point\0"))
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);
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);
ImGui::SliderFloat3("Direction", &light->direction.x, -1.0f, 1.0f);
ImGui::Checkbox("Cast Shadows", &light->castShadow);
}
else
{
ImGui::DragFloat("radius", &light->radius, 0.1f, 0.0f, 100.0f);
ImGui::DragFloat("Radius", &light->radius, 0.1f, 0.0f, 100.0f);
ImGui::TextDisabled("(position = entity Transform)");
}
if (!light->active)
@@ -513,7 +513,7 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
{
ImGui::BeginDisabled(true);
}
ImGui::ColorEdit4("ambient", &sl->ambientColor.x);
ImGui::ColorEdit4("Ambient", &sl->ambientColor.x);
if (!sl->active)
{
ImGui::EndDisabled();
@@ -535,7 +535,7 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
{
ImGui::BeginDisabled(true);
}
if (ImGui::Checkbox("primary", &cam->primary) && cam->primary)
if (ImGui::Checkbox("Primary", &cam->primary) && cam->primary)
{
// primary 互斥:启用本相机时清掉其余实体的 primary
//(引擎取第一个 primary 作为游戏形态渲染视角)
@@ -590,42 +590,46 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
"! conflicts with Character Controller "
"(disable or remove one)");
}
// ---- Body:类型与碰撞体选择(变更 → PhysicsWorld 重建 body)----
ImGui::SeparatorText("Body");
int type = static_cast<int>(rb->type);
if (ImGui::Combo("type", &type, "Static\0Dynamic\0Kinematic\0"))
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,
if (ImGui::Combo("Collider", &collider,
"Box\0Sphere\0Mesh\0Capsule\0Cylinder\0Compound\0"))
{
rb->collider = static_cast<Engine::ECS::RigidBodyComponent::ColliderType>(collider);
}
// ---- Shape:碰撞体尺寸(随类型切换;× Transform 缩放烘焙)----
ImGui::SeparatorText("Shape");
if (rb->collider == Engine::ECS::RigidBodyComponent::ColliderType::Box)
{
ImGui::DragFloat3("halfExtents", &rb->boxHalfExtents.x, 0.05f, 0.01f, 500.0f);
ImGui::DragFloat3("Half Extents", &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);
ImGui::DragFloat("Radius", &rb->sphereRadius, 0.05f, 0.01f, 500.0f);
}
else if (rb->collider == Engine::ECS::RigidBodyComponent::ColliderType::Capsule)
{
// 半高 = 圆柱段半高(不含半球端盖),总高 = 2*(halfHeight + radius)
ImGui::DragFloat("radius", &rb->capsuleRadius, 0.05f, 0.01f, 500.0f);
ImGui::DragFloat("halfHeight", &rb->capsuleHalfHeight, 0.05f, 0.0f, 500.0f);
ImGui::DragFloat("Radius", &rb->capsuleRadius, 0.05f, 0.01f, 500.0f);
ImGui::DragFloat("Half Height", &rb->capsuleHalfHeight, 0.05f, 0.0f, 500.0f);
}
else if (rb->collider == Engine::ECS::RigidBodyComponent::ColliderType::Cylinder)
{
ImGui::DragFloat("radius", &rb->cylinderRadius, 0.05f, 0.01f, 500.0f);
ImGui::DragFloat("halfHeight", &rb->cylinderHalfHeight, 0.05f, 0.01f, 500.0f);
ImGui::DragFloat("Radius", &rb->cylinderRadius, 0.05f, 0.01f, 500.0f);
ImGui::DragFloat("Half Height", &rb->cylinderHalfHeight, 0.05f, 0.01f, 500.0f);
}
else if (rb->collider == Engine::ECS::RigidBodyComponent::ColliderType::Compound)
{
// 子形状列表:尺寸/位置变更 → PhysicsWorld 重建 body(既有模式)
ImGui::Text("children: %d", static_cast<int>(rb->compound.size()));
ImGui::Text("Children: %d", static_cast<int>(rb->compound.size()));
ImGui::SameLine();
if (ImGui::Button("+ add"))
if (ImGui::Button("+ Add"))
{
rb->compound.emplace_back();
}
@@ -636,24 +640,24 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
ImGui::PushID(i);
ImGui::Separator();
int sh = static_cast<int>(c.shape);
if (ImGui::Combo("shape", &sh, "Box\0Sphere\0Capsule\0"))
if (ImGui::Combo("Shape", &sh, "Box\0Sphere\0Capsule\0"))
{
c.shape = static_cast<Engine::ECS::RigidBodyComponent::CompoundChild::Shape>(sh);
}
if (c.shape == Engine::ECS::RigidBodyComponent::CompoundChild::Shape::Box)
{
ImGui::DragFloat3("halfExtents", &c.boxHalfExtents.x, 0.05f, 0.01f, 500.0f);
ImGui::DragFloat3("Half Extents", &c.boxHalfExtents.x, 0.05f, 0.01f, 500.0f);
}
else
{
ImGui::DragFloat("radius", &c.radius, 0.05f, 0.01f, 500.0f);
ImGui::DragFloat("Radius", &c.radius, 0.05f, 0.01f, 500.0f);
if (c.shape == Engine::ECS::RigidBodyComponent::CompoundChild::Shape::Capsule)
{
ImGui::DragFloat("halfHeight", &c.capsuleHalfHeight, 0.05f, 0.0f, 500.0f);
ImGui::DragFloat("Half Height", &c.capsuleHalfHeight, 0.05f, 0.0f, 500.0f);
}
}
ImGui::DragFloat3("offset", &c.position.x, 0.05f);
if (ImGui::Button("remove"))
ImGui::DragFloat3("Offset", &c.position.x, 0.05f);
if (ImGui::Button("Remove"))
{
remove = i;
}
@@ -670,20 +674,22 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
// 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("isTrigger", &rb->isTrigger);
// ---- Simulation:动态体物理参数 ----
ImGui::SeparatorText("Simulation");
ImGui::DragFloat("Mass", &rb->mass, 0.1f, 0.1f, 1000.0f, "%.1f");
ImGui::SliderFloat("Gravity Scale", &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("Is Trigger", &rb->isTrigger);
// ---- Debug:遥测与驱动调试(冲量瞬态不序列化)----
if (rb->bodyValid &&
rb->type == Engine::ECS::RigidBodyComponent::BodyType::Dynamic)
{
ImGui::SeparatorText("Debug");
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);
ImGui::DragFloat3("Impulse", &s_debugImpulse.x, 0.1f);
if (ImGui::Button("Apply Impulse"))
{
ctx.app->GetPhysics().AddImpulse(
@@ -718,8 +724,10 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.2f, 1.0f),
"! requires a RigidBody on this entity");
}
// ---- Constraint:类型与目标体 ----
ImGui::SeparatorText("Constraint");
int type = static_cast<int>(jc->type);
if (ImGui::Combo("type", &type, "Fixed\0Hinge\0Ball\0"))
if (ImGui::Combo("Type", &type, "Fixed\0Hinge\0Ball\0"))
{
jc->type = static_cast<Engine::ECS::JointComponent::Type>(type);
}
@@ -745,21 +753,25 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
items.push_back('\0');
});
items.push_back('\0'); // ImGui Combo 双 \0 结尾
if (ImGui::Combo("target", &current, items.c_str()) && current >= 0)
if (ImGui::Combo("Target", &current, items.c_str()) && current >= 0)
{
jc->target = uuids[static_cast<size_t>(current)];
}
ImGui::DragFloat3("anchor A", &jc->anchorA.x, 0.05f);
ImGui::DragFloat3("anchor B", &jc->anchorB.x, 0.05f);
// ---- Anchors:各体局部系锚点 ----
ImGui::SeparatorText("Anchors");
ImGui::DragFloat3("Anchor A", &jc->anchorA.x, 0.05f);
ImGui::DragFloat3("Anchor B", &jc->anchorB.x, 0.05f);
// ---- Hinge:转轴与角度限位(度)----
if (jc->type == Engine::ECS::JointComponent::Type::Hinge)
{
ImGui::DragFloat3("hinge axis", &jc->hingeAxis.x, 0.05f);
ImGui::Checkbox("enable limit", &jc->enableLimit);
ImGui::SeparatorText("Hinge");
ImGui::DragFloat3("Hinge Axis", &jc->hingeAxis.x, 0.05f);
ImGui::Checkbox("Enable Limit", &jc->enableLimit);
if (jc->enableLimit)
{
ImGui::DragFloat("min deg", &jc->limitMin, 1.0f, -180.0f, 0.0f);
ImGui::DragFloat("max deg", &jc->limitMax, 1.0f, 0.0f, 180.0f);
ImGui::DragFloat("Limit Min", &jc->limitMin, 1.0f, -180.0f, 0.0f);
ImGui::DragFloat("Limit Max", &jc->limitMax, 1.0f, 0.0f, 180.0f);
}
}
if (!jc->active)
@@ -790,16 +802,20 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
"! conflicts with RigidBody "
"(disable or remove one)");
}
// 实体原点 = 脚底(胶囊底端),总高 = 2*(halfHeight + radius)
ImGui::DragFloat("radius", &cc->radius, 0.02f, 0.05f, 5.0f);
ImGui::DragFloat("halfHeight", &cc->capsuleHalfHeight, 0.02f, 0.0f, 5.0f);
ImGui::DragFloat("mass", &cc->mass, 1.0f, 1.0f, 500.0f, "%.0f");
ImGui::DragFloat("max slope deg", &cc->maxSlopeAngle, 1.0f, 1.0f, 89.0f);
ImGui::DragFloat("step height", &cc->stepHeight, 0.02f, 0.0f, 2.0f);
// ---- Body:胶囊体(实体原点 = 脚底,总高 = 2*(halfHeight + radius))----
ImGui::SeparatorText("Body");
ImGui::DragFloat("Radius", &cc->radius, 0.02f, 0.05f, 5.0f);
ImGui::DragFloat("Half Height", &cc->capsuleHalfHeight, 0.02f, 0.0f, 5.0f);
ImGui::DragFloat("Mass", &cc->mass, 1.0f, 1.0f, 500.0f, "%.0f");
// ---- Movement:扫掠运动学参数 ----
ImGui::SeparatorText("Movement");
ImGui::DragFloat("Max Slope (deg)", &cc->maxSlopeAngle, 1.0f, 1.0f, 89.0f);
ImGui::DragFloat("Step Height", &cc->stepHeight, 0.02f, 0.0f, 2.0f);
// 遥测(只读):运行时状态在 PhysicsWorld 的 CharacterController
// ---- Debug:遥测(只读,运行时状态在 PhysicsWorld)----
if (auto& phy = ctx.app->GetPhysics(); phy.IsEnabled())
{
ImGui::SeparatorText("Debug");
const uint32_t bits =
static_cast<uint32_t>(entt::to_integral(selected));
ImGui::Text("grounded: %s",
@@ -980,10 +996,16 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.2f, 1.0f),
"! requires a MeshRenderer component");
}
else if (model != nullptr)
else if (model == nullptr)
{
// 动画资产槽:列全部模型容器文件(无法静态区分"模型
// fbx"与"纯动画 fbx",一并列出,无 clip 的会警告跳过)
ImGui::TextDisabled("(no model assigned)");
}
else
{
// ---- Clips:动画资产槽(列全部模型容器文件——无法静态
// 区分"模型 fbx"与"纯动画 fbx",一并列出,无 clip 的会
// 警告跳过)----
ImGui::SeparatorText("Clips");
const auto& files = AssetModelFiles();
std::vector<const char*> opts;
opts.push_back("<attach animation file...>");
@@ -1016,9 +1038,15 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
.string()
.c_str());
}
if (!model->HasAnimations())
{
ImGui::TextDisabled("(no animation clips on model)");
}
}
if (model != nullptr && model->HasAnimations())
{
// ---- Playback:播放控制(切换 clip 触发 crossfade)----
ImGui::SeparatorText("Playback");
const auto& clips = model->GetClips();
std::vector<const char*> clipNames;
clipNames.reserve(clips.size());
@@ -1027,16 +1055,16 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
clipNames.push_back(c.name.c_str());
}
int cur = anim->clipIndex;
if (ImGui::Combo("clip", &cur, clipNames.data(),
if (ImGui::Combo("Clip", &cur, clipNames.data(),
static_cast<int>(clipNames.size())))
{
anim->Play(cur); // 触发 crossfade(fade 秒)
}
ImGui::Checkbox("playing", &anim->playing);
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);
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()))
{
@@ -1046,7 +1074,7 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
// 动画数据表:每个 clip 的时长与有效轨道数(attach 进来的
// 外部动画在此核对骨骼匹配结果——轨道数明显偏少 = 骨骼
// 命名不同源被跳过)
if (ImGui::TreeNode("clip data"))
if (ImGui::TreeNode("Clip Data"))
{
ImGui::Columns(3, nullptr, false);
ImGui::SetColumnWidth(0, 150.0f);
@@ -1083,10 +1111,6 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
ImGui::TreePop();
}
}
else
{
ImGui::TextDisabled("(no animation clips on model)");
}
if (!anim->active)
{
ImGui::EndDisabled();