Программирование графических процессоров с использованием Direct3D и HLSL


Освещенность и материалы


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

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

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

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

, где
- интенсивность точечного источника света,
- коэффициент диффузного отражения, ? - угол падения, рассчитываемый как угол между направлением на источник света и нормалью к поверхности. Если вектора N, L нормализованы, т.е. |N|=|L|=1, то Cos ?=(NL) - скалярное произведение векторов, и уравнение можно записать в виде




Свойством диффузного отражения является равномерность по всем направлениям отраженного света. Поэтому такие объекты имеют одинаковую яркость, вне зависимости от угла обзора. К примеру, матовые поверхности обладают таким свойством. Предположим, что у нас имеется два полигона, одинаково ориентированы относительно источника света, но расположенные на разных расстояниях от него. Если вычислить значения интенсивностей освещенности для каждого полигона, по приведенной формуле, то они окажутся одинаковыми. Это означает, что объекты, расположенные на разном расстоянии от источника света, будут освещаться независимо от их местоположения в трехмерной сцене. В этом случае интенсивность света должна быть обратнопропорциональна квадрату расстояния от источника до грани объекта. Тогда объект, расположенный дальше от источника будет темнее. Тем не менее на практике, как правило, используют не квадрат расстояния, а линейное затухание. В этом случае формула общей интенсивности может быть записана как:
где d - расстояние от центра проекции (наблюдателя) до поверхности, k - некоторая константа, k
1.

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


Это объясняется тем, что блестящие поверхности отражают свет неодинаково по всем направлениям. От идеально зеркальной поверхности свет отражается только в том направлении, для которого углы падения и отражения совпадают. Зеркально отраженный свет можно будет увидеть, если угол между вектором отражения (на рисунке обозначен R) и вектором наблюдения (на рисунке обозначен S) равен нулю.



Для неидеальных отражающих поверхностей интенсивность отраженного света резко падает с ростом
. В модели, предложенной Фонгом, быстрое убывание интенсивности света, описывается функцией
, где
в зависимости от вида поверхности. Для идеальной отражающей поверхности
. Интенсивность зеркального отражения света по модели Фонга записывается как
, где
- интенсивность источника света,
- коэффициент зеркального отражения. Если вектора R и S нормированы, то формула может быть преобразована к виду:
Интенсивность зеркального света обратно пропорциональна расстоянию от источника до грани. Для практических задач здесь используют также модель линейного затухания света, которая выражается следующей формулой:
где d - расстояние от наблюдателя до поверхности, k - некоторая константа.

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


Для более реалистичного отображения объектов сцены в библиотеке Direct3D определены понятия материал и свет. Материал в Direct3D задает свойства поверхности отображаемых примитивов. С помощью материала определяется как будет отражаться от поверхности примитива свет. Материал и свет (в контексте Direct3D) используются совместно. Одно без другого работать не будет.

Для работы с материалом предусмотрен специальный тип данных D3DMATERIAL9, который содержит следующие поля.

C++

typedef struct _D3DMATERIAL9 { D3DCOLORVALUE Diffuse; {рассеянный свет, исходящий от материала} D3DCOLORVALUE Ambient; {определяет окружающий свет} D3DCOLORVALUE Specular; {определяет отражающий (зеркальный) свет} D3DCOLORVALUE Emissive; {определяет излучающий свет материала} float Power; {мощность отражения} } D3DMATERIAL9;
Pascal

TD3DMaterial9 = packed record Diffuse: TD3DColorValue; {рассеянный свет, исходящий от материала} Ambient: TD3DColorValue; {определяет окружающий свет} Specular: TD3DColorValue; {определяет отражающий (зеркальный) свет} Emissive: TD3DColorValue; {определяет излучающий свет материала} Power: Single; {мощность отражения} end;
<


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

  1. Объявление переменной типа D3DMATERIAL9;
  2. Заполнение соответствующих полей данной структуры;
  3. Установка материала.


Программно эти шаги реализуются таким образом:

C++

D3DMATERIAL9 material; ZeroMemory( &material, sizeof(D3DMATERIAL9) ); material.Diffuse=D3DXCOLOR(0.7f, 0.0f, 0.0f, 0.0f); material.Ambient=D3DXCOLOR(0.2f, 0.0f, 0.0f, 0.0f); material.Specular=D3DXCOLOR(0.1f, 0.0f, 0.0f, 0.0f); ... device->SetMaterial( &material );
Pascal



