Lode 的 CG 教程

QuickCG 函数

目录

返回索引

简介

本章包含一些简单示例,帮助你学习使用 QuickCG 并探索其所有函数。

上一章包含了 QuickCG 所有函数的表格,但本章将对这些函数提供更详细的说明。

main 函数

QuickCG 目前由 6 个文件组成:
main.cpp 文件开头包含了若干头文件:QuickCG.h 用于使用 QuickCG 函数,SDL.h 用于使用 SDL 函数,以及一些有时会用到的标准 C++ 头文件。

这些头文件引用之后就是放置本教程各文章示例代码的地方。请保持引用部分不变(如有需要也可以添加)。

如果示例代码没有 main 函数,你需要自行添加,然后将示例代码复制粘贴到 main 函数内,如下所示:

int main(int argc, char *argv[])
{
  // Put the code here
}

如果示例需要多个函数(例如递归树章节中的递归函数),示例中已经包含了 main 函数,你可以直接将所有内容复制粘贴到 main.cpp 文件头文件引用的下方,编译运行应该完全没有问题。

基本屏幕函数

QuickCG 的基本屏幕函数用于创建窗口、在窗口上绘制像素、查看结果以及清除屏幕。

screen(int width, int height, bool fullscreen, char* text) 函数用于设置图形屏幕。整数 widthheight 分别是屏幕的宽度和高度。布尔值 fullscreen 用于选择是否全屏:设为 0 时图形输出在窗口中显示,设为 1 时进入全屏模式。字符串 text 是窗口的标题。

如果要使用全屏模式,必须确保使用显卡支持的分辨率,例如 640*480、1280*1024 或 400*300 等分辨率是支持的,但 123*548 不支持。在窗口模式下,所有分辨率都受支持,甚至像 1001*23 这样的分辨率也可以。

设置屏幕后,两个全局整数变量 wh 会分别被设置为屏幕的宽度和高度。这些变量名很短且设为全局变量,是因为它们在示例的数学表达式中经常被使用,这与数学和科学中使用单个符号表示常量和变量的做法如出一辙。

在屏幕坐标系中,坐标 (0, 0) 表示屏幕左上角,(w - 1, h - 1) 表示右下角。因此与数学坐标系不同,屏幕坐标中 y 值增大表示位置向下移动。

pset(int x, int y, ColorRGB color) 函数在坐标 x, y 处绘制一个像素,颜色为指定的 RGB 颜色:ColorRGB 颜色有 r、g、b 三个分量,值范围为 0 到 255。请确保坐标 x, y 在屏幕范围内,x 和 y 不能为负数,且应小于屏幕的宽度 w 和高度 h。

redraw() 函数将所有已绘制的像素上传到显卡,使其可见。如果只设置了屏幕并绘制了一些像素,在调用 redraw 之前是看不到这些像素的。不过此函数相对较慢,因此不要在每绘制一个像素后就调用它,而应在所有内容绘制完毕后再调用一次。

void cls(ColorRGB color = RGB_Black) 函数将整个屏幕清除为指定的 RGB 颜色。如果不提供颜色参数,屏幕将被清除为黑色。通常情况下,如果你先绘制像素,然后调用 redraw,再绘制更多像素,再次调用 redraw 时,如果新像素绘制在不同位置,之前的像素仍然会保留。如果不想保留之前的像素,请使用 cls 清除。

下面是上一章相同的示例,使用了所有这些函数(除了 cls):

int main(int argc, char *argv[])
{
  screen(256, 256, 0, "Small Test Script");
  for(int x = 0; x < w; x++)
  for(int y = 0; y < h; y++)
  {
    pset(x, y, ColorRGB(x, y, 128));
  }
  redraw();
  sleep();
}

输出效果如下:



程序遍历窗口的每个像素,根据其坐标赋予不同的颜色,从而形成这种颜色图案。

非图形函数

非图形函数负责处理输入、程序流程等事务。

sleep() 函数暂停程序,直到你按下某个键,或点击窗口的关闭按钮时关闭程序。

