Unity Shader案例篇—五子棋

发表于2018-06-12
评论0 1.8k浏览
本篇文章给大家介绍下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

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引