Android OpenGL 编写简单滤镜

  Android 上使用Opengl进行滤镜渲染效率较高,比起单纯的使用CPU给用户带来的体验会好很多。滤镜的对象是图片,图片是以Bitmap的形式表示,Opengl不能直接处理Bitmap,在Android上一般是通过GLSurfaceView来进行渲染的,也可以说成Android需要借助GLSurfaceView来完成对图片的渲染。

  GlSurfaceView 的图片来源依然是Bitmap,但是Bitmap需要以纹理(Texture)的形式载入到Opengl中。因此我首先来看一下载入纹理的步骤:

  1. GLES20.glGenTextures() : 生成纹理资源的句柄

  2. GLES20.glBindTexture(): 绑定句柄

  3. GLUtils.texImage2D() :将bitmap传递到已经绑定的纹理中

  4. GLES20.glTexParameteri() :设置纹理属性,过滤方式,拉伸方式等

  这里做滤镜使用Android4.x以后提供的 Effect 类来完成,Effect类实现也是通过Shader的方式来完成的,这些Shader程序内置在Android中,我们只需要按照一定的方式来调用就行了。在Android上使用GLSurfaceView来显示并完成图片的渲染,实现渲染需要实现GLSurfaceView.Render接口,该接口有三个方法:onDrawFrame(GL10 gl) ,该方法按照一定的刷新频率反复执行;onSurfaceChanged(GL10 gl, int width, int height),该方法在窗口重绘的时候执行;onSurfaceCreated(GL10 gl, EGLConfig config) 在创建SurfaceView的时候执行。

  使用Effect类会用到EffectFactory 和 EffectContex,在下面的例子中看看具体的使用方式。

  首先定义一个Activity:EffectivefilterActivity

package com.example.effectsfilterdemo;

import java.nio.IntBuffer;

import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.opengl.GLSurfaceView;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;

public class EffectsFilterActivity extends Activity {

    private GLSurfaceView mEffectView;

    private TextureRenderer renderer;


    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        renderer = new TextureRenderer();
        renderer.setImageBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.puppy));
        renderer.setCurrentEffect(R.id.none);
        
        mEffectView = (GLSurfaceView) findViewById(R.id.effectsview);
        //mEffectView = new GLSurfaceView(this);
        mEffectView.setEGLContextClientVersion(2);
        //mEffectView.setRenderer(this);
        mEffectView.setRenderer(renderer);
        mEffectView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
        
        //setContentView(mEffectView);
    }
    
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        Log.i("info", "menu create");
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        renderer.setCurrentEffect(item.getItemId());
        mEffectView.requestRender();
        return true;
    }
}

  EffectivefilterActivity 中使用了两个布局文件,一个用于Activity的布局,另一个用于菜单的布局。

  R.layout.main:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <android.opengl.GLSurfaceView
        android:id="@+id/effectsview"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
         />

</LinearLayout>

  R.menu.main:

<menu xmlns:android="http://schemas.android.com/apk/res/android" >

    <item
        android:id="@+id/none"
        android:showAsAction="never"
        android:title="none"/>
    <item
        android:id="@+id/autofix"
        android:showAsAction="never"
        android:title="autofix"/>
    <item
        android:id="@+id/bw"
        android:showAsAction="never"
        android:title="bw"/>
    <item
        android:id="@+id/brightness"
        android:showAsAction="never"
        android:title="brightness"/>
    <item
        android:id="@+id/contrast"
        android:showAsAction="never"
        android:title="contrast"/>
    <item
        android:id="@+id/crossprocess"
        android:showAsAction="never"
        android:title="crossprocess"/>
    <item
        android:id="@+id/documentary"
        android:showAsAction="never"
        android:title="documentary"/>
    <item
        android:id="@+id/duotone"
        android:showAsAction="never"
        android:title="duotone"/>
    <item
        android:id="@+id/filllight"
        android:showAsAction="never"
        android:title="filllight"/>
    <item
        android:id="@+id/fisheye"
        android:showAsAction="never"
        android:title="fisheye"/>
    <item
        android:id="@+id/flipvert"
        android:showAsAction="never"
        android:title="flipvert"/>
    <item
        android:id="@+id/fliphor"
        android:showAsAction="never"
        android:title="fliphor"/>
    <item
        android:id="@+id/grain"
        android:showAsAction="never"
        android:title="grain"/>
    <item
        android:id="@+id/grayscale"
        android:showAsAction="never"
        android:title="grayscale"/>
    <item
        android:id="@+id/lomoish"
        android:showAsAction="never"
        android:title="lomoish"/>
    <item
        android:id="@+id/negative"
        android:showAsAction="never"
        android:title="negative"/>
    <item
        android:id="@+id/posterize"
        android:showAsAction="never"
        android:title="posterize"/>
    <item
        android:id="@+id/rotate"
        android:showAsAction="never"
        android:title="rotate"/>
    <item
        android:id="@+id/saturate"
        android:showAsAction="never"
        android:title="saturate"/>
    <item
        android:id="@+id/sepia"
        android:showAsAction="never"
        android:title="sepia"/>
    <item
        android:id="@+id/sharpen"
        android:showAsAction="never"
        android:title="sharpen"/>
    <item
        android:id="@+id/temperature"
        android:showAsAction="never"
        android:title="temperature"/>
    <item
        android:id="@+id/tint"
        android:showAsAction="never"
        android:title="tint"/>
    <item
        android:id="@+id/vignette"
        android:showAsAction="never"
        android:title="vignette"/>

