虽然OpenGL能够满足大多数计算机图形应用,在某些情况下,它可能不是最好的解决办法,这就是为什么OpenGL的API催生了另外两个API。第一个是OpenGL ES,‘ES’意为嵌入式子系统,他是桌面版openGL的裁剪版,在系统资源相对缺乏的嵌入式设备中使用,例如移动电话、平板电脑电视和其他彩色屏幕设备。 另一个API就是WebGL,这使得OpenGL风格的函数能够在大多数的web浏览器下通过javascript调用。

##WebGL简介

       WebGL通过提高性能,以及HTML5的Canvas元素中的3D渲染功能将OpenGL(更确切的说,OpenGL ES 2.0)带入互联网浏览器之中。OpenGL ES 2.0版的所有功能都可以找到他们的确切形式,除了因为使用JavaScript接口带来的必要小变化。

       WebG现在可以在几乎所有现代Web浏览器上工作(微软的IE10记以前版本需要一个插件,IE11毫无问题,而且IE11的webGL性能是极其惊人的)。本例只注重渲染,不讨论事件处理和交互。

##在一个HTML5页面中设置WebGL

       为了给WebGL提供一个“窗口”用于渲染,首先在网页创建一个HTML5 Canvas元素。在这个例子中,设置他的id为gl-canvas。在后面要使用它的id来初始化WebGL。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>webGL shaders</title>

    <style type="text/css">
        canvas { background: blue; }
    </style>
</head>

<body>
<canvas id="gl-canvas" width="512" height="512">
    您的浏览器不支持canvas
</canvas>
</body>
</html>

       假设这个能够在浏览器上正常工作,现在woman可以继续进行下一步:创建一个WebGL上下文。有多种方法可以做到这一点,但是在这里我们将使用Khronos组织提供的一个公共方法,webgl-utils.js。它可以非常方便的吧嵌入到你的WebGL程序里。它包括WebGLUtils(类似GLUT在openGL中的低位)及其扩展方法setupWebGL(),其中它可以很容易的在一个HTML5画布中启用WebGL。

       下面的例子展示了建立一个WebGL上下文,在所有支持WebGL的浏览器上都可以很好的工作。从setupWebGL()的返回值是一个JavaScript对象,它包含WebGL支持的所有OpenGL函数方法。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>webGL shaders</title>

    <style type="text/css">
        canvas {background: blue;}
    </style>

</head>

<script src="https://www.khronos.org/registry/webgl/sdk/demos/common/webgl-utils.js"></script>

<script>
    var canvas;
    var gl;

    window.onload = init;

    function init() {
        canvas = document.getElementById("gl-canvas");

        gl = WebGLUtils.setupWebGL(canvas);
        if(!gl) {alert("webGL不可用")}

        gl.viewport(0, 0, canvas.width, canvas.height);
        gl.clearColor(1.0, 0.0, 0.0, 1.0);
        gl.clear(gl.COLOR_BUFFER_BIT);
    }
</script>

<body>

<canvas id="gl-canvas" width="512" height="512">
    您的浏览器不支持webGL
</canvas>

</body>
</html>

       上例所指定在加载页面时(通过window.onload = init;)执行一个init()函数。init()函数获取gl-canvas的id,并把它传递给setupWebGL(),他将返回一个WebGL对象,可以用来判断初始化是否成功;返回false,则提示错误信息。架设webGL是可用的,我们将设置webGL的一些状态,并清除窗口——一旦webGL接管画布,所有内容由WebGL控制,并且画面变成红色。

       现在知道了WebGL已经得到支持了,接下来要扩展实例程序、初始化指定着色器、设置顶点缓冲,以及最后渲染。

##初始化WebGL里的着色器

       WebGL基于openGL ES2.0版,因此WebGL是一个基于着色器的API,和openGL一样,要求每个应用程序使用顶点和片段着色器来实现渲染,所以,你会遇到加载着色器的需求,和在openGL中一样。

       在WebGL的应用程序要包含顶点着色器和片段着色器,最简单的方法就是直接包含在HTML页面中,这个页面需要被正确的标注出来。与WebGL的着色器相关的两个mime类型,如下表所示。

<script>标记类型 着色器类型
x-shader/x-vertex 顶点着色器
x-shader/x-fragment 片段着色器

       对于WebGL程序,下面为主要的HTML页面,包括着色器源文件。他还引用了两个javascript文件,作用如下:

  • demo.js,其中包括应用程序的JavaScript实现(包括init函数的最终实现)
  • InitShaders.js,一个用来加载着色器的辅助函数,类似于LoadShaders()程序

       html程序源文件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>webGL shaders</title>

    <style type="text/css">
        canvas {background: blue;}
    </style>

