В данном разделе я хотел было рассказать по порядку создание классов для нашего проекта, но сделав описание 4-5 классов, понял, что это будет слишком большой объем статьи. Вследствие этого я буду приводить код с комментариями в примерном порядке, в котором все должно быть спроектировано, но реализация классов будет полностью, а не частями.
Я постарался писать код как можно проще и там где надо будут (я надеюсь) исчерпывающие комментарии. Но начнем мы со структуры проекта.
Прежде всего, обратите внимание на ссылки. В них присутствуют ссылки на библиотеки Microsoft.Xna.Framework и на библиотеки Xen.
Проект контента в данном проекте содержит два Xml файла, level.xml и level.materials.xml. Оба этих файла мы получили при выполнении экспорта сцены из Blender. Оба файла не компилируются, а просто копируются в выходную папку в исходном виде. Читать оба файла бы будем с использованием класса XmlDocument. В папке Textures расположен набор текстур, которые могут быть использованы в качестве материалов геометрии сцены. Я не стал отображать все его содержимое для экономии места.
Обратите внимание на папку Effects, которая находится вне проекта контента и содержит файл SkyBox.fx – эффект, используемый для наложения кубической карты небесного куба на куб. Эта часть проекта (в том числе описание XenSkyBox.cs будет описана в конце статьи в качестве бонуса). Теперь по порядку хотел бы описать содержание файлов исходных кодов проекта (описывать файлы буду не в том порядке, в каком они расположены в проекте):
Game1.cs – основной класс игры. С него выполняется запуск игры.
BezTriple.cs – класс, представляющий собой узел кривой анимации. Содержит информацию о положении на временной шкале и вектора манипуляторов, используемые для интерполяции с использованием кривых Безье (класс с аналогичным названием присутствует и в документации к Blender Python API).
IpoCurve.cs – реализация кривой анимации на основе кубических кривых Безье.
IpoType.cs – перечисление, используемое для получения кривой анимации из коллекции по имени (работает быстрее чем по строковому имени).*
Ipo.cs – коллекция кривых анимации для одного объекта сцены.
TextureLoader.cs – реализация загрузчика текстур из всех форматов поддерживаемых XNA (в том числе и xnb).
XenReadHelper.cs – методы, облегчающие загрузку данных из xml-файла.
XenMaterial.cs – класс, представляющий материал сцены. Так же содержит статичный метод загрузки файла материалов.
XenSceneNode.cs – класс, инкапсулирующий в себе реализацию узла сцены.
XenCameraNode.cs, XenDirectionalLight.cs, XenPointLight.cs – наследники класса XenSceneNode, расширенные дополнительными свойствами и методами, присущими камере, и источникам света соответственно.
XenVertexPositionTangentSpaceTexture.cs – структура, предназначенная для формирования вершинных буферов геометрии сцены.
XenMesh.cs – класс геометрии, содержащий все необходимое для визуализации единицы геометрии (модели).
XenMeshPart.cs – класс, содержащий информацию о части модели (материал, и положение в вершинном буфере).
XenScene.cs и XenSceme.Loader.cs – две части одного класса XenScene. Данный класс содержит информацию о графе сцены и реализует логику его обновления.
Основной класс игры Game1 является наследником класса Xen.Application, который в свою очередь является аналогом класса Game в Xna. Реализация данного класса представлена далее:
// Пространства имен Net Framework usingSystem; usingSystem.Collections.Generic; usingSystem.Text; // Пространства имен Xen usingXen; usingXen.Camera; usingXen.Graphics; usingXen.Ex.Camera; usingXen.Ex.Graphics; usingXen.Ex.Graphics2D; usingXen.Graphics.State; usingXen.Ex.Graphics.Content; // Пространства имен Xna usingMicrosoft.Xna.Framework; usingMicrosoft.Xna.Framework.Graphics; usingMicrosoft.Xna.Framework.Content; namespace XenTutorial { // создаем наследника класса Xen.Application // это альтернатива класса Game в XNA publicclass Game1 : Application { // Главная точка входа в приложение. staticvoid Main(string[] args) { // Создаем экземпляр класса игры и запускаем игру на выполнение using(Game1 game =new Game1()) { game.Run(); } } #region Статичная ссылка на класс игры privatestatic Game1 singletion; // Предоставляет доступ к экземпляту класса игры, // запущенной в данный момент из любого места программы. publicstatic Game1 Singletion { get { if(Game1.singletion==null) { thrownew Exception("Игра еще не запущена!"); } return Game1.singletion; } private set { Game1.singletion= value;} } #endregion // Цель визуализации - экран, по этому создаем экземпляр класса DrawTargetScreen private DrawTargetScreen drawToScreen; // Переменная, в которую будет загружена сцена private XenScene scene; // Камера, которой мы будем пользоваться в нашем примере // в данном классе реализовано управление камерой от первого лица FirstPersonControlledCamera3D camera; // Переменные определяющие скорость анимации объектов сцены и текущий кадр анимации publicfloat IpoAnimFPS = 25f; publicfloat IpoAnimFrame = 0f; // Конструктор класса игры public Game1() { // Проверяем, не создана ли игра во второй раз в данном приложении if(Game1.singletion!=null) { thrownew Exception("Нельзя запускать несколько экземпляров игры в одном приложении!"); } else { // Инициализируем статичную ссылку на класс нашей игры Game1.singletion= this; } } // Переопределенный метод настройки грфического устройства protectedoverridevoid SetupGraphicsDeviceManager(GraphicsDeviceManager graphics, ref RenderTargetUsage presentation) { graphics.PreferredBackBufferWidth=1024; graphics.PreferredBackBufferHeight=768; base.SetupGraphicsDeviceManager(graphics, ref presentation); } // Инициализация игры protectedoverridevoid Initialise() { // Добавление пути поиска текстур (начиная с корневой папки игры) TextureLoader.Paths.Add( System.IO.Path.GetDirectoryName(this.GetType().Assembly.Location)); // Создаем и настраиваем камеру camera =new FirstPersonControlledCamera3D(this.UpdateManager, Vector3.Zero); camera.MovementSensitivity=new Vector2(0.05f, 0.05f); camera.Projection.FarClip=1000; camera.Projection.NearClip= 0.1f; camera.Projection.FieldOfView= MathHelper.PiOver4; // Создаем цель визуализации и инициализируем ее созданной камерой и текущим классом drawToScreen =new DrawTargetScreen(this, camera); // задаем цвет очистки экрана drawToScreen.ClearBuffer.ClearColour= Color.CornflowerBlue; } // Переопределенный метод загрузки контента игры protectedoverridevoid LoadContent(DrawState state, ContentManager manager) { base.LoadContent(state, manager); // Загружаем небесный куб XenSkyBox skyBox =new XenSkyBox("Sunny", manager); // загружаем материалы сцены XenMaterial.LoadMaterials("Content/Materials/Level.Materials.xml", manager); // загружаем сцену scene = XenScene.Load("Content/Levels/Level.xml"); // вызываем метод создания списка визуализируемой геометрии // его следует вызывать тогда, когда вы изменяете состав объектов сцены scene.RefreshMeshList(); // добавление визуализируемых элементов цели визуализации drawToScreen.Add(skyBox); drawToScreen.Add(scene); } // переопределяем метод инициализации ввода пользователя protectedoverridevoid InitialisePlayerInput(Xen.Input.PlayerInputCollection playerInput) { // говорим, что для первого игрока мышь должна остоваться в центре экрана // это нужно для нормальной работы камеры от первого лица playerInput[PlayerIndex.One].InputMapper.CentreMouseToWindow= true; } // переопределенный метод обновления игры protectedoverridevoid Update(UpdateState state) { // рассчитываем номер кадра анимации IpoAnimFrame += state.DeltaTimeSeconds* IpoAnimFPS; // обновляем сцену scene.Update(state); // обработка выхода их игры if(state.PlayerInput[PlayerIndex.One].InputState.Buttons.Back.OnPressed) this.Shutdown(); } // переопределенный метод визуализации protectedoverridevoid Draw(DrawState state) { // выводим все визуализируемые элементы на экран drawToScreen.Draw(state); } } }
Следующим этапом является проектирование структур данных для загруцки экспортируемой сцены.
Начнем мы проектировать структуры данных, необходимые для загрузки экспортированной сцены с класса материала.
Xen в своем составе имеет альтернативу BasicEffect (эффекту, используемому по умолчанию в XNA для загружаемых моделей), который находится в пространстве имен Xen.Ex.Material. Называется он MaterialShader. Данный шэйдер (эффект) реализует в себе базовую модель освещения, и поддерживает такие возможности как карты нормалей, большое число направленных и точечных источников света и др., а самое главное он подходит для наших целей.
Наш класс материала должен обеспечивать хранение всей информации о материале, такие как диффузная карта, карта нормалей, цвет модели, информацию о бликовой составляющей, и другие параметры описывающие материал.
Также он должен обеспечивать хранение информации о источниках света для каждого экземпляра класса, что можно реализовать статическими членами и методами класса.
Кроме того, данный класс будет неплохим шранилищем всех материалов сцены, к которым можно получить доступ, как по имени, так и получить всю коллекцию материалов, а так же содержать метод загрузки материалов из XML-файла.
Пришло время исследовать код класса XenMateral. Не хочется разделять код классов на части, поэтому комментарии непосредственно в коде. Для экономии места более не буду приводить импортируемые пространства имен.
namespace XenTutorial { publicclass XenMaterial { // Публичные поля класса. Конечно их можно реализовать как свойства, // но в данном случае не вижу в этом смысла publicstring Name =""; public Color Color = Color.LightGray; public Color SpecularColor = Color.White; publicfloat SpecularPower = 80f; publicfloat SpecularIntensity = 1.0f; publicfloat Ambient = 0.2f; public Texture2D DiffuseMap; public Texture2D NormalMap; // Ключевое поле материала, содержащее ссылку на экземпляр класса MaterialShader private MaterialShader material; // Публичный конструктор, выполняющий инициализацию материала public XenMaterial() { material =new MaterialShader(); } // Метод, устанавливающий текущий шейдер активным // После вызова данного метода все операции вывода на экран будут использовать // данный материал publicvoid Bind(IShaderSystem state) { // Установка параметров материала, сохраненных в данном экземпляре material.Alpha= Color.A/ 255f; material.DiffuseColour= Color.ToVector3(); material.NormalMap= NormalMap; material.SpecularColour= SpecularColor.ToVector3()* SpecularIntensity; material.SpecularPower= SpecularPower; material.TextureMap= DiffuseMap; // Установка качества фильтрации текстур material.TextureMapSampler= TextureSamplerState.AnisotropicHighFiltering; material.NormalMapSampler= TextureSamplerState.AnisotropicLowFiltering; // Установка коллекции источников света из статического поля класса XenMaterial material.Lights= XenMaterial.Lights; if(material.Lights!=null) { // Установка общих параметров источникв света material.Lights.AmbientLightColour=new Vector3(Ambient); material.Lights.LightingEnabled= true; } // Установка материала как текущего material.Bind(state); } // Статические члены класса // Коллекция материалов privatestatic List<XenMaterial> materials =new List<XenMaterial>(); // Материал, возвращаемый тогда, когда в коллекции нет материала с указанным именем privatestatic XenMaterial noneMaterial; // Коллекция источников света privatestatic MaterialLightCollection lights =new MaterialLightCollection(); // Текущая коллекция источников света (публичное свойство) publicstatic MaterialLightCollection Lights { get {return XenMaterial.lights;} set { XenMaterial.lights= value;} } // Коллекция материалов (публичное свойство) publicstatic List<XenMaterial> Materials { get {return XenMaterial.materials;} } // Функция, возвращающая материал коллекции по имени publicstatic XenMaterial GetMaterialByName(string name) { // Понижаем регистр символов в названии материала для поиска name = name.ToLower(); // Перебираем коллекцию материалов в поисках материала с заданным именем foreach(XenMaterial mat in Materials) { if(mat.Name.ToLower()== name) { // Возвращаем найденный материал return mat; } } // Если еще не нинициализировано свойство материала по умолчанию, то создаем его if(noneMaterial ==null) { noneMaterial =new XenMaterial(); } // Возвращаем материал по умолчанию return noneMaterial; } // Статический метод для загрузки материалов из экспортированного файла publicstaticvoid LoadMaterials(string materialsFileName, ContentManager manager) { // Загружаем документ XML XmlDocument materialsDoc =new XmlDocument(); materialsDoc.Load(materialsFileName); // Перебираем все ноды внутри корневого foreach(XmlNode node in materialsDoc.DocumentElement) { // если имя нода "Material", то загружаем материал if(node.Name.ToLower()=="material") { // Создаем новый экземпляр материала XenMaterial mat =new XenMaterial(); mat.Name= XmlReadHelper.GetAttributeValue(node, "Name"); // Читаем ветку Color для получения информации о цвете материала string[] vals = XmlReadHelper.GetAttributeValues( XmlReadHelper.FindChildNode(node, "Color"), newstring[]{"R", "G", "B", "A", "Ambient"} ); mat.Color=new Color( XmlReadHelper.Val(vals[0]), XmlReadHelper.Val(vals[1]), XmlReadHelper.Val(vals[2]), XmlReadHelper.Val(vals[3])); mat.Ambient= XmlReadHelper.Val(vals[4]); // Цвет и сила бликовой составляющей материала vals = XmlReadHelper.GetAttributeValues( XmlReadHelper.FindChildNode(node, "Specular"), newstring[]{"R", "G", "B", "Power", "Intensity"} ); mat.SpecularColor=new Color( XmlReadHelper.Val(vals[0]), XmlReadHelper.Val(vals[1]), XmlReadHelper.Val(vals[2])); mat.SpecularPower= XmlReadHelper.Val(vals[3]); mat.SpecularIntensity= XmlReadHelper.Val(vals[4]); // Читаем список текстур материала (если они есть) List<string> textures =new List<string>(); foreach(XmlNode tex in XmlReadHelper.FindChildNode(node, "Textures")) { if(tex.Name.ToLower()=="texture") { textures.Add( System.IO.Path.GetFileNameWithoutExtension( XmlReadHelper.GetAttributeValue(tex, "Name")) ); } } if(textures.Count>0) { // Если текстуры есть, то загружаем первую в качестве диффузной карты mat.DiffuseMap= TextureLoader.GetTextureByName<Texture2D>(textures[0], manager); } materials.Add(mat); } } } } }
Следующий класс, необходимый для визуализации геометрии – XenMeshPart. Данный класс будет содержать информацию о части геометрии и ее материале. Реализация его достаточно проста.
namespace XenTutorial { publicclass XenMeshPart { // поля класса int startIndex; int trianglesCount; XenMaterial material = null; // Конструктор без параметров public XenMeshPart(){} // Конструктор с параметрами public XenMeshPart(int start, int primCount, XenMaterial mat) { startIndex = start; trianglesCount = primCount; material = mat; } // Свойство, обеспечивающее доступ к материалу public XenMaterial Material { get {return material;} set { material = value;} } // Начальный индекс в массиве вершин модели publicint StartIndex { get {return startIndex;} set { startIndex = value;} } // Количество треугольников, входящих в состав данной части модели publicint TrianglesCount { get {return trianglesCount;} set { trianglesCount = value;} } } }
Для визуализации геометрии, необходимы два буфера – вершинный и индексный. Первый в себе хранит информацию о вершинах геометрии, такую как позиция, нормаль, текстурные координаты и д.р. Второй буфер хранит информацию об индексах вершин, входящих в состав примитивов. Сделано такое разделение для оптимизации рендеринга. Во-первых, оба буфера создаются в памяти видеокарты, что дает возможность не передавать большое кол-во информации из оперативной памяти в видеопамять. Во-вторых, такая организация дает возможность снизить объемы данных, хранящихся в памяти, т.к. одна и та же вершина в геометрии может использоваться несколько раз. Следовательно, чтобы вывести геометрию необходимо в индексный буфер поместить информацию о положении вершины в вершинном буфере для каждого примитива.
Но я отвлекся, давайте займемся реализацией своей структуры вершины, которая в себе будет хранить информацию, необходимую для визуализации с использованием карт нормалей.
Здесь Xen нам очень поможет, т.к. для объявления вершинных буферов нет необходимости создавать экземпляр класса VertexDeclaration, т.к. Xen это сделает автоматически.
namespace XenTutorial { publicstruct XenVertexPositionTangentSpaceTexture { public Vector3 Position; public Vector3 Normal; public Vector3 Binormal; public Vector3 Tangent; public Vector2 TextureCoordinate; public XenVertexPositionTangentSpaceTexture( Vector3 position, Vector3 normal, Vector3 binormal, Vector3 tangent, Vector2 textureCoordinate) { this.Position= position; this.Normal= normal; this.Binormal= binormal; this.Tangent= tangent; this.TextureCoordinate= textureCoordinate; } } }
Думаю, в комментировании кода нет необходимости, но я бы хотел рассказать о некоторых моментах. Прежде всего, как Вы заметили, здесь нет информации об использовании каждого поля структуры. Xen данную информацию определяет автоматически. Я не исследовал код Xen, создающий буфер вершин, но думаю, это делается исходя из имен полей структуры. В некоторых случаях Xen, возможно, не сможет определить данную информацию. Поэтому существует возможность явно указать, как стоит использовать поля структуры. Делается это с помощью атрибута VertexElement. Я предпочитаю явно указывать использование полей структуры, поэтому в скаченном примере вы увидите следующий код:
namespace XenTutorial { publicstruct XenVertexPositionTangentSpaceTexture { [Xen.Graphics.VertexElement(VertexElementUsage.Position)] public Vector3 Position; [Xen.Graphics.VertexElement(VertexElementUsage.Normal)] public Vector3 Normal; [Xen.Graphics.VertexElement(VertexElementUsage.Binormal)] public Vector3 Binormal; [Xen.Graphics.VertexElement(VertexElementUsage.Tangent)] public Vector3 Tangent; [Xen.Graphics.VertexElement(VertexElementUsage.TextureCoordinate)] public Vector2 TextureCoordinate; public XenVertexPositionTangentSpaceTexture( Vector3 position, Vector3 normal, Vector3 binormal, Vector3 tangent, Vector2 textureCoordinate) { this.Position= position; this.Normal= normal; this.Binormal= binormal; this.Tangent= tangent; this.TextureCoordinate= textureCoordinate; } } }
Также это поможет избежать некоторых проблем, при использовании обфускации.
Следующим классом, реализацией которого мы займемся, будет XenMesh.
В общем случае данный класс будет альтернативой классу XNA ModelMesh, но данная реализация будет ориентирована на Xen (хотя реализация на XNA будет похожа, но вывод геометрии будет выполнен с использованием большего числа строк кода). Кроме того необходимо сохранить информацию о вершинах и индексный буфер на будущее, когда мы соберемся опрашивать сцену на пересечение с лучем (т.е. реализовать выбор объектов на экране мышкой и др. подобных операций). Для оптимизации так же необходимо создать ограничивающий объем, который позволит не выводить геометрию, если она не попадает в поле зрения камеры, а так же позволит не просчитывать пересечение луча с треугольниками геометрии, если луч не пересекается с ограничивающим объемом. В данной статье я буду использовать BoundingBox (ограничивающий параллелепипед) вместо ограничивающей сферы, т.к. он в большинстве случаев обеспечивает более точное определение пересечения.
Xen предоставляет универсальный интерфейс IDraw, в котором также должен быть реализован интерфейс ICullable. ICullable содержит определение метода CullTest, в котором необходимо выполнить проверку на отсечение. Данный метод получает объект, в котором реализован интерфейс ICuller, обеспечивающий проверку на отсечение. IDraw содержит определение метода Draw. В данный метод передается переменная типа DrawState, которая в себе содержит все необходимое, для выполнения визуализации.
Теперь давайте перейдем к коду. Я постараюсь прокомментировать только те участки кода, на которые стоит обратить внимание.
namespace XenTutorial { publicclass XenMesh : IDraw { // Мировая матрица, необходимая для вывода геометрии. Расчет данной матрицы // будет выполнен в другом месте, к ней мы еще вернемся private Matrix world; // Ссылки на вершинный и индексный буферы, выполненные в виде интерфейсов private IVertices vertices; private IIndices indices; // массив индексов privatereadonlyushort[] inds; // Массив координат вершин privatereadonly Vector3[] points; // Массив частей геометрии. Необходим для вывода частей с разными материалами private XenMeshPart[] parts; // Ограничивающий объем геометрии private BoundingBox bbox; // Переменная, необходимая для сохранения информации пользователя privateobject tag; // Публичный конструктор, принимающий массив вершин, массив индексов // и др. необходимую информацию public XenMesh(ref XenVertexPositionTangentSpaceTexture[] verts, refushort[] inds, ref Vector3[] points, ref XenMeshPart[] parts, ref BoundingBox bbox) { this.vertices=new Vertices<XenVertexPositionTangentSpaceTexture>(verts); this.indices=new Indices<ushort>(inds); this.points= points; this.inds= inds; this.bbox= bbox; this.parts= parts; } // Свойство, обеспечивающие доступ к ограничивающему объему public BoundingBox BoundingBox { get {return bbox;} private set { bbox = value;} } // Свойство, обеспечивающие доступ к информации о частях геометрии public XenMeshPart[] MeshParts { get {return parts;} private set { parts = value;} } // Мировая матрица геометрии public Matrix World { get {return world;} set { world = value;} } // Тэг, место хранения пользовательской информации publicobject Tag { get {return tag;} set { tag = value;} } #region Члены IDraw // Наконец метод Draw publicvoid Draw(DrawState state) { // Как я уже говорил, переменная типа DrawState содержит все необходимое // для визуализации геометрии, включая методы Push и Pop для сохранения и // влсстановления состояния устройства. // Следующей строкой осуществляется сохранение текущей мировой матрицы состояния и // установка матрицы геометрии state.PushWorldMatrix(ref world); // Перебор каждой части геометрии и визуализация с учетом ее материала foreach(XenMeshPart part in parts) { part.Material.Bind(state); vertices.Draw(state, indices, PrimitiveType.TriangleList, part.TrianglesCount, part.StartIndex, 0); } // Восстановление сохраненной мировой матрицы в состоянии state.PopWorldMatrix(); } #endregion #region Члены ICullable // Метод, кроверябщий неометрию на отсечение publicbool CullTest(ICuller culler) { // Переменная culler имеет достаточно методов для тестов на отсечение неометрии return culler.TestBox(bbox.Min, bbox.Max, ref world); } #endregion } }
Как Вы видите все достаточно просто!
Пришло время реализовать дерево сцены (граф сцены, английское название может быть Scene Graph, Scene Tree и др. синонимы).
Дерево сцены включает в себя иерархию объектов (Node что в переводе узел) и их трансформаций. Каждый объект может иметь две матрицы трансформаций: локальную и мировую. Первая представляет собой трансформации в системе координат объекта-родителя. Вторая – трансформации в системе координат мира. Далее под словом «матрица» я буду подразумевать трансформации.
Для того чтобы правильно отобразить объект, необходимо выполнить преобразование вершин объекта с учетом мировой матрицы объекта. Мировую матрицу объекта можно получить умножением мировой матрицы объекта родителя и локальной матрицы самого объекта. У объектов, у которых нет родителя, мировая матрица совпадает с локальной матрицей. Обычно в дереве сцены присутствует корневой элемент (Root), который имеет единичную матрицу трансформаций.
Каждый объект сцены имеет одинаковые общие характеристики (такие как трансформации). Мы с Вами узнали это, когда занимались экспортированием сцены. Следовательно, необходимо реализовать класс, который содержал бы в себе все общие характеристики объектов.
Объекты могут иметь привязанную к ним геометрию, а могут не иметь. Я считаю, что надо дать возможность привязать к любому объекту сцены какую-либо геометрию, как например, к камере можно привязать руку с пистолетом персонажа и т.д.
Первым классом, который будет реализован в этом разделе – XenSceneNode. Затем мы объединим граф сцены и логику, связанную с ним в отдельный класс XenScene, который и будет являться завершающим, необходимым для загрузки объектов сцены из экспортированного файла.
namespace XenTutorial { publicclass XenSceneNode { // Поле с именем объекта сцены, в Blender и др. пакетах трехмерной графики // каждый объект сцены обязан иметь уникальное имя privatestring name; // Матрицы трансформаций, мировая и локальная private Matrix worldMatrix; private Matrix localMatrix; // Составляющие локальной трансформации. Эти поля будут использоваться для построения // локальной матрицы трансформаций. private Vector3 translation; private Vector3 rotation; private Vector3 scale; // Флаг, показывающий необходимость пересчета локальной матрицы трансформаций // (своего рода некоторая оптимизация) privatebool localMatrixChanged; // Поля, указывающие на наличие родителя и потомков private XenSceneNode parent; private List<XenSceneNode> childs; // Список прикрепленных геометрических объектов к данному ноду private List<XenMesh> meshes; // Список параметров, привязанных к данному объекту private List<XenSceneNodeParameter> parameters; // Переменная, служащая местом хранения пользовательской информации privateobject tag; // Хранилище кривых анимации private Ipo ipo =new Ipo(); // Публичный конструктор, инициализирующий все поля класса. // Обязательным параметром является имя объекта. public XenSceneNode(string nodeName) { name = nodeName; translation = Vector3.Zero; rotation = Vector3.Zero; scale = Vector3.One; worldMatrix = Matrix.Identity; localMatrix = Matrix.Identity; localMatrixChanged = false; parent = null; childs =new List<XenSceneNode>(); meshes =new List<XenMesh>(); parameters =new List<XenSceneNodeParameter>(); } publicstring Name { get {returnthis.name;} set {this.name= value;} } public Matrix LocalMatrix { get {returnthis.localMatrix;} private set {this.localMatrix= value;} } public Matrix WorldMatrix { get {returnthis.worldMatrix;} // Воизбежание несоответствия локальных трансформаций установленным значениям // ограничим возможность установки свойства this.worldMatrix текущим классом private set {this.worldMatrix= value;} } // Далее идут три свойства локальных трансформаций, установка которых меняет // флаг, информирующий о том, что необходимо пересчитать локальную матрицу public Vector3 Scale { get {returnthis.scale;} set { if(!this.scale.Equals(value)) { this.localMatrixChanged= true; } this.scale= value; } } public Vector3 Rotation { get {returnthis.rotation;} set { if(!this.rotation.Equals(value)) { this.localMatrixChanged= true; } this.rotation= value; } } public Vector3 Translation { get {returnthis.translation;} set { if(!this.translation.Equals(value)) { this.localMatrixChanged= true; } this.translation= value; } } // Коллекция геометрических объектов, привязанных к данному узлу public List<XenMesh> Meshes { get {return meshes;} } // Коллекция дополнительных параметров, которые можно использовать для построения логики public List<XenSceneNodeParameter> Parameters { get {return parameters;} set { parameters = value;} } // Хранилище дополнительной информации в любом формате ;) publicobject Tag { get {return tag;} set { tag = value;} } // Хранилище кривых анимаций public Ipo Ipo { get {return ipo;} set { ipo = value;} } // Ссылка на родительский узел графа сцены public XenSceneNode Parent { get {returnthis.parent;} private set {this.parent= value;} } // Далее свойства и методы, связанные с изменением коллекции потомков (childs) public XenSceneNode[] Childs { get {returnthis.childs.ToArray();} } publicint CountChilds { get {returnthis.childs.Count;} } // Коллекция потомков (childs) закрыта для того, чтобы ссылка на родительский узел была верная // поэтому реализуем несколько методов работы с потомками publicvoid AddChild(XenSceneNode child) { this.childs.Add(child); child.Parent= this; } publicvoid RemoveChild(XenSceneNode child) { this.childs.Remove(child); child.Parent= null; } publicvoid ClearChilds() { foreach(XenSceneNode child inthis.childs) { child.parent= null; } this.childs.Clear(); } // Метод Update принимает переменную типа UpdateState, которая кроме // игрового времени содержит ряд дополнительной информации publicvirtualvoid Update(UpdateState state) { // получение текущего кадра анимации float frame = Game1.Singletion.IpoAnimFrame; // Если у данного объекта коллекция кривых анимации if(ipo !=null) { // сначала получаем текущие значения // т.к. для анимации перемещения могут присутствовать кривые // не для всех каналов, то проверяем из по отдельности Vector3 trans =this.Translation; if(Ipo.GetIpo(IpoType.LocX)!=null) { trans.X= Ipo.GetIpo(IpoType.LocX).GetValueByFrameNumber(frame); } if(Ipo.GetIpo(IpoType.LocY)!=null) { trans.Y= Ipo.GetIpo(IpoType.LocY).GetValueByFrameNumber(frame); } if(Ipo.GetIpo(IpoType.LocZ)!=null) { trans.Z= Ipo.GetIpo(IpoType.LocZ).GetValueByFrameNumber(frame); } // устанавливаем новое значение как текущее this.Translation= trans; // делаем тоже самое для анимации вращения и масштабирования Vector3 rot =this.Rotation; if(Ipo.GetIpo(IpoType.RotX)!=null) { rot.X= Ipo.GetIpo(IpoType.RotX).GetValueByFrameNumber(frame); } if(Ipo.GetIpo(IpoType.RotY)!=null) { rot.Y= Ipo.GetIpo(IpoType.RotY).GetValueByFrameNumber(frame); } if(Ipo.GetIpo(IpoType.RotZ)!=null) { rot.Z= Ipo.GetIpo(IpoType.RotZ).GetValueByFrameNumber(frame); } this.Rotation= rot; Vector3 scl =this.Scale; if(Ipo.GetIpo(IpoType.ScaleX)!=null) { scl.X= Ipo.GetIpo(IpoType.ScaleX).GetValueByFrameNumber(frame); } if(Ipo.GetIpo(IpoType.ScaleY)!=null) { scl.Y= Ipo.GetIpo(IpoType.ScaleY).GetValueByFrameNumber(frame); } if(Ipo.GetIpo(IpoType.ScaleZ)!=null) { scl.Z= Ipo.GetIpo(IpoType.ScaleZ).GetValueByFrameNumber(frame); } this.Scale= scl; } // Если локальная матрица изменилась, то выполняем пересчет // и сбрасываем флаг if(this.localMatrixChanged) { // Сначала масштаб, потом вращение и наконец перемещение this.localMatrix= Matrix.CreateScale(this.Scale)* Matrix.CreateFromYawPitchRoll( this.Rotation.Y, this.Rotation.X, this.Rotation.Z)* Matrix.CreateTranslation(this.Translation); this.localMatrixChanged= false; } // В зависимости от того, есть ли родитель у данного нода, // выполняем расчет мировой матрицы if(Parent !=null) { this.worldMatrix=this.parent.WorldMatrix*this.LocalMatrix; } else { // Как я и говорил, у корневых нодов мировая матрица равна локальной this.worldMatrix=this.LocalMatrix; } } } // Небольшая структура, представляющая собой информацию о дополнительном параметре узла сцены publicstruct XenSceneNodeParameter { publicstring Name; publicstring Value; public XenSceneNodeParameter(string name, string value) { Name = name; Value = value; } } }
Реализация данного класса не составила у нас большого труда, т.к. нет никаких сложных конструкций. Наследниками данного класса являются XenCameraNode, XenDirectionalLight и XenPointLight. Описание данных классов присутствует в исходном коде и Вам не составит труда их разобрать самостоятельно.
Класс XenScene так же не потребует от нас много усилий по реализации. Здесь мы посмотрим возможности Xen по сортировке геометрических объектов перед выводом. Сортировка необходима для обеспечения правильного вывода прозрачной геометрии.
Первая часть класса, без части загрузки сцены приведена далее:
namespace XenTutorial { publicpartialclass XenScene : IDraw { // объявление экземпляра класса, сортирующего геометрию по глубине // данный сортировщик основан на CullTest'е и может сортировать объекты раз в // определенное число выводов на экран. В нашем случае мы будем использовать этот // объект с параметрами по умолчанию private DepthDrawSorter objectsToDraw =new DepthDrawSorter(DepthSortMode.BackToFront); // коллекция источников света данной сцены специализированная для MaterialShader private MaterialLightCollection lights =new MaterialLightCollection(); // корень графа сцены private XenSceneNode rootSceneNode; // источники света данной сцены private List<XenPointLightNode> plights =new List<XenPointLightNode>(); private List<XenDirectionalLightNode> dlights =new List<XenDirectionalLightNode>(); // конструктор сцены получает корневой узел public XenScene(XenSceneNode root) { rootSceneNode = root; objectsToDraw.SortDelayFrameCount=10; } // метод обновления сцены publicvoid Update(UpdateState state) { // очищаем коллекции lights.RemoveAllLights(); plights.Clear(); dlights.Clear(); // вызываем рекурсивное обновление узлов сцены, начиная с корневого UpdateNode(rootSceneNode, state); } // метод, выполняющий обновление узла сцены protectedvoid UpdateNode(XenSceneNode node, UpdateState state) { // вызов метода обновления узла node.Update(state); // заполнение всех коллекций источников света if(node is XenDirectionalLightNode) { XenDirectionalLightNode dl =(node as XenDirectionalLightNode); dlights.Add(dl); lights.AddDirectionalLight(false, -dl.Direction, dl.Color); } elseif(node is XenPointLightNode) { XenPointLightNode pl =(node as XenPointLightNode); plights.Add(pl); lights.AddPointLight(true, pl.Translation, pl.Range, pl.Color); } Matrix world = node.WorldMatrix; // установка мировой матрицы для всей геометрии foreach(XenMesh mesh in node.Meshes) { mesh.World= world; } // выполнение обновления всех потомков foreach(XenSceneNode child in node.Childs) { UpdateNode(child, state); } } // обход дерева сцены и заполнение коллекции геометрии для вывода publicvoid RefreshMeshList() { objectsToDraw.Clear(); RefreshMeshNode(rootSceneNode); } // вункция рекурсивного объхода дерева сцены protectedvoid RefreshMeshNode(XenSceneNode node) { foreach(XenMesh mesh in node.Meshes) { objectsToDraw.Add(mesh); mesh.Tag= node; } foreach(XenSceneNode child in node.Childs) { RefreshMeshNode(child); } } // вывод геометрии сцены в цель визуализации publicvoid Draw(DrawState state) { // сохранение старой коллекции источников света, которые были установлены ранее MaterialLightCollection oldLights = XenMaterial.Lights; // установка текущей коллекции источников света XenMaterial.Lights= lights; // вывод сортированной геометрии objectsToDraw.Draw(state); // восстановление старой коллекции источников света XenMaterial.Lights= oldLights; } public XenSceneNode RootSceneNode { get {return rootSceneNode;} private set { rootSceneNode = value;} } #region Члены ICullable // всегда визуализируем сцену // те объекты, которые не попадают в кадр отсечет сам DepthDrawSorter publicbool CullTest(ICuller culler) { return true; } #endregion } }
Далее рассмотрим набор классов, которые являются частным воспроизведением классов анимации Blender для нашей задачи (на самом деле я не копался в исходниках Blender’а, я просто предположил, что кривые анимации в нем выполнены с использованием кривых Безье, и я угадал).
namespace XenTutorial { // Класс, хранящий информацию об узле кривой Безье publicclass BezTriple { Vector2 handle1, point, handle2; public BezTriple() { Handle1 = Vector2.Zero; Point = Vector2.Zero; Handle2 = Vector2.Zero; } public BezTriple(Vector2 h1, Vector2 p, Vector2 h2) { Handle1 = h1; Point = p; Handle2 = h2; } public Vector2 Handle1 { get {return handle1;} set { handle1 = value;} } public Vector2 Point { get {return point;} set { point = value;} } public Vector2 Handle2 { get {return handle2;} set { handle2 = value;} } } } namespace XenTutorial { // Перечисление типов кривых Ipo, загружаемых из файла сцены. publicenum IpoType { //Object Ipo LocX =0, LocY, LocZ, RotX, RotY, RotZ, ScaleX, ScaleY, ScaleZ, //Camera Ipo Lens, FarClip, NearClip, //Lamp Ipo Energ, R, G, B, Dist, //Action Ipo (for Armature) //LocX, LocY, LocZ SizeX, SizeY, SizeZ, QuatX, QuatY, QuatZ, QuatW } } namespace XenTutorial { // Класс кривой анимации publicclass IpoCurve : ICollection<BezTriple> { // имя кривой string name; // узлы кривой List<BezTriple> points; // точки начала и конца кривой Vector2 start, end; // Зациклина ли кривая // здесь мы не реализуем все типы воспроизведения циклов анимации // выбираем самый простой – при переходе времени за промежуток анимации // повторяем анимацию сначала bool ciclic = false; publicbool Ciclic { get {return ciclic;} set { ciclic = value;} } // Далее два конструктора класса public IpoCurve(string curveName) { Name = curveName; points =new List<BezTriple>(); } public IpoCurve(string curveName, BezTriple[] points) { Name = curveName; this.points=new List<BezTriple>(); foreach(BezTriple point in points) { this.Add(point); } } // функция получения интерполированного значения кубической кривой Безье между двумя узлами private Vector2 GetValue(float t, BezTriple p1, BezTriple p2) { // вычисление положения на кубической кривой Безье return(float)System.Math.Pow((1.0f - t), 3)* p1.Point+ 3f *(t *(float)System.Math.Pow((1.0f - t), 2)* p1.Handle2+ t * t *(1.0f - t)* p2.Handle1)+ (float)System.Math.Pow(t, 3)* p2.Point; } // функция получения значения кривой по номеру кадра анимации publicfloat GetValueByFrameNumber(float frame) { float len = End.X- Start.X; // если промежуток анимации по времени равен нулю (например когда // создан всего один ключевой узел), то возвращаем значение в первом кадре if(len == 0.0f) { return Start.Y; } // если кривая зациклена, то рассчитываем значение времени внутри промежутка анимации if(Ciclic) { if(len > 0.0f) { if(frame < Start.X) { float over =(Start.X- frame)/ len; frame += len *(float)System.Math.Floor(over); } elseif(frame > End.X) { float over =(frame - End.X)/ len; frame -= len *(float)System.Math.Ceiling(over); } } } // поиск нужной пакы ключевых кадров и интерполяция с использованием кубической // кривой Безье if(frame <= Start.X) { return Start.Y; } elseif(frame >= End.X) { return End.Y; } else { for(int i =1; i < points.Count; i++) { if((frame >= points[i -1].Point.X)&&(frame <= points[i].Point.X)) { float t =(frame - points[i -1].Point.X)/(points[i].Point.X- points[i -1].Point.X); return GetValue(t, points[i -1], points[i]).Y; } } } return 0f; } publicstring Name { get {return name;} set { name = value;} } public Vector2 End { get {return end;} private set { end = value;} } public Vector2 Start { get {return start;} private set { start = value;} } // переопределение начала и конца анимации при добавлении или удалении // узлов из списка privatevoid RefreshEndStart() { if(points.Count>0) { Start = points[0].Point; End = points[points.Count-1].Point; } else { Start =new Vector2(0f); End =new Vector2(0f); } } // реализация коллекции узлов анимации с помощью ICollection // не ривожу из экономии места } } namespace XenTutorial { publicclass Ipo { // массив кривых анимации private IpoCurve[] curves = null; public Ipo() { // число кривых в массиве рассчитывается из кол-ва элементов перечисления IpoType curves =new IpoCurve[Enum.GetNames(typeof(IpoType)).Length]; } // установка кривой в коллекцию по индексу, хранящемуся в имени перечисления publicvoid SetIpo(IpoType type, IpoCurve curve) { curves[(int)type]= curve; } // установка кривой в коллекцию имени кривой publicvoid SetIpo(string name, IpoCurve curve) { string[] names =Enum.GetNames(typeof(IpoType)); for(int i =0; i < names.Length; i++) { if(names[i]== name) { curves[i]= curve; } } } // получение кривой из коллекции public IpoCurve GetIpo(IpoType type) { return curves[(int)type]; } // Метод проверки имени кривой на предмет наличия в перечислении publicstaticbool IsSupportIpoName(string name) { foreach(string n inEnum.GetNames(typeof(IpoType))) { if(n == name) { return true; } } return false; } } }
Итак мы подошли к финальной части нашей статьи, в который мы наконец реализуем загрузку сцены в игру. Реализация второй части класса XenScene, находящаяся в файле XenScene.Loader.cs представлена далее (в данном классе используются методы класса XmlReadHelper, который не приведен в самой статье, его описание Вы найдете в исходном коде, прилагаемом к статье):
namespace XenTutorial { publicpartialclass XenScene : IDraw { // Метод загрузки сцены их Xml-файла // Прежде чем его использовать загрузите материалы сцены publicstatic XenScene Load(string sceneFileName) { // Загружаем документ в память XmlDocument levelDoc =new XmlDocument(); levelDoc.Load(sceneFileName); // Создаем корневой элемент сцены XenSceneNode Root =new XenSceneNode("Root"); Root.Scale=new Vector3(1, 1, 1); Root.Name="Root"; // Загружаем все ноды сцены foreach(XmlNode node in levelDoc.DocumentElement) { if(node.Name.ToLower()=="node") { ReadSceneNode(node, Root); } } // Создаем сцену и передаем ей корневой элемент XenScene scene =new XenScene(Root); // освобождаем документ levelDoc = null; GC.Collect(); // возвращаем сцену return scene; } // Функция чтения узла сцены publicstaticvoid ReadSceneNode(XmlNode node, XenSceneNode parent) { // читаем атрибуты с именем и типом узла сцены string nodeName = XmlReadHelper.GetAttributeValue(node, "Name"); string nodeType = XmlReadHelper.GetAttributeValue(node, "Type"); XenSceneNode snode = null; string[] vals; // с учетом типа узла читаем специфические данные о нем switch(nodeType.ToLower().Trim()) { case"camera":// Загрузка камеры vals = XmlReadHelper.GetAttributeValues( node, newstring[]{"Lens", "ClipStart", "ClipEnd"}); // Создаем узел камеры XenCameraNode cam; cam =new XenCameraNode( nodeName, Matrix.CreatePerspectiveFieldOfView( XmlReadHelper.Val(vals[0]), 4f / 3f, XmlReadHelper.Val(vals[1]), XmlReadHelper.Val(vals[2])) ); snode = cam; break; case"mesh":// Загрузка геометрии XenMesh mesh; //Создаем простой узел snode =new XenSceneNode(nodeName); // выполняем поиск наличия данных о геометрии XmlNode meshNode = XmlReadHelper.FindChildNode(node, "Mesh"); if(meshNode ==null) { break; } // Если данные о геометрии есть, то ищем узлы, стодержащие данные о вершинах // и о частях модели XmlNode verticesNode = XmlReadHelper.FindChildNode(meshNode, "Vertices"); List<XmlNode> meshParts = XmlReadHelper.FindChildNodes(meshNode, "MeshPart"); List<Vector3> verticesPosition =new List<Vector3>(); List<ushort> indices =new List<ushort>(); List<XenMeshPart> parts =new List<XenMeshPart>(); // Список врешин, на основе которого будет построен вершинный буфер List<XenVertexPositionTangentSpaceTexture> vertices = new List<XenVertexPositionTangentSpaceTexture>(); // читаем позиции вершин for(int i =0; i < verticesNode.ChildNodes.Count; i++) { XmlNode n = verticesNode.ChildNodes[i]; if(n.Name.ToLower()=="vert") { vals = XmlReadHelper.GetAttributeValues(n, newstring[]{"X", "Y", "Z"}); verticesPosition.Add(XmlReadHelper.ReadVector3(n, "X", "Y", "Z")); } } // читаем данные о частях геометрии for(int i =0; i < meshParts.Count; i++) { foreach(XmlNode face in meshParts[i].ChildNodes) { for(int j =0; j < face.ChildNodes.Count; j++) { if(face.ChildNodes[j].Name.ToLower()=="v") { // добавляем вершину в список вершин с одновременным // добавлением индекса вершины в коллекцию indices.Add( AddVertexToList( ref vertices, ref verticesPosition, verticesPosition[int.Parse(XmlReadHelper.GetAttributeValue(face.ChildNodes[j], "Index"))], XmlReadHelper.ReadVector3(face.ChildNodes[j], "NX", "NY", "NZ"), XmlReadHelper.ReadVector3(face.ChildNodes[j], "TX", "TY", "TZ"), XmlReadHelper.ReadVector2(face.ChildNodes[j], "UVX", "UVY"))); } } } // создаем эеземпляр части геометрии и рассчитываем для нее величины XenMeshPart mp =new XenMeshPart(); parts.Add(mp); if(parts.Count==1) { mp.StartIndex=0; mp.TrianglesCount= indices.Count/3; } else { mp.StartIndex= parts[i -1].StartIndex+ parts[i -1].TrianglesCount*3; mp.TrianglesCount= indices.Count/3- parts[i].StartIndex/3; } // получаем материал по имени из коллекции загруженных материалов mp.Material= XenMaterial.GetMaterialByName( XmlReadHelper.GetAttributeValue(meshParts[i], "Material")); } // Удаляем "пустые" части геометрии // Это необходимо сделать, т.к. Blender иногда сохраняет ссылку на // пустую геометрию с назначенным материалом // Такая ситуация может произойти при редактировании геометрии моделером for(int i = parts.Count-1; i >=0; i--) { if(parts[i].TrianglesCount==0) { parts.RemoveAt(i); } } // Расчет ограничивающего бокса BoundingBox bbox = BoundingBox.CreateFromPoints(verticesPosition); ushort[] ins = indices.ToArray(); XenVertexPositionTangentSpaceTexture[] verts = vertices.ToArray(); Vector3[] pnts = verticesPosition.ToArray(); // создание экземпляра геометрии mesh =new XenMesh(ref verts, ref ins, ref pnts, ref parts, ref bbox); // добавление геометрии узлу сцены snode.Meshes.Add(mesh); break; case"empty":// Загрузка объекта пустышки snode =new XenSceneNode(nodeName); break; case"lamp":// Загрузка источников света vals = XmlReadHelper.GetAttributeValues( node, newstring[]{"LampType", "Dist", "Energy"}); XmlNode colorNode = XmlReadHelper.FindChildNode(node, "Color"); Vector3 color = XmlReadHelper.ReadVector3(colorNode, "X", "Y", "Z"); if(vals[0].ToLower()=="sun")// направленный источник света { XenDirectionalLightNode light = new XenDirectionalLightNode(nodeName, new Color(color)); snode = light; } else// все остальные источники считаем точечными { XenPointLightNode light = new XenPointLightNode(nodeName, new Color(color)); light.Range= XmlReadHelper.Val(vals[1]); light.Intensity= XmlReadHelper.Val(vals[2]); snode = light; } break; default:// другие типы объектов сцены не поддерживаем return; } if(snode ==null) { return; } // загружаем кривые анимации XmlNode ipoNode = XmlReadHelper.FindChildNode(node, "Ipo"); snode.Ipo=new Ipo(); foreach(XmlNode ipo in ipoNode.ChildNodes) { // если кривая анимации поддерживается if(Ipo.IsSupportIpoName(ipo.Name)) { // определяем тип кривой анимации IpoType type =(IpoType)Enum.Parse(typeof(IpoType), ipo.Name); // создаем коллекцию для ключевых узлов кривой анимации List<BezTriple> points =new List<BezTriple>(); // читаем все узлы анимации List<XmlNode> pointNodes = XmlReadHelper.FindChildNodes(ipo, "Point"); foreach(XmlNode point in pointNodes) { XmlNode h1, p, h2; h1 = XmlReadHelper.FindChildNode(point, "h1"); p = XmlReadHelper.FindChildNode(point, "p"); h2 = XmlReadHelper.FindChildNode(point, "h2"); points.Add(new BezTriple( XmlReadHelper.ReadVector2(h1, "X", "Y"), XmlReadHelper.ReadVector2(p, "X", "Y"), XmlReadHelper.ReadVector2(h2, "X", "Y"))); } // создаем кривую на основе узлов IpoCurve curve =new IpoCurve(ipo.Name, points.ToArray()); // проверяем, требуется ли повторять анимацию по окончании if(XmlReadHelper.GetAttributeValue(ipo, "Extend").ToUpper()=="CYCLIC") { curve.Ciclic= true; } // добавляем загруженную кривую в коллекцию кривых объекта snode.Ipo.SetIpo(type, curve); } } // Загружаем дополнительные параметры узлов сцены List<XenSceneNodeParameter> props =new List<XenSceneNodeParameter>(); XmlNode propertiesNode = XmlReadHelper.FindChildNode(node, "Properties"); if(propertiesNode !=null) { XenSceneNodeParameter prop; foreach(XmlNode property in propertiesNode.ChildNodes) { if(property.Name.ToLower()=="property") { // читаем имя и значение свойства vals = XmlReadHelper.GetAttributeValues( node, newstring[]{"Name", "Value"}); prop =new XenSceneNodeParameter(); prop.Name= vals[0]; prop.Value= vals[1]; props.Add(prop); } } } snode.Parameters= props; // загружаем начальные трансформации объекта сцены (локальные) XmlNode transformsNode = XmlReadHelper.FindChildNode(node, "Transforms"); if(transformsNode !=null) { XmlNode rotNode = XmlReadHelper.FindChildNode(transformsNode, "Rot"); XmlNode posNode = XmlReadHelper.FindChildNode(transformsNode, "Pos"); XmlNode sclNode = XmlReadHelper.FindChildNode(transformsNode, "Scl"); snode.Rotation= XmlReadHelper.ReadVector3(rotNode, "X", "Y", "Z"); snode.Translation= XmlReadHelper.ReadVector3(posNode, "X", "Y", "Z"); snode.Scale= XmlReadHelper.ReadVector3(sclNode, "X", "Y", "Z"); } snode.Name= nodeName; // добавляем текущий узел дерева сцены к родителю parent.AddChild(snode); // загружаем все дочерние узлы дерева сцены foreach(XmlNode child in node.ChildNodes) { if(child.Name.ToLower()=="node") { ReadSceneNode(child, snode); } } } // метод добавляющий в коллекцию вершин геометрии новую вершину и возвращающий ее индекс publicstaticushort AddVertexToList( ref List<XenVertexPositionTangentSpaceTexture> vertsList, ref List<Vector3> positions, Vector3 pos, Vector3 norm, Vector3 tan, Vector2 uv) { // Считаем бинормаль Vector3 binormal = Vector3.Cross(tan, norm); binormal.Normalize(); binormal.X=(float)System.Math.Round(binormal.X, 4); binormal.Y=(float)System.Math.Round(binormal.Y, 4); binormal.Z=(float)System.Math.Round(binormal.Z, 4); // Добавляемый вершину XenVertexPositionTangentSpaceTexture vertex = new XenVertexPositionTangentSpaceTexture(); vertex.Position= pos; vertex.Normal= norm; vertex.Binormal= binormal; vertex.Tangent= tan; vertex.TextureCoordinate= uv; //// Некоторая оптимизация, правда может сильно повлиять на скорость загрузки //XenVertexPositionTangentSpaceTexture tmp; //// Ищем такой же вертекс в списке //for (int i = 0; i < vertsList.Count; i++) //{ // tmp = vertsList; // if (CompareVector3(tmp.Position, vertex.Position) && // CompareVector3(tmp.Normal, vertex.Normal) && // CompareVector3(tmp.Tangent, vertex.Tangent) && // CompareVector2(tmp.TextureCoordinate, vertex.TextureCoordinate)) // { // return (ushort)i; // такой вертекс уже существует, возвращаем его индекс // } //} // Добавляем новую вершину, т.к. такая в списке не найдена (если включена оптимизация) vertsList.Add(vertex); // Возвращаем индекс последнего элемента списка return(ushort)(vertsList.Count-1); } // методы сравнения векторов publicstaticbool CompareVector3(Vector3 v1, Vector3 v2) { return(Math.Abs(v1.X- v2.X)<0.001)&&(Math.Abs(v1.Y- v2.Y)<0.001)&&(Math.Abs(v1.Z- v2.Z)<0.001); } publicstaticbool CompareVector2(Vector2 v1, Vector2 v2) { return(Math.Abs(v1.X- v2.X)<0.001)&&(Math.Abs(v1.Y- v2.Y)<0.001); } } }
Наконец мы справились с нашей задачей! Поздравляю всех, кто дошел до конца! Давайте посмотрим на результаты:
По моему неплохо :).
Некоторые выводы:
Т.к. скорость загрузки модели у нас получилась довольно медленная, я рекомендую использовать бинарный формат. Для этого нет необходимости переписывать экспортер. Достаточно написать утилиту на C# которая бы конвертировала файл сцены в бинарный формат.
Возможно, в скрипте экспорта не учтены все моменты, так что при экспорте ваших собственных сцен могут возникнуть проблемы.
В статье могут присутствовать очепятки и неточности, если Вы меня поправите, то буду только рад.
Если у Вас еще есть силы, то настало время для бонусной части данной статьи.
Использование шейдеров в Xen на примере создания SkyBox’а.
Специально на закуску я хочу продемонстрировать возможности Xen по работе с шейдерами.
Теоретическая часть.
В составе проекта Xen разрабатывается специальная утилита XenFX, которая позволяет на основе файла эффекта создавать класс на языке C#. В результате все файлы *.fx перемещаются из проекта контента в основной проект и при компиляции все шейдеры сохраняются в виде отдельных классов внутри *.exe. Все параметры шейдеров становятся полями сгенерированных классов.
Утилита XenFX генерирует отдельный класс для каждой техники шейдера, который можно использовать в дальнейшем также как и стандартный шейдер MaterialShader.
Каждый сгенерированный класс, кроме параметров шейдера, так же имеет метод Bind, который устанавливает шейдер как текущий, и все что будет выведено на экран будет использовать данный шейдер. Метод Bind так же устанавливает автоматически параметры для различных семантик, определенных для параметров шейдера. В качестве семантик можно использовать любые комбинации WORLD, VIEW и PROJECTION в указанном порядке (например WORLDVIEWPROJECTION – верно, VIEWWORLDPROJECTION – не верно) к которым так же можно добавлять суффиксы 'TRANSPOSE' или 'INVERSE'. Кром е того существует поддержка некоторого количества не матричных семантик. Все они приведены в следующем примере:
поддержка не матричных семантик
1 2 3 4 5 6 7 8 9
float4x4 worldViewProj : WORLDVIEWPROJECTION;// пример семантики WORLDVIEWPROJECTION // дополнительные семантики float2 windowSize : WINDOWSIZE;// Размер в пикселях текущей цели визуализации float2 cameraFov : CAMERAFOV;// горизонтальный и вертикальный FOV камеры float2 cameraFovTan : CAMERAFOVTANGENT; float2 cameraNearFar : CAMERANEARFAR; float3 viewPoint : VIEWPOINT;// позиция камеры float3 viewDirection : VIEWDIRECTION;
Существует специальная семантика GLOBAL с которой можно определить параметры шейдеров, имеющих одинаковое название (название параметра). И устанавливать во всех шейдерах автоматически одинаковый параметр с помощью метода DrawState.SetShaderGlobal(). Например можно во всех шейдерах, зависящих от времени объявить параметр
float Time : GLOBAL;
И при каждом вызове отрисовки игры устанавливать этот параметры для всех шейдеров следующим образом:
1 2 3 4 5 6 7 8
protectedoverridevoid Draw(DrawState state) { // установка глобального параметра state.SetShaderGlobal("Time", state.TotalTimeSeconds); //отрисовка сцены drawToScreen.Draw(state); }
Данный параметр будет устанавливаться в каждом шейдере, при вызове метода Bind().
При генерации кода шейдера существует возможность указать флаги компиляции. Данные флаги устанавливаются в виде комментария на первой строке шейдера следующим образом:
И в заключение данная утилита поддерживает команды препроцессора в виде:
1 2 3 4 5
#ifdef XBOX360 ... #else ... #endif
Практическая часть.
Ну вот, с теорией покончено, давайте приступим к практике. Для реализации скайбокса нам понадобится следующий шейдер (описывать его нет нужны, он очень простой):
Создадим для шейдеров папку Effects в нашем проекте игры (не в проекте контента!). Добавим туда новый файл SkyBox.fx с содержанием, приведенным выше. Выберем этот файл и установим для него в параметрах специальную утилиту XenFX (набрать вручную):
Если данная утилита зарегистрирована в системе (а она регистрируется при построении Xen с использованием файла «xen prebuild.bat» входящего в состав поставки). То для файла эффекта будет создан дочерний файл SkyBox.fx.cs, который и является сгенерированным кодом шейдера. Давайте посмотрим на структуру методов и свойств сгенерированного объекта:
Нам интересны два свойства (SkyBoxTexture и WVP) и метод Bind() на основе который мы напишем класс скайбокса. Его реализация далее:
namespace XenTutorial { publicclass XenSkyBox : IDraw { // Вершинный и индексный буфферы private IVertices vertices; private IIndices indices; // Ссылка на кубическую текстуру private TextureCube texture; // размер бокса privatefloat size = 10.0f; // Конструктор принимает имя текстры и ссылку на менеджер контента public XenSkyBox(string texName, ContentManager manager) { texture = TextureLoader.GetTextureByName<TextureCube>(texName, manager); // Используем стандартную структуру XNA для создания вершинного буфера VertexPositionNormalTexture[] cubeVertices = new VertexPositionNormalTexture[36]; // Положения вершин Vector3 topLeftFront =new Vector3(-size, size, size); Vector3 bottomLeftFront =new Vector3(-size, -size, size); Vector3 topRightFront =new Vector3(size, size, size); Vector3 bottomRightFront =new Vector3(size, -size, size); Vector3 topLeftBack =new Vector3(-size, size, -size); Vector3 topRightBack =new Vector3(size, size, -size); Vector3 bottomLeftBack =new Vector3(-size, -size, -size); Vector3 bottomRightBack =new Vector3(size, -size, -size); // текстурные координаты 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); // нормали граней Vector3 frontNormal =new Vector3(0.0f, 0.0f, 1.0f); Vector3 backNormal =new Vector3(0.0f, 0.0f, -1.0f); Vector3 topNormal =new Vector3(0.0f, 1.0f, 0.0f); Vector3 bottomNormal =new Vector3(0.0f, -1.0f, 0.0f); Vector3 leftNormal =new Vector3(-1.0f, 0.0f, 0.0f); Vector3 rightNormal =new Vector3(1.0f, 0.0f, 0.0f); // передняя грань cubeVertices[0]= new VertexPositionNormalTexture( topLeftFront, frontNormal, textureTopLeft); cubeVertices[1]= new VertexPositionNormalTexture( bottomLeftFront, frontNormal, textureBottomLeft); cubeVertices[2]= new VertexPositionNormalTexture( topRightFront, frontNormal, textureTopRight); cubeVertices[3]= new VertexPositionNormalTexture( bottomLeftFront, frontNormal, textureBottomLeft); cubeVertices[4]= new VertexPositionNormalTexture( bottomRightFront, frontNormal, textureBottomRight); cubeVertices[5]= new VertexPositionNormalTexture( topRightFront, frontNormal, textureTopRight); // задняя грань cubeVertices[6]= new VertexPositionNormalTexture( topLeftBack, backNormal, textureTopRight); cubeVertices[7]= new VertexPositionNormalTexture( topRightBack, backNormal, textureTopLeft); cubeVertices[8]= new VertexPositionNormalTexture( bottomLeftBack, backNormal, textureBottomRight); cubeVertices[9]= new VertexPositionNormalTexture( bottomLeftBack, backNormal, textureBottomRight); cubeVertices[10]= new VertexPositionNormalTexture( topRightBack, backNormal, textureTopLeft); cubeVertices[11]= new VertexPositionNormalTexture( bottomRightBack, backNormal, textureBottomLeft); // верхняя грань cubeVertices[12]= new VertexPositionNormalTexture( topLeftFront, topNormal, textureBottomLeft); cubeVertices[13]= new VertexPositionNormalTexture( topRightBack, topNormal, textureTopRight); cubeVertices[14]= new VertexPositionNormalTexture( topLeftBack, topNormal, textureTopLeft); cubeVertices[15]= new VertexPositionNormalTexture( topLeftFront, topNormal, textureBottomLeft); cubeVertices[16]= new VertexPositionNormalTexture( topRightFront, topNormal, textureBottomRight); cubeVertices[17]= new VertexPositionNormalTexture( topRightBack, topNormal, textureTopRight); // нижняя грань cubeVertices[18]= new VertexPositionNormalTexture( bottomLeftFront, bottomNormal, textureTopLeft); cubeVertices[19]= new VertexPositionNormalTexture( bottomLeftBack, bottomNormal, textureBottomLeft); cubeVertices[20]= new VertexPositionNormalTexture( bottomRightBack, bottomNormal, textureBottomRight); cubeVertices[21]= new VertexPositionNormalTexture( bottomLeftFront, bottomNormal, textureTopLeft); cubeVertices[22]= new VertexPositionNormalTexture( bottomRightBack, bottomNormal, textureBottomRight); cubeVertices[23]= new VertexPositionNormalTexture( bottomRightFront, bottomNormal, textureTopRight); // левая грань cubeVertices[24]= new VertexPositionNormalTexture( topLeftFront, leftNormal, textureTopRight); cubeVertices[25]= new VertexPositionNormalTexture( bottomLeftBack, leftNormal, textureBottomLeft); cubeVertices[26]= new VertexPositionNormalTexture( bottomLeftFront, leftNormal, textureBottomRight); cubeVertices[27]= new VertexPositionNormalTexture( topLeftBack, leftNormal, textureTopLeft); cubeVertices[28]= new VertexPositionNormalTexture( bottomLeftBack, leftNormal, textureBottomLeft); cubeVertices[29]= new VertexPositionNormalTexture( topLeftFront, leftNormal, textureTopRight); // правая грань cubeVertices[30]= new VertexPositionNormalTexture( topRightFront, rightNormal, textureTopLeft); cubeVertices[31]= new VertexPositionNormalTexture( bottomRightFront, rightNormal, textureBottomLeft); cubeVertices[32]= new VertexPositionNormalTexture( bottomRightBack, rightNormal, textureBottomRight); cubeVertices[33]= new VertexPositionNormalTexture( topRightBack, rightNormal, textureTopRight); cubeVertices[34]= new VertexPositionNormalTexture( topRightFront, rightNormal, textureTopLeft); cubeVertices[35]= new VertexPositionNormalTexture( bottomRightBack, rightNormal, textureBottomRight); // создание вершинного буфера vertices =new Vertices<VertexPositionNormalTexture>(cubeVertices); // создание индексного буфера indices =new Indices<ushort>(newushort[]{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35}); } // Функция отрисовки publicvoid Draw(DrawState state) { // сохраняем текущее состояние визуализации в стэк state.PushRenderState(); // отключаем кулинг и запись глубины state.RenderState.DepthColourCull.CullMode= CullMode.None; state.RenderState.DepthColourCull.DepthWriteEnabled= false; // получаем шейдер SkyBoxRendererEffect shader = null; // Метод GetShader<> структуры DrawState получает экземпляр шейдера // как говорит разработчик Xen: это самый быстрый способ работы с шейдерам в его библиотеке shader = state.GetShader<SkyBoxRendererEffect>(); // устанавливаем текстуру шейдера shader.SkyBoxCubeMap= texture; // получаем матрицы камеры Matrix proj, view; Vector2 size = state.DrawTarget.Size; Vector3 pos; state.Camera.GetProjectionMatrix(out proj, ref size); state.Camera.GetViewMatrix(out view); state.Camera.GetCameraPosition(out pos); // рассчитываем матрицу shader.WVP= Matrix.CreateTranslation(pos)* view * proj; // учтанавливаем шейдер текущим // напомню, что все что мы далее будем рисовать на экране, будет // использовать данный шейдер shader.Bind(state); // Рисуем скайбокс vertices.Draw(state, indices, PrimitiveType.TriangleList); // Восстанавливаем состояние визуализации из стэка // заметте очень удобно ;) state.PopRenderState(); } #region Члены ICullable // Рисуем скайбокс всегда publicbool CullTest(ICuller culler) { return true; } #endregion } }
Примечание:
Собственно всегда есть какие-то ограничения. XenFX так же имеет некоторые ограничения:
Нельзя использовать в качестве параметров шейдеров структуры. Разработчик не поддерживает такую возможность. Если Вам надо передавать в шейдер сложные данные, то используйте набор массивов.
XenFX не поддерживает многопроходные шейдеры, в следствие чего нет возможности для каждого шага указывать состояние устройства. Выходом из этого положения является ручная установка параметров устройства перед визуализацией и ручная визуализация нескольких проходов в виде отдельных техник.
Ну и бывают некоторые глюки при генерации кода, но у меня они встречались не часто.