Chango的数学Shader世界(十六)RayTrace三维分形(一)—— ue4中最简单的RayMarch

目的:

最近接触一些分形Shader。自己实现了一个2D fbm,感觉很好看。然后又看到人家三维的分形,看起来很棒,心里痒痒。

影视大作和网上一堆令人惊叹的分形艺术,我也不再多说了。

本篇旨在对在ue4中实现分形的第一步,RayTrace和RayMarch加以具体解释,然后给出一个最简单的RayMarch材质,便于读者扩展。

(RayMarch一个球形“云”)

参考:

我之前对一篇ShaderToy的解析

观察:

1.分形是什么?它有什么实际意义?

分形指缩放之后还自相似的图形。

分形的特点,在于以极少的初始条件产生极丰富的信息。大自然也处处是分形。

分形的意义在整个科学里比较松散。分形在上世纪最热的时候,人们预言称21世纪是“分形的世纪”。(像不像现在的人工智能?)事实证明人类至今还对其知之甚少,难以研究。现在,凉到了正常水平。

除了帅以外,分形的几个实际应用:

1.海岸线估计:数据证明海岸线具有自相似性,所以只要给出一个粗略的海岸线轮廓,再进行分形后计算长度,就能估算实际的海岸线长度。
2.图片超分辨率,压缩...
3.混沌理论

2.简述RayTrace,RayTrace实际意义,什么时候该用哪个?

RayMarch:沿着视线,每隔一小步长收集此点颜色信息,最终累加。不反射视线。
RayTrace:沿着视线,看是否与场景相交,在交点处计算颜色(光照模型,阴影...),并反/折/衍射

因为RayMarch在交线段上每一个步长都要收集信息,所以肯定处理半透明的,有体积的东西。常见如体积云,体积阴影...

因为它计算量大,效果好,只有在特别追求效果或互动的时候,才用它。

RayTrace不用多说,一般的透明半透明场景。现在显卡也支持硬件光追,十分提升场景的真实感。也许也可以应用更多PBR算法了。

3.为什么想要RayTrace分形,却先RayMarch?

RayMarch:沿着视线收集信息,不反射
RayTrace:沿着视线,碰到立马求颜色,并反/折/衍射

你看,他们的关键都是沿着视线去收集,区别不大。

而且你会发现很多问题是两者的混合。比如我们之后RayTrace Julia Set的时候,由于不能得到Julia Set分形的解析式,只能通过Distance Estimate 慢慢“RayMarch”直到十分接近分形表面,再进行RayTrace。

所以,拿到距离场再进行RayMarch,到达表面再进行RayTrace,是现代渲染的基本操作。

分析:

1.March

现在,我们来RayMarch一个中心在圆点,半径为100的云球。

简单起见,我们不作其他判断,暴力地每一个步长都判断。

伪代码

伪代码
float re=0;
for(int t=0;t<3000;t++)
{
    NowPos = CamPos + viewDir * t * Step;
    if(NowPos在球内)
    {
        //gain也可换成其他值,比如0.0001
        re+=1/3000.0f;
    }
}
return re;

显而易见,当越接近边缘,视线穿过的“云点”越少,就显得越薄。

最重要的问题来了,上面的伪代码中,在ue4中,这个viewDir怎么计算?

2.NDC空间

标准化设备坐标空间(Normalized Device Coordinates,NDC)

坐标系:

特点:

1.所有需要渲染到相机的图元都在这里面(或部分与之相交)
2.x,y,z属于[-1,1]。(如果有说z属于[0,1]的,那是老的或错的)
3.它就是MVP之后的结果。
  M(模型空间到世界空间)
  ->V(世界空间到相机空间)
  ->P(相机到投影空间,xyz已除w)
  ->调整原点位置和坐标系大小(投影空间到NDC)

在NDC中,

我们屏幕上的每一个像素点,都是视线射线端点,射线方向朝正Z。视线端点充满了Z=-1,x,y属于[-1,1]的正方形。

可惜UE4材质函数没有直接提供从NDC到其他空间的转换。我嫌麻烦,那就还是和大多数算法一样,在相机空间中进行RayMarch,再转世界系。

3.相机空间

在相机空间中,

我们的相机位置成为射线端点,射线方向由相机指向nearClipPlane上的每一个像素采样中心

???这啥玩意儿,这我在其他RayTrace教程里咋每看见过?ShaderToy里也没见过?

没错,这个词是我发明的,但是存在的。我发现大部分光追教程和ShaderToy里的视线方向,都没涉及到相机的nearClipPlane和FOV。也就是说,他们大多数都是错的

以文首参考那篇ShaderToy为例,如果你大幅调整窗口长宽比,你会发现结果会变畸形,并偏移中心。ShaderToy窗口一般不会想着动,而且很多固定视角或只移动一点点,畸变不明显,无大碍。

一个典型的错误例子

当相机空间=世界空间,射线方向=normalize((2u-1)*ratio,2v-1,1)
ratio=屏幕分辨率x/屏幕分辨率y

我也不知道它咋推来的,可能和NDC搞混了,或者就是推错了。如果把1换成ratio,那么就正确了。

正确的推导

当水平FOV=90度:相机空间射线方向=normalize(2u-1,(2v-1)/ratio,1)
ratio=屏幕分辨率x/屏幕分辨率y

它是由最初推导简化来的:

当水平FOV=90度:相机空间射线方向=normalize((2u-1)*near,(2v-1)/ratio*near,near)

我们必须考虑屏幕上(u,v)点,对应空间里的屏幕(也就是nearClipPlane)中的哪个点,然后再与相机位置(0,0,0)相减得到视线方向。

当FOV确定为90度(UE4默认),那么我们可得maxX=near。

那么此视线向量就是(dx,dy,near)。

假设

dx=k*(2u-1)

dy=p*(2v-1)

又有当u=1,有dx=(2u-1)*k=maxX

那么k=maxX=near

所以dx=(2u-1)*near

又当v=1,dy=dx/ratio=near/ratio=(2v-1)*p

那么p=near/ratio

所以dy=(2v-1)*near/ratio

所以

当水平FOV=90度:相机空间射线方向=normalize((2u-1)*near,(2v-1)/ratio*near,near)

FOV是个好东西,喜欢摄影一定要了解。

不过有一点,ue4中v坐标朝下,而相机系中y坐标朝上,要反一下,所以最终是

normalize((2u-1),-(2v-1)/ratio,1)

然后将射线方向转到世界空间,再March,就o了。

步骤

1.建立一个后期材质

float2 pos = -1.0 + 2.0 * uv;
float3 rayDir = normalize(float3(pos.x, -pos.y*Size.y/Size.x, 1));
return rayDir;
int i=0;
float3 nowPos;
float re=0;
for(i=0;i<RayDis;i++)
{
  nowPos = camPos+ RayStep*rayDir*i;
  if(length(nowPos)<100.0f){
    re+=GainStep;
  }
}
return re;

2.附加到后期体积的Material中。对于实际项目,需要通过Custom处理渲染顺序。

显然,如果我们将March的条件改成fbm或其他比较复杂的函数,能模拟很多不同的形状的云和雾。

结语

方法的方法还是比方法重要。

下节我们将在此基础上改进,并简单RayMarch出一个JuliaSet的形状。

发布了43 篇原创文章 · 获赞 11 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/qq_41524721/article/details/101113153
今日推荐