¿Cómo puedo dibujar contornos alrededor de modelos 3D? Me refiero a algo como los efectos en un juego reciente de Pokémon, que parece tener un contorno de un solo píxel a su alrededor:
¿Cómo puedo dibujar contornos alrededor de modelos 3D? Me refiero a algo como los efectos en un juego reciente de Pokémon, que parece tener un contorno de un solo píxel a su alrededor:
Respuestas:
No creo que ninguna de las otras respuestas aquí logre el efecto en Pokémon X / Y. No puedo saber exactamente cómo se hace, pero descubrí una forma que parece más o menos lo que hacen en el juego.
En Pokémon X / Y, los contornos se dibujan tanto alrededor de los bordes de la silueta como en otros bordes que no son de la silueta (como donde las orejas de Raichu se encuentran con su cabeza en la siguiente captura de pantalla).
Mirando la malla de Raichu en Blender, puede ver que la oreja (resaltada en naranja arriba) es solo un objeto separado y desconectado que se cruza con la cabeza, creando un cambio abrupto en las normales de la superficie.
Basado en eso, intenté generar el esquema basado en las normales, lo que requiere renderizar en dos pasadas:
Primer paso : renderice el modelo (texturizado y sombreado en celdas) sin los contornos, y renderice los espacios normales de la cámara a un segundo objetivo de renderizado.
Segunda pasada : realice un filtro de detección de bordes de pantalla completa sobre las normales de la primera pasada.
Las dos primeras imágenes a continuación muestran los resultados de la primera pasada. El tercero es el esquema en sí mismo, y el último es el resultado final combinado.
Aquí está el sombreador de fragmentos OpenGL que utilicé para la detección de bordes en la segunda pasada. Es lo mejor que se me ocurrió, pero podría haber una mejor manera. Probablemente tampoco esté muy bien optimizado.
// 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);
}
Y antes de renderizar la primera pasada, borro el objetivo de renderizado de las normales a un vector alejado de la cámara:
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 );
Leí en alguna parte (pondré un enlace en los comentarios) que la Nintendo 3DS usa una tubería de función fija en lugar de sombreadores, así que supongo que esto no puede ser exactamente como se hace en el juego, pero por ahora yo ' Estoy convencido de que mi método está lo suficientemente cerca.
Este efecto es particularmente común en los juegos que utilizan los efectos de sombreado de cel, pero en realidad es algo que se puede aplicar independientemente del estilo de sombreado de cel.
Lo que está describiendo se denomina "representación de bordes de características" y es, en general, el proceso de resaltar los diversos contornos y contornos de un modelo. Hay muchas técnicas disponibles y muchos documentos sobre el tema.
Una técnica simple es representar solo el borde de la silueta, el contorno exterior. Esto se puede hacer tan simple como renderizar el modelo original con una escritura de plantilla, y luego renderizarlo nuevamente en modo de estructura gruesa, solo donde no haya valor de plantilla. Mira aquí para un ejemplo de implementación.
Sin embargo, eso no resaltará el contorno interior y los bordes del pliegue (como se muestra en las imágenes). En general, para hacerlo de manera efectiva, debe extraer información sobre los bordes de la malla (en función de las discontinuidades en las caras normales a cada lado del borde, y construir una estructura de datos que represente cada borde.
Luego puede escribir sombreadores para extruir o representar esos bordes como geometría regular sobre su modelo base (o en conjunto con él). La posición de un borde y las normales de las caras adyacentes en relación con el vector de vista se usan para determinar si se puede dibujar un borde específico.
Puede encontrar más debates, detalles y documentos con varios ejemplos en Internet. Por ejemplo:
dz/dx
y / odz/dy
La forma más sencilla de hacer esto, común en hardware antiguo antes de sombreadores de píxeles / fragmentos, y todavía utilizado en dispositivos móviles, es duplicar el modelo, invertir el orden de enrollamiento de vértices para que el modelo se muestre al revés (o si lo desea, puede haga esto en su herramienta de creación de activos 3D, por ejemplo, Blender, volteando las normales de superficie, lo mismo), luego expanda todo el duplicado ligeramente alrededor de su centro y finalmente coloree / texturice este duplicado completamente en negro. Esto da como resultado contornos alrededor de su modelo original, si está se trata de un modelo simple, como un cubo. Para modelos más complejos con formas cóncavas (como la de la imagen a continuación), es necesario ajustar manualmente el modelo duplicado para que sea algo más "gordo" que su contraparte original, como una suma de Minkowskien 3D Puede comenzar empujando cada vértice un poco a lo largo de su normalidad para formar la malla del contorno, como lo hace la transformación Reducir / Engordar de Blender.
Pantalla enfoques de sombreado espacio / pixel tienden a ser más lento y más difícil de implementar bien , pero otoh No debe duplicar el número de vértices en su mundo. Entonces, si está haciendo un trabajo polivinílico alto, mejor opte por ese enfoque. Dada la consola moderna y la capacidad de escritorio para el procesamiento de la geometría, yo no me preocuparía por un factor de 2 en absoluto . Cartoon-style = low poly seguro, por lo tanto, duplicar la geometría es más fácil.
Puede probar el efecto por sí mismo en, por ejemplo, Blender sin tocar ningún código. Los contornos deben verse como la imagen a continuación, observe cómo algunos son internos, por ejemplo, debajo del brazo. Más detalles aquí .
.
Para modelos suaves (muy importante), este efecto es bastante simple. En su sombreador de fragmentos / píxeles necesitará la normalidad del fragmento que se está sombreando. Si está muy cerca de la perpendicular ( dot(surface_normal,view_vector) <= .01
es posible que deba jugar con ese umbral), coloree el fragmento de negro en lugar de su color habitual.
Este enfoque "consume" un poco del modelo para hacer el esquema. Esto puede o no ser lo que quieres. Es muy difícil saber por la imagen de Pokémon si esto es lo que se está haciendo. Depende de si espera que el contorno se incluya en cualquier silueta del personaje o si prefiere que el contorno encierre la silueta (que requiere una técnica diferente).
Lo más destacado estará en cualquier parte de la superficie donde pase de frente a atrás, incluidos los "bordes internos" (como las patas del Pokémon verde o su cabeza; algunas otras técnicas no agregarían ningún contorno a esas )
Los objetos que tienen bordes duros y no lisos (como un cubo) no recibirán un resaltado en las ubicaciones deseadas con este enfoque. Eso significa que este enfoque no es una opción en absoluto en algunos casos; No tengo idea si los modelos de Pokémon son suaves o no.
La forma más común en que he visto esto es a través de un segundo pase de renderizado en su modelo. Básicamente, duplíquelo y voltee las normales, y métalo en un sombreador de vértices. En el sombreador, escala cada vértice a lo largo de su normalidad. En el sombreador de píxeles / fragmentos, dibuja negro. Eso le dará contornos externos e internos, como alrededor de los labios, los ojos, etc. Esta es una llamada de sorteo bastante barata, si no es más, generalmente es más barato que el procesamiento posterior de la línea, dependiendo de la cantidad de modelos y su complejidad. Guilty Gear Xrd usa este método porque es fácil controlar el grosor de la línea a través del color del vértice.
La segunda forma de hacer líneas internas la aprendí del mismo juego. En su mapa UV, alinee su textura a lo largo del eje uo v, particularmente en las áreas donde desea una línea interna. Dibuje una línea negra a lo largo de cualquier eje y mueva sus coordenadas UV dentro o fuera de esa línea para crear la línea interior.
Vea el video de GDC para una mejor explicación: https://www.youtube.com/watch?v=yhGjCzxJV3E
Una de las formas de hacer un esquema es utilizar nuestros modelos de vectores normales. Los vectores normales son vectores que son perpendiculares a su superficie (apuntando lejos de la superficie). El truco aquí es dividir su modelo de personaje en dos partes. Los vértices que miran hacia la cámara y los vértices que miran hacia afuera de la cámara. Los llamaremos FRONTAL y BACK respectivamente.
Para el contorno, tomamos nuestros vértices BACK y los movemos ligeramente en la dirección de sus vectores normales. Piénselo como hacer que la parte de nuestro personaje que está mirando hacia afuera de la cámara sea un poco más gorda. Una vez hecho esto, les asignamos un color de nuestra elección y tenemos un buen contorno.
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
}
}
}
Línea 41: El ajuste "Cull Front" le dice al sombreador que realice un culling en los vértices frontales. Significa que ignoraremos todos los vértices frontales en esta pasada. Nos quedamos con el lado TRASERO que queremos manipular un poco.
Líneas 51-53: la matemática de los vértices en movimiento a lo largo de sus vectores normales.
Línea 54: Establecer el color del vértice a nuestro color de elección definido en las propiedades de los sombreadores.
Enlace útil: http://wiki.unity3d.com/index.php/Silhouette-Outlined_Diffuse
otro ejemplo
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"
}
Una de las mejores maneras de hacerlo es renderizando su escena en una textura Framebuffer , para luego renderizar esa textura mientras hace un Filtrado Sobel en cada píxel, que es una técnica fácil para la detección de bordes. De esta manera, no solo puede hacer que la escena esté pixelada (estableciendo una resolución baja a la textura Framebuffer), sino que también tiene acceso a todos los valores de píxeles para que Sobel funcione.