Освещение в OpenGL. Материалы и освещение Включение источников света в opengl

Чтож, господа. За последнее время мы довольно много узнали об OpenGL, в том числе научились управлять камерой , работать с текстурами , а также с моделями . Настало время поговорить о чем-то намного более интересном, а именно — об освещении. Интересна эта тема, потому что ничего готового для работы со светом в OpenGL нет, все нужно писать самостоятельно на шейдерах. В рамках этой заметки мы рассмотрим освещение по Фонгу. Это довольно большая тема, поэтому говорить мы будем исключительно об освещении . В том, как делаются тени , придется разобраться в другой раз.

Сохранение и использование нормалей

Прежде, чем перейти непосредственно к освещению, нам понадобится такая штука, как нормали.

Мы уже знаем, что у моделей есть вершины и соответствующие этим вершинам UV-координаты. Для создания освещения нам понадобится еще кое-какая информация о моделях, а именно — нормали. Нормаль — это единичный вектор, соответствующей вершине (или, как вариант, полигону, но это не наш случай). Какую именно роль играют нормали при реализации освещения мы узнаем ниже. Пока достаточно сказать, что нормали действительно очень важны. Например, благодаря им поверхности выглядят более гладкими и можно отличить шар от правильного выпуклого многогранника вроде икосаэдра . А раз нормали так важны, нам нужно научиться их сохранять при преобразовании моделей Blender в наш собственный формат.

Соответствующие изменения довольно тривиальны. Мы получаем нормали точно так же, как получали координаты вершин и UV-координаты:

// часть тела процедуры importedModelCreate

for (unsigned int j = 0 ; j < face.mNumIndices ; ++ j) {
unsigned int index = face.mIndices [ j] ;
aiVector3D pos = mesh- > mVertices[ index] ;
aiVector3D uv = mesh- > mTextureCoords[ 0 ] [ index] ;
aiVector3D normal = mesh- > mNormals[ index] ;

VerticesBuffer[ verticesBufferIndex++ ] = pos.x ;
verticesBuffer[ verticesBufferIndex++ ] = pos.y ;
verticesBuffer[ verticesBufferIndex++ ] = pos.z ;
verticesBuffer[ verticesBufferIndex++ ] = normal.x ;
verticesBuffer[ verticesBufferIndex++ ] = normal.y ;
verticesBuffer[ verticesBufferIndex++ ] = normal.z ;
verticesBuffer[ verticesBufferIndex++ ] = uv.x ;
verticesBuffer[ verticesBufferIndex++ ] = 1.0f - uv.y ;
}

Аналогично изменяется процедура оптимизации модели. А в процедуре modelLoad вместо двух массивов атрибутов нам теперь потребуется три:

// часть тела процедуры modelLoad

GlBindVertexArray(modelVAO) ;
glEnableVertexAttribArray(0 ) ;
glEnableVertexAttribArray(1 ) ;
glEnableVertexAttribArray(2 ) ;

GlBindBuffer(GL_ARRAY_BUFFER, modelVBO) ;
glBufferData(GL_ARRAY_BUFFER, header- > verticesDataSize, verticesPtr,
GL_STATIC_DRAW) ;

GLsizei stride = 8 * sizeof (GLfloat) ;
glVertexAttribPointer(0 , 3 , GL_FLOAT, GL_FALSE, stride, nullptr) ;
glVertexAttribPointer(1 , 3 , GL_FLOAT, GL_FALSE, stride,
(const void * ) (3 * sizeof (GLfloat) ) ) ;
glVertexAttribPointer(2 , 2 , GL_FLOAT, GL_FALSE, stride,
(const void * ) (6 * sizeof (GLfloat) ) ) ;

Также нам дополнительно понадобится uniform-переменная с матрицей M:

GLint uniformM = getUniformLocation(programId, "M" ) ;

// ...

GlUniformMatrix4fv(uniformM, 1 , GL_FALSE, & towerM[ 0 ] [ 0 ] ) ;

… чтобы в vertex shader правильно повернуть нормаль в пространстве:

#version 330 core

Layout(location = 0 ) in vec3 vertexPos;
layout(location = 1 ) in vec3 vertexNorm;
layout(location = 2 ) in vec2 vertexUV;

uniform mat4 MVP;
uniform mat4 M;

out vec2 fragmentUV;
out vec3 fragmentNormal;
out vec3 fragmentPos;

void main() {
fragmentUV = vertexUV;
fragmentNormal = (M * vec4 (vertexNorm, 0 ) ) . xyz ;
fragmentPos = (M * vec4 (vertexPos, 1 ) ) . xyz ;

gl_Position = MVP * vec4 (vertexPos, 1 ) ;
}

Наконец, fragment shader принимает интерполированную по трем вершинам нормаль:

// ...

void main() {

// ...
}

Таким вот незамысловатым образом мы получаем нормали для фрагментов .

Что такое освещение по Фонгу

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

Важно понимать разницу между следующими понятиями:

  • Затенение по Гуро (Gouraud shading) — это когда вы вычисляете освещенность каждой вершины, а освещенность фрагментов между ними интерполируется;
  • Затенение по Фонгу (Phong shading) — когда освещенность вычисляется отдельно для каждого фрагмента;
  • Освещение по Фонгу (Phong lighting или Phong reflection model) — конкретный способ освещения, о котором идет речь в этой заметке и который можно использовать как в затенении по Гуро, так и в затенении по Фонгу;

Не удивительно, что Phong shading и Phong lighting часто путают, и в некоторых туториалах можно прочитать ерунду вроде «Идея освещения Фонга (Phong shading) заключается в использовании трех компонентов …» что сразу заставляет сильно усомниться в авторитете написавшего этот туториал человека.

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

