Comment puis-je dessiner des contours autour de modèles 3D?

47

Comment puis-je dessiner des contours autour de modèles 3D? Je fais référence à quelque chose comme les effets d'un jeu Pokemon récent, qui semblent entourés d'un contour d'un pixel:

entrez la description de l'image ici entrez la description de l'image ici

portail mystique
la source
Utilisez-vous OpenGL? Si tel est le cas, vous devez rechercher sur Google comment tracer les contours d’un modèle avec OpenGL.
Oxysoft
1
Si vous faites référence aux images que vous avez insérées, je peux affirmer avec une certitude à 95% qu'il s'agit de sprites 2D dessinés à la main, et non de modèles 3D
Panda Pyjama.
3
@PandaPajama: Non, ce sont presque certainement des modèles 3D. Il y a quelques lacunes dans ce qui devrait être des lignes dures dans certaines images que je ne m'attendrais pas des sprites dessinés à la main, et de toute façon c'est en gros ce à quoi ressemblent les modèles 3D du jeu. Je suppose que je ne peux pas garantir à 100% ces images spécifiques, mais je ne peux pas imaginer pourquoi quiconque voudrait tenter de les simuler.
CA McCann
De quel jeu s'agit-il, en particulier? Il est magnifique.
Vegard
@Vegard La créature avec un navet sur le dos est un bulbasaur du jeu Pokémon.
Damian Yerrick

Réponses:

28

Je ne pense pas qu'aucune des autres réponses ici produira l'effet dans Pokémon X / Y. Je ne peux pas savoir exactement comment cela se passe, mais j’ai trouvé un moyen qui ressemble à peu près à ce qu’ils font dans le jeu.

Dans Pokémon X / Y, les contours sont dessinés à la fois autour des contours de la silhouette et sur d’autres contours non-silhouette (comme à l’endroit où les oreilles de Raichu se croisent dans la capture d’écran suivante).

Raichu

En regardant le maillage de Raichu dans Blender, vous pouvez voir que l'oreille (surlignée en orange ci-dessus) est juste un objet séparé et déconnecté qui coupe la tête, créant un changement brusque dans les normales à la surface.

Sur cette base, j'ai essayé de générer le contour en fonction des normales, ce qui nécessite un rendu en deux passes:

Première passe : restituez le modèle (texturé et ombré) sans les contours, puis restituez les normales de la caméra dans une seconde cible de rendu.

Deuxième passage : effectuez un filtre de détection de bord en plein écran sur les normales du premier passage.

Les deux premières images ci-dessous montrent les sorties du premier passage. Le troisième est le contour en lui-même et le dernier est le résultat final combiné.

Dratini

Voici le fragment shader OpenGL que j'ai utilisé pour la détection des contours lors du second passage. C'est le meilleur que j'ai pu trouver, mais il y a peut-être un meilleur moyen. Ce n'est probablement pas très bien optimisé non plus.

// 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);
}

Et avant d'effectuer le premier passage, j'efface la cible de rendu des normales sur un vecteur opposé à l'appareil photo:

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

J'ai lu quelque part (je mettrai un lien dans les commentaires) que la Nintendo 3DS utilise un pipeline à fonction fixe au lieu de shaders, alors je suppose que cela ne peut pas être exactement comme dans le jeu, mais pour l'instant je ' Je suis convaincu que ma méthode est suffisamment proche.

KTC
la source
Informations sur le matériel Nintendo 3DS: lien
KTC
Très belle solution! Comment prendriez-vous la profondeur en compte dans votre shader? (Dans le cas d'un avion devant un autre par exemple, les deux ont la même normale, aucun contour ne sera tracé)
ingham,
@ingham Cette affaire revient assez rarement sur un personnage organique que je n'ai pas eu besoin de la gérer, et il semble que le vrai jeu ne la gère pas non plus. Dans le jeu réel, vous pouvez parfois voir le contour disparaître lorsque les normales sont identiques, mais je ne pense pas que les gens le remarqueraient habituellement.
KTC
Je suis un peu sceptique quant au fait qu'un 3DS puisse utiliser des effets plein écran basés sur des shader comme celui-ci. Son support de shader est rudimentaire (s'il en a même).
Tara
17

Cet effet est particulièrement courant dans les jeux qui utilisent des effets d’ombrage cel, mais c’est en réalité quelque chose qui peut être appliqué indépendamment du style cel shading.

Ce que vous décrivez est appelé "rendu de contour de caractéristique" et consiste généralement à mettre en évidence les différents contours et contours d'un modèle. Il existe de nombreuses techniques disponibles et de nombreux articles sur le sujet.

Une technique simple consiste à ne rendre que le contour de la silhouette, le contour le plus éloigné. Cela peut être fait simplement en rendant le modèle original avec une écriture au pochoir, puis en le rendant à nouveau en mode filaire épais, uniquement là où il n'y avait pas de valeur de pochoir. Voir ici pour un exemple d'implémentation.

Cela ne mettra cependant pas en évidence le contour intérieur et les bords froissés (comme indiqué sur vos images). Généralement, pour faire cela efficacement, vous devez extraire des informations sur les bords du maillage (en fonction des discontinuités dans les normales des faces de chaque côté du bord et créer une structure de données représentant chaque bord.

Vous pouvez ensuite écrire des shaders pour extruder ou rendre ces arêtes comme une géométrie normale recouvrant votre modèle de base (ou conjointement avec celui-ci). La position d'un bord et les normales des faces adjacentes par rapport au vecteur de vue permettent de déterminer si un bord spécifique peut être tracé.

Vous pouvez trouver d'autres discussions, détails et documents avec divers exemples sur Internet. Par exemple:

Josh
la source
1
Je peux confirmer que la méthode du pochoir (de flipcode.com) fonctionne et est vraiment jolie. Vous pouvez donner l'épaisseur en coordonnées d'écran pour que l'épaisseur du contour ne dépende pas de la taille du modèle (ni de la forme du modèle).
Vegard
1
Une technique que vous n’avez pas mentionnée est l’effet d’ombrage-bordure de post-traitement souvent utilisé avec cel-shading, qui recherche les pixels avec une hauteur dz/dxet / oudz/dy
bcrist
8

Le moyen le plus simple de le faire, généralement utilisé sur les matériels plus anciens avant les shaders de pixels / fragments et toujours utilisé sur les appareils mobiles, consiste à dupliquer le modèle, à inverser l’ordre de bobinage des sommets afin que le modèle soit affiché à l’envers (ou, si vous le souhaitez, vous pouvez Faites cela dans votre outil de création d’actifs 3D, par exemple Blender, en inversant les normales de surface - même chose, puis développez légèrement la duplication entière autour de son centre, et enfin colorez / texturez cette duplication complètement noire. Cela se traduit par des contours autour de votre modèle d'origine, s'il s'agit d'un modèle simple tel qu'un cube. Pour les modèles plus complexes avec des formes concaves (comme dans l'image ci-dessous), il est nécessaire d'ajuster manuellement le modèle dupliqué pour qu'il soit un peu "plus gros" que son équivalent d'origine, comme un Minkowski Sum.en 3D. Vous pouvez commencer par pousser un peu chaque sommet le long de sa normale pour former le maillage de contour, comme le fait la transformation Rétrécir / Gras de Blender.

Approches shader espace / pixel écran ont tendance à être plus lent et plus difficile à mettre en œuvre bien , mais OTOH ne doublent pas le nombre de sommets dans votre monde. Donc, si vous faites du high poly, optez pour cette approche. Compte tenu de la capacité et la console de bureau moderne pour la géométrie de traitement, je vous inquiétez pas d'un facteur 2 du tout . Style de bande dessinée = low poly à coup sûr, dupliquer la géométrie est donc plus facile.

Vous pouvez tester l'effet par vous-même, par exemple dans Blender, sans toucher de code. Les contours doivent ressembler à l'image ci-dessous, notez comment certains sont internes, par exemple sous le bras. Plus de détails ici .

entrez la description de l'image ici.

Ingénieur
la source
1
Pourriez-vous s'il vous plaît expliquer, comment "élargir légèrement la duplication légèrement autour de son centre" est conforme à cette image, car une simple mise à l'échelle autour du centre ne fonctionnerait pas pour les bras et les autres parties qui ne sont pas concentriques, cela ne fonctionnera pas non plus pour tous les modèles des trous dedans.
Kromster dit de soutenir Monica
@KromStern Dans certains cas , les sous-ensembles de sommets doivent être mis à l'échelle manuellement pour s'adapter. Réponse modifiée.
Ingénieur
1
Il est courant de déplacer les sommets le long de leur surface normale normale, mais cela peut entraîner la séparation du maillage de contour développé le long des bords durs
DMGregory
Merci! Je ne pense pas qu'il soit inutile de renverser les normales, étant donné que le duplicata sera coloré en une couleur unie et plate (c.-à-d. Pas de calculs d'éclairage fantaisie qui dépendent de normales). J'ai obtenu le même effet en mettant simplement à l'échelle, en colorant à plat, puis en éliminant les faces avant du duplicata.
Jet Blue
6

Pour les modèles lisses (très important), cet effet est assez simple. Dans votre fragment / pixel shader, vous aurez besoin de la normale du fragment à l’ombre. S'il est très proche de la perpendiculaire ( dot(surface_normal,view_vector) <= .01vous devrez peut-être jouer avec ce seuil), coloriez le fragment en noir au lieu de sa couleur habituelle.

Cette approche "consomme" un peu du modèle pour faire le contour. Cela peut être ou ne pas être ce que vous voulez. Il est très difficile de dire à partir de la photo Pokemon si c'est ce qui est fait. Cela dépend si vous vous attendez à ce que le contour soit inclus dans n'importe quelle silhouette du personnage ou si vous préférez que le contour entoure la silhouette (ce qui nécessite une technique différente).

Le point culminant se trouvera sur n’importe quelle partie de la surface où il passe de face vers l’arrière, y compris les "bords intérieurs" (comme les pattes du Pokémon vert ou sa tête - certaines autres techniques n’ajouteraient aucun contour à celles ).

Les objets ayant des bords durs et non lisses (comme un cube) ne seront pas mis en évidence aux emplacements souhaités avec cette approche. Cela signifie que cette approche n'est pas une option du tout dans certains cas; Je ne sais pas si les modèles Pokemon sont tous lisses ou non.

Sean Middleditch
la source
5

La manière la plus courante que j'ai vue résoudre ce problème consiste à effectuer une deuxième passe de rendu sur votre modèle. Essentiellement, dupliquez-le et retournez les normales, puis transférez-le dans un vertex shader. Dans le shader, redimensionnez chaque sommet le long de sa normale. Dans le pixel / fragment shader, dessinez le noir. Cela vous donnera des contours externes et internes, comme autour des lèvres, des yeux, etc. Ceci est en fait un appel de tirage assez bon marché, si rien d'autre n'est généralement moins cher que de post-traiter la ligne, en fonction du nombre de modèles et de leur complexité. Guilty Gear Xrd utilise cette méthode car il est facile de contrôler l’épaisseur de la ligne via la couleur du sommet.

La deuxième façon de faire les lignes intérieures que j'ai apprises du même jeu. Dans votre carte UV, alignez votre texture le long de l’axe u ou v, en particulier dans les zones où vous souhaitez une ligne intérieure. Tracez une ligne noire le long de l'un ou l'autre axe et déplacez vos coordonnées UV dans ou hors de cette ligne pour créer la ligne intérieure.

Voir la vidéo de GDC pour une meilleure explication: https://www.youtube.com/watch?v=yhGjCzxJV3E

Andrew Q
la source
5

L’un des moyens de créer un contour consiste à utiliser nos modèles vecteurs normaux. Les vecteurs normaux sont des vecteurs perpendiculaires à leur surface (éloignés de la surface). Le truc ici est de scinder votre modèle de personnage en deux parties. Les sommets qui font face à la caméra et les sommets qui sont opposés à la caméra. Nous les appellerons respectivement AVANT et ARRIÈRE.

Pour le contour, prenons nos sommets BACK et les déplaçons légèrement dans la direction de leurs vecteurs normaux. Pensez-y comme si vous grossissiez un peu plus la partie de notre personnage qui fait face à la caméra. Une fois cela fait, nous leur attribuons une couleur de notre choix et nous avons un joli contour.

entrez la description de l'image ici

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
        }
    } 
}

Ligne 41: le paramètre «Cull Front» indique au shader d'effectuer une sélection sur les sommets frontaux. Cela signifie que nous ignorerons tous les sommets faisant face au front dans cette passe. Nous nous retrouvons avec le côté arrière que nous voulons manipuler un peu.

Lignes 51-53: Le calcul du déplacement de sommets le long de leurs vecteurs normaux.

Ligne 54: Définition de la couleur du sommet sur la couleur de votre choix définie dans les propriétés des nuanceurs.

Lien utile: http://wiki.unity3d.com/index.php/Silhouette-Outlined_Diffuse


Mise à jour

un autre exemple

entrez la description de l'image ici

entrez la description de l'image ici

   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
la source
Pourquoi l'utilisation du tampon de gabarit dans l'exemple mis à jour?
Tara
Ah je l'ai maintenant. Le deuxième exemple utilise une approche qui ne génère que des contours externes, contrairement au premier. Vous voudrez peut-être mentionner cela dans votre réponse.
Tara
0

Une des meilleures façons de le faire est de rendre votre scène sur une texture Framebuffer , puis de restituer cette texture tout en effectuant un filtrage Sobel sur chaque pixel, ce qui est une technique simple pour la détection des contours. De cette façon, vous pouvez non seulement rendre la scène pixelisée (en définissant une faible résolution pour la texture du framebuffer), mais également accéder à toutes les valeurs de pixel pour que Sobel fonctionne.

Ross Myhovych
la source