WebGL基础知识

2020-10-11 16:07:25

WebGL(Web图形库)通常被认为是一种3D API。人们认为我会使用WebGL,而Magic会得到很酷的3D效果。实际上,WebGL只是一个光栅化引擎。它根据您提供的代码绘制点、线和三角形。让WebGL做任何其他事情都取决于您提供代码来使用点、线和三角形来完成您的任务。

WebGL在计算机的GPU上运行。因此,您需要提供在该GPU上运行的代码。您需要以函数对的形式提供该代码。这两个函数被称为顶点着色器和片段着色器,它们都是用非常严格类型的C/C++语言编写的,称为GLSL。(GL着色器语言)。将它们配对在一起称为程序。

顶点着色器的工作是计算顶点位置。基于输出的位置,WebGL可以栅格化各种基元,包括点、线或三角形。栅格化这些基元时,它会调用第二个用户提供的函数,称为片段着色器。片段着色器的工作是为当前绘制的基元的每个像素计算颜色。

几乎所有的WebGL API都是关于为要运行的这对函数设置状态。对于要绘制的每一件事,都需要设置一组状态,然后通过调用gl.drawArray或gl.drawElements来执行一对函数,后者在GPU上执行着色器。

您希望这些函数能够访问的任何数据都必须提供给GPU。着色器可以接收数据的方式有4种。

缓冲区是您上传到GPU的二进制数据数组。通常缓冲区包含位置、法线、纹理坐标、顶点颜色等内容,尽管你可以自由地放入任何你想要的东西。

属性用于指定如何将拓扑数据从缓冲区中取出并将其提供给顶点着色器。例如,您可以将缓冲区中的位置作为三个32位浮点位置。您将告诉特定的属性从哪个缓冲区拉出位置,应该拉出什么类型的数据(3个分量32位浮点数),位置开始于缓冲区的哪个偏移量,以及从一个位置到下一个位置要获取多少字节。

缓冲区不是随机访问。相反,顶点着色器将按指定次数执行。每次执行它时,都会从每个指定的缓冲区中取出下一个值,并将其分配给一个属性。

纹理是可以在着色器程序中随机访问的数据阵列。纹理中最常见的是图像数据,但纹理只是数据,可以很容易地包含颜色以外的内容。

变化是顶点着色器将数据传递给片段着色器的一种方式。根据正在渲染的内容(点、线或三角形),顶点着色器在变化上设置的值将在执行碎片着色器时进行插值。

WebGL只关心两件事:剪辑空间坐标和颜色。作为使用WebGL的程序员,您的工作就是为WebGL提供这两件事。为此,您需要提供2个着色器。提供剪辑空间坐标的顶点着色器和提供颜色的片段着色器。

无论画布大小如何,剪辑空间坐标始终从-1到+1。

//属性将从缓冲区接收数据attribute_position;//所有着色器都有一个主函数void main(){//gl_position是顶点着色器的特殊变量//负责设置gl_position=a_position;}。

在执行时,如果整个代码都是用JavaScript而不是GSL编写的,您可以想象它会是这样使用的。

//*伪代码!!*var position tionBuffer=[0,0,0,0,0,0,0.5,0,0,0.7,0,0,0,];var属性={};var gl_position;draArray(...,Offset,count){var stride=4;var size=4;for(var i=0;i<;count;++i){/将下4个值从position tionBuffer复制到a_position属性const start=Offset+i*Stride;属性.a_position=position Buffer.Slice(start,start+size);runVertexShader();...。DoSomethingWith_gl_position();}。

实际上并不是那么简单,因为position Buffer需要转换成二进制数据(见下文),因此从缓冲区中取出数据的实际计算会略有不同,但希望这能让您了解顶点着色器是如何执行的。

//碎片着色器没有默认精度,因此我们需要//选择一个。Medium是一个很好的默认值。表示";中精度";精度中等浮点;void main(){//gl_FragColor是一个特殊变量,a片段着色器//负责设置gl_FragColor=ve4(1,0,0.5,1);//返回红紫色}。

上面我们重新设置gl_FragColor为1,0,0.5,1,即1代表红色,0代表绿色,0.5代表蓝色,1代表Alpha。WebGL中的颜色从0变为1。

现在,我们需要编译这些着色器以将它们放到GPU上,因此首先需要将它们转换为字符串。您可以按照在JavaScript中通常创建字符串的方式创建GLSL字符串:通过使用Ajax进行连接、使用多行模板字符串下载它们。或者在本例中,将它们放在非JavaScript类型的脚本标记中。

