多年来一直盯着这个问题,一直没有想出完美的解决方案,我终于做到了!
这几乎是一个简单的算法,不需要循环和近似。
这是它在更高级别上的工作方式:
- 如果从当前点到未来点的路径穿过该平面,则计算与每一侧平面的相交时间。
- 检查每一边的象限是否有单边交叉点,返回交叉点。
- 确定圆相撞的角。
- 求解当前点、拐角和相交中心(远离拐角的半径)之间的三角形。
- 计算时间、法线和交点中心。
现在是血淋淋的细节!
函数的输入是边界(它有一个左、上、右、下)和一个当前点(开始)和一个未来点(结束)。
输出是一个名为 Intersection 的类,它有 x、y、time、nx 和 ny。
- {x, y} 是相交时的圆心。
- 时间是一个从 0 到 1 的值,其中 0 表示开始,1 表示结束
- {nx, ny} 为法线,用于反映速度以确定圆的新速度
我们从经常使用的缓存变量开始:
float L = bounds.left;
float T = bounds.top;
float R = bounds.right;
float B = bounds.bottom;
float dx = end.x - start.x;
float dy = end.y - start.y;
并计算与每一面的平面相交的时间(如果起点和终点之间的向量经过该平面):
float ltime = Float.MAX_VALUE;
float rtime = Float.MAX_VALUE;
float ttime = Float.MAX_VALUE;
float btime = Float.MAX_VALUE;
if (start.x - radius < L && end.x + radius > L) {
ltime = ((L - radius) - start.x) / dx;
}
if (start.x + radius > R && end.x - radius < R) {
rtime = (start.x - (R + radius)) / -dx;
}
if (start.y - radius < T && end.y + radius > T) {
ttime = ((T - radius) - start.y) / dy;
}
if (start.y + radius > B && end.y - radius < B) {
btime = (start.y - (B + radius)) / -dy;
}
现在我们尝试看看它是否严格来说是一个侧面交叉点(而不是拐角)。如果碰撞点位于侧面,则返回交点:
if (ltime >= 0.0f && ltime <= 1.0f) {
float ly = dy * ltime + start.y;
if (ly >= T && ly <= B) {
return new Intersection( dx * ltime + start.x, ly, ltime, -1, 0 );
}
}
else if (rtime >= 0.0f && rtime <= 1.0f) {
float ry = dy * rtime + start.y;
if (ry >= T && ry <= B) {
return new Intersection( dx * rtime + start.x, ry, rtime, 1, 0 );
}
}
if (ttime >= 0.0f && ttime <= 1.0f) {
float tx = dx * ttime + start.x;
if (tx >= L && tx <= R) {
return new Intersection( tx, dy * ttime + start.y, ttime, 0, -1 );
}
}
else if (btime >= 0.0f && btime <= 1.0f) {
float bx = dx * btime + start.x;
if (bx >= L && bx <= R) {
return new Intersection( bx, dy * btime + start.y, btime, 0, 1 );
}
}
我们已经走到了这一步,所以我们知道要么没有交叉路口,要么它与一个角落相撞。我们需要确定角点:
float cornerX = Float.MAX_VALUE;
float cornerY = Float.MAX_VALUE;
if (ltime != Float.MAX_VALUE) {
cornerX = L;
} else if (rtime != Float.MAX_VALUE) {
cornerX = R;
}
if (ttime != Float.MAX_VALUE) {
cornerY = T;
} else if (btime != Float.MAX_VALUE) {
cornerY = B;
}
// Account for the times where we don't pass over a side but we do hit it's corner
if (cornerX != Float.MAX_VALUE && cornerY == Float.MAX_VALUE) {
cornerY = (dy > 0.0f ? B : T);
}
if (cornerY != Float.MAX_VALUE && cornerX == Float.MAX_VALUE) {
cornerX = (dx > 0.0f ? R : L);
}
现在我们有足够的信息来求解三角形。这使用距离公式,找到两个向量之间的角度,以及正弦定律(两次):
double inverseRadius = 1.0 / radius;
double lineLength = Math.sqrt( dx * dx + dy * dy );
double cornerdx = cornerX - start.x;
double cornerdy = cornerY - start.y;
double cornerdist = Math.sqrt( cornerdx * cornerdx + cornerdy * cornerdy );
double innerAngle = Math.acos( (cornerdx * dx + cornerdy * dy) / (lineLength * cornerdist) );
double innerAngleSin = Math.sin( innerAngle );
double angle1Sin = innerAngleSin * cornerdist * inverseRadius;
// The angle is too large, there cannot be an intersection
if (Math.abs( angle1Sin ) > 1.0f) {
return null;
}
double angle1 = Math.PI - Math.asin( angle1Sin );
double angle2 = Math.PI - innerAngle - angle1;
double intersectionDistance = radius * Math.sin( angle2 ) / innerAngleSin;
现在我们解决了所有边和角度,我们可以确定时间和其他所有内容:
// Solve for time
float time = (float)(intersectionDistance / lineLength);
// If time is outside the boundaries, return null. This algorithm can
// return a negative time which indicates the previous intersection.
if (time > 1.0f || time < 0.0f) {
return null;
}
// Solve the intersection and normal
float ix = time * dx + start.x;
float iy = time * dy + start.y;
float nx = (float)((ix - cornerX) * inverseRadius);
float ny = (float)((iy - cornerY) * inverseRadius);
return new Intersection( ix, iy, time, nx, ny );
哇!这很有趣……就效率而言,这有很大的改进空间。您可以对侧交叉点检查重新排序,以便尽早逃脱,同时尽可能少地进行计算。
我希望有一种方法可以在没有三角函数的情况下做到这一点,但我不得不让步!
这是我调用它并使用它来计算圆的新位置的示例,使用法线进行反射,并使用相交时间来计算反射幅度:
Intersection inter = handleIntersection( bounds, start, end, radius );
if (inter != null)
{
// Project Future Position
float remainingTime = 1.0f - inter.time;
float dx = end.x - start.x;
float dy = end.y - start.y;
float dot = dx * inter.nx + dy * inter.ny;
float ndx = dx - 2 * dot * inter.nx;
float ndy = dy - 2 * dot * inter.ny;
float newx = inter.x + ndx * remainingTime;
float newy = inter.y + ndy * remainingTime;
// new circle position = {newx, newy}
}
我已经在pastebin 上发布了完整的代码,其中包含一个完全交互式的示例,您可以在其中绘制起点和终点,并显示时间和由此产生的矩形反弹。
如果您想让它立即运行,您必须从 my blog 下载代码,否则将其粘贴到您自己的 Java2D 应用程序中。
编辑:
我已经修改了 pastebin 中的代码以包含碰撞点,并且还做了一些速度改进。
编辑:
您可以通过使用该矩形的角度来修改旋转矩形,以取消旋转具有圆形起点和终点的矩形。您将执行相交检查,然后旋转生成的点和法线。
编辑:
如果圆的路径的边界体积不与矩形相交,我修改了 pastebin 上的代码以提前退出。