По разным формулам вычисляется три компонента освещения:

  • Фоновое освещение (ambient lighting) — имитация света, достигшего заданной точки после отражения от других объектов. При расчете фонового освещения не учитываются ни нормали, ни текущее положение камеры;
  • Рассеянное освещение (diffuse lighting) — свет от источника, рассеянный после попадания в заданную точку. В зависимости от угла, под которым падает свет, освещение становится сильнее или слабее. Здесь учитываются нормали, но не положение камеры;
  • Отраженное освещение (specular lighting) — свет от источника, отраженный после попадания в заданную точку. Отраженный свет виден, если он попадает в камеру. Поэтому здесь учитываются как нормали, так и положение камеры;

Затем результаты суммируются, в результате чего получается общее освещение.

Чтобы стало еще интереснее, источники света бывают разные. Очевидно, что солнце на улице и фонарик в темноте освещают сцену совсем по-разному. Для начала мы рассмотрим наиболее простой источник — направленный свет.

Направленный свет (directional light)

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

struct DirectionalLight {
vec3 direction;

vec3 color;
float ambientIntensity;
float diffuseIntensity;
float specularIntensity;
} ;

В коде fragment shader определим процедуру calcDirectionalLight, которая будет использоваться как-то так:

in vec3 fragmentPos;
uniform vec3 cameraPos;
uniform DirectionalLight directionalLight;

// ...

void main() {
// normal should be corrected after interpolation
vec3 normal = normalize (fragmentNormal) ;


directionalLight) ;

// ...
}

Рассмотрим реализацию процедуры.

vec4 calcDirectionalLight(vec3 normal, vec3 fragmentToCamera,
DirectionalLight light) {
vec4 ambientColor = vec4 (light. color , 1 ) * light. ambientIntensity ;

// ...
}

Сначала вычисляется первый компонент — фоновое освещение. Это просто цвет излучаемого света умноженный на интенсивность фонового освещения. Пока все просто.

// ...

float diffuseFactor = max (0.0 , dot (normal, - light. direction ) ) ;
vec4 diffuseColor = vec4 (light. color , 1 ) * light. diffuseIntensity
* diffuseFactor;

// ...

Рассеянное освещение. Переменная diffuseFactor представляет собой косинус угла между нормалью к фрагменту и вектором, направленным от фрагмента к источнику света. Если свет падает перпендикулярно поверхности, угол равен нулю. Косинус этого угла равен единице и освещенность максимальна (см статью на Wikipedia о Законе Ламберта). С увеличением угла косинус уменьшается и становится равным нулю, если свет идет параллельно поверхности. Если косинус отрицательный, значит источник света находится где-то за поверхностью и она не освещена, поэтому отрицательные значения мы обращаем в ноль при помощи max(0.0, ...) . Помимо угла, под которым падает свет, также учитывается интенсивность рассеянного освещения diffuseIntensity.

// ...
vec3 lightReflect = normalize (reflect (light. direction , normal) ) ;
float specularFactor = pow (
max (0.0 , dot (fragmentToCamera, lightReflect) ) ,
materialSpecularFactor
) ;
vec4 specularColor = light. specularIntensity * vec4 (light. color , 1 )
* materialSpecularIntensity * specularFactor;
// ...

Отраженное освещение. Переменная lightReflect — это единичный вектор, задающий направление отраженного света. Переменная specularFactor вычисляется похожим на diffuseFactor способом, только на этот раз учитывается косинус угла между направлением, в котором отразился свет, и направлением от фрагмента до камеры. Если этот угол равен нулю, значит отраженный свет летит прямо в камеру и блики на поверхности максимальны. Если угол велик, значит никаких бликов не должно быть видно. Здесь materialSpecularFactor является uniform переменной. Чем она больше, тем меньше по площади блики на поверхности объекта. Также используется переменная materialSpecularIntensity, определяющая яркость бликов. Заметьте, что все это — свойства материала, а не света. Например, метал отражает свет, и потому имеет блики. А дерево свет не отражает, и потом вы никогда не видите бликов на деревьях (конечно, если поверхность сухая, и так далее).

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

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

// ...

return ambientColor + diffuseColor + specularColor;
}

Не так уж и сложно, правда?

Точечный источник света (point light)

Точечный источник света — это, к примеру, горящая лампочка. Свет от лампочки направлен во все стороны. Поэтому точечный источник света не характеризуется направлением света, но характеризуется положением источника в пространстве:

struct PointLight {
vec3 position;

vec3 color;
float ambientIntensity;
float diffuseIntensity;
float specularIntensity; // for debug purposes, should be set to 1.0
} ;

Освещенность от точечного источника света легко вычисляется через уже имеющуюся процедуру calcDirectionalLight:

vec4 calcPointLight(vec3 normal, vec3 fragmentToCamera,
PointLight light) {
vec3 lightDirection = normalize (fragmentPos - light. position ) ;
float distance = length (fragmentPos - light. position ) ;
float pointFactor = 1.0 / (1.0 + pow (distance , 2 ) ) ;

DirectionalLight tempDirectionalLight = DirectionalLight(
lightDirection,
light. color ,
light. ambientIntensity ,
light. diffuseIntensity ,
light. specularIntensity
) ;
return pointFactor * calcDirectionalLight(normal, fragmentToCamera,
tempDirectionalLight) ;
}

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

Прожектор (spot light)

В качестве примера этого источника света можно привести фонарик. Он похож на точечный источник света, только дополнительно имеет направление и угол влияния (cutoff):

