Como posso desenhar contornos em torno de modelos 3D?

47

Como posso desenhar contornos em torno de modelos 3D? Estou me referindo a algo como os efeitos de um jogo Pokemon recente, que parece ter um contorno de pixel único em torno deles:

insira a descrição da imagem aqui insira a descrição da imagem aqui

portal místico
fonte
Você está usando o OpenGL? Nesse caso, você deve pesquisar no google como desenhar contornos para um modelo com o OpenGL.
OxySoft
1
Se você está se referindo a essas imagens particulares que você colocou lá dentro, eu posso dizer com uma certeza de 95% que aqueles são mão tirada 2D sprites, e não modelos 3D
Panda pijama
3
@PandaPajama: Não, esses são quase certamente modelos 3D. Há alguma negligência no que deveria ser linhas difíceis em alguns quadros que eu não esperaria de sprites desenhados à mão, e de qualquer maneira é basicamente como os modelos 3D do jogo se parecem. Suponho que não posso garantir 100% para essas imagens específicas, mas não consigo imaginar por que alguém iria se esforçar para falsificá-las.
CA McCann
Que jogo é esse, especificamente? Parece lindo.
Vegard
@Vegard A criatura com um nabo nas costas é um bulbasauro do jogo Pokémon.
Damian Yerrick

Respostas:

28

Não acho que nenhuma das outras respostas aqui atinja o efeito no Pokémon X / Y. Não sei exatamente como é feito, mas descobri uma maneira que parece muito com o que eles fazem no jogo.

No Pokémon X / Y, os contornos são desenhados ao redor das bordas da silhueta e em outras bordas que não são da silhueta (como onde as orelhas de Raichu encontram sua cabeça na captura de tela a seguir).

Raichu

Olhando para a malha de Raichu no Blender, você pode ver que a orelha (destacada em laranja acima) é apenas um objeto separado e desconectado que cruza a cabeça, criando uma mudança abrupta nas normais da superfície.

Com base nisso, tentei gerar o esboço com base nas normais, o que requer renderização em duas passagens:

Primeira passagem : renderize o modelo (texturizado e sombreado a cel) sem os contornos e renderize os normais de espaço da câmera para um segundo destino de renderização.

Segunda passagem : faça um filtro de detecção de borda em tela cheia sobre os valores normais da primeira passagem.

As duas primeiras imagens abaixo mostram as saídas da primeira passagem. O terceiro é o esboço por si só, e o último é o resultado final combinado.

Dratini

Aqui está o sombreador OpenGL que usei para detecção de borda na segunda passagem. É o melhor que pude apresentar, mas pode haver uma maneira melhor. Provavelmente também não está muito bem otimizado.

// first render target from the first pass
uniform sampler2D uTexColor;
// second render target from the first pass
uniform sampler2D uTexNormals;

uniform vec2 uResolution;

in vec2 fsInUV;

out vec4 fsOut0;

void main(void)
{
  float dx = 1.0 / uResolution.x;
  float dy = 1.0 / uResolution.y;

  vec3 center = sampleNrm( uTexNormals, vec2(0.0, 0.0) );

  // sampling just these 3 neighboring fragments keeps the outline thin.
  vec3 top = sampleNrm( uTexNormals, vec2(0.0, dy) );
  vec3 topRight = sampleNrm( uTexNormals, vec2(dx, dy) );
  vec3 right = sampleNrm( uTexNormals, vec2(dx, 0.0) );

  // the rest is pretty arbitrary, but seemed to give me the
  // best-looking results for whatever reason.

  vec3 t = center - top;
  vec3 r = center - right;
  vec3 tr = center - topRight;

  t = abs( t );
  r = abs( r );
  tr = abs( tr );

  float n;
  n = max( n, t.x );
  n = max( n, t.y );
  n = max( n, t.z );
  n = max( n, r.x );
  n = max( n, r.y );
  n = max( n, r.z );
  n = max( n, tr.x );
  n = max( n, tr.y );
  n = max( n, tr.z );

  // threshold and scale.
  n = 1.0 - clamp( clamp((n * 2.0) - 0.8, 0.0, 1.0) * 1.5, 0.0, 1.0 );

  fsOut0.rgb = texture(uTexColor, fsInUV).rgb * (0.1 + 0.9*n);
}

E antes de renderizar a primeira passagem, limpo o alvo de renderização dos normais para um vetor voltado para longe da câmera:

glDrawBuffer( GL_COLOR_ATTACHMENT1 );
Vec3f clearVec( 0.0, 0.0, -1.0f );
// from normalized vector to rgb color; from [-1,1] to [0,1]
clearVec = (clearVec + Vec3f(1.0f, 1.0f, 1.0f)) * 0.5f;
glClearColor( clearVec.x, clearVec.y, clearVec.z, 0.0f );
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

