Unity——Shader学习20——From庄懂(特特效篇):钟表小人案例;TA基本流程:美术资源、Shader、脚本(钟表有关的绘画故事)钟表小故事绘画,
第二十课:钟表小人案例;TA基本流程:美术资源、Shader、脚本
庄懂的技术美术入门课(美术向)-直播录屏-第20课_哔哩哔哩_bilibiliwww.bilibili.com/video/BV1ia411c7bk本课任务:
模型、贴图准备Shader代码C#脚本同步系统时间一、模型、贴图准备
1.模型准备:
为小人模型添加上时针、分针、秒针,并为其分布添加红、绿、蓝顶点色用于Mask
2.SD处理纹理
2.1将之前的小人去脸(每个纹理都要:Main、Normal、AO、Specular)
2.2制作表盘Mask,输入给每个纹理
2.3制作表针Mask,输入给每个纹理
二、Shader代码
3.Shader代码:
以L10_OldSchoolPro作为模板
3.1时钟动画参数
//【时钟动画参数】 [Header(ClockAnim)] _HourAngle ("时针角度" , Range(0.0 ,360.0)) = 0.0 _MinuteAngle ("分针角度" , Range(0.0 ,360.0)) = 0.0 _SecondAngle ("秒针角度" , Range(0.0 ,360.0)) = 0.0 _RotateOffset ("旋转中心偏移" , Float) = 0.03.2输入结构追加:顶点色;顶点着色器使用时钟动画函数
//输入结构部分代码 float4 color0 : COLOR0; //顶点色,用于动画的Mask //顶点着色器部分代码 VertexOutput vert(VertexInput v) { VertexOutput o = (VertexOutput)0; clockAnimation(v.vertex.xyz , v.color0.rgb); //顶点动画3.3时钟动画函数(重点)
3.3.1 定义旋转动画函数(带旋转中心偏移)RotateAnimationZ
为什么要带旋转中心偏移:因为物体旋转中心默认为模型原点,我们需要先将旋转中心偏移到物体原点,再进行旋转操作,最后在将旋转中心偏移回原来位置,这样时针、分针、秒针才能绕着时钟中心旋转,而不是模型本身的原点。
3.3.2 先进行旋转中心偏移,注意乘mask
3.3.3 进行正常的旋转操作,注意要乘mask,根据模型本身,这里是绕着模型空间的z轴进行旋转,(因此我故意在视频里面改变模型世界旋转,对时钟无影响)
3.3.4 最后将旋转中心偏移回去,注意乘mask
3.3.5 应用时钟动画函数:分别给时针,分针,秒针应用旋转函数
//【旋转函数,输入:顶点、Mask、角度、旋转中心偏移】 void RotateAnimationZ(inout float3 vertex , float mask , float angle , float offset) { //【旋转中心移至原点】 vertex.y += offset * mask; //旋转中心偏移,我们需要将旋转中心移动至物体原点 //【旋转角度】 float radianZ = radians(angle * mask); //注意要乘mask,只有mask部分才做旋转 //【定义旋转矩阵】 float sin_radianZ , cos_radianZ = 0.0; //变量赋初值 sincos(radianZ , sin_radianZ , cos_radianZ); //变量重新计算值 float2x2 Rotate_Matrix_Z = float2x2(cos_radianZ , -sin_radianZ , sin_radianZ , cos_radianZ); //绕Z轴的旋转矩阵 //【绕偏移中心旋转】 vertex.xy = mul(Rotate_Matrix_Z , float2(vertex.x , vertex.y)); //【将顶点移至原位】 vertex.y -= offset * mask; //将顶点移动至原位 } //【时钟动画应用】 void clockAnimation(inout float3 vertex , float3 color) //我们不用输出color,所以color就不用inout { RotateAnimationZ(vertex , color.r , _HourAngle , _RotateOffset); //时针旋转 RotateAnimationZ(vertex , color.g , _MinuteAngle , _RotateOffset); //分针旋转 RotateAnimationZ(vertex , color.b , _SecondAngle , _RotateOffset); //秒针旋转 }Shader具体代码:
Shader "AP1/L20/ClockMan" { Properties { [Header(Texture)] //设置注释 _MainTex ("RGB:基础颜色 , A:AO" , 2D) = "white"{} _BumpMap ("NormalMap" , 2D) = "bump"{} _BumpInt ("BumpInt" , Float) = 1.0 _CubeMap ("CubeMap" , Cube) = "_Skybox"{} _SpecTex ("RGB:高光颜色 ;A:高光次幂(光滑度)" , 2D) = "gray"{} _EmiTex ("RGB:自发光贴图", 2D) = "balck"{} [Header(Diffuse)] _MainCol ("MainCol" , Color) = (1.0 , 1.0 , 1.0 , 1.0) _EnvUpCol ("EnvUpCol" , Color) = (1.0 , 1.0 , 1.0 , 1.0) _EnvSideCol ("EnvSideCol" , Color) = (0.5 , 0.5 , 0.5 , 1.0) _EnvDownCol ("EnvDownCol" , Color) = (0.0 , 0.0 , 0.0 , 1.0) _EnvDiffInt ("EnvDiffInt" , Float) = 1.0 [Header(Specular)] _SpecPow ("SpecPow" , Range(1.0 , 255.0)) = 5.0 _EnvSpecInt ("EnvSpecInt" , Float) = 1.0 _FresnelPow ("FresnelPow" , Range(0.0 , 8.0)) = 1.0 _CubeMapMip ("CubeMapMip" , Range(0.0 , 7.0)) = 0.0 [Header(Emissive)] _EmissiveInt("EmissiveInt" , Float) = 0.0 //【时钟动画参数】 [Header(ClockAnim)] _HourAngle ("时针角度" , Range(0.0 ,360.0)) = 0.0 _MinuteAngle ("分针角度" , Range(0.0 ,360.0)) = 0.0 _SecondAngle ("秒针角度" , Range(0.0 ,360.0)) = 0.0 _RotateOffset ("旋转中心偏移" , Float) = 0.0 } SubShader { Tags { "RenderType" = "Opaque" } Pass { Name "FORWARD" Tags { "LightMode" = "ForwardBase" } CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" #include "Lighting.cginc" #include "AutoLight.cginc" #include "../../../TCfunction/TC_CGinc.cginc" //..的意思是上一级文件夹,第一个..表示本级文件夹 #pragma multi_compile_fwdbase_fullshadows #pragma target 3.0 //Parameter //Texture uniform sampler2D _MainTex; uniform sampler2D _BumpMap; uniform float4 _BumpMap_ST; uniform float _BumpInt; uniform samplerCUBE _CubeMap; uniform sampler2D _SpecTex; uniform sampler2D _EmiTex; //Diffuse uniform fixed3 _MainCol; uniform fixed3 _EnvUpCol; uniform fixed3 _EnvSideCol; uniform fixed3 _EnvDownCol; uniform float _EnvDiffInt; //Specular uniform float _SpecPow; uniform float _EnvSpecInt; uniform float _FresnelPow; uniform float _CubeMapMip; //Emissive uniform float _EmissiveInt; //ClockAnim uniform float _HourAngle; uniform float _MinuteAngle; uniform float _SecondAngle; uniform float _RotateOffset; struct VertexInput { float4 vertex : POSITION; float2 uv0 : TEXCOORD0; float3 normal : NORMAL; float4 tangent: TANGENT; float4 color0 : COLOR0; //顶点色,用于动画的Mask }; struct VertexOutput { float4 pos : SV_POSITION; float4 uv0 : TEXCOORD0; float3 posWS : TEXCOORD1; float3 nDirWS : TEXCOORD2; float3 tDirWS : TEXCOORD3; float3 bDirWS : TEXCOORD4; LIGHTING_COORDS(5,6) }; //【旋转函数,输入:顶点、Mask、角度、旋转中心偏移】 void RotateAnimationZ(inout float3 vertex , float mask , float angle , float offset) { //【旋转中心移至原点】 vertex.y += offset * mask; //旋转中心偏移,我们需要将旋转中心移动至物体原点 //【旋转角度】 float radianZ = radians(angle * mask); //注意要乘mask,只有mask部分才做旋转 //【定义旋转矩阵】 float sin_radianZ , cos_radianZ = 0.0; //变量赋初值 sincos(radianZ , sin_radianZ , cos_radianZ); //变量重新计算值 float2x2 Rotate_Matrix_Z = float2x2(cos_radianZ , -sin_radianZ , sin_radianZ , cos_radianZ); //绕Z轴的旋转矩阵 //【绕偏移中心旋转】 vertex.xy = mul(Rotate_Matrix_Z , float2(vertex.x , vertex.y)); //【将顶点移至原位】 vertex.y -= offset * mask; //将顶点移动至原位 } //【时钟动画应用】 void clockAnimation(inout float3 vertex , float3 color) //我们不用输出color,所以color就不用inout { RotateAnimationZ(vertex , color.r , _HourAngle , _RotateOffset); //时针旋转 RotateAnimationZ(vertex , color.g , _MinuteAngle , _RotateOffset); //分针旋转 RotateAnimationZ(vertex , color.b , _SecondAngle , _RotateOffset); //秒针旋转 } VertexOutput vert(VertexInput v) { VertexOutput o = (VertexOutput)0; clockAnimation(v.vertex.xyz , v.color0.rgb); //顶点动画 o.pos = UnityObjectToClipPos(v.vertex); o.uv0.xy = v.uv0; o.uv0.zw = v.uv0 * _BumpMap_ST.xy + _BumpMap_ST.zw; o.posWS = mul(unity_ObjectToWorld,v.vertex).xyz; o.nDirWS = UnityObjectToWorldNormal(v.normal); o.tDirWS = UnityObjectToWorldDir(v.tangent.xyz); o.bDirWS = cross(o.nDirWS , o.tDirWS) * v.tangent.w; TRANSFER_VERTEX_TO_FRAGMENT(o) return o; } float4 frag(VertexOutput i) : COLOR { //vector fixed4 var_BumpMap = tex2D(_BumpMap , i.uv0.zw); fixed3 nDirTS = UnpackNormal(var_BumpMap); nDirTS.xy *= _BumpInt; nDirTS.z = sqrt(1.0 - saturate(dot(nDirTS.xy , nDirTS.xy))); nDirTS = normalize(nDirTS); float3x3 TBNtw = float3x3(i.tDirWS , i.bDirWS , i.nDirWS); fixed3 nDirWS = mul(nDirTS , TBNtw); fixed3 lDirWS = normalize(UnityWorldSpaceLightDir(i.posWS)); fixed3 vDirWS = normalize(UnityWorldSpaceViewDir(i.posWS)); fixed3 hDirWS = normalize(lDirWS + vDirWS); fixed3 vrDirWS = reflect(-vDirWS , nDirWS); //sample texture fixed4 var_MainTex = tex2D(_MainTex , i.uv0.xy); fixed4 var_SpecTex = tex2D(_SpecTex , i.uv0.xy); fixed3 var_EmiTex = tex2D(_EmiTex , i.uv0.xy); fixed3 var_CubeMap = texCUBElod(_CubeMap , float4(vrDirWS , lerp(_CubeMapMip , 0.0 ,var_SpecTex.a))); //用高光次幂lerp,高光次幂越大的地方,lerp值越接近第二个,即0,越光滑;高光次幂越小,lerp值越接近设定的Mip值,越粗糙 //DirLight //DirDiff fixed lambert = max(0.0 , dot(nDirWS , lDirWS)); fixed3 albedo = var_MainTex.rgb * _MainCol * lambert; //DirSpec fixed blinn = max(0.0 , dot(hDirWS , nDirWS)); fixed dirSpecPow = lerp( 1.0 ,_SpecPow , var_SpecTex.a) ; //通过高光次幂贴图,来控制power的值,贴图越亮处,power值越大 fixed3 DirSpecCol = pow(blinn , dirSpecPow) * var_SpecTex.rgb; //DirMix fixed shadow = LIGHT_ATTENUATION(i); fixed3 DirlightCol = (albedo + DirSpecCol) * _LightColor0 * shadow; //_LightColor0是光源的颜色,Unity自带 //EnvModel //EnvDiff fixed3 triEnvCol = triColAmbient(nDirWS , _EnvUpCol , _EnvSideCol , _EnvDownCol); //用了triColAmbient自定义函数 fixed3 envDiffCol = triEnvCol * var_MainTex.rgb * _EnvDiffInt; //EnvSpec fixed fresnel = pow(1.0-max(0.0 , dot(vDirWS , nDirWS)) , _FresnelPow); fixed3 envSpecCol = var_CubeMap * fresnel * _EnvSpecInt; //EnvMix fixed occlusion = var_MainTex.a; fixed3 envCol = (envDiffCol + envSpecCol) * occlusion; //EmissiveModel fixed3 emissive = var_EmiTex * _EmissiveInt; fixed3 finalCol = DirlightCol + envCol + emissive; return float4(finalCol , 1.0); } ENDCG } } FallBack "Diffuse" }三、C#脚本初识
选择IDE,如果是VSCode,可以安装 C#插件 和 Debugger for Unity 插件
创建C# Scrpit :创建C# Script脚本时就改名字,不要乱改脚本名,因为MonoBehaviour这类脚本的类名和脚本名得相等,如果要改,则类名和脚本名都要改
关于C#脚本
MonoBehaviour类型的脚本:该脚本能够继承GameObject的原有功能,在此基础上追加新的功能。因此该脚本可以直接追加到模型下
Start函数:在游戏开始运行时调用
Update函数:在游戏的每一帧都会调用
初识C#脚本:HelloWorld
在Console窗口上输出log
void Start() { Debug.Log("Hello World"); //游戏开始时,在Console窗口上输出log } // Update is called once per frame void Update() { Debug.Log("The world gose on"); //游戏运行时每一帧都会在Console窗口上输出log }将C#脚本文件拖入场景中的一个GameObject,运行游戏
第一行为"Hello World",之后不断输出"The world gose on"四、编写系统时间脚本
1.引用命名空间
using System 获取系统时间
using UnityEngine 获取Unity里面的内容
using System; // 主命名空间 包含所有.net基础类型和通用类型 这里用于获取系统时间 using UnityEngine; // Unity引擎命名空间 这里需使用UnityEngine定义的相关类和类方法,(如下面的 Material类 ,就需要)2.添加类
Public公共类:添加材质类
public Material clockMaterial; //public:材质类在脚本上,有个公共的材质类可以选择,将上边的“时钟材质”拖进去,(注:只有公共的类才可以出现)
Private私有类
定义一个 bool类型 的变量,判断脚本是否有效,我们希望材质不是“时钟材质”时,不启用该脚本
定义 材质参数ID(int型变量);C#脚本不会根据材质变量的参数名来使用变量,而是将材质参数转化为一个个ID,用ID来使用
//-----private----- private bool valid; //用于判断脚本有效性(默认为False) private int hourAngleID; //材质参数ID private int minuteAngleID; //材质参数ID private int secondAngleID; //材质参数IDPrivate私有变量正常情况是看不了的,单将模式改为Debug可以看到了
start函数
1.判断是否有材质,没有则跳过初始化
2.缓冲材质ID(即Get材质变量名,赋给ID),用PropertyToID函数
3.根据是否Get到材质变量(即所用材质是否存在上面的材质变量名),判断脚本的有效性,用HasProperty函数判断,最后对变量valid赋值
4.可以用输出log的方式Debug
void Start() { //【如果没有材质,则跳过初始化】 if (clockMaterial == null) return; //【缓冲材质属性ID】 hourAngleID = Shader.PropertyToID("_HourAngle"); //去Get公共材质变量 minuteAngleID = Shader.PropertyToID("_MinuteAngle"); //去Get公共材质变量 secondAngleID = Shader.PropertyToID("_SecondAngle"); //去Get公共材质变量 //【是否Get到材质变量,判断脚本有效性】 if (clockMaterial.HasProperty(hourAngleID) && clockMaterial.HasProperty(minuteAngleID) && clockMaterial.HasProperty(secondAngleID)) valid = true; //试着Debug Debug.Log("hourAngleID=" + hourAngleID); //输出log,hourAngleID Debug.Log("minuteAngleID=" + minuteAngleID); //输出log,minuteAngleID Debug.Log("secondAngleID=" + secondAngleID); //输出log,secondAngleID Debug.Log(valid); //输出log,valid }赋予了正确材质的Debug结果UpDate函数
1.用变量valid判断脚本有效性,无效则直接退出
2.设置秒针的旋转值
Get系统时间:秒(int型,0~60),将其转为角度(float浮点型)
更新材质的变量参数,因为材质变量为float型,所以这里用SetFloat函数
//【判断有效性,无效则直接退出】 if (!valid) return; //【秒针旋转】 int sysSecond = System.DateTime.Now.Second; //Get系统时间:秒(int型) float secondRot = sysSecond / 60.0f * 360.0f; //将时间转为角度,f是浮点数 clockMaterial.SetFloat(secondAngleID , secondRot); //更新所给材质的参数,因为是float类型的变量,所以这里用SetFloat函数关于SetFloat函数,这里的输入变量类型有2种写法,可以通过下面方法查看
第一种方法:变量名字
第二种方法:ID (Unity推荐,性能好)
用类似的方法处理分针和时针:
附加操作:平滑过渡,防止跳变:
给分针和时针的计算后面都加上了根据上一级计算的结果,进行平滑过渡
//【分针旋转】 int sysMinute = System.DateTime.Now.Minute; //Get系统时间:分 float minuteRot = sysMinute / 60.0f * 360.0f + secondRot /360.0f * 6.0f; //平滑过渡:+ secondRot /360.0f * 6.0f,秒针转一圈,分钟转6度 clockMaterial.SetFloat(minuteAngleID , minuteRot); //更新所给材质的参数 //【时针旋转】 int sysHour = System.DateTime.Now.Hour; //Get系统时间:时 float hourRot = (sysHour % 12) / 12.0f * 360.0f + minuteRot / 360.0f * 30.0f; //(sysHour % 12) 取余数,如13%12=1 // 12.0f * 360.0f ----12h/圈; //平滑过渡:+ minuteRot / 360.0f * 30.0f ,分针转360度,时针转60度,这句是为了防止时针跳变 clockMaterial.SetFloat(hourAngleID , hourRot); //更新所给材质的参数C#脚本具体代码:
//引用命名空间 using System; // 主命名空间 包含所有.net基础类型和通用类型 这里用于获取系统时间 using UnityEngine; // Unity引擎命名空间 这里需使用UnityEngine定义的相关类和类方法,(如下面的 Material类 ,就需要) public class SystemTime : MonoBehaviour { //-----public----- public Material clockMaterial; //public:材质类 //-----private----- private bool valid; //用于判断脚本有效性(默认为False) private int hourAngleID; //材质参数ID private int minuteAngleID; //材质参数ID private int secondAngleID; //材质参数ID void Start() { //【如果没有材质,则跳过初始化】 if (clockMaterial == null) return; //【缓冲材质属性ID】 hourAngleID = Shader.PropertyToID("_HourAngle"); //去Get公共材质变量 minuteAngleID = Shader.PropertyToID("_MinuteAngle"); //去Get公共材质变量 secondAngleID = Shader.PropertyToID("_SecondAngle"); //去Get公共材质变量 //【是否Get到材质变量,判断脚本有效性】 if (clockMaterial.HasProperty(hourAngleID) && clockMaterial.HasProperty(minuteAngleID) && clockMaterial.HasProperty(secondAngleID)) valid = true; } void Update() { //【判断有效性,无效则直接退出】 if (!valid) return; //【秒针旋转】 int sysSecond = System.DateTime.Now.Second; //Get系统时间:秒(int型) float secondRot = sysSecond / 60.0f * 360.0f; //将时间转为角度,f是浮点数 clockMaterial.SetFloat(secondAngleID , secondRot); //更新所给材质的参数,因为是float类型的变量,所以这里用SetFloat函数 //【分针旋转】 int sysMinute = System.DateTime.Now.Minute; //Get系统时间:分 float minuteRot = sysMinute / 60.0f * 360.0f + secondRot /360.0f * 6.0f; //平滑过渡:+ secondRot /360.0f * 6.0f,秒针转一圈,分钟转6度 clockMaterial.SetFloat(minuteAngleID , minuteRot); //更新所给材质的参数 //【时针旋转】 int sysHour = System.DateTime.Now.Hour; //Get系统时间:时 float hourRot = (sysHour % 12) / 12.0f * 360.0f + minuteRot / 360.0f * 30.0f; //(sysHour % 12) 取余数,如13%12=1 // 12.0f * 360.0f ----12h/圈; //平滑过渡:+ minuteRot / 360.0f * 30.0f ,分针转360度,时针转60度,这句是为了防止时针跳变 clockMaterial.SetFloat(hourAngleID , hourRot); //更新所给材质的参数 } }第二十课总结:
基本上是TA的正常工作套路:美术资源的制作,Shader的编写,脚本的编写Shader:用顶点色作Mask,对时针、分针、秒针进行旋转操作,暴露旋转角度给脚本调整C#Script:Get系统时间,通过一定的算法得到对应每个针的旋转角度,更新Shader参数参考资料:
- 支付宝扫一扫
- 微信扫一扫