struct SpotLight {
vec3 direction;
vec3 position;
float cutoff;

vec3 color;
float ambientIntensity;
float diffuseIntensity;
float specularIntensity; // for debug purposes, should be set to 1.0
} ;

Соответствующая процедура:

vec4 calcSpotLight(vec3 normal, vec3 fragmentToCamera,
SpotLight light) {
vec3 spotLightDirection = normalize (fragmentPos - light. position ) ;
float spotAngleCos = dot (spotLightDirection, light. direction ) ;
float attenuation = (1.0 - 1.0 * (1.0 - spotAngleCos) /
(1.0 - light. cutoff ) ) ;
float spotFactor = float (spotAngleCos > light. cutoff ) * attenuation;

PointLight tempPointLight = PointLight(
light. position ,
light. color ,
light. ambientIntensity ,
light. diffuseIntensity ,
light. ambientIntensity
) ;
return spotFactor * calcPointLight(normal, fragmentToCamera,
tempPointLight) ;
}

Направление света вычисляется точно так же, как и для точечного источника. Затем вычисляется косинус угла между этим направлением и направлением, указанным в свойствах самого источника света. При помощи выражения float(spotAngleCos > light.cutoff) свет жестко обрезается до указанного угла. Множитель attenuation добавляет плавное затухание света по мере отдаления фрагментов от направления света, указанного в свойствах источника. После этого все вычисления сводятся к вычислениям для точечного источника света.

Гамма-коррекция

Целиком процедура main во fragment shader выглядит так:

void main() {
// normal should be corrected after interpolation
vec3 normal = normalize (fragmentNormal) ;
vec3 fragmentToCamera = normalize (cameraPos - fragmentPos) ;

vec4 directColor = calcDirectionalLight(normal, fragmentToCamera,
directionalLight) ;
vec4 pointColor = calcPointLight(normal, fragmentToCamera,
pointLight) ;
vec4 spotColor = calcSpotLight(normal, fragmentToCamera, spotLight) ;
vec4 linearColor = texture(textureSampler, fragmentUV) *
(vec4 (materialEmission, 1 ) + directColor +
pointColor + spotColor) ;

vec4 gamma = vec4 (vec3 (1.0 / 2.2 ) , 1 ) ;
color = pow (linearColor, gamma) ; // gamma-corrected color
}

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

Что действительно заслуживает внимания — это гамма-коррекция , которая заключается в возведении всех компонентов света в степень 1/2.2. До сих пор мы работали в линейном пространстве цветов, исходя из предположения, что цвет с яркостью 1.0 в два раза ярче цвета с яркостью 0.5. Проблема в том, что человеческий глаз воспринимает яркость не линейно. Поэтому для получения реалистичного освещения необходимо после всех вычислений в линейном пространстве производить гамма-коррекцию.

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

Достаточно заменить в коде загрузки текстур все константы:

GL_COMPRESSED_RGBA_S3TC_DXT1_EXT
GL_COMPRESSED_RGBA_S3TC_DXT3_EXT
GL_COMPRESSED_RGBA_S3TC_DXT5_EXT

GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT
GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT
GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT

… соответственно. Так мы сообщим, что к изображением была применена гамма-коррекция, которую нужно отменить. Об остальном OpenGL позаботится сам.

В реальных приложениях параметр gamma (у нас gamma = 2.2) лучше выносить в настройки программы, чтобы пользователь при желании мог немного подстроить его под свой монитор.

Заключение

Настало время разглядывать картинки!

Здесь мы видим различные компоненты освещения. Слева направо, сверху вниз: фоновое, рассеянное, отраженное, все три вместе. Как видите, на сцену была добавлена модель тора . Из-за сложного расположения нормалей эту модель рекомендуется использовать для тестирования освещения.

Различные источники света. Слева направо, сверху вниз: белый направленный свет, красный точечный источник света, синий прожектор, все три вместе.

Отмечу еще раз, что один и тот же метод освещения может иметь разные реализации. Например, можно сделать свойства материала ambient, diffuse и specular color, что позволит рисовать красные объекты, рассеивающие зеленый цвет и имеющие синие блики. В некоторых реализациях освещения по Фонгу я видел вычисление фонового освещения один раз, а не для каждого источника света. Также я видел реализации, где свет от точечного источника затухал не просто пропорционально квадрату расстояния до него (d * d), а по более общей формуле (в стиле A + B*d + C*d*d). Кто-то делает ambient intensity и diffuse intensity свойством не только источника света, но и материала. Не уверен, правда, насколько все это имеет отношение к реалистичности освещения. Но в качестве домашнего задания можете поиграться со всем этим.

Часть 1. Подготовка

Для изучения освещения необходимы:

  • OpenGL ;
  • glut ;
  • IDE , хотя можно воспользоваться gedit или CodeBlocks ;
  • компилятор, например, gcc для Linux и mingw для Windows;
Часть 2. Пример простой программы

Рассмотрим пример программы, в которой используется освещение.

Код:

