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:
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:
Câu trả lời:
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).
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.
Đâ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.
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ụ:
dz/dx
và / hoặcdz/dy
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 .
.
Đố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.
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
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.
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
một vi dụ khac
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"
}
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.