Компьютерные игры наполнены анимированными персонажами. XNA содержит встроенные средства, поддерживающие базовые операции по анимации трехмерных персонажей. В частности, это средства для работы с так называемым скелетом персонажа. Скелет состоит из взаимосвязанных костей, управляя которыми можно анимировать модель. Создание анимированных моделей – это отдельная, достаточно трудоемкая задача, которая требует серьезных познаний в области 3D-моделирования. Для подготовки анимированных трехмерных моделей вы можете использовать практически любой 3D-редактор.
XNA содержит лишь базовые средства для управления скелетами объектов. Так, мы можем получить коллекцию костей объекта и проводить с ними какие-либо преобразования, вызывающие движение модели. Однако, такой подход ограничен – с его использованием весьма сложно создать реалистичное движение для сложных моделей. На основе базовых средств XNA разработаны библиотеки кода, которые содержат компоненты, позволяющие анимировать персонажи на более высоком уровне. В частности, одну из таких библиотек – XNAAnimation – можно найти на сайте http://www.codeplex.com/xnanimation.
Здесь мы рассмотрим базовый подход к анимации модели. Он заключается в следующем. Можно менять параметры отдельных костей модели и выводить ее на экран с учетом этих изменений. Мы использовали бесплатную модель .
Создадим новый проект P18_1. Загрузим в него модель Ballpen.fbx. На рис. 24.1. представлено окно Solution Explorer нашего нового проекта.
(рис 24.1) Окно Project ExplorerКод проекта сосредоточен в классе Game1.cs, его код вы можете найти в листинге 24.1.
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
namespace P18_1
{
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
//Матрицы
Matrix viewMatrix;
Matrix projMatrix;
//Модель
Model pen;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
protected override void Initialize()
{
base.Initialize();
}
protected override void LoadContent()
{
//Загрузка модели
pen = Content.Load<Model>("ballpen");
//Соотношение сторон
float aspectRatio = (float)graphics.GraphicsDevice.Viewport.Width /
(float)graphics.GraphicsDevice.Viewport.Height;
//Устанавливаем камеру
viewMatrix = Matrix.CreateLookAt(new Vector3(0, 30, 80), new Vector3(-32, 0, 0), new Vector3(0, 1, 0));
//Устанавливаем проекционную матрицу
projMatrix = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(45.0f), aspectRatio, 1.0f, 1000.0f);
}
protected override void UnloadContent()
{
}
protected override void Update(GameTime gameTime)
{
//Перемещение частей ручки
PenMove();
base.Update(gameTime);
}
//Процедура для перемещения частей ручки
void PenMove()
{
//Получить состояние клавиатуры
KeyboardState keyboardState = Keyboard.GetState();
//Поворот зажима - он представлен родительской костью сети №3
//Умножим его матрицу трансформации на матрицу поворота по оси X
pen.Meshes[3].ParentBone.Transform = pen.Meshes[3].ParentBone.Transform * Matrix.CreateRotationX
(MathHelper.ToRadians(-1.0f));
//Кнопка ручки перемещается по клавиатурным командам
//При нажатии кнопки "Влево"
//Она выходит из ручки
if (keyboardState.IsKeyDown(Keys.Left))
{
//Умножим матрицу трансформации родительской костью сети №1
//На матрицу трансляции, перемещающей ее на 2 позиции влево по оси Х
pen.Meshes[1].ParentBone.Transform = pen.Meshes[1].ParentBone.Transform * Matrix.CreateTranslation
(new Vector3(-2, 0, 0));
}
//При нажатии кнопки "Вправо"
//Кнопка входит в ручку
if (keyboardState.IsKeyDown(Keys.Right))
{
//Умножим матрицу трансформации родительской кости сети №1
//На матрицу трансляции, перемещающую ее на 2 позиции по оси X вправо
pen.Meshes[1].ParentBone.Transform = pen.Meshes[1].ParentBone.Transform * Matrix.CreateTranslation
(new Vector3(2, 0, 0));
}
}
protected override void Draw(GameTime gameTime)
{
graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
//Новый массив матриц размером, соответствующим количеству
//костей в скелете модели
Matrix[] absoluteTransformations = new Matrix[pen.Bones.Count];
//Скопировать матрицы трансформации костей в массив матриц
pen.CopyAbsoluteBoneTransformsTo(absoluteTransformations);
foreach (ModelMesh mesh in pen.Meshes)
{
foreach (BasicEffect effect in mesh.Effects)
{
effect.LightingEnabled = true;
effect.EnableDefaultLighting();
effect.Projection = projMatrix;
effect.View = viewMatrix;
//Установим новую мировую матрицу для родительской кости текущей сети
//это приводит к перемещению кнопки и к перемещению зажима
//Так же здесь мы уменьшаем модель, применяя коэффициент масштабирования 0,13
effect.World = absoluteTransformations[mesh.ParentBone.Index] * Matrix.CreateScale(0.13f);
}
//Выводим подготовленную сеть
mesh.Draw();
}
base.Draw(gameTime);
}
}
}
На рис. 24.2. вы можете видеть игровой экран проекта P18_1.
(рис 24.2) Игровой экран проекта P18_1Простые графические эффекты в XNA можно реализовать при выводе изображений стандартными средствами. Но стандартные эффекты не способны удовлетворить потребности разработчиков. В одной из предыдущих лекций мы уже упоминали о том, что XNA поддерживает использование шейдерных программ, которые позволяют управлять графическими эффектами.
Шейдерные программы или просто шейдеры представлены в виде файлов с расширением FX. Эти файлы можно создавать как с помощью специального ПО для разработки и отладки шейдеров, так и вручную, в редакторе кода Visual Studio. Для описания шейдерных программ существует специальный язык – HLSL – High Level
Шейдерные программы исполняются графическим процессором видеокарты, с их помощью можно создать множество графических эффектов, которыми наполнены современные игры.
Шейдеры принято делить на вершинные ( и пиксельные (.
Вершинные работают с вершинами объектов. С их помощью можно осуществить такие операции, как деформацию объектов, анимацию, перемещение и т.д. После того, как вершинный шейдер завершит работу по модификации модели, за дело принимается пиксельный шейдер.
Пиксельные шейдеры применяются к отдельным пикселям изображения. В частности, они отвечают за цвет пикселей, позволяют применять к изображению различные эффекты.
Для того, чтобы создать новый FX-файл в XNA-проект, достаточно выполнить щелчок левой кнопкой мыши по папке Content, выбрать пункт Add $$\Rightarrow$$ New Item и в появившемся окне выбрать в качестве типа добавляемого файла Effect File. После того, как файл эффектов будет добавлен в проект, он будет содержать некоторые стандартные части, которые представляют собой объявление переменных, необходимых для работы шейдера, объявление точки входа в шейдер и, собственно, шаблоны двух шейдеров – вершинного и пиксельного.
Для того, чтобы разрабатывать шейдеры самостоятельно с помощью XNA-редактора FX-файлов, вам нужно ознакомиться с языком HLSL. Вы можете сделать это, воспользовавшись справочной службой MSDN, в частности, этим разделом: http://msdn2.microsoft.com/en-us/library/bb509561.aspx.
На практике лучше всего пользоваться специализированным ПО для разработки и отладки шейдеров.
Рассмотрим пример работы с шейдерными эффектами – он создан с использованием примера применения шейдеров, приведенного в документации к XNA.
Создадим новый проект P18_2. Нарис. 24.3. вы можете видеть его окно Solution Explorer.
(рис 24.3) Окно Solution Explorer для проекта P18_2Файл tex.png используется в качестве текстуры для наложения на модель, созданную программными средствами. Код программы реализован в классе Game1. Рассмотрим его (листинг. 24.2.).
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
namespace P18_2
{
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
//Матрицы
Matrix WorldMAtrix, ViewMatrix, ProjectionMatrix;
//Объект для применения эффектов
Effect effect;
//Вершины куба
VertexDeclaration cubeVertexDeclaration;
//Координаты для наложения текстур
VertexPositionTexture[] cubeVertices;
//Вершинный буфер
VertexBuffer vertexBuffer;
//Индексный буфер
IndexBuffer indexBuffer;
//Массив индексов
short[] cubeIndices;
//Номер эффекта в шейдере
int CurPass;
//Множитель для текстуры
float Multiplier;
//Текстура
Texture2D texture;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
protected override void Initialize()
{
// TODO: Add your initialization logic here
base.Initialize();
}
protected override void LoadContent()
{
//Мировая матрица
WorldMAtrix = Matrix.Identity;
//Матрица вида
ViewMatrix = Matrix.CreateLookAt(new Vector3(0, 0, 5), Vector3.Zero,
Vector3.Up);
//Матрица проекции
ProjectionMatrix = Matrix.CreatePerspectiveFieldOfView(
MathHelper .ToRadians (45.0f),
(float)graphics.GraphicsDevice.Viewport.Width /
(float)graphics.GraphicsDevice.Viewport.Height,
1.0f, 10.0f);
//Текущий эффект имеет номер 0
CurPass = 0;
//Текущий множитель - 1
Multiplier = 1;
//Загрузим текстуру
texture = Content.Load<Texture2D>("tex");
//Загрузим шейдер shader в переменную типа Effect
effect = Content.Load<Effect>("shader");
//Передадим в переменную шейдера UserTexture текстуру
effect.Parameters["UserTexture"].SetValue(texture);
//Установим переменную шейдера Multiplier в значение
//соответствующей игровой переменной
effect.Parameters["Multiplier"].SetValue(Multiplier);
//Создадим куб
InitializeModel();
}
protected override void UnloadContent()
{
// TODO: Unload any non ContentManager content here
}
/// <summary>
/// Создаем 3D-модель
/// </summary>
public void InitializeModel()
{
// Переменная типа VertexDeclaration
cubeVertexDeclaration = new VertexDeclaration(
graphics.GraphicsDevice, VertexPositionTexture.VertexElements);
//Установка параметров точек, которые будут использованы для рисования фигуры
Vector3 topLeftFront = new Vector3(-1.0f, 1.0f, 1.0f);
Vector3 bottomLeftFront = new Vector3(-1.0f, -1.0f, 1.0f);
Vector3 topRightFront = new Vector3(1.0f, 1.0f, 1.0f);
Vector3 bottomRightFront = new Vector3(1.0f, -1.0f, 1.0f);
Vector3 topLeftBack = new Vector3(-1.0f, 1.0f, -1.0f);
Vector3 topRightBack = new Vector3(1.0f, 1.0f, -1.0f);
Vector3 bottomLeftBack = new Vector3(-1.0f, -1.0f, -1.0f);
Vector3 bottomRightBack = new Vector3(1.0f, -1.0f, -1.0f);
// Координаты текстуры
Vector2 textureTopLeft = new Vector2(0.0f, 0.0f);
Vector2 textureTopRight = new Vector2(1.0f, 0.0f);
Vector2 textureBottomLeft = new Vector2(0.0f, 1.0f);
Vector2 textureBottomRight = new Vector2(1.0f, 1.0f);
// Массив для хранения списка вершин
// Он используется для передачи данных в вершинный буфер
cubeVertices = new VertexPositionTexture[36];
// Передняя часть фигуры
cubeVertices[0] =
new VertexPositionTexture(
topLeftFront, textureTopLeft); // 0
cubeVertices[1] =
new VertexPositionTexture(
bottomLeftFront, textureBottomLeft); // 1
cubeVertices[2] =
new VertexPositionTexture(
topRightFront, textureTopRight); // 2
cubeVertices[3] =
new VertexPositionTexture(
bottomRightFront, textureBottomRight); // 3
// Задняя часть фигуры
cubeVertices[4] =
new VertexPositionTexture(
topLeftBack, textureTopRight); // 4
cubeVertices[5] =
new VertexPositionTexture(
topRightBack, textureTopLeft); // 5
cubeVertices[6] =
new VertexPositionTexture(
bottomLeftBack, textureBottomRight); //6
cubeVertices[7] =
new VertexPositionTexture(
bottomRightBack, textureBottomLeft); // 7
// Верхняя часть фигуры
cubeVertices[8] =
new VertexPositionTexture(
topLeftFront, textureBottomLeft); // 8
cubeVertices[9] =
new VertexPositionTexture(
topRightBack, textureTopRight); // 9
cubeVertices[10] =
new VertexPositionTexture(
topLeftBack, textureTopLeft); // 10
cubeVertices[11] =
new VertexPositionTexture(
topRightFront, textureBottomRight); // 11
// Нижняя часть фигуры
cubeVertices[12] =
new VertexPositionTexture(
bottomLeftFront, textureTopLeft); // 12
cubeVertices[13] =
new VertexPositionTexture(
bottomLeftBack, textureBottomLeft); // 13
cubeVertices[14] =
new VertexPositionTexture(
bottomRightBack, textureBottomRight); // 14
cubeVertices[15] =
new VertexPositionTexture(
bottomRightFront, textureTopRight); // 15
// Левая часть фигуры
cubeVertices[16] =
new VertexPositionTexture(
topLeftFront, textureTopRight); // 16
cubeVertices[17] =
new VertexPositionTexture(
bottomLeftFront, textureBottomRight); // 17
cubeVertices[18] =
new VertexPositionTexture(
topRightFront, textureTopLeft); // 18
cubeVertices[19] =
new VertexPositionTexture(
bottomRightFront, textureBottomLeft); // 19
//Создаем вершинный буфер для хранения информации о вершинах
vertexBuffer = new VertexBuffer(graphics.GraphicsDevice,
VertexPositionTexture.SizeInBytes * cubeVertices.Length,
BufferUsage.None
);
//Добавляем данные в вершинный буфер
vertexBuffer.SetData<VertexPositionTexture>(cubeVertices);
// С помощью этого массива определяем, к каким частям фигуры
//Относятся те или иные компоненты массива cubeVertices
cubeIndices = new short[] {
0, 1, 2, // Передняя плоскость
1, 3, 2,
4, 5, 6, // Задняя плоскость
6, 5, 7,
8, 9, 10, // Верхняя плоскость
8, 11, 9,
12, 13, 14, // Нижняя плоскость
12, 14, 15,
16, 13, 17, // Левая плоскость
10, 13, 16,
18, 19, 14, // Правая плоскость
9, 18, 14 };
//Индексный буфер
indexBuffer = new IndexBuffer(graphics.GraphicsDevice,
sizeof(short) * cubeIndices.Length,
BufferUsage.None,
IndexElementSize.SixteenBits
);
//Добавляем данные в индексный буфер
indexBuffer.SetData<short>(cubeIndices);
}
protected override void Update(GameTime gameTime)
{
//Получим состояние клавиатуры
KeyboardState Key = Keyboard.GetState();
//Если нажата клавиша 1 - установить
//номер прохода эффекта в 0
if (Key.IsKeyDown (Keys.D1))
{
CurPass = 0;
}
//Если нажата клавиша 2 -
//установить номер прохода в 1
if (Key.IsKeyDown(Keys.D2))
{
CurPass = 1;
}
//Если нажата клавиша 3
//установить номер прохода в 2
if (Key.IsKeyDown(Keys.D3))
{
CurPass = 2;
}
//По нажатию клавиш QWERT
//Устанавливать различные множители
//они влияют на размеры текстуры, размещенных
//на гранях куба
//Например, если Multiplier=1 - лишь один экземпляр
//изображения текстуры располагается на грани
//Если Multiplier=2 - на грани размещается уже
//4 текстуры и т.д.
if (Key.IsKeyDown(Keys.Q))
{
Multiplier = 10.0f;
}
if (Key.IsKeyDown(Keys.W))
{
Multiplier = 5.0f;
}
if (Key.IsKeyDown(Keys.E))
{
Multiplier = 2.0f;
}
if (Key.IsKeyDown(Keys.R))
{
Multiplier = 1.0f;
}
if (Key.IsKeyDown(Keys.T))
{
Multiplier = 0.5f;
}
//Модифицируем мировую матрицу таким образом, чтобы модель
//поворачивалась в пространстве с небольшой скоростью
WorldMAtrix = WorldMAtrix * Matrix.CreateRotationX(0.011f) *
Matrix.CreateRotationY(0.012f)*
Matrix.CreateRotationZ (0.013f);
//Установить матрицу, которая является результатом умножения мировой, видовой и
//проекционной матриц в эффекте
effect.Parameters["WorldViewProj"].SetValue(WorldMAtrix *ViewMatrix *ProjectionMatrix);
//Установить переменную Multiplier в эффекте
effect.Parameters["Multiplier"].SetValue(Multiplier);
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
//Очистить экран
graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
//Выключить режим удаления задних частей треугольников
graphics.GraphicsDevice.RenderState.CullMode =
CullMode.None;
//Установить матрицу вершин
graphics.GraphicsDevice.VertexDeclaration = cubeVertexDeclaration;
//Установить матрицу индексов
graphics.GraphicsDevice.Indices = indexBuffer;
//Установить вершинный буфер
graphics.GraphicsDevice.Vertices[0].SetSource(
vertexBuffer,
0,
VertexPositionTexture.SizeInBytes);
//Начало работы эффекта, используемого для вывода изображения
effect.Begin();
//Использовать проход шейдера, заданный переменной CurPass
effect.CurrentTechnique.Passes[CurPass].Begin();
//Вывести изображение как набор индексированных
//примитивов, используя настройки буферов, сделанные ранее
graphics.GraphicsDevice.DrawIndexedPrimitives(
PrimitiveType.TriangleList,
0,
0,
cubeVertices.Length,
0,
12);
//Завершить текущий проход шейдера
effect.CurrentTechnique.Passes[CurPass].End();
//Завершить вывод изображений
effect.End();
base.Draw(gameTime);
}
}
}
Теперь рассмотрим код шейдера – в листинге 24.3. вы можете найти код файла .
//Переменные, устанавливаемые при настройке шейдера
//Матрица - результат перемножений мировой, видовой, проекционной матриц
uniform extern float4x4 WorldViewProj : WORLDVIEWPROJECTION;
//Текстура
uniform extern texture UserTexture;
//Множитель
uniform extern float Multiplier;
struct VS_OUTPUT
{
float4 position : POSITION;
float4 textureCoordinate : TEXCOORD0;
};
sampler textureSampler = sampler_state
{
Texture = <UserTexture>;
mipfilter = LINEAR;
};
//Вывод вершин текстуры
//Вершинный шейдер, который используется
//во всех вариантах эффекта
VS_OUTPUT Transform(
float4 Position : POSITION,
float4 TextureCoordinate : TEXCOORD0 )
{
VS_OUTPUT Out = (VS_OUTPUT)0;
Out.position = mul(Position, WorldViewProj);
Out.textureCoordinate = TextureCoordinate*Multiplier;
return Out;
}
//Вывод текстуры без изменений для P0, пиксельный шейдер
//Возврат того же цвета, который был передан
//шейдеру
float4 ApplyTexture(VS_OUTPUT vsout) : COLOR
{
return tex2D(textureSampler, vsout.textureCoordinate).rgba;
}
//Вывод размытой текстуры для P1, пиксельный шейдер
//Цвет устанавливается исходя из цвета
//соседних пикселей
//Полученная цветовая информация делится на 5
//для сохранения исходной яркости
float4 ApplyTextureBlur(VS_OUTPUT vsout) : COLOR
{
float4 Col;
Col = tex2D(textureSampler, vsout.textureCoordinate);
Col += tex2D(textureSampler, vsout.textureCoordinate + (0.01));
Col += tex2D(textureSampler, vsout.textureCoordinate + (0.02));
Col += tex2D(textureSampler, vsout.textureCoordinate + (0.03));
Col += tex2D(textureSampler, vsout.textureCoordinate + (0.04));
Col = Col / 5;
//Возвращаем найденный цвет пикселя
return Col;
}
//Вывод текстуры в оттенках серого для P2 - пиксельный шейдер
//Интенсивность света всех каналов изображения складывается и
//делится на количество каналов
//Альфа-канал устанавливается равным 1
float4 ApplyTextureGray(VS_OUTPUT vsout) : COLOR
{
float4 Col;
Col = tex2D(textureSampler, vsout.textureCoordinate);
Col.a=1.0f;
Col.rgb=(Col.r+Col.g+Col.b)/3;
return Col;
}
//Техника шейдера
technique TransformAndTexture
{
//Проход P0, имеет индекс 0
pass P0
{
//Вершинный шейдер для данного прохода
vertexShader = compile vs_2_0 Transform();
//Пиксельный шейдер для данного прохода
pixelShader = compile ps_2_0 ApplyTexture();
}
//Проход P1, имеет индекс 1
pass P1
{
vertexShader = compile vs_2_0 Transform();
pixelShader = compile ps_2_0 ApplyTextureBlur();
}
//Проход P2, имеет индекс 2
pass P2
{
vertexShader = compile vs_2_0 Transform();
pixelShader = compile ps_2_0 ApplyTextureGray();
}
}
На рис. 24.4., 24.5., 24.6. вы можете видеть игровое окно проекта в различных режимах применения шейдера.

(рис 24.5) Наложение текстуры без изменения цвета, с множителем 2(рис 24.4) Наложение текстуры с размытием, множитель 1
(рис 24.6) Текстура в оттенках серого, множитель 1Данное семинарское занятие посвящено защите итоговых проектов учащихся.
Для проведения семинара необходимо презентационное оборудование.
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.