这里的一系列文章的开头第4部分:镜像
在这一部分中,我们将专注于镜面反射,由于这些反射,沙丘就像一片沙海。Journey沙画最吸引人的效果之一是沙丘如何在光线中闪烁。这种反射称为镜面反射。这个名字来自拉丁词speculum,意思是“镜子”。镜面反射是一个“伞”概念,将所有类型的相互作用结合在一起,其中光在一个方向上被强烈反射,而不是被散射或吸收。由于镜面反射,一定角度的水和抛光表面看起来都闪闪发光。
在旅途中呈现了三种类型的镜面反射:边缘照明,海洋镜面反射和闪光反射,如下图所示。 在这一部分中,我们将讨论前两种类型。施加镜面反射前后轮辋照明
您可能会注意到,在每个“旅程”级别,都提供了一组有限的颜色。尽管这产生了强烈而干净的美学效果,但这种方法使沙的渲染变得复杂。沙丘仅以有限数量的阴影进行渲染,因此玩家很难理解一个地方长距离结束而另一个开始。为了弥补这一点,每个沙丘的边缘都有轻微的辐射效果,突出了其轮廓。这使沙丘不会躲藏在地平线上,并造成更广阔,更复杂的环境的幻觉。在开始弄清楚如何实现此效果之前,让我们通过向其添加漫反射颜色来扩展照明功能。 (在本文的上半部分由我们考虑)和镜面反射的新的广义组成部分。float4 LightingJourney (SurfaceOutput s, fixed3 viewDir, UnityGI gi)
{
float3 L = gi.light.dir;
float3 N = s.Normal;
float3 diffuseColor = DiffuseColor (N, L);
float3 rimColor = RimLighting (N, V);
float3 color = diffuseColor + rimColor;
return float4(color * s.Albedo, 1);
}
在上面显示的代码段中,我们看到边缘照明的镜像组件(称为rimColor
)被简单地添加到原始漫反射颜色中。高动态范围和光晕效果扩散分量和边缘的辉光均为RGB颜色,范围为 之前 . . ,
.
, ,
. , ,
.
High Dynamic Range,
«» . bloom, .
.
菲涅耳反射
边缘的发光可以通过许多不同的方式实现。最流行的着色器编码使用众所周知的菲涅耳反射模型。要了解菲涅耳反射的基本方程式,可视化其发生位置会很有帮助。下图显示了如何通过摄像机看到沙丘(蓝色)。红色箭头表示沙丘顶部的正常表面,应该是镜像的地方。显而易见,沙丘的所有边缘都有一个共同的属性:它们的法线(,红色)垂直于观察方向(,为蓝色)。与我们在“漫反射色”部分所做的类似,您可以使用标量产品 和 衡量其并行性。在这种情况下 等于 ,因为两个单位向量是垂直的;相反,我们可以使用获得衡量其非并行性的方法。直接使用不会给我们好的结果,因为反射会太强烈。如果我们想使反射更锐利,我们可以采用度数表示。数量级从 之前 仍然限制在一个时间间隔内,但黑暗与光明之间的过渡变得更加清晰。菲涅耳反射模型指出,光的亮度 设置如下:哪里 power和 strength-这两个参数可用于控制效果的对比度和强度。参量power 和 strength有时也被称为镜面和光泽度,但名称可以改变。等式(1)很容易转换为代码:float _TerrainRimPower;
float _TerrainRimStrength;
float3 _TerrainRimColor;
float3 RimLighting(float3 N, float3 V)
{
float rim = 1.0 - saturate(dot(N, V));
rim = saturate(pow(rim, _TerrainRimPower) * _TerrainRimStrength);
rim = max(rim, 0);
return rim * _TerrainRimColor;
}
其结果显示在下面的动画中。海洋镜面
《Journey》游戏玩法最原始的方面之一是,有时玩家可以从字面上“冲浪”沙丘。领先的工程师约翰·爱德华兹解释说,thatgamecompany力求使沙更觉得不牢靠,但液体。这并不是完全错误的,因为可以将沙子视为一种非常近似的液体。在某些情况下,例如在沙漏中,他的举止甚至像液体。为了强化沙子可能具有液态成分的想法,Journey增加了第二种反射效果,这种效果通常在液态物体中发现。约翰·爱德华兹(John Edwards)称其为海洋高光:想法是获得与日落时在海洋或湖泊表面可见的相同类型的反射(请参阅下文)。和以前一样,我们将更改照明功能,LightingJourney
以向其添加新型镜面反射。float4 LightingJourney (SurfaceOutput s, fixed3 viewDir, UnityGI gi)
{
float3 L = gi.light.dir;
float3 N = s.Normal;
float3 V = viewDir;
float3 diffuseColor = DiffuseColor (N, L);
float3 rimColor = RimLighting (N, V);
float3 oceanColor = OceanSpecular (N, L, V);
float3 specularColor = saturate(max(rimColor, oceanColor));
float3 color = diffuseColor + specularColor;
return float4(color * s.Albedo, 1);
}
为什么我们最多采用两个反射分量?, rim lighting ocean specular. , , -. , .
, .
通常使用Blinn-Fong反射实现对水的镜面反射,这是一种用于发亮材料的低成本解决方案。它最初是由James F. Blinn在1977年(文章:“ 计算机合成图片的光反射模型 ”)描述的,是对Bui Tyong Fong于1973年开发的一种较早的着色技术的近似(文章:“ 计算机生成的图片的照明 ”)。 。使用Blinnu-Phong阴影时,亮度I 表面由下式给出:I=(N⋅H)power∗strength(2)
哪里H=V+L‖V+L‖(3)
等式(3)的分母将向量除 V+L在其长度上。这样可以确保H 有长度 1。执行此操作的等效着色器功能是this normalize
。从几何角度来看,H 代表之间的向量 V和 L,因此称为Half vector。为什么H在V和L之间?, ,
H—
VL.
, .
VLLVVL.
, ,
V+LL+V, . , :
, , . , (
V+L) . , ( ).
,
V+L—
VL,
1. ,
1, ( ).
有关Blinn-Fong反射的详细说明,请参见《基于物理的渲染和照明模型》教程。以下是着色器代码中的简单实现。float _OceanSpecularPower;
float _OceanSpecularStrength;
float3 _OceanSpecularColor;
float3 OceanSpecular (float3 N, float3 L, float3 V)
{
float3 H = normalize(V + L);
float NdotH = max(0, dot(N, H));
float specular = pow(NdotH, _OceanSpecularPower) * _OceanSpecularStrength;
return specular * _OceanSpecularColor;
}
该动画比较了根据Lambert的传统漫反射阴影和根据Blinn-Fong的反射镜:第五部分:精彩的反思
在这一部分中,我们将重现通常在沙丘上可见的明亮反射。在发表了我的系列文章后不久,朱利安·奥伯贝克(Julian Oberbek)和保罗·纳德莱克(Paul Nadelek)尝试重新尝试创建一个受Unity旅程游戏启发的场景。下面的推文显示了它们如何完善明亮的反射以提供更好的时间完整性。在IndieBurg Mip Map Folding上的文章中了解有关它们实现的更多信息。在课程的前一部分,我们介绍了Journey沙色渲染中两个镜像功能的实现:边缘照明和海洋镜面反射。在这一部分中,我将解释如何实现镜面反射的最新版本:glowing。如果您去过沙漠,您可能会注意到沙子实际上是如何发光的。正如在沙质法线部分所讨论的,每个沙粒都可能会在随机方向上反射光。由于随机数的性质,部分反射光线将掉入相机中。因此,沙子的随机点将显得非常明亮。这种光泽对运动非常敏感,因为最轻微的偏移会阻止反射的光线进入相机。在其他游戏中,例如《Astroneer》和《Slime Rancher》,沙子和洞穴使用了明亮的反射。光泽度:在应用效果之前和之后。在较大的图像中评估这些光泽度特性更容易:毫无疑问,对真实沙丘的闪光效果完全取决于以下事实:有些沙粒会随机将光反射到我们的眼睛中。严格来说,这就是我们在建模沙子的正态分布的第二部分中对正态分布的随机分布建模时已经建立的模型。那么为什么我们需要另一个效果呢?答案可能不太明显。假设我们仅在法线的帮助下尝试重新创建光泽效果。即使所有法线都对准了相机,沙子仍然不会发光,因为法线只能反射场景中可用的光量。也就是说,充其量,我们只会反射100%的光(如果沙子是完全白色的)。但是我们还需要其他东西。如果我们希望像素显得如此明亮,以至于光会传播到与其相邻的像素上,那么颜色应该更大1。发生这种情况是因为在Unity中,当使用后期处理效果将光晕滤镜应用到相机时,颜色会更亮1散布到相邻像素并产生光晕,从而产生一些像素在发光的感觉。这是HDR渲染的基础。因此,不能以简单的方式使用叠加法线来创建发亮的表面。因此,该效果更易于实现为单独的过程。微面理论
为了更正式地处理这种情况,我们需要感知沙丘由微观镜组成,每个镜都具有随机方向。这种方法称为微面理论,其中每个微小的反射镜称为微面。大多数现代着色模型的数学基础都是基于微面的理论,包括Unity的标准着色器模型。第一步是将沙丘的表面分为多个微面,并确定每个微面的方向。如前所述,我们在教程的部分中对沙的法线做了类似的操作,其中使用沙丘的3D模型的UV位置对随机纹理进行采样。此处可以使用相同的方法将随机方向附加到每个微面。每个微面的大小将取决于纹理的大小及其浸润纹理的水平。我们的任务是重现某种美感,而不是追求写实感。这种方法对我们来说已经足够了。对随机纹理进行采样后,我们可以将随机方向与沙丘的每个沙粒/微面相关联。叫他G。它指示亮度的方向,即我们正在查看的沙粒法线的方向。考虑到微刻面是沿方向定向的理想镜面,会反射落在沙粒上的光线G。产生的反射光线应进入相机。R (见下文)。在这里我们可以再次使用标量积 R和 V获得衡量其并行性的方法。一种方法是求幂R⋅V,如本文的前一部分(第四部分)所述。如果您尝试这样做,我们将发现结果与《旅程》中的结果有很大不同。明亮的反射应该很少且非常明亮。最简单的方法是只考虑那些R⋅V 低于某个阈值。实作
我们可以使用reflect
Cg中的函数轻松实现上述光泽效果,这使得计算非常容易R。sampler2D_float _GlitterTex;
float _GlitterThreshold;
float3 _GlitterColor;
float3 GlitterSpecular (float2 uv, float3 N, float3 L, float3 V)
{
float3 G = normalize(tex2D(_GlitterTex, uv).rgb * 2 - 1);
float3 R = reflect(L, G);
float RdotV = max(0, dot(R, V));
if (RdotV > _GlitterThreshold)
return 0;
return (1 - RdotV) * _GlitterColor;
}
严格来说,如果 G然后完全是偶然 R也将是完全随机的。看来使用是reflect
可选的。尽管对于静态框架来说确实如此,但是如果光源移动,会发生什么呢?这可能是由于太阳本身的移动,也可能是由于点光源束缚在播放器上。在这两种情况下,沙子都会失去当前帧与后续帧之间的时间完整性,因此,闪光效果将出现在随机位置。但是,使用该功能reflect
可以提供更加稳定的呈现。结果如下所示:正如我们在本教程的第一部分所回顾的那样,光泽成分已添加到最终颜色中。#pragma surface surf Journey fullforwardshadows
float4 LightingJourney (SurfaceOutput s, fixed3 viewDir, UnityGI gi)
{
float3 diffuseColor = DiffuseColor ();
float3 rimColor = RimLighting ();
float3 oceanColor = OceanSpecular ();
float3 glitterColor = GlitterSpecular ();
float3 specularColor = saturate(max(rimColor, oceanColor));
float3 color = diffuseColor + specularColor + glitterColor;
return float4(color * s.Albedo, 1);
}
某些像素最终很有可能会获得更多的颜色 1,这将导致绽放效果。那就是我们所需要的。该效果还添加到现有镜面反射的顶部(在本文的上半部分进行了讨论),因此,即使在沙丘光线充足的地方也可以找到闪亮的粗砂。有很多方法可以改进此技术。这完全取决于您要实现的结果。例如,在Astroneer和Slime Rancher中,此效果仅在晚上使用。这可以通过根据阳光的方向降低光泽效果的强度来实现。例如,值max(dot(L, fixed3(0,1,0),0))
是1当太阳从上方落下时,超出地平线时等于零。但是您可以创建自己的系统,其外观取决于您的偏好。为什么在Blinn-Fong反射中不使用反射?ocean specular, , -.
, 3D- , , . , reflect
. - R⋅VN⋅H, .
第6部分:波浪
在本文的最后一部分,我们将重新创建沙丘与风相互作用产生的典型沙波。在沙丘表面上波动:之前和之后从理论上讲,将这部分放在沙质法线之后是合乎逻辑的。我将其保留在最后,因为它是本教程效果中最困难的部分。这种复杂性的部分原因是由于曲面着色器执行许多其他步骤而存储和处理了法线贴图。法线贴图
在上一部分(第五部分)中,我们探索了一种生产非均质砂的方法。在专门用于砂法线的部分中,使用了一种非常流行的法线贴图技术来更改光与几何表面的交互方式。它通常用于3D图形中,以产生物体具有更复杂几何形状的错觉,并且通常用于使曲面更平滑(请参见下文)。为了实现此效果,每个像素都映射到法线的方向,以指示其方向。它用于计算光照而不是网格的真实方向。通过从明显随机的纹理中读取法线的方向,我们能够模拟颗粒感。尽管身体上不准确,她仍然看起来可信。沙浪
但是,沙丘显示出另一个不可忽视的特征:海浪。在每个沙丘上都有较小的沙丘,这是由于风的影响而出现的,并且由于各个沙粒的摩擦而保持在一起。这些波浪非常明显,在大多数沙丘上也可见。在下面显示的在阿曼拍摄的照片中,可以看到沙丘的附近部分具有明显的波浪形图案。这些波浪不仅取决于沙丘的形状,而且还取决于风的成分,方向和速度,因此变化很大。大多数带有尖峰的沙丘仅在一侧有波浪(见下文)。本教程中介绍的效果是为沙丘两边都较平滑的沙丘而设计的。这并不总是物理上准确的,而是足以令人相信的现实,并且是朝着更复杂的实现迈出的良好的第一步。浪潮的实现
有很多方法可以实现wave。最便宜的是仅将它们绘制在纹理上,但是在本教程中,我们要实现其他目标。原因很简单:波浪不是“平坦的”,必须与光线正确相互作用。如果仅绘制它们,则当照相机(或太阳)移动时将不可能获得逼真的效果。添加波浪的另一种方法是更改沙丘模型的几何形状。但是不建议增加模型的复杂性,因为它会极大地影响整体性能。正如我们在有关沙子法线的部分中看到的那样,您可以使用法线贴图解决此问题。实际上,它们像传统纹理一样在表面上绘制,但是用于光照计算以模拟更复杂的几何形状。也就是说,任务变成了另一个:如何创建这些法线贴图。手动渲染将非常耗时。此外,每次更换沙丘时,都需要重新绘制波浪。这将大大减慢创建资源的过程,这是许多技术专家都希望避免的。一个更有效和最佳的解决方案是以过程方式添加wave 。这意味着沙丘的法线方向会根据局部几何形状而变化,不仅要考虑沙粒,还要考虑波浪。由于需要在3D表面上模拟波,因此对每个像素的法线方向进行更改将更加合乎逻辑。使用带有波形的无缝法线贴图会更容易。然后,该地图将与以前用于沙子的现有法线地图合并。法线贴图
到目前为止,我们遇到了三种不同的常态:- 几何法线:3D模型每个面的方向,直接存储在顶点;
- 沙质法线:使用噪声纹理计算;
- Wave Normal:这部分讨论的新效果。
下面的示例摘自Unity Surface Shader示例页面,演示了重写3D模型法线的标准方法。这需要更改该值o.Normal
,通常在对纹理进行采样(通常称为法线贴图)后进行更改。 Shader "Example/Diffuse Bump" {
Properties {
_MainTex ("Texture", 2D) = "white" {}
_BumpMap ("Bumpmap", 2D) = "bump" {}
}
SubShader {
Tags { "RenderType" = "Opaque" }
CGPROGRAM
#pragma surface surf Lambert
struct Input {
float2 uv_MainTex;
float2 uv_BumpMap;
};
sampler2D _MainTex;
sampler2D _BumpMap;
void surf (Input IN, inout SurfaceOutput o) {
o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
o.Normal = UnpackNormal (tex2D (_BumpMap, IN.uv_BumpMap));
}
ENDCG
}
Fallback "Diffuse"
}
我们使用完全相同的技术用沙子的法线代替了几何法线。什么是UnpackNormal?.
( 1), , . X, Y Z R, G B .
−1+1.
01. , «» «» .
(normal packing) (normal unpacking). :
R=X2+12G=Y2+12B=Z2+12(1)
:
X=2R−1Y=2G−1Z=2B−1(2)
Unity (2),
UnpackNormal
. .
Normal Map Technical Details polycount.
沙丘的陡度
但是,第一个困难与以下事实有关:波形根据沙丘的清晰度而变化。低矮平坦的沙丘波浪小。在陡峭的沙丘上,波型更明显。这意味着您需要考虑沙丘的陡度。解决此问题的最简单方法是分别为平坦和陡峭的沙丘创建两个不同的法线贴图。基本思想是根据沙丘的陡度在两个法线贴图之间进行混合。陡峭沙丘的法线贴图平沙丘的法线贴图法线贴图和蓝色通道, .
, . , (X Y) (Z) .
length(N)=1X2+Y2+Z2=1(3)
:
X2+Y2+Z2=1X2+Y2+Z2=12Z2=1−X2−Y2Z=1−X2−Y2(4)
UnpackNormal
Unity. Shader API .
inline fixed3 UnpackNormal(fixed4 packednormal)
{
#if defined(SHADER_API_GLES) defined(SHADER_API_MOBILE)
return packednormal.xyz * 2 - 1;
#else
fixed3 normal;
normal.xy = packednormal.wy * 2 - 1;
normal.z = sqrt(1 - normal.x*normal.x - normal.y * normal.y);
return normal;
#endif
}
Unity
DXT5nm, (
packednormal.xy
) - (
packednormal.wy
).
Normal Map Compression polycount.
为什么法线贴图看起来是淡紫色的?, , .
, . , [0,0,1].
[0,0,1][0.5,0.5,1], RGB.
; , [0,0,1].
, «». , B −1+1, 0+1. .
可以使用标量积来计算陡度,标量积通常在着色器编码中用于计算两个方向的“平行度”。在这种情况下,我们采用几何体法线的方向(以蓝色显示),并将其与指向天空的向量(以黄色显示)进行比较。这两个向量的标量积返回的值接近1当向量几乎平行时(沙丘平坦),并且接近 0当它们之间的角度为90度时(陡峭的沙丘)。但是,我们面临第一个问题-此操作涉及两个向量。从函数surf
使用获得的法线向量o.Normal
在切空间中表示。这意味着用于编码法线方向的坐标系是相对于局部曲面几何形状的(请参见下文)。我们在有关普通沙子的部分中简要谈到了这个话题。在世界空间中表达指向天空的向量。为了获得正确的标量积,两个向量必须在同一坐标系中表示。这意味着我们需要变换它们中的一个,以使它们都在一个空间中表达。幸运的是,Unity提供了一项功能WorldNormalVector
,可以使我们将法线向量从切线空间转换为世界空间。要使用此功能,我们需要改变结构Input
,因此它被列入float3 worldNormal
和INTERNAL_DATA
:struct Input
{
...
float3 worldNormal;
INTERNAL_DATA
};
Unity Writing Surface Shaders文档中的一篇文章对此进行了说明:INTERNAL_DATA
— , o.Normal
.
, WorldNormalVector (IN, o.Normal)
.
在编写表面着色器时,这通常成为问题的主要根源。事实上,该值o.Normal
可用的功能surf
,而不同取决于你如何使用。如果仅阅读它,则o.Normal
包含世界空间中当前像素的法线向量。如果更改其值,则它在切线空间中。
如果您在中录制,但仍需要访问世界空间中的法线(如我们的例子),则可以使用它。但是,为此,您需要对上面显示的结构进行一些小的更改。o.Normal
o.Normal
WorldNormalVector (IN, o.Normal)
Input
什么是INTERNAL_DATA?INTERNAL_DATA
Unity.
, ,
WorldNormalVector
. ,
,
.
(
).
, 3D- 3×3. , ,
(tangent to world matrix), Unity
TtoW.
INTERNAL_DATA
TtoW
Input
. , «Show generated code» :
,
INTERNAL_DATA
— , TtoW:
#define INTERNAL_DATA
half3 internalSurfaceTtoW0;
half3 internalSurfaceTtoW1;
half3 internalSurfaceTtoW2;
half3x3
,
half3
.
WorldNormalVector
, , (
) TtoW:
#define WorldNormalVector(data,normal)
fixed3
(
dot(data.internalSurfaceTtoW0, normal),
dot(data.internalSurfaceTtoW1, normal),
dot(data.internalSurfaceTtoW2, normal)
)
mul
, TtoW ,
.
, :
[ToW1,1ToW1,2ToW1,3ToW2,1ToW2,2ToW2,3ToW3,1ToW3,2ToW3,3]⋅[N1N2N3]=[[ToW1,1ToW1,2ToW1,3]⋅[N1N2N3][ToW2,1ToW2,2ToW2,3]⋅[N1N2N3][ToW3,1ToW3,2ToW3,3]⋅[N1N2N3]]
LearnOpenGL.
实作
下面的代码片段将法线从切线转换为世界空间,并计算相对于向上方向的斜率。
float3 N_WORLD = WorldNormalVector(IN, o.Normal);
float3 UP_WORLD = float3(0, 1, 0);
float steepness = saturate(dot(N_WORLD, UP_WORLD));
现在我们已经计算了沙丘的陡度,我们可以使用它来混合两个法线贴图。这两个法线贴图均被采样,既平坦又凉爽(在下面的代码中称为_ShallowTex
和_SteepTex
)。然后根据值将它们混合steepness
:float2 uv = W.xz;
float3 shallow = UnpackNormal(tex2D(_ShallowTex, TRANSFORM_TEX(uv, _ShallowTex)));
float3 steep = UnpackNormal(tex2D(_SteepTex, TRANSFORM_TEX(uv, _SteepTex )));
float3 S = normalerp(steep, shallow, steepness);
如有关沙子法线的部分所述,很难正确地组合法线贴图,而这不能用完成lerp
。在这种情况下slerp
,使用会更正确,而使用更便宜的函数normalerp
。波混
如果我们使用上面显示的代码,结果可能会让我们失望。这是因为沙丘的陡度很小,导致两种正常纹理的混合过多。为了解决这个问题,我们可以对陡度应用非线性变换,这将增加混合的清晰度:
steepness = pow(steepness, _SteepnessSharpnessPower);
混合两种纹理时,通常用于控制它们的清晰度和对比度pow
。在我的“ 基于物理的渲染”教程中,我们了解了它的工作方式和原因。下面我们看到两个渐变。顶部显示从黑色到白色的颜色,并沿X轴线性插值c = uv.x
。在底部,相同的渐变用表示c = pow(uv.x*1.5)*3.0
:很容易注意到,这pow
使您可以在黑白之间创建更清晰的过渡。当我们使用纹理时,这会减少它们的重叠,从而创建更锐利的边缘。沙丘方向
我们之前所做的一切都可以完美运行。但是我们需要解决另一个最后的问题。波浪随陡度而变化,但随方向而变化。如上所述,由于风主要在一个方向上吹的事实,波浪通常是不对称的。为了使波浪更加逼真,我们需要添加两个以上的法线贴图(请参见下表)。可以根据X轴或Z轴的沙丘的平行度来混合它们。在这里,我们需要实现沙丘相对于Z轴的平行度的计算,这可以与陡度的计算类似地完成,但是float3 UP_WORLD = float3(0, 1, 0);
可以代替使用float3 Z_WORLD = float3(0, 0, 1);
。最后一步,我将留给您。如果您有任何问题,那么在本教程的最后,将提供一个下载完整Unity软件包的链接。结论
这是《旅程》中有关渲染沙子的一系列教程的最后一部分。下面显示了我们在本系列文章中所取得的进步:在此之前和之后,我要感谢您阅读本篇很长的教程。我希望您喜欢探索并重新创建此着色器。致谢
Journey
视频游戏由Thatgamecompany开发,并由Sony Computer Entertainment发行。它可用于PC(Epic Store)和PS4(PS Store)。3D沙丘模型,背景和照明选项是由Jiadi Deng创建的。在FacePunch论坛(现已关闭)中找到了旅途角色的3D模型。Unity包
如果要重新创建此效果,则可以在Patreon上下载完整的Unity包。它具有您所需的一切,从着色器到3D模型。