Oil Painting Post-process Effect in Unreal Engine



Oil painting post-effect is a rather common filter routinely found in the graphics software such as GIMP, or Photoshop. It allows you to give your image a more "blocky" and artistic feeling. It is also surprisingly easy to create via post-processing material in Unreal Engine.

Idea  

The idea behind the algorithm is relatively simple and rather similar to the Guassian blurring filter. For each pixel we need to search its neighborhood and find intensities of the neighboring pixels. The intensities are then divided into bins and the most common intensity is used to create new pixel color. 

I suggest reading this (with pseudocode) or this (with C++ code) page for deeper understanding of the issue.

Code

Directly within Unreal Engine, you will only need a simple setup to make the shader work. We will use SceneTexture:PostprocessInput0  to get the image that is to be rendered to the user, a constant to tweak with the radius of the effect, texture coordinates of the processed pixel and the custom node that will hold the majority of the computation.

Setup the nodes as follows:



Use following code within Custom Expression code field:

 int TexIndex = 14;

 int intensityCount[10];
 float avgR[10];
 float avgG[10];
 float avgB[10];
 

 for (int iLevel = 0; iLevel < 10; iLevel++){
     intensityCount[iLevel] = 0;
     avgR[iLevel] = 0.0;
     avgG[iLevel] = 0.0;
     avgB[iLevel] = 0.0;
  }


 //COUNT INTENSITIES
 uv *= 0.5;
 for (int i = 0; i < radius; ++i)
 {
     int offsetI = -1 *(radius / 2) + i;
     float v = uv.y + offsetI * invSize.y;
     int temp = i * radius;
     for (int j = 0; j < radius; ++j)
    {
         int offsetJ = -(radius / 2) + j;
         float u = uv.x + offsetJ * invSize.x;
         float2 uvShifted = uv + float2(u, v);
         float3 tex = SceneTextureLookup(uvShifted, TexIndex, false);

        float currentIntensity = ((tex.r + tex.g + tex.b) / 3 * 10);
        intensityCount[currentIntensity]++;
        avgR[currentIntensity] += tex.r;
        avgG[currentIntensity] += tex.g;
        avgB[currentIntensity] += tex.b;
    }
  }


 float maxIntensity = 0;
 int maxIndex = 0;

 //FIND CORRECT MOST COMMON INTENSITY LEVEL
 for(int cLevel = 0; cLevel < 10; cLevel++){
     if(intensityCount[cLevel] > maxIntensity){
         maxIntensity = intensityCount[cLevel];
         maxIndex = cLevel;
     }
  }

 float newR = avgR[maxIndex] / maxIntensity;
 float newG = avgG[maxIndex] / maxIntensity;
 float newB = avgB[maxIndex] / maxIntensity;

 float4 res = float4(newR, newG, newB, 1.0);

 return res;

The Custom node then returns the float4 representing the color of the processed pixel that can be directly connected to the Output node, or further processed.

Result

You can see the effect using Effect Radius = 10 picture below.


While following image shows the scene without applying the filter.



Following video shows the filter running on Soul City map.


Issues

- Since the shader code needs to search the neighborhood, it's obvious that with increased "Effect Radius" parameter this computation complexity increases. Parameters between 5 and 10 should make the effect pronounced enough for the most cases and still keep performance relatively stable.
- Currently the shader doesn't use adaptive radius based on the scene depth, so objects in background seem more blurred than images in foreground (this may or may not be desirable)
- If you are using UE 4.19+ you may get a strange issue where part of the screen is black. Refer to this article for solution

References

Oil Paint Effect by Santhosh_G - C++ CPU code for the Oil Painting effect along with the explanation of the filtering process.
The Supercomputing Blog on Oil Painting Algorithm - pseudocode CPU code of which my code was based off, along with short explanation of the filtering process.
Gaussian Blur Example - containing GPU code for UE4 used as the base for neighborhood search
Soul: City - free assets from Epic Games used for testing of the filter

