Files
HelloVulkan/App/editor/panels/ViewportPanel.cpp
T
atdunbg beba491bcb 阶段性合并:动画重定向、碎裂共享库、第三人称玩法、编辑器面板与 vendored ufbx
- 动画:HumanoidRig 三层架构(standard 标准槽 / dialects 方言映射 / mapper 配对),
  指/趾统一走世界增量两遍式烘焙;AnimationAttach 拆为纯算法 + facade;修正 HumanoidBasis
  行走基朝向与跨骨架去重、根运动通道
- 物理:CharacterController 角色互碰撞 / shapeOffset / 接触事件;PhysicsWorld 接触合成与
  字符冲量判定
- 渲染与材质:Model 多材质按材质拆子 mesh 与贴图路径解析、DDS 直传、GLB 内嵌纹理落盘、
  不透明通道 alpha 固定 1.0、glTF FlipUVs 对齐;SceneSerializer 支持 meshIndex 与材质覆盖
- 脚本:FractureLib 抽为共享库(HostView 桥接)、WallFracture 参数直绑、新增
  ThirdPersonCamera / Follower / WASDMove 玩法;移除 GameDirector / PlayerController / Target
- 编辑器:新增 GamepadPanel、LogPanel(LogRing),改进 Inspector / 材质 / 统计 / 视口面板
- 依赖:vendored ufbx(FBX 5.x/6.x/7.x 全版本解析,替代 assimp 对 FBX 6.1 的拒读)
2026-10-11 22:22:19 +08:00

