【Unity】Radeonで描画が乱れないシェーダーの書き方

今回はRadeon環境でのシェーダーの挙動を見てみます。今回の記事はRadeonVRの会からRadeon機を提供していただき執筆しています。
- RadeonVRの会: https://discord.gg/XtMhhtw5RK

また、本記事はシェーダー作者やUnityで開発を行っている方、VRChat等のワールドクリエイター向けとなっているため、一般ユーザーの方はやまなみさんの以下の記事が参考になるかと思います。
- [VRChatでRadeonを安定運用する設定集
](https://note.com/yamanami_vr/n/n3c6bab176b2b)

まず結論ですが、一部の例外を除いてシェーダーに関しては基本的にNVIDIAと同じ挙動をします。便宜上記事内ではNVIDIA環境・AMD環境という書き方をしていますが、ドライバのバージョンやモバイル・デスクトップなどの差異、シェーダーAPIの差異などで異なる特性を示す可能性があります。

【2023/04/02追記】
検証環境を更新しました。基本的に以前の検証結果との違いはありませんが、検証中に気づいた点を追記しています。

|項目|詳細|
|-|-|
|CPU|AMD Ryzen 7 5800X3D|
|GPU|AMD Radeon RX ~~6700 XT~~ 7900 XT|
|AMD Software|Adrenalin Edition ~~22.5.1~~ 23.3.2|
|Unity|2021.3.5f1|
|シェーダーAPI|DirectX 11|

# 描画が乱れるパターン
## 何もしないシェーダー
|環境|結果|
|-|-|
|NVIDIA|何も描画されていないように見えます。|
|AMD|一見問題無いように見えますが、頂点シェーダーで`SV_POSITION`が初期化されずランダムな位置に頂点が描画され、かなり激しい乱れ方をします。|

問題の発生するコード
```HLSL
void vert(){}
void frag(){}
```

置き換え例: 明示的に初期化を行います。
```
float4 vert() : SV_POSITION {return float4(0.0,0.0,0.0,0.0);}
float4 frag() : SV_Target {return float4(0.0,0.0,0.0,0.0);}
```

## 一部ポリゴンが描画されない
|環境|結果|
|-|-|
|NVIDIA|予期した通りに描画されます。|
|AMD|一部ポリゴンが描画されません。|

私が制作したものではないためコードは不明で、現在調査中です。該当シェーダーではテッセレーションやジオメトリシェーダーを使用している可能性があります。

## 巨大なポリゴン
ピクセルシェーダー実行前の補間に違いがあるためか、極端に大きなポリゴン ~~（スケールが1000000倍などのレベル）~~ でノイズが発生する場合があります。程度の違いこそありますが、NVIDIA環境でも浮動小数点の精度の問題でUVが乱れたり面がガタガタ揺れるような現象が発生するため極端に大きなポリゴンは使用すべきではなく、いくらか面が分割されたPlaneを利用する必要があります。VRChatにおいては無限遠に広がっている（ように見える）水面などで遭遇するケースがあります。ハードウェア依存の問題なのでシェーダー側の対応としては「ピクセルシェーダーの入力値に極端な値をセットしないように注意する」ぐらいですが、よほど特殊なことをしない限り遭遇しないと思います。

【2023/04/02追記】
小さなスケールでも極端に接近した際にStandard ShaderのSpecularやReflectionがちらついたり線のようなものが見えることを確認しました。そこから更に近づくとノイズが見えてきます。スケールと距離が反比例しているので、SV_POSITIONが極端に大きくなった場合に補間の精度が落ちているように見えます。解像度を変えて確認しましたが結果に違いはありませんでした。また、NVIDIAではちらつきのみ発生し、ノイズは発生しませんでした。

|Quadのスケール|ちらつき始める距離|ノイズが出る距離|
|-|-|-|
|1倍|0.00001|N/A|
|10倍|0.0001|0.00001|
|100倍|0.001|0.0001|
|1000倍|0.01|0.001|
|10000倍|0.1|0.01|

# 四則演算・組み込み関数
以下については特に差異がありませんでした。コンパイラによる最適化を回避するために値は全て変数として渡しています。また、これらの関数はピクセルシェーダー上で実行しています。

## 加算
|式|返り値|
|-|-|
|NaN + -INF|NaN|
|NaN + -1  |NaN|
|NaN + 0   |NaN|
|NaN + 1   |NaN|
|NaN + INF |NaN|
|NaN + NaN |NaN|
|INF + -INF|NaN|
|INF + -1  |INF|
|INF + 0   |INF|
|INF + 1   |INF|
|INF + INF |INF|

## 乗算
|式|返り値|
|-|-|
|NaN * -INF |NaN |
|NaN * -1   |NaN |
|NaN * 0    |NaN |
|NaN * 1    |NaN |
|NaN * INF  |NaN |
|NaN * NaN  |NaN |
|INF * -INF |-INF|
|INF * -1   |-INF|
|INF * 0    |NaN |
|INF * 1    |INF |
|INF * INF  |INF |
|-INF * -INF|INF |
|-INF * -1  |INF |
|-INF * 0   |NaN |
|-INF * 1   |-INF|

## 除算
|式|返り値|
|-|-|
|-1 / 0|-INF|
|0 / 0 |NaN |
|1 / 0 |INF |

## acos
|式|返り値|
|-|-|
|acos(-1.5)|NaN    |
|acos(-1)  |3.14159|
|acos(0)   |1.57073|
|acos(1)   |0.00000|
|acos(1.5) |NaN    |

## asin
|式|返り値|
|-|-|
|asin(-1.5)|NaN         |
|asin(-1)  |-1.57080    |
|asin(0)   |0.0000675916|
|asin(1)   |1.57080     |
|asin(1.5) |NaN         |

## atan
|式|返り値|
|-|-|
|atan(0,0) |NaN     |
|atan(1,0) |1.57080 |
|atan(-1,0)|-1.57080|
|atan(0,1) |0.00000 |
|atan(0,-1)|3.14159 |

## clamp
|式|返り値|
|-|-|
|clamp(-INF,-1,1)|-1.00000|
|clamp(INF,-1,1) |1.00000 |
|clamp(NaN,-1,1) |-1.00000|

## fmod
|式|返り値|
|-|-|
|fmod(-1,0)|NaN|
|fmod(0,0) |NaN|
|fmod(1,0) |NaN|

## lerp
`lerp(a,b,c)`は`a+c*(b-a)`として実行されます。
|式|返り値|
|-|-|
|lerp(0,INF,-1)   |-INF|
|lerp(0,INF,0)    |NaN |
|lerp(0,INF,1)    |INF |
|lerp(-INF,INF,-1)|-INF|
|lerp(-INF,INF,0) |NaN |
|lerp(-INF,INF,1) |NaN |
|lerp(-INF,INF,2) |NaN |

## log
|式|返り値|
|-|-|
|log(-1)  |NaN |
|log(0)   |-INF|
|log10(-1)|NaN |
|log10(0) |-INF|
|log2(-1) |NaN |
|log2(0)  |-INF|

## max
|式|返り値|
|-|-|
|max(NaN,NaN)|NaN     |
|max(NaN,-1) |-1.00000|
|max(NaN,0)  |0.00000 |
|max(NaN,1)  |1.00000 |
|max(-1,NaN) |-1.00000|
|max(0,NaN)  |0.00000 |
|max(1,NaN)  |1.00000 |

## min
|式|返り値|
|-|-|
|min(NaN,NaN)|NaN     |
|min(NaN,-1) |-1.00000|
|min(NaN,0)  |0.00000 |
|min(NaN,1)  |1.00000 |
|min(-1,NaN) |-1.00000|
|min(0,NaN)  |0.00000 |
|min(1,NaN)  |1.00000 |

## pow
|式|返り値|
|-|-|
|pow(-1,-1)|NaN    |
|pow(-1,0) |NaN    |
|pow(-1,1) |NaN    |
|pow(0,-1) |INF    |
|pow(0,0)  |NaN    |
|pow(0,1)  |0.00000|
|pow(1,-1) |1.00000|
|pow(1,0)  |1.00000|
|pow(1,1)  |1.00000|

## rcp
|式|返り値|
|-|-|
|rcp(-1)|-1 |
|rcp(0) |INF|

## rsqrt
|式|返り値|
|-|-|
|rsqrt(-1)|NaN|
|rsqrt(0) |INF|

## saturate
|式|返り値|
|-|-|
|saturate(-INF)|0.00000|
|saturate(NaN) |0.00000|
|saturate(INF) |1.00000|

## sign
|式|返り値|
|-|-|
|sign(-1) |-1.00000|
|sign(0)  |0.00000 |
|sign(1)  |1.00000 |
|sign(NaN)|0.00000 |

## sqrt
|式|返り値|
|-|-|
|sqrt(-1)|NaN|

## step
|式|返り値|
|-|-|
|step(-INF,NaN)|0.00000|
|step(-1,NaN)  |0.00000|
|step(0,NaN)   |0.00000|
|step(1,NaN)   |0.00000|
|step(INF,NaN) |0.00000|
|step(NaN,NaN) |0.00000|
|step(NaN,-INF)|0.00000|
|step(NaN,-1)  |0.00000|
|step(NaN,0)   |0.00000|
|step(NaN,1)   |0.00000|
|step(NaN,INF) |0.00000|

## normalize
normalizeは内部でrsqrtを使用しているため、入力が0の場合は0*INFでNaNになります。
|式|返り値|
|-|-|
|normalize(float3(0,0,0))|NaN|

# 私の所感
~~テッセレーションの「AMDの最適化」など、Radeonは初期設定そのままで運用すると安定しない印象があるため、初期値を安定したものにしてくれると良いかなと思います。~~ また、何もしないシェーダーで描画が乱れる件はドライバ側で0で初期化するか、初期化されていないSV_POSITIONを含むポリゴンをラスタライズしないようにしてくれると良さそうかなと思います。AMDはハードウェア面ではNVIDIAより優秀な印象がありますが、ソフトウェア面の不安定さが足かせとなっている印象です。この辺が改善され安定して利用できるようになるとRyzenのように利用率が上がるかなと思います。

【2023/05/12追記】
テッセレーションを用いたシェーダーが「AMDの最適化」設定でも安定して動作するようになっていました。現時点ではこの設定で壊れるシェーダーには遭遇していません。