Attention: Here be dragons
This is the latest
(unstable) version of this documentation, which may document features
not available in or compatible with released stable versions of Godot.
Checking the stable version of the documentation...
Usando um SubViewport como textura
Introdução
Este tutorial apresentará o uso do SubViewport como uma textura que pode ser aplicada a objetos 3D. Para isso, ele guiará você pelo processo de criação de um planeta procedural como o abaixo:
Nota
Este tutorial não cobre como programar uma atmosfera dinâmica como a que este planeta possui.
Este tutorial pressupõe que você esteja familiarizado com a configuração de uma cena básica contendo: uma Camera3D, uma fonte de luz, um MeshInstance3D com uma Primitive Mesh e a aplicação de um StandardMaterial3D ao mesh. O foco será o uso do SubViewport para criar texturas dinamicamente que podem ser aplicadas ao mesh.
Neste tutorial, abordaremos os seguintes tópicos:
Como usar um SubViewport como uma textura de renderização
Mapeando uma textura para uma esfera com mapeamento equirretangular
Técnicas de fragment shader para planetas procedurais
Definindo um mapa de Rugosidade (Roughness) a partir de uma Viewport Texture
Configurando a cena
Crie uma nova cena e adicione os seguintes nós exatamente como mostrado abaixo.
Go into the MeshInstance3D and make the mesh a SphereMesh.
Configurando o SubViewport
Clique no nó SubViewport e defina seu tamanho para (1024, 512). O SubViewport pode, na verdade, ter qualquer tamanho, desde que a largura seja o dobro da altura. A largura precisa ser o dobro da altura para que a imagem seja mapeada com precisão na esfera, pois usaremos a projeção equirretangular, mas falaremos mais sobre isso adiante.
Em seguida, desative o 3D. Usaremos um ColorRect para renderizar a superfície, então também não precisamos de 3D.
Selecione o ColorRect e, no inspetor, defina o preset de âncoras para Full Rect. Isso garantirá que o ColorRect ocupe todo o SubViewport.
Em seguida, adicionamos um Shader Material ao ColorRect (ColorRect > CanvasItem > Material > Material > New ShaderMaterial).
Nota
Uma familiaridade básica com sombreamento (shading) é recomendada para este tutorial. No entanto, mesmo se você for novo em shaders, todo o código será fornecido, então você não deve ter problemas para acompanhar.
Clique no botão do menu suspenso para o material do shader e clique em / Editar. A partir daqui, vá em Shader > New Shader. Dê um nome e clique em "Criar". Clique no shader no inspetor para abrir o editor de shader. Delete o código padrão e adicione o seguinte:
shader_type canvas_item;
void fragment() {
COLOR = vec4(UV.x, UV.y, 0.5, 1.0);
}
Salve o código do shader, você verá no inspetor que o código acima renderiza um gradiente como o abaixo.
Agora temos o básico de um SubViewport no qual renderizamos e temos uma imagem única que podemos aplicar à esfera.
Aplicando a textura
Agora vá no MeshInstance3D e adicione um StandardMaterial3D a ele. Não há necessidade de um Shader Material especial (embora isso seja uma boa ideia para efeitos mais avançados, como a atmosfera no exemplo acima).
MeshInstance3D > GeometryInstance > Geometry > Material Override > New StandardMaterial3D
Depois clique no menu suspenso do StandardMaterial3D e clique em "Editar"
Vá para a seção "Resource" e marque a caixa Local to scene. Depois, vá para a seção "Albedo" e clique ao lado da propriedade "Texture" para adicionar uma Albedo Texture. Aqui aplicaremos a textura que criamos. Escolha "New ViewportTexture"
Clique na ViewportTexture que você acabou de criar no inspetor e clique em "Assign". Em seguida, no menu que aparece, selecione a Viewport na qual renderizamos anteriormente.
Sua esfera agora deve estar colorida com as cores que renderizamos na Viewport.
Notou a costura feia que se forma onde a textura envolve o objeto? Isso acontece porque estamos escolhendo uma cor com base nas coordenadas UV, e as coordenadas UV não dão a volta na textura. Este é um problema clássico em projeção de mapas 2D. Desenvolvedores de jogos frequentemente têm um mapa bidimensional que desejam projetar em uma esfera, mas quando ele se envolve, apresenta grandes costuras. Existe uma solução elegante para esse problema que ilustraremos na próxima seção.
Fazendo a textura do planeta
Então agora, quando renderizamos no nosso SubViewport, isso aparece magicamente na esfera. Mas há uma costura feia criada por nossas coordenadas de textura. Então, como obtemos um intervalo de coordenadas que envolva a esfera de uma maneira agradável? Uma solução é usar uma função que se repita no domínio da nossa textura. sin e cos são duas dessas funções. Vamos aplicá-las à textura e ver o que acontece. Substitua o código de cores existente no shader pelo seguinte:
COLOR.xyz = vec3(sin(UV.x * 3.14159 * 4.0) * cos(UV.y * 3.14159 * 4.0) * 0.5 + 0.5);
Nada mal. Se você olhar ao redor, poderá ver que a costura agora desapareceu, mas em seu lugar, temos um estrangulamento (pinching) nos polos. Esse estrangulamento ocorre devido à maneira como o Godot mapeia texturas para esferas em seu StandardMaterial3D. Ele usa uma técnica de projeção chamada projeção equirretangular, que traduz um mapa esférico em um plano 2D.
Nota
Se você estiver interessado em um pouco mais de informação sobre a técnica, faremos a conversão de coordenadas esféricas para coordenadas cartesianas. As coordenadas esféricas mapeiam a longitude e a latitude da esfera, enquanto as coordenadas cartesianas são, para todos os efeitos práticos, um vetor do centro da esfera até o ponto.
Para cada pixel, calcularemos sua posição 3D na esfera. A partir disso, usaremos um noise (ruído) 3D para determinar um valor de cor. Ao calcular o noise em 3D, resolvemos o problema do estrangulamento nos polos. Para entender o porquê, imagine o noise sendo calculado na superfície da esfera em vez do plano 2D. Quando você calcula através da superfície da esfera, você nunca atinge uma borda e, portanto, nunca cria uma costura ou um ponto de estrangulamento no polo. O código a seguir converte as UVs em coordenadas cartesianas.
float theta = UV.y * 3.14159;
float phi = UV.x * 3.14159 * 2.0;
vec3 unit = vec3(0.0, 0.0, 0.0);
unit.x = sin(phi) * sin(theta);
unit.y = cos(theta) * -1.0;
unit.z = cos(phi) * sin(theta);
unit = normalize(unit);
E se usarmos unit como um valor de saída para COLOR, obteremos:
Agora que podemos calcular a posição 3D da superfície da esfera, podemos usar o noise 3D para fazer o planeta. Usaremos esta função de noise diretamente de um Shadertoy:
vec3 hash(vec3 p) {
p = vec3(dot(p, vec3(127.1, 311.7, 74.7)),
dot(p, vec3(269.5, 183.3, 246.1)),
dot(p, vec3(113.5, 271.9, 124.6)));
return -1.0 + 2.0 * fract(sin(p) * 43758.5453123);
}
float noise(vec3 p) {
vec3 i = floor(p);
vec3 f = fract(p);
vec3 u = f * f * (3.0 - 2.0 * f);
return mix(mix(mix(dot(hash(i + vec3(0.0, 0.0, 0.0)), f - vec3(0.0, 0.0, 0.0)),
dot(hash(i + vec3(1.0, 0.0, 0.0)), f - vec3(1.0, 0.0, 0.0)), u.x),
mix(dot(hash(i + vec3(0.0, 1.0, 0.0)), f - vec3(0.0, 1.0, 0.0)),
dot(hash(i + vec3(1.0, 1.0, 0.0)), f - vec3(1.0, 1.0, 0.0)), u.x), u.y),
mix(mix(dot(hash(i + vec3(0.0, 0.0, 1.0)), f - vec3(0.0, 0.0, 1.0)),
dot(hash(i + vec3(1.0, 0.0, 1.0)), f - vec3(1.0, 0.0, 1.0)), u.x),
mix(dot(hash(i + vec3(0.0, 1.0, 1.0)), f - vec3(0.0, 1.0, 1.0)),
dot(hash(i + vec3(1.0, 1.0, 1.0)), f - vec3(1.0, 1.0, 1.0)), u.x), u.y), u.z );
}
Nota
All credit goes to the author, Inigo Quilez. It is published under the MIT license.
Agora, para usar o noise, adicione o seguinte à função fragment:
float n = noise(unit * 5.0);
COLOR.xyz = vec3(n * 0.5 + 0.5);
Nota
Para destacar a textura, definimos o material como unshaded (sem sombreamento).
Você pode ver agora que o noise de fato envolve a esfera perfeitamente. Embora isso não se pareça em nada com o planeta que lhe foi prometido. Então vamos passar para algo mais colorido.
Colorindo o planeta
Agora, para fazer as cores do planeta. Embora existam muitas maneiras de fazer isso, por enquanto, vamos manter um gradiente entre água e terra.
Para fazer um gradiente em GLSL, usamos a função mix. A função mix recebe dois valores para interpolar e um terceiro argumento para escolher o quanto interpolar entre eles; essencialmente, ela mistura os dois valores. Em outras APIs, essa função costuma ser chamada de lerp. No entanto, lerp geralmente é reservado para misturar dois floats; mix pode receber quaisquer valores, sejam eles floats ou tipos vetoriais.
COLOR.xyz = mix(vec3(0.05, 0.3, 0.5), vec3(0.9, 0.4, 0.1), n * 0.5 + 0.5);
A primeira cor é azul para o oceano. A segunda cor é um tipo de cor avermelhada (porque todo planeta alienígena precisa de um terreno vermelho). E finalmente, eles são misturados por n * 0.5 + 0.5. n varia suavemente entre -1 e 1. Então, mapeamos isso para o intervalo de 0-1 que a função mix espera. Agora você pode ver que as cores mudam entre azul e vermelho.
Isso está um pouco mais borrado do que queremos. Os planetas costumam ter uma separação relativamente clara entre a terra e o mar. Para fazer isso, mudaremos o último termo para smoothstep(-0.1, 0.0, n). E assim, a linha inteira se torna:
COLOR.xyz = mix(vec3(0.05, 0.3, 0.5), vec3(0.9, 0.4, 0.1), smoothstep(-0.1, 0.0, n));
O que o smoothstep faz é retornar 0 se o terceiro argumento estiver abaixo do primeiro e 1 se o terceiro argumento for maior que o segundo, e mistura suavemente entre 0 e 1 se o terceiro número estiver entre o primeiro e o segundo. Portanto, nesta linha, o smoothstep retorna 0 sempre que n for menor que -0.1 e retorna 1 sempre que n estiver acima de 0.
Mais uma coisa para tornar isso um pouco mais parecido com um planeta. A terra não deveria ser tão uniforme; vamos deixar as bordas um pouco mais ásperas. Um truque frequentemente usado em shaders para fazer terrenos de aparência áspera com ruído é sobrepor camadas de ruído umas sobre as outras em várias frequências. Usamos uma camada para fazer a estrutura geral dos continentes. Depois, outra camada quebra um pouco as bordas, e depois outra, e assim por diante. O que faremos é calcular n com quatro linhas de código de shader em vez de apenas uma. n se torna:
float n = noise(unit * 5.0) * 0.5;
n += noise(unit * 10.0) * 0.25;
n += noise(unit * 20.0) * 0.125;
n += noise(unit * 40.0) * 0.0625;
E agora o planeta se parece com:
Fazendo um oceano
Uma última coisa para fazer isso parecer mais com um planeta. O oceano e a terra refletem a luz de maneira diferente. Portanto, queremos que o oceano brilhe um pouco mais do que a terra. Podemos fazer isso passando um quarto valor para o canal alpha da nossa saída COLOR e usando-o como um mapa de Rugosidade (Roughness map).
COLOR.a = 0.3 + 0.7 * smoothstep(-0.1, 0.0, n);
Esta linha retorna 0.3 para a água e 1.0 para a terra. Isso significa que a terra será bastante rugosa, enquanto a água será bastante lisa.
E então, no material, sob a seção "Metallic", certifique-se de que Metallic está definido como 0 e Specular está definido como 1. A razão para isso é que a água reflete muito bem a luz, mas não é metálica. Esses valores não são fisicamente precisos, mas são bons o suficiente para esta demonstração.
Em seguida, sob a seção "Roughness", defina a textura de rugosidade para uma Viewport Texture apontando para o nosso SubViewport da textura do planeta. Finalmente, defina o Texture Channel para Alpha. Isso instrui o renderizador a usar o canal alpha da nossa saída COLOR como o valor de Roughness.
Você notará que muito pouco muda, exceto que o planeta não está mais refletindo o céu. Isso está acontecendo porque, por padrão, quando algo é renderizado com um valor alfa, é desenhado como um objeto transparente sobre o fundo. E como o fundo padrão do SubViewport é opaco, o canal alpha da Viewport Texture é 1, resultando na textura do planeta sendo desenhada com cores ligeiramente mais fracas e um valor de Roughness de 1 em todos os lugares. Para corrigir isso, entramos no SubViewport e ativamos a propriedade "Transparent Bg". Como agora estamos renderizando um objeto transparente em cima de outro, queremos ativar blend_premul_alpha:
render_mode blend_premul_alpha;
Isso pré-multiplica as cores pelo valor alpha e depois as mistura corretamente. Normalmente, ao misturar uma cor transparente em cima de outra, mesmo que o fundo tenha um alpha de 0 (como neste caso), você acaba tendo problemas estranhos de vazamento de cor. Definir blend_premul_alpha corrige isso.
Agora o planeta deve parecer que está refletindo a luz no oceano, mas não na terra. Mova o OmniLight3D na cena para que você possa ver o efeito das reflexões no oceano.
E aí está. A procedural planet generated using a SubViewport.