본문 바로가기

Unity 공부시작/Unity3D 연습

[ Unity3D 잔상 효과 ] SkinnedMeshRenderer AfterImage Effect

 잔상을 남기려는 오브젝트의 Renderer가 SkinnedMeshRenderer인 경우에 가능한 방법입니다.

 

단계는 아래와 같습니다.

 

1. SkinnedMeshRenderer.BakeMesh( Mesh mesh ) 를 이용하여 오브젝트의 현재 상태를 저장한다.

2. 저장한 Mesh를 잔상으로 이용한다.

 

 

// 잔상 오브젝트에 필요한 것들

SkinnedMeshRenderer smr = gameObject.Getcomponent<SkinnedMeshRenderer>();

Material afterImageMaterial;

 

// 잔상 생성

Mesh mesh = new Mesh();

smr.BakeMesh(  mesh );

 

GameObject afterImageObj = new GameObject("AfterImage");

MeshFilter mf =  afterImageObj.AddComponent<MeshFilter>();

mf.mesh = mesh;

 

MeshRenderer mr = afterImageObj.AddComponent<MeshRenderer>();

mr.material = afterImageMaterial;

 

afterImageObj.transform.position = gameObject.transform.position;

afterImageObj.transform.rotation = gameObject.transform.rotation;

 

Destroy( afterImageObj, 1f );

 

 위의 코드는 잔상을 생성하는 과정을 쉽게 보여드리기 위한 코드입니다.

위와 같이 짜게 된다면 성능이 꽝인 것은 물론 잔상을 사라지게 하는 등 관리가 불편합니다.

 

 

[ AfterImage.cs ]

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Afterimage : MonoBehaviour
{
    Material m;
    MeshFilter mf = null;
    // GameObject afterImageObj = null;
    Coroutine fadeoutCoroutine = null;
    float originAlpha = 0f;
    public Mesh mesh { get { return mf.mesh; } }

    public void InitAfterImage(Material material)
    {
        // afterImageObj = new GameObject();
        MeshRenderer mr = gameObject.AddComponent<MeshRenderer>();

        m = new Material(material);
        originAlpha = m.color.a;
        mr.material = m;
        mf = gameObject.AddComponent<MeshFilter>();

        gameObject.SetActive(false);
    }

    public void CreateAfterImage(Vector3 position, Quaternion rot, float time)
    {
        if ( fadeoutCoroutine == null )
        {
            gameObject.SetActive(true);
            gameObject.transform.position = position;
            gameObject.transform.rotation = rot;

            mf.mesh = mesh;
            fadeoutCoroutine = StartCoroutine(FadeOut(time));
        }
    }

    IEnumerator FadeOut(float time)
    {
        while(time > 0f)
        {
            time -= Time.deltaTime;
            m.color = new Color(m.color.r, m.color.g, m.color.b, originAlpha * time);
            yield return null;
        }

        gameObject.SetActive(false);
        fadeoutCoroutine = null;
    }
}

 

[ SMRAfterImageCreator.cs ]

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SMRAfterImageCreator : MonoBehaviour
{
    [SerializeField]
    Material afterImageMaterial;

    SkinnedMeshRenderer smr;
    Afterimage[] afterImages;
    
    int afterImageCount;
    int currentAfterImageIndex;
    float remainAfterImageTime;
    float createAfterImagedelay;
    Coroutine createAfterImageCoroutine = null;

    bool isCreating = false;

    public void Setup(SkinnedMeshRenderer smr, int maxNumber, float remainTime)
    {
        this.smr = smr;
        afterImageCount = maxNumber;
        remainAfterImageTime = remainTime;
        createAfterImagedelay = remainAfterImageTime / (float)afterImageCount + 0.1f;

        CreateAfterImages();
    }

    void CreateAfterImages()
    {
        afterImages = new Afterimage[afterImageCount];
        for (int i = 0; i < afterImages.Length; ++i)
        {
            GameObject newObj = new GameObject();
            afterImages[i] = newObj.AddComponent<Afterimage>();
            afterImages[i].InitAfterImage(afterImageMaterial);
        }
    }

    public void Create(bool flag)
    {
        this.isCreating = flag;
        if ( flag )
        {
            if ( createAfterImageCoroutine == null )
                createAfterImageCoroutine = StartCoroutine(CreateAfterImageCoroution());
        }
    }

    IEnumerator CreateAfterImageCoroution()
    {
        float t = 0f;
        while ( isCreating )
        {
            t += Time.deltaTime;

            if (t >= createAfterImagedelay)
            {
                smr.BakeMesh(afterImages[currentAfterImageIndex].mesh);
                afterImages[currentAfterImageIndex].CreateAfterImage(transform.position, transform.rotation, remainAfterImageTime);
                currentAfterImageIndex = (currentAfterImageIndex + 1) % afterImageCount;
                t -= createAfterImagedelay;
            }
            yield return null;
        }

        createAfterImageCoroutine = null;
    }
}

 

[ 사용법 ]

    SMRAfterImageCreator aic;
    void CreateAfterImages()
    {
        aic = GetComponent<SMRAfterImageCreator>();
        aic.Setup(transform.GetComponentInChildren<SkinnedMeshRenderer>(), 5, 1.4f);
        actualSlowMotionCharge = maxSlowMotionCharge;
    }

SMRAfterImageCreator를 컴포넌트로 추가해주신 뒤, 위의 함수와 같이 셋업해주면 됩니다.

Setup의 인자로는 각각 SkinnedMeshRenderer, 생성할 총 잔상의 수, 잔상이 남아있을 시간 입니다.

 

셋업을 해두었다면, 잔상을 생성하고 싶을 때 aic.Create(true)를, 생성을 멈추고 싶다면 aic.Create(false)를 호출하면

됩니다.

 

[ 주의 사항 ]

 SkinnedMeshRenderer.BakeMesh( Mesh mesh ) 는 성능적으로 무거운 함수라고 되어 있습니다.

잔상을 생성하려면 런 타임에 BakeMesh를 지속적으로 호출 해주어야 하기 때문에 너무 많은 잔상은 성능에

무리가 많이갈 수 있을 것 같습니다.

 

 저는 5개 정도를 사용하고 있는데요, 제 핸드폰 갤럭시 S9+에서는 프레임 드랍없이 잘 돌아가는 것을 확인했습니다.