Skip to main content

PipelineModifier

PipelineModifier is a powerful feature in KraftShade that allows you to create reusable, composable pipeline components. These modifiers encapsulate complex pipeline operations into reusable units that can be easily integrated into larger pipeline setups.

Overview​

A PipelineModifier is essentially a pre-configured set of pipeline steps that can be applied within any pipeline setup scope. This promotes code reuse, maintainability, and allows you to build complex effects by composing simpler, well-tested components.

Creating a PipelineModifier​

To create a PipelineModifier, you need to extend one of the base modifier classes:

  • PipelineModifierWithInputTexture - For modifiers that process a single input texture
  • PipelineModifierWithBuffers - For modifiers that work with multiple buffer inputs/outputs

Example: Circular Blur with Edge Preservation​

Here's a complete example of creating a PipelineModifier that applies circular blur while preserving edges:

package com.cardinalblue.effects.effect.pipeline

import com.cardinalblue.effects.effect.blur.PreserveEdgeFilter
import com.cardinalblue.kraftshade.dsl.GraphPipelineSetupScope
import com.cardinalblue.kraftshade.pipeline.PipelineModifierWithInputTexture
import com.cardinalblue.kraftshade.pipeline.input.Input
import com.cardinalblue.kraftshade.shader.buffer.GlBufferProvider
import com.cardinalblue.kraftshade.shader.buffer.TextureProvider
import com.cardinalblue.kraftshade.shader.builtin.CircularBlurKraftShader

class CircularBlurPreservingEdgePipelineModifier(
private val amount: Input<Float>,
private val repeat: Input<Float>,
private val edgeStrength: Input<Float>,
) : PipelineModifierWithInputTexture() {

override suspend fun GraphPipelineSetupScope.addStep(
inputTexture: TextureProvider,
outputBuffer: GlBufferProvider
) {
// Create intermediate buffer references
val (edgeTexture, blurredTexture) = createBufferReferences("edge", "blurred")

// Step 1: Detect edges using Sobel operator
with(SobelEdgeDetectionModifier(edgeStrength)) {
addStep(inputTexture, edgeTexture)
}

// Step 2: Apply circular blur
step(
shader = CircularBlurKraftShader(),
targetBuffer = blurredTexture,
) { shader ->
shader.setInputTexture(inputTexture)
shader.amount = amount.get()
shader.repeat = repeat.get()
}

// Step 3: Combine original, blurred, and edge textures
step(
shader = PreserveEdgeFilter(),
targetBuffer = outputBuffer,
) { shader ->
shader.setInputTexture(inputTexture)
shader.setBlurredTexture(blurredTexture)
shader.setEdgeTexture(edgeTexture)
}
}
}

Using PipelineModifier in Pipelines​

Once you've created a PipelineModifier, you can use it within any pipeline setup scope using the with scope function:

In Serial Pipelines​

serialSteps(inputTexture, outputBuffer) {
val blurModifier = CircularBlurPreservingEdgePipelineModifier(
amount = blurAmount,
repeat = constInput(60f),
edgeStrength = edgeStrength
)

with(blurModifier) { addStep() }

step(BrightnessKraftShader()) { shader ->
shader.brightness = brightness.get()
}

// Add more steps as needed
}

In Graph Pipelines​

graphPipeline(inputTexture, outputBuffer) {
val edgeDetection = SobelEdgeDetectionModifier(constInput(1.0f))
val blurEffect = CircularBlurPreservingEdgePipelineModifier(
amount = blurAmount,
repeat = constInput(30f),
edgeStrength = constInput(0.5f)
)

// Use modifiers within graph nodes
node("preprocess") {
with(edgeDetection) { addStep() }
}

node("blur") {
with(blurEffect) { addStep() }
}

// Connect nodes...
}

Benefits of PipelineModifier​

1. Code Reusability​

Create once, use everywhere. PipelineModifiers can be shared across different effects and projects.

2. Encapsulation​

Hide complex implementation details behind a simple interface. Users only need to know the inputs and outputs.

3. Testing​

Modifiers can be tested independently, ensuring each component works correctly before integration.

4. Composition​

Build complex effects by combining simple, well-tested modifiers.

5. Maintainability​

Changes to a modifier's implementation automatically propagate to all uses without code duplication.

Best Practices​

1. Single Responsibility​

Each modifier should have a single, well-defined purpose. Avoid creating monolithic modifiers that do too much.

2. Clear Naming​

Use descriptive names that clearly indicate what the modifier does.

3. Input Validation​

Validate inputs within the modifier to ensure robust error handling.

4. Documentation​

Document the modifier's purpose, inputs, outputs, and any special considerations.

5. Testing​

Create unit tests for your modifiers to ensure they behave correctly in isolation.

Common Patterns​

Input Texture Modifier​

For modifiers that process a single texture:

class SimpleBlurModifier(private val radius: Input<Float>) : PipelineModifierWithInputTexture() {
override suspend fun GraphPipelineSetupScope.addStep(
inputTexture: TextureProvider,
outputBuffer: GlBufferProvider
) {
step(BlurKraftShader()) { shader ->
shader.inputTexture = inputTexture
shader.radius = radius.get()
}
}
}

Multi-Input Modifier​

For modifiers that work with multiple inputs:

class BlendModifier(
private val blendMode: Input<BlendMode>,
private val opacity: Input<Float>
) : PipelineModifierWithBuffers() {
override suspend fun GraphPipelineSetupScope.addStep(
inputs: List<TextureProvider>,
outputBuffer: GlBufferProvider
) {
require(inputs.size == 2) { "Blend modifier requires exactly 2 inputs" }

step(BlendKraftShader()) { shader ->
shader.baseTexture = inputs[0]
shader.overlayTexture = inputs[1]
shader.blendMode = blendMode.get()
shader.opacity = opacity.get()
}
}
}

Integration with Existing Code​

PipelineModifiers integrate seamlessly with existing KraftShade code:

  • They work with both serial and graph pipelines
  • They support all input types (constants, sliders, external inputs)
  • They can be nested within other modifiers
  • They maintain the same performance characteristics as inline pipeline steps

Next Steps​