Vamos aprender XNA? – Parte 16 – Projective Texturing

Publicado em 16/08/2011

2


Olá pessoal, como estamos?!

No post de hoje demonstro como criar um efeito projetor em XNA. Repare:

image

O logo está sendo projetado a partir da posição 1500,1500,1500 em direção a posição 0,150,0. Logo, qualquer objeto que seja “tocado” por esse vetor recebe a projeção.

image

Perceba que: se nossa nave “atravessar” a projeção, recebera também texturas.

image

Como você pode ver, não há teste de “cobertura”. A textura “atravessa” qualquer obstáculo.  (Efeito diferente será mostrado em outros posts).

image

Para gerar os efeitos de hoje, usei novamente HLSL (considere ver o que já escrevi usando essa técnica). Se gosta de XNA, talvez seja interessante ver os posts anteriores dessa série.

O que é Projective Texturing?

Projective Texturing é uma técnica que “projeta” uma textura através de uma cena, como uma imagem sendo projetada em objetos, dentro de uma sala, por um projetor real.

Esta técnica é importante para simular efeitos como “um projetor” ou para trabalhar com iluminação não circular (como em um spot).

Projetar texturas é relativamente simples. Se você está acompanhando a série, já viu quase todo o necessário para fazer isso:

  • cálculo de normais;
  • indicação de profundidade;
  • cálculo de luz;
  • aplicação de texturas.

A única diferença, agora, é que usamos diferentes matrizes view e projection para calcular a “posição na tela” da textura que está sendo projetada. Para ser mais claro:

  • calculamos a matriz view e projection para uma câmera virtual que está na posição do projetor apontando para o alvo;
  • pegamos as coordenadas de tela de cada pixel;
  • verificamos as cores correspondentes na textura que estamos projetando;
  • adicionamos “essa cor” naquela calculada pela técnica de shading que estivermos adotando.

Para o post de hoje, utilizo apenas iluminação simples, com specular, apresentada na parte 10.

Escrevendo o Effect

Para podermos gerar a projeção indicada, precisamos escrever um effect que suporte isso. Comecemos pelos “parâmetros” que nosso Effect aceita necessários para cálculo da iluminação simples:

float4x4 World;
float4x4 View;
float4x4 Projection;

float3 DiffuseColor = float3(1,1,1);
float3 AmbientColor = float3(.1, .1, .1);
float3 LightDirection = float3(1,1,1);
float3 LightColor = float3(0.9, 0.9, 0.9);

texture BasicTexture;

sampler BasicTextureSampler = sampler_state {
	texture = <BasicTexture>;
};

float SpecularPower = 32;
float3 SpecularColor = float3(1,1,1);
float3 CameraPosition;

bool TextureEnabled = false;

float ViewportWidth;
float ViewportHeight;

Aqui, nada diferente do que já haviamos visto na parte 10.

Agora, os parâmetros necessários para implementar nosso “projetor”.

float4x4 ProjectorViewProjection;
texture2D ProjectedTexture;

sampler2D ProjectorSampler = sampler_state
{
	texture = <ProjectedTexture>;
};

bool ProjectorEnabled = false;

O que temos?! Basicamente, temos um parâmetro para a textura que desejamos projetar, outro para a matriz ViewProjection (world) correspondente a “câmera” posicionada no ponto projetor.

Além disso, implementei três funções auxiliares. Observe:

float2 ProjectionToScreen(float4 position)
{
	float2 screenPos = position.xy / position.w;
	return 0.5f * (float2(screenPos.x, -screenPos.y) + 1);
}

float2 halfPixel()
{
	return 0.5f / float2(ViewportWidth, ViewportHeight);
}

float3 Project(float2 UV)
{
	if (UV.x < 0 || UV.x > 1 || UV.y < 0 || UV.y > 1)
		return float3(0,0,0);

	return tex2D(ProjectorSampler, UV);
}

Onde:

  1. ProjetionToScreen – Converte uma coordenada 3D para uma coordenada de tela (2D);
  2. halfPixel – calcula a dimensão correspondente a metade de um pixel na tela;
  3. Project – Recebe uma coordenada UV e garante que, na eventualidade do parâmetro estar fora das coordenadas (0 e 1), a cor resultante do sampler seja preto.

