WebGL基础教程:第一部分

2019-01-12 22:10:31 浏览数 (1)

WebGL是一种基于OpenGL的浏览器内置3D渲染器,它可以让你在HTML5页面中直接显示3维内容。 在本教程中,我会介绍你使用此框架所需的所有基础内容。

介绍

开始学习之前,有几件事你是需要了解的。 WebGL是将3D内容渲染到HTML5的canvas元素上的一种JavaScript API。 它是利用"3D世界"中称为着色器的两种脚本来实现这一点的。 这两种着色器分别是:

  • 顶点着色器
  • 片元着色器

听到这些名词时也不要过于惊慌;它们只不过是"位置计算器"和"颜色选择器"的另一种说法罢了。 片元着色器容易理解;它只是告诉WebGL,模型上的指点定应该是什么颜色。 而顶点着色器解释起来就需要点技术了,不过基本上它起到将3维模型转变为2维坐标的作用。 因为所有的计算机显示器都是2维平面,当你在屏幕上看3维物体时,它们只不过是透视后的幻象。

如果你想完整地理解这个计算过程,你最好是问一个数学家,因为这个过程中用到了高级的4x4矩阵乘法,实在是有点超过我们这个"基础"教程的范围呀。 幸运的是,你并不需要知道它所有的工作原理,因为WebGL会处理背后大部分的细节。 那么,我们开始吧。

第一步:设置WebGL

WebGL有许多细微的设置,而且每次你要在屏幕画什么东西之前都要设置一遍。 为了节约时间,并使代码整洁一些,我们把所有"幕后"的代码包装成一个JavaScript对象,并存于一个独立的文件中。 现在我们要开始了,先创建一个新文件'WebGL.js',并写入如下代码:

代码语言:javascript复制
function WebGL(CID, FSID, VSID){
    var canvas = document.getElementById(CID);
    if(!canvas.getContext("webgl") && !canvas.getContext("experimental-webgl"))
        alert("Your Browser Doesn't Support WebGL");
    else
    {
        this.GL = (canvas.getContext("webgl")) ? canvas.getContext("webgl") : canvas.getContext("experimental-webgl");

        this.GL.clearColor(1.0, 1.0, 1.0, 1.0); // this is the color 
        this.GL.enable(this.GL.DEPTH_TEST); //Enable Depth Testing
        this.GL.depthFunc(this.GL.LEQUAL); //Set Perspective View
        this.AspectRatio = canvas.width / canvas.height;

        //Load Shaders Here
    }
} 

这个构造函数的参数是canvas无形的ID,以及两个着色器对象。 首先,我们要获得canvas元素,并确保它是支持WebGL的。 如果支持WebGL,我们就将WebGL上下文赋值给一个局部变量,称为"GL"。 清除颜色(clearColor)其实就是设置背景颜色,值得一提的是,WebGL中大部分参数的取值范围都是0.0到1.0,所以我们需要让通常的rgb值除以255。 所以,我们的示例中,1.0,1.0,1.0,1.0表示背景为白色,且100%可见 (即无透明)。 接下来两行要求WebGL计算深度和透视,这样离你近的对象会挡住离你远的对象。 最后,我们设置宽高比,即canvas的宽度除以它的高度。

继续前行之前,我们要准备好两个着色器。 我把这些着色器写到HTML文件里去,这个HTML文件里还包含了我们的画布元素 (canvas)。 创建一个HTML文件,并将下面的两个script元素放在body标签之前。

代码语言:javascript复制
<script id="VertexShader" type="x-shader/x-vertex">

    attribute highp vec3 VertexPosition;
    attribute highp vec2 TextureCoord;


    uniform highp mat4 TransformationMatrix;
    uniform highp mat4 PerspectiveMatrix;

    varying highp vec2 vTextureCoord;

    void main(void) {
    gl_Position = PerspectiveMatrix * TransformationMatrix * vec4(VertexPosition, 1.0);
    vTextureCoord = TextureCoord;
}
</script>

<script id="FragmentShader" type="x-shader/x-fragment">
    varying highp vec2 vTextureCoord;

uniform sampler2D uSampler;

void main(void) {
    highp vec4 texelColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t));
    gl_FragColor = texelColor;
}
</script>

