自己实现PBS - 反射间接光
反射间接光其实就是环境反射,unity中环境颜色一般使用cubeMap等采样的方式来获取
1) IBL
全称Image-Based Lighting, 基于纹理的光照。也就是采样贴图,采样cubeMap这种。
2) 直接光和间接光的区别
直接光一般就是平行光,入射光线都是按一个方向射到物体表面。间接光一般就是环境光,是从各个方向射到物体表面。
3) 间接光反射公式
a) 也是和直接光一样,反射颜色=漫反射+镜面反射,只是直接光针对的是主光源,间接光针对的是环境颜色,所以叫法也从高光反射变成了镜面反射。
高光反射和镜面反射都是完全反射,是等价的,只是反射主光源时叫高光反射,反射环境时叫镜面反射。
b) 漫反射公式
c) 镜面反射公式
4) 漫反射环境颜色采样
漫反射的反射方向是随机的,所以就算知道反射方向,也没法确定入射方向,各个方向都有可能,那怎么采样环境颜色?各个方向都采样?貌似不现实,
所以就引入了球谐函数,他就是可以拟算达到各个方向都采样的效果。
5) 镜面反射环境颜色采样
镜面反射可以根据反射方向确定入射方向,所以根据入射方向来采样就行。
6) 粗糙度如何表现
直接光高光反射中,粗糙度表现为亮斑,粗糙度=1,N=H的光线会分散到整个物体表面;粗糙度=0时,N=H的光线会集中在一块亮斑区域。
因为环境光入射方向是各个方向的,所以他的表现应该是粗糙度=1时,整个表面都模糊;粗糙度=0时,整个表面都清晰。
实际是对环境贴图生成了lod图,然后根据粗糙度去映射使用对应的lod图,因为lod图本身就是模糊的,所以就能达成这样的效果。
7) LUT贴图
Look up texture,r通道保存下面公式中左侧的积分值,g通道保存下面公式中右侧的积分值,fr就是上面的镜面反射公式
最终效果
Shader "My/PBS/MyPBS_Indrect" { Properties { _MainTex("Texture", 2D) = "white" {} _Color("Tint Color", Color) = (1, 1, 1, 1) [Header(PBR)] _Metallic("Metallic", Range(0, 1)) = 0 //金属度, 0表现为塑料, 1表现为金属 _Smoothness("Smoothness", Range(0, 1)) = 0.5 //光滑度,其反义属性为粗糙度Roughness,他们的关系:Smoothness=1-Roughness _LUT("LUT Texture",2D) = "white"{} //积分结果查找贴图 } SubShader { Tags { "RenderType" = "Opaque" } LOD 100 Pass { Tags { "LightMode" = "ForwardBase" } CGPROGRAM #pragma multi_compile_fwdbase //#pragma target 3.0 #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" #include "Lighting.cginc" #include "UnityStandardBRDF.cginc" sampler2D _MainTex; float4 _MainTex_ST; half4 _Color; half _Metallic; half _Smoothness; sampler2D _LUT; struct appdata { float4 vertex : POSITION; float4 texcoord : TEXCOORD0; float3 normal : NORMAL; //顶点法线 }; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; float3 worldPos : TEXCOORD1; float3 worldNormal : TEXCOORD2; float3 worldViewDir : TEXCOORD3; float3 worldRefl : TEXCOORD4; }; //G (Geometry function) float GeometrySchlickGGX(float NdotV, float k) { float nom = NdotV; float denom = NdotV * (1.0 - k) + k; return nom / denom; } float GeometrySmith(float NdotV, float NdotL, float Roughness) { float squareRoughness = Roughness * Roughness; float k = pow(squareRoughness + 1, 2) / 8; float ggx1 = GeometrySchlickGGX(NdotV, k); // 视线方向的几何遮挡 float ggx2 = GeometrySchlickGGX(NdotL, k); // 光线方向的几何阴影 return ggx1 * ggx2; } //立方体贴图的Mip等级计算 half CubeMapMip(half perceptualRoughness) { //基于粗糙度计算CubeMap的Mip等级 half mip_roughness = perceptualRoughness * (1.7 - 0.7 * perceptualRoughness); //转换公式mip = r(1.7 - 0.7r),接近实际值的拟合曲线 half mip = mip_roughness * UNITY_SPECCUBE_LOD_STEPS; //得出mip层级。默认UNITY_SPECCUBE_LOD_STEPS=6(定义在UnityStandardConfig.cginc) return mip; } //近似的菲涅尔函数 float3 FresnelSchlick(float3 F0, float VdotH) { float3 F = F0 + (1 - F0) * exp2((-5.55473 * VdotH - 6.98316) * VdotH); return F; } //间接光的菲涅尔系数 float3 FresnelSchlickRoughness(float cosTheta, float3 F0, float roughness) { return F0 + (max(float3(1, 1, 1) * (1 - roughness), F0) - F0) * pow(1.0 - cosTheta, 5.0); } v2f vert(appdata v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); o.worldNormal = UnityObjectToWorldNormal(v.normal); //法线(世界空间) o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; //世界坐标 o.worldViewDir = UnityWorldSpaceViewDir(o.worldPos); //观看方向(世界空间), 顶点指向观察点 o.worldRefl = reflect(-o.worldViewDir, o.worldNormal); //入射光线方向, 通过入射光线方向获得从环境的哪里发射过来的 return o; } fixed4 frag(v2f i) : SV_Target { float3 worldNormal = normalize(i.worldNormal); float3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); float3 worldViewDir = normalize(i.worldViewDir); float3 worldHalfDir = normalize(worldLightDir + worldViewDir); float NdotV = max(saturate(dot(worldNormal, worldViewDir)), 1e-5); //防止除0 float NdotL = max(saturate(dot(worldNormal, worldLightDir)), 1e-5); //float LdotH = max(saturate(dot(worldLightDir, worldHalfDir)), 1e-5); float NdotH = max(saturate(dot(worldNormal, worldHalfDir)), 1e-5); float VdotH = max(saturate(dot(worldViewDir, worldHalfDir)), 1e-5); float perceptualRoughness = 1 - _Smoothness; float roughness = PerceptualRoughnessToRoughness(perceptualRoughness); //粗糙度 roughness = max(roughness, 0.002); //防止为0,保留一点点高光 half3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb; //取贴图颜色作为漫反射颜色(即: 光被吸收了部分后颜色后的剩余颜色) //直接光高光反射 half D = GGXTerm(NdotH, roughness); //法线分布函数(微表面反射) half G = GeometrySmith(NdotV, NdotL, roughness); //微平面间相互遮蔽的比率(微表面遮挡) half3 F0 = lerp(unity_ColorSpaceDielectricSpec.rgb, albedo, _Metallic); //unity_ColorSpaceDielectricSpec为常数: float3(0.04, 0.04, 0.04) half3 F = FresnelSchlick(F0, VdotH); //近似的菲涅尔函数(菲涅尔效应) half3 specular = (D * G * F * 0.25) / (NdotV * NdotL) * _LightColor0.rgb * NdotL * UNITY_PI; //直接光漫反射 //half3 kd = (1 - F)*(1 - metallic); //漫反射系数,公式上更遵循物理,但效果上没有内置宏好 half kd = OneMinusReflectivityFromMetallic(_Metallic); //漫反射系数,内置宏 half3 diffuse = kd * albedo * _LightColor0.rgb * NdotL; //没按照理论公式那样除PI, Unity说法是为了和Legacy保持一致的明暗才这么做的 //间接光漫反射 half3 ambient_contrib = ShadeSH9(float4(worldNormal, 1)); //球谐光照 half3 ambient = 0.03 * albedo.rgb; //环境光,取很小的值即可,可省略 half3 iblDiffuse = max(half3(0, 0, 0), ambient.rgb + ambient_contrib); //采样到的环境颜色 half3 F_Indirect = FresnelSchlickRoughness(NdotV, F0, roughness); half3 kd_Indirect = (1 - F_Indirect) * (1 - _Metallic); half3 diffuse_Indrect = kd_Indirect * albedo * iblDiffuse; //间接光镜面反射 half mip_lod = CubeMapMip(perceptualRoughness); half4 rgb_mip = UNITY_SAMPLE_TEXCUBE_LOD(unity_SpecCube0, i.worldRefl, mip_lod); half3 iblSpecular = DecodeHDR(rgb_mip, unity_SpecCube0_HDR); //间接光镜面反射采样的预过滤环境贴图 float2 envBRDF = tex2D(_LUT, float2(lerp(0, 0.99, NdotV), lerp(0, 0.99, roughness))).rg; // LUT采样 float3 specular2_Indirect = F_Indirect * envBRDF.r + envBRDF.g; half3 specular_Indirect = iblSpecular * specular2_Indirect; return fixed4(diffuse + specular + diffuse_Indrect + specular_Indirect, 1); } ENDCG } } }
参考
【Unity Shader】基于物理的渲染PBR(一) - 知乎 (zhihu.com)
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步