图解WebGL&Three.js工作原理

CarrolMJN 7年前
   <p>“哥,你又来啦?”</p>    <p>“是啊,我随便逛逛。”</p>    <p>“别介啊……给我20分钟,成不?”</p>    <p>“5分钟吧,我很忙的。”</p>    <p>“不行,20分钟,不然我真很难跟你讲清楚。”</p>    <p>“好吧……”</p>    <p>“行,那进来吧,咱好好聊聊”</p>    <h2>一、我们讲什么?</h2>    <p>我们讲两个东西:</p>    <p>1、WebGL背后的工作原理是什么?</p>    <p>2、以Three.js为例,讲述框架在背后扮演什么样的角色?</p>    <h2>二、我们为什么要了解原理?</h2>    <p>我们假定你对WebGL已经有一定了解,或者用Three.js做过了一些东西,这个时候,你可能碰到了这样一些问题:</p>    <p>1、很多东西还是做不出来,甚至没有任何思路;</p>    <p>2、碰到bug无法解决,甚至没有方向;</p>    <p>3、性能出现问题,完全不知道如何去优化。</p>    <p>这个时候,我们需要了解更多。</p>    <h2>三、先了解一个基础概念</h2>    <h3>1、什么是矩阵?</h3>    <p>简单说来,矩阵用于坐标变换,如下图:</p>    <p><img src="https://simg.open-open.com/show/4d71d40a6ec6f49d6c27d107ede91486.png"></p>    <h3>2、那它具体是怎么变换的呢,如下图:</h3>    <p><img src="https://simg.open-open.com/show/6fa4350453678758e73daa6721b8c938.png"></p>    <h3>3、举个实例,将坐标平移2,如下图:</h3>    <p><img src="https://simg.open-open.com/show/873eff1b2575a927b8fb872600bd5157.png"></p>    <p>如果这时候,你还是没有理解,没有关系,你只需要知道,矩阵用于坐标变换。</p>    <h2>四、WebGL的工作原理</h2>    <h3>4.1、WebGL API</h3>    <p>在了解一门新技术前,我们都会先看看它的开发文档或者API。</p>    <p>查看Canvas的绘图API,我们会发现它能画直线、矩形、圆、弧线、贝塞尔曲线。</p>    <p>于是,我们看了看WebGL绘图API,发现:</p>    <p><img src="https://simg.open-open.com/show/e785f336afea5ddfebe181a00730ac5d.png"></p>    <p>它只能会点、线、三角形?一定是我看错了。</p>    <p>没有,你没看错。</p>    <p><img src="https://simg.open-open.com/show/98f4dbac46b0bfa01f2bd7785e11b118.png"></p>    <p>就算是这样一个复杂的模型,也是一个个三角形画出来的。</p>    <h3>4.2、WebGL绘制流程</h3>    <p>简单说来,WebGL绘制过程包括以下三步:</p>    <p>1、获取顶点坐标</p>    <p>2、图元装配(即画出一个个三角形)</p>    <p>3、光栅化(生成片元,即一个个像素点)</p>    <p><img src="https://simg.open-open.com/show/9a9918ec622c2fe18b6ad62f2377310d.png"></p>    <p>接下来,我们分步讲解每个步骤。</p>    <p>4.2.1、获取顶点坐标</p>    <p>顶点坐标从何而来呢?一个立方体还好说,如果是一个机器人呢?</p>    <p>没错,我们不会一个一个写这些坐标。</p>    <p>往往它来自三维软件导出,或者是框架生成,如下图:</p>    <p><img src="https://simg.open-open.com/show/63a1f4b1851b0e90ffc98f097595dd3b.png"></p>    <p>写入缓存区是啥?</p>    <p>没错,为了简化流程,之前我没有介绍。</p>    <p>由于顶点数据往往成千上万,在获取到顶点坐标后,我们通常会将它存储在显存,即缓存区内,方便GPU更快读取。</p>    <p>4.2.2、图元装配</p>    <p>我们已经知道,图元装配就是由顶点生成一个个图元(即三角形)。那这个过程是自动完成的吗?答案是并非完全如此。</p>    <p>为了使我们有更高的可控性,即自由控制顶点位置,WebGL把这个权力交给了我们,这就是可编程渲染管线(不用理解)。</p>    <p>WebGL需要我们先处理顶点,那怎么处理呢?我们先看下图:</p>    <p><img src="https://simg.open-open.com/show/63963f39c0c86968dd8f9ca872e88e9e.png"></p>    <p>我们引入了一个新的名词,叫“顶点着色器”,它由opengl es编写,由javascript以字符串的形式定义并传递给GPU生成。</p>    <p>比如如下就是一段顶点着色器代码:</p>    <pre>  <code class="language-javascript">attribute vec4 position;  void main() {    gl_Position = position;    }  </code></pre>    <p>attribute修饰符用于声明由浏览器(javascript)传输给顶点着色器的变量值;</p>    <p>position即我们定义的顶点坐标;</p>    <p>gl_Position是一个内建的传出变量。</p>    <p>这段代码什么也没做,如果是绘制2d图形,没问题,但如果是绘制3d图形,即传入的顶点坐标是一个三维坐标,我们则需要转换成屏幕坐标。</p>    <p>比如:v(-0.5, 0.0, 1.0)转换为p(0.2, -0.4),这个过程类似我们用相机拍照。</p>    <p>4.2.2.1、顶点着色器处理流程</p>    <p>回到刚才的话题,顶点着色器是如何处理顶点坐标的呢?</p>    <p><img src="https://simg.open-open.com/show/669a9f76702c733830b844d08271b254.png"></p>    <p>如上图,顶点着色器会先将坐标转换完毕,然后由GPU进行图元装配,有多少顶点,这段顶点着色器程序就运行了多少次。</p>    <p>你可能留意到,这时候顶点着色器变为:</p>    <pre>  <code class="language-javascript">attribute vec4 position;  uniform mat4 matrix;  void main() {    gl_Position = position * matrix;    }  </code></pre>    <p>这就是应用了矩阵matrix,将三维世界坐标转换成屏幕坐标,这个矩阵叫投影矩阵,由javascript传入,至于这个matrix怎么生成,我们暂且不讨论。</p>    <p>4.2.3、光栅化</p>    <p>和图元装配类似,光栅化也是可控的。</p>    <p><img src="https://simg.open-open.com/show/cf3bf266fb35806713ba804d20fc57f7.png"></p>    <p>在图元生成完毕之后,我们需要给模型“上色”,而完成这部分工作的,则是运行在GPU的“片元着色器”来完成。</p>    <p>它同样是一段opengl es程序,模型看起来是什么质地(颜色、漫反射贴图等)、灯光等由片元着色器来计算。</p>    <p>如下是一段简单的片元着色器代码:</p>    <pre>  <code class="language-javascript">precision mediump float;    void main(void) {       gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);   }   </code></pre>    <p>gl_FragColor即输出的颜色值。</p>    <p>4.2.3.1、片元着色器处理流程</p>    <p>片元着色器具体是如何控制颜色生成的呢?</p>    <p><img src="https://simg.open-open.com/show/94340cfd5fe92a41b78e398394f4057d.png"></p>    <p>如上图,顶点着色器是有多少顶点,运行了多少次,而片元着色器则是,生成多少片元(像素),运行多少次。</p>    <h3>4.3、WebGL的完整工作流程</h3>    <p>至此,实质上,WebGL经历了如下处理流程:</p>    <p>1、准备数据阶段</p>    <p>在这个阶段,我们需要提供顶点坐标、索引(三角形绘制顺序)、uv(决定贴图坐标)、法线(决定光照效果),以及各种矩阵(比如投影矩阵)。</p>    <p>其中顶点数据存储在缓存区(因为数量巨大),以修饰符attribute传递给顶点着色器;</p>    <p>矩阵则以修饰符uniform传递给顶点着色器。</p>    <p>2、生成顶点着色器</p>    <p>根据我们需要,由Javascript定义一段顶点着色器(opengl es)程序的字符串,生成并且编译成一段着色器程序传递给GPU。</p>    <p>3、图元装配</p>    <p>GPU根据顶点数量,挨个执行顶点着色器程序,生成顶点最终的坐标,完成坐标转换。</p>    <p>4、生成片元着色器</p>    <p>模型是什么颜色,看起来是什么质地,光照效果,阴影(流程较复杂,需要先渲染到纹理,可以先不关注),都在这个阶段处理。</p>    <p>5、光栅化</p>    <p>能过片元着色器,我们确定好了每个片元的颜色,以及根据深度缓存区判断哪些片元被挡住了,不需要渲染,最终将片元信息存储到颜色缓存区,最终完成整个渲染。</p>    <p><img src="https://simg.open-open.com/show/ed94a150fceabb102d82a3ce7d2cce24.png"></p>    <h2>五、Three.js究竟做了什么?</h2>    <p>我们知道,three.js帮我们完成了很多事情,但是它具体做了什么呢,他在整个流程中,扮演了什么角色呢?</p>    <p>我们先简单看一下,three.js参与的流程:</p>    <p><img src="https://simg.open-open.com/show/f31068f65851bb090a85eb4f06fba35a.png"></p>    <p>黄色和绿色部分,都是three.js参与的部分,其中黄色是javascript部分,绿色是opengl es部分。</p>    <p>我们发现,能做的,three.js基本上都帮我们做了。</p>    <ul>     <li>辅助我们导出了模型数据;</li>     <li>自动生成了各种矩阵;</li>     <li>生成了顶点着色器;</li>     <li>辅助我们生成材质,配置灯光;</li>     <li>根据我们设置的材质生成了片元着色器。</li>    </ul>    <p>而且将webGL基于光栅化的2D API,封装成了我们人类能看懂的 3D API。</p>    <h3>5.1、Three.js顶点处理流程</h3>    <p>从WebGL工作原理的章节中,我们已经知道了顶点着色器会将三维世界坐标转换成屏幕坐标,但实际上,坐标转换不限于投影矩阵。</p>    <p>如下图:</p>    <p><img src="https://simg.open-open.com/show/7bf26e7e8a6949602e543d43cdf9cb5a.png"></p>    <p>之前WebGL在图元装配之后的结果,由于我们认为模型是固定在坐标原点,并且相机在x轴和y轴坐标都是0,其实正常的结果是这样的:</p>    <p><img src="https://simg.open-open.com/show/5e748316cf780aba5189cce1b7ef5322.png"></p>    <p>5.1.1、模型矩阵</p>    <p><img src="https://simg.open-open.com/show/31d854b21366ac7a0e9dedb76780d48e.png"></p>    <p>现在,我们将模型顺时针旋转Math.PI/6,所有顶点位置肯定都变化了。</p>    <pre>  <code class="language-javascript">box.rotation.y = Math.PI/6;  </code></pre>    <p>但是,如果我们直接将顶点位置用javascript计算出来,那性能会很低(顶点通常成千上万),而且,这些数据也非常不利于维护。</p>    <p>所以,我们用矩阵modelMatrix将这个旋转信息记录下来。</p>    <p>5.1.2、视图矩阵</p>    <p><img src="https://simg.open-open.com/show/adc769e416f8a07845430490f33b5e8d.png"></p>    <p>然后,我们将相机往上偏移30。</p>    <pre>  <code class="language-javascript">camera.position.y = 30;  </code></pre>    <p>同理,我们用矩阵viewMatrix将移动信息记录下来。</p>    <p>5.1.3、投影矩阵</p>    <p><img src="https://simg.open-open.com/show/20a0cdafca838b0348c5cd6ca5eaacc7.png"></p>    <p>这是我们之前介绍过的了,我们用projectMatrix记录。</p>    <p>5.1.4、应用矩阵</p>    <p>然后,我们编写顶点着色器:</p>    <pre>  <code class="language-javascript">gl_Position = position * modelMatrix * viewMatrix * projectionMatrix;  </code></pre>    <p>这样,我们就在GPU中,将最终顶点位置计算出来了。</p>    <p>实际上,上面所有步骤,three.js都帮我们完成了。</p>    <p><img src="https://simg.open-open.com/show/da53cedddf155b3b43d1635886f62cfc.png"></p>    <h3>5.2、片元着色器处理流程</h3>    <p>我们已经知道片元着色器负责处理材质、灯光等信息,但具体是怎么处理呢?</p>    <p>如下图:</p>    <p><img src="https://simg.open-open.com/show/2884b2184bffd8993ac1b1cdb24f62aa.png"></p>    <h3>5.3、three.js完整运行流程:</h3>    <p><img src="https://simg.open-open.com/show/a5ceb4ea78af9f5d8f8bdd86a55a4a1d.png"></p>    <p>当我们选择材质后,three.js会根据我们所选的材质,选择对应的顶点着色器和片元着色器。</p>    <p>three.js中已经内置了我们常用着色器。</p>    <p> </p>    <p>来自:http://web.jobbole.com/91357/</p>    <p> </p>