DirectX

使用 DirectX、C++ 和 XAML 实现实时、逼真的翻页效果

Eric Brumer

下载代码示例

在开发 Windows 8 与 Visual Studio 2012,期间一些开放源代码应用程序展示的各种可用的软件开发人员的 c + + 技术创建 Microsoft c + + 团队。 这些应用程序之一是"奥斯汀,"数字的笔记程序编写 c + +,在 Windows 运行时 (WinRT) 上使用 DirectX 和 XAML 的项目。

在此应用程序,用户可以创建笔记本和记笔记或自由曲线关系图。 有的添加和删除页面、 不同的墨水颜色和添加的图像文件从一台 PC 或 SkyDrive 的支持。 图 1 显示 app 的一些截图中的行动。

Project “Austin”
图 1 项目"奥斯汀"

用户可以查看自己的笔记本中三种方式:页的单个行 (如图 1)、 网格的页面或,如果页面彼此堆叠在一起。 在此堆积的视图中,用户可以翻页由他的手指刷整个页面,因为如果他在翻阅一本真正的书中。 数字页面是在真正的时间,基于用户的手指的位置,作为他翻转页面卷曲。 图 2 显示的页面卷曲在行动中。

Page Curling
图 2 页冰壶

页面-冰壶运动功能还处理页直。 当用户可以让去虽然冰壶的页面时,页面行为像一块真正的纸张:如果页面是低于某一阈值,它 uncurls 回躺平的位置 ; 如果该页面在临界值以上,它 uncurls 但完成车削。

本文介绍在深度的几何形状、 技术和代码用于执行实时页面蜷缩和直的。

几何的冰壶的页面

探索的整体设计之前, 我会几何和数学让路。 此信息从我的 MSDN 博客帖子,很大程度上采取"6 的项目奥斯汀第 2 部分:冰壶页"(bit.ly/THF40f)。

2006 纸,"3D 电子书的翻书"(L. 香港 S.K. 卡和 j. 陈),描述了如何页冰壶模拟可以通过变形假想的锥体,周围的文件中所示图 3。 通过更改形状和位置的锥体,可以模拟更多 (或更少) 的冰壶运动。

Flat Paper (Black) Curling Around a Cone (Green) to Become the Curled Paper (Red)
图 3 平纸锥周围 (黑色) 冰壶 (绿色) 变得卷曲的纸 (红色)

同样,页冰壶也模拟可以通过变形假想的圆柱周围的文件中所示图 4

Flat Paper (Black) Curling Around a Cylinder (Green) to Become the Curled Paper (Red)
图 4 平板纸 (黑色) 冰壶圆柱周围 (绿色) 变得卷曲的纸 (红色)

我的页面卷曲的方法,如下所示:

  • 如果用户从页面的右上角卷曲,变形锥角 θ 和 apex 在坐标 (0,0,Ay) 周围的页面。
  • 如果用户从中心-页面右侧的卷曲,变形与半径为 r 的圆柱周围的页面。
  • 如果用户从页面的右下角卷曲,变形周围倒锥壳的页面。
  • 如果用户在之间任何地方冰壶,变形周围的圆锥和圆柱,基于输入的 y 坐标的线性组合的页面。
  • 变形后, 将纸绕 y 轴旋转。

在这里是要变换的圆柱周围的页面的详细信息。 (香港文章描述类似几何变换锥周围的页)。用坐标给定的点 Pflat {x 1,y1,z1 = 0} 平页,目标是将其转换成 Pcurl 坐标 {x 2,y2,z2},躺在这本书的"脊柱"的半径的圆柱上的点。 现在看看图 5,其中显示了油缸的结尾。 你可以看到在 x 和 z 轴 (y 轴运行和页面)。 请注意我代表平纸和油缸使用相同的颜色,在以前的数字。

Transforming Pflat to Pcurl
图 5 转换到 Pcurl Pflat

关键的见解是从起源到 Pflat (1) 的距离相同,弧从起源到距离 Pcurl 沿柱状体。 所以,从简单的几何形状,我可以说,角 β = x 1 / r。 现在,将得到 Pcurl,拿出身的它向下移动 r 在 z 轴上,β,围绕旋转,然后将它向上移动 r 在 z 轴上。 CurlPage 法在图 6 显示变形的顶点缓冲区的页面的代码。 走抽象出来的顶点缓冲区和页面坐标信息。

