Бесплатная реклама

Автор: Сергей Резник «Sergio»

Опубликовано: 13.06.2010

Изменено: 13.06.2010

Постоянная ссылка

Комментарии [21]

Моделирование стеклянных поверхностей





Страницы: 1 2

3. Модель освещения

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

Как я уже говорил ранее при освещении стекла нам нужно забыть про модель освещения Ламберта. В лучшем случае для моделирования диффузного освещения можно использовать одну из моделей BSSRDF (подповерхностного рассевания), но только в том случае если моделируется матовое (непрозрачное стекло). Этим мы заниматься не будем, а лучше разберем бликовую (specular) составляющую освещения. Я предлагаю использовать одну из простых, но качественных бликовых моделей освещения – модель Кука-Торренса[3].

Очень удобно будет вынести код расчета бликовой составляющей в отдельную функцию. Давайте вкратце рассмотрим модель освещения Кука-Торренса (в терминах скалярных произведений векторов, вместо углов). Будем полагать, что N – это нормаль в точке, V – нормированный вектор взгляда, L – нормированный вектор от источника света до вершины, H – нормированная сумма векторов L и V.

Итак, количество отраженного света в модели Кука-Торренса зависит от трех факторов:
1. Коэффициент Френеля (F)
2. Геометрическая составляющая, учитывающая самозатенение (G)
3. Компонент, учитывающий шероховатость поверхности (D)

Общая формула для вычисления отраженного света такова:

Моделирование стеклянных поверхностей

Коэффициент Френеля мы уже рассчитали ранее, поэтому рассмотрим вычисление геометрической составляющей:

Моделирование стеклянных поверхностей

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

Моделирование стеклянных поверхностей

где параметр m (от 0 до 1) определяет шероховатость поверхности. Чем он больше, тем поверхность боле шероховата, и, следовательно, отражает свет даже под широкими углами. Давайте это запишем в виде функции на языке GLSL:

float CookTorrance(vec3 _normal, vec3 _light, vec3 _view, float _fresnel, float _roughness)
{
  vec3  half_vec = normalize( _view + _light ); // вектор H
  // теперь вычислим разнообразные скалярные произведения
  float NdotL = max( dot(_normal, _light), 0.0 );
  float NdotV = max( dot(_normal, _view), 0.0 );
  float NdotH = max( dot(_normal, half_vec), 1.0e-7 );
  float VdotH = max( dot(_view, half_vec), 1.0e-7 );

  // геометрическая составляющая
  float geometric = 2.0 * NdotH / VdotH;
  geometric = min( 1.0, geometric * min(NdotV, NdotL) );

  // шероховатость
  float r_sq = _roughness * _roughness;
  float NdotH_sq = NdotH * NdotH;
  float NdotH_sq_r = 1.0 / (NdotH_sq * r_sq);
  float roughness_exp = (NdotH_sq - 1.0) * ( NdotH_sq_r );
  float roughness = 0.25 * exp(roughness_exp) * NdotH_sq_r / NdotH_sq;

  // финальный результат
  return min(1.0, _fresnel * geometric * roughness / (NdotV + 1.0e-7));
}

Давайте возьмем разные коэффициенты шероховатости и посмотрим результаты на картинке:

Моделирование стеклянных поверхностей

Чем меньше шероховатость – тем больше световое пятно сконцентрировано. Большие значения шероховатости подходят для металлов, поэтому наш выбор – маленький коэффициент (< 0.1). В этом случае материал будет напоминать полированное стекло, к чему мы и стремимся. Ну вот, собственно, и все по модели освещения. В следующем разделе мы займемся глобальным освещением, а именно – моделированием каустики.

4. Моделирование каустики

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

Моделирование стеклянных поверхностей

Таким образом, для того, чтобы построить изображение каустики нам необходимо:
1. найти направление луча после преломления при переходе из воздуха в материал
2. так как мы моделируем стеклянные объекты, то нам также необходимо найти направление луча на выходе из объекта
3. найти точку пересечения преломленного луча с геометрией, на которую этот луч будет падать

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

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

