Raycasting学习记录_进阶与扩展

这篇博客是Raycasting学习记录的进阶与扩展部分,会在原来实现的基础上新添加一些内容

因此请先阅读Raycasting学习记录的内容,对“Raycasting”过程有了大致了解后再来阅读本博客

源代码和可执行程序已经部署到github上:Raycasting

纹理地板天花板

visual studio的Release模式

注意:实现此功能时,务必使用visual studio的Release模式,因为Debug模式非常卡

这是因为Debug模式和Release模式在编译选项上有所不同:

  • Debug模式:在Debug模式下,编译器会保留调试信息和不进行优化,以便开发者可以跟踪代码的执行情况,找出和修复错误。由于保留了更多的信息并且没有进行优化,所以Debug模式下的程序运行速度通常会比Release模式慢。
  • Release模式:在Release模式下,编译器会进行各种优化,比如消除冗余代码,进行代码重排,内联函数等,以提高程序的运行速度。同时,编译器不会保留调试信息,因此Release模式下的程序文件通常会比Debug模式小。

所以,在开发和调试程序时,通常会使用Debug模式。而当程序开发完成,准备发布时,通常会切换到Release模式,以提高程序的运行效率。


博客里面介绍了这种方法,但是不能像之前画墙一样取巧了,这里只能逐像素操作

画墙的时候,可以直接使用一条完整的竖线来截取纹理上对应一个像素宽度的部分,每一条竖线都对应纹理上的一个像素宽度,从而实现了墙壁的纹理映射

但是地板很不一样,因为同一条横线上可能会跨越好几块不同的地板,因此此处采用逐像素操作

SFML逐像素操作

