WebGL学习之HDR与Bloom (2)

首先我们要从渲染出来的场景中提取两张图片。可以渲染场景两次,每次使用一个不同的不同的着色器渲染到不同的帧缓冲中,但可以使用一个叫做MRT(Multiple Render Targets多渲染目标)的小技巧,有了它我们能够在一个单独渲染处理中提取两个图片。在片元着色器的输出前,我们指定一个布局location标识符,这样我们便可控制一个片元着色器写入到哪个颜色缓冲:

layout (location = 0) out vec4 FragColor; layout (location = 1) out vec4 BrightColor;

使用多个片元着色器输出的必要条件是,有多个颜色缓冲附加到了当前绑定的帧缓冲对象上。直到现在,我们一直使用着 gl.COLOR_ATTACHMENT0,但通过使用 gl.COLOR_ATTACHMENT1,可以得到一个附加了两个颜色缓冲的帧缓冲对象。

但首先我们还是将创建帧缓冲的功能进行封装:

function createFramebuffer(gl,opt,width,height){ const fb = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, fb); const framebufferInfo = { framebuffer: fb, textures: [] }; const texs = opt.texs || 1;//颜色缓冲数量 const depth = !!opt.depth; // SECTION 创建纹理 for(let i=0;i< texs;i++){ const tex = initTexture(gl,opt, width, height); framebufferInfo.textures.push(tex); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0 + i, gl.TEXTURE_2D, tex, 0); } // SECTION 创建用于保存深度的渲染缓冲区 if(depth) { const depthBuffer = gl.createRenderbuffer(); gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer); gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, width, height); gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthBuffer); } // 检查帧缓冲区对象 const e = gl.checkFramebufferStatus(gl.FRAMEBUFFER); if (gl.FRAMEBUFFER_COMPLETE !== e) { throw new Error('Frame buffer object is incomplete: ' + e.toString()); } // 解绑帧缓冲区对象 gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.bindTexture(gl.TEXTURE_2D, null); if(depth) gl.bindRenderbuffer(gl.RENDERBUFFER, null); return framebufferInfo; }

接着调用上面的函数创建包含两个颜色附件和一个深度附件的帧缓冲区。

//场景帧缓存(2颜色附件 包含正常颜色 和 hdr高光颜色,1深度附件) const fbo = createFramebuffer(gl,{informat:gl.RGBA16F, type:gl.FLOAT, texs:2, depth:true});

在渲染的时候还需要显式告知WebGL我们正在通过gl.drawBuffers渲染到多个颜色缓冲,否则WebGL只会渲染到帧缓冲的第一个颜色附件,而忽略所有其他的。

//采样到2个颜色附件 gl.drawBuffers([gl.COLOR_ATTACHMENT0, gl.COLOR_ATTACHMENT1]);

当渲染到这个帧缓冲的时候,一个着色器使用一个布局location修饰符,然后把不同颜色值渲染到相应的颜色缓冲。这样就省去了为提取高光区域的额外渲染步骤。

#version 300 es precision highp float; layout (location = 0) out vec4 FragColor; layout (location = 1) out vec4 BrightColor; //... void main() { vec3 normal = normalize(vNormal); vec3 viewDirection = normalize(u_viewPosition - vposition); //... vec3 result = ambient + lighting; // 检查结果值是否高于某个门槛,如果高于就渲染到高光颜色缓存中 float brightness = dot(result, vec3(0.2126, 0.7152, 0.0722)); if(brightness > 1.0){ BrightColor = vec4(result, 1.0); } else { BrightColor = vec4(0.0, 0.0, 0.0, 1.0); } FragColor = vec4(result, 1.0); }

这里先正常计算光照,将其传递给第一个片元着色器的输出变量FragColor。然后我们使用当前储存在FragColor的东西来决定它的亮度是否超过了一定阈限。我们通过恰当地将其转为灰度的方式计算一个fragment的亮度,如果它超过了一定阈限,我们就把颜色输出到第二个颜色缓冲,那里保存着所有亮部。

这也说明了为什么泛光在HDR基础上能够运行得很好。因为HDR中,我们可以将颜色值指定超过1.0这个默认的范围,我们能够得到对一个图像中的亮度的更好的控制权。没有HDR我们必须将阈限设置为小于1.0的数,虽然可行,但是亮部很容易变得很多,这就导致光晕效果过重。

有了一个提取出的亮区图像,我们现在就要把这个图像进行模糊处理。

高斯模糊

要实现高斯模糊过滤需要一个二维四方形作为权重,从这个二维高斯曲线方程中去获取它。然而这个过程有个问题,就是很快会消耗极大的性能。以一个32×32的模糊kernel为例,我们必须对每个fragment从一个纹理中采样1024次!

幸运的是,高斯方程有个非常巧妙的特性,它允许我们把二维方程分解为两个更小的方程:一个描述水平权重,另一个描述垂直权重。我们首先用水平权重在整个纹理上进行水平模糊,然后在经改变的纹理上进行垂直模糊。利用这个特性,结果是一样的,但是可以节省难以置信的性能,因为我们现在只需做32+32次采样,不再是1024了!这叫做两步高斯模糊。

高斯模糊


这意味着我们如果对一个图像进行模糊处理,至少需要两步,最好使用帧缓冲对象做这件事。具体来说,我们将实现像乒乓球一样的帧缓冲来实现高斯模糊。意思是使用一对帧缓冲,我们把另一个帧缓冲的颜色缓冲放进当前的帧缓冲的颜色缓冲中,使用不同的着色效果渲染指定的次数。基本上就是不断地切换帧缓冲和纹理去绘制。这样我们先在场景纹理的第一个缓冲中进行模糊,然后在把第一个帧缓冲的颜色缓冲放进第二个帧缓冲进行模糊,接着将第二个帧缓冲的颜色缓冲放进第一个,循环往复。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpdzjz.html