[SD,Raymarching]在2D窗口做渲染[1] —基本原理与场景构建

[SD,Raymarching]在2D窗口做渲染[1] —基本原理与场景构建

​Raymarching 是一种非常有趣的技术,在非常神奇的一个网站shadertoy上,几乎大多数纯用程序算出来的画面,都是用raymarching技术制作的。


shadertoy.com/view/Ms2S




shadertoy.com/view/4dSB




shadertoy.com/view/XslG




shadertoy.com/view/4ttS




shadertoy.com/view/ld3G




用raymarching做的东西,没有模型,所有的计算全是代码和一些基本函数,很多看似非常复杂的效果,也就百来行代码就完成了。


对于第一次接触的人来说,就三个字能形容—— 头皮发麻


而且这些技术,懂了理论以后,完全可以轻松移植到其他任何软件中。不管是UE4,unity ,甚至 maya, nuke,或者我们今天的主角substance designer


现在UE4中比较优秀的云雾效果的实现很多都是raymarching技术实现的[1];以及一些离线渲染器的体积雾算法,很多也是raymarching[2]。这可能是这个技术在实际应用领域最大的作用吧。

其余时候嘛,感觉就是纯炫技以及锻炼自己对于效果实现的思考能力。有点像是什么呢,像是足球和街头足球的关系吧。


小罗除了踢比赛厉害,同时也是非常秀的街球高手。我觉得练习那些在真正赛场上看起来似乎并没有多少卵用的街球技巧,也是潜移默化地让他变得超厉害的训练方式之一吧。





所以这次介绍的这个技术,应用场景总的来说并不广泛,但是,非常有趣,特别秀!还能极大增强内力。

在别的游戏引擎,或者 代码侧 的玩家,已经把这技术基本玩烂了。


在Substance Designer软件中的实现教程,这应该是第一篇。吃我一发安利!


这两期教程最后要完成的效果如下图。在SD的2d窗口中搭建三维场景,并且制作摄像机、灯光、软阴影、材质甚至Tonemapping等等效果。





SD里居然还能做这种东西?不好意思,就是能做,想不到吧。

下面,开始我的街头CG表演。







[1]

[2]







目录:

  • SDF(signed distance field)距离场介绍
  • raymarching原理介绍
  • 视频操作
  • 创建摄像机
  • 定义距离场物体
  • 创建Raymarching循环体
  • 使用循环体做迭代
  • 创建地面






SDF(signed distance field)距离场介绍


首先,raymarching技术和传统做法非常不一样的一点是,我们不使用任何三维模型。所有最终呈现出看起来像模型的东西,都是一种SDF距离场。

SDF的本质是一种数学描述。

比如我们要定义一个球,那么,定义球的中心坐标P,以及球的半径R。那么世界中任意一个点A,如果在这个球的外面,那么P点A点的距离就大于R;如果在球的里面,则小于R;如果正好在球表面上,则等于R。

所有 distance (P,A) = R 的点,共同构成了我们定义的这个球体 (其中distance(P,C)为一个计算欧式距离的函数,返还值为PC两点之间的距离)。

换一种说法, 任意点A 到球表面的最小距离 minD = distance (P,A) - R 。只有当minD 正好等于 0 的时候,才是我们定义的球表面


而A点在世界空间中任意穿梭的过程中,minD的值一直在变化,这个minD的值其实就构成了SDF距离场,这个距离场是标量;距离为正值表示在外,距离为负值表示在内,用这个正负关系表示内外关系的做法就是所谓的signed。



▲一个球的距离场预览图



▲这个定义可以扩展到任何形态上。再做一个相对复杂的距离场

同样的,所有距离场等于0的位置,共同构成了我们定义的这些形体的抽象表面。后续我们就可以利用raymarching算法,在距离场中进行各种计算,把这些定义好的抽象形态转变成可以看得见摸得着的东西。





Raymarching原理介绍:


如果我们想要看见这些距离场定义的物体,首先要有一个摄像机。同时摄像机前面还有一个显示画面的屏幕,摄像机透过屏幕发射射线向场景里观察,如果射线前进的道路上,遇到我们定义的抽象距离场物体,我们就可以把这些物体显示在我们的屏幕上,如果没东西就算了。

那问题是我们怎么知道,摄像机看向场景的射线,是否能看到物体呢?

