024-拾取

拾取

拾取(Picking)技术是交互式3D应用程序的核心功能,它允许用户通过鼠标点击或触摸屏幕来选择和操作3D场景中的对象。本章将详细介绍在DirectX 12中实现拾取的各种方法和技巧,包括坐标系统转换、射线投射和几何相交检测等关键概念。

17.1 屏幕空间到投影窗口的变换

实现拾取的第一步是将屏幕空间坐标(鼠标位置)转换为3D世界中的射线。这个过程始于将屏幕坐标转换为规范化设备坐标(Normalized Device Coordinates, NDC)。

17.1.1 屏幕坐标系统

在Windows系统中,屏幕坐标系以屏幕左上角为原点(0,0),X轴向右为正,Y轴向下为正。而在DirectX的NDC空间中,坐标范围为[-1,1]×[-1,1]×[0,1],以屏幕中心为原点,X轴向右为正,Y轴向上为正。

将屏幕坐标转换为NDC坐标的公式为:

ndcX = (2.0f * screenX / screenWidth) - 1.0f
ndcY = 1.0f - (2.0f * screenY / screenHeight)

对于深度值,我们通常使用近平面的深度值0.0f作为起点。

17.1.2 NDC到视图空间的变换

NDC坐标需要进一步转换到视图空间。这需要使用投影矩阵的逆矩阵:

cpp

// 从屏幕坐标到视图空间的变换
XMVECTOR ScreenToView(int screenX, int screenY, float screenWidth, float screenHeight, const XMMATRIX& projInverse)
{
    // 转换为NDC坐标
    float ndcX = (2.0f * screenX / screenWidth) - 1.0f;
    float ndcY = 1.0f - (2.0f * screenY / screenHeight);
    
    // NDC空间中的点(近平面上)
    XMVECTOR ndcNear = XMVectorSet(ndcX, ndcY, 0.0f, 1.0f);
    
    // NDC空间中的点(远平面上)
    XMVECTOR ndcFar = XMVectorSet(ndcX, ndcY, 1.0f, 1.0f);
    
    // 转换到视图空间
    XMVECTOR viewNear = XMVector4Transform(ndcNear, projInverse);
    XMVECTOR viewFar = XMVector4Transform(ndcFar, projInverse);
    
    // 透视除法(w分量)
    viewNear = viewNear / XMVectorGetW(viewNear);
    viewFar = viewFar / XMVectorGetW(viewFar);
    
    return viewNear;
}

17.1.3 视图空间到世界空间的变换

最后,我们需要将视图空间中的射线转换到世界空间,这需要使用视图矩阵的逆矩阵:

cpp

// 从视图空间到世界空间的变换
Ray ViewToWorld(const XMVECTOR& viewNear, const XMVECTOR& viewFar, const XMMATRIX& viewInverse)
{
    // 转换到世界空间
    XMVECTOR worldNear = XMVector4Transform(viewNear, viewInverse);
    XMVECTOR worldFar = XMVector4Transform(viewFar, viewInverse);
    
    // 构造世界空间中的射线
    Ray ray;
    XMStoreFloat3(&ray.origin, worldNear);
    
    XMVECTOR direction = XMVector3Normalize(worldFar - worldNear);
    XMStoreFloat3(&ray.direction, direction);
    
    return ray;
}

17.1.4 完整的屏幕到世界射线转换

将上述步骤组合起来,我们可以得到一个完整的从屏幕坐标到世界射线的转换函数:

cpp

// 射线结构定义
struct Ray
{
    XMFLOAT3 origin;     // 射线起点
    XMFLOAT3 direction;  // 射线方向(归一化)
};

