|
|
@@ -20,9 +20,9 @@
|
|
|
#include "core/strings/string_id.inl"
|
|
|
#include "device/log.h"
|
|
|
#include "resource/compile_options.inl"
|
|
|
+#include "resource/mesh.h"
|
|
|
#include "resource/mesh_resource.h"
|
|
|
#include "resource/resource_manager.h"
|
|
|
-#include <bx/readerwriter.h>
|
|
|
#include <bx/error.h>
|
|
|
#include <vertexlayout.h> // bgfx::write, bgfx::read
|
|
|
|
|
|
@@ -52,31 +52,6 @@ struct BgfxReader : public bx::ReaderI
|
|
|
}
|
|
|
};
|
|
|
|
|
|
-/// Writer interface.
|
|
|
-struct BgfxWriter : public bx::WriterI
|
|
|
-{
|
|
|
- BinaryWriter *_bw;
|
|
|
-
|
|
|
- ///
|
|
|
- explicit BgfxWriter(BinaryWriter &bw)
|
|
|
- : _bw(&bw)
|
|
|
- {
|
|
|
- }
|
|
|
-
|
|
|
- ///
|
|
|
- virtual ~BgfxWriter()
|
|
|
- {
|
|
|
- }
|
|
|
-
|
|
|
- ///
|
|
|
- virtual int32_t write(const void *_data, int32_t _size, bx::Error *_err)
|
|
|
- {
|
|
|
- CE_UNUSED(_err);
|
|
|
- _bw->write(_data, _size);
|
|
|
- return _size; // FIXME: return the actual number of bytes written
|
|
|
- }
|
|
|
-};
|
|
|
-
|
|
|
MeshResource::MeshResource(Allocator &a)
|
|
|
: nodes(a)
|
|
|
, geometries(a)
|
|
|
@@ -208,7 +183,7 @@ namespace mesh_resource_internal
|
|
|
} // namespace mesh_resource_internal
|
|
|
|
|
|
#if CROWN_CAN_COMPILE
|
|
|
-namespace mesh_resource_internal
|
|
|
+namespace mesh
|
|
|
{
|
|
|
static void parse_float_array(Array<f32> &output, const char *json)
|
|
|
{
|
|
|
@@ -232,369 +207,162 @@ namespace mesh_resource_internal
|
|
|
output[i] = (u16)sjson::parse_int(indices[i]);
|
|
|
}
|
|
|
|
|
|
- namespace mesh
|
|
|
- {
|
|
|
- s32 parse_nodes(Mesh &m, const char *sjson, CompileOptions &opts);
|
|
|
-
|
|
|
- s32 parse_node(Node &n, const char *sjson, Mesh *mesh, CompileOptions &opts)
|
|
|
- {
|
|
|
- TempAllocator4096 ta;
|
|
|
- JsonObject obj(ta);
|
|
|
- sjson::parse(obj, sjson);
|
|
|
-
|
|
|
- n._local_pose = sjson::parse_matrix4x4(obj["matrix_local"]);
|
|
|
-
|
|
|
- if (json_object::has(obj, "children")) {
|
|
|
- s32 err = mesh::parse_nodes(*mesh, obj["children"], opts);
|
|
|
- DATA_COMPILER_ENSURE(err == 0, opts);
|
|
|
- }
|
|
|
-
|
|
|
- if (json_object::has(obj, "geometry"))
|
|
|
- sjson::parse_string(n._geometry, obj["geometry"]);
|
|
|
-
|
|
|
- return 0;
|
|
|
- }
|
|
|
-
|
|
|
- void reset(Geometry &g)
|
|
|
- {
|
|
|
- array::clear(g._positions);
|
|
|
- array::clear(g._normals);
|
|
|
- array::clear(g._uvs);
|
|
|
- array::clear(g._tangents);
|
|
|
- array::clear(g._binormals);
|
|
|
-
|
|
|
- array::clear(g._position_indices);
|
|
|
- array::clear(g._normal_indices);
|
|
|
- array::clear(g._uv_indices);
|
|
|
- array::clear(g._tangent_indices);
|
|
|
- array::clear(g._binormal_indices);
|
|
|
-
|
|
|
- array::clear(g._vertex_buffer);
|
|
|
- array::clear(g._index_buffer);
|
|
|
- }
|
|
|
+ s32 parse_nodes(Mesh &m, const char *sjson, CompileOptions &opts);
|
|
|
|
|
|
- bool has_normals(Geometry &g)
|
|
|
- {
|
|
|
- return array::size(g._normals) != 0;
|
|
|
- }
|
|
|
+ s32 parse_node(Node &n, const char *sjson, Mesh *mesh, CompileOptions &opts)
|
|
|
+ {
|
|
|
+ TempAllocator4096 ta;
|
|
|
+ JsonObject obj(ta);
|
|
|
+ sjson::parse(obj, sjson);
|
|
|
|
|
|
- bool has_uvs(Geometry &g)
|
|
|
- {
|
|
|
- return array::size(g._uvs) != 0;
|
|
|
- }
|
|
|
+ n._local_pose = sjson::parse_matrix4x4(obj["matrix_local"]);
|
|
|
|
|
|
- u32 vertex_stride(Geometry &g)
|
|
|
- {
|
|
|
- u32 stride = 0;
|
|
|
- stride += 3 * sizeof(f32);
|
|
|
- stride += (has_normals(g) ? 3 * sizeof(f32) : 0);
|
|
|
- stride += (has_uvs(g) ? 2 * sizeof(f32) : 0);
|
|
|
- return stride;
|
|
|
+ if (json_object::has(obj, "children")) {
|
|
|
+ s32 err = mesh::parse_nodes(*mesh, obj["children"], opts);
|
|
|
+ DATA_COMPILER_ENSURE(err == 0, opts);
|
|
|
}
|
|
|
|
|
|
- bgfx::VertexLayout vertex_layout(Geometry &g)
|
|
|
- {
|
|
|
- bgfx::VertexLayout layout;
|
|
|
- memset((void *)&layout, 0, sizeof(layout));
|
|
|
-
|
|
|
- layout.begin();
|
|
|
- layout.add(bgfx::Attrib::Position, 3, bgfx::AttribType::Float);
|
|
|
-
|
|
|
- if (has_normals(g)) {
|
|
|
- layout.add(bgfx::Attrib::Normal, 3, bgfx::AttribType::Float, true);
|
|
|
- }
|
|
|
- if (has_uvs(g)) {
|
|
|
- layout.add(bgfx::Attrib::TexCoord0, 2, bgfx::AttribType::Float);
|
|
|
- }
|
|
|
+ if (json_object::has(obj, "geometry"))
|
|
|
+ sjson::parse_string(n._geometry, obj["geometry"]);
|
|
|
|
|
|
- layout.end();
|
|
|
- return layout;
|
|
|
- }
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
|
|
|
- void generate_vertex_and_index_buffers(Geometry &g)
|
|
|
- {
|
|
|
- array::resize(g._index_buffer, array::size(g._position_indices));
|
|
|
-
|
|
|
- u16 index = 0;
|
|
|
- for (u32 i = 0; i < array::size(g._position_indices); ++i) {
|
|
|
- g._index_buffer[i] = index++;
|
|
|
-
|
|
|
- const u16 p_idx = g._position_indices[i] * 3;
|
|
|
- Vector3 xyz;
|
|
|
- xyz.x = g._positions[p_idx + 0];
|
|
|
- xyz.y = g._positions[p_idx + 1];
|
|
|
- xyz.z = g._positions[p_idx + 2];
|
|
|
- array::push(g._vertex_buffer, (char *)&xyz, sizeof(xyz));
|
|
|
-
|
|
|
- if (has_normals(g)) {
|
|
|
- const u16 n_idx = g._normal_indices[i] * 3;
|
|
|
- Vector3 n;
|
|
|
- n.x = g._normals[n_idx + 0];
|
|
|
- n.y = g._normals[n_idx + 1];
|
|
|
- n.z = g._normals[n_idx + 2];
|
|
|
- array::push(g._vertex_buffer, (char *)&n, sizeof(n));
|
|
|
- }
|
|
|
- if (has_uvs(g)) {
|
|
|
- const u16 t_idx = g._uv_indices[i] * 2;
|
|
|
- Vector2 uv;
|
|
|
- uv.x = g._uvs[t_idx + 0];
|
|
|
- uv.y = g._uvs[t_idx + 1];
|
|
|
- array::push(g._vertex_buffer, (char *)&uv, sizeof(uv));
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
+ s32 parse_indices(Geometry &g, const char *json)
|
|
|
+ {
|
|
|
+ TempAllocator4096 ta;
|
|
|
+ JsonObject obj(ta);
|
|
|
+ sjson::parse(obj, json);
|
|
|
|
|
|
- OBB obb(Geometry &g)
|
|
|
- {
|
|
|
- AABB aabb;
|
|
|
- OBB obb;
|
|
|
- aabb::reset(aabb);
|
|
|
- memset(&obb, 0, sizeof(obb));
|
|
|
+ JsonArray data_json(ta);
|
|
|
+ sjson::parse_array(data_json, obj["data"]);
|
|
|
|
|
|
- aabb::from_points(aabb
|
|
|
- , array::size(g._positions) / 3
|
|
|
- , sizeof(g._positions[0]) * 3
|
|
|
- , array::begin(g._positions)
|
|
|
- );
|
|
|
+ parse_index_array(g._position_indices, data_json[0]);
|
|
|
|
|
|
- obb.tm = from_quaternion_translation(QUATERNION_IDENTITY, aabb::center(aabb));
|
|
|
- obb.half_extents = (aabb.max - aabb.min) * 0.5f;
|
|
|
- return obb;
|
|
|
+ if (has_normals(g)) {
|
|
|
+ parse_index_array(g._normal_indices, data_json[1]);
|
|
|
}
|
|
|
-
|
|
|
- s32 parse_indices(Geometry &g, const char *json)
|
|
|
- {
|
|
|
- TempAllocator4096 ta;
|
|
|
- JsonObject obj(ta);
|
|
|
- sjson::parse(obj, json);
|
|
|
-
|
|
|
- JsonArray data_json(ta);
|
|
|
- sjson::parse_array(data_json, obj["data"]);
|
|
|
-
|
|
|
- parse_index_array(g._position_indices, data_json[0]);
|
|
|
-
|
|
|
- if (has_normals(g)) {
|
|
|
- parse_index_array(g._normal_indices, data_json[1]);
|
|
|
- }
|
|
|
- if (has_uvs(g)) {
|
|
|
- parse_index_array(g._uv_indices, data_json[2]);
|
|
|
- }
|
|
|
-
|
|
|
- return 0;
|
|
|
+ if (has_uvs(g)) {
|
|
|
+ parse_index_array(g._uv_indices, data_json[2]);
|
|
|
}
|
|
|
|
|
|
- s32 parse_geometry(Geometry &g, const char *sjson)
|
|
|
- {
|
|
|
- TempAllocator4096 ta;
|
|
|
- JsonObject obj(ta);
|
|
|
- sjson::parse(obj, sjson);
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
|
|
|
- parse_float_array(g._positions, obj["position"]);
|
|
|
+ s32 parse_geometry(Geometry &g, const char *sjson)
|
|
|
+ {
|
|
|
+ TempAllocator4096 ta;
|
|
|
+ JsonObject obj(ta);
|
|
|
+ sjson::parse(obj, sjson);
|
|
|
|
|
|
- if (json_object::has(obj, "normal")) {
|
|
|
- parse_float_array(g._normals, obj["normal"]);
|
|
|
- }
|
|
|
- if (json_object::has(obj, "texcoord")) {
|
|
|
- parse_float_array(g._uvs, obj["texcoord"]);
|
|
|
- }
|
|
|
+ parse_float_array(g._positions, obj["position"]);
|
|
|
|
|
|
- return parse_indices(g, obj["indices"]);
|
|
|
+ if (json_object::has(obj, "normal")) {
|
|
|
+ parse_float_array(g._normals, obj["normal"]);
|
|
|
}
|
|
|
-
|
|
|
- s32 parse_geometries(Mesh &m, const char *sjson, CompileOptions &opts)
|
|
|
- {
|
|
|
- TempAllocator4096 ta;
|
|
|
- JsonObject geometries(ta);
|
|
|
- sjson::parse(geometries, sjson);
|
|
|
-
|
|
|
- auto cur = json_object::begin(geometries);
|
|
|
- auto end = json_object::end(geometries);
|
|
|
- for (; cur != end; ++cur) {
|
|
|
- JSON_OBJECT_SKIP_HOLE(geometries, cur);
|
|
|
-
|
|
|
- Geometry geo(default_allocator());
|
|
|
- s32 err = mesh::parse_geometry(geo, cur->second);
|
|
|
- DATA_COMPILER_ENSURE(err == 0, opts);
|
|
|
-
|
|
|
- DynamicString geometry_name(ta);
|
|
|
- geometry_name = cur->first;
|
|
|
- DATA_COMPILER_ASSERT(!hash_map::has(m._geometries, geometry_name)
|
|
|
- , opts
|
|
|
- , "Geometry redefined: '%s'"
|
|
|
- , geometry_name.c_str()
|
|
|
- );
|
|
|
- hash_map::set(m._geometries, geometry_name, geo);
|
|
|
- }
|
|
|
-
|
|
|
- return 0;
|
|
|
+ if (json_object::has(obj, "texcoord")) {
|
|
|
+ parse_float_array(g._uvs, obj["texcoord"]);
|
|
|
}
|
|
|
|
|
|
- s32 parse_nodes(Mesh &m, const char *sjson, CompileOptions &opts)
|
|
|
- {
|
|
|
- TempAllocator4096 ta;
|
|
|
- JsonObject nodes(ta);
|
|
|
- sjson::parse(nodes, sjson);
|
|
|
-
|
|
|
- auto cur = json_object::begin(nodes);
|
|
|
- auto end = json_object::end(nodes);
|
|
|
- for (; cur != end; ++cur) {
|
|
|
- JSON_OBJECT_SKIP_HOLE(nodes, cur);
|
|
|
-
|
|
|
- Node node(default_allocator());
|
|
|
- s32 err = mesh::parse_node(node, cur->second, &m, opts);
|
|
|
- DATA_COMPILER_ENSURE(err == 0, opts);
|
|
|
-
|
|
|
- DynamicString node_name(ta);
|
|
|
- node_name = cur->first;
|
|
|
- DATA_COMPILER_ASSERT(!hash_map::has(m._nodes, node_name)
|
|
|
- , opts
|
|
|
- , "Node redefined: '%s'"
|
|
|
- , node_name.c_str()
|
|
|
- );
|
|
|
-
|
|
|
- // For backwards compatibility: originally .mesh resources
|
|
|
- // enforced (implicitly) a 1:1 relationship between nodes
|
|
|
- // and geometries.
|
|
|
- if (node._geometry == "")
|
|
|
- node._geometry = node_name;
|
|
|
-
|
|
|
- DATA_COMPILER_ASSERT(hash_map::has(m._geometries, node._geometry)
|
|
|
- , opts
|
|
|
- , "Node '%s' references unexisting geometry '%s'"
|
|
|
- , node_name.c_str()
|
|
|
- , node._geometry.c_str()
|
|
|
- );
|
|
|
- hash_map::set(m._nodes, node_name, node);
|
|
|
- }
|
|
|
+ return parse_indices(g, obj["indices"]);
|
|
|
+ }
|
|
|
|
|
|
- return 0;
|
|
|
- }
|
|
|
+ s32 parse_geometries(Mesh &m, const char *sjson, CompileOptions &opts)
|
|
|
+ {
|
|
|
+ TempAllocator4096 ta;
|
|
|
+ JsonObject geometries(ta);
|
|
|
+ sjson::parse(geometries, sjson);
|
|
|
|
|
|
- s32 parse(Mesh &m, Buffer &buf, CompileOptions &opts)
|
|
|
- {
|
|
|
- TempAllocator4096 ta;
|
|
|
- JsonObject nodes(ta);
|
|
|
- JsonObject obj(ta);
|
|
|
- sjson::parse(obj, buf);
|
|
|
+ auto cur = json_object::begin(geometries);
|
|
|
+ auto end = json_object::end(geometries);
|
|
|
+ for (; cur != end; ++cur) {
|
|
|
+ JSON_OBJECT_SKIP_HOLE(geometries, cur);
|
|
|
|
|
|
- s32 err = mesh::parse_geometries(m, obj["geometries"], opts);
|
|
|
+ Geometry geo(default_allocator());
|
|
|
+ s32 err = mesh::parse_geometry(geo, cur->second);
|
|
|
DATA_COMPILER_ENSURE(err == 0, opts);
|
|
|
|
|
|
- return mesh::parse_nodes(m, obj["nodes"], opts);
|
|
|
- }
|
|
|
-
|
|
|
- s32 parse(Mesh &m, CompileOptions &opts)
|
|
|
- {
|
|
|
- return mesh::parse(m, opts, opts._source_path.c_str());
|
|
|
- }
|
|
|
-
|
|
|
- void geometry_names(Vector<DynamicString> &names, const Mesh &m, const DynamicString &geometry)
|
|
|
- {
|
|
|
- auto cur = hash_map::begin(m._nodes);
|
|
|
- auto end = hash_map::end(m._nodes);
|
|
|
- for (; cur != end; ++cur) {
|
|
|
- HASH_MAP_SKIP_HOLE(m._nodes, cur);
|
|
|
-
|
|
|
- if (cur->second._geometry == geometry)
|
|
|
- vector::push_back(names, cur->first);
|
|
|
- }
|
|
|
+ DynamicString geometry_name(ta);
|
|
|
+ geometry_name = cur->first;
|
|
|
+ DATA_COMPILER_ASSERT(!hash_map::has(m._geometries, geometry_name)
|
|
|
+ , opts
|
|
|
+ , "Geometry redefined: '%s'"
|
|
|
+ , geometry_name.c_str()
|
|
|
+ );
|
|
|
+ hash_map::set(m._geometries, geometry_name, geo);
|
|
|
}
|
|
|
|
|
|
- s32 write(Mesh &m, CompileOptions &opts)
|
|
|
- {
|
|
|
- opts.write(RESOURCE_HEADER(RESOURCE_VERSION_MESH));
|
|
|
- opts.write(hash_map::size(m._geometries));
|
|
|
-
|
|
|
- auto cur = hash_map::begin(m._geometries);
|
|
|
- auto end = hash_map::end(m._geometries);
|
|
|
- for (; cur != end; ++cur) {
|
|
|
- HASH_MAP_SKIP_HOLE(m._geometries, cur);
|
|
|
-
|
|
|
- Vector<DynamicString> geo_names(default_allocator());
|
|
|
- geometry_names(geo_names, m, cur->first);
|
|
|
- u32 num_geo_names = vector::size(geo_names);
|
|
|
-
|
|
|
- opts.write(num_geo_names);
|
|
|
- for (u32 i = 0; i < num_geo_names; ++i)
|
|
|
- opts.write(geo_names[i].to_string_id()._id);
|
|
|
-
|
|
|
- Geometry *geo = (Geometry *)&cur->second;
|
|
|
- mesh::generate_vertex_and_index_buffers(*geo);
|
|
|
-
|
|
|
- bgfx::VertexLayout layout = mesh::vertex_layout(*geo);
|
|
|
- u32 stride = mesh::vertex_stride(*geo);
|
|
|
- OBB bbox = mesh::obb(*geo);
|
|
|
-
|
|
|
- BgfxWriter writer(opts._binary_writer);
|
|
|
- bgfx::write(&writer, layout);
|
|
|
- opts.write(bbox);
|
|
|
-
|
|
|
- opts.write(array::size(geo->_vertex_buffer) / stride);
|
|
|
- opts.write(stride);
|
|
|
- opts.write(array::size(geo->_index_buffer));
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
|
|
|
- opts.write(geo->_vertex_buffer);
|
|
|
- opts.write(array::begin(geo->_index_buffer), array::size(geo->_index_buffer) * sizeof(u16));
|
|
|
- }
|
|
|
+ s32 parse_nodes(Mesh &m, const char *sjson, CompileOptions &opts)
|
|
|
+ {
|
|
|
+ TempAllocator4096 ta;
|
|
|
+ JsonObject nodes(ta);
|
|
|
+ sjson::parse(nodes, sjson);
|
|
|
|
|
|
- return 0;
|
|
|
- }
|
|
|
+ auto cur = json_object::begin(nodes);
|
|
|
+ auto end = json_object::end(nodes);
|
|
|
+ for (; cur != end; ++cur) {
|
|
|
+ JSON_OBJECT_SKIP_HOLE(nodes, cur);
|
|
|
|
|
|
- } // namespace mesh
|
|
|
+ Node node(default_allocator());
|
|
|
+ s32 err = mesh::parse_node(node, cur->second, &m, opts);
|
|
|
+ DATA_COMPILER_ENSURE(err == 0, opts);
|
|
|
|
|
|
- namespace mesh
|
|
|
- {
|
|
|
- s32 parse(Mesh &m, CompileOptions &opts, const char *path)
|
|
|
- {
|
|
|
- Buffer buf = opts.read(path);
|
|
|
+ DynamicString node_name(ta);
|
|
|
+ node_name = cur->first;
|
|
|
+ DATA_COMPILER_ASSERT(!hash_map::has(m._nodes, node_name)
|
|
|
+ , opts
|
|
|
+ , "Node redefined: '%s'"
|
|
|
+ , node_name.c_str()
|
|
|
+ );
|
|
|
|
|
|
- if (str_has_suffix(path, ".mesh"))
|
|
|
- return mesh::parse(m, buf, opts);
|
|
|
+ // For backwards compatibility: originally .mesh resources
|
|
|
+ // enforced (implicitly) a 1:1 relationship between nodes
|
|
|
+ // and geometries.
|
|
|
+ if (node._geometry == "")
|
|
|
+ node._geometry = node_name;
|
|
|
|
|
|
- DATA_COMPILER_ASSERT(false
|
|
|
+ DATA_COMPILER_ASSERT(hash_map::has(m._geometries, node._geometry)
|
|
|
, opts
|
|
|
- , "Unknown mesh '%s'"
|
|
|
- , path
|
|
|
+ , "Node '%s' references unexisting geometry '%s'"
|
|
|
+ , node_name.c_str()
|
|
|
+ , node._geometry.c_str()
|
|
|
);
|
|
|
+ hash_map::set(m._nodes, node_name, node);
|
|
|
}
|
|
|
|
|
|
- } // namespace mesh
|
|
|
-
|
|
|
- Node::Node(Allocator &a)
|
|
|
- : _local_pose(MATRIX4X4_IDENTITY)
|
|
|
- , _geometry(a)
|
|
|
- {
|
|
|
+ return 0;
|
|
|
}
|
|
|
|
|
|
- Geometry::Geometry(Allocator &a)
|
|
|
- : _positions(a)
|
|
|
- , _normals(a)
|
|
|
- , _uvs(a)
|
|
|
- , _tangents(a)
|
|
|
- , _binormals(a)
|
|
|
- , _position_indices(a)
|
|
|
- , _normal_indices(a)
|
|
|
- , _uv_indices(a)
|
|
|
- , _tangent_indices(a)
|
|
|
- , _binormal_indices(a)
|
|
|
- , _vertex_buffer(a)
|
|
|
- , _index_buffer(a)
|
|
|
+ s32 parse(Mesh &m, Buffer &buf, CompileOptions &opts)
|
|
|
{
|
|
|
- mesh::reset(*this);
|
|
|
+ TempAllocator4096 ta;
|
|
|
+ JsonObject nodes(ta);
|
|
|
+ JsonObject obj(ta);
|
|
|
+ sjson::parse(obj, buf);
|
|
|
+
|
|
|
+ s32 err = mesh::parse_geometries(m, obj["geometries"], opts);
|
|
|
+ DATA_COMPILER_ENSURE(err == 0, opts);
|
|
|
+
|
|
|
+ return mesh::parse_nodes(m, obj["nodes"], opts);
|
|
|
}
|
|
|
|
|
|
- Mesh::Mesh(Allocator &a)
|
|
|
- : _geometries(a)
|
|
|
- , _nodes(a)
|
|
|
+ s32 parse(Mesh &m, CompileOptions &opts)
|
|
|
{
|
|
|
+ return mesh::parse(m, opts, opts._source_path.c_str());
|
|
|
}
|
|
|
|
|
|
+}
|
|
|
+
|
|
|
+namespace mesh_resource_internal
|
|
|
+{
|
|
|
s32 compile(CompileOptions &opts)
|
|
|
{
|
|
|
Mesh mesh(default_allocator());
|
|
|
s32 err = mesh::parse(mesh, opts);
|
|
|
DATA_COMPILER_ENSURE(err == 0, opts);
|
|
|
-
|
|
|
return mesh::write(mesh, opts);
|
|
|
}
|
|
|
|