threejs でリアルタイムポリゴン屈折を実装する方法

threejs でリアルタイムポリゴン屈折を実装する方法

序文

このチュートリアルでは、Three.js を使用して 3 つのステップでオブジェクトをガラスのように見せる方法を学習します。

3D オブジェクトをレンダリングする場合、3D ソフトウェアを使用する場合でも、リアルタイム表示に WebGL を使用する場合でも、オブジェクトを表示して目的の外観にするには、常にマテリアルを割り当てる必要があります。

Three.js などのライブラリの既成の手順を使用して、さまざまな種類のマテリアルを模倣できますが、このチュートリアルでは、3 つのオブジェクト (3 つの手順) を使用して、オブジェクトがガラスのように動作するように見せる方法を説明します。

ステップ1: セットアップと前方屈折

このデモンストレーションではダイヤモンドジオメトリを使用しますが、単純なボックスやその他のジオメトリを使用することもできます。

プロジェクトを構築しましょう。レンダラー、シーン、パースペクティブカメラ、ジオメトリが必要です。ジオメトリをレンダリングするには、マテリアルを割り当てる必要があります。このマテリアルの作成がこのチュートリアルの主な焦点になります。それでは、基本的な頂点シェーダーとフラグメント シェーダーを使用して新しい ShaderMaterial を作成しましょう。

皆さんの予想に反して、私たちの素材は透明ではありません。実際、ダイヤモンドの背後にあるものはすべてサンプリングして変形します。これを行うには、シーン(ダイヤモンドなし)をテクスチャにレンダリングする必要があります。私は正投影カメラを使用してフルスクリーンの平面をレンダリングしていますが、これは他のオブジェクトで満たされたシーンである可能性もあります。 Three.js で背景ジオメトリを菱形から分割する最も簡単な方法は、「レイヤー」を使用することです。

this.orthoCamera = 新しい THREE.OrthographicCamera( 幅 / - 2, 幅 / 2, 高さ / 2, 高さ / - 2, 1, 1000 );
// カメラをレイヤー 1 に割り当てます (レイヤー 0 がデフォルト)
this.orthoCamera.layers.set(1);

const tex = loadTexture('texture.jpg') を待機します。
this.quad = 新しい THREE.Mesh(新しい THREE.PlaneBufferGeometry()、新しい THREE.MeshBasicMaterial({map: tex}));

this.quad.scale.set(幅、高さ、1);
// 平面もレイヤー1に移動します
this.quad.layers.set(1);
this.scene.add(this.quad);

レンダリング ループは次のようになります。

this.envFBO = 新しい THREE.WebGLRenderTarget(幅、高さ);

this.renderer.autoClear = false;

与える() {
    アニメーションフレームをリクエストします( 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 );
};

さて、ここで少し理論をお話ししましょう。ガラスなどの透明な素材は曲げることができるため、見えるようになります。これは、光がガラスを通過する際の速度が空気を通過する際の速度よりも遅いため、光波が斜めのガラス物体に当たると、速度の変化によって光波の方向が変わるためです。この波の方向の変化は屈折の現象を表しています。

これをコードで再現するには、ワールド空間における目のベクトルとダイヤモンドの表面 (法線) ベクトルの間の角度を知る必要があります。これらのベクトルを計算するために頂点シェーダーを更新しましょう。

変化する vec3 eyeVector;
変化する vec3 worldNormal;

void main() {
    vec4 ワールド位置 = modelMatrix * vec4( 位置、 1.0 );
    eyeVector = normalize(worldPos.xyz - cameraPosition);
    ワールドノーマル = 正規化( modelViewMatrix * vec4(normal, 0.0)).xyz;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(位置、1.0);
}

フラグメント シェーダーでは、glsl の組み込み屈折関数の最初の 2 つのパラメーターとして eyeVector と worldNormal を使用できるようになりました。 3 番目のパラメータは屈折率の比で、高速媒体 (空気) の屈折率 (IOR) を低速媒体 (ガラス) の IOR で割ったものです。この場合の値は 1.0/1.5 ですが、必要な結果を得るために値を調整できます。たとえば、水の IOR は 1.33 で、ダイヤモンドの IOR は 2.42 です。

均一なサンプラー2D envMap;
均一な vec2 解像度。

変化する vec3 worldNormal;
vec3 viewDirection を変更する;

void main() {
    // 画面座標を取得
    vec2 uv = gl_FragCoord.xy / 解像度;

    vec3 法線 = ワールド法線;
    // 屈折を計算して画面座標に追加します
    vec3 屈折 = refract(eyeVector, normal, 1.0/ior);
    uv += 屈折.xy;
    
    // 背景テクスチャをサンプリングする
    vec4 tex = texture2D(envMap, uv);

    vec4 出力 = tex;
    gl_FragColor = vec4(出力.rgb、1.0);
} 

