Главная » Разработка для Windows Phone 7 » Аффинные и неаффинные преобразования Windows Phone 7

0

Иногда удобно создать трансформацию, которая обеспечивает отображение определенного набора точек в определенном местоположении. Например, рассмотрим приложение, включающее три экземпляра только что рассмотренного нами компонента Dragger, и попробуем поперетягивать углы Texture2D по экрану произвольным образом:

Данное приложение использует аффинное преобразование. Это означает, что прямоугольники всегда отображаются в параллелограммы. Четвертый угол недоступен для перетягивания, потому что всегда определяется тремя другими.

Нельзя просто выбрать любые три точки. Ничего не получится, если попытаться сделать внутренний угол больше 180°.

Опишем все это математически. Для начала проще будет принять, что исходное изображение, которые мы будем впоследствии трансформировать, имеет ширину 1 пиксел и высоту тоже 1 пиксел. Мы хотим получить преобразование, обеспечивающее следующие отображения трех углов изображения в три произвольные точки:

(0,0)?(x0,yо)

 (1,0)?(x1 y1)

(0,1)?(x22)

Это верхний левый, верхний правый и нижний левый углы, соответственно. Применяя поля объекта Matrix, определенного в XNA, получаем такие уравнения преобразования:

х’ = M11*x + М21 * у + М41

у’ = М12*x + М22 * у + М42

Не сложно применить это преобразование к точкам (0, 0), (1, 0) и (0, 1) и найти элементы матрицы:

M11 = х10

М12 =у10

М21 = х20

М22 = у2-Уо

М41 = х0

М42 = уо

Статический класс MatrixHelper (Вспомогательный класс для работы с матрицей) из библиотеки Petzold.Phone.Xna включает метод ComputeAffineTransform (Вычислить аффинное преобразование), который создает объект Matrix на основании следующих уравнений:

Проект XNA: Petzold.Phone.Xna Файл: MatrixHelper.cs (фрагмент)

static Matrix ComputeAffineTransform(Vector2 ptUL, Vector2 ptUR, Vector2 ptLL) {

return new Matrix() {

M11 = (ptUR.X – ptUL.X), M12 = (ptUR.Y – ptUL.Y), M21 = (ptLL.X – ptUL.X), M22 = (ptLL.Y – ptUL.Y), M33 = 1, M41 = ptUL.X, M4 2 = ptUL.Y, M4 4 = 1

};

}

Этот метод не является открытым, потому что сам по себе он не представляет особой ценности. Он не очень полезен, потому что в используемых им уравнениях за основу взята трансформация изображения один пиксел шириной и один пиксел высотой. Но обратите внимание, что в коде полям M33 и M44 задается значение 1. Это не происходит автоматически и необходимо для обеспечения корректной работы матрицы.

Чтобы вычислить Matrix для аффинного преобразования, применяемого к объекту конкретного размера, намного полезнее следующий открытый метод:

Проект XNA: Petzold.Phone.Xna Файл: MatrixHelper.cs (фрагмент)

public static Matrix ComputeMatrix(Vector2 size, Vector2 ptUL, Vector2 ptUR, Vector2

ptLL) {

// Трансформация масштабирования

Matrix S = Matrix.CreateScale(1 / size.X, 1 / size.Y, 1);

// Аффинное преобразование

Matrix A = ComputeAffineTransform(ptUL, ptUR, ptLL);

// Произведение двух трансформаций return S * A;

}

Первая трансформация масштабирует объект, уменьшая его размер до 1×1. После этого к нему применяется вычисленное аффинное преобразование.

Два приведенные выше снимка экрана относятся к проекту AffineTransform (Аффинное преобразование). В нем перегруженный метод Initialize создает три экземпляра компонента Dragger, задает обработчик события PositionChanged и добавляет этот компонент в коллекцию Components:

Проект XNA: AffineTransform Файл: Game1.cs (фрагмент)

public class Game1 : Microsoft.Xna.Framework.Game {

GraphicsDeviceManager graphics; SpriteBatch spriteBatch;

Texture2D texture;

Matrix matrix = Matrix.Identity;

Dragger draggerUL, draggerUR, draggerLL;

protected override void Initialize() {

draggerUL = new Dragger(this);

draggerUL.PositionChanged += OnDraggerPositionChanged; this.Components.Add(draggerUL);

draggerUR = new Dragger(this);

draggerUR.PositionChanged += OnDraggerPositionChanged; this.Components.Add(draggerUR);

draggerLL = new Dragger(this);

draggerLL.PositionChanged += OnDraggerPositionChanged; this.Components.Add(draggerLL);

base.Initialize();

}

}

Не забывайте добавлять компоненты в коллекцию Components класса Game!

Перегруженный LoadContent отвечает за загрузку изображения, которое будет трансформироваться, и инициализацию свойств Position трех компонентов Dragger в трех углах изображения:

