Создание стереоизображений (анаглифов) при помощи XNA Game Studio. Часть 1

В этой статье мы рассмотрим простой алгоритм создания анаглифов на XNA Framework. Для начала это будут черно-белые изображения, а потом мы попробуем создать алгоритм для цветных изображений.

На самом деле я не смог найти достаточно информации о создании анаглифов в интернете, так что алгоритм может быть не совсем правильным.

Сначала взглянем на описание анаглифов, которое дает нам википедия:

Ана́глиф (от греч.
anáglyphos — рельефный) — метод получения стереоэффекта для стереопары обычных изображений при помощи цветового кодирования изображений, предназначенных для левого и правого глаза. Для получения эффекта необходимо использовать специальные (анаглифи́ческие) очки, в которых вместо диоптрийных стёкол вставлены специальные светофильтры, как правило, для левого глаза — красный, для правого — голубой или синий. Стереоизображение представляет собой комбинацию изображений стереопары, в которой в красном канале изображена картина для левого глаза (правый её не видит из-за светофильтра), a в синем (или синем и зелёном — для голубого светофильтра) — для правого. То есть каждый глаз воспринимает изображение, окрашенное в противоположный цвет.


Пример анаглифического изображения (красный — левый, голубой — правый фильтр) (
Для правильного просмотра этого изображения требуются 3D-очки.)

Основным недостатком метода анаглифов является неполная цветопередача. Формируемое объёмное изображение благодаря эффекту бинокулярного смешения цветов воспринимается однотонным или (при определённом соотношении яркостей) ахроматическим. Адаптация наблюдателя к специфическим условиям восприятия происходит достаточно быстро. Однако после не столь долгого (около 15 мин) пребывания в анаглифических очках у наблюдателя на продолжительное (порядка получаса) время снижается цветовая чувствительность и возникает ощущение дискомфорта от восприятия обычного (не красно-голубого) мира.

Создание анаглифов

В старых методах с использованием съёмочных светофильтров и плёнки изображения для правого и левого глаза печатались или воспроизводились в качестве проекции как единое изображение, или же два изображения через специальные светофильтры попадали на одну плёнку. Одно пропускалось через красный фильтр, а другое через фильтр контрастирующего цвета — синего, зелёного или бирюзового. Как подчёркивается ниже, сейчас возможно, преимущественно с использованием компьютерных программ, имитировать использование цветовых фильтров. При этом за основу можно брать как пару монохромных, так и цветных изображений. С 70-х годов Стивен Гибсон предложил свою патентованную систему «Дип вижн» (Deep Vision), использующую вместо зелёных бирюзовые фильтры. Это позволяет достичь более натуральных цветов, так как цвета покрывают почти весь видимый спектр. Сильнее всего искажается красный цвет: выглядит почти чёрным, меньше всего зелёный. Очки «Дип вижн» имеют красный светофильтр справа и бирюзовый слева или red/cyan (чтобы обойти патент Гибсона, некоторые компании выпускают красно-бирюзовые очки со светофильтрами расположенными в обратном порядке)

Основные средства для создания анаглифов есть в популярном профессиональном программном обеспечении, например в Adobe Photoshop, StereoPhoto Maker, Blender. Инструкций по созданию анаглифов в официальных руководствах пользователя не приводится, но их можно найти на различных сайтах, предлагающих бесплатные инструкции по 3D-графике для программы Adobe Photoshop. Суть заключена в том, что цвет для каждого глаза ставится в RGB и если очки red/cyan, то розовая картинка раздвоится на два глаза, так как в RGB розовый — это red+blue, а cyan=blue+green. Также существуют простые дешёвые программы для создания анаглифов. Например, бесплатный StereoPhoto Maker может создавать качественный анаглиф (и не только анаглиф) для любых типов очков автоматически. В настоящее время при использовании простых подходов из изображения для левого глаза отфильтровывается синий и зелёный цвета.

У меня как раз есть простенькие картонные анаглифные очки, так что можно приступать к разработке приложения.

Сначала нам нужно просто изобразить трехмерную сцену на экране.

Для этого мне понадобится библиотека графических примитивов и вспомогательный класс, для облегчения дальнейшей работы.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Primitives3D;
using Microsoft.Xna.Framework;

