URP下定制PBRShader
复刻URPLit
考虑到项目移植各个平台的顺利,URP还是目前最实用的管线了,但自带的lit还是缺少很多功能,比如对VR很重要的几何高光抗锯齿,混合SSR也不是很方便,各向异性,环境法线,高光遮蔽,视差映射等等功能都是缺少的。
为了功能的收缩与扩张方便,还是有必要手撸一个PBR的。
而且Lit的代码为了兼容各种环境,比如Dot,VR,LODCross,做了大量的判断,代码很杂乱,很难修改。整理一遍也是为了更好的理解URP的Lit着色器。
以上存粹是为了说服自己造这个轮子。。。
需求
先从简单的搞起,大致输入就先仿照URP最精简的Lit
- 按金属粗糙的工作流
- 不透明
- 仅正面
- alpha不裁切
- 接收阴影
- 阴影不级联
- BaseMap(RGBA)
- BaseColor(RGBA)
- Metallic
- MetallicMap(R金属 A光滑)
- Smoothness
- NormalMap
- 先不考虑高度图
- OcclusionMap
- OcclusionStrength
- EmissionColor
- EmissionMap
- 先不考虑细节贴图
- 先不考虑清漆
- 实时光仅平行光
- 有环境反射
- 先不考虑烘焙
开搞
这里就没什么好说的了,公式就翻翻URP的lit的实现,先尽量汇总到一个文件里,方便之后比对。
Properties
就参照上面的需求,大致列一下属性,不够之后再加。
Properties
{
[MainTexture] _BaseMap("Albedo", 2D) = "white" {}
[MainColor] _Color("Color", Color) = (1,1,1,1)
_Metallic("Metallic", Range(0.0, 1.0)) = 0.0
_MetallicGlossMap("Metallic", 2D) = "white" {}
_Smoothness("Smoothness", Range(0.0, 1.0)) = 0.5
_BumpMap("Normal Map", 2D) = "bump" {}
_OcclusionStrength("Strength", Range(0.0, 1.0)) = 1.0
_OcclusionMap("Occlusion", 2D) = "white" {}
[HDR] _EmissionColor("Color", Color) = (0,0,0)
_EmissionMap("Emission", 2D) = "white" {}
}
Tags
按照URP文档的要求,要配置一些通用的Tag
Tags
{
"RenderType" = "Opaque"
"RenderPipeline" = "UniversalPipeline"
"UniversalMaterialType" = "Lit"
}
Pass
URP要求一个Shader中要实现多个Pass,分别包含了
- UniversalForward
- ShadowCaster
- UniversalGBuffer
- DepthOnly
- DepthNormals
- Meta
- Universal2D
顾名思义,不一一介绍了,最为最简单的仅前向的shader,这里就挑出以下五个实现
- UniversalForward
- ShadowCaster
- DepthOnly
- DepthNormals
- Meta
关键字
参照urp的lit,分为材质的关键字、管线的关键字、unity的关键字和GPUInstancing。
材质的关键字主要用来定义一些局部的属性,如是否启用法线贴图,是否启用高度图,光滑的从哪里采样等等
这部分就先省略,之后优化时再加。
然后是管线的关键字,主要定义一些是否启用了主光源阴影,屏幕阴影,附加光阴影,软阴影等等。
这里就也先省略了,需要对应的功能再加。
然后是untiy的关键字。包含了光照贴图,阴影的shadowMask,雾气等等。
存储结构
参考lit,把输入以及一些常用的采样分到一个文件中,具体的着色器函数在另一个文件,方便引用,那这里也依葫芦画瓢。
值得一提的是URP中还把常见的一些贴图又分到了一个SurfaceInpute中,这里就没有必要了,合并到一起。
#ifndef MK_LIT_INPUT_INCLUDED
#define MK_LIT_INPUT_INCLUDED
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
CBUFFER_START(UnityPerMaterial)
float4 _BaseMap_ST;
half4 _BaseColor;
half4 _EmissionColor;
half _Smoothness;
half _Metallic;
half _OcclusionStrength;
CBUFFER_END
TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap);
TEXTURE2D(_BumpMap); SAMPLER(sampler_BumpMap);
TEXTURE2D(_EmissionMap); SAMPLER(sampler_EmissionMap);
TEXTURE2D(_OcclusionMap); SAMPLER(sampler_OcclusionMap);
TEXTURE2D(_MetallicGlossMap); SAMPLER(sampler_MetallicGlossMap);
#endif // MK_LIT_INPUT_INCLUDED
都是很常见的属性。
顶点着色器
Varyings LitPassVertex(Attributes input)
{
// 初始化赋值
Varyings output = (Varyings)0;
// GPUInstance和VR单pass的套路
UNITY_SETUP_INSTANCE_ID(input);
UNITY_TRANSFER_INSTANCE_ID(input, output);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);
// 一个方便的小函数,包含了各种位置的计算
VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz);
// 求BTN
// normalWS and tangentWS already normalize.
// this is required to avoid skewing the direction during interpolation
// also required for per-vertex lighting and SH evaluation
VertexNormalInputs normalInput = GetVertexNormalInputs(input.normalOS, input.tangentOS);
// 顶点光照颜色 这里就不用顶点光照了
half3 vertexLight = VertexLighting(vertexInput.positionWS, normalInput.normalWS);
// 也省略雾气
half fogFactor = 0;
#if !defined(_FOG_FRAGMENT)
fogFactor = ComputeFogFactor(vertexInput.positionCS.z);
#endif
// UV缩放
output.uv = TRANSFORM_TEX(input.texcoord, _BaseMap);
// 输出法线
// already normalized from normal transform to WS.
output.normalWS = normalInput.normalWS;
// 输出切线
#if defined(REQUIRES_WORLD_SPACE_TANGENT_INTERPOLATOR) || defined(REQUIRES_TANGENT_SPACE_VIEW_DIR_INTERPOLATOR)
real sign = input.tangentOS.w * GetOddNegativeScale();
half4 tangentWS = half4(normalInput.tangentWS.xyz, sign);
#endif
#if defined(REQUIRES_WORLD_SPACE_TANGENT_INTERPOLATOR)
output.tangentWS = tangentWS;
#endif
// 切线空间的视角向量
#if defined(REQUIRES_TANGENT_SPACE_VIEW_DIR_INTERPOLATOR)
half3 viewDirWS = GetWorldSpaceNormalizeViewDir(vertexInput.positionWS);
half3 viewDirTS = GetViewDirectionTangentSpace(tangentWS, output.normalWS, viewDirWS);
output.viewDirTS = viewDirTS;
#endif
// 输出光照贴图UV
OUTPUT_LIGHTMAP_UV(input.staticLightmapUV, unity_LightmapST, output.staticLightmapUV);
#ifdef DYNAMICLIGHTMAP_ON
output.dynamicLightmapUV = input.dynamicLightmapUV.xy * unity_DynamicLightmapST.xy + unity_DynamicLightmapST.zw;
#endif
// 如果没有光照贴图,就用光照探针的SH
OUTPUT_SH(output.normalWS.xyz, output.vertexSH);
// 雾气赋值
#ifdef _ADDITIONAL_LIGHTS_VERTEX
output.fogFactorAndVertexLight = half4(fogFactor, vertexLight);
#else
output.fogFactor = fogFactor;
#endif
// 输出世界位置
#if defined(REQUIRES_WORLD_SPACE_POS_INTERPOLATOR)
output.positionWS = vertexInput.positionWS;
#endif
// 输出阴影贴图UV
#if defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR)
output.shadowCoord = GetShadowCoord(vertexInput);
#endif
output.positionCS = vertexInput.positionCS;
return output;
}
那精简之后得到
Varyings LitPassVertex(Attributes input)
{
Varyings output = (Varyings)0;
// todo instance VR
VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz);
VertexNormalInputs normalInput = GetVertexNormalInputs(input.normalOS, input.tangentOS);
output.uv = TRANSFORM_TEX(input.texcoord, _BaseMap);
output.normalWS = normalInput.normalWS;
real sign = input.tangentOS.w * GetOddNegativeScale();
half4 tangentWS = half4(normalInput.tangentWS.xyz, sign);
output.tangentWS = tangentWS;
OUTPUT_LIGHTMAP_UV(input.staticLightmapUV, unity_LightmapST, output.staticLightmapUV);
output.positionWS = vertexInput.positionWS;
output.shadowCoord = GetShadowCoord(vertexInput);
output.positionCS = vertexInput.positionCS;
return output;
}
片元
然后就是最复杂的片元了
URP的SurfaceData略显复杂,精简一下
struct SurfaceData
{
half3 albedo;
half metallic;
half smoothness;
half3 normalTS;
half3 emission;
half occlusion;
};
然后是初始化SurfaceData,就是采样一堆贴图,然后根据系数得出最终的每个片元的属性。简化一下如下
inline void InitializeStandardLitSurfaceData(float2 uv, out SurfaceData outSurfaceData)
{
half4 albedoAlpha = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, uv);
outSurfaceData.albedo = albedoAlpha.rgb * _BaseColor.rgb;
half4 metallicSmoothness = SAMPLE_TEXTURE2D(_MetallicGlossMap, sampler_MetallicGlossMap, uv);
outSurfaceData.metallic = metallicSmoothness.r * _Metallic;
outSurfaceData.smoothness = metallicSmoothness.a * _Smoothness;
half4 n = SAMPLE_TEXTURE2D(_BumpMap, sampler_BumpMap, uv);
outSurfaceData.normalTS = UnpackNormal(n);
outSurfaceData.emission = SAMPLE_TEXTURE2D(_EmissionMap, sampler_EmissionMap, uv).rgb * _EmissionColor.rgb;
half occ = SAMPLE_TEXTURE2D(_OcclusionMap, sampler_OcclusionMap, uv).g;
outSurfaceData.occlusion = LerpWhiteTo(occ, _OcclusionStrength);
}
接着是初始化顶点的输入,主要涉及各种空间下的坐标以及GI、阴影、雾气等。
void InitializeInputData(Varyings input, half3 normalTS, out InputData inputData)
{
inputData = (InputData)0;
// 世界坐标
#if defined(REQUIRES_WORLD_SPACE_POS_INTERPOLATOR)
inputData.positionWS = input.positionWS;
#endif
// 视角方向
half3 viewDirWS = GetWorldSpaceNormalizeViewDir(input.positionWS);
// 法线相关
#if defined(_NORMALMAP) || defined(_DETAIL)
// 如果有法线贴图就要用TBN重新计算
float sgn = input.tangentWS.w; // should be either +1 or -1
float3 bitangent = sgn * cross(input.normalWS.xyz, input.tangentWS.xyz);
half3x3 tangentToWorld = half3x3(input.tangentWS.xyz, bitangent.xyz, input.normalWS.xyz);
#if defined(_NORMALMAP)
inputData.tangentToWorld = tangentToWorld;
#endif
inputData.normalWS = TransformTangentToWorld(normalTS, tangentToWorld);
#else
inputData.normalWS = input.normalWS;
#endif
// 归一化,还要考虑向量为0
inputData.normalWS = NormalizeNormalPerPixel(inputData.normalWS);
inputData.viewDirectionWS = viewDirWS;
// 阴影
#if defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR)
inputData.shadowCoord = input.shadowCoord;
#elif defined(MAIN_LIGHT_CALCULATE_SHADOWS)
inputData.shadowCoord = TransformWorldToShadowCoord(inputData.positionWS);
#else
inputData.shadowCoord = float4(0, 0, 0, 0);
#endif
// 雾
#ifdef _ADDITIONAL_LIGHTS_VERTEX
inputData.fogCoord = InitializeInputDataFog(float4(input.positionWS, 1.0), input.fogFactorAndVertexLight.x);
inputData.vertexLighting = input.fogFactorAndVertexLight.yzw;
#else
inputData.fogCoord = InitializeInputDataFog(float4(input.positionWS, 1.0), input.fogFactor);
#endif
// GI
#if defined(DYNAMICLIGHTMAP_ON)
inputData.bakedGI = SAMPLE_GI(input.staticLightmapUV, input.dynamicLightmapUV, input.vertexSH, inputData.normalWS);
#else
inputData.bakedGI = SAMPLE_GI(input.staticLightmapUV, input.vertexSH, inputData.normalWS);
#endif
// 屏幕空间UV
inputData.normalizedScreenSpaceUV = GetNormalizedScreenSpaceUV(input.positionCS);
// 阴影遮罩
inputData.shadowMask = SAMPLE_SHADOWMASK(input.staticLightmapUV);
#if defined(DEBUG_DISPLAY)
#if defined(DYNAMICLIGHTMAP_ON)
inputData.dynamicLightmapUV = input.dynamicLightmapUV;
#endif
#if defined(LIGHTMAP_ON)
inputData.staticLightmapUV = input.staticLightmapUV;
#else
inputData.vertexSH = input.vertexSH;
#endif
#endif
}
GI
依据不同的光照贴图的编码,有不同的解码方式,这里先取巧用FULL_HDR,就可以不用解码了。
大致流程就是采样贴图,得出方向与强度,用halfLambert得出最终的GI。
阴影
这里仅算主光源阴影,Untiy还会采样烘焙的阴影贴图,去混合实时光阴影与烘焙阴影。
PBR
整个计算流程就是分为算AO,实际的URP会在这里取看有没有SSAO,我们先省略。
然后得出主光源的信息,这里就包含了阴影。
然后就是计算GI,主光源,附加光,顶点光。
最后两项先省略。
代码如下:
half4 PBR(InputData inputData, SurfaceData surfaceData)
{
BRDFData brdfData;
InitializeBRDFData(surfaceData, brdfData);
AmbientOcclusionFactor aoFactor = CreateAmbientOcclusionFactor(inputData, surfaceData);
Light mainLight = GetMainLight(inputData, 1, aoFactor);
LightingData lightingData = CreateLightingData(inputData, surfaceData);
lightingData.giColor = GlobalIllumination(brdfData,
inputData.bakedGI, aoFactor.indirectAmbientOcclusion, inputData.positionWS,
inputData.normalWS, inputData.viewDirectionWS, inputData.normalizedScreenSpaceUV);
lightingData.mainLightColor = LightingPhysicallyBased(brdfData,
mainLight,
inputData.normalWS, inputData.viewDirectionWS);
return CalculateFinalColor(lightingData, 1);
}
详细的计算GI与实时光的代码就不贴了,总之这样就算有了一个改动方便的PBR。后续再想增加一些SSR、环境法线之类的功能就方便很多了。
当然这个还很不完善,比如级联阴影,透明,等等都没有考虑。
很多分支都被删减了,先看看好不好用吧,如果可行那就以后再慢慢补充。
分文件
还是有必要按照原来逻辑把hlsl代码分开,不然后期不太好维护。
大致分为以下几个文件
- AmbientOcclusion AO计算,包括直接光与间接光,SSAO得出的结果在前向渲染里也是在这里采样的。
- BRDF 这里就是PBR那一套公式了
- EntityLighting 涉及球谐函数以及光照贴图的采样(包括解码)
- GlobalIllumination 在对EntityLighting封装的基础上,添加了诸如BoxProjected,混合,根据光照贴图编码方式选择具体的解码,采样反射探针等
- ImageBasedLighting 包含了一些基于图像的光照工具函数
- Lighting 主要的光照计算,除了调用上面的BRDF,其实还包含了Unlit,BlinnPhong,BakedLit等多种光照模型。
- LitForwardPass 包含顶点与片元。
- LitInput 包含从材质传递过来的全部输入
- RealtimeLights 实时光的获取,并调用阴影
- Shadows 阴影相关,阴影贴图的采样,级联,也包含了采样光照贴图里的阴影贴图并混合
- SurfaceData 一个结构体,包含了一个要进入光照计算的片元的表面数据,如漫反射,金属,粗糙,遮蔽等。
- InputData 一个结构体,包含了一个要进入光照计算的片元的几何数据,如位置坐标,世界法线,阴影坐标,BakedGI等。
根据这些文件的调用关系,再梳理一下就是
LitForwardPass是总入口,汇总并梳理了全部的输入,整合成两个输入即InputData与SurfaceData。
当然,出于性能的考虑,这里的阴影坐标也提前在顶点着色器中获取了。
然后选择Lighting中的一个光照模型。类似与Lighting实现了一个通用的光照模型的接口。
这里就选用PBR的模型。
然后就开始采用PBR的公式,分以下几步,
- 首先是从InputData与SurfaceData构造BRDFData。
- BRDFData与SurfaceData很类似,主要是增加了一些诸如roughness2MinusOne等方便计算的属性。
- 取AO,从AmbientOcclusion取出AO,如果有SSAO,也会在这里处理。
- 取主光照数据(数据结构Light),从RealtimeLights获取实时光的数据。
- 这里又会调用Shadows来包含对应的阴影
- Light包含 方向,颜色,阴影。
- 然后是构造LightingData,包含了GI,主光源,附加光,顶点光,自发光。
- GI是调用BRDF的工具函数由公式计算得出,主要的输入如下
- 间接漫反射就是直接取的光照贴图,或者球谐函数。
- 间接高光就是采用的反射贴图了
- 主光源就单纯的用BRDF公式,也是分为直接漫反射和直接高光。
- 出于简化的目的,其余的先不看了。
- GI是调用BRDF的工具函数由公式计算得出,主要的输入如下
- 最后就是将这些来自各种光源的光叠加,得出最终的输出。
这应该就是URP下的最简单的PBR实现了
阴影投射Pass
只需在顶点着色器中应用一下法线和深度偏移,其余没有什么特殊的。
球谐函数
根据LIGHTMAP_ON这个关键字,来判断GI是用贴图还是球谐函数。
级联阴影
通过_MAIN_LIGHT_SHADOWS_CASCADE来判断是否级联,然后根据距离各个级联球的中心的距离与设定距离的差值,来判断应该取哪一个级联。
总结
说不上是从零开始手撸PBR造轮子,只能算是精简了URP的原本的lit的着色器,主要目的还是理清其各个文件的逻辑关系,方便之后实现各种效果。