本节书摘来异步社区《OpenGL ES 3.x游戏开发(下卷)》一书中的第2章,第2.5节,作者: 吴亚峰 责编: 张涛,更多章节内容可以访问云栖社区“异步社区”公众号查看。
2.5 二维扭曲
本章前面几节的案例都是在3D空间中对顶点位置进行的变换,但本节将给出一个在2D空间中基于顶点位置变换进行二维扭曲的案例。
提示
本质上可以采用本节介绍的技术扭曲任何2D形状,为方便起见,本节将基于等边三角形进行介绍。
2.5.1 基本原理
介绍本节案例的具体开发之前,首先需要了解二维扭曲的基本情况,如图2-15所示。
从图2-15中可以看出,左侧的原始三角形经过扭曲处理后产生了右侧奇异的形状,犹如一个风车。同时从图中可以看出,要想对原始三角形实现扭曲处理,必须将大三角形切分为很多小三角形。下面简单介绍扭曲的计算思路(如图2-16所示),具体步骤如下。
(1)设扭动的中心点为点O,其坐标为(X0,Y0);绕中心点被扭动的点为D,其坐标为(X1,Y1)。
(2)设D点在X方向上的偏移为XSpan,Y方向上的偏移为YSpan,则有以下结论。
<span style='display:block;text-align:center'>
</span>
XSpan= X1- X0
YSpan= Y1- Y0
<span style='display:block;text-align:center'>
</span>
\bold{OD}=\sqrt{\bold{XSpan}×\bold{XSpan}+\bold{YSpan}×\bold{YSpan}}
(3)接着就可以求出OD与X轴正方向的夹角θ,具体情况如下。
- 如果XSpan=0,并且YSpan大于0,那么θ=/2。
- 如果XSpan=0,并且YSpan小于0,那么θ=3/2。
- 如果XSpan不等于0,那么θ=atan(YSpan/ XSpan)。
(4)然后计算旋转后的D点与X轴正方向的夹角θ'。
θ'=θ+ratio×OD
说明
其中ratio表示与当前总体旋转角度线性相关的一个系数,用于将距离转化为当前考察点的旋转角度。
(5)计算出旋转后的夹角后,就可以求出旋转后点的X、Y坐标了。
X1'= X0+ OD×cos(θ')
Y1'= Y0+ OD ×sin(θ')
只需要用顶点着色器实现上述算法即可得到非常漂亮的二维扭曲效果。
2.5.2 开发步骤
上一小节介绍了二维扭曲的基本原理以及注意事项,本小节将给出一个三角形二维扭曲的案例Sample2_5,其运行效果如图2-17所示。
了解了案例的运行效果后,接下来简要介绍本案例的具体开发过程。由于本案例中的大部分类和上卷章节很多案例中的类非常相似,因此这里只给出本案例中比较有代表性的部分,具体内容如下。
(1)从前面的介绍中已经知道,本案例中的大三角形实际上是由很多小三角形构成的。因此下面首先给出的是自动生成各个小三角形顶点数据的initVertexData方法,其来自MultiTrangle类,具体代码如下。
1 public void initVertexData(float edgeLength,int levelNum) {
2 ArrayList<Float> al_vertex=new ArrayList<Float>(); //顶点坐标数据列表
3 ArrayList<Float> al_texture=new ArrayList<Float>(); //纹理坐标数据列表
4 float perLength = edgeLength/levelNum; //小三角形的边长
5 for(int i=0;i<levelNum;i++){ //循环每一层生成小三角形
6 int currTopEdgeNum=i; //当前层顶端边数
7 int currBottomEdgeNum=i+1; //当前层底端边数
8 float currTrangleHeight=(float) (perLength*Math.sin(Math.PI/3)); //每个三角形的高度
9 float topEdgeFirstPointX=-perLength*currTopEdgeNum/2;//当前层顶端最左边点的x坐标
10 float topEdgeFirstPointY=-i*currTrangleHeight;//当前层顶端最左边点的y坐标
11 float topEdgeFirstPointZ=0; //当前层顶端最左边点的z坐标
12 float bottomEdgeFirstPointX=-perLength*currBottomEdgeNum/2;//当前层底端最左边点的
13 float bottomEdgeFirstPointY=-(i+1)*currTrangleHeight; // x、y、z坐标
14 float bottomEdgeFirstPointZ=0;
15 float horSpan=1/(float)levelNum; //横向纹理的偏移量
16 float verSpan=1/(float)levelNum; //纵向纹理的偏移量
17 float topFirstS=0.5f-currTopEdgeNum*horSpan/2; //当前层顶端最左边点的
18 float topFirstT=i*verSpan; //纹理ST坐标
19 float bottomFirstS=0.5f-currBottomEdgeNum*horSpan/2; //当前层底端最左边点的
20 float bottomFirstT=(i+1)*verSpan; //纹理ST坐标
21 for(int j=0;j<currBottomEdgeNum;j++){//循环产生当前层各个上三角形的顶点数据
22 float topX=topEdgeFirstPointX+j*perLength; //当前三角形顶端点的
23 float topY=topEdgeFirstPointY; //x、y、z坐标
24 float topZ=topEdgeFirstPointZ;
25 float topS=topFirstS+j*horSpan; //当前三角形顶端点的
26 float topT=topFirstT; //S、T纹理坐标
27 float leftBottomX=bottomEdgeFirstPointX+j*perLength;//当前三角形左下侧
28 float leftBottomY=bottomEdgeFirstPointY; //点的x、y、z坐标
29 float leftBottomZ=bottomEdgeFirstPointZ;
30 float leftBottomS=bottomFirstS+j*horSpan; //当前三角形左下侧
31 float leftBottomT=bottomFirstT; //点的S、T纹理坐标
32 float rightBottomX=leftBottomX+perLength; //当前三角形右下侧
33 float rightBottomY=bottomEdgeFirstPointY; //点的x、y、z坐标
34 float rightBottomZ=bottomEdgeFirstPointZ;
35 float rightBottomS=leftBottomS+horSpan; //当前三角形右下侧
36 float rightBottomT=leftBottomT; //点的S、T纹理坐标
37 //将当前三角形顶点数据按照逆时针顺序送入顶点坐标、纹理坐标列表
38 al_vertex.add(topX);al_vertex.add(topY);al_vertex.add(topZ);
39 al_vertex.add(leftBottomX);al_vertex.add(leftBottomY);al_vertex.add
(leftBottomZ);
40 al_vertex.add(rightBottomX);al_vertex.add(rightBottomY);al_
vertex.add(rightBottomZ);
41 al_texture.add(topS);al_texture.add(topT);
42 al_texture.add(leftBottomS);al_texture.add(leftBottomT);
43 al_texture.add(rightBottomS);al_texture.add(rightBottomT);
44 }
45 for(int k=0;k<currTopEdgeNum;k++){//循环产生当前层各个下三角形的顶点数据
46 float leftTopX=topEdgeFirstPointX+k*perLength; //当前三角形左上侧
47 float leftTopY=topEdgeFirstPointY; //点的x、y、z坐标
48 float leftTopZ=topEdgeFirstPointZ;
49 float leftTopS=topFirstS+k*horSpan; //当前三角形左上侧
50 float leftTopT=topFirstT; //点的S、T纹理坐标
51 float bottomX=bottomEdgeFirstPointX+(k+1)*perLength;//当前三角形底端点
52 float bottomY=bottomEdgeFirstPointY; //的x、y、z坐标
53 float bottomZ=bottomEdgeFirstPointZ;
54 float bottomS=bottomFirstS+(k+1)*horSpan; //当前三角形右底端点
55 float bottomT=bottomFirstT; //的S、T纹理坐标
56 float rightTopX=leftTopX+perLength; //当前三角形右上侧
57 float rightTopY=leftTopY; //点的x、y、z坐标
58 float rightTopZ=leftTopZ;
59 float rightTopS=leftTopS+horSpan; //当前三角形右上侧
60 float rightTopT=topFirstT; //点的S、T纹理坐标
61 al_vertex.add(leftTopX);al_vertex.add(leftTopY);al_vertex.add(leftTopZ);
62 al_vertex.add(bottomX);al_vertex.add(bottomY);al_vertex.add(bottomZ);
63 al_vertex.add(rightTopX);al_vertex.add(rightTopY);al_vertex.add(rightTopZ);
64 al_texture.add(leftTopS);al_texture.add(leftTopT);
65 al_texture.add(bottomS);al_texture.add(bottomT);
66 al_texture.add(rightTopS);al_texture.add(rightTopT);
67 }}
68 ……//此处省略部分代码,需要的读者请参考
69 }
说明
上述代码根据传入的大三角形边长及分层数量自动计算每一层中各个三角形的顶点坐标、纹理坐标。每一层中的三角形分为上三角形与下三角形,顶点计算方法不同,如图2-18所示。
(2)要能够呈现出扭动的三角形就需要在绘制每一帧时将不同的、连续变化的整体扭动角度因子(即上一小节算法中的ratio)传入渲染管线。由于这部分代码非常简单,这里不再赘述,需要的读者请参考随书附带中的源码。
(3)接着介绍实现二维扭曲算法的顶点着色器,其代码如下。
1 #version 300 es
2 uniform mat4 uMVPMatrix; //总变换矩阵
3 in vec3 aPosition; //顶点位置
4 in vec2 aTexCoor; //顶点纹理坐标
5 out vec2 vTextureCoord; //用于传递给片元着色器的纹理坐标
6 uniform float ratio; //当前整体扭动角度因子
7 void main(){
8 float pi = 3.1415926; //圆周率
9 float centerX=0.0; //中心点的x坐标
10 float centerY=-5.0; //中心点的y坐标
11 float currX = aPosition.x; //当前点的x坐标
12 float currY = aPosition.y; //当前点的y坐标
13 float spanX = currX - centerX; //当前x偏移量
14 float spanY = currY - centerY; //当前y偏移量
15 float currRadius = sqrt(spanX * spanX + spanY * spanY); //计算距离
16 float currRadians; //当前点与x轴正方向的夹角
17 if(spanX != 0.0){ //一般情况
18 currRadians = atan(spanY , spanX);
19 }
20 else{ //0.5π和1.5π的特殊情况
21 currRadians = spanY > 0.0 ? pi/2.0 : 3.0*pi/2.0;
22 }
23 float resultRadians = currRadians + ratio*currRadius;//计算出扭曲后的角度
24 float resultX = centerX + currRadius * cos(resultRadians);//计算结果点的x坐标
25 float resultY = centerY + currRadius * sin(resultRadians);//计算结果点的y坐标
26 //构造结果点,并根据总变换矩阵计算此次绘制此顶点的位置
27 gl_Position = uMVPMatrix * vec4(resultX,resultY,0.0,1);
28 vTextureCoord = aTexCoor; //将接收的纹理坐标传递给片元着色器
29 }
说明
上述顶点着色器实现了上一小节介绍的二维扭曲算法,通过应用此顶点着色器可以对任意的二维物体进行扭曲,并不一定是三角形。
进行二维扭曲效果的开发时,需要特别注意的是,提供给渲染管线的模型一定要分得比较细,如果分得比较粗糙,最终的结果就会比较差,如图2-19所示。
说明
图2-19给出了将本案例中的大三角形切分得比较粗糙后的运行效果以及线框图。从图中可以看出,若切分得很粗糙,其实际扭曲的效果与期望的效果之间就有较大的差距。