15
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Windows でしか動かない描画処理をブラウザで動かす OSS を公開した

15
Last updated at Posted at 2026-03-22

昔の Windows で描画を伴うソフトやゲームでは DirectX や Direct3D という グラフィックスを描画するための API が使われていました。

この DirectX / Direct3D は Microsoft が開発したものであり、基本的に Windows でしか動作しません。

2000年代のゲームエンジン、社内ツール、昔書いたビジュアライザ。動かすにはWindows環境が必要で、今となっては誰かに見せるのも一苦労。

そのコード資産を、できることなら書き直さずにブラウザで動かしたい——そういう需要はニッチだけど確実に存在すると思っています。(ほとんどの人はないと思うけど笑)

このたび、そのための変換レイヤーをOSSとして公開しました。

d3d9-webgl — Direct3D 9 Fixed Function Pipeline を WebGL 2.0 として実装したEmscripten向けラッパーです。

image.png

何ができるのか

D3D9のAPIをそのままWebGL 2.0で実装したヘッダーと .cpp ファイルのセットです。Emscriptenビルドにこのラッパーを差し込むだけで、既存のD3D9コードはそのままコンパイルが通り、ブラウザ上で動作します。

// このD3D9コードが、書き換えなしにブラウザで動く
IDirect3D9* d3d = Direct3DCreate9(D3D_SDK_VERSION);

D3DPRESENT_PARAMETERS pp = {};
pp.BackBufferWidth  = 1024;
pp.BackBufferHeight = 768;

IDirect3DDevice9* device;
d3d->CreateDevice(0, D3DDEVTYPE_HAL, nullptr,
                  D3DCREATE_HARDWARE_VERTEXPROCESSING,
                  &pp, &device);

device->SetTransform(D3DTS_WORLD, &matWorld);
device->SetTransform(D3DTS_VIEW,  &matView);
device->SetTransform(D3DTS_PROJECTION, &matProj);
device->SetTexture(0, texture);
device->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0, numVerts, 0, numTris);

実用例 (というかこのために作ってついでに OSS 化した)

実戦での動作検証として、2003年のオンラインアクションゲーム「GunZ: The Duel」のレンダリングコードを一行も変えず ほぼ変えずにブラウザ移植できています。gunz.sigr.io で実際にプレイできます。

「本当にコード変更ゼロ ほぼせずに動いた」という証拠として先に挙げておきます。

関連記事: Claude Code で20年前の商用ゲームをほぼ書き直さずにブラウザ移植するまで

なぜこのアプローチなのか

Emscriptenを使えばC++コードはそのままWebAssemblyに変換できます。ではなぜD3D9プロジェクトのWasm移植が難しいかというと、d3d9.h を include した時点でビルドが止まるからです。D3D9はWindowsのDirectX SDKにしか存在せず、Linux/WebにはAPIが存在しません。

image.png

移植の選択肢を整理するとこうなります。

アプローチ 内容
レンダリングコードをWebGL / Three.js で書き直す 工数が莫大。大型プロジェクトでは現実的でない
D3D9をOpenGLに書き直してEmscriptenのGL→WebGL変換を使う 書き直しは避けられない
D3D9 APIを同じシグネチャでWebGLとして実装する 既存コードはそのまま

「インターフェースはD3D9、中身はWebGL」——これが今回取ったアプローチです。IDirect3D9IDirect3DDevice9IDirect3DTexture9といったインターフェースをすべて自前で実装し、メソッド呼び出しをそのままWebGL 2.0のAPIに変換します。既存コードからは本物のD3D9と区別がつかないので、コンパイルも実行もそのまま通る、という仕組みです。

image.png

実装で工夫した点

Fixed Function PipelineをGLSLで手書きした

Direct3D 9のFixed Function Pipeline(FFP)はWebGL 2.0には存在しません。SetLightSetMaterialSetTransform によってCPU側で組み立てるライティングモデルを、GPU上のGLSLシェーダーとして再現する必要があります。

頂点シェーダーではWorld/View/Projection変換・法線変換・ポイントライトによるディフューズ+スペキュラ計算を実装し、フラグメントシェーダーではテクスチャブレンドと最終的な色合成を行っています。DirectXの仕様書を参照しながら SetLight で渡されたパラメータをuniformとしてシェーダーに渡す形です。

FVF(Flexible Vertex Format)を動的に解析する

D3D9の頂点バッファはFVFというビットフラグで頂点レイアウトを表現します。

