WebGL 教程

概述

WebGL (Web 图形库),基于 OpenGL ES 2.0,为渲染 3D 图形提供了 JavaScript API。 大多数桌面和移动浏览器都支持它。 Internet Explorer 宣布,Windows 8.1 中的IE 11 也支持。 Tizen 平台也完全支持WebGL。 这项技术使开发人员能够创建可在多个平台运行的应用程序。

随着 HTML5的发展,web 浏览器成为了一个平台,将挑战本机代码的功能和性能。 从 HTML 到Html5 经历了很长的路,如语法清除,新的 JavaScript API,与卓越的多媒体支持移动功能。 WebGL 丢弃了 HTML5 提供的一部分先进图像技术,包括 CSS2D 和画布元素。

JavaScript 虚拟机 (VM) 的性能在使用HTML5开发的 web 应用程序中起了至关重要的作用。 由于WebGL和 Canvas2D 均是 JavaScript Api,他们运行的方式和执行它们的 JavaScript 代码直接相关。 为了实现这一目标,JavaScript VM 走过了漫长的道路。

最新的 web 平台正在为多线程编程提供一些功能:网络工作者,JavaScript 在后台运行,并独立于相同页面中执行的其他用户接口脚本 。 web工作者在高效处理多核 Cpu方面非常有用。 网络套接字提供基于TCP / IP的全双工通信。 这些功能与本地存储区和很多其他内容提供了一个丰富的平台,使我们能够开发高端 web 应用程序。

3D

在 3D 中,你可能知道,在三维坐标系统中呈现的数据渲染 2D 图像。 依据 3D 数据变化(源自动画或者用户交互),3D 场景是实时显示的。 顾名思义,3 维坐标系统采用了额外的坐标 z (2D x,y) 提供屏幕的深度。 如果您熟悉 2D 系统,2D 的画布上,这里正的y 总是从底部到顶部。 同样正的 x 从左到右,正的 z 从屏幕里到屏幕外。

WebGL

要开始处理 WebGL,您应该熟悉一些概念: 3D 网格 (3D 建模)、 顶点、 纹理、 照明、 变换、 矩阵、 视区、 虚拟摄像机和着色器。

网格、 顶点、 纹理和照明

3D 网格被定义为由多个顶点形成的一个对象,它定义了由一个或多个多边形 (用直线闭合区域) 形成的形状。 可以使用纯色或纹理表示网格的表面。 纹理基本上是 3D 网格表面的外观和感觉。 它可以使用位图来表示。 照明可以定义为基于光照信息,在场景的表面上改变颜色的过程。

例如,可以用数个多边形将人脸当作网格一样进行绘制,可以使用单一的颜色或位图表示脸部皮肤表面。 如果光源从脸上发出的话,照明可以用于显示阴影。

变换和矩阵

若要创建动画,那么3D 场景中的对象应当是移动的。 变换功能通过移动相应的网格,在不修改顶点的情况下实现动画效果。 启用渲染网格来实现旋转和移动。 使用矩阵来表示转换。 一个矩阵包括的一组代表行和列的数据,用于计算转换后顶点的位置。 即使你并不熟悉线性代数也没有关系,该工具包提供了在举证上执行旋转、 缩放或移动的功能。

虚拟摄像机和角度

呈现的图形场景取决于用户的观看角度,这个视角通常称为虚拟摄像机。 它通常使用一组矩阵,其中一个定义照相机 (用户的角度来看)的位置和方向,其他代表摄像机三维坐标转换为 2D 绘图区域的视区。 基本上,一个视口被定义为用于显示3D场景的二维矩形的用户视角或者虚拟摄像机。

着色器

着色器是 使用类似'C' 高级编程语言编写的程序代码,它实现了更新位置、 色相、 饱和度、 亮度和所有像素的对比度、 顶点或实时纹理的算法。 它基本上是用来将实际像素绘制到屏幕上。 着色语言基本上用于
编写图形处理单元。 着色器提供对 GPU 实现高性能和硬件加速的直接访问。 WebGL 使用 GLSL,OpenGL 着色语言。

WebGL 应用

WebGL 使用 HTML5<canvas>元素绘制 3D 图形。 典型的WebGL 应用包括的一组函数用于

  • 初始化 WebGL — — 创建一个画布并获得其上下文
  • 创建用于存储要呈现的数据的缓冲区
  • 创建着色器,以实现绘图算法
  • 绘制场景

你的 HTML 代码应该看起来像这样

<body onload="simpleWebGL()">
  <canvas id="mainCanvas" style="border: none;" width="300"
    height="300"<>/canvas> </body>

