【Unity Shader】剖析Unity Surface Shader背后机制(一)

时间:2021-10-25 04:37:55

概要

Unity自3.x起,推出了surface shader功能,极大地简化了shader的编写,尤其是光照处理这块。surface shader说白了就是一套代码生成器,最终还是转换为vertex/fragment shader,优点在于隐藏了许多很少会被改动,然而工作量却巨大的细节,例如处理不同光照类型,lightmap,阴影等。开放给开发者的是最多被修改的一些参数,例如颜色,法线等。当然还提供了很多参数和方法,可以让开发者自定义一些功能,例如光照模型,改变顶点等。
这篇文章不是讲解surface shader怎么使用,而是探究unity在背后到底做了些什么?怎么做的?本文接下去都会围绕这两个问题展开阐述。如果是初次接触surface shader,可以看下官方文档。另外本文针对的版本是unity 5之前的版本,unity 5加入了基于物理的渲染(PBR),surface shader也加入了对PBR的支持,还没仔细看过,稍后可能会更新到unity 5的内容。

Unity做了些什么

下面就是一个最简单的surface shader

Shader "Custom/Lambert" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 200

CGPROGRAM
#pragma surface surf Lambert

sampler2D _MainTex;

struct Input {
float2 uv_MainTex;
};

void surf (Input IN, inout SurfaceOutput o) {
half4 c = tex2D (_MainTex, IN.uv_MainTex);
o.Albedo = c.rgb;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}

这个shader很简单,定义了surface函数为surf,使用Lambert光照模型,定义了包含uv_MainTex的Input结构体。关于SurfaceOutput和Lambert的定义可以在Lighting.cginc中找到,定义如下:

struct SurfaceOutput {
fixed3 Albedo;
fixed3 Normal;
fixed3 Emission;
half Specular;
fixed Gloss;
fixed Alpha;
};

inline fixed4 LightingLambert (SurfaceOutput s, fixed3 lightDir, fixed atten)
{
fixed diff = max (0, dot (s.Normal, lightDir));

fixed4 c;
c.rgb = s.Albedo * _LightColor0.rgb * (diff * atten * 2);
c.a = s.Alpha;
return c;
}

接下去unity做了什么呢?首先他按不同的渲染路径生成代码,一般情况下是forward pathdeferred path。作为一个移动平台开发者,目前的设备性能还不支持deferred rendering,所以这里只讨论forward rendering的情况。
在forward path下,unity会生成2个pass:forwardbase和forwardadd。

  • forwardbase是基本的pass,必须得有,他处理场景中最重要的平行光,顶点光照,球谐光照,投射阴影和lightmap。
  • forwardadd处理额外的像素光照,当光源被设为Important或者QualitySettings里Pixel Light Count里允许的光源数量都将产生一个额外的forwardadd pass来计算光照,将最终的结果以叠加的方式,混合到buffer里去。

具体的流程如下图所示:
【Unity Shader】剖析Unity Surface Shader背后机制(一)

看了这张流程图后,对unity在背后做了些什么事情有一个大概的直观的概念。在surface shader中,你只需要在surf函数里描述,你的物体看起来是什么样的(填充SurfaceOutput结构体),以什么样的方式照亮(选择光照模型,unity内置了Lambert和BlinnPhong,当然你也可以自定义,有兴趣的可以戳这里)。有了这些信息,unity把SurfaceOutput作为输入参数,传入对应的光照方程来计算最终结果。unity在背后把脏活累活都给你做了:为你处理了顶点,法线,视点变换;纹理坐标计算;lightmap的处理;不同光源类型的光照与阴影计算等。
具体的代码如下。看到这茫茫多一坨先别慌,看不懂也没关系,后面我会详细分析每个细节,现在有个大致的印象就可以了。

Shader "Custom/Lambert" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 200


// ------------------------------------------------------------
// Surface shader code generated out of a CGPROGRAM block:


