Skip to main content

Input System

The Input System is a core component of the Kraft Shade framework that provides a flexible and powerful mechanism for handling various types of inputs in shader pipelines. It enables dynamic parameter control, animation, and interactive effects by abstracting input sources and providing a consistent interface for accessing input values.

Overview

In graphics programming, shaders often require various inputs to control their behavior - from simple parameters like intensity or color to more complex inputs like time or user interactions. The Kraft Shade Input System addresses these needs by providing:

  • A unified interface for all types of inputs
  • Support for both constant and dynamically changing values
  • Efficient caching and dirty state management for performance
  • Transformation capabilities to modify input values
  • Integration with the Pipeline system for frame-based processing

The Input System is designed to be both simple to use for common cases and flexible enough for complex scenarios. It plays a crucial role in creating dynamic, interactive visual effects by connecting external data sources to shader parameters.

Input Types

The Input System supports various types of inputs that can be used in shader pipelines. At its core is the abstract Input<T> class, which serves as the base for all input implementations.

Base Input Class

The Input<T> class is the foundation of the input system:

abstract class Input<T : Any> {
internal abstract fun Pipeline.internalGet(): T
}

This abstract class defines a generic type parameter T that represents the type of value the input provides. All input implementations extend this class and implement the internalGet() function, which is an extension function on Pipeline.

Immutable Inputs

Immutable inputs provide constant values that don't change during the lifetime of a pipeline. They're useful for fixed parameters that don't need to be updated dynamically.

class ImmutableInput<T : Any>(
private val value: T
) : Input<T>() {
override fun Pipeline.internalGet(): T {
return value
}
}

// Helper function to create immutable inputs
fun <T : Any> constInput(value: T) = ImmutableInput(value)

Example usage:

// Create a constant float input with value 0.5
val intensity = constInput(0.5f)

Sampled Inputs

Sampled inputs provide values that can change over time. They implement a caching mechanism to ensure consistent values within the same frame and only recalculate when necessary.

abstract class SampledInput<T : Any> : Input<T>() {
private var lastSample: T? = null
private var isDirty: Boolean = true

protected abstract fun Pipeline.provideSample(): T

override fun Pipeline.internalGet(): T {
trackInput(this@SampledInput)
if (isDirty) {
lastSample = provideSample()
isDirty = false
}
return lastSample!!
}

internal open fun markDirty() {
isDirty = true
}
}

The SampledInput class:

  • Maintains a cache of the last sampled value
  • Tracks whether the input is "dirty" and needs to be resampled
  • Provides a markDirty() method that's called at the beginning of each frame
  • Requires subclasses to implement provideSample() to generate new values

A convenient way to create sampled inputs is using the sampledInput function:

fun <T : Any> sampledInput(action: PipelineRunningScope.() -> T) : SampledInput<T> {
return WrappedSampledInput(action)
}

Example usage:

// Create a sampled input that provides a random value each frame
val randomValue = sampledInput { Math.random().toFloat() }

Time Input

The TimeInput class is a specialized SampledInput that provides time-based values, useful for animations and time-dependent effects:

class TimeInput(
private val getTime: () -> Long = { System.currentTimeMillis() }
) : SampledInput<Float>() {
// Implementation details...

fun reset() { /* ... */ }
fun start() { /* ... */ }
fun pause() { /* ... */ }
}

The TimeInput class:

  • Provides the elapsed time in seconds as a Float
  • Supports starting, pausing, and resetting the timer
  • Can be customized with a custom time source for testing

Example usage:

// Create a time input and start it
val time = CommonInputs.time()

// Create a paused time input
val pausedTime = CommonInputs.time(start = false)
pausedTime.start() // Start it later

Dynamic Inputs

Dynamic inputs allow for runtime changes to shader parameters and behaviors. They're essential for creating interactive and animated effects.

Understanding the Sampling Mechanism

