DirectX

Реалистичное изгибание страниц на основе DirectX, C++ и XAML

Эрик Брюмер

Продукты и технологии:

Windows 8, C++, DirectX, XAML

В статье рассматриваются:

  • геометрия изгибания страницы (page curling);
  • архитектура кода изгибания страницы;
  • обеспечение плавной анимации;
  • обработка пользовательского ввода;
  • достижение необходимой производительности.

При разработке Windows 8 и Visual Studio 2012 группа Microsoft C++ создала некоторые приложения с открытым исходным кодом, чтобы продемонстрировать различные технологии C++, доступные разработчикам ПО. Одно из таких приложений — Project «Austin», написанное на C++ с использованием DirectX и XAML в Windows Runtime (WinRT).

В этом приложении пользователь может создать блокнот and набросать какие-то заметки или схемы. Приложение поддерживает добавление и удаление страниц, разные цвета чернил и добавление файлов изображений с ПК или из SkyDrive. На рис. 1 приведены экранные снимки этого приложения в действии.

Project «Austin»
Рис. 1. Project «Austin»

Пользователи могут просматривать свои блокноты тремя способами: в виде единой последовательности страниц (как на рис. 1), как сетку страниц или так, будто страницы сложены в стопку — одна поверх другой. В последнем режиме просмотра можно пролистывать страницы движением пальца через страницу, имитируя переворачивание страниц в реальной книге. Электронные страницы изгибаются в реальном времени в зависимости от позиции пальца, когда пользователь переворачивает страницу. Изгибание страницы в действии показано на рис. 2.

Изгибание страницы
Рис. 2. Изгибание страницы

Функциональность изгибания страниц (page-curling feature) также обрабатывает их распрямление (page uncurling). Когда пользователь двигает страницу в процессе изгибания, она ведет себя подобно настоящему листу бумаги: если страница оказывается ниже определенного порогового значения, она распрямляется обратно, а если страница выше этого порогового значения, она все равно распрямляется, но заканчивает процесс переворачивания.

В этой статье подробно описывается геометрия, технологии и код, используемый для выполнения изгибания и распрямления страниц в реальном времени.

Геометрия изгибания страницы

Прежде чем исследовать общий дизайн, рассмотрим все, что связано с геометрией и математикой этого процесса. Эта информация в основном взята из моей публикации в блоге MSDN «Project Austin Part 2 of 6: Page Curling» (bit.ly/THF40f).

В статье «Turning Pages of 3D Electronic Books» (L. Hong, S.K. Card, J. Chen) за 2006 год описывается, как можно имитировать изгибание страницы, деформируя бумагу вокруг воображаемого конуса (рис. 3). Изменяя форму и положение этого конуса, можно более-менее точно имитировать изгибание.

Плоский лист изгибается вокруг конуса, чтобы превратиться в изогнутую бумагу
Рис. 3. Плоский лист изгибается вокруг конуса, чтобы превратиться в изогнутую бумагу

Аналогично изгибание страницы можно имитировать и деформацией бумаги вокруг воображаемого цилиндра, как показано на рис. 4.

Плоский лист изгибается вокруг цилиндра, чтобы превратиться в изогнутую бумагу
Рис. 4. Плоский лист изгибается вокруг цилиндра, чтобы превратиться в изогнутую бумагу

Мой метод изгибания страниц таков:

  • если пользователь изгибает страницу сверху справа, деформируем страницу вокруг конуса с углом θ и вершиной в координатах (0, Ay, 0);
  • если пользователь изгибает страницу от центра справа, деформируем страницу вокруг цилиндра с радиусом r;
  • если пользователь изгибает страницу снизу справа, деформируем страницу вокруг обратного конуса;
  • если пользователь изгибает страницу где-то посередине, деформируем страницу вокруг линейной комбинации конуса и цилиндра, беря за основу y-координату ввода;
  • после деформации поворачиваем лист вокруг оси y.

