从零开始手撸WebGL3D引擎11: PostProcessing框架(里程碑5了)

128 阅读8分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

项目状态

本系列文章停更了好几个月了,这期间也是有很多事情,不过这个项目(mini3d.js)肯定是不会放弃的,只会不断的进化。最近几个月主要在深度研究Unity,以渲染系统为主,后期可能会出一系列文章,Unity自身一直在进化,但是底层的那些技术并没有发展特别快,结合Unity的实现方式学习底层技术也是一个很好的思路,掌握这些技术的原理和学习了Unity的实现方式之后就可以在自己的引擎中实现这些技术了。当然并非是完全照搬Unity,首先工作量太大搬不起,其次不同的引擎有不同的使用场景和目标,并且站在巨人的肩上谁说我们不能有更好的设计呢?回到mini3d.js这个项目,当然远期目标是做引擎做框架,近期这就是一个学习和试验各种渲染技术的试验田,因为时间有限所以肯定是有所取舍有先有后了。之前已经实现了一个基本的PostProcessing框架了,后来又实现了一个Projector功能,这两个就作为里程碑5吧。本篇讲一下这个PostProcessing后处理系统。 mini3d.js后期系统 在线示例:mini3d.js后期系统示例

PostProcessing基本原理

后期处理顾名思义就是在场景渲染完成之后对Frame buffer中的内容进行处理。基本方法是将camera的渲染目标设置为一张render texture,将场景内容渲染到这个render texture上,然后再使用这个render texture作为输入,使用特定的shader画一个全屏的矩形到屏幕上。这是最基本的原理,各种各样花式繁多的后期效果主要是特定的后期shader实现的。

框架设计目标

上面的原理很简单,但是实际使用时需求会多一些。比如需要叠加多次后期处理的效果,这既可以在画全屏矩形的shader里面叠加多种后期处理操作(但是不能让shader太复杂而超出指令数的限制),也可以进行多次后期处理pass,即将前一步的后期渲染结果保存到一张render texture上作为后一步后期处理操作的输入,只有最后一步操作才将结果直接写到frame buffer中。另外有一些特殊的后期效果本身就需要多次的渲染来实现,比如bloom效果,首先要使用一个pass分离出亮度信息,然后水平方向和竖直方向进行多次的模糊处理得到bloom区域,最后将bloom区域和第一步的输入也就是原始的render texture进行叠加后写入到frame buffer中。mini3d.js后期框架的设计目标就是支持从简单到复杂的各种后期效果,可以自由的实现自定义后期效果,且提供一定的封装和便利性支持。和mini3d.js其他系统一样,开闭原则是首要的设计原则,在一个封闭稳定的框架基础上尽可能提供扩展的灵活性。

框架结构

  • PostEffectLayer:后期效果层的基类,所有的后期效果都从其派生。关联了一个材质,并且具有render方法: render(chain, srcRT, dstRT)
    • PostEffectLayerOnePass: 如果后期效果只需要一个pass渲染,则直接使用这个类,不需要自定义子类了。该类只是在render方法中调用了一次blit方法,使用设置的材质将源绘制到目标。
    • PostEffectBlur:高斯模糊效果,内部执行了多个pass。
    • PostEffectBloom:bloom效果,比较复杂的后期处理。
    • 用户可以扩展自己的子类实现各种后期效果,如果只需要一次Pass则直接使用PostEffectLayerOnePass,否则实现子类并在render中进行后期逻辑处理。同时用户需要自定义后期材质,材质中包含了render中用到的所有pass。
  • PostProcessingChain :后期处理链,整个后期系统的核心类,同时也提供了PostEffectLayer要使用到的便利方法。
  • Camera管理的后期:后期系统通过camera管理,一个camera如果开启了后期效果,则会生成一个PostProcessingChain实例。主要使用enablePostProcessingaddPostProcessing方法。如果开启了后期,camera的render target会被赋予一张新创建的render texture。因此如果之前camera已经设置了一个RT,需要自己管理释放该RT(前提是RT的引用不能丢弃),之所以这么设计是因为camera具有后期系统的所有权,因此会自动管理后期的RT释放,而外部设置给camera的render target, camera无所有权,只是使用,释放是外部的责任。

PostProcessingChain的主要成员

  • _postEffectLayers列表: 包含所有要处理的后期效果PostEffectLayer,后期效果会按照在列表中的顺序逐个处理。
  • add(layer): 将一个PostEffectLayer加入到列表中。
  • blit(srcRT, dstRT, material=null, passId=0):指定材质和pass id,将一张redner texture渲染到dstRT指定的渲染目标中,如果dstRT为null则直接渲染到frame buffer。
  • getTempRT(width, height): PostProcessingChain实例中包含一个Render texture池,使用这个方法获取一个临时的render texture,该RT的宽高由参数指定。
  • releaseTempRT(rt): 回收一张RT到池中。
  • render(camera)方法: 按顺序渲染列表中的所有PostEffectLayer,渲染目标会自动进行交换和设置。

PostProcessing材质

材质是后期处理的核心,特别对于单pass的效果,效果只和材质有关。框架提供了材质基类MatPP_Base,实际上这个类只是约定了必须包含mainTexture的getter/setter。后期系统的输入RT是通过mainTexture传入给shader。目前提供了几个简单的但pass后期效果材质:

  • MatPP_ColorBSC: 调整屏幕的亮度饱和度和对比度。
  • MatPP_EdgeDetection:使用sobel算子的边缘检测。
  • MatPP_Grayscale:简单的灰度化
  • MatPP_Vignette:晕影效果
  • MatPP_Wave:屏幕波动效果 另外还提供了两个多pass后期效果的材质:
  • MatPP_Blur:高斯模糊,包含了两个pass,分别是垂直和水平方向的模糊pass。但是高斯模糊后期时会分别渲染多次这两个pass。
  • MatPP_Bloom:bloom效果。前面说了包含多个pass。

多pass后期效果举例:PostEffectBloom

目前提供的内置后期效果中,bloom是最复杂的一个。上面说了它的材质里面就有多个pass,另外因为多次渲染,需要自定义Layer。核心方法是render方法,其中包含了如何进行多次渲染,每次渲染的源和目标分别是什么,具体请看代码。

class PostEffectBloom extends PostEffectLayer{
    constructor(){
        super(new MatPP_Bloom());

        //亮度阈值
        this._brightThreshold = 0.6;

        //模糊迭代次数(每次迭代分别执行一次竖直和水平方向高斯模糊)
        this._iterations = 3;   //0~4

        //每迭代一次的模糊尺寸扩散速度(值越大越模糊)
        this._blurSpread = 0.6; //0.2~3

        //RT缩小系数,值越大越模糊
        this._downSample = 2; //1~8
    }

    set brightThreshold(v){
        this._brightThreshold = v;
    }

    set iterations(v){
        this._iterations = v;
    }

    set blurSpread(v){
        this._blurSpread = v;
    }

    set downSample(v){
        this._downSample = v;
    }

    render(chain, srcRT, dstRT){
        this._material.brightThreshold = this._brightThreshold;

        let rtW = srcRT.width / this._downSample;
        let rtH = srcRT.height / this._downSample;

        let buffer0 = chain.getTempRT(rtW, rtH);
        chain.blit(srcRT, buffer0, this._material, 0); //Pass0: extract brightness

        for(let i=0; i<this._iterations; ++i){
            this._material.blurSize = 1.0 + i * this._blurSpread;
            let buffer1 = chain.getTempRT(rtW, rtH);

            // render the vertical pass
            chain.blit(buffer0, buffer1, this._material, 1);
            chain.releaseTempRT(buffer0);
            
            buffer0 = buffer1;
            buffer1 = chain.getTempRT(rtW, rtH);

            // render the horizontal pass
            chain.blit(buffer0, buffer1, this._material, 2);
            chain.releaseTempRT(buffer0);
            buffer0 = buffer1;
        }

        this._material.bloomTexture = buffer0.texture2D;
        chain.blit(srcRT, dstRT, this._material, 3); //Pass3: merge bloom

        chain.releaseTempRT(buffer0);
    }
}

export { PostEffectBloom };

需要完善的地方

这个后期框架基本上是可以实现大部分效果了,但是对于一些特殊效果,需要使用depth texture以及normal texture等,目前mini3d.js还不支持获取。虽然这个和后期框架本身没有关系,但是为了能实现效果还是要支持的。另外虽然我们这儿实现了bloom效果,但是效果真的不是很明显,因为我们现在还是LDR渲染,亮度信息只能是0到1,并不能很好的突出场景中较亮的部分。后期我们计划支持一下HDR,这样bloom效果会比较好。说到这儿关系到引擎整体的一个计划就是WebGL2.0的支持,目前虽然可以使用2.0的context,但是并没有使用2.0独有的方法,且glsl shader也是低版本的。我曾经想整体抛弃1.0直接只支持2.0,但是想到兼容性我觉得还是得尽量两个都支持,所以这个事情只能先等等了。

下期预告

  • 会有一篇文章讲下Projector的实现,目前框架中已经实现了Projector,但是我觉得还需要找一些更好的应用来说说。
  • 下面具体要支持的是阴影,这个不能再拖了。计划会同时实现传统的Shadow Map以及基于前向渲染的Screen Space Shadow Map。
  • 如果真的有人在看我写的这些东西。。麻烦给个Star鼓励一下吧。项目地址:mini3d.js