GetInterpolatedLightProbe(), interpreting the coefficients?

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??

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:

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?

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]

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

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!

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…)

// -----------------------------------------------------------------------------------------------
// LightProbeUtil
//
// Samples a lightprobe using LightmapSettings.lightProbes.GetInterpolatedLightProbe, giving an 
// RGB value for a given world space position and normal.
//
// Useful for lighting particle effects.
// -----------------------------------------------------------------------------------------------

using UnityEngine;
using System.Collections;

public class LightProbeUtil
{
	static float[]		aSample 	= new float[27];	// SH sample consists of 27 floats
	static Vector4[] 	avCoeff 	= new Vector4[7];	// SH coefficients in 'shader-ready' format
	static Vector3 	  	vRGB 		= new Vector3();	
		
	static float 		s_fSqrtPI 	= (float)Mathf.Sqrt(Mathf.PI);
	static float 		fC0	 		= 1.0f / (2.0f*s_fSqrtPI);
	static float 		fC1	 		= (float)Mathf.Sqrt(3.0f)  / (3.0f*s_fSqrtPI);
	static float 		fC2	 		= (float)Mathf.Sqrt(15.0f) / (8.0f*s_fSqrtPI);
	static float 		fC3	 		= (float)Mathf.Sqrt(5.0f)  / (16.0f*s_fSqrtPI);
	static float 		fC4	 		= 0.5f * fC2;		
	
	
	// ------------------------------------------------------------------------------------------
	// Sample light probes at a given world-space point for a given world-space normal
        // Returns an RGB value
	// ------------------------------------------------------------------------------------------
	
	public static Vector3 SampleLightProbes( Vector3 vPos, Renderer r, Vector3 vNormal3 ) 
	{				
		Vector4 vNormal;
		vNormal.x = vNormal3.x;
		vNormal.y = vNormal3.y;
		vNormal.z = vNormal3.z;
		vNormal.w = 1.0f;

		if ( LightmapSettings.lightProbes!=null )
		{
			// Sample the probes
			LightmapSettings.lightProbes.GetInterpolatedLightProbe( vPos, r, aSample );	
						
			// Convert this sample into 'shader-ready' coefficients
			// (See 'Stupid SH Tricks' doc by  Peter-Pike Sloan, code at the very bottom of the doc)
			// ------------------------------------------------------------------------------------------			
			for ( int iC=0; iC<3; iC++ )	
			{				
				avCoeff[iC].x =-fC1 * aSample[iC+9];
				avCoeff[iC].y =-fC1 * aSample[iC+3];
				avCoeff[iC].z = fC1 * aSample[iC+6];				
				avCoeff[iC].w = fC0 * aSample[iC+0] - fC3*aSample[iC+18];
			}
			
			for ( int iC=0; iC<3; iC++ )	
			{
				avCoeff[iC+3].x = 		 fC2 * aSample[iC+12];
				avCoeff[iC+3].y =		-fC2 * aSample[iC+15];
				avCoeff[iC+3].z = 3.0f * fC3 * aSample[iC+18];
				avCoeff[iC+3].w =		-fC2 * aSample[iC+21];
			}
			
			avCoeff[6].x = fC4 * aSample[24];
			avCoeff[6].y = fC4 * aSample[25];
			avCoeff[6].z = fC4 * aSample[26];
			avCoeff[6].w = 1.0f;
			
			
			// Calculate the RGB value, in the same way as 'ShadeSH9' in the shaders
			// ------------------------------------------------------------------------------------------
			
			// Linear and constant polynomial terms
			vRGB.x = Vector4.Dot( avCoeff[0], vNormal );
			vRGB.y = Vector4.Dot( avCoeff[1], vNormal );
			vRGB.z = Vector4.Dot( avCoeff[2], vNormal );
			
			// 4 of the quadratic polynomials
			Vector4 vB;
			vB.x = vNormal.x*vNormal.y;
			vB.y = vNormal.y*vNormal.z;
			vB.z = vNormal.z*vNormal.z;
			vB.w = vNormal.z*vNormal.x;
			
			vRGB.x += Vector4.Dot( avCoeff[3], vB );
			vRGB.y += Vector4.Dot( avCoeff[4], vB );
			vRGB.z += Vector4.Dot( avCoeff[5], vB );
			
			// Final quadratic polynomial
			float fC = vNormal.x*vNormal.x - vNormal.y*vNormal.y;
			vRGB.x += fC * avCoeff[6].x;
			vRGB.y += fC * avCoeff[6].y;
			vRGB.z += fC * avCoeff[6].z;
			
			// Add the ambient, as that isn't in the probes
			vRGB.x += RenderSettings.ambientLight.r;
			vRGB.y += RenderSettings.ambientLight.g;
			vRGB.z += RenderSettings.ambientLight.b;
			
			return vRGB;
		}	
		
		return Vector3.one;
	}
	
	
	// ------------------------------------------------------------------------------------------
	// Sample light probes at a given world-space point using the world up normal
        // Returns an RGB value
	// (Faster, simplified version, for lighting things like small particle effects)
	// ------------------------------------------------------------------------------------------
	
