三个步骤的三面折射

渲染3D对象时,始终需要向其中添加一些材料,以便其可见并以您想要的方式显示。 无论是通过特殊程序还是通过WebGL实时进行操作,都没有关系。


可以使用Three.js之类的内置工具来模拟大多数材料,但是在本教程中,我将向您展示如何使用Three.js在三个步骤中使对象看起来像玻璃。


步骤1:设定和正面思考


在此示例中,我将使用菱形的几何形状,但可以使用简单的立方体或任何其他形状。


让我们建立我们的项目。 我们需要一个可视化器,一个场景,一个照相机和一个几何体。 为了可视化我们的表面,我们需要材料。 创建该材料将是本课程的主要目的。 因此,让我们用顶点和片段着色器创建一个新的SharedMaterial对象。


与您的期望相反,我们的材料将不是透明的,实际上,我们将扭曲钻石的形状。 为此,我们需要在纹理中可视化场景(没有菱形)。 我只是使用正交相机渲染一个大小相当于整个范围的平面,但是您也可以使用其他对象渲染场景。 在Three.js中将背景表面与菱形分开的最简单方法是使用“图层”。


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); 

我们的可视化周期如下所示:


 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 ); }; 

太好了,该花点时间研究一下理论了。 像玻璃这样的透明材料是可见的,因为它们使光折射。 这是因为光通过玻璃的速度比通过空气的速度慢,并且当光束以一定角度与这种物体碰撞时,速度的差异会导致光改变方向。 方向变化就是折射。



要在代码中重复此步骤,我们需要知道凝视方向矢量和表面法线之间的角度。 让我们更改顶点着色器以计算这些向量。


 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); } 

在片段着色器中,我们现在可以使用eyeVectorworldNormal作为内置于refract的折射函数中的前两个参数。 第三个参数是折射率的比率,即我们的致密介质-玻璃的折射率(IOR)。 在我们的情况下,它将是1.0 / 1.5,但是您可以更改此值以获得所需的结果。 例如,水的折射率为1.33,而钻石的折射率为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


太好了! 我们已经成功编写了一个着色器。 但是钻石几乎看不见……部分是因为我们只加工了玻璃的一种特性。 并非所有的光都会穿过它并被折射;实际上,一部分光会被反射。 让我们看看如何实现这一目标!


步骤2:反射和菲涅耳方程


为了简单起见,在本课程中,我们将不计算真实的折射,而仅将白色用作折射光。 我们走得更远:您如何知道何时该反思,何时该反思? 理论上,这取决于材料的折射率:当入射向量与表面法线之间的角度大于阈值时,光将被反射。



在片段着色器中,我们将使用菲涅耳方程来计算反射光线和折射光线之间的比例。 不幸的是,glsl没有这个方程式,您可以从这里复制它:


 float Fresnel(vec3 eyeVector, vec3 worldNormal) { return pow( 1.0 + dot( eyeVector, worldNormal), 3.0 ); } 

我们可以使用我们刚刚计算的比例,简单地将折射光线的纹理颜色与反射的白色颜色混合。


 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


它看起来已经好多了,但是还缺少其他一些东西……确实,我们看不到透明对象的背面。 让我们修复它!


步骤3:多边折射


考虑到我们已经学到的关于折射和反射的知识,我们可以理解,光在离开物体之前可以在物体内部来回传播多次。
为了获得正确的结果,从物理角度来看,我们必须跟踪每条射线,但是不幸的是,这样的计算对于实时可视化来说太繁琐了。 因此,我将向您展示如何使用近似值至少显示钻石的隐藏边缘。
我们将需要一个法线贴图以及一个片段着色器中的正面和背面。 由于我们无法同时可视化两侧,因此首先需要获取后边缘作为纹理。



像第一步一样创建一个新的ShaderMaterial ,但是现在我们将在gl_FragColor渲染法线贴图。


 varying vec3 worldNormal; void main() { gl_FragColor = vec4(worldNormal, 1.0); } 

接下来,我们更新可视化周期并添加背面处理。


 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 ); }; 

现在,我们在材质中使用具有法线的纹理。


 vec3 backfaceNormal = texture2D(backfaceMap, uv).rgb; 

最后,正面和背面的法线是兼容的。


 float a = 0.33; vec3 normal = worldNormal * (1.0 - a) - backfaceNormal * a; 

在此等式中,a只是一个标量,它表示要使用多少个后沿法线。


https://codesandbox.io/embed/multi-side-refraction-step-33-ljnqj?fontsize=14&hidenavigation=1&theme=dark


原来! 钻石的所有侧面只有在反射和折射的帮助下才可见,我们将其添加到材料中。


局限性


正如我已经解释的那样,使用这种方法不太可能获得物理上正确的实时透明材料。 另一个问题是可视化彼此面对的多个玻璃物体。 由于我们仅可视化背景一次,因此无法通过一系列对象查看背景。 最后,我在这里展示的可见性区域中的反射将无法在屏幕的边界正常工作,因为光线可以折射超出平面边界的值,并且在将场景渲染为纹理时我们将无法捕捉到这些情况。


当然,有很多方法可以解决这些限制,但是并不是所有的限制都是出色的WebGL解决方案。


希望您喜欢本教程并学到新知识。 我想知道您现在将如何处理它! 在Twitter上让我知道。 随时向我询问一切!

Source: https://habr.com/ru/post/zh-CN477418/


All Articles