ฉันจะวาดโครงร่างรอบ ๆ แบบจำลอง 3 มิติได้อย่างไร ฉันพูดถึงบางสิ่งบางอย่างเช่นเอฟเฟกต์ในเกมโปเกมอนเมื่อเร็ว ๆ นี้ซึ่งดูเหมือนว่าจะมีโครงร่างพิกเซลเดียวรอบตัวพวกเขา:
ฉันจะวาดโครงร่างรอบ ๆ แบบจำลอง 3 มิติได้อย่างไร ฉันพูดถึงบางสิ่งบางอย่างเช่นเอฟเฟกต์ในเกมโปเกมอนเมื่อเร็ว ๆ นี้ซึ่งดูเหมือนว่าจะมีโครงร่างพิกเซลเดียวรอบตัวพวกเขา:
คำตอบ:
ฉันไม่คิดว่าคำตอบอื่นใดที่นี่จะได้ผลในโปเกมอน X / Y ฉันไม่รู้แน่ชัดว่าทำอย่างไร แต่ฉันคิดวิธีที่ดูเหมือนว่าพวกเขาทำอะไรในเกม
ในPokémon X / Y โครงร่างจะถูกวาดทั้งรอบขอบภาพเงาและบนขอบที่ไม่ใช่เงาอื่น ๆ (เช่นที่หูของ Raichu พบกับศีรษะของเขาในภาพหน้าจอต่อไปนี้)
เมื่อมองไปที่ตาข่ายของ Raichu ในเครื่องปั่นคุณสามารถเห็นหู (เน้นด้วยสีส้มด้านบน) เป็นเพียงวัตถุแยกต่างหากที่ไม่ได้เชื่อมต่อซึ่งตัดกับส่วนหัวทำให้เกิดการเปลี่ยนแปลงอย่างฉับพลันในพื้นผิวปกติ
จากนั้นฉันพยายามสร้างโครงร่างโดยยึดตามบรรทัดฐานซึ่งต้องแสดงผลเป็นสองรอบ:
บัตรผ่านครั้งแรก : แสดงโมเดล (พื้นผิวและเฉดสีเทา) โดยไม่แสดงโครงร่างและทำให้บรรทัดฐานของพื้นที่กล้องเป็นเป้าหมายการเรนเดอร์ที่สอง
รอบที่สอง : ทำการกรองการตรวจจับขอบแบบเต็มหน้าจอเหนือบรรทัดฐานจากรอบแรก
ภาพสองภาพแรกด้านล่างแสดงเอาท์พุทของการผ่านครั้งแรก ที่สามคือร่างโดยตัวเองและสุดท้ายคือผลรวมสุดท้าย
นี่คือส่วนย่อย 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 ใช้ฟังก์ชั่นคงที่แทนที่จะใช้ตัวแปลงสีดังนั้นฉันเดาว่านี่จะไม่ใช่วิธีที่มันทำในเกม แต่ตอนนี้ฉัน ฉันเชื่อว่าวิธีการของฉันใกล้พอ
เอฟเฟกต์นี้เป็นเรื่องธรรมดาโดยเฉพาะอย่างยิ่งในเกมที่ใช้เอฟเฟ็กต์การแรเงา cel แต่จริงๆแล้วเป็นสิ่งที่สามารถใช้งานได้อย่างอิสระจากสไตล์การแรเงา cel
สิ่งที่คุณกำลังอธิบายเรียกว่า "การแสดงผลคุณสมบัติขอบ" และอยู่ในกระบวนการทั่วไปของการเน้นรูปทรงและเค้าโครงต่าง ๆ ของแบบจำลอง มีเทคนิคมากมายและเอกสารมากมายในเรื่องนี้
เทคนิคง่าย ๆ คือการสร้างภาพเงาเฉพาะขอบร่าง สิ่งนี้สามารถทำได้ง่ายๆเหมือนกับการเรนเดอร์โมเดลดั้งเดิมด้วย stencil write จากนั้นเรนเดอร์อีกครั้งในโหมด wireframe หนาซึ่งไม่มีค่า stencil เท่านั้น ดูที่นี่สำหรับตัวอย่างการใช้งาน
ที่จะไม่เน้นเส้นขอบภายในและรอยพับขอบ (ดังที่แสดงในรูปภาพของคุณ) โดยทั่วไปแล้วการทำอย่างมีประสิทธิภาพนั้นคุณจะต้องดึงข้อมูลเกี่ยวกับเส้นขอบของตาข่าย (ขึ้นอยู่กับความไม่ต่อเนื่องของใบหน้าปกติที่ด้านใดด้านหนึ่งของขอบและสร้างโครงสร้างข้อมูลที่เป็นตัวแทนของแต่ละขอบ
จากนั้นคุณสามารถเขียนเฉดสีเพื่อขับไล่หรือสร้างขอบเหล่านั้นเป็นรูปทรงเรขาคณิตปกติบนโมเดลฐานของคุณ (หรือเชื่อมต่อกับมัน) ตำแหน่งของขอบและบรรทัดฐานของใบหน้าที่อยู่ติดกันซึ่งสัมพันธ์กับมุมมองเวกเตอร์ใช้เพื่อกำหนดว่าสามารถวาดขอบที่ระบุได้หรือไม่
คุณสามารถค้นหาการสนทนารายละเอียดเพิ่มเติมและเอกสารด้วยตัวอย่างต่าง ๆ บนอินเทอร์เน็ต ตัวอย่างเช่น:
dz/dx
และ / หรือdz/dy
วิธีที่ง่ายที่สุดในการทำเช่นนี้โดยทั่วไปกับฮาร์ดแวร์รุ่นเก่าก่อนพิกเซล / แฟรกเมนต์ส่วนและยังคงใช้บนมือถือคือการทำซ้ำแบบจำลองย้อนกลับลำดับจุดสุดยอดที่คดเคี้ยวเพื่อให้แบบจำลองแสดงภายใน (หรือหากคุณต้องการ ทำสิ่งนี้ในเครื่องมือสร้างสินทรัพย์ 3 มิติของคุณพูด Blender โดยพลิกบรรทัดฐานของพื้นผิว - สิ่งเดียวกัน) จากนั้นขยายสำเนาที่ซ้ำกันทั้งหมดรอบ ๆ จุดศูนย์กลางและในที่สุดสี / พื้นผิวนี้จะเป็นสีดำสนิท ผลลัพธ์นี้จะแสดงโครงร่างรอบโมเดลดั้งเดิมของคุณหากเป็นโมเดลที่เรียบง่ายเช่นคิวบ์ สำหรับแบบจำลองที่ซับซ้อนมากขึ้นที่มีรูปแบบแบบเว้า (เช่นในภาพด้านล่าง) จำเป็นต้องปรับแต่งแบบจำลองที่ซ้ำกันด้วยตนเองเพื่อให้ค่อนข้าง "อ้วนขึ้น" กว่ารุ่นเดิมเช่นMinkowski Sumในแบบ 3 มิติ คุณสามารถเริ่มต้นด้วยการกดจุดสุดยอดแต่ละจุดออกมาเล็กน้อยตามปกติเพื่อสร้างตาข่ายโครงร่างดังที่การแปลง Shrink / Fatten ของ Blender
พื้นที่หน้าจอ / ตัวแบ่งส่วนพิกเซลใกล้จะช้าและยากที่จะใช้งานได้ดีแต่ OTOH ไม่ได้เพิ่มจำนวนจุดยอดในโลกของคุณเป็นสองเท่า ดังนั้นถ้าคุณทำงานโพลีสูงเลือกที่ดีที่สุดสำหรับวิธีการที่ ได้รับคอนโซลที่ทันสมัยและความจุสก์ท็อปสำหรับการประมวลผลรูปทรงเรขาคณิตที่ผมไม่ต้องกังวลเกี่ยวกับปัจจัย 2 ที่ทุกคน รูปแบบการ์ตูน = โพลีต่ำอย่างแน่นอนดังนั้นการทำซ้ำเรขาคณิตจึงเป็นเรื่องง่ายที่สุด
คุณสามารถทดสอบเอฟเฟกต์ด้วยตัวคุณเองเช่นเครื่องปั่นโดยไม่ต้องแตะรหัสใด ๆ โครงร่างควรมีลักษณะเหมือนภาพด้านล่างสังเกตว่ามีบางอย่างอยู่ภายในเช่นใต้วงแขน รายละเอียดเพิ่มเติมที่นี่
.
สำหรับรุ่นที่ราบรื่น (สำคัญมาก) เอฟเฟกต์นี้ค่อนข้างง่าย ในการแยกส่วน / พิกเซลของคุณคุณจะต้องมีปกติของส่วนที่แรเงา หากใกล้เคียงกับฉากตั้งฉาก ( dot(surface_normal,view_vector) <= .01
- คุณอาจต้องเล่นกับขีด จำกัด นั้น) จากนั้นให้สีชิ้นส่วนสีดำแทนที่จะเป็นสีปกติ
วิธีการนี้ "สิ้นเปลือง" เล็กน้อยของแบบจำลองเพื่อทำโครงร่าง นี่อาจเป็นหรือไม่ใช่สิ่งที่คุณต้องการ เป็นการยากที่จะบอกจากรูปโปเกมอนถ้านี่คือสิ่งที่กำลังทำอยู่ มันขึ้นอยู่กับว่าคุณคาดหวังว่าร่างจะรวมอยู่ในภาพเงาใด ๆ ของตัวละครหรือถ้าคุณต้องการให้ร่างล้อมรอบเงา (ซึ่งต้องใช้เทคนิคที่แตกต่างกัน)
ไฮไลต์จะอยู่ที่ส่วนใดส่วนหนึ่งของพื้นผิวที่มีการเปลี่ยนจากด้านหน้าไปด้านหลังรวมถึง "ขอบด้านใน" (เช่นขาบนโปเกมอนสีเขียวหรือหัวของมัน - เทคนิคอื่น ๆ จะไม่เพิ่มโครงร่างใด ๆ )
วัตถุที่มีขอบแข็งและไม่เรียบ (เช่นลูกบาศก์) จะไม่ได้รับไฮไลต์ในตำแหน่งที่ต้องการด้วยวิธีการนี้ นั่นหมายความว่าวิธีการนี้ไม่ใช่ตัวเลือกในบางกรณี ฉันไม่รู้ว่ารุ่นโปเกมอนนั้นราบรื่นหรือไม่
วิธีที่พบมากที่สุดที่ฉันเห็นสิ่งนี้คือการส่งเรนเดอร์ตัวที่สองในโมเดลของคุณ โดยพื้นฐานแล้วให้ทำซ้ำและพลิกบรรทัดฐานแล้วดันเข้าไปในจุดสุดยอด ใน shader ให้ปรับยอดจุดยอดแต่ละจุดตามปกติ ใน pixel / shader shader ให้วาดสีดำ นั่นจะทำให้คุณมีโครงร่างภายนอกและภายในเช่นรอบริมฝีปากตาและอื่น ๆ นี่คือการโทรที่ค่อนข้างถูกถ้าไม่มีอะไรที่ถูกกว่าการโพสต์โพสต์ขึ้นอยู่กับจำนวนรุ่นและความซับซ้อนของพวกเขา Guilty Gear Xrd ใช้วิธีนี้เพราะง่ายต่อการควบคุมความหนาของเส้นผ่านสีจุดสุดยอด
วิธีที่สองของการทำเส้นด้านในที่ฉันเรียนรู้จากเกมเดียวกัน ในแผนที่ UV จัดตำแหน่งพื้นผิวของคุณตามแนวแกน u หรือ v โดยเฉพาะในพื้นที่ที่คุณต้องการเส้นด้านใน ลากเส้นสีดำตามแนวแกนใดแกนหนึ่งแล้วเลื่อนพิกัด UV ของคุณเข้าหรือออกจากเส้นนั้นเพื่อสร้างเส้นด้านใน
ดูวิดีโอจาก 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: การตั้งค่า“ Cull Front” บอกให้ shader ทำการเลือกสรรบริเวณด้านหน้าของจุดยอด หมายความว่าเราจะไม่สนใจจุดยอดหันหน้าทั้งหมดในบัตรนี้ เราถูกทิ้งไว้กับด้านหลังที่เราต้องการจัดการเล็กน้อย
บรรทัดที่ 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 ทำงานได้