物理修复与相机防穿墙:Jolt Trace 接管(缺省 DummyTrace 带 JPH_ASSERT(false),调试版一被调用即断;同文去重防多线程刷屏);MeshShape 三角索引补基址(此前把 materialIndex/userData 当顶点偏移传 → 每个 mesh 三角全指向前一 mesh 顶点区,碰撞形状错位 + AABBTree 随机分割告警);调试绘制巨型网格三角预算(>5 万三角退化为包围盒线框,避免百万级 DrawLine 打到个位数帧率);角色 mEnhancedInternalEdgeRemoval 消网格地面 ghost contact 顶起/抖动;新增 RayCastStatic(只命中非移动层,碎块/动态物不当遮挡)→ ThirdPersonCamera 防穿墙拉近(瞬时拉近、平滑回弹、贴墙最小距离与回退余量可配)
This commit is contained in:
@@ -11,6 +11,9 @@
|
|||||||
// OnStart 从编辑器摆的机位反推 yaw/pitch/dist:编辑器摆哪,游戏就
|
// OnStart 从编辑器摆的机位反推 yaw/pitch/dist:编辑器摆哪,游戏就
|
||||||
// 从哪个角度开场。Esc 释放光标(调参),左键重捕继续玩。
|
// 从哪个角度开场。Esc 释放光标(调参),左键重捕继续玩。
|
||||||
//
|
//
|
||||||
|
// 防穿墙:枢轴→期望机位做静态几何射线(UE SpringArm 同位),命中则拉近;
|
||||||
|
// 拉近瞬时、拉远平滑,贴墙最小距离可配。
|
||||||
|
//
|
||||||
|
|
||||||
#include <Engine/Scripting/Script.h>
|
#include <Engine/Scripting/Script.h>
|
||||||
#include <Engine/Core/Application.h>
|
#include <Engine/Core/Application.h>
|
||||||
@@ -18,17 +21,24 @@
|
|||||||
#include <Engine/Core/Log.h>
|
#include <Engine/Core/Log.h>
|
||||||
#include <Engine/ECS/Components.h>
|
#include <Engine/ECS/Components.h>
|
||||||
#include <Engine/ECS/Scene.h>
|
#include <Engine/ECS/Scene.h>
|
||||||
|
#include <Engine/Physics/PhysicsRay.h> // RayHit(防穿墙射线结果)
|
||||||
|
#include <Engine/Physics/PhysicsWorld.h> // App().GetPhysics().RayCastStatic 门面
|
||||||
|
|
||||||
#include <entt/entt.hpp>
|
#include <entt/entt.hpp>
|
||||||
|
|
||||||
#include <glm/glm.hpp>
|
#include <glm/glm.hpp>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
|
||||||
using namespace Engine;
|
using namespace Engine;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
// 拉远回弹响应(1/s,指数逼近):离开墙角后镜头平滑归位。
|
||||||
|
// 拉近是瞬时的(见 OnUpdate)——遮挡晚一帧就会穿墙,不能平滑
|
||||||
|
constexpr float kCameraReturnResponse = 8.0f;
|
||||||
|
|
||||||
class ThirdPersonCamera final : public Engine::Scripting::ScriptBehavior {
|
class ThirdPersonCamera final : public Engine::Scripting::ScriptBehavior {
|
||||||
public:
|
public:
|
||||||
const char* Name() const override { return "ThirdPersonCamera"; }
|
const char* Name() const override { return "ThirdPersonCamera"; }
|
||||||
@@ -50,6 +60,13 @@ namespace {
|
|||||||
pb.Field("FollowResponse", &m_FollowResponse, 12.0f);
|
pb.Field("FollowResponse", &m_FollowResponse, 12.0f);
|
||||||
// 目标 Y 瞬移超过该距离(PIE 传送/回合复位)→ 直接吸附
|
// 目标 Y 瞬移超过该距离(PIE 传送/回合复位)→ 直接吸附
|
||||||
pb.Field("SnapDistance", &m_SnapDistance, 3.0f);
|
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 {
|
void OnStart() override {
|
||||||
@@ -154,8 +171,31 @@ namespace {
|
|||||||
std::sin(yawR) * std::cos(pitchR),
|
std::sin(yawR) * std::cos(pitchR),
|
||||||
std::sin(pitchR),
|
std::sin(pitchR),
|
||||||
std::cos(yawR) * std::cos(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();
|
auto& camT = Transform();
|
||||||
camT.SetPosition(pivot + dir * m_Distance);
|
camT.SetPosition(pivot + dir * m_CurDist);
|
||||||
|
|
||||||
// ---- 朝向锁定目标 ----
|
// ---- 朝向锁定目标 ----
|
||||||
// 引擎 YXZ 序(R = Ry·Rx)视向 = (-cosP sinY, sinP, -cosP cosY)
|
// 引擎 YXZ 序(R = Ry·Rx)视向 = (-cosP sinY, sinP, -cosP cosY)
|
||||||
@@ -178,6 +218,11 @@ namespace {
|
|||||||
float m_SnapDistance = 3.0f; // Y 瞬移吸附阈值 m
|
float m_SnapDistance = 3.0f; // Y 瞬移吸附阈值 m
|
||||||
float m_FollowPivotY = 0.0f; // 阻尼后的枢轴高度(XZ 硬跟)
|
float m_FollowPivotY = 0.0f; // 阻尼后的枢轴高度(XZ 硬跟)
|
||||||
bool m_FollowInit = false; // 首帧吸附基准
|
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_Yaw = 0.0f;
|
||||||
float m_Pitch = 20.0f;
|
float m_Pitch = 20.0f;
|
||||||
entt::entity m_Target = entt::null;
|
entt::entity m_Target = entt::null;
|
||||||
|
|||||||
@@ -79,6 +79,11 @@ namespace Engine {
|
|||||||
bool RayCast(const glm::vec3& origin, const glm::vec3& dir,
|
bool RayCast(const glm::vec3& origin, const glm::vec3& dir,
|
||||||
f32 maxDist, RayHit& outHit) const;
|
f32 maxDist, RayHit& outHit) const;
|
||||||
|
|
||||||
|
// 仅命中静态(非移动层)几何:忽略碎块/动态物/NPC。相机防穿墙用它
|
||||||
|
// ——动态碎块撞进枢轴→相机射线会把镜头乱拉(抖动),静态语义才稳
|
||||||
|
bool RayCastStatic(const glm::vec3& origin, const glm::vec3& dir,
|
||||||
|
f32 maxDist, RayHit& outHit) const;
|
||||||
|
|
||||||
// ---- 刚体驱动(游戏逻辑/编辑器调试;按实体位模式寻址)----
|
// ---- 刚体驱动(游戏逻辑/编辑器调试;按实体位模式寻址)----
|
||||||
// Jolt 力在每步模拟后清零:游戏逻辑每帧持续施加 = 正确用法;
|
// Jolt 力在每步模拟后清零:游戏逻辑每帧持续施加 = 正确用法;
|
||||||
// 冲量一次性改变动量(自动唤醒休眠体);速度直写会打断自然
|
// 冲量一次性改变动量(自动唤醒休眠体);速度直写会打断自然
|
||||||
@@ -154,6 +159,10 @@ namespace Engine {
|
|||||||
JPH::RefConst<JPH::Shape> BuildCompoundShape(ECS::RigidBodyComponent& rb,
|
JPH::RefConst<JPH::Shape> BuildCompoundShape(ECS::RigidBodyComponent& rb,
|
||||||
ECS::TransformComponent& tr);
|
ECS::TransformComponent& tr);
|
||||||
void RemoveBody(u32 entityBits);
|
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; // 刚体驱动寻址
|
const BodyRecord* FindRecord(u32 entityBits) const; // 刚体驱动寻址
|
||||||
CharacterController* FindCharacter(u32 entityBits); // 角色门面寻址
|
CharacterController* FindCharacter(u32 entityBits); // 角色门面寻址
|
||||||
void QueueEvent(CollisionEvent&& ev); // 回调线程 append(多线程并发,加锁)
|
void QueueEvent(CollisionEvent&& ev); // 回调线程 append(多线程并发,加锁)
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ namespace Engine {
|
|||||||
s.mUp = JPH::Vec3::sAxisY();
|
s.mUp = JPH::Vec3::sAxisY();
|
||||||
s.mMass = std::max(params.mass, 1e-3f);
|
s.mMass = std::max(params.mass, 1e-3f);
|
||||||
s.mMaxSlopeAngle = glm::radians(params.maxSlopeAngle);
|
s.mMaxSlopeAngle = glm::radians(params.maxSlopeAngle);
|
||||||
|
// 静态三角网格地面/楼梯的内部边会产生 ghost contact,把角色顶起/
|
||||||
|
// 抖动。Jolt 增强内部边移除可消除(一次穿墙修正的额外开销),
|
||||||
|
// 对箱体/凸包地面无副作用
|
||||||
|
s.mEnhancedInternalEdgeRemoval = true;
|
||||||
s.mShape = new JPH::CapsuleShape(
|
s.mShape = new JPH::CapsuleShape(
|
||||||
std::max(params.radius, 0.01f),
|
std::max(params.radius, 0.01f),
|
||||||
std::max(params.halfHeight, 0.0f));
|
std::max(params.halfHeight, 0.0f));
|
||||||
|
|||||||
@@ -33,14 +33,54 @@
|
|||||||
#include <Jolt/Physics/Collision/CastResult.h>
|
#include <Jolt/Physics/Collision/CastResult.h>
|
||||||
#include <Jolt/Physics/Collision/RayCast.h>
|
#include <Jolt/Physics/Collision/RayCast.h>
|
||||||
#include <Jolt/Physics/Body/BodyLock.h>
|
#include <Jolt/Physics/Body/BodyLock.h>
|
||||||
|
#include <Jolt/Physics/Body/BodyFilter.h> // BodyDrawFilter(调试绘制按体过滤)
|
||||||
|
|
||||||
|
#include <Jolt/Core/IssueReporting.h>
|
||||||
|
|
||||||
#include <glm/gtc/quaternion.hpp>
|
#include <glm/gtc/quaternion.hpp>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cstdarg>
|
||||||
|
#include <cstdio>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
namespace Engine {
|
namespace Engine {
|
||||||
namespace {
|
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<std::string> seen;
|
||||||
|
std::lock_guard<std::mutex> 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 两层(非移动/移动),扩软体/传感器时再分层 ----
|
// ---- 层配置:MVP 两层(非移动/移动),扩软体/传感器时再分层 ----
|
||||||
constexpr JPH::ObjectLayer LAYER_NON_MOVING{0};
|
constexpr JPH::ObjectLayer LAYER_NON_MOVING{0};
|
||||||
constexpr JPH::ObjectLayer LAYER_MOVING{1};
|
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,
|
// Jolt ↔ glm 四元数直转(TransformComponent 旋转存 glm::quat,
|
||||||
// UE FTransform 语义):物理同步/写回不再经欧拉往返 → 姿态
|
// UE FTransform 语义):物理同步/写回不再经欧拉往返 → 姿态
|
||||||
// 零漂移(分量序:Jolt ctor (x,y,z,w),glm ctor (w,x,y,z))
|
// 零漂移(分量序:Jolt ctor (x,y,z,w),glm ctor (w,x,y,z))
|
||||||
@@ -167,6 +216,7 @@ namespace Engine {
|
|||||||
static std::once_flag s_JoltOnce;
|
static std::once_flag s_JoltOnce;
|
||||||
std::call_once(s_JoltOnce, [] {
|
std::call_once(s_JoltOnce, [] {
|
||||||
JPH::RegisterDefaultAllocator();
|
JPH::RegisterDefaultAllocator();
|
||||||
|
JPH::Trace = &JoltTrace; // 见文件顶部说明
|
||||||
JPH::Factory::sInstance = new JPH::Factory();
|
JPH::Factory::sInstance = new JPH::Factory();
|
||||||
JPH::RegisterTypes();
|
JPH::RegisterTypes();
|
||||||
});
|
});
|
||||||
@@ -288,7 +338,24 @@ namespace Engine {
|
|||||||
JPH::BodyManager::DrawSettings ds;
|
JPH::BodyManager::DrawSettings ds;
|
||||||
ds.mDrawShape = true;
|
ds.mDrawShape = true;
|
||||||
ds.mDrawShapeWireframe = 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) 约束(关节;软体/载具约束接入后自动生效)
|
// 2) 约束(关节;软体/载具约束接入后自动生效)
|
||||||
@@ -857,8 +924,13 @@ namespace Engine {
|
|||||||
// 退化三角(两索引相同)剔除:MeshShape 构建会因退化报错
|
// 退化三角(两索引相同)剔除:MeshShape 构建会因退化报错
|
||||||
if (idx[i] != idx[i + 1] && idx[i] != idx[i + 2] &&
|
if (idx[i] != idx[i + 1] && idx[i] != idx[i + 2] &&
|
||||||
idx[i + 1] != idx[i + 2]) {
|
idx[i + 1] != idx[i + 2]) {
|
||||||
tris.emplace_back(idx[i], idx[i + 1], idx[i + 2],
|
// 索引必须加基址偏移到拼接后的全局顶点表:IndexedTriangle
|
||||||
static_cast<u32>(0), base);
|
// 第 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<u32>(pos.size());
|
base += static_cast<u32>(pos.size());
|
||||||
@@ -1045,6 +1117,18 @@ namespace Engine {
|
|||||||
|
|
||||||
bool PhysicsWorld::RayCast(const glm::vec3 &origin, const glm::vec3 &dir,
|
bool PhysicsWorld::RayCast(const glm::vec3 &origin, const glm::vec3 &dir,
|
||||||
const f32 maxDist, RayHit &outHit) const {
|
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);
|
const f32 len = glm::length(dir);
|
||||||
if (len < 1e-8f) return false;
|
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::Vec3(dir.x / len, dir.y / len, dir.z / len) * maxDist);
|
||||||
|
|
||||||
JPH::RayCastResult hit;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user