четверг, 23 июня 2011 г.

Четыре метода использования Вершинных Текстур (Vertex Textures) Часть 4

 

Использование Вершинных Текстур в играх на платформе XNA (перевод)

Catalin Zima


Бонус: Освещение ландшафта
Как возможно Вы могли заметить, до сих пор мы рисовали ландшафт без освещения, сейчас мы будем это исправлять. Так как формирование формы поверхности ландшафта происходит в вершинном шейдере, то для расчета освещения данные о нормалях должны существовать до этапа формирования поверхности. Если бы у нас был статический ландшафт, то мы бы просто сформировали карту нормалей на этапе формирования карты высот и передавали бы их в шейдеры. Но при использовании динамически изменяемого ландшафта рельеф поверхности меняется практически непредсказуемым образом. Итак, для получения нормалей сетки ландшафта мы будет генерировать карту нормалей на основе интерполированной карты высот с использованием фильтра Собеля (Sobel Filter). Данная техника была позаимствована в одной из демосцен ATI. Откройте код шейдера TextureMorph.fx и добавьте следующий код:
    // Значение Normal Strength подбирается наугад, 
    // пока не добьетесь приемлемого результат 
    // Чем выше значение, тем более яркое освещение будет получаться
        
    float normalStrength = 8.0f;
 
    float4 ComputeNormalsPS(in float2 uv : TEXCOORD0) : COLOR
    {
        // левый верхний - top left
        float tl = abs(tex2D (textureSampler1, 
                              uv + texelSize * float2(-1, -1) ).x);
        // левый - left
        float  l = abs(tex2D (textureSampler1, 
                              uv + texelSize * float2(-1,  0) ).x);
        // левый нижний - bottom left
        float bl = abs(tex2D (textureSampler1, 
                              uv + texelSize * float2(-1,  1) ).x);
        // верхний - top
        float  t = abs(tex2D (textureSampler1, 
                              uv + texelSize * float2( 0, -1) ).x);
        // нижний - bottom
        float  b = abs(tex2D (textureSampler1, 
                              uv + texelSize * float2( 0,  1) ).x);
        // верхний правый - top right
        float tr = abs(tex2D (textureSampler1, 
                              uv + texelSize * float2( 1, -1) ).x);
        // правый - right
        float  r = abs(tex2D (textureSampler1, 
                              uv + texelSize * float2( 1,  0) ).x);
        // нижний правый - bottom right
        float br = abs(tex2D (textureSampler1, 
                              uv + texelSize * float2( 1,  1) ).x);
 
        // расчёт вектора dx using Sobel:
        //
        //           -1 0 1
        //           -2 0 2
        //           -1 0 1
        float dX = -tl - 2.0f * l - bl + tr + 2.0f * r + br;
 
        // расчёт вектора dy using Sobel:
        //
        //           -1 -2 -1
        //            0  0  0
        //            1  2  1
        float dY = -tl - 2.0f * t - tr + bl + 2.0f * b + br;
 
        // расчёт нормализованного вектора нормали - Normal
        float4 N = float4(normalize(float3(dX, 
                                           1.0f / normalStrength, 
                                           dY) ), 
                                           1.0f);
                          
        //преобразование из (-1.0 , 1.0) в (0.0 , 1.0);
        return N * 0.5f + 0.5f;
    }
 
    technique ComputeNormals
    {
        pass P0
        {
          pixelShader = compile ps_3_0 ComputeNormalsPS();
        }
    }

Этот код нужен для генерирования нормализованной карты. Теперь мы должны добавить код реализующий освещение непосредственно в шейдер рисования ландшафта. Открываем код шейдера VTFDisplacement.fx и добавляем в него вектор направления света, текстуру и самплер для нее. Так как обращение к текстуре будет осуществляться из пиксельного шейдера, то мы можем использовать билинейную фильтрацию.
float4 lightDirection = {1, -0.7, 1, 0};
    
    texture normalMap;
    sampler normalSampler = sampler_state
    {
        Texture   = <normalMap>;
        
        MipFilter = Linear;
        MinFilter = Linear;
        MagFilter = Linear;
        
        AddressU  = clamp;
        AddressV  = clamp;
    };

Нам нужно модифицировать пиксельный шейдер для реализации расчета освещения. Для расчета освещения мы будем использовать простую формулу L * N и немного подсвечивать сцену для эмуляции окружающего освещения на 0.2f.
float4 PixelShader(in float4 uv : TEXCOORD0, 
                       in float4 weights : TEXCOORD2) : COLOR
    {
        [...]
        
        float4 finalColor =  sand  * weights.x + 
                             grass * weights.y + 
                             rock  * weights.z + 
                             snow  * weights.w;  
                             
        // читаем данные нормали из карты и 
        // преобразовываем значения обратно в диапазон (-1, 1)
        float4 normal = normalize( 2.0f * (tex2D(normalSampler, uv) - 0.5f));   
        float4 light = normalize(-lightDirection);
        
        // dot product между векторами light and normal
        float ldn = dot(light, normal);
        
        ldn = max(0, ldn);
        // добавляем коэффициент окружающего освещения и перемножаем с           исходным цветом
        return finalColor * (0.2f + ldn);
    }

И последние изменения, которые надо произвести в классе Game1.cs. Нам нужна еще одна цель визуализации (RenderTarget2D) и код ее инициализации:
RenderTarget2D normalRenderTarget;
 
    protected override void LoadGraphicsContent(bool loadAllContent)
    {
        [...]
        
        normalRenderTarget = new RenderTarget2D(graphics.GraphicsDevice,256,256,1,SurfaceFormat.Color);                           
        [...]
    }
 

