text: /*
   * 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 <filament/Camera.h>
  #include <filament/Engine.h>
  #include <filament/IndexBuffer.h>
  #include <filament/IndirectLight.h>
  #include <filament/LightManager.h>
  #include <filament/MaterialInstance.h>
  #include <filament/RenderableManager.h>
  #include <filament/RenderTarget.h>
  #include <filament/Scene.h>
  #include <filament/Skybox.h>
  #include <filament/TransformManager.h>
  #include <filament/VertexBuffer.h>
  #include <filament/View.h>
  
  #include <utils/EntityManager.h>
  
  #include <gltfio/AssetLoader.h>
  #include <gltfio/FilamentAsset.h>
  #include <gltfio/ResourceLoader.h>
  
  #include <getopt/getopt.h>
  
  #include <imgui.h>
  
  #include <string>
  
  #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 = 15000;
      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()
          .texture(RenderTarget::COLOR, app.reflection.color)
          .texture(RenderTarget::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()
          .texture(RenderTarget::COLOR, app.primary.color)
          .texture(RenderTarget::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()
          .texture(RenderTarget::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()
          .texture(RenderTarget::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<ACCURATE>({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);
      app.reflection.view->setDithering(View::Dithering::NONE);
      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);
      app.primary.view->setDithering(View::Dithering::NONE);
      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);
  
      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 = "venetian_crossroads_2k";
  
      LucyApp app;
      app.config.title = "Lucy";
      app.config.iblDirectory = FilamentApp::getRootAssetsPath() + 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);
  
      return 0;
  }
  