Skip to content

PointLight

zilch edited this page Aug 18, 2021 · 3 revisions

前言

前置系列

本篇为SRP自定义渲染管线的点光源实现。先放个最终效果图

1. 点灯源(Point Light)

点光源向四面八方投射光亮,其辐射范围为一个球形,光照亮度随着辐射距离增加而减弱。

引用Unity文档中的一张图:

因此我们可以用最大辐射半径,光源颜色、光照强度三个变量来描述一个点光源。

在Shader中计算一个点所受的光照强度时,只需要计算其与点光源的距离,进行衰减即可。

2. 光强度距离衰减公式

不同的引擎,实现的点光源强度公式各有不同,但大致是遵循与距离平方呈反比的一个关系。为什么距离平方反比呢?可以参考如下这篇文章:

基于物理的光照推演

我们假设一个点光源的有效半径为r,空间中的一个点与光源距离为d。

令$x=\frac{d}{r}$

那么最直接的一个光强衰减公式为:

$L_{atten}=\frac{1}{x^2}$

用函数绘图工具把曲线画出来,是这样的:

其中横坐标x是距离,纵坐标y是衰减因子。

可以看出当x为1时,y并不为0。因此当物体正好处于光源的有效半径边界处时,会产生光强突变(突然从有变为无)。

为了能平滑的使光强度在有效半径内衰减到0,不同的引擎有不同的实现。我看了一下URP中的实现,其对Mobile和Switch平台使用了一套公式,而对其余的平台使用了另一套公式,下面分别比较一下。

首先对于Mobile和Switch平台,URP使用的衰减公式如下:

$$ \begin{aligned} &smooth=saturate(\frac{r^2-x^2}{r^2-(0.8r)^2}) \ &L_{atten}=\frac{smooth}{x^2} \end{aligned}

$$

以上公式对应的衰减曲线如下:

可以看出,在x处于[0,0.8]范围内时,曲线遵循1/x^2的衰减公式。而在[0.8,1]范围内时,使用smooth参数使其迅速衰减到0。这种方式有一个缺点就是在0.8r处,衰减速度会突增。

再看URP在其余平台的衰减公式:

$$ \begin{aligned}

&smooth=saturate(1 - (\frac{x}{r})^4)^2 \ &L_{atten}=\frac{smooth}{x^2}

\end{aligned}

$$

衰减曲线如下:

该公式牺牲了与距离平方呈反比的精确性,但是保证了衰减的导数连续性。误差在距离光源越远的时候会越大,近处就还好。不过图形学上反正是看起来对那就是对的。所以此处我们准备采用这个公式来实现点光源强度的衰减。

3. SRP实现

3.1 PerObject灯光

首先考虑这么一个场景:

假设一个大场景中分布着100盏点光源,很明显并不是所有物体都会受100盏光源的影响。通常来说,从性能角度考虑,我们会根据光源距离物体的距离以及光源的重要程度进行排序,筛选出对物体贡献程度最高的几盏光源,来进行光照计算。 因此相比较平行光,我们在这里需要做如下额外的内容:

  • 针对每个物体,计算出有哪些影响它的点光源要参与Shader阶段计算。
  • 把这部分灯光数据传递到Shader中。

很可惜(也可能是很幸运),这部分功能目前Unity是封装在引擎中的,我们无法自定义,只能按照如下规则,来获取引擎计算后的结果:

  • DrawingSettings.perObjectData中,开启LightDataLightIndices
  • 定义如下的CBUFFER,其中unity_LightData.y中记录了影响物体的光源数量,unity_LightIndices每个分量记录了一盏灯光索引,累计最多8盏灯光。这个索引即是cullingResults.visibleLights的下标。
CBUFFER_START(UnityPerDraw)

real4 unity_LightData;
real4 unity_LightIndices[2];

CBUFFER_END

注意: 当我们在LightInput.hlsl中定义UnityPerDraw的时候,会跟UnityShaderVariables.cginc中的定义冲突,那是因为我们之前的Shader,引进了UnityCG.cginc,而UnityCG.cginc又引用了UnityShaderVariables.cgic。 所以从现在开始,需要把UnityCG.cginc从Shader引用中移除,并自己补充缺失的相关函数。

3.2 Shader变量定义

首先写一个CommonInput.hlsl,在里面定义UnityCG.cginc移除后,相关缺失的内容:

float3 _WorldSpaceCameraPos;
float4x4 unity_MatrixVP;

#define TRANSFORM_TEX(tex, name) ((tex.xy) * name##_ST.xy + name##_ST.zw)

///UnityPerDraw是Unity引起内置约定好的一个CBUFFER,里面的变量名都是约定好的,不能修改
CBUFFER_START(UnityPerDraw)
float4x4 unity_ObjectToWorld;
float4x4 unity_WorldToObject;
float4 unity_LODFade; // x is the fade value ranging within [0,1]. y is x quantized into 16 levels
float4 unity_WorldTransformParams; // w is usually 1.0, or -1.0 for odd-negative scale transforms

float4 unity_LightData;
float4 unity_LightIndices[2];

CBUFFER_END

_WorldSpaceCameraPosunity_MatrixVP都是引擎会自动赋值的,我们这只要定义就好了。

UnityPerDraw和里面的变量名字,也都是引擎约定好的,我们只需要定义。

再写一个SpaceTransform.hlsl,里面把坐标变换的一些函数也补上:

float4 ObjectToHClipPosition(float3 positionOS){
    float4 positionWS = mul(unity_ObjectToWorld,float4(positionOS,1));
    return mul(unity_MatrixVP,positionWS);
}
#define UnityObjectToClipPos ObjectToHClipPosition

这样兼容工作就完成了。

然后在LightInput.hlsl中定义:

//场景同时生效的最大非主光源数量
#define MAX_OTHER_VISIBLE_LIGHT_COUNT  32 

//非主光源的位置和范围,xyz代表位置,w代表范围
float4 _XOtherLightPositionAndRanges[MAX_OTHER_VISIBLE_LIGHT_COUNT];
//非主光源的颜色
half4 _XOtherLightColors[MAX_OTHER_VISIBLE_LIGHT_COUNT];

MAX_OTHER_VISIBLE_LIGHT_COUNT是PerCamera的,意思是一个摄像机中同时能生效32盏灯光。 要注意与前面的PerObject最多8盏灯光作好区分。

3.3 传递数据给Shader

在Shader中完成变量定义后,我们需要在C#端的渲染管线中,将相关数据传递进GPU(主要是LightInput.hlsl中新定义的)

主要代码:

private void SetupOtherLightDatas(ref CullingResults cullingResults){
    var visibleLights = cullingResults.visibleLights;
    var lightMapIndex = cullingResults.GetLightIndexMap(Allocator.Temp);
    var otherLightIndex = 0;
    var visibleLightIndex = 0;
    foreach(var l in visibleLights){
        var visibleLight = l;
        switch(visibleLight.lightType){
            case LightType.Directional:
            lightMapIndex[visibleLightIndex] = -1;
            break;
            case LightType.Point:
            lightMapIndex[visibleLightIndex] = otherLightIndex;
            SetPointLightData(otherLightIndex,ref visibleLight);
            otherLightIndex ++;
            break;
            default:
            lightMapIndex[visibleLightIndex] = -1;
            break;
        }
        visibleLightIndex ++;
    }
    for(var i = visibleLightIndex; i < lightMapIndex.Length;i ++){
        lightMapIndex[i] = -1;
    }
    cullingResults.SetLightIndexMap(lightMapIndex);
    Shader.SetGlobalVectorArray(ShaderProperties.OtherLightPositionAndRanges,_otherLightPositionAndRanges);
    Shader.SetGlobalVectorArray(ShaderProperties.OtherLightColors,_otherLightColors);
}

这个函数里面做了两件事情:

  • 过滤出PointLight的数据,并赋值给数组_XOtherLightPositionAndRanges[]_XOtherLightColors[]
  • 修改LightIndexMap

首先看PointLight数据的赋值操作,比较简单:

private void SetPointLightData(int index,ref VisibleLight light){
    Vector4 positionAndRange = light.light.gameObject.transform.position;
    positionAndRange.w = light.range;
    _otherLightPositionAndRanges[index] = positionAndRange;
    _otherLightColors[index] = light.finalColor;
}

两个数组里分别记录了点光源的位置、有效范围以及颜色。

然后看LightIndexMap。 这个概念比较难理解一些,主要是负责了光源索引的再映射。几乎找不到相关的文档说明,我自己的理解大概是如下:

  • 当Unity绘制一个物体的时候,首先会按重要程度排序光源。这里的光源是不分类型(即包括了平行光)以及不管其是否可见的。
  • 从第一个光源开始检查,假设第一个光源的索引为i
  • 然后Unity用i去LightIndexMap中作为key查询,得到value.
  • 如果value是-1,那么表示该光源不起效,跳过,继续往下查询。
  • 否则将value作为最终的索引值,写入到unity_LightIndices的分量中.
  • unity_LightIndices8个分量都写满或者灯光都遍历完成,就完成了unity_LightIndices索引的建立。

LightIndexMap在默认情况下,key和value是相同的。 因为我们只过滤出PointLight,所以需要修改LightIndexMap进行再映射。这样unity_LightIndices的分量,才能对应_XOtherLightPositionAndRanges[]_XOtherLightColors[]的索引。

3.4 修改DrawSettings

ScriptableRenderContext绘制的drawSettings需要为perObjectData开启LightData和LightIndices功能。

drawSetting.perObjectData |= PerObjectData.LightData;
drawSetting.perObjectData |= PerObjectData.LightIndices;

3.5 Shader着色实现

基本思想就是遍历影响该物体的点光源,使用指定的光照模型为其着色(这里使用BlinnPhong模型)。着色公式为:

最终颜色 = 距离衰减因子 * 灯光颜色 * (漫反射项 + 高光项)

关键代码如下:

for(int i = 0; i < lightCount; i ++){
    XOtherLight otherLight = GetOtherLight(i);
    float3 lightPosition = otherLight.positionRange.xyz;
    //range是光源的有效范围
    float range = otherLight.positionRange.w;
    float rangeSqr = range * range;
    float3 lightVector = lightPosition - positionWS;
    float3 lightDir = normalize(lightVector);
    float distanceToLightSqr = dot(lightVector,lightVector);
    //距离衰减系数
    float atten = DistanceAtten(distanceToLightSqr,rangeSqr);
    //高光项
    half3 specular =  BlinnPhongSpecular(viewDir,normal,lightDir,property.shininess) * property.specularColor;
    //漫反射项
    half3 diffuse = LambertDiffuse(normal,lightDir) * property.diffuseColor;
    color += atten * otherLight.color.rgb *  (diffuse + specular) ;
}

4. 最终效果