根据鼠标拖动方向和模型中的位置旋转模型组

时间:2013-01-08 14:36:47

标签: c# wpf graphics 3d

我在WPF中向Viewport3D添加了几个立方体,现在我想用鼠标操作它们的组。

initial cube

当我点击&拖动这些立方体中的一半,我希望孔平面在拖动方向上旋转,旋转将由RotateTransform3D处理,因此不会出现问题。

问题在于我不知道应该如何处理阻力,更确切地说: 如何知道立方体的哪些面被拖过以确定要旋转的平面?

例如在下面的例子中,我想知道我需要顺时针旋转立方体的右平面,因此蓝色面的行将位于顶部,而不是白色的面。回来。

second cube

在此示例中,顶层应逆时针旋转90度: third cube

目前我的想法是在立方体上放置某种不可见的区域,以检查用VisualTreeHelper.HitTest进行拖动的哪一个,然后确定我应该旋转哪个平面,此区域将匹配第一个拖动例如:

enter image description here

但是当我添加所有四个区域时,我又回到原点,因为我仍然需要确定方向以及根据哪些区域被“触摸”来旋转哪个面。

我愿意接受各种想法。

请注意,此立方体可以自由移动,因此当用户点击并拖动时它可能不在初始位置,这最让我感到烦恼。

PS: 拖动将通过MouseLeftButtonDownMouseMoveMouseLeftButtonUp的组合实施。

1 个答案:

答案 0 :(得分:4)

<强> MouseEvents

您需要使用VisualTreeHelper.HitTest()来挑选Visual3D个对象(如果每张脸都是单独的ModelVisual3D,则过程可能会更简单)。 Here对HitTesting提供了一些帮助,here是一个非常有用的信息,可以极大地简化选择过程。

事件剔除

假设您现在有两个来自拣配测试的ModelVisual3D个对象(一个来自MouseDown事件,另一个来自MouseUp事件)。首先,我们应该检测它们是否是共面的(以避免从一个面到另一个面的拾取)。一种方法是比较面部法线以查看它们是否指向相同的方向。如果你已经在MeshGeometry3D中定义了法线,那就太好了。如果没有,那么我们仍然可以找到它。我建议为扩展添加一个静态类。计算法线的一个例子:

public static class GeometricExtensions3D
{
    public static Vector3D FaceNormal(this MeshGeometry3D geo)
    {
        // get first triangle's positions
        var ptA = geo.Positions[geo.TriangleIndices[0]];
        var ptB = geo.Positions[geo.TriangleIndices[1]];
        var ptC = geo.Positions[geo.TriangleIndices[2]];
        // get specific vectors for right-hand normalization
        var vecAB = ptB - ptA;
        var vecBC = ptC - ptB;
        // normal is cross product
        var normal = Vector3D.CrossProduct(vecAB, vecBC);
        // unit vector for cleanliness
        normal.Normalize();
        return normal;
    }
}   

使用此功能,您可以比较MeshGeometry3D点击中的Visual3D法线(此处涉及大量投射),并查看它们是否指向同一方向。为了安全起见,我会对矢量的X,Y,Z进行公差测试而不是直线等效。另一个扩展可能会有所帮助:

    public static double SSDifference(this Vector3D vectorA, Vector3D vectorB)
    {
        // set vectors to length = 1
        vectorA.Normalize();
        vectorB.Normalize();
        // subtract to get difference vector
        var diff = Vector3D.Subtract(vectorA, vectorB);
        // sum of the squares of the difference (also happens to be difference vector squared)
        return diff.LengthSquared;
    }

如果它们不是共面的(SSDifference&gt;某些任意测试值),您可以return在这里(或提供某种反馈)。

对象选择

现在我们已经确定了我们的两个面孔,并且它们确实已经成熟,可以满足我们所希望的事件处理,我们必须推断出一种从我们所拥有的信息中剔除信息的方法。您应该仍然具有之前计算的法线。我们将再次使用它们来挑选要旋转的其余面。另一种扩展方法可以帮助进行比较,以确定是否应将一个面包含在旋转中:

    public static bool SharedColumn(this MeshGeometry3D basis, MeshGeometry3D compareTo, Vector3D normal)
    {
        foreach (Point3D basePt in basis.Positions)
        {
            foreach (Point3D compPt in compareTo.Positions)
            {
                var compToBasis = basePt - compPt; // vector from compare point to basis point
                if (normal.SSDifference(compToBasis) < float.Epsilon) // at least one will be same direction as
                {                                                     // as normal if they are shared in a column
                    return true;
                }
            }
        }
        return false;
    }

