Come posso disegnare contorni attorno a modelli 3D?


47

Come posso disegnare contorni attorno a modelli 3D? Mi riferisco a qualcosa come gli effetti in un recente gioco Pokemon, che sembrano avere un contorno a pixel singolo attorno a loro:

inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine


Stai usando OpenGL? In tal caso, dovresti cercare su Google come disegnare i contorni di un modello con OpenGL.
oxysoft,

1
Se ti riferisci a quelle immagini particolari che metti lì, posso dire con una certezza del 95% che quelle sono sprite 2D disegnate a mano, non modelli 3D
Panda Pajama,

3
@PandaPajama: No, quelli sono quasi certamente modelli 3D. C'è un po 'di trasandatezza in quelle che dovrebbero essere le linee dure in alcuni fotogrammi che non mi aspetterei dagli sprite disegnati a mano, e comunque è fondamentalmente come appaiono i modelli 3D in-game. Suppongo che non posso garantire il 100% per quelle immagini specifiche, ma non riesco a immaginare perché qualcuno dovrebbe fare lo sforzo di falsificarle.
CA McCann,

Di che gioco si tratta, in particolare? Sembra stupendo.
Vegard

@Vegard La creatura con una rapa sul dorso è un Bulbasaur del gioco Pokémon.
Damian Yerrick,

Risposte:


28

Non credo che nessuna delle altre risposte qui avrà l'effetto in Pokémon X / Y. Non so esattamente come sia fatto, ma ho capito un modo che sembra praticamente quello che fanno nel gioco.

In Pokémon X / Y, i contorni sono disegnati sia attorno ai bordi della silhouette che su altri bordi non-silhouette (come dove le orecchie di Raichu incontrano la testa nella schermata seguente).

Raichu

Guardando la mesh di Raichu in Blender, puoi vedere l'orecchio (evidenziato in arancione sopra) è solo un oggetto separato e disconnesso che interseca la testa, creando un brusco cambiamento nelle normali della superficie.

Sulla base di ciò, ho provato a generare il contorno in base alle normali, che richiede il rendering in due passaggi:

Primo passaggio : eseguire il rendering del modello (strutturato e cel-shading) senza i contorni e rendere le normali dello spazio della telecamera su un secondo target di rendering.

Secondo passaggio : esegui un filtro di rilevamento dei bordi a schermo intero sulle normali dal primo passaggio.

Le prime due immagini sottostanti mostrano le uscite del primo passaggio. Il terzo è il contorno da solo, e l'ultimo è il risultato combinato finale.

Dratini

Ecco lo shader di frammenti OpenGL che ho usato per il rilevamento dei bordi nel secondo passaggio. È il migliore che sono riuscito a trovare, ma potrebbe esserci un modo migliore. Probabilmente non è nemmeno ottimizzato molto bene.

// 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 prima di eseguire il primo passaggio, deseleziono il target di rendering delle normali su un vettore rivolto lontano dalla telecamera:

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 );

Ho letto da qualche parte (inserirò un link nei commenti) che Nintendo 3DS utilizza una pipeline a funzione fissa invece di shader, quindi immagino che questo non sia esattamente il modo in cui è stato fatto nel gioco, ma per ora io ' Sono convinto che il mio metodo sia abbastanza vicino.


Informazioni sull'hardware Nintendo 3DS: link
KTC

Ottima soluzione! Come approfondiresti il ​​tuo shader? (Ad esempio, nel caso di un piano di fronte a un altro, entrambi hanno la stessa normalità, quindi non verrà tracciato alcun contorno)
ingham

@ingham Quel caso si presenta abbastanza raramente su un personaggio organico che non avevo bisogno di gestirlo, e sembra che nemmeno il vero gioco lo gestisca. Nel gioco reale a volte puoi vedere il contorno scomparire quando le normali sono le stesse, ma non penso che la gente lo noterebbe di solito.
KTC

Sono un po 'scettico sul fatto che un 3DS sia cablabile per eseguire effetti a schermo intero basati su shader come quello. Il suo supporto per lo shader è rudimentale (se ne ha addirittura uno).
Tara,

17

Questo effetto è particolarmente comune nei giochi che utilizzano effetti di ombreggiatura cel, ma in realtà è qualcosa che può essere applicato indipendentemente dallo stile di ombreggiatura cel.

Quello che stai descrivendo si chiama "rendering del bordo delle feature" ed è in generale il processo di evidenziazione dei vari contorni e contorni di un modello. Ci sono molte tecniche disponibili e molti articoli sull'argomento.