函数 void waitFrame(double oldTime, double frameDuration) 用于限制特效的最大帧率。参数 oldTime 是上一帧调用 waitframe 后的时间,frameDuration 以秒/帧为单位。使用示例如下:

oldTime = time;
waitFrame(oldTime, 0.05); // 50 milliseconds
time = getTime();

oldTime 始终是上一帧调用 waitframe 后的时间,waitframe 函数会消耗本帧其余代码尚未用完的那部分 50 毫秒时间。此函数内部使用 SDL_Delay,因此会释放 CPU 时间给其他应用程序。

bool done() 函数在用户按下 Escape 键或点击窗口关闭按钮时返回 true。如果你希望程序有一个主循环,并在用户退出时结束,可以使用此函数,例如:

screen(256, 256, 0, "Done?");
while(!done())
{ //do some stuff here
}

end() 函数被调用时立即结束程序。

readKeys() 函数让 SDL 检测当前按下的键,并将结果存储在数组 inkeys[] 中。 你可以在程序中通过检查 keyDown(NameOfTheKey) 或 keyPressed(NameOfTheKey) 是否为 true 来判断某个键是否被按下。keyPressed 每次按键按下只返回一次 true,而 keyDown 在键持续按下期间会一直返回 true。 所有键名的列表在上一章或 SDL 参考文档中可以找到。例如,"a" 键名为 SDLK_a,右方向键名为 SDLK_RIGHT。
注意:使用 "done()" 时,不需要再单独调用 readKeys(),因为 done() 函数内部已经调用了它,每帧调用一次即可。

函数 long getTicks() 返回程序启动以来经过的时间,单位为毫秒。

下面是一个综合使用这些函数的示例,可以用方向键以固定速度移动一个圆,速度与处理器速度无关:

int main(int argc, char *argv[])
{
  screen(400, 300, 0, "Movable Circle");
  float x = w / 2, y = h / 2; //the position of the circle; initially set it to the center of the screen
  float time, oldTime; //the time of the current and the previous loop
  while(!done())
  {
    //set the old time to the time of the previous loop
    oldTime = time;
    waitFrame(oldTime, 0.05);
    time = getTime(); //get the time since program start
    readKeys(); //get the current keystate

    //if an arrow key is pressed, move the circle in its direction at a speed of 50 pixels/second
    if(keyDown(SDLK_RIGHT)) x += (time - oldTime) / 20.0;
    if(keyDown(SDLK_LEFT)) x -= (time - oldTime) / 20.0;
    if(keyDown(SDLK_UP)) y -= (time - oldTime) / 20.0;
    if(keyDown(SDLK_DOWN)) y += (time - oldTime) / 20.0;
    drawDisk(int(x), int(y), 20, RGB_Gray); //draw a grey circle with radius 20 pixels at the position of the ball
    redraw();
    cls(); //clear the screen so the old position of the ball becomes black again
  }
}

首先,screen 函数创建图形窗口。然后初始化圆的位置和计时所需的变量。

接下来主循环开始,程序会持续运行并执行循环内的计算,直到你按下 Escape 键或点击关闭按钮。在主循环中,首先计算当前时间,并将上一帧的时间存入 "oldTime" 变量。时间差 t-oldTime 可用于表示本帧与上一帧之间经过的时间。

然后通过 readKeys 函数获取当前按键状态,程序会检测右、左、上、下方向键是否被按下,如果按下则改变圆的坐标。由于圆移动了 (t - oldTime) / 20.0 个像素,时间以毫秒(1/1000 秒)为单位,因此无论计算机速度快慢,圆都会以 50 像素/秒的速度移动。

最后,程序使用 disk 命令绘制填充圆,调用 redraw 使其可见,然后清除后备缓冲区以便绘制下一帧。

圆的位置使用浮点数,因为计时需要较高的精度。在调用 disk 函数时,位置需要转换为整数才能用作屏幕坐标。

2D 基本图元

以下函数在屏幕上绘制直线和圆等基本图元。这些不是标准的 SDL 功能(填充矩形除外),需要逐像素实现。这些函数使用优化算法,通过纯整数计算尽可能快速地绘制直线或圆。这些函数的工作原理在本教程的另一章中有说明。它们也被包含在 QuickCG 中,以便你可以在其他需要它们的程序中方便地使用(例如递归树)。

