Як я можу намалювати контури навколо 3D-моделей?


47

Як я можу намалювати контури навколо 3D-моделей? Я маю на увазі щось подібне до ефектів недавньої гри Pokemon, які, схоже, мають навколо себе один піксель:

введіть тут опис зображення введіть тут опис зображення


Ви використовуєте OpenGL? Якщо так, вам слід пошукати в Google, як намалювати контури моделі за допомогою OpenGL.
oxysoft

1
Якщо ви маєте на увазі ті конкретні зображення, які ви розміщуєте там, я можу з 95% впевненістю сказати, що це намальовані вручну двовимірні спрайти, а не 3D-моделі
піжама Panda

3
@PandaPajama: Ні, це майже напевно тривимірні моделі. Існує деяка неохайність у тому, що має бути жорстким рядком у деяких кадрах, чого я не очікував би від намальованих руками спрайтів, і як би там не було, як виглядають ігрові 3D-моделі. Я думаю, я не можу гарантувати 100% для цих конкретних зображень, але не можу уявити, чому хтось намагатиметься їх підробити.
CA McCann

Яка гра це конкретно? Це виглядає чудово.
Вегард

@Vegard Істота з ріпою на спині - це Бульбазаур із гри Покемон.
Damian Yerrick

Відповіді:


28

Я не думаю, що жоден з інших відповідей тут не дасть ефекту в Pokémon X / Y. Я не можу точно знати, як це робиться, але я придумав спосіб, який, здається, майже все, що вони роблять у грі.

У Покемоні X / Y контури малюються як навколо країв силуету, так і на інших не-силуетних краях (наприклад, де вуха Райчу відповідають його голові на наступному знімку екрана).

Райчу

Подивившись на сітку Райху в Блендері, ви можете побачити вухо (підкреслене помаранчевим кольором вгорі) - це лише окремий від'єднаний предмет, який перетинає голову, створюючи різкі зміни поверхневих норм.

Виходячи з цього, я спробував створити контур на основі нормалів, що вимагає надання в два проходи:

Перший прохід : Візуалізуйте модель (текстурованою та затіненою) без контурів та переведіть норми простору камери на другу ціль візуалізації.

Другий прохід : зробіть фільтр повного екрану виявлення країв над нормальними з першого проходу.

Перші два зображення нижче показують результати першого проходу. Третій - контур сам по собі, а останній - кінцевий комбінований результат.

Дратині

Ось фрагмент шейдера OpenGL, який я використовував для виявлення ребер у другому проході. Це найкраще, що я міг придумати, але може бути кращий спосіб. Це, мабуть, не дуже добре оптимізовано.

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

І перед тим, як зробити перший прохід, я очищаю ціль відтворення нормалів у вектор, звернений убік від камери:

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

Я десь прочитав (я покладу посилання в коментарях), що Nintendo 3DS використовує конвеєр з фіксованою функцією замість шейдерів, тому, мабуть, це не може бути саме так, як це робиться в грі, але поки що я ' Переконаний, мій метод досить близький.


Інформація про апаратне забезпечення Nintendo 3DS: посилання
KTC

Дуже приємне рішення! Як би ви взяли до уваги глибину свого шейдера? (Наприклад, якщо літак напроти іншого, наприклад, обидва мають однакові норми, тому контур не буде намальовано)
ingham

@ingham Цей випадок виникає досить рідко щодо органічного характеру, що мені не потрібно було це впоратися, і схоже, що справжня гра теж не справляється. У реальній грі іноді можна побачити, як контури зникають, коли нормали однакові, але я не думаю, що люди зазвичай це помічають.
KTC

Я трохи скептично ставився до того, що 3DS може працювати за допомогою шейдерних повноекранних ефектів на зразок цього. Його підтримка шейдера є рудиментарною (якщо вона навіть є).
Тара

17

Цей ефект особливо часто зустрічається в іграх, в яких використовуються ефекти затінення чал, але насправді це те, що можна застосувати незалежно від стилю затінення чела.

Те, що ви описуєте, називається "відображення крайових зображень", і загалом це процес виділення різних контурів та обрисів моделі. Існує багато методик і багато робіт з цього питання.

Проста техніка полягає в наданні лише краю силуету, самого контуру. Це можна зробити так само просто, як надання оригінальної моделі з трафаретним записом, а потім рендеринг знову в режимі товстого каркаса, лише там, де не було значення трафарету. Дивіться тут приклад реалізації.

Це не підкреслить внутрішній контур та зморшки країв (як показано на малюнках). Як правило, щоб зробити це ефективно, вам потрібно отримати інформацію про краї сітки (на основі розривів у нормальних сторонах обличчя по обидві сторони краю) та створити структуру даних, що представляє кожен край.

Потім ви можете написати шейдери, щоб видавити або іншим чином зробити ці краї як звичайна геометрія над базовою моделлю (або разом з нею). Положення ребра та нормалі сусідніх граней відносно вектора перегляду використовуються для визначення того, чи можна намалювати конкретний край.

Ви можете знайти подальшу дискусію, деталі та статті з різними прикладами в Інтернеті. Наприклад:


1
Я можу підтвердити, що метод трафарету (від flipcode.com) працює і виглядає дуже приємно. Ви можете надати товщину в координатах екрана, щоб товщина контуру не залежала ні від розміру моделі (ні від форми моделі).
Вегард

1
Один з методів, про який ви не згадували, - це ефект після затінення кордону після обробки, який часто використовується в поєднанні з затіненням dz/dxdz/dy
чела,

8

Найпростіший спосіб зробити це, поширений на старішому апаратному забезпеченні перед шейдерами пікселів / фрагментів, і все ще використовується на мобільному пристрої, - це дублювати модель, змінювати порядок намотування вершин, щоб модель відображалася зсередини (або якщо ви хочете, ви можете зробіть це у вашому інструменті створення 3D-активів, скажімо, Blender, перегортаючи нормальні поверхні - те саме), потім трохи розгорніть весь дублікат навколо його центру, і нарешті колір / текстуру цього дубліката буде повністю чорним. Це призводить до обрисів навколо вашої оригінальної моделі, якщо це проста модель, наприклад куб. Для більш складних моделей з увігнутими формами (як, наприклад, на зображенні нижче), потрібно вручну підкрутити подвійну модель, щоб бути дещо "товстішою", ніж її оригінальний аналог, як сума Minkowskiв 3D. Ви можете почати, натиснувши кожну вершину трохи вздовж її звичайної, щоб утворити контурну сітку, як це робить трансформація Blender's Shrink / Fatten.

Екранні простір / піксельних підходи , як правило, повільніше і важче реалізувати добре , але Ото не подвоїться кількість вершин у вашому світі. Тож якщо ви займаєтеся високомобільною роботою, краще вибирайте такий підхід. З огляду на сучасні консолі і робочого столу потужність для обробки геометрії, я б не турбуватися про те 2 рази взагалі . Мультиплікаційний стиль = низький поліф напевно, таким чином, дублювати геометрію найпростіше.

Ви можете перевірити ефект на собі, наприклад, Blender, не торкаючись жодного коду. Контури повинні виглядати як на зображенні нижче, зверніть увагу на те, наскільки вони є внутрішніми, наприклад, під пахвою. Детальніше тут .

введіть тут опис зображення.


1
Поясніть, будь ласка, як "трохи розширити весь дублікат навколо його центру" відповідає цій картині, оскільки просте масштабування навколо центру не буде працювати для зброї та інших деталей, які не є концентричними, а також не працюватимуть для будь-якої моделі, яка має отвори в ньому.
Кромстер каже, що підтримую Моніку

@KromStern У деяких випадках підмножини вершин потрібно масштабувати вручну для розміщення. Змінена відповідь.
Інженер

1
Як правило, висунути вершини вздовж їх локальної поверхні нормально, але це може призвести до розколювання розширеної контурної сітки уздовж жорстких країв
DMGregory

Дякую! Я не думаю, що немає сенсу гортати нормали, враховуючи, що дублікат буде кольоровим плоским суцільним кольором (тобто немає фантазійних розрахунків освітлення, які залежать від нормальних). Такого ж ефекту я домігся просто масштабуванням, плоским фарбуванням, а потім відсіканням лицьових сторін дубліката.
Jet Blue

6

Для гладких моделей (дуже важливо) цей ефект досить простий. У вашому шейдері фрагмент / піксель вам знадобиться нормальний фрагмент, що затінюється. Якщо він дуже близький до перпендикуляра ( dot(surface_normal,view_vector) <= .01- можливо, вам доведеться пограти з цим порогом), тоді пофарбуйте фрагмент чорним кольором замість його звичайного кольору.

Такий підхід «споживає» трохи моделі, щоб зробити контур. Це може бути або не бути тим, що ви хочете. З картини Покемона дуже важко сказати, чи саме це робиться. Це залежить від того, якщо ви очікуєте, що контур буде включений до будь-якого силуету персонажа або якщо ви хочете, щоб контур додав силует (для чого потрібна інша техніка).

Родзинка буде на будь-якій частині поверхні, де вона переходить від лицьової до зворотної, включаючи "внутрішні краї" (наприклад, ноги на зеленому Покемоні або його голова - деякі інші методи не додадуть жодних обрисів до цих ).

Об'єкти, які мають жорсткі, не гладкі краї (як кубик), не отримають виділення в потрібних місцях при такому підході. Це означає, що такий підхід в деяких випадках взагалі не є можливим; Я поняття не маю, чи всі моделі Pokemon плавні чи ні.


5

Найпоширеніший спосіб, який я бачив, - це через другий пропуск на вашій моделі. По суті, скопіюйте його і переверніть нормали, і засуньте їх у вершину шейдера. У шейдері масштабуйте кожну вершину вздовж її нормальної величини. На шейдері пікселя / фрагмента намалюйте чорний колір. Це дасть вам як зовнішні, так і внутрішні контури, як-от навколо губ, очей тощо. Це насправді досить дешевий дзвінок, якщо нічого іншого, як правило, дешевше, ніж після обробки лінії, залежно від кількості моделей та їх складності. Guilty Gear Xrd використовує цей метод, оскільки легко контролювати товщину лінії за допомогою вершинного кольору.

Другий спосіб виконання внутрішніх ліній я навчився з тієї ж гри. На УФ-карті вирівняйте текстуру вздовж осі u або v, особливо в тих місцях, де потрібно внутрішню лінію. Намалюйте чорну лінію вздовж будь-якої осі та перемістіть свої УФ-координати в цю лінію або поза нею, щоб створити внутрішню лінію.

Дивіться відео від GDC для кращого пояснення: https://www.youtube.com/watch?v=yhGjCzxJV3E


5

Один із способів скласти контур - це використовувати в наших моделях звичайні вектори. Нормальні вектори - це вектори, які перпендикулярні до їх поверхні (спрямовані від поверхні). Хитрість тут полягає в тому, щоб розділити свою модель персонажа на дві частини. Вершини, звернені до камери, і вершини, звернені в сторону від камери. Ми будемо називати їх відповідно FRONT та BACK.

Для контуру беремо наші вершини НАЗАД і злегка переміщуємо їх у напрямку їх нормальних векторів. Подумайте про це як про те, щоб зробити частину нашого персонажа, яка відводиться від камери, трохи товстішою. Після того, як ми це зробили, ми присвоюємо їм колір на наш вибір, і ми маємо хороший контур.

введіть тут опис зображення

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

Рядок 41: Настройка "Спереду фронту" повідомляє шейдеру виконувати відсікання на вершинах, що виходять спереду. Це означає, що ми будемо ігнорувати всі вершини, спрямовані перед обличчям у цьому проході. Нам залишається НАЗАД, що ми хочемо трохи маніпулювати.

Рядки 51-53: математика переміщення вершин уздовж їхніх нормальних векторів.

Рядок 54: Встановлення кольору вершини на наш колір вибору, визначений у властивостях шейдерів.

Корисне посилання: http://wiki.unity3d.com/index.php/Silhouette-Outlined_Diffuse


Оновлення

ще один приклад

введіть тут опис зображення

введіть тут опис зображення

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

Навіщо використовувати буфер трафарету в оновленому прикладі?
Тара

Ах, я зрозумів це зараз. У другому прикладі використовується підхід, який генерує лише зовнішні контури, на відміну від першого. Ви можете згадати це у своїй відповіді.
Тара

0

Один із чудових способів зробити це - візуалізувати свою сцену на текстурі Framebuffer , а потім відтворити цю текстуру, роблячи фільтрацію Sobel на кожному пікселі, що є простою методикою виявлення краю. Таким чином, ви можете не тільки зробити сцену піксельною (встановивши низьку роздільну здатність до текстури Framebuffer), але і мати доступ до всіх значень пікселів, щоб Sobel працював.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.