Forward路径中的StandardShader
这篇说一下unity中的前向渲染路径。
我们经常用的材质编辑器不管是unity的standard shader还是UE4中的连线的编辑器,都是经过完整的封装的。以使它能够适配引擎中的渲染方式及很多参数的调整。当我们将标准材质编译出来,可以看到一个篇幅巨长的shader框架。这篇手动写一下适配于前向渲染的不透明物体的shader。
扫盲首先提出个问题,渲染路径的概念是描述什么的?有什么作用?
首先初学shader的人肯定知道兰伯特模型,还有之前写的unity中的PBS这些光照模型解决的问题是一栈灯光照到一个模型上,产生怎样的光照效果。而现实的场景中灯光不止一个,模型更是很多。如何合理的绘制出物体所受的每个光照的影响,就变成了一个麻烦的问题。渲染路径就是解决这个问题的概念。
shader支持如下效果
1.使用unity内置的BRDF算法;
2.支持前向渲染三种灯光及其衰减;
3.灯光阴影
4.支持除去最大的逐顶点计算的4个顶点光照(点光源)
5.间接光照、环境反射
总的来讲光照分为直接光照和间接光照。而在计算直接光照是的逐像素计算很费,这时候,前向渲染也需使用逐顶点和SH球谐方式光照的计算。其中逐像素计算是需要走一边BRDF公式的,也就是每个Pass(不管是Base还是Add)。
间接光照由于每个物体只需要计算一次,就放在BasePass中。
一.BasePass与AddPass中的逐像素光照
BRDF公式(在之前的文章中写到过):
上面是一个UNITY_PBS公式的输入、其中的albedo、specular(从metallic中推)、smoothness、normal、viewDir等可以从模型及贴图中获取到,两个Pass中这些信息是相同的。BasePass与AddPass真正区分传入的参数区分是光照参数。而光照参数分为两种、一种是DirLight、一种是InDirLight。InDirLight一会儿再谈。
unity中,每个直接光照的结算就会走一遍单独的pass,而BasePass与AddPass的区别在于,BassPass会计算一个平行光照的。AddPass会用计算多余的像素光照,之后用Blend One One的模式叠加上去。
1、用宏定义区分光照类型 #pragma multi_compile_fwdadd
不同的光照中,对light的dir和attenuation的计算方法是不同的
fwdadd替换结果
#pragma multi_compile_fwdadd
#pragma multi_compile DIRECTIONAL DIRECTIONAL_COOKIE POINT SPOT POINT_COOKIE
结合autoLight.cginc中的定义 ,判断了光源的类型
// If none of the keywords are defined, assume directional?
#if !defined(POINT) && !defined(SPOT) && !defined(DIRECTIONAL) && !defined(POINT_COOKIE) && !defined(DIRECTIONAL_COOKIE)
#define DIRECTIONAL
#endif
2.不同类型光源的光照方向及光照衰减
UnityWorldSpaceLightDir(unityCG.cginc)
// Computes world space light direction, from world space position
inline float3 UnityWorldSpaceLightDir( in float3 worldPos )
{
#ifndef USING_LIGHT_MULTI_COMPILE
return _WorldSpaceLightPos0.xyz - worldPos * _WorldSpaceLightPos0.w;
#else
#ifndef USING_DIRECTIONAL_LIGHT
return _WorldSpaceLightPos0.xyz - worldPos;
#else
return _WorldSpaceLightPos0.xyz;
#endif
#endif
}
举个点光源衰减的例子(AutoLight.cginc)
#ifdef POINT
sampler2D_float _LightTexture0;
unityShadowCoord4x4 unity_WorldToLight;
# define UNITY_LIGHT_ATTENUATION(destName, input, worldPos) \
unityShadowCoord3 lightCoord = mul(unity_WorldToLight, unityShadowCoord4(worldPos, 1)).xyz; \
fixed shadow = UNITY_SHADOW_ATTENUATION(input, worldPos); \
fixed destName = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).r * shadow;
#endif
二、逐顶点光照
当光照计算超过设置的逐顶点计算的数量时,会将这是的光照信息甩入逐顶点计算的灯光中,顶点光照支持4个,再多就会甩入到SH光照。
Unity会使用VERTEXLIGHT_ON关键字查找基础渲染通道的着色器变量。
在BasePass中填入 #pragma multi_compile _ VERTEXLIGHT_ON
在UnityShaderVariables中定义 unity_4LightPosX0/unity_4LightPosY0/unity_4LightPosZ0等4个float4的数值 储存位置信息
顶点光源颜色数组 unity_LightColor[0].xyz
光源衰减 unity_4LightAtten0
内置函数
o.vertexLightColor=Shade4PointLights(
unity_4LightPosX0,unity_4LightPosY0,unity_4LightPosZ0,
unity_LightColor[0].xyz,unity_LightColor[1].xyz,
unity_LightColor[2].xyz,unity_LightColor[3].xyz,
unity_4LightAtten0,o.worldPos,o.normal);
函数定义
// Used in ForwardBase pass: Calculates diffuse lighting from 4 point lights, with data packed in a special way.
float3 Shade4PointLights (
float4 lightPosX, float4 lightPosY, float4 lightPosZ,
float3 lightColor0, float3 lightColor1, float3 lightColor2, float3 lightColor3,
float4 lightAttenSq,
float3 pos, float3 normal)
{
// to light vectors
float4 toLightX = lightPosX - pos.x;
float4 toLightY = lightPosY - pos.y;
float4 toLightZ = lightPosZ - pos.z;
// squared lengths
float4 lengthSq = 0;
lengthSq += toLightX * toLightX;
lengthSq += toLightY * toLightY;
lengthSq += toLightZ * toLightZ;
// don't produce NaNs if some vertex position overlaps with the light
lengthSq = max(lengthSq, 0.000001);
// NdotL
float4 ndotl = 0;
ndotl += toLightX * normal.x;
ndotl += toLightY * normal.y;
ndotl += toLightZ * normal.z;
// correct NdotL
float4 corr = rsqrt(lengthSq);
ndotl = max (float4(0,0,0,0), ndotl * corr);
// attenuation
float4 atten = 1.0 / (1.0 + lengthSq * lightAttenSq);
float4 diff = ndotl * atten;
// final color
float3 col = 0;
col += lightColor0 * diff.x;
col += lightColor1 * diff.y;
col += lightColor2 * diff.z;
col += lightColor3 * diff.w;
return col;
}
三、间接光照
间接光分为diffuse和specular
diffuse用来接收环境光照,用到球谐光公式,对此光照影响的因素有逐顶点光照、逐像素光照之外的灯光影响。和Lighting设置中,environment lighting参数的光照影响。
球谐光的实现原理:通过上述两方面的的光照,unity传入给shader7个float4,这定义在UnityShaderVariables中,之后通过 ShadeSH9(float4(i.normal.1)); 计算出来。
indirLight.diffuse+=max(0,ShadeSH9(float4(i.normal,1)));
specular间接高光、也就是我们常常见到的反射。这里的反射内容通过从天空求和反射捕捉中读取
Unity_GlossyEnvironmentData envData;
envData.roughness = 1 - GetSmoothness(i);
float3 reflectDir=reflect(-viewDir,i.normal);
float3 reflectDir1=BoxProjectedCubemapDirection(reflectDir, i.worldPos,unity_SpecCube0_ProbePosition,unity_SpecCube0_BoxMin, unity_SpecCube0_BoxMax);
envData.reflUVW = reflectDir1;
float3 probe0 = Unity_GlossyEnvironment(UNITY_PASS_TEXCUBE(unity_SpecCube0), unity_SpecCube0_HDR, envData);
float3 reflectDir2=BoxProjectedCubemapDirection(reflectDir, i.worldPos,unity_SpecCube1_ProbePosition,unity_SpecCube1_BoxMin, unity_SpecCube1_BoxMax);
envData.reflUVW = reflectDir2;
float3 probe1= Unity_GlossyEnvironment(UNITY_PASS_TEXCUBE_SAMPLER(unity_SpecCube1, unity_SpecCube0),unity_SpecCube0_HDR, envData);
indirLight.specular = lerp(probe1, probe0, unity_SpecCube0_BoxMin.w);
间接光照要考虑到AO,
float AO = GetAO(i);
indirLight.diffuse*=AO;
indirLight.specular*=AO;
五、其他的一些
1.阴影
unity中的屏幕空间阴影流程:
1.计算场景深度,也是前向渲染的第一步。目的、从深度缓冲得到世界坐标。
2.阴影投射,灯光角度计算场景深度。所见到的深度,就是灯光照亮的区域。否则就是阴影。
3.collectShadow收集屏幕隐形,从场景的世界坐标向光源投射,查看在该光源的深度是否大于,阴影投射的深度。如果大于就说明该区域处在阴影之中。得到shadowMap
casterShadow,简单了直接用FallBack“Diffuse"
关于接收阴影的宏
TRANSFER_SHADOW(o);
SHADOW_COORDS(1)
SHADOW_ATTENUATION(i);
UNITY_LIGHT_ATTENUATION(attenuation,i,i.worldPos);
//base pass
#pragma multi_compile _ SHADOWS_SCREEN
//add pass
#pragma multi_compile_fwdadd_fullshadows
2.自发光
不用任何计算,直接加载最后的输出上
fixed3 emissionCol=GetEmission(i);
fixed3 finalCol=BRDF+emissionCol;
shader
尽量将shader部分写的清晰,可能导致不够精简,但没有关系unity编译会将重复代码合并;
就像surfaceShader,可以直接将我们的贴图参数传入GetAlbedo()/GetMetallic()中;
Shader"myshader/forwardGoOver"{
Properties{
_mainTex("MainTex",2D)="white"{}
_Tine("Time",Color)=(1,1,1,1)
_NormalScale("NormalScale",float)=1
[NoScaleOffset]_MaskMap("MaskMap",2D)="white"{}
[NoScaleOffset]_MetallicMap("MetallicMap",2D)="black"{}
_Metallic("Metallic",Range(0,1))=0
_Smoothness("smoothness",Range(0,1))=0
_AoScale("AoScale",Range(0,1))=1
[NoScaleOffset]_EmissionMap("EmissionMap",2D)="black"
}
SubShader{
Tags{"RenderType"="Opaque"}
pass{
Tags{"LightMode"="ForwardBase"}
CGPROGRAM
#pragma target 3.0
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile _ VERTEXLIGHT_ON
#pragma multi_compile _ SHADOWS_SCREEN
#define FORWARD_BASE_PASS
#include"ForwardLighting.cginc"
ENDCG
}
pass{
Tags{"LightMode"="ForwardAdd"}
Blend one one
CGPROGRAM
#pragma target 3.0
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdadd_fullshadows
#include"ForwardLighting.cginc"
ENDCG
}
}
Fallback "Diffuse"
}
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
#if !defined(MY_LIGHTING_INCLUDED)
#define MY_LIGHTING_INCLUDED
#include"UnityPBSLighting.cginc"
#include"AutoLight.cginc"
#include"UnityStandardUtils.cginc"
sampler2D _mainTex;
float4 _mainTex_ST;
float3 _Tine;
sampler2D _NormalMap;
float _NormalScale;
sampler2D _MetallicMap;
float _Metallic;
float _Smoothness;
float _AoScale;
sampler2D _EmissionMap;
struct a2v{
float4 vertex:POSITION;
float3 normal:NORMAL;
float2 uv:TEXCOORD0;
float2 uv1:TEXCOORD1;
float4 tangent:TANGENT;
};
struct v2f{
float4 pos:SV_POSITION;
float3 worldPos:TEXCOORD0;
float3 normal:TEXCOORD1;
float4 tangent:TEXCOORD2;
float2 uv:TEXCOORD3;
float2 uv2:TEXCOORD6;
#if defined(VERTEXLIGHT_ON)
float3 vertexLightColor:TEXCOORD4;
#endif
SHADOW_COORDS(5)
};
void ComputevertexLight(inout v2f o){
#if defined(VERTEXLIGHT_ON)
o.vertexLightColor=Shade4PointLights(
unity_4LightPosX0,unity_4LightPosY0,unity_4LightPosZ0,
unity_LightColor[0].xyz,unity_LightColor[1].xyz,
unity_LightColor[2].xyz,unity_LightColor[3].xyz,
unity_4LightAtten0,o.worldPos,o.normal);
#endif
}
v2f vert(a2v v){
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
o.worldPos=mul(unity_ObjectToWorld,v.vertex).xyz;
o.normal=UnityObjectToWorldNormal(v.normal);
o.tangent=float4(UnityObjectToWorldDir(v.tangent.xyz),v.tangent.w);
o.uv=TRANSFORM_TEX(v.uv,_mainTex);
o.uv2=v.uv1;
TRANSFER_SHADOW(o);
ComputevertexLight(o);
return o;
}
float3 GetAlbedo(v2f i){
return tex2D(_mainTex,i.uv).xyz * _Tine.xyz;
}
float GetMetallic(v2f i){
return tex2D(_MetallicMap,i.uv).r + _Metallic;
}
float GetSmoothness(v2f i){
return _Smoothness;
}
float3 GetNormal(v2f i){
float3 worldNormal=normalize(i.normal);
float3 worldTangent=normalize(i.tangent.xyz);
float3 worldBinormal=cross(worldNormal,worldTangent) * i.tangent.w * unity_WorldTransformParams.w;
float3 tangentNormal=UnpackScaleNormal(tex2D(_NormalMap,i.uv),_NormalScale);
return float3(
tangentNormal.x*worldTangent+
tangentNormal.y*worldBinormal+
tangentNormal.z*worldNormal
);
}
float GetAO(v2f i){
return 1.;
}
UnityLight GetdirLight(v2f i){
UnityLight dirLight;
dirLight.dir=UnityWorldSpaceLightDir(i.worldPos);
UNITY_LIGHT_ATTENUATION(attenuation,i,i.worldPos);
dirLight.color=_LightColor0.xyz * attenuation;
return dirLight;
}
UnityIndirect GetindirLight(v2f i,float3 viewDir){
UnityIndirect indirLight;
indirLight.diffuse=0;
indirLight.specular=0;
#if defined(VERTEXLIGHT_ON)
indirLight.diffuse+=i.vertexLightColor;
#endif
#if defined(FORWARD_BASE_PASS)
indirLight.diffuse+=max(0,ShadeSH9(float4(i.normal,1)));
Unity_GlossyEnvironmentData envData;
envData.roughness = 1 - GetSmoothness(i);
float3 reflectDir=reflect(-viewDir,i.normal);
float3 reflectDir1=BoxProjectedCubemapDirection(reflectDir, i.worldPos,unity_SpecCube0_ProbePosition,unity_SpecCube0_BoxMin, unity_SpecCube0_BoxMax);
envData.reflUVW = reflectDir1;
float3 probe0 = Unity_GlossyEnvironment(UNITY_PASS_TEXCUBE(unity_SpecCube0), unity_SpecCube0_HDR, envData);
float3 reflectDir2=BoxProjectedCubemapDirection(reflectDir, i.worldPos,unity_SpecCube1_ProbePosition,unity_SpecCube1_BoxMin, unity_SpecCube1_BoxMax);
envData.reflUVW = reflectDir2;
float3 probe1= Unity_GlossyEnvironment(UNITY_PASS_TEXCUBE_SAMPLER(unity_SpecCube1, unity_SpecCube0),unity_SpecCube0_HDR, envData);
indirLight.specular = lerp(probe1, probe0, unity_SpecCube0_BoxMin.w);
float AO = GetAO(i);
indirLight.diffuse*=AO;
indirLight.specular*=AO;
#endif
return indirLight;
}
float3 GetEmission(v2f i){
float3 emission=float3(0,0,0);
#if defined(FORWARD_BASE_PASS)
emission=tex2D(_EmissionMap,i.uv).xyz;
#endif
return emission;
}
fixed4 frag(v2f i):SV_TARGET{
float3 albedo=GetAlbedo(i);
float metallic=GetMetallic(i);
float3 specular;
float OneMinuseReflectivity;
albedo = DiffuseAndSpecularFromMetallic(albedo,metallic,specular,OneMinuseReflectivity);
float smoothness=GetSmoothness(i);
float3 normal=GetNormal(i);
float3 viewDir=normalize(UnityWorldSpaceViewDir(i.worldPos));
UnityLight dirLight=GetdirLight(i);
UnityIndirect indirLight=GetindirLight(i,viewDir);
fixed3 BRDF=UNITY_BRDF_PBS(albedo,specular,OneMinuseReflectivity,smoothness,normal,viewDir,dirLight,indirLight);
fixed3 emissionCol=GetEmission(i);
fixed3 finalCol=BRDF+emissionCol;
return fixed4(finalCol,1.0);
}
#endif