Ilingis 发表于 2022-4-14 13:51

Unity 交互草制作

介绍常见的交互草实现思路


[*]通过脚本传递交互物体的世界坐标到Shader中,如果要实现拖影的效果,可以维护数组的方式进行存储前几帧的世界坐标,然后衰减交互强度。
[*]通过后处理的方式,维护一张拖影深度RT,然后采样这张深度RT,转换成法线,以此进行交互。
方法二的效果图



不要在意草为什么这么丑

能够通过效果图看到,移动过的位置,草并没有里面停止,而是有一种被拨动的感觉,并且在逐渐的衰减。这就是为什么不用Line Rendering 或者 Trail Rendering的原因。
实现

场景设置


[*]正交相机
暂时假设 物体是相机的父物体!!!! 后续会为了适配项目修改




后处理获得拖影深度RT

思考如何实现随着时间变换,以前的深度变小,来实现拖影的效果。
第一个想法是冯乐乐入门精要后处理部分教的Pingpong Buffer的方式。
效果图


Shader
UNITY_DECLARE_DEPTH_TEXTURE(_CameraDepthTexture);
float4 _CameraDepthTexture_TexelSize;
//上一帧的RT
sampler2D _LastFrame;
//同一像素上一帧和当前帧的世界坐标距离
float3 _PlayerPosOffset;
float _Size;
float _Velocity;

Vert:
    o.uv = v.uv;
    o.vertex = UnityObjectToClipPos(v.vertex);
    //因为当前我们假设的物体和相机一起移动那么 uv采样的时候也需要移动来修正这个差距。
    //不让相机会一直觉得物体并没有移动
    //不
    o.offsetUV = v.uv + _PlayerPosOffset.xz * _Size;

Frag:
    //检测平台图形API
    #if UNITY_UV_STARTS_AT_TOP
    if(_CameraDepthTexture_TexelSize.y<0)
      i.uv.y = 1 - i.uv.y;
    #endif

    //当前深度
    float d = UNITY_SAMPLE_DEPTH(tex2D(_CameraDepthTexture,i.uv ).r);
    //上一帧的RT
    float4 lastFrame = tex2D(_LastFrame,i.offsetUV );
    float3 final = float3(0,0,0);
    //R 后续会介绍 具体干什么
    final.r = 0;
    //G记录 深度衰减
    final.g = max(d,tex.g - _Velocity);
Post.cs
//一切常规的 数据初始化,具体看冯乐乐入门精要。 只写我这里特有的
private Vector3 previousPlayerPos;
private Vector3 currentPlayerPos;
public float size;
public float _Velocity;


OnRenderImage:
   if (material != null)
   {
       //Pingpong buffer,两个rt实现循环一次Shader。
       //目的:
             //第一次blit,初始化深度,这里可以优化,最后我会介绍方法。
             //第二次blit,因为第一次的blit后的RT作为了lastFrame,然后混合lastFrame和当前帧的深度,并且lastFrame会衰减,不要直接相加,最好max,防止爆掉。
             //多次blit,叠加效果,可以自行选择次数,当然一次blit就是一次DC,根据需要选择。
       RenderTexture buffer0 = RenderTexture.GetTemporary(src.width, src.height, 0);
       buffer0.filterMode = FilterMode.Bilinear;

       material.SetFloat("_Size", size);
       Shader.SetGlobalFloat("_Velocity", _Velocity);

       Graphics.Blit(src, buffer0,material,0);

       material.SetTexture("_LastFrame", buffer0);

       currentPlayerPos = camera.transform.position;
       Vector3 playerPosOffset = currentPlayerPos - previousPlayerPos;
       material.SetVector("_PlayerPosOffset", playerPosOffset);
       previousPlayerPos = currentPlayerPos;

       RenderTexture buffer1 = RenderTexture.GetTemporary(src.width, src.height, 0);
       buffer1.filterMode = FilterMode.Bilinear;
       Graphics.Blit(buffer0, buffer1, material, 0);
            
       RenderTexture.ReleaseTemporary(buffer0);
       buffer0 = buffer1;
            
            
       Graphics.Blit(buffer0, dest, material, 0);
       RenderTexture.ReleaseTemporary(buffer0);

   }
   else
       ...
为了通过这张RT让我们知道物体离开这个位置多久。我们需要添加时间属性。
添加时间属性。

