- move the control code out of FrameInfo and into View itself. FrameInfo now just gathers information about the frames instead of also doing part of the control. - the control code has been refactored into a more formal PIDController. - a few bugs were fixed in the control loop and default parameters tuned. The control loop itself now outputs a relative scale factor instead of an absolute one. The relative scale factor seems easier to control and is less jittery, and doesn't have to rely on the "integral" term of the PID. It's possible that more tuning is needed, but the scaling is now more stable.
753 lines
29 KiB
C++
753 lines
29 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.
|
|
*/
|
|
|
|
#include <filamentapp/Config.h>
|
|
#include <filamentapp/FilamentApp.h>
|
|
#include <filamentapp/IBL.h>
|
|
|
|
#include <filament/Camera.h>
|
|
#include <filament/ColorGrading.h>
|
|
#include <filament/Engine.h>
|
|
#include <filament/IndexBuffer.h>
|
|
#include <filament/RenderableManager.h>
|
|
#include <filament/Renderer.h>
|
|
#include <filament/Scene.h>
|
|
#include <filament/Skybox.h>
|
|
#include <filament/TransformManager.h>
|
|
#include <filament/VertexBuffer.h>
|
|
#include <filament/View.h>
|
|
|
|
#include <gltfio/AssetLoader.h>
|
|
#include <gltfio/FilamentAsset.h>
|
|
#include <gltfio/ResourceLoader.h>
|
|
|
|
#include <viewer/AutomationEngine.h>
|
|
#include <viewer/AutomationSpec.h>
|
|
#include <viewer/SimpleViewer.h>
|
|
|
|
#include <camutils/Manipulator.h>
|
|
|
|
#include <getopt/getopt.h>
|
|
|
|
#include <utils/NameComponentManager.h>
|
|
|
|
#include <math/vec3.h>
|
|
#include <math/vec4.h>
|
|
#include <math/mat3.h>
|
|
#include <math/norm.h>
|
|
|
|
#include <imgui.h>
|
|
#include <filagui/ImGuiExtensions.h>
|
|
|
|
#include <fstream>
|
|
#include <iostream>
|
|
#include <string>
|
|
|
|
#include "generated/resources/gltf_viewer.h"
|
|
|
|
using namespace filament;
|
|
using namespace filament::math;
|
|
using namespace filament::viewer;
|
|
|
|
using namespace gltfio;
|
|
using namespace utils;
|
|
|
|
enum MaterialSource {
|
|
GENERATE_SHADERS,
|
|
LOAD_UBERSHADERS,
|
|
};
|
|
|
|
struct App {
|
|
Engine* engine;
|
|
SimpleViewer* viewer;
|
|
Config config;
|
|
Camera* mainCamera;
|
|
|
|
AssetLoader* assetLoader;
|
|
FilamentAsset* asset = nullptr;
|
|
NameComponentManager* names;
|
|
|
|
MaterialProvider* materials;
|
|
MaterialSource materialSource = GENERATE_SHADERS;
|
|
|
|
gltfio::ResourceLoader* resourceLoader = nullptr;
|
|
bool recomputeAabb = false;
|
|
|
|
bool actualSize = false;
|
|
|
|
struct Scene {
|
|
Entity groundPlane;
|
|
VertexBuffer* groundVertexBuffer;
|
|
IndexBuffer* groundIndexBuffer;
|
|
Material* groundMaterial;
|
|
} scene;
|
|
|
|
// zero-initialized so that the first time through is always dirty.
|
|
ColorGradingSettings lastColorGradingOptions = { 0 };
|
|
|
|
ColorGrading* colorGrading = nullptr;
|
|
|
|
std::string messageBoxText;
|
|
std::string settingsFile;
|
|
std::string batchFile;
|
|
|
|
AutomationSpec* automationSpec = nullptr;
|
|
AutomationEngine* automationEngine = nullptr;
|
|
};
|
|
|
|
static const char* DEFAULT_IBL = "assets/ibl/lightroom_14b";
|
|
|
|
static void printUsage(char* name) {
|
|
std::string exec_name(Path(name).getName());
|
|
std::string usage(
|
|
"SHOWCASE renders the specified glTF file, or a built-in file if none is specified\n"
|
|
"Usage:\n"
|
|
" SHOWCASE [options] <gltf path>\n"
|
|
"Options:\n"
|
|
" --help, -h\n"
|
|
" Prints this message\n\n"
|
|
" --api, -a\n"
|
|
" Specify the backend API: opengl (default), vulkan, or metal\n\n"
|
|
" --batch=<path to JSON file or 'default'>, -b\n"
|
|
" Start automation using the given JSON spec, then quit the app\n\n"
|
|
" --headless, -e\n"
|
|
" Use a headless swapchain; ignored if --batch is not present\n\n"
|
|
" --ibl=<path>, -i <path>\n"
|
|
" Override the built-in IBL\n"
|
|
" path can either be a directory containing IBL data files generated by cmgen,\n"
|
|
" or, a .hdr equiretangular image file\n\n"
|
|
" --actual-size, -s\n"
|
|
" Do not scale the model to fit into a unit cube\n\n"
|
|
" --recompute-aabb, -r\n"
|
|
" Ignore the min/max attributes in the glTF file\n\n"
|
|
" --settings=<path to JSON file>, -t\n"
|
|
" Apply the settings in the given JSON file\n\n"
|
|
" --ubershader, -u\n"
|
|
" Enable ubershaders (improves load time, adds shader complexity)\n\n"
|
|
" --camera=<camera mode>, -c <camera mode>\n"
|
|
" Set the camera mode: orbit (default) or flight\n"
|
|
" Flight mode uses the following controls:\n"
|
|
" Click and drag the mouse to pan the camera\n"
|
|
" Use the scroll weel to adjust movement speed\n"
|
|
" W / S: forward / backward\n"
|
|
" A / D: left / right\n"
|
|
" E / Q: up / down\n\n"
|
|
" --split-view, -v\n"
|
|
" Splits the window into 4 views\n"
|
|
);
|
|
const std::string from("SHOWCASE");
|
|
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 std::ifstream::pos_type getFileSize(const char* filename) {
|
|
std::ifstream in(filename, std::ifstream::ate | std::ifstream::binary);
|
|
return in.tellg();
|
|
}
|
|
|
|
static int handleCommandLineArguments(int argc, char* argv[], App* app) {
|
|
static constexpr const char* OPTSTR = "ha:i:usc:rt:b:ev";
|
|
static const struct option OPTIONS[] = {
|
|
{ "help", no_argument, nullptr, 'h' },
|
|
{ "api", required_argument, nullptr, 'a' },
|
|
{ "batch", required_argument, nullptr, 'b' },
|
|
{ "headless", no_argument, nullptr, 'e' },
|
|
{ "ibl", required_argument, nullptr, 'i' },
|
|
{ "ubershader", no_argument, nullptr, 'u' },
|
|
{ "actual-size", no_argument, nullptr, 's' },
|
|
{ "camera", required_argument, nullptr, 'c' },
|
|
{ "recompute-aabb", no_argument, nullptr, 'r' },
|
|
{ "settings", required_argument, nullptr, 't' },
|
|
{ "split-view", no_argument, nullptr, 'v' },
|
|
{ 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 'a':
|
|
if (arg == "opengl") {
|
|
app->config.backend = Engine::Backend::OPENGL;
|
|
} else if (arg == "vulkan") {
|
|
app->config.backend = Engine::Backend::VULKAN;
|
|
} else if (arg == "metal") {
|
|
app->config.backend = Engine::Backend::METAL;
|
|
} else {
|
|
std::cerr << "Unrecognized backend. Must be 'opengl'|'vulkan'|'metal'.\n";
|
|
}
|
|
break;
|
|
case 'c':
|
|
if (arg == "flight") {
|
|
app->config.cameraMode = camutils::Mode::FREE_FLIGHT;
|
|
} else if (arg == "orbit") {
|
|
app->config.cameraMode = camutils::Mode::ORBIT;
|
|
} else {
|
|
std::cerr << "Unrecognized camera mode. Must be 'flight'|'orbit'.\n";
|
|
}
|
|
break;
|
|
case 'e':
|
|
app->config.headless = true;
|
|
break;
|
|
case 'i':
|
|
app->config.iblDirectory = arg;
|
|
break;
|
|
case 'u':
|
|
app->materialSource = LOAD_UBERSHADERS;
|
|
break;
|
|
case 's':
|
|
app->actualSize = true;
|
|
break;
|
|
case 'r':
|
|
app->recomputeAabb = true;
|
|
break;
|
|
case 't':
|
|
app->settingsFile = arg;
|
|
break;
|
|
case 'b': {
|
|
app->batchFile = arg;
|
|
break;
|
|
}
|
|
case 'v': {
|
|
app->config.splitView = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (app->config.headless && app->batchFile.empty()) {
|
|
std::cerr << "--headless is allowed only when --batch is present." << std::endl;
|
|
app->config.headless = false;
|
|
}
|
|
return optind;
|
|
}
|
|
|
|
static bool loadSettings(const char* filename, Settings* out) {
|
|
auto contentSize = getFileSize(filename);
|
|
if (contentSize <= 0) {
|
|
return false;
|
|
}
|
|
std::ifstream in(filename, std::ifstream::binary | std::ifstream::in);
|
|
std::vector<char> json(static_cast<unsigned long>(contentSize));
|
|
if (!in.read(json.data(), contentSize)) {
|
|
return false;
|
|
}
|
|
JsonSerializer serializer;
|
|
return serializer.readJson(json.data(), contentSize, out);
|
|
}
|
|
|
|
static void createGroundPlane(Engine* engine, Scene* scene, App& app) {
|
|
auto& em = EntityManager::get();
|
|
Material* shadowMaterial = Material::Builder()
|
|
.package(GLTF_VIEWER_GROUNDSHADOW_DATA, GLTF_VIEWER_GROUNDSHADOW_SIZE)
|
|
.build(*engine);
|
|
auto& viewerOptions = app.viewer->getSettings().viewer;
|
|
shadowMaterial->setDefaultParameter("strength", viewerOptions.groundShadowStrength);
|
|
|
|
const static uint32_t indices[] = {
|
|
0, 1, 2, 2, 3, 0
|
|
};
|
|
|
|
Aabb aabb = app.asset->getBoundingBox();
|
|
if (!app.actualSize) {
|
|
mat4f transform = fitIntoUnitCube(aabb, 4);
|
|
aabb = aabb.transform(transform);
|
|
}
|
|
|
|
float3 planeExtent{10.0f * aabb.extent().x, 0.0f, 10.0f * aabb.extent().z};
|
|
|
|
const static float3 vertices[] = {
|
|
{ -planeExtent.x, 0, -planeExtent.z },
|
|
{ -planeExtent.x, 0, planeExtent.z },
|
|
{ planeExtent.x, 0, planeExtent.z },
|
|
{ planeExtent.x, 0, -planeExtent.z },
|
|
};
|
|
|
|
short4 tbn = packSnorm16(
|
|
mat3f::packTangentFrame(
|
|
mat3f{
|
|
float3{ 1.0f, 0.0f, 0.0f },
|
|
float3{ 0.0f, 0.0f, 1.0f },
|
|
float3{ 0.0f, 1.0f, 0.0f }
|
|
}
|
|
).xyzw);
|
|
|
|
const static short4 normals[] { tbn, tbn, tbn, tbn };
|
|
|
|
VertexBuffer* vertexBuffer = VertexBuffer::Builder()
|
|
.vertexCount(4)
|
|
.bufferCount(2)
|
|
.attribute(VertexAttribute::POSITION,
|
|
0, VertexBuffer::AttributeType::FLOAT3)
|
|
.attribute(VertexAttribute::TANGENTS,
|
|
1, VertexBuffer::AttributeType::SHORT4)
|
|
.normalized(VertexAttribute::TANGENTS)
|
|
.build(*engine);
|
|
|
|
vertexBuffer->setBufferAt(*engine, 0, VertexBuffer::BufferDescriptor(
|
|
vertices, vertexBuffer->getVertexCount() * sizeof(vertices[0])));
|
|
vertexBuffer->setBufferAt(*engine, 1, VertexBuffer::BufferDescriptor(
|
|
normals, vertexBuffer->getVertexCount() * sizeof(normals[0])));
|
|
|
|
IndexBuffer* indexBuffer = IndexBuffer::Builder()
|
|
.indexCount(6)
|
|
.build(*engine);
|
|
|
|
indexBuffer->setBuffer(*engine, IndexBuffer::BufferDescriptor(
|
|
indices, indexBuffer->getIndexCount() * sizeof(uint32_t)));
|
|
|
|
Entity groundPlane = em.create();
|
|
RenderableManager::Builder(1)
|
|
.boundingBox({
|
|
{}, { planeExtent.x, 1e-4f, planeExtent.z }
|
|
})
|
|
.material(0, shadowMaterial->getDefaultInstance())
|
|
.geometry(0, RenderableManager::PrimitiveType::TRIANGLES,
|
|
vertexBuffer, indexBuffer, 0, 6)
|
|
.culling(false)
|
|
.receiveShadows(true)
|
|
.castShadows(false)
|
|
.build(*engine, groundPlane);
|
|
|
|
scene->addEntity(groundPlane);
|
|
|
|
auto& tcm = engine->getTransformManager();
|
|
tcm.setTransform(tcm.getInstance(groundPlane),
|
|
mat4f::translation(float3{ 0, aabb.min.y, -4 }));
|
|
|
|
auto& rcm = engine->getRenderableManager();
|
|
auto instance = rcm.getInstance(groundPlane);
|
|
rcm.setLayerMask(instance, 0xff, 0x00);
|
|
|
|
app.scene.groundPlane = groundPlane;
|
|
app.scene.groundVertexBuffer = vertexBuffer;
|
|
app.scene.groundIndexBuffer = indexBuffer;
|
|
app.scene.groundMaterial = shadowMaterial;
|
|
}
|
|
|
|
int main(int argc, char** argv) {
|
|
App app;
|
|
|
|
app.config.title = "Filament";
|
|
app.config.iblDirectory = FilamentApp::getRootAssetsPath() + DEFAULT_IBL;
|
|
|
|
int optionIndex = handleCommandLineArguments(argc, argv, &app);
|
|
|
|
utils::Path filename;
|
|
int num_args = argc - optionIndex;
|
|
if (num_args >= 1) {
|
|
filename = argv[optionIndex];
|
|
if (!filename.exists()) {
|
|
std::cerr << "file " << filename << " not found!" << std::endl;
|
|
return 1;
|
|
}
|
|
if (filename.isDirectory()) {
|
|
auto files = filename.listContents();
|
|
for (auto file : files) {
|
|
if (file.getExtension() == "gltf" || file.getExtension() == "glb") {
|
|
filename = file;
|
|
break;
|
|
}
|
|
}
|
|
if (filename.isDirectory()) {
|
|
std::cerr << "no glTF file found in " << filename << std::endl;
|
|
return 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
auto loadAsset = [&app](utils::Path filename) {
|
|
// Peek at the file size to allow pre-allocation.
|
|
long contentSize = static_cast<long>(getFileSize(filename.c_str()));
|
|
if (contentSize <= 0) {
|
|
std::cerr << "Unable to open " << filename << std::endl;
|
|
exit(1);
|
|
}
|
|
|
|
// Consume the glTF file.
|
|
std::ifstream in(filename.c_str(), std::ifstream::binary | std::ifstream::in);
|
|
std::vector<uint8_t> buffer(static_cast<unsigned long>(contentSize));
|
|
if (!in.read((char*) buffer.data(), contentSize)) {
|
|
std::cerr << "Unable to read " << filename << std::endl;
|
|
exit(1);
|
|
}
|
|
|
|
// Parse the glTF file and create Filament entities.
|
|
if (filename.getExtension() == "glb") {
|
|
app.asset = app.assetLoader->createAssetFromBinary(buffer.data(), buffer.size());
|
|
} else {
|
|
app.asset = app.assetLoader->createAssetFromJson(buffer.data(), buffer.size());
|
|
}
|
|
buffer.clear();
|
|
buffer.shrink_to_fit();
|
|
|
|
if (!app.asset) {
|
|
std::cerr << "Unable to parse " << filename << std::endl;
|
|
exit(1);
|
|
}
|
|
};
|
|
|
|
auto loadResources = [&app] (utils::Path filename) {
|
|
// Load external textures and buffers.
|
|
std::string gltfPath = filename.getAbsolutePath();
|
|
ResourceConfiguration configuration = {};
|
|
configuration.engine = app.engine;
|
|
configuration.gltfPath = gltfPath.c_str();
|
|
configuration.recomputeBoundingBoxes = app.recomputeAabb;
|
|
configuration.normalizeSkinningWeights = true;
|
|
if (!app.resourceLoader) {
|
|
app.resourceLoader = new gltfio::ResourceLoader(configuration);
|
|
}
|
|
app.resourceLoader->asyncBeginLoad(app.asset);
|
|
|
|
// Load animation data then free the source hierarchy.
|
|
app.asset->getAnimator();
|
|
app.asset->releaseSourceData();
|
|
|
|
auto ibl = FilamentApp::get().getIBL();
|
|
if (ibl) {
|
|
app.viewer->setIndirectLight(ibl->getIndirectLight(), ibl->getSphericalHarmonics());
|
|
}
|
|
};
|
|
|
|
auto setup = [&](Engine* engine, View* view, Scene* scene) {
|
|
app.engine = engine;
|
|
app.names = new NameComponentManager(EntityManager::get());
|
|
app.viewer = new SimpleViewer(engine, scene, view, 410);
|
|
app.viewer->getSettings().viewer.autoScaleEnabled = !app.actualSize;
|
|
|
|
const bool batchMode = !app.batchFile.empty();
|
|
|
|
// First check if a custom automation spec has been provided. If it fails to load, the app
|
|
// must be closed since it could be invoked from a script.
|
|
if (batchMode && app.batchFile != "default") {
|
|
auto size = getFileSize(app.batchFile.c_str());
|
|
if (size > 0) {
|
|
std::ifstream in(app.batchFile, std::ifstream::binary | std::ifstream::in);
|
|
std::vector<char> json(static_cast<unsigned long>(size));
|
|
in.read(json.data(), size);
|
|
app.automationSpec = AutomationSpec::generate(json.data(), size);
|
|
if (!app.automationSpec) {
|
|
std::cerr << "Unable to parse automation spec: " << app.batchFile << std::endl;
|
|
exit(1);
|
|
}
|
|
} else {
|
|
std::cerr << "Unable to load automation spec: " << app.batchFile << std::endl;
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
// If no custom spec has been provided, or if in interactive mode, load the default spec.
|
|
if (!app.automationSpec) {
|
|
app.automationSpec = AutomationSpec::generateDefaultTestCases();
|
|
}
|
|
|
|
app.automationEngine = new AutomationEngine(app.automationSpec, &app.viewer->getSettings());
|
|
|
|
if (batchMode) {
|
|
app.automationEngine->startBatchMode();
|
|
auto options = app.automationEngine->getOptions();
|
|
options.sleepDuration = 0.0;
|
|
options.exportScreenshots = true;
|
|
options.exportSettings = true;
|
|
app.automationEngine->setOptions(options);
|
|
app.viewer->stopAnimation();
|
|
}
|
|
|
|
if (app.settingsFile.size() > 0) {
|
|
bool success = loadSettings(app.settingsFile.c_str(), &app.viewer->getSettings());
|
|
if (success) {
|
|
std::cout << "Loaded settings from " << app.settingsFile << std::endl;
|
|
} else {
|
|
std::cerr << "Failed to load settings from " << app.settingsFile << std::endl;
|
|
}
|
|
}
|
|
|
|
app.materials = (app.materialSource == GENERATE_SHADERS) ?
|
|
createMaterialGenerator(engine) : createUbershaderLoader(engine);
|
|
app.assetLoader = AssetLoader::create({engine, app.materials, app.names });
|
|
app.mainCamera = &view->getCamera();
|
|
if (filename.isEmpty()) {
|
|
app.asset = app.assetLoader->createAssetFromBinary(
|
|
GLTF_VIEWER_DAMAGEDHELMET_DATA,
|
|
GLTF_VIEWER_DAMAGEDHELMET_SIZE);
|
|
} else {
|
|
loadAsset(filename);
|
|
}
|
|
|
|
loadResources(filename);
|
|
|
|
createGroundPlane(engine, scene, app);
|
|
|
|
app.viewer->setUiCallback([&app, scene, view, engine] () {
|
|
auto& automation = *app.automationEngine;
|
|
|
|
float progress = app.resourceLoader->asyncGetLoadProgress();
|
|
if (progress < 1.0) {
|
|
ImGui::ProgressBar(progress);
|
|
} else {
|
|
// The model is now fully loaded, so let automation know.
|
|
automation.signalBatchMode();
|
|
}
|
|
|
|
// The screenshots do not include the UI, but we auto-open the Automation UI group
|
|
// when in batch mode. This is useful when a human is observing progress.
|
|
const int flags = automation.isBatchModeEnabled() ? ImGuiTreeNodeFlags_DefaultOpen : 0;
|
|
|
|
if (ImGui::CollapsingHeader("Automation", flags)) {
|
|
ImGui::Indent();
|
|
|
|
const ImVec4 yellow(1.0f,1.0f,0.0f,1.0f);
|
|
if (automation.isRunning()) {
|
|
ImGui::TextColored(yellow, "Test case %zu / %zu",
|
|
automation.currentTest(), automation.testCount());
|
|
} else {
|
|
ImGui::TextColored(yellow, "%zu test cases", automation.testCount());
|
|
}
|
|
|
|
auto options = automation.getOptions();
|
|
|
|
ImGui::PushItemWidth(150);
|
|
ImGui::SliderFloat("Sleep (seconds)", &options.sleepDuration, 0.0, 5.0);
|
|
ImGui::PopItemWidth();
|
|
|
|
// Hide the tooltip during automation to avoid photobombing the screenshot.
|
|
if (ImGui::IsItemHovered() && !automation.isRunning()) {
|
|
ImGui::SetTooltip("Specifies the amount of time to sleep between test cases.");
|
|
}
|
|
|
|
ImGui::Checkbox("Export screenshot for each test", &options.exportScreenshots);
|
|
ImGui::Checkbox("Export settings JSON for each test", &options.exportSettings);
|
|
|
|
automation.setOptions(options);
|
|
|
|
if (automation.isRunning()) {
|
|
if (ImGui::Button("Stop batch test")) {
|
|
automation.stopRunning();
|
|
}
|
|
} else if (ImGui::Button("Run batch test")) {
|
|
automation.startRunning();
|
|
}
|
|
|
|
if (ImGui::Button("Export view settings")) {
|
|
automation.exportSettings(app.viewer->getSettings(), "settings.json");
|
|
app.messageBoxText = automation.getStatusMessage();
|
|
ImGui::OpenPopup("MessageBox");
|
|
}
|
|
ImGui::Unindent();
|
|
}
|
|
|
|
if (ImGui::CollapsingHeader("Stats")) {
|
|
ImGui::Indent();
|
|
ImGui::Text("%zu entities in the asset", app.asset->getEntityCount());
|
|
ImGui::Text("%zu renderables (excluding UI)", scene->getRenderableCount());
|
|
ImGui::Text("%zu skipped frames", FilamentApp::get().getSkippedFrameCount());
|
|
ImGui::Unindent();
|
|
}
|
|
|
|
if (ImGui::CollapsingHeader("Debug")) {
|
|
auto& debug = engine->getDebugRegistry();
|
|
if (ImGui::Button("Capture frame")) {
|
|
bool* captureFrame =
|
|
debug.getPropertyAddress<bool>("d.renderer.doFrameCapture");
|
|
*captureFrame = true;
|
|
}
|
|
auto dataSource = debug.getDataSource("d.view.frame_info");
|
|
if (dataSource.data) {
|
|
ImGuiExt::PlotLinesSeries("FrameInfo", 6,
|
|
[](int series) {
|
|
const ImVec4 colors[] = {
|
|
{ 1, 0, 0, 1 }, // target
|
|
{ 0, 0.5f, 0, 1 }, // frame-time
|
|
{ 0, 1, 0, 1 }, // frame-time denoised
|
|
{ 1, 1, 0, 1 }, // i
|
|
{ 1, 0, 1, 1 }, // d
|
|
{ 0, 1, 1, 1 }, // e
|
|
|
|
};
|
|
ImGui::PushStyleColor(ImGuiCol_PlotLines, colors[series]);
|
|
},
|
|
[](int series, void* buffer, int i) -> float {
|
|
auto const* p = (DebugRegistry::FrameHistory const*)buffer + i;
|
|
switch (series) {
|
|
case 0: return 0.03f * p->target;
|
|
case 1: return 0.03f * p->frameTime;
|
|
case 2: return 0.03f * p->frameTimeDenoised;
|
|
case 3: return p->pid_i * 0.5f / 100.0f + 0.5f;
|
|
case 4: return p->pid_d * 0.5f / 0.100f + 0.5f;
|
|
case 5: return p->pid_e * 0.5f / 1.000f + 0.5f;
|
|
default: return 0.0f;
|
|
}
|
|
},
|
|
[](int series) {
|
|
if (series < 6) ImGui::PopStyleColor();
|
|
},
|
|
const_cast<void*>(dataSource.data), int(dataSource.count), 0,
|
|
nullptr, 0.0f, 1.0f, { 0, 100 });
|
|
}
|
|
ImGui::SliderFloat("Kp", debug.getPropertyAddress<float>("d.view.pid.kp"), 0, 2);
|
|
ImGui::SliderFloat("Ki", debug.getPropertyAddress<float>("d.view.pid.ki"), 0, 10);
|
|
ImGui::SliderFloat("Kd", debug.getPropertyAddress<float>("d.view.pid.kd"), 0, 10);
|
|
}
|
|
|
|
if (ImGui::BeginPopupModal("MessageBox", NULL, ImGuiWindowFlags_AlwaysAutoResize)) {
|
|
ImGui::Text("%s", app.messageBoxText.c_str());
|
|
if (ImGui::Button("OK", ImVec2(120, 0))) {
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
});
|
|
};
|
|
|
|
auto cleanup = [&app](Engine* engine, View*, Scene*) {
|
|
app.automationEngine->terminate();
|
|
app.resourceLoader->asyncCancelLoad();
|
|
app.assetLoader->destroyAsset(app.asset);
|
|
app.materials->destroyMaterials();
|
|
|
|
engine->destroy(app.scene.groundPlane);
|
|
engine->destroy(app.scene.groundVertexBuffer);
|
|
engine->destroy(app.scene.groundIndexBuffer);
|
|
engine->destroy(app.scene.groundMaterial);
|
|
engine->destroy(app.colorGrading);
|
|
|
|
delete app.viewer;
|
|
delete app.materials;
|
|
delete app.names;
|
|
|
|
AssetLoader::destroy(&app.assetLoader);
|
|
};
|
|
|
|
auto animate = [&app](Engine* engine, View* view, double now) {
|
|
app.resourceLoader->asyncUpdateLoad();
|
|
|
|
// Optionally fit the model into a unit cube at the origin.
|
|
app.viewer->updateRootTransform();
|
|
|
|
// Add renderables to the scene as they become ready.
|
|
app.viewer->populateScene(app.asset);
|
|
|
|
app.viewer->applyAnimation(now);
|
|
};
|
|
|
|
auto resize = [&app](Engine* engine, View* view) {
|
|
Camera& camera = view->getCamera();
|
|
if (&camera == app.mainCamera) {
|
|
// Don't adjut the aspect ratio of the main camera, this is done inside of
|
|
// FilamentApp.cpp
|
|
return;
|
|
}
|
|
const Viewport& vp = view->getViewport();
|
|
double aspectRatio = (double) vp.width / vp.height;
|
|
camera.setScaling({1.0 / aspectRatio, 1.0 });
|
|
};
|
|
|
|
auto gui = [&app](Engine* engine, View* view) {
|
|
app.viewer->updateUserInterface();
|
|
|
|
FilamentApp::get().setSidebarWidth(app.viewer->getSidebarWidth());
|
|
};
|
|
|
|
auto preRender = [&app](Engine* engine, View* view, Scene* scene, Renderer* renderer) {
|
|
auto& rcm = engine->getRenderableManager();
|
|
auto instance = rcm.getInstance(app.scene.groundPlane);
|
|
const auto viewerOptions = app.automationEngine->getViewerOptions();
|
|
const auto& dofOptions = app.viewer->getSettings().view.dof;
|
|
rcm.setLayerMask(instance,
|
|
0xff, viewerOptions.groundPlaneEnabled ? 0xff : 0x00);
|
|
|
|
// Note that this focal length might be different from the slider value because the
|
|
// automation engine applies Camera::computeEffectiveFocalLength when DoF is enabled.
|
|
FilamentApp::get().getCameraFocalLength() = viewerOptions.cameraFocalLength;
|
|
|
|
const size_t cameraCount = app.asset->getCameraEntityCount();
|
|
view->setCamera(app.mainCamera);
|
|
|
|
const int currentCamera = app.viewer->getCurrentCamera();
|
|
if (currentCamera > 0 && currentCamera <= cameraCount) {
|
|
const utils::Entity* cameras = app.asset->getCameraEntities();
|
|
Camera* camera = engine->getCameraComponent(cameras[currentCamera - 1]);
|
|
assert_invariant(camera);
|
|
view->setCamera(camera);
|
|
|
|
// Override the aspect ratio in the glTF file and adjust the aspect ratio of this
|
|
// camera to the viewport.
|
|
const Viewport& vp = view->getViewport();
|
|
double aspectRatio = (double) vp.width / vp.height;
|
|
camera->setScaling({1.0 / aspectRatio, 1.0});
|
|
}
|
|
|
|
app.scene.groundMaterial->setDefaultParameter(
|
|
"strength", viewerOptions.groundShadowStrength);
|
|
|
|
// This applies clear options, the skybox mask, and some camera settings.
|
|
Camera& camera = view->getCamera();
|
|
Skybox* skybox = scene->getSkybox();
|
|
applySettings(app.viewer->getSettings().viewer, &camera, skybox, renderer);
|
|
|
|
// Check if color grading has changed.
|
|
ColorGradingSettings& options = app.viewer->getSettings().view.colorGrading;
|
|
if (options.enabled) {
|
|
if (options != app.lastColorGradingOptions) {
|
|
ColorGrading *colorGrading = createColorGrading(options, engine);
|
|
engine->destroy(app.colorGrading);
|
|
app.colorGrading = colorGrading;
|
|
app.lastColorGradingOptions = options;
|
|
}
|
|
view->setColorGrading(app.colorGrading);
|
|
} else {
|
|
view->setColorGrading(nullptr);
|
|
}
|
|
};
|
|
|
|
auto postRender = [&app](Engine* engine, View* view, Scene* scene, Renderer* renderer) {
|
|
if (app.automationEngine->shouldClose()) {
|
|
FilamentApp::get().close();
|
|
return;
|
|
}
|
|
AutomationEngine::ViewerContent content = {
|
|
.view = view,
|
|
.renderer = renderer,
|
|
.materials = app.asset->getMaterialInstances(),
|
|
.materialCount = app.asset->getMaterialInstanceCount(),
|
|
};
|
|
app.automationEngine->tick(content, ImGui::GetIO().DeltaTime);
|
|
};
|
|
|
|
FilamentApp& filamentApp = FilamentApp::get();
|
|
filamentApp.animate(animate);
|
|
filamentApp.resize(resize);
|
|
|
|
filamentApp.setDropHandler([&] (std::string path) {
|
|
app.resourceLoader->asyncCancelLoad();
|
|
app.resourceLoader->evictResourceData();
|
|
app.viewer->removeAsset();
|
|
app.assetLoader->destroyAsset(app.asset);
|
|
loadAsset(path);
|
|
loadResources(path);
|
|
});
|
|
|
|
filamentApp.run(app.config, setup, cleanup, gui, preRender, postRender);
|
|
|
|
return 0;
|
|
}
|