Como obter um efeito de iluminação 2D suave?

18

Estou fazendo um jogo baseado em blocos 2D em XNA.

Atualmente, meu raio se parece com isso .

Como posso obtê-lo para olhar como este ?

Em vez de cada bloco ter sua própria tonalidade, ele possui uma sobreposição suave.

Estou assumindo algum tipo de sombreador e para passar os valores de iluminação dos ladrilhos circundantes para o sombreador, mas sou iniciante com sombreadores, então não tenho certeza.

Minha iluminação atual calcula a luz e depois a passa para a SpriteBatche desenha com o parâmetro tint. Cada bloco possui um Colorcálculo calculado antes do sorteio no meu algoritmo de iluminação, usado para a tonalidade.

Aqui está um exemplo de como atualmente calculo a iluminação (eu faço isso da esquerda, direita e inferior também, mas eu estava realmente cansado de fazer isso quadro a quadro ...)

insira a descrição da imagem aqui

Então, na verdade, obter e desenhar a luz até agora não é problema !

Eu já vi inúmeros tutoriais no nevoeiro da guerra e usando sobreposições circulares gradientes para criar um raio suave, mas eu já tenho um bom método para atribuir a cada bloco um valor de raio, ele só precisa ser suavizado entre eles.

Então, para rever

  • Calcular iluminação (concluído)
  • Draw Tiles (Concluído, sei que precisarei modificá-lo para o shader)
  • Ladrilhos de sombra (como passar valores e aplicar "gradiente")
Cyral
fonte

Respostas:

6

Que tal algo como isso?

Não desenhe sua iluminação pintando seus sprites de azulejos. Desenhe seus blocos apagados em um destino de renderização e depois as luzes do bloco em um segundo alvo de renderização, representando cada um como um retângulo em escala de cinza que cobre a área do bloco. Para renderizar a cena final, use um shader para combinar os dois destinos de renderização, escurecendo cada pixel do primeiro de acordo com o valor do segundo.

Isso produzirá exatamente o que você tem agora. Isso não ajuda, então vamos mudar um pouco.

Altere as dimensões do seu destino de renderização do lightmap para que cada bloco seja representado por um único pixel , em vez de uma área retangular. Ao compor a cena final, use um estado de amostrador com filtragem linear. Caso contrário, deixe tudo o mesmo.

Supondo que você tenha escrito seu shader corretamente, o lightmap deve ser efetivamente "ampliado" durante a composição. Isso proporcionará um bom efeito de gradiente gratuitamente através do amostrador de textura do dispositivo gráfico.

Você também pode cortar o sombreador e fazê-lo de maneira mais simples com um BlendState 'escurecido', mas precisaria experimentar com ele antes que eu pudesse fornecer os detalhes.

ATUALIZAR

Hoje tive tempo para zombar disso. A resposta acima reflete meu hábito de usar shaders como minha primeira resposta a tudo, mas nesse caso eles não são realmente necessários e seu uso complica desnecessariamente as coisas.

Como sugeri, você pode obter exatamente o mesmo efeito usando um BlendState personalizado. Especificamente, este BlendState personalizado:

BlendState Multiply = new BlendState()
{
    AlphaSourceBlend = Blend.DestinationAlpha,
    AlphaDestinationBlend = Blend.Zero,
    AlphaBlendFunction = BlendFunction.Add,
    ColorSourceBlend = Blend.DestinationColor,
    ColorDestinationBlend = Blend.Zero,
    ColorBlendFunction = BlendFunction.Add
}; 

A equação de mesclagem é

result = (source * sourceBlendFactor) blendFunction (dest * destBlendFactor)

Portanto, com o nosso BlendState personalizado, isso se torna

result = (lightmapColor * destinationColor) + (0)

O que significa que uma cor de origem em branco puro (1, 1, 1, 1) preservará a cor de destino, uma cor de origem em preto puro (0, 0, 0, 1) escurecerá a cor de destino em preto puro e qualquer um tom de cinza no meio escurecerá a cor de destino em uma quantidade intermediária.

Para colocar isso em prática, primeiro faça o que for necessário para criar seu mapa de luz:

var lightmap = GetLightmapRenderTarget();

Em seguida, basta desenhar sua cena apagada diretamente no backbuffer, como faria normalmente:

spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend);
/* draw the world here */
spriteBatch.End();

Em seguida, desenhe o mapa de luz usando o BlendState personalizado:

var offsetX = 0; // you'll need to set these values to whatever offset is necessary
var offsetY = 0; // to align the lightmap with the map tiles currently being drawn
var width = lightmapWidthInTiles * tileWidth;
var height = lightmapHeightInTiles * tileHeight;

spriteBatch.Begin(SpriteSortMode.Immediate, Multiply);
spriteBatch.Draw(lightmap, new Rectangle(offsetX, offsetY, width, height), Color.White);
spriteBatch.End();

Isso multiplicará a cor de destino (ladrilhos apagados) pela cor de origem (mapa de luz), escurecendo adequadamente os ladrilhos apagados e criando um efeito de gradiente como resultado da escala da textura do mapa de luz no tamanho necessário.

