4
$\begingroup$

I have been experimenting with Matcaps, and also using procedural textures with Matcap vector mapping. I understand that Matcaps are using Sphere mapping, and so when applied to non-spherical objects, there is stretching.

enter image description here

In this example image, we see a simple sphere and Suzanne with basic procedural noise. Note that on Suzanne's face, brow ridges, and ears, the texture is stretched out.

What I would like to do is get a grayscale mask of this stretching area. So the sphere would be black, and on Suzanne, the previously mentioned areas would be a gradient up to white. As far as I can tell, this would be an area where the normals stay flat.

Is this possible, and if so, how?

$\endgroup$
  • 1
    $\begingroup$ Could you indicate your exact node setting so that there is no confusion about it? $\endgroup$ – lemon Mar 24 '17 at 8:38
  • 2
    $\begingroup$ The stretching is not on/off case, the amount of stretching is gradual on all of suzanne's surface. The question is, where would you like the threshold to be? $\endgroup$ – Jaroslav Jerryno Novotny Mar 24 '17 at 11:57
  • $\begingroup$ @lemon it's the same as in the linked question. Normal transformed from Object to Camera, then mapped with x and y location of .5 and x and y scale of .5 $\endgroup$ – Drudge Mar 24 '17 at 20:30
  • $\begingroup$ @Jerryno I should have said grayscale from 0 to 1, not a full black and white. I can always color ramp it for full black and white. Have updated the question. Thanks! $\endgroup$ – Drudge Mar 24 '17 at 20:30
  • $\begingroup$ @Jerryno I am using the edited version with the transform set to Normal. $\endgroup$ – Drudge Mar 24 '17 at 20:59
4
+50
$\begingroup$

Quick Method

Basically, what we want to do is find the Dot Product between the normals of a bounding ellipsoid projected on to the mesh, and the normals of the mesh. First, enable Auto Smooth normals.

enter image description here

Next, add a Normal Edit modifier after a Subsurf Modifier (4 to 5 subdivisions) and set the Angle on the Normal Edit modifier to about 80 degrees. This will give us the approximate projected normals of the bounding ellipsoid.

enter image description here

Finally, add a Geometry Node and take the Dot Product of the (modified) Normal and (original) True Normal inputs (with vector transform) (it doesn't matter).

![enter image description here

Result:

enter image description here

To increase the contrast, you can square the output with a Math node.

More Mathematically Accurate, Plus it Doesn't Mess Up Your Normals

The method above finds the projected normals of the bounding ellipsoid using Auto Smooth in combination with a Normal Edit modifier. This has some limitations: 1) the resolution of your map is tied to the number of faces you have, slowing down rendertimes, 2) the Normal texture coordinates are messed up, so to have "normal"-looking normals, you have to use True Normal, which is again tied to the face count, and 3) the map just isn't as detailed as it could be, due to limitations of the Normal Edit modifier.

If you are willing to work with OSL (Open Shading Language), you can avoid all this with some code.

First, we need to figure out a way to project the normals of the bounding ellipsoid on to the mesh (in this case, Suzanne). I found this helpful, if dense, article here. The important part we need to pull out of this are these equations:

$$y_0=x_0\left(1+\frac{t}{e_0^2}\right),y_1=x_1\left(1+\frac{t}{e_1^2}\right),y_2=x_2\left(1+\frac{t}{e_2^2}\right)$$

where the x's are the coordinates of a point on the mesh, the y's are the coordinates of a point on the ellipsoid, the e's are the axis-aligned radii of the ellipsoid, and t is a certain value that makes everything right (more on that later).

Let's see if this method even works. I drew up a proof of concept in Desmos. You can look at it here. By moving the red dot labeled "Point on Mesh", we can see the algorithm finding the closest point on the ellipse, we just have to manually adjust the t value.

While having to manually find the t value isn't ideal, it is only one variable. We can use a for loop to iterate through a bunch of values and figure out which value best gives a point that solves the equation for an ellipsoid:

$$\left(\frac{x_0}{e_0}\right)^2+\left(\frac{x_1}{e_1}\right)^2+\left(\frac{x_2}{e_2}\right)^2=1$$

(naming conventions the same as above)

So, what should our inputs and outputs be? We need the Object coordinates as an input to give us the coordinates of our mesh point, and the Dimensions of our object (X, Y, and Z), both vectors. Since we are calculating t with a for loop, we'll need an iterations count, and a step value to set the detail. As far as outputs go, we need a map of the stretch (dot product), and, just for fun, the ellipsoidal normals.

Let's start coding! Create a new text file in the Text Editor (or something else) and name it stretch.osl. Set inputs and outputs, and declare our variables:

#include "stdosl.h"

shader stretch(
    /*Set Inputs*/
    vector Object = P,
    vector Dimensions = vector(1),
    int iterations = 200,
    float step = 0.05,
    /*Set Outputs*/
    output float Fac = 0,
    output normal Ellip = 0
)
{
    /*Point on Mesh*/
    float X0 = Object[0];
    float X1 = Object[1];
    float X2 = Object[2];
    /*Radii of Ellipsoid*/
    float E0 = Dimensions[0];
    float E1 = Dimensions[1];
    float E2 = Dimensions[2];
    /*Point on Ellipsoid*/
    float Y0 = X0;
    float Y1 = X1;
    float Y2 = X2;
    /*t value*/
    float t = 0;
}

Next, we need to figure out how accurate we want to be. Since we don't want our algorithm to take ridiculously long (or fall prey to floating point errors), we need to figure out how many significant figures is reasonable. I chose 3. Unfortunately, OSL doesn't round to a certain number of significant figures, only to whole integers, so we have to write our own significant figures rounding function. Place it at the beginning of the code, below #include "stdosl.h":