Проект XNA: AffineTransform Файл: Game1.cs (фрагмент)

protected override void LoadContent() {

// Создаем новый SpriteBatch, который может использоваться для отрисовки текстур spriteBatch = new SpriteBatch(GraphicsDevice);

Viewport viewport = this.GraphicsDevice.Viewport; texture = this.Content.Load<Texture2D>("PetzoldTattoo");

draggerUL.Position = new Vector2((viewport.Width – texture.Width) / 2,

(viewport.Height – texture.Height) / 2);

draggerUR.Position = draggerUL.Position + new Vector2(texture.Width, 0); draggerLL.Position = draggerUL.Position + new Vector2(0, texture.Height);

OnDraggerPositionChanged(null, EventArgs.Empty);

}

Dragger формирует событие PositionChanged, только если компонент на самом деле перетягивается пользователем. Поэтому метод LoadContent завершается моделированием события PositionChanged, которое обеспечивает вычисление исходного Matrix на основании размера Texture2D и исходных координат компонентов Dragger:

Проект XNA: AffineTransform Файл: Game1.cs (фрагмент)

void OnDraggerPositionChanged(object sender, EventArgs args) {

matrix = MatrixHelper.ComputeMatrix(new Vector2(texture.Width, texture.Height),

draggerUL.Position, draggerUR.Position, draggerLL.Position);

}

Данное приложение не предполагает никакого другого сенсорного ввода, кроме посредством компонентов Dragger. Dragger реализует интерфейс IProcessTouch, поэтому приложение направляет сенсорный ввод в компоненты Dragger. Компоненты Dragger отвечают на это собственным перемещением и заданием новых значений свойствам Position, что обусловливает формирование событий PositionChanged.

Проект XNA: AffineTransform Файл: Game1.cs (фрагмент)

