OpenGL & GLSL 教程(二)

这个系列教程 是https://github.com/mattdesl/lwjgl-basics/wiki的系列翻译。

这篇教程我们来介绍纹理(texture)。我们最后会完成一个可以在我们写的简单引擎利用的纹理类。如果你用的是libgdx。你可以阅读这篇文章

前提知识:图像
一张图,如大家所知 ,是一个在二维空间里的颜色队列。让我们以两张非常小的图片为例;一张心形,一张半心形:

现在让我们用photoshop或者其他看图软件去放大它,我们可以清楚的看见,这个图片是由像素点构成的:

图片在电脑中有很多存储方式。最常见的就是以RGBA 每个通道8比特的方式存储。RGB分别代表红,绿,蓝三个染色通道,A代表透明通道。
下面是以三种不同的方式存储红色的例子:

Hex aka RGB int: #ff0000 or 0xff0000
RGBA byte: (R=255, G=0, B=0, A=255)
RGBA float: (R=1f, G=0f, B=0f, A=1f)

用RGBA字节的形式表示上面图片(32x16像素)的话会像下面的代码:

new byte[ imageWidth * imageHeight * 4 ] {
    0x00, 0x00, 0x00, 0x00, //Pixel index 0, position (x=0, y=0), transparent black
    0xFF, 0x00, 0x00, 0xFF, //Pixel index 1, position (x=1, y=0), opaque red
    0xFF, 0x00, 0x00, 0xFF, //Pixel index 2, position (x=2, y=0), opaque red
    ... etc ...
}

我们看到一个像素由四个字节组成。记住这是一个一维队列。队列的长度是WIDTH * HEIGHT * BPP,BPP 是每个像素存储的字节数量,这里的值是4。因为这个数据队列会非常的大,所以我们经常会用PNG或者JPEG去存储图片(进行了压缩)。

OpenGL 纹理
在opengl当中我们用纹理(texture)去存储图片的数据。纹理不只是存储图片的数据,它以浮点数队列的形式存储在GPU中,用来实现阴影映射(Shadow Mapping)和其他技术。
下面是从图片到纹理最基本的步骤:

1.解码成RGBA字节
2.获得一个新的纹理ID
3.绑定纹理
4.设置一些纹理参数
5.上传RGBA字节到OpenGL

从PNG转到RGBA字节
OpenGL读不懂png jpg 等图片格式。它只知道字节和浮动数,所以我们需要把png图片转换成字节数据放到缓存中。
这里我们用到一个解码png的第三方库PNGDecoder
下面是转换的代码:

//get an InputStream from our URL
input = pngURL.openStream();

//initialize the decoder
PNGDecoder dec = new PNGDecoder(input);

//read image dimensions from PNG header
width = dec.getWidth();
height = dec.getHeight();

//we will decode to RGBA format, i.e. 4 components or "bytes per pixel"
final int bpp = 4;

//create a new byte buffer which will hold our pixel data
ByteBuffer buf = BufferUtils.createByteBuffer(bpp * width * height);

//decode the image into the byte buffer, in RGBA format
dec.decode(buf, width * bpp, PNGDecoder.Format.RGBA);

//flip the buffer into "read mode" for OpenGL
buf.flip();

创建纹理(Texture)
OpenGL可以利用多纹理部件绑定多个纹理。但是我们这里只利用一个纹理部件绑定一个纹理。为了改变纹理的参数或者为了把字节传到OpenGL,首先我们需要绑定纹理(激活)。我们用glGenTextures 获得一个纹理唯一id。
创建纹理和上传RGBA数据的过程如下:

//Generally a good idea to enable texturing first
glEnable(GL_TEXTURE_2D);

//generate a texture handle or unique ID for this texture
id = glGenTextures();

//bind the texture
glBindTexture(GL_TEXTURE_2D, id);

//use an alignment of 1 to be safe
//this tells OpenGL how to unpack the RGBA bytes we will specify
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

//set up our texture parameters
glTexParameteri(...);

//upload our ByteBuffer to GL
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, buf);

去调用glTexImage2D 的目的是在OpenGL中建立一个实际的纹理。当我们想改变图片的长和宽或者想改变RGBA数据的时候,我们可以再次调用它。如果我们只是想改变RGBA数据的一部分我们可以调用glTexSubImage2D。对于每个像素点的更改我们可以利用片元着色器(fragment shaders),以后我们会讲到它。

