[Unity] WorldSpace 2D Grid Shader Selectable by Mouse Click 可以使用鼠标点击进行选择的世界坐标网格

280 阅读5分钟

仓库:

github.com/CheapMeow/U…

平铺 Grid

对于图片,平铺设置为:

Sprite Mode - Mesh Type = Full Rect

Advanced - Wrap Mode = Repeat

图片.png

对于 Sprite Render,平铺设置为:

Draw Mode = Tiled

图片.png

假设设置 Draw Mode - Size = (5, 5),效果为:

图片.png

简单的 Grid Shader

Shader "Unlit/UnlitGrid"
{
    Properties
    {
        [PerRendererData] _MainTex ("Texture", 2D) = "white" {}
        [FloatRange] _GridLineSize ("GridLineSize", Range(0,1)) = 0.1
        _LineColor ("Line Color", Color) = (1,1,1,1)    
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float _GridLineSize;
            float4 _LineColor;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                float2 bl = step(float2(_GridLineSize, _GridLineSize), i.uv);       // bottom-left
                float2 tr = step(float2(_GridLineSize, _GridLineSize), float2(1.0, 1.0) - i.uv);   // top-right
                float grid_brightness = 1.0 - bl.x * bl.y * tr.x * tr.y;

                fixed4 col = tex2D(_MainTex, i.uv) * (1.0 - grid_brightness) + _LineColor * grid_brightness;

                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
    }
}

其中画网格的部分是

float2 bl = step(float2(_GridLineSize, _GridLineSize), i.uv);       // bottom-left
float2 tr = step(float2(_GridLineSize, _GridLineSize), float2(1.0, 1.0) - i.uv);   // top-right
float grid_brightness = 1.0 - bl.x * bl.y * tr.x * tr.y;

fixed4 col = tex2D(_MainTex, i.uv) * (1.0 - grid_brightness) + _LineColor * grid_brightness;

思路可见:thebookofshaders.com/07/

这里还少了一个缩放网格的功能,不过效果差不多了

但是这样做有一个坏处就是,图片和网格是绑定在一起的,图片动了,网格也很会跟着动

这样,如果需要找到网格上面的某一个格子做一些功能的话,就需要考虑到图片的坐标系,图片的缩放,想想感觉挺麻烦

如果让网格变成世界坐标的,与图片坐标无关,那么找网格上面某一个格子就会很方便

WorldSpace Grid

World Position

获取图片中某个像素的世界坐标的 Shader

Shader "Unlit/UnlitGrid"
{
    Properties
    {
        [PerRendererData] _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
                float4 worldSpacePos : TEXCOORD1;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                UNITY_TRANSFER_FOG(o,o.vertex);

                o.worldSpacePos = mul(unity_ObjectToWorld, v.vertex);

                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = i.worldSpacePos;

                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
    }
}

其中的关键点在于从顶点着色器向片元着色器传递一个世界坐标的 float4

首先要在 v2f 中设置一个属性 float4 worldSpacePos

struct v2f
{
    float2 uv : TEXCOORD0;
    UNITY_FOG_COORDS(1)
    float4 vertex : SV_POSITION;
    float4 worldSpacePos : TEXCOORD1;
};

然后在顶点着色器里面计算这个世界坐标

o.worldSpacePos = mul(unity_ObjectToWorld, v.vertex);

在片元着色器中就可以测试了

fixed4 col = i.worldSpacePos;

效果

图片.png

可以看到颜色是分坐标系的,说明 shader 计算正确

WorldSpace Grid

对上面的着色器稍作修改,就得到了世界坐标中的 Grid

world_space_grid.gif

Shader "Unlit/UnlitGrid"
{
    Properties
    {
        [PerRendererData] _MainTex ("Texture", 2D) = "white" {}
        _GridCellSize ("GridCellSize", float) = 64
        [FloatRange] _GridLineSize ("GridLineSize", Range(0,1)) = 0.02
        _LineColor ("Line Color", Color) = (1,1,1,1)    
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
                float4 worldSpacePos : TEXCOORD1;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float _GridCellSize;
            float _GridLineSize;
            float4 _LineColor;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                UNITY_TRANSFER_FOG(o,o.vertex);

                o.worldSpacePos = mul(unity_ObjectToWorld, v.vertex);

                return o;
            }

            fixed4 AddGrid(fixed4 col, float2 fpos)
            {
                float scaled_line_size = _GridLineSize/_GridCellSize;
                
                float2 bl = step(float2(scaled_line_size, scaled_line_size), fpos);       // bottom-left
                float2 tr = step(float2(scaled_line_size, scaled_line_size), float2(1.0, 1.0) - fpos);   // top-right
                float grid_brightness = 1.0 - bl.x * bl.y * tr.x * tr.y;

                col = col * (1.0 - grid_brightness) + _LineColor * grid_brightness;

                return col;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                float2 st = i.worldSpacePos.xy / _GridCellSize;

                float2 ipos = floor(st);  // integer
                float2 fpos = frac(st);  // fraction

                fixed4 col = tex2D(_MainTex, i.uv);

                col = AddGrid(col, fpos);
                
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
    }
}

