LoginSignup
1
3

More than 1 year has passed since last update.

【React Three Fiber】ICO球の各面にTextureの断片を映し出すShaderの実装

Last updated at Posted at 2022-01-05

概要

Icosahedron Sphere(ICO球)の各面に、Textureを断片的に映し出すShaderの実装方法をまとめました。

https://nemutas.github.io/r3f-icosahedron-screen/
output(video-cutter-js.com) (6).gif

このアプリケーションには元ネタとなるサイトが存在します。

このサイトで使われている(であろう)実装技術を解説してくださっている動画があります。

【リポジトリ】は、この動画をReact Three Fiber + TypeScriptでまとめたものになっています。

ただし、動画内で扱われているワイヤーフレームマウスモーションに関しては、実装をしていません。

実装

ICO球の各面に、Textureを断片的に映し出すためには、以下の2つの技術を組み合わせます。

  • フラットシェーディング
  • 屈折表現

フラットシェーディング

Three.jsのMaterialの一部(MeshNormalMaterialなど)には、flatShadingというプロパティがあります。
これは、切片をスムーズに補間する(角ばらないようにする)設定で、デフォルトはfalse(スムーズにする)です。
このflatShadingがtureの状態、つまり切片が補間されず角ばった状態と同等の表現をShaderで実装します。

実装概要は、以下のサイトを見て頂くのが早いと思います。

要約すると、

dFdxdFdy(偏微分)を使用して、頂点座標から面の勾配を算出して、その外積をとることで面単位での法線ベクトルを求める。

ということです。
以下のICO球は、左がMeshNormalMaterialflatShading = trueを割り当てたもの、右がShaderdFdx・dFdyから面の法線ベクトルを取得してフラットシェーディング表現をしたものになっています。
色味は若干ことなりますが、ほぼ同じ表現ができています。

Shader部分のコードは、以下のようになっています。

VertexShader
varying vec3 v_normal;

void main() {
    v_normal = normalMatrix * normal;

    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
FragmentShader
varying vec3 v_normal;

void main() {
    vec3 x = dFdx(v_normal);
    vec3 y = dFdy(v_normal);
    vec3 normal = normalize(cross(x, y));

    gl_FragColor = vec4(normal, 1.0);
}

屈折表現

GLSLを使用した屈折表現については、以下のサイトでまとめられています。

要点として、屈折は以下のように求められます。

vec3 refracted = refract(eyeVector, normal, 1.0 / ior);
uv += refracted.xy;
  • refractは組み込み関数です。
  • eyeVectorは、カメラから頂点へのベクトルです。(ModelView変換した頂点とは異なります)
  • normalは、フラットシェーディングで求めた面の法線ベクトルです。
  • 1.0 / iorは、屈折率の比率で、空気の屈折率(ior)が1.0、素材の屈折率が変数iorとなっています。例えば、素材がガラス(を模したもの)ならior = 1.45になります。水なら1.33、ダイアモンドなら2.42になります。

この実装をまとめたものが、以下のSandboxになります。

Shader部分のコードは、以下のようになっています。

VertexShader
varying vec3 v_normal;
varying vec3 v_eye;

void main() {
    v_normal = normalMatrix * normal;

    vec4 mPos = modelMatrix * vec4( position, 1.0);
    v_eye = normalize(mPos.xyz - cameraPosition);

    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
FragmentShader
uniform sampler2D u_texture;
varying vec3 v_normal;
varying vec3 v_eye;

void main() {
    // flat shading
    vec3 x = dFdx(v_normal);
    vec3 y = dFdy(v_normal);
    vec3 normal = normalize(cross(x, y));

    vec2 uv = gl_FragCoord.xy / vec2(1000.0);

    // refraction
    float ior = 1.45;
    vec3 refracted = refract(v_eye, normal, 1.0 / ior);
    uv += refracted.xy * 0.5;

    vec4 tex = texture2D(u_texture, uv);

    gl_FragColor = tex;
}

uvについて

uvは、Textureのどの位置でpixel値(vec4情報、通常はrgbaの色情報)を取得するかを表します。
uv値は(x, y)の2次元情報で、値の取る範囲は(0, 0)~(1, 1)となっています。

Fragment Shaderでのuvの取り方はいくつか方法があります。

1)VertexShaderから取得する
Three.jsでVertexShaderを使用する場合、頂点attribute の uvを参照することができます。
これをvaryingでFragmentSahderに渡すことで、FragmentSahderでも頂点のuvを参照することができます。

VertexShader
varying vec2 v_uv;

void main() {
    v_uv = uv;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

2)画面座標で取得する
FragmentShaderでは、gl_FragCoordで現在GPUが処理しているウィンドウ上のpixel位置を取得することができます。
これをウィンドウサイズで割ることで、(0, 0)~(1, 1)のuvを取得することができます。
この手法は、Scene全体にEffectをかけるPost-processingでよく使います。

FragmentShader
vec2 uv = gl_FragCoord.xy / resolution;

今回の場合は、1)の方法で取得すればいいように思いますが、これだと以下の要因でうまくいきません。
今回使用しているTexture画像は、unsplash1024×1024の正方形の画像です。

