【LWJGL官方教程】渲染

原文:https://github.com/SilverTiger/lwjgl3-tutorial/wiki/Rendering
译注:并没有逐字逐句翻译一切,只翻译了自己觉得有用的部分。另外此翻译仅供参考,请一切以原文为准。代码例子文件链接什么的都请去原链接查找。括号里的内容一般也是译注,供理解参考用。总目录传送门:http://blog.csdn.net/zoharwolf/article/details/49472857

这次教程终于要用OpenGL 3.2核心profile来做渲染了。
注意源代码也提供了OpenGL 2.1的一个版本。

Creating the context 创建上下文

在我们开始之前,先要告诉GLFW要用的是OpenGL 3.2核心profile的上下文,用以下代码就可以:

glfwDefaultWindowHints();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
long window = glfwCreateWindow(width, height, title, NULL, NULL);

我们先设置窗口的hint一律为默认,用glfwDefaultWindowHints()即可。这样做,防止之前已经用其他hint创建过其他窗口影响到我们这次的创建。
GLFW_CONTEXT_VERSION_MAJOR和GLFW_CONTEXT_VERSION_MINOR顾名思义,就是要告诉GLFW创建的是3.2版本的上下文。
在GLFW_OPENGL_PROFILE,我们指定要使用核心功能。如果你想用低于3.2版本的OpenGL,你需要指定的是GLFW_OPENGL_ANY_PROFILE,默认其实就是这个值。
GLFW_OPENGL_FORWARD_COMPAT这个hint指定OpenGL的上下文是否应该向前兼容,如果设定成TRUE,它会停用所有的废弃功能。如果是用OpenGL3.0版本以下,请无视这个hint。

如果你的图形卡不支持 OpenGL 3.2版本,创建的窗口会是NULL,如果那样的话,试着用以下代码创建一个传统的OpenGL 2.1上下文。

glfwDefaultWindowHints();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 1);
long window = glfwCreateWindow(width, height, title, NULL, NULL);

Vertex Array Objects 顶点数组对象

现在,我们有了窗口,窗口里也有我们要的上下文了,可以开始初始化渲染。
首先应该创建一个顶点数组对象,简称VAO(Vertex Array Object),它用来储存所有的链接,这些链接用来连结你的顶点缓冲对象和属性们。所以,记得要最先创建VAO然后绑定它。

int vao = glGenVertexArrays();
glBindVertexArray(vao);

你能在LWJGL3的GL30类里找到这两个方法。因为版本是3,所以如果你用的是OpenGL2.1上下文,就别用它们了。

Vertex Buffer Objects 顶点缓冲对象

在老式的固定功能管线里,你只能在每次glBegin(target)和glEnd()间的渲染调用里传递你的顶点(指老版本的OpenGL的机理),但是现代的方法却是把它们放在顶点缓冲对象VBO(Vertext Buffer Object)里。
VBO用来储存所有你GPU里的顶点数据。LWJGL里,你需要创建一个缓冲将顶点传到GPU。在我们的简单例子里,就用入门教程里的那个三角形。创建缓冲的话,就用LWJGL里的BufferUtil来创建一个合适的FloatBuffer。

注意,为了方便起见,顶点是逆时针排序的。

FloatBuffer vertices = BufferUtils.createFloatBuffer(3 * 6);
vertices.put(-0.6f).put(-0.4f).put(0f).put(1f).put(0f).put(0f);
vertices.put(0.6f).put(-0.4f).put(0f).put(0f).put(1f).put(0f);
vertices.put(0f).put(0.6f).put(0f).put(0f).put(0f).put(1f);
vertices.flip();

别忘了来一下vertices.flip()!这很重要,因为不这样的话你JVM会崩出一个EXCEPTION_ACCESS_VIOLATION。
创建缓冲之后,把它上传到GPU。但是在那之前,我们需要先创建并绑定一个VBO。

int vbo = glGenBuffers();
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, vertices, GL_STATIC_DRAW);

这样我们有了一个存在GPU上的VBO。需要注意我们用的是一个交错VBO,既有顶点数据又有色彩数据。我们马上就会看到怎样指定数据了。

其实也可以用两个VBO分别存顶点和色彩数据。

Shaders 着色器(一般就直接称之为shader了)

初始化的下一步是创建和编译shader。在OpenGL里,用的是GLSL(OpenGL Shading Language),跟C语言略像。
本教程我们就用两个简单的shader,一个顶点shader,一个片断shader,在每个shader程序里一般也都要有这两个shader。

Vertex Shaders 顶点shader

