Tech · ArtShader

「Shader」从零开始的VolumeRaymarching PART Ⅱ

by Ayse, 2022-02-27


GIF 2-27-2022 8-13-50 PM.gif

Shader


本系列章节主要的过程

  • (1)Noise Raymarching的相关基础原理
  • (2)在glsl编辑器(shadertoy)中实现体积云的渲染
  • (3)在ue中搭建体积云的渲染管线

体积raymarching需要解决的问题主要分为两部分

1) Opacity (Light Absorption) 2) Color (Illumination, Scattering)

Opacity

如果云层的密度和颜色为一致的情况下,要计算成像情况就相当于计算它在云里面步进的长度。它的透过率就和DirectX中的雾气的计算方法是一致的,Direct3D exponential fog function( D3DFOG_EXP )

F = 1/ e ^(t * d).

t为穿入到云层的长度,d为密度,在计算中近似认为是采样的0-1的透射值。 而针对云其实有更加直接的计算方法,根据Beer-Lambert的公式,透射率如下

Transmittance = e ^ (-t * d). 

简单分析可以得知其实二者计算是几乎一致的。而在单位体素渲染方面Drebin提出了一个公式,有关光是如何经过一个体积单位出来的,它是用来确定具体的出来这个光的颜色,cin(v)是入射光的颜色,在这个公式里面透明度的补值模拟了对光的吸收,然后后半部分去模拟散射。根据之前的公式,如果整个体积是同质的且射入长度为dx,那么这个时候透射率近似为1。而下面公式仅仅用于一个单位体素的,而这个过程其实是一个fetch feedback的过程,然后所以这个公式的前半部分就和上面的公式是同次的(都是指数形下降)

Cout(v) = Cin(v) * (1 - Opacity(x)) + Color(x) * Opacity(x)

这也将会是本次raymarching中使用最多的计算方法。经过了上面的基本知识,我们就知道只需要通过rayMarching知道我们的点在里面走了多远,然后在里面的每个点的距离都乘上当前点的透明度,就可以知道它的线性的density是多少了。然后我们在通过上面提到的公式重新remap就可以知道每一个光线的透射率。 1.gif 了解到这些以后赶紧到glsl里试试drbin的定理,终于出现了一个有点感觉的云了!samplestep是64,固定步长为0.1。先做一个类sdf球体,fbm就是一个简单的fbm 3d perlin noise,可以跳转到本系列的上一篇文章: 2.png

  float sdcloud(vec3 p){
        float sphere_size = 1.;
        float size = 0.7;
        float freq = 0.8;
        float sphere_space_scale = 0.3;
        vec3 cloud_shape = vec3(0.8,1.0,0.8);

        float sph = sphere_size - length(p*cloud_shape) + fbm(p*freq )*size;
        return sph*sphere_space_scale;
    }

 for (int iter = 0; iter < sample_steps; iter++)
    {
        float depth = sdcloud(sample_pos);

        if (depth > 0. )//into cloud box
        {
            float current_density = 0.6 * sampleDensity(sample_pos);
              Transmittance *= 1.0 - current_density;
        if (Transmittance<=0.01) break;

        }
        sample_pos += rd * sample_dist; 
    }

如果用Beer-Lambert公式,Transmittance = exp(-total_density*sample_lenth); 在同样的采样数和乘积下稍微透一点,补一点系数其实和上面的结果是差不多的,而如果降低采样次数,在边缘的结块的情况就会更加明显,同时也会有摩尔纹的问题。 3.png

当然,目前的opacity计算方法在引擎中还需要优化,不过我们先把glsl里的内容先搞明白了再去做进一步的优化,让我们进入到下一步的流程

Color

在光照参数上主要解决的问题是散射和吸收(scattering absorption)首先我们研究Scattering,自然界中的散射分为两部分,Out-scattering 和 In-scattering,但是由于性能的控制,我们在实时渲染中只采用前者。Out-scattering 体积外面的光射到表面被漫反射(各向同性)的部分,In-scattering 体积里面的光在中间反弹的部分,很大程度上可以让结果更加真实 而在absorption的部分,其实就和之前提到的opacity是一致的,只是这次march的是光源。二者是同时计算的。

