Unity3D基础篇----Shader学习笔记(4)
这一篇,我们来继续学习Shader中纹理的添加以及实现纹理中凹凸的映射。
首先,我们先来看看实现的效果:
以上就是这一篇中所实现的内容,主要分为两个,一个是纹理,一个是凹凸映射。
实现代码如下:
首先是贴图着色器代码:
- //Shader模块定义
- Shader "xiaolezi/Simple Texture"
- {
- //属性设置
- Properties
- {
- //定义一个物体表面颜色,格式:[属性名]([Inspector面板显示名字],属性类型)=[初始值]
- _DiffuseColor("Main Color", Color) = (1, 1, 1, 1)
- _MainTex("Albedo(RGB)", 2D) = "white"{}
- _Glossness("Glossness", Range(8, 256)) = 20
- _SpecularColor("Specular Color", Color) = (1, 1, 1, 1)
- }
- //第一个SubShader块
- SubShader
- {
- //第一个Pass块
- Pass
- {
- //指定灯光渲染模式
- Tags{ "LightMode" = "ForwardBase" }
- //开启CG着色器编辑模块
- CGPROGRAM
- //定义顶点着手器函数名
- <span style="white-space:pre"> </span>#pragma vertex vert
- //定义片段着色器函数名
- <span style="white-space:pre"> </span>#pragma fragment frag
- //包含相关头文件
- <span style="white-space:pre"> </span>#include "UnityCG.cginc"
- <span style="white-space:pre"> </span>#include "Lighting.cginc"
- //定义一个从应用程序到顶点数据的结构体
- struct appdata
- {
- float4 vertex : POSITION;//POSITION语义:表示从该模型中获取到顶点数据
- float3 normal : NORMAL; //NORMAL语义:获取该模型法线
- float2 texcoord : TEXCOORD0;//TEXCOORD0语义:获取该模型纹理坐标
- };
- //定义一个从顶点数据到片段数据的结构体
- struct v2f
- {
- float4 pos : SV_POSITION;//SV_POSITION语义:从顶点输出数据中获取到顶点数据
- float3 normal : TEXCOORD0;//TEXCOORD0语义:定义法线变量
- float2 uv : TEXCOORD1;//TEXCOORD1语义:定义纹理贴图变量
- float3 lightDir : TEXCOORD2;//TEXCOORD2语义:定义灯光方向变量
- float3 viewDir : TEXCOORD3;//TEXCOORD3语义:定义观察方向变量
- };
- //从属性模块中取得该变量
- fixed4 _DiffuseColor;
- float _Glossness;
- fixed4 _SpecularColor;
- sampler2D _MainTex;
- float4 _MainTex_ST;
- //顶点着色器函数实现
- v2f vert(appdata v)
- {
- v2f o;
- o.pos = UnityObjectToClipPos(v.vertex);//让模型顶点数据坐标从本地坐标转化为屏幕剪裁坐标
- o.normal = v.normal;
- //计算灯光方向
- o.lightDir = normalize(ObjSpaceLightDir(v.vertex));
- //计算观察方向
- o.viewDir = normalize(ObjSpaceViewDir(v.vertex));
- //计算纹理坐标
- //o.uv = v.texcoord.xy * _MainTex_ST.xy _MainTex_ST.zw;
- //这里使用内置宏 TRANSFORM_TEX
- o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
- return o;
- }
- //片段着色器函数实现
- fixed4 frag(v2f f) : SV_Target//SV_Target语义:输出片元着色器值,可直接认为是输出到屏幕颜色
- {
- fixed3 normalDir = normalize(UnityObjectToWorldNormal(f.normal)); //计算世界法线方向
- //漫反色
- fixed3 albedo = tex2D(_MainTex, f.uv);
- float Lambert = 0.5 * dot(normalDir, f.lightDir) 0.5;//兰伯特值
- fixed3 diffuse = _LightColor0.rgb * _DiffuseColor.rgb * Lambert * albedo; //计算漫反色
- //环境光
- fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
- //高光
- fixed3 halfDir = normalize(f.lightDir f.viewDir);//根据物体表面法线计算光的反射光方向
- fixed3 specular = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(halfDir, normalDir)), _Glossness);//Phong氏高光计算
- return fixed4(ambient diffuse specular, 1.0);
- }
- //结束CG着色器编辑模块
- ENDCG
- }
- }
- Fallback "Diffuse"//默认着色器,这里选择漫反色
- }
凹凸映射着色器代码:
- //Shader模块定义
- Shader "xiaolezi/Bump Texture"
- {
- //属性设置
- Properties
- {
- //定义一个物体表面颜色,格式:[属性名]([Inspector面板显示名字],属性类型)=[初始值]
- _DiffuseColor("Main Color", Color) = (1, 1, 1, 1)
- _MainTex("Albedo(RGB)", 2D) = "white"{}
- _BumpMap("Normal Map", 2D) = "bump"{}
- _BumpScale("Bump Scale",Float)= 1.0
- _Glossness("Glossness", Range(8, 256)) = 20
- _SpecularColor("Specular Color", Color) = (1, 1, 1, 1)
- }
- //第一个SubShader块
- SubShader
- {
- //第一个Pass块
- Pass
- {
- //指定灯光渲染模式
- Tags{ "LightMode" = "ForwardBase" }
- //开启CG着色器编辑模块
- CGPROGRAM
- //定义顶点着手器函数名
- <span style="white-space:pre"> </span>#pragma vertex vert
- //定义片段着色器函数名
- <span style="white-space:pre"> </span>#pragma fragment frag
- //包含相关头文件
- <span style="white-space:pre"> </span>#include "UnityCG.cginc"
- <span style="white-space:pre"> </span>#include "Lighting.cginc"
- //定义一个从应用程序到顶点数据的结构体
- struct appdata
- {
- float4 vertex : POSITION;//POSITION语义:表示从该模型中获取到顶点数据
- float3 normal : NORMAL; //NORMAL语义:获取该模型法线
- float2 texcoord : TEXCOORD0;//TEXCOORD0语义:获取该模型纹理信息
- float4 tangent : TANGENT;//TANGENT语义:获取该模型切线坐标
- };
- //定义一个从顶点数据到片段数据的结构体
- struct v2f
- {
- float4 pos : SV_POSITION;//SV_POSITION语义:从顶点输出数据中获取到顶点数据
- float4 uv : TEXCOORD0;//TEXCOORD0语义:定义纹理贴图变量
- float3 lightDir : TEXCOORD1;//TEXCOORD1语义:定义灯光方向变量
- float3 viewDir : TEXCOORD2;//TEXCOORD2语义:定义观察方向变量
- };
- //从属性模块中取得该变量
- fixed4 _DiffuseColor;
- float _Glossness;
- fixed4 _SpecularColor;
- sampler2D _MainTex;
- float4 _MainTex_ST;
- sampler2D _BumpMap;
- float4 _BumpMap_ST;
- float _BumpScale;
- //顶点着色器函数实现
- v2f vert(appdata v)
- {
- v2f o;
- o.pos = UnityObjectToClipPos(v.vertex);//让模型顶点数据坐标从本地坐标转化为屏幕剪裁坐标
- //计算纹理坐标
- o.uv.xy = TRANSFORM_TEX(v.texcoord,_MainTex);
- o.uv.zw = TRANSFORM_TEX(v.texcoord, _BumpMap);
- //计算切线矩阵
- //float3 binormal = cross(normalize(v.normal), normalize(v.tangent.xyz)) * v.tangent.w;
- // float3x3 rotation = float3x3(v.tangent.xyz, binormal, v.normal);
- //这里直接使用内置宏 TANGENT_SPACE_ROTATION
- TANGENT_SPACE_ROTATION;
- //计算切线空间下的灯光方向
- o.lightDir = normalize(mul(rotation, ObjSpaceLightDir(v.vertex)));
- //计算切线空间下的观察方向
- o.viewDir = normalize(mul(rotation, ObjSpaceViewDir(v.vertex)));
- return o;
- }
- //片段着色器函数实现
- fixed4 frag(v2f f) : SV_Target//SV_Target语义:输出片元着色器值,可直接认为是输出到屏幕颜色
- {
- fixed4 normalAlbedo = tex2D(_BumpMap, f.uv.zw);
- fixed3 tangentNormal = UnpackNormal(normalAlbedo);
- tangentNormal.xy *= _BumpScale;
- //漫反色
- fixed3 albedo = tex2D(_MainTex, f.uv).rgb;
- float Lambert = 0.5 * dot(tangentNormal, f.lightDir) 0.5;//兰伯特值
- fixed3 diffuse = _LightColor0.rgb * _DiffuseColor.rgb * Lambert * albedo; //计算漫反色
- //环境光
- fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
- //高光
- fixed3 halfDir = normalize(f.lightDir f.viewDir);//根据物体表面法线计算光的反射光方向
- fixed3 specular = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(halfDir, tangentNormal)), _Glossness);//Phong氏高光计算
- return fixed4(ambient diffuse specular, 1.0);
- }
- //结束CG着色器编辑模块
- ENDCG
- }
- }
- Fallback "Bump Diffuse"//默认着色器,这里选择凹凸漫反色
- }
我这里所使用的都是基于Blinn-Phong光照模型进行实现。
好,我们先来了解一下纹理映射技术,该技术是实现在模型对应像素点上通过纹理坐标采样当前贴图的像素点从而控制模型当前像素点的颜色。所以关键的一点就是需要计算贴图的纹理坐标,即UV坐标。而对于不同图形编程语言,其对应的纹理坐标是不一样的,不过庆幸的是,Unity帮我们处理的这一点,统一了纹理坐标系统,如图:
通过上图可知道,纹理坐标是取值[0,1]。如果大于1,它会如何进行采样,则需要看纹理属性所设置的Wrap Mode模式来决定了,而设置该模式的选项在纹理属性面板中可以找到:
Unity提供两种方式,一种是重复(Repeat)采样,一种是拓展(Clamp)采样。
重复采样是指纹理坐标超出其限定范围时,会取其小数部分进行采样,例如目标纹理坐标为[-0.28,2.6],其实际纹理坐标为[0.72,0.6];
拓展采样是指纹理坐标超出其限定范围时,会取其限定范围,例如目标纹理坐标为[1.52,-0.5],其实际纹理坐标为[1,0]。
这里我给出一张纹理图:
我们对该纹理设置不同采样模式附加在材质中进行平铺(Tiling)两次,如下:
而材质中还有一个关键的属性,就是纹理贴图附加模型中的偏移量(Offset),这个偏移量很容易理解,一般纹理贴图附加在模型上默认有个附加基准点,而偏移量便是对这基准点的坐标进行偏移。
以下是对材质中设置不同偏移量的所显示的结果:
好,了解了纹理映射技术,先来说说实现过程的思路,如下:
1.在属性块中定义纹理属性;
2.Pass块中定义该纹理属性以及纹理平铺偏移量;
3.在结构体中定义相关属性;
3.在顶点着色器中计算纹理坐标;
4.在片段着色器中根据第2步所计算的纹理坐标对纹理贴图进行纹理采样,得到采样后的纹理颜色与表面颜色进行混合即可。
第一步,在属性中定义一张纹理图,默认是一张纯白色贴图:
- _MainTex("Albedo(RGB)", 2D) = "white"{}
第二步,Pass块中定义该纹理属性以及纹理平铺偏移量:
- sampler2D _MainTex;
- float4 _MainTex_ST;
- float4 var = 纹理名##_ST;
- ·x 包含X平铺值
- ·y 包含Y平铺值
- ·z 包含X偏移量
- ·w 包含Y偏移量
- //定义一个从应用程序到顶点数据的结构体
- struct appdata
- {
- ...
- float2 texcoord : TEXCOORD0;//TEXCOORD0语义:获取该模型纹理坐标
- };
- //定义一个从顶点数据到片段数据的结构体
- struct v2f
- {
- ...
- float2 uv : TEXCOORD1;//TEXCOORD1语义:定义纹理贴图变量
- };
- //计算纹理坐标
- //o.uv = v.texcoord.xy * _MainTex_ST.xy _MainTex_ST.zw;
- //这里使用内置宏 TRANSFORM_TEX
- o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
计算纹理坐标是通过从应用程序中获取本地纹理坐标,然后对本地纹理坐标进行纹理贴图的一个计算。
第五步,在片段着色器中根据第2步所计算的纹理坐标对纹理贴图进行纹理采样,得到采样后的纹理颜色与表面颜色进行混合:
- //漫反色
- fixed3 albedo = tex2D(_MainTex, f.uv);
- float Lambert = 0.5 * dot(normalDir, f.lightDir) 0.5;//兰伯特值
- fixed3 diffuse = _LightColor0.rgb * _DiffuseColor.rgb * Lambert * albedo;
纹理采样函数tex2D,第一个传入的是需要采样的纹理,第二个参数是采样的纹理坐标,这个我们在顶点着色器已求得,既然是混合,就直接进行相乘操作。
接着来讲讲凹凸映射(bump mapping)的原理。
通过上面的学习了解,我们已经知道如何对模型进行贴图操作。但是往往这是不够的,因为你所看到的可能有些是凹凸不齐的,更多的并不是一张图片贴上去那么简答。比如石头、砖块、墙等等。
有些人会提出,直接在贴图上显示这样的效果不就好了?其实不然,如果你这一面看上去的确参差不齐,但是你换个角度看应该会有不一样的效果的,所以便引申出了凹凸映射。
凹凸映射实际上就是使用一张纹理来修改模型表面的法线,以便为模型提供更多的细节。
对于凹凸映射所使用的纹理,主要有两种:
一种是高度图(height map),这种纹理是模拟表面位移,然后得到一个修改后法线的值;
一种是法线贴图(normal map),这一种是直接存储表面法线。如下图所示:
关于法线贴图,我们来探讨一下下面两个问题:
1.为什么模型空间下和切线空间下的法线贴图所展现的区别很大。
2.模型空间下的法线贴图和切线空间下的法线贴图使用的优劣。
在这之前,我们需要知道,法线纹理存储的是模型表面法线的方向。而表面法线(normal)方向取值[-1,1],像素(pixel)的分量为范围是[0,1]。所以这里存在着这么一个转换关系:pixel = (normal 1) / 2;
这就意味着当我们对法线贴图进行采样后还需要对其进行相对应的映射才能得到正确的法线方向。
好,现在再来解决上面的两个问题:
第一个问题:为什么模型空间下和切线空间下的法线贴图所展现的区别很大?
模型空间下的法线贴图显得五颜六色。这是由于在模型空间下,所有法线的坐标空间是同一坐标空间,即模型空间,又因为每个顶点的法线方向是各异的,所以通过纹理映射后取值结果会很多,例如法线(1,0,1),通过映射之后得到颜色值为(1,0.5,1),即紫色,又或者法线(0,1,0),通过映射之后得到颜色值为(0.5,1,0.5),即浅绿色。
切线空间下的法线贴图就显得偏浅蓝色,这是因为每个顶点法线所在的坐标空间都不一样,即表面每个顶点的切线空间,这种法线纹理所存储的其实就是各个顶点在各自的切线空间中的法线扰动方向,下图展示的是模型中某地所在的切线空间:
所以我们来假设一下,如果每个顶点的法线方向是不变的,那么在它的切线空间中,新的法线方向就是z轴方向,即值为(0,0,1),通过映射可得到其颜色值为(0.5,0.5,1),即浅蓝色。所以这些蓝色说明了顶点的大部分法线是和模型本身法线一致,不需要改变。
第二个问题:模型空间下的法线贴图和切线空间下的法线贴图使用的优劣。
可以看出,使用模型空间下的法线贴图更符合人类的直观认识,但是,模型空间中的法线贴图是绝对法线信息,如果使用另外的模型,那么,一切也将变得错误。而相对而言,切线空间下的法线贴图是相对法线信息,即便模型改变,但是依旧可以很好的融合进去,借助这一有点,很多像水、熔岩等动态凹凸效果,可以很好的被使用进来。当然,这里只是点一下主要的优劣。相信通过这个主要的优劣,我觉得很多人都会选择使用切线空间下的法线贴图来进行纹理的凹凸映射。
好,现在我们来看看切线空间下实现凹凸映射的着色器代码。
实现过程思路:
1.在属性块中定义凹凸映射相关属性;
2.在Pass块中重新定义凹凸映射相关属性;
3.在结构体中定义相关属性;
4.在顶点着色器中把光照方向以及观察坐标方向转换为切线空间下坐标;
5.在片段着色器中采样切线空间下的法线贴图并得到正确的法线,使用该法线进行相关计算得到最终效果。
有了上面的思路,我们再来一步一步分析:
第一步,在属性块中定义凹凸映射相关属性:
- //属性设置
- Properties
- {
- ...
- _BumpMap("Normal Map", 2D) = "bump"{}
- _BumpScale("Bump Scale",Float)= 1.0
- }
第二步,在Pass块中重新定义凹凸映射相关属性:
- sampler2D _BumpMap;
- float4 _BumpMap_ST;
- float _BumpScale;
- //定义一个从应用程序到顶点数据的结构体
- struct appdata
- {
- <span style="white-space:pre"> </span>...
- float3 normal : NORMAL; //NORMAL语义:获取该模型法线
- float2 texcoord : TEXCOORD0;//TEXCOORD0语义:获取该模型纹理信息
- float4 tangent : TANGENT;//TANGENT语义:获取该模型切线坐标
- };
- //定义一个从顶点数据到片段数据的结构体
- struct v2f
- {
- ...
- float4 uv : TEXCOORD0;//TEXCOORD0语义:定义纹理贴图变量
- };
第四步:在顶点着色器中把光照方向以及观察坐标方向转换为切线空间下坐标:
- //顶点着色器函数实现
- v2f vert(appdata v)
- {
- v2f o;
- ...
- //计算纹理坐标
- ...
- o.uv.zw = TRANSFORM_TEX(v.texcoord, _BumpMap);
- //计算切线矩阵
- //float3 binormal = cross(normalize(v.normal), normalize(v.tangent.xyz)) * v.tangent.w;
- // float3x3 rotation = float3x3(v.tangent.xyz, binormal, v.normal);
- //这里直接使用内置宏 TANGENT_SPACE_ROTATION
- TANGENT_SPACE_ROTATION;
- //计算切线空间下的灯光方向
- o.lightDir = normalize(mul(rotation, ObjSpaceLightDir(v.vertex)));
- //计算切线空间下的观察方向
- o.viewDir = normalize(mul(rotation, ObjSpaceViewDir(v.vertex)));
- return o;
- }
求得切线矩阵之后,我们直接对光方向以及观察方向与切线矩阵进行相乘完成切线空间下的转化。
第五步,在片段着色器中采样切线空间下的法线贴图并得到正确的法线,使用该法线进行相关计算得到最终效果:
- //片段着色器函数实现
- fixed4 frag(v2f f) : SV_Target//SV_Target语义:输出片元着色器值,可直接认为是输出到屏幕颜色
- {
- fixed4 normalAlbedo = tex2D(_BumpMap, f.uv.zw);
- fixed3 tangentNormal = UnpackNormal(normalAlbedo);
- tangentNormal.xy *= _BumpScale;
- //漫反色
- fixed3 albedo = tex2D(_MainTex, f.uv).rgb;
- float Lambert = 0.5 * dot(tangentNormal, f.lightDir) 0.5;//兰伯特值
- fixed3 diffuse = _LightColor0.rgb * _DiffuseColor.rgb * Lambert * albedo;
- //环境光
- fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
- //高光
- fixed3 halfDir = normalize(f.lightDir f.viewDir);//根据物体表面法线计算光的反射光方向
- fixed3 specular = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(halfDir, tangentNormal)), _Glossness);//Phong氏高光计算
- return fixed4(ambient diffuse specular, 1.0);
- }
还有一个关键点,就是对于你的法线贴图,需要在Unity编辑器中对其属性中的贴图类型进行设置,把Texture Type设置为“Normal map”即可:
以上便是在Unity中使用Shader中实现对模型纹理的添加以及实现纹理中凹凸的映射。希望对读者有所帮助。
如果你想要下载源码,可以在GitHub仓库中找到:GitHub仓库
Happy Coding...