mirror of
https://github.com/Nexoniarz/Fry3DX.git
synced 2026-11-03 04:24:26 +00:00
Fry3DX 0.4: an easy 3D modeller
Shapes with live settings, shape editing, knife and hole tools, drawing shapes onto other objects, combining objects (add / subtract / overlap), copies, thickness, painting with stickers and layers, a Layout tab, a Print tab with checks, packed .fry files with autosave, export to GLB, glTF, FBX, OBJ, 3MF, STL and PLY, folders in Layers, its own crash screen, and a website for GitHub Pages in docs/. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NMSrd5oe1GpUD7E6d825KP
This commit is contained in:
commit
f3e8eca87b
135 changed files with 23423 additions and 0 deletions
177
engine/asset/kmap.h
Normal file
177
engine/asset/kmap.h
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
#pragma once
|
||||
// KMAP: a compiled map, written by assetc from a TrenchBroom .map file.
|
||||
// KmapHeader
|
||||
// render model: a complete KMDL blob (renderModelSize bytes)
|
||||
// KmapBrush × brushCount, then glm-compatible float[3] × pointCount
|
||||
// entity text (entitySize bytes), Quake style: { "key" "value" ... } ...
|
||||
// light probes: float × 19 per probe (ambient cube: 6 × rgb for +X -X +Y -Y +Z -Z, then sun visibility)
|
||||
// material names, '\n'-separated (materialTableSize bytes): what KmapBrush::material indexes
|
||||
#include "asset/kmdl.h"
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
namespace keel {
|
||||
|
||||
inline constexpr char kKmapMagic[4] = {'K', 'M', 'A', 'P'};
|
||||
inline constexpr uint32_t kKmapVersion = 5;
|
||||
|
||||
enum BrushContents : uint32_t {
|
||||
CONTENTS_SOLID = 1 << 0,
|
||||
CONTENTS_PLAYERCLIP = 1 << 1, // blocks players only (tools/clip)
|
||||
CONTENTS_TRIGGER = 1 << 2, // not solid; reports players inside (trigger_* entities)
|
||||
};
|
||||
|
||||
inline constexpr uint32_t kWorldEntity = 0xFFFFFFFF;
|
||||
|
||||
struct KmapHeader {
|
||||
char magic[4];
|
||||
uint32_t version;
|
||||
uint32_t renderModelSize;
|
||||
uint32_t brushCount, pointCount;
|
||||
uint32_t entitySize;
|
||||
float probeOrigin[3];
|
||||
float probeSpacing;
|
||||
uint32_t probeDims[3];
|
||||
uint32_t materialTableSize;
|
||||
};
|
||||
|
||||
inline constexpr int kProbeFloats = 19;
|
||||
|
||||
// Baked lighting for moving things: a grid of ambient cubes (like Source).
|
||||
struct KmapProbes {
|
||||
glm::vec3 origin{0.0f};
|
||||
float spacing = 0.0f;
|
||||
glm::uvec3 dims{0u};
|
||||
std::vector<float> data; // kProbeFloats per probe, x fastest
|
||||
};
|
||||
|
||||
struct KmapBrush {
|
||||
uint32_t firstPoint, pointCount;
|
||||
uint32_t contents;
|
||||
uint32_t entity; // index into entities, or kWorldEntity
|
||||
// Texture of its top, sides and bottom (into KmapData::materials): what you walk on,
|
||||
// run into, bump your head on. Sounds and effects look up the surface from it.
|
||||
uint16_t material[3];
|
||||
uint16_t pad;
|
||||
};
|
||||
enum BrushFace { FACE_TOP = 0, FACE_SIDE = 1, FACE_BOTTOM = 2 };
|
||||
|
||||
using EntityKeys = std::map<std::string, std::string>;
|
||||
|
||||
struct KmapData {
|
||||
KmdlModel render;
|
||||
KmapProbes probes;
|
||||
std::vector<KmapBrush> brushes;
|
||||
std::vector<std::string> materials;
|
||||
std::vector<float> points; // xyz triples
|
||||
std::vector<EntityKeys> entities;
|
||||
};
|
||||
|
||||
// Parses `{ "k" "v" }` blocks. Shared by the compiler (for .map entity keys) and loader.
|
||||
inline std::vector<EntityKeys> parseEntityText(const std::string& text) {
|
||||
std::vector<EntityKeys> out;
|
||||
size_t i = 0;
|
||||
auto quoted = [&](std::string& s) {
|
||||
while (i < text.size() && text[i] != '"' && text[i] != '}') ++i;
|
||||
if (i >= text.size() || text[i] == '}') return false;
|
||||
const size_t end = text.find('"', i + 1);
|
||||
if (end == std::string::npos) return false;
|
||||
s = text.substr(i + 1, end - i - 1);
|
||||
i = end + 1;
|
||||
return true;
|
||||
};
|
||||
while ((i = text.find('{', i)) != std::string::npos) {
|
||||
++i;
|
||||
EntityKeys e;
|
||||
std::string k, v;
|
||||
while (quoted(k) && quoted(v)) e[k] = v;
|
||||
out.push_back(std::move(e));
|
||||
i = text.find('}', i);
|
||||
if (i == std::string::npos) break;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
inline std::string entityText(const std::vector<EntityKeys>& entities) {
|
||||
std::string out;
|
||||
for (const auto& e : entities) {
|
||||
out += "{\n";
|
||||
for (const auto& [k, v] : e) out += "\"" + k + "\" \"" + v + "\"\n";
|
||||
out += "}\n";
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
inline std::vector<uint8_t> writeKmap(const KmapData& m) {
|
||||
const std::vector<uint8_t> render = writeKmdl(m.render);
|
||||
const std::string ents = entityText(m.entities);
|
||||
KmapHeader h{};
|
||||
std::memcpy(h.magic, kKmapMagic, 4);
|
||||
h.version = kKmapVersion;
|
||||
h.renderModelSize = uint32_t(render.size());
|
||||
h.brushCount = uint32_t(m.brushes.size());
|
||||
h.pointCount = uint32_t(m.points.size() / 3);
|
||||
h.entitySize = uint32_t(ents.size());
|
||||
std::memcpy(h.probeOrigin, &m.probes.origin, 12);
|
||||
h.probeSpacing = m.probes.spacing;
|
||||
std::memcpy(h.probeDims, &m.probes.dims, 12);
|
||||
std::string table;
|
||||
for (const std::string& name : m.materials) table += name + "\n";
|
||||
h.materialTableSize = uint32_t(table.size());
|
||||
std::vector<uint8_t> out;
|
||||
auto put = [&](const void* p, size_t n) { out.insert(out.end(), (const uint8_t*)p, (const uint8_t*)p + n); };
|
||||
put(&h, sizeof h);
|
||||
put(render.data(), render.size());
|
||||
put(m.brushes.data(), m.brushes.size() * sizeof(KmapBrush));
|
||||
put(m.points.data(), m.points.size() * sizeof(float));
|
||||
put(ents.data(), ents.size());
|
||||
put(m.probes.data.data(), m.probes.data.size() * sizeof(float));
|
||||
put(table.data(), table.size());
|
||||
return out;
|
||||
}
|
||||
|
||||
inline std::optional<KmapData> readKmap(const std::vector<uint8_t>& data) {
|
||||
KmapHeader h;
|
||||
if (data.size() < sizeof h) return std::nullopt;
|
||||
std::memcpy(&h, data.data(), sizeof h);
|
||||
if (std::memcmp(h.magic, kKmapMagic, 4) != 0 || h.version != kKmapVersion) return std::nullopt;
|
||||
const uint64_t need = sizeof h + uint64_t(h.renderModelSize) + uint64_t(h.brushCount) * sizeof(KmapBrush) +
|
||||
uint64_t(h.pointCount) * 12 + h.entitySize;
|
||||
if (data.size() < need) return std::nullopt;
|
||||
KmapData m;
|
||||
size_t pos = sizeof h;
|
||||
auto render = readKmdl(std::vector<uint8_t>(data.begin() + pos, data.begin() + pos + h.renderModelSize));
|
||||
if (h.renderModelSize && !render) return std::nullopt;
|
||||
if (render) m.render = std::move(*render);
|
||||
pos += h.renderModelSize;
|
||||
m.brushes.resize(h.brushCount);
|
||||
std::memcpy(m.brushes.data(), data.data() + pos, h.brushCount * sizeof(KmapBrush));
|
||||
pos += h.brushCount * sizeof(KmapBrush);
|
||||
m.points.resize(size_t(h.pointCount) * 3);
|
||||
std::memcpy(m.points.data(), data.data() + pos, m.points.size() * sizeof(float));
|
||||
pos += m.points.size() * sizeof(float);
|
||||
for (const KmapBrush& b : m.brushes)
|
||||
if (uint64_t(b.firstPoint) + b.pointCount > h.pointCount) return std::nullopt;
|
||||
m.entities = parseEntityText(std::string((const char*)data.data() + pos, h.entitySize));
|
||||
pos += h.entitySize;
|
||||
const uint64_t probes = uint64_t(h.probeDims[0]) * h.probeDims[1] * h.probeDims[2];
|
||||
if (probes > 4'000'000 || data.size() < pos + probes * kProbeFloats * 4) return std::nullopt;
|
||||
std::memcpy(&m.probes.origin, h.probeOrigin, 12);
|
||||
m.probes.spacing = h.probeSpacing;
|
||||
std::memcpy(&m.probes.dims, h.probeDims, 12);
|
||||
m.probes.data.resize(size_t(probes) * kProbeFloats);
|
||||
std::memcpy(m.probes.data.data(), data.data() + pos, m.probes.data.size() * 4);
|
||||
pos += m.probes.data.size() * 4;
|
||||
if (data.size() < pos + h.materialTableSize) return std::nullopt;
|
||||
const std::string table((const char*)data.data() + pos, h.materialTableSize);
|
||||
for (size_t a = 0, b; (b = table.find('\n', a)) != std::string::npos; a = b + 1) m.materials.push_back(table.substr(a, b - a));
|
||||
for (const KmapBrush& b : m.brushes)
|
||||
for (uint16_t mat : b.material)
|
||||
if (mat >= m.materials.size() && !m.materials.empty()) return std::nullopt;
|
||||
return m;
|
||||
}
|
||||
|
||||
} // namespace keel
|
||||
200
engine/asset/kmdl.h
Normal file
200
engine/asset/kmdl.h
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
#pragma once
|
||||
// KMDL: the engine's runtime model format, written by assetc from glTF. Laid out so
|
||||
// the loader is a few memcpys:
|
||||
// KmdlHeader
|
||||
// KmdlMaterial × materialCount
|
||||
// KmdlSubmesh × submeshCount
|
||||
// KmdlVertex × vertexCount
|
||||
// uint32_t × indexCount
|
||||
// if jointCount > 0:
|
||||
// KmdlJoint × jointCount (parents before children)
|
||||
// KmdlSkin × vertexCount
|
||||
// animationCount × { KmdlAnimation, then per channel: KmdlChannel,
|
||||
// float times[keyCount], float values[keyCount × 3 or 4] }
|
||||
// hullCount × { uint32_t pointCount, float xyz[pointCount × 3] }
|
||||
// Hulls are the collision model (like Source's VPhysics .phy): convex pieces, model space.
|
||||
// None = collide as the convex hull of all vertices.
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
namespace keel {
|
||||
|
||||
inline constexpr char kKmdlMagic[4] = {'K', 'M', 'D', 'L'};
|
||||
inline constexpr uint32_t kKmdlVersion = 5;
|
||||
// KmdlHeader::flags
|
||||
inline constexpr uint32_t KMDL_PLAYER_SOLID = 1u; // players collide with it whatever its size (doors)
|
||||
|
||||
struct KmdlHeader {
|
||||
char magic[4];
|
||||
uint32_t version;
|
||||
uint32_t materialCount, submeshCount, vertexCount, indexCount;
|
||||
float boundsMin[3], boundsMax[3];
|
||||
uint32_t jointCount, animationCount;
|
||||
uint32_t hullCount;
|
||||
uint32_t flags;
|
||||
};
|
||||
|
||||
struct KmdlJoint {
|
||||
char name[32];
|
||||
int32_t parent; // -1 = root
|
||||
float inverseBind[16]; // column-major
|
||||
float translation[3], rotation[4], scale[3]; // rest pose, local; rotation xyzw
|
||||
};
|
||||
|
||||
struct KmdlSkin {
|
||||
uint8_t joints[4];
|
||||
uint8_t weights[4]; // sum to 255
|
||||
};
|
||||
|
||||
enum KmdlChannelPath : uint32_t { KMDL_TRANSLATION = 0, KMDL_ROTATION = 1, KMDL_SCALE = 2 };
|
||||
|
||||
struct KmdlAnimationHeader {
|
||||
char name[32];
|
||||
float duration;
|
||||
uint32_t channelCount;
|
||||
};
|
||||
|
||||
struct KmdlChannelHeader {
|
||||
uint32_t joint, path, keyCount;
|
||||
};
|
||||
|
||||
struct KmdlChannel {
|
||||
KmdlChannelHeader header;
|
||||
std::vector<float> times, values; // values: 3 per key (T, S) or 4 (R, xyzw)
|
||||
};
|
||||
|
||||
struct KmdlAnimation {
|
||||
KmdlAnimationHeader header;
|
||||
std::vector<KmdlChannel> channels;
|
||||
};
|
||||
|
||||
struct KmdlMaterial {
|
||||
char texture[120]; // game path to a .yapf, empty = white
|
||||
char lightmap[120]; // baked lighting (RGBM .yapf, uses KmdlVertex::lightmapUv); empty = lit at runtime
|
||||
float baseColor[4];
|
||||
};
|
||||
|
||||
struct KmdlSubmesh {
|
||||
uint32_t firstIndex, indexCount, material;
|
||||
};
|
||||
|
||||
struct KmdlVertex {
|
||||
float position[3];
|
||||
float normal[3];
|
||||
float uv[2];
|
||||
float lightmapUv[2];
|
||||
};
|
||||
|
||||
struct KmdlModel {
|
||||
KmdlHeader header{};
|
||||
std::vector<KmdlMaterial> materials;
|
||||
std::vector<KmdlSubmesh> submeshes;
|
||||
std::vector<KmdlVertex> vertices;
|
||||
std::vector<uint32_t> indices;
|
||||
std::vector<KmdlJoint> joints;
|
||||
std::vector<KmdlSkin> skin; // one per vertex when skinned
|
||||
std::vector<KmdlAnimation> animations;
|
||||
std::vector<std::vector<float>> hulls; // xyz triples
|
||||
};
|
||||
|
||||
inline std::vector<uint8_t> writeKmdl(KmdlModel m) {
|
||||
std::memcpy(m.header.magic, kKmdlMagic, 4);
|
||||
m.header.version = kKmdlVersion;
|
||||
m.header.materialCount = uint32_t(m.materials.size());
|
||||
m.header.submeshCount = uint32_t(m.submeshes.size());
|
||||
m.header.vertexCount = uint32_t(m.vertices.size());
|
||||
m.header.indexCount = uint32_t(m.indices.size());
|
||||
m.header.jointCount = uint32_t(m.joints.size());
|
||||
m.header.animationCount = uint32_t(m.animations.size());
|
||||
m.header.hullCount = uint32_t(m.hulls.size());
|
||||
std::vector<uint8_t> out;
|
||||
auto put = [&](const void* p, size_t n) { out.insert(out.end(), (const uint8_t*)p, (const uint8_t*)p + n); };
|
||||
put(&m.header, sizeof m.header);
|
||||
put(m.materials.data(), m.materials.size() * sizeof(KmdlMaterial));
|
||||
put(m.submeshes.data(), m.submeshes.size() * sizeof(KmdlSubmesh));
|
||||
put(m.vertices.data(), m.vertices.size() * sizeof(KmdlVertex));
|
||||
put(m.indices.data(), m.indices.size() * sizeof(uint32_t));
|
||||
if (!m.joints.empty()) {
|
||||
put(m.joints.data(), m.joints.size() * sizeof(KmdlJoint));
|
||||
put(m.skin.data(), m.skin.size() * sizeof(KmdlSkin));
|
||||
for (auto& a : m.animations) {
|
||||
a.header.channelCount = uint32_t(a.channels.size());
|
||||
put(&a.header, sizeof a.header);
|
||||
for (auto& c : a.channels) {
|
||||
c.header.keyCount = uint32_t(c.times.size());
|
||||
put(&c.header, sizeof c.header);
|
||||
put(c.times.data(), c.times.size() * sizeof(float));
|
||||
put(c.values.data(), c.values.size() * sizeof(float));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const auto& h : m.hulls) {
|
||||
const uint32_t n = uint32_t(h.size() / 3);
|
||||
put(&n, 4);
|
||||
put(h.data(), size_t(n) * 3 * sizeof(float));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
inline std::optional<KmdlModel> readKmdl(const std::vector<uint8_t>& data) {
|
||||
KmdlModel m;
|
||||
size_t pos = 0;
|
||||
auto take = [&](void* p, size_t n) {
|
||||
if (pos + n > data.size()) return false;
|
||||
std::memcpy(p, data.data() + pos, n);
|
||||
pos += n;
|
||||
return true;
|
||||
};
|
||||
if (!take(&m.header, sizeof m.header) || std::memcmp(m.header.magic, kKmdlMagic, 4) != 0 || m.header.version != kKmdlVersion)
|
||||
return std::nullopt;
|
||||
m.materials.resize(m.header.materialCount);
|
||||
m.submeshes.resize(m.header.submeshCount);
|
||||
m.vertices.resize(m.header.vertexCount);
|
||||
m.indices.resize(m.header.indexCount);
|
||||
if (!take(m.materials.data(), m.materials.size() * sizeof(KmdlMaterial)) ||
|
||||
!take(m.submeshes.data(), m.submeshes.size() * sizeof(KmdlSubmesh)) ||
|
||||
!take(m.vertices.data(), m.vertices.size() * sizeof(KmdlVertex)) ||
|
||||
!take(m.indices.data(), m.indices.size() * sizeof(uint32_t)))
|
||||
return std::nullopt;
|
||||
for (uint32_t i : m.indices)
|
||||
if (i >= m.vertices.size()) return std::nullopt;
|
||||
if (m.header.jointCount) {
|
||||
if (m.header.jointCount > 255) return std::nullopt;
|
||||
m.joints.resize(m.header.jointCount);
|
||||
m.skin.resize(m.header.vertexCount);
|
||||
if (!take(m.joints.data(), m.joints.size() * sizeof(KmdlJoint)) || !take(m.skin.data(), m.skin.size() * sizeof(KmdlSkin)))
|
||||
return std::nullopt;
|
||||
for (size_t j = 0; j < m.joints.size(); ++j)
|
||||
if (m.joints[j].parent >= int32_t(j)) return std::nullopt; // parents first
|
||||
for (const KmdlSkin& sk : m.skin)
|
||||
for (uint8_t j : sk.joints)
|
||||
if (j >= m.joints.size()) return std::nullopt;
|
||||
m.animations.resize(m.header.animationCount);
|
||||
for (auto& a : m.animations) {
|
||||
if (!take(&a.header, sizeof a.header) || a.header.channelCount > 4096) return std::nullopt;
|
||||
a.channels.resize(a.header.channelCount);
|
||||
for (auto& c : a.channels) {
|
||||
if (!take(&c.header, sizeof c.header) || c.header.joint >= m.joints.size() || c.header.path > 2 || c.header.keyCount > 1'000'000)
|
||||
return std::nullopt;
|
||||
c.times.resize(c.header.keyCount);
|
||||
c.values.resize(size_t(c.header.keyCount) * (c.header.path == KMDL_ROTATION ? 4 : 3));
|
||||
if (!take(c.times.data(), c.times.size() * 4) || !take(c.values.data(), c.values.size() * 4)) return std::nullopt;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (m.header.hullCount > 4096) return std::nullopt;
|
||||
m.hulls.resize(m.header.hullCount);
|
||||
for (auto& h : m.hulls) {
|
||||
uint32_t n = 0;
|
||||
if (!take(&n, 4) || n > 65536) return std::nullopt;
|
||||
h.resize(size_t(n) * 3);
|
||||
if (!take(h.data(), h.size() * sizeof(float))) return std::nullopt;
|
||||
}
|
||||
for (auto& s : m.submeshes)
|
||||
if (uint64_t(s.firstIndex) + s.indexCount > m.indices.size() || s.material >= m.materials.size()) return std::nullopt;
|
||||
return m;
|
||||
}
|
||||
|
||||
} // namespace keel
|
||||
25
engine/asset/ksky.h
Normal file
25
engine/asset/ksky.h
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
#pragma once
|
||||
// .ksky: a sky for the renderer, compiled by assetc from an equirectangular .hdr.
|
||||
// KskyHeader
|
||||
// `levels` images of width >> k × height >> k RGBM8 pixels (rows top-down), largest first
|
||||
// Directions are world space (+Z up): u = atan2(d.y, d.x) / 2π + 0.5, v = acos(d.z) / π.
|
||||
// A pixel's linear radiance is rgb · a · kSkyRange. The sky is normalised so its upper half
|
||||
// averages 1 (the engine scales it by the map's sky intensity), and the sun is taken out of
|
||||
// it (the engine draws its own, matching the shadows). Level k is the sky as a surface of
|
||||
// roughness k / (levels - 1) reflects it (GGX prefiltered), for image-based reflections.
|
||||
#include <cstdint>
|
||||
|
||||
namespace keel {
|
||||
|
||||
inline constexpr uint32_t kKskyVersion = 1;
|
||||
inline constexpr float kSkyRange = 16.0f;
|
||||
|
||||
struct KskyHeader {
|
||||
char magic[4]; // "KSKY"
|
||||
uint32_t version;
|
||||
uint32_t width, height, levels;
|
||||
float sunDir[3]; // towards where the sun was in the photo
|
||||
float sh[27]; // 9 RGB spherical harmonics of the light it casts: Σ shᵢ·Yᵢ(n) = irradiance / π
|
||||
};
|
||||
|
||||
} // namespace keel
|
||||
Loading…
Reference in a new issue