WebGL学习笔记【一】概述及三角形

  最近开始研究起WebGL来,发现以前在图形学课上看javascript还真是不太理智的做法。

  这一系列学习笔记是自己学习过程的总结,难免有错和不正确,希望发现问题的同学可以“惨无人道”的指出。

  WebGL简单说就是OpenGL在浏览器端的实现。那OpenGL又是什么?OpenGL就是一组提供了生成2d、3d图形的API。

  其实,要想用WebGL来真正“画”出一些东西,首先要对图形学的一些基本概念有理解。

  简明图形学

  图形学,指利用计算机来生成图形(creation)、绘制或者叫渲染图形(render)、处理图形(manipulation)的学科。

  (一)

  首先是生成的问题。我对图形生成的理解,就是怎么样来描述各种需要进行绘制的图形,尤其是那些复杂的人物啊、建筑啊等等。

  在图形学中,我们从描述最最基本的点(vertex)开始,描述一个点,图形学中的点和几何意义上的点稍有不同,除了基本的位置外,可能还会需要颜色、顶点发向量(用于计算光照)、纹理坐标(用于贴图)等额外的信息。正如图像是由像素构成,图形学中的图形其实可以理解成由点构成。比如:我们按照一个人的形状,在三维空间中定义出这个人的轮廓,就已经大致可以描述这个人了。那有了点,我们就可以把它们连起来,可是这么多的点该怎样连?图形学里会将点连成三角形(polygon)。然后这许多的三角形就可以组成模型的骨架,线框网格(wire mesh)。

  比如下面这个有点像人的恶心模型, 我们可以看到组成它的点连接起来,形成了模型的网格(mesh):

WebGL学习笔记【一】概述及三角形

 这就是模型的最最基本的描述,如果再加上一些材质、打上灯光什么的,它看起来会是这样:

