//
// Copyright Contributors to the MaterialX Project
// SPDX-License-Identifier: Apache-2.0
//

#include <MaterialXView/Viewer.h>
#include <MaterialXView/RenderPipeline.h>

#ifdef MATERIALXVIEW_METAL_BACKEND
#include <MaterialXView/RenderPipelineMetal.h>
#include <MaterialXGenMsl/MslShaderGenerator.h>
#include <nanogui/metal.h>
#else
#include <MaterialXRenderGlsl/GLUtil.h>
#include <MaterialXView/RenderPipelineGL.h>
#include <MaterialXGenGlsl/GlslShaderGenerator.h>
#endif

#include <MaterialXRender/ShaderRenderer.h>
#include <MaterialXRender/CgltfLoader.h>
#include <MaterialXRender/Harmonics.h>
#include <MaterialXRender/OiioImageLoader.h>
#include <MaterialXRender/StbImageLoader.h>
#include <MaterialXRender/TinyObjLoader.h>

#include <MaterialXGenShader/DefaultColorManagementSystem.h>
#include <MaterialXGenShader/ShaderTranslator.h>
#ifdef MATERIALX_BUILD_OCIO
#include <MaterialXGenShader/OcioColorManagementSystem.h>
#endif

#if MATERIALX_BUILD_GEN_MDL
#include <MaterialXGenMdl/MdlShaderGenerator.h>
#endif
#if MATERIALX_BUILD_GEN_OSL
#include <MaterialXGenOsl/OslShaderGenerator.h>
#endif
#include <MaterialXGenGlsl/EsslShaderGenerator.h>

#include <MaterialXFormat/Environ.h>
#include <MaterialXFormat/Util.h>

#include <nanogui/icons.h>
#include <nanogui/messagedialog.h>
#include <nanogui/opengl.h>
#include <nanogui/vscrollpanel.h>

#include <fstream>
#include <iostream>
#include <iomanip>
#include <limits>

const mx::Vector3 DEFAULT_CAMERA_POSITION(0.0f, 0.0f, 5.0f);
const float DEFAULT_CAMERA_VIEW_ANGLE = 45.0f;
const float DEFAULT_CAMERA_ZOOM = 1.0f;

namespace
{
#ifdef MATERIALXVIEW_METAL_BACKEND
const bool USE_FLOAT_BUFFER = true;
#else
const bool USE_FLOAT_BUFFER = false;
#endif

const int MIN_ENV_SAMPLE_COUNT = 4;
const int MAX_ENV_SAMPLE_COUNT = 1024;

const int MIN_AIRY_FRESNEL_ITERATIONS = 1;
const int MAX_AIRY_FRESNEL_ITERATIONS = 8;

const int SHADOW_MAP_SIZE = 2048;
const int ALBEDO_TABLE_SIZE = 128;
const int IRRADIANCE_MAP_WIDTH = 256;
const int IRRADIANCE_MAP_HEIGHT = 128;

const std::string DIR_LIGHT_NODE_CATEGORY = "directional_light";
const std::string IRRADIANCE_MAP_FOLDER = "irradiance";
const float ORTHO_VIEW_DISTANCE = 1000.0f;
const float ORTHO_PROJECTION_HEIGHT = 1.8f;

const float ENV_MAP_SPLIT_RADIANCE = 16.0f;
const float MAX_ENV_TEXEL_RADIANCE = 100000.0f;
const float IDEAL_ENV_MAP_RADIANCE = 6.0f;

const float IDEAL_MESH_SPHERE_RADIUS = 2.0f;

const float PI = std::acos(-1.0f);

const std::string UDIM_SEPARATORS = "._";

void writeTextFile(const std::string& text, const std::string& filePath)
{
    std::ofstream file;
    file.open(filePath);
    file << text;
    file.close();
}

void applyModifiers(mx::DocumentPtr doc, const DocumentModifiers& modifiers)
{
    for (mx::ElementPtr elem : doc->traverseTree())
    {
        if (modifiers.remapElements.count(elem->getCategory()))
        {
            elem->setCategory(modifiers.remapElements.at(elem->getCategory()));
        }
        if (modifiers.remapElements.count(elem->getName()))
        {
            elem->setName(modifiers.remapElements.at(elem->getName()));
        }
        mx::StringVec attrNames = elem->getAttributeNames();
        for (const std::string& attrName : attrNames)
        {
            if (modifiers.remapElements.count(elem->getAttribute(attrName)))
            {
                elem->setAttribute(attrName, modifiers.remapElements.at(elem->getAttribute(attrName)));
            }
        }
        if (elem->hasFilePrefix() && !modifiers.filePrefixTerminator.empty())
        {
            std::string filePrefix = elem->getFilePrefix();
            if (!mx::stringEndsWith(filePrefix, modifiers.filePrefixTerminator))
            {
                elem->setFilePrefix(filePrefix + modifiers.filePrefixTerminator);
            }
        }
        mx::ElementVec children = elem->getChildren();
        for (mx::ElementPtr child : children)
        {
            if (modifiers.skipElements.count(child->getCategory()) ||
                modifiers.skipElements.count(child->getName()))
            {
                elem->removeChild(child->getName());
            }
        }
    }

    // Remap unsupported texture coordinate indices.
    for (mx::ElementPtr elem : doc->traverseTree())
    {
        mx::NodePtr node = elem->asA<mx::Node>();
        if (node && node->getCategory() == "texcoord")
        {
            mx::InputPtr index = node->getInput("index");
            mx::ValuePtr value = index ? index->getValue() : nullptr;
            if (value && value->isA<int>() && value->asA<int>() != 0)
            {
                index->setValue(0);
            }
        }
    }
}

} // anonymous namespace

//
// Viewer methods
//

Viewer::Viewer(const std::string& materialFilename,
               const std::string& meshFilename,
               const std::string& envRadianceFilename,
               const mx::FileSearchPath& searchPath,
               const mx::FilePathVec& libraryFolders,
               int screenWidth,
               int screenHeight,
               const mx::Color3& screenColor) :
    ng::Screen(ng::Vector2i(screenWidth, screenHeight), "MaterialXView",
        true, false, true, true, USE_FLOAT_BUFFER, 4, 0),
    _materialFilename(materialFilename),
    _meshFilename(meshFilename),
    _envRadianceFilename(envRadianceFilename),
    _searchPath(searchPath),
    _libraryFolders(libraryFolders),
    _meshScale(1.0f),
    _turntableEnabled(false),
    _turntableSteps(360),
    _turntableStep(0),
    _cameraPosition(DEFAULT_CAMERA_POSITION),
    _cameraUp(0.0f, 1.0f, 0.0f),
    _cameraViewAngle(DEFAULT_CAMERA_VIEW_ANGLE),
    _cameraNearDist(0.05f),
    _cameraFarDist(5000.0f),
    _cameraZoom(DEFAULT_CAMERA_ZOOM),
    _userCameraEnabled(true),
    _userTranslationActive(false),
    _lightRotation(0.0f),
    _normalizeEnvironment(false),
    _splitDirectLight(false),
    _generateReferenceIrradiance(false),
    _saveGeneratedLights(false),
    _shadowSoftness(1),
    _ambientOcclusionGain(0.6f),
    _selectedGeom(0),
    _selectedMaterial(0),
    _identityCamera(mx::Camera::create()),
    _viewCamera(mx::Camera::create()),
    _envCamera(mx::Camera::create()),
    _shadowCamera(mx::Camera::create()),
    _lightHandler(mx::LightHandler::create()),
    _typeSystem(mx::TypeSystem::create()),
#ifndef MATERIALXVIEW_METAL_BACKEND
    _genContext(mx::GlslShaderGenerator::create(_typeSystem)),
    _genContextEssl(mx::EsslShaderGenerator::create(_typeSystem)),
#else
    _genContext(mx::MslShaderGenerator::create(_typeSystem)),
#endif
#if MATERIALX_BUILD_GEN_OSL
    _genContextOsl(mx::OslShaderGenerator::create(_typeSystem)),
#endif
#if MATERIALX_BUILD_GEN_MDL
    _genContextMdl(mx::MdlShaderGenerator::create(_typeSystem)),
#endif
    _unitRegistry(mx::UnitConverterRegistry::create()),
    _drawEnvironment(false),
    _outlineSelection(false),
    _renderTransparency(true),
    _renderDoubleSided(true),
    _colorTexture(nullptr),
    _splitByUdims(true),
    _mergeMaterials(false),
    _showAllInputs(false),
    _flattenSubgraphs(false),
    _targetShader("standard_surface"),
    _captureRequested(false),
    _exitRequested(false),
    _wedgeRequested(false),
    _wedgePropertyName("base"),
    _wedgePropertyMin(0.0f),
    _wedgePropertyMax(1.0f),
    _wedgeImageCount(8),
    _bakeHdr(false),
    _bakeAverage(false),
    _bakeOptimize(true),
    _bakeRequested(false),
    _bakeWidth(0),
    _bakeHeight(0),
    _bakeDocumentPerMaterial(false),
    _frameTiming(false),
    _avgFrameTime(0.0)
{
    // Resolve input filenames, taking both the provided search path and
    // current working directory into account.
    mx::FileSearchPath localSearchPath = searchPath;
    localSearchPath.append(mx::FilePath::getCurrentPath());
    _materialFilename = localSearchPath.find(_materialFilename);
    _meshFilename = localSearchPath.find(_meshFilename);
    _envRadianceFilename = localSearchPath.find(_envRadianceFilename);

    // Set the requested background color.
    set_background(ng::Color(screenColor[0], screenColor[1], screenColor[2], 1.0f));

    // Set default Glsl generator options.
    _genContext.getOptions().targetColorSpaceOverride = "lin_rec709";
    _genContext.getOptions().fileTextureVerticalFlip = true;
    _genContext.getOptions().hwShadowMap = true;
    _genContext.getOptions().hwImplicitBitangents = false;

#ifdef MATERIALXVIEW_METAL_BACKEND
    _renderPipeline = MetalRenderPipeline::create(this);
    _renderPipeline->initialize(ng::metal_device(),
                                ng::metal_command_queue());
#else
    _renderPipeline = GLRenderPipeline::create(this);
    
    // Set Essl generator options
    _genContextEssl.getOptions().targetColorSpaceOverride = "lin_rec709";
    _genContextEssl.getOptions().fileTextureVerticalFlip = false;
    _genContextEssl.getOptions().hwMaxActiveLightSources = 1;
#endif
#if MATERIALX_BUILD_GEN_OSL
    // Set OSL generator options.
    _genContextOsl.getOptions().targetColorSpaceOverride = "lin_rec709";
    _genContextOsl.getOptions().fileTextureVerticalFlip = false;
#endif
#if MATERIALX_BUILD_GEN_MDL
    // Set MDL generator options.
    _genContextMdl.getOptions().targetColorSpaceOverride = "lin_rec709";
    _genContextMdl.getOptions().fileTextureVerticalFlip = false;
#endif
}

