|
Directional Lights(平行光)
——直接照明
本节内容
使用法向量来计算光照 支持多达四个平行光 应用双向反射比分布函数(BRDF) 制作有光照的透明材质 创建一个自定义的着色器图形用户界面(Shader GUI)。
这是一个关于如何创建一个Custom SRP的系列教程的第三个部分,它添加了对多个平行光的支持。
这个教程使用的是Unity版本是2019.2.6f1.
由四个光照亮的各种球体
1. 照明(Lighting)
如果我们想要创建一个更真实的场景,那么我们必须模拟光如何与物体的表面相互作用。这需要一个比我们目前的无光照的shader更复杂的shader。
1.1 照明着色器(Lit Shader)
复制UnlitPass.hlsl文件并将其重命名为LitPass.hlsl。调整引用保护定义以及顶点和片元函数名。稍后我们将添加光照计算。
#ifndef CUSTOM_LIT_PASS_INCLUDED #define CUSTOM_LIT_PASS_INCLUDED … Varyings LitPassVertex (Attributes input) { … } float4 LitPassFragment (Varyings input) : SV_TARGET { … } #endif
也复制Unlit着色器,并将其重命名为Lit。更改其菜单名称、引用的文件、以及使用的函数。让我们同样也改变默认颜色为灰色,因为一个完全白色的表面在一个明亮的场景中会显得非常明亮。URP默认也使用灰色。
Shader "Custom RP/Lit" { Properties { _BaseMap("Texture", 2D) = "white" {} _BaseColor("Color", Color) = (0.5, 0.5, 0.5, 1.0) … } SubShader { Pass { … #pragma vertex LitPassVertex #pragma fragment LitPassFragment #include "LitPass.hlsl" ENDHLSL } } }
我们将使用一个自定义照明方法,通过设置shader的照明模式为CustomLit。在Pass中添加一个Tags块,包含"LightMode" = "CustomLit"。
Pass { Tags { "LightMode" = "CustomLit" } … }
要渲染使用这个pass的对象,我们必须在CameraRenderer中包含它。首先为它添加一个shader标签标识符。
static ShaderTagId unlitShaderTagId = new ShaderTagId("SRPDefaultUnlit"), litShaderTagId = new ShaderTagId("CustomLit");
然后将它添加到要在DrawVisibleGeometry中渲染的pass中,就像我们在DrawUnsupportedShaders中做的那样。
var drawingSettings = new DrawingSettings( unlitShaderTagId, sortingSettings ) { enableDynamicBatching = useDynamicBatching, enableInstancing = useGPUInstancing }; drawingSettings.SetShaderPassName(1, litShaderTagId);
现在我们可以创建一个新的非透明的材质,尽管目前它产生的结果与无光照的材质相同。
默认的非透明材质
1.2 法向量(Normal Vectors)
一个物体被照亮的程度取决于多种因素,包括光与物体表面之间的相对角度。为了知道表面的方向,我们需要访问表面的法线,它是一个垂直于表面的单位长度的向量。这个向量是顶点数据的一部分,在对象空间中定义,就像位置一样。 所以把它添加到LitPass的Attributes中。
struct Attributes { float3 positionOS : POSITION; float3 normalOS : NORMAL; float2 baseUV : TEXCOORD0; UNITY_VERTEX_INPUT_INSTANCE_ID };
光照是根据每个片元计算的,所以我们必须将法向量添加到Varyings中。我们将在世界空间中执行计算,因此将其命名为normalWS。
struct Varyings { float4 positionCS : SV_POSITION; float3 normalWS : VAR_NORMAL; float2 baseUV : VAR_BASE_UV; UNITY_VERTEX_INPUT_INSTANCE_ID };
我们可以使用来自SpaceTransforms.hlsl的TransformObjectToWorldNormal方法在LitPassVertex中将法线变换到世界空间中。
output.positionWS = TransformObjectToWorld(input.positionOS); output.positionCS = TransformWorldToHClip(positionWS); output.normalWS = TransformObjectToWorldNormal(input.normalOS);
TransformObjectToWorldNormal 是如何工作的?
.
检查代码时,您将看到它使用了两种方法之一,这取决于是否定义了 UNITY_ASSUME_UNIFORM_SCALING。
.
当 UNITY_ASSUME_UNIFORM_SCALING被定义时,它调用 TransformObjectToWorldDir,这和 TransformObjectToWorld做的是一样的,除了它忽略了平移部分,因为我们处理的是方向向量而不是位置。但是这个向量也会被均匀缩放,所以之后应该被归一化。
·
在另一种情况下,不假设是均匀缩放。这是更复杂的,因为当一个物体因非均匀缩放而变形时,法向量必须反向缩放以匹配新的表面方向。这需要与转置的 UNITY_MATRIX_I_M矩阵相乘,并进行归一化。
·
不正确和正确的法线变换
使用 UNITY_ASSUME_UNIFORM_SCALING是一个轻微的优化,你可以通过自己定义它来启用。然而,当使用 GPU-Instancing时,这将更有意义。因为 UNITY_MATRIX_I_M矩阵数组必须发送给GPU,在不需要的时候避免这样做是值得的。你可以通过在着色器中添加 #pragma instancing_options assumeuniformscaling指令来启用它,但只有在你用统一缩放渲染对象时才这么做。
为了验证我们是否在LitPassFragment中得到了正确的法向量,我们可以使用它作为颜色输出。
base.rgb = input.normalWS; return base;
世界空间的法向量
负值无法显示,所以它们被固定为零。
1.3 差值法线(Interpolated Normals)
虽然在顶点程序中,法向量是单位长度的,但三角形之间的线性插值会影响它们的长度。我们可以通过渲染1和向量的长度之间的差值来可视化误差,并将结果放大10倍,使其更明显。
base.rgb = abs(length(input.normalWS) - 1.0) * 10.0;
放大的法线差值的误差
我们可以通过对LitPassFragment中的法向量进行归一化来平滑插值失真。当只看法向量时,这种差异并不明显,但当用于照明时,这种差异就更明显了。
base.rgb = normalize(input.normalWS);
差值后的归一化
1.4 表面属性(Surface Properties)
在一个shader中产生照明需要模拟光照之间的交互作用,这意味着我们必须跟踪表面的属性。现在我们有一个法向量和一个基色。我们可以将后者分成两部分:RGB颜色和Alpha值。我们将在一些不同的地方使用这些数据,所以让我们定义一个方便的Surface结构体来包含所有相关数据。把这个结构体放在ShaderLibrary文件夹中的一个单独的Surface.hlsl文件中。
#ifndef CUSTOM_SURFACE_INCLUDED #define CUSTOM_SURFACE_INCLUDED struct Surface { float3 normal; float3 color; float alpha; }; #endif
我们不应该把法线定义为 normalWS 吗?
.
可以,但是表面不关心法线是在什么空间定义的。光照计算可以在任何合适的3D空间中进行,所以我们不为法线定义这个空间限制。当填充数据时,我们只需要在所有地方使用相同的空间。我们将使用世界空间,但我们之后有可能会切换到另一个空间,一切仍将保持不变。
在LitPass中Common之后引用它,这样我们就可以保持LitPass的简洁。从现在起,我们将把专用的代码放在它们自己的HLSL文件中,以便更容易地定位相关的功能。
#include "../ShaderLibrary/Common.hlsl" #include "../ShaderLibrary/Surface.hlsl"
在LitPassFragment中定义一个surface变量并填充它,结果变成表面的颜色和透明度。
Surface surface; surface.normal = normalize(input.normalWS); surface.color = base.rgb; surface.alpha = base.a; return float4(surface.color, surface.alpha);
这不是低效的代码吗?
.
这没有区别,因为着色器编译器将生成高度优化的程序,完全重写了我们的代码。结构体纯粹是为了方便我们使用。你可以通过在着色器面板的 Compile and show code按钮检查编译器的编译结果。
1.5 光照计算(Calculating Lighting)
为了计算实际的光照,我们将创建一个具有Surface参数的GetLighting函数。最初让它返回表面法线的Y分量。因为这是照明功能,我们将把它放在一个单独的Lighting.hlsl文件中。
#ifndef CUSTOM_LIGHTING_INCLUDED #define CUSTOM_LIGHTING_INCLUDED float3 GetLighting (Surface surface) { return surface.normal.y; } #endif
在LitPass中引用Surface之后引用它,因为照明依赖于Surface.hlsl。
#include "../ShaderLibrary/Surface.hlsl" #include "../ShaderLibrary/Lighting.hlsl"
为什么不在 Lighting.hlsl 中 引用 Surface.hlsl?
.
我们可以这样做,但最终的结果可能是多个文件依赖于多个其他文件,依赖关系会十分杂乱。相反,我选择将所有include语句放在一个地方,这样可以明确依赖关系。这也使得用一个文件替换另一个文件从而改变着色器的工作方式变得更加容易,只要新文件定义了其他文件依赖的相同功能。
现在我们可以在LitPassFragment中获得照明,并将其用于片元函数返回颜色的RGB部分。
float3 color = GetLighting(surface); return float4(color, surface.alpha);
漫反射光照
现在,输出的是表面法线的Y分量,所以它在球体的顶部是1,在它的两侧是0。再往下结果变为负值,并在底部达到- 1。但我们观察不到负值,它等于法向量和上(up)向量夹角的余弦值。忽略负的部分,这在视觉上就好像一个漫反射的方向光从上垂直的向下照明。最后一步是在GetLighting中把表面颜色合并到结果中,将其诠释为表面反照率(Albedo)。
float3 GetLighting (Surface surface) { return surface.normal.y * surface.color; }
应用反照率
反照率(Albedo)是什么意思?
.
反照率在拉丁语中是白色程度的意思。它代表着光被一个表面漫反射的程度。如果反照率不是全白,那么意味着部分光能被吸收而不是被反射。
2. 灯光(Lights)
为了表现合适的照明,我们还需要知道光源的属性。在本章节中,我们将只使用平行光。平行光代表着一个距离很远很远的光源,它的位置并不重要,重要的是它的方向。这是一种简化,但它足以模拟地球上的太阳光和其他单向光线的情况。
2.1 光源结构(Light Structure)
................
(未完待续,努力更新中 >.<) |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|