【GISER&&Painter】Chapter01:WebGL渲染初体验

基于上一篇OpenGL的渲染原理,这两周又陆续接触了一些关于WebGL绘图的一些内容,因为刚入门,很多东西又很晦涩,所以特意花了小半天的时间整理了一下,特此记录。

零  画一个多边形吧!

  把一个多边形画上屏幕分几步?

  答:分三步,第一,打开屏幕(1-1.从HTML中获取Canvas对象;1-2.从Canvas拿到WebGL的Context);第二,把数据画好(2-1.编译着色器;2-2.准备数据模型;2-3.顶点缓存VBO的生成和通知);第三,画上去(3-1.发出绘图命令,更新Canvas并渲染)。

   画布和画笔:创建Canvas && 获取WebGL的Context

  在开始WebGL的绘制故事之前,我们得先来认识一下Canvas,因为这玩意是我们之后绘图的基础底板:“Canvas元素创造了一个固定大小画布,并提供了一个或多个渲染上下文,用来绘制和处理要展示的内容”(摘自MDN)。按照定义,我们可以将其理解为渲染任务的中转站,因为最终绘图的输出是要将数据交给屏幕展示的,Canvas只是作为中间暂存待渲染数据的中转站,也可以看作是一个具有仿屏幕像素矩阵数据结构的容器,大概类似于中间缓存一类的概念。那为啥不直接在屏幕上输出画出来呢?之前看到过这样一个解释,缓存的作用在于下一帧没有及时渲染出来的时候(渲染时间超出了人眼的最低感知帧率,最低帧率为24帧),当前帧的数据就能够替代下一帧,以此来保证过程的完整性。

  WebGL的API提供了能够在支持的浏览器中无插件地绘制交互式的2D/3D图像,而WebGL中最重要的对象:渲染上下文WebGLRenderingContext是基于OpenGL ES 2.0的绘图上下文所实现,一般用在HTML5的<canvas>元素内绘图等任务上。而WebGLRenderingContext可以看作是渲染任务的“核心CPU“,所有的操作:从视口剪裁、状态信息、数据缓冲区、着色器的创建和调用、缓冲区绘制等等任务,都由WebGLRenderingContext调配和使用,所以,在任何Web程序开始绘制之前,我们所需要做的第一件事情就是创建一个画布作为数据容器,并将其与WebGL的上下文进行绑定,这样一来,我们既有了画布,又有了画笔,就可以开始绘制我们想要的图形了。

二  调色盘:着色器

  在传统的OpenGL的固定管线中,我们对于渲染过程的控制力度是有限的。从上一篇OpenGL基础里来看,在顶点操作和图元装配、纹理化、片元着色等过程中,我们能控制的只有调用底层硬件厂商提供的接口参数,使用固定的程序去进行上述过程的处理。这种级别的控制非常弱,记得不久前在知乎上看到了一个关于固定管线控制的比喻:“扳开关”,我觉得十分贴切,这个概念有点类似于铁道上的扳道工,火车的前进方向只能在铺设好的轨道上选择,如果没有轨道的地方,火车自然就没法开到。渲染同理,如果对于渲染效果有更高更灵活的要求(或者你无法接受硬件厂商提供的笨重不堪的渲染参数配置,想用更能让人接受的方式去定制属于自己的渲染结果),那么固定管线的处理方式就基本可以不考虑了。

图1 扳开关 

  那懒人就不能拿起画笔在屏幕上作画了吗?幸运的是,我们遇到了可编程渲染管线。它的出现给上面遇到的难题带来的答案,从图2可以看出:可编程渲染管线中出现了两个处理器:顶点着色器vertex shader和片元着色器fragment shader。这两个处理器绕过了传统的顶点操作和图元装配、片元着色等过程,通过自身的可编程特性,分别接手控制了顶点坐标转换、像素颜色计算的工作:

1)在顶点着色器Vertex Shader的处理阶段,顶点数据从GPU显存中读取出来,顶点着色器Vertex Shader可以对每个顶点进行模型视图变换、投影变换等顶点处理工作,替代了固定管线中的顶点处理管线;

<!-- vertex shader -->
<script id="2d-vertex-shader" type="x-shader/x-vertex">
    attribute vec2 a_position;
    attribute vec4 a_color;

    uniform mat3 u_matrix;

    varying vec4 v_color;

    void main() {
        gl_Position = vec4((vec3(a_position, 1)).xy, 0, 1);

        v_color = a_color;
    }
</script>

2)顶点着色器处理完之后,管线会对各个顶点进行光栅化处理。由于顶点着色器的计算次数由顶点的数量决定,即n个顶点对应着n个顶点像素数据,但在一个由若干个像素组成的图元中,非顶点像素的颜色该如何确定呢?此时就需要给大家介绍一个新对象:数据类型Varyings,从下面一段简短的顶点着色器DEMO中可以看出来,我们在顶点着色器中定义了好几种数据类型,有attribute,uniform以及varying。但来得早不如来得巧,我们先来认识一下Varying。

  Varying是一个变量,作为一个信使连接着顶点着色器Vertex Shader和片元着色器Fragment Shader。在一般情况下,顶点着色器Vertex Shader会计算出每个顶点的颜色、坐标等值,并用Varying变量来存储这些值。回到刚才提出的问题,非顶点的像素如何确定自己的值呢?这就需要片元着色器来理解这个信使了,好在片元着色器和顶点着色器之间有个光栅器牵线搭桥,当顶点着色器传来Varying类型的顶点值时,光栅器会指定一种插值模式,指导片元着色器按像素逐个进行渲染绘制。

