65.9K
CodeProject 正在变化。 阅读更多。
Home

AViD

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.81/5 (17投票s)

2009年2月17日

CPOL

10分钟阅读

viewsIcon

56150

downloadIcon

1265

一个用于可视化常见树枝状大分子模型的应用程序

AViD Logo

引言

AViD 是一个用于可视化枝晶分子的应用程序。简而言之,它旨在为枝晶分子(具有规则重复模式的高度分支分子)渲染“球棒模型”。其目的是为了满足向他人展示和交流这些结构简化模型的需求。

事实上,我不是化学家。碰巧的是,我的妻子现在即将完成她的化学博士学位,她需要一些帮助来可视化她的分子。这就是软件开发人员——我——出现的原因。

因此,我着手尝试开发一个能满足她需求的应用程序,同时在此过程中学习一些新东西。

应用程序

AViD Form

它包含以下功能:

  1. 能够渲染以下核心类型的枝晶分子:
    1. 线性 - 直线核心,键角 180°
    2. 三角 - 三点核心,键角 120°
    3. 四面体 - 四点核心,键角 109.47°
  2. 能够生长多达六代(重复模式的层级)
  3. 每代有 1-3 个分支点(枝晶),并具有适当的键角
  4. 2D 或 3D 渲染
  5. 可显示逐代生长过程
  6. 可在 3D 空间中自由旋转枝晶分子
  7. 可自定义元素的颜色
  8. 可截断/隐藏分支
  9. 可导出图像
  10. 可导出旋转动画 GIF
  11. 可将枝晶分子加载/保存到文件系统
  12. 可通过拖放文件到应用程序中加载文件

值得复用的代码片段

  1. 使用拾取射线将鼠标点击转换为世界空间
  2. 从 DirectX 捕获渲染图像
  3. DirectX 设备设置窗体的更新/修改版本(参见下面的 Ryan Cook 参考)

对未来 DirectX 开发人员有用的提示

由于这是我第一次使用 DirectX,我处理了几个对我来说并不直接明显的概念性障碍。但在我开始之前,先给出一些定义

  1. 设备 - 封装使用 DirectX 库进行渲染所有功能的 DirectX 对象
  2. 摄像机 - 客户端 DirectX 渲染的视角
  3. 世界空间 - 相对于 3D 世界原点的定位系统
  4. 对象空间 - 相对于 3D 对象原点的定位系统
  5. 基本对象类型 - 3D 世界中渲染的基础对象,所有其他对象都基于它们(点、线和三角形)
  6. 网格 - 由基本对象(即三角形)组成的复合体,用于表示 3D 对象
  7. 环境光 - 存在于整个世界空间中,无方向性的光照
  8. 漫反射光 - 存在于世界空间中,有方向的光照(定向光、点光源或聚光灯)
  9. 材质 - 为 3D 表面提供颜色信息(环境色、漫反射色、自发光色、镜面反射色和镜面反射锐度)的对象
  10. 环境色 - 当环境光照射到表面时应用于表面的颜色
  11. 漫反射色 - 当漫反射光照射到表面时应用于表面的颜色
  12. 自发光色 - 即使没有光线照射到表面时也应用于表面的颜色(即发光效果)
  13. 镜面反射色 - 作为漫反射光照射表面时产生的光泽的一部分应用于表面的颜色
  14. 镜面反射锐度 - 通过镜面反射色应用于表面的光泽度(数字越高表示光泽度越高,数字越低表示光泽度越低)
  15. 拾取射线 - 从客户端屏幕转换到 3D 世界的矢量,旨在在其内部找到一个对象

现在,让我们开始探讨使用 DirectX 时的一些难点。请注意,我提供的示例纯粹是为了理解概念。许多正在使用的对象如果可能的话应该缓存起来,因为它们的生成成本很高。

提示 #1 - 对象空间与世界空间

最初,我以为对象空间和世界空间之间的区别只是形式上的,允许你在概念上将某物称为相对于世界原点或对象原点。事实上,由于 DirectX 世界中对象渲染的结构,它远比这重要。

基本对象类型(点、线、三角形)都固有地包含位置信息。因此,当它们被渲染时,世界应该居中回到原点。这通过以下方式完成

// The following resets the world transform back to the origin.
device.Transform.World = Matrix.Identity;