Итак, давайте начнем рассматривать данный метод по порядку. Для начала нам потребуется создать вершинный буфер, который мы будем выводить на отрисовку в качестве фотонной карты. Рисовать данный буфер мы будем точками, при этом исходные положения вершин будут играть роль текстурных координат для выборки из текстуры. Создается такой буфер очень легко – нужно расположить вершины в пределах от нуля до единицы, со сдвигом на пол пикселя во избежание появления черных (пустых) полос по сторонам:

// На входе - название буфера и его "плотность" (например 1024х1024)
Ce2Buffer* Ce2Render::createPhotonMap(const string& name, const vec2i& size)
{
  int numPhotons = size.x * size.y; // вычисляем кол-во точек-фотонов
  vec2 texel = vec2(1.0f / size.x, 1.0f / size.y);
  vec2 dxdy = vec2(0.5f / size.x, 0.5f / size.y);

  vec2* photons = new vec2[numPhotons];
  Index* indices = new Index[numPhotons];

  int k = 0;
  for (int i = 0; i < size.y; ++i)
  for (int j = 0; j < size.x; ++j)
    {
      photons[k] = vec2(j * texel.x, i * texel.y) + dxdy;
      indices[k] = k;
      k++;
    }

  Ce2Buffer* photonBuffer = createVertexBuffer(name, vec2::getRA(), numPhotons, photons, numPhotons, indices, GL_POINTS);

  ...

  return photonBuffer;
}

Теперь буфер у нас есть. Если его вывести на отрисовку в текстуру с простым шейдером типа:

 
  in vec2 Vertex;
  ...
  gl_Position = vec4(Vertex, 0.0, 1.0);
   

то мы получим равномерно заполненную текстуру. Если её проективно наложить на сцену, то это будет выглядеть как равномерное освещение.

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

Моделирование стеклянных поверхностей
Слева на право: задние грани преломляющего объекта, передние грани, глубина сцены

Собственно этих пяти текстур (2 с нормалями и 3 с глубиной) нам будет достаточно, чтобы рассчитать текстуру с каустикой (для текстуры каустики я выбрал однокомпонентную текстуру с плавающей точкой и половинной точностью – R16F):

Моделирование стеклянных поверхностей

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

 uniform float indexOfRefraction;
 in vec3 vNormalWS;
 out vec4 FragColor;
 void main()
   {
   vec3 vNormal = normalize(vNormalWS);
   FragColor = vec4(vec3(0.5) + 0.5 * vNormal, indexOfRefraction);
   }

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

d – d* = 0

где d – положение фотона, d* - положение объектов на сцене, прочитанное из текстуры глубины и восстановление путем умножения на обратную матрицу проекции.

Мы последовательно (итерационно) приближаемся к нахождению пересечения, причем количество итераций в применяемом методе невелико. Суть метода состоит в следующем: изначально нам известно положение и направление (нормированный вектор) луча. Для того чтобы найти пересечение мы проецируем конец данного вектора на текстуру, читаем из неё значение, восстанавливаем положение прочитанной точки в мировых координатах, находим расстояние между прочитанной точкой и исходной, затем перемещаем точку вдоль направления на это расстояние и повторяем действия (то есть опять проецируем –> читаем –> восстанавливаем –> находим расстояние). После нескольких таких итераций мы придем в точку пересечения. Предлагаю вынести нахождение пересечения в функцию и рассмотреть её:

// функция, которая по заданной точке, матрице проекции, обратной матрице и текстуре глубины
// восстанавливает прочитанное из этой значение по спроецированным на эту текстуру координатам заданной точки
vec3 reproject(vec3 position, mat4 projection, mat4 inverseProjection, sampler2D depthmap)
{
  // проецируем точку
  vec4 vProjected = projection * vec4(position, 1.0);
  vec2 texCoord = vProjected.xy / vProjected.w;
  // читаем значение из текстуры
  float fSampledDepth = 2.0 * texture(depthmap, vec2(0.5) + 0.5 * texCoord).x - 1.0;
  // восстанавливаем положение в мировых координатах
  vec4 vWorldSpaceVertex = inverseProjection * vec4(texCoord, fSampledDepth, 1.0);
  return vWorldSpaceVertex.xyz / vWorldSpaceVertex.w;
}

