Як я можу намалювати контури навколо 3D-моделей? Я маю на увазі щось подібне до ефектів недавньої гри Pokemon, які, схоже, мають навколо себе один піксель:
Як я можу намалювати контури навколо 3D-моделей? Я маю на увазі щось подібне до ефектів недавньої гри Pokemon, які, схоже, мають навколо себе один піксель:
Відповіді:
Я не думаю, що жоден з інших відповідей тут не дасть ефекту в 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 використовує конвеєр з фіксованою функцією замість шейдерів, тому, мабуть, це не може бути саме так, як це робиться в грі, але поки що я ' Переконаний, мій метод досить близький.
Цей ефект особливо часто зустрічається в іграх, в яких використовуються ефекти затінення чал, але насправді це те, що можна застосувати незалежно від стилю затінення чела.
Те, що ви описуєте, називається "відображення крайових зображень", і загалом це процес виділення різних контурів та обрисів моделі. Існує багато методик і багато робіт з цього питання.
Проста техніка полягає в наданні лише краю силуету, самого контуру. Це можна зробити так само просто, як надання оригінальної моделі з трафаретним записом, а потім рендеринг знову в режимі товстого каркаса, лише там, де не було значення трафарету. Дивіться тут приклад реалізації.
Це не підкреслить внутрішній контур та зморшки країв (як показано на малюнках). Як правило, щоб зробити це ефективно, вам потрібно отримати інформацію про краї сітки (на основі розривів у нормальних сторонах обличчя по обидві сторони краю) та створити структуру даних, що представляє кожен край.
Потім ви можете написати шейдери, щоб видавити або іншим чином зробити ці краї як звичайна геометрія над базовою моделлю (або разом з нею). Положення ребра та нормалі сусідніх граней відносно вектора перегляду використовуються для визначення того, чи можна намалювати конкретний край.
Ви можете знайти подальшу дискусію, деталі та статті з різними прикладами в Інтернеті. Наприклад:
dz/dx
dz/dy
Найпростіший спосіб зробити це, поширений на старішому апаратному забезпеченні перед шейдерами пікселів / фрагментів, і все ще використовується на мобільному пристрої, - це дублювати модель, змінювати порядок намотування вершин, щоб модель відображалася зсередини (або якщо ви хочете, ви можете зробіть це у вашому інструменті створення 3D-активів, скажімо, Blender, перегортаючи нормальні поверхні - те саме), потім трохи розгорніть весь дублікат навколо його центру, і нарешті колір / текстуру цього дубліката буде повністю чорним. Це призводить до обрисів навколо вашої оригінальної моделі, якщо це проста модель, наприклад куб. Для більш складних моделей з увігнутими формами (як, наприклад, на зображенні нижче), потрібно вручну підкрутити подвійну модель, щоб бути дещо "товстішою", ніж її оригінальний аналог, як сума Minkowskiв 3D. Ви можете почати, натиснувши кожну вершину трохи вздовж її звичайної, щоб утворити контурну сітку, як це робить трансформація Blender's Shrink / Fatten.
Екранні простір / піксельних підходи , як правило, повільніше і важче реалізувати добре , але Ото не подвоїться кількість вершин у вашому світі. Тож якщо ви займаєтеся високомобільною роботою, краще вибирайте такий підхід. З огляду на сучасні консолі і робочого столу потужність для обробки геометрії, я б не турбуватися про те 2 рази взагалі . Мультиплікаційний стиль = низький поліф напевно, таким чином, дублювати геометрію найпростіше.
Ви можете перевірити ефект на собі, наприклад, Blender, не торкаючись жодного коду. Контури повинні виглядати як на зображенні нижче, зверніть увагу на те, наскільки вони є внутрішніми, наприклад, під пахвою. Детальніше тут .
.
Для гладких моделей (дуже важливо) цей ефект досить простий. У вашому шейдері фрагмент / піксель вам знадобиться нормальний фрагмент, що затінюється. Якщо він дуже близький до перпендикуляра ( dot(surface_normal,view_vector) <= .01
- можливо, вам доведеться пограти з цим порогом), тоді пофарбуйте фрагмент чорним кольором замість його звичайного кольору.
Такий підхід «споживає» трохи моделі, щоб зробити контур. Це може бути або не бути тим, що ви хочете. З картини Покемона дуже важко сказати, чи саме це робиться. Це залежить від того, якщо ви очікуєте, що контур буде включений до будь-якого силуету персонажа або якщо ви хочете, щоб контур додав силует (для чого потрібна інша техніка).
Родзинка буде на будь-якій частині поверхні, де вона переходить від лицьової до зворотної, включаючи "внутрішні краї" (наприклад, ноги на зеленому Покемоні або його голова - деякі інші методи не додадуть жодних обрисів до цих ).
Об'єкти, які мають жорсткі, не гладкі краї (як кубик), не отримають виділення в потрібних місцях при такому підході. Це означає, що такий підхід в деяких випадках взагалі не є можливим; Я поняття не маю, чи всі моделі Pokemon плавні чи ні.
Найпоширеніший спосіб, який я бачив, - це через другий пропуск на вашій моделі. По суті, скопіюйте його і переверніть нормали, і засуньте їх у вершину шейдера. У шейдері масштабуйте кожну вершину вздовж її нормальної величини. На шейдері пікселя / фрагмента намалюйте чорний колір. Це дасть вам як зовнішні, так і внутрішні контури, як-от навколо губ, очей тощо. Це насправді досить дешевий дзвінок, якщо нічого іншого, як правило, дешевше, ніж після обробки лінії, залежно від кількості моделей та їх складності. Guilty Gear Xrd використовує цей метод, оскільки легко контролювати товщину лінії за допомогою вершинного кольору.
Другий спосіб виконання внутрішніх ліній я навчився з тієї ж гри. На УФ-карті вирівняйте текстуру вздовж осі u або v, особливо в тих місцях, де потрібно внутрішню лінію. Намалюйте чорну лінію вздовж будь-якої осі та перемістіть свої УФ-координати в цю лінію або поза нею, щоб створити внутрішню лінію.
Дивіться відео від GDC для кращого пояснення: https://www.youtube.com/watch?v=yhGjCzxJV3E
Один із способів скласти контур - це використовувати в наших моделях звичайні вектори. Нормальні вектори - це вектори, які перпендикулярні до їх поверхні (спрямовані від поверхні). Хитрість тут полягає в тому, щоб розділити свою модель персонажа на дві частини. Вершини, звернені до камери, і вершини, звернені в сторону від камери. Ми будемо називати їх відповідно 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"
}
Один із чудових способів зробити це - візуалізувати свою сцену на текстурі Framebuffer , а потім відтворити цю текстуру, роблячи фільтрацію Sobel на кожному пікселі, що є простою методикою виявлення краю. Таким чином, ви можете не тільки зробити сцену піксельною (встановивши низьку роздільну здатність до текстури Framebuffer), але і мати доступ до всіх значень пікселів, щоб Sobel працював.