2D阴影效果

发布时间 2023-10-02 13:51:33作者: Ycj475401

效果:

效果图

实现类似手电筒的光照,有阴影部分也有光照部分,可以用在2D游戏里。
本篇博客将用绘制多边形和渐变圆的方法实现上图效果,语言是C++配Ege。

方法一:光线投射

计算量巨大,还会有类似摩尔纹的效果,这里就不实现了。

方法二:绘制光线区域

可以参考这篇博客,大致思路是对于每个顶点向他发射一条光线,所有光线撞到墙的位置按顺时针排序,然后直接作为多边形绘制就行。

这里放一下作者巨丑的远古代码,观众姥爷们能看懂思路就行 ?

代码

#include <bits/stdc++.h>
#include <graphics.h>

#define HEIGHT 400
#define WIDTH 400

using namespace std;
HWND Console = GetConsoleWindow(), Ege_window;
const double kPi = 3.1415926536, kEps = 1e-5;
mt19937 rng(GetTickCount());
int rdmInt(int l, int r) {return uniform_int_distribution<>(l, r)(rng);}
double rdmDb(double l, double r) {return uniform_real_distribution<>(l, r)(rng);}
color_t Rgb(const int r, const int g, const int b) {return EGEARGB(255, r, g, b);}
int GetR(const color_t color) {return color >> 16;} // EGEGET_R
int GetG(const color_t color) {return (color >> 8) - ((color >> 16) << 8);}
int GetB(const color_t color) {return color - ((color >> 8) << 8);}
double Dis(double x1, double y1, double x2, double y2) {return sqrt(pow(x1 - x2, 2) + pow(y1 - y2, 2));}
double Dis2(double x1, double y1, double x2, double y2) {return abs(x1 - x2) + abs(y1 - y2);}
template<typename List_type> class List { public: struct Lst_node { bool use; List_type var; int nxt; }; int head, tail, size, mxSize; Lst_node *a; List(int _mxSize) { head = -1, mxSize = _mxSize, size = 0; a = new Lst_node[_mxSize + 3]; } int Find_empty() { for (int i = 1; i <= mxSize; i++) { if (!a[i].use) { return i; } } return -1; } int Number(int _i) { int i = 1, j; for (j = head; a[j].use && i < _i; j = a[j].nxt, i++); return j; } bool Add(List_type _x) { if (size >= mxSize) return 0; if (head == -1) { head = 1, tail = 1, size++; a[1] = (Lst_node) {1, _x, -1}; } else { int _p = Find_empty(); a[tail].nxt = _p, tail = _p, a[_p] = (Lst_node) {1, _x, -1}; size++; } return 1; } bool Delete(int _x) { int _pre; if (!size) return 0; if (_x != head && _x != tail) { for (int j = head; a[j].use; j = a[j].nxt) { if (a[j].nxt == _x) { _pre = j; break; } } a[_pre].nxt = a[_x].nxt; a[_x].use = 0; } else if (_x == head && _x == tail) { a[head].use = 0; head = -1, tail = 0; } else if (_x == head) { head = a[_x].nxt, a[_x].use = 0; } else if (_x == tail) { for (int j = head; a[j].use; j = a[j].nxt) { if (a[j].nxt == tail) { _pre = j; break; } } tail = _pre, a[_x].use = 0; } size--; return 1; } };
double _sin(double angle) {return sin(angle * kPi / 180.0);}
double _cos(double angle) {return cos(angle * kPi / 180.0);}
double Sin(double angle) {if (angle >= 0 && angle <= 90) return -_sin(angle);if (angle >= 90 && angle <= 180)	return -_sin(180 - angle);if (angle >= 180 && angle <= 270)	return _sin(angle - 180); if (angle >= 270 && angle <= 360)	return _sin(360 - angle);}
double Cos(double angle) {if (angle >= 0 && angle <= 90) return _cos(angle); if (angle >= 90 && angle <= 180)	return -_cos(180 - angle);if (angle >= 180 && angle <= 270)	return -_cos(angle - 180);if (angle >= 270 && angle <= 360)	return _cos(360 - angle);}
template<typename T> void AngleCorrect(T &angle) {for (; angle < 0; angle += 360);for (; angle >= 360; angle -= 360);}
template<typename T> T AngleCorrect_(T angle) {for (; angle < 0; angle += 360);for (; angle >= 360; angle -= 360);return angle;}
double GetAngle(double x1, double y1, double x2, double y2) {if (abs(x1 - x2) <= 1e-4) {if (y1 > y2) return 90; if (y1 < y2) return 270;}double _angle = atan(abs(y1 - y2) / abs(x1 - x2)) * 180.0 / kPi;if (x1 > x2) {if (y1 > y2) _angle = 180 - _angle; else _angle -= 180;}if (x1 < x2) if (y1 < y2) _angle = 360 - _angle;AngleCorrect(_angle);return _angle;}
int mousePosX, mousePosY; void GetMousePos() {POINT p;GetCursorPos(&p);ScreenToClient(Ege_window, &p), mousePosX = p.x, mousePosY = p.y;}
char _cStr[1003];
void CutoutImage(PIMAGE dest, int x, int y, PIMAGE src, PIMAGE mask) {
	int width = getwidth(src), height = getheight(src);
	putimage(dest,  0, 0, mask, 0x00220326);
	PIMAGE temp = newimage(width, height);
	putimage(temp, 0, 0, src), putimage(temp, 0, 0, mask, SRCAND), putimage(dest, 0, 0, temp, SRCPAINT);
	delimage(temp);
}

