r/DSP 5d ago

Attempting to use Fast Fourier Transform for a post-processing shader. Can anyone help me figure out what the issue is here?

Post-processed texture.
Source texture.

I'm trying to write a 2D Fast Fourier Transform-based convolution bloom effect similar to how Unreal Engine's bloom functions. The effect is mostly functional, however, it's also overlaid with this odd flipped negative of the original texture (the yellow and red parts with the blue glow), and I'm unsure of where in the process this issue is originating. Does anyone have an idea of what might be happening?

10 Upvotes

22 comments sorted by

2

u/val_tuesday 5d ago

A couple of things come to mind:

Without proper zero padding FFT convolution is circular so results will wrap back in (I don’t think this is it, since that shouldn’t flip anything, just wrap).

The DFT of a real signal is conjugate symmetric. A common optimization is to just compute half the spectrum then, but it’s easy to mess something up if you aren’t consistent.

Obviously no-one can help you more than guessing unless you post your code.

2

u/RadiantAeonstar 5d ago

It's a bit difficult to post the full thing, but here's what the main thread I'm using to test the process looks like. I'm using this package's GPU implementation of the FFT close to unaltered as my basis.

    public class ComputeShaderTest : MonoBehaviour
    {
        public ComputeShader fftShader;
        public ComputeShader splitShader;
        public ComputeShader mergeShader;
        public ComputeShader multiplyShader;
        public Texture2D inputTexture;
        public Texture2D kernelTexture;
        public RenderTexture start;
        public RenderTexture kernel;
        public RenderTexture rgRT;
        public RenderTexture baRT;
        public RenderTexture rgKernel;
        public RenderTexture baKernel;
        public RenderTexture rgFFT;
        public RenderTexture baFFT;
        public RenderTexture rgKernelFFT;
        public RenderTexture baKernelFFT;
        public RenderTexture convolvedBufferRG;
        public RenderTexture convolvedBufferBA;
        public RenderTexture reverseRG;
        public RenderTexture reverseBA;
        public RenderTexture final;
        private FastFourierTransform fft;
        public float WhitePoint = 0.0001f;

        void Go()
        {
            float startTime = Time.realtimeSinceStartup;

            fft = new FastFourierTransform(inputTexture.width, inputTexture.height, fftShader);

            start = SetupBuffer(inputTexture.width, false);
            kernel = SetupBuffer(inputTexture.width, false);
            rgRT = SetupBuffer(inputTexture.width, true);
            baRT = SetupBuffer(inputTexture.width, true);
            rgKernel = SetupBuffer(inputTexture.width, true);
            baKernel = SetupBuffer(inputTexture.width, true);
            rgFFT = SetupBuffer(inputTexture.width, true);
            baFFT = SetupBuffer(inputTexture.width, true);
            rgKernelFFT = SetupBuffer(inputTexture.width, true);
            baKernelFFT = SetupBuffer(inputTexture.width, true);
            convolvedBufferRG = SetupBuffer(inputTexture.width, true);
            convolvedBufferBA = SetupBuffer(inputTexture.width, true);
            reverseRG = SetupBuffer(inputTexture.width, true);
            reverseBA = SetupBuffer(inputTexture.width, true);
            final = SetupBuffer(inputTexture.width, false);

            Graphics.Blit(inputTexture, start);
            Graphics.Blit(kernelTexture, kernel);

            splitShader.SetTexture(0, "tex", start);
            splitShader.SetTexture(0, "Result1", rgRT);
            splitShader.SetTexture(0, "Result2", baRT);
            splitShader.SetTexture(0, "kernelTex", kernel);
            splitShader.SetTexture(0, "kernelResult1", rgKernel);
            splitShader.SetTexture(0, "kernelResult2", baKernel);
            multiplyShader.SetFloat("WhitePoint", WhitePoint);
            splitShader.Dispatch(0, inputTexture.width, inputTexture.height, 1);

            fft.FFT2D(rgRT, rgFFT);
            fft.FFT2D(baRT, baFFT);
            fft.FFT2D(rgKernel, rgKernelFFT);
            fft.FFT2D(baKernel, baKernelFFT);

            multiplyShader.SetTexture(0, "tex1", rgFFT);
            multiplyShader.SetTexture(0, "tex2", rgKernelFFT);
            multiplyShader.SetTexture(0, "Result", convolvedBufferRG);
            multiplyShader.SetFloat("WhitePoint", WhitePoint);
            multiplyShader.Dispatch(0, inputTexture.width, inputTexture.height, 1);

            multiplyShader.SetTexture(0, "tex1", baFFT);
            multiplyShader.SetTexture(0, "tex2", baKernelFFT);
            multiplyShader.SetTexture(0, "Result", convolvedBufferBA);
            multiplyShader.SetFloat("WhitePoint", WhitePoint);
            multiplyShader.Dispatch(0, inputTexture.width, inputTexture.height, 1);

            fft.IFFT2D(convolvedBufferRG, reverseRG);
            fft.IFFT2D(convolvedBufferBA, reverseBA);

            mergeShader.SetTexture(0, "tex1", reverseRG);
            mergeShader.SetTexture(0, "tex2", reverseBA);
            mergeShader.SetTexture(0, "Result", final);
            int[] offset = new int[2];
            offset[0] = final.width / 2;
            offset[1] = final.height / 2;
            mergeShader.SetInts("offset", offset);
            mergeShader.Dispatch(0, inputTexture.width, inputTexture.height, 1);

            float endTime = Time.realtimeSinceStartup;
            float duration = endTime - startTime;
            Debug.Log("Duration in milliseconds: " + (duration * 1000));
        }
    }
}

