Основы трехмерного моделирования

172

Как вы уже знаете, платформа Silverlight появилась как подмножество средств, классов и функций, разработанных для библиотеки WPF. До первого релиза она даже носила рабочее название WPF/E (WPF for Everyone — WPF для всех).

Однако через несколько лет после версии Silverlight 1.0 эта платформа отделилась от WPF и пошла собственным путем. Как и следовало ожидать, она не отказалась от средств, унаследованных от WPF, включая многие элементы управления, команды, средства стилизации, связывания данных, доступа к файлам .NET и др. Некоторые средства сначала появились в Silverlight, и только потом в WPF. Это такие средства, как визуальные состояния и смягчение анимации. Некоторые новые средства Silverlight не имеют непосредственных аналогов в WPF, включая поддержку веб-камеры, глубокое зуммирование и сводные таблицы. Однако наиболее важное направление, в котором технология Silverlight далеко ушла от WPF — средства трехмерного моделирования.

В WPF есть собственные средства визуализации трехмерных моделей, основанные на элементе управления Viewport3D. С их помощью можно создавать трехмерные рисунки в формате XAML (обычно с использованием трехмерных иллюстрационных программ) или генерировать трехмерные модели в коде. В Silverlight элемента Viewport3D нет, вместо него используется совершенно другой элемент DrawingSurface, основанный на инфраструктуре Microsoft XNA (эта же инфраструктура используется для создания компьютерных игр Xbox).

Создавать трехмерные приложения в WPF с помощью элемента Viewport3D довольно тяжело. Впрочем, создавать их в Silverlight с помощью элемента DrawingSurface еще тяжелее (хотя и ненамного). Для визуализации даже простой трехмерной сцены необходимо позаботиться о низкоуровневых вещах, таких как пиксельные шейдеры.

Но если вы согласитесь потратить время на изучение необходимых концепций, то сможете создавать приложения совершенно нового типа. Благодаря встроенным в Silverltight средствам аппаратного ускорения вы будете вознаграждены возможностью качественно визуализировать практически любые сцены. Если же у вас к тому же есть опыт работы с XNA в Xbox или Windows Phone, вы обнаружите, что ваши знания будут очень полезными при создании веб-приложений на платформе Silverlight.

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

Помня об этих фактах, мы можем приступить к созданию нашей первой трехмерной сцены.

Добавление ссылок на сборки

Поддержка трехмерных средств платформой Silverlight выполняется с помощью ряда перечисленных ниже сборок. В проект необходимо добавить ссылки на все эти сборки:

System.Windows.Xna.dll

Определение базовых типов, таких как элемент управления DrawingSurface и управляющий им класс GraphicsDeviceManager.

Microsoft.Xna.Framework.dll

Версии XNA структур Color и Rectangle. Поддержка платформой XNA звуковых средств.

Microsoft.Xna.Framework.Graphics.dll

Базовые типы рисования, включая GraphicsDevice, посредством которого выполняются все операции трехмерного рисования на поверхности DrawingSurface.

Microsoft.Xna.Framework.Graphics.Extensions.dll

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

Microsoft.Xna.Framework.Graphics.Shaders.dll

В эту сборку включены классы PixelShader и VertexShader, которые позволяют загружать собственные шейдеры, полученные путем компиляции кода HLSL (High Level Shader Language — высокоуровневый шейдерный язык).

Microsoft.Xna.Framework.Math.dll

Эта сборка предоставляет базовые методы трехмерных вычислений точек (Vector3) и матриц (Matrix).

Включение трехмерных средств в проекте Silverlight

Для поддержки трехмерного моделирования платформой Silverlight необходимо аппаратное ускорение. Без него трехмерное моделирование невозможно. Даже хуже того: если не включить аппаратное ускорение, Silverlight не сообщит об этом, а элемент DrawingSurface останется пустым.

К счастью, включить аппаратное ускорение несложно. Для этого нужно всего лишь добавить параметр enableGPUAcceleration в раздел надстройки Silverlight на тестовой странице. Это можно сделать, например, следующим образом:

<form id="form1" runat="server" style="height:100%">
    <div id="silverlightControlHost">
        <object data="data:application/x-silverlight-2," type="application/x-silverlight-2" width="100%" height="100%">
          <param name="enableGPUAcceleration" value="true" />
		  ...
		  <a href="http://go.microsoft.com/fwlink/?LinkID=149156&v=5.0.61118.0" style="text-decoration:none">
 			  <img src="http://go.microsoft.com/fwlink/?LinkId=161376" alt="Get Microsoft Silverlight" style="border-style:none"/>
		  </a>
	    </object><iframe id="_sl_historyFrame" style="visibility:hidden;height:0px;width:0px;border:0px"></iframe></div>
