From d5a86c170ec0a0ba5721cfa45ff9f2626754719c Mon Sep 17 00:00:00 2001 From: atdunbg Date: Sat, 3 Oct 2026 17:43:55 +0800 Subject: [PATCH] fix(renderer): mesh collider source positions in render space for skinned meshes Skinned meshes keep vertices in raw mesh space (bakeTransform=false); rendering transforms them via bind palette (global(bone) x invBind, folds FBX unit scale). Physics MeshShape/ConvexHull and the mesh bounding sphere consumed the raw positions directly -> M1911 hull measured ~10x the visual size. ParseFromDisk now bakes bind-palette-skinned positions per vertex (same formula as the skinning shader) into ParsedMesh::cpuPositions; Mesh uses them for m_CpuPositions and bounds. GPU vertex data and rendering are untouched. --- Engine/include/Engine/Renderer/Mesh.h | 5 ++- Engine/include/Engine/Renderer/Model.h | 7 +++++ Engine/src/Renderer/Mesh.cpp | 26 +++++++++++----- Engine/src/Renderer/Model.cpp | 43 ++++++++++++++++++++++++-- 4 files changed, 70 insertions(+), 11 deletions(-) diff --git a/Engine/include/Engine/Renderer/Mesh.h b/Engine/include/Engine/Renderer/Mesh.h index 1abd2fe..78a9812 100644 --- a/Engine/include/Engine/Renderer/Mesh.h +++ b/Engine/include/Engine/Renderer/Mesh.h @@ -28,10 +28,13 @@ namespace Engine public: // material 非拥有指针:Model 的材质缓存拥有(同贴图共享一份) // lods[0] 必须为原始索引(与 vertices 对应),各级简化链由解析期生成 + // cpuPositions 非空(蒙皮 mesh)= 物理数据源与包围球改用它 + //(绑定姿态渲染空间,与视觉同源;vertices.position 是蒙皮原始空间) Mesh(Device& device, MemoryAllocator& allocator, const std::vector& vertices, const std::vector>& lods, - Material* material); + Material* material, + const std::vector* cpuPositions = nullptr); ~Mesh(); Mesh(const Mesh&) = delete; diff --git a/Engine/include/Engine/Renderer/Model.h b/Engine/include/Engine/Renderer/Model.h index 9b00cf7..531780a 100644 --- a/Engine/include/Engine/Renderer/Model.h +++ b/Engine/include/Engine/Renderer/Model.h @@ -42,6 +42,13 @@ namespace Engine std::array texPaths; MaterialParams params; bool transparent = false; // glTF alphaMode=BLEND(透明绘制队列) + bool skinned = false; // 有骨骼(渲染走 palette 蒙皮) + + // 蒙皮 mesh 专属:绑定姿态渲染空间的位置副本(palette(bind) 按 + // 顶点权重混合,与 shader 同式)。非空 = Mesh 的物理数据源与 + // 包围球改用它——FBX 单位换算折在骨骼链上(bind palette ≠ I), + // 直接用原始顶点会让物理凸包大一个数量级(M1911 实测 10×) + std::vector cpuPositions; }; // ---- 骨骼动画数据(assimp aiNode/aiAnimation/aiBone 的引擎侧镜像)---- diff --git a/Engine/src/Renderer/Mesh.cpp b/Engine/src/Renderer/Mesh.cpp index 7beabd7..ccfb838 100644 --- a/Engine/src/Renderer/Mesh.cpp +++ b/Engine/src/Renderer/Mesh.cpp @@ -25,7 +25,8 @@ namespace Engine Mesh::Mesh(Device& device, MemoryAllocator& allocator, const std::vector& vertices, const std::vector>& lods, - Material* material) + Material* material, + const std::vector* cpuPositions) : m_Material(material) , m_Slot(s_nextMeshSlot.fetch_add(1, std::memory_order_relaxed)) { @@ -35,11 +36,19 @@ namespace Engine return; } + // 位置取数:蒙皮 mesh 用绑定姿态渲染空间副本(与视觉同源), + // 否则用原始顶点位置 + const bool useBind = cpuPositions != nullptr && + cpuPositions->size() == vertices.size(); + auto posAt = [&](size_t i) -> const glm::vec3& { + return useBind ? (*cpuPositions)[i] : vertices[i].position; + }; + // CPU 位置副本:物理 MeshShape/ConvexHull 数据源(GPU 上传后 // 完整 Vertex 不再保留,见 Mesh.h) m_CpuPositions.reserve(vertices.size()); - for (const auto& v : vertices) { - m_CpuPositions.push_back(v.position); + for (size_t i = 0; i < vertices.size(); ++i) { + m_CpuPositions.push_back(posAt(i)); } m_CpuIndices = lods[0]; @@ -78,18 +87,19 @@ namespace Engine } // 局部空间包围球:顶点包围盒 → 中心 + 最大半径(视锥剔除用) + // 蒙皮 mesh 用绑定姿态副本(原始空间包围球大一个数量级 = 剔除/拾取失真) { glm::vec3 mn(std::numeric_limits::max()); glm::vec3 mx(std::numeric_limits::lowest()); - for (const auto& v : vertices) { - mn = glm::min(mn, v.position); - mx = glm::max(mx, v.position); + for (size_t i = 0; i < vertices.size(); ++i) { + mn = glm::min(mn, posAt(i)); + mx = glm::max(mx, posAt(i)); } m_Bounds.center = (mn + mx) * 0.5f; m_Bounds.radius = 0.0f; - for (const auto& v : vertices) { + for (size_t i = 0; i < vertices.size(); ++i) { m_Bounds.radius = glm::max(m_Bounds.radius, - glm::length(v.position - m_Bounds.center)); + glm::length(posAt(i) - m_Bounds.center)); } } diff --git a/Engine/src/Renderer/Model.cpp b/Engine/src/Renderer/Model.cpp index 85c2c2f..85a14f7 100644 --- a/Engine/src/Renderer/Model.cpp +++ b/Engine/src/Renderer/Model.cpp @@ -238,6 +238,42 @@ ParsedModel Model::ParseFromDisk(const std::string& path) bone.name); } } + + // 蒙皮 mesh 的 CPU 位置副本换算到渲染空间:渲染 = palette(t)×raw + //(palette = global(bone)×invBind,与 shader 同式),物理/包围球取 + // 绑定姿态。FBX 的单位换算折在骨骼链上(bind palette ≠ I),直接 + // 用 raw 会让物理凸包大一个数量级(M1911 实测 10×) + if (!out.bones.empty()) { + std::vector globals(out.nodes.size()); + for (size_t i = 0; i < out.nodes.size(); ++i) { + globals[i] = out.nodes[i].parent < 0 + ? out.nodes[i].localTransform + : globals[out.nodes[i].parent] * out.nodes[i].localTransform; + } + std::vector bindPalette(out.bones.size()); + for (size_t b = 0; b < out.bones.size(); ++b) { + bindPalette[b] = globals[out.bones[b].nodeIdx] + * out.bones[b].invBind; + } + for (auto& pm : out.meshes) { + if (!pm.skinned) continue; + pm.cpuPositions.resize(pm.vertices.size()); + for (size_t v = 0; v < pm.vertices.size(); ++v) { + const Vertex& vert = pm.vertices[v]; + glm::vec3 p(0.0f); + f32 wsum = 0.0f; + for (int k = 0; k < 4; ++k) { + const f32 w = vert.weights[k]; + if (w <= 0.0f) continue; + p += w * glm::vec3(bindPalette[vert.boneIds[k]] + * glm::vec4(vert.position, 1.0f)); + wsum += w; + } + // 零权重顶点(shader 侧塌到原点)物理侧保留原始位置兜底 + pm.cpuPositions[v] = wsum > 1e-6f ? p : vert.position; + } + } + } } ENGINE_INFO("Model parsed: {} ({} meshes, {} bones, {} anims)", @@ -383,7 +419,8 @@ Scope Model::FromParsed(Device& device, MemoryAllocator& allocator, for (auto& pm : parsed.meshes) { Material* mat = model->GetOrCreateMaterial(pm.texPaths.data(), pm.params); model->m_Meshes.push_back(CreateScope( - device, allocator, pm.vertices, pm.lods, mat)); + device, allocator, pm.vertices, pm.lods, mat, + pm.cpuPositions.empty() ? nullptr : &pm.cpuPositions)); } ENGINE_INFO("Model loaded: {} ({} meshes, {} textures)", parsed.path, model->m_Meshes.size(), model->m_TextureCache.size()); @@ -406,7 +443,8 @@ void Model::LoadModel(const std::string& path) for (auto& pm : parsed.meshes) { Material* mat = GetOrCreateMaterial(pm.texPaths.data(), pm.params); m_Meshes.push_back(CreateScope( - m_Device, m_Allocator, pm.vertices, pm.lods, mat)); + m_Device, m_Allocator, pm.vertices, pm.lods, mat, + pm.cpuPositions.empty() ? nullptr : &pm.cpuPositions)); } ENGINE_INFO("Model loaded: {} ({} meshes, {} textures)", path, m_Meshes.size(), m_TextureCache.size()); @@ -577,6 +615,7 @@ ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene, // 骨骼权重灌顶点(无骨骼 mesh 顶点保持默认 idx0/w1 → identity 直通) CollectMeshBones(mesh, vertices, bones, boneSlots); + out.skinned = hasBones; for (u32 i = 0; i < mesh->mNumFaces; ++i) { aiFace face = mesh->mFaces[i];