diff options
author | James Cowgill <jcowgill@debian.org> | 2018-05-15 13:58:15 +0100 |
---|---|---|
committer | James Cowgill <jcowgill@debian.org> | 2018-05-15 13:58:15 +0100 |
commit | b175eed3ca950204c9b52fdcebc897f337b7bf19 (patch) | |
tree | 0f50d921750c957f11bda18abc83f9228d010902 /examples/island/Island.cpp | |
parent | 31394fcd65a10f2d2c51c3d27afff5367bded901 (diff) |
New upstream version 2.5.0+dfsg
Diffstat (limited to 'examples/island/Island.cpp')
-rw-r--r-- | examples/island/Island.cpp | 590 |
1 files changed, 590 insertions, 0 deletions
diff --git a/examples/island/Island.cpp b/examples/island/Island.cpp new file mode 100644 index 0000000..50a8f1e --- /dev/null +++ b/examples/island/Island.cpp @@ -0,0 +1,590 @@ + +//////////////////////////////////////////////////////////// +// Headers +//////////////////////////////////////////////////////////// +#define STB_PERLIN_IMPLEMENTATION +#include "stb_perlin.h" +#include <SFML/Graphics.hpp> +#include <vector> +#include <deque> +#include <sstream> +#include <algorithm> +#include <cstring> +#include <cmath> + + +namespace +{ + // Width and height of the application window + const unsigned int windowWidth = 800; + const unsigned int windowHeight = 600; + + // Resolution of the generated terrain + const unsigned int resolutionX = 800; + const unsigned int resolutionY = 600; + + // Thread pool parameters + const unsigned int threadCount = 4; + const unsigned int blockCount = 32; + + struct WorkItem + { + sf::Vertex* targetBuffer; + unsigned int index; + }; + + std::deque<WorkItem> workQueue; + std::vector<sf::Thread*> threads; + int pendingWorkCount = 0; + bool workPending = true; + bool bufferUploadPending = false; + sf::Mutex workQueueMutex; + + struct Setting + { + const char* name; + float* value; + }; + + // Terrain noise parameters + const int perlinOctaves = 3; + + float perlinFrequency = 7.0f; + float perlinFrequencyBase = 4.0f; + + // Terrain generation parameters + float heightBase = 0.0f; + float edgeFactor = 0.9f; + float edgeDropoffExponent = 1.5f; + + float snowcapHeight = 0.6f; + + // Terrain lighting parameters + float heightFactor = windowHeight / 2.0f; + float heightFlatten = 3.0f; + float lightFactor = 0.7f; +} + + +// Forward declarations of the functions we define further down +void threadFunction(); +void generateTerrain(sf::Vertex* vertexBuffer); + + +//////////////////////////////////////////////////////////// +/// Entry point of application +/// +/// \return Application exit code +/// +//////////////////////////////////////////////////////////// +int main() +{ + // Create the window of the application + sf::RenderWindow window(sf::VideoMode(windowWidth, windowHeight), "SFML Island", + sf::Style::Titlebar | sf::Style::Close); + window.setVerticalSyncEnabled(true); + + sf::Font font; + if (!font.loadFromFile("resources/sansation.ttf")) + return EXIT_FAILURE; + + // Create all of our graphics resources + sf::Text hudText; + sf::Text statusText; + sf::Shader terrainShader; + sf::RenderStates terrainStates(&terrainShader); + sf::VertexBuffer terrain(sf::Triangles, sf::VertexBuffer::Static); + + // Set up our text drawables + statusText.setFont(font); + statusText.setCharacterSize(28); + statusText.setFillColor(sf::Color::White); + statusText.setOutlineColor(sf::Color::Black); + statusText.setOutlineThickness(2.0f); + + hudText.setFont(font); + hudText.setCharacterSize(14); + hudText.setFillColor(sf::Color::White); + hudText.setOutlineColor(sf::Color::Black); + hudText.setOutlineThickness(2.0f); + hudText.setPosition(5.0f, 5.0f); + + // Staging buffer for our terrain data that we will upload to our VertexBuffer + std::vector<sf::Vertex> terrainStagingBuffer; + + // Check whether the prerequisites are suppprted + bool prerequisitesSupported = sf::VertexBuffer::isAvailable() && sf::Shader::isAvailable(); + + // Set up our graphics resources and set the status text accordingly + if (!prerequisitesSupported) + { + statusText.setString("Shaders and/or Vertex Buffers Unsupported"); + } + else if (!terrainShader.loadFromFile("resources/terrain.vert", "resources/terrain.frag")) + { + prerequisitesSupported = false; + + statusText.setString("Failed to load shader program"); + } + else + { + // Start up our thread pool + for (unsigned int i = 0; i < threadCount; i++) + { + threads.push_back(new sf::Thread(threadFunction)); + threads.back()->launch(); + } + + // Create our VertexBuffer with enough space to hold all the terrain geometry + terrain.create(resolutionX * resolutionY * 6); + + // Resize the staging buffer to be able to hold all the terrain geometry + terrainStagingBuffer.resize(resolutionX * resolutionY * 6); + + // Generate the initial terrain + generateTerrain(&terrainStagingBuffer[0]); + + statusText.setString("Generating Terrain..."); + } + + // Center the status text + statusText.setPosition((windowWidth - statusText.getLocalBounds().width) / 2.f, (windowHeight - statusText.getLocalBounds().height) / 2.f); + + // Set up an array of pointers to our settings for arrow navigation + Setting settings[] = + { + {"perlinFrequency", &perlinFrequency}, + {"perlinFrequencyBase", &perlinFrequencyBase}, + {"heightBase", &heightBase}, + {"edgeFactor", &edgeFactor}, + {"edgeDropoffExponent", &edgeDropoffExponent}, + {"snowcapHeight", &snowcapHeight}, + {"heightFactor", &heightFactor}, + {"heightFlatten", &heightFlatten}, + {"lightFactor", &lightFactor} + }; + + const int settingCount = 9; + int currentSetting = 0; + + std::ostringstream osstr; + sf::Clock clock; + + while (window.isOpen()) + { + // Handle events + sf::Event event; + while (window.pollEvent(event)) + { + // Window closed or escape key pressed: exit + if ((event.type == sf::Event::Closed) || + ((event.type == sf::Event::KeyPressed) && (event.key.code == sf::Keyboard::Escape))) + { + window.close(); + break; + } + + // Arrow key pressed: + if (prerequisitesSupported && (event.type == sf::Event::KeyPressed)) + { + switch (event.key.code) + { + case sf::Keyboard::Return: generateTerrain(&terrainStagingBuffer[0]); break; + case sf::Keyboard::Down: currentSetting = (currentSetting + 1) % settingCount; break; + case sf::Keyboard::Up: currentSetting = (currentSetting + settingCount - 1) % settingCount; break; + case sf::Keyboard::Left: *(settings[currentSetting].value) -= 0.1f; break; + case sf::Keyboard::Right: *(settings[currentSetting].value) += 0.1f; break; + default: break; + } + } + } + + // Clear, draw graphics objects and display + window.clear(); + + window.draw(statusText); + + if (prerequisitesSupported) + { + { + sf::Lock lock(workQueueMutex); + + // Don't bother updating/drawing the VertexBuffer while terrain is being regenerated + if (!pendingWorkCount) + { + // If there is new data pending to be uploaded to the VertexBuffer, do it now + if (bufferUploadPending) + { + terrain.update(&terrainStagingBuffer[0]); + bufferUploadPending = false; + } + + terrainShader.setUniform("lightFactor", lightFactor); + window.draw(terrain, terrainStates); + } + } + + // Update and draw the HUD text + osstr.str(""); + osstr << "Frame: " << clock.restart().asMilliseconds() << "ms\n" + << "perlinOctaves: " << perlinOctaves << "\n\n" + << "Use the arrow keys to change the values.\nUse the return key to regenerate the terrain.\n\n"; + + for (int i = 0; i < settingCount; ++i) + osstr << ((i == currentSetting) ? ">> " : " ") << settings[i].name << ": " << *(settings[i].value) << "\n"; + + hudText.setString(osstr.str()); + + window.draw(hudText); + } + + // Display things on screen + window.display(); + } + + // Shut down our thread pool + { + sf::Lock lock(workQueueMutex); + workPending = false; + } + + while (!threads.empty()) + { + threads.back()->wait(); + delete threads.back(); + threads.pop_back(); + } + + return EXIT_SUCCESS; +} + + +//////////////////////////////////////////////////////////// +/// Get the terrain elevation at the given coordinates. +/// +//////////////////////////////////////////////////////////// +float getElevation(float x, float y) +{ + x = x / resolutionX - 0.5f; + y = y / resolutionY - 0.5f; + + float elevation = 0.0f; + + for (int i = 0; i < perlinOctaves; i++) + { + elevation += stb_perlin_noise3( + x * perlinFrequency * std::pow(perlinFrequencyBase, i), + y * perlinFrequency * std::pow(perlinFrequencyBase, i), + 0, 0, 0, 0 + ) * std::pow(perlinFrequencyBase, -i); + } + + elevation = (elevation + 1.f) / 2.f; + + float distance = 2.0f * std::sqrt(x * x + y * y); + elevation = (elevation + heightBase) * (1.0f - edgeFactor * std::pow(distance, edgeDropoffExponent)); + elevation = std::min(std::max(elevation, 0.0f), 1.0f); + + return elevation; +} + + +//////////////////////////////////////////////////////////// +/// Get the terrain moisture at the given coordinates. +/// +//////////////////////////////////////////////////////////// +float getMoisture(float x, float y) +{ + x = x / resolutionX - 0.5f; + y = y / resolutionY - 0.5f; + + float moisture = stb_perlin_noise3( + x * 4.f + 0.5f, + y * 4.f + 0.5f, + 0, 0, 0, 0 + ); + + return (moisture + 1.f) / 2.f; +} + + +//////////////////////////////////////////////////////////// +/// Get the lowlands terrain color for the given moisture. +/// +//////////////////////////////////////////////////////////// +sf::Color getLowlandsTerrainColor(float moisture) +{ + sf::Color color = + moisture < 0.27f ? sf::Color(240, 240, 180) : + 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) : + moisture < 0.4f ? sf::Color(0, 200, 0) : + moisture < 0.48f ? sf::Color(0, 200 - 40 * (moisture - 0.4f) / 0.08f, 0) : + moisture < 0.6f ? sf::Color(0, 160, 0) : + moisture < 0.7f ? sf::Color(34 * (moisture - 0.6f) / 0.1f, 160 - 60 * (moisture - 0.6f) / 0.1f, 34 * (moisture - 0.6f) / 0.1f) : + sf::Color(34, 100, 34); + + return color; +} + + +//////////////////////////////////////////////////////////// +/// Get the highlands terrain color for the given elevation +/// and moisture. +/// +//////////////////////////////////////////////////////////// +sf::Color getHighlandsTerrainColor(float elevation, float moisture) +{ + sf::Color lowlandsColor = getLowlandsTerrainColor(moisture); + + sf::Color color = + moisture < 0.6f ? sf::Color(112, 128, 144) : + sf::Color(112 + 110 * (moisture - 0.6f) / 0.4f, 128 + 56 * (moisture - 0.6f) / 0.4f, 144 - 9 * (moisture - 0.6f) / 0.4f); + + float factor = std::min((elevation - 0.4f) / 0.1f, 1.f); + + color.r = lowlandsColor.r * (1.f - factor) + color.r * factor; + color.g = lowlandsColor.g * (1.f - factor) + color.g * factor; + color.b = lowlandsColor.b * (1.f - factor) + color.b * factor; + + return color; +} + + +//////////////////////////////////////////////////////////// +/// Get the snowcap terrain color for the given elevation +/// and moisture. +/// +//////////////////////////////////////////////////////////// +sf::Color getSnowcapTerrainColor(float elevation, float moisture) +{ + sf::Color highlandsColor = getHighlandsTerrainColor(elevation, moisture); + + sf::Color color = sf::Color::White; + + float factor = std::min((elevation - snowcapHeight) / 0.05f, 1.f); + + color.r = highlandsColor.r * (1.f - factor) + color.r * factor; + color.g = highlandsColor.g * (1.f - factor) + color.g * factor; + color.b = highlandsColor.b * (1.f - factor) + color.b * factor; + + return color; +} + + +//////////////////////////////////////////////////////////// +/// Get the terrain color for the given elevation and +/// moisture. +/// +//////////////////////////////////////////////////////////// +sf::Color getTerrainColor(float elevation, float moisture) +{ + sf::Color color = + elevation < 0.11f ? sf::Color(0, 0, elevation / 0.11f * 74.f + 181.0f) : + 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) : + 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) : + elevation < 0.17f ? sf::Color(240, 230, 140) : + elevation < 0.4f ? getLowlandsTerrainColor(moisture) : + elevation < snowcapHeight ? getHighlandsTerrainColor(elevation, moisture) : + getSnowcapTerrainColor(elevation, moisture); + + return color; +} + + +//////////////////////////////////////////////////////////// +/// Compute a compressed representation of the surface +/// normal based on the given coordinates, and the elevation +/// of the 4 adjacent neighbours. +/// +//////////////////////////////////////////////////////////// +sf::Vector2f computeNormal(int x, int y, float left, float right, float bottom, float top) +{ + sf::Vector3f deltaX(1, 0, (std::pow(right, heightFlatten) - std::pow(left, heightFlatten)) * heightFactor); + sf::Vector3f deltaY(0, 1, (std::pow(top, heightFlatten) - std::pow(bottom, heightFlatten)) * heightFactor); + + sf::Vector3f crossProduct( + deltaX.y * deltaY.z - deltaX.z * deltaY.y, + deltaX.z * deltaY.x - deltaX.x * deltaY.z, + deltaX.x * deltaY.y - deltaX.y * deltaY.x + ); + + // Scale cross product to make z component 1.0f so we can drop it + crossProduct /= crossProduct.z; + + // Return "compressed" normal + return sf::Vector2f(crossProduct.x, crossProduct.y); +} + + +//////////////////////////////////////////////////////////// +/// Process a terrain generation work item. Use the vector +/// of vertices as scratch memory and upload the data to +/// the vertex buffer when done. +/// +//////////////////////////////////////////////////////////// +void processWorkItem(std::vector<sf::Vertex>& vertices, const WorkItem& workItem) +{ + unsigned int rowBlockSize = (resolutionY / blockCount) + 1; + unsigned int rowStart = rowBlockSize * workItem.index; + + if (rowStart >= resolutionY) + return; + + unsigned int rowEnd = std::min(rowStart + rowBlockSize, resolutionY); + unsigned int rowCount = rowEnd - rowStart; + + const float scalingFactorX = static_cast<float>(windowWidth) / static_cast<float>(resolutionX); + const float scalingFactorY = static_cast<float>(windowHeight) / static_cast<float>(resolutionY); + + for (unsigned int y = rowStart; y < rowEnd; y++) + { + for (int x = 0; x < resolutionX; x++) + { + int arrayIndexBase = ((y - rowStart) * resolutionX + x) * 6; + + // Top left corner (first triangle) + if (x > 0) + { + vertices[arrayIndexBase + 0] = vertices[arrayIndexBase - 6 + 5]; + } + else if (y > rowStart) + { + vertices[arrayIndexBase + 0] = vertices[arrayIndexBase - resolutionX * 6 + 1]; + } + else + { + vertices[arrayIndexBase + 0].position = sf::Vector2f(x * scalingFactorX, y * scalingFactorY); + vertices[arrayIndexBase + 0].color = getTerrainColor(getElevation(x, y), getMoisture(x, y)); + vertices[arrayIndexBase + 0].texCoords = computeNormal(x, y, getElevation(x - 1, y), getElevation(x + 1, y), getElevation(x, y + 1), getElevation(x, y - 1)); + } + + // Bottom left corner (first triangle) + if (x > 0) + { + vertices[arrayIndexBase + 1] = vertices[arrayIndexBase - 6 + 2]; + } + else + { + vertices[arrayIndexBase + 1].position = sf::Vector2f(x * scalingFactorX, (y + 1) * scalingFactorY); + vertices[arrayIndexBase + 1].color = getTerrainColor(getElevation(x, y + 1), getMoisture(x, y + 1)); + 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)); + } + + // Bottom right corner (first triangle) + vertices[arrayIndexBase + 2].position = sf::Vector2f((x + 1) * scalingFactorX, (y + 1) * scalingFactorY); + vertices[arrayIndexBase + 2].color = getTerrainColor(getElevation(x + 1, y + 1), getMoisture(x + 1, y + 1)); + 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)); + + // Top left corner (second triangle) + vertices[arrayIndexBase + 3] = vertices[arrayIndexBase + 0]; + + // Bottom right corner (second triangle) + vertices[arrayIndexBase + 4] = vertices[arrayIndexBase + 2]; + + // Top right corner (second triangle) + if (y > rowStart) + { + vertices[arrayIndexBase + 5] = vertices[arrayIndexBase - resolutionX * 6 + 2]; + } + else + { + vertices[arrayIndexBase + 5].position = sf::Vector2f((x + 1) * scalingFactorX, y * scalingFactorY); + vertices[arrayIndexBase + 5].color = getTerrainColor(getElevation(x + 1, y), getMoisture(x + 1, y)); + 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)); + } + } + } + + // Copy the resulting geometry from our thread-local buffer into the target buffer + std::memcpy(workItem.targetBuffer + (resolutionX * rowStart * 6), &vertices[0], sizeof(sf::Vertex) * resolutionX * rowCount * 6); +} + + +//////////////////////////////////////////////////////////// +/// Worker thread entry point. We use a thread pool to avoid +/// the heavy cost of constantly recreating and starting +/// new threads whenever we need to regenerate the terrain. +/// +//////////////////////////////////////////////////////////// +void threadFunction() +{ + unsigned int rowBlockSize = (resolutionY / blockCount) + 1; + + std::vector<sf::Vertex> vertices(resolutionX * rowBlockSize * 6); + + WorkItem workItem = {0, 0}; + + // Loop until the application exits + for (;;) + { + workItem.targetBuffer = 0; + + // Check if there are new work items in the queue + { + sf::Lock lock(workQueueMutex); + + if (!workPending) + return; + + if (!workQueue.empty()) + { + workItem = workQueue.front(); + workQueue.pop_front(); + } + } + + // If we didn't receive a new work item, keep looping + if (!workItem.targetBuffer) + { + sf::sleep(sf::milliseconds(10)); + + continue; + } + + processWorkItem(vertices, workItem); + + { + sf::Lock lock(workQueueMutex); + + --pendingWorkCount; + } + } +} + + +//////////////////////////////////////////////////////////// +/// Terrain generation entry point. This queues up the +/// generation work items which the worker threads dequeue +/// and process. +/// +//////////////////////////////////////////////////////////// +void generateTerrain(sf::Vertex* buffer) +{ + bufferUploadPending = true; + + // Make sure the work queue is empty before queuing new work + for (;;) + { + { + sf::Lock lock(workQueueMutex); + + if (workQueue.empty()) + break; + } + + sf::sleep(sf::milliseconds(10)); + } + + // Queue all the new work items + { + sf::Lock lock(workQueueMutex); + + for (unsigned int i = 0; i < blockCount; i++) + { + WorkItem workItem = {buffer, i}; + workQueue.push_back(workItem); + } + + pendingWorkCount = blockCount; + } +} |