Lorsque vous effectuez le rendu d'un objet 3D, vous devez toujours lui ajouter du matériel afin qu'il soit visible et ressemble à ce que vous voulez; peu importe que vous le fassiez dans des programmes spéciaux ou en temps réel via WebGL.
La plupart du matériel peut être simulé à l'aide des outils intégrés de bibliothèques comme Three.js, mais dans ce tutoriel, je vais vous montrer comment faire ressembler des objets à du verre en trois étapes en utilisant - vous l'avez deviné - Three.js.
Étape 1: configuration et réflexions avant
Dans cet exemple, je vais utiliser la géométrie du diamant, mais vous pouvez utiliser un simple cube ou toute autre forme.
Mettons en place notre projet. Nous avons besoin d'un visualiseur, d'une scène, d'une caméra et d'une géométrie. Afin de visualiser notre surface, nous avons besoin de matériel. La création de ce matériel sera l'objectif principal de la leçon. Créons donc un nouvel objet SharedMaterial avec des vertex et des shaders de fragments.
Contrairement à vos attentes, notre matériau ne sera pas transparent, en fait nous déformerons ce qui se cache derrière le diamant. Pour ce faire, nous devrons visualiser la scène (sans losange) dans la texture. Je rend juste un plan de la taille de la portée entière à l'aide d'une caméra orthogonale, mais vous pouvez également rendre une scène avec d'autres objets. La façon la plus simple de séparer la surface d'arrière-plan du diamant dans Three.js est d'utiliser les calques.
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);
Notre cycle de visualisation ressemblera à ceci:
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 ); };
Super, le temps d'une petite excursion en théorie. Les matériaux transparents comme le verre sont visibles car ils réfractent la lumière. En effet, la lumière traverse le verre plus lentement que l'air, et lorsque le faisceau lumineux entre en collision avec un tel objet sous un certain angle, la différence de vitesse fait changer la direction de la lumière. Ce changement de direction est ce que l'on entend par réfraction.

Pour répéter cela dans le code, nous devons connaître l'angle entre le vecteur de direction du regard et la normale de surface. Modifions le vertex shader pour compter ces vecteurs.
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); }
Dans le fragment shader, nous pouvons maintenant utiliser eyeVector
et worldNormal
comme les deux premiers paramètres de la fonction de réfraction intégrée à refract
. Le troisième paramètre est le rapport des indices de réfraction, c'est - à - dire l 'indice de réfraction (IOR) de notre verre dense moyen. Dans notre cas, ce sera 1,0 / 1,5, mais vous pouvez modifier cette valeur pour obtenir le résultat souhaité. Par exemple, l'indice de réfraction de l'eau est de 1,33 et celui du diamant de 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
Super! Nous avons réussi à écrire un shader. Mais le diamant est à peine visible ... En partie parce que nous n'avons traité qu'une seule propriété du verre. Toute la lumière ne la traversera pas et ne sera pas réfractée; en fait, une partie sera réfléchie. Voyons comment y parvenir!
Étape 2: réflexions et équation de Fresnel
Par souci de simplicité, dans cette leçon, nous ne calculerons pas les réfractions pour de vrai, mais utiliserons simplement la couleur blanche pour la lumière réfractée. Nous allons plus loin: comment savoir quand réfléchir et quand se réfracter? En théorie, cela dépend de l'indice de réfraction du matériau: lorsque l'angle entre le vecteur incident et la normale de surface est supérieur à la valeur seuil, la lumière sera réfléchie.

Dans le shader de fragment, nous utiliserons l'équation de Fresnel pour calculer les proportions entre les rayons réfléchis et réfractés. Malheureusement, glsl n'a pas cette équation, vous pouvez la copier à partir d'ici:
float Fresnel(vec3 eyeVector, vec3 worldNormal) { return pow( 1.0 + dot( eyeVector, worldNormal), 3.0 ); }
Nous pouvons simplement mélanger la couleur de texture du rayon réfracté avec la couleur blanche réfléchie en utilisant la proportion que nous venons de calculer.
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
Ça a déjà l'air beaucoup mieux, mais il manque quelque chose d'autre ... Exactement, on ne voit pas le dos de l'objet transparent. Corrigeons-le!
Étape 3: Réfraction multilatérale
En tenant compte de ce que nous avons déjà appris sur les réfractions et les réflexions, nous pouvons comprendre que la lumière peut aller et venir à l'intérieur d'un objet plusieurs fois avant de le quitter.
Pour obtenir le résultat correct, d'un point de vue physique, nous devrons suivre chaque rayon, mais, malheureusement, ces calculs sont trop lourds pour une visualisation en temps réel. Donc, à la place, je vais vous montrer comment utiliser l'approximation pour montrer au moins les bords cachés du diamant.
Nous aurons besoin d'une carte normale et de faces avant et arrière dans un shader de fragment. Comme nous ne pouvons pas visualiser les deux côtés en même temps, nous devons d'abord obtenir les bords arrière sous forme de texture.

Créez un nouveau ShaderMaterial
comme dans la première étape, mais maintenant nous allons rendre la carte normale dans gl_FragColor
.
varying vec3 worldNormal; void main() { gl_FragColor = vec4(worldNormal, 1.0); }
Ensuite, nous mettons à jour le cycle de visualisation et ajoutons le traitement des faces arrière.
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 ); };
Maintenant, nous utilisons une texture avec des normales dans le matériau.
vec3 backfaceNormal = texture2D(backfaceMap, uv).rgb;
Et enfin, les normales des faces avant et arrière sont compatibles.
float a = 0.33; vec3 normal = worldNormal * (1.0 - a) - backfaceNormal * a;
Dans cette équation, a est juste une quantité scalaire qui indique le nombre de normales des bords de fuite à utiliser.
https://codesandbox.io/embed/multi-side-refraction-step-33-ljnqj?fontsize=14&hidenavigation=1&theme=dark
Ça s'est avéré! Tous les côtés du diamant ne sont visibles qu'à l'aide de réflexions et de réfractions, que nous avons ajoutées au matériau.
Limitations
Comme je l'ai déjà expliqué, il n'est pas très possible d'obtenir des matériaux transparents en temps réel qui soient physiquement corrects en utilisant cette méthode. Un autre problème est de visualiser plusieurs objets en verre face à face. Puisque nous ne visualisons l'arrière-plan qu'une seule fois, cela ne fonctionnera pas de le voir à travers une série d'objets. Et enfin, les réflexions dans le domaine de la visibilité que j'ai démontré ici ne fonctionneront pas normalement aux frontières de l'écran, car les rayons peuvent se réfracter avec des valeurs qui dépassent les limites de l'avion, et nous ne pourrons pas attraper ces cas lors du rendu de la scène en texture.
Bien sûr, il existe des moyens de contourner ces limitations, mais toutes ne seront pas d'excellentes solutions WebGL.
J'espère que vous avez apprécié ce didacticiel et appris quelque chose de nouveau. Je me demande ce que tu vas en faire maintenant! Faites le moi savoir sur Twitter . Et n'hésitez pas à me poser des questions sur tout!