Una tecnica semplice è quella di rendere solo il bordo della sagoma, il contorno estremo. Questo può essere fatto semplicemente come il rendering del modello originale con una scrittura stencil, e quindi il rendering di nuovo in modalità wireframe spessa, solo dove non c'era valore di stencil. Vedi qui per un'implementazione di esempio.

Ciò, tuttavia, non evidenzierà il contorno interno e piega i bordi (come mostrato nelle immagini). In generale, per farlo in modo efficace, è necessario estrarre informazioni sui bordi della mesh (in base alle discontinuità nelle normali della faccia su entrambi i lati del bordo e costruire una struttura di dati che rappresenti ciascun bordo.

È quindi possibile scrivere shader per estrudere o rendere in altro modo quei bordi come geometria normale sopra il modello di base (o in combinazione con esso). La posizione di un bordo e le normali delle facce adiacenti rispetto al vettore della vista vengono utilizzate per determinare se è possibile disegnare un bordo specifico.

Puoi trovare ulteriori discussioni, dettagli e documenti con vari esempi su Internet. Per esempio:


1
Posso confermare che il metodo stencil (da flipcode.com) funziona e sembra davvero carino. È possibile indicare lo spessore nelle coordinate dello schermo in modo che lo spessore del contorno non dipenda dalle dimensioni del modello (né dalla forma del modello).
Vegard

1
Una tecnica che non hai menzionato è l'effetto di ombreggiatura del bordo post-elaborazione spesso usato in combinazione con l'ombreggiatura cel che cerca pixel con valori alti dz/dxe / odz/dy
bcrist,

8

Il modo più semplice per farlo, comune su hardware più vecchio prima degli shader di pixel / frammenti, e ancora usato su dispositivi mobili, è duplicare il modello, invertire l'ordine di avvolgimento del vertice in modo che il modello venga visualizzato al rovescio (o se lo si desidera, è possibile Fallo nel tuo strumento di creazione di risorse 3D, ad esempio Blender, capovolgendo le normali di superficie - stessa cosa), quindi espandi l'intero duplicato leggermente attorno al suo centro e infine colora / struttura questo duplicato completamente in nero. Ciò si traduce in contorni attorno al modello originale, se si tratta di un modello semplice come un cubo. Per i modelli più complessi con forme concave (come quello nell'immagine seguente), è necessario modificare manualmente il modello duplicato in modo che sia un po 'più "grasso" rispetto alla sua controparte originale, come una somma di Minkowskiin 3D. Puoi iniziare spingendo un po 'fuori ciascun vertice lungo la sua normale per formare la trama del contorno, come fa la trasformazione Riduci / Fattura di Blender.

Gli approcci dello spazio dello schermo / pixel shader tendono ad essere più lenti e più difficili da implementare bene , ma OTOH non raddoppia il numero di vertici nel tuo mondo. Quindi, se stai facendo un lavoro poli alto, optare per questo approccio. Dato console moderno e la capacità del desktop per l'elaborazione della geometria, non mi sarei preoccuparsi di un fattore di 2 a tutti . Stile cartoon = poli basso sicuramente, quindi duplicare la geometria è più semplice.

Puoi testare l'effetto da solo, ad esempio in Blender, senza toccare alcun codice. I contorni dovrebbero assomigliare all'immagine qui sotto, notare come alcuni sono interni, ad esempio sotto il braccio. Maggiori dettagli qui .

inserisci qui la descrizione dell'immagine.


1
Potresti spiegare, come "espandere l'intero duplicato leggermente attorno al suo centro" è conforme a questa immagine, perché il semplice ridimensionamento attorno al centro non funzionerebbe per le armi e altre parti che non sono concentriche, inoltre non funzionerà per nessun modello che abbia buchi.
Kromster dice di sostenere Monica il

@KromStern In alcuni casi , i sottoinsiemi di vertici devono essere ridimensionati manualmente per adattarsi. Risposta modificata.
Ingegnere

1
È comune spingere i vertici lungo la loro normale superficie locale, ma ciò può causare la divisione della mesh del contorno espanso lungo i bordi duri
DMGregory

Grazie! Non penso che ci sia motivo di capovolgere le normali, dato che il duplicato sarà colorato in tinta unita piatta (cioè nessun calcolo di illuminazione elaborato che dipende dalle normali). Ho ottenuto lo stesso effetto semplicemente ridimensionando, colorando in modo piatto e quindi abbattendo le facce anteriori del duplicato.
Jet Blue,

6

Per i modelli lisci (molto importante), questo effetto è abbastanza semplice. Nel tuo frammento / pixel shader avrai bisogno della normale ombreggiatura del frammento. Se è molto vicino alla perpendicolare ( dot(surface_normal,view_vector) <= .01- potresti aver bisogno di giocare con quella soglia), colora il frammento nero invece del suo solito colore.

Questo approccio "consuma" un po 'del modello per fare il contorno. Questo può o meno essere quello che vuoi. È molto difficile capire dall'immagine di Pokemon se questo è ciò che viene fatto. Dipende se ti aspetti che il contorno sia incluso in qualsiasi sagoma del personaggio o se preferisci che il contorno racchiuda la sagoma (che richiede una tecnica diversa).

Il momento clou sarà su qualsiasi parte della superficie in cui passa da fronte a retro, compresi i "bordi interni" (come le gambe del Pokemon verde o la sua testa - alcune altre tecniche non aggiungerebbero alcun contorno a quelle ).

Gli oggetti con bordi duri e non lisci (come un cubo) non riceveranno un punto culminante nelle posizioni desiderate con questo approccio. Ciò significa che in alcuni casi questo approccio non è affatto un'opzione; Non ho idea se i modelli Pokemon siano tutti fluidi o meno.


5

Il modo più comune in cui l'ho visto è tramite un secondo passaggio di rendering sul tuo modello. In sostanza, duplicalo e capovolgi le normali e inseriscilo in uno shader di vertice. Nello shader, ridimensiona ciascun vertice lungo la sua normale. Nello shader pixel / frammento, disegna il nero. Questo ti darà contorni sia esterni che interni, come intorno a labbra, occhi, ecc. Questa è in realtà una call call abbastanza economica, se non altro è generalmente più economico della post elaborazione della linea, a seconda del numero di modelli e della loro complessità. Guilty Gear Xrd utilizza questo metodo perché è facile controllare lo spessore della linea tramite il colore del vertice.

Il secondo modo di fare linee interne ho imparato dallo stesso gioco. Nella tua mappa UV, allinea la trama lungo l'asse uo v, in particolare nelle aree in cui desideri una linea interna. Disegna una linea nera lungo uno degli assi e sposta le coordinate UV dentro o fuori da quella linea per creare la linea interna.

Guarda il video di GDC per una spiegazione migliore: https://www.youtube.com/watch?v=yhGjCzxJV3E


5

Uno dei modi per creare una struttura è utilizzare i nostri vettori normali per i nostri modelli. I vettori normali sono vettori perpendicolari alla loro superficie (che punta lontano dalla superficie). Il trucco qui è dividere il modello del personaggio in due parti. I vertici rivolti verso la telecamera e i vertici rivolti verso la telecamera. Li chiameremo rispettivamente FRONTE e RETRO.

Per il contorno prendiamo i nostri vertici INDIETRO e li spostiamo leggermente nella direzione dei loro normali vettori. Pensaci come se rendessi la parte del nostro personaggio che sta di fronte alla telecamera un po 'più grassa. Dopo averlo fatto, assegniamo loro un colore a nostra scelta e abbiamo un bel contorno.

inserisci qui la descrizione dell'immagine

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
        }
    } 
}

