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:
- At the beginning of each frame, all sampled inputs are marked as "dirty"
- 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
- Subsequent requests within the same frame return the cached value
- 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
(viaconstInput()
) for values that don't change - Use
SampledInput
(viasampledInput()
) for values that need to be recalculated each frame - Use
TimeInput
(viaCommonInputs.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.