WebGL (Web Graphics Library) is often thought of as a 3D API. People think "I'll use WebGL and magic I'll get cool 3d".
In reality WebGL is just a rasterization engine. It draws points, lines, and triangles based
on code you supply. Getting WebGL to do anything else is up to you to provide code to use
points, lines, and triangles to accomplish your task.
WebGL(Web 图形库)通常被认为是 3D API。人们认为“我会使用 WebGL 和魔法,我会得到很酷的 3d”。实际上 WebGL 只是一个光栅化引擎。它根据您提供的代码绘制点、线和三角形。要让 WebGL 执行其他任何操作,您需要提供代码来使用点、线和三角形来完成您的任务。
WebGL runs on the GPU on your computer. As such you need to provide the code that runs on that GPU.
You provide that code in the form of pairs of functions. Those 2 functions are called a vertex shader
and a fragment shader and they are each written in a very strictly typed C/C++ like language called
GLSL. (GL Shader Language). Paired together they are called a program.
WebGL 在计算机上的 GPU 上运行。因此,您需要提供在该 GPU 上运行的代码。您以函数对的形式提供该代码。这两个函数称为顶点着色器和片段着色器,它们各自都是用类型非常严格的类似 C/C++ 的语言(称为 GLSL)编写的。 (GL 着色器语言)。它们配对在一起称为程序。
A vertex shader's job is to compute vertex positions. Based on the positions the function outputs
WebGL can then rasterize various kinds of primitives including
points, lines, or triangles.
When rasterizing these primitives it calls a second user supplied function called a fragment shader.
A fragment shader's job is to compute a color for each pixel of the primitive currently being drawn.
顶点着色器的工作是计算顶点位置。根据函数输出的位置,WebGL 可以光栅化各种图元,包括点、线或三角形。当光栅化这些基元时,它调用第二个用户提供的函数,称为片段着色器。片段着色器的工作是计算当前正在绘制的图元的每个像素的颜色。
Nearly all of the entire WebGL API is about setting up state for these pairs of functions to run.
For each thing you want to draw you setup a bunch of state then execute a pair of functions by calling
gl.drawArrays
or gl.drawElements
which executes your shaders on the GPU.
整个 WebGL API 的几乎所有内容都是关于设置这些函数对运行的状态。对于你想要绘制的每一个东西,你设置一堆状态,然后通过调用 gl.drawArrays
或 gl.drawElements
来执行一对函数,它们在 GPU 上执行你的着色器。
Any data you want those functions to have access to must be provided to the GPU. There are 4 ways
a shader can receive data.
您希望这些函数访问的任何数据都必须提供给 GPU。着色器有 4 种接收数据的方式。
Attributes and Buffers 属性和缓冲区
Buffers are arrays of binary data you upload to the GPU. Usually buffers contain
things like positions, normals, texture coordinates, vertex colors, etc although
you're free to put anything you want in them.
缓冲区是上传到 GPU 的二进制数据数组。通常缓冲区包含位置、法线、纹理坐标、顶点颜色等内容,尽管您可以自由地将任何内容放入其中。
Attributes are used to specify how to
pull data out of your buffers and provide them to your vertex shader.
For example you might put positions in a buffer as three 32bit floats
per position. You would tell a particular attribute which buffer to pull the positions out of, what type
of data it should pull out (3 component 32 bit floating point numbers), what offset
in the buffer the positions start, and how many bytes to get from one position to the next.
属性用于指定如何从缓冲区中提取数据并将其提供给顶点着色器。例如,您可以将位置放入缓冲区中,每个位置三个 32 位浮点数。您可以告诉特定属性从哪个缓冲区中提取位置、应提取什么类型的数据(3 个组成部分 32 位浮点数)、位置在缓冲区中的偏移量以及从其中获取多少字节位置到下一个。
Buffers are not random access. Instead a vertex shader is executed a specified number
of times. Each time it's executed the next value from each specified buffer is pulled
out and assigned to an attribute.
缓冲区不是随机访问。相反,顶点着色器会执行指定的次数。每次执行时,都会从每个指定的缓冲区中取出下一个值并将其分配给一个属性。
Uniforms 制服
Uniforms are effectively global variables you set before you execute your shader program.
制服实际上是您在执行着色器程序之前设置的全局变量。
Textures 纹理
Textures are arrays of data you can randomly access in your shader program. The most
common thing to put in a texture is image data but textures are just data and can just
as easily contain something other than colors.
纹理是您可以在着色器程序中随机访问的数据数组。纹理中最常见的内容是图像数据,但纹理只是数据,可以轻松包含颜色以外的其他内容。
Varyings 瓦林斯
Varyings are a way for a vertex shader to pass data to a fragment shader. Depending
on what is being rendered, points, lines, or triangles, the values set on a varying
by a vertex shader will be interpolated while executing the fragment shader.
Varying 是顶点着色器将数据传递到片段着色器的一种方式。根据正在渲染的内容(点、线或三角形),在执行片段着色器时将插值由顶点着色器设置的变化值。
WebGL only cares about 2 things: clip space coordinates and colors.
Your job as a programmer using WebGL is to provide WebGL with those 2 things.
You provide your 2 "shaders" to do this. A Vertex shader which provides the
clip space coordinates, and a fragment shader that provides the color.
WebGL 只关心两件事:剪辑空间坐标和颜色。作为使用 WebGL 的程序员,您的工作就是为 WebGL 提供这两件事。您提供 2 个“着色器”来执行此操作。提供剪辑空间坐标的顶点着色器和提供颜色的片段着色器。
Clip space coordinates always go from -1 to +1 no matter what size your
canvas is.
无论画布大小如何,剪辑空间坐标始终从 -1 到 +1。
Here is a simple WebGL example that shows WebGL in its simplest form.
这是一个简单的 WebGL 示例,以最简单的形式展示了 WebGL。
Let's start with a vertex shader
让我们从顶点着色器开始
When executed, if the entire thing was written in JavaScript instead of GLSL
you could imagine it would be used like this
执行时,如果整个内容是用 JavaScript 而不是 GLSL 编写的,您可以想象它会像这样使用
In reality it's not quite that simple because positionBuffer
would need to be converted to binary
data (see below) and so the actual computation for getting data out of the buffer
would be a little different but hopefully this gives you an idea of how a vertex
shader will be executed.
实际上,它并不是那么简单,因为 positionBuffer
需要转换为二进制数据(见下文),因此从缓冲区中获取数据的实际计算会有所不同,但希望这能为您提供一个顶点着色器如何执行的想法。
Next we need a fragment shader
接下来我们需要一个片段着色器
Above we're setting gl_FragColor
to 1, 0, 0.5, 1
which is 1 for red, 0 for green,
0.5 for blue, 1 for alpha. Colors in WebGL go from 0 to 1.
上面我们将 gl_FragColor
设置为 1, 0, 0.5, 1
,其中 1 表示红色,0 表示绿色,0.5 表示蓝色,1 表示 Alpha。 WebGL 中的颜色从 0 到 1。
Now that we have written the 2 shader functions lets get started with WebGL
现在我们已经编写了 2 个着色器函数,让我们开始使用 WebGL
First we need an HTML canvas element
首先我们需要一个 HTML canvas 元素
Then in JavaScript we can look that up
然后我们可以在 JavaScript 中查找
Now we can create a WebGLRenderingContext
现在我们可以创建一个WebGLRenderingContext
Now we need to compile those shaders to put them on the GPU so first we need to get them into strings.
You can create your GLSL strings any way you normally create strings in JavaScript: by concatenating,
by using AJAX to download them, by using multiline template strings. Or in this case, by
putting them in non-JavaScript typed script tags.
现在我们需要编译这些着色器以将它们放在 GPU 上,因此首先我们需要将它们放入字符串中。您可以按照通常在 JavaScript 中创建字符串的方式创建 GLSL 字符串:通过连接、使用 AJAX 下载字符串、使用多行模板字符串。或者在本例中,将它们放入非 JavaScript 类型的脚本标记中。
In fact, most 3D engines generate GLSL shaders on the fly using various types of templates, concatenation, etc.
For the samples on this site though none of them are complex enough to need to generate GLSL at runtime.
事实上,大多数 3D 引擎都会使用各种类型的模板、串联等动态生成 GLSL 着色器。对于本网站上的示例,尽管它们都复杂到不需要在运行时生成 GLSL。
Next we need a function that will create a shader, upload the GLSL source, and compile the shader.
Note I haven't written any comments because it should be clear from the names of the functions
what is happening.
接下来我们需要一个函数来创建着色器、上传 GLSL 源代码并编译着色器。请注意,我没有写任何注释,因为从函数名称中应该可以清楚地看出发生了什么。
We can now call that function to create the 2 shaders
We then need to link those 2 shaders into a program
然后我们需要将这 2 个着色器链接到一个程序中
And call it 并称之为
Now that we've created a GLSL program on the GPU we need to supply data to it.
The majority of the WebGL API is about setting up state to supply data to our GLSL programs.
In this case our only input to our GLSL program is a_position
which is an attribute.
The first thing we should do is look up the location of the attribute for the program
we just created
现在我们已经在 GPU 上创建了一个 GLSL 程序,我们需要向它提供数据。 WebGL API 的大部分内容是关于设置状态以向我们的 GLSL 程序提供数据。在本例中,GLSL 程序的唯一输入是 a_position
,它是一个属性。我们应该做的第一件事是查找我们刚刚创建的程序的属性位置
Looking up attribute locations (and uniform locations) is something you should
do during initialization, not in your render loop.
查找属性位置(和统一位置)是您应该在初始化期间执行的操作,而不是在渲染循环中执行的操作。
Attributes get their data from buffers so we need to create a buffer
属性从缓冲区获取数据,因此我们需要创建一个缓冲区
WebGL lets us manipulate many WebGL resources on global bind points. You can think of bind points as internal global variables inside WebGL. First you bind a resource to a bind point. Then, all other functions refer to the resource through the bind point. So, let's bind the position buffer.
Now we can put data in that buffer by referencing it through the bind point
There's a lot going on here. The first thing is we have positions
which is a
JavaScript array. WebGL on the other hand needs strongly typed data so the part
new Float32Array(positions)
creates a new array of 32bit floating point numbers
and copies the values from positions
. gl.bufferData
then copies that data to
the positionBuffer
on the GPU. It's using the position buffer because we bound
it to the ARRAY_BUFFER
bind point above.
The last argument, gl.STATIC_DRAW
is a hint to WebGL about how we'll use the data.
WebGL can try to use that hint to optimize certain things. gl.STATIC_DRAW
tells WebGL
we are not likely to change this data much.
The code up to this point is initialization code. Code that gets run once when we load the page. The code below this point is rendering code or code that should get executed each time we want to render/draw.
Before we draw we should resize the canvas to match its display size. Canvases just like Images have 2 sizes. The number of pixels actually in them and separately the size they are displayed. CSS determines the size the canvas is displayed. You should always set the size you want a canvas to be with CSS since it is far far more flexible than any other method.
To make the number of pixels in the canvas match the size it's displayed I'm using a helper function you can read about here.
In nearly all of these samples the canvas size is 400x300 pixels if the sample is run in its own window but stretches to fill the available space if it's inside an iframe like it is on this page. By letting CSS determine the size and then adjusting to match we easily handle both of these cases.
We need to tell WebGL how to convert from the clip space
values we'll be setting gl_Position
to back into pixels, often called screen space.
To do this we call gl.viewport
and pass it the current size of the canvas.
This tells WebGL the -1 +1 clip space maps to 0 <-> gl.canvas.width
for x and 0 <-> gl.canvas.height
for y.
We clear the canvas. 0, 0, 0, 0
are red, green, blue, alpha respectively so in this case we're making the canvas transparent.
We tell WebGL which shader program to execute.
Next we need to tell WebGL how to take data from the buffer we setup above and supply it to the attribute in the shader. First off we need to turn the attribute on
Then we need to specify how to pull the data out
A hidden part of gl.vertexAttribPointer
is that it binds the current ARRAY_BUFFER
to the attribute. In other words now this attribute is bound to
positionBuffer
. That means we're free to bind something else to the ARRAY_BUFFER
bind point.
The attribute will continue to use positionBuffer
.
note that from the point of view of our GLSL vertex shader the a_position
attribute is a vec4
vec4
is a 4 float value. In JavaScript you could think of it something like
a_position = {x: 0, y: 0, z: 0, w: 0}
. Above we set size = 2
. Attributes
default to 0, 0, 0, 1
so this attribute will get its first 2 values (x and y)
from our buffer. The z, and w will be the default 0 and 1 respectively.
After all that we can finally ask WebGL to execute our GLSL program.
Because the count is 3 this will execute our vertex shader 3 times. The first time a_position.x
and a_position.y
in our vertex shader attribute will be set to the first 2 values from the positionBuffer
.
The second time a_position.x
and a_position.y
will be set to the second 2 values. The last time they will be
set to the last 2 values.
Because we set primitiveType
to gl.TRIANGLES
, each time our vertex shader is run 3 times
WebGL will draw a triangle based on the 3 values we set gl_Position
to. No matter what size
our canvas is those values are in clip space coordinates that go from -1 to 1 in each direction.
Because our vertex shader is simply copying our positionBuffer
values to gl_Position
the
triangle will be drawn at clip space coordinates
Converting from clip space to screen space if the canvas size happened to be 400x300 we'd get something like this
WebGL will now render that triangle. For every pixel it is about to draw WebGL will call our fragment shader.
Our fragment shader just sets gl_FragColor
to 1, 0, 0.5, 1
. Since the Canvas is an 8bit
per channel canvas that means WebGL is going to write the values [255, 0, 127, 255]
into the canvas.
Here's a live version
In the case above you can see our vertex shader is doing nothing but passing on our position data directly. Since the position data is already in clip space there is no work to do. If you want 3D it's up to you to supply shaders that convert from 3D to clip space because WebGL is only a rasterization API.
You might be wondering why does the triangle start in the middle and go to toward the top right.
Clip space in x
goes from -1 to +1. That means 0 is in the center and positive values will
be to the right of that.
As for why it's on the top, in clip space -1 is at the bottom and +1 is at the top. That means 0 is in the center and so positive numbers will be above the center.
For 2D stuff you would probably rather work in pixels than clip space so let's change the shader so we can supply the position in pixels and have it convert to clip space for us. Here's the new vertex shader
Some things to notice about the changes. We changed a_position
to a vec2
since we're
only using x
and y
anyway. A vec2
is similar to a vec4
but only has x
and y
.
Next we added a uniform
called u_resolution
. To set that we need to look up its location.
The rest should be clear from the comments. By setting u_resolution
to the resolution
of our canvas the shader will now take the positions we put in positionBuffer
supplied
in pixels coordinates and convert them to clip space.
Now we can change our position values from clip space to pixels. This time we're going to draw a rectangle made from 2 triangles, 3 points each.
And after we set which program to use we can set the value for the uniform we created.
gl.useProgram
is like gl.bindBuffer
above in that it sets the current program. After
that all the gl.uniformXXX
functions set uniforms on the current program.
And of course to draw 2 triangles we need to have WebGL call our vertex shader 6 times
so we need to change the count
to 6
.
And here it is
Note: This example and all following examples use webgl-utils.js
which contains functions to compile and link the shaders. No reason to clutter the examples
with that boilerplate code.
Again you might notice the rectangle is near the bottom of that area. WebGL considers positive Y as up and negative Y as down. In clip space the bottom left corner -1,-1. We haven't changed any signs so with our current math 0, 0 becomes the bottom left corner. To get it to be the more traditional top left corner used for 2d graphics APIs we can just flip the clip space y coordinate.
And now our rectangle is where we expect it.
Let's make the code that defines a rectangle into a function so we can call it for different sized rectangles. While we're at it we'll make the color settable.
First we make the fragment shader take a color uniform input.
And here's the new code that draws 50 rectangles in random places and random colors.
And here's the rectangles.
I hope you can see that WebGL is actually a pretty simple API. Okay, simple might be the wrong word. What it does is simple. It just executes 2 user supplied functions, a vertex shader and fragment shader and draws triangles, lines, or points. While it can get more complicated to do 3D that complication is added by you, the programmer, in the form of more complex shaders. The WebGL API itself is just a rasterizer and conceptually fairly simple.
We covered a small example that showed how to supply data in an attribute and 2 uniforms. It's common to have multiple attributes and many uniforms. Near the top of this article we also mentioned varyings and textures. Those will show up in subsequent lessons.
Before we move on I want to mention that for most applications updating
the data in a buffer like we did in setRectangle
is not common. I used that
example because I thought it was easiest to explain since it shows pixel coordinates
as input and demonstrates doing a small amount of math in GLSL. It's not wrong, there
are plenty of cases where it's the right thing to do, but you should keep reading to find out
the more common way to position, orient and scale things in WebGL.
If you're new to web development or even if you're not please check out Setup and Installation for some tips on how to do WebGL development.
If you're 100% new to WebGL and have no idea what GLSL is or shaders or what the GPU does then checkout the basics of how WebGL really works. You might also want to take a look at this interactive state diagram for another way of understanding how WebGL works.
You should also, at least briefly read about the boilerplate code used here that is used in most of the examples. You should also at least skim how to draw multiple things to give you some idea of how more typical WebGL apps are structured because unfortunately nearly all the examples only draw one thing and so do not show that structure.
Otherwise from here you can go in 2 directions. If you are interested in image processing I'll show you how to do some 2D image processing. If you are interested in learning about translation, rotation and scale and eventually 3D then start here.