先来看顶点着色器,我们定义了两个属性 (attributes):

  • 顶点位置,它存储了当前顶点 (你的模型上的点) 的位置,包括x,y,z坐标。
  • 纹理坐标,即赋给这个点的纹理在纹理图像中的位置

接下来,我们创建变换和透视矩阵等变量。 它们被用于将3D模型转化为2D图像。 下一行是创建一个与片元着色器共享的变量vTextureCoord,在主函数中,我们计算gl_Position (即最终的2D位置)。 然后,我们将'当前纹理坐标'赋给这个共享变量vTextureCoord。

在片元着色器中,我们取出定义在顶点着色器中的这个坐标,然后用这个坐标来对纹理进行'采样'。 基本上,通过这个过程,我们得到了我们几何体上的当前点处的纹理的颜色。

现在写完了着色器,我们可回过头去在JS文件中加载这些着色器。 将"//Load Shaders Here"换成如下代码:

代码语言:javascript复制
var FShader = document.getElementById(FSID);
var VShader = document.getElementById(VSID);

if(!FShader || !VShader)
    alert("Error, Could Not Find Shaders");
else
{
    //Load and Compile Fragment Shader
    var Code = LoadShader(FShader);
    FShader = this.GL.createShader(this.GL.FRAGMENT_SHADER);
    this.GL.shaderSource(FShader, Code);
    this.GL.compileShader(FShader);

    //Load and Compile Vertex Shader
    Code = LoadShader(VShader);
    VShader = this.GL.createShader(this.GL.VERTEX_SHADER);
    this.GL.shaderSource(VShader, Code);
    this.GL.compileShader(VShader);

    //Create The Shader Program
    this.ShaderProgram = this.GL.createProgram();
    this.GL.attachShader(this.ShaderProgram, FShader);
    this.GL.attachShader(this.ShaderProgram, VShader);
    this.GL.linkProgram(this.ShaderProgram);
    this.GL.useProgram(this.ShaderProgram);

    //Link Vertex Position Attribute from Shader
    this.VertexPosition = this.GL.getAttribLocation(this.ShaderProgram, "VertexPosition");
    this.GL.enableVertexAttribArray(this.VertexPosition);

    //Link Texture Coordinate Attribute from Shader
    this.VertexTexture = this.GL.getAttribLocation(this.ShaderProgram, "TextureCoord");
    this.GL.enableVertexAttribArray(this.VertexTexture);
}

你的纹理必须是偶数字节大小,否则会出错。。。比如2x2,4x4,16x16,32x32。。。

首先,我们要确保这些着色器是存在的,然后,我们逐一地加载它们。 这个过程基本上是:得到着色器源码,编译,附着到核心的着色程序上。 从HTML文件中提取着色器源码的代码,封装到了一个函数中,称为LoadShader;稍后会讲到。 我们使用这个'着色器程序'将两个着色器链接起来,通过它,我们可以访问到着色器中的变量。 我们将数据储存到定义在着色器中的属性;然后,我们就可以将几何体输入到着色器中了。

现在,让我们看一下LoadShader函数,你应该将它置于WebGL函数之外。

代码语言:javascript复制
function LoadShader(Script){
    var Code = "";
    var CurrentChild = Script.firstChild;
    while(CurrentChild)
    {
        if(CurrentChild.nodeType == CurrentChild.TEXT_NODE)
            Code  = CurrentChild.textContent;
        CurrentChild = CurrentChild.nextSibling;
    }
    return Code;
}

基本上,这个函数通过遍历着色器来收集源码。

第二步:“简单”立方体

为了在WebGL中画出对象,你需要如下三个数组:

  • 顶点 (vertices):构造你的对象的那些点
  • 三角形 (triangles):告诉WebGL如何将顶点连接成面
  • 纹理坐标 (texture coordinates):定义顶点如何被映射到纹理图像上

这个过程称为UV映射。 我们的例子是构造一个简单的立方体。 我将这个立方体分成4个顶点一组,每一组又连成两个三角形。 我们可以用一个变量来存储立方体的这些数组。

