#include /* * Copyright (C) 2015 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.h" #if !defined(WIN32) # include #else # include # include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include "Cube.h" #include "NativeWindowHelper.h" using namespace filament; using namespace filagui; using namespace math; using namespace utils; static constexpr uint8_t TRANSPARENT_COLOR_PACKAGE[] = { #include "generated/material/transparentColor.inc" }; static constexpr uint8_t DEPTH_VISUALIZER_PACKAGE[] = { #include "generated/material/depthVisualizer.inc" }; FilamentApp& FilamentApp::get() { static FilamentApp filamentApp; return filamentApp; } FilamentApp::FilamentApp() { initSDL(); } FilamentApp::~FilamentApp() { SDL_Quit(); } void FilamentApp::run(const Config& config,SetupCallback setupCallback, CleanupCallback cleanupCallback, ImGuiCallback imguiCallback, PreRenderCallback preRender, PostRenderCallback postRender, size_t width, size_t height) { mEngine = Engine::create(config.backend); mDepthMaterial = Material::Builder() .package((void*) DEPTH_VISUALIZER_PACKAGE, sizeof(DEPTH_VISUALIZER_PACKAGE)) .build(*mEngine); mDepthMI = mDepthMaterial->createInstance(); mTransparentMaterial = Material::Builder() .package((void*) TRANSPARENT_COLOR_PACKAGE, sizeof(TRANSPARENT_COLOR_PACKAGE)) .build(*mEngine); std::unique_ptr window( new FilamentApp::Window(this, config, config.title, width, height)); std::unique_ptr cameraCube(new Cube(*mEngine, mTransparentMaterial, {1,0,0})); // we can't cull the light-frustum because it's not applied a rigid transform // and currently, filament assumes that for culling std::unique_ptr lightmapCube(new Cube(*mEngine, mTransparentMaterial, {0,1,0}, false)); mScene = mEngine->createScene(); window->mMainView->getView()->setVisibleLayers(0x4, 0x4); window->mUiView->getView()->setClearTargets(false, false, false); window->mUiView->getView()->setRenderTarget(View::TargetBufferFlags::DEPTH_AND_STENCIL); window->mUiView->getView()->setPostProcessingEnabled(false); window->mUiView->getView()->setShadowsEnabled(false); if (config.splitView) { auto& rcm = mEngine->getRenderableManager(); rcm.setLayerMask(rcm.getInstance(cameraCube->getSolidRenderable()), 0x3, 0x2); rcm.setLayerMask(rcm.getInstance(cameraCube->getWireFrameRenderable()), 0x3, 0x2); cameraCube->mapFrustum(*mEngine, window->mMainCameraMan.getCamera()); rcm.setLayerMask(rcm.getInstance(lightmapCube->getSolidRenderable()), 0x3, 0x2); rcm.setLayerMask(rcm.getInstance(lightmapCube->getWireFrameRenderable()), 0x3, 0x2); // Create the camera mesh window->mMainCameraMan.setCameraChangedCallback( [&cameraCube, &lightmapCube, &window, engine = mEngine](Camera const* camera) { cameraCube->mapFrustum(*engine, camera); lightmapCube->mapFrustum(*engine, window->mMainView->getView()->getDirectionalLightCamera()); }); mScene->addEntity(cameraCube->getWireFrameRenderable()); mScene->addEntity(cameraCube->getSolidRenderable()); mScene->addEntity(lightmapCube->getWireFrameRenderable()); mScene->addEntity(lightmapCube->getSolidRenderable()); window->mGodView->getView()->setVisibleLayers(0x6, 0x6); window->mOrthoView->getView()->setVisibleLayers(0x6, 0x6); // only preserve the color buffer for additional views; depth and stencil can be discarded. window->mDepthView->getView()->setRenderTarget(View::TargetBufferFlags::DEPTH_AND_STENCIL); window->mGodView->getView()->setRenderTarget(View::TargetBufferFlags::DEPTH_AND_STENCIL); window->mOrthoView->getView()->setRenderTarget(View::TargetBufferFlags::DEPTH_AND_STENCIL); window->mDepthView->getView()->setShadowsEnabled(false); window->mGodView->getView()->setShadowsEnabled(false); window->mOrthoView->getView()->setShadowsEnabled(false); } loadIBL(config); if (mIBL != nullptr) { mIBL->getSkybox()->setLayerMask(0x7, 0x4); mScene->setSkybox(mIBL->getSkybox()); mScene->setIndirectLight(mIBL->getIndirectLight()); } for (auto& view : window->mViews) { if (view.get() != window->mUiView) { view->getView()->setScene(mScene); } } setupCallback(mEngine, window->mMainView->getView(), mScene); if (imguiCallback) { mImGuiHelper = std::make_unique(mEngine, window->mUiView->getView(), getRootPath() + "assets/fonts/Roboto-Medium.ttf"); ImGuiIO& io = ImGui::GetIO(); #ifdef WIN32 SDL_SysWMinfo wmInfo; SDL_VERSION(&wmInfo.version); SDL_GetWindowWMInfo(window->getSDLWindow(), &wmInfo); io.ImeWindowHandle = wmInfo.info.win.window; #endif io.KeyMap[ImGuiKey_Tab] = SDL_SCANCODE_TAB; io.KeyMap[ImGuiKey_LeftArrow] = SDL_SCANCODE_LEFT; io.KeyMap[ImGuiKey_RightArrow] = SDL_SCANCODE_RIGHT; io.KeyMap[ImGuiKey_UpArrow] = SDL_SCANCODE_UP; io.KeyMap[ImGuiKey_DownArrow] = SDL_SCANCODE_DOWN; io.KeyMap[ImGuiKey_PageUp] = SDL_SCANCODE_PAGEUP; io.KeyMap[ImGuiKey_PageDown] = SDL_SCANCODE_PAGEDOWN; io.KeyMap[ImGuiKey_Home] = SDL_SCANCODE_HOME; io.KeyMap[ImGuiKey_End] = SDL_SCANCODE_END; io.KeyMap[ImGuiKey_Insert] = SDL_SCANCODE_INSERT; io.KeyMap[ImGuiKey_Delete] = SDL_SCANCODE_DELETE; io.KeyMap[ImGuiKey_Backspace] = SDL_SCANCODE_BACKSPACE; io.KeyMap[ImGuiKey_Space] = SDL_SCANCODE_SPACE; io.KeyMap[ImGuiKey_Enter] = SDL_SCANCODE_RETURN; io.KeyMap[ImGuiKey_Escape] = SDL_SCANCODE_ESCAPE; io.KeyMap[ImGuiKey_A] = SDL_SCANCODE_A; io.KeyMap[ImGuiKey_C] = SDL_SCANCODE_C; io.KeyMap[ImGuiKey_V] = SDL_SCANCODE_V; io.KeyMap[ImGuiKey_X] = SDL_SCANCODE_X; io.KeyMap[ImGuiKey_Y] = SDL_SCANCODE_Y; io.KeyMap[ImGuiKey_Z] = SDL_SCANCODE_Z; io.SetClipboardTextFn = [](void*, const char* text) { SDL_SetClipboardText(text); }; io.GetClipboardTextFn = [](void*) -> const char* { return SDL_GetClipboardText(); }; io.ClipboardUserData = nullptr; } bool mousePressed[3] = { false }; while (!mClosed) { // Allow the app to animate the scene if desired. if (mAnimation) { double now = (double) SDL_GetPerformanceCounter() / SDL_GetPerformanceFrequency(); mAnimation(mEngine, window->mMainView->getView(), now); } // Loop over fresh events twice: first stash them and let ImGui process them, then allow // the app to process the stashed events. This is done because ImGui might wish to block // certain events from the app (e.g., when dragging the mouse over an obscuring window). constexpr int kMaxEvents = 16; SDL_Event events[kMaxEvents]; int nevents = 0; while (nevents < kMaxEvents && SDL_PollEvent(&events[nevents]) != 0) { if (mImGuiHelper) { ImGuiIO& io = ImGui::GetIO(); SDL_Event* event = &events[nevents]; switch (event->type) { case SDL_MOUSEWHEEL: { if (event->wheel.x > 0) io.MouseWheelH += 1; if (event->wheel.x < 0) io.MouseWheelH -= 1; if (event->wheel.y > 0) io.MouseWheel += 1; if (event->wheel.y < 0) io.MouseWheel -= 1; break; } case SDL_MOUSEBUTTONDOWN: { if (event->button.button == SDL_BUTTON_LEFT) mousePressed[0] = true; if (event->button.button == SDL_BUTTON_RIGHT) mousePressed[1] = true; if (event->button.button == SDL_BUTTON_MIDDLE) mousePressed[2] = true; break; } case SDL_TEXTINPUT: { io.AddInputCharactersUTF8(event->text.text); break; } case SDL_KEYDOWN: case SDL_KEYUP: { int key = event->key.keysym.scancode; IM_ASSERT(key >= 0 && key < IM_ARRAYSIZE(io.KeysDown)); io.KeysDown[key] = (event->type == SDL_KEYDOWN); io.KeyShift = ((SDL_GetModState() & KMOD_SHIFT) != 0); io.KeyAlt = ((SDL_GetModState() & KMOD_ALT) != 0); io.KeyCtrl = ((SDL_GetModState() & KMOD_CTRL) != 0); io.KeySuper = ((SDL_GetModState() & KMOD_GUI) != 0); break; } } } nevents++; } // Now, loop over the events a second time for app-side processing. for (int i = 0; i < nevents; i++) { const SDL_Event& event = events[i]; ImGuiIO* io = mImGuiHelper ? &ImGui::GetIO() : nullptr; switch (event.type) { case SDL_QUIT: mClosed = true; break; case SDL_KEYDOWN: if (event.key.keysym.scancode == SDL_SCANCODE_ESCAPE) { mClosed = true; } break; case SDL_MOUSEWHEEL: if (!io || !io->WantCaptureMouse) window->mouseWheel(event.wheel.y); break; case SDL_MOUSEBUTTONDOWN: if (!io || !io->WantCaptureMouse) window->mouseDown(event.button.button, event.button.x, event.button.y); break; case SDL_MOUSEBUTTONUP: if (!io || !io->WantCaptureMouse) window->mouseUp(event.button.x, event.button.y); break; case SDL_MOUSEMOTION: if (!io || !io->WantCaptureMouse) window->mouseMoved(event.motion.x, event.motion.y); break; case SDL_WINDOWEVENT: switch (event.window.event) { case SDL_WINDOWEVENT_RESIZED: window->resize(); break; default: break; } break; default: break; } } // Populate the UI scene, regardless of whether Filament wants to a skip frame. We should // always let ImGui generate a command list; if it skips a frame it'll destroy its widgets. if (mImGuiHelper) { // Inform ImGui of the current window size in case it was resized. int windowWidth, windowHeight; int displayWidth, displayHeight; SDL_GetWindowSize(window->mWindow, &windowWidth, &windowHeight); SDL_GL_GetDrawableSize(window->mWindow, &displayWidth, &displayHeight); mImGuiHelper->setDisplaySize(windowWidth, windowHeight, windowWidth > 0 ? ((float)displayWidth / windowWidth) : 0, displayHeight > 0 ? ((float)displayHeight / windowHeight) : 0); // Setup mouse inputs (we already got mouse wheel, keyboard keys & characters // from our event handler) ImGuiIO& io = ImGui::GetIO(); int mx, my; Uint32 buttons = SDL_GetMouseState(&mx, &my); io.MousePos = ImVec2(-FLT_MAX, -FLT_MAX); io.MouseDown[0] = mousePressed[0] || (buttons & SDL_BUTTON(SDL_BUTTON_LEFT)) != 0; io.MouseDown[1] = mousePressed[1] || (buttons & SDL_BUTTON(SDL_BUTTON_RIGHT)) != 0; io.MouseDown[2] = mousePressed[2] || (buttons & SDL_BUTTON(SDL_BUTTON_MIDDLE)) != 0; mousePressed[0] = mousePressed[1] = mousePressed[2] = false; // TODO: Update to a newer SDL and use SDL_CaptureMouse() to retrieve mouse coordinates // outside of the client area; see the imgui SDL example. if ((SDL_GetWindowFlags(window->mWindow) & SDL_WINDOW_INPUT_FOCUS) != 0) { io.MousePos = ImVec2((float)mx, (float)my); } // Populate the UI Scene. static Uint64 frequency = SDL_GetPerformanceFrequency(); Uint64 now = SDL_GetPerformanceCounter(); float timeStep = mTime > 0 ? (float)((double)(now - mTime) / frequency) : (float)(1.0f / 60.0f); mTime = now; mImGuiHelper->render(timeStep, imguiCallback); } window->mMainCameraMan.updateCameraTransform(); // TODO: we need better timing or use SDL_GL_SetSwapInterval SDL_Delay(16); Renderer* renderer = window->getRenderer(); if (preRender) { for (auto const& view : window->mViews) { if (view.get() != window->mUiView) { preRender(mEngine, view->getView(), mScene, renderer); } } } if (renderer->beginFrame(window->getSwapChain())) { for (auto const& view : window->mViews) { renderer->render(view->getView()); } renderer->endFrame(); } if (postRender) { for (auto const& view : window->mViews) { if (view.get() != window->mUiView) { postRender(mEngine, view->getView(), mScene, renderer); } } } } if (mImGuiHelper) { mImGuiHelper.reset(); } cleanupCallback(mEngine, window->mMainView->getView(), mScene); cameraCube.reset(); lightmapCube.reset(); window.reset(); mIBL.reset(); mEngine->destroy(mDepthMI); mEngine->destroy(mDepthMaterial); mEngine->destroy(mTransparentMaterial); mEngine->destroy(mScene); Engine::destroy(&mEngine); mEngine = nullptr; } void FilamentApp::loadIBL(const Config& config) { if (!config.iblDirectory.empty()) { Path iblPath(config.iblDirectory); if (!iblPath.exists()) { std::cerr << "The specified IBL path does not exist: " << iblPath << std::endl; return; } if (!iblPath.isDirectory()) { std::cerr << "The specified IBL path is not a directory: " << iblPath << std::endl; return; } mIBL = std::make_unique(*mEngine); if (!mIBL->loadFromDirectory(iblPath)) { std::cerr << "Could not load the specified IBL: " << iblPath << std::endl; mIBL.reset(nullptr); return; } } } void FilamentApp::initSDL() { ASSERT_POSTCONDITION(SDL_Init(SDL_INIT_EVENTS) == 0, "SDL_Init Failure"); } // ------------------------------------------------------------------------------------------------ FilamentApp::Window::Window(FilamentApp* filamentApp, const Config& config, std::string title, size_t w, size_t h) : 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 | SDL_WINDOW_OPENGL; mWindow = SDL_CreateWindow(title.c_str(), x, y, (int) w, (int) h, windowFlags); // HACK: We don't use SDL's 2D rendering functionality, but by invoking it we cause // SDL to create a Metal backing layer, which allows us to run Vulkan apps via MoltenVK. #if defined(FILAMENT_DRIVER_SUPPORTS_VULKAN) && defined(__APPLE__) constexpr int METAL_DRIVER = 2; SDL_CreateRenderer(mWindow, METAL_DRIVER, SDL_RENDERER_ACCELERATED); #endif void* nativeWindow = ::getNativeWindow(mWindow); mSwapChain = mFilamentApp->mEngine->createSwapChain(nativeWindow); mRenderer = mFilamentApp->mEngine->createRenderer(); // create cameras mCameras[0] = mMainCamera = mFilamentApp->mEngine->createCamera(); mCameras[1] = mDebugCamera = mFilamentApp->mEngine->createCamera(); mCameras[2] = mOrthoCamera = mFilamentApp->mEngine->createCamera(); mCameras[3] = mUiCamera = mFilamentApp->mEngine->createCamera(); // set exposure for (auto camera : mCameras) { camera->setExposure(16.0f, 1 / 125.0f, 100.0f); } // create views mViews.emplace_back(mMainView = new CView(*mRenderer, "Main View")); if (config.splitView) { mViews.emplace_back(mDepthView = new CView(*mRenderer, "Depth View")); mViews.emplace_back(mGodView = new GodView(*mRenderer, "God View")); mViews.emplace_back(mOrthoView = new CView(*mRenderer, "Ortho View")); mDepthView->getView()->setDepthPrepass(View::DepthPrepass::DISABLED); } mViews.emplace_back(mUiView = new CView(*mRenderer, "UI View")); // set-up the camera manipulators double3 at(0, 0, -4); mMainCameraMan.setCamera(mMainCamera); mDebugCameraMan.setCamera(mDebugCamera); mMainView->setCamera(mMainCamera); mMainView->setCameraManipulator(&mMainCameraMan); mUiView->setCamera(mUiCamera); if (config.splitView) { // Depth view always uses the main camera mDepthView->setCamera(mMainCamera); // The god view uses the main camera for culling, but the debug camera for viewing mGodView->setCamera(mMainCamera); mGodView->setGodCamera(mDebugCamera); // Ortho view obviously uses an ortho camera mOrthoView->setCamera( (Camera *)mMainView->getView()->getDirectionalLightCamera() ); mDepthView->setCameraManipulator(&mMainCameraMan); mGodView->setCameraManipulator(&mDebugCameraMan); mOrthoView->setCameraManipulator(&mOrthoCameraMan); } // configure the cameras configureCamerasForWindow(); mMainCameraMan.lookAt(at + double3{ 0, 0, 4 }, at); mDebugCameraMan.lookAt(at + double3{ 0, 0, 4 }, at); mOrthoCameraMan.lookAt(at + double3{ 0, 0, 4 }, at); } FilamentApp::Window::~Window() { mViews.clear(); for (auto& camera : mCameras) { mFilamentApp->mEngine->destroy(camera); } mFilamentApp->mEngine->destroy(mRenderer); mFilamentApp->mEngine->destroy(mSwapChain); SDL_DestroyWindow(mWindow); } void FilamentApp::Window::mouseDown(int button, ssize_t x, ssize_t y) { fixupMouseCoordinatesForHdpi(x, y); y = mHeight - y; for (auto const& view : mViews) { if (view->intersects(x, y)) { mEventTarget = view.get(); view->mouseDown(button, x, y); break; } } } void FilamentApp::Window::mouseWheel(ssize_t x) { if (mEventTarget) { mEventTarget->mouseWheel(x); } else { for (auto const& view : mViews) { if (view->intersects(mLastX, mLastY)) { view->mouseWheel(x); break; } } } } void FilamentApp::Window::mouseUp(ssize_t x, ssize_t y) { fixupMouseCoordinatesForHdpi(x, y); if (mEventTarget) { y = mHeight - y; mEventTarget->mouseUp(x, y); mEventTarget = nullptr; } } void FilamentApp::Window::mouseMoved(ssize_t x, ssize_t y) { fixupMouseCoordinatesForHdpi(x, y); y = mHeight - y; if (mEventTarget) { mEventTarget->mouseMoved(x, y); } mLastX = x; mLastY = y; } void FilamentApp::Window::fixupMouseCoordinatesForHdpi(ssize_t& x, ssize_t& y) const { int dw, dh, ww, wh; SDL_GL_GetDrawableSize(mWindow, &dw, &dh); SDL_GetWindowSize(mWindow, &ww, &wh); x = x * dw / ww; y = y * dh / wh; } void FilamentApp::Window::resize() { mFilamentApp->mEngine->destroy(mSwapChain); mSwapChain = mFilamentApp->mEngine->createSwapChain(::getNativeWindow(mWindow)); configureCamerasForWindow(); } void FilamentApp::Window::configureCamerasForWindow() { // Determine the current size of the window in physical pixels. uint32_t w, h; SDL_GL_GetDrawableSize(mWindow, (int*) &w, (int*) &h); mWidth = (size_t) w; mHeight = (size_t) h; // Compute the "virtual pixels to physical pixels" scale factor that the // the platform uses for UI elements. int virtualWidth, virtualHeight; SDL_GetWindowSize(mWindow, &virtualWidth, &virtualHeight); float dpiScaleX = (float) w / virtualWidth; float dpiScaleY = (float) h / virtualHeight; const float3 at(0, 0, -4); const double ratio = double(h) / double(w); double near = 0.1; double far = 50; mMainCamera->setProjection(45.0, double(w) / h, near, far, Camera::Fov::VERTICAL); mDebugCamera->setProjection(45.0, double(w) / h, 0.0625, 4096, Camera::Fov::VERTICAL); mOrthoCamera->setProjection(Camera::Projection::ORTHO, -3, 3, -3 * ratio, 3 * ratio, near, far); mOrthoCamera->lookAt(at + float3{ 4, 0, 0 }, at); mUiCamera->setProjection(Camera::Projection::ORTHO, 0.0, w / dpiScaleX, h / dpiScaleY, 0.0, 0.0, 1.0); // We're in split view when there are more views than just the Main and UI views. if (mViews.size() > 2) { uint32_t vpw = w / 2; uint32_t vph = h / 2; mMainView->setViewport ({ 0, 0, vpw, vph }); mDepthView->setViewport({ int32_t(vpw), 0, w - vpw, vph }); mGodView->setViewport ({ int32_t(vpw), int32_t(vph), w - vpw, h - vph }); mOrthoView->setViewport({ 0, int32_t(vph), vpw, h - vph }); mMainView->getCameraManipulator()->updateCameraTransform(); mDepthView->getCameraManipulator()->updateCameraTransform(); mGodView->getCameraManipulator()->updateCameraTransform(); mOrthoView->getCameraManipulator()->updateCameraTransform(); } else { mMainView->setViewport({ 0, 0, w, h }); } mUiView->setViewport({ 0, 0, w, h }); } // ------------------------------------------------------------------------------------------------ FilamentApp::CView::CView(Renderer& renderer, std::string name) : engine(*renderer.getEngine()), mName(name) { view = engine.createView(); view->setClearColor({ 0 }); view->setName(name.c_str()); } FilamentApp::CView::~CView() { engine.destroy(view); } void FilamentApp::CView::setViewport(Viewport const& viewport) { mViewport = viewport; view->setViewport(viewport); if (mCameraManipulator) { mCameraManipulator->setViewport(viewport.width, viewport.height); } } void FilamentApp::CView::mouseDown(int button, ssize_t x, ssize_t y) { mLastMousePosition = double2(x, y); if (button == 1) { mMode = Mode::ROTATE; } else if (button == 3) { mMode = Mode::TRACK; } } void FilamentApp::CView::mouseUp(ssize_t x, ssize_t y) { mMode = Mode::NONE; } void FilamentApp::CView::mouseMoved(ssize_t x, ssize_t y) { if (mCameraManipulator) { double2 delta = double2(x, y) - mLastMousePosition; mLastMousePosition = double2(x, y); switch (mMode) { case Mode::NONE: break; case Mode::ROTATE: mCameraManipulator->rotate(delta); break; case Mode::TRACK: mCameraManipulator->track(delta); break; } } } void FilamentApp::CView::mouseWheel(ssize_t x) { if (mCameraManipulator){ mCameraManipulator->dolly(x); } } bool FilamentApp::CView::intersects(ssize_t x, ssize_t y) { if (x >= mViewport.left && x < mViewport.left + mViewport.width) if (y >= mViewport.bottom && y < mViewport.bottom + mViewport.height) return true; return false; } void FilamentApp::CView::setCameraManipulator(CameraManipulator* cm) { mCameraManipulator = cm; } void FilamentApp::CView::setCamera(Camera* camera) { view->setCamera(camera); } void FilamentApp::GodView::setGodCamera(Camera* camera) { getView()->setDebugCamera(camera); }