Skip to content
Sarah Busert edited this page Dec 8, 2021 · 3 revisions

SurfaceEffects

[Prerequisites]

Using ShaderEffects
Basic knowledge of how a to build a Scene Graph from code.
Basic knowledge of materials/shading in FUSEE or other engines.

Writing ShaderShards
Additionally to the above: (basic) knowledge of glsl or other shader languages.

Intro

SurfaceEffects are one of two ways of adding a Material to a SceneNode's components. They provide a simple way for the user to set uniform variables directly in the C# application code.
For advanced users SurfaceEffects also provide a way to insert shards of shader code to change certain values. Those shader shards will be automatically put in a special function of the shader code and be executed on the GPU (for details see section Adding Shader Shards to a SurfaceEffect).

How to use SurfaceEffects

The easiest way of using SurfaceEffects is to choose a predefined one from the static class Fusee.Engine.Core.MakeEffect. The class provides methods for each of the supported lighting methods:

  • Unlit
  • Diffuse
  • Diffuse/Specular
  • BRDF

The methods that create the SurfaceEffects expect initial values for the uniform variables. For example:

//Create the SurfaceEffect
_surfFx = MakeEffect.FromDiffuseSpecular
(
    albedoColor: new float4(1.0f, 0, 0, 1.0f),
    emissionColor: float4.Zero,
    shininess: 255,
    specularStrength: 1.0f
);

[...]

//Add the SurfaceEffect to the Components list of a SceneNode (before the Mesh)
_scene.Children[0].Components[1] = _surfFx;

This will yield in a red object with a specular highlight.

The input that DiffuseSpecular method expects is relatively self-explanatory, even without in-depth knowledge of different shading approaches. This may not be the case for BRDF (short for bidirectional reflectance distribution function) methods, but they do provide a more physically correct way to shade objects. With those methods we can create SurfaceEffects that are capable of simulating a wide range of real-world materials with a single Shader - simply by adjusting the different uniform variables. Examples can be seen in the picture below. You can find further information regarding BRDF Materials here: learnopengl/PBR/Theory.

💡 Note: As for now, FUSEE does not support Environment Lighting / Reflections. Subsurface Scattering is in a early alpha state (no Thickness Map, no Deferred Shading support).

Examples for BRDF Materials

Example Code for generating the Material for the golden object in the right of the picture:

//Create the SurfaceEffect
_gold_brdfFx = MakeEffect.FromBRDF
(
    albedoColor: new float4(1.0f, 227f / 256f, 157f / 256, 1.0f).LinearColorFromSRgb(),
    emissionColor: new float4(0, 0, 0, 0),
    roughness: 0.2f,
    metallic: 1,
    specular: 0,
    ior: 0.47f,
    subsurface: 0
);

[...]

Adding Shader Shards to a SurfaceEffect

To add custom shader code to a ShaderEffect we need to create a new instance of the class by ourself.

public SurfaceEffect(SurfaceInput input, List<string> surfOutVertBody = null, List<string> surfOutFragBody = null, RenderStateSet rendererStates = null)

The constructor expects a SurfaceInput or a object of a derived type. The two Shader Shards – named surfOutVertBody and surfOutFragBody of type List<string> – are optional.

The Surface Input is a C# Class that holds all lighting relevant uniform parameters we may want to customize via Shader Shards. Additionally it defines the lighting method for this effect.
It enables the users to set the uniform parameters from the C# code from where they are internally routed into the shader on the GPU.

There is a pre defined Surface Input for every lighting calculation that FUSEE supports at the moment. Those can be found here: SurfaceEffectInput.cs.

The example code below shows the class for specular lighting:

public class SpecularInput : INotifyValueChange<SurfaceEffectEventArgs>
{
    //The lighting calculation method
    public ShadingModel ShadingModel { get; protected set; } = ShadingModel.DiffuseSpecular;

    //Property for the base color
    public float4 Albedo
    {
        get => _albedo;
        set
        {
            if (value != _albedo)
            {
                _albedo = value;
                NotifyValueChanged(_albedo.GetType(), nameof(Albedo), _albedo);
            }
        }
    }
    private float4 _albedo;