/*http://сайт, isaer*/ #include #include #include void init() { glClearColor(0.3, 0.3, 0.3, 1.0); glEnable(GL_LIGHTING); glLightModelf(GL_LIGHT_MODEL_TWO_SIDE, GL_TRUE); glEnable(GL_NORMALIZE); } void reshape(int width, int height) { glViewport(0, 0, width, height); glMatrixMode(GL_PROJECTION); glLoadIdentity(); glOrtho(-1.2, 1.2, -1.2, 1.2, -1, 1); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); } void init_l() { float light0_diffuse = {0.4, 0.7, 0.2}; float light0_direction = {0.0, 0.0, 1.0, 0.0}; glEnable(GL_LIGHT0); glLightfv(GL_LIGHT0, GL_DIFFUSE, light0_diffuse); glLightfv(GL_LIGHT0, GL_POSITION, light0_direction); } void display() { glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); init_l(); GLfloat x, y; glBegin(GL_QUADS); glNormal3f(0.0, 0.0, -1.0); for (x = -1.0; x < 1.0; x += 0.005) { for (y = -1.0; y < 1.0; y += 0.005) { glVertex3f(x, y, 0.0); glVertex3f(x, y + 0.005, 0.0); glVertex3f(x + 0.005, y + 0.005, 0.0); glVertex3f(x + 0.005, y, 0.0); } } glEnd(); glDisable(GL_LIGHT0); glutSwapBuffers(); } int main(int argc, char** argv) { glutInit(&argc, argv); glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH); glutInitWindowPosition(50, 100); glutInitWindowSize(500, 500); glutCreateWindow("Light"); init(); glutDisplayFunc(display); glutReshapeFunc(reshape); glutMainLoop(); }

Часть 3. Разбор кода в примере

Комментарии даны через символ // - "слэш".

Код:


Здесь инициализируется освещение

Код:


Здесь происходит вся прорисовка.

Код:


Часть 4. Изучаем освещение

Сначала включим расчет освещения командой glEnable(GL_LIGHTING) . Затем надо разблокировать источник света командой glEnable(GL_LIGHT) . GL_LIGHT может принимать только 8 значений (по крайней мере в OpenGL 2.1), то есть GL_LIGHT0..GL_LIGHT7 .

Теперь надо создать источник света. У каждого источника света есть свои параметры по умолчанию, например, если вы просто разблокируете 2 источника света GL_LIGHT0 и GL_LIGHT1, то будет виден только 0, так как в нем параметры по умолчанию отличаються от остальных (у всех остальных они идентичны).

Источники света имеют несколько параметров, а именно: цвет, позиция и направление.

Команда, используемая для указания всех параметров света - это glLight*() . Она принимает три аргумента: идентификатор источника света, имя свойства и желаемое для него значение.

Код:


Если нет, то тогда это единственное значение.

Например:

Код:

/*http://сайт, isaer*/ glLightf(GL_LIGHT0, GL_GL_SPOT_CUTOFF, 180);

Вот листинг значений GLenum pname .

Читается так: первая строчка - это название параметра, вторая - это значение поумолчанию, а третья - пояснение. Если вы видите что-то типа (1.0,1.0,1.0,1.0) или (0.0,0.0,0.0,1.0), то это значит, что первая скобка - это значение по умолчанию для нулевого источника, а вторая скобка - для остальных:

Код:


Пример использования освещения:

Код:

/*http://сайт, isaer*/ float light_ambient = {0.0,0.0,0.0,1.0}; float light_diffuse = {1.0,1.0,1.0,1.0}; float light_specular = {1.0,1.0,1.0,1.0}; float light_position = {1.0,1.0,1.0,0.0}; glLightfv(GL_LIGHT0, GL_AMBIENT, light_ambient); glLightfv(GL_LIGHT0, GL_DIFFUSE, light_diffuse); glLightfv(GL_LIGHT0, GL_SPECULAR, light_specular); glLightfv(GL_LIGHT0, GL_POSITION, light_position);

Часть 5. Изучаем параметры света

1. Цвет

Diffuse

Параметр GL_DIFFUSE, наверное, наиболее точно совпадает с тем, что вы привыкли называть «цветом света». Он определяет RGBA-цвет диффузного света, который отдельный источник света добавляет к сцене.

Ambient

Параметр GL_AMBIENT влияет на цвет зеркального блика на объекте. В реальном мире на объектах вроде стеклянной бутылки имеется зеркальный блик соответствующего освещению цвета (часто белого).

Specular

Параметр GL_SPECULAR влияет на интенсивность зеркального блика на объектах.

2. Позиция

Параметр GL_POSITION (x, y, z, w) имеет 3 значения положения и одно, указывающее на то, какой источник света будет использоваться.

Первые 3 (x, y, z) параметра понятны, а 4-й (w) параметр указывает, будет ли использоваться бесконечно удаленный свет или точечный. Если значение w = 0 , то источник света бесконечно удаленный (что-то вроде солнца). Если w = 1 , то этот источник света точечный (что-то вроде лампочки).

Если w = 0 , то первые 3 параметра - это вектор от центра системы координат (0,0,0).

3. Прожектор

GL_SPOT_DIRECTION - направление света прожектора.

GL_SPOT_EXPONENT - концентрация светового луча.

GL_SPOT_CUTOFF - угловая ширина светового луча.

Единственное, что нужно уточнить - позиция должна быть w = 1 .

4. Ослабление

Если вам нужно ослаблять интенсивность света от центра (то есть чем дальше от центра, тем тускнее), то вам надо настроить параметры: GL_CONSTANT_ATTENUATION, GL_LINEAR_ATTENUATION, GL_QUADRATIC_ATTENUATION .

Но так не очень удобно, поэтому можно воспользоваться формулой:

Код:


Радиус задается от центра до конца.

Если вам надо уменьшить общую интенсивность, вы можете изменить параметр att .

Часть 6. Выбор модели освещения

OpenGL-понятие модели освещения разделяется на 4 компонента:

  • интенсивность глобального фонового света;
  • считается ли положение точки наблюдения локальным к сцене или бесконечно удаленным;
  • должен ли расчет освещенности производиться по-разному для лицевых и обратных граней объектов;
  • должен ли зеркальный цвет отделяться от фонового и диффузного и накладываться на объект после операций текстурирования.
