Bagaimana saya bisa menggambar garis besar di sekitar model 3D?

47

Bagaimana saya bisa menggambar garis besar di sekitar model 3D? Saya merujuk pada sesuatu seperti efek dalam game Pokemon baru-baru ini, yang tampaknya memiliki garis piksel tunggal di sekitarnya:

masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini

portal mistik
sumber
Apakah Anda menggunakan OpenGL? Jika demikian, Anda harus mencari di google cara menggambar garis besar untuk model dengan OpenGL.
oxysoft
1
Jika Anda mengacu pada gambar-gambar tertentu yang Anda masukkan di sana, saya dapat mengatakan dengan kepastian 95% bahwa itu adalah sprite 2D yang digambar tangan, bukan model 3D
Panda Pajama
3
@ Pandaanda: Tidak, itu hampir pasti model 3D. Ada beberapa kecerobohan dalam apa yang seharusnya menjadi garis keras di beberapa frame yang saya tidak harapkan dari sprite yang digambar dengan tangan, dan lagi pula itulah dasarnya bagaimana model 3D dalam game terlihat. Saya kira saya tidak dapat menjamin 100% untuk gambar-gambar spesifik itu, tetapi saya tidak dapat membayangkan mengapa ada orang yang berupaya memalsukannya.
CA McCann
Game mana itu, khususnya? Itu terlihat cantik.
Vegard
@ Legenda Makhluk dengan lobak di punggungnya adalah Bulbasaur dari gim Pokémon.
Damian Yerrick

Jawaban:

28

Saya tidak berpikir ada jawaban lain di sini yang akan mencapai efek di Pokemon X / Y. Saya tidak tahu persis bagaimana itu dilakukan, tetapi saya menemukan cara yang sepertinya cukup banyak apa yang mereka lakukan dalam permainan.

Dalam Pokémon X / Y, garis besar digambar di sekitar tepi siluet dan di tepi non-siluet lainnya (seperti di mana telinga Raichu bertemu kepalanya di screenshot berikut).

Raichu

Melihat jaring Raichu di Blender, Anda dapat melihat telinga (disorot dengan warna oranye di atas) hanyalah objek terpisah dan terputus yang memotong kepala, menciptakan perubahan mendadak pada permukaan normal.

Berdasarkan hal itu, saya mencoba membuat garis besar berdasarkan normals, yang memerlukan rendering dalam dua lintasan:

Pass pertama : Render model (bertekstur dan cel-shaded) tanpa garis besar, dan render normals ruang kamera ke target render kedua.

Lulus kedua : Lakukan filter deteksi tepi layar penuh di atas normals dari lintasan pertama.

Dua gambar pertama di bawah ini menunjukkan output dari pass pertama. Yang ketiga adalah garis besar dengan sendirinya, dan yang terakhir adalah hasil gabungan akhir.

Dratini

Inilah shader fragmen OpenGL yang saya gunakan untuk deteksi tepi pada lintasan kedua. Itu yang terbaik yang bisa saya lakukan, tetapi mungkin ada cara yang lebih baik. Ini mungkin tidak dioptimalkan dengan sangat baik juga.

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

Dan sebelum memberikan pass pertama, saya menghapus target render normals ke vektor yang menghadap jauh dari kamera:

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

Saya membaca di suatu tempat (saya akan memberikan tautan di komentar) bahwa Nintendo 3DS menggunakan pipeline fungsi-tetap alih-alih shader, jadi saya kira ini tidak persis seperti yang dilakukan dalam permainan, tetapi untuk saat ini saya ' Saya yakin metode saya cukup dekat.

KTC
sumber
Informasi tentang perangkat keras Nintendo 3DS: tautan
KTC
Solusi yang sangat bagus! Bagaimana Anda memperhitungkan kedalaman di shader Anda? (Dalam kasus pesawat di depan yang lain misalnya, keduanya memiliki normal yang sama sehingga tidak ada garis besar yang akan ditarik)
ingham
@ingham Kasus itu jarang muncul pada karakter organik sehingga saya tidak perlu menanganinya, dan sepertinya permainan sebenarnya juga tidak menanganinya. Dalam permainan sebenarnya Anda kadang-kadang dapat melihat garis hilang ketika normalnya sama, tetapi saya tidak berpikir orang biasanya akan melihatnya.
KTC
Saya agak skeptis tentang 3DS yang dapat dijalankan untuk menjalankan efek layar penuh berbasis shader seperti itu. Dukungan shadernya belum sempurna (jika ada).
Tara
17

Efek ini sangat umum dalam gim yang menggunakan efek cel shading, tetapi sebenarnya adalah sesuatu yang dapat diterapkan secara terpisah dari gaya cel shading.

Apa yang Anda gambarkan disebut "fitur edge rendering," dan dalam proses umum menyoroti berbagai kontur dan garis besar model. Ada banyak teknik yang tersedia dan banyak makalah tentang masalah ini.

Teknik sederhana adalah membuat hanya tepi siluet, garis besar. Ini dapat dilakukan sesederhana merender model asli dengan tulisan stensil, dan kemudian merendernya kembali dalam mode bingkai rangka tebal, hanya jika tidak ada nilai stensil. Lihat disini untuk contoh implementasi.