Post.cs
....
Shader.SetGlobalFloat("_DeltaTime", Time.deltaTime);

OnRenderImage:
    ....
    Shader.SetGlobalFloat("_DeltaTime", Time.deltaTime);Shader
Frag:
...
    float3 final = float3(0,0,0);
    //R记录 是否来过这里
    final.r = max(d,tex.r);
    //G记录 深度衰减
    final.g = max(d,tex.g - _DeltaTime * _Velocity);效果图


我们就可以根据RG通道的差值然后除以速度,就可以获得时间啦,是不是很想初中物理呢。
运用到草上

随便做个切片草意思一下,反正我们要交互的效果而已啦。OvO
Post.cs
OnRenderImage:
//近远裁剪平面,_DepthToNormalVector:(相机左下角,宽,高)
    Shader.SetGlobalFloat("_Far", camera.farClipPlane);
    Shader.SetGlobalFloat("_Near", camera.nearClipPlane);
    GetDepthToNormalVector(camera);

void GetDepthToNormalVector(Camera camera)
{
    Vector3 leftBottom = camera.ScreenToWorldPoint(Vector3.zero);
    //和上面等价,获得相机世界坐标下的左下角
    //leftBottom = camera.transform.position + new Vector3(-camera.orthographicSize, 0, camera.orthographicSize);
    float width = (2 * camera.orthographicSize) * camera.aspect;
    float height = (2 * camera.orthographicSize) / camera.aspect;
    Vector4 depthToNormalVector = new Vector4(leftBottom.x, leftBottom.z, width, height);
    Shader.SetGlobalVector("_DepthToNormalVector", depthToNormalVector);
    }GrassShader
//就是我们生成的RT图
_DepthToNormal("Depth To Normal",2D) = "black" {}

Vert:
//核心顶点偏移部分,其他的就没什么可说了
float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
//因为是正交相机,直接减左下角,然后归一化,这里需要移动(0,1)才能够正常显示,应该是反转后出现的问题。
float2 DTNcoords = (worldPos.xz - _DepthToNormalVector.xy) / _DepthToNormalVector.zw + float2(0,1);
DTNcoords.y = 1 - DTNcoords.y;
//RG通道
float maxDepth = UNITY_SAMPLE_DEPTH(tex2Dlod(_DepthToNormal,float4(DTNcoords,0,0)).r);
float d = UNITY_SAMPLE_DEPTH(tex2Dlod(_DepthToNormal,float4(DTNcoords,0,0)).g);
float fzDistance = _Far - _Near;

//相邻两个点的深度,如同ddx,ddy一样求得梯度。
float dr = UNITY_SAMPLE_DEPTH(tex2Dlod(_DepthToNormal,float4(DTNcoords + float2(_DepthToNormal_TexelSize.x,0),0,0)).r);
float dt = UNITY_SAMPLE_DEPTH(tex2Dlod(_DepthToNormal,float4(DTNcoords + float2(0,-_DepthToNormal_TexelSize.y),0,0)).r);
//如下图,根据获得的两个方向 叉乘获得法线。
float3 rightDir = float3(_DepthToNormal_TexelSize.x,-(dr - d) * fzDistance, 0);
float3 downDir = float3(0,(dt - d) * fzDistance,_DepthToNormal_TexelSize.y);
               
o.normal = normalize(cross(rightDir,downDir));


//最后叠加的世界坐标上
//当前点的时间函数
float time = max(0.0001,(maxDepth - d) / (0.005f * _Velocity));
//方向 * 强度 * 时间影响如下下图 因为sin(x) / x 正好符合我时间越久,摇摆强度越小的需求。
worldPos.z +=o.normal.z * _InteractionIntensity* sin(time) / time;
worldPos.x +=o.normal.x * _InteractionIntensity* sin(time) / time;





法线转换的效果图



关于相机跟随的问题

最后选择相机和物体分离,这样的好处在于相机按照一格一格的移动,保证了我们只有在有草地(或者需要交互)的时候才会用到。
优化思路


[*]使用RenderWithShader 来实现虚拟相机渲染,不走相机渲染的路线,这样就会少掉很多DC,可能手机端都能够使用。当然这样我们就需要用一个pass\shader来做深度图。
[*]控制RT的质量,毕竟草交互,用不着那么高精度的贴图。
最后找找22暑假实习,本人大三

QQ:2422538491 (邮箱同)
页: [1]
查看完整版本: Unity 交互草制作