Вот детали деформации страницы вокруг цилиндра. (В статье Хонга [Hong] описывается сходная геометрия деформации страницы вокруг конуса.) Взяв точку Pflat с координатами {x1, y1, z1 = 0} на плоской странице, нужно преобразовать ее в Pcurl с координатами {x2, y2, z2} — точку на цилиндре с радиусом r, лежащим на «корешке» книги. Теперь посмотрите на рис. 5, где показан конец цилиндра. Вы видите оси x и z (ось y входит и выходит из страницы). Заметьте, что я представляю плоский лист и цилиндр теми же оттенками серого, что и на предыдущих иллюстрациях.

Деформация буфера вершин
Рис. 5. Преобразование Pflat в Pcurl

Ключевой момент в том, что расстояние от начала координат до Pflat (x1) идентично длине дуги от начала координат до Pcurl по поверхности цилиндра. Поэтому, используя простую геометрию, я могу сказать, что угол β = x1/ r. Тогда, чтобы получить Pcurl, я беру начало координат, смещаю его вниз на величину r по оси z, поворачиваю на угол β, затем смещаю вверх на величину r по той же оси. Метод 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;
  // Когда пользователь движет палец справа к середине
  // страницы, изгибаем бумагу, деформируя ее на цилиндре.
  // Радиус цилиндра берется как конечная точка параметров
  // конуса, например cylRadius = R*sin(theta), где R
  // является крайней точкой по всей правой стороне страницы.
  float cylR = sqrt(  _vertexCountX * _vertexCountX
                     + (_vertexCountY /2 - Ay)*( _vertexCountY /2 - Ay));
  float cylRadius = cylR * sin(theta);
  // Переворачиваем из верхнего или нижнего угла?
  float posNegOne;
  if (conicContribution > 0)
  {
    // Верхний угол
    posNegOne = 1.0f;
  }
  else
  {
    // Нижний угол
    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;
      {
        // Вычисляем параметры конуса и деформируем
        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);
        // Потом поворачиваем на альфу вокруг оси y
        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;
        // Поворачиваем (0,0,0) на бету вокруг линии,
        // определяемой x = 0, z = cylRadius, т. е.
        // Rotate (0,0,-cylRadius) на бету, а затем
        // добавляем cylRadius обратно к z-координате
        cylZ = -cylRadius;
        cylX = -cylZ * sin(beta);
        cylZ = cylZ * cos(beta);
        cylZ += cylRadius;
        // Затем поворачиваем на альфу вокруг оси y
        cylX = cylX * cos(alpha) - cylZ * sin(alpha);
        cylZ = cylX * sin(alpha) + cylZ * cos(alpha);
      }
      // Комбинируем результаты по конусу и цилиндру
      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;  // угол прямого конуса
  float ay;     // положение на оси y вершины конуса
  float alpha;  // угол поворота вокруг оси y
  float conicContribution;  // южный конус вершин (south tip
                // cone) == -1, цилиндр == 0, а северный конус
                // вершин (north tip cone) == 1
};

Заметьте, что в этой структуре нет радиуса цилиндра; я вычисляю его на основе параметров конуса, как было показано на рис. 6.

Архитектура

Закончив с геометрией, сосредоточимся на дизайне и архитектуре кода изгибания страницы. Цель дизайна — обеспечить реалистичное изгибание и распрямление страницы, не теряя гибкости. Например, у пользователя должна быть возможность частично изогнуть страницу, отпустить ее, чтобы отчасти распрямилась, а затем продолжить изгибать страницу, и при этом анимация должна оставаться плавной и реалистичной.

