[コードリーディング vol.5] Blue Noise Fog（ゴッドレイ）

## 概要

今回はこちらのブログ（[Ray Marching Fog With Blue Noise - The blog at the bottom of the sea](https://blog.demofox.org/2020/05/10/ray-marching-fog-with-blue-noise/)）で紹介されているShadertoyの作品（[Blue Noise Fog ](https://www.shadertoy.com/view/WsfBDf)）のコードリーディングをしてみたいと思います。

実際に動いているシーンはこんな感じ。いわゆる『**ゴッドレイ**』を実現しています。
タイトルはBlue Noise Fogと、ノイズについての話がメイントピックなのですが、個人的にはこの『**ゴッドレイ**』の実装に興味があって読み解きました。

![capture.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5616/065b4811-6326-4ee1-c840-eaf944c2c197.gif)


## コードリーディング

さっそくコードリーディングしていきましょう。
レイマーチングのコードを読む場合は一番下から上に向かって読んでいくと理解がしやすいです。
C言語的な部分もあって関数の宣言前に呼び出すことができないからです。

なので処理は下から上に向かって書かれていることが多いです。


### フラグメントシェーダ（mainImage関数）

ということで最下部の `mainImage` 関数から見ていきましょう。
この関数はShadertoyが提供してくれている、フラグメントシェーダに相当するmain関数です。

```glsl:mainImage関数
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    // get the camera vectors
    vec3 cameraPos, cameraFwd, cameraUp, cameraRight;
    GetCameraVectors(cameraPos, cameraFwd, cameraUp, cameraRight);    
    
    // calculate the ray direction for this pixel
    vec2 uv = fragCoord / iResolution.xy;
    float aspectRatio = iResolution.x / iResolution.y;
    int panel = 0;
    vec3 rayDir;
    {   
        panel = int(dot(floor(uv*2.0f), vec2(1.0f, 2.0f)));
        
        vec2 screen = fract(uv*2.0f) * 2.0f - 1.0f;
        screen.y /= aspectRatio;
                
        float cameraDistance = tan(c_FOV * 0.5f * c_pi / 180.0f);       
        rayDir = vec3(screen, cameraDistance);
        rayDir = normalize(mat3(cameraRight, cameraUp, cameraFwd) * rayDir);
    }
    
    // do rendering for this pixel
    float rayHitTime;
    vec3 pixelColor = GetColorForRay(cameraPos, rayDir, rayHitTime);
    
    // apply fog
    pixelColor = ApplyFog(cameraPos, rayDir, pixelColor, rayHitTime, panel, fragCoord);
    
    // tone map the color to bring it from unbound HDR levels to SDR levels
    //pixelColor = ACESFilm(pixelColor);
    
    // convert to sRGB, then output
    pixelColor = LinearToSRGB(pixelColor);
    fragColor = vec4(pixelColor, 1.0f);        
}
```

冒頭部分はカメラの姿勢の計算と `Ray`の飛ばす方向を決めています。
なぜこの計算でそうなるのかは今回は本筋から外れるので割愛します。

関数部分は以下になっています。


### カメラの姿勢計算（GetCameraVectors関数）

カメラの姿勢を取得する関数です。

```glsl:GetCameraVectors関数
void GetCameraVectors(out vec3 cameraPos, out vec3 cameraFwd, out vec3 cameraUp, out vec3 cameraRight)
{   
    vec2 mouse = iMouse.xy;
    if (dot(mouse, vec2(1.0f, 1.0f)) == 0.0f)
    {
        mouse = c_defaultMousePos * iResolution.xy;    
    }
    
    float angleX = -mouse.x * 16.0f / float(iResolution.x);
    float angleY = mix(c_minCameraAngle, c_maxCameraAngle, mouse.y / float(iResolution.y));
    
    cameraPos.x = sin(angleX) * sin(angleY) * c_cameraDistance;
    cameraPos.y = -cos(angleY) * c_cameraDistance;
    cameraPos.z = cos(angleX) * sin(angleY) * c_cameraDistance;
    
    cameraPos += c_cameraAt;
    
    cameraFwd = normalize(c_cameraAt - cameraPos);
    cameraRight = normalize(cross(cameraFwd, vec3(0.0f, 1.0f, 0.0f)));
    cameraUp = normalize(cross(cameraRight, cameraFwd));   
}
```

`sin`, `cos` 関数を使って衛星軌道上のような動きをさせていますね。
加えてマウス操作で色々な方向からシーンを見ることができるようになっています。

### レイマーチングで描く（GetColorForRay関数）

名前からも分かる通りオブジェクトの色を算出するための関数です。
通常のレイマーチングを行ってオブジェクトをレンダリングしています。

この関数だけでレンダリングすると以下のような結果を得ることができます。
![ゴッドレイを適用しない絵](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5616/4c83538f-e847-3665-cac6-443499b5cd2a.png)
いわゆる普通のレイマーチングの画面ですね。

レイマーチングについては以前記事を書いているのでそちらを参考にしてください。

- [[GLSL] レイマーチング入門 vol.1](https://qiita.com/edo_m18/items/034665d42c562da88cb6)

ではひとつずつ見ていきましょう。まずは関数全体を俯瞰します。

```glsl:GetColorForRay関数
vec3 GetColorForRay(in vec3 rayPos, in vec3 rayDir, out float hitDistance)
{
    // trace primary ray
    SRayHitInfo hitInfo = RayVsScene(rayPos, rayDir);
    
    // set the hitDistance out parameter
    hitDistance = hitInfo.dist;
    
    if (hitInfo.dist == c_rayMaxDist)
        return texture(iChannel0, rayDir).rgb;
    
    // trace shadow ray
    vec3 hitPos = rayPos + rayDir * hitInfo.dist;
    hitPos += hitInfo.normal * c_hitNormalNudge;
    SRayHitInfo shadowHitInfo = RayVsScene(hitPos, c_lightDir);
    float shadowTerm = (shadowHitInfo.dist == c_rayMaxDist) ? 1.0f : 0.0f;
    
    // do diffuse lighting
    float dp = clamp(dot(hitInfo.normal, c_lightDir), 0.0f, 1.0f);
    return c_lightAmbient * hitInfo.diffuse + dp * hitInfo.diffuse * c_lightColor * shadowTerm;
}
```

この関数からさらに `RayVsScene` という関数が呼び出されています。
これはシーン内にあるオブジェクトを見つけてくる関数です。（要はレイマーチングさせるための関数）

戻り値は `SRayHitInfo` 構造体になっていて、この構造体にはレイがヒットしたときの情報が格納されています。

関数の定義は以下のようになっています。

```glsl:SRayHitInfo構造体の定義
struct SRayHitInfo
{
    float dist;
    vec3 normal;
    vec3 diffuse;
};
```

定義されているフィールドは3つで、距離、法線、拡散を返すようになっていますね。
距離はレイマーチングした際の距離、法線はヒットしたオブジェクトの法線、そして拡散はそのオブジェクトの色です。

`RayVsScene` 関数の中身については後ほど見るとして、まずは全体的になにをしているのか見ていきましょう。


#### 背景のレンダリング

```glsl:CubeMapサンプリング
if (hitInfo.dist == c_rayMaxDist)
    return texture(iChannel0, rayDir).rgb;
```

冒頭の `RayVsScene` の戻り値を元に `if` 文によって分岐しています。
これは単に、レイがどこにもヒットしていない場合 [^1] にCubeMapの値をサンプリングして終了しています。
要は**背景のレンダリング**ですね。

[^1]: ヒット位置の距離が `c_rayMaxDist` ならどのオブジェクトにもヒットせずにレイが進んだということ。


#### 影のトレース

続けて見ていきましょう。
コメントにもあるように次の処理は影のトレースです。
さらに `RayVsScene` が呼び出されているのが分かります。

```glsl:影のトレース
vec3 hitPos = rayPos + rayDir * hitInfo.dist;
hitPos += hitInfo.normal * c_hitNormalNudge;
SRayHitInfo shadowHitInfo = RayVsScene(hitPos, c_lightDir);
float shadowTerm = (shadowHitInfo.dist == c_rayMaxDist) ? 1.0f : 0.0f;
```

1行目の処理は前段の計算で得られた結果を元にレイが現在あたっている位置を求める処理です。
`rayPos` はカメラ位置なので、**レイの進行方向 x ヒット距離**を足すことで位置を求めているわけです。

```glsl:影計算のためのレイの位置を求める
vec3 hitPos = rayPos + rayDir * hitInfo.dist;
```

次に行っているのはヒットしたオブジェクトの法線方向に少しだけ位置をずらしています。

```glsl:法線方向に少しだけ移動させる
hitPos += hitInfo.normal * c_hitNormalNudge;
```

なぜこれをしているかというと、次の行でライト方向に再びレイマーチングを開始しますがオブジェクトの表面にレイ位置があるままだとレイマーチングの性質上レイが一切進まなくなってしまうため、それを防ぐ目的で少しだけオフセットさせているというわけです。
（ちなみに `c_hitNormalNudge` の値は定数で `0.1` に設定されています）

イメージ化するとこんな感じ↓
![法線方向へのオフセット](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5616/f77d04da-9fe4-8b70-e4a8-12259c5fe6f7.png)

なぜレイの位置をオフセットしないとレイの位置が更新されないのかについてはここでは解説しません。
以前自分が書いた[レイマーチング入門](https://qiita.com/edo_m18/items/034665d42c562da88cb6)の記事などレイマーチングの仕組みを理解すると分かるかと思います。


#### 影の計算

レイの出発点が決まったのでこれを元に影の計算をしていきます。

```glsl:影の計算
SRayHitInfo shadowHitInfo = RayVsScene(hitPos, c_lightDir);
float shadowTerm = (shadowHitInfo.dist == c_rayMaxDist) ? 1.0f : 0.0f;
```

再び `RayVsScene` が呼び出されています。
今度は引数が異なっていますね。最初の呼び出しは `RayVsScene(rayPos, rayDir);` でした。
今回の呼び出しは `RayVsScene(hitPos, c_lightDir);` となっています。

`RayVsScene` 関数が行っているのは引数に渡された**位置から指定方向へ**レイマーチングさせることです。
なぜこれで影の計算ができるのかは今回の主旨からはずれるので少しだけ解説するにとどめます。

----------------------------

もし詳細を知りたい方は以下の記事を読むとより理解が深まるでしょう。

- [レイマーチングソフトシャドウ - wgld.org](https://wgld.org/d/glsl/g020.html)

----------------------------

なぜこれで影の計算が行えるのかというと。
影というのは**ライトに遮られた箇所にできる**という視点に立つと分かります。
つまり、まず最初のレイマーチングでオブジェクトの位置を特定したあと、そこから**ライト方向に向かって**レイを飛ばすことでオブジェクトとライトとの間になにか遮蔽するものがあるかどうかを判定することができるわけです。

上記サイトから画像を引用させていただくと以下のイメージです。

![レイマーチングによるシャドウ表現イメージ](https://wgld.org/i/site/g020_02.jpg)

そして最後に、判定した値が `c_rayMaxDist` と同じかどうか、言い換えると**レイがどのオブジェクトにも当たらなかったか**をチェックし、当たっていなければ `1` を、当たっていれば `0` を設定します。

この `shadowTerm` の値が使われている箇所を見るとどういう処理かが分かると思います。

```glsl:shadowTermの使いみち
return c_lightAmbient * hitInfo.diffuse + dp * hitInfo.diffuse * c_lightColor * shadowTerm;
```

色の計算の最後に掛けられていますね。これが `1` or `0` なのですから、つまりは**影なら色をレンダリングしない（＝黒）**ということになります。
これで影がレンダリングされます。


## ゴッドレイのレンダリング

今回のコードリーディングの本丸、**ゴッドレイ**の実装部分を見ていきましょう。
ゴッドレイをレンダリングしている関数は `ApplyFog` です。

まずはさっと実装を見てみましょう。

```glsl:ApplyFog関数
// ray march from the camera to the depth of what the ray hit to do some simple scattering
vec3 ApplyFog(in vec3 rayPos, in vec3 rayDir, in vec3 pixelColor, in float rayHitTime, in int panel, in vec2 pixelPos)
{         
    // Offset the start of the ray between 0 and 1 ray marching steps.
    // This turns banding into noise.
    int frame = 0;
    #if ANIMATE_NOISE
    	frame = iFrame % 64;
    #endif
    
    float startRayOffset = 0.0f;
    if (panel == 0)
    {
        startRayOffset = 0.5f;
    }
    else if (panel == 1)
    {
        // white noise
        startRayOffset = hash13(vec3(pixelPos, float(frame)));
    }
    else if (panel == 2)
    {
        // blue noise
        startRayOffset = texture(iChannel1, pixelPos / 1024.0f).r;
        startRayOffset = fract(startRayOffset + float(frame) * c_goldenRatioConjugate);
    }    
    else if (panel == 3)
    {
        // interleaved gradient noise
        startRayOffset = InterleavedGradientNoise(pixelPos, frame);
    }
    
    // calculate how much of the ray is in direct light by taking a fixed number of steps down the ray
    // and calculating the percent.
    // Note: in a rasterizer, you'd replace the RayVsScene raytracing with a shadow map lookup!
    float fogLitPercent = 0.0f;
    for (int i = 0; i < c_numRayMarchSteps; ++i)
    {
        vec3 testPos = rayPos + rayDir * rayHitTime * ((float(i)+startRayOffset) / float(c_numRayMarchSteps));
        SRayHitInfo shadowHitInfo = RayVsScene(testPos, c_lightDir);
        fogLitPercent = mix(fogLitPercent, (shadowHitInfo.dist == c_rayMaxDist) ? 1.0f : 0.0f, 1.0f / float(i+1));
    }
    
    vec3 fogColor = mix(c_fogColorUnlit, c_fogColorLit, fogLitPercent);
    float absorb = exp(-rayHitTime * c_fogDensity);
    return mix(fogColor, pixelColor, absorb);
}
```

ぱっと見、長めに見えますが実は最初の分岐は4分割されたスクリーンそれぞれの設定を行っているだけです。
なので着目すべき部分は以下の箇所になります。

```glsl
// calculate how much of the ray is in direct light by taking a fixed number of steps down the ray
// and calculating the percent.
// Note: in a rasterizer, you'd replace the RayVsScene raytracing with a shadow map lookup!
float fogLitPercent = 0.0f;
for (int i = 0; i < c_numRayMarchSteps; ++i)
{
    vec3 testPos = rayPos + rayDir * rayHitTime * ((float(i)+startRayOffset) / float(c_numRayMarchSteps));
    SRayHitInfo shadowHitInfo = RayVsScene(testPos, c_lightDir);
    fogLitPercent = mix(fogLitPercent, (shadowHitInfo.dist == c_rayMaxDist) ? 1.0f : 0.0f, 1.0f / float(i+1));
}

vec3 fogColor = mix(c_fogColorUnlit, c_fogColorLit, fogLitPercent);
float absorb = exp(-rayHitTime * c_fogDensity);
return mix(fogColor, pixelColor, absorb);
```

順番に見ていきましょう。
ちなみにコメントでは以下のように説明されていますね。

> calculate how much of the ray is in direct light by taking a fixed number of steps down the ray
> and calculating the percent.
> Note: in a rasterizer, you'd replace the RayVsScene raytracing with a shadow map lookup!
>
>[訳] 固定長のステップで進めたレイがどれくらいの光（direct light）の中にいるかを計算し、そしてパーセンテージを計算します。
> 　　メモ：ラスタライザでは、RayVsSceneのレイトレーシングをシャドウマップのルックアップに置き換えることができます。

コードを詳細に読んでいく前に、ここがなにを説明しているかを想像しておきましょう。
以前書いた記事に載せた画像を引用するとおそらく以下のようなことをやるのだと思います。

![クラウドレイマーチングの解説図](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.amazonaws.com%2F0%2F5616%2Fe9ddd0a7-6090-8285-53e8-f48e5a40552a.jpeg?ixlib=rb-1.2.2&auto=format&gif-q=60&q=75&w=1400&fit=max&s=b3e570304c523ce69239379198e6d94e)

**上図は雲のライティングを行う際に計算する仕組みを図示したもの**です。
この図が言っているのは、カメラからレイを飛ばす際にレイを**決まった長さ分だけ進めて**そのときの雲の密度を計算するというものです。
要はレイが進む各点を観測点として、その点の部分の値を累積させることで陰影を出すわけです。

そしておそらくゴッドレイでも同様に、固定長でレイを進めてその**レイが光の中を進んでいるのかそれとも影の部分を進んでいるのか**を累積することで陰影を作っているのだと思います。

- 参考：[[コードリーディング vol.2] レイマーチングによる雲表現を読み解く](https://qiita.com/edo_m18/items/cbba0cc4e33a5aa3be55#%E3%81%9F%E3%81%A0%E3%81%97%E5%AF%86%E5%BA%A6%E3%82%92%E8%A8%88%E7%AE%97%E3%81%97%E3%81%A6%E3%81%84%E3%82%8B)



## ApplyFog関数を読み解く

では実際にコードを読んでいきましょう。

まずは以下の部分から。

```glsl:ApplyFogのメインの計算部分
float fogLitPercent = 0.0f;
for (int i = 0; i < c_numRayMarchSteps; ++i)
{
    vec3 testPos = rayPos + rayDir * rayHitTime * ((float(i)+startRayOffset) / float(c_numRayMarchSteps));
    SRayHitInfo shadowHitInfo = RayVsScene(testPos, c_lightDir);
    fogLitPercent = mix(fogLitPercent, (shadowHitInfo.dist == c_rayMaxDist) ? 1.0f : 0.0f, 1.0f / float(i+1));
}
```

`fogLitPercent` はコメントで解説されている**パーセンテージ**を表す変数でしょう。
続く `for` ループで実際の計算を行っています。

`c_numRayMarchSteps` は定数で `16` が設定されています。
つまりゴッドレイ用のレイマーチングを16回繰り返して累積させているというわけですね。

そのまま中の処理を見ていきます。

`rayHitTime` は「タイム」と名前がついていますが実際には距離が格納されています。
`GetColorForRay` 関数の `out float hitDistance` 変数がその正体です。

実装部分を見てみると以下のようになっています。

```glsl:rayHitTimeの正体
vec3 GetColorForRay(in vec3 rayPos, in vec3 rayDir, out float hitDistance)
{
    // trace primary ray
    SRayHitInfo hitInfo = RayVsScene(rayPos, rayDir);
    
    // set the hitDistance out parameter
    hitDistance = hitInfo.dist;

    // ... 後略
}
```

通常のレイマーチングをしてレイがヒットしたオブジェクトまでの距離が入っているということですね。
それを念頭にコードを見てみましょう。

```glsl
vec3 testPos = rayPos + rayDir * rayHitTime * ((float(i)+startRayOffset) / float(c_numRayMarchSteps));
```

`startRayOffset` はノイズによるゆらぎを表しているので、いったんこれをなくします。
さらに読みやすくするために `float` へのキャストも外してみると、

```glsl
vec3 testPos = rayPos + rayDir * rayHitTime * (i / c_numRayMarchSteps);
```

`i / c_numRayMarchSteps` はループが進むにつれてレイが伸びていくことを示しています。
つまり日本語で説明すると、

> オブジェクトがある場所までの距離をループ回数で割って、ループするたびにレイの位置を伸ばしていく

ということです。
図にすると以下のイメージです。

![オブジェクトまでの距離を分割したイメージ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5616/a53ff6d1-0b84-348e-6695-65537ec8cd2a.png)
さらに続きのコードを見ていきましょう。

```glsl
SRayHitInfo shadowHitInfo = RayVsScene(testPos, c_lightDir);
```

再び `RayVsScene` 関数が使われています。そして今回の場合の引数は、先ほど求めたレイの位置と**ライト方向**へのベクトルを渡して計算を行っています。
戻り値の意味は、**計算時点でのレイの位置からライト方向に遮蔽物があるかないか**を求めていることになりますね。

そしてその結果を使って**パーセンテージ**を求めます。
計算式は以下。

```glsl
fogLitPercent = mix(fogLitPercent, (shadowHitInfo.dist == c_rayMaxDist) ? 1.0f : 0.0f, 1.0f / float(i+1));
```

三項演算子があるので分解してみましょう。

```glsl
float inShadow = (shadowHitInfo.dist == c_rayMaxDist) ? 1.0f : 0.0f;
float progress = 1.0f / float(i + 1);
fogLitPercent = mix(fogLitPercent, inShadow, progress);
```

まず、計算時点のレイの位置からライト方向へのレイマーチングの結果がなにも遮蔽されていない場合、 `inShadow` 変数は `1.0` になり、なにかに遮蔽されている場合は `0.0` になります。
求めたいのは**なにかに遮蔽されているところをレイが進んでいるか**なのでそのために遮蔽を見ているというわけですね。

そして `progress` は `i` の値が増えるにつれてどんどんその値が小さくなっていきます。
グラフにすると以下のような曲線になります。

■ $\frac{1}{x + 1}$ のグラフ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5616/7497725c-66f0-32fe-d9fb-b01ae24390d8.png)

ここから分かるのはループが進むほど `inShadow` で計算された値の影響度が下がっていくということです。
`mix` 関数は第一引数と第二引数を第三引数の割合に応じて線形補間するものです。

つまり、ループが進むほど値が `0` に近づくため `inShadow` の値の影響度が小さくなっていく、というわけですね。

### 求めた `fogLitPercent` を使ってゴッドレイを表現する

最後に、今求めた `fogLitPercent` を利用して最終的な色を決定します。（つまりゴッドレイの表現）
計算式は以下です。

```glsl
vec3 fogColor = mix(c_fogColorUnlit, c_fogColorLit, fogLitPercent);
float absorb = exp(-rayHitTime * c_fogDensity);
return mix(fogColor, pixelColor, absorb);
```

`fogColor` は変数名の通り**フォグの色**ですね。
定数として定義されているUnlitなフォグの色とLitなフォグの色を `fogLitPercent` の値によってブレンドしています。

※ ちなみにそれぞれの色は完全な黒と白が定義されています。

```glsl:UnlitとLitの色定義
const vec3 c_fogColorLit = vec3(1.0f, 1.0f, 1.0f);
const vec3 c_fogColorUnlit = vec3(0.0f, 0.0f, 0.0f);
```

つまり、レイが通過していく直線がどのオブジェクトにも遮られていない場合は `c_fogColorLit` が100%採用される＝白くなるというわけですね。
![レイが進んでLit/Unlitが採用されるイメージ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5616/84bc5a79-dab9-5f29-801c-8d3f9244beea.png)


### 光の吸収を計算

次の計算は（変数名から察するに）光の吸収についての計算でしょう。

```glsl:光の吸収
// const float c_fogDensity = 0.002f;
float absorb = exp(-rayHitTime * c_fogDensity);
```

`exp(-rayHitTime * c_fogDensity);` の計算はレイの距離が大きくなればなるほど `0` に近づいていく計算です。
つまり言い換えると、なにかしらのオブジェクトにヒットしている場合は `rayHitTime` の値は小さい値になっているはずなので `pixelColor` の色が強く影響し、そうでない場合は `fogColor` の色が強く影響する、というわけです。

試しにゴッドレイを適用しないときの絵とゴッドレイだけの絵を比べてみると分かりやすいかと思います。

**ゴッドレイを適用しない絵（ `absorb` が `1.0` の絵）**
![ゴッドレイを適用しない絵](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5616/4c83538f-e847-3665-cac6-443499b5cd2a.png)


**ゴッドレイだけの絵（ `absorb` が `0.0` の絵）**
![ゴッドレイだけの絵](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5616/41f94ee9-2d6d-93af-9a14-19f1f910bfa9.png)


## 色の決定

以上でロジック部分は終了です。
最後に色を返しているところを見て終わりましょう。

```glsl:色のリニア化
pixelColor = LinearToSRGB(pixelColor);
fragColor = vec4(pixelColor, 1.0f);
```

`LinearToSRGB` 関数は名前から察するに色をリニアに変換しているのでしょう。
いわゆるガンマ補正をなくす処理ですね。

ガンマ補正については以下の記事が詳しいです。

- [ガンマ補正のうんちく](https://qiita.com/yoya/items/122b93970c190068c752)

ただガンマ補正は `1 / 2.2` なので若干値が異なり、さらに細かい数値で補正しているのが気になりますね。
これについては後日調べてみたいと思います。
ここではたんに色味を調整していると考えておけば大丈夫だと思います。

```glsl:LinearToSRGB関数
vec3 LinearToSRGB(vec3 rgb)
{
    rgb = clamp(rgb, 0.0f, 1.0f);
    
    return mix(
        pow(rgb * 1.055f, vec3(1.f / 2.4f)) - 0.055f,
        rgb * 12.92f,
        LessThan(rgb, 0.0031308f)
    );
}
```

そして変換を行った色を `fragColor` に設定して最終的な色を決定しています。


## イチから実装してみる

今回コードリーディングした内容を踏まえて自分でイチから実装してみました。
結果は以下です。
[![godray-capture.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5616/738f85b3-f415-3a49-13c4-f5bea4f82906.gif)](https://www.shadertoy.com/view/tt2fR3)
- [Godray fog](https://www.shadertoy.com/view/tt2fR3)

いくつかパラメータを調整しましたが概ねうまく動きました。
やはり光の表現が加わるとぐっといい絵になりますね。


## まとめ

読み解いてみると意外とシンプルな実装でした。
今回これを知ったのは [@VoxelKei](https://twitter.com/VoxelKei) さんがUnityでボリュームライトを実装していて質問したところ、この記事を教えてもらったのがきっかけでした。
[@VoxelKei](https://twitter.com/VoxelKei) さんのボリュームライトは[BOOTHで販売](https://voxelkei.booth.pm/items/2098480)されているのでそちらを購入することで実装を見ることができます。

基本的には今回読み解いたものをベースとしつつ、Unityはレイマーチングではないので代わりに深度を求める方法を採用してい実装されていました。
興味がある方は購入してコードを読んでみると学びがあると思います。

ちなみにこんな感じのものです↓
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">誰でもどこでも神になれるライト「VoxkeVolumetricLight」をBOOTHで公開しました。スクリプトが使えないVRChatでもボリューメトリックなライトが使えるパッケージです。詳しい使い方説明はFANBOXに投稿しました。デモワールドもあります。買ってね！！！<a href="https://twitter.com/hashtag/VRChat?src=hash&amp;ref_src=twsrc%5Etfw">#VRChat</a> <a href="https://t.co/ukfGwtWB9H">https://t.co/ukfGwtWB9H</a> <a href="https://t.co/3HesjvuEzc">pic.twitter.com/3HesjvuEzc</a></p>&mdash; VoxelKei (@VoxelKei) <a href="https://twitter.com/VoxelKei/status/1264830072894009346?ref_src=twsrc%5Etfw">May 25, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>