bool drawLine(int x1, int y1, int x2, int y2, ColorRGB color) 使用 Bresenham 算法,以指定颜色从点 (x1, y1) 到点 (x2, y2) 绘制一条直线。请确保两个点都在屏幕范围内,否则此函数不会绘制直线并返回 0(使用 clipLine 函数可将任意直线裁剪到屏幕范围内)。成功绘制直线时返回 1。

bool drawCircle(int xc, int yc, int radius, ColorRGB color) 以 (xc, yc) 为圆心,绘制指定半径和颜色的空心圆。如果圆的像素超出屏幕范围,则返回 0 且不绘制任何内容;成功绘制时返回 1。

bool drawDisk(int xc, int yc, int radius, ColorRGB color) 以 (xc, yc) 为圆心,绘制指定半径和颜色的填充圆。

bool drawRect(int x1, int y1, int x2, int y2, ColorRGB color) 以 x1, y1 和 x2, y2 为角点,绘制指定颜色的矩形。

bool clipLine(int x1,int y1,int x2,int y2, int & x3, int & y3, int & x4, int & y4): 当你需要将试图绘制的直线端点(或矩形角点)裁剪到屏幕范围内时,使用此函数。给定坐标为 (x1,y1)-(x2,y2) 的直线,函数将返回坐标为 (x3,y3)-(x4,y4) 的直线,该直线位于屏幕边缘或内部,即矩形 (0,w)-(0,h) 范围内。变量 x3、y3、x4 和 y4 需要以引用方式传入,例如使用 clipLine(x1, y1, x2, y2, x3, y3, x4, y4),其中 x3、y3、x4、y4 是可被函数修改的普通整数,用于接收新直线的坐标。直线在屏幕范围内时函数返回 1,不在屏幕范围内时返回 0。

下面的示例将在屏幕上绘制一条裁剪后的直线,同时还绘制圆、填充圆和矩形以展示这些命令的效果。

int main(int argc, char *argv[])
{
  screen(400, 300, 0, "2D primitives");
  int x1, y1, x2, y2; //The initial line, too big for the screen
  int x3, y3, x4, y4; //Will become the clipped line
  x1 = -50; //This is outside the screen!
  y1 = -20; //This is outside the screen!
  x2 = 1000; //This is outside the screen!
  y2 = 1200; //This is outside the screen!
  clipLine(x1,y1, x2, y2, x3, y3, x4, y4); //the new line represents the part of the old line that is visible on screen
  drawLine(x3, y3, x4, y4, RGB_Red); //The newline is drawn as a red line
  drawCircle(100, 100, 30, RGB_Green); //a green unfilled circle
  drawDisk(200, 100, 20, RGB_Yellow); //a yellow filled circle
  drawRect(150, 150, 200, 200, RGB_Cyan); //acyan square
  redraw(); //make it all visible
  sleep(); //pause
}

输出效果如下:


颜色转换

这些函数将颜色的表示从一种颜色模型转换为另一种。RGB 模型由计算机和屏幕使用,而 HSL 和 HSV 颜色模型对人类选择颜色更为直观。这些函数不是 SDL 的功能,在本教程的其他章节有说明,但被包含在 QuickCG 中,以便你在其他程序中方便地使用它们来获得漂亮的颜色渐变(例如绘制精美的分形或等离子效果)。有关颜色模型的更多信息,请参阅本教程的颜色章节。

4 个颜色模型转换函数为:

ColorHSL RGBtoHSL(ColorRGB colorRGB);
ColorRGB HSLtoRGB(ColorHSL colorHSL);
ColorHSV RGBtoHSV(ColorRGB colorRGB);
ColorRGB HSVtoRGB(ColorHSV colorHSV);


每种颜色模型用 3 个参数描述颜色。ColorRGB、ColorHSL 和 ColorHSV 数据类型都是简单的结构体,包含 3 个整数,区别仅在于整数的名称。转换函数允许你根据给定颜色模型计算返回颜色模型的各分量。