var material: TD3DMaterial9; ... ZeroMemory(@material,SizeOf(TD3DMaterial9)); material.Diffuse:=D3DXColor(0.7, 0, 0, 0); // r, g, b, a material.Ambient:=D3DXColor(0.2, 0, 0, 0); material.Specular:=D3DXColor(0.1, 0, 0, 0); ... device.SetMaterial(material);
Метод SetMaterial вызывается при выводе объектов (в процедуре рендеринга). Кроме определения свойств материала необходимо задать источники света. Библиотека Direct3D поддерживает три типа источника света:

  1. Параллельный (направленный) – этот тип не имеет определенного источника света, он как бы повсюду, но светит в одном направлении.



  2. Точечный – источник, светящий во всех направлениях (лампочка).



  3. Прожекторный (нацеленный) – тип, имеющий определенный источник света, но светящий в заданном направлении в виде направленного конуса (фонарик).





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

  1. Объявить переменную типа D3DLIGHT9;
  2. Указать тип источника и заполнить необходимые поля данной структуры;
  3. Включить (разрешить) освещенность в сцене;
  4. Установить источники света;
  5. Включить источники света.




Структура D3DLIGHT9 содержит следующие поля:

C++

typedef struct _D3DLIGHT9 { D3DLIGHTTYPE Type; { тип источника света} D3DCOLORVALUE Diffuse; {рассеянный свет, излучающий источником} D3DCOLORVALUE Specular; {зеркальный свет, излучающий источником} D3DCOLORVALUE Ambient; {окружающий свет, излучающий источником} D3DVECTOR Position; {положение источника в сцене} для точечного D3DVECTOR Direction; {направление падающего света (вектор)} float Range; {максимальное расстояние освещения} для точечного float Falloff; {используется в прожекторном источнике} float Attenuation0; {определяют закон изменения освещения} float Attenuation1; {от параметра расстояния} float Attenuation2; {1/(A0+A1*D+A2*D^2), D–расстояние от источника} float Theta; {внутренний угол источника света} float Phi; {внешний угол, [0…pi]} } D3DLIGHT9;
Pascal

TD3DLight9 = packed record _Type: TD3DLightType; {тип источника света} Diffuse: TD3DColorValue; {рассеянный свет, излучающий источником} Specular: TD3DColorValue; {зеркальный свет, излучающий источником} Ambient: TD3DColorValue; {окружающий свет, излучающий источником} Position: TD3DVector; {положение источника в сцене} для точечного Direction: TD3DVector; {направление падающего света (вектор)} Range: Single; {максимальное расстояние освещения} для точечного Falloff: Single; {используется в прожекторном источнике} Attenuation0: Single; {определяют закон изменения освещения} Attenuation1: Single; {от параметра расстояния} Attenuation2: Single; {1/(A0+A1*D+A2*D^2), D–расстояние от источника} Theta: Single; {внутренний угол источника света} Phi: Single; {внешний угол, [0…pi]} end;
Тип источника задается указанием одной из трех констант:

D3DLIGHT_POINT, // точечный источник D3DLIGHT_SPOT, // источник-прожектор D3DLIGHT_DIRECTIONAL. // направленный источник

Ниже приведен пример установки направленного источника освещения в сцене.

C++

// создание и заполнение полей структуры, установка типа источника D3DLIGHT9 light; ZeroMemory(&light, sizeof(D3DLIGHT9) ); light.Type = D3DLIGHT_DIRECTIONAL; light.Diffuse = D3DXCOLOR(1.0f, 0.0f, 0.0f, 0.0f); light.Direction = D3DXVECTOR3(0.0f, 0.0f,1.0f); ... // разрешаем работы с освещенностью device->SetRenderState( D3DRS_LIGHTING, TRUE ); ... // установка и включение первого (и единственного) источника света device->SetLight( 0, &light ); device->LightEnable( 0, TRUE );
Pascal

var light: TD3DLight9; ... // создание и заполнение полей структуры, установка типа источника ZeroMemory(@light,SizeOf(TD3DLight9)); light._Type:=D3DLIGHT_DIRECTIONAL; light.Diffuse:=D3DXColor(1,0,0,0); light.Direction:=D3DXVector3(0,0,1);

// разрешаем работы с освещенностью device.SetRenderState(D3DRS_LIGHTING, 1); ...

// установка и включение первого (и единственного) источника света device.SetLight(0,light); device.LightEnable(0,true);

<


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



Именно с помощью нормалей рассчитывается освещенность объекта (граней). Как правило, нормали задаются в каждой вершине примитива.



Поэтому необходимо корректно изменить формат вершин и набор FVF флагов.

C++

