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 .
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 <= 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<=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 < 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<(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<(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>(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>(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<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