namespace StereoGame
{
class GameObject
{
GeometricPrimitive primitive;
Vector3 position;
Color color;

public GameObject(GeometricPrimitive primitive, Vector3 position, Color color)
{
this.primitive = primitive;
this.position = position;
this.color = color;
}

public void Draw(Matrix world, Matrix view, Matrix projection)
{
world *= Matrix.CreateTranslation(position);
primitive.Draw(world, view, projection, color);
}

}
}

В Game1.cs напишем следующее:

 
        List<GameObject> primitives = new List<GameObject>();

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";

            graphics.PreferredBackBufferWidth = 640;
            graphics.PreferredBackBufferHeight = 480;
        }

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            // TODO: use this.Content to load your game content here

            primitives.Add(new GameObject(new TeapotPrimitive(GraphicsDevice, 1, 30), new Vector3(0,-0.4f,-0.3f), Color.Blue));
            primitives.Add(new GameObject(new TeapotPrimitive(GraphicsDevice, 1, 30), new Vector3(0.7f, +0.8f, -1.7f), Color.Yellow));
            primitives.Add(new GameObject(new CubePrimitive(GraphicsDevice, 1), new Vector3(0, +0.4f, -2f), Color.Tomato));
            primitives.Add(new GameObject(new CylinderPrimitive(GraphicsDevice, 1, 1, 35), new Vector3(-0.6f, +0.5f, -1.3f), Color.Teal));

        }

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {

            // TODO: Add your drawing code here
            Matrix world = Matrix.CreateRotationY((float)gameTime.TotalGameTime.TotalSeconds);

            Vector3 cameraPosition = new Vector3(0,0,2);

            Matrix view = Matrix.CreateLookAt(cameraPosition, Vector3.Zero, Vector3.Up);
            Matrix projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(45), GraphicsDevice.Viewport.AspectRatio, 0.1f, 10f);

            GraphicsDevice.Clear(Color.White);

            view = Matrix.CreateLookAt(cameraPosition + Vector3.Left*0.05f, Vector3.Zero, Vector3.Up);

            foreach (var item in primitives)
            {
                item.Draw(world, view, projection);
            }

            base.Draw(gameTime);
        }

Получится вот такая простенькая сцена:

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

Соответственно нам нужно будет еще две поверхности рисования, для каждой и которых мы будем при рендеринге сдвигать камеру влево или вправо.

 
        RenderTarget2D targetLeft;
        RenderTarget2D targetRight;

        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            // TODO: Add your initialization logic here
            targetLeft = new RenderTarget2D(GraphicsDevice, graphics.PreferredBackBufferWidth, graphics.PreferredBackBufferHeight);
            targetRight = new RenderTarget2D(GraphicsDevice, graphics.PreferredBackBufferWidth, graphics.PreferredBackBufferHeight);

            base.Initialize();
        }

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
           // GraphicsDevice.Clear(Color.Black);

            // TODO: Add your drawing code here
            Matrix world = Matrix.CreateRotationY((float)gameTime.TotalGameTime.TotalSeconds);

            Vector3 cameraPosition = new Vector3(0,0,2);

            Matrix view = Matrix.CreateLookAt(cameraPosition, Vector3.Zero, Vector3.Up);
            Matrix projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(45), GraphicsDevice.Viewport.AspectRatio, 0.1f, 10f);

            GraphicsDevice.SetRenderTarget(targetLeft);
            GraphicsDevice.Clear(Color.White);

            view = Matrix.CreateLookAt(cameraPosition + Vector3.Left*0.05f, Vector3.Zero, Vector3.Up);

            foreach (var item in primitives)
            {
                item.Draw(world, view, projection);
            }

            GraphicsDevice.SetRenderTarget(targetRight);
            GraphicsDevice.Clear(Color.White);

            view = Matrix.CreateLookAt(cameraPosition + Vector3.Right * 0.05f, Vector3.Zero, Vector3.Up);

            foreach (var item in primitives)
            {
                item.Draw(world, view, projection);
            }

            GraphicsDevice.SetRenderTarget(null);
            GraphicsDevice.Clear(Color.Black);

            spriteBatch.Begin();
            spriteBatch.Draw(targetLeft, new Rectangle(0, 0, 640, 480), Color.White);
            spriteBatch.End();

            base.Draw(gameTime);
        }

