Unity Shader案例篇—五子棋
发表于2018-06-12
本篇文章给大家介绍下shader案例,实现一个五子棋游戏,惯例先上效果图:
一、原理
1、首先,要明确一点,就是在GPU中没有像CPU中这样能够分配随机内存,因此所有点的状态最终还是要通过CPU来控制和保存。
2、在C#代码中通过二维数组来保存点的信息和状态,并将这个数组传递给GPU。本文是参考了这篇文章的基础上实现的点击打开链接,文章中还介绍了Unity5.4的测试版中直接实现了C#代码向Shader中传递数组,我还没有去试验,据文章介绍目前Unity对于传递数组还是个测试版。首先先在Shader中定义一个Uniform数组,“uniform float4 pointInfom[21][21];”,定义的二维数组需要指定大小。然后,在C#代码中写如下代码来进行赋值:
/// <summary> /// 初始化所有的点 /// </summary> private void InitAllPoint() { int ix = -1; for (float i = -1; i < 1.1f; i += 0.1f) { for (float j = -1; j < 1.1; j += 0.1f) { ix += 1; mat.SetVector("pointInfom" + ix, new Vector4(i, j, 1, 0)); } } }
PS:这样对性能开销确实非常大,不建议在大型项目中过多的使用。本文的目的是为了学习Shader提供案例的参考
编译器实际上会将上述的数组一个个翻译成“
uniform float4 pointInfom[XX];”变量。尽管是二维数组,其实还是翻译成了一维的四维变量,这个四维变量的个数是21乘21个。因此,试想一下这个是有多浪费。文章中介绍的“
但是在着色器中你还是可以通过索引来获取其值。因为是十个独立的值,也就意味着你必须分别为它们赋值,而无法一次性的为其赋值,但是如果是真正的数组,一次赋值即可达到目的。这就造成了很多的 Graphics API 和引擎 API 的消耗。
在 Unity 官方论坛上同样可以看到这方面的疑问,Passing-Arrays-to-shaders。在讨论的最后,一个激动人心的消息是,Unity 5.4 会支持传递数组到 Shader 的功能。Unity 5.4 目前还处于 Beta 版,我准备在其发布正式版之后,对这个 API 进行测试。在此之前先来看看其 API 吧:)
Shader.SetGlobalFloatArray Shader.SetGlobalMatrixArray Shader.SetGlobalVectorArray
MaterialPropertyBlock.SetGlobalFloatArray MaterialPropertyBlock.SetGlobalMatrixArray MaterialPropertyBlock.SetGlobalVectorArray”。
3、鼠标左右建点击实现不同颜色的点完整的C#代码如下:
using UnityEngine; using System.Collections; using System.Collections.Generic; public class Backgammon2 : MonoBehaviour { public Material mat; //设置点中的最小误差 public float clickMinError = 0.03f; //网格点集 private List<PointDisk> list_gridPointDisk = new List<PointDisk>(); //网格点的数量 private int gridIntersectionNums; private float gridSpace; private Vector2 vec_mouseBtnPos; // Use this for initialization void Start () { gridSpace = mat.GetFloat("_tickWidth"); //单个坐标轴上网格点的数量等于横轴坐标间距除以网格间距 gridIntersectionNums = (int)Mathf.Floor(1.0f / gridSpace); //这里不能只用强制类型转换,如果使用强制类型转换会丢失数据,比如1.0/0.1最后的结果是9 for(float i=-1;i<1+gridSpace;i+=gridSpace) { for(float j=-1;j<1+gridSpace;j+=gridSpace) { PointDisk pd = new PointDisk(); pd.col = 0; pd.x = i; pd.y = j; pd.isLocked = false; list_gridPointDisk.Add(pd); } } InitAllPoint(); } // Update is called once per frame void Update() { //左键点击 if (Input.GetMouseButtonDown(0)) { vec_mouseBtnPos = Input.mousePosition; //将鼠标的位置除以屏幕参数得到范围为0~1的坐标范围 vec_mouseBtnPos = new Vector2(vec_mouseBtnPos.x / Screen.width, vec_mouseBtnPos.y / Screen.height); //设定坐标原点为中点 vec_mouseBtnPos -= new Vector2(0.5f, 0.5f); vec_mouseBtnPos *= 2; // vec_mouseBtnPos.y = -vec_mouseBtnPos.y; //如果点中了网格的交叉点出就显示圆点 //int index = CheckClikedIntersection(vec_mouseBtnPos); int index = Check_ClickedInsection(vec_mouseBtnPos); if (index != -1) { //将准确的网格点的位置赋值给vec_mouseBtnPos // vec_mouseBtnPos = list_gridIntersectionPos[index]; vec_mouseBtnPos = new Vector2(list_gridPointDisk[index].x, list_gridPointDisk[index].y); mat.SetVector("pointInfom" + index, new Vector4(vec_mouseBtnPos.x, vec_mouseBtnPos.y, 1, 1)); } // Debug.Log("x:" + vec_mouseBtnPos.x + "y:" + vec_mouseBtnPos.y); } //右键点击 if (Input.GetMouseButtonDown(1)) { vec_mouseBtnPos = Input.mousePosition; //将鼠标的位置除以屏幕参数得到范围为0~1的坐标范围 vec_mouseBtnPos = new Vector2(vec_mouseBtnPos.x / Screen.width, vec_mouseBtnPos.y / Screen.height); //设定坐标原点为中点 vec_mouseBtnPos -= new Vector2(0.5f, 0.5f); vec_mouseBtnPos *= 2; // vec_mouseBtnPos.y = -vec_mouseBtnPos.y; //如果点中了网格的交叉点出就显示圆点 //int index = CheckClikedIntersection(vec_mouseBtnPos); int index = Check_ClickedInsection(vec_mouseBtnPos); if (index != -1) { //将准确的网格点的位置赋值给vec_mouseBtnPos // vec_mouseBtnPos = list_gridIntersectionPos[index]; vec_mouseBtnPos = new Vector2(list_gridPointDisk[index].x, list_gridPointDisk[index].y); mat.SetVector("pointInfom" + index, new Vector4(vec_mouseBtnPos.x, vec_mouseBtnPos.y, 2, 1)); } // Debug.Log("x:" + vec_mouseBtnPos.x + "y:" + vec_mouseBtnPos.y); } } /// <summary> /// 初始化所有的点 /// </summary> private void InitAllPoint() { int ix = -1; for (float i = -1; i < 1.1f; i += 0.1f) { for (float j = -1; j < 1.1; j += 0.1f) { ix += 1; mat.SetVector("pointInfom" + ix, new Vector4(i, j, 1, 0)); } } } private int Check_ClickedInsection(Vector2 vec2) { int clickIndex = -1; for (int i = 0; i < list_gridPointDisk.Count; i++) { float errorx = Mathf.Abs(vec2.x - list_gridPointDisk[i].x); float errory = Mathf.Abs(vec2.y - list_gridPointDisk[i].y); //如果误差的值小于预设的值则判定点中了 float error = Mathf.Sqrt(errorx * errorx + errory * errory); if (error < clickMinError) { clickIndex = i; break; } } return clickIndex; } } public struct PointDisk { public int col; public float x; public float y; public bool isLocked; }
注:没有实现人工智能下棋和判断输赢,本文旨在实现Shader的效果和练习,有兴趣的童鞋可以自己研究
Shader部分的代码如下:
Shader "Unlit/Backgammon2" { Properties { _backgroundColor("面板背景色",Color) = (1.0,1.0,1.0,1.0) _axesColor("坐标轴的颜色",Color) = (0.0,0.0,1.0) _gridColor("网格的颜色",Color) = (0.5,0.5,0.5) _tickWidth("网格的间距",Range(0.1,1)) = 0.1 _gridWidth("网格的宽度",Range(0.0001,0.01)) = 0.008 _axesXWidth("x轴的宽度",Range(0.0001,0.01)) = 0.006 _axesYWidth("y轴的宽度",Range(0.0001,0.01)) = 0.007 _radius("圆盘的半径",Range(0.001,0.05)) = 0.01 _col1("圆盘1的颜色",Color) = (0.867, 0.910, 0.247) // yellow _col2("圆盘2的颜色",Color) = (0.867, 0.910, 0.247) // yellow } SubShader { //去掉遮挡和深度缓冲 Cull Off ZWrite On //开启深度测试 ZTest Always CGINCLUDE //添加一个计算方法 float mod(float a,float b) { //floor(x)方法是Cg语言内置的方法,返回小于x的最大的整数 return a - b*floor(a / b); } //添加第二个计算方法,根据半径,原点和颜色来绘制圆盘 fixed3 disk(fixed2 r,fixed2 center,fixed radius,fixed3 color,fixed3 pixel) { fixed3 col = pixel; if (length(r - center) < radius) { col = color; } return col; } ENDCG Pass { CGPROGRAM //敲代码的时候要注意:“CGPROGRAM”和“#pragma...”中的拼写不同,真不知道“pragma”是什么单词 #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" uniform float4 _backgroundColor; uniform float4 _axesColor; uniform float4 _gridColor; uniform float _tickWidth; uniform float _gridWidth; uniform float _axesXWidth; uniform float _axesYWidth; uniform float4 _col1; uniform float4 _col2; uniform int pointsCount; uniform float _radius; uniform float4 pointInfom[21][21]; struct appdata { float4 vertex:POSITION; float2 uv:TEXCOORD0; }; struct v2f { float2 uv:TEXCOORD0; float4 vertex:SV_POSITION; }; v2f vert(appdata v) { v2f o; o.vertex = mul(UNITY_MATRIX_MVP,v.vertex); o.uv = v.uv; o.uv = float2(o.uv.x, 1-o.uv.y); return o; } fixed4 frag(v2f i) :SV_Target { //将坐标的中心从左下角移动到网格的中心 float2 r = 2.0*(i.uv - 0.5); float aspectRatio = _ScreenParams.x / _ScreenParams.y; //r.x *= aspectRatio; fixed3 backgroundColor = _backgroundColor.xyz; fixed3 axesColor = _axesColor.xyz; fixed3 gridColor = _gridColor.xyz; fixed3 pixel = backgroundColor; //定义网格的的间距 const float tickWidth = _tickWidth; if (mod(r.x, tickWidth) < _gridWidth) { pixel = gridColor; } if (mod(r.y, tickWidth) < _gridWidth) { pixel = gridColor; } //画两个坐标轴 if (abs(r.x) < _axesXWidth) { pixel = axesColor; } if (abs(r.y) < _axesYWidth) { pixel = axesColor; } for (int i = 0; i < 21; i++) { for (int j = 0; j < 21; j++) { if (pointInfom[i][j].w == 1) { fixed2 pos = pointInfom[i][j].xy; if (pointInfom[i][j].z == 1) { pixel = disk(r, pos, _radius, _col1.xyz, pixel); } if (pointInfom[i][j].z == 2) { pixel = disk(r, pos, _radius, _col2.xyz, pixel); } } } } //pixel = disk(r, fixed2(-0.2,0.0), _radius, _col1.xyz, pixel); return fixed4(pixel, 1.0); } ENDCG } } }
二、系列总结
1、学会如何编写一个绘制简单颜色图形的Shader代码,并了解各项参数的意义和用法
2、学会如何使用属性以及用C#脚本传递参数
3、学会在Shader中自定义函数并且在顶点或片段着色器代码中调用
4、学会怎样在Shader代码中定义Uniform的数组,并且通过C#脚本向Shader中传递数组
5、学会怎样通过数组来控制Shader的渲染状态和渲染图像的变化
来自:凯尔八阿哥专栏https://blog.csdn.net/zhangxiao13627093203/article/details/53071497