LumixEngine/src/renderer/editor/terrain_editor.cpp
Mikulas Florek 3dd6671dbf grass WIP
2020-08-28 00:00:43 +02:00

1570 lines
No EOL
46 KiB
C++

#include <imgui/imgui.h>
#include "terrain_editor.h"
#include "editor/asset_browser.h"
#include "editor/asset_compiler.h"
#include "editor/prefab_system.h"
#include "editor/studio_app.h"
#include "editor/utils.h"
#include "engine/crc32.h"
#include "engine/crt.h"
#include "engine/engine.h"
#include "engine/geometry.h"
#include "engine/log.h"
#include "engine/os.h"
#include "engine/path.h"
#include "engine/prefab.h"
#include "engine/profiler.h"
#include "engine/reflection.h"
#include "engine/resource_manager.h"
#include "engine/universe.h"
#include "physics/physics_scene.h"
#include "renderer/culling_system.h"
#include "renderer/editor/composite_texture.h"
#include "renderer/material.h"
#include "renderer/model.h"
#include "renderer/render_scene.h"
#include "renderer/renderer.h"
#include "renderer/texture.h"
#include "stb/stb_image.h"
namespace Lumix
{
static const ComponentType MODEL_INSTANCE_TYPE = Reflection::getComponentType("model_instance");
static const ComponentType TERRAIN_TYPE = Reflection::getComponentType("terrain");
static const ComponentType HEIGHTFIELD_TYPE = Reflection::getComponentType("physical_heightfield");
static const char* HEIGHTMAP_SLOT_NAME = "Heightmap";
static const char* SPLATMAP_SLOT_NAME = "Splatmap";
static const char* DETAIL_ALBEDO_SLOT_NAME = "Detail albedo";
static const char* DETAIL_NORMAL_SLOT_NAME = "Detail normal";
static const float MIN_BRUSH_SIZE = 0.5f;
struct PaintTerrainCommand final : IEditorCommand
{
struct Rectangle
{
int from_x;
int from_y;
int to_x;
int to_y;
};
PaintTerrainCommand(WorldEditor& editor,
TerrainEditor::ActionType action_type,
u16 grass_mask,
u64 textures_mask,
const DVec3& hit_pos,
const Array<bool>& mask,
float radius,
float rel_amount,
u16 flat_height,
Vec3 color,
ComponentUID terrain,
u32 layers_mask,
Vec2 fixed_value,
bool can_be_merged)
: m_world_editor(editor)
, m_terrain(terrain)
, m_can_be_merged(can_be_merged)
, m_new_data(editor.getAllocator())
, m_old_data(editor.getAllocator())
, m_items(editor.getAllocator())
, m_action_type(action_type)
, m_textures_mask(textures_mask)
, m_grass_mask(grass_mask)
, m_mask(editor.getAllocator())
, m_flat_height(flat_height)
, m_layers_masks(layers_mask)
, m_fixed_value(fixed_value)
{
ASSERT(terrain.isValid());
m_mask.resize(mask.size());
for (int i = 0; i < mask.size(); ++i) {
m_mask[i] = mask[i];
}
m_width = m_height = m_x = m_y = -1;
const Transform entity_transform = editor.getUniverse()->getTransform((EntityRef)terrain.entity).inverted();
DVec3 local_pos = entity_transform.transform(hit_pos);
float terrain_size = static_cast<RenderScene*>(terrain.scene)->getTerrainSize((EntityRef)terrain.entity).x;
local_pos = local_pos / terrain_size;
local_pos.y = -1;
Item& item = m_items.emplace();
item.m_local_pos = local_pos.toFloat();
item.m_radius = radius / terrain_size;
item.m_amount = rel_amount;
item.m_color = color;
}
bool execute() override
{
if (m_new_data.empty())
{
saveOldData();
generateNewData();
}
applyData(m_new_data);
return true;
}
void undo() override { applyData(m_old_data); }
const char* getType() override
{
return "paint_terrain";
}
bool merge(IEditorCommand& command) override
{
if (!m_can_be_merged)
{
return false;
}
PaintTerrainCommand& my_command = static_cast<PaintTerrainCommand&>(command);
if (m_terrain == my_command.m_terrain && m_action_type == my_command.m_action_type &&
m_textures_mask == my_command.m_textures_mask && m_layers_masks == my_command.m_layers_masks)
{
my_command.m_items.push(m_items.back());
my_command.resizeData();
my_command.rasterItem(getDestinationTexture(), my_command.m_new_data, m_items.back());
return true;
}
return false;
}
private:
struct Item
{
Rectangle getBoundingRectangle(int texture_size) const
{
Rectangle r;
r.from_x = maximum(0, int(texture_size * (m_local_pos.x - m_radius) - 0.5f));
r.from_y = maximum(0, int(texture_size * (m_local_pos.z - m_radius) - 0.5f));
r.to_x = minimum(texture_size, int(texture_size * (m_local_pos.x + m_radius) + 0.5f));
r.to_y = minimum(texture_size, int(texture_size * (m_local_pos.z + m_radius) + 0.5f));
return r;
}
float m_radius;
float m_amount;
Vec3 m_local_pos;
Vec3 m_color;
};
private:
Material* getMaterial()
{
auto* scene = static_cast<RenderScene*>(m_terrain.scene);
return m_terrain.entity.isValid() ? scene->getTerrainMaterial((EntityRef)m_terrain.entity) : nullptr;
}
Texture* getDestinationTexture()
{
const char* uniform_name;
switch (m_action_type)
{
case TerrainEditor::REMOVE_GRASS:
case TerrainEditor::LAYER:
uniform_name = SPLATMAP_SLOT_NAME;
break;
default:
uniform_name = HEIGHTMAP_SLOT_NAME;
break;
}
return getMaterial()->getTextureByName(uniform_name);
}
u16 computeAverage16(const Texture* texture, int from_x, int to_x, int from_y, int to_y)
{
ASSERT(texture->format == gpu::TextureFormat::R16);
u32 sum = 0;
int texture_width = texture->width;
for (int i = from_x, end = to_x; i < end; ++i)
{
for (int j = from_y, end2 = to_y; j < end2; ++j)
{
sum += ((u16*)texture->getData())[(i + j * texture_width)];
}
}
return u16(sum / (to_x - from_x) / (to_y - from_y));
}
float getAttenuation(Item& item, int i, int j, int texture_size) const
{
float dist =
((texture_size * item.m_local_pos.x - 0.5f - i) * (texture_size * item.m_local_pos.x - 0.5f - i) +
(texture_size * item.m_local_pos.z - 0.5f - j) * (texture_size * item.m_local_pos.z - 0.5f - j));
dist = powf(dist, 4);
float max_dist = powf(texture_size * item.m_radius, 8);
return 1.0f - minimum(dist / max_dist, 1.0f);
}
bool isMasked(float x, float y)
{
if (m_mask.size() == 0) return true;
int s = int(sqrtf((float)m_mask.size()));
int ix = int(x * s);
int iy = int(y * s);
return m_mask[int(ix + x * iy)];
}
void rasterLayerItem(Texture* texture, Array<u8>& data, Item& item)
{
int texture_size = texture->width;
Rectangle r = item.getBoundingRectangle(texture_size);
if (texture->format != gpu::TextureFormat::RGBA8)
{
ASSERT(false);
return;
}
float fx = 0;
float fstepx = 1.0f / (r.to_x - r.from_x);
float fstepy = 1.0f / (r.to_y - r.from_y);
u8 tex[64];
u32 tex_count = 0;
for (u8 i = 0; i < 64; ++i) {
if (m_textures_mask & ((u64)1 << i)) {
tex[tex_count] = i;
++tex_count;
}
}
if (tex_count == 0) return;
for (int i = r.from_x, end = r.to_x; i < end; ++i, fx += fstepx) {
float fy = 0;
for (int j = r.from_y, end2 = r.to_y; j < end2; ++j, fy += fstepy) {
if (!isMasked(fx, fy)) continue;
const int offset = 4 * (i - m_x + (j - m_y) * m_width);
for (u32 layer = 0; layer < 2; ++layer) {
if ((m_layers_masks & (1 << layer)) == 0) continue;
const float attenuation = getAttenuation(item, i, j, texture_size);
int add = int(attenuation * item.m_amount * 255);
if (add <= 0) continue;
if (((u64)1 << data[offset]) & m_textures_mask) {
if (layer == 1) {
if (m_fixed_value.x >= 0) {
data[offset + 1] = (u8)clamp(randFloat(m_fixed_value.x, m_fixed_value.y) * 255.f, 0.f, 255.f);
}
else {
data[offset + 1] += minimum(255 - data[offset + 1], add);
}
}
}
else {
if (layer == 1) {
if (m_fixed_value.x >= 0) {
data[offset + 1] = (u8)clamp(randFloat(m_fixed_value.x, m_fixed_value.y) * 255.f, 0.f, 255.f);
}
else {
data[offset + 1] = add;
}
}
data[offset] = tex[rand() % tex_count];
}
}
}
}
}
void rasterGrassItem(Texture* texture, Array<u8>& data, Item& item, bool remove)
{
int texture_size = texture->width;
Rectangle r = item.getBoundingRectangle(texture_size);
if (texture->format != gpu::TextureFormat::RGBA8)
{
ASSERT(false);
return;
}
float fx = 0;
float fstepx = 1.0f / (r.to_x - r.from_x);
float fstepy = 1.0f / (r.to_y - r.from_y);
for (int i = r.from_x, end = r.to_x; i < end; ++i, fx += fstepx) {
float fy = 0;
for (int j = r.from_y, end2 = r.to_y; j < end2; ++j, fy += fstepy) {
if (isMasked(fx, fy)) {
int offset = 4 * (i - m_x + (j - m_y) * m_width) + 2;
float attenuation = getAttenuation(item, i, j, texture_size);
int add = int(attenuation * item.m_amount * 255);
if (add > 0) {
u16* tmp = ((u16*)&data[offset]);
if (remove) {
*tmp &= ~m_grass_mask;
}
else {
*tmp |= m_grass_mask;
}
}
}
}
}
}
void rasterSmoothHeightItem(Texture* texture, Array<u8>& data, Item& item)
{
ASSERT(texture->format == gpu::TextureFormat::R16);
int texture_size = texture->width;
Rectangle rect = item.getBoundingRectangle(texture_size);
float avg = computeAverage16(texture, rect.from_x, rect.to_x, rect.from_y, rect.to_y);
for (int i = rect.from_x, end = rect.to_x; i < end; ++i)
{
for (int j = rect.from_y, end2 = rect.to_y; j < end2; ++j)
{
float attenuation = getAttenuation(item, i, j, texture_size);
int offset = i - m_x + (j - m_y) * m_width;
u16 x = ((u16*)texture->getData())[(i + j * texture_size)];
x += u16((avg - x) * item.m_amount * attenuation);
((u16*)&data[0])[offset] = x;
}
}
}
void rasterFlatHeightItem(Texture* texture, Array<u8>& data, Item& item)
{
ASSERT(texture->format == gpu::TextureFormat::R16);
int texture_size = texture->width;
Rectangle rect = item.getBoundingRectangle(texture_size);
for (int i = rect.from_x, end = rect.to_x; i < end; ++i)
{
for (int j = rect.from_y, end2 = rect.to_y; j < end2; ++j)
{
int offset = i - m_x + (j - m_y) * m_width;
float dist = sqrtf(
(texture_size * item.m_local_pos.x - 0.5f - i) * (texture_size * item.m_local_pos.x - 0.5f - i) +
(texture_size * item.m_local_pos.z - 0.5f - j) * (texture_size * item.m_local_pos.z - 0.5f - j));
float t = (dist - texture_size * item.m_radius * item.m_amount) /
(texture_size * item.m_radius * (1 - item.m_amount));
t = clamp(1 - t, 0.0f, 1.0f);
u16 old_value = ((u16*)&data[0])[offset];
((u16*)&data[0])[offset] = (u16)(m_flat_height * t + old_value * (1-t));
}
}
}
void rasterItem(Texture* texture, Array<u8>& data, Item& item)
{
if (m_action_type == TerrainEditor::LAYER || m_action_type == TerrainEditor::REMOVE_GRASS)
{
if (m_textures_mask) {
rasterLayerItem(texture, data, item);
}
if (m_grass_mask) {
rasterGrassItem(texture, data, item, m_action_type == TerrainEditor::REMOVE_GRASS);
}
return;
}
else if (m_action_type == TerrainEditor::SMOOTH_HEIGHT)
{
rasterSmoothHeightItem(texture, data, item);
return;
}
else if (m_action_type == TerrainEditor::FLAT_HEIGHT)
{
rasterFlatHeightItem(texture, data, item);
return;
}
ASSERT(texture->format == gpu::TextureFormat::R16);
int texture_size = texture->width;
Rectangle rect = item.getBoundingRectangle(texture_size);
const float STRENGTH_MULTIPLICATOR = 256.0f;
float amount = maximum(item.m_amount * item.m_amount * STRENGTH_MULTIPLICATOR, 1.0f);
for (int i = rect.from_x, end = rect.to_x; i < end; ++i)
{
for (int j = rect.from_y, end2 = rect.to_y; j < end2; ++j)
{
float attenuation = getAttenuation(item, i, j, texture_size);
int offset = i - m_x + (j - m_y) * m_width;
int add = int(attenuation * amount);
u16 x = ((u16*)texture->getData())[(i + j * texture_size)];
x += m_action_type == TerrainEditor::RAISE_HEIGHT ? minimum(add, 0xFFFF - x)
: maximum(-add, -x);
((u16*)&data[0])[offset] = x;
}
}
}
void generateNewData()
{
auto texture = getDestinationTexture();
const u32 bpp = gpu::getBytesPerPixel(texture->format);
Rectangle rect;
getBoundingRectangle(texture, rect);
m_new_data.resize(bpp * maximum(1, (rect.to_x - rect.from_x) * (rect.to_y - rect.from_y)));
if(m_old_data.size() > 0) {
memcpy(&m_new_data[0], &m_old_data[0], m_new_data.size());
}
for (int item_index = 0; item_index < m_items.size(); ++item_index)
{
Item& item = m_items[item_index];
rasterItem(texture, m_new_data, item);
}
}
void saveOldData()
{
auto texture = getDestinationTexture();
const u32 bpp = gpu::getBytesPerPixel(texture->format);
Rectangle rect;
getBoundingRectangle(texture, rect);
m_x = rect.from_x;
m_y = rect.from_y;
m_width = rect.to_x - rect.from_x;
m_height = rect.to_y - rect.from_y;
m_old_data.resize(bpp * (rect.to_x - rect.from_x) * (rect.to_y - rect.from_y));
int index = 0;
for (int j = rect.from_y, end2 = rect.to_y; j < end2; ++j)
{
for (int i = rect.from_x, end = rect.to_x; i < end; ++i)
{
for (u32 k = 0; k < bpp; ++k)
{
m_old_data[index] = texture->getData()[(i + j * texture->width) * bpp + k];
++index;
}
}
}
}
void applyData(Array<u8>& data)
{
if (!m_terrain.isValid()) return;
auto texture = getDestinationTexture();
const u32 bpp = gpu::getBytesPerPixel(texture->format);
for (int j = m_y; j < m_y + m_height; ++j)
{
for (int i = m_x; i < m_x + m_width; ++i)
{
int index = bpp * (i + j * texture->width);
for (u32 k = 0; k < bpp; ++k)
{
texture->getData()[index + k] = data[bpp * (i - m_x + (j - m_y) * m_width) + k];
}
}
}
texture->onDataUpdated(m_x, m_y, m_width, m_height);
const EntityRef e = (EntityRef)m_terrain.entity;
if (m_action_type != TerrainEditor::LAYER && m_action_type != TerrainEditor::REMOVE_GRASS)
{
IScene* scene = m_world_editor.getUniverse()->getScene(crc32("physics"));
if (!scene) return;
auto* phy_scene = static_cast<PhysicsScene*>(scene);
if (!scene->getUniverse().hasComponent(e, HEIGHTFIELD_TYPE)) return;
phy_scene->updateHeighfieldData(e, m_x, m_y, m_width, m_height, &data[0], bpp);
}
}
void resizeData()
{
Array<u8> new_data(m_world_editor.getAllocator());
Array<u8> old_data(m_world_editor.getAllocator());
auto texture = getDestinationTexture();
Rectangle rect;
getBoundingRectangle(texture, rect);
int new_w = rect.to_x - rect.from_x;
const u32 bpp = gpu::getBytesPerPixel(texture->format);
new_data.resize(bpp * new_w * (rect.to_y - rect.from_y));
old_data.resize(bpp * new_w * (rect.to_y - rect.from_y));
// original
for (int row = rect.from_y; row < rect.to_y; ++row)
{
memcpy(&new_data[(row - rect.from_y) * new_w * bpp],
&texture->getData()[row * bpp * texture->width + rect.from_x * bpp],
bpp * new_w);
memcpy(&old_data[(row - rect.from_y) * new_w * bpp],
&texture->getData()[row * bpp * texture->width + rect.from_x * bpp],
bpp * new_w);
}
// new
for (int row = 0; row < m_height; ++row)
{
memcpy(&new_data[((row + m_y - rect.from_y) * new_w + m_x - rect.from_x) * bpp],
&m_new_data[row * bpp * m_width],
bpp * m_width);
memcpy(&old_data[((row + m_y - rect.from_y) * new_w + m_x - rect.from_x) * bpp],
&m_old_data[row * bpp * m_width],
bpp * m_width);
}
m_x = rect.from_x;
m_y = rect.from_y;
m_height = rect.to_y - rect.from_y;
m_width = rect.to_x - rect.from_x;
m_new_data.swap(new_data);
m_old_data.swap(old_data);
}
void getBoundingRectangle(Texture* texture, Rectangle& rect)
{
int s = texture->width;
Item& item = m_items[0];
rect.from_x = maximum(int(s * (item.m_local_pos.x - item.m_radius) - 0.5f), 0);
rect.from_y = maximum(int(s * (item.m_local_pos.z - item.m_radius) - 0.5f), 0);
rect.to_x = minimum(1 + int(s * (item.m_local_pos.x + item.m_radius) + 0.5f), texture->width);
rect.to_y = minimum(1 + int(s * (item.m_local_pos.z + item.m_radius) + 0.5f), texture->height);
for (int i = 1; i < m_items.size(); ++i)
{
Item& item = m_items[i];
rect.from_x = minimum(int(s * (item.m_local_pos.x - item.m_radius) - 0.5f), rect.from_x);
rect.to_x = maximum(1 + int(s * (item.m_local_pos.x + item.m_radius) + 0.5f), rect.to_x);
rect.from_y = minimum(int(s * (item.m_local_pos.z - item.m_radius) - 0.5f), rect.from_y);
rect.to_y = maximum(1 + int(s * (item.m_local_pos.z + item.m_radius) + 0.5f), rect.to_y);
}
rect.from_x = maximum(rect.from_x, 0);
rect.to_x = minimum(rect.to_x, texture->width);
rect.from_y = maximum(rect.from_y, 0);
rect.to_y = minimum(rect.to_y, texture->height);
}
private:
Array<u8> m_new_data;
Array<u8> m_old_data;
u64 m_textures_mask;
u16 m_grass_mask;
int m_width;
int m_height;
int m_x;
int m_y;
TerrainEditor::ActionType m_action_type;
Array<Item> m_items;
ComponentUID m_terrain;
WorldEditor& m_world_editor;
Array<bool> m_mask;
u16 m_flat_height;
u32 m_layers_masks;
Vec2 m_fixed_value;
bool m_can_be_merged;
};
TerrainEditor::~TerrainEditor()
{
m_world_editor.universeDestroyed().unbind<&TerrainEditor::onUniverseDestroyed>(this);
if (m_brush_texture)
{
m_brush_texture->destroy();
LUMIX_DELETE(m_world_editor.getAllocator(), m_brush_texture);
}
m_app.removePlugin(*this);
}
void TerrainEditor::onUniverseDestroyed()
{
m_component.scene = nullptr;
m_component.entity = INVALID_ENTITY;
}
TerrainEditor::TerrainEditor(WorldEditor& editor, StudioApp& app)
: m_world_editor(editor)
, m_app(app)
, m_color(1, 1, 1)
, m_current_brush(0)
, m_selected_prefabs(editor.getAllocator())
, m_brush_mask(editor.getAllocator())
, m_brush_texture(nullptr)
, m_flat_height(0)
, m_is_enabled(false)
, m_size_spread(1, 1)
, m_y_spread(0, 0)
, m_albedo_composite(editor.getAllocator())
{
m_smooth_terrain_action = LUMIX_NEW(editor.getAllocator(), Action)("Smooth terrain", "Terrain editor - smooth", "smoothTerrain");
m_smooth_terrain_action->is_global = false;
m_lower_terrain_action = LUMIX_NEW(editor.getAllocator(), Action)("Lower terrain", "Terrain editor - lower", "lowerTerrain");
m_lower_terrain_action->is_global = false;
app.addAction(m_smooth_terrain_action);
app.addAction(m_lower_terrain_action);
m_remove_grass_action =
LUMIX_NEW(editor.getAllocator(), Action)("Remove grass from terrain", "Terrain editor - remove grass", "removeGrassFromTerrain");
m_remove_grass_action->is_global = false;
app.addAction(m_remove_grass_action);
m_remove_entity_action =
LUMIX_NEW(editor.getAllocator(), Action)("Remove entities from terrain", "Terrain editor - remove entities", "removeEntitiesFromTerrain");
m_remove_entity_action->is_global = false;
app.addAction(m_remove_entity_action);
app.addPlugin(*this);
m_terrain_brush_size = 10;
m_terrain_brush_strength = 0.1f;
m_textures_mask = 0b1;
m_layers_mask = 0b1;
m_grass_mask = 1;
m_is_align_with_normal = false;
m_is_rotate_x = false;
m_is_rotate_y = false;
m_is_rotate_z = false;
m_rotate_x_spread = m_rotate_y_spread = m_rotate_z_spread = Vec2(0, PI * 2);
editor.universeDestroyed().bind<&TerrainEditor::onUniverseDestroyed>(this);
}
void TerrainEditor::increaseBrushSize()
{
if (m_terrain_brush_size < 10)
{
++m_terrain_brush_size;
return;
}
m_terrain_brush_size = minimum(100.0f, m_terrain_brush_size + 10);
}
void TerrainEditor::decreaseBrushSize()
{
if (m_terrain_brush_size < 10)
{
m_terrain_brush_size = maximum(MIN_BRUSH_SIZE, m_terrain_brush_size - 1.0f);
return;
}
m_terrain_brush_size = maximum(MIN_BRUSH_SIZE, m_terrain_brush_size - 10.0f);
}
void TerrainEditor::drawCursor(RenderScene& scene, EntityRef terrain, const DVec3& center)
{
PROFILE_FUNCTION();
constexpr int SLICE_COUNT = 30;
constexpr float angle_step = PI * 2 / SLICE_COUNT;
if (m_mode == Mode::HEIGHT && m_is_flat_height && ImGui::GetIO().KeyCtrl) {
scene.addDebugCross(center, 1.0f, 0xff0000ff);
return;
}
float brush_size = m_terrain_brush_size;
const Vec3 local_center = getRelativePosition(center).toFloat();
const Transform terrain_transform = m_world_editor.getUniverse()->getTransform((EntityRef)m_component.entity);
for (int i = 0; i < SLICE_COUNT + 1; ++i) {
const float angle = i * angle_step;
const float next_angle = i * angle_step + angle_step;
Vec3 local_from = local_center + Vec3(cosf(angle), 0, sinf(angle)) * brush_size;
local_from.y = scene.getTerrainHeightAt(terrain, local_from.x, local_from.z);
local_from.y += 0.25f;
Vec3 local_to =
local_center + Vec3(cosf(next_angle), 0, sinf(next_angle)) * brush_size;
local_to.y = scene.getTerrainHeightAt(terrain, local_to.x, local_to.z);
local_to.y += 0.25f;
const DVec3 from = terrain_transform.transform(local_from);
const DVec3 to = terrain_transform.transform(local_to);
scene.addDebugLine(from, to, 0xffff0000);
}
}
DVec3 TerrainEditor::getRelativePosition(const DVec3& world_pos) const
{
const Transform transform = m_world_editor.getUniverse()->getTransform((EntityRef)m_component.entity);
const Transform inv_transform = transform.inverted();
return inv_transform.transform(world_pos);
}
Texture* TerrainEditor::getHeightmap() const
{
return getMaterial()->getTextureByName(HEIGHTMAP_SLOT_NAME);
}
u16 TerrainEditor::getHeight(const DVec3& world_pos) const
{
auto rel_pos = getRelativePosition(world_pos);
auto* heightmap = getHeightmap();
if (!heightmap) return 0;
auto* data = (u16*)heightmap->getData();
auto* scene = (RenderScene*)m_component.scene;
float scale = scene->getTerrainXZScale((EntityRef)m_component.entity);
return data[int(rel_pos.x / scale) + int(rel_pos.z / scale) * heightmap->width];
}
bool TerrainEditor::onMouseDown(UniverseView& view, int x, int y)
{
if (!m_is_enabled) return false;
const UniverseView::RayHit hit = view.getCameraRaycastHit(x, y);
if (!hit.entity.isValid()) return false;
const auto& selected_entities = m_world_editor.getSelectedEntities();
if (selected_entities.size() != 1) return false;
bool is_terrain = m_world_editor.getUniverse()->hasComponent(selected_entities[0], TERRAIN_TYPE);
if (!is_terrain) return false;
if (!m_component.isValid()) return false;
if ((EntityPtr)selected_entities[0] == hit.entity && m_component.isValid()) {
const DVec3 hit_pos = hit.pos;
switch(m_mode) {
case Mode::ENTITY:
if (m_remove_entity_action->isActive()) {
removeEntities(hit.pos);
}
else {
paintEntities(hit.pos);
}
break;
case Mode::HEIGHT:
if (m_is_flat_height) {
if (ImGui::GetIO().KeyCtrl) {
m_flat_height = getHeight(hit_pos);
}
else {
paint(hit.pos, TerrainEditor::FLAT_HEIGHT, false);
}
}
else {
TerrainEditor::ActionType action = TerrainEditor::RAISE_HEIGHT;
if (m_lower_terrain_action->isActive()) {
action = TerrainEditor::LOWER_HEIGHT;
}
else if (m_smooth_terrain_action->isActive()) {
action = TerrainEditor::SMOOTH_HEIGHT;
}
paint(hit.pos, action, false); break;
}
break;
case Mode::LAYER:
TerrainEditor::ActionType action = TerrainEditor::LAYER;
if (m_remove_grass_action->isActive()) {
action = TerrainEditor::REMOVE_GRASS;
}
paint(hit.pos, action, false);
break;
}
return true;
}
return true;
}
static void getProjections(const Vec3& axis,
const Vec3 vertices[8],
Ref<float> min,
Ref<float> max)
{
max = dotProduct(vertices[0], axis);
min = max;
for(int i = 1; i < 8; ++i)
{
float dot = dotProduct(vertices[i], axis);
min = minimum(dot, min);
max = maximum(dot, max);
}
}
static bool overlaps(float min1, float max1, float min2, float max2)
{
return (min1 <= min2 && min2 <= max1) || (min2 <= min1 && min1 <= max2);
}
static bool testOBBCollision(const AABB& a,
const Matrix& mtx_b,
const AABB& b)
{
Vec3 box_a_points[8];
Vec3 box_b_points[8];
a.getCorners(Matrix::IDENTITY, box_a_points);
b.getCorners(mtx_b, box_b_points);
const Vec3 normals[] = {Vec3(1, 0, 0), Vec3(0, 1, 0), Vec3(0, 0, 1)};
for(int i = 0; i < 3; i++)
{
float box_a_min, box_a_max, box_b_min, box_b_max;
getProjections(normals[i], box_a_points, Ref(box_a_min), Ref(box_a_max));
getProjections(normals[i], box_b_points, Ref(box_b_min), Ref(box_b_max));
if(!overlaps(box_a_min, box_a_max, box_b_min, box_b_max))
{
return false;
}
}
Vec3 normals_b[] = {
mtx_b.getXVector().normalized(), mtx_b.getYVector().normalized(), mtx_b.getZVector().normalized()};
for(int i = 0; i < 3; i++)
{
float box_a_min, box_a_max, box_b_min, box_b_max;
getProjections(normals_b[i], box_a_points, Ref(box_a_min), Ref(box_a_max));
getProjections(normals_b[i], box_b_points, Ref(box_b_min), Ref(box_b_max));
if(!overlaps(box_a_min, box_a_max, box_b_min, box_b_max))
{
return false;
}
}
return true;
}
void TerrainEditor::removeEntities(const DVec3& hit_pos)
{
if (m_selected_prefabs.empty()) return;
auto& prefab_system = m_world_editor.getPrefabSystem();
PROFILE_FUNCTION();
RenderScene* scene = static_cast<RenderScene*>(m_component.scene);
Universe& universe = scene->getUniverse();
ShiftedFrustum frustum;
frustum.computeOrtho(hit_pos,
Vec3(0, 0, 1),
Vec3(0, 1, 0),
m_terrain_brush_size,
m_terrain_brush_size,
-m_terrain_brush_size,
m_terrain_brush_size);
const AABB brush_aabb(Vec3(-m_terrain_brush_size), Vec3(m_terrain_brush_size));
CullResult* meshes = scene->getRenderables(frustum, RenderableTypes::MESH);
if(meshes) {
meshes->merge(scene->getRenderables(frustum, RenderableTypes::MESH_GROUP));
}
else {
meshes = scene->getRenderables(frustum, RenderableTypes::MESH_GROUP);
}
if(!meshes) return;
const u32 REMOVE_ENTITIES_HASH = crc32("remove_entities");
m_world_editor.beginCommandGroup(REMOVE_ENTITIES_HASH);
if (m_selected_prefabs.empty())
{
meshes->forEach([&](EntityRef entity){
if (prefab_system.getPrefab(entity) == 0) return;
const Model* model = scene->getModelInstanceModel(entity);
const AABB entity_aabb = model ? model->getAABB() : AABB(Vec3::ZERO, Vec3::ZERO);
const bool collide = testOBBCollision(brush_aabb, universe.getRelativeMatrix(entity, hit_pos), entity_aabb);
if (collide) m_world_editor.destroyEntities(&entity, 1);
});
}
else
{
meshes->forEach([&](EntityRef entity){
for (auto* res : m_selected_prefabs)
{
if ((prefab_system.getPrefab(entity) & 0xffffFFFF) == res->getPath().getHash())
{
const Model* model = scene->getModelInstanceModel(entity);
const AABB entity_aabb = model ? model->getAABB() : AABB(Vec3::ZERO, Vec3::ZERO);
const bool collide = testOBBCollision(brush_aabb, universe.getRelativeMatrix(entity, hit_pos), entity_aabb);
if (collide) m_world_editor.destroyEntities(&entity, 1);
}
}
});
}
m_world_editor.endCommandGroup();
meshes->free(scene->getEngine().getPageAllocator());
}
static bool isOBBCollision(RenderScene& scene,
const CullResult* meshes,
const Transform& model_tr,
Model* model)
{
float radius_a_squared = model->getBoundingRadius() * model_tr.scale;
radius_a_squared = radius_a_squared * radius_a_squared;
Universe& universe = scene.getUniverse();
const ModelInstance* model_instances = scene.getModelInstances();
const Transform* transforms = universe.getTransforms();
while(meshes) {
const EntityRef* entities = meshes->entities;
for (u32 i = 0, c = meshes->header.count; i < c; ++i) {
const EntityRef mesh = entities[i];
const ModelInstance& model_instance = model_instances[mesh.index];
const Transform& tr_b = transforms[mesh.index];
const float radius_b = model_instance.model->getBoundingRadius() * tr_b.scale;
const float radius_squared = radius_a_squared + radius_b * radius_b;
if ((model_tr.pos - tr_b.pos).squaredLength() < radius_squared) {
const Transform rel_tr = model_tr.inverted() * tr_b;
Matrix mtx = rel_tr.rot.toMatrix();
mtx.multiply3x3(rel_tr.scale);
mtx.setTranslation(rel_tr.pos.toFloat());
if (testOBBCollision(model->getAABB(), mtx, model_instance.model->getAABB())) {
return true;
}
}
}
meshes = meshes->header.next;
}
return false;
}
void TerrainEditor::paintEntities(const DVec3& hit_pos)
{
PROFILE_FUNCTION();
if (m_selected_prefabs.empty()) return;
auto& prefab_system = m_world_editor.getPrefabSystem();
static const u32 PAINT_ENTITIES_HASH = crc32("paint_entities");
m_world_editor.beginCommandGroup(PAINT_ENTITIES_HASH);
{
RenderScene* scene = static_cast<RenderScene*>(m_component.scene);
const Transform terrain_tr = m_world_editor.getUniverse()->getTransform((EntityRef)m_component.entity);
const Transform inv_terrain_tr = terrain_tr.inverted();
ShiftedFrustum frustum;
frustum.computeOrtho(hit_pos,
Vec3(0, 0, 1),
Vec3(0, 1, 0),
m_terrain_brush_size,
m_terrain_brush_size,
-m_terrain_brush_size,
m_terrain_brush_size);
CullResult* meshes = scene->getRenderables(frustum, RenderableTypes::MESH);
CullResult* mesh_groups = scene->getRenderables(frustum, RenderableTypes::MESH_GROUP);
Vec2 size = scene->getTerrainSize((EntityRef)m_component.entity);
float scale = 1.0f - maximum(0.01f, m_terrain_brush_strength);
for (int i = 0; i <= m_terrain_brush_size * m_terrain_brush_size / 100.0f * m_terrain_brush_strength; ++i)
{
const float angle = randFloat(0, PI * 2);
const float dist = randFloat(0, 1.0f) * m_terrain_brush_size;
const float y = randFloat(m_y_spread.x, m_y_spread.y);
DVec3 pos(hit_pos.x + cosf(angle) * dist, 0, hit_pos.z + sinf(angle) * dist);
const Vec3 terrain_pos = inv_terrain_tr.transform(pos).toFloat();
if (terrain_pos.x >= 0 && terrain_pos.z >= 0 && terrain_pos.x <= size.x && terrain_pos.z <= size.y)
{
pos.y = scene->getTerrainHeightAt((EntityRef)m_component.entity, terrain_pos.x, terrain_pos.z) + y;
pos.y += terrain_tr.pos.y;
Quat rot(0, 0, 0, 1);
if(m_is_align_with_normal)
{
RenderScene* scene = static_cast<RenderScene*>(m_component.scene);
Vec3 normal = scene->getTerrainNormalAt((EntityRef)m_component.entity, terrain_pos.x, terrain_pos.z);
Vec3 dir = crossProduct(normal, Vec3(1, 0, 0)).normalized();
Matrix mtx = Matrix::IDENTITY;
mtx.setXVector(crossProduct(normal, dir));
mtx.setYVector(normal);
mtx.setXVector(dir);
rot = mtx.getRotation();
}
else
{
if (m_is_rotate_x)
{
float angle = randFloat(m_rotate_x_spread.x, m_rotate_x_spread.y);
Quat q(Vec3(1, 0, 0), angle);
rot = q * rot;
}
if (m_is_rotate_y)
{
float angle = randFloat(m_rotate_y_spread.x, m_rotate_y_spread.y);
Quat q(Vec3(0, 1, 0), angle);
rot = q * rot;
}
if (m_is_rotate_z)
{
float angle = randFloat(m_rotate_z_spread.x, m_rotate_z_spread.y);
Quat q(rot.rotate(Vec3(0, 0, 1)), angle);
rot = q * rot;
}
}
float size = randFloat(m_size_spread.x, m_size_spread.y);
int random_idx = rand(0, m_selected_prefabs.size() - 1);
if (!m_selected_prefabs[random_idx]) continue;
const EntityPtr entity = prefab_system.instantiatePrefab(*m_selected_prefabs[random_idx], pos, rot, size);
if (entity.isValid()) {
if (scene->getUniverse().hasComponent((EntityRef)entity, MODEL_INSTANCE_TYPE)) {
Model* model = scene->getModelInstanceModel((EntityRef)entity);
const Transform tr = { pos, rot, size * scale };
if (isOBBCollision(*scene, meshes, tr, model) || isOBBCollision(*scene, mesh_groups, tr, model)) {
m_world_editor.undo();
}
}
}
}
}
meshes->free(m_app.getEngine().getPageAllocator());
mesh_groups->free(m_app.getEngine().getPageAllocator());
}
m_world_editor.endCommandGroup();
}
void TerrainEditor::onMouseMove(UniverseView& view, int x, int y, int, int)
{
if (!m_is_enabled) return;
RenderScene* scene = static_cast<RenderScene*>(m_component.scene);
DVec3 origin;
Vec3 dir;
view.getViewport().getRay({(float)x, (float)y}, origin, dir);
RayCastModelHit hit = scene->castRayTerrain((EntityRef)m_component.entity, origin, dir);
if (hit.is_hit) {
bool is_terrain = m_world_editor.getUniverse()->hasComponent((EntityRef)hit.entity, TERRAIN_TYPE);
if (!is_terrain) return;
const DVec3 hit_point = hit.origin + hit.dir * hit.t;
switch(m_mode) {
case Mode::ENTITY:
if (m_remove_entity_action->isActive()) {
removeEntities(hit_point);
}
else {
paintEntities(hit_point);
}
break;
case Mode::HEIGHT:
if (m_is_flat_height) {
if (ImGui::GetIO().KeyCtrl) {
m_flat_height = getHeight(hit_point);
}
else {
paint(hit_point, TerrainEditor::FLAT_HEIGHT, false);
}
}
else {
TerrainEditor::ActionType action = TerrainEditor::RAISE_HEIGHT;
if (m_lower_terrain_action->isActive()) {
action = TerrainEditor::LOWER_HEIGHT;
}
else if (m_smooth_terrain_action->isActive()) {
action = TerrainEditor::SMOOTH_HEIGHT;
}
paint(hit_point, action, false); break;
}
break;
case Mode::LAYER:
TerrainEditor::ActionType action = TerrainEditor::LAYER;
if (m_remove_grass_action->isActive()) {
action = TerrainEditor::REMOVE_GRASS;
}
paint(hit_point, action, false);
break;
}
}
}
Material* TerrainEditor::getMaterial() const
{
if (!m_component.isValid()) return nullptr;
auto* scene = static_cast<RenderScene*>(m_component.scene);
return scene->getTerrainMaterial((EntityRef)m_component.entity);
}
void TerrainEditor::layerGUI() {
m_mode = Mode::LAYER;
auto* scene = static_cast<RenderScene*>(m_component.scene);
if (getMaterial()
&& getMaterial()->getTextureByName(SPLATMAP_SLOT_NAME)
&& ImGui::ToolbarButton(m_app.getBigIconFont(), ICON_FA_SAVE, ImGui::GetStyle().Colors[ImGuiCol_Text], "Save"))
{
getMaterial()->getTextureByName(SPLATMAP_SLOT_NAME)->save();
}
if (m_brush_texture)
{
const gpu::TextureHandle th = m_brush_texture->handle;
ImGui::Image((void*)(uintptr_t)th.value, ImVec2(100, 100));
if (ImGui::ToolbarButton(m_app.getBigIconFont(), ICON_FA_TIMES, ImGui::GetStyle().Colors[ImGuiCol_Text], "Clear brush mask"))
{
m_brush_texture->destroy();
LUMIX_DELETE(m_world_editor.getAllocator(), m_brush_texture);
m_brush_mask.clear();
m_brush_texture = nullptr;
}
ImGui::SameLine();
}
ImGui::SameLine();
if (ImGui::ToolbarButton(m_app.getBigIconFont(), ICON_FA_MASK, ImGui::GetStyle().Colors[ImGuiCol_Text], "Select brush mask"))
{
char filename[MAX_PATH_LENGTH];
if (OS::getOpenFilename(Span(filename), "All\0*.*\0", nullptr))
{
int image_width;
int image_height;
int image_comp;
auto* data = stbi_load(filename, &image_width, &image_height, &image_comp, 4);
if (data)
{
m_brush_mask.resize(image_width * image_height);
for (int j = 0; j < image_width; ++j)
{
for (int i = 0; i < image_width; ++i)
{
m_brush_mask[i + j * image_width] =
data[image_comp * (i + j * image_width)] > 128;
}
}
auto& rm = m_world_editor.getEngine().getResourceManager();
if (m_brush_texture)
{
m_brush_texture->destroy();
LUMIX_DELETE(m_world_editor.getAllocator(), m_brush_texture);
}
Lumix::IPlugin* plugin = m_world_editor.getEngine().getPluginManager().getPlugin("renderer");
Renderer& renderer = *static_cast<Renderer*>(plugin);
m_brush_texture = LUMIX_NEW(m_world_editor.getAllocator(), Texture)(
Path("brush_texture"), *rm.get(Texture::TYPE), renderer, m_world_editor.getAllocator());
m_brush_texture->create(image_width, image_height, gpu::TextureFormat::RGBA8, data, image_width * image_height * 4);
stbi_image_free(data);
}
}
}
char grass_mode_shortcut[64];
if (m_remove_entity_action->shortcutText(Span(grass_mode_shortcut))) {
ImGuiEx::Label(StaticString<64>("Grass mode (", grass_mode_shortcut, ")"));
ImGui::TextUnformatted(m_remove_entity_action->isActive() ? "Remove" : "Add");
}
int type_count = scene->getGrassCount((EntityRef)m_component.entity);
for (int i = 0; i < type_count; ++i) {
ImGui::SameLine();
if (i == 0 || ImGui::GetContentRegionAvail().x < 50) ImGui::NewLine();
bool b = (m_grass_mask & (1 << i)) != 0;
m_app.getAssetBrowser().tile(scene->getGrassPath((EntityRef)m_component.entity, i), b);
if (ImGui::IsItemClicked()) {
if (!ImGui::GetIO().KeyCtrl) m_grass_mask = 0;
if (b) {
m_grass_mask &= ~(1 << i);
}
else {
m_grass_mask |= 1 << i;
}
}
}
Texture* albedo = getMaterial()->getTextureByName(DETAIL_ALBEDO_SLOT_NAME);
Texture* normal = getMaterial()->getTextureByName(DETAIL_NORMAL_SLOT_NAME);
if (!albedo) {
ImGui::Text("No detail albedo in material %s", getMaterial()->getPath().c_str());
return;
}
if (albedo->isFailure()) {
ImGui::Text("%s failed to load", albedo->getPath().c_str());
return;
}
if (!albedo->isReady()) {
ImGui::Text("Loading %s...", albedo->getPath().c_str());
return;
}
if (!normal) {
ImGui::Text("No detail normal in material %s", getMaterial()->getPath().c_str());
return;
}
if (normal->isFailure()) {
ImGui::Text("%s failed to load", normal->getPath().c_str());
return;
}
if (!normal->isReady()) {
ImGui::Text("Loading %s...", normal->getPath().c_str());
return;
}
if (albedo->layers != normal->layers) {
ImGui::TextWrapped(ICON_FA_EXCLAMATION_TRIANGLE " albedo texture %s has different number of layers than normal texture %s"
, albedo->getPath().c_str()
, normal->getPath().c_str());
}
bool primary = m_layers_mask & 0b1;
bool secondary = m_layers_mask & 0b10;
// TODO shader does not handle secondary surfaces now, so pretend they don't exist
// uncomment once shader is ready
m_layers_mask = 0xb11;
/*ImGuiEx::Label("Primary surface");
ImGui::Checkbox("##prim", &primary);
ImGuiEx::Label("Secondary surface");
ImGui::Checkbox("##sec", &secondary);
if (secondary) {
bool use = m_fixed_value.x >= 0;
ImGuiEx::Label("Use fixed value");
if (ImGui::Checkbox("##fxd", &use)) {
m_fixed_value.x = use ? 0.f : -1.f;
}
if (m_fixed_value.x >= 0) {
ImGuiEx::Label("Min/max");
ImGui::DragFloatRange2("##minmax", &m_fixed_value.x, &m_fixed_value.y, 0.01f, 0, 1);
}
}*/
FileSystem& fs = m_app.getWorldEditor().getEngine().getFileSystem();
if (albedo->getPath() != m_albedo_composite_path) {
m_albedo_composite.loadSync(fs, albedo->getPath());
m_albedo_composite_path = albedo->getPath();
}
m_layers_mask = (primary ? 1 : 0) | (secondary ? 0b10 : 0);
for (i32 i = 0; i < m_albedo_composite.layers.size(); ++i) {
ImGui::SameLine();
if (i == 0 || ImGui::GetContentRegionAvail().x < 50) ImGui::NewLine();
bool b = m_textures_mask & ((u64)1 << i);
m_app.getAssetBrowser().tile(m_albedo_composite.layers[i].red.path, b);
if (ImGui::IsItemClicked()) {
if (!ImGui::GetIO().KeyCtrl) m_textures_mask = 0;
if (b) m_textures_mask &= ~((u64)1 << i);
else m_textures_mask |= (u64)1 << i;
}
if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
ImGui::OpenPopup(StaticString<8>("ctx", i));
}
if (ImGui::BeginPopup(StaticString<8>("ctx", i))) {
if (ImGui::Selectable("Remove surface")) {
compositeTextureRemoveLayer(albedo->getPath(), i);
compositeTextureRemoveLayer(normal->getPath(), i);
m_albedo_composite_path = "";
}
ImGui::EndPopup();
}
}
if (albedo->layers < 255) {
if (ImGui::Button(ICON_FA_PLUS "Add surface")) ImGui::OpenPopup("Add surface");
}
ImGui::SetNextWindowSizeConstraints(ImVec2(200, 100), ImVec2(FLT_MAX, FLT_MAX));
if (ImGui::BeginPopupModal("Add surface")) {
ImGuiEx::Label("Albedo");
m_app.getAssetBrowser().resourceInput("albedo", Span(m_add_layer_popup.albedo), Texture::TYPE);
ImGuiEx::Label("Normal");
m_app.getAssetBrowser().resourceInput("normal", Span(m_add_layer_popup.normal), Texture::TYPE);
if (ImGui::Button(ICON_FA_PLUS "Add")) {
saveCompositeTexture(albedo->getPath(), m_add_layer_popup.albedo);
saveCompositeTexture(normal->getPath(), m_add_layer_popup.normal);
m_albedo_composite_path = "";
}
ImGui::SameLine();
if (ImGui::Button(ICON_FA_TIMES "Cancel")) {
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
}
void TerrainEditor::compositeTextureRemoveLayer(const Path& path, i32 layer) {
CompositeTexture texture(m_app.getAllocator());
FileSystem& fs = m_app.getWorldEditor().getEngine().getFileSystem();
if (!texture.loadSync(fs, path)) {
logError("Renderer") << "Failed to load " << path;
}
else {
texture.layers.erase(layer);
if (!texture.save(fs, path)) {
logError("Renderer") << "Failed to save " << path;
}
}
}
void TerrainEditor::saveCompositeTexture(const Path& path, const char* channel)
{
CompositeTexture texture(m_app.getAllocator());
FileSystem& fs = m_app.getWorldEditor().getEngine().getFileSystem();
if (!texture.loadSync(fs, path)) {
logError("Renderer") << "Failed to load " << path;
}
else {
CompositeTexture::Layer new_layer;
new_layer.red.path = channel;
new_layer.red.src_channel = 0;
new_layer.green.path = channel;
new_layer.green.src_channel = 1;
new_layer.blue.path = channel;
new_layer.blue.src_channel = 2;
new_layer.alpha.path = channel;
new_layer.alpha.src_channel = 3;
texture.layers.push(new_layer);
if (!texture.save(fs, path)) {
logError("Renderer") << "Failed to save " << path;
}
}
}
void TerrainEditor::entityGUI() {
m_mode = Mode::ENTITY;
static char filter[100] = {0};
ImGui::SetNextItemWidth(-20);
ImGui::InputTextWithHint("##filter", "Filter", filter, sizeof(filter));
ImGui::SameLine();
if (ImGuiEx::IconButton(ICON_FA_TIMES, "Clear filter")) {
filter[0] = '\0';
}
static ImVec2 size(-1, 200);
if (ImGui::ListBoxHeader("##prefabs", size)) {
auto& resources = m_app.getAssetCompiler().lockResources();
u32 count = 0;
for (const AssetCompiler::ResourceItem& res : resources) {
if (res.type != PrefabResource::TYPE) continue;
++count;
if (filter[0] != 0 && stristr(res.path.c_str(), filter) == nullptr) continue;
int selected_idx = m_selected_prefabs.find([&](PrefabResource* r) -> bool {
return r && r->getPath() == res.path;
});
bool selected = selected_idx >= 0;
if (ImGui::Checkbox(res.path.c_str(), &selected)) {
if (selected) {
ResourceManagerHub& manager = m_world_editor.getEngine().getResourceManager();
PrefabResource* prefab = manager.load<PrefabResource>(res.path);
m_selected_prefabs.push(prefab);
}
else {
PrefabResource* prefab = m_selected_prefabs[selected_idx];
m_selected_prefabs.swapAndPop(selected_idx);
prefab->getResourceManager().unload(*prefab);
}
}
}
if (count == 0) ImGui::TextUnformatted("No prefabs");
m_app.getAssetCompiler().unlockResources();
ImGui::ListBoxFooter();
}
ImGui::HSplitter("after_prefab", &size);
if(ImGui::Checkbox("Align with normal", &m_is_align_with_normal))
{
if(m_is_align_with_normal) m_is_rotate_x = m_is_rotate_y = m_is_rotate_z = false;
}
if (ImGui::Checkbox("Rotate around X", &m_is_rotate_x))
{
if (m_is_rotate_x) m_is_align_with_normal = false;
}
if (m_is_rotate_x)
{
Vec2 tmp = m_rotate_x_spread;
tmp.x = radiansToDegrees(tmp.x);
tmp.y = radiansToDegrees(tmp.y);
if (ImGui::DragFloat2("Rotate X spread", &tmp.x))
{
m_rotate_x_spread.x = degreesToRadians(tmp.x);
m_rotate_x_spread.y = degreesToRadians(tmp.y);
}
}
if (ImGui::Checkbox("Rotate around Y", &m_is_rotate_y))
{
if (m_is_rotate_y) m_is_align_with_normal = false;
}
if (m_is_rotate_y)
{
Vec2 tmp = m_rotate_y_spread;
tmp.x = radiansToDegrees(tmp.x);
tmp.y = radiansToDegrees(tmp.y);
if (ImGui::DragFloat2("Rotate Y spread", &tmp.x))
{
m_rotate_y_spread.x = degreesToRadians(tmp.x);
m_rotate_y_spread.y = degreesToRadians(tmp.y);
}
}
if(ImGui::Checkbox("Rotate around Z", &m_is_rotate_z))
{
if(m_is_rotate_z) m_is_align_with_normal = false;
}
if (m_is_rotate_z)
{
Vec2 tmp = m_rotate_z_spread;
tmp.x = radiansToDegrees(tmp.x);
tmp.y = radiansToDegrees(tmp.y);
if (ImGui::DragFloat2("Rotate Z spread", &tmp.x))
{
m_rotate_z_spread.x = degreesToRadians(tmp.x);
m_rotate_z_spread.y = degreesToRadians(tmp.y);
}
}
ImGui::DragFloat2("Size spread", &m_size_spread.x, 0.01f);
m_size_spread.x = minimum(m_size_spread.x, m_size_spread.y);
ImGui::DragFloat2("Y spread", &m_y_spread.x, 0.01f);
m_y_spread.x = minimum(m_y_spread.x, m_y_spread.y);
}
void TerrainEditor::onGUI()
{
auto* scene = static_cast<RenderScene*>(m_component.scene);
ImGui::Unindent();
if (!ImGui::CollapsingHeader("Terrain editor")) {
ImGui::Indent();
return;
}
ImGui::Indent();
ImGuiEx::Label("Enable");
ImGui::Checkbox("##ed_enabled", &m_is_enabled);
if (!m_is_enabled) return;
if (!getMaterial()) {
ImGui::Text("No material");
return;
}
if (!getMaterial()->getTextureByName(HEIGHTMAP_SLOT_NAME)) {
ImGui::Text("No heightmap");
return;
}
ImGuiEx::Label("Brush size");
ImGui::DragFloat("##br_size", &m_terrain_brush_size, 1, MIN_BRUSH_SIZE, FLT_MAX);
ImGuiEx::Label("Brush strength");
ImGui::SliderFloat("##br_str", &m_terrain_brush_strength, 0, 1.0f);
if (ImGui::BeginTabBar("brush_type")) {
if (ImGui::BeginTabItem("Height")) {
m_mode = Mode::HEIGHT;
if (ImGui::ToolbarButton(m_app.getBigIconFont(), ICON_FA_SAVE, ImGui::GetStyle().Colors[ImGuiCol_Text], "Save"))
{
getMaterial()->getTextureByName(HEIGHTMAP_SLOT_NAME)->save();
}
ImGuiEx::Label("Mode");
if (m_is_flat_height) {
ImGui::TextUnformatted("flat");
}
else if (m_smooth_terrain_action->isActive()) {
char shortcut[64];
if (m_smooth_terrain_action->shortcutText(Span(shortcut))) {
ImGui::Text("smooth (%s)", shortcut);
}
else {
ImGui::TextUnformatted("smooth");
}
}
else if (m_lower_terrain_action->isActive()) {
char shortcut[64];
if (m_lower_terrain_action->shortcutText(Span(shortcut))) {
ImGui::Text("lower (%s)", shortcut);
}
else {
ImGui::TextUnformatted("lower");
}
}
else {
ImGui::TextUnformatted("raise");
}
ImGui::Checkbox("Flat", &m_is_flat_height);
if (m_is_flat_height) {
ImGui::SameLine();
ImGui::Text("- Press Ctrl to pick height");
}
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Surface and grass")) {
layerGUI();
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Entity")) {
entityGUI();
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
if (!m_component.isValid() || !m_is_enabled) {
return;
}
const Vec2 mp = m_world_editor.getView().getMousePos();
for(auto entity : m_world_editor.getSelectedEntities()) {
if (!m_world_editor.getUniverse()->hasComponent(entity, TERRAIN_TYPE)) continue;
RenderScene* scene = static_cast<RenderScene*>(m_component.scene);
DVec3 origin;
Vec3 dir;
m_world_editor.getView().getViewport().getRay(mp, origin, dir);
const RayCastModelHit hit = scene->castRayTerrain(entity, origin, dir);
if(hit.is_hit) {
DVec3 center = hit.origin + hit.dir * hit.t;
drawCursor(*scene, entity, center);
return;
}
}
}
void TerrainEditor::paint(const DVec3& hit_pos, ActionType action_type, bool old_stroke)
{
PaintTerrainCommand* command = LUMIX_NEW(m_world_editor.getAllocator(), PaintTerrainCommand)(m_world_editor,
action_type,
m_grass_mask,
(u64)m_textures_mask,
hit_pos,
m_brush_mask,
m_terrain_brush_size,
m_terrain_brush_strength,
m_flat_height,
m_color,
m_component,
m_layers_mask,
m_fixed_value,
old_stroke);
m_world_editor.executeCommand(command);
}
} // namespace Lumix