【游戏技术群】959392658  【游戏出海群】12067810
游戏蛮牛 手机端
开启辅助访问
 找回密码
 注册帐号

扫一扫,访问微社区

开发者专栏

关注:2374

当前位置:游戏蛮牛 技术专区 开发者专栏

__________________________________________________________________________________
开发者干货区版块规则:

  1、文章必须是图文形式。(至少2幅图)
      2、文章字数必须保持在1500字节以上。(编辑器右下角有字数检查)
      3、本版块只支持在游戏蛮牛原创首发,不支持转载。
      4、本版块回复不得无意义,如:顶、呵呵、不错......【真的会扣分的哦】
      5、......
__________________________________________________________________________________
查看: 3495|回复: 23

[士郎] 【重现】死亡细胞中动画像素化在Unity里的实现

[复制链接]  [移动端链接]
排名
3
昨日变化

7088

主题

7615

帖子

2万

积分

Rank: 16

UID
1231
好友
185
蛮牛币
11121
威望
30
注册时间
2013-7-29
在线时间
3645 小时
最后登录
2018-12-17

社区QQ达人活力之星原创精华达人突出贡献奖财富之证游戏蛮牛QQ群会员蛮牛妹VIP

发表于 2018-9-25 10:39:28 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有帐号?注册帐号

x
目前我个人想到的比较好的3D模型转2D像素的渲染方法有:
1.通过后处理的方式将整个摄像机画面转为像素风格[1]。
2.将摄像机画面渲染到低分辨率的RenderTexture后再拉伸显示。
3.将3D模型预渲染成像素风格序列帧[2]。
当然,我标题都这么说了,肯定本文的主题就是第三种方法。前两种方法实际上是一个方向的变体,好处是不需要更改游戏素材设计,像素分布对齐,不方便的大概是对于素材的使用和掌控力度会小很多。
《死亡细胞》中美术师使用一款名为FBX Crouncher GUI的程序,将FBX的动画制作成序列帧后使用。经过一番搜索我并未发现这款软件被公开,所以只能手动重现。

我将3D模型转2D像素序列帧的功能罗列如下:
  • 支持将静态的模型渲染到一张带透明通道的像素图片上。
  • 支持像素图片的尺寸自定义。
  • 支持预览——所见即所得。
  • 支持将模型的一段动画输出到一组像素图片中。
  • 支持动画进度的调节。
  • 相对的界面支持*。
  • 支持预览输出后的像素动画*。(划掉)
思路
使用射线检测方法,发出射线阵列获取碰撞点,然后根据碰撞点信息输出贴图。之所以要碰撞点信息而不是直接采样颜色,是因为我另外打算实现2D像素动画中的光照效果,需要法线信息。
准备工作
既然是带动画的模型,导入到Unity中之后多半离不开SkinnedMeshRenderer,因此不能直接使用MeshCollider,而是先要将每帧的Mesh烘焙出来后再使用。
编写MeshBaker.cs:
[AppleScript] 纯文本查看 复制代码
/// <summary>
/// 用于统一MeshRenderer和SkinnedMeshRenderer的碰撞体设定用的
/// </summary>
[RequireComponent(typeof(MeshCollider))]
[RequireComponent(typeof(Renderer))]
public class MeshBaker : MonoBehaviour {
    private MeshCollider mc;
    private Renderer renderer;
    private Mesh mesh;
    private bool isStatic;
	// Use this for initialization
	void Start ()
    {
        mc = GetComponent<MeshCollider>();
        renderer = GetComponent<Renderer>();
        mesh = new Mesh();
        isStatic = !(renderer.GetType().Equals(typeof(SkinnedMeshRenderer)));
	}
	
	// Update is called once per frame
	void Update ()
    {
        if (isStatic == false && renderer != null)
        {
            ((SkinnedMeshRenderer)renderer).BakeMesh(mesh);
            mc.sharedMesh = mesh;
        }
	}
}

