残影效果在URP中的实现及性能优化

残影效果概述

残影效果是许多游戏中常见的效果,但要获得较好的效果往往需要创建出多个游戏对象,造成较大的性能负担。
本文将介绍一种对象池残影附带一些优化策略来探讨残影效果的实现。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Jmo1ijFO-1676973325118)(https://note.youdao.com/yws/res/1110/WEBRESOURCE08f25ebaf065fe83f8eba2a6e46a3376)]

对象池创建对象

残影效果主要的消耗在于两个方面,第一个方面就是物体的动态创建和删除。下文将简单地运用对象池技术,对残影对象进行管理。
当玩家经过一定时间的移动时残影的数量达到最大时,不再需要动态创建新的对象,而是取出对象池中已有的物体,重新设置他的position和rotation到新的残影生成位置,对这个对象进行复用来避免冗余的创建和删除操作。
对象池的实现使用一个queue容器来实现,他具有先进先出的特性,比较符合需求。

残影数量未达到最大

当残影数量未达到最大时,动态地生成对象,代码如下:

if (m_GhostInstances.Count < ghostObjCount)
{
    GhostInstance instance = new GhostInstance();
    instance.gameObj = Instantiate(ghostObject, curTransform.position, curTransform.rotation);
    
    //Delete ghost effect component of the clone game object
    var ghostEffect = instance.gameObj.GetComponent<GhostEffect>();
    if(ghostEffect) DestroyImmediate(ghostEffect);
    instance.instanceLifeTime = ghostEffectLifeTime;
}

这里需要注意,要删除生成对象上的ghost effect脚本,来避免生成的对像又生成出新的残影。

残影数量达到最大

var reuseObj = m_GhostInstances.Dequeue();
if (reuseObj == null || reuseObj.gameObj == null) return;
reuseObj.instanceLifeTime = ghostEffectLifeTime;
reuseObj.gameObj.transform.SetPositionAndRotation(curTransform.position, curTransform.rotation);
m_GhostInstances.Enqueue(reuseObj);

需要注意取出队列的物体需要重新加回队列来进行复用

对象池技术优化

本文使用的对象池还没有达到真正对象池使用的目的,其实完全可以在一开始创建好需要的残影,在运行时动态的enable和disable这些物体renderer,来进一步规避残影创建和销毁的CPU消耗

残影生成逻辑

有了基础的管理手段之后,残影生成的逻辑需要进一步实现。这里使用了时间作为残影生成的间隔:

//Check time interval
m_CurSumTime += Time.deltaTime;
if (m_CurSumTime < ghostCreateInterval) return;
m_CurSumTime -= ghostCreateInterval;
ghostCreateTimeRemaining -= ghostCreateInterval;

//Create ghost

当时间间隔达到之后,运行对象池创建物体的代码

生成残影的时机

在游戏中一般角色使用技能或者移动时会产生残影,当移动后突然停下,残影依然会持续创建一段时间,并慢慢停止。要实现这个逻辑,这里引入了一个持续计时的创建倒计时,当角色移动后,这个倒计时会刷新,并在这个倒计时里持续地运行创建残影的代码,当倒计时小于0的时候,我们开始根据设置的时间间隔销毁容器中的残影物体。

if (ghostCreateTimeRemaining <= 0)
{
    if (m_GhostInstances.Count > 0)
    {
        var reuseObj = m_GhostInstances.Dequeue();
        if (reuseObj != null && reuseObj.gameObj != null)
        {
            DestroyImmediate(reuseObj.gameObj);
        }
    }
    return;
}

残影材质处理

有了基础的残影效果之后,需要开始编写残影材质的独立shader,让残影效果看起来更COOL。
下面会简单给一个无光照的纯色shader来进行

Shader "GhostInstance"
{
    Properties
    {
	    [MainTexture] _BaseMap("Albedo", 2D) = "white" {}
        [MainColor] _BaseColor("Color", Color) = (1,1,1,1)
        _Alpha("Alpha", Range(0,1)) = 1
    }
    
    SubShader
    {
        LOD 100
        Tags 
        {
            "RenderType" = "Opaque"
            "RenderPipeline" = "UniversalPipeline"
            "Queue"="Geometry"
            "IgnoreProjector" = "True"
            "ShaderModel"="2.0"
        }

        HLSLINCLUDE

        #include "Packages/com.bilibili.burp/ShaderLibrary/Core.hlsl"
        #pragma target 3.0
        #pragma only_renderers gles3 glcore d3d11 metal vulkan

        //Cbuffer declared for shared by various passes
        CBUFFER_START(UnityPerMaterial)
            half4 _BaseColor;
            half4 _BaseMap_ST;

            half _Alpha;
        CBUFFER_END

        TEXTURE2D(_BaseMap);            SAMPLER(sampler_BaseMap);
        
        ENDHLSL
        
        Pass
        {
            Name "ForwardPass"
            Tags 
			{
			    "RenderType" = "Transparent" 
				"LightMode"="UniversalForward"
			    "Queue"="Transparent"
            }
            
            Blend SrcAlpha OneMinusSrcAlpha
            ZWrite Off
            Cull Back
    	    Ztest LEqual
        	
            HLSLPROGRAM

            #pragma multi_compile_instancing
            
            #pragma vertex ForwardPassVert
			#pragma fragment ForwardPassFrag

            #pragma target 3.0
            #pragma only_renderers gles3 glcore d3d11 metal vulkan
            
            struct Attributes
            {
                float4 positionOS	            : POSITION;
                float2 uv                       : TEXCOORD0;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct Varyings
            {
                float4 positionCS	            : SV_POSITION;
                float2 uv                       : TEXCOORD0;
                UNITY_VERTEX_INPUT_INSTANCE_ID
                UNITY_VERTEX_OUTPUT_STEREO
            };

            Varyings ForwardPassVert ( Attributes input )
            {
                Varyings output = (Varyings)0;

                UNITY_SETUP_INSTANCE_ID(input);
                UNITY_TRANSFER_INSTANCE_ID(input, output);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);
                
                output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
                output.uv = TRANSFORM_TEX(input.uv, _BaseMap);
                return output;
            }

            half4 ForwardPassFrag(Varyings input)  : SV_TARGET
            {
                UNITY_SETUP_INSTANCE_ID(input);
                UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
                
                half4 finalColor = 0;
                half4 baseColor = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv) * _BaseColor;
                finalColor += baseColor;
                finalColor.a *= _Alpha;
                return finalColor;
            }
            
            ENDHLSL
        }
    }
}

