2441 lines
85 KiB
C++
2441 lines
85 KiB
C++
//
|
|
// 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>
|
|
|
|
#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>
|
|
|
|
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<mx::ElementPtr> 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, 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<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(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<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(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<float>* 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<float>* 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<float>* 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<float>* 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<int>* 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<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);
|
|
|
|
// 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<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);
|
|
|
|
// 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<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::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<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.
|
|
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<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 = _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>())
|
|
{
|
|
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(_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();
|
|
}
|