Supplying Array Data via UBOs in Indigo
At the time of writing, sending array data to your shaders via UBOs in Indigo is a bit of a rough experience. There are open tickets to try and improve the situation, but this is an attempt to document a path that works and to call out a few points worth knowing. The goal is to give people a way forward while acknowledging that it's less than ideal.
Notes
Always send arrays in UBOs, separate from other data
For the sake of your sanity. Example:
Do:
case class UBO1(COUNT: Int)
case class UBO2(MAP: Array[Float])
Do NOT:
case class UBO1(COUNT: Int, MAP: Array[Float])
Array sizes must be known
Arrays in GLSL are a fixed size. If you need to send variable amounts of data, what you need to do is set an array large enough for all your possible data, and then supply another value indicating the count.
Beware of large arrays (Performance bottleneck)
By default, Ultraviolet supports auto conversion of array sizes up to 4096.
You can add support for arbitrarily sized arrays, up to the UBO max (..max supported by Indigo, in practice this is hardware dependent), like this:
given ultraviolet.macros.ShaderTypeOf[Array[Float]] with
def typeOf: String = s"float [65536]"
HOWEVER.
Because Indigo sets UBOs whenever it needs them, you pay the data transfer cost every time (unless you're using the same shader many times sequentially). The transfer cost is surprisingly high, and you may quickly find yourself running into performance problems.
Demo
Links
UniformBlock derivation limitations
Ultraviolet is particular about what you can derive to ToUniformBlock:
Unsupported shader uniform type. Supported types From Scala (Int, Long, Float, Double), Indigo [RGBA, RGB, Point, Size, Vertex, Vector2, Vector3, Vector4, Rectangle, Matrix4, Radians, Millis, Seconds, Array[Float], js.Array[Float]], and UltraViolet [vec2, vec3, vec4, mat4]. However, if you intend to use the same case class for both Indigo and UltraViolet, you should stick to Float + the UltraViolet types.
Here out first UBO with some simple data in it.
case class CustomData(CUSTOM_COLOR: vec4) derives ToUniformBlock
Avoid derives for arrays
Technically, derives is supported, but if we're treating this example as a recipe, then in this recipe we'll opt to avoid it and follow a known good path.
case class ArrayData(ANOTHER_COLOR: array[4, Float])
Setting up the shader Environment
This is a bit unfortunate. There are a number of ways to declare the Environment in the shader
itself, i.e. Shader[Env]. For example, you can do things like Env & CustomData.
In this case (..and again we're doing a happy path for arrays specifically) we're just going to make a new type with the same fields.
case class Env(
CUSTOM_COLOR: vec4,
ANOTHER_COLOR: array[4, Float]
) extends FragmentEnvReference
object Env:
def ref: Env =
Env(vec4(0.0f), array[4, Float]())
Sending the array data
More repetition as we send over our data to the shader via the shader data.
def present(context: Context, model: Model): Outcome[SceneUpdateFragment] =
Outcome(
SceneUpdateFragment(
Layer(
BlankEntity(
model.viewport,
ShaderData(CustomShader.shader.id)
.withUniformBlocks(
Batch(
UniformBlock(
UniformBlockName("ArrayData"),
Batch(
Uniform("ANOTHER_COLOR") -> ShaderPrimitive
.array(4, Array(0.0f, 1.0f, 1.0f, 1.0f))
)
)
)
)
.addUniformData(CustomData(vec4(1.0f, 0.0f, 1.0f, 1.0f)))
)
)
)
)
Using the array data
Once we've gone through the steps to get our array data set-up, things get back to normal. In this example we're accessing the components by index, but you could also loop over the arrays with something like a while loop if you prefered.
inline def fragment: Shader[Env, Unit] =
Shader[Env] { env =>
ubo[CustomData]
ubo[ArrayData]
def fragment(color: vec4): vec4 =
val magenta = env.CUSTOM_COLOR
val cyan = vec4(
env.ANOTHER_COLOR(0),
env.ANOTHER_COLOR(1),
env.ANOTHER_COLOR(2),
env.ANOTHER_COLOR(3)
)
val mByX = vec4(magenta.xyz * env.UV.x, env.UV.x)
val cByY = vec4(cyan.xyz * env.UV.y, env.UV.y)
mix(mByX, cByY, mByX.a)
}