diff --git a/examples/alembic_to_gltf/README.md b/examples/alembic_to_gltf/README.md index 3909e09..3188166 100644 --- a/examples/alembic_to_gltf/README.md +++ b/examples/alembic_to_gltf/README.md @@ -22,10 +22,40 @@ Edit include and lib paths for Alembic OpenEXR(ilmbase) in `Makefile`, then simp I am testing with Alembic data using Blender's Alembic exporter(feature available from Blender 2.78) +## Extensions + +### Curves + +Curves are reprenseted as point primitive(mode = 0) in glTF level for the backward compatibility of existing glTF specification. +The number of vertices per curve information is aded to `NVERRTS` attribute. +"ext_mode" extra property is added and set it to "curves" string to specify the object as curves primitive. + +Here is an example JSON description of curves primitive. + + + "meshes": { + "curves_1": { + "primitives": [ + { + "attributes": { + "POSITION": "accessor_vertices", + "NVERTS": "accessor_nverts", + }, + "material": "material_1", + "mode": 0, + "extra" { + "ext_mode" : "curves" + } + } + ] + } + }, + + ## TODO * [ ] Support normals and texcoords * [ ] Support mutiple meshes * [ ] Support animation(time samples) * [ ] Support scene graph(node hierarchy) -* [ ] Support Point, Curve and SubD? +* [ ] Support Point and SubD? diff --git a/examples/alembic_to_gltf/abc2gltf.cc b/examples/alembic_to_gltf/abc2gltf.cc index d198b8c..01bb443 100644 --- a/examples/alembic_to_gltf/abc2gltf.cc +++ b/examples/alembic_to_gltf/abc2gltf.cc @@ -40,13 +40,38 @@ #include "../../tiny_gltf_loader.h" // To import some TINYGLTF_*** macros. -// @todo { texcoords, normals } typedef struct { std::vector vertices; + + // Either `normals` or `facevarying_normals` is filled. + std::vector normals; + std::vector facevarying_normals; + + // Either `texcoords` or `facevarying_texcoords` is filled. + std::vector texcoords; + std::vector facevarying_texcoords; + std::vector faces; } Mesh; +// Curves are represented as an array of curve. +// i'th curve has nverts[i] points. +// TODO(syoyo) knots, order to support NURBS curve. +typedef struct +{ + std::vector points; + std::vector nverts; // # of vertices per strand(curve). +} Curves; + +// Points represent particle data. +// TODO(syoyo) +typedef struct +{ + std::vector points; + std::vector radiuss; +} Points; + // ---------------------------------------------------------------- // writer module // @todo { move writer code to tiny_gltf_writer.h } @@ -264,13 +289,114 @@ BuildFaceSet( } +static void readPolyNormals( + std::vector *normals, + std::vector *facevarying_normals, + Alembic::AbcGeom::IN3fGeomParam normals_param) +{ + normals->clear(); + facevarying_normals->clear(); + + if (!normals_param) { + return; + } + + if ((normals_param.getScope() != Alembic::AbcGeom::kVertexScope) && + (normals_param.getScope() != Alembic::AbcGeom::kVaryingScope) && + (normals_param.getScope() != Alembic::AbcGeom::kFacevaryingScope)) + { + std::cout << "Normal vector has an unsupported scope" << std::endl; + return; + } + + if (normals_param.getScope() == Alembic::AbcGeom::kVertexScope) { + std::cout << "Normal: VertexScope" << std::endl; + } else if (normals_param.getScope() == Alembic::AbcGeom::kVaryingScope) { + std::cout << "Normal: VaryingScope" << std::endl; + } else if (normals_param.getScope() == Alembic::AbcGeom::kFacevaryingScope) { + std::cout << "Normal: FacevaryingScope" << std::endl; + } + + // @todo { lerp normal among time sample.} + Alembic::AbcGeom::IN3fGeomParam::Sample samp; + Alembic::AbcGeom::ISampleSelector samplesel( + 0.0, Alembic::AbcGeom::ISampleSelector::kNearIndex); + normals_param.getExpanded(samp, samplesel); + + Alembic::Abc::N3fArraySamplePtr P = samp.getVals(); + size_t sample_size = P->size(); + + if (normals_param.getScope() == Alembic::AbcGeom::kFacevaryingScope) { + for (size_t i = 0; i < sample_size; i++) { + facevarying_normals->push_back((*P)[i].x); + facevarying_normals->push_back((*P)[i].y); + facevarying_normals->push_back((*P)[i].z); + } + } else { + for (size_t i = 0; i < sample_size; i++) { + normals->push_back((*P)[i].x); + normals->push_back((*P)[i].y); + normals->push_back((*P)[i].z); + } + } +} + +// @todo { Support multiple UVset. } +static void readPolyUVs( + std::vector *uvs, + std::vector *facevarying_uvs, + Alembic::AbcGeom::IV2fGeomParam uvs_param) +{ + uvs->clear(); + facevarying_uvs->clear(); + + if (!uvs_param) { + return; + } + + if (uvs_param.getNumSamples() > 0) { + std::string uv_set_name = Alembic::Abc::GetSourceName(uvs_param.getMetaData()); + std::cout << "UVset : " << uv_set_name << std::endl; + } + + if (uvs_param.isConstant()) { + std::cout << "UV is constant" << std::endl; + } + + if (uvs_param.getScope() == Alembic::AbcGeom::kVertexScope) { + std::cout << "UV: VertexScope" << std::endl; + } else if (uvs_param.getScope() == Alembic::AbcGeom::kVaryingScope) { + std::cout << "UV: VaryingScope" << std::endl; + } else if (uvs_param.getScope() == Alembic::AbcGeom::kFacevaryingScope) { + std::cout << "UV: FacevaryingScope" << std::endl; + } + + // @todo { lerp normal among time sample.} + Alembic::AbcGeom::IV2fGeomParam::Sample samp; + Alembic::AbcGeom::ISampleSelector samplesel( + 0.0, Alembic::AbcGeom::ISampleSelector::kNearIndex); + uvs_param.getIndexed(samp, samplesel); + + Alembic::Abc::V2fArraySamplePtr P = samp.getVals(); + size_t sample_size = P->size(); + + if (uvs_param.getScope() == Alembic::AbcGeom::kFacevaryingScope) { + for (size_t i = 0; i < sample_size; i++) { + facevarying_uvs->push_back((*P)[i].x); + facevarying_uvs->push_back((*P)[i].y); + } + } else { + for (size_t i = 0; i < sample_size; i++) { + uvs->push_back((*P)[i].x); + uvs->push_back((*P)[i].y); + } + } +} + // Traverse Alembic object tree and extract mesh object. -// Currently we only extract first found polygon mesh. -static void VisitObjectAndExtractMesh(Mesh* mesh, std::stringstream& ss, bool& foundMesh, const Alembic::AbcGeom::IObject& obj, const std::string& indent) { - if (foundMesh) { - return; - } +// Currently we only extract first found geometry object. +static void VisitObjectAndExtractObject(Mesh* mesh, Curves* curves, std::stringstream& ss, bool& foundMesh, bool &foundCurves, const Alembic::AbcGeom::IObject& obj, const std::string& indent) { std::string path = obj.getFullName(); @@ -283,8 +409,12 @@ static void VisitObjectAndExtractMesh(Mesh* mesh, std::stringstream& ss, bool& f for (size_t i = 0; i < obj.getNumChildren(); i++) { const Alembic::AbcGeom::ObjectHeader& header = obj.getChildHeader(i); + ss << " Child: header = " << header.getName() << std::endl; - if (Alembic::AbcGeom::IPolyMesh::matches(header)) { + Alembic::AbcGeom::ICompoundProperty cprops = obj.getChild(i).getProperties(); + VisitProperties(ss, props, indent); + + if ((!foundMesh) && (Alembic::AbcGeom::IPolyMesh::matches(header))) { // Polygon Alembic::AbcGeom::IPolyMesh pmesh(obj, header.getName()); @@ -303,10 +433,31 @@ static void VisitObjectAndExtractMesh(Mesh* mesh, std::stringstream& ss, bool& f << std::endl; + // normals + Alembic::AbcGeom::IN3fGeomParam normals_param = ps.getNormalsParam(); + std::vector normals; + std::vector facevarying_normals; + readPolyNormals(&normals, &facevarying_normals, normals_param); + std::cout << " # of normals = " << (normals.size() / 3) << std::endl; + std::cout << " # of facevarying normals = " << (facevarying_normals.size() / 3) << std::endl; + mesh->normals = normals; + + + // UV + Alembic::AbcGeom::IV2fGeomParam uvs_param = ps.getUVsParam(); + std::vector uvs; + std::vector facevarying_uvs; + readPolyUVs(&uvs, &facevarying_uvs, uvs_param); + std::cout << " # of uvs = " << (uvs.size() / 2) << std::endl; + std::cout << " # of facevarying_uvs = " << (facevarying_uvs.size() / 2) << std::endl; + mesh->texcoords = uvs; + mesh->facevarying_texcoords = facevarying_uvs; + std::vector faces; // temp bool ret = BuildFaceSet(faces, P, psample.getFaceIndices(), psample.getFaceCounts()); - assert(ret); - (void)(ret); + if (!ret) { + std::cout << " No faces in polymesh" << std::endl; + } mesh->vertices.resize(3 * P->size()); memcpy(mesh->vertices.data(), P->get(), sizeof(float) * 3 * P->size()); @@ -318,13 +469,75 @@ static void VisitObjectAndExtractMesh(Mesh* mesh, std::stringstream& ss, bool& f } else { std::cout << "Warning: # of samples = 0" << std::endl; } + } else if ((!foundCurves) && Alembic::AbcGeom::ICurves::matches(header)) { + // Curves + Alembic::AbcGeom::ICurves curve(obj, header.getName()); + + Alembic::AbcGeom::ISampleSelector samplesel( + 0.0, Alembic::AbcGeom::ISampleSelector::kNearIndex); + Alembic::AbcGeom::ICurvesSchema::Sample psample; + Alembic::AbcGeom::ICurvesSchema& ps = curve.getSchema(); + + std::cout << " # of samples = " << ps.getNumSamples() << std::endl; + + if (ps.getNumSamples() > 0) { + ps.get(psample, samplesel); + + const size_t num_curves = psample.getNumCurves(); + std::cout << " # of curves = " << num_curves << std::endl; + + Alembic::Abc::P3fArraySamplePtr P = psample.getPositions(); + Alembic::Abc::FloatArraySamplePtr knots = psample.getKnots(); + Alembic::Abc::UcharArraySamplePtr orders = psample.getOrders(); + + Alembic::Abc::Int32ArraySamplePtr num_vertices = psample.getCurvesNumVertices(); + + if (knots) std::cout << " # of knots= " << knots->size() << std::endl; + if (orders) std::cout << " # of orders= " << orders->size() << std::endl; + std::cout << " # of nvs= " << num_vertices->size() << std::endl; + + curves->points.resize(3 * P->size()); + memcpy(curves->points.data(), P->get(), sizeof(float) * 3 * P->size()); + + //for (size_t k = 0; k < P->size(); k++) { + // std::cout << "P[" << k << "] " << (*P)[k].x << ", " << (*P)[k].y << ", " << (*P)[k].z << std::endl; + //} + + //if (knots) { + //for (size_t k = 0; k < knots->size(); k++) { + // std::cout << "knots[" << k << "] " << (*knots)[k] << std::endl; + //} + //} + + //if (orders) { + //for (size_t k = 0; k < orders->size(); k++) { + // std::cout << "orders[" << k << "] " << (*orders)[k] << std::endl; + //} + //} + + //for (size_t k = 0; k < num_vertices->size(); k++) { + // std::cout << "nv[" << k << "] " << (*num_vertices)[k] << std::endl; + //} + + if (num_vertices) { + curves->nverts.resize(num_vertices->size()); + memcpy(curves->nverts.data(), num_vertices->get(), sizeof(int) * num_vertices->size()); + } + + foundCurves = true; + return; + + } else { + std::cout << "Warning: # of samples = 0" << std::endl; + } + } - VisitObjectAndExtractMesh(mesh, ss, foundMesh, Alembic::AbcGeom::IObject(obj, obj.getChildHeader(i).getName()), indent); + VisitObjectAndExtractObject(mesh, curves, ss, foundMesh, foundCurves, Alembic::AbcGeom::IObject(obj, obj.getChildHeader(i).getName()), indent); } } -static bool SaveGLTF(const std::string& output_filename, +static bool SaveMeshToGLTF(const std::string& output_filename, const Mesh& mesh) { picojson::object root; @@ -398,6 +611,7 @@ static bool SaveGLTF(const std::string& output_filename, picojson::object attributes; attributes["POSITION"] = picojson::value(std::string("accessor_vertices")); + picojson::object primitive; primitive["attributes"] = picojson::value(attributes); @@ -506,6 +720,209 @@ static bool SaveGLTF(const std::string& output_filename, return true; } +static bool SaveCurvesToGLTF(const std::string& output_filename, + const Curves& curves) { + picojson::object root; + + { + picojson::object asset; + asset["generator"] = picojson::value("abc2gltf"); + asset["premultipliedAlpha"] = picojson::value(true); + asset["version"] = picojson::value(static_cast(1)); + picojson::object profile; + profile["api"] = picojson::value("WebGL"); + profile["version"] = picojson::value("1.0.2"); + asset["profile"] = picojson::value(profile); + root["assets"] = picojson::value(asset); + } + + { + picojson::object buffers; + { + { + std::string b64data = base64_encode(reinterpret_cast(curves.points.data()), curves.points.size() * sizeof(float)); + picojson::object buf; + + buf["type"] = picojson::value("arraybuffer"); + buf["uri"] = picojson::value( + std::string("data:application/octet-stream;base64,") + b64data); + buf["byteLength"] = + picojson::value(static_cast(curves.points.size() * sizeof(float))); + + buffers["points"] = picojson::value(buf); + } + + // Out extension + { + std::string b64data = base64_encode(reinterpret_cast(curves.nverts.data()), curves.nverts.size() * sizeof(int)); + picojson::object buf; + + buf["type"] = picojson::value("arraybuffer"); + buf["uri"] = picojson::value( + std::string("data:application/octet-stream;base64,") + b64data); + buf["byteLength"] = + picojson::value(static_cast(curves.nverts.size() * sizeof(int))); + + buffers["nverts"] = picojson::value(buf); + } + + } + root["buffers"] = picojson::value(buffers); + } + + { + picojson::object buffer_views; + { + { + picojson::object buffer_view_points; + buffer_view_points["buffer"] = picojson::value(std::string("points")); + buffer_view_points["byteLength"] = picojson::value(static_cast(curves.points.size() * sizeof(float))); + buffer_view_points["byteOffset"] = picojson::value(static_cast(0)); + buffer_view_points["target"] = picojson::value(static_cast(TINYGLTF_TARGET_ARRAY_BUFFER)); + buffer_views["bufferView_points"] = picojson::value(buffer_view_points); + } + + { + picojson::object buffer_view_nverts; + buffer_view_nverts["buffer"] = picojson::value(std::string("nverts")); + buffer_view_nverts["byteLength"] = picojson::value(static_cast(curves.nverts.size() * sizeof(int))); + buffer_view_nverts["byteOffset"] = picojson::value(static_cast(0)); + buffer_view_nverts["target"] = picojson::value(static_cast(TINYGLTF_TARGET_ARRAY_BUFFER)); + buffer_views["bufferView_nverts"] = picojson::value(buffer_view_nverts); + } + + } + + root["bufferViews"] = picojson::value(buffer_views); + } + + { + picojson::object attributes; + + attributes["POSITION"] = picojson::value(std::string("accessor_points")); + attributes["NVERTS"] = picojson::value(std::string("accessor_nverts")); + + // Extra information for curves primtive. + picojson::object extra; + extra["ext_mode"] = picojson::value("curves"); + + picojson::object primitive; + primitive["attributes"] = picojson::value(attributes); + //primitive["indices"] = picojson::value("accessor_indices"); + primitive["material"] = picojson::value("material_1"); + primitive["mode"] = picojson::value(static_cast(TINYGLTF_MODE_POINTS)); // Use GL_POINTS for backward compatibility + primitive["extra"] = picojson::value(extra); + + + picojson::array primitive_array; + primitive_array.push_back(picojson::value(primitive)); + + picojson::object m; + m["primitives"] = picojson::value(primitive_array); + + picojson::object meshes; + meshes["mesh_1"] = picojson::value(m); + + + root["meshes"] = picojson::value(meshes); + } + + { + picojson::object accessors; + + { + picojson::object accessor_points; + accessor_points["bufferView"] = picojson::value(std::string("bufferView_points")); + accessor_points["byteOffset"] = picojson::value(static_cast(0)); + accessor_points["byteStride"] = picojson::value(static_cast(3 * sizeof(float))); + accessor_points["componentType"] = picojson::value(static_cast(TINYGLTF_COMPONENT_TYPE_FLOAT)); + accessor_points["count"] = picojson::value(static_cast(curves.points.size())); + accessor_points["type"] = picojson::value(std::string("VEC3")); + accessors["accessor_points"] = picojson::value(accessor_points); + } + + { + picojson::object accessor_nverts; + accessor_nverts["bufferView"] = picojson::value(std::string("bufferView_nverts")); + accessor_nverts["byteOffset"] = picojson::value(static_cast(0)); + accessor_nverts["byteStride"] = picojson::value(static_cast(sizeof(int))); + accessor_nverts["componentType"] = picojson::value(static_cast(TINYGLTF_COMPONENT_TYPE_INT)); + accessor_nverts["count"] = picojson::value(static_cast(curves.nverts.size())); + accessor_nverts["type"] = picojson::value(std::string("SCALAR")); + accessors["accessor_nverts"] = picojson::value(accessor_nverts); + } + + picojson::object accessor_indices; + + root["accessors"] = picojson::value(accessors); + } + + { + // Use Default Material(Do not supply `material.technique`) + picojson::object default_material; + picojson::object materials; + + materials["material_1"] = picojson::value(default_material); + + root["materials"] = picojson::value(materials); + + } + + { + picojson::object nodes; + picojson::object node; + picojson::array meshes; + + meshes.push_back(picojson::value(std::string("mesh_1"))); + + node["meshes"] = picojson::value(meshes); + + nodes["node_1"] = picojson::value(node); + root["nodes"] = picojson::value(nodes); + } + + { + picojson::object defaultScene; + picojson::array nodes; + + nodes.push_back(picojson::value(std::string("node_1"))); + + defaultScene["nodes"] = picojson::value(nodes); + + root["scene"] = picojson::value("defaultScene"); + picojson::object scenes; + scenes["defaultScene"] = picojson::value(defaultScene); + root["scenes"] = picojson::value(scenes); + } + + + // @todo {} + picojson::object shaders; + picojson::object programs; + picojson::object techniques; + picojson::object materials; + picojson::object skins; + root["shaders"] = picojson::value(shaders); + root["programs"] = picojson::value(programs); + root["techniques"] = picojson::value(techniques); + root["materials"] = picojson::value(materials); + root["skins"] = picojson::value(skins); + + std::ofstream ifs(output_filename.c_str()); + if (ifs.bad()) { + std::cerr << "Failed to open " << output_filename << std::endl; + return false; + } + + picojson::value v = picojson::value(root); + + std::string s = v.serialize(/* pretty */true); + ifs.write(s.data(), static_cast(s.size())); + ifs.close(); + + return true; +} + int main(int argc, char** argv) { std::string abc_filename; @@ -528,13 +945,15 @@ int main(int argc, char** argv) { std::stringstream ss; Mesh mesh; + Curves curves; bool foundMesh = false; - VisitObjectAndExtractMesh(&mesh, ss, foundMesh, root, /* indent */" "); + bool foundCurves = false; + VisitObjectAndExtractObject(&mesh, &curves, ss, foundMesh, foundCurves, root, /* indent */" "); std::cout << ss.str() << std::endl; if (foundMesh) { - bool ret = SaveGLTF(gltf_filename, mesh); + bool ret = SaveMeshToGLTF(gltf_filename, mesh); if (ret) { std::cout << "Wrote " << gltf_filename << std::endl; } else { @@ -544,5 +963,14 @@ int main(int argc, char** argv) { std::cout << "No polygon mesh found in Alembic file" << std::endl; } + if (foundCurves) { + bool ret = SaveCurvesToGLTF(gltf_filename, curves); + if (ret) { + std::cout << "Wrote " << gltf_filename << std::endl; + } else { + return EXIT_FAILURE; + } + } + return EXIT_SUCCESS; }