<;script id=";vertex-shader-2d";type=";notjs";>;//属性将从缓冲区属性ve4 a_position接收数据;//所有着色器都有一个主函数void main(){//gl_position是一个特殊变量顶点着色器//负责设置gl_position=a_position;}<;/script>;<;script id=";Fragment-shader-2d";type=";notjs";>;//碎片着色器没有默认精度,因此我们需要//选择一个。Mediump是一个很好的默认精度mediump浮点;void main(){//gl_FragColor是一个特殊变量a片段着色器//负责设置gl_FragColor=ve4(1,0,0.5,1);//返回红紫色}<;/script>;

事实上,大多数3D引擎使用各种类型的模板、串联等动态生成GLSL着色器。对于此站点上的示例,尽管它们都不够复杂,不需要在运行时生成GLSL。

下一步,我们需要一个函数来创建着色器,上传GLSL源代码,并编译着色器。注意,我没有写任何注释,因为从函数的名称应该可以清楚地看到正在发生的事情。

函数createShader(gl,type,source){var shader=gl.createShader(Type);gl.shaderSource(着色器,源);gl.编译eShader(着色器);var SUCCESS=gl.getShaderParameter(shader,gl.COMPILE_STATUS);IF(SUCCESS){return shader;}console.log(gl.getShaderInfoLog(Shader));gl.delete eShader(Shader);}。

Var vertexShaderSource=document.querySelector(";#vertex-shader-2d";).text;var碎片ShaderSource=document.querySelector(";#fragment-shader-2d";).text;var vertexShader=createShader(gl,gl.VERTEX_SHADER,vertexShaderSource);var碎片化Shader=createShader(gl,gl.FRAGMENT_SHADER,framentShaderSource);

函数createProgram(gl,vertexShader,FragmentShader){var program=gl.createProgram();gl.attachShader(program,vertexShader);gl.attachShader(program,framentShader);gl.linkProgram(Program);var SUCCESS=gl.getProgramParameter(program,gl.LINK_STATUS);IF(SUCCESS){返回PROGRAM;}console.log(gl.getProgramInfoLog(PROGRAM));gl.delete eProgram(PROGRAM);}。

现在我们已经在GPU上创建了一个GLSL程序,我们需要向它提供数据。WebGL API的大部分内容是关于设置状态以向我们的GLSL程序提供数据。在这种情况下,我们对GLSL程序的唯一输入是a_position,这是一个属性。我们应该做的第一件事是查找我们刚刚创建的程序的属性的位置。

查找属性位置(和统一位置)是您应该在初始化期间执行的操作,而不是在渲染循环中。

WebGL允许我们在全局绑定点上操作许多WebGL资源。您可以将绑定点看作WebGL内部的内部全局变量。首先,您将资源绑定到绑定点。然后,所有其他函数通过绑定点引用资源。因此,让我们绑定位置缓冲区。

现在,我们可以通过通过绑定点引用数据来将数据放入该缓冲区。

//三个二维点var位置=[0,0,0,0.5,0.7,0,];gl.BufferData(gl.ARRAY_BUFFER,new Float32Array(Position),gl.STATIC_DRAW);

这里发生了很多事情。第一件事是我们有位置,这是一个JavaScript数组。另一方面,WebGL需要强类型的数据,因此部分new Float32Array(Position)创建了一个新的32位浮点数字数组,并从位置复制值。然后,Gl.BufferData将该数据复制到GPU上的Position Buffer。它使用位置缓冲区,因为我们绑定到上面的ARRAY_BUFFER绑定点。

最后一个参数gl.STATIC_DRAW是对WebGL的一个提示,说明我们将如何使用数据。WebGL可以尝试使用该提示来优化某些内容。GL.STATIC_DRAW告诉WebGL我们不太可能对此数据进行太多更改。

到目前为止的代码是初始化代码。加载页面时运行一次的代码。这一点下面的代码是呈现代码或每次我们想要呈现/绘制时应该执行的代码。

在我们画之前,我们应该调整画布的大小以匹配它的显示大小。就像图像一样,画布有2个大小,其中实际的像素数和显示的大小是分开的。CSS确定画布显示的大小。您应该始终使用CSS设置您希望画布的大小,因为它比任何其他方法都要灵活得多。

要使画布中的像素数与其显示的大小相匹配,可以使用助手函数,您可以在此处阅读。

在几乎所有这些样本中,如果样本在自己的窗口中运行,则画布大小为400x300像素,但如果样本位于iFrame内(如本页所示),则画布大小会伸展以填充可用空间。通过让CSS确定大小,然后进行调整以匹配,我们可以轻松处理这两种情况。

我们需要告诉WebGL如何将我们将要设置的gl_position转换回像素(通常称为屏幕空间)。为此,我们调用gl.viewport并将画布的当前大小传递给它。

