WebGL自学教程——WebGL示例:开始

时间:2022-05-30 04:41:57
 

    终于开始WebGL的示例了,......

开始

 

    使用WebGL的步骤,很简单:

    1. 获得WebGL的渲染环境(也叫渲染上下文)。

    2. 发挥你的想象力,利用《WebGL参考手册》中的函数,参考《OpenGL ES 2.0编程指南》和各种已有的WebGL演示,针对获得的WebGL渲染环境进行操作,表达出你的意境。

    为了获得WebGL的渲染环境,或者说,为了给WebGL一个渲染环境,我们需要在Web页面中定义一个名称为“canvas ”的HTML5元素。每个canvas元素都可以对应一个WebGL渲染环境。注意,我说的是“可以对应”,而不是“对应”或“必须对应”;这是因为canvas元素还可以用作Web2D的渲染环境。——如果在一个页面中定义了多个canvas元素,我们就可以同时执行多个WebGL渲染。

    定义canvas元素不难,不过要记得给它指定一个id,以方便我们在js中对它进行访问。另外,由于要在js中使用向量和矩阵等相关的操作,还要引入一个js文件“glMatrix-0.9.5.js”。该文件可以从网络下载,后面的版本号可能会有所不同。做完这两项工作后,我的HTML文件的完整代码如下:

<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=gb2312">
<script type="text/javascript" src="glMatrix-0.9.5.js"></script>
</head>
<body>
    <canvas id="myCanvas" style="border:1px solid red;" width='600px' height='450px'></canvas>
</body>
</html>

    仔细看,我还给canvas元素指定了一个像素的红色边框,这在没有WebGL渲染或渲染失效的情况下,易于观察我们所要操作的区域。我还指定了该元素的宽度为600像素,高度为450像素。由这个宽度和高度所形成的元素的区域,就是我们可以操作的WebGL渲染区域。不过,同样请注意,我说的还是“可以”,因为真正可实际操作的WebGL渲染区域是需要我们使用WebGL API进行设置的。这个元素区域是我们可以设置的最大有效区域。

    获得WebGL的渲染环境,直接调用canvas元素的getContext("webgl")方法即可。不过要注意的是,由于目前WebGL还处于实验室阶段,传入的参数可能是“experimental-webgl”。具体的代码如下:

    var myCanvasObject = document.getElementById('myCanvas');
    var webgl = myCanvasObject.getContext("experimental-webgl");

    注意,为了方便演示,如无必要,我不会写容错性的代码。

    如果没有错误发生的话,webgl就是WebGL的渲染环境。必须说明且你必须记住,任何对WebGL函数、常量等的调用,都需要通过渲染环境进行,如webgl.viewport(...)、webgl.VERTEX_SHADER(此处的webgl就是myCanvasObject.getContext("experimental-webgl")的返回值)。通常,WebGL应用不是几个函数就搞定的;也就是说,webgl这个渲染环境需要在N多个地方使用,为便于访问,我把它存储在全局变量中。在本章的示例中,WebGL相关的初始化无需用户交互,为方便起见,我放在了body的onload事件中(如无必要,以后的示例也会如此),于是,我的HTML文件的内容变成了下面这个样子:

<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=gb2312">
<script type="text/javascript" src="glMatrix-0.9.5.js"></script>
<script>
    var webgl = null;

    function Init()
    {
        var myCanvasObject = document.getElementById('myCanvas');
        webgl = myCanvasObject.getContext("experimental-webgl");
    }

</script>
</head>
<body onload='Init()'>
    <canvas id="myCanvas" style="border:1px solid red;" width='>600px' height='450px'></canvas>
