如何用threejs實現實時多邊形折射
前言
在本教程中,您將學習如何使用Three.js在三個步驟中使物件看起來像玻璃。
渲染3D物件時,無論使用某種3D軟體還是使用WebGL進行實時顯示,始終都必須為其分配材料以使其可見並具有所需的外觀。
可以使用Three.js之類的庫中的現成程式來模仿許多型別的材料,但是在本教程中,我將向您展示如何使用三個物件(三個步驟)使物件看起來像玻璃一樣。
步驟1:設定和正面折射
在本演示中,我將使用菱形幾何圖形,但是您可以跟隨一個簡單的盒子或任何其他幾何圖形。
讓我們建立我們的專案。我們需要一個渲染器,一個場景,一個透視相機和我們的幾何圖形。為了渲染我們的幾何圖形,我們需要為其分配材質。建立此材料將是本教程的主要重點。因此,繼續建立具有基本頂點和片段著色器的新ShaderMaterial。
與您期望的相反,我們的材料將不是透明的,實際上,我們將對鑽石後面的任何東西進行取樣和變形。為此,我們需要將場景(沒有菱形)渲染為紋理。我只是使用正交攝影機渲染全屏平面,但這也可能是充滿其他物件的場景。在Three.js中從菱形分割背景幾何圖形的最簡單方法是使用“圖層”。
this.orthoCamera = new THREE.OrthographicCamera( width / - 2,width / 2,height / 2,height / - 2,1,1000 ); // assign the camera to layer 1 (layer 0 is default) 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); // also move the plane to layer 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(); 程式設計客棧 // render background to fbo this.renderer.setRenderTarget(this.envFbo); this.renderer.render( this.scene,this.orthoCamera ); // render background to screen this.renderer.setRenderTarget(null); this.renderer.r程式設計客棧ender( this.scene,this.orthoCamera ); this.renderer.clearDepth(); // render geometry to screen 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); }
在片段著色器中,我們現在可以將eyeVector和worldNormal用作glsl內建折射函式的前兩個引數。第三個引數是折射率的比率,即我們的快速介質(空氣)的折射率(IOR)fTonSKQu除以我們的慢速介質(玻璃)的IOR。在這種情況下,該值為1.0 / 1.5,但是您可以調整該值以獲得所需的結果。例如,水的IOR為1.33,鑽石的IOR為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); }
真好!我們成功編寫了折射著色器。但是我們的鑽石几乎看不見……部分原因是我們只處理了玻璃的一種視覺特性。並非所有的光都會穿過要折射的材料,實際上,一部分光會被反射。讓我們看看如何實現它!
步驟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,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,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); }
看起來已經好多了,但是還有一些不足之處……嗯,我們看不到透明物件的另一面。讓我們解決這個問題!
步驟3:多邊折射
到目前為止,我們已經瞭解了有關反射和折射的知識,我們可以理解,光在離開物件之前可以在物件內部來回反彈幾次。
為了獲得物理上正確的結果,我們將必須跟蹤每條光線,但是不幸的是,這種計算量太大,無法實時渲染。因此,我將向您展示一個簡單的近似值,至少可以直觀地看到我們鑽石的背面。
在一個片段著色器中,我們需要幾何圖形的正面和背面的世界法線。由於我們不能同時渲染兩側,因此需要首先將背面法線渲染為紋理。
讓我們像在步驟1中一樣製作一個新的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只是一個標量值,指示應應用背面法線的數量。
我們做到了!我們可以看到鑽石的所有側面,僅是因為我們對鑽石的材質進行了折射和反射。
侷限性
正如我已經解釋的那樣,用這種方法實時渲染物理上正確的透明材料是不可能的。當在彼此前面渲染多個玻璃物件時會發生另一個問題。由於我們僅對環境取樣一次,因此無法看透一連串的物件。最後,我在這裡演示的螢幕空間折射在畫布的邊緣附近效果不佳,因為光線可能會折射到其邊界之外的值,並且在將背景場景渲染到渲染目標時我們沒有捕獲到該資料。
當然,有多種方法可以克服這些限制,但是對於您在WebGL中進行實時渲染,它們可能並不是全部很好的解決方案。
以上就是如何用threejs實現實時多邊形折射的詳細內容,更多關於JS庫的資料請關注我們其它相關文章!