XNA
GameDev.ru / Сообщества / XNA / Статьи / Рисование простого SkyBox-а.

Рисование простого SkyBox-а.

Автор:

В этой статье мы рассмотрим метод создания SkyBox-а в XNA. SkyBox (небесный куб) — это задний фон в изображениях трёхмерной компьютерной графики. Дальние неподвижные объекты, как горы, небо, растительность, полоса горизонта — можно поместить заранее в текстуру и выводить только её. Чтобы задний фон присутствовал во всех направлениях, делают «большой» куб, на стенки которого устанавливаются соответственно 6-ть текстур, а внутри самого куба помещается наблюдатель — камера трёхмерного приложения.

SkyBox Simple Pic 3 | Рисование простого SkyBox-а.

Давайте создадим простой SkyBox для нашего 3D-мира в XNA. Для начала нам потребуется шесть текстур, которые будут представлять фронт, тыл, левую часть, правую, верх и низ нашего мира. Положите текстуры в папку внутри проекта.

SkyBox Simple Pic 1 | Рисование простого SkyBox-а.

Также нам понадобится простой шейдер файла эффекта (Effect), который мы будем использовать для рендеринга skybox’а.

Я отключил освещение и Z-буффер, а также включил texture clamping, чтобы увеличить производительность и предотвратить показ краев текстур.

//Входные переменные
float4x4 worldViewProjection;

texture baseTexture;

sampler baseSampler = 
sampler_state
{
    Texture = < baseTexture >;
    MipFilter = LINEAR;
    MinFilter = LINEAR;
    MagFilter = LINEAR;
    ADDRESSU = CLAMP;
    ADDRESSV = CLAMP;
};

struct VS_INPUT
{
    float4 ObjectPos: POSITION;
    float2 TextureCoords: TEXCOORD0;
};

struct VS_OUTPUT 
{
   float4 ScreenPos:   POSITION;
   float2 TextureCoords: TEXCOORD0;
};

struct PS_OUTPUT 
{
   float4 Color:   COLOR;
};


VS_OUTPUT SimpleVS(VS_INPUT In)
{
   VS_OUTPUT Out;

    //Переходим к экранному пространству
    Out.ScreenPos = mul(In.ObjectPos, worldViewProjection);
    Out.TextureCoords = In.TextureCoords;

    return Out;
}

PS_OUTPUT SimplePS(VS_OUTPUT In)
{
    PS_OUTPUT Out;

    Out.Color = tex2D(baseSampler,In.TextureCoords);

    return Out;
}

//-------------------------------------------------------------------------//
// Раздел техники для простого экранного преобразования
//-------------------------------------------------------------------------//
technique Simple
{
   pass Single_Pass
   {
        LIGHTING = FALSE;
        ZENABLE = FALSE;
        ZWRITEENABLE = FALSE;
        ALPHATESTENABLE = FALSE;
        ALPHABLENDENABLE = FALSE;

        CULLMODE = CCW;

        VertexShader = compile vs_1_1 SimpleVS();
        PixelShader = compile ps_1_1 SimplePS();
   }
}

Поскольку теперь у нас есть простой шейдер для рендеринга, давайте спроектируем класс skybox’а.

Мы будем использовать модель интерфейса GameComponent.

#region using’и
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Storage;
#endregion

namespace SkyBox
{

    class SkyBox : GameComponent,IDrawable
    {

Я добавил IDrawable к компоненту skybox’а. Это добавило некоторые функции под разделом со следующим названием:

#region Члены IDrawable

Вы можете автоматически сгенерировать описания этого члена, если кликнете правой кнопкой мыши на слове IDrawable и выберете Implement Interface. Это меню доступно, только если вы щелкаете на IDrawable первый раз. Если вы забыли выбрать Implement Interface, то вы можете вернуться и удалить слово IDrawable, а потом напечатать его снова, кликнув затем по нему правой кнопкой мыши и выбрав Implement Interface.

Skybox будет содержать шесть сторон, поэтому нам нужно место для шести текстур:

