转自:https://www.jianshu.com/p/e3d8a244f3d9

目录

  1. 初始化WebGL环境
  2. 顶点着色器(Vertex Shader)与片元着色器(Fragment Shader)
  3. 顶点数组对象(VBO)、索引数值对象(IBO)
  4. 绘制流程
  5. 总结

初始化WebGL环境

关于HTML5、<canvas>标签、WebGL的一些相关知识可以去MDN中查看,里面还有一些相关的学习干货,初始化WebGL环境可以参考初识WebGL,我们这里按下不表。

顶点着色器与片元着色器

WebGL图形渲染管线

介绍着色器之前,我们先过一下WebGL的图形管线:

 
WebGL Rendering Pipeline Overview

我们可以把WebGL的渲染管线当做一条车间里的流水线,这个车间的原材料是一些图形相关的数据(包括顶点坐标,顶点颜色等),这个车间生产的产品是我们屏幕上看到的各种图形。

图中绿色的两个方块就是我们要说的顶点着色器(Vertex Shader)片元着色器(Fragment Shader)

我们来一步一步简单介绍下这个条流水线:
图中最上面的蓝色方块Attribute可以看做是一条水管,一端连接顶点数据(Vertex Buffer Objects),另一端连接顶点着色器,这条水管的作用是把顶点数据输送给顶点着色器处理。紧接着,顶点着色器把处理过的顶点数据交给片元着色器处理,最后经过片元着色器处理过的数据将被输送到Framebuffer中去,为了便于理解,我们暂且、姑且可以把这个Framebuffer当做屏幕,而这最后的步骤,也就是把这些处理过的数据以图形的方式显示到屏幕上,至此,渲染管线也就完成了他的使命。

在这条流水线上,写代码的我们,扮演着的是流水线上的工人,所以我们做的事情是拿来数据,然后确保数据在这条流水线上,按照既定的流程,最终可以变成我们想要的图形。

顶点着色器与片元着色器

由上所述,WebGL编程中,我们需要为渲染流水线构建好顶点着色器和片元着色器。

顶点着色器的功能是对传进来的顶点数据通过矩阵进行换换位置、计算照明方程式以生成逐顶点的颜色以及生成或者变换纹理坐标。
片元着色器则是对即将送到屏幕上的像素内容进行更进一步的处理,包括一些特殊效果的定制等。

这两者的内容会在之后的学习过程中加以说明,本文只需对它们的作用有个大致的了解即可,更多的内容可以参考《OpenGL编程指南》或者是《OpenGL ES 3.0编程指南》,我们这里重点只在于简单整理下WebGL的渲染流程。

创建及使用着色器(Shader)

简要梳理下shader的创建过程。shader的作用前文已经介绍过了,我们可以把shader当做一个程序,顶点数据输入shader,输出经过shader处理过的数据,用于之后的渲染流程。

创建和使用shader的过程分为以下步骤:

  1. 首先创建并编译好着色器对象
  2. 将这些着色器对象链接为一个着色器程序。

对于着色器对象

  1. 创建一个着色器对象
  2. 将着色器源代码编译为对象
  3. 验证着色器的编译是否成功

创建着色器代码如下:

// 创建一个着色器对象
var vertexShader = gl.createShader(gl.VERTEX_SHADER); // 指定着色器源代码
gl.shaderSource(vertexShader , vertexshaderSourceCode); // 将着色器源代码编译为对象
gl.compileShader(vertexShader ); // 验证着色器的编译是否成功
if (!gl.getShaderParameter(vertexShader , gl.COMPILE_STATUS)) {
alert(gl.getShaderInfoLog(vertexShader ));
return;
} // 以上的代码创建了一个顶点着色器,对于片元着色器
var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
// 以下代码类似
// code gose here ...

创建完着色器对象后,我们得到了两个着色器 vertexShader 和 fragmentShader

对于着色器程序:

  1. 创建一个着色器程序
  2. 将着色器对象关联到着色器程序
  3. 连接着色器程序
  4. 判断着色器的链接过程是否成功完成
  5. 使用着色器来处理顶点和片元

创建着色器程序代码如下:

// 创建一个着色器程序
var program = gl.createProgram(); // 将着色器对象关联到着色器程序
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader); // 连接着色器程序
gl.linkProgram(program); // 判断着色器的链接过程是否成功完成
if (!gl.getProgramParameter(prg, gl.LINK_STATUS)) {
alert("Could not initialise shaders");
} // 使用着色器来处理顶点和片元
gl.useProgram(program);

在大致了解过这条渲染流水线后,我们接着将要介绍下顶点数组对象和索引数组对象。

顶点数组对象(VBO)、索引数值对象(IBO)

顶点数组对象(VBO)

顶点数组对象(VBO)对应着图中最上方的红色的方块。顶点数组对象包含着WebGL要渲染的图形的数据。可以看成是流水线上的原材料。在WebGL的渲染过程中,这些图形的数据往往通过顶点进行储存和输送,顶点数组对象包含的数据一般包括顶点位置信息、顶点位置上的法线向量、纹理坐标等。

索引数组对象(IBO)

在图形的绘制过程中,我们把每3个顶点绘制成一个三角形,即图形学中“面”(surface)的含义,而后通过成千上万的面组成了我们空间中的三维模型。索引数组对象(IBO)的作用是告诉WebGL要通过什么样的顺序来将我们传入的顶点数据连接成面。为了便于理解,我们举个栗子:

 
绘制一个梯形

当我们按照如图所示的方式绘制一个梯形时,我们弄来了5个顶点(0, 0)、(10, 10)、(20, 0)、(30, 10)、(40, 0)分别对应从0-4的五个索引。而当我们绘制图形时,定义的索引数组[0, 2, 1, 1, 2, 3, 2, 4, 3]的意思就是告诉WebGL,把索引为0、2、1的三个顶点组成一个三角形,索引为1、2、3的三个顶点组成另一个三角形,索引为2、4、3的顶点也组成一个三角形(可参照图示)。
索引数组对象的作用就是保存这些索引数组的数据,用以传输给WebGL渲染管线。

创建顶点数组对象&索引数组对象

WebGL状态机

在介绍如何创建这两个对象前,我们要先知道,WebGL是个状态机。我们可以这么理解,假设WebGL中的属性P的值为1,你在某一次操作中,把P的值设置成了2,那么在你下一次设置P的值之前,P的值永远是2。

更直观一点的表述,你可以想象WebGL像KFC套餐,有个套餐A,里面包含一个香辣鸡腿堡,一杯百事可乐。你每次点A套餐,都会得到一个香辣鸡腿堡,一杯百事可乐,而你在每次用餐的过程中,吃的汉堡都是香辣鸡腿堡,喝的可乐都是百事可乐。直到某一天,KFC把A套餐的内容改成了一个奥尔良烤鸡腿堡,一杯可口可乐。在那天之后,同样是A套餐,但是你吃的汉堡就变成了奥尔良烤鸡腿堡,喝的可乐变成了可口可乐,除非KFC对A套餐的内容再进行调整,不然点A套餐的你只能喝上一辈子的可口可乐(本人比较喜欢百事可乐)。

所以你可以把WebGL上下文想象成是A套餐,WebGL中的一些属性选项的对应的是汉堡、可乐,而香辣鸡腿堡、奥尔良烤鸡腿堡、百事可乐、可口可乐对应的是属性当前的值。类比一下。

顶点数组对象的创建

接下来插播一段代码:

var vertices = [
-50.0, 50.0, 0.0,
-50.0,-50.0, 0.0,
50.0,-50.0, 0.0,
50.0, 50.0, 0.0
]; // 调用gl.createBuffer()创建了一个块内存空间,并让 vertexBufferObject 变量指向这块空间
var vertexBufferObject = gl.createBuffer(); // 把WebGL中用于绘制的数组数据的属性(ARRAY_BUFFER)的值的地址指向上文我们创建的 vertexBuffer 内存空间。
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBufferObject ); // bufferData 函数对 ARRAY_BUFFER 属性对应的空间填充值
// WebGL 状态机概念出场!
// 由于我们在上一个语句中,修改了 ARRAY_BUFFER 的值,由于状态机的性质,所以我们调用该函数进行传值时,
// 传入的值是会给此时gl中的 ARRAY_BUFFER 属性指向的空间,也就是
vertexBufferObject 的内存空间。
// 此处调用把 vertices 中的顶点数据传入了 vertexBufferObject 对象
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); // 传值完毕,恢复 ARRAY_BUFFER 的值为空
gl.bindBuffer(gl.ARRAY_BUFFER, null);

所以这段代码就创建了一个名为 vertexBufferObject 的顶点数组对象。代码中具体的API可以查阅相关的资料。

索引数组对象的创建

索引数组对象的创建与顶点数组对象的创建大同小异

var indices = [
, , ,
, ,
];
var indicesBufferObject = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indicesBufferObject );
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices),
gl.STATIC_DRAW);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);

这里需要注意的是,索引数组对象对应的属性为ELEMENT_ARRAY_BUFFER。而传入的数组类型是Uint16Array。

绘制流程

最后,我们要把前面提到的内容按照渲染管线的图示结合起来。再看一眼我们的地图:

 
WebGL Rendering Pipeline Overview

好,上代码:

// 按照图示来看,要绘制一个图形,首先我们需要原材料:
// 1. 一个顶点数组对象(VBO),用于存储相关的顶点数据,
// 同时需要一个表示顶点组合顺序的索引数组对象(IBO)。 // 2. 需要两个着色器(vertex shader 及 fragment shader)和一个着色器程序,用来保证管线的顺利进行。
// 好了,开工! // 先来VBO和IBO
var vertices = [
-0.5,0.5,0.0,
-0.5,-0.5,0.0,
0.5,-0.5,0.0,
0.5,0.5,0.0
]; var indices = [,,,,,]; // 创建VBO
var vertexBufferObj = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBufferObj );
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, null); // 创建IBO
var indexBufferObj = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBufferObj );
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null); // 创建顶点着色器
var vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader , vertexshaderSourceCode);
gl.compileShader(vertexShader );
// 验证着色器的编译是否成功
if (!gl.getShaderParameter(vertexShader , gl.COMPILE_STATUS)) {
alert(gl.getShaderInfoLog(vertexShader ));
} // 同理创建片元着色器
var fragmentShader= gl.createShader(gl.FRAGMENT_SHADER);
/* so many balabalabala... */ // 创建着色器程序
var program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
// 判断着色器的链接过程是否成功完成
if (!gl.getProgramParameter(prg, gl.LINK_STATUS)) {
alert("Could not initialise shaders");
}

目前为止,我们已经准备好了绘制图形的所有原料了,接下里就是如何通过WebGL绘制出图形了。

// 先获取顶点数据进入着色器的入口(后文会讲解)
program.vertexPosition = gl.getAttribLocation(program, "aVertexPosition"); // 绘制场景的函数
function drawScene(){
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.enable(gl.DEPTH_TEST); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.viewport(, , c_width, c_height); // 指定绘制时使用的顶点数据
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBufferObj );
// 一下这两行代码对应的作用是将顶点数据读入着色器中,后文会加以解释
gl.vertexAttribPointer(program.aVertexPosition, , gl.FLOAT, false, , );
gl.enableVertexAttribArray(program.vertexPosition); // 指定绘制时使用的索引数组
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBufferObj);
// 以给定的形式绘制图形
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT,);
}

这样我们就实现了图形的绘制。
由于本文重点在于梳理绘制流程,并没有深入介绍着色器相关的内容,所以对于gl.vertexAttribPointer函数和gl.enableVertexAttribArray会有疑惑。
为了解释一下这个问题,我们先看一段顶点着色器的代码

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

其中代码中的aVertexPosition表示的是顶点数据从这个变量进入着色器。

就像我们前文比喻的那样,这个aVertexPosition就像一端连接顶点数据(Vertex Buffer Objects),另一端连接顶点着色器的水管的开关。

我们执行program.vertexPosition = gl.getAttribLocation(program, "aVertexPosition");时,相当于把这个开关的位置记录在program.vertexPosition上面。

以下代码:

gl.bindBuffer(gl.ARRAY_BUFFER, vertexBufferObj );
gl.vertexAttribPointer(prg.vertexPosition, , gl.FLOAT, false, , );
gl.enableVertexAttribArray(program.vertexPosition);

bindBuffer指定当前WebGL绘制的数据为vertexBufferObj中的顶点数据。

vertexAttribPointer函数则把顶点数据通过水管接到着色器"aVertexPosition"的位置上,也就是我们记录下来的program.vertexPosition的位置。

gl.enableVertexAttribArray(program.vertexPosition)则是最后一步把水管的阀门打开,让顶点数据流入着色器。

总结

本文主要目的是简单梳理了下WebGL绘制图形的大致编程流程,并没有做很深入的讲解。WebGL基于OpenGL ES 2.0,而早先的OpenGL用的都是固定的渲染管线,之前学OpenGL一开始都是glBegin,glEnd,glVertex2d啥的一堆东西,精简版的OpenGL ES一上来就抛弃了之前固定渲染管线的东西,使用了可编程的渲染管线,一上来就要求要编写Shader,所以绘制一个图形就变得比较繁琐一点。于是有了这篇笔记,以供刚开始学习WebGL的玩家们参考。
对于文中的一些API并没有进入详细的讲解,需要的同学可以查阅相关的资料书籍。

相关资料

《OpenGL编程指南》
《OpenGL ES 3.0编程指南》
《WebGL Beginner's Guide》

 

