Войти
Totem 4 Engine BlogСтатьи

Totem Engine 4 Blog. Статья 4. Environment. Atmosphere Scattering.

Автор:

article_5_final | Totem Engine 4 Blog. Статья 4. Environment. Atmosphere Scattering.

Начнем с картинки для затравки, так как она отображается на главной.

Тема атмосферного рассеивания света хорошо описана в разных англоязычных источниках (смотри в конце статьи), на нашем языке даже есть статья от fzr125 тут на gamedev.ru, но она про предрассчитанный вариант техники, я же буду вести речь про полный рассчет рассеивания.

Итак, для чего нужна еще одна статья. Во-первых, чтобы я сам не забыл со временем как что делал. Во-вторых, написание и попытка объяснить кому-то усиливает понимание вопроса. Ну а, в-третьих, когда я делал все равно были моменты, до которых приходилось доходить самому.

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

Но начнем мы не с этого.

1. Полусфера неба.

Для начала нам нужно на что-то отобразить небо. Лучший вариант отображать на полусферу, центр которой X и Z (Y – высота) всегда соответсвует X и Z позиции наблюдателя.

Я выбрал на мой взгляд самый простой вариант — взять верхнюю половину от октаэдра ( кто не в курсе http://ru.wikipedia.org/wiki/%D0%9E%D0%BA%D1%82%D0%B0%D1%8D%D0%B4%D1%80 ) и каждый его треугольник снова триангулировать (разбить на несколько треугольников внутри) до нужной густоты сетки, а затем вытянуть все вершины по сферическим координатам.

Я не стал использовать стандартный алгоритм триангуляции, потому что он требует рекурсии, в место этого я вывел некую общую закономерность в конечном результате триангуляции. Смотрим на рисунок ниже.
article_5_triangles | Totem Engine 4 Blog. Статья 4. Environment. Atmosphere Scattering.

На нем приведены три треугольника, каждый следующий это триангулированный вариант предыдущего. Как видим сетка постоянно нарастает, но сохраняет порядок. При этом каждый предыдущий треугольник помещается в последующий абсолютно.

Вводим следующее понятие — линия. Это все треугольники, которые умещаются в одну горизонтальную линию, высотой в высоту треугольника (на рисунке синими цифрами). Длина линии — это число нижних треугольников в линии. На рисунке по порядку для нижней линии это 2, 4, 8. Число линий (уровней) вершин всегда на 1 больше, чем длина максимальной линии.

Я не буду строить треугольник отдельно, а потом его растягивать, я сделаю это сразу. Для этого надо понять другие картинки.
article_5_sphere | Totem Engine 4 Blog. Статья 4. Environment. Atmosphere Scattering. article_5_triangles_angles | Totem Engine 4 Blog. Статья 4. Environment. Atmosphere Scattering.

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

По этой картинке можно сгенерировать позицию вершин и нормаль довольно просто.
Мы генерируем вершины по слоям от нижнего к верхнему (по возрастанию Z). Каждая последующая вершина в слое в сферических координатах смещается на некоторый постоянный угол от начала координат. Этот угол зависит от номера вершины в слое и общем числе вершин.

Alpha = PI/2 / (VertexesCount-1);

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

//-----------------------------------------------------------------------
// создать буфер вершин
//-----------------------------------------------------------------------
bool TE4RenderSceneExtAtmosphere::CreateVertexBuffer()
{
  std::vector<TE4Point_N_Vertex> vVertexes;
  LPTE4Point_N_Vertex pVertex = NULL;

  size_t sLevel = 0, sNumInLevel = 0, i =-1;
  size_t sCurrentRaduis = 0;
  float fCurRadiusAlpha = 0.0f, fCurRadiusBetta = 0.0f;
  D3DXVECTOR3 v3Normal;

  // проход по уровням
  for( sLevel = 0; sLevel < m_sGridLevelsCount + 1; sLevel++ )
  {
    // выделяем место для новой строки
    vVertexes.resize( vVertexes.size() + m_sGridLevelsCount - sLevel + 1 );

    // проходим по вершинам в уровне
    for( sNumInLevel = 0; sNumInLevel < m_sGridLevelsCount - sLevel + 1; sNumInLevel++ )
    {
      // новая вершина
      i++;
      // получаем
      pVertex = &vVertexes[i];

      // текущий радиус
      sCurrentRaduis  = sNumInLevel + sLevel;

      // считаем угол смещения в радиусе ( сектор в радианах делим на число вершин )
      fCurRadiusAlpha  = sCurrentRaduis == 0 ? 0.0f : sLevel * cTE4_fPI_DIV2 / sCurrentRaduis;
      // угол смещения по высоте
      fCurRadiusBetta  = cTE4_fPI_DIV2 - sCurrentRaduis * cTE4_fPI_DIV2 / m_sGridLevelsCount;

      // позиция
      pVertex->X  = m_fGridRadius * cosf( fCurRadiusBetta ) * cosf( fCurRadiusAlpha );
      pVertex->Y  = m_fGridRadius * sinf( fCurRadiusBetta );
      pVertex->Z  = m_fGridRadius * cosf( fCurRadiusBetta ) * sinf( fCurRadiusAlpha );
      
      // считаем нормаль
      v3Normal.x  = -pVertex->X;
      v3Normal.y  = -pVertex->Y;
      v3Normal.z  = -pVertex->Z;

      D3DXVec3Normalize( &v3Normal, &v3Normal );

      pVertex->nX  = v3Normal.x;
      pVertex->nY  = v3Normal.y;
      pVertex->nZ  = v3Normal.z;
    }

  }

// ...
}

Нужно еще связать наши вершины в индексный буфер. Учитывая регулярную сетку мы это сделаем просто — проходим по слоям вершин, высчитываем первую вершину в текущем слое и первую в следующем. Зная нижние и верхние 4 вершины создаем пару треугольников. Двигаемся к следующему делая тоже самое. Исключение составляет последняя вершина — для нее создается только один треугольник.

//-----------------------------------------------------------------------
// создать буфер индексов
//-----------------------------------------------------------------------
bool TE4RenderSceneExtAtmosphere::CreateIndexBuffer()
{
  std::vector<UINT> vIndexes;

  size_t sLevel = 0, sNumInLevel = 0, i = 0;

  size_t sCurLevelBegin = 0, sNextLevelBegin = 0, sCurLevelVertexesCount = 0;

  // проход по уровням
  for( sLevel = 0; sLevel < m_sGridLevelsCount; sLevel++ )
  {
    // число вершин в уровне
    sCurLevelVertexesCount = m_sGridLevelsCount - sLevel + 1;

    // выделяем место для новой строки
    vIndexes.resize( vIndexes.size() + ( m_sGridLevelsCount - sLevel - 1) * 6 + 3 );

    // первая вершина следующего уровня
    sNextLevelBegin = sCurLevelBegin + sCurLevelVertexesCount;

    // проходим по вершинам в уровне кроме последней
    for( sNumInLevel = 0; sNumInLevel < m_sGridLevelsCount - sLevel; sNumInLevel++ )
    {
      // 2 грани на 4 точки
      vIndexes[i++] = sNextLevelBegin + sNumInLevel;
      vIndexes[i++] = sCurLevelBegin + sNumInLevel;
      vIndexes[i++] = sCurLevelBegin + sNumInLevel + 1;

      if( sNumInLevel != m_sGridLevelsCount - sLevel - 1 )
      {
        vIndexes[i++] = sCurLevelBegin + sNumInLevel + 1;
        vIndexes[i++] = sNextLevelBegin + sNumInLevel + 1;
        vIndexes[i++] = sNextLevelBegin + sNumInLevel;
      }
    }

    // заканчиваем
    sCurLevelBegin = sNextLevelBegin;
  }

// ...
}

Восьмая часть сферы готова. Нам нужно таких 4 штуки. Мы просто 4 раза отобразим их (а точнее столько, сколько видим в идеале) или один раз с инстансингом.

article_5_sphere_wareframe | Totem Engine 4 Blog. Статья 4. Environment. Atmosphere Scattering.

2. Рассеивание.

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

Я читал про Precomputed Scattering, но поначалу не понял принцип генерации текстур, поэтому начал с полного расчета всего алгоритма в шейдере. После реализации меня вполне устроил результат и производительность, поэтому я на нем и остановился.

Итак, как я уже говорил, теории по этой части в интернете полно, я ниже приведу пару ссылок. Описывать ее полноценно я не буду, потому что просто не разбираюсь достаточно в теме. Я опишу в большей степени как правильно настроить шейдер. Для этого смотрим картинку.
article_5_scattering | Totem Engine 4 Blog. Статья 4. Environment. Atmosphere Scattering.

Нужно сразу показать вершинный шейдер, именно в нем по позиции точки мы расчитываем цвет неба, в пиксельном шейдере мы потом из трех цветов вершин собираем цвет в текущем пикселе.

//--------------------------------------------------------------------------------------
// Global variables
//--------------------------------------------------------------------------------------
// matrixes
float4x4  g_MatrixViewProj;
float4x4  g_MatrixWorld;

// light
float3    g_LightDir;
float4    g_LightDiffuse;

// camera
float3    g_EyePos;

// atmospheric scattering

// planet inner radius, planet outer radius, height min, max
float4    g_RadiusInOutHeightMinMax;
// scattering coefficients : KrESun, KmESun, Kr4PI, Km4PI
float4    g_ScatterFactors;
// optical depth : scale, scaleDepth, ScaleOverScaleDepth, Iterations
float4    g_ScaleIter;
// gravity and Exposure : g, g ^ 2, exposure, 
float4    g_Gravity;
// wave length
float4    g_InvWaveLength;

//--------------------------------------------------------------------------------------
// Structures
//--------------------------------------------------------------------------------------
struct vertexInput 
{
    float3 position        : POSITION;
    float3 normal        : NORMAL;
};

struct vertexOutput 
{
    float4 hPosition      : POSITION;
  float4 ColorRayleigh    : TEXCOORD0;
  float4 ColorMie        : TEXCOORD1;
  float3 Direction      : TEXCOORD2;
};

//--------------------------------------------------------------------------------------
// Vertex support functions
//--------------------------------------------------------------------------------------
float RayIntersection( float3 f3Pos, float3 f3Dir, float fOuterRadius ) 
{
  float A = dot(f3Dir, f3Dir);
  float B = 2.0f *  dot(f3Pos, f3Dir);
  float C = dot( f3Pos, f3Pos ) - fOuterRadius * fOuterRadius;
  
  float t0 = (-B + sqrt(B*B - 4.0f * A * C) ) / 2.0f * A;
  float t1 = (-B - sqrt(B*B - 4.0f * A * C) ) / 2.0f * A;

  return t0 >= 0.0f ? t0 : t1;
}

float scale(float cos)
{
  float x = 1.0f - cos;
  return g_ScaleIter.y * exp(-0.00287f + x*(0.459f + x*(3.83f + x*(-6.80f + x*5.25f))));
}

//--------------------------------------------------------------------------------------
// Vertex shader
//--------------------------------------------------------------------------------------
vertexOutput VS_Scattering( vertexInput IN ) 
{
    vertexOutput OUT;

  // world space vertex position without camera position
  float3 f3WorldPos  = mul( float4( IN.position, 1.0f ), g_MatrixWorld );

  // camera always in atmosphere
  
  // camera always in center of sphere, only height change
  // position camera in world space
  float3 v3Camera  = float3( 0.0f, g_EyePos.y, 0.0f );
  // ray from camera to current vertex (sky pos) in begin world space
  float3 v3Ray  = normalize(f3WorldPos - v3Camera);
  
  // camera height part between Min and Max
  float fCameraHeight  = (g_EyePos.y - g_RadiusInOutHeightMinMax.z) / (g_RadiusInOutHeightMinMax.w - g_RadiusInOutHeightMinMax.z);
  // camera height in planet space
  fCameraHeight    = g_RadiusInOutHeightMinMax.x + fCameraHeight * (g_RadiusInOutHeightMinMax.y - g_RadiusInOutHeightMinMax.x);
  
  // star position in planet space
  float3 v3Start  = float3( 0.0f, fCameraHeight, 0.0f );
  
  float fDistance  = RayIntersection( v3Start, v3Ray, g_RadiusInOutHeightMinMax.y );
    
  // star angle
  float fStartAngle  = dot(v3Ray, v3Start) / fCameraHeight;
    
  // depth = exp(ScaleOverScaleDepth * ( inner radius - eye height )
  float fStartDepth  = exp(g_ScaleIter.z * ( g_RadiusInOutHeightMinMax.x - fCameraHeight ) );
  float fStartOffset = fStartDepth * scale(fStartAngle);

  // Initialize the scattering loop variables

  float fSampleLength = fDistance / g_ScaleIter.w;
  float fScaledLength = fSampleLength * g_ScaleIter.x;

  float3 v3SampleRay    = v3Ray * fSampleLength;
  float3 v3SamplePoint  = v3Start + v3SampleRay * 0.5;

  // Now loop through the sample points

  float3 v3FrontColor = float3(0.0, 0.0, 0.0);
  
  for(int i=0; i< (int)g_ScaleIter.w; i++) 
  {

              float fHeight = length(v3SamplePoint);
    // depth = exp(ScaleOverScaleDepth * ( inner radius - cur height )
              float fDepth = exp(g_ScaleIter.z * (g_RadiusInOutHeightMinMax.x - fHeight) );

              float fLightAngle = dot(-g_LightDir.xyz, v3SamplePoint) / fHeight;

              float fCameraAngle = dot(v3Ray, v3SamplePoint) / fHeight;

              float fScatter = (fStartOffset + fDepth * (scale(fLightAngle) - scale(fCameraAngle)));

              float3 v3Attenuate = exp(-fScatter * (g_InvWaveLength.xyz * g_ScatterFactors.z + g_ScatterFactors.w));

              v3FrontColor += v3Attenuate * (fDepth * fScaledLength);

              v3SamplePoint += v3SampleRay;
  }

  float4x4 mWorldAlignCamera  = g_MatrixWorld;
  mWorldAlignCamera._41_43  += g_EyePos.xz;
  
  // view space pos
        OUT.hPosition    = mul( float4(  IN.position, 1.0f ), mul( mWorldAlignCamera, g_MatrixViewProj ) );

  // Finally, scale the Mie and Rayleigh colors
  OUT.ColorRayleigh.xyz  = v3FrontColor * (g_InvWaveLength.xyz * g_ScatterFactors.x);
  OUT.ColorRayleigh.w    = 1.0f;
  
  OUT.ColorMie.xyz    = v3FrontColor * g_ScatterFactors.y;
  OUT.ColorMie.w      = 1.0f;

  OUT.Direction      = v3Camera - f3WorldPos;
    
    return OUT;
}

Если просто объяснять принцип, то суть в том, что мы работаем с некоторой планетой. Находясь в ее атмосфере, мы располагаемся между двумя ее радиусами — внутренний — это радиус от центра планеты до поверхности. Внешний — от центра планеты до края атмосферы. Для земли это две цифры: 6356.75 и 6456.55 км. Не имеет значение в какой величине они заданы, главное, что они используются для просчета масштаба в переменной  g_ScatterFactors.

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

Когда мы смотрим на определенную точку нашей полусферы неба, мы в физическом смысле смотрим на определенную точку атмосферы, при чем ближе к горизону мы смотрим, тем более отдаленная это точка атмосферы (смотри 3 источник).

Вектор от этой точки к позиции камеры указывает какие лучи в данный момент со стороны неба мы видим, при чем это могут быть не только прямые лучи, но и пришедшие из другой части неба. Они несколько раз преломились и в этоге легли на курс текущего вектора. За прямые лучи отвечает Mie Scattering, a за рассеяные Reileight Scattering.

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

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

Отображения результата в пиксельном шейдере

//--------------------------------------------------------------------------------------
// Pixel support functions
//--------------------------------------------------------------------------------------

float getRayleighPhase( float fCos2)
{
  return 0.75 * (1.0 + fCos2);
}

float getMiePhase( float fCos, float fCos2 )
{
  float3 t3;
  t3.x = 1.5f * ( (1.0f - g_Gravity.y) / (2.0f + g_Gravity.y) );
  t3.y = 1.0f + g_Gravity.y;
  t3.z = 2.0f * g_Gravity.x;
  
  return t3.x * (1.0f + fCos2) / pow(t3.y - t3.z * fCos, 1.5f);
}

//--------------------------------------------------------------------------------------
// Pixel shader
//--------------------------------------------------------------------------------------
float4 PS_Scattering( vertexOutput IN ) : COLOR
{
  
  float4 Color = float4( 1.0f, 1.0f, 1.0f, 1.0f );
  
  float2 fCos;
  fCos.x = dot(-g_LightDir.xyz, IN.Direction ) / length(IN.Direction);
  fCos.y = fCos.x * fCos.x;

//  Color.rgb = getRayleighPhase(fCos2) * IN.ColorRayleigh.rgb + getMiePhase(fCos, fCos2) * IN.ColorMie.rgb;
  // getRayleighPhase - HDR
  // getMiePhase - sun disk
  Color.rgb = getRayleighPhase(fCos.y) * IN.ColorRayleigh.rgb + getMiePhase(fCos.x, fCos.y) * IN.ColorMie.rgb;
  
  Color.a = 1.0f;
  
  return Color;
}

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

    <!--  // planet inner radius, planet outer radius, height min, max
    -->
    <asRadius    x="6356.75" y="6456.55" z="-100.0" w="2000.0" />
   
    <!--  // Scale = 1 / (fOuterRadius - fInnerRadius)
          // ScaleDepth  = (fOuterRadius - fInnerRadius)
          // ScaleOverScaleDepth = Scale / ScaleDepth
          // Iterations
    -->
    <!-- влияет на видимость солнца (оптическую толщину среды) -->
    <asScaleIter  x="0.01" y="0.25" z="0.04" w="10.0" />
   
    <!--  // RayMult = 0.002f;
          // MieMult = 0.0015f;
          // KrESun = fRayMult * SunIntensity;
          // KmESun = fMieMult * SunIntensity;
          // kr4PI = fRayMult * 4.0f * PI;
          // km4PI = fMieMult * 4.0f * PI;
         
          // 0.01
          // 0.02

    -->
    <asFactors  x="0.04" y="0.03" z="0.025" w="0.0188" />
   
    <!--  // g
          // g*g
    -->
    <asGravity  x="-0.991" y="0.982" z="0.0" w="0.0" />
   
    <!--  // r = 650 nm
          // g = 570 nm
          // b = 475 nm
          // InvWaveLength = 1.0 / powf( x, 4.0f );
    -->
    <asInvWaveLength  x="5.602" y="9.473" z="19.644" w="0.0" />

Картинку результата вы уже видели в начале статьи.


3. Источники.

1. GPU Gems 2. Chapter 16. Accurate Atmospheric Scattering.
http://http.developer.nvidia.com/GPUGems2/gpugems2_chapter16.html

2. Atmosphere Scattering from Alex Urbano
http://xnacommunity.codeplex.com/wikipage?title=Componente%20Scatter

3. Modeling Skylight and Aerial Perspective
http://developer.amd.com/media/gpu_assets/PreethamSig2003CourseNotes.pdf

#render, #Totem 4 Engine, #Игровые движки

8 апреля 2012

Комментарии [17]