1
2////////////////////////////////////////////////////////////
3// Headers
4////////////////////////////////////////////////////////////
5#define STB_PERLIN_IMPLEMENTATION
6#include "stb_perlin.h"
7#include <SFML/Graphics.hpp>
8#include <vector>
9#include <deque>
10#include <sstream>
11#include <algorithm>
12#include <cstring>
13#include <cmath>
14
15
16namespace
17{
18 // Width and height of the application window
19 const unsigned int windowWidth = 800;
20 const unsigned int windowHeight = 600;
21
22 // Resolution of the generated terrain
23 const unsigned int resolutionX = 800;
24 const unsigned int resolutionY = 600;
25
26 // Thread pool parameters
27 const unsigned int threadCount = 4;
28 const unsigned int blockCount = 32;
29
30 struct WorkItem
31 {
32 sf::Vertex* targetBuffer;
33 unsigned int index;
34 };
35
36 std::deque<WorkItem> workQueue;
37 std::vector<sf::Thread*> threads;
38 int pendingWorkCount = 0;
39 bool workPending = true;
40 bool bufferUploadPending = false;
41 sf::Mutex workQueueMutex;
42
43 struct Setting
44 {
45 const char* name;
46 float* value;
47 };
48
49 // Terrain noise parameters
50 const int perlinOctaves = 3;
51
52 float perlinFrequency = 7.0f;
53 float perlinFrequencyBase = 4.0f;
54
55 // Terrain generation parameters
56 float heightBase = 0.0f;
57 float edgeFactor = 0.9f;
58 float edgeDropoffExponent = 1.5f;
59
60 float snowcapHeight = 0.6f;
61
62 // Terrain lighting parameters
63 float heightFactor = windowHeight / 2.0f;
64 float heightFlatten = 3.0f;
65 float lightFactor = 0.7f;
66}
67
68
69// Forward declarations of the functions we define further down
70void threadFunction();
71void generateTerrain(sf::Vertex* vertexBuffer);
72
73
74////////////////////////////////////////////////////////////
75/// Entry point of application
76///
77/// \return Application exit code
78///
79////////////////////////////////////////////////////////////
80int main()
81{
82 // Create the window of the application
83 sf::RenderWindow window(sf::VideoMode(windowWidth, windowHeight), "SFML Island",
84 sf::Style::Titlebar | sf::Style::Close);
85 window.setVerticalSyncEnabled(true);
86
87 sf::Font font;
88 if (!font.loadFromFile("resources/sansation.ttf"))
89 return EXIT_FAILURE;
90
91 // Create all of our graphics resources
92 sf::Text hudText;
93 sf::Text statusText;
94 sf::Shader terrainShader;
95 sf::RenderStates terrainStates(&terrainShader);
96 sf::VertexBuffer terrain(sf::Triangles, sf::VertexBuffer::Static);
97
98 // Set up our text drawables
99 statusText.setFont(font);
100 statusText.setCharacterSize(28);
101 statusText.setFillColor(sf::Color::White);
102 statusText.setOutlineColor(sf::Color::Black);
103 statusText.setOutlineThickness(2.0f);
104
105 hudText.setFont(font);
106 hudText.setCharacterSize(14);
107 hudText.setFillColor(sf::Color::White);
108 hudText.setOutlineColor(sf::Color::Black);
109 hudText.setOutlineThickness(2.0f);
110 hudText.setPosition(5.0f, 5.0f);
111
112 // Staging buffer for our terrain data that we will upload to our VertexBuffer
113 std::vector<sf::Vertex> terrainStagingBuffer;
114
115 // Check whether the prerequisites are suppprted
116 bool prerequisitesSupported = sf::VertexBuffer::isAvailable() && sf::Shader::isAvailable();
117
118 // Set up our graphics resources and set the status text accordingly
119 if (!prerequisitesSupported)
120 {
121 statusText.setString("Shaders and/or Vertex Buffers Unsupported");
122 }
123 else if (!terrainShader.loadFromFile("resources/terrain.vert", "resources/terrain.frag"))
124 {
125 prerequisitesSupported = false;
126
127 statusText.setString("Failed to load shader program");
128 }
129 else
130 {
131 // Start up our thread pool
132 for (unsigned int i = 0; i < threadCount; i++)
133 {
134 threads.push_back(new sf::Thread(threadFunction));
135 threads.back()->launch();
136 }
137
138 // Create our VertexBuffer with enough space to hold all the terrain geometry
139 terrain.create(resolutionX * resolutionY * 6);
140
141 // Resize the staging buffer to be able to hold all the terrain geometry
142 terrainStagingBuffer.resize(resolutionX * resolutionY * 6);
143
144 // Generate the initial terrain
145 generateTerrain(&terrainStagingBuffer[0]);
146
147 statusText.setString("Generating Terrain...");
148 }
149
150 // Center the status text
151 statusText.setPosition((windowWidth - statusText.getLocalBounds().width) / 2.f, (windowHeight - statusText.getLocalBounds().height) / 2.f);
152
153 // Set up an array of pointers to our settings for arrow navigation
154 Setting settings[] =
155 {
156 {"perlinFrequency", &perlinFrequency},
157 {"perlinFrequencyBase", &perlinFrequencyBase},
158 {"heightBase", &heightBase},
159 {"edgeFactor", &edgeFactor},
160 {"edgeDropoffExponent", &edgeDropoffExponent},
161 {"snowcapHeight", &snowcapHeight},
162 {"heightFactor", &heightFactor},
163 {"heightFlatten", &heightFlatten},
164 {"lightFactor", &lightFactor}
165 };
166
167 const int settingCount = 9;
168 int currentSetting = 0;
169
170 std::ostringstream osstr;
171 sf::Clock clock;
172
173 while (window.isOpen())
174 {
175 // Handle events
176 sf::Event event;
177 while (window.pollEvent(event))
178 {
179 // Window closed or escape key pressed: exit
180 if ((event.type == sf::Event::Closed) ||
181 ((event.type == sf::Event::KeyPressed) && (event.key.code == sf::Keyboard::Escape)))
182 {
183 window.close();
184 break;
185 }
186
187 // Arrow key pressed:
188 if (prerequisitesSupported && (event.type == sf::Event::KeyPressed))
189 {
190 switch (event.key.code)
191 {
192 case sf::Keyboard::Return: generateTerrain(&terrainStagingBuffer[0]); break;
193 case sf::Keyboard::Down: currentSetting = (currentSetting + 1) % settingCount; break;
194 case sf::Keyboard::Up: currentSetting = (currentSetting + settingCount - 1) % settingCount; break;
195 case sf::Keyboard::Left: *(settings[currentSetting].value) -= 0.1f; break;
196 case sf::Keyboard::Right: *(settings[currentSetting].value) += 0.1f; break;
197 default: break;
198 }
199 }
200 }
201
202 // Clear, draw graphics objects and display
203 window.clear();
204
205 window.draw(statusText);
206
207 if (prerequisitesSupported)
208 {
209 {
210 sf::Lock lock(workQueueMutex);
211
212 // Don't bother updating/drawing the VertexBuffer while terrain is being regenerated
213 if (!pendingWorkCount)
214 {
215 // If there is new data pending to be uploaded to the VertexBuffer, do it now
216 if (bufferUploadPending)
217 {
218 terrain.update(&terrainStagingBuffer[0]);
219 bufferUploadPending = false;
220 }
221
222 terrainShader.setUniform("lightFactor", lightFactor);
223 window.draw(terrain, terrainStates);
224 }
225 }
226
227 // Update and draw the HUD text
228 osstr.str("");
229 osstr << "Frame: " << clock.restart().asMilliseconds() << "ms\n"
230 << "perlinOctaves: " << perlinOctaves << "\n\n"
231 << "Use the arrow keys to change the values.\nUse the return key to regenerate the terrain.\n\n";
232
233 for (int i = 0; i < settingCount; ++i)
234 osstr << ((i == currentSetting) ? ">> " : " ") << settings[i].name << ": " << *(settings[i].value) << "\n";
235
236 hudText.setString(osstr.str());
237
238 window.draw(hudText);
239 }
240
241 // Display things on screen
242 window.display();
243 }
244
245 // Shut down our thread pool
246 {
247 sf::Lock lock(workQueueMutex);
248 workPending = false;
249 }
250
251 while (!threads.empty())
252 {
253 threads.back()->wait();
254 delete threads.back();
255 threads.pop_back();
256 }
257
258 return EXIT_SUCCESS;
259}
260
261
262////////////////////////////////////////////////////////////
263/// Get the terrain elevation at the given coordinates.
264///
265////////////////////////////////////////////////////////////
266float getElevation(float x, float y)
267{
268 x = x / resolutionX - 0.5f;
269 y = y / resolutionY - 0.5f;
270
271 float elevation = 0.0f;
272
273 for (int i = 0; i < perlinOctaves; i++)
274 {
275 elevation += stb_perlin_noise3(
276 x * perlinFrequency * std::pow(perlinFrequencyBase, i),
277 y * perlinFrequency * std::pow(perlinFrequencyBase, i),
278 0, 0, 0, 0
279 ) * std::pow(perlinFrequencyBase, -i);
280 }
281
282 elevation = (elevation + 1.f) / 2.f;
283
284 float distance = 2.0f * std::sqrt(x * x + y * y);
285 elevation = (elevation + heightBase) * (1.0f - edgeFactor * std::pow(distance, edgeDropoffExponent));
286 elevation = std::min(std::max(elevation, 0.0f), 1.0f);
287
288 return elevation;
289}
290
291
292////////////////////////////////////////////////////////////
293/// Get the terrain moisture at the given coordinates.
294///
295////////////////////////////////////////////////////////////
296float getMoisture(float x, float y)
297{
298 x = x / resolutionX - 0.5f;
299 y = y / resolutionY - 0.5f;
300
301 float moisture = stb_perlin_noise3(
302 x * 4.f + 0.5f,
303 y * 4.f + 0.5f,
304 0, 0, 0, 0
305 );
306
307 return (moisture + 1.f) / 2.f;
308}
309
310
311////////////////////////////////////////////////////////////
312/// Get the lowlands terrain color for the given moisture.
313///
314////////////////////////////////////////////////////////////
315sf::Color getLowlandsTerrainColor(float moisture)
316{
317 sf::Color color =
318 moisture < 0.27f ? sf::Color(240, 240, 180) :
319 moisture < 0.3f ? sf::Color(240 - 240 * (moisture - 0.27f) / 0.03f, 240 - 40 * (moisture - 0.27f) / 0.03f, 180 - 180 * (moisture - 0.27f) / 0.03f) :
320 moisture < 0.4f ? sf::Color(0, 200, 0) :
321 moisture < 0.48f ? sf::Color(0, 200 - 40 * (moisture - 0.4f) / 0.08f, 0) :
322 moisture < 0.6f ? sf::Color(0, 160, 0) :
323 moisture < 0.7f ? sf::Color(34 * (moisture - 0.6f) / 0.1f, 160 - 60 * (moisture - 0.6f) / 0.1f, 34 * (moisture - 0.6f) / 0.1f) :
324 sf::Color(34, 100, 34);
325
326 return color;
327}
328
329
330////////////////////////////////////////////////////////////
331/// Get the highlands terrain color for the given elevation
332/// and moisture.
333///
334////////////////////////////////////////////////////////////
335sf::Color getHighlandsTerrainColor(float elevation, float moisture)
336{
337 sf::Color lowlandsColor = getLowlandsTerrainColor(moisture);
338
339 sf::Color color =
340 moisture < 0.6f ? sf::Color(112, 128, 144) :
341 sf::Color(112 + 110 * (moisture - 0.6f) / 0.4f, 128 + 56 * (moisture - 0.6f) / 0.4f, 144 - 9 * (moisture - 0.6f) / 0.4f);
342
343 float factor = std::min((elevation - 0.4f) / 0.1f, 1.f);
344
345 color.r = lowlandsColor.r * (1.f - factor) + color.r * factor;
346 color.g = lowlandsColor.g * (1.f - factor) + color.g * factor;
347 color.b = lowlandsColor.b * (1.f - factor) + color.b * factor;
348
349 return color;
350}
351
352
353////////////////////////////////////////////////////////////
354/// Get the snowcap terrain color for the given elevation
355/// and moisture.
356///
357////////////////////////////////////////////////////////////
358sf::Color getSnowcapTerrainColor(float elevation, float moisture)
359{
360 sf::Color highlandsColor = getHighlandsTerrainColor(elevation, moisture);
361
362 sf::Color color = sf::Color::White;
363
364 float factor = std::min((elevation - snowcapHeight) / 0.05f, 1.f);
365
366 color.r = highlandsColor.r * (1.f - factor) + color.r * factor;
367 color.g = highlandsColor.g * (1.f - factor) + color.g * factor;
368 color.b = highlandsColor.b * (1.f - factor) + color.b * factor;
369
370 return color;
371}
372
373
374////////////////////////////////////////////////////////////
375/// Get the terrain color for the given elevation and
376/// moisture.
377///
378////////////////////////////////////////////////////////////
379sf::Color getTerrainColor(float elevation, float moisture)
380{
381 sf::Color color =
382 elevation < 0.11f ? sf::Color(0, 0, elevation / 0.11f * 74.f + 181.0f) :
383 elevation < 0.14f ? sf::Color(std::pow((elevation - 0.11f) / 0.03f, 0.3f) * 48.f, std::pow((elevation - 0.11f) / 0.03f, 0.3f) * 48.f, 255) :
384 elevation < 0.16f ? sf::Color((elevation - 0.14f) * 128.f / 0.02f + 48.f, (elevation - 0.14f) * 128.f / 0.02f + 48.f, 127.0f + (0.16f - elevation) * 128.f / 0.02f) :
385 elevation < 0.17f ? sf::Color(240, 230, 140) :
386 elevation < 0.4f ? getLowlandsTerrainColor(moisture) :
387 elevation < snowcapHeight ? getHighlandsTerrainColor(elevation, moisture) :
388 getSnowcapTerrainColor(elevation, moisture);
389
390 return color;
391}
392
393
394////////////////////////////////////////////////////////////
395/// Compute a compressed representation of the surface
396/// normal based on the given coordinates, and the elevation
397/// of the 4 adjacent neighbours.
398///
399////////////////////////////////////////////////////////////
400sf::Vector2f computeNormal(int x, int y, float left, float right, float bottom, float top)
401{
402 sf::Vector3f deltaX(1, 0, (std::pow(right, heightFlatten) - std::pow(left, heightFlatten)) * heightFactor);
403 sf::Vector3f deltaY(0, 1, (std::pow(top, heightFlatten) - std::pow(bottom, heightFlatten)) * heightFactor);
404
405 sf::Vector3f crossProduct(
406 deltaX.y * deltaY.z - deltaX.z * deltaY.y,
407 deltaX.z * deltaY.x - deltaX.x * deltaY.z,
408 deltaX.x * deltaY.y - deltaX.y * deltaY.x
409 );
410
411 // Scale cross product to make z component 1.0f so we can drop it
412 crossProduct /= crossProduct.z;
413
414 // Return "compressed" normal
415 return sf::Vector2f(crossProduct.x, crossProduct.y);
416}
417
418
419////////////////////////////////////////////////////////////
420/// Process a terrain generation work item. Use the vector
421/// of vertices as scratch memory and upload the data to
422/// the vertex buffer when done.
423///
424////////////////////////////////////////////////////////////
425void processWorkItem(std::vector<sf::Vertex>& vertices, const WorkItem& workItem)
426{
427 unsigned int rowBlockSize = (resolutionY / blockCount) + 1;
428 unsigned int rowStart = rowBlockSize * workItem.index;
429
430 if (rowStart >= resolutionY)
431 return;
432
433 unsigned int rowEnd = std::min(rowStart + rowBlockSize, resolutionY);
434 unsigned int rowCount = rowEnd - rowStart;
435
436 const float scalingFactorX = static_cast<float>(windowWidth) / static_cast<float>(resolutionX);
437 const float scalingFactorY = static_cast<float>(windowHeight) / static_cast<float>(resolutionY);
438
439 for (unsigned int y = rowStart; y < rowEnd; y++)
440 {
441 for (int x = 0; x < resolutionX; x++)
442 {
443 int arrayIndexBase = ((y - rowStart) * resolutionX + x) * 6;
444
445 // Top left corner (first triangle)
446 if (x > 0)
447 {
448 vertices[arrayIndexBase + 0] = vertices[arrayIndexBase - 6 + 5];
449 }
450 else if (y > rowStart)
451 {
452 vertices[arrayIndexBase + 0] = vertices[arrayIndexBase - resolutionX * 6 + 1];
453 }
454 else
455 {
456 vertices[arrayIndexBase + 0].position = sf::Vector2f(x * scalingFactorX, y * scalingFactorY);
457 vertices[arrayIndexBase + 0].color = getTerrainColor(getElevation(x, y), getMoisture(x, y));
458 vertices[arrayIndexBase + 0].texCoords = computeNormal(x, y, getElevation(x - 1, y), getElevation(x + 1, y), getElevation(x, y + 1), getElevation(x, y - 1));
459 }
460
461 // Bottom left corner (first triangle)
462 if (x > 0)
463 {
464 vertices[arrayIndexBase + 1] = vertices[arrayIndexBase - 6 + 2];
465 }
466 else
467 {
468 vertices[arrayIndexBase + 1].position = sf::Vector2f(x * scalingFactorX, (y + 1) * scalingFactorY);
469 vertices[arrayIndexBase + 1].color = getTerrainColor(getElevation(x, y + 1), getMoisture(x, y + 1));
470 vertices[arrayIndexBase + 1].texCoords = computeNormal(x, y + 1, getElevation(x - 1, y + 1), getElevation(x + 1, y + 1), getElevation(x, y + 2), getElevation(x, y));
471 }
472
473 // Bottom right corner (first triangle)
474 vertices[arrayIndexBase + 2].position = sf::Vector2f((x + 1) * scalingFactorX, (y + 1) * scalingFactorY);
475 vertices[arrayIndexBase + 2].color = getTerrainColor(getElevation(x + 1, y + 1), getMoisture(x + 1, y + 1));
476 vertices[arrayIndexBase + 2].texCoords = computeNormal(x + 1, y + 1, getElevation(x, y + 1), getElevation(x + 2, y + 1), getElevation(x + 1, y + 2), getElevation(x + 1, y));
477
478 // Top left corner (second triangle)
479 vertices[arrayIndexBase + 3] = vertices[arrayIndexBase + 0];
480
481 // Bottom right corner (second triangle)
482 vertices[arrayIndexBase + 4] = vertices[arrayIndexBase + 2];
483
484 // Top right corner (second triangle)
485 if (y > rowStart)
486 {
487 vertices[arrayIndexBase + 5] = vertices[arrayIndexBase - resolutionX * 6 + 2];
488 }
489 else
490 {
491 vertices[arrayIndexBase + 5].position = sf::Vector2f((x + 1) * scalingFactorX, y * scalingFactorY);
492 vertices[arrayIndexBase + 5].color = getTerrainColor(getElevation(x + 1, y), getMoisture(x + 1, y));
493 vertices[arrayIndexBase + 5].texCoords = computeNormal(x + 1, y, getElevation(x, y), getElevation(x + 2, y), getElevation(x + 1, y + 1), getElevation(x + 1, y - 1));
494 }
495 }
496 }
497
498 // Copy the resulting geometry from our thread-local buffer into the target buffer
499 std::memcpy(workItem.targetBuffer + (resolutionX * rowStart * 6), &vertices[0], sizeof(sf::Vertex) * resolutionX * rowCount * 6);
500}
501
502
503////////////////////////////////////////////////////////////
504/// Worker thread entry point. We use a thread pool to avoid
505/// the heavy cost of constantly recreating and starting
506/// new threads whenever we need to regenerate the terrain.
507///
508////////////////////////////////////////////////////////////
509void threadFunction()
510{
511 unsigned int rowBlockSize = (resolutionY / blockCount) + 1;
512
513 std::vector<sf::Vertex> vertices(resolutionX * rowBlockSize * 6);
514
515 WorkItem workItem = {0, 0};
516
517 // Loop until the application exits
518 for (;;)
519 {
520 workItem.targetBuffer = 0;
521
522 // Check if there are new work items in the queue
523 {
524 sf::Lock lock(workQueueMutex);
525
526 if (!workPending)
527 return;
528
529 if (!workQueue.empty())
530 {
531 workItem = workQueue.front();
532 workQueue.pop_front();
533 }
534 }
535
536 // If we didn't receive a new work item, keep looping
537 if (!workItem.targetBuffer)
538 {
539 sf::sleep(sf::milliseconds(10));
540
541 continue;
542 }
543
544 processWorkItem(vertices, workItem);
545
546 {
547 sf::Lock lock(workQueueMutex);
548
549 --pendingWorkCount;
550 }
551 }
552}
553
554
555////////////////////////////////////////////////////////////
556/// Terrain generation entry point. This queues up the
557/// generation work items which the worker threads dequeue
558/// and process.
559///
560////////////////////////////////////////////////////////////
561void generateTerrain(sf::Vertex* buffer)
562{
563 bufferUploadPending = true;
564
565 // Make sure the work queue is empty before queuing new work
566 for (;;)
567 {
568 {
569 sf::Lock lock(workQueueMutex);
570
571 if (workQueue.empty())
572 break;
573 }
574
575 sf::sleep(sf::milliseconds(10));
576 }
577
578 // Queue all the new work items
579 {
580 sf::Lock lock(workQueueMutex);
581
582 for (unsigned int i = 0; i < blockCount; i++)
583 {
584 WorkItem workItem = {buffer, i};
585 workQueue.push_back(workItem);
586 }
587
588 pendingWorkCount = blockCount;
589 }
590}
591