Главная » Разработка для Windows Phone 7 » Динамические текстуры Windows Phone 7

0

Самый обычный способ получения объекта Texture2D для приложения на XNA – загрузить его как содержимое. В главе 4 мы также видели, как приложение создает Texture2D из объекта Stream с помощью статического метода Texture2D.FromSteam. Этот объект может ссылаться на растровое изображение, загруженное из Интернета, или изображение из библиотеки фотографий пользователя, или фотографию, только что снятую камерой телефона.

Также объект Texture2D может создаваться полностью в коде с использованием такого конструктора:

Texture2D texture = new Texture2D(this.GraphicsDevice, width, height);

Аргументы width и height – это величины типа integer, обозначающие желаемый размер Texture2D в пикселах. После того как Texture2D создан, этот размер не может быть изменен. Общее число пикселов в растровом изображении легко подсчитать, умножив width на height. Данный конструктор создает растровую матрицу, заполненную нулями. И теперь главный вопрос: как заполнить эту растровую матрицу реальным содержимым?

Есть два способа:

•                                   Рисовать в растровой матрице точно так же, как мы рисуем на экране устройства.

•                                   Алгоритмически обрабатывать значения пикселов, составляющих растровую матрицу.

Эти две техники могут комбинироваться друг с другом или использоваться отдельно. С их помощью можно изменять уже существующее изображение.

Целевой объект прорисовки

Строго говоря, первая из упомянутых техник не может использоваться с объектом Texture2D. Необходимо создать экземпляр класса, наследуемого от Texture2D, под именем RenderTarget2D:

RenderTarget2D renderTarget = new RenderTarget2D(this.GraphicsDevice, width, height);

Как всегда при использовании свойства GraphicsDevice класса Game, мы должны подождать, пока метод LoadContent создаст объекты Texture2D или RenderTarget2D, необходимые нашему приложению. Обычно эти объекты сохраняются в полях, что позволяет выводить их на экран позже, при выполнении перегруженного метода Draw.

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

Как известно, отрисовка образов на экране устройства имеет место во время выполнения перегруженного метода Draw класса Game. Закрасить весь экран определенным цветом, можно вызвав метод Clear (Очистить) объекта GraphicsDevice, ассоциированного с нашей игрой:

this.GraphicsDevice.Clear(Color.CornflowerBlue);

Отрисовку объектов Texture2D и текстовых строк на экране можно реализовать, используя объект SpriteBatch:

spriteBatch.Begin();

spriteBatch.Draw(…);

spriteBatch.DrawString(…); spriteBatch.End();

Этот объект SpriteBatch, как обычно, создается в перегруженном методе LoadContent. Он ассоциирован с объектом GraphicsDevice, потому что GraphicsDevice необходим в его конструкторе:

spriteBatch = new SpriteBatch(this.GraphicsDevice);

Вызовы метода Clear объекта GraphicsDevice и методов Draw и DrawString объекта SpriteBatch, на самом деле, обеспечивают отрисовку в растровой матрице, называемой задним буфером, содержимое которой после этого передается на экран. Некоторые сведения о заднем буфере можно получить через свойство PresentationParameters (Параметры представления) объекта GraphicsDevice. Если приложение выполняется на телефоне с большим экраном, и для заднего буфера не обозначено то, что должны использоваться размеры, отличные от заданных по умолчанию, свойства BackBufferWidth (Ширина заднего буфера) и BackBufferHeight (Высота заднего буфера) объекта PresentationParameters будут возвращать значения 800 и 480, соответственно.

Класс PresentationParameters также определяет свойство BackBufferFormat (Формат заднего буфера), значением которого является член перечисления SurfaceFormat (Формат поверхности). Этот формат показывает и количество бит в каждом пикселе, и то, как эти биты представляют цвет. Для устройств, работающих под управлением Windows Phone 7, значением свойства BackBufferFormat является SurfaceFormat.Bgr565. Это означает, что каждый пиксел описывается 16 битами, из которых по 5 битов отведено для красного (R) и синего (B), и 6 – для зеленого (G) каналов в следующей конфигурации битов:

RRRRRGGGGGGBBBBB

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

Если посмотреть на градиенты цвета на экране устройства Windows Phone 7 – один из них вскоре будет представлен в данной главе – можно заметить, что они не настолько гладкие и равномерные, как выглядят градиенты на экранах настольных компьютеров. Чаще всего для настольных мониторов используются видеоадаптеры, в которых для каждого основного цвета отведено по 8 бит. Тех пяти или шести бит, которые отводятся в устройстве Windows Phone 7, совершенно не достаточно для представления градаций цвета, воспринимаемых большинством людей. Скорее всего, в будущем в устройствах Windows Phone мы выйдем за рамки 16-разрядного представления цвета.

