Android OpenGL ES开发(七)纹理

一、什么是纹理?

纹理,英文texture,中文可以翻译成纹理、纹理图、纹理映射等等一堆东西。不过不管翻译成啥,讲的都是一个东西。我们通常说的纹理指的是一张二维的图片,把它像贴纸一样贴在什么东西上面,让那个东西看起来我们贴纸索要表现的东西那样。

举个例子,加入我们想绘制一面墙,我们该怎么办?根据我们已经掌握的只是来看,我们需要用成千上万的点来模拟它的颜色,这要搞到猴年马月才能搞出来?显然不现实,于是聪明的程序员想出了一个好方法,那就是用一张图“贴”到物体表面上,让它看起来像是一面墙的样子,省时省力省心。
用一句话来总结,纹理就是一张贴在物体上的二维图像。

二、映射方式

既然是要把图贴在物体上,自然就要想怎么贴才行。我们可以横贴、竖贴、斜贴等等,怎么贴都行,但是怎么贴才能达到我们想要的效果呢?总要有个制度来规定一下才行吧。这个贴法,就成为映射。
规则是:以左下角为原点,向右伸展到1.0的位置,向上伸展到1.0的位置,表示一整张的纹理图像。

1.jpg

使用纹理图的时候,我们需要在顶点数据中添加一个纹理坐标的数据,表明我们是如何将纹理上的元素映射到顶点上的,这个我们后续环节再详细说明。

三、纹理环绕方式(Texture Wrapping)

通常,纹理坐标的范围在(0,0)到(1,1)之间,但是如果我们制定的坐标在这之外呢?OpenGL会如何做出反应?默认情况下,OpenGL会重复绘制纹理图,不过,OpenGL也提供了更多的选择方案:

GL_REPEAT:默认方案,重复纹理图片
GL_MIRRORED_REPEAT:类似默认方案,不过每次重复的时候进行镜像重复。
GL_CLAMP_TO_EDGE:将坐标限制在0到1之间。超过的坐标会重复绘制边缘的像素,变成一个扩展边缘的图案。
GL_CLAMP_TO_BOREDER:超出的坐标将会被绘制成用户指定的边界颜色(OpenGL ES2.0没有这种模式)

每种方案的显示效果截然不同,不必担心你会搞混,看看效果就知道了。


四种纹理环绕方式的效果对比.jpg

怎么样,只要实例在1000度之内,都能看出明显的区别吧。

设置环绕方式的方法是调用glTexParameteri函数,具体方式如下:

//设置环绕方向S:指定横向模式为GL_CLAMP_TO_EDGE
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
//设置环绕方向T:指定横向模式为GL_CLAMP_TO_EDGE
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);

纹理过滤

纹理坐标采用了浮点数的形式,表明了它跟分辨率无关。OpenGL需要非常精确的计算出纹理像素(通常称为纹素)和纹理坐标之前的对应关系。当你有一张低分辨率的纹理图,但是需要用到一个非常大的物体上时,这种操作的重要性就更加明显了。你可能猜到了,OpenGL提供了几种不同的方案来解决这个问题,我们只讨论最重要的两种:GL_NEAREST和GL_LINEAR。

GL_NEAREST
最近点过滤。指的是纹理坐标最近的哪个纹素。这是OpenGL默认的过滤方式,速度最快,但是效果最差。

GL_LINEAR
(双)线性过滤。指的是纹理坐标位置附近的几个纹素值进行某种插值计算之后的结果。这是应用最广泛的一种方式,效果一般,速度较快。

不过,你一定有个疑问,这些方法使用后,显示上有啥区别?别急,我们来看两张图就知道了


最近点过滤和线性过滤的区别.png

很明显,最近点过滤的像素很急非常明显,而线性过滤的方式效果就好很多,虽然感觉很模糊,单我们完全能理解一张小图放大后会模糊这件事。

设置过滤方式还是使用glTexParameteri,像这样:

GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);//缩小时的过滤方式
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);//放大时的过滤方式

Mipmaps

多级渐进纹理。想想这样一个场景,有非常多的同种物体,距离观察者的远近各不相同,我们可以对这种物体使用同一张纹理贴图,但是问题来了,对于那些离观察者比较远的物体,真有必要用原始的纹理贴图去映射吗?答案是否定的。太远的物体,我们看不清楚,显示的非常精细没有意义,而且使用原始贴图计算映射起来太麻烦,所以,我们使用一种mipmaps的方式来进行处理。

所谓的mipmaps,就是一系列的纹理图片,每张纹理图的大小都是前一张的1/4,直到剩最后一个像素位置。看起来就像是这一个样子:


mipmaps示例

当物体越离越远的时候,就可以选用较小纹理去映射,这样不仅效果好,而且速度也快。

你可能会有疑问,这个图片是程序弄的还是美术弄的呢?对于这个问题,我的回答是:美术弄不弄我不知道,但是程序肯定可以弄,而且弄起来还非常方便。我们只要调用一个函数就行了,那就是GLES20.glGenerateMipmap。
还有一个问题没有解决。

疑问:
看上去很好,可是我物体的大小刚好在两张mipmaps之间怎么办呢?

对于刚好在两张图片之前的物体,我们可以参考前面两种过滤方式,最近点(采用最接近的图)或者线性(采用两张图的加全平均)。这样,我们就有了四中不同的过滤方案。

GL_NEAREST_MIPMAP_NEAREST:采用最近的mipmap图,在纹理采样的时候使用最近点过滤采样。
GL_LINEAR_MIPMAP_NEAREST:采用最近的mipmap图,纹理采样的时候使用线性过滤采样。
GL_NEAREST_MIPMAP_LINEAR:采样两张mipmap图的线性插值纹理图,纹理采样的时候采用最近点过滤采样。
GL_LINEAR_MIPMAP_LINEAR:采用两张mipmap图的线性插值纹理图,纹理采样的时候采用线性过滤采样。

好了,这次是真的可以用了。使用的函数还是一样,如下:

GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR_MIPMAP_LINEAR);  //都是线性过滤
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);

这里要注意一点,mipmaps是用来处理物体变小是如何进行贴图问题的,所以不需要设置GL_TEXTURE_MAG_FILTER成mipmap方式。如果强行使用,会报错。

使用

1、顶点着色器

    private final String vertextShaderCode =
            "attribute vec4 vPosition;" +
            "uniform mat4 vMatrix;" +
            "attribute vec2 vCoordinate;" +
            "varying vec2 aCoordinate;" +
            "void main() {" +
            "   gl_Position = vMatrix * vPosition;" +
            "   aCoordinate=vCoordinate;" +
            "}";

2、片元着色器

    private final String fragmentShaderCode =
            "precision mediump float;" +
            "uniform sampler2D vTexture;" +
            "varying vec2 aCoordinate;" +
            "void main() {" +
            "   gl_FragColor = texture2D(vTexture,aCoordinate);" +
            "}";

3、顶点坐标

    private float[] shapePos = {
            -1.0f, 1.0f,    //左上角
            -1.0f, -1.0f,   //左下角
            1.0f, 1.0f,     //右上角
            1.0f, -1.0f     //右下角
    };

4、纹理坐标

    private float[] sCoord = {
            0.0f, 0.0f,
            0.0f, 1.0f,
            1.0f, 0.0f,
            1.0f, 1.0f
    };

4、创建、绑定纹理、加载图片

想OpenGL里的其他东西那样,纹理也需要一个唯一ID,我们来创建一个:

texture = new int[1];
//生成一个纹理,纹理ID放在int[] texture
GLES20.glGenTextures(1, texture, 0);

创建完后,我们需要绑定到OpenGL环境中才可以

 //将已经创建并命名的纹理绑定到一个纹理目标(GL_TEXTURE_2D)上
  GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture[0]);

绑定完成后,我们就可以把图片数据放到纹理中了。

//将图片数据传到上面创建的纹理中
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, mBitmap, 0);

5、计算变换矩阵

