前段时间总是加班,也没啥心情和精力去研究新东西,总结一下自己之前做的字帖的功能
先上效果图:
文章分为几部分:
(一) 画图板实现原理
(二) 画图具体实现过程中的核心点
(三) 在画图板的基础上 演变为字帖的思路
画图板功能一定要有两个东西:一个画布,一个画笔。
然后你需要知道Unity中有这样一个函数:
public static void Blit (Texture source, RenderTexture dest, Material mat) ;
这个函数的官方解释是:“Copies source texture into destination render texture with a shader”
我个人的理解就是: 把source贴图上的信息,通过一个材质上的shader里的处理方法,赋给dest贴图。
这就是画板的实现原理的支撑,Material 是画笔,RawImage 是画布。
具体点说,获取 RawImage 组件上的 RenderTexture 作为 source贴图,同时也作为dest贴图,然后用自己的Material去对RenderTexture做处理,处理结果还是保存回这个RenderTexture。
Graphics.Blit(m_renderTex, m_renderTex, brushMat);
材质的处理逻辑是写在shader上的。
那么接下来的问题变成了如下 :
1.Shader 如何知道你的“落笔位置”
2.Shader 如何把你画的东西 画到 RawImage 上
1. Shader 如何知道你的“落笔位置”
大家都知道shader中计算的坐标是贴图的uv坐标
所以如何知道你的落笔位置呢?这就需要一系列比较恶心的换算了~
(1) 得到画板中心在屏幕中的中心位置
(2) 得到鼠标/手指触碰位置在屏幕中的位置
(3) 计算鼠标/手指触碰位置 与 画板中心的相对位置
(4) 计算鼠标/手指触碰位置 相对于贴图的uv坐标
试了很多次的代码,满满干货 ~ 拿去拿去 (如果父物体及以上的层级有缩放,这里可能还需要修正的参数,这里就不写了)
Vector2 GetUV(Vector2 brushPos)
{
//获取图片在屏幕中的像素位置
Vector2 rawImagePos = Vector2.zero;
//判断所在画布的渲染方式,不同渲染方式的位置计算方式不同
switch (m_renderMode)
{
case RenderMode.ScreenSpaceOverlay:
rawImagePos = rawImage.rectTransform.position;
break;
default:
rawImagePos = m_uiCamera.WorldToScreenPoint(rawImage.rectTransform.position);
break;
}
//换算鼠标在图片中心点的像素位置
Vector2 pos = brushPos - rawImagePos;
//换算鼠标在图片中UV坐标
Vector2 uv = new Vector2(pos.x / m_rawImageSizeX + 0.5f, pos.y / m_rawImageSizeY + 0.5f);
return uv;
}
2. Shader 如何把你画的东西 画到 RawImage 上
经过1中的一些列的换算,我们知道了落笔位置对应图板的贴图的uv坐标了,
下一步就是把uv对应像素及周边的像素填上你想要的颜色,喏~ 一个点就画完了。
然后是如何画线呢? 你会说:简单,点多了就是线了啊,每帧去打点不就ok了~ 这时候就出问题了,如果画的太快,一帧的时间过去你的手已经画出去了好远。那么就是一些不连续的点,而不是完整的线。
所以,我们需要把当前点和上一个点存起来,两点之间做填充。
我这里用的方式是:将两个点为圆心的两个圆填满,再将将以两个点连线为对称轴,长为两点之间距离,圆直径为宽的矩形填满。(自己算法 不一定好~ 各位大神有高招欢迎讨论~ )
具体步骤看一下代码:
Shader "Hidden/DrawWord"
{
Properties
{
_Tex("Texture" , 2D) = "white" {}
_Size("Size", float) = 0
_Color("Color" , color) = (1,1,1,1)
_UV("UV" , vector) = (0,0,0,0)
_LastUV("LastUV" , vector) = (0,0,0,0)
}
SubShader
{
ZTest Always Cull Off ZWrite Off Fog{ Mode Off }
Blend SrcAlpha OneMinusSrcAlpha
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
v2f vert(appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
sampler2D _Tex;
float _Size;
fixed4 _UV;
fixed4 _LastUV;
fixed4 _Color;
fixed4 frag(v2f i) : SV_Target
{
fixed4 col = tex2D(_Tex, i.uv);
float a = _UV.x;
float b = _UV.y;
float c = _LastUV.x;
float d = _LastUV.y;
float AA = d - b;
float BB = a - c;
float CC = b * c - a * d;
float x = i.uv.x;
float y = i.uv.y;
float sqrDic1 = (x - a) * (x - a) + (y - b) * (y - b);
float sqrDic2 = (x - c) * (x - c) + (y - d) * (y - d);
float sqrDic11 = (AA * x + BB * y + CC) * (AA * x + BB * y + CC) / (AA * AA + BB * BB);
float sqrDic22 = (x - (a + c) / 2) * (x - (a + c) / 2) + (y - (b + d) / 2) * (y - (b + d) / 2);
float sqrDicStand1 = _Size/10000 * _Size/10000;
float sqrDicStand2 = ((a - c) * (a - c) + (b - d) * (b - d)) / 4;
//判断当前像素是否在被画的范围之内
if (sqrDic1 < sqrDicStand1 || sqrDic2 < sqrDicStand1 || (sqrDic11 < sqrDicStand1 && sqrDic22 < sqrDicStand2))
{
col = _Color;
}
return col;
}
ENDCG
}
}
}
好嘞~ 核心代码就这些啦,项目贴在最下面。
画图的Demo 项目地址:https://github.com/PatrickBoomBoom/board.git
这些已经在公司的项目中实现了,不太方便贴出来,就口述一下吧 ~
画图的思路是:找到操作的uv坐标,然后去改变贴图颜色,想做字帖的话在画图的基础上再加两个步骤:
1. 规定可涂色的范围:
准备一张有透明通道的写好的字的图片,shader中判断像素是否在写字的范围内的时候同时判断是否在预先准备好的字(或者对应笔画)的图片中,那么就只有(你画的 && 属于字内的)像素才会被填色;
2. 规定下笔起点、转折点、终点、方向:
配好一个汉字的各种配置,然后用一个Gameobject来充当画笔输入位置,规定这个obj只能沿着笔画走,如果写对了,跳转下一笔,如果写错重写。