ПрограммированиеСтатьиГрафика

GPU Global Illumination (фейковый)

Автор:

В этой статье будет обсуждаться реализация Глобально Освещения (Global Illumination) с использованием GPU, а фейк весь в том, что мы всё-таки будем использовать и CPU – для трассировки «фотонов».

images | GPU Global Illumination (фейковый)

В этой статье будет больше теории, чем практики, хотя и псевдокода есть немного.

Итак, приступим. Для начала уясним, что такое Indirect-Illumination (ненаправленный свет). Пока то, что мы сейчас видим в играх — это Direct Illumination (направленный свет).

Направленный свет:


direct_illumination | GPU Global Illumination (фейковый)
Направленный свет схематично.


Local_illumination | GPU Global Illumination (фейковый)
Направленный свет в сцене.

Ненаправленный свет:


indirect_illumination | GPU Global Illumination (фейковый)
Неправленный свет схематично.


Global_illumination | GPU Global Illumination (фейковый)
Ненаправленный свет в сцене.

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

Итак, на счёт моей идеи фейкового глобального освещения. Для каждой модели создаём текстуру R8G8B8 произвольного размера (всё зависит от количества видео памяти у видеокарты), но чем текстура больше, тем качественее картинка будет в итоге, делаем вторую uv развёртку для модели под эту текстуру (такую же как и для Lightmap или Ambient Map).

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

class IndirectLight{
public:
  // позиция источника
  float3 position;

  // цвет источника
  float3 radiant;

  // радиус источника
  float  radius;

  // интервал апдейта
  int    updateInterval;

  // время прошлого апдейта
  int    time;

  IndirectLight() {
    radiant = float3(1.0f,1.0f,1.0f);
    radius = 1.0f;
    updateInterval = -1;
  }

  IndirectLight( const float3& radiant , float radius , int updateInterval = -1 ){
    this->radiant = radiant;
    this->radius = radius;
    this->updateInterval = updateInterval;
  }

  ~IndirectLight(){
  }
};

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

class IndirectProcessor{
public:
  std::vector< IndirectLight* > emiterList;

  IndirectProcessor() {
  }

  ~IndirectProcessor(){
  }

  // Добавление нового источника 
  void addEmiter( IndirectLight* emiter ){
    removeEmiter(emiter);
    emiterList.push_back(emiter);
  }

  // Удаление источника из списка
  void removeEmiter( IndirectLight* emiter ){
    for( std::vector< IndirectLight* >::iterator it = emiterList.begin();
         it != emiterList.end(); it++ ){
      if( (*it) == emiter ) {
        emiterList.erase(it);
        return;
      }
    }
  }

  // Обновление источника
  void updateEmiter( IndirectLight* emiter ){
    // TODO
  }

  // Процессинг
  void processFrame( int time ){
    for( std::vector< IndirectLight* >::iterator it = emiterList.begin();
         it != emiterList.end(); it++ ){
      IndirectLight* emiter = (*it);

      // Проверяем, если апдейт интервал равен -1 или 0,
      // то значит обнавлять такой источник придётя каждый кадр
      // что не очень хорошо, но всё же...
      if( emiter->updateInterval == -1 || emiter->updateInterval == 0 ){
        updateEmiter(emiter);
        continue;
      }

      // Если текущее время источника равно -1,
      // то значит источник ещё ни разу не обновлялся
      if( emiter->time == -1 ) {
        updateEmiter(emiter);
        emiter->time = time;
        continue;
      }

      // Прибавляем к прошлому времени источника апдейт интервал
      // и если то, что у нас получилось равно текущему времени:
      if( emiter->time + emiter->updateInterval == time ){  
        updateEmiter(emiter);
        emiter->time = time;
      }
    }
  }
};

Итак, все затраты заключаются в том, что нам придётся трассировать лучи от источников, нам придётся трассировать от каждого источника N лучей с рандомным направлением + K отражений каждого луча.

Обновляем наш класс источника, добавим в него N и K:

class IndirectLight{
public:
  // позиция источника
  float3 position;
           
  // цвет источника
  float3 radiant;

  // радиус источника
  float  radius;

  // апдейт интервал
  int    updateInterval;

  // время прошлого апдейта
  int    time;

  // N - кол-во "фотонов"
  int    photonCount;

  // K - кол-во отражений фотона
  int    photonReflectionCount;


  IndirectLight() {
    radiant = float3(1.0f,1.0f,1.0f);
    radius = 1.0f;
    updateInterval = -1;
    time = -1;
    photonCount = 0;
    photonReflectionCount = 0;
  }

  IndirectLight( const float3& radiant , float radius , int photonCount = 100 , 
                 int photonReflectionCount = 4 , int updateInterval = -1 ){
    this->radiant = radiant;
    this->radius = radius;
    this->updateInterval = updateInterval;
    time = -1;
    this->photonCount = photonCount;
    this->photonReflectionCount = photonReflectionCount;
  }

  ~IndirectLight(){
  }
};

Алгоритм трассировки таков:

1) Забиваем нули в наши текстуры у моделей.
2) Трассируем текущий фотонный луч. Если попали то переходим к пункту 3, если же нет, трассируем следующий фотонный луч.
3) Берём UV пересечения (важно, чтобы UV пересечения вычислялось по второй развёртке для нашей текстуры) и добавляем по этим UV в текстуру модели с которой произошло пересечение emiter->radiant, если, конечно, текущее значение в текстурке равно нулю, если же нет, то смешиваем текущее значение с emiter->radiant. Далее мы отражаем текущий луч по нормали персечения и возвращаемся к пункту 2 но reflectionIndex уменьшаем на 1, чтобы не получить бесконечный цикл. После окончания обновления всех источников, шлем все текстуры моделей на GPU.

class IndirectProcessor{
public:
  std::vector< IndirectLight* > emiterList;

  IndirectProcessor() {
  }

  ~IndirectProcessor(){
  }

  // Добавление нового источника 
  void addEmiter( IndirectLight* emiter ){
    removeEmiter(emiter);
    emiterList.push_back(emiter);
  }

  // Удаление источника из списка
  void removeEmiter( IndirectLight* emiter ){
    for( std::vector< IndirectLight* >::iterator it = emiterList.begin();
        it != emiterList.end(); it++ ){
      if( (*it) == emiter ) {
        emiterList.erase(it);
        return;
      }
    }
  }

  // Трассировка луча
  void traceRadiant( const float3& orig, const float3& dir,
                     float radius , float3& radiant , int reflectionIdx ){
    Hit hit;

    // Если луч не персёкся ни с чем, то выходим из функции
    if( !world->traceRay(hit,dir,radius) ) return;

    // Домножаем текущий цвет на цвет материала
    radiant = hit.material.color * radiant;

    // Отправляем текущий цвет в нашу текстуру модели
    hit.model->uploadRadiant(hit.u,hit.v,radiant);

    
    if( reflectionIdx > 0 )
      traceRadiant(dir.reflect(hit.normal), radius, radiant, reflectionIdx - 1 );
  }

  // Обновление источника
  void updateEmiter( IndirectLight* emiter ){

    // Цикл от 0 до кол-ва "фотонов" источника
    for( int i = 0; i < emiter->photonCount; i++ ){
      // Генерируем случайный луч.
      float3 dir = generateRandomDirection();

      // Трассируем "фотон"
      traceRadiant( emiter->position, dir, emiter->radius, 
                    emiter->radiant, emiter->photonReflectionCount);
    }
  }

  // Процессинг
  void processFrame( int time ){
    for( std::vector< IndirectLight* >::iterator it = emiterList.begin();
         it != emiterList.end(); it++ ){
      IndirectLight* emiter = (*it);

      // Проверяем, если апдейт интервал равен -1,
      // то значит обновлять такой источник придётя каждый кадр
      // что не чень хорошо, но всё же...
      if( emiter->updateInterval == -1 ){
        updateEmiter(emiter);
        continue;
      }

      // Если текущее время источника равно -1,
      // то значит источник ещё ни разу не обновлялся.
      if( emiter->time == -1 ) {
        updateEmiter(emiter);
        emiter->time = time;
        continue;
      }

      // Прибавляем к прошлому времени источника апдейт интервал
      // и если то, что у нас получилось равно текущему времени:
      if( emiter->time + emiter->updateInterval == time ){  
        updateEmiter(emiter);
        emiter->time = time;
      }
    }
  }
};

Псевдо шейдер на языке NVIDIA Cg:

void main( in Fragment fragment , out Output target , uniform sampler2D photonMap ){
  float2 texcoord2 = fragment.photonTexcoord;

  float3 indirect = float3(0,0,0);
  const float multipler = 1.0f / 9.0f;
  for( int x = -1; x < 1; x++ ){
    indirect += tex2D(photonMap,texcoord2,int2(x,0)).rgb;
  }
  for( int y = -1; y < 1; y++ ){
    indirect += tex2D(photonMap,texcoord2,int2(0,y)).rgb;
  }
  indirect = clamp(indirect * multipler,0.0f,1.0f);

  // Далее идёт направленое освещение и в итоге
  target.color = direct + indirect;
}

Некоторые мысли по оптимизации:

1) Мультипоточная обработка источников: можно, например, сразу обновлять несколько источников в разных потоках.
2) BSP Tree было бы огромным плюсом для трассировки лучей.
3) Использования SSE для проверки пересечения луча с треугольником.
4) Использование OpenCL\CUDA для трассировки лучей.

Также есть менее затратный вариант в плане видеопамяти. Вместо текстур мы будет использовать Deferred Lighting и так называемые Virtual Point Light (Point Light не имеющий радиуса). И вместо hit.model->uploadRadiant(hit.u,hit.v,radiant); будем добавлять в список источников света vpl и обрабатывать его с помощью Deferred Lighting.

Вот и всё, удачи в начинаниях.

Литература:
- http://en.wikipedia.org/wiki/Global_illumination
- http://ru.wikipedia.org/wiki/Глобальное_освещение
- http://habrahabr.ru/blogs/3d_graphics/77235/
- http://ru.wikipedia.org/wiki/Трассировка_лучей
- http://en.wikipedia.org/wiki/Binary_space_partitioning
- http://ru.wikipedia.org/wiki/SSE
- http://en.wikipedia.org/wiki/Multithreading

#global illumination, #GPU, #indirect illumination

27 июня 2010 (Обновление: 12 июля 2010)

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