// // Copyright Contributors to the MaterialX Project // SPDX-License-Identifier: Apache-2.0 // #include #include #ifdef MATERIALXVIEW_METAL_BACKEND #include #include #include #else #include #include #include #endif #include #include #include #include #include #include #include #include #if MATERIALX_BUILD_GEN_MDL #include #endif #if MATERIALX_BUILD_GEN_OSL #include #endif #include #include #include #include #include #include #include #include #include #include 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 { const int MIN_ENV_SAMPLE_COUNT = 4; const int MAX_ENV_SAMPLE_COUNT = 1024; 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); 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); } } std::vector 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(); if (node && node->getCategory() == "texcoord") { mx::InputPtr index = node->getInput("index"); mx::ValuePtr value = index ? index->getValue() : nullptr; if (value && value->isA() && value->asA() != 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, false, 4, 0), _window(nullptr), _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), _geomLabel(nullptr), _geometrySelectionBox(nullptr), _selectedMaterial(0), _materialLabel(nullptr), _materialSelectionBox(nullptr), _identityCamera(mx::Camera::create()), _viewCamera(mx::Camera::create()), _envCamera(mx::Camera::create()), _shadowCamera(mx::Camera::create()), _lightHandler(mx::LightHandler::create()), #ifndef MATERIALXVIEW_METAL_BACKEND _genContext(mx::GlslShaderGenerator::create()), _genContextEssl(mx::EsslShaderGenerator::create()), #else _genContext(mx::MslShaderGenerator::create()), #endif #if MATERIALX_BUILD_GEN_OSL _genContextOsl(mx::OslShaderGenerator::create()), #endif #if MATERIALX_BUILD_GEN_MDL _genContextMdl(mx::MdlShaderGenerator::create()), #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) { // 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(_window, "Load Mesh"); createLoadMaterialsInterface(_window, "Load Material"); createLoadEnvironmentInterface(_window, "Load Environment"); createPropertyEditorInterface(_window, "Property Editor"); createAdvancedSettings(_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 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("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(size[0]), static_cast(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 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(Widget* parent, const std::string& label) { ng::Button* meshButton = new ng::Button(parent, label); meshButton->set_icon(FA_FOLDER); 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(Widget* parent, const std::string& label) { ng::Button* materialButton = new ng::Button(parent, label); materialButton->set_icon(FA_FOLDER); 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(Widget* parent, const std::string& label) { ng::Button* envButton = new ng::Button(parent, label); envButton->set_icon(FA_FOLDER); envButton->set_callback([this]() { m_process_events = false; mx::StringSet extensions = _imageHandler->supportedExtensions(); std::vector> 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(Widget* parent, const std::string& label) { ng::Button* materialButton = new ng::Button(parent, label); materialButton->set_icon(FA_SAVE); 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(Widget* parent, const std::string& label) { ng::Button* editorButton = new ng::Button(parent, label); editorButton->set_flags(ng::Button::ToggleButton); editorButton->set_change_callback([this](bool state) { _propertyEditor.setVisible(state); perform_layout(); }); } void Viewer::createAdvancedSettings(Widget* parent) { ng::PopupButton* advancedButton = new ng::PopupButton(parent, "Advanced Settings"); advancedButton->set_icon(FA_TOOLS); advancedButton->set_chevron_icon(-1); ng::Popup* advancedPopupParent = advancedButton->popup(); advancedPopupParent->set_layout(new ng::GroupLayout()); ng::VScrollPanel* scrollPanel = new ng::VScrollPanel(advancedPopupParent); scrollPanel->set_fixed_height(500); ng::Widget* advancedPopup = new ng::Widget(scrollPanel); advancedPopup->set_layout(new ng::GroupLayout(13)); ng::Label* viewLabel = new ng::Label(advancedPopup, "Viewing Options"); viewLabel->set_font_size(20); viewLabel->set_font("sans-bold"); ng::CheckBox* drawEnvironmentBox = new ng::CheckBox(advancedPopup, "Draw Environment"); drawEnvironmentBox->set_checked(_drawEnvironment); drawEnvironmentBox->set_callback([this](bool enable) { _drawEnvironment = enable; }); ng::CheckBox* outlineSelectedGeometryBox = new ng::CheckBox(advancedPopup, "Outline Selected Geometry"); outlineSelectedGeometryBox->set_checked(_outlineSelection); outlineSelectedGeometryBox->set_callback([this](bool enable) { _outlineSelection = enable; }); ng::Label* renderLabel = new ng::Label(advancedPopup, "Render Options"); renderLabel->set_font_size(20); renderLabel->set_font("sans-bold"); ng::CheckBox* transparencyBox = new ng::CheckBox(advancedPopup, "Render Transparency"); transparencyBox->set_checked(_renderTransparency); transparencyBox->set_callback([this](bool enable) { _renderTransparency = enable; }); ng::CheckBox* doubleSidedBox = new ng::CheckBox(advancedPopup, "Render Double-Sided"); doubleSidedBox->set_checked(_renderDoubleSided); doubleSidedBox->set_callback([this](bool enable) { _renderDoubleSided = enable; }); ng::CheckBox* importanceSampleBox = new ng::CheckBox(advancedPopup, "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::CheckBox* refractionBox = new ng::CheckBox(advancedPopup, "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::CheckBox* refractionSidedBox = new ng::CheckBox(advancedPopup, "Refraction Two-Sided"); refractionSidedBox->set_checked(_lightHandler->getRefractionTwoSided()); refractionSidedBox->set_callback([this](bool enable) { _lightHandler->setRefractionTwoSided(enable); }); ng::CheckBox* shaderInterfaceBox = new ng::CheckBox(advancedPopup, "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); }); Widget* albedoGroup = new Widget(advancedPopup); albedoGroup->set_layout(new ng::BoxLayout(ng::Orientation::Horizontal)); new ng::Label(albedoGroup, "Albedo Method:"); mx::StringVec albedoOptions = { "Analytic", "Table", "MC" }; 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(); } }); Widget* sampleGroup = new Widget(advancedPopup); 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::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::Label* lightingLabel = new ng::Label(advancedPopup, "Lighting Options"); lightingLabel->set_font_size(20); lightingLabel->set_font("sans-bold"); ng::CheckBox* directLightingBox = new ng::CheckBox(advancedPopup, "Direct Lighting"); directLightingBox->set_checked(_lightHandler->getDirectLighting()); directLightingBox->set_callback([this](bool enable) { _lightHandler->setDirectLighting(enable); }); ng::CheckBox* indirectLightingBox = new ng::CheckBox(advancedPopup, "Indirect Lighting"); indirectLightingBox->set_checked(_lightHandler->getIndirectLighting()); indirectLightingBox->set_callback([this](bool enable) { _lightHandler->setIndirectLighting(enable); }); ng::Widget* lightRotationRow = new ng::Widget(advancedPopup); 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::FloatBox* lightRotationBox = createFloatWidget(lightRotationRow, "Light Rotation:", _lightRotation, &ui, [this](float value) { _lightRotation = value; invalidateShadowMap(); }); lightRotationBox->set_editable(true); ng::Label* shadowingLabel = new ng::Label(advancedPopup, "Shadowing Options"); shadowingLabel->set_font_size(20); shadowingLabel->set_font("sans-bold"); ng::CheckBox* shadowMapBox = new ng::CheckBox(advancedPopup, "Shadow Map"); shadowMapBox->set_checked(_genContext.getOptions().hwShadowMap); shadowMapBox->set_callback([this](bool enable) { _genContext.getOptions().hwShadowMap = enable; reloadShaders(); }); ng::CheckBox* ambientOcclusionBox = new ng::CheckBox(advancedPopup, "Ambient Occlusion"); ambientOcclusionBox->set_checked(_genContext.getOptions().hwAmbientOcclusion); ambientOcclusionBox->set_callback([this](bool enable) { _genContext.getOptions().hwAmbientOcclusion = enable; reloadShaders(); }); ng::Widget* ambientOcclusionGainRow = new ng::Widget(advancedPopup); ambientOcclusionGainRow->set_layout(new ng::BoxLayout(ng::Orientation::Horizontal)); ng::FloatBox* ambientOcclusionGainBox = createFloatWidget(ambientOcclusionGainRow, "AO Gain:", _ambientOcclusionGain, nullptr, [this](float value) { _ambientOcclusionGain = value; }); ambientOcclusionGainBox->set_editable(true); ng::Label* sceneLabel = new ng::Label(advancedPopup, "Scene Options"); sceneLabel->set_font_size(20); sceneLabel->set_font("sans-bold"); Widget* unitGroup = new Widget(advancedPopup); unitGroup->set_layout(new ng::BoxLayout(ng::Orientation::Horizontal)); new ng::Label(unitGroup, "Distance Unit:"); 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::Label* meshLoading = new ng::Label(advancedPopup, "Mesh Loading Options"); meshLoading->set_font_size(20); meshLoading->set_font("sans-bold"); ng::CheckBox* splitUdimsBox = new ng::CheckBox(advancedPopup, "Split By UDIMs"); splitUdimsBox->set_checked(_splitByUdims); splitUdimsBox->set_callback([this](bool enable) { _splitByUdims = enable; }); ng::Label* materialLoading = new ng::Label(advancedPopup, "Material Loading Options"); materialLoading->set_font_size(20); materialLoading->set_font("sans-bold"); ng::CheckBox* mergeMaterialsBox = new ng::CheckBox(advancedPopup, "Merge Materials"); mergeMaterialsBox->set_checked(_mergeMaterials); mergeMaterialsBox->set_callback([this](bool enable) { _mergeMaterials = enable; }); ng::CheckBox* showInputsBox = new ng::CheckBox(advancedPopup, "Show All Inputs"); showInputsBox->set_checked(_showAllInputs); showInputsBox->set_callback([this](bool enable) { _showAllInputs = enable; }); ng::CheckBox* flattenBox = new ng::CheckBox(advancedPopup, "Flatten Subgraphs"); flattenBox->set_checked(_flattenSubgraphs); flattenBox->set_callback([this](bool enable) { _flattenSubgraphs = enable; }); ng::Label* envLoading = new ng::Label(advancedPopup, "Environment Loading Options"); envLoading->set_font_size(20); envLoading->set_font("sans-bold"); ng::CheckBox* normalizeEnvBox = new ng::CheckBox(advancedPopup, "Normalize Environment"); normalizeEnvBox->set_checked(_normalizeEnvironment); normalizeEnvBox->set_callback([this](bool enable) { _normalizeEnvironment = enable; }); ng::CheckBox* splitDirectLightBox = new ng::CheckBox(advancedPopup, "Split Direct Light"); splitDirectLightBox->set_checked(_splitDirectLight); splitDirectLightBox->set_callback([this](bool enable) { _splitDirectLight = enable; }); ng::Label* translationLabel = new ng::Label(advancedPopup, "Translation Options (T)"); translationLabel->set_font_size(20); translationLabel->set_font("sans-bold"); ng::Widget* targetShaderGroup = new ng::Widget(advancedPopup); targetShaderGroup->set_layout(new ng::BoxLayout(ng::Orientation::Horizontal)); new ng::Label(targetShaderGroup, "Target Shader"); 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::Label* textureLabel = new ng::Label(advancedPopup, "Texture Baking Options (B)"); textureLabel->set_font_size(20); textureLabel->set_font("sans-bold"); ng::CheckBox* bakeHdrBox = new ng::CheckBox(advancedPopup, "Bake HDR Textures"); bakeHdrBox->set_checked(_bakeHdr); bakeHdrBox->set_callback([this](bool enable) { _bakeHdr = enable; }); ng::CheckBox* bakeAverageBox = new ng::CheckBox(advancedPopup, "Bake Averaged Textures"); bakeAverageBox->set_checked(_bakeAverage); bakeAverageBox->set_callback([this](bool enable) { _bakeAverage = enable; }); ng::CheckBox* bakeOptimized = new ng::CheckBox(advancedPopup, "Optimize Baked Constants"); bakeOptimized->set_checked(_bakeOptimize); bakeOptimized->set_callback([this](bool enable) { _bakeOptimize = enable; }); ng::CheckBox* bakeDocumentPerMaterial= new ng::CheckBox(advancedPopup, "Bake Document Per Material"); bakeDocumentPerMaterial->set_checked(_bakeDocumentPerMaterial); bakeDocumentPerMaterial->set_callback([this](bool enable) { _bakeDocumentPerMaterial = enable; }); ng::Label* wedgeLabel = new ng::Label(advancedPopup, "Wedge Render Options (W)"); wedgeLabel->set_font_size(20); wedgeLabel->set_font("sans-bold"); ng::Widget* wedgeNameGroup = new ng::Widget(advancedPopup); wedgeNameGroup->set_layout(new ng::BoxLayout(ng::Orientation::Horizontal)); new ng::Label(wedgeNameGroup, "Property Name"); 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::Widget* wedgeMinGroup = new ng::Widget(advancedPopup); 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::FloatBox* wedgeMinBox = createFloatWidget(wedgeMinGroup, "Property Min:", _wedgePropertyMax, &wedgeProp, [this](float value) { _wedgePropertyMin = value; }); wedgeMinBox->set_value(0.0); wedgeMinBox->set_editable(true); ng::Widget* wedgeMaxGroup = new ng::Widget(advancedPopup); wedgeMaxGroup->set_layout(new ng::BoxLayout(ng::Orientation::Horizontal)); ng::FloatBox* wedgeMaxBox = createFloatWidget(wedgeMaxGroup, "Property Max:", _wedgePropertyMax, &wedgeProp, [this](float value) { _wedgePropertyMax = value; }); wedgeMaxBox->set_value(1.0); wedgeMaxBox->set_editable(true); ng::Widget* wedgeCountGroup = new ng::Widget(advancedPopup); 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::IntBox* wedgeCountBox = createIntWidget(wedgeCountGroup, "Image Count:", _wedgeImageCount, &wedgeCountProp, [this](int value) { _wedgeImageCount = value; }); wedgeCountBox->set_value(8); wedgeCountBox->set_editable(true); } 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 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 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 newMaterials; try { // Load source document. mx::DocumentPtr doc = mx::createDocument(); mx::readFromXmlFile(doc, filename, _searchPath, &readOptions); _materialSearchPath = mx::getSourceSearchPath(doc); // Import libraries. doc->importLibrary(libraries); // Apply direct lights. applyDirectLights(doc); // Apply modifiers to the content document. applyModifiers(doc, _modifiers); // 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 elems = mx::findRenderableElements(doc); if (elems.empty()) { throw mx::Exception("No renderable elements found in " + _materialFilename.getBaseName()); } std::vector materialNodes; for (mx::TypedElementPtr elem : elems) { mx::TypedElementPtr renderableElem = elem; mx::NodePtr node = elem->asA(); 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; igetDescendant(renderablePath); mx::TypedElementPtr typedElem = elem ? elem->asA() : nullptr; if (!typedElem) { continue; } if (udimSetValue && udimSetValue->isA()) { for (const std::string& udim : udimSetValue->asA()) { 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); // Add new materials to the global vector. _materials.insert(_materials.end(), newMaterials.begin(), newMaterials.end()); mx::MaterialPtr udimMaterial = nullptr; for (mx::MaterialPtr mat : newMaterials) { // Clear cached implementations, in case libraries on the file system have changed. _genContext.clearNodeImplementations(); #ifndef MATERIALXVIEW_METAL_BACKEND _genContextEssl.clearNodeImplementations(); #endif 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 implicit udim assignments, if any. for (mx::MaterialPtr mat : newMaterials) { mx::NodePtr materialNode = mat->getMaterialNode(); if (materialNode) { std::string udim = mat->getUdim(); 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(); if (shaderNode) { mx::FilePath baseFilename = getBaseOutputPath(); for (mx::InputPtr input : shaderNode->getInputs()) { mx::OutputPtr output = input->getConnectedOutput(); mx::ConstNodeGraphPtr nodeGraph = output ? output->getAncestorOfType() : 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() : 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::DefaultColorManagementSystemPtr cms = mx::DefaultColorManagementSystem::create(context.getShaderGenerator().getTarget()); 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 (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() && !origPropertyValue->isA() && !origPropertyValue->isA() && !origPropertyValue->isA() && !origPropertyValue->isA() && !origPropertyValue->isA() && !origPropertyValue->isA()) { new ng::MessageDialog(this, ng::MessageDialog::Type::Warning, "Material property type not supported", _wedgePropertyName); return nullptr; } std::vector 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()) { material->modifyUniform(_wedgePropertyName, mx::Value::createValue((int) propertyValue)); setValue = true; } else if (origPropertyValue->isA()) { material->modifyUniform(_wedgePropertyName, mx::Value::createValue(propertyValue)); setValue = true; } else if (origPropertyValue->isA()) { material->modifyUniform(_wedgePropertyName, mx::Value::createValue(mx::Vector2(propertyValue))); setValue = true; } else if (origPropertyValue->isA() || origPropertyValue->isA()) { material->modifyUniform(_wedgePropertyName, mx::Value::createValue(mx::Vector3(propertyValue))); setValue = true; } else if (origPropertyValue->isA() || origPropertyValue->isA()) { mx::Vector4 val(propertyValue, propertyValue, propertyValue, origPropertyValue->isA() ? 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. 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 } // 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(m_size[0]), static_cast(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 = _lightHandler->getFirstLightOfCategory(DIR_LIGHT_NODE_CATEGORY); 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 dir = mx::Matrix44::createRotationY(_lightRotation / 180.0f * PI).transformVector(value->asA()); _shadowCamera->setViewMatrix(mx::Camera::createViewMatrix(dir * -r, mx::Vector3(0.0f), _cameraUp)); } } } void Viewer::updateDisplayedProperties() { _propertyEditor.updateContents(this); createSaveMaterialsInterface(_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("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(); }