顶点shader用每个顶点和它的属性去计算最终的顶点位置(如果以后用镶嵌shader或几何shader的话,顶点shader可能仅仅是计算一个更新后的顶点位置)。它也传递片段shader需要的数据,比如色彩和纹理坐标。
然后,来看看我们的shader例子。

#version 150 core

in vec3 position;
in vec3 color;

out vec3 vertexColor;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main() {
    vertexColor = color;
    mat4 mvp = projection * view * model;
    gl_Position = mvp * vec4(position, 1.0);
}

第一行指定使用的GLSL版本。OpenGL 3.2使用GLSL版本是1.50,OpenGL 2.1用的版本是1.20。想快递查找所有的对应版本,看这篇教程的结尾处表格。
in关键字表示这些变量是从你的程序代码里传进来的,在这里是两个三元向量,保存位置和色彩用,具体值由每个顶点提供。
out变量将会传到下一个shader去,在这里也就是要传到片段shader,值也是由每个顶点提供。
最后,uniform变量是全局GLSL变量,也由程序代码传来,区别是,它们在每个顶点里的值相同。

最后看main方法,在顶点shader里,你实际上只需要设置gl_Position来决定最终的顶点位置。
在本例中,我们有out变量vertexColor,所以我们得告诉shader变量里面是什么。
我们还有model、view、projection矩阵用来计算MVP矩阵,注意你必须用反顺序计算它们。(MVP矩阵,在本教程后面会详细介绍)

最后我们用MVP矩阵乘以position,得到最终的顶点位置。

如果你用OpenGL 2.1,用以下shader代替上面的。

#version 120attribute vec3 position;
attribute vec3 color;

varying vec3 vertexColor;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main() {
    vertexColor = color;
    mat4 mvp = projection * view * model;
    gl_Position = mvp * vec4(position, 1.0);
}

这shader和其他顶点shader相似,只是关键字略有变化。你可以把attribute就当成in来看,它们都来自程序代码。varying就是out,传到下一个shader。

Fragment Shaders 片段shader

通过顶点shader,顶点们被转换为基本的象素点,这些象素被称为片段。片段shader会计算最后每个片段要输出的色彩。

#version 150 core

in vec3 vertexColor;

out vec4 fragColor;

void main() {
    fragColor = vec4(vertexColor, 1.0);
}

注意in变量和顶点shader中的out变量有同样的名字。
out变量用来储存片段的输出色彩。
main方法中,只需要传递转换后的顶点颜色到out变量里,然后out变量输出片段色彩。

如果你用OpenGL 2.1,用以下shader替代上面的。

#version 120
varying vec3 vertexColor;

void main() {
    gl_FragColor = vec4(vertexColor, 1.0);
}

你会看到其实跟其他片段shader几乎一样。
varying变量就是in变量,也跟之前的顶点shader里的varying变量名字相同。
传统OpenGL 2.1里不能用out变量,所以你必须用内置变量gl_FragColor来输出色彩。

Compiling Shaders 编译shader

现在我们了解这些shader怎样工作了,我们最后创建并编译它们。
和之前处理东西的流程相似,需要调用glCreateShader(type)来创建。之后设定shader源并编译它。shader源就是连在一起的GLSL代码,可以直接写成一个string,但是注意里面要有/n来换行,至少在声明版本的那行代码后必须要换行。

int vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, vertexSource);
glCompileShader(vertexShader);

int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, fragmentSource);
glCompileShader(fragmentShader);

最好要检查一下编译是否成功,用shader info log来看。

int status = glGetShaderi(shader, GL_COMPILE_STATUS);
if (status != GL_TRUE) {
    throw new RuntimeException(glGetShaderInfoLog(shader));
}

Shader Programs shader程序

编译后,还差最后一步就可以渲染了。必须把它和shader程序连接在一起。
现在猜猜要怎样处理shader程序,对,调用glCreateProgram()。然后把你的shader们挂在程序上连结在一起。

int shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glBindFragDataLocation(shaderProgram, 0, "fragColor");
glLinkProgram(shaderProgram);

你可能注意到了,glBindFragDataLocation(shaderProgram, 0, “fragColor”)这句之前没有解释过,这是因为这句可写可不写,因为我们在片段shader里只有一个out变量,所以默认它就会被绑定一个序号0。
这命令仍然只在GL30里才有效,所以用OpenGL2.1上下文的就别写了。
连结程序后,用程序的info log检查一下是否成功。

int status = glGetProgrami(shaderProgram, GL_LINK_STATUS);
if (status != GL_TRUE) {
    throw new RuntimeException(glGetProgramInfoLog(shaderProgram));
}

