Jak narysować kontury wokół modeli 3D?

47

Jak narysować kontury wokół modeli 3D? Mam na myśli coś takiego jak efekty z ostatniej gry Pokemon, które wydają się mieć wokół siebie zarys jednego piksela:

wprowadź opis zdjęcia tutaj wprowadź opis zdjęcia tutaj

mistyczny portal
źródło
Czy korzystasz z OpenGL? Jeśli tak, powinieneś wyszukać w Google, jak narysować kontury modelu w OpenGL.
oxysoft
1
Jeśli odwołujesz się do tych konkretnych obrazów, które tam umieszczasz, mogę powiedzieć z 95% pewnością, że są to ręcznie rysowane duszki 2D, a nie modele 3D
Panda Pajama
3
@PandaPajama: Nie, to prawie na pewno modele 3D. Jest pewna niechlujność w czymś, co powinno być twardymi liniami w niektórych klatkach, których nie spodziewałbym się po ręcznie rysowanych duszkach, a tak w zasadzie tak wyglądają modele 3D w grze. Przypuszczam, że nie mogę zagwarantować 100% dla tych konkretnych obrazów, ale nie mogę sobie wyobrazić, dlaczego ktoś miałby się starać je sfałszować.
CA McCann
Która to konkretnie gra? Wygląda wspaniale.
Vegard
@Vegard Stworzenie z rzepą na plecach to Bulbasaur z gry Pokémon.
Damian Yerrick

Odpowiedzi:

28

Nie sądzę, że żadna z pozostałych odpowiedzi tutaj osiągnie efekt w Pokémon X / Y. Nie wiem dokładnie, jak to się robi, ale wymyśliłem sposób, który wydaje się prawie tym, co robią w grze.

W Pokémon X / Y kontury są rysowane zarówno wokół krawędzi sylwetki, jak i na innych nie-sylwetkowych krawędziach (tak jak tam, gdzie uszy Raichu spotykają się z głową na poniższym zrzucie ekranu).

Raichu

Patrząc na siatkę Raichu w Blenderze, widać, że ucho (zaznaczone na pomarańczowo powyżej) jest tylko oddzielnym, odłączonym przedmiotem, który przecina głowę, powodując nagłą zmianę normalnych powierzchni.

Na tej podstawie próbowałem wygenerować kontur w oparciu o normalne, co wymaga renderowania w dwóch przebiegach:

Pierwsze przejście : Renderuj model (teksturowany i cieniowany) bez konturów i wyrenderuj normalne przestrzenie kamery na drugi cel renderowania.

Drugie przejście : Wykonaj pełnoekranowy filtr wykrywający krawędzie normalnych od pierwszego przejścia.

Pierwsze dwa obrazy poniżej pokazują wyniki pierwszego przejścia. Trzeci to sam zarys, a ostatni to końcowy łączny wynik.

Dratini

Oto moduł cieniujący fragmenty OpenGL, którego użyłem do wykrywania krawędzi w drugim przejściu. To najlepsze, co mogłem wymyślić, ale może być lepszy sposób. Prawdopodobnie też nie jest zbyt dobrze zoptymalizowany.

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

A przed renderowaniem pierwszego przejścia oczyszczam cel renderowania normalnych do wektora odwróconego od kamery:

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

Czytałem gdzieś (umieszczę link w komentarzach), że Nintendo 3DS używa potoku o stałej funkcji zamiast shaderów, więc myślę, że nie może tak być dokładnie tak, jak dzieje się w grze, ale na razie „ Jestem przekonany, że moja metoda jest wystarczająco bliska.

KTC
źródło
Informacje o sprzęcie Nintendo 3DS: link
KTC
Bardzo fajne rozwiązanie! Jak weźmiesz pod uwagę głębię swojego shadera? (Na przykład w przypadku samolotu przed innym, oba mają tę samą normę, więc nie zostanie narysowany kontur)
ingham
@ingham Sprawa ta pojawia się dość rzadko na organicznej postaci, że nie musiałem się nią zajmować, i wygląda na to, że prawdziwa gra też jej nie obsługuje. W prawdziwej grze czasami zarysy znikają, gdy normalne są takie same, ale nie sądzę, że ludzie zwykle to zauważą.
KTC
Jestem nieco sceptycznie nastawiony do tego, że 3DS jest przystosowany do uruchamiania pełnoekranowych efektów opartych na modułach cieniujących. Jego obsługa modułu cieniującego jest szczątkowa (jeśli w ogóle ma).
Tara
17

Ten efekt jest szczególnie powszechny w grach wykorzystujących efekty cieniowania cel, ale w rzeczywistości jest czymś, co można zastosować niezależnie od stylu cieniowania cel.

To, co opisujesz, nazywa się „renderingiem krawędzi obiektu” i ogólnie polega na podkreśleniu różnych konturów i konturów modelu. Dostępnych jest wiele technik i wiele artykułów na ten temat.

Prostą techniką jest renderowanie tylko krawędzi sylwetki, najbardziej zewnętrznego konturu. Można to zrobić tak samo, jak renderowanie oryginalnego modelu za pomocą szablonu, a następnie renderowanie go ponownie w trybie grubej siatki, tylko w przypadku braku wartości szablonu. Zobacz tutaj przykładową implementację.

To jednak nie podświetli wewnętrznego konturu ani fałdowania krawędzi (jak pokazano na zdjęciach). Zasadniczo, aby to zrobić skutecznie, musisz wyodrębnić informacje o krawędziach siatki (na podstawie nieciągłości w normalnych ścianach po obu stronach krawędzi i zbudować strukturę danych reprezentującą każdą krawędź.

Następnie możesz pisać moduły cieniujące, aby wyciągać lub w inny sposób renderować te krawędzie, gdy regularna geometria zachodzi na model podstawowy (lub w połączeniu z nim). Położenie krawędzi i normalne sąsiadujących ścian w stosunku do wektora widoku służą do ustalenia, czy można narysować konkretną krawędź.

W Internecie można znaleźć dalsze dyskusje, szczegóły i dokumenty z różnymi przykładami. Na przykład:

Josh
źródło
1
Mogę potwierdzić, że metoda szablonu (z flipcode.com) działa i wygląda naprawdę ładnie. Możesz podać grubość we współrzędnych ekranu, aby grubość konturu nie zależała od wielkości modelu (ani od kształtu modelu).
Vegard
1
Jedną z technik, o której nie wspomniałeś, jest efekt cieniowania dz/dxdz/dy
końcowego
8

Najprostszym sposobem na zrobienie tego, powszechnym na starszym sprzęcie przed modułami cieniującymi piksele / fragmenty i nadal używanym na urządzeniach mobilnych, jest powielenie modelu, odwrócenie kolejności nawijania wierzchołków, aby model wyświetlał się na lewą stronę (lub jeśli chcesz, możesz zrób to w narzędziu do tworzenia zasobów 3D, powiedzmy Blender, odwracając normalne powierzchni - to samo), a następnie lekko rozwiń cały duplikat wokół jego środka, a na koniec kolor / teksturę tego duplikatu całkowicie czarny. Daje to kontury wokół oryginalnego modelu, jeśli jest to prosty model, taki jak sześcian. W przypadku bardziej złożonych modeli z wklęsłymi formami (takich jak ten na poniższym obrazku) konieczne jest ręczne zmodyfikowanie duplikatu modelu, aby był nieco „grubszy” niż jego pierwotny odpowiednik, na przykład suma Minkowskiegow 3D. Możesz zacząć od wypchnięcia każdego wierzchołka nieco wzdłuż jego normalnej pozycji, aby utworzyć siatkę konturu, podobnie jak transformacja Zmniejszanie / Tuczenie Blendera.

Ekran podejścia przestrzeń / pixel shader wydają się być wolniejsze i trudniejsze do wdrożenia dobrze , ale OTOH nie podwoić liczbę wierzchołków w swoim świecie. Więc jeśli wykonujesz pracę w wielu polach, najlepiej wybierz to podejście. Biorąc pod uwagę nowoczesną konsolę i pojemność pulpitu do przetwarzania geometrii, nie chcesz martwić się o współczynnik 2 w ogóle . Styl kreskówkowy = na pewno niski poli, dlatego kopiowanie geometrii jest najłatwiejsze.

Możesz sam przetestować efekt np. W Blenderze bez dotykania żadnego kodu. Kontury powinny wyglądać jak na poniższym obrazku, zwróć uwagę, jak niektóre są wewnętrzne, np. Pod pachą. Więcej szczegółów tutaj .

wprowadź opis zdjęcia tutaj.

Inżynier
źródło
1
Czy mógłbyś wyjaśnić, w jaki sposób „nieznacznie rozwinąć cały duplikat wokół jego środka” jest zgodny z tym obrazem, ponieważ proste skalowanie wokół środka nie działałoby dla ramion i innych części, które nie są koncentryczne, nie działałoby również dla żadnego modelu, który ma dziury w nim.
Kromster mówi o wsparciu Moniki
@KromStern W niektórych przypadkach podzbiory wierzchołków muszą być skalowane ręcznie, aby uwzględnić. Poprawiona odpowiedź.
Inżynier
1
Często przesuwa się wierzchołki wzdłuż ich normalnej powierzchni lokalnej, ale może to powodować, że siatka rozszerzonego konturu rozdziela się wzdłuż twardych krawędzi
DMGregory
Dzięki! Nie sądzę, aby odwracanie normalnych miało sens, biorąc pod uwagę, że duplikat zostanie zabarwiony płaskim jednolitym kolorem (tj. Nie ma żadnych fantazyjnych obliczeń oświetlenia, które zależą od normalnych). Ten sam efekt osiągnąłem po prostu skalując, płasko zabarwiając, a następnie usuwając przednie powierzchnie duplikatu.
Jet Blue,
6

W przypadku gładkich modeli (bardzo ważne) efekt ten jest dość prosty. W cieniowaniu fragmentów / pikseli potrzebujesz normalnej wartości cieniowanego fragmentu. Jeśli jest on bardzo zbliżony do prostopadłego ( dot(surface_normal,view_vector) <= .01może być konieczne granie z tym progiem), następnie pokoloruj fragment na czarny zamiast zwykłego koloru.

Takie podejście „zużywa” trochę modelu do wykonania szkicu. To może, ale nie musi być to, czego chcesz. Bardzo trudno jest stwierdzić na podstawie obrazu Pokemona, czy tak właśnie się dzieje. Zależy to od tego, czy spodziewasz się, że kontur zostanie zawarty w jakiejkolwiek sylwetce postaci, czy też wolisz, aby kontur otaczał sylwetkę (co wymaga innej techniki).

Punktem kulminacyjnym będzie dowolna część powierzchni, na której będzie przechodzić od przodu do tyłu, w tym „wewnętrzne krawędzie” (jak nogi na zielonym pokemonie lub jego głowie - niektóre inne techniki nie dodałyby do nich żadnego konturu ).

Przy takim podejściu obiekty, które mają twarde, nieładne krawędzie (jak sześcian), nie zostaną wyróżnione w pożądanych lokalizacjach. Oznacza to, że takie podejście w ogóle nie jest opcją; Nie mam pojęcia, czy wszystkie modele Pokemonów są płynne, czy nie.

Sean Middleditch
źródło
5

Najczęstszym sposobem, w jaki to widziałem, jest powtórzenie renderowania w twoim modelu. Zasadniczo powiel go i odwróć normalne, i wepchnij to w moduł cieniujący wierzchołek. W module cieniującym przeskaluj każdy wierzchołek wzdłuż jego normalnej. W module cieniującym piksele / fragmenty narysuj kolor czarny. To da ci zarówno zarysy zewnętrzne, jak i wewnętrzne, takie jak wokół ust, oczu itp. Jest to w rzeczywistości dość tani call remis, jeśli nic innego nie jest ogólnie tańszy niż post przetwarzanie linii, w zależności od liczby modeli i ich złożoności. Guilty Gear Xrd używa tej metody, ponieważ łatwo jest kontrolować grubość linii za pomocą koloru wierzchołka.

Drugi sposób wykonywania linii wewnętrznych nauczyłem się z tej samej gry. Na mapie UV wyrównaj teksturę wzdłuż osi u lub v, szczególnie w obszarach, w których chcesz uzyskać linię wewnętrzną. Narysuj czarną linię wzdłuż każdej osi i przesuń współrzędne UV do lub z tej linii, aby utworzyć linię wewnętrzną.

Zobacz wideo z GDC dla lepszego wyjaśnienia: https://www.youtube.com/watch?v=yhGjCzxJV3E

Andrew Q
źródło
5

Jednym ze sposobów na wykonanie konspektu jest użycie naszych modeli normalnych wektorów. Wektory normalne to wektory, które są prostopadłe do ich powierzchni (skierowane od powierzchni). Sztuczka polega na podzieleniu modelu postaci na dwie części. Wierzchołki skierowane w stronę aparatu i wierzchołki skierowane w stronę przeciwną do aparatu. Nazwiemy je odpowiednio PRZÓD i POWRÓT.

Na potrzeby konspektu bierzemy nasze ZWROTY wierzchołki i nieznacznie przesuwamy je w kierunku ich normalnych wektorów. Pomyśl o tym, jakby część naszej postaci odwrócona od aparatu była nieco grubsza. Kiedy to zrobimy, przydzielamy im wybrany przez nas kolor i mamy ładny kontur.

wprowadź opis zdjęcia tutaj

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

Linia 41: Ustawienie „Cull Front” mówi modułowi cieniującemu, aby wykonał ubój na wierzchołkach skierowanych do przodu. Oznacza to, że w tym przejściu zignorujemy wszystkie wierzchołki skierowane do przodu. Pozostaje nam strona WSTECZ, którą chcemy nieco zmodyfikować.

Linie 51–53: Matematyka ruchów wierzchołków wzdłuż ich normalnych wektorów.

Linia 54: Ustawienie koloru wierzchołka na wybrany przez nas kolor zdefiniowany we właściwościach modułu cieniującego.

Przydatny link: http://wiki.unity3d.com/index.php/Silhouette-Outlined_Diffuse


Aktualizacja

inny przykład

wprowadź opis zdjęcia tutaj

wprowadź opis zdjęcia tutaj

   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
źródło
Dlaczego użycie bufora szablonów w zaktualizowanym przykładzie?
Tara
Ach, teraz to rozumiem. W drugim przykładzie zastosowano podejście, które generuje jedynie kontury zewnętrzne, w przeciwieństwie do pierwszego. Możesz wspomnieć o tym w swojej odpowiedzi.
Tara
0

Jednym ze wspaniałych sposobów na to jest renderowanie sceny na teksturze Framebuffer , a następnie renderowanie tej tekstury podczas filtrowania Sobela na każdym pikselu, co jest łatwą techniką wykrywania krawędzi. W ten sposób możesz nie tylko stworzyć piksele sceny (ustawienie niskiej rozdzielczości tekstury Framebuffer), ale także mieć dostęp do wszystkich wartości pikseli, aby Sobel działał.

Ross Myhovych
źródło