一、开始
在上篇文章中,我们已经让桌面的颜色有了变化,也让桌面适配了屏幕的比例,让手机在旋转的时候桌面也不会产生变形。但现在看到的桌面就像是在纸上画的一样,一点都没有立体感。所以现在我们要学习的就是3D的去展示我们的桌面,这个只是第一点,为了让我们的桌面更好看一些,我们准备了一张图片,我们也会学习如何让OpenGL去渲染我们要展示的图片,总的来说我们要学习两点:
1.3D展示桌面
2.OpenGL渲染图片
最终我们做出来的效果应该是这个样子的:
二、3D展示桌面
为了用3D效果展示桌面,我们先要知道一个gl_Position是如何转换成屏幕上显示的点的坐标的:
大致流程就像上图所示,书中讲的很详细,我这里简单讲下,大家感兴趣的可以自己去看书或者查资料,我们制造出3D效果主要实在Perspective Division(透视除法)这一步,一个点的坐标应该是(x , y , z , w)在Clip Space(裁剪空间)这个里面,这个时候x,y,z的取值范围是在-w到w之间的,然后会经过Perspective Division转换,这个时候的坐标就应该是(x/w , y/w , z/w)。
然后我们现在就用代码去实现了,这个时候我们一个点的坐标就应该有4个参数了,所以先修改如下代码:
private static final int POSITION_COMPONENT_COUNT = 4;
然后我们需要修改我们的数据
private final float[] mData = new float[]{
// Order of coordinates: X, Y, Z, W, R, G, B
// Triangle Fan
0f, 0f, 0f, 1.5f, 1f, 1f, 1f,
-0.5f, -0.8f, 0f, 1f, 0.7f, 0.7f, 0.7f,
0.5f, -0.8f, 0f, 1f, 0.7f, 0.7f, 0.7f,
0.5f, 0.8f, 0f, 2f, 0.7f, 0.7f, 0.7f,
-0.5f, 0.8f, 0f, 2f, 0.7f, 0.7f, 0.7f,
-0.5f, -0.8f, 0f, 1f, 0.7f, 0.7f, 0.7f,
//线
-0.5f, 0f, 0f, 1.5f, 1f, 0f, 0f,
0.5f, 0f, 0f, 1.5f, 1f, 0f, 0f,
//点
0f, -0.4f, 0f, 1.25f, 0f, 0f, 1f,
0f, 0.4f, 0f, 1.75f, 1f, 0f, 0f
};
运行下看看,是不是有点3D的感觉了!但是这样我们要硬编码w参数,这样明显是不好的,当然我们的前人也不会这么傻,当然有别的方法去处理,我们可以做移动,缩放,旋转等操作,让我们把之前的代码还原,重新开始吧。
这个时候我们又要回到矩阵的操作了,这次我们要同时处理屏幕适配,和视野的处理,书中讲了很多原理,大致就是讲透视投影是怎么一回事,有兴趣的可以自己去看看,我们就是要用这个东西去做出我们的3D效果。
这个是书中给出的投影矩阵的说明,这个投影矩阵可以同时适配屏幕和调整视野角度具体的参数说明都有说明,a,f,n可能需要看书中对透视投影的讲解才知道,大家可以google下,个人觉得不理解的话,写完代码之后也大概知道是什么了,所以就不讲解了,接下来我们定义一个这样的矩阵。
书中是这样告诉我们的
Android’s Matrix class contains two methods for this, frustumM() and perspectiveM(). Unfortunately, frustumM() has a bug that affects some types of projections,3 and perspectiveM() was only introduced in Android Ice Cream Sandwich and is not available on earlier versions of Android.
意思就是说,骚年,自己写吧!
所以我们就自己来写吧!我们在util
包下新建一个类叫MatrixHelper.java
类,并声明如下方法:
public static void perspectiveM(float[] m, float yFovInDegrees, float aspect, float n, float f) {
final float angleInRadians = (float) (yFovInDegrees * Math.PI / 180.0);
final float a = (float) (1.0 / Math.tan(angleInRadians / 2.0));
m[0] = a / aspect;
m[1] = 0f;
m[2] = 0f;
m[3] = 0f;
m[4] = 0f;
m[5] = a;
m[6] = 0f;
m[7] = 0f;
m[8] = 0f;
m[9] = 0f;
m[10] = -((f + n) / (f - n));
m[11] = -1f;
m[12] = 0f;
m[13] = 0f;
m[14] = -((2f * f * n) / (f - n));
m[15] = 0f;
}
这样我们就可以生成一个上面的矩阵了,现在让我们删掉onSurfacedChanged()
方法中除了glViewport()
之外的东西,然后调用上面的方法:
MatrixHelper.perspectiveM(mProjectionMatrix , 45 , (float)width / (float)height , 1 , 10);
运行一遍之后,你会发现,WTF,怎么成黑的了,小伙子,不要着急,这是正常的,因为我们n,f的值分别为1,10,就是说我们只能看到z轴上-1到-10之间的东西,这个时候我们可以用移动矩阵在z轴上移动一下,就能看到了。
我们先定义一个Model矩阵:
private float[] mModelMatrix = new float[16];
然后我们就要算出一个Model矩阵了,在onSurfacedChanged()
中添加如下代码:
setIdentityM(mModelMatrix, 0);
translateM(mModelMatrix, 0, 0f, 0f, -2f);
第一行代码是设置成单位矩阵,第二行代码是生成我们要的移动矩阵。这个时候我们就遇到一个问题,难道我们又要给gl_Point设置一个转换矩阵了么?答案是不需要,我们可以只使用一个矩阵就可以解决了,只不过这个时候需要把两个矩阵乘一下,但是顺序要特别注意,不能颠倒:
vertexclip = ProjectionMatrix * ModelMatrix * vertexmodel
在onSurfacedChanged()
方法中再添加如下方法:
final float[] temp = new float[16];
multiplyMM(temp, 0, mProjectionMatrix, 0, mModelMatrix, 0);
System.arraycopy(temp, 0, mProjectionMatrix, 0, temp.length);
我们用一个临时的变量去储存两个矩阵相乘的结果,然后再把临时变量里面的内容copy到mProjectionMatrix变量传递给OpenGL,这样我们就可以同时看到两个矩阵作用的效果了,运行下,看看吧,是不是能看到了。
然后我们可以再加上一个旋转的效果就能看到之前做出来的3D效果了,旋转需要选择绕着X,Y,Z那个轴旋转,根据不同的轴旋转矩阵也不一样,书中有说明,这里就不讲解了,我们在代码中只需要调用一个方法就可以了,在translateM(mModelMatrix, 0, 0f, 0f, -2f);
添加如下代码
rotateM(mModelMatrix, 0 , -60 , 1 , 0 , 0);
同时把移动的z轴的值改成-2.5,这样可以让桌面离我们远一点。
我们调用方法,让桌面绕着x轴旋转了-60度,这里可能涉及到坐标系的问题,书中有讲解,可以自行了解。再运行一遍,看看效果是不是就出来了。
三、用Texture渲染图片
现在我们就可以开始想办法让我们的桌面更好看一些了。首先我们要知道Texture是个什么东西
Textures in OpenGL can be used to represent images, pictures, and even fractal data that are generated by a mathematical algorithm.
这是原文的解释,就是用来展示图片的,甚至可以展示数学算法制作出来的数据。所以我们就是用这个东西去渲染我们要渲染的图片的。
首先我们要做的事情就是把图片加载到Texture上,我们在util
包下建一个类TextureHelper.java
,并实现如下代码:
public static int loadTexture(Context context , int resourceId){
final int[] textureObjectId = new int[1];
glGenTextures(1 , textureObjectId , 0);
if (textureObjectId[0] == 0){
Logger.debug(TAG , "create texture fail.");
return 0;
}
return textureObjectId[0];
}
我们声明了一个loadTexture()
方法来加载texture,我们先创建了一个Texture的对象,接下来我们要把图片加载到texture上,我们继续添加如下代码:
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inScaled = false;
final Bitmap bitmap = BitmapFactory.decodeResource(
context.getResources(), resourceId, options);
if (bitmap == null) {
Logger.debug(TAG, "Resource ID " + resourceId + " could not be decoded.");
glDeleteTextures(1, textureObjectId, 0);
return 0;
}
glBindTexture(GL_TEXTURE_2D , textureObjectId[0]);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
texImage2D(GL_TEXTURE_2D, 0, bitmap, 0);
bitmap.recycle();
glGenerateMipmap(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D , 0);
首先我们把图片资源转换成了bitmap,然后我们让OpenGL绑定了我们之前创建的Texture对象,并设置了两个参数GL_TEXTURE_MIN_FILTER
、GL_TEXTURE_MAG_FILTER
,这两个参数在书中有详细解释,大家可以自己去了解,大概就是设置图片的清晰度,同时如果考虑到效率的话有多种选择。然后我们就可以把图片加载到我们创建的Texture对象上了,最后就是bitmap的回收,解除Texture的绑定了。
加载图片资源到Texture的问题解决了,接下来我们需要知道OpenGL如何去绘制Texture了,这个跟之前只画颜色类似,我们需要新建一组.glsl
文件,我们先创建一个texture_vertex_shader.glsl
文件,实现如下代码:
uniform mat4 u_Matrix;
attribute vec4 a_Position;
attribute vec2 a_TextureCoordinates;
varying vec2 v_TextureCoordinates;
void main() {
gl_Position = u_Matrix * a_Position;
v_TextureCoordinates = a_TextureCoordinates;
}
大家注意到了,我们声明了一个有两个参数的向量的变量a_TextureCoordinates,这个Texture是有一个范围为(0,0)到(1,1)的坐标空间的,它的坐标系有两种ST、UV,具体解释书中是有讲解的,在这里不做过多讲解了。
接下来我们创建texture_fragment_shader.glsl
文件,实现如下代码:
precision mediump float;
uniform sampler2D u_TextureUnit;
varying vec2 v_TextureCoordinates;
void main() {
gl_FragColor = texture2D(u_TextureUnit, v_TextureCoordinates);
}
大家可以这样理解,u_TextureUnit就相当于我们要绘制的图片的每个点的颜色,然后v_TextureCoordinates就是告诉我们这些颜色要画在什么位置的坐标,最后通过texture2D()
方法获得我们在当前点要绘制的颜色。
书中讲到这里是重构了一遍代码,其实如果看过前面两篇文章的大概应该是知道如何去渲染了图片了,为了后面一篇文章做准备,我还是把书中的重构过程讲一遍吧。
首先我们把绘制的对象的架构改成下图的样子:
我们创建一个
data
包,然后创建VertexArray.java
类实现如下代码:
public class VertexArray {
private final FloatBuffer mFloatBuffer;
public VertexArray(float[] data){
mFloatBuffer = ByteBuffer
.allocateDirect(Constants.BYTE_PRE_FLOAT * data.length)
.order(ByteOrder.nativeOrder())
.asFloatBuffer();
mFloatBuffer.put(data);
}
public void setVertexAttribPointer(int dataOffset, int attributeLocation, int componentCount, int stride){
mFloatBuffer.position(dataOffset);
glVertexAttribPointer(attributeLocation, componentCount, GL_FLOAT,
false, stride, mFloatBuffer);
glEnableVertexAttribArray(attributeLocation);
mFloatBuffer.position(0);
}
}
在我们创建Mallet和Table之前,我们要先创建Shader类,因为我们想在Mallet和Table中实现绘制功能,所以我们先把Shader实现这部分代码抽离出来,整体架构如下:
我们先在
ShaderHelper.java
中添加如下代码:
public static int buildProgram(String vertexShaderCode , String fragmentShaderCode){
int vertexShaderObjectId = compileVertexShader(vertexShaderCode);
int fragmentShaderObjectId = compileFragmentShader(fragmentShaderCode);
int program = linkProgram(vertexShaderObjectId , fragmentShaderObjectId);
vaildProgram(program);
return program;
}
然后新建一个包programs
并创建类ShaderProgram.java
实现如下代码:
public class ShaderProgram {
// Uniform constants
protected static final String U_MATRIX = "u_Matrix";
protected static final String U_TEXTURE_UNIT = "u_TextureUnit";
// Attribute constants
protected static final String A_POSITION = "a_Position";
protected static final String A_COLOR = "a_Color";
protected static final String A_TEXTURE_COORDINATES = "a_TextureCoordinates";
// Shader program
protected final int mProgram;
protected ShaderProgram(Context context, int vertexShaderResourceId,
int fragmentShaderResourceId) {
// Compile the shaders and link the program.
mProgram = ShaderHelper.buildProgram(
TextResouceReader.readTextFileFromResource(context, vertexShaderResourceId),
TextResouceReader.readTextFileFromResource(context, fragmentShaderResourceId));
}
public void useProgram() {
// Set the current OpenGL shader program to this program.
glUseProgram(mProgram);
}
}
在该包下继续创建TextureShaderProgram.java
类实现如下代码:
public class TextureShaderProgram extends ShaderProgram{
// Uniform locations
private final int mUMatrixLocation;
private final int mUTextureUnitLocation;
// Attribute locations
private final int mAPositionLocation;
private final int mATextureCoordinatesLocation;
public TextureShaderProgram(Context context) {
super(context, R.raw.texture_vertex_shader, R.raw.texture_fragment_shader);
mUMatrixLocation = glGetUniformLocation(mProgram , U_MATRIX);
mUTextureUnitLocation = glGetUniformLocation(mProgram , U_TEXTURE_UNIT);
mAPositionLocation = glGetAttribLocation(mProgram , A_POSITION);
mATextureCoordinatesLocation = glGetAttribLocation(mProgram , A_TEXTURE_COORDINATES);
}
public void setUniforms(float[] matrix, int textureId) {
//把矩阵传递给Shader程序
glUniformMatrix4fv(mUMatrixLocation, 1, false, matrix, 0);
// Set the active texture unit to texture unit 0.
glActiveTexture(GL_TEXTURE0);
// Bind the texture to this unit.
glBindTexture(GL_TEXTURE_2D, textureId);
//告诉Texture Uniform Sampler用这个位置的Texture去渲染,从0开始
glUniform1i(mUTextureUnitLocation, 0);
}
public int getPositionAttributeLocation() {
return mAPositionLocation;
}
public int getTextureCoordinatesAttributeLocation() {
return mATextureCoordinatesLocation;
}
}
主要注意setUniforms()
这块的代码,这块代码就是用Texture去渲染的方法了。我们再在该包下面创建ColorShaderProgram.java
实现如下代码:
public class ColorShaderProgram extends ShaderProgram{
// Uniform locations
private final int mUMatrixLocation;
// Attribute locations
private final int mAPositionLocation;
private final int mAColorLocation;
public ColorShaderProgram(Context context) {
super(context, R.raw.simple_vertex_shader, R.raw.simple_fragment_shader);
mUMatrixLocation = glGetUniformLocation(mProgram , U_MATRIX);
mAColorLocation = glGetAttribLocation(mProgram , A_COLOR);
mAPositionLocation = glGetAttribLocation(mProgram , A_POSITION);
}
public void setUniforms(float[] matrix) {
// 把矩阵传递给渲染程序
glUniformMatrix4fv(mUMatrixLocation, 1, false, matrix, 0);
}
public int getPositionAttributeLocation() {
return mAPositionLocation;
}
public int getColorAttributeLocation() {
return mAColorLocation;
}
}
这部分代码就不用多做解释了,现在我们就可以去创建我们的Table、Mallet了,
先创建Table.java
,实现如下代码:
public class Table {
private final VertexArray mVertexData;
private static final int POSITION_COMPONENT_COUNT = 2;
private static final int TEXTURE_COORDINATES_COMPONENT_COUNT = 2;
private static final int STRIDE = (POSITION_COMPONENT_COUNT
+ TEXTURE_COORDINATES_COMPONENT_COUNT) * Constants.BYTE_PRE_FLOAT;
private static final float[] VERTEX_DATA = new float[]{
// Order of coordinates: X, Y, S, T
// Triangle Fan
0f, 0f, 0.5f, 0.5f,
-0.5f, -0.8f, 0f, 0.9f,
0.5f, -0.8f, 1f, 0.9f,
0.5f, 0.8f, 1f, 0.1f,
-0.5f, 0.8f, 0f, 0.1f,
-0.5f, -0.8f, 0f, 0.9f
};
public Table(){
mVertexData = new VertexArray(VERTEX_DATA);
}
public void bindData(TextureShaderProgram program){
mVertexData.setVertexAttribPointer(0 ,
program.getPositionAttributeLocation() ,
POSITION_COMPONENT_COUNT , STRIDE);
mVertexData.setVertexAttribPointer(POSITION_COMPONENT_COUNT ,
program.getTextureCoordinatesAttributeLocation() ,
TEXTURE_COORDINATES_COMPONENT_COUNT , STRIDE);
}
public void draw(){
glDrawArrays(GL_TRIANGLE_FAN, 0, 6);
}
}
再创建Mallet.java
,实现如下代码:
public class Mallet {
private static final int POSITION_COMPONENT_COUNT = 2;
private static final int COLOR_COMPONENT_COUNT = 3;
private static final int STRIDE = (POSITION_COMPONENT_COUNT + COLOR_COMPONENT_COUNT)
* Constants.BYTE_PRE_FLOAT;
private static final float[] VERTEXT_DATA = new float[]{
// Order of coordinates: X, Y, R, G, B
0f, -0.4f, 0f, 0f, 1f,
0f, 0.4f, 1f, 0f, 0f
};
private VertexArray mVertexData;
public Mallet(){
mVertexData = new VertexArray(VERTEXT_DATA);
}
public void bindData(ColorShaderProgram program){
mVertexData.setVertexAttribPointer(0 ,
program.getPositionAttributeLocation() ,
POSITION_COMPONENT_COUNT , STRIDE);
mVertexData.setVertexAttribPointer(
POSITION_COMPONENT_COUNT ,
program.getColorAttributeLocation(),
COLOR_COMPONENT_COUNT , STRIDE);
}
public void draw(){
glDrawArrays(GL_POINTS, 0, 2);
}
}
准备工作终于做好了,我们可以开始绘制了,因为在Renderer中基本要删掉所有代码,所以我们直接新建一个好了叫做TextrueRenderer.java
,并实现如下代码:
public class TextureRenderer implements GLSurfaceView.Renderer{
private final Context mContext;
private float[] mProjectionMatrix = new float[16];
private float[] mModelMatrix = new float[16];
private Table mTable;
private Mallet mMallet;
private TextureShaderProgram mTextureShaderProgram;
private ColorShaderProgram mColorShaderProgram;
private int mTexture;
public TextureRenderer(Context context){
mContext = context;
}
@Override
public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
mTable = new Table();
mMallet = new Mallet();
mTextureShaderProgram = new TextureShaderProgram(mContext);
mColorShaderProgram = new ColorShaderProgram(mContext);
mTexture = TextureHelper.loadTexture(mContext , R.drawable.air_hockey_surface);
}
@Override
public void onSurfaceChanged(GL10 gl10, int width, int height) {
glViewport(0 , 0 , width , height);
MatrixHelper.perspectiveM(mProjectionMatrix , 45 , (float)width / (float)height , 1f , 10f);
setIdentityM(mModelMatrix, 0);
translateM(mModelMatrix, 0, 0f, 0f, -2.5f);
rotateM(mModelMatrix, 0 , -60 , 1 , 0 , 0);
final float[] temp = new float[16];
multiplyMM(temp, 0, mProjectionMatrix, 0, mModelMatrix, 0);
System.arraycopy(temp, 0, mProjectionMatrix, 0, temp.length);
}
@Override
public void onDrawFrame(GL10 gl10) {
glClear(GL_COLOR_BUFFER_BIT);
mTextureShaderProgram.useProgram();
mTextureShaderProgram.setUniforms(mProjectionMatrix , mTexture);
mTable.bindData(mTextureShaderProgram);
mTable.draw();
mColorShaderProgram.useProgram();
mColorShaderProgram.setUniforms(mProjectionMatrix);
mMallet.bindData(mColorShaderProgram);
mMallet.draw();
}
}
然后现在运行一遍,你就能看到我们效果图的效果了。
项目代码在这里:https://github.com/KevinKmoo/AirHockey3DWithTexture
能力有限,自己读书的学习所得,有错误请指导,轻虐!
转载请注明出处。----by kmoo