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分量,目前暂时用不到。