using Pii = pair<int, int>;
struct Line {double x1, y1, x2, y2;};
struct Wall {int n; ege_point p[13];};

bool IsParallel(Line a, Line b) {
	return fabs((a.y2 - a.y1) * (b.x2 - b.x1) - (a.x2 - a.x1) * (b.y2 - b.y1)) < kEps;
}
bool IsIntersect(Line a, Line b) {
	double _l1 = (b.x1 * (b.y2 - b.y1) + a.y1 * (b.x2 - b.x1) - b.y1 * (b.x2 - b.x1) - a.x1 * (b.y2 - b.y1)) / ((a.x2 - a.x1) * (b.y2 - b.y1) - (b.x2 - b.x1) * (a.y2 - a.y1));
	double _l2 = (a.x1 * (a.y2 - a.y1) + b.y1 * (a.x2 - a.x1) - a.y1 * (a.x2 - a.x1) - b.x1 * (a.y2 - a.y1)) / ((b.x2 - b.x1) * (a.y2 - a.y1) - (a.x2 - a.x1) * (b.y2 - b.y1));
	return _l1 >= 0 && _l1 <= 1 && _l2 >= 0 && _l2 <= 1;
}
ege_point GetIntersect(Line a, Line b) {
	double _l1 = (b.x1 * (b.y2 - b.y1) + a.y1 * (b.x2 - b.x1) - b.y1 * (b.x2 - b.x1) - a.x1 * (b.y2 - b.y1)) / ((a.x2 - a.x1) * (b.y2 - b.y1) - (b.x2 - b.x1) * (a.y2 - a.y1));
	double _l2 = (a.x1 * (a.y2 - a.y1) + b.y1 * (a.x2 - a.x1) - a.y1 * (a.x2 - a.x1) - b.x1 * (a.y2 - a.y1)) / ((b.x2 - b.x1) * (a.y2 - a.y1) - (a.x2 - a.x1) * (b.y2 - b.y1));
	float _x = a.x1 + _l1 * (a.x2 - a.x1);
	float _y = a.y1 + _l1 * (a.y2 - a.y1);
	return ege_point{_x, _y};
}

const int kSightWidth = 30;

double px = WIDTH / 2, py = HEIGHT / 2;
double targetAngle;

PIMAGE screenImg = newimage(WIDTH, HEIGHT);
PIMAGE coverImg = newimage(WIDTH, HEIGHT), coverTmpImg = newimage(WIDTH, HEIGHT);
PIMAGE bgImg = newimage(WIDTH, HEIGHT);

vector<Wall> wall;
vector<Line> wallEdge;
vector<ege_point> vertex;
void InitData() {
	wall.push_back(Wall{5, {{0, 0}, {WIDTH, 0}, {WIDTH, HEIGHT}, {0, HEIGHT}, {0, 0}}});
	
	wall.push_back(Wall{5, {{100, 100}, {150, 110}, {150, 140}, {50, 150}, {100, 100}}});
	wall.push_back(Wall{4, {{300, 100}, {270, 150}, {300, 200}, {300, 100}}});
	wall.push_back(Wall{8, {{180, 260}, {140, 310}, {210, 320}, {290, 190}, {230, 210}, {100, 250}, {120, 260}, {180, 260}}});
}
map<Pii, bool> InitVertexMap;
void InitVertex() {
	vertex.clear(), InitVertexMap.clear();
	for (Wall &it : wall) {
		for (int i = 0; i < it.n; i++) {
			ege_point *p = &it.p[i];
			Pii p2 = make_pair(p -> x, p -> y);
			if (!InitVertexMap[p2]) {
				InitVertexMap[p2] = 1;
				vertex.push_back(*p);
			}
		}
	}
}
void InitEdge() {
	wallEdge.clear();
	for (Wall &it : wall) {
		ege_point lastPoint = it.p[0];
		for (int i = 1; i < it.n; i++) {
			ege_point p = it.p[i];
			wallEdge.push_back(Line{p.x, p.y, lastPoint.x, lastPoint.y});
			lastPoint = p;
		}
	}
}

struct VertexAngle {double a;};
bool operator<(VertexAngle x, VertexAngle y) {
	double x_a1 = AngleCorrect_(x.a - targetAngle), x_a2 = AngleCorrect_(targetAngle - x.a), xAngle = min(x_a1, x_a2) * (x_a1 < x_a2 ? 1 : -1);
	double y_a1 = AngleCorrect_(y.a - targetAngle), y_a2 = AngleCorrect_(targetAngle - y.a), yAngle = min(y_a1, y_a2) * (y_a1 < y_a2 ? 1 : -1);
	return xAngle < yAngle;
}
int shadowVertexCnt; ege_point shadowVertex[103];
void CalcShadow() {
	shadowVertexCnt = 0;
	set<VertexAngle> vertexAngle;
	vertexAngle.insert({AngleCorrect_(targetAngle - kSightWidth)}), vertexAngle.insert({AngleCorrect_(targetAngle + kSightWidth)});
	for (ege_point &it : vertex) {
		double _angle = GetAngle(px, py, it.x, it.y);
		if (AngleCorrect_(_angle - targetAngle) <= kSightWidth || AngleCorrect_(targetAngle - _angle) <= kSightWidth) {
			vertexAngle.insert({_angle});
			vertexAngle.insert({AngleCorrect_(_angle - 0.001)});
			vertexAngle.insert({AngleCorrect_(_angle + 0.001)});
		}
	}
	shadowVertex[shadowVertexCnt++] = ege_point{(float) px, (float) py};
	for (VertexAngle _vta : vertexAngle) {
		double angle = _vta.a, minDis = 1e9; ege_point minDisP;
		Line _pLine = Line{px, py, px + Cos(angle) * 10000, py + Sin(angle) * 10000};
		for (Line &line : wallEdge) {
			if (IsIntersect(line, _pLine) && !IsParallel(line, _pLine)) {
				ege_point _p = GetIntersect(line, _pLine);
				double _dis = Dis(px, py, _p.x, _p.y);
				if (_dis < minDis) {
					minDis = _dis, minDisP = _p;
				}
			}
		}
		shadowVertex[shadowVertexCnt++] = minDisP;
		
		if (keystate('O')) {setcolor(EGEARGB(50, 100, 255, 50)), line(px, py, minDisP.x, minDisP.y), setfillcolor(EGEARGB(255, 100, 255, 50)), ege_fillellipse(minDisP.x - 1, minDisP.y - 1, 5, 5);}
		if (keystate('I')) {
			double x_a1 = AngleCorrect_(angle - targetAngle), x_a2 = AngleCorrect_(targetAngle - angle), xAngle = min(x_a1, x_a2) * (x_a1 < x_a2 ? 1 : -1);
			setcolor(EGEARGB(255, 255, 100, 100)), setfont(10, 5, "consolas"), sprintf(_cStr, "%.1lf", xAngle), ege_drawtext(_cStr, minDisP.x, minDisP.y);
		}
	}
	shadowVertex[shadowVertexCnt++] = ege_point{(float) px, (float) py};
}
void Draw() {
	cleardevice(coverImg), setfillcolor(EGEARGB(255, 0, 0, 0), coverImg), ege_fillrect(0, 0, WIDTH, HEIGHT, coverImg), setfillcolor(EGEARGB(255, 255, 255, 255), coverImg), ege_fillpoly(shadowVertexCnt, shadowVertex, coverImg);
	CutoutImage(NULL, 0, 0, bgImg, coverImg);
	
	setlinewidth(1.6), setcolor(EGEARGB(255, 255, 255, 255));
	for (Wall &it : wall) {
		ege_drawpoly(it.n, it.p);
	}
	
	setcolor(Rgb(255, 0, 0)), setfillcolor(Rgb(255, 0, 0)), fillellipse(px, py, 3, 3);
}
void Main() {
	setfillcolor(EGEARGB(255, 100, 100, 100), bgImg), bar(0, 0, WIDTH, HEIGHT, bgImg);
	setfillcolor(EGEARGB(255, 255, 100, 100), bgImg), bar(50, 50, 60, 60, bgImg);
	
	ege_enable_aa(1);
	InitData();
	InitVertex();
	InitEdge();
	for (;; delay_fps(70)) {
		if (keystate(VK_ESCAPE)) break;
		GetMousePos();
		
		if (keystate('A')) px -= 2.5;
		if (keystate('D')) px += 2.5;
		if (keystate('W')) py -= 2.5;
		if (keystate('S')) py += 2.5;
		targetAngle = GetAngle(px * 1000, py * 1000, mousePosX * 1000, mousePosY * 1000);
		
		cleardevice(), Draw();
		CalcShadow();
	}
	delimage(coverImg), delimage(bgImg), delimage(coverTmpImg), delimage(screenImg);
}

