1 /* StarField.cpp
2 Copyright (c) 2014 by Michael Zahniser
3 
4 Endless Sky is free software: you can redistribute it and/or modify it under the
5 terms of the GNU General Public License as published by the Free Software
6 Foundation, either version 3 of the License, or (at your option) any later version.
7 
8 Endless Sky is distributed in the hope that it will be useful, but WITHOUT ANY
9 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
10 PARTICULAR PURPOSE.  See the GNU General Public License for more details.
11 */
12 
13 #include "StarField.h"
14 
15 #include "Angle.h"
16 #include "Body.h"
17 #include "DrawList.h"
18 #include "pi.h"
19 #include "Point.h"
20 #include "Preferences.h"
21 #include "Random.h"
22 #include "Screen.h"
23 #include "Sprite.h"
24 #include "SpriteSet.h"
25 
26 #include <algorithm>
27 #include <cmath>
28 #include <numeric>
29 
30 using namespace std;
31 
32 namespace {
33 	const int TILE_SIZE = 256;
34 	// The star field tiles in 4000 pixel increments. Have the tiling of the haze
35 	// field be as different from that as possible. (Note: this may need adjusting
36 	// in the future if monitors larger than this width ever become commonplace.)
37 	const double HAZE_WRAP = 6627.;
38 	// Don't let two haze patches be closer to each other than this distance. This
39 	// avoids having very bright haze where several patches overlap.
40 	const double HAZE_DISTANCE = 1200.;
41 	// This is how many haze fields should be drawn.
42 	const size_t HAZE_COUNT = 16;
43 }
44 
45 
46 
Init(int stars,int width)47 void StarField::Init(int stars, int width)
48 {
49 	SetUpGraphics();
50 	MakeStars(stars, width);
51 
52 	const Sprite *sprite = SpriteSet::Get("_menu/haze");
53 	for(size_t i = 0; i < HAZE_COUNT; ++i)
54 	{
55 		Point next;
56 		bool overlaps = true;
57 		while(overlaps)
58 		{
59 			next = Point(Random::Real() * HAZE_WRAP, Random::Real() * HAZE_WRAP);
60 			overlaps = false;
61 			for(const Body &other : haze)
62 			{
63 				Point previous = other.Position();
64 				double dx = remainder(previous.X() - next.X(), HAZE_WRAP);
65 				double dy = remainder(previous.Y() - next.Y(), HAZE_WRAP);
66 				if(dx * dx + dy * dy < HAZE_DISTANCE * HAZE_DISTANCE)
67 				{
68 					overlaps = true;
69 					break;
70 				}
71 			}
72 		}
73 		haze.emplace_back(sprite, next, Point(), Angle::Random(), 8.);
74 	}
75 }
76 
77 
78 
SetHaze(const Sprite * sprite)79 void StarField::SetHaze(const Sprite *sprite)
80 {
81 	// If no sprite is given, set the default one.
82 	if(!sprite)
83 		sprite = SpriteSet::Get("_menu/haze");
84 
85 	for(Body &body : haze)
86 		body.SetSprite(sprite);
87 }
88 
89 
90 
Draw(const Point & pos,const Point & vel,double zoom) const91 void StarField::Draw(const Point &pos, const Point &vel, double zoom) const
92 {
93 	// Draw the starfield unless it is disabled in the preferences.
94 	if(Preferences::Has("Draw starfield"))
95 	{
96 		glUseProgram(shader.Object());
97 		glBindVertexArray(vao);
98 
99 		float length = vel.Length();
100 		Point unit = length ? vel.Unit() : Point(1., 0.);
101 		// Don't zoom the stars at the same rate as the field; otherwise, at the
102 		// farthest out zoom they are too small to draw well.
103 		unit /= pow(zoom, .75);
104 
105 		float baseZoom = static_cast<float>(2. * zoom);
106 		GLfloat scale[2] = {baseZoom / Screen::Width(), -baseZoom / Screen::Height()};
107 		glUniform2fv(scaleI, 1, scale);
108 
109 		GLfloat rotate[4] = {
110 			static_cast<float>(unit.Y()), static_cast<float>(-unit.X()),
111 			static_cast<float>(unit.X()), static_cast<float>(unit.Y())};
112 		glUniformMatrix2fv(rotateI, 1, false, rotate);
113 
114 		glUniform1f(elongationI, length * zoom);
115 		glUniform1f(brightnessI, min(1., pow(zoom, .5)));
116 
117 		// Stars this far beyond the border may still overlap the screen.
118 		double borderX = fabs(vel.X()) + 1.;
119 		double borderY = fabs(vel.Y()) + 1.;
120 		// Find the absolute bounds of the star field we must draw.
121 		int minX = pos.X() + (Screen::Left() - borderX) / zoom;
122 		int minY = pos.Y() + (Screen::Top() - borderY) / zoom;
123 		int maxX = pos.X() + (Screen::Right() + borderX) / zoom;
124 		int maxY = pos.Y() + (Screen::Bottom() + borderY) / zoom;
125 		// Round down to the start of the nearest tile.
126 		minX &= ~(TILE_SIZE - 1l);
127 		minY &= ~(TILE_SIZE - 1l);
128 
129 		for(int gy = minY; gy < maxY; gy += TILE_SIZE)
130 			for(int gx = minX; gx < maxX; gx += TILE_SIZE)
131 			{
132 				Point off = Point(gx, gy) - pos;
133 				GLfloat translate[2] = {
134 					static_cast<float>(off.X()),
135 					static_cast<float>(off.Y())
136 				};
137 				glUniform2fv(translateI, 1, translate);
138 
139 				int index = (gx & widthMod) / TILE_SIZE + ((gy & widthMod) / TILE_SIZE) * tileCols;
140 				int first = 6 * tileIndex[index];
141 				int count = 6 * tileIndex[index + 1] - first;
142 				glDrawArrays(GL_TRIANGLES, first, count);
143 			}
144 
145 		glBindVertexArray(0);
146 		glUseProgram(0);
147 	}
148 
149 	// Draw the background haze unless it is disabled in the preferences.
150 	if(!Preferences::Has("Draw background haze"))
151 		return;
152 
153 	DrawList drawList;
154 	drawList.Clear(0, zoom);
155 	drawList.SetCenter(pos);
156 
157 	// Any object within this range must be drawn. Some haze sprites may repeat
158 	// more than once if the view covers a very large area.
159 	Point size = Point(1., 1.) * haze.front().Radius();
160 	Point topLeft = pos + (Screen::TopLeft() - size) / zoom;
161 	Point bottomRight = pos + (Screen::BottomRight() + size) / zoom;
162 	for(const Body &it : haze)
163 	{
164 		// Figure out the position of the first instance of this haze that is to
165 		// the right of and below the top left corner of the screen.
166 		double startX = fmod(it.Position().X() - topLeft.X(), HAZE_WRAP);
167 		startX += topLeft.X() + HAZE_WRAP * (startX < 0.);
168 		double startY = fmod(it.Position().Y() - topLeft.Y(), HAZE_WRAP);
169 		startY += topLeft.Y() + HAZE_WRAP * (startY < 0.);
170 
171 		// Draw any instances of this haze that are on screen.
172 		for(double y = startY; y < bottomRight.Y(); y += HAZE_WRAP)
173 			for(double x = startX; x < bottomRight.X(); x += HAZE_WRAP)
174 				drawList.Add(it, Point(x, y));
175 	}
176 	drawList.Draw();
177 }
178 
179 
180 
SetUpGraphics()181 void StarField::SetUpGraphics()
182 {
183 	static const char *vertexCode =
184 		"// vertex starfield shader\n"
185 		"uniform mat2 rotate;\n"
186 		"uniform vec2 translate;\n"
187 		"uniform vec2 scale;\n"
188 		"uniform float elongation;\n"
189 		"uniform float brightness;\n"
190 
191 		"in vec2 offset;\n"
192 		"in float size;\n"
193 		"in float corner;\n"
194 		"out float fragmentAlpha;\n"
195 		"out vec2 coord;\n"
196 
197 		"void main() {\n"
198 		"  fragmentAlpha = brightness * (4. / (4. + elongation)) * size * .2 + .05;\n"
199 		"  coord = vec2(sin(corner), cos(corner));\n"
200 		"  vec2 elongated = vec2(coord.x * size, coord.y * (size + elongation));\n"
201 		"  gl_Position = vec4((rotate * elongated + translate + offset) * scale, 0, 1);\n"
202 		"}\n";
203 
204 	static const char *fragmentCode =
205 		"// fragment starfield shader\n"
206 		"in float fragmentAlpha;\n"
207 		"in vec2 coord;\n"
208 		"out vec4 finalColor;\n"
209 
210 		"void main() {\n"
211 		"  float alpha = fragmentAlpha * (1. - abs(coord.x) - abs(coord.y));\n"
212 		"  finalColor = vec4(1, 1, 1, 1) * alpha;\n"
213 		"}\n";
214 
215 	shader = Shader(vertexCode, fragmentCode);
216 
217 	// make and bind the VAO
218 	glGenVertexArrays(1, &vao);
219 	glBindVertexArray(vao);
220 
221 	// make and bind the VBO
222 	glGenBuffers(1, &vbo);
223 	glBindBuffer(GL_ARRAY_BUFFER, vbo);
224 
225 	offsetI = shader.Attrib("offset");
226 	sizeI = shader.Attrib("size");
227 	cornerI = shader.Attrib("corner");
228 
229 	scaleI = shader.Uniform("scale");
230 	rotateI = shader.Uniform("rotate");
231 	elongationI = shader.Uniform("elongation");
232 	translateI = shader.Uniform("translate");
233 	brightnessI = shader.Uniform("brightness");
234 }
235 
236 
237 
MakeStars(int stars,int width)238 void StarField::MakeStars(int stars, int width)
239 {
240 	// We can only work with power-of-two widths above 256.
241 	if(width < TILE_SIZE || (width & (width - 1)))
242 		return;
243 
244 	widthMod = width - 1;
245 
246 	tileCols = (width / TILE_SIZE);
247 	tileIndex.clear();
248 	tileIndex.resize(static_cast<size_t>(tileCols) * tileCols, 0);
249 
250 	vector<int> off;
251 	static const int MAX_OFF = 50;
252 	static const int MAX_D = MAX_OFF * MAX_OFF;
253 	static const int MIN_D = MAX_D / 4;
254 	off.reserve(MAX_OFF * MAX_OFF * 5);
255 	for(int x = -MAX_OFF; x <= MAX_OFF; ++x)
256 		for(int y = -MAX_OFF; y <= MAX_OFF; ++y)
257 		{
258 			int d = x * x + y * y;
259 			if(d < MIN_D || d > MAX_D)
260 				continue;
261 
262 			off.push_back(x);
263 			off.push_back(y);
264 		}
265 
266 	// Generate random points in a temporary vector.
267 	// Keep track of how many fall into each tile, for sorting out later.
268 	vector<int> temp;
269 	temp.reserve(2 * stars);
270 
271 	int x = Random::Int(width);
272 	int y = Random::Int(width);
273 	for(int i = 0; i < stars; ++i)
274 	{
275 		for(int j = 0; j < 10; ++j)
276 		{
277 			int index = Random::Int(static_cast<uint32_t>(off.size())) & ~1;
278 			x += off[index];
279 			y += off[index + 1];
280 			x &= widthMod;
281 			y &= widthMod;
282 		}
283 		temp.push_back(x);
284 		temp.push_back(y);
285 		int index = (x / TILE_SIZE) + (y / TILE_SIZE) * tileCols;
286 		++tileIndex[index];
287 	}
288 
289 	// Accumulate item counts so that tileIndex[i] is the index in the array of
290 	// the first star that falls within tile i, and tileIndex.back() == stars.
291 	tileIndex.insert(tileIndex.begin(), 0);
292 	tileIndex.pop_back();
293 	partial_sum(tileIndex.begin(), tileIndex.end(), tileIndex.begin());
294 
295 	// Each star consists of five vertices, each with four data elements.
296 	vector<GLfloat> data(6 * 4 * stars, 0.f);
297 	for(auto it = temp.begin(); it != temp.end(); )
298 	{
299 		// Figure out what tile this star is in.
300 		int x = *it++;
301 		int y = *it++;
302 		int index = (x / TILE_SIZE) + (y / TILE_SIZE) * tileCols;
303 
304 		// Randomize its sub-pixel position and its size / brightness.
305 		int random = Random::Int(4096);
306 		float fx = (x & (TILE_SIZE - 1)) + (random & 15) * 0.0625f;
307 		float fy = (y & (TILE_SIZE - 1)) + (random >> 8) * 0.0625f;
308 		float size = (((random >> 4) & 15) + 20) * 0.0625f;
309 
310 		// Fill in the data array.
311 		auto dataIt = data.begin() + 6 * 4 * tileIndex[index]++;
312 		const float CORNER[6] = {
313 			static_cast<float>(0. * PI),
314 			static_cast<float>(.5 * PI),
315 			static_cast<float>(1.5 * PI),
316 			static_cast<float>(.5 * PI),
317 			static_cast<float>(1.5 * PI),
318 			static_cast<float>(1. * PI)
319 		};
320 		for(float corner : CORNER)
321 		{
322 			*dataIt++ = fx;
323 			*dataIt++ = fy;
324 			*dataIt++ = size;
325 			*dataIt++ = corner;
326 		}
327 	}
328 	// Adjust the tile indices so that tileIndex[i] is the start of tile i.
329 	tileIndex.insert(tileIndex.begin(), 0);
330 
331 	glBufferData(GL_ARRAY_BUFFER, sizeof(data.front()) * data.size(), data.data(), GL_STATIC_DRAW);
332 
333 	// Connect the xy to the "vert" attribute of the vertex shader.
334 	constexpr auto stride = 4 * sizeof(GLfloat);
335 	glEnableVertexAttribArray(offsetI);
336 	glVertexAttribPointer(offsetI, 2, GL_FLOAT, GL_FALSE,
337 		stride, nullptr);
338 
339 	glEnableVertexAttribArray(sizeI);
340 	glVertexAttribPointer(sizeI, 1, GL_FLOAT, GL_FALSE,
341 		stride, reinterpret_cast<const GLvoid *>(2 * sizeof(GLfloat)));
342 
343 	glEnableVertexAttribArray(cornerI);
344 	glVertexAttribPointer(cornerI, 1, GL_FLOAT, GL_FALSE,
345 		stride, reinterpret_cast<const GLvoid *>(3 * sizeof(GLfloat)));
346 
347 	// unbind the VBO and VAO
348 	glBindBuffer(GL_ARRAY_BUFFER, 0);
349 	glBindVertexArray(0);
350 }
351