1010 lines
40 KiB
C++
1010 lines
40 KiB
C++
/*
|
|
* Copyright (C) 2019 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
#define GLTFIO_SIMPLEVIEWER_IMPLEMENTATION
|
|
|
|
#include "app/Config.h"
|
|
#include "app/FilamentApp.h"
|
|
#include "app/IBL.h"
|
|
|
|
#include <filagui/ImGuiMath.h>
|
|
|
|
#include <filament/Camera.h>
|
|
#include <filament/Engine.h>
|
|
#include <filament/IndexBuffer.h>
|
|
#include <filament/Scene.h>
|
|
#include <filament/View.h>
|
|
#include <filament/VertexBuffer.h>
|
|
|
|
#include <gltfio/AssetLoader.h>
|
|
#include <gltfio/AssetPipeline.h>
|
|
#include <gltfio/FilamentAsset.h>
|
|
#include <gltfio/ResourceLoader.h>
|
|
#include <gltfio/SimpleViewer.h>
|
|
|
|
#include <image/ImageOps.h>
|
|
#include <image/LinearImage.h>
|
|
|
|
#include <imageio/ImageEncoder.h>
|
|
|
|
#include <utils/NameComponentManager.h>
|
|
#include <utils/JobSystem.h>
|
|
|
|
#include <math/vec2.h>
|
|
|
|
#include <getopt/getopt.h>
|
|
|
|
#include <atomic>
|
|
#include <functional>
|
|
#include <fstream>
|
|
#include <iostream>
|
|
#include <string>
|
|
|
|
#include "generated/resources/resources.h"
|
|
|
|
using namespace filament;
|
|
using namespace gltfio;
|
|
using namespace utils;
|
|
using filament::math::ushort2;
|
|
|
|
enum class Visualization : int {
|
|
MESH_CURRENT,
|
|
MESH_VERTEX_NORMALS,
|
|
MESH_MODIFIED,
|
|
MESH_PREVIEW_AO,
|
|
MESH_PREVIEW_UV,
|
|
MESH_GBUFFER_NORMALS,
|
|
IMAGE_OCCLUSION,
|
|
IMAGE_BENT_NORMALS,
|
|
IMAGE_GBUFFER_NORMALS
|
|
};
|
|
|
|
static const char* DEFAULT_IBL = "venetian_crossroads_2k";
|
|
static const char* INI_FILENAME = "gltf_baker.ini";
|
|
static const char* TMP_UV_FILENAME = "gltf_baker_tmp_uv.png";
|
|
static const char* TMP_AO_FILENAME = "gltf_baker_tmp_ao.png";
|
|
static const char* TMP_NORMALS_FILENAME = "gltf_baker_tmp_mn.png";
|
|
static constexpr int PATH_SIZE = 256;
|
|
|
|
struct BakerApp;
|
|
|
|
using BakerAppTask = std::function<void(BakerApp*)>;
|
|
|
|
struct BakerApp {
|
|
Config config;
|
|
Engine* engine = nullptr;
|
|
Camera* camera = nullptr;
|
|
SimpleViewer* viewer = nullptr;
|
|
NameComponentManager* names = nullptr;
|
|
MaterialProvider* materials = nullptr;
|
|
AssetLoader* loader = nullptr;
|
|
gltfio::AssetPipeline* pipeline = nullptr;
|
|
bool viewerActualSize = false;
|
|
utils::Path filename;
|
|
bool hasTestRender = false;
|
|
bool isWorking = false;
|
|
std::string statusText;
|
|
ImVec4 statusColor;
|
|
std::string messageBoxText;
|
|
bool requestViewerUpdate = false;
|
|
Visualization visualization = Visualization::MESH_CURRENT;
|
|
|
|
// Bundle of Filament entities (renderables, textures, etc.) for the currently displayed mesh.
|
|
FilamentAsset* viewerAsset = nullptr;
|
|
|
|
// Available glTF scenes suitable for display, depending on "visualization".
|
|
gltfio::AssetPipeline::AssetHandle flattenedAsset = nullptr;
|
|
gltfio::AssetPipeline::AssetHandle parameterizedAsset = nullptr;
|
|
gltfio::AssetPipeline::AssetHandle modifiedAsset = nullptr;
|
|
gltfio::AssetPipeline::AssetHandle previewAoAsset = nullptr;
|
|
gltfio::AssetPipeline::AssetHandle previewUvAsset = nullptr;
|
|
gltfio::AssetPipeline::AssetHandle normalsAsset = nullptr;
|
|
|
|
// Available 2D images suitable for display, depending on "visualization".
|
|
image::LinearImage ambientOcclusion;
|
|
image::LinearImage bentNormals;
|
|
image::LinearImage meshNormals;
|
|
image::LinearImage meshPositions;
|
|
|
|
// AssetPipeline callbacks are triggered from outside the UI thread. To keep things simple, we
|
|
// defer their execution until the next iteration of the main loop. We store only one item per
|
|
// callback type, which provides the side benefit of skipping callbacks that occur more than
|
|
// once per frame. We use std::function rather than raw C function pointers to allow simple
|
|
// lambdas with captures.
|
|
std::atomic<BakerAppTask*> onDone;
|
|
std::atomic<BakerAppTask*> onTile;
|
|
|
|
struct {
|
|
uint32_t resolution = 1024;
|
|
size_t samplesPerPixel = 256;
|
|
float aoRayNear = std::numeric_limits<float>::epsilon() * 10.0f;
|
|
bool dilateCharts = true;
|
|
bool applyDenoiser = true;
|
|
int maxIterations = 2;
|
|
} bakeOptions;
|
|
|
|
struct {
|
|
Visualization selection = Visualization::MESH_MODIFIED;
|
|
char outputFolder[PATH_SIZE];
|
|
char gltfPath[PATH_SIZE];
|
|
char binPath[PATH_SIZE];
|
|
char occlusionPath[PATH_SIZE];
|
|
char bentNormalsPath[PATH_SIZE];
|
|
} exportOptions;
|
|
|
|
struct {
|
|
View* view = nullptr;
|
|
Scene* scene = nullptr;
|
|
VertexBuffer* vb = nullptr;
|
|
IndexBuffer* ib = nullptr;
|
|
Texture* texture = nullptr;
|
|
MaterialInstance* material = nullptr;
|
|
utils::Entity entity;
|
|
} overlayQuad;
|
|
};
|
|
|
|
#define makeTileCallback(FN) [](ushort2, ushort2, void* userData) { \
|
|
BakerApp* app = (BakerApp*) userData; \
|
|
BakerAppTask* previous = app->onTile.exchange(new BakerAppTask(FN)); \
|
|
delete previous; \
|
|
}
|
|
|
|
#define makeDoneCallback(FN) [](void* userData) { \
|
|
BakerApp* app = (BakerApp*) userData; \
|
|
BakerAppTask* previous = app->onDone.exchange(new BakerAppTask(FN)); \
|
|
delete previous; \
|
|
}
|
|
|
|
static void printUsage(char* name) {
|
|
std::string exec_name(Path(name).getName());
|
|
std::string usage(
|
|
"BAKER can perform AO baking on the specified glTF file. If no file is specified,"
|
|
"it loads the most recently-used glTF file.\n"
|
|
"Usage:\n"
|
|
" BAKER [options] [gltf path]\n"
|
|
"Options:\n"
|
|
" --help, -h\n"
|
|
" Prints this message\n\n"
|
|
" --actual-size, -s\n"
|
|
" Do not scale the model to fit into a unit cube in the viewer\n\n"
|
|
);
|
|
const std::string from("BAKER");
|
|
for (size_t pos = usage.find(from); pos != std::string::npos; pos = usage.find(from, pos)) {
|
|
usage.replace(pos, from.length(), exec_name);
|
|
}
|
|
std::cout << usage;
|
|
}
|
|
|
|
static int handleCommandLineArguments(int argc, char* argv[], BakerApp* app) {
|
|
static constexpr const char* OPTSTR = "ha:i:us";
|
|
static const struct option OPTIONS[] = {
|
|
{ "help", no_argument, nullptr, 'h' },
|
|
{ "actual-size", no_argument, nullptr, 's' },
|
|
{ nullptr, 0, nullptr, 0 }
|
|
};
|
|
int opt;
|
|
int option_index = 0;
|
|
while ((opt = getopt_long(argc, argv, OPTSTR, OPTIONS, &option_index)) >= 0) {
|
|
std::string arg(optarg ? optarg : "");
|
|
switch (opt) {
|
|
default:
|
|
case 'h':
|
|
printUsage(argv[0]);
|
|
exit(0);
|
|
case 's':
|
|
app->viewerActualSize = true;
|
|
break;
|
|
}
|
|
}
|
|
return optind;
|
|
}
|
|
|
|
static std::ifstream::pos_type getFileSize(const char* filename) {
|
|
std::ifstream in(filename, std::ifstream::ate | std::ifstream::binary);
|
|
return in.tellg();
|
|
}
|
|
|
|
static void saveIniFile(BakerApp& app) {
|
|
std::ofstream out(INI_FILENAME);
|
|
out << "[recent]\n";
|
|
out << "filename=" << app.filename.c_str() << "\n";
|
|
}
|
|
|
|
static void loadIniFile(BakerApp& app) {
|
|
utils::Path iniPath(INI_FILENAME);
|
|
if (!app.filename.isEmpty() || !iniPath.isFile()) {
|
|
return;
|
|
}
|
|
std::ifstream infile(INI_FILENAME);
|
|
std::string line;
|
|
while (std::getline(infile, line)) {
|
|
size_t sep = line.find('=');
|
|
if (sep != std::string::npos) {
|
|
std::string lhs = line.substr(0, sep);
|
|
std::string rhs = line.substr(sep + 1);
|
|
if (lhs == "filename") {
|
|
utils::Path gltf = rhs;
|
|
if (gltf.isFile()) {
|
|
app.filename = rhs;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void createQuadRenderable(BakerApp& app) {
|
|
auto& rcm = app.engine->getRenderableManager();
|
|
Engine& engine = *app.engine;
|
|
|
|
struct OverlayVertex {
|
|
filament::math::float2 position;
|
|
filament::math::float2 uv;
|
|
};
|
|
static OverlayVertex kVertices[4] = {
|
|
{{0, 0}, {0, 0}}, {{ 1000, 0}, {1, 0}}, {{0, 1000}, {0, 1}}, {{ 1000, 1000}, {1, 1}}
|
|
};
|
|
static constexpr uint16_t kIndices[6] = { 0, 1, 2, 3, 2, 1 };
|
|
|
|
if (!app.overlayQuad.entity) {
|
|
app.overlayQuad.vb = VertexBuffer::Builder()
|
|
.vertexCount(4)
|
|
.bufferCount(1)
|
|
.attribute(VertexAttribute::POSITION, 0, VertexBuffer::AttributeType::FLOAT2, 0, 16)
|
|
.attribute(VertexAttribute::UV0, 0, VertexBuffer::AttributeType::FLOAT2, 8, 16)
|
|
.build(engine);
|
|
app.overlayQuad.ib = IndexBuffer::Builder()
|
|
.indexCount(6)
|
|
.bufferType(IndexBuffer::IndexType::USHORT)
|
|
.build(engine);
|
|
app.overlayQuad.ib->setBuffer(engine,
|
|
IndexBuffer::BufferDescriptor(kIndices, 12, nullptr));
|
|
auto mat = Material::Builder()
|
|
.package(RESOURCES_AOPREVIEW_DATA, RESOURCES_AOPREVIEW_SIZE)
|
|
.build(engine);
|
|
app.overlayQuad.material = mat->createInstance();
|
|
app.overlayQuad.entity = EntityManager::get().create();
|
|
}
|
|
|
|
constexpr int margin = 20;
|
|
const int sidebar = app.viewer->getSidebarWidth();
|
|
const auto size = ImGui::GetIO().DisplaySize - ImVec2(sidebar + margin * 2, margin * 2);
|
|
kVertices[0].position.x = sidebar + margin;
|
|
kVertices[1].position.x = sidebar + margin + size.x;
|
|
kVertices[2].position.x = sidebar + margin;
|
|
kVertices[3].position.x = sidebar + margin + size.x;
|
|
kVertices[0].position.y = margin;
|
|
kVertices[1].position.y = margin;
|
|
kVertices[2].position.y = margin + size.y;
|
|
kVertices[3].position.y = margin + size.y;
|
|
|
|
auto vb = app.overlayQuad.vb;
|
|
auto ib = app.overlayQuad.ib;
|
|
vb->setBufferAt(*app.engine, 0, VertexBuffer::BufferDescriptor(kVertices, 64, nullptr));
|
|
rcm.destroy(app.overlayQuad.entity);
|
|
RenderableManager::Builder(1)
|
|
.boundingBox({{ 0, 0, 0 }, { 1000, 1000, 1 }})
|
|
.material(0, app.overlayQuad.material)
|
|
.geometry(0, RenderableManager::PrimitiveType::TRIANGLES, vb, ib, 0, 6)
|
|
.culling(false)
|
|
.receiveShadows(false)
|
|
.castShadows(false)
|
|
.build(*app.engine, app.overlayQuad.entity);
|
|
}
|
|
|
|
static void updateViewerMesh(BakerApp& app) {
|
|
gltfio::AssetPipeline::AssetHandle handle;
|
|
switch (app.visualization) {
|
|
case Visualization::MESH_CURRENT: handle = app.flattenedAsset; break;
|
|
case Visualization::MESH_VERTEX_NORMALS: handle = app.flattenedAsset; break;
|
|
case Visualization::MESH_MODIFIED: handle = app.modifiedAsset; break;
|
|
case Visualization::MESH_PREVIEW_AO: handle = app.previewAoAsset; break;
|
|
case Visualization::MESH_PREVIEW_UV: handle = app.previewUvAsset; break;
|
|
case Visualization::MESH_GBUFFER_NORMALS: handle = app.normalsAsset; break;
|
|
default: return;
|
|
}
|
|
|
|
if (!app.viewerAsset || app.viewerAsset->getSourceAsset() != handle) {
|
|
auto previousViewerAsset = app.viewerAsset;
|
|
app.viewerAsset = app.loader->createAssetFromHandle(handle);
|
|
|
|
// Load external textures and buffers.
|
|
gltfio::ResourceLoader({
|
|
.engine = app.engine,
|
|
.gltfPath = app.filename.getAbsolutePath().c_str(),
|
|
.normalizeSkinningWeights = true,
|
|
.recomputeBoundingBoxes = false
|
|
}).loadResources(app.viewerAsset);
|
|
|
|
// Load animation data then free the source hierarchy.
|
|
app.viewerAsset->getAnimator();
|
|
|
|
// Remove old renderables and add new renderables to the scene.
|
|
app.viewer->populateScene(app.viewerAsset, !app.viewerActualSize);
|
|
|
|
// Destory old Filament entities.
|
|
app.loader->destroyAsset(previousViewerAsset);
|
|
}
|
|
}
|
|
|
|
static void updateViewerImage(BakerApp& app) {
|
|
Engine& engine = *app.engine;
|
|
using MinFilter = TextureSampler::MinFilter;
|
|
using MagFilter = TextureSampler::MagFilter;
|
|
|
|
// Gather information about the displayed image.
|
|
image::LinearImage image;
|
|
switch (app.visualization) {
|
|
case Visualization::IMAGE_OCCLUSION:
|
|
image = app.ambientOcclusion;
|
|
break;
|
|
case Visualization::IMAGE_BENT_NORMALS:
|
|
image = app.bentNormals;
|
|
break;
|
|
case Visualization::IMAGE_GBUFFER_NORMALS:
|
|
image = app.meshNormals;
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
const int width = image.getWidth();
|
|
const int height = image.getHeight();
|
|
const int channels = image.getChannels();
|
|
const void* data = image.getPixelRef();
|
|
const Texture::InternalFormat internalFormat = channels == 1 ?
|
|
Texture::InternalFormat::R32F : Texture::InternalFormat::RGB32F;
|
|
const Texture::Format format = channels == 1 ? Texture::Format::R : Texture::Format::RGB;
|
|
|
|
// Create a brand new texture object if necessary.
|
|
const Texture* tex = app.overlayQuad.texture;
|
|
if (!tex || tex->getWidth() != width || tex->getHeight() != height ||
|
|
tex->getFormat() != internalFormat) {
|
|
engine.destroy(tex);
|
|
app.overlayQuad.texture = Texture::Builder()
|
|
.width(width)
|
|
.height(height)
|
|
.levels(1)
|
|
.sampler(Texture::Sampler::SAMPLER_2D)
|
|
.format(internalFormat)
|
|
.build(engine);
|
|
TextureSampler sampler(MinFilter::LINEAR, MagFilter::LINEAR);
|
|
app.overlayQuad.material->setParameter("luma", app.overlayQuad.texture, sampler);
|
|
app.overlayQuad.material->setParameter("grayscale", channels == 1);
|
|
}
|
|
|
|
// Upload texture data.
|
|
Texture::PixelBufferDescriptor buffer(data, size_t(width * height * channels * sizeof(float)),
|
|
format, Texture::Type::FLOAT);
|
|
app.overlayQuad.texture->setImage(engine, 0, std::move(buffer));
|
|
}
|
|
|
|
static void updateViewer(BakerApp& app) {
|
|
switch (app.visualization) {
|
|
case Visualization::MESH_CURRENT:
|
|
case Visualization::MESH_VERTEX_NORMALS:
|
|
case Visualization::MESH_MODIFIED:
|
|
case Visualization::MESH_PREVIEW_AO:
|
|
case Visualization::MESH_PREVIEW_UV:
|
|
case Visualization::MESH_GBUFFER_NORMALS:
|
|
updateViewerMesh(app);
|
|
break;
|
|
case Visualization::IMAGE_OCCLUSION:
|
|
case Visualization::IMAGE_BENT_NORMALS:
|
|
case Visualization::IMAGE_GBUFFER_NORMALS:
|
|
updateViewerImage(app);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void loadAssetFromDisk(BakerApp& app) {
|
|
std::cout << "Loading " << app.filename << "..." << std::endl;
|
|
if (app.filename.getExtension() == "glb") {
|
|
std::cerr << "GLB files are not yet supported." << std::endl;
|
|
exit(1);
|
|
}
|
|
|
|
auto pipeline = new gltfio::AssetPipeline();
|
|
gltfio::AssetPipeline::AssetHandle handle = pipeline->load(app.filename);
|
|
if (!handle) {
|
|
delete pipeline;
|
|
std::cerr << "Unable to load model" << std::endl;
|
|
exit(1);
|
|
}
|
|
|
|
if (!gltfio::AssetPipeline::isFlattened(handle)) {
|
|
handle = pipeline->flatten(handle, AssetPipeline::FILTER_TRIANGLES);
|
|
if (!handle) {
|
|
delete pipeline;
|
|
std::cerr << "Unable to flatten model" << std::endl;
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
// Destroy the previous pipeline to free up resources used by the previous asset.
|
|
delete app.pipeline;
|
|
app.pipeline = pipeline;
|
|
|
|
app.viewer->setIndirectLight(FilamentApp::get().getIBL()->getIndirectLight(), nullptr);
|
|
app.flattenedAsset = handle;
|
|
app.requestViewerUpdate = true;
|
|
|
|
// Update the window title bar and the default output path.
|
|
const utils::Path defaultFolder = app.filename.getAbsolutePath().getParent();
|
|
strncpy(app.exportOptions.outputFolder, defaultFolder.c_str(), PATH_SIZE);
|
|
FilamentApp::get().setWindowTitle(app.filename.getName().c_str());
|
|
}
|
|
|
|
static void executeTestRender(BakerApp& app) {
|
|
app.isWorking = true;
|
|
app.hasTestRender = true;
|
|
gltfio::AssetPipeline::AssetHandle currentAsset = app.flattenedAsset;
|
|
|
|
// Allocate the render target for the path tracer as well as a GPU texture to display it.
|
|
auto viewportSize = ImGui::GetIO().DisplaySize;
|
|
viewportSize.x -= app.viewer->getSidebarWidth();
|
|
app.ambientOcclusion = image::LinearImage((uint32_t) viewportSize.x,
|
|
(uint32_t) viewportSize.y, 1);
|
|
app.statusText.clear();
|
|
app.visualization = Visualization::IMAGE_OCCLUSION;
|
|
|
|
// Compute the camera parameters for the path tracer.
|
|
// ---------------------------------------------------
|
|
// The path tracer does not know about the top-level Filament transform that we use to fit the
|
|
// model into a unit cube (see the -s option), so here we do little trick by temporarily
|
|
// transforming the Filament camera before grabbing its lookAt vectors.
|
|
auto& tcm = app.engine->getTransformManager();
|
|
auto root = tcm.getInstance(app.viewerAsset->getRoot());
|
|
auto cam = tcm.getInstance(app.camera->getEntity());
|
|
filament::math::mat4f prev = tcm.getTransform(root);
|
|
tcm.setTransform(root, inverse(prev));
|
|
tcm.setParent(cam, root);
|
|
filament::rays::SimpleCamera camera = {
|
|
.aspectRatio = viewportSize.x / viewportSize.y,
|
|
.eyePosition = app.camera->getPosition(),
|
|
.targetPosition = app.camera->getPosition() + app.camera->getForwardVector(),
|
|
.upVector = app.camera->getUpVector(),
|
|
.vfovDegrees = 45, // NOTE: fov is not queryable, must match with FilamentApp
|
|
};
|
|
tcm.setParent(cam, {});
|
|
tcm.setTransform(root, prev);
|
|
|
|
// Finally, set up some callbacks and invoke the path tracer.
|
|
auto onRenderTile = makeTileCallback([](BakerApp* app) {
|
|
app->requestViewerUpdate = true;
|
|
});
|
|
auto onRenderDone = makeDoneCallback([](BakerApp* app) {
|
|
app->requestViewerUpdate = true;
|
|
app->isWorking = false;
|
|
});
|
|
app.pipeline->renderAmbientOcclusion(currentAsset, app.ambientOcclusion, camera, {
|
|
.progress = onRenderTile,
|
|
.done = onRenderDone,
|
|
.userData = &app,
|
|
.samplesPerPixel = app.bakeOptions.samplesPerPixel,
|
|
.aoRayNear = app.bakeOptions.aoRayNear,
|
|
.enableDenoise = app.bakeOptions.applyDenoiser
|
|
});
|
|
}
|
|
|
|
static void generateUvVisualization(const utils::Path& pngOutputPath) {
|
|
using namespace image;
|
|
LinearImage uvimage(256, 256, 3);
|
|
for (int y = 0, h = uvimage.getHeight(); y < h; ++y) {
|
|
for (int x = 0, w = uvimage.getWidth(); x < w; ++x) {
|
|
float* dst = uvimage.getPixelRef(x, y);
|
|
dst[0] = float(x) / w;
|
|
dst[1] = float(y) / h;
|
|
dst[2] = 1.0f;
|
|
}
|
|
}
|
|
std::ofstream out(pngOutputPath.c_str(), std::ios::binary | std::ios::trunc);
|
|
ImageEncoder::encode(out, ImageEncoder::Format::PNG_LINEAR, uvimage, "",
|
|
pngOutputPath.c_str());
|
|
}
|
|
|
|
static void executeBakeAo(BakerApp& app) {
|
|
using namespace image;
|
|
|
|
auto onRenderTile = makeTileCallback([](BakerApp* app) {
|
|
app->requestViewerUpdate = true;
|
|
});
|
|
|
|
auto onRenderDone = makeDoneCallback([](BakerApp* app) {
|
|
gltfio::AssetPipeline* pipeline = app->pipeline;
|
|
gltfio::AssetPipeline::AssetHandle asset = app->parameterizedAsset;
|
|
app->requestViewerUpdate = true;
|
|
|
|
// Generate a simple red-green UV visualization texture.
|
|
const utils::Path folder = app->filename.getAbsolutePath().getParent();
|
|
generateUvVisualization(folder + TMP_UV_FILENAME);
|
|
app->previewUvAsset = pipeline->generatePreview(asset, TMP_UV_FILENAME);
|
|
|
|
// Export the generated AO texture.
|
|
const utils::Path tmpOcclusionPath = folder + TMP_AO_FILENAME;
|
|
std::ofstream out(tmpOcclusionPath.c_str(), std::ios::binary | std::ios::trunc);
|
|
ImageEncoder::encode(out, ImageEncoder::Format::PNG_LINEAR, app->ambientOcclusion,
|
|
"", tmpOcclusionPath.c_str());
|
|
|
|
// Export the mesh normals texture.
|
|
const utils::Path tmpNormalsPath = folder + TMP_NORMALS_FILENAME;
|
|
out = std::ofstream(tmpNormalsPath.c_str(), std::ios::binary | std::ios::trunc);
|
|
ImageEncoder::encode(out, ImageEncoder::Format::PNG_LINEAR, app->meshNormals,
|
|
"", tmpNormalsPath.c_str());
|
|
|
|
app->previewAoAsset = pipeline->generatePreview(asset, TMP_AO_FILENAME);
|
|
app->modifiedAsset = pipeline->replaceOcclusion(asset, TMP_AO_FILENAME);
|
|
app->normalsAsset = pipeline->generatePreview(asset, TMP_NORMALS_FILENAME);
|
|
app->isWorking = false;
|
|
});
|
|
|
|
auto doRender = [&app, onRenderTile, onRenderDone] {
|
|
const uint32_t res = app.bakeOptions.resolution;
|
|
app.statusText.clear();
|
|
app.hasTestRender = false;
|
|
app.visualization = Visualization::IMAGE_OCCLUSION;
|
|
app.ambientOcclusion = image::LinearImage(res, res, 1);
|
|
app.bentNormals = image::LinearImage(res, res, 3);
|
|
app.meshNormals = image::LinearImage(res, res, 3);
|
|
app.meshPositions = image::LinearImage(res, res, 3);
|
|
image::LinearImage outputs[] = {
|
|
app.ambientOcclusion, app.bentNormals, app.meshNormals, app.meshPositions
|
|
};
|
|
app.pipeline->bakeAllOutputs(app.parameterizedAsset, outputs, {
|
|
.progress = onRenderTile,
|
|
.done = onRenderDone,
|
|
.userData = &app,
|
|
.samplesPerPixel = app.bakeOptions.samplesPerPixel,
|
|
.aoRayNear = app.bakeOptions.aoRayNear,
|
|
.enableDenoise = app.bakeOptions.applyDenoiser,
|
|
.enableDilation = app.bakeOptions.dilateCharts
|
|
});
|
|
};
|
|
|
|
app.isWorking = true;
|
|
app.previewAoAsset = nullptr;
|
|
app.modifiedAsset = nullptr;
|
|
app.previewUvAsset = nullptr;
|
|
app.statusColor = ImVec4({0, 1, 0, 1});
|
|
app.statusText = "Parameterizing...";
|
|
|
|
utils::JobSystem* js = utils::JobSystem::getJobSystem();
|
|
utils::JobSystem::Job* parent = js->createJob();
|
|
utils::JobSystem::Job* prep = utils::jobs::createJob(*js, parent, [&app, doRender] {
|
|
auto parameterized = app.pipeline->parameterize(app.flattenedAsset,
|
|
app.bakeOptions.maxIterations);
|
|
auto callback = new BakerAppTask([doRender, parameterized](BakerApp* app) {
|
|
if (!parameterized) {
|
|
app->messageBoxText = "Unable to parameterize, check terminal output for details.";
|
|
app->isWorking = false;
|
|
return;
|
|
}
|
|
app->parameterizedAsset = parameterized;
|
|
app->requestViewerUpdate = true;
|
|
doRender();
|
|
});
|
|
BakerAppTask* previous = app.onDone.exchange(callback);
|
|
delete previous;
|
|
});
|
|
js->run(prep);
|
|
}
|
|
|
|
static void executeExport(BakerApp& app) {
|
|
const auto& options = app.exportOptions;
|
|
const utils::Path folder = options.outputFolder;
|
|
const utils::Path binPath = folder + options.binPath;
|
|
const utils::Path gltfPath = folder + options.gltfPath;
|
|
const utils::Path occlusionPath = folder + options.occlusionPath;
|
|
const utils::Path bentNormalsPath = folder + options.bentNormalsPath;
|
|
|
|
auto exportOcclusion = [&app, occlusionPath]() {
|
|
using namespace image;
|
|
std::ofstream out(occlusionPath.c_str(), std::ios::binary | std::ios::trunc);
|
|
return ImageEncoder::encode(out, ImageEncoder::Format::PNG_LINEAR, app.ambientOcclusion, "",
|
|
occlusionPath.c_str());
|
|
};
|
|
|
|
auto exportBentNormals = [&app, bentNormalsPath]() {
|
|
using namespace image;
|
|
std::ofstream out(bentNormalsPath.c_str(), std::ios::binary | std::ios::trunc);
|
|
return ImageEncoder::encode(out, ImageEncoder::Format::PNG_LINEAR, app.bentNormals, "",
|
|
bentNormalsPath.c_str());
|
|
};
|
|
|
|
std::string msg;
|
|
bool error = false;
|
|
const std::string join = ", ";
|
|
switch (options.selection) {
|
|
case Visualization::MESH_CURRENT:
|
|
error = error || !app.pipeline->save(app.flattenedAsset, gltfPath, binPath);
|
|
msg = options.gltfPath + join + options.binPath;
|
|
break;
|
|
case Visualization::MESH_MODIFIED:
|
|
error = error || !exportOcclusion();
|
|
app.pipeline->setOcclusionUri(app.modifiedAsset, options.occlusionPath);
|
|
error = error || !app.pipeline->save(app.modifiedAsset, gltfPath, binPath);
|
|
app.pipeline->setOcclusionUri(app.modifiedAsset, TMP_AO_FILENAME);
|
|
msg = options.gltfPath + join + options.binPath + join + options.occlusionPath;
|
|
break;
|
|
case Visualization::MESH_PREVIEW_AO:
|
|
error = error || !exportOcclusion();
|
|
app.pipeline->setBaseColorUri(app.previewAoAsset, options.occlusionPath);
|
|
error = error || !app.pipeline->save(app.previewAoAsset, gltfPath, binPath);
|
|
app.pipeline->setBaseColorUri(app.previewAoAsset, TMP_AO_FILENAME);
|
|
msg = options.gltfPath + join + options.binPath + join + options.occlusionPath;
|
|
break;
|
|
case Visualization::IMAGE_OCCLUSION:
|
|
error = error || !exportOcclusion();
|
|
msg = options.occlusionPath;
|
|
break;
|
|
case Visualization::IMAGE_BENT_NORMALS:
|
|
error = error || !exportBentNormals();
|
|
msg = options.bentNormalsPath;
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
app.statusColor = error ? ImVec4({1, 0, 0, 1}) : ImVec4({0, 1, 0, 1});
|
|
app.statusText = (error ? "Failed export to " : "Exported ") + msg;
|
|
}
|
|
|
|
int main(int argc, char** argv) {
|
|
BakerApp app;
|
|
|
|
app.onDone.exchange(nullptr);
|
|
app.onTile.exchange(nullptr);
|
|
|
|
strncpy(app.exportOptions.gltfPath, "baked.gltf", PATH_SIZE);
|
|
strncpy(app.exportOptions.binPath, "baked.bin", PATH_SIZE);
|
|
strncpy(app.exportOptions.occlusionPath, "occlusion.png", PATH_SIZE);
|
|
strncpy(app.exportOptions.bentNormalsPath, "bentNormals.png", PATH_SIZE);
|
|
|
|
app.config.title = "gltf_baker";
|
|
app.config.iblDirectory = FilamentApp::getRootAssetsPath() + DEFAULT_IBL;
|
|
|
|
utils::Path filename;
|
|
int option_index = handleCommandLineArguments(argc, argv, &app);
|
|
int num_args = argc - option_index;
|
|
if (num_args >= 1) {
|
|
filename = argv[option_index];
|
|
if (!filename.exists()) {
|
|
std::cerr << "file " << app.filename << " not found!" << std::endl;
|
|
return 1;
|
|
}
|
|
if (filename.isDirectory()) {
|
|
auto files = filename.listContents();
|
|
for (const auto& file : files) {
|
|
if (file.getExtension() == "gltf") {
|
|
app.filename = file;
|
|
break;
|
|
}
|
|
}
|
|
if (filename.isDirectory()) {
|
|
std::cerr << "no glTF file found in " << filename << std::endl;
|
|
return 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
app.filename = filename;
|
|
loadIniFile(app);
|
|
|
|
auto setup = [&](Engine* engine, View* view, Scene* scene) {
|
|
app.engine = engine;
|
|
app.names = new NameComponentManager(EntityManager::get());
|
|
|
|
const int kInitialSidebarWidth = 322;
|
|
app.viewer = new SimpleViewer(engine, scene, view, SimpleViewer::FLAG_COLLAPSED,
|
|
kInitialSidebarWidth);
|
|
app.viewer->enableSunlight(false);
|
|
app.viewer->enableSSAO(false);
|
|
app.viewer->setIBLIntensity(50000.0f);
|
|
|
|
app.materials = createMaterialGenerator(engine);
|
|
app.loader = AssetLoader::create({engine, app.materials, app.names });
|
|
app.loader->enableDiagnostics();
|
|
app.camera = &view->getCamera();
|
|
|
|
if (!app.filename.isEmpty()) {
|
|
loadAssetFromDisk(app);
|
|
saveIniFile(app);
|
|
}
|
|
|
|
app.viewer->setUiCallback([&app] () {
|
|
const ImU32 disabledColor = ImColor(ImGui::GetStyle().Colors[ImGuiCol_TextDisabled]);
|
|
const ImU32 hoveredColor = ImColor(ImGui::GetStyle().Colors[ImGuiCol_ButtonHovered]);
|
|
const ImU32 enabledColor = ImColor(0.5f, 0.5f, 0.0f);
|
|
const ImVec2 buttonSize(100, 50);
|
|
const float buttonPositions[] = { 0, 2 + buttonSize.x, 4 + buttonSize.x * 2 };
|
|
ImVec2 pos;
|
|
ImU32 color;
|
|
bool enabled;
|
|
|
|
// Begin action buttons
|
|
ImGui::GetStyle().ItemSpacing.x = 1;
|
|
ImGui::GetStyle().FrameRounding = 10;
|
|
ImGui::PushStyleColor(ImGuiCol_Button, enabledColor);
|
|
ImGui::Spacing();
|
|
ImGui::Spacing();
|
|
ImGui::BeginGroup();
|
|
|
|
using OnClick = void(*)(BakerApp& app);
|
|
auto showActonButton = [&](const char* label, int cornerFlags, OnClick fn) {
|
|
pos = ImGui::GetCursorScreenPos();
|
|
color = enabled ? enabledColor : disabledColor;
|
|
color = ImGui::IsMouseHoveringRect(pos, pos + buttonSize) ? hoveredColor : color;
|
|
ImGui::GetWindowDrawList()->AddRectFilled(pos, pos + buttonSize, color,
|
|
ImGui::GetStyle().FrameRounding, cornerFlags);
|
|
ImGui::PushStyleColor(ImGuiCol_Button, color);
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, color);
|
|
if (ImGui::Button(label, buttonSize) && enabled) {
|
|
fn(app);
|
|
}
|
|
ImGui::PopStyleColor();
|
|
ImGui::PopStyleColor();
|
|
};
|
|
|
|
// TEST RENDER
|
|
ImGui::SameLine(buttonPositions[0]);
|
|
enabled = !app.isWorking;
|
|
showActonButton("Test Render", ImDrawCornerFlags_Left, [](BakerApp& app) {
|
|
executeTestRender(app);
|
|
});
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::SetTooltip("Renders the asset from the current camera using a pathtracer.");
|
|
}
|
|
|
|
// BAKE
|
|
ImGui::SameLine(buttonPositions[1]);
|
|
enabled = !app.isWorking;
|
|
showActonButton("Bake AO", 0, [](BakerApp& app) {
|
|
executeBakeAo(app);
|
|
});
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::SetTooltip("Generates a new set of UVs and invokes a pathtracer.");
|
|
}
|
|
|
|
// EXPORT
|
|
ImGui::SameLine(buttonPositions[2]);
|
|
enabled = !app.isWorking && !app.hasTestRender && app.modifiedAsset;
|
|
showActonButton("Export...", ImDrawCornerFlags_Right, [](BakerApp& app) {
|
|
ImGui::OpenPopup("Export options");
|
|
});
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::SetTooltip("Saves the baked result to disk.");
|
|
}
|
|
|
|
// End action buttons
|
|
ImGui::EndGroup();
|
|
ImGui::Spacing();
|
|
ImGui::Spacing();
|
|
ImGui::PopStyleColor();
|
|
ImGui::GetStyle().FrameRounding = 20;
|
|
ImGui::GetStyle().ItemSpacing.x = 8;
|
|
|
|
// Model stats
|
|
if (app.viewerAsset) {
|
|
filament::Aabb aabb = app.viewerAsset->getBoundingBox();
|
|
ImGui::TextColored({1, 0, 1, 1}, "min (%g, %g, %g)", aabb.min.x, aabb.min.y, aabb.min.z);
|
|
ImGui::TextColored({1, 0, 1, 1}, "max (%g, %g, %g)", aabb.max.x, aabb.max.y, aabb.max.z);
|
|
ImGui::Spacing();
|
|
}
|
|
|
|
// Status text
|
|
if (app.statusText.size()) {
|
|
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, {10, 10} );
|
|
ImGui::TextColored(app.statusColor, "%s", app.statusText.c_str());
|
|
ImGui::PopStyleVar();
|
|
if (app.isWorking) {
|
|
static float fraction = 0;
|
|
fraction = fmod(fraction + 0.1f, 2.0f * M_PI);
|
|
ImGui::ProgressBar(sin(fraction) * 0.5f + 0.5f, {buttonSize.x * 3, 5.0f}, "");
|
|
}
|
|
ImGui::Spacing();
|
|
}
|
|
|
|
// Results
|
|
auto addOption = [&app](const char* msg, char num, Visualization e) {
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
int* ptr = (int*) &app.visualization;
|
|
if (io.InputCharacters[0] == num) { app.visualization = e; }
|
|
ImGui::RadioButton(msg, ptr, (int) e);
|
|
ImGui::SameLine();
|
|
ImGui::TextColored({1, 1, 0,1 }, "%c", num);
|
|
};
|
|
if (app.ambientOcclusion && ImGui::CollapsingHeader("Results",
|
|
ImGuiTreeNodeFlags_DefaultOpen)) {
|
|
ImGui::Indent();
|
|
const Visualization previousVisualization = app.visualization;
|
|
using RV = Visualization;
|
|
addOption("3D model with original materials", '1', RV::MESH_CURRENT);
|
|
addOption("3D model with vertex normals", '2', RV::MESH_VERTEX_NORMALS);
|
|
if (app.hasTestRender) {
|
|
addOption("Rendered AO test image", '3', RV::IMAGE_OCCLUSION);
|
|
} else if (!app.modifiedAsset) {
|
|
addOption("2D texture with occlusion", '3', RV::IMAGE_OCCLUSION);
|
|
addOption("2D texture with bent normals", '4', RV::IMAGE_BENT_NORMALS);
|
|
addOption("2D texture with mesh normals", '5', RV::IMAGE_GBUFFER_NORMALS);
|
|
} else {
|
|
addOption("3D model with modified materials", '3', RV::MESH_MODIFIED);
|
|
addOption("3D model with new occlusion only", '4', RV::MESH_PREVIEW_AO);
|
|
addOption("3D model with UV visualization", '5', RV::MESH_PREVIEW_UV);
|
|
addOption("3D model with gbuffer normals", '6', RV::MESH_GBUFFER_NORMALS);
|
|
addOption("2D texture with occlusion", '7', RV::IMAGE_OCCLUSION);
|
|
addOption("2D texture with bent normals", '8', RV::IMAGE_BENT_NORMALS);
|
|
addOption("2D texture with gbuffer normals", '9', RV::IMAGE_GBUFFER_NORMALS);
|
|
}
|
|
if (app.visualization != previousVisualization) {
|
|
app.requestViewerUpdate = true;
|
|
}
|
|
ImGui::Unindent();
|
|
ImGui::Spacing();
|
|
}
|
|
|
|
// Options
|
|
if (ImGui::CollapsingHeader("Bake Options")) {
|
|
int spp = app.bakeOptions.samplesPerPixel;
|
|
ImGui::InputInt("Samples per pixel", &spp);
|
|
app.bakeOptions.samplesPerPixel = spp;
|
|
|
|
static const int kFirstOption = (int) std::log2(512);
|
|
int bakeOption = (int) std::log2(app.bakeOptions.resolution) - kFirstOption;
|
|
ImGui::Combo("Texture size", &bakeOption,
|
|
"512 x 512\0"
|
|
"1024 x 1024\0"
|
|
"2048 x 2048\0");
|
|
app.bakeOptions.resolution = 1u << uint32_t(bakeOption + kFirstOption);
|
|
|
|
ImGui::InputFloat("Secondary ray tmin", &app.bakeOptions.aoRayNear,
|
|
std::numeric_limits<float>::epsilon(),
|
|
std::numeric_limits<float>::epsilon() * 10.0f, 10);
|
|
|
|
ImGui::InputInt("Max segmentation attempts", &app.bakeOptions.maxIterations);
|
|
ImGui::Checkbox("Dilate charts", &app.bakeOptions.dilateCharts);
|
|
ImGui::Checkbox("Apply denoiser", &app.bakeOptions.applyDenoiser);
|
|
}
|
|
|
|
// Modals
|
|
if (app.messageBoxText.size()) {
|
|
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, {10, 10} );
|
|
ImGui::OpenPopup("MessageBox");
|
|
if (ImGui::BeginPopupModal("MessageBox", nullptr,
|
|
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar)) {
|
|
ImGui::TextUnformatted(app.messageBoxText.c_str());
|
|
if (ImGui::Button("OK", ImVec2(120,0))) {
|
|
app.messageBoxText.clear();
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
ImGui::PopStyleVar();
|
|
}
|
|
|
|
if (ImGui::BeginPopupModal("Export options", nullptr,
|
|
ImGuiWindowFlags_AlwaysAutoResize)) {
|
|
using RV = Visualization;
|
|
auto& options = app.exportOptions;
|
|
ImGui::InputText("Output folder", options.outputFolder, PATH_SIZE);
|
|
ImGui::InputText("glTF filename", options.gltfPath, PATH_SIZE);
|
|
ImGui::InputText("Buffer data filename", options.binPath, PATH_SIZE);
|
|
ImGui::InputText("Occlusion image", options.occlusionPath, PATH_SIZE);
|
|
ImGui::InputText("Bent normals image", options.bentNormalsPath, PATH_SIZE);
|
|
|
|
auto radio = [&app](const char* name, RV value) {
|
|
int* ptr = (int*) &app.exportOptions.selection;
|
|
ImGui::RadioButton(name, ptr, (int) value);
|
|
};
|
|
radio("Export flattened glTF with original materials", RV::MESH_CURRENT);
|
|
radio("Export flattened glTF with modified materials", RV::MESH_MODIFIED);
|
|
radio("Export flattened glTF with new occlusion only", RV::MESH_PREVIEW_AO);
|
|
radio("Export occlusion image only", RV::IMAGE_OCCLUSION);
|
|
radio("Export bent normals image only", RV::IMAGE_BENT_NORMALS);
|
|
if (ImGui::Button("OK", ImVec2(120,0))) {
|
|
ImGui::CloseCurrentPopup();
|
|
executeExport(app);
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
});
|
|
|
|
// Leave FXAA enabled but we also enable MSAA for a nice result. The wireframe looks
|
|
// much better with MSAA enabled.
|
|
view->setSampleCount(4);
|
|
};
|
|
|
|
auto cleanup = [&app](Engine* engine, View*, Scene*) {
|
|
std::cout << "Destroying viewer..." << std::endl;
|
|
app.viewer->removeAsset();
|
|
delete app.viewer;
|
|
std::cout << "Destroying viewer asset..." << std::endl;
|
|
app.loader->destroyAsset(app.viewerAsset);
|
|
app.viewerAsset = nullptr;
|
|
std::cout << "Destroying pipeline..." << std::endl;
|
|
delete app.pipeline;
|
|
std::cout << "Destroying AssetLoader materials..." << std::endl;
|
|
app.materials->destroyMaterials();
|
|
delete app.materials;
|
|
std::cout << "Destroying AssetLoader..." << std::endl;
|
|
AssetLoader::destroy(&app.loader);
|
|
std::cout << "Destroying NameComponentManager..." << std::endl;
|
|
delete app.names;
|
|
};
|
|
|
|
auto animate = [&app](Engine* engine, View* view, double now) {
|
|
if (app.viewerAsset) {
|
|
|
|
// The baker doesn't support animation, just use frame 0.
|
|
app.viewer->applyAnimation(0.0);
|
|
|
|
const bool enableDiagnostics = app.visualization == Visualization::MESH_VERTEX_NORMALS;
|
|
auto begin = app.viewerAsset->getMaterialInstances();
|
|
auto end = begin + app.viewerAsset->getMaterialInstanceCount();
|
|
for (auto iter = begin; iter != end; ++iter) {
|
|
(*iter)->setParameter("enableDiagnostics", enableDiagnostics);
|
|
}
|
|
}
|
|
|
|
// Perform pending work.
|
|
BakerAppTask* tile = app.onTile.exchange(nullptr);
|
|
BakerAppTask* done = app.onDone.exchange(nullptr);
|
|
if (tile) { (*tile)(&app); delete tile; }
|
|
if (done) { (*done)(&app); delete done; }
|
|
|
|
// Update the overlay quad geometry just in case the window size changed.
|
|
app.overlayQuad.view = FilamentApp::get().getGuiView();
|
|
app.overlayQuad.scene = app.overlayQuad.view->getScene();
|
|
app.overlayQuad.scene->remove(app.overlayQuad.entity);
|
|
const bool showOverlay = app.visualization == Visualization::IMAGE_OCCLUSION
|
|
|| app.visualization == Visualization::IMAGE_BENT_NORMALS
|
|
|| app.visualization == Visualization::IMAGE_GBUFFER_NORMALS;
|
|
if (showOverlay) {
|
|
createQuadRenderable(app);
|
|
app.overlayQuad.scene->addEntity(app.overlayQuad.entity);
|
|
}
|
|
|
|
// If requested update the overlay quad texture or 3D mesh data.
|
|
if (app.requestViewerUpdate) {
|
|
updateViewer(app);
|
|
app.requestViewerUpdate = false;
|
|
}
|
|
};
|
|
|
|
auto gui = [&app](filament::Engine* engine, filament::View* view) {
|
|
app.viewer->updateUserInterface();
|
|
FilamentApp::get().setSidebarWidth(app.viewer->getSidebarWidth());
|
|
};
|
|
|
|
FilamentApp& filamentApp = FilamentApp::get();
|
|
filamentApp.animate(animate);
|
|
|
|
filamentApp.setDropHandler([&] (std::string path) {
|
|
app.viewer->removeAsset();
|
|
app.loader->destroyAsset(app.viewerAsset);
|
|
app.viewerAsset = nullptr;
|
|
app.filename = path;
|
|
app.hasTestRender = false;
|
|
app.ambientOcclusion = image::LinearImage();
|
|
app.bentNormals = image::LinearImage();
|
|
app.meshNormals = image::LinearImage();
|
|
app.meshPositions = image::LinearImage();
|
|
app.visualization = Visualization::MESH_CURRENT;
|
|
loadAssetFromDisk(app);
|
|
saveIniFile(app);
|
|
});
|
|
|
|
filamentApp.run(app.config, setup, cleanup, gui);
|
|
|
|
return 0;
|
|
}
|