Unity 翻页特效实现
Unity 翻页特效实现
游戏中都会有一些特效存在,这样才能吸引到玩家,就拿翻页特效这个特效来说,有了它,游戏场景中会变的既有特点又有趣,下面我们就来看看在unity中翻页特效的实现方法。
需求介绍:
我们是一款战棋类卡牌游戏,UI系统中有一个卡库界面,为了让页面切换有一个比较生动的过程,策划希望采用翻页效果。我们的卡库界面如下:
在页面切换时,出现如下的翻页效果:
实现过程:
AssetStore上其实有较多翻页特效插件,比较知名的有MegaFiers,不过我们最终还是不打算使用它们,大致有三个原因:一是,第三方插件的翻页效果会有一定的效率问题,没有专门为其做优化,比如页面的pool。尤其是MegaFiers,在变形网格过程中居然会不断的创建新顶点,效率低下可想而知。二是,第三方插件提供的翻页特效参数不够丰富,页面表现不够生动。三是,在不是特别了解第三方插件的情况下,尤其是我们只是用其中很小一部分功能的情况下,引入一个大包,容易引起整体工程的不稳定性。所以,干脆自己重新实现一个翻页特效,这样比较容易掌控效果表现和性能优化。
一, 翻页过程建模:
要实现网格平面的翻页过程,首先得对这个过程做数学建模:
(相关论文PDF:http://www2.parc.com/istl/groups/uir/publications/items/UIR-2004-10-Hong-DeformingPages.pdf
)
我们要模拟一本书上的页面从一面翻向另外一面,这期间有两个过程:
1,翻转:页面以书本的装订线为轴,翻动页面从0到180角度的过程。
2,变形:页面在翻动过程中卷曲的表现。
翻转很简单,如果不考虑变形,就是直接将一个页面绕装订线从0角度翻转到180度,可以很好的模拟硬板页面的翻页过程,如下图所示:
但对于薄纸页面,这样做就会显得比较生硬,所以需要配合翻转加上变形的过程才会比较真实。翻页的实现主要难点在于页面变形的推导,我们下面重点来分析其数学模型。
页面在翻动变形的时候,好像围绕着一个圆锥体旋转,所以我们使用圆锥体来模拟页面卷曲的过程。如下图所示:
上图黄色平面为书本展开的平面,右半边为翻转角度为0度的页面(即翻页的起始角度),左半边为翻转角度为180度的页面(即翻页的最终角度)。上图将一个半角为θ的圆锥体放到两页面中间,圆锥体表面与书本装订线相交于y轴(向上为正),x轴与书本下边缘同轴(向左为正),顶点A为圆锥体顶点。
将上图几何内容细化,并做一些辅助线,如下图:
页面的卷曲是随θ和的变化而变化的,而θ和是通过时间t得到的,是已知量,假设P点是页面上任意一点,在遍历顶点的过程会得到P点的具体值,所以P点也是已知的。我们最终要求的是P点在圆锥面上的投影T点。以A为圆心,AP为半径(设为R),在xy平面上画一个圆和Y轴交于点S。
圆锥体的几何原理告诉我们,弧会映射到圆锥面上形成另一段等长的弧,在与圆锥体底面平行的圆上,圆心为点C,半径为
我们假设角,,根据弧长弧度公式得到:
其中:
接下来,我们来求解T点的三个分量。
我们先来求:
由于C点和S点都在yz平面上,所以过C点可以找到一条直径和x轴平行的直径,且该直径和直线CS垂直,如下图所示:
D点为T点在该直径上的投影,于是得到:
接下来求:
换一张yz平面的侧面视图,注意,由于Unity是左手坐标系,所以z轴向下,如下图:
Q点是T点在线SC上的投影,K点为Q点在y轴上的投影,于是得到
其中:
将上述条件代入等式:
最终得到
二, Unity代码实现
有了以上推导结果,代码实现就较为简单了。
首先创建一个类PageMesh,用于负责页面Mesh的创建,它提供两个主要处理函数:
第一个函数BuildMesh:
根据传入的网格左下角位置,网格密度信息,页面正反面材质,正面所使用的Texture2D,进行创建网格。如下图:在UI前创建一张页面Mesh
这里有几个小细节需要注意:
1, 页面Mesh是分正反面的,所以需要分别创建正面和反面的Mesh,如下:
int[] ftontTriangles = newint[triangleCount];
int[] backTriangles = newint[triangleCount];
// create page plane mesh
………
// set submesh
mesh.subMeshCount =2;
mesh.SetTriangles(ftontTriangles,0);
mesh.SetTriangles(backTriangles,1);
2,由于一本书每页内容都不一样,所以需要至少为每个页面的正面单独创建一个材质实例,如下:
//set sharedMaterials(not materials)
Material [] materials = newMaterial[2];
materials[0] = newMaterial(frontMaterial);
materials[1] = newMaterial(backMaterial);
meshRenderer.sharedMaterials= materials;
3,由于翻页之前,需要对页面UI进行截图,保存到Texture2D上,在翻回的时候能够还原上一页内容,所以针对每个PageMesh都在外部创建一个指定为页面大小的Texture2D给其正面材质,如下:
// get page ui rect
Rectrect = ScreenShotManager.GetScreenRect(rectTransform, Camera.main);
// create texture2D for aMesh
Texture2Dtex =
newTexture2D((int)rect.width,(int)rect.height,TextureFormat.RGB24, false);
// set texture to frontpage material
meshRenderer.sharedMaterials[0].SetTexture("_MainTex", tex);
4,UICanvas需要设置为Screen Space – Camera模式,使得页面Mesh通过设置z轴深度显示在UI前面。
第二个函数ReCaculateMesh:
根据传入的百分比时间t(范围0-100),来计算页面Mesh上每个顶点在当前时间所对应的位置。其中GetThetaApexRho函数根据传入的t取得Theta(圆锥半角θ),Apex(圆锥顶点y分量值),Rho(当前页面绕装订线翻转的角度(0度-180度)),Mesh顶点位置通过这三个值算得。
这里需要注意的细节:
1, GetThetaApexRho内部计算是通过一个外部配置的阶段数组插值得到Theta、Apex、Rho的,这个阶段数组是一个经验值,需要通过不段实验来得到一个较为自然的结果,而且不同的页面宽高比也会有不同的经验数值。大致经验是我们翻书的时候,刚开始是先让页面从展开状态变得卷曲起来,然后翻动过程中,卷曲程度逐渐变小,最后翻到180度时恢复到展开状态。展开状态时Theta为90度,卷曲程度越大角度越小,范围在(0,90)。以下是我们调试过程中得到的一个经验数组,因为Apex的值和Theta的值都是影响页面的卷曲程度,所以固定它的值(1.875f)会更简单些。
如下:
(step为当前阶段的百分比值)
publicPageTurnStage[] pageTurnStages =
{
newPageTurnStage(){ step =0.0f, rho = 0.0f, thetaAngle = 90.0f, apexValue =-1.875f },
newPageTurnStage(){ step =50.0f, rho = 0.0f, thetaAngle =10.0f, apexValue = -1.875f },
newPageTurnStage(){ step =70.0f, rho = 50.0f, thetaAngle =10.0f, apexValue = -1.875f },
newPageTurnStage(){ step =90.0f, rho = 110.0f, thetaAngle =15.0f, apexValue = -1.875f },
newPageTurnStage(){ step =100.0f, rho = 180.0f, thetaAngle =90.0f, apexValue = -1.875f },
};
2, CurlTurn函数正是应用了前面推导的页面变形公式,如下:
privateVector3CurlTurn(Vector3p, floattheta, floatapex)
{
floatR = Mathf.Sqrt((p.x * p.x)+ Mathf.Pow((p.y - apex), 2.0f));
floatr = R * Mathf.Sin(theta);
floatbeta = Mathf.Asin(p.x / R)/ Mathf.Sin(theta);
p.x = r * Mathf.Sin(beta);
p.y =(R + apex) - ((r * (1 - Mathf.Cos(beta)))* Mathf.Sin(theta));
p.z = -(r * (1 - Mathf.Cos(beta))) * Mathf.Cos(theta);
returnp;
}
在页面变形之后,再对整个PageMesh物体进行翻转变换,即得到最终的顶点位置:
transform.eulerAngles
= newVector3(transform.eulerAngles.x, rho, transform.eulerAngles.z);
3, 优化细节:因为页面Mesh顶点位置的变形和翻转计算是每帧进行的,所以这个过程如果不断创建新的顶点会造成很大的性能和GC问题,上述提到的MeshFiers的翻页功能就存在这个问题。所以,在创建页面Mesh的时候,在原始位置顶点数组originVertexes的基础上,我们会多创建一个顶点数组deformateVetexes,每帧通过Theta、Rho、Apex以及originVertexes位置得到该时间点对应的变换后的顶点数组deformateVetexes。
// create two vetexarray
privateVector3[] originVertexes;
privateVector3[] deformateVetexes;
originVertexes = newVector3[vetexCount];
......// create originvertexes
deformateVetexes = newVector3[vetexCount];
// every frame calculatedeformateVetexes
deformateVetexes = f(originVertexes,theta, rho, apex);
meshFilter.mesh.vertices = deformateVetexes;
有了PageMesh之后,我们还需要有一个PageMeshManager对其进行内存池管理,以及截图流程的处理,这里就不细讲了。为了方便使用,PageMeshManager直接挂在某个矩形形状的UI上,配置好翻页时间,页面材质、翻页音效以及翻页阶段的经验数组,就能直接展示翻页特效。对于不同质地的页面,寻找其对应的音效,能够让翻页过程显得更加真实,配置如下:
效果动画: