Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Drawing semi-transparent rectangles in Direct2D pixel shader results in colors being additioned #921

Open
Odepax opened this issue May 26, 2023 · 0 comments

Comments

@Odepax
Copy link

Odepax commented May 26, 2023

Not sure if this is the right place to ask generic questions about Direct2D, but here goes nothing.

I'm trying to use a pixel shader effect in Vortice's Direct2D, with the objective of drawing (R 10%, G 20%, B 30%, A 40%) rectangles on an (R 10%, G 10%, B 10%, A 100%) background. So far, the only rectangle I haven't managed to draw correctly is the one output from the shader. 😭

What I do

👉 Repro repo

Draw the first rectangle using ID2D1DeviceContext6.FillRectangle():

// D2DeviceContext is ID2D1DeviceContext6
D2DeviceContext.FillRectangle(new RectangleF(x, y, 64, 64), OnscreenBrush);

Draw the second rectangle on an offscreen ID2D1BitmapRenderTarget, then draw this offscreen canvas using ID2D1DeviceContext6.DrawBitmap():

// OffscreenBuffer is ID2D1BitmapRenderTarget, of size 64 x 64
// OffscreenBrush  is ID2D1SolidColorBrush
// D2DeviceContext is ID2D1DeviceContext6
OffscreenBuffer.BeginDraw();
OffscreenBuffer.Clear(Colors.Transparent);
OffscreenBuffer.FillRectangle(new RectangleF(0, 0, 64, 64), OffscreenBrush);
OffscreenBuffer.EndDraw();

D2DeviceContext.DrawBitmap(
    bitmap: OffscreenBuffer.Bitmap,
    opacity: 1,
    interpolationMode: BitmapInterpolationMode.Linear,
    sourceRectangle: new RectangleF(0, 0, 64, 64),
    destinationRectangle: new RectangleF(x, y, 64, 64)
);

Draw the third rectangle from the same offscreen render target, then draw this offscreen canvas using ID2D1DeviceContext6.DrawImage() (instead of previously ID2D1DeviceContext6.DrawBitmap()):

// D2DeviceContext is ID2D1DeviceContext6
// OffscreenBuffer is ID2D1BitmapRenderTarget
D2DeviceContext.DrawImage(
    image: OffscreenBuffer.Bitmap,
    compositeMode: CompositeMode.SourceOver,
    interpolationMode: Direct2D1.InterpolationMode.NearestNeighbor,
    targetOffset: new Vector2(x, y)
);

Draw the fourth rectangle by passing the offscreen render target to a pixel-shader-powered ID2D1Effect, then draw the effect's output using ID2D1DeviceContext6.DrawImage():

// D2DeviceContext      is ID2D1DeviceContext6
// TextHightlightShader is TextHightlightShader, which extends ID2D1Effect
D2DeviceContext.DrawImage(
    image: TextHightlightShader.Output,
    compositeMode: CompositeMode.SourceOver,
    interpolationMode: Direct2D1.InterpolationMode.NearestNeighbor,
    targetOffset: new Vector2(x, y)
);

The shader doesn't use its input, aside from determining the size of the output. The shader just returns a hard-coded (R 10%, G 20%, B 30%, A 40%) color.

What I get

Expectations Reality
image image
All rectangles drawn the same. The rectangle from the pixel shader is off: in fact each color channel seems to have been additioned.

At first I thought CompositeMode.SourceOver was to blame, but I can't explain how similar calls to .DrawImage() output different results when passed an ID2D1Bitmap (third rectangle) and an ID2D1Image (fourth rectangle). This leads me to believe that the issue isn't caused by the Vortice wrapper, nor the way the final result is drawn, but lies somewhere in relation to the pixel shader...

The shader effect code

class TextHightlightShader : ID2D1Effect
{
    public static void Register(ID2D1Factory1 d2Factory) => d2Factory.RegisterEffect<Implementation>();

    public TextHightlightShader(ID2D1DeviceContext context) : base(context.CreateEffect(typeof(Implementation).GUID)) { }
    public TextHightlightShader(ID2D1EffectContext context) : base(context.CreateEffect(typeof(Implementation).GUID)) { }

    [CustomEffect(1)]
    class Implementation : CustomEffectBase, ID2D1EffectImpl
    {
        ShaderTransform? ShaderT;

        static readonly System.Collections.Generic.HashSet<Implementation> all = new(); // https://github.com/amerkoleci/Vortice.Windows/issues/315
        public Implementation() => all.Add(this);                                       // https://github.com/amerkoleci/Vortice.Windows/issues/315

        protected override void DisposeCore(bool disposing)
        {
            if (disposing)
            {
                ShaderT?.Dispose();
                ShaderT = null;

                all.Remove(this);                                                       // https://github.com/amerkoleci/Vortice.Windows/issues/315
            }

            base.DisposeCore(disposing);
        }

        public override void Initialize(ID2D1EffectContext effectContext, ID2D1TransformGraph transformGraph)
        {
            base.Initialize(effectContext, transformGraph);

            ShaderT = new ShaderTransform(effectContext);

            transformGraph.AddNode(ShaderT);
            transformGraph.SetSingleTransformNode(ShaderT);
        }

        public override void SetGraph(ID2D1TransformGraph transformGraph)
        {
            base.SetGraph(transformGraph);
        }

        public override void PrepareForRender(ChangeType changeType)
        {
            base.PrepareForRender(changeType);
        }
    }

    class ShaderTransform : CallbackBase, ID2D1DrawTransform
    {
        public ShaderTransform(ID2D1EffectContext effectContext)
        {
            var compilation = Compiler.Compile(
                shaderSource: @"
                float4 main() : SV_TARGET
                {
                    return float4(0.1f, 0.2f, 0.3f, 0.4f);
                }
                ",
                defines: Array.Empty<ShaderMacro>(),
                include: null!,
                entryPoint: "main",
                sourceName: "TextHightlight.hlsl",
                profile: "ps_5_0",
                shaderFlags: ShaderFlags.Debug,
                effectFlags: EffectFlags.None,
                out var blob,
                out var errorBlob
            );

            using (blob)
            using (errorBlob)
            {
                compilation.CheckError();

                var bytes = blob.AsBytes();
                effectContext.LoadPixelShader(typeof(ShaderTransform).GUID, bytes, bytes.Length);
            }
        }

        public int GetInputCount()
        {
            return 1;
        }

        public void MapInputRectsToOutputRect(RawRect[] inputRects, RawRect[] inputOpaqueSubRects, out RawRect outputRect, out RawRect outputOpaqueSubRect)
        {
            outputRect = inputRects[0];
            outputOpaqueSubRect = inputOpaqueSubRects[0];
        }

        public void MapOutputRectToInputRects(RawRect outputRect, RawRect[] inputRects)
        {
            inputRects[0] = outputRect;
        }

        public RawRect MapInvalidRect(int inputIndex, RawRect invalidInputRect)
        {
            return invalidInputRect;
        }

        public void SetDrawInfo(ID2D1DrawInfo drawInfo)
        {
            drawInfo.SetPixelShader(typeof(ShaderTransform).GUID, PixelOptions.None);
        }
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant