原文:https://github.com/SilverTiger/lwjgl3-tutorial/wiki/Textures
译注:并没有逐字逐句翻译一切,只翻译了自己觉得有用的部分。另外此翻译仅供参考,请一切以原文为准。代码例子文件链接什么的都请去原链接查找。括号里的内容一般也是译注,供理解参考用。总目录传送门:http://blog.csdn.net/zoharwolf/article/details/49472857
好久没更新了,因为看书补知识(以及玩游戏)去了。这篇翻译了一半的时候,发现官网教程全面更新了OTL。还好变动不算太大,这篇只是增加了新版LWJGL里更方便的一个API代替原来的大量手动工作,原来的手动工作被放在后面当作附篇了,看看也不错。至于之前翻译的几篇有没有更新,哪天我再去瞅瞅,变化要是不大我就懒得再改了……毕竟以后还是有可能再更新的,这些翻译也不可能一直保持跟他们同步更新。
译注2:翻译得我有点云里雾里,我自己亲自写的时候才发现,从这篇开始这教程的质量明显下降,乱七八糟胡写一气,代码都没有摘清楚,错误之处不少,还有许多是范例自己的封装代码毫无说明直接复制粘贴上来,简直不负责任。强烈建议结合范例代码看此教程,不然肯定晕。其实还是很简单的。
在最后的教程中,我们看看怎样用shader来渲染场景和怎样在更新之间做插值。还有,学下怎样用纹理。
既然已经知道怎样创建一个简单的shader程序了,我们就跳过初始化的部分。
生成纹理句柄并绑定的方法对现在的我们来讲应该不陌生了,就跟创建缓冲、顶点数组、shader和程序时那个相似。
int texture = glGenTextures();
glBindTexture(GL_TEXTURE_2D, handle);
现在我们有纹理句柄了,下步该开始设置纹理的wrapping和flitering的值。
但在那之前,应该注意opengl纹理跟纹理坐标映射关系的范围是从0.0到1.0的。这应该跟(x,y)坐标系统区分开来,所以叫它为(s,t)坐标系统,有时候你也能看到有人叫它(u,v)坐标,但是其实略有区别,不过大部分情况下就可以当作它们是一样的。
对OpenGL来说,我们就遵守其规格,用(s,t)坐标好了。
顺便一提,三维纹理坐标一般被称为(s,t,r)坐标系统。
举个例子,纹理坐标(0,0)是纹理的原点,在OpenGL里,这点是指左下角。具体规则如下:
当然你也可以用0.0到0.1区间之外的数值,根据你选择的wrapping模式,会自动处理。有四种:
用glTexParameteri(target, pname, parameter)来设置wrapping模式,如下
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
如果你用GL_TEXTURE_3D的话,也可以设置GL_TEXTURE_WRAP_R。
下一个要设置的参数就是纹理的filtering,当你把纹理绽放到与原尺寸不同的新尺寸时,此设置生效。有以下两种值:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
你也可以生成包含了一个既有图片又包含各种细节信息的mipmap。只需要调用glGenerateMipMap(target)即可。
glGenerateMipMat(GL_TEXTURE_2D);
用mipmap有四个不同的filtering参数:
设完纹理参数后,上传图片数据!
但是OpengGL里并没有直接读图片用的方法,我们可以用LWJGL里的STBImage。
只需几行代码,它支持的格式有JPEG、{NG、TGA、BMP、PSD、GIF、HDR、PIC和PNM。
读之前必须准备一些buffer来储存宽度、高度和图片成份(Image component,不知道怎么翻译好,暂时翻译成图片成分,就是图片一个象素中的RGBA值中的任意一个)
IntBuffer w = BufferUtils.createIntBuffer(1);
IntBuffer h = BufferUtils.createIntBuffer(1);
IntBuffer comp = BufferUtils.createIntBuffer(1);
如果你想要原点在左下角,而不是左上角,你可以调用stbi_set_flip_vertically_on_load(1),这样图片将以左下角为原点读取。
之后,你只要调用stbi_load(path, w, h, comp, req_comp),path是图片文件路径,w,h,comp用来存储之前说的宽度、高度和图片成份数目,用req_comp可以使每象素的的成份数为1、2、3、4,如果你设为0的话,那就用图片默认的成份数。如果读取失败,stbi_load 将返回null,但是你也可以通过调用stbi_failure_reason()来获取这个错误。
stbi_set_flip_vertically_on_load(1);
ByteBuffer image = stbi_load(path, w, h, comp, 4);
if (image == null) {
throw new RuntimeException("Failed to load a texture file!"+ System.lineSeparator() + stbi_failure_reason());
}
int width = w.get();
int height = h.get();
除此之外,你也可以用AWT来读取图片,具体请看结尾的附篇。
最后我们可以调用glTexImage2D(target, level, internalFormat, width, height, border, format, type, pixels)把图片数据存储在GPU里。target显而易见,level指细节等级,传统的OpenGL的mipmap一直都有用到,在现在OpenGL里,这值设成0. 纹理中的颜色成分的数目由internalFormat来指定,width和height是图片的宽和高,border值是0,format指定纹理格式,type指定象素数据类型,最后的pixels是包含纯象素数据的缓冲。
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, buffer);
接着只要把纹理放入shader就好了。
前边讲过怎样用shader了,现在来看看新的shader。
#version 150 core
in vec2 position;
in vec3 color;
in vec2 texcoord;
out vec3 vertexColor;
out vec2 textureCoord;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main() {
vertexColor = color;
textureCoord = texcoord;
mat4 mvp = projection * view * model;
gl_Position = mvp * vec4(position, 0.0, 1.0);
}
我们的顶点shader变化不大,最大的不同之处是现在有一个vec2类型的texcoord用来传递给片段shader。
OpenGL 2.1版本也相似,我们只需要加入属性vec2 texcoord即可。
#version 120
attribute vec2 position;
attribute vec3 color;
attribute vec2 texcoord;
varying vec3 vertexColor;
varying vec2 textureCoord;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main() {
vertexColor = color;
textureCoord = texcoord;
mat4 mvp = projection * view * model;
gl_Position = mvp * vec4(position, 0.0, 1.0);
}
目前还不算复杂,那么片段shader如何呢?这里我们使用采样器获取纹理颜色。
#version 150 core
in vec3 vertexColor;
in vec2 textureCoord;
out vec4 fragColor;
uniform sampler2D texImage;
void main() {
vec4 textureColor = texture(texImage, textureCoord);
fragColor = vec4(vertexColor, 1.0) * textureColor;
}
我们有一个新的uniform变量sampler2D,它用来指定片段应有的纹理颜色。
为了得到颜色,我们使用texture(sampler, texCoord)方法,然后我们可以将它与最后的输出颜色相乘。
在OpenGL2.1里,也几乎一样。
#version 120
varying vec3 vertexColor;
varying vec2 textureCoord;
uniform sampler2D texImage;
void main() {
vec4 textureColor = texture2D(texImage, textureCoord);
gl_FragColor = vec4(vertexColor, 1.0) * textureColor;
}
最大的不同这处就是我们用的是use texture2D(sampler, texCoord),因为在传统OpenGL里,我们不能用texture(sampler, texCoord)方法。
现在顶点shader里有一个新的值,指定顶点属性的代码也要微改如下:
/* Specify Vertex Pointer */
int posAttrib = program.getAttributeLocation("position");
program.enableVertexAttribute(posAttrib);
program.pointVertexAttribute(posAttrib, 2, 7 * Float.BYTES, 0);
/* Specify Color Pointer */
int colAttrib = program.getAttributeLocation("color");
program.enableVertexAttribute(colAttrib);
program.pointVertexAttribute(colAttrib, 3, 7 * Float.BYTES, 2 * Float.BYTES);
/* Specify Texture Pointer */
int texAttrib = program.getAttributeLocation("texcoord");
program.enableVertexAttribute(texAttrib);
program.pointVertexAttribute(texAttrib, 2, 7 * Float.BYTES, 5 * Float.BYTES);
当然我们还需要设置新的uniform值。但是因为只有一个纹理,所以这个是可选的,因为纹理的默认编号就是0.但是如果你有许多纹理的话,这步还是很重要的。
int uniTex = program.getUniformLocation("texImage");
program.setUniform(uniTex, 0);
接着创建一个材质块,在我们需要定义六个顶点,而不是四个,因为在现代OpenGL里只能渲染三角形。但是我们可以使缓冲对象元素(EBO,Element Buffer Objects),所以我们的VBO其实只含有四个顶点,EBO才用来指定正确的值。EBO的生成和VBO相似,但是绑定有所不同。
int ebo = glGenBuffers();
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
现在你可以建一个IntBuffer来存VBO的序号。
IntBuffer elements = BufferUtils.createIntBuffer(2 * 3);
elements.put(0).put(1).put(2);
elements.put(2).put(3).put(0);
elements.flip();
glBufferData(GL_ELEMENT_ARRAY_BUFFER, elements, GL_STATIC_DRAW);
我们的VBO有四个顶点,将材质显示在屏幕中央。
long window = GLFW.glfwGetCurrentContext();
IntBuffer widthBuffer = BufferUtils.createIntBuffer(1);
IntBuffer heightBuffer = BufferUtils.createIntBuffer(1);
GLFW.glfwGetFramebufferSize(window, widthBuffer, heightBuffer);
int width = widthBuffer.get();
int height = heightBuffer.get();
float x1 = (width - texture.getWidth()) / 2f;
float y1 = (height - texture.getHeight()) / 2f;
float x2 = x1 + texture.getWidth();
float y2 = y1 + texture.getHeight();
FloatBuffer vertices = BufferUtils.createFloatBuffer(4 * 7);
vertices.put(x1).put(y1).put(1f).put(1f).put(1f).put(0f).put(0f);
vertices.put(x2).put(y1).put(1f).put(1f).put(1f).put(1f).put(0f);
vertices.put(x2).put(y2).put(1f).put(1f).put(1f).put(1f).put(1f);
vertices.put(x1).put(y2).put(1f).put(1f).put(1f).put(0f).put(1f);
vertices.flip();
int vbo = glGenBuffers();
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, vertices, GL_STATIC_DRAW);
为了方便,缩放正交投影也要跟我们的窗口宽高相仿。
Matrix4f projection = Matrix4f.orthographic(0f, width, 0f, height, -1f, 1f);
int uniProjection = glGetUniformLocation(shaderProgram, "projection");
glUniformMatrix4fv(uniProjection, false, projection.getBuffer());
现在我们用EBO,不再用glDrawArrays(type, first, count)来渲染了,我们需要用glDrawElements(mode, count, type, offset)。mode是GL_TRIANGLES, count指定要画几个顶点,type指定EBO的序号类型,offset应该是清除。
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
到目前为止,你应该可以用纹理和颜色来弄一些好看的场景了。比如可以来一个用纹理渲染文字。
接下来讲怎样批量渲染,告别一成不变的场景。
如果你不想用STB读取纹理,你可以用AWT的BufferedImage和ImageIO。
读图片操作,可以用InputStream 或者File 。
你可以用ImageIO.getReaderFileSuffixes()来得到支持的图片格式,但是ImageIO只能用来读jpg, bmp, gif, png, jpeg 或 wbmp文件。其他的文件格式应该写你自己的读取器,或者搜一些读图片用的插件库。
InputStream in = new FileInputStream(path);
BufferedImage image = ImageIO.read(in);
读图片到BufferedImage之后,原点是左上角,所以我们得垂直翻转图片。
AffineTransform transform = AffineTransform.getScaleInstance(1f, -1f);
transform.translate(0, -image.getHeight());
AffineTransformOp operation = new AffineTransformOp(transform, AffineTransformOp.TYPE_NEAREST_NEIGHBOR);
image = operation.filter(image, null);
现在原点在左下角了,可以提取每个象素,用getRGB(startX, startY, w, h, rgbArray, offset, scansize),scansize应该是图片宽度,offset值应该是0.
int width = image.getWidth();
int height = image.getHeight();
int[] pixels = new int[width * height];
image.getRGB(0, 0, width, height, pixels, 0, width);
这方法取到的象素是ARGB格式的。用LWJGL3的话,必须以ByteBuffer的格式上传数据,所以需要转换。还要注意OpenGL的颜色是以RGBA格式表示的,它要求每个元素都要占一个字节,所以我们的ByteBuffer需要预留的空间是 width * height * 4。
我们还需要做些位运算,让我们看看代码:
ByteBuffer buffer = BufferUtils.createByteBuffer(width * height * 4);
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
/* Pixel as RGBA: 0xAARRGGBB */int pixel = pixels[y * width + x];
/* Red component 0xAARRGGBB >> (4 * 4) = 0x0000AARR */
buffer.put((byte) ((pixel >> 16) & 0xFF));
/* Green component 0xAARRGGBB >> (2 * 4) = 0x00AARRGG */
buffer.put((byte) ((pixel >> 8) & 0xFF));
/* Blue component 0xAARRGGBB >> 0 = 0xAARRGGBB */
buffer.put((byte) (pixel & 0xFF));
/* Alpha component 0xAARRGGBB >> (6 * 4) = 0x000000AA */
buffer.put((byte) ((pixel >> 24) & 0xFF));
}
}
/* Do not forget to flip the buffer! */
buffer.flip();
位运算看起来有点复杂,我们先看看红色成分的部分。
我们有了ARGB格式的象素,在十六进制里它表示为0xAARRGGBB,但是我们只想要0xRR这部分。我们知道十六进制数值是以四位表示的,我们试着把0xGGBB移走,它们占了4*4位,所以我们要右移16位才能得到0x000AARR。为了得到红色成分,还要做一次按位与操作,除掉0xFF,最后得到0x000000RR,也就是我们要的0xRR。
通过足够的位运算,最后得到了所有的颜色数据到我们的ByteBuffer里,但是记着,千万别忘了flip缓冲对象,否则JVM会崩。