Unity 完美假阴影实现文档

Unity 完美假阴影实现文档(快照法)

1. 核心原理解析

  1. 投射(Caster): 物体底部的阴影面片不直接渲染到屏幕,而是渲染到一张专属的 RenderTexture (渲染纹理) 上。利用 BlendOp Max 确保重叠部分取最大浓度,不发黑。
  2. 快照(Camera): 使用一个专属的"阴影正交相机"从上往下拍。该相机平时处于关闭状态,只有在物体移动时,才由 C# 脚本调用 Render() 拍一张"快照"。
  3. 接收(Receiver): 在平面上铺一个透明的大面片,读取这张 RenderTexture,与平面背景进行**正片叠底(Multiply)**融合。

---

2. 详细配置步骤

步骤一:配置专属 Layer (图层)

为了让阴影相机只拍阴影,主相机不拍阴影,我们需要隔离图层。

  1. 在 Unity 顶部菜单栏选择 Edit -> Project Settings -> Tags and Layers。
  2. 在 Layers 中添加一个新层,命名为 FakeShadow

步骤二:创建 RenderTexture (渲染纹理)

  1. 在 Project 窗口右键,Create -> Render Texture,命名为 RT_ShadowMap
  2. 选中它,在 Inspector 中设置:
    • Size: 512 x 512 (平面固定,512或1024足够,可按需调低优化)。
    • Color Format: 选择 R8_UNorm (仅需要单通道记录灰度) 或通用的 ARGB32。
    • Depth Buffer: No depth buffer (不需要深度,节省内存)。

步骤三:编写阴影"投射" Shader 与材质

这个 Shader 赋予给每个物体底部的影子面片。它的任务是把你的羽化贴图画到 RT 上,并处理重叠。

  1. 创建一个 Shader (Custom/Shadow_Caster),代码如下:
bash 复制代码
Shader "Custom/Shadow_Caster"
{
    Properties
    {
        _MainTex ("Shadow Alpha Texture", 2D) = "white" {}
        _Intensity ("Shadow Intensity", Range(0, 1)) = 0.6
    }
    SubShader
    {
        Tags { "RenderType"="Transparent" "Queue"="Transparent" "RenderPipeline" = "UniversalPipeline" }

        Pass
        {
            // 【核心魔法】取最大值混合。重叠部分不会变黑!
            BlendOp Max
            Blend SrcAlpha One
            
            ZWrite Off
            ZTest Always // 确保被其他挡住时也能正确画到RT上

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            struct Attributes
            {
                float4 positionOS : POSITION;
                float2 uv : TEXCOORD0;
            };

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

            TEXTURE2D(_MainTex);
            SAMPLER(sampler_MainTex);
            float _Intensity;

            Varyings vert (Attributes input)
            {
                Varyings output;
                output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
                output.uv = input.uv;
                return output;
            }

            half4 frag (Varyings input) : SV_Target
            {
                // 采样你的羽化贴图 (例如你提供的 Shadow.png)
                half alpha = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv).a;
                // 将浓度乘以我们设定的强度,输出到颜色通道中
                half finalShadow = alpha * _Intensity;
                return half4(finalShadow, finalShadow, finalShadow, 1.0);
            }
            ENDHLSL
        }
    }
}
  1. 创建一个材质(例如 Mat_MahjongShadow),使用上述 Shader,贴上你提供的 Shadow.png。
  2. 重要操作: 将物体预制体下的阴影面片对象 的 Layer 改为 FakeShadow,并赋予该材质。

步骤四:配置阴影"快照"相机

  1. 在场景中创建一个新的 Camera,命名为 ShadowCamera
  2. 调整相机的 Transform,使其从平面正上方垂直向下看(Rotation X 设置为 90)。
  3. 配置 Camera 组件属性:
    • Projection: 强烈建议改为 Orthographic (正交),Size 调整到正好覆盖整个平面。
    • Clear Flags / Background Type: 选择 Solid Color。
    • Background Color: 设为 纯黑色,Alpha 为 0 (0, 0, 0, 0)。
    • Culling Mask: 只勾选 FakeShadow 层
    • Target Texture: 拖入我们步骤二创建的 RT_ShadowMap
    • ❌ 重点:将 Camera 组件右上角的复选框取消勾选 (Disable)。 让它不自动执行。
  4. 清理主相机: 选中你的主渲染相机 (Main Camera),将其 Culling Mask 中的 FakeShadow 层取消勾选,避免主相机重复渲染这些面片。

步骤五:编写阴影"接收" Shader 与材质 (大面片)

这个面片铺在平面上,用于把拍好的 RT 以正片叠底的方式扣在平面上。

  1. 创建 Shader (Custom/Shadow_Receiver):
