水を作りたい!
水の表現
ゲームにおいて「水」を綺麗に表現する事は、ぱっと見のクオリティーに大きく寄与すると考えています。
特に自然の中を舞台にしたゲームだと、川や湖などが綺麗に描写されていると、「美しい自然」を感じやすくなるのではないでしょうか?
しかし、水の表現は非常に難しいです。ただ半透明にするだけでは水には見えず、色付きガラスのようになってしまいます。
水をそれっぽく表示する為には「深い場所の方が暗くなっている」「波によって水底が揺れる」「水面に景色が反射している」などの機能を実装する必要があります。
そこでこの記事では「ゲームにおいて水はどのように表現されるのか」を実装しながら解説しようと思います。使うゲームエンジンはGodotですが、基本的にはGLSLと同じなので、他のゲームエンジンでも活かす事が出来る値思います。
そもそもシェーダーとは?
Vertex Shader
描画の前段階として、表示する物(点・線・ポリゴン)の座標データを弄る必要があります。例えば移動や回転を表現したり、三次元空間を二次元に投影したり、そう言った調整を行います。
これを行うのがVertex Shaderです。日本語に直すと頂点シェーダーでしょうか。そのままですね。
Fragment Shader
Vertex Shaderで行う処理は、いわば「下書き」です。絵として完成させるには、色塗りを行う必要があります。
今回、水の表現の為に「波による揺らめきを反映した透過処理」や「水面での反射」などの計算を行う必要がありますが、これらは「色塗り」の一種と言えるでしょう。
これを行うのがFragment Shaderです。
Godotにおけるシェーダー
先ほど「基本的にはGLSLと同じ」と書きましたが、GodotにおけるシェーダーにはGLSLと異なる点もあります。
最も大きく異なる点は「基本的な処理は自動で行ってくれる」ことです。私たちが実装すべく部分は屈折や反射などの「特殊な事」だけです。
シェーダーの基本
1. ただの板
まずは「ただの青い板」を実装します。
シェーダーではuniformという特殊な変数を宣言する事で、後々調節可能なパラメータとなります。例えば水の色などのパラメータは、ゲーム画面を見ながら微調節したいです。こうしたパラメータをuniformとして宣言します。
これらのパラメータはFragment Shader内で「出力変数」に代入することで、出力完了となります。
| 出力変数 | 型 | 内容 |
|---|---|---|
ALBEDO |
vec3 |
色をRGBで指定する |
METALLIC |
float |
金属っぽさ |
ROUGHNESS |
float |
ザラザラ度合い |
SPECULAR |
float |
光沢の補正値。通常は0.5に指定する |
shader_type spatial; // 3D用のシェーダーであることを指定
// 以下、uniform変数を宣言
// 色。デフォルトは暗い青色(R,G,B = 0.1, 0.2, 0.6)
uniform vec3 water_color: source_color = vec3(0.1, 0.2, 0.6);
// 金属っぽさ。デフォルトは非金属
uniform float metalic: hint_range(0.0, 1.0) = 0.0;
// ザラザラ度合い。デフォルトはツルツル
uniform float roughness: hint_range(0.0, 1.0) = 0.025;
// 光沢の補正値。デフォルトは通常
uniform float specular: hint_range(0.0, 1.0) = 0.5;
// ここからがFragment Shaderの実装
// 各種パラメータを代入している
void fragment(){
ALBEDO = water_color;
METALLIC = metalic;
ROUGHNESS = roughness;
SPECULAR = specular;
}
これだけでは不透明だし、波もないし、ただの青色の板です。
なお、反射や屈折の効果を見る為に、黄色の円柱、緑色のトーラス、桃色の立方体を配置しています。
黄色の円柱と緑色のトーラスは半分だけ水中に沈めています。桃色の立方体は完全に水上にあります。
2. 波を実装
続いて波を実装します。凹凸はNORMALと呼ばれており、これを書き換える事で「波打って見える」ようになります。
※正確にはNORMALは法線と呼ばれ、面が向いている方向です。真上なら(0.0, 1.0, 0.0)ですし、Z方向に30度傾いているなら(0.0, sqrt(3)/2, 0.5)となります。
ここではランダムな法線ノイズwave_textureを使って、ランダムな凹凸を表現します。また、波が特定の方向に動くのを防ぐ為に、三方向へ動く波を重ね合わせています。
以下のコードでは、変更点のみコメントを記述しています。
shader_type spatial;
uniform vec3 water_color: source_color = vec3(0.1, 0.2, 0.6);
uniform float metalic: hint_range(0.0, 1.0) = 0.0;
uniform float roughness: hint_range(0.0, 1.0) = 0.025;
uniform float specular: hint_range(0.0, 1.0) = 0.5;
// ノイズテクスチャ
uniform sampler2D wave_texture: hint_normal, repeat_enable;
// 波の大きさ
uniform float wave_scale = 0.1;
// 波の速度
uniform float wave_speed = 0.02;
// 三方向に動く波を重ね合わせる
vec3 mix_three_waves(sampler2D wave_tex, vec2 uv, float delta){
vec3 wave1 = texture(wave_tex, uv + delta * vec2(1.0, 0.0)).xyz;
vec3 wave2 = texture(wave_tex, uv + delta * vec2(-0.5, 0.866)).xyz;
vec3 wave3 = texture(wave_tex, uv + delta * vec2(-0.5, -0.866)).xyz;
return (wave1 + wave2 + wave3) * 0.333333;
}
void fragment(){
// 「カメラから見た座標系」から「元の座標」に変換する
vec3 surface_position = (INV_VIEW_MATRIX * vec4(VERTEX, 1.0)).xyz;
// 三方向の波を重ね合わせ、一つの波と見なす
vec3 wave = mix_three_waves(wave_texture, surface_position.xz * wave_scale, TIME * wave_speed);
// テクスチャは0.0から1.0までしか保存できないので、-1.0から1.0に拡張する
// xyzではなくxzyにしたり、yを反転させたりしている。ノイズの方を調節してもOK
vec3 normal = normalize(wave.xzy * 2.0 - 1.0) * vec3(1.0, 1.0, -1.0);
// 波のデータを凹凸(NORMAL)として出力する
// 出力時には「カメラから見た方向」に変換する事をお忘れなく
NORMAL = (VIEW_MATRIX * vec4(normal, 0.0)).xyz;
ALBEDO = water_color;
METALLIC = metalic;
ROUGHNESS = roughness;
SPECULAR = specular;
}
これで「水面の揺らぎ」を表現する事が出来ました。
透明感を実装
1. バックバッファにアクセス
水をただ透けさせるだけならアルファブレンド(半透明化)すればいいのですが、これだと屈折などの表現を出来ません。そこで、ただアルファ値を決めるのではなく、半透明処理を自前で実装します。
そのためには「水面を描画する直前の画面」にアクセスする必要があります。
さて、一般的な感覚だと描画順を「水以外を描画→水面を描画」とすれば問題ないように思えますが、それでは上手く動きません。何故なら、描画処理では様々な並列化と最適化が行われるからです。
そこで「水以外を描画→出来た画面のコピー(バックバッファ)を作成→水面を描画」のように、間でコピーを行う必要があります。
と言っても、実際の処理はゲームエンジンが行ってくれるので、私たちがすべきことは「このシェーダではバックバッファにアクセスします」と宣言するだけです。
Godot、hint_screen_textureを使ってバックバッファへのアクセスを宣言します。
// バックバッファにアクセスする事を宣言
uniform sampler2D screen_texture: hint_screen_texture, repeat_disable, filter_nearest;
void fragment(){
// 画面の色を取得
vec3 background_color = textureLod(screen_texture, SCREEN_UV, 0.0).rgb;
}
これを使って、水を透過させてみます。
shader_type spatial;
// 画面のコピーにアクセスする事を宣言
uniform sampler2D screen_texture: hint_screen_texture, repeat_disable, filter_nearest;
uniform vec3 water_color: source_color = vec3(0.1, 0.2, 0.6);
uniform float metalic: hint_range(0.0, 1.0) = 0.0;
uniform float roughness: hint_range(0.0, 1.0) = 0.025;
uniform float specular: hint_range(0.0, 1.0) = 0.5;
uniform sampler2D wave_texture: hint_normal, repeat_enable;
uniform float wave_scale = 0.1;
uniform float wave_speed = 0.02;
vec3 mix_three_waves(sampler2D wave_tex, vec2 uv, float delta){
vec3 wave1 = texture(wave_tex, uv + delta * vec2(1.0, 0.0)).xyz;
vec3 wave2 = texture(wave_tex, uv + delta * vec2(-0.5, 0.866)).xyz;
vec3 wave3 = texture(wave_tex, uv + delta * vec2(-0.5, -0.866)).xyz;
return (wave1 + wave2 + wave3) * 0.333333;
}
void fragment(){
vec3 surface_position = (INV_VIEW_MATRIX * vec4(VERTEX, 1.0)).xyz;
vec3 wave = mix_three_waves(wave_texture, surface_position.xz * wave_scale, TIME * wave_speed);
vec3 normal = normalize(wave.xzy * 2.0 - 1.0) * vec3(1.0, 1.0, -1.0);
// 画面の色を取得
vec3 background_color = textureLod(screen_texture, SCREEN_UV, 0.0).rgb;
// 色を混ぜる(ここでは0.5:0.5の比で混ぜています)
ALBEDO = mix(background_color, water_color, 0.5);
METALLIC = metalic;
ROUGHNESS = roughness;
SPECULAR = specular;
NORMAL = (VIEW_MATRIX * vec4(normal, 0.0)).xyz;
}
これで、水中にあった部分が透過されるようになりました。
上手く透過できていますね! しかし、少し明るすぎますね……?
その原因は「二重にライティングの影響を受けているから」だと思われます。
すなわち、水中のオブジェクト自体が光を受け、さらにその値をコピーした水面が光を受ける、という二重のライティングが起きています。
2. EMISSIONを活用
これを防ぐ最も簡単な方法として「わざと暗くする」という方法が考えられます。しかし、それは曖昧であり、良い調節方法とは言えないでしょう。
そこで水の色ALBEDOは黒にして、代わりにライティングの影響を受けないEMISSIONに色情報を出力する方法を試しました。
// 水の色は黒(無色透明)
ALBEDO = vec3(0.0);
// ライティングの影響から除外する
EMISSION = mix(background_color, water_color, 0.5);
良い感じになりました!
3. 水深の表現
現状は「色を混ぜる」事で透過を実現していましたが、一定の割合で混ぜると違和感が残ります。何故なら、本来であれば水深が深いほど青く(暗く)なるはずだからです。
さて、バックバッファにアクセスする事で「水面下の色」にアクセスできるという話をしました。これと同様に「水面下の深度(カメラとの距離)」にアクセスする事が出来れば、水深の推定に役立ちます。
カメラとの距離情報は深度:depthと呼ばれており、Godotではhint_depth_textureを使って深度情報にアクセスします。
shader_type spatial;
uniform sampler2D depth_texture: hint_depth_texture, repeat_disable, filter_nearest;
vec3 get_position_from_depth(sampler2D depth_tex, vec2 screen_uv, mat4 matrix){
float depth = textureLod(depth_tex, screen_uv, 0.0).x;
#if CURRENT_RENDERER == RENDERER_COMPATIBILITY
vec3 ndc = vec3(screen_uv, depth) * 2.0 - 1.0;
#else
vec3 ndc = vec3(screen_uv * 2.0 - 1.0, depth);
#endif
vec4 pos = matrix * vec4(ndc, 1.0);
return pos.xyz / pos.w;
}
void fragment(){
// 水の表面とカメラの距離を計算
float surface_distance = length(VERTEX);
// 水面下の「位置」を取得
vec3 background_view_position = get_position_from_depth(depth_texture, SCREEN_UV, INV_PROJECTION_MATRIX);
// 水面下とカメラの距離を計算
float background_distance = length(background_view_position);
// デバッグ用。水面下から表面までの距離をグレースケールで出力
ALBEDO = vec3(0.0);
EMISSION = vec3((background_distance - surface_distance)*0.3);
}
結果は以下のようになります。水深がグレースケールで表現できている事が分かります。
なお、background_distance - surface_distanceは本来の意味での「水深」ではありませんが、今後はこれを水深として扱う事にします。
水深に応じて水の色を濃くしたいのですが、ここで「なぜ水は青色なのか」について考えたいと思います。
答えを言ってしまうと「水が赤や緑を吸収するから」です。言い換えると、白色の太陽光から赤や緑が取り除かれた結果、青が残ったと言えるでしょう。
これをシミュレーションするコードを書きます。元の色であるbackground_colorから1.0-water_colorが吸収される様子をシミュレーションすると以下のようになりそうです。
float under_water = (background_distance - surface_distance);
background_color = background_color * exp(-(1.0-water_color)*under_water_length);
上手く水深を表現できた……と思ったのですが、水面スレスレの部分(白矢印の部分)が明るすぎました(上画像)ので、適当な数値をかけて明るさを抑える事にしました。調節後(下画像)は想定通りになりました。
ここまでのソースコード
shader_type spatial;
uniform sampler2D screen_texture: hint_screen_texture, repeat_disable, filter_nearest;
// 深度情報にアクセスする事を宣言
uniform sampler2D depth_texture: hint_depth_texture, repeat_disable, filter_nearest;
uniform vec3 water_color: source_color = vec3(0.1, 0.2, 0.6);
// 調節用
uniform float underwater_multiplier = 0.75;
uniform float water_density: hint_range(0.0, 1.0) = 1.0;
uniform float metalic: hint_range(0.0, 1.0) = 0.0;
uniform float roughness: hint_range(0.0, 1.0) = 0.025;
uniform float specular: hint_range(0.0, 1.0) = 0.5;
uniform sampler2D wave_texture: hint_normal, repeat_enable;
uniform float wave_scale = 0.1;
uniform float wave_speed = 0.02;
vec3 mix_three_waves(sampler2D wave_tex, vec2 uv, float delta){
vec3 wave1 = texture(wave_tex, uv + delta * vec2(1.0, 0.0)).xyz;
vec3 wave2 = texture(wave_tex, uv + delta * vec2(-0.5, 0.866)).xyz;
vec3 wave3 = texture(wave_tex, uv + delta * vec2(-0.5, -0.866)).xyz;
return (wave1 + wave2 + wave3) * 0.333333;
}
// 深度情報をXYZ空間に変換するプログラム
// Rendererによって計算方法が異なるので注意!
vec3 get_position_from_depth(sampler2D depth_tex, vec2 screen_uv, mat4 matrix){
float depth = textureLod(depth_tex, screen_uv, 0.0).x;
#if CURRENT_RENDERER == RENDERER_COMPATIBILITY
vec3 ndc = vec3(screen_uv, depth) * 2.0 - 1.0;
#else
vec3 ndc = vec3(screen_uv * 2.0 - 1.0, depth);
#endif
vec4 pos = matrix * vec4(ndc, 1.0);
return pos.xyz / pos.w;
}
void fragment(){
vec3 surface_position = (INV_VIEW_MATRIX * vec4(VERTEX, 1.0)).xyz;
vec3 wave = mix_three_waves(wave_texture, surface_position.xz * wave_scale, TIME * wave_speed);
vec3 normal = normalize(wave.xzy * 2.0 - 1.0) * vec3(1.0, 1.0, -1.0);
// 水の表面とカメラの距離を計算
float surface_distance = length(VERTEX);
// 水面下の「位置」を取得
vec3 background_view_position = get_position_from_depth(depth_texture, SCREEN_UV, INV_PROJECTION_MATRIX);
// 水面下とカメラの距離を計算
float background_distance = length(background_view_position);
float under_water_length = (background_distance - surface_distance);
//吸収する光
vec3 absorption_color = (1.0-water_color) * water_density;
vec3 background_color = textureLod(screen_texture, SCREEN_UV, 0.0).rgb;
ALBEDO = vec3(0.0);
// 光の吸収をシミュレーションする
EMISSION = background_color * underwater_multiplier * exp(-absorption_color*under_water_length);
METALLIC = metalic;
ROUGHNESS = roughness;
SPECULAR = specular;
NORMAL = (VIEW_MATRIX * vec4(normal, 0.0)).xyz;
}
屈折を実装(簡易)
1. コンセプト
屈折の影響を正確に計算しても良いですが……まずはもっと単純でテキトーに表現してみます。
屈折によりズレる量は視線と水の法線がなす角度$\theta$と水深によって決まります(下図参照)。
2. 実装
これを踏まえて、水面下の背景を取得する際に位置をズラす処理を行います。決して正確ではありませんが、それっぽく見えたらOKでしょう。
// 屈折の強度を調節可能なパラメータとして登録
uniform float refraction_strength = 0.1;
/* 中略 */
// sin(θ)を求める
float refraction_sine = length(cross(normal, normalize(CAMERA_POSITION_WORLD - surface_position)));
// ズレを発生させる。なお、水深が深すぎるとズレが大きくなりすぎるので、水深は3mで切っている
vec2 refraction_screen_uv = SCREEN_UV + normal.xz * refraction_strength * refraction_sine * min(under_water_length, 3.0) / surface_distance;
// 水深を再計算する
vec3 refraction_background_view_position = get_position_from_depth(depth_texture, refraction_screen_uv, INV_PROJECTION_MATRIX);
float refraction_background_distance = length(refraction_background_view_position);
under_water_length = refraction_background_distance - surface_distance;
一応揺らめきが実装できましたが……表示がバグっています。(白矢印)
3. バグを修正
バグの正体は何でしょうか?
見た感じ「水中に無い物まで揺らめいている」という問題により発生しています。
このバグを防ぐ為に「揺らめき後の座標が水中かどうか」を判定し、そうでなければ屈折を無効にするプログラムを書く必要がありそうです。
shader_type spatial;
uniform sampler2D screen_texture: hint_screen_texture, repeat_disable, filter_nearest;
uniform sampler2D depth_texture: hint_depth_texture, repeat_disable, filter_nearest;
uniform float underwater_multiplier = 0.75;
uniform vec3 water_color: source_color = vec3(0.1, 0.2, 0.6);
uniform float water_density: hint_range(0.0, 1.0) = 1.0;
uniform float metalic: hint_range(0.0, 1.0) = 0.0;
uniform float roughness: hint_range(0.0, 1.0) = 0.025;
uniform float specular: hint_range(0.0, 1.0) = 0.5;
uniform sampler2D wave_texture: hint_normal, repeat_enable;
uniform float wave_scale = 0.1;
uniform float wave_speed = 0.02;
// 屈折の強度を調節可能なパラメータとして登録
uniform float refraction_strength = 0.1;
vec3 mix_three_waves(sampler2D wave_tex, vec2 uv, float delta){
vec3 wave1 = texture(wave_tex, uv + delta * vec2(1.0, 0.0)).xyz;
vec3 wave2 = texture(wave_tex, uv + delta * vec2(-0.5, 0.866)).xyz;
vec3 wave3 = texture(wave_tex, uv + delta * vec2(-0.5, -0.866)).xyz;
return (wave1 + wave2 + wave3) * 0.333333;
}
vec3 get_position_from_depth(sampler2D depth_tex, vec2 screen_uv, mat4 matrix){
float depth = textureLod(depth_tex, screen_uv, 0.0).x;
#if CURRENT_RENDERER == RENDERER_COMPATIBILITY
vec3 ndc = vec3(screen_uv, depth) * 2.0 - 1.0;
#else
vec3 ndc = vec3(screen_uv * 2.0 - 1.0, depth);
#endif
vec4 pos = matrix * vec4(ndc, 1.0);
return pos.xyz / pos.w;
}
void fragment(){
vec3 surface_position = (INV_VIEW_MATRIX * vec4(VERTEX, 1.0)).xyz;
vec3 wave = mix_three_waves(wave_texture, surface_position.xz * wave_scale, TIME * wave_speed);
vec3 normal = normalize(wave.xzy * 2.0 - 1.0) * vec3(1.0, 1.0, -1.0);
float surface_distance = length(VERTEX);
vec3 background_view_position = get_position_from_depth(depth_texture, SCREEN_UV, INV_PROJECTION_MATRIX);
float background_distance = length(background_view_position);
float under_water_length = background_distance - surface_distance;
// sin(θ)を求める
float refraction_sine = length(cross(normal, normalize(CAMERA_POSITION_WORLD - surface_position)));
// ズレを発生させる。なお、水深が深すぎるとズレが大きくなりすぎるので、水深は3mで切っている
vec2 refraction_screen_uv = SCREEN_UV + normal.xz * refraction_strength * refraction_sine * min(under_water_length, 3.0) / surface_distance;
// 水深を再計算する
vec3 refraction_background_view_position = get_position_from_depth(depth_texture, refraction_screen_uv, INV_PROJECTION_MATRIX);
float refraction_background_distance = length(refraction_background_view_position);
// もし屈折先が水上なら無効化する
if (refraction_background_distance < surface_distance){
refraction_screen_uv = SCREEN_UV;
refraction_background_view_position = background_view_position;
refraction_background_distance = background_distance;
}
under_water_length = refraction_background_distance - surface_distance;
vec3 absorption_color = (1.0-water_color) * water_density;
vec3 background_color = textureLod(screen_texture, refraction_screen_uv, 0.0).rgb;
ALBEDO = vec3(0.0);
EMISSION = background_color * underwater_multiplier * exp(-absorption_color*under_water_length);
METALLIC = metalic;
ROUGHNESS = roughness;
SPECULAR = specular;
NORMAL = (VIEW_MATRIX * vec4(normal, 0.0)).xyz;
}
バグが無くなりました!
静止画なので少し違和感がありますが、アニメーションだとそれほど違和感はありません。ひとまずこれでOKとしましょう。
SSRを使って反射を実装する
1. コンセプト
SSRとはスーパースーパーレアの略、ではなく「Screen Space Reflection」の略であり、スクリーンを使った反射を意味します。
結論から先にお見せします。確かに反射が起こっている事が見て取れます。
SSRは実際の反射を計算する代わりに、画面に映っている部分だけを使って反射の推定を行います。
画面に映っている部分だけを使っているので物体の奥行き情報が無く、よって「本当に反射が成立しているのか」「反射の本当の色」が不明です。
以下の図では、本来は物体の底面で反射が成立していますが、それを感知できません。
そこで泣く泣く「奥行き情報が多少ズレていても当たったと判定し、底面の色は分からないので前面と同じ色であると仮定する」とします。
2. SSRの実装
これらの処理をglslで書くと以下のようになります。
// screen_pixel_size:1ピクセルの大きさ。1.0/textureSize(depth_texture)で計算する
// ray_origin:反射が始まる位置(ここでは水面)
// ray_normal:反射の方向(glslに標準搭載されているreflect関数を使って事前に計算)
// 1. ray_originが「スクリーン上ではどこか」を計算する
// その結果をscreen_uvに保存
vec4 ray_origin_projected = projection * vec4(ray_origin, 1.0);
vec2 screen_uv = (ray_origin_projected.xy / ray_origin_projected.w) * 0.5 + 0.5;
// 2. ray_normalが「スクリーン上ではどの向きか」を計算する
// その結果をscreen_rayに保存
vec4 ray_direction_projected = (projection * vec4(ray_origin + ray_normal, 1.0));
vec2 screen_ray = ((ray_direction_projected.xy / ray_direction_projected.w) * 0.5 + 0.5) - screen_uv;
screen_ray = normalize(screen_ray) * max(screen_pixel_size.x, screen_pixel_size.y);
// 3. screen_uvをray_normalの方向に少しずつずらす
for (int i=0;i<N;++i){
screen_uv += screen_ray * 5.0;
// 4. 範囲外なら終わり
if(screen_uv.x < 0.0 || screen_uv.x > 1.0 || screen_uv.y < 0.0 || screen_uv.y > 1.0){
break;
}
// 5. その場所を計算する
vec3 screen_position = get_position_from_depth(depth_tex, screen_uv, inv_projection);
// 6. その場所がレイの前面にあるか(s > 1.0)調べる
// レイとの距離が閾値以下か調べる(s - 1.0) * length(screen_position) < threshold
// ベクトルの計算が正しいのか自身が無い……けど動いてるからヨシ!
float a = dot(screen_position, screen_position);
float b = dot(ray_normal, screen_position);
float c = dot(ray_normal, ray_normal);
float d = dot(ray_origin, screen_position);
float e = dot(ray_origin, ray_normal);
float s = (c*d-b*e)/(a*c-b*b);
if(s > 1.0 && (s - 1.0) * length(screen_position) < threshold){
return screen_uv;
}
}
3. プログラム全文
shader_type spatial;
uniform sampler2D screen_texture: hint_screen_texture, repeat_disable, filter_nearest;
uniform sampler2D depth_texture: hint_depth_texture, repeat_disable, filter_nearest;
uniform float underwater_multiplier = 0.75;
uniform vec3 water_color: source_color = vec3(0.1, 0.2, 0.6);
uniform float water_density: hint_range(0.0, 1.0) = 1.0;
uniform float metalic: hint_range(0.0, 1.0) = 0.0;
uniform float roughness: hint_range(0.0, 1.0) = 0.025;
uniform float specular: hint_range(0.0, 1.0) = 0.5;
uniform sampler2D wave_texture: hint_normal, repeat_enable;
uniform float wave_scale = 0.1;
uniform float wave_speed = 0.02;
uniform float refraction_strength = 0.1;
vec3 mix_three_waves(sampler2D wave_tex, vec2 uv, float delta){
vec3 wave1 = texture(wave_tex, uv + delta * vec2(1.0, 0.0)).xyz;
vec3 wave2 = texture(wave_tex, uv + delta * vec2(-0.5, 0.866)).xyz;
vec3 wave3 = texture(wave_tex, uv + delta * vec2(-0.5, -0.866)).xyz;
return (wave1 + wave2 + wave3) * 0.333333;
}
vec3 get_position_from_depth(sampler2D depth_tex, vec2 screen_uv, mat4 matrix){
float depth = textureLod(depth_tex, screen_uv, 0.0).x;
#if CURRENT_RENDERER == RENDERER_COMPATIBILITY
vec3 ndc = vec3(screen_uv, depth) * 2.0 - 1.0;
#else
vec3 ndc = vec3(screen_uv * 2.0 - 1.0, depth);
#endif
vec4 pos = matrix * vec4(ndc, 1.0);
return pos.xyz / pos.w;
}
const int RayStep = 50;
vec2 screen_space_ray_casting(sampler2D depth_tex, vec3 ray_origin, vec3 ray_normal, mat4 projection, mat4 inv_projection, float threshold) {
vec2 screen_pixel_size = 1.0 / vec2(textureSize(depth_tex, 0));
vec4 ray_origin_projected = projection * vec4(ray_origin, 1.0);
vec2 screen_uv = (ray_origin_projected.xy / ray_origin_projected.w) * 0.5 + 0.5;
vec4 ray_direction_projected = (projection * vec4(ray_origin + ray_normal, 1.0));
vec2 screen_ray = ((ray_direction_projected.xy / ray_direction_projected.w) * 0.5 + 0.5) - screen_uv;
screen_ray = normalize(screen_ray) * max(screen_pixel_size.x, screen_pixel_size.y);
for (int i=1;i<RayStep;++i){
screen_uv += screen_ray * 5.0;
if(screen_uv.x < 0.0 || screen_uv.x > 1.0 || screen_uv.y < 0.0 || screen_uv.y > 1.0){
break;
}
vec3 screen_position = get_position_from_depth(depth_tex, screen_uv, inv_projection);
float a = dot(screen_position, screen_position);
float b = dot(ray_normal, screen_position);
float c = dot(ray_normal, ray_normal);
float d = dot(ray_origin, screen_position);
float e = dot(ray_origin, ray_normal);
float s = (c*d-b*e)/(a*c-b*b);
if(s > 1.0 && (s - 1.0) * length(screen_position) < threshold){
return screen_uv;
}
}
return vec2(-1.0);
}
void fragment(){
vec3 surface_position = (INV_VIEW_MATRIX * vec4(VERTEX, 1.0)).xyz;
vec3 wave = mix_three_waves(wave_texture, surface_position.xz * wave_scale, TIME * wave_speed);
vec3 normal = normalize(wave.xzy * 2.0 - 1.0) * vec3(1.0, 1.0, -1.0);
float surface_distance = length(VERTEX);
vec3 background_view_position = get_position_from_depth(depth_texture, SCREEN_UV, INV_PROJECTION_MATRIX);
float background_distance = length(background_view_position);
float under_water_length = background_distance - surface_distance;
float refraction_sine = length(cross(normal, normalize(CAMERA_POSITION_WORLD - surface_position)));
vec2 refraction_screen_uv = SCREEN_UV + normal.xz * refraction_strength * refraction_sine * min(under_water_length, 3.0) / surface_distance;
vec3 refraction_background_view_position = get_position_from_depth(depth_texture, refraction_screen_uv, INV_PROJECTION_MATRIX);
float refraction_background_distance = length(refraction_background_view_position);
if (refraction_background_distance < surface_distance){
refraction_screen_uv = SCREEN_UV;
refraction_background_view_position = background_view_position;
refraction_background_distance = background_distance;
}
under_water_length = refraction_background_distance - surface_distance;
vec3 absorption_color = (1.0-water_color) * water_density;
vec3 background_color = textureLod(screen_texture, refraction_screen_uv, 0.0).rgb;
vec4 reflect_view = VIEW_MATRIX * vec4(reflect(surface_position - CAMERA_POSITION_WORLD, normal), 0.0);
vec2 reflect_uv = screen_space_ray_casting(depth_texture, VERTEX, normalize(reflect_view.xyz), PROJECTION_MATRIX , INV_PROJECTION_MATRIX, 1.0);
vec3 reflect_color = reflect_uv.x > 0.0 ? texture(screen_texture, reflect_uv).rgb : vec3(0.0);
ALBEDO = vec3(0.0);
EMISSION = mix(background_color * underwater_multiplier * exp(-absorption_color*under_water_length), reflect_color, 0.2);
METALLIC = metalic;
ROUGHNESS = roughness;
SPECULAR = specular;
NORMAL = (VIEW_MATRIX * vec4(normal, 0.0)).xyz;
}
今後の課題
SSR内のベクトル計算について
ベクトル計算が本当に正しいのか分かりません……。
上手く動いてそうに見えたので検算していないのですが、やる気が出たら再計算します。
SSRの精度について
現時点でSSRはスクリーン上で5pxずつ移動しながら計算を行っています。ですので時折変な縞模様が発生します。
しかしながら、1pxずつ動かすと処理が重くなります。
処理速度を向上させつつ、精度を上げる為に、例えば「初めは10pxずつ移動して、hit後は二分探索に切り替えて高精度を担保する」のようなプログラムを書いた方が良いかもしれません。
SSRを用いた屈折の計算
屈折にもSSRを用いる事で、より正確な屈折表現を出来るだろうと考えています。
やる気が出たら実装します!
そもそもSSRは必要なのか
反転した世界を映す「仮想のカメラ」を利用する事で、完璧な鏡面を実装できます。これだとSSRで問題となった「底面が見えない」「奥行きが分からない」などの問題が全て解決します。
ひょっとして、SSRは必要ない……?
ま、まあ。正確な屈折を実現するにはSSRが必要なので。「無駄な努力」ではないはず!
最後に
ここまで読んでいただき、本当にありがとうございます!!










