赞
踩
点关注不迷路,持续输出Unity
干货文章。
嗨,大家好,我是新发。
马上要六一儿童节了,我这个老人家又可以假装6岁了。
小时候我喜欢画画,那时候流行七龙珠、宠物小精灵、数码宝贝,这些我都画过。
二年级的时候老师带我去报名了校级的画画比赛,让我比赛的时候画五星红旗,我说这个太简单了,不过老师还是坚持让我画五星红旗。比赛时我很快就画好了,坐在位子上开始偷看四周人都在画什么,我转头看到后座的小朋友画的是一艘华丽的游轮… …
比赛结束当场就宣部了结果,我没有得奖;老师为了安慰我,说要带我去她家里炸虾丸给我吃;我坐在凳子上等了很久很久,尿急得不得了,但又不好意思说,差点就晕过去… …
长大后,不怎么画画了,兴趣点来了就画一幅,下面这张图是我用iPad
画的,场景是我现在租的真实的小窝(30平的小单间),蹲在我身上的是我养的猫——皮皮(它现在3周岁了),这幅画我命名为《睡吧孩子》,
注:本图的线稿作画过程和最终成图见
文章末尾
。
我把自己画成一个孩子,我希望每个人心中都有一个小孩子,不忘本心,永远清澈明朗,无忧亦无惧。
好了,扯远了,回归本文主题,六一儿童节,我们一起来当小朋友吧。
小孩子一般都喜欢画画,我用Unity
做了一个画猫猫的Demo
,效果如下,AI
会根据你画的线稿自动识别生成对应的猫图。
操作说明:
鼠标左键是画线,右键是橡皮擦(或者按Shift
+鼠标左键也是橡皮擦),按C
是清空画布。
想玩的同学可以直接下载我打好的包,
Windows版本:
网盘地址:https://pan.baidu.com/s/1RrpmwVhwEe0hKv06RCvCbQ
提取码:3yyq
另,本Demo
源码工程已上传到CodeChina
,感兴趣的同学可自行下载学习。
地址:https://codechina.csdn.net/linxinfa/UnityDrawCatAI
注:我使用的Unity
版本:Unity 2020.2.7f1c1 (64-bit)
。
下文我会讲解Demo
的制作细节。
画不好,没关系,多看些猫图,给你准备了爬虫,无穷只猫~
爬虫python代码
import requests import os import urllib # 百度图片爬虫 class Spider_baidu_image(): def __init__(self): self.url = 'http://image.baidu.com/search/acjson?' self.headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.\ 3497.81 Safari/537.36'} self.headers_image = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.\ 3497.81 Safari/537.36','Referer':'http://image.baidu.com/search/index?tn=baiduimage&ipn=r&ct=201326592&cl=2&lm=-1&st=-1&fm=result&fr=&sf=1&fmq=1557124645631_R&pv=&ic=&nc=1&z=&hd=1&latest=0©right=0&se=1&showtab=0&fb=0&width=&height=&face=0&istype=2&ie=utf-8&sid=&word=%E8%83%A1%E6%AD%8C'} # 构造参数数组 def get_param(self): keyword = urllib.parse.quote(self.keyword) params = [] for i in range(1,self.paginator+1): params.append('tn=resultjson_com&ipn=rj&ct=201326592&is=&fp=result&queryWord={}&cl=2&lm=-1&ie=utf-8&oe=utf-8&adpicid=&st=-1&z=&ic=&hd=1&latest=0©right=0&word={}&s=&se=&tab=&width=&height=&face=0&istype=2&qc=&nc=1&fr=&expermode=&force=&cg=star&pn={}&rn=30&gsm=78&1557125391211='.format(keyword,keyword,30*i)) return params # 构造url数组 def get_urls(self, params): urls = [] for i in params: urls.append(self.url+i) return urls # 遍历请求url并下载图片 def get_image(self, urls): cwd = os.getcwd() file_name = os.path.join(cwd,self.keyword) if not os.path.exists(self.keyword): os.mkdir(file_name) index = 0 for url in urls: json_data = requests.get(url,headers = self.headers).json() json_data = json_data.get('data') for i in json_data: if i: image_url = i.get('thumbURL') index += 1 with open(file_name+'\\{}.jpg'.format(index),'wb') as f: f.write(requests.get(image_url, headers = self.headers_image).content) print('下载: ' + image_url) def __call__(self, *args, **kwargs): # 构造参数 params = self.get_param() # 构造url数组 urls = self.get_urls(params) # 请求 self.get_image(urls) if __name__ == '__main__': spider = Spider_baidu_image() # 关键字 spider.keyword = '可爱的猫' # 页数,每页30张图 spider.paginator = 100 # 开始执行 spider()
给大家看看我画的几幅,大家可以大胆发挥想象力,考验一下人工智能~
又到了技术讲解环节了,下面我来讲下本Demo
的制作过程。
原理图如下:
首先,在网上找一些素材图,
接着,把素材图导入Unity
工程中,
图片的格式都设置成Sprite (2D and UI)
,
使用UGUI
制作界面,主要使用到的UI
组件是:
图片:Image
组件,文字:Text
组件,文字的描边使用了Outline
组件,
其中绘图的UI
是RawImage
,
做UI
一定要注意分辨率适配,比如后面的天空底图,将Anchor
设置为top - stretch
这样在不同分辨率下都可以适配铺满屏幕上方,其他UI
对象根据具体情况设置Anchor
。
如下,我们怎么捕获鼠标的事件?
这里是通过Event Trigger
组件来实现监听的,
在UI
对象上挂Event Trigger
组件,点击Add New EventType
即可添加对应的事件监听。因为我们是要在图上画画,所以我们要监听Drag
事件。
设置响应函数为SketchPad
脚本的OnDrag
函数。
这样我们就可以在SketchPad
脚本的OnDrag
函数中去实现相关的逻辑了。
// 拖拽事件响应函数
public void OnDrag(BaseEventData baseData)
{
PointerEventData data = (PointerEventData)baseData;
// 鼠标当前的坐标
Vector2 pos = data.position;
// 拖拽的变化向量
Vector2 delta = data.delta;
// ...
}
其中通过传递过来的参数,我们可以知道鼠标点击的坐标position
和拖拽的变化向量delta
。
上面的OnDrag
函数,我们得到的position
是世界坐标,我们需要转换为图片的局部坐标,这样我们才方便在图片的这个位置画线,通过Transform
的InverseTransformPoint
方法即可将世界坐标转为以该Transform
为父节点的局部坐标。
var area = data.pointerDrag.GetComponent<RectTransform>();
// 起始点局部坐标
var p0 = area.InverseTransformPoint(data.position - data.delta);
// 终止点局部坐标
var p1 = area.InverseTransformPoint(data.position);
InverseTransformPoint
函数模型如下:
如果是要将局部坐标转世界坐标,则是使用TransformPoint
函数,对应的函数模型如下:
我们在图上画画,本质上是画一段一段的小线段。可以近距离观察,
利用的是RenderTexture.active
,当我们给RenderTexture.active
赋值的时候,所有的渲染操作会进入这个激活的RenderTexture
对象,
// 备份
var prevRT = RenderTexture.active;
// 把画图的的RenderTexture对象_sourceTexture赋值给active
RenderTexture.active = _sourceTexture;
// 执行绘图,所有的绘图会渲染到_sourceTexture对象上
// ...
// 还原
RenderTexture.active = prevRT;
执行绘图的逻辑,用的是Graphics
类,它有大量的绘图操作函数,我们用的是DrawMeshNow
函数。
可以看到DrawMeshNow
需要一个Mesh
参数,所以我们需要构造一个Mesh
,我们构造一个线段的Mesh
,
Mesh _lineMesh = new Mesh();
_lineMesh.MarkDynamic();
_lineMesh.vertices = new Vector3[2];
_lineMesh.SetIndices(new[] { 0, 1 }, MeshTopology.Lines, 0);
再给这个Mesh
塞入两个顶点,
List<Vector3> _vertexList = new List<Vector3>(4);
_vertexList.Clear();
_vertexList.Add(p0);
_vertexList.Add(p1);
_lineMesh.SetVertices(_vertexList);
不过光有Mesh
还不够,Mesh
只是网格,它只定义了形状,还欠一个材质,所以我们再弄一个材质球,
Material _lineMaterial = new Material(_drawShader);
// 黑色
_lineMaterial.color = Color.black;
最后,调用Graphics.DrawMeshNow
进行绘图,
_lineMaterial.SetPass(0);
Graphics.DrawMeshNow(_lineMesh, Matrix4x4.identity);
这样子,我们的线段Mesh
就画到了_sourceTexture
对象上了。
画的过程中,想用使用橡皮擦,使用鼠标右键或者按住Shift
+鼠标左键即可。
橡皮擦的原理和画线的原理是一样的,只不过橡皮擦的Mesh
不是线段,而是正方形(两个三角形构成)。
其中
SetIndices
参数数组是顶点的序号,每三个序号为一组组成一个三角形,这里的顺序决定了Mesh
的法线方向,也即决定了正面,默认情况下shader
只会处理正面的渲染。
可以使用右手来判断法线的方向,四指按序号绕着旋转的方向,拇指指向的就是法线方向,下图的这个序号顺序,法线是指向屏幕外,也就是正面是超向屏幕外的。
Mesh _eraserMesh = new Mesh();
_eraserMesh.MarkDynamic();
_eraserMesh.vertices = new Vector3[4];
_eraserMesh.SetIndices(new[] { 0, 1, 2, 1, 3, 2 }, MeshTopology.Triangles, 0);
橡皮擦的材质球颜色为白色,
Material _eraserMaterial = new Material(_drawShader);
// 白色
_eraserMaterial.color = Color.white;
绘制的时候,需要设置4
个顶点的坐标,
// 半边长
const float d = 0.05f;
_vertexList.Clear();
_vertexList.Add(p0 + new Vector3(-d, -d, 0));
_vertexList.Add(p0 + new Vector3(+d, -d, 0));
_vertexList.Add(p0 + new Vector3(-d, +d, 0));
_vertexList.Add(p0 + new Vector3(+d, +d, 0));
_eraserMesh.SetVertices(_vertexList);
检测是否橡皮擦的逻辑:
public void OnDrag(BaseEventData baseData)
{
var data = (PointerEventData)baseData;
// ...
bool eraser = (data.button == PointerEventData.InputButton.Right);
eraser |= Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift);
// ...
}
画的过程中,想要重新画,按C
键即可。
重置图片用的是Graphics
的Blit
方法。
这个方法的作用就是使用着色器将源纹理复制到目标渲染纹理上。
重置逻辑如下:
if (Input.GetKeyDown(KeyCode.C))
{
Graphics.Blit(_defaultTexture, _sourceTexture);
Graphics.Blit(_defaultTexture, _resultTexture);
}
需要先下载训练模型,放到StreamingAssets
目录中,
训练模型下载地址:
https://raw.githubusercontent.com/affinelayer/pix2pix-tensorflow-models/master/edges2cats_AtoB.pict
初始化的时候,会去读取这个训练模型数据,
var filePath = Path.Combine(Application.streamingAssetsPath, "edges2cats_AtoB.pict");
Dictionary<string, Pix2Pix.Tensor> _weightTable = Pix2Pix.WeightReader.ReadFromFile(filePath);
Pix2Pix.Generator _generator = new Pix2Pix.Generator(_weightTable);
通过训练模型我们构造了生成器_generator
。
通过Start
方法传入我们画的图像,通过GetResult
方法得到生成的图像。
// Generator.cs 生成器
public void Start(Texture input)
{
Image.ConvertToTensor(input, _temp1);
_progress = 0;
}
public void GetResult(RenderTexture output)
{
Image.ConvertFromTensor(_temp1, output);
}
如果继续往Image
里面走,就是GpuBackend
模块,
对pix2pix
感兴趣的同学,可以访问它的开源项目:
地址:https://github.com/affinelayer/pix2pix-tensorflow
就先写这么多吧~
喜欢Unity
的同学,不要忘记点击关注,如果有什么Unity
相关的技术难题,也欢迎留言或私信~
推荐阅读:
《[Unity 3D] 权游红袍女在火中看到了什么,我看到了…(粒子系统 | 火焰特效 | ParticleSystem | 手把手制作)》
《[Unity 2D] 重温红白机经典FC游戏,顺便教你快速搭建2D游戏关卡(Tilemap | 场景 | 地图)》
《520程序员的浪漫,给CSDN近两万的粉丝比心心(python爬虫 | Unity循环复用列表 | 头像加载与缓存)》
《ShaderGraph使用教程与各种特效案例:Unity2020(持续更新)》
《Unity使用ShaderGraph配合粒子系统,制作子弹拖尾特效(Fate/stay night金闪闪的大招效果)》
《使用Unity ShaderGraph实现在模型上涂鸦的效果,那么,纹个手吧》
《学Unity的猫——第十五章:Unity粒子系统ParticleSystem,下雪啦下雪啦》
《Unity实现水果忍者切水果的刀痕效果教程(两种实现方式:TrailRenderer、LineRenderer)》
《Unity流体模拟,支持粒子系统,支持流体碰撞交互(Obi Fluid插件使用教程)》
《玩转贝塞尔曲线,教你在Unity中画Bezier贝塞尔曲线(二阶、三阶),手把手教你推导公式》
《Unity UGUI制作雷达图/天赋图/属性图/能力图,因为太怕痛就全点了防御力》
《使用Unity ShaderGraph实现刮刮乐的刮卡剔除效果,感受一下刮中500万的时刻》
《Unity后处理(图像优化特效技术),实现影视级别的镜头效果,辅助标签:PostProcessing》
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。