FShade


Technical Overview

How does it work?

FShade makes use of F#'s quotations (see Code Quotations) for translating the source-code to target languages like GLSL.

These quotations can be obtained by either applying the ReflectedDefinition attribute to functions/modules or by defining computation expression builders having a member Quote : unit -> unit. Since we wanted shaders to be first-class we opted for the latter and defined computation expression builders for the various available shader-stages.

Let's look at a simple vertex shader example.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
type Vertex =
    {
        [<Semantic("pos")>]
        p : V4d
    }

let scaleShader (v : Vertex) =
    vertex {
        return {
            p = V4d(3.0 * v.p.XYZ, 1.0)
        }
    }

The above shader simply scales the per-vertex-value p by a factor 3 and returns it.

The record field p here is annotated with Semantic("pos") which will be used as input-name in the resulting low-level code and guides compositions (more on that below).

To obtain a compilable Effect from the shader-function we use a somewhat magical combinator.

1: 
let scaleEffect = Effect.ofFunction scaleShader

Under the hood ofFunction actually invokes the given function using Unchecked.defaultof<_> as argument value which ensures that per-vertex values are not accessed by shaders at compile-time. The transformation will raise an exception stating shader functions may not access their vertex-input statically when trying to do so.

The resulting Effect represents an assignment of shaders to the available stages. This encoding is similar to DirectX's Effects where each Effect may contain any subset of shader-stages.

The effect from above can then be compiled to a Module which in turn can be compiled to GLSL like this:

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
scaleEffect
    |> Effect.toModule { 
        EffectConfig.empty with 
            lastStage = ShaderStage.Vertex
            outputs = Map.ofList ["pos", (typeof<V4d>, 2) ] 
        }
    |> ModuleCompiler.compileGLSL410
    |> printfn "%s"

The EffectConfig states that we're interested only in the vertex shader (lastStage) and that we'd like the result to include the output annotated with Semantic("pos") as 4 dimensional vector (V4d) which should be bound to output-location 2.

Here we use FShade's standard GLSL410 compiler to generate the final code:

#version 410

#ifdef Vertex
layout(location = 0) in vec4 pos;
layout(location = 2) out vec4 posOut;
void main()
{
    posOut = vec4((3.0 * pos.xyz), 1.0);
}

#endif

Compositions

FShade currently provides 5 shader stages that can be used in effects.

1: 
2: 
3: 
4: 
5: 
6: 
type ShaderStage = 
    | Vertex  = 0
    | TessControl = 1
    | TessEval = 2
    | Geometry = 3 
    | Fragment = 4

Just like in standard shader languages a shader can be composed with another shader for a later stage. Therefore effects can obviously be composed whenever maxStage(left) < minStage(right).

However FShade also provides composition for other scenarios. Consider the following effects:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
25: 
26: 
27: 
module Transformation = 
    type Vertex =
        {
            // [<Position>] is a shorthand for [<Semantic("Positions")>]
            [<Position>]                pos : V4d 
            [<Semantic("Normal")>]      normal : V3d
        }

    let shader (v : Vertex) =
        vertex {
            return {
                pos = 3.0 * v.pos
                normal = 4.0 * v.normal
            }
        }
  
module Coloring =
    type Vertex =
        {
            [<Semantic("Normal")>]      n       : V3d
            [<Semantic("Color")>]       color   : V4d
        }  

    let shader (v : Vertex) =
        vertex {
            return { v with color = V4d(v.n.X, v.n.Y, v.n.Z, 1.0) }
        }

These two (admittedly strage) effects are concerned with different aspects of a vertex but both make use of the vertex's Normal.

FShade's composition allows us to compose both effects sequentially using:

1: 
2: 
3: 
4: 
5: 
let composed1 =
    Effect.compose [
        Effect.ofFunction Transformation.shader
        Effect.ofFunction Coloring.shader 
    ]

which gives us the following GLSL code:

#version 410

#ifdef Vertex
layout(location = 0) in vec3 Normal;
layout(location = 1) in vec4 Positions;
layout(location = 0) out vec4 ColorOut;
layout(location = 1) out vec3 NormalOut;
layout(location = 2) out vec4 PositionsOut;
void main()
{
    vec3 NormalC = (4.0 * Normal);
    ColorOut = vec4(NormalC.x, NormalC.y, NormalC.z, 1.0);
    NormalOut = NormalC;
    PositionsOut = (3.0 * Positions);
}

#endif

Note that Coloring here uses the normal from Transformation

When composed the other way round:

1: 
2: 
3: 
4: 
5: 
let composed2 =
    Effect.compose [
        Effect.ofFunction Coloring.shader 
        Effect.ofFunction Transformation.shader
    ]

the resulting code looks like:

#version 410

#ifdef Vertex
layout(location = 0) in vec3 Normal;
layout(location = 1) in vec4 Positions;
layout(location = 0) out vec4 ColorOut;
layout(location = 1) out vec3 NormalOut;
layout(location = 2) out vec4 PositionsOut;
void main()
{
    ColorOut = vec4(Normal.x, Normal.y, Normal.z, 1.0);
    NormalOut = (4.0 * Normal);
    PositionsOut = (3.0 * Positions);
}

#endif

From a high-level point of view compose [a; b] is similar to F#'s a >> b since it pipes the outputs of a into b.

However compose works on n-ary functions taking an arbitrary number of inputs and producing an arbitrary number of outputs. Matching values are identified using the Semantic annotations defined in the types.

Currently only compositions for a subset of shader combinations are implemented.

  • compose [Vertex; Vertex] -> Vertex
  • compose [Geometry; Geometry] -> Geometry
  • compose [Geometry; Vertex] -> Geometry
  • compose [Fragment; Fragment] -> Fragment
  • compose [a; b] when a < b -> Effect [a;b]

However these seem sufficient in all cases we encountered so far ;)

Linking

As you may have noticed in the above composed effects tend to have a lot of outputs which may (partly) not be needed for rendering.

Therefore FShade provides functions for removing unused outputs and adding unspecified outputs by passing them along.

A little example:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
module Example = 
    type Vertex =
        {
            [<Semantic("Normal")>] n : V3d
        }

    type Fragment =
        {
            [<Color>] c : V4d
            [<Semantic("Normal")>] n : V3d
        }

    let normalColor (v : Vertex) =
        fragment {
            let n = v.n
            return {
                c = V4d(n.X, n.Y, n.Z, 1.0)
                n = n
            }
        }

Here our fragment shader needs an input Normal and returns Colors and Normal.

In our config we declare only Colors to be used and bind it to location 0.

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
Example.normalColor
    |> Effect.ofFunction
    |> Effect.toModule {
        EffectConfig.empty with
            lastStage = ShaderStage.Fragment
            outputs = Map.ofList ["Colors", (typeof<V4d>, 0)]
       }
    |> ModuleCompiler.compileGLSL410
    |> printfn "%s"

The compiler now automatically removes the output Normal from the effect and creates a vertex shader passing the remaining inputs along. Since fragment shaders always implicitly require a position the vertex shader will finally pass Positions and Normal.

We refer to this process as linking since it links all shaders together creating a tight set of in-/outputs.

#version 410

#ifdef Vertex
layout(location = 0) in vec3 Normal;
layout(location = 1) in vec4 Positions;
layout(location = 0) out vec3 fs_Normal;
void main()
{
    fs_Normal = Normal;
    gl_Position = Positions;
}

#endif

#ifdef Fragment
layout(location = 0) in vec3 fs_Normal;
layout(location = 0) out vec4 ColorsOut;
void main()
{
    ColorsOut = vec4(fs_Normal.x, fs_Normal.y, fs_Normal.z, 1.0);
}

#endif

Note that passing along inputs is simple for vertex/fragment shaders but can be rather complicated for other stages.

When passing values through a GeometryShader FShade looks for an output annotated with [<SourceVertexIndex>] and uses its value to determine the corresponding input vertex for the output vertex in question. For Point inputs the SourceVertexIndex is always 0.

FShade can also pass values through tessellation shaders as long as they can be interpolated linearly. (e.g. are floating point values)

Uniforms

FShade uses F#'s (?) operator for accessing uniforms on the global value uniform.

1: 
2: 
3: 
4: 
5: 
let trafoShader1 (v : Vertex) =
    vertex {
        let mvp : M44d = uniform?MyUniformBuffer?MVPMatrix
        return { v with p = mvp * v.p }
    }

this shader accesses the uniform named MVPMatrix which will be part of a uniform buffer MyUniformBuffer having type M44d.

Since the return type for (?) is generic the value mvp needs a type annotation. To avoid these annotations one may simply define extension properties for uniforms like:

1: 
2: 
3: 
4: 
5: 
6: 
7: 
type UniformScope with
    member x.MVPMatrix : M44d = uniform?MyUniformBuffer?MVPMatrix

let trafoShader2 (v : Vertex) =
    vertex {
        return { v with p = uniform.MVPMatrix * v.p }
    }

Note that FShade does not contain any typed uniform-accessors like that but rendering frameworks may define their available uniform values like that.

namespace Microsoft
namespace Microsoft.FSharp
namespace Microsoft.FSharp.Quotations
namespace Aardvark
namespace Aardvark.Base
namespace FShade
namespace FShade.Imperative
module Utilities
type Vertex =
  {p: V4d;}
