Рендеринг неба

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

Например, это возникает при необходимости моделировать изменение времени суток, когда хочется аккуратно отображать зависимость цвета неба от положения Солнца.

Самый простой алгоритм расчета цвета неба заключается в представлении требуемого цвета как суммы двух членов - один отвечает собственно за само небо, а второй - за вклад Солнца.

Обычно первый член строится путем интерполяции между двумя цветами - в зените и на горизонте, а в качестве параметра интерполяции обычно выступает функция угла между направлением верх (зенит) и на точку на небе (см. рис. 1. )

angles and directions

Рис 1. Используемые вектора и углы.

Вклад Солнца проще всего определять как функцию его отклонения от текущей точки на небе (например, как косинус угла gamma в некоторой степени).

Csky = mix(Czenith, Chorizon, max ( dot (t,l), 0 ) ) + Csun*pow ( max( dot (l,v),0), k )

Однако подобный подход дает не очень реалистическое изображение и к тому же требует задания дополнительных цветов (обратите внимание, что при закате Солнца , цвет на горизонте зависит от направления, а не является постоянной величиной).

Физически-корректное моделирование цвета неба оказывается довольно затруднительным из-за того, что рассеивание света в атмосфере (определяющее цвет неба) это очень сложный процесс. Подробно об этом можно прочесть в статье Арнота Притхема (Arnot J. Preetham) "Modelling Skylight and Aerial Perspective".

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

Одной из наиболее удачных моделей является всепогодная модель Переса (allweather Perez luminance model) позволяющая достаточно аккуратно вычислять яркость для произвольной точки на небе.

Согласно этой модели яркость Y точки на небе задается следующей формулой:

Perez equation for Y

Через F обозначена следующая функция:

Perez func F

Угол gamma, как и ранее, это угол между направлением на точку на небе и на Солнце (т.е. угол между векторами l и v). Угол thetas является углом между направлением на Солнце и направлением вверх (в зенит). Угол theta - угол между вектором v и направлением вверх.

Величина Yz задает яркость неба в зените. Параметры A, B, C, D и E определяют свойства атмосферы.

Эта модель была расширена Притхемом и в результате позволила определять цвет в произвольной точке неба в зависимости от рассмотренных углов и параметра turbidity, задающего свойства атмосферы (по небу находятся коэффициенты A, B, C, D и E).

Однако модель Притхема задает цвет не в привычном цветовом пространстве RGB,а в пространстве Yxy. В этом пространстве величина Y задает яркость (luminance), а xy - сам цвет (chromaticity).

В этом цветовом пространстве цвет в произвольной точке неба задается следующей формулой:

sky colors in Yxy color space

Через Yzxzyz обозначен цвет неба в зените. Для его определения в зависимости от turbidity (обозначаемой далее через T) служат следующие формулы:

basic equations

basic equations

basic equations

basic equations

Для каждой из цветовых компонент Yxy в функции F используется свой набор коэффициентов A, B, C, D и E, задаваемый следующими формулами:

A,B,C,D and E coefficients

A,B,C,D and E coefficients

A,B,C,D and E coefficients

Используя эти формулы несложно реализовать шейдер, осуществляющий расчет цвета для неба. Ясно, что если небо представлять не как куб, а как часть сферы (заданной набором треугольников), что основные параметры модели (а именно, цвет Yxy) можно вычислять в вершинном шейдера, в задачей фрагментного шейдера станет просто перевод его в цветовое пространство RGB.

Ниже приводится текст простейшего (не оптимизированнного) вершинного шейдера для расчета цвета неба по описанной выше модели.

varying	vec3 	v;
varying	vec3 	l;
varying	vec3 	pos;
varying	vec3 	colorYxy;
uniform	vec4 	sunPos;
uniform	vec4 	eyePos;
uniform	float	turbidity;

//
// Perez zenith func
//

