FEATURED · 精选文章

Unity LUT与TMP富文本颜色:URP后处理与动态上色实战

发布时间 / 2026/9/18 3:36:58
来源 / 创域科博编辑部
栏目 / 资讯中心
Unity LUT与TMP富文本颜色:URP后处理与动态上色实战 做 Unity 的人大概都撞上过这两个几乎同时冒出来的需求美术丢过来一张电影镜头截图说整屏就要这个味道别的地方都别动策划转头在群里喊这个暴击数字要单独亮一下别的字保持原样。前者落到技术上就是颜色查找表也就是常说的 LUT后者落到技术上就是富文本颜色。这两个东西一个作用在整张渲染结果上一个作用在几十个字的顶点色上看起来完全不搭边但在真实项目里它们经常出现在同一帧里——血条被打掉时画面整体压暗同时伤害数字泛红。我在两个中度项目和一个休闲项目里把这两套东西各自落地过一遍踩的坑也基本集中在固定的几个位置。这篇就把我实际用过的方案、参数计算过程和排查经验整理出来给正在做渲染调色或者 UI 文本表现的人一个可以直接抄的参考。不管你刚开始接触 Unity 渲染管线还是已经在 URP 里写后处理下面的内容都能拿去改。1. 两个需求共用同一条主线1.1 硬编码颜色为什么迟早失控先说个我亲眼看过的场面。一个项目做到中期主美的需求从暖色调黄昏改成冷色调阴天结果程序改了三天还没改完。原因很简单环境光的颜色写死在地形材质里UI 血条的渐变写在 Prefab 上粒子系统的起始色写在三份不同的预制体上还有几个角色的技能特效颜色是代码里的常量。改一处漏一处最后只能全局搜索十六进制色值一个一个肉眼比对。颜色查找表的思路就是把这个过程反过来。不再去改每一处颜色值而是把从输入颜色到输出颜色的整个映射关系打包成一份数据资产——一张贴图。所有颜色在渲染的最后一步统一过一遍这张表美术只要在 Photoshop 或者达芬奇里调好这张图整屏的色调就跟着变。程序这边从头到尾只维护一个材质和一个贴图引用。富文本颜色解决的是另一个方向的失控。UI 上的一段文字生命值 35/100如果 35 这个数字要变红最笨的做法是把这段文字拆成三个 Text 组件。拆完之后布局、对齐、换行、多语言翻译全都会出问题。富文本的思路是让颜色信息跟着文本内容走在字符串里直接标记哪一段用哪个颜色布局引擎照常把它当一整段文字排版。两件事表面上一个管全屏一个管一行字本质却是同一件事把颜色从代码里抽出来变成可以独立编辑、独立替换的数据。理解了这一点后面的选型就不会走偏。1.2 该用哪把刀LUT 与富文本的分工边界很多人第一次接触这两个概念时会犯一个错用 LUT 去解决单个 UI 元素变色的需求。比如想让某个按钮在被点击时变成金色就去搞一张只改了金色的 LUT。这在实际项目里是灾难因为 LUT 是全屏后处理你改金色的时候画面里所有接近金色的像素都会被一起改掉连背景的沙地都会变味。反过来用富文本去模拟全屏压色也不现实。TMP 的富文本标签只影响它所在的那个文本组件你没法靠它去影响场景里的模型。我一般用下面这张表来快速判断对比维度颜色查找表LUT富文本颜色作用范围整个相机输出、某个渲染层、某块全屏 UI单个文本组件内部的指定字符数据载体32×32×32 查找贴图或 Texture3D 资产字符串里的标签语法谁来维护美术在图像软件里出图策划或程序填色值即可运行时开销每像素一到两次纹理采样文本重建时的 CPU 解析稳定后接近零典型场景昼夜循环、剧情情绪片段、Boss 战压色、低血量视觉提示伤害数字、稀有度配色、关键词高亮、多语言强调明显不适用只改一个按钮或一个模型全屏色调统一有一条经验值得单独说如果一个颜色变化需要同时影响多个不相关的元素那就是 LUT 的活如果只影响一段文字里的一部分字那就是富文本的活。介于两者之间的比如整个 UI 面板在低血量时泛红更推荐用 CanvasGroup 加一个全屏 Image 遮罩的方式别硬塞进 LUT因为 UI 和场景的调色需求往往不在同一个时机触发。2. 颜色查找表的底层逻辑一张贴图替换整屏像素2.1 3D LUT 到底查的是什么先把概念说清楚。颜色查找表就是一张预先算好的对照表输入一个颜色输出另一个颜色。如果每个通道量化成 32 级那么三个通道组合起来就是 32×32×32 共 32768 种输入颜色每种对应一个输出颜色。这份对照关系在数学上就是三维数组所以叫 3D LUT。为什么不做成 256 级因为 256³ 是 1600 多万条目光是贴图就要占掉几百兆显存完全不划算。32 级意味着每个通道只有 32 个采样点采样点之间靠插值补出来。32 级对于大多数调色需求已经够用因为人眼对连续渐变的敏感度有限而且调色本身通常是平滑的低频变化。只有在暗部需要极精细的曲线时32 级才会暴露出断层这时候升到 33 级或者 65 级或者改用 Texture3D 让硬件做三线性插值。用生活化的说法32 级 LUT 就像一张只有 32 档刻度的色卡你拿任何一个颜色去比对色卡会告诉你在相邻两档之间应该落在哪个位置然后自动帮你调和出中间值。这张色卡本身怎么设计完全取决于美术想要什么风格。顺便提一句中文语境里颜色查找表有两个含义。一种是上面说的 LUT 调色另一种是早期的索引色调色板比如某些像素风游戏用一张 256 色的表来约束整个画面。第二种现在基本只在特定美术风格里用了本文讨论的是第一种。2.2 32×32×32 如何折叠进 1024×32三维数组没法直接存成普通 PNG。最通用的做法是把它切片成一张二维图把蓝色的 32 个取值当作 32 张 32×32 的小图横向排成一行。于是整张图就是 1024 像素宽、32 像素高。这就是 Unity 里 LUT 贴图默认的 1024×32 尺寸的由来。具体坐标对应关系是这样的图中第b个横向切片对应蓝色通道值b/31切片内部横向第r个像素对应红色通道值r/31纵向第g行对应绿色通道值g/31。这个布局有一个容易忽略的细节PNG 的第 0 行在图像顶部而 Unity 贴图的 UV 原点在左下角。如果直接按数组顺序写图绿色通道会整体上下翻转。这个问题在暗部或者色彩对比强的地方会表现为调色后画面发绿/发紫很难第一时间想到是轴向反了。另一个细节是像素中心。采样时不能直接用归一化颜色值当 UV必须做一次缩放偏移把采样点落到像素中心否则会出现半个像素的偏移画面边缘会串色。还有一点要提醒LUT 是定义在 0 到 1 区间的。如果你的画面是 HDR某些像素亮度超过 1直接拿去采样 LUT 会全部被截断到白色亮部细节就会糊成一片。所以调色 Pass 要么放在色调映射之后要么在采样之前先做一次压缩。2.3 Unity 侧的三条落地路径在 Unity 里做 LUT 调色根据你用的渲染管线可以分三条路。第一条是内置管线配 Post Processing Stack v2。这个包里的 Color Grading 效果自带 LUT 支持直接挂一个 Post Process Volume把 LUT 贴图拖进去就行。优点是开箱即用缺点是整个后处理栈比较重而且内置管线现在基本只维护不迭代了。第二条是 URP 的 Volume 系统。URP 的 Color Lookup 组件支持两种输入一种是 1024×32 的二维贴图另一种是 Texture3D。用 Volume 的好处是可以通过 Volume Profile 做混合不同区域、不同优先级叠加非常适合做进入 Boss 区域自动压色这类玩法需求。第三条是自己写 Renderer Feature 加 Blit Pass。什么时候需要自己写我遇到的情况有三种需要把调色放在特定的渲染阶段比如在透明物体之前需要在 LUT 之外叠加自定义逻辑比如同时做噪点、色差或者项目里根本不想引入后处理包。第三种情况在轻度项目里挺常见自己写一个 Pass 总共不到一百行代码还省掉了一个包的依赖。三条路我都用过个人偏好是原型阶段用 URP Volume 快速验证正式版本如果调色逻辑稳定、没有额外效果需求就换成自研 Pass把包的依赖摘掉包体积和启动耗时都能省一点。3. 手写 LUT 调色 PassShader、Renderer Feature 与烘焙流程3.1 采样公式逐行推导假设 LUT 尺寸 N 等于 32贴图宽度 W 等于 N×N 等于 1024高度 H 等于 N 等于 32。输入一个已经归一化到 0 到 1 的颜色 c采样过程分四步。第一步缩放偏移把采样点挪到像素中心c saturate(c) * (N - 1) / N 0.5 / N代入 N 等于 32系数就是 31/32 和 0.5/32。这样 c 的每个分量落在 0.5/32 到 31.5/32 之间正好是 32 个像素的中心位置。第二步算出蓝色通道落在哪两个切片之间fz c.z * N - 0.5 slice0 floor(fz) slice1 min(slice0 1, N - 1) t fz - slice0fz的范围是 0 到 31。slice0是下面那个切片slice1是上面那个t是它们之间的插值权重。注意slice1要钳制到 31否则在蓝色最大值处会越界采样到图像外面。第三步把切片索引换算成横向 UVuv0 float2((c.x slice0) / N, c.y) uv1 float2((c.x slice1) / N, c.y)这里能简化是因为 W 等于 N×N所以切片起始像素除以总宽度等于切片索引除以 N。c.y本身已经是 0 到 1 的 UV因为高度就是 N 个像素。第四步两次采样线性插值c0 tex2D(_LUT, uv0) c1 tex2D(_LUT, uv1) result lerp(c0, c1, t)写成 HLSL 就是下面这样。注意我用的是 URP 的宏如果你的项目还在内置管线把SAMPLE_TEXTURE2D换成tex2D、sampler_LUT换成_LUT就行了。float3 SampleLUT(float3 c) { const float N 32.0; float3 uvw saturate(c) * (N - 1.0) / N 0.5 / N; float fz uvw.z * N - 0.5; float slice0 floor(fz); float slice1 min(slice0 1.0, N - 1.0); float t fz - slice0; float2 uv0 float2((uvw.x slice0) / N, uvw.y); float2 uv1 float2((uvw.x slice1) / N, uvw.y); float3 c0 SAMPLE_TEXTURE2D(_LUT, sampler_LUT, uv0).rgb; float3 c1 SAMPLE_TEXTURE2D(_LUT, sampler_LUT, uv1).rgb; return lerp(c0, c1, t); }一个值得注意的点saturate是必须的。因为纹理采样时如果 UV 超出 0 到 1配合 Clamp 模式会取到边缘像素颜色看起来卡住不动但实际是因为越界了。加saturate至少能让行为可预期。完整的 Fragment 部分只需要把主纹理颜色取出来过一遍 LUT再按强度混合回去half4 frag(Varyings IN) : SV_Target { half4 col SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, IN.uv); float3 graded SampleLUT(col.rgb); col.rgb lerp(col.rgb, graded, _Contribution); return col; }_Contribution这个参数很实用。做低血量压色的时候可以直接用脚本把它从 0 渐变到 1比切换贴图平滑得多也不用准备两张 LUT。3.2 URP Renderer Feature 代码骨架有了 Shader还需要在 URP 的渲染流程里插一个 Blit。写一个 ScriptableRendererFeature核心是三步创建临时 RT、设置材质、执行 Blit。下面是我常用的骨架去掉了一些项目专属逻辑。public class LutGradingFeature : ScriptableRendererFeature { [System.Serializable] public class Settings { public Material material; public RenderPassEvent passEvent RenderPassEvent.BeforeRenderingPostProcessing; } public Settings settings new Settings(); private LutPass _pass; public override void Create() { if (settings.material null) return; _pass new LutPass(settings.material) { renderPassEvent settings.passEvent }; } public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData data) { if (_pass null) return; if (data.cameraData.cameraType CameraType.Preview) return; renderer.EnqueuePass(_pass); } class LutPass : ScriptableRenderPass { private readonly Material _mat; private RTHandle _source; public LutPass(Material mat) { _mat mat; } public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData data) { _source data.cameraData.renderer.cameraColorTargetHandle; } public override void Execute(ScriptableRenderContext ctx, ref RenderingData data) { var cmd CommandBufferPool.Get(LutGrading); Blitter.BlitCameraTexture(cmd, _source, _source, _mat, 0); ctx.ExecuteCommandBuffer(cmd); CommandBufferPool.Release(cmd); } } }这里有几个实际写代码时容易出问题的地方。第一BlitCameraTexture的源和目标传同一个 RTHandle在部分 URP 版本上会触发管线内部的警告或者直接黑屏。稳妥的做法是申请一个临时 RT先 Blit 到临时 RT再从临时 RT 拷回来。第二renderPassEvent的选择。放在BeforeRenderingPostProcessing意味着调色在 Bloom、色调映射之前执行HDR 亮度还没被压缩亮部会溢出。放在AfterRenderingPostProcessing就稳定得多。我一般默认用后者除非有明确理由要放在前面。第三如果场景里有多个相机比如小地图、UI 相机记得在AddRenderPasses里过滤掉不需要调色的相机否则小地图也会跟着变色。注意UI 默认是 Overlay 相机渲染的如果调色 Pass 挂在 Base 相机上UI 不会受影响。想让 UI 一起变色要么把 UI 转到 Base 相机要么单独给 UI 相机加一个 Pass。这个行为很多人第一次做的时候会误以为是 Bug。3.3 LUT 贴图的导入设置四个必须改的开关LUT 贴图本身是数据不是给人看的画面所以导入设置和普通贴图完全不一样。下面这几项不改出来的颜色一定不对。设置项推荐值不改会怎样sRGB (Color Texture)取消勾选采样时被硬件做一次 gamma 转线性颜色整体偏亮、饱和度错位Filter ModeBilinearPoint 会让切片之间没有过渡出现明显色阶断层Wrap ModeClampRepeat 会让边缘采样绕到另一边画面边缘出现奇怪的色块Generate Mip Maps关闭远处的 LUT 采样会用到低级别 mip颜色变得模糊不准CompressionNone 或 RGBA32有损压缩会引入误差在平滑渐变区域表现为噪点带sRGB 这一项是最关键的。Unity 在导入时会根据贴图用途自动判断如果你把 LUT 贴图的 Texture Type 设成 Sprite 或者 Default它很可能默认勾上 sRGB。而在线性色彩空间下渲染时被标记成 sRGB 的贴图在采样时会自动做一次到线性的转换等于把查找表的输入值整体改了。表现出来就是明明 LUT 图看着挺正常挂上去画面就是不对。我一般会把 LUT 贴图的 Texture Type 设成 DefaultsRGB 取消Mip Maps 关掉然后单独建一个文件夹放它们让美术别误改。3.4 把 .cube 转成 Unity 能吃的 strip 图美术通常在 Photoshop 里调好色调后导出成 .cube 文件这是一种通用的 3D LUT 文本格式达芬奇、OBS 之类的软件也认。Unity 默认不吃 .cube需要转成 1024×32 的 PNG。.cube 的文本结构大致是这样TITLE warm_dusk LUT_3D_SIZE 32 DOMAIN_MIN 0.0 0.0 0.0 DOMAIN_MAX 1.0 1.0 1.0 0.000000 0.000000 0.000000 1.000000 0.000000 0.000000 ...关键在最后那部分数据的排列顺序是红色变化最快然后绿色最后蓝色。也就是说第 k 行的索引等于r g*32 b*32*32。解析的时候要按这个顺序 reshape 成(32, 32, 32, 3)得到的是[b][g][r]的三维数组。转换脚本我用 Python 写依赖 numpy 和 Pillow跑一次几秒钟import numpy as np from PIL import Image SIZE 32 def read_cube(path): size SIZE values [] with open(path, r) as f: for line in f: s line.strip() if not s or s.startswith(#): continue if s.startswith(LUT_3D_SIZE): size int(s.split()[1]) continue if s.startswith((TITLE, DOMAIN_MIN, DOMAIN_MAX)): continue parts s.split() if len(parts) 3: values.append([float(v) for v in parts]) arr np.array(values, dtypenp.float32) return arr.reshape((size, size, size, 3)), size def cube_to_strip(cube_path, png_path): cube, size read_cube(cube_path) out np.zeros((size, size * size, 3), dtypenp.float32) for b in range(size): for g in range(size): for r in range(size): # PNG 第 0 行在顶部UV 原点在左下所以纵向翻转 row size - 1 - g col b * size r out[row][col] cube[b][g][r] img Image.fromarray((np.clip(out, 0.0, 1.0) * 255.0).round().astype(np.uint8)) img.save(png_path) if __name__ __main__: cube_to_strip(warm_dusk.cube, warm_dusk_lut.png)这段脚本里最重要的就是那句row size - 1 - g。少了这一句绿色通道就反了画面会往洋红或者绿偏而且很难通过肉眼看 LUT 图看出来——因为那张图本身看起来是正常的。如果你用的是 Texture3D 版本可以在运行时或者编辑器里把 strip 转回三维纹理public static Texture3D BuildTexture3D(Texture2D strip, int size) { var tex new Texture3D(size, size, size, TextureFormat.RGBA32, false) { wrapMode TextureWrapMode.Clamp, filterMode FilterMode.Bilinear }; var src strip.GetPixels(); var dst new Color[size * size * size]; for (int b 0; b size; b) for (int g 0; g size; g) for (int r 0; r size; r) { int srcIndex (b * size r) g * strip.width; int dstIndex r g * size b * size * size; dst[dstIndex] src[srcIndex]; } tex.SetPixels(dst); tex.Apply(); return tex; }Texture3D 的额外好处是 GPU 可以直接做硬件三线性插值不需要手动处理切片之间的插值Shader 里一行SAMPLE_TEXTURE3D就完事采样指令数也更少。代价是它不能在 Unity 里直接导入必须靠脚本生成而且在部分移动端机型上对 3D 纹理的支持不如 2D 稳。我的做法是移动端用 2D stripPC 端如果性能有余量就用 3D。4. 富文本颜色TMP 标签体系与动态上色4.1 常用标签语法与三个容易写错的地方TextMeshPro 的富文本标签是嵌在字符串里的写在文本组件的内容里运行时会自动解析。最常用的颜色相关标签有下面这些标签说明示例color指定字符颜色支持六位或八位十六进制也支持命名色color#FF5533危险/coloralpha只改透明度保留当前颜色alpha#80半透明/alphamark给文字加背景高亮色mark#FFFF0040关键词/markgradient应用一个颜色渐变预设gradientFire燃烧/gradientsprite内联图片可单独指定 color 参数sprite0 color#FF0000bius粗体、斜体、下划线、删除线b强调/b三个最常写错的地方。第一个是颜色位数。color#F00这种三位缩写 TMP 不认必须写满六位。想带透明度就写八位比如#FF553380最后两位是 alpha。很多人写六位但期望它不透明其实 TMP 对六位值的处理是保留当前 alpha如果之前某个标签把 alpha 改低了这里会继承下来。想确保不透明写#FF5533FF。第二个是命名色。colorred这种写法能用但前提是这个名字在 TMP Settings 的 Color Names 列表里注册过。默认注册的是 black、blue、green、orange、purple、red、white、yellow 这几个。写了一个不在列表里的名字TMP 不会报错而是把标签原样显示出来——屏幕上就会直接出现colorcyan这串字符。这个行为在开发期很容易被当成富文本不生效。第三个是标签闭合。TMP 支持嵌套color#FF0000b文字/b/color是合法的但漏掉一个/color会导致后面所有文字都被染色直到文本结束。而且这种错误在视觉上很难判断是漏了闭合还是本来就该那样排查时建议先把文本复制到 TMP 的示例场景里用它的调试视图看。提示TMP 组件的 Rich Text 勾选项默认是开的但如果你接手的是别人做过的 Prefab先确认一下这一项。关掉之后所有标签都会变成明文显示而且不会有任何警告。4.2 渐变、颜色预设与逐字上色纯色标签能解决大部分需求但有些表现必须用渐变比如标题文字的上下渐隐、技能名从红到黄的过渡。TMP 的渐变走的是颜色预设机制先在 Project 里创建一个 TMP Color Gradient 资产右键 Create → TextMeshPro → Color Gradient在里面配好颜色和角度的曲线然后在文本里写gradient资产名。资产名要能在 TMP Settings 的 Color Gradient Presets 列表里找到找不到同样会原样显示标签。另一种不改文本内容的做法是直接给 TMP_Text 组件的 Color Gradient 字段赋值这样整段文字都会应用这个渐变。它的好处是不用动字符串切换预设只是换一个资产引用对多语言文本特别友好——因为需要翻译的字符串里不用夹带任何标记。更细粒度的控制比如每个字依次亮起来的扫描效果就得直接操作顶点色了。TMP 在生成完网格之后会把每个可见字符的顶点信息放在textInfo里可以逐个字符改颜色void ApplyWave(TMP_Text text, float time) { text.ForceMeshUpdate(); var info text.textInfo; for (int i 0; i info.characterCount; i) { var ci info.characterInfo[i]; if (!ci.isVisible) continue; float wave Mathf.Sin(time * 4f i * 0.3f) * 0.5f 0.5f; Color32 c Color.Lerp(Color.white, new Color(1f, 0.4f, 0.1f), wave); int matIndex ci.materialReferenceIndex; int vertIndex ci.vertexIndex; var colors info.meshInfo[matIndex].colors32; colors[vertIndex 0] c; colors[vertIndex 1] c; colors[vertIndex 2] c; colors[vertIndex 3] c; } text.UpdateVertexData(TMP_VertexDataUpdateFlags.Colors32); }这里有三个关键点。ForceMeshUpdate只在文本内容变化后需要调用一次如果每帧都调会非常吃性能正确做法是在文本设置之后调一次之后每帧只改颜色和调用UpdateVertexData。materialReferenceIndex用来定位字符属于哪个材质段因为 TMP 支持一段文字里混用多个字体材质直接假设所有字符都在第 0 个 meshInfo 里会出错。最后colors32是Color32类型用Color赋值会有隐式转换开销。4.3 代码动态上色三条路线的代价给文本上色在代码里有三种做法性能差异很大。第一种是改text.color。这会影响整个文本组件的顶点色不做额外布局计算成本中等。适合整个数字从白变红这类需求。第二种是拼字符串。每次要变色就重新拼一个带标签的字符串然后赋给text.text。这条路的成本最高因为修改 text 会触发完整的文本解析、布局重建和网格生成一段几十字的文本在中低端机上大概要几十到上百微秒。如果每帧都改一个战斗场景里几十个飘字就能把帧率拖下来。如果确实需要动态拼标签至少要用SetText而不是给text属性赋值并且优先用接受StringBuilder的重载来避免字符串拼接产生的临时垃圾private readonly StringBuilder _sb new StringBuilder(64); void ShowDamage(int value, bool isCrit) { _sb.Clear(); if (isCrit) { _sb.Append(color#FF7722b); _sb.Append(value); _sb.Append(/b/color); } else { _sb.Append(value); } _label.SetText(_sb); }第三种是上面说的顶点色操作只改颜色不动文本成本最低代价是代码复杂度上去了而且要考虑多材质段的情况。我的选型习惯是整体变色用第一种局部变色但变化频率低比如只在数值更新时变用第二种高频动态效果扫描、波浪、呼吸用第三种。还有个小技巧做打字机效果时千万不要用Substring去截取字符串那样会把富文本标签从中间截断TMP 解析失败就会显示出一堆尖括号。正确做法是保持完整文本只改maxVisibleCharactersTMP 会自动按可见字符计算标签本身不占位。_label.text color#FF0000警告/color能量不足; _label.maxVisibleCharacters 0; // 之后按时间递增 _label.maxVisibleCharacters Mathf.Min(_label.textInfo.characterCount, shown);注意maxVisibleCharacters依赖textInfo已经生成赋值前最好先ForceMeshUpdate一次否则在某些时机下拿到的characterCount是 0字会一直不显示。5. 常见问题排查速查5.1 LUT 侧偏色、断层、亮部糊掉画面整体偏绿或偏洋红。大概率是 strip 图生成时绿色通道没有做垂直翻转。用上面那段的 Python 脚本重新导出一次注意row size - 1 - g这一行。颜色看着对但饱和度不对像蒙了一层灰。检查 LUT 贴图的 sRGB 勾选状态。取消勾选后重新导入如果还是不对确认项目的色彩空间是 Linear 而不是 Gamma。Gamma 空间下调色本身就不准确很多后处理效果在这个模式下表现都异常。平滑渐变区域出现明显色带。三个可能Filter Mode 被设成了 Point贴图被有损压缩了改成 RGBA32 或者 NoneLUT 只有 32 级而画面本身是 8 bit 输出暗部精度不够考虑升到 33 级或 65 级。亮部变成一片白没有层次。LUT 定义域是 0 到 1而渲染结果是 HDR。解决方法有两个要么把调色 Pass 放到色调映射之后要么在采样前先做一次压缩比如c c / (c 1.0)。前者更省事后者更可控。UI 没跟着变色或者跟着变色了但不想让它变。这取决于 Pass 挂在哪个相机上以及 UI 由哪个相机渲染。URP 里 UI 通常在 Overlay 相机上而调色 Pass 加在 Base 相机上两者互不影响。想让 UI 也变色就得调整相机的渲染层级。5.2 富文本侧标签失效、颜色不对、文字跳动标签直接显示出来了。按顺序检查TMP 组件的 Rich Text 有没有被关掉颜色值是不是写了三位缩写命名色有没有在 TMP Settings 里注册标签有没有配对闭合。某一段文字染到了后面的内容。漏了/color。建议把长文本拆成几段拼接每段自成闭环比一整条长字符串好排查。改完颜色之后文字位置跳了一下。富文本标签本身不占宽度但如果用了size标签改了字号行高就会重算导致整体位移。只改颜色不会影响布局。如果颜色改完也跳了检查是不是有font标签切换了字体资产不同字体的行高可能不一样。飘字数量一多就掉帧。先看是不是每帧都在给text.text赋值。飘字这类高频更新的文本尤其要注意尽量在文本创建时一次性写好标签之后只改颜色或者位置。另外每个飘字都会生成一个独立网格数量控制在二三十个以内比较稳超了就上对象池。多语言下翻译人员把标签删掉了。这是最头疼的情况。我的做法是把颜色标记从文案里抽出来改用 TMP 的style标签加样式表或者干脆在代码里根据关键词做后处理替换让翻译文件里只保留纯文本。这样翻译人员不会碰到尖括号权限也不会被误改。数字用等宽对齐但宽度还是会抖。这跟颜色无关但经常一起出现顺便说一下检查字体资产的数字是不是等宽的TMP 里可以给字体设置 Padding 和字距或者用mspace0.6em标签强制每个字符占用固定宽度。6. 一些取舍经验LUT 的烘焙流程我建议尽早固定下来做成一个能一键跑的命令行脚本。我上一个项目吃过亏美术每次改色调都是在 Photoshop 里手动导出 .cube再手动转 PNG再手动拖进 Unity中间漏一步就出问题而且没法追溯是哪一版。后来改成把 .cube 和转换脚本一起放进版本管理用批处理脚本在 CI 里跑出错的可能性一下就降下来了。另一个体会是LUT 和富文本颜色虽然都归到颜色这个大类里但它们的验收标准完全不同。LUT 的验收必须放到真机上看编辑器里的 Game 视图和手机的色域、亮度表现差得很远有时候在编辑器里看着合适的暖色调到了手机上会偏黄得厉害。富文本颜色的验收反而在编辑器里更准因为 UI 是固定的、和真机差异小但要额外检查一遍所有语言的文本长度颜色标签把字符串撑长之后可能会触发布局换行。最后分享一个小技巧调试 LUT 的时候可以在 Shader 里加一个纯色输出开关把frag的返回直接改成SampleLUT(IN.uv)也就是拿屏幕 UV 当作查找表的输入。这样屏幕上会出现一张完整的 LUT 展开图能一眼看出贴图布局对不对、有没有被压缩、有没有翻转。这个开关在排查上面提到的所有 LUT 问题时都特别有用比反复切相机、对比截图快得多。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