这告诉WebGL-1+1剪辑空间对于x映射到0<;->;gl.canvas.width,对于y映射到0<;->;gl.canvas.height.。

我们清理一下画布。0,0,0,0分别是红色,绿色,蓝色,阿尔法,所以在本例中我们要使画布透明。

接下来,我们需要告诉WebGL如何从上面设置的缓冲区中获取数据,并将其提供给着色器中的属性。首先,我们需要打开该属性

//绑定位置Buffer.gl.bindBuffer(gl.ARRAY_BUFFER,position tionBuffer);//告诉属性如何将数据移出Position Buffer(Array_Buffer)var size=2;//每次迭代2个组件var type=gl.FLOAT;//数据为32位浮点型var Normize=false;//don';t规格化数据量跨度=0;//0=前移大小*sizeof(Type)每次迭代获取下一个位置var Offset=0;//从Buffergl.vertexAttribPointer(PotributeLocation,Size,Type,Normize,Stride,Offset)开始处开始。

VertexAttribPointer的一个隐藏部分是它将当前ARRAY_BUFFER绑定到属性。换言之,现在该属性被绑定到position tionBuffer。这意味着我们可以自由地将其他内容绑定到ARRAY_BUFFER绑定点。该属性将继续使用position Buffer。

请注意,从我们的GLSL顶点着色器的角度来看,a_position属性是一个ve4。

Vector 4是一个4浮点值。在JavaScript中,您可以认为它类似于a_position={x:0,y:0,z:0,w:0}。上面我们将size=2.AttributesDefault设置为0,0,0,1,因此此属性将从缓冲区获取其前2个值(x和y)。Z和w将分别为默认值0和1。

因为计数是3,所以这将执行我们的顶点着色器3次。第一次,a_position.x和a_position tion.in顶点着色器属性将被设置为来自position Buffer的前2个值。第二次,a_position.x和a_position.y将被设置为后2个值。最后一次,它们将设置为最后两个值。

因为我们将primitiveType设置为gl.TriIANGLES,所以每次顶点着色器运行3次时,WebGL都会根据我们设置为gl_position的3个值绘制一个三角形。无论我们的画布大小是多少,这些值都在剪辑空间坐标中,每个方向从-1到1。

因为我们的顶点着色器只是将position Buffer值复制到gl_position,所以将在剪辑空间坐标处绘制三角形。

从剪辑空间转换到屏幕空间如果画布大小恰好是400x300,我们会得到这样的结果

剪辑空间屏幕空间0,0-&>200,150 0,0.5-->200,225 0.7,0-->340,150。

WebGL现在将渲染该三角形。对于将要绘制的每个像素,WebGL将调用我们的片段着色器。我们的片段着色器只将gl_FragColor设置为1,0,0.5,1。因为画布是8位/通道画布,这意味着WebGL将把值[255,0,127,255]写入画布。

在上面的例子中,你可以看到我们的顶点着色器除了直接传递我们的位置数据什么也不做。因为位置数据已经在剪辑空间中,所以没有工作要做。如果您想要3D,这取决于您提供的着色器,转换从3D到剪辑空间,因为WebGL是唯一的光栅化API。

你可能想知道为什么三角形从中间开始,一直到右上角,x中的剪裁空间从-1到+1,这意味着0在中心,正值在中心的右边。

至于为什么它在顶部,在剪辑空间中-1在底部,+1在顶部。这意味着0在中心,所以正数将在中心上方。

对于2D的东西,你可能宁愿以像素为单位而不是剪辑空间,更改着色器,这样我们就可以提供以像素为单位的位置,并为我们将其转换为剪辑空间。这是新的顶点着色器。