当您运行该应用程序时,将在load上调用 simpleWebGL() 函数。 在此函数中,你可以与使用 document.getElementById() 将画布元素关联和 DOM 对象关联在一起。 调用函数初始化 GL时,将创建的全局对象作为参数传递进去。 初始化着色器和缓冲区来存储数据,用于绘画。 在全局对象上调用 clearColor() 和 enable()方法清除屏幕并启用深度测试(隐藏了屏幕后绘制东西的详细信息)。 最后方法 tick(),将调用 requestAnimFrame() 并绘制场景。

function simpleWebGL() {
    var canvas = document.getElementById("mainCanvas");
    if (canvas.getContext) {
    initializeGL(canvas);
    initShaders()
    initBuffers();

    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.enable(gl.DEPTH_TEST);

    tick();
    }
}

initializeGL()

所有 WebGL 呈现的内容都发生在 WebGL 绘图上下文中。 该上下文反映了HTML5 Canvas中提供的2D绘图上下文。 您可以使用变量 viewportWidth 和 viewportHeight 设置 WebGl 的视区。

var gl;
function initializeGL(canvas) {
    try {
        gl= canvas.getContext("experimental-webgl");
        gl.viewportWidth = canvas.width;
        gl.viewportHeight = canvas.height;
    } catch (e) {
    }
    if (!gl) {
        alert("Failed to initialize WebGL");
    }
}

initShaders()

您可以找到如下使用 GLSL 语言编写的代码。 第一个着色器只是告诉显卡设置中等精度的浮点数。

<script id="shader-fs" type="x-shader/x-fragment">
    precision mediump float;

    varying vec4 vColor;

    void main(void) {
        gl_FragColor = vColor;
    }
</script>

第二个着色器是一个顶点着色器和它的显卡代码。 它有两个统一的变量uMVMatrix和uPMatrix,它可以在脚本外访问。 为了更好地理解,该脚本像其区域一样,保留为一个带有统一变量的对象。 绘制场景中的每一个顶点,都会调用该着色器。
使用vertexPositionAttribute. main()函数中的代码使用模型视图和投影矩阵复制了顶点的位置,并返回该顶点的最终位置。

<script id="shader-vs" type="x-shader/x-vertex">
    attribute vec3 aVertexPosition;
    attribute vec4 aVertexColor;

    uniform mat4 uMVMatrix;
    uniform mat4 uPMatrix;

    varying vec4 vColor;

    void main(void) {
        gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
        vColor = aVertexColor;
    }
</script>

它使用getShader获取局部渲染器和一个顶点渲染器。 然后将它们附加到一个 webGL 程序。 一个程序是运行在显卡上的东西。 每个程序可以与一个部分和一个顶点着色器相关联。 它也得到两个统一变量的位置。

var shaderProgram;

function initShaders() {
    var fragmentShader = getShader(gl, "shader-fs");
    var vertexShader = getShader(gl, "shader-vs");

    shaderProgram = gl.createProgram();
    gl.attachShader(shaderProgram, vertexShader);
    gl.attachShader(shaderProgram, fragmentShader);
    gl.linkProgram(shaderProgram);

    if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
        alert("Could not initialise shaders");
    }

    gl.useProgram(shaderProgram);

    shaderProgram.vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition");
    gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute);

    shaderProgram.vertexColorAttribute = gl.getAttribLocation(shaderProgram, "aVertexColor");
    gl.enableVertexAttribArray(shaderProgram.vertexColorAttribute);

    shaderProgram.pMatrixUniform = gl.getUniformLocation(shaderProgram, "uPMatrix");
    shaderProgram.mvMatrixUniform = gl.getUniformLocation(shaderProgram, "uMVMatrix");
}

顾名思义,getShader() 在HTML 页面中查找匹配 ID的元素,并作为参数传递。 它然后将编译所提取的代码来创建一个片段或一个顶点渲染器,并在显卡上运行。

getShader()

function getShader(gl, id) {
    var shaderScript = document.getElementById(id);
    if (!shaderScript) {
        return null;
    }

    var str = "";
    var k = shaderScript.firstChild;
    while (k) {
        if (k.nodeType == 3) {
            str += k.textContent;
        }
        k = k.nextSibling;
    }

    var shader;
    if (shaderScript.type == "x-shader/x-fragment") {
        shader = gl.createShader(gl.FRAGMENT_SHADER);
    } else if (shaderScript.type == "x-shader/x-vertex") {
        shader = gl.createShader(gl.VERTEX_SHADER);
    } else {
        return null;
    }

    gl.shaderSource(shader, str);
    gl.compileShader(shader);

    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
        alert(gl.getShaderInfoLog(shader));
        return null;
    }

    return shader;
}

initBuffers()

