矩形旋转碰撞,OBB方向包围盒算法实现

转载注明:http://www.scottcgi.com/?p=121

如何进行2D旋转矩形的碰撞检测,可以使用一种叫OBB的检测算法(Oriented bounding box)方向包围盒。这个算法是基于SAT(Separating Axis Theorem)分离轴定律的。而OBB不仅仅是计算矩形的碰撞检测,而是一种算法模型。简单解释一下概念,包围盒和分离轴定律。

包围盒:是根据物体的集合形状,来决定盒子的大小和方向,这样可以选择最紧凑的盒子来代表物体。见下图

黑色的就是包围盒,可以是凸多边形,最贴近检测物体即可。

 

分离轴定律:两个凸多边形物体,如果我们能找到一个轴,使得两个在物体在该轴上的投影互不重叠,则这两个物体之间没有碰撞发生,该轴为Separating Axis

那么用一般去检测那些轴呢,垂直于多边形每条边的轴。如下图:

所以,分离轴定律变成,两个多边形在所有轴上的投影都发生重叠,则判定为碰撞;否则,没有发生碰撞。

 

 

下面,我只考虑矩形的情况,如何检测分离轴。

很明显,矩形4条边,有4条检测轴,那么2个矩形就有8个。但是矩形有2个轴是重复的,所以只需要检测2条轴就可以了,既是矩形的两条互相垂直的边所在的轴。

如上图,判断碰撞,我们需要判断2个矩形在4个轴上的投影是否重叠。这里有2种可能的方式。第一种,把每个矩形的4个顶点投影到一个轴上,这样算出4个顶点最长的连线距离,以后同样对待第二个矩形,最后判断2个矩形投影距离是否重叠。

第二种方式,把2个矩形的半径距离投影到轴上,以后把2个矩形的中心点连线投影到轴上,以后判断2个矩形的中心连线投影,和2个矩形的半径投影之和的大小。本文使用这种方式。

 

这里用到一些向量的数学知识。如下图:

P点为矩形在X轴上的投影点,矩形在垂直轴上的投影点为原点。这里也能看出来,点P所在的矩形轴, 在X轴上的投影长度为OP,如果矩形逆时针绕远点O旋转,OP在X轴上的投影长度变小,直到为0,OP垂直于X轴。也就是,OP在X轴上的投影长度的最大与最小值。这也解释了,为什么我们选择检测轴为垂直于多边形边的轴,因为在这些轴上我们能取到极值,中间的那些轴就没必要检测了。

如何表示轴,我们需要用向量,正确的使用单位向量,能够简化模型,降低思考的难度。如下图:

假设P点的坐标为(px, py), 那么向量P就是(px, py),点P在X轴上的投影点Q坐标是(qx, qy),那么向量Q就是(qx, qy)。我们假设X轴上的单位向量是(1, 0)。那么向量P和X轴上单位向量点乘有:

向量P * X轴单位向量 = |P| * |X轴单位向量| * cosPQ  = px * 1 + py * 0 = px

又因为单位向量的长度等于1所以,px就是向量Q的长度。这是非常有意义的,我们就得到一个规律,就是把一个向量点乘一个单位向量,我们得到的是这个向量在这个单位向量上的投影长度。用代码表示为:

?
1
2
3
4
5
6
/**
  * dot-multiply
  */
private float dot( float [] axisA, float [] axisB) {
     return Math.abs(axisA[ 0 ] * axisB[ 0 ] + axisA[ 1 ] * axisB[ 1 ]);
}

这里float[] 存放的是一个点的x ,y 坐标。axisB 为单位向量,这个结果就是axisA向量在,单位向量axisB上投影的长度。

 

 

下面我们看一下,单位向量如何表示:

单位向量是用单位圆来描述的。假设这个圆的半径为1,那么圆上的任何一个坐标到原点构成的向量都可以看作一个单位向量,并且长度为1。这般,明显的点P就是一个单位向量。点P在单位圆上移动,那么这个单位向量就在旋转,向量P就和角A建立了关系。很明显的得出,cosA 就是向量P的X坐标,sinA 就是向量P的Y坐标。

这样我们就可以得出,单位向量P为(cosA,sinA)。这个模型的意义就是把单位向量P可以看成矩形的条边。如下图:

那么矩形的另一个边对应的单位向量S如何表示呢,向量S和向量P是垂直的,我们可以得出, S(-sinA, cosA), 向量S 点乘 向量P  = 0

至此,我们就可以通过一个旋转角度,得到一个矩形的2个检测轴的单位向量。代码如下:

?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// unit vector of x axis
private float [] axisX;
// unit vector of y axis
private float [] axisY;
 
// 0 -360
private float rotation;
 
/**
  * Set axis x and y by rotation
  *
  * @param rotation float 0 - 360
  */
