#include "stdafx.h" #include "TerrainGenerator.h" #include "Utility/stb_image.h" using namespace wi::ecs; using namespace wi::scene; using namespace wi::graphics; 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 < chunk_width - 1; x++) { for (int z = 0; z < chunk_width - 1; z++) { int lowerLeft = x + z * chunk_width; int lowerRight = (x + 1) + z * chunk_width; int topLeft = x + (z + 1) * chunk_width; int topRight = (x + 1) + (z + 1) * chunk_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 < chunk_width - 2; x += step) { for (int z = 1; z < chunk_width - 2; z += step) { int lowerLeft = x + z * chunk_width; int lowerRight = (x + step) + z * chunk_width; int topLeft = x + (z + step) * chunk_width; int topRight = (x + step) + (z + step) * chunk_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 < chunk_width - 1; ++x) { const int z = 0; int current = x + z * chunk_width; int neighbor = x + 1 + z * chunk_width; int connection = 1 + ((x + (step + 1) / 2 - 1) / step) * step + (z + 1) * chunk_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) * chunk_width; indices.push_back(current); indices.push_back(connection); indices.push_back(connection1); } } // top border: for (int x = 0; x < chunk_width - 1; ++x) { const int z = chunk_width - 1; int current = x + z * chunk_width; int neighbor = x + 1 + z * chunk_width; int connection = 1 + ((x + (step + 1) / 2 - 1) / step) * step + (z - 1) * chunk_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) * chunk_width; indices.push_back(current); indices.push_back(connection1); indices.push_back(connection); } } // left border: for (int z = 0; z < chunk_width - 1; ++z) { const int x = 0; int current = x + z * chunk_width; int neighbor = x + (z + 1) * chunk_width; int connection = x + 1 + (((z + (step + 1) / 2 - 1) / step) * step + 1) * chunk_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) * chunk_width; indices.push_back(current); indices.push_back(connection1); indices.push_back(connection); } } // right border: for (int z = 0; z < chunk_width - 1; ++z) { const int x = chunk_width - 1; int current = x + z * chunk_width; int neighbor = x + (z + 1) * chunk_width; int connection = x - 1 + (((z + (step + 1) / 2 - 1) / step) * step + 1) * chunk_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) * chunk_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(420, 590)); float x = 160; float y = 0; float step = 25; float hei = 20; centerToCamCheckBox.Create("Center to Cam: "); centerToCamCheckBox.SetTooltip("Automatically generate chunks around camera. This sets the center chunk to camera position."); centerToCamCheckBox.SetSize(XMFLOAT2(hei, hei)); centerToCamCheckBox.SetPos(XMFLOAT2(x, y += step)); 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(hei, hei)); removalCheckBox.SetPos(XMFLOAT2(x + 100, y)); removalCheckBox.SetCheck(true); AddWidget(&removalCheckBox); lodSlider.Create(0.0001f, 0.01f, 0.005f, 10000, "Mesh LOD Distance: "); lodSlider.SetTooltip("Set the LOD (Level Of Detail) distance multiplier.\nLow values increase LOD detail in distance"); lodSlider.SetSize(XMFLOAT2(200, hei)); lodSlider.SetPos(XMFLOAT2(x, y += step)); 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); texlodSlider.Create(0.01f, 0.05f, 0.01f, 10000, "Texture LOD Distance: "); texlodSlider.SetTooltip("Set the LOD (Level Of Detail) distance multiplier.\nLow values increase LOD detail in distance"); texlodSlider.SetSize(XMFLOAT2(200, hei)); texlodSlider.SetPos(XMFLOAT2(x, y += step)); AddWidget(&texlodSlider); 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, hei)); generationSlider.SetPos(XMFLOAT2(x, y += step)); AddWidget(&generationSlider); presetCombo.Create("Preset: "); presetCombo.SetTooltip("Select a terrain preset"); presetCombo.SetSize(XMFLOAT2(200, hei)); presetCombo.SetPos(XMFLOAT2(x, y += step)); 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, hei)); seedSlider.SetPos(XMFLOAT2(x, y += step)); AddWidget(&seedSlider); bottomLevelSlider.Create(-100, 0, -60, 10000, "Bottom Level: "); bottomLevelSlider.SetTooltip("Terrain mesh grid lowest level"); bottomLevelSlider.SetSize(XMFLOAT2(200, hei)); bottomLevelSlider.SetPos(XMFLOAT2(x, y += step)); AddWidget(&bottomLevelSlider); topLevelSlider.Create(0, 5000, 380, 10000, "Top Level: "); topLevelSlider.SetTooltip("Terrain mesh grid topmost level"); topLevelSlider.SetSize(XMFLOAT2(200, hei)); topLevelSlider.SetPos(XMFLOAT2(x, y += step)); AddWidget(&topLevelSlider); perlinBlendSlider.Create(0, 1, 0.5f, 10000, "Perlin Blend: "); perlinBlendSlider.SetTooltip("Amount of perlin noise to use"); perlinBlendSlider.SetSize(XMFLOAT2(200, hei)); perlinBlendSlider.SetPos(XMFLOAT2(x, y += step)); AddWidget(&perlinBlendSlider); perlinFrequencySlider.Create(0.0001f, 0.01f, 0.0008f, 10000, "Perlin Frequency: "); perlinFrequencySlider.SetTooltip("Frequency for the perlin noise"); perlinFrequencySlider.SetSize(XMFLOAT2(200, hei)); perlinFrequencySlider.SetPos(XMFLOAT2(x, y += step)); AddWidget(&perlinFrequencySlider); perlinOctavesSlider.Create(1, 8, 6, 7, "Perlin Octaves: "); perlinOctavesSlider.SetTooltip("Octave count for the perlin noise"); perlinOctavesSlider.SetSize(XMFLOAT2(200, hei)); perlinOctavesSlider.SetPos(XMFLOAT2(x, y += step)); AddWidget(&perlinOctavesSlider); voronoiBlendSlider.Create(0, 1, 0.5f, 10000, "Voronoi Blend: "); voronoiBlendSlider.SetTooltip("Amount of voronoi to use for elevation"); voronoiBlendSlider.SetSize(XMFLOAT2(200, hei)); voronoiBlendSlider.SetPos(XMFLOAT2(x, y += step)); 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, hei)); voronoiFrequencySlider.SetPos(XMFLOAT2(x, y += step)); 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, hei)); voronoiFadeSlider.SetPos(XMFLOAT2(x, y += step)); 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, hei)); voronoiShapeSlider.SetPos(XMFLOAT2(x, y += step)); 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, hei)); voronoiFalloffSlider.SetPos(XMFLOAT2(x, y += step)); 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, hei)); voronoiPerturbationSlider.SetPos(XMFLOAT2(x, y += step)); 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, hei)); heightmapButton.SetPos(XMFLOAT2(x, y += step)); AddWidget(&heightmapButton); heightmapBlendSlider.Create(0, 1, 1, 10000, "Heightmap Blend: "); heightmapBlendSlider.SetTooltip("Amount of displacement coming from the heightmap texture"); heightmapBlendSlider.SetSize(XMFLOAT2(200, hei)); heightmapBlendSlider.SetPos(XMFLOAT2(x, y += step)); AddWidget(&heightmapBlendSlider); region1Slider.Create(0, 8, 1, 10000, "Slope Region: "); region1Slider.SetTooltip("The region's falloff power"); region1Slider.SetSize(XMFLOAT2(200, hei)); region1Slider.SetPos(XMFLOAT2(x, y += step)); AddWidget(®ion1Slider); region2Slider.Create(0, 8, 2, 10000, "Low Altitude Region: "); region2Slider.SetTooltip("The region's falloff power"); region2Slider.SetSize(XMFLOAT2(200, hei)); region2Slider.SetPos(XMFLOAT2(x, y += step)); AddWidget(®ion2Slider); region3Slider.Create(0, 8, 8, 10000, "High Altitude Region: "); region3Slider.SetTooltip("The region's falloff power"); region3Slider.SetSize(XMFLOAT2(200, hei)); region3Slider.SetPos(XMFLOAT2(x, y += step)); 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(); chunks.clear(); scene->Entity_Remove(terrainEntity); scene->transforms.Create(terrainEntity); scene->names.Create(terrainEntity) = "terrain"; 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; weather.cloud_shadow_amount = 0.5f; weather.cloud_shadow_scale = 0.003f; weather.cloud_shadow_speed = 0.25f; weather.stars = 0.6f; } 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 = 8; light.SetCastShadow(true); //light.SetVolumetricsEnabled(true); TransformComponent& transform = *scene->transforms.GetComponent(sunEntity); transform.RotateRollPitchYaw(XMFLOAT3(XM_PIDIV4, 0, XM_PIDIV4)); transform.Translate(XMFLOAT3(0, 2, 0)); } } void TerrainGenerator::Generation_Update(const wi::scene::CameraComponent& camera) { // 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 scene->Merge(generation_scene); if (centerToCamCheckBox.GetCheck()) { center_chunk.x = (int)std::floor((camera.Eye.x + chunk_half_width) * chunk_width_rcp); center_chunk.z = (int)std::floor((camera.Eye.z + chunk_half_width) * chunk_width_rcp); } const int removal_threshold = (int)generationSlider.GetValue() + 2; const float texlodMultiplier = texlodSlider.GetValue(); GraphicsDevice* device = GetDevice(); virtual_texture_updates.clear(); virtual_texture_barriers_begin.clear(); virtual_texture_barriers_end.clear(); // Check whether there are any materials that would write to virtual textures: uint32_t max_texture_resolution = 0; bool virtual_texture_available[MaterialComponent::TEXTURESLOT_COUNT] = {}; virtual_texture_available[MaterialComponent::SURFACEMAP] = true; // this is always needed to bake individual material properties MaterialComponent* virtual_materials[4] = { &material_Base, &material_Slope, &material_LowAltitude, &material_HighAltitude, }; for (auto& material : virtual_materials) { for (int i = 0; i < MaterialComponent::TEXTURESLOT_COUNT; ++i) { switch (i) { case MaterialComponent::BASECOLORMAP: case MaterialComponent::NORMALMAP: case MaterialComponent::SURFACEMAP: if (material->textures[i].resource.IsValid()) { virtual_texture_available[i] = true; const TextureDesc& desc = material->textures[i].resource.GetTexture().GetDesc(); max_texture_resolution = std::max(max_texture_resolution, desc.width); max_texture_resolution = std::max(max_texture_resolution, desc.height); } break; default: break; } } } 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)); // chunk removal: if (removalCheckBox.GetCheck()) { 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) { scene->Entity_Remove(chunk_data.grass_entity); chunk_data.grass_exists = false; // grass can be generated here by generation thread... } } } } // Collect virtual texture update requests: if (max_texture_resolution > 0) { uint32_t texture_lod = 0; const float distsq = wi::math::DistanceSquared(camera.Eye, chunk_data.sphere.center); const float radius = chunk_data.sphere.radius; const float radiussq = radius * radius; if (distsq < radiussq) { texture_lod = 0; } else { const float dist = std::sqrt(distsq); const float dist_to_sphere = dist - radius; texture_lod = uint32_t(dist_to_sphere * texlodMultiplier); } uint32_t chunk_required_texture_resolution = uint32_t(max_texture_resolution / std::pow(2.0f, (float)std::max(0u, texture_lod))); chunk_required_texture_resolution = std::max(8u, chunk_required_texture_resolution); if (chunk_data.virtual_texture_resolution != chunk_required_texture_resolution) { chunk_data.virtual_texture_resolution = chunk_required_texture_resolution; MaterialComponent* material = scene->materials.GetComponent(chunk_data.entity); if (material != nullptr) { for (int i = 0; i < MaterialComponent::TEXTURESLOT_COUNT; ++i) { if (virtual_texture_available[i]) { TextureDesc desc; desc.width = chunk_required_texture_resolution; desc.height = chunk_required_texture_resolution; desc.format = Format::R8G8B8A8_UNORM; desc.bind_flags = BindFlag::SHADER_RESOURCE | BindFlag::UNORDERED_ACCESS; Texture texture; bool success = device->CreateTexture(&desc, nullptr, &texture); assert(success); material->textures[i].resource.SetTexture(texture); virtual_texture_barriers_begin.push_back(GPUBarrier::Image(&material->textures[i].resource.GetTexture(), desc.layout, ResourceState::UNORDERED_ACCESS)); virtual_texture_barriers_end.push_back(GPUBarrier::Image(&material->textures[i].resource.GetTexture(), ResourceState::UNORDERED_ACCESS, desc.layout)); } } virtual_texture_updates.push_back(chunk); } } } it++; } // Execute batched virtual texture updates: if (!virtual_texture_updates.empty()) { CommandList cmd = device->BeginCommandList(); device->EventBegin("TerrainVirtualTextureUpdate", cmd); auto range = wi::profiler::BeginRangeGPU("TerrainVirtualTextureUpdate", cmd); device->Barrier(virtual_texture_barriers_begin.data(), (uint32_t)virtual_texture_barriers_begin.size(), cmd); device->BindComputeShader(wi::renderer::GetShader(wi::enums::CSTYPE_TERRAIN_VIRTUALTEXTURE_UPDATE), cmd); ShaderMaterial materials[4]; material_Base.WriteShaderMaterial(&materials[0]); material_Slope.WriteShaderMaterial(&materials[1]); material_LowAltitude.WriteShaderMaterial(&materials[2]); material_HighAltitude.WriteShaderMaterial(&materials[3]); device->BindDynamicConstantBuffer(materials, 10, cmd); for (auto& chunk : virtual_texture_updates) { auto it = chunks.find(chunk); if (it == chunks.end()) continue; ChunkData& chunk_data = it->second; const GPUResource* res[] = { &chunk_data.region_weights_texture, }; device->BindResources(res, 0, arraysize(res), cmd); const MaterialComponent* material = scene->materials.GetComponent(chunk_data.entity); if (material != nullptr) { for (int i = 0; i < MaterialComponent::TEXTURESLOT_COUNT; ++i) { if (virtual_texture_available[i]) { device->BindUAV(material->textures[i].GetGPUResource(), i, cmd); } } } device->Dispatch(chunk_data.virtual_texture_resolution / 8u, chunk_data.virtual_texture_resolution / 8u, 1, cmd); } device->Barrier(virtual_texture_barriers_end.data(), (uint32_t)virtual_texture_barriers_end.size(), cmd); wi::profiler::EndRange(range); device->EventEnd(cmd); } // Start the generation on a background thread and keep it running until the next frame wi::jobsystem::Execute(generation_workload, [=](wi::jobsystem::JobArgs args) { wi::Timer timer; 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(); bool generated_something = false; 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_scene.Component_Attach(chunk_data.entity, terrainEntity); TransformComponent& transform = *generation_scene.transforms.GetComponent(chunk_data.entity); transform.ClearTransform(); const XMFLOAT3 chunk_pos = XMFLOAT3(float(chunk.x * (chunk_width - 1)), 0, float(chunk.z * (chunk_width - 1))); transform.Translate(chunk_pos); transform.UpdateTransform(); MaterialComponent& material = generation_scene.materials.Create(chunk_data.entity); // material params will be 1 because they will be created from only texture maps // because region materials are blended together into one texture material.SetRoughness(1); material.SetMetalness(1); material.SetReflectance(1); MeshComponent& mesh = generation_scene.meshes.Create(chunk_data.entity); object.meshID = chunk_data.entity; mesh.indices = indices; for (auto& lod : lods) { mesh.subsets.emplace_back(); mesh.subsets.back().materialID = chunk_data.entity; 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_uvset_0.resize(vertexCount); wi::HairParticleSystem grass = grass_properties; 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, chunk_width, [&](wi::jobsystem::JobArgs args) { uint32_t index = args.jobIndex; const float x = float(index % chunk_width) - chunk_half_width; const float z = float(index / chunk_width) - chunk_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; chunk_data.region_weights[index] = wi::Color::fromFloat4(materialBlendWeights); mesh.vertex_positions[index] = XMFLOAT3(x, height, z); mesh.vertex_normals[index] = normal; const XMFLOAT2 uv = XMFLOAT2(x * chunk_width_rcp + 0.5f, z * chunk_width_rcp + 0.5f); mesh.vertex_uvset_0[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), 8.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 wi::jobsystem::Execute(ctx, [&](wi::jobsystem::JobArgs args) { mesh.CreateRenderData(); chunk_data.sphere.center = mesh.aabb.getCenter(); chunk_data.sphere.center.x += chunk_pos.x; chunk_data.sphere.center.y += chunk_pos.y; chunk_data.sphere.center.z += chunk_pos.z; chunk_data.sphere.radius = mesh.aabb.getRadius(); }); // 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) { chunk_data.grass_entity = CreateEntity(); 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) chunk_data.grass.meshID = chunk_data.entity; chunk_data.grass.strandCount = grass_valid_vertex_count.load() * 3; chunk_data.grass.viewDistance = chunk_width; } // 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 XMFLOAT4 region0 = chunk_data.region_weights[ind0]; const XMFLOAT4 region1 = chunk_data.region_weights[ind1]; const XMFLOAT4 region2 = chunk_data.region_weights[ind2]; // 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); } } } // Create the blend weights texture for virtual texture update: { TextureDesc desc; desc.width = (uint32_t)chunk_width; desc.height = (uint32_t)chunk_width; desc.format = Format::R8G8B8A8_UNORM; desc.bind_flags = BindFlag::SHADER_RESOURCE; SubresourceData data; data.data_ptr = chunk_data.region_weights; data.row_pitch = chunk_width * sizeof(chunk_data.region_weights[0]); bool success = device->CreateTexture(&desc, &data, &chunk_data.region_weights_texture); assert(success); } wi::jobsystem::Wait(ctx); // wait until mesh.CreateRenderData() async task finishes generated_something = true; } // Grass patch placement: const int dist = std::max(std::abs(center_chunk.x - chunk.x), std::abs(center_chunk.z - chunk.z)); if (dist <= 1) { 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) { if (!chunk_data.grass_exists) { // add patch for this chunk wi::HairParticleSystem& grass = generation_scene.hairs.Create(chunk_data.grass_entity); grass = chunk_data.grass; generation_scene.materials.Create(chunk_data.grass_entity) = material_GrassParticle; generation_scene.transforms.Create(chunk_data.grass_entity); generation_scene.names.Create(chunk_data.grass_entity) = "grass"; generation_scene.Component_Attach(chunk_data.grass_entity, chunk_data.entity, true); chunk_data.grass_exists = true; // don't generate more grass here generated_something = true; } } } } if (generated_something && timer.elapsed_milliseconds() > generation_time_budget_milliseconds) { generation_cancelled.store(true); } }; // 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 } void TerrainGenerator::BakeVirtualTexturesToFiles() { if (terrainEntity == INVALID_ENTITY) { return; } wi::jobsystem::context ctx; static const std::string extension = "PNG"; for (auto it = chunks.begin(); it != chunks.end(); it++) { const Chunk& chunk = it->first; ChunkData& chunk_data = it->second; MaterialComponent* material = scene->materials.GetComponent(chunk_data.entity); if (material != nullptr) { for (int i = 0; i < MaterialComponent::TEXTURESLOT_COUNT; ++i) { auto& tex = material->textures[i]; switch (i) { case MaterialComponent::BASECOLORMAP: case MaterialComponent::SURFACEMAP: case MaterialComponent::NORMALMAP: if (tex.name.empty() && tex.GetGPUResource() != nullptr) { wi::vector filedata; if (wi::helper::saveTextureToMemory(tex.resource.GetTexture(), filedata)) { tex.resource.SetFileData(std::move(filedata)); wi::jobsystem::Execute(ctx, [i, &tex, chunk](wi::jobsystem::JobArgs args) { wi::vector filedata_ktx2; if (wi::helper::saveTextureToMemoryFile(tex.resource.GetFileData(), tex.resource.GetTexture().desc, extension, filedata_ktx2)) { tex.name = std::to_string(chunk.x) + "_" + std::to_string(chunk.z); switch (i) { case MaterialComponent::BASECOLORMAP: tex.name += "_basecolormap"; break; case MaterialComponent::SURFACEMAP: tex.name += "_surfacemap"; break; case MaterialComponent::NORMALMAP: tex.name += "_normalmap"; break; default: break; } tex.name += "." + extension; tex.resource = wi::resourcemanager::Load(tex.name, wi::resourcemanager::Flags::IMPORT_RETAIN_FILEDATA, filedata_ktx2.data(), filedata_ktx2.size()); } }); } } break; default: break; } } } } wi::helper::messageBox("Baking terrain virtual textures, this could take a while!", "Attention!"); wi::jobsystem::Wait(ctx); for (auto it = chunks.begin(); it != chunks.end(); it++) { const Chunk& chunk = it->first; ChunkData& chunk_data = it->second; MaterialComponent* material = scene->materials.GetComponent(chunk_data.entity); if (material != nullptr) { material->CreateRenderData(); } } }