Wie kann ich Umrisse um 3D-Modelle zeichnen? Ich beziehe mich auf so etwas wie die Effekte in einem kürzlich erschienenen Pokemon-Spiel, die anscheinend einen einpixeligen Umriss haben:
Wie kann ich Umrisse um 3D-Modelle zeichnen? Ich beziehe mich auf so etwas wie die Effekte in einem kürzlich erschienenen Pokemon-Spiel, die anscheinend einen einpixeligen Umriss haben:
Antworten:
Ich glaube nicht, dass eine der anderen Antworten hier den Effekt in Pokémon X / Y erzielen wird. Ich kann nicht genau wissen, wie es gemacht wird, aber ich habe einen Weg gefunden, der so ziemlich dem entspricht, was sie im Spiel machen.
In Pokémon X / Y werden Umrisse sowohl an den Konturkanten als auch an anderen Nicht-Konturkanten gezeichnet (wie in dem folgenden Screenshot, wo Raichus Ohren auf seinen Kopf treffen).
Wenn Sie sich Raichus Netz in Blender ansehen, sehen Sie, dass das Ohr (oben in Orange hervorgehoben) nur ein separates, nicht verbundenes Objekt ist, das den Kopf schneidet und eine abrupte Änderung der Oberflächennormalen bewirkt.
Darauf basierend habe ich versucht, den Umriss anhand der Normalen zu generieren, was ein Rendern in zwei Durchgängen erfordert:
Erster Durchgang : Rendern Sie das Modell (texturiert und cel-schattiert) ohne die Konturen und rendern Sie die Normalen des Kameraraums auf ein zweites Renderziel.
Zweiter Durchgang : Führen Sie einen Vollbild-Kantenerkennungsfilter über den Normalen des ersten Durchgangs aus.
Die ersten beiden Bilder unten zeigen die Ausgaben des ersten Durchgangs. Das dritte ist der Umriss für sich und das letzte ist das endgültige kombinierte Ergebnis.
Hier ist der OpenGL-Fragment-Shader, den ich im zweiten Durchgang für die Kantenerkennung verwendet habe. Es ist das Beste, was ich mir einfallen lassen konnte, aber es könnte einen besseren Weg geben. Es ist wahrscheinlich auch nicht sehr gut optimiert.
// 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);
}
Und bevor ich den ersten Durchgang rendere, lösche ich das Renderziel der Normalen auf einen Vektor, der von der Kamera abgewandt ist:
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 );
Ich habe irgendwo gelesen (ich werde einen Link in den Kommentaren einfügen), dass der Nintendo 3DS eine Pipeline mit festen Funktionen anstelle von Shadern verwendet. Ich bin überzeugt, dass meine Methode nahe genug ist.
Dieser Effekt tritt besonders häufig in Spielen auf, in denen Cel-Shading-Effekte verwendet werden, kann jedoch unabhängig vom Cel-Shading-Stil angewendet werden.
Was Sie beschreiben, wird als "Feature Edge Rendering" bezeichnet und hebt im Allgemeinen die verschiedenen Konturen und Umrisse eines Modells hervor. Es gibt viele Techniken und viele Artikel zu diesem Thema.
Eine einfache Technik besteht darin, nur die Konturkante, den äußersten Umriss, zu rendern. Dies ist so einfach wie das Rendern des Originalmodells mit einem Schablonenschreibvorgang und das anschließende erneute Rendern im Thick-Wireframe-Modus, nur wenn kein Schablonenwert vorhanden war. Hier finden Sie ein Implementierungsbeispiel.
Dadurch werden jedoch die Innenkontur und die Knickkanten nicht hervorgehoben (wie in Ihren Bildern gezeigt). Um dies effektiv zu erreichen, müssen Sie im Allgemeinen Informationen über die Kanten des Netzes extrahieren (basierend auf Diskontinuitäten in den Flächennormalen auf beiden Seiten der Kante) und eine Datenstruktur aufbauen, die jede Kante darstellt.
Sie können dann Shader schreiben, um diese Kanten zu extrudieren oder auf andere Weise als reguläre Geometrie über Ihrem Basismodell (oder in Verbindung damit) zu rendern. Die Position einer Kante und die Normalen der angrenzenden Flächen relativ zum Ansichtsvektor werden verwendet, um zu bestimmen, ob eine bestimmte Kante gezeichnet werden kann.
Weitere Diskussionen, Details und Vorträge mit verschiedenen Beispielen finden Sie im Internet. Zum Beispiel:
dz/dx
dz/dy
Der einfachste Weg, dies zu tun, ist das Duplizieren des Modells und das Umkehren der Scheitelpunkt-Wicklungsreihenfolge, so dass das Modell von innen nach außen angezeigt wird (oder wenn Sie möchten, können Sie dies tun) Tun Sie dies in Ihrem Werkzeug zur Erstellung von 3D-Objekten, sagen Sie Blender, indem Sie die Oberflächennormalen spiegeln (dasselbe gilt für). Erweitern Sie dann das gesamte Duplikat geringfügig um seine Mitte und färben / texturieren Sie dieses Duplikat schließlich vollständig schwarz. Dies führt zu Umrissen um Ihr ursprüngliches Modell, wenn es sich um ein einfaches Modell wie einen Würfel handelt. Bei komplexeren Modellen mit konkaven Formen (wie in der Abbildung unten) muss das duplizierte Modell manuell so angepasst werden, dass es etwas "dicker" als das ursprüngliche Modell ist, z. B. eine Minkowski-Summein 3D. Sie können beginnen, indem Sie jeden Scheitelpunkt entlang seiner Normalen etwas nach außen schieben, um das Umrissnetz zu bilden, wie dies bei der Schrumpf- / Fett-Transformation von Blender der Fall ist.
Platz auf dem Bildschirm / Pixel - Shader - Ansätze sind in der Regel langsamer und schwieriger zu implementieren gut , aber OTOH nicht doppelt so hoch die Anzahl von Eckpunkten in Ihrer Welt. Wenn Sie also viel arbeiten, entscheiden Sie sich am besten für diesen Ansatz. Angesichts moderne Konsole und Desktop - Kapazität Geometrie für die Verarbeitung, würde mich nicht um einen Faktor 2 Sorgen überhaupt . Cartoon-Stil = mit Sicherheit Low Poly, daher ist das Duplizieren von Geometrie am einfachsten.
Sie können den Effekt zB in Blender selbst testen, ohne einen Code zu berühren. Die Umrisse sollten wie in der Abbildung unten aussehen. Beachten Sie, dass einige davon innerlich sind, z. B. unter dem Arm. Mehr dazu hier .
.
Für glatte Modelle (sehr wichtig) ist dieser Effekt ziemlich einfach. In Ihrem Fragment- / Pixel-Shader benötigen Sie die Normalen des zu schattierenden Fragments. Wenn es sehr nahe an der Senkrechten liegt ( dot(surface_normal,view_vector) <= .01
- möglicherweise müssen Sie mit dieser Schwelle spielen), färben Sie das Fragment schwarz anstatt der üblichen Farbe.
Dieser Ansatz "verbraucht" einen kleinen Teil des Modells, um den Umriss zu erstellen. Dies kann oder kann nicht sein, was Sie wollen. Es ist sehr schwierig, anhand des Pokemon-Bildes zu erkennen, ob dies getan wird. Es hängt davon ab, ob Sie erwarten, dass der Umriss in einer Silhouette des Charakters enthalten ist, oder ob der Umriss die Silhouette einschließen soll (was eine andere Technik erfordert).
Das Highlight wird sich auf jedem Teil der Oberfläche befinden, auf dem der Übergang von der Vorderseite zur Rückseite erfolgt, einschließlich der "Innenkanten" (wie die Beine des grünen Pokémon oder dessen Kopf). Einige andere Techniken würden diesen keine Konturen hinzufügen ).
Objekte mit harten, nicht glatten Kanten (wie ein Würfel) erhalten bei diesem Ansatz an den gewünschten Stellen keine Hervorhebung. Dies bedeutet, dass dieser Ansatz in einigen Fällen überhaupt nicht in Frage kommt. Ich habe keine Ahnung, ob Pokemon-Modelle alle glatt sind oder nicht.
Am häufigsten habe ich dies durch einen zweiten Render-Pass für Ihr Modell gesehen. Duplizieren Sie es im Wesentlichen, drehen Sie die Normalen um und verschieben Sie sie in einen Vertex-Shader. Skalieren Sie im Shader jeden Scheitelpunkt entlang seiner Normalen. Zeichnen Sie im Pixel- / Fragment-Shader Schwarz. Dadurch erhalten Sie sowohl externe als auch interne Konturen, z. B. um die Lippen, die Augen usw. Dies ist eigentlich ein recht billiger Zeichnungsaufruf, wenn nichts anderes in der Regel billiger als die Nachbearbeitung der Linie, abhängig von der Anzahl der Modelle und ihrer Komplexität. Guilty Gear Xrd verwendet diese Methode, da die Linienstärke einfach über die Scheitelpunktfarbe gesteuert werden kann.
Die zweite Möglichkeit, innere Linien zu machen, habe ich aus demselben Spiel gelernt. Richten Sie in Ihrer UV-Karte Ihre Textur entlang der u- oder v-Achse aus, insbesondere in Bereichen, in denen Sie eine innere Linie wünschen. Zeichnen Sie eine schwarze Linie entlang einer der beiden Achsen und verschieben Sie die UV-Koordinaten in diese Linie hinein oder aus dieser heraus, um die innere Linie zu erstellen.
Eine bessere Erklärung finden Sie im Video von GDC: https://www.youtube.com/watch?v=yhGjCzxJV3E
Eine Möglichkeit, einen Umriss zu erstellen, besteht darin, die normalen Vektoren unserer Modelle zu verwenden. Normale Vektoren sind Vektoren, die senkrecht zu ihrer Oberfläche stehen (von der Oberfläche weg zeigen). Der Trick dabei ist, dein Charaktermodell in zwei Teile zu teilen. Die Scheitelpunkte, die der Kamera zugewandt sind, und die Scheitelpunkte, die der Kamera abgewandt sind. Wir werden sie FRONT und BACK nennen.
Für den Umriss nehmen wir unsere BACK-Eckpunkte und verschieben sie leicht in Richtung ihrer normalen Vektoren. Stellen Sie sich das so vor, als würde der Teil unseres Charakters, der von der Kamera abgewandt ist, etwas dicker. Nachdem wir das erledigt haben, weisen wir ihnen eine Farbe unserer Wahl zu und wir haben einen schönen Umriss.
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
}
}
}
Zeile 41: Die Einstellung "Front aussortieren" weist den Shader an, ein Aussortieren an den nach vorne gerichteten Scheitelpunkten durchzuführen. Dies bedeutet, dass wir in diesem Durchgang alle nach vorne gerichteten Eckpunkte ignorieren. Wir haben die RÜCKSEITE, die wir ein wenig manipulieren wollen.
Zeilen 51-53: Die Mathematik zum Verschieben von Eckpunkten entlang ihrer normalen Vektoren.
Zeile 54: Stellen Sie die Scheitelpunktfarbe auf die Farbe Ihrer Wahl ein, die in den Shadereigenschaften definiert ist.
Nützlicher Link: http://wiki.unity3d.com/index.php/Silhouette-Outlined_Diffuse
ein anderes Beispiel
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"
}
Eine der besten Möglichkeiten besteht darin, Ihre Szene auf einer Framebuffer- Textur zu rendern und diese Textur dann zu rendern, während Sie eine Sobel-Filterung für jedes Pixel durchführen. Dies ist eine einfache Technik zur Kantenerkennung. Auf diese Weise können Sie die Szene nicht nur pixelig machen (indem Sie eine niedrige Auflösung für die Framebuffer-Textur festlegen), sondern auch auf alle Pixelwerte zugreifen, damit Sobel funktioniert.