        Texture2D[] textures = new Texture2D[6];
        Effect effect;

Давайте объявим буфер вершин, буфер индексов и объявление буфера (vertex declaration), чтобы мы могли рендерить skybox вручную. Этот подход лучше, чем использование сетки (mesh), так как вы можете рендерить только видимые камере стороны skybox’а и пропустить остальные стороны.

        VertexBuffer vertices;
        IndexBuffer indices;
        VertexDeclaration vertexDecl;

Запомним направление и позицию камеры, чтобы skybox в последствии мог быть преобразован правильно:

        Vector3 vCameraDirection;
        Vector3 vCameraPosition;

Файлу эффекта понадобится комбинированная мировая/видовая/проекционная (World * View * Projection) матрица преобразования:

        Matrix viewMatrix;
        Matrix projectionMatrix;
        Matrix worldMatrix;

Давайте сохраним указатель на content manager. Это, вероятно, можно оптимизировать.

  
        ContentManager content;

Базовый класс игрового компонента требует от нас, чтобы мы передали указатель на экземпляр класса Game:

        public SkyBox(Game g)
            : base(g)
        {
        }

Давайте определим методы-аксессоры для переменных члена:

        public Vector3 CameraDirection
        {
            get { return vCameraDirection; }
            set { vCameraDirection = value; }
        }

Когда изменяется позиция камеры, нам необходимо пересчитать мировую матрицу преобразования:

        public Vector3 CameraPosition
        {
            get { return vCameraPosition; }
            set 
            {
                vCameraPosition = value;

                worldMatrix = Matrix.CreateTranslation(vCameraPosition);
            }
        }

        public Matrix ViewMatrix
        {
            set { viewMatrix = value; }
        }

        public Matrix ProjectionMatrix
        {
            set { projectionMatrix = value; }
        }

        public ContentManager ContentManager
        {
            set { content = value; }
        }

Внутри метода инициализации нам надо сделать работу по созданию skybox’а:

