|
1 | 1 | #include "scene/scene_serialization.hpp" |
2 | 2 |
|
3 | 3 | #include "app_context.hpp" |
| 4 | +#include "app_settings.hpp" |
| 5 | +#include "brushes/brush.hpp" |
4 | 6 | #include "content_library/content_library.hpp" |
5 | 7 | #include "geometry_graph/geometry_graph_mesh.hpp" |
6 | 8 | #include "geometry_graph/graph_mesh.hpp" |
@@ -890,6 +892,51 @@ auto save_scene( |
890 | 892 | } |
891 | 893 | } |
892 | 894 |
|
| 895 | + // Serialize brush assets in the content library (#247). A brush is an |
| 896 | + // editor-only concept (not expressible in glTF), so it is persisted here: |
| 897 | + // each brush's geometry is written to a companion .geogram file (like |
| 898 | + // geometry-normative meshes) and the brush metadata (name, referenced |
| 899 | + // material name, density, normal style) goes into scene.json. The |
| 900 | + // collision shape is intentionally not serialized - Brush::late_initialize |
| 901 | + // rebuilds a convex hull from the geometry at load, matching how brushes |
| 902 | + // are created at runtime. |
| 903 | + { |
| 904 | + const std::shared_ptr<Content_library> content_library = scene_root.get_content_library(); |
| 905 | + if (content_library && content_library->brushes) { |
| 906 | + GEO::MeshIOFlags brush_ioflags; |
| 907 | + brush_ioflags.set_dimension(3); |
| 908 | + brush_ioflags.set_attributes(GEO::MeshAttributesFlags::MESH_ALL_ATTRIBUTES); |
| 909 | + brush_ioflags.set_elements(GEO::MeshElementsFlags::MESH_ALL_ELEMENTS); |
| 910 | + int brush_index = 0; |
| 911 | + for (const std::shared_ptr<Brush>& brush : content_library->brushes->get_all<Brush>()) { |
| 912 | + const std::shared_ptr<erhe::geometry::Geometry> geometry = brush->get_geometry(); |
| 913 | + if (!geometry) { |
| 914 | + ++brush_index; |
| 915 | + continue; |
| 916 | + } |
| 917 | + const std::string geometry_base = fmt::format("brush_{}", brush_index); |
| 918 | + const std::string filename = fmt::format("{}.geogram", geometry_base); |
| 919 | + const std::filesystem::path full_path = scene_dir / filename; |
| 920 | + GEO::OutputGeoFile geofile{full_path.string(), 3}; |
| 921 | + if (!GEO::mesh_save(geometry->get_mesh(), geofile, brush_ioflags)) { |
| 922 | + log_parsers->error("save_scene: failed to save brush geometry: {}", full_path.string()); |
| 923 | + } |
| 924 | + |
| 925 | + Brush_data_serial brush_data; |
| 926 | + brush_data.name = brush->get_name(); |
| 927 | + brush_data.geometry_path = geometry_base; |
| 928 | + brush_data.density = brush->get_density(); |
| 929 | + brush_data.normal_style = static_cast<int>(brush->get_normal_style()); |
| 930 | + const std::shared_ptr<erhe::primitive::Material>& material = brush->get_material(); |
| 931 | + if (material) { |
| 932 | + brush_data.material_name = material->get_name(); |
| 933 | + } |
| 934 | + scene_file.brushes.push_back(std::move(brush_data)); |
| 935 | + ++brush_index; |
| 936 | + } |
| 937 | + } |
| 938 | + } |
| 939 | + |
893 | 940 | // Serialize layouts and layout items |
894 | 941 | for (const auto& node : flat_nodes) { |
895 | 942 | const auto layout = erhe::scene::get_attachment<erhe::scene::Layout>(node.get()); |
@@ -1716,6 +1763,86 @@ auto load_scene( |
1716 | 1763 | } |
1717 | 1764 | } |
1718 | 1765 |
|
| 1766 | + // Reconstruct brush assets into the content library (#247). Each brush's |
| 1767 | + // geometry is loaded from its companion .geogram file, its referenced |
| 1768 | + // material (if any) is resolved by name, and the collision shape is rebuilt |
| 1769 | + // from the geometry the first time the brush is instantiated |
| 1770 | + // (Brush::late_initialize). Needs App_context for build_info / app_settings; |
| 1771 | + // skipped on pure data loads (context == nullptr). |
| 1772 | + if ((context != nullptr) && content_library && content_library->brushes && !scene_file.brushes.empty()) { |
| 1773 | + const erhe::primitive::Build_info brush_build_info{ |
| 1774 | + .primitive_types = { |
| 1775 | + .fill_triangles = true, |
| 1776 | + .fill_triangles_expanded = true, |
| 1777 | + .edge_lines = true, |
| 1778 | + .corner_points = true, |
| 1779 | + .centroid_points = true, |
| 1780 | + }, |
| 1781 | + .buffer_info = context->mesh_memory->make_primitive_buffer_info() |
| 1782 | + }; |
| 1783 | + constexpr uint64_t brush_process_flags = |
| 1784 | + erhe::geometry::Geometry::process_flag_connect | |
| 1785 | + erhe::geometry::Geometry::process_flag_build_edges | |
| 1786 | + erhe::geometry::Geometry::process_flag_compute_facet_centroids | |
| 1787 | + erhe::geometry::Geometry::process_flag_compute_smooth_vertex_normals | |
| 1788 | + erhe::geometry::Geometry::process_flag_generate_facet_texture_coordinates; |
| 1789 | + |
| 1790 | + for (const Brush_data_serial& brush_data : scene_file.brushes) { |
| 1791 | + const std::string filename = fmt::format("{}.geogram", brush_data.geometry_path); |
| 1792 | + const std::filesystem::path geom_path = scene_dir / filename; |
| 1793 | + if (!std::filesystem::exists(geom_path)) { |
| 1794 | + log_parsers->warn("load_scene: brush geometry file not found: {}", geom_path.string()); |
| 1795 | + continue; |
| 1796 | + } |
| 1797 | + |
| 1798 | + auto geometry = std::make_shared<erhe::geometry::Geometry>(brush_data.name); |
| 1799 | + // Unbind erhe Mesh_attributes before loading, because mesh_load will |
| 1800 | + // try to bind attributes with the same names and assert. |
| 1801 | + geometry->get_attributes().unbind(); |
| 1802 | + GEO::Mesh& geo_mesh = geometry->get_mesh(); |
| 1803 | + geo_mesh.clear(false, false); |
| 1804 | + GEO::MeshIOFlags ioflags; |
| 1805 | + ioflags.set_dimension(3); |
| 1806 | + ioflags.set_attributes(GEO::MeshAttributesFlags::MESH_ALL_ATTRIBUTES); |
| 1807 | + ioflags.set_elements(GEO::MeshElementsFlags::MESH_ALL_ELEMENTS); |
| 1808 | + if (!GEO::mesh_load(geom_path.string().c_str(), geo_mesh, ioflags)) { |
| 1809 | + log_parsers->error("load_scene: failed to load brush geometry: {}", geom_path.string()); |
| 1810 | + continue; |
| 1811 | + } |
| 1812 | + geometry->get_attributes().bind(); |
| 1813 | + if (geo_mesh.vertices.nb() == 0 || geo_mesh.facets.nb() == 0) { |
| 1814 | + log_parsers->warn("load_scene: empty brush geometry: {}", geom_path.string()); |
| 1815 | + continue; |
| 1816 | + } |
| 1817 | + geo_mesh.vertices.set_single_precision(); |
| 1818 | + geometry->process({.flags = brush_process_flags}); |
| 1819 | + |
| 1820 | + // Resolve the referenced material by name from the content library. |
| 1821 | + std::shared_ptr<erhe::primitive::Material> material{}; |
| 1822 | + if (!brush_data.material_name.empty() && content_library->materials) { |
| 1823 | + for (const std::shared_ptr<erhe::primitive::Material>& candidate : content_library->materials->get_all<erhe::primitive::Material>()) { |
| 1824 | + if (candidate->get_name() == brush_data.material_name) { |
| 1825 | + material = candidate; |
| 1826 | + break; |
| 1827 | + } |
| 1828 | + } |
| 1829 | + } |
| 1830 | + |
| 1831 | + Brush_data create_info{ |
| 1832 | + .context = *context, |
| 1833 | + .app_settings = *context->app_settings, |
| 1834 | + .build_info = brush_build_info, |
| 1835 | + .normal_style = static_cast<erhe::primitive::Normal_style>(brush_data.normal_style), |
| 1836 | + .geometry = geometry, |
| 1837 | + .density = brush_data.density, |
| 1838 | + }; |
| 1839 | + const std::shared_ptr<Brush> brush = content_library->brushes->make<Brush>(create_info); |
| 1840 | + if (material) { |
| 1841 | + brush->set_material(material); |
| 1842 | + } |
| 1843 | + } |
| 1844 | + } |
| 1845 | + |
1719 | 1846 | return scene_root; |
1720 | 1847 | } |
1721 | 1848 |
|
|
0 commit comments