public OBB setRotation( float rotation) {
     this .rotation = rotation;
 
     this .axisX[ 0 ] = MathUtils.cos(rotation);
     this .axisX[ 1 ] = MathUtils.sin(rotation);
 
     this .axisY[ 0 ] = -MathUtils.sin(rotation);
     this .axisY[ 1 ] = MathUtils.cos(rotation);
 
     return this ;
}

 

下一步如何计算矩形的半径投影呢,什么又是半径投影呢,看下图:

橙色线段,是矩形的2条检测轴,3张图是矩形旋转的3个特殊位置截图。蓝色线段就是矩形半径投影。其实就是,矩形在X轴上最远处的交点,数学上意义就是2条检测轴的投影之和。

2条检测轴的向量和就是中心点到矩形一个顶点的向量,所以投影半径也是中心点到矩形顶点的向量投影长度。注意向量的方向会影响投影长度。按照中间那幅图,2条检测轴向量和的投影是,2条检测轴投影的差值。如果把其中一个轴,旋转180度,那么2个检测轴和的投影就是,2条轴投影的和值。

 

至此,如果我们把矩形在任意角度的2条轴向量投影到单位向量上,根据前面的单位向量规律。我们就得到了轴向量在单位向量上投影的长度,而单位向量的长度为1,那么我们得到的就是轴向量与单位向量的比例。在用这个比例乘以轴向量的长度,就得到了轴的投影长度,就能求出轴半径的长度了。如图

2个矩形检测过程中,每次以一个矩形的检测轴为坐标系,投影另一个矩形的检测轴。图中,蓝色线段为左边矩形的半径投影,黄色线段为右边矩形检测轴。我们需要把右边2条检测轴投影到蓝色线段所在X轴的单位向量,得到投影比例,以后在乘以2条检测轴的长度,就可以得到右边矩形的半径投影。

红色线段为2个矩形的中心点连心,计算其在X轴的投影长度。比较中心点连线的投影长度与2矩形的半径投影长度之和,如果连线投影大,那么在这条轴上没有碰撞,否则碰撞。半径投影代码如下:

?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
private float halfWidth;
 
private float halfHeight;
 
/**
  * Get axisX and axisY projection radius distance on axis
  */
public float getProjectionRadius( float [] axis) {
 
     // axis, axisX and axisY are unit vector
 
     // projected axisX to axis
     float projectionAxisX = this .dot(axis, this .axisX);
     // projected axisY to axis
     float projectionAxisY = this .dot(axis, this .axisY);
 
     return this .halfWidth * projectionAxisX + this .halfHeight * projectionAxisY;
}

 

判断2矩形最终是否碰撞,需要依次检测4个分离轴,如果在一个轴上没有碰撞,则2个矩形就没有碰撞。代码如下:

?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
  * OBB is collision with other OBB
  */
public boolean isCollision(OBB obb) {
     // two OBB center distance vector
     float [] centerDistanceVertor = {
             this .centerPoint[ 0 ] - obb.centerPoint[ 0 ],
             this .centerPoint[ 1 ] - obb.centerPoint[ 1 ]
     };
 
     float [][] axes = {
             this .axisX,
             this .axisY,
             obb.axisX,
             obb.axisY,
     };
 
     for ( int i = 0 ; i < axes.length; i++) {
         // compare OBB1 radius projection add OBB2 radius projection to centerDistance projection
         if ( this .getProjectionRadius(axes[i]) + obb.getProjectionRadius(axes[i])
                 <= this .dot(centerDistanceVertor, axes[i])) {
             return false ;
         }
     }
 
     return true ;
}

 

最后,给出OBB完整的代码封装。

?
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
/**
  * @author scott.cgi
  * @since  2012-11-19
 
  * Oriented bounding box
  */
public class OBB {
 
     private float [] centerPoint;
 
     private float halfWidth;
 
     private float halfHeight;
 
     // unit vector of x axis
     private float [] axisX;
     // unit vector of y axis
     private float [] axisY;
 
     // 0 -360
     private float rotation;
 
     /**
      * Create default OBB
      *
      * @param x Top left x
      * @param y Top left y
      * @param width
      * @param height
      */
     public OBB( float x, float y, float width, float height) {
 
         this .axisX = new float [ 2 ];
         this .axisY = new float [ 2 ];
 
         this .setRotation( 0 .0f);
 
         this .halfWidth  = width / 2 ;
         this .halfHeight = height / 2 ;
 
         this .centerPoint = new float [ 2 ];
 
         this .setXY(x, y);
     }
 
     /**
      * Get axisX and axisY projection radius distance on axis
      */
     public float getProjectionRadius( float [] axis) {
 
         // axis, axisX and axisY are unit vector
 
         // projected axisX to axis
         float projectionAxisX = this .dot(axis, this .axisX);
         // projected axisY to axis
         float projectionAxisY = this .dot(axis, this .axisY);
 
         return this .halfWidth * projectionAxisX + this .halfHeight * projectionAxisY;
     }
 
