Làm cách nào tôi có thể vẽ phác thảo xung quanh các mô hình 3D?


47

Làm cách nào tôi có thể vẽ phác thảo xung quanh các mô hình 3D? Tôi đang đề cập đến một cái gì đó giống như các hiệu ứng trong một trò chơi Pokemon gần đây, dường như có một phác thảo một pixel xung quanh chúng:

nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây


Bạn đang sử dụng OpenGL? Nếu vậy, bạn nên tìm kiếm trên google cách vẽ phác thảo cho một mô hình với OpenGL.
oxysoft

1
Nếu bạn đang đề cập đến những hình ảnh cụ thể mà bạn đưa vào đó, tôi có thể nói chắc chắn 95% rằng đó là những họa tiết 2D được vẽ bằng tay, không phải mô hình 3D
Panda Pajama

3
@PandaPajama: Không, đó gần như là những mô hình 3D. Có một chút chậm chạp trong những đường kẻ cứng trong một số khung hình mà tôi không mong đợi từ các họa tiết vẽ tay, và dù sao đó cũng cơ bản là các mô hình 3D trong trò chơi trông như thế nào. Tôi cho rằng tôi không thể đảm bảo 100% cho những hình ảnh cụ thể đó, nhưng tôi không thể tưởng tượng được tại sao mọi người lại nỗ lực làm giả chúng.
CA McCann

Trò chơi đó là gì, cụ thể? Nó trông tuyệt đẹp.
Vegard

@Vegard Sinh vật có củ cải trên lưng là một con Bóng đèn từ Pokémon trò chơi.
Damian Yerrick

Câu trả lời:


28

Tôi không nghĩ bất kỳ câu trả lời nào khác ở đây sẽ đạt được hiệu quả trong Pokémon X / Y. Tôi không thể biết chính xác nó đã được thực hiện như thế nào, nhưng tôi đã tìm ra một cách có vẻ giống như những gì họ làm trong trò chơi.

Trong Pokémon X / Y, các đường viền được vẽ cả xung quanh các cạnh bóng và trên các cạnh không bóng khác (như tai của Raichu gặp đầu trong ảnh chụp màn hình sau).

Raichu

Nhìn vào lưới của Raichu trong Blender, bạn có thể thấy tai (được tô màu cam ở trên) chỉ là một vật thể riêng biệt, bị ngắt kết nối với đầu, tạo ra sự thay đổi đột ngột trong các quy tắc bề mặt.

Dựa vào đó, tôi đã thử tạo phác thảo dựa trên các quy tắc, yêu cầu kết xuất theo hai lần:

Vượt qua đầu tiên : Kết xuất mô hình (có kết cấu và bóng mờ) mà không có đường viền và hiển thị các quy tắc không gian máy ảnh thành mục tiêu kết xuất thứ hai.

Vượt qua thứ hai : Thực hiện bộ lọc phát hiện cạnh toàn màn hình trên các thông số từ lần đầu tiên.

Hai hình ảnh đầu tiên bên dưới hiển thị kết quả đầu ra của lần đầu tiên. Thứ ba là bản phác thảo của chính nó, và cuối cùng là kết quả cuối cùng kết hợp.

Dratini

Đây là shader mảnh OpenGL mà tôi đã sử dụng để phát hiện cạnh trong đường chuyền thứ hai. Đó là điều tốt nhất tôi có thể nghĩ ra, nhưng có thể có một cách tốt hơn. Nó có lẽ cũng không được tối ưu hóa tốt.

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

Và trước khi kết xuất đường chuyền đầu tiên, tôi xóa mục tiêu kết xuất của các thông số thành một vectơ hướng ra khỏi máy ảnh:

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

Tôi đã đọc ở đâu đó (tôi sẽ đặt một liên kết trong các bình luận) rằng Nintendo 3DS sử dụng một đường ống chức năng cố định thay vì các shader, vì vậy tôi đoán đây không thể là cách nó được thực hiện trong trò chơi, nhưng bây giờ tôi ' m thuyết phục phương pháp của tôi là đủ gần.


Thông tin về phần cứng Nintendo 3DS: link
KTC

Giải pháp rất hay! Làm thế nào bạn sẽ có chiều sâu trong tài khoản của bạn shader? (Trong trường hợp máy bay ở phía trước máy bay khác chẳng hạn, cả hai đều có cùng mức bình thường nên sẽ không có phác thảo nào)
ingham

@ingham Trường hợp đó xuất hiện không thường xuyên trên một nhân vật hữu cơ mà tôi không cần phải xử lý nó, và có vẻ như trò chơi thực sự cũng không xử lý nó. Trong trò chơi thực tế, đôi khi bạn có thể thấy đường viền biến mất khi các quy tắc giống nhau, nhưng tôi không nghĩ mọi người thường chú ý đến nó.
KTC

Tôi hơi nghi ngờ về việc 3DS có thể chạy được để chạy các hiệu ứng toàn màn hình dựa trên shader như thế. Hỗ trợ shader của nó là thô sơ (nếu nó thậm chí có bất kỳ).
Tara

17

Hiệu ứng này đặc biệt phổ biến trong các trò chơi sử dụng hiệu ứng cel shading, nhưng thực sự là thứ có thể được áp dụng độc lập với phong cách cel shading.

Những gì bạn đang mô tả được gọi là "kết xuất cạnh tính năng" và là trong quá trình chung để làm nổi bật các đường viền và đường viền khác nhau của một mô hình. Có nhiều kỹ thuật có sẵn và nhiều giấy tờ về chủ đề này.

Một kỹ thuật đơn giản là chỉ hiển thị các cạnh bóng, đường viền ngoài cùng. Điều này có thể được thực hiện đơn giản như hiển thị mô hình ban đầu bằng cách viết stprint, và sau đó kết xuất lại nó ở chế độ khung dây dày, chỉ khi không có giá trị stprint. Xem ở đây để thực hiện một ví dụ.

