Blending for contrast in a layer

Hi Mark, Anna, & Paul,

During my recent work on blending parameters, I realized that the method I recommended for implementing stimulus contrast in a layer was not quite correct. I’m writing to explain the issue and provide an accurate solution.

To recap: Paul had noted that varying alpha_multiplier wasn’t producing the expected contrast changes for a stimulus in a layer. In response, I wrote:

The issue is that the alpha_multiplier parameter of MWorks’ stimuli only behaves like a contrast setting when the stimulus is drawn directly on top of a 50% gray background. When the stimulus is part of a layer that’s drawn on top of a 50% gray background, the blending works out differently. (Specifically, the stimulus is first blended with the layer’s background, whose red, green, blue, and alpha color components are all zero. Then, the layer is blended with the content under it.)

My explanation was (and still is) correct. However, my suggested solution, which was to add a 50% gray rectangle to the layer directly beneath the stimulus, was not (quite) correct. Here’s the story:

When a stimulus with an alpha_multiplier is drawn on top of an opaque, 50% gray background, the resulting color component values are given by the equation

C_out = C_in * A_in + 0.5 * (1 - A_in)

where C_in is the stimulus color, A_in is the stimulus alpha, and C_out is the resulting, blended color. You probably already know this.

What you may not know (and what I neglected to consider) is that the alpha values of the stimulus and the background are also blended, using the same blending factors:

A_out = A_in * A_in + 1.0 * (1 - A_in)

Here, the 1.0 is the gray rectangle’s alpha, and A_out is the resulting, blended alpha that’s written to the framebuffer.

Before MWorks had layers, there was no reason to care about alpha blending, as the alpha value in the framebuffer was never used. (Actually, it was used if you made the stimulus display window non-opaque, but that’s a pretty uncommon scenario.) However, it matters with layers, because the alpha values in a layer’s framebuffer determine how the layer is blended with the contents of the window’s framebuffer.

To make this concrete, suppose your layer contains a gray rectangle with alpha 1 and another stimulus, on top of the rectangle, with alpha 0.5. By the above equation for A_out, the blended alpha value written to the layer’s framebuffer is

0.5 * 0.5 + 1.0 * (1 - 0.5) = 0.75

Since this value is less than one, when the layer is drawn, your stimulus, which has already been blended with 50% gray using an alpha of 0.5, is blended with 50% gray again using an alpha of 0.75. Hence, the final output has a lower contrast than you intended.

I’ve attached an example experiment that demonstrates both the problem and some possible solutions. It displays three gratings. The grating on the left uses the incorrect method for implementing contrast, whereas the middle and right gratings use two different, correct methods. While the difference is subtle, you should be able to see that the left grating has somewhat lower contrast than the other two.

The first correct method (used by the middle grating) places the grating outside of and underneath the layer. The layer contains the gray rectangle and an inverted mask:

//
// Right way #1
//

drifting_grating grating_2 (
    spatial_frequency = 1
    speed = 1
    grating_type = square
    x_size = size
    x_position = 0
    alpha_multiplier = alpha_multiplier
    autoplay = true
    )

layer layer_2 {
    rectangle (
        color = 0.5, 0.5, 0.5
        x_size = size
        x_position = 0
        )

    mask (
        mask = raised_cosine
        inverted = true
        x_size = size
        x_position = 0
        )
}

The second correct method (used by the right grating) uses the new parameters source_alpha_blend_factor and dest_alpha_blend_factor to control the alpha blending. Specifically, it sets the source alpha blend factor to zero and the destination alpha blend factor to one, meaning that the resulting, blended alpha is identical to the gray rectangle’s alpha (1). This approach lets you keep the rectangle, grating, and mask together in a layer (as in the incorrect solution), but it does rely on the new parameters:

//
// Right way #2
//

layer layer_3 {
    rectangle (
        color = 0.5, 0.5, 0.5
        x_size = size
        x_position = offset
        )

    drifting_grating (
        spatial_frequency = 1
        speed = 1
        grating_type = square
        x_size = size
        x_position = offset
        alpha_multiplier = alpha_multiplier
        source_alpha_blend_factor = zero
        dest_alpha_blend_factor = one
        autoplay = true
        )

    mask (
        mask = raised_cosine
        x_size = size
        x_position = offset
        )
}

I hope that’s all reasonably clear, and I apologize for my original, flawed suggestion. If you have any questions, please let me know!

Chris

Attachment: layer_contrast.mwel.txt (1.73 KB)