ShaderToy_200+字符云Unity版
stubbe(推特:@Stubbesaurus)有一个名为“Tiny Clouds”的令人惊叹的ShaderToy作品,它只需 10 行代码 / 280 个字符就可以让你飞跃非常逼真的云层。demofox已经梳理了逻辑。Unity的视频:
https://blog.demofox.org/2017/11/26/dissecting-tiny-clouds/
这是完整的代码。iChannel0 中的纹理只是一个双线性采样的白噪声纹理。
#define T texture(iChannel0,(s*p.zw+ceil(s*p.x))/2e2).y/(s+=s)*4.
void mainImage(out vec4 O,vec2 x)
{
vec4 p,d=vec4(.8,0,x/iResolution.y-.8),c=vec4(.6,.7,d);
O=c-d.w;
for(float f,s,t=2e2+sin(dot(x,x));--t>0.;p=.05*t*d)
p.xz+=iTime,
s=2.,
f=p.w+1.-T-T-T-T,
f<0.?O+=(O-1.-f*c.zyxw)*f*.4:O;
} 顺便说一句,这个shadertoy是 iq大佬的一个更大、功能更丰富的shadertoy的微缩和重新解释的版本:https://www.shadertoy.com/view/XslGRr
在深入研究代码的细节之前,先简单说下,它是如何工作的:
[*]每个像素都会从远到近进行一次raymarching。反向步进是为了更好的进行颜色混合。
[*]每一步step,都会对 FBM 数据(分形布朗运动)进行采样,以确定当前位置是在云表面之下还是在其之上。
[*]如果低于表面,它将像素颜色与该点的云颜色混合,使用进入云的垂直距离作为云密度。
相当合理和简单,当然啦!,毕竟这么少的字符还看起来这么好!让我们继续深入研究代码。
第 1 行就是个define,第 2 行只是 mainImage 函数字符最少的定义。
第 3 行声明了几个变量:
[*]p - 这是在光线行进期间保持光线步进位置的变量。它并没有在这里初始化,但这没关系,因为位置是在循环中的每一步进行计算的,后面赋值就好了。有趣的是,作者从未使用过 p 的 y 分量。p.x 实际上是屏幕的深度,p.z 是屏幕的 x 轴,p.w 是屏幕的 y 轴(也就是向上的轴)。我相信轴的选择和从未使用过 y 分量实际上纯粹是为了使代码量更少。
[*]d – 这是该像素光线行进的方向。它使用与 p有着相同的轴约定,并且从不使用 y 分量(除了跟p.y进行隐式的计算 ,从来没被使用过)。d.z 和 d.w(屏幕 x 和屏幕 y 轴)都被减去 0.8。有趣的是,这会使得屏幕 x 轴的 0点几乎在屏幕上居中。它还将屏幕 y 轴稍微向移动了一点,将 0点放在屏幕顶部附近,以使相机更朝下看到更多的云看。
[*]c – 这是天空的颜色,很漂亮的天蓝色。x,y分量用常量进行初始化,然后 d 用于 z 和 w的初始化.。这将赋予c .z 分量0.8的值。因为与“.8,0”相比,使用“d”初始化的字符会更少。请注意,c.w 用于计算 O.w (Oa) 但输出像素值的 alpha 通道会被 shadertoy忽略,因此这个分量是无意义的,并非需要的功能。
第 4 行将输出像素颜色初始化为天空颜色 (c),然后减去 dw,即屏幕 y 轴上像素的光线行进方向。这对制作漂亮的天蓝色渐变有很好的效果。
为了看效果,我们可以在这里将 O 设置为 c:
将 O 设置为 cd.w则表现为:
它在顶部变得更深的蓝色——其中 dw 是正数——因为从天空颜色中减去了一个正数。颜色值变小。
它向底部变浅——其中 dw 为负数——因为从天空颜色中减去了一个负数。颜色值变大发白。
在第 5 行,开始进行光线行进的 for 循环。这里发生了几件事:
[*]声明f ——f 是空间中当前点到云的有符号垂直距离。如果为负,则表示该点在云内部。如果为正,则表示该点位于云之外(上方)。它没有在这里初始化,没事,因为它会每次循环中迭代计算。
[*]声明s ——s 是用于 FBM 数据的比例值。FBM 通过对多个倍频进行采样来工作。您放大位置并缩小每个倍频的值。s 就是比例值。并没有初始化而是每帧会计算一次。
[*]声明t 并初始化——t(rayMarching步长的index)被初始化为2e2即200。这样做是因为“2e2”比“200”少一个字符。请注意,for 循环将 t 从 200 变为 0。光线行进是从后往前以简化 alpha 混合。sin(dot(x,x))的 部分将在下面简要讨论。
[*]计算p ——p(也就是射线行进的当前step的位置)计算,这发生在循环的每一步。p 是 t(时间)乘以该像素的光线方向,再乘以 0.05 (缩小比例)。
将 sin(dot(x,x)) 添加到“光线步进时间”的原因是因为光线正在穿过体素数据(box状的)。而云不该是box状的,应该看起来是有机的。解决数据其看起来四四方方的问题的一种方法是向每条射线添加一点噪声以打乱其box样子的状态。您可以在结果中添加一些噪声,或者跟这个shader一样,即在光线的起始位置添加一些噪波,以便相邻光线在不同时间穿过框(体素)边界并看起来是很嘈杂的。
但是从shader中删除它,我也看不出有什么不一样,其他人也这么说。Rune 在评论中说,如果他需要剔除更多的字符,这部分肯定会被开刀。但是他已经达到了他的 280 个字符的目标,所以不需要删除它。
以下是表达式的可视化。-1 到 +1 通过将其乘以0.5并加上0.5映射到 0 到 1:
第 6 行将当前时间添加到 p.x 和 p.z分量, 请记住,x 分量是指向屏幕的轴,z 分量是屏幕空间的 x 轴,因此这行代码会随着时间的推移将相机向前和向右移动。
如果您想知道为什么 for 循环中的行以逗号而不是分号结尾,原因是如果使用分号代替,for 循环将需要两个字符:“{”和“}”来显示位置循环的范围开始和结束。用逗号结束行意味着它是一个很长的语句,因此可以使用 for 循环的单行版本。一个很有趣的技巧。
第 7 行将 s 初始化为 2。请记住,s 用作样本位置和结果值的倍频缩放值。这将在下一行中发挥作用。
首先让我们看一下第 1 行,它是“T”的宏。
该宏在光线步进中使用当前光线所在位置的坐标对纹理(只是白噪声)进行采样。s 变量用于放大位置,也用于缩小该位置的噪声值. 其中p.zw 分别是屏幕空间的 x 和 y 轴,但还包括 p.x 是指向屏幕的轴。这会将 3d 坐标映射到 2d 纹理位置(因为输出是float2)。我尝试了让shader采样的是3d 白噪声纹理而不是做上面的设置,也获得了类似的结果。
该宏还将每个采样的 s 乘以 2,以便下一个采样使用下一个倍频来计算。
有趣的是这种从 3d 到 2d 的纹理坐标转换中的p.x(进入屏幕的轴)分量是用ceil来处理过的。我不确定这除了这是将 3d 坐标转换为 2d 用于纹理查找的方法,还有没有其他逻辑在里头。
宏中没有 ceil 的样子(s*p.x), 它会以一种奇怪的方式拉伸噪声。p.x越大也就是距离屏幕越远的点对噪声采样的uv偏移的距离就越远。
我说怎么感觉那么熟悉,简直和拳皇全明星里的无界大招一模一样:
采样的 uv 坐标除以 2e2(即 200,但同样比200字符数更少)。我相信这个 200 的值是有意去匹配光线步进step的计数,以便光线每次步进都穿过整个纹理。
第 8 行使用了这个宏。我们将 f 设置为 p.w,即射线的高度。加1会将相机向上移动一个单位的高度(1个单位会比较好看,加多了相机很高,出现来效果就不那么好了)。最后,T 宏用于从 f 中减去 4 个不同倍频的噪声。
其结果是 f 在垂直轴上为我们提供了到云的有符号距离。换句话说,f 告诉我们我们在云层之上或之下的距离。正值表示位置在云层之上,负值表示位置在云层之下。
第 10 行,函数结束了,所以第 9 行是最后一行有意义的代码。
这行代码描述的是:
[*]如果 f 小于零(“如果点在云内”)
[*]就将一些颜色添加到像素颜色(稍后会详细介绍)
[*]否则,O。这是一个没有副作用的虚拟语句,可以用最少的字符满足三元运算符语法。
开始我认为这可能是一些更复杂的光散射/吸收函数的更便宜的函数拟合。
我问了Rune,它所做的只是从当前像素颜色到此位置的云颜色进行 alpha 混合(一个 lerp)。如果你在数学上做 lerp,稍微推演一下,你就会得到上述结果。这是他在推特上的解释(链接到推特线程):
累积颜色 (O) 和传入云颜色 (1+f*c.zyxw) 之间的 Alpha 混合。注意密度 (f) 为负:
O = lerp(O, 1+f*c.zyxw, -f*.4)
O = O * (1+f*.4) + (1+f*c.zyxw) *-f*.4
O = O + O*f*.4 + (1+f*c.zyxw)*-f*.4
O = O + (O-1-f*c.zyxw)*f* .4
O += (O-1-f*c.zyxw)*f*.4
请记住,行进是从远到近的,这大大简化了计算。如果行进方向相反,那么您还需要跟踪累积的密度。
那么一个明显的问题是:为什么“1+f*c.zyxw”是当前采样云的颜色?
有助于理清这一点的是 f 为负数。如果你让“f”表示“密度”并翻转它的符号,方程变为:“1-density*c.zyxw”
然后我们可以意识到,对于vec4“1”就是白色,而 c 是天空颜色。我们也可以扔掉 w,因为我们(shadertoy)不用关心alpha 通道。我们也可以用 r,g,b 替换 x,y,z。这使得等式变为:“white-density*skycolor.bgr”
在那个等式中,当density为 0 时,剩下的就是白色。随着density值的增加,颜色会越来越深深。
颜色是反转的天空颜色,因为天空颜色是 (0.6, 0.7, 0.8)。如果我们使用天空颜色而不是反转的天空颜色(xyz变成zyx),可以看到蓝色比绿色下降得更快,绿色比红色下降得更快。如果你这样做,云就会变成有点红的颜色,就如同你在下面看到的那样:
虽然我不是大气渲染方面的专家(可以查看底部的链接以获取更多相关信息!),但反转的结果看起来更自然,更正确。我们真正想要的是红,绿,蓝下降的速度递减。我相信更正确的做法是从 1.0 中减去天空颜色并使用该颜色来乘以密度。但是,在这种情况下,反转颜色通道是可以满足要求的,因此没必要花费额外的字符!
另一个明显的问题是:为什么 lerp的alpha是“-f*.4”?
看到 lerp的alpha为负值可能看起来很奇怪,但记住 f 在云内时为负意味着它是一个正值,乘以 0.4 使其更小。这步只是稍微缩放下密度。
其他注意事项
使用纹理的双线性插值会让结果大不一样。如果您将纹理切换为使用最近邻点采样,您会得到类似这样的东西,看起来非常四四方方。
在理解这个着shader时,想尝试的一件事是尝试使用用白噪声函数来替换白噪声纹理的查找。正如您在下面看到的那样,它确实有效,但是在我的机器上它明显变慢了。我已经习惯了把摆脱纹理绑定的操作当做一个胜利。但是我并没有停下来思考怎么样在这种情况下让发生的所有事情都基于计算并且没有一丁点儿纹理读取。在功能更全面的渲染器中,您可能会发现自己的逻辑里如果有纹理读取,将其移出可以帮助加快程序运行的速度!值得注意的是,要获得正确的结果,您需要将噪声函数离散化为网格并在值之间使用双线性插值 (模仿纹理读取的作用)。
有趣的是,您可以用其他纹理替换白噪声纹理。结果通常看起来还不错!下面是我让shadertoy使用“Abstract1”图像作为采样源后。云变得柔和了许多。
当然如果你完全理解了上面的逻辑,可以往其中添加各种自己的想法,比如做个云浪:
如果把它迁移到Unity中效果如何呢?可以在Fragment中利用屏幕UV来复现它,但是有些地方还是要改一下,比如:
1,shadertoy直接定义了O为输出,Unity需要自己定义。
Unity中需要改为:
2,如果我们想自定义step数来控制Raymarching进步的次数,不能把_step放进for循环的条件里
这将导致DirectX Shader编译报错 :unable to unroll loop, loop does not appear to terminate in a timely manner (1024 iterations)。
解决方法是:
当然之前定义T里的2e2要换成_step.
以下是Unity版本完整shader代码:
Shader &#34;URP/URPUnlitShader&#34;
{
Properties
{
_MainTex(&#34;MainTex&#34;,2D)=&#34;White&#34;{}
_BaseColor(&#34;BaseColor&#34;,Color)=(1,1,1,1)
//_s(&#34;s&#34;,Float) = 2
_step(&#34;step&#34;,Range(0,202)) = 202.
_speed(&#34;speed&#34;,Float) = 1.
_h(&#34;h&#34;,Float) = 1.
_rate(&#34;rate&#34;,Float) = 1.3
}
SubShader
{
Tags
{
&#34;RenderPipeline&#34;=&#34;UniversalRenderPipeline&#34;
&#34;RenderType&#34;=&#34;Opaque&#34;
}
HLSLINCLUDE
#include &#34;Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl&#34;
#define T SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex,(s*p.zw+ceil(s*p.x))/_step).x/(s+=s)*4.
CBUFFER_START(UnityPerMaterial)
float4 _MainTex_ST;
half4 _BaseColor;
CBUFFER_END
TEXTURE2D(_MainTex);
float _s;
float _step;
float _speed;
float _h;
float _rate;
SAMPLER(sampler_MainTex);
struct vertexInput{
float4 positionOS:POSITION;
float3 normalOS:NORMAL;
float4 tangentOS:TANGENT;
float2 uv:TEXCOORD;
};
struct vertexOutput{
float4 positionHCS:SV_POSITION;
float3 worldPos :TRXCOORD1;
float3 normalWS :TRXCOORD2;
float3 tangentWS :TRXCOORD3;
float4 scrPos :TRXCOORD4;
float2 uv:TEXCOORD0;
};
ENDHLSL
pass
{
HLSLPROGRAM
#pragma vertex Vertex
#pragma fragment Pixel
vertexOutput Vertex(vertexInput v)
{
vertexOutput o;
VertexPositionInputs positionInputs = GetVertexPositionInputs(v.positionOS.xyz); //计算不同空间(视图空间、世界空间、齐次裁剪空间)中的位置
VertexNormalInputs normalInputs = GetVertexNormalInputs(v.normalOS, v.tangentOS); //计算世界空间中的法线和切线
o.positionHCS = positionInputs.positionCS; //裁剪空间顶点位置
o.worldPos = positionInputs.positionWS; //世界空间下顶点位置
o.normalWS = normalInputs.normalWS;
o.tangentWS = normalInputs.tangentWS;
o.scrPos= ComputeScreenPos(o.positionHCS);//[-w,w]-> 把裁剪空间齐次坐标转换到屏幕空间的齐次坐标
return o;
}
half4 Pixel(vertexOutput i):SV_TARGET{
real time = _Time.x*_speed;
real3 ndcPos = i.scrPos.xyz / i.scrPos.w;// D3D
real2 uv = ndcPos.xy;
uv.x *= 1.3;
uv.y *= _rate;
real4 p = real4(0,0,0,0), d = real4(0.8,0,uv-0.8), c = real4(0.6,0.7,d.xy);
real4 O = c - d.w;
for(real f,s,t = 2e2+ sin(dot(uv,uv)); --t > 0; p = 0.05*t*d)
{
if(t<2e2-_step)
break;
p.xz += time;
d.xw += real2(sin(_Time.x),cos(_Time.x))*0.0035;
s= 2.;
f= p.w + _h-T-T-T-T;
O += f<0?(O-1.-f*c.zyxw)*f*0.4:0;
}
return O*_BaseColor;
}
ENDHLSL
}
}
}
https://www.alanzucconi.com/2017/10/10/atmospheric-scattering-1/
https://shaderbits.com/blog/creating-volumetric-ray-marcher 建议浓缩在一行,然后参加混乱代码大赛[捂脸] [飙泪笑] 好!
页:
[1]