</body>
</html>

    下面进行一些实际的事情。

    首先,设定WebGL的渲染区域(视见区):webgl.viewport(0, 0, width, height)。该渲染区域,并不是你要直接操作(也不能、不应当操作;除非你的是WebD3D(如果有的话)之类的玩意)的区域(即使看起来很像)。这点和2D渲染不同。该区域其实是一个最终结果的显示区域。当你在WebGL内部画好图像之后,WebGL会自动通过某种方式,将其映射到这个区域中。例如,假设我们指定视见区的宽和高都是10px;在WebGL中,我们画了一个5px*20px的图像(假设可以的话);那么,最终你看到的可能并不是原图中5px*10px的某部分,而可能是5px*20px这整幅图像被“拉伸”到10px*10px之后的效果。“拉伸”只是对前面句子“通过某种方式”中的“方式”一词的一种形象的说法,并不准确;它到底如何进行,这个是着色器的事情。

    WebGL中有两种着色器:顶点着色器和片段着色器。其中,顶点着色器用来处理顶点的位置;片段着色器用来处理顶点的颜色。什么是顶点?简单说,顶点就是定义了你要绘画的那些图像上的点。比如,两个点A和B可以确定一条线段,那么,在3D中,我们就说,A和B是线段AB的顶点。

    着色器用WebGL函数createShader()创建。该函数接收一个参数,用来指定要创建的着色器的类型。该类型是一个枚举量,顶点着色器用WebGL枚举FRAGMENT_SHADER表示, 片段着色器用WebGL枚举VERTEX_SHADER表示。虽然你可以直接指定一个和枚举量相等的整数作为传入参数,不过在不同的浏览器上,这些整数值可能不同,关键是不好记,容易出错,所以,还是建议使用枚举量。

    创建好着色器后,你还需要指定它们将3D内容转换到视见区的“方式”。该“方式”是一个字符串,由于它由符合WebGL着色语言语法的语句组成的,因此我们称之为源码。指定着色器的源码调用WebGL函数shaderSource(shaderObject, sourceCode)即可。此处有个算不上是技巧的技巧。在不使用技巧之前,设置源码的js语句应当是这个样子:

        var source = "attribute vec3 v3Position;void main(void){gl_Position = vec4(v3Position, 1.0);}";
        webgl.shaderSource(shaderObject, source);

    如果你稍微思考一下,就可能发现不爽的地方。一般,顶点着色器要执行的动作是复杂的,需要编写的着色语言语句也是不少的。直接用字符串提供的话,不方便修改和阅读。因此,我们有必要利用一下HTML,让那些着色语句以直观的、格式化的形式显示在HTML的源码之中,但不显示在最终的页面中。比如,我们可以将着色语句放置在一个样式指定为不可见不显示的块元素中。但还有一种更好的做法,就是将着色语句写在一个单独的script标记中,如下面这般(script标记中的type属性无关紧要;下面示例中的设置只是为了好看。当然,如果你聪明点的,可以用它来决定该script所对应的着色器的类型;不过在这种情况下,将typ设置为1、2或a、b,或者把type替换为其他任何属性,比如xyz,效果都是一样的。唯一要注意的就是,你在进行判断时使用的属性和值必须和此处所指定的相同):

        <script id="shader-vs" type="x-shader/x-vertex">
        attribute vec3 v3Position;
        void main(void)
        {
            gl_Position = vec4(v3Position, 1.0);
        }
        </script>

        <script id="shader-fs" type="x-shader/x-fragment">
        void main(void)
        {
            gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
        }
        </script>

    源码的设置语句也要作出相应修改。首先,获取script中的内容,这儿有一个现成的js函数,如下:

        function ShaderSourceFromScript(scriptID)
        {
            var shaderScript = document.getElementById(scriptID);
            if (shaderScript == null) return "";

            var sourceCode = "";
            var child = shaderScript.firstChild;
            while (child)
            {
                if (child.nodeType == child.TEXT_NODE ) sourceCode += child.textContent;
                child = child.nextSibling;
            }

            return sourceCode;
        }

    然后,将获取到的内容(已经是一个字符串)设置为着色器的源码:

        webgl.shaderSource(vertexShaderObject, ShaderSourceFromScript("shader-vs"));
        webgl.shaderSource(fragmentShaderObject, ShaderSourceFromScript("shader-fs"));

    OK,着色器有了,源码也有了,你应当也想到了,既然是源码,是需要编译的吧?确实,着色语言看起来有点象C,某些行为也象C,需要编译和链接。

    着色器源码的编译使用WebGL函数compileShader(shaderObject)。编译过程中,同样可能碰到一些诸如语法错误之类的问题。因此,在编译之后,需要检查一下编译的结果状态。如果编译没有成功,则WebGL渲染就不能正常进行。检查编译状态使用WebGL函数getShaderParameter(shaderObject, WebGL.COMPILE_STATUS)。该函数返回一个布尔值,true表示编译成功,false表示有错。要注意,有错,不一定就是语法错误,还可能是着色器中使用的资源超出了浏览器所能支持的范围,就象C程序中无法为某个数组分配4G大小的内存那样。

    接下来是链接。链接需要用到一个新的对象:程序对象。程序对象的创建使用WebGL函数createProgram()。在链接之前,需要确保着色器对象已附加到程序对象;附加着色器通过WebGL函数attachShader(programObject, shaderObject)进行。此处要说明的是,附加操作可以在链接之前的任何时刻进行,不用管着色器对象是否已经通过编译、是否已经指定了源码,只要它满足一个条件,就是一个程序对象只能且必须附加一个顶点着色器和一个片段着色器。链接通过WebGL函数linkProgram(programObject)进行。它会作许多事情,具体信息可参考《OpenGL ES 2.0 编程指南》的《第四章 着色器和程序/程序的创建和链接》和其他相关章节,在此不作多述。同样,链接也会有成功和失败,这可通过调用WebGL函数getProgramParameter(programObject, WebGL.LINK_STATUS)获得。

    最后,还要使用WebGL函数useProgram(programObject)来指定WebGL使用哪个程序对象进行渲染。

    但要让WebGL渲染出东西来,我们还要向WebGL提供数据。在我进行简单地复述讲解之前,你最好首先看一遍《OpenGL ES 2.0 编程指南》的《第六章 顶点属性、顶点数组和缓冲对象》

    WebGL中,有两种顶点数据:一种是常量数据,一种是数组数据。所谓常量数据,就是指顶点的某个属性(如,颜色)值恒定不变。而数组数据是指,顶点的某个属性(如,位置)并非恒定不变,而是由一系列不完全相同的值组成;我们将这些值组合成一个数组。换言之,数组中每个元素都对应着该属性的一个可能值。注意,此处说的是可能值,因为在一些时候,我们会跳过数组中的一些值。当进行渲染的时候,WebGL会按照我们指定的规则枚举数组中的元素,将枚举到的元素的值传递到着色器中的相应变量中。

    本章示例,只是简单地显示一个三角形,所需的数据是三个顶点的位置。由于这三个位置各不相同(否则也不会组成一个三角形),所以我们需要使用数组数据。

    首先,我们用js中的数组定义好这个三个顶点位置。要注意的是,WebGL中的坐标系的范围是[-1, +1]。在没有使用坐标转换之前,我们定义的坐标范围不能超出这个范围,否则就会显示不正确。另外,我们是在3D中画三角形,因此,使用的是三维坐标(当然,你也可以使用四维坐标)。

        var jsArrayData = [
         0.0,   1.0,   0.0,//上顶点
        -1.0,  -1.0,   0.0,//左顶点
         1.0,   0.0,   0.0];//右顶点

    你发现,jsArrayData只是很普通的一维数组,共有9个元素,每三个为一组,每组和一个顶点位置对应。WebGL无法直接访问js中的数据,我们要通过一定的方法将这些数据弄到WebGL可以访问的地方。这需要借助WebGL提供的API函数,步骤如下:

    1. 使用WebGL函数createBuffer()创建一块WebGL可以访问的存储区(我们称之为缓冲)。

    2. 将创建的存储区设置为相应存储区类型的当前操作对象,这通过WebGL的缓冲绑定函数bindBuffer(WebGL.ARRAY_BUFFER, buffer)完成。该函数的第一个参数表示要设置的存储区类型,可以为WebGL.ARRAY_BUFFER和WebGL.ELEMENT_ARRAY_BUFFER;前者表示绑定的缓冲为顶点数组数据,后者表示绑定的缓冲为顶点元素数组数据(对于它是啥,可以参考《OpenGL ES 2.0 编程指南》,或者耐心等待我在后面章节的复述讲解)。

    3. 将js中的数据“拷贝”到WebGL缓冲中:WebGL函数bufferData(WebGL.ARRAY_BUFFER, new Float32Array(jsArrayData), WebGL.STATIC_DRAW)。该函数的第一个参数和bindBuffer意义相同。第二个参数就是我们要拷贝的数据了。只不过,我们需要将js中的数组稍微处理一下,转换为WebGL可以识别的数据格式:ArrayBuffer或ArrayBufferView。其中,ArrayBufferView又有以下几种子类型:

        . Int8Array
        . Uint8Array
        . Int16Array
        . Uint16Array
        . Int32Array
        . Uint32Array
        . Float32Array
        . Float64Array

    要注意,这些类型是为了WebGL而产生的浏览器内建类型;我们可以在js中直接使用它们。它们的一个主要作用,就是将js中的数组转换为WebGL可以识别的数据格式。转换方式就和上面见到的那般简单,只要将js数组作为参数传递给构造函数即可。

    bufferData的第三个参数指定缓冲的用法,对于它的具体讲解,你可以参考《OpenGL ES 2.0 编程指南》,或者耐心等待以后的章节。

    数据到此就设置完了,接下来就应当让WebGL执行绘图了。

    绘图有两种方式,一个是根据实际传入的顶点数组数据绘图,还有一种是根据传入的顶点元素数组数据绘图。此处我们用的是前者,后者在以后的章节复述讲解,或者你也可以参考《OpenGL ES 2.0 编程指南》。

    不管是顶点数组数据还是顶点元素数组数据,在一个WebGL应用中,通常会同时有N个。那么在绘图的时候,我们就首先需要告诉WebGL,我们要用的是哪个。此操作和上面设置js数据到WebGL中相同,都是通过WebGL函数bindBuffer来完成。但是,光这样还不行,因为我们还需要让WebGL把顶点数组数据和着色器中的变量联系起来;只有这样,着色器才能访问到我们设置给它的顶点数据(位置,颜色等)。这需要两个步骤:

    1. 将着色器中的变量(必须是attribute变量)关联到一个属性索引,使用WebGL函数bindAttribLocation(programObject, positionIndex, "shaderAttributeName")。在本章示例中,第二个参数为0;第三个参数属性名称为“v3Position”。注意,该操作必须在程序对象链接前进行(这点,你将在最终的示例中看到),否则无效。

    2. 使用WebGL函数enableVertexAttribArray(positionIndex)启用相应关联索引上的数组数据或元素数组数据。

    3. 通过下面的WebGL函数,指定关联索引上的数组数据或元素数组数据的正确信息:

        void vertexAttribPointer(GLuint positionIndex, GLint size, GLenum type,
                 GLboolean normalized, GLsizei stride, GLintptr offset);

    其中,size之单个数据的大小。比如,顶点的位置我们一般用(x,y,z)表示,则,此值为3;顶点的纹理坐标用(s,t)表示,则此值为2。type指定数据的类型,可以为WebGL的BYTE、UNSIGNED_BYTE、SHORT、UNSIGNED_SHORT、FLOAT、FIXED等。normalized指定数据转换为浮点型时,是否需要规范化。stride指定相邻的两个数据之间的间隔(详细解释参考《OpenGL ES 2.0 编程指南》的《第六章 顶点属性、顶点数组和缓冲对象/顶点数组》)。offset指定起始数据的偏移,以字节为单位。

    绘图的最后一步工作,是调用WebGL函数drawArrays(mode, first, count)或drawElements(mode, count, type, offset)执行图形绘画。这两个函数的第一个参数mode,指定绘画的模式,有点、线、三角形、三角扇等(具体有哪些及其效果,请参考《OpenGL ES 2.0 编程指南》的《第七章 基元集和光栅化》)。函数drawElements同样在以后复述介绍,或自行参考《OpenGL ES 2.0 编程指南》的相关章节。函数drawArrays的第二个参数指定起始顶点的索引。第三个参数指定要绘画的顶点的个数。在本节示例中,我们的绘画模式为三角形,起始点是0,绘画个数是3(因为三角形只能有三个顶点)。注意,为了确保正确,你需要在每帧渲染开始之前进行必须要的擦除:首先使用WebGL函数clearColor(red, green, blue, alpha)等设置擦除信息,然后调用WebGL函数clear(WebGL.COLOR_BUFFER_BIT)执行擦除。

    现在,把上面的一切结合到一起,整个HTML源码如下:

<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=gb2312">
<script type="text/javascript" src="glMatrix-0.9.5.js"></script>

        <script id="shader-vs" type="x-shader/x-vertex">
        attribute vec3 v3Position;
        void main(void)
        {
            gl_Position = vec4(v3Position, 1.0);
        }
        </script>

        <script id="shader-fs" type="x-shader/x-fragment">
        void main(void)
        {
            gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
        }
        </script>

<script>
        function ShaderSourceFromScript(scriptID)
        {
            var shaderScript = document.getElementById(scriptID);
            if (shaderScript == null) return "";

            var sourceCode = "";
            var child = shaderScript.firstChild;
            while (child)
            {
                if (child.nodeType == child.TEXT_NODE ) sourceCode += child.textContent;
                child = child.nextSibling;
            }

            return sourceCode;
        }

    var webgl = null;
    var vertexShaderObject = null;
    var fragmentShaderObject = null;
    var programObject = null;
    var triangleBuffer = null;
    var v3PositionIndex = 0;

    function Init()
    {
        var myCanvasObject = document.getElementById('myCanvas');
        webgl = myCanvasObject.getContext("experimental-webgl");

        webgl.viewport(0, 0, myCanvasObject.clientWidth, myCanvasObject.clientHeight);

        vertexShaderObject = webgl.createShader(webgl.VERTEX_SHADER);
        fragmentShaderObject = webgl.createShader(webgl.FRAGMENT_SHADER);

        webgl.shaderSource(vertexShaderObject, ShaderSourceFromScript("shader-vs"));
        webgl.shaderSource(fragmentShaderObject, ShaderSourceFromScript("shader-fs"));

        webgl.compileShader(vertexShaderObject);
        webgl.compileShader(fragmentShaderObject);

        if(!webgl.getShaderParameter(vertexShaderObject, webgl.COMPILE_STATUS)){alert("error:vertexShaderObject");return;}
        if(!webgl.getShaderParameter(fragmentShaderObject, webgl.COMPILE_STATUS)){alert("error:fragmentShaderObject");return;}

        programObject = webgl.createProgram();

        webgl.attachShader(programObject, vertexShaderObject);
        webgl.attachShader(programObject, fragmentShaderObject);

        webgl.bindAttribLocation(programObject, v3PositionIndex, "v3Position");

        webgl.linkProgram(programObject);
        if(!webgl.getProgramParameter(programObject, webgl.LINK_STATUS)){alert("error:programObject");return;}

        webgl.useProgram(programObject);

        var jsArrayData = [
        0.0, 1.0, 0.0,//上顶点
        -1.0, -1.0, 0.0,//左顶点
        1.0, 0.0, 0.0];//右顶点

       triangleBuffer = webgl.createBuffer();
       webgl.bindBuffer(webgl.ARRAY_BUFFER, triangleBuffer);
       webgl.bufferData(webgl.ARRAY_BUFFER, new Float32Array(jsArrayData), webgl.STATIC_DRAW);

       webgl.clearColor(0.0, 0.0, 0.0, 1.0);
       webgl.clear(webgl.COLOR_BUFFER_BIT);

       webgl.bindBuffer(webgl.ARRAY_BUFFER, triangleBuffer);

       webgl.enableVertexAttribArray(v3PositionIndex);

       webgl.vertexAttribPointer(v3PositionIndex, 3, webgl.FLOAT, false, 0, 0);

       webgl.drawArrays(webgl.TRIANGLES, 0, 3);

    }
</script>
</head>
<body onload='Init()'>
    <canvas id="myCanvas" style="border:1px solid red;" width='600px' height='450px'></canvas>
</body>
</html>

    你可以将上面的源码复制到一个HTML文件中,然后在支持WebGL的浏览器中运行。在我使用的FF浏览器上,运行结果如下:

WebGL自学教程——WebGL示例:开始

从图中,你可以推断出WebGL x和y轴的坐标系统,左下角为(-1, -1), 右上角为(1, 1)。