Метод расчета карты нормалей очень сильно похож на метод расчета карты высот (Render2TextureMorph). Его надо вызвать сразу же за методом расчета карты высот, , затем рассчитанную карту нормалей точно так же передаем в параметр шейдера отрисовки ландшафта.
    protected void Render2TextureNormalCompute()
    {
 
        RenderTarget2D oldRT = 
                graphics.GraphicsDevice.GetRenderTarget(0) as RenderTarget2D;
                                                  
        DepthStencilBuffer oldDS = graphics.GraphicsDevice.DepthStencilBuffer;
 
        graphics.GraphicsDevice.DepthStencilBuffer = morphDepthBuffer;
        graphics.GraphicsDevice.SetRenderTarget(0, normalRenderTarget);
 
        graphics.GraphicsDevice.Clear(Color.White);
 
        morphSpriteBatch.Begin(SpriteBlendMode.None,
                               SpriteSortMode.Immediate,
                               SaveStateMode.None);
                          
        // передаём новую карту высот для расчёта карты нормалей
        morphEffect.Parameters["textureMap1"].SetValue(
                                            morphRenderTarget.GetTexture());
 
        // выбираем технику для расчёта нормалей
        morphEffect.CurrentTechnique = morphEffect.Techniques["ComputeNormals"];
        
        //отрисовка
        morphEffect.Begin();
        morphEffect.CurrentTechnique.Passes[0].Begin();
 
        morphSpriteBatch.Draw(morphRenderTarget.GetTexture(), 
                              new Rectangle(0, 0, 256, 256), 
                              Color.White);
                              
        morphEffect.CurrentTechnique.Passes[0].End();
        morphEffect.End();
 
        morphSpriteBatch.End();
 
        graphics.GraphicsDevice.ResolveRenderTarget(0);
        
        graphics.GraphicsDevice.SetRenderTarget(0, oldRT);
        graphics.GraphicsDevice.DepthStencilBuffer = oldDS;
 
    }
    [...]
    protected override void Draw(GameTime gameTime)
    {
        graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
        
        Render2TextureMorph((float)Math.Sin(gameTime.TotalGameTime.TotalSeconds)* 0.5f + 0.5f);                            
        Render2TextureNormalCompute(); 
        [...]  
        gridEffect.Parameters["normalMap"].SetValue(normalRenderTarget.GetTexture());
        [...]
    }

И, в конце концов, у Вас должно получиться, что-то вроде этого:

clip_image002

Для дальнейшего улучшения, карту нормалей можно “смешать” с каким-нибудь сгенерированным шумом для эффекта небольшого бампа (bump) на поверхности.

В завершении второго раздела подведём краткие итоги. Мы рассмотрели реализацию динамического изменения ландшафта. Выяснили, что использование отрисовки в текстуру (Render-To-Texture) может значительно поднять производительность и открывает дополнительные возможности по реализации специфичных эффектов. Так же мы рассмотрели, как можно рассчитать карту нормалей в реальном времени на основе карты высот. В последующих разделах мы оставим в покое отрисовку ландшафтов и рассмотрим другие интересные эффекты.

Полный код этого раздела Вы сможете скачать отсюда: Chapter2.zip (ссылка умерла)

I. Системы частиц


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

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

Реализация основывается на двух целях визуализации определенного размера. В текстурах связанных с этими целями визуализации хранится информация для системы частиц. Каждый пиксель текстуры содержит информацию об одной частице: первая хранит информацию скорости частицы, другая местоположение в пространстве. Текстуры обновляются методом Render To Texture, затем, когда данные о частице будет записаны в текстуру, извлекается информация о позиции частицы через VTF. Кроме информации о координатах частицы, в четвёртой компоненте вектора (w) содержаться данные о времени жизни частицы. Когда время жизни частицы достигает определенного значения, она «умирает» и пиксель может использоваться для генерации новой частицы. Обновление данных координат и скорости частицы в текстурах происходит в пиксельном шейдере, примерно так же, как мы делали морфинг в предыдущем разделе.

Для начала, пожалуйста, добавьте файл flare.dds в папку Textures нашего проекта и установите процессор Texture(mipmapped) processor. Далее, открываем Game1.cs и добавляем следующие поля:
// используется для генерации рандомных значений, 
    // которые будут использоваться в различных шейдерах
    Texture2D randomTexture;
    
    // текстура дл рисования частиц
    Texture2D particleTexture;
 
    // Render target  - сюда будут писаться данные координат частицы
    RenderTarget2D positionRT;
    
    // Render target  - сюда будут писаться данные скорости частицы
    RenderTarget2D velocityRT;
    
    // временная render target, 
    // необходима, когда идёт запись в другие render targets
    RenderTarget2D temporaryRT;
    
    // буфер глубины 
    //    используется при обновлении системы частиц
    DepthStencilBuffer simulationDepthBuffer;
 
    // вершинный буфер, который содержит вершины системы частиц
    VertexBuffer particlesVB;
    
    // Эффект для отрисовки частиц
    Effect renderParticleEffect;
    
    // Эффект для обновления физики (местоположение, скорость)
    Effect physicsEffect;
    
    // Sprite batch используется при 2D отрисовке
    SpriteBatch spriteBatch;
    
    // если True данные о физике будут сохраняться 
    // Если False текстуры скорости и координат 
    // будут установлены опять в начальное состояние
    Boolean isPhysicsReset;
    
    // разрешение render targets. 
    // Кол-во частиц particleCount * particleCount 
    int particleCount = 512;

Комментариев нет:

Отправить комментарий