图 6 变形的顶点缓冲区

void page_curl::curlPage(curl_parameters curlParams)
{
  float theta = curlParams.theta;
  float Ay = curlParams.ay;
  float alpha = curlParams.alpha;
  float conicContribution = curlParams.conicContribution;
  // As the user grabs toward the middle-right of the page, curl the
  // paper by deforming it on to a cylinder.
The cylinder radius is taken
  // as the endpoint of the cone parameters: for example,
  // cylRadius = R*sin(theta) distance to where R is the the rightmost
  // point on the page, all the way up.
float cylR = sqrt(  _vertexCountX * _vertexCountX
                    + (_vertexCountY /2 - Ay)*( _vertexCountY /2 - Ay));
  float cylRadius = cylR * sin(theta);
  // Flipping from top corner or bottom corner?
float posNegOne;
  if (conicContribution > 0)
  {
    // Top corner
    posNegOne = 1.0f;
  }
  else
  {
    // Bottom corner
    posNegOne = -1.0f;
    Ay = -Ay + _vertexCountY;
  }
  conicContribution = abs(conicContribution);
  for (int j = 0; j < _vertexCountY; j++)
  {
    for (int i = 0; i < _vertexCountX; i++)
    {
      float x = (float)i;
      float y = (float)j;
      float z = 0;
      float coneX = x;
      float coneY = y;
      float coneZ = z;
      {
        // Compute conical parameters and deform
        float R = sqrt(x * x + (y - Ay)*(y - Ay));
        float r = R * sin(theta);
        float beta  = asin(x / R) / sin(theta);
        coneX = r * sin(beta);
        coneY = R + posNegOne * Ay - r * (1 - cos(beta)) * sin(theta);
        coneZ = r * (1 - cos(beta)) * cos(theta);
        // Then rotate by alpha about the y axis
        coneX = coneX * cos(alpha) - coneZ * sin(alpha);
        coneZ = coneX * sin(alpha) + coneZ * cos(alpha);
      }
      float cylX = x;
      float cylY = y;
      float cylZ = z;
      {
        float beta = cylX / cylRadius;
        // Rotate (0,0,0) by beta around line given by x = 0, z = cylRadius
        // aka Rotate (0,0,-cylRadius) by beta, then add cylRadius back
        // to z coordinate
        cylZ = -cylRadius;
        cylX = -cylZ * sin(beta);
        cylZ = cylZ * cos(beta);
        cylZ += cylRadius;
        // Then rotate by alpha about the y axis
        cylX = cylX * cos(alpha) - cylZ * sin(alpha);
        cylZ = cylX * sin(alpha) + cylZ * cos(alpha);
      }
      // Combine cone & cylinder results
      x = conicContribution * coneX + (1-conicContribution) * cylX;
      y = conicContribution * coneY + (1-conicContribution) * cylY;
      z = conicContribution * coneZ + (1-conicContribution) * cylZ;
      _vertexBuffer[j * _vertexCountX + i].position.x = x;
      _vertexBuffer[j * _vertexCountX + i].position.y = y;
      _vertexBuffer[j * _vertexCountX + i].position.z = z;
    }
  }
}

变量 conicContribution,从-1 到 + 1,捕获在 y 轴上的位置,用户已触及。 值-1 表示触摸底部的页面,用户,+ 1 表示页面的顶部。

在 curl_parameters 中捕获变形参数的完整集合:

struct curl_parameters
{
  curl_parameters() {}
  curl_parameters(float t, float a, float ang, float c) :
    theta(t), ay(a), angle(ang), conicContribution(c) {}
  float theta;  // Angle of right-cone
  float ay;     // Location on y axis of cone apex
  float alpha;  // Rotation about y axis
  float conicContribution;  // South tip cone == -1, cylinder == 0,
    north tip cone == 1
};

请注意圆柱体的半径是缺少从这个结构 ; 我要带一个快捷方式通过计算它基于锥参数,如在图 6

结构

与几何让路,我可以集中页冰壶的体系结构与设计。 设计的目标是以允许为现实页蜷缩和直的而不会失去流动性。 例如,用户应该能够部分卷曲页、 页 uncurls 有些,所以放手然后继续冰壶页,虽然仍然是流体和切合实际的动画。

在 page_curl 类中,显示在实施该项目奥斯汀页卷曲结构图 7

