Wie kann ich Umrisse um 3D-Modelle zeichnen?

47

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:

Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben

mystisches Portal
quelle
Verwenden Sie OpenGL? In diesem Fall sollten Sie in Google nach Informationen suchen, wie Sie mit OpenGL Umrisse für ein Modell zeichnen.
Oxysoft
1
Wenn Sie sich auf die Bilder beziehen, die Sie dort eingefügt haben, kann ich mit 95% iger Sicherheit sagen, dass es sich um handgezeichnete 2D-Sprites handelt, nicht um 3D-Modelle
Panda Pyjama,
3
@PandaPyjama: Nein, das sind mit ziemlicher Sicherheit 3D-Modelle. In einigen Frames sollte es etwas schlampig sein, was ich von handgezeichneten Sprites nicht erwarten würde, und im Grunde sehen die 3D-Modelle im Spiel so aus. Ich nehme an, ich kann nicht 100% für diese spezifischen Bilder garantieren, aber ich kann mir nicht vorstellen, warum sich jemand die Mühe macht, sie zu fälschen.
CA McCann
Welches Spiel ist das konkret? Es sieht wunderschön aus.
Vegard
@Vegard Die Kreatur mit der Rübe auf dem Rücken ist ein Bulbasaur aus dem Spiel Pokémon.
Damian Yerrick

Antworten:

28

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

Raichu

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.

Dratini

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.

KTC
quelle
Informationen zur Nintendo 3DS-Hardware: Link
KTC
Sehr schöne Lösung! Wie würden Sie die Tiefe in Ihrem Shader berücksichtigen? (Im Falle eines Flugzeugs zum Beispiel vor einem anderen haben beide die gleiche Normalität, so dass keine Umrisse gezeichnet werden)
Ingham
@ingham Dieser Fall taucht bei einem organischen Charakter selten genug auf, so dass ich nicht damit umgehen musste, und es sieht so aus, als würde das echte Spiel auch nicht damit umgehen. Im echten Spiel kann man manchmal die Konturen verschwinden sehen, wenn die Normalen gleich sind, aber ich glaube nicht, dass die Leute es normalerweise bemerken würden.
KTC
Ich bin ein bisschen skeptisch, ob ein 3DS Shader-basierte Vollbildeffekte wie diesen ausführen kann. Die Shader-Unterstützung ist rudimentär (falls überhaupt vorhanden).
Tara
17

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:

Josh
quelle
1
Ich kann bestätigen, dass die Schablonenmethode (von flipcode.com) funktioniert und wirklich gut aussieht. Sie können die Dicke in Bildschirmkoordinaten angeben, damit die Dicke des Umrisses nicht von der Größe des Modells (oder der Form des Modells) abhängt.
Vegard
1
Eine Technik, die Sie nicht erwähnt haben, ist der dz/dxdz/dy
Randschattierungseffekt
8

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 .

Bildbeschreibung hier eingeben.

Techniker
quelle
1
Könnten Sie bitte erklären, wie "Erweitern Sie das gesamte Duplikat geringfügig um die Mitte" mit diesem Bild übereinstimmt, da eine einfache Skalierung um die Mitte für Arme und andere Teile, die nicht konzentrisch sind, nicht funktioniert. Dies gilt auch nicht für Modelle mit Löcher drin.
Kromster sagt Unterstützung Monica
@KromStern In einigen Fällen müssen Untergruppen von Scheitelpunkten von Hand skaliert werden, um sie aufzunehmen. Geänderte Antwort.
Ingenieur
1
Es ist üblich, die Scheitelpunkte entlang ihrer lokalen Oberflächennormalen herauszustoßen, dies kann jedoch dazu führen, dass sich das erweiterte
Umrissnetz
Vielen Dank! Ich glaube nicht, dass es Sinn macht, die Normalen umzudrehen, da das Duplikat eine flache Volltonfarbe hat (dh keine ausgefallenen Beleuchtungsberechnungen, die von Normalen abhängen). Den gleichen Effekt habe ich erzielt, indem ich nur die Vorderseite des Duplikats skaliert, flach gefärbt und dann ausgesucht habe.
Jet Blue
6

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.

Sean Middleditch
quelle
5

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

Andrew Q
quelle
5

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.

Bildbeschreibung hier eingeben

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


Aktualisieren

ein anderes Beispiel

Bildbeschreibung hier eingeben

Bildbeschreibung hier eingeben

   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
quelle
Warum der Stencil Buffer im aktualisierten Beispiel?
Tara
Ah, ich habe es jetzt verstanden. Im zweiten Beispiel wird ein Ansatz verwendet, der im Gegensatz zum ersten nur externe Konturen generiert. Vielleicht möchten Sie das in Ihrer Antwort erwähnen.
Tara
0

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.

Ross Myhovych
quelle