// функция, которая находит пересечение луча, выпущенного из заданной точки в заданном направлении
vec3 estimateIntersection(vec3 startPoint, vec3 ray, mat4 projection, mat4 inverseProjection, sampler2D depthmap)
{
  // первое приближение
  vec3 p = reproject(startPoint + ray, projection, inverseProjection, depthmap);
  // итерационное вычисление пересечения
  for (int i = 0; i < 5; ++i)
      p = reproject(startPoint + ray * distance(startPoint, p), projection, inverseProjection, depthmap);
  return p;
}

Некоторые заметки о количестве итераций: их не должно быть слишком мало (напр. 2 или 3), но и не нужно слишком много (напр. 20 или больше). Разница между двумя и пятью итерациям значительно больше, нежели между пятью и пятьюдесятью. В случае малого количества итераций – пересечение будет найдено неверно.

Итак, думаю самое время вплотную подойти к расчету каустики. Наиболее понятым будет приведение кода с пояснениями. Давайте начнем рассматривать наш шейдер. Пять текстур, которые мы подготовили:

 
 uniform sampler2D refractive_normals;
 uniform sampler2D refractive_depth;
 uniform sampler2D receiver_depth;
 uniform sampler2D refractive_backface_normals;
 uniform sampler2D refractive_backface_depth;
 

Далее нам потребуется положение камеры (так как мы рисуем из положения источника света, то положение камера совпадает с ним), матрица проекции и обратная матрица проекции:

 
 uniform vec3 vCamera;
 uniform mat4 mModelViewProjection;
 uniform mat4 mInverseModelViewProjection;
 

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

 
 vec3 sampleRefractivePosition(vec2 texCoords, sampler2D depthmap)
 {
   // читаем глубину
   float fSampledDepth = 2.0 * texture(depthmap, texCoords).x - 1.0;
   // восстанавливаем мировые координаты
   vec4 vWorldSpaceVertex = mInverseModelViewProjection * vec4(2.0 * texCoords - vec2(1.0), fSampledDepth, 1.0);
   return vWorldSpaceVertex.xyz / vWorldSpaceVertex.w;
 }

На вход вершинного шейдера нам поступает вершина из созданного ранее буфера, на выходе – цвет данной вершины:

 
 in vec2 Vertex;
 out vec4 LightColor;

 // теперь основная часть
 void main()
 {
   LightColor = vec4(1.0); // пока константа
   ...
 }

Помните, что у нас Vertex выступает в роли текстурных координат? По этим текстурным координатам мы читаем из текстуры с глубиной, содержащей преломляющий объект и если глубина равна единице, то в данной точке нет преломляющего объекта и нам нет необходимости проводить все расчеты - просто преобразовываем положение вершины из [0..1] в [-1..1], записываем его и выходим из шейдера:

 
 if (texture(refractive_depth, Vertex).x == 1.0) 
 {
   gl_Position = vec4(2.0 * Vertex - vec2(1.0), 0.0, 1.0);
   return;
 }

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

 
 vec4 vSampledNormal = texture(refractive_normals, Vertex);
 if (vSampledNormal.a == 1.0) 
 {
   gl_Position = vec4(2.0 * Vertex - vec2(1.0), 0.0, 1.0);
   return;
 }