按照以上步骤,大多数情况下,我们得到的一定是一张拉伸或者压缩的图片。为了让图片完整的显示,且不被拉伸和压缩,我们需要向绘制等腰直角三角形一样,计算一个合适的变换矩阵,传入顶点着色器,代码如下:

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height)
    {
        GLES20.glViewport(0, 0, width, height);
        int w = mBitmap.getWidth();
        int h = mBitmap.getHeight();
        float sWH = w / (float) h;
        float sWidthHeight = width / (float) height;
        if (width > height)
        {
            if (sWH > sWidthHeight)
            {
                //设置正交矩阵
                Matrix.orthoM(mProjectMatrix, 0, -sWidthHeight * sWH, sWidthHeight * sWH, -1, 1, 3, 7);
            } else
            {
                //设置正交矩阵
                Matrix.orthoM(mProjectMatrix, 0, -sWidthHeight / sWH, sWidthHeight / sWH, -1, 1, 3, 7);
            }
        } else
        {
            if (sWH > sWidthHeight)
            {
                //设置正交矩阵
                Matrix.orthoM(mProjectMatrix, 0, -1, 1, -1 / sWidthHeight * sWH, 1 / sWidthHeight * sWH, 3, 7);
            } else
            {
                //设置正交矩阵
                Matrix.orthoM(mProjectMatrix, 0, -1, 1, -sWH / sWidthHeight, sWH / sWidthHeight, 3, 7);
            }
        }
        //设置相机位置
        Matrix.setLookAtM(mViewMatrix, 0, 0, 0, 7.0f, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
        //计算变换矩阵
        Matrix.multiplyMM(mMVPMatrix, 0, mProjectMatrix, 0, mViewMatrix, 0);


    }

完整代码:

/*
 * 纹理贴图
 * */
public class MyNinthRender implements GLSurfaceView.Renderer
{

    private final String vertextShaderCode =
            "attribute vec4 vPosition;" +
            "uniform mat4 vMatrix;" +
            "attribute vec2 vCoordinate;" +
            "varying vec2 aCoordinate;" +
            "void main() {" +
            "   gl_Position = vMatrix * vPosition;" +
            "   aCoordinate=vCoordinate;" +
            "}";
    private final String fragmentShaderCode =
            "precision mediump float;" +
            "uniform sampler2D vTexture;" +
            "varying vec2 aCoordinate;" +
            "void main() {" +
            "   gl_FragColor = texture2D(vTexture,aCoordinate);" +
            "}";
    private final Context context;

    private int program;
    private FloatBuffer vertexBuffer;
    //投影矩阵
    private final float[] mProjectMatrix = new float[16];
    //相机位置矩阵
    private final float[] mViewMatrix = new float[16];
    //计算变换矩阵
    private final float[] mMVPMatrix = new float[16];
    private float[] shapePos = {
            -1.0f, 1.0f,    //左上角
            -1.0f, -1.0f,   //左下角
            1.0f, 1.0f,     //右上角
            1.0f, -1.0f     //右下角
    };
    private float[] sCoord = {
            0.0f, 0.0f,
            0.0f, 1.0f,
            1.0f, 0.0f,
            1.0f, 1.0f
    };

    private Bitmap mBitmap;
    private FloatBuffer coordBuffer;
    private int[] texture;

    public MyNinthRender(Context context)
    {
        this.context = context;
    }

    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config)
    {
//        GLES20.glEnable(GLES20.GL_DEPTH_TEST);
        //将背景设置为灰色
        GLES20.glClearColor(0.5f, 0.2f, 1.0f, 1.0f);
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);

        //申请底层空间
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(shapePos.length * 4);
        byteBuffer.order(ByteOrder.nativeOrder());
        //将坐标数据转换为FloatBuffer,用以传入给OpenGL ES程序
        vertexBuffer = byteBuffer.asFloatBuffer();
        //将三角形坐标传入FloatBuffer
        vertexBuffer.put(shapePos);
        vertexBuffer.position(0);

        //申请底层空间
        ByteBuffer coordByteBuffer = ByteBuffer.allocateDirect(sCoord.length * 4);
        coordByteBuffer.order(ByteOrder.nativeOrder());
        //将坐标数据转换为FloatBuffer,用以传入给OpenGL ES程序
        coordBuffer = coordByteBuffer.asFloatBuffer();
        //将三角形坐标传入FloatBuffer
        coordBuffer.put(sCoord);
        coordBuffer.position(0);


        //创建顶点着色器程序
        int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertextShaderCode);
        //创建片元着色器程序
        int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode);

        if (vertexShader == 0 || fragmentShader == 0)
        {
            return;
        }
        //创建一个空的OpenGL ES程序
        program = GLES20.glCreateProgram();
        //将顶点着色器加入程序
        GLES20.glAttachShader(program, vertexShader);
        //将片元着色器加入程序
        GLES20.glAttachShader(program, fragmentShader);
        //连接到着色器程序中
        GLES20.glLinkProgram(program);
        //使用程序
        GLES20.glUseProgram(program);


        texture = new int[1];
        //生成一个纹理,纹理ID放在int[] texture
        GLES20.glGenTextures(1, texture, 0);


        mBitmap = BitmapFactory.decodeResource(context.getResources(), R.mipmap.bg);

    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height)
    {
        GLES20.glViewport(0, 0, width, height);
        int w = mBitmap.getWidth();
        int h = mBitmap.getHeight();
        float sWH = w / (float) h;
        float sWidthHeight = width / (float) height;
        if (width > height)
        {
            if (sWH > sWidthHeight)
            {
                //设置正交矩阵
                Matrix.orthoM(mProjectMatrix, 0, -sWidthHeight * sWH, sWidthHeight * sWH, -1, 1, 3, 7);
            } else
            {
                //设置正交矩阵
                Matrix.orthoM(mProjectMatrix, 0, -sWidthHeight / sWH, sWidthHeight / sWH, -1, 1, 3, 7);
            }
        } else
        {
            if (sWH > sWidthHeight)
            {
                //设置正交矩阵
                Matrix.orthoM(mProjectMatrix, 0, -1, 1, -1 / sWidthHeight * sWH, 1 / sWidthHeight * sWH, 3, 7);
            } else
            {
                //设置正交矩阵
                Matrix.orthoM(mProjectMatrix, 0, -1, 1, -sWH / sWidthHeight, sWH / sWidthHeight, 3, 7);
            }
        }
        //设置相机位置
        Matrix.setLookAtM(mViewMatrix, 0, 0, 0, 7.0f, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
        //计算变换矩阵
        Matrix.multiplyMM(mMVPMatrix, 0, mProjectMatrix, 0, mViewMatrix, 0);


    }

    @Override
    public void onDrawFrame(GL10 gl)
    {
        if (program == 0)
            return;

        //获取变换矩阵vMatrix成员句柄
        int vMatrix = GLES20.glGetUniformLocation(program, "vMatrix");
        //设置vMatrix的值
        GLES20.glUniformMatrix4fv(vMatrix, 1, false, mMVPMatrix, 0);
        //获取顶点着色器的vPosition成员句柄
        int vPosition = GLES20.glGetAttribLocation(program, "vPosition");
        //启用vPosition句柄
        GLES20.glEnableVertexAttribArray(vPosition);
        //传的圆筒面的所有坐标数据
        GLES20.glVertexAttribPointer(vPosition, 2, GLES20.GL_FLOAT, false, 2 * 4, vertexBuffer);

        //获取顶点着色器的vCoordinate成员句柄
        int vCoordinate = GLES20.glGetAttribLocation(program, "vCoordinate");
        //启用vPosition句柄
        GLES20.glEnableVertexAttribArray(vCoordinate);
        //传的纹理坐标数据
        GLES20.glVertexAttribPointer(vCoordinate, 2, GLES20.GL_FLOAT, false, 2 * 4, coordBuffer);

        //将已经创建并命名的纹理绑定到一个纹理目标(GL_TEXTURE_2D)上
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture[0]);

        //设置缩小过滤为使用纹理中坐标最接近的一个像素的颜色作为需要绘制的像素颜色
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
        //设置放大过滤为使用纹理中坐标最接近的若干个颜色,通过加权平均算法得到需要绘制的像素颜色
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
        //设置环绕方向S:指定横向模式为GL_CLAMP_TO_EDGE
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT);
        //设置环绕方向T:指定横向模式为GL_CLAMP_TO_EDGE
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT);
        //将图片数据传到上面创建的纹理中
        GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, mBitmap, 0);

        //绘制
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);

        //禁止顶点数组的句柄
        GLES20.glDisableVertexAttribArray(vPosition);

    }

    public int loadShader(int type, String shaderCode)
    {
        //创建空的着色器
        int shader = GLES20.glCreateShader(type);
        //将着色器程序加到着色器中
        GLES20.glShaderSource(shader, shaderCode);
        //编译色器程序
        GLES20.glCompileShader(shader);

        return shader;

    }
}

你可能感兴趣的:(Android OpenGL ES开发(七)纹理)