Search Unity

Math behind computing world space view direction

Discussion in 'Shaders' started by chayanvinayak, Jan 4, 2016.

  1. chayanvinayak

    chayanvinayak

    Joined:
    Jul 11, 2012
    Posts:
    24
    Hello,

    Can someone please point me to the explanation behind computing world space view direction using following equation:
    float3 worldSpaceViewDir = _WorldSpaceCameraPos.xyz - worldPos;

    1. Why do we subtract worldspacePos from worldspaceCameraPos to get viewDirection.
    2. May be, I should ask what exactly is world-space view direction?

    When I try to do the math, following is what I get.
    Example :
    a point is located at (0,5,0) world-position and camera is located at (0,0,50) world-position
    and to get world-viewDirection:

    worldSpaceViewDir = _WorldSpaceCameraPos.xyz - worldPos;
    = (0,0,50) - (0,5,0)
    = (0,-5,50)

    Howcome this is view direction?


    Thanks
     
    Last edited: Jan 4, 2016
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    Because it's not really, that's the unnormalized world position to camera vector. There's a joke that graphics programing is just having an even number of negative errors. I'll explain.

    A common use for a view dir is for fresnel, the basic math for which is:
    float fresnel = 1.0 - saturate(dot(worldNormal.xyz, worldPosToCameraDir.xyz));

    Note the worldPosToCameraDir, that is the same as -worldSpaceViewDir, but we'll get back to that.

    So world space view dir is:
    float3 worldSpaceViewDir = normalize(worldPos.xyz - _WorldSpaceCameraPos.xyz);

    However if you calculate the normalized value in the vertex shader and pass it to the pixel shader the interpolated value won't be correct (linear interpolating between two normalized vectors will produce an unnormalized vector) and still needs to be normalized in the pixel shader before use. Really you just want to pass the unnormalized offset vector and normalize the interpolated value in the pixel shader. So that means the above turns into:
    vertex shader
    o.worldSpaceViewDir = worldPos.xyz - _WorldSpaceCameraPos.xyz;

    pixel shader
    float3 worldSpaceViewDir = normalize(i.worldSpaceViewDir);

    Now lets go back and look at the basic fresnel calculation above. Remember how it's the dot product of worldNormal and worldPosToCameraDir? Well, many people will just do:
    float fresnel = 1.0 - saturate(dot(worldNormal.xyz, normalize(worldSpaceViewDir.xyz)));

    The result of that is a negative dot product, as the world normal is pointing away from the surface and the worldSpaceViewDir in the proper form is pointing towards the surface. Since it's negative the saturate will clamp it to zero and you'll just get black and most people are confused. What you want to do here is do -worldSpaceViewDir.xyz but most people just try swapping the initial worldSpaceViewDir calculation and behold, it works!

    So, now we have lots of working code with:
    float3 worldSpaceViewDir = _WorldSpaceCameraPos.xyz - worldPos.xyz;
    Even though that's actually worldPosToCameraDir!

    So now the final working (but subtly wrong!) code is:
    vertex shader
    o.worldSpaceViewDir = worldPos.xyz - _WorldSpaceCameraPos.xyz; // Not the view dir!

    pixel shader
    float fresnel = 1.0 - saturate(dot(worldNormal.xyz, normalize(i.worldSpaceViewDir)));
     
    Last edited: Jan 5, 2016
  3. chayanvinayak

    chayanvinayak

    Joined:
    Jul 11, 2012
    Posts:
    24
    great! Thanks for taking time to explain.
    This gave me a good understanding of how Fresnel works and what exactly is worldSpaceViewDir.

    But still dont understanding the Cartesian (geometric) explanation behind:

    float3 worldSpaceViewDir = _WorldSpaceCameraPos.xyz - worldPos;

    As you suggested I tried the same thing with normalized values of worldSpaceCameraPos and worldPos and I got the same results

    Thanks!
     
  4. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    So the unnormalized vector (in the correct order) is the positional offset of the world position from the camera. In the original inverted order it's the offset position of the camera from the world position.

    Here's another way to think about it. You have a camera and another object in world space, if you subtract the position of the camera from the camera it'll be at zero. If you subtract the camera position from the object's position it'll now be in the same relative position from the camera as it was before. Normalize that value and that's the relative world space direction toward that object.


    Perhaps you're also getting confused by the term world space view dir, this is the direction from the center of the camera that point, so the direction changes for every pixel on screen. Something you might call the world space camera dir could be different, that might be the direction the camera is pointed and be constant across the view. Calling that the world space view dir could be an equally valid name for it, but in this case it's not. Perhaps worldCameraToPosDir would be more descriptive.
     
  5. chayanvinayak

    chayanvinayak

    Joined:
    Jul 11, 2012
    Posts:
    24
    Thank you so much!
     
  6. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    1,933
    Do you have a source for when/who started this abomination?

    (I'm losing my mind debugging shaders and running into insanity like this ... so many examples and references that name things literally incorrectly, along with others that use terms correctly ... but of course the two are fundamentally incompatible)
     
  7. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    Naming things is hard.

    Technically it is a view direction, just an un-normalized world space view direction... which is the same thing as the world space camera to position offset, or un-normalized direction. In this context calling it a view direction is "wrong" more because it is confusing rather than actually strictly wrong.

    Humorously I was wrong as I called it the worldPosToCameraDir when it should be the worldSpaceCameraToPosOffset. :rolleyes: Shame on me from >3 years ago.
     
  8. atomicjoe

    atomicjoe

    Joined:
    Apr 10, 2013
    Posts:
    1,869
    Hey, that was me just 10 minutes ago! LOL
    Seriously, what would I do without your shader math tricks @bgolus :D
    THANK YOU!

    I have been wondering for a long time: is there any performance penalty for using a variable as negative?
    I mean, is (X*Y) faster than (-X*Y) on a shader?
    I recall seeing a GDC shader optimization video were the guy reviewed the graphics assembler generated by several commonly used calculations and functions and I believe the "plus" or "minus" of a variable was free, along with the SATURATE function.
    What was weirdly slow were the MIN and MAX functions, which you can perform WAY faster by just using a conditional, as long as the conditional is in the form of X = X > 0.0 ? X : 0.0 (and it works! even on mobile!)
     
  9. atomicjoe

    atomicjoe

    Joined:
    Apr 10, 2013
    Posts:
    1,869
    Also POW was as slow as multiplying the variable 8 times, so you can just multiply up to 7 times a variable by itself and it will be faster than using POW.
    On mobile POW is even slower and should be avoided in the pixel shader like the plague.
     
  10. koirat

    koirat

    Joined:
    Jul 7, 2012
    Posts:
    2,073
    No change is free.
    You can just change your code so there is no change.
     
  11. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    It’s totally free. There’s going to be zero measurable cost in using the negative of a value.

    This one can be free. Other times it’s not.
    Code (CSharp):
    1. // free
    2. float val = saturate(foo * bar);
    3. // the asm for this is:
    4. mul v1, v2, v3 clamp // clamp is the saturate
    5.  
    6. // not free
    7. float val = foo * saturate(bar);
    8. // the asm for this is:
    9. max v2, v2, v2 clamp // the max does nothing, just exists so the clamp can happen
    10. mul v1, v2, v3
    Now if something else is done to
    bar
    before this, and it’s only ever used later as
    saturate(bar)
    , I would expect shader compilers to notice this and move the saturate into the previous instruction applied to
    bar
    , and thus it’s free again.

    This is going to be highly dependent on the hardware. On some GPUs the ternary (
    x > 0.0 ? x : 0.0
    ) is going to be faster, on others it’ll compile to the exact same code as what
    max(x, 0.0)
    produces.

    Using pow with a floating point value, material property, or input semantic as the exponent is a fixed cost, which depends on the hardware. Could be anywhere between 9 and 3 cycles.
    Code (csharp):
    1. log v2, v2 // could be 1 or 4 cycles
    2. mul v0, v2, v0
    3. exp v0, v0 // could be 1 or 4 cycles
    If you use a hard coded integer value, even if it’s presented as a float, the shader compiler may end up making that exactly the same instructions as manually multiplying it, or will use the fixed cost pow if it’s especially cheap for that device. Mobile compilers are probably bad at both “native” pow, and detecting when to use manual multiplication instead of the pow function, so there’s value in being explicit there.
     
    Last edited: Oct 25, 2020
    atomicjoe likes this.
  12. koirat

    koirat

    Joined:
    Jul 7, 2012
    Posts:
    2,073
    You mean there is a machine code that loads number as negative into a register or stack with the same speed as loading unchanged number ?

    In my understanding if there is no static optimization during compilation we need additional instruction to negate number.
    I tried to find some info about this but my short google-fu gave me no result.
     
  13. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    I mean the asm functions have the option to make any input negative as part of the function. Most asm functions are constant time, even with negative inputs or saturate.
     
    atomicjoe likes this.
  14. atomicjoe

    atomicjoe

    Joined:
    Apr 10, 2013
    Posts:
    1,869
    As per the GDC video, compilers can be REALLY DUMB and make things even slower.
    Things like using an Half floating point variable X, assigning it a value of 0.0 and the compiler forcing a Float to Half conversion right there...
    I recall when I was programming for the OUYA, I noticed my shaders ran WAY faster once I changed all the integer constants like X=0 to explicit floating points like X=0.0 for floating point variables.
    Seriously, that's something any compiler should be able to correct by itself, but they don't.
    I now explicitly mark my Half constant values like 0.0h to be sure it doesn't make precision conversions accidentally.
    It doesn't do anything on desktop, but it's potentially saving me some cycles here and there on mobile.
     
  15. koirat

    koirat

    Joined:
    Jul 7, 2012
    Posts:
    2,073
    actually

    half x = 0.0
    and conversion is not surprising.

    half x = 0
    and conversion would surprise me.

    Just take a look at c# for example
    float x = 0.0;
    is an error.


    I consider it a good behavior adding f or h at the end is not a problem.
    I just think that compiler should throw an error instead of conversion.
     
  16. atomicjoe

    atomicjoe

    Joined:
    Apr 10, 2013
    Posts:
    1,869
    Actually, I find C# to be extremely obnoxious not wanting to understand what I mean when I write
    float x=0;

    It's completely stupid to be forced to write that little "f" at the end of every single floating point number you write and consider double precision floating point to be the default for numbers.
    Go ahead and try that:

    double x = 0;


    No error in C# :rolleyes:
     
  17. atomicjoe

    atomicjoe

    Joined:
    Apr 10, 2013
    Posts:
    1,869
    Seriously, C# should use some automatic type casting for constants, unless you specify you want a very strict enforcement of types using some compiler directive.
    Maybe Unity is forcing a strict compiler option?
     
  18. koirat

    koirat

    Joined:
    Jul 7, 2012
    Posts:
    2,073
    float x = 0;
    will not give you error
    but
    float x=0.0;
    will.

    Believe me it is a good convention.
     
  19. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    Literals with decimals in c# are doubles. Literals with decimals in HLSL are floats. HLSL (and GLSL) have implicit conversion between most number types, and will do them at compile time.

    Also, in HLSL:
    half x = 0.0;

    Is an error, because HLSL has no
    half
    . That’s a Unity thing, and
    half
    is just
    #define half float
    . But even if it wasn’t, and you use
    min16float
    or
    double
    , both of which are real HLSL number type, it just does the implicit conversion, meaning the value may overflow or be clipped to 32 bits, unless you use
    h
    or
    L
    suffixes.
     
  20. atomicjoe

    atomicjoe

    Joined:
    Apr 10, 2013
    Posts:
    1,869
    bgolus likes this.
  21. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    touché
     
    atomicjoe likes this.
  22. atomicjoe

    atomicjoe

    Joined:
    Apr 10, 2013
    Posts:
    1,869
    Hahaha!
     
  23. Oducceu

    Oducceu

    Joined:
    Aug 13, 2013
    Posts:
    2
    I"d like to add that Fresnel is something different, not exactly you are described above.

    Part of incoming light is reflected back and part is consumed by surface. The proportion of reflected and consumed light is Fresnel.

    We can calculate it with Schlick"s aproximation of Fresnel:

    Fschlick = Fo + (1.0 - Fo) * ndotv^5

    where Fo is coefficient of reflection,
    ndotv must be in 0.0 - 1.0 range

    For dielectrics Fo is approximately 4%. With pure metals another story. There are reflecting all incoming light. So Fo is 100% and changing light color to its own color.

    Thus we have:

    if (metal == false) {
    diffuseColor = lightColor * baseColor;
    fresnelReflection = 0.04 + (1.0 - 0.04) * ndotv^5;

    return lerp (diffuseColor, lightColor, fresnelReflecton);
    } else if (metal == true) {
    return lightColor * baseColor;
    }

    Must say that diffuseColor its not just lightColor * baseColor, its a lot of things like Minaert Falloff, Refraction, and others, but its just simple example to describe Fresnel.
     
    Last edited: Oct 29, 2021