An FP game engine for Scala.

A basic custom shader

Indigo comes with a number of rendering options and materials, but sometimes you need a custom effect, and this is where shaders come into play.

Indigo's rendering is done using WebGL 2.0, and the shader language of WebGL 2.0 is called GLSL, specifically version 300. While it is possible to write shaders for Indigo in GLSL 300 as a string (possibly loaded at compile time via Indigo's generators), Indigo itself relies on Ultraviolet, a Scala 3 to GLSL transpiler, to do the heavy lifting, and you can use it too for a fun and pleasant shader writing experience.

See Ultraviolets docs for more information on Shaders and shader writing.

How to write and use a custom shader

We'll need an extra import for shader writing with Ultraviolet:

import ultraviolet.syntax.*

We then need to add our custom shader to the list of shaders:

val shaders: Set[ShaderProgram] = Set(CustomShader.shader)

A BlankEntity is then used to display the shader. A BlankEntity is the most basic form of entity, literally just registering a space on the screen, with no notion of how to paint into it until you supply a shader to do the work.

def present(context: Context[Unit], model: Unit): Outcome[SceneUpdateFragment] =
  Outcome(
    SceneUpdateFragment(
      BlankEntity(10, 10, 200, 200, ShaderData(CustomShader.shader.id))
    )
  )

The job of our shader is to manufacture a colour for each fragment (read: pixel - kinda..), defined at a vec4 of floats for the red, green, blue, and alpha values.

The custom shader is defined in two parts:

  1. The shader definition, in this case and ultraviolet fragment shader with a unique id, and a reference to the shader code with it's supplied environment.
  2. The shader code itself. In this case we've made a fragment shader that cycles through the colour wheel. (This is based on the default shadertoy code!)
object CustomShader:

  val shader: ShaderProgram =
    UltravioletShader.entityFragment(
      ShaderId("custom-shader"),
      EntityShader.fragment[FragmentEnv](fragment, FragmentEnv.reference)
    )

  inline def fragment: Shader[FragmentEnv, Unit] =
    Shader[FragmentEnv] { env =>
      def fragment(color: vec4): vec4 =
        val col: vec3 = 0.5f + 0.5f * cos(env.TIME + env.UV.xyx + vec3(0.0f, 2.0f, 4.0f))
        vec4(col, 1.0f)
    }