非常に素晴らしい!屈折シェーダーの作成に成功しました。しかし、私たちのダイヤモンドはほとんど見えません...それは、ガラスの視覚的特性の 1 つだけを扱っているからです。すべての光が物質を通過して屈折するわけではなく、実際には一部の光は反射されます。どうやってそれを達成するか見てみましょう!

ステップ2: 反射とフレネル方程式

簡単にするために、このチュートリアルでは適切な反射を計算せず、反射光として白だけを使用します。さて、いつ反射し、いつ屈折するかをどうやって知るのでしょうか?理論的には、これは材料の屈折率に依存し、入射ベクトルと表面法線の間の角度が臨界角よりも大きい場合、光波は反射されます。

フラグメント シェーダーでは、フレネル方程式を使用して、反射光線と屈折光線の比率を計算します。残念ながら glsl にもこの方程式は組み込まれていませんが、ここからコピーできます:

float Fresnel(vec3 eyeVector, vec3 worldNormal) {
    pow( 1.0 + dot( eyeVector, worldNormal), 3.0 ) を返します。
}

これで、先ほど計算したフレネル比に基づいて、屈折テクスチャの色と白い反射の色を簡単にブレンドできるようになりました。

均一なサンプラー2D envMap;
均一な vec2 解像度。

変化する vec3 worldNormal;
vec3 viewDirection を変更する;

float Fresnel(vec3 eyeVector, vec3 worldNormal) {
    pow( 1.0 + dot( eyeVector, worldNormal), 3.0 ) を返します。
}

void main() {
    // 画面座標を取得
    vec2 uv = gl_FragCoord.xy / 解像度;

    vec3 法線 = ワールド法線;
    // 屈折を計算して画面座標に追加します
    vec3 屈折 = refract(eyeVector, normal, 1.0/ior);
    uv += 屈折.xy;

    // 背景テクスチャをサンプリングする
    vec4 tex = texture2D(envMap, uv);

    vec4 出力 = tex;

    // フレネル比を計算する
    float f = Fresnel(eyeVector, normal);

    // 屈折色と反射色を混ぜる
    output.rgb = mix(output.rgb、vec3(1.0)、f);

    gl_FragColor = vec4(出力.rgb、1.0);
} 

見た目はかなり良くなりましたが、まだいくつか欠点があります... 透明なオブジェクトの反対側が見えません。これを直しましょう!

ステップ3: 多面屈折

これまで反射と屈折について学んだことから、光は物体から出ていく前に物体内部で何度も往復する可能性があることがわかります。

物理的に正しい結果を得るには、すべての光線をトレースする必要がありますが、残念ながら、これは計算負荷が大きすぎてリアルタイムでレンダリングできません。そこで、ダイヤモンドの裏側を少なくとも視覚的に確認するための簡単な近似値を紹介します。

フラグメント シェーダーでは、ジオメトリの前面と背面のワールド法線が必要です。両側を同時にレンダリングすることはできないため、最初に背面の法線をテクスチャにレンダリングする必要があります。

手順 1 と同じように新しい ShaderMaterial を作成しますが、今回はワールド法線を gl_FragColor としてレンダリングします。

変化する vec3 worldNormal;

void main() {
    gl_FragColor = vec4(ワールドノーマル、1.0);
}

次に、レンダリング ループを更新して、バックフェース パスを含めます。

this.backfaceFbo = 新しい THREE.WebGLRenderTarget(幅、高さ);

...

与える() {
    アニメーションフレームをリクエストします( this.render );

    this.renderer.clear();

    // 背景を fbo にレンダリングする
    this.renderer.setRenderTarget(this.envFbo);
    this.renderer.render( this.scene, this.orthoCamera );

    // ダイヤモンドの裏面を fbo にレンダリングします
    this.mesh.material = this.backfaceMaterial;
    this.renderer.setRenderTarget(this.backfaceFbo);
    this.renderer.clearDepth();
    this.renderer.render( this.scene, this.camera );

    // 背景を画面にレンダリングする
    this.renderer.setRenderTarget(null);
    this.renderer.render( this.scene, this.orthoCamera );
    this.renderer.clearDepth();

    // 屈折マテリアル付きのダイヤモンドを画面にレンダリングする
    this.mesh.material = this.refractionMaterial;
    this.renderer.render( this.scene, this.camera );
};

ここで、屈折マテリアルの背面法線テクスチャをサンプリングします。

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

最後に、前面法線と背面法線を結合します。

