#include "stdafx.h" #include "TerrainGenerator.h" #include "Utility/stb_image.h" using namespace wi::ecs; using namespace wi::scene; enum PRESET { PRESET_HILLS, PRESET_ISLANDS, PRESET_MOUNTAINS, PRESET_ARCTIC, }; void TerrainGenerator::init() { terrainEntity = CreateEntity(); indices.clear(); lods.clear(); lods.resize(max_lod); for (int lod = 0; lod < max_lod; ++lod) { lods[lod].indexOffset = (uint32_t)indices.size(); if (lod == 0) { for (int x = 0; x < width - 1; x++) { for (int z = 0; z < width - 1; z++) { int lowerLeft = x + z * width; int lowerRight = (x + 1) + z * width; int topLeft = x + (z + 1) * width; int topRight = (x + 1) + (z + 1) * width; indices.push_back(topLeft); indices.push_back(lowerLeft); indices.push_back(lowerRight); indices.push_back(topLeft); indices.push_back(lowerRight); indices.push_back(topRight); } } } else { const int step = 1 << lod; // inner grid: for (int x = 1; x < width - 2; x += step) { for (int z = 1; z < width - 2; z += step) { int lowerLeft = x + z * width; int lowerRight = (x + step) + z * width; int topLeft = x + (z + step) * width; int topRight = (x + step) + (z + step) * width; indices.push_back(topLeft); indices.push_back(lowerLeft); indices.push_back(lowerRight); indices.push_back(topLeft); indices.push_back(lowerRight); indices.push_back(topRight); } } // bottom border: for (int x = 0; x < width - 1; ++x) { const int z = 0; int current = x + z * width; int neighbor = x + 1 + z * width; int connection = 1 + ((x + (step + 1) / 2 - 1) / step) * step + (z + 1) * width; indices.push_back(current); indices.push_back(neighbor); indices.push_back(connection); if (((x - 1) % (step)) == step / 2) // halfway fill triangle { int connection1 = 1 + (((x - 1) + (step + 1) / 2 - 1) / step) * step + (z + 1) * width; indices.push_back(current); indices.push_back(connection); indices.push_back(connection1); } } // top border: for (int x = 0; x < width - 1; ++x) { const int z = width - 1; int current = x + z * width; int neighbor = x + 1 + z * width; int connection = 1 + ((x + (step + 1) / 2 - 1) / step) * step + (z - 1) * width; indices.push_back(current); indices.push_back(connection); indices.push_back(neighbor); if (((x - 1) % (step)) == step / 2) // halfway fill triangle { int connection1 = 1 + (((x - 1) + (step + 1) / 2 - 1) / step) * step + (z - 1) * width; indices.push_back(current); indices.push_back(connection1); indices.push_back(connection); } } // left border: for (int z = 0; z < width - 1; ++z) { const int x = 0; int current = x + z * width; int neighbor = x + (z + 1) * width; int connection = x + 1 + (((z + (step + 1) / 2 - 1) / step) * step + 1) * width; indices.push_back(current); indices.push_back(connection); indices.push_back(neighbor); if (((z - 1) % (step)) == step / 2) // halfway fill triangle { int connection1 = x + 1 + ((((z - 1) + (step + 1) / 2 - 1) / step) * step + 1) * width; indices.push_back(current); indices.push_back(connection1); indices.push_back(connection); } } // right border: for (int z = 0; z < width - 1; ++z) { const int x = width - 1; int current = x + z * width; int neighbor = x + (z + 1) * width; int connection = x - 1 + (((z + (step + 1) / 2 - 1) / step) * step + 1) * width; indices.push_back(current); indices.push_back(neighbor); indices.push_back(connection); if (((z - 1) % (step)) == step / 2) // halfway fill triangle { int connection1 = x - 1 + ((((z - 1) + (step + 1) / 2 - 1) / step) * step + 1) * width; indices.push_back(current); indices.push_back(connection); indices.push_back(connection1); } } } lods[lod].indexCount = (uint32_t)indices.size() - lods[lod].indexOffset; } RemoveWidgets(); ClearTransform(); wi::gui::Window::Create("TerraGen (Preview version)"); SetSize(XMFLOAT2(410, 560)); float xx = 150; float yy = 0; float stepstep = 25; float heihei = 20; centerToCamCheckBox.Create("Center to Cam: "); centerToCamCheckBox.SetTooltip("Automatically generate chunks around camera. This sets the center chunk to camera position."); centerToCamCheckBox.SetSize(XMFLOAT2(heihei, heihei)); centerToCamCheckBox.SetPos(XMFLOAT2(xx, yy += stepstep)); centerToCamCheckBox.SetCheck(true); AddWidget(¢erToCamCheckBox); removalCheckBox.Create("Removal: "); removalCheckBox.SetTooltip("Automatically remove chunks that are farther than generation distance around center chunk."); removalCheckBox.SetSize(XMFLOAT2(heihei, heihei)); removalCheckBox.SetPos(XMFLOAT2(xx + 100, yy)); removalCheckBox.SetCheck(true); AddWidget(&removalCheckBox); lodSlider.Create(0.0001f, 0.01f, 0.005f, 10000, "LOD Distance: "); lodSlider.SetTooltip("Set the LOD (Level Of Detail) distance multiplier.\nLow values increase LOD detail in distance"); lodSlider.SetSize(XMFLOAT2(200, heihei)); lodSlider.SetPos(XMFLOAT2(xx, yy += stepstep)); lodSlider.OnSlide([this](wi::gui::EventArgs args) { for (auto& it : chunks) { const ChunkData& chunk_data = it.second; if (chunk_data.entity != INVALID_ENTITY) { ObjectComponent* object = scene->objects.GetComponent(chunk_data.entity); if (object != nullptr) { object->lod_distance_multiplier = args.fValue; } } } }); AddWidget(&lodSlider); generationSlider.Create(0, 16, 12, 16, "Generation Distance: "); generationSlider.SetTooltip("How far out chunks will be generated (value is in number of chunks)"); generationSlider.SetSize(XMFLOAT2(200, heihei)); generationSlider.SetPos(XMFLOAT2(xx, yy += stepstep)); AddWidget(&generationSlider); presetCombo.Create("Preset: "); presetCombo.SetTooltip("Select a terrain preset"); presetCombo.SetSize(XMFLOAT2(200, heihei)); presetCombo.SetPos(XMFLOAT2(xx, yy += stepstep)); presetCombo.AddItem("Hills", PRESET_HILLS); presetCombo.AddItem("Islands", PRESET_ISLANDS); presetCombo.AddItem("Mountains", PRESET_MOUNTAINS); presetCombo.AddItem("Arctic", PRESET_ARCTIC); presetCombo.OnSelect([=](wi::gui::EventArgs args) { switch (args.userdata) { default: case PRESET_HILLS: seedSlider.SetValue(5333); bottomLevelSlider.SetValue(-60); topLevelSlider.SetValue(380); perlinBlendSlider.SetValue(0.5f); perlinFrequencySlider.SetValue(0.0008f); perlinOctavesSlider.SetValue(6); voronoiBlendSlider.SetValue(0.5f); voronoiFrequencySlider.SetValue(0.001f); voronoiFadeSlider.SetValue(2.59f); voronoiShapeSlider.SetValue(0.7f); voronoiFalloffSlider.SetValue(6); voronoiPerturbationSlider.SetValue(0.1f); region1Slider.SetValue(1); region2Slider.SetValue(2); region3Slider.SetValue(8); break; case PRESET_ISLANDS: seedSlider.SetValue(4691); bottomLevelSlider.SetValue(-79); topLevelSlider.SetValue(520); perlinBlendSlider.SetValue(0.5f); perlinFrequencySlider.SetValue(0.000991f); perlinOctavesSlider.SetValue(6); voronoiBlendSlider.SetValue(0.5f); voronoiFrequencySlider.SetValue(0.000317f); voronoiFadeSlider.SetValue(8.2f); voronoiShapeSlider.SetValue(0.126f); voronoiFalloffSlider.SetValue(1.392f); voronoiPerturbationSlider.SetValue(0.126f); region1Slider.SetValue(8); region2Slider.SetValue(0.7f); region3Slider.SetValue(8); break; case PRESET_MOUNTAINS: seedSlider.SetValue(8863); bottomLevelSlider.SetValue(0); topLevelSlider.SetValue(2960); perlinBlendSlider.SetValue(0.5f); perlinFrequencySlider.SetValue(0.00279f); perlinOctavesSlider.SetValue(8); voronoiBlendSlider.SetValue(0.5f); voronoiFrequencySlider.SetValue(0.000496f); voronoiFadeSlider.SetValue(5.2f); voronoiShapeSlider.SetValue(0.412f); voronoiFalloffSlider.SetValue(1.456f); voronoiPerturbationSlider.SetValue(0.092f); region1Slider.SetValue(1); region2Slider.SetValue(1); region3Slider.SetValue(0.8f); break; case PRESET_ARCTIC: seedSlider.SetValue(2124); bottomLevelSlider.SetValue(-50); topLevelSlider.SetValue(40); perlinBlendSlider.SetValue(1); perlinFrequencySlider.SetValue(0.002f); perlinOctavesSlider.SetValue(4); voronoiBlendSlider.SetValue(1); voronoiFrequencySlider.SetValue(0.004f); voronoiFadeSlider.SetValue(1.8f); voronoiShapeSlider.SetValue(0.518f); voronoiFalloffSlider.SetValue(0.2f); voronoiPerturbationSlider.SetValue(0.298f); region1Slider.SetValue(8); region2Slider.SetValue(8); region3Slider.SetValue(0); break; } Generation_Restart(); }); AddWidget(&presetCombo); seedSlider.Create(1, 12345, 3926, 12344, "Seed: "); seedSlider.SetTooltip("Seed for terrain randomness"); seedSlider.SetSize(XMFLOAT2(200, heihei)); seedSlider.SetPos(XMFLOAT2(xx, yy += stepstep)); AddWidget(&seedSlider); bottomLevelSlider.Create(-100, 0, -60, 10000, "Bottom Level: "); bottomLevelSlider.SetTooltip("Terrain mesh grid lowest level"); bottomLevelSlider.SetSize(XMFLOAT2(200, heihei)); bottomLevelSlider.SetPos(XMFLOAT2(xx, yy += stepstep)); AddWidget(&bottomLevelSlider); topLevelSlider.Create(0, 5000, 380, 10000, "Top Level: "); topLevelSlider.SetTooltip("Terrain mesh grid topmost level"); topLevelSlider.SetSize(XMFLOAT2(200, heihei)); topLevelSlider.SetPos(XMFLOAT2(xx, yy += stepstep)); AddWidget(&topLevelSlider); perlinBlendSlider.Create(0, 1, 0.5f, 10000, "Perlin Blend: "); perlinBlendSlider.SetTooltip("Amount of perlin noise to use"); perlinBlendSlider.SetSize(XMFLOAT2(200, heihei)); perlinBlendSlider.SetPos(XMFLOAT2(xx, yy += stepstep)); AddWidget(&perlinBlendSlider); perlinFrequencySlider.Create(0.0001f, 0.01f, 0.0008f, 10000, "Perlin Frequency: "); perlinFrequencySlider.SetTooltip("Frequency for the perlin noise"); perlinFrequencySlider.SetSize(XMFLOAT2(200, heihei)); perlinFrequencySlider.SetPos(XMFLOAT2(xx, yy += stepstep)); AddWidget(&perlinFrequencySlider); perlinOctavesSlider.Create(1, 8, 6, 7, "Perlin Octaves: "); perlinOctavesSlider.SetTooltip("Octave count for the perlin noise"); perlinOctavesSlider.SetSize(XMFLOAT2(200, heihei)); perlinOctavesSlider.SetPos(XMFLOAT2(xx, yy += stepstep)); AddWidget(&perlinOctavesSlider); voronoiBlendSlider.Create(0, 1, 0.5f, 10000, "Voronoi Blend: "); voronoiBlendSlider.SetTooltip("Amount of voronoi to use for elevation"); voronoiBlendSlider.SetSize(XMFLOAT2(200, heihei)); voronoiBlendSlider.SetPos(XMFLOAT2(xx, yy += stepstep)); AddWidget(&voronoiBlendSlider); voronoiFrequencySlider.Create(0.0001f, 0.01f, 0.001f, 10000, "Voronoi Frequency: "); voronoiFrequencySlider.SetTooltip("Voronoi can create distinctly elevated areas, the more cells there are, smaller the consecutive areas"); voronoiFrequencySlider.SetSize(XMFLOAT2(200, heihei)); voronoiFrequencySlider.SetPos(XMFLOAT2(xx, yy += stepstep)); AddWidget(&voronoiFrequencySlider); voronoiFadeSlider.Create(0, 100, 2.59f, 10000, "Voronoi Fade: "); voronoiFadeSlider.SetTooltip("Fade out voronoi regions by distance from cell's center"); voronoiFadeSlider.SetSize(XMFLOAT2(200, heihei)); voronoiFadeSlider.SetPos(XMFLOAT2(xx, yy += stepstep)); AddWidget(&voronoiFadeSlider); voronoiShapeSlider.Create(0, 1, 0.7f, 10000, "Voronoi Shape: "); voronoiShapeSlider.SetTooltip("How much the voronoi shape will be kept"); voronoiShapeSlider.SetSize(XMFLOAT2(200, heihei)); voronoiShapeSlider.SetPos(XMFLOAT2(xx, yy += stepstep)); AddWidget(&voronoiShapeSlider); voronoiFalloffSlider.Create(0, 8, 6, 10000, "Voronoi Falloff: "); voronoiFalloffSlider.SetTooltip("Controls the falloff of the voronoi distance fade effect"); voronoiFalloffSlider.SetSize(XMFLOAT2(200, heihei)); voronoiFalloffSlider.SetPos(XMFLOAT2(xx, yy += stepstep)); AddWidget(&voronoiFalloffSlider); voronoiPerturbationSlider.Create(0, 1, 0.1f, 10000, "Voronoi Perturbation: "); voronoiPerturbationSlider.SetTooltip("Controls the random look of voronoi region edges"); voronoiPerturbationSlider.SetSize(XMFLOAT2(200, heihei)); voronoiPerturbationSlider.SetPos(XMFLOAT2(xx, yy += stepstep)); AddWidget(&voronoiPerturbationSlider); heightmapButton.Create("Load Heightmap..."); heightmapButton.SetTooltip("Load a heightmap texture, where the red channel corresponds to terrain height and the resolution to dimensions.\nThe heightmap will be placed in the world center."); heightmapButton.SetSize(XMFLOAT2(200, heihei)); heightmapButton.SetPos(XMFLOAT2(xx, yy += stepstep)); AddWidget(&heightmapButton); heightmapBlendSlider.Create(0, 1, 1, 10000, "Heightmap Blend: "); heightmapBlendSlider.SetTooltip("Amount of displacement coming from the heightmap texture"); heightmapBlendSlider.SetSize(XMFLOAT2(200, heihei)); heightmapBlendSlider.SetPos(XMFLOAT2(xx, yy += stepstep)); AddWidget(&heightmapBlendSlider); region1Slider.Create(0, 8, 1, 10000, "Slope Region: "); region1Slider.SetTooltip("The region's falloff power"); region1Slider.SetSize(XMFLOAT2(200, heihei)); region1Slider.SetPos(XMFLOAT2(xx, yy += stepstep)); AddWidget(®ion1Slider); region2Slider.Create(0, 8, 2, 10000, "Low Altitude Region: "); region2Slider.SetTooltip("The region's falloff power"); region2Slider.SetSize(XMFLOAT2(200, heihei)); region2Slider.SetPos(XMFLOAT2(xx, yy += stepstep)); AddWidget(®ion2Slider); region3Slider.Create(0, 8, 8, 10000, "High Altitude Region: "); region3Slider.SetTooltip("The region's falloff power"); region3Slider.SetSize(XMFLOAT2(200, heihei)); region3Slider.SetPos(XMFLOAT2(xx, yy += stepstep)); AddWidget(®ion3Slider); auto generate_callback = [=](wi::gui::EventArgs args) { Generation_Restart(); }; seedSlider.OnSlide(generate_callback); bottomLevelSlider.OnSlide(generate_callback); topLevelSlider.OnSlide(generate_callback); perlinFrequencySlider.OnSlide(generate_callback); perlinBlendSlider.OnSlide(generate_callback); perlinOctavesSlider.OnSlide(generate_callback); voronoiBlendSlider.OnSlide(generate_callback); voronoiFrequencySlider.OnSlide(generate_callback); voronoiFadeSlider.OnSlide(generate_callback); voronoiShapeSlider.OnSlide(generate_callback); voronoiFalloffSlider.OnSlide(generate_callback); voronoiPerturbationSlider.OnSlide(generate_callback); heightmapBlendSlider.OnSlide(generate_callback); region1Slider.OnSlide(generate_callback); region2Slider.OnSlide(generate_callback); region3Slider.OnSlide(generate_callback); heightmapButton.OnClick([=](wi::gui::EventArgs args) { wi::helper::FileDialogParams params; params.type = wi::helper::FileDialogParams::OPEN; params.description = "Texture"; params.extensions = wi::resourcemanager::GetSupportedImageExtensions(); wi::helper::FileDialog(params, [=](std::string fileName) { wi::eventhandler::Subscribe_Once(wi::eventhandler::EVENT_THREAD_SAFE_POINT, [=](uint64_t userdata) { heightmap = {}; int bpp = 0; stbi_uc* rgba = stbi_load(fileName.c_str(), &heightmap.width, &heightmap.height, &bpp, 1); if (rgba != nullptr) { heightmap.data.resize(heightmap.width * heightmap.height); for (int i = 0; i < heightmap.width * heightmap.height; ++i) { heightmap.data[i] = rgba[i]; } stbi_image_free(rgba); Generation_Restart(); } }); }); }); heightmap = {}; SetPos(XMFLOAT2(50, 110)); SetVisible(false); SetEnabled(true); presetCombo.SetSelectedByUserdata(PRESET_HILLS); } void TerrainGenerator::Generation_Restart() { Generation_Cancel(); generation_scene.Clear(); generation_entities.clear(); // If these already exist, save them before recreating: // (This is helpful if eg: someone edits them in the editor and then regenerates the terrain) if (materialEntity_Base != INVALID_ENTITY) { MaterialComponent* material = scene->materials.GetComponent(materialEntity_Base); if (material != nullptr) { material_Base = *material; } } if (materialEntity_Slope != INVALID_ENTITY) { MaterialComponent* material = scene->materials.GetComponent(materialEntity_Slope); if (material != nullptr) { material_Slope = *material; } } if (materialEntity_LowAltitude != INVALID_ENTITY) { MaterialComponent* material = scene->materials.GetComponent(materialEntity_LowAltitude); if (material != nullptr) { material_LowAltitude = *material; } } if (materialEntity_HighAltitude != INVALID_ENTITY) { MaterialComponent* material = scene->materials.GetComponent(materialEntity_HighAltitude); if (material != nullptr) { material_HighAltitude = *material; } } chunks.clear(); scene->Entity_Remove(terrainEntity); scene->transforms.Create(terrainEntity); scene->names.Create(terrainEntity) = "terrain"; materialEntity_Base = scene->Entity_CreateMaterial("terrainMaterial_Base"); materialEntity_Slope = scene->Entity_CreateMaterial("terrainMaterial_Slope"); materialEntity_LowAltitude = scene->Entity_CreateMaterial("terrainMaterial_LowAltitude"); materialEntity_HighAltitude = scene->Entity_CreateMaterial("terrainMaterial_HighAltitude"); scene->Component_Attach(materialEntity_Base, terrainEntity); scene->Component_Attach(materialEntity_Slope, terrainEntity); scene->Component_Attach(materialEntity_LowAltitude, terrainEntity); scene->Component_Attach(materialEntity_HighAltitude, terrainEntity); // init/restore materials: *scene->materials.GetComponent(materialEntity_Base) = material_Base; *scene->materials.GetComponent(materialEntity_Slope) = material_Slope; *scene->materials.GetComponent(materialEntity_LowAltitude) = material_LowAltitude; *scene->materials.GetComponent(materialEntity_HighAltitude) = material_HighAltitude; const uint32_t seed = (uint32_t)seedSlider.GetValue(); perlin.init(seed); // Add some nice weather and lighting if there is none yet: if (scene->weathers.GetCount() == 0) { Entity weatherEntity = CreateEntity(); WeatherComponent& weather = scene->weathers.Create(weatherEntity); scene->names.Create(weatherEntity) = "terrainWeather"; scene->Component_Attach(weatherEntity, terrainEntity); weather.ambient = XMFLOAT3(0.2f, 0.2f, 0.2f); weather.SetRealisticSky(true); weather.SetVolumetricClouds(true); weather.volumetricCloudParameters.CoverageAmount = 0.4f; weather.volumetricCloudParameters.CoverageMinimum = 1.35f; if (presetCombo.GetItemUserData(presetCombo.GetSelected()) == PRESET_ISLANDS) { weather.SetOceanEnabled(true); } else { scene->ocean = {}; } weather.oceanParameters.waterHeight = -40; weather.oceanParameters.wave_amplitude = 120; weather.fogStart = 10; weather.fogEnd = 100000; weather.SetHeightFog(true); weather.fogHeightStart = 0; weather.fogHeightEnd = 100; weather.windDirection = XMFLOAT3(0.05f, 0.05f, 0.05f); weather.windSpeed = 4; } if (scene->lights.GetCount() == 0) { Entity sunEntity = scene->Entity_CreateLight("terrainSun"); scene->Component_Attach(sunEntity, terrainEntity); LightComponent& light = *scene->lights.GetComponent(sunEntity); light.SetType(LightComponent::LightType::DIRECTIONAL); light.energy = 6; light.SetCastShadow(true); //light.SetVolumetricsEnabled(true); TransformComponent& transform = *scene->transforms.GetComponent(sunEntity); transform.RotateRollPitchYaw(XMFLOAT3(XM_PIDIV4, 0, XM_PIDIV4)); } } void TerrainGenerator::Generation_Update() { // The generation task is always cancelled every frame so we are sure that generation is not running at this point Generation_Cancel(); if (terrainEntity == INVALID_ENTITY || !scene->transforms.Contains(terrainEntity)) { chunks.clear(); return; } // What was generated, will be merged in to the main scene and parented to the terrain entity scene->Merge(generation_scene); for (Entity entity : generation_entities) { scene->Component_Attach(entity, terrainEntity); } generation_entities.clear(); if (centerToCamCheckBox.GetCheck()) { const CameraComponent& camera = GetCamera(); center_chunk.x = (int)std::floor((camera.Eye.x + half_width) * width_rcp); center_chunk.z = (int)std::floor((camera.Eye.z + half_width) * width_rcp); } // Chunk removal checks: if (removalCheckBox.GetCheck()) { const int removal_threshold = (int)generationSlider.GetValue() + 2; for (auto it = chunks.begin(); it != chunks.end();) { const Chunk& chunk = it->first; ChunkData& chunk_data = it->second; const int dist = std::max(std::abs(center_chunk.x - chunk.x), std::abs(center_chunk.z - chunk.z)); if (dist > removal_threshold) { scene->Entity_Remove(it->second.entity); it = chunks.erase(it); continue; // don't increment iterator } else { // Grass patch removal: if (chunk_data.grass.meshID != INVALID_ENTITY) { if (dist > 1) { wi::HairParticleSystem* grass = scene->hairs.GetComponent(chunk_data.entity); if (grass != nullptr) { // remove this chunk's grass patch from the scene scene->hairs.Remove(chunk_data.entity); chunk_data.grass_exists = false; // grass can be generated here by generation thread... } } } } it++; } } // Start the generation on a background thread and keep it running until the next frame wi::jobsystem::Execute(generation_workload, [=](wi::jobsystem::JobArgs args) { const float lodMultiplier = lodSlider.GetValue(); const int generation = (int)generationSlider.GetValue(); const float bottomLevel = bottomLevelSlider.GetValue(); const float topLevel = topLevelSlider.GetValue(); const float heightmapBlend = heightmapBlendSlider.GetValue(); const float perlinBlend = perlinBlendSlider.GetValue(); const uint32_t seed = (uint32_t)seedSlider.GetValue(); const int perlinOctaves = (int)perlinOctavesSlider.GetValue(); const float perlinFrequency = perlinFrequencySlider.GetValue(); const float voronoiBlend = voronoiBlendSlider.GetValue(); const float voronoiFrequency = voronoiFrequencySlider.GetValue(); const float voronoiFade = voronoiFadeSlider.GetValue(); const float voronoiShape = voronoiShapeSlider.GetValue(); const float voronoiFalloff = voronoiFalloffSlider.GetValue(); const float voronoiPerturbation = voronoiPerturbationSlider.GetValue(); const float region1 = region1Slider.GetValue(); const float region2 = region2Slider.GetValue(); const float region3 = region3Slider.GetValue(); auto request_chunk = [&](int offset_x, int offset_z) { Chunk chunk = center_chunk; chunk.x += offset_x; chunk.z += offset_z; auto it = chunks.find(chunk); if (it == chunks.end() || it->second.entity == INVALID_ENTITY) { // Generate a new chunk: ChunkData& chunk_data = chunks[chunk]; chunk_data.entity = generation_scene.Entity_CreateObject("chunk_" + std::to_string(chunk.x) + "_" + std::to_string(chunk.z)); ObjectComponent& object = *generation_scene.objects.GetComponent(chunk_data.entity); object.lod_distance_multiplier = lodMultiplier; generation_entities.push_back(chunk_data.entity); TransformComponent& transform = *generation_scene.transforms.GetComponent(chunk_data.entity); transform.ClearTransform(); const XMFLOAT3 chunk_pos = XMFLOAT3(float(chunk.x * (width - 1)), 0, float(chunk.z * (width - 1))); transform.Translate(chunk_pos); transform.UpdateTransform(); MeshComponent& mesh = generation_scene.meshes.Create(chunk_data.entity); object.meshID = chunk_data.entity; mesh.indices = indices; mesh.SetTerrain(true); mesh.terrain_material1 = materialEntity_Slope; mesh.terrain_material2 = materialEntity_LowAltitude; mesh.terrain_material3 = materialEntity_HighAltitude; for (auto& lod : lods) { mesh.subsets.emplace_back(); mesh.subsets.back().materialID = materialEntity_Base; mesh.subsets.back().indexCount = lod.indexCount; mesh.subsets.back().indexOffset = lod.indexOffset; } mesh.subsets_per_lod = 1; mesh.vertex_positions.resize(vertexCount); mesh.vertex_normals.resize(vertexCount); mesh.vertex_colors.resize(vertexCount); mesh.vertex_uvset_0.resize(vertexCount); mesh.vertex_uvset_1.resize(vertexCount); mesh.vertex_atlas.resize(vertexCount); wi::HairParticleSystem grass; grass.vertex_lengths.resize(vertexCount); std::atomic grass_valid_vertex_count{ 0 }; // Do a parallel for loop over all the chunk's vertices and compute their properties: wi::jobsystem::context ctx; wi::jobsystem::Dispatch(ctx, vertexCount, width, [&](wi::jobsystem::JobArgs args) { uint32_t index = args.jobIndex; const float x = float(index % width) - half_width; const float z = float(index / width) - half_width; XMVECTOR corners[3]; XMFLOAT2 corner_offsets[3] = { XMFLOAT2(0, 0), XMFLOAT2(1, 0), XMFLOAT2(0, 1), }; for (int i = 0; i < arraysize(corners); ++i) { float height = 0; const XMFLOAT2 world_pos = XMFLOAT2(chunk_pos.x + x + corner_offsets[i].x, chunk_pos.z + z + corner_offsets[i].y); if (perlinBlend > 0) { XMFLOAT2 p = world_pos; p.x *= perlinFrequency; p.y *= perlinFrequency; height += (perlin.compute(p.x, p.y, 0, perlinOctaves) * 0.5f + 0.5f) * perlinBlend; } if (voronoiBlend > 0) { XMFLOAT2 p = world_pos; p.x *= voronoiFrequency; p.y *= voronoiFrequency; if (voronoiPerturbation > 0) { const float angle = perlin.compute(p.x, p.y, 0, 6) * XM_2PI; p.x += std::sin(angle) * voronoiPerturbation; p.y += std::cos(angle) * voronoiPerturbation; } wi::noise::voronoi::Result res = wi::noise::voronoi::compute(p.x, p.y, (float)seed); float weight = std::pow(1 - wi::math::saturate((res.distance - voronoiShape) * voronoiFade), std::max(0.0001f, voronoiFalloff)); height *= weight * voronoiBlend; } if (!heightmap.data.empty()) { XMFLOAT2 pixel = XMFLOAT2(world_pos.x + heightmap.width * 0.5f, world_pos.y + heightmap.height * 0.5f); if (pixel.x >= 0 && pixel.x < heightmap.width && pixel.y >= 0 && pixel.y < heightmap.height) { const int idx = int(pixel.x) + int(pixel.y) * heightmap.width; height = ((float)heightmap.data[idx] / 255.0f) * heightmapBlend; } } height = wi::math::Lerp(bottomLevel, topLevel, height); corners[i] = XMVectorSet(world_pos.x, height, world_pos.y, 0); } const float height = XMVectorGetY(corners[0]); const XMVECTOR T = XMVectorSubtract(corners[2], corners[1]); const XMVECTOR B = XMVectorSubtract(corners[1], corners[0]); const XMVECTOR N = XMVector3Normalize(XMVector3Cross(T, B)); XMFLOAT3 normal; XMStoreFloat3(&normal, N); const float region_base = 1; const float region_slope = std::pow(1.0f - wi::math::saturate(normal.y), region1); const float region_low_altitude = bottomLevel == 0 ? 0 : std::pow(wi::math::saturate(wi::math::InverseLerp(0, bottomLevel, height)), region2); const float region_high_altitude = topLevel == 0 ? 0 : std::pow(wi::math::saturate(wi::math::InverseLerp(0, topLevel, height)), region3); XMFLOAT4 materialBlendWeights(region_base, 0, 0, 0); materialBlendWeights = wi::math::Lerp(materialBlendWeights, XMFLOAT4(0, 1, 0, 0), region_slope); materialBlendWeights = wi::math::Lerp(materialBlendWeights, XMFLOAT4(0, 0, 1, 0), region_low_altitude); materialBlendWeights = wi::math::Lerp(materialBlendWeights, XMFLOAT4(0, 0, 0, 1), region_high_altitude); const float weight_norm = 1.0f / (materialBlendWeights.x + materialBlendWeights.y + materialBlendWeights.z + materialBlendWeights.w); materialBlendWeights.x *= weight_norm; materialBlendWeights.y *= weight_norm; materialBlendWeights.z *= weight_norm; materialBlendWeights.w *= weight_norm; mesh.vertex_positions[index] = XMFLOAT3(x, height, z); mesh.vertex_normals[index] = normal; mesh.vertex_colors[index] = wi::Color::fromFloat4(materialBlendWeights); const XMFLOAT2 uv = XMFLOAT2(x * width_rcp, z * width_rcp); mesh.vertex_uvset_0[index] = uv; mesh.vertex_uvset_1[index] = uv; mesh.vertex_atlas[index] = uv; XMFLOAT3 vertex_pos(chunk_pos.x + x, height, chunk_pos.z + z); const float grass_noise_frequency = 0.1f; const float grass_noise = perlin.compute(vertex_pos.x * grass_noise_frequency, vertex_pos.y * grass_noise_frequency, vertex_pos.z * grass_noise_frequency) * 0.5f + 0.5f; const float region_grass = std::pow(materialBlendWeights.x * (1 - materialBlendWeights.w), 4.0f) * grass_noise; if (region_grass > 0.1f) { grass_valid_vertex_count.fetch_add(1); grass.vertex_lengths[index] = region_grass; } else { grass.vertex_lengths[index] = 0; } }); wi::jobsystem::Wait(ctx); // wait until chunk's vertex buffer is fully generated mesh.CreateRenderData(); // If there were any vertices in this chunk that could be valid for grass, store the grass particle system: if (grass_valid_vertex_count.load() > 0) { grass.meshID = chunk_data.entity; grass.length = 5; grass.strandCount = grass_valid_vertex_count.load() * 3; grass.viewDistance = 80; grass.frameCount = 2; grass.framesX = 1; grass.framesY = 2; grass.frameStart = 0; generation_scene.materials.Create(chunk_data.entity) = material_GrassParticle; chunk_data.grass = std::move(grass); // the grass will be added to the scene later, only when the chunk is close to the camera (center chunk's neighbors) } // Prop placement: chunk_data.prop_rand.seed((uint32_t)chunk.compute_hash() ^ seed); for (auto& prop : props) { std::uniform_int_distribution gen_distr(prop.min_count_per_chunk, prop.max_count_per_chunk); int gen_count = gen_distr(chunk_data.prop_rand); for (int i = 0; i < gen_count; ++i) { std::uniform_real_distribution float_distr(0.0f, 1.0f); std::uniform_int_distribution ind_distr(0, lods[0].indexCount / 3 - 1); uint32_t tri = ind_distr(chunk_data.prop_rand); // random triangle on the chunk mesh uint32_t ind0 = mesh.indices[tri * 3 + 0]; uint32_t ind1 = mesh.indices[tri * 3 + 1]; uint32_t ind2 = mesh.indices[tri * 3 + 2]; const XMFLOAT3& pos0 = mesh.vertex_positions[ind0]; const XMFLOAT3& pos1 = mesh.vertex_positions[ind1]; const XMFLOAT3& pos2 = mesh.vertex_positions[ind2]; const uint32_t& col0 = mesh.vertex_colors[ind0]; const uint32_t& col1 = mesh.vertex_colors[ind1]; const uint32_t& col2 = mesh.vertex_colors[ind2]; const XMFLOAT4 region0 = wi::Color(col0).toFloat4(); const XMFLOAT4 region1 = wi::Color(col1).toFloat4(); const XMFLOAT4 region2 = wi::Color(col2).toFloat4(); // random barycentric coords on the triangle: float f = float_distr(chunk_data.prop_rand); float g = float_distr(chunk_data.prop_rand); if (f + g > 1) { f = 1 - f; g = 1 - g; } XMFLOAT3 vertex_pos; vertex_pos.x = pos0.x + f * (pos1.x - pos0.x) + g * (pos2.x - pos0.x); vertex_pos.y = pos0.y + f * (pos1.y - pos0.y) + g * (pos2.y - pos0.y); vertex_pos.z = pos0.z + f * (pos1.z - pos0.z) + g * (pos2.z - pos0.z); vertex_pos.x += chunk_pos.x; vertex_pos.z += chunk_pos.z; XMFLOAT4 region; region.x = region0.x + f * (region1.x - region0.x) + g * (region2.x - region0.x); region.y = region0.y + f * (region1.y - region0.y) + g * (region2.y - region0.y); region.z = region0.z + f * (region1.z - region0.z) + g * (region2.z - region0.z); region.w = region0.w + f * (region1.w - region0.w) + g * (region2.w - region0.w); const float noise = std::pow(perlin.compute(vertex_pos.x * prop.noise_frequency, vertex_pos.y * prop.noise_frequency, vertex_pos.z * prop.noise_frequency) * 0.5f + 0.5f, prop.noise_power); const float chance = std::pow(((float*)®ion)[prop.region], prop.region_power) * noise; if (chance > prop.threshold) { Entity entity = generation_scene.Entity_CreateObject(prop.name + std::to_string(i)); ObjectComponent* object = generation_scene.objects.GetComponent(entity); *object = prop.object; TransformComponent* transform = generation_scene.transforms.GetComponent(entity); XMFLOAT3 offset = vertex_pos; offset.y += wi::math::Lerp(prop.min_y_offset, prop.max_y_offset, float_distr(chunk_data.prop_rand)); transform->Translate(offset); const float scaling = wi::math::Lerp(prop.min_size, prop.max_size, float_distr(chunk_data.prop_rand)); transform->Scale(XMFLOAT3(scaling, scaling, scaling)); transform->RotateRollPitchYaw(XMFLOAT3(0, XM_2PI * float_distr(chunk_data.prop_rand), 0)); transform->UpdateTransform(); generation_scene.Component_Attach(entity, chunk_data.entity); } } } } // Grass patch placement: it = chunks.find(chunk); if (it != chunks.end() && it->second.entity != INVALID_ENTITY) { ChunkData& chunk_data = it->second; if (chunk_data.grass.meshID != INVALID_ENTITY) { const int dist = std::max(std::abs(center_chunk.x - chunk.x), std::abs(center_chunk.z - chunk.z)); if (dist <= 1) { if (!chunk_data.grass_exists) { // add patch for this chunk wi::HairParticleSystem& grass = generation_scene.hairs.Create(chunk_data.entity); grass = chunk_data.grass; const MeshComponent* mesh = generation_scene.meshes.GetComponent(chunk_data.entity); if (mesh != nullptr) { grass.CreateRenderData(*mesh); } chunk_data.grass_exists = true; // don't generate more grass here } } } } }; // generate center chunk first: request_chunk(0, 0); if (generation_cancelled.load()) return; // then generate neighbor chunks in outward spiral: for (int growth = 0; growth < generation; ++growth) { const int side = 2 * (growth + 1); int x = -growth - 1; int z = -growth - 1; for (int i = 0; i < side; ++i) { request_chunk(x, z); if (generation_cancelled.load()) return; x++; } for (int i = 0; i < side; ++i) { request_chunk(x, z); if (generation_cancelled.load()) return; z++; } for (int i = 0; i < side; ++i) { request_chunk(x, z); if (generation_cancelled.load()) return; x--; } for (int i = 0; i < side; ++i) { request_chunk(x, z); if (generation_cancelled.load()) return; z--; } } }); } void TerrainGenerator::Generation_Cancel() { generation_cancelled.store(true); // tell the generation thread that work must be stopped wi::jobsystem::Wait(generation_workload); // waits until generation thread exits generation_cancelled.store(false); // the next generation can run }