问题概述
项目在制作角色换装时候遇到一个问题, 就是如何将现有2D模型打组到Atlas之中. 因为Unity Canvas Image 使用的是一个Quad 绘制的一个图形,所以打组到Atlas中的png图也必须使用Full Rect
才可以
但是,为了方便美术编辑
所有五官图片都是预留空白用来对位置的,这样出图就无需每个进行调整锚点对位置. PS直接导出png即可直接使用.
但是这样存图再使用FullRect打组到Atlas必然产生极大的浪费. 所以必须想办法让Canvas中支持以Tight方式存储的Sprite才可以
现有解决方案
Google到的已有解决方案有两个, 一个是 Assets Store 售卖的插件 PolyImage,还有一个是国内大神在Github上面开源的库AdvancedImage.cs
两个组件我都有尝试,相比来说 PolyImage的解法比较独特,但是从功能上来说AdvancedImage要完善很多(不过有个锚点偏移的Bug).
自制 Tight Image Component
出于研(wu)究(liao)的目的,我还是准备自己实现一版. 不过后来发现 自己花了大量的时间在研究PIP PIT问题…😓
Unity Sprite 中包含哪些信息
在写Canvas的组件之前,首先要了解的是
如何把一张图渲染到屏幕上来的
这个问题我之前有在 手动创建Mesh及相关理论知识 中说过, 整个渲染的基础要素有
- 顶点(vertices)
- 三角形(triangles)
- 法线-非必须(normals)
- 切线-非必须(tangents)
- 每个顶点的UV信息
- 纹理(texture)
- 渲染所使用的Shader
对于一个3D物体的渲染,前面几项(vertices,triangles,normals,tangents,uv)的集合就是一个Mesh文件,texture和shader的集合就是一个Material文件.
对于2D物体的渲染,由于不考虑光照问题,所以把 vertices,triangles,uv,texture 放在一起就是一个Sprite文件 , 而Material中只需要包含一个Shader即可
重写SpriteRender
知道了以上概念后,我们就可以自己写一个简化版的SpriteRender,将一个png图渲染到屏幕之上.
代码如下:
1 | using System; |
有几点需要说明一下的
- 从代码中可以看出 一个Sprite文件包含了(vertices,triangles,uv,texture)信息
- 当png图被打入Atlas时候,所引用到的Sprite文件texture是Atlas文件,且uv,vertices信息变为 针对该atlas的uv信息了
- Sprite的渲染和Mesh的渲染本质上无区别
Sprite/Default
Shader使用的是双面渲染,应该是考虑到Sprite可以绕Y轴旋转所以才这样设计的吧
重载Image组件
Unity的UGUI是开源项目,项目地址在这里
对UGUI源码分析的文章不少,比较推荐 aillieo.cn的
组件可以直接继承自MaskableGraphic
重新写,但是直接继承自Image
去写还是最方便的.
aillieo 在Graphic分析中已经有指出了 Graphic
组件的刷新方式
1 | CanvasUpdate.PreRender |
初版 TightImage Component
1 | public class TightImage : Image |
初版的实现起始就是把 SpriteRenderV
部分的代码 整合到了 Canvas 之下. 有两点需要说明一下就是
rectTransform
无需Cache 随取随用即可, 其内部实现已经Cache了一份
1 | /// <summary> |
还有一点就是 缩放问题
rectTransform.rect 与 overrideSprite.bounds
1 | /// <summary> |
GetPixelAdjustedRect
函数内部实现 其实返回的就是rectTransform.rect
, 也就是在Editor中编辑模式下的外框大小
单位是已经转化为像素了的值
而 overrideSprite.bounds
取得的就是 整个png图的外框unit
尺寸(包含空白区域)
解决 Image/Sprite中锚点问题
上面代码 在Image中的锚点和Sprite中的锚点都是中心时候是正确的,但是如果修改了其中一个锚点 整个图像会发生偏移. 这里也就是 AdvancedImage
有点小Bug的地方,它只处理的Sprite中的锚点偏移,没有处理 Image中的锚点偏移问题.
1 | //Image center offset |
注意就是 Sprite中要乘上一个倍率,然后有个正负号问题. 我后来也没想明白为啥两个偏移一个正一个反. 反正试了一下这样结果是正确的 😂
PolyImage核心逻辑解析
版权问题,也就不粘贴PolyImage的代码了,不过它解决缩放和平移的问题是另外一个思路. 是从uv入手的. 这样也就不用考虑锚点的问题了.
其内部用到了几个函数 值得说一下的
1 | /// Image's dimensions used for drawing. X = left, Y = bottom, Z = right, W = top. |
该函数直接返回了Image的Bounds区域 其内部的
Sprites.DataUtility.GetPadding
可以取得Sprite中的空白外框区域.
具体的参考aillieo 在Image的分析
然后再用
Sprites.DataUtility.GetOuterUV(overrideSprite)
取得UV外框的区域,这样也就有了 Sprite 和 Image之间的比例关系. 剩下的就是 加加减减的计算了.
碰撞检测
没想多最终折在了碰撞检测上😑… 导致我重修了一遍 初中数学, 具体的直接参考 再论向量在游戏中的应用-x 特集就好了
Image自身的碰撞检测
Image 其实是有一个比较硬核的 忽略透明区域的 碰撞检测的.
1 | public virtual bool IsRaycastLocationValid(Vector2 screenPoint, Camera eventCamera) |
原理度娘上已经很多人写过了 比如这个: UGUI小贴士:使用不规则按钮
比较硬核 就像文章中说的 内存会加倍
. 但就我看来 应该理解为这样更合适:
原有纹理只是放在显存中,使用这种方式后 就需要在内存中Cache一份纹理了
所以alphaHitTestMinimumThreshold
并未设置在Editor的Inpetector
中,需要手动使用代码打开.
优化方案
替代方案,一种是设置 Polygon Collider
, 点可以直接使用Sprite中的vertices, 然后判断直接用
1 | public override bool IsRaycastLocationValid(Vector2 screenPoint, Camera eventCamera) |
或者直接把问题拆解为PIT问题 然后判断即可.
终版 TightImage Component
1 | using UnityEngine; |
需要说明的是 IsRaycastLocationValid
是已经经过AABB测试以后才会被调用的,所以其实现内无需再做一次了.