Информация в данном посте могла устареть. Пост сохранен для истории.

Продолжаю атмосферную тематику. Как я уже писал, для создания правдоподобной атмосферы для глобуса удобно использовать алгоритм О’Нила. В osgEarth уже есть класс, который на основе этого алгоритма рендерит атмосферу. Но реализована только половина алгоритма - для расчета цвета околоземного пространства. К цвету рельефа (виртуального глобуса) алгоритм не применяется. Вот как это выглядит:

Атмосфера

Исправить это недоразумение достаточно просто. Однако есть свои тонкости. Мы не можем «в лоб» применить шейдер О’Нила к рельфу, т.к. osgEarth УЖЕ использует шейдер для рендеринга рельефа для смешивания разных текстурных слоёв.

В osgEarth есть механизм, который позволяет нам встраивать куски шейдерного кода внутрь основного шейдера. В общем виде этот механизм описан здесь. Коротко рассмотрю основные шаги применительно к атмосфере:

1) Создать экземпляр osgEarth::VirtualProgram и присоединить к стейтсету osgEarth::MapNode:

osgEarth::VirtualProgram* vp = new osgEarth::VirtualProgram;
mapNode->getOrCreateStateSet()->setAttributeAndModes(vp);

2) Оформить наш шейдерный код в виде отдельных функций и поместить их в текстовые переменные:

std::string vertShader, fragShader;
//шейдер можно прочитать из файла

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

vp->setFunction("setup_ground", vertShader, osgEarth::ShaderComp::LOCATION_VERTEX_POST_LIGHTING);
vp->setFunction("apply_ground", fragShader, osgEarth::ShaderComp::LOCATION_FRAGMENT_POST_LIGHTING);

4) Далее, конкретно для работы атмосферы нужно задать несколько uniform-«переменных».

ss->getOrCreateUniform("atmos_v3LightPos", osg::Uniform::FLOAT_VEC3)->set(osg::Vec3(0.0, 1.0, 0.0));
ss->getOrCreateUniform("atmos_v3InvWavelength", osg::Uniform::FLOAT_VEC3)->set(RGB_wl);
ss->getOrCreateUniform("atmos_fInnerRadius", osg::Uniform::FLOAT)->set(_innerRadius);
ss->getOrCreateUniform("atmos_fInnerRadius2", osg::Uniform::FLOAT)->set(_innerRadius * _innerRadius);
ss->getOrCreateUniform("atmos_fOuterRadius", osg::Uniform::FLOAT)->set(_outerRadius);
ss->getOrCreateUniform("atmos_fOuterRadius2", osg::Uniform::FLOAT)->set(_outerRadius * _outerRadius);
ss->getOrCreateUniform("atmos_fKrESun", osg::Uniform::FLOAT)->set(Kr * ESun);
ss->getOrCreateUniform("atmos_fKmESun", osg::Uniform::FLOAT)->set(Km * ESun);
ss->getOrCreateUniform("atmos_fKr4PI", osg::Uniform::FLOAT)->set(Kr4PI);
ss->getOrCreateUniform("atmos_fKm4PI", osg::Uniform::FLOAT)->set(Km4PI);
ss->getOrCreateUniform("atmos_fScale", osg::Uniform::FLOAT)->set(Scale);
ss->getOrCreateUniform("atmos_fScaleDepth", osg::Uniform::FLOAT)->set(RayleighScaleDepth);
ss->getOrCreateUniform("atmos_fScaleOverScaleDepth", osg::Uniform::FLOAT)->set(Scale / RayleighScaleDepth);
ss->getOrCreateUniform("atmos_g", osg::Uniform::FLOAT)->set(MPhase);
ss->getOrCreateUniform("atmos_g2", osg::Uniform::FLOAT)->set(MPhase * MPhase);
ss->getOrCreateUniform("atmos_nSamples", osg::Uniform::INT)->set(Samples);
ss->getOrCreateUniform("atmos_fSamples", osg::Uniform::FLOAT)->set((float)Samples);
ss->getOrCreateUniform("atmos_fWeather", osg::Uniform::FLOAT)->set(Weather);

«Переменные» - в кавычках, потому что на самом деле это всё просто константы, которые обычно не меняются во время работы приложения. Настоящих переменных только 2: одна – для задания положения камеры (которая здесь не описана) и другая - atmos_v3LightPos – для задания направления источника света.

Пару слов про задание положения камеры. В коде вершинного шейдера необходимо вычислять координаты вершин рельефа в мировой системе координат. Для этого нужна обратная матрица вида. Она передаётся через встроенную в OSG uniform-переменную:

uniform mat4 osg_ViewMatrixInverse;

Вот как она применяется для вычисления положения вершин в мировой системе координат:

vec3 v3Pos = (osg_ViewMatrixInverse * gl_ModelViewMatrix * gl_Vertex).xyz;

Эту же самую матрицу удобно применять для вычисления положения камеры:

vec3 v3Cam = osg_ViewMatrixInverse[3].xyz;

С этой матрицей связан один глюк, про который я ещё напишу.

Прозрачная Земля

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

1) Полусфера, которая использовалась для показа околоземного пространства, стала просвечивать сквозь глобус:

Атмосфера просвечивает сквозь глобус

Решение пришло сразу: нужно сначала отрендерить глобус, а потом уже полусферу. Тогда часть полусферы за глобусом не пройдет тест глубины и не будет нарисована. Это можно сделать, например, поместив рельеф в RenderBin с сильно отрицательным номером:

mapNode->getOrCreateStateSet()->setRenderBinDetails(-15, "SORT_FRONT_TO_BACK");

При этом атмосфера находится в рендербине с номером -15.

Примечание: рендербины рендерятся в порядке возрастания номеров. Изменяя номер рендербина можно контролировать порядок отрисовки объектов.

2) Однако это очевидное решение привело к новым проблемам. Часть полусферы стала наезжать на рельеф каким-то совершенно фантастическим образом. Такое ощущение, как будто дальняя плоскость отсечения скачет.

После изучения кода класса osgEarth::Util::SkyNode всё стало ясно. При рендеринге полусферы с атмосферой была отключена запись в буфер глубины, а также использовались свои собственные ближняя и дальняя плоскости отсечения. Такая схема позволила вычислять ближнюю и дальнюю плоскости отсечения для рельефа без учёта атмосферы.

После того, как я стал рендерить атмосферу после рельефа, это привело к следующему эффекту: при выполнении теста глубины для атмосферы значение глубины в буфере (полученное из рельефа) и значение глубины пикселя атмосферы соответствовали разным значениям z-расстояния до камеры. Глубина меньше - а расстояние до камеры больше. (Значение глубины приведено к диапазону [0;1] от ближней до дальней плоскости отсечения). Поэтому успешное прохождение теста глубины привело к тому, что части атмосферы рисовались поверх рельефа.

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

Проблема была решена. Но вскоре был обнаружен новый глюк: атмосфера некорректно отображалась в стереорежиме. Но об это в следующем посте.