SFML的逐像素操作逻辑有些奇怪,如果要对宽screenWidthscreenHeight的窗口进行逐像素操作,步骤如下:

  1. 首先需要创建宽screenWidthscreenHeight的一张图像Image对象(因为只有Image能逐像素设置颜色,TextureSprite不行)
  2. 对这个Image调用setPixel函数逐像素设置颜色
  3. Image每个像素设置好颜色后,用这张Image创建一个新的纹理Texture对象(因为Image不能直接设置Sprite的纹理)
  4. 最后创建一个Sprite,用刚刚的纹理对象设置这个Sprite的纹理
  5. 最后在窗口上画出这个Sprite(因为draw函数只能画Sprite和一系列图形,不能画ImageTexture,所以需要这么麻烦才能逐像素操作,闹麻了

因此,地板天花板的实现就会按照这个逻辑执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void Floor_Ceiling_Casting(RenderWindow &window)
{
// 创建一个新的Image
Image Floor_Ceiling_image;
Floor_Ceiling_image.create(screenWidth, screenHeight);
...
// 遍历填充这个Image
for (int y = 0; y < screenHeight; y++)
{
...
for (int x = 0; x < screenWidth; x++)
{
...
// 计算像素颜色,逐像素赋值
Color Floorcolor = Floor_image.getPixel(tx, ty);
Color Ceilingcolor = Ceiling_image.getPixel(tx, ty);
Floor_Ceiling_image.setPixel(x, y, Floorcolor);
Floor_Ceiling_image.setPixel(x, screenHeight - y - 1, Ceilingcolor);
}
}
// 创建一个新的Texture
Texture Floor_Ceiling_texture;
Floor_Ceiling_texture.loadFromImage(Floor_Ceiling_image);
// 创建一个新的Sprite并设置其纹理
Sprite sprite;
sprite.setTexture(Floor_Ceiling_texture);
// 把这个Sprite画在屏幕上
window.draw(sprite);
}

绘制天花板的方式与绘制地板的方式相同,所以这里只解释地板,坐标几乎可以复用

画天花板和地板是在画墙之前的,简单来说,地板的投射工作如下:逐行扫描

对于当前的扫描线,计算匹配扫描线左像素的地板位置,以及匹配右像素的位置

这可以计算为从相机发出,通过相机平面的那个像素的射线击中地板的位置

具体逻辑

令人意想不到的是,地板的绘制比纹理墙面简单一些,只是不能像墙面绘制一样取巧

大致思路是一样的,并且不用考虑镜面翻转

计算FOV起点终点

在开始遍历之前,需要计算一些必要变量

首先是人物FOV的起点和终点,可以通过方向向量\(\overrightarrow{dir}\)和相机平面向量\(\overrightarrow{plane}\)加减法计算出来

1
2
3
4
5
// rayDir for leftmost ray (x = 0) and rightmost ray (x = w)
float FOV_StartX = dirX - planeX;
float FOV_StartY = dirY - planeY;
float FOV_EndX = dirX + planeX;
float FOV_EndY = dirY + planeY;

遍历y并计算rowDistance

所谓rowDistance,就是沿人物\(\overrightarrow{dir}\)方向的距离,我们需要根据这个距离计算地板与天花板对应坐标

一个具体的y会对应一个rowDistance,进而对应一条横线

y的取值可以从screenHeight / 2开始(因为天花板坐标可以复用地板的),这样可以减少一半计算量


比起原教程,我认为这个视频的图像更直观(毕竟我为了画出下面这个图还是画了一段时间的)

假设人物面前1个单位的距离有一个垂直平面,从摄像机发出的光线经过这个平面,交点就是y

如图所示,红色三角形和紫色三角形相似

在代码中,我们指定图中cam_z这个变量为posZ,大小为屏幕高度的一半,即posZ = 0.5 * screenHeight

指定图中xrowDistance,根据三角形相似得出rowDistance = posZ / row_y


可以通过这个动图了解所谓rowDistance有什么作用,左边是俯视图,右边是屏幕:

一个rowDistance就对应一条横线,画出全部横线就完成了画地板

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// Vertical position of the camera.
float posZ = 0.5 * screenHeight;

// 遍历全部y
for (int y = 0; y < screenHeight; y++)
{
// Current y position compared to the center of the screen (the horizon)
int row_y = y - screenHeight / 2;

// Horizontal distance from the camera to the floor for the current row.
// 0.5 is the z position exactly in the middle between floor and ceiling.
float rowDistance = posZ / row_y;
......

计算遍历变量

根据相似三角形,可以计算出floor遍历的起点,通过FOV的起点计算

代码如下:

1
2
3
// real world coordinates of the leftmost column. This will be updated as we step to the right.
float floorX = posX + rowDistance * FOV_StartX;
float floorY = posY + rowDistance * FOV_StartY;

还是根据相似三角形,求出stepXstepY

1
2
3
4
// calculate the real world step vector we have to add for each x (parallel to camera plane)
// adding step by step avoids multiplications with a weight in the inner loop
float floorStepX = rowDistance * (FOV_EndX - FOV_StartX) / screenWidth;
float floorStepY = rowDistance * (FOV_EndY - FOV_StartY) / screenWidth;

遍历x

跟纹理墙壁逻辑差不多

计算出tx, ty,找到纹理图片对应位置,把对应颜色画在屏幕(x, y)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
for (int x = 0; x < screenWidth; ++x)
{
// the cell coord is simply got from the integer parts of floorX and floorY
int cellX = (int)(floorX);
int cellY = (int)(floorY);

// get the texture coordinate from the fractional part
int tx = (int)(texWidth * (floorX - cellX)) & (texWidth - 1);
int ty = (int)(texHeight * (floorY - cellY)) & (texHeight - 1);

floorX += floorStepX;
floorY += floorStepY;

// 计算像素颜色,逐像素赋值
Color Floorcolor = Floor_image.getPixel(tx, ty);
Color Ceilingcolor = Ceiling_image.getPixel(tx, ty);
Floor_Ceiling_image.setPixel(x, y, Floorcolor);
Floor_Ceiling_image.setPixel(x, screenHeight - y - 1, Ceilingcolor);
}

效果

把画墙壁的函数注释掉,仅保留画地板天花板函数,效果如下,非常好

对应代码

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
void Floor_Ceiling_Casting(RenderWindow &window)
{
// 创建一个新的Image
Image Floor_Ceiling_image;
Floor_Ceiling_image.create(screenWidth, screenHeight);

// rayDir for leftmost ray (x = 0) and rightmost ray (x = w)
float FOV_StartX = dirX - planeX;
float FOV_StartY = dirY - planeY;
float FOV_EndX = dirX + planeX;
float FOV_EndY = dirY + planeY;

// Vertical position of the camera.
float posZ = 0.5 * screenHeight;

// FLOOR AND CEILING CASTING
for (int y = screenHeight / 2; y < screenHeight; y++) //其实可以直接从screenHeight / 2处开始遍历
//for (int y = 0; y < screenHeight; y++)
{
// Current y position compared to the center of the screen (the horizon)
int row_y = y - screenHeight / 2;

// Horizontal distance from the camera to the floor for the current row.
// 0.5 is the z position exactly in the middle between floor and ceiling.
float rowDistance = posZ / row_y;

// real world coordinates of the leftmost column. This will be updated as we step to the right.
float floorX = posX + rowDistance * FOV_StartX;
float floorY = posY + rowDistance * FOV_StartY;

// calculate the real world step vector we have to add for each x (parallel to camera plane)
// adding step by step avoids multiplications with a weight in the inner loop
float floorStepX = rowDistance * (FOV_EndX - FOV_StartX) / screenWidth;
float floorStepY = rowDistance * (FOV_EndY - FOV_StartY) / screenWidth;

for (int x = 0; x < screenWidth; ++x)
{
// the cell coord is simply got from the integer parts of floorX and floorY
int cellX = (int)(floorX);
int cellY = (int)(floorY);

// get the texture coordinate from the fractional part
int tx = (int)(texWidth * (floorX - cellX)) & (texWidth - 1);
int ty = (int)(texHeight * (floorY - cellY)) & (texHeight - 1);

floorX += floorStepX;
floorY += floorStepY;

// 计算像素颜色,逐像素赋值
Color Floorcolor = Floor_image.getPixel(tx, ty);
Color Ceilingcolor = Ceiling_image.getPixel(tx, ty);
Floor_Ceiling_image.setPixel(x, y, Floorcolor);
Floor_Ceiling_image.setPixel(x, screenHeight - y - 1, Ceilingcolor);
}
}
// 创建一个新的Texture
Texture Floor_Ceiling_texture;
Floor_Ceiling_texture.loadFromImage(Floor_Ceiling_image);
// 创建一个新的Sprite并设置其纹理
Sprite sprite;
sprite.setTexture(Floor_Ceiling_texture);
// 把这个Sprite画在屏幕上
window.draw(sprite);
}

鼠标控制视角旋转

之前控制旋转是用键盘的左右键实现的,转起来跟坦克一样

这次加入鼠标逻辑,并且跟正常游戏差不多,鼠标移动的快慢会影响视角转动的速度


鼠标转向的大致思路很容易,大一做绘图软件的时候实现过类似的操作:拖拽图形移动

通过判断这一帧和上一帧鼠标位置之间的差值,计算出图形对应位移的向量;对应到这里,就是计算出对应视角旋转的度数

但是鼠标在移动的过程中容易移动出窗口;这个旋转角度具体怎么计算?我参考这个视频以及对应的代码得出了一个很不错的解法:

  1. 固定鼠标在窗口中心,这样就不会移动出窗口
  2. 固定鼠标这个操作是在计算完旋转角度后做的,所以“这一帧和上一帧鼠标位置之间的差值”就直接转换为“这一帧鼠标位置和窗口中心位置的差值”,就不用保存上一帧的位置了
  3. 这一帧鼠标位置和窗口中心位置的差值只考虑水平分量,将这个差值除以窗口的宽度,得到一个介于-0.50.5之间的值,最后乘以水平FOV就得到理想的旋转角度

大致思路有了,下面开始实现


设置鼠标指针

因为涉及到鼠标操作,就需要把鼠标指针可视化关闭,并且让鼠标总是居中在窗口中央

可以在main函数开始主循环之前设置鼠标,代码如下:

1
2
Mouse::setPosition(Vector2i(screenWidth * 0.5f, screenHeight * 0.5f), window);
window.setMouseCursorVisible(0);

因为鼠标不能正常控制关闭了,需要添加按ESC退出,故在main函数主循环的事件处理循环中添加:

1
2
3
4
5
while (window.pollEvent(event))
{
if (event.type == sf::Event::Closed || sf::Keyboard::isKeyPressed(sf::Keyboard::Escape))
window.close();
}


FOV(Field Of Vision)

需要写一个计算FOV的函数

不过令我意外的是,我在之前的博客中并没有提到FOV(Field Of Vision)的计算,那就在这里补上吧

如图所示,最外面两条红线之间的夹角就是FOV,这个角度可以通过方向向量\(\overrightarrow{dir}\)和相机平面向量\(\overrightarrow{plane}\)的长度求出

而且可以看出,在方向向量\(\overrightarrow{dir}\)不变的前提下,相机平面向量\(\overrightarrow{plane}\)的长度越短,FOV越小

具体公式如下: \[ \begin{align*} \tan(\frac{FOV}{2})&=\frac{|\overrightarrow{plane}|}{|\overrightarrow{dir}|}\\ FOV&=\arctan(\frac{|\overrightarrow{plane}|}{|\overrightarrow{dir}|})\times 2 \end{align*} \] 计算FOV的函数如下:

1
2
3
4
5
6
7
8
9
10
// 计算FOV大小
double calculate_FOV()
{
double length_dir = sqrt(dirX * dirX + dirY * dirY);
double length_plane = sqrt(planeX * planeX + planeY * planeY);
// 弧度制
return atan(length_plane / length_dir) * 2;
// 角度制
//// return atan(length_plane / length_dir) * 2 * 180 / acos(-1);
}

注意:C++里面数学函数角度计算是弧度制,因此要得到具体的角度需要处理一下: \[ \begin{align*} \text{radian}=\frac{\text{degree}}{180}\times\pi \end{align*} \] 但是,我们后面的计算全部都需要用到三角函数,所以在这里还是统一采用弧度制的运算(不然鼠标一转就上天了,真的要注意统一)

可以在main函数开始循环之前就申明FOV这个变量,并且计算出具体的弧度制,然后就可以代入运算了

具体逻辑

计算好了FOV,就可以在Move函数中添加鼠标移动旋转方向向量和相机平面向量的逻辑了,具体逻辑如下:

  1. 获取窗口的中心位置
  2. 获取鼠标当前位置
  3. 计算鼠标的旋转角度
  4. 旋转方向向量\(\overrightarrow{dir}\)和相机平面向量\(\overrightarrow{plane}\)
  5. 将鼠标位置重置到窗口中心

对应代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 这些是计算鼠标移动逻辑
// 获取窗口的中心位置
Vector2i center(screenWidth * 0.5f, screenHeight * 0.5f);

// 获取鼠标当前位置
Vector2i position_now = Mouse::getPosition(window);

// 计算鼠标的旋转角度
// 水平方向旋转角度
double rotation_degree_horizontal = (center.x - position_now.x) * FOV / screenWidth;

// 更新方向
// dir和plane一起更新
double oldDirX = dirX;
dirX = dirX * cos(rotation_degree_horizontal) - dirY * sin(rotation_degree_horizontal);
dirY = oldDirX * sin(rotation_degree_horizontal) + dirY * cos(rotation_degree_horizontal);
double oldPlaneX = planeX;
planeX = planeX * cos(rotation_degree_horizontal) - planeY * sin(rotation_degree_horizontal);
planeY = oldPlaneX * sin(rotation_degree_horizontal) + planeY * cos(rotation_degree_horizontal);

// 将鼠标位置重置到窗口中心
Mouse::setPosition(center, window);

这段逻辑是独立于键盘操作的逻辑,因此放在最开始或者最后面都可以,只要不影响键盘逻辑就好


由于实现绘制decoration和illustration时需要截图,因此添加一个全局变量布尔值,用于控制是否允许鼠标逻辑:

1
2
//debug用,每次手动注释掉鼠标的逻辑烦死我了
bool ifMouse = 1;

main函数中修改:

1
2
3
4
5
if (ifMouse)
{
Mouse::setPosition(Vector2i(screenWidth * 0.5f, screenHeight * 0.5f), window);
window.setMouseCursorVisible(0);
}

Move函数中修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (ifMouse)
{
// 更新方向
// dir和plane一起更新
double oldDirX = dirX;
dirX = dirX * cos(rotation_degree_horizontal) - dirY * sin(rotation_degree_horizontal);
dirY = oldDirX * sin(rotation_degree_horizontal) + dirY * cos(rotation_degree_horizontal);
double oldPlaneX = planeX;
planeX = planeX * cos(rotation_degree_horizontal) - planeY * sin(rotation_degree_horizontal);
planeY = oldPlaneX * sin(rotation_degree_horizontal) + planeY * cos(rotation_degree_horizontal);

// 将鼠标位置重置到窗口中心
Mouse::setPosition(center, window);
}

增加Decoration Sprite

必要变量

首先定义一系列变量:

1
2
3
4
// decoration纹理总数
#define textureNum_Decoration 3
// decoration总数
#define decorationNum 19

画出一个decoration,需要记录x, y, texture_index,因此增加一个Decoration类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/************************************************************/
/* Decoration
/************************************************************/
class Decoration
{
public:
double x;
double y;
int texture_index;
int type; // 用于指明是decoration还是illustration
// 0为decoration,1为illustration
Decoration(double x = 0, double y = 0, int texindex = 0)
{
this->x = x;
this->y = y;
this->texture_index = texindex;
this->type = 0;
}
};

除此之外,decoration还需要读取新的纹理,新建一个纹理数组存储:

1
2
// decoration纹理数组
Texture decoration_texture[textureNum_Decoration];

还需要存储所有decoration的信息,建立decoration_lst数组存储x, y, texture_index

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// decoration数组,用于记录全部decoration的位置和对应纹理下标
Decoration decoration_lst[decorationNum] =
{
{20.5, 11.5, 0}, // green light in front of playerstart
// green lights in every room
{18.5, 4.5, 0},
{10.0, 4.5, 0},
{10.0, 12.5, 0},
{3.5, 6.5, 0},
{3.5, 20.5, 0},
{3.5, 14.5, 0},
{14.5, 20.5, 0},

// row of pillars in front of wall: fisheye test
{18.5, 10.5, 1},
{18.5, 11.5, 1},
{18.5, 12.5, 1},

// some barrels around the map
{21.5, 1.5, 2},
{15.5, 1.5, 2},
{16.0, 1.8, 2},
{16.2, 1.2, 2},
{3.5, 2.5, 2},
{9.5, 15.5, 2},
{10.0, 15.1, 2},
{10.5, 15.8, 2},
};

考虑到墙面的遮挡,需要建立一个ZBuffer数组,用于表示深度,即屏幕上每个x对应的perpWallDist

人视角只能发出screenWidth束光线,于是就建立大小为screenWidthZBuffer

1
2
// ZBuffer数组,记录屏幕上每个x对应的perpWallDist
double ZBuffer[screenWidth] = {0};

然后在Wall_Casting函数中对应位置更新ZBuffer

1
2
3
4
5
6
7
8
9
10
11
12
for (int x = 0; x < screenWidth; x++)
{
...
// 此处求出perpWallDist
if (side == 0)
perpWallDist = (sideDistX - deltaDistX);
else
perpWallDist = (sideDistY - deltaDistY);
// 紧接着更新ZBuffer
ZBuffer[x] = perpWallDist;
...
}

读取纹理时透明处理

这是一张灯的贴图:

可以看到,只有顶部有图,剩下都是纯黑色部分

这些纯黑色部分在绘制的时候,应该画成“完全透明”的颜色,即alpha值改为0

这一步操作可以在读取纹理的时候进行,我们先写一个SetTexture_Alpha函数,功能是遍历读取的纹理图像,将纯黑色的地方改为透明

SFML中对纹理图片的操作方法在上文这里提及,此处差不多

用读取的texture建立一个image,再对这个image进行逐像素操作,黑色改为完全透明,最后再把texture改写即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void SetTexture_Alpha(Texture *texture, int start_index, int end_index)
{
for (int i = start_index; i <= end_index; i++)
{
Image tmp = texture[i].copyToImage();
for (int y = 0; y < tmp.getSize().y; y++)
{
for (int x = 0; x < tmp.getSize().x; x++)
{
if (tmp.getPixel(x, y) == Color::Black)
{
// 设置为完全透明
tmp.setPixel(x, y, sf::Color(0, 0, 0, 0));
}
}
}
texture[i].loadFromImage(tmp); // 用修改后的图像覆盖原数组的纹理
}
}

然后就能在读取纹理的时候调用这个函数,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void LoadTexture()
{
// 读取墙壁、地板天花板纹理
texture[0].loadFromFile("pics/eagle.png");
texture[1].loadFromFile("pics/redbrick.png");
texture[2].loadFromFile("pics/purplestone.png");
texture[3].loadFromFile("pics/greystone.png");
texture[4].loadFromFile("pics/bluestone.png");
texture[5].loadFromFile("pics/mossy.png");
texture[6].loadFromFile("pics/wood.png");
texture[7].loadFromFile("pics/colorstone.png");
// 地板天花板渲染用的image
Ceiling_image = texture[6].copyToImage(); // 天花板为木头
Floor_image = texture[3].copyToImage(); // 地板为石头
// 读取decoration纹理
decoration_texture[0].loadFromFile("pics/greenlight.png");
decoration_texture[1].loadFromFile("pics/pillar.png");
decoration_texture[2].loadFromFile("pics/barrel.png");
// 纯黑部分改透明
SetTexture_Alpha(decoration_texture, 0, 2);
}

具体逻辑

理解这段逻辑,得对线性代数里面“基变换”的内容有所了解,不然可能就不知道我在说什么(也可能是我讲解的方式不清楚)

原教程里完全没有提到坐标变换的解释,因此我在这里会说的详细一点

  1. 将decoration从远到近排序,这样能实现先画远的,再画近的
  2. 得到decoration和玩家的相对位置,可以用decoration的坐标减去玩家坐标,得到一个从玩家指向decoration的向量\(\overrightarrow{a}\),这个是真实世界坐标轴下的向量(即以\((1,0),(0,1)\)作为基的坐标系)
  3. 在以\(\overrightarrow{plane},\overrightarrow{dir}\)为基的向量坐标系(这个坐标系貌似叫“相机坐标系”)下,指示玩家到decoration的向量为\(\overrightarrow{b}\),我们就需要求出这个\(\overrightarrow{b}\),从而正确画在屏幕上
  4. 根据基变换(如果对线性代数的基变换理解不是很清楚,墙裂推荐观看《线性代数的本质:基变换》这个章节,把\(\overrightarrow{b}\)理解为真实世界坐标系下的“误解”就可以):\(\overrightarrow{a}=Mat\cdot\overrightarrow{b}\),因此可以两遍同时左乘\(Mat^{-1}\)得到\(\overrightarrow{b}\);也就是说,想得到\(\overrightarrow{a}\)在以\(\overrightarrow{plane},\overrightarrow{dir}\)为基向量的坐标系下怎么表示,左乘\(Mat^{-1}\)就能得到答案
  5. 求出decoration Sprite在屏幕上的坐标
  6. 求出decoration Sprite在屏幕上的范围,即drawStartX, drawEndX, drawStartY,可以不用求出drawEndY,因为这里和画墙一样取了个巧,纹理图案上一个x直接对应了宽度为1像素的一条纹理
  7. 绘制!这里跟画纹理墙的思路一模一样

排序

想在对应位置画出正确的decoration Sprite,需要先画远处的,再画近处的,因为后画的近处Sprite会直接覆盖远处的图层,可以正确画出层叠关系

因此,我们需要在每一帧计算每个decoration与玩家的距离,存储在数组中,再排序就可以得到

为了防止下标混乱,这里把下标和距离绑定成一个pair<double, int>进行排序

同时为了获得这个pair,建立下标数组和距离数组:

1
2
3
4
// arrays used to sort the sprites
// Order数组和Distance数组,分别存储下标和离玩家的距离
int decoration_Order[decorationNum];
double decoration_Distance[decorationNum];

排序函数调用C++的sort函数,针对pair默认就用第一个变量按升序排列

最后再按降序赋值到原数组中,就实现距离从远到近排列,并且下标数组元素和距离一一对应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// sort algorithm
// sort the sprites based on distance
void sortSprites(int *order, double *dist, int amount)
{
std::vector<std::pair<double, int>> sprites(amount);
for (int i = 0; i < amount; i++)
{
sprites[i].first = dist[i];
sprites[i].second = order[i];
}
// 针对pair默认就用第一个变量按升序排列
std::sort(sprites.begin(), sprites.end());
// restore in reverse order to go from farthest to nearest
for (int i = 0; i < amount; i++)
{
dist[i] = sprites[amount - i - 1].first;
order[i] = sprites[amount - i - 1].second;
}
}

然后遍历decoration_Order数组,就能拿到从远到近每一个decoration Sprite的下标

基变换

具体逻辑那里可能讲的不是很清楚(?)那这里就根据代码讲解吧

首先,得到decoration和玩家的相对位置,可以用decoration的坐标减去玩家坐标,得到一个从玩家指向decoration的向量\(\overrightarrow{a}\)

这个向量\(\overrightarrow{a}\)是真实世界坐标轴下的向量(即以\((1,0),(0,1)\)作为基的坐标系)

1
2
3
// translate sprite position to relative to camera
double spriteX = decoration_lst[decoration_Order[i]].x - posX;
double spriteY = decoration_lst[decoration_Order[i]].y - posY;

如代码所示,向量\(\overrightarrow{a}\)就可以表示为: \[ \overrightarrow{a}= \begin{bmatrix} \text{spriteX}\\ \text{spriteY} \end{bmatrix} \]

在以\(\overrightarrow{plane},\overrightarrow{dir}\)为基的向量坐标系(这个坐标系貌似叫“相机坐标系”)下,指示玩家到decoration的向量为\(\overrightarrow{b}\)

我们就需要求出这个\(\overrightarrow{b}\),从而正确画在屏幕上

定义\(\overrightarrow{b}\)向量为: \[ \overrightarrow{b}= \begin{bmatrix} \text{transformX} \\ \text{transformY} \end{bmatrix} \]

根据基变换(再次墙裂推荐观看《线性代数的本质:基变换》这个章节),\(\overrightarrow{a}=Mat\cdot\overrightarrow{b}\),因此可以两遍同时左乘\(Mat^{-1}\)求解\(\overrightarrow{b}\)向量

其中,\(Mat\)矩阵是以\(\overrightarrow{plane}\)为第一列,\(\overrightarrow{dir}\)为第二列的矩阵,定义为: \[ Mat= \begin{bmatrix} \text{planeX} & \text{dirX} \\ \text{planeY} & \text{dirY} \end{bmatrix} \] 也就是说,想得到\(\overrightarrow{a}\)在以\(\overrightarrow{plane},\overrightarrow{dir}\)为基的向量坐标系下怎么表示,左乘\(Mat^{-1}\)就能得到答案

1
2
3
4
5
6
7
8
9
// transform sprite with the inverse camera matrix
// [ planeX dirX ] -1 [ dirY -dirX ]
// [ ] = 1/(planeX*dirY-dirX*planeY) * [ ]
// [ planeY dirY ] [ -planeY planeX ]

double invDet = 1.0 / (planeX * dirY - dirX * planeY); // required for correct matrix multiplication

double transformX = invDet * (dirY * spriteX - dirX * spriteY);
double transformY = invDet * (-planeY * spriteX + planeX * spriteY); // this is actually the depth inside the screen, that what Z is in 3D

由于没有引入Eigen库,用不了线性代数运算,因此只能把逆矩阵的求法写成代码里的这样

这段代码的意思就是: \[ \begin{align*} \overrightarrow{b}&=Mat^{-1}\cdot\overrightarrow{a}\\\\ \begin{bmatrix} \text{transformX} \\ \text{transformY} \end{bmatrix}&= \begin{bmatrix} \text{planeX} & \text{dirX} \\ \text{planeY} & \text{dirY} \end{bmatrix}^{-1}\cdot \begin{bmatrix} \text{spriteX}\\ \text{spriteY} \end{bmatrix} \end{align*} \] 故求出\(\overrightarrow{b}\)向量: \[ \overrightarrow{b}= \begin{bmatrix} \text{transformX} \\ \text{transformY} \end{bmatrix} \]

计算decoration Sprite在屏幕上的范围

首先计算sprite在屏幕上的x坐标

1
2
// sprite在屏幕上的x坐标
int spriteScreenX = int((screenWidth / 2) * (1 + transformX / transformY));

理解这个可以采取一些极端情况,比如人物立绘就在视角正前方时,transformX = 0, transformY != 0\(\overrightarrow{b}\)向量是以\(\overrightarrow{plane},\overrightarrow{dir}\)为基,transformX = 0表示没有左右偏移,即在正前方)

又比如有两个decoration Sprite: A和B,对应的transformX完全一样,但是transformY_B > transformY_A,画在图上就是:

C为A在屏幕上的点,D为B在屏幕上的点,明显看出D比C更靠近屏幕中心


计算spriteHeightspriteWidth

1
2
3
4
// calculate height of the sprite on screen
int spriteHeight = abs(int(screenHeight / (transformY))); // using 'transformY' instead of the real distance prevents fisheye
// calculate width of the sprite
int spriteWidth = abs(int(screenHeight / (transformY)));

用屏幕的宽度和高度做比例就行


计算drawStartX, drawEndX, drawStartY,这部分很好理解

1
2
3
4
5
6
7
8
9
10
11
12
// calculate lowest and highest pixel to fill in current stripe
// 计算y上的起点,不需要计算终点,因为竖线直接指定了Sprite的高度
int drawStartY = -spriteHeight / 2 + screenHeight / 2;
// if (drawStartY < 0) drawStartY = 0;

// 计算x上的起点和终点
int drawStartX = -spriteWidth / 2 + spriteScreenX;
if (drawStartX < 0)
drawStartX = 0;
int drawEndX = spriteWidth / 2 + spriteScreenX;
if (drawEndX >= screenWidth)
drawEndX = screenWidth - 1;

drawStartY还是以屏幕中心screenHeight/ 2为基准做减法

drawStartXdrawEndX:以纹理坐标中心spriteScreenX为基准,加减spriteWidth / 2

绘制

然后就可以从drawStartX遍历到drawEndX进行绘制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// loop through every vertical stripe of the sprite on screen
for (int stripe = drawStartX; stripe < drawEndX; stripe++)
{
int texX = int(256 * (stripe - (-spriteWidth / 2 + spriteScreenX)) * texWidth / spriteWidth) / 256;
// the conditions in the if are:
// 1) it's in front of camera plane so you don't see things behind you
// 2) it's on the screen (left)
// 3) it's on the screen (right)
// 4) ZBuffer, with perpendicular distance
if (transformY > 0 && stripe > 0 && stripe < screenWidth && transformY < ZBuffer[stripe])
{
// decoration一条线的Sprite
Sprite decoration_sprite;
// 设置对应纹理
decoration_sprite.setTexture(decoration_texture[decoration_lst[decoration_Order[i]].texture_index]);
// 设置sprite的纹理矩形,定义了在纹理中的哪个部分应用到decoration_sprite上
// 宽度为1个像素,相当于原来的画线
decoration_sprite.setTextureRect(sf::IntRect(texX, 0, 1, texHeight));
// 设置sprite的位置,坐标指定了左上角的位置
decoration_sprite.setPosition(stripe, drawStartY);
// 设置sprite的缩放因子
decoration_sprite.setScale(1, spriteHeight / (float)texHeight);
// 绘制sprite
window.draw(decoration_sprite);
}
}

绘制步骤还是和画纹理墙一样:

  1. 利用texX求出纹理图像上对应到当前像素列的位置
  2. 建立一个新的Sprite,对其设置相应纹理
  3. 设置这个Sprite为宽1个像素,高texHeight,相当于原来的画线
  4. 设置Sprite的位置,坐标指定了左上角的位置
  5. 设置Sprite的缩放因子,具体缩放y方向上的,缩放大小为spriteHeight / (float)texHeight
  6. 调用draw函数绘制

首先是求出texX

1
int texX = int(256 * (stripe - (-spriteWidth / 2 + spriteScreenX)) * texWidth / spriteWidth) / 256;

其中,stripe - (-spriteWidth / 2 + spriteScreenX)是计算出当前遍历的x坐标与Sprite在屏幕上的中心位置的偏移量

这个偏移量乘以256texWidth / spriteWidth,这里的256是一个放大因子,用来增加精度;texWidth / spriteWidth是纹理图像宽度和Sprite宽度的比例,用来将偏移量从Sprite的宽度转换到纹理的宽度

这个放大因子有没有好像都不是很影响


紧接着就是绘制部分,绘制条件有几点:

  1. 当前深度transformYZBuffer[stripe]做比较,如果当前深度小于远处墙的perpWallDist,就能画,不然就应该被墙挡住
  2. stripe > 0 && stripe < screenWidth,即在屏幕范围内,如果drawStartXdrawEndX在屏幕之外,就不用画了
  3. transformY > 0,即在面前的才能画,如果在身后就不能画

然后就正常绘制

结果如下:

还是很不错的

源代码

Decoration_Casting函数代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// 画出Decoration Sprite
void Decoration_Casting(RenderWindow &window)
{
for (int i = 0; i < decorationNum; i++)
{
decoration_Order[i] = i;
decoration_Distance[i] = ((posX - decoration_lst[i].x) * (posX - decoration_lst[i].x) + (posY - decoration_lst[i].y) * (posY - decoration_lst[i].y)); // sqrt not taken, unneeded
}
// 排序
// 先画远的再画近的不会导致透明遮挡问题
// 因为近的Sprite透明地方可以直接显示出远的Sprite
sortSprites(decoration_Order, decoration_Distance, decorationNum);
// after sorting the sprites, do the projection and draw them
for (int i = 0; i < decorationNum; i++)
{
// translate sprite position to relative to camera
double spriteX = decoration_lst[decoration_Order[i]].x - posX;
double spriteY = decoration_lst[decoration_Order[i]].y - posY;

// transform sprite with the inverse camera matrix
// [ planeX dirX ] -1 [ dirY -dirX ]
// [ ] = 1/(planeX*dirY-dirX*planeY) * [ ]
// [ planeY dirY ] [ -planeY planeX ]

double invDet = 1.0 / (planeX * dirY - dirX * planeY); // required for correct matrix multiplication

double transformX = invDet * (dirY * spriteX - dirX * spriteY);
double transformY = invDet * (-planeY * spriteX + planeX * spriteY); // this is actually the depth inside the screen, that what Z is in 3D

// sprite在屏幕上的x坐标
int spriteScreenX = int((screenWidth / 2) * (1 + transformX / transformY));

// calculate height of the sprite on screen
int spriteHeight = abs(int(screenHeight / (transformY))); // using 'transformY' instead of the real distance prevents fisheye
// calculate width of the sprite
int spriteWidth = abs(int(screenHeight / (transformY)));

// calculate lowest and highest pixel to fill in current stripe
// 计算y上的起点,不需要计算终点,因为竖线直接指定了Sprite的高度
int drawStartY = -spriteHeight / 2 + screenHeight / 2;
// if (drawStartY < 0) drawStartY = 0;

// 计算x上的起点和终点
int drawStartX = -spriteWidth / 2 + spriteScreenX;
if (drawStartX < 0)
drawStartX = 0;
int drawEndX = spriteWidth / 2 + spriteScreenX;
if (drawEndX >= screenWidth)
drawEndX = screenWidth - 1;

// loop through every vertical stripe of the sprite on screen
for (int stripe = drawStartX; stripe < drawEndX; stripe++)
{
int texX = int(256 * (stripe - (-spriteWidth / 2 + spriteScreenX)) * texWidth / spriteWidth) / 256;
// the conditions in the if are:
// 1) it's in front of camera plane so you don't see things behind you
// 2) it's on the screen (left)
// 3) it's on the screen (right)
// 4) ZBuffer, with perpendicular distance
if (transformY > 0 && stripe > 0 && stripe < screenWidth && transformY < ZBuffer[stripe])
{
// decoration一条线的Sprite
Sprite decoration_sprite;
// 设置对应纹理
decoration_sprite.setTexture(decoration_texture[decoration_lst[decoration_Order[i]].texture_index]);
// 设置sprite的纹理矩形,定义了在纹理中的哪个部分应用到decoration_sprite上
// 宽度为1个像素,相当于原来的画线
decoration_sprite.setTextureRect(sf::IntRect(texX, 0, 1, texHeight));
// 设置sprite的位置,坐标指定了左上角的位置
decoration_sprite.setPosition(stripe, drawStartY);
// 设置sprite的缩放因子
decoration_sprite.setScale(1, spriteHeight / (float)texHeight);
// 绘制sprite
window.draw(decoration_sprite);
}
}
}
}

增加Illustration Sprite

这是我的私货,我是一个WA2粉丝,于是就想着能不能在我的raycaster里面把小木曾雪菜的立绘显示出来

《WA2》立绘解包

解包WA2立绘工程参考《白色相簿2》中的PAK文件解包

下载对应的素材包,然后把游戏立绘char.pak复制到同一路径

cmd中运行:

1
python wa2_unpak.py char.pak

就能把立绘文件解压出来了

必要变量

跟刚刚的decoration几乎一样,声明如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// illustration纹理总数
#define textureNum_Illustration 12
// illustration总数
#define illustrationNum 12
// 人物立绘宽高
// 由于人物立绘各个尺寸不一样,因此需要把所有图片缩放到370x690
// 缩放已经通过python实现,此处代码不考虑
#define illustrationWidth 370
#define illustrationHeight 690

//更新了一下posX
double posX = 22.5, posY = 11.5; // x and y start position


/************************************************************/
/* illustration
/************************************************************/
// illustration纹理数组
Texture illustration_texture[textureNum_Illustration];
class Illustration
{
public:
double x;
double y;
int texture_index;
int type; // 用于指明是decoration还是illustration
// 0为decoration,1为illustration
Illustration(double x = 0, double y = 0, int texindex = 0)
{
this->x = x;
this->y = y;
this->texture_index = texindex;
this->type = 1;
}
};
// illustration数组,用于记录全部illustration的位置和对应纹理下标
Illustration illustration_lst[illustrationNum] =
{
// 北原春希立绘,刚进游戏就能看见
{20.6, 11.5, 0},
// 小木曾雪菜流汗黄豆立绘
{18, 3.25, 1},
// 小木曾雪菜怒立绘
{8.0, 4.5, 2},
// 小木曾雪菜哭1
{21,8,3},
// 小木曾雪菜哭2
{22.5,3.5,4},
// 冬马和纱斗鸡眼
{17.1,14.7,5},
// 冬马和纱怒
{22.5,18.5,6},
// 冬马和纱闭眼
{16.8,21.5,7},
// 冬马和纱笑
{3,12,8},
// 杉浦小春
{10.1,18.3,9},
// 和泉千晶
{2.5,21.2,10},
// 风冈麻理
{6.4,19.2,11}
};
// arrays used to sort the sprites
// Order数组和Distance数组,分别存储下标和离玩家的距离
int illustration_Order[illustrationNum];
double illustration_Distance[illustrationNum];

注意,立绘解包出来的长宽各不一样,我用python把用到的立绘全部resize到了370 x 690像素,对应python代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import cv2
import os

# 定义输入和输出文件夹路径
input_folder = './'
output_folder = './resized'

# 创建输出文件夹(如果不存在)
os.makedirs(output_folder, exist_ok=True)

# 定义新的尺寸
new_size = (370, 690)

# 遍历输入文件夹中的所有文件
for filename in os.listdir(input_folder):
if filename.endswith(('.png', '.jpg', '.jpeg', '.bmp', '.tiff')):
# 构建完整的文件路径
file_path = os.path.join(input_folder, filename)

# 读取图像文件
image = cv2.imread(file_path)

if image is not None:
# 调整图像大小
resized_image = cv2.resize(image, new_size, interpolation=cv2.INTER_AREA)

# 构建输出文件路径
output_path = os.path.join(output_folder, filename)

# 保存调整大小后的图像
cv2.imwrite(output_path, resized_image)

print(f'Resized and saved {filename} to {output_path}')
else:
print(f'Failed to read {file_path}')

print('All images have been resized.')

读取纹理透明处理

逻辑还是一样的,首先读取纹理,然后调用修改alpha函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
void LoadTexture()
{
// 读取墙壁、地板天花板纹理
texture[0].loadFromFile("pics/eagle.png");
texture[1].loadFromFile("pics/redbrick.png");
texture[2].loadFromFile("pics/purplestone.png");
texture[3].loadFromFile("pics/greystone.png");
texture[4].loadFromFile("pics/bluestone.png");
texture[5].loadFromFile("pics/mossy.png");
texture[6].loadFromFile("pics/wood.png");
texture[7].loadFromFile("pics/colorstone.png");
// 地板天花板渲染用的image
Ceiling_image = texture[6].copyToImage(); // 天花板为木头
Floor_image = texture[3].copyToImage(); // 地板为石头
// 读取decoration纹理
decoration_texture[0].loadFromFile("pics/greenlight.png");
decoration_texture[1].loadFromFile("pics/pillar.png");
decoration_texture[2].loadFromFile("pics/barrel.png");
// 纯黑部分改透明
SetTexture_Alpha(decoration_texture, 0, 2);
// 读取illustration纹理
illustration_texture[0].loadFromFile("illustration/haruki.png");
illustration_texture[1].loadFromFile("illustration/setsuna0.png");
illustration_texture[2].loadFromFile("illustration/setsuna1.png");
illustration_texture[3].loadFromFile("illustration/setsuna2.png");
illustration_texture[4].loadFromFile("illustration/setsuna3.png");
illustration_texture[5].loadFromFile("illustration/kazusa0.png");
illustration_texture[6].loadFromFile("illustration/kazusa1.png");
illustration_texture[7].loadFromFile("illustration/kazusa2.png");
illustration_texture[8].loadFromFile("illustration/kazusa3.png");
illustration_texture[9].loadFromFile("illustration/koharu.png");
illustration_texture[10].loadFromFile("illustration/chiaki.png");
illustration_texture[11].loadFromFile("illustration/mari.png");
// 纯黑部分改透明
SetTexture_Alpha(illustration_texture, 0, 11);
}

源代码

逻辑也是一样的,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
// 画出illustration Sprite
// 如果illustration与decoration分开画,会导致透明遮挡问题
void Illustration_Casting(RenderWindow &window)
{
for (int i = 0; i < illustrationNum; i++)
{
illustration_Order[i] = i;
illustration_Distance[i] = ((posX - illustration_lst[i].x) * (posX - illustration_lst[i].x) + (posY - illustration_lst[i].y) * (posY - illustration_lst[i].y)); // sqrt not taken, unneeded
}
// 排序
// 先画远的再画近的不会导致透明遮挡问题
// 因为近的Sprite透明地方可以直接显示出远的Sprite
sortSprites(illustration_Order, illustration_Distance, illustrationNum);
// after sorting the sprites, do the projection and draw them
for (int i = 0; i < illustrationNum; i++)
{
// translate sprite position to relative to camera
double spriteX = illustration_lst[illustration_Order[i]].x - posX;
double spriteY = illustration_lst[illustration_Order[i]].y - posY;

// transform sprite with the inverse camera matrix
// [ planeX dirX ] -1 [ dirY -dirX ]
// [ ] = 1/(planeX*dirY-dirX*planeY) * [ ]
// [ planeY dirY ] [ -planeY planeX ]

double invDet = 1.0 / (planeX * dirY - dirX * planeY); // required for correct matrix multiplication

double transformX = invDet * (dirY * spriteX - dirX * spriteY);
double transformY = invDet * (-planeY * spriteX + planeX * spriteY); // this is actually the depth inside the screen, that what Z is in 3D

// sprite在屏幕上的x坐标
int spriteScreenX = int((screenWidth / 2) * (1 + transformX / transformY));

// calculate height of the sprite on screen
// 注意illustration这里需要给高度乘一个illustrationHeight / illustrationWidth
// 用于保持长宽比,不然不好看,北原春希都被压成正方形了
int spriteHeight = abs(int(screenHeight / (transformY))) * illustrationHeight / illustrationWidth; // using 'transformY' instead of the real distance prevents fisheye
// calculate width of the sprite
int spriteWidth = abs(int(screenHeight / (transformY)));

// calculate lowest and highest pixel to fill in current stripe
// 计算y上的起点,不需要计算终点,因为竖线直接指定了Sprite的高度
int drawStartY = -spriteHeight / 2 + screenHeight / 2;
// if (drawStartY < 0) drawStartY = 0;

// 计算x上的起点和终点
int drawStartX = -spriteWidth / 2 + spriteScreenX;
if (drawStartX < 0)
drawStartX = 0;
int drawEndX = spriteWidth / 2 + spriteScreenX;
if (drawEndX >= screenWidth)
drawEndX = screenWidth - 1;

// loop through every vertical stripe of the sprite on screen
for (int stripe = drawStartX; stripe < drawEndX; stripe++)
{
int texX = int(256 * (stripe - (-spriteWidth / 2 + spriteScreenX)) * illustrationWidth / spriteWidth) / 256;
// the conditions in the if are:
// 1) it's in front of camera plane so you don't see things behind you
// 2) it's on the screen (left)
// 3) it's on the screen (right)
// 4) ZBuffer, with perpendicular distance
if (transformY > 0 && stripe > 0 && stripe < screenWidth && transformY < ZBuffer[stripe])
{
// illustration一条线的Sprite
Sprite illustration_sprite;
// 设置对应纹理
illustration_sprite.setTexture(illustration_texture[illustration_lst[illustration_Order[i]].texture_index]);
// 设置sprite的纹理矩形,定义了在纹理中的哪个部分应用到illustration_sprite上
// 宽度为1个像素,相当于原来的画线
illustration_sprite.setTextureRect(sf::IntRect(texX, 0, 1, illustrationHeight));
// 设置sprite的位置,坐标指定了左上角的位置
illustration_sprite.setPosition(stripe, drawStartY);
// 设置sprite的缩放因子
illustration_sprite.setScale(1, spriteHeight / (float)illustrationHeight);
// 绘制sprite
window.draw(illustration_sprite);
}
}
}
}

不过需要注意一点:计算spriteHeight时,需要乘长宽比,不然立绘画出来就被压成正方形,如图:

1
2
3
4
// calculate height of the sprite on screen
// 注意illustration这里需要给高度乘一个illustrationHeight / illustrationWidth
// 用于保持长宽比,不然不好看,北原春希都被压成正方形了
int spriteHeight = abs(int(screenHeight / (transformY))) * illustrationHeight / illustrationWidth; // using 'transformY' instead of the real distance prevents fisheye

结果如下(你说得对,但是我就喜欢看雪菜哭哭)

两种Sprite一起绘制出现遮挡问题

问题阐述

此时main函数的调用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int main()
{
...
// 主循环
while (window.isOpen())
{
...
window.clear();
Floor_Ceiling_Casting(window);
Wall_Casting(window);
// 此处先画立绘
Illustration_Casting(window);
// 再画decoration
Decoration_Casting(window);

Move(window);
...
}
return 0;
}

如图所示,decoration的绘制在illustration之后,因此出现图层覆盖的情况

我尝试在画Sprite的同时更新ZBuffer,希望能够解决问题,但引入了一个新问题,先看代码

1
2
3
4
5
6
7
8
9
10
// loop through every vertical stripe of the sprite on screen
for (int stripe = drawStartX; stripe < drawEndX; stripe++)
{
...
// 绘制sprite
window.draw(illustration_sprite);

//此处更新ZBUFFER
ZBuffer[stripe] = transformY;
}

引入的新问题就是:透明部分更新的ZBuffer阻止了本该正常画的新Sprite,如下图中,近处的雪菜透明部分挡住了远处本应该正常显示的灯的Sprite

如果这样看不太明显,我换一下调用顺序就能很清楚了,即先画decoration再画illustration:

这时就很明显了,近处灯纹理的透明部分更新了ZBuffer,导致应该正常绘制远处的雪菜在左半边绘制时被if判断跳过了:

1
if (... && transformY < ZBuffer[stripe])

灯的整个纹理都算在近处,所以透明部分更新ZBuffer的值,远处雪菜立绘的transformY确实是比ZBuffer

那么怎么解决呢?

我第一时间想的是修改ZBuffer,让它记录每一个像素的深度值,如果是透明的就不修改

如果这样修改,画两种Sprite的时候就需要对y值进行判断,对应的drawStartY也需要改变,太麻烦了

那么有没有一种简单的方法解决这个问题呢?

解决方案

通过观察,发现单独绘制decoration和illustration时都没出现这种问题

如下图所示,冬马和纱这张立绘的包围盒应该是红框,但是和泉千晶的立绘能够正常出现,没有被透明部分遮挡

因此得出结论:需要把decoration和illustration同时画,即一起排序,一起执行一遍上面的逻辑

于是,就需要新建一个类,除了需要存储x, y, texture_index以外,还需要存储这个变量是decoration还是illustration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/************************************************************/
/* 为了防止decoration和illustration互相的透明遮挡问题
/* 采用两种Sprite一起画的方法,反正这两种单独画的逻辑也是完全一样
/* 为此,需要声明MySprite类,和之前两种Sprite成员变量一样
/* 不需要声明新的纹理数组,可以直接复用decoration和illustration的纹理数组
/* 声明Order和Distance数组,并且初始化数目为Sprite总数
/* 已知Sprite总数为 decorationNum + illustrationNum
/* 需要一个能存下全部Sprite的数组Allsprites_lst,记录位置和下标
/* 还需要对应的排序函数sortAllSprites
/************************************************************/
/************************************************************/
/* MySprite,用于创建数组
/************************************************************/
class MySprite
{
public:
double x;
double y;
int texture_index;
int type; // 用于指明是decoration还是illustration
// 0为decoration,1为illustration
class MySprite(double x = 0, double y = 0, int texindex = 0, int type = -1)
{
this->x = x;
this->y = y;
this->texture_index = texindex;
this->type = type;
}
};


然后还需要跟上文逻辑一样的一系列数组,存储具体位置和纹理下标、Order数组,Distance数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// 全部Sprite的数组,记录位置和下标
MySprite Allsprites_lst[decorationNum + illustrationNum] =
{
/************************************************************/
/* Decoration
/************************************************************/
// green light in front of playerstart
{20.5, 11.5, 0, 0},
// green lights in every room
{18.5, 4.5, 0, 0},
{10.0, 4.5, 0, 0},
{10.0, 12.5, 0, 0},
{3.5, 6.5, 0, 0},
{3.5, 20.5, 0, 0},
{3.5, 14.5, 0, 0},
{14.5, 20.5, 0, 0},

// row of pillars in front of wall: fisheye test
{18.5, 10.5, 1, 0},
{18.5, 11.5, 1, 0},
{18.5, 12.5, 1, 0},

// some barrels around the map
{21.5, 1.5, 2, 0},
{15.5, 1.5, 2, 0},
{16.0, 1.8, 2, 0},
{16.2, 1.2, 2, 0},
{3.5, 2.5, 2, 0},
{9.5, 15.5, 2, 0},
{10.0, 15.1, 2, 0},
{10.5, 15.8, 2, 0},

/************************************************************/
/* illustration
/************************************************************/
// 北原春希立绘,刚进游戏就能看见
{20.6, 11.5, 0, 1},
// 小木曾雪菜流汗黄豆立绘
{18, 3.25, 1, 1},
// 小木曾雪菜怒立绘
{8.0, 4.5, 2, 1},
// 小木曾雪菜哭1
{21,8,3,1},
// 小木曾雪菜哭2
{22.5,3.5,4,1},
// 冬马和纱斗鸡眼
{17.1,14.7,5,1},
// 冬马和纱怒
{22.5,18.5,6,1},
// 冬马和纱闭眼
{16.8,21.5,7,1},
// 冬马和纱笑
{3,12,8,1},
// 小春
{10.1,18.3,9,1},
// 千晶
{2.5,21.2,10,1},
// 麻理
{6.4,19.2,11,1}
};
// Order数组,并且初始化数目为decorationNum + illustrationNum
vector<int> Allsprites_Order(decorationNum + illustrationNum);
// Distance数组,并且初始化数目为decorationNum + illustrationNum
vector<double> Allsprites_Distance(decorationNum + illustrationNum);

最后需要一个逻辑完全一样的排序函数:

1
2
3
// 全部Sprite的排序函数,用于画出全部Sprite
// 注意传递vector的引用
void sortAllSprites(vector<int> &Allsprites_Order, vector<double> &Allsprites_Distance, int amount);

值得注意的是,排序函数传递参数要传递vector引用(多弱质的错误,就因为太久没用了,难绷)

排序函数具体如下,逻辑完全没变:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void sortAllSprites(vector<int> &Allsprites_Order, vector<double> &Allsprites_Distance, int amount)
{
std::vector<std::pair<double, int>> sprites(amount);
for (int i = 0; i < amount; i++)
{
sprites[i].first = Allsprites_Distance[i];
sprites[i].second = Allsprites_Order[i];
}
std::sort(sprites.begin(), sprites.end());
// restore in reverse order to go from farthest to nearest
for (int i = 0; i < amount; i++)
{
Allsprites_Distance[i] = sprites[amount - i - 1].first;
Allsprites_Order[i] = sprites[amount - i - 1].second;
}
}

最后就是绘制函数,只需要注意判断类型是decoration还是illustration,两者spriteHeight计算方法不同,绘制方法也不太相同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
// 成功解决遮挡问题,原因:sortAllSprites排序没传递vector的引用,导致没有正常排序
void Allsprites_Casting(RenderWindow &window)
{
for (int i = 0; i < decorationNum + illustrationNum; i++)
{
Allsprites_Order[i] = i;
Allsprites_Distance[i] = ((posX - Allsprites_lst[i].x) * (posX - Allsprites_lst[i].x) + (posY - Allsprites_lst[i].y) * (posY - Allsprites_lst[i].y)); // sqrt not taken, unneeded
}
// 排序
// 先画远的再画近的不会导致透明遮挡问题
// 因为近的Sprite透明地方可以直接显示出远的Sprite
sortAllSprites(Allsprites_Order, Allsprites_Distance, decorationNum + illustrationNum);
// after sorting the sprites, do the projection and draw them
for (int i = 0; i < decorationNum + illustrationNum; i++)
{
// translate sprite position to relative to camera
double spriteX = Allsprites_lst[Allsprites_Order[i]].x - posX;
double spriteY = Allsprites_lst[Allsprites_Order[i]].y - posY;

// transform sprite with the inverse camera matrix
// [ planeX dirX ] -1 [ dirY -dirX ]
// [ ] = 1/(planeX*dirY-dirX*planeY) * [ ]
// [ planeY dirY ] [ -planeY planeX ]

double invDet = 1.0 / (planeX * dirY - dirX * planeY); // required for correct matrix multiplication

double transformX = invDet * (dirY * spriteX - dirX * spriteY);
double transformY = invDet * (-planeY * spriteX + planeX * spriteY); // this is actually the depth inside the screen, that what Z is in 3D

// sprite在屏幕上的x坐标
int spriteScreenX = int((screenWidth / 2) * (1 + transformX / transformY));

// calculate height of the sprite on screen
int spriteHeight = 0;
// decoration的height不用乘比例
if (Allsprites_lst[Allsprites_Order[i]].type == 0)
{
spriteHeight = abs(int(screenHeight / (transformY))); // using 'transformY' instead of the real distance prevents fisheye
}
// illustration的height需要乘比例
// 用于保持长宽比,不然立绘被压成正方形
else if (Allsprites_lst[Allsprites_Order[i]].type == 1)
{
spriteHeight = abs(int(screenHeight / (transformY))) * illustrationHeight / illustrationWidth; // using 'transformY' instead of the real distance prevents fisheye
}

// calculate width of the sprite
int spriteWidth = abs(int(screenHeight / (transformY)));

// calculate lowest and highest pixel to fill in current stripe
// 计算y上的起点,不需要计算终点,因为竖线直接指定了Sprite的高度
int drawStartY = -spriteHeight / 2 + screenHeight / 2;
// if (drawStartY < 0) drawStartY = 0;

// 计算x上的起点和终点
int drawStartX = -spriteWidth / 2 + spriteScreenX;
if (drawStartX < 0)
drawStartX = 0;
int drawEndX = spriteWidth / 2 + spriteScreenX;
if (drawEndX >= screenWidth)
drawEndX = screenWidth - 1;

// loop through every vertical stripe of the sprite on screen
for (int stripe = drawStartX; stripe < drawEndX; stripe++)
{
// the conditions in the if are:
// 1) it's in front of camera plane so you don't see things behind you
// 2) it's on the screen (left)
// 3) it's on the screen (right)
// 4) ZBuffer, with perpendicular distance
if (transformY > 0 && stripe > 0 && stripe < screenWidth && transformY < ZBuffer[stripe])
{
// 画decoration
if (Allsprites_lst[Allsprites_Order[i]].type == 0)
{
// 计算texX
int texX = int(256 * (stripe - (-spriteWidth / 2 + spriteScreenX)) * texWidth / spriteWidth) / 256;
// decoration一条线的Sprite
Sprite decoration_sprite;
// 设置对应纹理
decoration_sprite.setTexture(decoration_texture[Allsprites_lst[Allsprites_Order[i]].texture_index]);
// 设置sprite的纹理矩形,定义了在纹理中的哪个部分应用到decoration_sprite上
// 宽度为1个像素,相当于原来的画线
decoration_sprite.setTextureRect(sf::IntRect(texX, 0, 1, texHeight));
// 设置sprite的位置,坐标指定了左上角的位置
decoration_sprite.setPosition(stripe, drawStartY);
// 设置sprite的缩放因子
decoration_sprite.setScale(1, spriteHeight / (float)texHeight);
// 绘制sprite
window.draw(decoration_sprite);
}
// 画illustration
else if (Allsprites_lst[Allsprites_Order[i]].type == 1)
{
// 计算texX
int texX = int(256 * (stripe - (-spriteWidth / 2 + spriteScreenX)) * illustrationWidth / spriteWidth) / 256;
// illustration一条线的Sprite
Sprite illustration_sprite;
// 设置对应纹理
illustration_sprite.setTexture(illustration_texture[Allsprites_lst[Allsprites_Order[i]].texture_index]);
// 设置sprite的纹理矩形,定义了在纹理中的哪个部分应用到illustration_sprite上
// 宽度为1个像素,相当于原来的画线
illustration_sprite.setTextureRect(sf::IntRect(texX, 0, 1, illustrationHeight));
// 设置sprite的位置,坐标指定了左上角的位置
illustration_sprite.setPosition(stripe, drawStartY);
// 设置sprite的缩放因子
illustration_sprite.setScale(1, spriteHeight / (float)illustrationHeight);
// 绘制sprite
window.draw(illustration_sprite);
}
}
}
}
}

至此,完美解决透明遮挡问题,再把之前做的小地图功能拿出来,就是下图结果

最终源代码

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
#include <SFML/Graphics.hpp>
#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>

using namespace std;
using namespace sf;

#define screenWidth 1440
#define screenHeight 900
#define mapWidth 24
#define mapHeight 24
// 地板天花板、墙壁、decoration纹理宽高
// 纹理图片都来自《德军总部3D》,因此宽高一样
#define texWidth 64
#define texHeight 64
// 地板天花板墙壁纹理总数
#define textureNum_Wall 8
// decoration纹理总数
#define textureNum_Decoration 3
// illustration纹理总数
#define textureNum_Illustration 12
// decoration总数
#define decorationNum 19
// illustration总数
#define illustrationNum 12
// 人物立绘宽高
// 由于人物立绘各个尺寸不一样,因此需要把所有图片缩放到370x690
// 缩放已经通过python实现,此处代码不考虑
#define illustrationWidth 370
#define illustrationHeight 690

int worldMap[mapWidth][mapHeight] =
{
{4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 7, 7, 7, 7, 7, 7, 7, 7},
{4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 7},
{4, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7},
{4, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7},
{4, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 7},
{4, 0, 4, 0, 0, 0, 0, 5, 5, 5, 5, 5, 5, 5, 5, 5, 7, 7, 0, 7, 7, 7, 7, 7},
{4, 0, 5, 0, 0, 0, 0, 5, 0, 5, 0, 5, 0, 5, 0, 5, 7, 0, 0, 0, 7, 7, 7, 1},
{4, 0, 6, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 5, 7, 0, 0, 0, 0, 0, 0, 8},
{4, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 7, 7, 1},
{4, 0, 8, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 5, 7, 0, 0, 0, 0, 0, 0, 8},
{4, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 5, 7, 0, 0, 0, 7, 7, 7, 1},
{4, 0, 0, 0, 0, 0, 0, 5, 5, 5, 5, 0, 5, 5, 5, 5, 7, 7, 7, 7, 7, 7, 7, 1},
{6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6},
{8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4},
{6, 6, 6, 6, 6, 6, 0, 6, 6, 6, 6, 0, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6},
{4, 4, 4, 4, 4, 4, 0, 4, 4, 4, 6, 0, 6, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3},
{4, 0, 0, 0, 0, 0, 0, 0, 0, 4, 6, 0, 6, 2, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2},
{4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 2, 0, 0, 5, 0, 0, 2, 0, 0, 0, 2},
{4, 0, 0, 0, 0, 0, 1, 0, 0, 4, 6, 0, 6, 2, 0, 0, 0, 0, 0, 2, 2, 0, 2, 2},
{4, 0, 6, 0, 6, 0, 0, 0, 0, 4, 6, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 2},
{4, 0, 0, 5, 0, 0, 0, 0, 0, 4, 6, 0, 6, 2, 0, 0, 0, 0, 0, 2, 2, 0, 2, 2},
{4, 0, 6, 0, 6, 0, 0, 0, 0, 4, 6, 0, 6, 2, 0, 0, 5, 0, 0, 2, 0, 0, 0, 2},
{4, 0, 0, 0, 0, 0, 0, 0, 0, 4, 6, 0, 6, 2, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2},
{4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3}};

double posX = 22.5, posY = 11.5; // x and y start position
double dirX = -1, dirY = 0; // initial direction vector
double planeX = 0, planeY = 0.66; // the 2d raycaster version of camera plane
double FOV; // FOV视角大小,采用弧度制

double new_Time = 0; // time of current frame
double old_Time = 0; // time of previous frame

Texture texture[textureNum_Wall]; // 墙面、地板天花板纹理数组
// decoration和illustration纹理数组单独开
Clock Myclock; // 用于计时,计算这一帧和上一帧的时间

Image Ceiling_image; // 天花板纹理图像,用于(tx, ty)获取颜色
Image Floor_image; // 地板纹理图像,用于(tx, ty)获取颜色

// debug用,每次手动注释掉鼠标的逻辑烦死我了
bool ifMouse = 1;

// ZBuffer数组,记录屏幕上每个x对应的perpWallDist
double ZBuffer[screenWidth] = {0};

/************************************************************/
/* Decoration
/************************************************************/
// decoration纹理数组
Texture decoration_texture[textureNum_Decoration];
class Decoration
{
public:
double x;
double y;
int texture_index;
int type; // 用于指明是decoration还是illustration
// 0为decoration,1为illustration
Decoration(double x = 0, double y = 0, int texindex = 0)
{
this->x = x;
this->y = y;
this->texture_index = texindex;
this->type = 0;
}
};
// decoration数组,用于记录全部decoration的位置和对应纹理下标
Decoration decoration_lst[decorationNum] =
{
{20.5, 11.5, 0}, // green light in front of playerstart
// green lights in every room
{18.5, 4.5, 0},
{10.0, 4.5, 0},
{10.0, 12.5, 0},
{3.5, 6.5, 0},
{3.5, 20.5, 0},
{3.5, 14.5, 0},
{14.5, 20.5, 0},

// row of pillars in front of wall: fisheye test
{18.5, 10.5, 1},
{18.5, 11.5, 1},
{18.5, 12.5, 1},

// some barrels around the map
{21.5, 1.5, 2},
{15.5, 1.5, 2},
{16.0, 1.8, 2},
{16.2, 1.2, 2},
{3.5, 2.5, 2},
{9.5, 15.5, 2},
{10.0, 15.1, 2},
{10.5, 15.8, 2},
};
// arrays used to sort the sprites
// Order数组和Distance数组,分别存储下标和离玩家的距离
int decoration_Order[decorationNum];
double decoration_Distance[decorationNum];

/************************************************************/
/* illustration
/************************************************************/
// illustration纹理数组
Texture illustration_texture[textureNum_Illustration];
class Illustration
{
public:
double x;
double y;
int texture_index;
int type; // 用于指明是decoration还是illustration
// 0为decoration,1为illustration
Illustration(double x = 0, double y = 0, int texindex = 0)
{
this->x = x;
this->y = y;
this->texture_index = texindex;
this->type = 1;
}
};
// illustration数组,用于记录全部illustration的位置和对应纹理下标
Illustration illustration_lst[illustrationNum] =
{
// 北原春希立绘,刚进游戏就能看见
{20.6, 11.5, 0},
// 小木曾雪菜流汗黄豆立绘
{18, 3.25, 1},
// 小木曾雪菜怒立绘
{8.0, 4.5, 2},
// 小木曾雪菜哭1
{21, 8, 3},
// 小木曾雪菜哭2
{22.5, 3.5, 4},
// 冬马和纱斗鸡眼
{17.1, 14.7, 5},
// 冬马和纱怒
{22.5, 18.5, 6},
// 冬马和纱闭眼
{16.8, 21.5, 7},
// 冬马和纱笑
{3, 12, 8},
// 杉浦小春
{10.1, 18.3, 9},
// 和泉千晶
{2.5, 21.2, 10},
// 风冈麻理
{6.4, 19.2, 11}};
// arrays used to sort the sprites
// Order数组和Distance数组,分别存储下标和离玩家的距离
int illustration_Order[illustrationNum];
double illustration_Distance[illustrationNum];

// function used to sort the sprites
// 此函数用于单独画decoration或单独画illustration
// 如果一起画,则用sortAllSprites函数
void sortSprites(int *order, double *dist, int amount);

/************************************************************/
/* 为了防止decoration和illustration互相的透明遮挡问题
/* 采用两种Sprite一起画的方法,反正这两种单独画的逻辑也是完全一样
/* 为此,需要声明MySprite类,和之前两种Sprite成员变量一样
/* 不需要声明新的纹理数组,可以直接复用decoration和illustration的纹理数组
/* 声明Order和Distance数组,并且初始化数目为Sprite总数
/* 已知Sprite总数为 decorationNum + illustrationNum
/* 需要一个能存下全部Sprite的数组Allsprites_lst,记录位置和下标
/* 还需要对应的排序函数sortAllSprites
/************************************************************/
/************************************************************/
/* MySprite,用于创建数组
/************************************************************/
class MySprite
{
public:
double x;
double y;
int texture_index;
int type; // 用于指明是decoration还是illustration
// 0为decoration,1为illustration
class MySprite(double x = 0, double y = 0, int texindex = 0, int type = -1)
{
this->x = x;
this->y = y;
this->texture_index = texindex;
this->type = type;
}
};
// 全部Sprite的数组,记录位置和下标
MySprite Allsprites_lst[decorationNum + illustrationNum] =
{
/************************************************************/
/* Decoration
/************************************************************/
// green light in front of playerstart
{20.5, 11.5, 0, 0},
// green lights in every room
{18.5, 4.5, 0, 0},
{10.0, 4.5, 0, 0},
{10.0, 12.5, 0, 0},
{3.5, 6.5, 0, 0},
{3.5, 20.5, 0, 0},
{3.5, 14.5, 0, 0},
{14.5, 20.5, 0, 0},

// row of pillars in front of wall: fisheye test
{18.5, 10.5, 1, 0},
{18.5, 11.5, 1, 0},
{18.5, 12.5, 1, 0},

// some barrels around the map
{21.5, 1.5, 2, 0},
{15.5, 1.5, 2, 0},
{16.0, 1.8, 2, 0},
{16.2, 1.2, 2, 0},
{3.5, 2.5, 2, 0},
{9.5, 15.5, 2, 0},
{10.0, 15.1, 2, 0},
{10.5, 15.8, 2, 0},

/************************************************************/
/* illustration
/************************************************************/
// 北原春希立绘,刚进游戏就能看见
{20.6, 11.5, 0, 1},
// 小木曾雪菜流汗黄豆立绘
{18, 3.25, 1, 1},
// 小木曾雪菜怒立绘
{8.0, 4.5, 2, 1},
// 小木曾雪菜哭1
{21, 8, 3, 1},
// 小木曾雪菜哭2
{22.5, 3.5, 4, 1},
// 冬马和纱斗鸡眼
{17.1, 14.7, 5, 1},
// 冬马和纱怒
{22.5, 18.5, 6, 1},
// 冬马和纱闭眼
{16.8, 21.5, 7, 1},
// 冬马和纱笑
{3, 12, 8, 1},
// 小春
{10.1, 18.3, 9, 1},
// 千晶
{2.5, 21.2, 10, 1},
// 麻理
{6.4, 19.2, 11, 1}

};
// Order数组,并且初始化数目为decorationNum + illustrationNum
vector<int> Allsprites_Order(decorationNum + illustrationNum);
// Distance数组,并且初始化数目为decorationNum + illustrationNum
vector<double> Allsprites_Distance(decorationNum + illustrationNum);
// 全部Sprite的排序函数,用于画出全部Sprite
// 注意传递vector的引用
void sortAllSprites(vector<int> &Allsprites_Order, vector<double> &Allsprites_Distance, int amount);

// 获取时间
unsigned long getTicks();
// 读取纹理,分别读取墙面地板天花板纹理、decoration纹理、illustration纹理
// 并且在其中调用修改alpha的函数,防止出现纯黑遮挡
void LoadTexture();
// 修改纹理透明度,用于decoration和illustration纹理纯黑颜色修改为透明 (Alpha通道改为0)
void SetTexture_Alpha(Texture *texture, int start_index, int end_index);
// 纹理墙壁绘制
void Wall_Casting(RenderWindow &window);
// 纹理地板天花板绘制
void Floor_Ceiling_Casting(RenderWindow &window);
// 移动函数,用于改变位置和朝向
// 已添加鼠标操作逻辑
void Move(RenderWindow &window);
// 计算FOV大小,弧度制
double calculate_FOV();
// 画出Decoration Sprite
void Decoration_Casting(RenderWindow &window);
// 画出illustration Sprite
void Illustration_Casting(RenderWindow &window);
// 画出全部Sprite
void Allsprites_Casting(RenderWindow &window);

/************************************************************/
/* 绘制小地图
/************************************************************/
// 小地图的宽度和高度
const int minimapWidth = 24;
const int minimapHeight = 24;
// 小地图的大小(单位:像素)
const int miniMapScale = 15;
bool ifdraw_Minimap = 1;
// 创建一个表示视野范围的三角扇形
VertexArray FOV_Visualize(TriangleFan, screenWidth + 1);
// 记录视野范围每一处的位置
// 在墙壁绘制函数里修改值,在drawMiniMap函数里设置FOV位置
vector<Vector2f> FOV_position(screenWidth + 1);
// 绘制小地图函数
void drawMiniMap(RenderWindow &window);

int main()
{
RenderWindow window(sf::VideoMode(screenWidth, screenHeight), "Raycasting-Final");

// 加载纹理
LoadTexture();

// 是否允许鼠标逻辑控制
if (ifMouse)
{
Mouse::setPosition(Vector2i(screenWidth * 0.5f, screenHeight * 0.5f), window);
window.setMouseCursorVisible(0);
}

// 计算FOV视角
FOV = calculate_FOV();

while (window.isOpen())
{
sf::Event event;
while (window.pollEvent(event))
{
// 考虑到鼠标操作,使用ESC退出
if (event.type == sf::Event::Closed || sf::Keyboard::isKeyPressed(sf::Keyboard::Escape))
window.close();
// sf::Keyboard::isKeyPressed函数会在每一帧都检测键盘的状态
// sf::Event::KeyPressed事件只会在按键被按下的那一帧触发一次,即使你按住按键不放,它也不会在下一帧再次触发
// 因此,使用sf::Event::KeyPressed事件可以确保ifdraw_Minimap的值只会被翻转一次
if (event.type == sf::Event::KeyPressed)
{
// 按Tab键、~键或M键开关小地图
if (event.key.code == sf::Keyboard::Tab || event.key.code == sf::Keyboard::Tilde || event.key.code == sf::Keyboard::M)
{
ifdraw_Minimap = !ifdraw_Minimap;
}
}
}

window.clear();
Floor_Ceiling_Casting(window);
Wall_Casting(window);
// Decoration_Casting(window);
// Illustration_Casting(window);

Allsprites_Casting(window);

Move(window);

if (ifdraw_Minimap)
{
drawMiniMap(window);
}

// cout << "posX: " << posX << " posY: " << posY << endl;

window.display();
}

return 0;
}

unsigned long getTicks()
{
sf::Time elapsed = Myclock.getElapsedTime();
return elapsed.asMilliseconds();
}

void LoadTexture()
{
// 读取墙壁、地板天花板纹理
texture[0].loadFromFile("pics/eagle.png");
texture[1].loadFromFile("pics/redbrick.png");
texture[2].loadFromFile("pics/purplestone.png");
texture[3].loadFromFile("pics/greystone.png");
texture[4].loadFromFile("pics/bluestone.png");
texture[5].loadFromFile("pics/mossy.png");
texture[6].loadFromFile("pics/wood.png");
texture[7].loadFromFile("pics/colorstone.png");
// 地板天花板渲染用的image
Ceiling_image = texture[6].copyToImage(); // 天花板为木头
Floor_image = texture[3].copyToImage(); // 地板为石头
// 读取decoration纹理
decoration_texture[0].loadFromFile("pics/greenlight.png");
decoration_texture[1].loadFromFile("pics/pillar.png");
decoration_texture[2].loadFromFile("pics/barrel.png");
// 纯黑部分改透明
SetTexture_Alpha(decoration_texture, 0, 2);
// 读取illustration纹理
illustration_texture[0].loadFromFile("illustration/haruki.png");
illustration_texture[1].loadFromFile("illustration/setsuna0.png");
illustration_texture[2].loadFromFile("illustration/setsuna1.png");
illustration_texture[3].loadFromFile("illustration/setsuna2.png");
illustration_texture[4].loadFromFile("illustration/setsuna3.png");
illustration_texture[5].loadFromFile("illustration/kazusa0.png");
illustration_texture[6].loadFromFile("illustration/kazusa1.png");
illustration_texture[7].loadFromFile("illustration/kazusa2.png");
illustration_texture[8].loadFromFile("illustration/kazusa3.png");
illustration_texture[9].loadFromFile("illustration/koharu.png");
illustration_texture[10].loadFromFile("illustration/chiaki.png");
illustration_texture[11].loadFromFile("illustration/mari.png");
// 纯黑部分改透明
SetTexture_Alpha(illustration_texture, 0, 11);
}

void SetTexture_Alpha(Texture *texture, int start_index, int end_index)
{
for (int i = start_index; i <= end_index; i++)
{
Image tmp = texture[i].copyToImage();
for (int y = 0; y < tmp.getSize().y; y++)
{
for (int x = 0; x < tmp.getSize().x; x++)
{
if (tmp.getPixel(x, y) == Color::Black)
{
// 设置为完全透明
tmp.setPixel(x, y, sf::Color(0, 0, 0, 0));
}
}
}
texture[i].loadFromImage(tmp); // 用修改后的图像覆盖原数组的纹理
}
}

void Floor_Ceiling_Casting(RenderWindow &window)
{
// 创建一个新的Image
Image Floor_Ceiling_image;
Floor_Ceiling_image.create(screenWidth, screenHeight);

// rayDir for leftmost ray (x = 0) and rightmost ray (x = w)
float FOV_StartX = dirX - planeX;
float FOV_StartY = dirY - planeY;
float FOV_EndX = dirX + planeX;
float FOV_EndY = dirY + planeY;

// Vertical position of the camera.
float posZ = 0.5 * screenHeight;

// FLOOR AND CEILING CASTING
for (int y = screenHeight / 2; y < screenHeight; y++) // 其实可以直接从screenHeight / 2开始遍历
// for (int y = 0; y < screenHeight; y++)
{
// Current y position compared to the center of the screen (the horizon)
int row_y = y - screenHeight / 2;

// Horizontal distance from the camera to the floor for the current row.
// 0.5 is the z position exactly in the middle between floor and ceiling.
float rowDistance = posZ / row_y;

// real world coordinates of the leftmost column. This will be updated as we step to the right.
float floorX = posX + rowDistance * FOV_StartX;
float floorY = posY + rowDistance * FOV_StartY;

// calculate the real world step vector we have to add for each x (parallel to camera plane)
// adding step by step avoids multiplications with a weight in the inner loop
float floorStepX = rowDistance * (FOV_EndX - FOV_StartX) / screenWidth;
float floorStepY = rowDistance * (FOV_EndY - FOV_StartY) / screenWidth;

for (int x = 0; x < screenWidth; ++x)
{
// the cell coord is simply got from the integer parts of floorX and floorY
int cellX = (int)(floorX);
int cellY = (int)(floorY);

// get the texture coordinate from the fractional part
int tx = (int)(texWidth * (floorX - cellX)) & (texWidth - 1);
int ty = (int)(texHeight * (floorY - cellY)) & (texHeight - 1);

floorX += floorStepX;
floorY += floorStepY;

// 计算像素颜色,逐像素赋值
Color Floorcolor = Floor_image.getPixel(tx, ty);
Color Ceilingcolor = Ceiling_image.getPixel(tx, ty);
Floor_Ceiling_image.setPixel(x, y, Floorcolor);
Floor_Ceiling_image.setPixel(x, screenHeight - y - 1, Ceilingcolor);
}
}
// 创建一个新的Texture
Texture Floor_Ceiling_texture;
Floor_Ceiling_texture.loadFromImage(Floor_Ceiling_image);
// 创建一个新的Sprite并设置其纹理
Sprite sprite;
sprite.setTexture(Floor_Ceiling_texture);
// 把这个Sprite画在屏幕上
window.draw(sprite);
}

void Wall_Casting(RenderWindow &window)
{
for (int x = 0; x < screenWidth; x++)
{
// calculate ray position and direction
double cameraX = 2 * x / (double)screenWidth - 1; // x-coordinate in camera space
double rayDirX = dirX + planeX * cameraX;
double rayDirY = dirY + planeY * cameraX;
// which box of the map we're in
int mapX = int(posX);
int mapY = int(posY);

// length of ray from current position to next x or y-side
double sideDistX;
double sideDistY;

/************************************************************/
/* 避免鱼眼效应相关代码
/************************************************************/
// 此段代码会避免鱼眼效应,而不是修正鱼眼效应
double deltaDistX = (rayDirX == 0) ? 1e30 : std::abs(1 / rayDirX);
double deltaDistY = (rayDirY == 0) ? 1e30 : std::abs(1 / rayDirY);

//// 此段代码会造成鱼眼效应
// double rayDir_len = sqrt(rayDirX * rayDirX + rayDirY * rayDirY);
// double deltaDistX = (rayDirX == 0) ? 1e30 : std::abs(rayDir_len / rayDirX);
// double deltaDistY = (rayDirY == 0) ? 1e30 : std::abs(rayDir_len / rayDirY);

double perpWallDist;

// what direction to step in x or y-direction (either +1 or -1)
int stepX;
int stepY;

int hit = 0; // was there a wall hit?
int side; // was a NS or a EW wall hit?
// calculate step and initial sideDist
if (rayDirX < 0)
{
stepX = -1;
sideDistX = (posX - mapX) * deltaDistX;
}
else
{
stepX = 1;
sideDistX = (mapX + 1.0 - posX) * deltaDistX;
}
if (rayDirY < 0)
{
stepY = -1;
sideDistY = (posY - mapY) * deltaDistY;
}
else
{
stepY = 1;
sideDistY = (mapY + 1.0 - posY) * deltaDistY;
}
// perform DDA
while (hit == 0)
{
// jump to next map square, either in x-direction, or in y-direction
if (sideDistX < sideDistY)
{
sideDistX += deltaDistX;
mapX += stepX;
side = 0;
}
else
{
sideDistY += deltaDistY;
mapY += stepY;
side = 1;
}
// Check if ray has hit a wall
if (worldMap[mapX][mapY] > 0)
hit = 1;
}
// 教程里这部分解释的比较清楚了
// 也可以看我在博客里的证明过程,写的更清楚
if (side == 0)
perpWallDist = (sideDistX - deltaDistX);
else
perpWallDist = (sideDistY - deltaDistY);

/*********************************************************************************/
/* 此部分为添加FOV_Visualize用
/*********************************************************************************/
// 计算射线终点
double rayEndX = posX + perpWallDist * rayDirX;
double rayEndY = posY + perpWallDist * rayDirY;

// 记录三角扇形顶点位置
FOV_position[x + 1] = Vector2f(rayEndX * miniMapScale, (mapHeight - rayEndY) * miniMapScale);

/*********************************************************************************/
/* 添加ZBuffer
/*********************************************************************************/
ZBuffer[x] = perpWallDist;

// Calculate height of line to draw on screen
// 可以任意修改墙的高度
int lineHeight = (int)(screenHeight / perpWallDist);

// calculate lowest and highest pixel to fill in current stripe
int drawStart = -lineHeight / 2 + screenHeight / 2;
if (drawStart < 0)
drawStart = 0;
int drawEnd = lineHeight / 2 + screenHeight / 2;
if (drawEnd >= screenHeight)
drawEnd = screenHeight - 1;

/*********************************************************************************/
/* 此部分替换了原raycasting画线部分
/* 以下为纹理相关计算,是新内容
/* 具体和原来的画线函数差不多,都是在具体位置上画出一条线
/* 只不过这里画的是具体纹理上的一条线,利用了SFML中的sprite
/*********************************************************************************/

// texturing calculations
int texNum = worldMap[mapX][mapY] - 1; // 1 subtracted from it so that texture 0 can be used!

// calculate value of wallX
double wallX; // where exactly the wall was hit
if (side == 0)
wallX = posY + perpWallDist * rayDirY;
else
wallX = posX + perpWallDist * rayDirX;
wallX -= floor((wallX));

// x coordinate on the texture
int texX = int(wallX * double(texWidth));
// 防止镜像
if (side == 0 && rayDirX > 0)
texX = texWidth - texX - 1;
if (side == 1 && rayDirY < 0)
texX = texWidth - texX - 1;

// 为了获取纹理具体某一个坐标的像素颜色
// 创建一个新的sprite对象
sf::Sprite wall_sprite;

// 设置sprite的纹理
wall_sprite.setTexture(texture[texNum]);

// 设置sprite的纹理矩形,定义了在纹理中的哪个部分应用到wall_sprite上
// 宽度为1个像素,相当于原来的画线
wall_sprite.setTextureRect(sf::IntRect(texX, 0, 1, texHeight));

// 设置sprite的位置,坐标指定了左上角的位置
// wall_sprite.setPosition(x, drawStart); //太几把恶心了,破案了:指定左上角的位置为drawStart
// //drawStart在特殊情况下会被赋值为0,即if (drawStart < 0) drawStart = 0;
// //在此情况下绘制的起点就会改变为0,令人恶心
wall_sprite.setPosition(x, round(0.5f * screenHeight - 0.5f * lineHeight));

// 设置sprite的缩放因子
wall_sprite.setScale(1, lineHeight / (float)texHeight);

// 利用SFML中sprite的setColor功能,使y墙变暗
// 这个函数实现的是“调制混合”
// 举个例子:原来的R通道为200,与128混合为:200 * 128 / 255 = 100,实现了颜色除以2的效果
if (side == 1)
{
wall_sprite.setColor(Color(128, 128, 128));
}
// 绘制sprite
window.draw(wall_sprite);
}
}

// 移动函数,用于改变位置和朝向
// 已经实现鼠标左右转
void Move(RenderWindow &window)
{
// timing for input and FPS counter
old_Time = new_Time;
new_Time = getTicks();
double frameTime = (new_Time - old_Time) / 1000.0; // frameTime is the time this frame has taken, in seconds

// speed modifiers
double moveSpeed = frameTime * 3.0; // the constant value is in squares/second
// 这个旋转速度计算的是方向键左右逻辑,类似于坦克视角移动
double rotSpeed = frameTime * 2.0; // the constant value is in radians/second

// 这些是计算鼠标移动逻辑
// 获取窗口的中心位置
Vector2i center(screenWidth * 0.5f, screenHeight * 0.5f);

// 获取鼠标当前位置
Vector2i position_now = Mouse::getPosition(window);

// 计算鼠标的旋转角度
// 水平方向旋转角度
double rotation_degree_horizontal = (center.x - position_now.x) * FOV / screenWidth;

if (ifMouse)
{
// 更新方向
// dir和plane一起更新
double oldDirX = dirX;
dirX = dirX * cos(rotation_degree_horizontal) - dirY * sin(rotation_degree_horizontal);
dirY = oldDirX * sin(rotation_degree_horizontal) + dirY * cos(rotation_degree_horizontal);
double oldPlaneX = planeX;
planeX = planeX * cos(rotation_degree_horizontal) - planeY * sin(rotation_degree_horizontal);
planeY = oldPlaneX * sin(rotation_degree_horizontal) + planeY * cos(rotation_degree_horizontal);

// 将鼠标位置重置到窗口中心
Mouse::setPosition(center, window);
}

// move forward if no wall in front of you
if (sf::Keyboard::isKeyPressed(sf::Keyboard::Up) ||
sf::Keyboard::isKeyPressed(sf::Keyboard::W))
{
if (worldMap[int(posX + dirX * moveSpeed)][int(posY)] == false)
posX += dirX * moveSpeed;
if (worldMap[int(posX)][int(posY + dirY * moveSpeed)] == false)
posY += dirY * moveSpeed;
}
// move backwards if no wall behind you
if (sf::Keyboard::isKeyPressed(sf::Keyboard::Down) ||
sf::Keyboard::isKeyPressed(sf::Keyboard::S))
{
if (worldMap[int(posX - dirX * moveSpeed)][int(posY)] == false)
posX -= dirX * moveSpeed;
if (worldMap[int(posX)][int(posY - dirY * moveSpeed)] == false)
posY -= dirY * moveSpeed;
}
// 向右走,原理还是跟前后走一样,只是方向用矩阵相乘重新算了
// 向右走就是在(dirY, -dirX)这个方向走
if (sf::Keyboard::isKeyPressed(sf::Keyboard::D))
{
if (worldMap[int(posX + dirY * moveSpeed * 0.75)][int(posY)] == false)
posX += dirY * moveSpeed * 0.75;
if (worldMap[int(posX)][int(posY - dirX * moveSpeed * 0.75)] == false)
posY -= dirX * moveSpeed * 0.75;
}
// 向左走同理
// 在(-dirY, dirX)方向
if (sf::Keyboard::isKeyPressed(sf::Keyboard::A))
{
if (worldMap[int(posX - dirY * moveSpeed * 0.75)][int(posY)] == false)
posX -= dirY * moveSpeed * 0.75;
if (worldMap[int(posX)][int(posY + dirX * moveSpeed * 0.75)] == false)
posY += dirX * moveSpeed * 0.75;
}
// rotate to the right
if (sf::Keyboard::isKeyPressed(sf::Keyboard::Right))
{
// both camera direction and camera plane must be rotated
double oldDirX = dirX;
dirX = dirX * cos(-rotSpeed) - dirY * sin(-rotSpeed);
dirY = oldDirX * sin(-rotSpeed) + dirY * cos(-rotSpeed);
double oldPlaneX = planeX;
planeX = planeX * cos(-rotSpeed) - planeY * sin(-rotSpeed);
planeY = oldPlaneX * sin(-rotSpeed) + planeY * cos(-rotSpeed);
}
// rotate to the left
if (sf::Keyboard::isKeyPressed(sf::Keyboard::Left))
{
// both camera direction and camera plane must be rotated
double oldDirX = dirX;
dirX = dirX * cos(rotSpeed) - dirY * sin(rotSpeed);
dirY = oldDirX * sin(rotSpeed) + dirY * cos(rotSpeed);
double oldPlaneX = planeX;
planeX = planeX * cos(rotSpeed) - planeY * sin(rotSpeed);
planeY = oldPlaneX * sin(rotSpeed) + planeY * cos(rotSpeed);
}
}

double calculate_FOV()
{
double length_dir = sqrt(dirX * dirX + dirY * dirY);
double length_plane = sqrt(planeX * planeX + planeY * planeY);
// 弧度制
return atan(length_plane / length_dir) * 2;
// 角度制
//// return atan(length_plane / length_dir) * 2 * 180 / acos(-1);
}

// sort algorithm
// sort the sprites based on distance
void sortSprites(int *order, double *dist, int amount)
{
std::vector<std::pair<double, int>> sprites(amount);
for (int i = 0; i < amount; i++)
{
sprites[i].first = dist[i];
sprites[i].second = order[i];
}
// 针对pair默认就用第一个变量按升序排列
std::sort(sprites.begin(), sprites.end());
// restore in reverse order to go from farthest to nearest
for (int i = 0; i < amount; i++)
{
dist[i] = sprites[amount - i - 1].first;
order[i] = sprites[amount - i - 1].second;
}
}

// 画出Decoration Sprite
void Decoration_Casting(RenderWindow &window)
{
for (int i = 0; i < decorationNum; i++)
{
decoration_Order[i] = i;
decoration_Distance[i] = ((posX - decoration_lst[i].x) * (posX - decoration_lst[i].x) + (posY - decoration_lst[i].y) * (posY - decoration_lst[i].y)); // sqrt not taken, unneeded
}
// 排序
// 先画远的再画近的不会导致透明遮挡问题
// 因为近的Sprite透明地方可以直接显示出远的Sprite
sortSprites(decoration_Order, decoration_Distance, decorationNum);
// after sorting the sprites, do the projection and draw them
for (int i = 0; i < decorationNum; i++)
{
// translate sprite position to relative to camera
double spriteX = decoration_lst[decoration_Order[i]].x - posX;
double spriteY = decoration_lst[decoration_Order[i]].y - posY;

// transform sprite with the inverse camera matrix
// [ planeX dirX ] -1 [ dirY -dirX ]
// [ ] = 1/(planeX*dirY-dirX*planeY) * [ ]
// [ planeY dirY ] [ -planeY planeX ]

double invDet = 1.0 / (planeX * dirY - dirX * planeY); // required for correct matrix multiplication

double transformX = invDet * (dirY * spriteX - dirX * spriteY);
double transformY = invDet * (-planeY * spriteX + planeX * spriteY); // this is actually the depth inside the screen, that what Z is in 3D

// sprite在屏幕上的x坐标
int spriteScreenX = int((screenWidth / 2) * (1 + transformX / transformY));

// calculate height of the sprite on screen
int spriteHeight = abs(int(screenHeight / (transformY))); // using 'transformY' instead of the real distance prevents fisheye
// calculate width of the sprite
int spriteWidth = abs(int(screenHeight / (transformY)));

// calculate lowest and highest pixel to fill in current stripe
// 计算y上的起点,不需要计算终点,因为竖线直接指定了Sprite的高度
int drawStartY = -spriteHeight / 2 + screenHeight / 2;
// if (drawStartY < 0) drawStartY = 0;

// 计算x上的起点和终点
int drawStartX = -spriteWidth / 2 + spriteScreenX;
if (drawStartX < 0)
drawStartX = 0;
int drawEndX = spriteWidth / 2 + spriteScreenX;
if (drawEndX >= screenWidth)
drawEndX = screenWidth - 1;

// loop through every vertical stripe of the sprite on screen
for (int stripe = drawStartX; stripe < drawEndX; stripe++)
{
int texX = int(256 * (stripe - (-spriteWidth / 2 + spriteScreenX)) * texWidth / spriteWidth) / 256;
// the conditions in the if are:
// 1) it's in front of camera plane so you don't see things behind you
// 2) it's on the screen (left)
// 3) it's on the screen (right)
// 4) ZBuffer, with perpendicular distance
if (transformY > 0 && stripe > 0 && stripe < screenWidth && transformY < ZBuffer[stripe])
{
// decoration一条线的Sprite
Sprite decoration_sprite;
// 设置对应纹理
decoration_sprite.setTexture(decoration_texture[decoration_lst[decoration_Order[i]].texture_index]);
// 设置sprite的纹理矩形,定义了在纹理中的哪个部分应用到decoration_sprite上
// 宽度为1个像素,相当于原来的画线
decoration_sprite.setTextureRect(sf::IntRect(texX, 0, 1, texHeight));
// 设置sprite的位置,坐标指定了左上角的位置
decoration_sprite.setPosition(stripe, drawStartY);
// 设置sprite的缩放因子
decoration_sprite.setScale(1, spriteHeight / (float)texHeight);
// 绘制sprite
window.draw(decoration_sprite);
}
}
}
}

// 画出illustration Sprite
// 如果illustration与decoration分开画,会导致透明遮挡问题
void Illustration_Casting(RenderWindow &window)
{
for (int i = 0; i < illustrationNum; i++)
{
illustration_Order[i] = i;
illustration_Distance[i] = ((posX - illustration_lst[i].x) * (posX - illustration_lst[i].x) + (posY - illustration_lst[i].y) * (posY - illustration_lst[i].y)); // sqrt not taken, unneeded
}
// 排序
// 先画远的再画近的不会导致透明遮挡问题
// 因为近的Sprite透明地方可以直接显示出远的Sprite
sortSprites(illustration_Order, illustration_Distance, illustrationNum);
// after sorting the sprites, do the projection and draw them
for (int i = 0; i < illustrationNum; i++)
{
// translate sprite position to relative to camera
double spriteX = illustration_lst[illustration_Order[i]].x - posX;
double spriteY = illustration_lst[illustration_Order[i]].y - posY;

// transform sprite with the inverse camera matrix
// [ planeX dirX ] -1 [ dirY -dirX ]
// [ ] = 1/(planeX*dirY-dirX*planeY) * [ ]
// [ planeY dirY ] [ -planeY planeX ]

double invDet = 1.0 / (planeX * dirY - dirX * planeY); // required for correct matrix multiplication

double transformX = invDet * (dirY * spriteX - dirX * spriteY);
double transformY = invDet * (-planeY * spriteX + planeX * spriteY); // this is actually the depth inside the screen, that what Z is in 3D

// sprite在屏幕上的x坐标
int spriteScreenX = int((screenWidth / 2) * (1 + transformX / transformY));

// calculate height of the sprite on screen
// 注意illustration这里需要给高度乘一个illustrationHeight / illustrationWidth
// 用于保持长宽比,不然不好看,北原春希都被压成正方形了
int spriteHeight = abs(int(screenHeight / (transformY))) * illustrationHeight / illustrationWidth; // using 'transformY' instead of the real distance prevents fisheye
// calculate width of the sprite
int spriteWidth = abs(int(screenHeight / (transformY)));

// calculate lowest and highest pixel to fill in current stripe
// 计算y上的起点,不需要计算终点,因为竖线直接指定了Sprite的高度
int drawStartY = -spriteHeight / 2 + screenHeight / 2;
// if (drawStartY < 0) drawStartY = 0;

// 计算x上的起点和终点
int drawStartX = -spriteWidth / 2 + spriteScreenX;
if (drawStartX < 0)
drawStartX = 0;
int drawEndX = spriteWidth / 2 + spriteScreenX;
if (drawEndX >= screenWidth)
drawEndX = screenWidth - 1;

// loop through every vertical stripe of the sprite on screen
for (int stripe = drawStartX; stripe < drawEndX; stripe++)
{
int texX = int(256 * (stripe - (-spriteWidth / 2 + spriteScreenX)) * illustrationWidth / spriteWidth) / 256;
// the conditions in the if are:
// 1) it's in front of camera plane so you don't see things behind you
// 2) it's on the screen (left)
// 3) it's on the screen (right)
// 4) ZBuffer, with perpendicular distance
if (transformY > 0 && stripe > 0 && stripe < screenWidth && transformY < ZBuffer[stripe])
{
// illustration一条线的Sprite
Sprite illustration_sprite;
// 设置对应纹理
illustration_sprite.setTexture(illustration_texture[illustration_lst[illustration_Order[i]].texture_index]);
// 设置sprite的纹理矩形,定义了在纹理中的哪个部分应用到illustration_sprite上
// 宽度为1个像素,相当于原来的画线
illustration_sprite.setTextureRect(sf::IntRect(texX, 0, 1, illustrationHeight));
// 设置sprite的位置,坐标指定了左上角的位置
illustration_sprite.setPosition(stripe, drawStartY);
// 设置sprite的缩放因子
illustration_sprite.setScale(1, spriteHeight / (float)illustrationHeight);
// 绘制sprite
window.draw(illustration_sprite);
}
}
}
}

void sortAllSprites(vector<int> &Allsprites_Order, vector<double> &Allsprites_Distance, int amount)
{
std::vector<std::pair<double, int>> sprites(amount);
for (int i = 0; i < amount; i++)
{
sprites[i].first = Allsprites_Distance[i];
sprites[i].second = Allsprites_Order[i];
}
std::sort(sprites.begin(), sprites.end());
// restore in reverse order to go from farthest to nearest
for (int i = 0; i < amount; i++)
{
Allsprites_Distance[i] = sprites[amount - i - 1].first;
Allsprites_Order[i] = sprites[amount - i - 1].second;
}
}

// 成功解决遮挡问题,原因:sortAllSprites排序没传递vector的引用,导致没有正常排序
void Allsprites_Casting(RenderWindow &window)
{
for (int i = 0; i < decorationNum + illustrationNum; i++)
{
Allsprites_Order[i] = i;
Allsprites_Distance[i] = ((posX - Allsprites_lst[i].x) * (posX - Allsprites_lst[i].x) + (posY - Allsprites_lst[i].y) * (posY - Allsprites_lst[i].y)); // sqrt not taken, unneeded
}
// 排序
// 先画远的再画近的不会导致透明遮挡问题
// 因为近的Sprite透明地方可以直接显示出远的Sprite
sortAllSprites(Allsprites_Order, Allsprites_Distance, decorationNum + illustrationNum);
// after sorting the sprites, do the projection and draw them
for (int i = 0; i < decorationNum + illustrationNum; i++)
{
// translate sprite position to relative to camera
double spriteX = Allsprites_lst[Allsprites_Order[i]].x - posX;
double spriteY = Allsprites_lst[Allsprites_Order[i]].y - posY;

// transform sprite with the inverse camera matrix
// [ planeX dirX ] -1 [ dirY -dirX ]
// [ ] = 1/(planeX*dirY-dirX*planeY) * [ ]
// [ planeY dirY ] [ -planeY planeX ]

double invDet = 1.0 / (planeX * dirY - dirX * planeY); // required for correct matrix multiplication

double transformX = invDet * (dirY * spriteX - dirX * spriteY);
double transformY = invDet * (-planeY * spriteX + planeX * spriteY); // this is actually the depth inside the screen, that what Z is in 3D

// sprite在屏幕上的x坐标
int spriteScreenX = int((screenWidth / 2) * (1 + transformX / transformY));

// calculate height of the sprite on screen
int spriteHeight = 0;
// decoration的height不用乘比例
if (Allsprites_lst[Allsprites_Order[i]].type == 0)
{
spriteHeight = abs(int(screenHeight / (transformY))); // using 'transformY' instead of the real distance prevents fisheye
}
// illustration的height需要乘比例
// 用于保持长宽比,不然立绘被压成正方形
else if (Allsprites_lst[Allsprites_Order[i]].type == 1)
{
spriteHeight = abs(int(screenHeight / (transformY))) * illustrationHeight / illustrationWidth; // using 'transformY' instead of the real distance prevents fisheye
}

// calculate width of the sprite
int spriteWidth = abs(int(screenHeight / (transformY)));

// calculate lowest and highest pixel to fill in current stripe
// 计算y上的起点,不需要计算终点,因为竖线直接指定了Sprite的高度
int drawStartY = -spriteHeight / 2 + screenHeight / 2;
// if (drawStartY < 0) drawStartY = 0;

// 计算x上的起点和终点
int drawStartX = -spriteWidth / 2 + spriteScreenX;
if (drawStartX < 0)
drawStartX = 0;
int drawEndX = spriteWidth / 2 + spriteScreenX;
if (drawEndX >= screenWidth)
drawEndX = screenWidth - 1;

// loop through every vertical stripe of the sprite on screen
for (int stripe = drawStartX; stripe < drawEndX; stripe++)
{
// the conditions in the if are:
// 1) it's in front of camera plane so you don't see things behind you
// 2) it's on the screen (left)
// 3) it's on the screen (right)
// 4) ZBuffer, with perpendicular distance
if (transformY > 0 && stripe > 0 && stripe < screenWidth && transformY < ZBuffer[stripe])
{
// 画decoration
if (Allsprites_lst[Allsprites_Order[i]].type == 0)
{
// 计算texX
int texX = int(256 * (stripe - (-spriteWidth / 2 + spriteScreenX)) * texWidth / spriteWidth) / 256;
// decoration一条线的Sprite
Sprite decoration_sprite;
// 设置对应纹理
decoration_sprite.setTexture(decoration_texture[Allsprites_lst[Allsprites_Order[i]].texture_index]);
// 设置sprite的纹理矩形,定义了在纹理中的哪个部分应用到decoration_sprite上
// 宽度为1个像素,相当于原来的画线
decoration_sprite.setTextureRect(sf::IntRect(texX, 0, 1, texHeight));
// 设置sprite的位置,坐标指定了左上角的位置
decoration_sprite.setPosition(stripe, drawStartY);
// 设置sprite的缩放因子
decoration_sprite.setScale(1, spriteHeight / (float)texHeight);
// 绘制sprite
window.draw(decoration_sprite);
}
// 画illustration
else if (Allsprites_lst[Allsprites_Order[i]].type == 1)
{
// 计算texX
int texX = int(256 * (stripe - (-spriteWidth / 2 + spriteScreenX)) * illustrationWidth / spriteWidth) / 256;
// illustration一条线的Sprite
Sprite illustration_sprite;
// 设置对应纹理
illustration_sprite.setTexture(illustration_texture[Allsprites_lst[Allsprites_Order[i]].texture_index]);
// 设置sprite的纹理矩形,定义了在纹理中的哪个部分应用到illustration_sprite上
// 宽度为1个像素,相当于原来的画线
illustration_sprite.setTextureRect(sf::IntRect(texX, 0, 1, illustrationHeight));
// 设置sprite的位置,坐标指定了左上角的位置
illustration_sprite.setPosition(stripe, drawStartY);
// 设置sprite的缩放因子
illustration_sprite.setScale(1, spriteHeight / (float)illustrationHeight);
// 绘制sprite
window.draw(illustration_sprite);
}
}
}
}
}

// 要注意:SFML中的y轴是向下的,而用二维数组计算时y轴是向上的
// 需将游戏中的坐标系转换为小地图的坐标系,具体来说,就是将y坐标进行翻转
void drawMiniMap(RenderWindow &window)
{
// 遍历地图的每一个格子
for (int x = 0; x < minimapWidth; x++)
{
for (int y = 0; y < minimapHeight; y++)
{
// 创建一个表示地图格子的矩形
RectangleShape rect(Vector2f(miniMapScale, miniMapScale));
rect.setPosition(x * miniMapScale, (mapHeight - y - 1) * miniMapScale);

// 根据地图数据设置矩形的颜色
if (worldMap[x][y] > 0)
{
rect.setFillColor(Color::White);
}
else
{
rect.setFillColor(Color::Black);
}

// 在窗口上绘制矩形
window.draw(rect);
}
}

// 创建一个表示玩家位置的圆形
int radius = 4;
CircleShape player_circle(radius);
player_circle.setFillColor(Color::Red);
player_circle.setPosition(posX * miniMapScale - player_circle.getRadius(), (mapHeight - posY) * miniMapScale - player_circle.getRadius());
// 在窗口上绘制圆形
window.draw(player_circle);

// 设置三角扇形的原点为玩家的位置
// FOV顶点具体计算在DDA算法里,保存在FOV_position数组中
FOV_Visualize[0].position = Vector2f(posX * miniMapScale, (mapHeight - posY) * miniMapScale);
FOV_Visualize[0].color = Color::Transparent;
for (int i = 1; i <= screenWidth; i++)
{
// 设置FOV的位置
FOV_Visualize[i].position = FOV_position[i];
}
// 在窗口上绘制三角扇形
window.draw(FOV_Visualize);
}

参考资料

Raycasting (lodev.org)

Raycasting II: Floor and Ceiling (lodev.org)

Raycasting III: Sprites (lodev.org)

Raycasting学习记录

I Used RAYCASTING to Make a HORROR Game in C++

New-Raycasting: Horror game using Raycasting

《白色相簿2》中的PAK文件解包