signed main() {
	ShowWindow(Console, 1), initgraph(WIDTH, HEIGHT), setbkcolor(BLACK);
	setcaption("EGE Window"), Ege_window = FindWindow(NULL, "EGE Window");
	Main();
	closegraph();
	return 0;
}

效果

图

效率

cpu占用率

可以看到比这个速度还是不错的,但地图中有大量墙壁时计算量会很大。

方法三:绘制阴影区域

这个做法是作者自己想出来的,网上很少见但速度还是可以的。

对于每个墙壁(凸多边),我们对它的每个端点发射一条射线,找到最外端的两条,如图:

图

然后把这些点加入vector:

  1. 最外端A 指向的端点(即那条线上的那个红点)
  2. 最外端A 上离眼睛特别远的一点
  3. 最外端B 指向的端点
  4. 最外端B 上离眼睛特别远的一点

最后按顺时针顺序排序vector,绘制成多边形输出即可。

细节

我们怎么找最外端的两条射线呢?首先选出一个幸运儿端点,算出眼睛朝向它的方向作为基准射线。再对每条发射向端点的射线与基准射线算个夹角,再用这个射线在基准的顺时针远近作为符号。

举个例子,角度是这样算的:
图

代码

#include <bits/stdc++.h>
#include <graphics.h>

using namespace std;

struct Vec2 {
	double x, y;
	Vec2() {}
	Vec2(double _x, double _y) : x(_x), y(_y) {}
};
double Dis(Vec2 x, Vec2 y) {return sqrt((x.x - y.x) * (x.x - y.x) + (x.y - y.y) * (x.y - y.y));}
double Length(Vec2 x) {return sqrt(x.x * x.x + x.y * x.y);}
Vec2 operator-(Vec2 x) {return Vec2(-x.x, -x.y);}
Vec2 operator+(Vec2 x, Vec2 y) {return Vec2(x.x + y.x, x.y + y.y);}
Vec2 operator+=(Vec2 &x, Vec2 y) {return x = x + y;}
Vec2 operator-(Vec2 x, Vec2 y) {return Vec2(x.x - y.x, x.y - y.y);}
Vec2 operator-=(Vec2 &x, Vec2 y) {return x = x - y;}
Vec2 operator*(Vec2 x, double k) {return Vec2(x.x * k, x.y * k);}
Vec2 operator*=(Vec2 &x, double k) {return x = x * k;}
Vec2 Normalize(Vec2 x) {return x * (1 / Length(x));}
Vec2 MakeVec2(Vec2 x, Vec2 y) {return Normalize(y - x);}

