From 3cb388eef05033ffc30410b5b9746a26d17e3e3b Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 8 Jan 2026 18:26:19 +0300 Subject: [PATCH 01/14] add ModelBuilder --- src/coders/vcm.cpp | 139 +++++++++++++++++++++++++++------ src/graphics/commons/Model.cpp | 37 +++++++++ src/graphics/commons/Model.hpp | 8 ++ 3 files changed, 159 insertions(+), 25 deletions(-) diff --git a/src/coders/vcm.cpp b/src/coders/vcm.cpp index 5bf54ee99..73da37401 100644 --- a/src/coders/vcm.cpp +++ b/src/coders/vcm.cpp @@ -1,17 +1,19 @@ #include "vcm.hpp" -#include -#include -#include -#include - #include "xml.hpp" #include "util/stringutil.hpp" #include "graphics/commons/Model.hpp" #include "io/io.hpp" +#include +#include +#include +#include +#include + using namespace vcm; using namespace xml; +using namespace model; static const std::unordered_map side_indices { {"north", 0}, @@ -26,7 +28,87 @@ static bool to_boolean(const xml::Attribute& attr) { return attr.getText() != "off"; } -static void perform_rect(const xmlelement& root, model::Model& model) { +class ModelBuilder { +public: + ModelBuilder(Model& model) : model(model) {} + + void push(const glm::mat4& matrix) { + matrices.push_back(matrix); + calculateMatrix(); + } + + void pop() { + matrices.pop_back(); + calculateMatrix(); + } + + void addBox( + const std::string& texture, + bool shading, + const glm::vec3& pos, + const glm::vec3& size, + const UVRegion (&uvs)[6], + const bool enabledSides[6] + ) { + auto& mesh = model.addMesh(texture, shading); + mesh.addBox(pos, size, uvs, enabledSides, combined); + } + + void addTriangle( + const std::string& texture, + bool shading, + const glm::vec3& a, + const glm::vec3& b, + const glm::vec3& c, + const glm::vec3& norm, + const glm::vec2& uvA, + const glm::vec2& uvB, + const glm::vec2& uvC + ) { + auto& mesh = model.addMesh(texture, shading); + mesh.addTriangle( + combined * glm::vec4(a, 1.0f), + combined * glm::vec4(b, 1.0f), + combined * glm::vec4(c, 1.0f), + norm, + uvA, + uvB, + uvC + ); + } + + void addRect( + const std::string& texture, + bool shading, + const glm::vec3& pos, + const glm::vec3& right, + const glm::vec3& up, + const glm::vec3& norm, + const UVRegion& uv + ) { + auto& mesh = model.addMesh(texture, shading); + mesh.addRect(pos, right, up, norm, uv, combined); + } + + Model& getModel() { + return model; + } +private: + void calculateMatrix() { + combined = glm::mat4(1.0f); + for (const auto& matrix : matrices) { + combined *= matrix; + } + } + + Model& model; + std::vector matrices; + glm::mat4 combined {1.0f}; +}; + +static void perform_element(const xmlelement& root, ModelBuilder& builder); + +static void perform_rect(const xmlelement& root, ModelBuilder& builder) { auto from = root.attr("from").asVec3(); auto right = root.attr("right").asVec3(); auto up = root.attr("up").asVec3(); @@ -60,10 +142,11 @@ static void perform_rect(const xmlelement& root, model::Model& model) { from -= up; } std::string texture = root.attr("texture", "$0").getText(); - auto& mesh = model.addMesh(texture, shading); auto normal = glm::cross(glm::normalize(right), glm::normalize(up)); - mesh.addRect( + builder.addRect( + texture, + shading, from + right * 0.5f + up * 0.5f, right * 0.5f, up * 0.5f, @@ -72,7 +155,7 @@ static void perform_rect(const xmlelement& root, model::Model& model) { ); } -static void perform_triangle(const xmlelement& root, model::Model& model) { +static void perform_triangle(const xmlelement& root, ModelBuilder& builder) { auto pointA = root.attr("a").asVec3(); auto pointB = root.attr("b").asVec3(); auto pointC = root.attr("c").asVec3(); @@ -104,11 +187,10 @@ static void perform_triangle(const xmlelement& root, model::Model& model) { } std::string texture = root.attr("texture", "$0").getText(); - auto& mesh = model.addMesh(texture, shading); - mesh.addTriangle(pointA, pointB, pointC, normal, uvs[0], uvs[1], uvs[2]); + builder.addTriangle(texture, shading, pointA, pointB, pointC, normal, uvs[0], uvs[1], uvs[2]); } -static void perform_box(const xmlelement& root, model::Model& model) { +static void perform_box(const xmlelement& root, ModelBuilder& builder) { auto from = root.attr("from").asVec3(); auto to = root.attr("to").asVec3(); @@ -186,7 +268,7 @@ static void perform_box(const xmlelement& root, model::Model& model) { bool deleted[6] {}; if (root.has("delete")) { - // todo: replace by expression parsing + // todo: replace with expression parsing auto names = util::split(root.attr("delete").getText(), ','); for (auto& name : names) { util::trim(name); @@ -197,30 +279,37 @@ static void perform_box(const xmlelement& root, model::Model& model) { } } + builder.push(tsf); for (int i = 0; i < 6; i++) { if (deleted[i]) { continue; } bool enabled[6] {}; enabled[i] = true; - auto& mesh = model.addMesh(texfaces[i], shading); - mesh.addBox(center, halfsize, regions, enabled, tsf); + + builder.addBox(texfaces[i], shading, center, halfsize, regions, enabled); + } + builder.pop(); +} + +static void perform_element(const xmlelement& root, ModelBuilder& builder) { + auto tag = root.getTag(); + + if (tag == "rect") { + perform_rect(root, builder); + } else if (tag == "box") { + perform_box(root, builder); + } else if (tag == "tri") { + perform_triangle(root, builder); } } static std::unique_ptr load_model(const xmlelement& root) { - model::Model model; + Model model; + ModelBuilder builder(model); for (const auto& elem : root.getElements()) { - auto tag = elem->getTag(); - - if (tag == "rect") { - perform_rect(*elem, model); - } else if (tag == "box") { - perform_box(*elem, model); - } else if (tag == "tri") { - perform_triangle(*elem, model); - } + perform_element(*elem, builder); } return std::make_unique(std::move(model)); diff --git a/src/graphics/commons/Model.cpp b/src/graphics/commons/Model.cpp index e7c247868..8889400d4 100644 --- a/src/graphics/commons/Model.cpp +++ b/src/graphics/commons/Model.cpp @@ -1,6 +1,10 @@ +#define GLM_ENABLE_EXPERIMENTAL + #include "Model.hpp" #include +#include +#include using namespace model; @@ -71,6 +75,39 @@ void Mesh::addRect( vertices.push_back({pos-right+up, {uv.u1, uv.v2}, norm}); } +static glm::mat4 extract_rotation(const glm::mat4& matrix) { + glm::vec3 scale; + glm::quat rotation; + glm::vec3 translation; + glm::vec3 skew; + glm::vec4 perspective; + glm::decompose(matrix, scale, rotation, translation, skew, perspective); + return glm::mat4_cast(rotation); +} + +void Mesh::addRect( + const glm::vec3& pos, + const glm::vec3& right, + const glm::vec3& up, + const glm::vec3& norm, + const UVRegion& uv, + const glm::mat4& transform +) { + auto a = transform * glm::vec4(pos - right - up, 1.0f); + auto b = transform * glm::vec4(pos + right - up, 1.0f); + auto c = transform * glm::vec4(pos + right + up, 1.0f); + auto d = transform * glm::vec4(pos - right + up, 1.0f); + auto tsfnorm = extract_rotation(transform) * glm::vec4(norm, 1.0f); + + vertices.push_back({a, {uv.u1, uv.v1}, tsfnorm}); + vertices.push_back({b, {uv.u2, uv.v1}, tsfnorm}); + vertices.push_back({c, {uv.u2, uv.v2}, tsfnorm}); + + vertices.push_back({a, {uv.u1, uv.v1}, tsfnorm}); + vertices.push_back({c, {uv.u2, uv.v2}, tsfnorm}); + vertices.push_back({d, {uv.u1, uv.v2}, tsfnorm}); +} + void Mesh::addBox(const glm::vec3& pos, const glm::vec3& size) { UVRegion fullRegion (0, 0, 1, 1); addPlane(pos+Z*size, X*size, Y*size, Z, fullRegion); diff --git a/src/graphics/commons/Model.hpp b/src/graphics/commons/Model.hpp index 67618ed38..ac330c378 100644 --- a/src/graphics/commons/Model.hpp +++ b/src/graphics/commons/Model.hpp @@ -40,6 +40,14 @@ namespace model { const glm::vec3& norm, const UVRegion& region ); + void addRect( + const glm::vec3& pos, + const glm::vec3& right, + const glm::vec3& up, + const glm::vec3& norm, + const UVRegion& uv, + const glm::mat4& transform + ); void addTriangle( const glm::vec3& a, const glm::vec3& b, From 27683793a6d9276bda2f36dc386d696ded889c33 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 8 Jan 2026 22:07:36 +0300 Subject: [PATCH 02/14] add anonymous bones --- src/coders/vcm.cpp | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/coders/vcm.cpp b/src/coders/vcm.cpp index 73da37401..507512715 100644 --- a/src/coders/vcm.cpp +++ b/src/coders/vcm.cpp @@ -292,6 +292,34 @@ static void perform_box(const xmlelement& root, ModelBuilder& builder) { builder.pop(); } +static void perform_bone(const xmlelement& root, ModelBuilder& builder) { + glm::mat4 tsf(1.0f); + if (root.has("move")) { + tsf = glm::translate(tsf, root.attr("move").asVec3()); + } + if (root.has("rotate")) { + auto text = root.attr("rotate").getText(); + if (std::count(text.begin(), text.end(), ',') == 3) { + auto quat = root.attr("rotate").asVec4(); + tsf *= glm::mat4_cast(glm::quat(quat.w, quat.x, quat.y, quat.z)); + } else { + auto rot = root.attr("rotate").asVec3(); + tsf = glm::rotate(tsf, glm::radians(rot.x), glm::vec3(1, 0, 0)); + tsf = glm::rotate(tsf, glm::radians(rot.y), glm::vec3(0, 1, 0)); + tsf = glm::rotate(tsf, glm::radians(rot.z), glm::vec3(0, 0, 1)); + } + } + if (root.has("scale")) { + tsf = glm::scale(tsf, root.attr("scale").asVec3()); + } + + builder.push(std::move(tsf)); + for (const auto& elem : root.getElements()) { + perform_element(*elem, builder); + } + builder.pop(); +} + static void perform_element(const xmlelement& root, ModelBuilder& builder) { auto tag = root.getTag(); @@ -301,6 +329,8 @@ static void perform_element(const xmlelement& root, ModelBuilder& builder) { perform_box(root, builder); } else if (tag == "tri") { perform_triangle(root, builder); + } else if (tag == "bone") { + perform_bone(root, builder); } } From 22e0c469c006217b82b1dcac9193cd38b8072cd2 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 8 Jan 2026 22:17:34 +0300 Subject: [PATCH 03/14] fix nested vcm elements --- src/coders/xml.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/coders/xml.cpp b/src/coders/xml.cpp index 4efabc959..93e1c8031 100644 --- a/src/coders/xml.cpp +++ b/src/coders/xml.cpp @@ -396,7 +396,11 @@ public: void parseSubElements(Node& node) { skipWhitespace(); while (hasNext()) { - if (peek() != '@') { + char c = peek(); + if (c == '}') { + break; + } + if (c != '@') { throw error("unexpected character in element"); } nextChar(); @@ -415,14 +419,12 @@ public: if (!hasNext()) { break; } - char c = peek(); + c = peek(); if (c == '{') { nextChar(); parseSubElements(*subnode); expect('}'); skipWhitespace(); - } else if (c == '}') { - break; } } } From 495e5fa994bbdd6c2bcbdf2a438774fb46c7c446 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 13 Jan 2026 21:50:20 +0300 Subject: [PATCH 04/14] add Merge::transform, Model::merge --- src/graphics/commons/Model.cpp | 23 +++++++++++++++++++++++ src/graphics/commons/Model.hpp | 10 ++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/graphics/commons/Model.cpp b/src/graphics/commons/Model.cpp index 8889400d4..0615ce2d8 100644 --- a/src/graphics/commons/Model.cpp +++ b/src/graphics/commons/Model.cpp @@ -156,6 +156,12 @@ void Mesh::scale(const glm::vec3& size) { } } +void Mesh::transform(const glm::mat4& matrix) { + for (auto& vertex : vertices) { + vertex.coord = matrix * glm::vec4(vertex.coord, 1.0f); + } +} + void Model::clean() { meshes.erase( std::remove_if(meshes.begin(), meshes.end(), @@ -165,3 +171,20 @@ void Model::clean() { meshes.end() ); } + +void Model::transform(const glm::mat4& matrix) { + for (auto& mesh : meshes) { + mesh.transform(matrix); + } +} + +void Model::merge(const Model& source) { + for (const auto& srcMesh : source.meshes) { + auto& dstMesh = addMesh(srcMesh.texture, srcMesh.shading); + dstMesh.vertices.insert( + dstMesh.vertices.end(), + srcMesh.vertices.begin(), + srcMesh.vertices.end() + ); + } +} diff --git a/src/graphics/commons/Model.hpp b/src/graphics/commons/Model.hpp index ac330c378..36962be88 100644 --- a/src/graphics/commons/Model.hpp +++ b/src/graphics/commons/Model.hpp @@ -72,6 +72,9 @@ namespace model { const glm::mat4& transform ); void scale(const glm::vec3& size); + + /// @brief Apply transformation matrix to vertices coordinates + void transform(const glm::mat4& matrix); }; struct Model { @@ -91,5 +94,12 @@ namespace model { } /// @brief Remove all empty meshes void clean(); + + /// @brief Apply transformation matrix to vertices coordinates + void transform(const glm::mat4& matrix); + + /// @brief Copy or merge meshes from source model to this. + /// Does not remove duplicates + void merge(const Model& source); }; } From c34cb57fb877e02ad66f982610a58ba5c86a7e8d Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 15 Jan 2026 21:51:39 +0300 Subject: [PATCH 05/14] feat: named bones and skeleton building --- src/coders/vcm.cpp | 101 ++++++++++++++++++++++++++++----- src/coders/vcm.hpp | 16 +++++- src/graphics/commons/Model.cpp | 27 +++++++++ src/graphics/commons/Model.hpp | 11 ++++ src/objects/rigging.cpp | 5 ++ src/objects/rigging.hpp | 2 + 6 files changed, 147 insertions(+), 15 deletions(-) diff --git a/src/coders/vcm.cpp b/src/coders/vcm.cpp index 507512715..4acf8b631 100644 --- a/src/coders/vcm.cpp +++ b/src/coders/vcm.cpp @@ -2,7 +2,7 @@ #include "xml.hpp" #include "util/stringutil.hpp" -#include "graphics/commons/Model.hpp" +#include "objects/rigging.hpp" #include "io/io.hpp" #include @@ -14,6 +14,7 @@ using namespace vcm; using namespace xml; using namespace model; +using namespace rigging; static const std::unordered_map side_indices { {"north", 0}, @@ -42,6 +43,10 @@ public: calculateMatrix(); } + const glm::mat4& getTransform() const { + return combined; + } + void addBox( const std::string& texture, bool shading, @@ -106,7 +111,13 @@ private: glm::mat4 combined {1.0f}; }; -static void perform_element(const xmlelement& root, ModelBuilder& builder); +struct Context { + VcmModel& vcmModel; + Bone& bone; + size_t& boneIndex; +}; + +static void perform_element(const xmlelement& root, ModelBuilder& builder, Context& ctx); static void perform_rect(const xmlelement& root, ModelBuilder& builder) { auto from = root.attr("from").asVec3(); @@ -292,7 +303,9 @@ static void perform_box(const xmlelement& root, ModelBuilder& builder) { builder.pop(); } -static void perform_bone(const xmlelement& root, ModelBuilder& builder) { +static void perform_bone(const xmlelement& root, ModelBuilder& builder, Context& ctx) { + std::string name = root.attr("name", "").getText(); + glm::mat4 tsf(1.0f); if (root.has("move")) { tsf = glm::translate(tsf, root.attr("move").asVec3()); @@ -313,14 +326,29 @@ static void perform_bone(const xmlelement& root, ModelBuilder& builder) { tsf = glm::scale(tsf, root.attr("scale").asVec3()); } - builder.push(std::move(tsf)); - for (const auto& elem : root.getElements()) { - perform_element(*elem, builder); + if (name.empty()) { + builder.push(std::move(tsf)); + for (const auto& elem : root.getElements()) { + perform_element(*elem, builder, ctx); + } + builder.pop(); + } else { + glm::vec3 origin = builder.getTransform() * glm::vec4(0.0f, 0.0f, 0.0f, 1.0f); + auto& bone = ctx.bone.addBone( + Bone(ctx.boneIndex++, name, name, {}, std::move(origin)) + ); + Context boneContext {ctx.vcmModel, bone, ctx.boneIndex}; + Model boneModel; + ModelBuilder boneModelBuilder(boneModel); + boneModelBuilder.push(std::move(tsf)); + for (const auto& elem : root.getElements()) { + perform_element(*elem, boneModelBuilder, boneContext); + } + ctx.vcmModel.parts[std::move(name)] = std::move(boneModel); } - builder.pop(); } -static void perform_element(const xmlelement& root, ModelBuilder& builder) { +static void perform_element(const xmlelement& root, ModelBuilder& builder, Context& ctx) { auto tag = root.getTag(); if (tag == "rect") { @@ -330,22 +358,31 @@ static void perform_element(const xmlelement& root, ModelBuilder& builder) { } else if (tag == "tri") { perform_triangle(root, builder); } else if (tag == "bone") { - perform_bone(root, builder); + perform_bone(root, builder, ctx); } } -static std::unique_ptr load_model(const xmlelement& root) { +static VcmModel load_model(const xmlelement& root) { + VcmModel vcmModel {}; Model model; ModelBuilder builder(model); + size_t boneIndex = 0; + Bone rootBone(boneIndex++, "root", "root", {}, glm::vec3(0.0f)); + Context ctx { vcmModel, rootBone, boneIndex }; + for (const auto& elem : root.getElements()) { - perform_element(*elem, builder); + perform_element(*elem, builder, ctx); } - return std::make_unique(std::move(model)); + vcmModel.parts["root"] = std::move(model); + vcmModel.skeleton = std::make_unique( + "", std::make_unique(std::move(rootBone)), boneIndex + ); + return vcmModel; } -std::unique_ptr vcm::parse( +VcmModel vcm::parseFull( std::string_view file, std::string_view src, bool usexml ) { try { @@ -362,3 +399,41 @@ std::unique_ptr vcm::parse( throw std::runtime_error(err.errorLog()); } } + +static int calc_offsets( + const Bone& bone, std::vector& dst, int index, int depth, int parent +) { + if (depth == 0) { + dst[0] = bone.getOffset(); + } else { + dst[index] = dst[parent] + bone.getOffset(); + } + const auto& subBones = bone.getSubnodes(); + + int subIndex = index + 1; + for (int i = 0; i < subBones.size(); i++) { + subIndex += calc_offsets(*subBones[i], dst, subIndex, depth + 1, index); + } + return subIndex - index; +} + +std::unique_ptr vcm::parse( + std::string_view file, std::string_view src, bool usexml +) { + auto vcmModel = parseFull(file, src, usexml); + + std::vector fullOffsets(vcmModel.skeleton->getBones().size()); + calc_offsets(*vcmModel.skeleton->getRoot(), fullOffsets, 0, 0, 0); + + Model fullModel; + for (auto& [name, model] : vcmModel.parts) { + if (auto bone = vcmModel.skeleton->find(name)) { + model.translate(fullOffsets[bone->getIndex()]); + } else { + throw std::runtime_error("invalid state: bones/parts mismatch"); + } + fullModel.merge(std::move(model)); + } + + return std::make_unique(std::move(fullModel)); +} diff --git a/src/coders/vcm.hpp b/src/coders/vcm.hpp index e0629de18..1487ca847 100644 --- a/src/coders/vcm.hpp +++ b/src/coders/vcm.hpp @@ -2,12 +2,24 @@ #include #include +#include -namespace model { - struct Model; +#include "graphics/commons/Model.hpp" + +namespace rigging { + class SkeletonConfig; } namespace vcm { + struct VcmModel { + std::unordered_map parts; + std::unique_ptr skeleton; + }; + + VcmModel parseFull( + std::string_view file, std::string_view src, bool usexml + ); + std::unique_ptr parse( std::string_view file, std::string_view src, bool usexml ); diff --git a/src/graphics/commons/Model.cpp b/src/graphics/commons/Model.cpp index 0615ce2d8..2de10ad30 100644 --- a/src/graphics/commons/Model.cpp +++ b/src/graphics/commons/Model.cpp @@ -162,6 +162,12 @@ void Mesh::transform(const glm::mat4& matrix) { } } +void Mesh::translate(const glm::vec3& offset) { + for (auto& vertex : vertices) { + vertex.coord += offset; + } +} + void Model::clean() { meshes.erase( std::remove_if(meshes.begin(), meshes.end(), @@ -178,6 +184,12 @@ void Model::transform(const glm::mat4& matrix) { } } +void Model::translate(const glm::vec3& offset) { + for (auto& mesh : meshes) { + mesh.translate(offset); + } +} + void Model::merge(const Model& source) { for (const auto& srcMesh : source.meshes) { auto& dstMesh = addMesh(srcMesh.texture, srcMesh.shading); @@ -188,3 +200,18 @@ void Model::merge(const Model& source) { ); } } + +void Model::merge(Model&& source) { + for (auto& srcMesh : source.meshes) { + auto& dstMesh = addMesh(srcMesh.texture, srcMesh.shading); + if (dstMesh.vertices.empty()) { + dstMesh = std::move(srcMesh); + } else { + dstMesh.vertices.insert( + dstMesh.vertices.end(), + srcMesh.vertices.begin(), + srcMesh.vertices.end() + ); + } + } +} diff --git a/src/graphics/commons/Model.hpp b/src/graphics/commons/Model.hpp index 36962be88..e42928fba 100644 --- a/src/graphics/commons/Model.hpp +++ b/src/graphics/commons/Model.hpp @@ -75,6 +75,9 @@ namespace model { /// @brief Apply transformation matrix to vertices coordinates void transform(const glm::mat4& matrix); + + /// @brief Move vertices coordinates by offset + void translate(const glm::vec3& offset); }; struct Model { @@ -92,14 +95,22 @@ namespace model { meshes.push_back({texture, {}, shading}); return meshes[meshes.size() - 1]; } + /// @brief Remove all empty meshes void clean(); /// @brief Apply transformation matrix to vertices coordinates void transform(const glm::mat4& matrix); + /// @brief Move vertices coordinates by offset + void translate(const glm::vec3& offset); + /// @brief Copy or merge meshes from source model to this. /// Does not remove duplicates void merge(const Model& source); + + /// @brief Move or merge meshes from source model to this. + /// Does not remove duplicates + void merge(Model&& source); }; } diff --git a/src/objects/rigging.cpp b/src/objects/rigging.cpp index da56b26cf..52546c99a 100644 --- a/src/objects/rigging.cpp +++ b/src/objects/rigging.cpp @@ -32,6 +32,11 @@ Bone::Bone( model({model, nullptr, true}) { } +Bone& Bone::addBone(Bone&& bone) { + bones.push_back(std::make_unique(std::move(bone))); + return *bones[bones.size() - 1]; +} + void Bone::setModel(const std::string& name) { if (model.name == name) { return; diff --git a/src/objects/rigging.hpp b/src/objects/rigging.hpp index c0570c065..940a5d018 100644 --- a/src/objects/rigging.hpp +++ b/src/objects/rigging.hpp @@ -54,6 +54,8 @@ namespace rigging { const glm::vec3& offset ); + Bone& addBone(Bone&& bone); + void setModel(const std::string& name); const std::string& getName() const { From 6760d16052b42354518785b3d40ff8247fc713f0 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 15 Jan 2026 22:57:04 +0300 Subject: [PATCH 06/14] update vcm::parse semantics --- src/assets/assetload_funcs.cpp | 4 +- src/coders/vcm.cpp | 82 +++++++++++----------- src/coders/vcm.hpp | 13 ++-- src/logic/scripting/lua/libs/libassets.cpp | 7 +- 4 files changed, 55 insertions(+), 51 deletions(-) diff --git a/src/assets/assetload_funcs.cpp b/src/assets/assetload_funcs.cpp index a1c04dfcc..1bf76e6af 100644 --- a/src/assets/assetload_funcs.cpp +++ b/src/assets/assetload_funcs.cpp @@ -381,8 +381,8 @@ assetload::postfunc assetload::model( auto text = io::read_string(path); try { - auto model = vcm::parse(path.string(), text, path.extension() == ".xml") - .release(); + auto vcmModel = vcm::parse(path.string(), text, path.extension() == ".xml"); + auto model = std::make_unique(std::move(vcmModel.squash())).release(); return [=](Assets* assets) { request_textures(loader, *model); assets->store(std::unique_ptr(model), name); diff --git a/src/coders/vcm.cpp b/src/coders/vcm.cpp index 4acf8b631..8a0119e59 100644 --- a/src/coders/vcm.cpp +++ b/src/coders/vcm.cpp @@ -2,7 +2,6 @@ #include "xml.hpp" #include "util/stringutil.hpp" -#include "objects/rigging.hpp" #include "io/io.hpp" #include @@ -16,6 +15,45 @@ using namespace xml; using namespace model; using namespace rigging; +static int calc_offsets( + const Bone& bone, std::vector& dst, int index, int depth, int parent +) { + if (depth == 0) { + dst[0] = bone.getOffset(); + } else { + dst[index] = dst[parent] + bone.getOffset(); + } + const auto& subBones = bone.getSubnodes(); + + int subIndex = index + 1; + for (int i = 0; i < subBones.size(); i++) { + subIndex += calc_offsets(*subBones[i], dst, subIndex, depth + 1, index); + } + return subIndex - index; +} + +model::Model& VcmModel::squash() { + std::vector fullOffsets(skeleton->getBones().size()); + calc_offsets(*skeleton->getRoot(), fullOffsets, 0, 0, 0); + + Model squashed; + for (auto& [name, model] : parts) { + if (auto bone = skeleton->find(name)) { + model.translate(fullOffsets[bone->getIndex()]); + } else { + throw std::runtime_error("invalid state: bones/parts mismatch"); + } + squashed.merge(std::move(model)); + } + parts = { {"", std::move(squashed)} }; + skeleton.reset(); + return parts.at(""); +} + +model::Model VcmModel::squashed() const { + return std::move(VcmModel {parts, std::nullopt}).squash(); +} + static const std::unordered_map side_indices { {"north", 0}, {"south", 1}, @@ -376,13 +414,13 @@ static VcmModel load_model(const xmlelement& root) { } vcmModel.parts["root"] = std::move(model); - vcmModel.skeleton = std::make_unique( + vcmModel.skeleton = SkeletonConfig( "", std::make_unique(std::move(rootBone)), boneIndex ); return vcmModel; } -VcmModel vcm::parseFull( +VcmModel vcm::parse( std::string_view file, std::string_view src, bool usexml ) { try { @@ -399,41 +437,3 @@ VcmModel vcm::parseFull( throw std::runtime_error(err.errorLog()); } } - -static int calc_offsets( - const Bone& bone, std::vector& dst, int index, int depth, int parent -) { - if (depth == 0) { - dst[0] = bone.getOffset(); - } else { - dst[index] = dst[parent] + bone.getOffset(); - } - const auto& subBones = bone.getSubnodes(); - - int subIndex = index + 1; - for (int i = 0; i < subBones.size(); i++) { - subIndex += calc_offsets(*subBones[i], dst, subIndex, depth + 1, index); - } - return subIndex - index; -} - -std::unique_ptr vcm::parse( - std::string_view file, std::string_view src, bool usexml -) { - auto vcmModel = parseFull(file, src, usexml); - - std::vector fullOffsets(vcmModel.skeleton->getBones().size()); - calc_offsets(*vcmModel.skeleton->getRoot(), fullOffsets, 0, 0, 0); - - Model fullModel; - for (auto& [name, model] : vcmModel.parts) { - if (auto bone = vcmModel.skeleton->find(name)) { - model.translate(fullOffsets[bone->getIndex()]); - } else { - throw std::runtime_error("invalid state: bones/parts mismatch"); - } - fullModel.merge(std::move(model)); - } - - return std::make_unique(std::move(fullModel)); -} diff --git a/src/coders/vcm.hpp b/src/coders/vcm.hpp index 1487ca847..4f97ea8d3 100644 --- a/src/coders/vcm.hpp +++ b/src/coders/vcm.hpp @@ -2,9 +2,11 @@ #include #include +#include #include #include "graphics/commons/Model.hpp" +#include "objects/rigging.hpp" namespace rigging { class SkeletonConfig; @@ -13,14 +15,13 @@ namespace rigging { namespace vcm { struct VcmModel { std::unordered_map parts; - std::unique_ptr skeleton; + std::optional skeleton; + + model::Model& squash(); + model::Model squashed() const; }; - VcmModel parseFull( - std::string_view file, std::string_view src, bool usexml - ); - - std::unique_ptr parse( + VcmModel parse( std::string_view file, std::string_view src, bool usexml ); } diff --git a/src/logic/scripting/lua/libs/libassets.cpp b/src/logic/scripting/lua/libs/libassets.cpp index 35720195e..6a16c3cfd 100644 --- a/src/logic/scripting/lua/libs/libassets.cpp +++ b/src/logic/scripting/lua/libs/libassets.cpp @@ -57,11 +57,14 @@ static int l_parse_model(lua::State* L) { auto name = lua::require_string(L, 3); if (format == "xml" || format == "vcm") { + auto vcmModel = vcm::parse(name, string, format == "xml"); engine->getAssets()->store( - vcm::parse(name, string, format == "xml"), name + std::make_unique(std::move(vcmModel.squash())), name ); } else { - throw std::runtime_error("unknown format " + util::quote(std::string(format))); + throw std::runtime_error( + "unknown format " + util::quote(std::string(format)) + ); } return 0; } From 587c140a6b7fd18143bac3c472613b07ff431114 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 15 Jan 2026 23:17:47 +0300 Subject: [PATCH 07/14] remove bone::addBone --- src/coders/vcm.cpp | 27 +++++++++++++++++---------- src/objects/Entities.cpp | 2 +- src/objects/rigging.cpp | 11 +++-------- src/objects/rigging.hpp | 6 ++---- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/coders/vcm.cpp b/src/coders/vcm.cpp index 8a0119e59..0f408131d 100644 --- a/src/coders/vcm.cpp +++ b/src/coders/vcm.cpp @@ -23,7 +23,7 @@ static int calc_offsets( } else { dst[index] = dst[parent] + bone.getOffset(); } - const auto& subBones = bone.getSubnodes(); + const auto& subBones = bone.getBones(); int subIndex = index + 1; for (int i = 0; i < subBones.size(); i++) { @@ -151,7 +151,7 @@ private: struct Context { VcmModel& vcmModel; - Bone& bone; + std::vector>& bones; size_t& boneIndex; }; @@ -372,16 +372,19 @@ static void perform_bone(const xmlelement& root, ModelBuilder& builder, Context& builder.pop(); } else { glm::vec3 origin = builder.getTransform() * glm::vec4(0.0f, 0.0f, 0.0f, 1.0f); - auto& bone = ctx.bone.addBone( - Bone(ctx.boneIndex++, name, name, {}, std::move(origin)) - ); - Context boneContext {ctx.vcmModel, bone, ctx.boneIndex}; + size_t boneIndex = ctx.boneIndex++; + + std::vector> bones; + Context boneContext {ctx.vcmModel, bones, ctx.boneIndex}; Model boneModel; ModelBuilder boneModelBuilder(boneModel); boneModelBuilder.push(std::move(tsf)); for (const auto& elem : root.getElements()) { perform_element(*elem, boneModelBuilder, boneContext); } + ctx.bones.push_back(std::make_unique( + boneIndex, name, name, std::move(bones), std::move(origin) + )); ctx.vcmModel.parts[std::move(name)] = std::move(boneModel); } } @@ -405,9 +408,9 @@ static VcmModel load_model(const xmlelement& root) { Model model; ModelBuilder builder(model); - size_t boneIndex = 0; - Bone rootBone(boneIndex++, "root", "root", {}, glm::vec3(0.0f)); - Context ctx { vcmModel, rootBone, boneIndex }; + size_t boneIndex = 1; + std::vector> bones; + Context ctx { vcmModel, bones, boneIndex }; for (const auto& elem : root.getElements()) { perform_element(*elem, builder, ctx); @@ -415,7 +418,11 @@ static VcmModel load_model(const xmlelement& root) { vcmModel.parts["root"] = std::move(model); vcmModel.skeleton = SkeletonConfig( - "", std::make_unique(std::move(rootBone)), boneIndex + "", + std::make_unique( + 0, "root", "root", std::move(bones), glm::vec3(0.0f) + ), + boneIndex ); return vcmModel; } diff --git a/src/objects/Entities.cpp b/src/objects/Entities.cpp index cf31f5916..3b4e53627 100644 --- a/src/objects/Entities.cpp +++ b/src/objects/Entities.cpp @@ -333,7 +333,7 @@ static void debug_render_skeleton( const rigging::Skeleton& skeleton ) { size_t pindex = bone->getIndex(); - for (auto& sub : bone->getSubnodes()) { + for (auto& sub : bone->getBones()) { size_t sindex = sub->getIndex(); const auto& matrices = skeleton.calculated.matrices; batch.line( diff --git a/src/objects/rigging.cpp b/src/objects/rigging.cpp index 52546c99a..d1a41bef7 100644 --- a/src/objects/rigging.cpp +++ b/src/objects/rigging.cpp @@ -32,11 +32,6 @@ Bone::Bone( model({model, nullptr, true}) { } -Bone& Bone::addBone(Bone&& bone) { - bones.push_back(std::make_unique(std::move(bone))); - return *bones[bones.size() - 1]; -} - void Bone::setModel(const std::string& name) { if (model.name == name) { return; @@ -93,7 +88,7 @@ void Skeleton::deserialize(const dv::value& root) { static void get_all_nodes(std::vector& nodes, Bone* node) { nodes[node->getIndex()] = node; - for (auto& subnode : node->getSubnodes()) { + for (auto& subnode : node->getBones()) { get_all_nodes(nodes, subnode.get()); } } @@ -116,7 +111,7 @@ size_t SkeletonConfig::update( } skeleton.calculated.matrices[index] = matrix * baseMatrix * boneMatrix; size_t count = 1; - for (auto& subnode : node->getSubnodes()) { + for (auto& subnode : node->getBones()) { count += update( index + count, skeleton, @@ -192,7 +187,7 @@ void SkeletonConfig::render( } } -Bone* SkeletonConfig::find(std::string_view str) const { +const Bone* SkeletonConfig::find(std::string_view str) const { for (size_t i = 0; i < nodes.size(); i++) { auto* node = nodes[i]; if (node->getName() == str) { diff --git a/src/objects/rigging.hpp b/src/objects/rigging.hpp index 940a5d018..7726d0b95 100644 --- a/src/objects/rigging.hpp +++ b/src/objects/rigging.hpp @@ -54,8 +54,6 @@ namespace rigging { const glm::vec3& offset ); - Bone& addBone(Bone&& bone); - void setModel(const std::string& name); const std::string& getName() const { @@ -70,7 +68,7 @@ namespace rigging { return offset; } - const auto& getSubnodes() const { + const auto& getBones() const { return bones; } }; @@ -140,7 +138,7 @@ namespace rigging { return Skeleton(this); } - Bone* find(std::string_view str) const; + const Bone* find(std::string_view str) const; static std::unique_ptr parse( std::string_view src, std::string_view file, std::string_view name From 42c4716be2b4b6b0af9a0719f0b0d7b8b277849f Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 16 Jan 2026 00:34:20 +0300 Subject: [PATCH 08/14] change: skeletons are assets now, not content units --- src/assets/Assets.hpp | 3 +- src/assets/AssetsLoader.cpp | 28 +++---- src/assets/assetload_funcs.cpp | 26 +++++++ src/assets/assetload_funcs.hpp | 8 ++ src/content/Content.cpp | 25 ------- src/content/Content.hpp | 9 --- src/content/ContentBuilder.cpp | 7 -- src/content/ContentBuilder.hpp | 2 - src/content/ContentLoader.cpp | 12 --- src/frontend/screens/LevelScreen.cpp | 2 + src/graphics/render/WorldRenderer.cpp | 3 +- .../scripting/lua/libs/lib__skeleton.cpp | 6 +- src/logic/scripting/lua/libs/libentity.cpp | 13 +++- src/objects/Entities.cpp | 74 +++++++++++-------- src/objects/Entities.hpp | 11 ++- src/objects/Entity.cpp | 33 +++++---- src/objects/Entity.hpp | 2 +- 17 files changed, 144 insertions(+), 120 deletions(-) diff --git a/src/assets/Assets.hpp b/src/assets/Assets.hpp index c0853b0b4..ea0753aec 100644 --- a/src/assets/Assets.hpp +++ b/src/assets/Assets.hpp @@ -24,7 +24,8 @@ enum class AssetType { LAYOUT, SOUND, MODEL, - POST_EFFECT + POST_EFFECT, + SKELETON }; namespace assetload { diff --git a/src/assets/AssetsLoader.cpp b/src/assets/AssetsLoader.cpp index 49ce5e04d..9729eedb8 100644 --- a/src/assets/AssetsLoader.cpp +++ b/src/assets/AssetsLoader.cpp @@ -36,6 +36,7 @@ AssetsLoader::AssetsLoader(Engine& engine, Assets& assets, const ResPaths& paths addLoader(AssetType::SOUND, assetload::sound); addLoader(AssetType::MODEL, assetload::model); addLoader(AssetType::POST_EFFECT, assetload::posteffect); + addLoader(AssetType::SKELETON, assetload::skeleton); } void AssetsLoader::addLoader(AssetType tag, aloader_func func) { @@ -141,6 +142,8 @@ static std::string assets_def_folder(AssetType tag) { return MODELS_FOLDER; case AssetType::POST_EFFECT: return POST_EFFECTS_FOLDER; + case AssetType::SKELETON: + return SKELETONS_FOLDER; } return ""; } @@ -263,21 +266,20 @@ void AssetsLoader::addDefaults(AssetsLoader& loader, const Content* content) { add_layouts(pack->getEnvironment(), info.id, folder, loader); } - for (auto& entry : content->getSkeletons()) { - auto& skeleton = *entry.second; - for (auto& bone : skeleton.getBones()) { - std::string model = bone->model.name; - size_t pos = model.rfind('.'); - if (pos != std::string::npos) { - model = model.substr(0, pos); - } - if (!model.empty()) { - loader.add( - AssetType::MODEL, MODELS_FOLDER + "/" + model, model - ); - } + for (const auto& entry : content->getPacks()) { + io::path skeletonsDir = entry.first + ":skeletons"; + if (!io::is_directory(skeletonsDir)) { + continue; + } + for (const auto& file : io::directory_iterator(skeletonsDir)) { + loader.add( + AssetType::SKELETON, + file.string(), + entry.first + ":" + file.stem() + ); } } + for (const auto& [_, def] : content->blocks.getDefs()) { if (def->variants) { for (const auto& variant : def->variants->variants) { diff --git a/src/assets/assetload_funcs.cpp b/src/assets/assetload_funcs.cpp index 1bf76e6af..f739aee7c 100644 --- a/src/assets/assetload_funcs.cpp +++ b/src/assets/assetload_funcs.cpp @@ -393,6 +393,32 @@ assetload::postfunc assetload::model( } } +assetload::postfunc assetload::skeleton( + AssetsLoader* loader, + const ResPaths& paths, + const std::string& file, + const std::string& name, + const std::shared_ptr& settings +) { + return [=](auto assets) { + std::string text = io::read_string(file); + auto skeleton = rigging::SkeletonConfig::parse(text, file, name); + for (auto& bone : skeleton->getBones()) { + std::string model = bone->model.name; + size_t pos = model.rfind('.'); + if (pos != std::string::npos) { + model = model.substr(0, pos); + } + if (!model.empty()) { + loader->add( + AssetType::MODEL, MODELS_FOLDER + "/" + model, model + ); + } + } + assets->store(std::move(skeleton), name); + }; +} + static void read_anim_file( const std::string& animFile, std::vector>& frameList diff --git a/src/assets/assetload_funcs.hpp b/src/assets/assetload_funcs.hpp index 97ed60ea1..50a255bb2 100644 --- a/src/assets/assetload_funcs.hpp +++ b/src/assets/assetload_funcs.hpp @@ -69,4 +69,12 @@ namespace assetload { const std::string& name, const std::shared_ptr& settings ); + + postfunc skeleton( + AssetsLoader*, + const ResPaths& paths, + const std::string& file, + const std::string& name, + const std::shared_ptr& settings + ); } diff --git a/src/content/Content.cpp b/src/content/Content.cpp index 0c697fd1d..15d68512b 100644 --- a/src/content/Content.cpp +++ b/src/content/Content.cpp @@ -8,7 +8,6 @@ #include "items/ItemDef.hpp" #include "logic/scripting/scripting.hpp" #include "objects/EntityDef.hpp" -#include "objects/rigging.hpp" #include "voxels/Block.hpp" #include "world/generator/VoxelFragment.hpp" #include "world/generator/GeneratorDef.hpp" @@ -33,7 +32,6 @@ Content::Content( ContentUnitDefs generators, UptrsMap packs, UptrsMap blockMaterials, - UptrsMap skeletons, ResourceIndicesSet resourceIndices, dv::value defaults, std::unordered_map tags @@ -41,7 +39,6 @@ Content::Content( : indices(std::move(indices)), packs(std::move(packs)), blockMaterials(std::move(blockMaterials)), - skeletons(std::move(skeletons)), defaults(std::move(defaults)), tags(std::move(tags)), blocks(std::move(blocks)), @@ -56,23 +53,6 @@ Content::Content( Content::~Content() = default; -const rigging::SkeletonConfig* Content::getSkeleton(const std::string& id -) const { - auto found = skeletons.find(id); - if (found == skeletons.end()) { - return nullptr; - } - return found->second.get(); -} - -const rigging::SkeletonConfig& Content::requireSkeleton(const std::string& id) const { - auto skeleton = getSkeleton(id); - if (skeleton == nullptr) { - throw std::runtime_error("skeleton '" + id + "' not loaded"); - } - return *skeleton; -} - const BlockMaterial* Content::findBlockMaterial(const std::string& id) const { auto found = blockMaterials.find(id); if (found == blockMaterials.end()) { @@ -104,8 +84,3 @@ const UptrsMap& Content::getBlockMaterials() const { const UptrsMap& Content::getPacks() const { return packs; } - -const UptrsMap& Content::getSkeletons( -) const { - return skeletons; -} diff --git a/src/content/Content.hpp b/src/content/Content.hpp index 12dc77d2f..dd0e30bf8 100644 --- a/src/content/Content.hpp +++ b/src/content/Content.hpp @@ -20,10 +20,6 @@ struct ItemDef; struct EntityDef; struct GeneratorDef; -namespace rigging { - class SkeletonConfig; -} - class namereuse_error : public std::runtime_error { ContentType type; public: @@ -183,7 +179,6 @@ class Content { std::unique_ptr indices; UptrsMap packs; UptrsMap blockMaterials; - UptrsMap skeletons; dv::value defaults = nullptr; std::unordered_map tags; public: @@ -203,7 +198,6 @@ public: ContentUnitDefs generators, UptrsMap packs, UptrsMap blockMaterials, - UptrsMap skeletons, ResourceIndicesSet resourceIndices, dv::value defaults, std::unordered_map tags @@ -230,13 +224,10 @@ public: return found->second; } - const rigging::SkeletonConfig* getSkeleton(const std::string& id) const; - const rigging::SkeletonConfig& requireSkeleton(const std::string& id) const; const BlockMaterial* findBlockMaterial(const std::string& id) const; const ContentPackRuntime* getPackRuntime(const std::string& id) const; ContentPackRuntime* getPackRuntime(const std::string& id); const UptrsMap& getBlockMaterials() const; const UptrsMap& getPacks() const; - const UptrsMap& getSkeletons() const; }; diff --git a/src/content/ContentBuilder.cpp b/src/content/ContentBuilder.cpp index d32cb2860..ce77434ed 100644 --- a/src/content/ContentBuilder.cpp +++ b/src/content/ContentBuilder.cpp @@ -1,17 +1,11 @@ #include "ContentBuilder.hpp" -#include "objects/rigging.hpp" - ContentBuilder::~ContentBuilder() = default; void ContentBuilder::add(std::unique_ptr pack) { packs[pack->getId()] = std::move(pack); } -void ContentBuilder::add(std::unique_ptr skeleton) { - skeletons[skeleton->getName()] = std::move(skeleton); -} - BlockMaterial& ContentBuilder::createBlockMaterial(const std::string& id) { blockMaterials[id] = std::make_unique(); auto& material = *blockMaterials[id]; @@ -98,7 +92,6 @@ std::unique_ptr ContentBuilder::build() { generators.build(), std::move(packs), std::move(blockMaterials), - std::move(skeletons), std::move(resourceIndices), std::move(defaults), std::move(tags.map) diff --git a/src/content/ContentBuilder.hpp b/src/content/ContentBuilder.hpp index 8a938819b..b1c4d26c3 100644 --- a/src/content/ContentBuilder.hpp +++ b/src/content/ContentBuilder.hpp @@ -85,7 +85,6 @@ struct TagsIndices { class ContentBuilder { UptrsMap blockMaterials; - UptrsMap skeletons; UptrsMap packs; std::unordered_map allNames; public: @@ -100,7 +99,6 @@ public: ~ContentBuilder(); void add(std::unique_ptr pack); - void add(std::unique_ptr skeleton); BlockMaterial& createBlockMaterial(const std::string& id); diff --git a/src/content/ContentLoader.cpp b/src/content/ContentLoader.cpp index ed4ab34d8..e2bf81f2d 100644 --- a/src/content/ContentLoader.cpp +++ b/src/content/ContentLoader.cpp @@ -3,14 +3,12 @@ #include #include -#include #include "loading/ContentUnitLoader.hpp" #include "ContentBuilder.hpp" #include "ContentPack.hpp" #include "debug/Logger.hpp" #include "logic/scripting/scripting.hpp" -#include "objects/rigging.hpp" #include "util/listutil.hpp" #include "util/stringutil.hpp" #include "engine/EnginePaths.hpp" @@ -399,16 +397,6 @@ void ContentLoader::load() { } } - // Load skeletons - io::path skeletonsDir = folder / "skeletons"; - foreach_file(skeletonsDir, [this](const io::path& file) { - std::string name = pack->id + ":" + file.stem(); - std::string text = io::read_string(file); - builder.add( - rigging::SkeletonConfig::parse(text, file.string(), name) - ); - }); - // Process content.json and load defined content units auto contentFile = pack->getContentFile(); if (io::exists(contentFile)) { diff --git a/src/frontend/screens/LevelScreen.cpp b/src/frontend/screens/LevelScreen.cpp index 0f93de8cc..02119b9ad 100644 --- a/src/frontend/screens/LevelScreen.cpp +++ b/src/frontend/screens/LevelScreen.cpp @@ -26,6 +26,7 @@ #include "logic/scripting/scripting_hud.hpp" #include "maths/voxmaths.hpp" #include "objects/Players.hpp" +#include "objects/Entities.hpp" #include "physics/Hitbox.hpp" #include "util/stringutil.hpp" #include "voxels/Chunks.hpp" @@ -49,6 +50,7 @@ LevelScreen::LevelScreen( gui(engine.getGUI()), input(engine.getInput()) { Level* level = levelPtr.get(); + level->entities->setAssets(*engine.getAssets()); auto& settings = engine.getSettings(); auto& assets = *engine.getAssets(); diff --git a/src/graphics/render/WorldRenderer.cpp b/src/graphics/render/WorldRenderer.cpp index 70195f927..7b505d868 100644 --- a/src/graphics/render/WorldRenderer.cpp +++ b/src/graphics/render/WorldRenderer.cpp @@ -23,6 +23,7 @@ #include "maths/voxmaths.hpp" #include "objects/Entities.hpp" #include "objects/Player.hpp" +#include "objects/rigging.hpp" #include "settings.hpp" #include "voxels/Block.hpp" #include "voxels/Chunk.hpp" @@ -110,7 +111,7 @@ WorldRenderer::WorldRenderer( const auto& content = level.content; skeletons = std::make_unique(); - const auto& skeletonConfig = content.requireSkeleton( + const auto& skeletonConfig = assets->require( content.getDefaults()["hand-skeleton"].asString() ); hands = std::make_unique( diff --git a/src/logic/scripting/lua/libs/lib__skeleton.cpp b/src/logic/scripting/lua/libs/lib__skeleton.cpp index 3fd248725..be42c356e 100644 --- a/src/logic/scripting/lua/libs/lib__skeleton.cpp +++ b/src/logic/scripting/lua/libs/lib__skeleton.cpp @@ -1,6 +1,7 @@ #include "objects/rigging.hpp" #include "libentity.hpp" +#include "engine/Engine.hpp" #include "graphics/render/WorldRenderer.hpp" #include "graphics/render/NamedSkeletons.hpp" @@ -21,11 +22,14 @@ static int index_range_check( } static rigging::Skeleton* get_skeleton(lua::State* L) { + if (scripting::engine->isHeadless()) { + return nullptr; + } if (lua::isstring(L, 1)) { return scripting::renderer->skeletons->getSkeleton(lua::tostring(L, 1)); } if (auto entity = get_entity(L, 1)) { - return &entity->getSkeleton(); + return entity->getSkeleton(); } return nullptr; } diff --git a/src/logic/scripting/lua/libs/libentity.cpp b/src/logic/scripting/lua/libs/libentity.cpp index 5bf3e4d74..0ae21f3c7 100644 --- a/src/logic/scripting/lua/libs/libentity.cpp +++ b/src/logic/scripting/lua/libs/libentity.cpp @@ -1,5 +1,6 @@ #include "libentity.hpp" +#include "assets/Assets.hpp" #include "content/Content.hpp" #include "content/ContentPack.hpp" #include "engine/Engine.hpp" @@ -82,7 +83,11 @@ static int l_despawn(lua::State* L) { static int l_get_skeleton(lua::State* L) { if (auto entity = get_entity(L, 1)) { - return lua::pushstring(L, entity->getSkeleton().config->getName()); + auto skeleton = entity->getSkeleton(); + if (skeleton == nullptr) { + return 0; + } + return lua::pushstring(L, skeleton->config->getName()); } return 0; } @@ -97,9 +102,13 @@ static int l_get_player(lua::State* L) { } static int l_set_skeleton(lua::State* L) { + auto assets = engine->getAssets(); + if (assets == nullptr) { + return 0; + } if (auto entity = get_entity(L, 1)) { std::string skeletonName = lua::require_string(L, 2); - auto rigConfig = content->getSkeleton(skeletonName); + auto rigConfig = assets->get(skeletonName); if (rigConfig == nullptr) { throw std::runtime_error( "skeleton not found '" + skeletonName + "'" diff --git a/src/objects/Entities.cpp b/src/objects/Entities.cpp index 3b4e53627..522ba6a0a 100644 --- a/src/objects/Entities.cpp +++ b/src/objects/Entities.cpp @@ -1,6 +1,7 @@ #define VC_ENABLE_REFLECTION #include "Entities.hpp" +#include #include #include @@ -25,15 +26,22 @@ static debug::Logger logger("entities"); Entities::Entities(Level& level) - : level(level), + : registry(std::make_unique()), + level(level), sensorsTickClock(20, 3), updateTickClock(20, 3) { } +Entities::~Entities() = default; + +void Entities::setAssets(Assets& assets) { + this->assets = &assets; +} + std::optional Entities::get(entityid_t id) { const auto& found = entities.find(id); - if (found != entities.end() && registry.valid(found->second)) { - return Entity(*this, id, registry, found->second); + if (found != entities.end() && registry->valid(found->second)) { + return Entity(*this, id, *registry, found->second); } return std::nullopt; } @@ -45,9 +53,12 @@ entityid_t Entities::spawn( dv::value saved, entityid_t uid ) { - auto skeleton = level.content.getSkeleton(def.skeletonName); - if (skeleton == nullptr) { - throw std::runtime_error("skeleton " + def.skeletonName + " not found"); + rigging::SkeletonConfig* skeleton = nullptr; + if (assets) { + skeleton = assets->get(def.skeletonName); + if (skeleton == nullptr) { + throw std::runtime_error("skeleton " + def.skeletonName + " not found"); + } } entityid_t id; if (uid == 0) { @@ -64,12 +75,12 @@ entityid_t Entities::spawn( throw std::runtime_error(ss.str()); } } - auto entity = registry.create(); + auto entity = registry->create(); entities[id] = entity; uids[entity] = id; - registry.emplace(entity, static_cast(id), def); - const auto& tsf = registry.emplace( + registry->emplace(entity, static_cast(id), def); + const auto& tsf = registry->emplace( entity, position, glm::vec3(1.0f), @@ -77,7 +88,7 @@ entityid_t Entities::spawn( glm::mat4(1.0f), true ); - auto& body = registry.emplace( + auto& body = registry->emplace( entity, true, Hitbox {def.bodyType, position, def.hitbox * 0.5f}, @@ -85,8 +96,10 @@ entityid_t Entities::spawn( ); body.initialize(def, id, *this); - auto& scripting = registry.emplace(entity); - registry.emplace(entity, skeleton->instance()); + auto& scripting = registry->emplace(entity); + if (assets) { + registry->emplace(entity, skeleton->instance()); + } for (auto& instance : def.components) { auto component = std::make_unique( @@ -126,7 +139,7 @@ void Entities::loadEntity(const dv::value& map) { void Entities::loadEntity(const dv::value& map, Entity entity) { auto& transform = entity.getTransform(); auto& body = entity.getRigidbody(); - auto& skeleton = entity.getSkeleton(); + auto skeleton = entity.getSkeleton(); if (map.has(COMP_RIGIDBODY)) { body.deserialize(map[COMP_RIGIDBODY]); @@ -134,13 +147,16 @@ void Entities::loadEntity(const dv::value& map, Entity entity) { if (map.has(COMP_TRANSFORM)) { transform.deserialize(map[COMP_TRANSFORM]); } - std::string skeletonName = skeleton.config->getName(); + if (skeleton == nullptr) { + return; + } + std::string skeletonName = skeleton->config->getName(); map.at("skeleton").get(skeletonName); - if (skeletonName != skeleton.config->getName()) { - skeleton.config = level.content.getSkeleton(skeletonName); + if (skeletonName != skeleton->config->getName()) { + skeleton->config = assets->get(skeletonName); } if (auto foundSkeleton = map.at(COMP_SKELETON)) { - skeleton.deserialize(*foundSkeleton); + skeleton->deserialize(*foundSkeleton); } } @@ -148,7 +164,7 @@ std::optional Entities::rayCast( glm::vec3 start, glm::vec3 dir, float maxDistance, entityid_t ignore ) { Ray ray(start, dir); - auto view = registry.view(); + auto view = registry->view(); entityid_t foundUID = 0; glm::ivec3 foundNormal; @@ -214,17 +230,17 @@ void Entities::despawn(std::vector entities) { void Entities::clean() { for (auto it = entities.begin(); it != entities.end();) { - if (!registry.get(it->second).destroyFlag) { + if (!registry->get(it->second).destroyFlag) { ++it; } else { - auto& rigidbody = registry.get(it->second); + auto& rigidbody = registry->get(it->second); // todo: refactor auto physics = level.physics.get(); for (auto& sensor : rigidbody.sensors) { physics->removeSensor(&sensor); } uids.erase(it->second); - registry.destroy(it->second); + registry->destroy(it->second); it = entities.erase(it); } } @@ -266,7 +282,7 @@ void Entities::preparePhysics(float delta) { auto part = sensorsTickClock.getPart(); auto parts = sensorsTickClock.getParts(); - auto view = registry.view(); + auto view = registry->view(); auto physics = level.physics.get(); std::vector sensors; for (auto [entity, eid, transform, rigidbody] : view.each()) { @@ -285,7 +301,7 @@ void Entities::preparePhysics(float delta) { void Entities::updatePhysics(float delta) { preparePhysics(delta); - auto view = registry.view(); + auto view = registry->view(); auto physics = level.physics.get(); for (auto [entity, eid, transform, rigidbody] : view.each()) { if (!rigidbody.enabled || rigidbody.hitbox.type == BodyType::STATIC) { @@ -351,7 +367,7 @@ void Entities::renderDebug( { auto ctx = pctx.sub(&batch); ctx.setLineWidth(1); - auto view = registry.view(); + auto view = registry->view(); for (auto [entity, transform, rigidbody] : view.each()) { const auto& hitbox = rigidbody.hitbox; const auto& pos = transform.pos; @@ -374,7 +390,7 @@ void Entities::renderDebug( } } { - auto view = registry.view(); + auto view = registry->view(); auto ctx = pctx.sub(&batch); ctx.setDepthTest(false); ctx.setDepthMask(false); @@ -400,7 +416,7 @@ void Entities::render( bool pause, entityid_t fpsEntity ) { - auto view = registry.view(); + auto view = registry->view(); for (auto [entity, eid, transform, skeleton] : view.each()) { if (eid.uid == fpsEntity) { continue; @@ -425,7 +441,7 @@ void Entities::render( } bool Entities::hasBlockingInside(AABB aabb) { - auto view = registry.view(); + auto view = registry->view(); for (auto [entity, eid, body] : view.each()) { if (eid.def.blocking && aabb.intersect(body.hitbox.getAABB(), -0.05f)) { return true; @@ -436,7 +452,7 @@ bool Entities::hasBlockingInside(AABB aabb) { std::vector Entities::getAllInside(AABB aabb) { std::vector collected; - auto view = registry.view(); + auto view = registry->view(); for (auto [entity, eid, transform] : view.each()) { if (!eid.destroyFlag && aabb.contains(transform.pos)) { const auto& found = uids.find(entity); @@ -453,7 +469,7 @@ std::vector Entities::getAllInside(AABB aabb) { std::vector Entities::getAllInRadius(glm::vec3 center, float radius) { std::vector collected; - auto view = registry.view(); + auto view = registry->view(); for (auto [entity, transform] : view.each()) { if (glm::distance2(transform.pos, center) <= radius * radius) { const auto& found = uids.find(entity); diff --git a/src/objects/Entities.hpp b/src/objects/Entities.hpp index 03fcec8cb..3b3a7e1a1 100644 --- a/src/objects/Entities.hpp +++ b/src/objects/Entities.hpp @@ -12,7 +12,7 @@ #include "typedefs.hpp" #include "util/Clock.hpp" -#include +#include #include struct EntityDef; @@ -31,14 +31,15 @@ namespace rigging { class SkeletonConfig; } -class Entities { - entt::registry registry; +class Entities final { + std::unique_ptr registry; Level& level; std::unordered_map entities; std::unordered_map uids; entityid_t nextID = 1; util::Clock sensorsTickClock; util::Clock updateTickClock; + Assets* assets = nullptr; void updateSensors( Rigidbody& body, const Transform& tsf, std::vector& sensors @@ -53,6 +54,10 @@ public: Entities(Level& level); + ~Entities(); + + void setAssets(Assets& assets); + void clean(); void updatePhysics(float delta); void update(float delta); diff --git a/src/objects/Entity.cpp b/src/objects/Entity.cpp index 24aa17362..82e67babf 100644 --- a/src/objects/Entity.cpp +++ b/src/objects/Entity.cpp @@ -13,13 +13,16 @@ static inline std::string SAVED_DATA_VARNAME = "SAVED_DATA"; void Entity::setInterpolatedPosition(const glm::vec3& position) { - getSkeleton().interpolation.refresh(position); + if (auto skeleton = getSkeleton()) { + skeleton->interpolation.refresh(position); + } } glm::vec3 Entity::getInterpolatedPosition() const { - const auto& skeleton = getSkeleton(); - if (skeleton.interpolation.isEnabled()) { - return skeleton.interpolation.getCurrent(); + if (auto skeleton = getSkeleton()) { + if (skeleton->interpolation.isEnabled()) { + return skeleton->interpolation.getCurrent(); + } } return getTransform().pos; } @@ -30,8 +33,8 @@ void Entity::destroy() { } } -rigging::Skeleton& Entity::getSkeleton() const { - return registry.get(entity); +rigging::Skeleton* Entity::getSkeleton() const { + return registry.try_get(entity); } void Entity::setRig(const rigging::SkeletonConfig* rigConfig) { @@ -50,8 +53,8 @@ dv::value Entity::serialize() const { const auto& def = eid.def; const auto& transform = getTransform(); const auto& rigidbody = getRigidbody(); - const auto& skeleton = getSkeleton(); const auto& scripts = getScripting(); + auto skeleton = getSkeleton(); auto root = dv::object(); root["def"] = def.name; @@ -61,13 +64,15 @@ dv::value Entity::serialize() const { root[COMP_RIGIDBODY] = rigidbody.serialize(def.save.body.velocity, def.save.body.settings); - if (skeleton.config->getName() != def.skeletonName) { - root["skeleton"] = skeleton.config->getName(); - } - if (def.save.skeleton.pose || def.save.skeleton.textures) { - root[COMP_SKELETON] = skeleton.serialize( - def.save.skeleton.pose, def.save.skeleton.textures - ); + if (skeleton != nullptr) { + if (skeleton->config->getName() != def.skeletonName) { + root["skeleton"] = skeleton->config->getName(); + } + if (def.save.skeleton.pose || def.save.skeleton.textures) { + root[COMP_SKELETON] = skeleton->serialize( + def.save.skeleton.pose, def.save.skeleton.textures + ); + } } if (!scripts.components.empty()) { auto& compsMap = root.object("comps"); diff --git a/src/objects/Entity.hpp b/src/objects/Entity.hpp index 4a306c862..ef00a1bf2 100644 --- a/src/objects/Entity.hpp +++ b/src/objects/Entity.hpp @@ -58,7 +58,7 @@ public: ScriptComponents& getScripting() const; - rigging::Skeleton& getSkeleton() const; + rigging::Skeleton* getSkeleton() const; void setRig(const rigging::SkeletonConfig* rigConfig); From 19e180ddbb07d6963f0c67db5a778f0c47b79a5a Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 16 Jan 2026 02:24:00 +0300 Subject: [PATCH 09/14] feat: loading skeleton from vcm & add 'squash' model asset parameter --- src/assets/AssetsLoader.cpp | 7 ++++++ src/assets/AssetsLoader.hpp | 6 +++++ src/assets/assetload_funcs.cpp | 43 +++++++++++++++++++++++++++++----- 3 files changed, 50 insertions(+), 6 deletions(-) diff --git a/src/assets/AssetsLoader.cpp b/src/assets/AssetsLoader.cpp index 9729eedb8..cec4e0887 100644 --- a/src/assets/AssetsLoader.cpp +++ b/src/assets/AssetsLoader.cpp @@ -181,6 +181,12 @@ void AssetsLoader::processPreload( config = std::make_shared(advanced); break; } + case AssetType::MODEL: { + bool squashed = false; + map.at("squash").get(squashed); + config = std::make_shared(squashed); + break; + } default: break; } @@ -214,6 +220,7 @@ void AssetsLoader::processPreloadConfig(const io::path& file) { processPreloadList(AssetType::SOUND, root["sounds"]); processPreloadList(AssetType::MODEL, root["models"]); processPreloadList(AssetType::POST_EFFECT, root["post-effects"]); + processPreloadList(AssetType::SKELETON, root["skeletons"]); // layouts are loaded automatically } diff --git a/src/assets/AssetsLoader.hpp b/src/assets/AssetsLoader.hpp index e35a78ce2..78152b6ac 100644 --- a/src/assets/AssetsLoader.hpp +++ b/src/assets/AssetsLoader.hpp @@ -59,6 +59,12 @@ struct PostEffectCfg : AssetCfg { PostEffectCfg(bool advanced) : advanced(advanced) {} }; +struct ModelCfg : AssetCfg { + bool squashed; + + ModelCfg(bool squashed) : squashed(squashed) {} +}; + using aloader_func = std::function< assetload:: postfunc(AssetsLoader*, const ResPaths&, const std::string&, const std::string&, std::shared_ptr)>; diff --git a/src/assets/assetload_funcs.cpp b/src/assets/assetload_funcs.cpp index f739aee7c..4110278c0 100644 --- a/src/assets/assetload_funcs.cpp +++ b/src/assets/assetload_funcs.cpp @@ -325,8 +325,10 @@ assetload::postfunc assetload::model( const ResPaths& paths, const std::string& file, const std::string& name, - const std::shared_ptr& + const std::shared_ptr& config ) { + auto cfg = std::dynamic_pointer_cast(config); + auto path = paths.find(file + ".vec3"); if (io::exists(path)) { auto bytes = io::read_bytes_buffer(path); @@ -382,11 +384,40 @@ assetload::postfunc assetload::model( auto text = io::read_string(path); try { auto vcmModel = vcm::parse(path.string(), text, path.extension() == ".xml"); - auto model = std::make_unique(std::move(vcmModel.squash())).release(); - return [=](Assets* assets) { - request_textures(loader, *model); - assets->store(std::unique_ptr(model), name); - }; + + assert(vcmModel.parts.size() > 0); + + if (vcmModel.parts.size() == 1 || (cfg && cfg->squashed)) { + auto modelPtr = std::make_unique(std::move(vcmModel.squash())).release(); + return [=](Assets* assets) { + auto model = std::unique_ptr(modelPtr); + request_textures(loader, *model); + assets->store(std::move(model), name); + logger.info() << "store model " << util::quote(name); + }; + } else { + auto vcmModelPtr = std::make_unique(std::move(vcmModel)).release(); + return [=](Assets* assets) { + auto vcmModel = std::unique_ptr(vcmModelPtr); + for (auto& [partName, model] : vcmModel->parts) { + auto fullName = name + "." + partName; + logger.info() + << "store model part " << util::quote(partName) + << " as " << util::quote(fullName); + assets->store( + std::make_unique(std::move(model)), + fullName + ); + } + logger.info() << "store skeleton " << util::quote(name); + assets->store( + std::make_unique( + std::move(*vcmModel->skeleton) + ), + name + ); + }; + } } catch (const parsing_error& err) { std::cerr << err.errorLog() << std::endl; throw; From 7f2d9b887da8bced0a4cb8901c2350334341af6e Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 16 Jan 2026 02:25:34 +0300 Subject: [PATCH 10/14] feat: 'squash' support for vec3 models --- src/assets/assetload_funcs.cpp | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/assets/assetload_funcs.cpp b/src/assets/assetload_funcs.cpp index 4110278c0..dc489755e 100644 --- a/src/assets/assetload_funcs.cpp +++ b/src/assets/assetload_funcs.cpp @@ -333,7 +333,23 @@ assetload::postfunc assetload::model( if (io::exists(path)) { auto bytes = io::read_bytes_buffer(path); auto modelVEC3 = std::make_shared(vec3::load(path.string(), bytes)); - return [loader, name, modelVEC3=std::move(modelVEC3)](Assets* assets) { + return [loader, name, cfg, modelVEC3=std::move(modelVEC3)](Assets* assets) { + if (cfg && cfg->squashed) { + model::Model fullModel; + for (auto& entry : modelVEC3->models) { + auto& vec3model = entry.second; + auto& model = vec3model.model; + model.translate(vec3model.origin); + fullModel.merge(std::move(model)); + } + request_textures(loader, fullModel); + assets->store( + std::make_unique(fullModel), + name + ); + logger.info() << "store model " << util::quote(name); + return; + } for (auto& [modelName, model] : modelVEC3->models) { request_textures(loader, model.model); std::string fullName = name; From d07c8e523429393a26dbccaaf7fc7275079fa577 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 18 Jan 2026 17:57:21 +0300 Subject: [PATCH 11/14] fix skeleton name serialization --- src/objects/Entities.cpp | 2 +- src/objects/Entity.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/objects/Entities.cpp b/src/objects/Entities.cpp index 522ba6a0a..179a39fd6 100644 --- a/src/objects/Entities.cpp +++ b/src/objects/Entities.cpp @@ -151,7 +151,7 @@ void Entities::loadEntity(const dv::value& map, Entity entity) { return; } std::string skeletonName = skeleton->config->getName(); - map.at("skeleton").get(skeletonName); + map.at("skeleton-name").get(skeletonName); if (skeletonName != skeleton->config->getName()) { skeleton->config = assets->get(skeletonName); } diff --git a/src/objects/Entity.cpp b/src/objects/Entity.cpp index 82e67babf..5fbfe1d99 100644 --- a/src/objects/Entity.cpp +++ b/src/objects/Entity.cpp @@ -66,7 +66,7 @@ dv::value Entity::serialize() const { if (skeleton != nullptr) { if (skeleton->config->getName() != def.skeletonName) { - root["skeleton"] = skeleton->config->getName(); + root["skeleton-name"] = skeleton->config->getName(); } if (def.save.skeleton.pose || def.save.skeleton.textures) { root[COMP_SKELETON] = skeleton->serialize( From 059f56757759952af2b1adc90abc41f556980618 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 18 Jan 2026 18:06:55 +0300 Subject: [PATCH 12/14] fix vcm skeleton models --- src/assets/assetload_funcs.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/assets/assetload_funcs.cpp b/src/assets/assetload_funcs.cpp index dc489755e..4bd32d5d0 100644 --- a/src/assets/assetload_funcs.cpp +++ b/src/assets/assetload_funcs.cpp @@ -425,6 +425,9 @@ assetload::postfunc assetload::model( fullName ); } + for (auto& bone : vcmModel->skeleton->getBones()) { + bone->setModel(name + "." + bone->model.name); + } logger.info() << "store skeleton " << util::quote(name); assets->store( std::make_unique( From e0e9e1914b069efba4018d6b6b5591ae15f4d127 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 18 Jan 2026 21:43:34 +0300 Subject: [PATCH 13/14] feat: auto-load vcm skeletons --- src/assets/AssetsLoader.cpp | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/assets/AssetsLoader.cpp b/src/assets/AssetsLoader.cpp index cec4e0887..cf64a9c8e 100644 --- a/src/assets/AssetsLoader.cpp +++ b/src/assets/AssetsLoader.cpp @@ -1,26 +1,26 @@ #include "AssetsLoader.hpp" -#include -#include -#include - -#include "coders/imageio.hpp" +#include "assetload_funcs.hpp" +#include "Assets.hpp" #include "coders/commons.hpp" +#include "coders/imageio.hpp" #include "constants.hpp" #include "content/Content.hpp" #include "content/ContentPack.hpp" #include "debug/Logger.hpp" +#include "engine/Engine.hpp" #include "engine/EnginePaths.hpp" -#include "io/io.hpp" #include "graphics/core/Texture.hpp" +#include "io/io.hpp" +#include "items/ItemDef.hpp" #include "logic/scripting/scripting.hpp" +#include "objects/EntityDef.hpp" #include "objects/rigging.hpp" #include "util/ThreadPool.hpp" #include "voxels/Block.hpp" -#include "items/ItemDef.hpp" -#include "Assets.hpp" -#include "assetload_funcs.hpp" -#include "engine/Engine.hpp" + +#include +#include namespace fs = std::filesystem; @@ -305,6 +305,16 @@ void AssetsLoader::addDefaults(AssetsLoader& loader, const Content* content) { ); } } + for (const auto& [_, def] : content->entities.getDefs()) { + if (def->skeletonName.find(':') == std::string::npos) { + // expecting a VCM with skeleton + loader.add( + AssetType::MODEL, + MODELS_FOLDER + "/" + def->skeletonName, + def->skeletonName + ); + } + } } } From 53e4a0a103a9988e61dee3473a214d4555858d25 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 18 Jan 2026 22:14:52 +0300 Subject: [PATCH 14/14] update doc/ru/vcm.md --- doc/ru/vcm.md | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/doc/ru/vcm.md b/doc/ru/vcm.md index 8943a19a0..a1b72ca48 100644 --- a/doc/ru/vcm.md +++ b/doc/ru/vcm.md @@ -1,4 +1,4 @@ -## VCM +# VCM Текстовый формат описания 3D моделей, являющийся заменой устаревшего свойства `model-primitives`. @@ -27,6 +27,8 @@ > При размерах более 1.0 результат не определён и может измениться в последующих обновлениях. > Используйте `region` или `region-scale` для ручной настройки. +## Примитивы + ### Свойства `tri` Примитив описывает треугольник. @@ -75,3 +77,25 @@ - `texture` - отображаемая текстура. По-умолчанию: `$индекс_стороны`. - `region` - UV регион в границах выбранной текстуры, определяется позициями противоположных углов. Пример: `region (0,0,1,1)` - `region-scale` вектор-множитель для автоматически выбранного UV региона. Пример: `region-scale (0.5,1)` + +## Скелет + +VCM позволяет описывать [скелет](rigging.md) модели. Для этого используется элемент `@bone` (кость). + +Существует два вида костей: +- безымянные - используются только для трансформаций на влюженные примитивы, не попадает в итоговый скелет. +- именованные - будет включено в итоговый скелет. Имя указывается через атрибут `name`. + +Главная кость (`root`) создаётся автоматически для всей модели. +При наличии явно указанных костей модель будет разделена на несколько частей именованных в формате `имя_модели.имя_кости`. +Так как модель относится к ассетам и не имеет префикса, загруженный скелет получает имя модели, так же, без префикса. +Загруженный скелет можно использовать у [сущности](entity-properties.md#имя-скелета---skeleton-name) через свойство `skeleton-name`. + +В списке `пак/preload.json` при указании параметра `"squash": true` vcm будет загружен как единая модель, без скелета. + +### Свойства `bone` + +- `name` - имя кости, при отсутствии которого кость будет "растворена" и не попадёт в итоговый скелет, при этом, трансформации будут применены. +- `move` - смещение вложенных элементов. +- `scale` - масштабирование вложенных элементов. Применяется перед смещением. +- `rotate` - вращение вложенных элементов вокруг осей (x,y,z) в градусах, или кватернион (x,y,z,w). Пример: `rotate (45,0,0)` или `rotate (0.3826834, 0, 0, 0.9238795)`