当前位置:   article > 正文

【游戏开发创新】Unity+人工智能,让小朋友的画成真,六一儿童节一起来画猫猫吧(Unity | 人工智能 | 绘图 | 爬虫 | 猫妖)_unity 粒子绘画

unity 粒子绘画

一、前言

点关注不迷路,持续输出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&copyright=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&copyright=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()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63

四、猫猫作品展

给大家看看我画的几幅,大家可以大胆发挥想象力,考验一下人工智能~
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

五、Unity制作讲解

又到了技术讲解环节了,下面我来讲下本Demo的制作过程。
原理图如下:
在这里插入图片描述

1、界面素材

首先,在网上找一些素材图,
请添加图片描述请添加图片描述

请添加图片描述请添加图片描述
接着,把素材图导入Unity工程中,
在这里插入图片描述
图片的格式都设置成Sprite (2D and UI)
在这里插入图片描述

2、UGUI制作界面

使用UGUI制作界面,主要使用到的UI组件是:
图片:Image组件,文字:Text组件,文字的描边使用了Outline组件,
在这里插入图片描述
其中绘图的UIRawImage
在这里插入图片描述

3、分辨率适配

UI一定要注意分辨率适配,比如后面的天空底图,将Anchor设置为top - stretch
在这里插入图片描述
这样在不同分辨率下都可以适配铺满屏幕上方,其他UI对象根据具体情况设置Anchor
在这里插入图片描述

4、如何检测鼠标事件

如下,我们怎么捕获鼠标的事件?
在这里插入图片描述
这里是通过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;
	// ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

其中通过传递过来的参数,我们可以知道鼠标点击的坐标position和拖拽的变化向量delta
在这里插入图片描述

5、世界坐标转局部坐标

上面的OnDrag函数,我们得到的position是世界坐标,我们需要转换为图片的局部坐标,这样我们才方便在图片的这个位置画线,通过TransformInverseTransformPoint方法即可将世界坐标转为以该Transform为父节点的局部坐标。

var area = data.pointerDrag.GetComponent<RectTransform>();
// 起始点局部坐标
var p0 = area.InverseTransformPoint(data.position - data.delta);
// 终止点局部坐标
var p1 = area.InverseTransformPoint(data.position);
  • 1
  • 2
  • 3
  • 4
  • 5

InverseTransformPoint函数模型如下:
在这里插入图片描述
如果是要将局部坐标转世界坐标,则是使用TransformPoint函数,对应的函数模型如下:
在这里插入图片描述

6、画线原理

我们在图上画画,本质上是画一段一段的小线段。可以近距离观察,
在这里插入图片描述
利用的是RenderTexture.active,当我们给RenderTexture.active赋值的时候,所有的渲染操作会进入这个激活的RenderTexture对象,

// 备份
var prevRT = RenderTexture.active;
// 把画图的的RenderTexture对象_sourceTexture赋值给active
RenderTexture.active = _sourceTexture;
// 执行绘图,所有的绘图会渲染到_sourceTexture对象上
// ...
// 还原
RenderTexture.active = prevRT;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

执行绘图的逻辑,用的是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);
  • 1
  • 2
  • 3
  • 4

再给这个Mesh塞入两个顶点,

List<Vector3> _vertexList = new List<Vector3>(4);
_vertexList.Clear();
_vertexList.Add(p0);
_vertexList.Add(p1);
_lineMesh.SetVertices(_vertexList);
  • 1
  • 2
  • 3
  • 4
  • 5

不过光有Mesh还不够,Mesh只是网格,它只定义了形状,还欠一个材质,所以我们再弄一个材质球,

Material _lineMaterial = new Material(_drawShader);
// 黑色
_lineMaterial.color = Color.black;
  • 1
  • 2
  • 3

最后,调用Graphics.DrawMeshNow进行绘图,

_lineMaterial.SetPass(0);
Graphics.DrawMeshNow(_lineMesh, Matrix4x4.identity);
  • 1
  • 2

这样子,我们的线段Mesh就画到了_sourceTexture对象上了。

7、橡皮擦原理

画的过程中,想用使用橡皮擦,使用鼠标右键或者按住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);
  • 1
  • 2
  • 3
  • 4

橡皮擦的材质球颜色为白色,

Material _eraserMaterial = new Material(_drawShader);
// 白色
_eraserMaterial.color = Color.white;
  • 1
  • 2
  • 3

绘制的时候,需要设置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);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

检测是否橡皮擦的逻辑:

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);
	// ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
8、重置图片

画的过程中,想要重新画,按C键即可。
在这里插入图片描述
重置图片用的是GraphicsBlit方法。
在这里插入图片描述
这个方法的作用就是使用着色器将源纹理复制到目标渲染纹理上。
重置逻辑如下:

if (Input.GetKeyDown(KeyCode.C))
{
    Graphics.Blit(_defaultTexture, _sourceTexture);
    Graphics.Blit(_defaultTexture, _resultTexture);
}
  • 1
  • 2
  • 3
  • 4
  • 5
9、训练模型下载

需要先下载训练模型,放到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);
  • 1
  • 2
  • 3

通过训练模型我们构造了生成器_generator

10、通过生成器得到图像

通过Start方法传入我们画的图像,通过GetResult方法得到生成的图像。

// Generator.cs 生成器

public void Start(Texture input)
{
	Image.ConvertToTensor(input, _temp1);
	_progress = 0;
}

public void GetResult(RenderTexture output)
{
	Image.ConvertFromTensor(_temp1, output);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

如果继续往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》

七、附录,《睡吧孩子》画画过程记录

1、线稿过程

在这里插入图片描述

2、成稿

在这里插入图片描述

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/123593
推荐阅读
相关标签
  

闽ICP备14008679号