yu00’s blog

プログラミングに関する備忘録です

Unity Shaderで文字を描く

UnityではテクスチャやGUIを使って文字を描くことができますが、
動く文字をメッシュの表面に沿って描画することはできません。
そこで、Unity Shader Labで文字を描く方法について説明します。

方針

  • 文字が書かれたアトラステクスチャを用意する
  • アトラステクスチャから対象の文字を配置する
  • 文字以外を塗りつぶす

文字が書かれたアトラステクスチャを用意する

以下のような文字一覧が書かれたアトラステクスチャを用意します。

今回は数字のみ用意しましたが、文字が書かれていても同様にできます。

アトラステクスチャの座標変換

アトラステクスチャの任意の文字の左下の点と右上の点を
uv座標の任意の点p1とp2に変換することを考えます。

図のようにtex座標の点が点に、
が点に同じ変換行列Hで
変換されるとすると、以下の式が成り立ちます。

座標変換は移動とスケールのみで表せるので、

とし方程式を解くと、

となります。

アトラステクスチャから対象の文字を配置する

アトラステクスチャの数字と同じようにインデックスを付けます。
インデックスからtex座標の点を求めます。

uint _Index;
float _Size; // UV座標での1文字のサイズ
float _Px; // UV座標での左下の位置x
float _Py; // UV座標での左下の位置y
uint _DivNum; // アトラステクスチャの分割数

fixed4 frag (v2f i) : SV_Target
{
    uint colIndex = _Index % _DivNum;
    uint rowIndex = _Index / _DivNum;
    float2 p1_tex = float2(colIndex, rowIndex) / _DivNum;
    float2 p1_uv = float2(_Px, _Py);
    float t = 1.0 / _DivNum;
    float s = _Size;
    float t_s = t/s;
    i.uv = t_s * i.uv + p1_tex - t_s * p1_uv;

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

インデックス5、位置(0.5, 0.5)、サイズ0.5で表示すると
以下図のようになります。

文字以外を塗りつぶす

まずは文字以外を塗りつぶす正方形を作ります。

inline float Square(float2 UV, float Size)
{
    float2 d = abs(UV*2 - Size) - Size;
    d = 1 - d / fwidth(d);
    return saturate(min(d.x, d.y));
}
fixed4 frag (v2f i) : SV_Target
{
    float2 p1_uv = float2(_Px, _Py);
    fixed4 col = Square(i.uv - p1_uv, _Size);
    return col;
}

次にこの正方形と文字の積を取ることで、
文字以外を塗りつぶします。

inline float2 GetAtlas(float2 UV, uint DivNum, uint Index, float Size, float2 Pos)
{
    uint colIndex = Index % DivNum;
    uint rowIndex = Index / DivNum;
    float2 p1_tex = float2(colIndex, rowIndex) / DivNum;
    float t_s = 1.0/Size/DivNum;
    UV = t_s * UV + p1_tex - t_s * Pos;

    return UV;
}
inline float Square(float2 UV, float Size)
{
    float2 d = abs(UV*2 - Size) - Size;
    d = 1 - d / fwidth(d);
    return saturate(min(d.x, d.y));
}
fixed4 frag (v2f i) : SV_Target
{
    float2 p1_uv = float2(_Px, _Py);
    float2 textUv = GetAtlas(i.uv, _DivNum, _Index, _Size, p1_uv);
    fixed4 text = tex2D(_MainTex, textUv);
    float mask = Square(i.uv - p1_uv, _Size);
    fixed4 col = text.a * mask;
    return col;
}

コード全文

Shader "Unlit/Text"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        [IntRange] _Index ("Index", Range(0, 8)) = 0
        _Size ("Size", Range(0.01, 1.0)) = 1
        [IntRange] _DivNum ("DivNum", Range(1, 16)) = 1
        _Px ("Px", Range(0.0, 1.0)) = 0.0
        _Py ("Py", Range(0.0, 1.0)) = 0.0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

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

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            int _Index;
            float _Size; // UV座標での1文字のサイズ
            float _Px; // UV座標での左下の位置x
            float _Py; // UV座標での左下の位置y
            int _DivNum; // アトラステクスチャの分割数

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }
            inline float2 GetAtlas(float2 UV, uint DivNum, uint Index, float Size, float2 Pos)
            {
                uint colIndex = Index % DivNum;
                uint rowIndex = Index / DivNum;
                float2 p1_tex = float2(colIndex, rowIndex) / DivNum;
                float t_s = 1.0/Size/DivNum;
                UV = t_s * UV + p1_tex - t_s * Pos;

                return UV;
            }
            inline float Square(float2 UV, float Size)
            {
                float2 d = abs(UV*2 - Size) - Size;
                d = 1 - d / fwidth(d);
                return saturate(min(d.x, d.y));
            }
            fixed4 frag (v2f i) : SV_Target
            {
                float2 p1_uv = float2(_Px, _Py);
                float2 textUv = GetAtlas(i.uv, _DivNum, _Index, _Size, p1_uv);
                fixed4 text = tex2D(_MainTex, textUv);
                float mask = Square(i.uv - p1_uv, _Size);
                fixed4 col = text.a * mask;
                return col;
            }
            ENDCG
        }
    }
}

