Unity shader内置standard代码解析

最近有相关需求制作,所以这里编写一个文档,方便后续的流程查看。

下载源码

由于unity内置的shader是无法查看源码的,你需要去官网下载对应版本内置源码查看

在引擎下载那里,会有一个Built in Shaders,下载

打开以后,就是对应的shader

内置的shandard在DefaultResourcesExtra目录内,打开便是。

shader解析

Standard里面分了两套,一套正常的,一套精简版的,

这两套渲染的切换是通过设置shader的lod进行切换的。

每个shader下面由5个pass组成(简化版的不支持延迟渲染)

  1. 前向渲染主光源
  2. 前向渲染副光源
  3. 阴影渲染
  4. 延迟渲染
  5. 烘焙

简化版本的渲染也不支持视差偏移,它们是通过宏去控制的,更多不同在渲染代码内部。

ForwardBase 和ForwardAdd引用的一套渲染逻辑,然后通过定义的宏和调用不同的顶点/片元着色器函数来区分到底是base还是add。

base的是这样

add是这样

它们都引用的UnityStandardCoreForward渲染逻辑

在这个文件里面,是一些主要函数的定义,区分是否为简化的shader,如果简化的shader,则引入简化的库文件,非简化,则引入了UnityStandardCore.cginc,在这里定义了pass里面调用的顶点着色器和片元着色器函数,函数内直接调用了对应的UnityStandardCore库里的函数,这里也是standard的核心代码。

上面还引入了UnityStandardConfig.cginc,这个文件则是一些配置,主要定义的宏,抽几个比较重要的

下面定义了立体图贴图的曝光度以及lod层级数

定义brdf GGX

UnityStandardCore.cginc

里面代码的一些库的引用,核心库也是基于这些库实现的最终渲染

ForwardBase渲染主要就是调用了

c 复制代码
VertexOutputForwardBase vertBase (VertexInput v) { return vertForwardBase(v); }
half4 fragBase (VertexOutputForwardBase i) : SV_Target { return fragForwardBaseInternal(i); }

VertexInput 就是在UnityStandardInput.cginc内实现的需要传入到顶点着色器的数据

VertexOutputForwardBase 则是从顶点传入到片元的数据

vertForwardBase 函数里对位置,UV,法向等做了一些处理,更复杂的还有lightmap的UV,还有视差偏移

重点函数,片元着色器fragForwardBaseInternal,有点代码越少越狠的节奏,后面我将一个个的函数解析

UNITY_APPLY_DITHER_CROSSFADE(i.pos.xy)

这个是为了实现淡入淡出的效果

unity_DitherMask 为unity内置生成的抖动noise贴图

unity_LODFade 为需要设置的变量,在UnityShaderVariables.cginc里面定义

c 复制代码
float4 unity_LODFade; // x is the fade value ranging within [0,1]. y is x quantized into 16 levels

FRAGMENT_SETUP(s)

这个函数主要是生成后续使用的数据FragmentCommonData,定义为FragmentSetup函数

c 复制代码
#define FRAGMENT_SETUP(x) FragmentCommonData x = FragmentSetup(i.tex, i.eyeVec.xyz, IN_VIEWDIR4PARALLAX(i), i.tangentToWorldAndPackedData, IN_WORLDPOS(i));

i.tex 顶点着色器计算的uv

i.eyeVec.xyz 摄像机朝向

IN_VIEWDIR4PARALLAX(i) 摄像机朝向基于视差偏移的法向值

i.tangentToWorldAndPackedData 切线坐标系转世界坐标系矩阵 [3x3:tangentToWorld | 1x3:viewDirForParallax or worldPos]

IN_WORLDPOS(i) 渲染目标世界坐标位置

FragmentCommonData 则是返回从顶点着色器拿到的数据处理后的数据,后续获取通过s变量获取。oneMinusReflectivity 为1-反射率

然后就是函数FragmentSetup,设置数据,截图里面我也加了注释

这里主要讲的是UNITY_SETUP_BRDF_INPUT函数,它可以根据工作流去设置数据,有三种 SpecularSetup RoughnessSetup MetallicSetup分别对应 高光工作流 粗糙度工作里 金属度工作流,standard.shader里面定义了金属度工作流

如果没有定义的话,会切换高光工作流

由于我这里使用的是金属度工作流,这里讲解一些金属度工作流的相关内容,MetallicSetup函数,函数内有两个函数,第一个函数去获取贴图的值,第二个函数为计算漫反射颜色,镜面反射颜色和反射率

MetallicGloss内返回二维向量,x为金属度,y为光滑度,光滑度还可以选择是使用的_MetallicGlossMap的a通道还是_MainTex的a通道

DiffuseAndSpecularFromMetallic

unity_ColorSpaceDielectricSpec的值在线性空间中默认是 half4(0.04, 0.04, 0.04, 1.0 - 0.04),这也是物理渲染中默认反射率

根据金属度求出反射率

MainLight

c 复制代码
    UnityLight mainLight = MainLight(); //主光源
    UNITY_LIGHT_ATTENUATION(atten, i, s.posWorld); //合并阴影

UnityLight 结构里面有三个值 color 光的颜色 dir 光的朝向 ndotl 法向和光的点乘值(已弃用),MainLight函数里面就是获取第一盏灯的颜色和朝向

UNITY_LIGHT_ATTENUATION 计算阴影遮挡。会根据光的类型调用不同的函数,一般主光源都是平衡光,这里看一下平衡光的实现,代码在AutoLight.cginc里面

在AutoLight.cginc中,对多种情况的处理,比如屏幕空间阴影,包含烘焙阴影

这里我们看最简单的使用SHADOW_ATTENUATION生成的unitySampleShadow函数,这个函数会去获取shadowmap的值来做处理

