基于物理渲染-unity实现PBR shader + 多光源 + 半透明物体和阴影

一,PBR光照

  • pbr贴图知识里学习了次世代的两种流程,金属度粗糙度流程和高光度光滑度流程;鉴于我工作用到的是金属度粗糙度流程,所以本篇以金属度粗糙度流程来展开。
  • PBR光照分为直接光照和间接光照两部分,每部分又包含漫反射和镜面反射。

1.1直接光照

  • 先回顾下BRDF方程:
    在这里插入图片描述

1.1.1直接光照漫反射

上边方程中的第一项就是漫反射项,拆开来其实就是 漫 反 射 比 例 ∗ 表 面 颜 色 ∗ 光 源 颜 色 ∗ 光 源 分 量 P I \frac{漫反射比例*表面颜色*光源颜色*光源分量}{PI} PI。即:

float3 diffColor = kd * Albedo * lightColor * ndotl;

注意这个地方,网上有的文章除以π有的没有,搞得非常糊涂,原来是因为unity自己官方的解释所以这里不除以PI,如下:

// HACK: theoretically we should divide diffuseTerm by Pi and not multiply specularTerm!
// BUT 1) that will make shader look significantly darker than Legacy ones
// and 2) on engine side “Non-important” lights have to be divided by Pi too in cases when they are injected into ambient SH

意思是unity为了使shader效果看起来和Legacy差不多,并且避免对间接光照做特殊处理,决定在这里不除以π了,并且为了统一直接光照镜面反射部分需要乘上π。

因此我们的漫反射部分代码就出来了:

//下式是根据物理能量守恒计算的kd系数,F是镜面反射公式的F,但效果不如内置宏好
//float3 kd = (1 - F)*(1 - metallic);  
float3 kd = OneMinusReflectivityFromMetallic(metallic);

float3 Albedo = tex2D(_MainTex, i.uv);
//atten是阴影衰减系数,平行光不衰减都是1,点光射光衰减
float atten = LIGHT_ATTENUATION(i);
//_LightColor0是该pass内置光源颜色
float3 diffColor = kd * Albedo * _LightColor0.rgb * ndotl * atten;

1.1.2直接光照镜面反射

BRDF方程中第二项是镜面反射,其中DFG根据Cook-Torrance BRDF中各个公式计算即可得。

float Distribution(float roughness , float ndoth)
{
    
    
	float lerpSquareRoughness = pow(lerp(0.002, 1, roughness), 2);
	float D = lerpSquareRoughness / (pow((pow(ndoth, 2) * (lerpSquareRoughness - 1) + 1), 2) * UNITY_PI);
	return D;
}
float Geometry(float roughness , float ndotl , float ndotv)
{
    
    
    float k = pow(roughness + 1, 2) / 8;
    float GLeft = ndotl / lerp(ndotl, 1, k);
    float GRight = ndotv / lerp(ndotv, 1, k);
    float G = GLeft * GRight;
    return G;
}
float3 FresnelEquation(float3 F0 , float ldoth)
{
    
    
    float3 F = F0 + (1 - F0) * pow((1.0 - ldoth),5);
    return F;
}
//这里默认一个环境光基数为0.04,unity内置了一个预定义unity_ColorSpaceDielectricSpec,但与0.04有点差距,因此我们使用0.04
//half3 F0 = unity_ColorSpaceDielectricSpec.rgb;
half3 F0 = half3(0.04,0.04,0.04);
F0 = lerp(F0, Albedo, Matelness);
float3 F = FresnelEquation(F0 , ldoth);
float D = Distribution(roughness , ndoth);
float G = Geometry(roughness , ndotl , ndotv);

float3 Specular = F*D*G/(4 * ndotv * ndotl + 0.001);//一定要有这个+0.001,来防止边缘处除以0导致过曝
float3 specColor = Specular * UNITY_PI * _LightColor0.rgb * ndotl * atten;
//直接光照结果
float3 DirectLightResult = diffColor + specColor;

