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

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

 

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

Catalin Zima


Реализация морфинга в вершинном шейдере
Первый способ заключается в чтении данных высоты из второй карты высот в вершинном шейдере и интерполяции между этими значениями. Начнем мы с кода, написанного в первом разделе. Добавьте вторую карту высот height2.dds в проект, в папку Textures, затем в классе Game1.cs добавьте поле для текстуры и загрузите ее в методе LoadGraphicsContent.
Texture2D displacementTexture2;
  
    [...]
     protected override void LoadGraphicsContent(bool loadAllContent)
    {
        [...]     
        displacementTexture2 = 
                content.Load<Texture2D>("Textures\\height2");
    }

В файле шейдера (VTFDisplacement.fx) добавляем новый float параметр – morphFactor, а так же текстуру и самплер для второй карты высот.
float morphFactor = 0.0f;
    
    texture displacementMap2;
    sampler displacementSampler2 = sampler_state
    {
        Texture   = <displacementMap2>;
        
        MipFilter = Point;
        MinFilter = Point;
        MagFilter = Point;
        
        AddressU  = Clamp;
        AddressV  = Clamp;
    };
 

В вершинном шейдере будем читать данные высот из двух текстур, и интерполировать между этими двумя значениями, как сказано было ранее.
VS_OUTPUT Transform(VS_INPUT In)
    {
        [...]    
        float height1 = tex2Dlod_bilinear( displacementSampler, 
                                           float4(In.uv.xy,0,0));
 
        float height2 = tex2Dlod_bilinear( displacementSampler2, 
                                           float4(In.uv.xy,0,0));
 
        float height = lerp(height1, height2, morphFactor);
        [...]
    }

И последнее, что мы должны сделать, передать значения параметров morphFactor и displacementMap2 в шейдер из кода приложения. Идем опять в класс Game1.cs и в методе Draw добавляем следующие строчки. Воспользуемся функцией sin для зацикливания гладкого перехода между двумя картами высот.
gridEffect.Parameters["displacementMap2"].SetValue(displacementTexture2);
    
    gridEffect.Parameters["morphFactor"].SetValue( 
                    (float)Math.Sin( gameTime.TotalGameTime.TotalSeconds ) 
                     * 0.5f + 0.5f);
 

Когда мы запустим программу, мы увидим наш морфинг.

clip_image001

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


Морфинг с использованием отрисовки в текстуру

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

Для начала мы должны использовать код шейдера VTFDisplacement.fx в том состоянии, в котором он был в конце предыдущего раздела (уберите вторую карту высот и код реализующий интерполяцию). В папке Shaders создайте новый фал с именем TextureMorph.fx, откройте его. Нам надо добавить две текстуры, которые мы будем смешивать и самплеры для них.
texture textureMap1;
    sampler textureSampler1 : register(s0) = sampler_state
    {
       Texture   = (textureMap1);
       
       ADDRESSU  = WRAP;
       ADDRESSV  = WRAP;
       
       MAGFILTER = LINEAR;
       MINFILTER = LINEAR;
       MIPFILTER = LINEAR;
    };
 
    texture textureMap2;
    sampler textureSampler2 : register(s1) = sampler_state
    {
       Texture   = (textureMap2);
       
       ADDRESSU  = WRAP;
       ADDRESSV  = WRAP;
       
       MAGFILTER = LINEAR;
       MINFILTER = LINEAR;
       MIPFILTER = LINEAR;
    };

Нам надо добавить параметр morphFactor, и далее мы создаем пиксельный шейдер, в котором будем смешивать две текстуры. Мы интерполируем два цвета пикселя текстур на основе морф фактора, так же как мы это делали до этого в вершинном шейдере.
float morphFactor = 0.0f;
 
    float4 PixelShaderMorph(in float2 uv : TEXCOORD0) : COLOR
    {
     float4 color1 = tex2D(textureSampler1, uv);
     float4 color2 = tex2D(textureSampler2, uv);
     
     return lerp(color1, color2, morphFactor);
    }
 
    technique TextureMorph
    {
        pass P0
        {
            pixelShader = compile ps_3_0 PixelShaderMorph();
        }
    }

Для упрощения мы будем использовать SpriteBatcth для отрисовки нашей интерполированной текстуры. По этой причине для реализации нашего подхода TextureMorph нам необходим только пиксельный шейдер, так как работу с вершинами на себя берет SpriteBatch. Мы будем использовать SpriteBatcth в комбинации с нашим шейдером, так же как это реализовано в примере Sprite Effects Sample.

Далее откройте код класса Game1.cs и добавьте следующие поля в класс:
Effect morphEffect;
    
    RenderTarget2D morphRenderTarget;
    
    DepthStencilBuffer morphDepthBuffer;
    
    SpriteBatch morphSpriteBatch;