void Viewer::initialize()
{
    _window = new ng::Window(this, "Viewer Options");
    _window->set_position(ng::Vector2i(15, 15));
    _window->set_layout(new ng::GroupLayout());

    // Initialize the standard libraries and color/unit management.
    loadStandardLibraries();

    // Initialize image handler.
    _imageHandler = _renderPipeline->createImageHandler();
#if MATERIALX_BUILD_OIIO
    _imageHandler->addLoader(mx::OiioImageLoader::create());
#endif
    _imageHandler->setSearchPath(_searchPath);

    // Initialize user interfaces.
    createLoadMeshInterface((ng::ref<ng::Widget>) _window, "Load Mesh");
    createLoadMaterialsInterface((ng::ref<ng::Widget>) _window, "Load Material");
    createLoadEnvironmentInterface((ng::ref<ng::Widget>) _window, "Load Environment");
    createPropertyEditorInterface((ng::ref<ng::Widget>) _window, "Property Editor");
    createAdvancedSettings((ng::ref<ng::Widget>) _window);

    // Create geometry selection box.
    _geomLabel = new ng::Label(_window, "Select Geometry");
    _geometrySelectionBox = new ng::ComboBox(_window, { "None" });
    _geometrySelectionBox->set_chevron_icon(-1);
    _geometrySelectionBox->set_callback([this](int choice)
    {
        size_t index = (size_t) choice;
        if (index < _geometryList.size())
        {
            _selectedGeom = index;
            if (_materialAssignments.count(getSelectedGeometry()))
            {
                setSelectedMaterial(_materialAssignments[getSelectedGeometry()]);
            }
            updateDisplayedProperties();
            updateMaterialSelectionUI();
        }
    });

    // Create material selection box.
    _materialLabel = new ng::Label(_window, "Assigned Material");
    _materialSelectionBox = new ng::ComboBox(_window, { "None" });
    _materialSelectionBox->set_chevron_icon(-1);
    _materialSelectionBox->set_callback([this](int choice)
    {
        size_t index = (size_t) choice;
        if (index < _materials.size())
        {
            // Update selected material index
            _selectedMaterial = index;

            // Assign selected material to geometry
            assignMaterial(getSelectedGeometry(), _materials[index]);
        }
    });

    // Create frame timing display
    if (_frameTiming)
    {
        _timingLabel = new ng::Label(_window, "Timing");
        _timingPanel = new ng::Widget(_window);
        _timingPanel->set_layout(new ng::BoxLayout(ng::Orientation::Horizontal,
                                 ng::Alignment::Middle, 0, 6));
        new ng::Label(_timingPanel, "Frame time:");
        _timingText = new ng::TextBox(_timingPanel);
        _timingText->set_value("0");
        _timingText->set_units(" ms");
        _timingText->set_fixed_size(ng::Vector2i(80, 25));
        _timingText->set_alignment(ng::TextBox::Alignment::Right);
    }

    // Create geometry handler.
    mx::TinyObjLoaderPtr objLoader = mx::TinyObjLoader::create();
    mx::CgltfLoaderPtr gltfLoader = mx::CgltfLoader::create();
    _geometryHandler = mx::GeometryHandler::create();
    _geometryHandler->addLoader(objLoader);
    _geometryHandler->addLoader(gltfLoader);
    loadMesh(_searchPath.find(_meshFilename));

    _renderPipeline->initFramebuffer(width(), height(), nullptr);
    
    // Create environment geometry handler.
    _envGeometryHandler = mx::GeometryHandler::create();
    _envGeometryHandler->addLoader(objLoader);
    mx::FilePath envSphere("/usr/share/materialx/resources/Geometry/sphere.obj");
    _envGeometryHandler->loadGeometry(_searchPath.find(envSphere));

    // Initialize environment light.
    loadEnvironmentLight();

    // Initialize camera.
    initCamera();
    set_resize_callback([this](ng::Vector2i size)
    {
#ifdef MATERIALXVIEW_METAL_BACKEND
        _colorTexture = metal_texture();
#endif
        _renderPipeline->resizeFramebuffer(size.x(), size.y(), _colorTexture);
        _viewCamera->setViewportSize(mx::Vector2(static_cast<float>(size[0]), static_cast<float>(size[1])));
    });

    // Update geometry selections.
    updateGeometrySelections();

    // Load the requested material document.
    loadDocument(_materialFilename, _stdLib);

    // Finalize the UI.
    _propertyEditor.setVisible(false);
    perform_layout();

    _turntableTimer.startTimer();
}

void Viewer::loadEnvironmentLight()
{
    // Load the requested radiance map.
    mx::ImagePtr envRadianceMap = _imageHandler->acquireImage(_envRadianceFilename);
    if (!envRadianceMap)
    {
        new ng::MessageDialog(this, ng::MessageDialog::Type::Warning, "Failed to load environment light");
        return;
    }

    // If requested, normalize the environment upon loading.
    if (_normalizeEnvironment)
    {
        envRadianceMap = mx::normalizeEnvironment(envRadianceMap, IDEAL_ENV_MAP_RADIANCE, MAX_ENV_TEXEL_RADIANCE);
        if (_saveGeneratedLights)
        {
            _imageHandler->saveImage("NormalizedRadiance.hdr", envRadianceMap);
        }
    }

    // If requested, split the environment into indirect and direct components.
    if (_splitDirectLight)
    {
        splitDirectLight(envRadianceMap, envRadianceMap, _lightRigDoc);
        if (_saveGeneratedLights)
        {
            _imageHandler->saveImage("IndirectRadiance.hdr", envRadianceMap);
            mx::writeToXmlFile(_lightRigDoc, "DirectLightRig.mtlx");
        }
    }

    // Look for an irradiance map using an expected filename convention.
    mx::ImagePtr envIrradianceMap;
    if (!_normalizeEnvironment && !_splitDirectLight)
    {
        mx::FilePath envIrradiancePath = _envRadianceFilename.getParentPath() / IRRADIANCE_MAP_FOLDER / _envRadianceFilename.getBaseName();
        envIrradianceMap = _imageHandler->acquireImage(envIrradiancePath);
    }

    // If not found, then generate an irradiance map via spherical harmonics.
    if (!envIrradianceMap || envIrradianceMap->getWidth() == 1)
    {
        if (_generateReferenceIrradiance)
        {
            envIrradianceMap = mx::renderReferenceIrradiance(envRadianceMap, IRRADIANCE_MAP_WIDTH, IRRADIANCE_MAP_HEIGHT);
            if (_saveGeneratedLights)
            {
                _imageHandler->saveImage("ReferenceIrradiance.hdr", envIrradianceMap);
            }
        }
        else
        {
            mx::Sh3ColorCoeffs shIrradiance = mx::projectEnvironment(envRadianceMap, true);
            envIrradianceMap = mx::renderEnvironment(shIrradiance, IRRADIANCE_MAP_WIDTH, IRRADIANCE_MAP_HEIGHT);
            if (_saveGeneratedLights)
            {
                _imageHandler->saveImage("SphericalHarmonicIrradiance.hdr", envIrradianceMap);
            }
        }
    }

    // Release any existing environment maps and store the new ones.
    _imageHandler->releaseRenderResources(_lightHandler->getEnvRadianceMap());
    _imageHandler->releaseRenderResources(_lightHandler->getEnvPrefilteredMap());
    _imageHandler->releaseRenderResources(_lightHandler->getEnvIrradianceMap());

    _lightHandler->setEnvRadianceMap(envRadianceMap);
    _lightHandler->setEnvIrradianceMap(envIrradianceMap);
    _lightHandler->setEnvPrefilteredMap(nullptr);

    // Look for a light rig using an expected filename convention.
    if (!_splitDirectLight)
    {
        _lightRigFilename = _envRadianceFilename;
        _lightRigFilename.removeExtension();
        _lightRigFilename.addExtension(mx::MTLX_EXTENSION);
        _lightRigFilename = _searchPath.find(_lightRigFilename);
        if (_lightRigFilename.exists())
        {
            _lightRigDoc = mx::createDocument();
            mx::readFromXmlFile(_lightRigDoc, _lightRigFilename, _searchPath);
        }
        else
        {
            _lightRigDoc = nullptr;
        }
    }

    // Invalidate the existing environment material, if any.
    _envMaterial = nullptr;
}

void Viewer::applyDirectLights(mx::DocumentPtr doc)
{
    if (_lightRigDoc)
    {
        doc->importLibrary(_lightRigDoc);
        _xincludeFiles.insert(_lightRigFilename);
    }

    try
    {
        std::vector<mx::NodePtr> lights;
        _lightHandler->findLights(doc, lights);
        _lightHandler->registerLights(doc, lights, _genContext);
#ifndef MATERIALXVIEW_METAL_BACKEND
        _lightHandler->registerLights(doc, lights, _genContextEssl);
#endif
        _lightHandler->setLightSources(lights);
    }
    catch (std::exception& e)
    {
        new ng::MessageDialog(this, ng::MessageDialog::Type::Warning, "Failed to set up lighting", e.what());
    }
}

void Viewer::assignMaterial(mx::MeshPartitionPtr geometry, mx::MaterialPtr material)
{
    if (!geometry || _geometryHandler->getMeshes().empty())
    {
        return;
    }

    if (geometry == getSelectedGeometry())
    {
        setSelectedMaterial(material);
        if (material)
        {
            updateDisplayedProperties();
        }
    }

    if (material)
    {
        _materialAssignments[geometry] = material;
        material->unbindGeometry();
    }
    else
    {
        _materialAssignments.erase(geometry);
    }
}

mx::FilePath Viewer::getBaseOutputPath()
{
    mx::FilePath baseFilename = _searchPath.find(_materialFilename);
    baseFilename.removeExtension();
    mx::FilePath outputPath = mx::getEnviron("MATERIALX_VIEW_OUTPUT_PATH");
    if (!outputPath.isEmpty())
    {
        baseFilename = outputPath / baseFilename.getBaseName();
    }
    return baseFilename;
}

mx::ElementPredicate Viewer::getElementPredicate()
{
    return [this](mx::ConstElementPtr elem)
    {
        if (elem->hasSourceUri())
        {
            return (_xincludeFiles.count(elem->getSourceUri()) == 0);
        }
        return true;
    };
}

void Viewer::createLoadMeshInterface(ng::ref<Widget> parent, const std::string& label)
{
    ng::ref<ng::Button> meshButton = new ng::Button(parent, label);
    meshButton->set_icon(FA_FOLDER);
    meshButton->set_tooltip("Load a new geometry in the OBJ or glTF format.");
    meshButton->set_callback([this]()
    {
        m_process_events = false;
        std::string filename = ng::file_dialog(
        {
            { "obj", "Wavefront OBJ" },
            { "gltf", "GLTF ASCII" },
            { "glb", "GLTF Binary"}
        }, false);
        if (!filename.empty())
        {
            loadMesh(filename);

            _meshRotation = mx::Vector3();
            _meshScale = 1.0f;
            _cameraTarget = mx::Vector3();

            initCamera();
        }
        m_process_events = true;
    });
}

void Viewer::createLoadMaterialsInterface(ng::ref<Widget> parent, const std::string& label)
{
    ng::ref<ng::Button> materialButton = new ng::Button(parent, label);
    materialButton->set_icon(FA_FOLDER);
    materialButton->set_tooltip("Load a material document in the MTLX format.");
    materialButton->set_callback([this]()
    {
        m_process_events = false;
        std::string filename = ng::file_dialog({ { "mtlx", "MaterialX" } }, false);
        if (!filename.empty())
        {
            _materialFilename = filename;
            loadDocument(_materialFilename, _stdLib);
        }
        m_process_events = true;
    });
}

void Viewer::createLoadEnvironmentInterface(ng::ref<Widget> parent, const std::string& label)
{
    ng::ref<ng::Button> envButton = new ng::Button(parent, label);
    envButton->set_icon(FA_FOLDER);
    envButton->set_tooltip("Load a lat-long environment light in the HDR format.");
    envButton->set_callback([this]()
    {
        m_process_events = false;
        mx::StringSet extensions = _imageHandler->supportedExtensions();
        std::vector<std::pair<std::string, std::string>> filetypes;
        for (const auto& extension : extensions)
        {
            filetypes.emplace_back(extension, extension);
        }
        std::string filename = ng::file_dialog(filetypes, false);
        if (!filename.empty())
        {
            _envRadianceFilename = filename;
            loadEnvironmentLight();
            loadDocument(_materialFilename, _stdLib);
            invalidateShadowMap();
        }
        m_process_events = true;
    });
}

void Viewer::createSaveMaterialsInterface(ng::ref<Widget> parent, const std::string& label)
{
    ng::ref<ng::Button> materialButton = new ng::Button(parent, label);
    materialButton->set_icon(FA_SAVE);
    materialButton->set_tooltip("Save a material document in the MTLX format.");
    materialButton->set_callback([this]()
    {
        m_process_events = false;
        mx::MaterialPtr material = getSelectedMaterial();
        mx::FilePath filename = ng::file_dialog({ { "mtlx", "MaterialX" } }, true);

        // Save document
        if (material && !filename.isEmpty())
        {
            if (filename.getExtension() != mx::MTLX_EXTENSION)
            {
                filename.addExtension(mx::MTLX_EXTENSION);
            }

            mx::XmlWriteOptions writeOptions;
            writeOptions.elementPredicate = getElementPredicate();
            mx::writeToXmlFile(material->getDocument(), filename, &writeOptions);

            // Update material file name
            _materialFilename = filename;
        }
        m_process_events = true;
    });
}

void Viewer::createPropertyEditorInterface(ng::ref<Widget> parent, const std::string& label)
{
    ng::ref<ng::Button> editorButton = new ng::Button(parent, label);
    editorButton->set_flags(ng::Button::ToggleButton);
    editorButton->set_tooltip("View or edit properties of the current material.");
    editorButton->set_change_callback([this](bool state)
    {
        _propertyEditor.setVisible(state);
        perform_layout();
    });
}

