unity-game-engineshaderfragment-shadervertex-shadershaderlab

How to map letters on mesh with shader?


I'm trying to create a shader that allows mapping text on a mesh. How do I align each character horizontally? Currently, all of them overlap each other.

Currently looks like this:

current result

Texture used:

texture

Shader "Unlit/MapText"
{
    Properties
    {
        _MainTex("Texture", 2D) = "white" {}
        _FontTex("FontTexture", 2D) = "white" {}
        _Color("Color", Color) = (1,1,1,1)
    }
        SubShader
        {
            Tags { "RenderType" = "Opaque" }

            Pass
            {
                CGPROGRAM
                #pragma vertex vert
                #pragma fragment frag

                #include "UnityCG.cginc"

                uniform int _CharacterCount;
                uniform float4 _Characters[3];

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

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

                sampler2D _MainTex;
                float4 _MainTex_ST;

                sampler2D _FontTex;
                float4 _FontTex_ST;

                float4 _Color;

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

                fixed4 frag(v2f i) : SV_Target
                {
                    fixed4 col;

                    _Characters[0] = 0;
                    _Characters[1] = 1;
                    _Characters[2] = 2;

                    for (uint k = 0; k < _Characters.Length; k++)
                    {
                        float row = (k % 1024);
                        float column = (k / 1024);
                        float2 character = (i.uv1 + float2(row, column)) * 0.33;
                        _Characters[k] = tex2D(_FontTex, character);
                    }

                    col = (_Characters[0] + _Characters[1] + _Characters[2]) * _Color;
                    return col;
            }
            ENDCG
        }
    }
}

Solution

  • Explanation is in the comments

    Shader "Unlit/MapText"
    {
        Properties
        {
            _MainTex("Texture", 2D) = "white" {}
            _FontTex("FontTexture", 2D) = "white" {}
            _FontTextColumns("FontTexture Columns", Int) = 3
            _FontTextRows("FontTexture Rows", Int) = 3
            _StringCharacterCount("Length of String", Int) = 3
            _StringOffset("String offset", Vector) = (0.5,0.5,0,0)
            _StringScale("String scale", Vector) = (0.25,0.25,0,0)
            _CharWidth("Character width", Float) = 1.0
            _Color("Color", Color) = (1,1,1,1)
        }
        SubShader
        {
            Tags { "RenderType" = "Opaque" }
    
            Pass
            {
                CGPROGRAM
                #pragma vertex vert
                #pragma fragment frag
    
                #include "UnityCG.cginc"
    
                struct appdata
                {
                    float4 vertex : POSITION;
                    float2 uv : TEXCOORD0;
                    float2 uv1 : TEXCOORD1;
                };
    
                struct v2f
                {
                    float2 uv : TEXCOORD0;
                    float4 vertex : SV_POSITION;
                };
    
                sampler2D _MainTex;
                float4 _MainTex_ST;
    
                sampler2D _FontTex;
                float4 _FontTex_ST;
    
                float4 _Color;
    
                // font texture information
                int _FontTextColumns;
                int _FontTextRows;
    
                // string length
                int _StringCharacterCount;
    
                // float array because there's no SetIntArray in c#
                float _String_Chars[512];
    
                // string placement & scaling
                float4 _StringOffset;
                float4 _StringScale;
    
                // Character width - combine with StringScale to change character spacing
                float _CharWidth;
    
                v2f vert(appdata v)
                {
                    v2f o;
                    o.vertex = UnityObjectToClipPos(v.vertex);
                    o.uv = v.uv;
                    return o;
                }
    
                fixed4 frag(v2f i) : SV_Target
                {
                    // discard pixel if _StringCharacterCount < 1
                    clip(_StringCharacterCount - 1);
    
                    fixed4 col;
    
                    // Determine what character in the string this pixel is in
                    // And what UV of that character we are in
                    float charIndex = 0;
                    float2 inCharUV = float2(0,0);
    
                    // Avoid i.uv.x = 1 and indexing charIndex[_StringCharacterCount] 
                    i.uv.x = clamp(i.uv.x, 0.0, 0.99999);
    
                    // Scale and offset uv
                    i.uv = clamp((i.uv - _StringOffset.xy) / _StringScale.xy + 0.5, 0, 1);
    
                    // Find where in the char to sample
                    inCharUV = float2(
                        modf(i.uv.x * _StringCharacterCount, charIndex),
                        i.uv.y);
    
                    // Scale inCharUV.x based on charWidth factor
                    inCharUV.x = (inCharUV.x-0.5f)/charWidth + 0.5f;
    
                    // Clamp char uv
                    // alternatively you could clip if outside (0,0)-(1,1) rect
                    inCharUV = clamp(inCharUV, 0, 1);
    
                    // Get char uv in font texture space
                    float fontIndex = _String_Chars[charIndex];
                    float fontRow = floor(fontIndex / _FontTextColumns);
                    float fontColumn = floor(fontIndex % _FontTextColumns);
    
                    float2 fontUV = float2(
                            (fontColumn + inCharUV.x) / _FontTextColumns,
                            1.0 - (fontRow + 1.0 - inCharUV.y) / _FontTextRows);
    
                    // Sample the font texture at that uv
                    col = tex2D(_FontTex, fontUV);
    
                    // Modify by color:
                    col = col * _Color;
    
                    return col;
                }
               ENDCG
            }
        }
    }
    

    And then in C#, you have to set the string array and tell it the length of the string you'd like to draw. There's no SetIntArray but SetFloatArray works:

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class test : MonoBehaviour
    {
        Material mapTextMaterial;
    
        void Awake()
        {
            Renderer renderer = GetComponent<Renderer>();
            mapTextMaterial = renderer.material;
    
            float[] stringArray = new float[] { 2f, 0f, 6f, 4f, 3f }; // "CAGED"
            mapTextMaterial.SetFloatArray("_String_Chars", stringArray);
            mapTextMaterial.SetInt("_StringCharacterCount", stringArray.Length);
        }
    
    }
    

    Be sure to initialize _String_Chars and _StringCharacterCount to something in Awake or it could produce unexpected results. Consider including a "blank" character in the font texture so you can initialize to a one-character string that is just the blank character.

    This shader produces an effect like this (note the different shader properties on the right):

    red AAAA on quad cyan CAGED on quad

    Just be sure to have the font texture's "Generate Mip Maps" turned off (unchecked) or you will get unexpected artifacts:

    lines due to mip maps