struct VERTEX3D { FLOAT x, y, z; FLOAT nx, ny, nz; }; VERTEX3D data[];

#define MY_FVF (D3DFVF_XYZ | D3DFVF_NORMAL);
Pascal

type Vertex3D = packed record x, y, z: Single; nx, ny, nz: Single; end;

const MY_FVF = D3DFVF_XYZ or D3DFVF_NORMAL;

var data: array of Vertex3D;
Следует заметить, что для правильной освещенности граней объектов все вектора, участвующие в расчете освещенности, должны быть нормированы (длина вектора равна единице). Библиотека Direct3D располагает функцией D3DXVec3Normalize(), которая позволяет привести вектор к единичной длине.

C++

D3DXVECTOR3 direction = D3DXVECTOR3(0.0f, 0.0f,1.0f); D3DXVec3Normalize( (D3DXVECTOR3*)&light.Direction, &direction );
Pascal

D3DXVec3Normalize( light.Direction, D3DXVector3(0, 0, 1) );
Кроме того, чтобы все нормали вершин после серии преобразований автоматически имели длину единица, можно установить переменную состояния D3DRS_NORMALIZENORMALS в значение "истина".

C++device->SetRenderState(D3DRS_NORMALIZENORMALS, TRUE );
Pascaldevice.SetRenderState(D3DRS_NORMALIZENORMALS, 1 );
Для того, чтобы источник в сцене был активирован нужно проделать следующие шаги. Сначала нужно вызвать процедуру установки (регистрации) нужного источника света. Это реализуется через вызов метода SetLight() интерфейса IDirect3DDevice9. Данный метод имеет два параметра: номер источника (лампы), и второй – указатель на переменную типа D3DLIGHT9. Второй шаг состоит в вызове метода LightEnable() интерфейса IDirect3DDevice9 для включения/выключения нужного источника света.


Данный метод имеет два параметра: номер источника и второй – булевская переменная (истина- включение источника, ложь - выключение).

Ниже приведен пример освещения направленным источником света треугольной грани, у которой нормали в каждой вершине одинаковы. При этом положение камеры (наблюдателя) задано точкой (2,2,-2), а направление лучей света – (-2,-2,2).



Как видно в каждом положении грань освещена одинаково (нормали в каждой вершине равны (0, 0, -1)). Изменив хотя бы одну нормаль вершины, можно добиться того, что грань будет освещаться уже неравномерно. Пусть одна из вершин будет теперь иметь нормаль (1,0,-1). Результат освещенности грани при таких изменениях нормали показан ниже.



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

Пример для точечного источника света.

C++

D3DLIGHT9 light; ZeroMemory( &light, sizeof(D3DLIGHT9) ); light.Type = D3DLIGHT_POINT; light.Diffuse = D3DXCOLOR(1.0f, 0.0f, 0.0f, 0.0f); light.Position = D3DXVECTOR3(2.0f, 2.0f, -2.0f); light.Attenuation0 = 1.0f; light.Range = 100;
Pascal

var light: TD3DLight9; ... ZeroMemory(@light, SizeOf(TD3DLight9)); light._Type:=D3DLIGHT_POINT; light.Diffuse:=D3DXColor(1,1,0,0); light.Position:=D3DXVector3(2,2,-2); light.Attenuation0:=1; light.Range:=100;
Пример для прожекторного источника света.

C++

D3DLIGHT9 light; ZeroMemory( &light, sizeof(D3DLIGHT9) ); light.Type = D3DLIGHT_SPOT; light.Diffuse = D3DXCOLOR(1.0f, 0.0f, 0.0f, 0.0f); light.Position = D3DXVECTOR3(2.0f, 2.0f, -2.0f); D3DXVECTOR3 direction = D3DXVECTOR3(-2.0f, -2.0f, 2.0f); D3DXVec3Normalize( (D3DXVECTOR3*)&light.Direction, &direction ); light.Attenuation0 = 1.0f; light.Range = 100; light.Phi = D3DX_PI/2; light.Theta = D3DX_PI /3; light.Falloff = 1.0f;
Pascal

var light: TD3DLight9; ... ZeroMemory(@light, SizeOf(TD3DLight9)); light._Type:=D3DLIGHT_SPOT; light.Diffuse:=D3DXColor(1,1,0,0); light.Position:=D3DXVector3(2,2,-2); D3DXVec3Normalize(light.Direction, D3DXVector3(-2,-2,2)); light.Attenuation0:=1; light.Range:=100; light.Phi:=pi/2; light.Theta:=pi/3; light.Falloff:=1;

Содержание раздела