Cuando renderiza un objeto 3D, siempre necesita agregarle material para que sea visible y se vea de la manera deseada; no importa si haces esto en programas especiales o en tiempo real a través de WebGL.
La mayor parte del material se puede simular usando las herramientas integradas de bibliotecas como Three.js, pero en este tutorial le mostraré cómo hacer que los objetos se vean como vidrio en tres pasos usando, lo adivinó, Three.js.
Paso 1: configuración y reflejos frontales
En este ejemplo, usaré la geometría del diamante, pero puedes usar un cubo simple o cualquier otra forma.
Configuremos nuestro proyecto. Necesitamos un visualizador, una escena, una cámara y geometría. Para visualizar nuestra superficie, necesitamos material. La creación de este material será el objetivo principal de la lección. Entonces, creemos un nuevo objeto SharedMaterial con sombreadores de vértices y fragmentos.
Contrariamente a sus expectativas, nuestro material no será transparente, de hecho, distorsionaremos lo que estará detrás del diamante. Para hacer esto, necesitaremos visualizar la escena (sin diamante) en la textura. Solo renderizo un plano del tamaño de todo el alcance usando una cámara ortogonal, pero también puede renderizar una escena con otros objetos. La forma más fácil de separar la superficie de fondo del diamante en Three.js es usar Capas.
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);
Nuestro ciclo de visualización se verá así:
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 ); };
Genial, es hora de una pequeña excursión a la teoría. Los materiales transparentes como el vidrio son visibles porque refractan la luz. Esto se debe a que la luz atraviesa el vidrio más lentamente que a través del aire, y cuando el haz de luz colisiona con dicho objeto en ángulo, la diferencia de velocidad hace que la luz cambie de dirección. Este cambio de dirección es lo que se entiende por refracción.

Para repetir esto en código, necesitamos saber el ángulo entre el vector de dirección de la mirada y la superficie normal. Cambiemos el sombreador de vértices para contar estos vectores.
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); }
En el sombreador de fragmentos, ahora podemos usar eyeVector
y worldNormal
como los dos primeros parámetros en la función de refract
integrada en refract
. El tercer parámetro es la relación de los índices de refracción, es decir, el índice de refracción (IOR) de nuestro medio denso de vidrio. En nuestro caso, será 1.0 / 1.5, pero puede cambiar este valor para lograr el resultado deseado. Por ejemplo, el índice de refracción del agua es 1.33, y el del diamante es 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
Genial Hemos escrito con éxito un sombreador. Pero el diamante es apenas visible ... En parte porque procesamos solo una propiedad del vidrio. No toda la luz pasará a través de ella y será refractada; de hecho, parte se reflejará. ¡Veamos cómo lograr esto!
Paso 2: Reflexiones y la ecuación de Fresnel
En aras de la simplificación, en esta lección no calcularemos las refracciones reales, sino que simplemente usaremos el blanco para la luz refractada. Vamos más allá: ¿cómo saber cuándo reflexionar y cuándo refractar? En teoría, esto depende del índice de refracción del material: cuando el ángulo entre el vector incidente y la superficie normal es mayor que el valor umbral, la luz se reflejará.

En el sombreador de fragmentos, utilizaremos la ecuación de Fresnel para calcular las proporciones entre los rayos reflejados y refractados. Desafortunadamente, glsl no tiene esta ecuación, puede copiarla desde aquí:
float Fresnel(vec3 eyeVector, vec3 worldNormal) { return pow( 1.0 + dot( eyeVector, worldNormal), 3.0 ); }
Simplemente podemos mezclar el color de textura del rayo refractado con el color blanco reflejado usando la proporción 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
Ya se ve mucho mejor, pero falta algo más ... Exactamente, no vemos la parte posterior del objeto transparente. ¡Vamos a arreglarlo!
Paso 3: refracción multilateral
Teniendo en cuenta lo que ya hemos aprendido sobre las refracciones y los reflejos, se puede entender que la luz puede ir y venir dentro de un objeto muchas veces antes de abandonarlo.
Para lograr el resultado correcto, desde un punto de vista físico, tendremos que rastrear cada rayo, pero, desafortunadamente, tales cálculos son demasiado pesados para la visualización en tiempo real. Entonces, en cambio, le mostraré cómo usar la aproximación para al menos mostrar los bordes ocultos del diamante.
Necesitaremos un mapa normal y caras frontales y posteriores en un sombreador de fragmentos. Como no podemos visualizar ambos lados al mismo tiempo, primero necesitamos obtener los bordes posteriores como una textura.

Cree un nuevo ShaderMaterial
como en el primer paso, pero ahora representaremos el mapa normal en gl_FragColor
.
varying vec3 worldNormal; void main() { gl_FragColor = vec4(worldNormal, 1.0); }
A continuación, actualizamos el ciclo de visualización y agregamos el procesamiento de las caras 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 ); };
Ahora usamos una textura con normales en el material.
vec3 backfaceNormal = texture2D(backfaceMap, uv).rgb;
Y finalmente, las normales de las caras frontal y posterior son compatibles.
float a = 0.33; vec3 normal = worldNormal * (1.0 - a) - backfaceNormal * a;
En esta ecuación, a es solo una cantidad escalar que muestra cuántas normales de los bordes finales utilizar.
https://codesandbox.io/embed/multi-side-refraction-step-33-ljnqj?fontsize=14&hidenavigation=1&theme=dark
Resultó! Todos los lados del diamante son visibles solo con la ayuda de reflejos y refracciones, que agregamos al material.
Limitaciones
Como ya he explicado, no es muy posible obtener materiales transparentes en tiempo real que sean físicamente correctos utilizando este método. Otro problema es visualizar varios objetos de vidrio uno frente al otro. Como visualizamos el fondo solo una vez, no funcionará verlo a través de una serie de objetos. Y finalmente, los reflejos en el campo de visibilidad que demostré aquí no funcionarán normalmente en los bordes de la pantalla, porque los rayos pueden refractarse con valores que van más allá de los bordes del plano, y no podremos captar estos casos al convertir la escena en una textura.
Por supuesto, hay formas de sortear estas limitaciones, pero no todas serán excelentes soluciones de WebGL.
Espero que hayas disfrutado este tutorial y hayas aprendido algo nuevo. ¡Me pregunto qué harás con él ahora! Avísame en Twitter ¡Y siéntase libre de preguntarme sobre todo!