</form>

Важно учитывать следующее: по умолчанию использование надстройкой Silverlight аппаратного ускорения заблокировано. Это сделано для того, чтобы предотвратить некоторые типы хакерских DOS-атак (реализуемых через плохое программное обеспечение, перегружающее ресурсы видеокарты путем выполнения необоснованно громоздких шейдеров и отображения сложной геометрии). Некоторые другие трехмерные технологии (например, WebGL) тоже подвержены таким атакам.

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

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

Для обнаружения этой проблемы вы должны проверить значения свойств RenderMode и RenderModeReason объекта GraphicsDeviceManager при загрузке приложения. Приведенный ниже код выявляет наличие данной и ряда других проблем:

// Файл App.xaml.cs
using System.Windows.Graphics;

...

        private void Application_Startup(object sender, StartupEventArgs e)
        {
            GraphicsDeviceManager gdm = GraphicsDeviceManager.Current;
            if (gdm.RenderMode == RenderMode.Unavailable)
            {
                switch (gdm.RenderModeReason)
                {
                    case RenderModeReason.SecurityBlocked:
                        MessageBox.Show("Поддержка трехмерных средств" + 
                            " отключена из соображений безопасности. Вы можете" + 
                            " включить ее, выполнив следующие действия...");
                        break;
                    case RenderModeReason.GPUAccelerationDisabled:
                        MessageBox.Show("Ошибка разработчика! Используйте" + 
                            " параметр enableGPUAcceleration на тестовой" +
                            " странице для включения трехмерных средств");
                        break;
                    case RenderModeReason.Not3DCapable:
                        MessageBox.Show(
                          "Компьютер не поддерживает трехмерные средства.");
                        break;
                    case RenderModeReason.TemporarilyUnavailable:
                        MessageBox.Show(
                          "Проблема с доступом к драйверу видеокарты.");
                        break;
                }
            }
            else
            {
                // Нормальное выполнение приложения
                this.RootVisual = new MainPage();
            }
        }

Конечно, есть и лучшие способы решения этой проблемы, чем вывод диалоговых окон. Например, обнаружив, что свойство RenderModeReason равно SecurityBlocked, можно присвоить свойству приложения RootVisual объект окна с подробными инструкциями установки и предложить пользователю обновить страницу после изменения конфигурации.

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

Базовые средства трехмерной графики

В Silverlight все трехмерное рисование выполняется в элементе управления DrawingSurface, расположенном в пространстве имен System.Windows.Controls. Объект DrawingSurface — это обычный элемент управления Silverlight, поэтому для его использования не нужно добавлять ссылки на сборку или импортировать пространство имен. Чаще всего элемент DrawingSurface располагают в контейнере с черным фоном, чтобы поверх него можно было рисовать содержимое:

<Grid x:Name="LayoutRoot" Background="Black">
        <DrawingSurface Width="400" Height="300" />
</Grid>

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

Чтобы содержимое появилось в элементе управления DrawingSurface, нужно обработать его событие Draw:

<DrawingSurface Width="400" Height="300" Draw="DrawingSurface_Draw_1" />

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

Трехмерная система координат

Чтобы расположить в пространстве треугольник, нужно уметь ориентироваться в системе координат, используемой в Silverlight. В трехмерной модели Silverlight приняты соглашения Microsoft XNA и применяется правосторонняя система координат.

Это означает, что ось Z направлена перпендикулярно экрану в сторону наблюдателя. Когда точка движется вдоль оси Z в сторону наблюдателя, значение координаты Z увеличивается. Если же точка движется от наблюдателя, значение координаты Z уменьшается. Направление осей X и Y показано на рисунке:

Трехмерная система координат в Silverlight

Заполнение буфера вершин

Если вы когда-либо имели дело с задачами трехмерной графики (или хотя бы читали о технологиях, используемых в современных видеокартах), то знаете, что компьютер создает трехмерные сцены из треугольников. Это объясняется тем, что треугольник — простейший и, следовательно, наиболее фундаментальный элемент поверхности. Каждый треугольник определяется всего лишь тремя точками — вершинами в углах треугольника.

Дуги и другие изогнутые поверхности намного сложнее. Треугольники являются фундаментальными элементами поверхности по той причине, что любую фигуру с прямыми гранями (квадрат, прямоугольник, многоугольник и др.) можно расчленить на набор треугольников. Хотим мы того или нет, данная абстракция используется в программном обеспечении любого современного графического оборудования.

