using System; using TMPro; using UnityEngine; using Object = UnityEngine.Object; namespace ru.chikalin.textdecal { [ExecuteAlways] [RequireComponent(typeof(TMP_Text))] [DefaultExecutionOrder(11)] [AddComponentMenu("Mesh/TextMeshPro - Decal")] [HelpURL("https://assetstore.unity.com/")] // todo #if UNITY_2021_2_OR_NEWER [Icon("Editor/Resources/Icons/sticker.png")] // todo #endif public class TextMeshProDecal : MonoBehaviour { private const int VolumetricVertexCount = 8; private const int VolumetricTrianglesCount = 36; private readonly int[] _volumetricTriangles = { 0, 1, 2, //face front 2, 3, 0, 1, 5, 6, //face top 6, 2, 1, 3, 2, 6, //face right 6, 7, 3, 4, 5, 1, //face left 1, 0, 4, 4, 0, 3, //face back 3, 7, 4, 7, 6, 5, //face bottom 5, 4, 7 }; [Tooltip("Volumetric mesh depth")] public float depth = 1; private Vector3 _depthVector; private TMP_Text _text; private Vector3[] _vertices; private int[] _triangles; private Color[] _colors; private Vector2[] _uv; private Vector2[] _uv2; private Vector2[] _uv4; private Vector2[] _uv5; private Vector2[] _uv6; private Vector2[] _uv7; private Vector2[] _uv8; private Mesh _volumetricMesh; private Mesh VolumetricMesh { get { if (_volumetricMesh != null) return _volumetricMesh; _volumetricMesh = new Mesh { hideFlags = HideFlags.HideAndDontSave }; #if DEVELOPMENT_BUILD || UNITY_EDITOR _volumetricMesh.name = "TextMeshPro Decal Mesh"; #endif _volumetricMesh.MarkDynamic(); return _volumetricMesh; } } private MeshFilter _meshFilter; private MeshFilter MeshFilter { get { if (_meshFilter != null) return _meshFilter; _meshFilter = GetComponent(); if (_meshFilter != null) return _meshFilter; _meshFilter = gameObject.AddComponent(); _meshFilter.hideFlags = HideFlags.HideInInspector | HideFlags.HideAndDontSave; return _meshFilter; } } private void Awake() { _text = GetComponent(); } private void OnEnable() { UpdateDepthVector(); _text.OnPreRenderText += OnPreRenderText; TMPro_EventManager.TEXT_CHANGED_EVENT.Add(OnTextChangedEvent); OnPreRenderText(_text.textInfo); } private void OnDisable() { TMPro_EventManager.TEXT_CHANGED_EVENT.Remove(OnTextChangedEvent); _text.OnPreRenderText -= OnPreRenderText; _text.ForceMeshUpdate(); MeshFilter.sharedMesh = _text.mesh; if (_volumetricMesh != null) { DestroyImmediate(_volumetricMesh); } } private void OnTextChangedEvent(Object obj) { if (obj != _text || !string.IsNullOrEmpty(_text.text)) return; // force update if text is empty OnPreRenderText(_text.textInfo); } private void OnPreRenderText(TMP_TextInfo textInfo) { // todo remove if (_text.isVolumetricText) { var textMesh = _text.mesh; MeshFilter.sharedMesh = textMesh; } else { PrepareMeshData(textInfo); var characterIndex = 0; for (var i = 0; i < textInfo.characterCount; i++) { var characterInfo = textInfo.characterInfo[i]; if (!characterInfo.isVisible) continue; var materialIndex = characterInfo.materialReferenceIndex; var meshInfo = textInfo.meshInfo[materialIndex]; AddCharacter(characterInfo, meshInfo, characterIndex++); } UpdateMesh(); } } /// /// Add volumetric mesh for a character /// /// TextMesh character info /// TextMesh mesh info /// Character index private void AddCharacter(TMP_CharacterInfo charInfo, TMP_MeshInfo meshInfo, int characterIndex) { var volumetricVertexIndex = characterIndex * VolumetricVertexCount; var vertexIndex = charInfo.vertexIndex; /* * copy TextMesh vertices */ var bl = meshInfo.vertices[vertexIndex + 0]; var tl = meshInfo.vertices[vertexIndex + 1]; var tr = meshInfo.vertices[vertexIndex + 2]; var br = meshInfo.vertices[vertexIndex + 3]; _vertices[volumetricVertexIndex + 0] = bl; _vertices[volumetricVertexIndex + 1] = tl; _vertices[volumetricVertexIndex + 2] = tr; _vertices[volumetricVertexIndex + 3] = br; /* * Extrude face to make a cube */ _vertices[volumetricVertexIndex + 4] = bl + _depthVector; _vertices[volumetricVertexIndex + 5] = tl + _depthVector; _vertices[volumetricVertexIndex + 6] = tr + _depthVector; _vertices[volumetricVertexIndex + 7] = br + _depthVector; /* * Fill volumetric triangles */ for (var i = 0; i < _volumetricTriangles.Length; i++) { _triangles[characterIndex * VolumetricTrianglesCount + i] = volumetricVertexIndex + _volumetricTriangles[i]; } /* * Copy TextMesh UVs */ _uv[volumetricVertexIndex + 0] = charInfo.vertex_BL.uv; _uv[volumetricVertexIndex + 1] = charInfo.vertex_TL.uv; _uv[volumetricVertexIndex + 2] = charInfo.vertex_TR.uv; _uv[volumetricVertexIndex + 3] = charInfo.vertex_BR.uv; _uv[volumetricVertexIndex + 4] = charInfo.vertex_BL.uv; _uv[volumetricVertexIndex + 5] = charInfo.vertex_TL.uv; _uv[volumetricVertexIndex + 6] = charInfo.vertex_TR.uv; _uv[volumetricVertexIndex + 7] = charInfo.vertex_BR.uv; _uv2[volumetricVertexIndex + 0] = charInfo.vertex_BL.uv2; _uv2[volumetricVertexIndex + 1] = charInfo.vertex_TL.uv2; _uv2[volumetricVertexIndex + 2] = charInfo.vertex_TR.uv2; _uv2[volumetricVertexIndex + 3] = charInfo.vertex_BR.uv2; _uv2[volumetricVertexIndex + 4] = charInfo.vertex_BL.uv2; _uv2[volumetricVertexIndex + 5] = charInfo.vertex_TL.uv2; _uv2[volumetricVertexIndex + 6] = charInfo.vertex_TR.uv2; _uv2[volumetricVertexIndex + 7] = charInfo.vertex_BR.uv2; /* * Additional mesh data for decal shader */ // character bottom left vertex position var uv4 = new Vector2(bl.x, bl.y); // character bottom left UV var uv5 = charInfo.vertex_BL.uv; // character 3D size var width = Vector3.Distance(br, bl); var height = Vector3.Distance(tr, br); var uv6 = new Vector2(width, height); // character UV size var uvWidth = Vector2.Distance(charInfo.vertex_BR.uv, charInfo.vertex_BL.uv); var uvHeight = Vector2.Distance(charInfo.vertex_TR.uv, charInfo.vertex_BR.uv); var uv7 = new Vector2(uvWidth, uvHeight); // character 3D Z-rotation var angle = Vector3.SignedAngle(Vector3.up, tl - bl, Vector3.forward); var uv8 = Mathf.Deg2Rad * angle; for (var i = 0; i < VolumetricVertexCount; i++) { _uv4[volumetricVertexIndex + i] = uv4; _uv5[volumetricVertexIndex + i] = uv5; _uv6[volumetricVertexIndex + i] = uv6; _uv7[volumetricVertexIndex + i] = uv7; _colors[volumetricVertexIndex + i] = charInfo.color; _uv8[volumetricVertexIndex + i].x = uv8; } } private void UpdateMesh() { var mesh = VolumetricMesh; mesh.Clear(); mesh.vertices = _vertices; mesh.triangles = _triangles; mesh.uv = _uv; mesh.uv2 = _uv2; mesh.colors = _colors; mesh.uv4 = _uv4; mesh.uv5 = _uv5; mesh.uv6 = _uv6; mesh.uv7 = _uv7; mesh.uv8 = _uv8; if (MeshFilter.sharedMesh != mesh) { MeshFilter.sharedMesh = mesh; } mesh.RecalculateBounds(); mesh.MarkModified(); } private void PrepareMeshData(TMP_TextInfo textInfo) { var size = textInfo.characterInfo.Length; var vertexSize = size * VolumetricVertexCount; UpdateArray(ref _vertices, vertexSize); UpdateArray(ref _triangles, size * VolumetricTrianglesCount); UpdateArray(ref _colors, vertexSize); UpdateArray(ref _uv, vertexSize); UpdateArray(ref _uv2, vertexSize); UpdateArray(ref _uv4, vertexSize); UpdateArray(ref _uv5, vertexSize); UpdateArray(ref _uv6, vertexSize); UpdateArray(ref _uv7, vertexSize); UpdateArray(ref _uv8, vertexSize); } private static void UpdateArray(ref T[] array, int size) { if (array == null) { array = new T[size]; return; } if (array.Length < size) { Array.Resize(ref array, size); } Array.Clear(array, 0, array.Length); } #if UNITY_EDITOR private void OnValidate() { if (_text != null) { _text.ForceMeshUpdate(); } UpdateDepthVector(); } #endif private void UpdateDepthVector() => _depthVector = new Vector3(0, 0, depth); } }