// ---- forward rendering base pass:
Pass {
Name "FORWARD"
Tags { "LightMode" = "ForwardBase" }

CGPROGRAM
// compile directives
#pragma vertex vert_surf
#pragma fragment frag_surf
#pragma multi_compile_fwdbase
#include "HLSLSupport.cginc"
#include "UnityShaderVariables.cginc"
#define UNITY_PASS_FORWARDBASE
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"

#define INTERNAL_DATA
#define WorldReflectionVector(data,normal) data.worldRefl
#define WorldNormalVector(data,normal) normal

// Original surface shader snippet:
#line 7 ""
#ifdef DUMMY_PREPROCESSOR_TO_WORK_AROUND_HLSL_COMPILER_LINE_HANDLING
#endif

//#pragma surface surf Lambert exclude_path:prepass

sampler2D _MainTex;

struct Input {
float2 uv_MainTex;
};

void surf (Input IN, inout SurfaceOutput o) {
half4 c = tex2D (_MainTex, IN.uv_MainTex);
o.Albedo = c.rgb;
o.Alpha = c.a;
}


// vertex-to-fragment interpolation data
#ifdef LIGHTMAP_OFF
struct v2f_surf {
float4 pos : SV_POSITION;
float2 pack0 : TEXCOORD0;
fixed3 normal : TEXCOORD1;
fixed3 vlight : TEXCOORD2;
LIGHTING_COORDS(3,4)
};
#endif
#ifndef LIGHTMAP_OFF
struct v2f_surf {
float4 pos : SV_POSITION;
float2 pack0 : TEXCOORD0;
float2 lmap : TEXCOORD1;
LIGHTING_COORDS(2,3)
};
#endif
#ifndef LIGHTMAP_OFF
float4 unity_LightmapST;
#endif
float4 _MainTex_ST;

// vertex shader
v2f_surf vert_surf (appdata_full v) {
v2f_surf o;
o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
o.pack0.xy = TRANSFORM_TEX(v.texcoord, _MainTex);
#ifndef LIGHTMAP_OFF
o.lmap.xy = v.texcoord1.xy * unity_LightmapST.xy + unity_LightmapST.zw;
#endif
float3 worldN = mul((float3x3)_Object2World, SCALED_NORMAL);
#ifdef LIGHTMAP_OFF
o.normal = worldN;
#endif

// SH/ambient and vertex lights
#ifdef LIGHTMAP_OFF
float3 shlight = ShadeSH9 (float4(worldN,1.0));
o.vlight = shlight;
#ifdef VERTEXLIGHT_ON
float3 worldPos = mul(_Object2World, v.vertex).xyz;
o.vlight += Shade4PointLights (
unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,
unity_LightColor[0].rgb, unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb,
unity_4LightAtten0, worldPos, worldN );
#endif // VERTEXLIGHT_ON
#endif // LIGHTMAP_OFF

// pass lighting information to pixel shader
TRANSFER_VERTEX_TO_FRAGMENT(o);
return o;
}
#ifndef LIGHTMAP_OFF
sampler2D unity_Lightmap;
#ifndef DIRLIGHTMAP_OFF
sampler2D unity_LightmapInd;
#endif
#endif

// fragment shader
fixed4 frag_surf (v2f_surf IN) : SV_Target {
// prepare and unpack data
#ifdef UNITY_COMPILER_HLSL
Input surfIN = (Input)0;
#else
Input surfIN;
#endif
surfIN.uv_MainTex = IN.pack0.xy;
#ifdef UNITY_COMPILER_HLSL
SurfaceOutput o = (SurfaceOutput)0;
#else
SurfaceOutput o;
#endif
o.Albedo = 0.0;
o.Emission = 0.0;
o.Specular = 0.0;
o.Alpha = 0.0;
o.Gloss = 0.0;
#ifdef LIGHTMAP_OFF
o.Normal = IN.normal;
#endif

// call surface function
surf (surfIN, o);

// compute lighting & shadowing factor
fixed atten = LIGHT_ATTENUATION(IN);
fixed4 c = 0;

// realtime lighting: call lighting function
#ifdef LIGHTMAP_OFF
c = LightingLambert (o, _WorldSpaceLightPos0.xyz, atten);
#endif // LIGHTMAP_OFF || DIRLIGHTMAP_OFF
#ifdef LIGHTMAP_OFF
c.rgb += o.Albedo * IN.vlight;
#endif // LIGHTMAP_OFF

// lightmaps:
#ifndef LIGHTMAP_OFF
#ifndef DIRLIGHTMAP_OFF
// directional lightmaps
fixed4 lmtex = tex2D(unity_Lightmap, IN.lmap.xy);
fixed4 lmIndTex = tex2D(unity_LightmapInd, IN.lmap.xy);
half3 lm = LightingLambert_DirLightmap(o, lmtex, lmIndTex, 0).rgb;
#else // !DIRLIGHTMAP_OFF
// single lightmap
fixed4 lmtex = tex2D(unity_Lightmap, IN.lmap.xy);
fixed3 lm = DecodeLightmap (lmtex);
#endif // !DIRLIGHTMAP_OFF

// combine lightmaps with realtime shadows
#ifdef SHADOWS_SCREEN
#if (defined(SHADER_API_GLES) || defined(SHADER_API_GLES3)) && defined(SHADER_API_MOBILE)
c.rgb += o.Albedo * min(lm, atten*2);
#else
c.rgb += o.Albedo * max(min(lm,(atten*2)*lmtex.rgb), lm*atten);
#endif
#else // SHADOWS_SCREEN
c.rgb += o.Albedo * lm;
#endif // SHADOWS_SCREEN
c.a = o.Alpha;
#endif // LIGHTMAP_OFF

return c;
}

ENDCG

}