Итак, мы наконец-то добрались до основной части шейдера.

 
 // пункт первый – находим положение преломляющего объекта в мировых координатах:
 vec3 vRefractivePosition = sampleRefractivePosition(Vertex, refractive_depth);
 // пункт второй – восстанавливаем нормаль из [0..1] в [-1..1]:
 vec3 vNormal = 2.0 * vSampledNormal.xyz - vec3(1.0);
 // пункт третий – находим исходное направление луча света:
 vec3 vLightVector = normalize(vRefractivePosition - vCamera);
 // пункт четвертый – находим преломленный вектор направления луча света:
 vLightVector = refract(vLightVector, vNormal, vSampledNormal.a);

 // если мы не просчитываем два преломления, тогда просто 
 // находим пересечение луча с геометрией сцены и записываем его
 vec3 vIntersectedVertex = estimateIntersection(vRefractivePosition, vLightVector,
				mModelViewProjection, mInverseModelViewProjection, receiver_depth);
 gl_Position = mModelViewProjection * vec4(vIntersectedVertex, 1.0);

 // в случае двух преломлений нам необходимо будет найти
 // пересечение преломленного луча с задними гранями объекта:
 vec3 vFirstIntersectedVertex = estimateIntersection(vRefractivePosition, vLightVector,
				     mModelViewProjection, mInverseModelViewProjection, refractive_backface_depth);
 // спроецировать точку пересечения и найти новые текстурные координаты для выборки:
 vec4 vProjectedIntersection = mModelViewProjection * vec4(vFirstIntersectedVertex, 1.0);
 vec2 vProjectedIntersectionTC = vec2(0.5) + 0.5 * vProjectedIntersection.xy / vProjectedIntersection.w;
 // прочитать из текстуры нормаль задней грани:
 vec3 vBackfaceSampledNormal = vec3(1.0) - 2.0 * texture(refractive_backface_normals, vProjectedIntersectionTC).xyz;
 

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

 
  vLightVector = refract(vLightVector, vBackfaceSampledNormal, 1.0 / vSampledNormal.a);
 

Еще раз обратите внимание ваше внимание – в данном случае луч выходит из материала в воздух, поэтому показатель преломления будет 1/η.

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

Нам, конечно, хотелось бы это учесть, но нам не придется проверять угол падения, так как встроенная GLSL функция refract в случае полного внутреннего отражения вернет значение vec3(0.0). То есть нам следует лишь проверить возвращаемое значение:

 
 if (dot(vLightVector, vLightVector) < 1.0e-5) 
 {
   LightColor = vec4(0.0); 
   gl_Position = vec4(Vertex, 0.0, 1.0);
   return;
 }
 
 // если же мы успешно нашли направление преломленного луча, то нам остается 
 // сделать прочитать из текстуры глубины положение задней грани объекта:
 vRefractivePosition = sampleRefractivePosition(vProjectedIntersectionTC, refractive_backface_depth);
 // и найти пересечение этого луча с геометрией сцены:
 vec3 vIntersectedVertex = estimateIntersection(vRefractivePosition, vLightVector,
	                 mModelViewProjection, mInverseModelViewProjection, receiver_depth);
 // после чего мы точно так же проецируем найденную точку:
 gl_Position = mModelViewProjection * vec4(vIntersectedVertex, 1.0);
 

Фрагментный шейдер очень простой и выглядит вот так:

 
 in vec4 LightColor;
 out vec4 FragColor;
 void main()
 {
   FragColor = LightColor;
 }

Следует заметить, что при расчете каустики необходимо включить аддитивное смешивание (glBlendFunc(GL_ONE, GL_ONE)) чтобы наложение лучей света (фотонов) действительно повышало яркость.

Теперь у нас готова и текстура с каустикой, а наложить её на сцену – очень простая задача, которая напоминает использование техники shadow maps за тем лишь исключением, что мы ни что ни с чем не сравниваем, а просто проективно накладываем текстуру с каустикой на сцену. То есть, при отрисовке объектов сцены мы проецируем текущую вершину в пространство источника света и делаем выборку из текстуры, содержащей рассчитанную ранее каустику. При этом рассчитанная текстура с каустикой сразу заменяет нам карту теней. Все-таки photon mapping, как ни как :)

5. Фильтрация текстуры с каустикой

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

Моделирование стеклянных поверхностей