485 lines
26 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.
//
// ViewportPanel:场景渲染输出面板。
//
// tonemap 输出经 ImGui::Image 显示(Editor 模式),并提供
// Editor Viewport / Fullscreen 显示模式切换。面板可用区域
// (物理像素)上报为渲染分辨率 → 画面精确填满面板。
//
// 点击拾取(UE 式 GPU HitProxy):左键单击 → 鼠标 NDC 经
// Renderer::RequestPick 请求下一帧 id 渲染 + 像素拷贝;Draw 每帧
// 轮询 TakePickResult 取走结果。像素级精度,与视觉画面一致
//(视觉几何语义:与 Collider 无关,无 collider 实体同样可选中)。
//
#include "ViewportPanel.h"
#include <imgui.h>
#include <ImGuizmo.h>
#include <Engine/ECS/Components.h> // MeshRenderer / Name / RigidBody(日志对照)
#include <Engine/Renderer/Renderer.h> // RequestPick / TakePickResult 门面
#include <Engine/Renderer/SceneTypes.h> // kPickMiss
#include <Engine/Core/Log.h> // 命中打印
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <cstdio>
// GTX 扩展(实验性)需显式开启:matrix_decompose(矩阵→SRT 分解,
// 旋转以四元数输出,直接写回 TransformComponent)
#define GLM_ENABLE_EXPERIMENTAL
#include <glm/gtx/matrix_decompose.hpp>
using namespace Engine; // u32 / f32(Engine/Core/Types.h)
namespace
{
// Ctrl 吸附步长(UE Grid Snap 语义:按住 Ctrl 拖拽才吸附)
constexpr float kSnapTranslate = 0.25f; // 米/格
constexpr float kSnapRotateDeg = 15.0f; // 度
constexpr float kSnapScale = 0.1f; // 缩放步进
// 平移拖拽灵敏度下限:sinθ(轴与"相机→物体"连线夹角)。低于该值
// 时拖拽投影系数 <0.25 且激活平面与鼠标射线近共面——视觉上
// "看得见、抓了没反应"。≈14.5°,略宽于库默认可见角(≈11.5°),
// 只藏真正病态的轴,不动可用轴
constexpr float kMoveMinSin = 0.25f;
// 缩放退化轴裁剪(防误拖动)阈值。库内缩放 hover/拖拽依赖
// "法线=轴"平面求交,两种病态几何:
// - 轴⊥"相机→物体"连线(正对视角无横偏):交点分子 numer =
// -dist·dot(camToObj, 轴) ≡ 0 → 杆抓不到、数值随机猛跳
// - 轴∥视线(轴指向相机):屏幕零位移 → 拖了数值不变
constexpr float kScaleMinDot = 0.1f; // ≈84.3°
constexpr float kScaleMaxFwdDot = 0.98f; // ≈11.5°,与库藏杆角一致
// Vulkan proj → ImGuizmo 期望的 OpenGL 形式(Y-up、Z∈[-1,1]):
// Y 行翻回(抵消 Vulkan clip Y 向下翻转);z 行 = 2*z - w 即
// 深度 [0,1] → [-1,1]。glm 列主序 m[col][row],逐列改 row1/row2
glm::mat4 ProjVulkanToGL(const glm::mat4& projVulkan)
{
glm::mat4 p = projVulkan;
for (int c = 0; c < 4; ++c) {
p[c][1] *= -1.0f;
p[c][2] = 2.0f * p[c][2] - p[c][3];
}
return p;
}
// 视图相机分派(必须与 SceneRenderer::BuildFrame 渲染源同一规则,
// 否则 Gizmo 杆与画面错位):编辑器形态 = EditorCamera(Scene view
// 专用,与相机实体无关);游戏形态(kDisplayDirect 全屏预览或 PIE
// 内嵌播放)= primary 相机实体。
// 返回 nullptr = 游戏形态且无 primary 相机(本帧无 Gizmo)
Engine::Camera* ViewCamera(const EditorContext& ctx,
ECS::TransformComponent*& outT)
{
const bool playing = ctx.playState && *ctx.playState;
if (!playing && ctx.renderer->GetDisplayMode() == kDisplayViewport)
{
outT = &ctx.editorCamera->GetTransform();
return &ctx.editorCamera->GetController();
}
auto* cc = ctx.scene->GetPrimaryCamera(&outT);
return cc ? &cc->camera : nullptr;
}
}
std::optional<entt::entity> ViewportPanel::Draw(const EditorContext &ctx,
ViewportInputState &out,
GizmoState &gizmo,
const entt::entity selected) const {
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
std::optional<entt::entity> picked;
{
ImGui::Begin("Viewport", nullptr, ImGuiWindowFlags_NoCollapse);
// 输入焦点上报(悬停计入遮挡:模态框盖住时不误报)
out.hovered = ImGui::IsWindowHovered();
out.focused = ImGui::IsWindowFocused();
auto &renderer = *ctx.renderer;
const int mode = renderer.GetDisplayMode();
// ---- 拾取结果轮询(先于新请求:先取走上帧结果再发新请求)。
// 拷贝随上一帧命令缓冲提交,TakePickResult 内 waitIdle 保证
// 暂存可见(仅拷贝就绪帧阻塞,点击低频不可感知)----
if (auto bits = renderer.TakePickResult()) {
auto &reg = ctx.scene->GetRegistry();
const entt::entity hit = static_cast<entt::entity>(*bits);
if (*bits != kPickMiss && reg.valid(hit)) {
picked = hit;
LogPick(ctx, hit);
} else {
// 空白(kPickMiss)或悬空位模式:清选中(与旧语义一致)
picked = entt::null;
if (*bits == kPickMiss) {
ENGINE_INFO("[pick] miss (sky/empty)");
} else {
ENGINE_WARN("[pick] dangling entity bits {:#x}", *bits);
}
}
}
const void *sceneTex = renderer.GetSceneTexture();
if (sceneTex != nullptr && mode == kDisplayViewport) {
// 面板可用区域(物理像素)上报为渲染分辨率 → 画面精确填满
// 面板;下一帧帧首 BeginFrameSync 生效(一帧延迟,不可见)
const ImVec2 avail = ImGui::GetContentRegionAvail();
const ImVec2 fb = ImGui::GetIO().DisplayFramebufferScale;
const u32 wantW = static_cast<u32>(avail.x * fb.x + 0.5f);
const u32 wantH = static_cast<u32>(avail.y * fb.y + 0.5f);
if (wantW >= 16 && wantH >= 16) {
renderer.SetViewportSize(wantW, wantH);
}
// 纹理 = 渲染分辨率:等比适配面板可用区域
const vk::Extent2D ext = renderer.GetRenderExtent();
const f32 aspect =
ext.height != 0 ? f32(ext.width) / f32(ext.height) : 1.0f;
ImVec2 size = avail;
if (avail.x / avail.y > aspect) size.x = avail.y * aspect;
else size.y = avail.x / aspect;
ImGui::Image((ImTextureID) (intptr_t) sceneTex, size);
// ---- 骨骼关节 overlay(Endfield-Poser 范式):3D 骨架线
// 给结构,屏幕圆点给可读性。世界→屏幕与渲染同相机,
// Vulkan NDC y 向下与 ImGui 屏幕坐标同构 → 线性映射
// image 矩形即可。hover 关节 = 白圈高亮 + 骨骼名
{
const auto &frameData = renderer.GetFrameSceneData();
if (frameData.showBoneWires && !frameData.pieActive &&
!frameData.boneJoints.empty() &&
ctx.editorCamera != nullptr) {
const ImVec2 rmin = ImGui::GetItemRectMin();
const ImVec2 rmax = ImGui::GetItemRectMax();
const glm::mat4 vp =
ctx.editorCamera->GetController().GetProjection(aspect) *
ctx.editorCamera->GetView();
ImDrawList *dl = ImGui::GetWindowDrawList();
dl->PushClipRect(rmin, rmax, true);
const f32 r = frameData.boneJointPx;
const ImGuiIO &io = ImGui::GetIO();
const ImVec2 mouse = io.MousePos;
int hovered = -1;
float hoveredD = 0.0f;
for (size_t ji = 0; ji < frameData.boneJoints.size(); ++ji) {
const auto &jm = frameData.boneJoints[ji];
const glm::vec4 c = vp * glm::vec4(jm.pos, 1.0f);
if (c.w <= 0.0f) continue; // 相机身后
const ImVec2 p(
rmin.x + (c.x / c.w * 0.5f + 0.5f) * size.x,
rmin.y + (c.y / c.w * 0.5f + 0.5f) * size.y);
// 骨骼 = 蓝(Endfield humanoid 色),普通节点 = 青
const ImU32 fill = jm.isBone
? IM_COL32(70, 160, 255, 230)
: IM_COL32(70, 190, 190, 190);
dl->AddCircleFilled(p, r, fill);
dl->AddCircle(p, r + 1.5f,
IM_COL32(255, 255, 255, 140), 0, 1.2f);
// hover 命中(半径 +3px 容差,取最近)
const float dx = p.x - mouse.x, dy = p.y - mouse.y;
const float d = dx * dx + dy * dy;
const float hit = (r + 3.0f) * (r + 3.0f);
if (d <= hit && (hovered < 0 || d < hoveredD)) {
hovered = static_cast<int>(ji);
hoveredD = d;
}
}
// hover 高亮圈 + 骨骼名 tooltip(名字查选中实体模型
// 节点表;查不到 = 显示节点下标)
if (hovered >= 0 && ImGui::IsItemHovered()) {
const auto &jm = frameData.boneJoints[hovered];
const glm::vec4 c = vp * glm::vec4(jm.pos, 1.0f);
const ImVec2 p(
rmin.x + (c.x / c.w * 0.5f + 0.5f) * size.x,
rmin.y + (c.y / c.w * 0.5f + 0.5f) * size.y);
dl->AddCircle(p, r + 3.5f,
IM_COL32(255, 255, 255, 255), 0, 2.0f);
const char *name = nullptr;
if (selected != entt::null) {
// 本段在 reg 声明之前,就地取 registry
const auto *mrSel =
ctx.scene->GetRegistry().try_get<
Engine::ECS::MeshRendererComponent>(
selected);
if (mrSel && mrSel->model.Valid()) {
const Model *mdl =
ctx.assets->GetModel(mrSel->model);
if (mdl && jm.nodeIdx < mdl->GetNodes().size())
name = mdl->GetNodes()[jm.nodeIdx]
.name.c_str();
}
}
ImGui::SetTooltip(
"%s%s", name ? name : "(node)",
jm.isBone ? "" : " [non-bone]");
}
dl->PopClipRect();
}
}
// ---- 工具热键(UE 语义):Q 纯选择(不出杆)、W/E/R 切
// Gizmo 模式、T 切 local/world、F 焦点选中实体。仅 Viewport
// 持焦时生效——输入框打字(WantTextInput)由 BlockingKeyboard
// 拦游戏侧,但 ImGui 自身仍收键,故必须限定持焦面板。RMB 按
// 住 = 相机飞行模式(键盘归相机),工具热键全部抑制——W 前
// 进与 W 平移 Gizmo 的键位冲突就此解耦 ----
auto &reg = ctx.scene->GetRegistry();
ECS::TransformComponent *tr =
selected != entt::null
? reg.try_get<ECS::TransformComponent>(selected)
: nullptr;
// PIE 运行态:输入归游戏(脚本读 GLFW 轮询),Gizmo/编辑器
// 热键全部让位(UE Play 语义:Scene view 编辑操作冻结)
const bool playing = ctx.playState && *ctx.playState;
// PIE 启动瞬间:视口抢焦点(UE Play 语义——游戏视角接管)。
// 不抢的话焦点停在工具栏 Play 按钮上,编辑器相机飞行等
// 持焦操作下一局全失灵
static bool s_wasPlaying = false;
if (playing && !s_wasPlaying) {
ImGui::SetWindowFocus();
}
s_wasPlaying = playing;
if (!playing && out.focused && !ImGui::GetIO().WantTextInput &&
!ImGui::IsMouseDown(ImGuiMouseButton_Right)) {
// Q = 纯选择模式(UE Q 工具语义):op=0 无任何轴位,
// ImGuizmo 不绘制不响应——选中实体只高亮不出杆
if (ImGui::IsKeyPressed(ImGuiKey_Q)) gizmo.op = 0;
if (ImGui::IsKeyPressed(ImGuiKey_W)) gizmo.op = ImGuizmo::TRANSLATE;
if (ImGui::IsKeyPressed(ImGuiKey_E)) gizmo.op = ImGuizmo::ROTATE;
if (ImGui::IsKeyPressed(ImGuiKey_R)) gizmo.op = ImGuizmo::SCALE;
if (ImGui::IsKeyPressed(ImGuiKey_T)) gizmo.localSpace = !gizmo.localSpace;
const bool fCombo = ImGui::GetIO().KeyCtrl || ImGui::GetIO().KeyShift;
// F = 焦点选中实体(frame-selection):编辑器相机
// 环绕 pivot 设为选中实体位置,沿视线平移对准(环
// 绕距离沿用当前值,滚轮可调)。世界位姿(层级
// 实体 = 父链组合结果)。裸 F 才触发(组合键排他)
if (ImGui::IsKeyPressed(ImGuiKey_F) && !fCombo && tr != nullptr) {
ctx.editorCamera->FocusOn(tr->GetWorldPosition());
}
}
// ---- Gizmo 操纵杆(ImGui drawlist 叠加,无 GPU pass)----
// 选中实体 + 有 Transform 才出杆;分解 world 矩阵经
// SetWorldPose 写回(与 Inspector 同源,下一帧数值自动跟随)
// PIE 运行态不出杆(编辑操作冻结)
if (tr != nullptr && !playing) {
// 相机矩阵:跟随渲染源(编辑器形态 = EditorCamera,游戏
// 形态 = primary 相机实体;与 SceneRenderer::BuildFrame
// 同一分派),投影参数由相机自身承载(与渲染侧一致)
ECS::TransformComponent *camT = nullptr;
if (Engine::Camera *cam = ViewCamera(ctx, camT);
cam != nullptr && camT != nullptr) {
const glm::mat4 view = cam->GetView(*camT);
const glm::mat4 projGL = ProjVulkanToGL(
cam->GetProjection(aspect));
ImGuizmo::SetDrawlist(ImGui::GetWindowDrawList());
// 杆画在 Image 区域内(等比适配后的 item rect)
const ImVec2 mn = ImGui::GetItemRectMin();
ImGuizmo::SetRect(mn.x, mn.y, size.x, size.y);
// 不调 SetPlaneLimit/SetAxisLimit——可见度保持库默认
glm::mat4 m = tr->World();
// 操作空间:local(模型轴,旋转/缩放随物体朝向直观)/
// world(T 键切换)。无父层级时两种模式 world 矩阵同源,
// decompose 写回链路不变
const ImGuizmo::MODE mode =
gizmo.localSpace ? ImGuizmo::LOCAL : ImGuizmo::WORLD;
const glm::mat3 rotM(m);
// 退化轴裁剪(防误拖动):被裁轴不可见、不可激活,
// 转个视角即可用。三操作病态几何不同,按 op 分派:
// - 平移:拖拽灵敏度 = sinθ(轴与相机连线夹角),θ→0
// 时鼠标位移在轴上投影→0("数值不变")
// - 缩放:库内 hover/拖拽依赖"法线=轴"平面求交——
// 轴⊥相机连线(正对无横偏)时交点分子 numer≡0
// (抓不到/数值乱跳);轴∥视线时屏幕零位移(数值
// 不变)。两判据并集裁剪
// - 旋转:屏幕空间角度数学,无退化,不裁
const bool isTranslate = (gizmo.op
& int(ImGuizmo::TRANSLATE)) == int(ImGuizmo::TRANSLATE);
const bool isScale = (gizmo.op
& int(ImGuizmo::SCALE)) == int(ImGuizmo::SCALE);
if (isTranslate || isScale) {
bool maskAxis[3] = {false, false, false};
const glm::vec3 camPos(glm::inverse(view)[3]);
glm::vec3 camToObj = glm::vec3(m[3]) - camPos;
const float camDist = glm::length(camToObj);
if (camDist > 1e-4f) {
camToObj /= camDist;
const glm::vec3 viewFwd = -glm::normalize(
glm::vec3(view[0][2], view[1][2],
view[2][2]));
for (int i = 0; i < 3; ++i) {
const glm::vec3 axis = gizmo.localSpace
? glm::normalize(rotM[i])
: glm::vec3(float(i == 0), float(i == 1),
float(i == 2));
maskAxis[i] = isScale
? glm::abs(glm::dot(camToObj, axis))
< kScaleMinDot ||
glm::abs(glm::dot(viewFwd, axis))
> kScaleMaxFwdDot
: glm::length(glm::cross(camToObj, axis))
< kMoveMinSin;
}
}
ImGuizmo::SetAxisMask(maskAxis[0], maskAxis[1],
maskAxis[2]);
}
// Ctrl 吸附(UE Grid Snap 语义)。库的约定
//(docs/documentation.md):平移 = 世界单位 float[3]
// 逐轴;旋转 = snap[0] 按度(库内自行 DEG2RAD,传弧
// 度会被解释成极小步进);缩放 = 只取 snap[0]
float snap[3] = {0.0f, 0.0f, 0.0f};
const bool snapping = ImGui::GetIO().KeyCtrl;
if (snapping) {
if (gizmo.op == ImGuizmo::TRANSLATE) {
snap[0] = snap[1] = snap[2] = kSnapTranslate;
} else if (gizmo.op == ImGuizmo::ROTATE) {
snap[0] = kSnapRotateDeg; // 度,非弧度
} else if (gizmo.op == ImGuizmo::SCALE) {
snap[0] = kSnapScale;
}
}
ImGuizmo::Manipulate(&view[0][0], &projGL[0][0],
static_cast<ImGuizmo::OPERATION>(gizmo.op),
mode, &m[0][0], nullptr,
snapping ? snap : nullptr);
gizmo.dragging = ImGuizmo::IsUsing();
// 拖拽生效:分解 world 矩阵 → world SRT → 沿父链反解
// local 写回(SetWorldPose;根实体 = 直写)。旋转用
// decompose 出的 quat 直写(UE FTransform 语义)——
// 不经欧拉往返,拖拽零漂移、无万向锁
if (ImGuizmo::IsUsing()) {
glm::vec3 scale, transl, skew;
glm::quat rot;
glm::vec4 persp;
if (glm::decompose(m, scale, rot, transl, skew, persp)) {
ctx.scene->SetWorldPose(selected, transl, rot,
scale);
}
}
}
}
// Gizmo 状态提示(左上角 drawlist 叠加,原因同下:不占布局行)
// PIE 运行态 = 黄色 PLAYING/PAUSED 提示替代编辑状态
{
char tip[128];
if (playing) {
const bool paused = ctx.pausedState && *ctx.pausedState;
std::snprintf(tip, sizeof(tip),
paused ? "PAUSED - Ctrl+P to stop"
: "PLAYING - Ctrl+P to stop");
} else {
std::snprintf(tip, sizeof(tip),
"Q/W/E/R: Sel/Move/Rot/Scale T: %s "
"Snap: Ctrl F: Focus",
gizmo.localSpace ? "Local" : "World");
}
const ImVec2 imgMin = ImGui::GetItemRectMin();
ImGui::GetWindowDrawList()->AddText(
ImVec2(imgMin.x + 8.0f, imgMin.y + 8.0f),
playing ? IM_COL32(255, 184, 46, 220)
: IM_COL32(255, 255, 255, 150),
tip);
}
// Dynamic 刚体提示:物理每帧把姿态写回 Transform,拖拽即刻
// 被覆盖(Static/Kinematic 编辑器权威,可直接拖)。
// 用 drawlist 叠加而非 ImGui::TextDisabled:后者在 Image 下方
// 占一行布局 → 滚动条出现/消失循环 → avail 抖动 → 渲染分辨率
// 每帧重报(视口疯狂顶动);且默认字体无中文字形(显示 ?)
if (playing) {
// 运行态无 primary 相机实体:渲染回退编辑器视角(保持帧
// 管线节拍),提示补相机
ECS::TransformComponent *camT = nullptr;
if (ViewCamera(ctx, camT) == nullptr) {
const ImVec2 imgMin = ImGui::GetItemRectMin();
ImGui::GetWindowDrawList()->AddText(
ImVec2(imgMin.x + 8.0f,
imgMin.y + size.y * 0.5f),
IM_COL32(255, 120, 80, 230),
"No primary camera entity - showing editor "
"view. Add a Camera component and check "
"'Primary'");
}
}
// Dynamic 刚体提示(drawlist 叠加,理由同上)
if (selected != entt::null) {
if (const auto *rb = reg.try_get<ECS::RigidBodyComponent>(selected);
rb != nullptr &&
rb->type == ECS::RigidBodyComponent::BodyType::Dynamic) {
const ImVec2 imgMin = ImGui::GetItemRectMin();
ImGui::GetWindowDrawList()->AddText(
ImVec2(imgMin.x + 8.0f, imgMin.y + size.y - 20.0f),
IM_COL32(255, 255, 255, 160),
"(Dynamic body: transform driven by physics)");
}
}
// 点击拾取:单击 Image 且未命中 UI 遮挡(IsItemHovered 已含)。
// 抓杆时不出 pick(IsOver 悬停杆/IsUsing 拖拽中)——否则按在
// 操纵杆上的点击会穿透发拾取请求。PIE 运行态不出拾取:
// 点击 = 游戏输入(UE Play 语义,编辑操作冻结)
if (!playing && ImGui::IsItemHovered() && ImGui::IsMouseClicked(0) &&
!ImGuizmo::IsOver() && !ImGuizmo::IsUsing()) {
RequestPickAt(ctx, size);
}
} else {
ImGui::TextDisabled("(scene texture not ready)");
}
ImGui::End();
}
ImGui::PopStyleVar();
return picked;
}
void ViewportPanel::RequestPickAt(const EditorContext &ctx,
const ImVec2 &size) const {
// 鼠标在 Image 内的位置 → NDC。Vulkan NDC y 与屏幕 v 同向
//(向下为正,与渲染同构;skybox.frag 反投影交叉验证过方向)
const ImVec2 mouse = ImGui::GetMousePos();
const ImVec2 mn = ImGui::GetItemRectMin();
const glm::vec2 ndc{
(mouse.x - mn.x) / size.x * 2.0f - 1.0f,
(mouse.y - mn.y) / size.y * 2.0f - 1.0f,
};
// UE 式 GPU HitProxy:下一帧 id 渲染 + 请求像素 1×1 拷贝
ctx.renderer->RequestPick(ndc);
}
void ViewportPanel::LogPick(const EditorContext &ctx, entt::entity e) {
// 命中详情:实体名;有物理体则附 collider 对照(诊断"物理盒 ≠
// 视觉"的错位,如 M1911 的默认 0.5 半尺寸盒)
auto &reg = ctx.scene->GetRegistry();
const auto &name = reg.get<ECS::NameComponent>(e).name;
if (const auto *rb = reg.try_get<ECS::RigidBodyComponent>(e)) {
const auto &tr = reg.get<ECS::TransformComponent>(e);
ENGINE_INFO("[pick] '{}' (hitproxy) | collider={} type={} "
"boxHalf=({:.2f},{:.2f},{:.2f}) sphereR={:.2f} "
"bodyPos=({:.2f},{:.2f},{:.2f})",
name,
rb->collider == ECS::RigidBodyComponent::ColliderType::Box
? "box" : "sphere",
rb->type == ECS::RigidBodyComponent::BodyType::Static
? "static" : "dynamic",
rb->boxHalfExtents.x, rb->boxHalfExtents.y,
rb->boxHalfExtents.z, rb->sphereRadius,
tr.GetWorldPosition().x, tr.GetWorldPosition().y, tr.GetWorldPosition().z);
} else {
ENGINE_INFO("[pick] '{}' (hitproxy) | (no collider)", name);
}
}