随后编写BodyPart.cs,用于统一色彩的采样。因为像素风格的素材对纹理精细度要求不高,有些小部件用纯色替代更为恰当,所以采样模式分为纯色和纹理两种模式:
[AppleScript] 纯文本查看 复制代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 用于统一纯色和使用纹理的颜色采样用的
/// </summary>
[RequireComponent(typeof(Renderer))]
public class BodyPart : MonoBehaviour {
    public Color Color;
    public Material Mat;
    public Texture2D Texture;       
    public bool isPureColor;        //是否纯色

    private Renderer mr;
    private Material myMat;
    private void Awake()
    {
        myMat = Instantiate(Mat) as Material;
        mr = GetComponent<Renderer>();
        mr.material = myMat;
    }

    private void Update()
    {
        mr.material.SetColor("_Color", Color);
        mr.material.mainTexture = Texture;
    }

    internal UnityEngine.Color GetColor(Vector2 texcoord)
    {
        if (isPureColor)
            return Color;
        else
            return ((Texture2D)(mr.material.mainTexture)).GetPixelBilinear(texcoord.x, texcoord.y);
    }
}

然后新建一个Unlit Shader,命名为Model,并创建使用该Shader的Model材质。由于渲染的时候只需要颜色信息,所以无需做其他处理。
使用的动画模型来自于Asset Store的免费资源[3]。
射线检测
新建RaySample.cs,在这里进行采样操作。当然,下述代码仅仅是将射线检测结果直接转化成了图片,但实际上射线检测信息还有其他作用,当然因为我是边写代码边写这篇文章的
[AppleScript] 纯文本查看 复制代码
SampleInfo[] StartSample()
    {
        SampleInfo[] res = new SampleInfo[SampleWidth * SampleHeight];
        for (int y = 0; y < SampleHeight; y++)
            for (int x = 0; x < SampleWidth; x++)
            {
                res[y * SampleWidth + x] = SampleColor(new Ray(transform.position + new Vector3((x - SampleWidth * 0.5f) * PixelWidth, y * PixelWidth, 0), Vector3.forward));
            }
        return res;
    }

    SampleInfo SampleColor(Ray ray)
    {
        var hits = Physics.RaycastAll(ray, RayLength);
        if (hits.Length == 0)
            return null;
        RaycastHit firstHit = hits[0];
        if (firstHit.collider.gameObject.layer != 8) 
            return null;
        SampleInfo resInfo = new SampleInfo();
        resInfo.Normal = firstHit.normal;
        resInfo.Position = firstHit.point;
        resInfo.Color = firstHit.collider.GetComponent<BodyPart>().GetColor(firstHit.textureCoord);
        return resInfo;
    }

    Color[] CreateFrame(SampleInfo[] infos)
    {
        Color[] colors = new Color[SampleWidth * SampleHeight];
        for (int i = 0; i < colors.Length; i++)
        {
            if (infos[i] != null)
                colors[i] = infos[i].Color;
            else
                colors[i] = new Color(0, 0, 0, 0);
        }
        return colors;
    }

然后我们堆砌好场景:
1.jpg
然后渲染!
2.jpg

排序之前
然后我们就会发现,剑呢?这么大一把剑呢?哦,原来我忘了对RaycastHit进行排序,由于我规定射线的采样方向是Vector3.forward,所以只需要取z轴最小的检测结果即可:
3.jpg

排序之后实时预览
其实到这一步,fbx anim转pixel的基本功能也就差不多了。但是既然都说了是工作流程,当然功能不是最重要的,保证开发流程的便利才是本文的重点。
实时预览也很简单,用UGUI的RawImage即可,需要注意的是生成Texture2D的时候要更改其过滤模式,保证点阵的清楚。
[AppleScript] 纯文本查看 复制代码
Texture2D previewTex = CreatePreview(CreateFrame(StartSample()));
if (PreviewImg != null && previewTex != null)
{
   previewTex.filterMode = FilterMode.Point;
   PreviewImg.rectTransform.sizeDelta = new Vector2(SampleWidth, SampleHeight) * 5;
   PreviewImg.texture = previewTex;
}