之后,可以使用shader程序了。

glUseProgram(shaderProgram);

Specify Vertex Attributes 指定顶点属性

在本教程一开始,我们把顶点和色彩数据放进了VBO,但是我们还没有告诉程序如何使用这些数据。可以用顶点属性来完成这项工作。设置一个顶点属性需要三步:首先取到属性位置,然后激活它,最后指向顶点属性。

int floatSize = 4;

int posAttrib = glGetAttribLocation(shaderProgram, "position");
glEnableVertexAttribArray(posAttrib);
glVertexAttribPointer(posAttrib, 3, GL_FLOAT, false, 6 * floatSize, 0);

int colAttrib = glGetAttribLocation(shaderProgram, "color");
glEnableVertexAttribArray(colAttrib);
glVertexAttribPointer(colAttrib, 3, GL_FLOAT, false, 6 * floatSize, 3 * floatSize);

刚开始的两个方法如其字面意思,之后的glVertexAttribPointer(location, size, type, normalized, stride, offset)需要解释一下。
location不言而喻,就是我们刚得到的属性位置;
size是我们需要告诉程序每个顶点的数据包含了多少值;
type指明属性用哪种数据类型,本例中是float类型;
normailized是你是否想要程序标准化这些值,不过一般这里都用false,更多关于标准化的详细内容可以参照这里
stride和offset,这个很重要,你需要传入的是字节数。stride指的是在相邻的两个顶点属性之间隔了几个字节,本例中每个顶点有六个float数值,所以stride值为 6*一个float占字节数= 6*4 = 24。offset则指定了第一个顶点属性的偏移量,在我们的VBO里,顶点位置数据是每个顶点六数值中一开始的那个数值,所以偏移量就是0。而三个位置数值之后才是颜色数据,所以颜色属性的依稀量是3*一个float占字节数= 3*4 = 12。

Set uniform variables 设置uniform变量

初始化完成了。最后就是设置uniform变量了。
本教程我们有三个矩阵作为uniform变量,但是你也可以传其他类型的数据。如果你不晓得怎样做向量和矩阵的数学计算,你应该看看这里这里,或者参考里提到的链接。(反正全是英文,不如去百度搜搜中文的,其实就是些线代的基本知识)
但是其实如果你不想的话,你并不需要自己去实现这些东西,因为在本教程里已经有非常基础的向量和矩阵类提供了,你可以在这里看看它们。
想要一个更完整的数学计算库,你应该看看Java OpenGL Math Library(JOML)。(亲自试用了过了,这个JOML里的矩阵格式和范例代码里的矩阵是转置关系,使用的时候居然不用flip……)
设置uniform变量和设置顶点属性相似,先拿到位置然后再设置它。

int uniModel = glGetUniformLocation(shaderProgram, "model");
Matrix4f model = new Matrix4f();
glUniformMatrix4fv(uniModel, false, model.getBuffer());

int uniView = glGetUniformLocation(shaderProgram, "view");
Matrix4f view = new Matrix4f();
glUniformMatrix4fv(uniView, false, view.getBuffer());

int uniProjection = glGetUniformLocation(shaderProgram, "projection");
float ratio = 640f / 480f;
Matrix4f projection = Matrix4f.orthographic(-ratio, ratio, -1f, 1f, -1f, 1f);
glUniformMatrix4fv(uniProjection, false, projection.getBuffer());

