Skip to content
All posts

Unity

Casting custom Unity shaders with properties

In this post we will look at how to cast textures for custom Unity shaders that are using properties, like color tinting or scaling.

Casting custom Unity shaders with properties

Disclaimer: The code in this post is written using version 10.4.366.0 of Simplygon and Unity 6000.3.14f1. 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.

This is a continuation of the blog Custom texture casting in Unity. Only new concepts will be covered in this blog, so make sure to read the previous one if you want to understand the full process.

Prerequisites

This example will use the Simplygon integration in Unity and URP render pipeline, but the same concepts can be applied to all other integrations.

Problem to solve

We want to optimize a collection of assets that are using custom shaders. The shaders are using the following textures:

  • Albedo
  • NormalMap
  • ORMMap: Texture where Red channel is used for smoothness (inverted roughness), Green for metallic and Blue for ambient occlusion.

We also have a color that we tint albedo texture with, as well as scaling for smoothness, metallic and ambient occlusion values.

  • AlbedoTint
  • Roughness
  • Metallic
  • AOPower

Surgical bed showcasing material with color tinting

The shading network looks like this.

Shading network with color tinting node

We want to both remesh and aggregate the assets in this pack, and thus need to add support for our custom shader by implementing a custom compute caster.

Solution

We will use the same approach as in the previous blog, but with some modifications to support the additional properties. The main difference is that we will need to add support for the additional properties in our custom compute caster.

It is quite easy to make mistakes when working with custom shaders. A common error you will get is Error text: Fatal error. Error code: InvalidInputParam. This often indicates that something went wrong during shader compilation. To debug this you can check the log located at C:\Users\<user>\Documents\Simplygon\10\Simplygon.log for additional information.

Expose shader properties

Each parameter we expose in our material, like color for tinting or scalar values needs to be passed to Simplygon. This is done by traversing the Unity shader properties and adding the ones we need to a header and header data list. The header is used to define the structure of the data we are passing, and the header data is used to pass the actual values.

Here we have just exposed color and float properties. If we are using more values we should also add support for them here.

// Add any shader property we need to expose in shader here.
for (int i = 0; i < unityShader.GetPropertyCount(); i++)
{
    var type = unityShader.GetPropertyType(i);
    var name = unityShader.GetPropertyName(i);
    if (type == UnityEngine.Rendering.ShaderPropertyType.Color)
    {
        header.Add($"float4 {name}");
        headerData.Add(unityMaterial.GetColor(name).r);
        headerData.Add(unityMaterial.GetColor(name).g);
        headerData.Add(unityMaterial.GetColor(name).b);
        headerData.Add(unityMaterial.GetColor(name).a);
    }
    else if (type == UnityEngine.Rendering.ShaderPropertyType.Float)
    {
        header.Add($"float {name}");
        headerData.Add(unityMaterial.GetFloat(name));
    }
    // If we are using more data types we can expose them here.
}

Cast tinted color

To tint our albedo texture, we can simply multiply the sampled texture color with the tint color property we exposed above.

case ShaderCastingType.TintedColorChannel:
    return @"
float4 Sample$texturePropertyName() {
    half4 mainColor = CustomBuffer[0]._AlbedoTint;
    float4 tex = Simplygon$texturePropertyName.SampleLevel(Simplygon$texturePropertyNameSamplerState, TexCoord0,0); 
    return mainColor * tex;
}".Replace("$texturePropertyName", texturePropertyName);

Here is the casted albedo texture. We can see that the color tinting has been applied to the texture.

Tinted albedo texture

Cast scaled smoothness, metallic and ambient occlusion values

Similar as above, we can scale our smoothness, metallic and ambient occlusion values by multiplying the sampled texture color with the corresponding scaling properties we exposed above.

case ShaderCastingType.ORMChannel:
    return @"
float4 Sample$texturePropertyName() {
    float RoughnessScale = CustomBuffer[0]._Roughness;
    float MetallicScale = CustomBuffer[0]._Metallic;
    float AOPower = CustomBuffer[0]._AOPower;
    float4 tex = Simplygon$texturePropertyName.SampleLevel(Simplygon$texturePropertyNameSamplerState, TexCoord0,0); 
    tex.r *= RoughnessScale;
    tex.g *= MetallicScale;
    tex.b = pow(tex.b, AOPower);
    return tex;
}".Replace("$texturePropertyName", texturePropertyName);

Here is the casted ORMMap texture. The smoothness, metallic and ambient occlusion values have been scaled according to the properties in our shader and burned into the texture.

Output ORM map

Proxy shader

