From e7711134fe0f89cb2a71cd79f25ef289fa0c5476 Mon Sep 17 00:00:00 2001 From: atdunbg Date: Sun, 18 Oct 2026 15:23:01 +0800 Subject: [PATCH] =?UTF-8?q?=E7=89=A9=E7=90=86=E4=BF=AE=E5=A4=8D=E4=B8=8E?= =?UTF-8?q?=E7=9B=B8=E6=9C=BA=E9=98=B2=E7=A9=BF=E5=A2=99=EF=BC=9AJolt=20Tr?= =?UTF-8?q?ace=20=E6=8E=A5=E7=AE=A1=EF=BC=88=E7=BC=BA=E7=9C=81=20DummyTrac?= =?UTF-8?q?e=20=E5=B8=A6=20JPH=5FASSERT(false)=EF=BC=8C=E8=B0=83=E8=AF=95?= =?UTF-8?q?=E7=89=88=E4=B8=80=E8=A2=AB=E8=B0=83=E7=94=A8=E5=8D=B3=E6=96=AD?= =?UTF-8?q?=EF=BC=9B=E5=90=8C=E6=96=87=E5=8E=BB=E9=87=8D=E9=98=B2=E5=A4=9A?= =?UTF-8?q?=E7=BA=BF=E7=A8=8B=E5=88=B7=E5=B1=8F=EF=BC=89=EF=BC=9BMeshShape?= =?UTF-8?q?=20=E4=B8=89=E8=A7=92=E7=B4=A2=E5=BC=95=E8=A1=A5=E5=9F=BA?= =?UTF-8?q?=E5=9D=80=EF=BC=88=E6=AD=A4=E5=89=8D=E6=8A=8A=20materialIndex/u?= =?UTF-8?q?serData=20=E5=BD=93=E9=A1=B6=E7=82=B9=E5=81=8F=E7=A7=BB?= =?UTF-8?q?=E4=BC=A0=20=E2=86=92=20=E6=AF=8F=E4=B8=AA=20mesh=20=E4=B8=89?= =?UTF-8?q?=E8=A7=92=E5=85=A8=E6=8C=87=E5=90=91=E5=89=8D=E4=B8=80=20mesh?= =?UTF-8?q?=20=E9=A1=B6=E7=82=B9=E5=8C=BA=EF=BC=8C=E7=A2=B0=E6=92=9E?= =?UTF-8?q?=E5=BD=A2=E7=8A=B6=E9=94=99=E4=BD=8D=20+=20AABBTree=20=E9=9A=8F?= =?UTF-8?q?=E6=9C=BA=E5=88=86=E5=89=B2=E5=91=8A=E8=AD=A6=EF=BC=89=EF=BC=9B?= =?UTF-8?q?=E8=B0=83=E8=AF=95=E7=BB=98=E5=88=B6=E5=B7=A8=E5=9E=8B=E7=BD=91?= =?UTF-8?q?=E6=A0=BC=E4=B8=89=E8=A7=92=E9=A2=84=E7=AE=97=EF=BC=88>5=20?= =?UTF-8?q?=E4=B8=87=E4=B8=89=E8=A7=92=E9=80=80=E5=8C=96=E4=B8=BA=E5=8C=85?= =?UTF-8?q?=E5=9B=B4=E7=9B=92=E7=BA=BF=E6=A1=86=EF=BC=8C=E9=81=BF=E5=85=8D?= =?UTF-8?q?=E7=99=BE=E4=B8=87=E7=BA=A7=20DrawLine=20=E6=89=93=E5=88=B0?= =?UTF-8?q?=E4=B8=AA=E4=BD=8D=E6=95=B0=E5=B8=A7=E7=8E=87=EF=BC=89=EF=BC=9B?= =?UTF-8?q?=E8=A7=92=E8=89=B2=20mEnhancedInternalEdgeRemoval=20=E6=B6=88?= =?UTF-8?q?=E7=BD=91=E6=A0=BC=E5=9C=B0=E9=9D=A2=20ghost=20contact=20?= =?UTF-8?q?=E9=A1=B6=E8=B5=B7/=E6=8A=96=E5=8A=A8=EF=BC=9B=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=20RayCastStatic=EF=BC=88=E5=8F=AA=E5=91=BD=E4=B8=AD?= =?UTF-8?q?=E9=9D=9E=E7=A7=BB=E5=8A=A8=E5=B1=82=EF=BC=8C=E7=A2=8E=E5=9D=97?= =?UTF-8?q?/=E5=8A=A8=E6=80=81=E7=89=A9=E4=B8=8D=E5=BD=93=E9=81=AE?= =?UTF-8?q?=E6=8C=A1=EF=BC=89=E2=86=92=20ThirdPersonCamera=20=E9=98=B2?= =?UTF-8?q?=E7=A9=BF=E5=A2=99=E6=8B=89=E8=BF=91=EF=BC=88=E7=9E=AC=E6=97=B6?= =?UTF-8?q?=E6=8B=89=E8=BF=91=E3=80=81=E5=B9=B3=E6=BB=91=E5=9B=9E=E5=BC=B9?= =?UTF-8?q?=E3=80=81=E8=B4=B4=E5=A2=99=E6=9C=80=E5=B0=8F=E8=B7=9D=E7=A6=BB?= =?UTF-8?q?=E4=B8=8E=E5=9B=9E=E9=80=80=E4=BD=99=E9=87=8F=E5=8F=AF=E9=85=8D?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App/scripts/ThirdPersonCamera.cpp | 47 +++++++++- Engine/include/Engine/Physics/PhysicsWorld.h | 9 ++ Engine/src/Physics/CharacterController.cpp | 4 + Engine/src/Physics/PhysicsWorld.cpp | 94 +++++++++++++++++++- 4 files changed, 149 insertions(+), 5 deletions(-) diff --git a/App/scripts/ThirdPersonCamera.cpp b/App/scripts/ThirdPersonCamera.cpp index f23da5d..54cebab 100644 --- a/App/scripts/ThirdPersonCamera.cpp +++ b/App/scripts/ThirdPersonCamera.cpp @@ -11,6 +11,9 @@ // OnStart 从编辑器摆的机位反推 yaw/pitch/dist:编辑器摆哪,游戏就 // 从哪个角度开场。Esc 释放光标(调参),左键重捕继续玩。 // +// 防穿墙:枢轴→期望机位做静态几何射线(UE SpringArm 同位),命中则拉近; +// 拉近瞬时、拉远平滑,贴墙最小距离可配。 +// #include #include @@ -18,17 +21,24 @@ #include #include #include +#include // RayHit(防穿墙射线结果) +#include // App().GetPhysics().RayCastStatic 门面 #include #include +#include #include using namespace Engine; namespace { + // 拉远回弹响应(1/s,指数逼近):离开墙角后镜头平滑归位。 + // 拉近是瞬时的(见 OnUpdate)——遮挡晚一帧就会穿墙,不能平滑 + constexpr float kCameraReturnResponse = 8.0f; + class ThirdPersonCamera final : public Engine::Scripting::ScriptBehavior { public: const char* Name() const override { return "ThirdPersonCamera"; } @@ -50,6 +60,13 @@ namespace { pb.Field("FollowResponse", &m_FollowResponse, 12.0f); // 目标 Y 瞬移超过该距离(PIE 传送/回合复位)→ 直接吸附 pb.Field("SnapDistance", &m_SnapDistance, 3.0f); + // 相机防穿墙(UE SpringArm 同位):枢轴→期望机位之间若有静态 + // 几何,把相机拉到命中点前。关掉 = 旧行为(可穿墙) + pb.Field("CameraCollision", &m_Collision, true); + // 贴墙时相机离枢轴的最小距离(再近会切进角色/近平面) + pb.Field("MinDistance", &m_MinDistance, 0.35f); + // 命中点回退余量(防止相机面贴墙 → z-fighting/切面) + pb.Field("CollisionSkin", &m_CollisionSkin, 0.25f); } void OnStart() override { @@ -154,8 +171,31 @@ namespace { std::sin(yawR) * std::cos(pitchR), std::sin(pitchR), std::cos(yawR) * std::cos(pitchR)); + // ---- 相机防穿墙:枢轴 → 期望机位射线拉近 ---- + // 只打静态几何(碎块/动态物不当遮挡,避免镜头被飘过的碎片 + // 乱拉);枢轴在角色胶囊内,角色是 CharacterVirtual 不进射线, + // 不会自己挡自己。拉近瞬时(遮挡晚一帧就穿墙),拉远平滑 + float wantDist = m_Distance; + if (m_Collision) { + RayHit hit; + if (App().GetPhysics().RayCastStatic( + pivot, dir, m_Distance + m_CollisionSkin, hit)) { + wantDist = std::max(hit.distance - m_CollisionSkin, + m_MinDistance); + } + } + if (!m_DistInit) { // 首帧吸附(无回弹动画) + m_CurDist = wantDist; + m_DistInit = true; + } else if (wantDist < m_CurDist) { + m_CurDist = wantDist; // 拉近:瞬时 + } else { + m_CurDist += (wantDist - m_CurDist) * + (1.0f - std::exp(-kCameraReturnResponse * dt)); + } + auto& camT = Transform(); - camT.SetPosition(pivot + dir * m_Distance); + camT.SetPosition(pivot + dir * m_CurDist); // ---- 朝向锁定目标 ---- // 引擎 YXZ 序(R = Ry·Rx)视向 = (-cosP sinY, sinP, -cosP cosY) @@ -178,6 +218,11 @@ namespace { float m_SnapDistance = 3.0f; // Y 瞬移吸附阈值 m float m_FollowPivotY = 0.0f; // 阻尼后的枢轴高度(XZ 硬跟) bool m_FollowInit = false; // 首帧吸附基准 + bool m_Collision = true; // 相机防穿墙开关 + float m_MinDistance = 0.35f; // 贴墙最小距离 + float m_CollisionSkin = 0.25f; // 命中点回退余量 + float m_CurDist = 0.0f; // 防穿墙后的当前距离(拉近平滑态) + bool m_DistInit = false; // 距离首帧吸附基准 float m_Yaw = 0.0f; float m_Pitch = 20.0f; entt::entity m_Target = entt::null; diff --git a/Engine/include/Engine/Physics/PhysicsWorld.h b/Engine/include/Engine/Physics/PhysicsWorld.h index a2afbe0..e558c43 100644 --- a/Engine/include/Engine/Physics/PhysicsWorld.h +++ b/Engine/include/Engine/Physics/PhysicsWorld.h @@ -79,6 +79,11 @@ namespace Engine { bool RayCast(const glm::vec3& origin, const glm::vec3& dir, f32 maxDist, RayHit& outHit) const; + // 仅命中静态(非移动层)几何:忽略碎块/动态物/NPC。相机防穿墙用它 + // ——动态碎块撞进枢轴→相机射线会把镜头乱拉(抖动),静态语义才稳 + bool RayCastStatic(const glm::vec3& origin, const glm::vec3& dir, + f32 maxDist, RayHit& outHit) const; + // ---- 刚体驱动(游戏逻辑/编辑器调试;按实体位模式寻址)---- // Jolt 力在每步模拟后清零:游戏逻辑每帧持续施加 = 正确用法; // 冲量一次性改变动量(自动唤醒休眠体);速度直写会打断自然 @@ -154,6 +159,10 @@ namespace Engine { JPH::RefConst BuildCompoundShape(ECS::RigidBodyComponent& rb, ECS::TransformComponent& tr); void RemoveBody(u32 entityBits); + // RayCast / RayCastStatic 共用实现:layerFilter 为空 = 命中所有层 + bool RayCastImpl(const glm::vec3& origin, const glm::vec3& dir, + f32 maxDist, RayHit& outHit, + const JPH::ObjectLayerFilter* layerFilter) const; const BodyRecord* FindRecord(u32 entityBits) const; // 刚体驱动寻址 CharacterController* FindCharacter(u32 entityBits); // 角色门面寻址 void QueueEvent(CollisionEvent&& ev); // 回调线程 append(多线程并发,加锁) diff --git a/Engine/src/Physics/CharacterController.cpp b/Engine/src/Physics/CharacterController.cpp index 0df20d9..b754608 100644 --- a/Engine/src/Physics/CharacterController.cpp +++ b/Engine/src/Physics/CharacterController.cpp @@ -22,6 +22,10 @@ namespace Engine { s.mUp = JPH::Vec3::sAxisY(); s.mMass = std::max(params.mass, 1e-3f); s.mMaxSlopeAngle = glm::radians(params.maxSlopeAngle); + // 静态三角网格地面/楼梯的内部边会产生 ghost contact,把角色顶起/ + // 抖动。Jolt 增强内部边移除可消除(一次穿墙修正的额外开销), + // 对箱体/凸包地面无副作用 + s.mEnhancedInternalEdgeRemoval = true; s.mShape = new JPH::CapsuleShape( std::max(params.radius, 0.01f), std::max(params.halfHeight, 0.0f)); diff --git a/Engine/src/Physics/PhysicsWorld.cpp b/Engine/src/Physics/PhysicsWorld.cpp index a4f3be0..28af621 100644 --- a/Engine/src/Physics/PhysicsWorld.cpp +++ b/Engine/src/Physics/PhysicsWorld.cpp @@ -33,14 +33,54 @@ #include #include #include +#include // BodyDrawFilter(调试绘制按体过滤) + +#include #include #include +#include +#include #include +#include #include +#include namespace Engine { namespace { + // Jolt Trace 转发:缺省实现 DummyTrace 内部带 JPH_ASSERT(false) + // ("Please override Trace"),任何一次 Trace 都会把调试版直接打断。 + // Jolt 用 Trace 输出运行期告警——例如 AABBTreeBuilder 对相交三角做 + // 随机分割并注明"仍能正常工作,仅碰撞树效率降低"——必须接管。 + // 非捕获函数(GCC 不支持变参 lambda 转函数指针)。同文去重(Jolt + // 可能多线程并发 Trace)防重复刷屏 + void JoltTrace(const char *inFMT, ...) { + char buf[512]; + va_list args; + va_start(args, inFMT); + std::vsnprintf(buf, sizeof(buf), inFMT, args); + va_end(args); + static std::mutex mtx; + static std::unordered_set seen; + std::lock_guard lock(mtx); + if (seen.insert(buf).second) { + ENGINE_WARN("[Jolt] {}", buf); + } + } + + // ---- 调试绘制三角预算 ---- + // 巨型三角网格碰撞体(合并全场景 mesh,如 SunTemple 1056 mesh ≈ 百万 + // 三角)的线框绘制每帧要生成百万级 DrawLine(约 3M 线段)——直接把 + // 帧率打到个位数。超预算的 MeshShape 不画三角线框,CollectDebugData + // 退化为画其包围盒线框(位置/范围仍可见,成本与三角数无关) + constexpr JPH::uint kDebugMeshTriangleBudget = 50000; + struct SkipHeavyMeshDraw final : JPH::BodyDrawFilter { + bool ShouldDraw(const JPH::Body &inBody) const override { + return inBody.GetShape()->GetStats().mNumTriangles + <= kDebugMeshTriangleBudget; + } + }; + // ---- 层配置:MVP 两层(非移动/移动),扩软体/传感器时再分层 ---- constexpr JPH::ObjectLayer LAYER_NON_MOVING{0}; constexpr JPH::ObjectLayer LAYER_MOVING{1}; @@ -80,6 +120,15 @@ namespace Engine { } }; + // 射线层过滤:只放行静态(非移动)层——相机防穿墙不把碎块/动态物 + // 当遮挡(否则碎块飘过枢轴会把镜头乱拉) + class OnlyNonMovingLayerFilter final : public JPH::ObjectLayerFilter { + public: + bool ShouldCollide(JPH::ObjectLayer inLayer) const override { + return inLayer == LAYER_NON_MOVING; + } + }; + // Jolt ↔ glm 四元数直转(TransformComponent 旋转存 glm::quat, // UE FTransform 语义):物理同步/写回不再经欧拉往返 → 姿态 // 零漂移(分量序:Jolt ctor (x,y,z,w),glm ctor (w,x,y,z)) @@ -167,6 +216,7 @@ namespace Engine { static std::once_flag s_JoltOnce; std::call_once(s_JoltOnce, [] { JPH::RegisterDefaultAllocator(); + JPH::Trace = &JoltTrace; // 见文件顶部说明 JPH::Factory::sInstance = new JPH::Factory(); JPH::RegisterTypes(); }); @@ -288,7 +338,24 @@ namespace Engine { JPH::BodyManager::DrawSettings ds; ds.mDrawShape = true; ds.mDrawShapeWireframe = true; - m_System->DrawBodies(ds, m_DebugDraw.get()); + // 超预算的网格体被过滤(不画三角线框),下面补包围盒线框 + const SkipHeavyMeshDraw skipHeavy; + m_System->DrawBodies(ds, m_DebugDraw.get(), &skipHeavy); + + for (auto &[bits, rec]: m_Bodies) { + JPH::BodyLockRead lock(m_System->GetBodyLockInterface(), rec.id); + if (!lock.Succeeded()) { + continue; + } + const JPH::Body &body = lock.GetBody(); + if (body.GetShape()->GetStats().mNumTriangles + <= kDebugMeshTriangleBudget) { + continue; + } + // 巨型 MeshShape:包围盒线框(蓝色)替代百万三角线框 + m_DebugDraw->DrawWireBox(body.GetWorldSpaceBounds(), + JPH::Color(64, 200, 255, 255)); + } } // 2) 约束(关节;软体/载具约束接入后自动生效) @@ -857,8 +924,13 @@ namespace Engine { // 退化三角(两索引相同)剔除:MeshShape 构建会因退化报错 if (idx[i] != idx[i + 1] && idx[i] != idx[i + 2] && idx[i + 1] != idx[i + 2]) { - tris.emplace_back(idx[i], idx[i + 1], idx[i + 2], - static_cast(0), base); + // 索引必须加基址偏移到拼接后的全局顶点表:IndexedTriangle + // 第 4/5 参是 materialIndex/userData,不是顶点偏移——写错 + // 会让每个 mesh 的三角全部指向前一个 mesh 的顶点区, + // 碰撞网格形状彻底错位(且三角包围盒乱飞 → AABBTree 随机 + // 分割告警刷屏) + tris.emplace_back(idx[i] + base, idx[i + 1] + base, + idx[i + 2] + base, 0u, 0u); } } base += static_cast(pos.size()); @@ -1045,6 +1117,18 @@ namespace Engine { bool PhysicsWorld::RayCast(const glm::vec3 &origin, const glm::vec3 &dir, const f32 maxDist, RayHit &outHit) const { + return RayCastImpl(origin, dir, maxDist, outHit, nullptr); + } + + bool PhysicsWorld::RayCastStatic(const glm::vec3 &origin, const glm::vec3 &dir, + const f32 maxDist, RayHit &outHit) const { + static const OnlyNonMovingLayerFilter kFilter; + return RayCastImpl(origin, dir, maxDist, outHit, &kFilter); + } + + bool PhysicsWorld::RayCastImpl(const glm::vec3 &origin, const glm::vec3 &dir, + const f32 maxDist, RayHit &outHit, + const JPH::ObjectLayerFilter *layerFilter) const { const f32 len = glm::length(dir); if (len < 1e-8f) return false; @@ -1054,7 +1138,9 @@ namespace Engine { JPH::Vec3(dir.x / len, dir.y / len, dir.z / len) * maxDist); JPH::RayCastResult hit; - if (!m_System->GetNarrowPhaseQuery().CastRay(ray, hit)) { + const auto &nq = m_System->GetNarrowPhaseQuery(); + if (!(layerFilter ? nq.CastRay(ray, hit, {}, *layerFilter) + : nq.CastRay(ray, hit))) { return false; }