https://catlikecoding.com/unity/tutorials/custom-srp/directional-lights/
光照
Lit Shader
这里新建一个可接受光照的shader。新建LitPass.hlsl
文件,内容和UnlitPass.hlsl
一致,修改函数名:
#ifndef CUSTOM_LIT_PASS_INCLUDED
#define CUSTOM_LIT_PASS_INCLUDED
...
Varyings LitPassVertex(Attributes input)
{
}
float4 LitPassFragment(Varyings input) : SV_TARGET
{
}
#endif
Lit.shader
:
Shader "Custom RP/Lit" {
Properties {
_BaseMap("Texture", 2D) = "white" {}
_BaseColor("Color", Color) = (0.5, 0.5, 0.5, 1.0)
...
}
SubShader {
Pass {
HLSLPROGRAM
...
#pragma vertex LitPassVertex
#pragma fragment LitPassFragment
#include "LitPass.hlsl"
ENDHLSL
}
}
我们将使用自定义的光照模式,加上标签:
Pass {
Tags {
"LightMode" = "CustomLit"
}
在CameraRenderer
中加入该pass:
static ShaderTagId
unlitShaderTagId = new ShaderTagId("SRPDefaultUnlit"),
litShaderTagId = new ShaderTagId("CustomLit");
接着添加到DrawVisibleGeometry
中:
var drawingSettings = new DrawingSettings(unlitShaderTagId, sortingSettings)
{
enableDynamicBatching = useDynamicBatching,
enableInstancing = useGPUInstancing
};
drawingSettings.SetShaderPassName(1, litShaderTagId);
法线
为使用光照模型,我们需要法线。在结构体中声明:
struct Attributes
{
float3 positionOS : POSITION;
float3 normalOS : NORMAL;
float2 baseUV : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Varyings
{
float4 positionCS : SV_POSITION;
float3 positionWS : VAR_POSITION;
float3 normalWS : VAR_NORMAL;
float2 baseUV : VAR_BASE_UV;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
在顶点着色器中我们使用TransformObjectToWorldNormal
来完成法线从模型空间到世界空间的变换:
float3 positionWS = TransformObjectToWorld(input.positionOS);
output.positionCS = TransformWorldToHClip(positionWS);
output.normalWS = TransformObjectToWorldNormal(input.normalOS);
我们可以在片元着色器中用颜色显示法线:
base.rgb = input.normalWS;
return base;
注意,小于0的值会取为0。
法线插值
尽管法线在顶点着色器中是单位长度的,但经过沿三角形插值后其长度会变化,因此在片元着色器中要进行标准化:
base.rgb = normalize(input.normalWS);
表面属性
shader中的光照是模拟光线遇到表面后的反应的,因此我们需要跟踪表面的属性。我们新建一个Surface.hlsl
文件,置于ShaderLibrary
文件夹下:
#ifndef CUSTOM_SURFACE_INCLUDED
#define CUSTOM_SURFACE_INCLUDED
struct Surface
{
float3 normal;
float3 color;
float alpha;
};
#endif
在LitPass
中包含:
#include "../ShaderLibrary/Common.hlsl"
#include "../ShaderLibrary/Surface.hlsl"
在片元着色器中定义一个surface变量,并填充:
Surface surface;
surface.normal = normalize(input.normalWS);
surface.color = base.rgb;
surface.alpha = base.a;
return float4(surface.color, surface.alpha);
计算光照
新建一个Lighting.hlsl
文件,存放光照计算函数:
#ifndef CUSTOM_LIGHTING_INCLUDED
#define CUSTOM_LIGHTING_INCLUDED
float3 GetLighting(Surface surface)
{
return surface.normal.y * surface.color;
}
#endif
在LitPass
中包含:
#include "../ShaderLibrary/Common.hlsl"
#include "../ShaderLibrary/Surface.hlsl"
#include "../ShaderLibrary/Lighting.hlsl"
在片元着色器中可以使用GetLighting
获取光照:
float3 color = GetLighting(surface);
return float4(color, surface.alpha);
灯光
这里我们只模拟拥有方向的平行光。
灯光结构体
新建Light.hlsl
文件,定义一个平行光结构体,同时定义一个GetDirectionalLight
函数来返回一个配置好的平行光:
#ifndef CUSTOM_LIGHT_INCLUDED
#define CUSTOM_LIGHT_INCLUDED
struct Light
{
float3 color;
float3 direction;
float attenuation;
};
Light GetDirectionalLight(int index, Surface surfaceWS, ShadowData shadowData)
{
Light light;
light.color = 1.0;
light.direction = float3(0.0, 1.0, 0.0);
return light;
}
#endif
在LitPass
中包含:
#include "../ShaderLibrary/Common.hlsl"
#include "../ShaderLibrary/Surface.hlsl"
#include "../ShaderLibrary/Light.hlsl"
#include "../ShaderLibrary/Lighting.hlsl"
光照函数
在文件中新增一个IncomingLight
函数,用于计算光照程度,这里使用兰伯特模型:
float3 IncomingLight(Surface surface, Light light)
{
return saturate(dot(surface.normal, light.direction)* light.color;
}
同时添加一个重载函数GetLighting
,用于计算每个灯光的光照:
float3 GetLighting(Surface surface, Light light)
{
return IncomingLight(surface, light) * surface.color;
}
修改另一个GetLighting
,计算总的光照:
float3 GetLighting(Surface surface)
{
return GetLighting(surface, light);
}
将灯光数据送往GPU
为了在shader中可以得到灯光数据,我们定义几个uniform变量,放于_CustomLight
缓冲中:
CBUFFER_START(_CustomLight)
float3 _DirectionalLightColors;
float3 _DirectionalLightDirection;
CBUFFER_END
在GetDirectionalLight
中使用:
Light GetDirectionalLight()
{
Light light;
light.color = _DirectionalLightColors;
light.direction = _DirectionalLightDirections;
return light;
}
现在我们将数据送往GPU。创建一个新的类Lighting
,类似于CameraRenderer
:
public class Lighting
{
const string bufferName = "Lighting";
CommandBuffer buffer = new CommandBuffer
{
name = bufferName
};
public void Setup(ScriptableRenderContext context)
{
buffer.BeginSample(bufferName);
SetupDirectionalLight();
buffer.EndSample(bufferName);
context.ExecuteCommandBuffer(buffer);
buffer.Clear();
}
void SetupDirectionalLight()
{
}
}
同时跟踪两个shader属性:
static int
dirLightColorsId = Shader.PropertyToID("_DirectionalLightColor"),
dirLightDirectionsId = Shader.PropertyToID("_DirectionalLightDirection");
我们可以使用RenderSetting.sun
来获取场景中的主光源:
void SetupDirectionalLight(int index, ref VisibleLight visibleLight)
{
Light light = RenderSetting.sun;
buffer.SetGlobalVector(dirLightColorId, light.color.linear * light.intensity);
buffer.SetGlobalVector(dirLightDirectionalId, -light.transform.forward);
}
在CameraRenderer
中设置光照:
Lighting lighting = new Lighting();
public void Render(ScriptableRenderContext context, Camera camera, bool useDynamicBatching, bool useGPUInstancing)
{
Setup();
lighting.Setup(context);
DrawVisibleGeometry(useDynamicBatching, useGPUInstancing);
DrawUnsupportedShaders();
DrawGizmos();
lighting.Cleanup();
Submit();
}
可见光
在进行剔除时,Unity也会判断那些灯光会影响摄像机的可见范围。为此,Lighting
需要获取剔除结果,在Setup
中添加一个参数,同时使用SetupLights
进行替换:
public void Setup(ScriptableRenderContext context, CullingResults cullingResults)
{
this.cullingResults = cullingResults;
buffer.BeginSample(bufferName);
SetupLights();
...
}
void SetupLights()
{
}
在CameraRenderer.Render
中添加参数:
lighting.Setup(context, cullingResults);
使用NativeArray
设置可见光:
using Unity.Collections;
public class Lighting
{
...
void SetupLights()
{
NativeArray<VisibleLight> visibleLights = cullingResults.visibleLights;
}
...
}
多个平行光
使用可见光数据的话,可以支持多个平行光源,但我们需要将这些数据送往GPU。首先定义平行光源的最大数量,接着定义相应的颜色和方向数组,记得声明相应的材质属性ID:
const int maxDirLightCount = 4;
static int
dirLightCountId = Shader.PropertyToID("_DirectionalLightCount"),
dirLightColorsId = Shader.PropertyToID("_DirectionalLightColors"),
dirLightDirectionsId = Shader.PropertyToID("_DirectionalLightDirections");
static Vector4[]
dirLightColors = new Vector4[maxDirLightCount],
dirLightDirections = new Vector4[maxDirLightCount];
为SetupDirectionalLight
函数添加一个索引参数,可以根据索引获得相应光源的属性:
void SetupDirectionalLight(int index, ref VisibleLight visibleLight)
{
dirLightColors[index] = visibleLight.finalColor;
dirLightDirections[index] = -visibleLight.localToWorldMatrix.GetColumn(2);
}
Unity默认不使用线性空间的灯光强度:
public CustomRenderPipeline(bool useDynamicBatching, bool useGPUInstancing, bool useSRPBatcher)
{
this.useDynamicBatching = useDynamicBatching;
this.useGPUInstancing = useGPUInstancing;
GraphicsSettings.useScriptableRenderPipelineBatching = useSRPBatcher;
GraphicsSettings.lightsUseLinearIntensity = true;
}
接着我们遍历可见光,并设置灯光属性:
void SetupLights()
{
NativeArray<VisibleLight> visibleLights = cullingResults.visibleLights;
for (int i = 0; i < visibleLights.Length; i++)
{
VisibleLight visibleLight = visibleLights[i];
SetupDirectionalLight(i, visibleLight);
}
buffer.SetGlobalInt(dirLightCountId, dirLightCount);
buffer.SetGlobalVectorArray(dirLightColorsId, dirLightColors);
buffer.SetGlobalVectorArray(dirLightDirectionsId, dirLightDirections);
}
注意,我们目前只支持至多4个平行光:
void SetupLights()
{
NativeArray<VisibleLight> visibleLights = cullingResults.visibleLights;
int dirLightCount = 0;
for (int i = 0; i < visibleLights.Length; i++)
{
VisibleLight visibleLight = visibleLights[i];
if (visibleLight.lightType == LightType.Directional)
{
SetupDirectionalLight(dirLightCount++, ref visibleLight);
if (dirLightCount >= maxDirLightCount)
{
break;
}
}
}
buffer.SetGlobalInt(dirLightCountId, dirLightCount);
buffer.SetGlobalVectorArray(dirLightColorsId, dirLightColors);
buffer.SetGlobalVectorArray(dirLightDirectionsId, dirLightDirections);
}
Shader循环
修改Light.hlsl
中的_CustomLight
缓冲:
#define MAX_DIRECTIONAL_LIGHT_COUNT 4
CBUFFER_START(_CustomLight)
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;
}
接着修改针对一个表面的GetLighting
函数:
float3 GetLighting(Surface surface)
{
float3 color = 0.0;
for (int i = 0; i < GetDirectionalLightCount(); i++)
{
Light light = GetDirectionalLight(i);
color += GetLighting(surface, light);
}
return color;
}
BRDF
BRDF不过多介绍
表面属性
这里使用金属流程,我们在Surface
中添加金属度和光滑度的属性,在shader中也添加对应的属性:
_Metallic ("Metallic", Range(0, 1)) = 0
_Smoothness ("Smoothness", Range(0, 1)) = 0.5
struct Surface
{
float3 normal;
float3 color;
float alpha;
float metallic;
float smoothness;
};
UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
UNITY_DEFINE_INSTANCED_PROP(float4, _BaseMap_ST)
UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)
UNITY_DEFINE_INSTANCED_PROP(float, _Cutoff)
UNITY_DEFINE_INSTANCED_PROP(float, _Metallic)
UNITY_DEFINE_INSTANCED_PROP(float, _Smoothness)
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)
在片元着色器中填充相应属性:
surface.metallic = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _Metallic);
surface.smoothness = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _Smoothness);
BRDF属性
BRDF是漫反射和高光反射的结合,除了将表面颜色分为这两块外,我们还需要知道表面的粗糙度。新建BRDF.hlsl
,加入结构体:
#ifndef CUSTOM_BRDF_INCLUDED
#define CUSTOM_BRDF_INCLUDED
struct BRDF
{
float3 diffuse;
float3 specular;
float roughness;
};
#endif
添加GetBRDF
方法,返回某一表面的BRDF数据:
BRDF GetBRDF(inout Surface surface)
{
BRDF brdf;
brdf.diffuse = surface.color;
brdf.specular = 0.0;
brdf.roughness = 1.0;
return brdf;
}
在LitPass
中包含相应文件:
#include "../ShaderLibrary/Common.hlsl"
#include "../ShaderLibrary/Surface.hlsl"
#include "../ShaderLibrary/Light.hlsl"
#include "../ShaderLibrary/BRDF.hlsl"
#include "../ShaderLibrary/Lighting.hlsl"
在所有的GetLighting
函数中加入brdf参数,使用其diffuse属性替换:
float3 GetLighting(Surface surface, BRDF brdf, Light light)
{
return IncomingLight(surface, light) * brdf.diffuse;
}
float3 GetLighting(Surface surface, BRDF brdf)
{
float3 color = 0.0;
for (int i = 0; i < GetDirectionalLightCount(); i++)
{
Light light = GetDirectionalLight(i, surface);
color += GetLighting(surface, brdf, light);
}
return color;
}
在片元着色器末尾调用:
BRDF brdf = GetBRDF(surface);
float3 color = GetLighting(surface, brdf);
反射率
这里我们假设金属材质进行完全高光反射,反射率等价为表面的金属度,以此计算漫反射:
float oneMinusReflectivity = surface.metallic;
brdf.diffuse = surface.color * oneMinusReflectivity;
不过实际中,非金属也会有光的反射,即高光,它们的反射率的平均值大约为0.04。我们添加一个函数来将1-反射率的范围调整到0-0.96(即URP采用的方法):
#define MIN_REFLECTIVITY 0.04
float OneMinusReflectivity(float metallic)
{
float range = 1.0 - MIN_REFLECTIVITY;
return range - metallic * range;
}
使用该函数计算漫反射:
float oneMinusReflectivity = OneMinusReflectivity(surface.metallic);
brdf.diffuse = surface.color * oneMinusReflectivity;
高光
根据能量守恒公式,反射的出射光的能量不会大部入射光的能量,也就是说直接相减就能得到高光颜色:
brdf.diffuse = surface.color * oneMinusReflectivity;
brdf.specular = surface.color - brdf.diffuse;
不过这样就忽略了一点,金属材质会影响高光颜色,而非金属不会,非金属的高光应该是白色的,我们可以将表面颜色与最小的反射率根据表面的金属度插值得到:
brdf.specular = lerp(MIN_REFLECTIVITY, surface.color, surface.metallic);
粗糙度
Core RP Library有一个方法PerceptualSmoothnessToPerceptualRoughness
,可以根据表面的光滑程度获得粗糙度:
real PerceptualSmoothnessToPerceptualRoughness(real perceptualSmoothness)
{
return (1.0 - perceptualSmoothness);
}
perceptual即感知到的。
真正的粗糙度可以使用PerceptualRoughnessToRoughness
得到:
real RoughnessToPerceptualRoughness(real roughness)
{
return sqrt(roughness);
}
这些方法在CommonMaterial.hlsl
文件中:
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/CommonMaterial.hlsl"
#include "UnityInput.hlsl"
使用这些函数获得粗糙度:
float perceptualRoughness = PerceptualSmoothnessToPerceptualRoughness(surface.smoothness);
brdf.roughness = PerceptualRoughnessToRoughness(perceptualRoughness);
观察方向
我们需要摄像机的位置,在UnityInput
中定义:
float3 _WorldSpaceCameraPos;
同时相关光照计算我们需要顶点的世界坐标:
struct Varyings
{
float4 positionCS : SV_POSITION;
float3 positionWS : VAR_POSITION;
...
};
Varyings LitPassVertex(Attributes input)
{
...
output.positionWS = TransformObjectToWorld(input.positionOS);
output.positionCS = TransformWorldToHClip(output.positionWS);
...
}
我们可以将观察方向作为表面属性的一部分:
struct Surface
{
float3 normal;
float3 viewDirection;
float3 color;
float alpha;
float metallic;
float smoothness;
};
接着在片元着色器中计算:
surface.viewDirection = normalize(_WorldSpaceCameraPos - input.positionWS);
高光强度
这里的高光强度计算使用简化CookTorrance BRDF的一种变体,也是URP使用的。因为需要平方计算,我们在Common.hlsl
添加相应的方法:
float Square(float v)
{
return v * v;
}
上述BRDF的计算公式为:,其中 ,r是粗糙度,N是法线,L是入射光方向,H是中间矢量,:
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) + 1.0001);
float normalization = brdf.roughness * 4.0 + 2.0;
return r2 / (d2 * max(0.1, lh2) * normalization);
}
接着添加一个计算最后颜色的函数:
float3 DirectBRDF(Surface surface, BRDF brdf, Light light)
{
return SpecularStrength(surface, brdf, light) * brdf.specular + brdf.diffuse;
}
在针对单个光源的GetLighting
中应用:
float3 GetLighting(Surface surface, BRDF brdf, Light light)
{
return IncomingLight(surface, light) * DirectBRDF(surface, brdf, light);
}
透明
如果应用带有透明度的纹理进行透明度混合的话,会发现反射光会变暗:
这并不合理,漫发射的确会变暗,但高光反射就说不通了(比如玻璃)。
预乘Alpha
我们只想让漫发射衰减,而高光反射不变。为了能够不应用衰减,我们将混合模式的Source改为1。不过这样的话,漫反射也不会衰减了,因此我们在GetBRDF
中提前乘上alpha值:
brdf.diffuse = surface.color * oneMinusReflectivity;
brdf.diffuse *= surface.alpha;
预乘选项
我们想同时支持预乘和一般情况,因此加上一个参数:
BRDF GetBRDF(inout Surface surface, bool applyAlphaToDiffuse = false)
{
...
brdf.diffuse = surface.color * oneMinusReflectivity;
if (applyAlphaToDiffuse)
{
brdf.diffuse *= surface.alpha;
}
...
}
在片元着色器中,我们可以使用_PREMULTIPLY_ALPHA
关键字来决定是否使用预乘:
#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);
同时添加相应的着色器变体和属性配置:
#pragma shader_feature _PREMULTIPLY_ALPHA
[Toggle(_PREMULTIPLY_ALPHA)] _PremulAlpha ("Premultiply Alpha", Float) = 0