您可以声明两个全局变量 pyramidVertexPositionBuffer 和 cubePositionBuffer 来存储数据。 一个单一的变量可以达到目的,但两个变量可以用来使事情更简单。 你可以调用 gl.createBuffer()在显卡中创建一个缓冲区,并且可以绑定到 webGL 数组缓冲区。 接下来定义顶点位置。 对 gl.bufferData() 的调用用于创建 float32Array,它是pyramidVertexPositionBuffer,可以用来填充webGL 缓冲区
数组 (已绑定)。 您可以使用numItems 和 itemSize指定多个顶点的位置。 同样,你可以为立方体设置缓冲区。

var pyramidVertexPositionBuffer;
var pyramidVertexColorBuffer;
var cubePositionBuffer;
var cubeColorBuffer;
var cubeIndexBuffer;

function initBuffers() {
    pyramidVertexPositionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexPositionBuffer);
    var vertices = [
        // Front face
         0.0,  1.0,  0.0,
        -1.0, -1.0,  1.0,
         1.0, -1.0,  1.0,

        // Right face
         0.0,  1.0,  0.0,
         1.0, -1.0,  1.0,
         1.0, -1.0, -1.0,

        // Back face
         0.0,  1.0,  0.0,
         1.0, -1.0, -1.0,
        -1.0, -1.0, -1.0,

        // Left face
         0.0,  1.0,  0.0,
        -1.0, -1.0, -1.0,
        -1.0, -1.0,  1.0
    ];
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
    pyramidVertexPositionBuffer.itemSize = 3;
    pyramidVertexPositionBuffer.numItems = 12;

    pyramidVertexColorBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexColorBuffer);
    var colors = [
        // Front face
        1.0, 0.0, 0.0, 1.0,
        0.0, 1.0, 0.0, 1.0,
        0.0, 0.0, 1.0, 1.0,

        // Right face
        1.0, 0.0, 0.0, 1.0,
        0.0, 0.0, 1.0, 1.0,
        0.0, 1.0, 0.0, 1.0,

        // Back face
        1.0, 0.0, 0.0, 1.0,
        0.0, 1.0, 0.0, 1.0,
        0.0, 0.0, 1.0, 1.0,

        // Left face
        1.0, 0.0, 0.0, 1.0,
        0.0, 0.0, 1.0, 1.0,
        0.0, 1.0, 0.0, 1.0
    ];
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
    pyramidVertexColorBuffer.itemSize = 4;
    pyramidVertexColorBuffer.numItems = 12;

    cubePositionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, cubePositionBuffer);
    vertices = [
        // Front face
        -1.0, -1.0,  1.0,
         1.0, -1.0,  1.0,
         1.0,  1.0,  1.0,
        -1.0,  1.0,  1.0,

        // Back face
        -1.0, -1.0, -1.0,
        -1.0,  1.0, -1.0,
         1.0,  1.0, -1.0,
         1.0, -1.0, -1.0,

        // Top face
        -1.0,  1.0, -1.0,
        -1.0,  1.0,  1.0,
         1.0,  1.0,  1.0,
         1.0,  1.0, -1.0,

        // Bottom face
        -1.0, -1.0, -1.0,
         1.0, -1.0, -1.0,
         1.0, -1.0,  1.0,
        -1.0, -1.0,  1.0,

        // Right face
         1.0, -1.0, -1.0,
         1.0,  1.0, -1.0,
         1.0,  1.0,  1.0,
         1.0, -1.0,  1.0,

        // Left face
        -1.0, -1.0, -1.0,
        -1.0, -1.0,  1.0,
        -1.0,  1.0,  1.0,
        -1.0,  1.0, -1.0
    ];
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
    cubePositionBuffer.itemSize = 3;
    cubePositionBuffer.numItems = 24;

    cubeColorBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, cubeColorBuffer);
    colors = [
        [1.0, 0.0, 0.0, 1.0], // Front face
        [1.0, 1.0, 0.0, 1.0], // Back face
        [0.0, 1.0, 0.0, 1.0], // Top face
        [1.0, 0.5, 0.5, 1.0], // Bottom face
        [1.0, 0.0, 1.0, 1.0], // Right face
        [0.0, 0.0, 1.0, 1.0]  // Left face
    ];
    var unpackedColors = [];
    for (var i in colors) {
        var color = colors[i];
        for (var j=0; j < 4; j++) {
            unpackedColors = unpackedColors.concat(color);
        }
    }
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(unpackedColors), gl.STATIC_DRAW);
    cubeColorBuffer.itemSize = 4;
    cubeColorBuffer.numItems = 24;

    cubeIndexBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeIndexBuffer);
    var cubeVertexIndices = [
        0, 1, 2,      0, 2, 3,    // Front face
        4, 5, 6,      4, 6, 7,    // Back face
        8, 9, 10,     8, 10, 11,  // Top face
        12, 13, 14,   12, 14, 15, // Bottom face
        16, 17, 18,   16, 18, 19, // Right face
        20, 21, 22,   20, 22, 23  // Left face
    ];
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(cubeVertexIndices), gl.STATIC_DRAW);
    cubeIndexBuffer.itemSize = 1;
    cubeIndexBuffer.numItems = 36;
}

