寻求通过旋转碰撞体避免二维图形相交的高效旋转角度计算方法
你的循环逐度旋转试错的方式确实效率拉胯,尤其是需要大角度调整的时候——完全没必要一次次碰运气!咱们可以利用分离轴测试(SAT)的数学本质,直接推导出能让图形不相交的旋转角度范围,一步找到最接近当前角度的合法值,彻底抛弃低效循环。
核心思路:从SAT推导旋转约束
对于绕原点旋转的多边形(你的场景里主要是矩形),它的每条边的法向量会跟着旋转角度θ变化。我们的目标是找到所有θ,让平移后的矩形A和旋转后的矩形B在所有分离轴上的投影都不重叠。
针对矩形的情况,分离轴只需要考虑两类:
- 矩形A的两条边的法向量(固定不变,因为A只做水平平移)
- 矩形B的两条边的法向量(随θ旋转,因为B绕原点转动)
对每个分离轴,我们可以写出投影不重叠的不等式,解出θ的合法区间,最后取所有区间的交集,就是所有能避免相交的θ范围,再从中选最接近当前角度的那个值就行。
具体步骤(以矩形为例)
1. 先明确矩形参数
假设:
- 可平移矩形A:水平平移量为
tx,固定y坐标ty,半宽wA,半高hA(边平行坐标轴) - 绕原点旋转的矩形B:半宽
wB,半高hB,当前旋转角度为θ0(弧度制)
2. 逐个分析分离轴的约束
轴1:矩形A的水平法向量(1,0)
矩形A的投影范围:[tx - wA, tx + wA]
旋转后矩形B的投影范围:[-wB|cosθ| - hB|sinθ|, wB|cosθ| + hB|sinθ|]
不重叠条件:tx + wA < -wB|cosθ| - hB|sinθ| 或 tx - wA > wB|cosθ| + hB|sinθ|
轴2:矩形A的垂直法向量(0,1)
矩形A的投影范围:[ty - hA, ty + hA]
旋转后矩形B的投影范围:[-wB|sinθ| - hB|cosθ|, wB|sinθ| + hB|cosθ|]
不重叠条件同理,这里因为你的A只水平平移,这个约束大概率一直满足,可以简化处理。
轴3:矩形B的水平法向量(cosθ, sinθ)
矩形A的投影:tx*cosθ + ty*sinθ ± (wA|cosθ| + hA|sinθ|)
矩形B的投影:±wB
不重叠条件:tx*cosθ + ty*sinθ + wA|cosθ| + hA|sinθ| < -wB 或 tx*cosθ + ty*sinθ - wA|cosθ| - hA|sinθ| > wB
轴4:矩形B的垂直法向量(-sinθ, cosθ)
矩形A的投影:-tx*sinθ + ty*cosθ ± (wA|sinθ| + hA|cosθ|)
矩形B的投影:±hB
不重叠条件类似轴3。
3. 解不等式找合法θ区间
这些不等式可以用三角恒等式简化,比如a cosθ + b sinθ = c可以转化为R cos(θ - φ) = c(其中R=√(a²+b²),φ=arctan2(b,a)),进而解出θ的取值范围,得到合法的角度区间。
4. 选择最优旋转角度
从所有合法区间里,找到距离当前旋转角度θ0最近的θ值,直接设置即可,完全不需要循环试错。
优化后的核心代码
// 计算合法的旋转角度(弧度制) function findValidRotation(tx, ty, wA, hA, wB, hB, currentθ) { const R = Math.sqrt(wB**2 + hB**2); const phi = Math.atan2(hB, wB); const threshold = Math.abs(tx) - wA; let validIntervals = []; // 处理矩形A水平轴的约束 if (threshold > 0) { const c = threshold / R; if (Math.abs(c) < 1) { const alpha = Math.acos(c); // 转换为[0, 2π]范围内的区间 let interval1 = [clampAngle(phi - alpha), clampAngle(phi + alpha)]; let interval2 = [clampAngle(phi + Math.PI - alpha), clampAngle(phi + Math.PI + alpha)]; validIntervals.push(interval1, interval2); } } // 这里可以补充其他分离轴的约束处理,合并区间 // ... // 找到距离当前角度最近的合法值 let bestθ = currentθ; let minDiff = Infinity; for (const [start, end] of validIntervals) { // 检查区间端点和当前角度的距离 const candidates = [start, end, currentθ].map(clampAngle); for (const θ of candidates) { const diff = Math.min(Math.abs(currentθ - θ), 2*Math.PI - Math.abs(currentθ - θ)); if (diff < minDiff) { minDiff = diff; bestθ = θ; } } // 如果当前角度已经在合法区间内,直接保留 if (isAngleInInterval(currentθ, start, end)) { bestθ = currentθ; break; } } return bestθ; } // 将角度归一到[0, 2π]范围 function clampAngle(θ) { θ = θ % (2 * Math.PI); return θ < 0 ? θ + 2 * Math.PI : θ; } // 判断角度是否在区间内(处理角度的循环性) function isAngleInInterval(θ, start, end) { θ = clampAngle(θ); start = clampAngle(start); end = clampAngle(end); if (start <= end) { return θ >= start && θ <= end; } else { return θ >= start || θ <= end; } }
替换你的循环逻辑
把原来的do-while循环直接替换成下面的代码,一步到位设置合法角度:
// 替换原代码中的循环部分 var polyO = [{x:pO[0].X(),y:pO[0].Y()},{x:pO[1].X(),y:pO[1].Y()},{x:pO[2].X(),y:pO[2].Y()},{x:pO[3].X(),y:pO[3].Y()}]; // 提取矩形参数(对应你代码中的尺寸) const tx = sO.Value(); const ty = -2; // 矩形O的固定y坐标 const wA = 1.5; // 矩形O半宽(原w=3) const hA = 0.5; // 矩形O半高(原h=1) const wB = 2.5; // 矩形C半宽(原w=5) const hB = 3; // 矩形C半高(原h=6) const currentθ = sC.Value() * Math.PI / 180; // 转弧度 const validθ = findValidRotation(tx, ty, wA, hA, wB, hB, currentθ); sC.setValue(validθ * 180 / Math.PI); // 转回角度制设置 board.update();
扩展到圆形和三角形
- 圆形:不需要SAT,只需要计算旋转后的多边形顶点到圆心的最小距离是否大于半径,直接推导旋转角度约束即可。
- 三角形:和矩形逻辑一致,分离轴增加三角形的三条边的法向量,同样通过解不等式找到合法角度区间。
内容的提问来源于stack exchange,提问作者deblocker




