Как рисовать траву
Очень часто перед разработчиками игр стоит задача отрисовки меха, травы и т.п. Конечно, для фотореалистичного рендеринга у нас нет ни ресурсов, ни времени, но какое-никакое приближение мы сделать в силах. В этой статье я рассмотрю один из простейших методов отрисовки – послойную отрисовку. Для ее реализации нам понадобится шейдер (поэтому данный пример пока нельзя портировать на 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);
}
На нижеприведенном скриншоте видно, что из себя представляет каждая травинка
Далее приведен код шейдера. Шейдер относительно простой, к тому же он откомментирован :)
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 полигонов, сила в шейдерах)
Скачать исходный код к статье.