图 7 page_curl 类

class page_curl
{
public:
  void attachPage(const std::shared_ptr<paper_sheet_node> &pageNode);
  void startUserCurl(float x, float y);
  void startAutoCurl();
  void onRender();
private:
  struct curl_parameters
  {
    curl_parameters() {}
    curl_parameters(float t, float a, float ang, float c) :
      theta(t), ay(a), angle(ang), conicContribution(c) {}
    float theta;  // Angle of right cone
    float ay;     // Location on y axis of cone apex
    float alpha;  // Rotation about y axis
    float conicContribution;  
    // South tip cone == -1, cylinder == 0, north tip cone == 1
  };
  void continueAutoCurl();
  page_curl::curl_parameters computeCurlParameters(float x, float y);
  void page_curl::curlPage(page_curl::curl_parameters curlParams);
  ...
Other helpers that will be discussed later ...
std::shared_ptr<paper_sheet_node> _pageNode; // Page abstraction
  bool _userCurl;           // True if the user is curling the page
  bool _autoCurl;           // True if the page is uncurling
  float _autoCurlStartTime; // The time the user let go to start uncurling
  // Allows for smooth animations
  curl_parameters _currentCone;
  curl_parameters _nextCone;
};

这里是该问题的方法:

作废 page_curl::attachPage (const std::shared_ptr <paper_sheet_node> & pageNode) 每当卷曲页由项目奥斯汀的代码调用。 Paper_sheet_node 数据结构捕获所有的相关信息页面坐标系统,以及用于呈现此特定页的 DirectX 顶点缓冲区。 在这篇文章并不被讨论执行。

作废 page_curl::startUserCurl (x,y 浮浮) 称为项目奥斯汀用户输入处理程序代码来指示用户已按下他的手指,并且是冰壶 (x,y) 位置处。 这段代码执行下面的操作:

  • 设置 _userCurl 状态位来指示用户冰壶页
  • 中要停止任何直如果它正在进行中的 _autoCurl 状态位
  • 将 _nextCurlParams 设置为基于用户的位置 (x,y) 的变形参数

void page_curl::startAutoCurl 调用项目奥斯汀用户输入处理程序以表明用户已放开的屏幕。 这段代码执行下面的操作:

  • 中的 _userCurl 状态位来指示用户不再冰壶页
  • 集的 _autoCurl 状态位来指示 uncurl 正在进行中,带有时间戳的 uncurl 开始的时候

void page_curl::onRender,由每个框架项目奥斯汀呈现循环调用。 请注意这是唯一的函数,其实变形的顶点缓冲区。 此代码如下所示:

  • 如果设置了 _userCurl 或 _autoCurl,代码变形计算从 _nextCurlParams 和 _currentCurlParams 的参数的顶点缓冲区。 使用两个可确保平稳的冰壶运动,如在本文稍后部分讨论。
  • 如果设置了 _autoCurl,该代码调用 continueAutoCurl
  • 将 _currentCurlParams 设置为 _nextCurlParams

void page_curl::continueAutoCurl 由 page_curl::onRender 调用,如果页面直。 此代码:

  • 计算基于 uncurl 启动时 _nextCurlParams
  • 如果页已完成冰壶,中 _autoCurl

page_curl::curl_parameters page_curl::computeCurlParameters (x,y 浮浮) 计算基于用户输入的参数卷曲 (theta,Ay,阿尔法,conicContribution)。

现在,您已经看到的整体体系结构,我以后再填写每个公共和私有方法。 我选择的整体设计,以确保平滑的动画变得更加容易。 关键是 startUserCurl 和 onRender,拆散和维护两个之间的状态。

现在,我将讨论其中一些方法,提供有关的设计决策的背景或动机。

平滑的动画

鉴于前面所述的功能,也许看起来适当为 startUserCurl,只需读取用户的手指的位置和为 onRender 只是变形的那些参数页。

不幸的是,这种特定的动画可以看起来很丑,如果用户移动她手指的速度非常快。 OnRender 在 60 帧 / 秒 (fps) 绘制变形页,如果有可能两个帧之间用户手指中途在屏幕上移动。 上一帧,页面变形到几乎平的状态。 如果在接下来的一帧上,页面变形到完全卷曲的状态、 动画的流动性是丢失和... 看起来丑陋。

