在尝试此部分之前,请阅读Hello Triangle的示例。它描述了可编程管道的重要方面,包括着色器和片段。我不会解释这些细节。相反,我将主要关注JOGL的差异,因为一些JOGL方法有不同的参数,当然,Java没有指针,这是他的一些C示例所依赖的。(JOGL文档在这些情况下很有用。)
在现代OpenGL中,绘制三角形比你想象的更详细,因为作为可编程管道的一部分,在设置缓冲区和着色器方面需要做很多工作。顶点和片段着色器以及三角形数据可以以文本文件形式提供,并加载到CPU上的主程序中,也可以硬编码到程序中。然后,着色器必须传输到GPU,在那里它们将执行。三角形的数据还必须传输到GPU,并存储在一个或多个缓冲区中。然后,基于CPU的显示循环可以启动并使用GPU上的着色器程序来渲染存储在缓冲区中的数据。
整个程序很长,包含很多需要解释的细节,所以我将其分成了一系列部分。
第一部分是“场景”,我指的是初始化场景和渲染场景的方法——这里将给出render()方法。第二部分“数据”是三角形数据的声明位置。第三部分“缓冲区”是在GPU上设置缓冲区并填充数据的地方。第四部分“着色器”用于在GPU上设置顶点和片段着色器。使用这些不同标记的部分的原因之一是,它暗示了更复杂的程序的结构,其中一些部分将成为单独的类,例如数据部分。现在,我们将把所有内容都放在一个类中,这样就更容易看到“整体”发生了什么。
import java.nio.*;
import com.jogamp.common.nio.*;
import com.jogamp.opengl.*;
import com.jogamp.opengl.util.*;
import com.jogamp.opengl.util.awt.*;
import com.jogamp.opengl.util.glsl.*;
public class A02_GLEventListener implements GLEventListener {
public A02_GLEventListener() {
}
public void init(GLAutoDrawable drawable) {
GL3 gl = drawable.getGL().getGL3();
gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
gl.glClearDepth(1.0f);
gl.glEnable(GL.GL_DEPTH_TEST);
gl.glDepthFunc(GL.GL_LESS);
initialise(gl);
}
public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) {
}
public void display(GLAutoDrawable drawable) {
GL3 gl = drawable.getGL().getGL3();
render(gl);
}
public void dispose(GLAutoDrawable drawable) {
GL3 gl = drawable.getGL().getGL3();
gl.glDeleteBuffers(1, vertexBufferId, 0);
}
// ***************************************************
/* THE SCENE */
// ***************************************************
/* THE DATA */
// ***************************************************
/* THE BUFFERS */
// ***************************************************
/* THE SHADER */
}
让我们从数据部分开始。程序清单2.6给出了单个三角形的顶点数据,定义为九个浮点数的列表,这些浮点数是三个顶点中每个顶点的(x,y,z)坐标。在这个阶段,我们的三角形没有深度(进入屏幕),因此所有z值都是0。默认屏幕坐标在(-1,-1)到(1,1)的范围内。x和y坐标使用这个范围-我们实际上指定了世界坐标,这些坐标被转换为屏幕坐标。然而,在这个阶段,我们还没有建立一个查看管道,所以世界坐标中的值基本上变成了屏幕坐标中的数值。在后面的示例中,世界坐标可以取任何值。此外,将为顶点存储其他数据(例如“法线”),并使用单独的数据结构来描述如何将顶点数据连接在一起以形成三角形网格。我们还将更详细地查看查看管道以及世界坐标和屏幕坐标之间的差异。
顶点按逆时针顺序列出。它们可以很容易地按顺时针顺序列出。在后面的章节中,当我们查看3D对象时,更详细地考虑这一点非常重要,因为我们使用它来确定当三角形形成3D形状时,三角形的哪个面是外部面。
/* THE DATA
*/
// one triangle
private float[] vertices = {
-0.5f, -0.5f, 0.0f, // Bottom Left
0.5f, -0.5f, 0.0f, // Bottom Right
0.0f, 0.5f, 0.0f // Top middle
};
在GPU上设置缓冲区。我将重点介绍与乔伊·德·弗里斯的例子的不同之处。glGenVertexArrays()和glGenBuffers()的C版本都返回GPU内存中的地址。JOGL等价物通过使用数组并将地址存储在数组位置来处理此问题。方法中的第一个参数说明需要多少缓冲区,这就是为什么使用一个数组,以便在必要时返回多个地址(如果需要返回不止一个地址,则需要更新数组声明)。顶点数组对象只需要一个缓冲区,顶点缓冲区对象只需一个缓冲,因此每种情况下的第一个参数都是1。变量vertexBufferId和vertexArrayId声明为类的属性,因为类中的其他方法可能需要它们。glBufferData()的JOGL版本与其C表亲不同。JOGL版本中的第三个参数是java.nio.FloatBuffer。顶点(x,y,z)数据在传递给glBufferData()之前,必须传输到这种类型的变量中。方法Buffers。newDirectFloatBuffer()用于执行此操作。从Joey de Vries的例子中可以清楚地看到fillBuffers()方法的其余部分。还有一点需要注意的是,gl.glVertexAttribPointer()包含一些文字值(即“3”)。当我们传输到GPU的数据变得更加复杂时,我们将在后面的示例中对此进行更新。
/* THE BUFFERS
*/
private int[] vertexBufferId = new int[1];
private int[] vertexArrayId = new int[1];
private void fillBuffers(GL3 gl) {
gl.glGenVertexArrays(1, vertexArrayId, 0);
gl.glBindVertexArray(vertexArrayId[0]);
gl.glGenBuffers(1, vertexBufferId, 0);
gl.glBindBuffer(GL.GL_ARRAY_BUFFER, vertexBufferId[0]);
FloatBuffer fb = Buffers.newDirectFloatBuffer(vertices);
gl.glBufferData(GL.GL_ARRAY_BUFFER, Float.BYTES * vertices.length,
fb, GL.GL_STATIC_DRAW);
gl.glVertexAttribPointer(0, 3, GL.GL_FLOAT, false, 3*Float.BYTES, 0);
gl.glEnableVertexAttribArray(0);
gl.glBindBuffer(GL.GL_ARRAY_BUFFER, 0);
gl.glBindVertexArray(0);
}
在GPU上设置和编译着色器。在这个阶段,着色器的代码在程序中以两个字符串的形式给出-我已经将着色器的单独行放在显示器的单独行上,以使其更容易理解,尽管提供着色器仍然是一种混乱的方式。方法initialiseShader()只显示字符串-在以后的程序中,我们将使用它从文本文件中读取着色器。变量shaderProgram被声明为类的属性,以便可以在类的其他方法中引用它。在后面的示例中,我们将从文件中加载顶点和片段着色器文本。我们还将使用多个着色器,因此我们将开发一个类来处理着色器。现在,我们将视为“样板”代码,并在接下来的几个示例中使用它。(需要注意的一点是,颜色被硬编码到片段着色器中。这显然不是很灵活,我们将在后面的示例中看到如何改进。)
/* THE SHADER
*/
private String vertexShaderSource =
"#version 330 core\n" +
"\n" +
"layout (location = 0) in vec3 position;\n" +
"\n" +
"void main() {\n" +
" gl_Position = vec4(position.x, position.y, position.z, 1.0f);\n" +
"}";
private String fragmentShaderSource =
"#version 330 core\n" +
"\n" +
"out vec4 fragColor;\n" +
"\n" +
"void main() {\n" +
" fragColor = vec4(0.1f, 0.7f, 0.9f, 1.0f);\n" +
"}";
private int shaderProgram;
private void initialiseShader(GL3 gl) {
System.out.println(vertexShaderSource);
System.out.println(fragmentShaderSource);
}
private int compileAndLink(GL3 gl) {
String[][] sources = new String[1][1];
sources[0] = new String[]{ vertexShaderSource };
ShaderCode vertexShaderCode = new ShaderCode(GL3.GL_VERTEX_SHADER,
sources.length, sources);
boolean compiled = vertexShaderCode.compile(gl, System.err);
if (!compiled)
System.err.println("[error] Unable to compile vertex shader: " + sources);
sources[0] = new String[]{ fragmentShaderSource };
ShaderCode fragmentShaderCode = new ShaderCode(GL3.GL_FRAGMENT_SHADER,
sources.length, sources);
compiled = fragmentShaderCode.compile(gl, System.err);
if (!compiled)
System.err.println("[error] Unable to compile fragment shader: " + sources);
ShaderProgram program = new ShaderProgram();
program.init(gl);
program.add(vertexShaderCode);
program.add(fragmentShaderCode);
program.link(gl, System.out);
if (!program.validateProgram(gl, System.out))
System.err.println("[error] Unable to link program");
return program.program();
}
compileAndLink()方法可以通过多种方式实现。
private int alternative_compileAndLink(GL3 gl) {
int vertexShader = gl.glCreateShader(GL3.GL_VERTEX_SHADER);
String[] source = new String[]{ vertexShaderSource };
int[] sLength = new int[1];
sLength[0] = source[0].length();
gl.glShaderSource(vertexShader, 1, source, sLength, 0);
gl.glCompileShader(vertexShader);
if (shaderError(gl, vertexShader, "Vertex shader"))
System.exit(0);
int fragmentShader = gl.glCreateShader(GL3.GL_FRAGMENT_SHADER);
source = new String[]{fragmentShaderSource};
sLength[0] = source[0].length();
gl.glShaderSource(fragmentShader, 1, source, sLength, 0);
gl.glCompileShader(fragmentShader);
if (shaderError(gl, fragmentShader, "Fragment shader"))
System.exit(0);
int shaderProgram = gl.glCreateProgram();
gl.glAttachShader(shaderProgram, vertexShader);
gl.glAttachShader(shaderProgram, fragmentShader);
gl.glLinkProgram(shaderProgram);
gl.glValidateProgram(shaderProgram);
gl.glDetachShader(shaderProgram,vertexShader);
gl.glDetachShader(shaderProgram,fragmentShader);
return shaderProgram;
}
// ***************************************************
/* ERROR CHECKING for shader compiling and linking.
*/
private boolean shaderError(GL3 gl, int obj, String s) {
int[] params = new int[1];
gl.glGetShaderiv(obj, GL3.GL_COMPILE_STATUS, params, 0);
boolean error = (params[0] == GL.GL_FALSE);
if (error) {
gl.glGetShaderiv(obj, GL3.GL_INFO_LOG_LENGTH, params, 0);
int logLen = params[0];
byte[] bytes = new byte[logLen + 1];
gl.glGetShaderInfoLog(obj, logLen, null, 0, bytes, 0);
String logMessage = new String(bytes);
System.out.println("\n***ERROR***");
System.out.println(s + ": "+logMessage);
}
return error;
}
程序给出了初始化和渲染“场景”的方法,到目前为止,它是单个三角形。第一个方法initialse()使用上述方法设置着色器和缓冲区。第二种方法render()首先清除屏幕和深度缓冲区。然后设置要使用的特定着色器程序。现在我们可以看到为什么有一个名为shaderProgram的属性了。可以使用不同的顶点和片段着色器创建多个着色器程序,并且可以使用不同着色器渲染场景的不同部分。然后选择GPU上的相关顶点数据。同样,我们可以在GPU上存储多个顶点数据列表,并选择要渲染的顶点。
发出命令glDrawArrays(),以便GPU使用选定的着色器程序(shaderProgram)渲染选定缓冲区(vertexArrayId[0])中的数据。这些参数表示要绘制三角形,数据从缓冲区中的索引位置0开始,长度为3个顶点。对于多个三角形,值3将相应地改变,这取决于顶点数据在缓冲区中的存储方式,例如,如果数据在共享顶点处重复或不重复,请参见后面的示例。此外,我们将查询数组中数据的长度,而不是使用硬编码的文字值。最后,我们解除顶点数组的绑定。此时,可以绑定不同的顶点数组和着色器程序,并显示不同的数据集。
/* THE SCENE
* Now define all the methods to handle the scene.
* This will be added to in later examples.
*/
public void initialise(GL3 gl) {
initialiseShader(gl);
shaderProgram = compileAndLink(gl);
fillBuffers(gl);
}
public void render(GL3 gl) {
gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
gl.glUseProgram(shaderProgram);
gl.glBindVertexArray(vertexArrayId[0]);
gl.glDrawArrays(GL.GL_TRIANGLES, 0, 3); // drawing one triangle
gl.glBindVertexArray(0);
}
值得总结一下我们已经达到了什么程度。在图中,着色器显示为通过文本文件提供给程序。这是未来项目的一般情况。目前,这些都是在程序中硬编码的,这也提供了编译着色器并使其在GPU上可用的代码。三角形的数据也是硬编码的。程序用例程初始化着色器,并用三角形的数据填充GPU上的缓冲区。
显示循环是使用程序中的FPSAnimator实现的,它反复要求绘制画布(每秒60帧)。重新绘制画布时,将重新绘制A01_GLEventListener,这意味着A01_GlEventListene。自动调用display方法。如前所述,GPU上的缓冲区中的三角形数据现在使用GPU上编译的顶点和片段着色器进行渲染。由于我们不更改着色器或数据,因此无需再次将它们传输到GPU。相反,显示循环只是不断告诉GPU清除屏幕并渲染数据。我们需要在未来的计划中牢记这一点。从CPU向GPU传输数据相对较慢。最好将数据保存在GPU上,然后要求再次渲染。
这个相同的过程将用于所有后续程序:首先,设置着色器并加载数据缓冲区;其次,使用显示循环使用着色器渲染缓冲数据。当然,数据可以在渲染调用之间以某种方式进行更改。这可以在CPU或GPU上完成——我们将很快看到这一点。此外,可以在GPU上缓冲多个数据集,并使用不同的着色器进行渲染,这些着色器都位于GPU上。这是我们将在后面的示例中详细研究的内容。
我们已经概述了正在发生的事情,但是着色器如何使用GPU上的缓冲数据?GPU部分显示了更多细节。这表明顶点和片段着色器接收和输出数据。顶点着色器的“in data”是来自顶点缓冲区的数据。将为顶点缓冲区中的每个顶点自动创建顶点着色器的新副本(达到GPU的容量)。每个副本可以和其他副本并行执行。每个顶点着色器都会发送一些数据。GPU跟踪所有副本并重新关联每个副本创建的数据。对于三角形,它会重新关联相关的三个顶点。光栅化过程填充顶点之间的间隙,从而创建实心三角形。这个过程创建一组片段,每个屏幕像素一个片段。相关数据被传送到片段着色器,例如片段在屏幕上的位置,以及可能的片段的颜色。GPU会自动生成片段着色器的多个副本,每个片段一个。所有这些都并行执行。最后,每个片段着色器输出片段的最终颜色。GPU确保将每个值写入屏幕内存中的正确位置。(在这个阶段,我们不考虑z缓冲区,z缓冲区用于确定一个片段是否在另一个片段之前,因此应该显示哪个片段。它也可以用于确定片段着色器是否甚至被执行。我们将在后面的章节中讨论这一点,当我们考虑形成3D对象的三角形集合,从而在着色器中产生重叠的片段)
总之,所有的代码都应该是这样的:
import java.nio.*;
import com.jogamp.common.nio.*;
import com.jogamp.opengl.*;
import com.jogamp.opengl.util.*;
import com.jogamp.opengl.util.awt.*;
import com.jogamp.opengl.util.glsl.*;
public class A02_GLEventListener implements GLEventListener {
public A02_GLEventListener() {
}
public void init(GLAutoDrawable drawable) {
GL3 gl = drawable.getGL().getGL3();
gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
gl.glClearDepth(1.0f);
gl.glEnable(GL.GL_DEPTH_TEST);
gl.glDepthFunc(GL.GL_LESS);
initialise(gl);
}
public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) {
}
public void display(GLAutoDrawable drawable) {
GL3 gl = drawable.getGL().getGL3();
render(gl);
}
public void dispose(GLAutoDrawable drawable) {
GL3 gl = drawable.getGL().getGL3();
gl.glDeleteBuffers(1, vertexBufferId, 0);
}
// ***************************************************
/* THE SCENE */
public void initialise(GL3 gl) {
initialiseShader(gl);
shaderProgram = compileAndLink(gl);
fillBuffers(gl);
}
public void render(GL3 gl) {
gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
gl.glUseProgram(shaderProgram);
gl.glBindVertexArray(vertexArrayId[0]);
gl.glDrawArrays(GL.GL_TRIANGLES, 0, 3); // drawing one triangle
gl.glBindVertexArray(0);
}
// ***************************************************
/* THE DATA */
// one triangle
private float[] vertices = {
-0.5f, -0.5f, 0.0f, // Bottom Left
0.5f, -0.5f, 0.0f, // Bottom Right
0.0f, 0.5f, 0.0f // Top middle
};
// ***************************************************
/* THE BUFFERS */
private int[] vertexBufferId = new int[1];
private int[] vertexArrayId = new int[1];
private void fillBuffers(GL3 gl) {
gl.glGenVertexArrays(1, vertexArrayId, 0);
gl.glBindVertexArray(vertexArrayId[0]);
gl.glGenBuffers(1, vertexBufferId, 0);
gl.glBindBuffer(GL.GL_ARRAY_BUFFER, vertexBufferId[0]);
FloatBuffer fb = Buffers.newDirectFloatBuffer(vertices);
gl.glBufferData(GL.GL_ARRAY_BUFFER, Float.BYTES * vertices.length,
fb, GL.GL_STATIC_DRAW);
gl.glVertexAttribPointer(0, 3, GL.GL_FLOAT, false, 3*Float.BYTES, 0);
gl.glEnableVertexAttribArray(0);
gl.glBindBuffer(GL.GL_ARRAY_BUFFER, 0);
gl.glBindVertexArray(0);
}
// ***************************************************
/* THE SHADER */
private String vertexShaderSource =
"#version 330 core\n" +
"\n" +
"layout (location = 0) in vec3 position;\n" +
"\n" +
"void main() {\n" +
" gl_Position = vec4(position.x, position.y, position.z, 1.0f);\n" +
"}";
private String fragmentShaderSource =
"#version 330 core\n" +
"\n" +
"out vec4 fragColor;\n" +
"\n" +
"void main() {\n" +
" fragColor = vec4(0.1f, 0.7f, 0.9f, 1.0f);\n" +
"}";
private int shaderProgram;
private void initialiseShader(GL3 gl) {
System.out.println(vertexShaderSource);
System.out.println(fragmentShaderSource);
}
private int compileAndLink(GL3 gl) {
String[][] sources = new String[1][1];
sources[0] = new String[]{ vertexShaderSource };
ShaderCode vertexShaderCode = new ShaderCode(GL3.GL_VERTEX_SHADER,
sources.length, sources);
boolean compiled = vertexShaderCode.compile(gl, System.err);
if (!compiled)
System.err.println("[error] Unable to compile vertex shader: " + sources);
sources[0] = new String[]{ fragmentShaderSource };
ShaderCode fragmentShaderCode = new ShaderCode(GL3.GL_FRAGMENT_SHADER,
sources.length, sources);
compiled = fragmentShaderCode.compile(gl, System.err);
if (!compiled)
System.err.println("[error] Unable to compile fragment shader: " + sources);
ShaderProgram program = new ShaderProgram();
program.init(gl);
program.add(vertexShaderCode);
program.add(fragmentShaderCode);
program.link(gl, System.out);
if (!program.validateProgram(gl, System.out))
System.err.println("[error] Unable to link program");
return program.program();
}
}
练习
1.更改绘制的三角形的颜色。(提示:更改片段着色器)未来几章中的示例将显示如何从应用程序中实现这一点,而不是将其硬编码到片段着色器中。
解决方案:
替换代码:fragColor = vec4(0.1f, 0.7f, 0.9f, 1.0f); 让它有不同的颜色,例如:fragColor = vec4(0.5f, 0.1f, 0.8f, 1.0f);
2.更改程序以绘制两个三角形。(提示:(i)通过扩展现有的顶点数组,在“数据”部分定义第二个三角形-在数组中添加三个新的顶点;(ii)然后,在“场景”部分的方法render()中,更改gl.glDrawArrays的最后一个参数(gl.gl_TRIANGLES,0,3);相应地,有多少项数据?)在下一节中,我们将看到元素缓冲区对象的使用如何使绘制多个三角形更容易。
解决方案:
public void render(GL3 gl) {
gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
gl.glUseProgram(shaderProgram);
gl.glBindVertexArray(vertexArrayId[0]);
gl.glDrawArrays(GL.GL_TRIANGLES, 0, 6); // *** drawing two triangles, i.e. 6 vertices***
gl.glBindVertexArray(0);
}