Since we have now burned color tinting and scaling into our textures, we can use a simpler shader for our remeshed proxy. This is the shading network for it. It is using the same textures as before, but without any tinting or scaling. In a more advanced shader, we could also remove some expensive shading features that are not needed when the asset is viewed from a distance.

Shading network that is just sampling 3 textures

Update to how textures are passed to Simplygon

One update to Simplygon since the previous blog on custom shader casting is that we now ensure that exported textures have unique names. So we can not just reuse the texture name from Unity when we want to find the texture in Simplygon, but need to call GetUniqueTextureName to get the name of the texture in Simplygon.

Texture unityTexture = unityMaterial.GetTexture(texturePropertyName);

if (unityTexture != null)
{

    // Since a Simplygon update we generate unique file names for textures, so we can't just reuse Unity's texture name.
    var simplygonTextureName = GetUniqueTextureName(unityTexture);
    var sgTexture = sgTextureTable.FindTexture(simplygonTextureName);

    // Now we have access to both the Unity texture and the Simplygon texture!

Result

Now let us see what optimization we can apply with out custom shader support.

Aggregation

Aggregation is often done to address draw call overhead, and thus the main benefit of aggregation is that we can reduce the number of materials used. It does not optimize the triangle count, the surface is left untouched.

The red surgical table has it's roughness scaling value set to 0. We can see that it is very shiny both in the original asset and optimized model.

Original
Original
Aggregation
Aggregation

We can see that the output is very similar to the original asset, yet we have reduced the material count. Our three tables now use one and the same material.

Asset Total triangle count Total unique material count Unique textures
Original 3x87 k = 261 k 4 6x 1024x1024
Aggregated 261 1 3x 1024x1024

Remeshing

Remeshing is often applied to objects that are viewed from a distance. It simplifies the surface, reduces the triangle count, removes internal geometry and bakes details and textures down to a single material.

Original
Original
Remeshing
Remeshing

Our table proxies are much simpler compared to the original tables, yet would look good at a distance. Or perhaps even up close if we are targeting a low end platform.

Asset Total triangle count Total unique material count Unique textures
Original 3x45 k = 135 k 5 9x 1024x1024
Remeshed proxy 2 k 1 3x 1024x1024

Complete scripts

The following scripts should be placed in an Editor folder.

CustomShader.cs

// Copyright (c) Microsoft Corporation. 
// Licensed under the MIT license. 

using Simplygon.Unity.EditorPlugin;
using System.Linq;
using UnityEditor;
using UnityEngine;

namespace Simplygon.Examples.CustomShader
{
    /// <summary>
    /// What kind of casting are we going to do on the channel.
    /// </summary>
    public enum ShaderCastingType
    {
        ColorChannel,
        NormalChannel,
        TintedColorChannel,
        ORMChannel
    }

    /// <summary>
    /// Material channel data.
    /// </summary>
    public struct MaterialChannel
    {
        public string TexturePropertyName;
        public EImageColorSpace OutputColorSpace;
        public ShaderCastingType CastingType;
    }

    public class CustomShader
    {
        [MenuItem("Simplygon/Aggregation")]
        static void EntryPointAggregation()
        {
            if (Selection.gameObjects.Length > 0)
            {
                using (ISimplygon simplygon = global::Simplygon.Loader.InitSimplygon(out EErrorCodes simplygonErrorCode, out string simplygonErrorMessage))
                {
                    // if Simplygon handle is valid
                    if (simplygonErrorCode == EErrorCodes.NoError)
                    {
                        var channels = new MaterialChannel[] {
                            new MaterialChannel() { TexturePropertyName = "_Albedo", OutputColorSpace = EImageColorSpace.sRGB, CastingType = ShaderCastingType.TintedColorChannel },
                            new MaterialChannel() { TexturePropertyName = "_NormalMap", OutputColorSpace = EImageColorSpace.Linear, CastingType = ShaderCastingType.NormalChannel },
                            new MaterialChannel() { TexturePropertyName = "_ORMMap", OutputColorSpace = EImageColorSpace.Linear, CastingType = ShaderCastingType.ORMChannel },
                        };

                        string proxyShader = "Assets/Shaders/SimplygonProxyMaterial.shadergraph";
                        using var pipeline = CreateAggregationPipeline(simplygon, 1024);

                        RunPipeline(Selection.gameObjects, pipeline, channels, proxyShader, simplygon);
                    }
                }
            }
        }

        [MenuItem("Simplygon/Remeshing")]
        static void EntryPointRemeshing()
        {
            if (Selection.gameObjects.Length > 0)
            {
                using (ISimplygon simplygon = global::Simplygon.Loader.InitSimplygon(out EErrorCodes simplygonErrorCode, out string simplygonErrorMessage))
                {
                    // if Simplygon handle is valid
                    if (simplygonErrorCode == EErrorCodes.NoError)
                    {
                        var channels = new MaterialChannel[] {
                            new MaterialChannel() { TexturePropertyName = "_Albedo", OutputColorSpace = EImageColorSpace.sRGB, CastingType = ShaderCastingType.TintedColorChannel },
                            new MaterialChannel() { TexturePropertyName = "_NormalMap", OutputColorSpace = EImageColorSpace.Linear, CastingType = ShaderCastingType.NormalChannel },
                            new MaterialChannel() { TexturePropertyName = "_ORMMap", OutputColorSpace = EImageColorSpace.Linear, CastingType = ShaderCastingType.ORMChannel },
                        };

                        string proxyShader = "Assets/Shaders/SimplygonProxyMaterial.shadergraph";
                        using var pipeline = CreateRemeshingPipeline(simplygon, 300, 1024);

                        RunPipeline(Selection.gameObjects, pipeline, channels, proxyShader, simplygon);
                    }
                }
            }
        }

        /// <summary>
        /// Remesh game objects to specified screen size using custom shader.
        /// </summary>
        public static void RunPipeline(GameObject[] gameObjects, spPipeline pipeline, MaterialChannel[] channels, string proxyShader, ISimplygon simplygon)
        {
            // Add compute casters for every material channel.
            foreach (var channel in channels)
            {
                pipeline.AddMaterialCaster(CreateComputeCaster(simplygon, channel.TexturePropertyName, channel.OutputColorSpace), 0);
            }

            var simplygonProcessing = new SimplygonProcessing(simplygon);
            simplygonProcessing.Initialize(pipeline, Selection.gameObjects.ToList());

            using (spScene exportedScene = simplygonProcessing.Export(true))
            {
                if (exportedScene != null && !exportedScene.IsNull())
                {
                    // Add custom shaders we can use for compute casting.
                    AddMaterialShaders(simplygon, exportedScene, channels);

                    // Set Unity specific settings
                    simplygon.SetGlobalDefaultTangentCalculatorTypeSetting(ETangentSpaceMethod.MikkTSpace);
                    simplygon.EnableLogToFile(ELogLevel.Info, (uint)ELogDecoration.AllDecorations);

                    // Perform remeshing and casting.
                    var result = pipeline.RunScene(exportedScene, EPipelineRunMode.RunInThisProcess);

                    if (result == EErrorCodes.NoError)
                    {
                        simplygonProcessing.Import(true, false);
                        SetImportedMaterialShader(simplygonProcessing, channels, proxyShader);
                    }
                }
            }
        }


        /// <summary>
        /// <summary>
        /// Create compute caster for specified material channel and output color space.
        /// </summary>
        static spComputeCaster CreateComputeCaster(ISimplygon simplygon, string channel, EImageColorSpace colorSpace)
        {
            var caster = simplygon.CreateComputeCaster();
            var casterSettings = caster.GetComputeCasterSettings();
            casterSettings.SetOutputColorSpace(colorSpace);
            casterSettings.SetMaterialChannel(channel);
            return caster;
        }

        /// <summary>
        /// Create remeshing pipeline with mapping image.
        /// </summary>
        static spRemeshingPipeline CreateRemeshingPipeline(ISimplygon simplygon, uint screenSize, uint textureSize)
        {
            var pipeline = simplygon.CreateRemeshingPipeline();
            using var pipelineSettings = pipeline.GetRemeshingSettings();
            pipelineSettings.SetOnScreenSize(screenSize);

            var simplygonMappingImageSettings = pipeline.GetMappingImageSettings();
            simplygonMappingImageSettings.SetGenerateMappingImage(true);
            simplygonMappingImageSettings.SetGenerateTexCoords(true);
            simplygonMappingImageSettings.SetGenerateTangents(true);
            simplygonMappingImageSettings.SetUseFullRetexturing(true);
            simplygonMappingImageSettings.SetApplyNewMaterialIds(true);

            simplygonMappingImageSettings.GetOutputMaterialSettings(0).SetTextureHeight(textureSize);
            simplygonMappingImageSettings.GetOutputMaterialSettings(0).SetTextureWidth(textureSize);

            return pipeline;
        }

        /// <summary>
        /// Create an aggregation pipeline with mapping image.
        /// </summary>
        static spAggregationPipeline CreateAggregationPipeline(ISimplygon simplygon, uint textureSize)
        {
            var pipeline = simplygon.CreateAggregationPipeline();
            using var pipelineSettings = pipeline.GetAggregationSettings();
            pipelineSettings.SetMergeGeometries(true);
            pipelineSettings.SetEnableGeometryCulling(true);

            var simplygonMappingImageSettings = pipeline.GetMappingImageSettings();
            simplygonMappingImageSettings.SetGenerateMappingImage(true);
            simplygonMappingImageSettings.SetGenerateTexCoords(true);
            simplygonMappingImageSettings.SetGenerateTangents(true);
            simplygonMappingImageSettings.SetUseFullRetexturing(true);
            simplygonMappingImageSettings.SetApplyNewMaterialIds(true);

            simplygonMappingImageSettings.GetOutputMaterialSettings(0).SetTextureHeight(textureSize);
            simplygonMappingImageSettings.GetOutputMaterialSettings(0).SetTextureWidth(textureSize);

            return pipeline;
        }

        /// <summary>
        /// Sets all casted materials shader to specified shader and connect material channels to correct textures.
        /// </summary>
        private static void SetImportedMaterialShader(SimplygonProcessing processing, MaterialChannel[] channels, string shader)
        {
            foreach (var gameObject in processing.LODGameObjects)
            {
                foreach (var renderer in gameObject.GetComponentsInChildren<MeshRenderer>())
                {
                    foreach (var material in renderer.sharedMaterials)
                    {
                        if (material.name.Contains("SimplygonCastMaterial")) // We'll only change casted materials.
                        {
                            SetupMaterial(material, processing, channels, shader);
                        }
                    }

                }
            }
        }

        /// <summary>
        /// Set material to using shader and connects our newly casted textures to shader.
        /// </summary>
        private static void SetupMaterial(Material material, SimplygonProcessing processing, MaterialChannel[] channels, string shader)
        {
            material.shader = (Shader)AssetDatabase.LoadAssetAtPath(shader, typeof(Shader));

            var path = AssetDatabase.GetAssetPath(material);
            path = path.Substring(0, path.LastIndexOf("/"));

            foreach (var channel in channels)
            {
                var filePath = path + $"/{channel.TexturePropertyName}_0.png";
                Texture2D texture = (Texture2D)AssetDatabase.LoadAssetAtPath(filePath, typeof(Texture2D));
                material.SetTexture(channel.TexturePropertyName, texture);
            }
        }


        /// <summary>
        /// Add compute caster evaluation shaders for all materials in scene.
        /// </summary>
        private static void AddMaterialShaders(ISimplygon simplygon, spScene exportedScene, MaterialChannel[] channels)
        {
            foreach (var gameObject in Selection.gameObjects)
            {
                foreach (var meshRenderer in gameObject.GetComponentsInChildren<MeshRenderer>())
                {
                    foreach (var material in meshRenderer.sharedMaterials)
                    {
                        EvaluationShaderFactory.CreateEvaluationShader(simplygon, exportedScene, material, channels);
                    }
                }
            }
        }
    }
}

EvaluationShaderFactory.cs

// Copyright (c) Microsoft Corporation. 
// Licensed under the MIT license. 

using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

namespace Simplygon.Examples.CustomShader
{
    /// <summary>
    /// Class for adding evaluation shaders to exported scenes.
    /// </summary>
    public static class EvaluationShaderFactory
    {
        // Add functions we need to calculate normal map
        private static string normalShaderHelperFunctions = @"
float3x3 InvertMatrix(float3x3 m)
{
    // Calculate the determinant of the 3x3 matrix
    float det = m._11 * (m._22 * m._33 - m._23 * m._32) -
                m._12 * (m._21 * m._33 - m._23 * m._31) +
                m._13 * (m._21 * m._32 - m._22 * m._31);

    // Check if the determinant is non-zero (matrix is invertible)
    if (abs(det) < 1e-6)
    {
        // Return an identity matrix if the determinant is zero
        return float3x3(1, 0, 0, 0, 1, 0, 0, 0, 1);
    }

    // Calculate the inverse of the matrix
    float invDet = 1.0 / det;
    float3x3 inv;
    inv._11 = invDet * (m._22 * m._33 - m._23 * m._32);
    inv._12 = -invDet * (m._12 * m._33 - m._13 * m._32);
    inv._13 = invDet * (m._12 * m._23 - m._13 * m._22);
    inv._21 = -invDet * (m._21 * m._33 - m._23 * m._31);
    inv._22 = invDet * (m._11 * m._33 - m._13 * m._31);
    inv._23 = -invDet * (m._11 * m._23 - m._13 * m._21);
    inv._31 = invDet * (m._21 * m._32 - m._22 * m._31);
    inv._32 = -invDet * (m._11 * m._32 - m._12 * m._31);
    inv._33 = invDet * (m._11 * m._22 - m._12 * m._21);
    return inv;
}

float3 CalculateTangentSpaceNormalFromSourceObjectSpaceNormal(float3 objectSpaceNormal)
{
    float3 destinationBitangent = cross(sg_DestinationNormal, sg_DestinationTangent);
    destinationBitangent *= sign(dot(destinationBitangent, sg_DestinationBitangent));
    
    float3x3 TBN = float3x3(sg_DestinationTangent, destinationBitangent, sg_DestinationNormal);
    TBN = InvertMatrix( TBN );
    float3 tangentSpaceNormal = mul(objectSpaceNormal, TBN);
	return normalize(tangentSpaceNormal);
}

float3 CalculateObjectSpaceNormalFromSourceTangentSpaceNormal(float3 sourceTangentSpaceNormal)
{
    float3 tangentSpaceNormal = normalize(sourceTangentSpaceNormal);
    
    float3 objectSpaceNormal = tangentSpaceNormal.x * normalize(Tangents0) +
                               tangentSpaceNormal.y * normalize(Bitangents0) +
                               tangentSpaceNormal.z * normalize(Normal);
    
	return normalize(objectSpaceNormal);
}";

        /// <summary>
        /// Function for generate unique texture names for textured exported from Unity.
        /// This code is copy pasted from "C:\Program Files\Simplygon\10\Unity\src\ShaderUtils.cs"
        /// </summary>
        public static string GetUniqueTextureName(Texture texture)
        {
            string path = AssetDatabase.GetAssetPath(texture);
            if (path == null || path == "")
                return texture.name;

            char[] separators = { '/', '\\' };
            string[] pathComponents = path.Split(separators, StringSplitOptions.RemoveEmptyEntries);
            if (pathComponents.Length < 3) // no intermediary folder after asset root: "Assets/Folder/Texture.png"
                return texture.name;

            return String.Join("_", pathComponents[1..^1]) + $"_{texture.name}";
        }

        /// <summary>
        /// Create evaluation shader for material. It adds necessary vertex attributes, shader parameters and samplers for the textures used by the material and generates hlsl code for sampling the textures in the shader.
        /// </summary>
        public static void CreateEvaluationShader(ISimplygon simplygon, spScene sgScene, Material unityMaterial, MaterialChannel[] channels)
        {
            var sgTextureTable = sgScene.GetTextureTable();
            var sgMaterialTable = sgScene.GetMaterialTable();

            var sgMaterial = sgMaterialTable.FindMaterial(unityMaterial.name);
            var materialID = sgMaterialTable.FindItemId(unityMaterial.name);

            //Create the Simplygon evaluation shader for this material.
            var sgMaterialEvaluationShader = simplygon.CreateMaterialEvaluationShader();

            var sgShaderEvaluationFunctionTable = sgMaterialEvaluationShader.GetShaderEvaluationFunctionTable();
            var sgMaterialEvaluationShaderAttributeTable = sgMaterialEvaluationShader.GetMaterialEvaluationShaderAttributeTable();
            var sgMaterialEvaluationShaderParameterTable = sgMaterialEvaluationShader.GetShaderParameterTable();

            //Add normal, UV0, tangent and bitangent attributes so vertex data can be accessed in the compute shader
            CreateEvaluationShaderNormalAttribute(simplygon, sgMaterialEvaluationShaderAttributeTable);
            CreateEvaluationShaderTangentAttribute(simplygon, sgMaterialEvaluationShaderAttributeTable);
            CreateEvaluationShaderBitangentAttribute(simplygon, sgMaterialEvaluationShaderAttributeTable);
            CreateEvaluationShaderTexCoordAttribute(simplygon, sgMaterialEvaluationShaderAttributeTable, 0, "0");

            // Create header for shader
            var header = new List<string>();
            var headerData = new List<float>();

            var unityShader = unityMaterial.shader;
            
            // Add any shader property we need to expose in shader here.
            for (int i = 0; i < unityShader.GetPropertyCount(); i++)
            {
                var type = unityShader.GetPropertyType(i);
                var name = unityShader.GetPropertyName(i);
                if (type == UnityEngine.Rendering.ShaderPropertyType.Color)
                {
                    header.Add($"float4 {name}");
                    headerData.Add(unityMaterial.GetColor(name).r);
                    headerData.Add(unityMaterial.GetColor(name).g);
                    headerData.Add(unityMaterial.GetColor(name).b);
                    headerData.Add(unityMaterial.GetColor(name).a);
                }
                else if (type == UnityEngine.Rendering.ShaderPropertyType.Float)
                {
                    header.Add($"float {name}");
                    headerData.Add(unityMaterial.GetFloat(name));
                }
                // If we are using more data types we can expose them here.
            }

            if (header.Count > 0)
            {
                //Add header to evaluation shader
                //This will set up the constant buffer used to sent constant material data to the compute shader. 
                CreateEvaluationShaderHeader(
                        simplygon,
                        sgScene,
                        sgMaterialEvaluationShader,
                        sgMaterialEvaluationShaderParameterTable,
                        materialID,
                        header.ToArray(),
                        headerData.ToArray()
                    );
            }

            //Add texture and texture samplers to the shader
            string hlslFunction = "";
            hlslFunction += normalShaderHelperFunctions;

            string[] texturePropertyNames = unityMaterial.GetTexturePropertyNames();
            foreach (var channel in channels)
            {
                var texturePropertyName = channel.TexturePropertyName;
                Texture unityTexture = unityMaterial.GetTexture(texturePropertyName);

                if (unityTexture != null)
                {

                    // Since a Simplygon update we generate unique file names for textures, so we can't just reuse Unity's texture name.
                    var simplygonTextureName = GetUniqueTextureName(unityTexture);
                    var sgTexture = sgTextureTable.FindTexture(simplygonTextureName);

                    if (!sgTexture.IsNull())
                    {
                        CreateEvaluationShaderTextureSampler(simplygon,
                                                             sgMaterialEvaluationShaderParameterTable,
                                                             sgTexture.GetName(),
                                                             texturePropertyName,
                                                             GetWrap(unityTexture.wrapModeU),
                                                             GetWrap(unityTexture.wrapModeV),
                                                             GetWrap(unityTexture.wrapModeW),
                                                             GetFilter(unityTexture.filterMode));
                    }
                }
                sgShaderEvaluationFunctionTable.AddShaderEvaluationFunction(CreateShaderEvaluationFunction(simplygon, texturePropertyName));

                hlslFunction += GenerateEvaluationFunctionCode(channel);
            }

            //Add the exported hlsl code to the evaluation shader and add it to the current material.
            sgMaterialEvaluationShader.SetShaderCode(hlslFunction);
            sgMaterialEvaluationShader.SetShaderLanguage(EShaderLanguage.HLSL_6_0);
            sgMaterialEvaluationShader.SetName(unityMaterial.name + "_EvaluationShader");
            sgMaterial.SetMaterialEvaluationShader(sgMaterialEvaluationShader);
        }

        /// <summary>
        /// Generate evaluation function for specific material channel. If it is of NormalChannel type it converts from old tangent space to new.
        /// </summary>
        private static string GenerateEvaluationFunctionCode(MaterialChannel channel)
        {
            var texturePropertyName = channel.TexturePropertyName;
            switch (channel.CastingType)
            {
                case ShaderCastingType.ColorChannel:
                    return @"
float4 Sample$texturePropertyName() {
    return Simplygon$texturePropertyName.SampleLevel(Simplygon$texturePropertyNameSamplerState, TexCoord0,0); 
}".Replace("$texturePropertyName", texturePropertyName);

                case ShaderCastingType.TintedColorChannel:
                    return @"
float4 Sample$texturePropertyName() {
    half4 mainColor = CustomBuffer[0]._AlbedoTint;
    float4 tex = Simplygon$texturePropertyName.SampleLevel(Simplygon$texturePropertyNameSamplerState, TexCoord0,0); 
    return mainColor * tex;
}".Replace("$texturePropertyName", texturePropertyName);

                case ShaderCastingType.ORMChannel:
                    return @"
float4 Sample$texturePropertyName() {
    float RoughnessScale = CustomBuffer[0]._Roughness;
    float MetallicScale = CustomBuffer[0]._Metallic;
    float AOPower = CustomBuffer[0]._AOPower;
    float4 tex = Simplygon$texturePropertyName.SampleLevel(Simplygon$texturePropertyNameSamplerState, TexCoord0,0); 
    tex.r *= RoughnessScale;
    tex.g *= MetallicScale;
    tex.b = pow(tex.b, AOPower);
    return tex;
}".Replace("$texturePropertyName", texturePropertyName);

                case ShaderCastingType.NormalChannel:
                    return @"
float4 Sample$texturePropertyName() {
    half2 uv0 = TexCoord0;

    float3 sourceTangentSpaceNormal = (Simplygon$texturePropertyName.SampleLevel(Simplygon$texturePropertyNameSamplerState, TexCoord0,0).xyz * 2.0) - 1.0;
    float3 objectSpaceNormal = CalculateObjectSpaceNormalFromSourceTangentSpaceNormal(sourceTangentSpaceNormal);
    float3 tangentSpaceNormal = CalculateTangentSpaceNormalFromSourceObjectSpaceNormal(objectSpaceNormal);
    return float4((tangentSpaceNormal + 1.0f) / 2.0f, 1.0f);
}".Replace("$texturePropertyName", texturePropertyName);
                default:
                    throw new NotImplementedException();
            }
        }

        /// <summary>
        /// Create shader evaluation function.
        /// </summary>
        static private spShaderEvaluationFunction CreateShaderEvaluationFunction(ISimplygon simplygon, string channelName)
        {
            var evaluateNormalMap = simplygon.CreateShaderEvaluationFunction();
            evaluateNormalMap.SetChannel(channelName);
            evaluateNormalMap.SetEntryPoint($"Sample{channelName}");
            evaluateNormalMap.SetName(channelName);
            return evaluateNormalMap;
        }

        /// <summary>
        /// Create and add normal shader attribute.
        /// </summary>
        private static void CreateEvaluationShaderNormalAttribute(ISimplygon simplygon, spMaterialEvaluationShaderAttributeTable sgMaterialEvaluationShaderAttributeTable)
        {
            spMaterialEvaluationShaderAttribute sgEvaluationShaderAttributeNormal = simplygon.CreateMaterialEvaluationShaderAttribute();
            sgEvaluationShaderAttributeNormal.SetName("Normal");
            sgEvaluationShaderAttributeNormal.SetFieldType(EGeometryDataFieldType.Normals);
            sgEvaluationShaderAttributeNormal.SetFieldName("Normal");
            sgEvaluationShaderAttributeNormal.SetFieldFormat(EAttributeFormat.F32vec3);

            sgMaterialEvaluationShaderAttributeTable.AddAttribute(sgEvaluationShaderAttributeNormal);
        }

        /// <summary>
        /// Create and add tangent shader attribute.
        /// </summary>
        private static void CreateEvaluationShaderTangentAttribute(ISimplygon simplygon, spMaterialEvaluationShaderAttributeTable sgMaterialEvaluationShaderAttributeTable)
        {
            spMaterialEvaluationShaderAttribute sgEvaluationShaderAttributeTangent = simplygon.CreateMaterialEvaluationShaderAttribute();
            sgEvaluationShaderAttributeTangent.SetName("Tangents0");
            sgEvaluationShaderAttributeTangent.SetFieldType(EGeometryDataFieldType.Tangents);
            sgEvaluationShaderAttributeTangent.SetFieldName("Tangents0");
            sgEvaluationShaderAttributeTangent.SetFieldFormat(EAttributeFormat.F32vec3);

            sgMaterialEvaluationShaderAttributeTable.AddAttribute(sgEvaluationShaderAttributeTangent);
        }

        /// <summary>
        /// Create and add bitangent shader attribute.
        /// </summary>
        private static void CreateEvaluationShaderBitangentAttribute(ISimplygon simplygon, spMaterialEvaluationShaderAttributeTable sgMaterialEvaluationShaderAttributeTable)
        {
            spMaterialEvaluationShaderAttribute sgEvaluationShaderAttributeBitangent = simplygon.CreateMaterialEvaluationShaderAttribute();
            sgEvaluationShaderAttributeBitangent.SetName("Bitangents0");
            sgEvaluationShaderAttributeBitangent.SetFieldType(EGeometryDataFieldType.Bitangents);
            sgEvaluationShaderAttributeBitangent.SetFieldName("Bitangents0");
            sgEvaluationShaderAttributeBitangent.SetFieldFormat(EAttributeFormat.F32vec3);

            sgMaterialEvaluationShaderAttributeTable.AddAttribute(sgEvaluationShaderAttributeBitangent);
        }

        /// <summary>
        /// Create and add texture coordinate attribute.
        /// </summary>
        private static void CreateEvaluationShaderTexCoordAttribute(ISimplygon simplygon, spMaterialEvaluationShaderAttributeTable sgMaterialEvaluationShaderAttributeTable, int texCoordIndex, string fieldName)
        {
            spMaterialEvaluationShaderAttribute sgEvaluationShaderAttributeTexCoords = simplygon.CreateMaterialEvaluationShaderAttribute();
            sgEvaluationShaderAttributeTexCoords.SetName($"TexCoord{texCoordIndex}");
            sgEvaluationShaderAttributeTexCoords.SetFieldType(EGeometryDataFieldType.TexCoords);
            sgEvaluationShaderAttributeTexCoords.SetFieldName(fieldName);
            sgEvaluationShaderAttributeTexCoords.SetFieldFormat(EAttributeFormat.F32vec2);

            sgMaterialEvaluationShaderAttributeTable.AddAttribute(sgEvaluationShaderAttributeTexCoords);
        }

        /// <summary>
        /// Create and add texture parameter, sampler and sampler state for texture.
        /// </summary>
        private static void CreateEvaluationShaderTextureSampler(ISimplygon simplygon, spShaderParameterTable sgMaterialEvaluationShaderParameterTable,
                                                         string textureName,
                                                         string materialChannel,
                                                         ESamplerAddressMode addressModeU,
                                                         ESamplerAddressMode addressModeV,
                                                         ESamplerAddressMode addressModeW,
                                                         ESamplerFilter filterMode)
        {
            spShaderParameterTexture sgShaderParameterTexture = simplygon.CreateShaderParameterTexture();
            sgShaderParameterTexture.SetTextureName(textureName);
            sgShaderParameterTexture.SetName($"Simplygon{materialChannel}Texture");
            sgMaterialEvaluationShaderParameterTable.AddShaderParameter(sgShaderParameterTexture);

            spShaderParameterSamplerState sgShaderParameterSamplerState = simplygon.CreateShaderParameterSamplerState();
            sgShaderParameterSamplerState.SetName($"Simplygon{materialChannel}SamplerState");
            sgShaderParameterSamplerState.SetMinFilter(filterMode);
            sgShaderParameterSamplerState.SetMagFilter(filterMode);
            sgShaderParameterSamplerState.SetAddressU(addressModeU);
            sgShaderParameterSamplerState.SetAddressV(addressModeV);
            sgShaderParameterSamplerState.SetAddressW(addressModeW);
            sgShaderParameterSamplerState.SetUnNormalizedCoordinates(false);
            sgMaterialEvaluationShaderParameterTable.AddShaderParameter(sgShaderParameterSamplerState);

            spShaderParameterSampler sgShaderParameterSampler = simplygon.CreateShaderParameterSampler();
            sgShaderParameterSampler.SetName($"Simplygon{materialChannel}");
            sgShaderParameterSampler.SetSamplerState($"Simplygon{materialChannel}SamplerState");
            sgShaderParameterSampler.SetTextureName(textureName);
            sgMaterialEvaluationShaderParameterTable.AddShaderParameter(sgShaderParameterSampler);
        }

        /// <summary>
        /// Convert sampler address mode from Unity to Simplygon.
        /// </summary>
        private static ESamplerAddressMode GetWrap(TextureWrapMode unityWrap)
        {
            switch (unityWrap)
            {
                case TextureWrapMode.Clamp:
                    return ESamplerAddressMode.ClampToBorder;
                case TextureWrapMode.Repeat:
                    return ESamplerAddressMode.Repeat;
                case TextureWrapMode.Mirror:
                case TextureWrapMode.MirrorOnce:
                    return ESamplerAddressMode.MirrorRepeat;
                default:
                    return ESamplerAddressMode.ClampToBorder;
            }
        }


        /// <summary>
        /// Create shader header for material containing properties from material.
        /// </summary>
        private static void CreateEvaluationShaderHeader(ISimplygon simplygon, spScene sgScene, spMaterialEvaluationShader sgMaterialEvaluationShader, spShaderParameterTable sgMaterialEvaluationShaderParameterTable, int materialIndex, string[] headerAttributes, float[] headerAttributeValues)
        {
            byte[] valuesByteArray = new byte[headerAttributeValues.Length * sizeof(float)];
            Buffer.BlockCopy(headerAttributeValues, 0, valuesByteArray, 0, valuesByteArray.Length);
            spValueArray sgValues = sgScene.AddCustomField(EBaseTypes.TYPES_ID_UCHAR, $"ConstantDataFields_{materialIndex}", 1);
            spUnsignedCharArray sgConstantBuffer = spUnsignedCharArray.SafeCast(sgValues);
            sgConstantBuffer.SetData(valuesByteArray, (uint)valuesByteArray.Length);


            string headerStruct = $@"
                    struct SceneStruct
                    {{
                        {string.Join(";\n", headerAttributes)};
                    }};";

            sgMaterialEvaluationShader.SetShaderHeader(headerStruct);

            spShaderParameterBuffer sgShaderParameterBuffer = simplygon.CreateShaderParameterBuffer();
            sgShaderParameterBuffer.SetName("CustomBuffer");
            sgShaderParameterBuffer.SetFieldName($"ConstantDataFields_{materialIndex}");
            sgShaderParameterBuffer.SetType("SceneStruct");

            sgMaterialEvaluationShaderParameterTable.AddShaderParameter(sgShaderParameterBuffer);
        }

        /// <summary>
        /// Convert filter mode from Unity to Simplygon.
        /// </summary>
        private static ESamplerFilter GetFilter(FilterMode filterMode)
        {
            return filterMode == FilterMode.Point ?
                                 ESamplerFilter.Nearest :
                                 ESamplerFilter.Linear;
        }
    }
}
All posts