<;script id=";vertex-shader-2d";type=";notjs";>;-属性Vector 4 a_position;*属性Vector 2 a_position;+uniform Vector 2 u_Resolution;void main(){+//将位置从像素转换为0.0到1.0+ve2 zeroToOne=a_position/u_Resolution;++//从0->;1转换为0->;2+ve2 zeroToTwo=zeroToOne*2.0;++//从0->;2转换为-1->;+1(剪辑空间)+ve2 clipSpace=zeroToTwo-1.0;+*gl_position=ve4(clipSpace,0,1);}<;/script>;

关于这些更改,有一些需要注意的事项。我们将a_position更改为ve2,因为我们无论如何都只使用x和y。矢量2类似于矢量4,但只有x和y。

接下来,我们添加了名为u_Resolution的制服。要设置它,我们需要查找它的位置。

其余的应该从评论中看得很清楚了。通过将u_Resolution设置为画布的分辨率,着色器现在将采用我们在PositionBuffer提供的像素坐标中放置的位置,并将它们转换为剪辑空间。

现在我们可以将位置值从剪辑空间更改为像素。这一次我们要从两个三角形画一个长方形,每个三角形3个点。

Var Position=[*10,20,*80,20,*10,30,*10,30,*80,20,*80,30,];gl.BufferData(gl.ARRAY_BUFFER,new Float32Array(Position),gl.STATIC_DRAW);

在我们设置了要使用的程序之后,我们就可以设置我们创建的制服的值了。Gl.useProgram类似于上面的gl.bindBuffer,因为它设置当前程序。之后,所有gl.uniformXXX函数都在当前程序上设置制服。

当然,要绘制2个三角形,我们需要让WebGL调用我们的顶点着色器6次,所以我们需要将计数更改为6。

注意:此示例和以下所有示例都使用webgl-utils.js,其中包含编译和链接着色器的函数。没有理由把那些样本代码弄得乱七八糟。

同样,您可能会注意到该矩形位于该区域的底部附近。WebGL将正Y向上和负Y视为向下。在剪辑空间中,左下角-1,-1。我们没有改变任何符号,所以用我们当前的数学0,0成为左下角。要使它成为更传统的用于2D图形API的左上角,我们只需翻转剪辑空间y坐标即可。

让我们把定义矩形的代码变成一个函数,这样我们就可以为不同大小的矩形调用它。在我们做这件事的同时,我们将使颜色可定色。

<;script id=";Fragment-shader-2d";type=";notjs";>;Precision Medium Float;+Uniform Vector 4 u_color;void main(){*gl_FragColor=u_color;}<;/script>;

这里是一个新的代码,它可以在随机的位置和随机的颜色中绘制50个矩形。

Var ColorUniformLocation=gl.getUniformLocation(program,";u_color";);...//为(var ii=0;ii<;50;++ii){//设置一个随机矩形//这将写入position tionBuffer,因为//这是我们绑定在array_buffer//绑定点setRectangle(gl,andromant Int(300),andomant Int(300))上的最后一件事;//设置随机颜色。Gl.uniform4f(ColorUniformLocation,Math.Random(),1);//绘制矩形。Gl.drawArray(gl.TriIANGLES,0,6);}}//返回一个从0到Range-1的随机整数。Function RANDOM Int(Range){Return Math.Floor(Math.Random()*Range);}//用定义矩形的值填充缓冲区。函数setRectangle(gl,x,y,width,Height){var x1=x;var x2=x+width;var y1=y;var y2=y+Height;//备注:gl.buffer erData(gl.ARRAY_BUFFER,...)。将影响//任何绑定到`ARRAY_BUFFER`绑定点的缓冲区//但到目前为止我们只有一个缓冲区。如果我们有多个//缓冲区,我们应该首先将该缓冲区绑定到`ARRAY_BUFFER`。Gl.BufferData(gl.ARRAY_BUFFER,new Float32Array([x1,y1,x2,y1,x1,y2,x1,y2,y1,x2,y2]),gl.STATIC_DRAW);}。

我希望您能看到,WebGL实际上是一个非常简单的API。好吧,简单这个词可能用错了。它的作用很简单。它只执行两个用户提供的函数,顶点着色器和碎片着色器,并绘制三角形、线条或点。虽然3D可能会变得更加复杂,但复杂的情况是由程序员以更复杂的着色器的形式添加的。WebGL API本身只是一个光栅化器,概念上相当简单。

我们介绍了一个小示例,它展示了如何在一个属性和两个制服中提供数据,具有多个属性和多个制服是很常见的。在这篇文章的顶部,我们还提到了变化和纹理。这些将在随后的课程中展示。

在我们继续之前,我想提一下,对于大多数应用程序来说,像我们在setRectangle中那样更新缓冲区中的数据并不常见。我使用这个例子是因为我认为它是最容易解释的,因为它将像素坐标显示为输入,并且演示了用GLSL进行少量的数学运算。这不是错的,在很多情况下这是正确的,但你应该继续阅读,找出在WebGL中定位、定位和缩放事物的更常见的方式。

如果您是Web开发的新手,或者即使您不是,也请查看安装和安装以获取有关如何进行WebGL开发的一些提示。

如果您100%是WebGL新手,并且对GLSL是什么、着色器是什么或者GPU做什么一无所知,那么请查看WebGL实际工作原理的基础知识。您可能还想看看这个交互式状态图,以获得理解WebGL工作原理的另一种方式。

您还应该至少简要阅读一下大多数示例中使用的样板代码。你还应该至少浏览一下如何绘制多个东西,让你对更典型的WebGL应用程序的结构有一些了解,因为不幸的是,几乎所有的例子都只画了一个东西,所以没有显示这种结构。

否则从这里可以往两个方向走。如果你对图像处理感兴趣,我会教你怎么做。

.