传递鼠标的世界坐标到材质

在 Shader 中定义一个 uniform 变量

uniform float4 mouseWorldPos;

创建一个脚本,传递鼠标的世界坐标到材质

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Playground : MonoBehaviour
{
    private Renderer render;
    
    // Start is called before the first frame update
    void Start()
    {
        render = GetComponent<Renderer>();
    }

    // Update is called once per frame
    void Update()
    {
        Vector3 mouseWorldPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
        print(mouseWorldPos);
        render.material.SetVector("mouseWorldPos",new Vector4(mouseWorldPos.x, mouseWorldPos.y, 0, 0));
    }
}

在 Shader 中画一个圆来测试是否正确收到鼠标位置

fixed4 frag (v2f i) : SV_Target
{
    float2 st = i.worldSpacePos.xy / _GridCellSize;

    float2 ipos = floor(st);  // integer
    float2 fpos = frac(st);  // fraction

    fixed4 col = distance(st, mouseWorldPos.xy);
    
    // apply fog
    UNITY_APPLY_FOG(i.fogCoord, col);
    return col;
}

结果:

world_mouse_pos.gif

鼠标点击控制 Tint

现在要实现一个功能,鼠标点击一次,使得图片上相应位置的格子变色,再点击一次,图片恢复原来的颜色

首先从点击一次,让整个图片变色开始

在 Shader 中添加一个属性,使得 Tint 的颜色可以调整

_DeactiveTint ("DeactiveTint", Color) = (0.6, 0.6, 0.6, 1)

在 Shader 的 Pass 中设置对应的变量

float4 _DeactiveTint;

在 Shader 的 Pass 中设置一个 float 变量,来完成跟布尔值差不多的功能

uniform float shouldTint;

通过 float 变量 = 1 还是 = 0 来切换颜色

fixed4 add_tint(fixed4 col, float2 ipos)
{
    col = col * (1.0 - shouldTint) + col * _DeactiveTint * shouldTint;
    
    return col;
}

最终在片元着色器中应用

fixed4 frag (v2f i) : SV_Target
{
    float2 st = i.worldSpacePos.xy / _GridCellSize;

    float2 ipos = floor(st);  // integer
    float2 fpos = frac(st);  // fraction

    fixed4 col = tex2D(_MainTex, i.uv);

    col = add_tint(col, ipos);
    
    // apply fog
    UNITY_APPLY_FOG(i.fogCoord, col);
    return col;
}

脚本多添加一行,将点击次数传入材质

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Playground : MonoBehaviour
{
    private Renderer render;

    private float shouldTint = 0f;
    
    // Start is called before the first frame update
    void Start()
    {
        render = GetComponent<Renderer>();
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            Vector3 mouseWorldPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
            shouldTint = 1f - shouldTint;
            
            render.material.SetVector("clickPos",new Vector4(mouseWorldPos.x, mouseWorldPos.y, 0, 0));
            render.material.SetFloat("shouldTint", shouldTint);
        }
    }
}

这样就完成了鼠标点击一遍变色,再点击一遍恢复的功能

Selective Grid

更进一步,使用 ipos 来决定哪里需要着色,就完成了对指定格子着色的功能

click_pos.gif