Li em algum lugar (colocarei um link nos comentários) que o Nintendo 3DS usa um pipeline de função fixa em vez de sombreadores, então acho que não pode ser exatamente assim que é feito no jogo, mas por enquanto eu ' Estou convencido de que meu método está próximo o suficiente.

KTC
fonte
Informações sobre o hardware do Nintendo 3DS: link
KTC
Solução muito boa! Como você levaria em consideração a profundidade em seu shader? (No caso de um avião em frente da outra, por exemplo, ambos têm a mesma normal, de modo nenhum esboço irá ser desenhada)
Ingham
@ingham Esse caso aparece com pouca frequência em um personagem orgânico, que eu não precisava lidar com isso, e parece que o jogo real também não. No jogo real, às vezes você pode ver o contorno desaparecer quando os normais são os mesmos, mas não acho que as pessoas normalmente notariam.
KTC
Estou um pouco cético sobre o 3DS ser cabível para executar efeitos de tela cheia baseados em shader como esse. Seu suporte a shader é rudimentar (se houver algum).
Tara
17

Esse efeito é particularmente comum em jogos que usam efeitos de sombreamento de cel, mas na verdade é algo que pode ser aplicado independentemente do estilo de sombreamento de cel.

O que você está descrevendo é chamado de "renderização de borda do recurso" e, em geral, é o processo de destacar os vários contornos e contornos de um modelo. Existem muitas técnicas disponíveis e muitos trabalhos sobre o assunto.

Uma técnica simples é renderizar apenas a borda da silhueta, o contorno mais externo. Isso pode ser feito simplesmente como renderizar o modelo original com uma gravação de estêncil e, em seguida, renderizá-lo novamente no modo de estrutura de arame grossa, somente onde não havia valor de estêncil. Veja aqui um exemplo de implementação.

