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