纹理参数(Texture Parameters)
在调用glTexImage2D之前,我们要把纹理参数设置正确。
代码如下:

//Setup filtering, i.e. how OpenGL will interpolate the pixels when scaling up or down
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

//Setup wrap mode, i.e. how OpenGL will handle pixels outside of the expected range
//Note: GL_CLAMP_TO_EDGE is part of GL12
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

过滤器(Filtering)
扩大/缩小过滤器决定了图片缩放时的策略。对于像素级游戏利用GL_NEAREST 比较合适,边缘不会模糊化。
GL_LINEAR利用双线性模糊化可以得到一个比较平滑的结果。3D游戏中比较常见:

延伸模式(Wrap Modes)
为了解释的更加清楚,我们需要了解一下纹理的坐标和顶点。我们用下面这个2维图片做例子:

为了渲染上面这个对象我们需要给定OpenGL 4个定点。如我们所看到的,我们会得到一个2d四边形。每个定点有一定数量的属性包括位置坐标Position (x, y) 和纹理坐标Coordinates (s, t)。纹理坐标被定义在正切空间中,通常在0.0和1.0之间。这告诉OpenGL怎么去从纹理中取样。下图显示了每个定点的属性:

注意:图中的坐标系是左上的。libgdx是左下的。
有些编程和建模人员用UV而不是ST代表纹理坐标。这只是另一种标识方法而已。
那么当我们的纹理坐标值大于1.0或者小于0.0会发生什么呢。这就是Wrap Modes该出场的时候了。它将告诉OpenGl怎样去处理纹理坐标以外的数据。通常有两种模式,GL_CLAMP_TO_EDGE 简单的在纹理的边缘颜色取样,GL_REPEAT重复纹理。以下我们是使用2.0的结果:

Debug Rendering
在进入可编程管线和批量渲染系统之前,我们可以测试渲染。这些函数(glMatrixMode, glBegin, glColor4f, glVertex2f, etc)都是过时的。我们只是为了看的更清楚,在测试函数中调用他们,正式代码中我们不应该调用:

public static void debugTexture(Texture tex, float x, float y, float width, float height) {
    //usually glOrtho would not be included in our game loop
    //however, since it's deprecated, let's keep it inside of this debug function which we will remove later
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(0, Display.getWidth(), Display.getHeight(), 0, 1, -1);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    glEnable(GL_TEXTURE_2D); //likely redundant; will be removed upon migration to "modern GL"

    //bind the texture before rendering it
    tex.bind();

    //setup our texture coordinates
    //(u,v) is another common way of writing (s,t)
    float u = 0f;
    float v = 0f;
    float u2 = 1f;
    float v2 = 1f;

    //immediate mode is deprecated -- we are only using it for quick debugging
    glColor4f(1f, 1f, 1f, 1f);
    glBegin(GL_QUADS);
        glTexCoord2f(u, v);
        glVertex2f(x, y);
        glTexCoord2f(u, v2);
        glVertex2f(x, y + height);
        glTexCoord2f(u2, v2);
        glVertex2f(x + width, y + height);
        glTexCoord2f(u2, v);
        glVertex2f(x + width, y);
    glEnd();
}

纹理集Texture Atlases
有一点我还没提到的是纹理集的重要性。如果我们每次只绑定一个纹理,那么我们想在每一贞中渲染很多图片会很费性能。所以最好的方法是把多个图片整合到一个大的图片里。这样我们就可以在每贞里绑定最少个纹理。
下面就是一个纹理集的例子:

你可能从延伸章节注意到了,我们可以通过告诉OpenGL纹理坐标的方式去确定渲染纹理的那个部分。举例,如果我们想渲染坐标为(1,1)的小草贴图我们可以这样写:

float srcX = 64;
float srcY = 64;
float srcWidth = 64;
float srcHeight = 64;

float u = srcX / tex.width;
float v = srcY / tex.height;
float u2 = (srcX + srcWidth) / tex.width;
float v2 = (srcY + srcHeight) / tex.height;

硬件限制(Hardware Limitations)
你可以通过下面的代码获取设备可以支持的纹理最大的长和宽:

int maxSize = glGetInteger(GL_MAX_TEXTURE_SIZE);

通常来说现在最先进的电脑允许的长度是4096x4096,如果你想更安全点那么你可以限制到2048x2048,如果你想在老的设备运行(or Android, iOS, WebGL),你可以限制到 1024x1024.