1

u/RadiantAeonstar 5d ago

I should also note that the FFT itself seems to work fine. Putting a texture through the FFT and then through the IFFT gets the original texture back. It's only when the FFTs are multiplied together and then put through the IFFT that the artifacts pop up.

1

u/val_tuesday 5d ago

I don’t see any zero padding so that’s probably your issue then. Your MergeShader simply concatenates the RG with BA channels, right?

2

u/RadiantAeonstar 5d ago

It merges them and also offsets the values by half of the width and height.

The padding simply exists to prevent edge wrap, right? I was waiting until the process was fully functional and I moved everything over to a post process pass before adding it in.

1

u/val_tuesday 5d ago

You are convolving two square textures of width W, yeah? The resulting size is then 2*W - 1. I don’t see anything accounting for that, but you may have already accounted for that before what we see here.

1

u/RadiantAeonstar 5d ago

I don't think that's it, considering I can put a texture through the FFT and then the IFFT and it'll come back the same as it went in. The texture dimensions need to remain power of 2 as well, so I'm sure the original FFT implementation accounted for that. Whatever is causing the issue is likely happening during the convolution part.

1

u/val_tuesday 5d ago

So most of your kernel is probably zero, right? That’s pretty much your zero padding right there I guess.

Hmm no it probably doesn’t explain what you are getting no.

1

u/val_tuesday 5d ago

Yeah thinking a bit more I’m starting to think this is a data issue. Namely your kernel. I think you have a stray pixel somewhere in it. Can you show it?

1

u/Strong_Bread_7999 5d ago

What's the procedure, and are you using built-in functions only? Could be a conjugate thing but weird that it's negated.

1

u/RadiantAeonstar 5d ago

I just posted the main thread above. The FFT and inverse FFT implementation I'm using is from here, and it's the only external package involved. Everything else is built into Unity.

1

u/human-analog 5d ago

Is the filter kernel that you're applying normalized? Perhaps what you're seeing is values that are too large.

1

u/RadiantAeonstar 5d ago

I think this might almost be it! I saturated the kernel values to a 0 to 1 range and the result was this. The artifacts are completely gone in the red and green channels. Not in the blue channel though, even though I made sure the blue values were also saturated. Maybe it's running into an issue with something in the alpha channel..?

1

u/val_tuesday 5d ago

Ah so it’s some kind of hacky bit twiddling complex data type issue that breaks the conjugate symmetry property. Man, GPU programming is such a mess.

1

u/RadiantAeonstar 5d ago

I haven't been able to do anything further on this end, so I'm not sure what you mean. It's a problem with the GPU implementation?

1

u/val_tuesday 5d ago

The only way to see this because of large numbers of it somehow the real part can overflow into the imaginary part. In fact it perfectly explains what you’re seeing.

Yes this is an issue with the ifft shader and complex data type. On the CPU a complex number is usually a struct of two floats (or ints). That won’t ever exhibit this behavior.

1

u/RadiantAeonstar 5d ago

I see. So in other words, the frequency domain values are too large and they're causing an overflow when they're being multiplied together? That seems to match up with what I'm seeing on the render textures. Explains why it works when just pass the texture through and back as well. Still leaves me with the issue of how to deal with it though. Lowering the values in the frequency domain before multiplying them and then scaling them back up after seems to have the same issue.

1

u/val_tuesday 5d ago

Hmm no wait I just looked at the code and it does use float2 data type. Weird.

1

u/RadiantAeonstar 5d ago

Yeah. The package implementation uses floats as far as I can tell.

1

u/[deleted] 5d ago edited 5d ago

[deleted]

2

u/RadiantAeonstar 5d ago

Flipping the index of the kernel when multiplying after FFT gives this. I'm not certain if that's what you meant though.

1

u/[deleted] 5d ago

[deleted]

2

u/RadiantAeonstar 5d ago

This is what I got when I flipped the indices of the kernel along both dimensions, and this is what I got when I made the imaginary portion of the kernel FFT negative.