代码语言:javascript复制
var Cube = {
    Vertices : [ // X, Y, Z Coordinates

        //Front

        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

        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

        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

        -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

        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

        1.0, -1.0,  1.0,
        -1.0, -1.0,  1.0,
        1.0, -1.0, -1.0,
        -1.0, -1.0, -1.0

    ],
    Triangles : [ // Also in groups of threes to define the three points of each triangle
        //The numbers here are the index numbers in the vertex array

        //Front

        0, 1, 2,
        1, 2, 3,

        //Back

        4, 5, 6,
        5, 6, 7,

        //Right

        8, 9, 10,
        9, 10, 11,

        //Left

        12, 13, 14,
        13, 14, 15,

        //Top

        16, 17, 18,
        17, 18, 19,

        //Bottom

        20, 21, 22,
        21, 22, 23

    ],
    Texture : [ //This array is in groups of two, the x and y coordinates (a.k.a U,V) in the texture
        //The numbers go from 0.0 to 1.0, One pair for each vertex

        //Front

        1.0, 1.0,
        1.0, 0.0,
        0.0, 1.0,
        0.0, 0.0,


        //Back

        0.0, 1.0,
        0.0, 0.0,
        1.0, 1.0,
        1.0, 0.0,

        //Right

        1.0, 1.0,
        1.0, 0.0,
        0.0, 1.0,
        0.0, 0.0,

        //Left

        0.0, 1.0,
        0.0, 0.0,
        1.0, 1.0,
        1.0, 0.0,

        //Top

        1.0, 0.0,
        1.0, 1.0,
        0.0, 0.0,
        0.0, 1.0,

        //Bottom

        0.0, 0.0,
        0.0, 1.0,
        1.0, 0.0,
        1.0, 1.0
    ]
};

这样一个简单的立方体用到的数据似乎有点过多,不过,在我们教程的第二部分中,我们写一个导入3D模型的脚本,所以你现在不必计较这些。

你可能还在想,为什么需要24个顶点 (每一面4个) 呢,实际上只有8个呀? 我这样做是因为,你可以只用为每个顶点指定一个纹理坐标;而如果你用8个顶点,则整个立方体将看起来一样,因为它会将一个纹理值传播到顶点接触的所有面上。 通过我们的方式,每个面都有它独有的点,所以我们可以在每一面上指定不同的纹理区域。

现在,我们有了这样一个立方体变量 cube,然后,我们可以准备画它了。 我们还是回到WebGL方法中,并添加一个Draw函数。

第三步:Draw函数

WebGL中绘制对象的过程有许多步骤;所以最好是将每个步骤写成函数,来简化这个过程的代码。 基本的想法是将三个数组加载到WebGL的缓存中去。 然后,我们将这些缓存连接到着色器中定义的属性,以及变换和透视矩阵。 接下来,我们需要将纹理加载到内存中,并且最后调用draw命令。 那么,我们开始吧。

接下来的代码进入到WebGL函数中:

代码语言:javascript复制
this.Draw = function(Object, Texture)
{
    var VertexBuffer = this.GL.createBuffer(); //Create a New Buffer

    //Bind it as The Current Buffer
    this.GL.bindBuffer(this.GL.ARRAY_BUFFER, VertexBuffer);

    // Fill it With the Data
    this.GL.bufferData(this.GL.ARRAY_BUFFER, new Float32Array(Object.Vertices), this.GL.STATIC_DRAW);

    //Connect Buffer To Shader's attribute
    this.GL.vertexAttribPointer(this.VertexPosition, 3, this.GL.FLOAT, false, 0, 0);

    //Repeat For The next Two
    var TextureBuffer = this.GL.createBuffer();
    this.GL.bindBuffer(this.GL.ARRAY_BUFFER, TextureBuffer);
    this.GL.bufferData(this.GL.ARRAY_BUFFER, new Float32Array(Object.Texture), this.GL.STATIC_DRAW);
    this.GL.vertexAttribPointer(this.VertexTexture, 2, this.GL.FLOAT, false, 0, 0);
    var TriangleBuffer = this.GL.createBuffer();
    this.GL.bindBuffer(this.GL.ELEMENT_ARRAY_BUFFER, TriangleBuffer);
    //Generate The Perspective Matrix
    var PerspectiveMatrix = MakePerspective(45, this.AspectRatio, 1, 10000.0);

    var TransformMatrix = MakeTransform(Object);

    //Set slot 0 as the active Texture
    this.GL.activeTexture(this.GL.TEXTURE0);

    //Load in the Texture To Memory
    this.GL.bindTexture(this.GL.TEXTURE_2D, Texture);

    //Update The Texture Sampler in the fragment shader to use slot 0
    this.GL.uniform1i(this.GL.getUniformLocation(this.ShaderProgram, "uSampler"), 0);

    //Set The Perspective and Transformation Matrices
    var pmatrix = this.GL.getUniformLocation(this.ShaderProgram, "PerspectiveMatrix");
    this.GL.uniformMatrix4fv(pmatrix, false, new Float32Array(PerspectiveMatrix));

    var tmatrix = this.GL.getUniformLocation(this.ShaderProgram, "TransformationMatrix");
    this.GL.uniformMatrix4fv(tmatrix, false, new Float32Array(TransformMatrix));

    //Draw The Triangles
    this.GL.drawElements(this.GL.TRIANGLES, Object.Trinagles.length, this.GL.UNSIGNED_SHORT, 0);
};