1.2间接光照

  • 间接光照指CubeMap带来的环境光照,间接光照的BRDF方程和直接光相似,只不过其中保留了积分部分:
    在这里插入图片描述

1.2.1间接光照漫反射

第一部分的漫反射项部分的积分看起来复杂(这部分具体解法可以看《 Real-time Rendering》),我们可以用unity内置的球谐光照函数计算,用不同的球谐基底可以得到天空盒或光照探针的数据,对光照进行还原。

float3 ambient = 0.03 * Albedo;//基础的环境光
float3 irradiance = ShadeSH9(float4(worldNormal,1));//内置球谐光照计算相应的采样数据
float3 iblDiffuse = max(float3(0,0,0),irradiance + ambient.rgb);
float3 Flast = F0 + (max(float3(1 ,1, 1)*(1 - Roughness), F0) - F0) * pow(1.0 - ndotv, 5.0);
float3 iblKd = (float3(1.0,1.0,1.0) - Flast) * (1 - Matelness);//间接光漫反射系数
iblDiffuse *= iblKd * Albedo;

1.2.2间接光照镜面反射

  • 公式右边为间接光照镜面反射部分,可以进行如下简化:
    L o ( p , ω o ) = ∫ Ω k s D F G 4 ( ω o ⋅ n ) ( ω i ⋅ n ) L i ( p , ω i ) n ⋅ ω i d ω i = ∫ Ω f r ( p , ω i , ω i o ) L i ( p , ω i ) n ⋅ ω i d ω i = ∫ Ω L i ( p , ω i ) d ω i ∗ ∫ Ω f r ( p , ω i , ω i o ) n ⋅ ω i d ω i L_{o}(p,ω_o)= \int_{\Omega} k_s\frac {DFG}{4(ω_o \cdot n)(ω_i \cdot n)} L_i(p,ω_i)n \cdot ω_idω_i = \int_{\Omega} f_r(p,ω_i ,ω_io) L_i(p,ω_i)n \cdot ω_idω_i = \int_{\Omega}L_i(p,ω_i)dω_i * \int_{\Omega} f_r(p,ω_i ,ω_io) n \cdot ω_idω_i Lo(p,ωo)=Ωks4(ωon)(ωin)DFGLi(p,ωi)nωidωi=Ωfr(p,ωi,ωio)Li(p,ωi)nωidωi=ΩLi(p,ωi)dωiΩfr(p,ωi,ωio)nωidωi
  • 其中第一部分被称为预滤波环境贴图需要根据粗糙度从环境贴图的mipmap中计算。
  • 第二部分是镜面反射积分的 BRDF 部分,如果假设每个方向的入射辐射度都是白色的(因此L(p,x)=1.0 ),就可以对这部分积分进行预计算,Epic Games 将计算好的BRDF 存储在一张LUT图上,叫BRDF积分贴图,R表示菲涅耳响应系数、G表示偏差值。
    在这里插入图片描述
    所以间接光照镜面反射代码如下:
//unity的粗糙度和环境贴图mipmap等级的关系非线性,unity的转换公式为mip = r(1.7 - 0.7r)
float percetualRoughness = roughness * (1.7 - 0.7 * roughness);
float mip = percetualRoughness * UNITY_SPECCUBE_LOD_STEPS;// 把数值范围映射到0-内置宏UNITY_SPECCUBE_LOD_STEPS

half3 reflectDir = normalize(reflect(-ViewDir, worldNormal));
//根据计算出来的采样等级mip采样环境贴图
float4 rgbm = UNITY_SAMPLE_TEXCUBE_LOD(unity_SpecCube0,reflectDir,mip);
//从HDR解码,得到的iblSpecular是镜面公式的左边括号值
float3 iblSpecular = DecodeHDR(rgbm,unity_SpecCube0_HDR) * _CubeExposure;
//采样预计算的LUT图,参数clamp到0到0.99是防止lut采样颜色突变出错
half2 envBRDF = tex2D(_BRDFLut,half2(lerp(0, 0.99,ndotv),lerp(0, 0.99,Roughness))).rg;
iblSpecular *= (Flast * envBRDF.x + envBRDF.y);