Обратите внимание на следующие строки:

            view = Matrix.CreateLookAt(cameraPosition + Vector3.Left*0.05f, Vector3.Zero, Vector3.Up);

            view = Matrix.CreateLookAt(cameraPosition + Vector3.Right * 0.05f, Vector3.Zero, Vector3.Up);

В них происходит сдвиг камеры влево и вправо (при помощи вектором Vector3.Left, Vector3.Right) некоторое небольшое значение.

Заодно на этом примере мы посмотрим проблемы, которые могут возникать при работе с поверхностями рисования.

На картинке мы видим, что объекты выглядят крайне не реалистично. Дело в том, что отключен буфер глубины. Обычно такое происходит при вызове метода SpriteBatch.Begin так как он устанавливает собственные параметры GraphicDevice. (Правда в данном случае дело не в том, но об этом дальше)

В более ранних версиях XNA Framework существовало множество различных параметров GraphicsDevice. Сейчас все значительно проще и для того, чтобы полностью сбросить все параметры графического устройства можно обойтись всего 4 строчками кода.

 
        private void RestoreGraphics()
        {
            GraphicsDevice.BlendState = BlendState.Opaque;
            GraphicsDevice.DepthStencilState = DepthStencilState.Default;
            GraphicsDevice.RasterizerState = RasterizerState.CullCounterClockwise;
            GraphicsDevice.SamplerStates[0] = SamplerState.LinearWrap;
        }

Как я уже писал выше, сейчас дело, на самом деле, не в том. Просто наши поверхности рисования не поддерживают буфер глубины. Однако же стоит всегда устанавливать нужные настройки графического устройства перед тем, как рисовать кто-либо.

Правильные настройки плоскостей рисования выглядят вот так:

 
        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            // TODO: Add your initialization logic here
            targetLeft = new RenderTarget2D(GraphicsDevice, graphics.PreferredBackBufferWidth, graphics.PreferredBackBufferHeight, false, SurfaceFormat.Color, DepthFormat.Depth16);             targetRight = new RenderTarget2D(GraphicsDevice, graphics.PreferredBackBufferWidth, graphics.PreferredBackBufferHeight, false, SurfaceFormat.Color, DepthFormat.Depth16);

            base.Initialize();
        }

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
           // GraphicsDevice.Clear(Color.Black);

            // TODO: Add your drawing code here
            Matrix world = Matrix.CreateRotationY((float)gameTime.TotalGameTime.TotalSeconds);

            Vector3 cameraPosition = new Vector3(0,0,2);

            Matrix view = Matrix.CreateLookAt(cameraPosition, Vector3.Zero, Vector3.Up);
            Matrix projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(45), GraphicsDevice.Viewport.AspectRatio, 0.1f, 10f);

            GraphicsDevice.SetRenderTarget(targetLeft);
            GraphicsDevice.Clear(Color.White);

            view = Matrix.CreateLookAt(cameraPosition + Vector3.Left*0.05f, Vector3.Zero, Vector3.Up);

            foreach (var item in primitives)
            {
                item.Draw(world, view, projection);
            }

            GraphicsDevice.SetRenderTarget(targetRight);
            GraphicsDevice.Clear(Color.White);

            view = Matrix.CreateLookAt(cameraPosition + Vector3.Right * 0.05f, Vector3.Zero, Vector3.Up);

            foreach (var item in primitives)
            {
                item.Draw(world, view, projection);
            }

            GraphicsDevice.SetRenderTarget(null);
            GraphicsDevice.Clear(Color.Black);

            spriteBatch.Begin();
            spriteBatch.Draw(targetLeft, new Rectangle(0, 0, 640, 480), Color.White);
            spriteBatch.End();

            RestoreGraphics();

            base.Draw(gameTime);
        }

Теперь все будет правильно:

Реклама
Запись опубликована в рубрике Компьютерная графика с метками , . Добавьте в закладки постоянную ссылку.

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s