void Viewer::createDocumentationInterface(ng::ref<Widget> parent)
{
    ng::ref<ng::GridLayout> documentationLayout = new ng::GridLayout(ng::Orientation::Vertical, 3,
                                                                     ng::Alignment::Minimum, 13, 5);
    documentationLayout->set_row_alignment({ ng::Alignment::Minimum, ng::Alignment::Maximum });

    ng::ref<ng::Widget> documentationGroup = new ng::Widget(parent);
    documentationGroup->set_layout(documentationLayout);
    ng::ref<ng::Label> documentationLabel = new ng::Label(documentationGroup, "Documentation");
    documentationLabel->set_font_size(20);
    documentationLabel->set_font("sans-bold");

    _shortcutsButton = new ng::Button(documentationGroup, "Keyboard Shortcuts");
    _shortcutsButton->set_flags(ng::Button::ToggleButton);
    _shortcutsButton->set_icon(FA_CARET_RIGHT);
    _shortcutsButton->set_fixed_width(230);

    _shortcutsTable = new ng::Widget(documentationGroup);
    _shortcutsTable->set_layout(new ng::GroupLayout(13));
    _shortcutsTable->set_visible(false);

    // Recompute layout when showing/hiding shortcuts.
    _shortcutsButton->set_change_callback([this](bool state)
    {
        _shortcutsButton->set_icon(state ? FA_CARET_DOWN : FA_CARET_RIGHT);
        _shortcutsTable->set_visible(state);
        perform_layout();
    });

    // 2 cell layout for (key, description) pair.
    ng::ref<ng::GridLayout> gridLayout2 = new ng::GridLayout(ng::Orientation::Horizontal, 2,
                                                             ng::Alignment::Minimum, 2, 2);
    gridLayout2->set_col_alignment({ ng::Alignment::Minimum, ng::Alignment::Maximum });

    const std::array<std::pair<std::string, std::string>, 16> KEYBOARD_SHORTCUTS =
    {
        std::make_pair("R", "Reload the current material from file. "
                            "Hold SHIFT to reload all standard libraries as well."),
        std::make_pair("G", "Save the current GLSL shader source to file."),
        std::make_pair("O", "Save the current OSL shader source to file."),
        std::make_pair("M", "Save the current MDL shader source to file."),
        std::make_pair("L", "Load GLSL shader source from file. "
                            "Editing the source files before loading provides a way "
                            "to debug and experiment with shader source code."),
        std::make_pair("D", "Save each node graph in the current material as a DOT file. "
                            "See www.graphviz.org for more details on this format."),
        std::make_pair("F", "Capture the current frame and save to file."),
        std::make_pair("W", "Create a wedge rendering and save to file. "
                            "See Advanced Settings for additional controls."),
        std::make_pair("T", "Translate the current material to a different shading model. "
                            "See Advanced Settings for additional controls."),
        std::make_pair("B", "Bake the current material to textures. "
                            "See Advanced Settings for additional controls."),
        std::make_pair("UP","Select the previous geometry."),
        std::make_pair("DOWN","Select the next geometry."),
        std::make_pair("RIGHT", "Switch to the next material."),
        std::make_pair("LEFT", "Switch to the previous material."),
        std::make_pair("+", "Zoom in with the camera."),
        std::make_pair("-", "Zoom out with the camera.")
    };

    for (const auto& shortcut : KEYBOARD_SHORTCUTS)
    {
        ng::ref<ng::Widget> twoColumns = new ng::Widget(_shortcutsTable);
        twoColumns->set_layout(gridLayout2);

        ng::ref<ng::Label> keyLabel = new ng::Label(twoColumns, shortcut.first);
        keyLabel->set_font("sans-bold");
        keyLabel->set_font_size(16);
        keyLabel->set_fixed_width(40);

        ng::ref<ng::Label> descriptionLabel = new ng::Label(twoColumns, shortcut.second);
        descriptionLabel->set_font_size(16);
        descriptionLabel->set_fixed_width(160);
    }
}