protected override void Update(GameTime gameTime) {

// Обеспечиваем возможность выхода из игры

if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit();

TouchCollection touches = TouchPanel.GetState();

foreach (TouchLocation touch in touches) {

bool touchHandled = false;

foreach (GameComponent component in this.Components) {

if (component is IProcessTouch &&

(component as IProcessTouch).ProcessTouch(touch))

{

touchHandled = true; break;

}

}

if (touchHandled == true) continue;

}

base.Update(gameTime);

Приложение может не задавать обработчики событий PositionChanged компонентов Dragger и вместо этого проводить сверку значений свойств Position при каждом вызове Update и пересчитывать Matrix на основании этих значений. Однако намного более эффективно пересчитывать Matrix только по фактическому изменению значения одного из свойств Position.

Перегруженный Draw использует Matrix для отображения текстуры:

Проект XNA: AffineTransform Файл: Game1.cs (фрагмент)

protected override void Draw(GameTime gameTime) {

GraphicsDevice.Clear(Color.CornflowerBlue);

spriteBatch.Begin(SpriteSortMode.Immediate, null, null, null, null, null, matrix);

spriteBatch.Draw(texture, Vector2.Zero, Color.White); spriteBatch.End();

base.Draw(gameTime);

}

Для AffineTransform необходимо устранить возможность превышения значения 180° для любого из внутренних углов (иначе говоря, изображение должно оставаться выпуклым). Аффинные преобразования могут представлять привычные операции переноса, масштабирования, вращения и сдвига, но они никогда не приводят к превращению квадрата в нечто более экзотичное, чем параллелограмм.

Неаффинные преобразования намного чаще применяются в трехмерной графике, чем в двухмерной. В 3D графике неаффинные преобразования необходимы для реализации эффектов перспективы. Длинная прямая пустынная дорога в 3D-мире должна сужаться, убегая вдаль, точно так же как и в реальном мире. Хотя мы знаем, что стороны дороги остаются параллельными, визуально на бесконечном удалении они сходятся. Этот эффект сужения является характеристикой неаффинных преобразований.

Полная матрица трансформации для трехмерной координатной точки выглядит следующим образом:

Поскольку x, y и z – последние буквы алфавита, для представления четвертого измерения выбрана буква w. Прежде всего, для умножения на матрицу 4×4 трехмерную координатную точку необходимо представить как точку в четырехмерной системе координат. Следующие уравнения являются результатом умножения матриц:

х’ = М11*x + М21 * у + М31 * z + М41

у’ = М12 *x + М22 * у + М32 * z + М42

z’ = М13 *x + М23 * у + МЗЗ* z + М43

w’ = М14 *x + М24 * у + М34 * z + М44

Для аффинного преобразования M14, M24 и M34 равны нулю, и M44 равно 1, поэтому w’ равна 1, и все преобразование происходит в четырехмерном пространстве. Для неаффинных преобразований w’не равна 1, и чтобы спроецировать четырехмерное пространство назад в

Сужение здесь обусловлено делением. Если M14 имеет положительное значение, к примеру, тогда w" будет расти при увеличении x, и графический объект будет уменьшаться по мере увеличения x.

Что происходит, если w’ принимает нулевое значение? Процесс вступает в неаффинную фазу: координаты могут принимать бесконечные значения. Обычно бесконечные объекты стараются держать вне поля видимости, потому что они зрительно искривляют экран.

трехмерное, из четырехмерной точки (x’, y’, z’, w’) необходимо получить трехмерную точку следующим образом:

Данное приложение называется NonAffineTransform (Неаффинное преобразование) и очень похоже на AffineTransform. Отличие лишь в наличии четвертого компонента и использовании несколько более сложного метода класса MatrixHelper библиотеки Petzold.Phone.Xna. Пользователь получает довольно большую свободу по перемещению углов изображения, и если только он не пытается получить вогнутый четырехугольник, изображение будет растягиваться соответственно.

Опять же математическое описание строится на предположении о том, что высота и ширина исходного изображения, которое мы будем трансформировать, составляют 1 пиксел. Теперь нам необходимо получить преобразование, которое обеспечивало бы проецирование четырех углов квадрата в четыре произвольные точки:

(0,0)?(х0,у0)

 (1,0)?(Х1,Y1)

(0,1)?(х2,у2)

(1,1)?(x3,y3)

Необходимое нам преобразование будет намного проще получить, если разбить его на два преобразования:

(0,0 ) ? (0,0) ? (х00)

(1,0 )? (1,0)? (x1, у1)

 (0,1)? (0,1) ? (х2,y2)

(1,1 )?(а,Ь)?(x3,уз)

Первое преобразование является неаффинным, я назову его B. Второе преобразование я принудительно сделаю аффинным и назову его A (от «аффинный»). Составное преобразование – BxA. Задача – выполнить двойное преобразования для точки (a, b).

Точка (0, 0) проецируется в точку (0, 0). Это говорит нам о том, что M41 и M42 равны нулю, и M44 не равно нулю. Рискнем и примем M44 равным 1.

Точка (1, 0) проецируется в (1, 0), что говорит о том, что M12 равно нулю, и M14 = M11 – 1.

Я уже определил аффинное преобразование, но хочу, чтобы это аффинное преобразование обеспечивало проецирование точки (a, b) в точку (x3, У3). Что это за точка (a, b)? Если применить к точке (a, b) аффинное преобразование и разрешить его для a и b, мы получим:

Теперь обратимся к неаффинному преобразованию, которое должно обеспечивать такое проецирование:

(0,0)?(0,0)

(1,0)? (1,0)

(0,1)? (0,1)

(1,1)? (а,Ь)

Обобщенные уравнения для неаффинного преобразования в двухмерной плоскости (с использованием имен полей структуры Matrix и включением деления на w) имеют следующий вид:

И a и b уже вычислены для аффинного преобразования.

Все эти вычисления включены во второй статический метод MatrixHelper.ComputeMatrix библиотеки Petzold.Phone.Xna:

Проект XNA: Petzold.Phone.Xna Файл: MatrixHelper.cs

public static Matrix ComputeMatrix(Vector2 size, Vector2 ptUL, Vector2 ptUR,

Vector2 ptLL, Vector2 ptLR)

{

// Трансформация масштабирования

Matrix S = Matrix.CreateScale(1 / size.X, 1 / size.Y, 1); // Аффинное преобразование

Matrix A = ComputeAffineTransform(ptUL, ptUR, ptLL);

// Неаффинное преобразование Matrix B = new Matrix();

float den = A.M11 * A.M22 – A.M12 * A.M21; float a = (A.M22 * ptLR.X – A.M21 * ptLR.Y +

A.M21 * A.M42 – A.M22 * A.M41) / den;

float b = (A.M11 * ptLR.Y – A.M12 * ptLR.X +

A.M12 * A.M41 – A.M11 * A.M4 2) / den;

B.M11 = a / (a + b – 1); B.M22 = b / (a + b – 1); B.M33 = 1; B.M14 = B.M11 – 1; B.M24 = B.M22 – 1; B.M4 4 = 1;

// Произведение трех трансформаций return S * B * A;

}

Я не привожу приложение NonAffineTransform здесь, потому что оно во многом повторяет AffineTransform, лишь с добавлением четвертого компонента Dragger, свойство Position которого передается во второй метод ComputeMatrix.

Точка (0, 1) проецируется в (0, 1), что говорит о том, что M21 равно нулю, и M24 = M22 – 1. Точка (1, 1) проецируется в (a, b), что требует небольших вычислений:

Самое большое отличие этого нового приложения в том, что неаффинные преобразования намного более забавные!

Источник: Чарльз Петзольд, Программируем Windows Phone 7, Microsoft Press, © 2011.

По теме:

  • Комментарии