4.jpg
进度调节
原本打算用Animator的Record[4],但是发现它只能作用在默认动画状态上,随作罢。虽然ForceStateNormalizedTime显示已被弃用,但是还是挺好用的。
   
[AppleScript] 纯文本查看 复制代码
 public void OnProgressBarChanged()
    {
        Animator animator = modelRoot.GetComponentInChildren<Animator>();
        animator.speed = 0f;
        animator.ForceStateNormalizedTime(progress.value * ((AnimationClip)curState.state.motion).length);
    }

动画导出
既然能够自然调节进度,那么也就自然能够导出,使用一个协程在每帧渲染结束后导出当前帧的动画:
  
[AppleScript] 纯文本查看 复制代码
  IEnumerator StartExport(int totalFrames)
    {
        Debug.Log("Start export!");
        Animator animator = modelRoot.GetComponentInChildren<Animator>();
        animator.speed = 0f;
        var length = ((AnimationClip)curState.state.motion).length;
        for(int i = 0; i < totalFrames; i++)
        {
            animator.ForceStateNormalizedTime(i / (float)totalFrames);
            raySample.ExportCurrent(roleInput.text + "_auto", animInput.text + "-" + (i+1).ToString());
            yield return new WaitForEndOfFrame();
        }
        Debug.Log("Export complete!");
    }

5.jpg 导出的截图色调替换
色调的替换也是一个非常有利于开发的功能。这里我约定了8种颜色,分别是RGB通道的0/1组合,一般来说8种颜色已经够用了。
6.jpg
多重采样
目前而言我们只对一个像素采样一次,但是这是不够充分的,甚至在极端情况下是偏颇的。因此根据蒙特卡洛法,我们将对像素进行多次采样,并予以混合。
7.jpg

单次采样
8.jpg

5次采样
可以看到,原本的黑色边缘被淡化,受到了更多来自其他像素的干扰。
2D动态光照
这一部分我的想法是,通过将法线信息储存在另外一张序列帧中,在游戏运行中通过计算法线和光线的点积,对明暗面分别进行RGB调整。
不过最后的效果不尽如人意,过渡比较突兀,调节光照的过程不够直观,而且说实话,光是为了动态光照而需要多上一倍的资源占用,显然不是那么的……“足够产品”。
Shader代码如下,是拿Unity自带的Sprite Shader Default改的:
[AppleScript] 纯文本查看 复制代码
// Unity built-in shader source. Copyright (c) 2016 Unity Technologies. MIT license (see license.txt)

Shader "Sprites/Pixel"
{
    Properties
    {
        [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
	_NormalTex("Normal Texture", 2D) = "white" {}
	_LightDir ("Light Direction", Vector) = (0,0,1)
	_LightIntensity ("Intensity", Range(0, 1)) = 0.5 
        _Color ("Tint", Color) = (1,1,1,1)
        [MaterialToggle] PixelSnap ("Pixel snap", Float) = 0
        [HideInInspector] _RendererColor ("RendererColor", Color) = (1,1,1,1)
        [HideInInspector] _Flip ("Flip", Vector) = (1,1,1,1)
        [PerRendererData] _AlphaTex ("External Alpha", 2D) = "white" {}
        [PerRendererData] _EnableExternalAlpha ("Enable External Alpha", Float) = 0
    }

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

        Cull Off
        Lighting Off
        ZWrite Off
        Blend One OneMinusSrcAlpha

        Pass
        {
        CGPROGRAM
            #pragma vertex SpriteVert
            #pragma fragment frag
            #pragma target 2.0
            #pragma multi_compile_instancing
            #pragma multi_compile _ PIXELSNAP_ON
            #pragma multi_compile _ ETC1_EXTERNAL_ALPHA
            #include "UnitySprites.cginc"

		sampler2D _NormalTex;
		float3 _LightDir;
		float _LightIntensity;
		fixed4 frag(v2f IN) : SV_Target
		{
			fixed4 c = SampleSpriteTexture (IN.texcoord) * IN.color;
			fixed4 normal = tex2D(_NormalTex, IN.texcoord);
			fixed d = dot(normalize(normal.xyz - Domain Name For Sale | Undeveloped), normalize(_LightDir));
			c.rgb *= (1 - _LightIntensity + step(d, 0) * 2 * _LightIntensity);
			c.rgb *= c.a;
			return c;
		}

        ENDCG
        }
    }
}