// 从屏幕坐标创建世界空间射线
Ray ScreenPointToRay(int screenX, int screenY, int screenWidth, int screenHeight, 
                     const XMMATRIX& proj, const XMMATRIX& view)
{
    // 计算投影矩阵和视图矩阵的逆
    XMMATRIX projInverse = XMMatrixInverse(nullptr, proj);
    XMMATRIX viewInverse = XMMatrixInverse(nullptr, view);
    
    // 转换到NDC空间
    float ndcX = (2.0f * screenX / screenWidth) - 1.0f;
    float ndcY = 1.0f - (2.0f * screenY / screenHeight);
    
    // NDC空间中的点(近平面上)
    XMVECTOR ndcNear = XMVectorSet(ndcX, ndcY, 0.0f, 1.0f);
    
    // NDC空间中的点(远平面上)
    XMVECTOR ndcFar = XMVectorSet(ndcX, ndcY, 1.0f, 1.0f);
    
    // 转换到视图空间
    XMVECTOR viewNear = XMVector4Transform(ndcNear, projInverse);
    XMVECTOR viewFar = XMVector4Transform(ndcFar, projInverse);
    
    // 透视除法
    viewNear = viewNear / XMVectorGetW(viewNear);
    viewFar = viewFar / XMVectorGetW(viewFar);
    
    // 转换到世界空间
    XMVECTOR worldNear = XMVector4Transform(viewNear, viewInverse);
    XMVECTOR worldFar = XMVector4Transform(viewFar, viewInverse);
    
    // 构造世界空间中的射线
    Ray ray;
    XMStoreFloat3(&ray.origin, worldNear);
    
    XMVECTOR direction = XMVector3Normalize(worldFar - worldNear);
    XMStoreFloat3(&ray.direction, direction);
    
    return ray;
}

17.1.5 使用摄像机类简化射线计算

如果我们已经有一个封装好的摄像机类,射线计算可以进一步简化:

cpp

// 使用摄像机类创建世界空间射线
Ray Camera::ScreenPointToRay(int screenX, int screenY, int screenWidth, int screenHeight) const
{
    // 获取摄像机位置作为射线起点
    XMVECTOR eyePos = XMLoadFloat3(&mPosition);
    
    // 计算投影矩阵的逆
    XMMATRIX projInverse = XMMatrixInverse(nullptr, XMLoadFloat4x4(&mProj));
    
    // 转换到NDC空间
    float ndcX = (2.0f * screenX / screenWidth) - 1.0f;
    float ndcY = 1.0f - (2.0f * screenY / screenHeight);
    
    // NDC空间中的点(近平面上)
    XMVECTOR ndcPoint = XMVectorSet(ndcX, ndcY, 0.0f, 1.0f);
    
    // 转换到视图空间
    XMVECTOR viewPoint = XMVector4Transform(ndcPoint, projInverse);
    viewPoint = viewPoint / XMVectorGetW(viewPoint);
    
    // 转换到世界空间
    XMMATRIX viewInverse = XMMatrixLookToLH(
        eyePos,
        XMLoadFloat3(&mLook),
        XMLoadFloat3(&mUp)
    );
    viewInverse = XMMatrixInverse(nullptr, viewInverse);
    
    XMVECTOR worldPoint = XMVector4Transform(viewPoint, viewInverse);
    
    // 构造世界空间中的射线
    Ray ray;
    XMStoreFloat3(&ray.origin, eyePos);
    
    XMVECTOR direction = XMVector3Normalize(worldPoint - eyePos);
    XMStoreFloat3(&ray.direction, direction);
    
    return ray;
}

17.2 位于世界空间与局部空间中的拾取射线

一旦我们有了世界空间中的射线,通常需要将其转换到对象的局部空间进行更高效的碰撞检测。

17.2.1 世界空间拾取射线

世界空间射线是从摄像机位置指向鼠标点击位置的方向向量。它可以用于与已经在世界空间中的物体(如地形)进行碰撞检测。

17.2.2 局部空间拾取射线

对于具有复杂变换的对象,将射线转换到对象的局部空间通常更为高效。这需要使用对象的世界变换矩阵的逆矩阵:

cpp

// 将世界空间射线转换到局部空间
Ray TransformRayToLocalSpace(const Ray& worldRay, const XMMATRIX& worldInverse)
{
    // 转换射线原点
    XMVECTOR origin = XMLoadFloat3(&worldRay.origin);
    origin = XMVector4Transform(origin, worldInverse);
    
    // 转换射线方向(注意:不使用平移)
    XMVECTOR direction = XMLoadFloat3(&worldRay.direction);
    direction = XMVector4Transform(direction, worldInverse);
    direction = XMVector3Normalize(direction);
    
    // 构造局部空间中的射线
    Ray localRay;
    XMStoreFloat3(&localRay.origin, origin);
    XMStoreFloat3(&localRay.direction, direction);
    
    return localRay;
}

17.2.3 性能优化考虑

在选择是在世界空间还是局部空间进行碰撞检测时,应考虑以下因素:

计算开销:转换射线到局部空间需要矩阵求逆操作,但可能简化后续的碰撞检测
几何复杂度:对于轴对齐的简单几何体,局部空间检测通常更快
批量处理:对于同一射线与多个物体的检测,保持射线在世界空间可能更高效

17.3 射线与网格的相交检测

一旦我们有了射线,下一步是检测它是否与场景中的几何体相交。

17.3.1 射线与轴对齐包围盒的相交检测

轴对齐包围盒(AABB)是一种常用的简化几何体,用于快速的碰撞检测。下面是检测射线与AABB相交的算法:

cpp

// 检测射线与AABB的相交
bool IntersectRayAABB(const Ray& ray, const XMFLOAT3& boxMin, const XMFLOAT3& boxMax, float& t)
{
    // 计算射线参数方程与AABB各个平面的交点参数
    float tmin = (boxMin.x - ray.origin.x) / ray.direction.x;
    float tmax = (boxMax.x - ray.origin.x) / ray.direction.x;
    
    // 确保tmin <= tmax
    if (tmin > tmax) std::swap(tmin, tmax);
    
    float tymin = (boxMin.y - ray.origin.y) / ray.direction.y;
    float tymax = (boxMax.y - ray.origin.y) / ray.direction.y;
    
    if (tymin > tymax) std::swap(tymin, tymax);
    
    // 检查是否有交点
    if ((tmin > tymax) || (tymin > tmax))
        return false;
    
    // 更新交点范围
    if (tymin > tmin) tmin = tymin;
    if (tymax < tmax) tmax = tymax;
    
    float tzmin = (boxMin.z - ray.origin.z) / ray.direction.z;
    float tzmax = (boxMax.z - ray.origin.z) / ray.direction.z;
    
    if (tzmin > tzmax) std::swap(tzmin, tzmax);
    
    // 再次检查是否有交点
    if ((tmin > tzmax) || (tzmin > tmax))
        return false;
    
    // 更新交点范围
    if (tzmin > tmin) tmin = tzmin;
    if (tzmax < tmax) tmax = tzmax;
    
    // 检查交点是否在射线前方
    if (tmax < 0)
        return false;
    
    // 射线与AABB相交,返回最近交点的参数t
    t = (tmin < 0) ? tmax : tmin;
    return true;
}

或者,我们可以使用DirectXMath库提供的函数:

cpp

// 使用DirectXMath库检测射线与AABB的相交
bool IntersectRayAABB(const Ray& ray, const BoundingBox& box, float& distance)
{
    XMVECTOR origin = XMLoadFloat3(&ray.origin);
    XMVECTOR direction = XMLoadFloat3(&ray.direction);
    
    return box.Intersects(origin, direction, distance);
}

17.3.2 射线与球体的相交检测

射线与球体的相交检测是另一种常用的碰撞检测方法:

cpp

// 检测射线与球体的相交
bool IntersectRaySphere(const Ray& ray, const XMFLOAT3& center, float radius, float& t)
{
    // 射线原点到球心的向量
    XMVECTOR originV = XMLoadFloat3(&ray.origin);
    XMVECTOR centerV = XMLoadFloat3(&center);
    XMVECTOR directionV = XMLoadFloat3(&ray.direction);
    
    XMVECTOR oc = originV - centerV;
    
    // 二次方程系数
    float a = XMVectorGetX(XMVector3Dot(directionV, directionV)); // 通常为1,如果方向已归一化
    float b = 2.0f * XMVectorGetX(XMVector3Dot(oc, directionV));
    float c = XMVectorGetX(XMVector3Dot(oc, oc)) - radius * radius;
    
    // 判别式
    float discriminant = b * b - 4 * a * c;
    
    if (discriminant < 0)
        return false; // 无实数解,射线未与球体相交
    
    // 计算较小的t值(较近的交点)
    float sqrtDiscriminant = sqrt(discriminant);
    float t0 = (-b - sqrtDiscriminant) / (2 * a);
    float t1 = (-b + sqrtDiscriminant) / (2 * a);
    
    // 确保t值为正(交点在射线前方)
    if (t0 > t1) std::swap(t0, t1);
    
    if (t0 < 0) {
        t0 = t1; // 如果t0为负,则使用t1
        if (t0 < 0) return false; // 两个交点都在射线后方
    }
    
    t = t0;
    return true;
}

或者使用DirectXMath库:

cpp

