这个系列教程 是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);
}
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。