You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

寻求通过旋转碰撞体避免二维图形相交的高效旋转角度计算方法

高效计算避免图形相交的旋转角度

你的循环逐度旋转试错的方式确实效率拉胯,尤其是需要大角度调整的时候——完全没必要一次次碰运气!咱们可以利用分离轴测试(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θ| < -wBtx*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

火山引擎 最新活动