Using Imports & Shared Code

Imports and shared code are not entirely straight forward to use with Ultraviolet.

Ultraviolet would never have come into being without Scala 3's powerful inline macro system, but this is where you really start to see the limits of the current setup, and it's all down to how inlining works. No doubt all these problems are solvable, we only await someone with the time and inclination to come and fix them!

In any case, while sharing code in Ultraviolet is undoubtedly particular and peculiar to use, it is still a massive improvement over building shaders with strings as you would in JavaScript.

Please see the main docs on inlining for more information.

How to import shared code

Shared code

First we'll declare some contrived code that we're pretending we'd like to use in multiple shaders.

There are some important things to note about this small section of code.

  1. We've imported ultraviolet.syntax.*. Recall that, unfortunately, both Indigo and Ultraviolet both have types called things like vec2, vec4, etc. This import is to ensure that we're using the Ultraviolet types. In this specific example the import is done inside the object because indigo is imported at the top, but if the code was in a separate class then you could just put the import at the top as usual.

  2. All the fields and functions are declared as inline. This is because Ultraviolet is implemented with inline macros, and if the code isn't inlined, then Ultraviolet can't see it.

  3. redAmount looks like it should be a val... and indeed normally it would be. However, val declarations do not work, so we have to use inline def instead.

object SharedCode:
  import ultraviolet.syntax.*

  inline def redAmount: Float       = 0.5
  inline def grabX(uv: vec2): Float = uv.x

Within our custom shader object, we can import the shared code and use it in our shader, but we can't do things like SharedCode.redAmount at the present time.

import SharedCode.* // This is okay.
// SharedCode.redAmount // This does not work.

Another unfortunate limitation is that we cannot use our grabX function directly. The code will compile if you try to use it, but your shader will be black, and if you check your browser console you'll see a mysterious error, like this:

[Indigo] ERROR: 0:72: 'constructor' : too many arguments

In order to use the grabX function, we need to assign it to a proxy/delegate function within the scope of the shader.

val proxyGrabX: vec2 => Float = uv => grabX(uv)

Finally, we can see the shared code in use.

def fragment(color: vec4): vec4 =
  vec4(redAmount, proxyGrabX(env.UV), 0.0f, 1.0f)