Custom texture casting in Unity

Written by Jesper Tingvall, Product Expert, Simplygon

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

In this post we will look at how to cast textures for custom shaders in Unity.

For how to use compute casters in the more generic case we suggest reading using your own shaders for material baking with Compute Casting .

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 create a proxy model for a URP Unity scene for distant viewing. We want to optimize triangle count, draw calls and overdraw.

Unity scene with wood floor, pumpkins and stove.

The scene contains 3 different materials that uses a custom shader that Simplygon out of the box does not support.

Unity material showcasing texture inputs.

The shader in question is very simple. We have just packed several channels; metallic, roughness and ambient occlusion, into one texture. Since we have no other shading effects like UV tiling or color tinting it is enough for us to cast the textures into a new UV space.

Unity shader graph.

In this blog we are going to use the remesher to create a proxy model, but we will face the same issues if we want to create an aggregation or impostor.

Solution

To cast textures for our custom shader we are going to use compute casters. With compute casters we have full control of texture casting. We can with HLSL or GLSL shading code describe exactly how the textures should be merged. If we have a complex shader then this casting shader will also be complex. Here we will just introduce the most basic example, we have a shader that samples textures.

To describe the texture channels of our shader we introduce the following data structure. Here we describe the texture property name, color space and what compute shader we should use for casting.

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

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

The texture property name can be found in the properties of the shader we want to cast.

Shader properties for Simple prop shader.

This results in these list of channels to cast.

/// <summary>
/// Texture channels for Simple prop shader.
/// </summary>
private static MaterialChannel[] propChannels = new MaterialChannel[] {
    new MaterialChannel() { TexturePropertyName = "_BaseMap", OutputColorSpace = EImageColorSpace.sRGB, CastingType = ShaderCastingType.ColorChannel },
    new MaterialChannel() { TexturePropertyName = "_ORM", OutputColorSpace = EImageColorSpace.Linear, CastingType = ShaderCastingType.ColorChannel },
    new MaterialChannel() { TexturePropertyName = "_NormalMap", OutputColorSpace = EImageColorSpace.Linear, CastingType = ShaderCastingType.NormalChannel }
};

Remesh pipeline

We'll use a remeshing pipeline to create a proxy model to which we want to perform our custom texture casting. It is of course possible to use custom texture casting with other pipelines.

/// <summary>
/// Create remeshing pipeline with mapping image (required for material casting).
/// </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;
}

Adding compute casters

We will now add compute casters for our scene. In the casters we specify which material channel to bake and color space. The rest is handled by the material evaluation shader we'll set up soon.

/// <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;
}

We can now add compute casters to the remeshing pipeline for every material channel we want to bake.

// Add compute casters for every material channel.
foreach (var channel in channels)
{
    pipeline.AddMaterialCaster(CreateComputeCaster(simplygon, channel.TexturePropertyName, channel.OutputColorSpace), 0);
}

Add compute shaders used for casting

For every material in the scene we want to perform material casting on we need to setup evaluation shaders. We will create the class EvaluationShaderFactory that handles creation of these shaders.

/// <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);
            }
        }
    }
}

Create attributes

Geometry fields like UVs, vertex colors & normals that we want to use in our compute caster needs to be exposed as an attribute. This is done through creating MaterialEvaluationShaderAttribute and specifying the corresponding EGeometryDataFieldType, corresponding EAttributeFormat and field name. With SetName we specify how we should refer to the field in our compute shader.

/// <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);
}

We do the same for UV0, bitangent and tangents fields.

Create texture samplers

For every texture we want to use in our compute shader we need to create three things:

/// <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);
}

In Complete scripts below we also have some helper functions to convert from Unity's texture sampling and filtering modes to the ones used in Simplygon.

Evaluation functions

It is now time to write the actual compute shaders used for material casting. These can be written in GLSL or HLSL. It is important to notice that these shaders runs outside of Unity. So some Unity specific functions and constants are not accessible and needs to be reimplemented.

For our color channel we do the most simple shader possible. We just sample the texture for our current texture channel.

/// <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);

