Pular para o conteúdo
Lente · Vinheta e granulaçãoIniciante

Filme antigo

Uma cópia em sépia com cantos escuros, exposição que cintila e um grão que muda 24 vezes por segundo.

UVTempoTelaTela / pós
  1. 01ObjetivoLente
  2. 02TécnicaVinheta e granulação
  3. 03ExecuçãoFilme antigo
Cena de teste

Parâmetros

#ffdba8
0.35
0.60
0.35
1.5
0.12

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.

  1. dotDotProductdotdotdot
    UV
    Tempo
    Tela

    01 Brilho

  2. *Multiply***

    02 Tingir

  3. smoothstepSmoothStepsmoothstepsmoothstepsmoothstep

    03 Vinheta

  4. hashRandomRangehashhashhash

    04 Granulação

  5. *Multiply***

    05 Combinar

Como funciona

O card transforma a cena de teste numa projeção gasta: um único brilho tingido em vez de cor, cantos que caem para o escuro, uma cintilação no quadro inteiro e um grão fino. Tudo acontece numa única passada de pós-processamento que lê o quadro pronto uma vez; o resto é aritmética sobre essa cor.

Passo a passo

  1. Brilho. luma é a luminância da cena, o produto escalar da cor com o quanto cada canal parece claro ao olho. O filme antigo registra luz, não matiz, então esse número é tudo o que a cópia guarda.
  2. Tingir. toned = luma * p_tint mancha esse brilho com a cor do filme, um sépia quente por padrão. Como o p_tint chega em luz linear, o tom escala a luz do jeito que um filtro colorido faria.
  3. Vinheta. centered mede o pixel a partir do meio do quadro em alturas de tela, para o escurecimento continuar redondo num quadro largo em vez de seguir o retângulo. falloff vale 1 dentro de p_radius e cai para 0 ao longo de p_softness, por um smoothstep que não deixa borda visível.
  4. Granulação. speckle é um hash da célula do pixel e do quadro do filme: um valor em 0..1 que parece aleatório, diferente para cada célula e cada quadro. As células têm p_grainSize pixels de largura, então o grão pode ser mais grosso que a tela. O número do quadro é time × 24, arredondado para baixo: grão e cintilação mudam só 24 vezes por segundo, como num projetor, o que parece filme, enquanto um ruído novo a cada quadro parece chuvisco de vídeo.
  5. Combinar. filmed multiplica tudo: o brilho tingido, a vinheta, a cintilação (um hash por quadro, igual para todos os pixels) e o grão, centrado em 1 para clarear e escurecer por igual.

Por que o grão multiplica

Ruído somado levanta o preto para cinza e parece uma lente suja. Ruído multiplicado acompanha a imagem: o preto continua preto, as áreas claras ganham o salpicado mais forte, e o grão segue a imagem em vez de flutuar por cima dela. O grão de filme de verdade aparece mais nos meios-tons; uma versão de produção pode moldar o p_grain pelo luma para chegar lá.

O hash usa inteiros, não o conhecido truque fract(sin(...)). Esse truque depende de como a GPU arredonda um seno grande, o que muda entre fabricantes e cai em padrões visíveis com entradas altas; um hash de inteiros dá os mesmos bits em qualquer GPU. A função de mistura é o finalizador do MurmurHash3, que espalha cada bit da entrada pela saída.

Notas de produção

Uma leitura de textura e cerca de 95 operações por pixel, a maioria nos dois hashes (três rodadas de deslocamentos, XORs e multiplicações cada). Sai muito mais barato que qualquer desfoque, e barato o bastante para ficar ligado no jogo inteiro. O hash da cintilação é o mesmo para todos os pixels de um quadro, então uma versão de produção o calcula uma vez na CPU e o passa como uniform.

Rode-o por último, depois do tone mapping e da correção de cor, como as engines fazem. A cena da prévia entra direto, então as luzes mais fortes estouram, primeiro na cor do tom e depois no branco, quando até o azul dele passa de 1, o que combina com uma cópia antiga e queimada; num jogo, aplique o tone mapping antes, para o grão não ser comprimido junto com os brilhos.

O grão também esconde o banding: um degradê escuro quantizado em 8 bits mostra degraus, e um pouco de ruído antes da codificação final os quebra, o mesmo trabalho que o dither faz. Guarde um pouco de grão por esse motivo mesmo quando o visual não pede nenhum, com uma amplitude de mais ou menos um degrau de 8 bits.

Monte no GLSL

Uma passada de pós. Renderize a cena numa textura e depois desenhe no canvas um triângulo de tela cheia com este shader de fragmento, a cena ligada em t_screen, u_resolution com o tamanho do canvas e u_time em segundos.

O código copiado é um shader de fragmento completo. Ele roda sobre um triângulo de tela cheia cujo shader de vértice escreve v_uv de 0 a 1 de ponta a ponta, e escreve sRGB, já limitado, pronto para o canvas.

  1. Desenhe a cena num framebuffer com uma textura de cor, em luz linear. Uma textura RGBA16F guarda as luzes acima de 1 (o WebGL2 precisa de EXT_color_buffer_float para desenhar nela); RGBA8 também serve, já que este card as corta de qualquer jeito.
  2. Ligue o canvas e desenhe o triângulo de tela cheia com este shader e a textura da cena ligada em t_screen.
  3. A cada quadro, defina u_time com os segundos desde o início e u_resolution com o tamanho do canvas em pixels.
  4. Envie o p_tint decodificado para linear (o comentário ao lado dele avisa) e defina os outros uniforms p_ com os valores do card.

Para usar só a vinheta, ponha p_grain e p_flicker em 0 e um p_tint branco; a cor continua virando cinza, então, para uma vinheta colorida, troque toned por scene.