Multiple items
type SemanticAttribute =
  inherit Attribute
  new : s:string -> SemanticAttribute
  member Semantic : string

--------------------
new : s:string -> SemanticAttribute
Vertex.p: V4d
Multiple items
type V4d =
  struct
    new : v:int -> V4d + 25 overloads
    val X : float
    val Y : float
    val Z : float
    val W : float
    member Abs : V4d
    member AllDifferent : v:V4d -> bool + 1 overload
    member AllEqual : v:V4d -> bool + 1 overload
    member AllGreater : v:V4d -> bool + 1 overload
    member AllGreaterOrEqual : v:V4d -> bool + 1 overload
    ...
  end

--------------------
V4d ()
   (+0 other overloads)
V4d(v: int) : V4d
   (+0 other overloads)
V4d(a: int []) : V4d
   (+0 other overloads)
V4d(v: int64) : V4d
   (+0 other overloads)
V4d(a: int64 []) : V4d
   (+0 other overloads)
V4d(v: float32) : V4d
   (+0 other overloads)
V4d(a: float32 []) : V4d
   (+0 other overloads)
V4d(v: float) : V4d
   (+0 other overloads)
V4d(a: float []) : V4d
   (+0 other overloads)
V4d(index_fun: System.Func<int,float>) : V4d
   (+0 other overloads)
val scaleShader : v:Vertex -> Expr<Vertex>
val v : Vertex
val vertex : VertexBuilder
val scaleEffect : Effect
Multiple items
module Effect

from Utilities

--------------------
module Effect

from FShade

--------------------
type Effect =
  new : m:Lazy<Map<ShaderStage,Shader>> -> Effect
  new : m:Lazy<Map<ShaderStage,Shader>> * o:Effect list -> Effect
  private new : id:string * shaders:Lazy<Map<ShaderStage,Shader>> * composedOf:Effect list -> Effect
  member ComposedOf : Effect list
  member FirstShader : Shader option
  member FragmentShader : Shader option
  member GeometryShader : Shader option
  member Id : string
  member InputToplogy : InputTopology option
  member Inputs : Map<string,Type>
  ...

--------------------
new : m:System.Lazy<Map<ShaderStage,Shader>> -> Effect
new : m:System.Lazy<Map<ShaderStage,Shader>> * o:Effect list -> Effect
val ofFunction : shaderFunction:('a -> Expr<'b>) -> Effect
val toModule : config:EffectConfig -> effect:Effect -> Module
Multiple items
module EffectConfig

from FShade

--------------------
type EffectConfig =
  {depthRange: Range1d;
   flipHandedness: bool;
   lastStage: ShaderStage;
   outputs: Map<string,(Type * int)>;}
val empty : EffectConfig
Multiple items
module ShaderStage

from Aardvark.Base

--------------------
type ShaderStage =
  | Vertex = 0
  | TessControl = 1
  | TessEval = 2
  | Geometry = 3
  | Fragment = 4
  | Compute = -1
Multiple items
ShaderStage.Vertex: ShaderStage = 0

--------------------
ShaderStage.Vertex: ShaderStage = 1
Multiple items
module Map

from FShade.BasicQuotationPatterns

--------------------
module Map

from FShade.StateExtensions

--------------------
module Map

from Aardvark.Base.Prelude

--------------------
module Map

from Aardvark.Base.Predefined Lenses

--------------------
module Map

from Microsoft.FSharp.Collections

--------------------
type Map<'Key,'Value (requires comparison)> =
  interface IEnumerable
  interface IComparable
  interface IEnumerable<KeyValuePair<'Key,'Value>>
  interface ICollection<KeyValuePair<'Key,'Value>>
  interface IDictionary<'Key,'Value>
  new : elements:seq<'Key * 'Value> -> Map<'Key,'Value>
  member Add : key:'Key * value:'Value -> Map<'Key,'Value>
  member ContainsKey : key:'Key -> bool
  override Equals : obj -> bool
  member Remove : key:'Key -> Map<'Key,'Value>
  ...

--------------------
new : elements:seq<'Key * 'Value> -> Map<'Key,'Value>
val ofList : elements:('Key * 'T) list -> Map<'Key,'T> (requires comparison)
val typeof<'T> : System.Type
Multiple items
module ModuleCompiler

from Utilities

--------------------
module ModuleCompiler

from FShade.Imperative

--------------------
module ModuleCompiler

from FShade.SpirV Extensions

--------------------
module ModuleCompiler

from FShade.Backends
Multiple items
val compileGLSL410 : module_:Module -> string

--------------------
val compileGLSL410 : module_:Module -> GLSL.GLSLShader
val printfn : format:Printf.TextWriterFormat<'T> -> 'T
type ShaderStage =
  | Vertex = 0
  | TessControl = 1
  | TessEval = 2
  | Geometry = 3
  | Fragment = 4