В приложении на XNA обычный черный буфер в объекте GraphicsDevice временно может быть заменен объектом типа RenderTarget2D:

this. GraphicsDevice.SetRenderTarget(renderTarget);

После этого этот RenderTarget2D можно использовать для отрисовки так же, как используется черный буфер. По завершении рисования мы разрываем связь между RenderTarget2D и GraphicsDevice с помощью повторного вызова SetRenderTarget (Задать целевой объект прорисовки), передав в него аргумент null:

this. GraphicsDevice.SetRenderTarget(null);

Теперь GraphicsDevice возвращен в обычное состояние.

В случае создания RenderTarget2D, который остается неизменным в ходе всего выполнения приложения, вся эта операция обычно производится во время выполнения перегруженного метода LoadContent. Если RenderTarget2D должен меняться, рисование в растровой матрице может быть реализовано в ходе выполнения перегруженного Update. RenderTarget2D наследуется от Texture2D, поэтому RenderTarget2D может отображаться на экране во время выполнения перегруженного Draw, как любое другое изображение Texture2D.

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

Предположим, требуется выводить на экран нечто подобное:

Это набор текстовых строк «Windows Phone 7», выстроенных вокруг некоторой центральной точки, цвета которых изменяются от голубого к желтому. Конечно, мы можем включить в перегруженный Draw цикл, который будет делать 32 вызова метода DrawString объекта SpriteBatch. Но если объединить эти текстовые строки в одно растровое изображение, все эти вызовы в перегруженном Draw можно будет заменить всего одним вызовом метода Draw объекта SpriteBatch. Более того, в этом случае весь этот набор текстовых строк очень просто рассматривать как единую сущность и, например, вращать, как «волчок».

Эта идея легла в основу приложения PinwheelText (Волчок с текстом). Содержимое приложения включает SpriteFont, в качестве которого задан Segoe UI Mono размером 14 пунктов, но объекта SpriteFont нет среди полей приложения, как нет и самого текста:

Проект XNA: PinwheelText Файл: Game1.cs (фрагмент, демонстрирующий поля)

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

GraphicsDeviceManager graphics; SpriteBatch spriteBatch;

Vector2 screenCenter; RenderTarget2D renderTarget; Vector2 textureCenter; float rotationAngle;

}

Метод LoadContent – самая сложная часть приложения, но результатом его выполнения является лишь задание полей screenCenter, renderTarget и textureCenter (Центр текстуры). Переменные segoe14 и textSize, задаваемые в начале метода, как обычно, сохраняются как поля, но здесь они используются только локально:

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

protected override void LoadContent() {

// Создаем новый SpriteBatch, который может использоваться для отрисовки текстур.

spriteBatch = new SpriteBatch(GraphicsDevice);

// Получаем сведения об окне просмотра Viewport viewport = this.GraphicsDevice.Viewport;

screenCenter = new Vector2(viewport.Width / 2, viewport.Height / 2); // Загружаем шрифт и получаем размер текста

SpriteFont segoe14 = this.Content.Load<SpriteFont>("Segoe14");

string text = " Windows Phone 7";

Vector2 textSize = segoe14.MeasureString(text);

// Создаем RenderTarget2D renderTarget =

new RenderTarget2D(this.GraphicsDevice, 2 * (int)textSize.X,

2 * (int)textSize.X);

// Находим центр

textureCenter = new Vector2(renderTarget.Width / 2,

renderTarget.Height / 2);

Vector2 textOrigin = new Vector2(0, textSize.Y / 2);

// Задаем RenderTarget2D как GraphicsDevice this.GraphicsDevice.SetRenderTarget(renderTarget);

// Очищаем RenderTarget2D и формируем визуальное представление текста

this.GraphicsDevice.Clear(Color.Transparent);

spriteBatch.Begin();

for (float t = 0; t < 1; t += 1f / 32) {

float angle = t * MathHelper.TwoPi;

Color clr = Color.Lerp(Color.Cyan, Color.Yellow, t); spriteBatch.DrawString(segoe14, text, textureCenter, clr,

angle, textOrigin, 1, SpriteEffects.None, 0);

}

spriteBatch.End();

// Возвращаем GraphicsDevice в обычное состояние this.GraphicsDevice.SetRenderTarget(null);

}

