В этой статье будет больше теории, чем практики, хотя и псевдокода есть немного.
Итак, приступим. Для начала уясним, что такое Indirect-Illumination (ненаправленный свет). Пока то, что мы сейчас видим в играх — это Direct 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 ){
}
void processFrame( int time ){
for( std::vector< IndirectLight* >::iterator it = emiterList.begin();
it != emiterList.end(); it++ ){
IndirectLight* emiter = (*it);
if( emiter->updateInterval == -1 || emiter->updateInterval == 0 ){
updateEmiter(emiter);
continue;
}
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;
int photonCount;
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 ){
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);
if( emiter->updateInterval == -1 ){
updateEmiter(emiter);
continue;
}
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]