Ao renderizar um objeto 3D, você sempre precisa adicionar algum material para que fique visível e com a aparência desejada; não importa se você faz isso em programas especiais ou em tempo real via WebGL.
A maioria dos materiais pode ser simulada usando as ferramentas internas de bibliotecas como Three.js., mas neste tutorial mostrarei como fazer objetos parecerem com vidro em três etapas usando - você adivinhou - Three.js.
Etapa 1: Configuração e reflexões frontais
Neste exemplo, usarei a geometria do diamante, mas você pode usar um cubo simples ou qualquer outra forma.
Vamos montar nosso projeto. Precisamos de um visualizador, uma cena, uma câmera e geometria. Para visualizar nossa superfície, precisamos de material. A criação deste material será o principal objetivo da lição. Então, vamos criar um novo objeto SharedMaterial com sombreadores de vértice e fragmento.
Ao contrário de suas expectativas, nosso material não será transparente; na verdade, distorceremos o que estará por trás do diamante. Para fazer isso, precisaremos visualizar a cena (sem diamante) na textura. Acabo de renderizar um plano do tamanho de todo o escopo usando uma câmera ortogonal, mas você também pode renderizar uma cena com outros objetos. A maneira mais fácil de separar a superfície do plano de fundo do diamante no Three.js é usar Camadas.
this.orthoCamera = new THREE.OrthographicCamera( width / - 2,width / 2, height / 2, height / - 2, 1, 1000 ); // 1 (0 ) this.orthoCamera.layers.set(1); const tex = await loadTexture('texture.jpg'); this.quad = new THREE.Mesh(new THREE.PlaneBufferGeometry(), new THREE.MeshBasicMaterial({map: tex})); this.quad.scale.set(width, height, 1); // 1 this.quad.layers.set(1); this.scene.add(this.quad);
Nosso ciclo de visualização ficará assim:
this.envFBO = new THREE.WebGLRenderTarget(width, height); this.renderer.autoClear = false; render() { requestAnimationFrame( this.render ); this.renderer.clear(); // fbo this.renderer.setRenderTarget(this.envFbo); this.renderer.render( this.scene, this.orthoCamera ); // this.renderer.setRenderTarget(null); this.renderer.render( this.scene, this.orthoCamera ); this.renderer.clearDepth(); // this.renderer.render( this.scene, this.camera ); };
Ótimo, tempo para uma pequena excursão à teoria. Materiais transparentes como o vidro são visíveis porque refratam a luz. Isso ocorre porque a luz passa através do vidro mais lentamente do que pelo ar e, quando o feixe de luz colide com esse objeto em ângulo, a diferença de velocidade faz com que a luz mude de direção. Essa mudança de direção é o que se entende por refração.

Para repetir isso no código, precisamos saber o ângulo entre o vetor de direção do olhar e a superfície normal. Vamos mudar o sombreador de vértice para contar esses vetores.
varying vec3 eyeVector; varying vec3 worldNormal; void main() { vec4 worldPosition = modelMatrix * vec4( position, 1.0); eyeVector = normalize(worldPos.xyz - cameraPosition); worldNormal = normalize( modelViewMatrix * vec4(normal, 0.0)).xyz; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }
No sombreador de fragmentos, agora podemos usar eyeVector
e worldNormal
como os dois primeiros parâmetros na função refract incorporada ao refract
. O terceiro parâmetro é a razão dos índices de refração, ou seja, o índice de refração (IOR) do nosso vidro denso. No nosso caso, será 1,0 / 1,5, mas você pode alterar esse valor para alcançar o resultado desejado. Por exemplo, o índice de refração da água é 1,33 e o do diamante é 2,42.
uniform sampler2D envMap; uniform vec2 resolution; varying vec3 worldNormal; varying vec3 viewDirection; void main() { // get screen coordinates vec2 uv = gl_FragCoord.xy / resolution; vec3 normal = worldNormal; // calculate refraction and add to the screen coordinates vec3 refracted = refract(eyeVector, normal, 1.0/ior); uv += refracted.xy; // sample the background texture vec4 tex = texture2D(envMap, uv); vec4 output = tex; gl_FragColor = vec4(output.rgb, 1.0); }
https://codesandbox.io/embed/multi-side-refraction-step-13-pzxf9?fontsize=14&hidenavigation=1&theme=dark
Ótimo! Escrevemos com sucesso um shader. Mas o diamante quase não é visível ... Em parte porque processamos apenas uma propriedade do vidro. Nem toda luz passa por ela e é refratada; de fato, parte será refletida. Vamos ver como conseguir isso!
Etapa 2: Reflexões e a equação de Fresnel
Por uma questão de simplicidade, nesta lição, não calcularemos as refrações reais, mas simplesmente usaremos a cor branca para a luz refratada. Vamos além: como você sabe quando refletir e quando refratar? Em teoria, isso depende do índice de refração do material: quando o ângulo entre o vetor incidente e a superfície normal for maior que o valor limite, a luz será refletida.

No shader de fragmento, usaremos a equação de Fresnel para calcular as proporções entre os raios refletidos e refratados. Infelizmente, glsl não tem essa equação, você pode copiá-la daqui:
float Fresnel(vec3 eyeVector, vec3 worldNormal) { return pow( 1.0 + dot( eyeVector, worldNormal), 3.0 ); }
Podemos simplesmente misturar a cor da textura do raio refratado com a cor branca refletida usando a proporção que acabamos de calcular.
uniform sampler2D envMap; uniform vec2 resolution; varying vec3 worldNormal; varying vec3 viewDirection; float Fresnel(vec3 eyeVector, vec3 worldNormal) { return pow( 1.0 + dot( eyeVector, worldNormal), 3.0 ); } void main() { // get screen coordinates vec2 uv = gl_FragCoord.xy / resolution; vec3 normal = worldNormal; // calculate refraction and add to the screen coordinates vec3 refracted = refract(eyeVector, normal, 1.0/ior); uv += refracted.xy; // sample the background texture vec4 tex = texture2D(envMap, uv); vec4 output = tex; // calculate the Fresnel ratio float f = Fresnel(eyeVector, normal); // mix the refraction color and reflection color output.rgb = mix(output.rgb, vec3(1.0), f); gl_FragColor = vec4(output.rgb, 1.0); }
https://codesandbox.io/embed/multi-side-refraction-step-23-3vdty?fontsize=14&hidenavigation=1&theme=dark
Já parece muito melhor, mas falta outra coisa ... Exatamente, não vemos a parte de trás do objeto transparente. Vamos consertar!
Etapa 3: Refração Multilateral
Levando em conta o que já aprendemos sobre refrações e reflexões, podemos entender que a luz pode ir e voltar dentro de um objeto muitas vezes antes de deixá-lo.
Para alcançar o resultado correto, do ponto de vista físico, teremos que rastrear todos os raios, mas, infelizmente, esses cálculos são muito pesados para visualização em tempo real. Então, em vez disso, mostrarei como usar a aproximação para pelo menos mostrar as arestas ocultas do diamante.
Vamos precisar de um mapa normal e faces frontal e traseira em um shader de fragmento. Como não podemos visualizar os dois lados ao mesmo tempo, primeiro precisamos obter as bordas traseiras como uma textura.

Crie um novo ShaderMaterial
como no primeiro passo, mas agora renderizaremos o mapa normal em gl_FragColor
.
varying vec3 worldNormal; void main() { gl_FragColor = vec4(worldNormal, 1.0); }
Em seguida, atualizamos o ciclo de visualização e adicionamos o processamento das faces posteriores.
this.backfaceFbo = new THREE.WebGLRenderTarget(width, height); ... render() { requestAnimationFrame( this.render ); this.renderer.clear(); // render background to fbo this.renderer.setRenderTarget(this.envFbo); this.renderer.render( this.scene, this.orthoCamera ); // render diamond back faces to fbo this.mesh.material = this.backfaceMaterial; this.renderer.setRenderTarget(this.backfaceFbo); this.renderer.clearDepth(); this.renderer.render( this.scene, this.camera ); // render background to screen this.renderer.setRenderTarget(null); this.renderer.render( this.scene, this.orthoCamera ); this.renderer.clearDepth(); // render diamond with refraction material to screen this.mesh.material = this.refractionMaterial; this.renderer.render( this.scene, this.camera ); };
Agora usamos uma textura com normais no material.
vec3 backfaceNormal = texture2D(backfaceMap, uv).rgb;
E, finalmente, os normais das faces frontal e traseira são compatíveis.
float a = 0.33; vec3 normal = worldNormal * (1.0 - a) - backfaceNormal * a;
Nesta equação, a é apenas uma quantidade escalar que mostra quantas normais das arestas à direita usar.
https://codesandbox.io/embed/multi-side-refraction-step-33-ljnqj?fontsize=14&hidenavigation=1&theme=dark
Acabou! Todos os lados do diamante são visíveis apenas com a ajuda de reflexões e refrações, que adicionamos ao material.
Limitações
Como já expliquei, não é muito possível obter materiais transparentes em tempo real fisicamente corretos usando esse método. Outro problema é visualizar vários objetos de vidro de frente para o outro. Como visualizamos o plano de fundo apenas uma vez, não funcionará para vê-lo através de uma série de objetos. E, finalmente, as reflexões no campo de visibilidade que demonstrei aqui não funcionarão normalmente nas bordas da tela, porque os raios podem refratar com valores que vão além das bordas do avião, e não conseguiremos capturar esses casos ao renderizar a cena em uma textura.
Obviamente, existem maneiras de contornar essas limitações, mas nem todas serão ótimas soluções WebGL.
Espero que você tenha gostado deste tutorial e aprendido algo novo. Eu me pergunto o que você fará com isso agora! Deixe-me saber no Twitter . E fique à vontade para me perguntar sobre tudo!