1 前言
角色在地图上移动到岩石后面时,完全被岩石遮挡,玩家只能看到岩石。这逻辑看起来没问题,但并不是玩家想要看到的画面,玩家更希望关注角色的状态
为了避免角色被遮挡,可以使用 Cinemachine Collider 功能,虚拟相机会自动避开障碍物,或者将角色和摄像机之间的障碍物做半透明处理,这两种方式的原理都是利用物理射线
这里使用障碍物半透明的方案,利用物理射线,检测角色和摄像机之间指定 Layer 的障碍物,改变其透明度,角色移动后,恢复其原本的透明度
2 设置 Layer
场景中的对象类型多种多样,我们仅希望岩石、树木等环境中的物体遮挡角色时,才改变其透明度。因此需要将对象分为不同的类型,利用 Unity 的 Layer 功能可以轻松实现该需求
Layer 定义哪些游戏对象可以与不同的功能以及彼此交互。它们主要有两种用途:由摄像机用来仅渲染场景的某一部分;由光源用来仅照亮场景的某些部分。但是,层也可以供射线投射用于选择性地忽略碰撞体或创建碰撞。更多信息请参阅文档:「https://docs.unity3d.com/cn/2021.2/Manual/Layers.html」
1 添加 Layer
这里偷个懒,只创建一个 Environment 层,就不再细分了
2 分配 Layer
将 Free_Forest 及其子对象均设置为 Environment 层
3 射线检测
在场景中从角色向摄像机投射一条射线,获取 Environment 层中所有命中的对象
脚本中获取 Environment 层的方式有两种
var layerMask = LayerMask.GetMask("Environment");
或者
var layerMask = 1 << LayerMask.NameToLayer("Environment");
通过角色和摄像机的位置计算出射线投射的方向和距离,利用 Physics.RaycastNonAlloc 来获取射线命中对象,相比 Physics.RaycastAll,此函数不会产生任何垃圾,毕竟在 FixedUpdate 中进行射线检测,应当尽量减少性能损耗
var size = Physics.RaycastNonAlloc(selfPosition, direction, this._raycastHits, rayDistance, layerMask);
获取命中对象中所有子节点的材质集合,和上一次命中的材质集合对比,改变其透明度
private void TransparentObjects()
{
Vector3 selfPosition = transHead.position;
Vector3 cameraPosition = _cameraTrans.position;
var rayDistance = Vector3.Distance(selfPosition, cameraPosition);
Vector3 direction = Vector3.Normalize(cameraPosition - selfPosition);
Debug.DrawLine(selfPosition, cameraPosition, Color.red);
var layerMask = LayerMask.GetMask("Environment");
var size = Physics.RaycastNonAlloc(selfPosition, direction, this._raycastHits, rayDistance, layerMask);
List<Material> materials = new List<Material>();
for (int i = 0; i < size; i++)
{
var meshRenderers = this._raycastHits[i].collider.GetComponentsInChildren<MeshRenderer>();
foreach (var variable in meshRenderers)
{
materials.AddRange(variable.materials);
}
}
var transparentList = materials.Except(_materialList).ToList();
var opaqueList = _materialList.Except(materials).ToList();
foreach (var variable in transparentList)
{
MaterialTransparent.SetMaterialTransparent(true, variable, 0.22f);
}
foreach (var variable in opaqueList)
{
MaterialTransparent.SetMaterialTransparent(false, variable);
}
_materialList = materials;
}
4 更改透明度
由于性能等因素,默认情况下 3D 模型的材质是不支持更改透明度的。需要将材质的 Rendering Mode(对于内置渲染管线)或 Surface Type(对于 URP 或 HDRP)设置为 Transparent,才可以调整透明度相关的属性
这里我们需要更改的属性主要为:
-
Surface Type:控制材质是否支持透明度,更多信息,请参阅文档: 「https://docs.unity3d.com/cn/Packages/com.unity.render-pipelines.high-definition@7.4/manual/Surface-Type.html」
-
Opaque:模拟没有光线穿透的全实体材质。
-
Transparent:模拟光线可以穿透的半透明材质,例如透明塑料或玻璃。选择 Transparent 会在 Surface Options 部分中显示更多属性,还会显示一个额外的 Transparency Inputs 部分
-
-
Blend:确定 GPU 如何将片元着色器的输出与渲染目标进行合并,更多信息请参阅文档: 「https://docs.unity3d.com/cn/current/Manual/SL-Blend.html」
通过脚本更改 Surface Type 和 Blend 属性:
public class MaterialTransparent
{
private enum SurfaceType
{
Opaque,
Transparent
}
private enum BlendMode
{
Alpha,
Premultiply,
Additive,
Multiply
}
public static void SetMaterialTransparent(bool transparent, Material material, float alpha = 1)
{
if (transparent)
{
material.SetFloat("_Surface", (float)SurfaceType.Transparent);
material.SetFloat("_Blend", (float)BlendMode.Alpha);
}
else
{
material.SetFloat("_Surface", (float)SurfaceType.Opaque);
}
SetupMaterialBlendMode(material);
Color color = material.color;
color.a = alpha;
material.color = color;
}
private static void SetupMaterialBlendMode(Material material)
{
if (material == null)
{
return;
}
bool alphaClip = material.GetFloat("_AlphaClip") == 1;
if (alphaClip)
{
material.EnableKeyword("_ALPHATEST_ON");
}
else
{
material.DisableKeyword("_ALPHATEST_ON");
}
SurfaceType surfaceType = (SurfaceType)material.GetFloat("_Surface");
if (surfaceType == 0)
{
material.SetOverrideTag("RenderType", "");
material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.One);
material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.Zero);
material.SetInt("_ZWrite", 1);
material.DisableKeyword("_ALPHAPREMULTIPLY_ON");
material.renderQueue = -1;
material.SetShaderPassEnabled("ShadowCaster", true);
}
else
{
BlendMode blendMode = (BlendMode)material.GetFloat("_Blend");
switch (blendMode)
{
case BlendMode.Alpha:
material.SetOverrideTag("RenderType", "Transparent");
material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha);
material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
material.SetInt("_ZWrite", 0);
material.DisableKeyword("_ALPHAPREMULTIPLY_ON");
material.renderQueue = (int)UnityEngine.Rendering.RenderQueue.Transparent;
material.SetShaderPassEnabled("ShadowCaster", false);
break;
case BlendMode.Premultiply:
material.SetOverrideTag("RenderType", "Transparent");
material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.One);
material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
material.SetInt("_ZWrite", 0);
material.EnableKeyword("_ALPHAPREMULTIPLY_ON");
material.renderQueue = (int)UnityEngine.Rendering.RenderQueue.Transparent;
material.SetShaderPassEnabled("ShadowCaster", false);
break;
case BlendMode.Additive:
material.SetOverrideTag("RenderType", "Transparent");
material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.One);
material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.One);
material.SetInt("_ZWrite", 0);
material.DisableKeyword("_ALPHAPREMULTIPLY_ON");
material.renderQueue = (int)UnityEngine.Rendering.RenderQueue.Transparent;
material.SetShaderPassEnabled("ShadowCaster", false);
break;
case BlendMode.Multiply:
material.SetOverrideTag("RenderType", "Transparent");
material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.DstColor);
material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.Zero);
material.SetInt("_ZWrite", 0);
material.DisableKeyword("_ALPHAPREMULTIPLY_ON");
material.renderQueue = (int)UnityEngine.Rendering.RenderQueue.Transparent;
material.SetShaderPassEnabled("ShadowCaster", false);
break;
}
}
}
}
5 透明效果
最终效果如下图所示,角色走到岩石后面时,岩石呈现半透明状态