delinear o efeito do objeto

26

Como posso obter um efeito de contorno semelhante aos encontrados em League of Legends ou Diablo III?

Esboço de League of Legends Esboço de League of Legends Esboço de Diablo III

É feito usando um shader? Quão?
Prefiro respostas que não estejam vinculadas a nenhum mecanismo específico, mas que eu possa adaptar a qualquer mecanismo em que esteja trabalhando.

João Portela
fonte

Respostas:

19

Você vai ter que processar o objeto duas vezes em algum ponto. Você pode renderizar apenas os rostos voltados para a câmera uma vez e os rostos voltados para longe da câmera uma vez, mas tem suas vantagens e desvantagens.

A solução comum mais simples é feita renderizando o objeto duas vezes na mesma passagem:

  • Você usa um sombreador de vértice para inverter as normais do objeto e "ampliá-lo" pelo tamanho do contorno e um sombreador de fragmento para renderizá-lo na cor do contorno
  • Sobre essa renderização de contorno, você renderiza o objeto normalmente. A ordem z é geralmente automaticamente correta, mais ou menos, já que o contorno é feito pelas faces que estão na parte de trás do objeto, enquanto a própria figura é composta de faces voltadas para a câmera.

Isso é simples o suficiente para criar e implementar e evita truques de renderização para textura, mas tem algumas desvantagens visíveis:

  • O tamanho do contorno, se você não o escalar pela distância da câmera, variará. Objetos mais distantes terão um contorno menor do que os próximos. Claro, isso pode ser o que você realmente deseja .
  • O sombreador de vértice "explodido" não funciona muito bem para objetos complexos, como o esqueleto no seu exemplo, introduzindo facilmente artefatos de combate z na renderização. Para corrigi-lo, é necessário renderizar o objeto em duas passagens, mas evita que você reverta as normais.
  • O contorno e o objeto podem não funcionar muito bem quando outros objetos estão ocupando o mesmo espaço e, em geral, são um problema para acertar quando combinados com shaders de reflexão e refração.

A idéia básica para um sombreador se parece com a seguinte (Cg, para Unity - o código é um sombreador de toon ligeiramente modificado que encontrei em algum lugar e não anotei a fonte, portanto, é mais uma prova de conceito mal escrita do que uma shader de uso):

Shader "Basic Outline" {
    Properties {
        _Color ("Main Color", Color) = (.5,.5,.5,1)
        _OutlineColor ("Outline Color", Color) = (1,0.5,0,1)
        _Outline ("Outline width", Range (0.0, 0.1)) = .05
        _MainTex ("Base (RGB)", 2D) = "white" { }
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        Pass {
            Name "OUTLINE"
            Tags { "LightMode" = "Always" }
CGPROGRAM
#pragma exclude_renderers gles
#pragma exclude_renderers xbox360
#pragma vertex vert

struct appdata {
    float4 vertex;
    float3 normal;
};

struct v2f
{
    float4 pos : POSITION;
    float4 color : COLOR;
    float fog : FOGC;
};
float _Outline;
float4 _OutlineColor;
v2f vert(appdata v)
{
    v2f o;
    o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
    float3 norm = mul ((float3x3)UNITY_MATRIX_MV, v.normal);
    norm.x *= UNITY_MATRIX_P[0][0];
    norm.y *= UNITY_MATRIX_P[1][1];
    o.pos.xy += norm.xy * _Outline;
    o.fog = o.pos.z;
    o.color = _OutlineColor;
    return o;
}
ENDCG
            Cull Front
            ZWrite On
            ColorMask RGB
            Blend SrcAlpha OneMinusSrcAlpha
            SetTexture [_MainTex] { combine primary }
        }
        Pass {
        Name "BASE"
        Tags {"LightMode" = "Always"}
CGPROGRAM
#pragma fragment frag
#pragma vertex vert
#pragma fragmentoption ARB_fog_exp2
#pragma fragmentoption ARB_precision_hint_fastest
#include "UnityCG.cginc"

struct v2f {
    float4 pos : SV_POSITION;
    float2    uv            : TEXCOORD0;
    float3    viewDir        : TEXCOORD1;
    float3    normal        : TEXCOORD2;
}; 

v2f vert (appdata_base v)
{
    v2f o;
    o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
    o.normal = v.normal;
    o.uv = TRANSFORM_UV(0);
    o.viewDir = ObjSpaceViewDir( v.vertex );
    return o;
}

uniform float4 _Color;

uniform sampler2D _MainTex;
float4 frag (v2f i)  : COLOR
{
    half4 texcol = tex2D( _MainTex, i.uv );

    half3 ambient = texcol.rgb * (UNITY_LIGHTMODEL_AMBIENT.rgb);
    return float4( ambient, texcol.a * _Color.a );
}
ENDCG
    }
    }
    FallBack "Diffuse"
}

