Using custom shaders in Unity
Disclaimer: The code in this post is written using version 9.1.20400 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
Sometimes the built-in shaders in Unity is not enough. In this post we’ll demonstrate how you can optimize shaders using a material caster with a custom shading network.
Prerequisites
This example will utilize a scene import / export that is part of the Simplygon Unity- plug-in, while this is not required it is recommended to get the most consistent results compared to the plug-in. Other options are; write your own scene importer / exporter (Unity ↔ Simplygon), or use Unity USD directly (Unity ↔ USD ↔ Simplygon). Please follow the installation instructions to make sure both the Simplygon Unity plug-in and Unity USD 2.0.0 are installed properly.
The asset
In this example we’ll create a shader that combines two texture layers using the vertex color as an interpolation factor. The surface shader in Unity looks like this:
void surf (Input IN, inout SurfaceOutputStandard o)
{
float4 layer1 = tex2D(_Layer1, IN.uv_Layer1);
float4 layer2 = tex2D(_Layer2, IN.uv_Layer2);
float4 c = lerp(layer1, layer2, IN.color);
o.Albedo = c.rgb;
o.Alpha = c.a;
}
The result applied to a basic plane looks like this:
Material casting script
When running a material casting with a custom shading network you need to access the Simplygon API directly using a script. In this case we create a C# script that runs inside Unity.
First we need to export the selected gameobject to Simplygon using the built in Export method in the Simplygon plugin for Unity.
using (spScene sgScene = SimplygonExporter.Export(simplygon, exportTempDirectory, selectedGameObjects))
Then we add the extra textures used by our custom shader to the Simplygon scene.
Texture layer1 = meshRenderer.sharedMaterial.GetTexture("_Layer1");
Texture layer2 = meshRenderer.sharedMaterial.GetTexture("_Layer2");
string layer1Path = AssetDatabase.GetAssetPath(layer1);
string layer2Path = AssetDatabase.GetAssetPath(layer2);
using (spTexture sgLayer1Texture = simplygon.CreateTexture())
using (spTexture sgLayer2Texture = simplygon.CreateTexture())
{
sgLayer1Texture.SetFilePath(layer1Path);
sgLayer2Texture.SetFilePath(layer2Path);
sgLayer1Texture.SetName("Layer1");
sgLayer2Texture.SetName("Layer2");
sgTextureTable.AddTexture(sgLayer1Texture);
sgTextureTable.AddTexture(sgLayer2Texture);
}
To run the material casting we need to create a pipeline and a color caster. In this case we create a basic reduction pipeline.
Material casting is available for other processing pipelines like aggregation and remeshing and the same concept applies there.
using (spReductionPipeline sgReductionPipeline = simplygon.CreateReductionPipeline())
using (spColorCaster sgCombinedLayerColorCaster = simplygon.CreateColorCaster())
Now we can finally create the shading network where we replicate the surface shader from the custom shader as a Simplygon shading network.
using (spColorCaster sgCombinedLayerColorCaster = simplygon.CreateColorCaster())
using (spShadingVertexColorNode sgShadingVertexColorNode = simplygon.CreateShadingVertexColorNode())
using (spShadingInterpolateNode sgShadingInterpolationNode = simplygon.CreateShadingInterpolateNode())
using (spShadingTextureNode sgShadingLayer1TextureNode = simplygon.CreateShadingTextureNode())
using (spShadingTextureNode sgShadingLayer2TextureNode = simplygon.CreateShadingTextureNode())
{
sgShadingLayer1TextureNode.SetTexCoordLevel(0);
sgShadingLayer1TextureNode.SetTextureName("Layer1");
sgShadingLayer2TextureNode.SetTexCoordLevel(0);
sgShadingLayer2TextureNode.SetTextureName("Layer2");
sgShadingVertexColorNode.SetVertexColorIndex(0);
sgShadingInterpolationNode.SetInput(0, sgShadingLayer1TextureNode);
sgShadingInterpolationNode.SetInput(1, sgShadingLayer2TextureNode);
sgShadingInterpolationNode.SetInput(2, sgShadingVertexColorNode);
The shading network will consists of 3 nodes (2 textures and vertex color) as input into an interpolation node that will give us the final result.
We then add the exit node, in this case the interpolation node, to the only material in the scene using a custom material channel we call “CombinedLayer”.
sgMaterial.SetShadingNetwork("CombinedLayer", sgShadingInterpolationNode);
The final step before the processing can start is to assign the color caster to the custom material channel we just created and enabled mapping image which will be used internally by Simplygon when doing the material casting.
sgCombinedLayerColorCasterSettings.SetMaterialChannel("CombinedLayer");
sgReductionPipeline.AddMaterialCaster(sgCombinedLayerColorCaster, 0);
sgMappingImageSettings.SetGenerateMappingImage(true);
Let us start the Simplygon process.
sgReductionPipeline.RunScene(sgScene, Simplygon.EPipelineRunMode.RunInNewProcess);
The results
When the process is finished we can import the result using the built in Import method from the Simplygon plugin for Unity.
SimplygonImporter.Import(simplygon, sgReductionPipeline, ref lodIndex, assetFolderPath, processingName, optimizedGameObjects);
When doing material casting, Simplygon creates a new material and in this case a new texture called “CombinedLayer_0.png”.
The texture name is a combination of the material channel name, the material index (0 as we only has one output material) and file type is png as this is the default setting for the color caster.
Let us assign this newly created texture to an optimized version of our custom shader where the surface shader looks like this.
void surf (Input IN, inout SurfaceOutputStandard o)
{
float4 c = tex2D(_CombinedLayer, IN.uv_CombinedLayer);
o.Albedo = c.rgb;
o.Alpha = c.a;
}
As you can see, the new shader is simplified and only requires one texture look up.
Let us import the new texture, assign it to the optimized shader and set it as the material for the newly created LOD.
Texture2D combinedTexture = AssetDatabase.LoadAssetAtPath<Texture2D>(Path.Combine(assetFolderPath, "LOD1", "CombinedLayer_0.png"));
var optimizedMeshRenderer = optimizedGameObjects.First().GetComponent<MeshRenderer>();
Material material = new Material(Shader.Find("Custom/CustomOptimizedShader"));
material.SetTexture("_CombinedLayer", combinedTexture);
optimizedMeshRenderer.sharedMaterial = material;
Here you can see the result with the combined texture on the right.
You can use the same technique if you have many objects in your scene and you want to combine all of the individual textures and shader properties into a simplified shader.
You can even take this further if you use the aggregation or remeshing pipeline and combine all meshes and all materials into a single drawcall.
Complete script and shaders
C# script
using Simplygon;
using Simplygon.Unity.EditorPlugin;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using UnityEditor;
using UnityEngine;
public class CustomShader : EditorWindow
{
private List<GameObject> selectedGameObjects = new List<GameObject>();
[MenuItem("Window/SimplygonWithCustomerShader")]
static void Init()
{
var window = EditorWindow.GetWindow(typeof(CustomShader), false, "SimplygonWithCustomShader");
}
public void OnGUI()
{
try
{
List<GameObject> selectedGameObjects = new List<GameObject>();
foreach (var o in Selection.objects)
{
GameObject go = o as GameObject;
if (go != null)
{
selectedGameObjects.Add(go);
}
}
if (selectedGameObjects.Count == 0)
{
EditorGUILayout.HelpBox("Please select game object(s) to process.", MessageType.Info);
}
else if (selectedGameObjects.Count == 1)
{
if (GUILayout.Button("Run Simplygon"))
{
Simplygon.EErrorCodes simplygonErrorCode = 0;
string simplygonErrorMessage = string.Empty;
using (ISimplygon simplygon = global::Simplygon.Loader.InitSimplygon(out simplygonErrorCode, out simplygonErrorMessage))
{
if (simplygon != null)
{
var progressObserver = new SimplygonObserver();
EditorUtility.DisplayProgressBar("Simplygon", "Please wait while the asset is optimized.", 0.0f);
string exportTempDirectory = SimplygonUtils.GetNewTempDirectory();
using (spScene sgScene = SimplygonExporter.Export(simplygon, exportTempDirectory, selectedGameObjects))
{
string processingName = string.Join("_", selectedGameObjects.Select(p => p.name).Distinct().ToArray());
if (sgScene != null && !sgScene.IsNull())
{
using (spMaterialTable sgMaterialTable = sgScene.GetMaterialTable())
using (spTextureTable sgTextureTable = sgScene.GetTextureTable())
{
//Add layer textures to Simplygon.
var meshRenderer = selectedGameObjects.First().GetComponent<MeshRenderer>();
Texture layer1 = meshRenderer.sharedMaterial.GetTexture("_Layer1");
Texture layer2 = meshRenderer.sharedMaterial.GetTexture("_Layer2");
string layer1Path = AssetDatabase.GetAssetPath(layer1);
string layer2Path = AssetDatabase.GetAssetPath(layer2);
using (spTexture sgLayer1Texture = simplygon.CreateTexture())
using (spTexture sgLayer2Texture = simplygon.CreateTexture())
{
sgLayer1Texture.SetFilePath(layer1Path);
sgLayer2Texture.SetFilePath(layer2Path);
sgLayer1Texture.SetName("Layer1");
sgLayer2Texture.SetName("Layer2");
sgTextureTable.AddTexture(sgLayer1Texture);
sgTextureTable.AddTexture(sgLayer2Texture);
}
using (spReductionPipeline sgReductionPipeline = simplygon.CreateReductionPipeline())
using (spColorCaster sgCombinedLayerColorCaster = simplygon.CreateColorCaster())
using (spColorCasterSettings sgCombinedLayerColorCasterSettings = sgCombinedLayerColorCaster.GetColorCasterSettings())
using (spMappingImageSettings sgMappingImageSettings = sgReductionPipeline.GetMappingImageSettings())
using (spShadingVertexColorNode sgShadingVertexColorNode = simplygon.CreateShadingVertexColorNode())
using (spShadingInterpolateNode sgShadingInterpolationNode = simplygon.CreateShadingInterpolateNode())
using (spShadingTextureNode sgShadingLayer1TextureNode = simplygon.CreateShadingTextureNode())
using (spShadingTextureNode sgShadingLayer2TextureNode = simplygon.CreateShadingTextureNode())
{
//Replicate the CustomOriginalShader as a Simplygon shading network.
sgShadingLayer1TextureNode.SetTexCoordLevel(0);
sgShadingLayer1TextureNode.SetTextureName("Layer1");
sgShadingLayer2TextureNode.SetTexCoordLevel(0);
sgShadingLayer2TextureNode.SetTextureName("Layer2");
sgShadingVertexColorNode.SetVertexColorIndex(0);
sgShadingInterpolationNode.SetInput(0, sgShadingLayer1TextureNode);
sgShadingInterpolationNode.SetInput(1, sgShadingLayer2TextureNode);
sgShadingInterpolationNode.SetInput(2, sgShadingVertexColorNode);
//Add the shading network as a custom material channel.
using (spMaterial sgMaterial = sgMaterialTable.GetMaterial(0))
{
sgMaterial.SetShadingNetwork("CombinedLayer", sgShadingInterpolationNode);
}
//Add a color caster using the custom material channel.
sgCombinedLayerColorCasterSettings.SetMaterialChannel("CombinedLayer");
sgReductionPipeline.AddMaterialCaster(sgCombinedLayerColorCaster, 0);
sgMappingImageSettings.SetGenerateMappingImage(true);
//Run the reduction and cast the combined layer texture.
sgReductionPipeline.AddObserver(progressObserver);
sgReductionPipeline.RunScene(sgScene, Simplygon.EPipelineRunMode.RunInNewProcess);
if (!AssetDatabase.IsValidFolder("Assets/LOD"))
{
AssetDatabase.CreateFolder("Assets", "LOD");
}
string assetFolderGuid = AssetDatabase.CreateFolder("Assets/LOD", processingName);
string assetFolderPath = AssetDatabase.GUIDToAssetPath(assetFolderGuid);
int lodIndex = 1;
List<GameObject> optimizedGameObjects = new List<GameObject>();
SimplygonImporter.Import(simplygon, sgReductionPipeline, ref lodIndex, assetFolderPath, processingName, optimizedGameObjects);
if (optimizedGameObjects.FirstOrDefault() != null)
{
//Create a material using the CustomOptimizedShader and assign the combined layer texture.
Texture2D combinedTexture = AssetDatabase.LoadAssetAtPath<Texture2D>(Path.Combine(assetFolderPath, "LOD1", "CombinedLayer_0.png"));
var optimizedMeshRenderer = optimizedGameObjects.First().GetComponent<MeshRenderer>();
Material material = new Material(Shader.Find("Custom/CustomOptimizedShader"));
material.SetTexture("_CombinedLayer", combinedTexture);
optimizedMeshRenderer.sharedMaterial = material;
}
}
}
}
else
{
Debug.LogError("Failed to export selection for Simplygon processing");
}
EditorUtility.ClearProgressBar();
Directory.Delete(exportTempDirectory, true);
}
}
else
{
Debug.LogError(simplygonErrorMessage);
}
}
}
}
}
catch (System.Exception ex)
{
Debug.LogException(ex);
EditorUtility.ClearProgressBar();
}
}
}
CustomOriginalShader
Shader "Custom/CustomOriginalShader"
{
Properties
{
_Layer1 ("Layer1", 2D) = "white" {}
_Layer2 ("Layer2", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
// Physically based Standard lighting model, and enable shadows on all light types
#pragma surface surf Standard fullforwardshadows
// Use shader model 3.0 target, to get nicer looking lighting
#pragma target 3.0
sampler2D _Layer1;
sampler2D _Layer2;
struct Input
{
float4 color : COLOR0;
float2 uv_Layer1 : TEXCOORD0;
float2 uv_Layer2 : TEXCOORD1;
};
// Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader.
// See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
// #pragma instancing_options assumeuniformscaling
UNITY_INSTANCING_BUFFER_START(Props)
// put more per-instance properties here
UNITY_INSTANCING_BUFFER_END(Props)
void surf (Input IN, inout SurfaceOutputStandard o)
{
float4 layer1 = tex2D(_Layer1, IN.uv_Layer1);
float4 layer2 = tex2D(_Layer2, IN.uv_Layer2);
float4 c = lerp(layer1, layer2, IN.color);
o.Albedo = c.rgb;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
CustomOptimizedShader
Shader "Custom/CustomOptimizedShader"
{
Properties
{
_CombinedLayer ("Combined Layer", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
// Physically based Standard lighting model, and enable shadows on all light types
#pragma surface surf Standard fullforwardshadows
// Use shader model 3.0 target, to get nicer looking lighting
#pragma target 3.0
sampler2D _CombinedLayer;
struct Input
{
float2 uv_CombinedLayer : TEXCOORD0;
};
// Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader.
// See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
// #pragma instancing_options assumeuniformscaling
UNITY_INSTANCING_BUFFER_START(Props)
// put more per-instance properties here
UNITY_INSTANCING_BUFFER_END(Props)
void surf (Input IN, inout SurfaceOutputStandard o)
{
float4 c = tex2D(_CombinedLayer, IN.uv_CombinedLayer);
o.Albedo = c.rgb;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}