1. Help us improve the editor usability and artist workflows. Join our discussion to provide your feedback.
    Dismiss Notice
  2. We're looking for feedback on Unity Starter Kits! Let us know what you’d like.
    Dismiss Notice
  3. We’re giving 2017.1 beta testers a chance to win t-shirts and a Nintendo Switch. Read more on the blog.
    Dismiss Notice
  4. We want to know how you learned Unity! Help us by taking this quick survey and have a chance at a $25 gift card
    Dismiss Notice
  5. Are you an artist or level designer going to Unite Europe? Join our roundtables there to discuss artist features.
    Dismiss Notice
  6. Unity 5.6 is now released.
    Dismiss Notice
  7. Check out all the fixes for 5.6 on the patch releases page.
    Dismiss Notice

GetInterpolatedLightProbe(), interpreting the coefficients?

Discussion in 'Shaders' started by bluescrn, Nov 6, 2013.

  1. bluescrn

    bluescrn

    Joined:
    Feb 25, 2013
    Posts:
    398
    Has anybody had any success using this function, or seen a working example?

    I've been trying/struggling to use it to approximately light a particle system (as they don't natively support lightprobes).

    But the values I'm getting out of it don't seem to match up to the SH values used by the shaders in the ShadeSH9 function.

    I've got a shader that just writes out the constant term (unity_SHAr.w, unity_SHAg.w, unity_SHAb.w ), and I'm trying to match this up to the differently-ordered coefficients from GetInterpolatedLightProbe(). I thought that the first 3 coefficients would be the ones I needed.

    But at the moment, I can't even get this simplest term to match up... there's no values that come close to the output of the shader (and it's not just an out-by-factor-of-2 issue, already considered that one...)

    Does Unity do further calculations on these coefficients before reshuffling them into shader parameters??
     
  2. bluescrn

    bluescrn

    Joined:
    Feb 25, 2013
    Posts:
    398
    I've discovered that GetInterpolatedLightProbe() does not add any dynamic SH light, including ambient light - this was contributing to my problems, as the scene had a non-zero ambient light.

    But after removing the ambient, the best that I've managed to achieve is this:

    $probes.png

    The centre sphere is lit properly from the lightprobes (via ShadeSH9). The outer quads are attempting to replicate this in script, using GetInterpolatedLightProbe(), and setting their material colour - using code I posted here a while back: http://forum.unity3d.com/threads/136194-Using-light-probe-with-new-Shuriken-particle-system

    But it's still some way away from working properly. It looks closer than it really is, as to I've got a 'magic number' divide-by-two, which shouldn't be necessary, as the shader doesn't do it. And if I reduce both the shader and the script version to just display the constant term, there's quite some difference - I've not managed to get those to match up yet. (Also, in other scenes with different light probe setups, the difference in colour/brightness is much more severe)

    Surely somebody has figured this out, and managed to light a particle system reasonably well using lightprobes?
     
    Last edited: Nov 7, 2013
  3. Geeoff

    Geeoff

    Joined:
    Oct 30, 2013
    Posts:
    2
    Hey bluescrn,

    I was having the same problem but I think I figured it out. Check out the shader and code at the bottom of this PDF:

    [Stupid Spherical Harmonics (SH) Tricks, by Peter-Pike Sloan, Microsoft Corporation]
    http://www.ppsloan.org/publications/StupidSH36.pdf

    It's the exact same shader code that unity uses! I used the logic in SetSHEMapConstants in my scripts and it seems to work in my test scene.

    -Geeoff
     
  4. bluescrn

    bluescrn

    Joined:
    Feb 25, 2013
    Posts:
    398
    Ah, thanks for that - looks like it explains my problems, I'd (incorrectly) assumed that the values returned by GetInterpolatedLightProbe would be ready to feed straight into the shader, but it looks they need a bit of processing first - will give that a try!
     
  5. bluescrn

    bluescrn

    Joined:
    Feb 25, 2013
    Posts:
    398
    Based on the code from that doc, I've now got a solution that works quite nicely. Might be useful to anyone else trying to do the same thing.

    (Would be nicer if particle effects had some support for lightprobes natively, though, or if Unity had a built in function to sample a lightprobe and return an RGB value like this...)

    Code (csharp):
    1.  
    2.  
    3. // -----------------------------------------------------------------------------------------------
    4. // LightProbeUtil
    5. //
    6. // Samples a lightprobe using LightmapSettings.lightProbes.GetInterpolatedLightProbe, giving an
    7. // RGB value for a given world space position and normal.
    8. //
    9. // Useful for lighting particle effects.
    10. // -----------------------------------------------------------------------------------------------
    11.  
    12. using UnityEngine;
    13. using System.Collections;
    14.  
    15. public class LightProbeUtil
    16. {
    17.     static float[]      aSample     = new float[27];    // SH sample consists of 27 floats
    18.     static Vector4[]    avCoeff     = new Vector4[7];   // SH coefficients in 'shader-ready' format
    19.     static Vector3      vRGB        = new Vector3();   
    20.        
    21.     static float        s_fSqrtPI   = (float)Mathf.Sqrt(Mathf.PI);
    22.     static float        fC0         = 1.0f / (2.0f*s_fSqrtPI);
    23.     static float        fC1         = (float)Mathf.Sqrt(3.0f)  / (3.0f*s_fSqrtPI);
    24.     static float        fC2         = (float)Mathf.Sqrt(15.0f) / (8.0f*s_fSqrtPI);
    25.     static float        fC3         = (float)Mathf.Sqrt(5.0f)  / (16.0f*s_fSqrtPI);
    26.     static float        fC4         = 0.5f * fC2;      
    27.    
    28.    
    29.     // ------------------------------------------------------------------------------------------
    30.     // Sample light probes at a given world-space point for a given world-space normal
    31.         // Returns an RGB value
    32.     // ------------------------------------------------------------------------------------------
    33.    
    34.     public static Vector3 SampleLightProbes( Vector3 vPos, Renderer r, Vector3 vNormal3 )
    35.     {              
    36.         Vector4 vNormal;
    37.         vNormal.x = vNormal3.x;
    38.         vNormal.y = vNormal3.y;
    39.         vNormal.z = vNormal3.z;
    40.         vNormal.w = 1.0f;
    41.  
    42.         if ( LightmapSettings.lightProbes!=null )
    43.         {
    44.             // Sample the probes
    45.             LightmapSettings.lightProbes.GetInterpolatedLightProbe( vPos, r, aSample );
    46.                        
    47.             // Convert this sample into 'shader-ready' coefficients
    48.             // (See 'Stupid SH Tricks' doc by  Peter-Pike Sloan, code at the very bottom of the doc)
    49.             // ------------------------------------------------------------------------------------------          
    50.             for ( int iC=0; iC<3; iC++ )   
    51.             {              
    52.                 avCoeff[iC].x =-fC1 * aSample[iC+9];
    53.                 avCoeff[iC].y =-fC1 * aSample[iC+3];
    54.                 avCoeff[iC].z = fC1 * aSample[iC+6];               
    55.                 avCoeff[iC].w = fC0 * aSample[iC+0] - fC3*aSample[iC+18];
    56.             }
    57.            
    58.             for ( int iC=0; iC<3; iC++ )   
    59.             {
    60.                 avCoeff[iC+3].x =        fC2 * aSample[iC+12];
    61.                 avCoeff[iC+3].y =       -fC2 * aSample[iC+15];
    62.                 avCoeff[iC+3].z = 3.0f * fC3 * aSample[iC+18];
    63.                 avCoeff[iC+3].w =       -fC2 * aSample[iC+21];
    64.             }
    65.            
    66.             avCoeff[6].x = fC4 * aSample[24];
    67.             avCoeff[6].y = fC4 * aSample[25];
    68.             avCoeff[6].z = fC4 * aSample[26];
    69.             avCoeff[6].w = 1.0f;
    70.            
    71.            
    72.             // Calculate the RGB value, in the same way as 'ShadeSH9' in the shaders
    73.             // ------------------------------------------------------------------------------------------
    74.            
    75.             // Linear and constant polynomial terms
    76.             vRGB.x = Vector4.Dot( avCoeff[0], vNormal );
    77.             vRGB.y = Vector4.Dot( avCoeff[1], vNormal );
    78.             vRGB.z = Vector4.Dot( avCoeff[2], vNormal );
    79.            
    80.             // 4 of the quadratic polynomials
    81.             Vector4 vB;
    82.             vB.x = vNormal.x*vNormal.y;
    83.             vB.y = vNormal.y*vNormal.z;
    84.             vB.z = vNormal.z*vNormal.z;
    85.             vB.w = vNormal.z*vNormal.x;
    86.            
    87.             vRGB.x += Vector4.Dot( avCoeff[3], vB );
    88.             vRGB.y += Vector4.Dot( avCoeff[4], vB );
    89.             vRGB.z += Vector4.Dot( avCoeff[5], vB );
    90.            
    91.             // Final quadratic polynomial
    92.             float fC = vNormal.x*vNormal.x - vNormal.y*vNormal.y;
    93.             vRGB.x += fC * avCoeff[6].x;
    94.             vRGB.y += fC * avCoeff[6].y;
    95.             vRGB.z += fC * avCoeff[6].z;
    96.            
    97.             // Add the ambient, as that isn't in the probes
    98.             vRGB.x += RenderSettings.ambientLight.r;
    99.             vRGB.y += RenderSettings.ambientLight.g;
    100.             vRGB.z += RenderSettings.ambientLight.b;
    101.            
    102.             return vRGB;
    103.         }  
    104.        
    105.         return Vector3.one;
    106.     }
    107.    
    108.    
    109.     // ------------------------------------------------------------------------------------------
    110.     // Sample light probes at a given world-space point using the world up normal
    111.         // Returns an RGB value
    112.     // (Faster, simplified version, for lighting things like small particle effects)
    113.     // ------------------------------------------------------------------------------------------
    114.    
    115.     public static Vector3 SampleLightProbesUp( Vector3 vPos, Renderer r )
    116.     {              
    117.         if ( LightmapSettings.lightProbes!=null )
    118.         {
    119.             // Sample the probes
    120.             LightmapSettings.lightProbes.GetInterpolatedLightProbe( vPos, r, aSample );
    121.                        
    122.             // Convert this sample into 'shader-ready' coefficients
    123.             // (Simplified for the case of a 0,1,0 normal)
    124.             // ------------------------------------------------------------------------------------------          
    125.             for ( int iC=0; iC<3; iC++ )   
    126.             {                              
    127.                 avCoeff[iC].y =-fC1 * aSample[iC+3];               
    128.                 avCoeff[iC].w = fC0 * aSample[iC+0] - fC3*aSample[iC+18];
    129.             }
    130.            
    131.             avCoeff[6].x = fC4 * aSample[24];
    132.             avCoeff[6].y = fC4 * aSample[25];
    133.             avCoeff[6].z = fC4 * aSample[26];          
    134.            
    135.            
    136.             // Calculate the RGB value, in the same way as 'ShadeSH9' in the shaders
    137.             // (Simplified for the case of a 0,1,0 normal)
    138.             // ------------------------------------------------------------------------------------------
    139.             vRGB.x = avCoeff[0].y +  avCoeff[0].w;
    140.             vRGB.y = avCoeff[1].y +  avCoeff[1].w;
    141.             vRGB.z = avCoeff[2].y +  avCoeff[2].w;
    142.             vRGB.x -= avCoeff[6].x;
    143.             vRGB.y -= avCoeff[6].y;
    144.             vRGB.z -= avCoeff[6].z;
    145.            
    146.             // Add the ambient, as that isn't in the probes
    147.             vRGB.x += RenderSettings.ambientLight.r;
    148.             vRGB.y += RenderSettings.ambientLight.g;
    149.             vRGB.z += RenderSettings.ambientLight.b;
    150.  
    151.             return vRGB;
    152.         }  
    153.        
    154.         return Vector3.one;
    155.     }
    156.  
    157. }
    158.  
     
    colin299, mh114, kebrus and 1 other person like this.
  6. Dolkar

    Dolkar

    Joined:
    Jun 8, 2013
    Posts:
    576
    Now, pass the lightprobe to a shader that samples it per pixel with the particle's normal (usually sphere normals or a normal map) to create very accurately lit particles. Yay.
     
  7. Bas-Smit

    Bas-Smit

    Joined:
    Dec 23, 2012
    Posts:
    39
    Thanks for figuring this out! A more optimized version:

    Code (CSharp):
    1. static Vector3 ShadeSh9(Vector3 normal, float[] cs)
    2. {
    3.     const float c0 = .2820948f;
    4.     const float c1 = .325735f;
    5.     const float c2 = .2731371f;
    6.     const float c3 = .15769578f;
    7.     const float c4 = .1365685f;
    8.  
    9.     var x = normal.x;
    10.     var y = normal.y;
    11.     var z = normal.z;
    12.  
    13.     var x1 = x*y;
    14.     var y1 = y*z;
    15.     var z1 = z*z;
    16.     var w1 = z*x;
    17.  
    18.     var c = x * x - y * y;
    19.  
    20.     var r = C1*(cs[6]*z - cs[9]*x - cs[3]*y) + C0*cs[0] + C2*(cs[12]*x1 - cs[15]*y1 - cs[21]*w1) + C3*cs[18]*z1 + c*C4*cs[24];
    21.     var g = C1*(cs[7]*z - cs[10]*x - cs[4]*y) + C0*cs[1] + C2*(cs[13]*x1 - cs[16]*y1 - cs[22]*w1) + C3*cs[19]*z1 + c*C4*cs[25];
    22.     var b = C1*(cs[8]*z - cs[11]*x - cs[5]*y) + C0*cs[2] + C2*(cs[14]*x1 - cs[17]*y1 - cs[23]*w1) + C3*cs[20]*z1 + c*C4*cs[26];
    23.  
    24.     return new Vector3 {x = r, y = g, z = b};
    25. }
    Another optimization tip is to cache the objects you need to get the coefficients:

    Code (CSharp):
    1. _renderer = GetComponent<SkinnedMeshRenderer>();
    2. _transform = m_renderer.lightProbeAnchor ?? transform;
    3. _lightProbes = LightmapSettings.lightProbes;
     
    Last edited: Mar 17, 2015
    colin299, kebrus and ds44 like this.
  8. DaleMagicalWhale

    DaleMagicalWhale

    Joined:
    Aug 11, 2013
    Posts:
    7
    Hey, just wondering if anyone has successfully used this method with Unity 5.

    I have tried both bluescrn's and Bas Smit's solutions (changing the coefficient indexes over to the new SphericalHarmonicsL2[int, int]), with some success, but the results differ significantly from ShadeSH9 in a shader.
    Here are some examples (using a "toon" shader which samples the light in only two directions):
    ShadeSH9.PNG ShadeSH9Orange.PNG
    CSharpShadeSH9.PNG CSharpShadeSH9Orange.PNG
    The first row of images uses ShadeSH9 in a shader, and the second row uses Bas Smit's method.

    Assuming this method was working previously (and I don't have Unity 4 pro, so I can't check), I can think of two likely reasons this is happening:
    a) Unity 5 encodes the light probes differently.
    b) The SH coefficients aren't in the order I think they are. In altering Bas Smit's script, I just changed:
    cs[0] to cs[0, 0]
    cs[1] to cs[0, 1]
    cs[2] to cs[0, 2]
    cs[3] to cs[1, 0]...

    Any help would be appreciated.
     
    colin299 likes this.
  9. Bas-Smit

    Bas-Smit

    Joined:
    Dec 23, 2012
    Posts:
    39
    My code doesn't add ambient, you can try setting the ambient to black. Also, Unity adds realtime lights to the data from the probes at runtime so disable all lights after baking the probes. Please let us know that fixes it.
     
  10. DaleMagicalWhale

    DaleMagicalWhale

    Joined:
    Aug 11, 2013
    Posts:
    7
    Thank you, however changing the ambient light and disabling lights didn't seem to change anything. All of my lights were set to baked only, and I'm fairly sure that ambient light is now baked into the probes, being sort of bounced into the scene from some giant sphere surrounding it, which makes sense as a way to do it.
     
  11. Bas-Smit

    Bas-Smit

    Joined:
    Dec 23, 2012
    Posts:
    39
    Hmm ok, I'll have to port this to unity 5 soon, I'll post here when I get it to work. That said, it seems like the color is correct, but the intensity is off, bit of a stab in the dark but can you try multiplying the result by 2?
     
  12. DaleMagicalWhale

    DaleMagicalWhale

    Joined:
    Aug 11, 2013
    Posts:
    7
    No luck.
    ShadeSH9Orange.PNG CSharpShadeSH9Orange2.PNG CSharpShadeSH9Orange10.PNG
    From left to right: shader SH9; your code (x2); your code (x10) just to be sure.

    The intensities are different, but to my eye it also seems like the shader version is getting a fair green tinge from the light bouncing off the floor, where the other versions don't. This makes me think perhaps it's sampling in the wrong direction? (I'll try just flipping the vector coordinates around a bit)

    Good luck porting the code over, and thanks for the help.
     
  13. Jde

    Jde

    Joined:
    Oct 2, 2011
    Posts:
    113
    Did you guys manage to get this working in Unity 5? I've got something close enough but I'm not sure that it's "correct". I had to flip the X and Y of my normal vector when sampling for some reason, but not Z.
     
  14. Bas-Smit

    Bas-Smit

    Joined:
    Dec 23, 2012
    Posts:
    39
    Thanks for sharing your findings. We're are set to make the switch to Unity 5 in a week or two, I'll post here if and when I get it to work.
     
    DaleMagicalWhale likes this.
  15. Jde

    Jde

    Joined:
    Oct 2, 2011
    Posts:
    113
    This is what I'm using at the moment. Seems to work ok for me...
    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.Rendering;
    3.  
    4. public class LightProbeUtil
    5. {
    6.     static SphericalHarmonicsL2    aSample;
    7.  
    8.     public static Vector3 SampleLightProbe(Vector3 pos, Renderer r, Vector3 normal)
    9.     {
    10.         LightProbes.GetInterpolatedProbe( pos, r, out aSample );
    11.         return ShadeSh9(normal, aSample);
    12.     }
    13.  
    14.     static Vector3 ShadeSh9(Vector3 normal, SphericalHarmonicsL2 cs)
    15.     {
    16.         const float c0 = .2820948f;
    17.         const float c1 = .325735f;
    18.         const float c2 = .2731371f;
    19.         const float c3 = .15769578f;
    20.         const float c4 = .1365685f;
    21.      
    22.         float x = normal.x;
    23.         float y = normal.y;
    24.         float z = normal.z;
    25.  
    26.         float x1 = x*y;
    27.         float y1 = y*z;
    28.         float z1 = z*z;
    29.         float w1 = z*x;
    30.      
    31.         float c = x * x - y * y;
    32.  
    33.         float r = c1*(cs[0,2]*z - cs[0,3]*x - cs[0,1]*y) + c0*cs[0,0] + c2*(cs[0,4]*x1 - cs[0,5]*y1 - cs[0,7]*w1) + c3*cs[0,6]*z1 + c*c4*cs[0,8];
    34.         float g = c1*(cs[1,2]*z - cs[1,3]*x - cs[1,1]*y) + c0*cs[1,0] + c2*(cs[1,4]*x1 - cs[1,5]*y1 - cs[1,7]*w1) + c3*cs[1,6]*z1 + c*c4*cs[1,8];
    35.         float b = c1*(cs[2,2]*z - cs[2,3]*x - cs[2,1]*y) + c0*cs[2,0] + c2*(cs[2,4]*x1 - cs[2,5]*y1 - cs[2,7]*w1) + c3*cs[2,6]*z1 + c*c4*cs[2,8];
    36.      
    37.         return new Vector3 {x = r, y = g, z = b};
    38.     }
    39. }
     
    RomBinDaHouse and colin299 like this.
  16. Bas-Smit

    Bas-Smit

    Joined:
    Dec 23, 2012
    Posts:
    39
    It seems like baking lightprobes is currently simply broken, these are the result of a single directional light in 4 and 5. Clearly a single light should never light more than half a sphere. You can vote for the issue here

    lightprobes.jpg
     
  17. Bas-Smit

    Bas-Smit

    Joined:
    Dec 23, 2012
    Posts:
    39
    This should do the trick. The coefficients now come with the constants pre-applied. Let me know if it works.


    Edit: bear with me for a minute, I discovered a bug. Im getting there though
     
    Last edited: May 21, 2015
  18. Bas-Smit

    Bas-Smit

    Joined:
    Dec 23, 2012
    Posts:
    39
    Just in case this is useful to anyone, this is how the coefficients get transformed to the seven vectors

    Code (CSharp):
    1. var avCoeff = new Vector4[7];
    2.  
    3. for (int iC = 0; iC < 3; iC++)
    4. {
    5.     avCoeff[iC].x = aSample[iC, 3];
    6.     avCoeff[iC].y = aSample[iC, 1];
    7.     avCoeff[iC].z = aSample[iC, 2];
    8.     avCoeff[iC].w = aSample[iC, 0] - aSample[iC, 6];
    9. }
    10.  
    11. for (int iC = 0; iC < 3; iC++)
    12. {
    13.     avCoeff[iC + 3].x = aSample[iC, 4];
    14.     avCoeff[iC + 3].y = aSample[iC, 5];
    15.     avCoeff[iC + 3].z = 3.0f * aSample[iC, 6];
    16.     avCoeff[iC + 3].w = aSample[iC, 7];
    17. }
    18.  
    19. avCoeff[6].x = aSample[0, 8];
    20. avCoeff[6].y = aSample[1, 8];
    21. avCoeff[6].z = aSample[2, 8];
    22. avCoeff[6].w = 1.0f;
    The order in the array is:

    unity_SHAr
    unity_SHAg
    unity_SHAb
    unity_SHBr
    unity_SHBg
    unity_SHBb
    unity_SHC
     
    Daerst likes this.
  19. Bas-Smit

    Bas-Smit

    Joined:
    Dec 23, 2012
    Posts:
    39
    Ok so here it is, please let me know if it works for you.

    Code (CSharp):
    1. Vector3 ShadeSH9(SphericalHarmonicsL2 l, Vector3 n)
    2. {
    3.     var bx = n.x * n.y;
    4.     var by = n.y * n.z;
    5.     var bz = n.z * n.z * 3;
    6.     var bw = n.z * n.x;
    7.  
    8.     var c = n.x * n.x - n.y * n.y;
    9.  
    10.     return new Vector3
    11.     {
    12.         x = l[0, 3]*n.x + l[0, 1]*n.y + l[0, 2]*n.z + l[0, 0] - l[0, 6] + l[0, 4]*bx + l[0, 5]*by + l[0, 6]*bz + l[0, 7]*bw + c*l[0, 8],
    13.         y = l[1, 3]*n.x + l[1, 1]*n.y + l[1, 2]*n.z + l[1, 0] - l[1, 6] + l[1, 4]*bx + l[1, 5]*by + l[1, 6]*bz + l[1, 7]*bw + c*l[1, 8],
    14.         z = l[2, 3]*n.x + l[2, 1]*n.y + l[2, 2]*n.z + l[2, 0] - l[2, 6] + l[2, 4]*bx + l[2, 5]*by + l[2, 6]*bz + l[2, 7]*bw + c*l[2, 8]
    15.     };
    16. }
    Note that I only tested this without skybox and ambient.
     
    Last edited: May 22, 2015