Sunday, October 22, 2017

[Vulkan] GeForceでスワップチェーンはRGBAでは無くBGRA

グラフィックスAPIにおいて紛らわしかったカラーチャンネルの並びの問題ですが、DirectX11やDirectX12ではRGBAだけ使えば良く、BGRAは忘れても良い存在になっていました。ところが、GeForceやIntelの統合型GPUのVulkanではスワップチェーンがBGRAでなければいけないようです。

vkGetPhysicalDeviceSurfaceFormatsKHR関数で列挙したと思われるリストが、gpuinfo.orgにあります。(Surfaceタブ→Surface formatsタブ)
https://vulkan.gpuinfo.org/displayreport.php?id=1895#surface - GTX 1050
https://vulkan.gpuinfo.org/displayreport.php?id=1922#surface - Mali-G71
https://vulkan.gpuinfo.org/displayreport.php?id=1945#surface - Radeon RX 560
https://vulkan.gpuinfo.org/displayreport.php?id=1973#surface - Adreno 540

AdrenoやMaliは逆にRGBAのみのサポートで、RadeonはRGBAとBGRAの両方をサポートしていることがわかります。

レンダーターゲットのフォーマットはRenderPassに含まれ、RenderPassはPSOが参照します。スワップチェーンに直接描画するPSOはデバイスに合わせてRGBAかBGRAかどちらかで生成されたRenderPassを指定しなければいけないことになります。

中間バッファにBGRAを使うことは無いはずなので、結局Vulkanでは同じシェーダでも「描画対象がスワップチェーンか中間バッファか」によって出来上がるPSOが別物になると考えなければいけません。

PSO生成の際のパラメータが増えて管理が煩雑になりそうですが、実際の所スワップチェーンに直接描画するものといえばUIかポストプロセスくらいなので、場合分けは難しくないと思います。

Sunday, October 15, 2017

[Vulkan] RadeonでVK_FORMAT_D24_UNORM_S8_UINTのdepth stencilテクスチャが作れない

近年多く使われていたD24_UNORM_S8_UINTのdepth stencilテクスチャはPCのVulkanで生成できないケースがあります。例えば、Radeon 560 RXでは、D24_UNORM_S8_UINTが未サポートです。代わりにD32_SFLOAT_S8_UINTを使わなければいけません。
https://vulkan.gpuinfo.org/displayreport.php?id=1945#formats

GTX 1050の場合はD24_UNORM_S8_UINTがサポートされています。
https://vulkan.gpuinfo.org/displayreport.php?id=1895#formats

DirectX12ではRadeon 560 RXでもDXGI_FORMAT_D24_UNORM_S8_UINTが作れましたが、内部でエミュレートしていると思われます。

Vulkanといえば、モバイルデバイスも気になるところですが、Adreno 540、Mali-G71共に、D24_UNORM_S8_UINTしか対応していないようです。Radeonとは逆ですね。
https://vulkan.gpuinfo.org/displayreport.php?id=1925#formats
https://vulkan.gpuinfo.org/displayreport.php?id=1922#formats
いずれのプラットフォームもD32_SFLOATは存在しているので、stencilを使わないならD32_SFLOAT、stencilを使いたいならプラットフォーム毎に分岐しなければいけなさそうです。

Monday, October 9, 2017

ゲルストナー波の法線を計算する

前回に引き続き、ゲルストナー波をやります。

ゲルストナー波の法線を計算してみます。安直な方法としては、CalcGerstnerWaveOffset関数が波のあらゆる地点の頂点位置の移動量を求められるものであることを利用し、x方向とz方向に1センチずらしてそれぞれCalcGerstnerWaveOffsetを呼び、これを三角形に見立てて法線を求めます。

float3 ofsByGerstnerWave = CalcGerstnerWaveOffset(vertexPosition);
float3 dx = float3(0.01, 0, 0) + CalcGerstnerWaveOffset(vertexPosition + float3(0.01, 0, 0));
float3 dz = float3(0, 0, 0.01) + CalcGerstnerWaveOffset(vertexPosition + float3(0, 0, 0.01));
float3 N = normalize(cross(dz - ofsByGerstnerWave, dx - ofsByGerstnerWave));
これでも実用上問題無いレベルになるとは思いますが、GPU GemsのEffective Water Simulation from Physical Modelsによると、Xで偏微分するとBinormal、Yで偏微分するとTangent、それらの外積からNormalを求めることができます。親切にもNormal, Binormal, Tangentそれぞれの式がEquation 10~12として提示されていますので、そのままHLSL化してみます。ただし、Island11はYが上、論文ではZが上なので、YとZを入れ替えてあります。そのせいで、Normal = Binormal X Tangentとはならず、Normal = Tangent X Binormalになっています。

