Como posso desenhar contornos em torno de modelos 3D? Estou me referindo a algo como os efeitos de um jogo Pokemon recente, que parece ter um contorno de pixel único em torno deles:
Como posso desenhar contornos em torno de modelos 3D? Estou me referindo a algo como os efeitos de um jogo Pokemon recente, que parece ter um contorno de pixel único em torno deles:
Respostas:
Não acho que nenhuma das outras respostas aqui atinja o efeito no Pokémon X / Y. Não sei exatamente como é feito, mas descobri uma maneira que parece muito com o que eles fazem no jogo.
No Pokémon X / Y, os contornos são desenhados ao redor das bordas da silhueta e em outras bordas que não são da silhueta (como onde as orelhas de Raichu encontram sua cabeça na captura de tela a seguir).
Olhando para a malha de Raichu no Blender, você pode ver que a orelha (destacada em laranja acima) é apenas um objeto separado e desconectado que cruza a cabeça, criando uma mudança abrupta nas normais da superfície.
Com base nisso, tentei gerar o esboço com base nas normais, o que requer renderização em duas passagens:
Primeira passagem : renderize o modelo (texturizado e sombreado a cel) sem os contornos e renderize os normais de espaço da câmera para um segundo destino de renderização.
Segunda passagem : faça um filtro de detecção de borda em tela cheia sobre os valores normais da primeira passagem.
As duas primeiras imagens abaixo mostram as saídas da primeira passagem. O terceiro é o esboço por si só, e o último é o resultado final combinado.
Aqui está o sombreador OpenGL que usei para detecção de borda na segunda passagem. É o melhor que pude apresentar, mas pode haver uma maneira melhor. Provavelmente também não está muito bem otimizado.
// 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);
}
E antes de renderizar a primeira passagem, limpo o alvo de renderização dos normais para um vetor voltado para longe da câmera:
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 );
Li em algum lugar (colocarei um link nos comentários) que o Nintendo 3DS usa um pipeline de função fixa em vez de sombreadores, então acho que não pode ser exatamente assim que é feito no jogo, mas por enquanto eu ' Estou convencido de que meu método está próximo o suficiente.
Esse efeito é particularmente comum em jogos que usam efeitos de sombreamento de cel, mas na verdade é algo que pode ser aplicado independentemente do estilo de sombreamento de cel.
O que você está descrevendo é chamado de "renderização de borda do recurso" e, em geral, é o processo de destacar os vários contornos e contornos de um modelo. Existem muitas técnicas disponíveis e muitos trabalhos sobre o assunto.
Uma técnica simples é renderizar apenas a borda da silhueta, o contorno mais externo. Isso pode ser feito simplesmente como renderizar o modelo original com uma gravação de estêncil e, em seguida, renderizá-lo novamente no modo de estrutura de arame grossa, somente onde não havia valor de estêncil. Veja aqui um exemplo de implementação.
Porém, isso não destacará o contorno interno e as bordas do vinco (como mostrado nas fotos). Geralmente, para fazer isso de forma eficaz, é necessário extrair informações sobre as arestas da malha (com base nas descontinuidades nas faces normais de ambos os lados da aresta e criar uma estrutura de dados representando cada aresta.
Você pode escrever shaders para extrudar ou renderizar essas arestas como geometria regular sobre o modelo base (ou em conjunto com ele). A posição de uma aresta e as normais das faces adjacentes em relação ao vetor de vista são usadas para determinar se uma aresta específica pode ser desenhada.
Você pode encontrar mais discussões, detalhes e documentos com vários exemplos na internet. Por exemplo:
dz/dx
e / oudz/dy
A maneira mais simples de fazer isso, comum em hardware mais antigo antes dos sombreadores de pixel / fragmento e ainda usada no celular, é duplicar o modelo, reverter a ordem de enrolamento do vértice para que o modelo seja exibido de dentro para fora (ou, se desejar, você pode faça isso na sua ferramenta de criação de ativos 3D, digamos o Blender, alternando os normais da superfície - a mesma coisa), depois expanda a duplicata inteira levemente em torno do centro e, finalmente, colore / texture essa duplicata completamente preta. Isso resulta em contornos do modelo original, se for um modelo simples, como um cubo. Para modelos mais complexos com formas côncavas (como na imagem abaixo), é necessário ajustar manualmente o modelo duplicado para ser um pouco "mais gordo" do que o seu homólogo original, como um Minkowski Sumem 3D. Você pode começar empurrando cada vértice um pouco ao longo de seu normal para formar a malha de estrutura de tópicos, como a transformação Shrink / Fatten do Blender faz.
As abordagens de espaço na tela / sombreamento de pixels tendem a ser mais lentas e difíceis de implementar bem , mas a OTOH não duplica o número de vértices em seu mundo. Portanto, se você estiver realizando um trabalho altamente polivalente, é melhor optar por essa abordagem. Dada consola moderna e capacidade de área de trabalho para o processamento de geometria, eu não me preocuparia com um fator de 2 em tudo . Estilo dos desenhos animados = baixo poli, com certeza, portanto, duplicar a geometria é mais fácil.
Você pode testar o efeito, por exemplo, no Blender, sem tocar em nenhum código. Os contornos devem se parecer com a imagem abaixo; observe como alguns são internos, por exemplo, debaixo do braço. Mais detalhes aqui .
.
Para modelos suaves (muito importantes), esse efeito é bastante simples. No seu fragmento / pixel shader, você precisará do normal do fragmento sendo sombreado. Se estiver muito próximo da perpendicular ( dot(surface_normal,view_vector) <= .01
- pode ser necessário jogar com esse limite), pinte o fragmento de preto em vez da cor usual.
Essa abordagem "consome" um pouco do modelo para fazer o esboço. Isso pode ou não ser o que você deseja. É muito difícil dizer pela imagem de Pokemon se é isso que está sendo feito. Depende se você espera que o contorno seja incluído em qualquer silhueta do personagem ou se prefere que o contorno inclua a silhueta (o que requer uma técnica diferente).
O destaque estará em qualquer parte da superfície onde ele transitar de frente para trás, incluindo "bordas internas" (como as pernas do Pokémon verde ou a cabeça - algumas outras técnicas não adicionariam nenhum contorno àquelas )
Objetos com arestas duras e não suaves (como um cubo) não receberão destaque nos locais desejados com esta abordagem. Isso significa que essa abordagem não é uma opção em alguns casos; Não faço ideia se os modelos de Pokemon são lisos ou não.
A maneira mais comum de fazer isso é através de um segundo passe de renderização no seu modelo. Essencialmente, duplique e vire os normais, e coloque-o em um shader de vértice. No sombreador, dimensione cada vértice ao longo do seu normal. No sombreador de pixels / fragmentos, desenhe preto. Isso fornecerá contornos externos e internos, como em torno dos lábios, olhos etc. Essa é realmente uma chamada de compra bastante barata, se nada mais for geralmente mais barato do que o pós-processamento da linha, dependendo do número de modelos e de sua complexidade. O Guilty Gear Xrd usa esse método porque é fácil controlar a espessura da linha através da cor do vértice.
A segunda maneira de fazer linhas internas eu aprendi com o mesmo jogo. No seu mapa UV, alinhe sua textura ao longo do eixo u ou v, principalmente nas áreas em que você deseja uma linha interna. Desenhe uma linha preta ao longo de um dos eixos e mova as coordenadas UV para dentro ou fora dessa linha para criar a linha interna.
Veja o vídeo da GDC para uma explicação melhor: https://www.youtube.com/watch?v=yhGjCzxJV3E
Uma das maneiras de fazer um esboço é usar os vetores normais de nossos modelos. Os vetores normais são vetores perpendiculares à sua superfície (apontando para fora da superfície). O truque aqui é dividir seu modelo de personagem em duas partes. Os vértices que estão voltados para a câmera e os vértices que estão voltados para fora da câmera. Nós os chamaremos FRONT e BACK respectivamente.
Para o esboço, pegamos nossos vértices BACK e os movemos levemente na direção de seus vetores normais. Pense nisso como tornar a parte do personagem que está voltada para a câmera um pouco mais gorda. Depois disso, atribuímos a eles uma cor de nossa escolha e temos um bom esboço.
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
}
}
}
Linha 41: A configuração "Cull Front" diz ao shader para realizar uma seleção nos vértices da frente. Isso significa que ignoraremos todos os vértices da frente deste passe. Ficamos com o lado de trás que queremos manipular um pouco.
Linhas 51-53: A matemática de mover vértices ao longo de seus vetores normais.
Linha 54: Configurando a cor do vértice para a nossa cor de escolha definida nas propriedades do shaders.
Link útil: http://wiki.unity3d.com/index.php/Silhouette-Outlined_Diffuse
outro exemplo
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"
}
Uma das melhores maneiras de fazer isso é renderizar sua cena em uma textura do Framebuffer e renderizá-la enquanto faz uma filtragem Sobel em cada pixel, o que é uma técnica fácil para a detecção de bordas. Dessa forma, você pode não apenas tornar a cena pixelizada (definindo uma baixa resolução para a textura Framebuffer), mas também ter acesso a todos os valores de pixel para fazer Sobel funcionar.