const double kPi = 3.14159265357;
void AngleCorrect(double &angle) {
	for (; angle < 0; angle += 360);
	for (; angle >= 360; angle -= 360);
}
double AngleCorrect_(double angle) {
	for (; angle < 0; angle += 360);
	for (; angle >= 360; angle -= 360);
	return angle;
}
double GetAngle(double x1, double y1, double x2, double y2) {
	if (abs(x1 - x2) <= 1e-4) {
		return (y1 > y2 ? 90 : 270);
	}
	double _angle = atan(abs(y1 - y2) / abs(x1 - x2)) * 180.0 / kPi;
	if (x1 > x2) {
		if (y1 > y2) _angle = 180 - _angle;
		else _angle -= 180;
	}
	if (x1 < x2) if (y1 < y2) _angle = 360 - _angle;
	return AngleCorrect_(_angle);
}
const int SHEIGHT = 200, SWIDTH = 200;


double px = SWIDTH / 2, py = SHEIGHT / 2;

PIMAGE darkFieldImg = newimage(1000, 1000);

struct Shape {
	vector<Vec2> vertex;
	Shape() {}
	Shape(vector<Vec2> _vertex) {
		for (Vec2 it : _vertex) {
			vertex.push_back(it);
		}
	}
	void Draw(PIMAGE dest = NULL) {
		ege_point ep[(unsigned int) vertex.size()];
		for (int i = 0; i < vertex.size(); i++) {
			ep[i] = ege_point{(float) vertex[i].x, (float) vertex[i].y};
		}
		ege_fillpoly(vertex.size(), ep, dest);
	}
}; vector<Shape> shapes;
void MakeShape_rectangle(double lft, double top, double rht, double btm) {
	shapes.push_back(Shape({Vec2(lft, top), Vec2(rht, top), Vec2(rht, btm), Vec2(lft, btm)}));
}
void MakeShape_lineRect(Vec2 a, Vec2 b, double r = 2) {
	MakeShape_rectangle(min(a.x, b.x) - r, min(a.y, b.y) - r, max(a.x, b.x) + r, max(a.y, b.y) + r);
}
double GetAngleDeflect(double baseAngle, double angle) {
	double leftDeflect = AngleCorrect_(baseAngle - angle), rightDeflect = AngleCorrect_(angle - baseAngle);
	if (leftDeflect <= rightDeflect) {
		return leftDeflect;
	} else {
		return -rightDeflect;
	}
}
namespace ShadowSystem {
	struct VertexAngle {
		bool isOnVertex;
		double angle, dis;
		Vec2 pos;
	}; vector<VertexAngle> node;
	
