解决UGUI的图集导致Shader采样时UV错误的问题

news2024/11/25 18:31:39

大家好,我是阿赵。
在我们用UGUI的时候,很多时候需要通过在UI上面挂材质球,写Shader,来实现一些特殊的效果。
这里句一个很简单的例子,只为说明问题。

一、简单例子说明

这个例子是这样的,我想在某个Image上面加一个渐变遮罩,只显示角色的头像。
这里我准备了一张角色贴图,然后根据角色头像的位置画了个遮罩。
在这里插入图片描述
在这里插入图片描述

接下来的实现很简单,通过图片的UV采样遮罩贴图,然后和原来的图片叠加透明度,之后就得到了这样的效果:
在这里插入图片描述

这个例子的shader是这样的:

Shader "azhao/UIAlphaMask"
{
	Properties
	{
		[PerRendererData] _MainTex("Sprite Texture", 2D) = "white" {}
		_Color("Tint", Color) = (1,1,1,1)

		_StencilComp("Stencil Comparison", Float) = 8
		_Stencil("Stencil ID", Float) = 0
		_StencilOp("Stencil Operation", Float) = 0
		_StencilWriteMask("Stencil Write Mask", Float) = 255
		_StencilReadMask("Stencil Read Mask", Float) = 255

		_ColorMask("Color Mask", Float) = 15
		[Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip("Use Alpha Clip", Float) = 0
		_MaskMap("MaskMap",2D) = "white"{}
	}

		SubShader
	{
		Tags
		{
			"Queue" = "Transparent"
			"IgnoreProjector" = "True"
			"RenderType" = "Transparent"
			"PreviewType" = "Plane"
			"CanUseSpriteAtlas" = "True"
		}

		Stencil
		{
			Ref[_Stencil]
			Comp[_StencilComp]
			Pass[_StencilOp]
			ReadMask[_StencilReadMask]
			WriteMask[_StencilWriteMask]
		}

		Cull Off
		Lighting Off
		ZWrite Off
		ZTest[unity_GUIZTestMode]
		Blend SrcAlpha OneMinusSrcAlpha
		ColorMask[_ColorMask]
		Pass
		{
			Name "Default"
		CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#pragma target 2.0

			#include "UnityCG.cginc"
			#include "UnityUI.cginc"

			#pragma multi_compile __ UNITY_UI_ALPHACLIP

			struct appdata_t
			{
				float4 vertex   : POSITION;
				float4 color    : COLOR;
				float2 uv : TEXCOORD0;
			};

			struct v2f
			{
				float4 vertex   : SV_POSITION;
				fixed4 color : COLOR;
				half2 uv  : TEXCOORD0;
				float4 worldPosition : TEXCOORD1;
			};

			fixed4 _Color;
			fixed4 _TextureSampleAdd;
			float4 _ClipRect;
			sampler2D _MaskMap;

			v2f vert(appdata_t i)
			{
				v2f o;
				o.worldPosition = i.vertex;
				o.vertex = UnityObjectToClipPos(o.worldPosition);

				o.uv = i.uv;

				#ifdef UNITY_HALF_TEXEL_OFFSET
				o.vertex.xy += (_ScreenParams.zw - 1.0) * float2(-1,1) * o.vertex.w;
				#endif

				o.color = i.color * _Color;
				return o;

			}

			sampler2D _MainTex;

			half4 frag(v2f i) : SV_Target
			{
				half4 color = (tex2D(_MainTex, i.uv) + _TextureSampleAdd) * i.color;

				half4 maskCol = tex2D(_MaskMap, i.uv);
				color.a = maskCol.r*color.a;
				return color;
			}
		ENDCG
		}
	}
}

二、打图集之后,遇到的问题。

由于这个图片是用在UI的image上的,所以它的类型是Sprite。然后一般来说,使用UGUI的Sprite都会打成图集来使用。
于是我对这张图片设置一下PackingTag,让它成为一张图集的一部分。
在这里插入图片描述

然后把图片打包成AssetBundle,再加载使用,这个时候,发现刚才显示很正常的例子,变得不正常了。
在这里插入图片描述

三、分析问题

我修改一下shader,单独把这种图片的颜色显示出来。

half4 frag(v2f i) : SV_Target
{
	half4 color = (tex2D(_MainTex, i.uv) + _TextureSampleAdd) * i.color;

	half4 maskCol = tex2D(_MaskMap, i.uv);
	color.a = maskCol.r*color.a;
	return maskCol;
}

如果没有打包AssetBundle加载的时候,我们的遮罩图的位置应该是这样的。
在这里插入图片描述

但如果通过AssetBundle加载之后,遮罩的位置会变成这样:
在这里插入图片描述

比较容易就能看出来,出现问题的原因是,采样遮罩图的UV似乎变得不正确了。
由于刚才用于例子的两张图都太居中,不利于分析问题,于是我把角色图和遮罩图都修改了一下位置,变成这样:
在这里插入图片描述
在这里插入图片描述

如果正常显示,现在的效果应该是这样的:
在这里插入图片描述

由于Unity的图集为了能适配硬件的图片压缩,会自动变成2的次幂,所以会对原图做一定的修改,先去掉空白的地方,然后再补到最接近的2的次幂的大小。

用工具打开AssetBundle,可以看到,这张图片去除了空白之后,实际sprite的像素是256*577
在这里插入图片描述

而由于577已经超过了512,所以下一级是1024,于是这个贴图的完整尺寸是256*1024
在这里插入图片描述

所以,实际上这张图片在AssetBundle里面是长这样的:
在这里插入图片描述

这张图在Unity里面打开SpritePacker可以看到
在这里插入图片描述

然后,把场景渲染模式改成渲染+线框
在这里插入图片描述

可以看到,虽然原始的Image的范围是在红框这么大,但实际上,生成的网格只有绿框的范围。
在这里插入图片描述

网上很多文章在介绍到这一步的时候,为了说明原理,就开始看UGUI的源码了,不过我认为,源码估计很多人都不想去看,或者说看不太懂。所以我我直接说结论。
一般图集的优化,包括我以前自己写的引擎,对于图集的实现方式,都是这样,在父级设置一个和原图大小一样的范围,然后在里面,只在有效像素范围内生成网格模型,这个网格模型的UV就不能是0-1,而是有效像素实际占有原图的比例。
这个UV比例,Unity是提供了方法给我们获取的

Vector4 outerUV = UnityEngine.Sprites.DataUtility.GetOuterUV(sprite);

这个例子,把outerUV打印出来,发现值是:
outerUV:(0,0,1,0.56)
这是怎么理解呢?
在这里插入图片描述

由于图片本身是256宽,而有效像素也是256,所以uv的x坐标是完全使用了这张贴图的整个宽度,所以取值范围是0-1
由于图片高度是1024,而有效的像素范围只到577,所以只用到了图片的0.56。
于是,生成的小网格的UV坐标,实际上是这样的一个取值,宽度0-1,高度只取到0-0.56;

那是不是我们只需要重新计算一下UV坐标,就能解决问题呢?
其实并不是的,由于实际生成的网格只占原图的一部分,那么网格的四条边,离整个Image的四条边分别是多远呢?Unity同样提供了方法给我们查询

Vector4 padding = UnityEngine.Sprites.DataUtility.GetPadding(sp);

把padding打印出来:
padding:(388,9,54,28)
这里padding的含义是如下图所示的:
在这里插入图片描述

通过打印sprite.rect,可以得到这个sprite在没有去掉空白之前的像素是698*698
所以通过padding,我们就可以算出这个小网格的4条边离Image的四条边的距离在UV上的比例

Vector4 customUV = new Vector4();
customUV.x = padding.x/ sp.rect.width;
customUV.y = padding.y / sp.rect.height;
customUV.z = padding.z / sp.rect.width;
customUV.w = padding.w / sp.rect.height;

由于上面的例子只有一张图片,不够复杂,所以下面把图集再扩充一下,打多几张图进去:
在这里插入图片描述

我们挑了其中一个图作为渲染,打印出的outerUV是:
outerUV:(0.378,0.282,0.5,0.56)
实际上,在整张图集里面,它的UV范围会是这样的:
在这里插入图片描述

虽然打多了很多张图进去,但padding是不会变的,sp.rect也是不会变的。

四、解决问题

通过上面的一堆分析,我们知道了这几个事情:
1.打成图集之后,生成的网格是只有在有效像素范围的,所以UV并不是0-1,而是截取一小部分
2.我们要算出离边缘的UV比例,然后把采样的范围从网格内部延伸到整个原始图片的大小。

1、重新映射UV范围:

实际上我们要做的事情,是从原来的0-1的uv范围,截取出一个当前小网格对应的部分
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

比如这个例子,原始的UV是0-1,但小网格里面的UV范围
x轴是0.378-0.5
y轴是0.282-0.56
为了能让这个小范围映射回0-1,在shader里面可以这样处理
先写一个Remap方法

float Remap(float val,float minOld,float maxOld,float minNew,float maxNew)
{
	return (minNew + (val - minOld) * (maxNew - minNew) / (maxOld - minOld));
}

然后

float minNew = 0;
float maxNew = 1;
float minOld = _OuterUV.x;
float maxOld = _OuterUV.z;
float tx = Remap(i.uv.x, minOld, maxOld, minNew, maxNew);
float _minOld = _OuterUV.x;
float _maxOld = _OuterUV.z;
float ty = Remap(i.uv.y, minOld, maxOld, minNew, maxNew);

这样,UV就从小网格映射回大Image范围了。

2、计算Padding

要计算边缘的范围,实际上就是把刚才重新映射的UV,再添加一个Padding的开始和结束位置的偏移,具体是这样算的:

tx = _Padding.x + (tx * (1 - _Padding.z - _Padding.x));
ty = _Padding.y + (ty * (1 - _Padding.w - _Padding.y));

通过这两步计算之后,UV坐标的映射和偏移都做完了,为了让大家看得更清楚,我准备了一张数字网格图(本来是打算用来做另外一个例子的)。计算后,通过对同一张图进行采样,背后比较暗的是原图,前面比较亮的是通过小网格反过来推算UV的结果。可以看出来,两张图的接缝处是完全重叠的,没有一点点偏差。
在这里插入图片描述

到了这一步,其实问题已经解决完成了。把计算出来的新UV采样遮罩图,然后叠加颜色,就能得出正确的结果。下面是裁剪后的图和原遮罩图的对比,可以看出,裁剪的范围刚刚好是在原图的白色区域。
在这里插入图片描述

五、源码

1、C#端获取outerUV和padding的代码

Sprite sp = imgAB.LoadAsset<Sprite>("longzhu");
image.sprite = sp;
Vector4 outerUV = UnityEngine.Sprites.DataUtility.GetOuterUV(sp);
Vector4 padding = UnityEngine.Sprites.DataUtility.GetPadding(sp);
Vector4 customUV = new Vector4();

customUV.x = padding.x/ sp.rect.width;
customUV.y = padding.y / sp.rect.height;
customUV.z = padding.z / sp.rect.width;
customUV.w = padding.w / sp.rect.height;

image.material.SetVector("_Padding", customUV);
image.material.SetVector("_OuterUV", outerUV);

2、完整Shader

Shader "azhao/UIAlphaMask"
{
	Properties
	{
		[PerRendererData] _MainTex("Sprite Texture", 2D) = "white" {}
		_Color("Tint", Color) = (1,1,1,1)

		_StencilComp("Stencil Comparison", Float) = 8
		_Stencil("Stencil ID", Float) = 0
		_StencilOp("Stencil Operation", Float) = 0
		_StencilWriteMask("Stencil Write Mask", Float) = 255
		_StencilReadMask("Stencil Read Mask", Float) = 255

		_ColorMask("Color Mask", Float) = 15
		_Padding("Padding",Vector) = (0,0,0,0)
		_OuterUV("OuterUV",Vector) = (0,0,1,1)
		[Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip("Use Alpha Clip", Float) = 0
		_MaskMap("MaskMap",2D) = "white"{}
	}

		SubShader
	{
		Tags
		{
			"Queue" = "Transparent"
			"IgnoreProjector" = "True"
			"RenderType" = "Transparent"
			"PreviewType" = "Plane"
			"CanUseSpriteAtlas" = "True"
		}

		Stencil
		{
			Ref[_Stencil]
			Comp[_StencilComp]
			Pass[_StencilOp]
			ReadMask[_StencilReadMask]
			WriteMask[_StencilWriteMask]
		}

		Cull Off
		Lighting Off
		ZWrite Off
		ZTest[unity_GUIZTestMode]
		Blend SrcAlpha OneMinusSrcAlpha
		ColorMask[_ColorMask]
		Pass
		{
			Name "Default"
		CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#pragma target 2.0

			#include "UnityCG.cginc"
			#include "UnityUI.cginc"

			#pragma multi_compile __ UNITY_UI_ALPHACLIP

			struct appdata_t
			{
				float4 vertex   : POSITION;
				float4 color    : COLOR;
				float2 uv : TEXCOORD0;
			};

			struct v2f
			{
				float4 vertex   : SV_POSITION;
				fixed4 color : COLOR;
				half2 uv  : TEXCOORD0;
				float4 worldPosition : TEXCOORD1;
			};

			fixed4 _Color;
			fixed4 _TextureSampleAdd;
			float4 _ClipRect;
			sampler2D _MaskMap;
			float4 _Padding;
			float4 _OuterUV;

			v2f vert(appdata_t i)
			{
				v2f o;
				o.worldPosition = i.vertex;
				o.vertex = UnityObjectToClipPos(o.worldPosition);

				o.uv = i.uv;

				#ifdef UNITY_HALF_TEXEL_OFFSET
				o.vertex.xy += (_ScreenParams.zw - 1.0) * float2(-1,1) * o.vertex.w;
				#endif

				o.color = i.color * _Color;
				return o;

			}

			sampler2D _MainTex;

			float Remap(float val,float minOld,float maxOld,float minNew,float maxNew)
			{
				return (minNew + (val - minOld) * (maxNew - minNew) / (maxOld - minOld));
			}

			half4 frag(v2f i) : SV_Target
			{
				half4 color = (tex2D(_MainTex, i.uv) + _TextureSampleAdd) * i.color;
				float minNew = 0;
				float maxNew = 1;
				float minOld = _OuterUV.x;
				float maxOld = _OuterUV.z;
				float tx = Remap(i.uv.x, minOld, maxOld, minNew, maxNew);
				tx = _Padding.x + (tx * (1 - _Padding.z - _Padding.x));
				float _minOld = _OuterUV.x;
				float _maxOld = _OuterUV.z;
				minOld = _OuterUV.y;
				maxOld = _OuterUV.w;
				float ty = Remap(i.uv.y, minOld, maxOld, minNew, maxNew);
				ty = _Padding.y + (ty * (1 - _Padding.w - _Padding.y));
				float2 maskUV = float2(tx, ty);
				half4 maskCol = tex2D(_MaskMap, maskUV);
				color.a = maskCol.r*color.a;
				return color;
			}
		ENDCG
		}
	}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/647758.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Python模块openpyxl 操作Excel文件

简介 openpyxl是一个用于读取和编写Excel 2010 xlsx/xlsm/xltx/xltm文件的Python库。openpyxl以Python语言和MIT许可证发布。 openpyxl可以处理Excel文件中的绝大多数内容&#xff0c;包括图表、图像和公式。它可以处理大量数据&#xff0c;支持Pandas和NumPy库导入和导出数据。…

chatgpt赋能python:Python本地安装库:一个简单易懂的指南

Python本地安装库&#xff1a;一个简单易懂的指南 Python是一种高级的编程语言&#xff0c;它拥有庞大的社区支持和无数的第三方库。如果你在使用Python时需要一些额外的功能&#xff0c;那么你可能需要安装一些库。本文将介绍如何在本地安装库&#xff0c;以及一些需要注意的…

chatgpt赋能python:如何更新Python库?Python更新库完全指南

如何更新Python库&#xff1f;Python更新库完全指南 Python作为一种最受欢迎的编程语言&#xff0c;其库和工具的数量是惊人的。这些库是Python生态系统的重要组成部分&#xff0c;以便帮助开发人员解决不同类型的问题。然而&#xff0c;这些库会更新&#xff0c;开发人员需要…

什么是椭圆曲线上的加法

椭圆曲线图形示例 注意&#xff0c;椭圆曲线随着你参数的不同&#xff0c;有不同的形态&#xff0c;这里仅是一种示例&#xff0c;详细的关于椭圆曲线的知识可以后附扩展知识连接 椭圆曲线上的加法 椭圆曲线上的加法不是我们通常意义上的数值加法&#xff0c;而是一种特殊的几…

干翻Mybatis源码系列之第十篇:Mybatis Plugins基本概念

给自己的每日一句 不从恶人的计谋&#xff0c;不站罪人的道路&#xff0c;不坐亵慢人的座位&#xff0c;惟喜爱耶和华的律法&#xff0c;昼夜思想&#xff0c;这人便为有福&#xff01;他要像一棵树栽在溪水旁&#xff0c;按时候结果子&#xff0c;叶子也不枯干。凡他所做的尽…

Oracle中的行列互转———pivot、unpivot函数用法

一、需求说明 项目开发过程中涉及到oracle数据库的数据操作&#xff1b;但是需要将数据进行列的互转&#xff0c;通过查阅资料可知在oracle中有三种方式可以实现行列互转&#xff1a; ①使用decode 函数&#xff1b; ②使用case when 函数&#xff1b; ③使用pivot函数&…

Linux之设置主机名

目录 Linux之设置主机名 查看主机名 语法格式 案例 修改主机名 语法格式 案例 --- 修改静态主机名为joker 配置静态解析 为Linux主机指派域名解析 Linux之设置主机名 查看主机名 语法格式 hostnamectl [status] [--static|--transient|--pretty] 解析&#xff1a; s…

极致呈现系列之:Echarts地图的浩瀚视野(一)

目录 Echarts中的地图组件地图组件初体验下载地图数据准备Echarts的基本结构导入地图数据并注册展示地图数据结合visualMap展示地图数据 Echarts中的地图组件 Echarts中的地图组件是一种用于展示地理数据的可视化组件。它可以显示全国、各省市和各城市的地图&#xff0c;并支持…

整形在内存中的存储-原码补码反码的理解与应用

目录 一、概论 1.1 C语言中基本的数据类型 1.2 类型的基本归类 二、整形在内存中的存储 2.1 原码、反码、补码 2.2 存储补码和大小端存储 三、计算各基本数据类型的范围计算原理 3.1 有符号类型的整形范围 3.2 无符号类型的整形范围 3.3 例题 一、概论 C语言提供了非常…

【Java基础学习打卡07】Java语言概述

目录 引言一、Java语言1.Java语言简介2.Java语言优势3.Java能做什么&#xff1f; 二、Java之父三、Java简史1.Java版本时间线2.Java发展重要节点 总结 引言 一、Java语言 1.Java语言简介 Java语言是一种以面向对象为基础的高级编程语言。吸收了C语言的各种优点&#xff0c;又…

【IMX6ULL驱动开发学习】06.APP与驱动程序传输数据+自动创建设备节点(hello驱动)

一、APP与驱动之间传输数据 /*驱动从APP获取数据*/ unsigned long copy_from_user(void *to, const void *from, unsigned long n)/*驱动传输数据到APP*/ unsigned long copy_to_user(void *to, const void *from, unsigned long n)二、使用copy_to_user、copy_from_user在AP…

32908字长文理解Large CV Model:Segment Anything

作者&#xff1a;猛码Memmat 目录 Abstract1. IntroductionTaskModelData engineDatasetResponsible AIExperimentsRelease 2. Segment Anything TaskTaskPre-trainingZero-shot transferRelated tasksDiscussion 3. Segment Anything ModelImage encoderPrompt encoderMask de…

十个实用MySQL函数

函数 0. 显示当前时间 命令&#xff1a;。 作用: 显示当前时间。 应用场景: 创建时间&#xff0c;修改时间等默认值。 例子&#xff1a; 1. 字符长度 命令&#xff1a;。 作用: 显示指定字符长度。 应用场景: 查看字符长度时。 例子&#xff1a; 2. 日期格式化 命令…

【群智能算法改进】一种改进的白鲸优化算法 改进白鲸优化算法 改进后的EBWO[1]算法【Matlab代码#40】

文章目录 【获取资源请见文章第5节&#xff1a;资源获取】1. 原始BWO算法2. 改进的白鲸优化算法EBWO2.1 Logistic映射2.2 透镜成像折射方向学习 3. 部分代码展示4. 仿真结果展示5. 资源获取 【获取资源请见文章第5节&#xff1a;资源获取】 1. 原始BWO算法 白鲸优化算法 (BWO&…

LeetCode —— 206. 反转链表

LeetCode —— 206. 反转链表 一、题目描述&#xff1a; 给你单链表的头节点 head &#xff0c;请你反转链表&#xff0c;并返回反转后的链表。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,4,5] 输出&#xff1a;[5,4,3,2,1] 示例 2&#xff1a; 输入&#xff1a;head …

【Shiro】第二章 Shiro概述

目录 1、Shiro简介 2、核心组件 1、Shiro简介 【1】什么是Shiro? Shiro是apache旗下一个开源框架&#xff0c;它将软件系统的安全认证相关的功能抽取出来&#xff0c;实现用户身份认证&#xff0c;权限授权、加密、会话管理等功能&#xff0c;组成了一个通用的安全认证框…

华为OD机试真题 JavaScript 实现【租车骑绿道】【2023Q1 100分】

一、题目描述 部门组织绿岛骑行团建活动&#xff0c;租用公共双人自行车骑行&#xff0c;每辆自行车最多坐两人、最大载重 M。 给出部门每个人的体重&#xff0c;请问最多需要租用多少双人自行车。 二、输入描述 第一行两个数字 m、n&#xff0c;自行车限重 m&#xff0c;代…

管理类联考——逻辑——技巧篇——论说文模块

正论模块 一直为社会、企业所重视。正是因为___让我们在应对挑战中超越自我&#xff0c;形成了一种保持可持续发展的能力与定力,涵养了自强不息、卧薪尝胆的品质&#xff0c;弥补了心智和能力的短板。古今中外这样的事例比比皆是/正如__所言“__”(事实证据或引典&#xff09;…

Mybatis学习之插件

Mybatis学习之插件 Plugins Mybatis中的插件虽然名称叫插件&#xff0c;但实质上是通过动态代理实现的。和我们平时讲的插件概念不一样&#xff0c;但是本质上都是给外部提供接口进行扩展。 MyBatis 允许我们在映射语句执行过程中的某一点进行拦截调用。MyBatis允许我们使用…

【C/C++】引用()的概念和用法

创作不易&#xff0c;本篇文章如果帮助到了你&#xff0c;还请点赞 关注支持一下♡>&#x16966;<)!! 主页专栏有更多知识&#xff0c;如有疑问欢迎大家指正讨论&#xff0c;共同进步&#xff01; &#x1f525;c系列专栏&#xff1a;C/C零基础到精通 &#x1f525; 给大…