1. 项目概述:为什么我们需要段落级渐变色?
在Unity的UI开发中,尤其是使用UGUI或TextMeshPro时,我们经常遇到一个看似简单却令人头疼的需求:如何让一大段文字拥有丰富的色彩变化?默认情况下,无论是UGUI的Text还是TextMeshPro,都只支持对单个字符应用顶点色,或者通过Shader实现全局的、基于UV的渐变。但当我们想要实现“从这一段开始是蓝色,过渡到那一段变成红色”这种基于语义段落、而非单个字符或整个文本框的渐变色效果时,常规方法就捉襟见肘了。
我接手过一个项目,需要制作一个视觉小说风格的对话界面。主角的台词需要根据情绪变化,从冷静的深蓝色渐变为激动的橙红色。如果对整个文本框应用一个从左到右的渐变,当台词换行时,颜色过渡会变得支离破碎,完全破坏了情绪表达的连贯性。这就是“段落级”渐变色的核心价值所在——它让颜色的变化与文本的语义结构(段落、句子)同步,而不是被UI元素的物理布局(文本框的宽高)所束缚。
传统的解决方案无外乎几种:把文字拆成多个GameObject分别上色(维护噩梦)、使用富文本标签逐字标记(效率低下且不灵活)、或者编写复杂的Shader根据字符索引计算颜色(实现复杂且难以动态控制)。直到我深入研究了TextMeshPro的贴图(Atlas)和材质系统,才发现了一条更优雅、更高效的道路:利用TextMeshPro对每个字符独立采样贴图的特性,通过动态生成并应用一张渐变贴图,并将贴图的UV映射与文本的段落布局关联起来,从而实现精准的段落级色彩控制。
这个方法听起来有点绕,但原理其实很直观。想象一下,TextMeshPro渲染的每个字符,本质上都是从一张大贴图(字体图集)上“抠”下来一小块进行显示。如果我们能控制每个字符采样贴图时所用的“坐标”(即UV),并且我们准备的贴图本身就是一张从上到下或从左到右的渐变色图,那么通过程序动态地调整不同段落字符的UV坐标范围,就能让它们分别采样到渐变贴图上不同区段的颜色。这样一来,一段文字的所有字符共享同一张渐变贴图,但通过UV偏移,就能呈现出段落间的颜色差异和段落内的平滑过渡。
2. 核心思路与方案选型:为什么是贴图UV控制?
在决定采用贴图UV控制方案前,我系统性地评估了市面上常见的几种给TextMeshPro文字上色的方法,每一种都有其明显的局限性,而我们的“段落级渐变色”需求恰好放大了这些缺点。
2.1 常见方案对比与淘汰原因
方案一:UGUI/TextMeshPro顶点色逐字修改
- 原理:直接操作
TMP_Text的textInfo.characterInfo数组,遍历每个字符的四个顶点,修改其color属性。 - 缺点:这是最“重”的操作。每次文本更新(包括内容、字体大小、布局变化)都需要重新计算并修改大量顶点数据(一段话可能有成百上千个顶点)。对于需要频繁更新或长文本的场景,性能开销巨大。更重要的是,它难以实现平滑的“渐变”,因为你需要为每个顶点计算一个独立的颜色值,逻辑复杂且不直观。
- 原理:直接操作
方案二:使用Rich Text颜色标签
- 原理:在文本中嵌入``这样的标签,TextMeshPro会解析并应用颜色。
- 缺点:这是“段落级”需求的死穴。颜色标签的作用范围是标签之后的文本,直到被下一个颜色标签覆盖。这意味着你必须精确知道每个段落的起止位置,并手动插入大量标签。如果文本是动态生成的(比如从配置表读取),或者需要根据内容长度自动换行,维护这些标签将是一场灾难。它也无法实现一个段落内部的平滑渐变。
方案三:编写自定义Shader实现基于位置的渐变
- 原理:写一个Surface Shader或Unlit Shader,在片段着色器中根据字符在文本框中的归一化位置(如x坐标)来插值两种颜色。
- 缺点:这是“文本框级”渐变,而非“段落级”。其颜色变化完全依赖于字符在矩形文本框内的相对位置。一旦文本换行,第二行开头的颜色会“跳回”渐变起点,导致视觉上的不连续。下图清晰地展示了这个问题:
[假设此处有一张对比图,左侧是理想的段落渐变(颜色随段落流动),右侧是基于文本框UV的渐变(换行处颜色断裂)]- 虽然可以通过传递更多信息(如行索引、字符索引)给Shader来尝试修复,但Shader会变得极其复杂,且动态控制性差。
2.2 胜出方案:贴图UV控制法的核心优势
经过对比,利用TextMeshPro的贴图UV控制方案脱颖而出,因为它完美地解决了上述痛点:
- 性能高效:核心操作发生在文本生成时,即修改字符的UV坐标。这是一次性的CPU计算,渲染时GPU只需采样一张现成的贴图,没有额外的逐顶点颜色计算或复杂的Shader逻辑。
- 实现真正的段落级控制:我们可以根据字符所属的段落索引,为其UV坐标施加一个统一的偏移量。同一个段落的所有字符共享一个UV偏移,从而采样到渐变贴图的同一颜色区域;不同段落的UV偏移不同,从而采样到不同区域。颜色过渡与文本的语义结构紧密绑定。
- 动态与灵活:渐变贴图可以预先制作好,也可以在运行时动态生成(例如,根据游戏内情绪值生成从冷色到暖色的渐变)。通过简单地替换贴图或调整UV映射算法,就能实现各种复杂的色彩效果,而无需改动文本内容或Shader。
- 与TextMeshPro生态无缝集成:TextMeshPro本身就是为了高质量文本渲染而设计的,其底层就是基于贴图(字体图集)渲染。我们的方法只是“借用”了这套成熟机制,为其增加了另一层“内容贴图”,因此稳定性极高,兼容所有TextMeshPro特性(如字距调整、富文本基线等)。
注意:此方案需要你对TextMeshPro的
TMP_TextInfo数据结构有一定的了解,因为它需要遍历并修改文本信息中的字符数据。不过别担心,接下来的实操部分会一步步拆解。
3. 实战准备:构建渐变贴图与基础场景
理论说得再多,不如动手做一遍。我们先从最基础的准备工作开始。
3.1 创建与配置渐变贴图
渐变贴图是我们整个效果的“颜料盘”。它的质量直接决定了最终渐变的平滑程度。
使用Photoshop/GIMP等工具创建:
- 新建一个纹理文件。宽度建议为4的倍数(如256、512),高度为1。为什么高度是1?因为对于水平方向的段落渐变(颜色从左到右变化),我们只需要水平方向有颜色变化,垂直方向是单一的。这样在采样时,V坐标(垂直方向)可以固定为0.5,只变化U坐标(水平方向)。
- 使用渐变工具,填充你想要的色彩序列。例如,从左到右可以是
#3498db(蓝) ->#9b59b6(紫) ->#e74c3c(红)。 - 将文件保存为PNG格式,导入Unity。
在Unity中配置导入设置:
- 将PNG图片导入Unity后,选中它,在Inspector面板中做如下关键设置:
Texture Type: 选择Sprite (2D and UI)。虽然我们不是当Sprite用,但这个类型能确保纹理以非压缩的RGBA格式存在,颜色精度最高。Generate Mip Maps:取消勾选。UI纹理通常不需要Mipmap,关闭可以节省内存并避免远处模糊。Wrap Mode: 选择Clamp。这可以防止在UV坐标轻微超出[0,1]范围时,颜色采样到贴图另一端的意外情况。Filter Mode: 选择Bilinear即可,保证渐变平滑。
- 点击
Apply。
- 将PNG图片导入Unity后,选中它,在Inspector面板中做如下关键设置:
(可选)运行时动态生成贴图:如果你的渐变需要根据游戏状态动态变化(比如血量越低,文字越红),可以在运行时用代码创建
Texture2D并填充像素。// 示例:创建一个256x1的从黑到红的渐变贴图 int width = 256; int height = 1; Texture2D gradientTex = new Texture2D(width, height, TextureFormat.RGBA32, false); gradientTex.wrapMode = TextureWrapMode.Clamp; for (int x = 0; x < width; x++) { float t = (float)x / (width - 1); Color color = Color.Lerp(Color.black, Color.red, t); for (int y = 0; y < height; y++) { gradientTex.SetPixel(x, y, color); } } gradientTex.Apply(); // 必须调用Apply才能使更改生效
3.2 设置TextMeshPro与材质
在Unity场景中创建一个UI Canvas。
右键Canvas -> UI -> Text - TextMeshPro,创建一个TMP文本对象,命名为
TMP_GradientText。我们需要一个专用的材质来使用我们的渐变贴图。最简单的方法是复制一个TextMeshPro自带的材质进行修改。
- 在Project窗口中,找到
TextMeshPro/Resources/Shaders目录,复制TMP_SDF-Mobile.shader(或TMP_SDF.shader,根据项目平台选择)对应的材质球,或者直接复制你刚创建的TMP文本对象上的材质实例。 - 将复制出的新材质命名为
Mat_TMP_Gradient。 - 选中这个新材质,在Inspector中,你会看到它的Shader属性有很多参数。我们需要关注的是
Face部分下的Face Color和Texture。 - 关键操作:将我们准备好的渐变贴图,拖拽到材质
Face部分的Texture槽中。此时,文本可能会变成一片纯色,这是因为默认的Face Color是白色,它会与贴图颜色相乘。为了让贴图颜色完全显示,我们需要将Face Color设置为纯白色(RGB 255,255,255)。同时,确保Vertex Color参数对颜色的影响也设置为纯白(通常来自文本的color属性)。
- 在Project窗口中,找到
将我们刚配置好的
Mat_TMP_Gradient材质,拖拽赋值给场景中TMP_GradientText对象的Font Material属性。
此时,如果你输入文字,可能会发现所有文字都显示了渐变贴图最左侧那一竖条的颜色。这是因为目前所有字符的UV坐标都默认指向贴图的(0,0)位置。接下来,我们就需要通过代码来动态调整这些UV坐标。
4. 核心实现:解析文本与动态映射UV
这是整个方案最核心的代码部分。我们需要编写一个脚本,挂载到TMP文本对象上,它负责在文本更新时,分析段落结构,并为每个字符计算合适的UV坐标。
4.1 脚本结构与初始化
首先创建一个C#脚本,命名为TMPParagraphGradient。
using TMPro; using UnityEngine; [RequireComponent(typeof(TMP_Text))] public class TMPParagraphGradient : MonoBehaviour { [SerializeField] private Texture2D gradientTexture; // 拖入我们准备的渐变贴图 private TMP_Text m_TextComponent; private Material m_MaterialInstance; // 材质实例 void Awake() { m_TextComponent = GetComponent<TMP_Text>(); // 重要:获取材质的实例,避免修改影响到共享材质 m_MaterialInstance = m_TextComponent.fontMaterial; // 将渐变贴图赋给材质实例 if (gradientTexture != null && m_MaterialInstance != null) { m_MaterialInstance.SetTexture("_FaceTex", gradientTexture); // 对于TMP Shader,主贴图属性名可能是_MainTex,需要根据实际Shader查看 // 更通用的方法是:m_MaterialInstance.mainTexture = gradientTexture; } } void OnEnable() { // 订阅文本变更事件,确保文本每次更新(包括通过代码设置text)后都重新应用渐变 TMPro_EventManager.TEXT_CHANGED_EVENT.Add(OnTextChanged); // 初始应用一次 ApplyGradientToParagraphs(); } void OnDisable() { TMPro_EventManager.TEXT_CHANGED_EVENT.Remove(OnTextChanged); } void OnTextChanged(Object obj) { // 确保只有当前对象的文本变化时才触发,避免性能浪费 if (obj == m_TextComponent) ApplyGradientToParagraphs(); } }4.2 段落检测与UV映射算法
现在实现最关键的ApplyGradientToParagraphs方法。这里的逻辑是:遍历所有字符,根据换行符(\n)来划分段落,然后为每个段落分配渐变贴图上的一段UV范围。
private void ApplyGradientToParagraphs() { if (m_TextComponent == null || gradientTexture == null) return; // 强制TextMeshPro生成最新的网格和文本信息 m_TextComponent.ForceMeshUpdate(); TMP_TextInfo textInfo = m_TextComponent.textInfo; if (textInfo.characterCount == 0) return; // 1. 首先,我们需要找出所有段落的起止索引 List<(int start, int length)> paragraphs = new List<(int, int)>(); int currentParagraphStart = 0; string rawText = m_TextComponent.text; for (int i = 0; i <= rawText.Length; i++) { // 如果遇到换行符,或者已经是文本末尾,则一个段落结束 if (i == rawText.Length || rawText[i] == '\n') { int paragraphLength = i - currentParagraphStart; if (paragraphLength > 0) // 忽略空段落(连续换行) { paragraphs.Add((currentParagraphStart, paragraphLength)); } currentParagraphStart = i + 1; // 下一个字符是下一段的开始 } } int totalParagraphs = paragraphs.Count; if (totalParagraphs == 0) return; // 没有有效段落 // 2. 计算每个字符的UV偏移 // 假设我们使用渐变贴图的水平方向,将贴图平均分给每个段落 float uvStepPerParagraph = 1.0f / totalParagraphs; for (int paraIndex = 0; paraIndex < totalParagraphs; paraIndex++) { var (paraStartCharIndex, paraLength) = paragraphs[paraIndex]; // 当前段落对应的UV范围的中心U值(我们让一个段落的字符都采样这个U值附近,实现纯色段落) // 如果想要段落内也有渐变,可以计算基于字符在段落内位置的偏移,这里先实现段落纯色 float paragraphCenterU = (paraIndex + 0.5f) * uvStepPerParagraph; // 取段落UV范围的中心 // 3. 遍历这个段落内的每一个字符(在TMP信息中的索引) for (int charIdxInPara = 0; charIdxInPara < paraLength; charIdxInPara++) { int actualCharIndex = paraStartCharIndex + charIdxInPara; // 确保不越界,并且该字符是有效的(非空格、非不可见字符等) if (actualCharIndex >= textInfo.characterCount) break; TMP_CharacterInfo charInfo = textInfo.characterInfo[actualCharIndex]; if (!charInfo.isVisible) continue; // 跳过空格和不可见字符 // 4. 获取这个字符的网格顶点索引,并修改其UV坐标 int materialIndex = charInfo.materialReferenceIndex; int vertexIndex = charInfo.vertexIndex; // 获取此字符对应的网格顶点数据 Vector2[] uvs0 = textInfo.meshInfo[materialIndex].uvs0; // uvs0通常存储主贴图UV // 一个字符由4个顶点(两个三角形)组成 for (int j = 0; j < 4; j++) { // 修改UV的U坐标,将其映射到我们为这个段落分配的中心U值。 // 注意:这里直接替换了原有的字体图集UV的U值,保留了V值。 // 这意味着字符形状依然来自字体图集,但颜色来自我们渐变贴图的特定位置。 Vector2 originalUV = uvs0[vertexIndex + j]; uvs0[vertexIndex + j] = new Vector2(paragraphCenterU, originalUV.y); } } } // 5. 将所有修改后的网格数据上传回渲染器 for (int i = 0; i < textInfo.meshInfo.Length; i++) { TMP_MeshInfo meshInfo = textInfo.meshInfo[i]; if (meshInfo.mesh != null) { meshInfo.mesh.uv = meshInfo.uvs0; // 仅更新UV,避免不必要的整体网格重建 m_TextComponent.UpdateGeometry(meshInfo.mesh, i); } } }将脚本挂载到TMP_GradientText对象上,并将之前创建的渐变贴图拖拽到脚本的Gradient Texture字段。运行游戏,在TMP组件的Text输入框中输入多段文字(用回车换行),你应该能看到不同段落显示了渐变贴图上不同位置的颜色!
4.3 实现段落内部渐变
上面的代码让每个段落呈现单一颜色。如何实现一个段落内部也从左到右渐变呢?关键在于为段落内的每个字符计算一个基于其在段落中位置的、更精细的U值。
修改ApplyGradientToParagraphs方法中计算paragraphCenterU和遍历字符的部分:
// ... 前面段落检测代码不变 ... for (int paraIndex = 0; paraIndex < totalParagraphs; paraIndex++) { var (paraStartCharIndex, paraLength) = paragraphs[paraIndex]; // 计算这个段落在渐变贴图上占据的U范围 float paragraphUStart = paraIndex * uvStepPerParagraph; float paragraphUEnd = (paraIndex + 1) * uvStepPerParagraph; for (int charIdxInPara = 0; charIdxInPara < paraLength; charIdxInPara++) { int actualCharIndex = paraStartCharIndex + charIdxInPara; if (actualCharIndex >= textInfo.characterCount) break; TMP_CharacterInfo charInfo = textInfo.characterInfo[actualCharIndex]; if (!charInfo.isVisible) continue; // **关键修改:计算字符在段落内的水平位置比例** // 方法1:简单线性分布(不考虑字符宽度不均) // float t_inParagraph = (float)charIdxInPara / (paraLength - 1); // 方法2:基于字符中心点的x坐标在段落总宽度中的比例(更准确) // 我们需要先计算整个段落的总宽度(单位可能是像素或归一化坐标,这里需要根据TMP坐标空间调整) // 这里提供一个简化但有效的思路:使用字符在文本框中的索引进行线性插值。 // 对于大多数等宽字体或效果要求不极端的情况,方法1已经足够。 float t_inParagraph = (paraLength > 1) ? ((float)charIdxInPara / (paraLength - 1)) : 0.5f; // 根据比例,计算该字符在段落UV范围内的具体U值 float charU = Mathf.Lerp(paragraphUStart, paragraphUEnd, t_inParagraph); int materialIndex = charInfo.materialReferenceIndex; int vertexIndex = charInfo.vertexIndex; Vector2[] uvs0 = textInfo.meshInfo[materialIndex].uvs0; for (int j = 0; j < 4; j++) { Vector2 originalUV = uvs0[vertexIndex + j]; uvs0[vertexIndex + j] = new Vector2(charU, originalUV.y); } } } // ... 后面更新网格的代码不变 ...实操心得:计算字符在段落内的位置比例(
t_inParagraph)是效果自然与否的关键。简单的字符索引线性插值在字符宽度差异大时(如‘i’和‘W’)会导致颜色变化不匀。更精确的方法是获取字符几何中心的x坐标,相对于段落第一个和最后一个字符的中心x坐标进行归一化。这需要从charInfo.bottomLeft和charInfo.topRight等顶点位置信息中计算,逻辑更复杂但效果更佳。对于艺术性要求高的项目,建议实现更精确的版本。
5. 高级技巧与性能优化
基础功能实现后,我们还需要关注效果的美观性和运行效率。
5.1 处理富文本标签(如颜色、大小、粗体)
TextMeshPro的富文本标签(,等)会改变字符的材质索引(materialReferenceIndex)。我们的代码在遍历textInfo.meshInfo数组时,已经通过materialIndex区分了不同的材质子网格,因此天然支持富文本。但是,如果你在文本中使用了``标签,它会在顶点颜色上叠加一个颜色,这可能会干扰我们的渐变效果。有几种处理方式:
- 覆盖策略:在我们的Shader或材质中,确保
Face Color和Vertex Color的影响权重设置正确,让贴图颜色成为主导。通常将Face Color设为白色,并降低Vertex Color的权重(如果Shader支持)。 - 预处理文本:在应用渐变前,使用正则表达式移除文本中所有的颜色标签,或者将其替换为不影响我们算法的占位符。
using System.Text.RegularExpressions; private string StripColorTags(string input) { // 移除类似<#FF00FF>或</color>的标签 return Regex.Replace(input, @"<color=[^>]*>|</color>", ""); } // 在设置m_TextComponent.text或ForceMeshUpdate前调用
5.2 动态渐变与动画
我们的渐变贴图是静态的,但我们可以动态修改它来实现动画效果。
滚动渐变:每帧轻微偏移所有字符的UV坐标的U值,可以实现颜色在文字上流动的效果。注意在Update中频繁修改UV对性能有影响,需谨慎使用。
public float scrollSpeed = 0.1f; private float currentUOffset = 0f; void Update() { if (animateGradient) { currentUOffset += Time.deltaTime * scrollSpeed; currentUOffset %= 1.0f; // 循环 ApplyGradientWithOffset(currentUOffset); // 需要实现一个带全局偏移量的应用方法 } }在
ApplyGradientWithOffset中,计算每个字符的最终U值时加上currentUOffset即可。动态生成贴图:如前所述,可以在运行时根据游戏状态(如玩家血量、能量值)生成从A色到B色的渐变贴图,然后重新赋值给材质并调用
ApplyGradientToParagraphs。
5.3 性能优化要点
- 避免每帧调用
ForceMeshUpdate:ForceMeshUpdate()和遍历所有字符修改UV是相对昂贵的操作。务必只在文本内容真正改变时触发(我们已通过TEXT_CHANGED_EVENT实现)。对于静态文本,只需在Start或OnEnable时调用一次。 - 减少网格更新范围:
UpdateGeometry会更新整个子网格。如果只有少量字符变化(如打字机效果),可以尝试只更新受影响字符所在的网格块,但这实现起来非常复杂。对于大多数UI场景,全文更新是可以接受的。 - 对象池与复用:如果场景中存在大量需要此效果的动态文本(如聊天框、日志),考虑使用对象池复用
TMPParagraphGradient组件,避免频繁的AddComponent和Destroy。 - 使用Job System/Burst Compiler(高级):对于极大量文本(如全文小说阅读器),字符遍历和UV计算可以尝试用C# Job System并行化,并使用Burst Compiler加速。但这需要对ECS/Job有深入了解,且TextMeshPro的数据结构不是线程安全的,需要先复制到NativeArray中处理,复杂度陡增。
6. 常见问题排查与调试技巧
在实际使用中,你可能会遇到一些奇怪的现象。这里记录了我踩过的坑和解决方法。
6.1 问题:文字显示为纯白色或黑色,没有渐变。
- 排查步骤:
- 检查材质贴图:首先确认脚本中
gradientTexture是否已赋值,并且材质实例m_MaterialInstance是否成功获取并设置了贴图。可以在Awake或Start中打印m_MaterialInstance.mainTexture的名字来确认。 - 检查Face Color:确保材质
Face Color为纯白(255,255,255,255)。如果它是灰色或黑色,会和贴图颜色相乘导致变暗。 - 检查Shader:确认使用的Shader是支持贴图的。TextMeshPro的SDF Shader默认使用
_FaceTex作为附加贴图。如果你用的Shader不支持,颜色可能来自别处。可以尝试在材质面板直接拖拽贴图,看预览球是否有渐变。 - 检查UV修改代码:在
ApplyGradientToParagraphs方法中,在修改UV的循环里添加Debug.Log,打印出计算出的charU值,看是否在预期的[0, 1]范围内。
- 检查材质贴图:首先确认脚本中
6.2 问题:渐变颜色错乱,或者所有文字颜色一样。
- 排查步骤:
- 段落检测逻辑:检查
paragraphs列表是否正确生成。在计算完段落后,打印出每个段落的(start, length),看是否和你的输入文本匹配。常见的错误是忽略了首段或尾段,或者把空格行也算进去了。 - UV计算逻辑:确认
uvStepPerParagraph的计算是否正确(1.0f / totalParagraphs)。如果totalParagraphs为0或1,会导致除零错误或不正确的步长。 - 字符索引映射:确保
actualCharIndex(在TMP字符信息数组中的索引)与paraStartCharIndex + charIdxInPara的计算是正确的。特别注意:TMP的textInfo.characterInfo数组包含所有字符,包括空格和富文本标签生成的不可见字符。我们的paragraphs是基于原始字符串索引的,而charInfo.isVisible可以帮助我们跳过不可见字符,但索引对应关系需要仔细处理。一个更稳健的方法是直接遍历textInfo.characterInfo数组,同时维护一个段落索引和段落内字符计数器。
- 段落检测逻辑:检查
6.3 问题:文字变得模糊或边缘有锯齿。
- 原因与解决:
- 贴图Filter Mode:检查渐变贴图的导入设置,
Filter Mode设为Bilinear,避免Point模式导致的色块。 - 材质Shader参数:如果是SDF Shader,检查
Gradient Scale和Sharpness参数。不合适的值会影响字体边缘的平滑度。通常使用默认值即可,除非你大幅改变了字体渲染方式。 - UV精度:如果你为每个字符计算了唯一的、非常精细的U值,当贴图分辨率不高时,相邻字符采样的颜色可能差异极小,但在某些显卡或渲染设置下可能出现精度问题。确保渐变贴图有足够的分辨率(如512x1)。
- 贴图Filter Mode:检查渐变贴图的导入设置,
6.4 调试工具:在Scene视图中可视化UV
为了直观地看到UV修改是否生效,可以编写一个简单的Editor脚本,在Scene视图绘制字符的UV信息。
#if UNITY_EDITOR using UnityEditor; [CustomEditor(typeof(TMPParagraphGradient))] public class TMPParagraphGradientEditor : Editor { void OnSceneGUI() { TMPParagraphGradient tg = target as TMPParagraphGradient; if (tg == null || !tg.isActiveAndEnabled) return; TMP_Text textComp = tg.GetComponent<TMP_Text>(); if (textComp == null) return; TMP_TextInfo info = textComp.textInfo; for (int i = 0; i < info.characterCount; i++) { TMP_CharacterInfo charInfo = info.characterInfo[i]; if (!charInfo.isVisible) continue; // 获取字符的世界空间中心点 Vector3 bottomLeft = charInfo.bottomLeft; Vector3 topRight = charInfo.topRight; Vector3 center = (bottomLeft + topRight) / 2; // 假设你有办法获取到计算后的UV值,这里只是示意 // 你可以将UV的U值转换为一个颜色,在Scene视图画出来 // Handles.Label(center, $"U: {calculatedU:F2}"); } } } #endif实现这个方案后,我将其应用在了那个视觉小说项目里。当主角情绪激动时,程序根据情绪值生成一张从蓝到红的渐变贴图,并应用到台词文本上。效果出奇的好,颜色的流动与语音的节奏和台词的含义同步,极大地增强了表现力。最关键的是,这一切的性能开销微乎其微,完全满足移动端的要求。