     /**
      * OBB is collision with other OBB
      */
     public boolean isCollision(OBB obb) {
         // two OBB center distance vector
         float [] centerDistanceVertor = {
                 this .centerPoint[ 0 ] - obb.centerPoint[ 0 ],
                 this .centerPoint[ 1 ] - obb.centerPoint[ 1 ]
         };
 
         float [][] axes = {
                 this .axisX,
                 this .axisY,
                 obb.axisX,
                 obb.axisY,
         };
 
         for ( int i = 0 ; i < axes.length; i++) {
             // compare OBB1 radius projection add OBB2 radius projection to centerDistance projection
             if ( this .getProjectionRadius(axes[i]) + obb.getProjectionRadius(axes[i])
                     <= this .dot(centerDistanceVertor, axes[i])) {
                 return false ;
             }
         }
 
         return true ;
     }
 
     /**
      * dot-multiply
      */
     private float dot( float [] axisA, float [] axisB) {
         return Math.abs(axisA[ 0 ] * axisB[ 0 ] + axisA[ 1 ] * axisB[ 1 ]);
     }
 
     /**
      * Set axis x and y by rotation
      *
      * @param rotation float 0 - 360
      */
     public OBB setRotation( float rotation) {
         this .rotation = rotation;
 
         this .axisX[ 0 ] = MathUtils.cos(rotation);
         this .axisX[ 1 ] = MathUtils.sin(rotation);
 
         this .axisY[ 0 ] = -MathUtils.sin(rotation);
         this .axisY[ 1 ] = MathUtils.cos(rotation);
 
         return this ;
     }
 
     /**
      * Set OBB top left x, y
      */
     public OBB setXY( float x, float y) {
         this .centerPoint[ 0 ] = x + this .halfWidth;
         this .centerPoint[ 1 ] = y + this .halfHeight;
 
         return this ;
     }
 
     public float getRotation() {
         return this .rotation;
     }
 
     public float getX() {
         return this .centerPoint[ 0 ] - this .halfWidth;
     }
 
     public float getY() {
         return this .centerPoint[ 1 ] - this .halfHeight;
     }
 
     public float getWidth() {
         return this .halfWidth * 2 ;
     }
 
     public float getHeight() {
         return this .halfHeight * 2 ;
     }
 
}

 

  • 0
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
实现obb包围算法的MATLAB代码如下: ```matlab function [obb, corners] = compute_obb(points) % points为n*3的点云,obb为向量[中心点,长度,宽度,高度,旋转角度],corners为obb的8个顶点 % 计算协方差矩阵 cov_mat = cov(points); % 计算特征向量和特征值 [eigenvec, eigenval] = eig(cov_mat); % 将特征值从大到小排序,并将其对应的特征向量排序 [~, ind] = sort(diag(eigenval), 'descend'); eigenvec = eigenvec(:,ind); % 计算obb中心点坐标 center = mean(points); % 计算obb长度、宽度和高度 length = sqrt(eigenval(ind(1), ind(1))); width = sqrt(eigenval(ind(2), ind(2))); height = sqrt(eigenval(ind(3), ind(3))); % 计算obb旋转角度 x_axis = eigenvec(:,1); y_axis = eigenvec(:,2); z_axis = eigenvec(:,3); rotation_mat = [x_axis, y_axis, z_axis]; rotation_angle = [acosd(rotation_mat(1,1)), acosd(rotation_mat(2,1)), acosd(rotation_mat(3,1))]; % 计算obb的8个顶点 corners = zeros(8,3); corners(1,:) = center + length/2 * x_axis + width/2 * y_axis + height/2 * z_axis; corners(2,:) = center + length/2 * x_axis + width/2 * y_axis - height/2 * z_axis; corners(3,:) = center + length/2 * x_axis - width/2 * y_axis + height/2 * z_axis; corners(4,:) = center + length/2 * x_axis - width/2 * y_axis - height/2 * z_axis; corners(5,:) = center - length/2 * x_axis + width/2 * y_axis + height/2 * z_axis; corners(6,:) = center - length/2 * x_axis + width/2 * y_axis - height/2 * z_axis; corners(7,:) = center - length/2 * x_axis - width/2 * y_axis + height/2 * z_axis; corners(8,:) = center - length/2 * x_axis - width/2 * y_axis - height/2 * z_axis; % 将obb信息存储在向量中 obb = [center, length, width, height, rotation_angle]; end ``` 该代码实现obb包围算法,输入为一个n*3的点云,输出为一个obb向量和8个obb的顶点坐标。其中obb向量包含中心点坐标、长度、宽度、高度和旋转角度。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值