当前位置: 首页 > 工具软件 > LWJGL > 使用案例 >

【LWJGL官方教程】纹理

陶博耘
2023-12-01

原文:https://github.com/SilverTiger/lwjgl3-tutorial/wiki/Textures
译注:并没有逐字逐句翻译一切,只翻译了自己觉得有用的部分。另外此翻译仅供参考,请一切以原文为准。代码例子文件链接什么的都请去原链接查找。括号里的内容一般也是译注,供理解参考用。总目录传送门:http://blog.csdn.net/zoharwolf/article/details/49472857
好久没更新了,因为看书补知识(以及玩游戏)去了。这篇翻译了一半的时候,发现官网教程全面更新了OTL。还好变动不算太大,这篇只是增加了新版LWJGL里更方便的一个API代替原来的大量手动工作,原来的手动工作被放在后面当作附篇了,看看也不错。至于之前翻译的几篇有没有更新,哪天我再去瞅瞅,变化要是不大我就懒得再改了……毕竟以后还是有可能再更新的,这些翻译也不可能一直保持跟他们同步更新。
译注2:翻译得我有点云里雾里,我自己亲自写的时候才发现,从这篇开始这教程的质量明显下降,乱七八糟胡写一气,代码都没有摘清楚,错误之处不少,还有许多是范例自己的封装代码毫无说明直接复制粘贴上来,简直不负责任。强烈建议结合范例代码看此教程,不然肯定晕。其实还是很简单的。

在最后的教程中,我们看看怎样用shader来渲染场景和怎样在更新之间做插值。还有,学下怎样用纹理。

Creating a texture 创建纹理

既然已经知道怎样创建一个简单的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) 左下角
  • (1,0) 右下角
  • (1,1) 右上角
  • (0,1) 左上角

Wrapping

当然你也可以用0.0到0.1区间之外的数值,根据你选择的wrapping模式,会自动处理。有四种:

  • GL_REPEAT 简单地重复纹理。
  • GL_MIRRORED_REPEAT 也重复纹理,但是超出的部分是镜象的形式。
  • GL_CLAMP_TO_EDGE 限定坐标在0.0在1.0之间
  • GL_CLAMP_TO_BORDER 提供0.0到1.0区间外的坐标一个特殊的边界颜色。

用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

下一个要设置的参数就是纹理的filtering,当你把纹理绽放到与原尺寸不同的新尺寸时,此设置生效。有以下两种值:

  • GL_NEAREST 选择跟所选的纹理坐标相近的值
  • GL_LINEAR 计算四周象素平均权值
    跟使用的wrapping模式差不多,用glTexParameteri(target, pname, parameter)方法:
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参数:

  • GL_NEAREST_MIPMAP_NEAREST 匹配象素和样本的尺寸,使用最邻插值取样
  • GL_LINEAR_MIPMAP_NEAREST 用双线性插值取样
  • GL_NEAREST_MIPMAP_LINEAR 取两个mipmap,匹配象素和样本的尺寸,使用最邻插值取样
  • GL_LINEAR_MIPMAP_LINEAR 用三线插值取样
    你可以像设置GL_NEAREST和GL_LINEAR一样设置它们。

Uploading picture data 上传图片数据

设完纹理参数后,上传图片数据!
但是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就好了。

Using a texture in shaders 在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);

到目前为止,你应该可以用纹理和颜色来弄一些好看的场景了。比如可以来一个用纹理渲染文字

接下来讲怎样批量渲染,告别一成不变的场景。

附篇: Loading Textures with AWT 用AWT读取纹理

如果你不想用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会崩。

 类似资料: