Dithering de 1 bit
A cena inteira em duas cores, com o sombreado carregado por um padrão de Bayer de pontos de tinta e papel.
- 01ObjetivoEstilizar
- 02TécnicaPosterização e dithering
- 03ExecuçãoDithering de 1 bit
Parâmetros
Copie para a sua plataforma
- GLSLCopiar
- GodotCopiar
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.
textureTexture2Dtexturetexturetexture Tela01 Cena, uma leitura por bloco
dotDotProductdotdotdot Entrada02 Brilho
float[16]Expressionfloat[16]float[16]float[16] Entrada03 Limiar de Bayer
stepStepstepstepstep Entrada04 Ligado ou desligado
mixMixmixmixmix Entrada05 Tinta e papel
Como funciona
Cada pixel da saída é tinta ou papel, duas cores e nada entre elas. Todo o sombreado da cena, a queda de luz da esfera,
o degradê do céu, o chão quadriculado, é carregado por quantos pontos estão ligados. Este é o
ditheringBanding são as faixas em que um degradê suave se parte quando guardado com poucos níveis. O dithering soma um ruído minúsculo por pixel antes do arredondamento, e as faixas viram uma granulação fina. Banding e dithering Banding são as faixas em que um degradê suave se parte quando guardado com poucos níveis. O dithering soma um ruído minúsculo por pixel antes do arredondamento, e as faixas viram uma granulação fina.
1. Cena, uma leitura por bloco. O passe corta a tela em blocos de Tamanho do ponto × Tamanho do ponto pixels e lê a cena uma vez, no centro do bloco, para que todos os pixels de um bloco concordem. Com tamanho acima de 1, cada bloco parece um pixel gordo, como uma tela antiga de baixa resolução ampliada.
2. Brilho. A cor é multiplicada pela Exposição, passa por um tone mapping Luminância O quanto uma cor parece clara, a partir do vermelho, verde e azul lineares, pesados pela sensibilidade do olho a cada um.c / (1 + c) para as formas emissivas
claras ficarem abaixo de 1 e vira um número com os pesos de luminânciaO quanto uma cor parece clara, a partir do vermelho, verde e azul lineares, pesados pela sensibilidade do olho a cada um.Y = 0.2126 R + 0.7152 G + 0.0722 B
3. Limiar de Bayer. Cada bloco recebe um limiar de uma matriz de BayerUm pequeno quadrado de limiares, repetido pela tela, ordenado para que, em qualquer brilho, os pixels que passam fiquem espalhados da forma mais uniforme possível. É a base do dithering ordenado. Matriz de Bayer Um pequeno quadrado de limiares, repetido pela tela, ordenado para que, em qualquer brilho, os pixels que passam fiquem espalhados da forma mais uniforme possível. É a base do dithering ordenado.threshold = (M[y % 4][x % 4] + 0.5) / 16(n + 0.5) / 16 os transforma em 16 limiares igualmente espaçados entre 0 e 1. A prévia do passo mostra o ladrilho: uma
trama fina de 16 cinzas, repetida a cada quatro blocos.
4. Ligado ou desligado. step(threshold, brightness) vale 1 onde o brilho supera o limiar do bloco. Um brilho de
0,25 supera 4 dos 16 limiares, então 4 blocos de cada ladrilho ligam, bem espalhados. De uma distância normal, um quarto
da área é papel e parece um cinza escuro.
5. Tinta e papel. Um mix entre as duas cores termina o trabalho.
Por que comparar no brilho linear
O limiar é comparado com a luminância linear, não com o valor sRGB que a tela guarda. Os pontos se misturam no olho: um ladrilho que é um quarto papel manda um quarto da luz do papel para o olho, que é exatamente o que um valor linear de 0,25 quer dizer. Comparar o valor sRGB ligaria pontos demais nos escuros e lavaria a imagem.
Bom saber
- O padrão fica preso à tela, não aos objetos, então ele fica parado enquanto a cena se move por baixo. Muitos jogos com dithering aceitam isso; outros fazem o padrão acompanhar a câmera para escondê-lo.
- Detalhes finos se partem. Uma linha de um pixel num cinza médio cai em pontos alternados e vira tracejada. Pontos maiores pioram isso. Linhas de contorno desenhadas depois do dithering continuam inteiras.
- Redimensionar estraga. O padrão só funciona pixel a pixel. Se a saída for redimensionada por um fator não inteiro, os pontos batem com os pixels da tela e fazem moiré. Renderize no tamanho final, ou amplie por números inteiros com filtragem nearest.
Monte no GLSL
Um passe de pós-processamento. Renderize a cena numa textura e depois desenhe um triângulo de tela inteira com este shader de fragmento, a cena ligada em t_screen e u_resolution com o tamanho da saída. O padrão vem de gl_FragCoord, então o passe precisa desenhar no tamanho em que a imagem é mostrada.
Renderize a cena num framebuffer com uma textura como anexo de cor (RGBA16F guarda os valores emissivos acima de 1; o
shader faz tone mapping deles). Depois ligue o framebuffer padrão, ligue a textura em t_screen com clamp-to-edge e
desenhe um triângulo de tela inteira com este shader.
O main() preenche uv a partir da varying v_uv e fragCoord a partir de gl_FragCoord; o card só lê fragCoord,
mas as outras varyings ainda precisam ser escritas, e v_normal e v_view não podem ser zero (main() as normaliza).
Preencha as duas cores decodificadas para linear, p_pixelSize com um número inteiro de pixels e p_exposure a gosto.
O passe codifica a própria saída em sRGB.
Para um visual de baixa resolução de verdade, renderize a cena num alvo pequeno (digamos 320 × 180), rode este passe nesse tamanho com tamanho de ponto 1 e depois amplie o resultado para a tela por um fator inteiro com filtragem nearest.
Monte no Godot
Um shader spatial para um quad de tela inteira, que lê o quadro por hint_screen_texture. A matriz de Bayer é um array const global, que a linguagem de shader do Godot aceita como está escrito.
Acrescente um MeshInstance3D sob a Camera3D com um QuadMesh de 2 × 2 e dê ao QuadMesh um novo ShaderMaterial com este
shader. A função de vértice escreve o quad direto no clip space para ele cobrir a tela, e o depth_test_disabled
impede que a cena o esconda. Ponha o Extra Cull Margin do MeshInstance3D em 16384 para o Godot nunca descartá-lo.
Para uma resolução retrô, renderize a cena 3D num SubViewport de, digamos, 320 × 180, mostre-o num TextureRect ampliado por um fator inteiro com o Texture Filter em Nearest e ponha o quad na câmera desse SubViewport com o Tamanho do ponto em 1. Cada ponto passa a ser um pixel da imagem de baixa resolução.
No VisualShader, a cadeia é um nó Texture2D com Source em Screen, um DotProduct com os pesos de luminância, um nó Expression que guarda o array de Bayer e devolve o limiar, um Step e um Mix ligados em Albedo, com a flag Unshaded ligada em Flags.
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// Posterize & dither → 1-bit dither, 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_pixelSize; // Dot size (px): 2.0 (1.0 to 8.0)uniform float p_exposure; // Exposure: 1.5 (0.25 to 4.0)uniform vec3 p_ink; // Ink: vec3(0.13, 0.11, 0.2), sRGB; upload it decoded to linearuniform vec3 p_paper; // Paper: vec3(0.93, 0.9, 0.8), sRGB; upload it decoded to linearuniform sampler2D t_screen; // the scene's color: linear HDR (RGBA16F), linear filter, clamp to edgein 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));}// A 4×4 Bayer matrix: each threshold from 0 to 15 appears once per tile, ordered so that for any brightness the pixels// that switch on are spread as evenly as the tile allows.const float BAYER[16] = float[16]( 0.0, 8.0, 2.0, 10.0, 12.0, 4.0, 14.0, 6.0, 3.0, 11.0, 1.0, 9.0, 15.0, 7.0, 13.0, 5.0);vec4 effect(Inputs i) { // Every pixel of a block reads the block's center, so a block is one dither dot vec2 cell = floor(i.fragCoord / p_pixelSize); vec3 scene = texture(t_screen, (cell + 0.5) * p_pixelSize / i.resolution).rgb * p_exposure; vec3 mapped = scene / (1.0 + scene); float brightness = dot(mapped, vec3(0.2126, 0.7152, 0.0722)); ivec2 p = ivec2(cell) & ivec2(3); float threshold = (BAYER[p.y * 4 + p.x] + 0.5) / 16.0; float lit = step(threshold, brightness); vec3 color = mix(p_ink, p_paper, 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);}// Posterize & dither → 1-bit dither, Godot 4. From Creative Center, MIT license.shader_type spatial;// A full-screen quad over the finished frame: no lighting, no culling, never hidden by depthrender_mode unshaded, cull_disabled, depth_test_disabled;uniform float pixelSize : hint_range(1.0, 8.0, 1.0) = 2.0;uniform float exposure : hint_range(0.25, 4.0, 0.05) = 1.5;uniform vec3 ink : source_color = vec3(0.13, 0.11, 0.2);uniform vec3 paper : source_color = vec3(0.93, 0.9, 0.8);uniform sampler2D t_screen : hint_screen_texture, filter_linear, repeat_disable;struct Inputs { vec2 uv; vec3 position; vec3 normal; vec3 view; float time; vec2 fragCoord; vec2 resolution;};// Godot's textures start at the top-left and the effect's uv at the bottom-left: each sample flips v backvec2 u_flipUv(vec2 uv) { return vec2(uv.x, 1.0 - uv.y);}// A 4×4 Bayer matrix: each threshold from 0 to 15 appears once per tile, ordered so that for any brightness the pixels// that switch on are spread as evenly as the tile allows.const float BAYER[16] = float[16]( 0.0, 8.0, 2.0, 10.0, 12.0, 4.0, 14.0, 6.0, 3.0, 11.0, 1.0, 9.0, 15.0, 7.0, 13.0, 5.0);vec4 effect(Inputs i) { // Every pixel of a block reads the block's center, so a block is one dither dot vec2 cell = floor(i.fragCoord / pixelSize); vec3 scene = texture(t_screen, u_flipUv((cell + 0.5) * pixelSize / i.resolution)).rgb * exposure; vec3 mapped = scene / (1.0 + scene); float brightness = dot(mapped, vec3(0.2126, 0.7152, 0.0722)); ivec2 p = ivec2(cell) & ivec2(3); float threshold = (BAYER[p.y * 4 + p.x] + 0.5) / 16.0; float lit = step(threshold, brightness); vec3 color = mix(ink, paper, lit); return vec4(color, 1.0);}void vertex() { // Put the shader on a MeshInstance3D with a 2x2 QuadMesh in front of the camera; it covers the screen wherever it is POSITION = vec4(VERTEX.xy, 1.0, 1.0);}void fragment() { Inputs i; // SCREEN_UV starts at the top-left; the effect expects GLSL's bottom-left i.uv = vec2(SCREEN_UV.x, 1.0 - SCREEN_UV.y); // A flat quad facing the viewer, the same as the site's 2D preview i.position = vec3(i.uv * 2.0 - 1.0, 0.0); i.normal = vec3(0.0, 0.0, 1.0); i.view = vec3(0.0, 0.0, 1.0); 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.
Leitura complementar
- Ordered dithering (abre em uma nova aba)Referência
- Dither (abre em uma nova aba)Referência
- Relative luminance (abre em uma nova aba)Referência
- gl_FragCoord (abre em uma nova aba)Referência