void Viewer::createAdvancedSettings(ng::ref<Widget> parent)
{
    ng::ref<ng::PopupButton> advancedButton = new ng::PopupButton(parent, "Advanced Settings");
    advancedButton->set_icon(FA_TOOLS);
    advancedButton->set_chevron_icon(-1);
    advancedButton->set_tooltip("Asset and rendering options.");
    ng::ref<ng::Popup> advancedPopupParent = advancedButton->popup();
    advancedPopupParent->set_layout(new ng::GroupLayout());

    ng::ref<ng::VScrollPanel> scrollPanel = new ng::VScrollPanel(advancedPopupParent);
    scrollPanel->set_fixed_height(500);
    ng::ref<ng::Widget> advancedPopup = new ng::Widget(scrollPanel);
    advancedPopup->set_layout(new ng::BoxLayout(ng::Orientation::Vertical));

    ng::ref<ng::Widget> settingsGroup = new ng::Widget(advancedPopup);
    settingsGroup->set_layout(new ng::GroupLayout(13));
    ng::ref<ng::Label> viewLabel = new ng::Label(settingsGroup, "Viewing Options");
    viewLabel->set_font_size(20);
    viewLabel->set_font("sans-bold");

    ng::ref<ng::CheckBox> drawEnvironmentBox = new ng::CheckBox(settingsGroup, "Draw Environment");
    drawEnvironmentBox->set_checked(_drawEnvironment);
    drawEnvironmentBox->set_callback([this](bool enable)
    {
        _drawEnvironment = enable;
    });

    ng::ref<ng::CheckBox> outlineSelectedGeometryBox = new ng::CheckBox(settingsGroup, "Outline Selected Geometry");
    outlineSelectedGeometryBox->set_checked(_outlineSelection);
    outlineSelectedGeometryBox->set_callback([this](bool enable)
    {
        _outlineSelection = enable;
    });

    ng::ref<ng::Label> renderLabel = new ng::Label(settingsGroup, "Render Options");
    renderLabel->set_font_size(20);
    renderLabel->set_font("sans-bold");

    ng::ref<ng::CheckBox> transparencyBox = new ng::CheckBox(settingsGroup, "Render Transparency");
    transparencyBox->set_checked(_renderTransparency);
    transparencyBox->set_callback([this](bool enable)
    {
        _renderTransparency = enable;
    });

    ng::ref<ng::CheckBox> doubleSidedBox = new ng::CheckBox(settingsGroup, "Render Double-Sided");
    doubleSidedBox->set_checked(_renderDoubleSided);
    doubleSidedBox->set_callback([this](bool enable)
    {
        _renderDoubleSided = enable;
    });

    ng::ref<ng::CheckBox> importanceSampleBox = new ng::CheckBox(settingsGroup, "Environment FIS");
    importanceSampleBox->set_checked(_genContext.getOptions().hwSpecularEnvironmentMethod == mx::SPECULAR_ENVIRONMENT_FIS);
    _lightHandler->setUsePrefilteredMap(_genContext.getOptions().hwSpecularEnvironmentMethod != mx::SPECULAR_ENVIRONMENT_FIS);
    importanceSampleBox->set_callback([this](bool enable)
    {
        _genContext.getOptions().hwSpecularEnvironmentMethod = enable ? mx::SPECULAR_ENVIRONMENT_FIS : mx::SPECULAR_ENVIRONMENT_PREFILTER;
#ifndef MATERIALXVIEW_METAL_BACKEND
        _genContextEssl.getOptions().hwSpecularEnvironmentMethod = _genContext.getOptions().hwSpecularEnvironmentMethod;
#endif
        _lightHandler->setUsePrefilteredMap(!enable);
        reloadShaders();
    });

    ng::ref<ng::CheckBox> refractionBox = new ng::CheckBox(settingsGroup, "Transmission Refraction");
    refractionBox->set_checked(_genContext.getOptions().hwTransmissionRenderMethod == mx::TRANSMISSION_REFRACTION);
    refractionBox->set_callback([this](bool enable)
    {
        _genContext.getOptions().hwTransmissionRenderMethod = enable ? mx::TRANSMISSION_REFRACTION : mx::TRANSMISSION_OPACITY;
#ifndef MATERIALXVIEW_METAL_BACKEND
        _genContextEssl.getOptions().hwTransmissionRenderMethod = _genContext.getOptions().hwTransmissionRenderMethod;
#endif
        reloadShaders();
    });

    ng::ref<ng::CheckBox> refractionSidedBox = new ng::CheckBox(settingsGroup, "Refraction Two-Sided");
    refractionSidedBox->set_checked(_lightHandler->getRefractionTwoSided());
    refractionSidedBox->set_callback([this](bool enable)
    {
        _lightHandler->setRefractionTwoSided(enable);
    });

    ng::ref<ng::CheckBox> shaderInterfaceBox = new ng::CheckBox(settingsGroup, "Reduce Shader Interface");
    shaderInterfaceBox->set_checked(_genContext.getOptions().shaderInterfaceType == mx::SHADER_INTERFACE_REDUCED);
    shaderInterfaceBox->set_callback([this](bool enable)
    {
        mx::ShaderInterfaceType interfaceType = enable ? mx::SHADER_INTERFACE_REDUCED : mx::SHADER_INTERFACE_COMPLETE;
        setShaderInterfaceType(interfaceType);
    });

    ng::ref<ng::CheckBox> constantElisionBox = new ng::CheckBox(settingsGroup, "Constant Elision");
    constantElisionBox->set_checked(_genContext.getOptions().elideConstantNodes);
    constantElisionBox->set_callback([this](bool enable)
    {
        _genContext.getOptions().elideConstantNodes = enable;
        reloadShaders();
    });

    ng::ref<ng::CheckBox> premultipliedBsdfAddBox = new ng::CheckBox(settingsGroup, "Premultiplied BSDF Add");
    premultipliedBsdfAddBox->set_checked(_genContext.getOptions().premultipliedBsdfAdd);
    premultipliedBsdfAddBox->set_callback([this](bool enable)
    {
        _genContext.getOptions().premultipliedBsdfAdd = enable;
        reloadShaders();
    });

    ng::ref<ng::CheckBox> distributeLayerBox = new ng::CheckBox(settingsGroup, "Distribute Layer Over Mix");
    distributeLayerBox->set_checked(_genContext.getOptions().distributeLayerOverBsdfMix);
    distributeLayerBox->set_callback([this](bool enable)
    {
        _genContext.getOptions().distributeLayerOverBsdfMix = enable;
        reloadShaders();
    });

    ng::ref<ng::Widget> albedoGroup = new Widget(settingsGroup);
    albedoGroup->set_layout(new ng::BoxLayout(ng::Orientation::Horizontal));
    new ng::Label(albedoGroup, "Albedo Method:");
    mx::StringVec albedoOptions = { "Analytic", "Table", "MC" };
    ng::ref<ng::ComboBox> albedoBox = new ng::ComboBox(albedoGroup, albedoOptions);
    albedoBox->set_chevron_icon(-1);
    albedoBox->set_selected_index((int) _genContext.getOptions().hwDirectionalAlbedoMethod );
    albedoBox->set_callback([this](int index)
    {
        _genContext.getOptions().hwDirectionalAlbedoMethod = (mx::HwDirectionalAlbedoMethod) index;
        reloadShaders();
        try
        {
            _renderPipeline->updateAlbedoTable(ALBEDO_TABLE_SIZE);
        }
        catch (mx::ExceptionRenderError& e)
        {
            for (const std::string& error : e.errorLog())
            {
                std::cerr << error << std::endl;
            }
            new ng::MessageDialog(this, ng::MessageDialog::Type::Warning, "Shader generation error", e.what());
            _materialAssignments.clear();
        }
        catch (std::exception& e)
        {
            new ng::MessageDialog(this, ng::MessageDialog::Type::Warning, "Failed to update albedo table", e.what());
            _materialAssignments.clear();
        }
    });

    ng::ref<ng::Widget> sampleGroup = new Widget(settingsGroup);
    sampleGroup->set_layout(new ng::BoxLayout(ng::Orientation::Horizontal));
    new ng::Label(sampleGroup, "Environment Samples:");
    mx::StringVec sampleOptions;
    for (int i = MIN_ENV_SAMPLE_COUNT; i <= MAX_ENV_SAMPLE_COUNT; i *= 4)
    {
        m_process_events = false;
        sampleOptions.push_back(std::to_string(i));
        m_process_events = true;
    }
    ng::ref<ng::ComboBox> sampleBox = new ng::ComboBox(sampleGroup, sampleOptions);
    sampleBox->set_chevron_icon(-1);
    sampleBox->set_selected_index((int)std::log2(_lightHandler->getEnvSampleCount() / MIN_ENV_SAMPLE_COUNT) / 2);
    sampleBox->set_callback([this](int index)
    {
        _lightHandler->setEnvSampleCount(MIN_ENV_SAMPLE_COUNT * (int) std::pow(4, index));
    });

    ng::ref<ng::Widget> fresnelGroup = new Widget(settingsGroup);
    fresnelGroup->set_layout(new ng::BoxLayout(ng::Orientation::Horizontal));
    new ng::Label(fresnelGroup, "Airy Fresnel Iterations:");
    mx::StringVec fresnelOptions;
    for (int i = MIN_AIRY_FRESNEL_ITERATIONS; i <= MAX_AIRY_FRESNEL_ITERATIONS; i *= 2)
    {
        fresnelOptions.push_back(std::to_string(i));
    }
    ng::ref<ng::ComboBox> fresnelBox = new ng::ComboBox(fresnelGroup, fresnelOptions);
    fresnelBox->set_chevron_icon(-1);
    fresnelBox->set_selected_index((int)std::log2(_genContext.getOptions().hwAiryFresnelIterations / MIN_AIRY_FRESNEL_ITERATIONS));
    fresnelBox->set_callback([this](int index)
    {
        _genContext.getOptions().hwAiryFresnelIterations = MIN_AIRY_FRESNEL_ITERATIONS * (int)std::pow(2, index);
#ifndef MATERIALXVIEW_METAL_BACKEND
        _genContextEssl.getOptions().hwAiryFresnelIterations = _genContext.getOptions().hwAiryFresnelIterations;
#endif
        reloadShaders();
    });

    ng::ref<ng::Label> lightingLabel = new ng::Label(settingsGroup, "Lighting Options");
    lightingLabel->set_font_size(20);
    lightingLabel->set_font("sans-bold");

    ng::ref<ng::CheckBox> directLightingBox = new ng::CheckBox(settingsGroup, "Direct Lighting");
    directLightingBox->set_checked(_lightHandler->getDirectLighting());
    directLightingBox->set_callback([this](bool enable)
    {
        _lightHandler->setDirectLighting(enable);
    });

    ng::ref<ng::CheckBox> indirectLightingBox = new ng::CheckBox(settingsGroup, "Indirect Lighting");
    indirectLightingBox->set_checked(_lightHandler->getIndirectLighting());
    indirectLightingBox->set_callback([this](bool enable)
    {
        _lightHandler->setIndirectLighting(enable);
    });

    ng::ref<ng::Widget> lightRotationRow = new ng::Widget(settingsGroup);
    lightRotationRow->set_layout(new ng::BoxLayout(ng::Orientation::Horizontal));
    mx::UIProperties ui;
    ui.uiMin = mx::Value::createValue(0.0f);
    ui.uiMax = mx::Value::createValue(360.0f);
    ng::ref<ng::FloatBox<float>> lightRotationBox = createFloatWidget(lightRotationRow, "Light Rotation:",
        _lightRotation, &ui, [this](float value)
    {
        _lightRotation = value;
        invalidateShadowMap();
    });
    lightRotationBox->set_editable(true);

    ng::ref<ng::Label> shadowingLabel = new ng::Label(settingsGroup, "Shadowing Options");
    shadowingLabel->set_font_size(20);
    shadowingLabel->set_font("sans-bold");

    ng::ref<ng::CheckBox> shadowMapBox = new ng::CheckBox(settingsGroup, "Shadow Map");
    shadowMapBox->set_checked(_genContext.getOptions().hwShadowMap);
    shadowMapBox->set_callback([this](bool enable)
    {
        _genContext.getOptions().hwShadowMap = enable;
        reloadShaders();
    });

    ng::ref<ng::CheckBox> ambientOcclusionBox = new ng::CheckBox(settingsGroup, "Ambient Occlusion");
    ambientOcclusionBox->set_checked(_genContext.getOptions().hwAmbientOcclusion);
    ambientOcclusionBox->set_callback([this](bool enable)
    {
        _genContext.getOptions().hwAmbientOcclusion = enable;
        reloadShaders();
    });

    ng::ref<ng::Widget> ambientOcclusionGainRow = new ng::Widget(settingsGroup);
    ambientOcclusionGainRow->set_layout(new ng::BoxLayout(ng::Orientation::Horizontal));
    ng::ref<ng::FloatBox<float>> ambientOcclusionGainBox = createFloatWidget(ambientOcclusionGainRow, "AO Gain:",
        _ambientOcclusionGain, nullptr, [this](float value)
    {
        _ambientOcclusionGain = value;
    });
    ambientOcclusionGainBox->set_editable(true);

    ng::ref<ng::Label> sceneLabel = new ng::Label(settingsGroup, "Scene Options");
    sceneLabel->set_font_size(20);
    sceneLabel->set_font("sans-bold");

    ng::ref<Widget> unitGroup = new Widget(settingsGroup);
    unitGroup->set_layout(new ng::BoxLayout(ng::Orientation::Horizontal));
    new ng::Label(unitGroup, "Distance Unit:");
    ng::ref<ng::ComboBox> distanceUnitBox = new ng::ComboBox(unitGroup, _distanceUnitOptions);
    distanceUnitBox->set_fixed_size(ng::Vector2i(100, 20));
    distanceUnitBox->set_chevron_icon(-1);
    if (_distanceUnitConverter)
    {
        distanceUnitBox->set_selected_index(_distanceUnitConverter->getUnitAsInteger("meter"));
    }
    distanceUnitBox->set_callback([this](int index)
    {
        m_process_events = false;
        _genContext.getOptions().targetDistanceUnit = _distanceUnitOptions[index];
#ifndef MATERIALXVIEW_METAL_BACKEND
        _genContextEssl.getOptions().targetDistanceUnit = _distanceUnitOptions[index];
#endif
#if MATERIALX_BUILD_GEN_OSL
        _genContextOsl.getOptions().targetDistanceUnit = _distanceUnitOptions[index];
#endif
#if MATERIALX_BUILD_GEN_MDL
        _genContextMdl.getOptions().targetDistanceUnit = _distanceUnitOptions[index];
#endif
        reloadShaders();
        m_process_events = true;
    });

    ng::ref<ng::Label> meshLoading = new ng::Label(settingsGroup, "Mesh Loading Options");
    meshLoading->set_font_size(20);
    meshLoading->set_font("sans-bold");

    ng::ref<ng::CheckBox> splitUdimsBox = new ng::CheckBox(settingsGroup, "Split By UDIMs");
    splitUdimsBox->set_checked(_splitByUdims);
    splitUdimsBox->set_callback([this](bool enable)
    {
        _splitByUdims = enable;
    });

    ng::ref<ng::Label> materialLoading = new ng::Label(settingsGroup, "Material Loading Options");
    materialLoading->set_font_size(20);
    materialLoading->set_font("sans-bold");

    ng::ref<ng::CheckBox> mergeMaterialsBox = new ng::CheckBox(settingsGroup, "Merge Materials");
    mergeMaterialsBox->set_checked(_mergeMaterials);
    mergeMaterialsBox->set_callback([this](bool enable)
    {
        _mergeMaterials = enable;
    });

    ng::ref<ng::CheckBox> showInputsBox = new ng::CheckBox(settingsGroup, "Show All Inputs");
    showInputsBox->set_checked(_showAllInputs);
    showInputsBox->set_callback([this](bool enable)
    {
        _showAllInputs = enable;
    });

    ng::ref<ng::CheckBox> flattenBox = new ng::CheckBox(settingsGroup, "Flatten Subgraphs");
    flattenBox->set_checked(_flattenSubgraphs);
    flattenBox->set_callback([this](bool enable)
    {
        _flattenSubgraphs = enable;
    });

    ng::ref<ng::Label> envLoading = new ng::Label(settingsGroup, "Environment Loading Options");
    envLoading->set_font_size(20);
    envLoading->set_font("sans-bold");

    ng::ref<ng::CheckBox> normalizeEnvBox = new ng::CheckBox(settingsGroup, "Normalize Environment");
    normalizeEnvBox->set_checked(_normalizeEnvironment);
    normalizeEnvBox->set_callback([this](bool enable)
    {
        _normalizeEnvironment = enable;
    });

    ng::ref<ng::CheckBox> splitDirectLightBox = new ng::CheckBox(settingsGroup, "Split Direct Light");
    splitDirectLightBox->set_checked(_splitDirectLight);
    splitDirectLightBox->set_callback([this](bool enable)
    {
        _splitDirectLight = enable;
    });

    ng::ref<ng::Label> translationLabel = new ng::Label(settingsGroup, "Translation Options (T)");
    translationLabel->set_font_size(20);
    translationLabel->set_font("sans-bold");

    ng::ref<ng::Widget> targetShaderGroup = new ng::Widget(settingsGroup);
    targetShaderGroup->set_layout(new ng::BoxLayout(ng::Orientation::Horizontal));
    new ng::Label(targetShaderGroup, "Target Shader");
    ng::ref<ng::TextBox> targetShaderBox = new ng::TextBox(targetShaderGroup, _targetShader);
    targetShaderBox->set_callback([this](const std::string& choice)
    {
        _targetShader = choice;
        return true;
    });
    targetShaderBox->set_font_size(16);
    targetShaderBox->set_editable(true);

    ng::ref<ng::Label> textureLabel = new ng::Label(settingsGroup, "Texture Baking Options (B)");
    textureLabel->set_font_size(20);
    textureLabel->set_font("sans-bold");

    ng::ref<ng::CheckBox> bakeHdrBox = new ng::CheckBox(settingsGroup, "Bake HDR Textures");
    bakeHdrBox->set_checked(_bakeHdr);
    bakeHdrBox->set_callback([this](bool enable)
    {
        _bakeHdr = enable;
    });

    ng::ref<ng::CheckBox> bakeAverageBox = new ng::CheckBox(settingsGroup, "Bake Averaged Textures");
    bakeAverageBox->set_checked(_bakeAverage);
    bakeAverageBox->set_callback([this](bool enable)
    {
        _bakeAverage = enable;
    });

    ng::ref<ng::CheckBox> bakeOptimized = new ng::CheckBox(settingsGroup, "Optimize Baked Constants");
    bakeOptimized->set_checked(_bakeOptimize);
    bakeOptimized->set_callback([this](bool enable)
    {
        _bakeOptimize = enable;
    });

    ng::ref<ng::CheckBox> bakeDocumentPerMaterial= new ng::CheckBox(settingsGroup, "Bake Document Per Material");
    bakeDocumentPerMaterial->set_checked(_bakeDocumentPerMaterial);
    bakeDocumentPerMaterial->set_callback([this](bool enable)
    {
        _bakeDocumentPerMaterial = enable;
    });    

    ng::ref<ng::Label> wedgeLabel = new ng::Label(settingsGroup, "Wedge Render Options (W)");
    wedgeLabel->set_font_size(20);
    wedgeLabel->set_font("sans-bold");

    ng::ref<ng::Widget> wedgeNameGroup = new ng::Widget(settingsGroup);
    wedgeNameGroup->set_layout(new ng::BoxLayout(ng::Orientation::Horizontal));
    new ng::Label(wedgeNameGroup, "Property Name");
    ng::ref<ng::TextBox> wedgeNameBox = new ng::TextBox(wedgeNameGroup, _wedgePropertyName);
    wedgeNameBox->set_callback([this](const std::string& choice)
    {
        _wedgePropertyName = choice;
        return true;
    });
    wedgeNameBox->set_font_size(16);
    wedgeNameBox->set_editable(true);

    ng::ref<ng::Widget> wedgeMinGroup = new ng::Widget(settingsGroup);
    wedgeMinGroup->set_layout(new ng::BoxLayout(ng::Orientation::Horizontal));
    mx::UIProperties wedgeProp;
    wedgeProp.uiSoftMin = mx::Value::createValue(0.0f);
    wedgeProp.uiSoftMax = mx::Value::createValue(1.0f);
    ng::ref<ng::FloatBox<float>> wedgeMinBox = createFloatWidget(wedgeMinGroup, "Property Min:",
        _wedgePropertyMax, &wedgeProp, [this](float value)
    {
        _wedgePropertyMin = value;
    });
    wedgeMinBox->set_value(0.0);
    wedgeMinBox->set_editable(true);

    ng::ref<ng::Widget> wedgeMaxGroup = new ng::Widget(settingsGroup);
    wedgeMaxGroup->set_layout(new ng::BoxLayout(ng::Orientation::Horizontal));
    ng::ref<ng::FloatBox<float>> wedgeMaxBox = createFloatWidget(wedgeMaxGroup, "Property Max:",
        _wedgePropertyMax, &wedgeProp, [this](float value)
    {
        _wedgePropertyMax = value;
    });
    wedgeMaxBox->set_value(1.0);
    wedgeMaxBox->set_editable(true);

    ng::ref<ng::Widget> wedgeCountGroup = new ng::Widget(settingsGroup);
    wedgeCountGroup->set_layout(new ng::BoxLayout(ng::Orientation::Horizontal));
    mx::UIProperties wedgeCountProp;
    wedgeCountProp.uiMin = mx::Value::createValue(1);
    wedgeCountProp.uiSoftMax = mx::Value::createValue(8);
    wedgeCountProp.uiStep = mx::Value::createValue(1);
    ng::ref<ng::IntBox<int>> wedgeCountBox = createIntWidget(wedgeCountGroup, "Image Count:",
        _wedgeImageCount, &wedgeCountProp, [this](int value)
    {
        _wedgeImageCount = value;
    });
    wedgeCountBox->set_value(8);
    wedgeCountBox->set_editable(true);

    createDocumentationInterface(advancedPopup);
}

void Viewer::updateGeometrySelections()
{
    _geometryList.clear();
    if (_geometryHandler->getMeshes().empty())
    {
        return;
    }
    for (auto mesh : _geometryHandler->getMeshes())
    {
        for (size_t partIndex = 0; partIndex < mesh->getPartitionCount(); partIndex++)
        {
            mx::MeshPartitionPtr part = mesh->getPartition(partIndex);
            _geometryList.push_back(part);
        }
    }

    std::vector<std::string> items;
    for (const mx::MeshPartitionPtr& part : _geometryList)
    {
        std::string geomName = part->getName();
        mx::StringVec geomSplit = mx::splitString(geomName, ":");
        if (!geomSplit.empty() && !geomSplit[geomSplit.size() - 1].empty())
        {
            geomName = geomSplit[geomSplit.size() - 1];
        }
        items.push_back(geomName);
    }
    _geometrySelectionBox->set_items(items);

    _geomLabel->set_visible(items.size() > 1);
    _geometrySelectionBox->set_visible(items.size() > 1);
    _selectedGeom = 0;

    perform_layout();
}

void Viewer::updateMaterialSelections()
{
    std::vector<std::string> items;
    for (const auto& material : _materials)
    {
        mx::ElementPtr displayElem = material->getMaterialNode() ?
                                     material->getMaterialNode() :
                                     material->getElement();
        std::string displayName = displayElem->getName();
        if (displayName == "out")
        {
            displayName = displayElem->getParent()->getName();
        }
        if (!material->getUdim().empty())
        {
            displayName += " (" + material->getUdim() + ")";
        }
        items.push_back(displayName);
    }
    _materialSelectionBox->set_items(items);

    _materialLabel->set_visible(items.size() > 1);
    _materialSelectionBox->set_visible(items.size() > 1);

    perform_layout();
}

void Viewer::updateMaterialSelectionUI()
{
    for (const auto& pair : _materialAssignments)
    {
        if (pair.first == getSelectedGeometry())
        {
            for (size_t i = 0; i < _materials.size(); i++)
            {
                if (_materials[i] == pair.second)
                {
                    _materialSelectionBox->set_selected_index((int) i);
                    break;
                }
            }
        }
    }
    perform_layout();
}

void Viewer::loadMesh(const mx::FilePath& filename)
{
    _geometryHandler->clearGeometry();
    if (_geometryHandler->loadGeometry(filename))
    {
        _meshFilename = filename;
        if (_splitByUdims)
        {
            for (auto mesh : _geometryHandler->getMeshes())
            {
                mesh->splitByUdims();
            }
        }

        updateGeometrySelections();

        // Assign the selected material to all geometries.
        _materialAssignments.clear();
        mx::MaterialPtr material = getSelectedMaterial();
        if (material)
        {
            for (mx::MeshPartitionPtr geom : _geometryList)
            {
                assignMaterial(geom, material);
            }
        }

        // Unbind utility materials from the previous geometry.
        if (_wireMaterial)
        {
            _wireMaterial->unbindGeometry();
        }
        if (_shadowMaterial)
        {
            _shadowMaterial->unbindGeometry();
        }

        invalidateShadowMap();
    }
    else
    {
        new ng::MessageDialog(this, ng::MessageDialog::Type::Warning, "Mesh Loading Error", filename);
    }
}

void Viewer::loadDocument(const mx::FilePath& filename, mx::DocumentPtr libraries)
{
    // Set up read options.
    mx::XmlReadOptions readOptions;
    readOptions.readXIncludeFunction = [](mx::DocumentPtr doc, const mx::FilePath& filename,
                                          const mx::FileSearchPath& searchPath, const mx::XmlReadOptions* options)
    {
        mx::FilePath resolvedFilename = searchPath.find(filename);
        if (resolvedFilename.exists())
        {
            readFromXmlFile(doc, resolvedFilename, searchPath, options);
        }
        else
        {
            std::cerr << "Include file not found: " << filename.asString() << std::endl;
        }
    };

    // Clear user data on the generator.
    _genContext.clearUserData();
#ifndef MATERIALXVIEW_METAL_BACKEND
    _genContextEssl.clearUserData();
#endif

    // Clear materials if merging is not requested.
    if (!_mergeMaterials)
    {
        for (mx::MeshPartitionPtr geom : _geometryList)
        {
            if (_materialAssignments.count(geom))
            {
                assignMaterial(geom, nullptr);
            }
        }
        _materials.clear();
    }

    std::vector<mx::MaterialPtr> newMaterials;
    try
    {
        // Load source document.
        mx::DocumentPtr doc = mx::createDocument();
        mx::readFromXmlFile(doc, filename, _searchPath, &readOptions);
        _materialSearchPath = mx::getSourceSearchPath(doc);

        // Store data library reference.
        doc->setDataLibrary(libraries);

        // Apply direct lights.
        applyDirectLights(doc);

        // Apply modifiers to the content document.
        applyModifiers(doc, _modifiers);

        // Register any data types in the document.
        _genContext.getShaderGenerator().registerTypeDefs(doc);

        // Flatten subgraphs if requested.
        if (_flattenSubgraphs)
        {
            doc->flattenSubgraphs();
            for (mx::NodeGraphPtr graph : doc->getNodeGraphs())
            {
                if (graph->getActiveSourceUri() == doc->getActiveSourceUri())
                {
                    graph->flattenSubgraphs();
                }
            }
        }

        // Validate the document.
        std::string message;
        if (!doc->validate(&message))
        {
            std::cerr << "*** Validation warnings for " << _materialFilename.getBaseName() << " ***" << std::endl;
            std::cerr << message;
        }

        // If requested, add implicit inputs to top-level nodes.
        if (_showAllInputs)
        {
            for (mx::NodePtr node : doc->getNodes())
            {
                node->addInputsFromNodeDef();
            }
        }

        // Find new renderable elements.
        mx::StringVec renderablePaths;
        std::vector<mx::TypedElementPtr> elems = mx::findRenderableElements(doc);
        if (elems.empty())
        {
            throw mx::Exception("No renderable elements found in " + _materialFilename.getBaseName());
        }
        std::vector<mx::NodePtr> materialNodes;
        for (mx::TypedElementPtr elem : elems)
        {
            mx::TypedElementPtr renderableElem = elem;
            mx::NodePtr node = elem->asA<mx::Node>();
            materialNodes.push_back(node && node->getType() == mx::MATERIAL_TYPE_STRING ? node : nullptr);
            renderablePaths.push_back(renderableElem->getNamePath());
        }

        // Check for any udim set.
        mx::ValuePtr udimSetValue = doc->getGeomPropValue(mx::UDIM_SET_PROPERTY);

        // Create new materials.
        mx::TypedElementPtr udimElement;
        for (size_t i=0; i<renderablePaths.size(); i++)
        {
            const auto& renderablePath = renderablePaths[i];
            mx::ElementPtr elem = doc->getDescendant(renderablePath);
            mx::TypedElementPtr typedElem = elem ? elem->asA<mx::TypedElement>() : nullptr;
            if (!typedElem)
            {
                continue;
            }
            if (udimSetValue && udimSetValue->isA<mx::StringVec>())
            {
                for (const std::string& udim : udimSetValue->asA<mx::StringVec>())
                {
                    mx::MaterialPtr mat = _renderPipeline->createMaterial();
                    mat->setDocument(doc);
                    mat->setElement(typedElem);
                    mat->setMaterialNode(materialNodes[i]);
                    mat->setUdim(udim);
                    newMaterials.push_back(mat);
                    
                    udimElement = typedElem;
                }
            }
            else
            {
                mx::MaterialPtr mat = _renderPipeline->createMaterial();
                mat->setDocument(doc);
                mat->setElement(typedElem);
                mat->setMaterialNode(materialNodes[i]);
                newMaterials.push_back(mat);
            }
        }

        if (!newMaterials.empty())
        {
            // Extend the image search path to include material source folders.
            mx::FileSearchPath extendedSearchPath = _searchPath;
            extendedSearchPath.append(_materialSearchPath);
            _imageHandler->setSearchPath(extendedSearchPath);

            // Clear cached implementations, in case libraries on the file system have changed.
            _genContext.clearNodeImplementations();
#ifndef MATERIALXVIEW_METAL_BACKEND
            _genContextEssl.clearNodeImplementations();
#endif

            // Add new materials to the global vector.
            _materials.insert(_materials.end(), newMaterials.begin(), newMaterials.end());

            mx::MaterialPtr udimMaterial = nullptr;
            for (mx::MaterialPtr mat : newMaterials)
            {
                mx::TypedElementPtr elem = mat->getElement();

                std::string udim = mat->getUdim();
                if (!udim.empty())
                {
                    if ((udimElement == elem) && udimMaterial)
                    {
                        // Reuse existing material for all udims
                        mat->copyShader(udimMaterial);
                    }
                    else
                    {
                        // Generate a shader for the new material.
                        mat->generateShader(_genContext);
                        if (udimElement == elem)
                        {
                            udimMaterial = mat;
                        }
                    }
                }
                else
                {
                    // Generate a shader for the new material.
                    mat->generateShader(_genContext);
                }
            }
       
            // Apply material assignments in the order in which they are declared within the document,
            // with later assignments superseding earlier ones.
            for (mx::LookPtr look : doc->getLooks())
            {
                for (mx::MaterialAssignPtr matAssign : look->getActiveMaterialAssigns())
                {
                    const std::string& activeGeom = matAssign->getActiveGeom();
                    for (mx::MeshPartitionPtr part : _geometryList)
                    {
                        std::string geom = part->getName();
                        for (const std::string& id : part->getSourceNames())
                        {
                            geom += mx::ARRAY_PREFERRED_SEPARATOR + id;
                        }
                        if (mx::geomStringsMatch(activeGeom, geom, true))
                        {
                            for (mx::MaterialPtr mat : newMaterials)
                            {
                                if (mat->getMaterialNode() == matAssign->getReferencedMaterial())
                                {
                                    assignMaterial(part, mat);
                                    break;
                                }
                            }
                        }
                        mx::CollectionPtr coll = matAssign->getCollection();
                        if (coll && coll->matchesGeomString(geom))
                        {
                            for (mx::MaterialPtr mat : newMaterials)
                            {
                                if (mat->getMaterialNode() == matAssign->getReferencedMaterial())
                                {
                                    assignMaterial(part, mat);
                                    break;
                                }
                            }
                        }
                    }
                }
            }

            // Apply UDIM assignments, if any.
            for (mx::MaterialPtr mat : newMaterials)
            {
                mx::NodePtr materialNode = mat->getMaterialNode();
                if (materialNode)
                {
                    // First check for implicit UDIM assignments.
                    std::string udim = mat->getUdim();
                    if (udim.empty())
                    {
                        // Fall back to name-based UDIM assignments.
                        for (const std::string& token : mx::splitString(materialNode->getName(), UDIM_SEPARATORS))
                        {
                            if (token.size() == 4 && std::all_of(token.begin(), token.end(), isdigit))
                            {
                                udim = token;
                            }
                        }
                    }
                    if (!udim.empty())
                    {
                        for (mx::MeshPartitionPtr geom : _geometryList)
                        {
                            if (geom->getName() == udim)
                            {
                                assignMaterial(geom, mat);
                            }
                        }
                    }
                }
            }

            // Apply fallback assignments.
            mx::MaterialPtr fallbackMaterial = newMaterials[0];
            if (!_mergeMaterials || fallbackMaterial->getUdim().empty())
            {
                for (mx::MeshPartitionPtr geom : _geometryList)
                {
                    if (!_materialAssignments[geom])
                    {
                        assignMaterial(geom, fallbackMaterial);
                    }
                }
            }
        }
    }
    catch (mx::ExceptionRenderError& e)
    {
        for (const std::string& error : e.errorLog())
        {
            std::cerr << error << std::endl;
        }
        new ng::MessageDialog(this, ng::MessageDialog::Type::Warning, "Shader generation error", e.what());
        _materialAssignments.clear();
    }
    catch (std::exception& e)
    {
        new ng::MessageDialog(this, ng::MessageDialog::Type::Warning, "Failed to load material", e.what());
        _materialAssignments.clear();
    }

    // Update material UI.
    updateMaterialSelections();
    updateMaterialSelectionUI();

    invalidateShadowMap();
    perform_layout();
}

void Viewer::reloadShaders()
{
    try
    {
        for (mx::MaterialPtr material : _materials)
        {
            material->generateShader(_genContext);
        }
        return;
    }
    catch (mx::ExceptionRenderError& e)
    {
        for (const std::string& error : e.errorLog())
        {
            std::cerr << error << std::endl;
        }
        new ng::MessageDialog(this, ng::MessageDialog::Type::Warning, "Shader generation error", e.what());
    }
    catch (std::exception& e)
    {
        new ng::MessageDialog(this, ng::MessageDialog::Type::Warning, "Failed to reload shaders", e.what());
    }

    _materialAssignments.clear();
}