首先介绍的方法在未优化之前按照作者的话来说就是非常drastic的,就是对里面的每一个marching点进行一次新的lightdir的marching,那么就意味着每一次的采样次数就变成了DensitySteps * ShadowSteps, 如下图, 4.gif

那么对这个进行优化的方法,主要分为几部分,第一部分当然是直接把灰色那部分的loop直接break掉,第二部分就继续研究对于这个light transmittance的计算有什么可以优化的地方,还是基于drebin公式的后半段,但是这个公式是基于一条线路的,这个opacity需要重新进行一次计算。

Cout(v) = Cin(v) * (1 - Opacity(x)) + Color(x) * Opacity(x)

首先线性的密度其实就是一个将opacity针对ray上的x到x的积分 ![5.gif][6] 然后透射率就是这样的一个公式,这和我们之前opacity是一致的, ![6.gif][7] 那么对于云内部的任意一点,就有下面的这个公式,w是光到这个点的向量,l是在volume表面的点, ![7.gif][8] 在这个基础上我们计算处一个ray上所有的scattering,就是对所有ray上的点做outscattering然后乘一个transmittance(光的透射衰减)和opacity(也就是这个地方的线性density)形容光在ray上运行的损失,这一部分统一放到T(x,s)中

8.gif os函数就是上面的outscattering,在自然界中,理论上还要考虑background的自发光经过散射后的curving和blurring,但是因为是实时渲染,我们只需要整个简单的Opacity * Color(AlphaComposite)就可以了

那么根据上面的说法,lightmarching就可以写成

 for (int s = 0; s < ShadowSteps; s++)
        {
            lpos += LightVector; //sample position
            float lsample = PseudoVolumeTexture(Tex, TexSampler, saturate(lpos), XYFrames, numFrames).r; //sampling the density of the position
            shadowdist += lsample; //get the shadow density
        }

        curdensity = saturate(cursample * Density); //get current density,相当于当前点drebin公式中的opacity,Density相当于对sample的系数,下面的Shadowdensity同理,都和采样的stepsize线性关系。
        float shadowterm = exp(-shadowdist * ShadowDensity);
        float3 absorbedlight = shadowterm * curdensity; //光在该点遭遇的损失
        lightenergy += absorbedlight * light_transmittance;  //光在ds的距离上遭遇的损失(相当于drebin的前半段部分)
        light_transmittance *= 1-curdensity;//
    }

在优化方面作者提到了下面几种策略:

  • 对提前结束lightmarching的一些优化,先是把在盒体0-1的pos映射到0.5到1之间(假如box位于1,1,1),假如任意值超过了一个边界就直接排除。(这一步如果是shadertoy是不需要的,用sdf不香么)

    float3 shadowboxtest = floor( 0.5 + ( abs( 0.5 - lpos ) ) ); float exitshadowbox = shadowboxtest .x + shadowboxtest .y + shadowboxtest .z; if(exitshadowbox >= 1) break;

  • 如果transmittance小于一个阈值0.001也直接排除(全部吸没了)

    DistanceThreshold = -log(0.001) / d

  • 可以直接把它算出来当一个距离的阈值,要是marching到这个阈值就退出,当然这个标准的density可以就写成1; 同理在光照计算上也可以用这个,当然ShadowDensity(自己定义的乘数)需要先乘上一个stepsize来规整。然后再进行threshold的计算

    float shadowthresh = -log(ShadowThreshold) / ShadowDensity


然后我兴冲冲使用了上面的方法在shadertoy操作了一番,但是最终的结果不论我怎么调整参数,都没有办法得到很好的结果,一方面是云的透射存在问题,light照亮了整片云,然后我放弃了这种通过Lightenergy统一进行计算的方法,直接使用两个透射率乘以每一个采样点的。Color部分的透射率就是:Transmittance *light_Transmittance * Light。而一般的opacity部分就作为ambient乘以一个skycolor,SkyColor * Transmittance,然后每一次的步进采样得到的sdcloud值(相当于有noise的深度值)就乘到颜色值上,也就是上面提到关于scattering公式上乘的那个density(当然这里是取最近值近似了)。 于是我们就得到了下面这个结果。


本文参考:https://shaderbits.com/blog/creating-volumetric-ray-marcher

作者: Ayse

2024 © typecho & elise