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.
This commit is contained in:
2026-10-03 17:43:55 +08:00
parent e1cef53158
commit d5a86c170e
4 changed files with 70 additions and 11 deletions
+41 -2
View File
@@ -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<glm::mat4> 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<glm::mat4> 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> 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<Mesh>(
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<Mesh>(
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];