FPlane相关
FPlane结构体定义在Engine/Source/Runtime/Core/Public/Math/Plane.h
MS_ALIGN(16) struct FPlane : public FVector { public: /** The w-component. */ float W; }
▲这是FPlane的数据成员的构成:由X、Y、Z、W构成。
▼下面的几个典型的构造函数可以看出这四个数据成员的意义:
FORCEINLINE FPlane::FPlane(float InX, float InY, float InZ, float InW) : FVector(InX,InY,InZ) , W(InW) {}
FORCEINLINE FPlane::FPlane(FVector InNormal, float InW) : FVector(InNormal) , W(InW) {}
FORCEINLINE FPlane::FPlane(FVector InBase, const FVector &InNormal) : FVector(InNormal) , W(InBase | InNormal) {}
FORCEINLINE FPlane::FPlane(FVector A, FVector B, FVector C) : FVector(((B-A)^(C-A)).GetSafeNormal()) { W = A | (FVector)(*this); }
▲可以清楚的看到,X、Y、Z存储的是Plane的Normal,W存储的是Plane上的一点与Normal的点积。由这个点积除以Normal,就可以还原出Plane上的这个点坐标。 当W=0时,表明该Plane过原点;当W>0时,表明Plane正空间不包含原点;当W<0时,表明Plane正空间包含原点。
▲可以形象认为Plane将空间分成两部分,在Plane上任选一点出发作出其法线,则法线所在的空间规定为正空间,另一半空间规定为负空间。
▲以+Z作为法线举例,则W=0时,平面就是XOY平面;当W>0时,平面沿+Z分布;当W<0时,平面沿-Z分布。
▲可以形象认为Plane将空间分成两部分,在Plane上任选一点出发作出其法线,则法线所在的空间规定为正空间,另一半空间规定为负空间。
▲以+Z作为法线举例,则W=0时,平面就是XOY平面;当W>0时,平面沿+Z分布;当W<0时,平面沿-Z分布。
▼下面分析几个FPlane的API的数学原理
- FPlane::PlaneDot
- FVector FMath::LinePlaneIntersection
- UKismetMathLibrary::LinePlaneIntersection
- PointPlaneProject
- VectorPlaneProject
FPlane::PlaneDot
/** * Calculates distance between plane and a point. * * @param P The other point. * @return >0: point is in front of the plane, <0: behind, =0: on the plane. */ FORCEINLINE float FPlane::PlaneDot(const FVector &P) const { return X * P.X + Y * P.Y + Z * P.Z - W; }
从其注释表明,这是计算点到Plane的距离的函数,同时要知道,Plane将空间分成了两部分,其法线正向所在的部分定义为正,另一部分定义为负。在正空间的点到Plane的距离为正,负空间的点到Plane的距离为负。
#189的解释简单直接
PointPlaneProject
/** * Calculate the projection of a point on the given plane. * * @param Point The point to project onto the plane * @param Plane The plane * @return Projection of Point onto Plane */ FVector FVector::PointPlaneProject(const FVector& Point, const FPlane& Plane) { //Find the distance of X from the plane //Add the distance back along the normal from the point return Point - Plane.PlaneDot(Point) * Plane; }
设投影点是Q,有OP = OQ + QP. 因此Q点坐标即OQ = OP - QP
QP = (Point到Plane的有向距离) * (Plane.Normal). 当P在正空间时,有向距离为正,QP结果与Normal同向;当P在负空间时,有向距离为负,QP结果与Normal反向。因此这里的QP计算式是正确的,符合事实。
章节(12.5) 平面
平面方程的隐式定义:
$$ax + by + cz = d\\
\textbf{p * n} = d$$
点到平面的距离
关于#130里FPlane的定义,其实就是n*p=d的形式。其中x、y、z分量存法线n,w存的d。
实际上,n*p就是OP在方向向量n上的投影,对于平面上所有的点P,其OP在n上的投影都是相同的,即具有相同的d。反过来也就是说:对于一个确定的方向向量n,若有OP在n上的投影(即n*p)相同(记为d),则称这些P在同一个平面上。因此可以用n*p = d来表达一个平面。
而点到平面的距离也可以直观的思考:任意点Q都可以求出其在n上的投影n*q,若Q在平面上,则有n*q=d,若Q在平面的正空间,则有n*q>d,反之若Q在平面的负空间,则有n*q<d。因此Q点到平面的有向距离就是:n*q - d
还有一种定义平面的方式:已知平面法线n,平面上一点p0,那么对于平面上的任一点p,都有n*(p-p0) = 0
FVector FMath::LinePlaneIntersection
inline FVector FMath::LinePlaneIntersection ( const FVector &Point1, const FVector &Point2, const FPlane &Plane ) { return Point1 + (Point2-Point1) * ((Plane.W - (Point1|Plane))/((Point2 - Point1)|Plane)); }
该功能还有一个实现,在KismetMathLibrary.h里:
bool UKismetMathLibrary::LinePlaneIntersection(const FVector& LineStart, const FVector& LineEnd, const FPlane& APlane, float& T, FVector& Intersection) { FVector RayDir = LineEnd - LineStart;
// Check ray is not parallel to plane if ((RayDir | APlane) == 0.0f) { T = -1.0f; Intersection = FVector::ZeroVector; return false; }
T = ((APlane.W - (LineStart | APlane)) / (RayDir | APlane));
// Check intersection is not outside line segment if (T < 0.0f || T > 1.0f) { Intersection = FVector::ZeroVector; return false; }
// Calculate intersection point Intersection = LineStart + RayDir * T;
return true; }
这两个实现其实是一样的原理,第二个可以当作第一个实现的详细注释版。
解释一下:其中(Vector | Plane)的操作是合法的,是因为FPlane是FVector的派生类型,因此(Vector | Plane)其实就是向量点乘,具体到这里就是Point点乘(Plane的Normal)。【|】运算符是对FVector.DotProduct的重载
就以Kismet里的蓝图函数来说:
- RayDir是从LineStart指向LineEnd的方向向量,(RayDir | APlane)是RayDir点乘APlane的Normal,若结果为0,表示RayDir⊥Normal,或说RayDir // APlane。
- 由 FPlane::PlaneDot 函数的解释可以知道(LineStart | APlane) - APlane.W是LineStart到APlane的有向距离,RayDir | APlane【即RayDir.Dot(APlane.Normal)】是RayDir在Normal上的投影,当RayDir与Normal基本同向时(是指RayDir与Normal正向夹角小于90°),投影值为正,否则为负。
- 取一种典型情况来分析:当LineStart在APlane分割出来的正空间中,LineEnd在负空间中,此时,LineStart与LineEnd连线与APlane有交点X,比例T=|X - LineStart| / |LineEnd - LineStart|. 这个比例根据比例理论,等于[(LineStart | APlane) - APlane.W] / |[RayDir | APlane]|,因为LineStart与LineEnd分处APlane分割的正负空间中,RayDir | APlane此时为负,所以 |[RayDir | APlane]| = - RayDir | APlane,最终得到比例T = [APlane.W - (LineStart | APlane)] / RayDir | APlane.
- 还有几种其他情形,比如LineStart与LineEnd分别在负空间和正空间,或都在正空间,或都在负空间,只要分析清楚每种情形下LineStart到APlane的有向距离值的符号,RayDir到APlane.Normal的投影值的符号,最终都会得出上面关于T的结论。
- 接下来函数检测了T值的范围,只关注了直线<LineStart, LineEnd>与APlane交点在LineStart与LineEnd连线之间的情况,简单点说就是这个函数只关注了线段[LineStart, LineEnd]与APlane有交点的情况,交点可以是两端点。这是这个蓝图函数与FMath::LinePlaneIntersection函数唯一的区别。FMath::LinePlaneIntersection函数也处理T在线段外的情况。因此,准确的说,FMath::LinePlaneIntersection函数名副其实,求解的是直线与平面的交点。而这里的蓝图函数求解的是线段与平面的交点,如果线段与平面没有交点,则返回false。
- 在FMath::LinePlaneIntersection()的实现中:当从P1出发射向P2的射线与Plane有交点时,T值为正,即((Plane.W - (Point1|Plane))/((Point2 - Point1)|Plane))>0;当从P2出发射向P1的射线与Plane有交点时,T值为负,即((Plane.W - (Point1|Plane))/((Point2 - Point1)|Plane))<0。并有:当P1与P2在Plane同一边时,|T|>1;当P1与P2分属Plane的两边时,|T|<1
PointPlaneProject
/** * Calculate the projection of a point on the given plane. * * @param Point The point to project onto the plane * @param Plane The plane * @return Projection of Point onto Plane */ FVector FVector::PointPlaneProject(const FVector& Point, const FPlane& Plane) { //Find the distance of X from the plane //Add the distance back along the normal from the point return Point - Plane.PlaneDot(Point) * Plane; }
设投影点是Q,有OP = OQ + QP. 因此Q点坐标即OQ = OP - QP
QP = (Point到Plane的有向距离) * (Plane.Normal). 当P在正空间时,有向距离为正,QP结果与Normal同向;当P在负空间时,有向距离为负,QP结果与Normal反向。因此这里的QP计算式是正确的,符合事实。
VectorPlaneProject
/** * Calculate the projection of a vector on the plane defined by PlaneNormal. * * @param V The vector to project onto the plane. * @param PlaneNormal Normal of the plane (assumed to be unit length). * @return Projection of V onto plane. */ FVector FVector::VectorPlaneProject(const FVector& V, const FVector& PlaneNormal) { return V - V.ProjectOnToNormal(PlaneNormal); }
设O为PlaneNormal上的一点(可以为原点),ON为PlaneNormal,将OV投影到ON为OQ,投影点Q为PlaneNormal上离V最近的点。此时QV即为所求:OV到Plane上的投影。
OQ + QV = OV => QV = OV - OQ
OQ = (V.Dot(Normal)) * Normal
因此,最后所求QV = OV - OQ = V - (V.Dot(Normal))* Normal.
其中,(V.Dot(Normal)) * Normal记为函数V.ProjectOnToNormal(Normal).
ColorCorrectRegionsCommon.usf中的RotateAboutAxis函数,其实现里的ClosestPointOnAxis就是Q点,UAxis就是QV
章节(12.5) 平面
平面方程的隐式定义:
$$ax + by + cz = d\\
\textbf{p * n} = d$$
点到平面的距离
关于#130里FPlane的定义,其实就是n*p=d的形式。其中x、y、z分量存法线n,w存的d。
实际上,n*p就是OP在方向向量n上的投影,对于平面上所有的点P,其OP在n上的投影都是相同的,即具有相同的d。反过来也就是说:对于一个确定的方向向量n,若有OP在n上的投影(即n*p)相同(记为d),则称这些P在同一个平面上。因此可以用n*p = d来表达一个平面。
而点到平面的距离也可以直观的思考:任意点Q都可以求出其在n上的投影n*q,若Q在平面上,则有n*q=d,若Q在平面的正空间,则有n*q>d,反之若Q在平面的负空间,则有n*q<d。因此Q点到平面的有向距离就是:n*q - d
还有一种定义平面的方式:已知平面法线n,平面上一点p0,那么对于平面上的任一点p,都有n*(p-p0) = 0
空间向量基本定理
如果三个向量a,b,c不共面,那么对空间 任一向量,都存在唯一的有序实数组x,y,z,使得
\[\begin{aligned} \textbf{p} = x\textbf{a} + y\textbf{b} + z\textbf{c}.\end{aligned}\]
或者:设O,A,B,C是不共面的四点,则对空间任一点P,都存在唯一的有序实数组x,y,z,使得$$\vec{OP} = x\vec{OA} + y\vec{OB} + z\vec{OC}$$
- 空间任意三个不共面向量都可以作为空间向量的一个基底;
- 如果三个向量a,b,c不共面,那么所有空间向量所组成的集合就是{p|p=xa+yb+zc, x,y,z∈R},这个集合可看作由向量a,b,c生成的,其中把{a, b, c}叫做空间的一个基底,a,b,c都叫做基向量;
- 一个基底是指一个向量组,一个基向量是指基底中的某一个向量,二者是相关联的不同的概念;
- 由于0可视为与任意非零向量共线,与任意两个非零向量共面,所以,三个向量不共面就隐含着它们都不是0.
Find Closest Point On Line
// 点投影到直线,或过点作直线的垂线与直线的交点. // 注意:当点在直线上时,函数获取的就是该点 FVector UKismetMathLibrary::FindClosestPointOnLine(FVector Point, FVector LineOrigin, FVector LineDirection) { const FVector SafeDir = LineDirection.GetSafeNormal(); const FVector ClosestPoint = LineOrigin + (SafeDir * ((Point-LineOrigin) | SafeDir)); return ClosestPoint; }