Anéis de madeira
Madeira maciça entalhada no espaço 3D, anéis de crescimento entortados por ruído gradiente e riscados por fibras finas.
- 01ObjetivoMateriais
- 02TécnicaMadeira e mármore
- 03ExecuçãoAnéis de madeira
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.
lengthLength2Dlengthlengthlength PosiçãoUV01 Distância até o eixo do tronco
gradientNoiseTexture3DgradientNoisegradientNoisegradientNoise Entrada02 Ruído gradiente
fractFractfractfractfract Entrada03 Repetir em anéis
powPowpowpowpow Entrada04 Moldar cada ano
gradientNoiseTexture3DgradientNoisegradientNoisegradientNoise Entrada05 Fibras finas
mixMixmixmixmix Entrada06 Colorir a madeira
Como funciona
A esfera parece entalhada num tronco: o topo do tronco, em anéis concêntricos, em cima e embaixo, e listras longas e
onduladas pelos lados. Nenhuma UV entra na conta. O shader mede a distância de cada ponto da superfície até um tronco
imaginário que sobe pelo eixo y, transforma essa distância em anéis e deixa o ruídoUma função pseudoaleatória e suave da posição. Valores ou gradientes aleatórios ficam numa grade e são misturados entre os pontos, então pontos próximos recebem valores parecidos e o resultado não tem padrão visível. Ruído Uma função pseudoaleatória e suave da posição. Valores ou gradientes aleatórios ficam numa grade e são misturados entre os pontos, então pontos próximos recebem valores parecidos e o resultado não tem padrão visível.
Passo a passo
- Distância até o eixo do tronco.
length(p.xz)ignora a altura, então é a distância até o eixo y: zero no cerne do tronco e crescendo para fora. O cerne sai do centro do objeto, para (0,3, 0, −0,2), como na maioria das peças de madeira serrada, para que os anéis não se centralizem nos polos da esfera. - Ruído gradiente. Uma oitava de ruído gradiente 3D, com a entrada achatada em y para um quinto da escala na direção transversal: o veio vaga devagar árvore acima e depressa em volta dela. A prévia mostra só a metade positiva; o preto é onde o ruído é negativo e empurra os anéis para dentro.
- Repetir em anéis. O ruído, vezes Distorção, é somado ao raio antes que
fracto repita Anéis por unidade vezes. Cada anel é empurrado para dentro ou para fora pelo ruído embaixo dele, e anéis vizinhos se movem juntos, então ondulam sem se cruzar. Aumente bastante a Distorção (ou a Escala do ruído, que deixa o mesmo empurrão mais íngreme) e o empurrão se dobra sobre si mesmo: os anéis se fecham em ilhas. Algumas parecem nós da madeira; muitas parecem mármore. - Moldar cada ano.
fractsozinho é um dente de serra de 0 a 1. Elevá-lo a uma potência mantém quase todo o anel perto de 0 (o lenho inicial, claro, que cresce rápido na primavera) e sobe de repente no fim (o lenho tardio, denso, do fim do verão). Aífractrecomeça em 0: a borda escura de um ano encontra o começo pálido do seguinte numa linha dura, e é isso que deixa os anéis legíveis. Nitidez do lenho tardio é essa potência. - Fibras finas. O mesmo ruído com 40 células por unidade na direção transversal ao tronco e 1,5 ao longo dele desenha riscos finos paralelos ao veio, as células cortadas da madeira. Nas faces de topo elas parecem um pontilhado fraco.
- Colorir a madeira. A forma do anel mais as fibras, escaladas por Contraste das fibras e limitadas entre 0 e 1,
escolhem entre as duas cores com
mix. As duas cores chegam lineares, então a mistura entre elas é uniforme.
O ruído
gradientNoise é o ruído melhorado de Perlin em 3D. O hash do ponto da grade escolhe uma de doze direções de
gradiente, rumo às arestas de um cubo, como na revisão dele de 2002: mais barato que uma direção aleatória e sem
aglomerados. Cada um dos oito cantos do cubo contribui com o produto escalar do seu gradiente pelo deslocamento até o
ponto, e uma suavização quíntica os mistura. O hash é aritmética inteira (uma multiplicação por três primos grandes,
depois o finalizador do MurmurHash3), então dá os mesmos bits em qualquer GPU, enquanto o hash comum
fract(sin(x) * 43758.5) perde precisão e faz listras longe da origem.
Custo
Cerca de 680 operações aritméticas e nenhuma leitura de textura: cada chamada de ruído custa umas 320, oito hashes e
produtos escalares de canto mais a suavização e sete interpolações, e o card faz duas. É pesado perto de uma leitura de
textura, e é por isso que as alternativas acima importam. A borda do anel é uma descontinuidade, então ela serrilha
quando um anel fica mais estreito que alguns pixels; uma textura bakeada com mipmaps filtra isso de graça, e o
fundamento Derivadas em espaço de tela mostra como suavizá-la com fwidth.
Monte no GLSL
Os anéis são medidos a partir do eixo y do espaço em que v_position estiver. A malha da prévia não tem transformação, então ali o espaço de mundo e o de objeto coincidem; numa cena, passe em v_position a posição em espaço de objeto (o atributo do vértice antes da matriz de modelo), ou a madeira fica parada no mundo enquanto o objeto se move através dela.
O botão de copiar entrega um shader de fragmento WebGL2 completo. O seu shader de vértice preenche v_position (veja
a nota sobre o espaço) e v_normal, v_view e v_uv, que este card não lê mas que o main() gerado declara. Envie
os dois parâmetros de cor decodificados para linear, como dizem os comentários do cabeçalho; o main() codifica o
resultado para sRGB uma vez.
Tudo o que o efeito precisa está no arquivo: mix32, gradient, cornerDot e gradientNoise são funções comuns,
então dá para levar gradientNoise para outro shader do jeito que está. Ele precisa de GLSL ES 3.00 (WebGL2) por causa
dos inteiros sem sinal e das operações de bits; no WebGL1, troque o hash por um de ponto flutuante e mantenha as
coordenadas pequenas.
Para fazer mármore com o mesmo código, mantenha gradientNoise, some quatro ou cinco oitavas dele com frequências
dobrando e amplitudes caindo pela metade, some isso a p.x * frequency e desenhe os veios com
1.0 - abs(sin(value)) elevado a uma potência.
Monte no Godot
O VisualShader não tem nó de ruído gradiente. O caminho por nós amostra uma NoiseTexture3D (FastNoiseLite, tipo Perlin, Seamless ligado) com um nó Texture3D na posição achatada; ela devolve 0..1 em vez de -1..1, então subtraia 0,5 antes de multiplicar pela Distorção, e a resolução dela limita o quão finas as fibras podem ser. O shader exportado calcula o ruído sozinho e não precisa de textura.
Crie um MeshInstance3D, dê a ele um novo ShaderMaterial e cole o shader. Ele é unshaded, então a madeira mostra
exatamente as cores do card; para iluminá-la, tire unshaded da linha render_mode e a cor vira o albedo do material
sob as luzes da cena.
No VisualShader a cadeia é: um nó Input de VERTEX levado para espaço de mundo (ou mantido em espaço de modelo, como diz a armadilha), um Vector3 Decompose para descartar o y, um Vector2 Compose e um Length2D para o raio. O ruído é um nó Texture3D amostrando uma NoiseTexture3D na posição vezes um Vector3 Constant como (2,5, 0,5, 2,5), menos 0,5. Depois um MultiplyAdd (ruído × Distorção + raio), um Multiply por Anéis por unidade, um Fract, um Pow pela Nitidez do lenho tardio, um segundo Texture3D para as fibras e um Mix entre dois ColorParameters na porta Albedo.
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// Wood & marble → Wood rings, 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_density; // Rings per unit: 9.0 (2.0 to 24.0)uniform float p_distortion; // Distortion: 0.07 (0.0 to 0.3)uniform float p_noiseScale; // Noise scale: 2.5 (0.5 to 8.0)uniform float p_sharpness; // Latewood sharpness: 2.0 (1.0 to 10.0)uniform float p_fiberContrast; // Fiber contrast: 0.2 (0.0 to 0.6)uniform vec3 p_earlywood; // Earlywood: vec3(0.86, 0.66, 0.43), sRGB; upload it decoded to linearuniform vec3 p_latewood; // Latewood: vec3(0.4, 0.2, 0.09), 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));}// MurmurHash3's finalizer (public domain): integer math gives the same bits on every GPU, where a sin() hash drifts// and stripes at large coordinatesuint mix32(uint x) { x ^= x >> 16u; x *= 0x85ebca6bu; x ^= x >> 13u; x *= 0xc2b2ae35u; x ^= x >> 16u; return x;}// One of the twelve directions toward a cube's edges, as in Perlin's 2002 noise: cheaper than a random direction and// never clumped. The primes spread neighbouring cells apart before the finalizer mixes the bits.vec3 gradient(ivec3 cell) { uint h = mix32(uint(cell.x) * 73856093u ^ uint(cell.y) * 19349663u ^ uint(cell.z) * 83492791u) % 12u; float a = (h & 1u) == 0u ? 1.0 : -1.0; float b = (h & 2u) == 0u ? 1.0 : -1.0; uint zeroAxis = h >> 2u; return zeroAxis == 0u ? vec3(0.0, a, b) : zeroAxis == 1u ? vec3(a, 0.0, b) : vec3(a, b, 0.0);}float cornerDot(ivec3 cell, vec3 f, ivec3 corner) { return dot(gradient(cell + corner), f - vec3(corner));}// 3D gradient noise, about -1..1. Solid noise, so the grain runs through the object like a block carved from a log.float gradientNoise(vec3 p) { ivec3 cell = ivec3(floor(p)); vec3 f = fract(p); vec3 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0); float x00 = mix(cornerDot(cell, f, ivec3(0, 0, 0)), cornerDot(cell, f, ivec3(1, 0, 0)), u.x); float x10 = mix(cornerDot(cell, f, ivec3(0, 1, 0)), cornerDot(cell, f, ivec3(1, 1, 0)), u.x); float x01 = mix(cornerDot(cell, f, ivec3(0, 0, 1)), cornerDot(cell, f, ivec3(1, 0, 1)), u.x); float x11 = mix(cornerDot(cell, f, ivec3(0, 1, 1)), cornerDot(cell, f, ivec3(1, 1, 1)), u.x); return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);}vec4 effect(Inputs i) { // The trunk runs along y with its heart off the object's center, as in most cut timber vec3 p = i.position - vec3(0.3, 0.0, -0.2); float radius = length(p.xz); // Squashed along the trunk: rings wander slowly up a tree and quickly around it float warp = gradientNoise(p * vec3(p_noiseScale, p_noiseScale * 0.2, p_noiseScale)); float rings = fract((radius + warp * p_distortion) * p_density); // A year's wood grows light in spring and darkens toward late summer, then the next year starts light again: the // dark side of each ring ends in a sharp edge float tone = pow(rings, p_sharpness); // Fine streaks along the grain: high frequency across the trunk, very low along it float fibers = gradientNoise(p * vec3(40.0, 1.5, 40.0)); vec3 color = mix(p_earlywood, p_latewood, clamp(tone + fibers * p_fiberContrast, 0.0, 1.0)); 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);}// Wood & marble → Wood rings, Godot 4. From Creative Center, MIT license.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 density : hint_range(2.0, 24.0, 0.5) = 9.0;uniform float distortion : hint_range(0.0, 0.3, 0.005) = 0.07;uniform float noiseScale : hint_range(0.5, 8.0, 0.1) = 2.5;uniform float sharpness : hint_range(1.0, 10.0, 0.1) = 2.0;uniform float fiberContrast : hint_range(0.0, 0.6, 0.01) = 0.2;uniform vec3 earlywood : source_color = vec3(0.86, 0.66, 0.43);uniform vec3 latewood : source_color = vec3(0.4, 0.2, 0.09);struct Inputs { vec2 uv; vec3 position; vec3 normal; vec3 view; float time; vec2 fragCoord; vec2 resolution;};// MurmurHash3's finalizer (public domain): integer math gives the same bits on every GPU, where a sin() hash drifts// and stripes at large coordinatesuint mix32(uint x) { x ^= x >> 16u; x *= 0x85ebca6bu; x ^= x >> 13u; x *= 0xc2b2ae35u; x ^= x >> 16u; return x;}// One of the twelve directions toward a cube's edges, as in Perlin's 2002 noise: cheaper than a random direction and// never clumped. The primes spread neighbouring cells apart before the finalizer mixes the bits.vec3 gradient(ivec3 cell) { uint h = mix32(uint(cell.x) * 73856093u ^ uint(cell.y) * 19349663u ^ uint(cell.z) * 83492791u) % 12u; float a = (h & 1u) == 0u ? 1.0 : -1.0; float b = (h & 2u) == 0u ? 1.0 : -1.0; uint zeroAxis = h >> 2u; return zeroAxis == 0u ? vec3(0.0, a, b) : zeroAxis == 1u ? vec3(a, 0.0, b) : vec3(a, b, 0.0);}float cornerDot(ivec3 cell, vec3 f, ivec3 corner) { return dot(gradient(cell + corner), f - vec3(corner));}// 3D gradient noise, about -1..1. Solid noise, so the grain runs through the object like a block carved from a log.float gradientNoise(vec3 p) { ivec3 cell = ivec3(floor(p)); vec3 f = fract(p); vec3 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0); float x00 = mix(cornerDot(cell, f, ivec3(0, 0, 0)), cornerDot(cell, f, ivec3(1, 0, 0)), u.x); float x10 = mix(cornerDot(cell, f, ivec3(0, 1, 0)), cornerDot(cell, f, ivec3(1, 1, 0)), u.x); float x01 = mix(cornerDot(cell, f, ivec3(0, 0, 1)), cornerDot(cell, f, ivec3(1, 0, 1)), u.x); float x11 = mix(cornerDot(cell, f, ivec3(0, 1, 1)), cornerDot(cell, f, ivec3(1, 1, 1)), u.x); return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);}vec4 effect(Inputs i) { // The trunk runs along y with its heart off the object's center, as in most cut timber vec3 p = i.position - vec3(0.3, 0.0, -0.2); float radius = length(p.xz); // Squashed along the trunk: rings wander slowly up a tree and quickly around it float warp = gradientNoise(p * vec3(noiseScale, noiseScale * 0.2, noiseScale)); float rings = fract((radius + warp * distortion) * density); // A year's wood grows light in spring and darkens toward late summer, then the next year starts light again: the // dark side of each ring ends in a sharp edge float tone = pow(rings, sharpness); // Fine streaks along the grain: high frequency across the trunk, very low along it float fibers = gradientNoise(p * vec3(40.0, 1.5, 40.0)); vec3 color = mix(earlywood, latewood, clamp(tone + fibers * fiberContrast, 0.0, 1.0)); 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.
Leitura complementar
- Wood (abre em uma nova aba)Referência
- Improving Noise (abre em uma nova aba)Paper
- Perlin noise (abre em uma nova aba)Referência
- MurmurHash (abre em uma nova aba)Referência
- Hash Functions for GPU Rendering (abre em uma nova aba)Paper
- Wave Texture Node (abre em uma nova aba)Documentação
- NoiseTexture3D (abre em uma nova aba)Referência
- length (abre em uma nova aba)Referência
- fract (abre em uma nova aba)Referência
- pow (abre em uma nova aba)Referência
- mix (abre em uma nova aba)Referência