检测一个点是否在三角形内
1. 夹角求和
当△ABC中有一点P,很容易得到如下关系:
∠APB+∠BPC+∠CPA=2π
但是,P点在三角形外时:
{∠APB+∠BPC+∠CPA∠APB=2∠APB<π⇒∠APB+∠BPC+∠CPA<2π
实现起来很简单:
function sumCheck(triangle, p) {
const pA = new THREE.Vector3().subVectors(triangle.a, p);
const pB = new THREE.Vector3().subVectors(triangle.b, p);
const pC = new THREE.Vector3().subVectors(triangle.c, p);
const angleAPB = pA.angleTo(pB);
const angleBPC = pB.angleTo(pC);
const angleCPA = pC.angleTo(pA);
return Math.abs(angleAPB + angleBPC + angleCPA - Math.PI * 2) < 1e-10
}
关于向量之间的夹角,我们这里直接使用了Three.js
定义好的方法,其实就是通过向量“点积”求出余弦值,然后通过反三角函数求出角度。这里就不再赘述了。
这个方法的好处就是算法简单,便于理解。但是,在求夹角的时候使用了反三角函数,这是比较耗时,接下来介绍一个更快的算法。
2. 同侧检测
上图中,点P在△ABC中的充要条件是:
- 点P在AB的下侧
- 点P在BC的左侧
- 点p在AC的右侧
但是,上下左右这样的描述并不准确,也不通用;我们将它转换为更通用的描述方式:
- AP在AB的顺时针方向
- BP在BC的顺时针方向
- CP在CA的顺时针方向
好像看起来通用了,但是,如果是一个空间中的三角形,从一个方向我们上面的说法没有问题;从另一个方向看,如果要保证正确,我们就要把上面的说法全改成逆时针。
怎么知道它在那个方向?
当我们比较P和AB时,我们可以利用C,如果P在三角形内,P和C一定是同侧的!!!
所以,我们再次修改我们上面的算法:
- AP和AC在AB的同侧
- BP和BA在BC的同侧
- CP和CB在CA的同侧
那用数学怎么判断同侧呢?向量叉乘可以判断方向
要判断AP和AC是不是在AB的同一侧,只需要将AP和AC分别与AB做向量积,如果它们结果的方向相同,那就是在同一侧。即,direction(AP×AB)是否等于direction(AP×AB)。
下面我们用代码实现它:
function sameSide(v1, v2, base) {
const direction1 = new THREE.Vector3().crossVectors(v1, base);
const direction2 = new THREE.Vector3().crossVectors(v2, base);
return direction1.dot(direction2) > 0;
}
function sameSideCheck(triangle, p) {
const _AB = new THREE.Vector3().subVectors(triangle.b, triangle.a);
const _BC = new THREE.Vector3().subVectors(triangle.c, triangle.b);
const _CA = new THREE.Vector3().subVectors(triangle.a, triangle.c);
const _BA = _AB.clone().negate();
const _AC = _CA.clone().negate();
const _CB = _BC.clone().negate();
const _AP = new THREE.Vector3().subVectors(p, triangle.a);
const _BP = new THREE.Vector3().subVectors(p, triangle.b);
const _CP = new THREE.Vector3().subVectors(p, triangle.c);
return sameSide(_AP, _AC, _AB) && sameSide(_BP, _BA, _BC) && sameSide(_CP, _CB, _CA);
}
相比于之前的夹角求和的方式,我们这里只是用了向量的内积和外积,运算速度更快。
3. 重心坐标
A,B,C三个点可以确定一个平面,平面上一点P可以通过A,B,C的线性组合表示:
设,则,化简得,AP=uAB+vACP−A=u(B−A)+v(C−A)P=(1−u−v)A+uB+vC
即,P=αA+βB+γC,α+β+γ=1
坐标(α,β,γ)点P关于A,B,C的重心坐标。
那为什么叫重心坐标呢?我们看一下计算重心的方式:
设,有n个质点,质点的坐标位置和质量分别为Pi和mi(i=1...n);则,这些质点的重心的坐标为:
P=i=1∑nmii=1∑nmi⋅Pi
从形式上看,就是将质量当作权值的一个加权平均数:
P=i=1∑nmim1⋅P1+i=1∑nmim1⋅P1+...+i=1∑nmimn⋅Pn记,w1=i=1∑nmim1,w2=i=1∑nmim2,...,wn=i=1∑nmim1则,w1+w2+...+wn=1
当n=3时,正是我们上面推导的公式:
P=αA+βB+γC,α+β+γ=1
(w1,w2,w3)就是重心坐标(α,β,γ)。
重心坐标在三角形内的判断条件
我们把上面的公式变一下形式:P=A+uAB+vAC
从几何上解释这个公式就是:
- A为起点
- 沿着AB的方向走了u
- 沿着AC的方向走了v
那u,v分别在什么范围内可以保证P不“走出”三角形呢?
如果u,v有一个小于0,那一定落在三角形外面。
如果u,v有一个大于1,那一定落在三角形外面
现在,我们知道了u,v都在[0,1]区间内,接下来讨论一下0
如果它们其中有一个为0,P点就会落在AB或AC边所在的直线上,另一个必须是[0,1]区间内的一个值,才能落在三角形的边上。
当它们都为0时,P就落在了A点。
关于在边上是不是属于三角形内,这个怎么认为都可以,我们这里就认为边上的点在三角形内。
因此,我们得到了u,v都必须在[0,1]区间内。
现在我们还差最后一个坐标的范围,它很简单,就是1−(u+v),它的取值范围是什么呢?不防先来看下u+v。
当u+v=1时,就表示P=(1−u−v)A+uB+vC=uB+(1−u)C
P=uB+(1−u)C这是什么?正是BC边,也就是说,当u+v=1时,P点必定落在BC边上。一旦u+v>1,那就“走出”了三角形。
因此,我们可以确定0<u+v<1⇒0<1−(u+v)<1。
综上,我们可以得出,当P的重心坐标的三个值都属于[0,1]区间,则P点在三角形内。
所以,一旦我们知道了重心坐标,判断起来会相当简单。
function baryCoordCheck(triangle, p) {
const baryCoord = getBaryCoordCheck(triangle, p);
return baryCoord.x >= 0 && baryCoord.y >= 0 && baryCoord.z <= 1
}
现在的问题是,怎么求重心坐标?
重心坐标的求法
P−A令,v0v1v2得,v2=u(C−A)+v(B−A)=C−A=B−A=P−A=uv0+vv1
⇒{v2⋅v0v2⋅v1=(uv0+vv1)⋅v0=(uv0+vv1)⋅v1{v2⋅v0v2⋅v1=u(v0⋅v0)+v(v0⋅v1)=u(v0⋅v1)+v(v1⋅v1)
两个未知数u,v可以通过求解上述方程组得到:
u=(v0⋅v0)(v1⋅v1)−(v0⋅v1)(v1⋅v0)(v1⋅v1)(v2⋅v0)−(v1⋅v0)(v2⋅v1)v=(v0⋅v0)(v1⋅v1)−(v0⋅v1)(v1⋅v0)(v0⋅v0)(v2⋅v1)−(v0⋅v1)(v2⋅v0)
Three.js
使用的就是这样的算法,我们直接看源码:
static getBarycoord( point, a, b, c, target ) {
_v0.subVectors( c, a );
_v1.subVectors( b, a );
_v2.subVectors( point, a );
const dot00 = _v0.dot( _v0 );
const dot01 = _v0.dot( _v1 );
const dot02 = _v0.dot( _v2 );
const dot11 = _v1.dot( _v1 );
const dot12 = _v1.dot( _v2 );
const denom = ( dot00 * dot11 - dot01 * dot01 );
if ( denom === 0 ) {
return target.set( - 2, - 1, - 1 );
}
const invDenom = 1 / denom;
const u = ( dot11 * dot02 - dot01 * dot12 ) * invDenom;
const v = ( dot00 * dot12 - dot01 * dot02 ) * invDenom;
return target.set( 1 - u - v, v, u );
}
同样,检查一个点是否在三角形内,也有对应得实现:
static containsPoint( point, a, b, c ) {
this.getBarycoord( point, a, b, c, _v3 );
return ( _v3.x >= 0 ) && ( _v3.y >= 0 ) && ( ( _v3.x + _v3.y ) <= 1 );
}
验证代码
我们这里借助canvas
写一段模拟代码,用来验证我们的函数。
首先,我们需要定义我们的三角形。我们假定视窗大小为8
,然后用canvas
将这个尺寸为8
的视窗画出来。
为了坐标的统一,需要将坐标除以视窗大小,规范化到[0,1]。
参考文章
Point in triangle test (blackpawn.com)