作为示例,让我们同时使用 HSLtoRGB 和 HSVtoRGB 函数来比较 HSL 和 HSV 颜色模型。有关这些函数工作原理的更多信息,请参阅光与颜色章节。

int main(int argc, char *argv[])
{
  screen(512, 256, 0, "HSL versus HSV");
  ColorRGB color;

  for(int x = 0; x < 256; x++)
  for(int y = 0; y < h; y++)
  {
    color = HSLtoRGB(ColorHSL(x, 255, y)); //convert the color "hue=x, saturation=255, lightness=y" to RGB
    pset(x, y, color); //plot a pixel from the HSL model
    color = HSVtoRGB(ColorHSV(x,255, y)); //convert the color "hue=x, saturation=255, value=y" to RGB
    pset(x + 256, y, color); //plot a pixel from the HSV model
  }
  redraw();
  sleep();
}



先使用 RGBtoHSL,再对结果使用 HSLtoRGB,可以得到原始值。

还有函数 Uint32 RGBtoINT(ColorRGB colorRGB)ColorRGB INTtoRGB(Uint32 colorINT),用于将 RGB 颜色与单个整数值相互转换。

图像加载

QuickCG 有一个可以加载 PNG 文件的函数:loadImage。该函数有两个版本,一个将图像存储在 ColorRGB 数组中,另一个将图像存储在 Uint32 数组中,每个像素的 RGB 颜色以单个 32 位整数存储。

int loadImage(std::vector<ColorRGB>& out, unsigned long& w, unsigned long& h, const std::string& filename); 成功加载图像时函数返回 0,否则返回 1。
例如,要使用此函数加载一个 64*64 的 PNG 文件,输入:“std::vector<ColorRGB> image; unsigned long w, h; loadImage(&image, &w, &h, "filename.png");

此函数的另一个版本是 int loadImage(std::vector<Uint32>& out, unsigned long& w, unsigned long& h, const std::string& filename);。 该版本以 32 位整数而非 ColorRGB 存储颜色。

下面的示例将加载一张 128*128 像素的图像 "test.png" 并在屏幕上显示。要使程序正常运行,你需要将该图像放在与编译后的可执行文件相同的目录下(或者使用完整路径)。

int main(int argc, char *argv[])
{
  std::vector<ColorRGB> image;
  unsigned long w, h;
  if(loadImage(&image, &w, &h, "filename.png")) return 1;
  screen(w, h, 0, "An image");

  //draw each pixel of the image
  for(int y = 0; y < h; y++)
  for(int x = 0; x < w; x++)
  {
    pset(x, y, image[y * w + x]);
  }

  redraw();
  sleep();
}



如果你想用单个数组存储多张纹理,表示当前纹理编号的参数必须是数组的第一个维度。你可以对每张纹理分别正常使用 loadBMP 函数,但需要添加表示纹理编号的额外数组参数。

文本函数

QuickCG 内置了一种字体,不需要加载任何包含字体的图像或文件,字体是一个布尔数组 bool font[256][8][8],完全在代码中定义。该字体是 IBM 计算机的 ASCII 字符集,包含 256 个字符,每个字符为 8*8 像素。下面是完整的 ASCII 字符表,其中包含每个字符的十六进制代码,例如符号 "j" 的十六进制代码为 A6,十进制为 166。



int printString(const std::string& text, int x = 0, int y = 0, const ColorRGB& color = RGB_White, bool bg = 0, const ColorRGB& color2 = RGB_Black) 绘制一段普通文本字符串。该函数有很多带默认值的参数,如果你对默认值满意,就不必输入所有参数。
当文本过长到达屏幕右侧时,位置会跳到下一行的左侧。如果到达底部,则会从屏幕左上角继续。

返回值为 h * x + y,其中 x 是下一个字母的 x 坐标,y 是下一个字母的 y 坐标,h 是屏幕高度。此返回值可用于找到下一个 print 命令的起始位置,以便紧接在上一段文本之后显示:x 坐标为 "returnValue / h",y 坐标为 "returnValue % h"。参见下方示例了解其用法。

