Практикум прикладного программирования на C# в среде VS.NET 2005

Более совершенные технологии рендеринга в DirectX

Показывать лекцию целиком

Файлы к лабораторной работе Вы можете скачать здесь.

В этой работе мы рассмотрим следующее

  • Использование примитивов, отличных от треугольников
  • Использование индексных буферов для более управляемого рендеринга
  • Использование буферов глубины ( Z -буферов) для более реалистичного рендеринга
  • Для аппроксимации трехмерных объектов в Direct3D имеются следующие примитивы:

  • Microsoft.DirectX.Direct3D.PrimitiveType. PointList - отображает на экране объект в виде набора точек. Этот тип можно использовать для рисования индексных примитивов.
  • Microsoft.DirectX.Direct3D.PrimitiveType. LineList - набор линий, соединяющих пары вершин. При использовании этого примитива нужно иметь для соединения четное число вершин.
  • Microsoft.DirectX.Direct3D.PrimitiveType. LineStrip - набор ломаных прямых, соединяющих между собой последовательно все вершины объекта. Должно быть минимум две вершины для соединения с использованием этого примитива.
  • Microsoft.DirectX.Direct3D.PrimitiveType. TriangleList - Отображает набор трех вершин как отдельный изолированный треугольник. Прорисовка невидимой поверхности определяется текущим состоянием рендера отбора (device.RenderState.CullMode = Cull.None - не скрывать обратную сторону плоской фигуры)
  • Microsoft.DirectX.Direct3D.PrimitiveType. TriangleStrip - непрерывная треугольная ферма, когда каждый следующий треугольник рисуется на базе двух вершин предыдущего треугольника и одной новой вершины. По умолчанию отображаются все четные треугольники.
  • Microsoft.DirectX.Direct3D.PrimitiveType. TriangleFan - веер из треугольников, имеющих одну общую вершину.
  • При рисовании можно использовать одни и те же данные о вершинах для любого из этих типов примитивов. Приложение будет интерпретировать при каждом рисовании тот тип примитива, который мы ему сообщаем. Испытаем на практике новые типы примитивов.

    Создание заготовки приложения

  • Постройте новый пустой проект для C# с именем RenderBest
  • Добавьте через меню Project командой Add Reference, удерживая клавишу Ctrl, ссылки на библиотечные сборки
  • System.dll
  • System.Drawing.dll
  • System.Windows.Forms.dll
  • Microsoft.DirectX.dll
  • Microsoft.DirectX.Direct3D.dll
  • Через меню Project оболочки командой Add Existing Item добавьте к проекту файл Form1.cs из лабораторной работы №12
  • Отключите консольное окно, вызвав окно свойств проекта через меню Project оболочки и установив во вкладке Application свойство Output type в значение Windows Application
  • Запустите приложение, чтобы проверить, что вариант Form1 работы №12 функционирует нормально
  • На этом подготовительный этап к выполнению данной лабораторной работы завершен.

    Рендеринг различных типов примитивов

    В качестве основы для дальнейших изменений мы взяли из лабораторной работы №12 код построения одного куба без текстурирования сторон, размещенный в файле Form1.cs. Мы уже присоединили этот файл к новому проекту (оболочка физически скопировала этот файл в каталог размещения текущего проекта). Теперь мы усовершенствуем файл Form1.cs, чтобы продемонстрировать отображение одного и того же набора вершин с помощью различных примитивов. Испытания примитивов не предполагают вращения рисуемых объектов, поэтому в функции SetupCamera() необходимо удалить вращение вершин и угол поворота.

  • В функции SetupCamera() удалите две последних секции кода, отвечающих за повороты объекта и приращение угла поворота, а также объявление самой переменной-поля angle
  • private float angle = 0;// Закрытый член класса
        
            // Установка камеры в сцену
            private void SetupCamera()
            {
                //Создание перспективы 
                device.Transform.Projection = Matrix.PerspectiveFovLH(
                    (float)Math.PI / 4, // Угол зрения равен 45 градусов
                                        // Форматное соотношение сторон
                    (float)this.ClientSize.Width / (float)this.ClientSize.Height, 
                    1.0F,               // Ближний план
                    100.0F);            // Дальний план
        
                //Добавление камеры 
                device.Transform.View = Matrix.LookAtLH(
                    new Vector3(0, 0, 5.0F),    // Положение камеры
                    new Vector3(),              // Положение объекта текущее
                    new Vector3(0, 1, 0));      // Направление камеры
        
                // Освещение
                device.RenderState.Lighting = false;
        
                device.Transform.World = Matrix.RotationYawPitchRoll(
                    angle / (float)Math.PI,
                    angle / (float)Math.PI * 2.0F,
                    angle / (float)Math.PI);
                if (flagRotate)
                    angle += 0.1F;
            }
  • Удалите из класса Form1 сначала подписку обработчика на событие Click формы, затем обработчик Form1_Click(), а также описание ненужного теперь флага flagRotate
  • public Form1()
            {
                InitializeComponent();
        
                // Устраняем мерцание окна при бесконечной перерисовке
                this.SetStyle(ControlStyles.AllPaintingInWmPaint |
                    ControlStyles.Opaque, true);
                
                this.Click += new EventHandler(Form1_Click);
            }
        
            bool flagRotate = true;
        
            void Form1_Click(object sender, EventArgs e)
            {
                flagRotate = !flagRotate;
            }
  • Добавьте в класс объявление поля-ссылки NumberItems на количество вершин случайно генерируемого объекта, который мы будем отображать с применением разных примитивов
  • private Device device = null;       // Графический конвейер
            private VertexBuffer vb = null;     // Вершинный буфер
            private const int NumberItems = 12; // Количество вершин объекта

    Мы выбрали количество вершин 12 потому, что это число четное, делится на 3 (что подходит для примитивов LineList и TriangleList ) и не настолько большое, чтобы загромождать рисунок. Объявление переменной как константы поручает компилятору следить за тем, чтобы мы случайно где-нибудь в коде не попытались ее изменить.

  • Измените в функции InitializeGraphics() размерность вершинного буфера, заменив 36 на NumberItems
  • public void InitializeGraphics()
        {
          ..................................
          // Создать вершинный буфер
          vb = new VertexBuffer(typeof(CustomVertex.PositionColored),
                              NumberItems, 
                              device, 
                              Usage.Dynamic | Usage.WriteOnly,
                              CustomVertex.PositionColored.Format,
                              Pool.Default);
          ..................................
        }

    Теперь изменим алгоритм задания координат вершин и заполнения вершинного буфера в функции vb_Created(). Мы применим средства случайного формирования координат вершин и их цветов.

  • Объявите в составе класса Form1 ссылочную переменную-поле на генератор случайных чисел по равномерному закону распределения, а в конструкторе Form1() разместите код создания и инициализации этого генератора случайных чисел
  • public Form1()
            {
                InitializeComponent();
        
                // Устраняем мерцание окна при бесконечной перерисовке
                this.SetStyle(ControlStyles.AllPaintingInWmPaint |
                    ControlStyles.Opaque, true);
        
                // Инициализация генератора случайных чисел
                rand = new System.Random();
            }
        
            private Device device = null;       // Графический конвейер
            private VertexBuffer vb = null;     // Вершинный буфер
            private const int NumberItems = 12; // Количество вершин объекта
            System.Random rand;                 // Поле для генератора случайных чисел
  • Измените код создания вершин объекта в функции vb_Created()
  • void vb_Created(object sender, EventArgs e)
            {
                // Определить внутреннюю ссылку на вершинный буфер
                VertexBuffer buffer = (VertexBuffer)sender; // Явное приведение типов
        
                // Создать локальный массив ссылок для вершин объекта
                CustomVertex.PositionColored[] verts = new
                    CustomVertex.PositionColored[NumberItems];
        
                // Сгенерировать случайно координаты и цвета вершин
                double factor = 2.5d;// Весовой коэффициент
                for (int i = 0; i < NumberItems; i++)
                {
                    float xPos = (float)((rand.NextDouble() - rand.NextDouble()) * factor);
                    float yPos = (float)((rand.NextDouble() - rand.NextDouble()) * factor);
                    float zPos = (float)((rand.NextDouble() - rand.NextDouble()) * factor);
                    verts[i].SetPosition(new Vector3(xPos, yPos, zPos));
                    verts[i].Color = System.Drawing.Color.FromArgb(rand.Next(256),
                                                                   rand.Next(256),
                                                                   rand.Next(256)).ToArgb();
                }
        
                // Заполнить вершинный буфер данными треугольников
                buffer.SetData(verts, 0, LockFlags.None);
            }

    Организовать просмотр работы различных примитивов можно последовательным их перебором с достаточной задержкой. Саму задержку можно измерять как разницу во временных тактах процессора между текущим временем и временем запуска приложения и на это время отображать объект одним и тем же примитивом. После завершения очередного цикла перебора всех примитивов необходимо сгенерировать новые параметры вершинного буфера, чтобы случайно изменить рисуемый объект.

  • Добавьте в класс Form1 объявление двух новых полей, которые для читабельности удобнее расположить перед функцией OnPaint()
  • // Флаг регенерации вершин перед началом
            // каждого нового цикла обхода всех примитивов.
            // Первое заполнение вершинного буфера выполнено
            // в InitializeGraphics() при запуске приложения
            private bool needRecreate = false;
            // Захват стартового времени в тактах
            private static readonly int beginTickCount =
                System.Environment.TickCount;
      
        protected override void OnPaint(PaintEventArgs e)
        {
        ........................................
        }

    Переменная beginTickCount объявлена как статическая для того, чтобы ее данные хранились в объекте-типе, а не объекте-экземпляре, на случай, если будет создано несколько экземпляров класса Form1. Ключевое слово readonly делает ее доступной "только для чтения". В отличие от константных переменных, требующих немедленной инициализации при объявлении, переменные "только для чтения" допускают отложенную инициализацию в конструкторе класса.

  • В функции OnPaint() замените вызов библиотечной функции DrawPrimitives() в секции кода формирования сцены и цвет фона формы выделенным ниже блоком кода, чтобы окончательно функция выглядела так
  • // Флаг регенерации вершин перед началом
            // каждого нового цикла обхода всех примитивов.
            // Первое заполнение вершинного буфера выполнено
            // в InitializeGraphics() при запуске приложения
            private bool needRecreate = false;
            // Захват стартового времени в тактах
            private static readonly int beginTickCount =
                System.Environment.TickCount;
        
            protected override void OnPaint(PaintEventArgs e)
            {
                // Очистить цветом клиентскую область формы
                device.Clear(ClearFlags.Target,
                    System.Drawing.Color.White,
                    1.0F, 0);
        
                // Вызов нашей функции установки камеры
                SetupCamera();
        
                // Сформировать сцену с учетом параметров 
                // "положение-нормаль-цвет"
                device.BeginScene();
                // Установить формат обработки вершин при отображении
                device.VertexFormat = CustomVertex.PositionColored.Format;
                // Нарисовать треугольник по данным из буфера
                device.SetStreamSource(0, vb, 0);
        
                const int countTime = 5 * 1000; // Держать изображение 5 секунд 
                const int countIndex = 6;       // Количество вариантов показа
                int index = ((System.Environment.TickCount - beginTickCount) / countTime)
                    % countIndex;
                switch (index)
                {
                    case 0: // PointList
                        device.DrawPrimitives(PrimitiveType.PointList, 0, NumberItems);
                        if (needRecreate)// Выполнить только при очередном index == 0
                        {
                            vb_Created(vb, null);// Сгенерировать новый вершинный буфер
                            needRecreate = false;// Сбросить флаг регенерации вершин
                        }
                        this.Text = "Несоединенные точки вершин";
                        break;
                    case 1: // LineList
                        device.DrawPrimitives(PrimitiveType.LineList, 0, NumberItems / 2);
                        needRecreate = true;// Приготовить флаг для регенерации вершин при index == 0
                        this.Text = "Соединие пар вершин линиями";
                        break;
                    case 2: // LineStrip
                        device.DrawPrimitives(PrimitiveType.LineStrip, 0, NumberItems - 1);
                        this.Text = "Соединение вершин ломаной";
                        break;
                    case 3: // TriangleList
                        device.DrawPrimitives(PrimitiveType.TriangleList, 0, NumberItems / 3);
                        this.Text = "Отдельные треугольники";
                        break;
                    case 4: // TriangleStrip
                        device.DrawPrimitives(PrimitiveType.TriangleStrip, 0, NumberItems - 2);
                        this.Text = "Непрерывная ферма треугольников";
                        break;
                    case 5: // TriangleFan
                        device.DrawPrimitives(PrimitiveType.TriangleFan, 0, NumberItems - 2);
                        this.Text = "Треугольный веер из точки";
                        break;
                }
        
                device.EndScene();
        
                // Показать буфер кадра 
                device.Present();
        
                // Принудительно перерисовать
                this.Invalidate();
            }
  • Добавьте в конец функции SetupCamera() код изменения состояния рендера для увеличения размера отображаемых точек вершин, чтобы их можно было различить в нулевом режиме PointList
  • // Установка камеры в сцену
            private void SetupCamera()
            {
                //Создание перспективы 
                device.Transform.Projection = Matrix.PerspectiveFovLH(
                    (float)Math.PI / 4, // Угол зрения равен 45 градусов
                                        // Форматное соотношение сторон
                    (float)this.ClientSize.Width / (float)this.ClientSize.Height, 
                    1.0F,               // Ближний план
                    100.0F);            // Дальний план
        
                //Добавление камеры 
                device.Transform.View = Matrix.LookAtLH(
                    new Vector3(0, 0, 5.0F),    // Положение камеры
                    new Vector3(),              // Положение объекта текущее
                    new Vector3(0, 1, 0));      // Направление камеры
        
                // Освещение
                device.RenderState.Lighting = false;
        
                // Увеличение масштаба отображения точек вершин объекта
                device.RenderState.PointSize = 5.0f;
            }
  • Постройте приложение и убедитесь, что форма Form1 периодически генерирует новые объекты и отображает их разными примитивами
  • Использование индексных буферов

    Давайте еще раз рассмотрим геометрию куба, который мы рисовали в предыдущих лабораторных работах.

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

    Для устранения избыточности удобнее в вершинном буфере задавать только координаты уникальных точек, которыми являются вершины куба, а для каждого аппроксимирующего треугольника указывать три индекса из массива вершинного буфера. Получается, что мы храним уникальные координаты объекта "внавал" отдельно, а для треугольных примитивов указываем триадный список индексов этих координат в массиве вершинного буфера. Такой подход существенно сокращает количество хранимых данных и уменьшает необходимую память компьютера.

    Массив с индексами опорных точек объекта, записанных в вершинном буфере, называется индексным буфером. Индексы, сохраняемые в таком буфере, могут быть 16- или 32-разрядными целыми числами. Давайте используем индексный буфер для моделирования вращающегося треугольника.

  • Добавьте к текущему проекту форму Form1 из лабораторной работы №12. Для этого временно переименуйте через панель Solution Explorer файл Form1.cs текущего проекта и в меню Project (при выделенном узле проекта в панели Solution Explorer ) выполните команду Add Existing Item
  • После копирования файла Form1.cs в текущий каталог проекта присвойте ему через панель Solution Explorer имя Form2.cs, а временно переименованному файлу верните первоначальное имя Form1.cs
  • Откройте файл Form2.cs в режиме кода и замените в нем все вхождения Form1 на Form2, вызвав в редакторе кода окно замены комбинацией клавиш Ctrl-H
  • Не закрывая окна Find and Replace откройте файл Form2.designer.cs и тоже замените в нем все вхождения Form1 на Form2
  • Не закрывая окна Find and Replace установите в нем опцию Look in в значение Current Project и замените все вхождения пространства имен RenderCube на RenderBest, выполнив команду Replace All
  • Проследите за тем, чтобы в обеих файлах было использовано одно и то же пространство имен, равное имени текущего проекта ( namespace RenderBest )
  • Теперь нужно установить форму Form2 стартовой для возможности ее начального запуска. Для этого:

  • В панели Solution Explorer выделите узел проекта и щелкните на пиктограмме Properties, чтобы вызвать окно свойств проекта
  • Во вкладке Application окна свойств проекта установите опцию Startup object в значение RenderBest.Form2
  • Запустите проект на выполнение, чтобы проверить начальную работоспособность Form2
  • На этом все подготовительные работы по форме Form2 закончены и можно приступать к построению куба с использованием индексного буфера.

  • Откройте файл Form2.cs в режиме View Code и через раскрывающийся список Members правой верхней части окна редактора позиционируйтесь на функцию vb_Created() класса Form2
  • Введите в тело функции код определения вершин куба (у нашего куба 8 вершин) в соответствии с рисунком
  • void vb_Created(object sender, EventArgs e)
            {
                // Определить внутреннюю ссылку на вершинный буфер
                VertexBuffer buffer = (VertexBuffer)sender; // Явное приведение типов
        
                // Создать локальный массив структур непреобразованных координат
                CustomVertex.PositionColored[] verts = 
                    new CustomVertex.PositionColored[8]; // Задать размерность массива на 8 вершин
        
                // Задать параметры вершин куба в соответствии с рисунком
                //
                verts[0] = new CustomVertex.PositionColored(-1.0F, 1.0F, 1.0F, Color.Purple.ToArgb());
                verts[1] = new CustomVertex.PositionColored(-1.0F, -1.0F, 1.0F, Color.Red.ToArgb());
                verts[2] = new CustomVertex.PositionColored(1.0F, 1.0F, 1.0F, Color.Blue.ToArgb());
                verts[3] = new CustomVertex.PositionColored(1.0F, -1.0F, 1.0F, Color.Yellow.ToArgb());
                verts[4] = new CustomVertex.PositionColored(-1.0F, 1.0F, -1.0F, Color.Gold.ToArgb());
                verts[5] = new CustomVertex.PositionColored(1.0F, 1.0F, -1.0F, Color.Green.ToArgb());
                verts[6] = new CustomVertex.PositionColored(-1.0F, -1.0F, -1.0F, Color.Black.ToArgb());
                verts[7] = new CustomVertex.PositionColored(1.0F, -1.0F, -1.0F, Color.WhiteSmoke.ToArgb());
        
                // Заполнить вершинный буфер данными треугольников
                buffer.SetData(verts, 0, LockFlags.None);
            }

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

  • Найдите в функции InitializeGraphics() секцию создания вершинного буфера и измените его размерность с 36 на 8 (у нашего куба 8 вершин)
  • // Создать вершинный буфер
          vb = new VertexBuffer(typeof(CustomVertex.PositionColored),
                              8, 
                              device, 
                              Usage.Dynamic | Usage.WriteOnly,
                              CustomVertex.PositionColored.Format,
                              Pool.Default);
  • Добавьте в класс Form2 поле для индексного массива 16-разрядных целых чисел (тип short или Int16 )
  • // Определение индексного массива вершин треугольных примитивов
            private static readonly short[] indices = {
                2,0,1,// Задняя грань:    треугольник 2-0-1 (невидимая сторона - обход левый)
                2,1,3,// Задняя грань:    треугольник 2-1-3 (невидимая сторона - обход левый)
                5,6,4,// Передняя грань:  треугольник 5-6-4 (лицевая сторона - обход правый)
                5,7,6,// Передняя грань:  треугольник 5-7-6 (лицевая сторона - обход правый)
                0,5,4,// Верхняя грань:   треугольник 0-5-4 (лицевая сторона - обход правый)
                0,2,5,// Верхняя грань:   треугольник 0-2-5 (лицевая сторона - обход правый)
                1,6,7,// Нижняя грань:    треугольник 1-6-7 (невидимая сторона - обход левый)
                1,7,3,// Нижняя грань:    треугольник 1-7-3 (невидимая сторона - обход левый)
                0,6,1,// Левая грань:     треугольник 0-6-1 (невидимая сторона - обход левый)
                0,4,6,// Левая грань:     треугольник 0-4-6 (невидимая сторона - обход левый)
                2,3,7,// Правая грань:    треугольник 2-3-7 (лицевая сторона - обход правый)
                2,7,5 // Правая грань:    треугольник 2-7-5 (лицевая сторона - обход правый)
        };
  • В классе Form2 объявите поле типа Microsoft.DirectX.Direct3D. IndexBuffer с именем ib для хранения ссылки на индексный буфер. Ее удобнее разместить рядом со ссылкой на вершинный буфер
  • private Device device = null;
        private VertexBuffer vb = null;
        private IndexBuffer ib = null;
      
        public void InitializeGraphics()
        {
        .....................
        }

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

  • Разместите в самом конце функции InitializeGraphics() (после кода создания вершинного буфера) код создания индексного буфера и подписку на событие Created. Подписку на событие Created выполните вручную, чтобы оболочка правильно создала заготовку обработчика
  • public void InitializeGraphics()
        {
          // Создание объекта и настройка параметров представления
          // Создать объект параметров представления
          PresentParameters presentParams = new PresentParameters();
          // Установить оконный режим
          presentParams.Windowed = true;
          // Сбрасывать содержимое буфера, если он не готов к представлению
          presentParams.SwapEffect = SwapEffect.Discard;
          // Создать объект устройства и сохранить ссылку на него
          device = new Device(0, DeviceType.Hardware, this,
            CreateFlags.SoftwareVertexProcessing, presentParams);
        
          // Создать вершинный буфер
          vb = new VertexBuffer(typeof(CustomVertex.PositionColored),
                              8, 
                              device, 
                              Usage.Dynamic | Usage.WriteOnly,
                              CustomVertex.PositionColored.Format,
                              Pool.Default);
      
          // Регистрация события Created вершинного буфера
          vb.Created += new EventHandler(vb_Created);
      
          // Принудительный вызов создания треугольника 
          // и заполнения вершинного буфера при первом 
          // создании формы
          vb_Created(vb, null);
      
          // Создать индексный буфер для координат вершин куба
          ib = new IndexBuffer(typeof(short),
                             indices.Length,
                             device,
                             Usage.WriteOnly,
                             Pool.Default);
      
          // Регистрация события Created индексного буфера
          // Код написать вручную (для создания обработчика)!!!
          ib.Created += new EventHandler(ib_Created);
      
          // Принудительный вызов обработчика при первом запуске
          ib_Created(ib, null);
        }
  • Код обработчик заполнения индексного буфера после сброса будет таким
  • // Обработчик события повторного заполнения индексного буфера
            void ib_Created(object sender, EventArgs e)
            {
                IndexBuffer buffer = (IndexBuffer)sender;
                buffer.SetData(indices, 0, LockFlags.None);
            }

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

  • Установите в функции SetupCamera() новую величину приращения угла, определяющую меньшую скорость вращения куба
  • if (flagRotate)
                    angle += 0.02F;
  • В функции OnPaint() найдите секцию кода "Сформировать сцену" и внесите в нее регистрацию индексного буфера в устройстве и вызов функции DrawIndexedPrimitives(), которая при рисовании использует индексный буфер
  • // Сформировать сцену с учетом параметров 
                // "положение-нормаль-цвет"
                device.BeginScene();
                // Установить формат обработки вершин при отображении
                device.VertexFormat = CustomVertex.PositionColored.Format;
                // Нарисовать треугольник по данным из буфера
                device.SetStreamSource(0, vb, 0);
                device.Indices = ib;
                device.DrawIndexedPrimitives(
                    PrimitiveType.TriangleList, 0, 0, 8, 0, indices.Length / 3);
                device.EndScene();

    Функция рисования, которую мы использовали, имеет следующий синтаксис

    public void DrawIndexedPrimitives(
      Microsoft.DirectX.Direct3D.PrimitiveType primitiveType, // Чем рисуем
      System.Int32 baseVertex, // Смещение от начало буфера до индекса первой вершины
      System.Int32 minVertexIndex, // Минимальный индекс вершины в текущем вызове
      System.Int32 numVertices, // Число вершин, используемых в текущем вызове
      System.Int32 startIndex, // Стартовый индекс для считывания и отображения
      System.Int32 primCount) // Число отображаемых примитивов
  • Постройте приложение и получите вращающийся куб с красочными градиентами цветов на гранях
  • Здесь мы не сможем сделать каждую отдельную грань одноцветной, поскольку цвет примитива берется из параметров вершины, а у нас заданы цвета вершин куба, к которым примыкают сразу несколько примитивов разных граней. Но на данный момент так даже красивее!!!

    Создание нескольких вращающихся кубов с применением буфера глубины

    Буфер глубины ( depth buffer, Z-буфер, W-буфер ) применяется в приложениях Direct3D для хранения информации о глубине отображаемого объекта. Эта информация используется в процессе растеризации (из векторной в растровую) для определения того, насколько пикселы перекрывают друг друга. На данном этапе наше приложение не имеет буфера глубины, но в этом разделе мы его введем и используем.

  • Через контекстное меню панели Solution Explorer получите копию файла Form2 и переименуйте ее в Form3.cs
  • Через контекстное меню панели Solution Explorer откройте файл Form3.cs в режиме View Code
  • В панели Solution Explorer двойным щелчком на файле Form3.designer.cs откройте его в рабочей области редактора оболочки
  • Строго проследите, чтобы открытыми были только файлы Form3.cs Form3.designer.cs и замените в них все вхождения Form2 на Form3 через окно замены, которое можно вызвать комбинацией клавиш Ctrl-H. Предварительно настройте опции окна замены по приведенному снимку
  • Выполните команду меню Project/RenderBest Properties и установите форму Form3 стартовой
  • Запустите проект на выполнение и убедитесь, что Form3 работает в прежнем варианте Form2, т.е. целостность кода сохранилась и мы можем продолжать развитие проекта
  • Вначале вместо одного куба создадим 9 кубов и для правильного их отображения отодвинем камеру.

  • Добавьте в класс Form3 новую функцию DrawCubes() для рисования 9 кубов
  • private void DrawCubes()
            {
                //
                // Средний ряд
                //
                device.Transform.World = Matrix.RotationYawPitchRoll(
                    angle / (float)Math.PI,
                    angle / (float)Math.PI * 2.0f,
                    angle / (float)Math.PI / 4.0f) *
                    Matrix.Translation(0.0f, 0.0f, 0.0f);   // По центру
                device.DrawIndexedPrimitives(
                    PrimitiveType.TriangleList, 0, 0, 8, 0, indices.Length / 3);
        
                device.Transform.World = Matrix.RotationYawPitchRoll(
                    angle / (float)Math.PI,
                    angle / (float)Math.PI / 2.0f,
                    angle / (float)Math.PI * 4.0f) *
                    Matrix.Translation(4.0f, 0.0f, 0.0f);   // Сдвиг вправо
                device.DrawIndexedPrimitives(
                    PrimitiveType.TriangleList, 0, 0, 8, 0, indices.Length / 3);
        
                device.Transform.World = Matrix.RotationYawPitchRoll(
                    angle / (float)Math.PI,
                    angle / (float)Math.PI * 4.0f,
                    angle / (float)Math.PI / 2.0f) *
                    Matrix.Translation(-4.0f, 0.0f, 0.0f);   // Сдвиг влево
                device.DrawIndexedPrimitives(
                    PrimitiveType.TriangleList, 0, 0, 8, 0, indices.Length / 3);
        
                //
                // Нижний ряд
                //
                device.Transform.World = Matrix.RotationYawPitchRoll(
                    angle / (float)Math.PI,
                    angle / (float)Math.PI * 2.0f,
                    angle / (float)Math.PI / 4.0f) *
                    Matrix.Translation(0.0f, -4.0f, 0.0f);   // По центру
                device.DrawIndexedPrimitives(
                    PrimitiveType.TriangleList, 0, 0, 8, 0, indices.Length / 3);
        
                device.Transform.World = Matrix.RotationYawPitchRoll(
                    angle / (float)Math.PI,
                    angle / (float)Math.PI / 2.0f,
                    angle / (float)Math.PI * 4.0f) *
                    Matrix.Translation(4.0f, -4.0f, 0.0f);   // Сдвиг вправо
                device.DrawIndexedPrimitives(
                    PrimitiveType.TriangleList, 0, 0, 8, 0, indices.Length / 3);
        
                device.Transform.World = Matrix.RotationYawPitchRoll(
                    angle / (float)Math.PI,
                    angle / (float)Math.PI * 4.0f,
                    angle / (float)Math.PI / 2.0f) *
                    Matrix.Translation(-4.0f, -4.0f, 0.0f);   // Сдвиг влево
                device.DrawIndexedPrimitives(
                    PrimitiveType.TriangleList, 0, 0, 8, 0, indices.Length / 3);
        
                //
                // Верхний ряд
                //
                device.Transform.World = Matrix.RotationYawPitchRoll(
                    angle / (float)Math.PI,
                    angle / (float)Math.PI * 2.0f,
                    angle / (float)Math.PI / 4.0f) *
                    Matrix.Translation(0.0f, 4.0f, 0.0f);   // По центру
                device.DrawIndexedPrimitives(
                    PrimitiveType.TriangleList, 0, 0, 8, 0, indices.Length / 3);
        
                device.Transform.World = Matrix.RotationYawPitchRoll(
                    angle / (float)Math.PI,
                    angle / (float)Math.PI / 2.0f,
                    angle / (float)Math.PI * 4.0f) *
                    Matrix.Translation(4.0f, 4.0f, 0.0f);   // Сдвиг вправо
                device.DrawIndexedPrimitives(
                    PrimitiveType.TriangleList, 0, 0, 8, 0, indices.Length / 3);
        
                device.Transform.World = Matrix.RotationYawPitchRoll(
                    angle / (float)Math.PI,
                    angle / (float)Math.PI * 4.0f,
                    angle / (float)Math.PI / 2.0f) *
                    Matrix.Translation(-4.0f, 4.0f, 0.0f);   // Сдвиг влево
                device.DrawIndexedPrimitives(
                    PrimitiveType.TriangleList, 0, 0, 8, 0, indices.Length / 3);
        
                if (flagRotate)
                    angle += 0.02F;
            }
  • Удалите из функции SetupCamera() две последних секции, ответственные за вращение куба, поскольку эту функциональность мы включили в новую функцию DrawCubes()
  • // Установка камеры в сцену
            private void SetupCamera()
            {
                //Создание перспективы 
                device.Transform.Projection = Matrix.PerspectiveFovLH(
                    (float)Math.PI / 4, // Угол зрения равен 45 градусов
                                        // Форматное соотношение сторон
                    (float)this.ClientSize.Width / (float)this.ClientSize.Height, 
                    1.0F,               // Ближний план
                    100.0F);            // Дальний план
        
                //Добавление камеры 
                device.Transform.View = Matrix.LookAtLH(
                    new Vector3(0, 0, 5.0F),    // Положение камеры
                    new Vector3(),              // Положение объекта текущее
                    new Vector3(0, 1, 0));      // Направление камеры
        
                // Освещение
                device.RenderState.Lighting = false;
        
                device.Transform.World = Matrix.RotationYawPitchRoll(
                    angle / (float)Math.PI,
                    angle / (float)Math.PI * 2.0F,
                    angle / (float)Math.PI);
                if (flagRotate)
                    angle += 0.02F;
            }
  • Замените в секции формирования сцены функции OnPaint() вызов функции DrawIndexedPrimitives() на вызов функции DrawCubes()
  • // Сформировать сцену с учетом параметров 
                // "положение-нормаль-цвет"
                device.BeginScene();
                // Установить формат обработки вершин при отображении
                device.VertexFormat = CustomVertex.PositionColored.Format;
                // Нарисовать треугольник по данным из буфера
                device.SetStreamSource(0, vb, 0);
                device.Indices = ib;
                DrawCubes();
                device.EndScene();
  • В функции SetupCamera() отодвиньте камеру, чтобы была видна вся сцена с 9 кубами
  • // Установка камеры в сцену
            private void SetupCamera()
            {
                //Создание перспективы 
                device.Transform.Projection = Matrix.PerspectiveFovLH(
                    (float)Math.PI / 4, // Угол зрения равен 45 градусов
                                        // Форматное соотношение сторон
                    (float)this.ClientSize.Width / (float)this.ClientSize.Height, 
                    1.0F,               // Ближний план
                    100.0F);            // Дальний план
        
                //Добавление камеры 
                device.Transform.View = Matrix.LookAtLH(
                    new Vector3(0, 0, 15.0F),    // Положение камеры
                    new Vector3(),              // Положение объекта текущее
                    new Vector3(0, 1, 0));      // Направление камеры
        
                // Освещение
                device.RenderState.Lighting = false;
            }
  • Запустите приложение и получите примерно следующее
  • Настало время перейти к использованию буферов глубины объектов. Давайте добавим еще три куба к нашей сцене вращающихся кубов.

  • Добавьте в конец функции DrawCubes() код для рисования еще трех кубов
  • private void DrawCubes()
            {
                //
                // Средний ряд
                //
                //.................................................
                //
                // Дополнительные кубы
                device.Transform.World = Matrix.RotationYawPitchRoll(
                    angle / (float)Math.PI,
                    angle / (float)Math.PI * 2.0f,
                    angle / (float)Math.PI / 4.0f) *
                    Matrix.Translation(
                        0.0f,
                        (float)Math.Cos(angle),
                        (float)Math.Sin(angle));
                device.DrawIndexedPrimitives(
                    PrimitiveType.TriangleList, 0, 0, 8, 0, indices.Length / 3);
        
                device.Transform.World = Matrix.RotationYawPitchRoll(
                    angle / (float)Math.PI,
                    angle / (float)Math.PI / 2.0f,
                    angle / (float)Math.PI * 4.0f) *
                    Matrix.Translation(
                        4.0f,
                        (float)Math.Sin(angle),
                        (float)Math.Cos(angle));
                device.DrawIndexedPrimitives(
                    PrimitiveType.TriangleList, 0, 0, 8, 0, indices.Length / 3);
        
                device.Transform.World = Matrix.RotationYawPitchRoll(
                    angle / (float)Math.PI,
                    angle / (float)Math.PI * 2.0f,
                    angle / (float)Math.PI / 2.0f) *
                    Matrix.Translation(
                    -4.0f,
                    (float)Math.Cos(angle),
                    (float)Math.Sin(angle));
                device.DrawIndexedPrimitives(
                    PrimitiveType.TriangleList, 0, 0, 8, 0, indices.Length / 3);
        
                if (flagRotate)
                    angle += 0.02F;
            }
  • Запустите приложение и увидите по горизонтальному среднему ряду еще дополнительных три куба
  • Здесь мы хотим получить вращение дополнительных кубов вокруг трех центральных кубов, расположенных по горизонтали. Чтобы этого достичь, следует добавить буфер глубины в устройство device.

    Существуют разные форматы буферов глубины, но большинство современных графических плат поддерживают 16-разрядные буферы глубины. Поэтому используем именно такую глубину.

  • Добавьте в функцию InitializeGraphics() перед кодом создания устройства параметры настройки буфера глубины
  • public void InitializeGraphics()
            {
                // Создание объекта и настройка параметров представления
                // Создать объект параметров представления
                PresentParameters presentParams = new PresentParameters();
                // Установить оконный режим
                presentParams.Windowed = true;
                // Сбрасывать содержимое буфера, если он не готов к представлению
                presentParams.SwapEffect = SwapEffect.Discard;
        
                // Настройка буфера глубины для параметров устройства
                presentParams.EnableAutoDepthStencil = true;
                presentParams.AutoDepthStencilFormat =
                    Microsoft.DirectX.Direct3D.DepthFormat.D16;
        
                // Создать объект устройства и сохранить ссылку на него
                device = new Device(0, DeviceType.Hardware, this,
                    CreateFlags.SoftwareVertexProcessing, presentParams);
        
                // Создать вершинный буфер
                // .................................................
            }
  • Запускаем приложение и видим, что вместо кубов появились их осколки
  • Исчезновение сцены после добавления параметров буфера глубины произошло потому, что этот буфер не был очищен

  • Добавьте к первой секции кода функции OnPaint() дополнительный флаг очистки буфера глубины
  • protected override void OnPaint(PaintEventArgs e)
        {
                // Очистить цветом клиентскую область формы
                device.Clear(ClearFlags.Target 
                    | ClearFlags.ZBuffer,
                    System.Drawing.Color.CornflowerBlue,
                    1.0F, 0);
          ..........................................
        }
  • Запустите приложение и мы увидим фантастическую картину, иллюстрирующую использование буфера глубины
  • Краткие выводы

    В данной лабораторной работе мы познакомились с более совершенными методами рендеринга, а именно

  • Посмотрели на работу различных типов примитивов, приминяемых для рендеринга объектов.
  • Попробовали использовать индексные буферы, позволяющие существенно сократить объем данных за счет устранения дублирования описания параметров вершин.
  • Применили буфер глубины устройства для более реалистичного (или фантастического) отображения объектов.
  • Вернуться к учебному плану