float3 IndirectResult = iblDiffuse + iblSpecular;
  • 注意,这里的lut采样实际是虚幻和opengl的算法,unity内置了另一套算法如下,:
float grazingTerm = saturate(1 - roughness + kd);
float surfaceReduction = 1 / (pow(roughness,2) + 1);
float3 iblSpecularResult= surfaceReduction * iblSpecular * FresnelLerp(float4(F0,1.0),grazingTerm,ndv);

二,多光源处理+阴影

  • unity支持平行光、点光源、聚光灯和面光源(烘焙时才有用,先不讨论)。
  • 普通向前渲染的Pass默认为Base Pass,只能执行一次,渲染一个主光源(最亮的平行光);如果想要实现多光源需要添加Additional Pass,该pass会为每个除主光源外的光源执行一次。
  • 环境光照和自发光也都是在Base Pass中计算,Additional Pass中只需要直接光照。
  • Base Pass默认支持阴影,而Additional Pass默认不支持,需要使用#pragma multi_compile_fwdadd_fullshadows打开阴影效果。
  • Additional Pass需要设置混合模式以叠加多光源效果,通常使用Blend One One。
Pass {
    
    
	Tags {
    
     "LightMode"="ForwardBase" }
	
	CGPROGRAM
	//需要使用指令使光照衰减等光照变量正确赋值
	#include "AutoLight.cginc"
	#pragma multi_compile_fwdbase
	
	struct v2f {
    
    
		......
		LIGHTING_COORDS(5, 6) //参数为已用到的TEXCOORD X 中X的下两位
	};
	v2f vert(a2v v)
	{
    
    
		......
		TRANSFER_VERTEX_TO_FRAGMENT(o);//与LIGHTING_COORDS一起根据光源类型计算光源坐标
	}
	fixed4 fragLig(v2f i) :SV_Target
	{
    
    
		......
		float atten = LIGHT_ATTENUATION(i);//计算光源衰弱,与直接光相乘
		......
	}
	……
}
Pass {
    
    
	Tags {
    
     "LightMode"="ForwardAdd" }
	#include "AutoLight.cginc"
	Blend One One
	
	CGPROGRAM
	//赋值光照变量并开启阴影
	#pragma multi_compile_fwdadd_fullshadows
	……
	//与ForwardBase一致
}
  • unity渲染阴影有一个专门的LightMode是ShadowCaster,我们一般shader没有这个pass也可以渲染阴影在于最后的Fallback"Specular",Fallback unity内置的shader时,内置shader都调用了内置VertexLit「builtin-shaders-xxx->DefaultResourcesExtra->NormalVertexLit.shader」,这个shader有默认的阴影渲染。

三,半透明物体

网上关于半透明的文章比较乱,有的也比较旧,看了很多资料尝试自己总结一下吧~

关于这个“透明”在应用中可能有两种,一种剪裁半透明(cutout,减掉透明度小于某值的所有区域,可能树叶、碎片、消解动画等场景会用到),一种混合半透明(就是玻璃、塑料片等能透光看到后边物体的透明)。

扫描二维码关注公众号,回复: 14630944 查看本文章
  • 先看下unity渲染队列,渲染顺序从小到大,在Tag中用"Queue"设置(默认Geometry):
    在这里插入图片描述

3.1 裁剪半透明

  • “Queue”=“AlphaTest”
  • 打开深度写入
  • 设置cutout数值并用clip(alpha - _Cutoff)实现剪裁
  • 网图:
    在这里插入图片描述

3.2 混合半透明

3.2.1 关闭深度写入的方法