複数の文字を描く

x座標によってインデックスを変えることで複数の文字を
描くことができます。

uint _Index0;
uint _Index1;
uint _Index2;
uint _Index3;
static const uint IndexLength = 4; // indexesの要素数
fixed4 frag (v2f i) : SV_Target
{
    uint indexes[4] = {_Index0, _Index1, _Index2, _Index3};
    uint j = (uint)((i.uv.x - _Px) / _Size) % IndexLength;
    float2 p1_uv = float2(_Px + j * _Size, _Py);
    float2 textUv = GetAtlas(i.uv, _DivNum, indexes[j], _Size, p1_uv);
    fixed4 text = tex2D(_MainTex, textUv);
    float mask = Square(i.uv - p1_uv, _Size);
    fixed4 col = text.a * mask;
    return col;
}

応用例 現在の時刻を表示する

C#スクリプトからインデックスを変更することで現在時刻を表示します。

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

public class TimeChanger : MonoBehaviour
{
    Material Material;
    // Start is called before the first frame update
    void Start()
    {
        Material = GetComponent<MeshRenderer>().material;
    }

    // Update is called once per frame
    void Update()
    {
        DateTime dt = DateTime.Now;
        Material.SetInt("_Index0", dt.Minute / 10);
        Material.SetInt("_Index1", dt.Minute % 10);
        Material.SetInt("_Index2", dt.Second / 10);
        Material.SetInt("_Index3", dt.Second % 10);
    }
}
Shader "Custom/MultiTextSurf"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _Glossiness ("Smoothness", Range(0,1)) = 0.5
        _Metallic ("Metallic", Range(0,1)) = 0.0

        [IntRange] _Index0 ("Index0", Range(0, 8)) = 0
        [IntRange] _Index1 ("Index1", Range(0, 8)) = 0
        [IntRange] _Index2 ("Index2", Range(0, 8)) = 0
        [IntRange] _Index3 ("Index3", Range(0, 8)) = 0
        _Size ("Size", Range(0.01, 1.0)) = 1
        [IntRange] _DivNum ("DivNum", Range(1, 16)) = 1
        _Px ("Px", Range(0.0, 1.0)) = 0.0
        _Py ("Py", Range(0.0, 1.0)) = 0.0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200

        CGPROGRAM
        // Physically based Standard lighting model, and enable shadows on all light types
        #pragma surface surf Standard fullforwardshadows

        // Use shader model 3.0 target, to get nicer looking lighting
        #pragma target 3.0

        sampler2D _MainTex;

        struct Input
        {
            float2 uv_MainTex;
        };

        half _Glossiness;
        half _Metallic;
        fixed4 _Color;

        uint _Index0;
        uint _Index1;
        uint _Index2;
        uint _Index3;
        float _Size; // UV座標での1文字のサイズ
        float _Px; // UV座標での左下の位置x
        float _Py; // UV座標での左下の位置y
        uint _DivNum; // アトラステクスチャの分割数
        
        static const uint IndexLength = 4; // indexesの要素数

        // Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader.
        // See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
        // #pragma instancing_options assumeuniformscaling
        UNITY_INSTANCING_BUFFER_START(Props)
            // put more per-instance properties here
        UNITY_INSTANCING_BUFFER_END(Props)

        inline float2 GetAtlas(float2 UV, uint DivNum, uint Index, float Size, float2 Pos)
        {
            uint colIndex = Index % DivNum;
            uint rowIndex = Index / DivNum;
            float2 p1_tex = float2(colIndex, rowIndex) / DivNum;
            float t_s = 1.0/Size/DivNum;
            UV = t_s * UV + p1_tex - t_s * Pos;

            return UV;
        }
        inline float Square(float2 UV, float Size)
        {
            float2 d = abs(UV*2 - Size) - Size;
            d = 1 - d / fwidth(d);
            return saturate(min(d.x, d.y));
        }
        
        void surf (Input IN, inout SurfaceOutputStandard o)
        {

            uint indexes[4] = {_Index0, _Index1, _Index2, _Index3};
            uint j = (uint)((IN.uv_MainTex.x - _Px) / _Size) % IndexLength;
            float2 p1_uv = float2(_Px + j * _Size, _Py);
            float2 textUv = GetAtlas(IN.uv_MainTex, _DivNum, indexes[j], _Size, p1_uv);
            fixed4 text = tex2D(_MainTex, textUv);
            float mask = Square(IN.uv_MainTex - p1_uv, _Size);
            fixed4 col = text.a * mask;

            // Albedo comes from a texture tinted by color
            o.Albedo = col.rgb;
            // Metallic and smoothness come from slider variables
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

バージョン

  • Unity : 2022.3.22f1


Powered by MathJax

This page is based on MathJax technology.