        public override void Initialize()
        {
            base.Initialize();

Загружаем все шесть сторон (текстур) skybox'а:

            textures[0] = content.Load<Texture2D>("Skybox\\back");
            textures[1] = content.Load<Texture2D>("Skybox\\front");
            textures[2] = content.Load<Texture2D>("Skybox\\bottom");
            textures[3] = content.Load<Texture2D>("Skybox\\top");
            textures[4] = content.Load<Texture2D>("Skybox\\left");
            textures[5] = content.Load<Texture2D>("Skybox\\right");

            effect = content.Load<Effect>("Skybox\\skybox");

Получаем указатель на графическое устройство, чтобы мы могли создать вершинный и индексный буферы:

            IGraphicsDeviceService graphicsService = (IGraphicsDeviceService)
                Game.Services.GetService(typeof(IGraphicsDeviceService));

Для рендеринга от нас потребуется определить объявление вершин (vertex declaration). Поскольку мы используем тип вершины PositionTexture, то он содержит два элемента: Position (Vector3) и TextureCoordinate (Vector2):

            vertexDecl = new VertexDeclaration(graphicsService.GraphicsDevice,
                new VertexElement[] {
                    new VertexElement(0,0,VertexElementFormat.Vector3,
                           VertexElementMethod.Default,
                            VertexElementUsage.Position,0),
                    new VertexElement(0,sizeof(float)*3,VertexElementFormat.Vector2,
                           VertexElementMethod.Default,
                            VertexElementUsage.TextureCoordinate,0)});

Создаем вершинный буфер с 4*6 вершинами. Он будет содержать по четыре вершины на каждую сторону skybox’а. Установите ResourceUsage на WriteOnly, чтобы оптимизировать производительность:

            vertices = new VertexBuffer(graphicsService.GraphicsDevice, 
                                typeof(VertexPositionTexture), 
                                4 * 6, 
                                ResourceUsage.WriteOnly);

Заполните вершинный буфер:

            VertexPositionTexture[] data = new VertexPositionTexture[4*6];
            
            Vector3 vExtents = new Vector3(500, 500, 500);
            //зад
            data[0].Position = new Vector3(vExtents.X, -vExtents.Y, -vExtents.Z);
            data[0].TextureCoordinate.X = 1.0f; data[0].TextureCoordinate.Y = 1.0f;
            data[1].Position = new Vector3(vExtents.X, vExtents.Y, -vExtents.Z);
            data[1].TextureCoordinate.X = 1.0f; data[1].TextureCoordinate.Y = 0.0f;
            data[2].Position = new Vector3(-vExtents.X, vExtents.Y, -vExtents.Z);
            data[2].TextureCoordinate.X = 0.0f; data[2].TextureCoordinate.Y = 0.0f;
            data[3].Position = new Vector3(-vExtents.X, -vExtents.Y, -vExtents.Z);
            data[3].TextureCoordinate.X = 0.0f; data[3].TextureCoordinate.Y = 1.0f;

            //перед
            data[4].Position = new Vector3(-vExtents.X, -vExtents.Y, vExtents.Z);
            data[4].TextureCoordinate.X = 1.0f; data[4].TextureCoordinate.Y = 1.0f;
            data[5].Position = new Vector3(-vExtents.X, vExtents.Y, vExtents.Z);
            data[5].TextureCoordinate.X = 1.0f; data[5].TextureCoordinate.Y = 0.0f;
            data[6].Position = new Vector3(vExtents.X, vExtents.Y, vExtents.Z);
            data[6].TextureCoordinate.X = 0.0f; data[6].TextureCoordinate.Y = 0.0f;
            data[7].Position = new Vector3(vExtents.X, -vExtents.Y, vExtents.Z);
            data[7].TextureCoordinate.X = 0.0f; data[7].TextureCoordinate.Y = 1.0f;

            //низ
            data[8].Position = new Vector3(-vExtents.X, -vExtents.Y, -vExtents.Z);
            data[8].TextureCoordinate.X = 1.0f; data[8].TextureCoordinate.Y = 0.0f;
            data[9].Position = new Vector3(-vExtents.X, -vExtents.Y, vExtents.Z);
            data[9].TextureCoordinate.X = 1.0f; data[9].TextureCoordinate.Y = 1.0f;
            data[10].Position = new Vector3(vExtents.X, -vExtents.Y, vExtents.Z);
            data[10].TextureCoordinate.X = 0.0f; data[10].TextureCoordinate.Y = 1.0f;
            data[11].Position = new Vector3(vExtents.X, -vExtents.Y, -vExtents.Z);
            data[11].TextureCoordinate.X = 0.0f; data[11].TextureCoordinate.Y = 0.0f;

            //верх
            data[12].Position = new Vector3(vExtents.X, vExtents.Y, -vExtents.Z);
            data[12].TextureCoordinate.X = 0.0f; data[12].TextureCoordinate.Y = 0.0f;
            data[13].Position = new Vector3(vExtents.X, vExtents.Y, vExtents.Z);
            data[13].TextureCoordinate.X = 0.0f; data[13].TextureCoordinate.Y = 1.0f;
            data[14].Position = new Vector3(-vExtents.X, vExtents.Y, vExtents.Z);
            data[14].TextureCoordinate.X = 1.0f; data[14].TextureCoordinate.Y = 1.0f;
            data[15].Position = new Vector3(-vExtents.X, vExtents.Y, -vExtents.Z);
            data[15].TextureCoordinate.X = 1.0f; data[15].TextureCoordinate.Y = 0.0f;


            //лево
            data[16].Position = new Vector3(-vExtents.X, vExtents.Y, -vExtents.Z);
            data[16].TextureCoordinate.X = 1.0f; data[16].TextureCoordinate.Y = 0.0f;
            data[17].Position = new Vector3(-vExtents.X, vExtents.Y, vExtents.Z);
            data[17].TextureCoordinate.X = 0.0f; data[17].TextureCoordinate.Y = 0.0f;
            data[18].Position = new Vector3(-vExtents.X, -vExtents.Y, vExtents.Z);
            data[18].TextureCoordinate.X = 0.0f; data[18].TextureCoordinate.Y = 1.0f;
            data[19].Position = new Vector3(-vExtents.X, -vExtents.Y, -vExtents.Z);
            data[19].TextureCoordinate.X = 1.0f; data[19].TextureCoordinate.Y = 1.0f;

            //право
            data[20].Position = new Vector3(vExtents.X, -vExtents.Y, -vExtents.Z);
            data[20].TextureCoordinate.X = 0.0f; data[20].TextureCoordinate.Y = 1.0f;
            data[21].Position = new Vector3(vExtents.X, -vExtents.Y, vExtents.Z);
            data[21].TextureCoordinate.X = 1.0f; data[21].TextureCoordinate.Y = 1.0f;
            data[22].Position = new Vector3(vExtents.X, vExtents.Y, vExtents.Z);
            data[22].TextureCoordinate.X = 1.0f; data[22].TextureCoordinate.Y = 0.0f;
            data[23].Position = new Vector3(vExtents.X, vExtents.Y, -vExtents.Z);
            data[23].TextureCoordinate.X = 0.0f; data[23].TextureCoordinate.Y = 0.0f;

            vertices.SetData<VertexPositionTexture>(data);

Теперь нам надо создать индексный буфер, который будет использоваться для индексирования каждой поверхности во время рендеринга:

            indices = new IndexBuffer(graphicsService.GraphicsDevice, 
                                typeof(short),6*6, 
                                ResourceUsage.WriteOnly);

            short[] ib = new short[6 * 6];

            for (int x = 0; x < 6; x++)
            {
                ib[x * 6 + 0] = (short) (x * 4 + 0);
                ib[x * 6 + 2] = (short) (x * 4 + 1);
                ib[x * 6 + 1] = (short) (x * 4 + 2);

                ib[x * 6 + 3] = (short) (x * 4 + 2);
                ib[x * 6 + 5] = (short) (x * 4 + 3);
                ib[x * 6 + 4] = (short) (x * 4 + 0);
            }

            indices.SetData<short>(ib);
            
        }

Любой код, который должен быть выполнен между каждым кадром, помещаем в метод Update:

        public override void Update(GameTime gameTime)
        {
            
            base.Update(gameTime);
        }

Вот реализация интерфейса IDrawable:

        #region Члены IDrawable

        public void Draw(GameTime gameTime)
        {
            if (vertices == null)
                return;

Давайте начнем использовать эффект (Effect):

            effect.Begin();
            effect.Parameters["worldViewProjection"].SetValue(
                             worldMatrix * viewMatrix * projectionMatrix);

Цикл проходит через каждую сторону skybox’а:

            for (int x = 0; x < 6; x++)
            {

Сделаем простой тест видимости для каждой стороны skybox’а:

                float f=0;
                switch(x)
                {
                case 0: //зад
                    f = Vector3.Dot(vCameraDirection,Vector3.Forward);
                    break;
                case 1: //перед
                    f = Vector3.Dot(vCameraDirection,Vector3.Backward);
                    break;
                case 2: //низ
                    f = Vector3.Dot(vCameraDirection,Vector3.Up);
                    break;
                case 3: //верх
                    f = Vector3.Dot(vCameraDirection,Vector3.Down);
                    break;
                case 4: //лево
                    f = Vector3.Dot(vCameraDirection,Vector3.Right);
                    break;
                case 5: //право
                    f = Vector3.Dot(vCameraDirection,Vector3.Left);
                    break;
                }

Если данная сторона видна, то устанавливаем текстуру и рендерим:

                if (f <= 0)
                {
                    IGraphicsDeviceService graphicsService = (IGraphicsDeviceService)
                        Game.Services.GetService(typeof(IGraphicsDeviceService));

                    GraphicsDevice device = graphicsService.GraphicsDevice;
                    device.VertexDeclaration = vertexDecl;
                    device.Vertices[0].SetSource(vertices, 0, 
                        vertexDecl.GetVertexStrideSize(0));

                    device.Indices = indices;

                    effect.Parameters["baseTexture"].SetValue(textures[x]);
                    effect.Techniques[0].Passes[0].Begin();
                    
                    device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 
                        0,x*4,4,x*6,2);
                    effect.Techniques[0].Passes[0].End();
                    
                }
            }
            
