Scala 3 as GLSL - Language Tour

These examples illustrate the basic language syntax and features available for writing GLSL shaders in Scala 3 with Ultraviolet.

This is not an attempt to cover everything, but it should give you enough to get started.

Demo

▶ Click to play

Primitive types

Shaders are numeric calculations, so all of the primitives are arrays of floats in disguise.

vec2(1.0f, 1.0f)
vec3(1.0f, 1.0f, 1.0f)
vec4(1.0f, 1.0f, 1.0f, 1.0f)
mat2(1.0f, 1.0f, 1.0f, 1.0f)
mat3(1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f)
mat4(1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f,
  1.0f)
array[2, Float](1.0f, 1.0f)

All of the types work with all of the opperations, so you can mod a vec2 if you like / need. They are all pretty adaptable and come with very expressive constructors.

vec2(1.0f, 1.0f)
vec2(1.0f)
vec3(1.0f, 1.0f, 1.0f)
vec4(vec3(1.0f), 1.0f)
vec4(1.0f)

Variables

Almost everything is a Float, and there is poor support for automative numeric conversion. If you're seeing errors, you probably forgot to add the f to 10.0f.

val someValue: Float = 1.0f

vals work as normal, and continue to enjoy type inference.

val position: vec2 = vec2(1.0f, 2.0f)
val color          = vec4(1.0f, 2.0f, 3.0f, 4.0f)

You will need an occasional var too, there is only so far you can go with functional programming here.

var c: Float = 0.0f
c = 4.0f

Functions

Functions can be declared in two ways, and they behave differently, so beware.

You can declare a def, and ultraviolet will place it in the same relative position that you did.

Rules: You cannot nest def's, and you cannot use name shadowing.

def addOne(i: Int): Int = i + 1

You can also declare functions as vals. This concept is not natively supported in GLSL, and to make it work, Ultraviolet assumes that functions declared this way are pure, i.e. they do not refer, for example to any variables declared at the same level.

This is important, because at compile time the body of this functions is moved to the top of the output code. The avoids numerous problems, but if you have refered to something you shouldn't have, you'll get a confusing 'forward reference' error.

Rules: Functions declared as vals must be pure.

val addTwo: Int => Int   = i => i + 2
def addThree: Int => Int = i => i + 3

Unary functions can be composed with compose and andThen, as normal

val e: Float => Float = v => v - 0.5f
val f: Float => vec3  = r => vec3(r, 0.0f, 0.0f)
val g: vec3 => vec4   = val3 => vec4(val3, 0.5f)
val h: Float => vec4  = g compose f compose e

h(1.0f)

If statements

val red    = vec4(1.0, 0.0, 0.0, 1.0)
val green  = vec4(0.0, 1.0, 0.0, 1.0)
val blue   = vec4(0.0, 0.0, 1.0, 1.0)
val w: Int = 1

if w <= 0 then red
else {
  val y = 10
  if w == 1 && y == 10 then blue
  else green
}

Loops

Loops can either be while loops...

var i = 0.0f
while i < 4.0f do i += 1.0f

...or for loops.

val steps = 10
cfor(0, _ < steps, _ + 1) { _ =>
  i += 1.0f
}

Pattern matching / Switch statements

Rules: Yes, there is pattern matching BUT it is converted to switch statements, and GLSL isn't terrible clever about what it can switch on. Keep it simple!

val flag: Int = 2

var res: Int = -1

flag match
  case 0 => res = 10
  case 1 => res = 20
  case 2 => res = 30
  case _ => res = -100

res

Casting

val x  = 1.0f.toInt // Cast is inlined on literal
val y  = 1.toFloat  // Cast is inlined on literal
val z  = x.toFloat
val zz = y.toInt
val w1 = 2
val w2 = (1 + w1).toFloat
x + y

Swizzling

Swizzling is very important in shader programming. It allows you to access and reorganise component values quickly as needed.

A common example is that you are given an RGBA color, but you only want the rgb values.

vec4(1.0f, 2.0f, 3.0f, 4.0f).wzyx
vec3(1.0f, 2.0f, 3.0f).xxy

val fill = vec3(1.0f, 2.0f, 3.0f)
fill.zyx
fill.x
fill.yyy
fill.yz

vec4(1.0f, 2.0f, 3.0f, 4.0f).abgr

Structs

You can delare a struct with a normal class.

class Light(
    val eyePosOrDir: vec3,
    val isDirectional: Boolean,
    val intensity: vec3,
    val attenuation: Float
)

Arrays

val arr: array[4, Float] = array[4, Float](0.0f, 2.0f, 3.0f, 4.0f)
val valueAtOne           = arr(1)

Accessing the Environment

The environment comes loaded with lots of useful data points. If are animating your shaders, for example, your environment might include the time:

val t = env.TIME

Environments in general are customisable, however if you're using Indigo or Shadertoy, there are premade environments for them that you can use or augment.

GLSL Annotations

Sometimes you need to annotate to give GLSL / UV more information.

@flat @in var a1: vec2       = null
@smooth @out val b1: vec4    = null
@global var c1: vec2         = null
@layout(7) @in val d1: Float = 0.0;
@const var e1: vec2          = null
@uniform var f1: Float       = 0.0f

Common Shader Operations and Values

Ultraviolet supports a comprehensive full suite of shader operations, and while they use the real GPU implementation in your shaders, most of them also have Scala implementations to help you with testing.

Here are a few:

sin(1.0f)
mix(1.0f, 2.0f, 0.6f)
step(0.5f, 0.1f)
mod(1.0f, 10.0f)

Embedding Raw GLSL

You can embed RAW GLSL if you need to. It is up to you to ensure it is corect, no compiler support!

      RawGLSL("float v = 1.0;")
      raw("COLOR = vec4(v, v, v, 0.5);")
      RawGLSL(
        """
//#vertex_start
vec4 vertex(vec4 v){
return v;
}
//#vertex_end
        """
      )