Add support for light culling or "filtering".

When rendering a geometry, light filtering is performed against the geometrys' world lights
to avoid rendering lights which will not effect the rendering outcome.
If the light is outside the camera frustum OR the light is outside the model's bounding volume,
then it will not be rendered.
experimental
shadowislord 10 years ago
parent 75a67d0611
commit 1c0d798707
  1. 14
      jme3-core/src/main/java/com/jme3/light/AmbientLight.java
  2. 91
      jme3-core/src/main/java/com/jme3/light/DefaultLightFilter.java
  3. 13
      jme3-core/src/main/java/com/jme3/light/DirectionalLight.java
  4. 37
      jme3-core/src/main/java/com/jme3/light/Light.java
  5. 72
      jme3-core/src/main/java/com/jme3/light/LightFilter.java
  6. 32
      jme3-core/src/main/java/com/jme3/light/PointLight.java
  7. 73
      jme3-core/src/main/java/com/jme3/light/SpotLight.java
  8. 31
      jme3-core/src/main/java/com/jme3/material/Material.java
  9. 4
      jme3-core/src/main/java/com/jme3/renderer/Camera.java
  10. 28
      jme3-core/src/main/java/com/jme3/renderer/RenderManager.java

@ -31,7 +31,11 @@
*/
package com.jme3.light;
import com.jme3.bounding.BoundingBox;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.scene.Spatial;
import com.jme3.util.TempVars;
/**
* An ambient light adds a constant color to the scene.
@ -45,6 +49,16 @@ import com.jme3.scene.Spatial;
*/
public class AmbientLight extends Light {
@Override
public boolean intersectsBox(BoundingBox box, TempVars vars) {
return true;
}
@Override
public boolean intersectsFrustum(Camera camera, TempVars vars) {
return true;
}
@Override
public void computeLastDistance(Spatial owner) {
}

@ -0,0 +1,91 @@
/*
* Copyright (c) 2009-2014 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.light;
import com.jme3.bounding.BoundingBox;
import com.jme3.bounding.BoundingSphere;
import com.jme3.bounding.BoundingVolume;
import com.jme3.renderer.Camera;
import com.jme3.scene.Geometry;
import com.jme3.util.TempVars;
import java.util.HashSet;
public final class DefaultLightFilter implements LightFilter {
private Camera camera;
private final HashSet<Light> processedLights = new HashSet<Light>();
public void setCamera(Camera camera) {
this.camera = camera;
for (Light light : processedLights) {
light.frustumCheckNeeded = true;
}
}
public void filterLights(Geometry geometry, LightList filteredLightList) {
TempVars vars = TempVars.get();
try {
LightList worldLights = geometry.getWorldLightList();
for (int i = 0; i < worldLights.size(); i++) {
Light light = worldLights.get(i);
if (light.frustumCheckNeeded) {
processedLights.add(light);
light.frustumCheckNeeded = false;
light.intersectsFrustum = light.intersectsFrustum(camera, vars);
}
if (!light.intersectsFrustum) {
continue;
}
BoundingVolume bv = geometry.getWorldBound();
if (bv instanceof BoundingBox) {
if (!light.intersectsBox((BoundingBox)bv, vars)) {
continue;
}
} else if (bv instanceof BoundingSphere) {
if (!Float.isInfinite( ((BoundingSphere)bv).getRadius() )) {
// Non-infinite bounding sphere... Not supported yet.
throw new UnsupportedOperationException("Only AABB supported for now");
}
}
filteredLightList.add(light);
}
} finally {
vars.release();
}
}
}

@ -31,12 +31,15 @@
*/
package com.jme3.light;
import com.jme3.bounding.BoundingBox;
import com.jme3.export.InputCapsule;
import com.jme3.export.JmeExporter;
import com.jme3.export.JmeImporter;
import com.jme3.export.OutputCapsule;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.scene.Spatial;
import com.jme3.util.TempVars;
import java.io.IOException;
/**
@ -81,6 +84,16 @@ public class DirectionalLight extends Light {
}
}
@Override
public boolean intersectsBox(BoundingBox box, TempVars vars) {
return true;
}
@Override
public boolean intersectsFrustum(Camera camera, TempVars vars) {
return true;
}
@Override
public Type getType() {
return Type.Directional;

@ -31,9 +31,12 @@
*/
package com.jme3.light;
import com.jme3.bounding.BoundingBox;
import com.jme3.export.*;
import com.jme3.math.ColorRGBA;
import com.jme3.renderer.Camera;
import com.jme3.scene.Spatial;
import com.jme3.util.TempVars;
import java.io.IOException;
/**
@ -64,8 +67,8 @@ public abstract class Light implements Savable, Cloneable {
/**
* Spot light.
* <p>
* Not supported by jMonkeyEngine
*
* @see SpotLight
*/
Spot(2),
@ -109,6 +112,9 @@ public abstract class Light implements Savable, Cloneable {
*/
protected String name;
boolean frustumCheckNeeded = true;
boolean intersectsFrustum = false;
/**
* Returns the color of the light.
*
@ -169,6 +175,33 @@ public abstract class Light implements Savable, Cloneable {
}
*/
/**
* Determines if the light intersects with the given bounding box.
* <p>
* For non-local lights, such as {@link DirectionalLight directional lights},
* {@link AmbientLight ambient lights}, or {@link PointLight point lights}
* without influence radius, this method should always return true.
*
* @param box The box to check intersection against.
* @param vars TempVars in case it is needed.
*
* @return True if the light intersects the box, false otherwise.
*/
public abstract boolean intersectsBox(BoundingBox box, TempVars vars);
/**
* Determines if the lgiht intersects with the given camera frustum.
*
* For non-local lights, such as {@link DirectionalLight directional lights},
* {@link AmbientLight ambient lights}, or {@link PointLight point lights}
* without influence radius, this method should always return true.
*
* @param camera The camera frustum to check intersection against.
* @param vars TempVars in case it is needed.
* @return True if the light intersects the frustum, false otherwise.
*/
public abstract boolean intersectsFrustum(Camera camera, TempVars vars);
@Override
public Light clone(){
try {

@ -0,0 +1,72 @@
/*
* Copyright (c) 2009-2014 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.light;
import com.jme3.renderer.Camera;
import com.jme3.scene.Geometry;
/**
* <code>LightFilter</code> is used to determine which lights should be
* rendered for a particular {@link Geometry} + {@link Camera} combination.
*
* @author Kirill Vainer
*/
public interface LightFilter {
/**
* Sets the camera for which future filtering is to be done against in
* {@link #filterLights(com.jme3.scene.Geometry, com.jme3.light.LightList)}.
*
* @param camera The camera to perform light filtering against.
*/
public void setCamera(Camera camera);
/**
* Determine which lights on the {@link Geometry#getWorldLightList() world
* light list} are to be rendered.
* <p>
* The simplest implementation (e.g. one that performs no filtering) would
* simply copy the contents of {@link Geometry#getWorldLightList()} to
* {@code filteredLightList}.
* <p>
* An advanced implementation would determine if the light intersects
* the {@link Geometry#getWorldBound() geometry's bounding volume} and if
* the light intersects the frustum of the camera set in
* {@link #setCamera(com.jme3.renderer.Camera)} as well as sort the lights
* according to some "influence" criteria - this will then provide
* an optimal set of lights that should be used for rendering.
*
* @param geometry The geometry for which the light filtering is performed.
* @param filteredLightList The results are to be stored here.
*/
public void filterLights(Geometry geometry, LightList filteredLightList);
}

@ -31,13 +31,19 @@
*/
package com.jme3.light;
import com.jme3.bounding.BoundingBox;
import com.jme3.bounding.BoundingSphere;
import com.jme3.bounding.BoundingVolume;
import com.jme3.export.InputCapsule;
import com.jme3.export.JmeExporter;
import com.jme3.export.JmeImporter;
import com.jme3.export.OutputCapsule;
import com.jme3.math.FastMath;
import com.jme3.math.Plane;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.scene.Spatial;
import com.jme3.util.TempVars;
import java.io.IOException;
/**
@ -134,6 +140,32 @@ public class PointLight extends Light {
return Light.Type.Point;
}
@Override
public boolean intersectsBox(BoundingBox box, TempVars vars) {
if (this.radius == 0) {
return true;
} else {
// Sphere v. box collision
return FastMath.abs(box.getCenter().x - position.x) < radius + box.getXExtent()
&& FastMath.abs(box.getCenter().y - position.y) < radius + box.getYExtent()
&& FastMath.abs(box.getCenter().z - position.z) < radius + box.getZExtent();
}
}
@Override
public boolean intersectsFrustum(Camera camera, TempVars vars) {
if (this.radius == 0) {
return true;
} else {
for (int i = 5; i >= 0; i--) {
if (camera.getWorldPlane(i).pseudoDistance(position) <= -radius) {
return false;
}
}
return true;
}
}
@Override
public void write(JmeExporter ex) throws IOException {
super.write(ex);

@ -31,11 +31,14 @@
*/
package com.jme3.light;
import com.jme3.bounding.BoundingBox;
import com.jme3.bounding.BoundingVolume;
import com.jme3.export.*;
import com.jme3.math.FastMath;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.scene.Spatial;
import com.jme3.util.TempVars;
import java.io.IOException;
/**
@ -62,15 +65,19 @@ public class SpotLight extends Light implements Savable {
protected float invSpotRange = 1 / 100;
protected float packedAngleCos=0;
protected float outerAngleCosSqr, outerAngleSinSqr;
protected float outerAngleSinRcp;
public SpotLight() {
super();
computePackedCos();
computeAngleParameters();
}
private void computePackedCos() {
private void computeAngleParameters() {
float innerCos = FastMath.cos(spotInnerAngle);
float outerCos = FastMath.cos(spotOuterAngle);
packedAngleCos = (int) (innerCos * 1000);
//due to approximations, very close angles can give the same cos
//here we make sure outer cos is bellow inner cos.
if (((int) packedAngleCos) == ((int) (outerCos * 1000))) {
@ -81,6 +88,60 @@ public class SpotLight extends Light implements Savable {
if (packedAngleCos == 0.0f) {
throw new IllegalArgumentException("Packed angle cosine is invalid");
}
// compute parameters needed for cone vs sphere check.
float outerSin = FastMath.sin(spotOuterAngle);
outerAngleCosSqr = outerCos * outerCos;
outerAngleSinSqr = outerSin * outerSin;
outerAngleSinRcp = 1.0f / outerSin;
}
@Override
public boolean intersectsBox(BoundingBox box, TempVars vars) {
if (this.spotRange > 0f) {
// Check spot range first.
// Sphere v. box collision
if (FastMath.abs(box.getCenter().x - position.x) >= spotRange + box.getXExtent()
|| FastMath.abs(box.getCenter().y - position.y) >= spotRange + box.getYExtent()
|| FastMath.abs(box.getCenter().z - position.z) >= spotRange + box.getZExtent()) {
return false;
}
}
Vector3f otherCenter = box.getCenter();
Vector3f radVect = vars.vect4;
radVect.set(box.getXExtent(), box.getYExtent(), box.getZExtent());
float otherRadiusSquared = radVect.lengthSquared();
float otherRadius = FastMath.sqrt(otherRadiusSquared);
// Check if sphere is within spot angle.
// Cone v. sphere collision.
Vector3f E = direction.mult(otherRadius * outerAngleSinRcp, vars.vect1);
Vector3f U = position.subtract(E, vars.vect2);
Vector3f D = otherCenter.subtract(U, vars.vect3);
float dsqr = D.dot(D);
float e = direction.dot(D);
if (e > 0f && e * e >= dsqr * outerAngleCosSqr) {
D = otherCenter.subtract(position, vars.vect3);
dsqr = D.dot(D);
e = -direction.dot(D);
if (e > 0f && e * e >= dsqr * outerAngleSinSqr) {
return dsqr <= otherRadiusSquared;
} else {
return true;
}
}
return false;
}
@Override
public boolean intersectsFrustum(Camera camera, TempVars vars) {
// TODO: implement cone vs. frustum collision detection.
return true;
}
@Override
@ -166,7 +227,7 @@ public class SpotLight extends Light implements Savable {
*/
public void setSpotInnerAngle(float spotInnerAngle) {
this.spotInnerAngle = spotInnerAngle;
computePackedCos();
computeAngleParameters();
}
/**
@ -185,7 +246,7 @@ public class SpotLight extends Light implements Savable {
*/
public void setSpotOuterAngle(float spotOuterAngle) {
this.spotOuterAngle = spotOuterAngle;
computePackedCos();
computeAngleParameters();
}
/**
@ -196,8 +257,6 @@ public class SpotLight extends Light implements Savable {
return packedAngleCos;
}
@Override
public void write(JmeExporter ex) throws IOException {
super.write(ex);
@ -215,7 +274,7 @@ public class SpotLight extends Light implements Savable {
InputCapsule ic = im.getCapsule(this);
spotInnerAngle = ic.readFloat("spotInnerAngle", FastMath.QUARTER_PI / 8);
spotOuterAngle = ic.readFloat("spotOuterAngle", FastMath.QUARTER_PI / 6);
computePackedCos();
computeAngleParameters();
direction = (Vector3f) ic.readSavable("direction", new Vector3f());
position = (Vector3f) ic.readSavable("position", new Vector3f());
spotRange = ic.readFloat("spotRange", 100);

@ -741,12 +741,11 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
* g_LightPosition.w is the inverse radius (1/r) of the light (for
* attenuation) <br/> </p>
*/
protected void updateLightListUniforms(Shader shader, Geometry g, int numLights) {
protected void updateLightListUniforms(Shader shader, Geometry g, LightList lightList, int numLights) {
if (numLights == 0) { // this shader does not do lighting, ignore.
return;
}
LightList lightList = g.getWorldLightList();
Uniform lightColor = shader.getUniform("g_LightColor");
Uniform lightPos = shader.getUniform("g_LightPosition");
Uniform lightDir = shader.getUniform("g_LightDirection");
@ -813,10 +812,9 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
}
}
protected void renderMultipassLighting(Shader shader, Geometry g, RenderManager rm) {
protected void renderMultipassLighting(Shader shader, Geometry g, LightList lightList, RenderManager rm) {
Renderer r = rm.getRenderer();
LightList lightList = g.getWorldLightList();
Uniform lightDir = shader.getUniform("g_LightDirection");
Uniform lightColor = shader.getUniform("g_LightColor");
Uniform lightPos = shader.getUniform("g_LightPosition");
@ -1116,9 +1114,10 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
* </ul>
*
* @param geom The geometry to render
* @param lights Presorted and filtered light list to use for rendering
* @param rm The render manager requesting the rendering
*/
public void render(Geometry geom, RenderManager rm) {
public void render(Geometry geom, LightList lights, RenderManager rm) {
autoSelectTechnique(rm);
Renderer r = rm.getRenderer();
@ -1126,7 +1125,7 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
TechniqueDef techDef = technique.getDef();
if (techDef.getLightMode() == LightMode.MultiPass
&& geom.getWorldLightList().size() == 0) {
&& lights.size() == 0) {
return;
}
@ -1163,15 +1162,15 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
r.setLighting(null);
break;
case SinglePass:
updateLightListUniforms(shader, geom, 4);
updateLightListUniforms(shader, geom, lights, 4);
break;
case FixedPipeline:
r.setLighting(geom.getWorldLightList());
r.setLighting(lights);
break;
case MultiPass:
// NOTE: Special case!
resetUniformsNotSetByCurrent(shader);
renderMultipassLighting(shader, geom, rm);
renderMultipassLighting(shader, geom, lights, rm);
// very important, notice the return statement!
return;
}
@ -1186,6 +1185,20 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
renderMeshFromGeometry(r, geom);
}
/**
* Called by {@link RenderManager} to render the geometry by
* using this material.
*
* Note that this version of the render method
* does not perform light filtering.
*
* @param geom The geometry to render
* @param rm The render manager requesting the rendering
*/
public void render(Geometry geom, RenderManager rm) {
render(geom, geom.getWorldLightList(), rm);
}
public void write(JmeExporter ex) throws IOException {
OutputCapsule oc = ex.getCapsule(this);
oc.write(def.getAssetName(), "material_def", null);

@ -1052,6 +1052,10 @@ public class Camera implements Savable, Cloneable {
return rVal;
}
public Plane getWorldPlane(int planeId) {
return worldPlane[planeId];
}
/**
* <code>containsGui</code> tests a bounding volume against the ortho
* bounding box of the camera. A bounding box spanning from

@ -31,6 +31,9 @@
*/
package com.jme3.renderer;
import com.jme3.light.DefaultLightFilter;
import com.jme3.light.LightFilter;
import com.jme3.light.LightList;
import com.jme3.material.Material;
import com.jme3.material.MaterialDef;
import com.jme3.material.RenderState;
@ -81,9 +84,11 @@ public class RenderManager {
private boolean shader;
private int viewX, viewY, viewWidth, viewHeight;
private Matrix4f orthoMatrix = new Matrix4f();
private LightList filteredLightList = new LightList(null);
private String tmpTech;
private boolean handleTranlucentBucket = true;
private AppProfiler prof;
private LightFilter lightFilter = new DefaultLightFilter();
/**
* Create a high-level rendering interface over the
@ -522,6 +527,16 @@ public class RenderManager {
setWorldMatrix(g.getWorldMatrix());
}
// Perform light filtering if we have a light filter.
LightList lightList = g.getWorldLightList();
if (lightFilter != null) {
filteredLightList.clear();
lightFilter.filterLights(g, filteredLightList);
lightList = filteredLightList;
}
//if forcedTechnique we try to force it for render,
//if it does not exists in the mat def, we check for forcedMaterial and render the geom if not null
//else the geom is not rendered
@ -536,7 +551,7 @@ public class RenderManager {
forcedRenderState = g.getMaterial().getActiveTechnique().getDef().getForcedRenderState();
}
// use geometry's material
g.getMaterial().render(g, this);
g.getMaterial().render(g, lightList, this);
g.getMaterial().selectTechnique(tmpTech, this);
//restoring forcedRenderState
@ -546,13 +561,13 @@ public class RenderManager {
//If forcedTechnique does not exists, and frocedMaterial is not set, the geom MUST NOT be rendered
} else if (forcedMaterial != null) {
// use forced material
forcedMaterial.render(g, this);
forcedMaterial.render(g, lightList, this);
}
} else if (forcedMaterial != null) {
// use forced material
forcedMaterial.render(g, this);
forcedMaterial.render(g, lightList, this);
} else {
g.getMaterial().render(g, this);
g.getMaterial().render(g, lightList, this);
}
}
@ -790,6 +805,11 @@ public class RenderManager {
Camera cam = vp.getCamera();
boolean depthRangeChanged = false;
// Tell the light filter which camera to use for filtering.
if (lightFilter != null) {
lightFilter.setCamera(cam);
}
// render opaque objects with default depth range
// opaque objects are sorted front-to-back, reducing overdraw
if (prof!=null) prof.vpStep(VpStep.RenderBucket, vp, Bucket.Opaque);

Loading…
Cancel
Save