Dziś parę słów na temat świateł i i cieni. Generalnie w codziennym życiu i wygenerowanej grafice możemy wytypować parę różnych typów oświetlenia:
- światła punktowe (point light) – najprostsze, żarówka koło ściany
- światła kierunkowe (directional light) – typowy reflektor estradowy
- oświetlanie słoneczne – w grafice odbywa się poprzez symulację globalnego światła kierunkowego
- ambient light – obiekty świecą swoim światłem
Do tego dochodzą elementy podnoszące realistyczność generowanego obrazu, typu:
- Ambient Occlusion – czyli generowanie cienia dla światła rozproszonego
- Twarde cienie (Hard Shadow)- generowany jest cień bez rozmycia na krawędzi
- miękkie cienie (Soft Shadow)- generowane są „bardzie realistyczne” miękkie cienie poprzez rozmywanie ich na krawędziach.
Z uwagi na to ze w shaderze wykorzystuję technikę sphere tracingu, po znalezieniu punktu przecięcia promienia wychodzącego od obserwatora z punktem na scenie dysponuje już kompletem informacji do obliczenia cienia, światła, itd. W shaderze znajdują się w strukturze Object.
W przypadku normalnych silników bazujących na trójkątach twykorzystuje się w tym informacje zachowane w buforach głębi, koloru, czy wektorów normalnych.
W shaderze mam zaimplementowaną obsługę jedynie światła typu point light oraz generowania cieni typu Ambient Occlusion i Soft oraz Hard, ale pozwala to już na „zabawę” z grafiką.
Wiec startujemy z kodem:
Główny fragment odpowiedzialny za odpalanie fragmentów kodu w zależności od ustawień preprocesora.
W przypadku światła deklarujemy dwie zmienne globalne lightColor – kolor głównego światła, lightSpecular – kolor światła odbitego, które są przekazywane do funkcji pointLight z użyciem deklaracji inout. Powoduje ona ze zmienna staje się parametrem jak również wynikiem funkcji.
#if ENABLE_LIGHT vec3 lightColor=vec3(0.0); vec3 lightSpecular=vec3(0.0); pointLight(lightColor, lightSpecular, light1, sceneObject); pointLight(lightColor, lightSpecular, light2, sceneObject); #else vec3 lightColor=vec3(1.0); vec3 lightSpecular=vec3(0.0); #endif float baseShadow=1.0; #if ENABLE_AO float ao=pointAO(sceneObject.pos, sceneObject.normal); baseShadow=baseShadow * ao; #endif #if SHADOW_MODE==1 baseShadow *= pointShadow(sceneObject.pos + SHADOW_POINT * sceneObject.normal, light1.posistion); baseShadow *= pointShadow(sceneObject.pos + SHADOW_POINT * sceneObject.normal, light2.posistion); #endif #if SHADOW_MODE==2 baseShadow *= pointSoftShadow(sceneObject.pos + SHADOW_POINT * sceneObject.normal, light1.posistion, SHADOW_BLUR); baseShadow *= pointSoftShadow(sceneObject.pos + SHADOW_POINT * sceneObject.normal, light2.posistion, SHADOW_BLUR); #endif outColor=sceneObject.mat.ambient + outColor * lightColor * baseShadow + sceneObject.mat.specular * lightSpecular; |
Główna procedura przeliczania światła następująco:
- obliczamy kierunek padania światła. Wektor normalny wyliczany z pozycji światła w stosunku do pozycji punktu przecięcia wektora kamery z obiektem sceny
- obliczamy „moc” źródła światła
- obliczamy kolor światła
- jeżeli materiał ma zdeklarowane odbicie to wyliczamy wartość tego odbica, w kodzie przedstawione są przykładowe 3 sposoby liczenia odbicia
- wyliczone zwartości zwracamy przy pomocy zmiennych: lightColor, lightSpecular
void pointLight(inout vec3 lightColor, inout vec3 lightSpecular, Light light, Object sceneObject) { vec3 lightDir=normalize(light.posistion - sceneObject.pos); float lightPower=dot(sceneObject.normal, lightDir); lightColor=clamp(lightColor+light.ambientColor + light.color * lightPower, 0.0, 1.0); if (sceneObject.mat.shininess > 0.0){ vec3 reflect = reflect(lightDir, sceneObject.normal); float specural=pow(max(0.,dot(lightDir,-reflect)), sceneObject.mat.shininess); float specural2=pow(lightPower, sceneObject.mat.shininess); float specural3=pow(max(0.,dot(lightDir,-reflect)) * 10., sceneObject.mat.shininess); lightSpecular=clamp(lightSpecular+light.color * specural, 0.0, 1.0); } } |
Cienie, krótkie omówienie poniższych funkcji generujących cienie. Generowanie cienia odbywa się od wyznaczenia drogi od punktu przecięcia do źródła światła, a na przedłużeniu drogi, wyznaczamy punktu przecięcia z płaszczyzną. Jeżeli znajdziemy ten punkt to zwracamy 0.0 (100% cień) lub 1.0 (pełna przezroczystość). W miękkim cieniu dodatkowo iteracyjne rozmywamy krawędzie.
// // Generowanie twardego cienia // float pointShadow(in vec3 ro, in vec3 rd) { float t=SHADOW_MIN_DISTANCE; for(int i=0; i < SHADOW_ITERATION; i++) { float h=map(ro + rd * t).x; if (h < SHADOW_EPSILON){ return 0.0; } t += h * SHADOW_BOOST; if (t > SHADOW_MAX_DISTANCE){ break; } } return 1.0; } // // Generowanie miękkiego cienia // float pointSoftShadow(in vec3 ro, in vec3 rd, float k) { float res=1.0; float t=SHADOW_MIN_DISTANCE; for(int i=0; i < SHADOW_ITERATION; i++) { float h=map(ro + rd * t).x; if (h < SHADOW_EPSILON){ return 0.0; } res=min(res, k * h / t); t += SHADOW_DELTA + h * SHADOW_BOOST; if (t > SHADOW_MAX_DISTANCE){ break; } } return res; } // // Ambient occlusion - generowanie cienia rozproszenia // float pointAO(in vec3 pos, in vec3 nor) { float totao=0.0; float sca=AO_BLUR_START; for(int aoi=0; aoi < AO_ITERATION; aoi++) { float hr=AO_START + AO_STEP * float(aoi); vec3 aoPos=nor * hr + pos; float distance=map(aoPos).x; totao += -(distance - hr) * sca; sca *= AO_BLUR; } return clamp(1.0 - 4.0 * totao, 0.0, 1.0); } |
Każda z metod w moim shaderze posiada możliwość ustawienia i modyfikowania paramentów zapisanych w setupie. Działający kod można zobaczyć tutaj
Możliwość komentowania jest wyłączona.