</head>

<script id="vertex-shader" type="x-shader/x-vertex">
    #ifdef GL_ES
    precision highp float;
    #endif

    attribute vec4 vPos;
    attribute vec2 vTexCoord;
    uniform float uFrame;  //帧数
    varying vec2 texCoord;

    void main() {
        float angle = radians(uFrame);
        float c = cos(angle);
        float s = sin(angle);

        mat4 m = mat4(1.0);

        m[0][0] = c;
        m[0][1] = s;
        m[1][1] = c;
        m[1][0] = -s;

        texCoord = vTexCoord;
        gl_Position = m * vPos;
    }
</script>

<script id="fragment-shader" type="x-shader/x-fragment">
    #ifdef GL_ES
    precision highp float;
    #endif

    uniform sampler2D uTexture;
    varying vec2 texCoord;

    void main() {
        gl_FragColor = texture2D(uTexture, texCoord);
    }
</script>

<script src="https://www.khronos.org/registry/webgl/sdk/demos/common/webgl-utils.js"></script>
<script src="InitShaders.js"></script>
<script src="demo.js"></script>

<body>
<canvas id="gl-canvas" width="512" height="512">
    您的浏览器不支持canvas
</canvas>
</body>
</html>

       为了简化编译和链接WebGL的着色器代码,创建了一个InitShaders()程序,但是没有加载glsl文件;着色器定义在了HTML页面源代码中。为了组织好代码,创建InitShaders.js文件。

##WebGL着色器载入器:InitShaders.js

//
//InitShaders.js
//
function InitShaders(gl, vertexShaderId, fragmentShaderId) {
    var vertShdr;
    var fragShdr;

    var vertElem = document.getElementById(vertexShaderId);
    if (!vertElem) {
        alert("Unable to load vertex shader : " + vertexShaderId);
        return -1;
    } else {
        vertShdr = gl.createShader(gl.VERTEX_SHADER);
        gl.shaderSource(vertShdr, vertElem.text);
        gl.compileShader(vertShdr);
        if (!gl.getShaderParameter(vertShdr, gl.COMPILE_STATUS)) {
            var msg = "Vertex shader failed to compile." +
                "The error log is :" +
                "<pre>" + gl.getShaderInfoLog(vertShdr) + "</pre>";
            alert(msg);
            return -1;
        }
    }

    var fragElem = document.getElementById(fragmentShaderId);
    if (!fragElem) {
        alert("Unable to load fragment shader " + fragmentShaderId);
        return -1;
    } else {
        fragShdr = gl.createShader(gl.FRAGMENT_SHADER);
        gl.shaderSource(fragShdr, fragElem.text);
        gl.compileShader(fragShdr);
        if (!gl.getShaderParameter(fragShdr, gl.COMPILE_STATUS)) {
            var msg = "Vertex shader failed to compile." +
                "The error log is :" +
                "<pre>" + gl.getShaderInfoLog(fragShdr) + "</pre>";
            alert(msg);
            return -1;
        }
    }

    var program = gl.createProgram();
    gl.attachShader(program, vertShdr);
    gl.attachShader(program, fragShdr);
    gl.linkProgram(program);

    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
        var msg = "Shader program failed to link." +
            "The error log is : " +
            "<pre>" + gl.getProgramInfoLog(program) + "</pre>";
        return -1;
    }

    return program;
}

       虽然InitShaders()采用HTML元素的ID寻找顶点着色器和片段着色器的代码。返回的是传递到glUseProgram()的程序名称。

       有了编译和链接到着色器的方法,就可以继续初始化图形数据、加载纹理,并完成WebGL程序的其他部分。

##WebGL初始化顶点数据

       WebGL带给JavaScript的一个显著特征就是类型化数组(typed arrays),它扩展为JavaScript数组概念,并满足OpenGL数据类型风格,几种类型化数组的类型如下表所示。

数组类型 C类型
Int8Array signed char
Uint8Array unsigned char
Uint8ClampedArray unsigned char
Int16Array signed short
Uint16Array unsigned short
Int32Array signed int
Uint32Array unsigned int
Float32Array float
Float64Array double

       第一次需要分配和填充(这两者可以在一个单一的操作里面做)一个类型化数组来存储顶点数据。在此之后,设置VBO即可,和在openGL中是一样的。

##初始化WebGL里的顶点缓冲