※ 以下のURLにアクセスすると、ランダムなランドスケープの画像(1024×1024)が入手できます。
https://source.unsplash.com/random/1024x1024?landscape

ICO球のuv展開をBlenderで確認します。

このように、正方形のTextureを使用する場合、ICO球のデフォルトのuv展開では、画像の下半分しか使われません。(BlenderとThree.jsでuv展開に差異があるかもしれませんが...)

このため、1)の方法で頂点のuvを取得しても、画像のすべての範囲が使用されず、またICO球の面に映し出されるTextureの断片は、間延びしたものになります。

では、2)の方法ではどうでしょう。
ICO球のuv展開によらないので良さそうですが、ウィンドウの縦横比は常に1ではないので、正方形のTextureに使用した場合、こちらも間延びしてしまいます。

例えば、ウィンドウサイズが横長の場合、uv(白枠)と画像は下図のような関係になり、画像が横に間延びすることがわかります。

そのため、uvは以下のように設定します。

vec2 uv = gl_FragCoord.xy / vec2(1000.0);

vec2(1000.0)で固定値にすることで、正方形のTextureをその比率で取得します。
ただし、ウィンドウサイズが1000px以上の場合、uvが1を超えます。この整合性をとるため、読み込んだTextureのWrapping ModesTHREE.MirroredRepeatWrappingにします。

texture.wrapS = texture.wrapT = THREE.MirroredRepeatWrapping

Fresnel反射

Fresnel反射は、材料の屈折率に依存し、入射ベクトルと表面法線の間の角度が臨界角より大きいとき、光波は反射されます。

Real-time Multiside Refraction in Three Steps

Fresnel反射は、以下の式で求められます。

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

output(video-cutter-js.com) (7).gif

どのアングルから見ても同じ効果を得たいので、VertexShaderからModelView変換をした頂点座標を、varyingを使用してFragmentShaderに渡します。

VertexShader
varying vec3 v_mvPos;

void main() {
    vec4 mvPos = modelViewMatrix * vec4( position, 1.0);
    v_mvPos = normalize(mvPos.xyz);

    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
FragmentShader
varying vec3 v_mvPos;

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

void main() {
    // fresnel reflection
    float fresnel = Fresnel(v_mvPos, normal);
    vec4 color = mix(tex, vec4(1.0), fresnel);

    gl_FragColor = vec4(vec3(fresnel), 1.0);
}

コードでは、fresnelをそのままgl_FragColorとして出力していますが、実際にはmix関数を使用して、読み込んだtexture(tex)の色と白色を混ぜて反射を表現しています。

ランダマイズ

ここまでで、この記事の趣旨はまとめ終わりました。
ただ、このままだとあまり面白みがないので、時間経過で面に表示される画像の位置を切り替える実装をします。

この実装は、冒頭の解説動画に基づいています。私自身完全に理解できていないので、実装の詳細には触れません。
実装の全体像は、【リポジトリ】を参照してください。

FragmentShader
vec2 hash22( vec2 p ){
    p = vec2( dot(p,vec2(127.1,311.7)),dot(p,vec2(269.5,183.3)));
    return fract(sin(p)*43758.5453);
}

void main() {
    // 省略
    // uv switcher
    float diffuse = dot(normal, vec3(1.0));
    float periodicity = (sin(u_time) + 1.0) / 2.0;
    vec2 seed = vec2(floor(diffuse * 5.0 + periodicity * 1.5));
    vec2 rand = hash22(seed);
    rand -= 0.5;
    vec2 switcher = sign(rand) * 1.0 + rand * 0.6;
    uv *= switcher;
    // 省略
}

動画では、periodicityはなく、単純にICO球の回転させることで法線ベクトルを変化させ、それによって画像が切り替わる処理になっています。

hash22は、以下のリポジトリから引用させて頂きました。
hash22の命名規則ですが、最初の2は引数の次元、次の2は返り値の次元となっています。

Post-processing

Post-processingについても、この記事では深く掘り下げません。
実装している内容は、以下のようになっています。

  • gray scale
  • rgb shift
  • noise

スクリーンショット 2022-01-05 231819.png

ご興味のある方は、動画や【リポジトリ】を参照してください。

リポジトリ

まとめ

0 ~ 1の値をとるだけのuvが奥深すぎる...:rolling_eyes:

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3