суббота, 25 июня 2011 г.

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



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

Catalin Zima
Нам нужна переменная, которая будет хранить максимальное время жизни частицы. Если помните, текстура, содержащая данные о координатах, имеет по 4 значения float для каждой частицы, три – это пространственные координаты, а четверное значение – время жизни частицы. Сброс значений осуществляется в двух пиксельных шейдерах, они устанавливают позицию в новое исходное положение, время жизни задается случайно в диапазоне от 0 до maxLife, а начальная скорость в 0. Странное значение в шейдере ResetPositionPS (10.2484) – это просто случайная величина. Можно сказать, что мы сделали это для некоторого разнообразия. Она введена для того, чтобы немного внести непредсказуемости и хаоса в нашу системы частиц.
    float maxLife = 5.0f;
 
    float3 generateNewPosition(float2 uv)
    {
            float4 rand =  tex2D(randomSampler, uv);
            
            return float3(rand.x * 1024, 250, rand.y * 1024);
    }
    
    float4 ResetPositionsPS(in float2 uv : TEXCOORD0) : COLOR
    {
            return float4(generateNewPosition(uv),  
                          maxLife * frac( tex2D( randomSampler, 
                                                 10.2484 * uv).w) );
    }
    
    float4 ResetVelocitiesPS(in float2 uv : TEXCOORD0) : COLOR
    {
            return float4(0,0,0,0);
    }
 
    technique ResetPositions
    {
        pass P0
        {
            pixelShader  =  compile ps_3_0 ResetPositionsPS();
        }
    }
 
    technique ResetVelocities
    {
        pass P0
        {
            pixelShader  =  compile ps_3_0 ResetVelocitiesPS();
        }
    }
 

В функции generateNewPosition, мы установили высоту 250, так как реализовываем эффект “падения частиц”. Реализация этой функции может быть переписана, как захотите, к примеру, если мы заменим реализацию на следующую, то система частиц будет описана сферой.
    float3 generateNewPosition(float2 uv)
    {
        float3 rand = tex2D(randomSampler, uv);
        
        rand = normalize(rand);
        
        return float3(rand.x * 128, 
                      rand.y * 128, 
                      rand.z * 128);
    }
 

В этом приложении мы остановимся на реализации плоскости.
Как упоминалось ранее, нам необходим пиксельный шейдер, позволяющий передавать данные между несколькими целями визуализации, как раз этим будет заниматься следующий пиксельный шейдер.
    float4 CopyTexturePS(in float2 uv : TEXCOORD0) : COLOR
    {
        return tex2D(temporarySampler,uv);
    }
 
    technique CopyTexture
    {
        pass P0
        {
            pixelShader = compile ps_3_0 CopyTexturePS();
        }
    }

Для того чтобы анимация частиц у нас не зависела от частоты кадров, функция обновления должна быть в курсе о прошедшем времени с момента последнего обновления. Код обновления координат:
    float elapsedTime = 0.0f;
 
    float4 UpdatePositionsPS(in float2 uv : TEXCOORD0) : COLOR
    {
        float4 pos = tex2D(positionSampler, uv);
        
        // проверка на жизнь
        if (pos.w >= maxLife)
        {
            // если частица умерла, мы доложны её воскресить
            // перезапускаем время
            pos.w = 0;
            
            // рассчитываем новое местоположение
            pos.xyz = generateNewPosition(uv);
        } 
        else
        {
            // обновляем координаты частицы и время её жизни
            float4 velocity = tex2D(velocitySampler, uv);
            
            pos.xyz += elapsedTime * velocity;
            
            pos.w += elapsedTime;
        }
        return pos;
    }
 

В процессе обновления сначала мы сравниваем, не превышает ли время жизни частицы максимально допустимое значение, если превышает, то обнуляем его и генерируем новое местоположение для частицы. Если время жизни превышает максимальное значение, устанавливаем новую случайную скорость, в общем, обновлять скорости можете, как пожелаете. В этом месте вы можете переписывать код под Ваше собственное поведение частиц, для реализации различных эффектов. К примеру, в этом месте можно реализовать столкновения (collisions) или действие различных сил (гравитация, генератор частиц, ветер), передаваемых через параметры шейдера. Далее будет рассмотрен пример реализации.
В процессе обновления сначала мы сравниваем, не превышает ли время жизни частицы максимально допустимое значение, если превышает, то обнуляем его и генерируем новое местоположение для частицы. Если время жизни превышает максимальное значение, устанавливаем новую случайную скорость, короче обновлять скорости можете, как пожелаете. В этом месте вы можете переписывать код под ваше собственное поведение частиц, для реализации различных эффектов. К примеру, в этом месте можно реализовать столкновения (collisions) или действие различных сил (гравитация, генератор частиц, ветер), передаваемых через параметры шейдера. Далее будет рассмотрен пример реализации.
    float4 UpdateVelocitiesPS(in float2 uv : TEXCOORD0) : COLOR
    {
        float4 velocity = tex2D(velocitySampler, uv);
        
        float4 pos      = tex2D(positionSampler, uv);
 
 
        if (pos.w >= maxLife)
        {
            //  сбор скорости по псевдо-случайному значению
            float4 rand  = tex2D(randomSampler, 
                                 uv);
                                 
            float4 rand2 = tex2D(randomSampler, 
                                 uv + float2(rand.y,rand.w));
                                 
            velocity.xyz = rand2.xyz * 8 + 10.0 * rand.xyz;
        }
        else
        {
            //  гравитационное ускорение 
            //  подлежит модификации для Ваших собственных эффектов
            velocity.y -= 20.0 * elapsedTime;
        }
        return velocity;
    }
 

Все что нам остается сделать, это добавить техники и закончить с написанием HLSL.
    technique UpdatePositions
    {
        pass P0
        {
            pixelShader = compile ps_3_0 UpdatePositionsPS();
        }
    }
 
    technique UpdateVelocities
    {
        pass P0
        {
            pixelShader = compile ps_3_0 UpdateVelocitiesPS();
        }
    }

Возвращаемся к написаю C# кода. Давайте загрузим шейдеры в соответствующие переменные.
    protected override void LoadGraphicsContent(bool loadAllContent)
    {
        [...]
        
        physicsEffect = content.Load<Effect>(
                                            "Shaders\\ParticlePhysics");
                                            
        renderParticleEffect = content.Load<Effect>(
                                            "Shaders\\Particle");
    }

Так как мы очень часть будем использовать техники шейдера ParticlePhysics.fx, то вынесем их вызов в отдельный метод. В качестве параметров будем передавать имя используемой техники и цель визуализации, в которую необходимо записать результат. Мы переключаем технику, записываем данные во временную цель визуализации, затем используя технику «CopyTexture» передаем данные в необходимую цель визуализации. Если необходимо сбросить данные физики (флаг isPhysicsReset установлен в значение false), мы не можем вызывать метод positionRT.GetTexture(), так как это приведет к генерации ошибки, к примеру, если была потеря графического устройства, в этом случае цель визуализации просто очищается.
    private void DoPhysicsPass(string technique, 
                               RenderTarget2D resultTarget)
    {
        RenderTarget2D oldRT = 
                    graphics.GraphicsDevice.GetRenderTarget(0) 
                                                as RenderTarget2D;
                                                        
        DepthStencilBuffer oldDS = 
                    graphics.GraphicsDevice.DepthStencilBuffer;
 
        graphics.GraphicsDevice.DepthStencilBuffer = simulationDepthBuffer;
        graphics.GraphicsDevice.SetRenderTarget(0, temporaryRT);
 
        graphics.GraphicsDevice.Clear(
                          ClearOptions.Target | ClearOptions.DepthBuffer, 
                          Color.White, 
                          1, 
                          0);
 
        spriteBatch.Begin(SpriteBlendMode.None,
                          SpriteSortMode.Immediate,
                          SaveStateMode.None);
 
        physicsEffect.CurrentTechnique = physicsEffect.Techniques[technique];
                            
        physicsEffect.Begin();
 
        if (isPhysicsReset)
        {
            physicsEffect.Parameters["positionMap"].SetValue(
                                            positionRT.GetTexture() );
                                            
            physicsEffect.Parameters["velocityMap"].SetValue(
                                            velocityRT.GetTexture() );
        }
 
        physicsEffect.CurrentTechnique.Passes[0].Begin();
        
        //    positionMap and velocityMap передаются через параметры
        //    Нам нужно передать текстуру в spriteBatch.Draw(), 
        //    даже если не будем использовать некоторое время, 
        //    т.к. мы передаём randomTexture
        spriteBatch.Draw(randomTexture, 
                         new Rectangle(0, 
                                       0, 
                                       particleCount, 
                                       particleCount), 
                         Color.White);
                         
        physicsEffect.CurrentTechnique.Passes[0].End();
        physicsEffect.End();
 
        spriteBatch.End();
        graphics.GraphicsDevice.ResolveRenderTarget(0);
 
        graphics.GraphicsDevice.SetRenderTarget(0, resultTarget);
        
        
        spriteBatch.Begin(SpriteBlendMode.None, 
                          SpriteSortMode.Immediate, 
                          SaveStateMode.None);
 
        physicsEffect.CurrentTechnique = 
                                physicsEffect.Techniques["CopyTexture"];
        physicsEffect.Begin();
        physicsEffect.CurrentTechnique.Passes[0].Begin();
        
        spriteBatch.Draw(temporaryRT.GetTexture(), 
                         new Rectangle(0,  
                                       0,  
                                       particleCount,  
                                       particleCount),  
                         Color.White);
                         
        physicsEffect.CurrentTechnique.Passes[0].End();
        physicsEffect.End();
        
        spriteBatch.End();
        
        graphics.GraphicsDevice.ResolveRenderTarget(0);
        
        graphics.GraphicsDevice.SetRenderTarget(0, oldRT);
        graphics.GraphicsDevice.DepthStencilBuffer = oldDS;
    }

К примеру, если мы ходим использовать технику «UpdateVelocities» и результат обновления скоростей частиц записать в цель визуализации velocityRT, то вызов будет выглядеть следующим образом: DoPhysicsPass("UpdateVelocities", velocityRT).
Далее пишем метод симуляции системы частиц SimulateParticles, который в качестве параметра будет принимать игровое время. Если необходимо очистить данные физики, мы вызываем техники ResetPositions и ResetVelocities. На каждом кадре мы обновляем данные скоростей, а затем данные позиций частиц.
    private void SimulateParticles(GameTime gameTime)
    {
        physicsEffect.Parameters["elapsedTime"].SetValue(
                        (float) gameTime.ElapsedGameTime.TotalSeconds);
                        
        if (!isPhysicsReset)
        {
            DoPhysicsPass("ResetPositions",  positionRT);
            DoPhysicsPass("ResetVelocities", velocityRT);
            
            isPhysicsReset = true;
        }
        
        DoPhysicsPass("UpdateVelocities", velocityRT);
        DoPhysicsPass("UpdatePositions",  positionRT);
    }
 

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

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

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