2的次幂
有一点我忘记了说,那就是2的次幂。以前OpenGL只允许纹理的长度为2的次幂:
1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096... etc
现在大部分设备已经支持不是2的次幂长度的纹理了。你可以用下面的代码测试是否支持非2次幂的的纹理长度:

boolean npotSupported = GLContext.getCapabilities().GL_ARB_texture_non_power_of_two;

值得注意的是尽管设备支持了非2次幂的纹理。但是2次幂的纹理的表现和存储效率都会更加的好。

代码
下面是纹理(texture)的所有代码:

package mdesl.graphics;

import static org.lwjgl.opengl.GL11.GL_CLAMP;
import static org.lwjgl.opengl.GL11.GL_LINEAR;
import static org.lwjgl.opengl.GL11.GL_NEAREST;
import static org.lwjgl.opengl.GL11.GL_REPEAT;
import static org.lwjgl.opengl.GL11.GL_RGBA;
import static org.lwjgl.opengl.GL11.GL_TEXTURE_2D;
import static org.lwjgl.opengl.GL11.GL_TEXTURE_MAG_FILTER;
import static org.lwjgl.opengl.GL11.GL_TEXTURE_MIN_FILTER;
import static org.lwjgl.opengl.GL11.GL_TEXTURE_WRAP_S;
import static org.lwjgl.opengl.GL11.GL_TEXTURE_WRAP_T;
import static org.lwjgl.opengl.GL11.GL_UNPACK_ALIGNMENT;
import static org.lwjgl.opengl.GL11.GL_UNSIGNED_BYTE;
import static org.lwjgl.opengl.GL11.glBindTexture;
import static org.lwjgl.opengl.GL11.glEnable;
import static org.lwjgl.opengl.GL11.glGenTextures;
import static org.lwjgl.opengl.GL11.glPixelStorei;
import static org.lwjgl.opengl.GL11.glTexImage2D;
import static org.lwjgl.opengl.GL11.glTexParameteri;
import static org.lwjgl.opengl.GL12.GL_CLAMP_TO_EDGE;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.ByteBuffer;

import org.lwjgl.BufferUtils;

import de.matthiasmann.twl.utils.PNGDecoder;

public class Texture {

    public final int target = GL_TEXTURE_2D;
    public final int id;
    public final int width;
    public final int height;

    public static final int LINEAR = GL_LINEAR;
    public static final int NEAREST = GL_NEAREST;

    public static final int CLAMP = GL_CLAMP;
    public static final int CLAMP_TO_EDGE = GL_CLAMP_TO_EDGE;
    public static final int REPEAT = GL_REPEAT;

    public Texture(URL pngRef) throws IOException {
        this(pngRef, GL_NEAREST);
    }

    public Texture(URL pngRef, int filter) throws IOException {
        this(pngRef, filter, GL_CLAMP_TO_EDGE);
    }

    public Texture(URL pngRef, int filter, int wrap) throws IOException {
        InputStream input = null;
        try {
            //get an InputStream from our URL
            input = pngRef.openStream();

            //initialize the decoder
            PNGDecoder dec = new PNGDecoder(input);

            //set up image dimensions 
            width = dec.getWidth();
            height = dec.getHeight();

            //we are using RGBA, i.e. 4 components or "bytes per pixel"
            final int bpp = 4;

            //create a new byte buffer which will hold our pixel data
            ByteBuffer buf = BufferUtils.createByteBuffer(bpp * width * height);

            //decode the image into the byte buffer, in RGBA format
            dec.decode(buf, width * bpp, PNGDecoder.Format.RGBA);

            //flip the buffer into "read mode" for OpenGL
            buf.flip();

            //enable textures and generate an ID
            glEnable(target);
            id = glGenTextures();

            //bind texture
            bind();

            //setup unpack mode
            glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

            //setup parameters
            glTexParameteri(target, GL_TEXTURE_MIN_FILTER, filter);
            glTexParameteri(target, GL_TEXTURE_MAG_FILTER, filter);
            glTexParameteri(target, GL_TEXTURE_WRAP_S, wrap);
            glTexParameteri(target, GL_TEXTURE_WRAP_T, wrap);

            //pass RGBA data to OpenGL
            glTexImage2D(target, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, buf);
        } finally {
            if (input != null) {
                try { input.close(); } catch (IOException e) { }
            }
        }
    }

    public void bind() {
        glBindTexture(target, id);
    }
}
相关文章
相关标签/搜索