Как рисовать траву

s1

Очень часто перед разработчиками игр стоит задача отрисовки меха, травы и т.п. Конечно, для фотореалистичного рендеринга у нас нет ни ресурсов, ни времени, но какое-никакое приближение мы сделать в силах. В этой статье я рассмотрю один из простейших методов отрисовки – послойную отрисовку. Для ее реализации нам понадобится шейдер (поэтому данный пример пока нельзя портировать на WP7) и две текстуры – собственно текстура с травой и программно генерируемая карта высот травинок. Во второй текстуре мы будем хранить высоты травинок и чем больше у нас разрешение этой текстуры, тем больше отдельных травинок можно отрисовать. В XNA 4 есть ограничение на максимальный размер текстур – 4096х4096 пикселей, так что мы можем рисовать вплоть до 16 миллионов травинок за раз. Карта высот у нас генерируется, основываясь на плотности поля.

Принцип работы

Я уже упоминал что метод этот простой, работает он так: рисуем много-много слоев с нашей текстурой, причем во время отрисовки слоя для каждого пикселя сравниваем его высоту со значением из карты высот.

Код метода генерирующего карту высот:
 

private void CreateHeightMap()

{

    //Площадь карты высот равна количеству отдельных травинок

    int square = grassMap.Width * grassMap.Height;

    Color[] colors = new Color[square];

    for (int i = 0; i < square; i++)

        colors[i] = Color.Transparent;

    int sqDensity = (int)(square * density);

    //Количество травинок, кончающихся на уровне

    int levelEndings = sqDensity / levelCount;

 

    for (int i = 0; i < sqDensity; i++)

    {

        int x = random.Next(grassMap.Width);

        int y = random.Next(grassMap.Height);

 

        float level = (float)i / (float)(levelEndings * levelCount);

        //Храним высоту травинки в красном канале карты высот

        colors[x * grassMap.Width + y] = new Color((byte)(level * 255), 0, 0, 255);

    }

    grassMap.SetData<Color>(colors);

}

 

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

s2

Далее приведен код шейдера. Шейдер относительно простой, к тому же он откомментирован :)

float CurrentLayer; //текущий слой - 0 для первого слоя, 1 для последнего [0,1]

float GrassHeight;   //Максимальная высота травинки

 

float3 Offset;       //Сдвиг травинок

 

float4x4 World;      //Матрица мира

float4x4 View;       //Матрица вида

float4x4 Projection; //Матрица проекции

 

 

texture GrassTexture;     //Карта высот

sampler GrassSampler = sampler_state

{

    Texture = (GrassTexture);

    MinFilter = Point;

    MagFilter = Point;

    MipFilter = Point;

    AddressU = Wrap;

    AddressV = Wrap;

};

 

texture Texture;     //Текстура травы

sampler DiffuseTextureSampler = sampler_state

{

    Texture = (Texture);

    MinFilter = Linear;

    MagFilter = Linear;

    MipFilter = Linear;

    AddressU = Wrap;

    AddressV = Wrap;

};

 

 

struct VertexShaderInput

{

    float3 Position : POSITION0;

    float3 Normal : NORMAL0;

    float2 TexCoord : TEXCOORD0;

};

 

struct VertexShaderOutput

{

    float4 Position : POSITION0;

    float2 TexCoord : TEXCOORD0;

};

 

VertexShaderOutput GrassVertexShader(VertexShaderInput input)

{

    VertexShaderOutput output;

    float3 pos;

     //Высчитываем позицию отдельного квада

    pos = input.Position + input.Normal * GrassHeight * CurrentLayer;

 

    float4 worldPosition = mul(float4(pos,1), World);

   

     //сдвигаем его относительно начала травинки по вектору offset

    float OffsetFactor = pow(CurrentLayer, 3);

    worldPosition.xyz +=Offset*OffsetFactor ;

   

   

    float4 viewPosition = mul(worldPosition, View);

    output.Position = mul(viewPosition, Projection);

 

    output.TexCoord = input.TexCoord;

    return output;

}

 

float4 GrassPixelShader(VertexShaderOutput input) : COLOR0

{

    float4 GrassData = tex2D(GrassSampler, input.TexCoord);

    float4 GrassColor = tex2D(DiffuseTextureSampler, input.TexCoord);

   

     //у основания травинки меньше света, чем у ее конца, поэтому немного затеним основание

    float shadow = lerp(0.3,1,CurrentLayer);

     GrassColor *= shadow;

   

    float GrassVisibility =(CurrentLayer > GrassData.r) ? 0 : GrassData.a;

     GrassColor.a = (CurrentLayer == 0) ? 1 : GrassVisibility;

    return GrassColor;

}

 

technique Grass

{

    pass Pass1

    {

        AlphaBlendEnable = true;

        SrcBlend = SRCALPHA;

        DestBlend = INVSRCALPHA;

        CullMode = None;

 

        VertexShader = compile vs_2_0 GrassVertexShader();

        PixelShader = compile ps_2_0 GrassPixelShader();

    }

}

 

Конечно, рисовать плоские квадраты покрытые травой быстро надоедает, поэтому в качестве бонуса привожу код отрисовки «травяной модели»:

public void DrawModel(Camera camera, GameTime gameTime, Model model, Matrix World)

{

    Matrix[] bones = new Matrix[model.Bones.Count];

    model.CopyAbsoluteBoneTransformsTo(bones);

 

    Vector3 offset = Vector3.Zero;

    offset.X = (float)Math.Sin(gameTime.TotalGameTime.TotalSeconds) * 0.125f;

    offset.Z = (float)Math.Cos(gameTime.TotalGameTime.TotalSeconds / 2) * 0.125f;

 

    grassEffect.Parameters["Offset"].SetValue(offset);

    grassEffect.Parameters["GrassHeight"].SetValue(grassHeight);

    grassEffect.Parameters["GrassTexture"].SetValue(grassMap);

 

    grassEffect.Parameters["View"].SetValue(camera.View);

    grassEffect.Parameters["Projection"].SetValue(camera.Projection);

 

    for (int i = 0; i < levelCount; i++)

    {

        grassEffect.Parameters["CurrentLayer"].SetValue((float)i / (float)levelCount);

        grassEffect.CurrentTechnique.Passes[0].Apply();

        foreach (ModelMesh mesh in model.Meshes)

        {

            grassEffect.Parameters["World"].SetValue(World * bones[mesh.ParentBone.Index]);

            foreach (ModelMeshPart meshpart in mesh.MeshParts)

            {

                grassEffect.Parameters["Texture"].SetValue(((BasicEffect)meshpart.Effect).Texture);

                GraphicsDevice.SetVertexBuffer(meshpart.VertexBuffer, meshpart.VertexOffset);

                GraphicsDevice.Indices = meshpart.IndexBuffer;

                GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, meshpart.VertexOffset, 0,

                    meshpart.NumVertices, meshpart.StartIndex, meshpart.PrimitiveCount);

            }

        }

    }

}

 

Производительность

Метод достаточно быстрый, я на своей не самой новой машине получал 60 FPS, рисуя 4 миллиона травинок с 30 слоями. Разумеется, в реальных играх кроме травы у нас есть еще куча других объектов, но все же метод дает нам неплохую картинку при маленьких затратах. Посчитайте сами: на эти 4 миллиона травинок мы рендерим всего-лишь 60 полигонов, сила в шейдерах)

Скачать исходный код к статье.