Освещение трехмерных объектов

181

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

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

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

Silverlight поддерживает два типа источников света: рассеянные (ambient) и направленные (directional). Рассеянный свет освещает все фигуры одинаково, независимо от их размещения. Направленный свет распространяется в определенном направлении, поэтому освещает поверхности, повернутые к нему лицевой стороной. Обычно освещение сцены создают путем комбинирования рассеянного света и одного или нескольких направленных источников.

Наиболее простой способ добавления освещения в трехмерную сцену состоит в установке свойств AmbientLightColor и LightingEnabled объекта BasicEffect. Свойство AmbientLightColor задает цвет рассеянного освещения. Ниже показано задание красного цвета:

effect.AmbientLightColor = new Vector3(1.0f, 0.0f, 0.0f);

Как ни странно, цвета освещения задаются с помощью объектов типа Vector3, а не Color. При этом используются те же, что и в объекте Color, цветовые компоненты красный, зеленый и синий, однако диапазон их значений установлен не от 0 до 255, а от 0 до 1. В предыдущем примере красному компоненту присвоено значение 1 (чисто красный цвет). В системе RGB это соответствует цвету (255,0,0).

Следующий шаг — включение освещения, выполняемое путем присвоения свойству LightingEnabled значения true:

effect.LightingEnabled = true;

Если включить это освещение в примере с кубом, на который наложена текстура, то поверхность куба приобретет красный оттенок:

Наложение красного освещения

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

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

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

Настройка всех свойств освещения — довольно сложная задача. Для ее облегчения в класс BasicEffect встроен метод EnableDefaultLighting():

effect.EnableDefaultLighting();

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

Куб с текстурой и освещением

Для точной настройки освещения полезно более подробно знать, что делает метод EnableDefaultLighting(). Ниже приведен код, выполняемый этим методом "за кулисами":

effect.AmbientLightColor = new Vector3(0.053f, 0.098f, 0.181f);
effect.SpecularColor = new Vector3(0, 0, 0);
effect.DiffuseColor = new Vector3(0.64f, 0.64f, 0.64f);

effect.DirectionalLight0.Enabled = true;
effect.DirectionalLight0.DiffuseColor = new Vector3(1f, 0.96f, 0.81f);
effect.DirectionalLight0.Direction = new Vector3(-0.52f, -0.57f, -0.62f);
effect.DirectionalLight0.SpecularColor = new Vector3(1f, 0.96f, 0.81f);

effect.DirectionalLight1.Enabled = true;
effect.DirectionalLight1.DiffuseColor = new Vector3(0.96f, 0.76f, 0.40f);
effect.DirectionalLight1.Direction = new Vector3(0.71f, 0.34f, 0.60f);
effect.DirectionalLight1.SpecularColor = new Vector3(0f, 0f, 0f);

effect.DirectionalLight2.Enabled = true;
effect.DirectionalLight2.DiffuseColor = new Vector3(0.32f, 0.36f, 0.39f);
effect.DirectionalLight2.Direction = new Vector3(0.45f, -0.76f, 0.45f);
effect.DirectionalLight2.SpecularColor = new Vector3(0.32f, 0.36f, 0.39f);

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

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

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

Нормали куба

Чтобы получить информацию о нормальных векторах в коде, нужно заменить объекты VertexPositionTexture объектами VertexPositionNormalTexture. Различие между ними состоит в том, что в каждом объекте VertexPositionNormalTexture есть нормальный вектор:

...
// Массив вершин
VertexPositionNormalTexture[] vertices = new VertexPositionNormalTexture[36];

Vector3 frontNormal = new Vector3(0, 0, 1);
Vector3 backNormal = new Vector3(0, 0, -1);
Vector3 topNormal = new Vector3(0, 1, 0);
Vector3 bottomNormal = new Vector3(0, -1, 0);
Vector3 leftNormal = new Vector3(-1, 0, 0);
Vector3 rightNormal = new Vector3(1, 0, 0);