Tuy nhiên, điều đó sẽ không làm nổi bật các đường viền bên trong và các nếp gấp (như thể hiện trong hình ảnh của bạn). Nói chung, để làm điều đó một cách hiệu quả, bạn cần trích xuất thông tin về các cạnh của lưới (dựa trên sự không liên tục trong các quy tắc khuôn mặt ở hai bên của cạnh và xây dựng cấu trúc dữ liệu đại diện cho mỗi cạnh.

Sau đó, bạn có thể viết các shader để đùn hoặc hiển thị các cạnh đó dưới dạng hình học thông thường trên mô hình cơ sở của bạn (hoặc kết hợp với nó). Vị trí của một cạnh và các quy tắc của các mặt liền kề so với vectơ xem, được sử dụng để xác định xem một cạnh cụ thể có thể được vẽ hay không.

Bạn có thể tìm thêm thảo luận, chi tiết và giấy tờ với các ví dụ khác nhau trên internet. Ví dụ:


1
Tôi có thể xác nhận rằng phương pháp stprint (từ flipcode.com) hoạt động và trông thực sự tốt đẹp. Bạn có thể cung cấp độ dày trong tọa độ màn hình để độ dày của đường viền không phụ thuộc vào kích thước của mô hình (cũng không phụ thuộc vào hình dạng của mô hình).
Vegard

1
Một kỹ thuật bạn không đề cập đến là hiệu ứng tạo bóng viền sau xử lý thường được sử dụng cùng với cel-shading để tìm các pixel có độ cao dz/dxvà / hoặcdz/dy
bcrist

8

Cách đơn giản nhất để thực hiện việc này, phổ biến trên phần cứng cũ trước khi đổ bóng pixel / mảnh và vẫn được sử dụng trên thiết bị di động, là sao chép mô hình, đảo ngược thứ tự cuộn dây đỉnh để mô hình hiển thị từ trong ra ngoài (hoặc nếu bạn muốn, bạn có thể thực hiện điều này trong công cụ tạo tài sản 3D của bạn, giả sử Blender, bằng cách lật các quy tắc bề mặt - điều tương tự), sau đó mở rộng toàn bộ bản sao một chút xung quanh trung tâm của nó và cuối cùng tô màu / kết cấu này sao chép hoàn toàn màu đen. Điều này dẫn đến các phác thảo xung quanh mô hình ban đầu của bạn, nếu đó là một mô hình đơn giản như khối lập phương. Đối với các mô hình phức tạp hơn với các dạng lõm (chẳng hạn như trong hình bên dưới), cần phải chỉnh thủ công mô hình trùng lặp để hơi "béo" hơn so với đối tác ban đầu của nó, như một chồnở chế độ 3D. Bạn có thể bắt đầu bằng cách đẩy mỗi đỉnh ra một chút dọc theo bình thường của nó để tạo thành lưới phác thảo, như phép biến đổi Shrink / Fatten của Blender thực hiện.

Các cách tiếp cận đổ bóng không gian / pixel màn hình có xu hướng chậm hơn và khó thực hiện hơn , nhưng OTOH không nhân đôi số đỉnh trong thế giới của bạn. Vì vậy, nếu bạn đang làm công việc poly cao, tốt nhất nên chọn cách tiếp cận đó. Với giao diện điều khiển hiện đại và công suất máy tính để bàn để xử lý hình học, tôi không lo lắng về hệ số 2 ở tất cả . Cartoon-style = low poly chắc chắn, do đó sao chép hình học là dễ nhất.

Bạn có thể tự kiểm tra hiệu ứng trong ví dụ Blender mà không cần chạm vào bất kỳ mã nào. Các phác thảo sẽ trông giống như hình ảnh bên dưới, lưu ý một số nội bộ, ví dụ như dưới cánh tay. Chi tiết hơn ở đây .

nhập mô tả hình ảnh ở đây.


1
Bạn có thể giải thích, làm thế nào "mở rộng toàn bộ bản sao xung quanh trung tâm của nó" phù hợp với hình ảnh này không, vì quy mô đơn giản xung quanh trung tâm sẽ không hoạt động cho cánh tay và các bộ phận khác không đồng tâm, nó cũng không hoạt động cho bất kỳ mô hình nào có lỗ hổng trong đó.
Kromster nói hỗ trợ Monica

@KromStern Trong một số trường hợp , các tập hợp con của đỉnh cần được thu nhỏ bằng tay để chứa. Sửa đổi câu trả lời.
Kỹ sư

1
Đó là thông thường để di chuyển các đỉnh dọc theo bề mặt địa phương của họ bình thường, nhưng điều này có thể gây ra những phác thảo mở rộng lưới để chia dọc theo các cạnh cứng
DMGregory

Cảm ơn! Tôi không nghĩ rằng có bất kỳ điểm nào trong việc lật các quy tắc, với điều kiện là bản sao sẽ được tô một màu đơn sắc phẳng (nghĩa là không có phép tính ánh sáng ưa thích nào phụ thuộc vào quy tắc). Tôi đã đạt được hiệu quả tương tự bằng cách chỉ chia tỷ lệ, tô màu phẳng, và sau đó loại bỏ các mặt trước của bản sao.
Jet Blue

6

Đối với các mô hình trơn (rất quan trọng), hiệu ứng này khá đơn giản. Trong shader mảnh / pixel của bạn, bạn sẽ cần bình thường của mảnh được tô bóng. Nếu nó rất gần với đường vuông góc ( dot(surface_normal,view_vector) <= .01- bạn có thể cần chơi với ngưỡng đó), sau đó tô màu cho mảnh màu đen thay vì màu thông thường của nó.

Cách tiếp cận này "tiêu thụ" một chút mô hình để thực hiện phác thảo. Điều này có thể hoặc không thể là những gì bạn muốn. Rất khó để nói từ bức tranh Pokemon nếu đây là những gì đang được thực hiện. Nó phụ thuộc vào việc bạn mong muốn phác thảo được bao gồm trong bất kỳ hình bóng nào của nhân vật hoặc nếu bạn muốn có phác thảo bao quanh hình bóng (đòi hỏi một kỹ thuật khác).

Điểm nổi bật sẽ nằm ở bất kỳ phần nào của bề mặt nơi nó chuyển từ mặt trước sang mặt sau, bao gồm cả "các cạnh bên trong" (như chân trên Pokemon xanh hoặc đầu của nó - một số kỹ thuật khác sẽ không thêm bất kỳ phác thảo nào vào đó ).

Các đối tượng có các cạnh cứng, không nhẵn (như khối lập phương) sẽ không nhận được điểm nổi bật ở các vị trí mong muốn với phương pháp này. Điều đó có nghĩa là phương pháp này hoàn toàn không phải là một lựa chọn trong một số trường hợp; Tôi không biết các mô hình Pokemon có trơn tru hay không.


5

Cách phổ biến nhất mà tôi đã thấy điều này được thực hiện là thông qua lần kết xuất thứ hai trên mô hình của bạn. Về cơ bản, sao chép nó và lật các quy tắc, và chuyển nó thành một shader đỉnh. Trong shader, chia tỷ lệ từng đỉnh dọc theo bình thường của nó. Trong shader pixel / mảnh, vẽ màu đen. Điều đó sẽ cung cấp cho bạn cả phác thảo bên ngoài và bên trong, như xung quanh môi, mắt, v.v ... Đây thực sự là một cuộc gọi rút thăm khá rẻ, nếu không có gì khác thường rẻ hơn so với xử lý bài, tùy thuộc vào số lượng mô hình và độ phức tạp của chúng. Guilty Gear Xrd sử dụng phương pháp này vì nó dễ dàng kiểm soát độ dày của đường thông qua màu đỉnh.

Cách thứ hai để thực hiện các dòng bên trong tôi đã học được từ cùng một trò chơi. Trong bản đồ UV của bạn, căn chỉnh kết cấu của bạn dọc theo trục u hoặc v, đặc biệt là trong các khu vực mà bạn muốn có một đường bên trong. Vẽ một đường màu đen dọc theo một trong hai trục và di chuyển tọa độ UV của bạn vào hoặc ra khỏi đường thẳng đó để tạo đường bên trong.

Xem video từ GDC để được giải thích rõ hơn: https://www.youtube.com/watch?v=yhGjCzxJV3E


5

Một trong những cách để tạo ra một phác thảo là sử dụng các mô hình vectơ bình thường của chúng tôi. Các vectơ bình thường là các vectơ vuông góc với bề mặt của chúng (chỉ ra khỏi bề mặt). Mẹo ở đây là chia mô hình nhân vật của bạn thành hai phần. Các đỉnh đối diện với máy ảnh và các đỉnh đối diện với máy ảnh. Chúng tôi sẽ gọi chúng là FRONT và BACK tương ứng.

Đối với phác thảo, chúng tôi lấy các đỉnh BACK của chúng tôi và di chuyển chúng một chút theo hướng của các vectơ bình thường của chúng. Hãy suy nghĩ về nó giống như làm cho một phần của nhân vật của chúng ta đang quay mặt ra khỏi máy ảnh một chút béo hơn. Sau khi hoàn thành, chúng tôi chỉ định cho họ một màu mà chúng tôi chọn và chúng tôi có một phác thảo đẹp.

nhập mô tả hình ảnh ở đây

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

Dòng 41: Cài đặt Mặt trước của Cull Cull cho người đổ bóng thực hiện việc loại bỏ trên các đỉnh đối diện với mặt trước. Nó có nghĩa là chúng ta sẽ bỏ qua tất cả các đỉnh phải đối mặt trong đường chuyền này. Chúng tôi chỉ còn lại phía BACK mà chúng tôi muốn thao tác một chút.

Dòng 51-53: Toán học về các đỉnh di chuyển dọc theo các vectơ bình thường của chúng.

Dòng 54: Đặt màu đỉnh cho màu lựa chọn của chúng tôi được xác định trong thuộc tính shader.

Liên kết hữu ích: http://wiki.unity3d.com/index.php/Sil Silhouette-Outlines_Diffuse


Cập nhật

một vi dụ khac

nhập mô tả hình ảnh ở đây

nhập mô tả hình ảnh ở đây

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

Tại sao việc sử dụng bộ đệm stpson trong ví dụ cập nhật?
Tara

À tôi hiểu rồi Ví dụ thứ 2 sử dụng một cách tiếp cận chỉ tạo ra các phác thảo bên ngoài, không giống như cách đầu tiên. Bạn có thể muốn đề cập đến điều đó trong câu trả lời của bạn.
Tara

0

Một trong những cách tuyệt vời là kết xuất cảnh của bạn trên kết cấu Framebuffer , sau đó kết xuất kết cấu đó trong khi thực hiện Lọc Sobel trên mỗi pixel, đây là một kỹ thuật dễ dàng để phát hiện cạnh. Bằng cách này, bạn không chỉ có thể làm cho cảnh được pixel hóa (đặt độ phân giải thấp cho kết cấu Framebuffer), mà còn có quyền truy cập vào mọi giá trị pixel để làm cho Sobel hoạt động.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.