Without any doubt the more complicated tool I did so far.
This is the second part of the solution for the smalls particules in the lighthouse of the movie "The End". We wanted to illuminate the particules with the wall lighting. To avoid the huge 3D render time, I find a 2D solution.
I discovered last year the blink scripts, and I have to say that it gives nuke a whole new power !
This gizmo relight an object using, instead of a point, a luminance passe and a position pass. For that, it uses two blink script, one for optimisation, one for the actual computation.
Basically, the gizmo compares each pixel of the target position passe to every pixels of the lighting position passe. If there are close enough, it relights the target object according to the distance and the amount of light in the area.
Let's get into it step by step :
The tolerance knob controls the black point of the Grade node, that allows the user to choose from which amount of light the relight is active.
Here is the first expression node. Simple expression meaning :
if alpha < 0 : alpha = 0; else : alpha = 0
The first blink script
As I said, the gizmo compares each pixel of the target position passe to every pixels of the lighting position passe.
For an HD image as we had for the movie. It means :
1280 * 720 = 921600
921600 pixels compared each time to 921600 pixels
So.... 921600*921600 = 849346560000 comparation. This is to much.
So I had to optimized the thing. First, the reformat downscale a bit the lighting passe: That helps.
Then, this blink script check one time every pixel. It keeps and orders only the one that are not black in the alpha (which is the luminance).
Instead of having that :
This blink script gives us that :
Because the blink scripts are mutli core process, I used the alpha of the pixel (0,-1) as a mutex. The actual position in the image is saved in this pixel.
Here is the code :
kernel glow3D_Kernel: ImageComputationKernel<ePixelWise> { Image<eRead, eAccessPoint, eEdgeClamped> src; //target position Image<eWrite, eAccessRandom> dst; //the output image //In define(), parameters can be given labels and default values. param: int width; void define() { defineParam(width, "Width", 1280); } void process() { // this fonction is executed for every pixel if (src(3) != 0){ //if the alpha is not black int position = dst(0,-1,3); // get the pixel index where it should write from the mutex pixel dst(0,-1,3) = (float)(position+1); // set the mutex pixel to the next pixel index int x = position % width; // get x and y from the pixel index int y = position / width; dst(x,y) = src(); // write the actual pixel value in the actual position } } };The color is still the position pass, and the alpha is still the luminance. But the pixels are ordered, we don't have to check the whole image for each pixel now.
The second blink script.
This is where everything happens. This blink script takes two entry the light position pass and the target object position pass.
The script runs through the target object image, for each pixel, if the alpha is not 0, it goes trough the light position passe.
For each pixel in the light position passe, it checks if the pixel is in the zone defined by the user. If it is. It weight the luminance value by the distance and the decay set by the user.
The result is added to the light amount of the target object pixel and the denominator is incremented.
The denominator is actually the number of pixel affecting the target pixel
After running through the light position passe, the luminance amount is divided by the denominator. And the value is wrote in the target object pixel.
Here is the code :
kernel glow3D_Kernel: ImageComputationKernel<ePixelWise> { Image<eRead, eAccessPoint, eEdgeClamped> posDst; //target position Image<eRead, eAccessRandom, eEdgeClamped> posRef; //reference position pass Image<eWrite> dst; //the output image param: float size; //Size of the glow float decay; int width; //In define(), parameters can be given labels and default values. void define() { defineParam(size, "Size", 1.0f); defineParam(decay, "Decay", 1.0f); defineParam(width, "Width", 1280); } local: int x1; int x2; int y1; int y2; //The init() function is run before any calls to kernel(). void init() { } float distance(float ref0,float targ0, float ref1, float targ1, float ref2, float targ2) {// this fonction calculate the distance, thx pythagore :) float dist = sqrt((targ0-ref0)*(targ0-ref0) +(targ1-ref1)*(targ1-ref1) +(targ2-ref2)*(targ2-ref2) ); return dist; } void process(int2 pos) { if (posDst(3) != 0) { //if alpha is not 0 float tempDist; int denominator = 0; float lightAmount = 0; float weight; int i = 0; int x = i % width; int y = i / width; while (posRef(x,y,3) != 0) { //this loop run through the light passe, but stops when alpha = 0 tempDist = distance(posRef(x,y,0), posDst(0), posRef(x,y,1), posDst(1), posRef(x,y,2), posDst(2)); // get the distqnce between the two pixel if (tempDist<size) { // if the pixel is in the area weight = 1-(tempDist/size); // calcul the weight with the distance weight = pow(ponderation,decay); // add the decay lightAmount += (posRef(x,y,3)*ponderation); // add the actual light impact to the light amount denominator++; // add one pixel impacting the target pixel } i += 1; x = i % width; y = i / width; } if (denominateur != 0) { dst() = lightAmount/denominator; // divide the light amount by the amount of pixel impacting } else { dst() = 0; } } else {dst() = 0;} } };This way, the light is calculated in relation with the distance, the light amount, and the decay and gives a real smooth results.
Here is a result of the particle system used with this gizmo :
Since this method doesn't compute the pixel that are outside the field of view, I overscaned the 3D renders to have a better results.
I hope you understood, if you have any question, please ask ! :)