// Задание всех вершин
// Передняя грань
vertices[0] = new VertexPositionNormalTexture(topRightFront, frontNormal, textureTopRight);
vertices[1] = new VertexPositionNormalTexture(bottomLeftFront, frontNormal, textureBottomLeft);
vertices[2] = new VertexPositionNormalTexture(topLeftFront, frontNormal, textureTopLeft);
vertices[3] = new VertexPositionNormalTexture(topRightFront, frontNormal, textureTopRight);
vertices[4] = new VertexPositionNormalTexture(bottomRightFront, frontNormal, textureBottomRight);
vertices[5] = new VertexPositionNormalTexture(bottomLeftFront, frontNormal, textureBottomLeft);
// Задняя грань
vertices[6] = new VertexPositionNormalTexture(bottomLeftBack, backNormal, textureBottomRight);
vertices[7] = new VertexPositionNormalTexture(topRightBack, backNormal, textureTopLeft);
vertices[8] = new VertexPositionNormalTexture(topLeftBack, backNormal, textureTopRight);
vertices[9] = new VertexPositionNormalTexture(bottomRightBack, backNormal, textureBottomLeft);
vertices[10] = new VertexPositionNormalTexture(topRightBack, backNormal, textureTopLeft);
vertices[11] = new VertexPositionNormalTexture(bottomLeftBack, backNormal, textureBottomRight);
// Верх
vertices[12] = new VertexPositionNormalTexture(topLeftBack, topNormal, textureTopLeft);
vertices[13] = new VertexPositionNormalTexture(topRightBack, topNormal, textureTopRight);
vertices[14] = new VertexPositionNormalTexture(topLeftFront, topNormal, textureBottomLeft);
vertices[15] = new VertexPositionNormalTexture(topRightBack, topNormal, textureTopRight);
vertices[16] = new VertexPositionNormalTexture(topRightFront, topNormal, textureBottomRight);
vertices[17] = new VertexPositionNormalTexture(topLeftFront, topNormal, textureBottomLeft);
// Низ
vertices[18] = new VertexPositionNormalTexture(bottomRightBack, bottomNormal, textureTopLeft);
vertices[19] = new VertexPositionNormalTexture(bottomLeftBack, bottomNormal, textureTopRight);
vertices[20] = new VertexPositionNormalTexture(bottomLeftFront, bottomNormal, textureBottomRight);
vertices[21] = new VertexPositionNormalTexture(bottomRightFront, bottomNormal, textureBottomLeft);
vertices[22] = new VertexPositionNormalTexture(bottomRightBack, bottomNormal, textureTopLeft);
vertices[23] = new VertexPositionNormalTexture(bottomLeftFront, bottomNormal, textureBottomRight);
// Левая грань
vertices[24] = new VertexPositionNormalTexture(bottomLeftFront, leftNormal, textureBottomRight);
vertices[25] = new VertexPositionNormalTexture(bottomLeftBack, leftNormal, textureBottomLeft);
vertices[26] = new VertexPositionNormalTexture(topLeftFront, leftNormal, textureTopRight);
vertices[27] = new VertexPositionNormalTexture(topLeftFront, leftNormal, textureTopRight);
vertices[28] = new VertexPositionNormalTexture(bottomLeftBack, leftNormal, textureBottomLeft);
vertices[29] = new VertexPositionNormalTexture(topLeftBack, leftNormal, textureTopLeft);
// Правая грань
vertices[30] = new VertexPositionNormalTexture(bottomRightBack, rightNormal, textureBottomRight);
vertices[31] = new VertexPositionNormalTexture(bottomRightFront, rightNormal, textureBottomLeft);
vertices[32] = new VertexPositionNormalTexture(topRightFront, rightNormal, textureTopLeft);
vertices[33] = new VertexPositionNormalTexture(bottomRightBack, rightNormal, textureBottomRight);
vertices[34] = new VertexPositionNormalTexture(topRightFront, rightNormal, textureTopLeft);
vertices[35] = new VertexPositionNormalTexture(topRightBack, rightNormal, textureTopRight);

// Установка буфера вершин
GraphicsDevice device = GraphicsDeviceManager.Current.GraphicsDevice;
vertexBuffer = new VertexBuffer(device, typeof(VertexPositionNormalTexture), vertices.Length, BufferUsage.WriteOnly);
vertexBuffer.SetData(0, vertices, 0, vertices.Length, 0);

...

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

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