Unity&Shader案例篇—五子棋

一、前言

尊重原创,转载请注明出处凯尔八阿哥专栏
上篇Unity&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的渲染状态和渲染图像的变化
最后附上此篇文章的工程下载地址点击打开链接