Граф сцены – логическая структура, которая упорядочивает объекты сцены в древовидную зависимость. Текущая реализация графа сцены немного отличается от распространенных подходов решения подобных задач, он позволяет легко размещать элементы в пространстве, изменять и отрисовывать их. В дополнение, реализована фильтрация элементов, которые не попадают в поле зрения камеры.
Данная статья подразумевает знакомство читателя с C#, XNA и базовым пониманием принципов 3Dграфики.
Понимание Матриц и Кватернионов
Вся математическая «поднаготная» матриц и кватернионов может оказаться слишком сложным материалом для новичков, поэтому тут будут рассмотрены основные их характеристики и достоинства.
На самом деле матрицы и кватернионы можно использовать без углубления в математические дебри их реализаций, но желательно все же знать математику этих объектов, тогда их поведение для вас будет более предсказуемым. Если объясняться простым языком, то эти объекты инкапсулируют комбинации операции, которые можно произвести над трехмерным объектом. Матричные операции включают в себя перемещение (translation), вращение (rotation) и масштабирование (scaling). Кватернионы в основном ограничиваются только вращением.
Перемещение, как нетрудно догадаться, реализует изменение объектом своего местоположения в пространстве. В XNA Framework за перемещение отвечает свойство «Translation» структуры Matrix, через это свойство можно с легкостью как устанавливать так получать данные о перемещении объекта в пространстве. Для получения матрицы получения можно воспользоваться статичным методом Matrix.CreateTranslation.
Вращение – оно собственно и в Африке вращение. Обе структуры и матрицы, и кватернионы могут вращать объект как по одной, так и по всем остальным осям. В XNA также можно создавать матрицы и кватернионы вращения, указывая классические yaw, pitch, и roll значения.
Масштабирование отвечает за изменение размера объекта по одной или нескольким осям. Матрица масштабирования может быть создана статичным методом Matrix.CreateScale.
Иногда возникает необходимость, чтобы матрица или кватернион не оказывали воздействия на объект, для этого используется «единичная» (identity) матрица (кватернион). Умножение на единичную структуру матрицы или кватерниона, не приводит ни к какому изменению объекта. В XNA получить единичные экземпляры можно через Matrix.Identity и Quaternion.Identity.
Основное свойство матриц и кватернионов заключается в том, что несколько операций можно объединить в одну структуру, к примеру в место того чтобы к модели по очереди применять перемещение, вращение и масштабирование и хранить все три матрицы трансформации их можно объединить в одну и работать только с одной структурой. Объединение и матриц, и кватернионов производится операцией перемножения, умножение на единичную структуру не приводит к изменению объекта, это фактически операция «*1».
Для отрисовки модели используются три основные матрицы: world, view и projection. Мировая матрица (world) предназначена для позиционирования модели в пространстве, это может быть не обязательно пространство сцены, это может быть мировое пространство другой модели. После позиционирования модели в пространстве, он подвергается трансформации в пространство камеры (view), это позволяет перемещать камеру без изменения трансформаций самой модели. И наконец, производится перевод трехмерного пространства сцены в двумерное, для вывода на экран, при помощи матрицы проекции (projection).
Фундамент графа сцены
Построение дерева графа
Как уже упоминало выше граф сцены – это древовидная структура, в текущей реализации представленная двумя классами: SceneNodeCollection – коллекция элементов дерева и SceneNode – одиночный элемент. Интерфейс IController будет использоваться для анимации элементов графа.
publicclass SceneNodeCollection : ICollection<SceneNode>{/// Тут простая реализация коллекции}publicinterface IController{void UpdateSceneNode(SceneNode node, GameTime gameTime);}publicclass SceneNode{// Поля SceneNodeCollection children =new SceneNodeCollection(); Vector3 position = Vector3.Zero; Vector3 offset = Vector3.Zero; Quaternion rotation = Quaternion.Identity;bool visible = true; Matrix absoluteTransform = Matrix.Identity; IController controller;// Свойстваpublic SceneNodeCollection Children{ get {return children;}}public Vector3 Position{ get {return position;} set { position = value;}}public Vector3 Offset{ get {return offset;} set { offset = value;}}public Quaternion Rotation{ get {return rotation;} set { rotation = value;}}publicbool Visible{ get {return visible;} set { visible = value;}}public BoundingSphere BoundingSphere{ get {return GetBoundingSphere();}}public Matrix AbsoluteTransform{ get {return absoluteTransform;} set { absoluteTransform = value;}}public IController Controller{ get {return controller;} set { controller = value;}}// Конструктор по умолчаниюpublic SceneNode(){}//Методыprotectedvirtual BoundingSphere GetBoundingSphere(){returnnew BoundingSphere(center, 0);}publicvirtualvoid Update(SceneGraph sceneGraph){if(controller !=null) controller.UpdateSceneNode(this, sceneGraph.GameTime);}publicvirtualvoid Draw(SceneGraph sceneGraph){}}
Класс SceneNodeCollection реализует стандартную коллекцию, при желании его можно заменить классом List, но не рекомендуется использовать List как незащищенное публичное поле.
Класс SceneNode представляет собой «листья» дерева сцены. Каждый SceneNode содержит в себе коллекцию дочерних «листьев» которые хранятся в SceneNodeCollection, таким образом мы получаем иерархию объектов сцены. Свойства местоположения (Position) и вращения (Rotation) содержат в себе значения по отношению к родительскому элементу, трансформации в мировом пространстве впоследствии будут вычисляться из этих значений проходя по всему графу сцены. Свойство BoundingSphere отвечает за расчет видимости объекта в пирамиде (frustum) камеры, если объект не попадает в камеру, он просто не будет отрисовываться. Метод Update отвечает за пересчет внутренних состояний объекта, Draw отвечает, как нетрудно догадаться за отрисовку объекта.
Создание визуального представления объектов
Теперь, когда написана основа для графа сцены, настало время прикрутить к нему модель.
publicclass ModelSceneNode : SceneNode{// Поля Model model; BoundingSphere modelSphere;// Свойстваpublic Model Model{ get {return model;} set{ model = value;if(model ==null) modelSphere =new BoundingSphere(Center, 0);else CalculateBoundingSphere();}}// Конструкторыpublic ModelSceneNode(){}public ModelSceneNode(Model model){this.model= model; CalculateBoundingSphere();}// Методыprivatevoid CalculateBoundingSphere(){//Расчет BoundingSphere для модели modelSphere =new BoundingSphere();foreach(ModelMesh mesh in model.Meshes){ modelSphere = Microsoft.Xna.Framework.BoundingSphere.CreateMerged( modelSphere, model.Meshes[0].BoundingSphere);}}protectedoverride BoundingSphere GetBoundingSphere(){return modelSphere;}publicoverridevoid Draw(SceneGraph sceneGraph){if(sceneGraph.Camera!=null){if(model !=null){// Копирование иерархии трансформаций модели Matrix[] transforms =new Matrix[model.Bones.Count]; model.CopyAbsoluteBoneTransformsTo(transforms);// отрисовка кождого меша из моделиforeach(ModelMesh mesh in model.Meshes){foreach(Effect effect in mesh.Effects){ BasicEffect basicEffect = effect as BasicEffect;if(basicEffect ==null){thrownew NotSupportedException("Only the BasicEffect is supported in a model.");}//Установка матриц basicEffect.World= transforms[mesh.ParentBone.Index]* AbsoluteTransform; basicEffect.View= sceneGraph.Camera.View; basicEffect.Projection= sceneGraph.Camera.Projection; basicEffect.EnableDefaultLighting();} mesh.Draw(SaveStateMode.SaveState);}}}}}
Класс ModelSceneNode наследует от SceneNode и добавляет свойство Model для работы с моделью объекта, переопределяет метод CalculateBoundingSphere, который в свою очередь возвращает сферу окружения всей модели. Также переопределяется метод Draw который рассчитывает одну общую матрицу трансформации для модели из матриц мешей и матрицы элемента графа сцены (AbsoulteTransform), которая в свою очередь рассчитывается графом. Что же это нам дает? Модель размещенная где-то в пространстве и назначенная как дочерняя для другой модели, будет наследовать изменения своего «родителя».
Как уже упоминалось выше, камера дает возможность отображать сцену под любым углом и приближением без необходимости изменения параметров самих объектов.
publicclass Camera{// Поля Vector3 position = Vector3.Zero; Quaternion rotation = Quaternion.Identity;float fieldOfView =(float)(System.Math.PI/4.0);float aspectRatio = 4f / 3f;// Ширина/Высотаfloat nearPlane = 1.0f;float farPlane = 10500.0f; Matrix view; Matrix projection; BoundingFrustum frustum;// Свойстваpublic Vector3 Position{ get {return position;} set { position = value;}}public Quaternion Rotation{ get {return rotation;} set { rotation = value;}}publicfloat FieldOfView{ get {return fieldOfView;} set { fieldOfView = value;}}publicfloat AspectRatio{ get {return aspectRatio;} set { aspectRatio = value;}}publicfloat NearPlane{ get {return nearPlane;} set { nearPlane = value;}}publicfloat FarPlane{ get {return farPlane;} set { farPlane = value;}}public Matrix View{ get {return view;}}public Matrix Projection{ get {return projection;}}public BoundingFrustum Frustum{ get {return frustum;}}// Конструктор по умолчаниюpublic Camera(){}// Методыpublicvoid LookAt(Vector3 target, Vector3 up){ view = Matrix.CreateLookAt(position, target, up); rotation = Quaternion.CreateFromRotationMatrix(Matrix.Invert(view));}publicvirtualvoid Update(SceneGraph sceneGraph){ projection = Matrix.CreatePerspectiveFieldOfView(fieldOfView, aspectRatio, nearPlane, farPlane); Matrix matrix = Matrix.CreateFromQuaternion(rotation); matrix.Translation= position; view = Matrix.Invert(matrix); frustum =new BoundingFrustum(Matrix.Multiply(view, projection));}}
Свойства Position и Rotation класса камеры задают местоположение камеры в пространстве и ее ориентацию. Метод LookAt предназначен для «нацеливания» камеры в точку пространства. Метод Update производит необходимые расчеты, рассчитывает матрицы вида (view), проекции (projection) и рассчитывает новую усеченную пирамиду камеры (frustum), которая впоследствии пригодится для исключения отрисовки невидимых объектов.
Собираем все воедино
Когда все основные элементы реализованы, настало время собрать их в граф сцены.
publicclass SceneGraph{// Поля SceneNode rootNode =new SceneNode(); GraphicsDevice device; Camera camera; GameTime gameTime;int nodesCulled;// Свойстваpublic SceneNode RootNode{ get {return rootNode;} set { rootNode = value;}}public Camera Camera{ get {return camera;} set { camera = value;}}public GameTime GameTime{ get {return gameTime;}}public GraphicsDevice GraphicsDevice{ get {return device;}}publicint NodesCulled{ get {return nodesCulled;}}// Конструкторpublic SceneGraph(GraphicsDevice device){this.device= device;}// Методыvoid CalculateTransformsRecursive(SceneNode node){ node.AbsoluteTransform= Matrix.CreateTranslation(node.Offset)* Matrix.CreateFromQuaternion(node.Rotation)* node.AbsoluteTransform* Matrix.CreateTranslation(node.Position);//Рекурсивное обновление "потомков"foreach(SceneNode childNode in node.Children){ childNode.AbsoluteTransform= node.AbsoluteTransform; CalculateTransformsRecursive(childNode);}}void UpdateRecursive(SceneNode node){//Обновление элемента node.Update(this);//Рекурсивное обновление "потомков"foreach(SceneNode childNode in node.Children){ UpdateRecursive(childNode);}}void DrawRecursive(SceneNode node){//Отрисовкаif(node.Visible){ BoundingSphere transformedSphere =new BoundingSphere(); transformedSphere.Center= Vector3.Transform(node.BoundingSphere.Center, node.AbsoluteTransform); transformedSphere.Radius= node.BoundingSphere.Radius;if(camera.Frustum.Intersects(transformedSphere)){ node.Draw(this);}else{ nodesCulled++;}}foreach(SceneNode childNode in node.Children){ DrawRecursive(childNode);}}void CalculateTransforms(){ CalculateTransformsRecursive(rootNode);}publicvoid Update(GameTime time){ gameTime = time;if(camera !=null) camera.Update(this); UpdateRecursive(rootNode); CalculateTransformsRecursive(rootNode);}publicvoid Draw(){ nodesCulled =0; rootNode.AbsoluteTransform= Matrix.Identity; DrawRecursive(rootNode);}}
В основе дерева графа сцены лежит корневой элемент (rootNode), который содержит коллекцию дочерних элементов, каждый из которых содержит свою коллекцию дочерних элементов и так далее. Обход дерева происходит рекурсивно, граф сцены выполняет какую-нибудь операцию над элементом дерева и вызывает эту же операцию у потомка этого элемента, если у элемента нет потомков, то граф переходит к следующему элементу коллекции. Обработка будет происходить до тех пор, пока не будут обработаны все элементы дерева.
В процессе работы графа для каждого элемента рассчитывается матрица AbsoluteTransform, которая содержит в себе все действия по трансформации объекта в пространстве, родительский элемент передает эту матрицу в дочерние элементы и дочерний элемент «дописывает» в нее свои изменения присущие только этому элементу, таким образом если к примеру сдвинуть родительский элемент, все дочерние элементы сдвинутся вслед за родительским.
В процессе обхода дерева вычисляется, попадает ли сфера окружения (BoudingSphere) объекта в усеченную пирамиду (frustum) камеры и если попадает, то объект будет рисоваться, если же не попадает то, соответственно объект отрисовываться не будет и как следствие отпадает необходимость производить расчеты и сравнения для потомков этого объекта.
Пример использования Ниже приведен минимальный объем кода для отрисовки модели в XNA с использование графа сцены.
publicclass SceneGraphSampleGame : Microsoft.Xna.Framework.Game{ GraphicsDeviceManager graphics; ContentManager content; SceneGraph sceneGraph;// Конструкторpublic SceneGraphSampleGame(){ graphics =new GraphicsDeviceManager(this); content =new ContentManager(Services);}// Методыprotectedoverridevoid Initialize(){base.Initialize();}protectedoverridevoid LoadGraphicsContent(bool loadAllContent){ sceneGraph =new SceneGraph(graphics.GraphicsDevice);// Создание объекта для отображения модели ModelSceneNode modelNode =new ModelSceneNode( content.Load<Model>("Content/Ship"));// Присоединение объекта к графу сцены sceneGraph.RootNode.Children.Add(modelNode);// Создание камеры sceneGraph.Camera=new Camera(); sceneGraph.Camera.Position=new Vector3(0, 0, 200);}protectedoverridevoid Update(GameTime gameTime){if(GamePad.GetState(PlayerIndex.One).Buttons.Back== ButtonState.Pressed)this.Exit();// Обновление графа сценыif(sceneGraph !=null) sceneGraph.Update(gameTime);// Обновление прочих компонентовbase.Update(gameTime);}protectedoverridevoid Draw(GameTime gameTime){ graphics.GraphicsDevice.Clear(Color.CornflowerBlue);// Отрисовка графа сценыif(sceneGraph !=null) sceneGraph.Draw();// Отрисовка прочих компонентовbase.Draw(gameTime);}}
Как видно из кода граф сцены встраивается в основной цикл XNA. Сам граф сцены инициализируется в методе загрузки контента LoadGraphicsContent, там же инициализируется объект модели графа (ModelSceneNode) и камеры (Camera). Обновление и отрисовка графа вызывается в стандартных методах Update и Draw.
Управление элементами графа
Теперь реализуем управление элементами графа, так называемые контроллеры, которые будут отвечать за трансформации (передвижение, вращение, масштабирование) элементов в пределах графа. Класс контроллера реализует интерфейс IController и будет вызываться при обновлении элемента графа.
ControllerBase является абстрактным классом которые реализует интерфейс IController, он создан просто для удобства использования.
Классы XRotationController, YRotationController и ZRotationController обеспечивают вращение объекта графа. Коэффициент вращение определяется свойством RadiansPerSecond и пересчитывается в методе Update. Метод Update использует объединение кватернионов (операцией перемножения), для прибавления коэффициента вращения.
Ниже представленный код отображает изменения которые нужно внести в метод LoadGraphicContent основного класса Game для реализации использования контроллеров.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protectedoverridevoid LoadGraphicsContent(bool loadAllContent){ sceneGraph =new SceneGraph(graphics.GraphicsDevice); ModelSceneNode modelNode =new ModelSceneNode(content.Load<Model>("Content/Ship")); sceneGraph.RootNode.Children.Add(modelNode); sceneGraph.Camera=new Camera(); sceneGraph.Camera.Position=new Vector3(0,0,200);// Код добавленный для использования контроллера YRotationController rotationController =new YRotationController(); rotationController.RadiansPerSecond=(float)Math.PI; modelNode.Controller= rotationController;}
После присвоения контроллера экземпляру объекта, он будет изменять данный объект без написания какого-либо дополнительного кода. В текущем примере модель будет вращаться по оси Y? полный оборот за 2 секунды.
Заключение
Итоги
Граф сцены помогает упростить и делает игровой код более логичным. Позволяет централизованно управлять поведением объектов и их отрисовкой. В дополнение ко всему вышесказанному, граф сцен очень легко можно дополнять новым функционалом, с минимальными трудозатратами.
Куда копать
Граф сцены можно (и нужно) дополнять новыми типами объектов и контроллеров, в приложенном к статье проекту вы найдете реализацию дополнительных элементов графа.
Известные ограничения и недоработки
Проверяйте чтобы дерево сцены не замыкалось само на себя, в текущем примере это не контролировалось. Указание потомком элемента, данного элемента может привести к переполнению стека при первом же вызове рекурсивной функции.
Представленный тут код, написан для более понятного усвоения материала, он никоем образом не оптимизирован по производительности.
1035 Прочтений • [Создание графа сцены для XNA] [08.08.2012] [Комментариев: 0]