// 使用DirectXMath库检测射线与球体的相交
bool IntersectRaySphere(const Ray& ray, const BoundingSphere& sphere, float& distance)
{
    XMVECTOR origin = XMLoadFloat3(&ray.origin);
    XMVECTOR direction = XMLoadFloat3(&ray.direction);
    
    return sphere.Intersects(origin, direction, distance);
}

17.3.3 射线与三角形的相交检测

最精确的拾取通常需要射线与场景中的三角形网格进行交叉测试。Möller–Trumbore算法是一种高效的射线-三角形交叉检测算法:

cpp

// 使用Möller–Trumbore算法检测射线与三角形的相交
bool IntersectRayTriangle(
    const Ray& ray,
    const XMFLOAT3& v0, const XMFLOAT3& v1, const XMFLOAT3& v2,
    float& t, float& u, float& v)
{
    // 转换为XMVECTOR
    XMVECTOR rayOrigin = XMLoadFloat3(&ray.origin);
    XMVECTOR rayDirection = XMLoadFloat3(&ray.direction);
    XMVECTOR vertex0 = XMLoadFloat3(&v0);
    XMVECTOR vertex1 = XMLoadFloat3(&v1);
    XMVECTOR vertex2 = XMLoadFloat3(&v2);
    
    // 计算边向量
    XMVECTOR edge1 = vertex1 - vertex0;
    XMVECTOR edge2 = vertex2 - vertex0;
    
    // 计算行列式的分母: cross(rayDirection, edge2)
    XMVECTOR h = XMVector3Cross(rayDirection, edge2);
    float a = XMVectorGetX(XMVector3Dot(edge1, h));
    
    // 如果行列式接近于0,射线与三角形平行
    if (a > -EPSILON && a < EPSILON)
        return false;
    
    float f = 1.0f / a;
    
    // 计算射线原点到顶点0的向量
    XMVECTOR s = rayOrigin - vertex0;
    
    // 计算u值
    u = f * XMVectorGetX(XMVector3Dot(s, h));
    
    // 检查u是否在[0,1]范围内
    if (u < 0.0f || u > 1.0f)
        return false;
    
    // 计算q = cross(s, edge1)
    XMVECTOR q = XMVector3Cross(s, edge1);
    
    // 计算v值
    v = f * XMVectorGetX(XMVector3Dot(rayDirection, q));
    
    // 检查v是否在[0,1]范围内,且u+v<=1
    if (v < 0.0f || u + v > 1.0f)
        return false;
    
    // 计算t值(射线参数)
    t = f * XMVectorGetX(XMVector3Dot(edge2, q));
    
    // 确保交点在射线前方
    if (t > EPSILON)
        return true;
    
    return false;
}

17.3.4 射线与网格的高效相交检测

对于由成千上万个三角形组成的复杂网格,我们需要更高效的方法。通常的做法是使用空间分割数据结构(如BVH、k-d树或八叉树)来加速相交检测:

cpp

// 使用层次包围盒(BVH)进行射线与网格的相交检测
bool IntersectRayMesh(const Ray& ray, const Mesh& mesh, float& distance, int& triangleIndex)
{
    // 首先检查射线是否与网格的包围盒相交
    float tBox;
    if (!IntersectRayAABB(ray, mesh.bounds.min, mesh.bounds.max, tBox))
        return false;
    
    bool hit = false;
    distance = FLT_MAX;
    
    // 遍历所有三角形
    for (int i = 0; i < mesh.indices.size(); i += 3)
    {
        XMFLOAT3 v0 = mesh.vertices[mesh.indices[i]].position;
        XMFLOAT3 v1 = mesh.vertices[mesh.indices[i+1]].position;
        XMFLOAT3 v2 = mesh.vertices[mesh.indices[i+2]].position;
        
        float t, u, v;
        if (IntersectRayTriangle(ray, v0, v1, v2, t, u, v))
        {
            if (t < distance)
            {
                distance = t;
                triangleIndex = i / 3;
                hit = true;
            }
        }
    }
    
    return hit;
}

更高效的实现会使用BVH等结构来减少需要检测的三角形数量:

cpp

// BVH节点结构
struct BVHNode
{
    BoundingBox bounds;
    int leftChild;     // 左子节点索引
    int rightChild;    // 右子节点索引
    int firstTriangle; // 第一个三角形索引
    int triangleCount; // 三角形数量
};

