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

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

Также нам понадобится простой шейдер файла эффекта (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, основы, уроки