C++和OpenGL实现3D游戏编程【连载26】——添加TransformComponent组件(设置子物体的位移、旋转、缩放)
1、本节要实现的内容
在上一节我们了解了组件的概念,接下来我们就要了解一下Transform组件。这个组件非常的常用,Transform是最基础且核心的组件之一,每个GameObject(游戏对象)都默认拥有一个Transform组件。它负责管理游戏对象的位置(Position)、旋转(Rotation)和缩放(Scale),是三维空间变换的核心。 Transform是Unity中连接场景中所有物体空间属性的桥梁。它不仅定义了物体的基本状态(位置、旋转、缩放),还通过层级关系管理复杂的场景结构。本节实现了角色朝向问题,黄色石头人与红色石头人的方向始终保持一致;绿色石头人始终面向着红色石头人的方向;蓝色石头人也始终朝着红色石头人的方向,并且红色石头人上下移动时,蓝色石头人也会改变仰俯的角度。这使用了四元数方便模型任意角度旋转。示例如下:
角色朝向问题(四元数的应用)
1.1、设置Transform组件
Transform 组件是每个游戏对象(GameObject)都自带的基础组件,它定义了游戏对象在三维空间中的位置、旋转和缩放信息,决定了游戏对象在场景中的基本状态和空间关系。所有的游戏对象都需要通过 Transform 组件来确定其在场景中的位置和姿态。
Transform组件定义了一个物体在三维(或二维)空间中的状态,具体包括:
-
位置(Position):物体在世界坐标系或父对象坐标系中的位置,用(x, y, z)三个分量表示。
-
旋转(Rotation):物体的朝向或旋转角度,可以通过欧拉角(Euler Angles)或四元数(Quaternion)表示。
-
缩放(Scale):物体的大小缩放比例,默认值为(1, 1, 1),表示原始大小。
class TransformComponent:public Component
{private://物体在世界空间下的位置、角度和缩放比例;欧拉角为弧度值。glm::vec3 WorldPosition;glm::vec3 WorldEulerAngle;glm::vec3 WorldScale;//物体再局部空间下的位置、角度和缩放比例;欧拉角为弧度值。glm::vec3 LocalPosition;glm::vec3 LocalEulerAngle;glm::vec3 LocalScale;private://父子结构中将局部坐标转换为世界坐标的矩阵组合glm::mat4 LocalToWorldMatrix;//父子结构中将世界坐标转换为局部坐标的矩阵组合glm::mat4 WorldToLocalMatrix;public://返回相应的向量,物体在世界空间下的位置、角度和缩放比例glm::vec3 GetWorldPosition(){return WorldPosition;};glm::vec3 GetWorldEulerAngle(){return WorldEulerAngle;};glm::vec3 GetWorldScale(){return WorldScale;};//返回相应的向量,物体再局部空间下的位置、角度和缩放比例glm::vec3 GetLocalPosition(){return LocalPosition;};glm::vec3 GetLocalEulerAngle(){return LocalEulerAngle;};glm::vec3 GetLocalScale(){return LocalScale;};//返回相应的矩阵glm::mat4 GetLocalToWorldMatrix(){return LocalToWorldMatrix;}glm::mat4 GetWorldToLocalMatrix(){return WorldToLocalMatrix;}private://根据局部坐标数据更新世界坐标数据void UpdateWorldPositionData();void UpdateWorldEulerAngleData();void UpdateWorldScaleData();public://初始化TransformComponent();//获取当前矩阵位移、旋转和缩放的组合矩阵;参数1表示仅返回位移矩阵;参数2表示仅返回位旋转阵;参数3表示仅返回缩放矩阵;参数0表示返回三种矩阵glm::mat4 GetAbsoluteMatrix(int iTempMatrixType=0);//获取当前矩阵位移、旋转和缩放的组合逆矩阵;参数1表示仅返回位移矩阵;参数2表示仅返回位旋转阵;参数3表示仅返回缩放矩阵;参数0表示返回三种矩阵glm::mat4 GetRelativeMatrix(int iTempMatrixType=0);//根据父子层级关系获取局部转世界的组合矩阵(从根节点开始计算,时间成本略高,但准确率性高)glm::mat4 GetLocalToWorldMatrixFromRootGameObject(int iTempMatrixType=0,GameObject *ptGameObject=NULL);//根据父子层级关系获取局部转世界的组合逆矩阵(从根节点开始计算,时间成本略高,但准确率性高)glm::mat4 GetWorldToLocalMatrixFromRootGameObject(int iTempMatrixType=0,GameObject *ptGameObject=NULL);//根据父子层级关系获取局部转世界的组合矩阵(非递归方式,为了节省每帧的计算成本)glm::mat4 GetLocalToWorldMatrixFromParentGameObject();//根据父子层级关系获取局部转世界的组合逆矩阵(非递归方式,为了节省每帧的计算成本)glm::mat4 GetWorldToLocalMatrixFromParentGameObject();//设置局部坐标位置glm::vec3 SetLocalPosition(glm::vec3 tempLocalPosition);//设置局部坐标旋转glm::vec3 SetLocalEulerAngle(glm::vec3 tempLocalEulerAngle);//设置局部坐标缩放glm::vec3 SetLocalScale(glm::vec3 tempLocalScale);//设置世界坐标位置glm::vec3 SetWorldPosition(glm::vec3 tempWorldPosition);//设置世界坐标旋转glm::vec3 SetWorldEulerAngle(glm::vec3 tempWorldEulerAngle);//设置世界坐标缩放glm::vec3 SetWorldScale(glm::vec3 tempWorldScale);//让模型始终看向某个目标点,不考虑仰俯角的情况,第二个参数tempSourcePoint表示要将模型那个方向面对目标点,默认为Z轴方向void LookAt(glm::vec3 tempTargetPoint,glm::vec3 tempSourcePoint=glm::vec3(0.0f,0.0f,1.0f));//让模型始终看向某个目标点,要考虑仰俯角的情况,第二个参数tempSourcePoint表示要将模型那个方向面对目标点,默认为Z轴方向void LookRotation(glm::vec3 tempTargetPoint,glm::vec3 tempSourcePoint=glm::vec3(0.0f,0.0f,1.0f));public://显示子节点信息,返回显示所有信息后的光标位置,参数iIndentLevel表示缩进层级,参数iEchoIndex子物体的统计编号glm::vec2 Hierarchy_ShowComponentNodeInformation(HWND hWnd,HDC hDC,float x,float y,int iIndentLevel=0,int iEchoIndex=0,float h=20);};TransformComponent::TransformComponent()
{//当前物体的名称SetName("TransformComponent");//当前物体的类型SetType("TransformComponent");//物体在世界空间下的位置、角度和缩放比例WorldPosition=glm::vec3(0.0f,0.0f,0.0f);WorldEulerAngle=glm::vec3(0.0f,0.0f,0.0f);WorldScale=glm::vec3(1.0f,1.0f,1.0f);//物体在局部空间下的位置、角度和缩放比例LocalPosition=glm::vec3(0.0f,0.0f,0.0f);LocalEulerAngle=glm::vec3(0.0f,0.0f,0.0f);LocalScale=glm::vec3(1.0f,1.0f,1.0f);//父子结构中将局部坐标转换为世界坐标的矩阵组合LocalToWorldMatrix=glm::mat4(1.0f);//父子结构中将世界坐标转换为局部坐标的矩阵组合WorldToLocalMatrix=glm::mat4(1.0f);}
-
LocalPosition:相对于父对象的本地坐标。如果没有父对象,则等同于世界坐标。
-
Worldposition:世界坐标系中的绝对位置。
-
LocalEulerAngles: 以欧拉角表示的本地旋转。
-
WorldEulerAngles: 以欧拉角表示的世界旋转。
-
LocalScale:相对于父对象的缩放比例。
-
WorldScale:世界坐标系下的缩放,受父对象缩放影响。
由于GameObject类还管理对象的层级结构(Hierarchy),可以通过GameObject实例的父子层级关系来控制Transform组件,来组织场景中的物体位置、旋转和缩放。
2、获取局部转世界矩阵和世界转局部矩阵
由于物体之间存在父子关系,父子物体之间的位置、旋转和缩放就依然存在关系,简单的理解,局部矩阵就是子物体相对于父物体的矩阵就叫做局部矩阵,保存了相对于父物体的位置旋转和缩放信息;世界矩阵是在相对于父物体的局部矩阵基础上,在综合考虑父物体的矩阵,以及递归父物体和父物体之间的矩阵关系,从而获得该子物体相对于世界坐标的真正位置,旋转和缩放信息,那我们首先来看一下如何从子物体的位置,旋转和缩放信息获取局部矩阵。
下边这个GetAbsoluteMatrix就是在获取当前物体的位移、旋转、缩放矩阵的组合矩阵,注意先后顺序即可,先位移,后旋转,再缩放。注意这里为了方便,旋转中我们旋转轴的顺序是先Y轴,再X轴,最后Z轴。
glm::mat4 TransformComponent::GetAbsoluteMatrix(int iTempMatrixType)
{glm::mat4 tempMatrix=glm::mat4(1.0f);//先位移,后旋转,再缩放if(iTempMatrixType==0 || iTempMatrixType==1){tempMatrix=glm::translate(tempMatrix,this->LocalPosition);}//先位移,后旋转,再缩放if(iTempMatrixType==0 || iTempMatrixType==2){tempMatrix=glm::rotate(tempMatrix,this->LocalEulerAngle.y,glm::vec3(0.0f,1.0f,0.0f));tempMatrix=glm::rotate(tempMatrix,this->LocalEulerAngle.x,glm::vec3(1.0f,0.0f,0.0f));tempMatrix=glm::rotate(tempMatrix,this->LocalEulerAngle.z,glm::vec3(0.0f,0.0f,1.0f));}//先位移,后旋转,再缩放if(iTempMatrixType==0 || iTempMatrixType==3){tempMatrix=glm::scale(tempMatrix,this->LocalScale);}return tempMatrix;}下边这个GetRelativeMatrix函数所有操作都和上边GetAbsoluteMatrix的操作相反,是一种逆向的操作。我们可以看到与前边顺序(先位移,后旋转,再缩放)刚好相反,同时注意位移、旋转、缩放的参数值发生了细微的变化。glm::mat4 TransformComponent::GetRelativeMatrix(int iTempMatrixType)
{glm::mat4 tempMatrix=glm::mat4(1.0f);//先缩放,后旋转,再位移if(iTempMatrixType==0 || iTempMatrixType==3){tempMatrix=glm::scale(tempMatrix,1.0f/this->LocalScale);}//先缩放,后旋转,再位移if(iTempMatrixType==0 || iTempMatrixType==2){tempMatrix=glm::rotate(tempMatrix,this->LocalEulerAngle.z*(-1.0f),glm::vec3(0.0f,0.0f,1.0f));tempMatrix=glm::rotate(tempMatrix,this->LocalEulerAngle.x*(-1.0f),glm::vec3(1.0f,0.0f,0.0f));tempMatrix=glm::rotate(tempMatrix,this->LocalEulerAngle.y*(-1.0f),glm::vec3(0.0f,1.0f,0.0f));}//先缩放,后旋转,再位移if(iTempMatrixType==0 || iTempMatrixType==1){tempMatrix=glm::translate(tempMatrix,this->LocalPosition*(-1.0f));}return tempMatrix;}
这里其实要实现的内容很简单,就是计算出那一级子物体的位置旋转和缩放矩阵,获取该物体的矩阵组合,就是将这些矩阵简单的相乘,但是为了能便捷的获取该物体的有一种矩阵,我们设置了变量参数iTempType,比如说当这个参数为零时,返回的矩阵就是位置旋转和缩放矩阵的相乘;如果我们的参数为1时,函数返回的就仅仅是位置矩阵;当参数为2时,函数返回的就仅仅是旋转矩阵。为什么要这样呢?是由于我们在后期单纯计算固体在世界坐标的旋转或者缩放或者位移信息时,就要用到某种单一的矩阵。做好了以上准备,我们就可以轻松的获取世界局部转世界矩阵和世界转局部矩阵了。
2.1、获取局部转世界矩阵
我们也可以看到,存在父子关系的树形结构时,获取局部转世界矩阵无非就时将各级父子关系的矩阵相乘即可。对,你没有看错,就是这么简单,我们轻松的获取了局部转世界矩阵。这里就是用了一个循环函数将该物体以及树状结构中的所有父物体的矩阵相乘即可。
glm::mat4 TransformComponent::GetLocalToWorldMatrixFromRootGameObject(int iTempMatrixType,GameObject *ptGameObject)
{glm::mat4 tempResultMatrix=glm::mat4(1.0f);//如果用户指定物体为空,则默认为该组件的拥有者物体GameObject *ptTempGameObject=(ptGameObject==NULL?ptOwner:ptGameObject);//循环获取局部矩阵并相乘while(ptTempGameObject!=NULL){tempResultMatrix=ptTempGameObject->Transform.GetAbsoluteMatrix(iTempMatrixType)*tempResultMatrix;ptTempGameObject=ptTempGameObject->ptParent;}return tempResultMatrix;}
由于我们Transform中的LocalToWorldMatrix会在后边位置、方向、缩放的改变时不断更新并保存矩阵,因此为了高效获取局部转世界矩阵,我们会直接使用父物体的这个矩阵以达到提供计算效率的目的。
glm::mat4 TransformComponent::GetLocalToWorldMatrixFromParentGameObject()
{GameObject *ptGameObject=this->ptOwner;//如果组件没有父物体,则不进行计算if(ptGameObject==NULL){return glm::mat4(1.0f);}//通过非递归循环的方式,获取父子关系中的组合矩阵if(ptGameObject->ptParent!=NULL){return ptGameObject->ptParent->Transform.GetLocalToWorldMatrix()*GetAbsoluteMatrix();}else{return GetAbsoluteMatrix();}}
2.2、获取世界转局部矩阵
同样的道理,我们可以轻松的获取世界转局部矩阵,这里矩阵相乘的顺序也是于GetLocalToWorldMatrixFromRootGameObject函数相反的。
glm::mat4 TransformComponent::GetWorldToLocalMatrixFromRootGameObject(int iTempMatrixType,GameObject *ptGameObject)
{glm::mat4 tempResultMatrix=glm::mat4(1.0f);//如果用户指定物体为空,则默认为该组件的拥有者物体GameObject *ptTempGameObject=(ptGameObject==NULL?ptOwner:ptGameObject);//循环获取局部矩阵并相乘while(ptTempGameObject!=NULL){tempResultMatrix=tempResultMatrix*ptTempGameObject->Transform.GetRelativeMatrix(iTempMatrixType);ptTempGameObject=ptTempGameObject->ptParent;}return tempResultMatrix;}
由于我们Transform中的WorldToLocalMatrix会在后边位置、方向、缩放的改变时不断更新并保存矩阵,因此为了高效获取世界转局部矩阵,我们会直接使用父物体的这个矩阵以达到提供计算效率的目的。
glm::mat4 TransformComponent::GetWorldToLocalMatrixFromParentGameObject()
{GameObject *ptGameObject=this->ptOwner;//如果组件没有父物体,则不进行计算if(ptGameObject==NULL){return glm::mat4(1.0f);}//通过非递归循环的方式,获取父子关系中的组合矩阵if(ptGameObject->ptParent!=NULL){return GetRelativeMatrix()*ptGameObject->ptParent->Transform.GetWorldToLocalMatrix();}else{return GetRelativeMatrix();}}
2.3、从局部坐标转换为世界坐标
这里我们。
void TransformComponent::UpdateWorldPositionData()
{//获取转换矩阵glm::mat4 tempLocalToWorldPositionMatrix=LocalToWorldMatrix;//计算世界位置坐标WorldPosition=glm::vec3(tempLocalToWorldPositionMatrix*glm::vec4(0.0f,0.0f,0.0f,1.0f));}void TransformComponent::UpdateWorldEulerAngleData()
{//获取旋转矩阵glm::mat4 tempLocalToWorldRotateMatrix=GetLocalToWorldMatrixFromRootParentGameObject(glm::mat4(1.0f),2);//计算世界旋转矩阵glm::extractEulerAngleXYZ(tempLocalToWorldRotateMatrix,WorldEulerAngle.x,WorldEulerAngle.y,WorldEulerAngle.z);}void TransformComponent::UpdateWorldScaleData()
{//获取缩放矩阵glm::mat4 tempLocalToWorldRotateMatrix=GetLocalToWorldMatrixFromRootParentGameObject(glm::mat4(1.0f),3);//计算世界位置坐标WorldScale=glm::vec3(tempLocalToWorldRotateMatrix[0][0],tempLocalToWorldRotateMatrix[1][1],tempLocalToWorldRotateMatrix[2][2]);}
3、局部转世界矩阵和世界转局部矩阵的应用
我们现在通过以上函数就能轻松获取局部转世界矩阵和世界转局部矩阵了,那这些矩阵有什么用那?我么来研究一下:
3.1、将局部坐标转变为世界坐标
比如我们设置Transform组件的LocalPosition为glm::vec3(0.0f,0.0f,0.0f),也就是局域坐标中的原点,那么,当该物体假如拥有多次父子关系,我们怎么求出该局部坐标对应的世界坐标呢?聪明的你一下就猜到了,就用下边这个计算就可以了。
WorldPosition=glm::vec3(GetLocalToWorldMatrixFromRootGameObject()*glm::vec4(0.0f,0.0f,0.0f,1.0f));
当然我们这里要注意一下,这里GetLocalToWorldMatrixFromRootGameObject()获得的矩阵为4阶齐次矩阵,因此在计算时,也要将位置向量变成glm::vec4格式,待转换完毕后,在转换成glm::vec3格式。
同样的道理,我们还可以将局部坐标内的任意坐标转换为世界坐标,比如glm::vec3(10.0f,20.0f,30.0f)转换成世界坐标。
WorldPosition=glm::vec3(GetLocalToWorldMatrixFromRootGameObject()*glm::vec4(10.0f,20.0f,30.0f,1.0f));
以上计算出的WorldPosition就是真正的世界坐标了。
3.2、将世界坐标转变为局部坐标
比如我们设置Transform组件的WorldPosition为glm::vec3(0.0f,0.0f,0.0f),也就是世界坐标中的原点,那么,当该物体假如拥有多次父子关系,我们怎么求出该世界坐标对应的局部坐标呢?聪明的你一下就猜到了,就用下边这个计算就可以了。
LocalPosition=glm::vec3(GetWorldToLocalMatrixFromRootGameObject()*glm::vec4(0.0f,0.0f,0.0f,1.0f));
当然我们这里要注意一下,这里GetWorldToLocalMatrixFromRootGameObject()获得的矩阵为4阶齐次矩阵,因此在计算时,也要将位置向量变成glm::vec4格式,待转换完毕后,在转换成glm::vec3格式。
同样的道理,我们还可以将局部坐标内的任意坐标转换为世界坐标,比如glm::vec3(10.0f,20.0f,30.0f)转换成世界坐标。
LocalPosition=glm::vec3(GetWorldToLocalMatrixFromRootGameObject()*glm::vec4(10.0f,20.0f,30.0f,1.0f));
以上计算出的LocalPosition就是真正的局部坐标了。
4、通成员函数设置局部位置、旋转、缩放
我们的Transform类,在拥有了以上成员函数后,可以进行方便的局域坐标转世界坐标、世界坐标转局部坐标。但是,我们还有一些基本的诉求,就是通过设置局域位置、旋转、缩放后,我们能同时更新Transform的世界位置、旋转、缩放数据,并保存更新后的局域坐标转世界坐标、世界坐标转局部矩阵。同时,我们还有这样一个诉求,此前知道由于矩阵的性质,我们知道设置位置、旋转、缩放矩阵的先后顺序不同,会导致最终的效果截然不同,这给游戏设计带来了极大的不便,我们可以通过一下函数来解决这些问题。
4.1、设置子物体的局部位置
//设置局部坐标位置glm::vec3 TransformComponent::SetLocalPosition(glm::vec3 tempLocalPosition)
{//保存位置的更改LocalPosition=tempLocalPosition;//更新LocalToWorldMatrix和WorldToLocalMatrix数据,这里采用父物体已经计算好的矩阵,因此必须确保父物体的矩阵准确LocalToWorldMatrix=GetLocalToWorldMatrixFromParentGameObject();WorldToLocalMatrix=GetWorldToLocalMatrixFromParentGameObject();//更新世界欧拉角WorldPositionUpdateWorldPositionData();//更新世界欧拉角WorldEulerAngleUpdateWorldEulerAngleData();//更新世界缩放比例WorldScaleUpdateWorldScaleData();//循环遍历下级节点if(ptOwner!=NULL){//获取首个子节点物体Node<GameObject*> *ptTempNode=ptOwner->ChildNodeList.ptNodeHead;//循环执行各个子节点物体的消息处理函数while(ptTempNode!=NULL){if(ptTempNode->ptInstance!=NULL){ptTempNode->ptInstance->Transform.SetLocalPosition(ptTempNode->ptInstance->Transform.LocalPosition);}//进行下一个节点ptTempNode=ptTempNode->ptNextNode;}}//返回信息return tempLocalPosition;}
4.2、设置子物体的局部旋转
//设置局部坐标旋转glm::vec3 TransformComponent::SetLocalEulerAngle(glm::vec3 tempLocalEulerAngle)
{//保存位置的更改LocalEulerAngle=tempLocalEulerAngle;//更新LocalToWorldMatrix和WorldToLocalMatrix,这里采用父物体已经计算好的矩阵,因此必须确保父物体的矩阵准确LocalToWorldMatrix=GetLocalToWorldMatrixFromParentGameObject();WorldToLocalMatrix=GetWorldToLocalMatrixFromParentGameObject();//更新世界欧拉角WorldPositionUpdateWorldPositionData();//更新世界欧拉角WorldEulerAngleUpdateWorldEulerAngleData();//更新世界缩放比例WorldScaleUpdateWorldScaleData();//循环遍历下级节点if(ptOwner!=NULL){//获取首个子节点物体Node<GameObject*> *ptTempNode=ptOwner->ChildNodeList.ptNodeHead;//循环执行各个子节点物体的消息处理函数while(ptTempNode!=NULL){if(ptTempNode->ptInstance!=NULL){ptTempNode->ptInstance->Transform.SetLocalEulerAngle(ptTempNode->ptInstance->Transform.LocalEulerAngle);}//进行下一个节点ptTempNode=ptTempNode->ptNextNode;}}//返回信息return tempLocalEulerAngle;}
4.3、设置子物体的局部旋转
//设置局部坐标缩放glm::vec3 TransformComponent::SetLocalScale(glm::vec3 tempLocalScale)
{//保存位置的更改LocalScale=tempLocalScale;//更新LocalToWorldMatrix和WorldToLocalMatrix,这里采用父物体已经计算好的矩阵,因此必须确保父物体的矩阵准确LocalToWorldMatrix=GetLocalToWorldMatrixFromParentGameObject();WorldToLocalMatrix=GetWorldToLocalMatrixFromParentGameObject();//更新世界欧拉角WorldPositionUpdateWorldPositionData();//更新世界欧拉角WorldEulerAngleUpdateWorldEulerAngleData();//更新世界缩放比例WorldScaleUpdateWorldScaleData();//循环遍历下级节点if(ptOwner!=NULL){//获取首个子节点物体Node<GameObject*> *ptTempNode=ptOwner->ChildNodeList.ptNodeHead;//循环执行各个子节点物体的消息处理函数while(ptTempNode!=NULL){if(ptTempNode->ptInstance!=NULL){ptTempNode->ptInstance->Transform.SetLocalScale(ptTempNode->ptInstance->Transform.LocalScale);}//进行下一个节点ptTempNode=ptTempNode->ptNextNode;}}//返回信息return tempLocalScale;}
4.4、设置局部位置、旋转 、缩放的原理
我们看以上三个函数感觉比方复杂,但其实也很简单,它们都有共同的特点。每个函数都分了4个部分,第一部分将参数的局部参数赋值给成员变量,这个很简单;第二部分根据赋值后的成员变量获取新的局部转世界矩阵、世界转局部矩阵并保存到成员变量中,方便后期使用;第三部分,根据前两个部分获取的成员变量更新世界位置、旋转、缩放数据,并保存到成员变量中;第四部分,由于父物体位置、旋转、缩放的变化会影响到子物体的变化,因此递归更新所有子物体及子物体的子物体位置、旋转、缩放数据。而且由于我们在GetAbsoluteMatrix和GetRelativeMatrix函数中已经设置好了位置,旋转、缩放的顺序,因此,在设置多个位置,旋转、缩放函数时不会造成先后位置的混乱。当然以上函数用到了一下公共部分,用以更新世界位置、旋转、缩放数据。
void TransformComponent::UpdateWorldPositionData()
{//获取转换矩阵glm::mat4 tempLocalToWorldPositionMatrix=LocalToWorldMatrix;//计算世界位置坐标WorldPosition=glm::vec3(tempLocalToWorldPositionMatrix*glm::vec4(0.0f,0.0f,0.0f,1.0f));}void TransformComponent::UpdateWorldEulerAngleData()
{//获取旋转矩阵glm::mat4 tempLocalToWorldRotateMatrix=GetLocalToWorldMatrixFromRootGameObject(2);//计算世界旋转矩阵,这里使用YXZ模式(后来操作的XZ轴旋转不干扰先操作的Y轴旋转)glm::extractEulerAngleYXZ(tempLocalToWorldRotateMatrix,WorldEulerAngle.y,WorldEulerAngle.x,WorldEulerAngle.z);}void TransformComponent::UpdateWorldScaleData()
{//获取缩放矩阵glm::mat4 tempLocalToWorldRotateMatrix=GetLocalToWorldMatrixFromRootGameObject(3);//计算世界位置坐标WorldScale=glm::vec3(tempLocalToWorldRotateMatrix[0][0],tempLocalToWorldRotateMatrix[1][1],tempLocalToWorldRotateMatrix[2][2]);}
5、通成员函数设置世界位置、旋转、缩放
那么可以设置物体的局部坐标后,可能会存在不方便的情况,比如,我们项设置物体在世界坐标下的位置、旋转、缩放,难道我们还要认为的转换一下,在设置成局部位置、旋转、缩放吗?当然不是,我们也是可以直接设定世界坐标体系下的位置、旋转、缩放的。我们引入一下函数实现。
5.1、设置子物体的世界位置
//设置世界坐标位置glm::vec3 TransformComponent::SetWorldPosition(glm::vec3 tempWorldPosition)
{//记录当前的世界旋转角度和世界缩放大小glm::vec3 tempLocalEulerAngle=LocalEulerAngle;glm::vec3 tempLocalScale=LocalScale;//重置局部旋转角度和局部缩放大小,防止对位置设置的影响LocalEulerAngle=glm::vec3(0.0f,0.0f,0.0f);LocalScale=glm::vec3(1.0f,1.0f,1.0f);//临时获取世界位置矩阵glm::mat4 tempWorldToLocalMatrix=glm::mat4(1.0f);//获取该物体父物体的世界转局部矩阵if(ptOwner!=NULL){tempWorldToLocalMatrix=GetWorldToLocalMatrixFromRootGameObject(0,ptOwner->ptParent);}//保存位置的更改SetLocalPosition(glm::vec3(tempWorldToLocalMatrix*glm::vec4(tempWorldPosition,1.0f)));//还原原本旋转和缩放的设定SetLocalEulerAngle(tempLocalEulerAngle);SetLocalScale(tempLocalScale);//返回信息return WorldPosition;}
5.2、设置子物体的世界旋转
//设置世界坐标旋转glm::vec3 TransformComponent::SetWorldEulerAngle(glm::vec3 tempWorldEulerAngle)
{//计算目标旋转矩阵glm::mat4 tempMatrix=glm::mat4(1.0f);tempMatrix=glm::rotate(tempMatrix,tempWorldEulerAngle.y,glm::vec3(0.0f,1.0f,0.0f));tempMatrix=glm::rotate(tempMatrix,tempWorldEulerAngle.x,glm::vec3(1.0f,0.0f,0.0f));tempMatrix=glm::rotate(tempMatrix,tempWorldEulerAngle.z,glm::vec3(0.0f,0.0f,1.0f));//获取组合旋转矩阵glm::mat4 tempWorldToLocalRotateMatrix=glm::mat4(1.0f);//获取该物体父物体的世界转局部矩阵if(ptOwner!=NULL){tempWorldToLocalRotateMatrix=GetWorldToLocalMatrixFromRootGameObject(2,ptOwner->ptParent)*tempMatrix;}//获取世界旋转矩阵下对应的局部旋转欧拉角glm::vec3 tempLocalEulerAngle=glm::vec3(0.0f,0.0f,0.0f);//计算世界旋转矩阵,这里使用YXZ模式(后来操作的XZ轴旋转不干扰先操作的Y轴旋转)glm::extractEulerAngleYXZ(tempWorldToLocalRotateMatrix,tempLocalEulerAngle.y,tempLocalEulerAngle.x,tempLocalEulerAngle.z);//更新世界欧拉角WorldEulerAngleSetLocalEulerAngle(tempLocalEulerAngle);//返回信息return WorldEulerAngle;}
5.3、设置子物体的世界缩放
//设置世界坐标缩放glm::vec3 TransformComponent::SetWorldScale(glm::vec3 tempWorldScale)
{//计算目标缩放矩阵glm::mat4 tempMatrix=glm::mat4(1.0f);tempMatrix=glm::scale(tempMatrix,tempWorldScale);//获取组合缩放矩阵glm::mat4 tempWorldToLocalScaleMatrix=glm::mat4(1.0f);//获取该物体父物体的世界转局部矩阵if(ptOwner!=NULL){tempWorldToLocalScaleMatrix=GetWorldToLocalMatrixFromRootGameObject(3,ptOwner->ptParent)*tempMatrix;}//计算世界缩放比例SetLocalScale(glm::vec3(tempWorldToLocalScaleMatrix[0][0],tempWorldToLocalScaleMatrix[1][1],tempWorldToLocalScaleMatrix[2][2]));//返回信息return WorldScale;}
以上函数的实现是根据个人经验判断的,可能于是最高效的,注释比较详细供参考。
6、角色朝向问题的应用
我们接下来要实现一个场景,世界坐标下有红、黄、绿、蓝4个颜色的石头人,红色石头人可以移动,我们需要通过Transform的相关操作实现各个角色之间的朝向问题,具体的要求如下:
1、黄色石头人与红色石头人的方向始终保持一致。
2、绿色石头人始终面向着红色石头人的方向。
3、蓝色石头人也始终朝着红色石头人的方向,并且红色石头人上下移动时,蓝色石头人也会改变仰俯的角度。这使用了四元数方便模型任意角度旋转。
为了实现以上功能我们需要一下函数。
6.1、角色朝向问题(角色始终面向目标的LookAt函数)
这个函数主要让角色始终面向指定目标点,不考虑仰俯的角度的情况。
//让模型始终看向某个目标点,不考虑仰俯角的情况,第二个参数tempSourcePoint表示要将模型那个方向面对目标点,默认为Z轴方向void TransformComponent::LookAt(glm::vec3 tempTargetPoint,glm::vec3 tempSourcePoint)
{//保存转换矩阵glm::mat4 towardMat=glm::mat4(1.0f);//重置局部旋转角度,以便随后获取初始化世界转局部矩阵SetLocalEulerAngle(glm::vec3(0,0,0));//获取目标点(世界坐标下)在局部坐标中的位置glm::vec3 tempLocalDestPoint=glm::vec3(GetWorldToLocalMatrix()*glm::vec4(tempTargetPoint,1.0f));//计算水平平面的旋转if(tempLocalDestPoint.x!=0 || tempLocalDestPoint.z!=0){//计算出上下旋转的矩阵。在局部坐标系中计算等待旋转的模型向量,模型位置向量将Y轴置零就获取到Z轴向量towardMat=GetMatrix_Quaternion(tempSourcePoint,tempLocalDestPoint-glm::vec3(0.0f,tempLocalDestPoint.y,0.0f),glm::vec3(0.0f,1.0f,0.0f))*towardMat;}//计算以上旋转矩阵对应的欧拉角glm::vec3 tempLocalEulerAngle=glm::vec3(0.0f,0.0f,0.0f);glm::extractEulerAngleYXZ(towardMat,tempLocalEulerAngle.y,tempLocalEulerAngle.x,tempLocalEulerAngle.z);SetLocalEulerAngle(tempLocalEulerAngle);}
6.2、角色朝向问题(角色始终面向目标的LookRotation函数)
这个函数主要让角色始终面向指定目标点,并且红色石头人(目标)上下移动时,蓝色石头人也会改变仰俯的角度。
//让模型始终看向某个目标点,要考虑仰俯角的情况,第二个参数tempSourcePoint表示要将模型那个方向面对目标点,默认为Z轴方向void TransformComponent::LookRotation(glm::vec3 tempTargetPoint,glm::vec3 tempSourcePoint)
{//保存转换矩阵glm::mat4 towardMat=glm::mat4(1.0f);//重置局部旋转角度,以便随后获取初始化世界转局部矩阵SetLocalEulerAngle(glm::vec3(0,0,0));//获取目标点(世界坐标下)在局部坐标中的位置glm::vec3 tempLocalDestPoint=glm::vec3(GetWorldToLocalMatrix()*glm::vec4(tempTargetPoint,1.0f));//计算水平平面的旋转;如果需要处理目标位置和自身位置相同(主要为X和Z轴坐标相同,可以在此处判断分支进行处理,稍后在另行添加)if(tempLocalDestPoint.x!=0 || tempLocalDestPoint.z!=0){//计算出上下旋转的矩阵。在局部坐标系中计算等待旋转的模型向量,模型位置向量将Y轴置零就获取到Z轴向量。这里第三个参数确保四元数旋转0度或180度的特殊情况,指定特殊旋转轴。towardMat=GetMatrix_Quaternion(tempSourcePoint,tempLocalDestPoint-glm::vec3(0.0f,tempLocalDestPoint.y,0.0f),glm::vec3(0.0f,1.0f,0.0f))*towardMat;}//计算垂直平面的旋转if(tempLocalDestPoint.y!=0){//计算出上下旋转的矩阵。在局部坐标系中计算需要旋转到的目标向量,模型位置向量将Y轴置零就获取到Z轴向量towardMat=GetMatrix_Quaternion(tempLocalDestPoint-glm::vec3(0.0f,tempLocalDestPoint.y,0.0f),tempLocalDestPoint)*towardMat;}//计算以上旋转矩阵对应的欧拉角glm::vec3 tempLocalEulerAngle=glm::vec3(0.0f,0.0f,0.0f);glm::extractEulerAngleYXZ(towardMat,tempLocalEulerAngle.y,tempLocalEulerAngle.x,tempLocalEulerAngle.z);SetLocalEulerAngle(tempLocalEulerAngle);}
6.3、角色朝向问题(四元数旋转函数)
要解决角色朝向问题,这使用了四元数方便模型任意角度旋转。我们发现以上不管是LookAt函数,还是LookRotation函数都用到了一个GetMatrix_Quaternion函数,这里主要是用这个函数进行四元数旋转,方便获取对应的旋转矩阵。这里实现方法不唯一,也可以用其他的方法。
四元数刚接触的时候发现都是定义,看来看去很久都没有思路,当实际使用过程中才能慢慢体会它的含义和应用,这里就记录一下这个辛苦的历程。
//根据两个向量,计算从tempSourcePoint旋转到tempDestinationPoint的旋转矩阵,这里使用四元数来获取任意旋转矩阵glm::mat4 GetMatrix_Quaternion(glm::vec3 tempSourcePoint,glm::vec3 tempDestinationPoint,glm::vec3 tempSpecialAxis)
{//设置默认的返回值glm::mat4 towardMat=glm::mat4(1.0f);//需要对两个向量进行零向量判断,防止后续求模函数异常if(tempSourcePoint!=glm::vec3(0.0f,0.0f,0.0f) && tempDestinationPoint!=glm::vec3(0.0f,0.0f,0.0f)){//获取两个向量叉乘后的法向量,作为四元数的旋转轴glm::vec3 aAxis=glm::cross(tempSourcePoint,tempDestinationPoint);//叉乘为零向量,说明两个向量可能为平行或反向平行if(aAxis==glm::vec3(0.0f,0.0f,0.0f)){if(tempSpecialAxis!=glm::vec3(0.0f,0.0f,0.0f)){aAxis=tempSpecialAxis;}}//设置完毕后还需要检测一次,确保旋转轴不能为无效轴if(aAxis!=glm::vec3(0.0f,0.0f,0.0f)){//根据向量性质计算两个向量的夹角的余弦值,由于浮点数计算的原因dotvalue值可能略大于1或小于-1,比如小数1.0000000001做参数float dotvalue=glm::dot(tempSourcePoint,tempDestinationPoint)/glm::length(tempSourcePoint)/glm::length(tempDestinationPoint);//对该值的有效性进行处理,反余弦函数acosf参数范围为[-1,1],参数值大于1或小于-1时acosf函数会出错if(dotvalue>1.0f){dotvalue=1.0f;} if(dotvalue<-1.0f){dotvalue=-1.0f;}//根据余弦值计算对应的旋转角float angle=acosf(dotvalue);//自定义四元数glm::quat towardQuat;//根据求得的旋转轴和旋转角度定义四元数if(angle!=0){//使用四元数时需要将旋转轴单位化aAxis=glm::normalize(aAxis);//将旋转轴和旋转角度传递给四元数towardQuat.w=cos(angle/2);towardQuat.x=aAxis.x*sin(angle/2);towardQuat.y=aAxis.y*sin(angle/2);towardQuat.z=aAxis.z*sin(angle/2);//返回获取的四元数转换成对应的旋转矩阵towardMat=glm::toMat4(towardQuat);}}}return towardMat;}
