前言
在许多游戏中都会遇到人物被场景或者其他物品遮挡的问题,遮挡半透是游戏中非常常用的处理遮挡的手段。
这里介绍一种使用摄像机脚本和 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<Renderer, TransparentParam> transparentDic = new();
private readonly List<Renderer> 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<Renderer>();
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)结合起来。SrcAlpha
和OneMinusSrcAlpha
是混合方程中使用的两个因子,它们分别代表了源颜色的透明度(Alpha)和源颜色不透明度。
在 Unity 等图形引擎中,混合模式通常用于控制透明度渲染。当使用SrcAlpha
和OneMinusSrcAlpha
作为混合因子时,混合方程如下:
最终颜色 = 源颜色 * SrcAlpha + 目标颜色 * OneMinusSrcAlpha
这里的SrcAlpha
是源颜色的 Alpha 值,而OneMinusSrcAlpha
是 1 减去源颜色的 Alpha 值。Alpha 值是一个介于 0 和 1 之间的数,其中 0 表示完全透明,1 表示完全不透明。
具体来说:
SrcAlpha
:使用源颜色的 Alpha 值作为混合因子,这意味着源颜色的不透明度越高,其对最终颜色的影响就越大。OneMinusSrcAlpha
:使用 1 减去源颜色的 Alpha 值作为混合因子,这意味着目标颜色的影响随着源颜色的不透明度增加而减少。
这种混合模式通常用于实现半透明效果,如玻璃、水、烟雾等。在这种情况下,源颜色(例如烟雾的颜色)会根据其 Alpha 值与背景(目标颜色)混合,从而产生正确的半透明视觉效果。
脚本原理
这个 Unity 脚本TransparentControl
用于控制 3 D 场景中物体的透明度,以实现遮挡时物体逐渐变为半透明效果。脚本通过射线投射来判断目标对象是否被其他物体遮挡,并据此调整透明度。以下是该脚本的原理解析:
- 定义与变量:
TransparentParam
类:一个自定义类,用于存储与透明度相关的参数,包括材质数组、当前淡入时间和透明状态。targetObject
:需要关注的对象,通常是一个角色或重要的场景元素。height
:目标对象在 Y 轴的偏移量,用于调整射线投射的起点。destTransparent
:目标对象被遮挡时应达到的透明度。fadeInTime
:目标对象变为半透明状态的时间。occlusionLayers
:定义哪些层级的物体可以被视为遮挡物。transparentDic
:一个字典,用于存储每个渲染器的透明度参数。clearList
:一个列表,用于存储不再透明的渲染器,以便从字典中移除。
- Update 方法:
- 检查
targetObject
是否为空,若为空则不进行任何操作。 - 如果目标对象的位置发生了变化,则更新射线投射。
- 检查
- UpdateTransparentObject 方法:
- 遍历
transparentDic
中的每个渲染器,将其材质的透明度参数逐渐调整至目标值,实现淡入效果。
- 遍历
- RemoveUnuseTransparent 方法:
- 清除
clearList
。 - 遍历
transparentDic
,如果某个渲染器不再透明,则将其从字典中移除,并恢复其原始材质。
- 清除
- UpdateRayCastHit 方法:
- 从摄像机位置向目标对象发射射线。
- 如果射线击中的是标签为“Player”的对象,说明目标没有被遮挡,调用
RemoveUnuseTransparent
移除半透明效果。 - 如果目标被遮挡,获取所有遮挡物,并调用
AddTransparent
使它们变为半透明。
- AddTransparent 方法:
- 如果渲染器不在
transparentDic
中,将其添加到字典中,并设置其材质为指定的透明度着色器。 - 标记该渲染器为透明状态。
整体而言,这个脚本通过射线检测来识别目标对象是否被遮挡,并动态调整遮挡物的透明度,从而创造出一种视觉上的半透明效果。这在游戏和 3 D 可视化中是一种常用的技术,可以增强场景的深度感和真实感。
- 如果渲染器不在
后记
看到这里,你应该已经实现了遮挡半透明效果,但你有没有发现,当物体半透明时,它的阴影缺失了。
没关系,下篇文章将介绍如何解决该问题!