¿Cómo puedo dibujar contornos alrededor de modelos 3D?

47

¿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:

ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí

portal místico
fuente
¿Estás usando OpenGL? Si es así, debe buscar en Google cómo dibujar contornos para un modelo con OpenGL.
oxysoft
1
Si te refieres a esas imágenes particulares que pones allí, puedo decir con un 95% de certeza que son sprites 2D dibujados a mano, no modelos 3D
Panda Pyjama
3
@PandaPajama: No, es casi seguro que son modelos 3D. Hay algo de descuido en lo que deberían ser líneas duras en algunos cuadros que no esperaría de los sprites dibujados a mano, y de todos modos así es básicamente como se ven los modelos 3D en el juego. Supongo que no puedo garantizar el 100% de esas imágenes específicas, pero no puedo imaginar por qué alguien haría el esfuerzo de fingirlas.
CA McCann
¿Qué juego es ese, específicamente? Se ve preciosa
Vegard
@Vegard La criatura con un nabo en la espalda es un Bulbasaur del juego Pokémon.
Damian Yerrick

Respuestas:

28

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).

Raichu

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.

Dratini

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.

KTC
fuente
Información sobre el hardware de Nintendo 3DS: enlace
KTC
Muy buena solución! ¿Cómo tomaría en cuenta la profundidad en su sombreador? (En el caso de un avión frente a otro, por ejemplo, ambos tienen la misma normalidad, por lo que no se dibujará ningún contorno)
ingham
@ingham Ese caso aparece con poca frecuencia en un personaje orgánico que no necesitaba manejarlo, y parece que el juego real tampoco lo maneja. En el juego real, a veces puedes ver desaparecer el contorno cuando las normales son las mismas, pero no creo que la gente lo note.
KTC
Soy un poco escéptico acerca de que un 3DS sea cabable para ejecutar efectos de pantalla completa basados ​​en sombreadores como ese. Su soporte de sombreador es rudimentario (si es que tiene alguno).
Tara
17

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:

Josh
fuente
1
Puedo confirmar que el método de plantilla (de flipcode.com) funciona y se ve muy bien. Puede dar el grosor en las coordenadas de la pantalla para que el grosor del contorno no dependa del tamaño del modelo (ni de la forma del modelo).
Vegard el
1
Una técnica que no ha mencionado es el efecto frontera-shading post-procesamiento a menudo se utiliza junto con cel-shading cual busca píxeles con una alta dz/dxy / odz/dy
bcrist
8

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í .

ingrese la descripción de la imagen aquí.

Ingeniero
fuente
1
¿Podría explicar, por favor, cómo "expandir todo el duplicado ligeramente alrededor de su centro" cumple con esta imagen, porque el escalado simple alrededor del centro no funcionaría para brazos y otras partes que no sean concéntricas, tampoco funcionará para ningún modelo que tenga agujeros en ella.
Kromster dice que apoya a Mónica
@KromStern En algunos casos , los subconjuntos de vértices deben escalarse manualmente para acomodarse. Respuesta enmendada.
Ingeniero
1
Es común empujar los vértices a lo largo de su superficie local normal, pero esto puede hacer que la malla del contorno expandido se divida a lo largo de los bordes duros
DMGregory
¡Gracias! No creo que haya ningún punto en cambiar las normales, dado que el duplicado tendrá un color sólido plano (es decir, no hay cálculos de iluminación sofisticados que dependan de las normales). Logré el mismo efecto simplemente escalando, coloreando y luego eliminando las caras frontales del duplicado.
Jet Blue
6

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) <= .01es 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.

Sean Middleditch
fuente
5

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

Andrew Q
fuente
5

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.

ingrese la descripción de la imagen aquí

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


Actualizar

otro ejemplo

ingrese la descripción de la imagen aquí

ingrese la descripción de la imagen aquí

   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"
        }
Seyed Morteza Kamali
fuente
¿Por qué el uso del búfer de plantilla en el ejemplo actualizado?
Tara
Ah lo tengo ahora. El segundo ejemplo utiliza un enfoque que solo genera esquemas externos, a diferencia del primero. Es posible que desee mencionar eso en su respuesta.
Tara
0

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.

Ross Myhovych
fuente