Chapter 17 · 渲染管线

视口与裁剪矩形

学习用 Viewport 分割渲染区域、用 Clip 矩形限制绘制范围,实现分屏、小地图、滚动容器等效果。

17

概念:Viewport vs Clip

  • Viewport(视口):定义渲染输出的目标矩形区域。改变视口后,所有绘制坐标都相对于新视口。用于分屏渲染。
  • Clip(裁剪矩形):限制绘制可见范围。超出裁剪矩形的像素被丢弃,但坐标系统不变。用于滚动容器、遮罩窗口。

API 速查

函数说明
void xgeViewportSet(xge_rect_t tRect)设置视口矩形
xge_rect_t xgeViewportGet(void)获取当前视口
void xgeViewportClear(void)恢复全屏视口
void xgeClipSet(xge_rect_t tRect)设置裁剪矩形
xge_rect_t xgeClipGet(void)获取当前裁剪矩形
void xgeClipClear(void)取消裁剪(全屏可见)

编译本章示例

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

xge_rect_t 结构

typedef struct xge_rect_t {
    float fX;      // 左上角 X
    float fY;      // 左上角 Y
    float fWidth;  // 宽度
    float fHeight; // 高度
} xge_rect_t;

示例 1:左右分屏

static int onFrame(void* pUser) {
    int w = xgeGetWidth(), h = xgeGetHeight();

    // 左半屏:玩家 1 视角
    xge_rect_t left = { 0, 0, w/2.0f, h };
    xgeViewportSet(left);
    xgeClear(0x0A1A20FF);
    // ... 绘制玩家 1 世界 ...

    // 右半屏:玩家 2 视角
    xge_rect_t right = { w/2.0f, 0, w/2.0f, h };
    xgeViewportSet(right);
    xgeClear(0x14100AFF);
    // ... 绘制玩家 2 世界 ...

    // 恢复全屏
    xgeViewportClear();
    return XGE_OK;
}

示例 2:小地图视口

// 主视图(全屏)
xgeViewportClear();
xgeClear(0x081114FF);
// ... 主世界绘制 ...

// 小地图(右下角 200×150)
xge_rect_t mini = { 590, 440, 200, 150 };
xgeViewportSet(mini);

// 缩小相机显示全局
xge_camera_t cam = xgeCameraDefault(200, 150);
cam.tScale.fX = cam.tScale.fY = 0.05f;  // 缩小 20 倍
xgeCameraSet(&cam);

xgeClear(0x00000080);  // 半透明黑底
// ... 绘制简化世界 ...

xgeViewportClear();  // 恢复

示例 3:裁剪矩形实现滚动窗口

// 定义列表可见区域
xge_rect_t listArea = { 50, 100, 300, 400 };

// 设置裁剪 → 超出区域的绘制被裁掉
xgeClipSet(listArea);

// 绘制长列表(可能超出 400px 高度)
for (int i = 0; i < 100; i++) {
    float y = 100 + i * 40.0f - g_fScrollOffset;
    xgeShapeRectFill(50, y, 300, 38, 0x1A2A30FF);
    // xgeTextDraw(60, y + 10, items[i], ...);
}

// 取消裁剪
xgeClipClear();

// 绘制滚动条(不受裁剪影响)
xgeShapeRectFill(352, 100, 6, 400, 0x333333FF);

完整代码

将以下代码保存为 ch17_main.c,编译后运行即可看到裁剪矩形实现的滚动列表:

#include "xge.h"

static float g_fScroll = 0.0f;

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

    // 滚轮控制滚动偏移
    xge_vec2_t wheel = xgeMouseGetWheel();
    g_fScroll -= wheel.fY * 20.0f;
    if (g_fScroll < 0) g_fScroll = 0;
    if (g_fScroll > 3600.0f) g_fScroll = 3600.0f;

    xgeClear(0x081114FF);

    // 定义列表可见区域
    xge_rect_t listArea = { 100, 80, 400, 440 };

    // 绘制背景框
    xgeShapeRectFill(100, 80, 400, 440, 0x0E1E24FF);

    // 设置裁剪 → 超出区域的绘制被裁掉
    xgeClipSet(listArea);

    // 绘制 100 行列表项
    for (int i = 0; i < 100; i++) {
        float y = 80 + i * 40.0f - g_fScroll;
        uint32_t bg = (i % 2) ? 0x1A2A30FF : 0x162228FF;
        xgeShapeRectFill(100, y, 400, 38, bg);
    }

    // 取消裁剪
    xgeClipClear();

    // 滚动条(不受裁剪影响)
    float barH = 440.0f * 440.0f / 4000.0f;
    float barY = 80 + g_fScroll / 4000.0f * 440.0f;
    xgeShapeRectFill(504, barY, 6, barH, 0x4FD8C280);

    return XGE_OK;
}

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

代码讲解

  • xgeClipSet(listArea) 设置裁剪矩形,之后超出该区域的绘制全部被丢弃。
  • 列表有 100 行× 40px = 4000px 总高,但可见区域只有 440px,裁剪将不可见部分切除。
  • xgeClipClear() 取消裁剪后绘制的滚动条不受影响。
  • Clip 不改变坐标系,只是“遮住”超出部分,与 Viewport 改变坐标原点不同。

要点与陷阱

  • Viewport 改变坐标原点:设置视口后,(0,0) 变为视口左上角。绘制坐标是相对于视口的。
  • Clip 不改变坐标:裁剪只是"遮住"超出部分,坐标系统不变。
  • 每帧重置:Viewport 和 Clip 在 xgeBegin 后重置为全屏。需要每帧重新设置。
  • 嵌套裁剪:多次调用 xgeClipSet 不会叠加,后一次覆盖前一次。需要交集裁剪时手动计算。
  • Viewport + Camera 组合:先设 Viewport 确定渲染区域,再设 Camera 确定该区域内的坐标映射。

进阶练习

  1. 实现四格漫画布局:4 个视口各显示不同相机角度的同一场景。
  2. 用 Clip 实现一个圆形遮罩(提示:结合 ShapeEx 的 ClipShape,第 59 章)。
  3. 实现画中画:主视口显示游戏,小视口显示摄像机监控画面。
视口与裁剪矩形效果
图:视口与裁剪矩形渲染效果