Add project files.
This commit is contained in:
commit
a9ae435321
52 files changed
+65378
No files matched your search
@@ -0,0 +1,16 @@
|
||||
add_executable(hammerpp
|
||||
main.cpp
|
||||
editor.cpp
|
||||
render.cpp
|
||||
viewport.cpp
|
||||
ui.cpp)
|
||||
target_link_libraries(hammerpp PRIVATE hmap hpp_imgui raylib)
|
||||
target_compile_definitions(hammerpp PRIVATE HPP_ASSET_DIR="${CMAKE_SOURCE_DIR}/assets")
|
||||
if(MSVC)
|
||||
target_compile_options(hammerpp PRIVATE /W3 /utf-8)
|
||||
endif()
|
||||
|
||||
# keep the assets next to the executable so the editor also runs from anywhere
|
||||
add_custom_command(TARGET hammerpp POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/assets $<TARGET_FILE_DIR:hammerpp>/assets
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/samples $<TARGET_FILE_DIR:hammerpp>/samples)
|
||||
@@ -0,0 +1,790 @@
|
||||
#include "editor.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
|
||||
namespace hpp {
|
||||
|
||||
bool Selection::has(const ObjRef& r) const {
|
||||
for (const ObjRef& o : objs)
|
||||
if (o == r) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
void Editor::log(const char* fmt, ...) {
|
||||
char buf[512];
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
std::vsnprintf(buf, sizeof buf, fmt, ap);
|
||||
va_end(ap);
|
||||
logLines.push_back(buf);
|
||||
if (logLines.size() > 500) logLines.erase(logLines.begin(), logLines.begin() + 100);
|
||||
TraceLog(LOG_INFO, "[Hammer++] %s", buf);
|
||||
}
|
||||
|
||||
// ============================================================ lifecycle
|
||||
void Editor::init() {
|
||||
renderer.init();
|
||||
entdefs = hmap::EntityRegistry::builtin();
|
||||
|
||||
// locate the asset folder: ./assets, <exe>/assets, or the source tree it was built from
|
||||
std::string appAssets = std::string(GetApplicationDirectory()) + "assets";
|
||||
if (DirectoryExists("assets")) assetRoot = "assets";
|
||||
else if (DirectoryExists(appAssets.c_str())) assetRoot = appAssets;
|
||||
#ifdef HPP_ASSET_DIR
|
||||
else assetRoot = HPP_ASSET_DIR;
|
||||
#endif
|
||||
std::string defsPath = assetRoot + "/entities.def";
|
||||
if (FileExists(defsPath.c_str())) {
|
||||
char* txt = LoadFileText(defsPath.c_str());
|
||||
if (txt) {
|
||||
hmap::Status s = entdefs.parse(txt);
|
||||
UnloadFileText(txt);
|
||||
if (!s) log("entities.def: %s", s.message.c_str());
|
||||
else log("Loaded custom entity definitions from %s", defsPath.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
const char* names[4] = {"3D", "Top", "Front", "Side"};
|
||||
const ViewKind kinds[4] = {ViewKind::Persp, ViewKind::Top, ViewKind::Front, ViewKind::Side};
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
views[i].title = names[i];
|
||||
views[i].kind = kinds[i];
|
||||
views[i].center = {0, 64, 0};
|
||||
views[i].zoom = 0.4f;
|
||||
}
|
||||
views[0].pos = {320, 260, 420};
|
||||
views[0].yaw = 38;
|
||||
views[0].pitch = -22;
|
||||
newMap(true);
|
||||
log("Hammer++ ready. Right-mouse + WASD flies the 3D view; F1 shows all controls.");
|
||||
}
|
||||
|
||||
void Editor::shutdown() {
|
||||
for (Viewport& v : views)
|
||||
if (v.rt.id != 0) UnloadRenderTexture(v.rt);
|
||||
renderer.shutdown();
|
||||
}
|
||||
|
||||
void Editor::touch() {
|
||||
++docVersion;
|
||||
dirty = true;
|
||||
compiledStale = hasCompiled;
|
||||
}
|
||||
|
||||
void Editor::pushUndo() {
|
||||
undoStack.push_back(doc);
|
||||
if (undoStack.size() > 48) undoStack.erase(undoStack.begin());
|
||||
redoStack.clear();
|
||||
}
|
||||
|
||||
void Editor::editBegin() {
|
||||
if (!inEdit) {
|
||||
pushUndo();
|
||||
inEdit = true;
|
||||
}
|
||||
}
|
||||
|
||||
void Editor::undo() {
|
||||
if (undoStack.empty()) return;
|
||||
redoStack.push_back(doc);
|
||||
doc = std::move(undoStack.back());
|
||||
undoStack.pop_back();
|
||||
touch();
|
||||
pruneSelection();
|
||||
}
|
||||
|
||||
void Editor::redo() {
|
||||
if (redoStack.empty()) return;
|
||||
undoStack.push_back(doc);
|
||||
doc = std::move(redoStack.back());
|
||||
redoStack.pop_back();
|
||||
touch();
|
||||
pruneSelection();
|
||||
}
|
||||
|
||||
// ============================================================ documents
|
||||
void Editor::newMap(bool demo) {
|
||||
if (demo) {
|
||||
doc = hmap::makeDemoMap();
|
||||
} else {
|
||||
doc = hmap::Map();
|
||||
hmap::addDefaultMaterials(doc);
|
||||
int wall = doc.findMaterial("dev/wall"), floorM = doc.findMaterial("dev/grid");
|
||||
hmap::Brush shell = hmap::makeBox({-256, -16, -256}, {256, 208, 256}, wall);
|
||||
for (hmap::Brush& w : hmap::hollowBrush(shell, 16)) {
|
||||
for (hmap::Face& f : w.faces)
|
||||
if (f.plane.n.y > 0.9f && std::fabs(f.plane.d) < 0.5f) f.material = floorM;
|
||||
w.id = doc.newId();
|
||||
doc.brushes.push_back(w);
|
||||
}
|
||||
auto addEnt = [&](const char* cls, Vec3 o, Vec3 a) {
|
||||
hmap::Entity e;
|
||||
e.id = doc.newId();
|
||||
e.classname = cls;
|
||||
e.origin = o;
|
||||
e.angles = a;
|
||||
entdefs.applyDefaults(e);
|
||||
doc.entities.push_back(e);
|
||||
};
|
||||
addEnt("light", {0, 150, 0}, {0, 0, 0});
|
||||
addEnt("info_player_start", {0, 36, 128}, {0, 0, 0});
|
||||
addEnt("light_environment", {0, 200, 0}, {-50, 30, 0});
|
||||
}
|
||||
docPath.clear();
|
||||
undoStack.clear();
|
||||
redoStack.clear();
|
||||
sel.clear();
|
||||
hasCompiled = compiledStale = false;
|
||||
curMaterial = std::max(0, doc.findMaterial("dev/grid"));
|
||||
grid = doc.settings.gridSize > 0 ? doc.settings.gridSize : 16;
|
||||
touch();
|
||||
dirty = false;
|
||||
cache.version = 0;
|
||||
}
|
||||
|
||||
bool Editor::openMap(const std::string& path) {
|
||||
hmap::Map m;
|
||||
hmap::Status s = hmap::loadMap(path, m);
|
||||
if (!s) {
|
||||
log("Open failed: %s", s.message.c_str());
|
||||
return false;
|
||||
}
|
||||
doc = std::move(m);
|
||||
docPath = path;
|
||||
undoStack.clear();
|
||||
redoStack.clear();
|
||||
sel.clear();
|
||||
hasCompiled = compiledStale = false;
|
||||
if (doc.materials.empty()) hmap::addDefaultMaterials(doc);
|
||||
curMaterial = std::min(curMaterial, (int)doc.materials.size() - 1);
|
||||
grid = doc.settings.gridSize > 0 ? doc.settings.gridSize : 16;
|
||||
touch();
|
||||
dirty = false;
|
||||
cache.version = 0;
|
||||
log("Opened %s (%zu brushes, %zu meshes, %zu entities)", path.c_str(), doc.brushes.size(), doc.meshes.size(), doc.entities.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Editor::saveMap(const std::string& path) {
|
||||
doc.settings.gridSize = grid;
|
||||
hmap::Status s = hmap::saveMap(path, doc, saveCompiledToo ? &compiled : nullptr);
|
||||
if (!s) {
|
||||
log("Save failed: %s", s.message.c_str());
|
||||
return false;
|
||||
}
|
||||
docPath = path;
|
||||
dirty = false;
|
||||
log("Saved %s", path.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
void Editor::compileNow() {
|
||||
compiled = hmap::compile(doc, compileOpt);
|
||||
hasCompiled = true;
|
||||
compiledStale = false;
|
||||
renderer.setCompiledBatches(compiled.batches);
|
||||
const hmap::CompileStats& st = compiled.stats;
|
||||
log("Compiled in %.0f ms: %u brushes, %u meshes, %u triangles, BSP %u nodes / %u leaves (depth %u), %u lights%s", st.ms, st.brushes,
|
||||
st.meshes, st.triangles, st.bspNodes, st.bspLeaves, st.bspDepth, st.lights, compiled.baked ? ", lighting baked" : "");
|
||||
if (compileOpt.bakeLighting) shading = Shading::Baked;
|
||||
}
|
||||
|
||||
void Editor::exportFbx(const std::string& path) {
|
||||
hmap::CompiledMap cm = hmap::compile(doc, compileOpt);
|
||||
hmap::Status s = hmap::exportFBX(path, cm, fbxOpt);
|
||||
log(s ? "Exported FBX: %s (%u triangles)" : "FBX export failed: %s", s ? path.c_str() : s.message.c_str(), cm.stats.triangles);
|
||||
}
|
||||
|
||||
void Editor::exportObj(const std::string& path) {
|
||||
hmap::CompiledMap cm = hmap::compile(doc, compileOpt);
|
||||
hmap::Status s = hmap::exportOBJ(path, cm, fbxOpt.scale);
|
||||
log(s ? "Exported OBJ: %s" : "OBJ export failed: %s", s ? path.c_str() : s.message.c_str());
|
||||
}
|
||||
|
||||
void Editor::exportCompiled(const std::string& path) {
|
||||
hmap::CompiledMap cm = hmap::compile(doc, compileOpt);
|
||||
hmap::Status s = hmap::saveCompiled(path, cm);
|
||||
log(s ? "Exported runtime map: %s" : "Export failed: %s", s ? path.c_str() : s.message.c_str());
|
||||
}
|
||||
|
||||
// ============================================================ scene cache / picking
|
||||
const SceneCache& Editor::scene() {
|
||||
if (cache.version != docVersion) {
|
||||
cache.brushGeo.resize(doc.brushes.size());
|
||||
for (size_t i = 0; i < doc.brushes.size(); ++i) cache.brushGeo[i] = hmap::buildBrushGeometry(doc.brushes[i]);
|
||||
cache.meshBounds.resize(doc.meshes.size());
|
||||
for (size_t i = 0; i < doc.meshes.size(); ++i) cache.meshBounds[i] = hmap::meshBounds(doc.meshes[i]);
|
||||
cache.version = docVersion;
|
||||
refreshGpu();
|
||||
}
|
||||
return cache;
|
||||
}
|
||||
|
||||
void Editor::refreshGpu() {
|
||||
std::vector<hmap::Batch> batches;
|
||||
hmap::buildEditBatches(doc, batches);
|
||||
renderer.setEditBatches(batches);
|
||||
lights = hmap::collectLights(doc);
|
||||
}
|
||||
|
||||
AABB Editor::entityBounds(const hmap::Entity& e) {
|
||||
const hmap::EntityClass* c = classOf(e);
|
||||
Vec3 half = c ? c->half : Vec3(8, 8, 8);
|
||||
AABB bb;
|
||||
if (c && c->hasModel) {
|
||||
std::string path = e.get("model");
|
||||
if (!path.empty()) {
|
||||
ModelEntry& me = renderer.model(path, assetRoot);
|
||||
if (me.ok) {
|
||||
Mat34 xf = e.transform();
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
Vec3 p((i & 1) ? me.box.max.x : me.box.min.x, (i & 2) ? me.box.max.y : me.box.min.y, (i & 4) ? me.box.max.z : me.box.min.z);
|
||||
bb.add(xf.point(p));
|
||||
}
|
||||
return bb;
|
||||
}
|
||||
}
|
||||
}
|
||||
Vec3 s(std::fabs(e.scale.x), std::fabs(e.scale.y), std::fabs(e.scale.z));
|
||||
Vec3 h(half.x * (c && c->hasModel ? s.x : 1.0f), half.y * (c && c->hasModel ? s.y : 1.0f), half.z * (c && c->hasModel ? s.z : 1.0f));
|
||||
bb.add(e.origin - h);
|
||||
bb.add(e.origin + h);
|
||||
return bb;
|
||||
}
|
||||
|
||||
Vec3 Editor::entityColor(const hmap::Entity& e) const {
|
||||
const hmap::EntityClass* c = classOf(e);
|
||||
return c ? c->color : Vec3(0.8f, 0.8f, 0.8f);
|
||||
}
|
||||
|
||||
AABB Editor::objectBounds(const ObjRef& r) {
|
||||
const SceneCache& sc = scene();
|
||||
switch (r.type) {
|
||||
case ObjType::Brush:
|
||||
for (size_t i = 0; i < doc.brushes.size(); ++i)
|
||||
if (doc.brushes[i].id == r.id) return sc.brushGeo[i].bounds;
|
||||
break;
|
||||
case ObjType::Mesh:
|
||||
for (size_t i = 0; i < doc.meshes.size(); ++i)
|
||||
if (doc.meshes[i].id == r.id) return sc.meshBounds[i];
|
||||
break;
|
||||
case ObjType::Entity:
|
||||
if (const hmap::Entity* e = doc.findEntity(r.id)) return entityBounds(*e);
|
||||
break;
|
||||
}
|
||||
return AABB();
|
||||
}
|
||||
|
||||
AABB Editor::selectionBounds() {
|
||||
AABB bb;
|
||||
for (const ObjRef& r : sel.objs) bb.add(objectBounds(r));
|
||||
return bb;
|
||||
}
|
||||
|
||||
Vec3 Editor::selectionCenter() {
|
||||
AABB bb = selectionBounds();
|
||||
return bb.empty() ? Vec3() : bb.center();
|
||||
}
|
||||
|
||||
Hit Editor::pick(const HRay& r) {
|
||||
Hit best;
|
||||
const SceneCache& sc = scene();
|
||||
for (size_t i = 0; i < doc.brushes.size(); ++i) {
|
||||
const hmap::Brush& b = doc.brushes[i];
|
||||
if (b.flags & hmap::OBJ_HIDDEN) continue;
|
||||
const AABB& bb = sc.brushGeo[i].bounds;
|
||||
float tn, tf;
|
||||
if (bb.empty() || !hmap::rayAABB(r.o, r.d, bb.expanded(0.05f), tn, tf)) continue;
|
||||
float t;
|
||||
int f;
|
||||
if (hmap::rayBrush(b, r.o, r.d, t, f) && t < best.t) {
|
||||
best.hit = true; best.t = t; best.obj = {ObjType::Brush, b.id}; best.face = f; best.normal = b.faces[(size_t)f].plane.n;
|
||||
}
|
||||
}
|
||||
for (size_t i = 0; i < doc.meshes.size(); ++i) {
|
||||
const hmap::MeshObject& m = doc.meshes[i];
|
||||
if (m.flags & hmap::OBJ_HIDDEN) continue;
|
||||
float tn, tf;
|
||||
if (sc.meshBounds[i].empty() || !hmap::rayAABB(r.o, r.d, sc.meshBounds[i].expanded(0.05f), tn, tf)) continue;
|
||||
float t;
|
||||
int p;
|
||||
if (hmap::rayMesh(m, r.o, r.d, t, p) && t < best.t) {
|
||||
hmap::Polygon poly;
|
||||
for (uint32_t k : m.polys[(size_t)p].idx) poly.push_back(m.verts[k]);
|
||||
best.hit = true; best.t = t; best.obj = {ObjType::Mesh, m.id}; best.face = p; best.normal = hmap::polyNormal(poly);
|
||||
}
|
||||
}
|
||||
for (const hmap::Entity& e : doc.entities) {
|
||||
const hmap::EntityClass* c = classOf(e);
|
||||
if ((c && c->brush) || (e.flags & hmap::OBJ_HIDDEN)) continue;
|
||||
AABB bb = entityBounds(e);
|
||||
float tn, tf;
|
||||
if (!hmap::rayAABB(r.o, r.d, bb, tn, tf)) continue;
|
||||
float t = std::max(tn, 0.0f);
|
||||
if (t < best.t) {
|
||||
best.hit = true; best.t = t; best.obj = {ObjType::Entity, e.id}; best.face = -1; best.normal = {0, 1, 0};
|
||||
}
|
||||
}
|
||||
if (best.hit) best.point = r.o + r.d * best.t;
|
||||
return best;
|
||||
}
|
||||
|
||||
// ============================================================ selection
|
||||
void Editor::clearSelection() { sel.clear(); }
|
||||
|
||||
void Editor::select(const ObjRef& r, bool additive) {
|
||||
if (!additive) {
|
||||
sel.clear();
|
||||
sel.objs.push_back(r);
|
||||
return;
|
||||
}
|
||||
auto it = std::find(sel.objs.begin(), sel.objs.end(), r);
|
||||
if (it != sel.objs.end()) sel.objs.erase(it);
|
||||
else sel.objs.push_back(r);
|
||||
sel.faces.clear();
|
||||
sel.verts.clear();
|
||||
}
|
||||
|
||||
void Editor::selectAll() {
|
||||
sel.clear();
|
||||
for (const hmap::Brush& b : doc.brushes)
|
||||
if (!(b.flags & hmap::OBJ_HIDDEN)) sel.objs.push_back({ObjType::Brush, b.id});
|
||||
for (const hmap::MeshObject& m : doc.meshes)
|
||||
if (!(m.flags & hmap::OBJ_HIDDEN)) sel.objs.push_back({ObjType::Mesh, m.id});
|
||||
for (const hmap::Entity& e : doc.entities) {
|
||||
const hmap::EntityClass* c = classOf(e);
|
||||
if (!(c && c->brush) && !(e.flags & hmap::OBJ_HIDDEN)) sel.objs.push_back({ObjType::Entity, e.id});
|
||||
}
|
||||
}
|
||||
|
||||
void Editor::pruneSelection() {
|
||||
std::vector<ObjRef> keep;
|
||||
for (const ObjRef& r : sel.objs) {
|
||||
bool ok = r.type == ObjType::Brush ? doc.findBrush(r.id) != nullptr
|
||||
: r.type == ObjType::Mesh ? doc.findMesh(r.id) != nullptr
|
||||
: doc.findEntity(r.id) != nullptr;
|
||||
if (ok) keep.push_back(r);
|
||||
}
|
||||
sel.objs = keep;
|
||||
ObjRef p = sel.primary();
|
||||
int limit = 0;
|
||||
if (p.type == ObjType::Brush && doc.findBrush(p.id)) limit = (int)doc.findBrush(p.id)->faces.size();
|
||||
if (p.type == ObjType::Mesh && doc.findMesh(p.id)) limit = (int)doc.findMesh(p.id)->polys.size();
|
||||
sel.faces.erase(std::remove_if(sel.faces.begin(), sel.faces.end(), [&](int f) { return f >= limit; }), sel.faces.end());
|
||||
int vlimit = (p.type == ObjType::Mesh && doc.findMesh(p.id)) ? (int)doc.findMesh(p.id)->verts.size() : 0;
|
||||
sel.verts.erase(std::remove_if(sel.verts.begin(), sel.verts.end(), [&](int v) { return v >= vlimit; }), sel.verts.end());
|
||||
}
|
||||
|
||||
bool Editor::selectedBrushes(std::vector<hmap::Brush*>& out) {
|
||||
for (const ObjRef& r : sel.objs)
|
||||
if (r.type == ObjType::Brush)
|
||||
if (hmap::Brush* b = doc.findBrush(r.id)) out.push_back(b);
|
||||
return !out.empty();
|
||||
}
|
||||
|
||||
// ============================================================ edit commands
|
||||
static void pruneOrphanEntities(Editor& ed) {
|
||||
auto used = [&](uint32_t id) {
|
||||
for (const hmap::Brush& b : ed.doc.brushes) if (b.owner == id) return true;
|
||||
for (const hmap::MeshObject& m : ed.doc.meshes) if (m.owner == id) return true;
|
||||
return false;
|
||||
};
|
||||
auto& es = ed.doc.entities;
|
||||
es.erase(std::remove_if(es.begin(), es.end(),
|
||||
[&](const hmap::Entity& e) {
|
||||
const hmap::EntityClass* c = ed.classOf(e);
|
||||
return c && c->brush && !used(e.id);
|
||||
}),
|
||||
es.end());
|
||||
}
|
||||
|
||||
void Editor::deleteSelection() {
|
||||
if (sel.empty()) return;
|
||||
pushUndo();
|
||||
for (const ObjRef& r : sel.objs) {
|
||||
auto sameId = [&](const auto& o) { return o.id == r.id; };
|
||||
if (r.type == ObjType::Brush) doc.brushes.erase(std::remove_if(doc.brushes.begin(), doc.brushes.end(), sameId), doc.brushes.end());
|
||||
else if (r.type == ObjType::Mesh) doc.meshes.erase(std::remove_if(doc.meshes.begin(), doc.meshes.end(), sameId), doc.meshes.end());
|
||||
else doc.entities.erase(std::remove_if(doc.entities.begin(), doc.entities.end(), sameId), doc.entities.end());
|
||||
}
|
||||
pruneOrphanEntities(*this);
|
||||
sel.clear();
|
||||
touch();
|
||||
}
|
||||
|
||||
void Editor::duplicateSelection() {
|
||||
if (sel.empty()) return;
|
||||
pushUndo();
|
||||
Vec3 off(grid, 0, grid);
|
||||
std::vector<ObjRef> created;
|
||||
for (const ObjRef& r : std::vector<ObjRef>(sel.objs)) {
|
||||
if (r.type == ObjType::Brush) {
|
||||
if (const hmap::Brush* src = doc.findBrush(r.id)) {
|
||||
hmap::Brush b = *src;
|
||||
b.id = doc.newId();
|
||||
hmap::translateBrush(b, off, true);
|
||||
doc.brushes.push_back(b);
|
||||
created.push_back({ObjType::Brush, b.id});
|
||||
}
|
||||
} else if (r.type == ObjType::Mesh) {
|
||||
if (const hmap::MeshObject* src = doc.findMesh(r.id)) {
|
||||
hmap::MeshObject m = *src;
|
||||
m.id = doc.newId();
|
||||
for (Vec3& v : m.verts) v += off;
|
||||
doc.meshes.push_back(m);
|
||||
created.push_back({ObjType::Mesh, m.id});
|
||||
}
|
||||
} else if (const hmap::Entity* src = doc.findEntity(r.id)) {
|
||||
hmap::Entity e = *src;
|
||||
e.id = doc.newId();
|
||||
e.origin += off;
|
||||
doc.entities.push_back(e);
|
||||
created.push_back({ObjType::Entity, e.id});
|
||||
}
|
||||
}
|
||||
sel.clear();
|
||||
sel.objs = created;
|
||||
touch();
|
||||
log("Duplicated %zu object(s)", created.size());
|
||||
}
|
||||
|
||||
void Editor::hideSelection(bool hide) {
|
||||
for (const ObjRef& r : sel.objs) {
|
||||
uint32_t* flags = nullptr;
|
||||
if (r.type == ObjType::Brush) { if (auto* b = doc.findBrush(r.id)) flags = &b->flags; }
|
||||
else if (r.type == ObjType::Mesh) { if (auto* m = doc.findMesh(r.id)) flags = &m->flags; }
|
||||
else if (auto* e = doc.findEntity(r.id)) flags = &e->flags;
|
||||
if (!flags) continue;
|
||||
if (hide) *flags |= hmap::OBJ_HIDDEN;
|
||||
else *flags &= ~(uint32_t)hmap::OBJ_HIDDEN;
|
||||
}
|
||||
if (hide) sel.clear();
|
||||
touch();
|
||||
}
|
||||
|
||||
void Editor::unhideAll() {
|
||||
for (auto& b : doc.brushes) b.flags &= ~(uint32_t)hmap::OBJ_HIDDEN;
|
||||
for (auto& m : doc.meshes) m.flags &= ~(uint32_t)hmap::OBJ_HIDDEN;
|
||||
for (auto& e : doc.entities) e.flags &= ~(uint32_t)hmap::OBJ_HIDDEN;
|
||||
touch();
|
||||
}
|
||||
|
||||
void Editor::hollowSelection(float thickness) {
|
||||
std::vector<uint32_t> ids;
|
||||
for (const ObjRef& r : sel.objs)
|
||||
if (r.type == ObjType::Brush) ids.push_back(r.id);
|
||||
if (ids.empty()) { log("Hollow: select a brush first"); return; }
|
||||
pushUndo();
|
||||
std::vector<ObjRef> next;
|
||||
int hollowed = 0;
|
||||
for (uint32_t id : ids) {
|
||||
const hmap::Brush* b = doc.findBrush(id);
|
||||
if (!b) continue;
|
||||
std::vector<hmap::Brush> walls = hmap::hollowBrush(*b, thickness);
|
||||
if (walls.empty()) continue;
|
||||
doc.brushes.erase(std::remove_if(doc.brushes.begin(), doc.brushes.end(), [&](const hmap::Brush& x) { return x.id == id; }), doc.brushes.end());
|
||||
for (hmap::Brush& w : walls) {
|
||||
w.id = doc.newId();
|
||||
doc.brushes.push_back(w);
|
||||
next.push_back({ObjType::Brush, w.id});
|
||||
}
|
||||
++hollowed;
|
||||
}
|
||||
if (!hollowed) { undoStack.pop_back(); log("Hollow failed: wall thickness is too large for this brush"); return; }
|
||||
sel.clear();
|
||||
sel.objs = next;
|
||||
touch();
|
||||
log("Hollowed %d brush(es) into %zu wall brushes", hollowed, next.size());
|
||||
}
|
||||
|
||||
void Editor::convertSelectionToMesh() {
|
||||
std::vector<uint32_t> ids;
|
||||
for (const ObjRef& r : sel.objs)
|
||||
if (r.type == ObjType::Brush) ids.push_back(r.id);
|
||||
if (ids.empty()) return;
|
||||
pushUndo();
|
||||
std::vector<ObjRef> next;
|
||||
for (const ObjRef& r : sel.objs)
|
||||
if (r.type != ObjType::Brush) next.push_back(r);
|
||||
for (uint32_t id : ids) {
|
||||
const hmap::Brush* b = doc.findBrush(id);
|
||||
if (!b) continue;
|
||||
hmap::MeshObject m = hmap::brushToMesh(*b);
|
||||
m.id = doc.newId();
|
||||
m.name = "mesh " + std::to_string(m.id);
|
||||
doc.brushes.erase(std::remove_if(doc.brushes.begin(), doc.brushes.end(), [&](const hmap::Brush& x) { return x.id == id; }), doc.brushes.end());
|
||||
next.push_back({ObjType::Mesh, m.id});
|
||||
doc.meshes.push_back(std::move(m));
|
||||
}
|
||||
sel.clear();
|
||||
sel.objs = next;
|
||||
touch();
|
||||
log("Converted %zu brush(es) to editable meshes", ids.size());
|
||||
}
|
||||
|
||||
void Editor::tieSelectionToEntity(const std::string& cls) {
|
||||
const hmap::EntityClass* c = entdefs.find(cls);
|
||||
if (!c || !c->brush) return;
|
||||
std::vector<ObjRef> targets;
|
||||
for (const ObjRef& r : sel.objs)
|
||||
if (r.type != ObjType::Entity) targets.push_back(r);
|
||||
if (targets.empty()) { log("Tie to entity: select some brushes or meshes first"); return; }
|
||||
pushUndo();
|
||||
hmap::Entity e;
|
||||
e.id = doc.newId();
|
||||
e.classname = cls;
|
||||
entdefs.applyDefaults(e);
|
||||
uint32_t eid = e.id;
|
||||
doc.entities.push_back(e);
|
||||
for (const ObjRef& r : targets) {
|
||||
if (r.type == ObjType::Brush) {
|
||||
if (auto* b = doc.findBrush(r.id)) { b->owner = eid; b->contents = c->brushContents; }
|
||||
} else if (auto* m = doc.findMesh(r.id)) m->owner = eid;
|
||||
}
|
||||
pruneOrphanEntities(*this);
|
||||
touch();
|
||||
log("Tied %zu object(s) to new %s", targets.size(), cls.c_str());
|
||||
}
|
||||
|
||||
void Editor::untieSelection() {
|
||||
pushUndo();
|
||||
for (const ObjRef& r : sel.objs) {
|
||||
if (r.type == ObjType::Brush) {
|
||||
if (auto* b = doc.findBrush(r.id)) { b->owner = 0; b->contents = hmap::CONTENTS_SOLID; }
|
||||
} else if (r.type == ObjType::Mesh) {
|
||||
if (auto* m = doc.findMesh(r.id)) m->owner = 0;
|
||||
}
|
||||
}
|
||||
pruneOrphanEntities(*this);
|
||||
touch();
|
||||
}
|
||||
|
||||
void Editor::clipSelection(const Plane& p, int mode) {
|
||||
std::vector<uint32_t> ids;
|
||||
for (const ObjRef& r : sel.objs)
|
||||
if (r.type == ObjType::Brush) ids.push_back(r.id);
|
||||
if (ids.empty()) { log("Clip: select the brush(es) to cut first"); return; }
|
||||
pushUndo();
|
||||
std::vector<ObjRef> next;
|
||||
for (uint32_t id : ids) {
|
||||
const hmap::Brush* b = doc.findBrush(id);
|
||||
if (!b) continue;
|
||||
hmap::Brush front, back;
|
||||
hmap::clipBrush(*b, p, &front, &back);
|
||||
std::vector<hmap::Brush> keep;
|
||||
if (!front.faces.empty() && mode != 1) keep.push_back(front);
|
||||
if (!back.faces.empty() && mode != 0) keep.push_back(back);
|
||||
if (keep.empty() || (front.faces.empty() && back.faces.empty())) continue;
|
||||
doc.brushes.erase(std::remove_if(doc.brushes.begin(), doc.brushes.end(), [&](const hmap::Brush& x) { return x.id == id; }), doc.brushes.end());
|
||||
for (hmap::Brush& k : keep) {
|
||||
k.id = doc.newId();
|
||||
doc.brushes.push_back(k);
|
||||
next.push_back({ObjType::Brush, k.id});
|
||||
}
|
||||
}
|
||||
sel.clear();
|
||||
sel.objs = next;
|
||||
touch();
|
||||
}
|
||||
|
||||
void Editor::createPrimitive(Vec3 mn, Vec3 mx) {
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
if (mx[i] < mn[i]) std::swap(mx[i], mn[i]);
|
||||
if (mx[i] - mn[i] < 1.0f) mx[i] = mn[i] + std::max(grid, 1.0f);
|
||||
}
|
||||
pushUndo();
|
||||
std::vector<ObjRef> created;
|
||||
int mat = curMaterial;
|
||||
auto addB = [&](hmap::Brush b) {
|
||||
b.id = doc.newId();
|
||||
created.push_back({ObjType::Brush, b.id});
|
||||
doc.brushes.push_back(std::move(b));
|
||||
};
|
||||
auto addM = [&](hmap::MeshObject m, const char* name) {
|
||||
m.id = doc.newId();
|
||||
m.name = name;
|
||||
created.push_back({ObjType::Mesh, m.id});
|
||||
doc.meshes.push_back(std::move(m));
|
||||
};
|
||||
bool asMesh = createAsMesh || shape == Shape::Sphere || shape == Shape::Torus || shape == Shape::Terrain;
|
||||
auto brushOrMesh = [&](hmap::Brush b, const char* name) {
|
||||
if (asMesh) addM(hmap::brushToMesh(b), name);
|
||||
else addB(std::move(b));
|
||||
};
|
||||
switch (shape) {
|
||||
case Shape::Box:
|
||||
if (asMesh) addM(hmap::meshBox(mn, mx, mat), "box");
|
||||
else addB(hmap::makeBox(mn, mx, mat));
|
||||
break;
|
||||
case Shape::Wedge: brushOrMesh(hmap::makeWedge(mn, mx, mat), "wedge"); break;
|
||||
case Shape::Cylinder:
|
||||
if (asMesh) addM(hmap::meshCylinder(mn, mx, shapeSides, mat), "cylinder");
|
||||
else addB(hmap::makeCylinder(mn, mx, shapeSides, mat));
|
||||
break;
|
||||
case Shape::Cone:
|
||||
if (asMesh) addM(hmap::meshCone(mn, mx, shapeSides, mat), "cone");
|
||||
else addB(hmap::makeCone(mn, mx, shapeSides, mat));
|
||||
break;
|
||||
case Shape::Stairs:
|
||||
for (hmap::Brush& s : hmap::makeStairs(mn, mx, stairSteps, mat)) brushOrMesh(std::move(s), "step");
|
||||
break;
|
||||
case Shape::Sphere: addM(hmap::meshSphere(mn, mx, std::max(4, shapeSides / 2), shapeSides, mat), "sphere"); break;
|
||||
case Shape::Torus: addM(hmap::meshTorus(mn, mx, shapeSides * 2, 12, mat), "torus"); break;
|
||||
case Shape::Terrain: addM(hmap::meshGrid(mn, mx, 16, 16, mat), "terrain"); break;
|
||||
}
|
||||
sel.clear();
|
||||
sel.objs = created;
|
||||
touch();
|
||||
}
|
||||
|
||||
void Editor::createEntityAt(const Vec3& origin) {
|
||||
const hmap::EntityClass* c = entdefs.find(curEntity);
|
||||
if (!c) return;
|
||||
if (c->brush) { log("%s is a brush entity: select brushes and use Tie to entity (Ctrl+T)", curEntity.c_str()); return; }
|
||||
pushUndo();
|
||||
hmap::Entity e;
|
||||
e.id = doc.newId();
|
||||
e.classname = curEntity;
|
||||
e.origin = origin;
|
||||
entdefs.applyDefaults(e);
|
||||
if (c->hasModel && !curModel.empty()) e.set("model", curModel);
|
||||
doc.entities.push_back(e);
|
||||
sel.clear();
|
||||
sel.objs.push_back({ObjType::Entity, e.id});
|
||||
touch();
|
||||
}
|
||||
|
||||
void Editor::applyMaterialToSelection(int material) {
|
||||
if (sel.empty()) return;
|
||||
pushUndo();
|
||||
ObjRef p = sel.primary();
|
||||
for (const ObjRef& r : sel.objs) {
|
||||
if (r.type == ObjType::Brush) {
|
||||
if (auto* b = doc.findBrush(r.id)) {
|
||||
bool faceSel = selMode == SelMode::Face && r == p && !sel.faces.empty();
|
||||
for (size_t i = 0; i < b->faces.size(); ++i)
|
||||
if (!faceSel || std::find(sel.faces.begin(), sel.faces.end(), (int)i) != sel.faces.end()) b->faces[i].material = material;
|
||||
}
|
||||
} else if (r.type == ObjType::Mesh) {
|
||||
if (auto* m = doc.findMesh(r.id)) {
|
||||
bool faceSel = selMode == SelMode::Face && r == p && !sel.faces.empty();
|
||||
for (size_t i = 0; i < m->polys.size(); ++i)
|
||||
if (!faceSel || std::find(sel.faces.begin(), sel.faces.end(), (int)i) != sel.faces.end()) m->polys[i].material = material;
|
||||
}
|
||||
}
|
||||
}
|
||||
touch();
|
||||
}
|
||||
|
||||
void Editor::paintHit(const Hit& h, bool wholeObject) {
|
||||
if (!h.hit) return;
|
||||
pushUndo();
|
||||
if (h.obj.type == ObjType::Brush) {
|
||||
if (auto* b = doc.findBrush(h.obj.id))
|
||||
for (size_t i = 0; i < b->faces.size(); ++i)
|
||||
if (wholeObject || (int)i == h.face) b->faces[i].material = curMaterial;
|
||||
} else if (h.obj.type == ObjType::Mesh) {
|
||||
if (auto* m = doc.findMesh(h.obj.id))
|
||||
for (size_t i = 0; i < m->polys.size(); ++i)
|
||||
if (wholeObject || (int)i == h.face) m->polys[i].material = curMaterial;
|
||||
} else {
|
||||
undoStack.pop_back();
|
||||
return;
|
||||
}
|
||||
touch();
|
||||
}
|
||||
|
||||
void Editor::flipSelection() {
|
||||
pushUndo();
|
||||
for (const ObjRef& r : sel.objs)
|
||||
if (r.type == ObjType::Mesh)
|
||||
if (auto* m = doc.findMesh(r.id)) hmap::flipMesh(*m);
|
||||
touch();
|
||||
}
|
||||
|
||||
void Editor::importObjFile(const std::string& path) {
|
||||
pushUndo();
|
||||
hmap::MeshObject m;
|
||||
hmap::Status s = hmap::importOBJ(path, doc, m);
|
||||
if (!s) {
|
||||
undoStack.pop_back();
|
||||
log("OBJ import failed: %s", s.message.c_str());
|
||||
return;
|
||||
}
|
||||
m.id = doc.newId();
|
||||
sel.clear();
|
||||
sel.objs.push_back({ObjType::Mesh, m.id});
|
||||
log("Imported %s as mesh '%s' (%zu verts, %zu polys)", path.c_str(), m.name.c_str(), m.verts.size(), m.polys.size());
|
||||
doc.meshes.push_back(std::move(m));
|
||||
touch();
|
||||
}
|
||||
|
||||
// ============================================================ drag support
|
||||
DragSnap Editor::captureSnap() {
|
||||
DragSnap s;
|
||||
for (const ObjRef& r : sel.objs) {
|
||||
if (r.type == ObjType::Brush) { if (auto* b = doc.findBrush(r.id)) s.brushes.push_back(*b); }
|
||||
else if (r.type == ObjType::Mesh) { if (auto* m = doc.findMesh(r.id)) s.meshes.push_back(*m); }
|
||||
else if (auto* e = doc.findEntity(r.id)) s.entities.push_back(*e);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
void Editor::restoreSnap(const DragSnap& s) {
|
||||
for (const auto& b : s.brushes) if (auto* p = doc.findBrush(b.id)) *p = b;
|
||||
for (const auto& m : s.meshes) if (auto* p = doc.findMesh(m.id)) *p = m;
|
||||
for (const auto& e : s.entities) if (auto* p = doc.findEntity(e.id)) *p = e;
|
||||
}
|
||||
|
||||
static bool isTranslation(const Mat34& m) {
|
||||
for (int i = 0; i < 3; ++i)
|
||||
for (int j = 0; j < 3; ++j)
|
||||
if (std::fabs(m.m[i][j] - (i == j ? 1.0f : 0.0f)) > 1e-5f) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void Editor::applyMatrix(const Mat34& m, bool lockTexture) {
|
||||
bool pure = isTranslation(m);
|
||||
Vec3 t = m.translation();
|
||||
for (const ObjRef& r : sel.objs) {
|
||||
if (r.type == ObjType::Brush) {
|
||||
if (auto* b = doc.findBrush(r.id)) {
|
||||
if (pure) hmap::translateBrush(*b, t, lockTexture);
|
||||
else hmap::transformBrush(*b, m);
|
||||
}
|
||||
} else if (r.type == ObjType::Mesh) {
|
||||
if (auto* me = doc.findMesh(r.id)) hmap::transformMesh(*me, m);
|
||||
} else if (auto* e = doc.findEntity(r.id)) {
|
||||
e->origin = m.point(e->origin);
|
||||
if (pure) continue;
|
||||
Vec3 c0(m.m[0][0], m.m[1][0], m.m[2][0]), c1(m.m[0][1], m.m[1][1], m.m[2][1]), c2(m.m[0][2], m.m[1][2], m.m[2][2]);
|
||||
Vec3 ls(hmap::length(c0), hmap::length(c1), hmap::length(c2));
|
||||
if (ls.x > 1e-6f && ls.y > 1e-6f && ls.z > 1e-6f) {
|
||||
Mat34 rot;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
rot.m[i][0] = m.m[i][0] / ls.x;
|
||||
rot.m[i][1] = m.m[i][1] / ls.y;
|
||||
rot.m[i][2] = m.m[i][2] / ls.z;
|
||||
}
|
||||
if (rot.det3() > 0) e->angles = (rot * Mat34::fromEuler(e->angles)).toEuler();
|
||||
if (std::fabs(ls.x - 1) > 1e-4f || std::fabs(ls.y - 1) > 1e-4f || std::fabs(ls.z - 1) > 1e-4f)
|
||||
e->scale = Vec3(e->scale.x * ls.x, e->scale.y * ls.y, e->scale.z * ls.z);
|
||||
}
|
||||
}
|
||||
}
|
||||
touch();
|
||||
}
|
||||
|
||||
void Editor::translateSelectedVerts(const Vec3& d) {
|
||||
ObjRef p = sel.primary();
|
||||
if (p.type != ObjType::Mesh) return;
|
||||
if (auto* m = doc.findMesh(p.id))
|
||||
for (int v : sel.verts)
|
||||
if (v >= 0 && (size_t)v < m->verts.size()) m->verts[(size_t)v] += d;
|
||||
touch();
|
||||
}
|
||||
|
||||
} // namespace hpp
|
||||
+284
@@ -0,0 +1,284 @@
|
||||
// Hammer++ editor: shared types. The editor is a thin UI layer over libhmap.
|
||||
#pragma once
|
||||
#include <hmap/hmap.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "raylib.h"
|
||||
#include "render.h"
|
||||
|
||||
namespace hpp {
|
||||
|
||||
using hmap::AABB;
|
||||
using hmap::Mat34;
|
||||
using hmap::Plane;
|
||||
using hmap::Vec2;
|
||||
using hmap::Vec3;
|
||||
|
||||
enum class Tool { Select, Block, Entity, Clip };
|
||||
enum class SelMode { Object, Face, Vertex };
|
||||
enum class Shading { Wire, Flat, Lit, Baked };
|
||||
enum class ViewKind { Persp, Top, Front, Side };
|
||||
enum class ObjType { Brush, Mesh, Entity };
|
||||
enum class Shape { Box, Wedge, Cylinder, Cone, Stairs, Sphere, Torus, Terrain };
|
||||
|
||||
struct ObjRef {
|
||||
ObjType type = ObjType::Brush;
|
||||
uint32_t id = 0;
|
||||
bool operator==(const ObjRef& o) const { return type == o.type && id == o.id; }
|
||||
};
|
||||
|
||||
struct Selection {
|
||||
std::vector<ObjRef> objs;
|
||||
std::vector<int> faces; // faces (brush) / polygons (mesh) of the primary object
|
||||
std::vector<int> verts; // vertex indices of the primary mesh
|
||||
bool has(const ObjRef& r) const;
|
||||
void clear() { objs.clear(); faces.clear(); verts.clear(); }
|
||||
bool empty() const { return objs.empty(); }
|
||||
ObjRef primary() const { return objs.empty() ? ObjRef{} : objs.back(); }
|
||||
};
|
||||
|
||||
struct HRay { Vec3 o, d; };
|
||||
|
||||
struct Hit {
|
||||
bool hit = false;
|
||||
float t = 1e30f;
|
||||
ObjRef obj;
|
||||
int face = -1;
|
||||
Vec3 point, normal;
|
||||
};
|
||||
|
||||
struct Viewport {
|
||||
const char* title = "";
|
||||
ViewKind kind = ViewKind::Persp;
|
||||
bool open = true;
|
||||
RenderTexture2D rt = {};
|
||||
int w = 0, h = 0;
|
||||
// perspective camera
|
||||
Vec3 pos{0, 256, 512};
|
||||
float yaw = 0, pitch = -25, fov = 70, flySpeed = 320;
|
||||
// orthographic camera
|
||||
Vec3 center{0, 0, 0};
|
||||
float zoom = 0.5f; // pixels per world unit
|
||||
// per-frame interaction state
|
||||
ImVec2 imgMin{0, 0};
|
||||
bool hovered = false, flying = false;
|
||||
|
||||
bool ortho() const { return kind != ViewKind::Persp; }
|
||||
Vec3 forward() const;
|
||||
Vec3 right() const;
|
||||
Vec3 up() const;
|
||||
int depthAxis() const; // ortho: world axis along the view direction
|
||||
Camera3D camera() const;
|
||||
HRay ray(float sx, float sy) const;
|
||||
bool project(const Vec3& p, float& sx, float& sy) const; // false when behind the camera
|
||||
Vec3 planePoint(float sx, float sy, float depth) const; // ortho: world point on the plane at `depth`
|
||||
};
|
||||
|
||||
struct DragSnap {
|
||||
std::vector<hmap::Brush> brushes;
|
||||
std::vector<hmap::MeshObject> meshes;
|
||||
std::vector<hmap::Entity> entities;
|
||||
};
|
||||
|
||||
struct DragState {
|
||||
enum Kind { None, Move, Resize, Gizmo, BoxSelect, Block } kind = None;
|
||||
Viewport* view = nullptr;
|
||||
Vec3 startWorld;
|
||||
ImVec2 startScreen{0, 0}, curScreen{0, 0};
|
||||
bool moved = false;
|
||||
bool tookUndo = false;
|
||||
int hx = 0, hy = 0; // resize handle
|
||||
float rectU0 = 0, rectU1 = 0, rectV0 = 0, rectV1 = 0; // selection rect in view-plane coordinates
|
||||
int axis = -1; // gizmo axis
|
||||
float s0 = 0;
|
||||
Vec3 origin;
|
||||
Vec3 blockA, blockB;
|
||||
DragSnap snap;
|
||||
};
|
||||
|
||||
struct SceneCache {
|
||||
uint64_t version = 0;
|
||||
std::vector<hmap::BrushGeometry> brushGeo; // parallel to Map::brushes
|
||||
std::vector<AABB> meshBounds; // parallel to Map::meshes
|
||||
};
|
||||
|
||||
class FileDialog {
|
||||
public:
|
||||
using Callback = std::function<void(const std::string&)>;
|
||||
void open(const std::string& title, const std::string& startDir, std::vector<std::string> exts, bool save, Callback cb,
|
||||
const std::string& defaultName = "");
|
||||
void draw();
|
||||
|
||||
private:
|
||||
std::string title_, dir_, name_;
|
||||
std::vector<std::string> exts_;
|
||||
bool save_ = false, pending_ = false;
|
||||
Callback cb_;
|
||||
};
|
||||
|
||||
class Editor {
|
||||
public:
|
||||
// ------------------------------------------------------------ lifecycle
|
||||
void init();
|
||||
void frame(); // one full UI frame; call between rlImGuiBegin() and rlImGuiEnd()
|
||||
void shutdown();
|
||||
|
||||
// ------------------------------------------------------------ document
|
||||
hmap::Map doc;
|
||||
std::string docPath;
|
||||
bool dirty = false;
|
||||
uint64_t docVersion = 1;
|
||||
hmap::EntityRegistry entdefs;
|
||||
std::string assetRoot = "assets";
|
||||
std::vector<hmap::Map> undoStack, redoStack;
|
||||
|
||||
// ------------------------------------------------------------ state
|
||||
Selection sel;
|
||||
Tool tool = Tool::Select;
|
||||
SelMode selMode = SelMode::Object;
|
||||
Shading shading = Shading::Lit;
|
||||
bool snapOn = true;
|
||||
float grid = 16;
|
||||
int curMaterial = 0;
|
||||
Shape shape = Shape::Box;
|
||||
bool createAsMesh = false;
|
||||
int shapeSides = 12;
|
||||
int stairSteps = 8;
|
||||
std::string curEntity = "prop_static";
|
||||
std::string curModel;
|
||||
float blockHeight = 64;
|
||||
bool saveCompiledToo = false;
|
||||
|
||||
hmap::CompiledMap compiled;
|
||||
bool hasCompiled = false, compiledStale = false;
|
||||
hmap::CompileOptions compileOpt;
|
||||
hmap::FbxOptions fbxOpt;
|
||||
|
||||
Renderer renderer;
|
||||
Viewport views[4];
|
||||
DragState drag;
|
||||
SceneCache cache;
|
||||
std::vector<hmap::Light> lights;
|
||||
uint64_t lightsVersion = 0;
|
||||
|
||||
FileDialog fileDialog;
|
||||
std::vector<std::string> logLines;
|
||||
bool showDemo = false, resetLayout = false, showHelp = false, wantQuit = false;
|
||||
std::function<void()> pendingAction;
|
||||
bool askDiscard = false;
|
||||
void guarded(std::function<void()> act); // runs `act`, asking first when there are unsaved changes
|
||||
bool panelOutliner = true, panelInspector = true, panelAssets = true, panelLog = true;
|
||||
|
||||
// clip tool
|
||||
bool clipHasA = false, clipHasB = false;
|
||||
Vec3 clipA, clipB;
|
||||
int clipMode = 0; // 0 keep front, 1 keep back, 2 keep both
|
||||
int clipView = 0;
|
||||
|
||||
// ------------------------------------------------------------ commands (editor.cpp)
|
||||
void log(const char* fmt, ...);
|
||||
void newMap(bool demo);
|
||||
bool openMap(const std::string& path);
|
||||
bool saveMap(const std::string& path);
|
||||
void touch(); // document changed
|
||||
void pushUndo();
|
||||
void editBegin(); // call before applying an inspector edit (one undo entry per edit session)
|
||||
void undo();
|
||||
void redo();
|
||||
|
||||
void clearSelection();
|
||||
void select(const ObjRef& r, bool additive);
|
||||
void selectAll();
|
||||
void pruneSelection();
|
||||
bool selectedBrushes(std::vector<hmap::Brush*>& out);
|
||||
AABB objectBounds(const ObjRef& r);
|
||||
AABB selectionBounds();
|
||||
Vec3 selectionCenter();
|
||||
hmap::Brush* findBrush(uint32_t id) { return doc.findBrush(id); }
|
||||
const hmap::EntityClass* classOf(const hmap::Entity& e) const { return entdefs.find(e.classname); }
|
||||
AABB entityBounds(const hmap::Entity& e);
|
||||
Vec3 entityColor(const hmap::Entity& e) const;
|
||||
|
||||
void deleteSelection();
|
||||
void duplicateSelection();
|
||||
void hideSelection(bool hide);
|
||||
void unhideAll();
|
||||
void hollowSelection(float thickness);
|
||||
void convertSelectionToMesh();
|
||||
void tieSelectionToEntity(const std::string& cls);
|
||||
void untieSelection();
|
||||
void clipSelection(const Plane& p, int mode);
|
||||
void createPrimitive(Vec3 mn, Vec3 mx);
|
||||
void createEntityAt(const Vec3& origin);
|
||||
void applyMaterialToSelection(int material);
|
||||
void paintHit(const Hit& h, bool wholeObject);
|
||||
void flipSelection();
|
||||
void importObjFile(const std::string& path);
|
||||
|
||||
DragSnap captureSnap();
|
||||
void restoreSnap(const DragSnap& s);
|
||||
void applyMatrix(const Mat34& m, bool lockTexture);
|
||||
void translateSelectedVerts(const Vec3& d);
|
||||
|
||||
void compileNow();
|
||||
void exportFbx(const std::string& path);
|
||||
void exportObj(const std::string& path);
|
||||
void exportCompiled(const std::string& path);
|
||||
|
||||
const SceneCache& scene();
|
||||
void refreshGpu();
|
||||
Hit pick(const HRay& r);
|
||||
float snapValue(float v) const { return snapOn ? std::round(v / grid) * grid : v; }
|
||||
|
||||
// ------------------------------------------------------------ ui (ui.cpp)
|
||||
void drawUI();
|
||||
void drawMenu();
|
||||
void drawToolbar();
|
||||
void drawOutliner();
|
||||
void drawInspector();
|
||||
void drawAssets();
|
||||
void drawLog();
|
||||
void drawHelp();
|
||||
void shortcuts();
|
||||
void buildLayout(ImGuiID dock);
|
||||
bool propEditor(hmap::Entity& e, const hmap::PropDef& p);
|
||||
void entityInspector(hmap::Entity& e, bool brushEntity);
|
||||
void faceInspector();
|
||||
void transformInspector();
|
||||
void openModelPicker(std::function<void(const std::string&)> cb);
|
||||
bool inEdit = false;
|
||||
std::vector<std::string> modelFiles;
|
||||
std::vector<std::string> textureFiles;
|
||||
bool filesScanned = false;
|
||||
void scanAssets();
|
||||
std::string relativeToAssets(const std::string& path) const;
|
||||
|
||||
// ------------------------------------------------------------ viewports (viewport.cpp)
|
||||
void drawViewport(Viewport& v);
|
||||
void viewInput(Viewport& v, bool active);
|
||||
void toolInput3D(Viewport& v, const ImVec2& m);
|
||||
void toolInput2D(Viewport& v, const ImVec2& m);
|
||||
void renderView(Viewport& v);
|
||||
void drawGrid(Viewport& v);
|
||||
void drawScene3D(Viewport& v);
|
||||
void drawWire(Viewport& v);
|
||||
void drawEntities(Viewport& v);
|
||||
void drawGizmo(Viewport& v);
|
||||
void drawOverlay2D(Viewport& v);
|
||||
void focusSelection();
|
||||
bool gizmoVisible() const;
|
||||
Vec3 gizmoCenter();
|
||||
int gizmoHit(Viewport& v, const ImVec2& m);
|
||||
bool selectionRect(Viewport& v, float& u0, float& u1, float& v0, float& v1);
|
||||
};
|
||||
|
||||
void applyTheme();
|
||||
void firstPlace(float fx, float fy, float fw, float fh);
|
||||
|
||||
} // namespace hpp
|
||||
@@ -0,0 +1,39 @@
|
||||
// Hammer++ - map editor built on raylib + Dear ImGui (rlImGui) + libhmap.
|
||||
#include "editor.h"
|
||||
#include "imgui.h"
|
||||
#include "raylib.h"
|
||||
#include "rlImGui.h"
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
SetConfigFlags(FLAG_WINDOW_RESIZABLE | FLAG_VSYNC_HINT | FLAG_MSAA_4X_HINT);
|
||||
InitWindow(1600, 900, "Hammer++");
|
||||
SetWindowMinSize(960, 600);
|
||||
SetExitKey(KEY_NULL); // Esc cancels tools instead of quitting
|
||||
|
||||
rlImGuiSetup(true);
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
#ifdef IMGUI_HAS_DOCK
|
||||
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
|
||||
#endif
|
||||
io.IniFilename = "hammerpp.ini";
|
||||
io.ConfigWindowsMoveFromTitleBarOnly = true;
|
||||
hpp::applyTheme();
|
||||
|
||||
hpp::Editor editor;
|
||||
editor.init();
|
||||
if (argc > 1) editor.openMap(argv[1]);
|
||||
|
||||
while (!WindowShouldClose() && !editor.wantQuit) {
|
||||
BeginDrawing();
|
||||
ClearBackground(Color{20, 22, 27, 255});
|
||||
rlImGuiBegin();
|
||||
editor.frame();
|
||||
rlImGuiEnd();
|
||||
EndDrawing();
|
||||
}
|
||||
|
||||
editor.shutdown();
|
||||
rlImGuiShutdown();
|
||||
CloseWindow();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
#include "render.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#include "raymath.h"
|
||||
|
||||
namespace hpp {
|
||||
|
||||
// One shader serves every geometry source. uMode: 0 = flat shaded, 1 = ambient + sun + point lights
|
||||
// (cheap per-pixel Lambert), 2 = baked per-vertex lighting produced by hmap::bakeLighting().
|
||||
static const char* kVS = R"GLSL(#version 330
|
||||
in vec3 vertexPosition;
|
||||
in vec2 vertexTexCoord;
|
||||
in vec3 vertexNormal;
|
||||
in vec4 vertexColor;
|
||||
uniform mat4 mvp;
|
||||
uniform mat4 matModel;
|
||||
uniform mat4 matNormal;
|
||||
out vec3 fragPos;
|
||||
out vec2 fragUV;
|
||||
out vec3 fragN;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
fragPos = vec3(matModel * vec4(vertexPosition, 1.0));
|
||||
fragUV = vertexTexCoord;
|
||||
fragN = normalize(vec3(matNormal * vec4(vertexNormal, 0.0)));
|
||||
fragColor = vertexColor;
|
||||
gl_Position = mvp * vec4(vertexPosition, 1.0);
|
||||
}
|
||||
)GLSL";
|
||||
|
||||
static const char* kFS = R"GLSL(#version 330
|
||||
in vec3 fragPos;
|
||||
in vec2 fragUV;
|
||||
in vec3 fragN;
|
||||
in vec4 fragColor;
|
||||
uniform sampler2D texture0;
|
||||
uniform vec4 colDiffuse;
|
||||
uniform int uMode;
|
||||
uniform vec3 uAmbient;
|
||||
uniform vec3 uSunDir;
|
||||
uniform vec3 uSunColor;
|
||||
uniform int uLightCount;
|
||||
uniform vec4 uLightPosR[16];
|
||||
uniform vec4 uLightColI[16];
|
||||
out vec4 finalColor;
|
||||
void main() {
|
||||
vec4 tex = texture(texture0, fragUV) * colDiffuse;
|
||||
vec3 n = normalize(fragN);
|
||||
vec3 light;
|
||||
if (uMode == 1) {
|
||||
light = uAmbient + uSunColor * max(dot(n, -uSunDir), 0.0);
|
||||
for (int i = 0; i < uLightCount; i++) {
|
||||
vec3 d = uLightPosR[i].xyz - fragPos;
|
||||
float dist = length(d);
|
||||
float r = uLightPosR[i].w;
|
||||
float att = clamp(1.0 - (dist * dist) / (r * r), 0.0, 1.0);
|
||||
att *= att;
|
||||
light += uLightColI[i].rgb * uLightColI[i].a * max(dot(n, d / max(dist, 0.001)), 0.0) * att;
|
||||
}
|
||||
} else if (uMode == 2) {
|
||||
light = fragColor.rgb;
|
||||
} else {
|
||||
light = vec3(0.50 + 0.50 * max(dot(n, normalize(vec3(0.45, 0.8, 0.4))), 0.0));
|
||||
}
|
||||
finalColor = vec4(tex.rgb * light, tex.a);
|
||||
}
|
||||
)GLSL";
|
||||
|
||||
void Renderer::init() {
|
||||
shader_ = LoadShaderFromMemory(kVS, kFS);
|
||||
locMode_ = GetShaderLocation(shader_, "uMode");
|
||||
locAmbient_ = GetShaderLocation(shader_, "uAmbient");
|
||||
locSunDir_ = GetShaderLocation(shader_, "uSunDir");
|
||||
locSunColor_ = GetShaderLocation(shader_, "uSunColor");
|
||||
locCount_ = GetShaderLocation(shader_, "uLightCount");
|
||||
locPos_ = GetShaderLocation(shader_, "uLightPosR");
|
||||
locCol_ = GetShaderLocation(shader_, "uLightColI");
|
||||
mat_ = LoadMaterialDefault();
|
||||
mat_.shader = shader_;
|
||||
}
|
||||
|
||||
void Renderer::freeGpu(std::vector<Gpu>& v) {
|
||||
for (Gpu& g : v) UnloadMesh(g.mesh);
|
||||
v.clear();
|
||||
}
|
||||
|
||||
void Renderer::clearCaches() {
|
||||
for (auto& kv : textures_) UnloadTexture(kv.second);
|
||||
textures_.clear();
|
||||
for (auto& kv : models_) {
|
||||
if (!kv.second.ok) continue;
|
||||
Shader def = {rlGetShaderIdDefault(), rlGetShaderLocsDefault()};
|
||||
for (int i = 0; i < kv.second.model.materialCount; ++i) kv.second.model.materials[i].shader = def; // keep our shader alive
|
||||
UnloadModel(kv.second.model);
|
||||
}
|
||||
models_.clear();
|
||||
}
|
||||
|
||||
void Renderer::shutdown() {
|
||||
freeGpu(edit_);
|
||||
freeGpu(compiled_);
|
||||
clearCaches();
|
||||
MemFree(mat_.maps);
|
||||
mat_.maps = nullptr;
|
||||
UnloadShader(shader_);
|
||||
}
|
||||
|
||||
void Renderer::upload(const std::vector<hmap::Batch>& in, std::vector<Gpu>& out, size_t& tris) {
|
||||
freeGpu(out);
|
||||
tris = 0;
|
||||
for (const hmap::Batch& b : in) {
|
||||
size_t n = b.indices.size();
|
||||
n -= n % 3;
|
||||
if (n == 0) continue;
|
||||
Mesh m = {};
|
||||
m.vertexCount = (int)n; // expanded to a plain triangle list: no 16-bit index limit
|
||||
m.triangleCount = (int)(n / 3);
|
||||
m.vertices = (float*)MemAlloc((unsigned)(n * 3 * sizeof(float)));
|
||||
m.texcoords = (float*)MemAlloc((unsigned)(n * 2 * sizeof(float)));
|
||||
m.normals = (float*)MemAlloc((unsigned)(n * 3 * sizeof(float)));
|
||||
m.colors = (unsigned char*)MemAlloc((unsigned)(n * 4));
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
const hmap::Vertex& v = b.vertices[b.indices[i]];
|
||||
m.vertices[i * 3 + 0] = v.pos.x; m.vertices[i * 3 + 1] = v.pos.y; m.vertices[i * 3 + 2] = v.pos.z;
|
||||
m.normals[i * 3 + 0] = v.normal.x; m.normals[i * 3 + 1] = v.normal.y; m.normals[i * 3 + 2] = v.normal.z;
|
||||
m.texcoords[i * 2 + 0] = v.uv.x; m.texcoords[i * 2 + 1] = v.uv.y;
|
||||
m.colors[i * 4 + 0] = (unsigned char)(v.color & 0xff);
|
||||
m.colors[i * 4 + 1] = (unsigned char)((v.color >> 8) & 0xff);
|
||||
m.colors[i * 4 + 2] = (unsigned char)((v.color >> 16) & 0xff);
|
||||
m.colors[i * 4 + 3] = (unsigned char)((v.color >> 24) & 0xff);
|
||||
}
|
||||
UploadMesh(&m, false);
|
||||
out.push_back({m, b.material});
|
||||
tris += n / 3;
|
||||
}
|
||||
}
|
||||
|
||||
void Renderer::setEditBatches(const std::vector<hmap::Batch>& b) { upload(b, edit_, editTriangles); }
|
||||
void Renderer::setCompiledBatches(const std::vector<hmap::Batch>& b) { upload(b, compiled_, compiledTriangles); }
|
||||
|
||||
void Renderer::setLights(const std::vector<hmap::Light>& lights, const hmap::Vec3& ambient) {
|
||||
float amb[3] = {ambient.x, ambient.y, ambient.z};
|
||||
float sunDir[3] = {0, -1, 0}, sunCol[3] = {0, 0, 0};
|
||||
float pos[16 * 4] = {}, col[16 * 4] = {};
|
||||
int n = 0;
|
||||
bool haveSun = false;
|
||||
for (const hmap::Light& L : lights) {
|
||||
if (L.type == hmap::LightType::Sun) {
|
||||
if (haveSun) continue;
|
||||
hmap::Vec3 d = hmap::normalize(L.dir);
|
||||
sunDir[0] = d.x; sunDir[1] = d.y; sunDir[2] = d.z;
|
||||
sunCol[0] = L.color.x * L.intensity; sunCol[1] = L.color.y * L.intensity; sunCol[2] = L.color.z * L.intensity;
|
||||
haveSun = true;
|
||||
continue;
|
||||
}
|
||||
if (n >= 16) continue; // spot lights are previewed as point lights
|
||||
pos[n * 4 + 0] = L.pos.x; pos[n * 4 + 1] = L.pos.y; pos[n * 4 + 2] = L.pos.z; pos[n * 4 + 3] = std::max(L.radius, 1.0f);
|
||||
col[n * 4 + 0] = L.color.x; col[n * 4 + 1] = L.color.y; col[n * 4 + 2] = L.color.z; col[n * 4 + 3] = L.intensity;
|
||||
++n;
|
||||
}
|
||||
SetShaderValue(shader_, locAmbient_, amb, SHADER_UNIFORM_VEC3);
|
||||
SetShaderValue(shader_, locSunDir_, sunDir, SHADER_UNIFORM_VEC3);
|
||||
SetShaderValue(shader_, locSunColor_, sunCol, SHADER_UNIFORM_VEC3);
|
||||
SetShaderValue(shader_, locCount_, &n, SHADER_UNIFORM_INT);
|
||||
if (n > 0) {
|
||||
SetShaderValueV(shader_, locPos_, pos, SHADER_UNIFORM_VEC4, n);
|
||||
SetShaderValueV(shader_, locCol_, col, SHADER_UNIFORM_VEC4, n);
|
||||
}
|
||||
}
|
||||
|
||||
void Renderer::setMode(int m) { SetShaderValue(shader_, locMode_, &m, SHADER_UNIFORM_INT); }
|
||||
|
||||
Texture2D Renderer::texture(const hmap::Material& m, const std::string& root) {
|
||||
std::string key = m.name + "|" + m.texture + "|" + root;
|
||||
auto it = textures_.find(key);
|
||||
if (it != textures_.end()) return it->second;
|
||||
Texture2D t = {};
|
||||
if (!m.texture.empty()) {
|
||||
std::string path = root + "/" + m.texture;
|
||||
if (FileExists(path.c_str())) t = LoadTexture(path.c_str());
|
||||
}
|
||||
if (t.id == 0) { // no file: procedural checker, tinted by Material::color at draw time
|
||||
Image img = GenImageChecked(64, 64, 8, 8, Color{236, 236, 236, 255}, Color{176, 176, 182, 255});
|
||||
t = LoadTextureFromImage(img);
|
||||
UnloadImage(img);
|
||||
}
|
||||
GenTextureMipmaps(&t);
|
||||
SetTextureFilter(t, TEXTURE_FILTER_TRILINEAR);
|
||||
SetTextureWrap(t, TEXTURE_WRAP_REPEAT);
|
||||
textures_[key] = t;
|
||||
return t;
|
||||
}
|
||||
|
||||
static bool isTranslucent(const std::string& name) {
|
||||
return name.find("trigger") != std::string::npos || name.find("clip") != std::string::npos ||
|
||||
name.find("water") != std::string::npos || name.find("glass") != std::string::npos;
|
||||
}
|
||||
|
||||
void Renderer::drawBatches(bool useCompiled, const std::vector<hmap::Material>& mats, const std::string& root, int mode) {
|
||||
setMode(mode);
|
||||
static const hmap::Material fallback;
|
||||
std::vector<Gpu>& list = useCompiled ? compiled_ : edit_;
|
||||
for (int pass = 0; pass < 2; ++pass) { // opaque first, then translucent volumes
|
||||
for (Gpu& g : list) {
|
||||
const hmap::Material& hm = (g.material >= 0 && (size_t)g.material < mats.size()) ? mats[(size_t)g.material] : fallback;
|
||||
bool tr = isTranslucent(hm.name);
|
||||
if ((pass == 1) != tr) continue;
|
||||
auto q = [](float v) { return (unsigned char)(std::max(0.0f, std::min(1.0f, v)) * 255.0f); };
|
||||
mat_.maps[MATERIAL_MAP_DIFFUSE].texture = texture(hm, root);
|
||||
mat_.maps[MATERIAL_MAP_DIFFUSE].color = Color{q(hm.color.x), q(hm.color.y), q(hm.color.z), (unsigned char)(tr ? 130 : 255)};
|
||||
DrawMesh(g.mesh, mat_, MatrixIdentity());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ModelEntry& Renderer::model(const std::string& rel, const std::string& root) {
|
||||
auto it = models_.find(rel);
|
||||
if (it != models_.end()) return it->second;
|
||||
ModelEntry me;
|
||||
bool isAbs = rel.size() > 1 && (rel[0] == '/' || rel[1] == ':');
|
||||
std::string path = isAbs ? rel : root + "/" + rel;
|
||||
if (!rel.empty() && FileExists(path.c_str())) {
|
||||
me.model = LoadModel(path.c_str());
|
||||
if (me.model.meshCount > 0) {
|
||||
me.ok = true;
|
||||
for (int i = 0; i < me.model.materialCount; ++i) {
|
||||
me.model.materials[i].shader = shader_;
|
||||
me.baseColors.push_back(me.model.materials[i].maps[MATERIAL_MAP_DIFFUSE].color);
|
||||
}
|
||||
me.box = GetModelBoundingBox(me.model);
|
||||
}
|
||||
}
|
||||
return models_.emplace(rel, std::move(me)).first->second;
|
||||
}
|
||||
|
||||
void Renderer::drawModel(ModelEntry& me, const hmap::Mat34& xf, Color tint, int mode) {
|
||||
if (!me.ok) return;
|
||||
setMode(mode);
|
||||
Matrix t = toRlMatrix(xf);
|
||||
for (int i = 0; i < me.model.meshCount; ++i) {
|
||||
int mi = me.model.meshMaterial[i];
|
||||
Material& m = me.model.materials[mi];
|
||||
Color base = (size_t)mi < me.baseColors.size() ? me.baseColors[(size_t)mi] : WHITE;
|
||||
m.maps[MATERIAL_MAP_DIFFUSE].color = Color{(unsigned char)(base.r * tint.r / 255), (unsigned char)(base.g * tint.g / 255),
|
||||
(unsigned char)(base.b * tint.b / 255), (unsigned char)(base.a * tint.a / 255)};
|
||||
DrawMesh(me.model.meshes[i], m, MatrixMultiply(me.model.transform, t));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace hpp
|
||||
@@ -0,0 +1,79 @@
|
||||
// Renderer: one shader + one draw path for both BSP (brush) batches and mesh-object batches.
|
||||
#pragma once
|
||||
#include <hmap/hmap.h>
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "raylib.h"
|
||||
#include "rlgl.h"
|
||||
|
||||
namespace hpp {
|
||||
|
||||
inline Vector3 toRl(const hmap::Vec3& v) { return {v.x, v.y, v.z}; }
|
||||
|
||||
// hmap::Mat34 uses column vectors (p' = M p + t); raylib's Matrix is laid out so that
|
||||
// MatrixToFloat() yields the same column-major OpenGL matrix.
|
||||
inline Matrix toRlMatrix(const hmap::Mat34& a) {
|
||||
Matrix m;
|
||||
m.m0 = a.m[0][0]; m.m4 = a.m[0][1]; m.m8 = a.m[0][2]; m.m12 = a.m[0][3];
|
||||
m.m1 = a.m[1][0]; m.m5 = a.m[1][1]; m.m9 = a.m[1][2]; m.m13 = a.m[1][3];
|
||||
m.m2 = a.m[2][0]; m.m6 = a.m[2][1]; m.m10 = a.m[2][2]; m.m14 = a.m[2][3];
|
||||
m.m3 = 0; m.m7 = 0; m.m11 = 0; m.m15 = 1;
|
||||
return m;
|
||||
}
|
||||
|
||||
// Batches immediate-mode lines so a huge wireframe never overflows rlgl's vertex buffer.
|
||||
struct LineBatch {
|
||||
int n = 0;
|
||||
void begin() { rlBegin(RL_LINES); n = 0; }
|
||||
void line(const hmap::Vec3& a, const hmap::Vec3& b, Color c) {
|
||||
rlColor4ub(c.r, c.g, c.b, c.a);
|
||||
rlVertex3f(a.x, a.y, a.z);
|
||||
rlVertex3f(b.x, b.y, b.z);
|
||||
if (++n >= 2048) { rlEnd(); rlBegin(RL_LINES); n = 0; }
|
||||
}
|
||||
void end() { rlEnd(); }
|
||||
};
|
||||
|
||||
struct ModelEntry {
|
||||
Model model = {};
|
||||
bool ok = false;
|
||||
BoundingBox box = {};
|
||||
std::vector<Color> baseColors;
|
||||
};
|
||||
|
||||
class Renderer {
|
||||
public:
|
||||
void init();
|
||||
void shutdown();
|
||||
|
||||
void setEditBatches(const std::vector<hmap::Batch>& b); // raw per-object geometry (no CSG)
|
||||
void setCompiledBatches(const std::vector<hmap::Batch>& b); // CSG'd + baked geometry
|
||||
void setLights(const std::vector<hmap::Light>& lights, const hmap::Vec3& ambient);
|
||||
|
||||
// mode: 0 = flat, 1 = dynamic lights, 2 = baked vertex colours
|
||||
void drawBatches(bool compiled, const std::vector<hmap::Material>& mats, const std::string& assetRoot, int mode);
|
||||
void drawModel(ModelEntry& me, const hmap::Mat34& xf, Color tint, int mode);
|
||||
ModelEntry& model(const std::string& relPath, const std::string& assetRoot);
|
||||
Texture2D texture(const hmap::Material& m, const std::string& assetRoot);
|
||||
void clearCaches();
|
||||
|
||||
size_t editTriangles = 0, compiledTriangles = 0;
|
||||
|
||||
private:
|
||||
struct Gpu { Mesh mesh; int material; };
|
||||
void upload(const std::vector<hmap::Batch>& in, std::vector<Gpu>& out, size_t& tris);
|
||||
void freeGpu(std::vector<Gpu>& v);
|
||||
void setMode(int m);
|
||||
|
||||
Shader shader_ = {};
|
||||
Material mat_ = {};
|
||||
int locMode_ = -1, locAmbient_ = -1, locSunDir_ = -1, locSunColor_ = -1, locCount_ = -1, locPos_ = -1, locCol_ = -1;
|
||||
std::vector<Gpu> edit_, compiled_;
|
||||
std::unordered_map<std::string, Texture2D> textures_;
|
||||
std::unordered_map<std::string, ModelEntry> models_;
|
||||
};
|
||||
|
||||
} // namespace hpp
|
||||
+1131
File diff suppressed because it is too large.
Load diff
+1002
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user