1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
|
---
title: 一个 2D WebGL 动画的基础示例
slug: Web/API/WebGL_API/Basic_2D_animation_example
tags:
- 测试翻译
translation_of: Web/API/WebGL_API/Basic_2D_animation_example
---
<div>{{WebGLSidebar}}</div>
<div id="live-sample">
<p>在这个WebGL示例中,我们创建一个画布,并在其中使用WebGL渲染旋转正方形。我们用来表示场景的坐标系与画布的坐标系相同。也就是说,(0, 0)这个坐标在左上角,右下角是坐标在(600, 460)。</p>
<h2 id="Vertex_shader">Vertex shader</h2>
<p>首先,让我们看一下顶点着色器。它的工作如同以往,是将我们用于场景的坐标转换为剪贴空间的坐标(即系统中的(0,0)位于上下文的中心,每个轴从-1.0扩展到1.0,而不管上下文的实际大小)。</p>
<pre class="brush: html"><script id="vertex-shader" type="x-shader/x-vertex">
attribute vec2 aVertexPosition;
uniform vec2 uScalingFactor;
uniform vec2 uRotationVector;
void main() {
vec2 rotatedPosition = vec2(
aVertexPosition.x * uRotationVector.y +
aVertexPosition.y * uRotationVector.x,
aVertexPosition.y * uRotationVector.y -
aVertexPosition.x * uRotationVector.x
);
gl_Position = vec4(rotatedPosition * uScalingFactor, 0.0, 1.0);
}
</script></pre>
<p dir="ltr" id="tw-target-text">主程序与我们共享属性aVertexPosition,它是顶点在其使用的任何坐标系中的位置。我们需要转换这些值,以便位置的两个组件都在-1.0到1.0的范围内。通过乘以基于上下文宽高比的缩放因子,可以很容易地完成此操作。我们很快就会看到这个计算。</p>
<p>我们也可以通过一次变换来旋转这个图形。 The rotated position of the vertex is computed by applying the rotation vector, found in the uniform <code>uRotationVector</code>, that's been computed by the JavaScript code.</p>
<p>Then the final position is computed by multiplying the rotated position by the scaling vector provided by the JavaScript code in <code>uScalingFactor</code>. The values of <code>z</code> and <code>w</code> are fixed at 0.0 and 1.0, respectively, since we're drawing in 2D.</p>
<p>The standard WebGL global <code>gl_Position</code> is then set to the transformed and rotated vertex's position.</p>
<h2 id="Fragment_shader">Fragment shader</h2>
<p>Next comes the fragment shader. Its role is to return the color of each pixel in the shape being rendered. Since we're drawing a solid, untextured object with no lighting applied, this is exceptionally simple:</p>
<pre class="brush: html"><script id="fragment-shader" type="x-shader/x-fragment">
#ifdef GL_ES
precision highp float;
#endif
uniform vec4 uGlobalColor;
void main() {
gl_FragColor = uGlobalColor;
}
</script></pre>
<p>This starts by specifying the precision of the <code>float</code> type, as required. Then we set the global <code>gl_FragColor</code> to the value of the uniform <code>uGlobalColor</code>, which is set by the JavaScript code to the color being used to draw the square.</p>
<h2 id="HTML">HTML</h2>
<p>The HTML consists solely of the {{HTMLElement("canvas")}} that we'll obtain a WebGL context on.</p>
<pre class="brush: html"><canvas id="glcanvas" width="600" height="460">
Oh no! Your browser doesn't support canvas!
</canvas></pre>
<h2 id="JavaScript">JavaScript</h2>
<h3 id="Globals_and_initialization">Globals and initialization</h3>
<p>First, the global variables. We won't discuss these here; instead, we'll talk about them as they're used in the code to come.</p>
<pre class="brush: js">let gl = null;
let glCanvas = null;
// Aspect ratio and coordinate system
// details
let aspectRatio;
let currentRotation = [0, 1];
let currentScale = [1.0, 1.0];
// Vertex information
let vertexArray;
let vertexBuffer;
let vertexNumComponents;
let vertexCount;
// Rendering data shared with the
// scalers.
let uScalingFactor;
let uGlobalColor;
let uRotationVector;
let aVertexPosition;
// Animation timing
let previousTime = 0.0;
let degreesPerSecond = 90.0;
</pre>
<p>Initializing the program is handled through a {{event("load")}} event handler called <code>startup()</code>:</p>
<pre class="brush: js">window.addEventListener("load", startup, false);
function startup() {
glCanvas = document.getElementById("glcanvas");
gl = glCanvas.getContext("webgl");
const shaderSet = [
{
type: gl.VERTEX_SHADER,
id: "vertex-shader"
},
{
type: gl.FRAGMENT_SHADER,
id: "fragment-shader"
}
];
shaderProgram = buildShaderProgram(shaderSet);
aspectRatio = glCanvas.width/glCanvas.height;
currentRotation = [0, 1];
currentScale = [1.0, aspectRatio];
vertexArray = new Float32Array([
-0.5, 0.5, 0.5, 0.5, 0.5, -0.5,
-0.5, 0.5, 0.5, -0.5, -0.5, -0.5
]);
vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertexArray, gl.STATIC_DRAW);
vertexNumComponents = 2;
vertexCount = vertexArray.length/vertexNumComponents;
currentAngle = 0.0;
rotationRate = 6;
animateScene();
}</pre>
<p>After getting the WebGL context, <code>gl</code>, we need to begin by building the shader program. Here, we're using code designed to let us add multiple shaders to our program quite easily. The array <code>shaderSet</code> contains a list of objects, each describing one shader function to be compiled into the program. Each function has a type (one of <code>gl.VERTEX_SHADER</code> or <code>gl.FRAGMENT_SHADER</code>) and an ID (the ID of the {{HTMLElement("script")}} element containing the shader's code).</p>
<p>The shader set is passed into the function <code>buildShaderProgram()</code>, which returns the compiled and linked shader program. We'll look at how this works next.</p>
<p>Once the shader program is built, we compute the aspect ratio of our context by dividing its width by its height. Then we set the current rotation vector for the animation to <code>[0, 1]</code>, and the scaling vector to <code>[1.0, aspectRatio]</code>. The scaling vector, as we saw in the vertex shader, is used to scale the coordinates to fit the -1.0 to 1.0 range.</p>
<p>The array of vertices is created next, as a {{jsxref("Float32Array")}} with six coordinates (three 2D vertices) per triangle to be drawn, for a total of 12 values.</p>
<p>As you can see, we're using a coordinate system of -1.0 to 1.0 for each axis. Why, you may ask, do we need to do any adjustments at all? This is simply because our context is not square. We're using a context that's 600 pixels wide and 460 tall. Each of those dimensions is mapped to the range -1.0 to 1.0. Since the two axes aren't the same length, if we don't adjust the values of one of the two axes, the square will get stretched out in one direction or the other. So we need to normalize these values.</p>
<p>Once the vertex array has been created, we create a new GL buffer to contain them by calling {{domxref("WebGLRenderingContext.createBuffer", "gl.createBuffer()")}}. We bind the standard WebGL array buffer reference to that by calling {{domxref("WebGLRenderingContext.bindBuffer", "gl.bindBuffer()")}} and then copy the vertex data into the buffer using {{domxref("WebGLRenderingContext.bufferData", "gl.bufferData()")}}. The usage hint <code>gl.STATIC_DRAW</code> is specified, telling WebGL that the data will be set only one time and never modified, but will be used repeatedly. This lets WebGL consider any optimizations it can apply that may improve performance based on that information.</p>
<p>With the vertex data now provided to WebGL, we set <code>vertexNumComponents</code> to the number of components in each vertex (2, since they're 2D vertexes) and <code>vertexCount</code> to the number of vertexes in the vertex list.</p>
<p>Then the current rotation angle (in degrees) is set to 0.0, since we haven't performed any rotation yet, and the rotation speed (in degrees per screen refresh period, typically 60 FPS) is set to 6.</p>
<p>Finally, <code>animateScene()</code> is called to render the first frame and schedule the rendering of the next frame of the animation.</p>
<h3 id="Compiling_and_linking_the_shader_program">Compiling and linking the shader program</h3>
<h4 id="Constructing_and_linking_the_program">Constructing and linking the program</h4>
<p>The <code>buildShaderProgram()</code> function accepts as input an array of objects describing a set of shader functions to be compiled and linked into the shader program and returns the shader program after it's been built and linked.</p>
<pre class="brush: js">function buildShaderProgram(shaderInfo) {
let program = gl.createProgram();
shaderInfo.forEach(function(desc) {
let shader = compileShader(desc.id, desc.type);
if (shader) {
gl.attachShader(program, shader);
}
});
gl.linkProgram(program)
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.log("Error linking shader program:");
console.log(gl.getProgramInfoLog(program));
}
return program;
}</pre>
<p>First, {{domxref("WebGLRenderingContext.createProgram", "gl.createProgram()")}} is called to create a new, empty, GLSL program.</p>
<p>Then, for each shader in the specified list of shaders, we call a <code>compileShader()</code> function to compile it, passing into it the ID and type of the shader function to build. Each of those objects includes, as mentioned before, the ID of the <code><script></code> element the shader code is found in and the type of shader it is. The compiled shader is attached to the shader program by passing it into {{domxref("WebGLRenderingContext.attachShader", "gl.attachShader()")}}.</p>
<div class="note">
<p>We could go a step farther here, actually, and look at the value of the <code><script></code> element's <code>type</code> attribute to determine the shader type.</p>
</div>
<p>Once all of the shaders are compiled, the program is linked using {{domxref("WebGLRenderingContext.linkProgram", "gl.linkProgram()")}}.</p>
<p>If an error occurrs while linking the program, the error message is logged to console.</p>
<p>Finally, the compiled program is returned to the caller.</p>
<h4 id="Compiling_an_individual_shader">Compiling an individual shader</h4>
<p>The <code>compileShader()</code> function, below, is called by <code>buildShaderProgram()</code> to compile a single shader.</p>
<pre class="brush: js">function compileShader(id, type) {
let code = document.getElementById(id).firstChild.nodeValue;
let shader = gl.createShader(type);
gl.shaderSource(shader, code);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.log(`Error compiling ${type === gl.VERTEX_SHADER ? "vertex" : "fragment"} shader:`);
console.log(gl.getShaderInfoLog(shader));
}
return shader;
}</pre>
<p>The code is fetched from the HTML document by obtaining the value of the text node contained within the {{HTMLElement("script")}} element with the specified ID. Then a new shader of the specified type is created using {{domxref("WebGLRenderingContext.createShader", "gl.createShader()")}}.</p>
<p>The source code is sent into the new shader by passing it into {{domxref("WebGLRenderingContext.shaderSource", "gl.shaderSource()")}}, and then the shader is compiled using {{domxref("WebGLRenderingContext.compileShader", "gl.compileShader()")}}</p>
<p>Compile errors are logged to the console. Note the use of a <a href="/en-US/docs/Web/JavaScript/Reference/Template_literals">template literal</a> string to insert the correct shader type string into the message that gets generated. The actual error details are obtained by calling {{domxref("WebGLRenderingContext.getShaderInfoLog", "gl.getShaderInfoLog()")}}.</p>
<p>Finally, the compiled shader is returned to the caller (which is the <code>buildShaderProgram()</code> function.</p>
<h3 id="Drawing_and_animating_the_scene">Drawing and animating the scene</h3>
<p>The <code>animateScene()</code> function is called to render each animation frame.</p>
<pre class="brush: js">function animateScene() {
gl.viewport(0, 0, glCanvas.width, glCanvas.height);
gl.clearColor(0.8, 0.9, 1.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
let radians = currentAngle * Math.PI / 180.0;
currentRotation[0] = Math.sin(radians);
currentRotation[1] = Math.cos(radians);
gl.useProgram(shaderProgram);
uScalingFactor =
gl.getUniformLocation(shaderProgram, "uScalingFactor");
uGlobalColor =
gl.getUniformLocation(shaderProgram, "uGlobalColor");
uRotationVector =
gl.getUniformLocation(shaderProgram, "uRotationVector");
gl.uniform2fv(uScalingFactor, currentScale);
gl.uniform2fv(uRotationVector, currentRotation);
gl.uniform4fv(uGlobalColor, [0.1, 0.7, 0.2, 1.0]);
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
aVertexPosition =
gl.getAttribLocation(shaderProgram, "aVertexPosition");
gl.enableVertexAttribArray(aVertexPosition);
gl.vertexAttribPointer(aVertexPosition, vertexNumComponents,
gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLES, 0, vertexCount);
window.requestAnimationFrame(function(currentTime) {
let deltaAngle = ((currentTime - previousTime) / 1000.0)
* degreesPerSecond;
currentAngle = (currentAngle + deltaAngle) % 360;
previousTime = currentTime;
animateScene();
});
}</pre>
<p>The first thing that needs to be done in order to draw a frame of the animation is to clear the background to the desired color. In this case, we set the viewport based on the size of the {{HTMLElement("canvas")}}, call {{domxref("WebGLRenderingContext.clearColor", "clearColor()")}} to set the color to use when clearing content, then we clear the buffer with {{domxref("WebGLRenderingContext.clear", "clear()")}}.</p>
<p>Next, the current rotation vector is computed by converting the current rotation in degrees (<code>currentAngle</code>) into {{interwiki("wikipedia", "radians")}}, then setting the first component of the rotation vector to the {{interwiki("wikipedia", "sine")}} of that value and the second component to the {{interwiki("wikipedia", "cosine")}}. The <code>currentRotation</code> vector is now the location of the point on the {{interwiki("wikipedia", "unit circle")}} located at the angle <code>currentAngle</code>.</p>
<p>{{domxref("WebGLRenderingContext.useProgram", "useProgram()")}} is called to activate the GLSL shading program we established previously. Then we obtain the locations of each of the uniforms used to share information between the JavaScript code and the shaders (with {{domxref("WebGLRenderingContext.getUniformLocation", "getUniformLocation()")}}).</p>
<p>The uniform named <code>uScalingFactor</code> is set to the <code>currentScale</code> value previously computed; this, as you may recall, is the value used to adjust the coordinate system based on the aspect ratio of the context. This is done using {{domxref("WebGLRenderingContext.uniform2fv", "uniform2fv()")}} (since this is a 2-value floating-point vector).</p>
<p><code>uRotationVector</code> is set to the current rotation vector (<code>currentRotation)</code>, also using <code>uniform2fv()</code>.</p>
<p><code>uGlobalColor</code> is set using {{domxref("WebGLRenderingContext.uniform4fv", "uniform4fv()")}} to the color we wish to use when drawing the square. This is a 4-component floating-point vector (one component each for red, green, blue, and alpha).</p>
<p>Now that that's all out of the way, we can set up the vertex buffer and draw our shape, first, the buffer of vertexes that will be used to draw the triangles of the shape is set by calling {{domxref("WebGLRenderingContext.bindBuffer", "bindBuffer()")}}. Then the vertex position attribute's index is obtained from the shader program by calling {{domxref("WebGLRenderingContext.getAttribLocation", "getAttribLocation()")}}.</p>
<p>With the index of the vertex position attribute now available in <code>aVertexPosition</code>, we call <code>enableVertexAttribArray()</code> to enable the position attribute so it can be used by the shader program (in particular, by the vertex shader).</p>
<p>Then the vertex buffer is bound to the <code>aVertexPosition</code> attribute by calling {{domxref("WebGLRenderingContext.vertexAttribPointer", "vertexAttribPointer()")}}. This step is not obvious, since this binding is almost a side effect. But as a result, accessing <code>aVertexPosition</code> now obtains data from the vertex buffer.</p>
<p>With the association in place between the vertex buffer for our shape and the <code>aVertexPosition</code> attribute used to deliver vertexes one by one into the vertex shader, we're ready to draw the shape by calling {{domxref("WebGLRenderingContext.drawArrays", "drawArrays()")}}.</p>
<p>At this point, the frame has been drawn. All that's left to do is to schedule to draw the next one. That's done here by calling {{domxref("Window.requestAnimationFrame", "requestAnimationFrame()")}}, which asks that a callback function be executed the next time the browser is ready to update the screen.</p>
<p>Our <code>requestAnimationFrame()</code> callback receives as input a single parameter, <code>currentTime</code>, which specifies the time at which the frame drawing began. We use that and the saved time at which the last frame was drawn, <code>previousTime</code>, along with the number of degrees per second the square should rotate (<code>degreesPerSecond</code>) to calculate the new value of <code>currentAngle</code>. Then the value of <code>previousTime</code> is updated and we call <code>animateScene()</code> to draw the next frame (and in turn schedule the next frame to be drawn, ad infinitum).</p>
</div>
<h2 id="Result">Result</h2>
<p>This is a pretty simple example, since it's just drawing one simple object, but the concepts used here extend to much more complex animations.</p>
<p>{{EmbedLiveSample("live-sample", 660, 500)}}</p>
<h2 id="See_also">See also</h2>
<ul>
<li><a href="/en-US/docs/Web/API/WebGL_API">WebGL API</a></li>
<li><a href="/en-US/docs/Web/API/WebGL_API/Tutorial">WebGL tutorial</a></li>
</ul>
|