其中,alpha和color将会在脚本中进行动态的控制.

残影材质赋予

将残影的每个renderer的material的设置为override shader中赋予的shader生成的材质,必须保证他们都是独立的材质,这样才能分别控制他们的参数。
这里不能够使用material property block。因为其会中断srp合批,不如多创建点材质。对性能的影响较小。
这里可以参考unity官方的回答:
https://forum.unity.com/threads/an-analogue-of-the-material-property-block-in-urp.1193074/
参考代码如下:

//If use assign override shader
if (overrideShader != null)
{   
    //Get all renderers in gameobject
    var renderers = instance.gameObj.GetComponentsInChildren<Renderer>();
    foreach (var instanceRenderer in renderers)
    {
        //Assign override material to all shard material in renderer
        var newMats = new List<Material>(instanceRenderer.sharedMaterials.Length);
        foreach (var mat in newMats)
        {
            var newMat = new Material(overrideShader);
            
            //set color if feasible
            if (newMat.HasProperty(k_Color))
            {
                newMat.SetColor(k_Color, Color.red);
            }
            
            //set alpha if feasible
            if (newMat.HasProperty(k_Alpha))
            {
                newMat.SetFloat(k_Alpha, 1);
            }
            newMats.Add(newMat);
        }

        //Assign new material array to renderer
        instanceRenderer.sharedMaterials = newMats.ToArray();
    }
}

残影材质更新

引入一个每帧更新的函数来更新各个残影的材质:

private void UpdateGhostInstances()
{
    foreach (var ghostInstance in m_GhostInstances)
    {
        ghostInstance.instanceLifeTime -= Time.deltaTime;
        float alpha = ghostInstance.instanceLifeTime / ghostEffectLifeTime;
        ghostInstance.material.SetFloat(k_Alpha, alpha);
    }
}

这里可以根据需求填入想要的参数进行控制,并在shader中同步实现

残影动画匹配

由于没有使用赋值skin mesh renderer的方式实现,这里我们需要让模型做出和本体一样的动作并暂停。这里给出一个没有blend tree的动画state的情况下的简单处理方法:

//Get animator from player object
var createdAnimator = GetComponent<Animator>();

//Get animator from ghost object
var ghostAnimator = ghostObject.GetComponent<Animator>();
if (ghostAnimator == null) return null;
var ghostAnimatorController = ghostAnimator.runtimeAnimatorController as AnimatorOverrideController;
if (ghostAnimatorController == null)
	ghostAnimatorController = new AnimatorOverrideController(createdAnimator.runtimeAnimatorController);
ghostAnimator.runtimeAnimatorController = ghostAnimatorController;

//Get animation clips and state
var clips = createdAnimator.GetCurrentAnimatorClipInfo(0);
var state = createdAnimator.GetCurrentAnimatorStateInfo(0);
if (clips != null && clips.Length > 0)
{
	var clip = clips[0].clip;
	ghostAnimatorController[clip.name] = clip;

	ghostAnimator.speed = 0f;
	ghostAnimator.Play(clip.name, 0, state.normalizedTime);
	ghostAnimator.Update(0);
}

当animation state中包含blend tree时,这种情况处理起来需要按照逻辑,比如设置blend tree的parameter来让blend tree自动blend动画。

实例化绘制

残影效果的第二方面的性能压力来自于渲染。使用实例化绘制是在SRP BATCHER之外提高性能的优化手段。这里需要注意实例化绘制时材质的参数变化以及动画的更新。

其他优化策略

使用合并的低模来创建残影

通过降低顶点数计算的方式来降低残影的渲染性能压力,但现代GPU的瓶颈一般不在顶点着色器上,但将角色的各个mesh合并成一个完成的低模有助于降低渲染压力。

使用尽量简单的shader来绘制残影

片元着色器的渲染压力一般是造成渲染瓶颈的主要来源,因此残影材质的shader要尽量简单

参考

https://forum.unity.com/threads/an-analogue-of-the-material-property-block-in-urp.1193074/

猜你喜欢

转载自blog.csdn.net/jianfei_zhou/article/details/129147286