fix: apply scale and padding to mesh collision bodies - #3
Conversation
ConvexMesh ignored both: the shape switch in SelfMask had an empty MESH branch, ConvexMesh had no scale or padding of its own, and updateInternalData copied the vertices through unchanged behind a "uniform scaling placeholder" comment. Only sphere, box and cylinder bodies ever saw the configured values. A URDF that ships .dae collisions therefore ran every link at zero padding, and the mask kept only points strictly inside the hull -- returns off the surface of a thin part such as a leg sit a fraction outside it and survived. Measured on a Go2 walking bag: points within 3 cm of a leg fell from 10.7 to 0.19 per frame once 2 cm of padding actually reached the mesh, and to 0.01 at 4 cm, with the obstacle returns in front of the robot unchanged (23.2 -> 23.5). Padding shifts every face of the convex hull outward, which for unit-length plane normals is a straight comparison against the signed distance. The bounding box grows with it too, since it is the first rejection test in containsPoint and would otherwise drop the padded points before the plane test.
Padding goes through the plane test and the bounding box, so it applies to containsPoint. Scale only reaches the vertices, which feed intersectsRay and the markers, leaving containment unscaled.
Follow-up to the padding fix. setScale existed but only moved the vertices, so containsPoint behaved as if the body were unscaled while intersectsRay and the published markers used the scaled one. Bring the query point back into the unscaled frame instead of growing the hull. Note the direction: the code this replaces multiplied the point by the scale, which shrinks the effective body as the scale grows. Dividing is what makes a scale above 1 mean a larger body, and that is what the measurement shows -- on a Go2 walking bag with zero padding, removal goes 2.7 points per frame at scale 0.5, 17.9 at 1.0 and 40.4 at 1.5. Padding is given in metres, so it divides by the scale to stay in metres once the comparison moves into the unscaled frame. Rename the two members to m_scale and m_padding to match every other body here.
The containment test divides the padding by the scale, so a non-positive scale turns that term into infinity, reports every point as inside and deletes the whole cloud without a word.
There was a problem hiding this comment.
approveです。直していただく必要があるものはありません。
containsPoint()がscaleで割る変更は正しいことを確認しました。単位立方体でscaleとpaddingを変えて実測し、期待値と一致することを確認しています。
気づいた点を2つ、インラインコメントに書きました。
- setPadding()にNaNを渡すガードが無く、1行で塞げます
- Known limitationに「ずれる向きは自己点が残る側」と一行加えると親切です
Known limitationについて補足すると、実meshで測った最大のずれはbase.daeの約6cmでした。ただしscaleが1.0なら厳密にゼロになり、現状のconfigはすべてscale 1.0なので影響はありません。
他のopen PRとの関係も共有します。#1 、#2 とはファイルが重複せず、実際にマージして確認したので、この PR は他の2本を待たずに先に取り込めます。
| void setPadding(double p) | ||
| { | ||
| m_padding = p; | ||
| updateInternalData(); | ||
| } |
There was a problem hiding this comment.
non-blocking
setScale()は非正値やNaNを1.0にフォールバックしますが、setPadding()にはそのガードがありません。
m_paddingにNaNを渡すと、内外判定がすべての点をinside扱いにしてしまい、点群が丸ごと消えます。
提案: m_padding = std::isfinite(p) ? p : 0.0; の1行で塞げます。
| m_scaledVertices.resize(m_vertices.size()); | ||
| for (size_t i=0; i<m_vertices.size(); i++) | ||
| { | ||
| // Uniform scaling placeholder | ||
| m_scaledVertices[i] = m_vertices[i]; | ||
| tf2::Vector3 v = m_vertices[i] - m_meshCenter; | ||
| double norm = v.length(); | ||
| if (norm > 1e-9) | ||
| m_scaledVertices[i] = m_meshCenter + v * (m_scale + m_padding / norm); | ||
| else | ||
| m_scaledVertices[i] = m_vertices[i]; | ||
| } |
There was a problem hiding this comment.
non-blocking
ここで作るm_scaledVerticesは、少し上のintersectsRay()の内外判定に使われます。intersectsRay()が使う面(m_planes)は常に未スケールなので、面と三角形がずれます。
そのぶんレイの当たり判定がわずかに外側まで伸びます。単位立方体ではpad 0.02で約11.5mm外側まで伸びました。
この構造自体は本PR以前からあり、paddingが常に0だったため今まで表面化していませんでした。
Root cause
scaleandpaddingnever reached mesh collision bodies.SelfMaskapplies them per shape type, but theshapes::MESHcase of that switch was acomment-only stub, and
ConvexMeshhad nothing to call: nosetScale()/setPadding(), nom_scale/m_paddingmembers. Three further places insideConvexMeshwere placeholders aswell:
containsPoint()—ip = m_meshCenter + (ip - m_meshCenter);, an identity.updateInternalData()— the bounding box got neither scale nor padding,m_radiusB = m_meshRadiusBignored both, and
m_scaledVertices[i] = m_vertices[i]copied the vertices unscaled despite the name.isPointInsidePlanes()—dist > 0.0, with no padding offset.So padding worked for spheres, boxes and cylinders only. On a URDF whose collisions are all
.daemeshes the filter effectively ran at padding 0 no matter what the config said, dropping only points
strictly inside the convex hull. Returns off thin parts such as legs land just outside it and survived.
Confirmed inert rather than merely weak: padding
0.02and0.10produced identical output(137.0 / 5.9 / 8.9 points, the same on both).
This is a regression, not a design choice
Before the ROS 2 rewrite,
406eacehadConvexMesh::updateInternalData()callm_boundingBox.setPadding(m_padding)/setScale(m_scale), computem_radiusB = m_meshRadiusB * m_scale + m_padding, scale the vertices bym_scale + m_padding / l,and subtract
m_paddingin the plane test.792e81a("added launch files and params",2025-01-16, 852 changed lines in
bodies.cppalone) replaced all of it with the"Uniform scaling placeholder"stubs this PR removes.Fixes
ConvexMeshgainsm_scale/m_paddingandsetScale()/setPadding()— the singlescale/padding pair a sphere takes.
shapes::MESHcase inSelfMaskcalls them.406eace: padded bounding box,m_radiusB,scaled vertices, and
dist > m_paddingin the plane test.containsPoint()divides the query point by the scale instead of multiplying. The planesdescribe the unscaled hull, so dividing is what makes
scale > 1a larger body;406eacemultiplied, which shrinks it. This is the one place where the restored code intentionally differs
from the pre-rewrite version.
setScale()falls back to1.0on a non-positive value.m_padding / m_scalein the plane testwould otherwise be infinite, report every point as inside, and silently delete the whole cloud.
Verification
Real Unitree Go2, walking rosbag,
default_sphere_paddingnow reaching the 18 mesh links of therobot description. Points within 3 cm of a leg centre line, per frame:
Obstacle returns in front of the robot over the same bag: 23.2 → 23.5 points/frame, i.e. unchanged.
Nothing real is being eaten by the padding.
Throughput on the same bag at padding 0.04: 14.32 Hz out against a 14.63 Hz input
(859 vs 878 frames over 60 s), so the added work keeps up with the sensor.
Known limitation
scale != 1is not exercised by the configuration this was measured with (it uses1.0). Thebounding-box growth is not exact for a mesh whose bounding-box centre differs from its mesh centre,
because the box is scaled about its own centre while
containsPoint()measures about the meshcentre. Padding is unaffected.
🤖 Generated with Claude Code