我们在第一篇里面浏览了一下WebGL的参考卡片,也列出了一些WebGL的功能点,但是对于没有接触过3D编程的人来说,这些东西很难理解并形成一个统一的认识。所以这篇我就以WebGL为参考平台谈谈3D编程的一些基础概念。
3D坐标系变换
正常来说,人们学习新东西都会从已有的知识出发,对于3D编程,在屏幕上画出3D图形,如果有2D图形API使用的基础,就会想着从2D编程的经验出发。比如对于2D编程,都是指定一个屏幕坐标,设定一个颜色,去画一个点,你能画点就能画任何东西,很简单很直观。但是到3D这儿就不行了,因为你没法简单直接且直观的指定一个立方体的各个点在屏幕上是什么位置。为了解决这个问题,在3D编程中,需要将3D空间的点变换到屏幕空间,即我们在3D空间指定点的坐标(一组3d坐标),然后设置一个摄像机,将摄像机镜头拍到的点映射到屏幕上,这样就可以绘制出来。这个过程就需要用不同的坐标空间,以及在这些坐标空间之间变换坐标。简单说一说。
- 模型空间
3D模型通常由很多三角形构成,而每个三角形又是由三个顶点构成。一个3D模型有很多顶点,这个顶点的坐标定义在模型自身的坐标系中,这个坐标系就是模型空间。坐标系的原点根据模型的形状特点以及使用的方便情况而设置。比如两足动物(人)的模型,通常将坐标原点放在两脚中间。而球的模型中心一般放在球心。使用模型空间一方面建模方便,另一方面同样的模型可以复用,例如世界场景中放置很多人。 - 世界空间
3D世界里面的物体是有层级关系的,比如人坐在车里,车在路上。每个物体的本地坐标就是其在父坐标系中的坐标,移动这个本地坐标就等于在其父坐标系中移动他的位置,而不需要修改其顶点的模型坐标。例如人坐在车上换了位置,换位置前后只是修改了其在车上的本地坐标,而人的模型上的所有顶点的坐标并没有改变,还是相对于人模型的中心点。父坐标系之上可能还有父坐标系,这样父坐标系空间的移动会带着其内部的子坐标系一起移动,而最终有一个最外层的坐标系是不会动的,这就是世界坐标系。因为我们要渲染世界中的模型,而每个模型在世界坐标系中有自己的位置,甚至有自己的父坐标系。为了渲染怎个世界,需要把模型坐标变换到世界坐标,这样所有的物体的所有顶点都在同一个坐标系中描述,WebGL就能把他们都渲染出来了。 - 视图空间
对于3D世界来说,我们会通过一个摄像机从某个角度去观察他,摄像机镜头内的物体才会绘制。摄像机一般也是放在世界中的,有他的本地坐标(但是由于摄像机不是一个实体模型,所以没有顶点,也就没有模型坐标)。另外摄像机还会有不同的朝向。通过摄像机去看物体,其实就是在摄像机的坐标系里面定义物体的位置,而我们知道物体的位置是定义在世界坐标系中的,所以需要把物体的顶点进一步变换到摄像机的视图空间。视图空间虽然和模型空间,世界空间一样只是一个坐标空间,理论上没有边界的概念,但是为了渲染效率以及为了能够实现投影,会用六个面将摄像机能够绘制的物体包围起来,这样只需要渲染这六个面中的物体。这六个面分别是前后,上下,左右视截面,构成的形状叫视景体。 - 裁剪空间和NDC
上面说了视景体的概念,在视景体外面的点是不绘制的,需要裁剪掉。但是视景体有各种不同的参数,虽然都是六个面,但是六个面的位置,大小,甚至视景体本身的形状都不同,这样裁剪起来很麻烦,于是出现了裁剪空间。裁剪空间其实不是3D空间,而是一个齐次坐标系。但是齐次坐标可以转化到一个3d空间,方法是对于齐次坐标(x,y,z,w)的x,y,z分量分别除以w分量,得到的新的x,y,z组成一个3d空间。这个除法在这儿叫做透视除法,而得到的3d空间叫做归一化坐标空间(NDC),为啥叫归一化呢?因为视景体内部的点对应到这个空间中点的x,y,z坐标的范围都是在-1和1之间。这个NDC其实和裁剪空间是一体两面的,他们其实是同一个空间,只是用齐次坐标表示时是裁剪空间。由于NDC的这个性质,对于裁剪空间,所有满足在视景体内部的点的范围就是x,y,z都在-w和w之间。这样裁剪的时候只要用x,y,z去和w比较就可以了,非常容易。 - 屏幕空间
通过裁剪空间的裁剪而过关的点,进行透视除法而得到NDC空间的3D点之后,其实x,y值就已经可以对应到屏幕上了,只是屏幕坐标一般是按像素描述的,所以经过一个线性映射后得到屏幕坐标的x,y。而z值也经过一个转换后存入深度缓冲。
WebGL是什么
现在来看看WebGL做了什么,上面科普了很多关于坐标系的知识,但是却和WebGL没有一毛钱关系。。WebGL实际上只是一个光栅化引擎。那什么是光栅化,先看一下OpenGLES 3.0的流水线(没找到WebGL的图):
WebGL对模型顶点的处理从Vertex shader开始,shader就是运行在显卡上的小程序,同一时间会有很多的这样的小程序在并行运行。vertex shader是处理顶点的,三角形的三个顶点都处理好之后,只是得到了三角形的边界。而绘制三角形的内部,就需要把三角形分解成一个个的像素片段,这个过程就是光栅化Rasterization。光栅化产生的片段Fragment通过Fragment shader的处理,被设置上颜色,再经过逐片段操作最终被写入FrameBuffer中。其实WebGL所做的事情可以总结为把三角形的顶点通过vertex shader计算出坐标,然后分解三角形成片段,对片段执行fragment shader设置颜色,最后显示到屏幕上。所以WebGL做的事情真的就是光栅化而已。而其他的事情都在shader里面,这个需要开发人员自己去实现。而我们上面说了半天坐标系到底和WebGL有什么关系呢?还是有关系的,那就是WebGL要求Vertex shader计算出来的顶点坐标位于裁剪空间中。这样WebGL会进行图元集成(Primitive Assembly):
所谓图元就是三角形,线和点。图元集成首先做裁剪(clip),因此vertex shader的输出需要是一个裁剪空间中的坐标。不管你的shader怎么写的,反正WebGL就当你输出的是一个裁剪空间的(x,y,z,w)齐次坐标,然后用x,y,z和-w,w比较。不满足条件的顶点被抛弃。但是裁剪不仅仅是抛弃顶点,还得满足图元的要求,你一个三角形的一个点被抛弃了,剩下两个点怎么办,就需要WebGL去补充额外的点让剩下的两个点构成两个三角形。之后就是透视触发以及view port变换上面说了。
使用WebGL需要的一些操作
结合流水线总结一下,为了使用WebGL这个强大的光栅化引擎,我们需要哪些操作。
- 首先需要提供给WebGL顶点数据(包括顶点索引:定义了顶点是如何组成三角形的),这个一般放在Vertex Buffer中。WebGL并不关心你提供的顶点在什么坐标空间中定义,但是遵从3D编程的一般规律,一般我们提供的是模型空间的坐标。顶点数据被上传到显存中的vertex buffer中。
- 我们可能还需要提供一些贴图,把他们上传到显存中供shader使用。
- 我们需要设置一些全局的渲染状态,有一种说法是OpenGL是一个状态机。包括当前使用哪个vetex buffer都是一个状态。
- 然后我们需要编写一组Shader,即顶点着色器和片段着色器。把他们编译出来之后link成一个program一起使用。顶点shader必须输出裁减空间中的坐标,并且可以输出一些顶点相关的参数。而片段shader是最终计算出颜色的地方,片段shader被执行很多次所以效率很重要。
- 我们设置了所有的数据,贴图,shader,状态之后,通过调用draw call启动一次绘制。然后就等着WebGL搞定所有的事情。一次draw call一般被称为一个pass,某些渲染效果需要多个pass。而一些高级效果需要把中间结果保存在某个buffer中再进一步处理。虽然实际事情很复杂,但是WebGL本身并不复杂。复杂的是图形学的算法和各种优化。
下一步
写科普太累了,所以后面不会写很多。好在虽然细节还不到位,但基本的概念都有了,足够开始介绍代码了。
来源:CSDN
作者:勤奋happyfire
链接:https://blog.csdn.net/n5/article/details/103903926