</menu>

  在R.layout.main中只定义了一个GLSurfaceView用于显示图片,R.menu.main用于显示多个菜单项,通过点击菜单来完成调用不同滤镜实现对图片的处理。

  接下来看比较关键的Renderer接口的实现。

package com.example.effectsfilterdemo;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.media.effect.Effect;
import android.media.effect.EffectContext;
import android.media.effect.EffectFactory;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import android.opengl.GLUtils;
import android.util.Log;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.util.LinkedList;
import java.util.Queue;

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

public class TextureRenderer implements GLSurfaceView.Renderer{

    private int mProgram;
    private int mTexSamplerHandle;
    private int mTexCoordHandle;
    private int mPosCoordHandle;

    private FloatBuffer mTexVertices;
    private FloatBuffer mPosVertices;

    private int mViewWidth;
    private int mViewHeight;

    private int mTexWidth;
    private int mTexHeight;
    
    private Context mContext;
    private final Queue<Runnable> mRunOnDraw;
    private int[] mTextures = new int[2];
    int mCurrentEffect;
    private EffectContext mEffectContext;
    private Effect mEffect;
    private int mImageWidth;
    private int mImageHeight;
    private boolean initialized = false;

    private static final String VERTEX_SHADER =
        "attribute vec4 a_position;/n" +
        "attribute vec2 a_texcoord;/n" +
        "varying vec2 v_texcoord;/n" +
        "void main() {/n" +
        "  gl_Position = a_position;/n" +
        "  v_texcoord = a_texcoord;/n" +
        "}/n";

    private static final String FRAGMENT_SHADER =
        "precision mediump float;/n" +
        "uniform sampler2D tex_sampler;/n" +
        "varying vec2 v_texcoord;/n" +
        "void main() {/n" +
        "  gl_FragColor = texture2D(tex_sampler, v_texcoord);/n" +
        "}/n";

    private static final float[] TEX_VERTICES = {
        0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f
    };

    private static final float[] POS_VERTICES = {
        -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f
    };

    private static final int FLOAT_SIZE_BYTES = 4;
    
    public TextureRenderer() {
        // TODO Auto-generated constructor stub
        mRunOnDraw = new LinkedList<>();

    }

    public void init() {
        // Create program
        mProgram = GLToolbox.createProgram(VERTEX_SHADER, FRAGMENT_SHADER);

        // Bind attributes and uniforms
        mTexSamplerHandle = GLES20.glGetUniformLocation(mProgram,
                "tex_sampler");
        mTexCoordHandle = GLES20.glGetAttribLocation(mProgram, "a_texcoord");
        mPosCoordHandle = GLES20.glGetAttribLocation(mProgram, "a_position");

        // Setup coordinate buffers
        mTexVertices = ByteBuffer.allocateDirect(
                TEX_VERTICES.length * FLOAT_SIZE_BYTES)
                .order(ByteOrder.nativeOrder()).asFloatBuffer();
        mTexVertices.put(TEX_VERTICES).position(0);
        mPosVertices = ByteBuffer.allocateDirect(
                POS_VERTICES.length * FLOAT_SIZE_BYTES)
                .order(ByteOrder.nativeOrder()).asFloatBuffer();
        mPosVertices.put(POS_VERTICES).position(0);
    }

