Unity Shader 学习之旅中,有哪些技巧可以一学就会?

摘要:Unity Shader 学习之旅 unityshader图形图像 纸上学来终觉浅,绝知此事要躬行 美丽的梦和美丽的诗一样 都是可遇而不可求的——席慕蓉 一、渲染流水线 示例图 Tips:什么是 GPU 加速计算? 1.1Draw Call
Unity Shader 学习之旅 unityshader图形图像 纸上学来终觉浅,绝知此事要躬行 美丽的梦和美丽的诗一样 都是可遇而不可求的——席慕蓉 一、渲染流水线 示例图 Tips:什么是 GPU 加速计算? 1.1Draw Call CPU过Draw Call来g告诉GPU开始一个渲染过程。一个Draw Call会指向本次调用需要渲染的图元列表。 通俗的讲我们可以把CPU理解成一群专家,他们有着超强和快速的计算能力,能解决各种各样的问题。GPU则是许许多多个流水线上的工人,尽管它们只能做简易单一的任务,但是成千上万个工人一起开动可以迅速处理大量的工作。但是专家和这些工人协同工作的过程中需要交接任务,此时交接的过程就是Draw Call调用的过程。专家告诉工人们这些东西你们需要把这些零件加工成什么样,交接沟通是需要代价,这也就是为什么Draw Call过多会影响帧率。琐碎的零部件一点点的交接给工人是很浪费资源的,工人成千上万个对于100个零部件加工和一万个零部件加工耗费的代价是一样的,这样专家也忙不过来,这时专家忙的焦头烂额,工人还在等待下一批零部件,零部件组装成成品的过程就出现了卡顿。所以Draw Call优化的过程也就是尽可能一次由专家给工人一大批需要统一操作组装的零件(合并网格),和减少这些零件的类型(避免过多材质,尽可能多的公用材质)。 二、最简单的顶点片元着色器 2.1.顶点片元着色器基本结构 Unity Shader基本结构:Shader ,Properties,SubShder,Fallback等。 2.1.1结构 Shader "ShaderName"{ Properties{ //属性,暴露在inspector面板上 } SubShader{ //针对显卡A的SubShader Pass{ //设置渲染状态和标签 CGPROGRAM //该代码的编译指令,如: #pragma vertex vert #pragma fragment frag //CG代码 ENDCG //其他设置 } } SubShader{ //针对显卡B的SubShader } //上述SubShader都失败后调用回调的Unity Shader Fallback "VertexLit" } 2.1.2一个简单的示例 Shader "UnityShaderBoook/SimplerShader"{ //Properties并非必须,此shader中不声明任何材质属性 SubShader{ Pass{ CGPROGRAM //编译命令,指定顶点/片元着色器处理函数 #pragma vertex vert #pragma fragment frag //顶点着色器代码,逐个顶点执行 //使用POSITION语义指定函数的输入参数v为顶点的位置信息 //SV_POSITION语义指明函数的返回值为顶点着色器输出的是裁剪空间中的位置 //UnityObjectToClipPos则是Unity内置的模型-观察-投影矩阵,帮我们完成操作(mul(UNITY_MATRIX_MVP,*)' 现已更新为'UnityObjectToClipPos(*)') float4 vert(float4 v:POSITION):SV_POSITION{ return UnityObjectToClipPos(v); } //片元着色器,逐个面片执行 //此片元着色器没有任何输入参数 //SV_TARGET语义指定函数输出颜色存储一个渲染目标上 //这里仅仅简单的返回表示白色的fixed4的变量 fixed4 frag():SV_TARGET{ return fixed4(1.0,1.0,1.0,1.0); } ENDCG } } } 当我们把使用该shader的材质赋给场景物体时,物体仅仅表现出单纯的白色。我们在shader中没有赋予它更丰富的内容,如法线,纹理等,所以他不会表现出任何深度,阴影等,当然也不会受到光照的影响。 注意: POSITION和SV_POSITION都是CG/HLSL中的语义,他们是不可省略的,这些语义用来告诉系统输入值和输出值的含义。POSTION告诉Unity这里将模型的顶点数据传入到v参数,而SV_POSITION则告诉Unity顶点着色器输出的是裁剪空间中的顶点坐标。 UnityObjectToClipPos(v) 是mul(UNITY_MATRIX_MVP,v)更新后的函数,他是Unity内置函数,用来帮我们进行顶点坐标从模型空间转换到裁剪空间的转换。 SV_TARGET 是HLSL中的系统语义,它输出的是一个float4类型的变量,它等同于告诉渲染器,把用户的输出颜色存储到一个渲染目标中,有时也使用COLOR代替,考虑到平台的通用性最好使用SV_TARGET。 模型空间:模型空间我们可以理解为模型自身的局部坐标,一个模型上的顶点位置信息,都是依托自身的局部坐标的,但当我们把它放到场景中的时候,就需要我们转换这些信息来使用。 裁剪空间:可以简单的理解为Unity中摄像机视椎体包围的空间,不在其空间内的信息将会被剔除,保留的信息后续会被投影到屏幕上被看到。 2.2.顶点着色器与片元着色器之间通信 片元着色器是无法直接获取陈模型的顶点信息的,这时就需要我们通过使用结构体作为媒介,有顶点着色器向片元着色器传递一些数据,如:模型法线,纹理坐标等。 2.2.1示例 // Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)' Shader "UnityShaderBoook/vert2frag"{ Properties { _Color ("Color Tint", Color) = (1, 1, 1, 1) } //Properties并非必须,此shader中不声明任何材质属性 SubShader{ Pass{ CGPROGRAM //编译命令,指定顶点/片元着色器处理函数 #pragma vertex vert #pragma fragment frag uniform fixed4 _Color; //结构体用于指定向顶点着色器传递的信息 struct a2v{ float4 vertex:POSITION;//模型的顶点坐标 float4 normal:NORMAL;//模型的法线信息 float4 texcoord:TEXCOORD0;//模型的第一套纹理 }; //结构体指定了顶点着色器向片元着色器传出的信息 struct v2f{ float4 pos:SV_POSITION;//裁剪空间中的坐标信息 fixed3 color:COLOR0;//COLOR0语义可用来存储颜色信息 }; v2f vert(a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.color = v.normal * 0.5 + fixed3(0.5, 0.5, 0.5); return o; } //接收传入的信息 fixed4 frag(v2f i):SV_TARGET{ fixed3 c = i.color; c *= _Color.rgb; return fixed4(c, 1.0); } ENDCG } } } 注意: 顶点着色器是逐个顶点调用的,片元着色器是逐片元调用的。片元着色器中的输入实际是吧顶点着色器的输出插值后得到的结果。 2.3.Unity内置文件及变量 2.3.1 名词解释 ShaderLab是unity自己封装调用CG/HLSL/GLSL的接口。可以理解为C#与C,一个方便我们使用,一个更加接近底层。 语义是CG/HLSL这些底层提供的用于限定参数含义的字符串。而unity为了方便的对模型的数据进行传输,对一些语义进行了特别规定。如:顶点着色器中用TEXCOORD0来描述texcoord,unity会自动识别出TEXCOORD0的语义,并把模型的第一套纹理坐标信息填充到texcoord中。 2.3.2 Unity支持的常用语义 从应用阶段传递模型数据到顶点着色器 语义描述 POSITION 模型空间中的顶点位置,通常是float4类型 NORMAL 顶点法线,通常是float3类型 TANGENT 顶点切线,通常是float4类型 TEXCOORDn 如TEXCOORD0、TEXCOORD1该顶点的纹理坐标,TEXCOORD0表示第一组纹理坐标,依此类推,通常是float2或float4类型 COLOR 顶点颜色,通常是fixed4或float4类型 注意: 其中TEXCOORDn中n的数目是和Shader Model有关的,例如一般在Shader Model 2(即Unity默认编译到的Shader Model版本)和Shader Model 3中,n等于8,而在Shader Model 4和Shader Mode 5中,n等于16。通常情况下,一个模型的纹理坐标组数一般不超过2,即我们往往只使用TEXCOORD0和TEXCOORD1。在Unity内置的数据结构体appdata_full中,它最多使用了6个坐标纹理组。 从顶点着色器传递数据给片元着色器 语义 描述 SV_POSITION 裁剪空间中的顶点坐标,结构体中必须包含一个用该语义修饰的变量。等同于DirectX9中的POSITION,但最好使用SV_POSITION COLOR0 通常用于输出第一组顶点颜色,但不是必须的 COLOR1 通常用于输出第二组顶点颜色,但不是必须的 TEXCOORD0~TEXCOORD7 通常用于输出纹理坐标,但不是必须的 注意 上面的语义中,除了SV_POSITION是有特别含义外,其他语义对变量的含义没有明确要求,也就是说,我们可以存储任意值到这些语义描述变量中。通常,如果我们需要把一些自定义的数据从顶点着色器传递给片元着色器,一般选用TEXCOORD0等。 从片元着色器输出时Unity支持的语义 语义 描述 SV_Target 输出值将会存储到渲染目标(render target)中。等同于DirectX9中的COLOR语义,但最好使用SV_Target 注意 一个语义可以使用的寄存器只能处理4个浮点值(float)。因此我们想要定义矩阵类型,如float3×4,float4×4等变量是就需要使用更多的空间。一种方法是,把这些变量拆分成多个变量,例如对于float4×4的矩阵类型,我们可以拆分成四个float类型的变量,每个变量存储矩阵中的一行数据。 三、Unity中三种shader类型 3.1固定管线着色器 3.1.1简介 固定功能着色器为固定功能渲染管线的具体表现。实现较为简单,但是功能单一,效果较差。Unity5.2及以后备抛弃,所有的固定管线着色器都会别Unity编译成对应的顶点片元着色器。 3.1.2效果 固定功能 3.1.3示例代码 Shader "ShaderCookbook/固定功能着色器" { //-------------------[属性]-------------------------- Properties { _Color("主颜色",Color)=(1,1,1,0) _SpecColor("高光颜色",Color)=(1,1,1,1) _Emission("自发光颜色",Color)=(0,0,0,0) _Shininess("光泽度",Range(0.01,1))=0.7 _MainTex("基本纹理",2D)="white"{} _SecTex("副纹理",2D)="white"{} } //------------------[子着色器]----------------------- SubShader { Tags{"Queue"="Transparent"} //-------------------[通道]-------------------------- Pass { Blend SrcAlpha OneMinusSrcAlpha //-------------------[材质]-------------------------- Material { //漫反射光 Diffuse[_Color] //环境光 Ambient[_Color] //光泽度 Shininess[_Shininess] //高光颜色 Specular[_SpecColor] //自发光暗色 Emission[_Emission] } //开启光照 Lighting On //开启独立镜面反射 SeparateSpecular On //设置纹理并进行纹理混合 SetTexture[_MainTex] { Combine texture* primary DOUBLE,texture * primary } SetTexture[_SecTex]{ Combine texture * previous double,texture * previous } } } } 3.2表面着色器 表面着色器介绍 3.2.1简介 Unity包装过一层的着色器类型,需要较少的代码量就能达到很好的效果,但由于Unity背后会做很多工作,渲染的代价比较大。 3.2.2效果 表面着色器 3.2.1示例代码 Shader "ShaderCookbook/简单表面着色器" { //-------------------------------【属性】----------------------------------------- Properties { _MainTex ("【纹理】Texture", 2D) = "white" {} _BumpMap ("【凹凸纹理】Bumpmap", 2D) = "bump" {} _RimColor ("【边缘颜色】Rim Color", Color) = (0.17,0.36,0.81,0.0) _RimPower ("【边缘颜色强度】Rim Power", Range(0.6,9.0)) = 1.0 } //----------------------------【开始一个子着色器】--------------------------- SubShader { //渲染类型为Opaque,不透明 Tags { "RenderType" = "Opaque"} Blend SrcAlpha OneMinusSrcAlpha //-------------------开始CG着色器编程语言段----------------- CGPROGRAM //使用兰伯特光照模式 #pragma surface surf Lambert //输入结构 struct Input { float2 uv_MainTex;//纹理贴图 float2 uv_BumpMap;//法线贴图 float3 viewDir;//观察方向 }; //变量声明 sampler2D _MainTex;//主纹理 sampler2D _BumpMap;//凹凸纹理 float4 _RimColor;//边缘颜色 float _RimPower;//边缘颜色强度 //表面着色函数的编写 void surf (Input IN, inout SurfaceOutput o) { //表面反射颜色为纹理颜色 o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb; //表面法线为凹凸纹理的颜色 o.Normal = UnpackNormal (tex2D (_BumpMap, IN.uv_BumpMap)); //边缘颜色 half rim = 1.0 - saturate(dot (normalize(IN.viewDir), o.Normal)); //边缘颜色强度 o.Emission = _RimColor.rgb * pow (rim, _RimPower); } //-------------------结束CG着色器编程语言段------------------ ENDCG } //普通漫反射 Fallback "Diffuse" } 3.3顶点片元着色器 3.3.1 简介 更为复杂,但也更为灵活,可以完成很多复杂的效果,但是需要我们控制渲染的实现细节。 3.3.2效果 顶点片元着色器 3.3.3示例代码: shader "ShaderCookbook/简单顶点片元着色器"{ //-------------------------------【属性】-------------------------------------- Properties { _Color ("Color", Color) = (1.0,1.0,1.0,1.0) _MainTex("MainTex",2D)="white"{} _SpecColor ("Specular Color", Color) = (1.0,1.0,1.0,1.0) _Shininess ("Shininess", Float) = 10 } //--------------------------------【子着色器】-------------------------------- SubShader { //-----------子着色器标签---------- Tags { "LightMode" = "ForwardBase" } //----------------通道--------------- Pass { //-------------------开始CG着色器编程语言段----------------- CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" //---------------声明变量-------------- float4 _Color; sampler2D _MainTex; float4 _MainTex_ST; float4 _SpecColor; float _Shininess; //--------------定义变量-------------- float4 _LightColor0; //--------------顶点输入结构体------------- struct vertexInput { float4 vertex : POSITION; float3 normal : NORMAL; float4 texcoord : TEXCOORD0; }; //--------------顶点输出结构体------------- struct vertexOutput { float4 pos : SV_POSITION; float4 col : COLOR; float2 uv :TEXCOORD0; }; //--------------顶点函数-------------- vertexOutput vert(vertexInput v) { vertexOutput o; //获取纹理信息 o.uv= v.texcoord * _MainTex_ST.xy + _MainTex_ST.zw;; //一些方向 float3 normalDirection = normalize( mul( float4(v.normal, 0.0), unity_WorldToObject ).xyz ); float3 viewDirection = normalize( float3( float4( _WorldSpaceCameraPos.xyz, 1.0) - mul(unity_ObjectToWorld, v.vertex).xyz ) ); float3 lightDirection; float atten = 1.0; //光照 lightDirection = normalize(_WorldSpaceLightPos0.xyz); float3 diffuseReflection = atten * _LightColor0.xyz * max( 0.0, dot( normalDirection, lightDirection ) ); float3 specularReflection = atten * _LightColor0.xyz * _SpecColor.rgb * max( 0.0, dot( normalDirection, lightDirection ) ) * pow( max( 0.0, dot( reflect( -lightDirection, normalDirection ), viewDirection ) ), _Shininess ); float3 lightFinal = diffuseReflection + specularReflection + UNITY_LIGHTMODEL_AMBIENT; //计算结果 o.col = float4(lightFinal * _Color.rgb, 1.0);//颜色 o.pos = UnityObjectToClipPos(v.vertex);//位置 return o; } //--------------片段函数--------------- float4 frag(vertexOutput i) : COLOR { fixed4 tex = tex2D(_MainTex,i.uv); i.col*=tex; return i.col; } //-------------------结束CG着色器编程语言段------------------ ENDCG } } Fallback "Diffuse" } 3.3.4 效果 在世界坐标系中顶点坐标超过限定值则不显示,类似切面效果 切面 3.3.5 示例代码 // Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld' Shader "ShaderCookbook/VertexWorldPos" { //------------------------属性-------------------------- Properties { _Color ("Color", Color) = (1,1,1,1) _MainTex ("Albedo (RGB)", 2D) = "white" {} _YLimit("YLimit", float)= 0.0 } //------------------------子shader-------------------------- SubShader { //开启透明度混合 之一 设置渲染类型和序列 Tags { "RenderType"="AlphaTest" "IgnoreProjector"="True" "Queue"="Transparent"} Pass{ Tags{"LightMode"="ForwardBase"} //开启透明度混合 之二 关闭深度写入 ZWrite off //开启透明度混合 之三 设置混合参数 Blend SrcAlpha OneMinusSrcAlpha //关闭剔除,显示双面 Cull off //------------------------开启CG-------------------------- CGPROGRAM #pragma vertex vert #pragma fragment frag sampler2D _MainTex; float4 _MainTex_ST; fixed4 _Color; float _YLimit; struct a2v{ float4 vertex:POSITION; float4 texcoord:TEXCOORD0; }; struct v2f{ float4 pos:SV_POSITION; float4 uv:TEXCOORD; float3 worldPos:TEXCOORD1; }; v2f vert(a2v i){ v2f v; v.pos=UnityObjectToClipPos(i.vertex); v.uv.xy=i.texcoord.xy*_MainTex_ST.xy+_MainTex_ST.zw; //获取顶点的世界坐标 v.worldPos=mul(unity_ObjectToWorld,i.vertex); return v; } fixed4 frag(v2f v):SV_TARGET{ fixed4 Col=_Color*tex2D(_MainTex,v.uv.xy); //丢弃的未符合的像素 if(v.worldPos.y>_YLimit) discard; return Col; } //------------------------关闭CG-------------------------- ENDCG } } //------------------------弃子-------------------------- FallBack "Diffuse" }
3.3.6 效果 这里我们再次使用顶点片元着色器造一个黑洞一样的效果。物体靠近黑洞时会被黑洞吸引拉扯,逐渐缩成一点,在另一端则会逐渐变大出现。 BlankHole 3.3.7 示例代码 // Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld' Shader "ShaderCookbook/黑洞" { //------------------------属性-------------------------- Properties { _Color ("Color", Color) = (1,1,1,1) _MainTex ("Albedo (RGB)", 2D) = "white" {} _YLimit("YLimit", float)= 0.0 _Length("Length",float)=1 } //------------------------子shader-------------------------- SubShader { Pass{ Tags{"LightMode"="ForwardBase"} //------------------------开启CG-------------------------- CGPROGRAM #pragma vertex vert #pragma fragment frag sampler2D _MainTex; float4 _MainTex_ST; fixed4 _Color; float _YLimit; float _Length; struct a2v{ float4 vertex:POSITION; float4 texcoord:TEXCOORD0; }; struct v2f{ float4 pos:SV_POSITION; float4 uv:TEXCOORD; float3 worldPos:TEXCOORD1; }; v2f vert(a2v i){ v2f v; //获取模型顶点坐标在世界坐标下的位置 v.worldPos=mul(unity_ObjectToWorld,i.vertex); //如果其距离我们设定的临界值_YLimit小于_Length长度,我们将会按比例缩放其X,Z轴的顶点位置 if(distance(v.worldPos.y,_YLimit)<_Length) { float s=(distance(v.worldPos.y,_YLimit)/_Length); i.vertex.xz*=s; } //将修改后的顶点坐标转换为裁剪坐标 v.pos=UnityObjectToClipPos(i.vertex); //获取图形的UI v.uv.xy=i.texcoord.xy*_MainTex_ST.xy+_MainTex_ST.zw; return v; } fixed4 frag(v2f v):SV_TARGET{ fixed4 Col=_Color*tex2D(_MainTex,v.uv.xy); return Col; } //------------------------关闭CG-------------------------- ENDCG } } //------------------------弃子-------------------------- FallBack "Diffuse" }
開簾頓覺春風暖 滿紙淋漓白雲聲——杨玉香 X Shader实例 X.1.1 热度图 X.1.1.1 效果 运行结果: 渐变颜色效果 梯度颜色效果 热度图纹理: 渐变ramp图片 梯度颜色效果 X.1.1.2 示例代码 Shader "Ellioman/Heatmap" { //-----------------------属性-------------------- Properties { _HeatTex("Texture", 2D) = "white" {} } //----------------------子shader----------------- SubShader { //设置为透明格式 Tags{ "Queue" = "Transparent" } //设置Blend类型 Blend SrcAlpha OneMinusSrcAlpha //----------------------通道---------------------- Pass { //-------------------开始CG------------- CGPROGRAM #pragma vertex vert #pragma fragment frag struct vertInput { float4 pos : POSITION; }; struct vertOutput { float4 pos : POSITION; fixed3 worldPos : TEXCOORD1; }; //自定义变量 uniform int _Points_Length = 0; //cg中只能声明确定长度的数组 uniform float3 _Points[100]; // (x, y, z) = 位置 uniform float2 _Properties[100]; // x = 半径, y = 强度 sampler2D _HeatTex; vertOutput vert(vertInput input) { vertOutput o; o.pos = UnityObjectToClipPos(input.pos); o.worldPos = mul(unity_ObjectToWorld, input.pos).xyz; return o; } half4 frag(vertOutput output) : COLOR { half h = 0; for (int i = 0; i < _Points_Length; i++) { // 计算每一个分布点位置与当前像素的距离 half di = distance(output.worldPos, _Points[i].xyz); half ri = _Properties[i].x; //抛弃超过半径分布点的影响,距离output.worldPos越近,其值越高 half hi = 1 - saturate(di / ri); //叠加又分布点位置_Points[i].xyz与output.worldPos之间的距离及x半径和y强度值得到的值 h += hi * _Properties[i].y; } h = saturate(h);//限定范围至[0,1] //通过ramp图,类似toonshader的方式获得阶梯式的颜色效果 half4 col = tex2D(_HeatTex, fixed2(h, 0.5)); return col; } //-------------------开始CG------------- ENDCG } } Fallback "Diffuse" } X.1.1.3 分析 首先,我们需要将ramp图片的wrapMode格式设为Clamp。 Texture.wrapMode 循环模式: TextureWrapMode.Clamp:设置纹理充满拉伸使用 TextureWrapMode.Repeat:纹理重复平铺使用 如果采用Repeat,那么等于U>=1的情况就会用纹理图在右边在平铺一张图。 我们使用了类似使用ramp纹理制作toon卡通风格shader效果的方式,来用uv中的u指获得颜色的强度。 这里在对blend混合命令做一个笔记: Blend SrcAlpha OneMinusSrcAlpha // 传统透明度 Blend One OneMinusSrcAlpha // 预乘透明度 Blend One One // 叠加 Blend OneMinusDstColor One // 柔和叠加 Blend DstColor Zero // 相乘——正片叠底 那海和天空之间星星消失的地方 连时间也没有确切的命运——杨炼 X.1.2 山崖 X.1.2.1 效果 这里主要实现了山崖上草地覆盖面积,高度还有山石高光,平滑度,法线强度的控制。 X1.2.2 示例代码 Shader "Unlit/Cliff" { //---------------参数--------------------------- Properties { _MainTex ("Texture", 2D) = "white" {} _MainNormal("MainNormal",2D)="white"{} _GrassTex ("GrassTex", 2D) = "white" {} _GrassNormal ("GrassNormal", 2D) = "white" {} _Smooth("smooth",Range(0,1))=1 _Gloss("Gloss",Range(0,128))=96 _Height("Height",float)=0.5 _Offset("Offset",float)=0.5 _BumpScale("bumpscale",float)=0.5 } //-------------subshader--------------------------- SubShader { Tags { "LightMode"="ForwardBase" } LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" #include "Lighting.cginc" //-----------传递数据用的结构体--------------------------- struct appdata { float4 vertex : POSITION; float4 uv : TEXCOORD0; float3 normal:NORMAL; float4 tangent:TANGENT; }; struct v2f { float4 uv : TEXCOORD0; float4 vertex : SV_POSITION; float3 lightDir:TEXCOORD1; float3 worldPos:TEXCOORD2; float3 tanView:TEXCOORD3; }; //----------传递参数数据--------------------------- sampler2D _MainTex; float4 _MainTex_ST; sampler2D _GrassTex; float4 _GrassTex_ST; sampler2D _MainNormal; sampler2D _GrassNormal; float _Offset; float _BumpScale; float _Height; float _Gloss; float _Smooth; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv.xy = TRANSFORM_TEX(v.uv.xy, _MainTex); o.uv.zw = TRANSFORM_TEX(v.uv.zw, _MainTex); //----------unity宏定义,用于得到从模型空间矩阵到切线空间矩阵rotation----- TANGENT_SPACE_ROTATION; //------------通过上一步得到rotation用于将模型空间下的光照法线转换到法线贴图对应的空间中去,这里的法线贴图位于切线空间---- o.worldPos=UnityObjectToWorldDir(v.vertex); //------------需要借助法线贴图计算光照,这里计算出需要用到符合法线贴图空间的光照方向和视角方向 o.lightDir=normalize(mul(rotation,ObjSpaceLightDir(v.vertex)).xyz); o.tanView=normalize(mul(rotation,ObjSpaceViewDir(v.vertex))); return o; } fixed4 frag (v2f i) : SV_Target { float3 halfDir=normalize(i.tanView+i.lightDir); fixed4 col = tex2D(_MainTex, i.uv.xy); fixed4 grass=tex2D(_GrassTex,i.uv.zw); float3 colNormal=UnpackNormal(tex2D(_MainNormal,i.uv.xy))*_BumpScale; float3 GrassNormal=UnpackNormal(tex2D(_GrassNormal,i.uv.zw))*_BumpScale; colNormal.z=sqrt(1.0-saturate(dot(colNormal.xy,colNormal.xy))); GrassNormal.z=sqrt(1.0-saturate(dot(GrassNormal.xy,GrassNormal.xy))); float angleY=1-saturate(dot(UnityObjectToWorldDir(colNormal),float3(0,1,0) )+_Offset); angleY-=i.worldPos.y>_Height?0:i.worldPos.y-_Height; fixed4 finalColor=lerp(grass,col,angleY); //---------------lambert光照--------------------------- float3 diffuse=finalColor*max(0.0,dot(i.lightDir,colNormal))*_LightColor0.xyz; //---------------Blin-phong光照--------------------------- float3 specular= _LightColor0.rgb * pow(saturate(dot(halfDir, colNormal)), _Gloss)*_Smooth; return fixed4(diffuse+specular+UNITY_LIGHTMODEL_AMBIENT.xyz*finalColor,1); } ENDCG }//end pass }//end subshader Fallback "Diffuse"//阴影 }