Spherical harmonics lighting in Jmonkey

Hello all. I was able to get spherical harmonics lighting working with Jmonkey. The brightness value generation is fairly slow, but values can be precalculated and stored if desired. Provides very realistic lighting, great for caves. Thought it might be of use to some :wink: .

Just pass your Geometry to SphericalHarmonics.generateBrightnessValues and set the returned float array to the Size buffer.
For now the shader only supports a single diffuse map texture.

[video]http://www.youtube.com/watch?v=DEYKgSKm7MY[/video]

The code consists of two classes SphericalHarmonics.java and SHSample.java.
Links to pastebins of MatDef and Shader source below.

SphericalHarmonics.java:

[java]

import com.jme3.collision.CollisionResults;
import com.jme3.math.Ray;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.VertexBuffer.Type;
import com.jme3.util.BufferUtils;
import java.io.File;
import java.io.IOException;

import java.nio.FloatBuffer;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
*

  • @author trblair
    */
    public class SphericalHarmonics {

    public SHSample[] samples;
    public double[] lightCoefficients;
    public double[] rotatedCoeffs;
    public int numSamples;
    public int numBands;
    public int sqrtNumSamples;
    public int numFunctions;
    public float EPSILON = 0.01f;
    public double theta = 43.3;
    public double phi = 225;

    public SphericalHarmonics(int sqrtNumSamples, int numBands){

     this.numBands = numBands;
     this.sqrtNumSamples = sqrtNumSamples;
     numSamples = sqrtNumSamples*sqrtNumSamples;
     samples = new SHSample[numSamples];
     numFunctions = numBands*numBands;
     lightCoefficients = new double[numFunctions];
     rotatedCoeffs = new double[numFunctions];
     generateSamples();
     generateLightCoeficiants();
     rotatedCoeffs = rotateSHCoefficients(lightCoefficients,theta,phi);
     writeSamplesToXML();
    

    }

    public SphericalHarmonics(int sqrtNumSamples, int numBands, File xmlSamples){

     System.out.println("Using precalculated spherical harmonics samples");
     this.numBands = numBands;
     this.sqrtNumSamples = sqrtNumSamples;
     numSamples = sqrtNumSamples*sqrtNumSamples;
     this.samples = this.loadSamplesFromXML(xmlSamples);
     numFunctions = numBands*numBands;
     lightCoefficients = new double[numFunctions];
     rotatedCoeffs = new double[numFunctions];
     generateLightCoeficiants();
     rotatedCoeffs = rotateSHCoefficients(lightCoefficients,theta,phi);
    

    }

    public void generateSamples(){

     int index = 0;
     
     for(int i = 0; i < sqrtNumSamples; i++){
         
         for(int j = 0; j < sqrtNumSamples; j++){
             
             double x=(i+((double)Math.random()))/sqrtNumSamples;
             double y=(j+((double)Math.random()))/sqrtNumSamples;
             samples[index] = new SHSample(numBands);
             
             double sample_theta=2.0*Math.acos(Math.sqrt(1.0-x));
             double sample_phi=2.0*Math.PI*y;
    
             samples[index].theta=sample_theta;
             samples[index].phi=sample_phi;
    
             //Convert to cartesians
             samples[index].direction = new Vector3f((float)(Math.sin(sample_theta)*Math.cos(sample_phi)),
                                             (float)(Math.sin(sample_theta)*Math.sin(sample_phi)),
                                             (float)(Math.cos(sample_theta)));
    
             //Compute SH coefficients for this sample
             for(int l=0; l<numBands; ++l){
    
                 for(int m=-l; m<=l; ++m){
    
                         int index2=l*(l+1)+m;
    
                         samples[index].shValues[index2]=shValue(l, m, sample_theta, sample_phi);
                 
                 }
             
             }
    
             ++index;
         
         }
     
     }
    

    }

    private void generateLightCoeficiants(){

     for(int i=0; i<numFunctions; ++i){
     
         lightCoefficients[i]=0.0;
    
         for(int j=0; j<numSamples; ++j){
    
             lightCoefficients[i]+=light(samples[j].theta, samples[j].phi)*samples[j].shValues[i];
             
         }
         
         lightCoefficients[i]*=4*Math.PI/numSamples;
     
     }
    

    }

    public float[] generateBrightnessValues(Geometry geometry){

     Mesh mesh = geometry.getMesh();
     FloatBuffer vertBuff = mesh.getFloatBuffer(Type.Position);
     float[] verts = BufferUtils.getFloatArray(vertBuff);
     FloatBuffer normBuff = mesh.getFloatBuffer(Type.Normal);
     float[] normals = BufferUtils.getFloatArray(normBuff);
     float[] brightness = new float[verts.length/3];
     
     for(int i = 0; i < verts.length; i+=3){
         
         double[] vertCoefficiaets = new double[numFunctions];
         
         for(int set = 0; set < vertCoefficiaets.length; set++){
         
             vertCoefficiaets[set] = 0.0;
         
         }
         
         Vector3f currentVert = new Vector3f(verts[i],verts[i+1],verts[i+2]);
         Vector3f currentNorm = new Vector3f(normals[i],normals[i+1],normals[i+2]);
         
         
         
         for(int j = 0;j<numSamples;j++){
    
             double dot=(double)samples[j].direction.dot(currentNorm);
    
             if(dot>0.0){
    
                 CollisionResults results = new CollisionResults();
                 Ray ray = new Ray(currentVert.add(currentNorm.mult(EPSILON*2)),samples[j].direction);
                 rootNode.collideWith(ray, results);
                 boolean occluded = false;
    
                 if(results.size()>0){
                     occluded = true;
    
                 }
    
                 if(!occluded){
    
                     for(int l = 0;l<numFunctions;l++){
    
                         double contribution=dot*samples[j].shValues[l];
                         vertCoefficiaets[l] += contribution;
    
                     }
    
                 }
    
             }
    
         }
         
         
         
         for(int n = 0; n < numFunctions; n++){
         
             vertCoefficiaets[n]*=4*Math.PI/numSamples;
             
         }
         
         double bright = 0.0;
         
         for(int m = 0;m<numFunctions;m++){
         
             bright+=rotatedCoeffs[m]*vertCoefficiaets[m];
         
         }
         
         brightness[i/3] = (float)bright;
         
     }
     
     return brightness;
    

    }

    public void writeSamplesToXML(){

     try {
     
         DocumentBuilderFactory dbfac = DocumentBuilderFactory.newInstance();
         DocumentBuilder docBuilder = dbfac.newDocumentBuilder();
         Document doc = docBuilder.newDocument();
    
         Element root = doc.createElement("root");
         doc.appendChild(root);
         
         Element body = doc.createElement("samples");
         
         for(int i = 0; i < samples.length; i++){
         
             Element sample = doc.createElement("sample");
             SHSample currentSample = samples[i];
             Double currentTheta = currentSample.theta;
             sample.setAttribute("theta", currentTheta.toString());
             Double currentPhi = currentSample.phi;
             sample.setAttribute("phi", currentPhi.toString());
             
             Float x = currentSample.direction.x;
             sample.setAttribute("x",x.toString());
             Float y = currentSample.direction.y;
             sample.setAttribute("y",y.toString());
             Float z = currentSample.direction.z;
             sample.setAttribute("z",z.toString());
             
             double[] values = currentSample.shValues;
             Integer numValues = values.length;
             sample.setAttribute("numValues",numValues.toString());
             
             for(int j = 0;j<values.length;j++){
                 Element sh = doc.createElement("shValue");
                 Double value = values[j];
                 sh.setAttribute("value", value.toString());
                 Integer index = j;
                 sh.setAttribute("index", index.toString());
                 sample.appendChild(sh);
             
             }
             
             body.appendChild(sample);
         
         }
         
         root.appendChild(body);
         TransformerFactory transfac = TransformerFactory.newInstance();
         Transformer trans = transfac.newTransformer();
         DOMSource source = new DOMSource(doc);
         StreamResult result = new StreamResult(new File("shSamples.xml"));
         trans.transform(source, result);
         
         
     } catch (ParserConfigurationException ex) {
     
         Logger.getLogger(SphericalHarmonics.class.getName()).log(Level.SEVERE, null, ex);
     
     } catch (TransformerConfigurationException ex) {
     
         Logger.getLogger(SphericalHarmonics.class.getName()).log(Level.SEVERE, null, ex);
     
     } catch (TransformerException ex) {
     
         Logger.getLogger(SphericalHarmonics.class.getName()).log(Level.SEVERE, null, ex);
     
     }
    

    }

    public SHSample[] loadSamplesFromXML(File xmlSamples){

     try {
     
         SHSample[] shSamples = new SHSample[this.sqrtNumSamples * sqrtNumSamples];
         DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
         DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
         Document doc = dBuilder.parse(xmlSamples);
         doc.getDocumentElement().normalize();
         NodeList sampleNodes = doc.getElementsByTagName("sample");
           
         
         for(int i = 0; i < sampleNodes.getLength(); i++){
    
             SHSample currentSample = new SHSample(numBands);
             org.w3c.dom.Node sampleNode = sampleNodes.item(i);
             Element sampleElement = (Element) sampleNode;
             double theta = Double.parseDouble(sampleElement.getAttribute("theta"));
             currentSample.theta = theta;
             double phi = Double.parseDouble(sampleElement.getAttribute("phi"));
             currentSample.phi = phi;
             float x = Float.parseFloat(sampleElement.getAttribute("x"));
             float y = Float.parseFloat(sampleElement.getAttribute("y"));
             float z = Float.parseFloat(sampleElement.getAttribute("z"));
             Vector3f direction = new Vector3f(x,y,z);
             currentSample.direction = direction;
             boolean nodeCheck = sampleNode.hasChildNodes();
             NodeList sampleValues = sampleNode.getChildNodes();
             double[] values = new double[sampleValues.getLength()];
    
                 for(int j = 0;j<sampleValues.getLength();j++){
    
                     org.w3c.dom.Node valueNode = sampleValues.item(j);
    
                     if(!(valueNode instanceof Element)){
    
                         continue;
    
                     }
    
                     Element valueElement = (Element) valueNode;
                     double value = Double.parseDouble(valueElement.getAttribute("value"));
                     int index = Integer.parseInt(valueElement.getAttribute("index"));
                     values[index] = value;
    
                 }
             
             currentSample.shValues = values;
             shSamples[i] = currentSample;
         
           }
           
     return shSamples;  
       
     } catch (ParserConfigurationException ex) {
     
         Logger.getLogger(InGameState.class.getName()).log(Level.SEVERE, null, ex);
       
     } catch (SAXException ex) { 
     
         Logger.getLogger(SphericalHarmonics.class.getName()).log(Level.SEVERE, null, ex);
     
     } catch (IOException ex) {
     
         Logger.getLogger(SphericalHarmonics.class.getName()).log(Level.SEVERE, null, ex);
     
     } 
     
     return null;
    

    }

    private double light(double theta, double phi){

     if(theta<Math.PI/6){
    
         return 1;
     
     }else{
     
         return 0;
     
     }
    

    }

    //Sample a spherical harmonic basis function Y(l, m) at a point on the unit sphere
    private double shValue(int l, int m, double theta, double phi){

     double sqrt2=Math.sqrt(2.0);
    

    if(m==0){

         return kValue(l, 0)*pValue(l, m, Math.cos(theta));
     
     }
    
     if(m>0){
     
         return sqrt2*kValue(l, m)*Math.cos(m*phi)*pValue(l, m, Math.cos(theta));
     
     }
    

    //m<0
    return sqrt2*kValue(l,-m)Math.sin(-mphi)*pValue(l, -m, Math.cos(theta));

    }

    private double pValue(int l, int m, double x){

     //First generate the value of P(m, m) at x
    

    double pmm=1.0;

    if(m>0){

         double sqrtOneMinusX2=Math.sqrt(1.0-x*x);
    
         double fact=1.0;
    
         for(int i = 1; i &lt;= m; ++i){
              
             pmm*=(-fact)*sqrtOneMinusX2;
             fact+=2.0;
         
         }
    
     }
    

    //If l==m, P(l, m)==P(m, m)
    if(l==m){

         return pmm;
     
     }
    

    //Use rule 3 to calculate P(m+1, m) from P(m, m)
    double pmp1m=x*(2.0*m+1.0)*pmm;

    //If l==m+1, P(l, m)==P(m+1, m)
    if(l==m+1){

         return pmp1m;
     
     }
    

    //Otherwise, l>m+1.
    //Iterate rule 1 to get the result
    double plm=0.0;

    for(int i = m+2; i <= l; ++i){

         plm=((2.0*i-1.0)*x*pmp1m-(i+m-1.0)*pmm)/(i-m);
         pmm=pmp1m;
         pmp1m=plm;
    
     }
    

    return plm;
    }

    //Calculate the normalisation constant for an SH function
    //No need to use |m| since SH always passes positive m
    double kValue(int l, int m){

     double temp=((2.0*l+1.0)*factorial(l-m))/((4.0*Math.PI)*factorial(l+m));
    

    return Math.sqrt(temp);

    }

    //Calculate n! (n>=0)
    private int factorial(int n){

     if(n&lt;=1){
         
         return 1;
     
     }
    
     int result=n;
    

    while(–n > 1){

         result*=n;
     
     }
    
     return result;
    

    }

    private double[] getZRotationMatrix(int band, double[] entries, double angle){

     //Calculate the size of the matrix
    

    int size=2*band+1;

    //Convert angle to radians
    angle*=Math.PI/180.0;

    //Entry index
    int currentEntry=0;

    //Loop through the rows and columns of the matrix
    for(int i = 0; i < size; ++i){

         for(int j = 0; j &lt; size; ++j, ++currentEntry){
    
             //Initialise this entry to zero
             entries[currentEntry]=0.0;
    
     	//For the central row (i=(size-1)/2), entry is 1 if j==i, else zero
     	if(i==(size-1)/2){
     		
                         if(j==i){
     	
                             entries[currentEntry]=1.0;
                             
                         }
     		
     	}
    
     	//For i&lt;(size-1)/2, entry is cos if j==i or sin if j==size-i-1
     	//The angle used is k*angle where k=(size-1)/2-i
     	if(i&lt;(size-1)/2){
     		
                         int k=(size-1)/2-i;
    
     		if(j==i){
     		
                                 entries[currentEntry]=Math.cos(k*angle);
                             
                             }
     		
                             if(j==size-i-1){
     		
                                 entries[currentEntry]= -Math.sin(k*angle);
                             
                             }
     		
     	}
    
     	//For i&gt;(size-1)/2, entry is cos if j==i or -sin if j==size-i-1
     	//The angle used is k*angle where k=i-(size-1)/2
     	if(i&gt;(size-1)/2){
     		
                         int k=i-(size-1)/2;
    
     		if(j==i){
     	
                                 entries[currentEntry]=Math.cos(k*angle);
                             
                             }
     		
                             if(j==size-i-1){
     		
                                 entries[currentEntry]=Math.sin(k*angle);
                             
                             }
     		
     	}
     
             }
    
     }
    

    return entries;

    }

    private double[] getX90DegreeRotationMatrix(int band, double[] entries){

    if(band==0){

         entries[0]= 1.0;
    
     }
    

    if(band==1){

         entries[0]= 0.0;
         entries[1]= 1.0;
         entries[2]= 0.0;
         entries[3]= -1.0;
         entries[4]= 0.0;
         entries[5]= 0.0;
         entries[6]= 0.0;
         entries[7]= 0.0;
         entries[8]= 1.0;
    
     }
     
     return entries;
    

    }

    private double[] applyMatrix(int size, double[] matrix, boolean transpose, double[] inVector, double[] outVector){

     //Loop through entries
    

    for(int i = 0; i < size; ++i){

         //Clear this entry of outVector
         outVector[i]=0.0;
    
     //Loop through matrix row/column
     for(int j=0; j&lt;size; ++j){
     
                 if(transpose){
     
                     outVector[i]+=matrix[j*size+i]*inVector[j];
                     
                 }else{
     
                     outVector[i]+=matrix[i*size+j]*inVector[j];
                     
                 }
             
             }
    
     }
     
     return outVector;
    

    }

    private double[] rotateSHCoefficients(double[] unrotatedCoeffs,double theta, double phi){

     double[] rotated = new double[numFunctions];
    

    for(int i=0; i<numFunctions; ++i){

         rotated[i]=unrotatedCoeffs[i];
     
     }
    
     //Band 0 coefficient is unchanged
    

    rotated[0]=unrotatedCoeffs[0];

    //Rotate band 1 coefficients
    if(numBands>1){

         //Get the rotation matrices for band 1 (want to apply Z1*Xt*Z2*X)
         double[] band1X = new double[9];
         double[] band1Z1 = new double[9];
         double[] band1Z2 = new double[9];
    
         band1Z1 = getZRotationMatrix(1, band1Z1, phi);
         band1X = getX90DegreeRotationMatrix(1, band1X);
         band1Z2 = getZRotationMatrix(1, band1Z2, theta);
    
         //Create space to hold the intermediate results
         double[] band1A = new double[3];
         double[] band1B = new double[3];
         double[] band1C = new double[3];
    
         //Apply the matrices
         band1A = applyMatrix(3, band1X, false, unrotatedCoeffs, band1A);
         band1B = applyMatrix(3, band1Z2, false, band1A, band1B);
         band1C = applyMatrix(3, band1X, true, band1B, band1C);
    
         rotated = applyMatrix(3, band1Z1, false, band1C, rotated);
    
     }
     
     return rotated;
    

    }

}

[/java]

SHSample.java:

[java]

import com.jme3.math.Vector3f;

/**
*

  • @author trblair
    */
    public class SHSample {

    public double theta, phi;
    public Vector3f direction;
    public double[] shValues;

    public SHSample(int numBands){

     int numFunctions = numBands*numBands;
     shValues = new double[numFunctions];
    

    }

}

[/java]

Sphericals Harmonics Material Definition

Spherical Harmonics Fragment Shader

Spherical Harmonics Vertex Shader

6 Likes

Sorry posted the wrong video!

[video]http://www.youtube.com/watch?v=62Q2j_Kv73o&edit=vd[/video]

2 Likes

A capuchin baby cries every time there’s a youtube video without sound (doubly so when there’s barely audible background noise! :stuck_out_tongue: ).

Wicked cool effect though. @mifth should be here to harvest your saplings in no time :smiley: