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:
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:
Réponses:
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).
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é.
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.
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:
dz/dx
et / oudz/dy
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 .
.
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) <= .01
vous 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.
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
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.
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
un autre exemple
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"
}
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.