You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Unity体素游戏中立方体网格的射线检测实现方案咨询

Unity体素游戏射线检测实现方案

问题背景

我正在Unity中开发一款体素游戏,游戏世界由3D体素(立方体)数组构成,每个体素分为空或被占据两种状态。我需要从世界内任意点发射射线,确定其首次击中被占据体素表面的位置。相关代码结构如下:

public struct Vector3 {
    public float x, y, z;
}

public struct Vector3Int {
    public int x, y, z;
}

public enum Voxel {
    Empty,
    Occupied,
}

public struct RaycastResult {
    public bool success;  // whether any block was hit by raycast
    public Vector3 point;  // point where ray hits the surface of first solid voxel
    public Vector3Int block;  // what block was hit by raycast
}

public class World {
    public Voxel[,,] voxels;

    public Voxel GetVoxel(Vector3Int position) {
        return voxels[position.x, position.y, position.z];
    }

    public RaycastResult Raycast(Vector3 origin, Vector3 direction) {
        // What to do here?
    }
}

请问应使用何种算法实现该功能?针对大型世界,Unity中是否有无需自行编写射线检测器的高效实现方式?若无合适内置方案,是否有通用算法可用?


回答

一、Unity内置高效方案

针对大型体素世界,优先借助Unity物理引擎的优化能力:

  • 不要给每个体素单独添加碰撞体,这会导致性能急剧下降。可以将可见的体素合并为MeshCollider,或者使用Unity 2020+支持的CompositeCollider3D,把一片区域的体素碰撞体合并为一个整体,让物理引擎高效处理射线检测。
  • 如果是动态可编辑的体素世界(支持玩家破坏/放置方块),可以采用分区管理:只在玩家周围的活跃区域生成合并后的碰撞体,远处非活跃区域用简化碰撞体或直接禁用碰撞检测,平衡性能与检测精度。

二、通用高效算法:3D DDA算法

如果不想依赖Unity物理引擎,或者需要更精细的体素级控制,3D Digital Differential Analyzer (DDA) 是体素射线检测的标准方案,它能高效遍历射线穿过的体素,直到找到第一个被占据的目标。

核心逻辑

  1. 计算射线起点所在的体素坐标,初始化当前遍历的体素位置。
  2. 计算射线到达X、Y、Z轴下一个体素边界的距离(tMaxX/Y/Z),以及每次跨越体素时的步长增量(tDeltaX/Y/Z)。
  3. 每次选择tMax最小的轴,移动到相邻体素,更新当前体素坐标和对应轴的tMax值。
  4. 检查当前体素状态:若为被占据,计算射线击中体素表面的精确位置;若为空,则继续循环,直到超出世界边界。

代码实现示例

public RaycastResult Raycast(Vector3 origin, Vector3 direction) {
    RaycastResult result = new RaycastResult { success = false };

    // 归一化方向向量,确保步长计算准确
    direction = Normalize(direction);

    // 获取起点所在的初始体素坐标
    Vector3Int currentVoxel = new Vector3Int(
        Mathf.FloorToInt(origin.x),
        Mathf.FloorToInt(origin.y),
        Mathf.FloorToInt(origin.z)
    );

    // 射线在各轴上的移动步长(±1)
    int stepX = direction.x > 0 ? 1 : -1;
    int stepY = direction.y > 0 ? 1 : -1;
    int stepZ = direction.z > 0 ? 1 : -1;

    // 计算到达下一个体素边界的t值
    float tMaxX = CalculateTMax(origin.x, direction.x, stepX);
    float tMaxY = CalculateTMax(origin.y, direction.y, stepY);
    float tMaxZ = CalculateTMax(origin.z, direction.z, stepZ);

    // 计算每次跨越体素的t增量,处理方向为0的情况
    float tDeltaX = stepX / direction.x;
    if (float.IsInfinity(tDeltaX)) tDeltaX = float.MaxValue;
    float tDeltaY = stepY / direction.y;
    if (float.IsInfinity(tDeltaY)) tDeltaY = float.MaxValue;
    float tDeltaZ = stepZ / direction.z;
    if (float.IsInfinity(tDeltaZ)) tDeltaZ = float.MaxValue;

    // 遍历体素直到找到目标或超出边界
    while (IsVoxelInBounds(currentVoxel)) {
        if (GetVoxel(currentVoxel) == Voxel.Occupied) {
            // 计算击中表面的精确位置
            float tHit = Mathf.Min(tMaxX - tDeltaX, tMaxY - tDeltaY, tMaxZ - tDeltaZ);
            result.point = new Vector3(
                origin.x + direction.x * tHit,
                origin.y + direction.y * tHit,
                origin.z + direction.z * tHit
            );
            result.block = currentVoxel;
            result.success = true;
            return result;
        }

        // 移动到下一个体素
        if (tMaxX < tMaxY) {
            if (tMaxX < tMaxZ) {
                currentVoxel.x += stepX;
                tMaxX += tDeltaX;
            } else {
                currentVoxel.z += stepZ;
                tMaxZ += tDeltaZ;
            }
        } else {
            if (tMaxY < tMaxZ) {
                currentVoxel.y += stepY;
                tMaxY += tDeltaY;
            } else {
                currentVoxel.z += stepZ;
                tMaxZ += tDeltaZ;
            }
        }
    }

    return result;
}

// 辅助函数:归一化向量
private Vector3 Normalize(Vector3 vec) {
    float mag = Mathf.Sqrt(vec.x*vec.x + vec.y*vec.y + vec.z*vec.z);
    return new Vector3(vec.x/mag, vec.y/mag, vec.z/mag);
}

// 辅助函数:计算到达下一体素边界的t值
private float CalculateTMax(float originCoord, float directionCoord, int step) {
    if (directionCoord == 0) return float.MaxValue;
    float boundary = step > 0 ? Mathf.Ceil(originCoord) : Mathf.Floor(originCoord);
    return (boundary - originCoord) / directionCoord;
}

// 辅助函数:检查体素是否在世界范围内
private bool IsVoxelInBounds(Vector3Int voxel) {
    return voxel.x >= 0 && voxel.x < voxels.GetLength(0) &&
           voxel.y >= 0 && voxel.y < voxels.GetLength(1) &&
           voxel.z >= 0 && voxel.z < voxels.GetLength(2);
}

三、性能优化建议

  • 边界前置检查:每次循环先验证当前体素是否在世界数组范围内,避免数组越界错误。
  • 方向向量归一化:必须确保方向向量为单位向量,否则tMax和tDelta的计算会出现偏差。
  • 区块空间分区:超大型世界可将体素划分为区块,射线检测时先通过AABB判断哪些区块需要遍历,减少无效体素检查。

内容的提问来源于stack exchange,提问作者K. A. Kusakov

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.14 02:35:41