Skip to content

Examples

Teragam edited this page Jan 12, 2024 · 14 revisions

Creating a simple EffectShader.

This example requires a JavaFX project and may have the following entry point:

public final class EntryPoint {

    public static void main(String[] args) {
        // Only for modular JavaFX applications, ignore otherwise.
        JFXShaderModule.setup();
        Application.launch(CustomShaderUsageExample.class, args);
    }
}

The following example will be used to start a simple JavaFX application with a white circle on a black background. Note that the custom effect gets applied on the root StackPane (The effect can be applied to any node).

public final class CustomShaderUsageExample extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception {
        final StackPane root = new StackPane(new Circle(100, Color.WHITE));
        root.setBackground(new Background(new BackgroundFill(Color.BLACK, null, null)));

        // This is where the custom effect is applied, which will be implemented later.
        final CustomShaderEffect effect = new CustomShaderEffect(1);
        root.setEffect(effect.getFXEffect());

        primaryStage.setScene(new Scene(root, 800, 600));
        primaryStage.show();
    }
}

To define a custom Effect, the new class must inherit from ShaderEffect. For this example, we will inherit the class OneSamplerEffect as it already provides the setInput functionality to chain multiple effects. This is the case for almost every built-in JavaFX effect.

Every effect must be annotated with @EffectDependencies. Effect peers in JavaFX are used to update the shader object for every instance of the effect. Therefore, the JFXShader library needs to know which peers to use for the effect.

The effect has a custom parameter strength that can be used later in the OpenGL and DirectX shader files.

...
import de.teragam.jfxshader.effect.EffectDependencies;
import de.teragam.jfxshader.effect.OneSamplerEffect;

@EffectDependencies(CustomShaderEffectPeer.class)
public final class CustomShaderEffect extends OneSamplerEffect {

    private final DoubleProperty strength;

    public CustomShaderEffect(double strength) {
        this.strength = this.createEffectDoubleProperty(strength, "strength");
    }

    public double getStrength() {
        return this.strength.get();
    }

    public DoubleProperty strengthProperty() {
        return this.strength;
    }

    public void setStrength(double strength) {
        this.strength.set(strength);
    }
}

Every effect peer must be annotated with @EffectPeer and must have a unique ID. (The built-in effects of JavaFX also have a unique ID, like "ColorAdjust" or "SepiaTone".) The ShaderEffectPeer provides the shader sources and updates the shader constants according to the ShaderEffect.

...
import com.sun.prism.ps.Shader;

import de.teragam.jfxshader.ShaderDeclaration;
import de.teragam.jfxshader.effect.EffectPeer;
import de.teragam.jfxshader.effect.ShaderEffectPeer;
import de.teragam.jfxshader.effect.internal.ShaderEffectPeerConfig;

@EffectPeer("UniquePeerName")
public final class CustomShaderEffectPeer extends ShaderEffectPeer<CustomShaderEffect> {

    public CustomShaderEffectPeer(ShaderEffectPeerConfig config) {
        super(config);
    }

    @Override
    protected ShaderDeclaration createShaderDeclaration() {
        final Map<String, Integer> samplers = new HashMap<>();
        samplers.put("baseImg", 0);
        final Map<String, Integer> params = new HashMap<>();
        params.put("strength", 0);
        params.put("texCoords", 1);
        return new ShaderDeclaration(samplers, params, 
                CustomShaderEffectPeer.class.getResourceAsStream("/CustomShader.frag"), // OpenGL fragment shader (Linux, Mac)
                CustomShaderEffectPeer.class.getResourceAsStream("/CustomShader.obj")); // Compiled DirectX 9 pixel shader (Windows)
    }

    @Override
    protected void updateShader(Shader shader, CustomShaderEffect effect) {
        shader.setConstant("strength", (float) effect.getStrength());
        // JavaFX uses an image pool to select textures that are at least as large as the requested dimensions.
        // Therefore, the texture coordinates for the actual content may not be in the range [0, 1], which may cause issues for shaders that rely on this range.
        // Here we pass the content texture coordinates of the input texture "baseImg" to the shader.
        final float[] texCoords = this.getTextureCoords(0);
        shader.setConstant("texCoords", texCoords[0], texCoords[1], texCoords[2], texCoords[3]);
    }
}

The following shader code will be used for OpenGL. This shader will be used on Linux and macOS (there is currently no Metal backend).

#ifdef GL_ES // This definition block is used by every JavaFX shader. This may be copied to allow for further compatibility.
#extension GL_OES_standard_derivatives: enable
#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float;
precision highp int;
#else
precision mediump float;
precision mediump int;
#endif
#else
#define highp
#define mediump
#define lowp
#endif

// Note: The vertex shader is handled by JavaFX and is not customizable for 2D effects.
varying vec2 texCoord0; // The texture coordinate for the first texture sampler.

uniform vec4 jsl_pixCoordOffset; // This offset will be set by JavaFX and may be discarded if not needed.
uniform sampler2D baseImg; // The first texture sampler. The name must match the declared name in the ShaderDeclaration.
uniform float strength; // The declared strength parameter. The name must match the declared name in the ShaderDeclaration.
uniform vec4 texCoords; // The same applies to the texCoords parameter.

void main() {
    // gl_FragCoord alone does not match the pixel coordinates of the image.
    // JavaFX uses this calculation to get the correct pixel coordinates.
    // (For this shader, this is not needed, but it is included for completeness.)
    vec2 pixcoord = vec2(gl_FragCoord.x - jsl_pixCoordOffset.x, ((jsl_pixCoordOffset.z - gl_FragCoord.y) * jsl_pixCoordOffset.w) - jsl_pixCoordOffset.y);
    // For this example, the shader will shift some color channels by a variable amount.
    vec4 img = texture2D(baseImg, texCoord0);
    // Here we need to compensate the offset for the varying texture dimensions by scaling the offset by the provided texture dimensions.
    float imgR = texture2D(baseImg, texCoord0 + (vec2(0.02, 0.0) * strength) * texCoords.zw).r;
    float imgB = texture2D(baseImg, texCoord0 + (vec2(-0.02, 0.0) * strength) * texCoords.zw).b;

    gl_FragColor = vec4(imgR, img.g, imgB, img.a);
}

The following shader code will be used for Windows DirectX.

sampler2D baseImg : register(s0); // The first texture sampler. The index specified in register(s#) must match the declared index in the ShaderDeclaration.
float strength : register(c0); // The declared strength parameter. The index specified in register(c#) must match the declared index in the ShaderDeclaration.
float4 texCoords: register(c1); // The same applies to the texCoords parameter.

void main(in float2 pos0 : TEXCOORD0, in float2 pos1 : TEXCOORD1, in float2 pixcoord : VPOS, in float4 jsl_vertexColor : COLOR0, out float4 color : COLOR0) {
    float4 img = tex2D(baseImg, pos0);
    float imgR = tex2D(baseImg, pos0 + (float2(0.02, 0.0) * strength) * texCoords.zw).r;
    float imgB = tex2D(baseImg, pos0 + (float2(-0.02, 0.0) * strength) * texCoords.zw).b;

    color = float4(imgR, img.g, imgB, img.a);
}

JavaFX requires DirectX shaders to be compiled with the FXC compiler. The following command can be used to compile the shader into an .obj file:

fxc.exe /nologo /T ps_3_0 /Fo CustomShader.obj CustomShader.hlsl

The resulting effect on the white circle looks like this:

CustomShaderResult

Clone this wiki locally