之后,基本对象可以在 DirectX 世界中正确渲染。设备的 Transform 属性是我们能够移动对象在 3D 世界中渲染位置的方式。

对于更复杂的对象(使用网格的对象),网格不包含任何位置信息。当您渲染这些对象时,它们需要您使用世界变换来将对象移动到其正确位置。

// The following moves (or translates) the origin of the world to align
// with the origin of the object (i.e. its object space).
device.Transform.World = Matrix.Translation(new Vector3(12f, 14f, 8f));

不幸的是,这并不能解决我们所有的问题。我们需要在空间中旋转对象的能力,而同样的“世界变换”允许我们简单地做到这一点。

// Now, we take care of rotating the object prior to moving.
device.Transform.World = Matrix.RotationYawPitchRoll(0.1f, 0.5f, 0f)
                       * Matrix.Translation(new Vector3(12f, 14f, 8f));

然而,即使是上述方法也带来了另一个问题,这里显示的旋转将围绕对象的原点进行。在许多情况下,对象的原点不会是进行旋转的最佳点。例如,圆柱体。围绕其末端旋转与沿着圆柱体长度中点旋转是截然不同的。因此,对于这些情况,您必须首先平移旋转点,旋转,然后将旋转后的对象平移到世界空间中的正确位置。

// Rotate to move the point of rotation, rotate, and then translate the rotated object.
device.Transform.World = Matrix.Translation(new Vector3(0f, 0f, 5f)
                       * Matrix.RotationYawPitchRoll(0.1f, 0.5f, 0f)
                       * Matrix.Translation(new Vector3(12f, 14f, 8f));

提示 #2 - 网格

在我学习 DirectX 开发时,我发现网格是生成形状的默认方式。不幸的是,由于我对 DirectX 的知识不足,我没有意识到每个网格所携带的开销。

由于网格只包含对象形状的蓝图,因此它们被设计为可重用。并且应该如此。从某种意义上说,3D 对象各种特性(形状、位置、材质等)的分离实际上允许重用,因为这些组件彼此不相关。在有意义的情况下,它们可以快速为应用程序提供性能提升。

提示 #3 - 生成拾取射线

如果您想与 3D 世界进行任何形式的交互,拾取射线非常重要。在我的案例中,我希望用户能够通过鼠标右键点击来选择一个对象。通过捕获鼠标点击事件,我可以获取鼠标的位置,但我仍然需要将其转换为我的 3D 世界空间。为此,我使用了类似于以下代码的代码

int intMouseX, intMouseY;
Vector3 vecFar, vecPickRayPosition, vecPickRayDirection;

// Make sure our position is within the proper range.
intMouseX = Math.Max(0, Math.Min(mouseLocation.X, this.m_device.Viewport.Width));
intMouseY = Math.Max(0, Math.Min(mouseLocation.Y, this.m_device.Viewport.Height));

// Create two vectors for finding the thing at which the user had clicked.
vecNear = new Vector3(intMouseX, intMouseY, 0);
vecFar = new Vector3(intMouseX, intMouseY, 1);

// Transform the vectors to world space.
vecNear.Unproject(this.m_device.Viewport, this.m_device.Transform.Projection, 
                  this.m_device.Transform.View, Matrix.Identity);
vecFar.Unproject(this.m_device.Viewport, this.m_device.Transform.Projection, 
                 this.m_device.Transform.View, Matrix.Identity);

vecPickRayPosition = vecNear;
vecPickRayDirection = Vector3.Subtract(vecFar, vecNear);
vecPickRayDirection.Normalize();

近向量代表拾取射线的原点,而远向量提供方向。您可能已经注意到,近向量和远向量的 Z 分量分别为 0 和 1。这样做的原因是,它强制近向量指向点击的原点(即相机位置),并且在取消投影这两个向量时,远向量被强制指向世界空间中最远的点。

提示 #4 - 变换拾取射线

虽然网格确实包含一个名为 Intersect 的方法,它完成了大部分繁重的工作,但它没有考虑网格是如何渲染到世界中的。因此,为了克服这个问题,您必须获取用于将对象平移到世界中的矩阵并将其反转(即 Matrix.Invert(matrix))。反转矩阵后,将其应用于拾取射线,使其与未变换的网格正确对齐。

// vecPosition = The position of the pick ray
// vecDirection = The direction of the pick ray
// m_mtxInverseWorldProjection = The inverted world transform matrix for the object.

// Calculate the position and direction.
vecPosition.TransformCoordinate(this.m_mtxInverseWorldProjection);
vecDirection.TransformNormal(this.m_mtxInverseWorldProjection);
vecDirection.Normalize();

bool blnIntersected = this.m_Mesh.Intersect(vecPosition, vecDirection);

在上述代码中,当您需要保持结果向量的长度时,会使用 TransformCoordinate。对于我不关心长度的方向,我使用了 TransformNormal,它只保持方向。完成此操作后,网格现在可以正确地告诉我,转换后的拾取射线是否确实与其相交。

提示 #5 - 拾取射线和最近的对象

既然我们能够找到一个对象是否与拾取射线相交,我们面临的问题是如果有多个对象怎么办。因为我们处理的是 3D 空间,所以我们肯定会遇到多个对象落在拾取射线内的情况。为了克服这个问题,我们必须考虑拾取射线和对象的位置。简单来说,我们找到距离拾取射线原点最近的对象

// Find the distance from the pick ray position for the figure.
fltTempFigureDistance = Vector3.Subtract(vecPosition, tempFigure.Position).Length();

if ((figure == null) || (fltTempFigureDistance < fltFigureDistance))
{
  figure = tempFigure;
  fltFigureDistance = fltTempFigureDistance;
}

提示 #6 - 区域选择

我尝试了几种算法迭代,以确定区域内的所有图形。

尝试 #1 - 我的第一个想法是遍历选中区域中的每个像素并执行拾取射线。虽然代码简单且正确,但效率极低。对于任何大于 40x40 像素的盒子,它都会使 UI 卡顿几秒钟以上。

尝试 #2 - 我的下一个想法是利用碰撞检测的概念来生成该区域的 3D 盒子,并找出哪些图形与此盒子相交。在纠结是使用轴对齐边界框[^]还是有向边界框之后,我意识到我仍然有一个主要的障碍摆在我面前。我如何选择离相机最近的对象?确实,即使我能找到区域中的图形,我也无法轻易分辨哪些是可见的。

解决方案 - 经过一段时间,我突然明白了。我不需要重新发明轮子来弄清楚谁可见或不可见。图形引擎已经为我做好了这些。我只需要一种方法,根据图像来确定哪些图形在区域中。这时,我童年时期的数字绘画游戏给了我一个可行的解决方案。每个图形都会获得一个独特的颜色。保留一种颜色作为背景,我就可以将从 0xFF000001 到 0xFFFFFFFF 的所有颜色(总共 16,777,214 种颜色)映射到特定的图形。应该注意的是,这些颜色会非常接近,肉眼无法分辨它们的独特性。下面是这种技术的示例

AViD Unique Colors

不过,我确实必须克服最后一个障碍。为了提高渲染质量,我在设置中开启了多重采样。不幸的是,这会导致渲染时边缘与背景“模糊”,从而使独特的颜色假设失效。为了禁用此功能,我不得不使用以下代码动态强制渲染不使用多重采样。

// Disable multisampling to make the edges hard.
this.m_device.RenderState.MultiSampleAntiAlias = false;

致谢

  1. Ryan Cook 适配的 DirectX 设置控制面板 [^]
  2. NGif 1.0.0.0 - 来自 gOODiDEA.NET 的 GIF 动画代码 [^]

历史

  • 1.2.0.0 - 发布 (2009 年 3 月 1 日)
    • 添加了执行区域选择的功能
    • 添加了切换显示选定图形的功能
    • 添加了在图形旋转/选择之间切换的功能
    • 添加了在表单内打印的功能(以及打印预览)
    • 选定的图形现在在所有选项卡上闪烁,并由上述切换控制
  • 1.1.1.0 - 发布 (2009 年 2 月 22 日)
    • 修复了背景颜色加载不正确的问题
  • 1.1.0.0 - 发布 (2009 年 2 月 22 日)
    • 修复了图像导出仅保存为位图格式的问题
    • 添加了更改背景颜色的功能(在“自定义”选项卡上)
    • 添加了使用 Ctrl/Shift 键在“自定义”选项卡上选择/取消选择项目的功能
    • 增加了选定项目的闪烁频率,使其更显眼
  • 1.0.0.0 - 首次发布 (2009 年 2 月 7 日)
© . All rights reserved.