blob: a384aad5b3306e6326efab193e8b71cff03321f0 [file] [log] [blame]
// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <fuchsia/ui/scenic/cpp/fidl.h>
#include <fuchsia/ui/scenic/internal/cpp/fidl.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/async-loop/default.h>
#include <lib/sys/cpp/component_context.h>
#include <lib/syslog/cpp/macros.h>
#include <lib/trace-provider/provider.h>
#include <png.h>
#include <cstdlib>
#include <iostream>
#include <memory>
#include "rapidjson/document.h"
#include "rapidjson/prettywriter.h"
#include "rapidjson/stringbuffer.h"
#include "src/lib/fsl/vmo/vector.h"
#include "src/lib/fxl/command_line.h"
#include "src/lib/fxl/log_settings_command_line.h"
#include "src/ui/scenic/lib/gfx/snapshot/snapshot_generated.h"
#include "src/ui/scenic/lib/gfx/snapshot/version.h"
#include "third_party/modp_b64/modp_b64.h"
using namespace rapidjson;
using namespace scenic_impl::gfx;
const char *EMPTY_GLTF_DOC = R"glTF({
"scenes": [{
"nodes": []
}],
"scene": 0,
"nodes": [],
"meshes": [],
"buffers": [],
"bufferViews": [],
"accessors": [],
"materials": [],
"textures": [],
"images": [],
"samplers": [{}],
"asset": {
"version": "2.0"
}
})glTF";
const char *EMPTY_TEXTURE_MATERIAL = R"glTF({
"pbrMetallicRoughness" : {
"baseColorTexture" : {
},
"metallicFactor" : 0.0,
"roughnessFactor" : 1.0
}
})glTF";
const char *EMPTY_COLOR_MATERIAL = R"glTF({
"pbrMetallicRoughness" : {
"baseColorFactor" : [1.0, 1.0, 1.0, 1.0],
"metallicFactor" : 0.0,
"roughnessFactor" : 1.0
}
})glTF";
// Converts uncompressed raw image to PNG.
bool RawToPNG(size_t width, size_t height, const uint8_t *data, std::vector<uint8_t> &out);
// Encode bytes to base64.
std::string Base64Encode(const uint8_t *bytes, size_t size);
// Dumps rapidjson Value to ostream.
std::ostream &operator<<(std::ostream &os, const Value &v) {
StringBuffer buffer;
PrettyWriter<StringBuffer> writer(buffer);
v.Accept(writer);
return os << buffer.GetString();
}
// Defines a class to take a snapshot of the current scenic composition.
class SnapshotTaker {
public:
explicit SnapshotTaker(async::Loop *loop)
: loop_(loop), context_(sys::ComponentContext::CreateAndServeOutgoingDirectory()) {
// Connect to the Scenic service.
scenic_ = context_->svc()->Connect<fuchsia::ui::scenic::Scenic>();
scenic_.set_error_handler([this](zx_status_t status) {
FX_LOGS(ERROR) << "Lost connection to Scenic service.";
encountered_error_ = true;
loop_->Quit();
});
// Connect to the internal snapshot service.
snapshotter_ = context_->svc()->Connect<fuchsia::ui::scenic::internal::Snapshot>();
snapshotter_.set_error_handler([this](zx_status_t status) {
FX_LOGS(ERROR) << "Lost connection to Snapshot service.";
encountered_error_ = true;
loop_->Quit();
});
}
bool encountered_error() const { return encountered_error_; }
// Takes a snapshot of the current scenic composition and dumps it to
// std::out in glTF format.
void TakeSnapshot() {
// If we wait for a call back from GetDisplayInfo, we are guaranteed that
// the GFX system is initialized, which is a prerequisite for taking a
// screenshot. TODO(fxbug.dev/23901): Remove call to GetDisplayInfo once bug done.
scenic_->GetDisplayInfo([this](fuchsia::ui::gfx::DisplayInfo /*unused*/) {
snapshotter_->TakeSnapshot(
[this](std::vector<fuchsia::ui::scenic::internal::SnapshotResult> results) {
if (results.size() == 0) {
FX_LOGS(INFO) << "No compositors found.";
loop_->Quit();
}
// Although multiple results can be returned, one for each compositor, the glTF exporter
// currently only makes use of the first compositor that is found.
if (results.size() > 1) {
FX_LOGS(WARNING) << "Multiple snapshot buffers were returned, but glTF exporter is "
"only using the first one.";
}
const auto &result = results[0];
if (!result.success) {
FX_LOGS(ERROR) << "Snapshot was not successful.";
encountered_error_ = true;
loop_->Quit();
}
const auto &buffer = result.buffer;
std::vector<uint8_t> data;
if (!fsl::VectorFromVmo(buffer, &data)) {
FX_LOGS(ERROR) << "TakeSnapshot failed";
encountered_error_ = true;
loop_->Quit();
return;
}
// We currently support flatbuffer.v1_0 format.
auto snapshot = (const SnapshotData *)data.data();
if (snapshot->type != SnapshotData::SnapshotType::kFlatBuffer ||
snapshot->version != SnapshotData::SnapshotVersion::v1_0) {
FX_LOGS(ERROR) << "Invalid snapshot format encountered. Aborting.";
encountered_error_ = true;
loop_->Quit();
return;
}
// De-serialize the snapshot from flatbuffer.
auto node = flatbuffers::GetRoot<snapshot::Node>(snapshot->data);
// Start with an empty glTF document.
document_.Parse(EMPTY_GLTF_DOC);
// Export root node of the scene graph. This recursively exports all
// descendant nodes.
int index = glTF_export_node(node, true);
auto &gltf_nodes = document_["scenes"][0]["nodes"];
gltf_nodes.PushBack(index, document_.GetAllocator());
// Dump the result json document in glTF format to stdout.
std::cout << document_;
loop_->Quit();
});
});
}
private:
int glTF_export_node(const snapshot::Node *node, bool flip_yaxis = false) {
// The allocator used through out.
auto &allocator = document_.GetAllocator();
auto gltf_node = Value(kObjectType);
if (node->name()) {
gltf_node.AddMember("name", node->name()->str(), allocator);
}
if (node->transform()) {
auto translation = node->transform()->translation();
if (translation->x() != 0.0 || translation->y() != 0.0 || translation->z() != 0.0) {
auto gltf_translation = Value(kArrayType);
gltf_translation.PushBack(translation->x(), allocator);
gltf_translation.PushBack(translation->y(), allocator);
gltf_translation.PushBack(-translation->z(), allocator);
gltf_node.AddMember("translation", gltf_translation, allocator);
}
auto rotation = node->transform()->rotation();
if (rotation->x() != 0.0 || rotation->y() != 0.0 || rotation->z() != 0.0 ||
rotation->w() != 1.0) {
auto gltf_rotation = Value(kArrayType);
gltf_rotation.PushBack(rotation->x(), allocator);
gltf_rotation.PushBack(rotation->y(), allocator);
gltf_rotation.PushBack(rotation->z(), allocator);
gltf_rotation.PushBack(rotation->w(), allocator);
gltf_node.AddMember("rotation", gltf_rotation, allocator);
}
auto scale = node->transform()->scale();
if (scale->x() != 1.0 || scale->y() != 1.0 || scale->z() != 1.0) {
auto gltf_scale = Value(kArrayType);
gltf_scale.PushBack(scale->x(), allocator);
if (flip_yaxis) {
gltf_scale.PushBack(scale->y() * -1, allocator);
} else {
gltf_scale.PushBack(scale->y(), allocator);
}
gltf_scale.PushBack(scale->z(), allocator);
gltf_node.AddMember("scale", gltf_scale, allocator);
}
}
if (node->mesh()) {
int index = glTF_export_mesh(node);
gltf_node.AddMember("mesh", index, allocator);
}
auto &gltf_nodes = document_["nodes"];
auto index = gltf_nodes.Size();
gltf_nodes.PushBack(gltf_node, allocator);
if (node->children()) {
auto child_nodes = Value(kArrayType);
for (auto child : *node->children()) {
int index = glTF_export_node(child);
child_nodes.PushBack(index, allocator);
}
auto &gltf_node = document_["nodes"][index];
gltf_node.AddMember("children", child_nodes, allocator);
}
return index;
}
int glTF_export_mesh(const snapshot::Node *node) {
// The allocator used through out.
auto &allocator = document_.GetAllocator();
auto gltf_primitive = Value(kObjectType);
gltf_primitive.AddMember("material", glTF_export_material(node), allocator);
gltf_primitive.AddMember("attributes", glTF_export_buffer(node->mesh(), true), allocator);
gltf_primitive.AddMember("indices", glTF_export_buffer(node->mesh(), false), allocator);
auto gltf_primitives = Value(kArrayType);
gltf_primitives.PushBack(gltf_primitive, allocator);
auto gltf_mesh = Value(kObjectType);
gltf_mesh.AddMember("primitives", gltf_primitives, allocator);
auto &gltf_meshes = document_["meshes"];
auto index = gltf_meshes.Size();
gltf_meshes.PushBack(gltf_mesh, allocator);
return index;
}
Value glTF_export_buffer(const snapshot::Geometry *mesh, bool is_vertex_buffer) {
auto &allocator = document_.GetAllocator();
const uint8_t *bytes = is_vertex_buffer ? mesh->attributes()->Get(0)->buffer()->Data()
: mesh->indices()->buffer()->Data();
size_t size = is_vertex_buffer ? mesh->attributes()->Get(0)->buffer()->Length()
: mesh->indices()->buffer()->Length();
int count = is_vertex_buffer ? mesh->attributes()->Get(0)->vertex_count()
: mesh->indices()->index_count();
// Create a glTF buffer.
std::string base64_bytes = Base64Encode(bytes, size);
std::ostringstream data;
data << "data:application/octet-stream;base64," << base64_bytes;
auto data_str = data.str();
Value data_uri(kStringType);
data_uri.SetString(data_str.c_str(), data_str.length(), allocator);
Value gltf_buffer(kObjectType);
gltf_buffer.AddMember("uri", data_uri, allocator);
gltf_buffer.AddMember("byteLength", size, allocator);
auto &gltf_buffers = document_["buffers"];
int buffer_index = gltf_buffers.Size();
gltf_buffers.PushBack(gltf_buffer, allocator);
// Create a glTF bufferView.
Value gltf_bufferView(kObjectType);
gltf_bufferView.AddMember("buffer", Value(buffer_index), allocator);
gltf_bufferView.AddMember("byteOffset", Value(0), allocator);
gltf_bufferView.AddMember("byteLength", size, allocator);
gltf_bufferView.AddMember("target", is_vertex_buffer ? Value(34962) : Value(34963), allocator);
if (is_vertex_buffer) {
gltf_bufferView.AddMember("byteStride", mesh->attributes()->Get(0)->stride(), allocator);
}
auto &gltf_bufferViews = document_["bufferViews"];
int buffer_view_index = gltf_bufferViews.Size();
gltf_bufferViews.PushBack(gltf_bufferView, allocator);
// Create a glTF accessor.
Value gltf_accessor(kObjectType);
gltf_accessor.AddMember("bufferView", Value(buffer_view_index), allocator);
gltf_accessor.AddMember("byteOffset", Value(0), allocator);
gltf_accessor.AddMember("componentType", is_vertex_buffer ? Value(5126) : Value(5125),
allocator);
gltf_accessor.AddMember("count", count, allocator);
gltf_accessor.AddMember("type", is_vertex_buffer ? Value("VEC3") : Value("SCALAR"), allocator);
if (is_vertex_buffer) {
Value gltf_max(kArrayType);
gltf_max.PushBack(mesh->bbox_max()->x(), allocator);
gltf_max.PushBack(mesh->bbox_max()->y(), allocator);
gltf_max.PushBack(mesh->bbox_max()->z(), allocator);
Value gltf_min(kArrayType);
gltf_min.PushBack(mesh->bbox_min()->x(), allocator);
gltf_min.PushBack(mesh->bbox_min()->y(), allocator);
gltf_min.PushBack(mesh->bbox_min()->z(), allocator);
gltf_accessor.AddMember("max", gltf_max, allocator);
gltf_accessor.AddMember("min", gltf_min, allocator);
}
auto &gltf_accessors = document_["accessors"];
int accessor_index = gltf_accessors.Size();
gltf_accessors.PushBack(gltf_accessor, allocator);
int texture_accessor_index = gltf_accessors.Size();
// Add texture accessor for vertex buffer.
if (is_vertex_buffer) {
Value gltf_accessor(kObjectType);
gltf_accessor.AddMember("bufferView", Value(buffer_view_index), allocator);
gltf_accessor.AddMember("byteOffset", Value(8), allocator);
gltf_accessor.AddMember("componentType", Value(5126), allocator);
gltf_accessor.AddMember("count", count, allocator);
gltf_accessor.AddMember("type", Value("VEC2"), allocator);
Value gltf_max(kArrayType);
gltf_max.PushBack(1.0, allocator);
gltf_max.PushBack(1.0, allocator);
Value gltf_min(kArrayType);
gltf_min.PushBack(0.0, allocator);
gltf_min.PushBack(0.0, allocator);
gltf_accessor.AddMember("max", gltf_max, allocator);
gltf_accessor.AddMember("min", gltf_min, allocator);
gltf_accessors.PushBack(gltf_accessor, allocator);
}
// Add the accessor index to the glTF primitive.
if (is_vertex_buffer) {
auto gltf_attributes = Value(kObjectType);
gltf_attributes.AddMember("POSITION", accessor_index, allocator);
gltf_attributes.AddMember("TEXCOORD_0", texture_accessor_index, allocator);
return gltf_attributes;
} else {
return Value(accessor_index);
}
}
int glTF_export_material(const snapshot::Node *node) {
auto &allocator = document_.GetAllocator();
if (node->material_type() == snapshot::Material_Color) {
auto color = static_cast<const snapshot::Color *>(node->material());
Document gltf_material(kObjectType, &allocator);
gltf_material.Parse(EMPTY_COLOR_MATERIAL);
auto &gltf_color = gltf_material["pbrMetallicRoughness"]["baseColorFactor"];
gltf_color.Clear();
gltf_color.PushBack(color->red(), allocator);
gltf_color.PushBack(color->green(), allocator);
gltf_color.PushBack(color->blue(), allocator);
gltf_color.PushBack(color->alpha(), allocator);
auto &gltf_materials = document_["materials"];
int materials_index = gltf_materials.Size();
gltf_materials.PushBack(gltf_material, allocator);
return materials_index;
} else {
auto image = static_cast<const snapshot::Image *>(node->material());
// Convert raw image to png.
const uint8_t *bytes = image->data()->Data();
std::vector<uint8_t> out;
if (!RawToPNG(image->width(), image->height(), bytes, out)) {
FX_LOGS(FATAL) << "Unable to convert to PNG";
return -1;
}
// Encode to base64.
std::string base64_bytes = Base64Encode(out.data(), out.size());
std::ostringstream data;
data << "data:image/png;base64," << base64_bytes;
auto data_str = data.str();
Value data_uri(kStringType);
data_uri.SetString(data_str.c_str(), data_str.length(), allocator);
Value gltf_image(kObjectType);
gltf_image.AddMember("mimeType", "image/png", allocator);
gltf_image.AddMember("width", image->width(), allocator);
gltf_image.AddMember("height", image->height(), allocator);
gltf_image.AddMember("format", image->format(), allocator);
gltf_image.AddMember("size", out.size(), allocator);
gltf_image.AddMember("uri", data_uri, allocator);
auto &gltf_images = document_["images"];
int image_index = gltf_images.Size();
gltf_images.PushBack(gltf_image, allocator);
Value gltf_texture(kObjectType);
gltf_texture.AddMember("sampler", 0, allocator);
gltf_texture.AddMember("source", image_index, allocator);
auto &gltf_textures = document_["textures"];
int texture_index = gltf_textures.Size();
gltf_textures.PushBack(gltf_texture, allocator);
Document gltf_material(kObjectType, &allocator);
gltf_material.Parse(EMPTY_TEXTURE_MATERIAL);
auto &gltf_baseColorTexture = gltf_material["pbrMetallicRoughness"]["baseColorTexture"];
gltf_baseColorTexture.AddMember("index", texture_index, allocator);
auto &gltf_materials = document_["materials"];
int materials_index = gltf_materials.Size();
gltf_materials.PushBack(gltf_material, allocator);
return materials_index;
}
}
private:
async::Loop *loop_;
std::unique_ptr<sys::ComponentContext> context_;
bool encountered_error_ = false;
fuchsia::ui::scenic::ScenicPtr scenic_;
fuchsia::ui::scenic::internal::SnapshotPtr snapshotter_;
Document document_;
};
int main(int argc, const char **argv) {
auto command_line = fxl::CommandLineFromArgcArgv(argc, argv);
if (!fxl::SetLogSettingsFromCommandLine(command_line))
return 1;
const auto &positional_args = command_line.positional_args();
if (positional_args.size() != 0) {
FX_LOGS(ERROR) << "Usage: gltf_export\n"
<< "Takes a snapshot in glTF format and writes it to stdout.\n"
<< "To write to a file, redirect stdout, e.g.: "
<< "gltf_export > \"${DST}\"";
return 1;
}
async::Loop loop(&kAsyncLoopConfigAttachToCurrentThread);
trace::TraceProviderWithFdio trace_provider(loop.dispatcher());
SnapshotTaker taker(&loop);
taker.TakeSnapshot();
loop.Run();
return taker.encountered_error() ? EXIT_FAILURE : EXIT_SUCCESS;
}
////////////////////////////////////////////////////////////////////////////////
// PNG encode.
bool RawToPNG(size_t width, size_t height, const uint8_t *data, std::vector<uint8_t> &out) {
out.clear();
png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
png_infop info_ptr = png_create_info_struct(png_ptr);
if (setjmp(png_jmpbuf(png_ptr))) {
png_destroy_write_struct(&png_ptr, NULL);
FX_LOGS(ERROR) << "Unable to create write struct";
return false;
}
png_set_IHDR(png_ptr, info_ptr, width, height, 8, PNG_COLOR_TYPE_RGBA, PNG_INTERLACE_NONE,
PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
// Image snapshots are always in sRGB color space.
png_set_sRGB(png_ptr, info_ptr, PNG_sRGB_INTENT_PERCEPTUAL);
std::vector<uint8_t *> rows(height);
for (size_t y = 0; y < height; ++y) {
rows[y] = (uint8_t *)data + y * width * 4;
}
png_set_rows(png_ptr, info_ptr, &rows[0]);
png_set_write_fn(
png_ptr, &out,
[](png_structp png_ptr, png_bytep data, png_size_t length) {
std::vector<uint8_t> *p = (std::vector<uint8_t> *)png_get_io_ptr(png_ptr);
p->insert(p->end(), data, data + length);
},
NULL);
png_write_png(png_ptr, info_ptr, PNG_TRANSFORM_BGR, NULL);
if (setjmp(png_jmpbuf(png_ptr))) {
png_destroy_write_struct(&png_ptr, NULL);
FX_LOGS(ERROR) << "Unable to create write png";
return false;
}
png_destroy_write_struct(&png_ptr, NULL);
return true;
}
////////////////////////////////////////////////////////////////////////////////
// Base64 encode.
std::string Base64Encode(const uint8_t *bytes, size_t size) {
std::string encoded(modp_b64_encode_len(size), '\0');
size_t trim_begin = modp_b64_encode(const_cast<char *>(encoded.data()),
reinterpret_cast<const char *>(bytes), size);
encoded.erase(trim_begin, std::string::npos);
return encoded;
}