要解决此问题,我跟踪的不仅是 _nextCurlParams (基于用户输入或直的公式所需的位置,卷曲应该去哪里),也是在 _currentCurlParams 卷曲的当前状态。 如果所需的卷曲位置离太远卷曲的现有位置,然后我应该改为冰壶到中间值,以确保动画是光滑的。

"太远"一词的解释。 因为在 cone_parameters 结构中,有四个元素,每个人都是一个浮点数,我把 _currentCurlParams 和 _nextCurlParams 当作四维空间中的点。 卷毛两个参数之间的距离就只是两个点之间的距离。

同样,"中间值"一词也是开放的解释。 如果 _nextCurlParams 是 _currentCurlParams 的距离太远,我选择一个中间点接近 _nextCurlParams 的两个点之间的距离成正比的。 所以,如果用户启动一个扁平的网页和卷发它速度极快,页面出现最初春天将提前到来快速,但然后减慢接近它获取到所需位置。 因为这发生在 60 帧/秒,总体效果是很轻微的但从可用性的角度看结果看起来很棒。

图 8 显示完整呈现代码。

图 8 渲染代码

void page_curl::onRender()
{
  // Read state under a lock
  curl_parameters nextCurlParams;
  curl_parameters currentCurlParams;
  bool userCurl;
  bool autoCurl;
  LOCK(_mutex)
  {
    nextCurlParams = _nextCurlParams;
    currentCurlParams = _currentCurlParams;
    userCurl = _userCurl;
    autoCurl = _autoCurl;
  }
  // Smooth going from currentCurlParams to nextCurlParams
  curl_parameters curl;
  float dt = nextCurlParams.theta - currentCurlParams.theta;
  float da = nextCurlParams.ay    - currentCurlParams.ay;
  float dr = nextCurlParams.alpha - currentCurlParams.alpha;
  float dc = nextCurlParams.conicContribution -
    currentCurlParams.conicContribution;
  float distance = sqrt(dt * dt + da * da + dr * dr + dc * dc);
  if (distance < constants::general::maxCurlDistance())
  {
    curl = nextCurlParams;
  }
  else
  {
    float scale = maxDistance / distance;
    curl.theta = currentCurlParams.theta + scale * dt;
    curl.ay =  currentCurlParams.ay  + scale * da;
    curl.alpha = currentCurlParams.alpha + scale * dr;
    curl.conicContribution = 
      currentCurlParams.conicContribution + scale * dc;
  }
  // Deform the vertex buffer
  if (userCurl || autoCurl)
  {
    LOCK(_mutex)
    {
      _currentCurlParams = curl;
    }
    this->curlPage(curl);
  }
  // Continue (or stop) uncurling
  if (autoCurl)
  {
    this->continueAutoCurl();
  }
}

处理用户输入 (和缺乏)

项目奥斯汀,正在 DirectX XAML c + + 应用程序,使得使用 WinRT 的 api。 手势识别由操作系统处理 — — 特别是通过 Windows::UI::Input::GestureRecognizer。

挂钩要调用 startUserCurl (x,y) 的 onManipulationUpdated 事件时,用户冰壶的页面是直截了当。 StartUserCurl 的代码然后是:

// x is scaled to (0, 1) and y is scaled to (-1, 1)
void page_curl::startUserCurl(float x, float y)
{
  curl_parameters curl = this->computeCurlParameters(x, y);
  LOCK(_mutex)
  {
    // Set curl state, to be consumed by onRender()
    _nextCurlParams = curl;
    _userCurl = true;
    _autoCurl = false;
  }
}

同样地,它是直接挂钩的 onManipulationCompleted 事件时要调用 startAutoCurl 用户可以让页面去。 图 9 显示的代码为 startAutoCurl。

图 9 startAutoCurl 方法

void page_curl::startAutoCurl()
{
  LOCK(_mutex)
  {
    // It's possible the user let go, but the page is
    // already fully curled or uncurled
    bool shouldAutoCurl = !this->doneAutoCurling(curl);
    _userCurl = false;
    _autoCurl = shouldAutoCurl;
    if (shouldAutoCurl)
    {
      _autoCurlStartTime = constants::general::currentTime();
    }
  }
}

