自己实现PBS - 只考虑直接光
PBS的全称为Physically Based Shading,就是基于物理的渲染技术
1) 为什么需要PBS?
因为PBS渲染出来的效果更好。
2) PBS复杂吗?
理论很复杂,但是应用其实也是套用公式,和传统光照不同的公式。
3) BRDF是什么?
全称为Bidirectional Reflectance Distribution Function,双向反射分布函数。
就是PBS中的公式的叫法,叫BRDF会更专业,公式就是老土的叫法。
4) BRDF有什么特别之处吗?
PBS的公式需要满足交换律和能量守恒。满足不了这两个条件,就不能成为PBS的公式。
a) 交换律: 摄像机位置和光源位置调换位置,函数计算的最终结果是相同的
b) 能量守恒: 函数的计算结果不能超过光强
4) PBS的公式是怎样的?
PBS的公式也有很多流派,这边主要讲Cook-Torrance流派的公式,这个目前也是游戏行业主流。
a) 只考虑直接光反射的情况,反射颜色=漫反射+高光反射,没错这里和传统光照是一样的。
b) 漫反射公式:kd为漫反射系数,除以PI是保证能量守恒的
c) 高光反射公式:ks一般为1-kd,D, F, G表示的是函数
5) 高光反射中的D, F, G函数,以及微平面理论。
D: 法线分布函数, 用于拟算多少比例的微面元能把光线反射到我们的眼睛中
微观层面,所有平面都不是光滑的,由许多微面元组成,也就是表面凹凸不平,这也就造成了入射光线会向各个方向反射,只有反射方向和观察方向重叠的(即微平面法线和半角向量重叠),才能进入到我们的眼睛。
所以,入射光线经过微面元镜面反射后,只有一定比例的光线能进入我们的眼睛。
G: 阴影遮掩函数, 用于拟算多少比例的微面元反射被遮掉
微观层面,反射的时候,又遇到突起很高的微面元被挡住了;或者其他情况的遮挡。
F: 菲涅尔函数,用于拟算高光反射比例。
就是达到掠射角大,反射弱;掠射角小,反射强这样的效果。
最终效果
上面提到的那些公式都是理论公式,但按照理论公式写实际运行起来的效果不一定完全正确,此时可能会做下相关修改,把某一部分按经验公式的方式写,让最终效果要看着能接受
Shader "My/PBS/MyPBS" { Properties { _MainTex("Texture", 2D) = "white" {} _Color("Tint Color", Color) = (1, 1, 1, 1) _Metallic("Metallic", Range(0, 1)) = 0 //金属度, 0表现为塑料, 1表现为金属 _Smoothness("Smoothness", Range(0, 1)) = 0.5 //光滑度,其反义属性为粗糙度Roughness,他们的关系:Smoothness=1-Roughness } 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; 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; }; //G (Geometry function) float GeometrySchlickGGX(float dotResult, float k) { float nom = dotResult; float denom = dotResult * (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; } //近似的菲涅尔函数 float3 FresnelSchlick(float3 F0, float VdotH) { float3 F = F0 + (1 - F0) * exp2((-5.55473 * VdotH - 6.98316) * VdotH); return F; } 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); //观看方向(世界空间), 顶点指向观察点 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); //法线分布函数, 粗糙度越高N=H区域就越分散, 越低就越集中 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保持一致的亮度才这么做的 return fixed4(diffuse + specular, 1); } ENDCG } } }
6) 把高光部分拿掉,就是frag最后return fixed4(diffuse, 1); _Metallic调成1。
金属度为1不应该表现的像金属吗?为什么是一个黑色的球?
a) 由金属特性决定,折射进入内部的光会被自由电子立即转换为其他形式的能量,就是有吸收没反射。(参考《Shader入门精要》CH18.1.4, CH18.2.2)
b) 这边拿掉了高光的干扰(调光滑度没有任何效果)
7) 把漫反射部分拿掉,就是frag最后return fixed4(specular, 1);
_Smoothness影响高光亮斑集中度,_Metallic影响菲涅尔效果(琼射角越小反射越就明显)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 通过 API 将Deepseek响应流式内容输出到前端
· AI Agent开发,如何调用三方的API Function,是通过提示词来发起调用的吗