Архитектура кода изгибания страницы в Project Austin реализована в классе 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;  // угол прямого конуса
    float ay;     // положение на оси y вершины конуса
    float alpha;  // угол поворота вокруг оси y
    float conicContribution;  // южный конус вершин (south tip
                // cone) == -1, цилиндр == 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);
  ...другие вспомогательные методы, которые мы обсудим позже...
  // Абстракция страницы
  std::shared_ptr<paper_sheet_node> _pageNode;
  // True, если пользователь изгибает страницу
  bool _userCurl;
  // True, если страница распрямляется
  bool _autoCurl;
  // Время, в которое пользователь начал распрямлять страницу
  float _autoCurlStartTime;
  // Обеспечиваем плавную анимацию
  curl_parameters _currentCone;
  curl_parameters _nextCone;
};

Вот важные методы:

void page_curl::attachPage(const std::shared_ptr<paper_sheet_node> &pageNode) — вызывается кодом Project Austin всякий раз, когда происходит изгибание страницы. Структура данных paper_sheet_node захватывает всю релевантную информацию о системе координат страницы, а также о буфер вершин DirectX, используемом для рендеринга данной страницы. Реализация в этой статье не обсуждается;

void page_curl::startUserCurl(float x, float y) — вызывается кодом обработчика пользовательского ввода Project Austin, указывая, что пользователь прижал палец и начал процедуру изгибания в точке (x, y). Этот код делает следующее:

  • устанавливает бит состояния _userCurl, сообщая о том, что пользователь изгибает страницу;
  • сбрасывает бит состояния _autoCurl, чтобы остановить распрямление, если оно происходит;
  • присваивает значения из _nextCurlParams параметрам деформации на основе позиции пальца пользователя (x, y);

void page_curl::startAutoCurl — вызывается обработчиком пользовательского ввода Project Austin, указывая, что пользователь убрал палец с экрана. Этот код делает следующее:

  • сбрасывает бит состояния _userCurl, сообщая, что пользователь больше не изгибает страницу;
  • устанавливает бит состояния _autoCurl, указывая, что происходит распрямление, и сообщает временную отметку начала распрямления;

void page_curl::onRender — вызывается циклом рендеринга Project Austin для каждого кадра. Заметьте, что это единственная функция, которая реально деформирует буфер вершин. Этот код делает следующее:

  • если установлен _userCurl или _autoCurl, код деформирует буфер вершин до параметров, вычисленных по _nextCurlParams и _currentCurlParams. Использование обеих переменных гарантирует плавное изгибание, как будет показано далее в этой статье.

Цель дизайна — обеспечить реалистичное изгибание и распрямление страницы, не теряя гибкости.

  • если установлен _autoCurl, код вызывает continueAutoCurl;
  • записывает _currentCurlParams в _nextCurlParams;

void page_curl::continueAutoCurl — вызывается page_curl::onRender, если страница распрямляется. Этот код:

  • вычисляет _nextCurlParams на основе того, когда началось распрямление;
  • сбрасывает _autoCurl, если изгибание страницы закончено;

page_curl::curl_parameters page_curl::computeCurlParameters(float x, float y) вычисляет параметры изгибания (theta, Ay, alpha, conicContribution) на основе пользовательского ввода.

Теперь, когда вы увидели общую архитектуру, я заполню все открытые и закрытые методы. Я выбрал дизайн таковым, чтобы было легко обеспечить плавную анимацию. Здесь самое главное — разделение startUserCurl и onRender и поддержание состояния между ними.

Далее мы обсудим некоторые из этих методов, в том числе мотивацию, касающуюся решений, принимавшихся по дизайну.

Плавная анимация

Учитывая ранее описанные функции, может показаться подходящим сделать так, чтобы startUserCurl просто считывала позицию пальца пользователя, а onRender просто деформировала страницу по этим параметрам.

К сожалению, эта анимация может выглядеть уродливо, если пользователь будет двигать пальцем очень быстро. Если onRender рисует деформированную страницу с частотой 60 кадров в секунду (fps), то вполне вероятно, что между двумя кадрами пользователь успеет сместить палец через половину экрана. За один кадр страница будет деформирована почти до плоского состояния. Если на следующем кадре страница окажется деформированной до полного изгибания, плавность анимации исчезнет и... будет выглядеть просто ужасно.