vec3 perezZenith ( float t, float thetaSun )
{
	const float	pi = 3.1415926;
	const vec4	cx1 = vec4 ( 0,       0.00209, -0.00375, 0.00165  );
	const vec4	cx2 = vec4 ( 0.00394, -0.03202, 0.06377, -0.02903 );
	const vec4	cx3 = vec4 ( 0.25886, 0.06052, -0.21196, 0.11693  );
	const vec4	cy1 = vec4 ( 0.0,     0.00317, -0.00610, 0.00275  );
	const vec4	cy2 = vec4 ( 0.00516, -0.04153, 0.08970, -0.04214 );
	const vec4	cy3 = vec4 ( 0.26688, 0.06670, -0.26756, 0.15346  );

	float	t2    = t*t;
	float	chi   = (4.0 / 9.0 - t / 120.0 ) * (pi - 2.0 * thetaSun );
	vec4	theta = vec4 ( 1, thetaSun, thetaSun*thetaSun, thetaSun*thetaSun*thetaSun );

	float	Y = (4.0453 * t - 4.9710) * tan ( chi ) - 0.2155 * t + 2.4192;
	float	x = t2 * dot ( cx1, theta ) + t * dot ( cx2, theta ) + dot ( cx3, theta );
	float	y = t2 * dot ( cy1, theta ) + t * dot ( cy2, theta ) + dot ( cy3, theta );

	return vec3 ( Y, x, y );
}

//
// Perez allweather func (turbidity, cosTheta, cosGamma)
//

vec3  perezFunc ( float t, float cosTheta, float cosGamma )
{
    float  gamma      = acos ( cosGamma );
    float  cosGammaSq = cosGamma * cosGamma;
    float  aY =  0.17872 * t - 1.46303;
    float  bY = -0.35540 * t + 0.42749;
    float  cY = -0.02266 * t + 5.32505;
    float  dY =  0.12064 * t - 2.57705;
    float  eY = -0.06696 * t + 0.37027;
    float  ax = -0.01925 * t - 0.25922;
    float  bx = -0.06651 * t + 0.00081;
    float  cx = -0.00041 * t + 0.21247;
    float  dx = -0.06409 * t - 0.89887;
    float  ex = -0.00325 * t + 0.04517;
    float  ay = -0.01669 * t - 0.26078;
    float  by = -0.09495 * t + 0.00921;
    float  cy = -0.00792 * t + 0.21023;
    float  dy = -0.04405 * t - 1.65369;
    float  ey = -0.01092 * t + 0.05291;

    return vec3 ( (1.0 + aY * exp(bY/cosTheta)) * (1.0 + cY * exp(dY * gamma) + eY*cosGammaSq),
                  (1.0 + ax * exp(bx/cosTheta)) * (1.0 + cx * exp(dx * gamma) + ex*cosGammaSq),
                  (1.0 + ay * exp(by/cosTheta)) * (1.0 + cy * exp(dy * gamma) + ey*cosGammaSq) );
}

vec3  perezSky ( float t, float cosTheta, float cosGamma, float cosThetaSun )
{
    float thetaSun = acos        ( cosThetaSun );
    vec3  zenith   = perezZenith ( t, thetaSun );
    vec3  clrYxy   = zenith * perezFunc ( t, cosTheta, cosGamma ) / perezFunc ( t, 1.0, cosThetaSun );

    clrYxy [0] *= smoothstep ( 0.0, 0.1, cosThetaSun );			// make sure when thetaSun > PI/2 we have black color
	
    return clrYxy;
}

void main(void)
{
    v               = normalize ( (gl_Vertex-eyePos).xyz );
    l               = normalize ( sunPos.xyz );
    pos             = 0.1 * gl_Vertex.xyz;
    colorYxy        = perezSky ( turbidity, max ( v.z, 0.0 ) + 0.05, dot ( l, v ), l.z );
    gl_Position     = gl_ModelViewProjectionMatrix * gl_Vertex;
    gl_TexCoord [0] = gl_MultiTexCoord0;
}

Обратите внимание, что функция perezFunc получает на вход не значения углов, а значения их косинусов.

Так как функция Переса имеет сингулярность косинусу первого угла, то для борьбы с этим полученная яркость умножается на smoothstep ( 0, 0, cosThetaSun ) (заодно гарантируя, что ночь будет темной :)), и к v.z добавляется малая поправка, также защищающая от сингулярности.

