将可掀桌的黑白棋移植到 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 啊!还好可以把它们封装起来。
所以在核心模式下,画图的流程大概是:
- 加载+编译 shader
- 加载 texture
- 加载模型(就是上面那坨魔法)
- 每帧更新的时候
- 激活 shader
- 计算好 model 和 projection 矩阵,喂给 shader
- 画模型
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 的一些控制(我放弃了……实在没办法把环境配好) - 嵌入的网页稍微修整一下吧……