Introduce libs/viewer and serializable view settings.
This introduces the `viewer::Settings` struct, and a JSON reader / writer. This will be used for automated testing and for client / server communication. Note that `viewer::Settings` is closely associated with the `filament::View` API; when updating the latter we will often need to update the former, as well as some serialization code. This increases the maintenance burden and I think we should consider using a parser library like libclang or a macro-based reflection utility. This PR also migrates SimpleViewer into libs/viewer and un-inlines its implementation. It does not belong in gltfio because it has an imgui dependency.
This commit is contained in:
407
libs/viewer/src/SimpleViewer.cpp
Normal file
407
libs/viewer/src/SimpleViewer.cpp
Normal file
@@ -0,0 +1,407 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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 mIcable 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 <viewer/SimpleViewer.h>
|
||||
|
||||
#include <filament/RenderableManager.h>
|
||||
#include <filament/TransformManager.h>
|
||||
#include <filament/LightManager.h>
|
||||
#include <filament/Material.h>
|
||||
|
||||
#include <utils/EntityManager.h>
|
||||
|
||||
#include <math/mat4.h>
|
||||
#include <math/vec3.h>
|
||||
|
||||
#include <imgui.h>
|
||||
#include <filagui/ImGuiExtensions.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace filament {
|
||||
namespace viewer {
|
||||
|
||||
filament::math::mat4f fitIntoUnitCube(const filament::Aabb& bounds) {
|
||||
using namespace filament::math;
|
||||
auto minpt = bounds.min;
|
||||
auto maxpt = bounds.max;
|
||||
float maxExtent;
|
||||
maxExtent = std::max(maxpt.x - minpt.x, maxpt.y - minpt.y);
|
||||
maxExtent = std::max(maxExtent, maxpt.z - minpt.z);
|
||||
float scaleFactor = 2.0f / maxExtent;
|
||||
float3 center = (minpt + maxpt) / 2.0f;
|
||||
center.z += 4.0f / scaleFactor;
|
||||
return mat4f::scaling(float3(scaleFactor)) * mat4f::translation(-center);
|
||||
}
|
||||
|
||||
SimpleViewer::SimpleViewer(filament::Engine* engine, filament::Scene* scene, filament::View* view,
|
||||
int sidebarWidth) :
|
||||
mEngine(engine), mScene(scene), mView(view),
|
||||
mSunlight(utils::EntityManager::get().create()),
|
||||
mSidebarWidth(sidebarWidth) {
|
||||
using namespace filament;
|
||||
LightManager::Builder(LightManager::Type::SUN)
|
||||
.color(mSunlightColor)
|
||||
.intensity(mSunlightIntensity)
|
||||
.direction(normalize(mSunlightDirection))
|
||||
.castShadows(true)
|
||||
.sunAngularRadius(1.9)
|
||||
.sunHaloSize(10.0)
|
||||
.sunHaloFalloff(80.0)
|
||||
.build(*engine, mSunlight);
|
||||
if (mEnableSunlight) {
|
||||
mScene->addEntity(mSunlight);
|
||||
}
|
||||
view->setAmbientOcclusionOptions({ .upsampling = View::QualityLevel::HIGH });
|
||||
}
|
||||
|
||||
SimpleViewer::~SimpleViewer() {
|
||||
mEngine->destroy(mSunlight);
|
||||
}
|
||||
|
||||
void SimpleViewer::populateScene(FilamentAsset* asset, bool scale,
|
||||
FilamentInstance* instanceToAnimate) {
|
||||
if (mAsset != asset) {
|
||||
removeAsset();
|
||||
mAsset = asset;
|
||||
if (!asset) {
|
||||
mAnimator = nullptr;
|
||||
return;
|
||||
}
|
||||
mAnimator = instanceToAnimate ? instanceToAnimate->getAnimator() : asset->getAnimator();
|
||||
if (scale) {
|
||||
auto& tcm = mEngine->getTransformManager();
|
||||
auto root = tcm.getInstance(mAsset->getRoot());
|
||||
filament::math::mat4f transform = fitIntoUnitCube(mAsset->getBoundingBox());
|
||||
tcm.setTransform(root, transform);
|
||||
}
|
||||
|
||||
mScene->addEntities(asset->getLightEntities(), asset->getLightEntityCount());
|
||||
}
|
||||
|
||||
auto& tcm = mEngine->getRenderableManager();
|
||||
|
||||
static constexpr int kNumAvailable = 128;
|
||||
utils::Entity renderables[kNumAvailable];
|
||||
while (size_t numWritten = mAsset->popRenderables(renderables, kNumAvailable)) {
|
||||
for (size_t i = 0; i < numWritten; i++) {
|
||||
auto ri = tcm.getInstance(renderables[i]);
|
||||
tcm.setScreenSpaceContactShadows(ri, true);
|
||||
}
|
||||
mScene->addEntities(renderables, numWritten);
|
||||
}
|
||||
}
|
||||
|
||||
void SimpleViewer::removeAsset() {
|
||||
if (mAsset) {
|
||||
mScene->removeEntities(mAsset->getEntities(), mAsset->getEntityCount());
|
||||
}
|
||||
mAsset = nullptr;
|
||||
mAnimator = nullptr;
|
||||
}
|
||||
|
||||
void SimpleViewer::setIndirectLight(filament::IndirectLight* ibl,
|
||||
filament::math::float3 const* sh3) {
|
||||
using namespace filament::math;
|
||||
mFogOptions.color = sh3[0];
|
||||
mIndirectLight = ibl;
|
||||
if (ibl) {
|
||||
float3 d = filament::IndirectLight::getDirectionEstimate(sh3);
|
||||
float4 c = filament::IndirectLight::getColorEstimate(sh3, d);
|
||||
mSunlightDirection = d;
|
||||
mSunlightColor = c.rgb;
|
||||
mSunlightIntensity = c[3] * ibl->getIntensity();
|
||||
updateIndirectLight();
|
||||
}
|
||||
}
|
||||
|
||||
void SimpleViewer::updateIndirectLight() {
|
||||
using namespace filament::math;
|
||||
if (mIndirectLight) {
|
||||
mIndirectLight->setIntensity(mIblIntensity);
|
||||
mIndirectLight->setRotation(mat3f::rotation(mIblRotation, float3{ 0, 1, 0 }));
|
||||
}
|
||||
}
|
||||
|
||||
void SimpleViewer::applyAnimation(double currentTime) {
|
||||
if (!mAnimator) {
|
||||
return;
|
||||
}
|
||||
static double startTime = 0;
|
||||
const size_t numAnimations = mAnimator->getAnimationCount();
|
||||
if (mResetAnimation) {
|
||||
startTime = currentTime;
|
||||
for (size_t i = 0; i < numAnimations; i++) {
|
||||
mAnimator->applyAnimation(i, 0);
|
||||
}
|
||||
mResetAnimation = false;
|
||||
}
|
||||
if (numAnimations > 0 && mCurrentAnimation > 0) {
|
||||
mAnimator->applyAnimation(mCurrentAnimation - 1, currentTime - startTime);
|
||||
}
|
||||
mAnimator->updateBoneMatrices();
|
||||
}
|
||||
|
||||
void SimpleViewer::updateUserInterface() {
|
||||
using namespace filament;
|
||||
|
||||
auto& tm = mEngine->getTransformManager();
|
||||
auto& rm = mEngine->getRenderableManager();
|
||||
auto& lm = mEngine->getLightManager();
|
||||
|
||||
// Show a common set of UI widgets for all renderables.
|
||||
auto renderableTreeItem = [this, &rm](utils::Entity entity) {
|
||||
bool rvis = mScene->hasEntity(entity);
|
||||
ImGui::Checkbox("visible", &rvis);
|
||||
if (rvis) {
|
||||
mScene->addEntity(entity);
|
||||
} else {
|
||||
mScene->remove(entity);
|
||||
}
|
||||
auto instance = rm.getInstance(entity);
|
||||
bool scaster = rm.isShadowCaster(instance);
|
||||
ImGui::Checkbox("casts shadows", &scaster);
|
||||
rm.setCastShadows(instance, scaster);
|
||||
size_t numPrims = rm.getPrimitiveCount(instance);
|
||||
for (size_t prim = 0; prim < numPrims; ++prim) {
|
||||
const char* mname = rm.getMaterialInstanceAt(instance, prim)->getName();
|
||||
if (mname) {
|
||||
ImGui::Text("prim %zu: material %s", prim, mname);
|
||||
} else {
|
||||
ImGui::Text("prim %zu: (unnamed material)", prim);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
auto lightTreeItem = [this, &lm](utils::Entity entity) {
|
||||
bool lvis = mScene->hasEntity(entity);
|
||||
ImGui::Checkbox("visible", &lvis);
|
||||
|
||||
if (lvis) {
|
||||
mScene->addEntity(entity);
|
||||
} else {
|
||||
mScene->remove(entity);
|
||||
}
|
||||
|
||||
auto instance = lm.getInstance(entity);
|
||||
bool lcaster = lm.isShadowCaster(instance);
|
||||
ImGui::Checkbox("shadow caster", &lcaster);
|
||||
lm.setShadowCaster(instance, lcaster);
|
||||
};
|
||||
|
||||
// Declare a std::function for tree nodes, it's an easy way to make a recursive lambda.
|
||||
std::function<void(utils::Entity)> treeNode;
|
||||
|
||||
treeNode = [&](utils::Entity entity) {
|
||||
auto tinstance = tm.getInstance(entity);
|
||||
auto rinstance = rm.getInstance(entity);
|
||||
auto linstance = lm.getInstance(entity);
|
||||
intptr_t treeNodeId = 1 + entity.getId();
|
||||
|
||||
const char* name = mAsset->getName(entity);
|
||||
auto getLabel = [&name, &rinstance, &linstance]() {
|
||||
if (name) {
|
||||
return name;
|
||||
}
|
||||
if (rinstance) {
|
||||
return "Mesh";
|
||||
}
|
||||
if (linstance) {
|
||||
return "Light";
|
||||
}
|
||||
return "Node";
|
||||
};
|
||||
const char* label = getLabel();
|
||||
|
||||
ImGuiTreeNodeFlags flags = 0; // rinstance ? 0 : ImGuiTreeNodeFlags_DefaultOpen;
|
||||
std::vector<utils::Entity> children(tm.getChildCount(tinstance));
|
||||
if (ImGui::TreeNodeEx((const void*) treeNodeId, flags, "%s", label)) {
|
||||
if (rinstance) {
|
||||
renderableTreeItem(entity);
|
||||
}
|
||||
if (linstance) {
|
||||
lightTreeItem(entity);
|
||||
}
|
||||
tm.getChildren(tinstance, children.data(), children.size());
|
||||
for (auto ce : children) {
|
||||
treeNode(ce);
|
||||
}
|
||||
ImGui::TreePop();
|
||||
}
|
||||
};
|
||||
|
||||
// Disable rounding and draw a fixed-height ImGui window that looks like a sidebar.
|
||||
ImGui::GetStyle().WindowRounding = 0;
|
||||
ImGui::SetNextWindowPos(ImVec2(0, 0));
|
||||
|
||||
const float width = ImGui::GetIO().DisplaySize.x;
|
||||
const float height = ImGui::GetIO().DisplaySize.y;
|
||||
ImGui::SetNextWindowSize(ImVec2(mSidebarWidth, height), ImGuiCond_Once);
|
||||
ImGui::SetNextWindowSizeConstraints(ImVec2(20, height), ImVec2(width, height));
|
||||
|
||||
ImGui::Begin("Filament", nullptr, ImGuiWindowFlags_NoTitleBar);
|
||||
if (mCustomUI) {
|
||||
mCustomUI();
|
||||
}
|
||||
|
||||
DebugRegistry& debug = mEngine->getDebugRegistry();
|
||||
|
||||
if (ImGui::CollapsingHeader("View")) {
|
||||
ImGui::Indent();
|
||||
ImGui::Checkbox("Dithering", &mEnableDithering);
|
||||
ImGui::Checkbox("MSAA 4x", &mEnableMsaa);
|
||||
ImGui::Checkbox("TAA", &mTAAOptions.enabled);
|
||||
// this clutters the UI and isn't that useful (except when working on TAA)
|
||||
//ImGui::Indent();
|
||||
//ImGui::SliderFloat("feedback", &mTAAOptions.feedback, 0.0f, 1.0f);
|
||||
//ImGui::SliderFloat("filter", &mTAAOptions.filterWidth, 0.0f, 2.0f);
|
||||
//ImGui::Unindent();
|
||||
ImGui::Checkbox("FXAA", &mEnableFxaa);
|
||||
ImGui::Checkbox("SSAO", &mSSAOOptions.enabled);
|
||||
ImGui::Checkbox("Bloom", &mBloomOptions.enabled);
|
||||
if (ImGui::CollapsingHeader("SSAO Options")) {
|
||||
int quality = (int) mSSAOOptions.quality;
|
||||
int lowpass = (int) mSSAOOptions.lowPassFilter;
|
||||
bool upsampling = mSSAOOptions.upsampling != View::QualityLevel::LOW;
|
||||
ImGui::SliderInt("Quality", &quality, 0, 3);
|
||||
ImGui::SliderInt("Low Pass", &lowpass, 0, 2);
|
||||
ImGui::Checkbox("High quality upsampling", &upsampling);
|
||||
ImGui::SliderFloat("Min Horizon angle", &mSSAOOptions.minHorizonAngleRad, 0.0f, (float)M_PI_4);
|
||||
mSSAOOptions.upsampling = upsampling ? View::QualityLevel::HIGH : View::QualityLevel::LOW;
|
||||
mSSAOOptions.quality = (View::QualityLevel) quality;
|
||||
mSSAOOptions.lowPassFilter = (View::QualityLevel) lowpass;
|
||||
if (ImGui::CollapsingHeader("Dominant Light Shadows (experimental)")) {
|
||||
int sampleCount = mSSAOOptions.ssct.sampleCount;
|
||||
ImGui::Checkbox("Enabled##dls", &mSSAOOptions.ssct.enabled);
|
||||
ImGui::SliderFloat("Cone angle", &mSSAOOptions.ssct.lightConeRad, 0.0f, (float)M_PI_2);
|
||||
ImGui::SliderFloat("Start dist", &mSSAOOptions.ssct.startTraceDistance, 0.0f, 1.0f);
|
||||
ImGui::SliderFloat("Contact dist max", &mSSAOOptions.ssct.contactDistanceMax, 0.0f, 100.0f);
|
||||
ImGui::SliderFloat("Intensity##dls", &mSSAOOptions.ssct.intensity, 0.0f, 10.0f);
|
||||
ImGui::SliderFloat("Depth bias", &mSSAOOptions.ssct.depthBias, 0.0f, 1.0f);
|
||||
ImGui::SliderFloat("Depth slope bias", &mSSAOOptions.ssct.depthSlopeBias, 0.0f, 1.0f);
|
||||
ImGui::SliderFloat("Scale", &mSSAOOptions.ssct.scale, 0.0f, 10.0f);
|
||||
ImGui::SliderInt("Sample Count", &sampleCount, 1, 32);
|
||||
ImGuiExt::DirectionWidget("Direction##dls", mSSAOOptions.ssct.lightDirection.v);
|
||||
mSSAOOptions.ssct.sampleCount = sampleCount;
|
||||
}
|
||||
}
|
||||
ImGui::Unindent();
|
||||
}
|
||||
|
||||
if (ImGui::CollapsingHeader("Light")) {
|
||||
ImGui::Indent();
|
||||
ImGui::SliderFloat("IBL intensity", &mIblIntensity, 0.0f, 100000.0f);
|
||||
ImGui::SliderAngle("IBL rotation", &mIblRotation);
|
||||
ImGui::SliderFloat("Sun intensity", &mSunlightIntensity, 50000.0, 150000.0f);
|
||||
ImGuiExt::DirectionWidget("Sun direction", mSunlightDirection.v);
|
||||
ImGui::Checkbox("Enable sunlight", &mEnableSunlight);
|
||||
ImGui::Checkbox("Enable shadows", &mEnableShadows);
|
||||
ImGui::Checkbox("Enable VSM", &mEnableVsm);
|
||||
ImGui::SliderInt("Cascades", &mShadowCascades, 1, 4);
|
||||
ImGui::Checkbox("Debug cascades", debug.getPropertyAddress<bool>("d.shadowmap.visualize_cascades"));
|
||||
ImGui::Checkbox("Enable contact shadows", &mEnableContactShadows);
|
||||
ImGui::SliderFloat("Split pos 0", &mSplitPositions[0], 0.0f, 1.0f);
|
||||
ImGui::SliderFloat("Split pos 1", &mSplitPositions[1], 0.0f, 1.0f);
|
||||
ImGui::SliderFloat("Split pos 2", &mSplitPositions[2], 0.0f, 1.0f);
|
||||
ImGui::Unindent();
|
||||
}
|
||||
|
||||
if (ImGui::CollapsingHeader("Fog")) {
|
||||
ImGui::Indent();
|
||||
ImGui::Checkbox("Enable fog", &mFogOptions.enabled);
|
||||
ImGui::SliderFloat("Start", &mFogOptions.distance, 0.0f, 100.0f);
|
||||
ImGui::SliderFloat("Density", &mFogOptions.density, 0.0f, 1.0f);
|
||||
ImGui::SliderFloat("Height", &mFogOptions.height, 0.0f, 100.0f);
|
||||
ImGui::SliderFloat("Height falloff", &mFogOptions.heightFalloff, 0.0f, 10.0f);
|
||||
ImGui::SliderFloat("Scattering start", &mFogOptions.inScatteringStart, 0.0f, 100.0f);
|
||||
ImGui::SliderFloat("Scattering size", &mFogOptions.inScatteringSize, 0.1f, 100.0f);
|
||||
ImGui::Checkbox("Color from IBL", &mFogOptions.fogColorFromIbl);
|
||||
ImGui::ColorPicker3("Color", mFogOptions.color.v);
|
||||
ImGui::Unindent();
|
||||
}
|
||||
|
||||
mView->setDithering(mEnableDithering ? View::Dithering::TEMPORAL : View::Dithering::NONE);
|
||||
mView->setAntiAliasing(mEnableFxaa ? View::AntiAliasing::FXAA : View::AntiAliasing::NONE);
|
||||
mView->setSampleCount(mEnableMsaa ? 4 : 1);
|
||||
mView->setAmbientOcclusionOptions(mSSAOOptions);
|
||||
mView->setBloomOptions(mBloomOptions);
|
||||
mView->setFogOptions(mFogOptions);
|
||||
mView->setTemporalAntiAliasingOptions(mTAAOptions);
|
||||
|
||||
if (mEnableSunlight) {
|
||||
mScene->addEntity(mSunlight);
|
||||
auto sun = lm.getInstance(mSunlight);
|
||||
lm.setIntensity(sun, mSunlightIntensity);
|
||||
lm.setDirection(sun, normalize(mSunlightDirection));
|
||||
lm.setColor(sun, mSunlightColor);
|
||||
lm.setShadowCaster(sun, mEnableShadows);
|
||||
} else {
|
||||
mScene->remove(mSunlight);
|
||||
}
|
||||
|
||||
lm.forEachComponent([this, &lm](utils::Entity e, LightManager::Instance ci) {
|
||||
auto options = lm.getShadowOptions(ci);
|
||||
options.screenSpaceContactShadows = mEnableContactShadows;
|
||||
options.shadowCascades = mShadowCascades;
|
||||
std::copy_n(mSplitPositions.begin(), 3, options.cascadeSplitPositions);
|
||||
lm.setShadowOptions(ci, options);
|
||||
lm.setShadowCaster(ci, mEnableShadows);
|
||||
});
|
||||
|
||||
mView->setShadowType(mEnableVsm ? View::ShadowType::VSM : View::ShadowType::PCF);
|
||||
|
||||
if (mAsset != nullptr) {
|
||||
if (ImGui::CollapsingHeader("Hierarchy")) {
|
||||
ImGui::Indent();
|
||||
ImGui::Checkbox("Show bounds", &mEnableWireframe);
|
||||
treeNode(mAsset->getRoot());
|
||||
ImGui::Unindent();
|
||||
}
|
||||
|
||||
if (mAnimator->getAnimationCount() > 0 && ImGui::CollapsingHeader("Animation")) {
|
||||
ImGui::Indent();
|
||||
int selectedAnimation = mCurrentAnimation;
|
||||
ImGui::RadioButton("Disable", &selectedAnimation, 0);
|
||||
for (size_t i = 0, count = mAnimator->getAnimationCount(); i < count; ++i) {
|
||||
std::string label = mAnimator->getAnimationName(i);
|
||||
if (label.empty()) {
|
||||
label = "Unnamed " + std::to_string(i);
|
||||
}
|
||||
ImGui::RadioButton(label.c_str(), &selectedAnimation, i + 1);
|
||||
}
|
||||
if (selectedAnimation != mCurrentAnimation) {
|
||||
mCurrentAnimation = selectedAnimation;
|
||||
mResetAnimation = true;
|
||||
}
|
||||
ImGui::Unindent();
|
||||
}
|
||||
|
||||
if (mEnableWireframe) {
|
||||
mScene->addEntity(mAsset->getWireframe());
|
||||
} else {
|
||||
mScene->remove(mAsset->getWireframe());
|
||||
}
|
||||
}
|
||||
|
||||
mSidebarWidth = ImGui::GetWindowWidth();
|
||||
ImGui::End();
|
||||
|
||||
updateIndirectLight();
|
||||
}
|
||||
|
||||
} // namespace viewer
|
||||
} // namespace filament
|
||||
Reference in New Issue
Block a user