Как я могу нарисовать контуры вокруг 3D-моделей?

47

Как я могу нарисовать контуры вокруг 3D-моделей? Я имею в виду что-то вроде эффектов в недавней игре про покемонов, которые кажутся однопиксельными очертаниями вокруг них:

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

мистический портал
источник
Вы используете OpenGL? Если это так, вы должны искать в Google, как рисовать контуры для модели с OpenGL.
oxysoft
1
Если вы имеете в виду те конкретные изображения, которые вы вставили туда, я могу с уверенностью сказать, что это двумерные спрайты, а не трехмерные модели, нарисованные от руки
Panda Pajama
3
@PandaPajama: Нет, это почти наверняка 3D-модели. В некоторых кадрах есть некоторая неряшливость в том, что должно быть жесткими линиями, которых я бы не ожидал от нарисованных от руки спрайтов, и в любом случае именно так выглядят 3D-модели в игре. Я полагаю, что не могу гарантировать 100% для этих конкретных изображений, но я не могу себе представить, почему кто-то попытается сфальсифицировать их.
CA McCann
Что это за игра, конкретно? Это выглядит великолепно.
Vegard
@Vegard Существо с репой на спине - это Бульбазавр из игры Покемон.
Дамиан Йеррик

Ответы:

28

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

В Pokémon X / Y контуры рисуются как по краям силуэта, так и по другим не-силуэтным краям (например, там, где уши Райчу встречаются с его головой на следующем скриншоте).

Raichu

Глядя на сетку Райчу в Blender, вы видите, что ухо (выделено оранжевым цветом выше) - это просто отдельный, не связанный объект, который пересекает голову, создавая резкое изменение нормалей поверхности.

Исходя из этого, я попытался сгенерировать контур на основе нормалей, который требует рендеринга в два прохода:

Первый проход : визуализируйте модель (текстурированную и cel-shaded) без контуров и визуализируйте нормали пространства камеры для второй цели рендеринга.

Второй проход : Сделайте полноэкранный фильтр обнаружения края по нормали с первого прохода.

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

Dratini

Вот фрагментный шейдер 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

Этот эффект особенно распространен в играх, в которых используются эффекты затенения cel, но на самом деле он может применяться независимо от стиля затенения cel.

То, что вы описываете, называется «рендеринг края элемента» и представляет собой процесс выделения различных контуров и контуров модели. Есть много доступных методик и много статей на эту тему.

Простая техника - визуализировать только край силуэта, самый внешний контур. Это можно сделать так же просто, как рендеринг исходной модели с записью трафарета, а затем рендеринг ее снова в режиме толстого каркаса, только там, где значение трафарета отсутствует. Смотрите здесь для примера реализации.

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

Затем вы можете написать шейдеры для выдавливания или иным образом визуализировать эти ребра как обычную геометрию поверх базовой модели (или в сочетании с ней). Положение ребра и нормали смежных граней относительно вектора вида используются для определения возможности рисования конкретного ребра.

Вы можете найти дальнейшее обсуждение, детали и документы с различными примерами в Интернете. Например:

Джош
источник
1
Я могу подтвердить, что метод трафарета (с flipcode.com) работает и выглядит действительно хорошо. Вы можете указать толщину в координатах экрана, чтобы толщина контура не зависела ни от размера модели (ни от формы модели).
Вегард
1
Одним из методов, который вы не упомянули, является эффект постобработки затенения границ, часто используемый в сочетании с затенением cel, который ищет пиксели с высоким dz/dxи / илиdz/dy
bcrist,
8

Самый простой способ сделать это, распространенный на старом оборудовании до пиксельных / фрагментных шейдеров и все еще используемый на мобильных устройствах, - это продублировать модель, изменить порядок намотки вершин так, чтобы модель отображалась наизнанку (или, если хотите, вы можете сделайте это в своем инструменте создания 3D-активов, скажем, Blender, переворачивая нормали поверхности - то же самое), затем слегка растяните весь дубликат вокруг его центра, и, наконец, раскрасьте / дублируйте текстуру этого дубликата полностью черным. Это приводит к контурам вокруг вашей исходной модели, если это простая модель, такая как куб. Для более сложных моделей с вогнутыми формами (например, на изображении ниже) необходимо вручную настроить дубликат модели, чтобы она была несколько «толще», чем ее исходный аналог, например сумма Минковского.в 3D. Вы можете начать с выталкивания каждой вершины немного вдоль ее нормали, чтобы сформировать контурную сетку, как это делает преобразование Blender Shrink / Fatten.

Экранные пространство / пиксельных подходы , как правило, медленнее и труднее реализовать хорошо , но Ото не удвоит число вершин в вашем мире. Так что, если вы делаете работу с высоким поли, лучше всего выбрать этот подход. Учитывая современные возможности консоли и рабочего стола для обработки геометрии, я бы не стал беспокоиться о коэффициенте 2 вообще . Cartoon-style = low poly наверняка, поэтому дублировать геометрию проще всего.

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

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

инженер
источник
1
Не могли бы вы объяснить, как «слегка расширить весь дубликат вокруг его центра» соответствует этой картине, потому что простое масштабирование вокруг центра не будет работать для рук и других деталей, которые не являются концентрическими, оно также не будет работать для любой модели, которая имеет дыры в нем.
Кромстер говорит, что поддерживает Монику
@KromStern В некоторых случаях подмножества вершин необходимо масштабировать вручную, чтобы приспособиться. Исправленный ответ.
инженер
1
Распространено выталкивать вершины вдоль их локальной нормали поверхности, но это может привести к расщеплению расширенной контурной сетки по жестким краям
DMGregory
Спасибо! Я не думаю, что есть смысл переворачивать нормали, учитывая, что дубликат будет окрашен в ровный сплошной цвет (т. Е. Никакие необычные вычисления освещения, которые зависят от нормалей). Я достиг того же эффекта, просто масштабируя, раскрашивая, а затем отбрасывая передние грани дубликата.
Jet Blue
6

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

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

Выделение будет на любой части поверхности, где оно переходит от передней к задней стороне, включая «внутренние края» (например, ноги на зеленом покемоне или его голову - некоторые другие методы не добавят какой-либо контур к этим ).

При таком подходе объекты с жесткими, негладкими краями (например, куб) не будут выделяться в нужных местах. Это означает, что в некоторых случаях этот подход вообще не подходит; Я понятия не имею, все ли модели Pokemon гладкие или нет.

Шон Миддледич
источник
5

Наиболее распространенный способ, которым я видел это, - это второй проход рендера на вашей модели. По сути, продублируйте его, переверните нормали и вставьте его в вершинный шейдер. В шейдере масштабируйте каждую вершину вдоль ее нормали. В пиксельном / фрагментном шейдере нарисуйте черный. Это даст вам как внешние, так и внутренние контуры, например, вокруг губ, глаз и т. Д. Это на самом деле довольно дешевый колл-розыгрыш, если вообще ничего дешевле, чем пост-обработка линии, в зависимости от количества моделей и их сложности. Guilty Gear Xrd использует этот метод, потому что легко контролировать толщину линии с помощью цвета вершины.

Второй способ выполнения внутренних линий я узнал из той же игры. На вашей УФ-карте выровняйте текстуру вдоль оси u или v, особенно в областях, где вы хотите получить внутреннюю линию. Нарисуйте черную линию вдоль любой оси и переместите свои UV-координаты в или из этой линии, чтобы создать внутреннюю линию.

Посмотрите видео из 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: настройка «Cull Front» указывает шейдеру выполнять выборку на передних вершинах. Это означает, что мы будем игнорировать все передние вершины в этом проходе. У нас осталась ОБРАТНАЯ сторона, которой мы хотим немного манипулировать.

Строки 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 Filtering для каждого пикселя, что является простой техникой для обнаружения краев. Таким образом, вы можете не только сделать сцену пикселированной (установив низкое разрешение для текстуры Framebuffer), но и получить доступ к значениям каждого пикселя, чтобы заставить работать Sobel.

Росс Михович
источник