Чтобы обойти эту проблему, я отслеживаю не только _nextCurlParams (желательное место, до которого должно дойти изгибание на основе либо пользовательского ввода, либо расчетов для распрямления), но и текущее состояние изгибания в _currentCurlParams. Если желательное место изгибания находится слишком далеко от существующей позиции изгиба, то вместо этого следует привести позицию изгиба к промежуточным значениям, чтобы обеспечить плавность анимации.

Термин «слишком далеко» открыт для разных интерпретаций. Поскольку в структуре cone_parameters четыре элемента, каждый из которых является значением с плавающей точкой, я рассматриваю _currentCurlParams и _nextCurlParams как точки в четырехмерном пространстве. В таком случае расстояние между двумя наборами параметров изгиба — это просто расстояние между двумя точками.

Аналогично можно интерпретировать и термин «промежуточные значения». Если _nextCurlParams слишком далек от _currentCurlParams, я выбираю промежуточную точку, которая ближе к _nextCurlParams пропорционально расстоянию между этими двумя точками. Таким образом, если пользователь начинает с плоской страницы и изгибает ее крайне быстро, страница сначала быстр изогнется, но потом будет замедляться по мере приближения к желательному месту. Поскольку это происходит с частотой 60 fps, общий эффект очень незначителен, но с точки зрения пользователя результат выглядит просто отлично.

Весь код рендеринга показан на рис. 8.

Рис. 8. Код рендеринга

void page_curl::onRender()
{
  // Считываем состояние под блокировкой
  curl_parameters nextCurlParams;
  curl_parameters currentCurlParams;
  bool userCurl;
  bool autoCurl;
  LOCK(_mutex)
  {
    nextCurlParams = _nextCurlParams;
    currentCurlParams = _currentCurlParams;
    userCurl = _userCurl;
    autoCurl = _autoCurl;
  }
  // Плавный переход от currentCurlParams к 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;
  }
  // Деформируем буфер вершин
  if (userCurl || autoCurl)
  {
    LOCK(_mutex)
    {
      _currentCurlParams = curl;
    }
    this->curlPage(curl);
  }
  // Продолжаем (или прекращаем) распрямление
  if (autoCurl)
  {
    this->continueAutoCurl();
  }
}

Обработка пользовательского ввода (или отсутствия такового)

Project Austin, будучи приложением на основе C++, DirectX и XAML, использует WinRT API. Распознавание жестов обрабатывается ОС, а именно: Windows::UI::Input::GestureRecognizer.

Подключение события onManipulationUpdated к вызову startUserCurl(x, y), когда пользователь изгибает страницу, достаточно прямолинейно. Код для startUserCurl выглядит так:

// Масштаб по оси x (0, 1), а по оси y - (-1, 1)
void page_curl::startUserCurl(float x, float y)
{
  curl_parameters curl = this->computeCurlParameters(x, y);
  LOCK(_mutex)
  {
    // Устанавливаем состояние curl, используемое onRender()
    _nextCurlParams = curl;
    _userCurl = true;
    _autoCurl = false;
  }
}

Более интересный код — обработка автоматического распрямления.

Так же несложно подключить событие onManipulationCompleted к вызову startAutoCurl, когда пользователь отпускает страницу. Код для startAutoCurl показан на рис. 9.

Рис. 9. Метод startAutoCurl

void page_curl::startAutoCurl()
{
  LOCK(_mutex)
  {
    // Есть вероятность, что пользователь отпустит страницу,
    // но она уже полностью изогнута или распрямлена
    bool shouldAutoCurl = !this->doneAutoCurling(curl);
    _userCurl = false;
    _autoCurl = shouldAutoCurl;
    if (shouldAutoCurl)
    {
      _autoCurlStartTime = constants::general::currentTime();
    }
  }
}