void Viewer::saveShaderSource(mx::GenContext& context)
{
    try
    {
        mx::MaterialPtr material = getSelectedMaterial();
        mx::TypedElementPtr elem = material ? material->getElement() : nullptr;
        if (elem)
        {
            mx::FilePath sourceFilename = getBaseOutputPath();
#ifndef MATERIALXVIEW_METAL_BACKEND
            if (context.getShaderGenerator().getTarget() == mx::GlslShaderGenerator::TARGET)
            {
                mx::ShaderPtr shader = material->getShader();
                const std::string& pixelShader = shader->getSourceCode(mx::Stage::PIXEL);
                const std::string& vertexShader = shader->getSourceCode(mx::Stage::VERTEX);
                writeTextFile(pixelShader, sourceFilename.asString() + "_ps.glsl");
                writeTextFile(vertexShader, sourceFilename.asString() + "_vs.glsl");
                new ng::MessageDialog(this, ng::MessageDialog::Type::Information, "Saved GLSL source: ",
                    sourceFilename.asString() + "_*.glsl");
            }
            else if (context.getShaderGenerator().getTarget() == mx::EsslShaderGenerator::TARGET)
            {
                mx::ShaderPtr shader = createShader(elem->getNamePath(), context, elem);
                const std::string& pixelShader = shader->getSourceCode(mx::Stage::PIXEL);
                const std::string& vertexShader = shader->getSourceCode(mx::Stage::VERTEX);
                writeTextFile(vertexShader, sourceFilename.asString() + "_essl_vs.glsl");
                writeTextFile(pixelShader, sourceFilename.asString() + "_essl_ps.glsl");
                new ng::MessageDialog(this, ng::MessageDialog::Type::Information, "Saved ESSL source: ",
                    sourceFilename.asString() + "_essl_*.glsl");
            }
#else
            if (context.getShaderGenerator().getTarget() == mx::MslShaderGenerator::TARGET)
            {
                mx::ShaderPtr shader = material->getShader();
                const std::string& pixelShader = shader->getSourceCode(mx::Stage::PIXEL);
                const std::string& vertexShader = shader->getSourceCode(mx::Stage::VERTEX);
                writeTextFile(pixelShader, sourceFilename.asString() + "_ps.metal");
                writeTextFile(vertexShader, sourceFilename.asString() + "_vs.metal");
                new ng::MessageDialog(this, ng::MessageDialog::Type::Information, "Saved GLSL source: ",
                    sourceFilename.asString() + "_*.metal");
            }
#endif
#if MATERIALX_BUILD_GEN_OSL
            else if (context.getShaderGenerator().getTarget() == mx::OslShaderGenerator::TARGET)
            {
                mx::ShaderPtr shader = createShader(elem->getNamePath(), context, elem);
                const std::string& pixelShader = shader->getSourceCode(mx::Stage::PIXEL);
                sourceFilename.addExtension("osl");
                writeTextFile(pixelShader, sourceFilename);
                new ng::MessageDialog(this, ng::MessageDialog::Type::Information, "Saved OSL source: ", sourceFilename);
            }
#endif
#if MATERIALX_BUILD_GEN_MDL
            else if (context.getShaderGenerator().getTarget() == mx::MdlShaderGenerator::TARGET)
            {
                mx::ShaderPtr shader = createShader(elem->getNamePath(), context, elem);
                const std::string& pixelShader = shader->getSourceCode(mx::Stage::PIXEL);
                sourceFilename.addExtension("mdl");
                writeTextFile(pixelShader, sourceFilename);
                new ng::MessageDialog(this, ng::MessageDialog::Type::Information, "Saved MDL source: ", sourceFilename);
            }
#endif
        }
    }
    catch (std::exception& e)
    {
        new ng::MessageDialog(this, ng::MessageDialog::Type::Warning, "Cannot save source for material", e.what());
    }
}

void Viewer::loadShaderSource()
{
    try
    {
        mx::MaterialPtr material = getSelectedMaterial();
        mx::TypedElementPtr elem = material ? material->getElement() : nullptr;
        if (elem)
        {
            mx::FilePath sourceFilename = getBaseOutputPath();
            mx::FilePath pixelSourceFilename = sourceFilename.asString() + "_ps.glsl";
            mx::FilePath vertexSourceFilename = sourceFilename.asString() + "_vs.glsl";
            if (material->loadSource(vertexSourceFilename, pixelSourceFilename, material->hasTransparency()))
            {
                assignMaterial(getSelectedGeometry(), material);
            }
        }
    }
    catch (std::exception& e)
    {
        new ng::MessageDialog(this, ng::MessageDialog::Type::Warning, "Cannot load source for material", e.what());
    }
}

void Viewer::saveDotFiles()
{
    try
    {
        mx::MaterialPtr material = getSelectedMaterial();
        mx::TypedElementPtr elem = material ? material->getElement() : nullptr;
        mx::NodePtr shaderNode = elem->asA<mx::Node>();
        if (shaderNode)
        {
            mx::FilePath baseFilename = getBaseOutputPath();
            for (mx::InputPtr input : shaderNode->getInputs())
            {
                mx::OutputPtr output = input->getConnectedOutput();
                mx::ConstNodeGraphPtr nodeGraph = output ? output->getAncestorOfType<mx::NodeGraph>() : nullptr;
                if (nodeGraph)
                {
                    std::string dotString = nodeGraph->asStringDot();
                    std::string dotFilename = baseFilename.asString() + "_" + nodeGraph->getName() + ".dot";
                    writeTextFile(dotString, dotFilename);
                }
            }

            mx::NodeDefPtr nodeDef = shaderNode->getNodeDef();
            mx::InterfaceElementPtr implement = nodeDef ? nodeDef->getImplementation() : nullptr;
            mx::NodeGraphPtr nodeGraph = implement ? implement->asA<mx::NodeGraph>() : nullptr;
            if (nodeGraph)
            {
                std::string dotString = nodeGraph->asStringDot();
                std::string dotFilename = baseFilename.asString() + "_" + nodeDef->getName() + ".dot";
                writeTextFile(dotString, dotFilename);
            }
            new ng::MessageDialog(this, ng::MessageDialog::Type::Information, "Saved dot files: ", baseFilename.asString() + "_*.dot");
        }
    }
    catch (std::exception& e)
    {
        new ng::MessageDialog(this, ng::MessageDialog::Type::Warning, "Cannot save dot file for material", e.what());
    }
}

mx::UnsignedIntPair Viewer::computeBakingResolution(mx::ConstDocumentPtr doc)
{
    mx::ImageVec imageVec = _imageHandler->getReferencedImages(doc);
    mx::UnsignedIntPair bakingRes = mx::getMaxDimensions(imageVec);
    bakingRes.first = std::max(bakingRes.first, (unsigned int) 4);
    bakingRes.second = std::max(bakingRes.second, (unsigned int) 4);
    if (_bakeWidth)
    {
        bakingRes.first = std::max(_bakeWidth, (unsigned int) 4);
    }
    if (_bakeHeight)
    {
        bakingRes.second = std::max(_bakeHeight, (unsigned int) 4);
    }
    return bakingRes;
}

mx::DocumentPtr Viewer::translateMaterial()
{
    mx::MaterialPtr material = getSelectedMaterial();
    mx::DocumentPtr doc = material ? material->getDocument() : nullptr;
    if (!doc)
    {
        return nullptr;
    }

    mx::DocumentPtr translatedDoc = doc->copy();
    mx::ShaderTranslatorPtr translator = mx::ShaderTranslator::create();
    try
    {
        translator->translateAllMaterials(translatedDoc, _targetShader);
    }
    catch (std::exception& e)
    {
        new ng::MessageDialog(this, ng::MessageDialog::Type::Warning, "Failed to translate material", e.what());
        return nullptr;
    }

    return translatedDoc;
}

void Viewer::initContext(mx::GenContext& context)
{
    // Initialize search path.
    context.registerSourceCodeSearchPath(_searchPath);

    // Initialize color management.
    mx::ColorManagementSystemPtr cms;
#ifdef MATERIALX_BUILD_OCIO
    try
    {
        cms = mx::OcioColorManagementSystem::createFromBuiltinConfig(
            "ocio://studio-config-latest",
            context.getShaderGenerator().getTarget());
    }
    catch (const std::exception& /*e*/)
    {
        cms = mx::DefaultColorManagementSystem::create(context.getShaderGenerator().getTarget());
    }
#else
    cms = mx::DefaultColorManagementSystem::create(context.getShaderGenerator().getTarget());
#endif
    cms->loadLibrary(_stdLib);
    context.getShaderGenerator().setColorManagementSystem(cms);

    // Initialize unit management.
    mx::UnitSystemPtr unitSystem = mx::UnitSystem::create(context.getShaderGenerator().getTarget());
    unitSystem->loadLibrary(_stdLib);
    unitSystem->setUnitConverterRegistry(_unitRegistry);
    context.getShaderGenerator().setUnitSystem(unitSystem);
    context.getOptions().targetDistanceUnit = "meter";
}

void Viewer::loadStandardLibraries()
{
    // Initialize the standard library.
    try
    {
        _stdLib = mx::createDocument();
        _xincludeFiles = mx::loadLibraries(_libraryFolders, _searchPath, _stdLib);
        if (_xincludeFiles.empty())
        {
            std::cerr << "Could not find standard data libraries on the given search path: " << _searchPath.asString() << std::endl;
        }
    }
    catch (std::exception& e)
    {
        std::cerr << "Failed to load standard data libraries: " << e.what() << std::endl;
        return;
    }

    // Initialize unit management.
    mx::UnitTypeDefPtr distanceTypeDef = _stdLib->getUnitTypeDef("distance");
    _distanceUnitConverter = mx::LinearUnitConverter::create(distanceTypeDef);
    _unitRegistry->addUnitConverter(distanceTypeDef, _distanceUnitConverter);
    mx::UnitTypeDefPtr angleTypeDef = _stdLib->getUnitTypeDef("angle");
    mx::LinearUnitConverterPtr angleConverter = mx::LinearUnitConverter::create(angleTypeDef);
    _unitRegistry->addUnitConverter(angleTypeDef, angleConverter);

    // Create the list of supported distance units.
    auto unitScales = _distanceUnitConverter->getUnitScale();
    _distanceUnitOptions.resize(unitScales.size());
    for (const auto& unitScale : unitScales)
    {
        int location = _distanceUnitConverter->getUnitAsInteger(unitScale.first);
        _distanceUnitOptions[location] = unitScale.first;
    }

    // Initialize the generator contexts.
    initContext(_genContext);
#ifndef MATERIALXVIEW_METAL_BACKEND
    initContext(_genContextEssl);
#endif
#if MATERIALX_BUILD_GEN_OSL
    initContext(_genContextOsl);
#endif
#if MATERIALX_BUILD_GEN_MDL
    initContext(_genContextMdl);
#endif
}