    public void tearDown() {
        GLES20.glDeleteProgram(mProgram);
    }

    public void updateTextureSize(int texWidth, int texHeight) {
        mTexWidth = texWidth;
        mTexHeight = texHeight;
        computeOutputVertices();
    }

    public void updateViewSize(int viewWidth, int viewHeight) {
        mViewWidth = viewWidth;
        mViewHeight = viewHeight;
        computeOutputVertices();
    }

    public void renderTexture(int texId) {
        GLES20.glUseProgram(mProgram);
        GLToolbox.checkGlError("glUseProgram");

        GLES20.glViewport(0, 0, mViewWidth, mViewHeight);
        GLToolbox.checkGlError("glViewport");

        GLES20.glDisable(GLES20.GL_BLEND);

        GLES20.glVertexAttribPointer(mTexCoordHandle, 2, GLES20.GL_FLOAT, false,
                0, mTexVertices);
        GLES20.glEnableVertexAttribArray(mTexCoordHandle);
        GLES20.glVertexAttribPointer(mPosCoordHandle, 2, GLES20.GL_FLOAT, false,
                0, mPosVertices);
        GLES20.glEnableVertexAttribArray(mPosCoordHandle);
        GLToolbox.checkGlError("vertex attribute setup");

        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
        GLToolbox.checkGlError("glActiveTexture");
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texId);//把已经处理好的Texture传到GL上面
        GLToolbox.checkGlError("glBindTexture");
        GLES20.glUniform1i(mTexSamplerHandle, 0);

        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
    }

    private void computeOutputVertices() { //调整AspectRatio 保证landscape和portrait的时候显示比例相同,图片不会被拉伸
        if (mPosVertices != null) {
            float imgAspectRatio = mTexWidth / (float)mTexHeight;
            float viewAspectRatio = mViewWidth / (float)mViewHeight;
            float relativeAspectRatio = viewAspectRatio / imgAspectRatio;
            float x0, y0, x1, y1;
            if (relativeAspectRatio > 1.0f) {
                x0 = -1.0f / relativeAspectRatio;
                y0 = -1.0f;
                x1 = 1.0f / relativeAspectRatio;
                y1 = 1.0f;
            } else {
                x0 = -1.0f;
                y0 = -relativeAspectRatio;
                x1 = 1.0f;
                y1 = relativeAspectRatio;
            }
            float[] coords = new float[] { x0, y0, x1, y0, x0, y1, x1, y1 };
            mPosVertices.put(coords).position(0);
        }
    }
    
    private void initEffect() {
        EffectFactory effectFactory = mEffectContext.getFactory();
        if (mEffect != null) {
            mEffect.release();
        }
        /**
         * Initialize the correct effect based on the selected menu/action item
         */
        switch (mCurrentEffect) {

        case R.id.none:
            break;

        case R.id.autofix:
            mEffect = effectFactory.createEffect(EffectFactory.EFFECT_AUTOFIX);
            mEffect.setParameter("scale", 0.5f);
            break;

        case R.id.bw:
            mEffect = effectFactory.createEffect(EffectFactory.EFFECT_BLACKWHITE);
            mEffect.setParameter("black", .1f);
            mEffect.setParameter("white", .7f);
            break;

        case R.id.brightness:
            mEffect = effectFactory.createEffect(EffectFactory.EFFECT_BRIGHTNESS);
            mEffect.setParameter("brightness", 2.0f);
            break;

        case R.id.contrast:
            mEffect = effectFactory.createEffect(EffectFactory.EFFECT_CONTRAST);
            mEffect.setParameter("contrast", 1.4f);
            break;

        case R.id.crossprocess:
            mEffect = effectFactory.createEffect(EffectFactory.EFFECT_CROSSPROCESS);
            break;

        case R.id.documentary:
            mEffect = effectFactory.createEffect(EffectFactory.EFFECT_DOCUMENTARY);
            break;

        case R.id.duotone:
            mEffect = effectFactory.createEffect(EffectFactory.EFFECT_DUOTONE);
            mEffect.setParameter("first_color", Color.YELLOW);
            mEffect.setParameter("second_color", Color.DKGRAY);
            break;

        case R.id.filllight:
            mEffect = effectFactory.createEffect(EffectFactory.EFFECT_FILLLIGHT);
            mEffect.setParameter("strength", .8f);
            break;

        case R.id.fisheye:
            mEffect = effectFactory.createEffect(EffectFactory.EFFECT_FISHEYE);
            mEffect.setParameter("scale", .5f);
            break;

        case R.id.flipvert:
            mEffect = effectFactory.createEffect(EffectFactory.EFFECT_FLIP);
            mEffect.setParameter("vertical", true);
            break;

        case R.id.fliphor:
            mEffect = effectFactory.createEffect(EffectFactory.EFFECT_FLIP);
            mEffect.setParameter("horizontal", true);
            break;

        case R.id.grain:
            mEffect = effectFactory.createEffect(EffectFactory.EFFECT_GRAIN);
            mEffect.setParameter("strength", 1.0f);
            break;

        case R.id.grayscale:
            mEffect = effectFactory.createEffect(EffectFactory.EFFECT_GRAYSCALE);
            break;

        case R.id.lomoish:
            mEffect = effectFactory.createEffect(EffectFactory.EFFECT_LOMOISH);
            break;

        case R.id.negative:
            mEffect = effectFactory.createEffect(EffectFactory.EFFECT_NEGATIVE);
            break;

        case R.id.posterize:
            mEffect = effectFactory.createEffect(EffectFactory.EFFECT_POSTERIZE);
            break;

        case R.id.rotate:
            mEffect = effectFactory.createEffect(EffectFactory.EFFECT_ROTATE);
            mEffect.setParameter("angle", 180);
            break;

        case R.id.saturate:
            mEffect = effectFactory.createEffect(EffectFactory.EFFECT_SATURATE);
            mEffect.setParameter("scale", .5f);
            break;

        case R.id.sepia:
            mEffect = effectFactory.createEffect(EffectFactory.EFFECT_SEPIA);
            break;

        case R.id.sharpen:
            mEffect = effectFactory.createEffect(EffectFactory.EFFECT_SHARPEN);
            break;

        case R.id.temperature:
            mEffect = effectFactory.createEffect(EffectFactory.EFFECT_TEMPERATURE);
            mEffect.setParameter("scale", .9f);
            break;

        case R.id.tint:
            mEffect = effectFactory.createEffect(EffectFactory.EFFECT_TINT);
            mEffect.setParameter("tint", Color.MAGENTA);
            break;

        case R.id.vignette:
            mEffect = effectFactory.createEffect(EffectFactory.EFFECT_VIGNETTE);
            mEffect.setParameter("scale", .5f);
            break;

        default:
            break;

        }
    }
    
    public void setCurrentEffect(int effect) {
        mCurrentEffect = effect;
    }

    
    public void setImageBitmap(final Bitmap bmp){
        runOnDraw(new Runnable() {
            
            @Override
            public void run() {
                // TODO Auto-generated method stub
                loadTexture(bmp);
            }
        });
    }
    
    private void loadTexture(Bitmap bmp){
        GLES20.glGenTextures(2, mTextures , 0);

        updateTextureSize(bmp.getWidth(), bmp.getHeight());
        
        mImageWidth = bmp.getWidth();
        mImageHeight = bmp.getHeight();

        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextures[0]);
        GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bmp, 0);

        GLToolbox.initTexParams();
    }
    
    private void applyEffect() {
        if(mEffect == null){
            Log.i("info","apply Effect null mEffect");
        }
        
        mEffect.apply(mTextures[0], mImageWidth, mImageHeight, mTextures[1]);
    }

    private void renderResult() {
        if (mCurrentEffect != R.id.none) {
            renderTexture(mTextures[1]);
        } else {
            renderTexture(mTextures[0]);
        }
    }

    @Override
    public void onDrawFrame(GL10 gl) {
        // TODO Auto-generated method stub
        if(!initialized){
            init();
            mEffectContext = EffectContext.createWithCurrentGlContext();
            initialized = true;
        }
        
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
        synchronized (mRunOnDraw) {
            while (!mRunOnDraw.isEmpty()) {
                mRunOnDraw.poll().run();
            }
        }
        
        if (mCurrentEffect != R.id.none) {
            initEffect();
            applyEffect();
        }
        renderResult();
    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        // TODO Auto-generated method stub
        updateViewSize(width, height);
    }

    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        // TODO Auto-generated method stub
        
    }
    
    protected void runOnDraw(final Runnable runnable) {
        synchronized (mRunOnDraw) {
            mRunOnDraw.add(runnable);
        }
    }
}

  这里有一个地方需要注意,任何使用Opengl接口的方法调用需要在Opengl Context中进行,否则会出现:call to OpenGL ES API with no current context (logged once per thread) 报错信息。所谓的Opengl Context 其实就是需要在onDrawFrame(GL10 gl),onSurfaceChanged(GL10 gl, int width, int height),onSurfaceCreated(GL10 gl, EGLConfig config)中调用,注意到这三个方法都有一个参数GL10。这里还有一个地方就是在载入纹理之前需要载入位图,使用了runOnDraw()方法将loadTexure的步骤放在onDrawFrame() 中来完成,巧妙的为外界提供了一个接口并使得操作在具有Opengl Context的黄金中完成。

  最后来看看辅助的工具类(GLToolbox),该类完成Shader程序的创建,应用程序提供Shader 源码给该工具类编译:

package com.example.effectsfilterdemo;
import android.opengl.GLES20;

public class GLToolbox {

    public static int loadShader(int shaderType, String source) {
        int shader = GLES20.glCreateShader(shaderType);
        if (shader != 0) {
            GLES20.glShaderSource(shader, source);
            GLES20.glCompileShader(shader);
            int[] compiled = new int[1];
            GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0);
            if (compiled[0] == 0) {
                String info = GLES20.glGetShaderInfoLog(shader);
                GLES20.glDeleteShader(shader);
                shader = 0;
                throw new RuntimeException("Could not compile shader " +
                shaderType + ":" + info);
            }
        }
        return shader;
    }

    public static int createProgram(String vertexSource,
            String fragmentSource) {
        int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource);
        if (vertexShader == 0) {
            return 0;
        }
        int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);
        if (pixelShader == 0) {
            return 0;
        }

        int program = GLES20.glCreateProgram();
        if (program != 0) {
            GLES20.glAttachShader(program, vertexShader);
            checkGlError("glAttachShader");
            GLES20.glAttachShader(program, pixelShader);
            checkGlError("glAttachShader");
            GLES20.glLinkProgram(program);
            int[] linkStatus = new int[1];
            GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus,
                    0);
            if (linkStatus[0] != GLES20.GL_TRUE) {
                String info = GLES20.glGetProgramInfoLog(program);
                GLES20.glDeleteProgram(program);
                program = 0;
                throw new RuntimeException("Could not link program: " + info);
            }
        }
        return program;
    }

    public static void checkGlError(String op) {
        int error;
        while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) {
            throw new RuntimeException(op + ": glError " + error);
        }
    }

    public static void initTexParams() {
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
                GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
                GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S,
                GLES20.GL_CLAMP_TO_EDGE);
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T,
                GLES20.GL_CLAMP_TO_EDGE);
   }

}

  这里就不提供整个工程了,结合上面的代码,自己在资源文件中提供一个图片载入就可以看到效果了。