// 位置 + 法線 + 頂点カラー + UV1セット
DWORD fvf = D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_DIFFUSE | D3DFVF_TEX1;

このフラグを実行時に解析して glVertexAttribPointer のストライドとオフセットを動的に組み立てています。固定のシェーダーに対してFVFに応じた属性バインディングを切り替えることで、様々な頂点フォーマットを透過的に扱えます。

テクスチャフォーマットの変換

D3D9ではBGRAのバイトオーダーが標準ですが、WebGLはRGBAを期待します。A8R8G8BHのようなフォーマットはアップロード時にスウィズルで対応し、R5G6B5やA4R4G4B4のような16bitフォーマットはRGBA8に展開してからWebGLに渡しています。

DXT1/DXT3/DXT5の圧縮テクスチャはS3TC拡張(WEBGL_compressed_texture_s3tc)がほぼすべてのデスクトップブラウザで使用可能なため、変換なしにそのまま渡せます。

Y軸反転の場合分け

D3D9は左上を原点としてY軸が下向き、OpenGLは左下を原点としてY軸が上向きです。この差は最終的なスクリーン描画では問題になりませんが、Render-to-Texture(FBO)を使う場合に影響します。

SetRenderTarget でFBOに描画する際はY方向が反転するため、StretchRect でスクリーンにブリットするときに補正が必要です。一方で直接スクリーンに描くパスには補正が不要なので、ターゲットに応じて場合分けしています。地味に見落としやすく、最初は一部UIが上下反転するバグになっていました。

クリップ平面を discard でエミュレート

WebGLにはD3D9の SetClipPlane / D3DRS_CLIPPLANEENABLE に相当するハードウェアクリップ平面がありません。gl_ClipDistance はWebGL 2.0では使用できないため、フラグメントシェーダー内でクリップ平面との距離を計算し、負なら discard するアプローチで対応しています。

ステートキャッシュで WebGL API コールを削減

D3D9アプリケーションは毎フレーム大量の SetRenderState / SetTexture / SetSamplerState を呼び出します。WebGLへの変換をそのままスルーすると不要なGL呼び出しが増えてパフォーマンスが落ちるため、テクスチャバインディング・シェーダープログラム・サンプラーステート・ビューポート・シザーはすべてキャッシュして差分があるときだけGLを呼ぶようにしています。

使い方

5つのファイルをプロジェクトにコピーするだけです。

d3d9.h          — D3D9 型定義・インターフェース
d3d9.cpp        — WebGL 2.0 実装本体(約3,400行)
d3dx9math.h     — D3DX 数学ライブラリ(ベクトル・行列・クォータニオン)
d3dx9.h         — D3DX スタブ(ID3DXLine、ID3DXFont)
windows_compat.h — Windows API スタブ

CMakeの場合はこれだけです。

add_executable(my_app main.cpp d3d9.cpp)
target_link_options(my_app PRIVATE
    -sUSE_WEBGL2=1
    -sFULL_ES3=1
    -sWASM=1
    -sALLOW_MEMORY_GROWTH=1
)

あとは emcmake cmakeemmake make でビルドすれば、既存のD3D9コードがブラウザで動きます。

制限

正直に書きます。

FFPのみ対応です。 HLSLのバーテックスシェーダー・ピクセルシェーダーを使っているプロジェクトはそのままでは動きません。ラッパーは VertexShaderVersion = 0 を返すので、シェーダーを持つアプリケーションはFFPのコードパスにフォールバックする必要があります。

その他の制限:

  • ポイントライト最大3つ(ディレクショナル・スポットライト未対応)
  • 頂点バッファはストリーム0のみ
  • レンダーターゲットへの LockRect(GPUリードバック)は未対応
  • D3DXMatrixInverse はスタブ(恒等行列を返す)

2000年代前半のゲームやツールのほとんどはFFPを使っています。シェーダーが普及し始めたのはDX9世代の後期なので、その時期以前のプロジェクトならほぼカバーできるはずです。

同じ悩みを持つ人に届いてほしい

自分がGunZ移植でD3D9ラッパーを必死に書いていたとき、同じものが既に存在していれば——と何度も思いました。このOSSが「昔のD3D9プロジェクトをブラウザで動かしたい」という人の助けになれば嬉しいです。

フィードバック・PRもお待ちしています!


関連記事: Claude Code で20年前の商用ゲームをほぼ書き直さずにブラウザ移植するまで

15
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?