Ширина и высота создаваемого RenderTarget2D в два раза превышают ширину и высоту текстовой строки. RenderTarget2D задается в GraphicsDevice посредством вызова SetRenderTarget и затем очищается до прозрачного цвета методом Clear. В этот момент последовательность вызовов объекта SpriteBatch обеспечивает формирование 32 визуальных представлений текстовой строки на RenderTarget2D. Вызов LoadContent завершается возвращением GraphicsDevice к обычному заднему буферу.

Метод Update вычисляет угол поворота результирующего растрового изображения, обеспечивая его разворот на 360° каждые 8 секунд:

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

protected override void Update(GameTime gameTime) {

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

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

rotationAngle =

(MathHelper.TwoPi * (float) gameTime.TotalGameTime.TotalSeconds / 8) %

MathHelper.TwoPi;

base.Update(gameTime);

}

Как и обещалось, перегруженный метод Draw может работать с этим RenderTarget2D как с обычным Texture2D с помощью единственного вызова Draw объекта SpriteBatch. Создается эффект синхронного вращения всех 32 текстовых строк:

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

protected override void Draw(GameTime gameTime) {

GraphicsDevice.Clear(Color.Navy); spriteBatch.Begin();

spriteBatch.Draw(renderTarget, screenCenter, null, Color.White,

rotationAngle, textureCenter, 1, SpriteEffects.None, 0); spriteBatch.End();

base.Draw(gameTime);

}

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

Texture2D horzBar = Content.Load<Texture2D>("HorzBar"); Texture2D vertBar = Content.Load<Texture2D>("VertBar");

можно заменить следующими:

RenderTarget2D horzBar = new RenderTarget2D(this.GraphicsDevice, 45, 5); this.GraphicsDevice.SetRenderTarget(horzBar); this.GraphicsDevice.Clear(Color.White); this.GraphicsDevice.SetRenderTarget(null);

RenderTarget2D vertBar = new RenderTarget2D(this.GraphicsDevice, 5, 75); this .GraphicsDevice.SetRenderTarget(vertBar); this.GraphicsDevice.Clear(Color.White); this.GraphicsDevice.SetRenderTarget(null);

Да, кода стало больше, но теперь нам не нужны два файла растровых изображений в качестве содержимого приложения. Также если когда-либо нам понадобится изменить размеры этих растровых изображений, в коде это не составит никакого труда.

Приложение DragAndDraw (Перетягиваем и рисуем), которое мы рассмотрим следующим, позволяет отрисовывать посредством сенсорного ввода множество одноцветных прямоугольников. Каждый раз, когда пользователь касается и проводит пальцем по экрану, отрисовывается новый прямоугольник случайного цвета. И при этом в приложении используется всего один объект RenderTarget2D, содержащий всего один закрашенный белым пиксел!

Этот единственный объект RenderTarget2D хранится как поле вместе с коллекцией объектов RectangleInfo, описывающих каждый отрисованный прямоугольник:

Проект XNA: DragAndDraw Файл: Game1.cs (фрагмент, демонстрирующий поля)

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

GraphicsDeviceManager graphics; SpriteBatch spriteBatch;

struct RectangleInfo {

public Vector2 point1; public Vector2 point2; public Color color;

}

List<RectangleInfo> rectangles = new List<RectangleInfo>(); Random rand = new Random(); RenderTarget2D tinyTexture; bool isDragging;

public Game1() {

graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content";

// Частота кадров по умолчанию для всех устройств Windows Phone – 30

кадров/с

TargetElapsedTime = TimeSpan.FromTicks(333333); // Активируем жесты перетягивания

TouchPanel.EnabledGestures = GestureType.FreeDrag |

GestureType.DragComplete;

}

}

Также, обратите внимание, в конце конструктора Game1 активируются два сенсорных жеста, FreeDrag и DragComplete. Эти жесты описывают касание экрана, проведение пальцем по экрану (в любом направлении) и снятие касания.

Метод LoadContent создает крошечный объект RenderTarget2D и закрашивает его белым цветом:

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

protected override void LoadContent() {

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

// Создаем белое растровое изображение 1×1

tinyTexture = new RenderTarget2D(this.GraphicsDevice, 1, 1); this.GraphicsDevice.SetRenderTarget(tinyTexture); this.GraphicsDevice.Clear(Color.White); this.GraphicsDevice.SetRenderTarget(null);

}

Метод Update обрабатывает жесты перетягивания. Как вы, возможно, помните из главы 3, статический класс TouchPanel распознает и простое касание, и сложные жесты. В данном приложении я использую поддержку жестов.

Если жесты активированы, необходимо задать TouchPanel.IsGestureAvailable (Жесты доступны) значение true, чтобы сделать их доступными. После этого можно вызывать

TouchPanel.ReadGesture, который возвратит объект типа GestureSample.