glLightModel*() – это команда, используемая для задания всех параметров модели освещения. Может принимать два аргумента: имя параметра модели освещения в виде константы и значение для этого параметра.

Код:


Теперь вы можете создавать отличные источники света.

Произведения годов. Волошин Максимилиан. ДОБЛЕСТЬ ПОЭТА. 1. Править поэму, как текст заокеанской депеши: Сухость, ясность, нажим - начеку каждое слово.

Букву за буквой врубать на твердом и тесном камне: Чем скупее слова, тем напряженней их сила. Мысли заряд волевой равен замолчанным строфам.

Вытравить из словаря слова «Красота», «Вдохновенье» - Подлый жаргон рифмачей Поэту - понятья: Правда, конструкция, план, равносильность, cжатость и точность. В трезвом, тугом ремесле - вдохновенье и честь поэта: В глухонемом веществе заострять запредельную зоркость. Волошин М.А. Библиотека: Орловская областная научная универсальная публичная библиотека им. И.А. Бунина. - М., ; Избранные произведения: В 2-х т.

М., ; Красный дым: Повести. - М., ; Гладышев из разведроты: Повести. - М., ; Эшелон; Неизбежность: Романы. Много занимался переводами марийских и удмуртских поэтов. Время от времени пробовал свои силы также в прозе. Соч. Максимилиан Александрович Волошин () - один из крупнейших поэтов первой трети XX века. Это талантливый художник, многогранный лирик, прошедший путь от символистских, эзотерических стихотворений к гражданско-публицистической и научно-философской поэзии, через антропософские пристрастия - к «идеалу Града Божия».

Предлагаемое издание дает возможность читателю ознакомиться не только с лучшими поэтическими произведениями Волошина, но также - с его наиболее интересными работами по эстетике, мемуарной прозой, публицистикой и письмами, имеющими отношение к драматическим событиям в жизни стран. Автор. Волошин Максимилиан. Все стихи автора. Произведение. Доблесть поэта. 2. Звёзды. Создавать избранные коллекции авторов и стихов!

Общаться с единомышленниками! Писать отзывы, участвовать в поэтических дуэлях и конкурсах! Присоединяйтесь к лучшему! Спасибо, что присоединились к Поэмбук! На вашу почту отправлено письмо с данными доступа к аккаунту!

Необходимо авторизоваться в течение 24 часов. В противном случае аккаунт будет удален! Зарегистрированные пользователи получают массу преимуществ: Публиковать стихи - реализовать свой талант! Создавать избранные коллекции авторов и стихов! Общаться с единомышленниками! Писать отзывы, участвовать в поэтических дуэлях и конкурсах!. Максимилиан Волошин. Описание. Максимилиан Александрович Волошин - один из крупнейших поэтов первой трети XX века.

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

Избранные произведения и письма. М. А. Волошин. Цена. руб. Максимилиан Александрович Волошин - один из крупнейших поэтов первой трети XX века. Это талантливый художник, многогранный лирик, прошедший путь от символистских, эзотерических стихотворений к гражданско-публицистической и научно-философской поэзии, через антропософские пристрастия - к "идеалу Града Божия".

Волошин М.А., Доблесть поэта: Избранные произведения и письма. серия: Новая библиотека русской классики: обязательный экземпляр Парад, г., стр., Описание книги. Максимилиан Александрович Волошин () - один из крупнейших поэтов первой трети XX века. Это талантливый художник, многогранный лирик, прошедший путь от символистских, эзотерических стихотворений к гражданско-публицистической и научно-философской поэзии, через антропософские пристрастия - к «идеалу Града Божия».

Categories Post navigation

Вэтомурокемыбудемучитьсяосвещатьизатенятьнаши3дмодели.Вотсписоктого,чтомыизучим:

  • Каксделатьтак,чтобыобъектбылярчекогданаходитсяближекисточникусвета.
  • Каксделатьотблескикогдамывидимотраженныйсветнапредмете(specular lighting )
  • Как сделать, чтобы объект был немного затененный, когда свет падает не прямо на объект(diffuse lighting)
  • Подсветка сцены(ambient lighting)
  • Тени. Эта тема заслуживает отдельного урока(или уроков, если даже не книг).
  • Зеркальное отражение(например, вода)
  • Подповерхностное рассеивание(например, как у воска)
  • Анизотропные материалы(окрашенный металл, например)
  • Затенение основанное на физических процессах, чтобы имитировать реальность еще лучше.
  • Преграждениесвета(Ambient Occlusion есличто-топреграждаетсвет,тостановитсятемнее)
  • Отражение цвета(красный ковер будет делать белый потолок слегка слегка красноватым)
  • Прозрачность
  • Глобальное освещение(в принципе все что мы указали выше можно назвать этим термином)

Другими словами, самое простое освещение и затенение.

Нормали

В прошлом уроке мы работали с нормалями, но без особого понимания, зачем они вообще нужны.

Нормали Треугольников

Нормаль к плоскости — это единичный вектор который направлен перпендикулярно к этой плоскости.

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

Вот псевдокод вычисления нормали:

треугольник(v1, v2, v3)
сторона1 = v2-v1
сторона2 = v3-v1
треугольник.нормаль = вектПроизведение(сторона1, сторона2).нормализировать()

Вершинная Нормаль

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

вершина v1, v2, v3, ....
треугольник tr1, tr2, tr3 // они все используют вершину v1
v1.нормаль = нормализовать(tr1.нормаль + tr2.нормаль + tr3.нормаль)

Использование нормалей вершин в OpenGL

