将可掀桌的黑白棋移植到 Wasm !(持续更新)

刚上大学的时候,我写了 owen8877/Othello 作为我编程课的大作业。最近,当我想把它展示给其他人看的时候,却遇到了编译和链接上的困难,更别说大部分人没有 linux 环境了。想到移植到 Wasm 上应该是给不错的主意,那么这篇文章就梳理一下整体的流程和遇到的困难吧!

发布请移步至 这里

配环境和 Smoke Test

照官方教程

我们首先安装了 emsdk,然后编译一个 hello.cpp 看看能不能通过(这里还要用一步 screen python3 -m http.server 去加载本地的 wasm 文件)。

What about OpenGL and FreeGLUT?

嗯好问题!其实 emscripten 是支持 FreeGLUT 的,想不到吧!

但是支持就支持完整一些啊!为什么 不能画圆柱 啊!(明明茶杯都可以画

/*
 * Geometry functions, see freeglut_geometry.c
 */
FGAPI void    FGAPIENTRY glutWireCube( GLdouble size );
FGAPI void    FGAPIENTRY glutSolidCube( GLdouble size );
FGAPI void    FGAPIENTRY glutWireSphere( GLdouble radius, GLint slices, GLint stacks );
FGAPI void    FGAPIENTRY glutSolidSphere( GLdouble radius, GLint slices, GLint stacks );
FGAPI void    FGAPIENTRY glutWireCone( GLdouble base, GLdouble height, GLint slices, GLint stacks );
FGAPI void    FGAPIENTRY glutSolidCone( GLdouble base, GLdouble height, GLint slices, GLint stacks );

(浪费了一下午的宝贵生命后)意识到 GLUT 是个被时代抛弃的玩意儿,不如趁早转到 SDL (虽然两者并不能直接这样比较……而且画圆柱也变得麻烦了)

然后又摸索了 n 久……发现虽然说是支持 OpenGL 1.0,但其实那套立即模式早就不支持了(请允悲),所以我们需要摸索一套不使用立即模式的绘画方式!

那么,(中二的)少年一起来学习着色器吧!

Shaders

一开始走了一点弯路,后来照着 LearnOpenGL CN 的教程学chao习daima。

(准确地说,这个教程教的是 OpenGL 3.2 的核心模式,不过基本上和 OpenGL ES 2.0 差异不大,而 WebGL 和 ES 2.0 差异也不大,所以是没有问题的啦!)

但是学了一会儿之后发现……呃……这套 API 和以前一样丑啊……

比如,如果我想画一些三角形……

// `vertices` stores vertex position, normal and texture position.

glGenVertexArrays(1, &vao);
glGenBuffers(1, &vbo);

glBindVertexArray(vao);

glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices[0]) * vertices.size(), &vertices[0], GL_STATIC_DRAW);
drawCount = vertices.size();

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), nullptr);
glEnableVertexAttribArray(0);

// ...

怎么这么 old style 啊!还好可以把它们封装起来。

所以在核心模式下,画图的流程大概是:

  1. 加载+编译 shader
  2. 加载 texture
  3. 加载模型(就是上面那坨魔法)
  4. 每帧更新的时候
    1. 激活 shader
    2. 计算好 model 和 projection 矩阵,喂给 shader
    3. 画模型

emmm,好像也不是很麻烦?但问题是这种旧的 API 没有类型保证,所以很有可能传错指针或者忘记激活 shader ,导致各类莫名奇妙的事情发生。

(啊忘记了,节标题是着色器,那我们讲讲着色器吧)

简单来说,

  • 定点着色器会把模型中的坐标(世界坐标)变成屏幕上可以画出来的坐标;
  • 片段着色器负责决定每个像素是怎么被着色的; 所以——都需要手写(请允悲)。

还好这些东西都可以抄的(开心),而且渲染出来直接是 Phong 光照,一下真实了很多呢!

坏处是……如果传错参数,连 runtime error 都没有……

Emscripten revisited

那么现在问题是如何将这些代码跑在浏览器里呢?

官方教程说用 cmake 的同学可以

emcmake cmake .

但很显然,对于像我们这种有一些冷门依赖的项目是走不通的。所以还是只能手写 makefile:

DIREM = bin-em
BINEM = $(DIREM)/Othello.html
OBJEM = $(DIREM)/main.o $(DIREM)/ai.o $(DIREM)/display.o $(DIREM)/element.o $(DIREM)/game.o $(DIREM)/io.o $(DIREM)/model.o $(DIREM)/player.o
EMXX = em++
EMXXFLAGS = -Wall -O2
EMLINKFLAGS = -s FULL_ES2=1 -s FULL_ES3=1 -s USE_GLFW=3 -s LLD_REPORT_UNDEFINED -s WASM=1 --preload-file resources --preload-file render --preload-file Settings

所幸也不是很长(

这里有个问题是 js 天生不支持读本地文件,所以 filesystem 其实是 runtime 模拟的,要用 --preload-file 搞进去。

还有个问题是,原本跑在 native 上的版本是给渲染单独开了一个线程,怎么在 wasm 上办到呢?官方说可以用 pthread ,但是试了一下 bug 太多,而且还会牵连到 firefox 的各种 bug 和兼容性问题。简单起见我们就全塞到 main 线程算啦~(于是遍地都是 #ifdef __EMSCRIPTEN__ 宏)

GLFW

哦对了,后来我没用 SDL ,转成 GLFW ,因为教程用的这个(

不过总体上来说都要比 GLUT 好,毕竟 main loop 是自己的了(

其他一些 callback 照虎画猫就行,不难的

白嫖 Cloudflare worker

既然编译出来都是静态文件,自然可以用 Cloudflare 的 worker 去 serve。

Roadmap

还需要填的坑:

  • 把 Watch_Doge 的算法移植上去(哼虽然 botzone 上比分不高,但是和人下还是小菜一碟呢)(于 177cd06 完成)
  • (求求你)换个好看点的材质吧
  • 一键掀桌!(于 13d09fa 完成)
  • 看看能不能用 ocornut/imgui 做一下 GUI 的一些控制(我放弃了……实在没办法把环境配好)
  • 嵌入的网页稍微修整一下吧……