Sampled inputs use a sophisticated mechanism to ensure efficient and consistent value provision:

  1. At the beginning of each frame, all sampled inputs are marked as "dirty"
  2. When a sampled input's value is requested for the first time in a frame:
    • The input checks if it's dirty
    • If dirty, it calls provideSample() to get a new value and caches it
    • The dirty flag is cleared
  3. Subsequent requests within the same frame return the cached value
  4. This ensures that even if an input is used multiple times in a frame, the value remains consistent

This mechanism is particularly important for inputs like TimeInput where consistent values within a frame are crucial for correct rendering.

Creating Custom Sampled Inputs

You can create custom sampled inputs by extending the SampledInput class:

class MyCustomInput : SampledInput<Float>() {
private var value = 0f

override fun Pipeline.provideSample(): Float {
// Increment the value each frame
value += 0.01f
return value
}

// Additional methods as needed
}

Or more simply using the sampledInput function:

var counter = 0f
val incrementingValue = sampledInput {
counter += 0.01f
counter
}

Using TimeInput for Animations

The TimeInput class is particularly useful for creating animations:

// Create a time input
val time = CommonInputs.time()

// Use it in a shader
shader.setUniform("time", time)

This allows creating effects that change over time, such as:

  • Oscillating parameters
  • Animated transitions
  • Procedural animations
  • Time-based color changes

Input Transformations

Input transformations enable the modification and processing of input values before they are used in shaders. This allows for creating complex behaviors from simple inputs.

Mapping Inputs

The map extension function allows transforming the value of any input:

fun <T : Any, R : Any> Input<T>.map(mapper: (T) -> R): Input<R>

This function automatically chooses the appropriate implementation based on the input type:

  • For regular inputs, it returns a MappedInput
  • For sampled inputs, it returns a MappedSampledInput to preserve the sampling behavior

Example usage:

// Create a time input
val time = CommonInputs.time()

// Map it to a sine wave oscillating between 0 and 1
val oscillator = time.map { sin(it * Math.PI.toFloat() * 2) * 0.5f + 0.5f }

Utility Transformations

The input system provides several utility transformations:

Scale

fun Input<Float>.scale(scale: Float): Input<Float>

Multiplies the input value by a constant factor.

Example:

// Create a time input that runs twice as fast
val fastTime = CommonInputs.time().scale(2f)

Bounce Between

fun Input<Float>.bounceBetween(value1: Float, value2: Float): Input<Float>

Creates an input that oscillates between two values.

Example:

// Create an input that bounces between 0 and 1
val bouncing = CommonInputs.time().bounceBetween(0f, 1f)

Chaining Transformations

Transformations can be chained to create complex behaviors:

// Create a time input
val time = CommonInputs.time()

// Create a value that oscillates between 0.2 and 0.8 at half speed
val oscillator = time
.scale(0.5f)
.map { sin(it * Math.PI.toFloat() * 2) * 0.3f + 0.5f }

Creating Custom Transformations

You can create custom transformations by implementing extension functions on Input<T>:

fun Input<Float>.squared(): Input<Float> = map { it * it }

fun Input<Float>.clamp(min: Float, max: Float): Input<Float> =
map { kotlin.math.max(min, kotlin.math.min(it, max)) }

Best Practices

When to Use Different Input Types

  • Use ImmutableInput (via constInput()) for values that don't change
  • Use SampledInput (via sampledInput()) for values that need to be recalculated each frame
  • Use TimeInput (via CommonInputs.time()) for time-based animations
  • Use transformations to derive complex behaviors from simple inputs

Performance Considerations

  • Sampled inputs recalculate their values only once per frame, even if accessed multiple times
  • Complex calculations in provideSample() methods should be optimized
  • Consider caching expensive calculations even across frames if appropriate
  • Be mindful of creating too many input transformations in performance-critical code

Integration with Shaders

Inputs are typically used to set shader uniforms:

// Create inputs
val time = CommonInputs.time()
val intensity = constInput(0.8f)

// Use them in a shader
shader.setUniform("time", time)
shader.setUniform("intensity", intensity)

This creates a dynamic connection between the inputs and the shader, allowing the shader's behavior to change based on the input values.