[software development] 【技术分享】如何在 Unity URP 中实现遮挡半透明效果
Tofloor
poster avatar
柚子
deepin
2024-05-11 22:27
Author
最近在专研Unity,实现了一个小效果,跟大家分享一下


前言

在许多游戏中都会遇到人物被场景或者其他物品遮挡的问题,遮挡半透是游戏中非常常用的处理遮挡的手段。 这里介绍一种使用摄像机脚本和URP Shader实现遮挡半透明效果的方法。

效果预览

实现步骤

1. 创建Shader

在Unity中创建一个Shader,并将其命名为“TransparentColorURP”,然后在Shader中添加以下代码:

Shader "Custom/TransparentColorURP" {
    // 属性块定义了可以在材质编辑器中调整的参数
	Properties {
		_Color("Color Tint", Color) = (1,1,1,1)
		_MainTex("Main Tex", 2D) = "white" {}
		_AlphaScale("Alpha Scale", Range(0, 1)) = 0.5 // 透明度缩放,控制材质的透明度程度
	}
 
	SubShader {
		Tags {
			"RenderPipeline"="UniversalPipeline"
			"Queue"="Transparent" "IngoreProjector"="True" "RenderType"="Transparent"
		}
 
		Pass {
			ZWrite On // 开启深度写入
			ColorMask 0 // 关闭颜色通道写入
		}
 
 
		Pass {
 
			Tags {"LightMode"="UniversalForward"}
 
			ZWrite Off
			Blend SrcAlpha OneMinusSrcAlpha // 设置混合模式,基于源Alpha值的透明度混合
			
			HLSLPROGRAM
 
			#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
			#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
 
			#pragma vertex vert
			#pragma fragment frag
 
CBUFFER_START(UnityPerMaterial)
			half4 _Color;
			float4 _MainTex_ST;
			float _AlphaScale;
CBUFFER_END
 
			TEXTURE2D(_MainTex);
			SAMPLER(sampler_MainTex);
 
			struct a2v {
				float4 vertex : POSITION;
				half3 normal : NORMAL;
				float4 texcoord : TEXCOORD0;
			};
 
			struct v2f {
				float4 pos : SV_POSITION;
				half3 worldNormal : TEXCOORD0;
				half3 worldPos : TEXCOORD1;
				float2 uv : TEXCOORD2;
			};
 
			v2f vert(a2v i) {
				v2f o;
				o.pos = TransformObjectToHClip(i.vertex.xyz);
				o.worldPos = mul(UNITY_MATRIX_M, i.vertex).xyz;
				o.worldNormal = TransformObjectToWorldNormal(i.normal);
				o.uv = i.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
 
				return o;
			}
 
			half4 frag(v2f i) : SV_Target {
				half3 worldNormal = normalize(i.worldNormal);
				half3 worldLightDir = normalize(_MainLightPosition.xyz);
 
				half4 texColor = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);
				half3 albedo = texColor.rgb * _Color.rgb;
				half3 ambient = half3(unity_SHAr.w, unity_SHAg.w, unity_SHAb.w) * albedo;
				half3 diffuse = _MainLightColor.rgb * albedo * saturate(dot(worldLightDir, worldNormal));
				return half4(ambient + diffuse, texColor.a * _AlphaScale);
			}
 
			ENDHLSL
		}
	}
	FallBack "Univeral Render Pipeline/Simple Lit"
}

这个 shader 通过透明度叠加的方式实现了半透明效果,并且透明度可调。

2. 创建摄像机脚本

在Unity中创建一个脚本,并将其命名为“TransparentControl”,然后在脚本中添加以下代码:

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

public class TransparentControl : MonoBehaviour
{
    public class TransparentParam
    {
        public Material[] materials;
        public Material[] sharedMats;
        public float currentFadeTime = 0;
        public bool isTransparent = true;
    }

