Movatterモバイル変換


[0]ホーム

URL:


  1. 面向开发者的 Web 技术
  2. Web API
  3. WebGL:web 中的 2D 的 3D 图形
  4. 一个 2D WebGL 动画的基础示例

此页面由社区从英文翻译而来。了解更多并加入 MDN Web Docs 社区。

View in EnglishAlways switch to English

一个 2D WebGL 动画的基础示例

在这个 WebGL 示例中,我们创建一个画布,并在其中使用 WebGL 渲染旋转正方形。我们用来表示场景的坐标系与画布的坐标系相同。也就是说,(0, 0)这个坐标在左上角,右下角是坐标在(600, 460)。

旋转正方形示例

Vertex shader

首先,让我们看一下顶点着色器。它的工作如同以往,是将我们用于场景的坐标转换为剪贴空间的坐标(即系统中的(0,0)位于上下文的中心,每个轴从 -1.0 扩展到 1.0,而不管上下文的实际大小)。

html
<script 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>

主程序与我们共享属性 aVertexPosition,它是顶点在其使用的任何坐标系中的位置。我们需要转换这些值,以便位置的两个组件都在 -1.0 到 1.0 的范围内。通过乘以基于上下文宽高比的缩放因子,可以很容易地完成此操作。我们很快就会看到这个计算。

我们也可以通过一次变换来旋转这个图形。The rotated position of the vertex is computed by applying the rotation vector, found in the uniformuRotationVector, that's been computed by the JavaScript code.

Then the final position is computed by multiplying the rotated position by the scaling vector provided by the JavaScript code inuScalingFactor. The values ofz andw are fixed at 0.0 and 1.0, respectively, since we're drawing in 2D.

The standard WebGL globalgl_Position is then set to the transformed and rotated vertex's position.

Fragment shader

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:

html
<script type="x-shader/x-fragment">  #ifdef GL_ES    precision highp float;  #endif  uniform vec4 uGlobalColor;  void main() {    gl_FragColor = uGlobalColor;  }</script>

This starts by specifying the precision of thefloat type, as required. Then we set the globalgl_FragColor to the value of the uniformuGlobalColor, which is set by the JavaScript code to the color being used to draw the square.

HTML

The HTML consists solely of the<canvas> that we'll obtain a WebGL context on.

html
<canvas width="600" height="460">  Oh no! Your browser doesn't support canvas!</canvas>

Globals and initialization

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.

js
let gl = null;let glCanvas = null;// Aspect ratio and coordinate system// detailslet aspectRatio;let currentRotation = [0, 1];let currentScale = [1.0, 1.0];// Vertex informationlet vertexArray;let vertexBuffer;let vertexNumComponents;let vertexCount;// Rendering data shared with the// scalers.let uScalingFactor;let uGlobalColor;let uRotationVector;let aVertexPosition;// Animation timinglet previousTime = 0.0;let degreesPerSecond = 90.0;

Initializing the program is handled through aload event handler calledstartup():

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;  animateScene();}

