在正向渲染中,最亮的几个光源会进行逐像素计算,较次的后继四个会进行逐顶点计算,省下的则是进行球谐(Spherical Harmonics)计算
逐像素计算的光源的最多数量可以是自己设置 Edit->Project Seting->Quality 默认是4个
决定一个光源是逐像素计算还是其他的,取决于以下几点
1、最亮的直射光(directional light)一定是逐像素光源
2、每个光源的RenderMode设置为Important 时总是逐像素的,设置为Not Important则总是逐顶点或者球谐的 默认是Auto(PS:在我的测试下,设置光源为Important一定会是逐像素的,即使最终数量超过Quality里设置的)
3、如果上面的设置导致像素光数量低于在QualitySeting里设置的数量,那么就按光照亮度的顺序补全像素光
正向渲染的物体需要两个Pass计算光照
一个是BasePass:渲染第一个逐像素直射光,以及(顺便渲染)所有的逐顶点光源和球谐光源。
一个是Additional Pass:除了第一个逐像素直射光,其余每一个逐像素光源都会用这个pass渲染一次,并将最终颜色叠加到basePass渲染出的颜色上。
Pass{
Tags { "LightMode" = "ForwardBase"}
CGPROGRAM
//...
ENDCG
}
Albedo是反射率,光照射过来一部分被吸收,剩余被反射,部分漫反射、部分高光反射
diffuse:
float3 diffuse=albedo*lightColor*(DotClamped(lightDir,i.normal)*0.5+0.5);//半兰伯特
DotClamped 是点乘并将结果限制在01范围内,该函数位于文件 UnityStandardBRDF.cginc
Specular:
float3 specular=_SpecularColor*lightColor*pow(DotClamped(halfDir,i.normal),_Smoothness*100);//Blinn-Phong
能量守恒:
漫反射和高光反射的强度总和肯定是不可能比射入的光的强度还大的,那就不自然了
所以把计算漫反射需要的albedo减去高光。
float3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;
albedo *= 1 - _SpecularColor.rgb;
如果高光颜色是灰调的话上面的方法没问题,但如果是彩色的,颜色显示就会异常,这种情况就应该减去rgb中的最大值
albedo *= 1 - max(_SpecularTint.r, max(_SpecularTint.g, _SpecularTint.b));
Unity已经为我们准备好了实现的函数 函数位于UnityStandardUtils.cginc文件
EnergyConservationBetweenDiffuseAndSpecular ()
float oneMinusReflectivity; albedo=EnergyConservationBetweenDiffuseAndSpecular(albedo,_SpecularColor.rgb,oneMinusReflectivity);
oneMinusReflectivity中的值就是前面*=的那个,因为你可能在其他地方也会用到,所以给你提出来了。
金属工作流 :
材质一般可以分为金属和非金属(介电材质)
镜面高光工作流(specular workflow):金属一般有强力并且彩色的镜面高光,而介电材质往往是只有较弱的单色镜面高光
金属工作流(metallic workflow):金属往往没有albedo,而非金属也没有彩色的SpecularColor,所以在金属和非金属之间过渡时 可以把一个颜色既作为albedo又做为SpecularColor
这两两个工作流Unity都有提供标准的着色器 分别是 Standard(Specular setup) 和 Standard
这里我们举例金属工作流
float3 specularColor = albedo * _Metallic;
float oneMinusReflectivity = 1 - _Metallic;
albedo *= oneMinusReflectivity;
这里是简化的计算,现实中纯电介质仍然具有一些镜面高光反射,而这里直接为0了,而且没有处理不同的颜色空间(linear&gamma space) 好在Unity同样给我们准备了函数,仍然在UnityStandardUtils.cginc文件内
float3 specularColor; albedo=DiffuseAndSpecularFromMetallic(albedo,_Metallic,specularColor,oneMinusReflectivity);
另外,_Metallic值应该位于伽马空间,在线性空间渲染时Unity不会自动对伽马值进行伽马校正 所以用[Gamma] 提醒Unity进行伽马校正。
[Gamma] _Metallic ("Metallic", Range(0, 1)) = 0
基于物理渲染 PBS_BRDF
前面用的是Blinn-Phong光照模型,而现在基于物理渲染的PBS(Physically-Based Shading)成为行业热点,他有更好的效果,所以我们把前面的光照模型改成PBS的。BRDF是指双向反射分布函数,这些算法都比较复杂
(我不会),但是Unity有提供实现的宏 UNITY_BRDF_PBS 这个宏位于UnityStandardBRDF.cginc但我们可以直接引入UnityPBSLighting.cginc ,它同时包含了UnityStandardBRDF.cginc和UnityStandardUtils.cginc
为了确保使用最好的BRDF 要把定位着色器级别高于3.0
#pragma target 3.0
...
UnityLight light;
light.color=_LightColor0.rgb;
light.dir=_WorldSpaceLightPos0.xyz;
light.ndotl=DotClamped(i.normal,light.dir);
UnityIndirect indirectLight;
indirectLight.diffuse=0;
indirectLight.specular=0;
return UNITY_BRDF_PBS(albedo,_SpecularColor,oneMinusReflectivity,_Smoothness,i.normal,viewDir,light,indirectLight);
UnityLight和UnityIndirect是定义在UnityLightingCommon.cginc的结构(UnityPBSLighting.cginc 已经包括了),用于储存光和间接光的信息,因为这里我们是在讲逐像素光的部分,所以间接光我们先当做不存在,后面会补充的。
顶点光源只支持点光源,不支持定方向光源和聚光灯
首先为了看起来整洁,我把代码单独放到了一个cginc文件里 里,自己创建一个cginc拓展名的文本文件,把前面的代码复制进去,然后#include "xxx.cginc" 为了避免重复载入,先判断一下之前有没有include过
#if !defined(ZERONE_LIGHTING_INCLUDED) #define ZERONE_LIGHTING_INCLUDED //...context #end if
其次我把UnityLight 和 UnityIndirect单独写成两个函数,方便后面的修改
UnityLight CreateLight(v2f i){ UnityLight light; light.color=_LightColor0.rgb; light.dir=_WorldSpaceLightPos0.xyz; light.ndotl=DotClamped(i.normal,light.dir); return light; } UnityIndirect CreateIndirectLight(v2f i){ UnityIndirect indirectLight; indirectLight.diffuse=0; indirectLight.specular=0; return indirectLight; }
要使用顶点光源,我们必须在我们的基础渲染通道中添加一个多重编译语句。它只需要一个关键字VERTEXLIGHT_ON。另一个选项根本就是没有关键字。 为了表示没有关键字,我们必须使用_。
Pass{
Tags{"LightMode"="ForwardBase"}
CGPROGRAM
#pragma target 3.0
//为顶点光照添加一个多重编译
#pragma multi_compile _ VERTEXLIGHT_ON
#pragma vertex vert
#pragma fragment frag
ENDCG
}
首先修改顶点函数输出结构,增加顶点光源颜色这一字段,四个光源的颜色计算完最后会加在一起。defined(VERTEXLIGHT_ON)是用于判断是否开启了顶点光照,因为我们后面的AddPass也是同样的输入输出结构以及顶点片片元函数,而Addpass是没有开启顶点光源的,所以用这个预处理来区别。
struct v2f{
float4 pos:POSITION;
float3 worldPos:TEXCOORD0;
float3 normal:TEXCOORD1;
half2 uv:TEXCOORD2;
#if defined(VERTEXLIGHT_ON)
float3 vertexLightColor : TEXCOORD3;
#endif
};
四个顶点光源相关信息
颜色可以通过unity_LightColor[i].rgb , i=1,2,3,4 得到
位置可以通过
unity_4LightPosX0.xyzw
unity_4LightPosY0.xyzw
unity_4LightPosZ0.xyzw
得到四个顶点光源各自位置的XYZ分量
这些数据都来自UnityShaderVariables.cginc 很多Unity提供的数据都来自这,当然不用再单独导入了,前面的文件已经包含了。
四个顶点光源的计算方法是一样的,所以以第一个顶点光源来举例:
把计算过程独立成一个ComputeVertexLightColor()放在顶点函数里
void ComputeVertexLightColor (inout v2f i) {
#if defined(VERTEXLIGHT_ON)
float3 lightPos = float3(unity_4LightPosX0.x, unity_4LightPosY0.x, unity_4LightPosZ0.x);
float3 lightVec = lightPos - i.worldPos;
//点光源方向和衰减的计算方法具体原理看后面的AddPass
float3 lightDir = normalize(lightVec);
float ndotl = DotClamped(i.normal, lightDir);
//float attenuation = 1 / (1 + dot(lightVec, lightVec));
//Unity提供了一个帮助衰减的值unity_4LightAtten0 ,用上它可以有更好的效果
float attenuation = 1 /(1 + dot(lightVec, lightVec) * unity_4LightAtten0.x);
i.vertexLightColor = unity_LightColor[0].rgb * ndotl * attenuation;
#endif
}
是的,Unity又帮我们把这个计算过程提供好了,所以你不用把这些代码再复制三次了 所以四个顶点光照颜色的叠加值可以直接用Shade4PointLights()得到
void ComputeVertexLightColor (inout v2f i) { #if defined(VERTEXLIGHT_ON) i.vertexLightColor = Shade4PointLights( unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0, unity_LightColor[0].rgb, unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb, unity_4LightAtten0, i.worldPos, i.normal ); #endif }
然后就是把顶点光照的颜色结果传递到片元函数中,还记得前面的间接光吗,我们把颜色归到间接光的diffuse中去
UnityIndirect CreateIndirectLight(v2f i){
UnityIndirect indirectLight;
indirectLight.specular=0;
indirectLight.diffuse=0;
#if defined(VERTEXLIGHT_ON)
indirectLight.diffuse = i.vertexLightColor;
#endif
return indirectLight;
}
把逐像素光源数量设为0 再把点光源设为not import就可以看到效果了 没效果可能是光的强度太弱
球面谐波我个人理解就是将物体表面和一个球面的各个部分互相对应,将球面分成许多层细分层,通过不同细分层的不同强度的叠加,来模拟一个近似表面,用的细分层的细分层次越大,细节也就越好,有点像噪声里的布朗分形运动,通过不同比例噪声的不同强度的叠加,来实现细节,然后光照就可以直接附加在各个细分层上面,通过计算得到一个函数,然后通过输入法线就可以得到该点的近似光源颜色了。
仅个人理解,不一定对,因为各种数学公式我看不懂,只能看懂几张图片ಠ_ಠ 文末会放我看的几篇文章
每个细分层的每个部分部分可以用一个函数表达,所有的细分表达函数加载一起,就构成了最终的函数
Unity只用了细分的前三层,毕竟层数越多虽然效果越好,但性能消耗也越大,用球谐光源本来就是为了降低消耗
第一层就是整个球面 可以看成一个常值 一个函数
第二层细分xyz三个方向 分为3个函数
第三层是两个法线坐标的乘积,细分更加精确,这里有五个函数
系数Unity会帮我们算好,所以加在一起就是这样:
可以返回这个值来看每个子函数所代表的部分
float t = i.normal.x;
return t > 0 ? t : float4(1, 0, 0, 1) * -t;
通过这九个部分的组合叠加,就可以模拟各种光照情况的近似了。
原理大概就这样,用法就简单地一批了,毕竟Unity已经为我们准备好函数了
indirectLight.diffuse += max(0, ShadeSH9(float4(i.normal, 1)));
区分是basepass和addpass,加个define
#define FORWARD_BASE_PASS
#if defined(FORWARD_BASE_PASS)
indirectLight.diffuse += max(0, ShadeSH9(float4(i.normal, 1)));
#endif
绿色是顶点光源,粉色是球谐计算的光源,因为包括了天空盒的一点光源,所以中间有一点淡蓝
ShadeSH9是这样的,可以自己对着看一遍
// normal should be normalized, w=1.0 half3 SHEvalLinearL0L1 (half4 normal) { half3 x; // Linear (L1) + constant (L0) polynomial terms x.r = dot(unity_SHAr,normal); x.g = dot(unity_SHAg,normal); x.b = dot(unity_SHAb,normal); return x; } // normal should be normalized, w=1.0 half3 SHEvalLinearL2 (half4 normal) { half3 x1, x2; // 4 of the quadratic (L2) polynomials half4 vB = normal.xyzz * normal.yzzx; x1.r = dot(unity_SHBr,vB); x1.g = dot(unity_SHBg,vB); x1.b = dot(unity_SHBb,vB); // Final (5th) quadratic (L2) polynomial half vC = normal.x * normal.x - normal.y * normal.y; x2 = unity_SHC.rgb * vC; return x1 + x2; } // normal should be normalized, w=1.0 // output in active color space half3 ShadeSH9 (half4 normal) { // Linear + constant polynomial terms half3 res = SHEvalLinearL0L1(normal); // Quadratic polynomials res += SHEvalLinearL2(normal); if (IsGammaSpace()) res = LinearToGammaSpace(res); return res; }
Pass{
Tags{"LightMode"="ForwardAdd"}
Blend one one //add是混合到base上,而不是覆盖base 所以开启混合模式
ZWrite off //base已经写入过深度了,所以add这里没有必要再写一次
CGPROGRAM
#include "ZeroneLight.cginc"
#pragma target 3.0
#pragma vertex vert
#pragma fragment frag
ENDCG
}
AddPass用于渲染剩下的像素光。你可以在场景中放1个直射光和4个点光源(光源要能照到物体,且设置成auto ),然后打开frameDebuger 因为我这里设置的最大像素光源是4 所以即使有5个光源,unity把较弱的一个当成了顶点光源(如果多一个可能是阴影,关闭阴影)
如果在addpass中套用basepass中的顶点片元函数,你会发现有光照有问题,那是因为在basepass中的光源往往是最亮的直射光,直射光光源强度通常是恒定的,但像点光源、聚光灯之类的光源,他不可能照亮整个游戏空间,它的强度会随着光源的中心距离不断增大而衰减,而且其光源方向也不可能像直射光一样,永远都是固定方向,它是向外辐散的。
首先点光源的方向应该与点光源的位置减去片元的位置
然后是衰减值的计算
想象一个场景中的一个点,在各个点上我们发射一次光子爆炸。这些光子均匀分布,向场景的各个方向直线运动。这些光子脉冲在所有方向上进行移动。随着时间的推移,光子脉冲进一步远离该点。由于这些光子都以相同的速度进行运动,这些光子在场景中会分布在一个以光源位置为中心的球面上。这个球的半径随着光子的移动而增加。随着球体的生长,其表面也随之增长。但是这个表面总是包含相同数量的光子。因此,随着球体的半径增大,单位表面积上光子的密度会减少。。这决定了观察到的光的亮度。半径r的球体的表面积等于。为了确定光子密度,我们可以除以球体的表面积。我们可以忽略常数“”,因为我们可以假设它被纳入光的强度之中。这导致了“”的衰减因子,其中“d”是到光源的距离。
所以点光源应该这样计算
float attenuation=1;
#ifdef POINT
float3 lightVec = _WorldSpaceLightPos0.xyz - i.worldPos;
light.dir=normalize(lightVec);
//attenuation = 1 / (dot(lightVec, lightVec));
//底数加1效果更好,点光源贴近靠近物体时值不会因为趋近无穷大而太亮
attenuation = 1 / (1+dot(lightVec, lightVec));
#endif
light.color = _LightColor0.rgb * attenuation;
自己写的还是还是有点瑕疵,就是在光照的边缘(Range)时attenuation不会刚好变成 0 就会有明显的裂缝
Unity在此基础上进行了更进一步的优化。Unity通过距离的平方值来对衰减纹理进行采样,将采样结果作为衰减的值。这样做是为了确保光源在光照范围边缘上更早的衰减为0。不使用这一方法,你仍然能够在物体进出光照影响范围时,感受到轻微的光照跳跃效果。
Unity在AutoLight.cginc里提供了宏UNITY_LIGHT_ATTENUATION()来计算衰减值 第二个参数和阴影有关,暂时忽略,浮点数attenuation是在宏里创建的,所以不用先创建。
UNITY_LIGHT_ATTENUATION()不仅能处理点光源,也包含聚光灯或其他类型光源,具体可以去AutoLight.cginc里看
UnityLight CreateLight(v2f i){ UnityLight light; light.color=_LightColor0.rgb; #ifdef POINT light.dir=normalize(_WorldSpaceLightPos0.xyz - i.worldPos); #else light.dir=_WorldSpaceLightPos0.xyz; #endif UNITY_LIGHT_ATTENUATION(attenuation, 0, i.worldPos); light.color = _LightColor0.rgb * attenuation; light.ndotl=DotClamped(i.normal,light.dir); return light; }
最后在AddPass define POINT区别,注意要define在#include"my.cginc"前,不然没用,shader只会从前向后看。
CGPROGRAM
#pragma target 3.0
#define POINT
#pragma vertex vert
#pragma fragment frag
#include "xxx.cginc"
ENDCG
也可以用multi_compile直接生成2个shader的变体,一个用于方向光一个用于电光 来代替#define POINT
#pragma multi_compile DIRECTIONAL POINT
光源方向和点光源一样,所以
#if defined(POINT)||defined(SPOT)
light.dir=normalize(_WorldSpaceLightPos0.xyz - i.worldPos);
#else
light.dir=_WorldSpaceLightPos0.xyz;
#endif
衰减方法开始与点光源的衰减方法相同。转换到光照空间,然后计算衰减因子。然后,对于位于原点后面的所有点,将衰减强制为零。这将光的范围限制在位于聚光灯前的物体。
然后,光照空间中的X和Y坐标用作UV坐标以对纹理进行采样。这种纹理用于对光进行掩码。纹理只是一个具有模糊边缘的圆。这产生光的圆柱体。为了将其变成锥形,到光照空间的转换实际上是透视变换,并且使用齐次坐标。
UNITY_LIGHT_ATTENUATION里同样有他的实现
下面的图是为齐次前、以光源为中心的光照空间 齐次后应该是圆柱体,tex2D采样的是齐次后的xy
tex2D采样的图是Unity提供的一个模糊的圆 你可以自己的纹理代替,用于这个的纹理叫做Cookie
cookie的透明度通道用于掩码光线。其他通道无关紧要。下面是一个纹理示例,其中所有四个通道都被设置为相同的值。
图片格式设为Cookie,就可以直接用了
点光源和平行光也可以用Cookie但是点光源的Cookie得是立方体纹理,UNITY_LIGHT_ATTENUATION都提供了对应的实现,只需要define
#if defined(POINT)||defined(SPOT)||defined(POINT_COOKIE)
light.dir=normalize(_WorldSpaceLightPos0.xyz - i.worldPos);
#else
light.dir=_WorldSpaceLightPos0.xyz;
#endif
然后实现多个变种
#pragma multi_compile DIRECTIONAL POINT SPOT POINT_COOKIE DIRECTIONAL_COOKIE
五个看起来是不是很麻烦所以可以用#pragma multi_compile_fwdadd替代
PS:#pragma multi_compile_fwdadd_fullshadows 可以让addpass的光源也产生阴影,默认是不产生的。
shader
Shader "Custom/ZeroneLightShader" {
Properties {
_Color("Color",Color)=(1,1,1,1)
_MainTex("Texture",2D)="white"{}
//_SpecularColor("Specular",Color)=(1,1,1,1)
_Metallic("Metallic",Range(0,1))=.5
_Smoothness("_Smoothness",Range(0,1))=0.5
}
SubShader {
Pass{
Tags{"LightMode"="ForwardBase"}
CGPROGRAM
#pragma target 3.0
//为顶点光照添加一个多重编译
#pragma multi_compile _ VERTEXLIGHT_ON
#define FORWARD_BASE_PASS
#include "ZeroneLight.cginc"
#pragma vertex vert
#pragma fragment frag
ENDCG
}
Pass{
Tags{"LightMode"="ForwardAdd"}
Blend one one //add是混合到base上,而不是覆盖base 所以开启混合模式
ZWrite off //base已经写入过深度了,所以add这里没有必要再写一次
CGPROGRAM
#pragma target 3.0
//#define POINT
//#pragma multi_compile DIRECTIONAL POINT SPOT POINT_COOKIE DIRECTIONAL_COOKIE
#pragma multi_compile_fwdadd;
#pragma vertex vert
#pragma fragment frag
#include "ZeroneLight.cginc"
ENDCG
}
}
FallBack "Diffuse"
}
cginc
//防止重复定义
#if !defined(ZERONE_LIGHTING_INCLUDED)
#define ZERONE_LIGHTING_INCLUDED
#include "UnityCG.cginc"
#include "UnityPBSLighting.cginc"
#include "AutoLight.cginc"
struct a2v{
float4 vertex:POSITION;
float3 normal:NORMAL;
half2 uv:TEXCOORD0;
};
struct v2f{
float4 pos:POSITION;
float3 worldPos:TEXCOORD0;
float3 normal:TEXCOORD1;
half2 uv:TEXCOORD2;
#if defined(VERTEXLIGHT_ON)
float3 vertexLightColor : TEXCOORD3;
#endif
};
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _Color;
//float4 _SpecularColor;
float _Smoothness;
float _Metallic;
UnityLight CreateLight(v2f i){
UnityLight light;
light.color=_LightColor0.rgb;
#if defined(POINT)||defined(SPOT)||defined(POINT_COOKIE)
light.dir=normalize(_WorldSpaceLightPos0.xyz - i.worldPos);
#else
light.dir=_WorldSpaceLightPos0.xyz;
#endif
UNITY_LIGHT_ATTENUATION(attenuation, 0, i.worldPos);
light.color = _LightColor0.rgb * attenuation;
light.ndotl=DotClamped(i.normal,light.dir);
return light;
}
UnityIndirect CreateIndirectLight(v2f i){
UnityIndirect indirectLight;
indirectLight.specular=0;
indirectLight.diffuse=0;
#if defined(VERTEXLIGHT_ON)
indirectLight.diffuse += i.vertexLightColor;
#endif
#if defined(FORWARD_BASE_PASS)
indirectLight.diffuse += max(0, ShadeSH9(float4(i.normal, 1)));
#endif
return indirectLight;
}
void ComputeVertexLightColor (inout v2f i) {
#if defined(VERTEXLIGHT_ON)
i.vertexLightColor = Shade4PointLights(
unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,
unity_LightColor[0].rgb, unity_LightColor[1].rgb,
unity_LightColor[2].rgb, unity_LightColor[3].rgb,
unity_4LightAtten0, i.worldPos, i.normal
);
#endif
}
v2f vert(a2v v){
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
o.uv=TRANSFORM_TEX(v.uv,_MainTex);
o.normal=UnityObjectToWorldNormal(v.normal);
o.worldPos=mul(unity_ObjectToWorld,v.vertex);
ComputeVertexLightColor(o);
return o;
}
fixed4 frag(v2f i):SV_Target{
//单位向量插值后的结果略小于1 所以想要标准单位向量的话的话,要再对i.normal做标准化处理
i.normal=normalize(i.normal);
float3 lightDir=_WorldSpaceLightPos0.xyz;
float3 lightColor=_LightColor0.rgb;
float3 albedo=tex2D(_MainTex,i.uv)*_Color.rgb;
float oneMinusReflectivity;
//albedo=EnergyConservationBetweenDiffuseAndSpecular(albedo,_SpecularColor.rgb,oneMinusReflectivity);
float3 specularColor;
albedo=DiffuseAndSpecularFromMetallic(albedo,_Metallic,specularColor,oneMinusReflectivity);
//float3 diffuse=albedo*lightColor*(DotClamped(lightDir,i.normal)*0.5+0.5);
float3 viewDir=normalize(_WorldSpaceCameraPos-i.worldPos);
//float3 halfDir=normalize(lightDir+viewDir);
//float3 specular=_SpecularColor*lightColor*pow(DotClamped(halfDir,i.normal),_Smoothness*100);
return UNITY_BRDF_PBS(albedo,specularColor,oneMinusReflectivity,_Smoothness,i.normal,viewDir,CreateLight(i),CreateIndirectLight(i));
}
#endif
https://docs.unity3d.com/Manual/RenderTech-ForwardRendering.html
http://www.cnblogs.com/effulgent/archive/2008/04/15/1153799.html
原文:
https://catlikecoding.com/unity/tutorials/rendering/part-4/
https://catlikecoding.com/unity/tutorials/rendering/part-5/
翻译:
http://gad.qq.com/program/translateview/7173932
https://www.jianshu.com/p/c1a9a5d27765