#include "stdosl.h"

float sfround(float num, int d){
    num = num * pow(10, d - 1);
    num = round(num);
    num = num / pow(10, d - 1);
    return num;
}

For the next part of our algorithm, we need a way to figure out how close we are to having an accurate t value. As we saw in the Desmos test, when the t value is correct, plugging numbers back in will yield 1. Let's write a small function to calculate this for us. Place it after the variable declarations:

    float onEllipsoid(){
        return sfround(pow(Y0, 2) / pow(E0, 2) + pow(Y1, 2) / pow(E1, 2) + pow(Y2, 2) / pow(E2, 2), 3);
    }

Now we can write our for loop! Place it below the onEllipsoid() function. I've commented in what everything means.

    for(int i = 0; i < iterations; i++){
        if(onEllipsoid() < 1.0){ /*Point inside ellipsoid*/
            t = t + step;
        }else if(onEllipsoid() > 1.0){ /*Point outside ellipsoid*/
            t = t - step;
        }else{ /*Point on ellipsoid*/
            break;
        }
        /*Refine point estimate*/
        Y0 = X0 * (1 + t / pow(E0, 2));
        Y1 = X1 * (1 + t / pow(E1, 2));
        Y2 = X2 * (1 + t / pow(E2, 2));
    }

To finish it off, we need to turn the point we got into a normal. We can use this answer off of Math SE to accomplish this.

    normal elliptic = normalize(2 * vector(Y0 / pow(E0, 2), Y1 / pow(E1, 2), Y2 / pow(E2, 2)));

Now, all that is left to do is to write our answer to the outputs:

    Ellip = elliptic;
    Fac = dot(elliptic, N);

TL;DR - Just Give Me the Code

#include "stdosl.h"

float sfround(float num, int d){
    num = num * pow(10, d - 1);
    num = round(num);
    num = num / pow(10, d - 1);
    return num;
}

shader stretch(
    /*Set Inputs*/
    vector Object = P,
    vector Dimensions = vector(1),
    int iterations = 200,
    float step = 0.05,
    /*Set Outputs*/
    output float Fac = 0,
    output normal Ellip = 0
)
{
    /*Point on Mesh*/
    float X0 = Object[0];
    float X1 = Object[1];
    float X2 = Object[2];
    /*Radii of Ellipsoid*/
    float E0 = Dimensions[0];
    float E1 = Dimensions[1];
    float E2 = Dimensions[2];
    /*Point on Ellipsoid*/
    float Y0 = X0;
    float Y1 = X1;
    float Y2 = X2;
    /*t value*/
    float t = 0;

    float onEllipsoid(){
        return sfround(pow(Y0, 2) / pow(E0, 2) + pow(Y1, 2) / pow(E1, 2) + pow(Y2, 2) / pow(E2, 2), 3);
    }
    for(int i = 0; i < iterations; i++){
        if(onEllipsoid() < 1.0){ /*Point inside ellipsoid*/
            t = t + step;
        }else if(onEllipsoid() > 1.0){ /*Point outside ellipsoid*/
            t = t - step;
        }else{ /*Point on ellipsoid*/
            break;
        }
        /*Refine point estimate*/
        Y0 = X0 * (1 + t / pow(E0, 2));
        Y1 = X1 * (1 + t / pow(E1, 2));
        Y2 = X2 * (1 + t / pow(E2, 2));
    }
    normal elliptic = normalize(2 * vector(Y0 / pow(E0, 2), Y1 / pow(E1, 2), Y2 / pow(E2, 2)));

    Ellip = elliptic;
    Fac = dot(elliptic, N);
}

Well, now that we have the code, how do we use it? First, we need to enable OSL under the render settings.

enter image description here

Next, add a Script node with Shift + A > Script > Script and select the stretch.osl file, or whatever you named it. We need to give it the Object coordinates, and the dimensions of your object. Since my Suzanne object doesn't rotate, deform, or scale, I just copy-pasted the information out of the 3D Viewport (below the transforms in the N panel). If this doesn't work for your situation, it wouldn't be hard to implement drivers. Anyway, just copy the X, Y, and Z values into a Combine XYZ (Shift + A > Converter > Combine XYZ ) node and plug it in.

enter image description here

As far as the iterations and step values go, the default step should be fine; adjust the iterations until there isn't a visible change in the Fac output (it happens really slowly (depending on your step value), so use increments of at least 25 at first).

Now, a word on the Fac output. Black is more distorted, not white, and subtracting the output from 1 doesn't work to make it that way. Here's why: using Suzanne as our example, there are some areas like the ears or the back of the chin where the closest point is facing into the mesh. In the Fac output, this will look like a big black patch. In reality, it is just a negative value, showing a different kind of distortion. If we plug the Fac value into a Math node (Shift + A > Converter > Math ) set to Absolute, we can see that they are just negative values. Here is a GIF switching back and forth:

enter image description here

Anyway, hope this helps. It was fun to work on ;-). Congrats for reading this far!

$\endgroup$
1
$\begingroup$

I think you have the concept wrong of matcap materials. They are purely based on direction, ea the black circle is a sphere. and the view you have of the sphere is how a surface angle as (camera to object), reflects to a equal surface angle of your object.

ea a random rotated cube, always has surface angles that would be the same as some points on a sphere.

So thats why stretching isnt related to matcap materials.

There is no stretch property in cycles nodes. There is only pointiness, that takes into acount angle between faces (great for dirt/rust setups).

Other ways might perhaps be painting canvas, to color based upon how near something is to a seccond (sphere) object, and have the factor back in your cycles nodes again.

Or use a particle system unborn particles as point density texture in a volume material.. looks kinda creepy ;)

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.