Использовать нормали в OpenGL очень просто. Нормаль — это просто атрибут вершины, точно так же, как и позиция, цвет или UV координаты...Тоесть ничего нового учить не придется...даже наша простенькая функция loadOBJ уже загружает нормали.

GLuint normalbuffer;
glGenBuffers(1, &normalbuffer);

glBufferData(GL_ARRAY_BUFFER, normals.size() * sizeof(glm::vec3), &normals, GL_STATIC_DRAW);

// Третий атрибутный буфер: нормали
glEnableVertexAttribArray(2);

glBindBuffer(GL_ARRAY_BUFFER, normalbuffer);
glVertexAttribPointer(
2, // атрибут
3, // размер
GL_FLOAT, // тип
GL_FALSE, // нормализованный ли?
0, // шаг
(void*)0 // смещение в буфере
);

И этого достаточно чтобы начать:


Диффузное освещение

Важность нормали к поверхности

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

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


С точки зрения компьютерной графики, цвет пикселя очень зависит от разности углов направления света и нормали поверхности.


//
//
float cosTheta = dot(n,l);

В этом коде «n» - это нормаль, а «l» - единичный вектор который идет от поверхности к источнику света(а не наоборот, хотя это может показатьсянепонятным)

Будьте внимательны со знаком

Иногда наша формула будет не работать. Например, когда свет будет находиться за треугольником, n и l будут противоположны, поэтому n.l будет отрицательным. И в итоге у нас будет какой-то отрицательный цвет, и в итоге какой-то бред. Поэтому мы приведем все отрицательный числа к 0 с помощью функции clamp.

// Косинус угла между нормалью и направлением света
// 1 — если свет перпендикулярен к треугольнику
// 0 — если свет параллелен к треугольнику
// 0 — если свет позади треугольника
float cosTheta = clamp(dot(n,l), 0,1);
color = LightColor * cosTheta;

Цвет материала

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



Мы можем промоделировать это простым умножением:

color = MaterialDiffuseColor * LightColor * cosTheta;

Моделирование света

Давайте предположим, что у нас есть точечный источник света, который излучает свет во все направления, как, например, свечка.

С таким источником света, уровень освещения поверхности будет зависеть от расстояния до источника света: чем дальше, тем темнее. Эта зависимости рассчитывается так:

color = MaterialDiffuseColor * LightColor * cosTheta / (distance*distance);

Вскоре нам понадобится еще один параметр чтобы управлять уровнем силы света — цвет света, но пока, давайте предположим, что у нас есть лампочка белого света с определенной мощностью(например, 60 ватт).

color = MaterialDiffuseColor * LightColor * LightPower * cosTheta / (distance*distance);

Объединяем все вместе

Чтобы этот код работал нам нужен определенный набор параметров(цвета и мощности) и немного дополнительного кода.

MaterialDiffuseColor — мы можем взять прямо из текстуры.

LightColor и LightPower нужно будет выставить в шейдере с помощью GLSL uniform.

CosTheta будет зависеть от векторов n и l. Его можно вычислять для любого из пространств, угол будет одним и тем же. Мы будем использовать пространство камеры, так как тут очень просто посчитать положение светового источника:

// Нормаль фрагмента в пространстве камеры
vec3 n = normalize(Normal_cameraspace);
// Направление света(от фрагмента к источнику света
vec3 l = normalize(LightDirection_cameraspace);

Normal _cameraspace и LightDirection _ cameraspace подсчитываются в вершинном шейдере и передаются во фрагментный для дальнейшей обработки:

// Позиция вершины в пространстве камеры:МВП * положение
gl_Position = MVP * vec4(vertexPosition_modelspace,1);
// Положение вершины в мировом пространстве: M * положение
Position_worldspace = (M * vec4(vertexPosition_modelspace,1)).xyz;
// Вектор который идет от вершины камере в пространстве камеры
// В пространстве камеры, камера находится по положению (0,0,0)
vec 3 vertexPosition _ cameraspace = ( V * M * vec 4( vertexPosition _ modelspace ,1)). xyz ;
EyeDirection_cameraspace = vec3(0,0,0) - vertexPosition_cameraspace;
// Вектор который идет от вершины к источнику света в пространстве камеры.
//Матрица M пропущена, так как она в в этом пространстве единичная.
vec3 LightPosition_cameraspace = (V * vec4(LightPosition_worldspace,1)).xyz;
LightDirection_cameraspace = LightPosition_cameraspace +
EyeDirection_cameraspace;
// Нормаль вершины в пространстве камеры
Normal_cameraspace = (V * M * vec4(vertexNormal_modelspace,0)).xyz; // Будет работать лишь в том случае , когда матрица модели не изменяет её размер .

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

Обязательно попробуйте!!!

M и V – это матрицы Модели и Вида, которые передаются в шейдер точно так же, как и наша старая добрая MVP.

Время испытаний

Я рассказал вам все что нужно, чтобы сделать диффузное освещение. Вперед, попробуйте.

Результат

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



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

Окружающее освещение(ambient lighting)

Окружающее освещение – это чистой воды читерство.

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

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

vec3 MaterialAmbientColor = vec3(0.1,0.1,0.1) * MaterialDiffuseColor;
color =
// Окружающее освещение : симулируем непрямое освещение
MaterialAmbientColor +
// Диффузное : " цвет " самого объекта
MaterialDiffuseColor * LightColor * LightPower * cosTheta /
(distance*distance);

Результат

Вот так вот будет немного лучше. Вы можете по игратьсяс коефициентами (0.1, 0.1, 0.1) чтобы попробовать добиться лучшего результата.



Отраженный свет(Specular light)