您需要为两个网格(MouseDown和MouseUp)剔除面,迭代所有面。保存需要旋转的几何列表。

<强> RotateTransform

现在是棘手的部分。轴角旋转有两个参数:Vector3D表示与旋转垂直的轴(使用右手规则)和旋转角度。但是我们的立方体的中点可能不在(0,0,0),所以旋转可能很棘手。首先,我们必须找到立方体的中点!我能想到的最简单的方法是添加立方体中每个点的X,Y和Z分量,然后将它们除以点数。当然,诀窍不是不止一次添加同一点!你如何做到这将取决于你的数据如何组织,但我认为这是一个(相对)微不足道的练习。您不需要应用变换,而是想要自己移动点,因此我们不打算创建并添加到TransformGroup,而是构建矩阵!翻译矩阵如下所示:

    1, 0, 0, dx
    0, 1, 0, dy
    0, 0, 1, dz
    0, 0, 0, 1

因此,考虑到您的立方体的中点,您的翻译矩阵将是:

    var cp = GetCubeCenterPoint(); // user-defined method of retrieving cube's center point
    // gpu's process matrices in column major order, and they are defined thusly
    var matToCenter = new Matrix3D(
            1, 0, 0, 0,
            0, 1, 0, 0,
            0, 0, 0, 1,
            -cp.X, -cp.Y, -cp.Z, 1);
    var matBackToPosition = new Matrix3D(
            1, 0, 0, 0,
            0, 1, 0, 0,
            0, 0, 0, 1,
            cp.X, cp.Y, cp.Z, 1);

这就是我们的轮换。你还参考我们从MouseEvents中挑选的两个网格吗?好!让我们定义另一个扩展名:

    public static Point3D CenterPoint(this MeshGeometry3D geo)
    {
        var midPt = new Point3D(0, 0, 0);
        var n = geo.Positions.Count;
        foreach (Point3D pt in geo.Positions)
        {
            midPt.Offset(pt.X, pt.Y, pt.Z);
        }
        midPt.X /= n; midPt.Y /= n; midPt.Z /= n;
        return midPt;
    }

获取MouseDown网格中的向量到MouseUp的网格(顺序很重要)。

    var swipeVector = MouseUpMesh.CenterPoint() - MouseDownMesh.CenterPoint();

你的命中面孔仍然正常,对吗?我们可以(基本上是神奇地)获得旋转轴:

    var rotationAxis = Vector3D.CrossProduct(swipeVector, faceNormal);

这将使您的旋转角度始终 + 90°。制作RotationMatrix(source):

    swipeVector.Normalize();
    var cosT = Math.Cos(Math.PI/2);
    var sinT = Math.Cos(Math.PI/2);
    var x = swipeVector.X;
    var y = swipeVector.Y;
    var z = swipeVector.Z;
    // build matrix, remember Column-Major
    var matRotate = new Matrix3D(
         cosT + x*x*(1 -cosT), y*x*(1 -cosT) + z*sinT, z*x*(1 -cosT) -y*sinT, 0,
        x*y*(1 -cosT) -z*sinT,   cosT + y*y*(1 -cosT), y*z*(1 -cosT) -x*sinT, 0,
        x*z*(1 -cosT) -y*sinT,  y*z*(1 -cosT) -x*sinT,  cosT + z*z*(1 -cosT), 0,
                            0,                      0,                     0, 1);

将它们组合起来获得变换矩阵,请注意顺序很重要。我们想要取点,将其转换为相对于原点的坐标,旋转它,然后按顺序将其转换回原始坐标。所以:

    var matTrans = Matrix3D.Multiply(Matrix3D.Multiply(matToCenter, matRotate), matBackToPosition);

然后,你准备移动积分了。迭代您之前标记为轮换的每个Point3D中的每个MeshGeometry3D,并执行以下操作:

    foreach (MeshGeometry3D geo in taggedGeometries)
    {
        for (int i = 0; i < geo.Positions.Count; i++)
        {
            geo.Positions[i] *= matTrans;
        }
    }

然后......哦等等,我们完成了!