Web 技术研究所

我一直坚信着,Web 将会成为未来应用程序的主流

WebGL(拾柒) 基本阴影处理 - 原理篇

  之前的例子虽然很多都使用了光照效果,但是没有做阴影处理,看上去就很不真实。别被标题中的“基本”欺骗了!虽然是最基本的阴影处理,但是实现它需要涉及的面太广,感觉上会很复杂。写个很简单的Demo都两百多行代码,没封装的WebGL果然伤不起啊!
  首先要搞明白阴影的本质是什么?阴影不是刻意在某个地方画一个灰色的东西让它看上去像阴影,那是平面设计时的做法。咱现在做的是3D,应该模拟自然界中阴影的本质来实现。当一束光被物体挡住时,光线原本会照到的地方就照不到了,这时候那个地方就看上特别暗。这才是阴影!所以有光照效果才会有阴影的概念。由于光源的类型有很多种,它们的阴影计算方式也不同。而且,即使是同一种光源,实现方法也不止一种。我的例子使用的是平行光,实现的方法并不是最好的,不过用它来理解实现原理已经足够了。
  既然阴影的本质是光没照到的地方,那么我们的工作就是要想办法计算出到底什么地方光照不到。这个工作看似非常复杂吧?当我第一次想做阴影效果时候,脑子里就蹦出一个想法。应该在CPU中计算对象之间是否有挡住光的地方再传入着色计算。但是上个厕所回来这个想法就抛弃了,因为物体并不是对象级的,即使是同一个物体也会挡住光的。比如一个杯具,它整个是一个对象,杯底的法向量确实适合光线照射,但是光应该被杯壁挡住而产生阴影。也就是说,这个计算应该是片元级的,显然不是CPU可以胜任的工作,所以只能想办法让着色器来处理。
  正常绘制一个物体,着色器程序是逐个顶点计算然后再处理片元的。要计算阴影就必须同时计算所有顶点,要不然怎么判断它们之间是否有存在遮挡关系呢?既然如此,我们只能在正常绘制之前先让着色器处理一遍所有顶点,并做出相应的计算。着色器不会平白无故的工作,这就必须用上帧缓冲了。我们必须先在帧缓冲中计算物体之间的遮挡关系再正常绘制到屏幕上。但是还有问题,着色器程序和JavaScript的通信并不是那么容易的。绘制时可以通过attribute和uniform传入参数,但是着色器本身并没有把数据返回给JavaScript的接口。那我们就来要偷梁换柱的解决吧,把原本在帧缓冲中应该绘制出的图像作为帧缓冲的数据输出。
  以上这么多东西都是完全可以实现的吧?但是最关键的一点是,如何计算物体的遮挡关系呢?假如在一条光线上有多个物体存在,那么只有离光源最近的物体才能被照射到。我们为何不站到光源的位置观察呢?站在光源的位置上,我们能看到的东西就是光能照到的,看不到的东西就是产生阴影的地方了。由于光源的种类很多,这个计算也会因为光源的不同而不同,现在我只说我例子中的平行光。平行光本身没有光源的概念,我们只能先往光线方向看,然后退后一定的距离,让自己能看到整个场景。这些转向和位移就是视图矩阵的工作了。另外,平行光当然是平行的,它不会发散。我们平时看物体都有透视现象,那就是因为我们的视线发散了。在这里,我们的视线应该是模拟平行光的不发散,所以不能使用平时的投射矩阵,应该是正交投影的矩阵。
  做完这些,我们就能够把平行光能照到的东西都绘制到帧缓冲中了。但是就算绘制出了这些东西有什么用的呢?我们未必要把看到的都绘制出来!既然这个帧缓冲绘中制的东西是作为数据输出的接口,我们当然不需要原原本本的把看到的东西输出了。只要把我们需要的数据作为颜色数据输出即可,反正这个帧缓冲的数据不是用来看的,而是用来计算的。由于绘制时候开启了DEPTH_TEST,我们已经不需要自己去计算它们的遮挡关系了,被挡住的自然看不见。我们现在需要的是在正常绘制到屏幕上时用来判断是否需要加上光线效果的数据。也就是说,在我们把东西正式绘制到屏幕上时,需要判断那个绘制的坐标是不是需要光照效果。得有一个可以根据坐标来判断是否启用光照效果的数据。所以现在我们应该把顶点的坐标信息绘制到帧缓冲中。确切的说,应该是把深度信息输出到帧缓冲中。所谓的深度信息就是坐标到光源的距离,由于我的例子使用的是平行光,深度信息就是z坐标了。xy坐标不会用到,可以不需要保存下来,因为帧缓冲的数据是作为贴图使用的。要读取贴图上的数据本身就需要计算xy坐标,它是作为已知条件的。另外,深度信息的储存方式有很多,你可以把帧缓冲的各个颜色通道当作普通的字节来使用,也可以在正交投影矩阵中对z坐标做一些处理后直接当作颜色保存。我的例子使用了后者,因为那比较简单。下面这个图就是我的例子中帧缓冲的图像,我绘制的是两个球体的运动。也许现在看不出啥,在下一篇文章的例子中就可以看出了。

  现在,我们已经有了一个帧缓冲中计算出的贴图数据,只要对这个贴图数据传入xy坐标就可以得到相应位置的深度信息。当我们在屏幕上绘制某个顶点时也计算出这个xy坐标和深度信息,然后通过xy坐标去贴图中取出存储在上面的深度信息,用获取到的深度信息和自己计算出的深度信息做比较,如果相近则说明这个顶点是在光源处可看的,所以需要光照效果。如果不相近就说明是在光源处不可见的,不需要光照效果。这里只能使用相近来判断,因为转换成贴图数据再使用时总会有一些误差。另外,我们对屏幕绘制时当然不会把摄像机设置在光源的位置,也不会使用正交投影矩阵,因为绘制到屏幕上的东西是用来看的。所以在正常绘制到屏幕上时除了计算正常绘制需要的坐标外,还需要做一次和在帧缓冲中完全相同的计算,为了计算出和帧缓冲中等价的xy坐标和深度信息,为了获取贴图上的深度信息,为了这两个深度信息的相近度比较,这个计算是必须的。
  这样,整个阴影实现的原理和工作流程就都清晰了。这篇文章篇幅已经很长了,我在下一篇中再来说实例中的代码。
网名:
34.203.245.*
电子邮箱:
仅用于接收通知
提交 悄悄的告诉你,Ctrl+Enter 可以提交哦
神奇海螺
[查看全部主题]
各类Web技术问题讨论区
发起新主题
本模块采用即时聊天邮件通知的模式
让钛合金F5成为历史吧!
次碳酸钴的技术博客,文章原创,转载请保留原文链接 ^_^