Comments

  1. its not working i did everything but the scene texture just has the brown error texture.

    ReplyDelete
  2. Replies
    1. Yes sir! Sorry for late reply, this tutorial (as most of the others I'm posting) was written for UE 4.18., but I tested it on 4.20.

      Check the issues section of the tutorial, mainly the last point, that could help you solve the problem. If it doesn't let me know and we can try to resolve it together.

      Delete
    2. Hi, thanks for the tutorial. My post process material shows up as just pure white. Wondering what I'm doing wrong. Probably a lot of things. :) Also using 4.18.

      Delete
    3. You forgot to enter the code into the nodes code box I however have the problem of everything i see taking up only 75% of the screen

      Delete
    4. hey, check this out, might be helpful https://ferkizue.blogspot.com/2018/07/fixing-wrong-scene-size-in-unreal-419.html

      Delete
  3. Hi, thanks for the tutorial. My post process material shows up as just pure white. Wondering what I'm doing wrong. Probably a lot of things. :) Also using 4.18.

    ReplyDelete
    Replies
    1. hey, you can you post your setup and/or code? Are you getting any error? Can't say I've ever encountered screen being all white with this shader, but we will figure it out

      Delete
    2. I am having this problem as well was it ever solved?

      Delete
  4. Hey, this looks great but I can't get it to work at all. In 4.18 it results in an entirely black image and in 4.20 it results in the usual "brown edge" effect for Post Process materials. Here are my material setups and such: https://imgur.com/a/8pNf9Xv Hope you an help.

    ReplyDelete
  5. Hi there, I wondered if there was a way to cull this effect so that the skysphere is not effected. Am I right in thinking you could use scene depth to achieve this. Could you let me know if you have any thoughts or advice on how to achieve this as essentially all I want to effect is the landscape and the buildings and leave the sky as it is normally.

    ReplyDelete
    Replies
    1. Hey, certainly! You can use Scene Depth to sample the depth and if it's too much, use PostProcessing color, otherwise use the filter. I think the scene depth changed a little bit since 4.19, so you might need to give it a shot and see if it works.

      Alternatively, you can use Custom Depth Buffer to pick only the objects to which you want to apply to filter, see https://ferkizue.blogspot.com/2018/08/partial-post-process-effect-application.html
      If you have the actual object for the skyshape, then you can render it to Custom Depth Buffer and check in postprocessing material if given pixel is in custom depth buffer and at the same time there is not something in scene depth that is closer to camera. Hope it makes at least some sense.

      Delete
    2. Hi Zuzana, thanks for the speedy reply. I will give this a try as I am a noob but your tutorials have been really helpful in learning and understanding this topic. Hopefully I have some luck with this else no doubt I will be back asking for some help haha

      Delete
    3. Always happy to help :) Feel free to drop me PM at my twitter account here https://twitter.com/FerkiZue if you need help, should be faster than via comments

      Delete
  6. Hello Zuzana, thanks for the tutorial. I tested this code in both 4.20 and 4.18 but it only works in 4.18. Your issues section can not fix it too, could you check your code in 4.20 again? thanks!

    ReplyDelete
    Replies
    1. Hey!

      have you tried to go through Issues section? If you add the pin and connect it with the scene texture, you also need to delete the first line in the Custom node. If this doesn't help, try turning on shader development mode and let me know what the error is.

      Unfortunately I don't have UE available right now, so I won't be able to post 4.20 tweaks until New Year

      Delete
  7. Hi. I followed your instructions, but I have a weird issue where overly white areas of the image (Such as the sun, or on 2D sprites) are turquoise.

    https://imgur.com/a/todCTXk

    ReplyDelete
    Replies
    1. Hey, unfortunately I never tested it on scene with 2D sprites and frankly I haven't worked with 2D sprites much when it comes to UE. I am currently swamped but I will try to take a look and get some sprites in. If this happens to you on UE sun too, then I must say never happened to me before, either, but things changed a tad in 4.20, so I will need to investigate.

      Delete
  8. Getting this error...

    [SM5] /Engine/Generated/Material.ush(1521,18-43): error X3004: undeclared identifier 'GetPostProcessInputSize'

    when I create a new pin for the InvSize, and connect it up I get this error:
    [SM5] /Engine/Generated/Material.ush(1521,18-43): error X3004: undeclared identifier 'GetPostProcessInputSize'


    ReplyDelete
  9. OK... I changed the first line of code to this:

    float2 invSize = InvSize;

    it works but cuts out all emissive materials they turn black...

    ReplyDelete
  10. uh... ok looks like its just making black any value above in the render? mainly hot reflections and glows

    ReplyDelete
  11. sorry a question in the material when you have the custom node what does the output node connect to for the actual material?

    ReplyDelete
  12. Sorry as a followup it seems i can get the effect to work however it scrunches the resolution, has anyone com across this?

    ReplyDelete
    Replies
    1. Hey, this could help https://ferkizue.blogspot.com/2018/07/fixing-wrong-scene-size-in-unreal-419.html

      Delete

Post a Comment

Popular Posts