效果如下:
1.1.gif


结语
这篇文章并无技术难点,主要是还原了《死亡细胞》美术师的工作流程。
当然,这里要多嘴提一句,并不是所有3D模型都适合用这种方法制作出像素素材的,因为《死亡细胞》的模型在设计之初就是为了像素表现,突出强调了角色身上的形状,而没有精雕细琢。
我认为适于像素化的3D模型最好也是为此重新制作的,3D模型像素化的目的不是表现效果,而是开发速度。
知乎@破晓

评分

参与人数 2鲜花 +10 收起 理由
Trick + 5 很给力!
bb513169218 + 5 很给力!

查看全部评分


跟我念“站长妹纸萌萌哒!”我说站长,你说YO!爱你们么么哒~
回复

使用道具 举报

3偶尔光临
250/300
排名
7071
昨日变化
3

0

主题

7

帖子

250

积分

Rank: 3Rank: 3Rank: 3

UID
154566
好友
0
蛮牛币
942
威望
0
注册时间
2016-6-30
在线时间
65 小时
最后登录
2018-12-11
发表于 2018-9-25 11:46:27 | 显示全部楼层
谢谢大佬,一直在找这方面的材料。

回复 支持 反对

使用道具 举报

4四处流浪
454/500
排名
5508
昨日变化
1

0

主题

51

帖子

454

积分

Rank: 4

UID
274847
好友
0
蛮牛币
142
威望
0
注册时间
2018-3-29
在线时间
155 小时
最后登录
2018-12-15
发表于 2018-9-25 11:53:36 | 显示全部楼层
牛批,学习到了
[发帖际遇]: 李业丰 被钱袋砸中进医院,看病花了 2 蛮牛币. 幸运榜 / 衰神榜

回复

使用道具 举报

5熟悉之中
686/1000
排名
4166
昨日变化
1

0

主题

188

帖子

686

积分

Rank: 5Rank: 5

UID
267103
好友
0
蛮牛币
1709
威望
0
注册时间
2018-1-31
在线时间
158 小时
最后登录
2018-12-17
发表于 2018-9-26 08:45:50 | 显示全部楼层

回复

使用道具 举报

6蛮牛粉丝
1322/1500
排名
1482
昨日变化
4

0

主题

176

帖子

1322

积分

Rank: 6Rank: 6Rank: 6

UID
137070
好友
0
蛮牛币
2620
威望
0
注册时间
2016-2-20
在线时间
318 小时
最后登录
2018-12-17
发表于 2018-9-26 10:18:07 | 显示全部楼层
666666

回复

使用道具 举报

4四处流浪
457/500
排名
5437
昨日变化
2

0

主题

82

帖子

457

积分

Rank: 4

UID
282652
好友
1
蛮牛币
2586
威望
0
注册时间
2018-5-24
在线时间
121 小时
最后登录
2018-12-17
发表于 2018-9-26 10:30:52 | 显示全部楼层
可以的.........

回复

使用道具 举报

5熟悉之中
888/1000
排名
3237
昨日变化

0

主题

132

帖子

888

积分