var verticles = {};
verticles.data = new Float32Array([
    -0.5, -0.5,
     0.5, -0.5,
     0.5,  0.5,
    -0.5,  0.5
]);

verticles.bufferId = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, verticles.bufferId);
gl.bufferData(gl.ARRAY_BUFFER, verticles.data, gl.STATIC_DRAW);
var vPos = gl.getAttribLocation(program, "vPos");
gl.vertexAttribPointer(vPos, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(vPos);

##在WebGL中使用纹理贴图

       在webGL中使用纹理和openGL一样,但加载处理要简单得多,因为有HTML的帮助。事实上,从文件加载纹理仅仅需要一行代码即可完成。比如,加载一个名为的单一纹理。

var image = new Image();
image.src = "material.jpg";

       HTML加载图片虽然简单,然而,他是异步加载的,所以要知道该图像文件何时接受并载入,可以在回调中处理。JavaScript在图像中有一个处理这种情况的方法:onload()

image.onload = function () {
    configureTexture(image);
    render();
};

       上面给出的onload函数实在图像被完全加载并且可以被WebGL使用的时候才调用一次。我们可以将所以的纹理初始化代码都封装到一个本地函数configureTexture中。

function configureTexture(image) {
    texture = gl.createTexture();
    gl.activeTexture(gl.TEXTURE0);
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image);
    gl.generateMipmap(gl.TEXTURE_2D);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST_MIPMAP_LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
}

       WebGL扩展的glPixelStore*()用于翻转图像数据,WebGL的宏UNPACK_FLIP_Y_WEBGL是旋转图像数据来匹配WebGL时需要的。

由于OpenGL ES 2.0版本的原因,WebGL只支持分辨率为2的幂的纹理。

demo.js的完整源文件

//
//demo.js
//
var canvas;
var gl;
var texture;
var uFrame;

window.onload = init;

function CheckError(msg) {
    var error = gl.getError();
    if (error != 0) {
        var errMsg = "OpenGL error: " + error.toString(16);
        if (msg) {errMsg = msg + "\n" + errMsg; }
        alert(errMsg);
    }
}

function configureTexture(image) {
    texture = gl.createTexture();
    gl.activeTexture(gl.TEXTURE0);
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image);
    gl.generateMipmap(gl.TEXTURE_2D);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST_MIPMAP_LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
}

function init() {
    canvas = document.getElementById("gl-canvas");

    gl = WebGLUtils.setupWebGL(canvas);
    if(!gl) {alert("WebGL is not available");}

    gl.viewport(0, 0, canvas.width, canvas.height);
    gl.clearColor(1.0, 0.0, 0.0, 1.0);

    //
    // 读取着色器并初始化属性数组
    //

    var program = InitShaders(gl, "vertex-shader", "fragment-shader");
    gl.useProgram(program);

    var verticles = {};
    verticles.data = new Float32Array([
        -0.5, -0.5,
         0.5, -0.5,
         0.5,  0.5,
        -0.5,  0.5
    ]);

    verticles.bufferId = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, verticles.bufferId);
    gl.bufferData(gl.ARRAY_BUFFER, verticles.data, gl.STATIC_DRAW);
    var vPos = gl.getAttribLocation(program, "vPos");
    gl.vertexAttribPointer(vPos, 2, gl.FLOAT, false, 0, 0);
    gl.enableVertexAttribArray(vPos);

    var texCoords = {};
    texCoords.data = new Float32Array([
        0.0, 0.0,
        1.0, 0.0,
        1.0, 1.0,
        0.0, 1.0
    ]);

    texCoords.bufferId = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, texCoords.bufferId);
    gl.bufferData(gl.ARRAY_BUFFER, texCoords.data, gl.STATIC_DRAW);
    var vTexCoord = gl.getAttribLocation(program, "vTexCoord");
    gl.vertexAttribPointer(vTexCoord, 2, gl.FLOAT, false, 0, 0);
    gl.enableVertexAttribArray(vTexCoord);

    //
    // 初始化纹理
    //

    var image = new Image();
    image.onload = function () {
        configureTexture(image);
        render();
    };
    image.src = "material.jpg";

    gl.activeTexture(gl.TEXTURE0);
    var uTexture = gl.getUniformLocation(program, "uTexture");
    gl.uniform1i(uTexture, 0);

    uFrame = gl.getUniformLocation(program, "uFrame");
}

var frameNumber = 0;

function render() {
    gl.uniform1f(uFrame, frameNumber++);

    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
    gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);

    window.requestAnimFrame(render, canvas);
}