WebGL学习笔记【一】概述及三角形

  (二)

  大致说了图形描述的问题,那接着是绘制。也就是怎么样把三维的东西绘制到二维的屏幕。

  这里,我们想到,要是有一个图形渲染机,我们把我们对模型的描述,也就上面说的一堆点的信息,统统倒入这个机器,机器一阵处理后就可以在屏幕上绘制出模型,那就好了。确实也有这样一个机器,它的输入可以理解成一堆点(用数组之类的表示),输出就是我们屏幕上闪闪动人的模型。其实,这个机器可以理解成显卡。那它是怎么做到的?

  首先是我们怎么样把点的信息告诉显卡,OpenGL(或者WebGL)这时终于是派上用场了,它封装了底层的调用,提供给了我们和设备无关的函数来进行这些操作;显卡拿到了点的信息,就必须进行处理,因为最后屏幕上显示的是像素信息呀,这些处理简单说只有2个部分:点处理+像素处理(又叫光栅化),下面详细叙述;生成了像素数据后,这些像素数据会保存到一块叫帧缓存的内存中,然后,这些像素数据就最终在屏幕上显示了。

  上面我们说,处理过程分为点处理和像素处理。点处理,主要是根据输入的点的信息再进行一些必要的计算,最终产生每个点实际在屏幕上的显示位置;像素处理,则是真正计算每个像素应该显示的颜色。

  接着,我们需要想想更详细的东西。

  我们通过点来描述模型,那必然会需要一个坐标系,否则,点的位置、法向量该如何描述。这个参照的坐标系一般称为模型坐标系(或者object/local space)。那现在我们有了很多个模型,就需要有一个放置它们的世界,或者叫场景吧,这时也需要一个坐标系来定位模型的位置,这个坐标系就叫做世界坐标系(或者world space)。世界如此之大,我们或许不可能把整个世界都显示出来,那有点贪心了,这时我们需要一台摄像机,或者称它观察者吧,透过它的眼睛来看我们的世界,它一般会形成一个观察体,在这个观察体里的才显示,不在的就当作隐形了,这个坐标系就叫做观察坐标系(或者view space)。处在观察者注视下的世界其实还是一个3d的世界,可是我们需要显示在2d平面上啊,这时就需要一个很重要的变换过程了,叫做投影(projection),有正交投影和透视投影二种,投影变换后3d的世界就会被投影到一个2d的投影平面上,并且同时基本保持了在3d空间的性质,如:近大远小等等。最后,我们再进行一个视口(viewport)变换,将投影窗口变换为屏幕上的一个矩形区域(其实就是我们显示图形的窗口)。这些变换后,我们应该就会开始计算视口中每个像素该显示的颜色,然后绘制出来,整个绘制过程就完成了。这一系列操作,可以称为渲染管线。就好象linux的管道一样,这个管道输出的信息作为下一个管道输入的信息,不断进行下去。

  这个过程,其实简单说来,就是将我们输入的点从一个坐标系变换到另一个坐标系,那这种变换,矩阵运算就是最拿手的了。比如:在world space会对模型进行一些平移、旋转、缩放操作,都可以定义成一个变换矩阵;在view space,要将世界坐标中的点变换为观察坐标中的点,也是定义成一个变换矩阵;投影变换、视口变换也都是变换矩阵。这些细节,OpenGL的API其实都有函数能够直接使用,但是WebGL又稍微有点特殊,需要我们深入到渲染管线。

  (三)

  总结下,渲染管线的流程大致是这样:

  点信息(vertices data) -> 世界坐标系中的变换,如:平移、缩放、选择(world space transformation) -> 世界坐标系转为观察坐标系,需要定义摄像机(view space transformation) -> 投影变换(projection transformation) -> 裁剪(clipping,摄像机外的就不显示),背面剔除(backface culling,背对摄像机的那个面不显示) -> 齐次裁剪空间 -> 视口变换(viewport transformation) -> 光栅化(rasterize,可能会包括:贴图生成、光照生成、场景雾生成等) -> 最终像素颜色 -> 帧缓存(frame buffer) -> 显示到屏幕

  在这个管线中,大部分都是显卡在做工作,但是,我们却有机会直接对显卡编程,来操作这些数据,可以编程的2个部分分别叫做:vertex shader和fragment shader,其实就是分别对应对点的操作和对像素的操作。其中,vertex shader是对输入的每个点依次执行,生成该点的最终位置;fragment shader对每个像素操作,生成该像素的显示颜色;这2个shader之间也可以传递数据,不过只能是vertex shader传递给fragment shader,因为总是先执行vertex shader(比如:vertex shader内先根据点的法向量计算一些光照参数,然后传给fragment shader生成最终有光照考虑的像素颜色;vertex shader直接传递贴图坐标给fragment shader,fragment shader根据贴图坐标计算加上了贴图考虑的像素颜色等)。使用WebGL恶心的地方就是,就算只是显示一个三角形,都需要自己写shader。

  对应渲染管线的话,插入这2个可编程部件后,大致应该是这样:

  点信息(vertices data) ->

  vertex shader { 世界坐标系中的变换,如:平移、缩放、选择(world space transformation) -> 世界坐标系转为观察坐标系,需要定义摄像机(view space transformation) -> 投影变换(projection transformation) -> 其他一些计算 } -> 

 裁剪(clipping,摄像机外的就不显示),背面剔除(backface culling,背对摄像机的那个面不显示) -> 齐次裁剪空间 -> 视口变换(viewport transformation) ->

 fragment shader { 贴图生成、光照生成、场景雾生成、其他一些计算 } ->

 光栅化(rasterize,可能会包括:贴图生成、光照生成、场景雾生成等) -> 最终像素颜色 ->

 帧缓存(frame buffer) -> 显示到屏幕

  WebGL绘制三角形

  简单的总结完图形学的基本概念后,我们可以动手写程序了。就好象第一个程序都是Hello World,个人觉得图形学里的Hello World应该就是画一个三角形。

  我们可以先来看看OpenGL写的话,或许会是这样的(引用自:http://fly.cc.fer.hr/~unreal/theredbook/chapter01.html):

#include <whateverYouNeed.h>

main() {

   OpenAWindowPlease();

   glClearColor(0.0, 0.0, 0.0, 0.0);
   glClear(GL_COLOR_BUFFER_BIT);

   glColor3f(1.0, 1.0, 1.0);

   glOrtho(-2.0, 2.0, -2.0, 2.0, -1.0, 1.0); 

   glBegin(GL_TRIANGLES);
      glVertex2f(0.0, 1.0);
      glVertex2f(-1.0, 1.0);
      glVertex2f(1.0, -1.0);
   glEnd();

   glFlush();

   KeepTheWindowOnTheScreenForAWhile();
}

  glClearColor/glClear那里是清屏,为绘制做准备;glOrtho那句就是定义一个正交投影的“摄像机”;glBegin/glEnd那里就是通过三个点定义了一个三角形;glFlush就是将帧缓存画到屏幕。挺简洁呀~但是,WebGL没有像glBegin/glEnd这种东西,也不会很好心的自己帮你把点根据你定义的摄像机进行合适的变换,我们需要做更多的工作。

  以下代码,引用自MDN的文档(https://developer.mozilla.org/en/WebGL),文档的demo代码真是太乱了,然后做了适当的调整和修改。为了偷懒,我就对自己学习时觉得不好理解的部分进行一下记录,全部代码可以在这里获取:https://github.com/KohPoll/webgl-learn

  (一)关于shader及program的创建

function getShader(gl, id) {
    var shaderScript = document.getElementById(id),
        theSource = '',
        shader = null;

    if (!shaderScript) return shader;

    theSource = text(shaderScript);

    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 shader;
    }

    gl.shaderSource(shader, theSource); 
    gl.compileShader(shader);
    
    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) return null;

    return shader;
}

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

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

    if (gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
        gl.useProgram(shaderProgram);
    }
}

  shader的创建步骤:1.创建一个shader(gl.createShader);2.获取shader的源代码(这里是从dom节点中获取)并进行设置(gl.shaderSource);3.编译shader(gl.compileShader)。

  将shader“注入”到可编程组件program的步骤:1.创建一个program(gl.createProgram);2.依附shader到program上(gl.attachShader);3.链接program(gl.linkProgram);4.使用该program(gl.useProgram)。

  (二)关于点信息的创建(buffer的使用)

  我们上面说,可以将点的描述传送给显卡,这些信息其实是存放在内存里面的。