更有趣的代码是用于处理自动直时用户可以让去页 ; 页面将继续,直到它是完全地平或直到它完全地向前卷曲的手稿。 直开始当前 curl 参数和经过的时间 (平方) 的这一转变。 这种方式,页面开始慢慢地解开,但随着时间的推移它 uncurls 越来越快。 这是廉价的方式,试图模拟重力。 图 10 显示了相应代码。

图 10 处理汽车直

void page_curl::continueAutoCurl()
{
  LOCK(_mutex)
  {
    if (this->doneAutoCurling(curl))
    {
      _autoCurl = false;
    }
    else
    {
      float time = constants::general::currentTime() - 
        _autoCurlStartTime;
      _nextCurlParams = this->nextAutoCurlParams(
        _currentCurlParams, 
        time * time);
    }
  }
}

调整为现实主义的冰壶

缺席从上一节是 computeCurl 的代码­参数、 doneAutoCurling 和 nextAutoCurlParams。 这些都是可调的职能,包括常量、 公式和试探法,是小时的艰苦试验和错误的结果。

例如,我花了几个小时,想达到 computeCurlParameters 的合理结果。 图 11 显示代码的两个版本 — — 模拟的一块厚厚的纸 (里面的书页),冰壶的一个和第二次模拟 (软封面的书,例如封面) 的塑料盖。

图 11 冰壶两种类型的页面

// Helper macro for a straight line F(x) that passes through {x1, y1} and {x2, y2}.
// This can't be a template function (C++ doesn't let you have float literals
// as template parameters).
#define STRAIGHT_LINE(x1, y1, x2, y2, x) 
    (((y2 - y1) / (x2 - x1)) * (x - x1) + y1)
page_curl::curl_parameters page_curl::paperParams(float x, float y)
{
  float theta, ay, alpha;
  if (x > 0.95f)
  {
    theta = STRAIGHT_LINE(1.0f,  90.0f, 0.95f, 60.0f, x);
    ay    = STRAIGHT_LINE(1.0f, -20.0f, 0.95f, -5.0f, x);
    alpha = 0.0;
  }
  else if (x > 0.8333f)
  {
    theta = STRAIGHT_LINE(0.95f,  60.0f, 0.8333f, 55.0f, x);
    ay    = STRAIGHT_LINE(0.95f, -5.0f,  0.8333f, -4.0f, x);
    alpha = STRAIGHT_LINE(0.95f,  0.0f,  0.8333f, 13.0f, x);
  }
  else if (x > 0.3333f)
  {
    theta = STRAIGHT_LINE(0.8333f, 55.0f, 0.3333f,  45.0f, x);
    ay    = STRAIGHT_LINE(0.8333f, -4.0f, 0.3333f, -10.0f, x);
    alpha = STRAIGHT_LINE(0.8333f, 13.0f, 0.3333f,  35.0f, x);
  }
  else if (x > 0.1666f)
  {
    theta = STRAIGHT_LINE(0.3333f,  45.0f, 0.1666f,  25.0f, x);
    ay    = STRAIGHT_LINE(0.3333f, -10.0f, 0.1666f, -30.0f, x);
    alpha = STRAIGHT_LINE(0.3333f,  35.0f, 0.1666f,  60.0f, x);
  }
  else
  {
    theta = STRAIGHT_LINE(0.1666f,  25.0f, 0.0f,  20.0f, x);
    ay    = STRAIGHT_LINE(0.1666f, -30.0f, 0.0f, -40.0f, x);
    alpha = STRAIGHT_LINE(0.1666f,  60.0f, 0.0f,  95.0f, x);
  }
  page_curl::curl_parameters cp(theta, ay, alpha, y);
  return cp;
}
page_curl::curl_parameters page_curl::plasticParams(float x, float y)
{
  float theta, ay, alpha;
  if (x > 0.95f)
  {
    theta = STRAIGHT_LINE(1.0f,  90.0f, 0.9f,  40.0f, x);
    ay    = STRAIGHT_LINE(1.0f, -30.0f, 0.9f, -20.0f, x);
    alpha = 0.0;
  }
  else
  {
    theta = STRAIGHT_LINE(0.95f,  40.0f, 0.0f,  35.0f, x);
    ay    = STRAIGHT_LINE(0.95f, -20.0f, 0.0f, -25.0f, x);
    alpha = STRAIGHT_LINE(0.95f,   0.0f, 0.0f,  95.0f, x);
  }
  page_curl::curl_parameters cp(theta, ay, angle, y);
  return cp;
}