// 使用BVH加速射线与网格的相交检测
bool IntersectRayMeshBVH(const Ray& ray, const Mesh& mesh, const std::vector<BVHNode>& bvh, float& distance, int& triangleIndex)
{
    bool hit = false;
    distance = FLT_MAX;
    
    // 使用栈代替递归,提高效率
    std::stack<int> nodeStack;
    nodeStack.push(0); // 从根节点开始
    
    while (!nodeStack.empty())
    {
        int nodeIndex = nodeStack.top();
        nodeStack.pop();
        
        const BVHNode& node = bvh[nodeIndex];
        
        // 检查射线是否与节点包围盒相交
        float tBox;
        if (!IntersectRayAABB(ray, node.bounds.min, node.bounds.max, tBox))
            continue;
        
        // 如果是叶子节点,检查其中的三角形
        if (node.triangleCount > 0)
        {
            for (int i = 0; i < node.triangleCount; ++i)
            {
                int triIndex = node.firstTriangle + i;
                XMFLOAT3 v0 = mesh.vertices[mesh.indices[triIndex*3]].position;
                XMFLOAT3 v1 = mesh.vertices[mesh.indices[triIndex*3+1]].position;
                XMFLOAT3 v2 = mesh.vertices[mesh.indices[triIndex*3+2]].position;
                
                float t, u, v;
                if (IntersectRayTriangle(ray, v0, v1, v2, t, u, v))
                {
                    if (t < distance)
                    {
                        distance = t;
                        triangleIndex = triIndex;
                        hit = true;
                    }
                }
            }
        }
        else // 如果是内部节点,将子节点压入栈中
        {
            // 先将更远的子节点压入栈中,以便先处理更近的子节点
            XMVECTOR origin = XMLoadFloat3(&ray.origin);
            XMVECTOR direction = XMLoadFloat3(&ray.direction);
            
            // 计算射线到左右子节点包围盒中心的距离
            XMFLOAT3 leftCenter, rightCenter;
            XMStoreFloat3(&leftCenter, (XMLoadFloat3(&bvh[node.leftChild].bounds.min) + XMLoadFloat3(&bvh[node.leftChild].bounds.max)) * 0.5f);
            XMStoreFloat3(&rightCenter, (XMLoadFloat3(&bvh[node.rightChild].bounds.min) + XMLoadFloat3(&bvh[node.rightChild].bounds.max)) * 0.5f);
            
            float leftDist = XMVectorGetX(XMVector3Length(XMLoadFloat3(&leftCenter) - origin));
            float rightDist = XMVectorGetX(XMVector3Length(XMLoadFloat3(&rightCenter) - origin));
            
            // 根据距离决定先处理哪个子节点
            if (leftDist > rightDist)
            {
                nodeStack.push(node.leftChild);
                nodeStack.push(node.rightChild);
            }
            else
            {
                nodeStack.push(node.rightChild);
                nodeStack.push(node.leftChild);
            }
        }
    }
    
    return hit;
}

17.4 应用例程

接下来,让我们看一些实际的拾取应用示例。

17.4.1 鼠标拾取实现

下面是一个完整的鼠标拾取实现示例:

cpp

// 鼠标拾取实现
bool PickObject(int screenX, int screenY, std::vector<GameObject>& objects, int& selectedIndex)
{
    // 创建从鼠标位置发射的射线
    Camera& camera = GetCamera();
    Ray ray = camera.ScreenPointToRay(screenX, screenY, mClientWidth, mClientHeight);
    
    bool hit = false;
    float closestDistance = FLT_MAX;
    selectedIndex = -1;
    
    // 检查射线是否与任何对象相交
    for (int i = 0; i < objects.size(); ++i)
    {
        // 首先检查是否与对象的包围球相交(快速测试)
        float sphereDistance;
        if (IntersectRaySphere(ray, objects[i].boundingSphere.center, objects[i].boundingSphere.radius, sphereDistance))
        {
            // 如果相交,进行更精确的网格测试
            float meshDistance;
            int triangleIndex;
            if (IntersectRayMesh(ray, objects[i].mesh, meshDistance, triangleIndex))
            {
                if (meshDistance < closestDistance)
                {
                    closestDistance = meshDistance;
                    selectedIndex = i;
                    hit = true;
                }
            }
        }
    }
    
    return hit;
}

17.4.2 地形拾取

地形拾取是一种特殊形式的拾取,通常需要检测射线与高度图的交点:

cpp