Более интересный код — обработка автоматического распрямления (auto-uncurling), когда пользователь отпускает страницу; она продолжит распрямляться, пока не станет совершенно плоской или пока не закончится ее перелистывание. Я основываю это преобразование на текущих параметрах curl и времени (квадратичном), прошедшем с момента начала распрямления. Тем самым страница начинает медленно распрямляться, но со временем этот процесс ускоряется. Это простой способ имитации гравитации. Код представлен на рис. 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);
    }
  }
}

Тонкая настройка изгибания для большей реалистичности

В предыдущих раздел отсутствовал код для computeCurlParameters, doneAutoCurling и nextAutoCurlParams. Это настраиваемые функции, включающие константы, формулы и эвристическую логику, выведенные на основе долгих экспериментов методом проб и ошибок.

Например, я потратил много-много часов, пытаясь добиться приемлемых результатов для computeCurlParameters. На рис. 11 показаны две версии кода: одна имитирует изгибание толстой пачки страниц (в книге), а другая — пластиковой обложки (скажем, обложки книги в мягком переплете).

Рис. 11. Изгибание двух видов страниц

// Вспомогательный макрос для прямой линии F(x), которая
// пересекает {x1, y1} и {x2, y2}. Функция-шаблон здесь
// не годится (C++ не позволяет использовать литералы
// с плавающей точкой в качестве параметров шаблона).
#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) опирается на текущую позицию изгиба (curl) и квадратичное значение времени, прошедшего с момента начала изгибания. Вместо распрямления страницы тем же способом, которым она была изогнута, я просто линейно приближаю параметры изгиба к параметрам плоской страницы, но обеспечиваю падение страницы обратно (если пользователь отпускает страницу, когда она лишь немного изогнута) или вперед (если страница отпущена, когда изгибание близко к завершению). Используя эту методику и квадратичное значение истекшего времени, я добиваюсь того, что страница реалистично отскакивает на место при ее отпускании. Мне очень нравится, как это выглядит.

Рис. 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 и примерно два десятка операций умножения, а также операции сложения и вычитания, и все это происходит для каждого визуализируемого кадра!

Project Austin рассчитан на 60 fps; таким образом, обработка каждого кадра может занимать не более 15 мс, а иначе приложение будет запаздывать.

Необходимая производительность достигается векторизацией внутреннего цикла, где компилятор Visual C++ генерирует инструкции Streaming SIMD Extensions 2 (SSE2), чтобы задействовать преимущества аппаратных блоков процессора для обработки векторов. Компилятор способен к векторизации всех трансцендентных функций в заголовочном файле math.h.

В этом случае внутренний цикл вычисляет позицию изгиба для четырех вершин одновременно. Этот скачок в производительности освобождает процессор для других задач рендеринга, таких как применение теней к изогнутой странице.

Подробнее об автоматической векторизации читайте в MSDN и блоге «Parallel Programming in Native Code» (bit.ly/bWfC5Y).

Заключение

Я хотел бы выразить благодарность великим людям, работавшим над Project Austin, в частности Хорхе Перейре (Jorge Pereira), Джорджу Майлику (George Mileka) и Алану Чану (Alan Chan). К тому моменту, когда я включился в работу над этим проектом, он уже был великолепным приложением, и я рад, что мне повезло немного поучаствовать в этом проекте, придав приложению чуточку больше реализма. Это помогло мне понять красоту в простоте и то, насколько трудно делать вещи простыми!

Более подробные сведения об этом приложении, включая некоторые видеоролики, вы найдете в блогах MSDN с помощью поиска по ключевому словосочетанию «Project Austin». А его исходный код хранится на austin.codeplex.com.


Эрик Брюмер (Eric Brumer) — инженер-разработчик ПО в Microsoft из группы оптимизаций компилятора Visual C++. С ним можно связаться по адресу ericbr@microsoft.com.

Выражаю благодарность за рецензирование статьи эксперту Microsoft Джорджу Майлику (George Mileka).