Android OpenGL 基础入门

  Android 自从2.2 版本之后之后开始支持OpenGL,在没有支持OpenGL 的 GPU的情况下,也可以使用(通过软件来模拟)。在Android上使用Opengl操作的对象是GLSurfaceView,这是一个继承自View的扩展。

  在Android上Opengl是通过Vertex Shader 和 Fragment Shader 这两种定点着色器程序来实现图片的加载和渲染的,中文称为定点着色器和片段着色器。一个完整的Opengl程序需要创建定点着色器和片段着色器并将他们Link起来组成一个完整的OpenGL程序。

  顶点着色器的作用是为每一个顶点生成坐标,因此每个顶点都要运行一遍顶点着色器程序,一旦顶点坐标计算出来之后,OpenGL就能够使用这些顶点来组成点,线,和三角形。所有任意的图形都是由这三种基本元素来描述的。下图是顶点着色器进行坐标转换的过程(稍微有点复杂):

  Android OpenGL 基础入门

  这个过程包含了从原始的对象坐标经过模型视图转换生成眼坐标,再经过投影转换生成裁剪坐标,再通过w坐标的归一化转换成为NDC(顶点坐标由(x,y,z,w)构成,在shader程序中一般用一个四维向量vec4来描述),最终通过视口变换生成屏幕坐标显示在屏幕上。这一系列的转换都是通过矩阵来完成的,转换过程和原理是Opengl的精华内容,对于需要进入3D世界的同学需要掌握。在2D世界中我们只需要了解NDC就行了,这里就把NDC叫做OpenGL 坐标,加上Normalized 的修饰是因为这些坐标的值都在(-1,1)之间,OpenGL坐标表示见下图:

