Vamos aprender XNA? – Parte 17 – Post Processing

Publicado em 23/08/2011

2


Olá pessoal, como estamos?

O TDC 2011 Floripa foi incrível. Foi muito bacana encontrar tantas pessoas legais, com tanto expertise em diferentes tecnologias. Entranto, evento passou e o blog voltou Smiley de boca aberta.

Hoje, vou mostrar como executar um processo chamado Post Processing. Para isso, vou usar um bocado de HLSL.

Considere dar uma olhada nos posts anteriores dessa série. Se desejar, baixe o código-fonte em https://github.com/ElemarJR/VamosAprenderXNA 

O que é Post Processing?

Post Processing é uma técnica que permite adicionar bons efeitos visuais aos nossos games.  Isso ocorre através do tratamento da imagem resultante do processo de renderização 3D. Ou seja, primeiro criamos a imagem correspondente ao quadro. Depois, aplicamos um “filtro” a imagem gerada chegando ao resultado que desejamos apresentar.

Nosso ponto de partida

Para o post de hoje, usaremos como ponto de partida o código produzido na parte 13.

image

Como fazer Post Processing?!

O processo para aplicarmos post processing implica no cumprimento das seguintes etapas:

  1. Criar um atributo “RenderTarget2D” onde será “gerada a textura”;
  2. Iniciar o objeto "RenderTarget2D” dentro do método LoadContent;
  3. Dentro do método Draw:
    1. Alterar, no objeto Device, o destino da renderização da tela (back buffer) para o objeto Render Target;
    2. Setar o background (cor de fundo) da textura;
    3. Desenhar todos os objetos normalmente;
    4. Alterar, no objeto Device, o destino da renderização para a tela (back buffer);
    5. Setar o background (cor de fundo) da tela;
    6. Configurar o objeto SpriteBatch para aplicação do efeito de PostProcessing;
    7. Pintar a textura na tela;

Vejamos agora, como fazer isso na prática.

Desenhando em uma textura

Como indicado, para podermos melhorar as imagens geradas em nosso game através de Post Processing precisamos, antes, direcionar a renderização para uma textura (imagem). Em XNA, fazemos isso através da criação de um objeto RenderTarget2D. Repare:

RenderTarget2D target;
protected override void LoadContent()
{
    var device = graphics.GraphicsDevice;
    target = new RenderTarget2D(device, 
        device.Viewport.Width, 
        device.Viewport.Height, 
        false,
        SurfaceFormat.Color,
        DepthFormat.Depth24);

    // ... restante do código não é significativo aqui
}

Como pode ver, o objeto RenderTarget2D é inicializado no método LoadContent. Repare que utilizei largura e altura da viewport como indicação para largura e altura do objeto RenderTarget.

Agora, vamos dar uma olhadinha no método Draw:

protected override void Draw(GameTime gameTime)
{
    // direcionando a renderização para uma textura
    GraphicsDevice.SetRenderTarget(target);
    GraphicsDevice.DepthStencilState = DepthStencilState.Default;
            
    // desenhando a cena normalmente
    GraphicsDevice.Clear(Color.CornflowerBlue);
    foreach (IDrawableModel model in models)
        model.Draw(camera.View, camera.Projection);

    // direcionando a renderização para a tela
    GraphicsDevice.SetRenderTarget(null);

    // desenhando a textura gerada acima
    GraphicsDevice.Clear(Color.White);
    spriteBatch.Begin();
    spriteBatch.Draw(target, Vector2.Zero, Color.White);
    spriteBatch.End();

    base.Draw(gameTime);
}

Optei por comentar o código indicando o que etá ocorrendo em cada parte do método. Ficou claro?

A técnica para pintar texturas é a mesma que indiquei na Parte 2.

Escrevendo um efeito para Post Processing

Para começar, vamos escrever um efeito que “não faz nada”. Ou seja, vamos escrever um efeito que não altera a textura que é renderizada (arquivo None.fx).

sampler TextureSampler;

struct PixelInput
{
	float2 TexCoord : TEXCOORD0;
};

float4 PixelShaderFunction(PixelInput input) : COLOR
{
	float4 color = tex2D(TextureSampler, input.TexCoord);
	return color;
}

technique Technique1
{
	pass Pass1
	{
		PixelShader = compile ps_2_0 PixelShaderFunction();
	}
}

Este efeito é muito simples. Considere:

  • criamos um sampler que será “setado” pelo objeto SpriteBatch (que faz os desenhos 2D);
  • dispensamos um vertex shader pois estamos desenhando uma textura simples (passada pelo Sprite Batch). Não há coordenadas;
  • criamos um PixelShader que obtém o pixel correspondente na textura;

Na prática, não mudamos nada (ainda);

Utilizando o Effect no Game

Para poder utilizar o Effect no Game, precisamos:

  • criar um atributo para armazenar o efeito;
  • carregar o efeito;
  • modificar o método Draw para considerar o efeito.

Comecemos pela carga do efeito:

Effect postProcessingEffect;
protected override void LoadContent()
{
    postProcessingEffect = Content.Load<Effect>(@"PostProcessing\None");
    // ...
}

Agora, vamos ver a alteração do Draw:

protected override void Draw(GameTime gameTime)
{
    // direcionando a renderização para uma textura
    GraphicsDevice.SetRenderTarget(target);
    GraphicsDevice.DepthStencilState = DepthStencilState.Default;

    // desenhando a cena normalmente
    GraphicsDevice.Clear(Color.CornflowerBlue);
    foreach (IDrawableModel model in models)
        model.Draw(camera.View, camera.Projection);

    // direcionando a renderização para a tela
    GraphicsDevice.SetRenderTarget(null);

    // desenhando a textura gerada acima
    GraphicsDevice.Clear(Color.White);
    spriteBatch.Begin(SpriteSortMode.Immediate, 
        BlendState.Opaque, 
        null, null, null, 
        postProcessingEffect)
        ;
    spriteBatch.Draw(target, Vector2.Zero, Color.White);
    spriteBatch.End();

    base.Draw(gameTime);
}

Como podemos perceber, a única modificação ocorreu na chamada do método Begin. Executando …

image

Como esperado, nada mudou .. Para isso, precisamos de um efeito que faça alguma coisa.

Night Effect

Vamos começar dando um aspecto de noite para nosso jogo.

image

O que eu fiz para criar esse aspecto foi descontar muito os elementos ver vermelho e verde de cada pixel e reforçar bastante o azul (Night.fx). Observe:

float4 PixelShaderFunction(PixelInput input) : COLOR
{
	float4 color = tex2D(TextureSampler, input.TexCoord);
	color.b *= 1.25;
	color.rg *= 0.25;
    return color;
}

Negative Effect

Para obter uma imagem “negativa”..

image

 

Simplesmente … (Negative.fx)

float4 PixelShaderFunction(PixelInput input) : COLOR
{
	return 1 - tex2D(TextureSampler, input.TexCoord);
}

Sharpen Effect

image

float4 PixelShaderFunction(PixelInput input) : COLOR
{
	float4 color = tex2D(TextureSampler, input.TexCoord);
	color += tex2D(TextureSampler, input.TexCoord - 0.0001) * 15.0f;
	color -= tex2D(TextureSampler, input.TexCoord + 0.0001) * 15.0f;
	return color; 
}

Emboss Effect

(Emboss.fx)

image

float4 PixelShaderFunction(PixelInput input) : COLOR
{
	float4 color = tex2D(TextureSampler, input.TexCoord);
	color.rgb = 0.5f;
	color.a = 1;
	color += tex2D(TextureSampler, input.TexCoord - 0.0001) * 15.0f;
	color -= tex2D(TextureSampler, input.TexCoord + 0.0001) * 15.0f;
	color = (color.r + color.g + color.b) / 3.0f;
	return color; 
}

GrayScale Effect

(GrayScale.fx)

image

float4 PixelShaderFunction(PixelInput input) : COLOR
{
	float4 color = tex2D(TextureSampler, input.TexCoord);
	color.rgb = dot(color.rgb, float3(0.3, 0.59, 0.11));
    return color;
}

Em metade da cena?!

image

Chalk Effect (giz)

(Chalk.fx)

image

 

Wavy Effect

(wavy.fx)

image

float4 PixelShaderFunction(PixelInput input) : COLOR
{
	float y = input.TexCoord.y;
	float x = input.TexCoord.x;
	y = y + (sin(x * 200) * 0.01);
	float4 color = tex2D(TextureSampler, float2(x,y));
	return color; 
}

Black and White Effect

(bw.fx)

image

float4 PixelShaderFunction(PixelInput input) : COLOR
{
	float4 color = tex2D(TextureSampler, input.TexCoord);
	color = (color.r + color.g + color.b) / 3.0f;

	if (color.r < 0.2 || color.r > 0.8) 
		color.rgb = 0.0f; 
	else 
		color.rgb = 1.0f; 
	return color; 
}

Por hoje, era isso!

Smiley piscando

Etiquetado:, ,
Publicado em: Post