    //Property for roughness of the material
    public float Roughness[...]
    private float _roughness;
   
    //Property for strength of the specular highlight
    public float SpecularStrength[...]
    private float _specularStrength;
    
    //Property for the shininess of the specular highlight
    public float Shininess[...]
    private float _shininess;

    [...]
}

From this FUSEE builds a glsl struct. A field of this type serves as out parameter of the vertex shader and in parameter of the fragment shader.

struct SurfOut
{
  vec3 position;
  vec4 albedo;
  vec4 emission;
  vec3 normal;
  float roughness;
  float specularStrength;
  float shininess;
};

The built shader also provides a method to change the values of this struct in the vertex and fragment shaders. The List<string> surfOutVertBody of the SurfaceEffects constructor will be added to this methods body as we can see in the code below (lines marked as Shader Shard).

For the vertex shader you need to calculate the position and normal.

//C#
var surfOutVertBody = new List<string>(){@"
    OUT.position = fuVertex;
    OUT.normal = fuNormal;"
};
//VERTEX SHADER

struct SurfOut
{
    [...]
};
out SurfOut surfOut;

SurfOut ChangeSurfVert()
{
    SurfOut OUT = SurfOut(vec3(0), vec4(0), vec4(0), vec3(0), 0.0, 0.0, 0.0);    
    
    // ----- Shader Shard ---- //
    OUT.position = fuVertex;
    OUT.normal = fuNormal;
    //------------------------//

    return OUT;
}

void main()
{
    surfOut = ChangeSurfVert();
    [...]
}

The same principle applies for the fragment shader. There you must make sure to set a value to all lighting relevant properties of your input struct.

//C#
var surfOutFragBody = new List<string>(){@"
    OUT.albedo = IN.Albedo;
    OUT.specularStrength = IN.SpecularStrength;
    OUT.shininess = IN.Shininess;
    OUT.roughness = IN.Roughness;
    OUT.emission = IN.Emission;"
};
//FRAGMENT SHADER

//Uniform, the values are settable from C#
struct SpecularInput
{
    vec4 Emission;
    float SpecularStrength;
    float Shininess;
    float Roughness;
    vec4 Albedo;
};
uniform SpecularInput SurfaceInput;

//Input from the vertex shader
struct SurfOut
{
    [...]
};
in SurfOut surfOut;

out vec4 oColor;

SurfOut ChangeSurfFrag(SpecularInput IN)
{
    SurfOut OUT = surfOut;
    //--------- Shader Shard ------------------//
    OUT.albedo = IN.Albedo;
    OUT.specularStrength = IN.SpecularStrength;
    OUT.shininess = IN.Shininess;
    OUT.roughness = IN.Roughness;
    OUT.emission = IN.Emission;
    //----------------------------------------//
    return OUT;
}

void main()
{
    SurfOut surfOut = ChangeSurfFrag(SurfaceInput);

    //lighting calculation using the values in surfOut
    [...]
    oColor = [...]
}

💡 Note: If no custom Shader Shards are given to the constructor of the SurfaceEffect FUSEE will determine standard ones that only transfer the given values into the shaders main method, as we can see in the code above.

👷 Engine Developer

Notes on how to write a new SurfaceEffect

  • Always derive from SurfaceEffectBase (or its subclasses).

  • If you want to add uniforms directly to it do it as a property and use the appropriate Attributes, for example:

//Example of how to add a uniform to a custom SurfaceEffect

[FxShader(ShaderCategory.Fragment)]
[FxShard(ShardCategory.Uniform)]
public int ColorMode
{
    get { return _colorMode; }
    set
    {
        _colorMode = value;
        SetFxParam(nameof(ColorMode), _colorMode);
    }
}
private int _colorMode;

⚠️ Caution: You need to call SetFxParam in the setter to ensure the value is passed form the C# code to the shader.

  • If you want to insert custom shader code, think about whether it can be done inside the ChangeSurf methods (injected via Shader Shards). If this isn't possible refer to the files in the Fusee.Engine.Core.ShaderShards namespace to get an idea on how to add shader code.

  • Make sure the surface effect can be used with forward and deferred rendering.

Clone this wiki locally