ShaderStage.Vertex: ShaderStage = 0
ShaderStage.TessControl: ShaderStage = 1
ShaderStage.TessEval: ShaderStage = 2
ShaderStage.Geometry: ShaderStage = 3
ShaderStage.Fragment: ShaderStage = 4
Multiple items
module ShaderStage

from Aardvark.Base

--------------------
type ShaderStage = ShaderStage
type ShaderStage =
  | Vertex = 0
  | TessControl = 1
  | TessEval = 2
  | Geometry = 3
  | Fragment = 4
  | Compute = -1
type Vertex =
  {pos: V4d;
   normal: V3d;}
Multiple items
type PositionAttribute =
  inherit SemanticAttribute
  new : unit -> PositionAttribute

--------------------
new : unit -> PositionAttribute
Vertex.pos: V4d
Vertex.normal: V3d
Multiple items
type V3d =
  struct
    new : v:int -> V3d + 25 overloads
    val X : float
    val Y : float
    val Z : float
    member Abs : V3d
    member AllDifferent : v:V3d -> bool + 1 overload
    member AllEqual : v:V3d -> bool + 1 overload
    member AllGreater : v:V3d -> bool + 1 overload
    member AllGreaterOrEqual : v:V3d -> bool + 1 overload
    member AllInfinity : bool
    ...
  end

--------------------
V3d ()
   (+0 other overloads)
V3d(v: int) : V3d
   (+0 other overloads)
V3d(a: int []) : V3d
   (+0 other overloads)
V3d(v: int64) : V3d
   (+0 other overloads)
V3d(a: int64 []) : V3d
   (+0 other overloads)
V3d(v: float32) : V3d
   (+0 other overloads)
V3d(a: float32 []) : V3d
   (+0 other overloads)
V3d(v: float) : V3d
   (+0 other overloads)
V3d(a: float []) : V3d
   (+0 other overloads)
V3d(index_fun: System.Func<int,float>) : V3d
   (+0 other overloads)
val shader : v:Vertex -> Expr<Vertex>
type Vertex =
  {n: V3d;
   color: V4d;}
Vertex.n: V3d
Vertex.color: V4d
field V3d.X: float
field V3d.Y: float
field V3d.Z: float
val composed1 : Effect
Multiple items
val compose : effects:#seq<Effect> -> Effect

--------------------
val compose : effects:#seq<Effect> -> Effect
module Transformation

from Overview
val shader : v:Transformation.Vertex -> Expr<Transformation.Vertex>
module Coloring

from Overview
val shader : v:Coloring.Vertex -> Expr<Coloring.Vertex>
val composed2 : Effect
type Vertex =
  {n: V3d;}
type Fragment =
  {c: V4d;
   n: V3d;}
Multiple items
union case TextureAspect.Color: TextureAspect

--------------------
type ColorAttribute =
  inherit SemanticAttribute
  new : unit -> ColorAttribute

--------------------
new : unit -> ColorAttribute
Fragment.c: V4d
Fragment.n: V3d
val normalColor : v:Vertex -> Expr<Fragment>
val fragment : FragmentBuilder
val n : V3d
module Example

from Overview
val normalColor : v:Example.Vertex -> Expr<Example.Fragment>
Multiple items
ShaderStage.Fragment: ShaderStage = 4

--------------------
ShaderStage.Fragment: ShaderStage = 5
val trafoShader1 : v:Vertex -> Expr<Vertex>
val mvp : M44d
Multiple items
type M44d =
  struct
    new : a:float[] -> M44d + 2 overloads
    val M00 : float
    val M01 : float
    val M02 : float
    val M03 : float
    val M10 : float
    val M11 : float
    val M12 : float
    val M13 : float
    val M20 : float
    ...
  end

--------------------
M44d ()
M44d(a: float []) : M44d
M44d(a: float [], start: int) : M44d
M44d(m00: float, m01: float, m02: float, m03: float, m10: float, m11: float, m12: float, m13: float, m20: float, m21: float, m22: float, m23: float, m30: float, m31: float, m32: float, m33: float) : M44d
val uniform : UniformScope
type UniformScope =
  interface IComparable
  private new : parent:Option<UniformScope> * name:string -> UniformScope
  override Equals : o:obj -> bool
  member GetChildScope : n:string -> UniformScope
  override GetHashCode : unit -> int
  member FullName : string
  member private Id : int
  member Name : string
  member Parent : Option<UniformScope>
  static member CreatePickler : r:IPicklerResolver -> Pickler<UniformScope>
  ...
val x : UniformScope
val trafoShader2 : v:Vertex -> Expr<Vertex>
Fork me on GitHub