顶点着色器对你的对象进行放置,旋转和缩放时,依据的都是变换和透视矩阵。 在本教程第二部分中,我们会更深入地介绍变换。

我已经添加了两个函数:MakePerspective()和MakeTransform()。 它们只不过生成了WebGL所需的4x4矩阵。 MakePerspective()函数接受几个参数:视场竖直高度,宽高比,最近和最远点。 任何比1个单位近或比10000个单位远的对象都不会被显示,但是你可以调整这些值,以得到你所期望的效果。 现在,让我们看一看这两个函数:

代码语言:javascript复制
function MakePerspective(FOV, AspectRatio, Closest, Farest){
    var YLimit = Closest * Math.tan(FOV * Math.PI / 360);
    var A = -( Farest   Closest ) / ( Farest - Closest );
    var B = -2 * Farest * Closest / ( Farest - Closest );
    var C = (2 * Closest) / ( (YLimit * AspectRatio) * 2 );
    var D = (2 * Closest) / ( YLimit * 2 );
    return [
        C, 0, 0, 0,
        0, D, 0, 0,
        0, 0, A, -1,
        0, 0, B, 0
    ];
}
function MakeTransform(Object){
    return [
        1, 0, 0, 0,
        0, 1, 0, 0,
        0, 0, 1, 0,
        0, 0, -6, 1
    ];
}

这些矩阵都会影响到你的对象的最终视觉效果,但透视矩阵影响的是你的“3维世界”,比如视场和可见对象,而变换矩阵影响的是单个对象,比如它们的旋转和位置。 完成这些之后,我们几何可以开始画了,剩下的工作只是将一个图像转变为一个WebGL纹理。

第四步:加载纹理

加载一个纹理分两步。 首先,我们要用JavaScript的标准做法来加载一幅图像,然后,我们将其转化为一个WebGL纹理。 所以,我们先从第二步开始吧,毕竟我们正在讨论的是JS文件。 将下面的代码加到WebGL函数的底部,恰好在Draw命令之后。

代码语言:javascript复制
this.LoadTexture = function(Img){
    //Create a new Texture and Assign it as the active one
    var TempTex = this.GL.createTexture();
    this.GL.bindTexture(this.GL.TEXTURE_2D, TempTex);

    //Flip Positive Y (Optional)
    this.GL.pixelStorei(this.GL.UNPACK_FLIP_Y_WEBGL, true);

    //Load in The Image
    this.GL.texImage2D(this.GL.TEXTURE_2D, 0, this.GL.RGBA, this.GL.RGBA, this.GL.UNSIGNED_BYTE, Img);

    //Setup Scaling properties
    this.GL.texParameteri(this.GL.TEXTURE_2D, this.GL.TEXTURE_MAG_FILTER, this.GL.LINEAR);
    this.GL.texParameteri(this.GL.TEXTURE_2D, this.GL.TEXTURE_MIN_FILTER, this.GL.LINEAR_MIPMAP_NEAREST);
    this.GL.generateMipmap(this.GL.TEXTURE_2D);

    //Unbind the texture and return it.
    this.GL.bindTexture(this.GL.TEXTURE_2D, null);
    return TempTex;
};