Porém, isso não destacará o contorno interno e as bordas do vinco (como mostrado nas fotos). Geralmente, para fazer isso de forma eficaz, é necessário extrair informações sobre as arestas da malha (com base nas descontinuidades nas faces normais de ambos os lados da aresta e criar uma estrutura de dados representando cada aresta.

Você pode escrever shaders para extrudar ou renderizar essas arestas como geometria regular sobre o modelo base (ou em conjunto com ele). A posição de uma aresta e as normais das faces adjacentes em relação ao vetor de vista são usadas para determinar se uma aresta específica pode ser desenhada.

Você pode encontrar mais discussões, detalhes e documentos com vários exemplos na internet. Por exemplo:

Josh
fonte
1
Posso confirmar que o método stencil (do flipcode.com) funciona e parece muito bom. Você pode especificar a espessura das coordenadas da tela para que a espessura do contorno não dependa do tamanho do modelo (nem da forma do modelo).
Vegard
1
Uma técnica que você não mencionou é o efeito de borda-shading de pós-processamento, muitas vezes usado em conjunto com cel-shading que procura pixels com alta dz/dxe / oudz/dy
bcrist
8

A maneira mais simples de fazer isso, comum em hardware mais antigo antes dos sombreadores de pixel / fragmento e ainda usada no celular, é duplicar o modelo, reverter a ordem de enrolamento do vértice para que o modelo seja exibido de dentro para fora (ou, se desejar, você pode faça isso na sua ferramenta de criação de ativos 3D, digamos o Blender, alternando os normais da superfície - a mesma coisa), depois expanda a duplicata inteira levemente em torno do centro e, finalmente, colore / texture essa duplicata completamente preta. Isso resulta em contornos do modelo original, se for um modelo simples, como um cubo. Para modelos mais complexos com formas côncavas (como na imagem abaixo), é necessário ajustar manualmente o modelo duplicado para ser um pouco "mais gordo" do que o seu homólogo original, como um Minkowski Sumem 3D. Você pode começar empurrando cada vértice um pouco ao longo de seu normal para formar a malha de estrutura de tópicos, como a transformação Shrink / Fatten do Blender faz.

As abordagens de espaço na tela / sombreamento de pixels tendem a ser mais lentas e difíceis de implementar bem , mas a OTOH não duplica o número de vértices em seu mundo. Portanto, se você estiver realizando um trabalho altamente polivalente, é melhor optar por essa abordagem. Dada consola moderna e capacidade de área de trabalho para o processamento de geometria, eu não me preocuparia com um fator de 2 em tudo . Estilo dos desenhos animados = baixo poli, com certeza, portanto, duplicar a geometria é mais fácil.

Você pode testar o efeito, por exemplo, no Blender, sem tocar em nenhum código. Os contornos devem se parecer com a imagem abaixo; observe como alguns são internos, por exemplo, debaixo do braço. Mais detalhes aqui .

insira a descrição da imagem aqui.

Engenheiro
fonte
1
Você poderia explicar como "expandir a duplicata inteira levemente em torno de seu centro" está em conformidade com esta figura, porque a simples escala em torno do centro não funcionaria para braços e outras partes que não são concêntricas, também não funcionará para qualquer modelo que tenha buracos nele.
Kromster diz apoio Monica
@KromStern Em alguns casos , subconjuntos de vértices precisam ser dimensionados manualmente para acomodar. Resposta alterada.
Engenheiro
1
É comum para deslocar os vértices ao longo de sua superfície locais normal, mas isso pode fazer com que o esboço expandiu malha de divisão ao longo das bordas duras
DMGregory
Obrigado! Acho que não faz sentido inverter as normais, já que a duplicata terá uma cor sólida e plana (ou seja, nenhum cálculo de iluminação sofisticado que dependa das normais). Consegui o mesmo efeito escalando, colorindo a superfície e, em seguida, selecionando as faces frontais da duplicata.
Jet Blue
6

Para modelos suaves (muito importantes), esse efeito é bastante simples. No seu fragmento / pixel shader, você precisará do normal do fragmento sendo sombreado. Se estiver muito próximo da perpendicular ( dot(surface_normal,view_vector) <= .01- pode ser necessário jogar com esse limite), pinte o fragmento de preto em vez da cor usual.

Essa abordagem "consome" um pouco do modelo para fazer o esboço. Isso pode ou não ser o que você deseja. É muito difícil dizer pela imagem de Pokemon se é isso que está sendo feito. Depende se você espera que o contorno seja incluído em qualquer silhueta do personagem ou se prefere que o contorno inclua a silhueta (o que requer uma técnica diferente).

O destaque estará em qualquer parte da superfície onde ele transitar de frente para trás, incluindo "bordas internas" (como as pernas do Pokémon verde ou a cabeça - algumas outras técnicas não adicionariam nenhum contorno àquelas )

Objetos com arestas duras e não suaves (como um cubo) não receberão destaque nos locais desejados com esta abordagem. Isso significa que essa abordagem não é uma opção em alguns casos; Não faço ideia se os modelos de Pokemon são lisos ou não.

Sean Middleditch
fonte
5

A maneira mais comum de fazer isso é através de um segundo passe de renderização no seu modelo. Essencialmente, duplique e vire os normais, e coloque-o em um shader de vértice. No sombreador, dimensione cada vértice ao longo do seu normal. No sombreador de pixels / fragmentos, desenhe preto. Isso fornecerá contornos externos e internos, como em torno dos lábios, olhos etc. Essa é realmente uma chamada de compra bastante barata, se nada mais for geralmente mais barato do que o pós-processamento da linha, dependendo do número de modelos e de sua complexidade. O Guilty Gear Xrd usa esse método porque é fácil controlar a espessura da linha através da cor do vértice.

A segunda maneira de fazer linhas internas eu aprendi com o mesmo jogo. No seu mapa UV, alinhe sua textura ao longo do eixo u ou v, principalmente nas áreas em que você deseja uma linha interna. Desenhe uma linha preta ao longo de um dos eixos e mova as coordenadas UV para dentro ou fora dessa linha para criar a linha interna.

Veja o vídeo da GDC para uma explicação melhor: https://www.youtube.com/watch?v=yhGjCzxJV3E

Andrew Q
fonte
5

Uma das maneiras de fazer um esboço é usar os vetores normais de nossos modelos. Os vetores normais são vetores perpendiculares à sua superfície (apontando para fora da superfície). O truque aqui é dividir seu modelo de personagem em duas partes. Os vértices que estão voltados para a câmera e os vértices que estão voltados para fora da câmera. Nós os chamaremos FRONT e BACK respectivamente.

Para o esboço, pegamos nossos vértices BACK e os movemos levemente na direção de seus vetores normais. Pense nisso como tornar a parte do personagem que está voltada para a câmera um pouco mais gorda. Depois disso, atribuímos a eles uma cor de nossa escolha e temos um bom esboço.

insira a descrição da imagem aqui

Shader "Custom/OutlineShader" {
    Properties {
        _MainTex ("Base (RGB)", 2D) = "white" {}
        _Outline("Outline Thickness", Range(0.0, 0.3)) = 0.002
        _OutlineColor("Outline Color", Color) = (0,0,0,1)
    }

    CGINCLUDE
    #include "UnityCG.cginc"

    sampler2D _MainTex;
    half4 _MainTex_ST;

    half _Outline;
    half4 _OutlineColor;

    struct appdata {
        half4 vertex : POSITION;
        half4 uv : TEXCOORD0;
        half3 normal : NORMAL;
        fixed4 color : COLOR;
    };

    struct v2f {
        half4 pos : POSITION;
        half2 uv : TEXCOORD0;
        fixed4 color : COLOR;
    };
    ENDCG

    SubShader 
    {
        Tags {
            "RenderType"="Opaque"
            "Queue" = "Transparent"
        }

        Pass{
            Name "OUTLINE"

            Cull Front

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            v2f vert(appdata v)
            {
                v2f o;
                o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
                half3 norm = mul((half3x3)UNITY_MATRIX_IT_MV, v.normal);
                half2 offset = TransformViewToProjection(norm.xy);
                o.pos.xy += offset * o.pos.z * _Outline;
                o.color = _OutlineColor;
                return o;
            }

            fixed4 frag(v2f i) : COLOR
            {
                fixed4 o;
                o = i.color;
                return o;
            }
            ENDCG
        }

        Pass 
        {
            Name "TEXTURE"

            Cull Back
            ZWrite On
            ZTest LEqual

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            v2f vert(appdata v)
            {
                v2f o;
                o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.color = v.color;
                return o;
            }

            fixed4 frag(v2f i) : COLOR 
            {
                fixed4 o;
                o = tex2D(_MainTex, i.uv.xy);
                return o;
            }
            ENDCG
        }
    } 
}

Linha 41: A configuração "Cull Front" diz ao shader para realizar uma seleção nos vértices da frente. Isso significa que ignoraremos todos os vértices da frente deste passe. Ficamos com o lado de trás que queremos manipular um pouco.

Linhas 51-53: A matemática de mover vértices ao longo de seus vetores normais.

Linha 54: Configurando a cor do vértice para a nossa cor de escolha definida nas propriedades do shaders.

Link útil: http://wiki.unity3d.com/index.php/Silhouette-Outlined_Diffuse


Atualizar

outro exemplo

insira a descrição da imagem aqui

insira a descrição da imagem aqui

   Shader "Custom/CustomOutline" {
            Properties {
                _Color ("Color", Color) = (1,1,1,1)
                _Outline ("Outline Color", Color) = (0,0,0,1)
                _MainTex ("Albedo (RGB)", 2D) = "white" {}
                _Glossiness ("Smoothness", Range(0,1)) = 0.5
                _Size ("Outline Thickness", Float) = 1.5
            }
            SubShader {
                Tags { "RenderType"="Opaque" }
                LOD 200

                // render outline

                Pass {
                Stencil {
                    Ref 1
                    Comp NotEqual
                }

                Cull Off
                ZWrite Off

                    CGPROGRAM
                    #pragma vertex vert
                    #pragma fragment frag
                    #include "UnityCG.cginc"
                    half _Size;
                    fixed4 _Outline;
                    struct v2f {
                        float4 pos : SV_POSITION;
                    };
                    v2f vert (appdata_base v) {
                        v2f o;
                        v.vertex.xyz += v.normal * _Size;
                        o.pos = UnityObjectToClipPos (v.vertex);
                        return o;
                    }
                    half4 frag (v2f i) : SV_Target
                    {
                        return _Outline;
                    }
                    ENDCG
                }

                Tags { "RenderType"="Opaque" }
                LOD 200

                // render model

                Stencil {
                    Ref 1
                    Comp always
                    Pass replace
                }


                CGPROGRAM
                // Physically based Standard lighting model, and enable shadows on all light types
                #pragma surface surf Standard fullforwardshadows
                // Use shader model 3.0 target, to get nicer looking lighting
                #pragma target 3.0
                sampler2D _MainTex;
                struct Input {
                    float2 uv_MainTex;
                };
                half _Glossiness;
                fixed4 _Color;
                void surf (Input IN, inout SurfaceOutputStandard o) {
                    // Albedo comes from a texture tinted by color
                    fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
                    o.Albedo = c.rgb;
                    // Metallic and smoothness come from slider variables
                    o.Smoothness = _Glossiness;
                    o.Alpha = c.a;
                }
                ENDCG
            }
            FallBack "Diffuse"
        }
Seyed Morteza Kamali
fonte
Por que o uso do buffer de estêncil no exemplo atualizado?
Tara
Ah, entendi agora. O segundo exemplo usa uma abordagem que gera apenas contornos externos, ao contrário do primeiro. Você pode mencionar isso na sua resposta.
Tara
0

Uma das melhores maneiras de fazer isso é renderizar sua cena em uma textura do Framebuffer e renderizá-la enquanto faz uma filtragem Sobel em cada pixel, o que é uma técnica fácil para a detecção de bordas. Dessa forma, você pode não apenas tornar a cena pixelizada (definindo uma baixa resolução para a textura Framebuffer), mas também ter acesso a todos os valores de pixel para fazer Sobel funcionar.

Ross Myhovych
fonte