Files
WickedEngine/Editor/TerrainWindow.cpp
T
2026-01-14 13:16:17 +01:00

1869 lines
63 KiB
C++

#include "stdafx.h"
#include "TerrainWindow.h"
#include "Utility/stb_image.h"
using namespace wi::ecs;
using namespace wi::scene;
ModifierWindow::ModifierWindow(const std::string& name)
{
wi::gui::Window::Create(name, wi::gui::Window::WindowControls::CLOSE_AND_COLLAPSE | wi::gui::Window::WindowControls::FIT_ALL_WIDGETS_VERTICAL);
SetSize(XMFLOAT2(200, 100));
blendCombo.Create("Blend: ");
blendCombo.SetSize(XMFLOAT2(100, 20));
blendCombo.AddItem("Normal", (uint64_t)wi::terrain::Modifier::BlendMode::Normal);
blendCombo.AddItem("Additive", (uint64_t)wi::terrain::Modifier::BlendMode::Additive);
blendCombo.AddItem("Multiply", (uint64_t)wi::terrain::Modifier::BlendMode::Multiply);
blendCombo.OnSelect([=](wi::gui::EventArgs args) {
modifier->blend = (wi::terrain::Modifier::BlendMode)args.userdata;
generation_callback();
});
AddWidget(&blendCombo);
weightSlider.Create(0, 1, 0.5f, 10000, "Weight: ");
weightSlider.SetSize(XMFLOAT2(100, 20));
weightSlider.SetTooltip("Blending amount");
weightSlider.OnSlide([=](wi::gui::EventArgs args) {
modifier->weight = args.fValue;
generation_callback();
});
AddWidget(&weightSlider);
scaleSlider.Create(0.1f, 10000, 1000, 10000, "Scale: ");
scaleSlider.SetSize(XMFLOAT2(100, 20));
scaleSlider.SetTooltip("Horizontal world scale of the modifier");
scaleSlider.OnSlide([=](wi::gui::EventArgs args) {
modifier->SetScale(args.fValue);
generation_callback();
});
AddWidget(&scaleSlider);
}
void ModifierWindow::Bind(wi::terrain::Modifier* ptr)
{
modifier = ptr;
modifier->blend = (wi::terrain::Modifier::BlendMode)blendCombo.GetItemUserData(blendCombo.GetSelected());
modifier->weight = weightSlider.GetValue();
modifier->SetScale(scaleSlider.GetValue());
}
void ModifierWindow::From(wi::terrain::Modifier* ptr)
{
modifier = ptr;
blendCombo.SetSelectedByUserdataWithoutCallback((uint64_t)ptr->blend);
weightSlider.SetValue(ptr->weight);
scaleSlider.SetValue(ptr->GetScale());
}
PerlinModifierWindow::PerlinModifierWindow() : ModifierWindow("Perlin Noise")
{
octavesSlider.Create(1, 8, 6, 7, "Octaves: ");
octavesSlider.SetTooltip("Octave count for the perlin noise");
octavesSlider.SetSize(XMFLOAT2(100, 20));
octavesSlider.OnSlide([=](wi::gui::EventArgs args) {
((wi::terrain::PerlinModifier*)modifier)->octaves = args.iValue;
generation_callback();
});
AddWidget(&octavesSlider);
SetSize(XMFLOAT2(200, 140));
SetCollapsed(true);
}
void PerlinModifierWindow::ResizeLayout()
{
ModifierWindow::ResizeLayout();
layout.margin_left = 100;
layout.add(blendCombo);
layout.add(weightSlider);
layout.add(scaleSlider);
layout.add(octavesSlider);
}
void PerlinModifierWindow::Bind(wi::terrain::PerlinModifier* ptr)
{
ModifierWindow::Bind(ptr);
ptr->octaves = (int)octavesSlider.GetValue();
}
void PerlinModifierWindow::From(wi::terrain::PerlinModifier* ptr)
{
ModifierWindow::From(ptr);
octavesSlider.SetValue((float)ptr->octaves);
}
VoronoiModifierWindow::VoronoiModifierWindow() : ModifierWindow("Voronoi Noise")
{
fadeSlider.Create(0, 100, 2.59f, 10000, "Fade: ");
fadeSlider.SetTooltip("Fade out voronoi regions by distance from cell's center");
fadeSlider.SetSize(XMFLOAT2(100, 20));
fadeSlider.OnSlide([=](wi::gui::EventArgs args) {
((wi::terrain::VoronoiModifier*)modifier)->fade = args.fValue;
generation_callback();
});
AddWidget(&fadeSlider);
shapeSlider.Create(0, 1, 0.7f, 10000, "Shape: ");
shapeSlider.SetTooltip("How much the voronoi shape will be kept");
shapeSlider.SetSize(XMFLOAT2(100, 20));
shapeSlider.OnSlide([=](wi::gui::EventArgs args) {
((wi::terrain::VoronoiModifier*)modifier)->shape = args.fValue;
generation_callback();
});
AddWidget(&shapeSlider);
falloffSlider.Create(0, 8, 6, 10000, "Falloff: ");
falloffSlider.SetTooltip("Controls the falloff of the voronoi distance fade effect");
falloffSlider.SetSize(XMFLOAT2(100, 20));
falloffSlider.OnSlide([=](wi::gui::EventArgs args) {
((wi::terrain::VoronoiModifier*)modifier)->falloff = args.fValue;
generation_callback();
});
AddWidget(&falloffSlider);
perturbationSlider.Create(0, 1, 0.1f, 10000, "Perturbation: ");
perturbationSlider.SetTooltip("Controls the random look of voronoi region edges");
perturbationSlider.SetSize(XMFLOAT2(100, 20));
perturbationSlider.OnSlide([=](wi::gui::EventArgs args) {
((wi::terrain::VoronoiModifier*)modifier)->perturbation = args.fValue;
generation_callback();
});
AddWidget(&perturbationSlider);
SetSize(XMFLOAT2(200, 200));
SetCollapsed(true);
}
void VoronoiModifierWindow::ResizeLayout()
{
ModifierWindow::ResizeLayout();
layout.margin_left = 100;
layout.add(blendCombo);
layout.add(weightSlider);
layout.add(scaleSlider);
layout.add(fadeSlider);
layout.add(shapeSlider);
layout.add(falloffSlider);
layout.add(perturbationSlider);
}
void VoronoiModifierWindow::Bind(wi::terrain::VoronoiModifier* ptr)
{
ModifierWindow::Bind(ptr);
ptr->fade = fadeSlider.GetValue();
ptr->shape = shapeSlider.GetValue();
ptr->falloff = falloffSlider.GetValue();
ptr->perturbation = perturbationSlider.GetValue();
}
void VoronoiModifierWindow::From(wi::terrain::VoronoiModifier* ptr)
{
ModifierWindow::From(ptr);
fadeSlider.SetValue(ptr->fade);
shapeSlider.SetValue(ptr->shape);
falloffSlider.SetValue(ptr->falloff);
perturbationSlider.SetValue(ptr->perturbation);
}
HeightmapModifierWindow::HeightmapModifierWindow() : ModifierWindow("Heightmap")
{
weightSlider.SetValue(1);
amountSlider.SetValue(1000);
amountSlider.Create(0, 1, 0.1f, 1000, "Amount: ");
amountSlider.SetSize(XMFLOAT2(100, 20));
amountSlider.OnSlide([=](wi::gui::EventArgs args) {
((wi::terrain::HeightmapModifier*)modifier)->amount = args.fValue;
generation_callback();
});
AddWidget(&amountSlider);
loadButton.Create("Load Heightmap...");
loadButton.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.\nIt is recommended to use a 16-bit PNG for heightmaps.");
loadButton.OnClick([=](wi::gui::EventArgs args) {
wi::helper::FileDialogParams params;
params.type = wi::helper::FileDialogParams::OPEN;
params.description = "*.png";
params.extensions = { "PNG" };
wi::helper::FileDialog(params, [=](std::string fileName) {
wi::eventhandler::Subscribe_Once(wi::eventhandler::EVENT_THREAD_SAFE_POINT, [=](uint64_t userdata) {
auto* heightmap_modifier = (wi::terrain::HeightmapModifier*)modifier;
heightmap_modifier->data.clear();
heightmap_modifier->width = 0;
heightmap_modifier->height = 0;
int bpp = 0;
wi::vector<uint8_t> filedata;
if (!wi::helper::FileRead(fileName, filedata))
{
wi::backlog::post("Heightmap loading failed, file couldn't be found: " + fileName, wi::backlog::LogLevel::Error);
}
if (stbi_is_16_bit_from_memory((const stbi_uc*)filedata.data(), (int)filedata.size()))
{
stbi_us* rgba = stbi_load_16_from_memory((const stbi_uc*)filedata.data(), (int)filedata.size(), &heightmap_modifier->width, &heightmap_modifier->height, &bpp, 1);
if (rgba != nullptr)
{
heightmap_modifier->data.resize(heightmap_modifier->width * heightmap_modifier->height * sizeof(uint16_t));
std::memcpy(heightmap_modifier->data.data(), rgba, heightmap_modifier->data.size());
stbi_image_free(rgba);
generation_callback(); // callback after heightmap load confirmation
}
else
{
wi::backlog::post("Heightmap loading unknown failure for 16-bit image: " + fileName, wi::backlog::LogLevel::Error);
}
}
else
{
stbi_uc* rgba = stbi_load_from_memory((const stbi_uc*)filedata.data(), (int)filedata.size(), &heightmap_modifier->width, &heightmap_modifier->height, &bpp, 1);
if (rgba != nullptr)
{
heightmap_modifier->data.resize(heightmap_modifier->width * heightmap_modifier->height * sizeof(uint8_t));
std::memcpy(heightmap_modifier->data.data(), rgba, heightmap_modifier->data.size());
generation_callback(); // callback after heightmap load confirmation
}
else
{
wi::backlog::post("Heightmap loading failure for 8-bit image: " + fileName, wi::backlog::LogLevel::Error);
}
}
});
});
});
AddWidget(&loadButton);
SetSize(XMFLOAT2(200, 180));
SetCollapsed(true);
}
void HeightmapModifierWindow::ResizeLayout()
{
ModifierWindow::ResizeLayout();
layout.margin_left = 100;
layout.add(blendCombo);
layout.add(weightSlider);
layout.add(scaleSlider);
layout.add(amountSlider);
layout.add(loadButton);
}
void HeightmapModifierWindow::Bind(wi::terrain::HeightmapModifier* ptr)
{
ModifierWindow::Bind(ptr);
ptr->amount = amountSlider.GetValue();
}
void HeightmapModifierWindow::From(wi::terrain::HeightmapModifier* ptr)
{
ModifierWindow::From(ptr);
amountSlider.SetValue(ptr->amount);
}
PropWindow::PropWindow(wi::terrain::Terrain* terrain, wi::terrain::Prop* prop, wi::scene::Scene* scene)
:prop(prop)
,scene(scene)
{
if (terrain == nullptr)
return;
std::string windowName = "Prop: ";
std::string propName = "NONE";
Entity entity = INVALID_ENTITY;
if(!prop->data.empty()) // extract object name
{
wi::Archive archive = wi::Archive(prop->data.data(), prop->data.size());
EntitySerializer serializer;
entity = scene->Entity_Serialize(
archive,
serializer,
INVALID_ENTITY,
wi::scene::Scene::EntitySerializeFlags::RECURSIVE |
wi::scene::Scene::EntitySerializeFlags::KEEP_INTERNAL_ENTITY_REFERENCES
);
const NameComponent* name = scene->names.GetComponent(entity);
if (name != nullptr)
{
propName = name->name;
windowName += propName;
}
scene->Entity_Remove(entity);
}
wi::gui::Window::Create(windowName, wi::gui::Window::WindowControls::CLOSE_AND_COLLAPSE | wi::gui::Window::WindowControls::FIT_ALL_WIDGETS_VERTICAL);
constexpr auto elementSize = XMFLOAT2(100, 20);
meshCombo.Create("Object: ");
meshCombo.SetTooltip("Select object component");
meshCombo.SetSize(elementSize);
meshCombo.AddItem(propName, entity);
for (size_t i = 0; i < scene->objects.GetCount(); ++i)
{
const Entity ent = scene->objects.GetEntity(i);
const NameComponent* name = scene->names.GetComponent(ent);
if (!scene->Entity_IsDescendant(ent, terrain->chunkGroupEntity))
{
meshCombo.AddItem(name != nullptr ? name->name : std::to_string(ent), ent);
}
}
AddWidget(&meshCombo);
meshCombo.OnSelect([=](wi::gui::EventArgs args) {
if(args.userdata == entity)
{
return;
}
const auto name = "Prop: " + args.sValue;
SetName(name);
SetText(name);
label.SetText(name);
const wi::ecs::Entity ent = static_cast<wi::ecs::Entity>(args.userdata);
wi::Archive archive;
EntitySerializer serializer;
scene->Entity_Serialize(
archive,
serializer,
ent,
wi::scene::Scene::EntitySerializeFlags::RECURSIVE | wi::scene::Scene::EntitySerializeFlags::KEEP_INTERNAL_ENTITY_REFERENCES
);
archive.WriteData(prop->data);
prop->source_entity = ent;
generation_callback();
});
minCountPerChunkInput.Create("");
minCountPerChunkInput.SetDescription("Min count per chunk: ");
minCountPerChunkInput.SetSize(elementSize);
minCountPerChunkInput.SetValue(prop->min_count_per_chunk);
minCountPerChunkInput.SetTooltip("A chunk will try to generate min this many props of this type");
minCountPerChunkInput.OnInputAccepted([=](wi::gui::EventArgs args) {
prop->min_count_per_chunk = std::min(prop->max_count_per_chunk, args.iValue);
generation_callback();
});
AddWidget(&minCountPerChunkInput);
maxCountPerChunkInput.Create("");
maxCountPerChunkInput.SetDescription("Max count per chunk: ");
maxCountPerChunkInput.SetSize(elementSize);
maxCountPerChunkInput.SetValue(prop->max_count_per_chunk);
maxCountPerChunkInput.SetTooltip("A chunk will try to generate max this many props of this type");
maxCountPerChunkInput.OnInputAccepted([=](wi::gui::EventArgs args) {
prop->max_count_per_chunk = std::max(prop->min_count_per_chunk, args.iValue);
generation_callback();
});
AddWidget(&maxCountPerChunkInput);
regionCombo.Create("Region: ");
regionCombo.SetTooltip("Select a terrain region");
regionCombo.SetSize(elementSize);
regionCombo.AddItem("Base", 0);
regionCombo.AddItem("Slopes", 1);
regionCombo.AddItem("Low altitude ", 2);
regionCombo.AddItem("High altitude", 3);
regionCombo.OnSelect([=](wi::gui::EventArgs args) {
prop->region = static_cast<int>(args.userdata);
generation_callback();
});
AddWidget(&regionCombo);
regionPowerSlider.Create(0.0f, 1.0f, 1.0f, 1000, "Region power: ");
regionPowerSlider.SetSize(elementSize);
regionPowerSlider.SetValue(prop->region_power);
regionPowerSlider.SetTooltip("Region weight affection power factor");
regionPowerSlider.OnSlide([=](wi::gui::EventArgs args) {
prop->region_power = args.fValue;
generation_callback();
});
AddWidget(&regionPowerSlider);
noiseFrequencySlider.Create(0.0f, 1.0f, 1.0f, 1000, "Noise frequency: ");
noiseFrequencySlider.SetSize(elementSize);
noiseFrequencySlider.SetValue(prop->noise_frequency);
noiseFrequencySlider.SetTooltip("Perlin noise's frequency for placement factor");
noiseFrequencySlider.OnSlide([=](wi::gui::EventArgs args) {
prop->noise_frequency = args.fValue;
generation_callback();
});
AddWidget(&noiseFrequencySlider);
noisePowerSlider.Create(0.0f, 1.0f, 1.0f, 1000, "Noise pwer: ");
noisePowerSlider.SetSize(elementSize);
noisePowerSlider.SetValue(prop->noise_power);
noisePowerSlider.SetTooltip("Perlin noise's power");
noisePowerSlider.OnSlide([=](wi::gui::EventArgs args) {
prop->noise_power = args.fValue;
generation_callback();
});
AddWidget(&noisePowerSlider);
thresholdSlider.Create(0.0f, 1.0f, 0.5f, 1000, "Threshold: ");
thresholdSlider.SetSize(elementSize);
thresholdSlider.SetValue(prop->threshold);
thresholdSlider.SetTooltip("The chance of placement (higher is less chance)");
thresholdSlider.OnSlide([=](wi::gui::EventArgs args) {
prop->threshold = args.fValue;
generation_callback();
});
AddWidget(&thresholdSlider);
minSizeSlider.Create(0.0f, 10.0f, 1.0f, 1000, "Min size: ");
minSizeSlider.SetSize(elementSize);
minSizeSlider.SetValue(prop->min_size);
minSizeSlider.SetTooltip("Scaling randomization range min");
minSizeSlider.OnSlide([=](wi::gui::EventArgs args) {
prop->min_size = std::min(args.fValue, prop->max_size);
generation_callback();
});
AddWidget(&minSizeSlider);
maxSizeSlider.Create(0.0f, 10.0f, 1.0f, 1000, "Max size: ");
maxSizeSlider.SetSize(elementSize);
maxSizeSlider.SetValue(prop->max_size);
maxSizeSlider.SetTooltip("Scaling randomization range max");
maxSizeSlider.OnSlide([=](wi::gui::EventArgs args) {
prop->max_size = std::max(args.fValue, prop->min_size);
generation_callback();
});
AddWidget(&maxSizeSlider);
minYOffsetInput.Create("");
minYOffsetInput.SetDescription("Min vertical offset: ");
minYOffsetInput.SetSize(elementSize);
minYOffsetInput.SetValue(prop->min_y_offset);
minYOffsetInput.SetTooltip("Minimal randomized offset on vertical axis");
minYOffsetInput.OnInputAccepted([=](wi::gui::EventArgs args) {
prop->min_y_offset = std::min(args.fValue, prop->max_y_offset);
generation_callback();
});
AddWidget(&minYOffsetInput);
maxYOffsetInput.Create("");
maxYOffsetInput.SetDescription("Max vertical offset: ");
maxYOffsetInput.SetSize(elementSize);
maxYOffsetInput.SetValue(prop->max_y_offset);
maxYOffsetInput.SetTooltip("Maximum randomized offset on vertical axis");
maxYOffsetInput.OnInputAccepted([=](wi::gui::EventArgs args) {
prop->max_y_offset = std::max(args.fValue, prop->min_y_offset);
generation_callback();
});
AddWidget(&maxYOffsetInput);
SetSize(XMFLOAT2(200, 312));
SetCollapsed(true);
}
void PropWindow::ResizeLayout()
{
wi::gui::Window::ResizeLayout();
layout.margin_left = 150;
layout.add(meshCombo);
layout.add(minCountPerChunkInput);
layout.add(maxCountPerChunkInput);
layout.add(regionCombo);
layout.add(regionPowerSlider);
layout.add(noiseFrequencySlider);
layout.add(noisePowerSlider);
layout.add(thresholdSlider);
layout.add(minSizeSlider);
layout.add(maxSizeSlider);
layout.add(minYOffsetInput);
layout.add(maxYOffsetInput);
}
PropsWindow::PropsWindow(EditorComponent* editor)
:editor(editor)
{
wi::gui::Window::Create("Props", wi::gui::Window::WindowControls::COLLAPSE);
SetCollapsed(true);
addButton.Create("Add prop");
addButton.SetSize(XMFLOAT2(100, 20));
addButton.OnClick([this](wi::gui::EventArgs args) {
terrain->props.emplace_back();
AddWindow(terrain->props.back());
});
AddWidget(&addButton);
reloadButton.Create("Reload props");
reloadButton.SetSize(XMFLOAT2(100, 20));
reloadButton.SetTooltip("Reload all props from their source entities to update with latest component changes");
reloadButton.OnClick([editor](wi::gui::EventArgs args) {
editor->ReloadTerrainProps();
});
AddWidget(&reloadButton);
SetSize(XMFLOAT2(420, 332));
}
void PropsWindow::Rebuild()
{
for(auto& window : windows)
{
RemoveWidget(window.get());
}
windows.clear();
windows_to_remove.clear();
if(terrain == nullptr)
{
return;
}
generation_callback = [&] {
terrain->InvalidateProps();
};
for(auto i = terrain->props.begin(); i != terrain->props.end(); ++i)
{
AddWindow(*i);
}
}
void PropsWindow::AddWindow(wi::terrain::Prop& prop)
{
PropWindow* wnd = new PropWindow(terrain, &prop, &editor->GetCurrentScene());
wnd->generation_callback = generation_callback;
wnd->OnClose([this, wnd](wi::gui::EventArgs args) {
windows_to_remove.push_back(wnd);
});
AddWidget(wnd);
windows.emplace_back().reset(wnd);
editor->generalWnd.RefreshTheme();
}
void PropsWindow::Update(const wi::Canvas& canvas, float dt)
{
if(windows.size() != terrain->props.size())
{
// recreate all windows
Rebuild();
}
else
{
if(!windows_to_remove.empty())
{
for(const auto& window : windows_to_remove)
{
for (size_t i = 0; i < windows.size(); ++i)
{
if (windows[i].get() == window)
{
RemoveWidget(window);
terrain->props.erase(terrain->props.begin() + i);
windows.erase(windows.begin() + i);
break;
}
}
}
// updating props pointers
for(size_t i = 0; i < windows.size(); ++i)
{
windows[i]->prop = &terrain->props[i];
}
windows_to_remove.clear();
generation_callback();
}
}
Window::Update(canvas, dt);
}
void PropsWindow::ResizeLayout()
{
layout.margin_left = 150;
layout.add_fullwidth(addButton);
layout.add_fullwidth(reloadButton);
for(auto& window : windows)
{
layout.add_fullwidth(*window);
window->SetEnabled(true);
}
SetSize(XMFLOAT2(GetScale().x, control_size + layout.y));
wi::gui::Window::ResizeLayout();
}
void TerrainWindow::Create(EditorComponent* _editor)
{
editor = _editor;
RemoveWidgets();
ClearTransform();
wi::gui::Window::Create(ICON_TERRAIN " Terrain", wi::gui::Window::WindowControls::COLLAPSE | wi::gui::Window::WindowControls::CLOSE | wi::gui::Window::WindowControls::FIT_ALL_WIDGETS_VERTICAL);
SetSize(XMFLOAT2(420, 1000));
closeButton.SetTooltip("Delete Terrain.");
OnClose([=](wi::gui::EventArgs args) {
wi::Archive& archive = editor->AdvanceHistory();
archive << EditorComponent::HISTORYOP_COMPONENT_DATA;
editor->RecordEntity(archive, entity);
editor->GetCurrentScene().terrains.Remove(entity);
editor->RecordEntity(archive, entity);
editor->componentsWnd.RefreshEntityTree();
});
float x = 140;
float y = 0;
float step = 25;
float hei = 20;
float wid = 120;
auto generate_callback = [&]() {
terrain->SetCenterToCamEnabled(centerToCamCheckBox.GetCheck());
terrain->SetRemovalEnabled(removalCheckBox.GetCheck());
terrain->SetGrassEnabled(grassCheckBox.GetCheck());
terrain->lod_bias = lodSlider.GetValue();
terrain->generation = (int)generationSlider.GetValue();
terrain->prop_generation = (int)propGenerationSlider.GetValue();
terrain->physics_generation = (int)physicsGenerationSlider.GetValue();
terrain->prop_density = propDensitySlider.GetValue();
terrain->grass_density = grassDensitySlider.GetValue();
terrain->grass_properties.length = grassLengthSlider.GetValue();
terrain->grass_properties.viewDistance = grassDistanceSlider.GetValue();
terrain->chunk_scale = scaleSlider.GetValue();
terrain->seed = (uint32_t)seedSlider.GetValue();
terrain->bottomLevel = bottomLevelSlider.GetValue();
terrain->topLevel = topLevelSlider.GetValue();
terrain->region1 = region1Slider.GetValue();
terrain->region2 = region2Slider.GetValue();
terrain->region3 = region3Slider.GetValue();
terrain->SetGenerationStarted(false);
};
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));
centerToCamCheckBox.SetCheck(true);
centerToCamCheckBox.OnClick([=](wi::gui::EventArgs args) {
terrain->SetCenterToCamEnabled(args.bValue);
});
AddWidget(&centerToCamCheckBox);
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);
removalCheckBox.OnClick([=](wi::gui::EventArgs args) {
terrain->SetRemovalEnabled(args.bValue);
});
AddWidget(&removalCheckBox);
grassCheckBox.Create("Grass: ");
grassCheckBox.SetTooltip("Specify whether grass generation is enabled.");
grassCheckBox.SetSize(XMFLOAT2(hei, hei));
grassCheckBox.SetPos(XMFLOAT2(x, y += step));
grassCheckBox.SetCheck(true);
grassCheckBox.OnClick([=](wi::gui::EventArgs args) {
terrain->SetGrassEnabled(args.bValue);
});
AddWidget(&grassCheckBox);
physicsCheckBox.Create("Physics: ");
physicsCheckBox.SetTooltip("Specify whether physics is enabled for terrain chunks.");
physicsCheckBox.SetSize(XMFLOAT2(hei, hei));
physicsCheckBox.SetPos(XMFLOAT2(x, y += step));
physicsCheckBox.SetCheck(true);
physicsCheckBox.OnClick([=](wi::gui::EventArgs args) {
terrain->SetPhysicsEnabled(args.bValue);
});
AddWidget(&physicsCheckBox);
tessellationCheckBox.Create("Tessellation: ");
tessellationCheckBox.SetTooltip("Specify whether tessellation is enabled for terrain surface.\nTessellation requires GPU hardware support\nTessellation doesn't work with raytracing effects or Visibility Compute Shading rendering mode.");
tessellationCheckBox.SetSize(XMFLOAT2(hei, hei));
tessellationCheckBox.SetPos(XMFLOAT2(x, y += step));
tessellationCheckBox.SetCheck(true);
tessellationCheckBox.OnClick([=](wi::gui::EventArgs args) {
terrain->SetTessellationEnabled(args.bValue);
});
AddWidget(&tessellationCheckBox);
lodSlider.Create(-8, 8, 0, 100, "Chunk LOD Bias: ");
lodSlider.SetTooltip("Set the LOD (Level Of Detail) distance multiplier.\nLow values increase LOD detail in distance");
lodSlider.SetSize(XMFLOAT2(wid, hei));
lodSlider.SetPos(XMFLOAT2(x, y += step));
lodSlider.OnSlide([this](wi::gui::EventArgs args) {
for (auto& it : terrain->chunks)
{
const wi::terrain::ChunkData& chunk_data = it.second;
if (chunk_data.entity != INVALID_ENTITY)
{
ObjectComponent* object = terrain->scene->objects.GetComponent(chunk_data.entity);
if (object != nullptr)
{
object->lod_bias = args.fValue;
}
}
}
terrain->lod_bias = 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(wid, hei));
generationSlider.SetPos(XMFLOAT2(x, y += step));
generationSlider.OnSlide([this](wi::gui::EventArgs args) {
terrain->generation = args.iValue;
});
AddWidget(&generationSlider);
propGenerationSlider.Create(0, 16, 10, 16, "Prop Distance: ");
propGenerationSlider.SetTooltip("How far out props will be generated (value is in number of chunks)");
propGenerationSlider.SetSize(XMFLOAT2(wid, hei));
propGenerationSlider.SetPos(XMFLOAT2(x, y += step));
propGenerationSlider.OnSlide([this](wi::gui::EventArgs args) {
terrain->prop_generation = args.iValue;
});
AddWidget(&propGenerationSlider);
physicsGenerationSlider.Create(0, 16, 3, 16, "Physics Distance: ");
physicsGenerationSlider.SetTooltip("How far out physics meshes will be generated (value is in number of chunks)");
physicsGenerationSlider.SetSize(XMFLOAT2(wid, hei));
physicsGenerationSlider.SetPos(XMFLOAT2(x, y += step));
physicsGenerationSlider.OnSlide([this](wi::gui::EventArgs args) {
terrain->physics_generation = args.iValue;
});
AddWidget(&physicsGenerationSlider);
propDensitySlider.Create(0, 10, 1, 1000, "Prop Density: ");
propDensitySlider.SetTooltip("Modifies overall prop density.");
propDensitySlider.SetSize(XMFLOAT2(wid, hei));
propDensitySlider.SetPos(XMFLOAT2(x, y += step));
propDensitySlider.OnSlide([this](wi::gui::EventArgs args) {
terrain->prop_density = args.fValue;
});
AddWidget(&propDensitySlider);
grassDensitySlider.Create(0, 4, 2, 1000, "Grass Density: ");
grassDensitySlider.SetTooltip("Modifies overall grass density.");
grassDensitySlider.SetSize(XMFLOAT2(wid, hei));
grassDensitySlider.SetPos(XMFLOAT2(x, y += step));
grassDensitySlider.OnSlide([this](wi::gui::EventArgs args) {
terrain->grass_density = args.fValue;
});
AddWidget(&grassDensitySlider);
grassLengthSlider.Create(0, 8, 2, 1000, "Grass Length: ");
grassLengthSlider.SetTooltip("Modifies overall grass length.");
grassLengthSlider.SetSize(XMFLOAT2(wid, hei));
grassLengthSlider.SetPos(XMFLOAT2(x, y += step));
grassLengthSlider.OnSlide([this](wi::gui::EventArgs args) {
terrain->grass_properties.length = args.fValue;
wi::HairParticleSystem* hair = terrain->scene->hairs.GetComponent(terrain->grassEntity);
if (hair != nullptr)
{
hair->length = args.fValue;
}
});
AddWidget(&grassLengthSlider);
grassDistanceSlider.Create(10, 100, 60, 1000, "Grass Distance: ");
grassDistanceSlider.SetTooltip("Modifies grass view distance.");
grassDistanceSlider.SetSize(XMFLOAT2(wid, hei));
grassDistanceSlider.SetPos(XMFLOAT2(x, y += step));
grassDistanceSlider.OnSlide([this](wi::gui::EventArgs args) {
terrain->grass_properties.viewDistance = args.fValue;
wi::HairParticleSystem* hair = terrain->scene->hairs.GetComponent(terrain->grassEntity);
if (hair != nullptr)
{
hair->viewDistance = args.fValue;
}
});
AddWidget(&grassDistanceSlider);
presetCombo.Create("Preset: ");
presetCombo.SetTooltip("Select a terrain preset");
presetCombo.SetSize(XMFLOAT2(wid, 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.AddItem("Desert", PRESET_DESERT);
presetCombo.OnSelect([=](wi::gui::EventArgs args) {
terrain->Generation_Cancel();
for (auto& modifier : modifiers)
{
RemoveWidget(modifier.get());
}
modifiers.clear();
modifiers_to_remove.clear();
terrain->modifiers.clear();
terrain->modifiers_to_remove.clear();
PerlinModifierWindow* perlin = new PerlinModifierWindow;
VoronoiModifierWindow* voronoi = new VoronoiModifierWindow;
perlin->blendCombo.SetSelectedByUserdataWithoutCallback((uint64_t)wi::terrain::Modifier::BlendMode::Additive);
voronoi->blendCombo.SetSelectedByUserdataWithoutCallback((uint64_t)wi::terrain::Modifier::BlendMode::Multiply);
switch (args.userdata)
{
default:
case PRESET_HILLS:
terrain->weather.SetOceanEnabled(false);
seedSlider.SetValue(5415);
bottomLevelSlider.SetValue(-60);
topLevelSlider.SetValue(380);
perlin->weightSlider.SetValue(0.5f);
perlin->scaleSlider.SetValue(1250);
perlin->octavesSlider.SetValue(6);
voronoi->weightSlider.SetValue(0.5f);
voronoi->scaleSlider.SetValue(1000);
voronoi->fadeSlider.SetValue(2.59f);
voronoi->shapeSlider.SetValue(0.7f);
voronoi->falloffSlider.SetValue(6);
voronoi->perturbationSlider.SetValue(0.1f);
region1Slider.SetValue(0.8f);
region2Slider.SetValue(1.2f);
region3Slider.SetValue(100);
break;
case PRESET_ISLANDS:
terrain->weather.SetOceanEnabled(true);
seedSlider.SetValue(8526);
bottomLevelSlider.SetValue(-79);
topLevelSlider.SetValue(520);
perlin->weightSlider.SetValue(0.5f);
perlin->scaleSlider.SetValue(500);
perlin->octavesSlider.SetValue(6);
voronoi->weightSlider.SetValue(0.5f);
voronoi->scaleSlider.SetValue(3154);
voronoi->fadeSlider.SetValue(8.2f);
voronoi->shapeSlider.SetValue(0.126f);
voronoi->falloffSlider.SetValue(1.392f);
voronoi->perturbationSlider.SetValue(0.126f);
region1Slider.SetValue(1);
region2Slider.SetValue(0.2f);
region3Slider.SetValue(100);
break;
case PRESET_MOUNTAINS:
terrain->weather.SetOceanEnabled(false);
seedSlider.SetValue(5213);
bottomLevelSlider.SetValue(0);
topLevelSlider.SetValue(2960);
perlin->weightSlider.SetValue(0.5f);
perlin->scaleSlider.SetValue(699);
perlin->octavesSlider.SetValue(8);
voronoi->weightSlider.SetValue(0.5f);
voronoi->scaleSlider.SetValue(2016);
voronoi->fadeSlider.SetValue(5.2f);
voronoi->shapeSlider.SetValue(0.412f);
voronoi->falloffSlider.SetValue(1.456f);
voronoi->perturbationSlider.SetValue(0.092f);
region1Slider.SetValue(0.7f);
region2Slider.SetValue(2);
region3Slider.SetValue(0.2f);
break;
case PRESET_ARCTIC:
terrain->weather.SetOceanEnabled(false);
seedSlider.SetValue(11597);
bottomLevelSlider.SetValue(-50);
topLevelSlider.SetValue(40);
perlin->weightSlider.SetValue(1);
perlin->scaleSlider.SetValue(500);
perlin->octavesSlider.SetValue(4);
voronoi->weightSlider.SetValue(1);
voronoi->scaleSlider.SetValue(250);
voronoi->fadeSlider.SetValue(1.8f);
voronoi->shapeSlider.SetValue(0.518f);
voronoi->falloffSlider.SetValue(0.2f);
voronoi->perturbationSlider.SetValue(0.298f);
region1Slider.SetValue(1);
region2Slider.SetValue(1);
region3Slider.SetValue(0);
break;
case PRESET_DESERT:
terrain->weather.SetOceanEnabled(false);
seedSlider.SetValue(1597);
bottomLevelSlider.SetValue(-50);
topLevelSlider.SetValue(40);
perlin->weightSlider.SetValue(1);
perlin->scaleSlider.SetValue(500);
perlin->octavesSlider.SetValue(4);
voronoi->weightSlider.SetValue(1);
voronoi->scaleSlider.SetValue(250);
voronoi->fadeSlider.SetValue(1.8f);
voronoi->shapeSlider.SetValue(0.518f);
voronoi->falloffSlider.SetValue(0.2f);
voronoi->perturbationSlider.SetValue(0.298f);
region1Slider.SetValue(8);
region2Slider.SetValue(0);
region3Slider.SetValue(100);
break;
}
wi::allocator::shared_ptr<wi::terrain::PerlinModifier> terrain_perlin = wi::allocator::make_shared_single<wi::terrain::PerlinModifier>();
terrain->modifiers.emplace_back() = terrain_perlin;
wi::allocator::shared_ptr<wi::terrain::VoronoiModifier> terrain_voronoi = wi::allocator::make_shared_single<wi::terrain::VoronoiModifier>();
terrain->modifiers.emplace_back() = terrain_voronoi;
editor->GetCurrentEditorScene().scene.weathers.Clear();
perlin->Bind(terrain_perlin.get());
voronoi->Bind(terrain_voronoi.get());
AddModifier(perlin);
AddModifier(voronoi);
generate_callback();
editor->componentsWnd.RefreshEntityTree();
});
AddWidget(&presetCombo);
addModifierCombo.Create("Add Modifier: ");
addModifierCombo.selected_font.anim.typewriter.looped = true;
addModifierCombo.selected_font.anim.typewriter.time = 2;
addModifierCombo.selected_font.anim.typewriter.character_start = 1;
addModifierCombo.SetTooltip("Add a new modifier that will affect terrain generation.");
addModifierCombo.SetSize(XMFLOAT2(wid, hei));
addModifierCombo.SetPos(XMFLOAT2(x, y += step));
addModifierCombo.SetInvalidSelectionText("...");
addModifierCombo.AddItem("Perlin Noise");
addModifierCombo.AddItem("Voronoi Noise");
addModifierCombo.AddItem("Heightmap Image");
addModifierCombo.OnSelect([=](wi::gui::EventArgs args) {
addModifierCombo.SetSelectedWithoutCallback(-1);
terrain->Generation_Cancel();
switch (args.iValue)
{
default:
break;
case 0:
{
PerlinModifierWindow* ptr = new PerlinModifierWindow;
wi::allocator::shared_ptr<wi::terrain::PerlinModifier> modifier = wi::allocator::make_shared_single<wi::terrain::PerlinModifier>();
terrain->modifiers.push_back(modifier);
ptr->From(modifier.get());
AddModifier(ptr);
}
break;
case 1:
{
VoronoiModifierWindow* ptr = new VoronoiModifierWindow;
wi::allocator::shared_ptr<wi::terrain::VoronoiModifier> modifier = wi::allocator::make_shared_single<wi::terrain::VoronoiModifier>();
terrain->modifiers.push_back(modifier);
ptr->From(modifier.get());
AddModifier(ptr);
}
break;
case 2:
{
HeightmapModifierWindow* ptr = new HeightmapModifierWindow;
wi::allocator::shared_ptr<wi::terrain::HeightmapModifier> modifier = wi::allocator::make_shared_single<wi::terrain::HeightmapModifier>();
terrain->modifiers.push_back(modifier);
ptr->From(modifier.get());
AddModifier(ptr);
}
break;
}
generate_callback();
});
AddWidget(&addModifierCombo);
scaleSlider.Create(1, 10, 1, 9, "Chunk Scale: ");
scaleSlider.SetTooltip("Size of one chunk in horizontal directions.\nLarger chunk scale will cover larger distance, but will have less detail per unit.");
scaleSlider.SetSize(XMFLOAT2(wid, hei));
scaleSlider.SetPos(XMFLOAT2(x, y += step));
scaleSlider.OnSlide([=](wi::gui::EventArgs args) {
terrain->chunk_scale = args.fValue;
generate_callback();
});
AddWidget(&scaleSlider);
seedSlider.Create(1, 12345, 3926, 12344, "Seed: ");
seedSlider.SetTooltip("Seed for terrain randomness");
seedSlider.SetSize(XMFLOAT2(wid, hei));
seedSlider.SetPos(XMFLOAT2(x, y += step));
seedSlider.OnSlide([=](wi::gui::EventArgs args) {
terrain->seed = (uint32_t)args.iValue;
generate_callback();
});
AddWidget(&seedSlider);
bottomLevelSlider.Create(-100, 0, -60, 10000, "Bottom Level: ");
bottomLevelSlider.SetTooltip("Terrain mesh grid lowest level");
bottomLevelSlider.SetSize(XMFLOAT2(wid, hei));
bottomLevelSlider.SetPos(XMFLOAT2(x, y += step));
bottomLevelSlider.OnSlide([=](wi::gui::EventArgs args) {
terrain->bottomLevel = args.fValue;
generate_callback();
});
AddWidget(&bottomLevelSlider);
topLevelSlider.Create(0, 5000, 380, 10000, "Top Level: ");
topLevelSlider.SetTooltip("Terrain mesh grid topmost level");
topLevelSlider.SetSize(XMFLOAT2(wid, hei));
topLevelSlider.SetPos(XMFLOAT2(x, y += step));
topLevelSlider.OnSlide([=](wi::gui::EventArgs args) {
terrain->topLevel = args.fValue;
generate_callback();
});
AddWidget(&topLevelSlider);
saveHeightmapButton.Create("Save Heightmap...");
saveHeightmapButton.SetTooltip("Save a heightmap texture from the currently generated terrain, where the red channel corresponds to terrain height and the resolution to dimensions.\nThe image will be saved as a single channel 16-bit PNG.");
saveHeightmapButton.SetSize(XMFLOAT2(wid, hei));
saveHeightmapButton.SetPos(XMFLOAT2(x, y += step));
AddWidget(&saveHeightmapButton);
saveRegionButton.Create("Save Blendmap...");
saveRegionButton.SetTooltip("Save a color texture from the currently generated terrain where RGBA channels indicate terrain property blend weights.\nNote that you can get a completely transparent image easily if the alpha channel weights are zero and you export to PNG.");
saveRegionButton.SetSize(XMFLOAT2(wid, hei));
saveRegionButton.SetPos(XMFLOAT2(x, y += step));
AddWidget(&saveRegionButton);
region1Slider.Create(0, 2, 1, 1000, "Slope Region: ");
region1Slider.SetTooltip("The region's falloff power");
region1Slider.SetSize(XMFLOAT2(wid, hei));
region1Slider.SetPos(XMFLOAT2(x, y += step));
region1Slider.OnSlide([=](wi::gui::EventArgs args) {
terrain->region1 = args.fValue;
generate_callback();
});
AddWidget(&region1Slider);
region2Slider.Create(0, 2, 1, 1000, "Low Altitude Region: ");
region2Slider.SetTooltip("The region's falloff power");
region2Slider.SetSize(XMFLOAT2(wid, hei));
region2Slider.SetPos(XMFLOAT2(x, y += step));
region2Slider.OnSlide([=](wi::gui::EventArgs args) {
terrain->region2 = args.fValue;
generate_callback();
});
AddWidget(&region2Slider);
region3Slider.Create(0, 2, 1, 1000, "High Altitude Region: ");
region3Slider.SetTooltip("The region's falloff power");
region3Slider.SetSize(XMFLOAT2(wid, hei));
region3Slider.SetPos(XMFLOAT2(x, y += step));
region3Slider.OnSlide([=](wi::gui::EventArgs args) {
terrain->region3 = args.fValue;
generate_callback();
});
AddWidget(&region3Slider);
materialCombos[wi::terrain::MATERIAL_BASE].Create("Base material: ");
materialCombos[wi::terrain::MATERIAL_SLOPE].Create("Slope material: ");
materialCombos[wi::terrain::MATERIAL_LOW_ALTITUDE].Create("Low altitude material: ");
materialCombos[wi::terrain::MATERIAL_HIGH_ALTITUDE].Create("High altitude material: ");
for (size_t i = 0; i < arraysize(materialCombos); ++i)
{
materialCombos[i].SetTooltip("Select material entity");
materialCombos[i].SetSize(XMFLOAT2(wid, hei));
materialCombos[i].SetPos(XMFLOAT2(x, y += step));
materialCombos[i].OnSelect([this, i](wi::gui::EventArgs args) {
const Scene& scene = editor->GetCurrentScene();
wi::ecs::Entity entity = static_cast<wi::ecs::Entity>(args.userdata);
if (entity != INVALID_ENTITY && scene.materials.Contains(entity))
{
if (terrain->materialEntities[i] != entity)
{
terrain->materialEntities[i] = entity;
terrain->Generation_Restart();
}
}
else
{
terrain->materialEntities[i] = INVALID_ENTITY;
}
editor->paintToolWnd.RecreateTerrainMaterialButtons();
});
AddWidget(&materialCombos[i]);
}
materialCombo_GrassParticle.Create("Grass material: ");
materialCombo_GrassParticle.SetTooltip("Select material entity");
materialCombo_GrassParticle.SetSize(XMFLOAT2(wid, hei));
materialCombo_GrassParticle.SetPos(XMFLOAT2(x, y += step));
materialCombo_GrassParticle.OnSelect([this](wi::gui::EventArgs args) {
const Scene& scene = editor->GetCurrentScene();
wi::ecs::Entity entity = static_cast<wi::ecs::Entity>(args.userdata);
if (entity != INVALID_ENTITY && scene.materials.Contains(entity))
{
if (terrain->grassEntity != entity)
{
terrain->grassEntity = entity;
terrain->Generation_Restart();
}
}
else
{
terrain->grassEntity = INVALID_ENTITY;
}
});
AddWidget(&materialCombo_GrassParticle);
propsWindow.reset(new PropsWindow(editor));
AddWidget(propsWindow.get());
saveHeightmapButton.OnClick([=](wi::gui::EventArgs args) {
wi::helper::FileDialogParams params;
params.type = wi::helper::FileDialogParams::SAVE;
params.description = "PNG";
params.extensions = { "PNG" };
wi::helper::FileDialog(params, [=](std::string fileName) {
wi::eventhandler::Subscribe_Once(wi::eventhandler::EVENT_THREAD_SAFE_POINT, [=](uint64_t userdata) {
wi::primitive::AABB aabb;
for (auto& chunk : terrain->chunks)
{
const size_t index = terrain->scene->objects.GetIndex(chunk.second.entity);
if (index == INVALID_INDEX)
continue;
const wi::primitive::AABB& object_aabb = terrain->scene->aabb_objects[index];
aabb = wi::primitive::AABB::Merge(aabb, object_aabb);
}
wi::vector<uint8_t> data;
int width = int(aabb.getHalfWidth().x * 2 + 1);
int height = int(aabb.getHalfWidth().z * 2 + 1);
data.resize(width * height * sizeof(uint16_t));
std::fill(data.begin(), data.end(), 0u);
uint16_t* dest = (uint16_t*)data.data();
for (auto& chunk : terrain->chunks)
{
const ObjectComponent* object = terrain->scene->objects.GetComponent(chunk.second.entity);
if (object != nullptr)
{
const MeshComponent* mesh = terrain->scene->meshes.GetComponent(object->meshID);
if (mesh != nullptr)
{
size_t objectIndex = terrain->scene->objects.GetIndex(chunk.second.entity);
const XMMATRIX W = XMLoadFloat4x4(&terrain->scene->matrix_objects[objectIndex]);
for (auto& x : mesh->vertex_positions)
{
XMVECTOR P = XMLoadFloat3(&x);
P = XMVector3Transform(P, W);
XMFLOAT3 p;
XMStoreFloat3(&p, P);
p.x -= aabb._min.x;
p.z -= aabb._min.z;
int coord = int(p.x) + int(p.z) * width;
dest[coord] = uint16_t(wi::math::InverseLerp(aabb._min.y, aabb._max.y, p.y) * 65535u);
}
}
}
}
std::string extension = wi::helper::GetExtensionFromFileName(fileName);
std::string filename_replaced = fileName;
if (extension != "PNG")
{
filename_replaced = wi::helper::ReplaceExtension(fileName, "PNG");
}
wi::graphics::TextureDesc desc;
desc.width = uint32_t(width);
desc.height = uint32_t(height);
desc.format = wi::graphics::Format::R16_UNORM;
bool success = wi::helper::saveTextureToFile(data, desc, filename_replaced);
assert(success);
if (success)
{
editor->PostSaveText("Exported terrain height map: ", filename_replaced);
}
});
});
});
saveRegionButton.OnClick([=](wi::gui::EventArgs args) {
wi::helper::FileDialogParams params;
params.type = wi::helper::FileDialogParams::SAVE;
params.description = "JPG, PNG";
params.extensions = { "JPG", "PNG" };
wi::helper::FileDialog(params, [=](std::string fileName) {
wi::eventhandler::Subscribe_Once(wi::eventhandler::EVENT_THREAD_SAFE_POINT, [=](uint64_t userdata) {
wi::primitive::AABB aabb;
for (auto& chunk : terrain->chunks)
{
const size_t index = terrain->scene->objects.GetIndex(chunk.second.entity);
if (index == INVALID_INDEX)
continue;
const wi::primitive::AABB& object_aabb = terrain->scene->aabb_objects[index];
aabb = wi::primitive::AABB::Merge(aabb, object_aabb);
}
wi::vector<uint8_t> data;
int width = int(aabb.getHalfWidth().x * 2 + 1);
int height = int(aabb.getHalfWidth().z * 2 + 1);
data.resize(width * height * sizeof(wi::Color));
std::fill(data.begin(), data.end(), 0u);
wi::Color* dest = (wi::Color*)data.data();
for (auto& chunk : terrain->chunks)
{
const wi::terrain::ChunkData& chunk_data = chunk.second;
const ObjectComponent* object = terrain->scene->objects.GetComponent(chunk.second.entity);
if (object != nullptr)
{
const MeshComponent* mesh = terrain->scene->meshes.GetComponent(object->meshID);
if (mesh != nullptr)
{
size_t objectIndex = terrain->scene->objects.GetIndex(chunk.second.entity);
const XMMATRIX W = XMLoadFloat4x4(&terrain->scene->matrix_objects[objectIndex]);
int i = 0;
for (auto& x : mesh->vertex_positions)
{
XMVECTOR P = XMLoadFloat3(&x);
P = XMVector3Transform(P, W);
XMFLOAT3 p;
XMStoreFloat3(&p, P);
p.x -= aabb._min.x;
p.z -= aabb._min.z;
int coord = int(p.x) + int(p.z) * width;
dest[coord] = wi::Color(
chunk_data.blendmap_layers[0].pixels[i],
chunk_data.blendmap_layers[1].pixels[i],
chunk_data.blendmap_layers[2].pixels[i],
chunk_data.blendmap_layers[3].pixels[i]
);
i++;
}
}
}
}
std::string extension = wi::helper::toUpper(wi::helper::GetExtensionFromFileName(fileName));
std::string filename_replaced = fileName;
if (extension != "JPG" && extension != "PNG")
{
filename_replaced = wi::helper::ReplaceExtension(fileName, "JPG");
}
wi::graphics::TextureDesc desc;
desc.width = uint32_t(width);
desc.height = uint32_t(height);
desc.format = wi::graphics::Format::R8G8B8A8_UNORM;
bool success = wi::helper::saveTextureToFile(data, desc, filename_replaced);
assert(success);
if (success)
{
editor->PostSaveText("Exported terrain blend map: ", filename_replaced);
}
});
});
});
SetCollapsed(true);
}
void TerrainWindow::SetEntity(Entity entity)
{
wi::scene::Scene& scene = editor->GetCurrentScene();
if (this->entity == entity)
return;
this->entity = entity;
terrain = scene.terrains.GetComponent(entity);
propsWindow->terrain = terrain;
if (terrain == nullptr)
return;
for (auto& x : modifiers)
{
modifiers_to_remove.push_back(x.get());
}
centerToCamCheckBox.SetCheck(terrain->IsCenterToCamEnabled());
removalCheckBox.SetCheck(terrain->IsRemovalEnabled());
grassCheckBox.SetCheck(terrain->IsGrassEnabled());
physicsCheckBox.SetCheck(terrain->IsPhysicsEnabled());
tessellationCheckBox.SetCheck(terrain->IsTessellationEnabled());
lodSlider.SetValue(terrain->lod_bias);
generationSlider.SetValue((float)terrain->generation);
propGenerationSlider.SetValue((float)terrain->prop_generation);
physicsGenerationSlider.SetValue((float)terrain->physics_generation);
propDensitySlider.SetValue(terrain->prop_density);
grassDensitySlider.SetValue(terrain->grass_density);
grassLengthSlider.SetValue(terrain->grass_properties.length);
grassDistanceSlider.SetValue(terrain->grass_properties.viewDistance);
scaleSlider.SetValue(terrain->chunk_scale);
seedSlider.SetValue((float)terrain->seed);
bottomLevelSlider.SetValue(terrain->bottomLevel);
topLevelSlider.SetValue(terrain->topLevel);
region1Slider.SetValue(terrain->region1);
region2Slider.SetValue(terrain->region2);
region3Slider.SetValue(terrain->region3);
RefreshMaterialComboBoxes();
for (auto& x : terrain->modifiers)
{
switch (x->type)
{
default:
case wi::terrain::Modifier::Type::Perlin:
{
PerlinModifierWindow* modifier = new PerlinModifierWindow;
modifier->From((wi::terrain::PerlinModifier*)x.get());
AddModifier(modifier);
}
break;
case wi::terrain::Modifier::Type::Voronoi:
{
VoronoiModifierWindow* modifier = new VoronoiModifierWindow;
modifier->From((wi::terrain::VoronoiModifier*)x.get());
AddModifier(modifier);
}
break;
case wi::terrain::Modifier::Type::Heightmap:
{
HeightmapModifierWindow* modifier = new HeightmapModifierWindow;
modifier->From((wi::terrain::HeightmapModifier*)x.get());
AddModifier(modifier);
}
break;
}
}
propsWindow->Rebuild();
editor->paintToolWnd.RecreateTerrainMaterialButtons();
}
void TerrainWindow::RefreshMaterialComboBoxes()
{
wi::scene::Scene& scene = editor->GetCurrentScene();
auto fillMaterialCombo = [&](wi::gui::ComboBox& comboBox, Entity selected) {
comboBox.ClearItems();
comboBox.AddItem("NO MATERIAL", INVALID_ENTITY);
for (size_t i = 0; i < scene.materials.GetCount(); ++i)
{
if (scene.materials[i].IsInternal())
{
continue;
}
Entity entity = scene.materials.GetEntity(i);
if (scene.names.Contains(entity))
{
const NameComponent& name = *scene.names.GetComponent(entity);
comboBox.AddItem(name.name, entity);
}
else
{
comboBox.AddItem(std::to_string(entity), entity);
}
if (selected == entity)
{
comboBox.SetSelectedWithoutCallback(int(i + 1));
}
}
};
for (size_t i = 0; i < arraysize(materialCombos); ++i)
{
fillMaterialCombo(materialCombos[i], terrain->materialEntities[i]);
}
fillMaterialCombo(materialCombo_GrassParticle, terrain->grassEntity);
}
void TerrainWindow::AddModifier(ModifierWindow* modifier_window)
{
modifier_window->generation_callback = [=]() {
terrain->Generation_Restart();
};
modifiers.emplace_back().reset(modifier_window);
AddWidget(modifier_window);
modifier_window->OnClose([=](wi::gui::EventArgs args) {
// Can't delete modifier in itself, so add to a deferred deletion queue:
terrain->modifiers_to_remove.push_back(modifier_window->modifier);
modifiers_to_remove.push_back(modifier_window);
});
editor->generalWnd.RefreshTheme();
}
void TerrainWindow::SetupAssets()
{
// Customize terrain generator before it's initialized:
Scene& currentScene = editor->GetCurrentScene();
Entity terrainEntity = CreateEntity();
wi::terrain::Terrain& terrain_preset = currentScene.terrains.Create(terrainEntity);
currentScene.names.Create(terrainEntity) = "terrain";
SetEntity(terrainEntity);
terrain_preset.materialEntities.clear();
terrain_preset.materialEntities.resize(wi::terrain::MATERIAL_COUNT);
for (int i = 0; i < wi::terrain::MATERIAL_COUNT; ++i)
{
terrain_preset.materialEntities[i] = CreateEntity();
currentScene.materials.Create(terrain_preset.materialEntities[i]);
switch (i)
{
case wi::terrain::MATERIAL_BASE:
currentScene.names.Create(terrain_preset.materialEntities[i]) = "Base";
break;
case wi::terrain::MATERIAL_SLOPE:
currentScene.names.Create(terrain_preset.materialEntities[i]) = "Slope";
break;
case wi::terrain::MATERIAL_LOW_ALTITUDE:
currentScene.names.Create(terrain_preset.materialEntities[i]) = "LowAltitude";
break;
case wi::terrain::MATERIAL_HIGH_ALTITUDE:
currentScene.names.Create(terrain_preset.materialEntities[i]) = "HighAltitude";
break;
}
currentScene.Component_Attach(terrain_preset.materialEntities[i], entity);
}
MaterialComponent* material_Base = currentScene.materials.GetComponent(terrain_preset.materialEntities[wi::terrain::MATERIAL_BASE]);
MaterialComponent* material_Slope = currentScene.materials.GetComponent(terrain_preset.materialEntities[wi::terrain::MATERIAL_SLOPE]);
MaterialComponent* material_LowAltitude = currentScene.materials.GetComponent(terrain_preset.materialEntities[wi::terrain::MATERIAL_LOW_ALTITUDE]);
MaterialComponent* material_HighAltitude = currentScene.materials.GetComponent(terrain_preset.materialEntities[wi::terrain::MATERIAL_HIGH_ALTITUDE]);
material_Base->SetRoughness(1);
material_Base->SetReflectance(0.005f);
material_Slope->SetRoughness(0.1f);
material_LowAltitude->SetRoughness(1);
material_HighAltitude->SetRoughness(1);
std::string asset_path = wi::helper::GetCurrentPath() + "/Content/terrain/";
if (!wi::helper::DirectoryExists(asset_path))
{
// Usually in source download, the assets are one level outside of Editor:
asset_path = wi::helper::GetCurrentPath() + "/../Content/terrain/";
}
if (!wi::helper::DirectoryExists(asset_path))
{
// In UWP or older Editor content, it's not in Content folder:
asset_path = wi::helper::GetCurrentPath() + "/terrain/";
}
material_Base->textures[MaterialComponent::BASECOLORMAP].name = asset_path + "base.jpg";
material_Base->textures[MaterialComponent::NORMALMAP].name = asset_path + "base_nor.jpg";
material_Slope->textures[MaterialComponent::BASECOLORMAP].name = asset_path + "slope.jpg";
material_Slope->textures[MaterialComponent::NORMALMAP].name = asset_path + "slope_nor.jpg";
material_LowAltitude->textures[MaterialComponent::BASECOLORMAP].name = asset_path + "low_altitude.jpg";
material_LowAltitude->textures[MaterialComponent::NORMALMAP].name = asset_path + "low_altitude_nor.jpg";
material_HighAltitude->textures[MaterialComponent::BASECOLORMAP].name = asset_path + "high_altitude.jpg";
material_HighAltitude->textures[MaterialComponent::NORMALMAP].name = asset_path + "high_altitude_nor.jpg";
material_Base->SetTextureStreamingDisabled();
material_Slope->SetTextureStreamingDisabled();
material_LowAltitude->SetTextureStreamingDisabled();
material_HighAltitude->SetTextureStreamingDisabled();
// Extra material: rock
{
Entity materialEntity = CreateEntity();
MaterialComponent& mat = currentScene.materials.Create(materialEntity);
currentScene.names.Create(materialEntity) = "Rock";
currentScene.Component_Attach(materialEntity, entity);
mat.textures[MaterialComponent::BASECOLORMAP].name = asset_path + "rock.jpg";
mat.textures[MaterialComponent::NORMALMAP].name = asset_path + "rock_nor.jpg";
mat.roughness = 0.9f;
mat.SetTextureStreamingDisabled();
terrain_preset.materialEntities.push_back(materialEntity);
}
// Extra material: ground
{
Entity materialEntity = CreateEntity();
MaterialComponent& mat = currentScene.materials.Create(materialEntity);
currentScene.names.Create(materialEntity) = "Ground";
currentScene.Component_Attach(materialEntity, entity);
mat.textures[MaterialComponent::BASECOLORMAP].name = asset_path + "ground.jpg";
mat.textures[MaterialComponent::NORMALMAP].name = asset_path + "ground_nor.jpg";
mat.roughness = 0.9f;
mat.SetTextureStreamingDisabled();
terrain_preset.materialEntities.push_back(materialEntity);
}
// Extra material: ground2
{
Entity materialEntity = CreateEntity();
MaterialComponent& mat = currentScene.materials.Create(materialEntity);
currentScene.names.Create(materialEntity) = "Ground2";
currentScene.Component_Attach(materialEntity, entity);
mat.textures[MaterialComponent::BASECOLORMAP].name = asset_path + "ground2.jpg";
mat.textures[MaterialComponent::NORMALMAP].name = asset_path + "ground2_nor.jpg";
mat.roughness = 0.9f;
mat.SetTextureStreamingDisabled();
terrain_preset.materialEntities.push_back(materialEntity);
}
// Extra material: bricks
{
Entity materialEntity = CreateEntity();
MaterialComponent& mat = currentScene.materials.Create(materialEntity);
currentScene.names.Create(materialEntity) = "Bricks";
currentScene.Component_Attach(materialEntity, entity);
mat.textures[MaterialComponent::BASECOLORMAP].name = asset_path + "bricks.jpg";
mat.textures[MaterialComponent::NORMALMAP].name = asset_path + "bricks_nor.jpg";
mat.roughness = 0.9f;
mat.SetTextureStreamingDisabled();
terrain_preset.materialEntities.push_back(materialEntity);
}
// Extra material: darkrock
{
Entity materialEntity = CreateEntity();
MaterialComponent& mat = currentScene.materials.Create(materialEntity);
currentScene.names.Create(materialEntity) = "Dark Rock";
currentScene.Component_Attach(materialEntity, entity);
mat.textures[MaterialComponent::BASECOLORMAP].name = asset_path + "darkrock.jpg";
mat.textures[MaterialComponent::NORMALMAP].name = asset_path + "darkrock_nor.jpg";
mat.roughness = 0.8f;
mat.SetTextureStreamingDisabled();
terrain_preset.materialEntities.push_back(materialEntity);
}
// Extra material: metalplate
{
Entity materialEntity = CreateEntity();
MaterialComponent& mat = currentScene.materials.Create(materialEntity);
currentScene.names.Create(materialEntity) = "Metal Plate";
currentScene.Component_Attach(materialEntity, entity);
mat.textures[MaterialComponent::BASECOLORMAP].name = asset_path + "metalplate.jpg";
mat.textures[MaterialComponent::NORMALMAP].name = asset_path + "metalplate_nor.jpg";
mat.metalness = 1;
mat.roughness = 0.5f;
mat.SetTextureStreamingDisabled();
terrain_preset.materialEntities.push_back(materialEntity);
}
// Extra material: foil
{
Entity materialEntity = CreateEntity();
MaterialComponent& mat = currentScene.materials.Create(materialEntity);
currentScene.names.Create(materialEntity) = "Foil";
currentScene.Component_Attach(materialEntity, entity);
mat.textures[MaterialComponent::BASECOLORMAP].name = asset_path + "foil.jpg";
mat.textures[MaterialComponent::NORMALMAP].name = asset_path + "foil_nor.jpg";
mat.metalness = 1;
mat.roughness = 0.01f;
mat.SetTextureStreamingDisabled();
terrain_preset.materialEntities.push_back(materialEntity);
}
// Extra material: pavingstone
{
Entity materialEntity = CreateEntity();
MaterialComponent& mat = currentScene.materials.Create(materialEntity);
currentScene.names.Create(materialEntity) = "Paving Stone";
currentScene.Component_Attach(materialEntity, entity);
mat.textures[MaterialComponent::BASECOLORMAP].name = asset_path + "pavingstone.jpg";
mat.textures[MaterialComponent::NORMALMAP].name = asset_path + "pavingstone_nor.jpg";
mat.SetTextureStreamingDisabled();
terrain_preset.materialEntities.push_back(materialEntity);
}
// Extra material: tactilepaving
{
Entity materialEntity = CreateEntity();
MaterialComponent& mat = currentScene.materials.Create(materialEntity);
currentScene.names.Create(materialEntity) = "Tactile Paving";
currentScene.Component_Attach(materialEntity, entity);
mat.textures[MaterialComponent::BASECOLORMAP].name = asset_path + "tactilepaving.jpg";
mat.textures[MaterialComponent::NORMALMAP].name = asset_path + "tactilepaving_nor.jpg";
mat.SetTextureStreamingDisabled();
terrain_preset.materialEntities.push_back(materialEntity);
}
// Extra material: lava
{
Entity materialEntity = CreateEntity();
MaterialComponent& mat = currentScene.materials.Create(materialEntity);
currentScene.names.Create(materialEntity) = "Lava";
currentScene.Component_Attach(materialEntity, entity);
mat.textures[MaterialComponent::BASECOLORMAP].name = asset_path + "lava.jpg";
mat.textures[MaterialComponent::NORMALMAP].name = asset_path + "lava_nor.jpg";
mat.textures[MaterialComponent::EMISSIVEMAP].name = asset_path + "lava_emi.jpg";
mat.roughness = 0.8f;
mat.SetTextureStreamingDisabled();
terrain_preset.materialEntities.push_back(materialEntity);
}
wi::jobsystem::context ctx;
wi::jobsystem::Dispatch(ctx, (uint32_t)terrain_preset.materialEntities.size(), 1, [&](wi::jobsystem::JobArgs args) {
Entity entity = terrain_preset.materialEntities[args.jobIndex];
MaterialComponent* material = currentScene.materials.GetComponent(entity);
if (material == nullptr)
return;
material->CreateRenderData();
});
wi::config::File config;
config.Open(std::string(asset_path + "props.ini").c_str());
std::unordered_map<std::string, Scene> prop_scenes;
for (const auto& it : config)
{
const std::string& section_name = it.first;
const wi::config::Section& section = it.second;
Entity entity = INVALID_ENTITY;
Scene* scene = &editor->GetCurrentScene();
if (section.Has("file"))
{
std::string text = section.GetText("file");
if (prop_scenes.count(text) == 0)
{
wi::scene::LoadModel(prop_scenes[text], asset_path + text);
}
if (prop_scenes.count(text) != 0)
{
scene = &prop_scenes[text];
}
}
if (section.Has("entity"))
{
std::string text = section.GetText("entity");
entity = scene->Entity_FindByName(text);
}
if (entity == INVALID_ENTITY)
{
continue;
}
wi::terrain::Prop& prop = terrain_preset.props.emplace_back();
if (section.Has("min_count_per_chunk"))
{
prop.min_count_per_chunk = section.GetInt("min_count_per_chunk");
}
if (section.Has("max_count_per_chunk"))
{
prop.max_count_per_chunk = section.GetInt("max_count_per_chunk");
}
if (section.Has("region"))
{
prop.region = section.GetInt("region");
}
if (section.Has("region_power"))
{
prop.region_power = section.GetFloat("region_power");
}
if (section.Has("noise_frequency"))
{
prop.noise_frequency = section.GetFloat("noise_frequency");
}
if (section.Has("noise_power"))
{
prop.noise_power = section.GetFloat("noise_power");
}
if (section.Has("threshold"))
{
prop.threshold = section.GetFloat("threshold");
}
if (section.Has("min_size"))
{
prop.min_size = section.GetFloat("min_size");
}
if (section.Has("max_size"))
{
prop.max_size = section.GetFloat("max_size");
}
if (section.Has("min_y_offset"))
{
prop.min_y_offset = section.GetFloat("min_y_offset");
}
if (section.Has("max_y_offset"))
{
prop.max_y_offset = section.GetFloat("max_y_offset");
}
wi::Archive archive;
EntitySerializer seri;
scene->Entity_Serialize(
archive,
seri,
entity,
wi::scene::Scene::EntitySerializeFlags::RECURSIVE | wi::scene::Scene::EntitySerializeFlags::KEEP_INTERNAL_ENTITY_REFERENCES
);
archive.WriteData(prop.data);
scene->Entity_Remove(entity); // The entities will be placed by terrain generator, we don't need the default object that the scene has anymore
}
wi::jobsystem::Wait(ctx);
for (auto& it : prop_scenes)
{
editor->GetCurrentScene().Merge(it.second);
}
// Grass config:
std::string grassWiscenePath = asset_path + "grass.wiscene";
if (wi::helper::FileExists(grassWiscenePath))
{
// New method: grass from wiscene file
Scene grassScene;
LoadModel(grassScene, grassWiscenePath);
if (grassScene.hairs.GetCount() > 0)
{
terrain_preset.grassEntity = grassScene.hairs.GetEntity(0);
currentScene.Merge(grassScene);
}
}
else
{
// Old method: from grass.ini, doesn't support some spritesheet features...
terrain_preset.grassEntity = CreateEntity();
currentScene.Component_Attach(terrain_preset.grassEntity, entity);
currentScene.materials.Create(terrain_preset.grassEntity);
currentScene.hairs.Create(terrain_preset.grassEntity) = terrain_preset.grass_properties;
MaterialComponent* material_Grass = currentScene.materials.GetComponent(terrain_preset.grassEntity);
wi::HairParticleSystem* grass = currentScene.hairs.GetComponent(terrain_preset.grassEntity);
wi::config::File grass_config;
grass_config.Open(std::string(asset_path + "grass.ini").c_str());
if (grass_config.Has("texture"))
{
material_Grass->textures[MaterialComponent::BASECOLORMAP].name = asset_path + grass_config.GetText("texture");
material_Grass->CreateRenderData();
}
if (grass_config.Has("alphaRef"))
{
material_Grass->alphaRef = grass_config.GetFloat("alphaRef");
}
if (grass_config.Has("length"))
{
grass->length = grass_config.GetFloat("length");
}
uint32_t framesX = 1;
uint32_t framesY = 1;
uint32_t frameCount = 1;
uint32_t frameStart = 0;
if (grass_config.Has("frameCount"))
{
frameCount = grass_config.GetInt("frameCount");
}
if (grass_config.Has("framesX"))
{
framesX = grass_config.GetInt("framesX");
}
if (grass_config.Has("framesY"))
{
framesY = grass_config.GetInt("framesY");
}
if (grass_config.Has("frameCount"))
{
frameStart = grass_config.GetInt("frameStart");
}
grass->ConvertFromOLDSpriteSheet(framesX, framesY, frameCount, frameStart);
}
{
Entity sunEntity = currentScene.Entity_CreateLight("sun");
LightComponent& light = *currentScene.lights.GetComponent(sunEntity);
light.SetType(LightComponent::LightType::DIRECTIONAL);
light.intensity = 16;
light.SetCastShadow(true);
TransformComponent& transform = *currentScene.transforms.GetComponent(sunEntity);
transform.RotateRollPitchYaw(XMFLOAT3(XM_PIDIV4, 0, XM_PIDIV4));
transform.Translate(XMFLOAT3(0, 4, 0));
}
presetCombo.SetSelected(0);
editor->paintToolWnd.RecreateTerrainMaterialButtons();
RefreshMaterialComboBoxes();
}
void TerrainWindow::Update(const wi::Canvas& canvas, float dt)
{
// Check whether any modifiers were "closed", and we will really remove them here if so:
if (!modifiers_to_remove.empty())
{
for (auto& modifier : modifiers_to_remove)
{
for (auto it = modifiers.begin(); it != modifiers.end(); ++it)
{
if (it->get() == modifier)
{
modifiers.erase(it);
RemoveWidget(modifier);
break;
}
}
}
modifiers_to_remove.clear();
}
wi::gui::Window::Update(canvas, dt);
}
void TerrainWindow::ResizeLayout()
{
layout.margin_left = 150;
layout.add_right(removalCheckBox);
centerToCamCheckBox.SetPos(XMFLOAT2(removalCheckBox.GetPos().x - 100, removalCheckBox.GetPos().y));
layout.add_right(grassCheckBox);
physicsCheckBox.SetPos(XMFLOAT2(grassCheckBox.GetPos().x - 100, grassCheckBox.GetPos().y));
layout.add_right(tessellationCheckBox);
layout.add(lodSlider);
layout.add(generationSlider);
layout.add(propGenerationSlider);
layout.add(physicsGenerationSlider);
layout.add(propDensitySlider);
layout.add(grassDensitySlider);
layout.add(grassLengthSlider);
layout.add(grassDistanceSlider);
layout.add(presetCombo);
layout.add(scaleSlider);
layout.add(seedSlider);
layout.add(bottomLevelSlider);
layout.add(topLevelSlider);
layout.add(region1Slider);
layout.add(region2Slider);
layout.add(region3Slider);
for (size_t i = 0; i < arraysize(materialCombos); ++i)
{
layout.add(materialCombos[i]);
}
layout.add(materialCombo_GrassParticle);
layout.add(saveHeightmapButton);
layout.add(saveRegionButton);
layout.add(addModifierCombo);
for (auto& modifier : modifiers)
{
layout.add_fullwidth(*modifier);
modifier->SetEnabled(true);
}
layout.add_fullwidth(*propsWindow.get());
propsWindow->SetEnabled(true);
SetSize(XMFLOAT2(GetScale().x, layout.y + control_size));
wi::gui::Window::ResizeLayout();
}