https://catlikecoding.com/unity/tutorials/custom-srp/point-and-spot-lights/
1 点光
1.1 其它灯光数据
除平行光外,我们至多模拟64个其它类型的光源。Lighting.cs
中:
const int maxDirLightCount = 4, maxOtherLightCount = 64;
我们需要将其它光源的数量、颜色和位置等信息送往GPU:
static int
otherLightCountId = Shader.PropertyToID("_OtherLightCount"),
otherLightColorsId = Shader.PropertyToID("_OtherLightColors"),
otherLightPositionsId = Shader.PropertyToID("_OtherLightPositions");
static Vector4[]
otherLightColors = new Vector4[maxOtherLightCount],
otherLightPositions = new Vector4[maxOtherLightCount];
在SetupLights
中,确保只有有光源的时候才将数据送往GPU:
void SetupLights ()
{
NativeArray visibleLights = cullingResults.visibleLights;
int dirLightCount = 0, otherLightCount = 0;
for (int i = 0; i < visibleLights.Length; i++)
{
…
}
buffer.SetGlobalInt(dirLightCountId, dirLightCount);
if (dirLightCount > 0)
{
buffer.SetGlobalVectorArray(dirLightColorsId, dirLightColors);
buffer.SetGlobalVectorArray(dirLightDirectionsId, dirLightDirections);
buffer.SetGlobalVectorArray(dirLightShadowDataId, dirLightShadowData);
}
buffer.SetGlobalInt(otherLightCountId, otherLightCount);
if (otherLightCount > 0)
{
buffer.SetGlobalVectorArray(otherLightColorsId, otherLightColors);
buffer.SetGlobalVectorArray(
otherLightPositionsId, otherLightPositions
);
}
}
shader层面,在Light.hlsl
中定义相应的变量:
#define MAX_DIRECTIONAL_LIGHT_COUNT 4
#define MAX_OTHER_LIGHT_COUNT 64
CBUFFER_START(_CustomLight)
int _DirectionalLightCount;
float4 _DirectionalLightColors[MAX_DIRECTIONAL_LIGHT_COUNT];
float4 _DirectionalLightDirections[MAX_DIRECTIONAL_LIGHT_COUNT];
float4 _DirectionalLightShadowData[MAX_DIRECTIONAL_LIGHT_COUNT];
int _OtherLightCount;
float4 _OtherLightColors[MAX_OTHER_LIGHT_COUNT];
float4 _OtherLightPositions[MAX_OTHER_LIGHT_COUNT];
CBUFFER_END
定义一个获取其它灯光数量的方法:
int GetOtherLightCount ()
{
return _OtherLightCount;
}
1.2 初始化点光
在Lighting.cs
中创建一个SetupPointLight
方法,初始化一个点光。其位置可由局部到世界转换矩阵的第4列得到:
void SetupPointLight (int index, ref VisibleLight visibleLight)
{
otherLightColors[index] = visibleLight.finalColor;
otherLightPositions[index] = visibleLight.localToWorldMatrix.GetColumn(3);
}
现在,我们需要修改SetupLights
中的循环,根据光源的类型进行不同的选择:
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;
// }
//}
switch (visibleLight.lightType)
{
case LightType.Directional:
if (dirLightCount < maxDirLightCount)
{
SetupDirectionalLight(dirLightCount++, ref visibleLight);
}
break;
case LightType.Point:
if (otherLightCount < maxOtherLightCount)
{
SetupPointLight(otherLightCount++, ref visibleLight);
}
break;
}
}
1.3 着色
在Light.hlsl
中创建一个获取其它光数据的GetOtherLight
方法:
Light GetOtherLight (int index, Surface surfaceWS, ShadowData shadowData)
{
Light light;
light.color = _OtherLightColors[index].rgb;
float3 ray = _OtherLightPositions[index].xyz - surfaceWS.position;
light.direction = normalize(ray);
light.attenuation = 1.0;
return light;
}
在GetLighting
中添加其它光的循环:
float3 GetLighting (Surface surfaceWS, BRDF brdf, GI gi)
{
ShadowData shadowData = GetShadowData(surfaceWS);
shadowData.shadowMask = gi.shadowMask;
float3 color = IndirectBRDF(surfaceWS, brdf, gi.diffuse, gi.specular);
for (int i = 0; i < GetDirectionalLightCount(); i++)
{
Light light = GetDirectionalLight(i, surfaceWS, shadowData);
color += GetLighting(surfaceWS, brdf, light);
}
for (int j = 0; j < GetOtherLightCount(); j++)
{
Light light = GetOtherLight(j, surfaceWS, shadowData);
color += GetLighting(surfaceWS, brdf, light);
}
return color;
}
1.4 基于距离的衰减
这里我们进行尝试进行平方衰减,,其中是强度,是距离:
float distanceSqr = max(dot(ray, ray), 0.00001);
light.attenuation = 1.0 / distanceSqr;
1.5 灯光范围
灯光衰减到一定距离就很难察觉,这时可直接将衰减置为0,也就是说我们需要一个最大可影响范围,但在到达范围时不能立即设置为0。Unity的URP使用来得到范围衰减,其中是灯光的范围(球半径)。
我们在灯光位置的第四个组件处存储灯光范围:
void SetupPointLight (int index, ref VisibleLight visibleLight)
{
otherLightColors[index] = visibleLight.finalColor;
Vector4 position = visibleLight.localToWorldMatrix.GetColumn(3);
position.w =
1f / Mathf.Max(visibleLight.range * visibleLight.range, 0.00001f);
otherLightPositions[index] = position;
}
在GetOtherLight
中重新计算衰减:
float distanceSqr = max(dot(ray, ray), 0.00001);
float rangeAttenuation = Square(
saturate(1.0 - Square(distanceSqr * _OtherLightPositions[index].w))
);
light.attenuation = rangeAttenuation / distanceSqr;
2 聚光
2.1 方向
聚光除位置等外,还需要方向:
static int
otherLightCountId = Shader.PropertyToID("_OtherLightCount"),
otherLightColorsId = Shader.PropertyToID("_OtherLightColors"),
otherLightPositionsId = Shader.PropertyToID("_OtherLightPositions"),
otherLightDirectionsId = Shader.PropertyToID("_OtherLightDirections");
static Vector4[]
otherLightColors = new Vector4[maxOtherLightCount],
otherLightPositions = new Vector4[maxOtherLightCount],
otherLightDirections = new Vector4[maxOtherLightCount];
我们需要将相应的数据送往GPU。
创建一个SetupSpotLight
方法,灯光的方向由局部到世界矩阵的第三列取反得到:
void SetupSpotLight (int index, ref VisibleLight visibleLight)
{
otherLightColors[index] = visibleLight.finalColor;
Vector4 position = visibleLight.localToWorldMatrix.GetColumn(3);
position.w =
1f / Mathf.Max(visibleLight.range * visibleLight.range, 0.00001f);
otherLightPositions[index] = position;
otherLightDirections[index] =
-visibleLight.localToWorldMatrix.GetColumn(2);
}
在SetupLights
的循环中加入聚光支持:
case LightType.Spot:
if (otherLightCount < maxOtherLightCount)
{
SetupSpotLight(otherLightCount++, ref visibleLight);
}
break;
shader方面,加入新数据:
float4 _OtherLightDirections[MAX_OTHER_LIGHT_COUNT];
衰减方面,我们暂时先用聚光方向和光源到片元方向的点积来模拟:
float spotAttenuation =
saturate(dot(_OtherLightDirections[index].xyz, light.direction));
light.attenuation = spotAttenuation * rangeAttenuation / distanceSqr;
2.2 聚光角
锥形光,外角控制范围,内角来控制灯光衰减的开始角度。URP在使用saturate前先缩放点积并加上一些东西,然后对结果开方,,其中是点积,,,和是内角和外角。
我们可以提前计算好和,先声明变量:
static int
otherLightCountId = Shader.PropertyToID("_OtherLightCount"),
otherLightColorsId = Shader.PropertyToID("_OtherLightColors"),
otherLightPositionsId = Shader.PropertyToID("_OtherLightPositions"),
otherLightDirectionsId = Shader.PropertyToID("_OtherLightDirections"),
otherLightSpotAnglesId = Shader.PropertyToID("_OtherLightSpotAngles");
static Vector4[]
otherLightColors = new Vector4[maxOtherLightCount],
otherLightPositions = new Vector4[maxOtherLightCount],
otherLightDirections = new Vector4[maxOtherLightCount],
otherLightSpotAngles = new Vector4[maxOtherLightCount];
在SetupSpotLight
中,外角可由VisibleLight.spotAngle
获得,而内角可由VisibleLight.light.innerSpotAngle
获得:
void SetupSpotLight (int index, ref VisibleLight visibleLight)
{
…
Light light = visibleLight.light;
float innerCos = Mathf.Cos(Mathf.Deg2Rad * 0.5f * light.innerSpotAngle);
float outerCos = Mathf.Cos(Mathf.Deg2Rad * 0.5f * visibleLight.spotAngle);
float angleRangeInv = 1f / Mathf.Max(innerCos - outerCos, 0.001f);
otherLightSpots[index] = new Vector4(
angleRangeInv, -outerCos * angleRangeInv
);
}
shader层面,我们应用等式得到聚光衰减:
float4 spotAngles = _OtherLightSpotAngles[index];
float spotAttenuation = Square(
saturate(dot(_OtherLightDirections[index].xyz, light.direction) *
spotAngles.x + spotAngles.y)
);
light.attenuation = spotAttenuation * rangeAttenuation / distanceSqr;
为确保点光不会被聚光衰减影响,我们将聚光角分别设置为0和1:
void SetupPointLight (int index, ref VisibleLight visibleLight)
{
…
otherLightSpotAngles[index] = new Vector4(0f, 1f);
}
2.3 配置内角
Unity默认的灯光组件不能配置聚光内角,为此,我们可以自定义灯光编辑器,新的类需继承LightEditor
。加上[CanEditMultipleObjects]
确保可修改多个灯光。加上[CustomEditorForRenderPipeline]
属性,第一个参数是灯光的类型,第二个参数必须是自定义RP资产的类型:
using UnityEngine;
using UnityEditor;
[CanEditMultipleObjects]
[CustomEditorForRenderPipeline(typeof(Light), typeof(CustomRenderPipelineAsset))]
public class CustomLightEditor : LightEditor {}
然后重写OnInspectorGUI
方法,先调用默认的GUI:
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
}
我们可通过settings
来获得相应的属性,如果只是聚光灯我们就调整内角和外角:
base.OnInspectorGUI();
if (
!settings.lightType.hasMultipleDifferentValues &&
(LightType)settings.lightType.enumValueIndex == LightType.Spot
)
{
settings.DrawInnerAndOuterSpotAngle();
settings.ApplyModifiedProperties();
}
3 烘培光和阴影
3.1 完全烘培
烘培点光和聚光的话会发现比实时光亮不少,这是因为Unity默认使用的光衰减不正确。
3.2 灯光代理
我们可以让Unity使用不同的光衰减值,在Unity于编辑器内执行lightmapping前为本该调用的方法提供一个代理值,为此,我们将CustomRenderPipeline
变为一个partial类,构造方法中调用编辑器专属的InitializeForEditor
方法:
public partial class CustomRenderPipeline : RenderPipeline
{
…
public CustomRenderPipeline (
bool useDynamicBatching, bool useGPUInstancing, bool useSRPBatcher,
ShadowSettings shadowSettings
)
{
…
InitializeForEditor();
}
…
}
定义另一个编辑器版本的partial类:
using Unity.Collections;
using UnityEngine;
using UnityEngine.Experimental.GlobalIllumination;
using LightType = UnityEngine.LightType;
public partial class CustomRenderPipeline
{
partial void InitializeForEditor ();
}
我们重写Lightmapper初始化灯光数据的方法,提供一个方法代理,将来自输入Light
数组的数据传输到NativeArray
输出中,代理类型为Lightmapping.RequestLightsDelegate
,这里我们使用lambda表达式来定义方法:
partial void InitializeForEditor ();
#if UNITY_EDITOR
static Lightmapping.RequestLightsDelegate lightsDelegate =
(Light[] lights, NativeArray output) => {};
#endif
我们需要为每个灯光配置LightDataGI
结构体,并添加到输出中,对每种灯光类型,我们进行不同的操作,默认情况下不烘培光:
static Lightmapping.RequestLightsDelegate lightsDelegate =
(Light[] lights, NativeArray output) => {
var lightData = new LightDataGI();
for (int i = 0; i < lights.Length; i++)
{
Light light = lights[i];
switch (light.type)
{
default:
lightData.InitNoBake(light.GetInstanceID());
break;
}
output[i] = lightData;
}
};
对每种类型的光,我们调用LightmapperUtils.Extract
方法获得灯光结构体数据,然后调用灯光数据的Init
方法初始化:
switch (light.type)
{
case LightType.Directional:
var directionalLight = new DirectionalLight();
LightmapperUtils.Extract(light, ref directionalLight);
lightData.Init(ref directionalLight);
break;
case LightType.Point:
var pointLight = new PointLight();
LightmapperUtils.Extract(light, ref pointLight);
lightData.Init(ref pointLight);
break;
case LightType.Spot:
var spotLight = new SpotLight();
LightmapperUtils.Extract(light, ref spotLight);
lightData.Init(ref spotLight);
break;
case LightType.Area:
var rectangleLight = new RectangleLight();
LightmapperUtils.Extract(light, ref rectangleLight);
rectangleLight.mode = LightMode.Baked;
lightData.Init(ref rectangleLight);
break;
default:
lightData.InitNoBake(light.GetInstanceID());
break;
}
最后,我们修改衰减类型:
lightData.falloff = FalloffType.InverseSquared;
output[i] = lightData;
在InitializeForForEditor
中我们设置灯光代理:
partial void InitializeForEditor ();
#if UNITY_EDITOR
partial void InitializeForEditor ()
{
Lightmapping.SetDelegate(lightsDelegate);
}
我们还需要重写Dispose
方法:
protected override void Dispose (bool disposing)
{
base.Dispose(disposing);
Lightmapping.ResetDelegate();
}
3.3 阴影遮罩
我们添加ReserveOtherShadow
方法,目前只支持阴影这招模式,且只考虑配置阴影强度和遮罩通道:
public Vector4 ReserveOtherShadows (Light light, int visibleLightIndex) {
if (light.shadows != LightShadows.None && light.shadowStrength > 0f)
{
LightBakingOutput lightBaking = light.bakingOutput;
if (
lightBaking.lightmapBakeType == LightmapBakeType.Mixed &&
lightBaking.mixedLightingMode == MixedLightingMode.Shadowmask
)
{
useShadowMask = true;
return new Vector4(
light.shadowStrength, 0f, 0f,
lightBaking.occlusionMaskChannel
);
}
}
return new Vector4(0f, 0f, 0f, -1f);
}
在SetupPointLight
和SetupSpotLight
中配置数据:
void SetupPointLight (int index, ref VisibleLight visibleLight)
{
…
Light light = visibleLight.light;
otherLightShadowData[index] = shadows.ReserveOtherShadows(light, index);
}
void SetupSpotLight (int index, ref VisibleLight visibleLight)
{
…
otherLightShadowData[index] = shadows.ReserveOtherShadows(light, index);
}
shader层面,在Shadows.hlsl
中创建OtherShadowData
结构体和GetOtherShadowAttenuation
方法,目前先使用和平行光阴影相同的处理过程:
struct OtherShadowData
{
float strength;
int shadowMaskChannel;
};
float GetOtherShadowAttenuation (
OtherShadowData other, ShadowData global, Surface surfaceWS
)
{
#if !defined(_RECEIVE_SHADOWS)
return 1.0;
#endif
float shadow;
if (other.strength > 0.0)
{
shadow = GetBakedShadow(
global.shadowMask, other.shadowMaskChannel, other.strength
);
}
else
{
shadow = 1.0;
}
return shadow;
}
Light.hlsl
中,加入GetOtherShadowData
方法,并在GetOtherLight
中配置:
CBUFFER_START(_CustomLight)
…
float4 _OtherLightShadowData[MAX_OTHER_LIGHT_COUNT];
CBUFFER_END
…
OtherShadowData GetOtherShadowData (int lightIndex)
{
OtherShadowData data;
data.strength = _OtherLightShadowData[lightIndex].x;
data.shadowMaskChannel = _OtherLightShadowData[lightIndex].w;
return data;
}
Light GetOtherLight (int index, Surface surfaceWS, ShadowData shadowData)
{
…
OtherShadowData otherShadowData = GetOtherShadowData(index);
light.attenuation =
GetOtherShadowAttenuation(otherShadowData, shadowData, surfaceWS) *
spotAttenuation * rangeAttenuation / distanceSqr;
return light;
}
4 逐物体灯光
目前的光照计算都是逐片元的,这对于平行光来说可以接受,但对于其它类型的灯光来说就消耗太大了,毕竟数量很多,而且大部分片元不会受某一光源影响。因此,我们需要减少哪些逐片元计算的光源数量,最简单的方法是使用Unity的逐物体灯光索引。
Unity决定哪些灯光影响每个物体,然后将信息送往GPU,这样对于每个物体我们就只用考虑几个光源,这对于小物体来说很不错,但对于较大的物体来说,会丢失一些光源影响。
4.1 逐物体灯光数据
在CameraRenderer.DrawVisibleGeometry
中,添加一个布尔参数来指示是否使用逐物体灯光模式。如果是则开启PerObejctData.LightData
和PerObjectData.LightIndices
:
void DrawVisibleGeometry (
bool useDynamicBatching, bool useGPUInstancing, bool useLightsPerObject
)
{
PerObjectData lightsPerObjectFlags = useLightsPerObject ?
PerObjectData.LightData | PerObjectData.LightIndices :
PerObjectData.None;
var sortingSettings = new SortingSettings(camera) {
criteria = SortingCriteria.CommonOpaque
};
var drawingSettings = new DrawingSettings(
unlitShaderTagId, sortingSettings
) {
enableDynamicBatching = useDynamicBatching,
enableInstancing = useGPUInstancing,
perObjectData =
PerObjectData.ReflectionProbes |
PerObjectData.Lightmaps | PerObjectData.ShadowMask |
PerObjectData.LightProbe | PerObjectData.OcclusionProbe |
PerObjectData.LightProbeProxyVolume |
PerObjectData.OcclusionProbeProxyVolume |
lightsPerObjectFlags
};
…
}
CameraRenderer.Render
和CustomRenderPipeline
中我们都加上对应的参数。
在CustomRenderPipelineAsset
中我们加上对应的选项:
[SerializeField]
bool
useDynamicBatching = true,
useGPUInstancing = true,
useSRPBatcher = true,
useLightsPerObject = true;
[SerializeField]
ShadowSettings shadows = default;
protected override RenderPipeline CreatePipeline ()
{
return new CustomRenderPipeline(
useDynamicBatching, useGPUInstancing, useSRPBatcher,
useLightsPerObject, shadows
);
}
4.2 优化灯光索引
Unity为每个物体创建一个灯光列表,按照重要性排序,列表中不管可不可见都会包含,并且包含平行光,因此我们要进行优化,只留下那些可见的非平行光。在SetupLights
中进行。在进入循环前,先调用GetLightIndexMap
获得灯光索引表:
NativeArray indexMap = useLightsPerObject ?
cullingResults.GetLightIndexMap(Allocator.Temp) : default;
NativeArray visibleLights = cullingResults.visibleLights;
我们只需要点光和聚光的索引,不要的灯光的索引设为-1,并修改剩余灯光的索引:
for (int i = 0; i < visibleLights.Length; i++)
{
int newIndex = -1;
VisibleLight visibleLight = visibleLights[i];
switch (visibleLight.lightType) {
…
case LightType.Point:
if (otherLightCount < maxOtherLightCount)
{
newIndex = otherLightCount;
SetupPointLight(otherLightCount++, ref visibleLight);
}
break;
case LightType.Spot:
if (otherLightCount < maxOtherLightCount)
{
newIndex = otherLightCount;
SetupSpotLight(otherLightCount++, ref visibleLight);
}
break;
}
if (useLightsPerObject)
{
indexMap[i] = newIndex;
}
}
不可见光的索引也剔除,在第一次循环后进行(从未重新设置的索引处开始):
if (useLightsPerObject)
{
for (; i < indexMap.Length; i++)
{
indexMap[i] = -1;
}
}
完成后,我们要修改索引表,然后删除不再使用的表:
if (useLightsPerObject)
{
for (; i < indexMap.Length; i++)
{
indexMap[i] = -1;
}
cullingResults.SetLightIndexMap(indexMap);
indexMap.Dispose();
}
记得设置对应的关键字:
static string lightsPerObjectKeyword = "_LIGHTS_PER_OBJECT";
…
void SetupLights (bool useLightsPerObject)
{
…
if (useLightsPerObject)
{
for (; i < indexMap.Length; i++)
{
indexMap[i] = -1;
}
cullingResults.SetLightIndexMap(indexMap);
indexMap.Dispose();
Shader.EnableKeyword(lightsPerObjectKeyword);
}
else
{
Shader.DisableKeyword(lightsPerObjectKeyword);
}
…
}
4.3 使用索引
使用multi_compile
:
#pragma multi_compile _ _LIGHTS_PER_OBJECT
在UnityPerDraw
缓冲中定义灯光数据和灯光索引,unity_LightData
的y组件包含灯光数量,unity_LightIndices
的两个vector的每个通道包含一个灯光索引:
real4 unity_WorldTransformParams;
real4 unity_LightData;
real4 unity_LightIndices[2];
在GetLighting
中,如果定义了_LIGHTS_PER_OBJECT
,遍历所有的逐物体光:
#if defined(_LIGHTS_PER_OBJECT)
for (int j = 0; j < min(unity_LightData.y, 8); j++)
{
int lightIndex = unity_LightIndices[j / 4][j % 4];
Light light = GetOtherLight(lightIndex, surfaceWS, shadowData);
color += GetLighting(surfaceWS, brdf, light);
}
#else
for (int j = 0; j < GetOtherLightCount(); j++)
{
Light light = GetOtherLight(j, surfaceWS, shadowData);
color += GetLighting(surfaceWS, brdf, light);
}
#endif