把 DirectX 9 游戏搬进浏览器,不改一行代码——这个开源库是怎么做到的

把一款 2003 年的 Windows 游戏搬进浏览器,通常意味着大量的重写工作。但如果游戏的底层渲染用的是 Direct3D 9,d3d9-webgl 提供了另一条路:不改游戏代码,通过一个兼容层把 D3D9 的 API 调用实时翻译成 WebGL 2.0,再用 Emscripten 编译成 WebAssembly,整个游戏就能在浏览器里跑起来。

这个库正是为了把《GunZ: The Duel》(2003年韩国动作网游)移植到浏览器而开发的,最终在没有修改游戏源码的前提下,让它以 WebAssembly 形式完整运行。项目目前约有 3400 行 C++ 代码,MIT 协议开源。

问题在哪里

Direct3D 9 是微软 2002 年发布的图形 API,当时大量 PC 游戏基于它开发。要把这类游戏移植到 Web,面临的核心问题是:

  • 浏览器没有 Direct3D,只有 WebGL
  • D3D9 和 WebGL 的概念体系不同——坐标系、纹理方向、渲染状态的命名和结构都不一样
  • D3D9 的固定功能管线(Fixed-Function Pipeline,FFP)在现代 WebGL 里不再存在,需要用着色器模拟

一个直接的解决方向是:写一套头文件和实现,让 D3D9 的 API 调用"看上去"正常,但底层把每一个操作翻译成对应的 WebGL 调用。这就是 d3d9-webgl 做的事。

翻译层如何工作

API 接口镜像

d3d9.h 定义了与原始 D3D9 完全相同的接口结构:IDirect3D9IDirect3DDevice9IDirect3DTexture9 等,包括约 70 个设备方法。游戏代码调用 CreateDevice()DrawIndexedPrimitive() 等函数,感知不到任何变化——但这些函数的实现在 d3d9.cpp 里,执行的是 WebGL 操作。

固定功能管线的着色器模拟

D3D9 的固定功能管线(光照计算、变换、纹理混合)在 WebGL 里不存在,需要用 GLSL 着色器来复现。d3d9-webgl 在内部动态生成 GLSL shader,根据当前的渲染状态(是否启用光照、纹理坐标数量、混合模式等)组合出正确的着色器代码,模拟 FFP 的行为。

支持的能力包括:

  • 顶点光照:最多 3 个点光源,带材质属性(漫反射、镜面反射、自发光)
  • FVF 自动解析:D3D9 用 Flexible Vertex Format 描述顶点布局,库会自动识别顶点中包含哪些属性(位置、法线、颜色、最多 8 个纹理坐标)并映射到 WebGL 的 vertex attribute

纹理格式转换

D3D9 支持多种纹理格式,WebGL 只支持其中一部分。库内部处理了格式转换:

  • DXT1/DXT3/DXT5 压缩纹理:通过 WebGL 的 WEBGL_compressed_texture_s3tc 扩展直接使用
  • A8R8G8B8 / X8R8G8B8:颜色通道顺序转换(ARGB → RGBA)
  • 16 位格式(R5G6B5、A1R5G5B5 等):转换为 WebGL 支持的格式

坐标系差异处理

D3D9 和 WebGL 在纹理 Y 轴方向上是相反的。库在渲染到纹理(Render-to-Texture)时自动处理 Y 轴翻转,避免图像倒置。

状态缓存

WebGL 的状态切换有开销,频繁调用 gl.enable()gl.blendFunc() 等会影响性能。库对渲染状态进行缓存,只在状态实际发生变化时才向 WebGL 提交更新。


集成方式

整个库由 5 个文件组成,集成非常简单:

d3d9.h          # D3D9 接口定义
d3d9.cpp        # 翻译层实现(约3400行)
d3dx9math.h     # 数学类型(矩阵、向量)
d3dx9.h         # D3DX 工具函数
windows_compat.h # Windows 类型定义(HRESULT、DWORD 等)

集成步骤

  1. 将这 5 个文件复制到项目中
  2. 在 Emscripten 编译命令中添加链接参数:
emcc your_game.cpp d3d9.cpp \
  -sUSE_WEBGL2=1 \
  -sFULL_ES3=1 \
  -o output.html
  1. 游戏代码中原有的 #include <d3d9.h> 保持不变,直接使用库提供的头文件替代系统头文件即可。

当前限制

库的文档对限制说明比较明确,使用前需要了解:

  • 仅支持固定功能管线:D3D9 的可编程着色器(顶点着色器、像素着色器)目前不在支持范围内。依赖自定义 shader 的游戏无法直接使用
  • 最多 3 个点光源:光照模型基于 GLSL 着色器手写实现,目前上限为 3 个
  • 单顶点流:不支持多个顶点缓冲区同时绑定
  • 不支持 GPU 回读:渲染目标的像素无法读回到 CPU 内存

这些限制意味着它更适合渲染逻辑相对简单的早期游戏,而不是使用了复杂着色器效果的中晚期 D3D9 游戏。

实际案例:GunZ: The Duel 的浏览器移植

这个库本身就是为了一个真实的移植项目而写的。

GunZ: The Duel 是韩国 MAIET Entertainment 于 2003 年开发的第三人称动作网游,以夸张的空中战斗和弹反机制著称。游戏于 2016 年官方关服,但仍有玩家社区在维护私服。@LostMyCode 将它完整移植到了浏览器,项目名为 Whiplash GunZ,可以在 gunz.sigr.io (opens in a new tab) 直接访问。

移植方案:游戏原有的 D3D9 渲染代码一行未改,直接通过 d3d9-webgl 的翻译层运行。整个移植流程是:

  1. 将 d3d9-webgl 的 5 个文件加入项目
  2. 用 Emscripten 重新编译游戏代码为 WebAssembly
  3. 游戏资源通过 Browser Cache API 缓存到本地,支持离线访问

游戏以 1024×768 分辨率渲染在 Canvas 上,资源加载完成后直接在浏览器内运行,不需要任何插件。

这个案例说明了 d3d9-webgl 的能力边界:GunZ 的渲染逻辑完全基于固定功能管线(正是库支持的范围),所以整套方案可以直接套用。如果游戏使用了自定义着色器效果,则需要等待库后续对可编程管线的支持。

写在最后

d3d9-webgl 目前只有 5 个 Star,是个非常早期的项目,但它解决的问题很具体:让一类历史上大量存在的 Windows 游戏有了一条进入浏览器的路径。

从技术角度看,它的价值在于提供了一个完整的"API 翻译层"实现思路——如何用有限的现代图形 API 模拟一套已经不存在的旧 API,包括着色器动态生成、格式转换、坐标系处理等细节都有真实可参考的代码。对于做 WebAssembly 移植、研究 WebGL 底层,或者单纯对图形 API 兼容层感兴趣的开发者,这个仓库值得一读。

GitHub 项目地址:https://github.com/LostMyCode/d3d9-webgl (opens in a new tab)