unityunity3d shaderr里 dot(lightCoord, lightCoord).rr

Unity3D中的Shader
挥剑对风尘
发布时间: 10:33:01
简单的说,Shader是为渲染管线中的特定处理阶段提供算法的一段代码。Shader是伴随着可编程渲染管线出现的,从而可以对渲染过程加以控制。
1. Unity提供了很多内建的Shader,这些可以从官网下载,打开looking for older version的链接就能看到Build-in shaders。选择合适的Shader很重要,以下是开销从低到高的排序:
(1)Unlit:仅使用纹理颜色,不受光照影响
(2)VertexLit:顶点光照
(3)Diffuse:漫反射
(4)Specular:在漫反射基础上增加高光计算
(5)Normal mapped:法线贴图,增加了一张法线贴图和几个着色器指令
(6)Normal Mapped Specular:带高光法线贴图
(7)Parallax Normal Mapped:视差法线贴图,增加了视差贴图的计算开销
(8)Parallax Normal Mapped Specular:带高光视差法线贴图
& 对于现在流行的移动平台游戏,Unity提供了几种专门的着色器放在Shader-&Mobile下,它们是专门优化过的。
2. 在Unity中,可以编写3种类型的Shader:
表面着色器(Surface Shaders):最常用的Shader,可以与灯光、阴影、投影器交互,以Cg/HLSL语言进行编写,不涉及光照时尽量不要使用。
顶点和片段着色器(Vertex and Fragment Shaders):全屏图像效果,代码比较多,以Cg/HLSL编写,难以和光照交互。
固定功能管线着色器(Fixed Function Shaders):游戏要运行在不支持可编程管线的老旧机器上时,需要用ShaderLab语言来编写。
无论编写哪种Shader,实际的Shader代码都需要嵌入ShaderLab代码中,Unity通过ShaderLab代码来组织Shader结构。
下面是我新建的一个Shader的默认内容:
Shader "Custom/TestShader" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
SubShader {
Tags { "RenderType"="Opaque" }
#pragma surface surf Lambert
sampler2D _MainT
struct Input {
float2 uv_MainT
void surf (Input IN, inout SurfaceOutput o) {
half4 c = tex2D (_MainTex, IN.uv_MainTex);
o.Albedo = c.
o.Alpha = c.a;
FallBack "Diffuse"
Properties:用来定义着色器中使用的贴图资源或者数值参数等,这里定义了一个Base (RGB)的2D纹理。
SubShader:一个着色器包含的一个或多个子着色器。Unity从上到下遍历子着色器,找到第一个能被设备支持的着色器。
FallBack:备用着色器,一个对硬件要求最低的Shader名字。
(1)Properties定义的属性
名称("显示名称", Vector) = 默认向量值,一个四维向量
名称("显示名称", Color) = 默认颜色值,一个颜色(取值0~1的四维向量)属性
名称("显示名称", Float) = 默认浮点数值,一个浮点数
名称("显示名称", Range(min,max)) = 默认浮点数值,一个浮点数,取值min~max
名称("显示名称", 2D) = 默认贴图名称{选项},一个2D纹理属性
名称("显示名称", Rect) = 默认贴图名称{选项},一个矩形纹理属性(非2的n次幂)
名称("显示名称", Cube) = 默认贴图名称{选项},一个立方体纹理属性
选项指的是一些纹理的可选参数,包括:
TexGen:纹理生成模式,可以是ObjectLinear、EyeLinear、SphereMap、CubeReflect、CubeNormal中的一种。如果使用了自定义的顶点程序,这些参数会被忽略。
LightmapMode:纹理将受渲染器的光照贴图参数影响。纹理将不会从材质中获取,而是取自渲染器的设置。
示例如下:
Properties {
_RefDis ("Reflect Distance", Range(0, 1)) = 0.3 //范围数值
_Color ("Reflect Color", Color) = (.34, .85, .92, 1) //颜色
_MainTex ("Reflect Color", 2D) = "white"{} //纹理
常用的变量类型如下:
颜色和向量:float4, half4, fixed4
范围和浮点数:float, half, fixed
2D纹理贴图:sampler2D
Cubemap:samplerCUBE
3D纹理贴图:sampler3D
(2)SubShader,子着色器由标签(可选)、通用状态(可选)、Pass列表组成。使用子着色器渲染时,每个pass都会渲染一次对象,所以应尽量减少Pass数量。
(3)Category,分类用于提供让子着色器继承的命令。
3. 表面着色器,使用Cg/HLSL编写,然后嵌在ShaderLab的结构代码中使用。仅需编写最关键的表面函数,其余代码由Unity生成,包括适配各种光源类型、渲染实时阴影以及集成到前向/延迟渲染管线中。如果你需要的效果与光照无关,最好不要使用表面着色器,否则会进行很多不必要的光照计算。使用#pragma surface...来指明是一个表面着色器。输入结构体Input一般包含必须的纹理坐标,还可以在输入结构中加入一些附加数据。
#pragma surface surf Lambert
sampler2D _MainTex;
fixed4 _Color;
struct Input {
float2 uv_MainTex;
void surf (Input IN, inout SurfaceOutput o) {
half4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = c.rgb;
o.Alpha = c.a;
SurfaceOutpu描述了表面的各种参数,它的标准结构如下:
struct SurfaceOutput{
4. 顶点和片段着色器,运行于具有可编程渲染管线的硬件上,它包括顶点程序和片段程序。使用该着色器渲染时,固定功能管线将会关闭,即编写好的顶点程序替代原有的3D变换、光照、纹理坐标生成等功能,片段程序会替换掉SetTexture命令中的纹理混合模式。代码使用Cg/HLSL编写,放在Pass命令中,格式如下:
SubShader{
#pragma vertex vert
#pragma fragment frag
&编译命令说明如下:
#pragma vertex name-----------------------------将函数name的代码编译成顶点程序
#pragma fragment name---------------------------将函数name的代码编译成片段程序
#pragma geometry name---------------------------将函数name的代码编译成DX10的几何着色器
#pragma hull name-------------------------------将函数name的代码编译成DX11的hull着色器
#pragma domain name-----------------------------将函数name的代码编译成DX11的domain着色器
#pragma fragmentoption option-------------------添加选项到编译的OpenGL片段程序,对于顶点程序或编译目标不是OpenGL的无效
#pragma target name-----------------------------设置着色器的编译目标
#pragma only_renderers space separated names----仅编译到指定的渲染平台
#pragma exclude_renderers space separated names-不编译到指定的渲染平台
#pragma glsl------------------------------------为桌面系统的OpenGL进行编译时,将Cg/HLSL代码转换成GLSL代码
#pragma glsl_no_auto_normalization--------------编译到移动平台GLSL时,关闭顶点着色器中对法线和切线进行自动规范化
示例代码,使用命令:
Shader "Custom/Shader1" {
Properties {
_Color("Main Color", Color) = (1,1,1, 0.5)
_SpecColor("Spec Color", Color) = (1,1,1,1)
_Emission("Emmisive Color", Color) = (0,0,0,0)
_Shininess("Shininess", Range(0.01, 1)) = 0.7
_MainTex("Base (RGB)", 2D) = "white" {}
SubShader {
Diffuse[_Color]
Ambient[_Color]
Shininess[_Shininess]
Specular[_SpecColor]
Emission[_Emission]
Lighting On
SeparateSpecular
SetTexture[_MainTex]{
constantColor[_Color]
Combine texture * primary DOUBLE, texture * constant
FallBack "Diffuse"
&示例代码,使用Cg。其中的包含文件可以在/Data/CGIncludes/目录下找到。
Shader "Custom/Shader2" {
//定义属性(变量)
Properties {
_MainTex ("Texture", 2D) = "white" {} //纹理
_Color ("Main Color", Color) = (1,1,1,0.5) //颜色
//子着色器
SubShader {
//每个Pass中,对象几何体都被渲染一次
CGPROGRAM //Cg代码开始
#pragma vertex vert //将函数vert编译为顶点程序
#pragma fragment frag //将函数frag编译为片段程序
//包含一个内置的cg文件,提供了常用的声明和函数,比如appdata_base结构
#include "UnityCG.cginc"
float4 _C //变量,颜色的向量表示
sampler2D _MainT
float4 _MainTex_ST;
//定义一个结构体v2f
struct v2f{
float4 pos:SV_POSITION;
float2 uv:TEXCOORD0;
//顶点处理程序
v2f vert(appdata_base v)
//3D坐标被投影到2D窗口中,与矩阵Model-View-Projection相乘
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
//片段处理程序
half4 frag(v2f i):COLOR
half4 texcol = tex2D(_MainTex, i.uv);
//自定义颜色_Color与纹理的融合
return texcol * _C
ENDCG //Cg代码结束
//备用着色器
FallBack "VertexLit"
上面的例子用到了一些内置的变量,有下面这些:
UNITY_MATRIX_MVP--------------------------当前的model*view*projection矩阵
UNITY_MATRIX_MV---------------------------当前的model*view矩阵
UNITY_MATRIX_V----------------------------当前的view矩阵
UNITY_MATRIX_P----------------------------当前的projection矩阵
UNITY_MATRIX_VP---------------------------当前的view*projection矩阵
UNITY_MATRIX_T_MV-------------------------model*view矩阵的转置矩阵
UNITY_MATRIX_IT_MV------------------------model*view矩阵的转置逆矩阵
UNITY_MATRIX_TEXTURE0
UNITY_MATRIX_TEXTURE1
UNITY_MATRIX_TEXTURE2
UNITY_MATRIX_TEXTURE3---------------------纹理变换矩阵
UNITY_LIGHTMODEL_AMBIENT------------------当前的环境光颜色
下面是官方文档中的一个例子,可以产生不同颜色交错的效果:
Shader "Custom/Bars" {
SubShader {
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct vertOut {
float4 pos:SV_POSITION;
float4 scrP
vertOut vert(appdata_base v) {
o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
//ComputeScreenPos将返回片段着色器的屏幕位置scrPos
o.scrPos = ComputeScreenPos(o.pos);
fixed4 frag(vertOut i) : COLOR0 {
float2 wcoord = (i.scrPos.xy/i.scrPos.w);
//改变50可以调整间距
if (fmod(50.0*wcoord.x,2.0)&1.0) {
color = fixed4(wcoord.xy,0.6,1.0);//这里可以改变颜色
color = fixed4(0.1,0.3,0.7,1.0);//这里可以改变颜色
下面的例子来自官方手册,棋盘格效果:
Shader "Custom/Chess" {
SubShader {
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
//输入顶点结构体,包含位置和颜色
struct vertexInput {
float4 vertex : POSITION;
float4 texcoord0 : TEXCOORD0;
//片段结构体,包含位置和颜色
struct fragmentInput{
float4 position : SV_POSITION;
float4 texcoord0 : TEXCOORD0;
//顶点处理
fragmentInput vert(vertexInput i){
o.position = mul (UNITY_MATRIX_MVP, i.vertex);
o.texcoord0 = i.texcoord0;
//片段处理
float4 frag(fragmentInput i) : COLOR {
//fmod用来取余数,物体表面X方向被分成了8/2=4个区间
//X坐标对2求余,所以这里用1来作为比较,黑、白各占一半
if ( fmod(i.texcoord0.x*8.0,2.0) & 1.0
if ( fmod(i.texcoord0.y*8.0,2.0) & 1.0 )
color = float4(1.0,1.0,1.0,1.0);//白色
color = float4(0.0,0.0,0.0,1.0);//黑色
if ( fmod(i.texcoord0.y*8.0,2.0) & 1.0 )
color = float4(1.0,1.0,1.0,1.0);//白色
color = float4(0.0,0.0,0.0,1.0);//黑色
FallBack "Diffuse"
相同效果的简化代码:
Shader "Custom/ChessOpt" {
SubShader {
#pragma vertex vert_img
#pragma fragment frag
#include "UnityCG.cginc"
float4 frag(v2f_img i) : COLOR {
bool p = fmod(i.uv.x*8.0,2.0) & 1.0;
bool q = fmod(i.uv.y*8.0,2.0) & 1.0;
return float4(float3((p && q) || !(p || q)),1.0);
上一个例子中有个texcoord0变量,它的x和y的值都是从0到1的,刚好映射到一张特殊的纹理上。
来源:/shihui142857/p/3848580.html1124人阅读
Unity Shader(2)
结尾,给出了一个最简单的Diffuse的surface shader翻译成vertex/fragment shader之后的代码。乍看上去可能一头雾水,下面将会一一分解。
整体来看,相较于surface shader,unity自动生成了两个pass(ForwardBase,ForwardAdd),这两个pass的作用,在上一篇中也已经说明。接下去,对照着代码,我们来分解下unity具体做了些什么。
ForwardBase
Tags { "LightMode" = "ForwardBase" }
告诉渲染管线,这个pass作为ForwardBase处理,缺少它将画不出任何东西。
#pragma multi_compile_fwdbase
unity官方文档,只有multi_compile的说明,对于multi_compile_fwdbase可谓只字未提,网上也是一语带过(虽然不甚详细,但是非常感谢作者分享)。后来打开unity shader面板的Variants里看到:
DIRECTIONAL LIGHTMAP_OFF DIRLIGHTMAP_OFF SHADOWS_OFF
DIRECTIONAL LIGHTMAP_ON DIRLIGHTMAP_OFF SHADOWS_OFF
DIRECTIONAL LIGHTMAP_ON DIRLIGHTMAP_ON SHADOWS_OFF
DIRECTIONAL LIGHTMAP_OFF DIRLIGHTMAP_OFF SHADOWS_SCREEN
DIRECTIONAL LIGHTMAP_ON DIRLIGHTMAP_OFF SHADOWS_SCREEN
DIRECTIONAL LIGHTMAP_ON DIRLIGHTMAP_ON SHADOWS_SCREEN
DIRECTIONAL LIGHTMAP_OFF DIRLIGHTMAP_OFF SHADOWS_OFF VERTEXLIGHT_ON
DIRECTIONAL LIGHTMAP_OFF DIRLIGHTMAP_OFF SHADOWS_SCREEN VERTEXLIGHT_ON
DIRECTIONAL LIGHTMAP_OFF DIRLIGHTMAP_OFF SHADOWS_SCREEN SHADOWS_NATIVE
DIRECTIONAL LIGHTMAP_ON DIRLIGHTMAP_OFF SHADOWS_SCREEN SHADOWS_NATIVE
DIRECTIONAL LIGHTMAP_ON DIRLIGHTMAP_ON SHADOWS_SCREEN SHADOWS_NATIVE
DIRECTIONAL LIGHTMAP_OFF DIRLIGHTMAP_OFF SHADOWS_SCREEN SHADOWS_NATIVE VERTEXLIGHT_ON
很多看过unity生成的vertex/fragment代码的同学,会感到很困惑,代码里充斥着各种条件编译代码,然而unity也没有给出文档,到底有哪些keyword?各自的作用是什么?
看到这个就非常清楚了,unity为forwardbase pass定义的keyword都在这里,而multi_compile_fwdbase就是unity专门为forwardbase预定义的multi_compile。
如果再打开unity最终编译成的glsl代码的话,可以看到,unity为每一组keywards都生成了单独的代码。
如果缺少这行代码的话,那么unity默认会编译
DIRECTIONAL LIGHTMAP_ON DIRLIGHTMAP_ON SHADOWS_OFF
这一组条件,碰到其他的情况,那么渲染就会出错。
我们看到,上述每一组条件,都是DIRECTIONAL,那么如果场景中没有平行光呢?点光源或者聚光灯还会起作用么?
答案是不会。forwardbase pass只能以逐像素的方式处理平行光,点光源和聚光灯都会被忽略掉,对应的_LightColor0都将是黑色。注意上面黑体的逐像素,因为可能有人会说,我场景里只有一个点光源,可以把场景照亮。是的,但它是以逐顶点的方式照亮的,后面将会细说。
#ifdef LIGHTMAP_OFF
struct v2f_surf {
float4 pos : SV_POSITION;
float2 pack0 : TEXCOORD0;
fixed3 normal : TEXCOORD1;
fixed3 vlight : TEXCOORD2;
LIGHTING_COORDS(3,4)
#ifndef LIGHTMAP_OFF
struct v2f_surf {
float4 pos : SV_POSITION;
float2 pack0 : TEXCOORD0;
float2 lmap : TEXCOORD1;
LIGHTING_COORDS(2,3)
unity定义了v2f结构,有无lightmap的两个版本。
LIGHTING_COORDS(3,4)
这个定义在AutoLight.cginc中,它定义了光照和阴影坐标。因为forwardbase只支持平行光,因为平行光的特性,受到的光照强度是一样的,所以并不需要光照坐标,如果投射阴影的话,只需要阴影坐标来采样shadow map。(稍后再forwardadd章节中,会详细讨论不同光源的光照计算)
现在只要知道,这个宏的定义类似如下所示。参数3,4就是下个可用的TEXCOORD序号。
#define LIGHTING_COORDS(idx1,idx2) float3 _LightCoord : TEXCOORD##idx1; SHADOW_COORDS(idx2)
接下去就是填充v2f结构,计算齐次空间坐标,uv坐标,normal或者lightmap的uv坐标。就如我们平时写vertex/fragment shader那样,就不累述了。
float3 shlight = ShadeSH9 (float4(worldN,1.0));
这里是计算球谐光照,没有作为像素光照和顶点光照处理的光源,都在这里计算,包括light probe。
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 )
这里计算顶点光照,unity只能处理4个顶点光源。unity在上层处理掉了很多东西。如果光源影响到该物体,那么光源的属性(位置,原色,衰减)都会被设置,反之则会直接被忽略。具体计算过程在UnityCG.cginc中,如下所示:
float3 Shade4PointLights (
float4 lightPosX, float4 lightPosY, float4 lightPosZ,
float3 lightColor0, float3 lightColor1, float3 lightColor2, float3 lightColor3,
float4 lightAttenSq,
float3 pos, float3 normal)
float4 toLightX = lightPosX - pos.x;
float4 toLightY = lightPosY - pos.y;
float4 toLightZ = lightPosZ - pos.z;
float4 lengthSq = 0;
lengthSq += toLightX * toLightX;
lengthSq += toLightY * toLightY;
lengthSq += toLightZ * toLightZ;
float4 ndotl = 0;
ndotl += toLightX * normal.x;
ndotl += toLightY * normal.y;
ndotl += toLightZ * normal.z;
float4 corr = rsqrt(lengthSq);
ndotl = max (float4(0,0,0,0), ndotl * corr);
float4 atten = 1.0 / (1.0 + lengthSq * lightAttenSq);
float4 diff = ndotl *
float3 col = 0;
col += lightColor0 * diff.x;
col += lightColor1 * diff.y;
col += lightColor2 * diff.z;
col += lightColor3 * diff.w;
可以看到,顶点光照就是用简单的Lambert光照方程。
float4 corr = rsqrt(lengthSq);
ndotl = max (float4(0,0,0,0), ndotl * corr);
需要注意的是,因为法向量是归一化的,而光源方向未归一化,所以要乘以顶点离光源距离的平方根的倒数,得到光源和法线夹角的余弦。
lightAttenSq这个值也是由unity设定的,平行光恒为1,点光源和聚光灯随着距离增大而递减。
TRANSFER_VERTEX_TO_FRAGMENT(o);
这个的定义也是在AutoLight.cginc中,它计算之前定义的光照和阴影坐标的值。
点光源宏的定义如下:
#define TRANSFER_VERTEX_TO_FRAGMENT(a) a._LightCoord = mul(_LightMatrix0, mul(_Object2World, v.vertex)). TRANSFER_SHADOW(a)
实际就是把顶点坐标转换到光源空间中。
fixed atten = LIGHT_ATTENUATION(IN);
这个也是定义在AutoLight.cginc中,它就是把之前得到的光照和阴影坐标,采样光照图和shadow map,来得出光源的最终衰减值。
点光源的宏定义如下:
#define LIGHT_ATTENUATION(a)
(tex2D(_LightTexture0, dot(a._LightCoord,a._LightCoord).rr).UNITY_ATTEN_CHANNEL * SHADOW_ATTENUATION(a))
那么这里的_LightTexture0又是什么?在哪里设置的?别急,在forwardadd章节会讲到,这里对这个宏定义的作用有个认识就可以了。
c = LightingLambert (o, _WorldSpaceLightPos0.xyz, atten);
unity定义的光照方程都在Lighting.cginc这个文件中,需要注意的是,因为forwarbase只支持平行光,而如果光源类型是平行光,那么_WorldSpaceLightPos0这个变量保存的直接是光源方向。所以这里直接作为第二个参数,传入光照方程。当然也可以定义自己的光照方程,具体。
ForwardAdd
Tags { "LightMode" = "ForwardAdd" }
同样告诉渲染管线,这个pass作为forwardadd处理。这里处理额外的像素光源。
#pragma multi_compile_fwdadd
这个也是unity为forwardadd pass定制的multi_compile,打开Viriants看到如下:
DIRECTIONAL
POINT_COOKIE
DIRECTIONAL_COOKIE
unity会把forwardadd pass分别编译成适用于上述5种不同光源类型的版本。如果缺少这一行代码,那么unity只会编译DIRECTIONAL这一种情况。
ZWrite Off Blend One One Fog { Color (0,0,0,0) }
注意一下add pass有别于base pass的地方,因为之前base写过深度了,所以add就不用再次写深度了,以叠加的方式渲染到缓存,并且不受雾效影响。
整个add pass相较于base pass显得精简很多,它只考虑实时的光照计算,没有顶点光照,SH,lightmap,阴影这一系列东西。但是,它支持点光源,聚光灯以及cookie。接下去分别讲下,不同光源的光照处理(这里不讨论阴影部分的处理)。
平行光可以说是最简单的,因为它没有衰减。在AutoLight.cginc中找到unity中对平行光光照的定义:
#ifdef DIRECTIONAL
#define LIGHTING_COORDS(idx1,idx2) SHADOW_COORDS(idx1)
#define TRANSFER_VERTEX_TO_FRAGMENT(a) TRANSFER_SHADOW(a)
#define LIGHT_ATTENUATION(a)
SHADOW_ATTENUATION(a)
可以看到,只有对阴影部分处理的代码,光照部分是空的。因此,不考虑阴影的话,平行光的衰减值永远是1。
在AutoLight.cginc中找到unity中对点光源光照的定义:
#ifdef POINT
#define LIGHTING_COORDS(idx1,idx2) float3 _LightCoord : TEXCOORD##idx1; SHADOW_COORDS(idx2)
uniform sampler2D _LightTexture0;
uniform float4x4 _LightMatrix0;
#define TRANSFER_VERTEX_TO_FRAGMENT(a) a._LightCoord = mul(_LightMatrix0, mul(_Object2World, v.vertex)). TRANSFER_SHADOW(a)
#define LIGHT_ATTENUATION(a)
(tex2D(_LightTexture0, dot(a._LightCoord,a._LightCoord).rr).UNITY_ATTEN_CHANNEL * SHADOW_ATTENUATION(a))
_LightMatrix0是由unity设置的变换矩阵,把世界空间的顶点,变换到光源空间中。而_LightTexture0是一张由unity生成的渐变图,8位的alpha图,如下图所示:
[图-1 点光源的_LightTexture0贴图]
UNITY_ATTEN_CHANNEL 也就是alpha通道。最终用光照坐标来采样_LightTexture0得到光照的衰减值。
同样在AutoLight.cginc中找到对聚光灯的光照定义:
#ifdef SPOT
#define LIGHTING_COORDS(idx1,idx2) float4 _LightCoord : TEXCOORD##idx1; SHADOW_COORDS(idx2)
uniform sampler2D _LightTexture0;
uniform float4x4 _LightMatrix0;
uniform sampler2D _LightTextureB0;
#define TRANSFER_VERTEX_TO_FRAGMENT(a) a._LightCoord = mul(_LightMatrix0, mul(_Object2World, v.vertex)); TRANSFER_SHADOW(a)
inline fixed UnitySpotCookie(float4 LightCoord)
return tex2D(_LightTexture0, LightCoord.xy / LightCoord.w + 0.5).w;
inline fixed UnitySpotAttenuate(float3 LightCoord)
return tex2D(_LightTextureB0, dot(LightCoord, LightCoord).xx).UNITY_ATTEN_CHANNEL;
#define LIGHT_ATTENUATION(a)
( (a._LightCoord.z & 0) * UnitySpotCookie(a._LightCoord) * UnitySpotAttenuate(a._LightCoord.xyz) * SHADOW_ATTENUATION(a) )
可以看到,聚光灯相较于点光源,新增了一张光照贴图,用来表示聚光灯的光照范围,如下图所示:
[图-2 聚光灯的_LightTexture0贴图]
[图-3 聚光灯的_LightTextureB0贴图]
这也符合聚光灯的特性,除了距离的衰减之外,投射到平面的光照范围再通过采样[图-2]来确定,哪里应该被照亮。
以上对于由unity把一个最简单的surface shader转换为vertex/fragment shader之后的代码,做了具体的分析。但也并非面面俱到。限于作者的水平,对于SH,为何用dot(LightCoord, LightCoord).xx来作为uv坐标采样[图-1]光照图,用LightCoord.xy / LightCoord.w + 0.5来采样[图-2],也不理解,如果有大牛知道,还望告知,不胜感激。
&&相关文章推荐
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:6318次
排名:千里之外
原创:12篇
(2)(1)(1)(1)(1)(6)Unity ShaderLab Surface Shader背后的机制
Unity ShaderLab Surface Shader背后的机制
编辑日期: 字体:
一直以来,Unity Surface Shader背后的机制一直是初学者为之困惑的地方。Unity Surface Shader在Unity 3.0的时候被开放给公众使用,其宣传手段也是号称让所有人都可以轻松地写shader。但由于资料缺乏,很多人知其然不知其所以然,无法理解Unity Surface Shader在背后为我们做了哪些事情。
前几天一直被问到一个问题,为什么我的场景里没有灯光,但物体不是全黑的呢?为什么我把Light的颜色调成黑色,物体还是有一些默认颜色呢?这些问题其实都是因为那些物体使用了Surface Shader的缘故。因此,了解Surface Shader背后的机制是非常重要滴~
虽然Surface Shader一直是一个神秘的存在,但其实Unity给了我们揭开她面纱的方式:查看它生成的CG代码。大家应该都知道,所谓的Surface Shader实际上是封装了CG语言,隐藏了很多光照处理的细节,它的设计初衷是为了让用户仅仅使用一些指令(#pragma)就可以完成很多事情,并且封装了很多常用的光照模型和函数,例如Lambert、Blinn-Phong等。而查看Surface Shader生成的代码也很简单:在每个编译完成的Surface Shader的面板上,都有个“Show generated code”的按钮,像下面这样:
点开后,就可以查看啦~面板上还表明了很多其他的有用信息。而这些方便的功能实际上是Unity 4.5发布出来的。详情可见。
使用Surface Shader,很多时候,我们只需要告诉shader,“嘿,使用这些纹理去填充颜色,法线贴图去填充法线,使用Lambert光照模型,其他的不要来烦我!!!”我们不需要考虑是使用forward还是deferred rendering,有多少光源类型、怎样处理这些类型,每个pass需要处理多少个光源!!!(人们总会rant写一个shader是多么的麻烦。。。)So!Unity说,不要急,放着我来~
上面的情景当然对于小白是比较简单的方式,Surface Shader可以让初学者快速实现很多常见的shader,例如漫反射、高光反射、法线贴图等,这些常见的效果也都不错。而对应面就是,由于隐藏了很多细节,如果想要自定义一些比较复杂或特殊的效果,使用Surface Shader就无法达到了(或者非常麻烦)。在学了一段时间的Surface Shader后,我认为:
o如果你从来没有学习过怎样编写shader,而又想写一些常见的、比较简单的shader,那仅学习Surface Shader是一个不错的选择。
o如果你向往那些高品质的游戏画面,那么Surface Shader是远远无法满足你的,而且某种方面来说它会让你变得越来越困惑。
困惑了怎么办呢?老老实实去学习主流的渲染语言吧~比如CG、GLSL、HLSL等。等学了一些上述内容后,再回过头来看Surface Shader就会别有一番理解了。
说教了这么多,本篇的主旨其实是分析下Surface Shader背后做的事情啦!也就是,分析Surface Shader到底是怎样解析我们编写的那些surf、LightingXXX等函数的,又是如何得到像素颜色的。那么,开始吧!
首先,我们要明白Surface Shader支持哪些特性。详情请见
Surface Shader最重要的部分是两个结构体以及它的编译指令。
两个结构体
两个结构体就是指struct Input和SurfaceOutput。其中Input结构体是允许我们自定义的。它可以包含一些纹理坐标和其他提前定义的变量,例如view direction(float3 viewDir)、world space position(worldPos)、world space reflection vector(float3 worldRefl)等。这些变量只有在真正使用的时候才会被计算生成。比如,在某些Pass里生成而某些就生成。
另一个结构体是SurfaceOutput。我们无法自定义这个结构体内的变量。关于它最难理解的也就是每个变量的具体含义以及工作机制(对像素颜色的影响)。我们来看一下它的定义(在Lighting.cginc里面):
struct SurfaceOutput {
oAlbedo:我们通常理解的对光源的反射率。它是通过在Fragment Shader中计算颜色叠加时,和一些变量(如vertex lights)相乘后,叠加到最后的颜色上的。
oNormal:即其对应的法线方向。只要是受法线影响的计算都会受到影响。
oEmission:自发光。会在Fragment 最后输出前(调用final函数前,如果定义了的话),使用下面的语句进行简单的颜色叠加:c.rgb += o.E
oSpecular:高光反射中的指数部分的系数。影响一些高光反射的计算。按目前的理解,也就是在光照模型里会使用到(如果你没有在光照函数等函数——包括Unity内置的光照函数,中使用它,这个变量就算设置了也没用)。有时候,你只在surf函数里设置了它,但也会影响最后的结果。这是因为,你可能使用了Unity内置的光照模型,如BlinnPhong,它会使用如下语句计算高光反射的强度(在Lighting.cginc里):float spec = pow (nh, s.Specular*128.0) * s.G
oGloss:高光反射中的强度系数。和上面的Specular类似,一般在光照模型里使用。
oAlpha:通常理解的透明通道。在Fragment Shader中会直接使用下列方式赋值(如果开启了透明通道的话):c.a = o.A
上述结论是分析生成的代码所得,若有不对欢迎指出。大家碰到不懂的,也可以像这样分析生成的代码,一般问题都可以理解啦~
编译指令的一般格式如下:
#pragma surface surfaceFunction lightModel [optionalparams]
Surface Shader和CG其他部分一样,代码也是要写在CGPROGRAM和ENDCG之间。但区别是,它必须写在SubShader内部,而不能写在Pass内部。Surface Shader自己会自动生成所需的各个Pass。由上面的编译格式可以看出,surfaceFunction和lightModel是必须指定的,而且是可选部分。
surfaceFunction通常就是名为surf的函数(函数名可以任意),它的函数格式是固定的:
void surf (Input IN, inout SurfaceOutput o)
即Input是输入,SurfaceOutput是输出。
lightModel也是必须指定的。由于Unity内置了一些光照函数——Lambert(diffuse)和Blinn-Phong(specular),因此这里在默认情况下会使用内置的Lambert模型。当然我们也可以自定义。
optionalparams包含了很多可用的指令类型,包括开启、关闭一些状态,设置生成的Pass类型,指定可选函数等。这里,我们只关注可指定的函数,其他可去官网自行查看。除了上述的surfaceFuntion和lightModel,我们还可以自定义两种函数:vertex:VertexFunction和finalcolor:ColorFunction。也就是说,Surface Shader允许我们自定义四种函数。
两个结构体+四个函数——它们在整个的render pipeline中的流程如下:
从上图可以看出来,Surface Shader背后的”那些女人“就是vertex shader和fragment shader。除了VertexFunction外,另外两个结构体和三个函数都是在fragment shader中扮演了一些角色。Surface Shader首先根据我们的代码生成了很多Pass,用于forwardbase和forwardadd等,这不在本篇的讨论范围。而每个Pass的代码是基于上述四个函数生成的。
以一个Pass的代码为例,Surface Shader的生成过程简述如下:
1.直接将CGPROGRAM和ENDCG之间的代码复制过来(其实还是更改了一些编译指令),这些代码包括了我们对Input、surfaceFuntion、LightingXXX等变量和函数的定义。这些函数和变量会在之后的处理过程中当成普通的结构体和函数进行调用,就和在C++中我们会在main函数中调用某些函数一样;
2.分析上述代码,生成v2f_surf结构,用于在Vertex Shader和Fragment Shader之间进行数据传递。Unity会分析我们在四个自定义函数中所使用的变量,例如纹理坐标等。如果需要,它会在v2f_surf中生成相应的变量。而且,即便有时我们在Input中定义了某些变量(如某些纹理坐标),但Unity在分析后续代码时发现我们并没有使用这些变量,那么这些变量实际上是不会在v2f_surf中生成的。这也就是说,Unity做了一些优化动作。
3.生成Vertex Shader。
* 如果我们自定义了VertexFunction,Unity会在这里首先调用VertexFunction修改顶点数据;然后分析VertexFunction修改的数据,最后通过Input结构体将修改结果存储到v2f_surf中。
* 计算v2f_surf中其他默认的变量值。这主要包括了pos、纹理坐标、normal(如果没有使用LightMap)、vlight(如果没有使用LightMap)、lmap(如果使用LightMap)等。
* 最后,通过内置的TRANSFER_VERTEX_TO_FRAGMENT指令将v2f_surf传递给下面的Fragment Shader。
4.生成Fragment Shader。
* 使用v2f_surf中的对应变量填充Input结构,例如一些纹理坐标等。
* 调用surfFuntion填充SurfaceOutput结构。
* 调用LightingXXX函数得到初始的颜色值。
* 进行其他的颜色叠加。如果没有启用LightMap,这里会使用SurfaceOutput.Albedo和v2f_surf.vlight的乘积和原颜色值进行叠加;否则会进行一些更复杂的颜色叠加。
* 最后,如果自定了final函数,则调用它进行最后额颜色修改。
我们以一个Surface Shader为例,分析它生成的代码。
Surface Shader如下:
Shader “Custom/BasicDiffuse” {
Properties {
_EmissiveColor (“Emissive Color”, Color) = (1,1,1,1)
_AmbientColor (“Ambient Color”, Color) = (1,1,1,1)
_MySliderValue (“This is a Slider”, Range(0,10)) = 2.5
_RampTex (“Ramp Texture”, 2D) = “white”{}
SubShader {
Tags { “RenderType”=”Opaque” “RenderType”=”Opaque” }
#pragma surface surf BasicDiffuse vertex:vert finalcolor:final noforwardadd
#pragma debug
float4 _EmissiveC
float4 _AmbientC
float _MySliderV
sampler2D _RampT
struct Input
float2 uv_RampT
float4 vertC
void vert(inout appdata_full v, out Input o)
o.vertColor = v.
void surf (Input IN, inout SurfaceOutput o)
c = pow((_EmissiveColor + _AmbientColor), _MySliderValue);
o.Albedo = c.rgb + tex2D(_RampTex, IN.uv_RampTex).
o.Alpha = c.a;
inline float4 LightingBasicDiffuse (SurfaceOutput s, fixed3 lightDir, fixed atten)
float difLight = max(0, dot (s.Normal, lightDir));
float hLambert = difLight * 0.5 + 0.5;
float3 ramp = tex2D(_RampTex, float2(hLambert)).
col.rgb = s.Albedo * _LightColor0.rgb * (ramp) *
col.a = s.A
void final(Input IN, SurfaceOutput o, inout fixed4 color) {
color = color * 0.5 + 0.5;
FallBack “Diffuse”
它包含了全部四个函数,以及一些比较常见的运算。为了只关注一个Pass,我添加了noforwardadd指令。它所得到的渲染结果不重要(事实上我只是在BasicDiffuse上瞎改了一些。。。)
我们点开查看它生成的代码:
Shader “Custom/BasicDiffuse_Gen” {
Properties {
_EmissiveColor (“Emissive Color”, Color) = (1,1,1,1)
_AmbientColor (“Ambient Color”, Color) = (1,1,1,1)
_MySliderValue (“This is a Slider”, Range(0,10)) = 2.5
_RampTex (“Ramp Texture”, 2D) = “white”{}
SubShader {
Tags { “RenderType”=”Opaque” “RenderType”=”Opaque” }
// ————————————————————
// Surface shader code generated out of a CGPROGRAM block:
// —- forward rendering base pass:
Name “FORWARD”
Tags { “LightMode” = “ForwardBase” }
// compile directives
#pragma vertex vert_surf
#pragma fragment frag_surf
#pragma multi_compile_fwdbase nodirlightmap
#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 11 “”
#ifdef DUMMY_PREPROCESSOR_TO_WORK_AROUND_HLSL_COMPILER_LINE_HANDLING
//#pragma surface surf BasicDiffuse vertex:vert finalcolor:final noforwardadd
#pragma debug
float4 _EmissiveC
float4 _AmbientC
float _MySliderV
sampler2D _RampT
struct Input
float2 uv_RampT
float4 vertC
void vert(inout appdata_full v, out Input o)
o.vertColor = v.
void surf (Input IN, inout SurfaceOutput o)
c = pow((_EmissiveColor + _AmbientColor), _MySliderValue);
o.Albedo = c.rgb + tex2D(_RampTex, IN.uv_RampTex).
o.Alpha = c.a;
inline float4 LightingBasicDiffuse (SurfaceOutput s, fixed3 lightDir, fixed atten)
float difLight = max(0, dot (s.Normal, lightDir));
float hLambert = difLight * 0.5 + 0.5;
float3 ramp = tex2D(_RampTex, float2(hLambert)).
col.rgb = s.Albedo * _LightColor0.rgb * (ramp);
col.a = s.A
void final(Input IN, SurfaceOutput o, inout fixed4 color) {
color = color * 0.5 + 0.5;
// vertex-to-fragment interpolation data
#ifdef LIGHTMAP_OFF
struct v2f_surf {
float4 pos : SV_POSITION;
float2 pack0 : TEXCOORD0;
float4 cust_vertColor : TEXCOORD1;
fixed3 normal : TEXCOORD2;
fixed3 vlight : TEXCOORD3;
// LIGHTING_COORDS在AutoLight.cginc里定义
// 本质上就是一个#define指令
// #define LIGHTING_COORDS(idx1,idx2) float3 _LightCoord : TEXCOORD##idx1; SHADOW_COORDS(idx2)
// #define SHADOW_COORDS(idx1) float3 _ShadowCoord : TEXCOORD##idx1;
LIGHTING_COORDS(4,5)
#ifndef LIGHTMAP_OFF
struct v2f_surf {
float4 pos : SV_POSITION;
float2 pack0 : TEXCOORD0;
float4 cust_vertColor : TEXCOORD1;
float2 lmap : TEXCOORD2;
LIGHTING_COORDS(3,4)
#ifndef LIGHTMAP_OFF
float4 unity_LightmapST;
// 定义所需的纹理坐标
float4 _RampTex_ST;
// vertex shader
v2f_surf vert_surf (appdata_full v) {
// 使用自定义的vert函数填充Input结构
Input customInputD
vert (v, customInputData);
// 再赋值给真正所需的v2f_surf结构
o.cust_vertColor = customInputData.vertC
o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
// 将顶点的纹理坐标转换到纹理对应坐标
o.pack0.xy = TRANSFORM_TEX(v.texcoord, _RampTex);
#ifndef LIGHTMAP_OFF
// 如果启用了LightMap,则计算对应的LightMap坐标
o.lmap.xy = v.texcoord1.xy * unity_LightmapST.xy + unity_LightmapST.
// 计算世界坐标系中法线的方向
// SCALED_NORMAL在UnityCG.cginc里定义
// 本质上就是一个#define指令
// #define SCALED_NORMAL (v.normal * unity_Scale.w)
float3 worldN = mul((float3x3)_Object2World, SCALED_NORMAL);
// 如果没有开启LightMap,
// 顶点法线方向就是worldN
#ifdef LIGHTMAP_OFF
o.normal = worldN;
// SH/ambient and vertex lights
#ifdef LIGHTMAP_OFF
// 如果没有开启LightMap,
// vertex lights就是球面调和函数的结果
// 球面调和函数ShadeSH9在UnityCG.cginc里定义
float3 shlight = ShadeSH9 (float4(worldN,1.0));
o.vlight =
// unity_4LightPosX0等变量在UnityShaderVariables.cginc里定义
#ifdef VERTEXLIGHT_ON
float3 worldPos = mul(_Object2World, v.vertex).
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在AutoLight.cginc里定义,
// 本质上就是一个#define指令
// 用于转换v2f_surf中的_LightCoord和_ShadowCoord
TRANSFER_VERTEX_TO_FRAGMENT(o);
#ifndef LIGHTMAP_OFF
sampler2D unity_L
#ifndef DIRLIGHTMAP_OFF
sampler2D unity_LightmapI
// fragment shader
fixed4 frag_surf (v2f_surf IN) : SV_Target {
// prepare and unpack data
#ifdef UNITY_COMPILER_HLSL
Input surfIN = (Input)0;
Input surfIN;
// 使用v2f_surf中的变量给Input中的纹理坐标进行赋值
surfIN.uv_RampTex = IN.pack0.
surfIN.vertColor = IN.cust_vertC
#ifdef UNITY_COMPILER_HLSL
SurfaceOutput o = (SurfaceOutput)0;
// 初始化SurfaceOutput结构
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.
// call surface function
// 调用自定义的surf函数填充SurfaceOutput结构
surf (surfIN, o);
// compute lighting & shadowing factor
// LIGHT_ATTENUATION在AutoLight.cginc里定义,
// 本质上就是一个#define指令
// 用于计算光衰减
fixed atten = LIGHT_ATTENUATION(IN);
fixed4 c = 0;
// realtime lighting: call lighting function
#ifdef LIGHTMAP_OFF
// 如果没有开启LightMap,
// 调用自定义的LightXXX函数,
// 使用填充好的SurfaceOutput等变量作为参数,
// 得到初始的像素值
c = LightingBasicDiffuse (o, _WorldSpaceLightPos0.xyz, atten);
#endif // LIGHTMAP_OFF || DIRLIGHTMAP_OFF
#ifdef LIGHTMAP_OFF
// 如果没有开启LightMap,
// 向像素叠加vertex light的光照颜色
c.rgb += o.Albedo * IN.
#endif // LIGHTMAP_OFF
// lightmaps:
#ifndef LIGHTMAP_OFF
// 计算LightMap,这部分不懂
#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).
#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);
c.rgb += o.Albedo * max(min(lm,(atten*2)*lmtex.rgb), lm*atten);
#else // SHADOWS_SCREEN
c.rgb += o.Albedo *
#endif // SHADOWS_SCREEN
// 给Alpha通道赋值
#endif // LIGHTMAP_OFF
// 调用自定义的final函数,
// 对像素值进行最后的更改
final (surfIN, o, c);
// —- end of surface shader generated code
FallBack “Diffuse”
其中比较重要的部分我都写了注释。
回到我们一开始的那个问题:为什么我的场景里没有灯光,但物体不是全黑的呢?这一切都是Fragment Shader中一些颜色叠加计算的结果。
我们仔细观察Fragment Shader中计算颜色的部分。前面说过,它使用LightingXXX对颜色值进行初始化,但后面还进行了一系列颜色叠加计算。其中,在没有使用LightMap的情况下,Unity还计算了vertex lights对颜色的影响,也就是下面这句话:
#ifdef LIGHTMAP_OFF
// 如果没有开启LightMap,
// 向像素叠加vertex light的光照颜色
c.rgb += o.Albedo * IN.
#endif // LIGHTMAP_OFF
而IN.vlight是在Vertex Shader中计算的:
// 如果没有开启LightMap,
// vertex lights就是球面调和函数的结果
// 球面调和函数ShadeSH9在UnityCG.cginc里定义
float3 shlight = ShadeSH9 (float4(worldN,1.0));
o.vlight =
我们可以去查看ShadeSH9函数的实现:
// normal should be normalized, w=1.0
half3 ShadeSH9 (half4 normal)
half3 x1, x2, x3;
// Linear + constant polynomial terms
x1.r = dot(unity_SHAr,normal);
x1.g = dot(unity_SHAg,normal);
x1.b = dot(unity_SHAb,normal);
// 4 of the quadratic polynomials
half4 vB = normal.xyzz * normal.
x2.r = dot(unity_SHBr,vB);
x2.g = dot(unity_SHBg,vB);
x2.b = dot(unity_SHBb,vB);
// Final quadratic polynomial
float vC = normal.x*normal.x – normal.y*normal.y;
x3 = unity_SHC.rgb * vC;
return x1 + x2 + x3;
它是一个球面调和函数,但unity_SHAr这些变量具体是什么我还不清楚。。。如果有人知道麻烦告诉我一下,不胜感激~但是,这些变量是和Unity使用了一个全局环境光(你可以在Edit-&RenderSettings-&Ambient Light中调整)有关。如果把这个环境光也调成黑色,那么场景就真的全黑了。
本文固定链接:
转载请注明:
作者:wy182000
这个作者貌似有点懒,什么都没有留下。
您可能还会对这些文章感兴趣!}

我要回帖

更多关于 unity3d shader 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信