Automatic material casters with glTF in C++
Disclaimer: The code in this post is written on version 9.1 of Simplygon. If you encounter this post at a later stage, some of the API calls might have changed. However, the core concepts should still remain valid.
Introduction
If your asset pipeline holds different material setups for different assets, it can be useful to know how to make Simplygon cast materials correctly without having to know the exact setup for each asset. In this post we will guide you through a way of detecting and applying the correct material casters based on the input materials of the loaded scene.
Please note that this guide assumes that all channels you are interested in casting are channels with textures and the asset being a glTF file. Channels with constant colors, vertex colors or other types are not taken into account here. And other file formats are likely to require different options.
The asset
The original asset we will be using can be seen below. It is a GLB file that consists of a few materials of which one has transparency. So we need to take this into account when setting up the output material later on.
The story
The program we will write here will take a path to an asset as command-line argument, process that asset and write out the processed asset next to the original asset. The format we are working with is glTF as mentioned, and since different file formats have different material setups, it is likely that you would have to modify the code in this example if you want it to work with a different file format than glTF. Apart from main()
we will set up three additional methods that will help us get the task done:
SetupMappingImage()
- Sets up the mapping image settings for the material castersAddMaterialCasterFromChannel()
- Add the appropriate caster for given channel (will be called fromAddDefaultCasters()
)AddDefaultCasters()
- Finds all the material channels with textures connected to them and creates the appropriate caster for the channel
The main() method
Let's start with the main part of the code that initializes Simplygon, reads the scene and sets up the ReductionPipeline object that will do the processing:
#include <Simplygon.h>
#include <SimplygonLoader.h>
#include <iostream>
#include <set>
#include <string>
#include <filesystem>
using namespace Simplygon;
ISimplygon* sg = nullptr;
spScene scene = nullptr;
int main( int argc, char* argv[] )
{
EErrorCodes errorCode = Initialize( &sg );
if( errorCode == EErrorCodes::NoError )
{
// Read glTF file
spSceneImporter importer = sg->CreateSceneImporter();
importer->SetImportFilePath( argv[ 1 ] );
if( importer->RunImport() )
{
scene = importer->GetScene();
spReductionPipeline reductionPipeline = sg->CreateReductionPipeline();
}
}
}
Setting up input and output material information
Once the reduction pipeline is set up, we can start to look at setting up the mapping image settings for Simplygon. We need to provide some general settings that tells Simplygon how many input materials there are and how many output materials we want and some general mapping image settings (more info in the documentation for each setting respectively, as I will not go into the details of those here). To be able to set the correct number of input materials, we can get the number of input materials from the loaded scene, and we want two output materials - one material for input materials without transparency and one which will use alpha blend mode to which we will cast the input materials that has transparency.
// Sets up the mapping image settings for the material casters
void SetupMappingImage( spReductionPipeline pipeline )
{
spMappingImageSettings mappingImageSettings = pipeline->GetMappingImageSettings();
// We want two output materials to properly support transparency
mappingImageSettings->SetInputMaterialCount( scene->GetMaterialTable()->GetMaterialsCount() );
mappingImageSettings->SetOutputMaterialCount( 2 );
// Set some general mapping image settings
mappingImageSettings->SetGenerateMappingImage( true );
mappingImageSettings->SetApplyNewMaterialIds( true );
mappingImageSettings->SetGenerateTangents( true );
mappingImageSettings->SetUseFullRetexturing( true );
mappingImageSettings->SetTexCoordGeneratorType( ETexcoordGeneratorType::ChartAggregator );
// Enable the chart aggregator and reuse UV space.
spChartAggregatorSettings chartAggregatorSettings = mappingImageSettings->GetChartAggregatorSettings();
chartAggregatorSettings->SetChartAggregatorMode( EChartAggregatorMode::SurfaceArea );
chartAggregatorSettings->SetSeparateOverlappingCharts( false );
// Setting the size of the output materials for the mapping image.
// This will be the output size of the textures when we do material
// casting in a later stage.
spMappingImageOutputMaterialSettings firstMaterialSettings = mappingImageSettings->GetOutputMaterialSettings( 0 );
firstMaterialSettings->SetTextureWidth( 2048 );
firstMaterialSettings->SetTextureHeight( 2048 );
spMappingImageOutputMaterialSettings secondMaterialSettings = mappingImageSettings->GetOutputMaterialSettings( 1 );
secondMaterialSettings->SetTextureWidth( 2048 );
secondMaterialSettings->SetTextureHeight( 2048 );
}
To detect and add the correct material casters
Let's bring in the code for adding the correct material caster to use depending on input channel name. It's a fairly straight forward process as long as the channel names are matching that of Simplygon's and glTF's standard names. If you are using channel names different to this in your assets, you will have to modify this part. So, let's define a method AddMaterialCasterFromChannel()
which takes three arguments: the pipeline to add the material caster to, the channel name and finally what output material ID that it should go into (0 for the opaque material and 1 for the material with blend mode Blend
that supports transparency):
// Add the appropriate caster for given channel
void AddMaterialCasterFromChannel( spReductionPipeline pipeline, std::string channel, int outputMaterialId )
{
spMaterialCaster caster;
spMaterialCasterSettings casterSettings;
std::string casterType = "color";
if( channel == SG_MATERIAL_CHANNEL_NORMALS )
{
caster = sg->CreateNormalCaster();
casterSettings = spNormalCaster::SafeCast( caster )->GetNormalCasterSettings();
spNormalCasterSettings::SafeCast( casterSettings )->SetGenerateTangentSpaceNormals( true );
casterType = "normal";
}
else if( channel == SG_MATERIAL_CHANNEL_OPACITY )
{
caster = sg->CreateOpacityCaster();
casterSettings = spOpacityCaster::SafeCast( caster )->GetOpacityCasterSettings();
casterType = "opacity";
}
else if( channel == SG_MATERIAL_CHANNEL_DIFFUSE || channel == SG_MATERIAL_CHANNEL_BASECOLOR )
{
caster = sg->CreateColorCaster();
casterSettings = spColorCaster::SafeCast( caster )->GetColorCasterSettings();
// GLB files stores opacity in the alpha channel of the color map.
casterSettings->SetOpacityChannelComponent( EColorComponent::Alpha );
}
else
{
// For all other channels we'll just add a straight forward
// pixel to pixel color caster.
caster = sg->CreateColorCaster();
casterSettings = spColorCaster::SafeCast( caster )->GetColorCasterSettings();
}
std::string opaqueTransparent = outputMaterialId == 1 ? "transparent" : "opaque";
printf("Adding %s caster for %s material channel: %s\n", casterType.c_str(), opaqueTransparent.c_str(), channel.c_str());
casterSettings->SetMaterialChannel( channel.c_str() );
pipeline->AddMaterialCaster( caster, outputMaterialId );
}
Simplygon provides a few different material casters, and in this case we are setting up an OpacityCaster
for the opacity channel, a NormalCaster
for the normals channel, a ColorCaster
for the base color/diffuse channel and a ColorCaster
for all other channels that we have not explicitly defined. The SafeCast()
method allows us to safely cast the parent class MaterialCaster
to any of the inherited caster types so that we can add it to the pipeline in this fashion.
Input → output material mapping and the detection part
To successfully map input → output materials, we need to tell the mapping image settings what output material a specific input material should map to. We also want to loop over all texture channels which have textures, and Simplygon has a neat function called GetMaterialChannelsWithTextureInputs()
- so let's use that one. And we want a list of unique channel names that exists for both opaque and transparent materials so we only set up the casters for each channel once per output material. We will use a std::set
for that purpose in this example. So, two sets of unique (textured) channel names, one for each output material - and once we have that, we can loop over those channel names and call the AddMaterialCasterFromChannel()
method we just wrote above to actually add the casters to each output material respectively. Let's dig into the code:
// Finds all the material channels with textures connected to them
// and creates the appropriate caster for the channel.
void AddDefaultCasters( spReductionPipeline pipeline )
{
spMaterialTable materials = scene->GetMaterialTable();
spMappingImageSettings mappingImageSettings = pipeline->GetMappingImageSettings();
// Loop over all input materials to find out which channels
// are used in each output material (opaque or transparent)
std::set<std::string> opaqueChannels{};
std::set<std::string> transparentChannels{};
for( int m = 0; m < materials->GetMaterialsCount(); m++ )
{
spMaterial inputMaterial = materials->GetMaterial( m );
// Set the mapping of the input material to the correct output material
// depending on if it's opaque or transparent
int outputMaterialId = 0;
if( inputMaterial->GetBlendMode() == EMaterialBlendMode::Blend )
outputMaterialId = 1;
mappingImageSettings->GetInputMaterialSettings( m )->SetMaterialMapping( outputMaterialId );
// Loop over all texture channels which have textures and add them to
// the correct set depending on if the input material is opaque or transparent
spStringArray textureChannels = sg->CreateStringArray();
inputMaterial->GetMaterialChannelsWithTextureInputs( textureChannels );
for( int c = 0; c < textureChannels->GetItemCount(); c++ )
{
if( inputMaterial->GetBlendMode() == EMaterialBlendMode::Blend )
transparentChannels.insert( textureChannels->GetItem( c ).Data() );
else
opaqueChannels.insert( textureChannels->GetItem( c ).Data() );
}
}
// Add material casters for selected channels
for( std::string channelName : transparentChannels )
AddMaterialCasterFromChannel( pipeline, channelName, 1 );
for( std::string channelName : opaqueChannels )
AddMaterialCasterFromChannel( pipeline, channelName, 0 );
}
Output material blend mode and export
So, to tie everything together we need to add a few extra lines to the code of main()
that we started on before. We want to run the SetupMappingImage()
and AddDefaultCasters()
methods we have defined, run the actual processing, set the blend mode to Blend
on output material with ID 1 and finally do the export of the processed scene to file. So let's add this after creating the pipeline
object:
SetupMappingImage( reductionPipeline );
AddDefaultCasters( reductionPipeline );
// Set reduction target to triangle ratio with a ratio of 10%.
spReductionSettings reductionSettings = reductionPipeline->GetReductionSettings();
reductionSettings->SetReductionTargets( EStopCondition::All, true, false, false, false );
reductionSettings->SetReductionTargetTriangleRatio( 0.1f );
// Run the processing
reductionPipeline->RunScene( scene, EPipelineRunMode::RunInThisProcess );
// Set blend mode of the output material that should have transparency
spMaterial outputMaterial = scene->GetMaterialTable()->GetMaterial( 1 );
outputMaterial->SetBlendMode( EMaterialBlendMode::Blend );
// Save out the processed scene as GLB
spSceneExporter exporter = sg->CreateSceneExporter();
std::filesystem::path outputFilePath = argv[ 1 ];
outputFilePath = outputFilePath.replace_filename( outputFilePath.stem().string() + "_output.glb" );
exporter->SetExportFilePath( outputFilePath.string().c_str() );
exporter->SetScene( reductionPipeline->GetProcessedScene() );
if( exporter->RunExport() )
printf( "Saved output to: %s", outputFilePath.string().c_str() );
The result
The complete program
#include <Simplygon.h>
#include <SimplygonLoader.h>
#include <iostream>
#include <set>
#include <filesystem>
using namespace Simplygon;
ISimplygon* sg = nullptr;
spScene scene = nullptr;
// Sets up the mapping image settings for the material casters
void SetupMappingImage( spReductionPipeline pipeline )
{
spMappingImageSettings mappingImageSettings = pipeline->GetMappingImageSettings();
// We want two output materials to properly support transparency
mappingImageSettings->SetInputMaterialCount( scene->GetMaterialTable()->GetMaterialsCount() );
mappingImageSettings->SetOutputMaterialCount( 2 );
// Set some general mapping image settings
mappingImageSettings->SetGenerateMappingImage( true );
mappingImageSettings->SetApplyNewMaterialIds( true );
mappingImageSettings->SetGenerateTangents( true );
mappingImageSettings->SetUseFullRetexturing( true );
mappingImageSettings->SetTexCoordGeneratorType( ETexcoordGeneratorType::ChartAggregator );
// Enable the chart aggregator and reuse UV space.
spChartAggregatorSettings chartAggregatorSettings = mappingImageSettings->GetChartAggregatorSettings();
chartAggregatorSettings->SetChartAggregatorMode( EChartAggregatorMode::SurfaceArea );
chartAggregatorSettings->SetSeparateOverlappingCharts( false );
// Setting the size of the output materials for the mapping image.
// This will be the output size of the textures when we do material
// casting in a later stage.
spMappingImageOutputMaterialSettings firstMaterialSettings = mappingImageSettings->GetOutputMaterialSettings( 0 );
firstMaterialSettings->SetTextureWidth( 2048 );
firstMaterialSettings->SetTextureHeight( 2048 );
spMappingImageOutputMaterialSettings secondMaterialSettings = mappingImageSettings->GetOutputMaterialSettings( 1 );
secondMaterialSettings->SetTextureWidth( 2048 );
secondMaterialSettings->SetTextureHeight( 2048 );
}
// Add the appropriate caster for given channel
void AddMaterialCasterFromChannel( spReductionPipeline pipeline, std::string channel, int outputMaterialId )
{
spMaterialCaster caster;
spMaterialCasterSettings casterSettings;
std::string casterType = "color";
if( channel == SG_MATERIAL_CHANNEL_NORMALS )
{
caster = sg->CreateNormalCaster();
casterSettings = spNormalCaster::SafeCast( caster )->GetNormalCasterSettings();
spNormalCasterSettings::SafeCast( casterSettings )->SetGenerateTangentSpaceNormals( true );
casterType = "normal";
}
else if( channel == SG_MATERIAL_CHANNEL_OPACITY )
{
caster = sg->CreateOpacityCaster();
casterSettings = spOpacityCaster::SafeCast( caster )->GetOpacityCasterSettings();
casterType = "opacity";
}
else if( channel == SG_MATERIAL_CHANNEL_DIFFUSE || channel == SG_MATERIAL_CHANNEL_BASECOLOR )
{
caster = sg->CreateColorCaster();
casterSettings = spColorCaster::SafeCast( caster )->GetColorCasterSettings();
// GLB files stores opacity in the alpha channel of the color map.
casterSettings->SetOpacityChannelComponent( EColorComponent::Alpha );
}
else
{
// For all other channels we'll just add a straight forward
// pixel to pixel color caster.
caster = sg->CreateColorCaster();
casterSettings = spColorCaster::SafeCast( caster )->GetColorCasterSettings();
}
std::string opaqueTransparent = outputMaterialId == 1 ? "transparent" : "opaque";
printf("Adding %s caster for %s material channel: %s\n", casterType.c_str(), opaqueTransparent.c_str(), channel.c_str());
casterSettings->SetMaterialChannel( channel.c_str() );
pipeline->AddMaterialCaster( caster, outputMaterialId );
}
// Finds all the material channels with textures connected to them
// and creates the appropriate caster for the channel.
void AddDefaultCasters( spReductionPipeline pipeline )
{
spMaterialTable materials = scene->GetMaterialTable();
spMappingImageSettings mappingImageSettings = pipeline->GetMappingImageSettings();
// Loop over all input materials to find out which channels
// are used in each output material (opaque or transparent)
std::set<std::string> opaqueChannels{};
std::set<std::string> transparentChannels{};
for( int m = 0; m < materials->GetMaterialsCount(); m++ )
{
spMaterial inputMaterial = materials->GetMaterial( m );
// Set the mapping of the input material to the correct output material
// depending on if it's opaque or transparent
int outputMaterialId = 0;
if( inputMaterial->GetBlendMode() == EMaterialBlendMode::Blend )
outputMaterialId = 1;
mappingImageSettings->GetInputMaterialSettings( m )->SetMaterialMapping( outputMaterialId );
// Loop over all texture channels which have textures and add them to
// the correct set depending on if the input material is opaque or transparent
spStringArray textureChannels = sg->CreateStringArray();
inputMaterial->GetMaterialChannelsWithTextureInputs( textureChannels );
for( int c = 0; c < textureChannels->GetItemCount(); c++ )
{
if( inputMaterial->GetBlendMode() == EMaterialBlendMode::Blend )
transparentChannels.insert( textureChannels->GetItem( c ).Data() );
else
opaqueChannels.insert( textureChannels->GetItem( c ).Data() );
}
}
// Add material casters for selected channels
for( std::string channelName : transparentChannels )
AddMaterialCasterFromChannel( pipeline, channelName, 1 );
for( std::string channelName : opaqueChannels )
AddMaterialCasterFromChannel( pipeline, channelName, 0 );
}
int main( int argc, char* argv[] )
{
EErrorCodes errorCode = Initialize( &sg );
if( errorCode == EErrorCodes::NoError )
{
// Read glTF file
spSceneImporter importer = sg->CreateSceneImporter();
importer->SetImportFilePath( argv[ 1 ] );
if( importer->RunImport() )
{
scene = importer->GetScene();
spReductionPipeline reductionPipeline = sg->CreateReductionPipeline();
SetupMappingImage( reductionPipeline );
AddDefaultCasters( reductionPipeline );
// Set reduction target to triangle ratio with a ratio of 10%.
spReductionSettings reductionSettings = reductionPipeline->GetReductionSettings();
reductionSettings->SetReductionTargets( EStopCondition::All, true, false, false, false );
reductionSettings->SetReductionTargetTriangleRatio( 0.1f );
// Run the processing
reductionPipeline->RunScene( scene, EPipelineRunMode::RunInThisProcess );
// Set blend mode of the output material that should have transparency
spMaterial outputMaterial = scene->GetMaterialTable()->GetMaterial( 1 );
outputMaterial->SetBlendMode( EMaterialBlendMode::Blend );
// Save out the processed scene as GLB
spSceneExporter exporter = sg->CreateSceneExporter();
std::filesystem::path outputFilePath = argv[ 1 ];
outputFilePath = outputFilePath.replace_filename( outputFilePath.stem().string() + "_output.glb" );
exporter->SetExportFilePath( outputFilePath.string().c_str() );
exporter->SetScene( reductionPipeline->GetProcessedScene() );
if( exporter->RunExport() )
printf( "Saved output to: %s", outputFilePath.string().c_str() );
}
}
}