在 Unity 中还原浊心斯卡蒂的动态立绘【前奏】

    前言

    浊心斯卡蒂是明日方舟二周年时出的六星异格干员。她实在太美啦,而且还有动态立绘!!!作为蒂厨的我,当然要行动起来啦~(坏笑)

    先来看看最终效果吧!

    实录画面(战损版)

    为斯卡蒂献上心脏!!!

    好的,回到正事。接下来我将会更新一系列文章来详细讲解它的还原方法,主要会涵盖以下内容:

    • AssetStudio & uTinyRipper 解包资源
    • Spine 插件导入以及错误修复
    • Shader 还原
    • C# 脚本编写

    这篇文章,作为该系列的第零篇,主要用来为后面的内容做一些铺垫。

    下文内容将会被移动至其他文章。


    Unity 版本:2020.3.2f1c1。

    导入 Spine 插件

    明日方舟使用的是 Spine 3.5.51。其 Unity 版本的插件可以在此处下载。下载完成后导入至 Unity 项目中。

    由于这个 Spine 的版本比较老,所以导入较新版本的 Unity 后会出现一系列编译错误。

    修复编译错误

    Error Messages
    error CS0165: Use of unassigned local variable 'color'

    在对应文件中找到下面的方法并作出相应修改:

    public static void FillVerts (Skeleton skeleton, int startSlot, int endSlot, float zSpacing, bool pmaColors, Vector3[] verts, Vector2[] uvs, Color32[] colors, ref int vertexIndex, ref float[] tempVertBuffer, ref Vector3 boundsMin, ref Vector3 boundsMax, bool renderMeshes = true) {
    -   Color32 color;
    +   Color32 color = default;
        var skeletonDrawOrderItems = skeleton.DrawOrder.Items;
        float a = skeleton.a * 255, r = skeleton.r, g = skeleton.g, b = skeleton.b;
    
        // 下面内容省略......
    }

    Error Messages
    error CS0122: 'Texture.Texture()' is inaccessible due to its protection level

    在对应文件中找到下面的字段并作出相应修改:

    -   Texture m_previewTex = new Texture();
    +   Texture m_previewTex = null;
    
    public override void OnInteractivePreviewGUI (Rect r, GUIStyle background) {
        this.InitPreview();
    
        // 下面内容省略......
    }

    至此,问题就都解决了(至少我遇到的问题是都解决了¯\(ツ)/¯)。

    修改部分功能

    这个版本的 Spine 会在切换动画时先自动切换到 SetupPose,从而导致动画间过渡变得很鬼畜不自然。为了解决这个问题,我们需要在 /Spine/spine-csharp/AnimationState.cs 中做一些修改,如下所示:

    /// <summary>From last to first mixingFrom entries, sets timelinesFirst to true on last, calls checkTimelineUsage on rest.</summary>
    private void SetTimelinesFirst (TrackEntry entry) {
        if (entry.mixingFrom != null) {
            SetTimelinesFirst(entry.mixingFrom);
            CheckTimelinesUsage(entry, entry.timelinesFirst);
            return;
        }
        var propertyIDs = this.propertyIDs;
        var timelines = entry.animation.timelines;
        int n = timelines.Count;
        entry.timelinesFirst.EnsureCapacity(n); // entry.timelinesFirst.setSize(n);
        var usage = entry.timelinesFirst.Items;
        var timelinesItems = timelines.Items;
        for (int i = 0; i < n; i++) {
            propertyIDs.Add(timelinesItems[i].PropertyId);
    -       usage[i] = true;
    +       usage[i] = false;
        }
    }

    提取资源文件

    明日方舟的资源文件的常用获取方式有两种:

    1. PRTS 网站上获取(F12 后找源文件下载)。
    2. 解包游戏的源文件。

    由于第一种比较简单,这里我只介绍第二种方式。

    Spine 文件

    找到 /arts/dynchars/char_1012_skadi2/dyn_illust_char_1012_skadi2_2.ab 这个 AssetBundle 文件。它可能直接被包含在游戏包体内,也可能在 Application.persistentDataPath 下(这取决于你的游戏版本)。找到以后用 AssetStudio 打开。

    需要提取的资源如下:

    名称类型
    dyn_illust_char_1012_skadi2.atlasTextAsset
    dyn_illust_char_1012_skadi2.skelTextAsset
    dyn_illust_char_1012_skadi2Texture2D
    dyn_illust_char_1012_skadi2[alpha]Texture2D
    dyn_illust_char_1012_skadi22Texture2D
    dyn_illust_char_1012_skadi22[alpha]Texture2D

    音频文件

    同样的方法找到 /audio/sound_beta_2/voice/char_1012_skadi2.ab 文件。提取其中的全部音频(共35个)。

    Shader 编写

    TODO

    ShaderLab
    Shader "Unlit/AlphaSplit" { Properties { [NoScaleOffset] _MainTex ("Texture(Main)", 2D) = "white" {} [NoScaleOffset] _AlphaTex ("Texture(Alpha)", 2D) = "white" {} _Color("Color", Color) = (1, 1, 1, 1) } SubShader { Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" } LOD 100 Fog { Mode Off } Cull Off ZWrite Off Blend One OneMinusSrcAlpha Lighting Off Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; fixed4 color : COLOR; }; struct v2f { float4 vertex : SV_POSITION; float2 uv : TEXCOORD0; fixed4 color : TEXCOORD1; }; sampler2D _MainTex; sampler2D _AlphaTex; fixed4 _Color; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = v.uv; o.color = v.color; return o; } fixed4 frag (v2f i) : SV_Target { fixed4 color = tex2D(_MainTex, i.uv); color.a = tex2D(_AlphaTex, i.uv).r; return color * _Color * i.color; } ENDCG } } }

    控制代码

    TODO

    C#
    using System; using System.Collections.Generic; using Spine; using Spine.Unity; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; using Random = UnityEngine.Random; [RequireComponent(typeof(RawImage))] public class DynIllustChar : MonoBehaviour, IPointerClickHandler { [Serializable] public struct VoiceData { public AudioClip Audio; [TextArea(5, 8)] public string Text; } [Header("Configs")] [SerializeField] private Vector2Int m_ScreenResolution = new Vector2Int(1920, 1080); [Header("References")] [SerializeField] private SkeletonAnimation m_Animation; [SerializeField] private Camera m_CharRenderCamera; [SerializeField] private AudioSource m_VoiceAudioSource; [SerializeField] private Text m_VoiceText; [Header("Animations")] [SpineAnimation(dataField = "m_Animation")] public string IdleAnimation = "Idle"; [SpineAnimation(dataField = "m_Animation")] public string InteractAnimation = "Interact"; [SpineAnimation(dataField = "m_Animation")] public string SpecialAnimation = "Special"; [Header("Voices")] public VoiceData StartVoice; public VoiceData IdleVoice; public List<VoiceData> InteractVoices; private RenderTexture m_CharRT; private bool m_IsPlayingInteract; private float m_LastTimePlaySpecial; private float m_LastTimeInteract; private void Start() { m_CharRT = RenderTexture.GetTemporary(m_ScreenResolution.x, m_ScreenResolution.y, 0, RenderTextureFormat.BGRA32, RenderTextureReadWrite.sRGB); m_CharRenderCamera.targetTexture = m_CharRT; GetComponent<RawImage>().texture = m_CharRT; m_IsPlayingInteract = false; m_LastTimePlaySpecial = Time.time; m_LastTimeInteract = Time.time; m_Animation.AnimationState.End += (TrackEntry trackEntry) => { if (trackEntry.Animation.Name == InteractAnimation) { m_IsPlayingInteract = false; } }; m_Animation.AnimationState.Complete += (TrackEntry trackEntry) => { float time = Time.time; if (time - m_LastTimePlaySpecial >= 8 && Random.value < 0.3f) { m_LastTimePlaySpecial = time; PlayAnimation(SpecialAnimation); } }; PlayVoice(StartVoice); } private void OnDestroy() { RenderTexture.ReleaseTemporary(m_CharRT); } private void Update() { float time = Time.time; if (time - m_LastTimeInteract >= 5 * 60) { m_LastTimeInteract = time; PlayVoice(IdleVoice); } } void IPointerClickHandler.OnPointerClick(PointerEventData eventData) { if (m_IsPlayingInteract) { return; } m_IsPlayingInteract = true; m_LastTimeInteract = Time.time; int index = Random.Range(0, InteractVoices.Count); VoiceData voice = InteractVoices[index]; PlayVoice(voice); PlayAnimation(InteractAnimation); } private void PlayVoice(VoiceData voice) { if (m_VoiceAudioSource.isPlaying) { m_VoiceAudioSource.Stop(); } m_VoiceAudioSource.clip = voice.Audio; m_VoiceAudioSource.Play(); m_VoiceText.text = voice.Text; } private void PlayAnimation(string animationName) { m_Animation.AnimationState.SetAnimation(0, animationName, false); m_Animation.AnimationState.AddAnimation(0, IdleAnimation, true, 0); Debug.Log("Play " + animationName); } }



    点击此处可投喂作者 (っ´▽`)っ

    感谢!您的名字将被记录于「」~

    微信赞赏码

    微信