3D 图形 - 带阴影的立方体
仅使用 GDI+ 创建一个可以旋转和添加阴影的立方体。
引言
基于最初的 欧拉旋转 文章,现在是时候重温 3D 图形 GDI+ 的世界了。 在上一篇文章中,我们讨论了在二维位图表面上绘制简单立方体形状的基础知识。 现在,是时候更上一层楼了。 我们将绘制一个带有阴影面的立方体,并且我们将找到一种绕过烦人的万向节锁问题的方法。
避免万向节锁
首先,是万向节锁。 在上一篇文章中,我们在绕不同轴旋转立方体时遇到了一个奇怪的问题。 由于矩阵乘法不是累积的,因此以不同顺序绕不同轴旋转会产生并非总是相同的结果。
避免该问题的一种方法是升级到更高级的数学,例如四元数。 但是,一个更简单的解决方案是重新构建 Cube
类处理旋转的方式。
让我们比较一下原始结构和新的改进结构。 最初处理旋转的方式是
- 填充立方体的 3D 点
- 旋转它们(X 轴,然后 Y 轴,然后 Z 轴)
- 将其投影到 2D 表面上
你能发现问题吗? 只要应用旋转,立方体就会从头开始并重新应用所有旋转。 因此,某些旋转看起来可能朝错误的方向移动。 答案很简单
- 在类初始化时填充立方体的 3D 点
- 旋转现有点与上次旋转的差值
- 将其投影到 2D 表面上
基本上,我们不是每次都从头开始,而是旋转我们已经拥有的东西。
引入面
在继续为立方体添加阴影之前,我们必须重新构建项目的另一部分。 将所有立方体的点分开处理,不如将它们分组为立方体面。 这样做的好处是我们可以避免手动编写绘制立方体时哪个点应该连接到哪个点。 我们所要做的就是说明每个面将如何绘制。
此外,在为立方体添加阴影时,我们需要知道绘制面的顺序。
Faces 类只需要一些基本属性
- 对应于面的 2D 点数组
- 对应于面的 3D 点数组
- 它代表的立方体侧面
- 一个代表中心的 3D 点
我们还需要包含一个 CompareTo
函数,其中比较中心点的 z
值。
public int CompareTo(Face otherFace)
{
return (int)(this.Center.z - otherFace.Center.z);
}
2D 绘图已修复
在为立方体添加阴影之前要修复的最后一件事:3D 投影。 当我第一次开始编写阴影例程时,我发现来自 欧拉旋转 文章的 2D 绘图存在缺陷。 它向后绘制所有内容! 经过大量简化和审查,这是新功能
private PointF Get2D(Vector3D vec)
{
PointF returnPoint = new PointF();
float zoom = (float)Screen.PrimaryScreen.Bounds.Width / 1.5f;
Camera tempCam = new Camera();
tempCam.position.x = cubeOrigin.x;
tempCam.position.y = cubeOrigin.y;
tempCam.position.z = (cubeOrigin.x * zoom) / cubeOrigin.x;
float zValue = -vec.z - tempCam.position.z;
returnPoint.X = (tempCam.position.x - vec.x) / zValue * zoom;
returnPoint.Y = (tempCam.position.y - vec.y) / zValue * zoom;
return returnPoint;
}
让我们来分析一下代码。 事实是,这是 3D 投影的最基本形式。 X 和 Y 值只是 3D 点的 x/z 和 y/z。 只是,我们考虑了透视或摄像机的位置,我们将其放置在立方体的中心。 最后一个关键因素是缩放变量。 将 3D 点投影到 2D 表面时,我们必须考虑该区域。 在一个较小的区域中,立方体会被压缩得更厉害,反之亦然。 在这种情况下,我们使用整个屏幕的宽度来保持立方体中的直线与整个屏幕平行/垂直。
为立方体添加阴影
最后,是时候为立方体添加阴影了。 信不信由你,这会非常简单,这要归功于我们花了很多时间来创建一个坚实的基础。 恰如其分地,一个立方体面将根据已经投影的 2D 点进行着色。 由于四个角可以创建任何随机四边形,最好使用 FillPolygon
函数
g.FillPolygon(Brushes.Gray, GetTopFace());
其中 g
是绘制到任何适当表面的 Graphics
对象。 此外,不要担心 GetTopFace()
调用,它在源代码中,它所做的就是找到立方体面的 2D 角点(在此示例中为顶部面)。
现在,一点推理将揭示以特定顺序为面添加阴影并不总是正确显示。 我们要填充立方体面的顺序取决于立方体的朝向。
幸运的是,这就是 Face
类中的 CompareTo
函数的作用。 我们可以将所有面添加到数组中,对数组进行排序,然后对其进行迭代。 记住,这些面是根据其中心的 z
值排序的。 这意味着离屏幕最近的将在第一位。 这些实际上是应该最后填充的面。 因此,我们从末尾开始,向后迭代数组。
结论
考虑到这一切都是用 GDI+ 完成的,结果令人印象深刻。 进一步的改进将是根据光照创建阴影。 但是,这超出了本文的范围。 目前,这些边是根据指定的颜色添加阴影的。