Linea 41: L'impostazione “Cull Front” dice allo shader di eseguire una culling sui vertici rivolti frontalmente. Significa che ignoreremo tutti i vertici rivolti in avanti in questo passaggio. Ci resta il lato INDIETRO che vogliamo manipolare un po '.

Linee 51-53: la matematica dei vertici in movimento lungo i loro normali vettori.

Riga 54: impostazione del colore del vertice sul colore di nostra scelta definito nelle proprietà degli shader.

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


Aggiornare

un altro esempio

inserisci qui la descrizione dell'immagine

inserisci qui la descrizione dell'immagine

   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"
        }

Perché l'uso del buffer di stencil nell'esempio aggiornato?
Tara

Ah, ho capito adesso. Il secondo esempio utilizza un approccio che genera solo contorni esterni, a differenza del primo. Potresti menzionarlo nella tua risposta.
Tara,

0

Uno dei modi migliori per farlo è renderizzare la tua scena su una trama Framebuffer , quindi renderizzare quella trama mentre esegui un filtro Sobel su ogni pixel, che è una tecnica semplice per il rilevamento dei bordi. In questo modo puoi non solo rendere la scena pixelata (impostando una bassa risoluzione sulla trama Framebuffer), ma anche avere accesso a tutti i valori di pixel per far funzionare Sobel.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.