float3 CalcGerstnerWaveNormal(float3 P)
{
float3 normal = float3(0, 1, 0);
[unroll]
for (int i = 0; i < numWaves; i++)
{
Wave wave = waves[i];
float wi = 2 / wave.waveLength;
float WA = wi * wave.amplitude;
float phi = speed * wi;
float rad = wi * dot(wave.dir, P.xz) + phi * g_time;
float Qi = steepness / (wave.amplitude * wi * numWaves);
normal.xz -= wave.dir * WA * cos(rad);
normal.y -= Qi * WA * sin(rad);
}
return normalize(normal);
}
float3 CalcGerstnerWaveBinormal(float3 P)
{
float3 binormal = float3(1, 0, 0);
[unroll]
for (int i = 0; i < numWaves; i++)
{
Wave wave = waves[i];
float wi = 2 / wave.waveLength;
float WA = wi * wave.amplitude;
float phi = speed * wi;
float rad = wi * dot(wave.dir, P.xz) + phi * g_time;
float Qi = steepness / (wave.amplitude * wi * numWaves);
binormal.x -= Qi * wave.dir.x * wave.dir.x * WA * sin(rad);
binormal.z -= Qi * wave.dir.x * wave.dir.y * WA * sin(rad);
binormal.y += wave.dir.x * WA * cos(rad);
}
return normalize(binormal);
}
float3 CalcGerstnerWaveTangent(float3 P)
{
float3 tangent = float3(0, 0, 1);
[unroll]
for (int i = 0; i < numWaves; i++)
{
Wave wave = waves[i];
float wi = 2 / wave.waveLength;
float WA = wi * wave.amplitude;
float phi = speed * wi;
float rad = wi * dot(wave.dir, P.xz) + phi * g_time;
float Qi = steepness / (wave.amplitude * wi * numWaves);
tangent.x -= Qi * wave.dir.x * wave.dir.y * WA * sin(rad);
tangent.z -= Qi * wave.dir.y * wave.dir.y * WA * sin(rad);
tangent.y += wave.dir.y * WA * cos(rad);
}
return normalize(tangent);
}

あえてくどくどと関数を分けてありますが、論文で使われている式や変数名をなるべく残すためで、現場で使う場合は1ループで一気にfloat3x3の回転行列まで求めると良いと思います。

Island11のドメインシェーダの既存の法線はそのままワールド空間の法線として使われていましたが、それをゲルストナー波の接空間(tangent space)とみなしてワールド空間に配置します。

float3 gerstnerWaveN = CalcGerstnerWaveNormal(vertexPosition + ofsByGerstnerWave);
float3 gerstnerWaveB = CalcGerstnerWaveBinormal(vertexPosition + ofsByGerstnerWave);
float3 gerstnerWaveT = CalcGerstnerWaveTangent(vertexPosition + ofsByGerstnerWave);
float3x3 rotator;
rotator[0] = gerstnerWaveB;
rotator[1] = gerstnerWaveN;
rotator[2] = gerstnerWaveT;
output.normal = mul(water_normal.xyz, rotator);

次回は水面に物を浮かべてみます。

Saturday, October 7, 2017

HLSLでゲルストナー波を実装

NVIDIAのIsland11サンプルを改造してゲルストナー波を加えてみました。

Copyright (c) 2011 NVIDIA Corporation.


ゲルストナー波の実装例はいくつかのサイトで紹介されていますが、多くはGPU GemsのEffective Water Simulation from Physical ModelsのEquation 9の式が引用されています。

以下はこの式をHLSLで実装してみたものです。
struct Wave
{
float2 dir;
float amplitude;
float waveLength;
};
cbuffer cb2 : register(b2)
{
Wave waves[100];
};
static int numWaves = 20;
static float steepness = 0.8;
static float speed = 15;
float3 CalcGerstnerWaveOffset(float3 v)
{
float3 sum = float3(0, 0, 0);
[unroll]
for (int i = 0; i < numWaves; i++)
{
Wave wave = waves[i];
float wi = 2 / wave.waveLength;
float Qi = steepness / (wave.amplitude * wi * numWaves);
float phi = speed * wi;
float rad = dot(wave.dir, v.xz) * wi + g_time * phi;
sum.y += sin(rad) * wave.amplitude;
sum.xz += cos(rad) * wave.amplitude * Qi * wave.dir;
}
return sum;
}
引数のvで海面の頂点座標を受け取り、その位置においてどれだけ頂点を移動させればいいのかオフセットを返しています。C++側から乱数で生成した100個の波のパラメ ータを受け取っていますが、試行錯誤の結果20個だけ使っています。

C++でパラメータを生成するコードは以下です。

struct Wave
{
Vec2 dir;
float amplitude;
float waveLength;
};
struct ImmutableCB
{
Wave waves[100];
} cb;
for (int i = 0; i < dimof(cb.waves); i++)
{
Wave& w = cb.waves[i];
float randomRad = (float)(Random() * M_PI * 2 * 0.3f);
w.dir.x = sinf(randomRad);
w.dir.y = cosf(randomRad);
w.amplitude = 0.03f + powf(2.0f, (float)Random() * 2.0f) * 0.05f;
w.waveLength = 1.0f + powf(2.f, 1.f + (float)Random()) * 10.f;
}
view raw make_waves.cpp hosted with ❤ by GitHub
randomRadの計算で、最後の0.3fを無くすと全方向への波をランダムに生成することになります。0.3を掛けてある程度似た方向に向けて生成するようにしています。

amplitudeは波の高さで、waveLengthは波の幅です。特に決まった式があるわけではなく、見た目から数値や式を適当に選んだ結果です。ただし、GPU GemsによるとwaveLengthは中央値を基準に半分の長さから2倍の長さの波を作るといいと書いてあります。

下のgifはオリジナルのIsland11の波です。RGBをノーマル、Aをハイトマップとしたテクスチャを元に頂点を移動するだけというシンプルなアルゴリズムですが、このテクスチャの出来がいいのでとてもきれいです。

Copyright (c) 2011 NVIDIA Corporation.


波による頂点移動処理はドメインシェーダ内あり、そこをゲルストナー波に置き換えてみます。うねる波が表現できた半面、海らしい荒さが足りない気がします。

Copyright (c) 2011 NVIDIA Corporation.


そこで、Island11のオリジナルの波と合成してみると、それなりに見られるものになりました。結局ゲルストナー波単体できれいな絵はできず、さざ波や白波の表現も工夫していく必要がありそうです。

Copyright (c) 2011 NVIDIA Corporation.


次回は法線を求めてみます。

References:
Effective Water Simulation from Physical Models
Ocean Shader with Gerstner Waves
GERSTNER WAVE IMPLEMENTATION
DX11 Samples