c 复制代码
#define UNITY_SAMPLE_SCREEN_SHADOW(tex, uv) UNITY_SAMPLE_TEX2DARRAY(tex, float3((uv).x / (uv).w, (uv).y / (uv).w, (float)unity_StereoEyeIndex)).r

这一块解析起来确实麻烦,如果你需要阴影的话,记得直接使用UNITY_LIGHT_ATTENUATION函数。第一个值就是阴影的值。

FragmentGI

全局光照,包含了lightmap,sh球谐光照,ibl等对物体影响的内容

c 复制代码
UnityGI gi = FragmentGI(s, occlusion, i.ambientOrLightmapUV, atten, mainLight); //全局光照

UnityGI包含全局光照有光的数据,以及间接光的漫反射和镜面反射颜色

FragmentGI 函数主要是设置一些所需要的值,然后调用UnityGlobalIllumination生成最终所需的UnityGI数据

UnityGlossyEnvironmentSetup 主要是求出了两个值 SmoothnessToPerceptualRoughness是通过光滑度求出粗糙度,也就是1-光滑度,reflUVW,根据眼睛和法向求出反射方向

准备好需要的全局光照计算数据以后,就要开始调用UnityGlobalIllumination计算了,分别去计算间接光漫反射,以及间接光镜面反射

在间接光漫反射里面,考虑光照贴图和动态光照贴图,这个我在之前我的文章里面说过,这里就不再多解释。

解析一下,上图比较重要的几行代码:

c 复制代码
o_gi.light.color *= data.atten;
o_gi.indirect.diffuse = ShadeSHPerPixel(normalWorld, data.ambient, data.worldPos);
o_gi.indirect.diffuse *= occlusion;

ShadeSHPerPixel 计算间接光漫反射,相对于lightmap里面获取的,它具有动态性。球谐光照是由七个四维向量组成,

由引擎设置参数。unity还兼容的3d纹理方式的SHEvalLinearL0L1_SampleProbeVolume

计算完成间接光漫反射以后,就是计算间接光镜面反射,在unity里面是通过,实现原理就是通过立方体贴图去拾取颜色作为镜面反射的颜色

里面主要的方法就是Unity_GlossyEnvironment,这个去拾取引擎设置的立方体贴图,并获取颜色

perceptualRoughnessToMipmapLevel就是粗糙度乘以LOD级数UNITY_SPECCUBE_LOD_STEPS,粗糙度越低,表示越光滑,那么lod层级就越低,图片拾取的也最清晰。

最后,将全局光的灯光颜色,间接光漫反射,间接光镜面反射计算完成,交给物理渲染BRDF函数实现最后的颜色。

UNITY_BRDF_PBS

c 复制代码
half4 c = UNITY_BRDF_PBS(s.diffColor, s.specColor, s.oneMinusReflectivity, s.smoothness, s.normalWorld, -s.eyeVec, gi.light, gi.indirect); //基于物理的渲染

看代码,standard里面内置了三套方式,

  1. BRDF1_Unity_PBS 是基于物理的BRDF(Bidirectional Reflectance Distribution Function,双向反射分布函数)
  2. BRDF2_Unity_PBS 是基于极简的微表面理论的BRDF http://www.thetenthplanet.de/archives/255
  3. BRDF3_Unity_PBS 是不是微表面的基于修正归一化的 Blinn-Phong BRDF

    这里,我只介绍质量最好的第一种BRDF1_Unity_PBS,看注释,也能了解到它的模型是如何计算的
    直接光漫反射 kD / pi
    直接光镜面反射 kS * (D * V * F) / 4
    最后乘以NdotL
    BRDF里面还有两种,一种是GGX的高光,另一种是老旧的BlinnPhong的

    首先函数获取到需要用的数据,粗糙度,半角向量halfDir,NdotV,NdotL,NdotH,LdotV,LdotH

    然后基于数据求直接光漫反射
c 复制代码
    // Diffuse term
    half diffuseTerm = DisneyDiffuse(nv, nl, lh, perceptualRoughness) * nl;

求漫反射,还给注释迪士尼的漫反射必须除以PI,在函数外实现,貌似unity都亮了PI,所以不用除以PI了

然后解释了一下为什么不除以PI

接下来,先求出BRDF的D项和V项

然后在最后颜色合并的时候,求出菲涅尔项 F,怪不得之前听朋友说,unity的BRDF写的很难看,确实难看

最终计算出来了颜色加上自发光合并雾效,返回片元颜色

相关推荐
一个小狼娃3 小时前
Android集成Unity避坑指南
android·游戏·unity
极客柒3 小时前
Unity 协程GC优化记录
java·unity·游戏引擎
黄思搏4 小时前
Unity SpriteRenderer 进度条 Shader 实现
unity·游戏引擎
猫屋小鱼丸5 小时前
手把手教你在unity中实现一个视觉小说系统(一)
unity
国服第二切图仔9 小时前
Rust开发实战之简单游戏开发(piston游戏引擎)
开发语言·rust·游戏引擎
HahaGiver66619 小时前
Unity与Android原生交互开发入门篇 - 打开Unity游戏的设置
android·unity·交互
@LYZY20 小时前
Unity TextMeshPro 文本对齐方式详解
unity·游戏引擎·textmeshpro·tmp
在路上看风景20 小时前
2.1 ShaderLab - 渲染状态
unity
AA陈超1 天前
虚幻引擎5 GAS开发俯视角RPG游戏 P07-06 能力输入的回调
c++·游戏·ue5·游戏引擎·虚幻
一线灵1 天前
跨平台游戏引擎 Axmol-2.9.1 发布
游戏引擎