	public static Vector3 SampleLightProbesUp( Vector3 vPos, Renderer r ) 
	{				
		if ( LightmapSettings.lightProbes!=null )
		{
			// Sample the probes
			LightmapSettings.lightProbes.GetInterpolatedLightProbe( vPos, r, aSample );	
						
			// Convert this sample into 'shader-ready' coefficients
			// (Simplified for the case of a 0,1,0 normal)
			// ------------------------------------------------------------------------------------------			
			for ( int iC=0; iC<3; iC++ )	
			{								
				avCoeff[iC].y =-fC1 * aSample[iC+3];				
				avCoeff[iC].w = fC0 * aSample[iC+0] - fC3*aSample[iC+18];
			}
			
			avCoeff[6].x = fC4 * aSample[24];
			avCoeff[6].y = fC4 * aSample[25];
			avCoeff[6].z = fC4 * aSample[26];			
			
			
			// Calculate the RGB value, in the same way as 'ShadeSH9' in the shaders
			// (Simplified for the case of a 0,1,0 normal)
			// ------------------------------------------------------------------------------------------
			vRGB.x = avCoeff[0].y +  avCoeff[0].w;
			vRGB.y = avCoeff[1].y +  avCoeff[1].w;
			vRGB.z = avCoeff[2].y +  avCoeff[2].w;
			vRGB.x -= avCoeff[6].x;
			vRGB.y -= avCoeff[6].y;
			vRGB.z -= avCoeff[6].z;
			
			// Add the ambient, as that isn't in the probes
			vRGB.x += RenderSettings.ambientLight.r;
			vRGB.y += RenderSettings.ambientLight.g;
			vRGB.z += RenderSettings.ambientLight.b;

			return vRGB;
		}	
		
		return Vector3.one;
	}

}
4 Likes

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.

Thanks for figuring this out! A more optimized version:

static Vector3 ShadeSh9(Vector3 normal, float[] cs)
{
    const float c0 = .2820948f;
    const float c1 = .325735f;
    const float c2 = .2731371f;
    const float c3 = .15769578f;
    const float c4 = .1365685f;

    var x = normal.x;
    var y = normal.y;
    var z = normal.z;

    var x1 = x*y;
    var y1 = y*z;
    var z1 = z*z;
    var w1 = z*x;

    var c = x * x - y * y;
 
    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];
    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];
    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];

    return new Vector3 {x = r, y = g, z = b};
}

Another optimization tip is to cache the objects you need to get the coefficients:

_renderer = GetComponent<SkinnedMeshRenderer>();
_transform = m_renderer.lightProbeAnchor ?? transform;
_lightProbes = LightmapSettings.lightProbes;
3 Likes

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):



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.

1 Like

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.

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.

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?

No luck.


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.

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.

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.

1 Like

This is what I’m using at the moment. Seems to work ok for me…

using UnityEngine;
using UnityEngine.Rendering;

public class LightProbeUtil
{
    static SphericalHarmonicsL2    aSample;

    public static Vector3 SampleLightProbe(Vector3 pos, Renderer r, Vector3 normal)
    {
        LightProbes.GetInterpolatedProbe( pos, r, out aSample );
        return ShadeSh9(normal, aSample);
    }

    static Vector3 ShadeSh9(Vector3 normal, SphericalHarmonicsL2 cs)
    {
        const float c0 = .2820948f;
        const float c1 = .325735f;
        const float c2 = .2731371f;
        const float c3 = .15769578f;
        const float c4 = .1365685f;
      
        float x = normal.x;
        float y = normal.y;
        float z = normal.z;

        float x1 = x*y;
        float y1 = y*z;
        float z1 = z*z;
        float w1 = z*x;
      
        float c = x * x - y * y;

        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];
        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];
        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];
      
        return new Vector3 {x = r, y = g, z = b};
    }
}
2 Likes

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

2121708--139438--lightprobes.jpg

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

Just in case this is useful to anyone, this is how the coefficients get transformed to the seven vectors

var avCoeff = new Vector4[7];

for (int iC = 0; iC < 3; iC++)
{
    avCoeff[iC].x = aSample[iC, 3];
    avCoeff[iC].y = aSample[iC, 1];
    avCoeff[iC].z = aSample[iC, 2];
    avCoeff[iC].w = aSample[iC, 0] - aSample[iC, 6];
}

for (int iC = 0; iC < 3; iC++)
{
    avCoeff[iC + 3].x = aSample[iC, 4];
    avCoeff[iC + 3].y = aSample[iC, 5];
    avCoeff[iC + 3].z = 3.0f * aSample[iC, 6];
    avCoeff[iC + 3].w = aSample[iC, 7];
}

avCoeff[6].x = aSample[0, 8];
avCoeff[6].y = aSample[1, 8];
avCoeff[6].z = aSample[2, 8];
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

8 Likes

Ok so here it is, please let me know if it works for you.

Vector3 ShadeSH9(SphericalHarmonicsL2 l, Vector3 n)
{
    var bx = n.x * n.y;
    var by = n.y * n.z;
    var bz = n.z * n.z * 3;
    var bw = n.z * n.x;

    var c = n.x * n.x - n.y * n.y;

    return new Vector3
    {
        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],
        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],
        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]
    };
}

Note that I only tested this without skybox and ambient.

3 Likes