3)片元着色器Fragment Shader在光栅化工具(这个工具我也没有仔细研究,暂时当作一个黑盒吧)的指导下进行工作(片元在上一篇OpenGL基础中已经提到过了,所谓片元即指光栅化后的图元)。片元着色器fs的主要工作是为当前光栅化的像素提供颜色值,屏幕中的每一个像素都需要调用一次片元着色器Fragment Shader,每次调用都会从一个特殊的全局变量gl_FragColor中获取颜色信息。

<!-- fragment shader -->
<script id="2d-fragment-shader" type="x-shader/x-fragment">

    precision mediump float;

    varying vec4 v_color;

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

 

 

图2 可编程渲染管线

三 轮廓骨架线

  在前面的步骤大概能够初探一二之后,下一步就是在显存中创建顶点对象VBO了:所谓VBO,顶点缓冲区对象( Vertex buffer object )这个概念来自于OpenGL,其概念定义为一个将顶点Vertex的各类属性信息(如vertex坐标,vertex法向量以及颜色等)存储在显存的一块专用的缓存区中,在执行渲染命令时,可直接从显存中取出VBO。由于整个过程都是在GPU中进行,不同于之前传统的绘制方式(CPU命令GPU执行绘制动作,反复传输大量的顶点数据到GPU中,渲染速度较慢),所以一般将VBO视为一个能够改善数据传输效率的对象。

  那么VBO是如何在WebGL中应用的呢?我们通常第一步通过createBuffer方法创建一个缓存对象VBO,通过图3的MDN中的定义可以看出,返回值VBO可以是颜色或者顶点坐标值的缓冲对象。此时只需调用gl.bufferData向gl_ARRAY_BUFFER中写入数据,再使用gl. bindBuffer方法就可以把buffer数据与gl上下文中的ARRAY_BUFFER关联起来,也就是把顶点数据成功地写入了GPU显存中。

图3 WebGLBuffer

    function main(){
          ......
            //Create a buffer & bind buffer
            //创建buffer数据,并绑定到gl的上下文中
            var positionBuffer = gl.createBuffer();
            gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
            //set Geometry
            //填充buffer
            setGeometry(gl);
          ......
    }
function setGeometry(gl) { gl.bufferData( gl.ARRAY_BUFFER, new Float32Array([ -150, -100, 150, -100, -150, 100, 150, -100, -150, 100, 150, 100]), gl.STATIC_DRAW); }

  进行到上述阶段为止,渲染之前的初始化工作基本完成:画布空间已创建  -->  将画布Canvas与WebGL环境绑定 ---> 创建并指定了相关自定义着色器填充顶点数据 ---> 创建顶点缓存VBO。一切准备就绪,千军万马一般的屏幕像素们都在等着一个明确渲染绘图指令,只待gl令旗一挥,在GPU的指挥下,千万个屏幕像素将会按照规定的位置和颜色,以迅雷不及掩耳之势一蹴而就,以你所规定的样式完美地呈现在你的屏幕上。

四 画!

  一切准备都已完成,所有任务执行资源就位后,我们再来一起看看最后一步的绘图渲染命令是如何发出的。

  简单来看,我们可以把绘图渲染部分分为以下三部分:

    1)画布清洁

    2)指定环境

    3)执行着色程序

//Rendering code: 渲染代码
        function drawScene() {

       //-----------------------画布清洁--------------------- webglUtils.resizeCanvasToDisplaySize(gl.canvas);
//covert from clip space to pixels gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); //clear canvas gl.clear(gl.COLOR_BUFFER_BIT); //direct to our program gl.useProgram(program); //------------------------指定环境------------------------------- //打开属性attribute开关 gl.enableVertexAttribArray(positionAttributeLocation); //对当前状态进行绑定 : 绑定已经完成填充点数据的buffer数据块positionBuffer gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); // Tell the attribute how to get data out of positionBuffer (ARRAY_BUFFER) var size = 2; // 2 components per iteration var type = gl.FLOAT; // the data is 32bit floats var normalize = false; // don't normalize the data var stride = 0; // 步长(byte),每个顶点数据所占的字节数:0 = move forward size * sizeof(type) each iteration to get the next position var offset = 0; // start at the beginning of the buffer //vertexAttribPointer:顶点属性指路牌 //告诉显卡从当前绑定的缓冲区(drawScene方法中的bindBuffer)中读取顶点数据vertex data gl.vertexAttribPointer( positionAttributeLocation, size, type, normalize, stride, offset); //------------------------------执行着色程序-------------------------------------// Draw the geometry. var primitiveType = gl.TRIANGLES; //绘制图元模式 var offset = 0;//从缓冲区开始读取数据的首地址偏移first下标 var count = 6;//绘制顶点数据的个数,即Shader代码的运行次数 gl.drawArrays(primitiveType, offset, count); } //==================================================================================

 

  具体的WebGLRenderingContext提供的接口我在这里就不赘述了,这篇仅仅只是为了带给大家一个如何在浏览器中绘制渲染图形的概念,之后应该会有针对各个环节的专题,毕竟才刚刚入坑,来日方长!

 

posted @ 2017-12-15 20:54  ESCAGE  阅读(694)  评论(0编辑  收藏  举报