Shader "Unlit/UnlitGrid"
{
    Properties
    {
        [PerRendererData] _MainTex ("Texture", 2D) = "white" {}
        _GridCellSize ("Grid Cell Size", float) = 1
        [FloatRange] _GridLineSize ("Grid Line Size", Range(0,1)) = 0.02
        _LineColor ("Line Color", Color) = (0.7, 0.7, 0.7, 1)
        _ReachableTint ("Reachable Tint", Color) = (0.9, 0.9, 0.9, 1)
        _UnreachableTint ("Unreachable Tint", Color) = (0.6, 0.6, 0.6, 1)    
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
                float4 worldSpacePos : TEXCOORD1;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float _GridCellSize;
            float _GridLineSize;
            float4 _LineColor;
            float4 _ReachableTint;
            float4 _UnreachableTint;
            
            uniform float4 click_pos;
            uniform float should_tint;
            uniform float tint_radius;
            
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                UNITY_TRANSFER_FOG(o,o.vertex);

                o.worldSpacePos = mul(unity_ObjectToWorld, v.vertex);

                return o;
            }

            fixed4 add_tint(fixed4 col, float2 ipos)
            {
                float2 ipos_relatived_to_click_ipos = ipos - floor(click_pos/_GridCellSize);
                
                float dist = ipos_relatived_to_click_ipos.x * ipos_relatived_to_click_ipos.x
                            + ipos_relatived_to_click_ipos.y * ipos_relatived_to_click_ipos.y;
                
                float reachable = step(dist, tint_radius * tint_radius + 0.001);
                
                fixed4 tint = col * _UnreachableTint * (1.0 - reachable) + col * _ReachableTint * reachable;
                
                col = col * (1.0 - should_tint) + tint * should_tint;

                return col;
            }
            
            fixed4 add_grid(fixed4 col, float2 fpos)
            {
                float scaled_line_size = _GridLineSize/_GridCellSize;
                
                float2 bl = step(float2(scaled_line_size, scaled_line_size), fpos);       // bottom-left
                float2 tr = step(float2(scaled_line_size, scaled_line_size), float2(1.0, 1.0) - fpos);   // top-right
                float grid_brightness = 1.0 - bl.x * bl.y * tr.x * tr.y;

                col = col * (1.0 - grid_brightness) + _LineColor * grid_brightness;

                return col;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                float2 st = i.worldSpacePos.xy / _GridCellSize;

                float2 ipos = floor(st);  // integer
                float2 fpos = frac(st);  // fraction

                fixed4 col = tex2D(_MainTex, i.uv);

                col = add_tint(col, ipos);
                col = add_grid(col, fpos);
                
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
    }
}

使用脚本传入选择半径

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Playground : MonoBehaviour
{
    public float tintRadius = 1f;
    
    private Renderer render;

    private float shouldTint = 0f;
    
    // Start is called before the first frame update
    void Start()
    {
        render = GetComponent<Renderer>();
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            Vector3 mouseWorldPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
            if (IsMouseClickChessboard(mouseWorldPos))
            {
                print(GetClickCellPos(mouseWorldPos));

                shouldTint = 1f - shouldTint;

                render.material.SetVector("click_pos", new Vector4(mouseWorldPos.x, mouseWorldPos.y, 0, 0));
                render.material.SetFloat("should_tint", shouldTint);
                render.material.SetFloat("tint_radius", tintRadius);
            }
        }
    }

    private Vector2 GetTiledSpriteSize()
    {
        Vector2 trSize = new Vector2(transform.localScale.x, transform.localScale.y);
        Vector2 renderSize = render.size;
        Vector2 tiledSpriteSize = trSize * renderSize;

        return tiledSpriteSize;
    }

    /// <summary>
    /// 根据 SpriteRenderer Tile 的 Size 来判断点击位置是否在 Sprite 区域内
    /// </summary>
    /// <param name="mouseWorldPos">鼠标点击位置的世界坐标</param>
    /// <returns></returns>
    private bool IsMouseClickChessboard(Vector3 mouseWorldPos)
    {
        Vector2 tiledSpriteSize = GetTiledSpriteSize();

        // mouse Position Relative to Chessboard
        Vector2 mouseRelPos = mouseWorldPos - transform.position;

        if (mouseRelPos.x > 0 &&
            mouseRelPos.x < tiledSpriteSize.x &&
            mouseRelPos.y > 0 &&
            mouseRelPos.y < tiledSpriteSize.y)
        {
            return true;
        }

        return false;
    }

    private Vector2Int GetClickCellPos(Vector3 mouseWorldPos)
    {
        Vector2 mouseGridWorldPos = (mouseWorldPos - transform.position) / gridCellSize;
        Vector2Int clickCellPos = Vector2Int.FloorToInt(mouseGridWorldPos);
        return clickCellPos;
    }
}