TouchPanel.IsGestureAvailable возвращает false, если в этом конкретном вызове Update больше нет доступных жестов.

Для данного приложения значением свойства GestureType объекта GestureSample будет один из двух членов перечисления: GestureType.FreeDrag или GestureType.DragComplete. Тип FreeDrag указывает на то, что палец коснулся экрана или перемещается по нему. DragComplete указывает на то, что касание завершено.

Для жеста FreeDrag действительны два других свойства класса GestureSample: Position – объект Vector2, обозначающий текущее положение пальца относительно экрана; Delta – это также объект Vector2, который показывает разницу между текущим положением пальца и положением пальца в предыдущем FreeDrag. (Я не использую свойство Delta в данном приложении.) Эти свойства недействительны для жеста DragComplete.

В приложении предусмотрено поле isDragging, которое помогает различать первое касание экрана и проведение пальцем по нему, поскольку оба этих жеста описываются с помощью FreeDrag:

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

protected override void Update(GameTime gameTime) {

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

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

while (TouchPanel.IsGestureAvailable) {

GestureSample gesture = TouchPanel.ReadGesture();

switch (gesture.GestureType) {

case GestureType.FreeDrag:

if (!isDragging) {

RectangleInfo rectInfo = new RectangleInfo(); rectInfo.point1 = gesture.Position; rectInfo.point2 = gesture.Position; rectInfo.color = new Color(rand.Next(25 6),

rand.Next(256), rand.Next(25 6));

rectangles.Add(rectInfo); isDragging = true;

}

else {

RectangleInfo rectInfo = rectangles[rectangles.Count – 1]; rectInfo.point2 = gesture.Position; rectangles[rectangles.Count – 1] = rectInfo;

}

break;

case GestureType.DragComplete: if (isDragging)

isDragging = false; break;

}

}

base.Update(gameTime);

Если isDragging имеет значение false, значит, палец впервые коснулся экрана. Приложение создает новый объект RectangleInfo и добавляет его в коллекцию. В этот момент значениями полей point1 и point2 объекта RectangleInfo задаются координаты точки касания экрана пальцем, и полю color – случайное значение Color.

С последующими жестами FreeDrag полю point2 объекта RectangleInfo, добавленного в коллекцию самым последним на текущий момент, задаются координаты точки, соответствующей текущему положению пальца на экране. С формированием события DragComplete больше ничего не требуется делать, и полю isDragging присваивается значение false.

В перегруженном методе Draw (представленном ниже) приложение вызывает метод Draw объекта SpriteBatch по одному разу для каждого объекта RectangleInfo в коллекции. При этом каждый раз используется разновидность Draw, которая обеспечивает масштабирование Texture2D до размера целевого Rectangle:

Draw(Texture2D texture, Rectangle destination, Color color)

Первым аргументом всегда является белый RenderTarget2D размером 1×1, называемый tinyTexture (Крошечная текстура); и последний аргумент – это случайный цвет, хранящийся в объекте RectangleInfo.

Тем не менее, аргумент Rectangle метода Draw требует некоторой обработки. Каждый объект RectangleInfo включает две точки, point1 и point2, которые представляют противоположные углы прямоугольника, отрисовываемого пользователем. Но в зависимости от того, как палец перемещается по экрану, point1 может быть верхним правым углом, и point2 – нижним левым углом прямоугольника, либо point1 может быть нижним правым углом, и point2 – верхним левым углом прямоугольника, либо два других возможных варианта.

Передаваемый в Draw объект Rectangle должен иметь точку, обозначающую верхний левый угол прямоугольника, а также неотрицательные значения ширины и высоты. (На самом деле, Rectangle может также принимать точку, соответствующую нижнему правому углу, и отрицательные значения ширины и высоты, но этот небольшой факт не упрощает логики.) Для этой цели вызываются методы Math.Min и Math.Abs:

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

protected override void Draw(GameTime gameTime) {

GraphicsDevice.Clear(Color.Navy); spriteBatch.Begin();

foreach (RectangleInfo rectInfo in rectangles) {

Rectangle rect =

new Rectangle((int)Math.Min(rectInfo.point1.X, rectInfo.point2.X), (int)Math.Min(rectInfo.point1.Y, rectInfo.point2.Y), (int)Math.Abs(rectInfo.point2.X – rectInfo.point1.X), (int)Math.Abs(rectInfo.point2.Y – rectInfo.point1.Y));

spriteBatch.Draw(tinyTexture, rect, rectInfo.color);

}

spriteBatch.End(); base.Draw(gameTime);

Вот как все это выглядит в действии:

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

По теме:

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