Часть света которая отражается, в основном отражается в сторону отраженного луча к поверхности.



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

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


// вектор взгляда(в сторону камеры)
vec3 E = normalize(EyeDirection_cameraspace);
//Направление в котором треугольник отражает свет
vec 3 R = reflect (- l , n );
// Косинус угла между вектором взгляда и вектором отражения обрезанный до нуля если нужно
// - Смотрим прям на отражение -> 1
// -Смотрим куда-то в другую сторону -> < 1
float cosAlpha = clamp(dot(E,R), 0,1);
color =
// Окружающее освещение:симулируем непрямое освещение
MaterialAmbientColor +
// Диффузное : " цвет " самого объекта
MaterialDiffuseColor * LightColor * LightPower * cosTheta /
(distance*distance) ;
// Отраженное: отраженные отблески, как зеркало
MaterialSpecularColor * LightColor * LightPower * pow(cosAlpha,5) /

В следующем уроке мы будем разбирать, как можно ускорить рендеринг нашего VBO.

В этом уроке мы научимся создавать вершинное освещение.
Исходные файлы main.cpp, main.h, init.h взяты из урока "Начало - инициализация".

Обьявим глобальную переменную позиции света в файле main.h :

extern float g_LightPosition[ 4 ] ; // Позиция источника света

Итак, в первую очередь нам нужно инициализировать освещение.
Сделаем мы это в файле init.cpp :

// Сразу переходим к функции InitializeOpenGL().
// В конец функции, перед закрывающейся скобкой, дописываем новый код.

// Лучше всего устанавливать цвет пространства в цвет "темноты" - отсутствия освещения.
// Так как мы будем использовать белый как цвет освещения, установим цвет
// пространства в черный:

glClearColor(0 , 0 , 0 , 1 ) ;

// Ниже создадим два массива цветов. Первый, ambience - цвет "рассеянного света", когда
// на полигон не падает прямое освещение. Если мы не установим этот цвет, не освещенные полигоны
// будут совершенно черными, и мы их не увидим. Цвет освещения мы сделаем белый, а рассеянный
// свет установим в половину этого значения.
// Второй массив, diffuse, это цвет направленного света. Мы выбрали белый. Первые три
// цифры массивы - R,G,B, последняя - значение альфа. Пока что не беспокойтесь о нём.
// Для рассеянного света выберем глубоко серый цвет, для дифузного - среднее между
// белым и черным.

float ambience[ 4 ] = { 0.3f , 0.3f , 0.3f , 1.0 } ; // Цвет мирового света
float diffuse[ 4 ] = { 0.5f , 0.5f , 0.5f , 1.0 } ; // Цвет позиционного света

// Чтобы установить мировое освещене, нужно передать OpenGL наши массивы.
// OpenGL даёт нам возможность использовать несколько источников света. Общее количество
// источников зависит от переменной GL_MAX_LIGHTS. Для первого используемого источника будем
// использовать дефайн OpenGL GL_LIGHT0. После указания используемого источника передаём
// OpenGL флаг говорящий, что мы устанавливаем значение ambience, и массив ambience-цветов.
// Точно то же делаем с diffuse и GL_DIFFUSE.

// Устанавливаем цвет рассеянного цвета (без направленного света)
glLightfv( GL_LIGHT0, GL_AMBIENT, ambience ) ;
// И диффузный цвет (цвет света)
glLightfv( GL_LIGHT0, GL_DIFFUSE, diffuse ) ;

// Далее нам нужно установить позицию источника света. У нас может быть много простых
// источников в разных местах, но в этом уроке будет использоваться только один.
// Для последующих источников нужно использовать GL_LIGHT1, GL_LIGHT2, GL_LIGHT3 и т.д...
// Мы сможем изменять позицию источника клавишами "+" и "-". Позиция по умолчанию - (0,1,0,1).
// Это расположит источник прямо над центральной пирамидой. Последнее значение в массиве
// g_LightPosition сообщает OpenGL, нужен ли нам ПОЗИЦИОННЫЙ или НАПРАВЛЕННЫЙ свет.
// Если значение = 1, цвет будет позиционным, иначе он будет направлен от камеры.
// Если мы установим направленный свет, он будет освещать всё "по дороге" от нашего "глаза".
// Если же свет будет позиционным, он "повиснет" в координатах x,y,z и будет освещать
// всё вокруг себя. Это то, что нам нужно, так что установим значение в единицу.

// После инициализации источника света нам нужно включить его:
glEnable( GL_LIGHT0 ) ;

// Но недостаточно включить один источник; кроме этого нужно включить само
// освещение в OpenGL:
glEnable(GL_LIGHTING) ;

// Следующая строка позволяет закрашивать полигоны цветом при включенном освещении:
glEnable(GL_COLOR_MATERIAL) ;

Помните, если мы используем функции glColor*() вместе с освещением,
они будут проигнорированы, пока мы не включим GL_COLOR_MATERIALS.

В init.cpp мы инициализировали освещение в функции InicializeOpenGL().

Теперь используем освещение в main.cpp

// В начале файла, после включения хидеров, добавим переменные:
float g_LightPosition[ 4 ] = { 0 , 1 , 0 , 1 } ; // Позиция источника света
float g_bLight = true ; // Включен ли свет

float rotY = 0.0f ; // Вращение по Y