Для устранения этого эффекта нам необходимо отфильтровать текстуру с каустикой. Для этого нам понадобиться дополнительный проход отрисовки в текстуру. Рисовать в текстуру мы будет один прямоугольник на весь экран (fullscreen quad) с шейдером, которой обеспечит нам удаление шума и небольшое сглаживание текстуры. Основная идея фильтрации состоит в том, чтобы найти произведение текущей точки и суммы соседних точек. Если вокруг точки будут нулевые значения – значит, эта точка является «шумом» и после умножения на сумму соседних она будет отфильтрована (так же равняется нулю). Шейдер выглядит следующим образом:

 
 uniform sampler2D source_image; // на входе у нас текстура с каустикой
 uniform vec2 texel; // размеры одного текстеля исходной текстуры

 in vec2 TexCoord;
 out vec4 FragColor;

 void main()
 {
   vec2 dx = vec2(texel.x, 0.0);
   vec2 dy = vec2(0.0, texel.x);
   // сделаем выборку по текущим текстурным координатам
   float fCenter = texture(source_image, TexCoord).x;
   // а так же сделаем выборку по соседним текселям текстуры
   float fNear = texture(source_image, TexCoord - dx).x + texture(source_image, TexCoord + dx).x +
	               texture(source_image, TexCoord - dy).x + texture(source_image, TexCoord + dy).x;
   // разделим на четыре и возведем в квадрат, после чего разделим на два
   fNear = 0.03125 * fNear * fNear; // 0.5 * 0.25 * 0.25
   // результат вычислим как минимум исходного значения и нового – для того, 
   // чтобы данный фильтр не повышал нам яркость изображения
   float value = min(fCenter, fNear * (fCenter + 1.0));
   FragColor = vec4(value);
}

Результатом работы данного шейдера будет отфильтрованное изображение, содержащее каустику:

Моделирование стеклянных поверхностей

6. Два премломления при отрисовке стекла

В предыдущем разделе мы разобрались, как рассчитать два преломления лучей света в объекте. А давайте подумаем – нельзя ли применить такую же технику для того, чтобы улучшить рендеринг нашего объекта? Ранее мы просчитывали преломленный один раз луч и делали выборку из кубической карты в этом направлении. Такой метод хорошо подходит для моделирования воды – когда преломленный луч потом не выходит их среды. Но мы же моделируем стеклянный предмет, которые имеет конечные размеры. То есть, проще говоря – если луч света попал в наш объект, то рано или поздно он из него должен выйти. Так вот я предлагаю рассчитать собственно эту точку, из которой будет совершен переход стекло-воздух и направление, в котором он будет двигается после второго преломления. Для этого нам так же понадобиться дополнительный проход – отрисовка нормалей и глубины задних граней объекта, но уже из позиции наблюдателя.

Помните, в разделе 2 мы находили направление вектора взгляда после первого преломления на границе воздух-стекло и обозначили его как vRefracted?

Теперь же нам необходимо найти пересечение этого луча с задней гранью объекта (функция пересечения у нас уже есть):

 
 vec3 vBackfaceIntersection = estimateIntersection(vVertexWS.xyz, vRefracted, mModelViewProjection, 
                                                   mModelViewProjectionInverse, backface_depth);
 // спроецируем его и прочитаем из текстуры нормаль:
 vec4 vProjectedBI = mModelViewProjection * vec4(vBackfaceIntersection, 1.0);
 vec3 vBackfaceNormal = vec3(1.0) - 2.0 * texture(backface_texture, vec2(0.5) + 
                                                    0.5 * vProjectedBI.xy / vProjectedBI.w).xyz;
 // найдем вектор, указывающий направление луча внутри объекта:
 vec3 vInnerRay = normalize(vBackfaceIntersection - vVertexWS.xyz);
 

и... здесь тоже следует сделать паузу и кое-что объяснить по поводу полного внутреннего отражения в данном случае.