// 地形拾取实现
bool PickTerrain(int screenX, int screenY, Terrain& terrain, XMFLOAT3& hitPosition)
{
    // 创建从鼠标位置发射的射线
    Camera& camera = GetCamera();
    Ray ray = camera.ScreenPointToRay(screenX, screenY, mClientWidth, mClientHeight);
    
    // 检查射线是否与地形的包围盒相交
    float boxDistance;
    if (!IntersectRayAABB(ray, terrain.bounds.min, terrain.bounds.max, boxDistance))
        return false;
    
    // 对射线进行参数化,在包围盒内采样一系列点
    float stepSize = 1.0f; // 步长,可根据地形大小调整
    float maxDistance = 1000.0f; // 最大检测距离
    
    for (float t = boxDistance; t < maxDistance; t += stepSize)
    {
        // 计算射线上的点
        XMVECTOR pointOnRay = XMLoadFloat3(&ray.origin) + XMLoadFloat3(&ray.direction) * t;
        XMFLOAT3 point;
        XMStoreFloat3(&point, pointOnRay);
        
        // 检查点是否在地形平面范围内
        if (point.x < terrain.minX || point.x > terrain.maxX ||
            point.z < terrain.minZ || point.z > terrain.maxZ)
            continue;
        
        // 在该点获取地形高度
        float height = terrain.GetHeight(point.x, point.z);
        
        // 如果射线点的高度低于地形高度,说明射线穿过了地形
        if (point.y <= height)
        {
            // 使用二分法精确找到交点
            float t0 = t - stepSize;
            float t1 = t;
            
            for (int i = 0; i < 8; ++i) // 迭代8次精确到厘米级别
            {
                float tMid = (t0 + t1) * 0.5f;
                XMVECTOR midPoint = XMLoadFloat3(&ray.origin) + XMLoadFloat3(&ray.direction) * tMid;
                XMFLOAT3 mid;
                XMStoreFloat3(&mid, midPoint);
                
                float midHeight = terrain.GetHeight(mid.x, mid.z);
                
                if (mid.y <= midHeight)
                    t1 = tMid;
                else
                    t0 = tMid;
            }
            
            // 计算最终交点
            XMVECTOR finalHitPoint = XMLoadFloat3(&ray.origin) + XMLoadFloat3(&ray.direction) * t0;
            XMStoreFloat3(&hitPosition, finalHitPoint);
            
            // 修正高度为精确的地形高度
            hitPosition.y = terrain.GetHeight(hitPosition.x, hitPosition.z);
            
            return true;
        }
    }
    
    return false;
}

17.4.3 拾取结果处理

拾取后,我们通常需要处理选择结果,例如高亮显示选中的对象:

cpp

// 响应鼠标点击事件
void OnMouseDown(WPARAM btnState, int x, int y)
{
    if ((btnState & MK_LBUTTON) != 0)
    {
        int selectedObjectIndex;
        if (PickObject(x, y, mGameObjects, selectedObjectIndex))
        {
            // 取消之前选中对象的高亮
            if (mSelectedObjectIndex >= 0 && mSelectedObjectIndex < mGameObjects.size())
            {
                mGameObjects[mSelectedObjectIndex].isSelected = false;
            }
            
            // 高亮新选中的对象
            mSelectedObjectIndex = selectedObjectIndex;
            mGameObjects[mSelectedObjectIndex].isSelected = true;
            
            // 更新UI以显示选中对象的信息
            UpdateUI(mGameObjects[mSelectedObjectIndex]);
        }
        else
        {
            // 检查是否点击了地形
            XMFLOAT3 hitPosition;
            if (PickTerrain(x, y, mTerrain, hitPosition))
            {
                // 在地形上放置一个标记或对象
                PlaceObjectAt(hitPosition);
            }
            else
            {
                // 未选中任何对象,清除当前选择
                if (mSelectedObjectIndex >= 0 && mSelectedObjectIndex < mGameObjects.size())
                {
                    mGameObjects[mSelectedObjectIndex].isSelected = false;
                }
                mSelectedObjectIndex = -1;
                
                // 清除UI选择信息
                ClearSelectionUI();
            }
        }
    }
}

17.4.4 拖放操作

拾取结合拖放操作可以实现对象移动功能:

cpp