Monte no Godot

Um shader spatial para um quad de tela cheia na frente da câmera, que lê o quadro por hint_screen_texture. Ele roda antes do tone mapping do Godot, então ponha o tone mapper do Environment em Linear para ter o visual exato do card.

O shader copiado está pronto para usar; o Godot desenha uma passada de pós como uma malha na frente da câmera, e a função de vértice deste shader a estica sobre a tela inteira.

  1. Adicione um MeshInstance3D como filho da sua Camera3D e dê a ele um QuadMesh de tamanho 2×2.
  2. Dê a ele um novo ShaderMaterial e cole o shader num novo Shader.
  3. Aumente o Extra Cull Margin da malha até o máximo (em Geometry), para o Godot nunca descartá-la como fora da tela.
  4. No WorldEnvironment, ponha Tonemap > Mode em Linear.
  5. Ajuste tint, radius, softness, grain, grainSize e flicker nos Shader Parameters do material.

O shader lê o TIME, que continua correndo no editor, então o grão e a cintilação também se animam no viewport.

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

old-film.fragGLSL
#version 300 es// Vignette & film grain → Old film, 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 vec3 p_tint; // Tint: vec3(1.0, 0.86, 0.66), sRGB; upload it decoded to linearuniform float p_radius; // Vignette radius: 0.35 (0.0 to 1.0)uniform float p_softness; // Vignette softness: 0.6 (0.05 to 1.5)uniform float p_grain; // Grain: 0.35 (0.0 to 1.0)uniform float p_grainSize; // Grain size (px): 1.5 (1.0 to 4.0)uniform float p_flicker; // Flicker: 0.12 (0.0 to 0.5)uniform 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));}// MurmurHash3's finalizer (public domain): every input bit ends up flipping about half of the output bits. highp, because// a fragment shader's integers default to mediump, which some mobile GPUs keep in 16 bitshighp uint mix32(highp uint x) {	x ^= x >> 16u;	x *= 0x85ebca6bu;	x ^= x >> 13u;	x *= 0xc2b2ae35u;	x ^= x >> 16u;	return x;}// A value in 0..1 that looks random, one per cell and film frame; an integer hash gives the same bits on every GPUfloat hash(highp ivec2 cell, highp int frame) {	highp uint h = mix32(uint(cell.x) ^ mix32(uint(cell.y) ^ mix32(uint(frame))));	return float(h >> 8u) / 16777216.0;}vec4 effect(Inputs i) {	vec3 scene = texture(t_screen, i.uv).rgb;	float luma = dot(scene, vec3(0.2126, 0.7152, 0.0722));	vec3 toned = luma * p_tint;	// Measured in screen heights from the center, so the darkening stays round on a wide frame	vec2 centered = (i.uv - 0.5) * vec2(i.resolution.x / i.resolution.y, 1.0);	float falloff = 1.0 - smoothstep(p_radius, p_radius + p_softness, length(centered));	// A projector shows 24 frames a second: grain and flicker change only that often, so they read as film, not static	highp int frame = int(i.time * 24.0);	float speckle = hash(ivec2(i.fragCoord / p_grainSize), frame);	float flash = 1.0 + (hash(ivec2(0), frame) - 0.5) * p_flicker;	// Grain scales the color rather than adding to it, so black stays black and the grain follows the picture	vec3 filmed = toned * falloff * flash * (1.0 + (speckle - 0.5) * p_grain);	return vec4(filmed, 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);}
old-film.gdshaderShader do Godot
// Vignette & film grain → Old film, 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 vec3 tint : source_color = vec3(1.0, 0.86, 0.66);uniform float radius : hint_range(0.0, 1.0, 0.01) = 0.35;uniform float softness : hint_range(0.05, 1.5, 0.01) = 0.6;uniform float grain : hint_range(0.0, 1.0, 0.01) = 0.35;uniform float grainSize : hint_range(1.0, 4.0, 0.1) = 1.5;uniform float flicker : hint_range(0.0, 0.5, 0.01) = 0.12;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);}// MurmurHash3's finalizer (public domain): every input bit ends up flipping about half of the output bits. highp, because// a fragment shader's integers default to mediump, which some mobile GPUs keep in 16 bitshighp uint mix32(highp uint x) {	x ^= x >> 16u;	x *= 0x85ebca6bu;	x ^= x >> 13u;	x *= 0xc2b2ae35u;	x ^= x >> 16u;	return x;}// A value in 0..1 that looks random, one per cell and film frame; an integer hash gives the same bits on every GPUfloat hash(highp ivec2 cell, highp int frame) {	highp uint h = mix32(uint(cell.x) ^ mix32(uint(cell.y) ^ mix32(uint(frame))));	return float(h >> 8u) / 16777216.0;}vec4 effect(Inputs i) {	vec3 scene = texture(t_screen, u_flipUv(i.uv)).rgb;	float luma = dot(scene, vec3(0.2126, 0.7152, 0.0722));	vec3 toned = luma * tint;	// Measured in screen heights from the center, so the darkening stays round on a wide frame	vec2 centered = (i.uv - 0.5) * vec2(i.resolution.x / i.resolution.y, 1.0);	float falloff = 1.0 - smoothstep(radius, radius + softness, length(centered));	// A projector shows 24 frames a second: grain and flicker change only that often, so they read as film, not static	highp int frame = int(i.time * 24.0);	float speckle = hash(ivec2(i.fragCoord / grainSize), frame);	float flash = 1.0 + (hash(ivec2(0), frame) - 0.5) * flicker;	// Grain scales the color rather than adding to it, so black stays black and the grain follows the picture	vec3 filmed = toned * falloff * flash * (1.0 + (speckle - 0.5) * grain);	return vec4(filmed, 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