一 、算法
1. 冒泡排序
public class BubbleSort:ISort
{
public void sort(int[] arr)
{
for (int i = 0; i < arr.Length-1; i++)
{
for (int j = 0; j < arr.Length-i-1; j++)
{
if (arr[j] > arr[j+1])
{
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}
}
}
}
2.快速排序
class QuickSort : ISort
{
public void sort(int[] arr)
{
quicksort(arr,0,arr.Length-1);
}
void quicksort(int[] a, int left, int right)
{
int i, j, key;
if (left > right)
return;
key = a[left]; //temp中存的就是基准数
i = left;
j = right;
while (i != j)
{
//顺序很重要,要先从右边开始找小于基准数的值
while (a[j] >= key && i < j)
j--;
//再找左边的
while (a[i] <= key && i < j)
i++;
//交换两个数在数组中的位置
if (i < j)
{
int tmp = a[i];
a[i] = a[j];
a[j] = tmp;
}
}
//最终将基准数归位
a[left] = a[i];
a[i] = key;
quicksort(a, left, i - 1);//继续处理左边的,这里是一个递归的过程
quicksort(a, i + 1, right);//继续处理右边的 ,这里是一个递归的过程
}
3. A*寻路算法
A寻路算法的估量代价
在A算法中核心的寻路依据就是估量代价,在A中通常用 F 表示。
F = G + H
其中G表示当前点到起始点的估量代价,H表示当前点到终点的代价。
G的计算方式:最开始,以起点为中心开始计算周边的八个格子,然后在以这算出来的八个格子为中心,继续计算他们周围的八个格子的G值。以此类推,直到找到终端,或遍历完所有的格子。G值的计算结果为:中心点的G值+距离值【10 or 14】
FGH示意图
图中格子左下角为G值,右下角为H值,左上角为F值
因此从当前格子周边的八个格子中找到下一步所走的格子,依据F值,当F值相同时随机选择。
当然在寻路过程中,会有障碍,不能通过,通过可以行走的格子走到终点。下图中绿色为起点,红色为终点,蓝色是障碍,浅蓝边框是参与计算的格子,A*就是通过这样的一系列计算完成的最优寻路。
AStar寻路步骤
- 设置开放列表OpenList和关闭列表CloseList
- 将起点放置到OpenList
- 开启循环While(OpenList.count > 0)
{
3.1 将OpenList排序【F值从小到大】
3.2 OpenList[0]必定是F值最小的,命名为Center
3.2.1 发现Center就是终点,回溯找到导航路径
3.3 以这个点为中心,去发现它周围的8个格子
3.4 计算发现的每个格子G H F三个值
3.5 如果这个格子没有被计算过或原来G值,比这次计算出来的G要大
3.5.1 此时,设置新的FGH值给这个格子,并设置该格子的发现者是Center
3.6 如果这个格子被计算过,且原G值,比这次计算出来的G要小
3.6.1 此时,就不能替换原来FGH值
3.7 将发现的每个格子放入到OpenList
3.7.1 放入的时候要做检测【该格子不在OpenList、该格子不在CloseList】
3.8 将此次循环的发现者Center放入到CloseList
3.8 判断OpenList空了
3.8.1 说明所有的可发现的格子都被遍历过了,始终没有找到中,说明无法到达终点
}
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class MyAStar : MonoBehaviour
{
/// <summary>
/// 单例脚本
/// </summary>
public static MyAStar instance;
//参考物体预设体
public GameObject reference;
//格子数组
public Grid[,] grids;
//格子数组对应的参考物(方块)对象
public GameObject[,] objs;
//开启列表
public ArrayList openList;
//关闭列表
public ArrayList closeList;
//目标点坐标
public int targetX;
public int targetY;
//起始点坐标
public int startX;
public int startY;
//格子行列数
private int row;
private int colomn;
//结果栈
private Stack<string> parentList;
//基础物体
private Transform plane;
private Transform start;
private Transform end;
private Transform obstacle;
//流颜色参数
private float alpha = 0;
private float incrementPer = 0;
void Awake ()
{
instance = this;
plane = GameObject.Find ("Plane").transform;
start = GameObject.Find ("Start").transform;
end = GameObject.Find ("End").transform;
obstacle = GameObject.Find ("Obstacle").transform;
parentList = new Stack<string> ();
openList = new ArrayList ();
closeList = new ArrayList ();
}
/// <summary>
/// 初始化操作
/// </summary>
void Init ()
{
//计算行列数
int x = (int)(plane.localScale.x * 20);
int y = (int)(plane.localScale.z * 20);
row = x;
colomn = y;
grids = new Grid[x, y];
objs = new GameObject[x, y];
//起始坐标
Vector3 startPos =
new Vector3 (plane.localScale.x * -5, 0, plane.localScale.z * -5);
//生成参考物体(Cube)
for (int i = 0; i < x; i++) {
for (int j = 0; j < y; j++) {
grids [i, j] = new Grid (i, j);
GameObject item = (GameObject)Instantiate (reference,
new Vector3 (i * 0.5f, 0, j * 0.5f) + startPos,
Quaternion.identity);
item.transform.GetChild (0).GetComponent<Reference> ().x = i;
item.transform.GetChild (0).GetComponent<Reference> ().y = j;
objs [i, j] = item;
}
}
}
/// <summary>
/// A*计算
/// </summary>
IEnumerator Count ()
{
//等待前面操作完成
yield return new WaitForSeconds (0.1f);
//添加起始点
openList.Add (grids [startX, startY]);
//声明当前格子变量,并赋初值
Grid currentGrid = openList [0] as Grid;
//循环遍历路径最小F的点
while (openList.Count > 0 && currentGrid.type != GridType.End) {
//获取此时最小F点
currentGrid = openList [0] as Grid;
//如果当前点就是目标
if (currentGrid.type == GridType.End) {
Debug.Log ("Find");
//生成结果
GenerateResult (currentGrid);
}
//上下左右,左上左下,右上右下,遍历
for (int i = -1; i <= 1; i++) {
for (int j = -1; j <= 1; j++) {
if (i != 0 || j != 0) {
//计算坐标
int x = currentGrid.x + i;
int y = currentGrid.y + j;
//如果未超出所有格子范围,不是障碍物,不是重复点
if (x >= 0 && y >= 0 && x < row && y < colomn
&& grids [x, y].type != GridType.Obstacle
&& !closeList.Contains (grids [x, y])) {
//计算G值
int g = currentGrid.g + (int)(Mathf.Sqrt ((Mathf.Abs (i) + Mathf.Abs (j))) * 10);
//与原G值对照
if (grids [x, y].g == 0 || grids [x, y].g > g) {
//更新G值
grids [x, y].g = g;
//更新父格子
grids [x, y].parent = currentGrid;
}
//计算H值
grids [x, y].h = Manhattan (x, y);
//计算F值
grids [x, y].f = grids [x, y].g + grids [x, y].h;
//如果未添加到开启列表
if (!openList.Contains (grids [x, y])) {
//添加
openList.Add (grids [x, y]);
}
//重新排序
openList.Sort ();
}
}
}
}
//完成遍历添加该点到关闭列表
closeList.Add (currentGrid);
//从开启列表中移除
openList.Remove (currentGrid);
//如果开启列表空,未能找到路径
if (openList.Count == 0) {
Debug.Log ("Can not Find");
}
}
}
/// <summary>
/// 生成结果
/// </summary>
/// <param name="currentGrid">Current grid.</param>
void GenerateResult (Grid currentGrid)
{
//如果当前格子有父格子
if (currentGrid.parent != null) {
//添加到父对象栈(即结果栈)
parentList.Push (currentGrid.x + "|" + currentGrid.y);
//递归获取
GenerateResult (currentGrid.parent);
}
}
/// <summary>
/// 显示结果
/// </summary>
/// <returns>The result.</returns>
IEnumerator ShowResult ()
{
//等待前面计算完成
yield return new WaitForSeconds (0.3f);
//计算每帧颜色值增量
incrementPer = 1 / (float)parentList.Count;
//展示结果
while (parentList.Count != 0) {
//出栈
string str = parentList.Pop ();
//等0.3秒
yield return new WaitForSeconds (0.3f);
//拆分获取坐标
string[] xy = str.Split (new char[]{ '|' });
int x = int.Parse (xy [0]);
int y = int.Parse (xy [1]);
//当前颜色值
alpha += incrementPer;
//以颜色方式绘制路径
objs [x, y].transform.GetChild (0).GetComponent<MeshRenderer> ().material.color
= new Color (1 - alpha, alpha, 0, 1);
}
}
/// <summary>
/// 曼哈顿方式计算H值
/// </summary>
/// <param name="x">The x coordinate.</param>
/// <param name="y">The y coordinate.</param>
int Manhattan (int x, int y)
{
return (int)(Mathf.Abs (targetX - x) + Mathf.Abs (targetY - y)) * 10;
}
void Start ()
{
Init ();
StartCoroutine (Count ());
StartCoroutine (ShowResult ());
}
}
二、Lua
//之后补充
三 、Unity引擎
1. Dotween
使用的DOTween插件后,transform居然能够点出DOMove方法,这是因为C#的拓展性,使其和Unity的一些类能产生链接,是不是感觉很神奇。因为这些特性,使我们在使用起来非常简单易懂,想让哪个物体动,就让它的transform组件来调用DOTWeen的方法就可以了。
DOMove相关方法:
世界坐标上移动:transform.DOMove
本地坐标上移动:transform.DOLocalMove
世界坐标的X轴上移动:transform.DOMoveX
本地坐标的X轴上移动:transform.DOLocalMoveX
动画事件相关方法:
OnStart: 动画第一次播放时调用
OnPlay: 动画每次从暂停状态解除时调用(包括初次播放)
Pause: 动画暂停时调用一次
OnUpdate: 动画播放过程中每帧调用
OnStepComplete: 每次动画播放结束时调用(受循环次数影响)
OnComplete: 每次动画播放结束时调用(不受循环次数影响,且倒放时不适用)
2.UI框架
UI框架主要实现的功能接口:
- 快速寻找重要元件
- 使用对象池生成、隐藏Json中配置好的UI模块
- 单窗口 、 多窗口 : 主要使用了CanvasGroup里BlockTarget属性
- 语言本地化
UI框架概述:
-
1.快速查找对象 * 2.事件中⼼ * 3.动画加载模块、模态处理
-
事先将可能需要使用的UI对象以特定的结尾符号命名,这些UI对象都称为元件widget,元件会事先遍历所有的子物体,获取到所有可能的UI组件。不同的UI模块以功能划分,比如主界面模块,商城模块,模块内存在的UI元件配合框架可以十分简易地获取想要的UI对象,而不用每一次都Find,这样既避免了资源浪费而又提高了开发效率。
-
模块还划分为单窗口模式和多窗口模式,在打开单窗口模式后无法与其他UI模块界面交互,多窗口则可以,这里主要使用了CanvasGroup里面的BlockTarget属性。框架内使用栈的数据结构存储模块,因为栈是先入后出,打开UI界面后也是最先打开的会垫在最下面
,行为相吻合,所以使用栈。 -
框架内提供了事件中心,避免脚本间交交互过于频繁,在事件中心管理消息的分发,降低耦合。
-
可以重写模块的四个事件:OnOpen、OnPause、OnResume、OnClose
3.FSM框架
FSM简单来说就是
4个类
- 状态基类 状态基类需要3个虚方法,以及虚拟机对象
- 拥有者泛型类 继承状态基类,构造出使用者对象
- 玩家脚本类
在Start方法里构造new各个状态机类(传入枚举id,使用者this(自己)),new状态机对象,添加各个状态类对象。
在Update通过按键,并一直调用状态机的切换方法。
在LateUpdate()调用状态机的保持状态方法 - 状态机类
包含添加状态类,切换状态类方法(控制Enter()),保存状态类方法(控制(Stay()))
加若干个状态类
每个状态具体有3种状态方法(OnEnter,OnStay,OnExit)需要重写来具体实现,通过OnExit()方法
4. 对象池框架
对象池是一项优化CPU的技术,将CPU的压力分给内存。
框架内是字典嵌套一层List存储数据,通过Json配置资源路径获取到的资源添加到List中,通过配置名获取游戏对象。
框架内提供两个消息函数:OnSpawn() , OnReCycle()
5.单例模式
使用反射搭建单例框架:
using System;
namespace Frame.Tool
{
public class Singleton<T>
{
//单例
private static T instance;
//获取单例
public static T GetInstance()
{
if (instance == null)
{
instance = (T)Activator.CreateInstance(typeof(T), true);
}
return instance;
}
}
}
1.主要优点
单例模式的主要优点如下:
(1) 单例模式提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。
(2) 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能。
(3) 允许可变数目的实例。基于单例模式我们可以进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例,既节省系统资源,又解决了单例单例对象共享过多有损性能的问题。
2.主要缺点
单例模式的主要缺点如下:
(1) 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
(2) 单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。
(3) 现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的共享对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致共享的单例对象状态的丢失。
3.适用场景
在以下情况下可以考虑使用单例模式:
(1) 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器或资源管理器,或者需要考虑资源消耗太大而只允许创建一个对象。
(2) 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。
6.工厂模式
定义⼀个创建对象的接口,让⼦类去决定实例化哪⼀个类的对象。工厂方法使⼀个类的实例化延迟到子类
7.观察者模式
很多观察者想要观察被观察者的状态,因此委托⼀个通知者去观察,观察到变化后,通知给所有参与的观察者
一般数据更新的时候要更新ui的显示,采用观察者模式,ui界面的代码监听事件,数据变化时抛出事件,从而实现ui的显示刷新。
封装一个事件订阅和发送的类:EventDispatcher。
8. 策略模式
上下文类通过构造传入具体的策略对象,实现具体策略算法,从⽽返回不同的策略结果
实例:游戏⻆⾊攻击策略的不同【矩形攻击、圆形攻击、扇形攻击】
using UnityEngine;
namespace Game
{
/// <summary>
/// 攻击判定
/// </summary>
public static class AttackJudge
{
/// <summary>
/// 圆形判定
/// </summary>
/// <param name="self"></param>
/// <param name="enemy"></param>
/// <param name="range"></param>
/// <returns></returns>
public static bool CircleAttack(Transform self,Transform enemy,float range)
{
Vector3 i2Target = enemy.transform.position - self.position;
//超出攻击范围
if (i2Target.magnitude > range)
{
return false;
}
return true;
}
/// <summary>
/// 扇形判定
/// </summary>
/// <returns></returns>
public static bool SectorAttack(Transform self,Transform enemy,float range,float angle)
{
Vector3 i2Target = enemy.transform.position - self.position;
//超出攻击范围
if (i2Target.magnitude > range)
{
return false;
}
Vector3 forward = self.forward;
i2Target.Normalize();
float cosValue = Vector3.Dot(forward, i2Target);
float angleI2Emeny = Mathf.Rad2Deg * Mathf.Acos(cosValue);
//超出角度
if (Mathf.Abs(angleI2Emeny) > angle / 2)
{
return false;
}
return true;
}
}
}
老师写的
/// <summary>
/// 攻击策略
/// </summary>
public abstract class AttackStrategy
{
/// <summary>
/// 攻击
/// </summary>
/// <param name="attackTarget">攻击目标</param>
/// <returns>给攻击目标造成的伤害</returns>
public abstract float Attack(Transform attacker, Transform attackTarget);
}
/// <summary>
/// 扇形攻击策略
/// </summary>
public class SectorAttackStrategy : AttackStrategy
{
//半径
private float skillRadius;
//角度
private float skillAngle;
//技能释放方向
private Vector3 skillDir;
//最大伤害
private float maxDamage;
public SectorAttackStrategy(float skillRadius, float skillAngle, Vector2 skillDir,float maxDamage)
{
this.skillRadius = skillRadius;
this.skillAngle = skillAngle;
this.skillDir = skillDir;
this.maxDamage = maxDamage;
}
public override float Attack(Transform attacker,Transform attackTarget)
{
//计算攻击者指向被攻击者的方向向量
Vector3 direction = attackTarget.position - attacker.position;
//计算两者距离
float distance = direction.magnitude;
//如果与敌人的距离超越技能半径
if (distance > skillRadius)
return 0;
//计算敌人与玩家的方向与技能方向的夹角
float enemyAngle = Vector3.Angle(direction, skillDir);
//攻击目标不再扇形范围内
if (enemyAngle > skillAngle / 2)
return 0;
//返回伤害【越近伤害越高】
return (skillRadius - distance)/skillRadius * maxDamage;
}
}
/// <summary>
/// 圆形攻击策略
/// </summary>
public class CircleAttackStrategy : AttackStrategy
{
//技能位置
private Vector3 skillPosition;
//技能半径
private float skillRadius;
//高伤害半径
private float maxDamageRadius;
//内层的最大伤害
private float maxDamage;
//外层的最校伤害
private float minDamage;
public CircleAttackStrategy(Vector3 skillPosition, float skillRadius,float maxDamageRadius, float maxDamage, float minDamage)
{
this.skillPosition = skillPosition;
this.skillRadius = skillRadius;
this.maxDamageRadius = maxDamageRadius;
this.maxDamage = maxDamage;
this.minDamage = minDamage;
}
public override float Attack(Transform attacker, Transform attackTarget)
{
//攻击目标与技能圆心的距离
float distance = Vector3.Distance(skillPosition, attackTarget.position);
//不在攻击范围内
if (distance > skillRadius)
return 0;
//在高伤害圈内
if (distance < maxDamageRadius)
return maxDamage;
//在低伤害环内
return minDamage;
}
}
/// <summary>
/// 矩形攻击策略
/// </summary>
public class RectangleAttackStrategy : AttackStrategy
{
private float distance;
private float length;
private float width;
private float damage;
public RectangleAttackStrategy(float distance, float length, float width,float damage)
{
this.distance = distance;
this.length = length;
this.width = width;
this.damage = damage;
}
public override float Attack(Transform attacker, Transform attackTarget)
{
//计算技能中心的坐标
Vector3 skillCenter = attacker.forward * distance + attacker.position;
//技能中心点指向目标的方向向量
Vector3 dir = attackTarget.position - skillCenter;
//求这个方向向量在角色水平和垂直方向的投影向量
Vector3 rightProjection = Vector3.Project(dir, attacker.right);
Vector3 forwardProjection = Vector3.Project(dir, attacker.forward);
//判断长度【不在矩形范围内】
if (rightProjection.magnitude > length / 2
|| forwardProjection.magnitude > width / 2)
return 0;
//在范围内
return damage;
}
}
public class Hero
{
private Transform _transform;
public Transform Transform
{
get => _transform;
set => _transform = value;
}
public Hero(Transform transform)
{
_transform = transform;
}
public void Attack(Hero target,AttackStrategy attackStrategy)
{
float damage = attackStrategy.Attack(_transform, target._transform);
if (damage == 0)
{
Debug.Log("敌人未受到技能伤害...");
}
else
{
Debug.Log("敌人受到了" + damage + "点伤害");
}
}
}
四、Socket通信
1.TCP与UDP
1、TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接
2、TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付
3、TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的
UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等)
4、每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信
5、TCP首部开销20字节;UDP的首部开销小,只有8个字节
6、TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道
2.Get和Post
Get和Post在面试中一般都会问到,一般的区别:
(1)post更安全(不会作为url的一部分,不会被缓存、保存在服务器日志、以及浏览器浏览记录中)
(2)post发送的数据更大(get有url长度限制)
(3)post能发送更多的数据类型(get只能发送ASCII字符)
(4)post比get慢
(5)post用于修改和写入数据,get一般用于搜索排序和筛选之类的操作(淘宝,支付宝的搜索查询都是get提交),目的是资源的获取,读取数据
虽然在开发中经常用get或者post请求,但是由于我们资历经验的欠缺,或许就重来没有深究过什么场合用get请求,什么场合用post请求,我相信不止我一个人当看到第4,5条的时候,就会明白为什么面试官对我们的回答不满意,也明白了自己对get或post用法理解的欠缺,那么get比post更快,究竟快多少呢?表现在那些方面?
一、为什么get比post更快
1.post请求包含更多的请求头
因为post需要在请求的body部分包含数据,所以会多了几个数据描述部分的首部字段(如:content-type),这其实是微乎其微的。
2.最重要的一条,post在真正接收数据之前会先将请求头发送给服务器进行确认,然后才真正发送数据
post请求的过程:
(1)浏览器请求tcp连接(第一次握手)
(2)服务器答应进行tcp连接(第二次握手)
(3)浏览器确认,并发送post请求头(第三次握手,这个报文比较小,所以http会在此时进行第一次数据发送)
(4)服务器返回100 Continue响应
(5)浏览器发送数据
(6)服务器返回200 OK响应
get请求的过程:
(1)浏览器请求tcp连接(第一次握手)
(2)服务器答应进行tcp连接(第二次握手)
(3)浏览器确认,并发送get请求头和数据(第三次握手,这个报文比较小,所以http会在此时进行第一次数据发送)
(4)服务器返回200 OK响应
也就是说,目测get的总耗是post的2/3左右,这个口说无凭,网上已经有网友进行过测试。
3.get会将数据缓存起来,而post不会
可以做个简短的测试,使用ajax采用get方式请求静态数据(比如html页面,图片)的时候,如果两次传输的数据相同,第二次以后消耗的时间将会在10ms以内(chrome测试),而post每次消耗的时间都差不多。经测试,chrome和firefox下如果检测到get请求的是静态资源,则会缓存,如果是数据,则不会缓存,但是IE什么都会缓存起来,当然,应该没有人用post去获取静态数据吧,反正我是没见过。
五、Sqlite与MySql
1.Sqlite
2.MySql
导入dll动态链接库 :
创建连接时需要传入:
//IP地址
private static string host;
//端口号
private static string port;
//用户名
private static string userName;
//密码
private static string password;
//数据库名称
private static string databaseName;
六、AssetsBundle
1.AssetsBundle加载流程
2.AssetsBundle卸载流程
七、Shader
1. 渲染管线流程
什么是渲染(Rendering)
渲染简单的理解可能可以是这样:就是将三维物体或三维场景的描述转化为一幅二维图像,生成的二维图像能很好的反应三维物体或三维场景(如图1):
什么是渲染管线
渲染管线也称为渲染流水线,是显示芯片内部处理图形信号相互独立的的并行处理单元。一个流水线是一序列可以并行和按照固定顺序进行的阶段。每个阶段都从它的前一阶段接收输入,然后把输出发给随后的阶段。就像一个在同一时间内,不同阶段不同的汽车一起制造的装配线,传统的图形硬件流水线以流水的方式处理大量的顶点、几何图元和片段。
图2显示了当今图形处理器所使用的图形硬件流水线。三维应用程序传给图形处理器一序列的顶点组成不同的几何图元:典型的多边形、线段和点。正如图3所示,有许多种方法来制定几何图元。
渲染管线模型(Rendering)
渲染管线的流程是在GPU中进行的,它主要占用计算机的显存部分。渲染管线在这个过程进行了顶点处理、面处理、光栅化、像素处理。
顶点处理
通过一系列坐标系的变换,让各个顶点通过一定的规律在摄像机前位移,最终在屏幕上对应这些顶点的过程。在观察者坐标系转换到投影坐标系的过程中,GPU(图形处理单元)还对材质属性和光照属性进行了处理。
面处理
三点成一面。面处理有三个部分:面的组装、面的截取、面的剔除。
- 面的组装:模型中的三个点会组成一个三角形的面(非任意点,因为每个点都有自己的编号)。这些面,面面相接,组成了我们能看到的模型。
- 面的截取:由于摄像机和人眼一样,可视的区域是一个锥形,模型在摄像机可视范围内可能并不是全覆盖,也就是在摄像机外,这些在摄像机之内的部分就会被截取。
- 面的剔除:为了模拟肉眼,摄像机前的物体会出现近大远小的现象,那么物体和物体之间会有遮挡,被遮挡的面会被剔除不处理;每个面都有法向量,所以只有在面的法向量和摄像机散射向量夹角大于90度的才会被摄像机捕捉到。
光栅化
光栅化,又称之合并阶段。它的主要功能是将面转换成一帧中的像素集合。这一阶段是不可以编程的,它负责执行多个片段测试,包括:深度测试、alpha 测试和模板测试,程序员可以通过高度配置来实现想要的效果。如果通过了所有的测试,这部分颜色就会与帧缓冲存储的颜色通过 alpha 混合函数进行合并。
像素处理
这个阶段将像素区域着色,然后赋予贴图。
(左上为3D网格模型,左下为赋予贴图后的3D模型,右图为贴图)
总结渲染绘图管线流程
2.描边特效shader
老师写的
Shader "MyShader/SurfaceShader/Surface008"
{
Properties
{
_MainColor("主颜色",Color) = (1,0,0,1)
_MainTex("主纹理",2D) = ""{}
_NormalMap("法线贴图",2D) = ""{}
_DetailTex("细节纹理",2D) = ""{}
_RimPower("发光强度",Range(0,1)) = 1
_RimColor("发光颜色",Color) = (1,0,0,1)
}
SubShader
{
CGPROGRAM //---------CG语言开始-----------
//命令 着色器类型 着色器入口函数名 光照模型[兰伯特]
#pragma surface surf Lambert finalcolor:setcolor
//外部属性声明
fixed4 _MainColor;
sampler2D _MainTex;
sampler2D _NormalMap;
sampler2D _DetailTex;
half _RimPower;
fixed4 _RimColor;
//声明输入结构体
struct Input
{
//对应主纹理的uv坐标
fixed2 uv_MainTex;
//对应细节纹理的uv坐标
fixed2 uv_DetailTex;
//对应法线贴图的uv坐标
fixed2 uv_NormalMap;
//视图方向
half3 viewDir;
};
//入口函数
void surf(Input IN,inout SurfaceOutput o)
{
//将外部属性设置为反射颜色
o.Albedo = tex2D(_MainTex,IN.uv_MainTex) * tex2D(_DetailTex,IN.uv_DetailTex) *2;
//计算视图方向的标准化向量
half3 normalViewDir = normalize(IN.viewDir);
//计算法线方向的标准化向量
half3 normalNormalDir = normalize(o.Normal);
//计算边缘系数
half rim = 1 - dot(normalViewDir,normalNormalDir);
//设置边缘自发光
o.Emission = _RimColor * _RimPower * rim;
//通过法线贴图的颜色转换为法线方向向量,并设置
o.Normal = UnpackNormal(tex2D(_NormalMap,IN.uv_NormalMap));
}
//设置最终颜色的函数
void setcolor(Input IN,SurfaceOutput o,inout fixed4 color)
{
color *= _MainColor;
}
ENDCG //---------CG语言结束-----------
}
}
别人博客的案例
思路:
- 游戏中经常需要制作角色受击打的身体边缘光效果,使用的方法是,给Renderer叠加一个制作好的边缘光材质球,并通过脚本动态控制边缘光的渐变效果,表现出受击后的边缘光效果
Shader
Shader "Effect/TransparentRim" {
Properties{
_RimColor("Rim Color", Color) = (0.5,0.5,0.5,0.5)
_InnerColor("Inner Color", Color) = (0.5,0.5,0.5,0.5)
_InnerColorPower("Inner Color Power", Range(0.0,1.0)) = 0.5
_RimPower("Rim Power", Range(0.0,5.0)) = 2.5
_AlphaPower("Alpha Rim Power", Range(0.0,8.0)) = 4.0
_AllPower("All Power", Range(0.0, 10.0)) = 1.0
}
SubShader{
Tags { "Queue" = "Transparent" }
CGPROGRAM
#pragma surface surf Lambert alpha
struct Input {
float3 viewDir;
INTERNAL_DATA
};
float4 _RimColor;
float _RimPower;
float _AlphaPower;
float _AlphaMin;
float _InnerColorPower;
float _AllPower;
float4 _InnerColor;
void surf(Input IN, inout SurfaceOutput o) {
half rim = 1.0 - saturate(dot(normalize(IN.viewDir), o.Normal));
o.Emission = _RimColor.rgb * pow(rim, _RimPower)*_AllPower + (_InnerColor.rgb * 2 * _InnerColorPower);
o.Alpha = (pow(rim, _AlphaPower))*_AllPower;
}
ENDCG
}
Fallback "VertexLit"
}
提供设置材质接口的脚本
// HittedMatEffect.cs
using UnityEngine;
using System.Collections;
public class HittedMatEffect : MonoBehaviour
{
bool mbActive = false;
bool mbInit = false;
Material mMat = null;
public float mLife;
private static int s_InnerColor = -1;
private static int s_AllPower = -1;
private static int s_AlphaPower = -1;
void Awake()
{
s_InnerColor = Shader.PropertyToID("_InnerColor");
s_AllPower = Shader.PropertyToID("_AllPower");
s_AlphaPower = Shader.PropertyToID("_AlphaPower");
}
// Use this for initialization
/// <summary>
/// 设置材质颜色
/// </summary>
/// <param name="color"></param>
public void SetColor(Color color)
{
mMat.SetColor(s_InnerColor, color);
}
public void SetLifeTime(float time)
{
mLife = time;
}
public void Active()
{
if (!mbInit)
AddEffect();
mMat.SetFloat(s_AllPower, 0.9f);
mbActive = true;
mLife = 0.2f;
}
void Update()
{
if (!mbActive)
return;
mLife -= Time.deltaTime;
if (mLife < 0)
{
mbActive = false;
mMat.SetFloat(s_AllPower, 0);
}
float v = Mathf.Sin((1 - mLife) * 8 * Mathf.PI) + 2;
mMat.SetFloat(s_AlphaPower, v);
}
void AddEffect()
{
Object mat = Resources.Load("Material/HittedMatEffect");
mMat = GameObject.Instantiate(mat) as Material;
foreach (var curMeshRender in transform.GetComponentsInChildren<Renderer>())
{
Material[] newMaterialArray = new Material[curMeshRender.materials.Length + 1];
for (int i = 0; i < curMeshRender.materials.Length; i++)
{
if (curMeshRender.materials[i].name.Contains("HittedMatEffect"))
{
return;
}
else
{
newMaterialArray[i] = curMeshRender.materials[i];
}
}
if (null != mMat)
newMaterialArray[curMeshRender.materials.Length] = mMat;
curMeshRender.materials = newMaterialArray;
}
mbInit = true;
}
void RemoveEffect()
{
foreach (var curMeshRender in transform.GetComponentsInChildren<Renderer>())
{
int newMaterialArrayCount = 0;
for (int i = 0; i < curMeshRender.materials.Length; i++)
{
if (curMeshRender.materials[i].name.Contains("HittedMatEffect"))
{
newMaterialArrayCount++;
}
}
if (newMaterialArrayCount > 0)
{
Material[] newMaterialArray = new Material[newMaterialArrayCount];
int curMaterialIndex = 0;
for (int i = 0; i < curMeshRender.materials.Length; i++)
{
if (curMaterialIndex >= newMaterialArrayCount)
{
break;
}
if (!curMeshRender.materials[i].name.Contains("HittedMatEffect"))
{
newMaterialArray[curMaterialIndex] = curMeshRender.materials[i];
curMaterialIndex++;
}
}
curMeshRender.materials = newMaterialArray;
}
}
}
}
测试类
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Runner : MonoBehaviour
{
// Use this for initialization
void Start()
{
}
// Update is called once per frame
void Update()
{
if (Input.GetMouseButtonDown(0))
{
HittedMatEffect sc = gameObject.GetComponent<HittedMatEffect>();
if (null == sc)
sc = gameObject.AddComponent<HittedMatEffect>();
sc.Active();
sc.SetColor(Color.red);
}
}
}