// Copyright (c) 2010-2022, Lawrence Livermore National Security, LLC. Produced // at the Lawrence Livermore National Laboratory. All Rights reserved. See files // LICENSE and NOTICE for details. LLNL-CODE-443271. // // This file is part of the GLVis visualization tool and library. For more // information and source code availability see https://glvis.org. // // GLVis is free software; you can redistribute it and/or modify it under the // terms of the BSD-3 license. We welcome feedback and contributions, see file // CONTRIBUTING.md for details. #include "gltf.hpp" #include "aux_vis.hpp" // SaveAsPNG using namespace std; const char *glTF_Builder::tensorTypes[] = { "SCALAR", "VEC2", "VEC3", "VEC4", "MAT2", "MAT3", "MAT4" }; glTF_Builder::buffer_id glTF_Builder::addBuffer(const string &bufferName) { buffers.resize(buffers.size() + 1); auto &buf = buffers.back(); buf.uri.value = file_prefix + "." + bufferName + ".bin"; buf.uri.valid = true; buf.byteLength.value = 0; buf.byteLength.valid = true; buf.file.reset(new ofstream(buf.uri.value, ios::out | ios::binary)); return {(unsigned)buffers.size() - 1}; } glTF_Builder::buffer_view_id glTF_Builder::addBufferView(buffer_id buffer, const void *data, size_t byteLength, size_t byteStride, size_t byteAlign, target_type target) { if (buffer.id >= buffers.size()) { return {INVALID_ID}; } buffer_views.resize(buffer_views.size() + 1); auto &buf_view = buffer_views.back(); auto &buf = buffers[buffer.id]; buf_view.buffer.value = buffer.id; buf_view.buffer.valid = true; const unsigned buf_offset = buf.byteLength.value; const unsigned new_offset = byteAlign*((buf_offset+byteAlign-1)/byteAlign); buf_view.byteOffset.value = new_offset; buf_view.byteOffset.valid = true; buf_view.byteLength.value = byteLength; buf_view.byteLength.valid = true; if (target == target_type::ARRAY_BUFFER) { buf_view.byteStride.value = byteStride; buf_view.byteStride.valid = true; } buf_view.target.value = (unsigned)target; buf_view.target.valid = true; // append padding to file for (unsigned i = buf_offset; i != new_offset; ++i) { buf.file->put('\0'); } // write data to file buf.file->write(reinterpret_cast(data), byteLength); buf.byteLength.value = new_offset + byteLength; return {(unsigned)buffer_views.size() - 1}; } void glTF_Builder::appendToBufferView(buffer_view_id bufferView, const void *data, size_t byteLength) { if (bufferView.id >= buffer_views.size()) { return; } auto &buf_view = buffer_views[bufferView.id]; auto &buf = buffers[buf_view.buffer.value]; buf_view.byteLength.value += byteLength; buf.file->write(reinterpret_cast(data), byteLength); buf.byteLength.value += byteLength; } glTF_Builder::accessor_id glTF_Builder::addAccessor(buffer_view_id bufferView, size_t byteOffset, component_type componentType, size_t count, tensor_type tensorType) { if (bufferView.id >= buffer_views.size() || count == 0) { return {INVALID_ID}; } accessors.resize(accessors.size() + 1); auto &acc = accessors.back(); acc.bufferView.value = bufferView.id; acc.bufferView.valid = true; acc.byteOffset.value = byteOffset; acc.byteOffset.valid = true; acc.componentType.value = (unsigned)componentType; acc.componentType.valid = true; acc.count.value = count; acc.count.valid = true; acc.type.value = tensorTypes[(unsigned)tensorType]; acc.type.valid = true; // Note: acc.min and acc.max remain invalid and will not be written too file if (componentType != component_type::FLOAT && buffer_views[bufferView.id].target.value != (unsigned)target_type::ELEMENT_ARRAY_BUFFER) { acc.normalized.value = true; acc.normalized.valid = true; } return {(unsigned)accessors.size() - 1}; } glTF_Builder::accessor_id glTF_Builder::addAccessorVec2f(buffer_view_id bufferView, size_t byteOffset, size_t count, vec2f min, vec2f max) { auto id = addAccessor(bufferView, byteOffset, component_type::FLOAT, count, tensor_type::VEC2); if (id.id != INVALID_ID) { auto &acc = accessors[id.id]; acc.min.value.assign(min.begin(), min.end()); acc.min.valid = true; acc.max.value.assign(max.begin(), max.end()); acc.max.valid = true; } return id; } glTF_Builder::accessor_id glTF_Builder::addAccessorVec3f(buffer_view_id bufferView, size_t byteOffset, size_t count, vec3f min, vec3f max) { auto id = addAccessor(bufferView, byteOffset, component_type::FLOAT, count, tensor_type::VEC3); if (id.id != INVALID_ID) { auto &acc = accessors[id.id]; acc.min.value.assign(min.begin(), min.end()); acc.min.valid = true; acc.max.value.assign(max.begin(), max.end()); acc.max.valid = true; } return id; } glTF_Builder::image_id glTF_Builder::addImage(const string &imageName, int width, int height, const color4f *pixels) { #ifndef GLVIS_USE_LIBPNG return {INVALID_ID}; #else images.resize(images.size() + 1); auto &img = images.back(); img.uri.value = file_prefix + "." + imageName + ".png"; img.uri.valid = true; img.name.value = imageName; img.name.valid = true; // write the image auto get_row = [&](int row, void *pxls) { auto pxls_out = reinterpret_cast*>(pxls); auto pxls_in = pixels + row*width; for (int i = 0; i < width; ++i) { for (int j = 0; j < 4; ++j) { pxls_out[i][j] = std::min(int(pxls_in[i][j]*256), 255); } } }; SaveAsPNG(img.uri.value.c_str(), width, height, /* is_hidpi: */ false, /* with_alpha: */ true, get_row); return {(unsigned)images.size() - 1}; #endif // GLVIS_USE_LIBPNG } glTF_Builder::sampler_id glTF_Builder::addSampler(mag_filter magFilter, min_filter minFilter, wrap_type wrapS, wrap_type wrapT) { samplers.resize(samplers.size() + 1); auto &sampler = samplers.back(); sampler.magFilter.value = (unsigned)magFilter; sampler.magFilter.valid = true; sampler.minFilter.value = (unsigned)minFilter; sampler.minFilter.valid= true; sampler.wrapS.value = (unsigned)wrapS; sampler.wrapS.valid = true; sampler.wrapT.value = (unsigned)wrapT; sampler.wrapT.valid = true; return {(unsigned)samplers.size() - 1}; } glTF_Builder::texture_id glTF_Builder::addTexture(sampler_id sampler, image_id source) { if (sampler.id >= samplers.size() || source.id >= images.size()) { return {INVALID_ID}; } textures.resize(textures.size() + 1); auto &tex = textures.back(); tex.sampler.value = sampler.id; tex.sampler.valid = true; tex.source.value = source.id; tex.source.valid = true; return {(unsigned)textures.size() - 1}; } glTF_Builder::material_id glTF_Builder::addMaterial(const string &materialName, const pbr_matallic_roughness &pbrMetallicRoughness, bool doubleSided) { if (pbrMetallicRoughness.haveTexture && pbrMetallicRoughness.baseColorTexture.id >= textures.size()) { return {INVALID_ID}; } materials.resize(materials.size() + 1); auto &mat = materials.back(); mat.name.value = materialName; mat.name.valid = true; auto &pbr = mat.pbrMetallicRoughness.value; pbr.baseColorFactor.value = pbrMetallicRoughness.baseColorFactor; pbr.baseColorFactor.valid = true; auto &tex_info = pbr.baseColorTexture.value; tex_info.index.value = pbrMetallicRoughness.baseColorTexture.id; tex_info.index.valid = true; tex_info.texCoord.value = 0; tex_info.texCoord.valid = true; pbr.baseColorTexture.valid = pbrMetallicRoughness.haveTexture; pbr.metallicFactor.value = pbrMetallicRoughness.metallicFactor; pbr.metallicFactor.valid = true; pbr.roughnessFactor.value = pbrMetallicRoughness.roughnessFactor; pbr.roughnessFactor.valid = true; mat.pbrMetallicRoughness.valid = true; mat.doubleSided.value = doubleSided; mat.doubleSided.valid = true; return {(unsigned)materials.size() - 1}; } glTF_Builder::mesh_id glTF_Builder::addMesh(const string &meshName) { meshes.resize(meshes.size() + 1); auto &mesh = meshes.back(); mesh.name.value = meshName; mesh.name.valid = true; return {(unsigned)meshes.size() - 1}; } void glTF_Builder::addMeshTriangles(mesh_id mesh, accessor_id vertexPositions, accessor_id vertexNormals, accessor_id vertexTexCoords0, accessor_id vertexIndices, material_id material) { if (mesh.id >= meshes.size() || vertexPositions.id >= accessors.size()) { return; } auto &primitives = meshes[mesh.id].primitives; primitives.resize(primitives.size() + 1); auto &pri = primitives.back(); pri.attributes.value.POSITION.value = vertexPositions.id; pri.attributes.value.POSITION.valid = true; if (vertexNormals.id < accessors.size()) { pri.attributes.value.NORMAL.value = vertexNormals.id; pri.attributes.value.NORMAL.valid = true; } if (vertexTexCoords0.id < accessors.size()) { pri.attributes.value.TEXCOORD_0.value = vertexTexCoords0.id; pri.attributes.value.TEXCOORD_0.valid = true; } pri.attributes.valid = true; if (vertexIndices.id < accessors.size()) { pri.indices.value = vertexIndices.id; pri.indices.valid = true; } if (material.id < materials.size()) { pri.material.value = material.id; pri.material.valid = true; } // pri.mode remains undefined since default is 4 = TRIANGLES } void glTF_Builder::addMeshLines(mesh_id mesh, accessor_id vertexPositions, accessor_id vertexTexcoords0, accessor_id vertexColors0, material_id material) { if (mesh.id >= meshes.size()) { return; } auto &primitives = meshes[mesh.id].primitives; primitives.resize(primitives.size() + 1); auto &pri = primitives.back(); pri.attributes.value.POSITION.value = vertexPositions.id; pri.attributes.value.POSITION.valid = true; if (vertexTexcoords0.id < accessors.size()) { pri.attributes.value.TEXCOORD_0.value = vertexTexcoords0.id; pri.attributes.value.TEXCOORD_0.valid = true; } else if (vertexColors0.id < accessors.size()) { pri.attributes.value.COLOR_0.value = vertexColors0.id; pri.attributes.value.COLOR_0.valid = true; } // NORMAL remains undefined pri.attributes.valid = true; // pri.indices remain undefined if (material.id < materials.size()) { pri.material.value = material.id; pri.material.valid = true; } pri.mode.value = 1; // = LINES pri.mode.valid = true; } glTF_Builder::node_id glTF_Builder::addNode(const string &nodeName) { nodes.resize(nodes.size() + 1); auto &node = nodes.back(); node.name.value = nodeName; node.name.valid = true; return {(unsigned)nodes.size() - 1}; } void glTF_Builder::addNodeMesh(node_id node, mesh_id mesh) { if (node.id >= nodes.size()) { return; } nodes[node.id].mesh.value = mesh.id; nodes[node.id].mesh.valid = true; } void glTF_Builder::addNodeScale(node_id node, vec3f scale) { if (node.id >= nodes.size()) { return; } nodes[node.id].scale.value = scale; nodes[node.id].scale.valid = true; } void glTF_Builder::addNodeTranslation(node_id node, vec3f translation) { if (node.id >= nodes.size()) { return; } nodes[node.id].translation.value = translation; nodes[node.id].translation.valid = true; } void glTF_Builder::getMaterialPBRMR(material_id material, pbr_matallic_roughness &pbr_mr_copy) { if (material.id >= materials.size()) { return; } auto &mat = materials[material.id]; auto &pbr = mat.pbrMetallicRoughness.value; pbr_mr_copy.haveTexture = pbr.baseColorTexture.valid; pbr_mr_copy.baseColorFactor = pbr.baseColorFactor.value; pbr_mr_copy.baseColorTexture = {pbr.baseColorTexture.value.index.value}; pbr_mr_copy.metallicFactor = pbr.metallicFactor.value; pbr_mr_copy.roughnessFactor = pbr.roughnessFactor.value; } int glTF_Builder::writeFile() { if (nodes.size() == 0) { return 1; } ofstream gltf(file_prefix + ".gltf"); gltf.precision(8); gltf.setf(ios::boolalpha); // ~~~ Tutorial ~~~ // https://github.com/KhronosGroup/glTF-Tutorials/blob/master/gltfTutorial/README.md // https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#reference-scene gltf << "{\n" " \"scene\": 0,\n" " \"scenes\" : [ {\n" " \"nodes\" : ["; for (size_t i = 0; i != nodes.size(); ++i) { gltf << sep(i) << ' ' << i; } gltf << " ]\n" " } ],\n\n"; // https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#reference-node gltf << " \"nodes\" : ["; for (size_t i = 0; i != nodes.size(); ++i) { gltf << sep(i) << " {"; int pos = 0; print_node(gltf, pos, "\n ", nodes[i].name); print_node(gltf, pos, "\n ", nodes[i].mesh); print_node(gltf, pos, "\n ", nodes[i].scale); print_node(gltf, pos, "\n ", nodes[i].translation); gltf << "\n }"; } gltf << " ],\n\n"; // https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#reference-mesh gltf << " \"meshes\" : ["; for (size_t i = 0; i != meshes.size(); ++i) { gltf << sep(i) << " {"; int pos = 0; print_node(gltf, pos, "\n ", meshes[i].name); gltf << sep(pos++) << "\n \"primitives\" : ["; auto &primitives = meshes[i].primitives; for (size_t j = 0; j != primitives.size(); ++j) { gltf << sep(j) << " {"; int pos2 = 0; print_node(gltf, pos2, "\n ", primitives[j].attributes); print_node(gltf, pos2, "\n ", primitives[j].indices); print_node(gltf, pos2, "\n ", primitives[j].material); print_node(gltf, pos2, "\n ", primitives[j].mode); gltf << "\n }"; } gltf << " ]\n }"; } gltf << " ],\n\n"; // https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#reference-material gltf << " \"materials\" : ["; for (size_t i = 0; i != materials.size(); ++i) { gltf << sep(i) << " {"; int pos = 0; print_node(gltf, pos, "\n ", materials[i].name); print_node(gltf, pos, "\n ", materials[i].pbrMetallicRoughness); print_node(gltf, pos, "\n ", materials[i].doubleSided); gltf << "\n }"; } gltf << " ],\n\n"; gltf << " \"textures\" : ["; for (size_t i = 0; i != textures.size(); ++i) { gltf << sep(i) << " {"; int pos = 0; print_node(gltf, pos, "\n ", textures[i].sampler); print_node(gltf, pos, "\n ", textures[i].source); gltf << "\n }"; } gltf << " ],\n\n"; gltf << " \"images\" : ["; for (size_t i = 0; i != images.size(); ++i) { gltf << sep(i) << " {"; int pos = 0; print_node(gltf, pos, "\n ", images[i].name); print_node(gltf, pos, "\n ", images[i].uri); gltf << "\n }"; } gltf << " ],\n\n"; // https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#reference-sampler // see also: PaletteState::{ToTextureDiscrete(),ToTextureSmooth()} gltf << " \"samplers\" : ["; for (size_t i = 0; i != samplers.size(); ++i) { gltf << sep(i) << " {"; int pos = 0; print_node(gltf, pos, "\n ", samplers[i].magFilter); print_node(gltf, pos, "\n ", samplers[i].minFilter); print_node(gltf, pos, "\n ", samplers[i].wrapS); print_node(gltf, pos, "\n ", samplers[i].wrapT); gltf << "\n }"; } gltf << " ],\n\n"; // https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#reference-buffer gltf << " \"buffers\" : ["; for (size_t i = 0; i != buffers.size(); ++i) { gltf << sep(i) << " {"; int pos = 0; print_node(gltf, pos, "\n ", buffers[i].uri); print_node(gltf, pos, "\n ", buffers[i].byteLength); gltf << "\n }"; } gltf << " ],\n\n"; // https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#reference-bufferview gltf << " \"bufferViews\" : ["; for (size_t i = 0; i != buffer_views.size(); ++i) { gltf << sep(i) << " {"; int pos = 0; print_node(gltf, pos, "\n ", buffer_views[i].buffer); print_node(gltf, pos, "\n ", buffer_views[i].byteOffset); print_node(gltf, pos, "\n ", buffer_views[i].byteLength); print_node(gltf, pos, "\n ", buffer_views[i].byteStride); print_node(gltf, pos, "\n ", buffer_views[i].target); gltf << "\n }"; } gltf << " ],\n\n"; // https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#reference-accessor gltf << " \"accessors\" : ["; for (size_t i = 0; i != accessors.size(); ++i) { gltf << sep(i) << " {"; int pos = 0; print_node(gltf, pos, "\n ", accessors[i].bufferView); print_node(gltf, pos, "\n ", accessors[i].byteOffset); print_node(gltf, pos, "\n ", accessors[i].componentType); print_node(gltf, pos, "\n ", accessors[i].count); print_node(gltf, pos, "\n ", accessors[i].type); print_node(gltf, pos, "\n ", accessors[i].min); print_node(gltf, pos, "\n ", accessors[i].max); print_node(gltf, pos, "\n ", accessors[i].normalized); gltf << "\n }"; } gltf << " ],\n\n"; // https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#reference-asset gltf << " \"asset\" : {\n" " \"version\" : \"2.0\",\n" " \"generator\" : \"GLVis\"\n" " }\n" "}\n"; return 0; }