// 拖放对象
void DragObject(int x, int y)
{
    if (mSelectedObjectIndex < 0 || mSelectedObjectIndex >= mGameObjects.size())
        return;
    
    // 获取拖动平面(通常是XZ平面)
    XMVECTOR planeNormal = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f); // Y轴朝上
    XMVECTOR planePoint = XMLoadFloat3(&XMFLOAT3(0.0f, mGameObjects[mSelectedObjectIndex].position.y, 0.0f));
    
    // 创建从鼠标位置发射的射线
    Camera& camera = GetCamera();
    Ray ray = camera.ScreenPointToRay(x, y, mClientWidth, mClientHeight);
    
    // 计算射线与平面的交点
    XMVECTOR rayOrigin = XMLoadFloat3(&ray.origin);
    XMVECTOR rayDirection = XMLoadFloat3(&ray.direction);
    
    float denominator = XMVectorGetX(XMVector3Dot(rayDirection, planeNormal));
    if (abs(denominator) < 1e-6f) // 射线平行于平面
        return;
    
    float t = XMVectorGetX(XMVector3Dot(planePoint - rayOrigin, planeNormal)) / denominator;
    if (t < 0) // 交点在射线反方向
        return;
    
    // 计算交点位置
    XMVECTOR hitPoint = rayOrigin + rayDirection * t;
    XMFLOAT3 newPosition;
    XMStoreFloat3(&newPosition, hitPoint);
    
    // 更新选中对象的位置
    mGameObjects[mSelectedObjectIndex].position = newPosition;
    
    // 更新对象的世界矩阵
    XMMATRIX translation = XMMatrixTranslation(
        newPosition.x, newPosition.y, newPosition.z);
        
    XMMATRIX rotation = XMLoadFloat4x4(&mGameObjects[mSelectedObjectIndex].rotation);
    XMMATRIX scaling = XMMatrixScaling(
        mGameObjects[mSelectedObjectIndex].scale.x,
        mGameObjects[mSelectedObjectIndex].scale.y,
        mGameObjects[mSelectedObjectIndex].scale.z);
        
    XMMATRIX world = scaling * rotation * translation;
    XMStoreFloat4x4(&mGameObjects[mSelectedObjectIndex].world, world);
}

17.5 小结

拾取是交互式3D应用中的基础功能,本章详细介绍了在DirectX 12中实现拾取所需的关键技术和算法。

首先,我们学习了如何将屏幕空间坐标转换为投影窗口和世界空间中的射线。这涉及将鼠标位置从屏幕坐标系统转换到规范化设备坐标(NDC),然后通过投影矩阵和视图矩阵的逆矩阵将其转换为世界空间中的射线。

接下来,我们探讨了如何在世界空间和局部空间中表示拾取射线,并分析了在不同空间中进行相交检测的性能考量。对于复杂对象,将射线转换到局部空间通常能简化碰撞检测计算。

我们详细讨论了射线与各种几何体的相交检测算法,包括轴对齐包围盒(AABB)、球体和三角形。对于包含大量三角形的网格,我们介绍了如何使用空间分割数据结构(如BVH)来加速相交检测过程。

最后,我们通过实际应用示例,展示了如何将拾取技术应用于对象选择、地形点击和拖放操作等常见交互场景。

拾取技术是实现3D场景交互的基础,它使用户能够直观地选择和操作虚拟世界中的对象。通过本章的学习,读者应该能够在自己的DirectX 12应用程序中实现高效、准确的拾取功能,从而创建更具交互性的3D体验。

17.6 练习

实现一个简单的3D场景,允许用户通过鼠标点击选择场景中的对象。选中的对象应该有视觉反馈(如高亮显示或显示边框)。

扩展上述应用,添加拖放功能,允许用户拖动选中的对象在XZ平面上移动。

实现一个地形编辑器,用户可以通过点击地形来放置或移除对象(如树木、岩石等)。

创建一个射线拾取调试可视化工具,显示从摄像机发射的射线以及与物体的交点。

实现一个多级别选择系统,允许用户先选择一个对象,然后选择该对象的特定部分(如车辆的轮子或角色的武器)。

扩展拾取系统以支持多选,用户可以通过按住Shift键选择多个对象。

实现一个基于路径的导航系统,用户可以通过点击地形来设置目标位置,然后角色会自动寻路到该位置。

创建一个物理互动系统,用户可以通过点击对象来应用力或冲量,使对象移动或旋转。

这些练习将帮助你巩固对拾取系统的理解,并将其应用到实际的交互式3D应用程序中。

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容