《影子冒险》—— 生成角色影子的包围盒

前言

这段时间在忙个人微信小游戏的开发,正好碰到个有意思的问题,需要计算出角色在马路上的投影的包围盒,用来判断角色的影子是否与汽车发生了碰撞。在这篇博文中记录分析一下生成投影包围盒的过程,如果你已经看到这篇博文,证明我的微信小游戏已经顺利上线了,它的名字叫《影子冒险》,欢迎大家来玩哦。

平行光光线投影公式

因为我的游戏目前只存在平行光,所以我这里着重介绍一下平行光的光线投影公式,用于计算出角色在地面上的投影。不过不会立刻介绍,会先解释一下基础的概念,希望大家耐心往下看。

参数化直线方程

在了解投影公式前,我们先了解下参数化直线方程,参数化直线方程是描述直线的一种数学方式,通过引入一个标量参数(通常用 t 表示)来表示直线上任意一点的位置。其核心思想是将直线视为点的连续运动轨迹,用参数 t 控制点的位置变化。

  1. 基本形式
    image
  2. 几何意义
    image
  3. 分量化表示
    将方程分解为坐标分量:
    image
    每个坐标分量都独立地随 t 线性变化。

投影公式

光线投影公式的详细解释
在Unity中计算物体沿平行光方向投影到地面的位置,核心在于理解 光线与平面的交点计算。

  1. 基本几何概念
  • 光线(Light Ray):由物体顶点沿平行光方向无限延伸的直线。
  • 地面平面(Ground Plane):假设为水平面,方程一般为 y = groundHeight。
  1. 参数化直线方程
    image
  2. 平面方程联立求解
    image
  3. 投影点坐标计算
    image

坐标计算出后,需要分3种情况讨论下:

  • 光线平行于地面(Ly = 0):无法产生有效投影,需跳过计算
  • 光线朝上照射(Ly > 0):仅当 Cy < groundHeight 时有效
  • 光线朝下照射(Ly < 0):仅当 Cy > groundHeight 时有效

代码实现

foreach (Vector3 corner in worldCorners)
{
    // 计算参数t
    float t = (groundHeight - corner.y) / lightDir.y;

    // 计算投影点坐标
    Vector3 proj = new Vector3(
        corner.x + lightDir.x * t,  // X分量
        groundHeight,               // Y固定为地面高度
        corner.z + lightDir.z * t   // Z分量
    );

    // 更新包围盒边界
    minX = Mathf.Min(minX, proj.x);
    maxX = Mathf.Max(maxX, proj.x);
    minZ = Mathf.Min(minZ, proj.z);
    maxZ = Mathf.Max(maxZ, proj.z);
}

效果展示

image

完整代码

最后贴下完整代码分享给大家,有需要的可以自取

using UnityEngine;

namespace XiaYun.Core
{
    [ExecuteInEditMode]
    public class ShadowProjector : MonoBehaviour
    {
        [Header("Core Settings")] 
        public BoxCollider targetCollider;
        public BoxCollider shadowCollider;
        public Light directionalLight;
        public float groundHeight = 0f;
        public float shadowHeight = 0.5f;

        [Header("Visualization")] 
        public Color boundsColor = new Color(0, 1, 0, 0.7f);
        [Range(1f, 5f)] public float lineThickness = 2f;
        public bool showInPlayMode = true;

        private Bounds currentBounds;

        void Update()
        {
            if (ShouldSkipUpdate()) return;

            CalculateProjection();
        }
        
        private bool ShouldSkipUpdate()
        {
            if (targetCollider == null || directionalLight == null)
            {
                Debug.LogWarning("Essential components not assigned!");
                return true;
            }

            if (Mathf.Approximately(directionalLight.transform.forward.y, 0))
            {
                Debug.LogWarning("Light direction is parallel to ground!");
                return true;
            }

            return false;
        }

        private void CalculateProjection()
        {
            Vector3 lightDir = directionalLight.transform.forward.normalized;
            Vector3[] corners = GetWorldSpaceCorners(targetCollider);
            CalculateProjectedBounds(corners, lightDir, out Vector3 center, out Vector3 size);

            if (shadowCollider != null)
            {
                shadowCollider.center = center;
                shadowCollider.size = size;
                currentBounds = shadowCollider.bounds;
            }
        }
        
