Contorno por casca invertida
A malha desenhada duas vezes, a segunda inflada, do avesso e em tinta, para que uma borda apareça em volta da silhueta.
- 01ObjetivoEstilizar
- 02TécnicaContornos
- 03ExecuçãoContorno por casca invertida
Parâmetros
Copie para a sua plataforma
- GLSLAdaptar
- GodotAdaptar
Cadeia de nós
Montada passo a passo, no estilo do GLSL, com os nomes de nós dele.
Montada passo a passo, no estilo do Godot, com os nomes de nós dele.
Este card ainda não tem versão para Unreal Engine 5, então os nós mostram os nomes do GLSL.
Este card ainda não tem versão para Unity, então os nós mostram os nomes do GLSL.
Este card ainda não tem versão para Blender, então os nós mostram os nomes do GLSL.
uniformColorParameteruniformuniformuniform UVPosiçãoNormalTela01 Casca crescida, só o verso
Passe hull
dotDotProductdotdotdot Entrada02 Quanto encara a luz
Passe main
smoothstepSmoothStepsmoothstepsmoothstepsmoothstep Entrada03 Duas faixas
Passe main
mixMixmixmixmix Entrada04 Malha sobre a casca
Passe main
Como funciona
Este é o truque de contorno em tempo real mais antigo, e ele não usa nenhum processamento de imagem. A malha é desenhada duas vezes. O primeiro desenho é o contorno: uma cópia um pouco maior da malha, virada do avesso. O segundo é a própria malha, que cobre quase todo o primeiro.
1. Crescer a casca. O passe Normal Um vetor unitário perpendicular a uma superfície, apontando para onde ela está virada. Iluminação e efeitos Fresnel o comparam com outras direções.hull tem uma entrada de vértice, extrude, que move cada vértice ao longo da
normalUm vetor unitário perpendicular a uma superfície, apontando para onde ela está virada. Iluminação e efeitos Fresnel o comparam com outras direções.
2. Ficar só com o verso. O mesmo passe descarta as faces da frente em vez das de trás. Vista da câmera, sobra só o lado de dentro da metade de trás da casca, pintado chapado na cor da linha. A prévia do passo a mostra sozinha: um disco de tinta um pouco maior que a malha.
3. Desenhar a malha por cima. O passe Buffer de profundidade Uma imagem do tamanho do framebuffer que guarda, para cada pixel, a distância da superfície mais próxima desenhada até agora. O teste de profundidade a usa para esconder o que fica atrás.main desenha a malha de verdade com o descarte normal, dividindo o
depth bufferUma imagem do tamanho do framebuffer que guarda, para cada pixel, a distância da superfície mais próxima desenhada até agora. O teste de profundidade a usa para esconder o que fica atrás.
4. Sombrear a malha. A malha recebe uma luz toon de duas faixas (um produto escalarMultiplica dois vetores componente a componente e soma os resultados. Para vetores unitários é o cosseno do ângulo entre eles, de 1 (mesma direção) a -1 (opostos). Produto escalar Multiplica dois vetores componente a componente e soma os resultados. Para vetores unitários é o cosseno do ângulo entre eles, de 1 (mesma direção) a -1 (opostos). Smoothstep Um limiar suave. Vale 0 abaixo da primeira borda, 1 acima da segunda e segue uma curva em S com as pontas planas entre elas.dot(a, b) = |a| |b| cos θt = clamp((x − e0) / (e1 − e0), 0, 1); s = t² (3 − 2t)
Por que descartar as faces da frente funciona
Uma malha fechada vista de fora mostra as faces da frente e esconde as de trás atrás delas. Descarte as da frente e você vê as de trás pelo buraco, o lado de dentro da parede do fundo. Essa parede do fundo está sempre atrás da malha de verdade, e é por isso que a malha ganha o teste de profundidade em todo lugar, menos na borda. É também por isso que a casca nunca cobre o que está na frente da malha: ela fica atrás da malha, então a geometria mais perto da câmera continua ganhando.
Onde ele quebra
- Arestas duras. Um cubo tem vértices separados em cada canto, um por face, cada um com a própria normal. Empurradas ao longo de normais diferentes, as faces se afastam e deixam buracos na linha. Use normais suavizadas para a casca, muitas vezes gravadas num segundo conjunto de normais ou nas cores de vértice, e mantenha as normais duras para a luz.
- Espessura em unidades de mundo. Uma linha de 0,04 unidade é grossa de perto e fina de longe. Multiplicar a espessura pela distância até a câmera a mantém constante na tela; isso exige a posição da câmera no estágio de vértice.
- Só contornos. Um braço na frente do corpo ganha a linha dele, porque a casca do braço fica na frente do corpo; mas um vinco, ou uma dobra onde duas superfícies quase se tocam, fica mais perto que a espessura da linha e não ganha nenhuma. Linhas ali, e dentro de áreas planas, exigem detecção de bordas.
- O dobro de trabalho de vértice. Cada malha contornada é desenhada duas vezes, então uma multidão de personagens
contornados dobra o custo de vértices. A casca é desenhada primeiro, então a maior parte dos pixels dela é sombreada e
depois coberta pela malha, mas o shader de fragmento dela só devolve uma cor, então esse
overdrawSombrear o mesmo pixel mais de uma vez num quadro, como fazem camadas transparentes sobrepostas. Cada camada roda o shader de fragmento de novo.Fundamento
Overdraw
Sombrear o mesmo pixel mais de uma vez num quadro, como fazem camadas transparentes sobrepostas. Cada camada roda o shader de fragmento de novo.
Fontessai barato.- GPU optimization (abre em uma nova aba)Documentação
- WebGL best practices (abre em uma nova aba)Documentação
Monte no GLSL
Dois passes sobre a mesma malha e um único depth buffer. O passe "hull" (o shader de vértice roda extrude, o de fragmento roda outline) é desenhado primeiro com gl.cullFace(gl.FRONT); o passe "main" vem depois com gl.cullFace(gl.BACK). O teste e a escrita de profundidade ficam ligados nos dois. O botão de copiar entrega três shaders, os shaders de vértice e de fragmento do passe "hull" e o shader de fragmento do passe "main"; o passe "main" precisa de um shader de vértice simples que escreva os mesmos varyings, que é o shader de vértice da casca sem a linha `i = extrude(i);`.
Compile dois programas: o da casca, com os shaders de vértice e de fragmento copiados, e o principal, com o shader de
fragmento do passe “main” copiado e uma cópia do shader de vértice da casca sem a chamada de extrude(). Os dois
shaders de vértice leem os mesmos atributos (a_position, a_normal, a_uv, aqui em espaço de
mundo, já que os shaders não aplicam matriz de modelo) e os mesmos uniforms: u_viewProjection, u_camera e os
parâmetros. Preencha os parâmetros nos dois programas, porque cada um declara todos eles.
A cada quadro, limpe cor e profundidade e então:
gl.enable(gl.CULL_FACE),gl.cullFace(gl.FRONT), use o programa da casca e desenhe a malha.gl.cullFace(gl.BACK), use o programa principal e desenhe a mesma malha de novo.
O teste de profundidade dá a mesma imagem em qualquer ordem. Desenhar a malha principal primeiro sai um pouco mais barato, porque o teste de profundidade antecipado descarta os pixels escondidos da casca antes de o shader de fragmento dela rodar; mas esse shader só devolve uma cor, então a diferença é pequena.
Se a malha tem uma matriz de modelo, aplique-a na posição e na normal antes de extrude() rodar, para a espessura da
linha continuar em unidades de mundo qualquer que seja a escala do objeto.
Monte no Godot
Dois materiais num MeshInstance3D. O primeiro shader (passe "hull", render_mode cull_front) vai no ShaderMaterial da malha; o segundo (passe "main") vai num segundo ShaderMaterial posto como Next Pass do primeiro. O Godot desenha o next pass logo depois, com o mesmo depth buffer.
Selecione o MeshInstance3D e crie um novo ShaderMaterial no Material Override (ou no material da superfície). Cole o primeiro shader, aquele cujo cabeçalho diz pass “hull” (1 of 2), num novo Shader ali. Nesse material, abra Next Pass, crie outro ShaderMaterial e cole o segundo shader nele.
O material base desenha a casca crescida com as faces da frente descartadas, e o Godot desenha em seguida o next pass, a malha com luz toon, no mesmo depth buffer, que esconde tudo da casca menos a borda. Os dois são unshaded, então as cores são exatamente as da prévia; ponha o Tonemap Mode do WorldEnvironment em Linear para que continuem assim.
Sem código, o mesmo truque é um StandardMaterial3D para a malha com um segundo StandardMaterial3D no Next Pass que seja unshaded, tenha o Cull Mode em Front, a cor da linha como Albedo e o Grow ligado com um Grow Amount pequeno.
Ainda não há tutorial de Unreal Engine 5 para este card.
Ainda não há tutorial de Unity para este card.
Ainda não há tutorial de Blender para este card.
Código
#version 300 es// Outlines → Inverted-hull outline, pass "hull" (1 of 2), vertex shader, web GLSL (GLSL ES 3.00). From Creative Center, MIT license.precision highp float;uniform mat4 u_viewProjection;uniform vec3 u_camera;uniform float u_time;uniform vec2 u_resolution; // this pass's output size in pixelsuniform float p_width; // Line width: 0.04 (0.0 to 0.15)uniform vec3 p_lineColor; // Line color: vec3(0.1, 0.07, 0.12), sRGB; upload it decoded to linearuniform vec3 p_base; // Base color: vec3(0.42, 0.74, 0.9), sRGB; upload it decoded to linearuniform vec3 p_shadow; // Shadow color: vec3(0.27, 0.4, 0.66), sRGB; upload it decoded to linearlayout(location = 0) in vec3 a_position;layout(location = 1) in vec3 a_normal;layout(location = 2) in vec2 a_uv;out vec2 v_uv;out vec3 v_position;out vec3 v_normal;out vec3 v_view;struct Inputs { vec2 uv; vec3 position; vec3 normal; vec3 view; float time; vec2 fragCoord; vec2 resolution;};// The hull is the same mesh pushed out along its normals. Pass "hull" culls its front faces, so only the far inside of// the shell is drawn; the mesh drawn after it covers all of that except the rim that sticks out past its silhouette.Inputs extrude(Inputs i) { i.position += i.normal * p_width; return i;}vec4 outline(Inputs i) { vec3 ink = p_lineColor; return vec4(ink, 1.0);}vec4 effect(Inputs i) { float lambert = dot(i.normal, normalize(vec3(-0.4, 0.8, 0.45))); float lit = smoothstep(0.08, 0.12, lambert); vec3 color = mix(p_shadow, p_base, lit); return vec4(color, 1.0);}void main() { Inputs i; i.uv = a_uv; // The test meshes have no model transform: object space is world space i.position = a_position; i.normal = normalize(a_normal); i.view = normalize(u_camera - a_position); i.time = u_time; // No pixel yet in a vertex shader i.fragCoord = vec2(0.0); i.resolution = u_resolution; i = extrude(i); v_uv = a_uv; v_position = i.position; v_normal = i.normal; // From the moved surface toward the camera; the fragment shader normalizes it v_view = u_camera - i.position; gl_Position = u_viewProjection * vec4(i.position, 1.0);}#version 300 es// Outlines → Inverted-hull outline, pass "hull" (1 of 2), web GLSL (GLSL ES 3.00). From Creative Center, MIT license.precision highp float;uniform float u_time;uniform vec2 u_resolution; // this pass's output size in pixelsuniform float p_width; // Line width: 0.04 (0.0 to 0.15)uniform vec3 p_lineColor; // Line color: vec3(0.1, 0.07, 0.12), sRGB; upload it decoded to linearuniform vec3 p_base; // Base color: vec3(0.42, 0.74, 0.9), sRGB; upload it decoded to linearuniform vec3 p_shadow; // Shadow color: vec3(0.27, 0.4, 0.66), sRGB; upload it decoded to linearin vec2 v_uv;in vec3 v_position;in vec3 v_normal;in vec3 v_view;out vec4 fragColor;struct Inputs { vec2 uv; vec3 position; vec3 normal; vec3 view; float time; vec2 fragCoord; vec2 resolution;};vec3 u_toSrgb(vec3 c) { return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c));}// The hull is the same mesh pushed out along its normals. Pass "hull" culls its front faces, so only the far inside of// the shell is drawn; the mesh drawn after it covers all of that except the rim that sticks out past its silhouette.Inputs extrude(Inputs i) { i.position += i.normal * p_width; return i;}vec4 outline(Inputs i) { vec3 ink = p_lineColor; return vec4(ink, 1.0);}vec4 effect(Inputs i) { float lambert = dot(i.normal, normalize(vec3(-0.4, 0.8, 0.45))); float lit = smoothstep(0.08, 0.12, lambert); vec3 color = mix(p_shadow, p_base, lit); return vec4(color, 1.0);}void main() { Inputs i; i.uv = v_uv; i.position = v_position; // Interpolation shortens the vectors between vertices i.normal = normalize(v_normal); i.view = normalize(v_view); i.time = u_time; i.fragCoord = gl_FragCoord.xy; i.resolution = u_resolution; vec4 frag_out = outline(i); // The output is sRGB: clamped and encoded once, here, with no tone mapping; opaque, so alpha is 1 fragColor = vec4(u_toSrgb(clamp(frag_out.rgb, 0.0, 1.0)), 1.0);}#version 300 es// Outlines → Inverted-hull outline, pass "main" (2 of 2), web GLSL (GLSL ES 3.00). From Creative Center, MIT license.precision highp float;uniform float u_time;uniform vec2 u_resolution; // this pass's output size in pixelsuniform float p_width; // Line width: 0.04 (0.0 to 0.15)uniform vec3 p_lineColor; // Line color: vec3(0.1, 0.07, 0.12), sRGB; upload it decoded to linearuniform vec3 p_base; // Base color: vec3(0.42, 0.74, 0.9), sRGB; upload it decoded to linearuniform vec3 p_shadow; // Shadow color: vec3(0.27, 0.4, 0.66), sRGB; upload it decoded to linearin vec2 v_uv;in vec3 v_position;in vec3 v_normal;in vec3 v_view;out vec4 fragColor;struct Inputs { vec2 uv; vec3 position; vec3 normal; vec3 view; float time; vec2 fragCoord; vec2 resolution;};vec3 u_toSrgb(vec3 c) { return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c));}// The hull is the same mesh pushed out along its normals. Pass "hull" culls its front faces, so only the far inside of// the shell is drawn; the mesh drawn after it covers all of that except the rim that sticks out past its silhouette.Inputs extrude(Inputs i) { i.position += i.normal * p_width; return i;}vec4 outline(Inputs i) { vec3 ink = p_lineColor; return vec4(ink, 1.0);}vec4 effect(Inputs i) { float lambert = dot(i.normal, normalize(vec3(-0.4, 0.8, 0.45))); float lit = smoothstep(0.08, 0.12, lambert); vec3 color = mix(p_shadow, p_base, lit); return vec4(color, 1.0);}void main() { Inputs i; i.uv = v_uv; i.position = v_position; // Interpolation shortens the vectors between vertices i.normal = normalize(v_normal); i.view = normalize(v_view); i.time = u_time; i.fragCoord = gl_FragCoord.xy; i.resolution = u_resolution; vec4 frag_out = effect(i); // The output is sRGB: clamped and encoded once, here, with no tone mapping; opaque, so alpha is 1 fragColor = vec4(u_toSrgb(clamp(frag_out.rgb, 0.0, 1.0)), 1.0);}// Outlines → Inverted-hull outline, pass "hull" (1 of 2), Godot 4. From Creative Center, MIT license.// Godot draws a material's next_pass after it: this is the ShaderMaterial, and pass "main"'s shader its next_pass.shader_type spatial;// Unshaded shows the effect's color as is, like the site's preview; scene lights don't touch it// world_vertex_coords: vertex() sees VERTEX and NORMAL in world space, as the effect's vertex entry expectsrender_mode unshaded, world_vertex_coords, cull_front;uniform float width : hint_range(0.0, 0.15, 0.005) = 0.04;uniform vec3 lineColor : source_color = vec3(0.1, 0.07, 0.12);uniform vec3 base : source_color = vec3(0.42, 0.74, 0.9);uniform vec3 shadow : source_color = vec3(0.27, 0.4, 0.66);struct Inputs { vec2 uv; vec3 position; vec3 normal; vec3 view; float time; vec2 fragCoord; vec2 resolution;};// The hull is the same mesh pushed out along its normals. Pass "hull" culls its front faces, so only the far inside of// the shell is drawn; the mesh drawn after it covers all of that except the rim that sticks out past its silhouette.Inputs extrude(Inputs i) { i.position += i.normal * width; return i;}vec4 outline(Inputs i) { vec3 ink = lineColor; return vec4(ink, 1.0);}vec4 effect(Inputs i) { float lambert = dot(i.normal, normalize(vec3(-0.4, 0.8, 0.45))); float lit = smoothstep(0.08, 0.12, lambert); vec3 color = mix(shadow, base, lit); return vec4(color, 1.0);}void vertex() { Inputs i; // Godot's UV starts at the top-left; the effect expects GLSL's bottom-left i.uv = vec2(UV.x, 1.0 - UV.y); i.position = VERTEX; i.normal = normalize(NORMAL); i.view = normalize(CAMERA_POSITION_WORLD - VERTEX); i.time = TIME; // No pixel yet in vertex() i.fragCoord = vec2(0.0); i.resolution = VIEWPORT_SIZE; i = extrude(i); VERTEX = i.position; NORMAL = i.normal;}void fragment() { Inputs i; // Godot's UV starts at the top-left; the effect expects GLSL's bottom-left i.uv = vec2(UV.x, 1.0 - UV.y); // VERTEX, NORMAL and VIEW are in view space here; the effect works in world space i.position = (INV_VIEW_MATRIX * vec4(VERTEX, 1.0)).xyz; i.normal = normalize((INV_VIEW_MATRIX * vec4(NORMAL, 0.0)).xyz); i.view = normalize((INV_VIEW_MATRIX * vec4(VIEW, 0.0)).xyz); i.time = TIME; // FRAGCOORD's origin is the top-left too i.resolution = VIEWPORT_SIZE; i.fragCoord = vec2(FRAGCOORD.x, i.resolution.y - FRAGCOORD.y); vec4 frag_out = outline(i); ALBEDO = frag_out.rgb;}// Outlines → Inverted-hull outline, pass "main" (2 of 2), Godot 4. From Creative Center, MIT license.// The next_pass of pass "hull"'s material.shader_type spatial;// Unshaded shows the effect's color as is, like the site's preview; scene lights don't touch itrender_mode unshaded;uniform float width : hint_range(0.0, 0.15, 0.005) = 0.04;uniform vec3 lineColor : source_color = vec3(0.1, 0.07, 0.12);uniform vec3 base : source_color = vec3(0.42, 0.74, 0.9);uniform vec3 shadow : source_color = vec3(0.27, 0.4, 0.66);struct Inputs { vec2 uv; vec3 position; vec3 normal; vec3 view; float time; vec2 fragCoord; vec2 resolution;};// The hull is the same mesh pushed out along its normals. Pass "hull" culls its front faces, so only the far inside of// the shell is drawn; the mesh drawn after it covers all of that except the rim that sticks out past its silhouette.Inputs extrude(Inputs i) { i.position += i.normal * width; return i;}vec4 outline(Inputs i) { vec3 ink = lineColor; return vec4(ink, 1.0);}vec4 effect(Inputs i) { float lambert = dot(i.normal, normalize(vec3(-0.4, 0.8, 0.45))); float lit = smoothstep(0.08, 0.12, lambert); vec3 color = mix(shadow, base, lit); return vec4(color, 1.0);}void fragment() { Inputs i; // Godot's UV starts at the top-left; the effect expects GLSL's bottom-left i.uv = vec2(UV.x, 1.0 - UV.y); // VERTEX, NORMAL and VIEW are in view space here; the effect works in world space i.position = (INV_VIEW_MATRIX * vec4(VERTEX, 1.0)).xyz; i.normal = normalize((INV_VIEW_MATRIX * vec4(NORMAL, 0.0)).xyz); i.view = normalize((INV_VIEW_MATRIX * vec4(VIEW, 0.0)).xyz); i.time = TIME; // FRAGCOORD's origin is the top-left too i.resolution = VIEWPORT_SIZE; i.fragCoord = vec2(FRAGCOORD.x, i.resolution.y - FRAGCOORD.y); vec4 frag_out = effect(i); ALBEDO = frag_out.rgb;}Ainda não há código de Unreal Engine 5 para este card.
Ainda não há código de Unity para este card.
Ainda não há código de Blender para este card.