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