        private Vector3[] GetWorldSpaceCorners(BoxCollider collider)
        {
            Vector3[] corners = new Vector3[8];
            Bounds b = collider.bounds;

            corners[0] = new Vector3(b.min.x, b.min.y, b.min.z);
            corners[1] = new Vector3(b.min.x, b.min.y, b.max.z);
            corners[2] = new Vector3(b.max.x, b.min.y, b.min.z);
            corners[3] = new Vector3(b.max.x, b.min.y, b.max.z);
            corners[4] = new Vector3(b.min.x, b.max.y, b.min.z);
            corners[5] = new Vector3(b.min.x, b.max.y, b.max.z);
            corners[6] = new Vector3(b.max.x, b.max.y, b.min.z);
            corners[7] = new Vector3(b.max.x, b.max.y, b.max.z);

            return corners;
        }

        private void CalculateProjectedBounds(Vector3[] worldCorners, Vector3 lightDir, out Vector3 center, out Vector3 size)
        {
            float minX = float.MaxValue, maxX = float.MinValue;
            float minZ = float.MaxValue, maxZ = float.MinValue;

            foreach (Vector3 corner in worldCorners)
            {
                float t = (groundHeight - corner.y) / lightDir.y;
                Vector3 proj = new Vector3(
                    corner.x + lightDir.x * t,
                    groundHeight,
                    corner.z + lightDir.z * t
                );

                minX = Mathf.Min(minX, proj.x);
                maxX = Mathf.Max(maxX, proj.x);
                minZ = Mathf.Min(minZ, proj.z);
                maxZ = Mathf.Max(maxZ, proj.z);
            }

            center = new Vector3((minX + maxX) / 2, groundHeight, (minZ + maxZ) / 2);
            size = new Vector3(maxX - minX, shadowHeight, maxZ - minZ);
        }

        void OnDrawGizmos()
        {
            if (!showInPlayMode && Application.isPlaying) return;
            if (shadowCollider == null) return;

            DrawBounds(currentBounds);
        }

        void DrawBounds(Bounds bounds)
        {
            Gizmos.color = boundsColor;
            Gizmos.matrix = Matrix4x4.identity;

            Vector3 center = bounds.center;
            Vector3 size = bounds.size;

            Vector3 frontTopLeft = center + new Vector3(-size.x / 2, size.y / 2, size.z / 2);
            Vector3 frontTopRight = center + new Vector3(size.x / 2, size.y / 2, size.z / 2);
            Vector3 frontBottomLeft = center + new Vector3(-size.x / 2, -size.y / 2, size.z / 2);
            Vector3 frontBottomRight = center + new Vector3(size.x / 2, -size.y / 2, size.z / 2);
            Vector3 backTopLeft = center + new Vector3(-size.x / 2, size.y / 2, -size.z / 2);
            Vector3 backTopRight = center + new Vector3(size.x / 2, size.y / 2, -size.z / 2);
            Vector3 backBottomLeft = center + new Vector3(-size.x / 2, -size.y / 2, -size.z / 2);
            Vector3 backBottomRight = center + new Vector3(size.x / 2, -size.y / 2, -size.z / 2);

            DrawThickLine(frontTopLeft, frontTopRight);
            DrawThickLine(frontTopRight, frontBottomRight);
            DrawThickLine(frontBottomRight, frontBottomLeft);
            DrawThickLine(frontBottomLeft, frontTopLeft);

            DrawThickLine(backTopLeft, backTopRight);
            DrawThickLine(backTopRight, backBottomRight);
            DrawThickLine(backBottomRight, backBottomLeft);
            DrawThickLine(backBottomLeft, backTopLeft);

            DrawThickLine(frontTopLeft, backTopLeft);
            DrawThickLine(frontTopRight, backTopRight);
            DrawThickLine(frontBottomRight, backBottomRight);
            DrawThickLine(frontBottomLeft, backBottomLeft);
        }

        void DrawThickLine(Vector3 start, Vector3 end)
        {
#if UNITY_EDITOR
            Camera sceneCam = UnityEditor.SceneView.currentDrawingSceneView?.camera;
            if (sceneCam == null) return;

            Vector3 dir = (end - start).normalized;
            Vector3 camForward = sceneCam.transform.forward;
            Vector3 offset = Vector3.Cross(dir, camForward).normalized * lineThickness * 0.005f;

            Gizmos.DrawLine(start + offset, end + offset);
            Gizmos.DrawLine(start - offset, end - offset);
            Gizmos.DrawLine(start + offset, start - offset);
            Gizmos.DrawLine(end + offset, end - offset);
#endif
        }
    }
}
posted @ 2025-03-05 23:36  陈侠云  阅读(24)  评论(0)    收藏  举报
//雪花飘落效果