网上说的最多就是这种方法,包括冯乐乐的书里也介绍的这种方法,但实际应用场景很复杂,这个方法在物体自身和物体之间前后关系上问题多,亲测不好用。

  • 混合当前片元与已渲染不透明物体的颜色
  • “Queue”=“Transparent”
  • 开启混合Blend SrcAlpha OneMinusSrcAlpha
  • 关闭深度写入ZWrite Off----如果开启深度写入unity检测到半透明物体后就不会渲染后边的物体了,因此强制关闭深度写入
  • 注意:但只有简单物体、简单场景能应用,实际中的复杂自相交物体、透明相交物体用了这种方法就会出错,失去前后遮挡关系。见下图:

在这里插入图片描述

  • 两个透明物体相交也会出现问题,左图为俯视图表示前后关系,右图为相机视角,可以看到相交部分本来应该是B盖住A的,但因为关闭了深度写入,强制先渲染了靠后的B后渲染靠前的A。
    在这里插入图片描述在这里插入图片描述

3.2.2 开启深度写入的方法

  • 但如果把深度写入开启,就会出现透明物体后边的物体被剔除的问题:
    在这里插入图片描述
  • 正是因为开不开启深度写入都有交叉重叠问题,而开启有这么明显的bug问题,相对来说关闭的问题稍小一点,因此教程中教的都是关闭深度写入。(????)
  • 这些问题的本质原因是unity的排序是基于object level的,而不是基于pixel level。(感谢Uniy shader半透明物体相互遮挡的渲染问题这篇文章作者)因此想根本解决问题,可以使用不依赖排序的半透明渲染(Order Independent Transparency,OIT)。

因此在两难情况下,最终我采取了双面渲染+开启深度写入+调整模型区分透明与不透明部分使用两种shader的方式。

四,半透明物体阴影

4.1 半透明物体产生半透明阴影

半透阴影可以用dither网点或专门取shadowmap的方法,为了不增加额外成本,我们这里用网点的方法。

在shader中增加一个ShadowCaster 的Pass:

Pass {
    
    
	Tags {
    
     "LightMode"="ShadowCaster" }
	CGPROGRAM
	……
	float4 _MainTex_ST;
    ampler3D _DitherMaskLOD;//Unity内置的三维抖动纹理
    sampler2D _MainTex;
	fixed _Alpha;
	fixed _Cutoff;
	……
	float4 frag( v2f i ) : SV_Target
	{
    
    
        //hard shadow:镂空物体的阴影
		fixed4 texcol = tex2D( _MainTex, i.uv );
        float alpha = texcol.a * _Alpha;
        clip(alpha - _Cutoff);

        //soft shadow(fade shadow):半透明物体的阴影,会加剧阴影的闪烁
        float dither = tex3D(_DitherMaskLOD, float3((i.pos.xy)*0.25, alpha*0.9375 )).a;
		clip(dither - _Cutoff);

        //计算深度
		return UnityEncodeCubeShadowDepth((length(i.vec) + unity_LightShadowBias.x) * _LightPositionRange.w);
	}
	ENDCG
}

4.2 半透明物体接收阴影

  • “RenderType” = "Transparent"时是不能接受阴影的,文档有如下说明:

Only opaque objects cast and receive shadows so objects using the built-in Transparent or Particle shaders will neither cast nor receive. Generally, you can use the Transparent Cutout shaders instead for objects with “gaps” such as fences, vegetation, etc. Custom Shaders must be pixel-lit and use the Geometry render queue.

  • 因此把Render Queue设置为2500,即“AlphaTest+50”就可以接收阴影啦。

参考资料

  1. learnOpenGL–IBL学习文档
  2. 知乎-如何在Unity中造一个PBR Shader轮子
  3. 知乎-基于物理的渲染PBR
  4. Unity 通用透明物体漫反射Shader
  5. 知乎-Uniy shader半透明物体相互遮挡的渲染问题

猜你喜欢

转载自blog.csdn.net/yx314636922/article/details/124626687