温馨提示:使用PC端浏览器阅读可获得最佳体验
阅读本文时,请时不时就对照参考图看一下。
什么是overview?
如果你有使用过3D模型制作工具,例如3dsMax等等,在编辑模型时这些软件通常会展示四个视图:
- 前视图
- 左视图
- 顶视图
- 透视图
overview类似与顶视图。
HL引擎可以给任意一张地图生成overview,为了生成overview,你需要加上 -dev 参数来启动游戏,进入任意一张地图,然后在控制台输入 dev_overview 1 即可。
你会看到游戏画面变成了类似上图这样,这正是当前地图的overview,也就是顶视图。
同时我们还需要注意一下画面顶部显示的一些参数。
Overview: Zoom 1.57, Map Origin (-248.00, 16.00, 192.00), Z Min 673.00, Z Max -296.00, Rotated 0
然后把这个画面截图,就获得了一张overview图片(下文简称OV图),HL和CS已经制作好了一些,它们放在 valve/overviews 或者 cstrike/overviews 目录下。
生成overview的原理
为了正确使用OV图,我们有必要了解一下它是怎么生成的。
我制作了一个简单的地图模型:
上图坐标系中:
横向为 X轴 ,纵向为 Y轴 。
蓝色矩形是 世界区域 ,位置以 O 为中心,大小为 8192×8192 ,这个大小是引擎规定的。 O 是 世界中心点 。
紫色矩形是 overview区域 (下文简称 OV区域 ),位置和大小由地图作者制作的地图决定,引擎会自动计算,图中大小为 3000×3000 。 ORIGIN 是 OV区域 的中心点。
灰色区域是地图模型,仅仅作为观赏用。
当你输入 dev_overview 1 后,引擎就会把 OV区域 显示到游戏窗口:
假设 游戏窗口 大小(不包含窗口的边框)为 800×800 ,那么引擎就是把 OV区域 缩小到 800×800 来显示了。
缩小倍数
再次提醒, OV图 实际上就是 游戏窗口 的截图,所以它们的大小是相同的。
继续之前,需要先了解坐标系统, OV区域 使用 世界坐标 。而 游戏窗口 使用 窗口坐标 。
世界坐标最小值: x = -4096, y = -4096 世界矩形左下角 世界坐标最大值: x = +4096, y = +4096 世界矩形右上角
窗口坐标最小值: x = 0, y = 0 窗口左上角 窗口坐标最大值: x = 800, y = 800 窗口右下角
我们首先需要关注的是,引擎做了一个缩小操作。
引擎把 OV区域 缩小到 游戏窗口 大小,也就是 3000×3000 缩小到 800×800 ,我们只需要知道引擎缩小了多少倍,就能将 世界坐标 单位转换为 窗口坐标 单位。
计算方法很简单:
scale.x = overview.width ÷ window.width scale.y = overview.height ÷ window.height
代入参考图中的数据:
scale.x = 3000 ÷ 800 = 3.75 scale.y = 3000 ÷ 800 = 3.75
参考点
参考图中 P 的坐标是 800,1400 ,这是 世界坐标 ,以 世界中心点 作为参考点。但引擎缩小 OV区域 的时候,显然不是以 世界中心点 为中心缩放的。
引擎会以 ORIGIN 为中心来缩小 OV区域 。这意味着,如果我们要缩小 P 的坐标,就不能以 世界中心点 为参考点来缩小,否则会产生错位。
既然如此,那就把 P 的参考点也变成 ORIGIN 不就行了。
我们已经知道 OV区域 的 中心点 是 ORIGIN ,那就可以计算出 P 以 OV区域 的 中心点 为参考点的新坐标了。
计算如下:
P2.x = P.x - ORIGIN.x P2.y = P.y - ORIGIN.y
代入参考图中的数据:
P2.x = 800 - 500 = 300 P2.y = 1400 - 500 = 900
好了,现在让我们忘掉 世界中心点 吧。现在 ORIGIN 才是 中心点 。
然后我们把 P2 的坐标按照上文中计算出来的缩小倍数来缩小,就能得到 P2 的 缩小后的OV区域 坐标 。
P3.x = P2.x ÷ scale.x P3.y = P2.y ÷ scale.y
代入参考图中的数据:
P3.x = 300 ÷ 3.75 = 80 P3.x = 900 ÷ 3.75 = 240
此时 P3 坐标的单位已经和 窗口坐标 单位一致。
缩小后的OV区域 和 缩小后的P2的坐标 如下:
(什么?地形变了?那是因为我重新画过了-.-)
但是别忘了, 窗口坐标 的坐标值范围是:
窗口坐标最小值: x = 0, y = 0 窗口左上角 窗口坐标最大值: x = 800, y = 800 窗口右下角
而我们上面计算出的 P3 是以 0,0 为参考点的,显然 窗口坐标 的 中心点 不是 0,0 ,我们需要计算出来。
计算 窗口坐标 的 中心点 如下:
O2.x = window.width ÷ 2 O2.y = window.height ÷ 2
代入参考图中的数据:
O2.x = 800 ÷ 2 = 400 O2.y = 800 ÷ 2 = 400
如下图:
然后我们把 P3 的参考点转为 窗口坐标 的 中心点 ,如下:
P4.x = O2.x + P3.x P4.y = O2.y + P3.y
代入参考图中的数据:
P4.x = 400 + 80 = 480 P4.y = 400 - 240 = 160
得到最终P4的坐标:
因为 OV图 就是 游戏窗口 的截图,所以 P4 在 OV图 中也是一样的坐标。
计算OV区域的大小
如果你还记得
Overview: Zoom 1.57, Map Origin (-248.00, 16.00, 192.00), Z Min 673.00, Z Max -296.00, Rotated 0
你应该会注意到这里并没有提供 OV区域 的大小,而 OV区域 的大小是我们最终计算出 P4 所必需的。
为此,引擎提供了 Zoom 参数。它的计算方法如下:
zoom.x = 世界区域.width ÷ overview.width zoom.y = 世界区域.height ÷ overview.height
代入参考图中的数据:
zoom.x = 8192 ÷ 3000 = 2.73 zoom.y = 8192 ÷ 3000 = 2.73
我们已经知道 世界区域 的大小,因此计算出 OV区域 的大小非常容易:
overview.width = 世界区域.width ÷ zoom.x overview.height = 世界区域.height ÷ zoom.y
代入参考图中的数据:
overview.width = 8192 ÷ 2.73 = 3000 overview.height = 8192 ÷ 2.73 = 3000
OV区域的中心点
OV区域 的 中心点 (即 ORIGIN )也是必须的,所以引擎提供了这个值:
Overview: Zoom 1.57, Map Origin (-248.00, 16.00, 192.00), Z Min 673.00, Z Max -296.00, Rotated 0
编写代码
有了计算方法,和必需的已知条件,我们就可以开始写代码了。
定义一个结构体 overview_t 来组织 OV图 的数据:
typedef struct { GLuint textureId; // GL纹理ID GLuint width; // OV图宽度 GLuint height; // OV图高度 GLfloat zoom; // 用于计算OV区域大小 GLfloat originX; // OV区域中心点X坐标 GLfloat originY; // OV区域中心点Y坐标 } overview_t;
定义一个变量 g_overview 来存储 OV图 的数据:
overview_t g_overview; void loadOverviewImage() { // 这两个函数请自己搞定 loadTexture("overviews/cs_italy.tga", &g_overview.textureId, &g_overview.width, &g_overview.height); loadInfo("overviews/cs_italy.txt", &g_overview.zoom, &g_overview.originX, &g_overview.originY); }
将 OV图 绘制到HUD上:
void HUD_Redraw() { gExportfuncs.HUD_Redraw(); RECT rc; rc.left = 0; rc.top = 0; rc.right = rc.left + g_overview.width; rc.bottom = rc.top + g_overview.height; glBindTexture(GL_TEXTURE_2D, g_overview.textureId); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glColor4f(1.0f, 1.0f, 1.0f, 1.0f); glBegin(GL_QUADS); // -------- 左上角 ------- glTexCoord2f(0.0f, 0.0f); glVertex2f(rc.left, rc.top); // -------- 右上角 ------- glTexCoord2f(1.0f, 0.0f); glVertex2f(rc.right, rc.top); // -------- 右下角 ------- glTexCoord2f(1.0f, 1.0f); glVertex2f(rc.right, rc.bottom); // -------- 左下角 ------- glTexCoord2f(0.0f, 1.0f); glVertex2f(rc.left, rc.bottom); glEnd(); }
计算 OV区域 大小:
typedef struct { GLfloat width; GLfloat height; } SIZE_t; SIZE_t overview_size; overview_size.width = 8192.0f / g_overview.zoom; overview_size.height = 8192.0f / g_overview.zoom / 1.3333; // 4÷3=1.3333
你应该注意到了计算 OV区域 高度时,额外再除了一个 1.3333 ,这是因为引擎只给出了 zoom.x 和 zoom.y 的其中一个。
引擎总是认为,游戏窗口的宽度一定会大于高度(比例是4:3),而 OV区域 总是正方形。
为了保证生成 OV区域 显示在游戏窗口中不会变形(把正方形拉成长方形显示肯定会变形呀),实际上缩小 OV区域 的高度时会缩得比宽度更多一点。
所以我们计算 OV区域 的高度时,也要这么做。
补充:无论实际 游戏窗口 的宽高是多少,显示 OV区域 时,引擎都始终认为宽高比例是 4:3 ,所以写固定的 1.333 就行了。如果你用宽屏模式去查看 OV区域 ,将会是变形的(被拉宽了)。
计算缩小比例:
float scaleX = overview_size.width / g_overview.width; float scaleY = overview_size.height / g_overview.height;
取一个 世界坐标 来测试:
typedef struct { GLfloat x; GLfloat y; } POINT_t; cl_entity_t* local = gEngfuncs.GetLocalPlayer(); // 取本机客户端对应的玩家实体 POINT_t P; P.x = local->curstate.origin[0]; // X P.y = local->curstate.origin[1]; // Y
将 P 的 参考点 转换为 OV区域 的 中心点 :
POINT_t P2; P2.x = P.x - g_overview.originX; P2.y = P.y - g_overview.originY;
将 世界坐标 的 单位 转换为 窗口坐标 的 单位 :
POINT_t P3; P3.x = P2.x / scaleX; P3.y = P2.y / scaleY;
计算 窗口坐标 的 中心点 :
POINT_t overview_image_origin; overview_image_origin.x = g_overview.width / 2.0f; overview_image_origin.y = g_overview.height / 2.0f;
将 P3 的 参考点 转换为 窗口坐标 的 中心点 :
POINT_t P4; P4.x = overview_image_origin.x + P3.x; P4.y = overview_image_origin.y - P3.y;
绘制 P4 到HUD上:
gEngfuncs.pfnFillRGBA(P4.x - 4, P4.y - 4, // X,Y 8, 8, // width,height 255, 255, 255, 255); // R,G,B,A
参考代码
typedef struct { GLfloat x; GLfloat y; } POINT_t; typedef struct { GLfloat width; GLfloat height; } SIZE_t; typedef struct { GLuint textureId; // OV图文理ID GLuint width; // OV图宽度 GLuint height; // OV图高度 GLfloat zoom; // 用于计算OV区域大小 GLfloat originX; // OV区域中心点X坐标 GLfloat originY; // OV区域中心点Y坐标 bool rotated; // OV区域是否需要旋转 } overview_t; overview_t g_overview = { 0 }; void HUD_Init(void) { gExportfuncs.HUD_Init(); LoadTexture("overviews/cs_siege.tga", &g_overview.textureId, &g_overview.width, &g_overview.height); LoadInfo("overviews/cs_siege.txt", &g_overview.zoom, &g_overview.originX, &g_overview.originY, &g_overview.rotated); } int HUD_Redraw(float time, int intermission) { gExportfuncs.HUD_Redraw(time, intermission); RECT rc; rc.left = 0; rc.top = 0; rc.right = rc.left + g_overview.width; rc.bottom = rc.top + g_overview.height; // ------------- 把OV图绘制到HUD上 ------------- glBindTexture(GL_TEXTURE_2D, g_overview.textureId); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glColor4f(1.0f, 1.0f, 1.0f, 1.0f); glBegin(GL_QUADS); // -------- 左上角 ------- glTexCoord2f(0.0f, 0.0f); glVertex2f(rc.left, rc.top); // -------- 右上角 ------- glTexCoord2f(1.0f, 0.0f); glVertex2f(rc.right, rc.top); // -------- 右下角 ------- glTexCoord2f(1.0f, 1.0f); glVertex2f(rc.right, rc.bottom); // -------- 左下角 ------- glTexCoord2f(0.0f, 1.0f); glVertex2f(rc.left, rc.bottom); glEnd(); // -------------- 计算OV区域大小 --------------- SIZE_t overview_size; overview_size.width = 8192.0f / g_overview.zoom; overview_size.height = 8192.0f / g_overview.zoom / 1.3333f; // --------------- 计算缩小比例 ---------------- float scaleX = overview_size.width / g_overview.width; float scaleY = overview_size.height / g_overview.height; // --------------- 取自己的坐标 ---------------- cl_entity_t* local = gEngfuncs.GetLocalPlayer(); POINT_t P; P.x = local->curstate.origin[0]; P.y = local->curstate.origin[1]; // ------- 将P的参考点转为OV区域的中心点 -------- POINT_t P2; P2.x = P.x - g_overview.originX; P2.y = P.y - g_overview.originY; // ------- 将P2的坐标单位转为窗口坐标单位 ------- POINT_t P3; P3.x = P2.x / scaleX; P3.y = P2.y / scaleY; // -------------- 计算OV图中心点 --------------- POINT_t overview_image_origin; overview_image_origin.x = g_overview.width / 2.0f; overview_image_origin.y = g_overview.height / 2.0f; // -------- 将P3的参考点转为OV图的中心点 -------- POINT_t P4; if (g_overview.rotated) { P4.x = overview_image_origin.x + (P3.x); P4.y = overview_image_origin.y + (-P3.y); } else { P4.x = overview_image_origin.x + (-P3.y); P4.y = overview_image_origin.y + (-P3.x); } // -------------- 把P4绘制到HUD上 -------------- gEngfuncs.pfnFillRGBA(rc.left + (P4.x - 4), rc.top + (P4.y - 4), // X,Y 8, 8, // width,height 255, 255, 255, 255); // R,G,B,A return 1; }
载入HL的OV的配置文件
bool LoadOverviewInfo(const char* fileName, overview_t* data) { char* buffer = (char*)gEngfuncs.COM_LoadFile((char*)fileName, 5, nullptr); if (!buffer) { return false; } char* parsePos = buffer; char token[128]; bool parseSuccess = false; while (true) { parsePos = gEngfuncs.COM_ParseFile(parsePos, token); if (!parsePos) { break; } if (!stricmp(token, "global")) { parsePos = gEngfuncs.COM_ParseFile(parsePos, token); if (!parsePos) { goto error; } if (strcmp(token, "{")) { goto error; } while (true) { parsePos = gEngfuncs.COM_ParseFile(parsePos, token); if (!parsePos) { goto error; } if (!stricmp(token, "zoom")) { parsePos = gEngfuncs.COM_ParseFile(parsePos, token); data->zoom = atof(token); } else if (!stricmp(token, "origin")) { parsePos = gEngfuncs.COM_ParseFile(parsePos, token); data->originX = atof(token); parsePos = gEngfuncs.COM_ParseFile(parsePos, token); data->originY = atof(token); parsePos = gEngfuncs.COM_ParseFile(parsePos, token); } else if (!stricmp(token, "rotated")) { parsePos = gEngfuncs.COM_ParseFile(parsePos, token); data->rotated = atoi(token) != 0; } else if (!stricmp(token, "}")) { break; } else { goto error; } } } else if (!stricmp(token, "layer")) { parsePos = gEngfuncs.COM_ParseFile(parsePos, token); if (!parsePos) { goto error; } if (strcmp(token, "{")) { goto error; } while (true) { parsePos = gEngfuncs.COM_ParseFile(parsePos, token); if (!stricmp(token, "image")) { parsePos = gEngfuncs.COM_ParseFile(parsePos, token); strcpy(data->image, token); } else if (!stricmp(token, "height")) { parsePos = gEngfuncs.COM_ParseFile(parsePos, token); } else if (!stricmp(token, "}")) { break; } else { goto error; } } } else { goto error; } } parseSuccess = true; error: if (buffer) { gEngfuncs.COM_FreeFile(buffer); } return parseSuccess; }