Rank: 5Rank: 5

UID
128112
好友
11
蛮牛币
1125
威望
0
注册时间
2015-11-6
在线时间
314 小时
最后登录
2018-12-17
发表于 2018-9-26 14:29:10 | 显示全部楼层
厉害厉害厉害

回复

使用道具 举报

7日久生情
2857/5000
排名
2474
昨日变化
8

2

主题

1848

帖子

2857

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
241666
好友
0
蛮牛币
10745
威望
0
注册时间
2017-9-6
在线时间
443 小时
最后登录
2018-12-16
发表于 2018-9-27 06:25:30 | 显示全部楼层
感谢分享代码

回复

使用道具 举报

5熟悉之中
768/1000
排名
16534
昨日变化

0

主题

500

帖子

768

积分

Rank: 5Rank: 5

UID
199204
好友
0
蛮牛币
355
威望
0
注册时间
2017-1-5
在线时间
228 小时
最后登录
2018-12-14
发表于 2018-9-27 09:06:37 | 显示全部楼层
感谢分享代码

回复

使用道具 举报

5熟悉之中
591/1000
排名
6896
昨日变化
1

0

主题

285

帖子

591

积分

Rank: 5Rank: 5

UID
250350
好友
2
蛮牛币
588
威望
0
注册时间
2017-10-23
在线时间
124 小时
最后登录
2018-11-10
发表于 2018-9-27 09:46:07 | 显示全部楼层

感谢分享代码

回复

使用道具 举报

7日久生情
2341/5000
排名
2508
昨日变化
6

1

主题

1490

帖子

2341

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
119154
好友
0
蛮牛币
2628
威望
0
注册时间
2015-8-21
在线时间
296 小时
最后登录
2018-12-16
发表于 2018-9-27 10:44:20 | 显示全部楼层
谢谢楼主大大。

回复

使用道具 举报

5熟悉之中
651/1000
排名
3935
昨日变化

2

主题

43

帖子

651

积分

Rank: 5Rank: 5

UID
261068
好友
1
蛮牛币
1666
威望
0
注册时间
2017-12-26
在线时间
240 小时
最后登录
2018-12-17
发表于 2018-9-27 14:33:46 | 显示全部楼层
谢谢分享

回复

使用道具 举报

3偶尔光临
173/300
排名
15824
昨日变化

0

主题

67

帖子

173

积分

Rank: 3Rank: 3Rank: 3

UID
152176
好友
0
蛮牛币
266
威望
0
注册时间
2016-6-15
在线时间
62 小时
最后登录
2018-12-12
发表于 2018-9-27 14:53:03 | 显示全部楼层
厉害厉害厉害
[发帖际遇]: 哈哈哈123 发帖时在路边捡到 2 蛮牛币,偷偷放进了口袋. 幸运榜 / 衰神榜

回复

使用道具 举报

排名
20835
昨日变化
4

0

主题

13

帖子

54

积分

Rank: 2Rank: 2

UID
241931
好友
0
蛮牛币
10
威望
0
注册时间
2017-9-7
在线时间
15 小时
最后登录
2018-12-17
发表于 2018-9-28 14:19:03 | 显示全部楼层
有没有整份代码呀!想仔细看看!

回复 支持 反对

使用道具 举报

5熟悉之中
532/1000
排名
13150
昨日变化
1

3

主题

153

帖子

532

积分

Rank: 5Rank: 5

UID
216830
好友
2
蛮牛币
415
威望
0
注册时间
2017-4-9
在线时间
312 小时
最后登录
2018-12-17
发表于 2018-9-28 14:57:00 | 显示全部楼层
这个好厉害啊
[发帖际遇]: wx_Zh7tc9i4 发帖时在路边捡到 2 蛮牛币,偷偷放进了口袋. 幸运榜 / 衰神榜

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册帐号

本版积分规则

快速回复 返回顶部 返回列表