            effect.End();
        }

Skybox должен быть нарисован одним из первых (сделаем нулевым по порядку):

        public int DrawOrder
        {
            get { return 0; }
        }

        public event EventHandler DrawOrderChanged;

        public bool Visible
        {
            get { return true; }
        }

        public event EventHandler VisibleChanged;

        #endregion
    }

Давайте теперь взглянем на класс игры и на то, как используется компонент skybox’а:

    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        ContentManager content;


        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            content = new ContentManager(Services);
        }


        protected override void Initialize()
        {
            // TODO: Add your initialization logic here

            base.Initialize();
        }


        protected override void LoadGraphicsContent(bool loadAllContent)
        {
            if (loadAllContent)
            {

Создаем skybox:

                SkyBox sb = new SkyBox(this);

                sb.ContentManager = this.content;

                sb.CameraDirection = Vector3.Forward;
                sb.CameraPosition = Vector3.Zero;

                sb.ViewMatrix = Matrix.CreateLookAt(sb.CameraPosition, 
                    sb.CameraPosition + sb.CameraDirection, Vector3.Up);

                Viewport viewport = graphics.GraphicsDevice.Viewport;
                float aspectRatio = (float)viewport.Width / (float)viewport.Height;

                sb.ProjectionMatrix = Matrix.CreatePerspectiveFieldOfView(
                    MathHelper.PiOver4, aspectRatio, 1.0f, 10000.0f);

Игровой компонент должен вызывать метод Initialize() внутренне. Хотя я и не использую его на своем компьютере, я добавлю его здесь:

                sb.Initialize();

Не забудьте добавить skybox к списку игровых компонентов:

                this.Components.Add(sb);
            }

            // TODO: Load any ResourceManagementMode.Manual content
        }


        protected override void UnloadGraphicsContent(bool unloadAllContent)
        {
            if (unloadAllContent == true)
            {
                content.Unload();
            }
        }

        protected override void Update(GameTime gameTime)
        {
            // Allows the default game to exit on Xbox 360 and Windows
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();

Для проверки правильности работы skybox’а, я добавил кое-какой код, который будет вращать камеру в зависимости от оси левого джойстика:

            SkyBox sb = (SkyBox)this.Components[0];

            Matrix matX = Matrix.CreateRotationY(
                -GamePad.GetState(PlayerIndex.One).ThumbSticks.Left.X * .05f);
            
            sb.CameraDirection = Vector3.TransformNormal(sb.CameraDirection, matX);

            Matrix matY = Matrix.CreateFromAxisAngle(
                Vector3.Normalize(Vector3.Cross(sb.CameraDirection,Vector3.Up)),
                -GamePad.GetState(PlayerIndex.One).ThumbSticks.Left.Y * .05f);

            Vector3 vDir = Vector3.TransformNormal(sb.CameraDirection, matY);
            
            if(Math.Abs(Vector3.Dot(Vector3.Up,vDir)) > 0.9f)
                vDir = sb.CameraDirection;
            sb.CameraDirection = vDir;

            
            sb.ViewMatrix = Matrix.CreateLookAt(sb.CameraPosition, 
                sb.CameraPosition + sb.CameraDirection,Vector3.Up);

            base.Update(gameTime);
        }


        protected override void Draw(GameTime gameTime)
        {
            graphics.GraphicsDevice.Clear(Color.CornflowerBlue);

            // TODO: Add your drawing code here

            base.Draw(gameTime);
        }
    }
}

Скачать Пример простого SkyBox для XNA 1.0
Скачать Пример простого SkyBox для XNA 3.1

Оригинал статьи здесь. Перевод предоставлен сайтом Russian Ziggyware.

22 августа 2009

#SkyBox, #XNA, #основы, #уроки

2001—2017 © GameDev.ru — Разработка игр