Normal map needs a bit more consideration when casting. First we need to remap it from color to vector space. After that we transform it into object space for the original asset. After that we transform it into tangent space for our optimized asset. Lastly we transform it back into color space. The helper functions we use can be found in Complete Scripts section below are from Unity's shader code.

    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);

Reconnect materials

After import our asset will use the default shader for project's render pipeline. We add a function that changes the imported materials shader and sets all texture properties to the new textures we just cased.

We can use the same shader as our original object, but a better idea is probably to use a more slimmed down version.

/// <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);
  }
}

Result

After processing the asset we get this proxy mesh.

Original
Remeshing

We get a resulting texture per texture channel; BaseMap, NormalMap and ORM. We can see that the different materials; stove, wooden floor and pumpkin are present in the texture.

Output textures

With the texture applied to the proxy model it is hard to see the difference between original and remeshed model.

Original
Remeshing

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
    }

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

    public class CustomShader
    {
        /// <summary>
        /// Texture channels for Simple prop shader.
        /// </summary>
        private static MaterialChannel[] propChannels = new MaterialChannel[] {
            new MaterialChannel() { TexturePropertyName = "_BaseMap", OutputColorSpace = EImageColorSpace.sRGB, CastingType = ShaderCastingType.ColorChannel },
            new MaterialChannel() { TexturePropertyName = "_ORM", OutputColorSpace = EImageColorSpace.Linear, CastingType = ShaderCastingType.ColorChannel },
            new MaterialChannel() { TexturePropertyName = "_NormalMap", OutputColorSpace = EImageColorSpace.Linear, CastingType = ShaderCastingType.NormalChannel }
        };

        /// <summary>
        /// Path to simple prop shader.
        /// </summary>
        private static string propShader = "Assets/Shaders/Simple_Prop_Shader.shadergraph";

        [MenuItem("Simplygon/Remeshing with Simple_Prop_Shader")]
        static void EntryPoint()
        {
            if (Selection.gameObjects.Length > 0)
            {
                Remesh(Selection.gameObjects, 300, 1024, propChannels, propShader);
            }
        }

        /// <summary>
        /// Remesh game objects to specified screen size using custom shader.
        /// </summary>
        public static void Remesh(GameObject[] gameObjects, uint screenSize, uint textureSize, MaterialChannel[] channels, string shader)
        {
            using (ISimplygon simplygon = global::Simplygon.Loader.InitSimplygon
            (out EErrorCodes simplygonErrorCode, out string simplygonErrorMessage))
            {
                // if Simplygon handle is valid
                if (simplygonErrorCode == EErrorCodes.NoError)
                {
                    using var pipeline = CreateRemeshingPipeline(simplygon, screenSize, textureSize);

                    // 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, shader);
                            }
                        }
                    }
                }

                // if invalid handle, output error message to the Unity console
                else
                {
                    Debug.LogError("Simplygon initializing failed!");
                }
            }
        }

        /// <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 (required for material casting).
        /// </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>
        /// 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 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);
}";

        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);

            //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");

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

            string[] texturePropertyNames = unityMaterial.GetTexturePropertyNames();
            //foreach (var texturePropertyName in texturePropertyNames)
            foreach (var channel in channels)
            {
                var texturePropertyName = channel.TexturePropertyName;
                Texture unityTexture = unityMaterial.GetTexture(texturePropertyName);
                if (unityTexture != null)
                {
                    var sgTexture = sgTextureTable.FindTexture(unityTexture.name);

                    // Temporary fix, Set correct color space for textures
                    sgTexture.SetColorSpace(unityTexture.isDataSRGB ? EImageColorSpace.sRGB : EImageColorSpace.Linear);

                    if (sgTexture != null)
                    {
                        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.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>
        /// Convert filter mode from Unity to Simplygon.
        /// </summary>
        private static ESamplerFilter GetFilter(FilterMode filterMode)
        {
            return filterMode == FilterMode.Point ?
                                 ESamplerFilter.Nearest :
                                 ESamplerFilter.Linear;
        }
    }
}
⇐ Back to all posts

Request 30-days free evaluation license

*
*
*
*
Industry
*

Request 30-days free evaluation license

*
*
*
*
Industry
*