-
Notifications
You must be signed in to change notification settings - Fork 36
SurfaceEffects
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.
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).
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).
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
);
[...]
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 shadersmain
method, as we can see in the code above.
-
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 callSetFxParam
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 theFusee.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.
- Using FUSEE
- Tutorials
- Examples
- In-Depth Topics
- Input and Input Devices
- The Rendering Pipeline
- Render Layer
- Camera
- Textures
- FUSEE Exporter Blender Add on
- Assets
- Lighting & Materials
- Serialization and protobuf-net
- ImGui
- Blazor/WebAssembly
- Miscellaneous
- Developing FUSEE