Chapter 16 · 渲染管线

坐标变换

学习世界坐标与屏幕坐标的相互转换,实现精确的鼠标拾取、HUD 定位等功能。

16

概念:两套坐标系

XGE 中存在两套坐标:

  • 世界坐标(World):游戏逻辑使用的坐标,受相机平移/缩放/旋转影响。
  • 屏幕坐标(Screen):像素坐标,(0,0) 在窗口左上角,鼠标/触摸返回的就是屏幕坐标。

当相机发生平移或缩放后,同一个世界坐标会映射到不同的屏幕位置。xgeWorldToScreen 和 xgeScreenToWorld 帮你在两者之间转换。

API 速查

函数说明
xge_vec2_t xgeWorldToScreen(xge_vec2_t tPoint)世界坐标 → 屏幕坐标
xge_vec2_t xgeScreenToWorld(xge_vec2_t tPoint)屏幕坐标 → 世界坐标

编译本章示例

gcc -O2 -I. -o ch16.exe ch16_main.c build\xge.lib -lm -lws2_32 -liphlpapi -lgdi32 -luser32 -lshell32 -lopengl32 -lole32 -lwinmm -lavrt
.\ch16.exe

示例 1:鼠标拾取世界物体

static int onFrame(void* pUser) {
    // 设置相机(平移 + 缩放)
    xge_camera_t cam = xgeCameraDefault(800, 600);
    cam.tPosition.fX = 500;  // 相机看向世界 (500, 300)
    cam.tPosition.fY = 300;
    cam.tScale.fX = cam.tScale.fY = 2.0f;  // 2x 放大
    xgeCameraSet(&cam);

    // 获取鼠标屏幕坐标
    xge_vec2_t mouse = xgeMouseGet();

    // 转换为世界坐标
    xge_vec2_t world = xgeScreenToWorld(mouse);

    // 判断是否点击了世界中的物体
    float dx = world.fX - g_fObjX;
    float dy = world.fY - g_fObjY;
    if (dx*dx + dy*dy < 30*30) {
        // 命中!
    }

    xgeClear(0x081114FF);
    return XGE_OK;
}

示例 2:在世界物体上方显示 HUD 标签

// 已知世界坐标(如 NPC 头顶)
xge_vec2_t npcPos = { .fX = 1200, .fY = 450 };

// 转换为屏幕坐标
xge_vec2_t screen = xgeWorldToScreen(npcPos);

// 在屏幕坐标处绘制名字标签(不受相机影响)
// 先重置相机为默认(屏幕坐标 = 世界坐标)
xge_camera_t hud = xgeCameraDefault(800, 600);
xgeCameraSet(&hud);

// 在 screen 位置绘制文本
// xgeTextDraw(screen.fX, screen.fY - 40, "NPC", ...);

示例 3:小地图标记

// 小地图区域:屏幕右下角 200×150
float mapX = 590, mapY = 440, mapW = 200, mapH = 150;

// 世界范围 0~4000 × 0~3000
float worldW = 4000, worldH = 3000;

// 玩家世界坐标 → 小地图屏幕坐标
xge_vec2_t player = { .fX = g_fPlayerX, .fY = g_fPlayerY };
float dotX = mapX + (player.fX / worldW) * mapW;
float dotY = mapY + (player.fY / worldH) * mapH;

xgeShapeCircleFill(dotX, dotY, 4, 0xFF6B5EFF);

完整代码

将以下代码保存为 ch16_main.c,编译后运行即可体验鼠标拾取世界坐标:

#include "xge.h"

static float g_fObjX = 500.0f, g_fObjY = 350.0f;
static int   g_bHit = 0;

static int onFrame(void* pUser) {
    (void)pUser;

    // 设置 2x 放大相机
    xge_camera_t cam = xgeCameraDefault(800.0f, 600.0f);
    cam.tPosition.fX = 400;
    cam.tPosition.fY = 300;
    cam.tScale.fX = cam.tScale.fY = 2.0f;
    xgeCameraSet(&cam);

    // 鼠标屏幕坐标 → 世界坐标
    xge_vec2_t mouse = xgeMouseGet();
    xge_vec2_t world = xgeScreenToWorld(mouse);

    // 判断是否悬停在物体上
    float dx = world.fX - g_fObjX;
    float dy = world.fY - g_fObjY;
    g_bHit = (dx*dx + dy*dy < 30*30);

    xgeClear(0x081114FF);

    // 绘制目标物体(悬停时变色)
    uint32_t color = g_bHit ? 0xFF6B5EFF : 0x4FD8C2FF;
    xgeShapeCircleFill(g_fObjX, g_fObjY, 30, color);

    return XGE_OK;
}

int main(void) {
    xge_desc_t d = {0};
    d.iWidth  = 800;
    d.iHeight = 600;
    d.sTitle  = "ch16 - Coordinate Transform";
    d.iFlags  = XGE_INIT_VSYNC;
    xgeInit(&d);
    xgeRun(onFrame, NULL);
    xgeUnit();
    return 0;
}

代码讲解

  • xgeMouseGet() 返回鼠标的屏幕坐标(像素)。
  • xgeScreenToWorld() 将屏幕坐标转换为当前相机下的世界坐标。
  • 相机放大 2 倍后,屏幕上的 1 像素对应世界中 0.5 单位,转换结果与默认相机不同。
  • 悬停检测用距离平方避免开方运算,半径 30 的圆对应 900 的平方阈值。

要点与陷阱

  • 依赖当前相机:转换结果取决于当前设置的相机。确保在 xgeCameraSet 之后调用。
  • 默认相机时两者相等:如果相机没有平移/缩放/旋转,世界坐标 = 屏幕坐标。
  • 旋转相机:相机旋转后,转换包含旋转变换,鼠标正下方不一定对应"正下方"的世界坐标。
  • 不要用于 3D:这对函数仅适用于 2D 正交/透视相机的 XY 平面。真正的 3D 拾取需要射线检测。
  • 性能:转换是纯数学运算(矩阵乘法),每帧调用数百次也不会有性能问题。

进阶练习

  1. 实现拖拽功能:鼠标按下时 xgeScreenToWorld 记录偏移,拖动时实时更新物体世界坐标。
  2. 在旋转相机下实现鼠标拾取,验证转换的正确性。
  3. 实现"框选":鼠标拖出矩形(屏幕坐标),转换为世界坐标后选中范围内的所有物体。
坐标变换效果
图:坐标变换渲染效果