    public Transform targetObject;   //目标对象
    public float height = 3.0f;             //目标对象Y方向偏移
    public float destTransparent = 0.2f;    //遮挡半透的最终半透强度,
    public float fadeInTime = 1.0f;         //开始遮挡半透时渐变时间
    public LayerMask occlusionLayers;       // 定义哪些层会被视为遮挡层
    private Dictionary transparentDic = new();
    private readonly List clearList = new();

    private Vector3 lastTargetPos;

    void Update()
    {
        if (targetObject == null)
            return;

        // 仅当目标位置改变或超过更新间隔时更新射线投射
        if (targetObject.position != lastTargetPos)
        {
            lastTargetPos = targetObject.position;
            UpdateRayCastHit();
        }
        UpdateTransparentObject();
    }

    void UpdateTransparentObject()
    {
        foreach (var kvp in transparentDic)
        {
            TransparentParam param = kvp.Value;
            param.isTransparent =false;

            // 实现渐入半透明效果,避免太过突兀
            foreach (var mat in param.materials)
            {
                float destAlphaScale = destTransparent;
                float currentAlphaScale = mat.GetFloat("_AlphaScale");
                float newAlphaScale = Mathf.MoveTowards(currentAlphaScale, destAlphaScale, Time.deltaTime / fadeInTime);
                mat.SetFloat("_AlphaScale", newAlphaScale);
            }
        }
    }

    void RemoveUnuseTransparent()
    {
        clearList.Clear();
        foreach (var kvp in transparentDic)
        {
            if (!kvp.Value.isTransparent)
            {
                kvp.Key.materials = kvp.Value.sharedMats;
                clearList.Add(kvp.Key);
            }
        }
        foreach (var renderer in clearList)
            transparentDic.Remove(renderer);
    }

    void UpdateRayCastHit()
    {
        RaycastHit[] rayHits;
        //视线方向为从自身(相机)指向目标位置
        Vector3 targetPos = targetObject.position + new Vector3(0, height, 0);
        Vector3 viewDir = (targetPos - transform.position).normalized;

        RaycastHit HitInfo;
        Physics.Raycast(transform.position, viewDir, out HitInfo);

        // 假如角色没有被遮挡,则删除半透明效果
        if (HitInfo.transform.tag == "Player")
        {
            RemoveUnuseTransparent();
        }
        // 角色被遮挡时,获取所有遮挡物,并设置为透明
        else
        {
            Vector3 oriPos = transform.position;
            float distance = Vector3.Distance(oriPos, targetPos);
            Ray ray = new Ray(oriPos, viewDir);
            rayHits = Physics.RaycastAll(ray, distance, occlusionLayers);
            // 直接在Scene画一条线,方便观察射线
            Debug.DrawLine(oriPos, targetPos, Color.red);

            foreach (var hit in rayHits)
            {
                Renderer[] renderers = hit.collider.GetComponentsInChildren();
                foreach (Renderer r in renderers)
                {
                    AddTransparent(r);
                }
            }
        }
    }

    void AddTransparent(Renderer renderer)
    {
        TransparentParam param;
        if (!transparentDic.TryGetValue(renderer, out param))
        {
            param = new TransparentParam();
            transparentDic.Add(renderer, param);
            // 此处顺序不能反,调用material会产生材质实例。
            param.sharedMats = renderer.sharedMaterials;
            param.materials = renderer.materials;
            foreach (var v in param.materials)
            {
                v.shader = Shader.Find("Custom/TransparentColorURP");
            }
        }
        param.isTransparent = true;
    }
}

这个脚本从摄像机向目标对象发射一条射线,并检测是否与遮挡物发生碰撞,如果碰撞则将该物体设置为透明。 如果射线直达目标位置,则说明角色没有被遮挡,删除半透明效果。

3. 绑定摄像机脚本

将摄像机脚本绑定到摄像机上,并将目标对象设置为脚本的属性。 并按需设置高度和遮挡层。

原理解析

Shader 原理

Shader 的主要功能是将材质的颜色、透明度、光照等参数映射到物体的表面上,使得物体看起来更逼真、更有质感。