// ---- forward rendering additive lights pass:
Pass {
Name "FORWARD"
Tags { "LightMode" = "ForwardAdd" }
ZWrite Off Blend One One Fog { Color (0,0,0,0) }

CGPROGRAM
// compile directives
#pragma vertex vert_surf
#pragma fragment frag_surf
#pragma multi_compile_fwdadd
#include "HLSLSupport.cginc"
#include "UnityShaderVariables.cginc"
#define UNITY_PASS_FORWARDADD
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"

#define INTERNAL_DATA
#define WorldReflectionVector(data,normal) data.worldRefl
#define WorldNormalVector(data,normal) normal

// Original surface shader snippet:
#line 7 ""
#ifdef DUMMY_PREPROCESSOR_TO_WORK_AROUND_HLSL_COMPILER_LINE_HANDLING
#endif

//#pragma surface surf Lambert exclude_path:prepass

sampler2D _MainTex;

struct Input {
float2 uv_MainTex;
};

void surf (Input IN, inout SurfaceOutput o) {
half4 c = tex2D (_MainTex, IN.uv_MainTex);
o.Albedo = c.rgb;
o.Alpha = c.a;
}


// vertex-to-fragment interpolation data
struct v2f_surf {
float4 pos : SV_POSITION;
float2 pack0 : TEXCOORD0;
fixed3 normal : TEXCOORD1;
half3 lightDir : TEXCOORD2;
LIGHTING_COORDS(3,4)
};
float4 _MainTex_ST;

// vertex shader
v2f_surf vert_surf (appdata_full v) {
v2f_surf o;
o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
o.pack0.xy = TRANSFORM_TEX(v.texcoord, _MainTex);
o.normal = mul((float3x3)_Object2World, SCALED_NORMAL);
float3 lightDir = WorldSpaceLightDir( v.vertex );
o.lightDir = lightDir;

// pass lighting information to pixel shader
TRANSFER_VERTEX_TO_FRAGMENT(o);
return o;
}

// fragment shader
fixed4 frag_surf (v2f_surf IN) : SV_Target {
// prepare and unpack data
#ifdef UNITY_COMPILER_HLSL
Input surfIN = (Input)0;
#else
Input surfIN;
#endif
surfIN.uv_MainTex = IN.pack0.xy;
#ifdef UNITY_COMPILER_HLSL
SurfaceOutput o = (SurfaceOutput)0;
#else
SurfaceOutput o;
#endif
o.Albedo = 0.0;
o.Emission = 0.0;
o.Specular = 0.0;
o.Alpha = 0.0;
o.Gloss = 0.0;
o.Normal = IN.normal;

// call surface function
surf (surfIN, o);
#ifndef USING_DIRECTIONAL_LIGHT
fixed3 lightDir = normalize(IN.lightDir);
#else
fixed3 lightDir = IN.lightDir;
#endif
fixed4 c = LightingLambert (o, lightDir, LIGHT_ATTENUATION(IN));
c.a = 0.0;
return c;
}

ENDCG

}

// ---- end of surface shader generated code

#LINE 23

}
FallBack "Diffuse"
}

想知道unity到底怎么做的,且听下回分解。