浮動小数点数 a = 0.33;
vec3 法線 = worldNormal * (1.0 - a) - backfaceNormal * a;

この式では、a は、背面法線をどの程度適用するかを示すスカラー値にすぎません。

やったー!私たちがダイヤモンドのすべての面を見ることができるのは、ダイヤモンドの材質によって屈折したり反射したりしているからです。

制限

すでに説明したように、このアプローチでは物理的に正しい透明なマテリアルをリアルタイムでレンダリングすることは不可能です。複数のガラス オブジェクトを互いの前にレンダリングするときに、別の問題が発生します。環境を一度だけサンプリングするため、オブジェクトのチェーンを透視することはできません。最後に、ここで示したスクリーン空間屈折は、キャンバスの端の近くではうまく機能しません。これは、光が境界外の値に屈折する可能性があり、背景シーンをレンダリング ターゲットにレンダリングするときにそのデータをキャプチャしなかったためです。

もちろん、これらの制限を克服する方法はありますが、それらはすべて WebGL でのリアルタイム レンダリングに適したソリューションではない可能性があります。

以上が、threejsを使用してリアルタイムポリゴン屈折を実現する方法の詳細です。JSライブラリの詳細については、123WORDPRESS.COMの他の関連記事に注目してください。

以下もご興味があるかもしれません:
  • three.js で 3D ダイナミック テキスト効果を実現する方法
  • 露滴アニメーション効果を実装するための Three.js サンプル コード
  • three.js でのマルチスレッドの使用とパフォーマンステストの詳細な説明
  • 中国語フォントとトゥイーンアプリケーションを表示する three.js の詳細な分析
  • WeChat ミニゲームの three.js オフスクリーン キャンバスのサンプル コード
  • three.jsはuvとThreeBSPを使用して宅配キャビネット機能を作成します
  • Three.js シェーダーマテリアル組み込み変数の例の詳細な説明
  • Vueページでは3Dアニメーションシーン操作を実現するthree.jsを紹介
  • 動的 QR コードを作成するための Three.js サンプル コード
  • 画像をモザイク化する Three.js サンプル コード

<<:  Python スクリプトを Ubuntu で直接実行する方法

>>:  Windows での MySQL 5.7.10 のインストールと設定のチュートリアル

推薦する

MySQLで関連テーブルを削除する実用的な方法

MySQL データベースでは、テーブルが互いに関連付けられた後は、それらを任意に削除することはできま...

WeChat アプレット wxss で外部 CSS ファイルとアイコンフォントを参照する方法

原因外部ファイルをミニプログラムにインポートする方法は次のとおりです: @import "...

無視されたDOCTYPE記述の分析

doctype もその 1 つです。 <!DOCTYPE HTML PUBLIC "...

Vueカウンターの実装

目次1. カウンターの実装2. 成果を達成する1. カウンターの実装ページにカウンターを実装するだけ...

ミニマルなウェブサイトデザインの例

Web アプリケーション クラス1. みんなのためにダウンまたは私だけのためにこのウェブサイトは、ウ...

CSS でリスト スタイル属性を設定する方法 (この記事を読むだけ)

リストスタイルのプロパティHTMLには、順序なしリストと順序ありリストの2種類のリストがあります。仕...

今日は、珍しいけれど役に立つJSテクニックをいくつか紹介します

1. 戻るボタンhistory.back() を使用してブラウザの「戻る」ボタンを作成します。 &l...

MySQLデータベースの操作とメンテナンスのデータ復旧方法

これまでの 3 つの記事では、論理バックアップと物理バックアップを含む、MySQL データベースの一...

MySql エラー 1698 (28000) の解決策

1. 問題の説明: MysqlERROR1698 (28000) の解決方法、新しくインストールされ...

ソフトウェア テスト - MySQL (VI: データベース関数)

1.MySQL関数1. 数学関数PI() # 円周率 (pi) の値を返します。デフォルトの小数点...

IE8ブラウザはWebページ標準に完全互換となる

<br />海外メディアの報道によると、マイクロソフトはソフトウェアの相互運用性への取り...

JavaScript でオブジェクトのプロパティを削除する方法

1. 削除delete は、オブジェクトのプロパティを残さずに削除する唯一の方法ですが、その「代替」...

Node.js http モジュールの使用

目次序文ウェブHTTP サーバーファイルサーバー練習する序文Node.js 開発の目的は、JavaS...

MySQL でテーブルスペースの断片化を解消する詳細な例

MySQL でテーブルスペースの断片化を解消する詳細な例断片化の原因(1)テーブルのストレージは断片...

PIP で docker-compose をインストールする際のタイムアウト問題の解決方法

1: インストールコマンドpip install docker-compose例外情報socket....