Lode 的计算机图形学教程

隧道特效

目录

简介

隧道特效是老式 demo 中常用的炫酷效果之一。该特效呈现一条隧道,你在其中飞行,同时隧道旋转,看起来像 3D 效果。本教程将讲解如何实现它。

工作原理

隧道示例如下图所示,实际运行时会有动画效果:



特效的工作原理如下:

首先,需要一张纹理,即隧道侧壁的纹理。

隧道的动画实际上并非在运行时实时计算,而是预先计算好的。这些计算结果存储在两张表中:一张用于角度,一张用于距离。

距离表存储屏幕上每个像素到屏幕中心距离的倒数。屏幕中心的像素具有非常大的值(它们距离很远,如上图所示),而屏幕边缘的像素值较小(这些像素代表隧道中靠近摄像机的部分)。

角度表存储屏幕上每个像素相对于屏幕中心的角度。

预计算完成后,动画循环开始。动画循环每帧遍历每个像素 (x, y),利用角度表和距离表确定应在当前像素绘制哪个纹素。为了产生动画效果,角度表和距离表的值会发生偏移:偏移角度表使隧道旋转,偏移距离表使你向前或向后移动。

纹理的尺寸是有限的,而距离值可以趋向无穷大,角度值是周期性的。当需要纹理外的值时,对纹理尺寸取模,这样纹理就会不断重复(但越靠近屏幕中心越小)。在代码中你会看得更清楚。

代码

该代码创建一个隧道,你在向前飞行的同时隧道旋转,隧道中心始终保持在屏幕中心。代码的设计使得无论纹理大小如何,特效速度始终相同,纹理在屏幕上的大小也相同。

这里定义了一些值并创建了缓冲区。

texture 数组就是纹理。尺寸越大效果越好,建议至少 256×256 像素,因为它将与屏幕一样大,纹理太小时旋转会非常抖动。

distanceTable 是每个像素距离倒数的预计算表,angleTable 是每个像素角度的预计算表。它们至少要与屏幕一样大,但如果还想在屏幕上移动隧道中心,则需要更大。

buffer 数组用于存储待绘制的像素,这样可以一次性绘制整个屏幕缓冲区,而不必对每个像素单独调用 pset。

#define texWidth 256
#define texHeight 256
#define screenWidth 640
#define screenHeight 480

// Y-coordinate first because we use horizontal scanlines
Uint32 texture[texHeight][texWidth];
int distanceTable[screenHeight][screenWidth];
int angleTable[screenHeight][screenWidth];
Uint32 buffer[screenHeight][screenWidth];

主函数从这里开始,生成一张蓝色 XOR 纹理(也可以从位图文件加载)。

int main(int argc, char *argv[])
{
  screen(screenWidth, screenHeight, 0, "Tunnel Effect");

  //generate texture
  for(int y = 0; y < texHeight; y++)
  for(int x = 0; x < texWidth; x++)
  {
    texture[y][x] = (x * 256 / texWidth) ^ (y * 256 / texHeight);
  }

接下来生成缓冲区。距离缓冲区使用到屏幕中心的距离倒数公式(2D),乘以 texHeight,使得无论纹理大小如何,显示效果始终相同。同时对 texHeight 取模,使同一纹理在整个距离范围内不断重复。

角度缓冲区使用 atan2 函数计算当前像素相对于屏幕中心像素的角度。atan2 函数属于 <cmath> 头文件,通过给定点的 x 和 y 坐标返回该点的弧度角。除以 pi,使纹理恰好在隧道周围包裹一圈。

ratio 变量是纹理在屏幕上宽高比,即纹理在纵深方向上的拉伸程度。

  //generate non-linear transformation table
  for(int y = 0; y < h; y++)
  for(int x = 0; x < w; x++)
  {
    int angle, distance;
    float ratio = 32.0;
    distance = int(ratio * texHeight / sqrt((x - w / 2.0) * (x - w / 2.0) + (y - h / 2.0) * (y - h / 2.0))) % texHeight;
    angle = (unsigned int)(0.5 * texWidth * atan2(y - h / 2.0, x - w / 2.0) / 3.1416);
    distanceTable[y][x] = distance;
    angleTable[y][x] = angle;
  }

然后动画循环开始。

animation 变量设置为以秒为单位的时间,用于偏移旋转和移动所用的表。

对于每个像素 (x, y),通过查表并加上动画偏移值从纹理中获取正确的纹素。对纹理宽高取模,确保不会请求纹理外的像素,而是使用平铺的纹理。使用无符号整数,因为值可能变为负数,而取模运算只有在无符号数下才能正确处理负数的环绕。因此纹理的宽高应为 2 的幂次,否则平铺效果不好。动画值(shiftX 和 shiftY)乘以纹理宽高,使特效速度与纹理大小无关,只与时间有关。

距离和角度的动画也乘以一个常量(这里分别为 1.0 和 0.25),通过修改这些值可以独立调整旋转速度和前进速度。

屏幕像素绘制到缓冲区,所有像素绘制完成后,缓冲区输出到屏幕,然后开始下一帧的处理。

  float animation;
  //begin the loop
  while(!done())
  {
    animation = getTime();
    //calculate the shift values out of the animation value
    int shiftX = int(texWidth * 1.0 * animation);
    int shiftY = int(texHeight * 0.25 * animation);

    for(int y = 0; y < h; y++)
    for(int x = 0; x < w; x++)
    {
      //get the texel from the texture by using the tables, shifted with the animation values
      int color = texture[(unsigned int)(distanceTable[y][x] + shiftX)  % texWidth][(unsigned int)(angleTable[y][x] + shiftY) % texHeight];
      buffer[y][x] = color;
    }
    drawBuffer(buffer[0]);
    redraw();
  }
  return(0);
}