Переменная morphEffect будет использоваться для шейдера, написанного ранее (TextureMorph.fx), morphRenderTarget, morphDepthBuffer и morphSpriteBatch будут использованы для отрисовки в текстуру (Render To Texture) при создании новой карты высот. Далее в методе LoadGraphicsContent инициализируем все новые поля. В качестве размера используется значение 256, так как такой же размер у обеих карт высот.
morphEffect = content.Load<Effect>("Shaders\\TextureMorph");
    
    morphRenderTarget = new RenderTarget2D(
                            graphics.GraphicsDevice, 
                            256, 
                            256, 
                            1, 
                            SurfaceFormat.Single);
                            
    morphDepthBuffer = new DepthStencilBuffer(
                            graphics.GraphicsDevice, 
                            256, 
                            256, 
                            graphics.GraphicsDevice.DepthStencilBuffer.Format);
                            
    morphSpriteBatch = new SpriteBatch(graphics.GraphicsDevice);

Теперь давайте напишем новый метод, принимающий в качестве параметра значение с типом данных float. Он будет реализовывать отрисовку в текстуру для получения новой карты высот.
protected void Render2TextureMorph(float morphFactor)
    {

Для начала мы должны сохранить текущее значение RenderTarget (цель визуализации) и DepthStencilBuffer (стенсильный буфер глубины). В нашем случае RenderTarget имеет значение null, но если мы хотим это использовать в реальном проекте, в большинстве случаев целью визуализации может оказаться другой RenderTarget, использующийся для постобработки, letterboxing`a, или других целей. Так что предполодим, что мы не знаем эти значения и сохраним их. После этого мы устанавливаем свои RenderTarget and DepthBuffer.
        //сохраняем старый RenderTarget
        RenderTarget2D oldRT = 
                graphics.GraphicsDevice.GetRenderTarget(0) as RenderTarget2D;
                
        // сохраняем старый Depth Buffer
        DepthStencilBuffer oldDS = graphics.GraphicsDevice.DepthStencilBuffer;
        
        // устанавливаем наш depth buffer    
        graphics.GraphicsDevice.DepthStencilBuffer = morphDepthBuffer;
        
        // устанавливаем наш render target
        graphics.GraphicsDevice.SetRenderTarget(0, morphRenderTarget);
 

Затем мы очищаем RenderTarget, начинаем процесс рисования и передаем параметры в шейдер. Мы рисуем displacementTexture на всю поверхность RenderTarget. Процессом рисования управляет наш пиксельный шейдер.
graphics.GraphicsDevice.Clear(Color.White);
 
    morphSpriteBatch.Begin(SpriteBlendMode.None, 
                           SpriteSortMode.Immediate, 
                           SaveStateMode.None);
 
    morphEffect.Parameters["textureMap1"].SetValue(displacementTexture);
    morphEffect.Parameters["textureMap2"].SetValue(displacementTexture2);
    morphEffect.Parameters["morphFactor"].SetValue(morphFactor);
    
    morphEffect.CurrentTechnique = morphEffect.Techniques["TextureMorph"];
    
    morphEffect.Begin();
    morphEffect.CurrentTechnique.Passes[0].Begin();
 
    morphSpriteBatch.Draw(displacementTexture, 
                          new Rectangle(0, 0, 256, 256), 
                          Color.White);
 
    morphEffect.CurrentTechnique.Passes[0].End();
    morphEffect.End();
 
    morphSpriteBatch.End();

Далее мы должны переключить графическое устройство в режим, в котором можем забрать текстуру из цели визуализации и вернуть старые RenderTarget и DepthStencilBuffer на место.
        graphics.GraphicsDevice.ResolveRenderTarget(0);
        graphics.GraphicsDevice.SetRenderTarget(0, oldRT);
        graphics.GraphicsDevice.DepthStencilBuffer = oldDS;
    }



В методе Draw мы вызываем наш метод Render2TextureMorph, затем восстанавливаем состояния рендеринга (Render States), которые модифицировались для spriteBatch. Передаем в шейдер (gridEffect) параметр displacementMap новую модифицированную текстуру, которую в свою очередь получаем из цели визуализации morphRederTarget.GetTexture();
    protected override void Draw(GameTime gameTime)
    {
        Render2TextureMorph((float)Math.Sin(
                            gameTime.TotalGameTime.TotalSeconds) 
                            * 0.5f + 0.5f);
                                    
        graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
        
        graphics.GraphicsDevice.RenderState.CullMode = CullMode.None;
        graphics.GraphicsDevice.RenderState.DepthBufferEnable = true;
        
        [...]
        
        gridEffect.Parameters["displacementMap"].SetValue(
                                        morphRenderTarget.GetTexture());
                                        
        [...]
    }



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

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

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

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