值得一提的是,你的纹理大小必须是偶数字节,否则你会得到错误信息;比如它们可能的维度包括:2x2,4x4,16x16,32x32,等等。 我另加了一行来翻转Y坐标,只是因为我的3D应用的Y坐标是朝后的,但是否这样做完全取决于你。 这是因为一些程序取Y的零点为左上角,而其它则为左下角。 我设置的这些缩放性质只是告诉WebGL,图像应该如何向上采样和向下采样。 你可以使用其它的选项来得到不同的效果,不过我认为这个组合效果最佳。

现在,我们完成了JS文件,我们可以回到HTML文件,来完成最后一步了。

第五步:合起来

如前所述,WebGL是在canvas元素上画画。 因此,在body部分里,我们所需要的就只是一个canvas画布。 在添加canvas元素之后,你的html页面看起来像下面这样:

代码语言:javascript复制
<html>
<head>
    <!-- Include Our WebGL JS file -->
    <script src="WebGL.js" type="text/javascript"></script>
    <script>

    </script>
</head>
<body onload="Ready()">
<canvas id="GLCanvas" width="720" height="480">
    Your Browser Doesn't Support HTML5's Canvas.
</canvas>

<!-- Your Vertex Shader -->

<!-- Your Fragment Shader -->

</body>
</html>

这个页面相当简单。 在head区域,我链接了JS文件。 现在,让我们实现Ready函数,它在页面加载时调用。

代码语言:javascript复制
//This will hold our WebGL variable
var GL;

//Our finished texture
var Texture;

//This will hold the textures image 
var TextureImage;

function Ready(){
    GL = new WebGL("GLCanvas", "FragmentShader", "VertexShader");
    TextureImage = new Image();
    TextureImage.onload = function(){
        Texture = GL.LoadTexture(TextureImage);
        GL.Draw(Cube, Texture);
    };
    TextureImage.src = "Texture.png";
}

所以,我们创建一个新的WebGL对象,并将canvas和着色器的ID传递进去。 接下来,我们加载纹理图像。 一旦加载完成,我们对立方体Cube和纹理Texture调用Draw()方法。 如果你一路跟下来,你的屏幕上应该有一个覆盖有纹理的静止立方体。

虽然我说了下一次再讲变换,但我们不可能只丢给你一个静止矩形,这还不够三维。 让我们回过头去,再添加一个小小的旋转吧。 在HTML文件中,修改onload函数,使之如下面的代码:

代码语言:javascript复制
TextureImage.onload = function(){
    Texture = GL.LoadTexture(TextureImage);
    setInterval(Update, 33);
};

这会使得每隔33毫秒调用一个称为Update()的函数,因而我们得到约30fps的帧率。 下面是这个更新函数:

代码语言:javascript复制
function Update(){
    GL.GL.clear(16384 | 256);
    GL.Draw(GL.Cube, Texture);
}

这个函数相当简单;它只不过清除屏幕,然后绘制更新后的立方体。 现在,让我们进入JS文件,添加旋转代码。

第六步:添加一些旋转

我们不会完全实现变换的代码,因为我说了要等到下次现说,这次我们只是加一个绕Y轴的旋转。 要做的第一件事就是在Cube对象中加一个Rotation变量。 它会跟踪当前的角度,并让我们可以递增地保持旋转。 所以你的Cube变量的顶部代码应该如下面这样:

代码语言:javascript复制
var Cube = {
    Rotation : 0,
    //The Other Three Arrays
};

现在,让我们修改MakeTransform()函数,添加旋转功能:

代码语言:javascript复制
function MakeTransform(Object){
    var y = Object.Rotation * (Math.PI / 180.0);
    var A = Math.cos(y);
    var B = -1 * Math.sin(y);
    var C = Math.sin(y);
    var D = Math.cos(y);
    Object.Rotation  = .3;
    return [
        A, 0, B, 0,
        0, 1, 0, 0,
        C, 0, D, 0,
        0, 0, -6, 1
    ];
}

更多精彩内容,请微信关注”前端达人”公众号!

新年大礼包

关注“前端达人”公众号,回复“新年大礼包”获取英文电子书:

更多精彩内容,请微信关注”前端达人”公众号!

0 人点赞