第二十课:钟表小人案例;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.0

3.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; //材质参数ID

Private私有变量正常情况是看不了的,单将模式改为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参数

参考资料:

庄懂的技术美术入门课(美术向)-直播录屏-第20课_哔哩哔哩_bilibili