刻度线函数用来绘制场景和执行动画。 在谷歌 webGL utils 中定义的函数 requestAnimFrame(),用作替代 setInterval ,以下例程执行绘图和动画。

function tick () {
    requestAnimFrame(tick);
    drawScene();
    animatePyramid();
}

drawScene()

在drawScene函数()中,您可以真实地使用缓存来绘制场景。 前两个语句设置视区,并分别清除屏幕。 您可以设置您想要查看的 th 屏幕角度 (虚拟摄像机位置)。 下面的行设置字视图区域为 45 和画布上感兴趣的区域从0.1。
到 100 个单位的观测点。 对 mat4.identity() 的调用设置相对于屏幕中心的绘图位置。 单位矩阵像起始位置一样开始绘制场景,mvMatrix是模型视图矩阵。 现在,调用translate将金字塔向左移动1.5单位向左(负x轴)和8个单位距离
从角度来看 (负 z 轴)。 mat4.translate使用这些参数乘以给定矩阵(标识)的转换矩阵。

对 bindBuffer 的调用来指定当前缓冲区上它后面运行的代码。 该setMatrixUniforms函数移动顶点位置到显卡上。 g.drawArrays将根据给定矩阵实际绘制三角形。

var rPyramid = 0;
var rCube = 0;

function drawScene() {
    gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

    mat4.perspective(45, gl.viewportWidth / gl.viewportHeight, 0.1, 100.0, pPyramidMatrix);

    mat4.identity(mvPyramidMatrix);

    mat4.translate(mvPyramidMatrix, [-1.5, 0.0, -8.0]);

    mvPyramidPushMatrix();
    mat4.rotate(mvPyramidMatrix, degToRadPyramid(rPyramid), [0, 1, 0]);

    gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexPositionBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, pyramidVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

    gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexColorBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, pyramidVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0);

    setPyramidMatrixUniforms();
    gl.drawArrays(gl.TRIANGLES, 0, pyramidVertexPositionBuffer.numItems);

    mvPyramidPopMatrix();

    mat4.translate(mvPyramidMatrix, [3.0, 0.0, 0.0]);

    mvPyramidPushMatrix();
    mat4.rotate(mvPyramidMatrix, degToRadPyramid(rCube), [1, 1, 1]);

    gl.bindBuffer(gl.ARRAY_BUFFER, cubePositionBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, cubePositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

    gl.bindBuffer(gl.ARRAY_BUFFER, cubeColorBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, cubeColorBuffer.itemSize, gl.FLOAT, false, 0, 0);

    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeIndexBuffer);
    setPyramidMatrixUniforms();
    gl.drawElements(gl.TRIANGLES, cubeIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);

    mvPyramidPopMatrix();
}

变量 mvPyramidMatrix 和 pPyramidMatrix 分别表示模型视图矩阵和投影矩阵。 mat4.create() 初始化矩阵。

var mvPyramidMatrix = mat4.create();
var mvPyramidMatrixStack = [];
var pPyramidMatrix = mat4.create();

function mvPyramidPushMatrix() {
    var copy = mat4.create();
    mat4.set(mvPyramidMatrix, copy);
    mvPyramidMatrixStack.push(copy);
}

function mvPyramidPopMatrix() {
    if (mvPyramidMatrixStack.length == 0) {
        throw "Invalid popMatrix!";
    }
    mvPyramidMatrix = mvPyramidMatrixStack.pop();
}

function setPyramidMatrixUniforms() {
    gl.uniformMatrix4fv(shaderProgram.pMatrixUniform, false, pPyramidMatrix);
    gl.uniformMatrix4fv(shaderProgram.mvMatrixUniform, false, mvPyramidMatrix);
}

function degToRadPyramid(degrees) {
    return degrees * Math.PI / 180;
}

animatePyramid()

在动画中,您可以为 aka 金字塔设置和方形设置旋转的角度。 设置三角形以每秒 90 度旋转,方形以每秒75度旋转。

var last_time = 0;

function animatePyramid() {
    var timeNow = new Date().getTime();
    if (last_time != 0) {
        var elapsed = timeNow - last_time;

        rPyramid += (90 * elapsed) / 1000.0;
        rCube -= (75 * elapsed) / 1000.0;
    }
    last_time = timeNow;
}

参考