Фрагментный шейдер получает на вход проинтерполированное значение цвета в пространстве Yxy и должен перевести его в привычное пространство RGB. Однако довольно часто получаемые значения цвета имеют слишком большую яркость, затрудняющую перевод. Поэтому удобно сначала преобразовать яркость Y, чтобы избежать ее выхода за пределы отрезка [0,1].

Довольно просто это можно сделать при помощи экспоненциальной функции:

Y' = 1 - exp ( -Y / 25.0 )

После этого цвет переводится сперва в цветовое пространство XYZ, а затем уже в традиционное RGB.

Ниже приводится соответствующий фрагментный шейдер:

varying	vec3 	pos;
varying	vec3 	v;
varying	vec3 	l;
varying	vec3 	colorYxy;

uniform float     time;
uniform sampler3D noiseMap;

vec3	convertColor ()
{
    vec3  clrYxy = vec3 ( colorYxy );
                                            // now rescale Y component
    clrYxy [0] = 1.0 - exp ( -clrYxy [0] / 25.0 );

    float ratio = clrYxy [0] / clrYxy [2];  // Y / y = X + Y + Z
    vec3  XYZ;

    XYZ.x = clrYxy [1] * ratio;             // X = x * ratio
    XYZ.y = clrYxy [0];                     // Y = Y
    XYZ.z = ratio - XYZ.x - XYZ.y;          // Z = ratio - X - Y

    const vec3 rCoeffs = vec3 ( 3.240479, -1.53715, -0.49853  );
    const vec3 gCoeffs = vec3 ( -0.969256, 1.875991, 0.041556 );
    const vec3 bCoeffs = vec3 ( 0.055684, -0.204043, 1.057311 );

    return vec3 ( dot ( rCoeffs, XYZ ), dot ( gCoeffs, XYZ ), dot ( bCoeffs, XYZ ) );
}

void main ()
{
                                            // compute color
    vec4  skyColor = vec4 ( clamp ( convertColor (), 0.0, 1.0 ), 1.0 );

    gl_FragColor = skyColor;
}

sky screenshots

sky screenshots

Рис 2. Скриншоты соответствующие разным значениям turbidity.

Как видно, изменяя параметр turbidity можно получать различные и довольно неожиданные цвета.

Облака

Хорошее небо всегда должно быть с облаками !

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

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

Полученное значение плотности облаков подвергается линейному преобразованию для управления плотностью облаков и смешивается с цветом неба. При этом мы получаем облака плывущие по небу и медленно изменяющие при этом свою форму.

Ниже приводится фрагментный шейдер соответствующий данному подходу.

varying	vec3 pos;
varying	vec3 v;
varying	vec3 l;
varying	vec3 colorYxy;

uniform	float     cloudDensity;
uniform	float     time;
uniform	sampler3D noiseMap;

vec3 convertColor ()
{
    vec3 clrYxy = vec3 ( colorYxy );
                                            // now rescale Y component
    clrYxy [0] = 1.0 - exp ( -clrYxy [0] / 25.0 );

    float ratio = clrYxy [0] / clrYxy [2];  // Y / y = X + Y + Z
    vec3  XYZ;

    XYZ.x = clrYxy [1] * ratio;             // X = x * ratio
    XYZ.y = clrYxy [0];                     // Y = Y
    XYZ.z = ratio - XYZ.x - XYZ.y;          // Z = ratio - X - Y

    const vec3 rCoeffs = vec3 ( 3.240479, -1.53715, -0.49853  );
    const vec3 gCoeffs = vec3 ( -0.969256, 1.875991, 0.041556 );
    const vec3 bCoeffs = vec3 ( 0.055684, -0.204043, 1.057311 );

    return vec3 ( dot ( rCoeffs, XYZ ), dot ( gCoeffs, XYZ ), dot ( bCoeffs, XYZ ) );
}