bool Viewer::keyboard_event(int key, int scancode, int action, int modifiers)
{
    if (Screen::keyboard_event(key, scancode, action, modifiers))
    {
        return true;
    }

    // Adjust camera zoom.
    if (_userCameraEnabled)
    {
        if (key == GLFW_KEY_KP_ADD && action == GLFW_PRESS)
        {
            _cameraZoom *= 1.1f;
        }
        if (key == GLFW_KEY_KP_SUBTRACT && action == GLFW_PRESS)
        {
            _cameraZoom = std::max(0.1f, _cameraZoom * 0.9f);
        }
    }

    // Reload the current document, and optionally the standard libraries, from
    // the file system.
    if (key == GLFW_KEY_R && action == GLFW_PRESS)
    {
        mx::MaterialPtr material = getSelectedMaterial();
        mx::DocumentPtr doc = material ? material->getDocument() : nullptr;
        mx::FilePath filename = doc ? mx::FilePath(doc->getSourceUri()) : _materialFilename;
        if (modifiers == GLFW_MOD_SHIFT)
        {
            loadStandardLibraries();
        }
        loadDocument(filename, _stdLib);
        return true;
    }

    // Save GLSL shader source to file.
    if (key == GLFW_KEY_G && action == GLFW_PRESS)
    {
        saveShaderSource(_genContext);
        return true;
    }

#if MATERIALX_BUILD_GEN_OSL
    // Save OSL shader source to file.
    if (key == GLFW_KEY_O && action == GLFW_PRESS)
    {
        saveShaderSource(_genContextOsl);
        return true;
    }
#endif

#if MATERIALX_BUILD_GEN_MDL
    // Save MDL shader source to file.
    if (key == GLFW_KEY_M && action == GLFW_PRESS)
    {
        saveShaderSource(_genContextMdl);
        return true;
    }
#endif

    // Save Essl shader source to file.
    if (key == GLFW_KEY_E && action == GLFW_PRESS)
    {
#ifndef MATERIALXVIEW_METAL_BACKEND
        saveShaderSource(_genContextEssl);
#endif
        return true;
    }

    // Load GLSL shader source from file.  Editing the source files before
    // loading provides a way to debug and experiment with shader source code.
    if (key == GLFW_KEY_L && action == GLFW_PRESS)
    {
        loadShaderSource();
        return true;
    }

    // Clear the image cache, reloading all required images from the file system.
    if (key == GLFW_KEY_I && action == GLFW_PRESS && modifiers == GLFW_MOD_SHIFT)
    {
        _imageHandler->clearImageCache();
        return true;
    }

    // Save each node graph in the current material as a dot file.
    if (key == GLFW_KEY_D && action == GLFW_PRESS)
    {
        saveDotFiles();
        return true;
    }

    // Capture the current frame and save as an image file.
    if (key == GLFW_KEY_F && action == GLFW_PRESS)
    {
        _captureFilename = ng::file_dialog({ { mx::ImageLoader::PNG_EXTENSION, "Image File" } }, true);
        if (!_captureFilename.isEmpty())
        {
            if (_captureFilename.getExtension().empty())
            {
                _captureFilename.addExtension(mx::ImageLoader::PNG_EXTENSION);
            }
            _captureRequested = true;
        }
    }

    // Render a wedge for the current material.
    if (key == GLFW_KEY_W && action == GLFW_PRESS)
    {
        _wedgeFilename = ng::file_dialog({ { mx::ImageLoader::PNG_EXTENSION, "Image File" } }, true);
        if (!_wedgeFilename.isEmpty())
        {
            if (_wedgeFilename.getExtension().empty())
            {
                _wedgeFilename.addExtension(mx::ImageLoader::PNG_EXTENSION);
            }
            _wedgeRequested = true;
        }
    }

    // Request shader translation for the current material.
    if (key == GLFW_KEY_T && action == GLFW_PRESS)
    {
        mx::DocumentPtr translatedDoc = translateMaterial();
        if (translatedDoc)
        {
            mx::FilePath translatedFilename = getBaseOutputPath();
            translatedFilename = translatedFilename.asString() + "_" + _targetShader;
            translatedFilename.addExtension(mx::MTLX_EXTENSION);

            mx::XmlWriteOptions writeOptions;
            writeOptions.elementPredicate = getElementPredicate();
            mx::writeToXmlFile(translatedDoc, translatedFilename, &writeOptions);

            new ng::MessageDialog(this, ng::MessageDialog::Type::Information, "Saved translated material: ", translatedFilename);
        }
        return true;
    }

    // Request a bake of the current material.
    if (key == GLFW_KEY_B && action == GLFW_PRESS)
    {
        mx::FilePath filename = ng::file_dialog({ { "mtlx", "MaterialX" } }, true);
        if (!filename.isEmpty())
        {
            if (filename.getExtension() != mx::MTLX_EXTENSION)
            {
                filename.addExtension(mx::MTLX_EXTENSION);
            }
            _bakeRequested = true;
            _bakeFilename = filename;
        }
    }

    // Up and down keys cycle through selected geometries.
    if ((key == GLFW_KEY_UP || key == GLFW_KEY_DOWN) && action == GLFW_PRESS)
    {
        if (_geometryList.size() > 1)
        {
            if (key == GLFW_KEY_DOWN)
            {
                _selectedGeom = (_selectedGeom < _geometryList.size() - 1) ? _selectedGeom + 1 : 0;
            }
            else
            {
                _selectedGeom = (_selectedGeom > 0) ? _selectedGeom - 1 : _geometryList.size() - 1;
            }
        }
        _geometrySelectionBox->set_selected_index((int) _selectedGeom);
        updateMaterialSelectionUI();
        return true;
    }

    // Left and right keys cycle through selected materials.
    if ((key == GLFW_KEY_LEFT || key == GLFW_KEY_RIGHT) && action == GLFW_PRESS)
    {
        if (_materials.size() > 1)
        {
            if (key == GLFW_KEY_RIGHT)
            {
                _selectedMaterial = (_selectedMaterial < _materials.size() - 1) ? _selectedMaterial + 1 : 0;
            }
            else
            {
                _selectedMaterial = (_selectedMaterial > 0) ? _selectedMaterial - 1 : _materials.size() - 1;
            }
            assignMaterial(getSelectedGeometry(), getSelectedMaterial());
            updateMaterialSelectionUI();
        }
        return true;
    }

    if (key == GLFW_KEY_U && action == GLFW_PRESS)
    {
        _window->set_visible(!_window->visible());
    }

    return false;
}

mx::ImagePtr Viewer::renderWedge()
{
    mx::MaterialPtr material = getSelectedMaterial();
    mx::ShaderPort* uniform = material ? material->findUniform(_wedgePropertyName) : nullptr;
    if (!uniform)
    {
        new ng::MessageDialog(this, ng::MessageDialog::Type::Warning, "Material property not found", _wedgePropertyName);
        return nullptr;
    }

    mx::ValuePtr origPropertyValue = uniform->getValue();
    if (origPropertyValue)
    {
        if (!origPropertyValue->isA<int>() && !origPropertyValue->isA<float>() &&
            !origPropertyValue->isA<mx::Vector2>() &&
            !origPropertyValue->isA<mx::Color3>() && !origPropertyValue->isA<mx::Vector3>() &&
            !origPropertyValue->isA<mx::Color4>() && !origPropertyValue->isA<mx::Vector4>())
        {
            new ng::MessageDialog(this, ng::MessageDialog::Type::Warning, "Material property type not supported", _wedgePropertyName);
            return nullptr;
        }

        std::vector<mx::ImagePtr> imageVec;
        float wedgePropertyStep = (_wedgePropertyMax - _wedgePropertyMin) / (_wedgeImageCount - 1);
        for (unsigned int i = 0; i < _wedgeImageCount; i++)
        {
            bool setValue = false;
            float propertyValue = (i == _wedgeImageCount - 1) ? _wedgePropertyMax : _wedgePropertyMin + wedgePropertyStep * i;
            if (origPropertyValue->isA<int>())
            {
                material->modifyUniform(_wedgePropertyName, mx::Value::createValue((int) propertyValue));
                setValue = true;
            }
            else if (origPropertyValue->isA<float>())
            {
                material->modifyUniform(_wedgePropertyName, mx::Value::createValue(propertyValue));
                setValue = true;
            }
            else if (origPropertyValue->isA<mx::Vector2>())
            {
                material->modifyUniform(_wedgePropertyName, mx::Value::createValue(mx::Vector2(propertyValue)));
                setValue = true;
            }
            else if (origPropertyValue->isA<mx::Color3>() ||
                     origPropertyValue->isA<mx::Vector3>())
            {
                material->modifyUniform(_wedgePropertyName, mx::Value::createValue(mx::Vector3(propertyValue)));
                setValue = true;
            }
            else if (origPropertyValue->isA<mx::Color4>() ||
                     origPropertyValue->isA<mx::Vector4>())
            {
                mx::Vector4 val(propertyValue, propertyValue, propertyValue, origPropertyValue->isA<mx::Color4>() ? 1.0f : propertyValue);
                material->modifyUniform(_wedgePropertyName, mx::Value::createValue(val));
                setValue = true;
            }
            if (setValue)
            {
#ifdef MATERIALXVIEW_METAL_BACKEND
                _colorTexture = metal_texture();
#endif
                if (!_geometryList.empty() && !_materialAssignments.empty())
                {
                    _renderPipeline->renderFrame(_colorTexture,
                                                 SHADOW_MAP_SIZE,
                                                 DIR_LIGHT_NODE_CATEGORY.c_str());
                }

                imageVec.push_back(_renderPipeline->getFrameImage());
            }
        }

        material->modifyUniform(_wedgePropertyName, origPropertyValue);

        return mx::createImageStrip(imageVec);
    }
    return nullptr;
}

void Viewer::renderTurnable()
{
    int frameCount = abs(_turntableSteps);

    float currentRotation = _meshRotation[1];
    _meshRotation[1] = 0.0f;
    int currentTurntableStep = _turntableStep;

    mx::FilePath turnableFileName = _captureFilename;
    const std::string extension = turnableFileName.getExtension();
    turnableFileName.removeExtension();

    for (_turntableStep = 0; _turntableStep < frameCount; _turntableStep++)
    {
        updateCameras();
        clear();
        invalidateShadowMap();
        _renderPipeline->renderFrame(_colorTexture,
                                     SHADOW_MAP_SIZE,
                                     DIR_LIGHT_NODE_CATEGORY.c_str());

        mx::ImagePtr frameImage = _renderPipeline->getFrameImage();
        if (frameImage)
        {
            std::stringstream intfmt;
            intfmt << std::setfill('0') << std::setw(4) << _turntableStep;
            std::string saveName = turnableFileName.asString() + "_" + intfmt.str() + "." + extension;
            if (_imageHandler->saveImage(saveName, frameImage, true))
            {
                std::cout << "Wrote turntable frame to file: " << saveName << std::endl;
            }
        }
    }

    _turntableStep = currentTurntableStep;
    _meshRotation[1] = currentRotation;
}

void Viewer::renderScreenSpaceQuad(mx::MaterialPtr material)
{
    if (!_quadMesh)
    {
        _quadMesh = mx::GeometryHandler::createQuadMesh();
    }
    
    material->bindMesh(_quadMesh);
    material->drawPartition(_quadMesh->getPartition(0));
}

void Viewer::draw_contents()
{
    updateCameras();

#ifndef MATERIALXVIEW_METAL_BACKEND
    mx::checkGlErrors("before viewer render");

    // Clear the screen.
    clear();
#endif

    if (_turntableEnabled && _turntableSteps)
    {
        if (!_captureRequested)
        {
            const double updateTime = 1.0 / 24.0;
            if (_turntableTimer.elapsedTime() > updateTime)
            {
                _turntableStep++;
                _turntableTimer.startTimer();
                invalidateShadowMap();
            }
        }
        else
        {
            _captureRequested = false;
            renderTurnable();
        }
    }

    // Render a wedge for the current material.
    if (_wedgeRequested)
    {
        _wedgeRequested = false;
        mx::ImagePtr wedgeImage = renderWedge();
        if (wedgeImage && _imageHandler->saveImage(_wedgeFilename, wedgeImage, true))
        {
            std::cout << "Wrote wedge to disk: " << _wedgeFilename.asString() << std::endl;
        }
        else
        {
            new ng::MessageDialog(this, ng::MessageDialog::Type::Information,
                "Failed to write wedge to disk: ", _wedgeFilename.asString());
        }
    }

    // Render the current frame.
    constexpr auto FRAME_MAX_VALUE = std::numeric_limits<decltype(_renderPipeline->_frame)>::max();
    _renderPipeline->_frame = (_renderPipeline->_frame + 1) % FRAME_MAX_VALUE;
    try
    {
        _renderPipeline->renderFrame(_colorTexture,
                                     SHADOW_MAP_SIZE,
                                     DIR_LIGHT_NODE_CATEGORY.c_str());
    }
    catch (std::exception& e)
    {
        new ng::MessageDialog(this, ng::MessageDialog::Type::Warning,
            "Failed to render frame: ", e.what());
        _materialAssignments.clear();
#ifndef MATERIALXVIEW_METAL_BACKEND
        glDisable(GL_FRAMEBUFFER_SRGB);
#endif
    }

    // Update frame timing.
    if (_frameTiming)
    {
        const double DEFAULT_SMOOTHING_BIAS = 0.9;
        double elapsedTime = _frameTimer.elapsedTime() * 1000.0;
        double bias = (_avgFrameTime > 0.0) ? DEFAULT_SMOOTHING_BIAS : 0.0;
        _avgFrameTime = bias * _avgFrameTime + (1.0 - bias) * elapsedTime;
        _timingText->set_value(std::to_string((int) _avgFrameTime));
        _frameTimer.startTimer();
    }

    // Capture the current frame.
    if (_captureRequested && !_turntableEnabled)
    {
        _captureRequested = false;
        mx::ImagePtr frameImage = _renderPipeline->getFrameImage();
        if (frameImage && _imageHandler->saveImage(_captureFilename, frameImage, true))
        {
            std::cout << "Wrote frame to disk: " << _captureFilename.asString() << std::endl;
        }
        else
        {
            new ng::MessageDialog(this, ng::MessageDialog::Type::Information,
                "Failed to write frame to disk: ", _captureFilename.asString());
        }
    }

    // Bake textures for the current material.
    if (_bakeRequested)
    {
        _bakeRequested = false;
        bakeTextures();
    }

    // Handle exit requests.
    if (_exitRequested)
    {
        ng::leave();
        set_visible(false);
    }

#ifndef MATERIALXVIEW_METAL_BACKEND
    mx::checkGlErrors("after viewer render");
#endif
}