Cole Campbell
fonte
Parece bem simples, vou ver o que posso fazer mais tarde. Eu tentei algo semelhante antes (eu renderizei o lightmap sobre todo o resto e usei um shader blur, mas eu não consegui tornar o renderTarget "transparente", ele tinha uma sobreposição roxa, mas eu queria que ele visse a camada de azulejo
real
Depois de definir o destino da renderização no dispositivo, chame device.Clear (Color.Transparent);
Cole Campbell
Embora, nesse caso, valha a pena ressaltar que seu mapa de luz provavelmente deve ser limpo em branco ou preto, sendo aqueles com luz total e escuridão completa, respectivamente.
Cole Campbell
Eu acho que foi o que eu tentei antes, mas ainda estava roxo, bem. Vou investigar amanhã, quando tiver algum tempo para trabalhar nisso.
Cyral
Quase tudo deu certo, mas desaparece do preto para o branco, em vez do preto para o transparente. A parte do shader é o que eu estou confuso, como escrever uma para escurecer a cena não apagada original da cena nova.
Cyral
10

Ok, aqui está um método muito simples para criar um raio 2D simples e suave, renderizar em três passagens:

  • Passe 1: o mundo do jogo sem raios.
  • Passe 2: o relâmpago sem o mundo
  • Combinação do passe 1. e 2. que é exibido na tela.

No passe 1, você desenha todos os sprites e o terreno.

No passe 2, você desenha um segundo grupo de sprites que são as fontes de luz. Eles devem ser parecidos com este:
insira a descrição da imagem aqui
Inicialize o alvo de renderização com preto e desenhe esses sprites nele com a mistura Máxima ou Aditiva.

No passe 3, você combina os dois passes anteriores. Existem várias maneiras diferentes de como elas podem ser combinadas. Mas o método mais simples e menos artístico é combiná-los via Multiply. Isso ficaria assim:
insira a descrição da imagem aqui

API-Beast
fonte
O problema é que eu já tenho um sistema de iluminação e as luzes pontuais não funcionam aqui por causa de alguns objetos que precisam de luz para passar e outros não.
22412 Cyral
2
Bem, isso é mais complicado. Você precisará transmitir raios para obter sombras. Teraria usa grandes blocos para o terreno, então não há problema nisso. Você poderia usar a projeção de raios imprecisa, mas isso não pareceria melhor que a terraria e poderia caber ainda menos se você não bloquear os gráficos. Para uma técnica avançada e de boa aparência, existe este artigo: gamedev.net/page/resources/_/technical/…
API-Beast
2

O segundo link que você postou parece uma espécie de nevoeiro de guerra , existem algumas outras perguntas sobre isso

Isso me parece uma textura , onde você "apaga" os bits da sobreposição preta (configurando o alfa desses pixels como 0) à medida que o player avança.

Eu diria que a pessoa deve ter usado um tipo de pincel "apagar" onde o jogador explorou.

bobobobo
fonte
11
Não é névoa de guerra, calcula o raio de cada quadro. O jogo que apontei é semelhante ao Terraria.
Cyral
O que eu quis dizer é que poderia ser implementado semelhante a névoa da guerra
bobobobo
2

Vértices claros (cantos entre ladrilhos) em vez de ladrilhos. Misture a iluminação em cada bloco com base em seus quatro vértices.

Faça testes de linha de visão para cada vértice para determinar como está aceso (você pode fazer com que um bloco pare toda luz ou diminua a luz, por exemplo, conte quantas interseções em cada vértice combinam com a distância para calcular um valor de luz em vez de usar um teste binário puro visível / não visível).

Ao renderizar um bloco, envie o valor da luz para cada vértice para a GPU. Você pode usar facilmente um sombreador de fragmento para obter o valor da luz interpolada em cada fragmento no sprite do bloco para iluminar cada pixel suavemente. Já faz muito tempo desde que eu toquei no GLSL que não me sentiria confortável em fornecer um exemplo de código real, mas no pseudo-código seria tão simples quanto:

texture t_Sprite
vec2 in_UV
vec4 in_Light // coudl be one float; assuming you might want colored lights though

fragment shader() {
  output = sample2d(t_Sprite, in_UV) * in_Light;
}

O sombreador de vértice simplesmente precisa passar os valores de entrada para o sombreador de fragmento, nada complicado nem remotamente.

Você obterá uma iluminação ainda mais suave com mais vértices (por exemplo, se cada bloco for renderizado como quatro sub-blocos), mas isso pode não valer a pena, dependendo da qualidade que você deseja. Experimente da maneira mais simples primeiro.

Sean Middleditch
fonte
11
line-of sight tests to each vertex? Isso vai ser caro.
ashes999
Você pode otimizar com alguma inteligência. Para um jogo 2D, você pode fazer um número surpreendente de testes de raios contra uma grade rígida rapidamente. Em vez de um teste LOS direto, você poderia "fluir" (eu não consigo pensar no termo adequado agora; noite da cerveja) os valores da luz sobre os vértices, em vez de fazer testes de raio também.
Sean Middleditch
Usar testes de linha de visão complicaria ainda mais isso, eu já descobri a iluminação. Agora, quando envio os 4 vértices para o sombreador, como posso fazer isso? Sei que você não se sente confortável em fornecer um exemplo de código real, mas se eu pudesse obter um pouco mais de informações, isso seria bom. Também editei a pergunta com mais informações.
Cyral