OpenGL ES 正交投影
绘制正方形
在最开始绘制的六边形里面好像看起来挺容易的,也没有出现什么问题,接下来不妨忘记前面绘制六边形的代码,让我们按照自己的理解来绘制一个简单的正方形。
按照我的理解,要想在屏幕中间显示一个正方形,效果如下图所示
应该创建的数据如下图所示
即传给渲染管线的顶点数据如下图:
float[] vertexArray = new float[] {
(float) -0.5, (float) -0.5, 0,
(float) 0.5, (float) -0.5, 0,
(float) -0.5, (float) 0.5, 0,
(float) 0.5, (float) 0.5, 0
};
于是代码大概是这样子的,这里省略掉与主题无关的代码,颜色用纯色填充,因此在片元着色器中指定颜色,也省略掉一系列矩阵变换。顶点着色器中直接将顶点传给渲染管线,片元着色器中给片元设置固定颜色红色。
Rectangle.java
public class Rectangle {
private FloatBuffer mVertexBuffer;
private int mProgram;
private int mPositionHandle;
public Rectangle(float r) {
initVetexData(r);
}
public void initVetexData(float r) {
// 初始化顶点坐标
float[] vertexArray = new float[] {
(float) -0.5, (float) -0.5, 0,
(float) 0.5, (float) -0.5, 0,
(float) -0.5, (float) 0.5, 0,
(float) 0.5, (float) 0.5, 0
};
ByteBuffer buffer = ByteBuffer.allocateDirect(vertexArray.length * 4);
buffer.order(ByteOrder.nativeOrder());
mVertexBuffer = buffer.asFloatBuffer();
mVertexBuffer.put(vertexArray);
mVertexBuffer.position(0);
int vertexShader = loaderShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode);
int fragmentShader = loaderShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode);
mProgram = GLES20.glCreateProgram();
GLES20.glAttachShader(mProgram, vertexShader);
GLES20.glAttachShader(mProgram, fragmentShader);
GLES20.glLinkProgram(mProgram);
mPositionHandle = GLES20.glGetAttribLocation(mProgram, "aPosition");
}
public void draw() {
GLES20.glUseProgram(mProgram);
// 将顶点数据传递到管线,顶点着色器
GLES20.glVertexAttribPointer(mPositionHandle, 3, GLES20.GL_FLOAT, false, 0, mVertexBuffer);
GLES20.glEnableVertexAttribArray(mPositionHandle);
// 绘制图元
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
}
private int loaderShader(int type, String shaderCode) {
int shader = GLES20.glCreateShader(type);
GLES20.glShaderSource(shader, shaderCode);
GLES20.glCompileShader(shader);
return shader;
}
private String vertexShaderCode = "attribute vec3 aPosition;"
+ "void main(){"
+ "gl_Position = vec4(aPosition,1);"
+ "}";
private String fragmentShaderCode = "precision mediump float;"
+ "void main(){"
+ "gl_FragColor = vec4(1,0,0,0);"
+ "}";
}
RectangleView.java
public class RectangleView extends GLSurfaceView{
public RectangleView(Context context) {
super(context);
setEGLContextClientVersion(2);
setRenderer(new MyRender());
}
class MyRender implements GLSurfaceView.Renderer {
private Rectangle rectangle;
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
GLES20.glClearColor(0.5f, 0.5f, 0.5f, 1);
rectangle = new Rectangle(0.5f);
GLES20.glEnable(GLES20.GL_DEPTH_TEST);
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
GLES20.glViewport(0, 0, width, height);
}
@Override
public void onDrawFrame(GL10 gl) {
GLES20.glClear( GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);
rectangle.draw();
}
}
}
然后出来的效果是这样子的,实际上屏幕上的坐标并不是这样子的,后面可以知道上面画的这个样子其实只是一个归一化的设备坐标。归一化设备坐标可以通过公式映射到实际的手机屏幕,后面会学到。
咦,实际效果好像和想象中的不太一样呀。我的本意是显示一个正方形,但实际上现实的却是一个矩形了,y轴上被拉伸了,并且横屏状态下也是类似的情况。但比较巧的是,如果以屏幕中心做一个坐标轴,就会发现,这个矩形的四个顶点在这个坐标轴x、y范围为[-1,1]的中间。
实际上,要显示的所有物体映射到手机屏幕上,都是要映射到x、y、z轴上的[-1,1]范围内,这个范围内的坐标称为归一化设备坐标,独立于屏幕的实际尺寸和形状。
因此按照这样的规定,我们要创建一个正方形就非常困难了,因为要创建正方形就必须考虑手机的宽高比,传入数据的时候就比较复杂了:不能仅仅站在要绘制物体的自身角度来看了。也就是说,上面的例子中要绘制一个正方形,传入的顶点数据的y坐标要按照比例进行一点转换,比如对16:9的屏幕,将上面传入的顶点数据的y坐标都乘以9/16即可。但同时会发现当处于横屏时,又要处理传入的x坐标的值,显然这不是一个好的方案。
引入投影
实际上,对于一个物体来说它有它自身的坐标,这个空间称为物体空间,也就是设计物体的时候采用的一个坐标空间,物体的几何中心在坐标原点上,归一化后坐标范围在[-1,1]之间,x和y轴分度是一致的。
将在这个空间的物体直接往手机屏幕的归一化坐标绘制时,由于屏幕的宽高比的问题,就会出现和预料结果不一样。所以只需要对物体空间的坐标做一个映射即可。
正交投影就是为了解决这个问题的,
public static void orthoM(float[] m, int mOffset,
float left, float right, float bottom, float top,
float near, float far)
正交投影背后的数学
orthoM函数产生的矩阵会把所有的左右之间、上下之间,远近之间的点映射到归一化设备坐标中。
各参数的含义如图所示
正交投影是一种平行投影,投影线是平行的,其视景体是一个长方体,坐标位于视景体中的物体才有效,视景体里面的物体投影到*面上的部分最终会显示到屏幕的视口中,关于视口后面会降到。
会产生下面的矩阵,z轴的负值会反转z坐标,这是因为归一化设备坐标是左手系统,而OpenGL ES中的坐标系统都是右手系统,这里还涉及到顶点坐标的w分量,目前暂时用不到。
利用矩阵的就可以将物体空间[-1,1]之间的坐标映射到屏幕归一化设备坐标的[-1,1]之间。归一化屏幕坐标是右手坐标系统,原点在屏幕正中心,向右为x轴正方向,向上为y轴正方向,z轴垂直屏幕向外。以竖屏为例,比如设置left=-1,right=1,bottom=-hight/width,top=hight/width,比如我的手机分辨率为1920*1080 =1.8 对上面的正方形点(0.5,0.5)坐标而言经过变化就成了(0.5,0.3)
在屏幕的归一化设备坐标中来看就是一个正方形了,因为y轴范围显然比x轴大,0.3对应的实际长度和x轴的0.5长度是一样的。
上面的代码需要做如下修改,在onSurfaceChanged里面增加如下代码
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
GLES20.glViewport(0, 0, width, height);
// 根据屏幕方向设置投影矩阵
float ratio= width > height ? (float)width / height : (float)height / width;
if (width > height) {
// 横屏
Matrix.orthoM(mProjectionMatrix, 0, -ratio, ratio, -1, 1, 0, 5);
} else {
Matrix.orthoM(mProjectionMatrix, 0, -1, 1, -ratio, ratio, 0, 5);
}
}
接着在顶点着色器中对顶点乘以投影矩阵
private String vertexShaderCode = "uniform mat4 uProjectionMatrix;" // 增加这一行
+ "attribute vec3 aPosition;"
+ "void main(){"
+ "gl_Position = uProjectionMatrix * vec4(aPosition,1);" // 不是直接赋值而是乘以投影矩阵
+ "}";
最后增加获取着色器中uProjectionMatrix以及传入值的代码部分即可。最终的效果不论横屏还是竖屏,显示的都是我们期望的正方形。
摄像机设置
需要补充的是,上面的参数near、far的含义指的是和视点的距离,视点貌似到目前还未接触到,它指的是摄像机的位置,和实际生活中用相机看物体一样,从不同的角度和位置拍摄同一个物体获得的照片肯定是不一样的,摄像机位置用setLookAtM函数指定。
public static void setLookAtM(float[] rm, // 生成的摄像机矩阵
int rmOffset,
float eyeX, float eyeY, float eyeZ, // 摄像机的位置
float centerX, float centerY, float centerZ, // 观察目标点的位置
// 摄像机位置和观察目标点的位置确定了观察方向
float upX, float upY,float upZ // up向量在x、y、z轴上的分量,我觉得一般应该是和观察方向垂直的
)
前面提到的确定的视景体就和上面函数指定的摄像机位置和观察方向有关。摄像机默认位置在(0,0,0)处,在上面的设置下,如果将改正方形沿z轴正方向平移1个单位,屏幕上就显示不了,因为已经跑到了设置的视景体外面了。
关于摄像机的参数和投影near和far参数的设置需要注意,肯定不是胡乱设置的!摄像机的位置、方向和投影矩阵定义的视景体最终确定了视景体的位置,如果设置不当就会导致物体没有显示在屏幕上,因为物体的坐标可能位于视景体外面。
视口
前面说过在视景体中的物体最终会投影到*面上,最终显示到视口上,正如前面在onSurfaceChanged设置的那样。
public static native void glViewport(
int x,
int y,
int width,
int height
);
视口中各参数的含义
视口用的屏幕坐标系原点并不在屏幕左上角而是在左下角,x轴向右,y轴向上。其实还不是很准确,准确的说,视口的坐标原点位于该View的左下角,因为GLSurfaceView并不总是占据整个屏幕的。