Дело в том, что при полном внутреннем отражении луч света может выйти из объекта в любой другой точке (там, где угол падения будет меньше отношения показателей преломления). Но ведь мы используем 2Д текстуру с глубиной и нормалями и в общем случае не можем "увидеть" точку, в которой отраженный от внутренней поверхности луч света выйдет из объекта. Поэтому сделаем некоторое допущение и будем считать, что отраженный луч света сразу выходит из объекта. В данном случае как-то нецелесообразно получается делать проверку на равенство нулю результата функции refract, поэтому мы возьмем её исходник и немного модифицируем его под свои нужды. А именно в случае полного внутреннего отражения сразу будем возвращать отраженный вектор:

 
 vec3 computeRefractedVector(vec3 incidence, vec3 normal, float eta)
 {
   float etaIdotN = eta * dot(incidence, normal);
   float k = 1.0 - eta*eta + etaIdotN*etaIdotN; 
   if (k < 0.0)
     return reflect(incidence, normal);
     else
     return eta * incidence - normal * (etaIdotN + sqrt(k));
 }

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

 
 vec3 vBackfaceRefracted = computeRefractedVector(vInnerRay, vBackfaceNormal.xyz, 1.0 / indexOfRefraction);
 // теперь сделаем выборку из кубической текстуры в этом направлении:
 vec4 cBackfaceColor = texture(environment_map, vBackfaceRefracted);
 // и так же смешаем её с вычисленным ранее отражением по вычисленному ранее коэффициенту Френеля:
 vec4 cColor = mix(cBackfaceColor, cReflection, fFresnel);
 

Ну вот, собственно, и все. Теперь мы должны будем получить вот такую картину:

Моделирование стеклянных поверхностей

Моделирование стеклянных поверхностей

7. Дальнейшее развитие техники

В качестве дополнительной "фишки" описанной техники можно ввести в расчеты дисперсию света – явление, при котором абсолютный показатель преломления зависит от длины волны света. То есть белый свет на границе материала расщепляется на спектр, каждый из компонентов которого отклоняется на разный угол. Реализация данного метода описана в [2].

Для моделирования сложных многосвязных поверхностей, в которых есть "дырки" (например тора) или для просчета каустики от нескольких объектов, расположенных друг за другом необходимо использовать разложение сцены по слоям [7] так как двух проходов (с передними и задними гранями) будет уже недостаточно.

8. Рабочая программа и исходники

Ссылка на программу, демонстрирующую описанную технику: Glass_Rendering.zip

Управление в программе:
WASD - перемещение
Левая кнопка мыши – вращение камеры
Тильда – показать информацию (FPS и текущие настройки)

Настройки:
"-"/"+" - уменьшение/увеличение показателя преломления стекла (варьируется от 1.01 до 2.5)
"F1" — включение/выключение HDR
"F2" — включение/выключение использования геометрических шейдеров
"F3" — включение/выключение просчета двух преломлений при рендеринге стекла
"F4" — включение/выключение просчета двух преломлений для каустики

Так же, в папке "config" лежит файл с именем "appconfig", в котором можно настроить размеры используемых текстур и файл модели, которую следует отображать. Модели лежат в папке "data/models".

Примечания:
Программа может не очень корректно работать на видеокартах ATI. В частности неверно работают геометрические шейдеры.
В архиве содержится исходный код программы, в который включен мой движок. Файл проекта в формате MS Visual Studio 2010.

Полезные ссылки

1. Спецификация OpenGL
http://www.opengl.org/registry/

2. Каустика в реальном времени
http://www.gamedev.ru/code/articles/caustic

3. Быстрая реализация модели освещения Кука-Торренса с использованием GLSL
http://www.gamedev.ru/code/articles/Cook-Torrance

4. Caustics Mapping: An Image-space Technique for Real-time Caustics
http://graphics.cs.ucf.edu/caustics/

5. Уравнения Френеля
http://en.wikipedia.org/wiki/Fresnel_equations

6. Дисперсия света
http://en.wikipedia.org/wiki/Dispersion_(optics)

7. Depth Peel или разложение сцены по слоям
http://steps3d.narod.ru/tutorials/depth-peel-tutorial.html

 


 

Статья участвовала в конкурсе статей по программированию #4 (2009).

 

Страницы: 1 2