Oświetlenie:

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:

  1.  obliczamy kierunek padania światła. Wektor normalny wyliczany z pozycji światła w stosunku do pozycji punktu przecięcia wektora kamery z obiektem sceny
  2.  obliczamy “moc” źródła światła
  3.  obliczamy kolor światła
  4.  jeżeli materiał ma zdeklarowane odbicie to wyliczamy wartość tego odbica, w kodzie przedstawione są przykładowe 3 sposoby liczenia odbicia
  5.  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