Itu tidak akan menyoroti kontur interior dan tepi lipatan, meskipun (seperti yang ditunjukkan pada gambar Anda). Secara umum, untuk melakukan itu secara efektif, Anda perlu mengekstraksi informasi tentang tepi mesh (berdasarkan diskontinuitas pada normals wajah di kedua sisi tepi, dan membangun struktur data yang mewakili setiap tepi.

Anda kemudian dapat menulis shader untuk mengusir atau membuat tepi tersebut sebagai geometri biasa melebihi model dasar Anda (atau bersamaan dengan itu). Posisi tepi, dan normals dari permukaan yang berdekatan relatif terhadap vektor tampilan, digunakan untuk menentukan apakah tepi tertentu dapat digambar.

Anda dapat menemukan diskusi lebih lanjut, detail, dan makalah dengan berbagai contoh di internet. Sebagai contoh:

Josh
sumber
1
Saya dapat mengkonfirmasi bahwa metode stensil (dari flipcode.com) berfungsi dan terlihat sangat bagus. Anda dapat memberikan ketebalan dalam koordinat layar sehingga ketebalan garis tidak tergantung pada ukuran model (atau pada bentuk model).
Vegard
1
Salah satu teknik yang tidak Anda sebutkan adalah efek border-shading pasca-pemrosesan yang sering digunakan bersama dengan cel-shading yang mencari piksel dengan tinggi dz/dxdan / ataudz/dy
bcrist
8

Cara paling sederhana untuk melakukan ini, umum pada perangkat keras yang lebih lama sebelum pixel / fragmen shaders, dan masih digunakan pada ponsel, adalah untuk menduplikasi model, membalikkan urutan belitan vertex sehingga model menampilkan luar dalam (atau jika Anda mau, Anda dapat lakukan ini di alat pembuatan aset 3D Anda, katakanlah Blender, dengan membalikkan permukaan normal - hal yang sama), kemudian rentangkan seluruh duplikat sedikit di sekitar pusatnya, dan akhirnya warna / tekstur duplikat ini sepenuhnya hitam. Ini menghasilkan garis besar di sekitar model asli Anda, jika model sederhana seperti kubus. Untuk model yang lebih kompleks dengan bentuk cekung (seperti pada gambar di bawah), perlu untuk secara manual mengubah model duplikat menjadi "lebih gemuk" daripada rekan aslinya, seperti Minkowski Sumdalam 3D. Anda bisa mulai dengan mendorong setiap simpul sedikit di sepanjang normalnya untuk membentuk garis kerangka, seperti yang dilakukan oleh Blender's Shrink / Fatten transform.

Pendekatan ruang layar / piksel shader cenderung lebih lambat dan lebih sulit untuk diterapkan dengan baik , tetapi OTOH tidak menggandakan jumlah simpul di dunia Anda. Jadi, jika Anda melakukan pekerjaan poli tinggi, sebaiknya pilih pendekatan itu. Mengingat konsol modern dan kapasitas desktop untuk memproses geometri, saya tidak khawatir tentang faktor 2 sama sekali . Gaya kartun = poli rendah pasti, sehingga menduplikasi geometri termudah.

Anda dapat menguji efeknya sendiri dalam mis. Blender tanpa menyentuh kode apa pun. Garis besar harus terlihat seperti gambar di bawah ini, perhatikan bagaimana beberapa internal, misalnya di bawah lengan. Lebih detail di sini .

masukkan deskripsi gambar di sini.

Insinyur
sumber
1
Bisakah Anda jelaskan, bagaimana "memperluas seluruh duplikat sedikit di sekitar pusatnya" sesuai dengan gambar ini, karena penskalaan sederhana di sekitar pusat tidak akan bekerja untuk senjata dan bagian lain yang tidak konsentris, itu juga tidak akan berfungsi untuk model yang memiliki lubang di dalamnya.
Kromster mengatakan mendukung Monica
@ KromStern Dalam beberapa kasus , himpunan bagian dari simpul perlu diskalakan dengan tangan untuk mengakomodasi. Jawaban yang diubah.
Insinyur
1
Adalah umum untuk mendorong simpul keluar di sepanjang permukaan lokal mereka normal, tetapi ini dapat menyebabkan mesh garis yang diperluas membelah sepanjang tepi yang keras
DMGregory
Terima kasih! Saya tidak berpikir ada gunanya membalik normals, mengingat bahwa duplikat akan diwarnai warna solid datar (yaitu tidak ada perhitungan pencahayaan mewah yang tergantung pada normals). Saya mencapai efek yang sama hanya dengan penskalaan, pewarnaan datar, dan kemudian memoles wajah depan duplikat.
Jet Blue
6

Untuk model yang halus (sangat penting), efek ini cukup sederhana. Dalam shader fragmen / piksel Anda, Anda perlu normal dari fragmen yang diarsir. Jika sangat dekat dengan tegak lurus ( dot(surface_normal,view_vector) <= .01- Anda mungkin perlu bermain dengan ambang itu) maka warnai fragmen hitam daripada warna yang biasa.

Pendekatan ini "mengkonsumsi" sedikit model untuk melakukan garis besar. Ini mungkin atau mungkin bukan yang Anda inginkan. Sangat sulit untuk mengetahui dari gambar Pokemon jika ini yang sedang dilakukan. Itu tergantung pada apakah Anda mengharapkan garis besar untuk dimasukkan dalam siluet karakter atau jika Anda lebih suka memiliki garis besar melampirkan siluet (yang membutuhkan teknik yang berbeda).

Sorotan akan berada pada bagian mana pun dari permukaan di mana ia bertransisi dari menghadap ke depan ke menghadap ke belakang, termasuk "tepi bagian dalam" (seperti kaki pada Pokemon hijau, atau kepalanya - beberapa teknik lain tidak akan menambahkan garis besar pada yang ).

Objek yang memiliki tepi yang keras dan tidak mulus (seperti kubus) tidak akan menerima sorotan di lokasi yang diinginkan dengan pendekatan ini. Itu berarti pendekatan ini sama sekali bukan opsi dalam beberapa kasus; Saya tidak tahu apakah model Pokemon semuanya mulus atau tidak.

Sean Middleditch
sumber
5

Cara paling umum yang pernah saya lihat dilakukan adalah melalui render pass kedua pada model Anda. Pada dasarnya, duplikat dan balik normals, dan dorong itu menjadi vertex shader. Dalam shader, skala setiap simpul sepanjang normalnya. Dalam pixel / fragmen shader, gambarlah hitam. Itu akan memberi Anda garis besar eksternal dan internal, seperti di sekitar bibir, mata, dll. Ini sebenarnya adalah panggilan undian yang cukup murah, jika tidak ada yang lain itu biasanya lebih murah daripada pasca memproses garis, tergantung pada jumlah model dan kompleksitasnya. Guilty Gear Xrd menggunakan metode ini karena mudah untuk mengontrol ketebalan garis melalui warna titik.

Cara kedua melakukan garis dalam saya belajar dari permainan yang sama. Dalam peta UV Anda, sejajarkan tekstur Anda sepanjang sumbu u atau v, terutama di daerah di mana Anda ingin garis dalam. Gambarlah garis hitam di sepanjang salah satu sumbu, dan gerakkan koordinat UV Anda ke dalam atau ke luar garis itu untuk membuat garis dalam.

Lihat video dari GDC untuk penjelasan yang lebih baik: https://www.youtube.com/watch?v=yhGjCzxJV3E

Andrew Q
sumber
5

Salah satu cara untuk membuat garis besar adalah dengan menggunakan vektor normal model kami. Vektor normal adalah vektor yang tegak lurus terhadap permukaannya (menunjuk menjauh dari permukaan). Kuncinya di sini adalah untuk membagi model karakter Anda menjadi dua bagian. Verteks yang menghadap kamera dan simpul yang menghadap jauh dari kamera. Kami akan memanggil mereka FRONT dan BACK masing-masing.

Untuk garis besarnya kita ambil simpul KEMBALI kita dan gerakkan sedikit ke arah vektor normalnya. Pikirkan itu seperti membuat bagian dari karakter kita yang menghadap jauh dari kamera sedikit lebih gemuk. Setelah kita selesai melakukannya, kita memberi mereka warna pilihan kita dan kita memiliki garis besar yang bagus.

masukkan deskripsi gambar di sini

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

Baris 41: Pengaturan "Cull Front" memberi tahu shader untuk melakukan pemusnahan pada simpul yang menghadap ke depan. Ini berarti bahwa kita akan mengabaikan semua simpul yang menghadap ke depan dalam lintasan ini. Kita dibiarkan dengan sisi KEMBALI yang ingin kita manipulasi sedikit.

Garis 51-53: Matematika memindahkan simpul di sepanjang vektor normal mereka.

Baris 54: Mengatur warna titik ke warna pilihan kami yang ditentukan dalam properti shaders.

Tautan Berguna: http://wiki.unity3d.com/index.php/Silhouette-Outlined_Diffuse


Memperbarui

contoh lain

masukkan deskripsi gambar di sini

masukkan deskripsi gambar di sini

   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"
        }
Sayy Morteza Kamali
sumber
Mengapa menggunakan buffer stensil dalam contoh yang diperbarui?
Tara
Ah saya mengerti sekarang. Contoh ke-2 menggunakan pendekatan yang hanya menghasilkan garis besar eksternal, tidak seperti yang pertama. Anda mungkin ingin menyebutkan itu dalam jawaban Anda.
Tara
0

Salah satu cara terbaik untuk melakukannya adalah dengan membuat adegan Anda pada tekstur Framebuffer , untuk kemudian membuat tekstur itu sambil melakukan Sobel Filtering pada setiap piksel, yang merupakan teknik mudah untuk deteksi tepi. Dengan cara ini Anda tidak hanya dapat membuat adegan menjadi pixelated (menetapkan resolusi rendah ke tekstur Framebuffer), tetapi juga memiliki akses ke setiap nilai piksel untuk membuat Sobel berfungsi.

Ross Myhovych
sumber