Raymarching厉害的地方就在这里了,我们摄像机发射射线的时候,是一小截一小截地往前步进的(这就是为什么这个算法叫raymarching了),每走一步都确认一下当前射线走到的位置的距离场数值,如果数值大于0,则还没撞到东西,如果数值小于0,则已经在物体以内了,如果恰好等于0,嗯哼,我们就看到这个物体的表面啦!

上面这个图,我画的是固定步长的算法。但是大家很容易想到,这个做法会有一个很大的弊端,就是步长大了的话,射线很容易穿过物体,找不到物体表面。而如果步长设置很小的话,前面跑的很多很多步都是浪费的,很消耗资源。

这里就涉及到raymarching很聪明的一个优化。当我们摄像机位置确定的时候,我们根据距离场,可以知道相机现在到球表面的最小距离是多少,场景中当前不可能有任何东西离相机的距离比这个最小值还小的。所以我们大胆地步进这个最小值的距离,绝对不会错过任何东西!


注意在这一次步进中,只有一个像素点能恰巧找到球体表面,如图所示,大部分射线离球表面还有一定距离。这个时候我们要再按照之前的思路再次步进。因为有距离场,射线第一次步进之后的位置,依然知道自己这个点离球的最近距离是多少。


这是第二次步进的效果,青色箭头是当前点离球的最近距离红色箭头是摄像机发出的射线按照原方向继续步进的长度


于是会出现两种情况:

  1. 一种是射线经过很多次步进之后,逐渐地靠近球的表面


2.一种是射线经过多次步进之后,逐渐远离球的表面。这根射线他将永远无法和这个球体相交。

总之,经过很多次的步进之后,所有射线最后所处位置的距离场,如果非常非常小的话,比如距离小于0.01,那么我们就断定,这个点基本就是球的表面了!以上就是raymarching的原理,有了距离场以后,真的很简单。

接着,我们开始在SD中实现这一系列操作。





视频操作


视频部分主要是弥补文字表达能力的局限。有很多制作的细节,可能在文章中体现不清楚,视频里就会很明显。但是视频是以讲解操作和演示效果为主的,并不涉及太多原理分析。请务必确保自己读懂了原理以后再跟视频,最好能把后面的文字讲解操作部分都过一遍,心里有了大概的认知,对于还是不确切的地方,再去视频里寻找印证。


b站链接:


notice:- 本篇视频教程中使用了个人开发的第三方插件SDShortcutsEnhance来创建节点,会和你们正常使用的SD感觉有很大的不同。如果你也想使用这个插件,戳这个链接:





创建摄像机


首先这一切都是用pixel processor算的。所有效果都是在SD的 2d view窗口中显示。我们假定摄像机的位置是(0,0,-1)显示屏幕窗口的坐标是(uv.x,uv.y,0)

其中x ,y 坐标是我们能看到的2d窗口的横轴和纵轴,z是我们抽象的三维空间的深度方向。这个方向本身是不存在于SD的2d 世界的,是我们自己幻象与定义的。但经过后续计算,我们可以在2d窗口搭建真实可信的3d效果。


然后屏幕窗口的横纵轴,我们就直接用窗口本身的uv节点就合适。uv节点直接显示的结果如下图。

你可以想象现在看到的效果就是我们想要的显示屏幕,摄像机在这个屏幕正中间往后退一个单位的位置。

但现在的问题是,正中间的uv坐标是(0.5,0.5),而实际上,我们想要把正中间当作世界原点,坐标应该是(0,0)。所以默认的uv节点创建出来以后,要再减掉(0.5,0.5)。


然后从摄像机向我们的显示屏幕发射射线,计算射线的方向,最后要做normalize变成单位向量,方便后续计算:

以上就定义好了我们简陋的摄像机。接着我们开始做场景里的物体,也就是定义距离场。




定义距离场物体

我们先做个球,位置处于(0,0,5),半径为1。距离场的本体本质上是距离,所以我们必须要计算出指定的某个点到这个球表面的最小距离。这个最小距离才是距离场。那么我们先做一个float3,随便给个参数,我这里给(-1,-2,-3),这里就先用来占位,表示是一个未知vector3变量,求这个点到球的最小距离,输出的就是距离场。算法是两点距离减半径。







创建Raymarching循环体

