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 のインストールと設定のチュートリアル

推薦する

Nginx は gzip 圧縮に基づいてアクセス速度を向上します

1. nginx はなぜ gzip を使用するのですか? 1. 圧縮の役割:ページがgzipで圧縮さ...

MySQL msiバージョンのダウンロードとインストールの初心者向けの詳細なグラフィックチュートリアル

目次1. MySQL msiバージョンをダウンロードする2. インストール3. 環境変数を設定する1...

ミニプログラムはリストのカウントダウン機能を実装します

この記事の例では、ミニプログラムでリストカウントダウンを実装するための具体的なコードを参考までに共有...

JavaScriptの擬似配列と配列の使い方と違い

擬似配列と配列JavaScript では、5 つのプリミティブ データ型を除き、関数を含め、その他す...

Vue 開発ガイドの重要な知識の要約

目次概要0. JavaScriptとWeb開発の基礎1. Vueの基本概念Vue コア機能コンポーネ...

MySQL グリーンバージョン設定コードと 1067 エラーの詳細

MySQL グリーンバージョン設定コードと 1067 エラーMySQL エンコーディングを表示 &#...

Pythonの関数知識についての簡単な説明

目次関数パラメータの2つの主要なカテゴリ位置パラメータ可変長パラメータ名前空間要約する関数パラメータ...

MySQLのexecute、executeUpdate、executeQueryの違い

execute、executeUpdate、executeQuery の違い (およびそれらの戻り値...

VMwareがLinuxシステムをインストールして起動した後に黒い画面が表示される問題を解決する

1. 設置環境1. HUAWEI mate x CPU i5 82500u、8g メモリ、独立グラフ...

Linux zabbix エージェントの展開と設定方法の詳細な説明

1. web01にzabbix-agentをインストールするZabbix ウェアハウスをデプロイする...

ウェブデザインの概要

<br />1998年に最初の個人ページが誕生してから2008年の今日まで、デザイン業界...

Windows Server 2016 に Docker をインストールする方法

最近、Microsoft は Docker をネイティブにサポートする Windows Server...

Dockerバッチコンテナオーケストレーションの実装

導入Dockerfile ビルドの実行は、単一のコンテナの手動操作です。マイクロサービス アーキテク...

検索データ表示を実装するJavaScript

この記事ではJavaScript検索のデータ表示コードを参考までに共有します。具体的な内容は以下のと...

Centos7 システムでの MySQL マスター スレーブ同期構成スキーム

序文最近、高可用性プロジェクトに取り組む際には、データの同期が必要になっています。ノードが 2 つし...