Вершина — это объект данных, представляющий точку в трехмерном пространстве. Большинство систем трехмерной графики функционируют путем конструирования сложных поверхностей и объектов на основе многих тысяч крошечных треугольников. Каждый треугольник определен тремя точками, причем каждая точка является вершиной треугольника.

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

На рисунке ниже показана сфера, образованная из треугольников. Чем больше треугольников, тем более гладкой будет результирующая поверхность. В правой части сфера образована из прямоугольников, но даже в этом случае каждый прямоугольник в трехмерной модели Silverllght интерпретируется как два треугольника, границы между которыми не показаны:

Составление трехмерного объекта из треугольников

К сожалению, в Silverlight для создания трехмерных объектов нет высокоуровневых абстракций, таких как сферы, кубы, сети и т.п. Однако на рынке уже начинают появляться наборы инструментов независимых поставщиков, основанные на трехмерной модели Silverlight и предоставляющие высокоуровневые компоненты моделей.

Чтобы нарисовать треугольник, нужно определить координаты каждой вершины. Буфер вершин — это объект, содержащий набор вершин всех треугольников, составляющих данную фигуру.

Предположим, нужно нарисовать простой треугольник. В первую очередь необходимо импортировать следующие пространства имен:

using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;
using System.Windows.Graphics;

Затем нужно задать три вершины треугольника в объекте типа VertexBuffer. Для оптимизации производительности процесса рисования буферы вершин модели объекта и другие компоненты трехмерной графики необходимо всегда определять за пределами обработчика события DrawingSurface.Draw, который фактически рисует их.

Поэтому нужно определить VertexBuffer на уровне класса внутри пользовательского элемента управления. Одновременно нужно создать экземпляр класса BasicEffect, который будет управлять различными аспектами процесса рисования:

// Хранение вершин треугольников
VertexBuffer vertexBuffer;

// Параметры процесса рисования
BasicEffect effect;

Рисовать объекты можно при загрузке страницы. Рекомендуется поместить код подготовки процесса рисования в отдельный метод, например в приведенный ниже метод PrepareDrawing:

private void PrepareDrawing()
{
   ...
}

Затем этот метод можно будет вызвать в конструкторе пользовательское элемента управления:

public MainPage()
{
     InitializeComponent();
     PrepareDrawing();
}

Выполнение инициализации в обработчике события Loaded — серьезная ошибка. Объект DrawingSurface в некоторых случаях генерирует событие Draw перед завершением события Loaded. Кроме того, событие Draw генерируется в другом потоке (он называется потоком визуализации). В результате код рисования начнет выполняться до создания и заполнения буфера вершин, и надстройка сгенерирует исключение.

Далее рассматривается код. который нужно поместить в метод PrepareDrawing(). Прежде всего нужно создать вершины фигуры. Каждая вершина представлена экземпляром класса Vector3. Ниже приведен код, создающий три вершины треугольника:

// Определение вершин треугольника
Vector3 topCenter = new Vector3(0, 1, 0);
Vector3 bottomLeft = new Vector3(-1, 0, 0);
Vector3 bottomRight = new Vector3(1, 0, 0);

Каждой вершине нужно присвоить определенный цвет. Когда пиксельный шейдер (например, объект PixelShader) рисует треугольник, он закрашивает отдельно каждый пиксель путем интерполяции цветов трех окружающих вершин. Если всем вершинам присвоен один и тот же цвет, шейдер заливает треугольник этим цветом. Если же вершинам присвоены разные цвета, шейдер смешивает их и создает градиент:

// Необходимо задать значения красного, зеленого и синего цветов
// и (необязательно) значение alpha. 
// Белый цвет
Color color1 = new Color(255, 255, 255);

// Красный цвет
Color color2 = new Color(255, 0, 0);

// Зеленый цвет
Color color3 = new Color(0, 255, 0);

Чтобы избежать конфликтов пространств имен, убедитесь в том, что не импортируется пространство имен System.Windows.Media, в котором есть собственная версия структуры Color, конфликтующая с классами трехмерной модели.

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

// Объединение информации о вершинах и цветах
VertexPositionColor[] vertices = new VertexPositionColor[3];
vertices[0] = new VertexPositionColor(bottomLeft, color1);
vertices[1] = new VertexPositionColor(topCenter, color3);
vertices[2] = new VertexPositionColor(bottomRight, color2);

Следующий этап — создание объекта VertexBuffer (буфер вершин). Для этого сначала нужно получить ссылку на текущее устройство GraphicsDevice, предоставляемую диспетчером GraphicsDeviceManager:

GraphicsDevice device = GraphicsDeviceManager.Current.GraphicsDevice;

Затем нужно создать и заполнить буфер вершин:

vertexBuffer = new VertexBuffer(device, typeof(VertexPositionColor), 
    vertices.Length, BufferUsage.WriteOnly);
vertexBuffer.SetData(0, vertices, 0, vertices.Length, 0);

Код получается довольно сложным по той причине, что структура буфера вершин может быть разной. В данном примере буфер вершин заполнен объектами VertexPositionColor, что указано во втором аргументе, передаваемом конструктору VertexBuffer. Однако буфер вершин может не содержать информацию о цветах или содержать информацию о текстуре. Фактически буфер вершин работает так же, как низкоуровневый байтовый массив, поэтому вы должны явно скопировать в него диапазон данных с помощью метода VertexBuffer.SetData().

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

Позиционирование камеры

Перед отображением трехмерной сцены на экране нужно расположить камеру в правильной позиции, а также сориентировать ее в нужном направлении.

Для позиционирования камеры нужно задать три параметра. Первый — точка в трехмерном пространстве, в которой должна располагаться камера. Второй — точка, на которую направлена камера. Эти два параметра фактически определяют, как далеко камера находится от содержимого (и, следовательно, насколько большой будет выглядеть сцена на поверхности элемента управления DrawingSurface). Значения параметров позиционирования определяют также, будет ли видна сцена вообще (может, камера направлена в другую сторону):

Позиционирование камеры

Но двух параметров недостаточно. Нужно также указать камере, какое направление считается направлением вверх. Если жестко зафиксировать только позицию камеры и направление взгляда, ничто не помешает камере вращаться вокруг оси взгляда; два первых параметра при этом не нарушаются. На рисунке выше камера ориентирована в направлении (0, 1, 0). Это естественное направление вверх. Однако можно наклонить камеру:

Задание направления вверх

Для установки камеры нужно создать матрицу, содержащую все три указанных выше параметра. В Silverlight включено достаточно средств, позволяющих создавать матрицы и работать с ними, не зная матричной алгебры.

Для позиционирования камеры используется удобный метод Matrix.CreateLookAt(), принимающий позицию камеры, направление взгляда и направление вверх. Каждый из этих параметров представлен отдельным объектом Vector3. Ниже приведен вызов метода CreateLookAt(), размещающий камеру на оси Z:

Matrix view = Matrix.CreateLookAt(new Vector3(0, 0, 5), 
   Vector3.Zero, Vector3.Up);

Класс Matrix упрощает разработку трехмерной модели благодаря использованию специальных методов. Метод CreatePerspectiveFieldOfView() принимает часть пространства, которую видит камера и задается четырьмя числами с плавающей точкой. Видимая часть сцены называется усеченной пирамидой вида (viewing frustrum).

Параметры метода CreatePerspectiveFieldOfView()
Параметр Описание
fieldOfView Угол в радианах (слева направо), в котором камера видит сцену; обычно этот параметр равен 45 или π/4. Объект MathHelper предоставляет это значение посредством статического поля MathHelper.
aspectRation Отношение размера по горизонтали к размеру по вертикали. Обычно значение этого параметра определяется шириной элемента управления DrawingSurface (если она фиксированная) или шириной контейнера либо страницы (если элементу DrawingSurface разрешено расширяться до размеров доступного пространства).
nearPlaneDistance Расстояние до передней плоскости отсечения. Если объект ближе к камере, чем задано данным параметром, он не обрабатывается видеокартой и не отображается на экране.
farPlaneDistqnce Расстояние до задней плоскости отсечения. Если объект расположен дальше, он не обрабатывается и не отображается.

Приведенный ниже код создает проекцию для примера с одним треугольником. Он отображает все, что находится на расстоянии от 1 до 10 единиц от камеры. Значение aspectRatio равно 1,33. Оно вычислено путем деления ширины DrawingSurface (400) на высоту (300):

Matrix projection = Matrix.CreatePerspectiveFieldOfView(
      MathHelper.PiOver4, 1.33f, 1, 10);

Немного сбивает с толку то, что метод CreatePerspectiveFieldOfView() возвращает перспективную проекцию, которая не является окончательной проекцией вида, необходимой для отображения трехмерной сцены. Чтобы вычислить окончательную проекцию вида, необходим еще один дополнительный этап. Метод Matrix.CreatePerspectiveFieldOfView() возвращает матрицу проекции. Ее нужно умножить на матрицу вида:

Matrix viewProjection = view * projection;

Теперь у нас есть матрицы вида и проекции и мы готовы к созданию и конфигурированию объекта BasicEffect. Это окончательный этап инициализации, выполняемый в методе PrepareDrawing().