Android OpenGL 基础入门

  OpenGL坐标原点在屏幕中央,左右坐标范围为[-1,1],在2d环境中坐标值只存在(x,y),并且坐标范围都只能在-1 到 1之间,屏幕中心坐标为(0,0)。因此如果需要指定一张图片的显示位置,指定坐标需要根据这个坐标系来,此外为了保证图片显示比例(长宽比例),在portrait 到 landscape 之间变换的时候一般需要乘以一个aspectRatio (width / height) 来重新设定坐标值。

  了解了Opengl坐标,在来了解一下屏幕坐标(屏幕坐标的坐标原点在左上角)和纹理坐标(纹理坐标的坐标原点在左下角):

Android OpenGL 基础入门

屏幕坐标系                Android OpenGL 基础入门

  纹理坐标系

  片段着色器的作用是为点,线或者三角形的每一个顶点的片段(Fragment)生成渲染后的最终颜色。片段就是一个小的单色矩形区域,可以简单的认为是屏幕上的一个像素点。

  以上基本知识基本上可以处理Opengl实现2D图像的绘制和处理。下面来简单看一下Shader的写法,在Android平台上Shader程序一般以字符串的形式出现,或者在res/raw/目录下以*.glsl的格式出现,如果是以单独的文件出现,需要定义专门的文件读入接口来加载Shader程序。这里就介绍一下字符串的形式的Shader程序,来看下面简单的Vertex Shader 和 Fragment Shader:

    private static final String VERTEX_SHADER =
        "attribute vec4 a_position;/n" +
        "attribute vec2 a_texcoord;/n" +
        "varying vec2 v_texcoord;/n" +
        "void main() {/n" +
        "  gl_Position = a_position;/n" +
        "  v_texcoord = a_texcoord;/n" +
        "}/n";

    private static final String FRAGMENT_SHADER =
        "precision mediump float;/n" +
        "uniform sampler2D tex_sampler;/n" +
        "varying vec2 v_texcoord;/n" +
        "void main() {/n" +
        "  gl_FragColor = texture2D(tex_sampler, v_texcoord);/n" +
        "}/n";

  先看一下VERTEX_SHADER,定义了两种类型的变量 atrribute 和 varying。

  其中 attribute变量是只能在vertex shader中使用的变量。(它不能在fragment shader中声明attribute变量,也不能被fragment shader中使用),一般用attribute变量来表示一些顶点的数据,这些顶点数据包含了顶点坐标,法线,纹理坐标,顶点颜色等。应用中一般用函数glBindAttribLocation()来绑定每个attribute变量的位置,然后用函数glVertexAttribPointer()为每个attribute变量赋值。

  varying被称为易变量,一般用于从Vertex Shader 向 Fragment Shader传递数据,上面例子中在VertexShader中定义了attribute 类型的二维向量a_texcoord,并将该值赋值给varying类型的二维向量 v_texcoord。此外对于Vertex Shader 在main() 中必须将顶点坐标赋值给系统变量gl_Position。

  看一下FRAGMENT_SHADER,定义了两种类型的变量,uniform 和 varying。此外还多了一句 precision mediump float,这句话用于定义数据精度,Opengl中可以设置三种类型的精度(lowp,medium 和 highp),对于Vertex Shader来说,Opengl使用的是默认最高精度级别(highp),因此没有定义。

  uniform变量是APP序传递给(vertex和fragment)shader的变量。通过函数glUniform**()函数赋值的。 在(vertex和fragment)shader程序内部,uniform变量就像是C语言里面的常量(const ),它不能被shader程序修改(shader只能用,不能改)。如果uniform变量在vertex和fragment两者之间声明方式完全一样,则它可以在vertex和fragment共享使用。 (相当于一个被vertex和fragment shader共享的全局变量)uniform变量一般用来表示:变换矩阵,材质,光照参数和颜色等信息。

  对于Fragment Shader也需对gl_FragColor赋值。

  现在对基本的Shader有了了解,来看一下Android怎么使用Shader程序。先说明一下Opengl中的资源一般都是用一个句柄(handle)来引用,句柄一般由gl***接口返回,代表一个特定的资源。

  在Android上使用gl,需要用到一些列接口,这里按照一般的调用顺序来列一下基本的接口(暂不包含错误处理)

  1. 和创建Shader相关的:glCreateShader -> glShaderSource -> glCompileShader , 通过这几个接口的调用最终返回一个Shader的句柄

  2. 和创建Shader程序相关的(每个shader程序都必须包含Vertex Shader 和 Fragment Shader两部分):glCreateProgram -> glAttachShader -> glLinkProgram,通过这些接口的调用最终返回个Program的句柄。

  3. 获取Shader内部变量赋值和传递数据到gl,对于不同类型的数据使用不同的接口:GLES20.glGetUniformLocation(mProgram, “tex_sampler”),GLES20.glGetAttribLocation(mProgram, “a_texcoord”),以上两个接口中mProgram 代表glCreateProgram返回的 Shader程序句柄,“a_texcoord”和”tex_sampler”是 Vertex Shader和 Fragment Shader中定义的变量,通过这两个接口获取了变量的句柄,便于向这些变量中传入值。获得变量的句柄之后就要向其中传值,一般通过glVertexAttribPointer()来完成,详细参数这里没有列出,在实例代码中可以学习一下,传值之后需要使glEnableVertexAttribArray才能将数据传入到gl中,传入数据使用glDrawArrays() 。

  这里介绍了Opengl的基本知识,下一篇结合实例介绍一下Opengl在Android上的使用。

