Construindo Shaders com Babylon.js
Publicados: 2022-03-10Shaders são um conceito chave se você quiser liberar o poder bruto de sua GPU. Vou ajudá-lo a entender como eles funcionam e até mesmo experimentar seu poder interior de uma maneira fácil, graças ao Babylon.js.
Como funciona?
Antes de experimentar, devemos ver como as coisas funcionam internamente.
Ao lidar com 3D acelerado por hardware, você terá que lidar com duas CPUs: a CPU principal e a GPU. A GPU é uma espécie de CPU extremamente especializada.
Leitura adicional no SmashingMag:
- Construindo um jogo WebGL multiplataforma com Babylon.js
- Usando a API do Gamepad em jogos da web
- Introdução à modelagem poligonal e Three.js
- Como criar uma máquina de bateria de 8 bits responsiva
A GPU é uma máquina de estado que você configura usando a CPU. Por exemplo, a CPU configurará a GPU para renderizar linhas em vez de triângulos; definirá se a transparência está ativada; e assim por diante.
Uma vez que todos os estados estejam definidos, a CPU pode definir o que renderizar: a geometria.
A geometria é composta por:
- uma lista de pontos que são chamados de vértices e armazenados em uma matriz chamada buffer de vértices,
- uma lista de índices que definem as faces (ou triângulos) armazenadas em uma matriz denominada buffer de índice.
O passo final para a CPU é definir como renderizar a geometria; para esta tarefa, a CPU definirá shaders na GPU. Shaders são pedaços de código que a GPU executará para cada um dos vértices e pixels que ela precisa renderizar. (Um vértice – ou vértices quando há vários deles – é um “ponto” em 3D).
Existem dois tipos de sombreadores: sombreadores de vértice e sombreadores de pixel (ou fragmento).
Pipeline Gráfico
Antes de mergulhar nos shaders, vamos voltar atrás. Para renderizar pixels, a GPU pegará a geometria definida pela CPU e fará o seguinte:
- Usando o buffer de índice, três vértices são reunidos para definir um triângulo.
- O buffer de índice contém uma lista de índices de vértice. Isso significa que cada entrada no buffer de índice é o número de um vértice no buffer de vértice.
- Isso é realmente útil para evitar a duplicação de vértices.
Por exemplo, o seguinte buffer de índice é uma lista de duas faces: [1 2 3 1 3 4]. A primeira face contém vértice 1, vértice 2 e vértice 3. A segunda face contém vértice 1, vértice 3 e vértice 4. Portanto, existem quatro vértices nesta geometria:
O sombreador de vértice é aplicado a cada vértice do triângulo. O objetivo principal do vertex shader é produzir um pixel para cada vértice (a projeção na tela 2D do vértice 3D):
Usando esses três pixels (que definem um triângulo 2D na tela), a GPU interpolará todos os valores anexados ao pixel (pelo menos suas posições), e o pixel shader será aplicado a cada pixel incluído no triângulo 2D para gerar uma cor para cada pixel:
Este processo é feito para cada face definida pelo buffer de índice.
Obviamente, devido à sua natureza paralela, a GPU é capaz de processar essa etapa para muitas faces simultaneamente e obter um desempenho muito bom.
GLSL
Acabamos de ver que para renderizar triângulos, a GPU precisa de dois shaders: o vertex shader e o pixel shader. Esses sombreadores são escritos em uma linguagem chamada Graphics Library Shader Language (GLSL). Parece c.
Aqui está uma amostra de um sombreador de vértice comum:
precision highp float; // Attributes attribute vec3 position; attribute vec2 uv; // Uniforms uniform mat4 worldViewProjection; // Varying varying vec2 vUV; void main(void) { gl_Position = worldViewProjection * vec4(position, 1.0); vUV = uv; }
Estrutura do sombreador de vértice
Um sombreador de vértice contém o seguinte:
- Atributos . Um atributo define uma parte de um vértice. Por padrão, um vértice deve conter pelo menos uma posição (um
vector3:x, y, z
). No entanto, como desenvolvedor, você pode decidir adicionar mais informações. Por exemplo, no antigo shader, existe umvector2
chamadouv
(ou seja, coordenadas de textura que permitem aplicar uma textura 2D a um objeto 3D). - Uniformes . Um uniforme é uma variável usada pelo shader e definida pela CPU. O único uniforme que temos aqui é uma matriz usada para projetar a posição do vértice (x, y, z) na tela (x, y).
- Variando . Variáveis variáveis são valores criados pelo sombreador de vértice e transmitidos ao sombreador de pixel. Aqui, o vertex shader transmitirá um
vUV
(uma cópia simples deuv
) para o pixel shader. Isso significa que um pixel é definido aqui com uma posição e coordenadas de textura. Esses valores serão interpolados pela GPU e usados pelo sombreador de pixel. - Principal . A função chamada
main
é o código executado pela GPU para cada vértice e deve, no mínimo, produzir um valor paragl_position
(a posição do vértice atual na tela).
Podemos ver em nosso exemplo que o sombreador de vértices é bem simples. Ele gera uma variável de sistema (começando com gl_
) chamada gl_position
para definir a posição do pixel associado e define uma variável variável chamada vUV
.
O vodu por trás das matrizes
A coisa sobre nosso sombreador é que temos uma matriz chamada worldViewProjection
e usamos essa matriz para projetar a posição do vértice para a variável gl_position
. Isso é legal, mas como obtemos o valor dessa matriz? É um uniforme, então temos que defini-lo no lado da CPU (usando JavaScript).
Esta é uma das partes complexas de fazer 3D. Você deve entender matemática complexa (ou terá que usar um motor 3D como Babylon.js, que veremos mais tarde).
A matriz worldViewProjection
é a combinação de três matrizes diferentes:
O uso da matriz resultante nos permite transformar vértices 3D em pixels 2D, levando em consideração o ponto de vista e tudo relacionado à posição, escala e rotação do objeto atual.
Esta é sua responsabilidade como desenvolvedor 3D: criar e manter essa matriz atualizada.
De volta aos Shaders
Uma vez que o vertex shader é executado em cada vértice (três vezes, então), teremos três pixels com o gl_position
correto e um valor vUV
. A GPU vai interpolar esses valores em cada pixel contido no triângulo produzido com esses pixels.
Então, para cada pixel, ele executará o pixel shader:
precision highp float; varying vec2 vUV; uniform sampler2D textureSampler; void main(void) { gl_FragColor = texture2D(textureSampler, vUV); }
Estrutura de sombreador de pixel (ou fragmento)
A estrutura de um sombreador de pixel é semelhante à de um sombreador de vértice:
- Variando . Variáveis variáveis são valores criados pelo sombreador de vértice e transmitidos ao sombreador de pixel. Aqui, o sombreador de pixel receberá um valor
vUV
do sombreador de vértice. - Uniformes . Um uniforme é uma variável usada pelo shader e definida pela CPU. O único uniforme que temos aqui é um sampler, que é uma ferramenta usada para ler as cores das texturas.
- Principal . A função chamada
main
é o código executado pela GPU para cada pixel e que deve produzir pelo menos um valor paragl_FragColor
(ou seja, a cor do pixel atual).
Este pixel shader é bastante simples: ele lê a cor da textura usando as coordenadas de textura do vertex shader (que, por sua vez, a obtém do vértice).
O problema é que quando os shaders são desenvolvidos, você está apenas na metade do caminho, porque você tem que lidar com muito código WebGL. De fato, o WebGL é realmente poderoso, mas também de baixo nível, e você tem que fazer tudo sozinho, desde criar os buffers até definir estruturas de vértices. Você também precisa fazer toda a matemática, definir todos os estados, lidar com o carregamento de textura e assim por diante.
Demasiado difícil? BABYLON.ShaderMaterial para o resgate
Eu sei o que você está pensando: “Shaders são muito legais, mas eu não quero me incomodar com o encanamento interno do WebGL ou mesmo com a matemática.”
E você está certo! Esta é uma pergunta perfeitamente legítima, e é exatamente por isso que criei o Babylon.js!
Para usar Babylon.js, primeiro você precisa de uma página web simples:
<!DOCTYPE html> <html> <head> <title>Babylon.js</title> <script src="Babylon.js"></script> <script type="application/vertexShader"> precision highp float; // Attributes attribute vec3 position; attribute vec2 uv; // Uniforms uniform mat4 worldViewProjection; // Normal varying vec2 vUV; void main(void) { gl_Position = worldViewProjection * vec4(position, 1.0); vUV = uv; } </script> <script type="application/fragmentShader"> precision highp float; varying vec2 vUV; uniform sampler2D textureSampler; void main(void) { gl_FragColor = texture2D(textureSampler, vUV); } </script> <script src="index.js"></script> <style> html, body { width: 100%; height: 100%; padding: 0; margin: 0; overflow: hidden; margin: 0px; overflow: hidden; } #renderCanvas { width: 100%; height: 100%; touch-action: none; -ms-touch-action: none; } </style> </head> <body> <canvas></canvas> </body> </html>
Você notará que os shaders são definidos por tags <script>
. Com Babylon.js, você também pode defini-los em arquivos separados (arquivos .fx
).
- fonte Babylon.js
- Repositório do GitHub
Finalmente, o código JavaScript principal é este:
"use strict"; document.addEventListener("DOMContentLoaded", startGame, false); function startGame() { if (BABYLON.Engine.isSupported()) { var canvas = document.getElementById("renderCanvas"); var engine = new BABYLON.Engine(canvas, false); var scene = new BABYLON.Scene(engine); var camera = new BABYLON.ArcRotateCamera("Camera", 0, Math.PI / 2, 10, BABYLON.Vector3.Zero(), scene); camera.attachControl(canvas); // Creating sphere var sphere = BABYLON.Mesh.CreateSphere("Sphere", 16, 5, scene); var amigaMaterial = new BABYLON.ShaderMaterial("amiga", scene, { vertexElement: "vertexShaderCode", fragmentElement: "fragmentShaderCode", }, { attributes: ["position", "uv"], uniforms: ["worldViewProjection"] }); amigaMaterial.setTexture("textureSampler", new BABYLON.Texture("amiga.jpg", scene)); sphere.material = amigaMaterial; engine.runRenderLoop(function () { sphere.rotation.y += 0.05; scene.render(); }); } };
Você pode ver que eu uso BABYLON.ShaderMaterial
para me livrar do fardo de compilar, vincular e manipular shaders.
Ao criar BABYLON.ShaderMaterial
, você deve especificar o elemento DOM usado para armazenar os sombreadores ou o nome base dos arquivos onde os sombreadores estão. Se você optar por usar arquivos, deverá criar um arquivo para cada sombreador e usar o seguinte padrão: basename.vertex.fx
e basename.fragment.fx
. Então, você terá que criar o material assim:
var cloudMaterial = new BABYLON.ShaderMaterial("cloud", scene, "./myShader", { attributes: ["position", "uv"], uniforms: ["worldViewProjection"] });
Você também deve especificar os nomes dos atributos e uniformes que usa.
Em seguida, você pode definir diretamente os valores de seus uniformes e amostradores usando setTexture
, setFloat
, setFloats
, setColor3
, setColor4
, setVector2
, setVector3
, setVector4
, setMatrix
.
Bem simples, certo?
E você se lembra da matriz worldViewProjection
anterior, usando Babylon.js e BABYLON.ShaderMaterial
. Você simplesmente não precisa se preocupar com isso! BABYLON.ShaderMaterial
irá computá-lo automaticamente para você porque você irá declará-lo na lista de uniformes.
BABYLON.ShaderMaterial
também pode manipular as seguintes matrizes para você:
-
world
, -
view
, -
projection
, -
worldView
de mundo, -
worldViewProjection
.
Não há mais necessidade de matemática. Por exemplo, cada vez que você executar sphere.rotation.y += 0.05
, a matriz world
da esfera será gerada para você e transmitida para a GPU.
Veja você mesmo o resultado ao vivo.
Crie seu próprio sombreador (CYOS)
Agora, vamos aumentar e criar uma página onde você pode criar dinamicamente seus próprios shaders e ver o resultado imediatamente. Esta página usará o mesmo código que discutimos anteriormente e usará o objeto BABYLON.ShaderMaterial
para compilar e executar os shaders que você criará.
Eu usei o editor de código ACE para Create Your Own Shader (CYOS). É um editor de código incrível, com realce de sintaxe. Sinta-se à vontade para dar uma olhada.
Usando a primeira caixa de combinação, você poderá selecionar shaders predefinidos. Veremos cada um deles logo em seguida.
Você também pode alterar a malha (ou seja, o objeto 3D) usada para visualizar seus sombreadores usando a segunda caixa de combinação.
O botão de compilação é usado para criar um novo BABYLON.ShaderMaterial
de seus shaders. O código usado por este botão é o seguinte:
// Compile shaderMaterial = new BABYLON.ShaderMaterial("shader", scene, { vertexElement: "vertexShaderCode", fragmentElement: "fragmentShaderCode", }, { attributes: ["position", "normal", "uv"], uniforms: ["world", "worldView", "worldViewProjection"] }); var refTexture = new BABYLON.Texture("ref.jpg", scene); refTexture.wrapU = BABYLON.Texture.CLAMP_ADDRESSMODE; refTexture.wrapV = BABYLON.Texture.CLAMP_ADDRESSMODE; var amigaTexture = new BABYLON.Texture("amiga.jpg", scene); shaderMaterial.setTexture("textureSampler", amigaTexture); shaderMaterial.setTexture("refSampler", refTexture); shaderMaterial.setFloat("time", 0); shaderMaterial.setVector3("cameraPosition", BABYLON.Vector3.Zero()); shaderMaterial.backFaceCulling = false; mesh.material = shaderMaterial;
Incrivelmente simples, certo? O material está pronto para lhe enviar três matrizes pré-computadas ( world
, worldView
e worldViewProjection
). Os vértices virão com coordenadas de posição, normal e textura. Duas texturas também já estão carregadas para você:
Por fim, o renderLoop
é onde atualizo dois uniformes convenientes:
- Um é chamado de
time
e recebe algumas animações engraçadas. - O outro é chamado
cameraPosition
, que obtém a posição da câmera em seus shaders (útil para equações de iluminação).
engine.runRenderLoop(function () { mesh.rotation.y += 0.001; if (shaderMaterial) { shaderMaterial.setFloat("time", time); time += 0.02; shaderMaterial.setVector3("cameraPosition", camera.position); } scene.render(); });
Sombreador básico
Vamos começar com o primeiro shader definido no CYOS: o shader básico.
Já conhecemos esse shader. Ele calcula a gl_position
e usa coordenadas de textura para buscar uma cor para cada pixel.
Para calcular a posição do pixel, precisamos apenas da matriz worldViewProjection
e da posição do vértice:
precision highp float; // Attributes attribute vec3 position; attribute vec2 uv; // Uniforms uniform mat4 worldViewProjection; // Varying varying vec2 vUV; void main(void) { gl_Position = worldViewProjection * vec4(position, 1.0); vUV = uv; }
As coordenadas de textura ( uv
) são transmitidas sem modificações para o pixel shader.
Observe que precisamos adicionar precision mediump float
na primeira linha para os sombreadores de vértice e pixel porque o Chrome exige isso. Ele especifica que, para melhor desempenho, não usamos valores flutuantes de precisão total.
O pixel shader é ainda mais simples, pois só precisamos usar coordenadas de textura e buscar uma cor de textura:
precision highp float; varying vec2 vUV; uniform sampler2D textureSampler; void main(void) { gl_FragColor = texture2D(textureSampler, vUV); }
Anteriormente vimos que o uniforme textureSampler
é preenchido com a textura amiga
. Então, o resultado é o seguinte:
Sombreador preto e branco
Vamos continuar com um novo shader: o shader preto e branco. O objetivo deste shader é usar o anterior, mas com um modo de renderização somente em preto e branco.
Para isso, podemos manter o mesmo sombreador de vértices. O pixel shader será ligeiramente modificado.
A primeira opção que temos é pegar apenas um componente, como o verde:
precision highp float; varying vec2 vUV; uniform sampler2D textureSampler; void main(void) { gl_FragColor = vec4(texture2D(textureSampler, vUV).ggg, 1.0); }
Como você pode ver, em vez de usar .rgb
(esta operação é chamada de swizzle), usamos .ggg
.
Mas se quisermos um efeito preto e branco realmente preciso, calcular a luminância (que leva em consideração todos os componentes) seria melhor:
precision highp float; varying vec2 vUV; uniform sampler2D textureSampler; void main(void) { float luminance = dot(texture2D(textureSampler, vUV).rgb, vec3(0.3, 0.59, 0.11)); gl_FragColor = vec4(luminance, luminance, luminance, 1.0); }
A operação de dot
(ou produto dot
) é calculada assim: result = v0.x * v1.x + v0.y * v1.y + v0.z * v1.z
.
Então, no nosso caso, luminance = r * 0.3 + g * 0.59 + b * 0.11
. (Esses valores são baseados no fato de que o olho humano é mais sensível ao verde.)
Parece legal, não é?
Sombreador de célula
Vamos passar para um sombreador mais complexo: o sombreador de sombreamento de célula.
Este exigirá que obtenhamos o normal do vértice e a posição do vértice no pixel shader. Então, o vertex shader ficará assim:
precision highp float; // Attributes attribute vec3 position; attribute vec3 normal; attribute vec2 uv; // Uniforms uniform mat4 world; uniform mat4 worldViewProjection; // Varying varying vec3 vPositionW; varying vec3 vNormalW; varying vec2 vUV; void main(void) { vec4 outPosition = worldViewProjection * vec4(position, 1.0); gl_Position = outPosition; vPositionW = vec3(world * vec4(position, 1.0)); vNormalW = normalize(vec3(world * vec4(normal, 0.0))); vUV = uv; }
Observe que também usamos a matriz mundial porque a posição e a normal são armazenadas sem nenhuma transformação, e devemos aplicar a matriz mundial para levar em consideração a rotação do objeto.
O pixel shader é o seguinte:
precision highp float; // Lights varying vec3 vPositionW; varying vec3 vNormalW; varying vec2 vUV; // Refs uniform sampler2D textureSampler; void main(void) { float ToonThresholds[4]; ToonThresholds[0] = 0.95; ToonThresholds[1] = 0.5; ToonThresholds[2] = 0.2; ToonThresholds[3] = 0.03; float ToonBrightnessLevels[5]; ToonBrightnessLevels[0] = 1.0; ToonBrightnessLevels[1] = 0.8; ToonBrightnessLevels[2] = 0.6; ToonBrightnessLevels[3] = 0.35; ToonBrightnessLevels[4] = 0.2; vec3 vLightPosition = vec3(0, 20, 10); // Light vec3 lightVectorW = normalize(vLightPosition - vPositionW); // diffuse float ndl = max(0., dot(vNormalW, lightVectorW)); vec3 color = texture2D(textureSampler, vUV).rgb; if (ndl > ToonThresholds[0]) { color *= ToonBrightnessLevels[0]; } else if (ndl > ToonThresholds[1]) { color *= ToonBrightnessLevels[1]; } else if (ndl > ToonThresholds[2]) { color *= ToonBrightnessLevels[2]; } else if (ndl > ToonThresholds[3]) { color *= ToonBrightnessLevels[3]; } else { color *= ToonBrightnessLevels[4]; } gl_FragColor = vec4(color, 1.); }
O objetivo deste sombreador é simular a luz e, em vez de calcular o sombreamento suave, aplicaremos a luz de acordo com limites de brilho específicos. Por exemplo, se a intensidade da luz estiver entre 1 (máximo) e 0,95, a cor do objeto (obtida da textura) será aplicada diretamente. Se a intensidade estiver entre 0,95 e 0,5, a cor seria atenuada por um fator de 0,8. E assim por diante.
Existem basicamente quatro etapas neste shader.
Primeiro, declaramos limites e constantes de níveis.
Em seguida, calculamos a iluminação usando a equação de Phong (consideraremos que a luz não está se movendo):
vec3 vLightPosition = vec3(0, 20, 10); // Light vec3 lightVectorW = normalize(vLightPosition - vPositionW); // diffuse float ndl = max(0., dot(vNormalW, lightVectorW));
A intensidade da luz por pixel depende do ângulo entre a direção normal e a direção da luz.
Então, obtemos a cor da textura para o pixel.
Por fim, verificamos o limite e aplicamos o nível à cor.
O resultado se parece com um objeto de desenho animado:
Phong Shader
Usamos uma parte da equação de Phong no shader anterior. Vamos usá-lo completamente agora.
O vertex shader é claramente simples aqui porque tudo será feito no pixel shader:
precision highp float; // Attributes attribute vec3 position; attribute vec3 normal; attribute vec2 uv; // Uniforms uniform mat4 worldViewProjection; // Varying varying vec3 vPosition; varying vec3 vNormal; varying vec2 vUV; void main(void) { vec4 outPosition = worldViewProjection * vec4(position, 1.0); gl_Position = outPosition; vUV = uv; vPosition = position; vNormal = normal; }
De acordo com a equação, devemos calcular as partes “difusas” e “especulares” usando a direção da luz e a normal do vértice:
precision highp float; // Varying varying vec3 vPosition; varying vec3 vNormal; varying vec2 vUV; // Uniforms uniform mat4 world; // Refs uniform vec3 cameraPosition; uniform sampler2D textureSampler; void main(void) { vec3 vLightPosition = vec3(0, 20, 10); // World values vec3 vPositionW = vec3(world * vec4(vPosition, 1.0)); vec3 vNormalW = normalize(vec3(world * vec4(vNormal, 0.0))); vec3 viewDirectionW = normalize(cameraPosition - vPositionW); // Light vec3 lightVectorW = normalize(vLightPosition - vPositionW); vec3 color = texture2D(textureSampler, vUV).rgb; // diffuse float ndl = max(0., dot(vNormalW, lightVectorW)); // Specular vec3 angleW = normalize(viewDirectionW + lightVectorW); float specComp = max(0., dot(vNormalW, angleW)); specComp = pow(specComp, max(1., 64.)) * 2.; gl_FragColor = vec4(color * ndl + vec3(specComp), 1.); }
Já usamos a parte difusa no shader anterior, então aqui só precisamos adicionar a parte especular. Você pode encontrar mais informações sobre o sombreamento Phong na Wikipedia.
O resultado da nossa esfera:
Descartar Shader
Para o shader de descarte, gostaria de apresentar um novo conceito: a palavra-chave discard
.
Este shader descarta todos os pixels não vermelhos e cria a ilusão de um objeto escavado.
O vertex shader é o mesmo usado pelo shader básico:
precision highp float; // Attributes attribute vec3 position; attribute vec3 normal; attribute vec2 uv; // Uniforms uniform mat4 worldViewProjection; // Varying varying vec2 vUV; void main(void) { gl_Position = worldViewProjection * vec4(position, 1.0); vUV = uv; }
O pixel shader do seu lado terá que testar a cor e usar o descarte quando, por exemplo, o componente verde estiver muito alto:
precision highp float; varying vec2 vUV; // Refs uniform sampler2D textureSampler; void main(void) { vec3 color = texture2D(textureSampler, vUV).rgb; if (color.g > 0.5) { discard; } gl_FragColor = vec4(color, 1.); }
O resultado é um pouco engraçado:
Sombreador de onda
Brincamos muito com pixel shader, mas também quero que saibam que podemos fazer muitas coisas com vertex shaders.
Para o sombreador de onda, reutilizaremos o sombreador de pixel Phong.
O vertex shader usará o uniforme chamado time
para obter alguns valores animados. Usando este uniforme, o shader irá gerar uma onda com as posições dos vértices:
precision highp float; // Attributes attribute vec3 position; attribute vec3 normal; attribute vec2 uv; // Uniforms uniform mat4 worldViewProjection; uniform float time; // Varying varying vec3 vPosition; varying vec3 vNormal; varying vec2 vUV; void main(void) { vec3 v = position; vx += sin(2.0 * position.y + (time)) * 0.5; gl_Position = worldViewProjection * vec4(v, 1.0); vPosition = position; vNormal = normal; vUV = uv; }
Um sinus é aplicado a position.y
e o resultado é o seguinte:
Mapeamento de ambiente esférico
Este foi amplamente inspirado pelo artigo “Criando um Shader de Reflexão Esférica/Mapeamento de Ambiente”. Vou deixar você ler esse excelente artigo e brincar com o shader associado.
Sombreador Fresnel
Gostaria de concluir este artigo com o meu favorito: o shader Fresnel.
Este sombreador é usado para aplicar uma intensidade diferente de acordo com o ângulo entre a direção da vista e a normal do vértice.
O vertex shader é o mesmo usado pelo cell-shading shader, e podemos facilmente calcular o termo Fresnel em nosso pixel shader (pois temos a posição normal e da câmera, que pode ser usada para avaliar a direção da visão):
precision highp float; // Lights varying vec3 vPositionW; varying vec3 vNormalW; // Refs uniform vec3 cameraPosition; uniform sampler2D textureSampler; void main(void) { vec3 color = vec3(1., 1., 1.); vec3 viewDirectionW = normalize(cameraPosition - vPositionW); // Fresnel float fresnelTerm = dot(viewDirectionW, vNormalW); fresnelTerm = clamp(1.0 - fresnelTerm, 0., 1.); gl_FragColor = vec4(color * fresnelTerm, 1.); }
Seu Shader?
Agora você está mais preparado para criar seu próprio shader. Sinta-se à vontade para postar no fórum Babylon.js para compartilhar seus experimentos!
Se você quiser ir mais longe, aqui estão alguns links úteis:
- Babylon.js, site oficial
- Babylon.js, repositório GitHub
- Fórum Babylon.js, desenvolvedores de jogos HTML5
- Crie seu próprio sombreador (CYOS), Babylon.js
- Linguagem de sombreamento OpenGL,” Wikipedia
- Linguagem de sombreamento OpenGL, documentação