Конфигурирование объекта BasicEffect

Чтобы понять объект BasicEffect, нужно знать немного больше о трехмерной модели Silverlight. Технически в Silverlight все операции отображения осуществляются с помощью шейдеров — фрагментов кода, выполняемых непосредственно процессором видеокарты. Есть два типа шейдеров:

Вершинный шейдер

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

Пиксельный шейдер

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

Вы можете создать собственный шейдер и скомпилировать его с помощью утилиты fxc.exe из пакета DirectX SDK. Синтаксис шейдеров определен в языке HLSL, аналогичном языку C#. Однако на любом языке написание низкоуровневых алгоритмов работы с пикселями — тяжелая и скучная работа.

К счастью, Silverlight наследует высокоуровневую модель эффектов платформы Microsoft XNA. Приложение использует объекты эффектов, содержащие собственные встроенные вершинные и пиксельные шейдеры. Многие объекты эффектов предоставляют довольно сложные алгоритмы визуализации, поддерживающие текстуры, освещение, материалы, туман и т.п.

Все классы эффектов наследуют класс Effect, определенный в пространстве имен Microsoft.Xna.Framework.Graphics. В Silverlight включен самый необходимый класс эффектов BasicEffect. Несмотря на свое название, он довольно практичен и предоставляет на удивление много средств, включая встроенный алгоритм базовых шейдерных операций, отображения текстур и освещения.

Чтобы подготовить объект BasicEffect для использования в текущем примере, нужно установить вид и проекцию с помощью матриц, созданных в предыдущем разделе:

effect = new BasicEffect(device);
effect.View = view;
effect.Projection = viewProjection;

Необходимо также установить мировую матрицу, которая, кроме прочего, позволяет перемещать объект в новую позицию:

effect.World = Matrix.Identity;

Мировая матрица удобна для позиционирования многих объектов сцены, когда в них используются разные объекты BasicEffect. Она позволяет перемещать каждый объект из исходного места, в котором он определен (обычно относительно точки (0,0,0)), в другую позицию, задаваемую конфигурацией сцены. Концептуально на данном этапе содержимое перемещается из исходного объектного пространства в мировое пространство окончательной сцены. Технически перемещение выполняется путем умножения исходной позиции на мировую матрицу.

Поскольку в данном примере имеется только один объект, перемещать его куда-либо нет необходимости. Поэтому в данном примере используется единичная матрица (identity matrix), т.е. матрица, при умножении на которую ничего не изменяется и объект остается на прежнем месте. Далее вы узнаете об использовании мировой матрицы для перемещения и поворота трехмерных объектов.

Необходимо также установить свойство VertexColorEnabled объекта BasicEffect. Значение true означает, что будет применен встроенный пиксельный шейдер, который создает цветовой градиент треугольника:

effect.VertexColorEnabled = true;

Если не выполнить данный шаг и не предоставить какой-нибудь другой пиксельный шейдер, объект BasicEffect не нарисует ничего.

Этим завершается код инициализации в методе PrepareDrawing(). Теперь все готово для написания кода рисования, в котором используются все компоненты отображения трехмерного содержимого.

Прорисовка сцены

Элемент управления DrawingSurface генерирует событие Draw в момент, когда код приступает к визуализации сцены. В обработчике в первую очередь необходимо получить ссылку на текущий объект графического устройства GraphicsDevice и очистить его содержимое методом Clear():

private void DrawingSurface_Draw_1(object sender, DrawEventArgs e)
{
       GraphicsDevice device = GraphicsDeviceManager.Current.GraphicsDevice;
       device.Clear(new Color(0, 0, 0));
       ...
}

Каждая операция рисования должна начинаться с вызова метода Clear(). Если забыть очистить поверхность в первый раз, объект DrawingSurface напомнит об этом, отобразив красный фон.

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

device.SetVertexBuffer(vertexBuffer);

И наконец, окончательный этап - прорисовка:

// Проход по всем объектам EffectPass
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
         pass.Apply();
         device.DrawPrimitives(PrimitiveType.TriangleList, 0, vertexBuffer.VertexCount / 3);
} 

На рисунке ниже показан долгожданный, но тем не менее неожиданный результат. Камера направлена прямо на треугольник, расположенный на небольшом расстоянии от нее. В треугольнике используются три разных цвета (по одному на каждой вершине), создающих градиентную заливку с помощью пиксельного шейдера, встроенного в объект BasicEffect:

Простейшая трехмерная сцена
Пройди тесты
Лучший чат для C# программистов