bool Viewer::scroll_event(const ng::Vector2i& p, const ng::Vector2f& rel)
{
    if (Screen::scroll_event(p, rel))
    {
        return true;
    }

    if (_userCameraEnabled)
    {
        _cameraZoom = std::max(0.1f, _cameraZoom * ((rel.y() > 0) ? 1.1f : 0.9f));
        return true;
    }

    return false;
}

bool Viewer::mouse_motion_event(const ng::Vector2i& p,
                                const ng::Vector2i& rel,
                                int button,
                                int modifiers)
{
    if (Screen::mouse_motion_event(p, rel, button, modifiers))
    {
        return true;
    }

    mx::Vector2 pos((float) p.x(), (float) p.y());
    if (_viewCamera->applyArcballMotion(pos))
    {
        return true;
    }

    if (_userTranslationActive)
    {
        updateCameras();

        mx::Vector3 boxMin = _geometryHandler->getMinimumBounds();
        mx::Vector3 boxMax = _geometryHandler->getMaximumBounds();
        mx::Vector3 sphereCenter = (boxMax + boxMin) / 2.0f;

        float viewZ = _viewCamera->projectToViewport(sphereCenter)[2];
        mx::Vector3 pos1 = _viewCamera->unprojectFromViewport(
            mx::Vector3(pos[0], (float) m_size.y() - pos[1], viewZ));
        mx::Vector3 pos0 = _viewCamera->unprojectFromViewport(
            mx::Vector3(_userTranslationPixel[0], (float) m_size.y() - _userTranslationPixel[1], viewZ));
        mx::Vector3 translation = (pos1 - pos0);
#ifdef MATERIALXVIEW_METAL_BACKEND
        translation[1] = -translation[1];
#endif
        _userTranslation = _userTranslationStart + translation;

        return true;
    }

    return false;
}

bool Viewer::mouse_button_event(const ng::Vector2i& p, int button, bool down, int modifiers)
{
    if (Screen::mouse_button_event(p, button, down, modifiers))
    {
        return true;
    }

    mx::Vector2 pos((float) p.x(), (float) p.y());
    if (button == GLFW_MOUSE_BUTTON_1 && !modifiers)
    {
        _viewCamera->arcballButtonEvent(pos, down);
    }
    else if (button == GLFW_MOUSE_BUTTON_2 ||
            (button == GLFW_MOUSE_BUTTON_1 && modifiers == GLFW_MOD_SHIFT))
    {
        _userTranslationStart = _userTranslation;
        _userTranslationActive = true;
        _userTranslationPixel = pos;
    }
    if (button == GLFW_MOUSE_BUTTON_1 && !down)
    {
        _viewCamera->arcballButtonEvent(pos, false);
    }
    if (!down)
    {
        _userTranslationActive = false;
    }
    return true;
}

void Viewer::initCamera()
{
    _viewCamera->setViewportSize(mx::Vector2(static_cast<float>(m_size[0]), static_cast<float>(m_size[1])));

    // Disable user camera controls when non-centered views are requested.
    _userCameraEnabled = _cameraTarget == mx::Vector3(0.0) &&
                         _meshScale == 1.0f;

    if (!_userCameraEnabled || _geometryHandler->getMeshes().empty())
    {
        return;
    }

    const mx::Vector3& boxMax = _geometryHandler->getMaximumBounds();
    const mx::Vector3& boxMin = _geometryHandler->getMinimumBounds();
    mx::Vector3 sphereCenter = (boxMax + boxMin) * 0.5;

    float turntableRotation = fmod((360.0f / _turntableSteps) * _turntableStep, 360.0f);
    float yRotation = _meshRotation[1] + (_turntableEnabled ? turntableRotation : 0.0f);
    mx::Matrix44 meshRotation = mx::Matrix44::createRotationZ(_meshRotation[2] / 180.0f * PI) *
                                mx::Matrix44::createRotationY(yRotation / 180.0f * PI) *
                                mx::Matrix44::createRotationX(_meshRotation[0] / 180.0f * PI);
    _meshTranslation = -meshRotation.transformPoint(sphereCenter);
    _meshScale = IDEAL_MESH_SPHERE_RADIUS / (sphereCenter - boxMin).getMagnitude();
}

void Viewer::updateCameras()
{
#ifdef MATERIALXVIEW_METAL_BACKEND
    auto& createPerspectiveMatrix = mx::Camera::createPerspectiveMatrixZP;
    auto& createOrthographicMatrix = mx::Camera::createOrthographicMatrixZP;
#else
    auto& createPerspectiveMatrix = mx::Camera::createPerspectiveMatrix;
    auto& createOrthographicMatrix = mx::Camera::createOrthographicMatrix;
#endif
    mx::Matrix44 viewMatrix, projectionMatrix;
    float aspectRatio = (float) m_size.x() / (float) m_size.y();
    if (_cameraViewAngle != 0.0f)
    {
        viewMatrix = mx::Camera::createViewMatrix(_cameraPosition, _cameraTarget, _cameraUp);
        float fH = std::tan(_cameraViewAngle / 360.0f * PI) * _cameraNearDist;
        float fW = fH * aspectRatio;
        projectionMatrix = createPerspectiveMatrix(-fW, fW, -fH, fH, _cameraNearDist, _cameraFarDist);
    }
    else
    {
        viewMatrix = mx::Matrix44::createTranslation(mx::Vector3(0.0f, 0.0f, -ORTHO_VIEW_DISTANCE));
        float fH = ORTHO_PROJECTION_HEIGHT;
        float fW = fH * aspectRatio;
        projectionMatrix = createOrthographicMatrix(-fW, fW, -fH, fH, 0.0f, ORTHO_VIEW_DISTANCE + _cameraFarDist);
    }
#ifdef MATERIALXVIEW_METAL_BACKEND
    projectionMatrix[1][1] = -projectionMatrix[1][1];
    _colorTexture = metal_texture();
#endif
    float turntableRotation = fmod((360.0f / _turntableSteps) * _turntableStep, 360.0f);
    float yRotation = _meshRotation[1] + (_turntableEnabled ? turntableRotation : 0.0f);
    mx::Matrix44 meshRotation = mx::Matrix44::createRotationZ(_meshRotation[2] / 180.0f * PI) *
                                mx::Matrix44::createRotationY(yRotation / 180.0f * PI) *
                                mx::Matrix44::createRotationX(_meshRotation[0] / 180.0f * PI);

    mx::Matrix44 arcball = mx::Matrix44::IDENTITY;
    if (_userCameraEnabled)
    {
        arcball = _viewCamera->arcballMatrix();
    }

    _viewCamera->setWorldMatrix(meshRotation *
                                mx::Matrix44::createTranslation(_meshTranslation + _userTranslation) *
                                mx::Matrix44::createScale(mx::Vector3(_meshScale * _cameraZoom)));
    _viewCamera->setViewMatrix(arcball * viewMatrix);
    _viewCamera->setProjectionMatrix(projectionMatrix);

    _envCamera->setWorldMatrix(mx::Matrix44::createScale(mx::Vector3(300.0f)));
    _envCamera->setViewMatrix(_viewCamera->getViewMatrix());
    _envCamera->setProjectionMatrix(_viewCamera->getProjectionMatrix());

    mx::NodePtr dirLight = !_materialAssignments.empty() ? _lightHandler->getFirstLightOfCategory(DIR_LIGHT_NODE_CATEGORY) : nullptr;
    if (dirLight)
    {
        mx::Vector3 sphereCenter = (_geometryHandler->getMaximumBounds() + _geometryHandler->getMinimumBounds()) * 0.5;
        float r = (sphereCenter - _geometryHandler->getMinimumBounds()).getMagnitude();
        _shadowCamera->setWorldMatrix(meshRotation * mx::Matrix44::createTranslation(-sphereCenter));
        _shadowCamera->setProjectionMatrix(mx::Camera::createOrthographicMatrixZP(-r, r, -r, r, 0.0f, r * 2.0f));
        mx::ValuePtr value = dirLight->getInputValue("direction");
        if (value->isA<mx::Vector3>())
        {
            mx::Vector3 dir = mx::Matrix44::createRotationY(_lightRotation / 180.0f * PI).transformVector(value->asA<mx::Vector3>());
            _shadowCamera->setViewMatrix(mx::Camera::createViewMatrix(dir * -r, mx::Vector3(0.0f), _cameraUp));
        }
    }
}

void Viewer::updateDisplayedProperties()
{
    _propertyEditor.updateContents(this);
    createSaveMaterialsInterface((ng::ref<ng::Widget>) _propertyEditor.getWindow(), "Save Material");
    perform_layout();
}

mx::ImagePtr Viewer::getAmbientOcclusionImage(mx::MaterialPtr material)
{
    const mx::string AO_FILENAME_SUFFIX = "_ao";
    const mx::string AO_FILENAME_EXTENSION = "png";

    if (!material || !_genContext.getOptions().hwAmbientOcclusion)
    {
        return nullptr;
    }

    std::string aoSuffix = material->getUdim().empty() ? AO_FILENAME_SUFFIX : AO_FILENAME_SUFFIX + "_" + material->getUdim();
    mx::FilePath aoFilename = _meshFilename;
    aoFilename.removeExtension();
    aoFilename = aoFilename.asString() + aoSuffix;
    aoFilename.addExtension(AO_FILENAME_EXTENSION);
    return _imageHandler->acquireImage(aoFilename);
}

void Viewer::splitDirectLight(mx::ImagePtr envRadianceMap, mx::ImagePtr& indirectMap, mx::DocumentPtr& dirLightDoc)
{
    mx::Vector3 lightDir;
    mx::Color3 lightColor;
    mx::ImagePair imagePair = envRadianceMap->splitByLuminance(ENV_MAP_SPLIT_RADIANCE);

    mx::computeDominantLight(imagePair.second, lightDir, lightColor);
    float lightIntensity = std::max(std::max(lightColor[0], lightColor[1]), lightColor[2]);
    if (lightIntensity)
    {
        lightColor /= lightIntensity;
    }

    dirLightDoc = mx::createDocument();
    mx::NodePtr dirLightNode = dirLightDoc->addNode(DIR_LIGHT_NODE_CATEGORY, "dir_light", mx::LIGHT_SHADER_TYPE_STRING);
    dirLightNode->setInputValue("direction", lightDir);
    dirLightNode->setInputValue("color", lightColor);
    dirLightNode->setInputValue("intensity", lightIntensity);
    indirectMap = imagePair.first;
}

mx::MaterialPtr Viewer::getEnvironmentMaterial()
{
    if (!_envMaterial)
    {
        mx::FilePath envFilename = _searchPath.find(mx::FilePath("/usr/share/materialx/resources/Lights/environment_map.mtlx"));
        try
        {
            _envMaterial = _renderPipeline->createMaterial();
            _envMaterial->generateEnvironmentShader(_genContext, envFilename, _stdLib, _envRadianceFilename);
        }
        catch (std::exception& e)
        {
            std::cerr << "Failed to generate environment shader: " << e.what() << std::endl;
            _envMaterial = nullptr;
        }
    }

    return _envMaterial;
}

mx::MaterialPtr Viewer::getWireframeMaterial()
{
    if (!_wireMaterial)
    {
        try
        {
            mx::ShaderPtr hwShader = mx::createConstantShader(_genContext, _stdLib, "__WIRE_SHADER__", mx::Color3(1.0f));
            _wireMaterial = _renderPipeline->createMaterial();
            _wireMaterial->generateShader(hwShader);
        }
        catch (std::exception& e)
        {
            std::cerr << "Failed to generate wireframe shader: " << e.what() << std::endl;
            _wireMaterial = nullptr;
        }
    }

    return _wireMaterial;
}

void Viewer::invalidateShadowMap()
{
    if (_shadowMap)
    {
        _imageHandler->releaseRenderResources(_shadowMap);
        _shadowMap = nullptr;
    }
}

void Viewer::toggleTurntable(bool enable)
{
    _turntableEnabled = enable;

    if (enable)
    {
        _turntableTimer.startTimer();
    }
    else
    {
        float turntableRotation = fmod((360.0f / _turntableSteps) * _turntableStep, 360.0f);
        _meshRotation[1] = fmod(_meshRotation[1] + turntableRotation, 360.0f);
        _turntableTimer.endTimer();
    }
    invalidateShadowMap();
    _turntableStep = 0;
}

void Viewer::setShaderInterfaceType(mx::ShaderInterfaceType interfaceType)
{
    _genContext.getOptions().shaderInterfaceType = interfaceType;
#ifndef MATERIALXVIEW_METAL_BACKEND
    _genContextEssl.getOptions().shaderInterfaceType = interfaceType;
#endif
#if MATERIALX_BUILD_GEN_OSL
    _genContextOsl.getOptions().shaderInterfaceType = interfaceType;
#endif
#if MATERIALX_BUILD_GEN_MDL
    _genContextMdl.getOptions().shaderInterfaceType = interfaceType;
#endif
    reloadShaders();
    updateDisplayedProperties();
}