After getting the WebGL context,gl, 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 arrayshaderSet contains a list of objects, each describing one shader function to be compiled into the program. Each function has a type (one ofgl.VERTEX_SHADER orgl.FRAGMENT_SHADER) and an ID (the ID of the<script> element containing the shader's code).

The shader set is passed into the functionbuildShaderProgram(), which returns the compiled and linked shader program. We'll look at how this works next.

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[0, 1], and the scaling vector to[1.0, aspectRatio]. 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.

The array of vertices is created next, as aFloat32Array with six coordinates (three 2D vertices) per triangle to be drawn, for a total of 12 values.

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.

Once the vertex array has been created, we create a new GL buffer to contain them by callinggl.createBuffer(). We bind the standard WebGL array buffer reference to that by callinggl.bindBuffer() and then copy the vertex data into the buffer usinggl.bufferData(). The usage hintgl.STATIC_DRAW 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.

With the vertex data now provided to WebGL, we setvertexNumComponents to the number of components in each vertex (2, since they're 2D vertexes) andvertexCount to the number of vertexes in the vertex list.

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.

Finally,animateScene() is called to render the first frame and schedule the rendering of the next frame of the animation.

Compiling and linking the shader program

ThebuildShaderProgram() 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.

js
function buildShaderProgram(shaderInfo) {  const program = gl.createProgram();  shaderInfo.forEach((desc) => {    const 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;}

First,gl.createProgram() is called to create a new, empty, GLSL program.

Then, for each shader in the specified list of shaders, we call acompileShader() 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<script> 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 intogl.attachShader().

备注:We could go a step farther here, actually, and look at the value of the<script> element'stype attribute to determine the shader type.

Once all of the shaders are compiled, the program is linked usinggl.linkProgram().

If an error occurrs while linking the program, the error message is logged to console.

Finally, the compiled program is returned to the caller.

Compiling an individual shader

ThecompileShader() function, below, is called bybuildShaderProgram() to compile a single shader.

js
function compileShader(id, type) {  const code = document.getElementById(id).firstChild.nodeValue;  const 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;}

The code is fetched from the HTML document by obtaining the value of the text node contained within the<script> element with the specified ID. Then a new shader of the specified type is created usinggl.createShader().

The source code is sent into the new shader by passing it intogl.shaderSource(), and then the shader is compiled usinggl.compileShader()

Compile errors are logged to the console. Note the use of atemplate literal string to insert the correct shader type string into the message that gets generated. The actual error details are obtained by callinggl.getShaderInfoLog().

Finally, the compiled shader is returned to the caller (which is thebuildShaderProgram() function.

Drawing and animating the scene

TheanimateScene() function is called to render each animation frame.

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);  const 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);  requestAnimationFrame((currentTime) => {    const deltaAngle =      ((currentTime - previousTime) / 1000.0) * degreesPerSecond;    currentAngle = (currentAngle + deltaAngle) % 360;    previousTime = currentTime;    animateScene();  });}

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<canvas>, callclearColor() to set the color to use when clearing content, then we clear the buffer withclear().

Next, the current rotation vector is computed by converting the current rotation in degrees (currentAngle) intoradians, then setting the first component of the rotation vector to thesine of that value and the second component to thecosine. ThecurrentRotation vector is now the location of the point on theunit circle located at the anglecurrentAngle.

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 (withgetUniformLocation()).

The uniform nameduScalingFactor is set to thecurrentScale 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 usinguniform2fv() (since this is a 2-value floating-point vector).

uRotationVector is set to the current rotation vector (currentRotation), also usinguniform2fv().

uGlobalColor is set usinguniform4fv() 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).

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 callingbindBuffer(). Then the vertex position attribute's index is obtained from the shader program by callinggetAttribLocation().

With the index of the vertex position attribute now available inaVertexPosition, we callenableVertexAttribArray() to enable the position attribute so it can be used by the shader program (in particular, by the vertex shader).

Then the vertex buffer is bound to theaVertexPosition attribute by callingvertexAttribPointer(). This step is not obvious, since this binding is almost a side effect. But as a result, accessingaVertexPosition now obtains data from the vertex buffer.

With the association in place between the vertex buffer for our shape and theaVertexPosition attribute used to deliver vertexes one by one into the vertex shader, we're ready to draw the shape by callingdrawArrays().

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 callingrequestAnimationFrame(), which asks that a callback function be executed the next time the browser is ready to update the screen.

OurrequestAnimationFrame() callback receives as input a single parameter,currentTime, which specifies the time at which the frame drawing began. We use that and the saved time at which the last frame was drawn,previousTime, along with the number of degrees per second the square should rotate (degreesPerSecond) to calculate the new value ofcurrentAngle. Then the value ofpreviousTime is updated and we callanimateScene() to draw the next frame (and in turn schedule the next frame to be drawn, ad infinitum).

结果

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.

参见

Help improve MDN

Learn how to contribute

This page was last modified on byMDN contributors.


[8]ページ先頭

©2009-2026 Movatter.jp