glUniformMatrix4fv(location, transpose, values)的参数很明显,location就是得到位置。transpose如果设定为true,矩阵在使用前会被转置。values设一个想要填充的缓冲。
为了计算一个正交矩阵,可以看glOrtho(left, right, bottom, top, near, far),里面描述了怎样计算它。(其实也可以百度一下glOrtho看看中文的介绍)
你自己写的时候需要注意,当你把值放入FloatBuffer中时,你应该清楚在OpenGL里,矩阵是列主序矩阵。(列主序矩阵可以参见这里:http://blog.csdn.net/zhihuier/article/details/9098179

现在是时候解释一下model、view、projection矩阵了。(即MVP矩阵,模型视图投影矩阵)
模型(model)矩阵会将物体的本地坐标计算到世界坐标上。在OpenGL中,你使用它来缩放、平移、旋转物体。(本地→世界)
视图(view)矩阵将世界坐标计算为视点坐标,视点坐标用于决定摄像机位置。但是实际上你并没有移动摄像机,你移动的是世界,所以为了得到正确的视图矩阵,你必须以摄像机平移相反的方向平移世界。(想象一下电脑屏幕是摄像机,当你想移动摄像机移动画面的时候其实是整个世界在向反方向动,你的电脑并没有动)(世界→摄像机)
投影(projection)矩阵将视点坐标计算成修剪坐标,你用这矩阵来实现一个正交或透视的矩阵。(摄像机→修剪)
上面提过了,计算MVP的时候必须反着乘三个矩阵。这是因为最后其实是:新矩阵 = 投影矩阵 * 视图矩阵 * 模型矩阵 * 向量,所以实际上它的计算顺序是:新矩阵 = 投影矩阵 * (视图矩阵 * (模型矩阵 * 向量)) (译注:矩阵乘法满足结合律,所以这句话的意思其实是说,本地→世界的转换矩阵M肯定要和向量相乘才能正确生效,故MVP不能正着乘,不然就是M*V*P*向量,P和向量相乘了,计算结果肯定不对。至于为何不是“向量*M*V*P”这样的顺序,这是因为前边提过,openGL里用的是列主序矩阵,向量用的也都是列向量,所以应该倒过来乘才能正确计算。如果是在DirectX里,用的是行主序矩阵和行向量,那就是正着乘了。)

Rendering 渲染

最后我们渲染我们的三角形,只需要调用glDrawArrays(type, first, count)。别忘了在那之前清一下屏。

glClear(GL_COLOR_BUFFER_BIT);
glDrawArrays(GL_TRIANGLES, 0, 3);

第一个方法就是清除屏幕缓冲里的色彩。如果不这样做会得到很诡异的效果。
glDrawArrays(type, first, count)会描画目前绑定的VBO。type值大部分情况下都是GL_TRIANGLES,你也可以用一些其他的三角形类型比如GL_TRIANGLE_STRIP,或者其他类型比如GL_POINTS。既然我们要画出所有顶点,start那就设成0,有三个顶点要画,所以count是3。

Interpolating with alpha value 使用alpha插值

但是一个不动的三角形非常无聊,所以我们让它像入门范例里那样转起来!还得在之前的游戏循环教程里的update(float delta)和render(float alpha)吗?现在就是用到它们的时候了。
要转动三角形,我们需要设置我们的模型矩阵为一个旋转矩阵,源代码中已经提供了方法,所以只要自己实现它就可以了,参见glRotate(angle, x, y, z).
除了旋转矩阵我们还需要当前的角度和每秒要转的角度。在范例代码中,三角形每秒旋转50度。

private int uniModel;
private float angle = 0f;
private float anglePerSecond = 50f;

在我们的update方法里,我们用delta值计算当前循环的角度,在render方法里,我们只需将当前的角度用到模型矩阵上。

public void update(float delta) {
    angle += delta * anglePerSecond;
}

public void render(float alpha) {
    glClear(GL_COLOR_BUFFER_BIT);

    Matrix4f model = Matrix4f.rotate(angle, 0f, 0f, 1f);
    glUniformMatrix4fv(uniModel, false, model.getBuffer());

    glDrawArrays(GL_TRIANGLES, 0, 3);
}

对可变时步来讲,这样就已经OK了。再看看固定时步会怎样,假设UPS设定为5,这个循环会变得结结巴巴的。
怎么办?这就需要用的alpha插值了。
为了用到它,我们还需要另一个变量,上次循环时的角度。

private int uniModel;
private float previousAngle = 0f;
private float angle = 0f;
private float anglePerSecond = 50f;

这样我们就可以插入本次角度和上次角度的中间状态。你仔细想想就会意识到,你的屏幕将会延迟一帧,但是这没什么问题。所有的专业的游戏都是这样做的,使用固定时步,你不会注意到的。
需要在update方法里设置之前的角度。

public void update(float delta) {
    previousAngle = angle;
    angle += delta * anglePerSecond;
}

插值将会在render方法中生效,并应用在旋转矩阵之上。

public void render(float alpha) {
    glClear(GL_COLOR_BUFFER_BIT);

    float lerpAngle = (1f - alpha) * previousAngle + alpha * angle;
    Matrix4f model = Matrix4f.rotate(lerpAngle, 0f, 0f, 1f);
    glUniformMatrix4fv(uniModel, false, model.getBuffer());

    glDrawArrays(GL_TRIANGLES, 0, 3);
}

通过两种状态之前的插值,无论是5UPS还是60UPS都没问题,它看起来将始终是一个平滑的变换。

Cleaning up 清除

你的程序结束时,好习惯是要将图形数据清除掉。

glDeleteVertexArrays(vao);
glDeleteBuffers(vbo);
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
glDeleteProgram(shaderProgram);

下一篇,讲怎样使用材质。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值