From 8142ffdfb768de1724f802e65182d5734d961817 Mon Sep 17 00:00:00 2001 From: Philip Rideout Date: Thu, 6 Jun 2019 17:17:45 -0700 Subject: [PATCH] Add bloom demo and release notes for #119. --- RELEASE_NOTES.md | 2 + filament/include/filament/RenderableManager.h | 4 + filament/src/details/Texture.h | 1 + samples/CMakeLists.txt | 11 +- samples/app/Config.h | 1 + samples/app/FilamentApp.cpp | 8 +- samples/app/FilamentApp.h | 3 + samples/gltf_baker.cpp | 4 +- samples/lucy_bloom.cpp | 404 ++++++++++++++++++ samples/lucy_utils.cpp | 382 +++++++++++++++++ samples/lucy_utils.h | 38 ++ third_party/models/lucy/lucy.glb | Bin 0 -> 792828 bytes 12 files changed, 854 insertions(+), 4 deletions(-) create mode 100644 samples/lucy_bloom.cpp create mode 100644 samples/lucy_utils.cpp create mode 100644 samples/lucy_utils.h create mode 100644 third_party/models/lucy/lucy.glb diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index a173109cfd..eb9132cad3 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -6,6 +6,8 @@ A new header is inserted each time a *tag* is created. # Release notes +- Introduced `RenderTarget` API that allows View to reference an offscreen render target. +- Added `lucy_bloom` sample to demonstrate the new `RenderTarget` API. - Added Screen Space Ambient Occlusion support (SAO) - New blending modes: `multiply` and `screen` - Fixed an issue when sorting blended objects with different blending modes diff --git a/filament/include/filament/RenderableManager.h b/filament/include/filament/RenderableManager.h index 762156cfe8..0d2f877e0d 100644 --- a/filament/include/filament/RenderableManager.h +++ b/filament/include/filament/RenderableManager.h @@ -76,6 +76,7 @@ public: Builder& material(size_t index, MaterialInstance const* materialInstance) noexcept; // The axis aligned bounding box of the Renderable. Mandatory unless culling is disabled. Builder& boundingBox(const Box& axisAlignedBoundingBox) noexcept; + // See View::setVisibleLayers(). Builder& layerMask(uint8_t select, uint8_t values) noexcept; // The priority is clamped to the range [0..7], defaults to 4; 7 is lowest priority Builder& priority(uint8_t priority) noexcept; @@ -130,7 +131,10 @@ public: void destroy(utils::Entity e) noexcept; void setAxisAlignedBoundingBox(Instance instance, const Box& aabb) noexcept; + + // See View::setVisibleLayers void setLayerMask(Instance instance, uint8_t select, uint8_t values) noexcept; + void setPriority(Instance instance, uint8_t priority) noexcept; void setCastShadows(Instance instance, bool enable) noexcept; void setReceiveShadows(Instance instance, bool enable) noexcept; diff --git a/filament/src/details/Texture.h b/filament/src/details/Texture.h index 59a9cc261f..b41face538 100644 --- a/filament/src/details/Texture.h +++ b/filament/src/details/Texture.h @@ -50,6 +50,7 @@ public: size_t getLevels() const noexcept { return mLevels; } Sampler getTarget() const noexcept { return mTarget; } InternalFormat getFormat() const noexcept { return mFormat; } + Usage getUsage() const noexcept { return mUsage; } bool isRgbm() const noexcept { return mRgbm; } void setImage(FEngine& engine, size_t level, diff --git a/samples/CMakeLists.txt b/samples/CMakeLists.txt index bd3b2bf883..fda364986b 100644 --- a/samples/CMakeLists.txt +++ b/samples/CMakeLists.txt @@ -149,13 +149,18 @@ endif() add_library(suzanne-resources ${DUMMY_SRC} ${RESGEN_SOURCE}) # ================================================================================================== -# Resources for gltf_viewer +# Resources for the gltf_viewer and lucy demos # ================================================================================================== get_resgen_vars(${RESOURCE_DIR} gltf) set(GLTF_RESOURCE_FILES ${ROOT_DIR}/third_party/models/DamagedHelmet/DamagedHelmet.glb + ${ROOT_DIR}/third_party/models/lucy/lucy.glb + ${ROOT_DIR}/third_party/textures/Blue_tiles_01/Blue_tiles_01_AO.png + ${ROOT_DIR}/third_party/textures/Blue_tiles_01/Blue_tiles_01_Color.png + ${ROOT_DIR}/third_party/textures/Blue_tiles_01/Blue_tiles_01_Normal.png + ${ROOT_DIR}/third_party/textures/Blue_tiles_01/Blue_tiles_01_Roughness.png ) add_custom_command( @@ -263,6 +268,10 @@ if (NOT ANDROID) target_link_libraries(suzanne PRIVATE suzanne-resources) target_link_libraries(gltf_viewer PRIVATE gltf-resources gltfio) target_link_libraries(gltf_baker PRIVATE gltf-resources gltfio imageio) + + add_executable(lucy_bloom lucy_bloom.cpp lucy_utils.h lucy_utils.cpp) + target_compile_options(lucy_bloom PRIVATE ${COMPILER_FLAGS}) + target_link_libraries(lucy_bloom PRIVATE ${APP_LIBS} gltf-resources gltfio) endif() # ================================================================================================== diff --git a/samples/app/Config.h b/samples/app/Config.h index 8efe32d3e0..5145573179 100644 --- a/samples/app/Config.h +++ b/samples/app/Config.h @@ -27,6 +27,7 @@ struct Config { float scale = 1.0f; bool splitView = false; filament::Engine::Backend backend = filament::Engine::Backend::OPENGL; + bool resizeable = true; }; #endif // TNT_FILAMENT_SAMPLE_CONFIG_H diff --git a/samples/app/FilamentApp.cpp b/samples/app/FilamentApp.cpp index edec2dd357..b2d5c528a6 100644 --- a/samples/app/FilamentApp.cpp +++ b/samples/app/FilamentApp.cpp @@ -371,6 +371,9 @@ void FilamentApp::run(const Config& config, SetupCallback setupCallback, } if (renderer->beginFrame(window->getSwapChain())) { + for (filament::View* offscreenView : mOffscreenViews) { + renderer->render(offscreenView); + } for (auto const& view : window->mViews) { renderer->render(view->getView()); } @@ -442,7 +445,10 @@ FilamentApp::Window::Window(FilamentApp* filamentApp, : mFilamentApp(filamentApp) { const int x = SDL_WINDOWPOS_CENTERED; const int y = SDL_WINDOWPOS_CENTERED; - const uint32_t windowFlags = SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI; + uint32_t windowFlags = SDL_WINDOW_SHOWN | SDL_WINDOW_ALLOW_HIGHDPI; + if (config.resizeable) { + windowFlags |= SDL_WINDOW_RESIZABLE; + } mWindow = SDL_CreateWindow(title.c_str(), x, y, (int) w, (int) h, windowFlags); // Create the Engine after the window in case this happens to be a single-threaded platform. diff --git a/samples/app/FilamentApp.h b/samples/app/FilamentApp.h index 82880d31e8..0f19251622 100644 --- a/samples/app/FilamentApp.h +++ b/samples/app/FilamentApp.h @@ -82,6 +82,8 @@ public: void setSidebarWidth(int width) { mSidebarWidth = width; } void setWindowTitle(const char* title) { mWindowTitle = title; } + void addOffscreenView(filament::View* view) { mOffscreenViews.push_back(view); } + size_t getSkippedFrameCount() const { return mSkippedFrames; } FilamentApp(const FilamentApp& rhs) = delete; @@ -214,6 +216,7 @@ private: int mSidebarWidth = 0; size_t mSkippedFrames = 0; std::string mWindowTitle; + std::vector mOffscreenViews; }; #endif // TNT_FILAMENT_SAMPLE_FILAMENTAPP_H diff --git a/samples/gltf_baker.cpp b/samples/gltf_baker.cpp index c05f7507f8..74d1add8e4 100644 --- a/samples/gltf_baker.cpp +++ b/samples/gltf_baker.cpp @@ -253,7 +253,7 @@ static void createQuadRenderable(BakerApp& app) { static OverlayVertex kVertices[4] = { {{0, 0}, {0, 0}}, {{ 1000, 0}, {1, 0}}, {{0, 1000}, {0, 1}}, {{ 1000, 1000}, {1, 1}} }; - static constexpr uint16_t kInidices[6] = { 0, 1, 2, 3, 2, 1 }; + static constexpr uint16_t kIndices[6] = { 0, 1, 2, 3, 2, 1 }; if (!app.overlayQuad.entity) { app.overlayQuad.vb = VertexBuffer::Builder() @@ -267,7 +267,7 @@ static void createQuadRenderable(BakerApp& app) { .bufferType(IndexBuffer::IndexType::USHORT) .build(engine); app.overlayQuad.ib->setBuffer(engine, - IndexBuffer::BufferDescriptor(kInidices, 12, nullptr)); + IndexBuffer::BufferDescriptor(kIndices, 12, nullptr)); auto mat = Material::Builder() .package(RESOURCES_AOPREVIEW_DATA, RESOURCES_AOPREVIEW_SIZE) .build(engine); diff --git a/samples/lucy_bloom.cpp b/samples/lucy_bloom.cpp new file mode 100644 index 0000000000..6a07958724 --- /dev/null +++ b/samples/lucy_bloom.cpp @@ -0,0 +1,404 @@ +/* + * 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 "app/Config.h" +#include "app/FilamentApp.h" +#include "app/IBL.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include + +#include + +#include "lucy_utils.h" + +using namespace filament; +using namespace filament::math; +using namespace gltfio; +using namespace utils; + +static constexpr uint32_t FBO_WIDTH = 1024; +static constexpr uint32_t FBO_HEIGHT = 1024; +static const uint32_t WINDOW_WIDTH = 800; +static const uint32_t WINDOW_HEIGHT = 800; + +struct Framebuffer { + Camera* camera = nullptr; + View* view = nullptr; + Scene* scene = nullptr; + Texture* color = nullptr; + Texture* depth = nullptr; + RenderTarget* target = nullptr; +}; + +struct LucyApp { + Config config; + bool showQuads; + bool showImgui; + float iblIntensity = 40000; + float iblRotation = M_PI * 1.5f; + AssetLoader* loader; + FilamentAsset* asset; + MaterialProvider* materials; + Entity rotationRoot; + Entity hudQuads[4]; + Entity finalQuad; + Entity hblurQuad; + Entity vblurQuad; + Framebuffer reflection; + Framebuffer primary; + Framebuffer hblur; + Framebuffer vblur; + Camera* finalCamera; +}; + +static void setup(LucyApp& app, Engine* engine, View* finalView, Scene* finalScene) { + + app.reflection.color = Texture::Builder() + .width(FBO_WIDTH).height(FBO_HEIGHT).levels(1) + .usage(Texture::Usage::COLOR_ATTACHMENT | Texture::Usage::SAMPLEABLE) + .format(Texture::InternalFormat::RGBA8).build(*engine); + app.reflection.depth = Texture::Builder() + .width(FBO_WIDTH).height(FBO_HEIGHT).levels(1) + .usage(Texture::Usage::DEPTH_ATTACHMENT) + .format(Texture::InternalFormat::DEPTH24).build(*engine); + app.reflection.target = RenderTarget::Builder() + .color(app.reflection.color) .depth(app.reflection.depth) .build(*engine); + + app.primary.color = Texture::Builder() + .width(FBO_WIDTH).height(FBO_HEIGHT).levels(1) + .usage(Texture::Usage::COLOR_ATTACHMENT | Texture::Usage::SAMPLEABLE) + .format(Texture::InternalFormat::RGBA16F).build(*engine); + app.primary.depth = Texture::Builder() + .width(FBO_WIDTH).height(FBO_HEIGHT).levels(1) + .usage(Texture::Usage::DEPTH_ATTACHMENT) + .format(Texture::InternalFormat::DEPTH24).build(*engine); + app.primary.target = RenderTarget::Builder() + .color(app.primary.color) .depth(app.primary.depth) .build(*engine); + + app.hblur.color = Texture::Builder() + .width(FBO_WIDTH).height(FBO_HEIGHT).levels(1) + .usage(Texture::Usage::COLOR_ATTACHMENT | Texture::Usage::SAMPLEABLE) + .format(Texture::InternalFormat::RGBA16F).build(*engine); + app.hblur.target = RenderTarget::Builder().color(app.hblur.color).build(*engine); + + app.vblur.color = Texture::Builder() + .width(FBO_WIDTH).height(FBO_HEIGHT).levels(1) + .usage(Texture::Usage::COLOR_ATTACHMENT | Texture::Usage::SAMPLEABLE) + .format(Texture::InternalFormat::RGBA16F).build(*engine); + app.vblur.target = RenderTarget::Builder().color(app.vblur.color).build(*engine); + + // Create lights. + auto* ibl = FilamentApp::get().getIBL(); + ibl->getIndirectLight()->setIntensity(app.iblIntensity); + ibl->getIndirectLight()->setRotation(mat3f::rotation(app.iblRotation, float3{ 0, 1, 0 })); + ibl->getSkybox()->setLayerMask(0xff, 0xff); + + auto sunlight = EntityManager::get().create(); + LightManager::Builder(LightManager::Type::SUN) + .color(Color::toLinear({0.98, 0.92, 0.89})) + .intensity(50000) + .direction(normalize(float3 {0.6, -1.0, -0.8})) + .castShadows(true) + .sunAngularRadius(1.9) + .sunHaloSize(10.0) + .sunHaloFalloff(80.0) + .build(*engine, sunlight); + + // Create filament entities for the embedded glTF model. + app.materials = createMaterialGenerator(engine); + app.loader = AssetLoader::create({engine, app.materials }); + app.asset = app.loader->createAssetFromBinary(RESOURCE_ARGS(LUCY)); + gltfio::ResourceLoader({ .engine = engine }).loadResources(app.asset); + app.asset->releaseSourceData(); + + // Tweak the model materials. + auto begin = app.asset->getMaterialInstances(); + auto end = begin + app.asset->getMaterialInstanceCount(); + for (auto iter = begin; iter != end; ++iter) { + (*iter)->setParameter("roughnessFactor", 0.25f); + (*iter)->setParameter("baseColorFactor", float4 {1.0, 0.6, 0.5, 1.0}); + } + + // Transform the model to fit into the unit cube, then parent it under a rotation node. + auto& tcm = engine->getTransformManager(); + auto root = tcm.getInstance(app.asset->getRoot()); + mat4f transform = LucyUtils::fitIntoUnitCube(app.asset->getBoundingBox()); + tcm.setTransform(root, transform); + app.rotationRoot = EntityManager::get().create(); + tcm.create(app.rotationRoot); + tcm.setParent(root, tcm.getInstance(app.rotationRoot)); + + // Create the podium. + Entity disk = LucyUtils::createDisk(engine, app.reflection.color); + mat4f diskTransform = mat4f::scaling(float3 {1.5f, 1.5, 1.0f}); + diskTransform = mat4f::rotation(-M_PI / 2, float3 {1, 0, 0}) * diskTransform; + diskTransform = mat4f::translation(float3 {0, -1, 0}) * diskTransform; + tcm.create(disk, tcm.getInstance(app.rotationRoot), diskTransform); + + // Create full-screen quads. + app.finalQuad = LucyUtils::createQuad(engine, app.primary.color, LucyUtils::MIX, + app.vblur.color); + app.hblurQuad = LucyUtils::createQuad(engine, app.primary.color, LucyUtils::HBLUR); + app.vblurQuad = LucyUtils::createQuad(engine, app.hblur.color, LucyUtils::VBLUR); + + mat4f quadScale = mat4f::scaling(float3 {FBO_WIDTH, FBO_HEIGHT, 1}); + tcm.setTransform(tcm.getInstance(app.hblurQuad), quadScale); + tcm.setTransform(tcm.getInstance(app.vblurQuad), quadScale); + + app.reflection.scene = engine->createScene(); + app.reflection.scene->setIndirectLight(ibl->getIndirectLight()); + app.reflection.scene->addEntities(app.asset->getEntities(), app.asset->getEntityCount()); + app.reflection.scene->addEntity(sunlight); + + app.primary.scene = engine->createScene(); + app.primary.scene->addEntity(disk); + app.primary.scene->addEntities(app.asset->getEntities(), app.asset->getEntityCount()); + app.primary.scene->addEntity(sunlight); + app.primary.scene->setSkybox(ibl->getSkybox()); + app.primary.scene->setIndirectLight(ibl->getIndirectLight()); + + app.hblur.scene = engine->createScene(); + app.hblur.scene->addEntity(app.hblurQuad); + app.vblur.scene = engine->createScene(); + app.vblur.scene->addEntity(app.vblurQuad); + + app.reflection.camera = engine->createCamera(); + app.reflection.view = engine->createView(); + app.reflection.view->setName("reflection"); + app.reflection.view->setRenderTarget(app.reflection.target); + app.reflection.view->setClearColor({0, 0, 0, 1}); + app.reflection.view->setClearTargets(true, true, false); + app.reflection.view->setViewport(Viewport(0, 0, FBO_WIDTH, FBO_HEIGHT)); + app.reflection.view->setScene(app.reflection.scene); + app.reflection.view->setCamera(app.reflection.camera); + app.reflection.view->setToneMapping(View::ToneMapping::LINEAR); + FilamentApp::get().addOffscreenView(app.reflection.view); + + app.primary.camera = engine->createCamera(); + app.primary.view = engine->createView(); + app.primary.view->setName("primary"); + app.primary.view->setRenderTarget(app.primary.target); + app.primary.view->setViewport(Viewport(0, 0, FBO_WIDTH, FBO_HEIGHT)); + app.primary.view->setScene(app.primary.scene); + app.primary.view->setCamera(app.primary.camera); + app.primary.view->setToneMapping(View::ToneMapping::LINEAR); + FilamentApp::get().addOffscreenView(app.primary.view); + + app.hblur.camera = engine->createCamera(); + app.hblur.view = engine->createView(); + app.hblur.view->setName("hblur"); + app.hblur.view->setRenderTarget(app.hblur.target); + app.hblur.view->setViewport(Viewport(0, 0, FBO_WIDTH, FBO_HEIGHT)); + app.hblur.view->setScene(app.hblur.scene); + app.hblur.view->setCamera(app.hblur.camera); + app.hblur.view->setPostProcessingEnabled(false); + FilamentApp::get().addOffscreenView(app.hblur.view); + app.hblur.camera->setProjection(Camera::Projection::ORTHO, + 0.0, FBO_WIDTH, FBO_HEIGHT, 0.0, 0.0, 1.0); + + app.vblur.camera = engine->createCamera(); + app.vblur.view = engine->createView(); + app.vblur.view->setName("vblur"); + app.vblur.view->setRenderTarget(app.vblur.target); + app.vblur.view->setViewport(Viewport(0, 0, FBO_WIDTH, FBO_HEIGHT)); + app.vblur.view->setScene(app.vblur.scene); + app.vblur.view->setCamera(app.vblur.camera); + app.vblur.view->setPostProcessingEnabled(false); + FilamentApp::get().addOffscreenView(app.vblur.view); + app.vblur.camera->setProjection(Camera::Projection::ORTHO, + 0.0, FBO_WIDTH, FBO_HEIGHT, 0.0, 0.0, 1.0); + + app.finalCamera = engine->createCamera(); + finalScene->addEntity(app.finalQuad); + finalScene->setSkybox(nullptr); + finalScene->setIndirectLight(nullptr); + finalView->setSampleCount(1); + finalView->setCamera(app.finalCamera); +}; + +static float3 reflect(float3 i, float3 n) { return i - 2.0f * dot(n, i) * n; }; + +static void animate(LucyApp& app, Engine* engine, View* finalView, double now) { + // Rotate the embedded mesh. (use now = -0.5 for a nice screenshot) + const mat4f xform = mat4f::rotation(now, float3 {0, 1, 0}); + auto& tcm = engine->getTransformManager(); + tcm.setTransform(tcm.getInstance(app.rotationRoot), xform); + + // Adjust the projection in case the window has been resized. + const auto viewport = finalView->getViewport(); + const float aspect = float(viewport.width) / viewport.height; + app.primary.camera->setProjection(30, aspect, 1.0, 10.0); + app.reflection.camera->setProjection(30, aspect, 1.0, 10.0); + app.finalCamera->setProjection(Camera::Projection::ORTHO, + 0.0, viewport.width, viewport.height, 0.0, 0.0, 1.0); + + // Adjust the size of the final quad in case the window has been resized. + mat4f finalQuadSize = mat4f::scaling(float3 {viewport.width, viewport.height, 1}); + tcm.setTransform(tcm.getInstance(app.finalQuad), finalQuadSize); + + // Primary camera. The target point will be moved along the gazing direction to aid reflection. + const float3 eye = {0.0f, 1.5f, 4.0f}; + float3 target = {0, 0, 0}; + + // Find where the central camera ray intersects the podium. + const float3 gaze = normalize(target - eye); + const float planeD = -1; + const float3 planeN = {0, 1, 0}; + const float t = dot(planeN * planeD - eye, planeN) / dot(gaze, planeN); + target = eye + t * gaze; + + // Set up the primary camera. + const float3 up = cross(gaze, float3 {-1, 0, 0}); + app.primary.camera->lookAt(eye, target, up); + + // Find the location of the reflected camera. + const float eyedist = length(target - eye); + const float3 newgaze = reflect(gaze, planeN); + const float3 neweye = target - eyedist * newgaze; + const float3 newup = cross(newgaze, float3 {-1, 0, 0}); + app.reflection.camera->lookAt(neweye, neweye + newgaze, newup); +}; + +static void drawHud(LucyApp& app, Engine* engine, View* hudView) { + Scene* hudScene = hudView->getScene(); + if (!app.hudQuads[0] && app.showQuads) { + auto& tcm = engine->getTransformManager(); + app.hudQuads[0] = LucyUtils::createQuad(engine, app.reflection.color, LucyUtils::BLIT); + app.hudQuads[1] = LucyUtils::createQuad(engine, app.primary.color, LucyUtils::BLIT); + app.hudQuads[2] = LucyUtils::createQuad(engine, app.hblur.color, LucyUtils::BLIT); + app.hudQuads[3] = LucyUtils::createQuad(engine, app.vblur.color, LucyUtils::BLIT); + hudScene->addEntity(app.hudQuads[0]); + hudScene->addEntity(app.hudQuads[1]); + hudScene->addEntity(app.hudQuads[2]); + hudScene->addEntity(app.hudQuads[3]); + mat4f scale = mat4f::scaling(float3 {100, 100, 1}); + tcm.create(app.hudQuads[0], {}, mat4f::translation(float3 {10, 10, 0}) * scale); + tcm.create(app.hudQuads[1], {}, mat4f::translation(float3 {10, 120, 0}) * scale); + tcm.create(app.hudQuads[2], {}, mat4f::translation(float3 {10, 230, 0}) * scale); + tcm.create(app.hudQuads[3], {}, mat4f::translation(float3 {10, 340, 0}) * scale); + } + if (app.showImgui) { + ImGui::SetNextWindowSize(ImVec2(305, 90)); + ImGui::Begin("Parameters"); + ImGui::SliderFloat("ibl", &app.iblIntensity, 0.0f, 50000.0f); + ImGui::SliderAngle("ibl rotation", &app.iblRotation); + ImGui::End(); + auto* ibl = FilamentApp::get().getIBL(); + ibl->getIndirectLight()->setIntensity(app.iblIntensity); + ibl->getIndirectLight()->setRotation(mat3f::rotation(app.iblRotation, float3{ 0, 1, 0 })); + } +} + +static void cleanup(LucyApp& app, Engine* engine) { + engine->destroy(app.hudQuads[0]); + engine->destroy(app.hudQuads[1]); + engine->destroy(app.hudQuads[2]); + engine->destroy(app.hudQuads[3]); + + engine->destroy(app.hblurQuad); + engine->destroy(app.finalQuad); + + engine->destroy(app.hblur.camera); + engine->destroy(app.hblur.scene); + engine->destroy(app.hblur.view); + engine->destroy(app.hblur.color); + engine->destroy(app.hblur.target); + + engine->destroy(app.vblur.camera); + engine->destroy(app.vblur.scene); + engine->destroy(app.vblur.view); + engine->destroy(app.vblur.color); + engine->destroy(app.vblur.target); + + engine->destroy(app.reflection.camera); + engine->destroy(app.reflection.scene); + engine->destroy(app.reflection.view); + engine->destroy(app.reflection.color); + engine->destroy(app.reflection.depth); + engine->destroy(app.reflection.target); + + engine->destroy(app.primary.camera); + engine->destroy(app.primary.scene); + engine->destroy(app.primary.view); + engine->destroy(app.primary.color); + engine->destroy(app.primary.depth); + engine->destroy(app.primary.target); + + engine->destroy(app.finalCamera); + + Fence::waitAndDestroy(engine->createFence()); + app.loader->destroyAsset(app.asset); + app.materials->destroyMaterials(); + delete app.materials; + AssetLoader::destroy(&app.loader); +}; + +int main(int argc, char** argv) { + static const char* DEFAULT_IBL = "envs/pillars"; + + LucyApp app; + app.config.title = "Lucy"; + app.config.iblDirectory = FilamentApp::getRootPath() + DEFAULT_IBL; + app.config.resizeable = false; + app.showQuads = true; + app.showImgui = true; + + // Handle arguments. + static constexpr const char* OPTSTR = "i:"; + static const struct option OPTIONS[] = { + { "ibl", required_argument, nullptr, 'i' }, + { nullptr, 0, nullptr, 0 } + }; + int opt, option_index = 0; + while ((opt = getopt_long(argc, argv, OPTSTR, OPTIONS, &option_index)) >= 0) { + std::string arg(optarg ? optarg : ""); + switch (opt) { + case 'i': app.config.iblDirectory = arg; break; + } + } + + // Configure the animation callback. + FilamentApp::get().animate( + [&app](Engine* engine, View* view, double now) { animate(app, engine, view, now); }); + + // Start the app. + FilamentApp::get().run(app.config, + [&app](Engine* engine, View* view, Scene* scene) { setup(app, engine, view, scene); }, + [&app](Engine* engine, View*, Scene*) { cleanup(app, engine); }, + [&app](Engine* engine, View* view) { drawHud(app, engine, view); }, + FilamentApp::PreRenderCallback(), + FilamentApp::PostRenderCallback(), + WINDOW_WIDTH, WINDOW_HEIGHT); +} diff --git a/samples/lucy_utils.cpp b/samples/lucy_utils.cpp new file mode 100644 index 0000000000..2aa3475fe1 --- /dev/null +++ b/samples/lucy_utils.cpp @@ -0,0 +1,382 @@ +/* + * 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 "lucy_utils.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +using namespace filament; +using namespace filament::math; +using namespace utils; + +using filament::geometry::SurfaceOrientation; + +#define FILTER_SIZE 9 + +namespace LucyUtils { + +constexpr float M_PIf = float(M_PI); + +Entity createQuad(Engine* engine, Texture* tex, ImageOp op, Texture* secondary) { + + static VertexBuffer* vb = [](Engine& engine) { + struct OverlayVertex { + float2 position; + float2 uv; + quath tangent; + }; + static OverlayVertex verts[4] = { + {{0, 0}, {0, 0}, {0, 0, 0, 1}}, + {{1, 0}, {1, 0}, {0, 0, 0, 1}}, + {{0, 1}, {0, 1}, {0, 0, 0, 1}}, + {{1, 1}, {1, 1}, {0, 0, 0, 1}} + }; + static float3 normals[4] = { + {0, 0, 1}, + {0, 0, 1}, + {0, 0, 1}, + {0, 0, 1}, + }; + SurfaceOrientation::Builder() + .vertexCount(4) + .normals(normals) + .build() + .getQuats(2 + (quath*) verts, 4, 24); + auto vb = VertexBuffer::Builder() + .vertexCount(4) + .bufferCount(1) + .attribute(VertexAttribute::POSITION, 0, VertexBuffer::AttributeType::FLOAT2, 0, 24) + .attribute(VertexAttribute::UV0, 0, VertexBuffer::AttributeType::FLOAT2, 8, 24) + .attribute(VertexAttribute::TANGENTS, 0, VertexBuffer::AttributeType::HALF4, 16, 24) + .build(engine); + vb->setBufferAt(engine, 0, VertexBuffer::BufferDescriptor(verts, sizeof(verts), nullptr)); + return vb; + }(*engine); + + static IndexBuffer* ib = [](Engine& engine) { + static constexpr uint16_t indices[6] = { 2, 1, 0, 1, 2, 3 }; + auto ib = IndexBuffer::Builder() + .indexCount(6) + .bufferType(IndexBuffer::IndexType::USHORT) + .build(engine); + ib->setBuffer(engine, IndexBuffer::BufferDescriptor(indices, sizeof(indices), nullptr)); + return ib; + }(*engine); + + static Material* blit = [](Engine& engine) { + filamat::Package pkg = filamat::MaterialBuilder() + .name("blit") + .material(R"SHADER( + void material(inout MaterialInputs material) { + prepareMaterial(material); + material.baseColor = texture(materialParams_color, getUV0()); + } + )SHADER") + .require(VertexAttribute::UV0) + .parameter(filamat::MaterialBuilder::SamplerType::SAMPLER_2D, "color") + .targetApi(filamat::MaterialBuilder::TargetApi::OPENGL) + .shading(Shading::UNLIT) + .depthWrite(false) + .depthCulling(false) + .build(); + return Material::Builder().package(pkg.getData(), pkg.getSize()).build(engine); + }(*engine); + + static Material* mix = [](Engine& engine) { + filamat::Package pkg = filamat::MaterialBuilder() + .name("mix") + .material(R"SHADER( + void material(inout MaterialInputs material) { + prepareMaterial(material); + vec4 primary = texture(materialParams_color, getUV0()); + vec4 blurred = texture(materialParams_secondary, getUV0()); + // HACK: this is a crude bloom effect + float L = max(0.0, max(blurred.r, max(blurred.g, blurred.b)) - 1.0); + material.baseColor = mix(primary, blurred, min(1.0, L)); + } + )SHADER") + .require(VertexAttribute::UV0) + .parameter(filamat::MaterialBuilder::SamplerType::SAMPLER_2D, "color") + .parameter(filamat::MaterialBuilder::SamplerType::SAMPLER_2D, "secondary") + .targetApi(filamat::MaterialBuilder::TargetApi::OPENGL) + .shading(Shading::UNLIT) + .depthWrite(false) + .depthCulling(false) + .build(); + return Material::Builder().package(pkg.getData(), pkg.getSize()).build(engine); + }(*engine); + + static const float4* weights = []() { + static float4 weights[FILTER_SIZE * 2]; + float4* hweights = weights; + float4* vweights = weights + FILTER_SIZE; + static const float radius = 2; + const auto filter = [](float t) { + t /= 2.0; + if (t >= 1.0) return 0.0f; + const float scale = 1.0f / std::sqrt(0.5f * M_PIf); + return std::exp(-2.0f * t * t) * scale; + }; + constexpr float pixelWidth = 2.0f / float(FILTER_SIZE); + float sum = 0.0f; + for (int i = 0; i < FILTER_SIZE; i++) { + float x = -1.0f + pixelWidth / 2.0f + pixelWidth * i; + hweights[i].x = vweights[i].x = filter(std::abs(x)); + hweights[i].y = vweights[i].z = radius * (i - (FILTER_SIZE - 1) / 2); + hweights[i].z = vweights[i].y = 0.0f; + sum += weights[i].x; + } + for (int i = 0; i < FILTER_SIZE; i++) { + hweights[i].x /= sum; + vweights[i].x /= sum; + } + return weights; + }(); + static const float4* hweights = weights; + static const float4* vweights = weights + FILTER_SIZE; + + static Material* blur = [](Engine& engine) { + std::string txt = R"SHADER( + void material(inout MaterialInputs material) { + prepareMaterial(material); + float2 uv = gl_FragCoord.xy; + vec4 c = vec4(0); + for (int i = 0; i < FILTER_SIZE; i++) { + float2 st = (uv + materialParams.weights[i].yz) * getResolution().zw; + c += texture(materialParams_color, st) * materialParams.weights[i].x; + } + material.baseColor = c; + } + )SHADER"; + const std::string from("FILTER_SIZE"); + const std::string to = std::to_string(FILTER_SIZE); + size_t pos = txt.find(from); + txt.replace(pos, from.length(), to.c_str()); + filamat::Package pkg = filamat::MaterialBuilder() + .name("hblur") + .material(txt.c_str()) + .require(VertexAttribute::UV0) + .parameter(filamat::MaterialBuilder::SamplerType::SAMPLER_2D, "color") + .parameter(filamat::MaterialBuilder::UniformType::FLOAT4, FILTER_SIZE, "weights") + .targetApi(filamat::MaterialBuilder::TargetApi::OPENGL) + .shading(Shading::UNLIT) + .depthWrite(false) + .depthCulling(false) + .build(); + return Material::Builder().package(pkg.getData(), pkg.getSize()).build(engine); + }(*engine); + + TextureSampler::MinFilter minfilt; + TextureSampler::MagFilter magfilt; + MaterialInstance* matinstance; + switch (op) { + case BLIT: + minfilt = TextureSampler::MinFilter::LINEAR; + magfilt = TextureSampler::MagFilter::LINEAR; + matinstance = blit->createInstance(); + break; + case HBLUR: + minfilt = TextureSampler::MinFilter::NEAREST; + magfilt = TextureSampler::MagFilter::NEAREST; + matinstance = blur->createInstance(); + matinstance->setParameter("weights", hweights, FILTER_SIZE); + break; + case VBLUR: + minfilt = TextureSampler::MinFilter::NEAREST; + magfilt = TextureSampler::MagFilter::NEAREST; + matinstance = blur->createInstance(); + matinstance->setParameter("weights", vweights, FILTER_SIZE); + break; + case MIX: + minfilt = TextureSampler::MinFilter::LINEAR; + magfilt = TextureSampler::MagFilter::LINEAR; + matinstance = mix->createInstance(); + matinstance->setParameter("secondary", secondary, TextureSampler(minfilt, magfilt)); + break; + } + matinstance->setParameter("color", tex, TextureSampler(minfilt, magfilt)); + + Entity entity = EntityManager::get().create(); + RenderableManager::Builder(1) + .boundingBox({{ 0, 0, 0 }, { 9000, 9000, 9000 }}) + .material(0, matinstance) + .geometry(0, RenderableManager::PrimitiveType::TRIANGLES, vb, ib) + .build(*engine, entity); + + return entity; +} + +Entity createDisk(Engine* engine, Texture* reflection) { + constexpr int nslices = 64; + constexpr int nverts = nslices + 2; + + static float4 verts[nverts]; + int i = 0; + while (i < nslices + 1) { + float theta = 2.0f * M_PI * i / nslices; + verts[i++] = float4 { + std::cos(theta), std::sin(theta), + 0.5 + 0.5 * std::cos(theta), 0.5 + 0.5 * std::sin(theta) + }; + } + verts[i] = {0.0f, 0.0f, 0.5f, 0.5f}; + + static uint16_t indices[nslices * 3]; + for (int i = 0, j = 0; i < nslices; ++i) { + indices[j++] = nverts - 1; + indices[j++] = i; + indices[j++] = (i + 1) % (nslices + 1); + } + + VertexBuffer* vb = VertexBuffer::Builder() + .vertexCount(nverts) + .bufferCount(1) + .attribute(VertexAttribute::POSITION, 0, VertexBuffer::AttributeType::FLOAT2, 0, 16) + .attribute(VertexAttribute::UV0, 0, VertexBuffer::AttributeType::FLOAT2, 8, 16) + .build(*engine); + + vb->setBufferAt(*engine, 0, VertexBuffer::BufferDescriptor(verts, sizeof(verts), nullptr)); + + IndexBuffer* ib = IndexBuffer::Builder() + .indexCount(nslices * 3) + .bufferType(IndexBuffer::IndexType::USHORT) + .build(*engine); + + ib->setBuffer(*engine, IndexBuffer::BufferDescriptor(indices, sizeof(indices), nullptr)); + + filamat::Package pkg = filamat::MaterialBuilder() + .name("podium") + .material(R"SHADER( + void material(inout MaterialInputs material) { + vec3 n = texture(materialParams_normal, getUV0()).xyz * 2.0 - 1.0; + + // The blue tiles normal map is very harsh, so we soften the normal vector. + material.normal = normalize(n + vec3(0, 0, 5)); + + prepareMaterial(material); + material.baseColor = texture(materialParams_color, getUV0()); + material.roughness = texture(materialParams_roughness, getUV0()).r; + material.ambientOcclusion = texture(materialParams_ao, getUV0()).r; + + float2 uv = gl_FragCoord.xy * getResolution().zw; uv.y = 1.0 - uv.y; + vec3 reflectance = texture(materialParams_reflection, uv).xyz; + + // HACK: blend the reflection with the baseColor. + material.baseColor.rgb = mix(reflectance, material.baseColor.rgb, 0.75); + } + )SHADER") + .require(VertexAttribute::UV0) + .parameter(filamat::MaterialBuilder::SamplerType::SAMPLER_2D, "normal") + .parameter(filamat::MaterialBuilder::SamplerType::SAMPLER_2D, "color") + .parameter(filamat::MaterialBuilder::SamplerType::SAMPLER_2D, "roughness") + .parameter(filamat::MaterialBuilder::SamplerType::SAMPLER_2D, "ao") + .parameter(filamat::MaterialBuilder::SamplerType::SAMPLER_2D, "reflection") + .targetApi(filamat::MaterialBuilder::TargetApi::OPENGL) + .specularAntiAliasing(true) + .shading(Shading::LIT) + .build(); + + Material* mat = Material::Builder().package(pkg.getData(), pkg.getSize()).build(*engine); + MaterialInstance* matinstance = mat->createInstance(); + + auto createTexture4 = [engine](const uint8_t* data, int size, bool srgb) { + int width, height, nchan; + auto texels = stbi_load_from_memory(data, size, &width, &height, &nchan, 4); + Texture::PixelBufferDescriptor buffer(texels, size_t(width * height * 4), + Texture::Format::RGBA, Texture::Type::UBYTE, + (Texture::PixelBufferDescriptor::Callback) &stbi_image_free); + auto tex = Texture::Builder() + .width(uint32_t(width)).height(uint32_t(height)).levels(1) + .sampler(Texture::Sampler::SAMPLER_2D) + .format(srgb ? Texture::InternalFormat::SRGB8_A8 : Texture::InternalFormat::RGBA8) + .build(*engine); + tex->setImage(*engine, 0, std::move(buffer)); + tex->generateMipmaps(*engine); + return tex; + }; + + auto createTexture1 = [engine](const uint8_t* data, int size) { + int width, height, nchan; + auto texels = stbi_load_from_memory(data, size, &width, &height, &nchan, 1); + Texture::PixelBufferDescriptor buffer(texels, size_t(width * height), + Texture::Format::R, Texture::Type::UBYTE, + (Texture::PixelBufferDescriptor::Callback) &stbi_image_free); + auto tex = Texture::Builder() + .width(uint32_t(width)).height(uint32_t(height)).levels(1) + .sampler(Texture::Sampler::SAMPLER_2D) + .format(Texture::InternalFormat::R8) + .build(*engine); + tex->setImage(*engine, 0, std::move(buffer)); + tex->generateMipmaps(*engine); + return tex; + }; + + auto normal = createTexture4(RESOURCE_ARGS(BLUE_TILES_01_NORMAL), false); + auto color = createTexture4(RESOURCE_ARGS(BLUE_TILES_01_COLOR), true); + auto roughness = createTexture1(RESOURCE_ARGS(BLUE_TILES_01_ROUGHNESS)); + auto ao = createTexture1(RESOURCE_ARGS(BLUE_TILES_01_AO)); + + TextureSampler sampler(TextureSampler::MinFilter::LINEAR_MIPMAP_LINEAR, + TextureSampler::MagFilter::LINEAR); + + matinstance->setParameter("normal", normal, sampler); + matinstance->setParameter("color", color, sampler); + matinstance->setParameter("roughness", roughness, sampler); + matinstance->setParameter("ao", ao, sampler); + matinstance->setParameter("reflection", reflection, + TextureSampler(TextureSampler::MagFilter::LINEAR)); + + auto entity = EntityManager::get().create(); + RenderableManager::Builder(1) + .boundingBox({{ -1, -1, 1 }, { 1, 1, 1 }}) + .material(0, matinstance) + .geometry(0, RenderableManager::PrimitiveType::TRIANGLES, vb, ib) + .culling(false) + .receiveShadows(true) + .castShadows(false) + .build(*engine, entity); + + auto& tcm = engine->getTransformManager(); + tcm.create(entity); + + return entity; +} + +mat4f fitIntoUnitCube(const Aabb& bounds) { + auto minpt = bounds.min; + auto maxpt = bounds.max; + float maxExtent = 0; + 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; + return mat4f::scaling(float3(scaleFactor)) * mat4f::translation(-center); +} + +} // namespace LucyUtils diff --git a/samples/lucy_utils.h b/samples/lucy_utils.h new file mode 100644 index 0000000000..db661a1b27 --- /dev/null +++ b/samples/lucy_utils.h @@ -0,0 +1,38 @@ +/* + * 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. + */ + +#ifndef TNT_FILAMENT_SAMPLES_LUCY_UTILS_H +#define TNT_FILAMENT_SAMPLES_LUCY_UTILS_H + +#include +#include +#include + +#include + +#include "generated/resources/gltf.h" + +#define RESOURCE_ARGS(name) GLTF_##name##_DATA, GLTF_##name##_SIZE + +namespace LucyUtils { + enum ImageOp { BLIT, HBLUR, VBLUR, MIX }; + utils::Entity createQuad(filament::Engine* engine, filament::Texture* tex, ImageOp op, + filament::Texture* secondary = nullptr); + utils::Entity createDisk(filament::Engine* engine, filament::Texture* reflection); + filament::math::mat4f fitIntoUnitCube(const filament::Aabb& bounds); +} + +#endif // #define TNT_FILAMENT_SAMPLES_LUCY_UTILS_H diff --git a/third_party/models/lucy/lucy.glb b/third_party/models/lucy/lucy.glb new file mode 100644 index 0000000000000000000000000000000000000000..e7f43d1f25eca018793421a8a1a82f0dc602c0bc GIT binary patch literal 792828 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