知道冰壶仅仅是完成时的代码涉及到检查页面是否完全平坦或完全卷曲:

bool page_curl::doneAutoCurling(curl_parameters curl)
{
  bool doneCurlBackwards =  (curl.theta > 89.999f)
                         && (curl.ay < -69.999f)
                         && (curl.alpha < 0.001f)
                         && (abs(curl.conicContribution) > 0.999f);
  bool doneCurlForwards = (curl.alpha > 99.999f);
  return doneCurlBackwards || doneCurlForwards;
}

最后,我的自动卷曲,所示版本图 12,卷曲开始依靠当前的卷曲位置和所用时间的平方。 直它卷曲的方式相同的页,而我只是有卷曲参数线性方法的参数为扁平的页面,但页面让秋天落后 (如果用户放开当页面被只是略有卷曲) 或转发 (如果用户放开时页面大多卷曲)。 使用这种技术和经过的时间的平方,页面有不错反弹到它你放手的时候。 我真的很喜欢它的外观。

图 12 我的自动卷曲的版本

page_curl::curl_parameters page_curl::nextAutoCurlParams(
  curl_parameters curl, float time)
{
  curl_parameters nextCurl;
  if (curl.alpha > 40)
  {
    nextCurl.theta = min(curl.theta + time/800000.0f,  50.0f);
    nextCurl.ay    = curl.ay;
    nextCurl.alpha = min(curl.alpha + time/200000.0f, 100.0f);
  }
  else
  {
    nextCurl.theta = min(curl.theta + time/100000.0f, 90.0f);
    nextCurl.ay    = max(curl.ay - time/200000.0f,   -70.0f);
    nextCurl.alpha = max(curl.alpha - time/300000.0f,  0.0f);
  }
  if (curl.conicContribution > 0.0)
  {
    nextCurl.conicContribution =
      min(curl.conicContribution + time/100000.0f, 1.0f);
  }
  else
  {
    nextCurl.conicContribution =
      max(curl.conicContribution - time/100000.0f, -1.0f);
  }
  return nextCurl;
}

我希望我已经实施的一件事时直页惯性。 用户应该能够扔在页面。 当他们放手时,页面应该继续冰壶它被扔,直到它停止并放置到背平拖力方向相同。 这可通过将历史记录添加到 onRender,跟踪在 nextAutoCurlParams 中的公式中的最后几个位置的用户的手指,并利用此实施。

性能测试

CurlPage 方法已经做了相当多的数学卷曲单个页面。 按我的计算,有九个电话到 sin 函数,八至 cos,一到 arcsin,一到 sqrt,并围绕着两个相乘,再加上加法和减法 — — 为纸模型中的每个顶点 — — 正在呈现的每个框架 !

奥斯汀项目的目的是为 60 帧/秒 ; 因此,处理每个帧可以采取不超过 15 毫秒,以免 app 感觉呆滞。

通过确保最内层的循环矢量,在 Visual Studio c + + 编译器生成流 SIMD 扩展 2 (SSE2) 指令,利用 CPU 向量单位获得必要的性能。 编译器之所以能够超越函数 math.h 头文件中的所有矢量化。

在这种情况下,内部循环一次计算的四个顶点的卷曲的位置。 性能提升可以释放 CPU 其他渲染完成任务,如应用页面卷曲阴影。

更多关于汽车-矢量器使用 MSDN 和并行编程中的本机代码的博客 (bit.ly/bWfC5Y)。

总结

我想感谢工作项目奥斯汀,尤其是豪尔赫 · 佩雷拉、 乔治 Mileka 和 Alan Chan 的伟大的人。 我开始在该项目上的工作的时候他们已经有了很大的应用程序,和我感到幸运的是,花了一些时间给它添加一个小的现实主义。 它帮助我理解了美的简单性,以及如何艰难可以使简单的事情 !

您可以找到有关该应用程序,包括一些视频,通过搜索项目奥斯汀的 MSDN 博客上的详细信息。 你就会发现在代码 austin.codeplex.com

Eric Brumer  是在微软工作的 Visual c + + 编译器优化团队软件开发工程师。他在到达 ericbr@microsoft.com

感谢以下技术专家对本文的审阅:乔治 Mileka (Microsoft)
乔治 Mileka 是在微软的 Visual c + + 库团队工作软件开发工程师。 他在到达 gmileka@microsoft.com