Add project files.

This commit is contained in:
alejo committed 2026-10-25 03:29:04 -03:00
commit a9ae435321
52 files changed
+65378

No files matched your search

+16
View File
@@ -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)
+790
View File
@@ -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
View File
@@ -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
+39
View File
@@ -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;
}
+252
View File
@@ -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
+79
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff
+1002
View File
File diff suppressed because it is too large. Load diff