隧道效果如下图所示,实际运行时会有动画:



更好的纹理


用以下代码替换生成 XOR 图案的代码,以加载纹理:

loadBMP("textures/tunnel.bmp", texture[0], texWidth, texHeight);

以下是两张来自《虚幻竞技场 2003》的纹理应用于隧道的效果:





环视四周

由于一直向前移动,特效很快就会变得单调。为了增加趣味性,可以让摄像机环视四周。只需移动隧道中心,就会产生摄像机旋转的感觉。若要移动隧道中心,需要更大的预计算缓冲区。以下是实现该功能的修改版代码,加粗部分为新增或修改的内容,注释会加以说明。

#define texWidth 256
#define texHeight 256
#define screenWidth 400
#define screenHeight 300

int texture[texWidth][texHeight];

//Make the tables twice as big as the screen. The center of the buffers is now the position (w,h).
int distanceTable[2 * screenWidth][2 * screenHeight];
int angleTable[2 * screenWidth][2 * screenHeight];

int buffer[screenWidth][screenHeight];

int main(int argc, char *argv[])
{
  screen(screenWidth, screenHeight, 0, "Tunnel Effect");

  loadBMP("textures/tunnel.bmp", texture[0], texWidth, texHeight);

  //generate non-linear transformation table, now for the bigger buffers (twice as big)
  for(int y = 0; y < h * 2; y++)
  for(int x = 0; x < w * 2; x++)
  {
    int angle, distance;
    float ratio = 32.0;
    //these formulas are changed to work with the new center of the tables
    distance = int(ratio * texHeight / sqrt(float((x - w) * (x - w) + (y - h) * (y - h)))) % texHeight;
    angle = (unsigned int)(0.5 * texWidth * atan2(float(y - h), float(x - w)) / 3.1416);
    distanceTable[y][x] = distance;
    angleTable[y][x] = angle;
  }

  float animation;

  //begin the loop
  while(!done())
  {
    animation = getTime() / 1000.0;

    //calculate the shift values out of the animation value
    int shiftX = int(texWidth * 1.0 * animation);
    int shiftY = int(texHeight * 0.25 * animation);
    //calculate the look values out of the animation value
    //by using sine functions, it'll alternate between looking left/right and up/down
    //make sure that x + shiftLookX never goes outside the dimensions of the table, same for y
    int shiftLookX = w / 2 + int(w / 2 * sin(animation));
    int shiftLookY = h / 2 + int(h / 2 * sin(animation * 2.0));

    for(int y = 0; y < h; y++)
    for(int x = 0; x < w; x++)
    {
      //get the texel from the texture by using the tables, shifted with the animation variable
      //now, x and y are shifted as well with the "look" animation values
      int color = texture[(unsigned int)(distanceTable[x + shiftLookX][y + shiftLookY] + shiftX)  % texWidth]
                 [(unsigned int)(angleTable[x + shiftLookX][y + shiftLookY]+ shiftY) % texHeight];
      buffer[y][x] = color;
    }
    drawBuffer(buffer[0]);
    redraw();
  }

  return(0);
}

例如,此时正在向左下方看,所以隧道中心位于屏幕右上方,但在特效运行过程中视角方向会不断变化:



如果你感兴趣,由于使用了两个正弦函数来环视四周——一个用于 x 方向,一个用于 y 方向——环视运动(即隧道中心在屏幕上的运动轨迹)呈利萨如图形(Lissajous figure)的形状。


最后编辑:2004 年

版权所有 (c) 2004-2007 Lode Vandevenne,保留所有权利。UT2003 纹理版权归 Epic Games 所有。