尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Unity TMP_SDF 分析(二)纹理映射 UV 与 PackUV

Unity TMP_SDF 分析(二)纹理映射 UV 与 PackUV Unity TMP_SDF 分析二纹理映射 UV 与 PackUV适用版本Unity2021.3.23f1c1TextMeshPro3.0.9UGUI1.0.0分析范围项目中的标准Assets/TextMesh Pro/Shaders/TMP_SDF.shader及其关联源码Mobile、SSD、Surface 等变体不直接套用。系列导航第 2/9 篇前置顶点数据与TEXCOORD1的来源下一篇顶点着色器中的屏幕空间缩放上一篇已经说明最终的第二组 UV 是Mesh.uv2 (PackUV(mappingU, mappingV), xScale)本篇分析的是PackUV()调用之前的二维映射坐标(mappingU, mappingV)以及它如何被编码到最终的Mesh.uv2.x。先给出最重要的结论TextureMappingOptions控制的是_FaceTex和_OutlineTex的装饰纹理映射不控制_MainTex中的 SDF 字体图集。SDF Atlas 始终由TEXCOORD0采样。一、同一个uv2字段有两个阶段TMP 中每个可见字符通常由四个顶点组成vertex_BL左下Bottom Leftvertex_TL左上Top Leftvertex_TR右上Top Rightvertex_BR右下Bottom Right在GenerateTextMesh()的映射阶段四个TMP_Vertex.uv2暂时保存二维坐标TMP_Vertex.uv2 (mappingU, mappingV)在后续打包阶段它们会被改写为TMP_Vertex.uv2 (PackUV(mappingU, mappingV), xScale)随后数据复制到TMP_MeshInfo.uvs2再进入Mesh.uv2和 Shader 的TEXCOORD1。因此阅读源码时必须先确认观察点是在“映射前”还是“打包后”。二、映射 UV 在 Shader 中的真正用途TMP_SDF.shader顶点阶段的代码如下节选float2 textureUV UnpackUV(input.texcoord1.x); float2 faceUV TRANSFORM_TEX(textureUV, _FaceTex); float2 outlineUV TRANSFORM_TEX(textureUV, _OutlineTex); output.atlas input.texcoord0; output.textures float4(faceUV, outlineUV);片元阶段采样 SDF Atlas 时使用的是input.atlas节选float c tex2D(_MainTex, input.atlas).a;所以两条路径是独立的TEXCOORD0 └─ _MainTexSDF 字形图集 TEXCOORD1.x └─ UnpackUV() ├─ _FaceTex字面装饰纹理 └─ _OutlineTex描边装饰纹理Character、Line、Paragraph 和 MatchAspect 的视觉差异只有在材质实际使用 Face/Outline Texture 时才容易直接观察到。三、TextureMappingOptions映射模式定义如下publicenumTextureMappingOptions{Character0,Line1,Paragraph2,MatchAspect3}TMP 分别保存水平映射m_horizontalMapping和垂直映射m_verticalMapping。前者计算临时uv2.x后者计算临时uv2.yMatchAspect则根据另一轴反推当前轴。四、水平映射1.Character每个字符独立映射水平方向直接把字符左侧设为0、右侧设为1节选caseTextureMappingOptions.Character:characterInfos[i].vertex_BL.uv2.x0;characterInfos[i].vertex_TL.uv2.x0;characterInfos[i].vertex_TR.uv2.x1;characterInfos[i].vertex_BR.uv2.x1;break;每个字符都会单独铺一遍横向纹理。Character 模式初始使用[0, 1]其他模式的坐标可以超出该范围。2.Line以当前行为基准普通对齐下顶点 X 坐标会相对于当前行的lineExtents计算节选uv.x(vertex.position.x-lineExtents.min.x)/(lineExtents.max.x-lineExtents.min.x)uvOffset;同一行内的字符共享一套横向范围因此装饰纹理通常在每一行内铺一遍。源码对TextAlignmentOptions.Justified有特殊处理此时 Line 模式改用整个文本对象的m_meshExtents并加入justificationOffset.x节选uv.x(vertex.position.xjustificationOffset.x-m_meshExtents.min.x)/(m_meshExtents.max.x-m_meshExtents.min.x)uvOffset;Line 模式在普通对齐下使用lineExtents在 Justified 对齐下使用m_meshExtents。3.Paragraph以整个文本对象为基准Paragraph 模式使用m_meshExtents节选uv.x(vertex.position.xjustificationOffset.x-m_meshExtents.min.x)/(m_meshExtents.max.x-m_meshExtents.min.x)uvOffset;这里的 “Paragraph” 是 TMP 枚举名称实际基准是当前文本对象生成内容的整体网格范围包括空行在内的内容共享同一套整体映射坐标。4.MatchAspect先算 V再反推 U当水平轴设置为MatchAspect时TMP 先按垂直轴的 Character、Line 或 Paragraph 模式得到uv2.y再用字符四边形的aspectRatio反推 X节选floatxDelta(1-((characterInfos[i].vertex_BL.uv2.ycharacterInfos[i].vertex_TL.uv2.y)*characterInfos[i].aspectRatio))/2;characterInfos[i].vertex_BL.uv2.xcharacterInfos[i].vertex_BL.uv2.y*characterInfos[i].aspectRatioxDeltauvOffset;characterInfos[i].vertex_TL.uv2.xcharacterInfos[i].vertex_BL.uv2.x;characterInfos[i].vertex_TR.uv2.xcharacterInfos[i].vertex_TL.uv2.y*characterInfos[i].aspectRatioxDeltauvOffset;characterInfos[i].vertex_BR.uv2.xcharacterInfos[i].vertex_TR.uv2.x;xDelta用于把按宽高比计算出的横向区间居中aspectRatio换算负责保持两轴比例。五、垂直映射水平映射完成后TMP 会依据m_verticalMapping生成临时uv2.y。1.CharactercaseTextureMappingOptions.Character:characterInfos[i].vertex_BL.uv2.y0;characterInfos[i].vertex_TL.uv2.y1;characterInfos[i].vertex_TR.uv2.y1;characterInfos[i].vertex_BR.uv2.y0;break;每个字符从底部0映射到顶部1。2.Line垂直 Line 模式使用当前行的descender与ascender节选uv.y(vertex.position.y-lineInfo.descender)/(lineInfo.ascender-lineInfo.descender);垂直 Line 模式直接使用lineInfo.descender / ascender作为范围。3.Paragraph垂直 Paragraph 模式使用整个文本的m_meshExtents节选uv.y(vertex.position.y-m_meshExtents.min.y)/(m_meshExtents.max.y-m_meshExtents.min.y);4.MatchAspect根据 U 反推 V当垂直轴设置为MatchAspect时水平映射已经给出uv2.x。TMP 通过aspectRatio反推 Y节选floatyDelta(1-((characterInfos[i].vertex_BL.uv2.xcharacterInfos[i].vertex_TR.uv2.x)/characterInfos[i].aspectRatio))/2;characterInfos[i].vertex_BL.uv2.yyDeltacharacterInfos[i].vertex_BL.uv2.x/characterInfos[i].aspectRatio;characterInfos[i].vertex_TL.uv2.yyDeltacharacterInfos[i].vertex_TR.uv2.x/characterInfos[i].aspectRatio;characterInfos[i].vertex_BR.uv2.ycharacterInfos[i].vertex_BL.uv2.y;characterInfos[i].vertex_TR.uv2.ycharacterInfos[i].vertex_TL.uv2.y;水平与垂直映射不支持同时设为MatchAspect。源码在这种组合下会输出ERROR: Cannot Match both Vertical Horizontal.原因很直接两个轴都要求依赖另一轴就没有先计算的基准轴。六、lineExtents、m_meshExtents与行偏移1.lineExtentslineExtents描述当前行的范围X从该行首字符的左侧范围到最后一个可见字符的右侧范围。Y由行的descender到ascender。它服务于逐行映射。2.m_meshExtentsm_meshExtents在可见字符生成过程中累计整个文本对象的最小、最大顶点坐标用于 Paragraph以及 Justified 条件下的特殊 Line 映射。范围粒度主要用途lineExtents/lineInfo当前行Line 映射m_meshExtents当前文本对象整体Paragraph 映射、Justified Line 特例3.m_uvLineOffset每个字符进入映射逻辑时TMP 会按当前行号计算横向偏移节选floatuvOffset(m_uvLineOffset*currentLine)%1;这个偏移用于水平 Line、Paragraph 和水平 MatchAspect 等计算使不同文本行的装饰纹理起点产生错位。当前源码的 Character 模式保持固定的字符内映射起点。七、打包前移除公共整数部分二维映射完成后TMP 取左下与右上顶点并从同一字符的两侧坐标中减去以左下角为基准的整数部分节选floatx0characterInfos[i].vertex_BL.uv2.x;floaty0characterInfos[i].vertex_BL.uv2.y;floatx1characterInfos[i].vertex_TR.uv2.x;floaty1characterInfos[i].vertex_TR.uv2.y;floatdx(int)x0;floatdy(int)y0;x0-dx;x1-dx;y0-dy;y1-dy;这段代码移除同一字符四个顶点共享的整数平铺偏移同时保留字符内部的坐标跨度处理后的右侧或顶部坐标仍可大于1。例如x0 1.25, x1 2.10 dx 1 结果x0 0.25, x1 1.10右侧坐标仍然大于1从而保留了跨越纹理边界的映射跨度。C# 的(int)对负数向零截断。八、PackUV()的数值编码TMP 3.0.9 的实现如下节选protectedfloatPackUV(floatx,floaty){doublex0(int)(x*511);doubley0(int)(y*511);return(float)((x0*4096)y0);}编码过程是qx (int)(x * 511) qy (int)(y * 511) packed qx * 4096 qy编码要点如下X、Y 都先按511缩放并截断获得量化整数。X 的槽间距是4096即2^12Y 量化值位于该槽内。返回值类型虽然是float但它承载的是上述整数式编码结果。两个坐标都采用约 9 位量化精度Y 被放在宽度为4096的 X 槽内。九、Shader 的UnpackUV()与量化误差TMPro.cginc的解包实现为float2 UnpackUV(float uv) { float2 output; output.x floor(uv / 4096); output.y uv - 4096 * output.x; return output * 0.001953125; }其中0.001953125 1 / 512若打包值为qx * 4096 qy解包结果就是(qx / 512, qy / 512)但打包时使用的是511qx (int)(x * 511) qy (int)(y * 511)Pack 与 Unpack 包含固定量化误差。例如输入1.0量化值为511解包后为511 / 512。解包值是原始映射坐标的有限精度近似。十、最终数据流字符四边形位置 ├─ m_horizontalMapping → 临时 mappingU └─ m_verticalMapping → 临时 mappingV │ ├─ Character / Line / Paragraph └─ MatchAspect由另一轴和 aspectRatio 反推 │ ▼ 去除左下角的公共整数部分 │ ▼ PackUV(mappingU, mappingV) │ ▼ Mesh.uv2.x / TEXCOORD1.x │ ▼ UnpackUV() ┌───────┴────────┐ ▼ ▼ _FaceTex _OutlineTex 与此同时 Mesh.uv2.y / TEXCOORD1.y xScale 另一条独立路径 Mesh.uv / TEXCOORD0 → _MainTex SDF Atlas十一、模式对照表模式水平基准垂直基准说明Character单个字符左右边单个字符上下边每个字符独立铺图Line当前行范围Justified 时有特殊分支当前行 descender/ascender逐行铺图Paragraph整个文本对象的m_meshExtents整个文本对象的m_meshExtents以完整文本对象为范围MatchAspect根据 V 和aspectRatio反推 U根据 U 和aspectRatio反推 V两轴互斥选择最终结论映射阶段的TMP_Vertex.uv2是 Face/Outline 装饰纹理的二维坐标打包后最终Mesh.uv2.x保存这两个坐标的编码值Mesh.uv2.y保存xScale_MainTex的 SDF Atlas UV 位于Mesh.uv。十二、源码索引Scripts/Runtime/TMPro_UGUI_Private.cs水平、垂直映射MatchAspect 和最终 UV2 打包。Scripts/Runtime/TMPro_Private.cs3D 文本对应的映射与打包流程。Scripts/Runtime/TMP_Text.csTextureMappingOptions与PackUV()。Assets/TextMesh Pro/Shaders/TMP_SDF.shader映射 UV 与 Atlas UV 的实际用途。Assets/TextMesh Pro/Shaders/TMPro.cgincUnpackUV()。
返回列表