Saturday, August 22, 2015

DDS自力ロード(Cube Map, Mipmap, DXT1, DXT3, DXT5対応)

Windows10が提供されて誰でもDirectX12による開発ができるようになりました。しかし、DirectXTKやDirectXTexのようなテクスチャのロードが出来るライブラリがまだマイクロソフトから提供されていないようです。DDSは特に複雑なフォーマットではないはずなので、DX12の準備を兼ねて自力でロードを試みてみます。ただし、DirectX11です。(DX12も対応しました。

以下でほぼ全部です。キューブマップ、ミップマップ、圧縮/非圧縮すべて対応しています。エラーチェックはちゃんとしていないので壊れたファイルや未対応の形式でバッファーオーバーランなどの危険があるかもしれません。

struct DDSHeader {
uint32_t h3[3];
int h, w;
uint32_t h2[2];
int mipCnt;
uint32_t h13[13];
uint32_t fourcc, bitsPerPixel, rMask, gMask, bMask, aMask, caps1, caps2;
bool IsCubeMap() const { return caps2 == 0xFE00; }
int GetArraySize() const { return IsCubeMap() ? 6 : 1; }
int GetMipCnt() const { return std::max(mipCnt, 1); }
};
static void ArrangeRawDDS(void* img, int size)
{
const DDSHeader* hdr = (DDSHeader*)img;
DWORD rShift, gShift, bShift, aShift;
_BitScanForward(&rShift, hdr->rMask);
_BitScanForward(&gShift, hdr->gMask);
_BitScanForward(&bShift, hdr->bMask);
_BitScanForward(&aShift, hdr->aMask);
std::for_each((uint32_t*)img + 128 / 4, (uint32_t*)img + size / 4, [&](uint32_t& im) {
im = ((hdr->aMask & im) >> aShift << 24) + ((hdr->bMask & im) >> bShift << 16) + ((hdr->gMask & im) >> gShift << 8) + ((hdr->rMask & im) >> rShift);
} );
}
static ComPtr<ID3D11ShaderResourceView> LoadDDSTexture(const char* name, ivec2& texSize)
{
int size;
ComPtr<ID3D11ShaderResourceView> srv;
void* img = LoadFile(name, &size);
if (!img) {
aflog("LoadDDSTexture failed! %s", name);
return 0;
}
const DDSHeader* hdr = (DDSHeader*)img;
DXGI_FORMAT format;
int (*pitchCalcurator)(int, int) = nullptr;
switch (hdr->fourcc) {
case 0x31545844: //'1TXD':
format = DXGI_FORMAT_BC1_UNORM;
pitchCalcurator = [](int w, int h) { return ((w + 3) / 4) * ((h + 3) / 4) * 8; };
break;
case 0x33545844: //'3TXD':
format = DXGI_FORMAT_BC2_UNORM;
pitchCalcurator = [](int w, int h) { return ((w + 3) / 4) * ((h + 3) / 4) * 16; };
break;
case 0x35545844: //'5TXD':
format = DXGI_FORMAT_BC3_UNORM;
pitchCalcurator = [](int w, int h) { return ((w + 3) / 4) * ((h + 3) / 4) * 16; };
break;
default:
ArrangeRawDDS(img, size);
format = DXGI_FORMAT_R8G8B8A8_UNORM;
pitchCalcurator = [](int w, int h) { return w * h * 4; };
break;
}
texSize.x = hdr->w;
texSize.y = hdr->h;
int arraySize = hdr->GetArraySize();
int mipCnt = hdr->GetMipCnt();
CD3D11_TEXTURE2D_DESC desc(format, hdr->w, hdr->h, arraySize, hdr->GetMipCnt(), D3D11_BIND_SHADER_RESOURCE, D3D11_USAGE_DEFAULT, 0, 1, 0, hdr->IsCubeMap() ? D3D11_RESOURCE_MISC_TEXTURECUBE : 0);
CD3D11_SHADER_RESOURCE_VIEW_DESC srvDesc(hdr->IsCubeMap() ? D3D_SRV_DIMENSION_TEXTURECUBE : D3D_SRV_DIMENSION_TEXTURE2D, desc.Format, 0, -1);
std::vector<D3D11_SUBRESOURCE_DATA> r;
int offset = 128;
for (int a = 0; a < arraySize; a++) {
for (int m = 0; m < mipCnt; m++) {
int w = std::max(1, hdr->w >> m);
int h = std::max(1, hdr->h >> m);
r.push_back({ (char*)img + offset, (uint32_t)pitchCalcurator(w, 1), 0 });
offset += pitchCalcurator(w, h);
}
}
ComPtr<ID3D11Texture2D> tex;
deviceMan11.GetDevice()->CreateTexture2D(&desc, &r[0], &tex);
deviceMan11.GetDevice()->CreateShaderResourceView(tex.Get(), &srvDesc, &srv);
free(img);
return srv;
}
view raw dds_loader.cpp hosted with ❤ by GitHub


ID3D11Texture2DとDDSファイルは共にキューブマップやミップマップの複数のイメージを内包します。よって、DDSからそれらを含むID3D11Texture2Dを生成するときにD3D11_SUBRESOURCE_DATAの配列でその複数イメージを指定します。

ミップマップ入りキューブマップ等になるとD3D11_SUBRESOURCE_DATAの順で悩みそうですが、これはIntroduction To Textures in Direct3D 11が詳しいです。更に都合のよいことに、D3D11_SUBRESOURCE_DATAとDDSの画像の配列順は同じになっているようです。

また、各画像の開始位置を正確にD3D11_SUBRESOURCE_DATAに設定しなければいけませんが、テクスチャ用の DDS ファイルのレイアウトと題されたページが参考になります。

やってみると意外と短いコードで出来たので紹介してみました。

Friday, August 21, 2015

HLSLでフォトスフィアをLittle Planetに変換する

スマホで撮った近所の公園のフォトスフィアからLittle PlanetとかTiny Planetと呼ばれている絵ができました。



ソース画像となるフォトスフィアとはGoogleによる呼称で、「360度パノラマVR」と呼ばれたり「全方位パノラマ」と呼ばれたり呼称が統一されていない様です。最近は「HDRi」で検索すると海外の素材集がいっぱいでてきますね。投影法はEquirectangular Projection(正距円筒図法)といいます。ここではフォトスフィアで統一します。

また、結果物であるLittle Planetのような投影法をStereographic Projection(ステレオ投影)といいます。

今回の目的は、HLSLによってEquirectangular Projectionされた画像をソースとしてStereographic Projectionした画像を得ることです。 まず、簡単のためにキューブマップをソースにしてやってみます。



float4 mainPS(VsToPs inp) : SV_Target
{
float2 scale = float2(4.0f / 3.0f, 1) * 3; // scale & aspect ratio
float2 plane = inp.screenPos.xy * scale;
float3 dir = float3(plane.x * 2, plane.y * 2, -1 + dot(plane, plane)) / (1 + dot(plane, plane));
return texCube.Sample(samplerState, dir.xzy); // y is upper
}



scaleはアスペクト比と、Little Planetの大きさを決めます。ここは見た目が良くなるように適当に数値を決めればよいところです。
キューブマップのフェッチに使うdirはWikipediaのステレオ投影で紹介されている立体射影の逆写像の式そのままです。実際にフェッチの段階でyとzを入れ替えるのは、キューブマップのY方向が上下になっているからです。どうスィズルするかによってLittle Planetの中心をどこにするか決められます。

次に、フォトスフィアをソースにしてやってみます。冒頭の絵はこれで生成します。

float4 mainPS(VsToPs inp) : SV_Target
{
float2 scale = float2(4.0f / 3.0f, 1) * 3; // scale & aspect ratio
float2 plane = inp.screenPos.xy * scale;
float3 dir = float3(plane.x * 2, plane.y * 2, -1 + dot(plane, plane)) / (1 + dot(plane, plane));
dir = dir.xzy; // y is upper
float longitude = atan2(dir.x, dir.z) * (180 / 3.14159265f);
float latitude = asin(dir.y) * (180 / 3.14159265f);
return gTexture.Sample(samplerState, float2(longitude, latitude) / float2(360, -180) + 0.5);
}


前半部はキューブマップと全く同じで、dirを使ってフォトスフィアのどこからフェッチするかの計算が加わりました。まず、三次元ベクトルのdirを経度(longitude、-180~180)と緯度(latitude、-90~90)に変換します。そうすると経度と緯度がフォトスフィアのX方向とY方向に対応します。あとはテクスチャUVである0~1の範囲に変換すれば完成です。

Friday, August 14, 2015

Visual Studio 2015でCとJavaを併用したAndroidサンプル、Target API Levelの変更方法

https://code.msdn.microsoft.com/windowsdesktop/hello-jni-Android-790ab73d

JavaからActivityを作り、Cから文字列を生成してTextViewに表示するプログラムがMicrosoftから公開されています。Android NDKに含まれているhello-jniサンプルと同じもので、Visual Studio 2015のプロジェクトファイルが含まれています。

KitKat用にビルドされる設定になっていたのでうちの古い機種(Vega Racer)で動かす為にTarget API Levelを下げてみました。https://developer.android.com/about/dashboards/index.html によるとGingerbreadが4.6%ほどしぶとく残っていたので、Gingerbreadまで下げてみます。



プロジェクトのプロパティと、hello-jni.Packaging というファイルのプロパティの2箇所に設定があって、それぞれDebug/Releaseと、ARM/x86の組み合わせに対してTarget API Levelを設定します。(8箇所!)PackagingのほうはAndroid SDK Managerdで該当のAPIをダウンロードしておかないと選択肢が出てきませんでした。

古いAndroid実機でF5で実行しようとすると、"Unable to start debugging. The Visual Studio C++ Android debugger requires that the target emulator/device be running Android API level 17 (version 4.2) or newer."のメッセージが出ました。この場合でもCtrl-F5でデバッグ無しで実行できました。

Sunday, August 9, 2015

Visual Studio 2015からAndroid(+OpenGL ES)開発

Visual Studio 2015でAndroidが開発できるとのことで試してみました。OpenGL ESもちゃんと動きました。





インストールオプションは適当です。



新規プロジェクト画面。インストール時に何かが抜けていたのか、Android開発ツールの追加インストール画面へのリンクが表示されています。


追加インストール画面。


Android用のテンプレートが追加されました。OpenGLESによるAndroidとiOSのクロスプラットフォームのテンプレートがあります。

WindowsからiOSが開発できるのかと期待しましたが、結論から言うと残念ながらMacが別途必要のようです。
https://msdn.microsoft.com/en-US/library/mt147405.aspx
http://stackoverflow.com/questions/31843957/vs2015-build-agent-request-has-failed-uri-not-set



テンプレートからプロジェクトを生成してビルドすると、"Build agent request has failed, URI not set..."というiOS関連のエラーが出ました。*.iOS.StaticLibrary(iOS)と*.iOS.Applicationプロジェクトを削除するとAndroidの開発環境だけが残りました。

ところで、Visual Studioが認識するデバイスとしないデバイスがあるようです。Vega RacerとAcer Iconiaは認識しました。LG GFlex2は認識せず、"No devices available"と表示されます。GFlex2はadb devicesではちゃんと認識しており、Android Studioからのデバッグも問題がなかったのでadb以外の他の原因がありそうです。





また、認識した2機種も"No devices available."と出る時がありました。少し待てば認識されることもありましたが、プロジェクトを閉じて開き直すと認識することもありました。



ACER ICONIAはx86なのですが、armeabiと表示がでているのはバグでしょうか。左のプルダウンメニューからARMかx86かを選んでデバッグするようになっており、正しいアーキテクチャを指定すれば実行できました。

[slide]프로그래머를 위한 360VR

로드뷰, 360VR의 기반 기술인 Equirectangular Projection(이퀴렉탱귤러/등장방형도법)을 이해해, DirectX11과 HLSL로 화면 출력, 큐브맵과 상호 변환 방법을 공부합니다.

Saturday, August 1, 2015

HLSLでフォトスフィアビューアを作る

ストリートビューなどで使われ、最近はAndroidのカメラなどでも誰でも手軽に作れるようになったフォトスフィアをDirectX11とHLSLでレンダリングしてみます。全方向の色を記録するためEquirectangular projectionという投影方法で長方形になっています。世界地図などでは正距円筒図法と呼ばれるようです。



これにPhoto Sphere XMP Metadataと呼ばれるメタデータが埋まることで、対応アプリから認識されます。非対応アプリからは普通のJPGファイルです。

全方向画像と言えば、ゲーム開発で主に使われるのはcube mapです。実際、cube mapで遠景を描いているゲームも多いと思いますが、フォトスフィアビューアでやる事とほとんど同じです。各シェーダの全文の例を掲示します。何をしているかというと、各ピクセルの視線ベクトルを求め、その方向の色をcubemap又はPhoto Sphereから読み取っています。見ての通り頂点シェーダ(mainVS)からピクセルシェーダ(mainPS)の第一行目まで全く同じです。

cbuffer perObject : register(b0)
{
row_major matrix invVP;
}
TextureCube texCube : register(t0);
SamplerState samplerState : register(s0);
struct VsToPs
{
float4 pos : SV_POSITION;
float4 pos2 : POS2;
};
VsToPs mainVS(uint id : SV_VertexID)
{
VsToPs ret;
ret.pos = float4(id & 1 ? 1 : -1, id & 2 ? -1 : 1, 1, 1);
ret.pos2 = ret.pos;
return ret;
}
float4 mainPS(VsToPs inp) : SV_Target
{
float3 dir = normalize(mul(inp.pos2, invVP).xyz);
return texCube.Sample(samplerState, dir);
}
view raw cubemap.hlsl hosted with ❤ by GitHub
cbuffer perObject : register(b0)
{
row_major matrix invVP;
}
Texture2D gTexture : register(t0);
SamplerState samplerState : register(s0);
struct VsToPs
{
float4 pos : SV_POSITION;
float4 pos2 : POS2;
};
VsToPs mainVS(uint id : SV_VertexID)
{
VsToPs ret;
ret.pos = float4(id & 1 ? 1 : -1, id & 2 ? -1 : 1, 1, 1);
ret.pos2 = ret.pos;
return ret;
}
float4 mainPS(VsToPs inp) : SV_Target
{
float3 dir = normalize(mul(inp.pos2, invVP).xyz);
float coordX = atan2(dir.x, dir.z) / 3.1415926;
float coordY = asin(dir.y) / (3.1415926 / 2);
return gTexture.Sample(samplerState, float2(coordX, coordY) * float2(0.5, -0.5) + 0.5);
}


C++からは頂点は4つ出力しています。頂点バッファをバインドしない代わりに頂点シェーダで画面の四隅の位置を設定して全画面を塗りつぶします。また、pos2をラスタライザに渡してピクセルシェーダからピクセルの画面上の位置を取得できるようにします。ピクセルシェーダの一行目で画面上の位置にinvVPを掛けることで画角とカメラの方向が反映された視線方向を求められます。invVPはView行列とProjection行列を掛けたものの逆行列です。

ここから先はcube mapとphotosphereで処理が変わりますが、キューブマップの場合は視線ベクトルから色を求めるのはたったの1行です。キューブマップは元々三次元ベクトルから色を取得するものなので当然ですね。それに対し、Photo Sphereは三次元ベクトルをEquirectangularで投影した二次元テクスチャの座標に変換する作業が間に入ります。

このプログラムの三次元の座標系は、右が+x、上が+y、奥が+zと定義しています。フォトスフィア上の二次元の座標系coordX, coordYは、フォトスフィアの中心を(0,0)とし、左上を(-1,-1) 右下を(+1,+1)と定義します。便宜上の定義であって実際のテクスチャの座標ではないことに注意してください。



写真だと位置をイメージしにくいので、Equirectangular projectionされた世界地図を使って、実例で座標を指定してみます。
東経0度、北緯0度 =(0, 0)、ガーナ南方の赤道上の地点、ここは視線ベクトル(0, 0, 1)になります。
東経0度、北緯45度 =(0, 0.5)、フランスのボルドー、ここは視線ベクトルは(0, 0.7071, 0.7071)です。
西経90度、北緯45度 =(-0.5, 0.5) ウィスコンシン州、ここは視線ベクトルは(-0.7071, 0.7071, 0)です。

こうして数値を見比べてみると数式が見えてこないでしょうか。
coordXは視線ベクトルのうちy成分を無視して、xとzからatan2で角度を求めればそれがそのまま経度になります。
coordYは視線ベクトルy成分からのみ算出します。どちらも-1から1の範囲ですが、経度は球面上の距離なのでasinで変換します。

最後に、coordX, coordYは実際のテクスチャ座標系ではないので0から1の範囲に変換してテクスチャを読み込みます。