print 函数允许打印任意变量类型(只要能通过 std::stringstream 转换为 std::string)。除值之外,它与 printString 函数共享相同的参数。

template<typename T> int print(const T& val, int x = 0, int y = 0, const ColorRGB& color = RGB_White, bool bg = 0, const ColorRGB& color2 = RGB_Black, int forceLength = 0))

void drawLetter(unsigned char n, int x, int y, const ColorRGB& color = RGB_White, bool bg = 0, const ColorRGB& color2 = RGB_Black); 根据 ASCII 码打印单个字符。

当你想将多段文本连续绘制,不希望每次修改文本时手动更新坐标,或者打印一个不知道长度的数字并想在其后紧接着打印内容时,使用 print 函数的返回值非常方便。在 "All your base..." 部分就用到了这一技巧,每行将 pos 设置为 print 函数的返回值,并用于下一行的参数。

下面是一个使用所有这些函数的示例。

int main(int argc, char *argv[])
{
  screen(256, 256, 0, "Text");
  //draw some texts
  print("Hello World!");
  print("I am spooky green text!", 20, 20, ColorRGB(0, 255, 0), 1, ColorRGB(0, 128, 0));

  //draw an integer variable
  int number = 1337;
  print(number, 16, 40, ColorRGB(0, 255, 255));

  //draw a floating point variable
  float pi = 3.1415926535897932384626433832795;
  print(pi, 0, 8);

  //draw every single character of the font
  for(int x = 0; x < 16; x++)
  for(int y = 0; y < 16; y++)
  {
    drawLetter(x + 16 * y, 100 + x * 8, 100 + y * 8, ColorRGB(255, 255, 255), 1, ColorRGB(255, 0, 0));
  }

  //draw multiple texts in a row easily by using the return value
  int pos;
  pos = print("All", 0, 70, RGB_Red);
  pos = print("your", pos / h, pos % h, RGB_Green);
  pos = print("base", pos / h, pos % h, RGB_Blue);
  pos = print("are", pos / h, pos % h, RGB_Yellow);
  pos = print("belong", pos / h, pos % h, RGB_Cyan);
  pos = print("to", pos / h, pos % h, RGB_Magenta);
  pos = print("us!!", pos / h, pos % h, RGB_White);

  redraw();
  sleep();
}

输出效果如下:

文本输入

文本输入函数允许你在程序运行时输入字符串或其他变量类型。这些命令在指定位置以指定颜色和背景打印提示信息,并允许你在提示信息后输入字符、字符串或数字。

下面的示例展示了如何使用这些函数:

int main(int
argc, char *argv[])
{
  screen(256, 256, 0, "Input Text");
  //input a string
  std::string someText;
  getInputString(someText, "Enter some text:", false, 0, 0);
  //input an integer
  int someInteger = getInput<int>("Enter an integer:", false, 0, 8);
  //input a double
  double someDouble = getDouble<double>("Enter a number:", false, 0, 16);
}

以下是输入内容并按三次回车后的结果(第三次按回车后程序关闭):


(已废弃的截图)

drawBuffer

drawBuffer(Uint32 *buffer) 函数一次性绘制整个 2D 像素数组。这比对每个像素单独使用 pset 要快得多。

要绘制的缓冲区必须是与屏幕尺寸完全相同的 2D 数组,RGB 颜色值以单个整数存储。将 RGB 颜色转换为整数值,使用 colorINT = B + 256 * G + 256 * 256 * R。 将其转换回 RGB,使用 R = (colorINT / 65536); G = (colorINT / 256) % 256; B = colorINT % 256。这正是 RGBtoINT 和 INTtoRGB 函数所做的事情。

创建数组的方式为

Uint32 buffer[h][w];

(注意高度坐标在前,因为内存中按扫描线存储)填充颜色后,使用以下方式绘制

drawBuffer(buffer[0]);

如果你要模拟调色板,在整数中以 BGR 顺序创建调色板颜色也很方便,火焰特效文章中有此用法的示例。

最后编辑于:2007 年 8 月 12 日

版权所有 (c) 2004-2007 Lode Vandevenne。保留所有权利。