bash 复制代码
Shader "Custom/Shadow_Receiver"
{
    Properties
    {
        _MainTex ("Shadow RT", 2D) = "black" {}
        _ShadowColor ("Shadow Tint Color", Color) = (0.2, 0.2, 0.2, 1)
    }
    SubShader
    {
        // 渲染队列放在透明,确保在平面画完之后画
        Tags { "RenderType"="Transparent" "Queue"="Transparent" "RenderPipeline" = "UniversalPipeline" }

        Pass
        {
            // 正片叠底混合:当前像素颜色 = 背景颜色 * 材质输出颜色
            Blend DstColor Zero
            ZWrite Off
            
            // 微微偏移防止Z-Fighting
            Offset -1, -1

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            struct Attributes
            {
                float4 positionOS : POSITION;
                float2 uv : TEXCOORD0;
            };

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

            TEXTURE2D(_MainTex);
            SAMPLER(sampler_MainTex);
            float4 _ShadowColor;

            Varyings vert (Attributes input)
            {
                Varyings output;
                output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
                output.uv = input.uv;
                return output;
            }

            half4 frag (Varyings input) : SV_Target
            {
                // 读取阴影相机的 RT (由于我们之前输出的是灰阶,读 R 通道即可表示浓度)
                half shadowIntensity = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv).r;
                
                // 计算叠底颜色:没有阴影的地方是纯白 (1,1,1),有阴影的地方偏向_ShadowColor
                half3 finalColor = lerp(half3(1.0, 1.0, 1.0), _ShadowColor.rgb, shadowIntensity * _ShadowColor.a);
                
                return half4(finalColor, 1.0);
            }
            ENDHLSL
        }
    }
}
  1. 创建材质(如 Mat_TableShadowReceiver),将 RT_ShadowMap 拖入 Shadow RT 贴图槽中。可以调整 Shadow Tint Color 控制阴影最终的偏色和最暗程度。
  2. 在平面的正上方(Y轴稍微高出0.01),创建一个 Quad 面片。调整它的缩放和位置,使其完全匹配 ShadowCamera 的正交视区大小。赋予它这个接收材质。

步骤六:C# 控制脚本 (按需渲染核心)

创建一个 ShadowCameraManager.cs,挂载到 ShadowCamera 所在的 GameObject 上。

csharp 复制代码
using UnityEngine;

[RequireComponent(typeof(Camera))]
public class ShadowCameraManager : MonoBehaviour
{
    public static ShadowCameraManager Instance;

    private Camera m_ShadowCamera;
    private bool m_IsAnimating = false;

    private void Awake()
    {
        Instance = this;
        m_ShadowCamera = GetComponent<Camera>();
        
        // 确保相机初始化时不自动渲染
        if (m_ShadowCamera != null)
        {
            m_ShadowCamera.enabled = false;
        }
    }

    private void Start()
    {
        // 游戏刚开始时,强行拍一张初始的静态阴影
        RenderShadowOnce();
    }

    private void LateUpdate()
    {
        // 如果有物体移动或者被拖拽,每帧渲染
        if (m_IsAnimating && m_ShadowCamera != null)
        {
            m_ShadowCamera.Render();
        }
    }

    /// <summary>
    /// 调用此方法:单独拍一张(例如玩家移动物体后)
    /// </summary>
    public void RenderShadowOnce()
    {
        if (m_ShadowCamera != null && !m_IsAnimating)
        {
            m_ShadowCamera.Render();
        }
    }

    /// <summary>
    /// 调用此方法:开启连续渲染(例如动画开始、或者玩家按住物体拖拽时)
    /// </summary>
    public void BeginContinuousRendering()
    {
        m_IsAnimating = true;
    }

    /// <summary>
    /// 调用此方法:关闭连续渲染(动画结束落地时)
    /// </summary>
    public void EndContinuousRendering()
    {
        m_IsAnimating = false;
        // 结束时补拍最后一张确保完美对齐
        RenderShadowOnce(); 
    }
}

---

3. 业务逻辑中的实际应用方式

在你的交互逻辑中,像这样调用:

C#

// 场景 1:玩家开始拖动 (或者用到 Raycasting 进行射线检测时)

ShadowCameraManager.Instance.BeginContinuousRendering();

// 场景 2:玩家特定位置并停下

ShadowCameraManager.Instance.EndContinuousRendering();

// 直接调用单次刷新,性能开销只在这一帧

ShadowCameraManager.Instance.RenderShadowOnce();

相关推荐
WarPigs16 小时前
游戏签到系统
unity
小拉达不是臭老鼠18 小时前
Unity中的UI系统之UGUI
学习·ui·unity
万兴丶18 小时前
Coplay适用于 Unity 的“Al 代理”使用指南
unity·游戏引擎·ai编程
魔士于安21 小时前
Unity材质球大合集
unity·游戏引擎·材质
mxwin1 天前
Unity Shader 冰面 Shader 制作原理与流程
unity·游戏引擎·shader
玖玥拾1 天前
Cocos学习笔记:关卡系统、音频管理与物理控制
游戏引擎·cocos2d
小拉达不是臭老鼠1 天前
Unity中的UI系统之UGUI_登陆面板实现
ui·unity
郝学胜-神的一滴1 天前
[简化版 GAMES 101] 计算机图形学 11:频域·卷积·抗锯齿
c++·unity·图形渲染·opengl·three·unreal
玖玥拾1 天前
Cocos学习笔记:滚动视图、关卡系统与本地存储
游戏引擎·cocos2d
元气少女小圆丶2 天前
SenseGlove Nova 2+Unity开发笔记2
笔记·unity·游戏引擎