Agora, as estruturas de input e output para o VertexShader:

struct VertexShaderInput
{
	float4 Position : POSITION0;
	float2 UV : TEXCOORD0;
	float3 Normal : NORMAL0;
};

struct VertexShaderOutput
{
	float4 Position : POSITION0;
	float2 UV : TEXCOORD0;
	float3 Normal : TEXCOORD1;
	float3 ViewDirection: TEXCOORD2;

	float4 ProjectorScreenPosition : TEXCOORD4;
};

A novidade aqui é a posição do projetor. No mais, tudo igual ao que já usavamos para iluminação básica.

Por fim, Vertex e Pixel shaders:

VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
	VertexShaderOutput output;

	float4 worldPosition = mul(input.Position, World);
	float4 viewPosition = mul(worldPosition, View);
	output.Position = mul(viewPosition, Projection);

	output.UV = input.UV;
	output.Normal = mul(input.Normal, World); 
	output.ViewDirection = worldPosition - CameraPosition;

	output.ProjectorScreenPosition = mul(mul(input.Position, World), 
		ProjectorViewProjection);

	return output;
}



float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
	float3 color = DiffuseColor;

	if (TextureEnabled)
		color *= tex2D(BasicTextureSampler, input.UV);

	float3 lighting = AmbientColor;
	float3 lightDir = normalize(LightDirection);
	float3 normal = normalize(input.Normal);

	lighting += saturate(dot(lightDir, normal)) * LightColor;

	float3 refl = reflect(lightDir, normal);
	float3 view = normalize(input.ViewDirection);
	lighting += pow(saturate(dot(refl, view)), SpecularPower) * SpecularColor;
	
	float3 output = saturate(lighting) * color;

	if (ProjectorEnabled)
		output += Project(ProjectionToScreen(input.ProjectorScreenPosition) + halfPixel());
	
	return float4(output, 1);
}

A novidade, em ambos os shaders está no final.

  • Primeiro, calculo a posição de um ponto no mundo, conforme as matrizes da câmera onde está ocorrendo a projeção. Depois, transformo para a visão da “câmera” que está na emissão da textura.
  • Depois, calculo a coordenada UV correspondente a posição do ponto com relação a essa posição;
  • Usando essa coordenada UV, obtenho a cor do pixel correspondente na textura que estamos projetando;
  • Por fim, “adiciono” a cor da textura a cor do pixel sendo processado.

Configurando o Efeito (no game)

Como vimos, o effect tem forte dependência da matriz correspondente a “câmera” posicionada no projetor. Por isso, ao carregar o efeito, fazemos sua configuração. Observe:

protected override void LoadContent()
{
#if DEBUG
    this.Components.Add(new FpsGameComponent(this, graphics));
#endif
    effect = Content.Load<Effect>("projection");

    var viewport = graphics.GraphicsDevice.Viewport;
    effect.Parameters["ViewportWidth"].SetValue(viewport.Width);
    effect.Parameters["ViewportHeight"].SetValue(viewport.Width);

    var texture = Content.Load<Texture2D>("xnalogo");
    effect.Parameters["ProjectedTexture"].SetValue(texture);

    var scale = 3.0f;
    var projection = Matrix.CreateOrthographicOffCenter(
        (-texture.Width / 2) * scale ,
        (texture.Width / 2) * scale,
        (-texture.Height / 2) * scale,
        (texture.Height / 2) * scale,
        -100000, 100000);


    var view = Matrix.CreateLookAt(
        new Vector3(1500, 1500, 1500),
        new Vector3(0, 150, 0),
        Vector3.Up
        );

    effect.Parameters["ProjectorViewProjection"].SetValue(view * projection);
    effect.Parameters["ProjectorEnabled"].SetValue(true);

    LoadSpaceship();
    LoadTeapot();
    LoadGround();
}

Como pode perceber, crio uma matriz projection e uma view para minha câmera de acordo com minha textura, posição do projetor e target.

A textura que utilizo é essa:

xnalogo

Por hoje, era isso.

Smiley piscando

Etiquetado:, ,
Publicado em: Post