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:
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:
Risposte:
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).
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.
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.
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:
dz/dx
e / odz/dy
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 .
.
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.
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
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.
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
un altro esempio
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"
}
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.