vec3 clouds ( const vec3 tex, float t, const vec2 vel )
{
    vec3 dt   = vec3 ( vel, 0.061 ) * t;
    vec3 tex1 = vec3 ( tex.xy, 0.234753 ) + dt; 
    vec3 n1   = texture3D ( noiseMap, tex1       ).xyz / 2.0;
    vec3 n2   = texture3D ( noiseMap, tex1 * 2.0 ).xyz / 4.0;
    vec3 n3   = texture3D ( noiseMap, tex1 * 4.0 ).xyz / 8.0;
    vec3 n4   = texture3D ( noiseMap, tex1 * 8.0 ).xyz / 16.0;

    return (n1 + n2 + n3 + n4) / (0.5 + 0.25 + 0.125 + 0.0625 );
}

float density ( float v )
{
    return clamp ( (1.0 + cloudDensity)*pow ( v, 4.0 ) - 0.3, 0.0, 1.0 );
}

void main ()
{
    const vec4 cloudColor = vec4 ( 1.0, 1.0, 1.0, 1.0 );

    vec3  n1       = clouds ( 0.6*pos, 0.0631*time, vec2 ( 0.6, 0.2 ) ); 
    vec4  skyColor = vec4	( clamp ( convertColor (), 0.0, 1.0 ), 1.0 );

    gl_FragColor = mix ( skyColor, cloudColor, density ( n1.x ) );
}

clouded sky screenshots

Рис 3. Скриншоты неба с процедурными облаками.

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

Для получения итогового цвета значения цвета для каждого слоя по очереди смешивается с цветом неба. Ниже приводится фрагмент соответствующего фрагментного шейдера и получающиеся скриншоты для двух слоев облаков.

void main ()
{
    const vec4  cloudColor     = vec4 ( 1.0, 1.0, 1.0, 1.0 );
    const vec4  cloudColor2    = vec4 ( 0.66, 0.6, 0.6, 1.0 );
    const float cloudDensity1  = 1.5;
    const float cloudDensity2  = 2.9;

    vec3 n1       = clouds ( 0.6*pos, 0.0631*time, vec2 ( 0.2, 0.1 ) ); 
    vec3 n2       = clouds ( 0.512312*pos, time * 0.281003, vec2 ( 0.0, 0.1377) );
    vec4 skyColor = vec4   ( clamp ( convertColor (), 0.0, 1.0 ), 1.0 );
    vec4 cloud    = mix    ( skyColor, cloudColor, density ( n1.x, cloudDensity1 ) );

    gl_FragColor = mix ( cloud, cloudColor2, density ( n2.x, cloudDensity2 ) );
}

sky with two cloud layers

Рис 4. Скриншоты неба с двумя слоями процедурных облаков.

Смена времени суток - день/ночь

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

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

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

В дневное время вклад этой текстуры должен быть уменьшен - днем звезды обычно не видны, в то время как луна, другие планеты (вспомните небо в Serious Sam 2) вполне видны. В качестве величины, используемой для модулирования кубической карты проще всего использовать max ( -dot (l,v), 0.0 ).

Важным моментом при этом является то, что данная карта (точнее вектор, используемый для индексирования в нее) должна поворачиваться, т.е. она подчиняется такому же закону как и солнце.

В результате у нас по мере ухода солнца на горизонт начинают появляться звезды, которые двигаются с течением времени.

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

alpha = 0.5 * ( 1 + dot ( l, v ) )

Для увеличения реалистичности можно при закате и рассвете добавлять к цвету облаков оттенки красного в зависимости их близости к Солнцу.

Для увеличения реалистичности можно для солнца и луны использовать эффекты glare и lens flare.

early morning sky screenshot

Рис 5. Небо на рассвете.

Возможные оптимизации

Наиболее очевидными оптимизациями являются вычисление цвета в зените и всех коэффициентов A, B, C, D и E на CPU и передача их шейдеру как uniform-переменных.

Исходный код ко всем примерам можно скачать здесь. Уже откомпилированные программы для M$ Windows и Linux можно скачать здесь и здесь.

Во всех примерах клавиши + и - используются для управления временем суток, а клавиши * и / позволяют изменять значение turbidity.

Valid HTML 4.01 Transitional

Напиши мне