使用Codeblock搭建Windows下Objec-c学习环境

  学习Object-c如果使用的是Windows,一般推荐使用虚拟机,但是太重量级了,先要下载OS-X,又要下载x-code。这里推荐一种比较简便的方式,使用code-block来搭建简易的Object-c学习环境,下载地址是:http://www.codblocks.org/。

  Objective-C的编译器有很多,这里使用GnuStep,网址是http://www.gnustep.org/experience/Windows.html,从这里可以下载Windows版本的gcc编译器,共有四个软件包,其中GNUstep System和GNUstep Core是必装的,GNUstep Devel和Cairo Backend是选装的(用迅雷拖要快点)。

  下载并安装好Code-block 和 GnuStep 之后,对Code-block进行配置。

  打开Code::Blocks,点击菜单Settings>Compiler and debugger>Global compiler settings在Selected compiler下拉框下面点击Copy, 在弹出窗口中填入: GNUstep MinGW Compiler
之后,点击Toolchain executables选项卡,将Compiler’s installation directory选择为C:/GNUstep,如下图:
使用Codeblock搭建Windows下Objec-c学习环境
  

  接下来找到Other Options 选项卡,写入-fconstant-string-class=NSConstantString -std=c99 :

 

使用Codeblock搭建Windows下Objec-c学习环境

    接下来设置静态链接库,在Link settings 中进行添加,需要添加的.a文件在:C:/GNUstep/GNUstep/System/Library/Libraries: libgnustep-base.dll.a 和 libobjc.dll.a,

    使用Codeblock搭建Windows下Objec-c学习环境

 

     接下来在Search irectories 中的Compiler选项卡中增加:C:/GNUstep/GNUstep/System/Library/Headers;在Linker选项卡中增加:C:/GNUstep/GNUstep/System/Library/Libraries。这里就不截图了。

       进入Settings->Environment…,选择Files extension handling 添加*.m;进入 Project->Projecttree->Edit file types & categories… ,在Sources, 下面添加*.m到文件类型列表中。

       进入Settings->Editor…,选择 Syntaxhighlighting,点击“Filemasks….”按钮,在弹出框尾部添加*.m 到文件类型;点击“Keywords…”按钮 (紧靠Filemasks…按钮) 添加下面Object-C的关键字到EditKeywords列表中:@interface @implementation @end  @class @selector @protocol @public @protected @private id BOOL YES NO SEL nil  NULL self。

     

     最后进行测试,选择File->New->Project…,会出现一个工程类型窗口,选择Console Application,然后按照工程建立指引,建立一个mTest的工程,并将main.c的文件更名为main.m,写入代码:

#import <Foundation/Foundation.h>
int main (int argc, const char * argv[])
{
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

    NSDate *now = [NSDate date];
    NSLog(@"This NSDate object lives at %p", now);
    NSLog(@"The date is %@", now);

    [pool drain];
    return 0;
}

  编译运行上述代码,结果如下:

  使用Codeblock搭建Windows下Objec-c学习环境