LumixEngine/src/renderer/editor/terrain_editor.cpp
Mikulas Florek e06c67b2f7 cleanup
2023-06-27 23:38:17 +02:00

2235 lines
No EOL
67 KiB
C++

#include <imgui/imgui.h>
#include "terrain_editor.h"
#include "editor/asset_browser.h"
#include "editor/asset_compiler.h"
#include "editor/entity_folders.h"
#include "editor/prefab_system.h"
#include "editor/studio_app.h"
#include "editor/utils.h"
#include "engine/core.h"
#include "engine/crt.h"
#include "engine/engine.h"
#include "engine/geometry.h"
#include "engine/hash.h"
#include "engine/log.h"
#include "engine/lua_wrapper.h"
#include "engine/os.h"
#include "engine/path.h"
#include "engine/prefab.h"
#include "engine/profiler.h"
#include "engine/resource_manager.h"
#include "engine/stack_array.h"
#include "engine/world.h"
#include "physics/physics_module.h"
#include "renderer/culling_system.h"
#include "renderer/draw_stream.h"
#include "renderer/editor/composite_texture.h"
#include "renderer/material.h"
#include "renderer/model.h"
#include "renderer/render_module.h"
#include "renderer/renderer.h"
#include "renderer/terrain.h"
#include "renderer/texture.h"
#include "stb/stb_image.h"
namespace Lumix
{
static const ComponentType INSTANCED_MODEL_TYPE = reflection::getComponentType("instanced_model");
static const ComponentType MODEL_INSTANCE_TYPE = reflection::getComponentType("model_instance");
static const ComponentType TERRAIN_TYPE = reflection::getComponentType("terrain");
static const ComponentType SPLINE_TYPE = reflection::getComponentType("spline");
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 FillClearGrassCommand final : IEditorCommand {
FillClearGrassCommand(u32 grass_idx, bool fill, EntityRef terrain, WorldEditor& editor)
: m_world_editor(editor)
, m_terrain(terrain)
, m_grass_idx(grass_idx)
, m_fill(fill)
, m_old_data(editor.getAllocator())
{}
Texture* getDestinationTexture() const
{
RenderModule* module = (RenderModule*)m_world_editor.getWorld()->getModule(TERRAIN_TYPE);
return module->getTerrainMaterial(m_terrain)->getTextureByName(SPLATMAP_SLOT_NAME);
}
bool merge(IEditorCommand& command) override { return false; }
bool execute() override {
Texture* texture = getDestinationTexture();
if (!texture) return false;
const u32 Bpp = gpu::getBytesPerPixel(texture->format);
if (4 != Bpp) return false;
u16* data = (u16*)texture->getData();
if (!data) return false;
m_old_data.resize(texture->width * texture->height * 4);
if (m_old_data.empty()) return false;
memcpy(m_old_data.begin(), data, m_old_data.byte_size());
u16 grass_mask = 1 << m_grass_idx;
for (u32 j = 0; j < texture->height; ++j) {
for (u32 i = 0; i < texture->width; ++i) {
const u32 index = Bpp * (i + j * texture->width) + 2;
if (m_fill)
data[index / sizeof(data[0])] |= grass_mask;
else
data[index / sizeof(data[0])] &= ~grass_mask;
}
}
texture->onDataUpdated(0, 0, texture->width, texture->height);
RenderModule* module = (RenderModule*)m_world_editor.getWorld()->getModule(TERRAIN_TYPE);
module->getTerrain(m_terrain)->setGrassDirty();
return true;
}
void undo() override {
ASSERT(!m_old_data.empty());
Texture* texture = getDestinationTexture();
ASSERT(texture);
ASSERT(gpu::getBytesPerPixel(texture->format) == 4);
ASSERT(texture->width * texture->height * 4 == m_old_data.byte_size());
u8* data = texture->getData();
ASSERT(data);
memcpy(data, m_old_data.begin(), m_old_data.byte_size());
texture->onDataUpdated(0, 0, texture->width, texture->height);
RenderModule* module = (RenderModule*)m_world_editor.getWorld()->getModule(TERRAIN_TYPE);
module->getTerrain(m_terrain)->setGrassDirty();
}
const char* getType() override { return "fill_clear_grass"; }
WorldEditor& m_world_editor;
EntityRef m_terrain;
u32 m_grass_idx;
bool m_fill;
Array<u8> m_old_data;
};
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,
EntityRef 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)
{
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;
World& world = *editor.getWorld();
const Transform entity_transform = world.getTransform(terrain).inverted();
RenderModule* module = (RenderModule*)world.getModule(TERRAIN_TYPE);
DVec3 local_pos = entity_transform.transform(hit_pos);
float terrain_size = module->getTerrainSize(terrain).x;
local_pos = local_pos / terrain_size;
local_pos.y = -1;
Item& item = m_items.emplace();
item.m_local_pos = Vec3(local_pos);
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:
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;
}
RenderModule* module = (RenderModule*)m_world_editor.getWorld()->getModule(TERRAIN_TYPE);
return module->getTerrainMaterial(m_terrain)->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)
{
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);
if (m_action_type != TerrainEditor::LAYER && m_action_type != TerrainEditor::REMOVE_GRASS)
{
IModule* module = m_world_editor.getWorld()->getModule("physics");
if (!module) return;
auto* phy_module = static_cast<PhysicsModule*>(module);
if (!module->getWorld().hasComponent(m_terrain, HEIGHTFIELD_TYPE)) return;
phy_module->updateHeighfieldData(m_terrain, m_x, m_y, m_width, m_height, &data[0], bpp);
}
else {
RenderModule* module = (RenderModule*)m_world_editor.getWorld()->getModule(TERRAIN_TYPE);
return module->getTerrain(m_terrain)->setGrassDirty();
}
}
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& first_item = m_items[0];
rect.from_x = maximum(int(s * (first_item.m_local_pos.x - first_item.m_radius) - 0.5f), 0);
rect.from_y = maximum(int(s * (first_item.m_local_pos.z - first_item.m_radius) - 0.5f), 0);
rect.to_x = minimum(1 + int(s * (first_item.m_local_pos.x + first_item.m_radius) + 0.5f), texture->width);
rect.to_y = minimum(1 + int(s * (first_item.m_local_pos.z + first_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:
WorldEditor& m_world_editor;
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;
EntityRef m_terrain;
Array<bool> m_mask;
u16 m_flat_height;
u32 m_layers_masks;
Vec2 m_fixed_value;
bool m_can_be_merged;
};
TerrainEditor::~TerrainEditor()
{
m_app.removeAction(&m_smooth_terrain_action);
m_app.removeAction(&m_lower_terrain_action);
m_app.removeAction(&m_remove_grass_action);
m_app.removeAction(&m_remove_entity_action);
if (m_brush_texture)
{
m_brush_texture->destroy();
LUMIX_DELETE(m_app.getAllocator(), m_brush_texture);
}
m_app.removePlugin(*this);
Engine& engine = m_app.getEngine();
Lumix::ISystem* system = engine.getSystemManager().getSystem("renderer");
Renderer& renderer = *static_cast<Renderer*>(system);
DrawStream& stream = renderer.getDrawStream();
for (gpu::TextureHandle t : m_layer_views) stream.destroy(t);
}
TerrainEditor::TerrainEditor(StudioApp& app)
: m_app(app)
, m_color(1, 1, 1)
, m_current_brush(0)
, m_selected_prefabs(app.getAllocator())
, m_brush_mask(app.getAllocator())
, m_brush_texture(nullptr)
, m_flat_height(0)
, m_is_enabled(false)
, m_size_spread(1, 1)
, m_y_spread(0, 0)
, m_layer_views(app.getAllocator())
{
m_smooth_terrain_action.init("Smooth terrain", "Terrain editor - smooth", "smoothTerrain", "", false);
m_lower_terrain_action.init("Lower terrain", "Terrain editor - lower", "lowerTerrain", "", false);
m_remove_grass_action.init("Remove grass from terrain", "Terrain editor - remove grass", "removeGrassFromTerrain", "", false);
m_remove_entity_action.init("Remove entities from terrain", "Terrain editor - remove entities", "removeEntitiesFromTerrain", "", false);
app.addAction(&m_smooth_terrain_action);
app.addAction(&m_lower_terrain_action);
app.addAction(&m_remove_grass_action);
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);
}
struct TerrainTextureChangeCommand : IEditorCommand {
TerrainTextureChangeCommand(WorldEditor& editor, Terrain& terrain, bool splatmap, IAllocator& allocator)
: editor(editor)
, before(allocator)
, after(allocator)
, entity(terrain.m_entity)
, splatmap(splatmap)
{
Texture* texture = splatmap ? terrain.m_splatmap : terrain.m_heightmap;
const u8* mask = texture->getData();
const u32 size = texture->width * texture->height * (splatmap ? 4 : 2);
before.resize(size);
after.resize(size);
memcpy(before.getMutableData(), mask, size);
memcpy(after.getMutableData(), mask, size);
}
bool apply(OutputMemoryStream& blob) {
RenderModule* render_module = (RenderModule*)editor.getWorld()->getModule(TERRAIN_TYPE);
Terrain* terrain = render_module->getTerrain(entity);
if (!terrain) return false;
Texture* texture = splatmap ? terrain->m_splatmap : terrain->m_heightmap;
if (!texture) return false;
if (!texture->isReady()) return false;
u8* data = texture->getData();
if (!data) return false;
const u32 bytes_per_pixel = splatmap ? 4 : 2;
if (texture->width * texture->height * bytes_per_pixel != blob.size()) return false;
memcpy(data, blob.data(), blob.size());
texture->onDataUpdated(0, 0, texture->width, texture->height);
terrain->setGrassDirty();
return true;
}
bool execute() override { return apply(after); }
void undo() override { apply(before); }
const char* getType() override { return "terrain_texture_change"; }
bool merge(IEditorCommand& command) override { return false; }
WorldEditor& editor;
EntityRef entity;
OutputMemoryStream before;
OutputMemoryStream after;
bool splatmap;
};
Terrain* TerrainEditor::getTerrain() const {
WorldEditor& editor = m_app.getWorldEditor();
const Array<EntityRef>& selected_entities = editor.getSelectedEntities();
if (selected_entities.size() != 1) return nullptr;
World& world = *editor.getWorld();
bool is_terrain = world.hasComponent(selected_entities[0], TERRAIN_TYPE);
if (!is_terrain) return nullptr;
RenderModule* module = (RenderModule*)world.getModule(TERRAIN_TYPE);
return module->getTerrain(selected_entities[0]);
}
struct PrefabProbability {
PrefabResource* resource;
Vec4 distances;
Vec2 scale;
Vec2 y_offset;
float multiplier = 1.f;
};
struct ModelProbability {
Model* resource;
Vec4 distances;
Vec2 scale;
Vec2 y_offset;
float multiplier = 1.f;
};
static void getPrefabs(StudioApp& app, lua_State* L, i32 idx, const char* key, Array<PrefabProbability>& prefabs) {
const int type = LuaWrapper::getField(L, idx, key);
if (type == LUA_TNIL) luaL_error(L, "missing `%s`", key);
if (type != LUA_TTABLE) luaL_error(L, "`%s` is not a table", key);
WorldEditor& editor = app.getWorldEditor();
ResourceManagerHub& rm = editor.getEngine().getResourceManager();
const i32 n = (int)lua_objlen(L, -1);
for (i32 i = 0; i < n; ++i) {
lua_rawgeti(L, -1, i + 1);
if(lua_istable(L, -1)) {
if (LuaWrapper::getField(L, -1, "prefab") != LUA_TSTRING) {
lua_pop(L, 1);
luaL_argerror(L, idx, "'prefab' is not string or is missing");
}
const char* prefab_path = LuaWrapper::toType<const char*>(L, -1);
PrefabResource* res = rm.load<PrefabResource>(Path(prefab_path));
lua_pop(L, 1);
lua_getfield(L, -1, "distances");
if (!LuaWrapper::isType<Vec4>(L, -1)) {
lua_pop(L, 1);
res->decRefCount();
luaL_argerror(L, idx, "'distances' is not vec4 or is missing");
}
const Vec4 distances = LuaWrapper::toType<Vec4>(L, -1);
lua_pop(L, 1);
Vec2 scale = Vec2(1, 1);
LuaWrapper::getOptionalField(L, -1, "scale", &scale);
Vec2 y_offset = Vec2(0, 0);
LuaWrapper::getOptionalField(L, -1, "y_offset", &y_offset);
PrefabProbability& prob = prefabs.emplace();
LuaWrapper::getOptionalField(L, -1, "multiplier", &prob.multiplier);
prob.resource = res;
prob.distances = distances;
prob.scale = scale;
prob.y_offset = y_offset;
}
else {
lua_pop(L, 1);
luaL_argerror(L, idx, "table of prefabs expected");
}
lua_pop(L, 1);
}
lua_pop(L, 1);
}
static void getModels(StudioApp& app, lua_State* L, i32 idx, const char* key, Array<ModelProbability>& prefabs) {
const int type = LuaWrapper::getField(L, idx, key);
if (type == LUA_TNIL) luaL_error(L, "missing `%s`", key);
if (type != LUA_TTABLE) luaL_error(L, "`%s` is not a table", key);
WorldEditor& editor = app.getWorldEditor();
ResourceManagerHub& rm = editor.getEngine().getResourceManager();
const i32 n = (int)lua_objlen(L, -1);
for (i32 i = 0; i < n; ++i) {
lua_rawgeti(L, -1, i + 1);
if(lua_istable(L, -1)) {
if (LuaWrapper::getField(L, -1, "model") != LUA_TSTRING) {
lua_pop(L, 1);
luaL_argerror(L, idx, "'model' is not string or is missing");
}
const char* prefab_path = LuaWrapper::toType<const char*>(L, -1);
Model* res = rm.load<Model>(Path(prefab_path));
lua_pop(L, 1);
lua_getfield(L, -1, "distances");
if (!LuaWrapper::isType<Vec4>(L, -1)) {
lua_pop(L, 1);
res->decRefCount();
luaL_argerror(L, idx, "'distances' is not vec4 or is missing");
}
const Vec4 distances = LuaWrapper::toType<Vec4>(L, -1);
lua_pop(L, 1);
if (distances.x > distances.w) {
res->decRefCount();
luaL_argerror(L, idx, "'distances' are not sorted");
}
Vec2 scale = Vec2(1, 1);
LuaWrapper::getOptionalField(L, -1, "scale", &scale);
Vec2 y_offset = Vec2(0, 0);
LuaWrapper::getOptionalField(L, -1, "y_offset", &y_offset);
ModelProbability& prob = prefabs.emplace();
LuaWrapper::getOptionalField(L, -1, "multiplier", &prob.multiplier);
prob.resource = res;
prob.distances = distances;
prob.scale = scale;
prob.y_offset = y_offset;
}
else {
lua_pop(L, 1);
luaL_argerror(L, idx, "table of models expected");
}
lua_pop(L, 1);
}
lua_pop(L, 1);
}
template <typename T>
static u32 getRandomItem(float distance, const Array<T>& probs) {
float sum = 0;
auto get = [](float distance, const T& prob){
if (distance < prob.distances.x) return 0.f;
if (distance > prob.distances.w) return 0.f;
if (distance < prob.distances.y) {
return prob.multiplier * (distance - prob.distances.x) / (prob.distances.y - prob.distances.x);
}
else if (distance < prob.distances.z) {
return prob.multiplier;
}
return prob.multiplier * (1 - (distance - prob.distances.z) / (prob.distances.w - prob.distances.z));
};
for (const T& prob : probs) {
sum += get(distance, prob);
}
if (sum == 0) return 0xffFFffFF;
float r = randFloat() * sum;
for (i32 i = 0; i < probs.size(); ++i) {
const T& prob = probs[i];
float p = get(distance, prob);
if (r < p) return i;
r -= p;
}
ASSERT(false);
return 0xffFFffFF;
}
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(RenderModule& module, EntityRef entity, const DVec3& center) const
{
PROFILE_FUNCTION();
Terrain* terrain = module.getTerrain(entity);
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) {
module.addDebugCross(center, 1.0f, 0xff0000ff);
return;
}
float brush_size = m_terrain_brush_size;
const Vec3 local_center = Vec3(getRelativePosition(center, entity, module.getWorld()));
const Transform terrain_transform = module.getWorld().getTransform(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 = terrain->getHeight(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 = terrain->getHeight(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);
module.addDebugLine(from, to, Color::RED);
}
const Vec3 rel_pos = Vec3(terrain_transform.inverted().transform(center));
const float scale = terrain->getXZScale();
const IVec3 p = IVec3(rel_pos / scale);
const i32 half_extents = i32(1 + brush_size / scale);
for (i32 j = p.z - half_extents; j <= p.z + half_extents; ++j) {
for (i32 i = p.x - half_extents; i <= p.x + half_extents; ++i) {
DVec3 p00(i * scale, 0, j * scale);
DVec3 p10((i + 1) * scale, 0, j * scale);
DVec3 p11((i + 1) * scale, 0, (j + 1) * scale);
DVec3 p01(i * scale, 0, (j + 1) * scale);
p00.y = terrain->getHeight(i, j);
p10.y = terrain->getHeight(i + 1, j);
p11.y = terrain->getHeight(i + 1, j + 1);
p01.y = terrain->getHeight(i, j + 1);
p00 = terrain_transform.transform(p00);
p10 = terrain_transform.transform(p10);
p01 = terrain_transform.transform(p01);
p11 = terrain_transform.transform(p11);
module.addDebugLine(p10, p01, Color(0x80, 0, 0, 0xff));
module.addDebugLine(p10, p11, Color(0x80, 0, 0, 0xff));
module.addDebugLine(p00, p10, Color(0x80, 0, 0, 0xff));
module.addDebugLine(p01, p11, Color(0x80, 0, 0, 0xff));
module.addDebugLine(p00, p01, Color(0x80, 0, 0, 0xff));
}
}
}
DVec3 TerrainEditor::getRelativePosition(const DVec3& world_pos, EntityRef terrain, World& world) const
{
const Transform transform = world.getTransform(terrain);
const Transform inv_transform = transform.inverted();
return inv_transform.transform(world_pos);
}
u16 TerrainEditor::getHeight(const DVec3& world_pos, RenderModule* module, EntityRef terrain) const
{
const DVec3 rel_pos = getRelativePosition(world_pos, terrain, module->getWorld());
ComponentUID cmp;
cmp.entity = terrain;
cmp.module = module;
cmp.type = TERRAIN_TYPE;
Texture* heightmap = getMaterial(cmp)->getTextureByName(HEIGHTMAP_SLOT_NAME);
if (!heightmap) return 0;
u16* data = (u16*)heightmap->getData();
float scale = module->getTerrainXZScale(terrain);
return data[int(rel_pos.x / scale) + int(rel_pos.z / scale) * heightmap->width];
}
void TerrainEditor::onMouseWheel(float value) {
m_terrain_brush_size = maximum(0.f, m_terrain_brush_size + value);
}
bool TerrainEditor::onMouseDown(WorldView& view, int x, int y)
{
if (!m_is_enabled) return false;
WorldEditor& editor = view.getEditor();
const Array<EntityRef>& selected_entities = editor.getSelectedEntities();
if (selected_entities.size() != 1) return false;
World& world = *editor.getWorld();
bool is_terrain = world.hasComponent(selected_entities[0], TERRAIN_TYPE);
if (!is_terrain) return false;
RenderModule* module = (RenderModule*)world.getModule(TERRAIN_TYPE);
DVec3 origin;
Vec3 dir;
view.getViewport().getRay({(float)x, (float)y}, origin, dir);
const RayCastModelHit hit = module->castRayTerrain(origin, dir);
if (!hit.is_hit) return false;
const DVec3 hit_pos = hit.origin + hit.dir * hit.t;
switch(m_mode) {
case Mode::ENTITY:
if (m_remove_entity_action.isActive()) {
removeEntities(hit_pos, editor);
}
else {
paintEntities(hit_pos, editor, selected_entities[0]);
}
break;
case Mode::HEIGHT:
if (m_is_flat_height) {
if (ImGui::GetIO().KeyCtrl) {
m_flat_height = getHeight(hit_pos, module, selected_entities[0]);
}
else {
paint(hit_pos, TerrainEditor::FLAT_HEIGHT, false, selected_entities[0], editor);
}
}
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, selected_entities[0], editor); 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, selected_entities[0], editor);
break;
}
return true;
}
void TerrainEditor::removeEntities(const DVec3& hit_pos, WorldEditor& editor) const
{
if (m_selected_prefabs.empty()) return;
PrefabSystem& prefab_system = editor.getPrefabSystem();
PROFILE_FUNCTION();
World& world = *editor.getWorld();
RenderModule* module = static_cast<RenderModule*>(world.getModule(TERRAIN_TYPE));
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 = module->getRenderables(frustum, RenderableTypes::MESH);
if(meshes) {
meshes->merge(module->getRenderables(frustum, RenderableTypes::MESH_MATERIAL_OVERRIDE));
}
else {
meshes = module->getRenderables(frustum, RenderableTypes::MESH_MATERIAL_OVERRIDE);
}
if(!meshes) return;
editor.beginCommandGroup("remove_entities");
if (m_selected_prefabs.empty())
{
meshes->forEach([&](EntityRef entity){
if (prefab_system.getPrefab(entity).getHashValue() == 0) return;
const Model* model = module->getModelInstanceModel(entity);
const AABB entity_aabb = model ? model->getAABB() : AABB(Vec3::ZERO, Vec3::ZERO);
const bool collide = testOBBCollision(brush_aabb, world.getRelativeMatrix(entity, hit_pos), entity_aabb);
if (collide) editor.destroyEntities(&entity, 1);
});
}
else
{
meshes->forEach([&](EntityRef entity){
for (auto* res : m_selected_prefabs)
{
if (prefab_system.getPrefab(entity) == res->getPath().getHash())
{
const Model* model = module->getModelInstanceModel(entity);
const AABB entity_aabb = model ? model->getAABB() : AABB(Vec3::ZERO, Vec3::ZERO);
const bool collide = testOBBCollision(brush_aabb, world.getRelativeMatrix(entity, hit_pos), entity_aabb);
if (collide) editor.destroyEntities(&entity, 1);
}
}
});
}
editor.endCommandGroup();
meshes->free(module->getEngine().getPageAllocator());
}
static bool isOBBCollision(RenderModule& module,
const CullResult* meshes,
const Transform& model_tr,
Model* model,
bool ignore_not_in_folder,
const EntityFolders& folders,
EntityFolders::FolderHandle folder)
{
float radius_a_squared = model->getOriginBoundingRadius() * maximum(model_tr.scale.x, model_tr.scale.y, model_tr.scale.z);
radius_a_squared = radius_a_squared * radius_a_squared;
World& world = module.getWorld();
Span<const ModelInstance> model_instances = module.getModelInstances();
const Transform* transforms = world.getTransforms();
while(meshes) {
const EntityRef* entities = meshes->entities;
for (u32 i = 0, c = meshes->header.count; i < c; ++i) {
const EntityRef mesh = entities[i];
// we resolve collisions when painting by removing recently added mesh, but we do not refresh `meshes`
// so it can contain invalid entities
if (!world.hasEntity(mesh)) continue;
const ModelInstance& model_instance = model_instances[mesh.index];
const Transform& tr_b = transforms[mesh.index];
const float radius_b = model_instance.model->getOriginBoundingRadius() * maximum(tr_b.scale.x, tr_b.scale.y, tr_b.scale.z);
const float radius_squared = radius_a_squared + radius_b * radius_b;
if (squaredLength(model_tr.pos - tr_b.pos) < radius_squared) {
const Transform rel_tr = model_tr.inverted() * tr_b;
Matrix mtx = rel_tr.rot.toMatrix();
mtx.multiply3x3(rel_tr.scale);
mtx.setTranslation(Vec3(rel_tr.pos));
if (testOBBCollision(model->getAABB(), mtx, model_instance.model->getAABB())) {
if (ignore_not_in_folder && folders.getFolder(EntityRef{mesh.index}) != folder) {
continue;
}
return true;
}
}
}
meshes = meshes->header.next;
}
return false;
}
static bool areAllReady(Span<PrefabResource*> prefabs) {
for (PrefabResource* p : prefabs) if (!p->isReady()) return false;
return true;
}
void TerrainEditor::paintEntities(const DVec3& hit_pos, WorldEditor& editor, EntityRef terrain) const
{
PROFILE_FUNCTION();
if (m_selected_prefabs.empty()) return;
if (!areAllReady(m_selected_prefabs)) return;
auto& prefab_system = editor.getPrefabSystem();
editor.beginCommandGroup("paint_entities");
{
World& world = *editor.getWorld();
RenderModule* module = static_cast<RenderModule*>(world.getModule(TERRAIN_TYPE));
const Transform terrain_tr = world.getTransform(terrain);
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 = module->getRenderables(frustum, RenderableTypes::MESH);
if (meshes) meshes->merge(module->getRenderables(frustum, RenderableTypes::MESH_MATERIAL_OVERRIDE));
else meshes = module->getRenderables(frustum, RenderableTypes::MESH_MATERIAL_OVERRIDE);
const EntityFolders& folders = editor.getEntityFolders();
const EntityFolders::FolderHandle folder = folders.getSelectedFolder();
Vec2 terrain_size = module->getTerrainSize(terrain);
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 = Vec3(inv_terrain_tr.transform(pos));
if (terrain_pos.x >= 0 && terrain_pos.z >= 0 && terrain_pos.x <= terrain_size.x && terrain_pos.z <= terrain_size.y)
{
pos.y = module->getTerrainHeightAt(terrain, 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)
{
Vec3 normal = module->getTerrainNormalAt(terrain, terrain_pos.x, terrain_pos.z);
Vec3 dir = normalize(cross(normal, Vec3(1, 0, 0)));
Matrix mtx = Matrix::IDENTITY;
mtx.setXVector(cross(normal, dir));
mtx.setYVector(normal);
mtx.setXVector(dir);
rot = mtx.getRotation();
}
else
{
if (m_is_rotate_x)
{
float xangle = randFloat(m_rotate_x_spread.x, m_rotate_x_spread.y);
Quat q(Vec3(1, 0, 0), xangle);
rot = q * rot;
}
if (m_is_rotate_y)
{
float yangle = randFloat(m_rotate_y_spread.x, m_rotate_y_spread.y);
Quat q(Vec3(0, 1, 0), yangle);
rot = q * rot;
}
if (m_is_rotate_z)
{
float zangle = randFloat(m_rotate_z_spread.x, m_rotate_z_spread.y);
Quat q(rot.rotate(Vec3(0, 0, 1)), zangle);
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, Vec3(size));
if (entity.isValid()) {
if (world.hasComponent((EntityRef)entity, MODEL_INSTANCE_TYPE)) {
Model* model = module->getModelInstanceModel((EntityRef)entity);
const Transform tr = { pos, rot, Vec3(size * scale) };
if (isOBBCollision(*module, meshes, tr, model, m_ignore_entities_not_in_folder, folders, folder)) {
editor.undo();
}
}
}
}
}
meshes->free(m_app.getEngine().getPageAllocator());
}
editor.endCommandGroup();
}
void TerrainEditor::onMouseMove(WorldView& view, int x, int y, int, int)
{
if (!m_is_enabled) return;
WorldEditor& editor = view.getEditor();
const Array<EntityRef>& selected_entities = editor.getSelectedEntities();
if (selected_entities.size() != 1) return;
const EntityRef entity = selected_entities[0];
World& world = *editor.getWorld();
if (!world.hasComponent(entity, TERRAIN_TYPE)) return;
RenderModule* module = (RenderModule*)world.getModule(TERRAIN_TYPE);
DVec3 origin;
Vec3 dir;
view.getViewport().getRay({(float)x, (float)y}, origin, dir);
const RayCastModelHit hit = module->castRayTerrain(origin, dir);
if (!hit.is_hit) return;
if (hit.entity != entity) return;
const DVec3 hit_pos = hit.origin + hit.dir * hit.t;
switch(m_mode) {
case Mode::ENTITY:
if (m_remove_entity_action.isActive()) {
removeEntities(hit_pos, editor);
}
else {
paintEntities(hit_pos, editor, entity);
}
break;
case Mode::HEIGHT:
if (m_is_flat_height) {
if (ImGui::GetIO().KeyCtrl) {
m_flat_height = getHeight(hit_pos, module, entity);
}
else {
paint(hit_pos, TerrainEditor::FLAT_HEIGHT, true, selected_entities[0], editor);
}
}
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, true, selected_entities[0], editor); break;
}
break;
case Mode::LAYER:
TerrainEditor::ActionType action = TerrainEditor::LAYER;
if (m_remove_grass_action.isActive()) {
action = TerrainEditor::REMOVE_GRASS;
}
paint(hit_pos, action, true, selected_entities[0], editor);
break;
}
}
Material* TerrainEditor::getMaterial(ComponentUID cmp) const
{
if (!cmp.isValid()) return nullptr;
auto* module = static_cast<RenderModule*>(cmp.module);
return module->getTerrainMaterial((EntityRef)cmp.entity);
}
static Array<u8> getFileContent(const char* path, IAllocator& allocator) {
Array<u8> res(allocator);
os::InputFile file;
if (!file.open(path)) return res;
res.resize((u32)file.size());
if (!file.read(res.begin(), res.byte_size())) res.clear();
file.close();
return res;
}
void TerrainEditor::exportGrass(u32 idx, EntityRef terrain, WorldEditor& editor) {
OutputMemoryStream blob(editor.getAllocator());
RenderModule* module = (RenderModule*)editor.getWorld()->getModule(TERRAIN_TYPE);
Texture* texture = module->getTerrainMaterial(terrain)->getTextureByName(SPLATMAP_SLOT_NAME);
if (!texture) return;
ASSERT(texture->format == gpu::TextureFormat::RGBA8);
const u8* src = texture->getData();
ASSERT(src);
char filename[LUMIX_MAX_PATH];
if (!os::getSaveFilename(Span(filename), "Targa TGA\0*.tga\0", "tga")) return;
const Path path(filename);
Array<u32> data(editor.getAllocator());
data.resize(texture->width * texture->height);
for (u32 j = 0; j < texture->height; ++j) {
for (u32 i = 0; i < texture->width; ++i) {
const u16 grass_mask = *(const u16*)(&src[(i + j * texture->width) * 4 + 2]);
const bool masked = grass_mask & (1 << idx);
data[i + j * texture->width] = masked ? 0xffFFffFF : 0;
}
}
bool saved = Texture::saveTGA(&blob, texture->width, texture->height, gpu::TextureFormat::RGBA8, (const u8*)data.begin(), true, path, editor.getAllocator());
if (!saved) {
logError("Failed to save ", path);
return;
}
os::OutputFile file;
if (!file.open(filename)) {
logError("Failed to open ", filename);
return;
}
if (!file.write(blob.data(), blob.size())) {
logError("Failed to write ", filename, " properly, it's corrupted.");
}
file.close();
}
void TerrainEditor::importGrass(u32 idx, EntityRef terrain, WorldEditor& editor) {
RenderModule* module = (RenderModule*)editor.getWorld()->getModule(TERRAIN_TYPE);
Texture* texture = module->getTerrainMaterial(terrain)->getTextureByName(SPLATMAP_SLOT_NAME);
if (!texture) return;
u8* dst = texture->getData();
ASSERT(dst);
char filename[LUMIX_MAX_PATH];
if (!os::getOpenFilename(Span(filename), "Targa TGA\0*.tga\0", nullptr)) return;
Array<u8> src = getFileContent(filename, editor.getAllocator());
TGAHeader header;
if (src.size() < sizeof(header)) {
logError("Invalid TGA ", filename);
return;
}
memcpy(&header, src.begin(), sizeof(header));
if (header.dataType != 2 && header.bitsPerPixel != 32) {
logError("Unsupported TGA ", filename);
return;
}
if (texture->width != header.width || texture->height != header.height) {
logError("Size of ", filename, " does not match terrain's size");
return;
}
const u32* data = (const u32*)(src.begin() + sizeof(header));
for (u32 j = 0; j < texture->height; ++j) {
const u32 dst_j = header.imageDescriptor & 32 ? j : texture->height - j - 1;
for (u32 i = 0; i < texture->width; ++i) {
u16& grass_mask = *(u16*)(&dst[(i + dst_j * texture->width) * 4 + 2]);
const bool masked = data[i + j * texture->width] != 0;
if (masked) grass_mask |= 1 << idx;
else grass_mask &= ~(1 << idx);
}
}
texture->onDataUpdated(0, 0, texture->width, texture->height);
}
void TerrainEditor::fillGrass(u32 idx, EntityRef terrain, WorldEditor& editor) {
UniquePtr<FillClearGrassCommand> command = UniquePtr<FillClearGrassCommand>::create(editor.getAllocator(),
idx,
true,
terrain,
editor);
editor.executeCommand(command.move());
}
void TerrainEditor::clearGrass(u32 idx, EntityRef terrain, WorldEditor& editor) {
UniquePtr<FillClearGrassCommand> command = UniquePtr<FillClearGrassCommand>::create(editor.getAllocator(),
idx,
false,
terrain,
editor);
editor.executeCommand(command.move());
}
static void thumbnail(gpu::TextureHandle texture, float size, bool selected) {
ImVec2 img_size(size, size);
ImGui::Image(texture, img_size);
if (selected) {
ImDrawList* dl = ImGui::GetWindowDrawList();
const u32 color = ImGui::ColorConvertFloat4ToU32(ImGui::GetStyle().Colors[ImGuiCol_ButtonActive]);
dl->AddRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), color, 0, 0, 3.f);
}
}
Renderer& TerrainEditor::getRenderer() {
Engine& engine = m_app.getEngine();
Lumix::ISystem* system = engine.getSystemManager().getSystem("renderer");
return *static_cast<Renderer*>(system);
}
void TerrainEditor::layerGUI(ComponentUID cmp) {
m_mode = Mode::LAYER;
RenderModule* module = static_cast<RenderModule*>(cmp.module);
Material* material = module->getTerrainMaterial((EntityRef)cmp.entity);
if (!material) return;
if (material->getTextureByName(SPLATMAP_SLOT_NAME) && ImGuiEx::ToolbarButton(m_app.getBigIconFont(), ICON_FA_SAVE, ImGui::GetStyle().Colors[ImGuiCol_Text], "Save"))
{
material->getTextureByName(SPLATMAP_SLOT_NAME)->save();
}
if (m_brush_texture)
{
ImGui::Image(m_brush_texture->handle, ImVec2(100, 100));
if (ImGuiEx::ToolbarButton(m_app.getBigIconFont(), ICON_FA_TIMES, ImGui::GetStyle().Colors[ImGuiCol_Text], "Clear brush mask"))
{
m_brush_texture->destroy();
LUMIX_DELETE(m_app.getAllocator(), m_brush_texture);
m_brush_mask.clear();
m_brush_texture = nullptr;
}
ImGui::SameLine();
}
ImGui::SameLine();
if (ImGuiEx::ToolbarButton(m_app.getBigIconFont(), ICON_FA_MASK, ImGui::GetStyle().Colors[ImGuiCol_Text], "Select brush mask"))
{
char filename[LUMIX_MAX_PATH];
if (os::getOpenFilename(Span(filename), "All\0*.*\0", nullptr))
{
int image_width;
int image_height;
int image_comp;
Array<u8> tmp = getFileContent(filename, m_app.getAllocator());
auto* data = stbi_load_from_memory(tmp.begin(), tmp.byte_size(), &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;
}
}
Engine& engine = m_app.getEngine();
ResourceManagerHub& rm = engine.getResourceManager();
if (m_brush_texture)
{
m_brush_texture->destroy();
LUMIX_DELETE(m_app.getAllocator(), m_brush_texture);
}
m_brush_texture = LUMIX_NEW(m_app.getAllocator(), Texture)(
Path("brush_texture"), *rm.get(Texture::TYPE), getRenderer(), m_app.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 = module->getGrassCount((EntityRef)cmp.entity);
for (int i = 0; i < type_count; ++i) {
ImGui::SameLine();
ImGui::PushID(i);
if (i == 0 || ImGui::GetContentRegionAvail().x < 50) ImGui::NewLine();
bool b = (m_grass_mask & (1 << i)) != 0;
m_app.getAssetBrowser().tile(module->getGrassPath((EntityRef)cmp.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;
}
}
if (ImGui::BeginPopupContextItem("grs_ctx")) {
if (ImGui::Selectable("Fill")) fillGrass(i, *cmp.entity, m_app.getWorldEditor());
if (ImGui::Selectable("Clear")) clearGrass(i, *cmp.entity, m_app.getWorldEditor());
if (ImGui::Selectable("Export")) exportGrass(i, *cmp.entity, m_app.getWorldEditor());
if (ImGui::Selectable("Import")) importGrass(i, *cmp.entity, m_app.getWorldEditor());
ImGui::EndPopup();
}
ImGui::PopID();
}
Texture* albedo = material->getTextureByName(DETAIL_ALBEDO_SLOT_NAME);
Texture* normal = material->getTextureByName(DETAIL_NORMAL_SLOT_NAME);
if (!albedo) {
ImGui::Text("No detail albedo in material %s", material->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", material->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->depth != normal->depth) {
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);
}
}*/
if ((albedo->getPath() != m_albedo_composite_path || albedo->depth != m_layer_views.size()) && albedo->isReady()) {
m_albedo_composite_path = albedo->getPath();
DrawStream& stream = getRenderer().getDrawStream();
for (gpu::TextureHandle t : m_layer_views) {
stream.destroy(t);
}
m_layer_views.clear();
for (u32 layer = 0; layer < albedo->depth; ++layer) {
gpu::TextureHandle view = gpu::allocTextureHandle();
m_layer_views.push(view);
stream.createTextureView(view, albedo->handle, layer);
}
}
m_layers_mask = (primary ? 1 : 0) | (secondary ? 0b10 : 0);
for (u32 i = 0; i < albedo->depth; ++i) {
ImGui::SameLine();
if (i == 0 || ImGui::GetContentRegionAvail().x < 50) ImGui::NewLine();
bool b = m_textures_mask & ((u64)1 << i);
if (i < (u32)m_layer_views.size()) {
thumbnail(m_layer_views[i], 75, m_textures_mask & ((u64)1 << i));
}
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->depth < 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) const {
CompositeTexture texture(m_app, m_app.getAllocator());
FileSystem& fs = m_app.getEngine().getFileSystem();
if (!texture.loadSync(fs, path)) {
logError("Failed to load ", path);
}
else {
texture.removeArrayLayer(layer);
if (!texture.save(fs, path)) {
logError("Failed to save ", path);
}
}
}
void TerrainEditor::saveCompositeTexture(const Path& path, const char* channel) const
{
CompositeTexture texture(m_app, m_app.getAllocator());
FileSystem& fs = m_app.getEngine().getFileSystem();
if (!texture.loadSync(fs, path)) {
logError("Failed to load ", path);
}
else {
texture.addArrayLayer(channel);
if (!texture.save(fs, path)) {
logError("Failed to save ", path);
}
}
}
void TerrainEditor::entityGUI() {
m_mode = Mode::ENTITY;
ImGuiEx::Label("Ignore other folders (?)");
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("When placing entities, ignore collisions with "
"entities in other folders than the currently selected folder "
"in hierarchy view");
}
ImGui::Checkbox("##ignore_filter", &m_ignore_entities_not_in_folder);
static char filter[100] = {0};
const float w = ImGui::CalcTextSize(ICON_FA_TIMES).x + ImGui::GetStyle().ItemSpacing.x * 2;
ImGui::SetNextItemWidth(-w);
ImGui::InputTextWithHint("##filter", "Filter", filter, sizeof(filter), ImGuiInputTextFlags_AutoSelectAll);
ImGui::SameLine();
if (ImGuiEx::IconButton(ICON_FA_TIMES, "Clear filter")) {
filter[0] = '\0';
}
static ImVec2 size(-1, 200);
if (ImGui::BeginListBox("##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;
const char* loading_str = selected_idx >= 0 && m_selected_prefabs[selected_idx]->isEmpty() ? " - loading..." : "";
StaticString<LUMIX_MAX_PATH + 15> label(res.path.c_str(), loading_str);
if (ImGui::Selectable(label, &selected)) {
if (selected) {
ResourceManagerHub& manager = m_app.getEngine().getResourceManager();
PrefabResource* prefab = manager.load<PrefabResource>(res.path);
if (!ImGui::GetIO().KeyShift) {
for (PrefabResource* p : m_selected_prefabs) p->decRefCount();
m_selected_prefabs.clear();
}
m_selected_prefabs.push(prefab);
}
else {
PrefabResource* prefab = m_selected_prefabs[selected_idx];
if (!ImGui::GetIO().KeyShift) {
for (PrefabResource* p : m_selected_prefabs) p->decRefCount();
m_selected_prefabs.clear();
}
else {
m_selected_prefabs.swapAndPop(selected_idx);
prefab->decRefCount();
}
}
}
}
if (count == 0) ImGui::TextUnformatted("No prefabs");
m_app.getAssetCompiler().unlockResources();
ImGui::EndListBox();
}
ImGuiEx::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::DragFloatRange2("Rotate X spread", &tmp.x, &tmp.y))
{
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::DragFloatRange2("Rotate Y spread", &tmp.x, &tmp.y))
{
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::DragFloatRange2("Rotate Z spread", &tmp.x, &tmp.y))
{
m_rotate_z_spread.x = degreesToRadians(tmp.x);
m_rotate_z_spread.y = degreesToRadians(tmp.y);
}
}
ImGui::DragFloatRange2("Size spread", &m_size_spread.x, &m_size_spread.y, 0.01f);
m_size_spread.x = minimum(m_size_spread.x, m_size_spread.y);
ImGui::DragFloatRange2("Y spread", &m_y_spread.x, &m_y_spread.y, 0.01f);
m_y_spread.x = minimum(m_y_spread.x, m_y_spread.y);
}
void TerrainEditor::exportToOBJ(ComponentUID cmp) const {
char filename[LUMIX_MAX_PATH];
if (!os::getSaveFilename(Span(filename), "Wavefront obj\0*.obj\0", "obj")) return;
os::OutputFile file;
if (!file.open(filename)) {
logError("Failed to open ", filename);
return;
}
char basename[LUMIX_MAX_PATH];
copyString(Span(basename), Path::getBasename(filename));
auto* module = static_cast<RenderModule*>(cmp.module);
const EntityRef e = (EntityRef)cmp.entity;
const Texture* hm = getMaterial(cmp)->getTextureByName(HEIGHTMAP_SLOT_NAME);
OutputMemoryStream blob(m_app.getAllocator());
blob.reserve(8 * 1024 * 1024);
blob << "mtllib " << basename << ".mtl\n";
blob << "o Terrain\n";
const float xz_scale = module->getTerrainXZScale(e);
const float y_scale = module->getTerrainYScale(e);
ASSERT(hm->format == gpu::TextureFormat::R16);
const u16* hm_data = (const u16*)hm->getData();
for (u32 j = 0; j < hm->height; ++j) {
for (u32 i = 0; i < hm->width; ++i) {
const float height = hm_data[i + j * hm->width] / float(0xffff) * y_scale;
blob << "v " << i * xz_scale << " " << height << " " << j * xz_scale << "\n";
}
}
for (u32 j = 0; j < hm->height; ++j) {
for (u32 i = 0; i < hm->width; ++i) {
blob << "vt " << i / float(hm->width - 1) << " " << j / float(hm->height - 1) << "\n";
}
}
blob << "usemtl Material\n";
auto write_face_vertex = [&](u32 idx){
blob << idx << "/" << idx;
};
for (u32 j = 0; j < hm->height - 1; ++j) {
for (u32 i = 0; i < hm->width - 1; ++i) {
const u32 idx = i + j * hm->width + 1;
blob << "f ";
write_face_vertex(idx);
blob << " ";
write_face_vertex(idx + 1);
blob << " ";
write_face_vertex(idx + 1 + hm->width);
blob << "\n";
blob << "f ";
write_face_vertex(idx);
blob << " ";
write_face_vertex(idx + 1 + hm->width);
blob << " ";
write_face_vertex(idx + hm->width);
blob << "\n";
}
}
if (!file.write(blob.data(), blob.size())) {
logError("Failed to write ", filename);
}
file.close();
char dir[LUMIX_MAX_PATH];
copyString(Span(dir), Path::getDir(filename));
StaticString<LUMIX_MAX_PATH> mtl_filename(dir, basename, ".mtl");
if (!file.open(mtl_filename)) {
logError("Failed to open ", mtl_filename);
return;
}
blob.clear();
blob << "newmtl Material";
if (!file.write(blob.data(), blob.size())) {
logError("Failed to write ", mtl_filename);
}
file.close();
}
static bool readData(Texture& texture, u32 x, u32 y, u32 w, u32 h, OutputMemoryStream& blob) {
u8* pixels = texture.getData();
if (!pixels) return false;
if (x + w > texture.width) return false;
if (y + h > texture.height) return false;
const u32 bpp = gpu::getBytesPerPixel(texture.format);
blob.resize(bpp * w * h);
u8* dst = blob.getMutableData();
for (u32 j = y; j < y + h; ++j) {
for (u32 i = x; i < x + w; ++i) {
const i32 index = bpp * (i + j * texture.width);
for (u32 k = 0; k < bpp; ++k) {
dst[index + k] = pixels[bpp * (i - x + (j - y) * w) + k];
}
}
}
return true;
}
struct TextureEditCommand final : IEditorCommand {
TextureEditCommand(WorldEditor& editor)
: m_editor(editor)
, m_new_data(editor.getAllocator())
, m_old_data(editor.getAllocator())
{}
bool set(OutputMemoryStream& data) {
World* world = m_editor.getWorld();
RenderModule* module = (RenderModule*)world->getModule(TERRAIN_TYPE);
Terrain* terrain = module->getTerrain(m_entity);
Texture* texture = m_is_splatmap ? terrain->getSplatmap() : terrain->getHeightmap();
if (!texture || !texture->isReady()) return false;
u8* pixels = texture->getData();
if (!pixels) return false;
const u32 bpp = gpu::getBytesPerPixel(texture->format);
ASSERT(data.size() >= m_w * m_h * bpp);
for (u32 j = m_y; j < m_y + m_h; ++j) {
for (u32 i = m_x; i < m_x + m_w; ++i) {
const i32 index = bpp * (i + j * texture->width);
for (u32 k = 0; k < bpp; ++k) {
pixels[index + k] = data[bpp * (i - m_x + (j - m_y) * m_w) + k];
}
}
}
texture->onDataUpdated(m_x, m_y, m_w, m_h);
if (m_dirty_grass) terrain->setGrassDirty();
return true;
}
bool execute() override { return set(m_new_data); }
void undo() override {
bool res = set(m_old_data);
ASSERT(res);
}
const char* getType() override { return "terrain_editor_tex_edit"; }
bool merge(IEditorCommand& command) override { return false; }
WorldEditor& m_editor;
OutputMemoryStream m_new_data;
OutputMemoryStream m_old_data;
EntityRef m_entity;
u32 m_x, m_y, m_w, m_h;
bool m_dirty_grass;
bool m_is_splatmap;
};
void TerrainEditor::updateHeightmap(Terrain* terrain, OutputMemoryStream&& new_data, u32 x, u32 y, u32 w, u32 h) {
Texture* texture = terrain->getHeightmap();
WorldEditor& world_editor = m_app.getWorldEditor();
UniquePtr<TextureEditCommand> cmd = UniquePtr<TextureEditCommand>::create(world_editor.getAllocator(), world_editor);
cmd->m_is_splatmap = false;
cmd->m_x = x;
cmd->m_y = y;
cmd->m_w = w;
cmd->m_h = h;
cmd->m_entity = terrain->m_entity;
cmd->m_dirty_grass = false;
if (!readData(*texture, x, y, w, h, cmd->m_old_data)) return;
cmd->m_new_data = static_cast<OutputMemoryStream&&>(new_data);
world_editor.executeCommand(cmd.move());
}
void TerrainEditor::updateSplatmap(Terrain* terrain, OutputMemoryStream&& new_data, u32 x, u32 y, u32 w, u32 h, bool dirty_grass) {
Texture* texture = terrain->getSplatmap();
WorldEditor& world_editor = m_app.getWorldEditor();
UniquePtr<TextureEditCommand> cmd = UniquePtr<TextureEditCommand>::create(world_editor.getAllocator(), world_editor);
cmd->m_is_splatmap = true;
cmd->m_x = x;
cmd->m_y = y;
cmd->m_w = w;
cmd->m_h = h;
cmd->m_entity = terrain->m_entity;
cmd->m_dirty_grass = dirty_grass;
if (!readData(*texture, x, y, w, h, cmd->m_old_data)) return;
cmd->m_new_data = static_cast<OutputMemoryStream&&>(new_data);
world_editor.executeCommand(cmd.move());
}
void TerrainEditor::onGUI(ComponentUID cmp, WorldEditor& editor) {
ASSERT(cmp.type == TERRAIN_TYPE);
RenderModule* module = static_cast<RenderModule*>(cmp.module);
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;
Material* material = getMaterial(cmp);
if (!material) {
ImGui::Text("No material");
return;
}
if (!material->getTextureByName(HEIGHTMAP_SLOT_NAME)) {
ImGui::Text("No heightmap");
return;
}
if (ImGui::Button(ICON_FA_FILE_EXPORT)) exportToOBJ(cmp);
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 (ImGuiEx::ToolbarButton(m_app.getBigIconFont(), ICON_FA_SAVE, ImGui::GetStyle().Colors[ImGuiCol_Text], "Save"))
{
material->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(cmp);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Entity")) {
entityGUI();
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
if (!cmp.isValid() || !m_is_enabled) {
return;
}
const Vec2 mp = editor.getView().getMousePos();
World& world = *editor.getWorld();
for(auto entity : editor.getSelectedEntities()) {
if (!world.hasComponent(entity, TERRAIN_TYPE)) continue;
DVec3 origin;
Vec3 dir;
editor.getView().getViewport().getRay(mp, origin, dir);
const RayCastModelHit hit = module->castRayTerrain(origin, dir);
if(hit.is_hit) {
DVec3 center = hit.origin + hit.dir * hit.t;
drawCursor(*module, entity, center);
return;
}
}
}
void TerrainEditor::paint(const DVec3& hit_pos, ActionType action_type, bool old_stroke, EntityRef terrain, WorldEditor& editor) const
{
UniquePtr<PaintTerrainCommand> command = UniquePtr<PaintTerrainCommand>::create(editor.getAllocator(),
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,
terrain,
m_layers_mask,
m_fixed_value,
old_stroke);
editor.executeCommand(command.move());
}
} // namespace Lumix