catlike coding base篇之一
前言
决定自己学习一遍unity的经典教程catlike coding,因为catlike coding是一个非常好的教程,学习的东西比较全面,小白能全方面学习,大佬能查漏补缺。这是个人学习笔记,勿喷,欢迎各位指错。
教程源地址:https://catlikecoding.com/unity/tutorials/
一、颜色空间(个人笔记,看不懂可跳过)
1、什么是颜色空间?
颜色空间是指一个色彩模型里面(计算机图形学里面最常用的是RGB)的任意值通过某种特定的方法计算出来另外一个值,而色彩空间里面某种特定的计算方法在Unity中有两种关系,分别为线性关系和Gamma关系,所有Unity中color space可以选中两种工作流(线性工作流与Gamma工作流)。
关于Color space、Gamma、Linear和sRGB有关的视频讲解:
https://www.bilibili.com/video/BV1k64y1o7Ni/?spm_id_from=333.337.search-card.all.click&vd_source=d6b9e3a22052f82e07e715a82d495b9a
2、什么是Linear、Gamma、sRGB
I、Linear
首先拿光照为例子,在现实物理里,光的亮度和光的粒子数量有关,而线性关系是光的强度增强一倍,那么光的亮度就跟着增强一倍。
II、Gamma和sRGB
Gamma最早来源于CRT显示器。且电压和亮度之间的关系为i = u^22 所以早期的CRT显示器的Gamma值均为2.2,而在现代的LED显示器其实已经不需要Gamma了,为了兼容以前的Gamma = 2.2,现代显示器的Gamma也是2.2。sRGB则是处于Gamma 0.45,sRGB的目的是为了使亮度从0到1之间使得颜色变得均匀,如下图分别是人眼感知(Perceived brightness)和在Gamma2.2中的物理真实(Physical brightness):
0.45 * 2.2 约等于1,我们就有i = (u ^ 0.45) ^ 2.2,就能得到 i = u ^ 0.99 从而实现出Linear的效果。
III、Linear工作流和Gamma工作流的流程
两工作流如图所示:
Linear工作流:
Linear Texture Gamma 1.0 —> Linear Gamma 1.0 —> Shader —> Gamma矫正 —> Gamma 0.45 —> Gamma 1.0 —> 从显示器看到的颜色 = 现实世界
Gamma工作流
sRBG Texture Gamma0.45 —> Shader —> Gamma 2.2 -->显示器看到的颜色
总结:
Gamma工作流性能好。贴图不用进行关照计算的时候。
Gamma工作流的影响范围:插值、光照、透明混合。
Linear工作流相对步骤多而复杂,但是效果好,这个开销是值得的,不会因为Gamma矫正而引起曝光问题。
Unity中Gamma工作流和Linear工作流的设置
Prohect Settings > Player > other setting >Color space
二、使用Unity实现时钟
由于此处是第一次使用到这个Unity工程,我们需要把Unity工程的色彩空间改成Linear工作流,主要是想要显示的色彩效果更好,关于介绍上篇有。
选择Unity的左上角的工具栏里面的Edit,点击打开Project Settings选中Player,里面的Other Setting的Color Space选择Linear,(注意:)【这里一般是要在项目开始的时候决定使用哪个色彩空间,当项目工程非常庞大的时候,转换色彩空间会消耗非常多的时间,并且一些项目里的光照效果需要重新调整】。
I、实现步骤
1、在场景中创建游戏对象
创建时钟圆盘
在Hierachy面板上右击,选择Create Empty创建一个空对象,并且命名为Clock,此时这个物体只挂载着Transform组件。
鼠标右击Clock对象,并且把摄像机对准z轴,设置坐标为(0,0,-20)。
创建一个3d物体对象,选中并且创建Cylinder,并且把名字改为Face,Transform中的Position的XYZ设置为(0,0,0)、Rotation的XYZ设置为(90,0,0),Scale的XYZ设置为(10,0.2,10),最终效果如下图
创建时钟刻度
我们需要先准备三个材质球(创建方式,在Project面板下右击鼠标放到Create上面,选中Materal),分别为ClockArm、HourIndicator、SecondArm用于时针分针、12刻度以及秒针的材质,如下图:
以上是创建一个时钟的脸盘和需要用到的材质球,下面对时钟的12个刻度进行创建。创建刻度对象,位置最上面的刻度坐标设定为(0,4,-0.25),把材质球HourIndicator拖拽上这份对象,并且复制为12个,如图:
可以把刻度对象通过Scene面板编辑位置,这样很麻烦。这里需要写一个脚本通过代码运行把这些刻度修改到正确的位置上。
public class ClockInit : MonoBehaviour
{
//存放时钟十二个指示的引用
[HideInInspector] private List<Transform> _hourIndocators = new List<Transform>();
//x长度为4
[HideInInspector] private float _x = 4.0f;
//y长度为4
[HideInInspector] private float _y = 4.0f;
private void Awake()
{
Transform clockTran = GameObject.Find("Clock").transform;
for (int i = 1; i < 13; i++)
{
_hourIndocators.Add(clockTran.GetChild(i));
}
//计算各个指示器的位置和旋转
float radius = 360.0f / 12.0f;
//刻度1 - 12
for (int i = 0;i < _hourIndocators.Count;i++)
{
float sin = Mathf.Sin(radius * i * Mathf.Deg2Rad);
float cos = Mathf.Cos(radius * i * Mathf.Deg2Rad);
_hourIndocators[i].localPosition = new Vector3(_x * sin ,_y * cos , -0.25f);
_hourIndocators[i].localRotation = Quaternion.Euler(new Vector3(0.0f,0.0f,- radius * i));
}
}
}
创建这个ClockInit脚本并且挂到摄像机上面。点击运行,这样刻度就都在正确的位置上面了。如下图:
其实原理很简单,我们已这个圆盘的中间为圆心建立xy坐标系,就有如下刻度1的坐标计算:
假设刻度1的坐标为(x,y,-0.25),则x = x * sin(30),y = y* cos(30),所以则有如下代码:
float sin = Mathf.Sin(radius * i * Mathf.Deg2Rad);
float cos = Mathf.Cos(radius * i * Mathf.Deg2Rad);
_hourIndocators[i].localPosition = new Vector3(_x * sin ,_y * cos , -0.25f);
_hourIndocators[i].localRotation = Quaternion.Euler(new Vector3(0.0f,0.0f,- radius * i));
里面的i代表了第几个刻度。并且需要修改改物体围绕z轴的旋转角度,刚刚好是刻度索引 * -1 * 360 / 刻度数量。
创建时针、分针、秒针
如上图所示,为Clock对象创建一个空的子物体并且命名为HoursArmPivot,设置坐标为(0,0,0),而为该物体再创建出来的一个Cube的子物体,并且把Psotion的XYZ设置为(0,0.75,-0.25),Scale设置为(0.3,2.5,0.1)
这样就把时针创建出来了。当我们让HoursArmPivot对象围绕z轴旋转时候,会如下效果:
这样就实现了,时钟的效果,重复以上步骤创建出来分针和秒针。
由于分针更长跟细小,所以坐标和缩放设置为:
由于秒针的坐标和缩放设置为:
这样我们就创建出来了一个有12刻度、时针、分针和秒针的一个时钟对象。
让这个时钟对象动起来
创建一下的Clock脚本,把他挂在到Clock对象上就能让时钟动起来。把注释的部分1取消注释,把下面的部分2注释掉,是两种不同的时钟效果,可以尝试一下。原来就是获取系统时间,根据系统时间按照游戏的运行帧来修改时针、分针、秒针的角度。
/// <summary>
/// 时钟脚本
/// </summary>
public class Clock : MonoBehaviour
{
//引用时针、分针、秒针的pivot的transform
[HideInInspector] private Transform _hoursPivot,_minutesPivot,_secondsPivot;
[HideInInspector] private const float _hoursToDegrees = -30f, _minutesToDegrees = -6f, _secondsToDegrees = -6f;
private void Start()
{
_hoursPivot = GameObject.Find("Clock/HoursArmPivot").transform;
_minutesPivot = GameObject.Find("Clock/MinutesArmPivot").transform;
_secondsPivot = GameObject.Find("Clock/SecondsArmPivot").transform;
}
private void Update()
{
//部分1 秒针抖动版时钟
//var time = DateTime.Now;
//_hoursPivot.localRotation = Quaternion.Euler(new Vector3(0, 0,hoursToDegrees * time.Hour));
//_minutesPivot.localRotation = Quaternion.Euler(new Vector3(0,0,minutesToDegrees * time.Minute));
//_secondsPivot.localRotation = Quaternion.Euler(new Vector3(0,0,secondsToDegrees * time.Second));
//部分2 秒针连续转动版时钟
TimeSpan time = DateTime.Now.TimeOfDay;
_hoursPivot.localRotation =
Quaternion.Euler(0f, 0f, _hoursToDegrees *(float)time.TotalHours);
_minutesPivot.localRotation =
Quaternion.Euler(0f, 0f, _minutesToDegrees *(float)time.TotalMinutes);
_secondsPivot.localRotation =
Quaternion.Euler(0f, 0f, _secondsToDegrees *(float)time.TotalSeconds);
}
}
部分1效果:
部分2效果:
II、总结使用到的知识点
1.游戏对象的脚本皆为组件,可以随意挂载游戏对象
2.四元数与欧拉角
3.三角函数计算坐标轴
4.编程语言基础(类、公共修饰符等)
三、创建一个图像
I、创建游戏对象
1、步骤一,准备需要使用的预制体和游戏对象
Hierarchy面板空白处鼠标右击,选择Crate Empty并且把对象名字修改为Graph。
再创建一个3D Object里面的Cube,并且把对象名字修改为Point用作预制体。之后拖拽到保存的预制体文件夹里面去。
2、步骤二,创建脚本,利用函数映射创建图像
创建一个游戏脚本Graph,并且对脚本进行编辑。
public class Graph : MonoBehaviour
{
[SerializeField] public Transform _pointPrefabs;
[Range(0, 100)] public float _pointNum = 10;
//初始化图形代码
void Start()
{
float step = 2.0f / _pointNum;
//统一修改预制体大小
_pointPrefabs.localScale = Vector3.one * step ;
for (int i = 0;i < _pointNum;i++)
{
Transform point = Instantiate(_pointPrefabs);
//设置创建的子对象
point.SetParent(transform);
}
}
//让图像动起来
void Update()
{
float time = Time.time;
float step = 2.0f / _pointNum;
for (int i = 0; i < _pointNum; i++)
{
Vector3 position = Vector3.zero;
Transform point = transform.GetChild(i);
position.x = (i + 0.5f) * step - 1f;
position.y = Mathf.Sin(Mathf.PI * (position.x + time)) ;
point.position = position;
}
}
}
把脚本挂载到场景内的Graph对象上面,并且把脚本里面的_pointPrefabs字段引用预制体Point,为了效果好看一点,把字段_pointNum的值修改成30。如下图:
然后运行代码有如下结果:
上面的脚本无非就是,使用一种函数的思想去映射3d物体在空间中的坐标,主要是由x的值映射到y的值。脚本里使用的函数为y = f(x) = Sin(x * PI)。其中x = x + time。因为time是一个会随着时间而改变的变量。所以这个图像会一直动起来。
II.添加Shader效果
1.使用传统的Shader实现
屏幕上的每一个像素点经过渲染管线的流程处理的,这个流程如下:
顶点输入->顶点着色器->曲面着色器->几何着色器->裁剪->屏幕映射->片元着色器->光栅化处理->模板深度测试->屏幕显示
其中在shaderlab里面对可以对顶点着色器和片元着色器进行高度代码编辑。可使用语言有CG、HLSL、GLSL。
现在我们的目的是上一个步骤的游戏物体对象的x值和y值的关系,如果编写渲染管线来实现不同颜色的输出。
先创建一个StandradShader,再把shader的名字改成PointShader。如下图:
清空默认创建的代码。(全部不留)。然后写入代码如下
shader "Custom/PointShader"{}
这是shaderlab的一个语义。通过shader “位置/名字” {} 命令来创建一个传统流水线。再编写。如下:
shader "Custom/PointShader" {
SubShader{}
SubShader{}
FallBack "Diffuse"
}
这里添加了新的语句。两个SubShader{}以及一句FallBack “Diffuse”。可以知道,一个使用ShaderLab声明一个Shader的时候,可以有多个SubShader,而不同的SubShader可以针对不同的显卡去编写自己的Pass。如果所有SubShader都无法匹配到自己对应的显卡,那么就会通过FallBack “Diffuse"回退到Unity自己的自带的标准漫反射shader,这个shader名字是"diffuse”。
接下来,再编写渲染管线:
shader "Custom/PointShader"
{
SubShader{
Tags { "RenderType" = "Opaque"}
LOD 200
Pass{
CGPROGRAM
#pragma target 3.0 //定义图形库版本
#pragma vertex vert //定义一个顶点着色器
#pragma fragment frag //定义一个片元着色器
//定义一个结构体。用于输入顶点着色器
struct adp_data
{
float4 vert:POSITION; //告诉这个结构体,从模型空间获取顶点位置(语义:POSITION起的作用)
};
//定义一个结构。用于顶点输出,片元输入。
struct v2f
{
//输出时,让顶点着色器的顶点数据放置处。输入时,片元着色器的数据获取处(语义:SV_PSOITION的作用)
float4 vert:SV_POSITION;
//同上。只是放置的地方和获取的地方由SV_POSITION改为TEXCOORD0
fixed4 color:TEXCOORD0;
};
//顶点着色器的实现
v2f vert(adp_data i)
{
v2f o;
o.vert = UnityObjectToClipPos(i.vert);
o.color = mul(unity_ObjectToWorld,i.vert);
return o;
}
//片元着色器的实现
fixed4 frag(v2f i):SV_Target
{
return normalize(i.color);
}
ENDCG
}
}
FallBack "Diffuse"
}
Shader里面的LOD200是设置在SubShader里面的,我们可以利用C#里面的Shader.globalMaximumLOD全局属性里设定LOD最低的选择阈值。假如一个SubShader里面设定LOD 200,另一个设定LOD 100。假如设定Shader.globalMaximumLOD = 100,那么在运行这个Shader的时候不会选择LOD 200的SubShader。
上面是一个编写了一个简单的渲染管线。实现的效果只是由物体的世界坐标去决定物体自身片元的颜色。把Point的Shader材质选择为Custom/PointShader则效果如下:
1.使用surf着色器实现
和传统Shader一样,先编写一个结构。
shader "Custom/PointShader" {
SubShader{
}
FallBack "Diffuse"
}
这里我们可以直接在SubShader里面嵌入CGPROGRAM-ENDCG语义了。不需要使用Pass结构。并且定义surf着色器。surf着色器简单的地方就是把顶点着色器和片元着色器隐藏了,无需各自编写着色器的实现。所以surf着色器相对比较简单。
shader "Graph/PointSurface" {
//定义一下面板可设置属性
Properties{
//设置名字和数据类型。并且赋值默认值
_Smoothness("Smoothness" , Range(0,1)) = 0.5;
}
SubShader{
Tags{ "RenderType" = "Opaque"}
LOD 200
CGPROGRAM
//定义一个Surface着色器。并且设置参数Standrad fullforwardshadows
#pragma surface surf Standrad fullforwardshadows
#pragma target 3.0
//定义一个输入用的结构体
struct Input{
float3 worldPos;
};
float _Smoothness; //用于接收Properties里面的_Smoothness
void surf(Input input,SurfaceOutputStandrad o){
//修改反射率(光的反射率就人眼观察的颜色)
o.Albedo = input.worldPos;
//修改平滑度
o.Smoothness = _Smoothness;
}
ENDCG
}
FallBack "Diffuse"
}
这里编写了一个简单的surf着色器,由物体的世界坐标去决定自身的元素点颜色。把Point预制体的材质Shader选择为Graph/PointSurface。效果和使用传统着色器一样。如图:
3.使用URP(Universal Render Pipeline,通用渲染管线)实现
前置准备
1.项目工程的Project面板创建一个URP文件来存放URP
2.选择Unity菜单栏里的Window。之后点击打开Package Manager。下载并且按照URP插件。如下图:
3.按照完成之后。选择Unity菜单栏的Assets依次打开,Create->Rendering->Universal Render Pipeline->Pipeline Asset(forward render)。插件出一个URP的Asset文件,改名为URP,并且放置到URP文件夹里面,不过会自动创建一个URP_Renderer文件。如下图:
4.给Unity当前项目选择渲染管线。打开Unity菜单栏Editor->Project Setting->Graphics。然后选择刚刚创建的URP。如下图:
设置好。上面编写的surf着色器会失效,这是正常的现象。
5.创建一个编辑Shader的文件。点击Unity菜单栏Assets,然后依次选择Create->Shader->Universal Render Pipeline->Lit Shader Graphic。顺便命名为PointURP。如下图:
鼠标点击两下PointURP,打开一个图形编辑界面。你会发现可以通过图形编辑来编辑Shader,无需编写脚本。这也是URP插件非常牛逼的地方之一,如下图:
根据需求编辑上面的Fragment(片元着色器就可以了)。
之前的需求是,由物体世界坐标决定物体像素颜色。编辑后操作如下:
空白处右击鼠标,选择Create Node。搜索Psoition并且插件出来,把插件出来的Postion Node链接到Fragment里面的Base Color,如下截图:
然后就左上角点击Save Asset,保存下来一个Shader文件。然后给Point预制体的材质Shader选择为PointURP。
之后就实现出来一模一样的效果了。