一篇文章理清WebGL绘制流程的更多相关文章

  1. JAVA:一篇文章理清多态

    很多人总是喜欢,或者说错误地将JAVA中的多态理解得很复杂,最常见的错误说法就是所谓"方法的多态",他们会给出类似下面的例子来佐证"多态是指方法的多态": // ...

  2. 一篇文章教你读懂UI绘制流程

    最近有好多人问我Android没信心去深造了,找不到好的工作,其实我以一个他们进行回复,发现他们主要是内心比较浮躁,要知道技术行业永远缺少的是高手.建议先阅读浅谈Android发展趋势分析,在工作中, ...

  3. Android视图绘制流程完全解析,带你一步步深入了解View(二)

    转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/16330267 在上一篇文章中,我带着大家一起剖析了一下LayoutInflater ...

  4. 一篇文章一张思维导图看懂Android学习最佳路线

    一篇文章一张思维导图看懂Android学习最佳路线 先上一张android开发知识点学习路线图思维导图 Android学习路线从4个阶段来对Android的学习过程做一个全面的分析:Android初级 ...

  5. WebGL绘制有端头的线

    关于WebGL绘制线原理不明白的小伙伴,可以看看我之前的文章WebGL绘制有宽度的线.这一篇我们主要来介绍端头的绘制,先看效果图. 端头一般被称为lineCap,主要有以下三种形式: butt最简单等 ...

  6. WebGL 绘制Line的bug(二)

    上一篇文章简单介绍了WebGL绘制Line的bug,不少朋友给我发了私信,看来这个问题大家都遇上过哈.今天这篇文章会讲述解决这个问题的work around. 基本思路 上一篇文章结尾简单提了下解决的 ...

  7. RecyclerView 源码分析(一) —— 绘制流程解析

    概述 对于 RecyclerView 是那么熟悉又那么陌生.熟悉是因为作为一名 Android 开发者,RecyclerView 是经常会在项目里面用到的,陌生是因为只是知道怎么用,但是却不知道 Re ...

  8. Android应用层View绘制流程与源码分析

    1  背景 还记得前面<Android应用setContentView与LayoutInflater加载解析机制源码分析>这篇文章吗?我们有分析到Activity中界面加载显示的基本流程原 ...

  9. Android:学习AIDL,这一篇文章就够了(下)

    前言 上一篇博文介绍了关于AIDL是什么,为什么我们需要AIDL,AIDL的语法以及如何使用AIDL等方面的知识,这一篇博文将顺着上一篇的思路往下走,接着介绍关于AIDL的一些更加深入的知识.强烈建议 ...

随机推荐

  1. css中“~”和“>”是什么意思

    p~ul选择器 p之后出现的所有ul. 两种元素必须拥有相同的父元素,但是 ul不必直接紧随 p. css中“>”是: css3特有的选择器,A>B 表示选择A元素的所有子B元素. 与A ...

  2. 201621123018《Java程序设计》第5周学习报告

    1. 本周学习总结 1.1 写出你认为本周学习中比较重要的知识点关键词 接口.interface.implements.Comparable.Comparator. 1.2 尝试使用思维导图将这些关键 ...

  3. Linux系统文件与目录管理(1)

    文件与目录的操作 在Linux系统的文件与目录的管理上,不外乎『显示属性』.『拷贝』.『删除文件』.『移动文件或目录』.『重命名』等常用操作,由于文件与目录的管理在 Linux当中是很重要的,尤其是每 ...

  4. Swift5 语言指南(二十) 类型转换

    类型转换是一种检查实例类型的方法,或者将该实例视为与其自己的类层次结构中的其他位置不同的超类或子类. Swift中的类型转换是使用is和as运算符实现的.这两个运算符提供了一种简单而富有表现力的方法来 ...

  5. updateByPrimaryKey 和 updateByPrimaryKeySelective

    1. 数据库记录 2. updateByPrimaryKey Preparing: UPDATE t_token_info SET entity_id = ?,entity_type = ?,time ...

  6. POJ 2853

    #include<iostream> #include<stdio.h> #include<vector> #include<math.h> #incl ...

  7. canvars 画花

    index.html <!DOCTYPE html><html><head> <title>旋转的花</title> <meta ch ...

  8. 课程一(Neural Networks and Deep Learning),第二周(Basics of Neural Network programming)—— 3、Python Basics with numpy (optional)

    Python Basics with numpy (optional)Welcome to your first (Optional) programming exercise of the deep ...

  9. Python多线程-Barrier(障碍对象)

    Barrier(parties, action=None, timeout=None) 每个线程通过调用wait()尝试通过障碍,并阻塞,直到阻塞的数量达到parties时,阻塞的线程被同时全部释放. ...

  10. SQL Server性能优化(6)查询语句建议

    1. 如果对数据不是工业级的访问(允许脏读),在select里添加 with(nolock) ID FROM Measure_heat WITH (nolock) 2. 限制结果集的数据量,如使用TO ...