在这个 shader 中,我们使用了混合模式 Blend SrcAlpha OneMinusSrcAlpha,即基于源Alpha值的透明度混合。

在计算机图形学中,混合模式(Blend Mode)是指在渲染过程中如何将源颜色(Source Color)和目标颜色(Destination Color)结合起来。SrcAlphaOneMinusSrcAlpha是混合方程中使用的两个因子,它们分别代表了源颜色的透明度(Alpha)和源颜色不透明度。 在Unity等图形引擎中,混合模式通常用于控制透明度渲染。当使用SrcAlphaOneMinusSrcAlpha作为混合因子时,混合方程如下:

最终颜色 = 源颜色 * SrcAlpha + 目标颜色 * OneMinusSrcAlpha

这里的SrcAlpha是源颜色的Alpha值,而OneMinusSrcAlpha是1减去源颜色的Alpha值。Alpha值是一个介于0和1之间的数,其中0表示完全透明,1表示完全不透明。 具体来说:

  • SrcAlpha:使用源颜色的Alpha值作为混合因子,这意味着源颜色的不透明度越高,其对最终颜色的影响就越大。
  • OneMinusSrcAlpha:使用1减去源颜色的Alpha值作为混合因子,这意味着目标颜色的影响随着源颜色的不透明度增加而减少。 这种混合模式通常用于实现半透明效果,如玻璃、水、烟雾等。在这种情况下,源颜色(例如烟雾的颜色)会根据其Alpha值与背景(目标颜色)混合,从而产生正确的半透明视觉效果。

脚本原理

这个Unity脚本TransparentControl用于控制3D场景中物体的透明度,以实现遮挡时物体逐渐变为半透明效果。脚本通过射线投射来判断目标对象是否被其他物体遮挡,并据此调整透明度。以下是该脚本的原理解析:

  1. 定义与变量:
    • TransparentParam类:一个自定义类,用于存储与透明度相关的参数,包括材质数组、当前淡入时间和透明状态。
    • targetObject:需要关注的对象,通常是一个角色或重要的场景元素。
    • height:目标对象在Y轴的偏移量,用于调整射线投射的起点。
    • destTransparent:目标对象被遮挡时应达到的透明度。
    • fadeInTime:目标对象变为半透明状态的时间。
    • occlusionLayers:定义哪些层级的物体可以被视为遮挡物。
    • transparentDic:一个字典,用于存储每个渲染器的透明度参数。
    • clearList:一个列表,用于存储不再透明的渲染器,以便从字典中移除。
  2. Update方法:
    • 检查targetObject是否为空,若为空则不进行任何操作。
    • 如果目标对象的位置发生了变化,则更新射线投射。
  3. UpdateTransparentObject方法:
    • 遍历transparentDic中的每个渲染器,将其材质的透明度参数逐渐调整至目标值,实现淡入效果。
  4. RemoveUnuseTransparent方法:
    • 清除clearList
    • 遍历transparentDic,如果某个渲染器不再透明,则将其从字典中移除,并恢复其原始材质。
  5. UpdateRayCastHit方法:
    • 从摄像机位置向目标对象发射射线。
    • 如果射线击中的是标签为“Player”的对象,说明目标没有被遮挡,调用RemoveUnuseTransparent移除半透明效果。
    • 如果目标被遮挡,获取所有遮挡物,并调用AddTransparent使它们变为半透明。
  6. AddTransparent方法:
    • 如果渲染器不在transparentDic中,将其添加到字典中,并设置其材质为指定的透明度着色器。
    • 标记该渲染器为透明状态。 整体而言,这个脚本通过射线检测来识别目标对象是否被遮挡,并动态调整遮挡物的透明度,从而创造出一种视觉上的半透明效果。这在游戏和3D可视化中是一种常用的技术,可以增强场景的深度感和真实感。
Reply Favorite View the author
All Replies
阿尼樱奈奈
Moderator
2024-05-11 22:41
#1
like
Reply View the author