	void AddVertex(Vec2 eyePos, Vec2 vertex) {
		Vec2 extendVertex = vertex + MakeVec2(eyePos, vertex) * 1e5;
		double angle = GetAngle(eyePos.x, eyePos.y, vertex.x, vertex.y);
		node.push_back(VertexAngle{1, GetAngleDeflect(180, angle), Dis(eyePos, vertex), vertex});
		node.push_back(VertexAngle{0, GetAngleDeflect(180, angle), Dis(eyePos, extendVertex), extendVertex});
	}
	void DrawShadow(Vec2 eyePos) {
		for (Shape itShape : shapes) {
			node.clear();
			Vec2 mostLeftVertex = itShape.vertex[0], mostRightVertex = itShape.vertex[0];
			double mostLeftAngle = 0, mostRightAngle = 0;
			double baseAngle = GetAngle(eyePos.x, eyePos.y, itShape.vertex[0].x, itShape.vertex[0].y);
			for (Vec2 it : itShape.vertex) {
				double angle = GetAngle(eyePos.x, eyePos.y, it.x, it.y);
				double deflect = GetAngleDeflect(baseAngle, angle);
				if (deflect < 0) {
					if (deflect < mostLeftAngle) {
						mostLeftAngle = deflect;
						mostLeftVertex = it;
					}
				} else {
					if (deflect > mostRightAngle) {
						mostRightAngle = deflect;
						mostRightVertex = it;
					}
				}
			}
			AddVertex(eyePos, mostLeftVertex);
			AddVertex(eyePos, mostRightVertex);
			sort(node.begin(), node.end(), [](VertexAngle x, VertexAngle y) {
				if (x.isOnVertex != y.isOnVertex) {
					return x.isOnVertex;
				}
				if (x.isOnVertex) {
					if (x.angle == y.angle) {
						return x.dis < y.dis;
					} else {
						return x.angle < y.angle;
					}
				} else {
					if (x.angle == y.angle) {
						return x.dis < y.dis;
					} else {
						return x.angle > y.angle;
					}
				}
			});
			ege_point ep[(unsigned int) itShape.vertex.size()];
			for (int i = 0; i < node.size(); i++) {
				ep[i] = ege_point{(float) node[i].pos.x, (float) node[i].pos.y};
			}
			setfillcolor(EGERGB(0, 0, 0)), ege_fillpoly(node.size(), ep);
		}
	}
}

void Draw() {
	setfillcolor(EGERGB(100, 100, 100)), bar(0, 0, SWIDTH, SHEIGHT);
	
	ShadowSystem::DrawShadow(Vec2(px, py));
	setfillcolor(EGERGB(255, 255, 255));
	for (Shape itShape : shapes) {
		itShape.Draw();
	}
	
	setfillcolor(EGERGB(0, 162, 232)), bar(px - 3, py - 3, px + 3, py + 3);
	putimage_withalpha(NULL, darkFieldImg, px - 500, py - 500);
	//putimage_alphablend(NULL, darkFieldImg, px - 500, py - 500, 128);
}

void Init_DarkFieldImg() {
	color_t *buffer = getbuffer(darkFieldImg);
	for (int i = 0; i < 1000; i++) {
		for (int j = 0; j < 1000; j++) {
			buffer[i * 1000 + j] = EGEARGB(0, 0, 0, 0);
		}
	}
	ege_enable_aa(1, darkFieldImg), ege_setpattern_ellipsegradient(ege_point{500, 500}, EGEARGB(0, 0, 0, 0), 0, 0, 1000, 1000, EGEARGB(255, 0, 0, 0), darkFieldImg), ege_fillrect(0, 0, 1000, 1000, darkFieldImg), ege_setpattern_none(darkFieldImg);
}
void Init_Map() {
	MakeShape_lineRect(Vec2(50, 0), Vec2(50, 50));
	MakeShape_lineRect(Vec2(50, 50), Vec2(150, 50));
	MakeShape_lineRect(Vec2(190, 50), Vec2(190, 0));
	
	MakeShape_lineRect(Vec2(0, 150), Vec2(100, 150));
	MakeShape_lineRect(Vec2(100, 150), Vec2(100, 180));
	
	shapes.push_back(Shape({Vec2(110, 110), Vec2(150, 130), Vec2(120, 140), Vec2(100, 120)}));
}
void Main() {
	Init_DarkFieldImg();
	Init_Map();
	
	for (;; delay_fps(60)) {
		if (keystate(key_esc)) {
			goto exitGameLoop;
		}
		
		if (keystate('W')) py -= 2.5;
		if (keystate('S')) py += 2.5;
		if (keystate('A')) px -= 2.5;
		if (keystate('D')) px += 2.5;
		
		cleardevice(), Draw();
	}
	exitGameLoop:;
}

signed main() {
	initgraph(SWIDTH, SHEIGHT);
	setcaption("EGE Window");
	ege_enable_aa(1);
	Main();
	closegraph();
	return 0;
}

效果

图

效率

图

虽然端点数量不同,但看得出来这个明显比绘制视野快很多的。

结语

这种2D阴影效果还是挺不错的,与仅仅是绘制地图相比增加了游戏的神秘感(?),希望这篇博客能帮到大家 ?。