// Напишем функцию, создающую пирамиду:
void CreatePyramid(float x, float y, float z, int width, int height)
{

GlBegin(GL_TRIANGLES) ;

// Ниже мы добавим что-то новое: НОРМАЛИ. Значение, насколько сильно нужно
// осветить конкретный полигон, OpenGL рассчитывает из его нормалей. Что такоей нормаль?
// Нормаль - это направление полигона. Вы заметите, что мы присваиваем заднему полигону
// нормаль (0,1,-1). Это значит, что полигон направлен в обратную сторону по оси Z (внутрь
// экрана). Запомните, нормали - это не координаты, а только направления.
// Функция glNormal3f() позволяют нам указать нормаль для вершин, переданных за ней.
// Сейчас мы напишем нормали вручную, но вы можете вернутся к урокам камеры, где есть
// ничто иное, как функция для рассчета нормалей.

// Задняя сторона
glNormal3f(0 , 1 , - 1 ) ; // Полигон направлен назад и вверх

// Передняя сторона
glNormal3f(0 , 1 , 1 ) ;
glColor3ub(255 , 0 , 0 ) ; glVertex3f(x, y + height, z) ;

// Левая сторона
glNormal3f(- 1 , 1 , 0 ) ;
glColor3ub(255 , 0 , 0 ) ; glVertex3f(x, y + height, z) ;
glColor3ub(255 , 0 , 255 ) ; glVertex3f(x - width, y - height, z + width) ;
glColor3ub(0 , 255 , 255 ) ; glVertex3f(x - width, y - height, z - width) ;

// Передняя правая сторона
glNormal3f(1 , 1 , 0 ) ;
glColor3ub(255 , 0 , 0 ) ; glVertex3f(x, y + height, z) ;
glColor3ub(255 , 0 , 255 ) ; glVertex3f(x + width, y - height, z - width) ;
glColor3ub(0 , 255 , 255 ) ; glVertex3f(x + width, y - height, z + width) ;

GlEnd() ;

// Теперь отрендерим дно пирамиды

GlBegin(GL_QUADS) ;

// Эти вершины образуют дно пирамиды
glNormal3f(0 , - 1 , 0 ) ;
glColor3ub(0 , 0 , 255 ) ; glVertex3f(x - width, y - height, z + width) ;
glColor3ub(0 , 0 , 255 ) ; glVertex3f(x + width, y - height, z + width) ;
glColor3ub(0 , 0 , 255 ) ; glVertex3f(x + width, y - height, z - width) ;
glColor3ub(0 , 0 , 255 ) ; glVertex3f(x - width, y - height, z - width) ;
glEnd() ;
}

//////////////////////////////////////////////////////////////////////////
// Внесём изменения в обработку клавиш, блок WM_KEYDOWN функции WinProc():

case WM_KEYDOWN:
switch (wParam)
{
// Ниже мы позволяем пользователю увеличивать и уменьшать позицию Y источника света
// клавишами "+" и "-". После изменения позиции необходимо сообщить об этом
// OpenGL вызовом glLightfv(). Мы передаём номер источника (GL_LIGHT0) и флаг
// GL_POSITION, плюс саму позицию.
// Нажатием "L" свет включается и выключается.

case VK_ESCAPE:
PostQuitMessage(0 ) ;
break ;

case VK_ADD: // Если нажата ПЛЮС
g_LightPosition[ 1 ] += 0.1f ; // Увеличиваем значение Y
// Убедимся, что не превысили 5
if (g_LightPosition[ 1 ] > 5 ) g_LightPosition[ 1 ] = 5 ;
break ;

case VK_SUBTRACT: // Если МИНУС
g_LightPosition[ 1 ] -= 0.1f ; // Уменьшим значение Y
// Убедиммся, что оно не меньше -5
if (g_LightPosition[ 1 ] < - 5 ) g_LightPosition[ 1 ] = - 5 ;
break ;

case "L" :

G_bLight = ! g_bLight; // Выключим свет

if (g_bLight) // Включим свет
glEnable(GL_LIGHTING) ;
else
glDisable(GL_LIGHTING) ; // Выключим свет
break ;
}
break ;

////////////////////////////////////////////////////////////////////////

// И, наконец, изменим функцию RenderScene():
void RenderScene()
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) ;
glLoadIdentity() ;

// Обновим позицию света ДО вызова glLookAt(), чтобы свет обновился
// корректно. Опустите функцию вниз, чтобы увидеть, что иначе случится.
// Если источник не двигается, вызывать эту функцию заново не нужно.
glLightfv( GL_LIGHT0, GL_POSITION, g_LightPosition ) ;

GluLookAt(0 , 0 , 6 , 0 , 0 , 0 , 0 , 1 , 0 ) ;

GlRotatef(rotY, 0.0f , 1.0f , 0.0f ) ; // Вращаем пирамиду вокруг оси Y

// Это создаёт 3д пирамиду в центре (0,0,0)
CreatePyramid(0 , 0 , 0 , 1 , 1 ) ;

// Создадим пирамиды вокруг:

CreatePyramid(3 , 0 , - 3 , 1 , 1 ) ;
CreatePyramid(- 3 , 0 , - 3 , 1 , 1 ) ;
CreatePyramid(0 , 0 , 5 , 1 , 1 ) ;

// Увеличиваем впращение.

RotY += 0.6f ;

SwapBuffers(g_hDC) ;
}

Итак, вот шаги, которые мы проделали:

1) Установили ambience-цвет мира:
glLightfv(GL_LIGHT0, GL_AMBIENT, ambienceArray);

2) Установили diffuse-цвет источника света:
glLightfv(GL_LIGHT0, GL_DIFFUSE, diffuseArray);

3) Установили позицию источника света:
glLightfv(GL_LIGHT0, GL_POSITION, positionArray);

4) Включили источник света:
glEnable(GL_LIGHT0);

5) И освещение в OpenGL:
glEnable(GL_LIGHTING);