function initBuffers() {
    var vertices, colors;

    // vertex buffer
    vertices = [
        0.0, 1.0, 0.0,
        -1.0, -1.0, 0.0,
        1.0, -1.0, 0.0
    ];

    verticesBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, verticesBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);

    // vertex color buffer
    colors = [
        1.0, 0.0, 0.0, 1.0,
        0.0, 1.0, 0.0, 1.0,
        0.0, 0.0, 1.0, 1.0
    ];
  
    verticesColorBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, verticesColorBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
}

  大致步骤是这样的:1.我们将点信息存放在数组里;2.然后创建buffer(gl.createBuffer),并绑定它(gl.bindBuffer),以便可以对它进行操作;3.设置数据(gl.bufferData)。PS:那个gl.STATIC_DRAW的意思我也不是很理解,大概是这样的:STATIC_DRAW保存的数据内容只被程序定义一次,GL绘制命令可以使用多次;DYNAMIC_DRAW保存的数据内容将被程序重复定义,GL绘制命令可以使用多次。

  (三)关于渲染

function drawScene() {
    var projectMatrix, worldMatrix, viewMatrix,
        pUniform, wUniform, vUniform;

    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

    // {{ phase 1
    // bind to a shader attribute so the shader code can access.
    vertexPositionAttribute = gl.getAttribLocation(shaderProgram, 'aVertexPosition');
    gl.enableVertexAttribArray(vertexPositionAttribute);
    gl.bindBuffer(gl.ARRAY_BUFFER, verticesBuffer);
    gl.vertexAttribPointer(vertexPositionAttribute, 3, gl.FLOAT, false, 0, 0);

    vertexColorAttribute = gl.getAttribLocation(shaderProgram, 'aVertexColor');
    gl.enableVertexAttribArray(vertexColorAttribute);
    gl.bindBuffer(gl.ARRAY_BUFFER, verticesColorBuffer);
    gl.vertexAttribPointer(vertexColorAttribute, 4, gl.FLOAT, false, 0, 0);
    // }}

    
    // {{ phase 2
    //projectMatrix= makePerspective(75, canvas.width / canvas.height, 1.0, 100.0);
    projectMatrix = makeOrtho(-10.0, 10.0, -10.0, 10.0, 1.0, 100.0);

    //modelviewMatrix= Matrix.I(4);
    worldMatrix = Matrix.I(4);
    worldMatrix = worldMatrix.x(Matrix.RotationZ(0.6).ensure4x4());

    viewMatrix = Matrix.I(4);
    viewMatrix = viewMatrix.x(Matrix.Translation($V([0.0, 0.0, -95.0])).ensure4x4()); 
    
    // generate and deliver to the shader.
    pUniform = gl.getUniformLocation(shaderProgram, 'uPMatrix');
    gl.uniformMatrix4fv(pUniform, false, new Float32Array(projectMatrix.flatten()));

    wUniform = gl.getUniformLocation(shaderProgram, 'uWMatrix');
    gl.uniformMatrix4fv(wUniform, false, new Float32Array(worldMatrix.flatten()));

    vUniform = gl.getUniformLocation(shaderProgram, 'uVMatrix');
    gl.uniformMatrix4fv(vUniform, false, new Float32Array(viewMatrix.flatten()));
    // }}

    gl.drawArrays(gl.TRIANGLES, 0, 3); // (mode, first, count of point used to draw)
}

  这里有很多重要的东西。

  首先是js怎么和shader交互的问题,就是怎么把相应的数据传递给shader使用。简单说明下shader的变量的“类型”,attribute只有vertex shader有,是通过程序(js)传递给它的变量。uniform两种shader都有,而且是不能改变的,可以理解成常量;varying是vertex shader向fragment shader传递数据,fragment shader接受数据的方式。

  然后,我们看上面的phase 1部分的代码,这里就是将刚刚设置到buffer中的点信息作为attribute传递给shader使用的代码。

  1.调用vertexPositionAttribute = gl.getAttribLocation(shaderProgram, 'aVertexPosition')会返回一个“位置”,这个位置可以理解成shader中对名为aVertexPosition这个attribute的引用(指针);

  2.使用gl.enableVertexAttribArray(vertexPositionAttribute)开启attribute的数组传递(大概是这样吧?);

  3.绑定我们创建并填充了点信息的那块verticesBuffer,gl.bindBuffer(gl.ARRAY_BUFFER, verticesBuffer);

  4.让步骤1中的shader的attribute指向这块buffer,gl.vertexAttribPointer(vertexPositionAttribute, 3, gl.FLOAT, false, 0, 0),我们之前创建buffer时传递的数据都是一维的,那第2个参数3就用来说明每3个数组元素组成一个attribute(其实就是一个vector3,代表点的位置)。表示颜色的attribute的过程与此类似。

  接着,我们看phase 2部分的代码,这里就是设置观察变换矩阵、投影变换矩阵,并作为uniform传递给shader使用的代码。

  1.projectMatrix = makeOrtho(-10.0, 10.0, -10.0, 10.0, 1.0, 100.0),创建正交投影矩阵,用于投影变换;

  2.worldMatrix = Matrix.I(4);worldMatrix = worldMatrix.x(Matrix.RotationZ(0.6).ensure4x4());创建worldMatrix,并绕z轴旋转,这里其实就是在进行世界坐标系中的变换(平移、选择、缩放);

  3.viewMatrix= Matrix.I(4);modelviewMatrix = modelviewMatrix.x(Matrix.Translation($V([0.0, 0.0, -95.0])).ensure4x4());创建viewMatrix,并进行平移,这里之所以要进行平移,是因为我们的点的z轴设置的都是0,而我们的摄像机的z轴范围是1到100,进行这个平移,以便摄像机能看到这些点,实际上就是从世界坐标系到观察坐标系的一个变换;

  4.pUniform = gl.getUniformLocation(shaderProgram, 'uPMatrix'),与attribute类似,会返回一个“位置”,这个位置可以理解成shader中对名为uPMatrix这个uniform的引用(指针);

  5.gl.uniformMatrix4fv(pUniform, false, new Float32Array(projectMatrix.flatten()));设置这个uniform的数据,那个flatten是将2维的矩阵转成1维数组的方法。其它的uniform设置与此类似。

  最后,我们调用gl.drawArrays(gl.TRIANGLES, 0, 3); // (mode, first, count of point used to draw),告诉程序以三角形的模式绘制,使用3个点。关于模式的参数,可以参考这里:http://fly.cc.fer.hr/~unreal/theredbook/figures/fig2-6.gif

 (四)关于shader

  一切看起来都挺好,但是,shader呢?没有shader来进行真正的处理,传递这些数据是一点用处也没有的啊。我们就来依次来看看2个shader。

  首先是vertex shader:

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

    uniform mat4 uVMatrix;
    uniform mat4 uWMatrix;
    uniform mat4 uPMatrix;

    varying lowp vec4 vColor;
    //varing lowp vec2 vTextureCoord;

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

  可以看到,vertex shader我们定义了2个attribute,分别表示点的位置和点的颜色信息;3个uniform,分别表示世界变换矩阵、观察变换矩阵、投影变换矩阵,这些值通过程序传递给shader。我们还定义了一个varying,用于传递给fragmeng shader颜色信息(因为vertex shader实际上无法操作像素,所以把颜色信息传递下去比较合理)。

  gl_Position = uPMatrix * uVMatrix * uWMatrix * vec4(aVertexPosition, 1.0);就是对每一个点进行对应的变换,先是世界坐标中的变换(乘以uWMatrix);然后是观察坐标变换(乘以uVMatrix),最后投影变换(乘以uPMatrix)。然后赋值给shader内置的变量gl_Position,表示点的最终计算出的位置。

  vColor = aVertexColor;将传递进来的点的颜色信息,直接赋值给vColor,以便fragment shader使用。

  然后,看看fragment shader:

<script id="shader-fs" type="x-shader/x-fragment">
    //uniform sampler2D uSampler;

    varying lowp vec4 vColor;
    //varing lowp vec2 vTextureCoord;

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

  可以看到,fragment shader定义了一个varying,用于接收vertex shader传递的颜色信息;然后将这个颜色信息赋值给shader的内置变量gl_FragColor,表示顶点颜色。光栅化时,实际上,会对顶点表示的这个图元(三角形)的像素颜色进行插值,然后确定出最终颜色,用来插值的就是顶点颜色。

  所以,最后的效果就是这样:

WebGL学习笔记【一】概述及三角形

你可能感兴趣的:(WebGL)