回忆一下raymarching的关键其实是一个开始的步进的坐标,以及步进的方向。最开始的时候,我们raymarching的原始坐标肯定是从摄像机位置开始的,而步进的方向则是摄像机对屏幕发射的每一根射线。我们之前做摄像机的时候,定义好的cameraRay就是这个步进方向。步进一次之后,又会从新的起始坐标再次步进。所以你发现这是一种迭代,上一次计算完的结果,变成下一次迭代的初始参数再次计算。在代码里就是一个for循环的事。

这里我们就要在SD里做一个for循环。for循环的计算体是这样的:已知输入是 步进开始点p0,以及步进方向rd,求这次步进结束以后的终点p1。

p1 = p0 + rd * dLast

我们创建一个新的graph来做这个for循环的循环体,循环体的输入是p0 ,rd,输出是p1。注意,他们不是位置就是向量,都有三个通道,要使用彩色input。


然后我们在这个新创建的pixel processor里开始定义循环体,通过下图的连法,可以拿到输入的信息。但是我们要如何获得距离场信息dLast呢?


记得在定义距离场物体中,我们已经做了一套节点,那套节点就可以对输入的任意位置计算出距离场结果。我们把这套节点先拷贝过来。用p0替换原来的占位变量。再用p1 = p0 + rd * dLast计算出输出位置:

这样,我们的一个循环体就做好了。




使用循环体做迭代


(注意,以下所有pixel processor节点为了计算高动态范围,都设置成了 HDR Low Precision 16F 模式)

现在开始来使用循环体。 先输入最开始的位置和摄像机的发射方向。我们从第一个pixel processor可以输出cameraRay,但是cameraPos就不够输出了。因为一个pixel processor最多只能输出四个通道。两个向量有六个通道。所以我们要重新做一下摄像机的部分。


我们把摄像机的位置拿到最外面来,用uniform color 定义,重新输入给摄像机的pixel processor,让他只输出cameraRay。



然后再复制循环体做循环。每一个节点输出的p1都输入到下一个节点输入的p0



经过这一套计算,我们可以想象,最后一个节点输出的位置信息,就是在球的表面位置信息。为了预览效果,最后我们再使用一个新的pixel processor节点来计算一下最后的位置到摄像机的距离。


但是结果纯白一片,是因为距离都超过1了,我们把结果亮度压下来观察一下。

可以看到画面中显示出了球形。

为了更好地观察场景,我们再创建一个地面。(注意,以上所有pixel processor节点为了计算高动态范围,都设置成了 HDR Low Precision 16F 模式)





创建地面

地面的创建逻辑和球体的创建逻辑一致。用公式计算出符合地面的距离场即可。假设我们要在 y轴为 h(因为SD的uv空间,v方向向下为正值,所以正h表示摄像机下方)的位置创建一个平面,那么任意点A到这个平面的最小距离为 h -A.y

所以我们再加一些节点,用定义的一个高度,比如这里的1,减去p0的y轴,就是高度为1的平面的距离场了。


最后这个平面的距离场要和球的距离场进行一个 取最小值的计算,这样二者才能同时生效:



同理,还可以定义左边墙和右边墙:




最后:

这是系列教程第一篇。主要讲解了算法的基本原理,以及最简单的场景搭建。后续还准备再在这个基础上做光照 阴影和材质。

第二篇可能才涉及到Raymarching算法令人发指的应用思维,绝对会吹掉你的脑子。我这个向来不嘻嘻哈哈的人,都忍不住发个表情包来表达那种无法用语言形容的震撼!



我应该能保证会有第二篇。但是再往后就不好说了。毕竟你看我这一篇下来,把各种问题讲透,尽可能让更多水平相对一般的爱好者也能看懂,你们肯定也感受到了这里面花的时间精力。对比一下网上很多 那种 博客,虽然也有像教程一样把知识都梳理下来的,但大部分都明显不是面向读者的,而更像是他自己的学习笔记,根本没想过要让你们能看懂。对于不小心搜到的人来说,看了几乎等于没看,聊胜于无吧。

我平常也是在这种无用信息的海洋里尽力扒拉点能看得过去的资料学习,很能体会个中滋味。

所以 我发出来的东西都够格算得上详细教程,而不是简简单单的个人笔记。我觉得我尽力了。

如果看的人多,反响好就多出一点。 反响一般的话,就只出两篇。

还有一个课后思考,大家可以好好想一想。前面我说的理论部分,当距离场小于某个很小的值的时候,我们就可以认定这个点就是我们定义的物体上的点。然而我在做的过程中,似乎并没有添加任何相关的判断机制, 这是为什么呢?‍

发布于 2019-08-11 10:57