现代的OpenGL,进而延伸到WebGL,与我过去学习的遗留OpenGL有很大的不同。我了解光栅化的工作原理,所以我对这些概念很满意。然而,我读过的每个教程都引入了抽象和帮助器函数,这让我更难理解哪些部分是OpenGLAPI的真正核心。
需要明确的是,抽象(如将位置数据和呈现功能分离到单独的类中)在实际应用程序中非常重要。但是,这些抽象将代码分散到多个区域,并由于样板和在逻辑单元之间传递数据而引入开销。我最好的学习方式是线性代码流,其中每一行都是手头主题的核心。
首先,功劳归功于我用过的教程。从这个基础开始,我剥离了所有的抽象,直到我有了一个“最小可行的程序”。希望这将帮助您开始使用现代OpenGL。这是我们正在制作的:
有了WebGL,我们需要一块画布来作画。您肯定希望包含所有常见的HTML样板、一些样式等,但画布是最关键的。加载DOM后,我们将能够使用Javascript访问画布。
<;canvas id=";容器";width=";500";高度=";500&34;>;<;/canvas>;<;脚本>;文档。addEventListener(';DOMContentLoaded';,()=>;{//下面的所有Javascript代码都放在这里});<;/script>;
访问画布后,我们可以获得WebGL渲染上下文,并初始化其透明颜色。OpenGL世界中的颜色是RGBA,每个组件都在0到1之间。透明颜色是用于在重绘场景的任何帧的开头绘制画布的颜色。
在实际的程序中,可以进行更多的初始化,也应该进行更多的初始化。特别要注意的是启用深度缓冲区,这将允许基于Z坐标对几何体进行排序。对于这个只包含一个三角形的BASIC程序,我们将避免这种情况。
OpenGL的核心是一个光栅化框架,在这里我们可以决定如何实现除光栅化之外的所有东西。这需要在GPU上至少运行两段代码:
针对每个输入运行的顶点着色器,为每个输入输出一个3D(实际上,在齐次坐标中为4D)位置。
为屏幕上的每个像素运行的片段着色器,输出该像素应该是什么颜色。
在这两个步骤之间,OpenGL从顶点着色器获取几何体,并确定该几何体实际覆盖了屏幕上的哪些像素。这是光栅化部分。
这两个着色器通常都是用GLSL(OpenGL着色语言)编写的,然后将其向下编译为GPU的机器码。然后将机器代码发送到GPU,因此它可以在渲染过程中运行。我不会在GLSL上花费太多时间,因为我只想展示一些基础知识,但是该语言非常接近C语言,大多数程序员都很熟悉。
首先,我们编译一个顶点着色器并将其发送到GPU。在这里,着色器的源代码存储在字符串中,但可以从其他位置加载。最终,该字符串被发送到WebGL API。
const source V=`属性Vector 3 position;可变Vector 4 color;void main(){gl_position=ve4(position,1);color=gl_position*0.5+0.5;}`;const shaderV=gl。createShader(gl.。Vertex_Shader);gl.。shaderSource(shaderV,sourceV);gl.。编译Shader(ShaderV);如果(!总帐。getShaderParameter(shaderV,gl.。COMPILE_STATUS)){控制台。错误(总帐。getShaderInfoLog(ShaderV));抛出新错误(';编译顶点着色器失败);}。
一个名为Position的属性。属性本质上是一个输入,并且为每个这样的输入调用着色器。
一种不同的称为颜色的颜色。这是顶点着色器的输出(每个输入一个),也是片段着色器的输入。将该值传递给片段着色器时,将根据光栅化的属性对该值进行插值。
gl_position值。本质上是顶点着色器的输出,就像任何变化值一样。这个是特殊的,因为它用于确定需要绘制哪些像素。
还有一个名为Uniform的变量类型,它将在顶点着色器的多次调用中保持不变。这些制服用于变换矩阵等属性,变换矩阵对于单个几何体上的所有顶点都是恒定的。
接下来,我们对片段着色器执行相同的操作,编译并将其发送到GPU。请注意,来自顶点着色器的颜色变量现在由片段着色器读取。
const source F=`精度中等浮点;可变ve4颜色;void main(){gl_FragColor=color;}`;const shaderF=gl。createShader(gl.。Fragment_Shader);gl.。shaderSource(shaderF,sourceF);gl.。编译Shader(ShaderF);如果(!总帐。getShaderParameter(shaderF,gl.。COMPILE_STATUS)){控制台。错误(总帐。getShaderInfoLog(ShaderF));抛出新错误(';无法编译片段着色器';);}。
常量程序=gl。createProgram();gl.。attachShader(程序,shaderV);gl.。attachShader(program,shaderF);gl.。linkProgram(程序);如果(!总帐。getProgramParameter(PROGRAM,gl.。link_status)){控制台。错误(总帐。getProgramInfoLog(Program));抛出新错误(';无法链接程序';);}gl。useProgram(程序);
我们告诉GPU上面定义的着色器就是我们要运行的着色器。所以,现在剩下的就是创建输入,让GPU自由处理这些输入。
输入数据将存储在GPU的内存中,并从那里进行处理。整个输入被传输到GPU并从那里读取,而不是对每个输入进行单独的绘制调用,这将一次传输一个相关的数据。(旧版OpenGL会一次传输一块数据,导致性能下降。)。
OpenGL提供称为顶点缓冲区对象(VBO)的抽象。我仍在弄清楚所有这些功能是如何工作的,但最终,我们将使用抽象完成以下工作:
使用使用gl.createBuffer()创建的唯一缓冲区和gl.ARRAY_BUFFER的绑定点将字节传输到GPU的内存。
我们将在顶点着色器中为每个输入变量(属性)设置一个VBO,不过也可以将单个VBO用于多个输入。
常量位置Data=new Float32Array([-0.75,-0.65,-1,0.75,-0.65,-1,0,0.65,-1,]);常量缓冲区=gl.。createBuffer();gl.。绑定缓冲区(gl.。ARRAY_BUFFER,BUFFER);gl.。BufferData(总帐。ARRAY_BUFFER,PositionsData,gl.。STATIC_DRAW);
通常,您将使用对应用程序有意义的任何坐标指定几何体,然后使用顶点着色器中的一系列变换将它们放入OpenGL的剪辑空间。我不会深入到剪辑空间的细节(它们与齐次坐标有关),但是现在,X和Y从-1到+1变化。因为我们的顶点着色器只是按原样传递输入数据,所以我们可以直接在剪辑空间中指定坐标。
接下来,我们还将缓冲区与顶点着色器中的一个变量相关联。在这里,我们:
告诉OpenGL从gl.ARRAY_BUFFER绑定点读取数据,每批3个,带有特定的参数,如偏移量和步距为零。
请注意,我们可以通过这种方式创建VBO并将其与顶点着色器属性相关联,因为我们会一个接一个地执行这两项操作。如果我们将这两个函数分开(例如,一次性创建所有VBO,然后将它们与各个属性相关联),则需要调用gl.bindBuffer(.)。在将每个VBO与其相应属性相关联之前。
最后,GPU内存中的所有数据都按照我们想要的方式进行了设置,我们可以告诉OpenGL清除屏幕并在我们设置的数组上运行程序。作为光栅化的一部分(确定哪些像素被顶点覆盖),我们告诉OpenGL将每组3个顶点视为三角形。
我们以线性方式设置它的方式确实意味着程序在一次拍摄中运行。在任何实际应用中,我们都会以结构化的方式存储数据,当数据发生变化时将其发送到GPU,并执行每一帧的绘制。
将所有内容放在一起,下图显示了在屏幕上显示第一个三角形所需的最小概念集。即使这样,图表也得到了极大的简化,因此您最好将本文中提供的75行代码组合在一起,并对其进行研究。
对我来说,学习OpenGL最难的部分是在屏幕上获得最基本的图像所需的大量样板文件。因为光栅化框架要求我们提供3D渲染功能,并且与GPU的通信很繁琐,所以有很多概念需要在前面学习。我希望这篇文章能说明基础知识比其他教程更简单,让它们看起来更简单!