catlikecoding Custom SRP第三章,主要是有关直接光的内容。
1、Lighting
1.1 Lit Shader
1、将UnlitPass.hlsl复制一份,改名为LitPass,用来写有关光照的着色器。
2、在Lit.shader和LitPass.hlsl代码中的Unlit改成Lit,包括shader的名字,#include的文件名,顶点和片元着色器的函数名称。
3、在Lit.shader中的Pass中添加Tags
Pass {
Tags {
"LightMode" = "CustomLit"
}
…
}
4、在CameraRender.cs中,为drawingSettings添加着色器标记标识符。
static ShaderTagId
unlitShaderTagId = new ShaderTagId("SRPDefaultUnlit"),
litShaderTagId = new ShaderTagId("CustomLit");
void DrawVisibleGeometry(bool useDynamicBatching, bool useGPUInstancing)
{
···
var drawingSettings = new DrawingSettings(unlitShaderTagId, sortingSettings)
{
···
};
// 要渲染使用这个pass的对象,我们必须把它添加进CameraRenderer中,利用Tags标识符。
drawingSettings.SetShaderPassName(1, litShaderTagId);
···
}
1.2 Normal Vectors
法线向量也是顶点数据的一部分,将其定义在Attributes和Varyings结构体中:
struct Attributes {
···
float3 normalOS : NORMAL;
};
struct Varyings {
···
float3 normalWS : VAR_NORMAL;
};
OS和WS分别代表:ObjectSpace和WorldSpace,即模型空间和世界空间。
通过函数TransformObjectToWorldNormal,可以将法线空间转换为世界空间。
这个函数的原理,在原教程中解释如下图:
1.3 Interpolated Normals
这一步我之前写shader确实没注意到,之前都是在VertexShader中,先将法线给normalize了。。。
尽管法线向量在顶点程序中是单位长度,但跨三角形的线性插值会影响它们的长度。所以,我们需要在FragmentShader中,通过normalize(normal),来消除插值失真。
1.4 Surface Properties
创建了一个Surface.hlsl文件,主要是通过定义一个方便的结构来包含与光照计算的相关数据。
#ifndef CUSTOM_SURFACE_INCLUDED
#define CUSTOM_SURFACE_INCLUDED
struct Surface {
float3 normal;
float3 color;
float alpha;
};
#endif
// LitPass.hlsl
#include "../ShaderLibrary/Common.hlsl"
#include "../ShaderLibrary/Surface.hlsl"
1.5
创建一个Lightint.hlsl的文件,为了之后计算实际照明,首先返回法线的y分量。此时就相当于:法向量和向上向量之间角度的余弦。这句话可以这样理解,就是当一束光指向正下方的平行光,照射到物体身上所产生的漫反射,就是dot(LightDir,Normal),会根据物体各个点上的法线与竖直方向的平行光来计算夹角的余弦。
最后的点睛之笔是,如果将表面颜色考虑在结果中。就是我们常说的Albode,它是表面漫反射光量的量度。
#ifndef CUSTOM_LIGHTING_INCLUDED
#define CUSTOM_LIGHTING_INCLUDED
float3 GetLighting (Surface surface) {
return surface.normal.y * surface.color;
}
#endif
// LitPass.hlsl
#include "../ShaderLibrary/Surface.hlsl"
#include "../ShaderLibrary/Lighting.hlsl"
此时的LitPass.hlsl文件中的FragmentShader代码如下:
// Fragment Shader
float4 LitPassFragment (Varyings input) : SV_TARGET {
UNITY_SETUP_INSTANCE_ID(input);
float4 baseMap = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.baseUV);
float4 baseColor = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseColor);
float4 base = baseMap * baseColor;
clip(base.a - UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _Cutoff));
// base.rgb = normalize(input.normal);
Surface surface;
surface.normal = normalize(input.normal);
surface.color = base.rgb;
surface.alpha = base.a;
float3 color = GetLighting(surface);
return float4(color, surface.alpha);
}
2、Lights
这部分概述如下:
1、在LitPass.hlsl中添加灯光结构体和光照函数(目前只有漫反射)。
2、将场景中的灯光数据,通过Command Buffer传递给GPU,提供给shader计算光照使用。原理是通过在Command Buffer中绑定两个着色器属性的标识符。
// 定义 static int dirLightCountId = Shader.PropertyToID("_DirectionalLightCount"); static int dirLightColorsId = Shader.PropertyToID("_DirectionalLightColors"); static int dirLightDirectionsId = Shader.PropertyToID("_DirectionalLightDirections"); // 绑定 buffer.SetGlobalInt(dirLightCountId, visibleLights.Length); buffer.SetGlobalVectorArray(dirLightColorsId, dirLightColors); buffer.SetGlobalVectorArray(dirLightDirectionsId, dirLightDirections);
3、获取摄像机视锥体范围内的平行光,本教程设置最大支持4个平行光。代码:cullingResults.visibleLights
4、在Lighting.cs的SetupDirectionalLight()中计算灯光的最终颜色,最终颜色已经提供了灯光的强度,但是Unity不会把它转换到线性空间里。我们必须在CustomRenderPipeline.cs中设置 GraphicsSettings.lightsUseLinearIntensity = true。
5、在CameraRender.cs脚本的Render()函数中,在绘制可见几何体之前添加Lighting实例, 用它来设置lighting。lighting.Setup(context, cullingResults);6、对于多平行光了,可通过循环赋予灯光颜色和灯光方向的数组,传递给Shader。
7、多平行光中,获取单个平行光的颜色和方向(visibleLight.localToWorldMatrix,矩阵中的第三列)
颜色:visibleLight.finalColor;
方向:-visibleLight.localToWorldMatrix.GetColumn(2);
// Lighting.cs
using Unity.Collections;
using UnityEngine;
using UnityEngine.Rendering;
public class Lighting
{
const int maxDirLightCount = 4;
/*static int dirLightColorId = Shader.PropertyToID("_DirectionalLightColor");
static int dirLightDirectionId = Shader.PropertyToID("_DirectionalLightDirection");*/
// shader对struct buffer的支持还不够好,要么只存在于片段程序中,要么性能比常规的数组差 。好消息是数据在CPU和GPU之间传递的细节只在少数地方需要关注。
static int dirLightCountId = Shader.PropertyToID("_DirectionalLightCount");
static int dirLightColorsId = Shader.PropertyToID("_DirectionalLightColors");
static int dirLightDirectionsId = Shader.PropertyToID("_DirectionalLightDirections");
static Vector4[] dirLightColors = new Vector4[maxDirLightCount];
static Vector4[] dirLightDirections = new Vector4[maxDirLightCount];
const string bufferName = "Lighting";
CommandBuffer buffer = new CommandBuffer
{
name = bufferName
};
CullingResults cullingResults;
public void Setup(ScriptableRenderContext context, CullingResults cullingResults)
{
this.cullingResults = cullingResults;
buffer.BeginSample(bufferName);
SetupLights();
buffer.EndSample(bufferName);
context.ExecuteCommandBuffer(buffer);
buffer.Clear();
}
// 使用CommandBuffer.SetGlobalVector将灯光数据发送到GPU。颜色是灯光在线性空间中的颜色,而方向是灯光变换的前向向量求反。
void SetupLights()
{
// 它是类似数组的一种结构,但提供与本地内存缓冲的连接功能。它使得在托管C代码和本机Unity引擎代码之间高效地共享数据成为可能。
// 使用可见光数据让RP支持多个平行光成为可能,但是我们必须发送这些灯光数据给GPU。
NativeArray<VisibleLight> visibleLights = cullingResults.visibleLights;
// 遍历Lighting.SetupLights中所有的可见光,并为每个元素调用SetupDirectionalLight。
// 然后调用缓冲区上的SetGlobalInt和SetGlobalVectorArrray将数据发送给GPU。
int dirLightCount = 0;
for (int i = 0; i < visibleLights.Length; i++)
{
VisibleLight visibleLight = visibleLights[i];
if (visibleLight.lightType == LightType.Directional)
{
// visibleLight 是结构体,这里通过传递引用,来节省内存资源。
SetupDirectionalLight(dirLightCount++, ref visibleLight);
if (dirLightCount >= maxDirLightCount)
{
break;
}
}
}
buffer.SetGlobalInt(dirLightCountId, visibleLights.Length);
buffer.SetGlobalVectorArray(dirLightColorsId, dirLightColors);
buffer.SetGlobalVectorArray(dirLightDirectionsId, dirLightDirections);
}
void SetupDirectionalLight(int index, ref VisibleLight visibleLight)
{
dirLightColors[index] = visibleLight.finalColor;
// 前向向量可以在VisibleLight.localToWorldMatrix属性中找到。矩阵中的第三列就是,这里还是要求个反向。
dirLightDirections[index] = -visibleLight.localToWorldMatrix.GetColumn(2);
}
}
// CustomRenderPipeline.cs
public CustomRenderPipeline(bool useDynamicBatching, bool useGPUInstancing, bool useSRPBatcher)
{
···
GraphicsSettings.useScriptableRenderPipelineBatching = useSRPBatcher;
// 在Lighting.cs的SetupDirectionalLight()中计算灯光的最终颜色,最终颜色已经提供了灯光的强度,但是Unity不会把它转换到线性空间里。我们必须设置 GraphicsSettings.lightsUseLinearIntensity 为真。
GraphicsSettings.lightsUseLinearIntensity = true;
}
// CameraRender.cs
public void Render(ScriptableRenderContext context, Camera camera, bool useDynamicBatching, bool useGPUInstancing)
{
···
Setup();
// Lighting实例, 在绘制可见几何体之前用它来设置lighting。
lighting.Setup(context, cullingResults);
···
}
// Light.hlsl
#ifndef CUSTOM_LIGHT_INCLUDED
#define CUSTOM_LIGHT_INCLUDED
struct Light {
float3 color;
float3 direction;
};
#define MAX_DIRECTIONAL_LIGHT_COUNT 4
CBUFFER_START(_CustomLight)
//float4 _DirectionalLightColor;
//float4 _DirectionalLightDirection;
int _DirectionalLightCount;
float4 _DirectionalLightColors[MAX_DIRECTIONAL_LIGHT_COUNT];
float4 _DirectionalLightDirections[MAX_DIRECTIONAL_LIGHT_COUNT];
CBUFFER_END
int GetDirectionalLightCount () {
return _DirectionalLightCount;
}
Light GetDirectionalLight (int index) {
Light light;
light.color = _DirectionalLightColors[index].rgb;
light.direction = _DirectionalLightDirections[index].xyz;
return light;
}
#endif
//Lighting.hlsl
#ifndef CUSTOM_LIGHTING_INCLUDED
#define CUSTOM_LIGHTING_INCLUDED
float3 IncomingLight (Surface surface, Light light) {
return saturate(dot(surface.normal, light.direction)) * light.color;
}
float3 GetLighting (Surface surface, Light light) {
return IncomingLight(surface, light) * surface.color;
}
float3 GetLighting (Surface surface) {
float3 color = 0.0;
for (int i = 0; i < GetDirectionalLightCount(); i++) {
color += GetLighting(surface, GetDirectionalLight(i));
}
return color;
}
#endif
3、BRDF(双向反射分布函数)
本教程采用URP所使用的方法。
使用metallic工作流,这需要我们在Lit shader中添加两个表面属性:_Metallic和_Smoothness,即表面金属度和光滑度。
接下来和shader中的_Cutoff属性一样,需要分别在
UnityPerMaterial
buffer、Surface struct、LitPassFragment函数、PerObjectMaterialProperties.cs中添加相同的代码。
这里再强调一下,使用PerObjectMaterialProperties.cs脚本,相当于不同物体使用相同的材质球,但是可以使用不同的属性,因此可以采用GPU Instance合批处理。
这也是为什么要把属性添加到UnityPerMaterial buffer中的UNITY_DEFINE_INSTANCED_PROP中,和LitFragment中的UNITY_ACCESS_INSTANCED_PROP
使用表面属性来计算BRDF方程,需要把曲面的颜色分为漫反射和镜面反射的部分,我们还需要知道曲面的粗糙度。
#ifndef CUSTOM_BRDF_INCLUDED
#define CUSTOM_BRDF_INCLUDED
struct BRDF {
float3 diffuse;
float3 specular;
float roughness;
};
#endif
接下来每步的操作可以看官网教程,我这里就直接把代码放出来复盘一下执行流程。
struct Light {
float3 color;
float3 direction;
};
struct Surface {
float3 normal;
float3 viewDirection;
float3 color;
float alpha;
float metallic;
float smoothness;
};
struct BRDF {
float3 diffuse;
float3 specular;
float roughness;
};
// Fragment Shader
float4 LitPassFragment (Varyings input) : SV_TARGET {
UNITY_SETUP_INSTANCE_ID(input);
float4 baseMap = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.baseUV);
float4 baseColor = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseColor);
float4 base = baseMap * baseColor;
#if defined(_CLIPPING)
clip(base.a - UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _Cutoff));
#endif
Surface surface;
surface.normal = normalize(input.normal);
surface.viewDirection = normalize(_WorldSpaceCameraPos - input.pos_World);
surface.color = base.rgb;
surface.alpha = base.a;
surface.metallic = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _Metallic);
surface.smoothness = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _Smoothness);
// 针对玻璃这种透明材质
#if defined(_PREMULTIPLY_ALPHA)
BRDF brdf = GetBRDF(surface, true);
#else
BRDF brdf = GetBRDF(surface);
#endif
float3 color = GetLighting(surface, brdf);
return float4(color, surface.alpha);
}
1、我们先来看看在没有执行BRDF的时候,我们有什么(暂忽略clip)。
surface.normal:归一化的世界空间下的法线。
surface.viewDirection:物体表面的点指向摄像机方向。
surface.color:贴图 * baseColor。
surface.alpha:物体透明度。
surface.metallic和surface.smoothness:物体金属度和光滑度。
2、执行完BRDF,我们获得了什么。
BRDF GetBRDF (Surface surface, bool applyAlphaToDiffuse = false) { BRDF brdf; float oneMinusReflectivity = OneMinusReflectivity(surface.metallic); brdf.diffuse = surface.color * oneMinusReflectivity; // 当材质是半透明时,漫反射要乘alpha,这是因为blend的src设置为one了。 if (applyAlphaToDiffuse) { brdf.diffuse *= surface.alpha; } // 能量守恒:出射光的数量不能超过入射光的数量。 // 金属会影响镜面反射的颜色,但是非金属不会。非金属表面的反射颜色应该是白色。 brdf.specular = lerp(MIN_REFLECTIVITY, surface.color, surface.metallic); // roughness就相当于1 - smoothness,我们采用CommonMaterial.hlsl库里的转换函数:PerceptualSmoothnessToPerceptualRoughness(real smoothness)。 // 新的变量类型real定义在commom.hlsl库里,根据不同的平台编译float或half。 // PerceptualSmoothnessToPerceptualRoughness()源码里其实就是 1 - roughness // 这里用的是迪士尼光照模型,转化为平方,这样之后在材质编辑的时候更加直观。 float perceptualRoughness = PerceptualSmoothnessToPerceptualRoughness(surface.smoothness); brdf.roughness = PerceptualRoughnessToRoughness(perceptualRoughness); return brdf; }
我们获取了表面diffuse和specular部分,这里并不是最终的diffuse和specular的结果,而是表面漫反射和镜面反射颜色,会在之后跟光的计算中使用到。
我们还获得了表面粗糙度的属性。粗糙度 = 1 - 光滑度。
接下来看看最终的颜色值,也就是片段着色器中的float3 color = GetLighting(surface, brdf);
float Square (float v) { return v * v; }
// 入射光 float3 IncomingLight (Surface surface, Light light) { return saturate(dot(surface.normal, light.direction)) * light.color; } float3 GetLighting (Surface surface, BRDF brdf, Light light) { return IncomingLight(surface, light) * DirectBRDF(surface, brdf, light); } float3 GetLighting (Surface surface, BRDF brdf) { float3 color = 0.0; for (int i = 0; i < GetDirectionalLightCount(); i++) { color += GetLighting(surface, brdf, GetDirectionalLight(i)); } return color; } // 我们观察到的镜面反射强度取决于我们的观察方向与完美反射方向的匹配程度。我们将使用和URP一样的公式,它是简化CookTorrance的变体。 // 简单来说就是计算高光强度。 float SpecularStrength (Surface surface, BRDF brdf, Light light) { float3 h = SafeNormalize(light.direction + surface.viewDirection); float nh2 = Square(saturate(dot(surface.normal, h))); float lh2 = Square(saturate(dot(light.direction, h))); float r2 = Square(brdf.roughness); float d2 = Square(nh2 * (r2 - 1.0) + 1.00001); float normalization = brdf.roughness * 4.0 + 2.0; return r2 / (d2 * max(0.1, lh2) * normalization); } // 给定曲面,灯光,BRDF通过直接照明获得的颜色。结果是结果为镜面反射颜色乘以镜面反射强度加上漫反射。 float3 DirectBRDF (Surface surface, BRDF brdf, Light light) { return SpecularStrength(surface, brdf, light) * brdf.specular + brdf.diffuse; }
这里只说一下单灯光,多灯光就是循环叠加。
首先是入射光IncomingLight,通过NdotV来计算表面有多少入射光进入,再乘以灯光颜色。
接下来将入射光乘以表面的镜面反射与漫反射颜色之和。
其中镜面反射通过高光颜色乘以高光强度,高光强度使用的是和URP一样的公式,它是简化CookTorrance的变体:
3.10 MeshBall.cs
为MeshBall添加对metallic和smoothness的支持。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MeshBall : MonoBehaviour
{
static int baseColorId = Shader.PropertyToID("_BaseColor");
static int metallicId = Shader.PropertyToID("_Metallic");
static int smoothnessId = Shader.PropertyToID("_Smoothness");
[SerializeField]
Mesh mesh = default;
[SerializeField]
Material material = default;
Matrix4x4[] matrices = new Matrix4x4[1023];
Vector4[] baseColors = new Vector4[1023];
float[] metallic = new float[1023];
float[] smoothness = new float[1023];
MaterialPropertyBlock block;
void Awake()
{
for (int i = 0; i < matrices.Length; i++)
{
matrices[i] = Matrix4x4.TRS(Random.insideUnitSphere * 10f,
Quaternion.Euler(Random.value * 360f, Random.value * 360f, Random.value * 360f),
Vector3.one * Random.Range(0.5f, 1.5f));
baseColors[i] = new Vector4(Random.value, Random.value, Random.value, Random.Range(0.5f, 1f));
metallic[i] = Random.value < 0.25f ? 1f : 0f;
smoothness[i] = Random.Range(0.05f, 0.95f);
}
}
void Update()
{
if (block == null)
{
block = new MaterialPropertyBlock();
block.SetVectorArray(baseColorId, baseColors);
block.SetFloatArray(metallicId, metallic);
block.SetFloatArray(smoothnessId, smoothness);
}
Graphics.DrawMeshInstanced(mesh, 0, material, matrices, 1023, block);
}
}
4、Transparent 5、Shader GUI
这两部分直接看教程吧。。。Transparent、Shader GUI
最后上两张效果图,第三章完结撒花 ~~~