只需三步,實時多邊形折射

原文在 https://tympanus.net/codrops/2019/10/29/real-time-multiside-refraction-in-three-steps/
做者是 傑斯珀·沃斯(Jesper Vos)ide

在本教程中,您將學習如何使用Three.js在三個步驟中使對象看起來像玻璃。函數

image.png

渲染3D對象時,不管使用某種3D軟件仍是使用WebGL進行實時顯示,始終都必須爲其分配材料以使其可見並具備所需的外觀。學習

可使用Three.js之類的庫中的現成程序來模仿許多類型的材料,可是在本教程中,我將向您展現如何使用三個對象(三個步驟)使對象看起來像玻璃同樣。this

步驟1:設定和正面折射

在本演示中,我將使用菱形幾何圖形,可是您能夠跟隨一個簡單的盒子或任何其餘幾何圖形。spa

讓咱們創建咱們的項目。咱們須要一個渲染器,一個場景,一個透視相機和咱們的幾何圖形。爲了渲染咱們的幾何圖形,咱們須要爲其分配材質。建立此材料將是本教程的主要重點。所以,繼續建立具備基本頂點和片斷着色器的新ShaderMaterial。.net

與您指望的相反,咱們的材料將不是透明的,實際上,咱們將對鑽石後面的任何東西進行採樣和變形。爲此,咱們須要將場景(沒有菱形)渲染爲紋理。我只是使用正交攝影機渲染全屏平面,但這也多是充滿其餘對象的場景。在Three.js中從菱形分割背景幾何圖形的最簡單方法是使用「圖層」。code

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

咱們的渲染循環以下所示:orm

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.render( this.scene, this.orthoCamera );
    this.renderer.clearDepth();

    // render geometry to screen
    this.renderer.render( this.scene, this.camera );
};

好吧,如今該花一點點理論了。透明材料(如玻璃)能夠彎曲,所以可見。那是由於光在玻璃中的傳播要比空氣中的傳播慢,所以當光波以必定角度撞擊玻璃物體時,這種速度變化會致使光波改變方向。波浪方向的這種變化描述了折射現象。對象

image.png

爲了在代碼中複製這一點,咱們將須要知道咱們的眼睛向量與世界空間中鑽石表面(法線)向量之間的角度。讓咱們更新頂點着色器以計算這些向量。blog

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)除以咱們的慢速介質(玻璃)的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);
}

image.png

真好!咱們成功編寫了折射着色器。可是咱們的鑽石几乎看不見……部分緣由是咱們只處理了玻璃的一種視覺特性。並不是全部的光都會穿過要折射的材料,實際上,一部分光會被反射。讓咱們看看如何實現它!

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

爲了簡單起見,在本教程中,咱們將不計算適當的反射,而僅將白色用做反射光。如今,咱們怎麼知道何時該反思,何時該折射?理論上,這取決於材料的折射率,當入射矢量和表面法線之間的角度大於臨界角時,光波將被反射。

image.png

在片斷着色器中,咱們將使用菲涅耳方程來計算反射光線與折射光線之間的比率。不幸的是,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);
}

image.png

看起來已經好多了,可是還有一些不足之處……嗯,咱們看不到透明對象的另外一面。讓咱們解決這個問題!

步驟3:多邊折射

到目前爲止,咱們已經瞭解了有關反射和折射的知識,咱們能夠理解,光在離開對象以前能夠在對象內部來回反彈幾回。

爲了得到物理上正確的結果,咱們將必須跟蹤每條光線,可是不幸的是,這種計算量太大,沒法實時渲染。所以,我將向您展現一個簡單的近似值,至少能夠直觀地看到咱們鑽石的背面。

在一個片斷着色器中,咱們須要幾何圖形的正面和背面的世界法線。因爲咱們不能同時渲染兩側,所以須要首先將背面法線渲染爲紋理。

image.png

讓咱們像在步驟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只是一個標量值,指示應應用背面法線的數量。

image.png

咱們作到了!咱們能夠看到鑽石的全部側面,僅是由於咱們對鑽石的材質進行了折射和反射。

侷限性

正如我已經解釋的那樣,用這種方法實時渲染物理上正確的透明材料是不可能的。當在彼此前面渲染多個玻璃對象時會發生另外一個問題。因爲咱們僅對環境採樣一次,所以沒法看透一連串的對象。最後,我在這裏演示的屏幕空間折射在畫布的邊緣附近效果不佳,由於光線可能會折射到其邊界以外的值,而且在將背景場景渲染到渲染目標時咱們沒有捕獲到該數據。

固然,有多種方法能夠克服這些限制,可是對於您在WebGL中進行實時渲染,它們可能並非所有很好的解決方案。


搬運文章,機翻修改。沒有公衆號須要你關注。

相關文章
相關標籤/搜索