O outro método comum renderiza o objeto duas vezes também, mas evita o sombreador de vértice completamente. Por outro lado, não pode ser feito facilmente em uma única passagem e precisa renderizar para textura: renderize o objeto uma vez com um sombreador de fragmento "plano" na cor de contorno e use um borrão (ponderado) nessa renderização em espaço na tela e , em seguida, renderize o objeto normalmente.

Há também um terceiro método e, possivelmente, mais fácil de implementar, embora trate um pouco mais a GPU e faça com que seus artistas o matem enquanto você dorme, a menos que você facilite a geração: Os objetos têm o esboço como separado malha o tempo todo, apenas totalmente transparente ou movido para algum lugar onde não seja visto (como no subsolo) até que você precise

Martin Sojka
fonte
O buffer de estêncil não é uma abordagem comumente usada aqui?
EdA-qa mort-ora-y
11
@ edA-qamort-ora-y: Isso também poderia funcionar, mas nunca tentei essa abordagem, por isso não posso comentar. :) Se você tem um algoritmo funcional em mente, sinta-se à vontade para adicionar esse método como outra resposta.
Martin Sojka
Eu não sei muito mais sobre isso sozinho, apenas que os contornos são freqüentemente mencionados em referência aos buffers de estêncil. :) Parece que pode ser apenas uma versão mais baseada em hardware da sua primeira abordagem (duas passagens, primeira maior).
EdA-qa mort-ora-y
A abordagem do buffer de estêncil pode usar mais largura de banda na atualização e limpeza dos buffers de estêncil e requer várias passagens. A abordagem que as listas de Martin podem ser feitas em uma passagem em alguns casos limitados, duas no máximo, e requer uma sobrecarga mínima de largura de banda.
21812 Sean Sean Middleditch
Essa abordagem (ampliando o tamanho ao longo das normais) não funciona com objetos como cubos (especialmente com a câmera orto). Alguma solução para isso?
NPS
4

Além da resposta de Martin Sojkas, para objetos estáticos (ou sprites) você pode se safar com algo mais simples.

Você pode salvar o mesmo sprite, mas com o contorno no atlas de textura ou em outra textura, o que facilita a troca. Isso também permitirá que você faça contornos personalizados que sejam mais atraentes visualmente ou que pareçam diferentes.

O outro método é salvar o sprite como uma forma de uma cor ligeiramente maior e renderizá-la imediatamente antes da renderização do sprite. Isso permitirá alterar facilmente a cor da seleção e talvez você não precise de tantas formas de cores diferentes quanto de sprites de contorno com o método nº 1.

Ambos aumentarão sua pegada de memória.

Darcara
fonte
4

Conforme apontado nos comentários à resposta de Martin Sojka, um efeito semelhante também pode ser alcançado utilizando o buffer de estêncil ou profundidade, conforme detalhado por Max McGuire no FlipCode:

http://www.flipcode.com/archives/Object_Outlining.shtml

Basicamente, está desenhando uma versão em wireframe do modelo que você deseja delinear com maior largura de linha (ou, caso isso não seja possível, como no D3D, por exemplo, usando quads voltados para a câmera para linhas) enquanto define o buffer de estêncil para um valor constante.

Essa abordagem pode ser um pouco antiquada usando o OpenGL de hoje e, para que o contorno do objeto seja embaçado, a renderização na textura ainda será necessária.

Koarl
fonte