Lighting and shadow efficiency

I’m lighting an entire house with roughly 15 rooms and 15 hallways using 12-15 lights. My walls (about 8 per room) are set on CastAndReceive shadow mode. As you can imagine, this program crawls (8 fps on average). How can I make this more efficient while still lighting about the entire house?

I’m using PointLights, and I’ve followed the light and shadow tutorial. I’m not doing any Ambient Occlusion or other fancy stuff, just shadows and light.

Shadow renderer code:

// add lights to scene and create a shadow renderer for each light
for (PointLight i : r.getLights()) {
	enviro.addLight(i);
    // manager = assetManager
	PointLightShadowRenderer shadow = new PointLightShadowRenderer(manager, 1024);
	shadow.setLight(i);
	port.addProcessor(shadow);
}

Maybe I just need a better processor/GPU thingy :laughing:

Edit

One more thing. It seems that some light is somehow passing through or under walls, but only when my camera’s at a certain angle. And sometimes a particular light seems to to dim or turn off altogether (again, when the camera is at certain angle).

edited light thru wall

I did have a similar strange issue about parts of the ceiling disappearing when the cam was at a certain angle, and it turned out to be GPU issues that cleared up after rebooting. I’m not sure if this is the same way.

Using many point and spot lights will always lower the frame-rate, especially with shadows.

One solution would be to write a small class that manages your lights, then set a maximum number of lights allowed at a single time, and detach the lights that are furthest from the camera in order to stay under the limit. This way the the GPU will only be rendering the lights that are closest to the player. and will likely go unnoticed by players, especially if you allow the max lights to correspond to an adjustable value in settings so players with better GPUs can adjust it.

But if you have a situation where you have static lights that are really close together in the same room , and if they all need to all be visible at the same time, then you may want to consider baking some of he lights into a light map.

I have noticed a similar bug, but it only happened to me when I used intense fog, or when I had models that were a certain shade of gray that coincidentally matched the color that intense fog causes. So I’m not sure if its the same issue.

2 Likes

The real solution is:

  1. For a large number of light source calculations for a large number of dynamic objects, the jme renderer should be redesigned based on the deferred lighting rendering path (unfortunately, jme currently does not have multiple rendering paths, and only supports the basic Forward rendering path)
  2. For most static objects, use baking (baked GI+Shadow+AO, etc.). Unfortunately, jme3 does not provide a baking tool. You have to use blender or 3dmax to create a second uv as a light map uv, Then every time you make a level, bake it in blender or 3dmax, and then import jm3 one by one to reset the second lightMap UV and add the corresponding lightMap.
    In short, there is currently no ideal solution. I think the future jme3.4 or 4.0 should redesign the renderer (support multiple rendering paths, such as Forward, Forward++, DeferredShading, Light-PrePass, etc.), and refactor the shader definition, adding definitions like SubPass, In order to realize the definition of multi-stage rendering directly in the material definition.

If JME provided a baking tool then the next complaint would be that it isn’t as good as Blender’s or Maya’s or 3D Max, etc…

JME made a very specific decision not to be a 3D modeling tool… for very good reason.

Though several people have managed to implement their own deferred rendering in JME. So it is possible.

1 Like


In jme3, DeferredLighting.jm3d is an incomplete material definition. I fixed it a long time ago, but the rendering process needs to be controlled by Java code (in short, the user needs to manually control the GBuffer Pass first, and then manually control the DeferredShading Pass, but this is not flexible enough. From the perspective of usage, The user should only need to set the material, instead of manually going to the java code for rendering control, these should be designed to be managed in the rendering path of the renderer) Therefore, I suggest that in the future, it may be necessary to refactor the renderer and add the rendering path. The definition of design and material rendering process, in this way, jme3 users do not need to manually pass Java code controls to execute pass0 first, and then perform operations such as pass1. Of course, this is my idea, I know everyone is busy , So anyone with the ability can do it.

1 Like

Update:
I’ve created a class that limits the total number of lights engaged by disengaging the farthest lights away from the player, but I’m not sure if it’s even working because the lights keep flickering on and off all the time depending on the camera.

I’m sure it isn’t my light managing class, since I’ve had this problem before implementing it, and that lights will flicker even when I’m stationary and just looking around.

I’m also having trouble with particular light casting its beams through walls. I don’t think any other lights are doing it, but my scene is filled with flickering lights, it’s hard to tell.

How many lights do you have? OpenGL has limits on the number of active lights in a scene, if you are past that limit strange things may occur.

Only 7 at most

Ah Ok, should be good then.

But it isn’t, it’s just plain aweful. :nauseated_face:

Can you post a test program here demonstrating the issue?

I don’t think this will come up with jME - the light limit in OpenGL is for the fixed-function pipeline where OpenGL does the shading. If you’re using a shader, “lights” are just some info that gets passed to your shader in a buffer and you can have as many as you like.

And specifically, even with “single pass” lighting, JME only does a fixed number of lights per pass. So beyond some number (configurable I think), you still have multiple passes.

Edit: and thus OpenGL never sees more than what’s in a single pass.

Here’s the requested test case:

Main Class
package mygame;

import com.jme3.app.SimpleApplication;
import com.jme3.bullet.BulletAppState;
import com.jme3.bullet.control.RigidBodyControl;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
import com.jme3.renderer.RenderManager;
import com.jme3.shadow.PointLightShadowRenderer;

/**
 * 
 * @author codex
 *
 * Things missing:
furniture
textured floor/ceiling
 */
public class Main extends SimpleApplication {
	
	// settings, change freely
	public static final int MAX_LIGHTS = 5, WORLD_WIDTH = 4, WORLD_DEPTH = 4;
	public static final boolean ENABLE_CEILING = !true; // you can fly above building for birds-eye view (if this is false)
	
	
	
	BulletAppState bullet;
	LightManager lightManager = new LightManager(MAX_LIGHTS+1);
	
    public static void main(String[] args) {
        Main app = new Main();
        app.start();
    }

    @Override
    public void simpleInitApp() {
        bullet = new BulletAppState();
		stateManager.attach(bullet);
		
		//bullet.setDebugEnabled(true);
		
		viewPort.setBackgroundColor(ColorRGBA.Cyan);
		flyCam.setMoveSpeed(50);
		
		for (int i = 0; i < WORLD_WIDTH; i++) {
			for (int j = 0; j < WORLD_DEPTH; j++) {
				Room r = new Room("Test#"+i, new Vector3f(Room.UNIT*10*i, 0, Room.UNIT*10*j), Room.UNIT*5, Room.UNIT*5);
				attachRoom(r);
			}
		}
		
    }
    @Override
    public void simpleUpdate(float tpf) {
        lightManager.update(rootNode, viewPort, cam.getLocation());
    }
    @Override
    public void simpleRender(RenderManager rm) {
        //TODO: add render code
    }
	
	private void attachRoom(Room r) {
		r.setAssets(assetManager, viewPort);
		r.buildRoom();
		rootNode.attachChild(r.getNode());
		for (RigidBodyControl i : r.getPhysics()) bullet.getPhysicsSpace().add(i);
		lightManager.add(r.getLight(), new PointLightShadowRenderer(assetManager, 1024));
	}
}
Room Class
package mygame;

import com.jme3.asset.AssetManager;
import com.jme3.bullet.control.RigidBodyControl;
import com.jme3.light.PointLight;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Vector3f;
import com.jme3.renderer.ViewPort;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.shape.Box;
import com.jme3.shadow.PointLightShadowRenderer;
import java.util.ArrayList;

/**
 *
 * @author codex
 * 
 * @Purges
 * walls
 * lighting
 * decor
 * image textures
 */
public class Room {	
	
	public static final float UNIT = 5;
	
	private final Node room;
	
	private final String name;
	private final Vector3f pos;
	private final float w;
	private final float d;
	
	private final float thickness = 0.5f;
	
	private final float height = 8;	
	private final ArrayList<RigidBodyControl> physics = new ArrayList<>();	
	private AssetManager manager = null;
	private ViewPort port = null;
	
	private final PointLight light = new PointLight();
	
	private final Wall topwall, bottomwall, leftwall, rightwall;
	
	
	Room(String name, Vector3f pos, float w, float h) {
		this.name = name;
		this.pos = pos;
		this.w = w;
		this.d = h;
		
		room = new Node("room:"+this.name);
		topwall = new Wall(pos.add(new Vector3f(0, 0, -this.d+thickness)), Vector3f.UNIT_X, this.w, height, thickness);
		bottomwall = new Wall(pos.add(new Vector3f(0, 0, this.d-thickness)), Vector3f.UNIT_X, this.w, height, thickness);
		leftwall = new Wall(pos.add(new Vector3f(-this.w+thickness, 0, 0)), Vector3f.UNIT_Z, this.d, height, thickness);
		rightwall = new Wall(pos.add(new Vector3f(this.w-thickness, 0, 0)), Vector3f.UNIT_Z, this.d, height, thickness);
	}
	
	
	protected void setAssets(AssetManager manager, ViewPort port) {
		this.manager = manager;
		this.port = port;
	}
	
	protected void buildRoom() {
		RigidBodyControl phys = new RigidBodyControl(0);
		
		buildFloor();
		if (Main.ENABLE_CEILING) buildCeil();
		//buildPillar(pos.add(new Vector3f(FastMath.nextRandomInt((int)-w, (int)w), 0, FastMath.nextRandomInt((int)-d, (int)d))));
		
		Material m = new Material(manager, "Common/MatDefs/Light/Lighting.j3md");
		m.setTexture("DiffuseMap", manager.loadTexture("Textures/houseWall01.png"));
		topwall.setMaterial(m);
		bottomwall.setMaterial(m);
		leftwall.setMaterial(m);
		rightwall.setMaterial(m);
		
		topwall.build();
		room.attachChild(topwall.getNode());
		bottomwall.build();
		room.attachChild(bottomwall.getNode());
		leftwall.build();
		room.attachChild(leftwall.getNode());
		rightwall.build();
		room.attachChild(rightwall.getNode());
		
		light.setPosition(pos);
		light.setRadius(400f);
		light.setColor(ColorRGBA.White);
		
		room.addControl(phys);
		physics.add(phys);
	}
	
	private void buildFloor() {
		Box b = new Box(w, 0.5f, d);
		Geometry g = new Geometry("floor", b);
		g.setLocalTranslation(new Vector3f(0, -height*0.5f, 0).add(pos));
		Material mat = new Material(manager, "Common/MatDefs/Light/Lighting.j3md");
		//mat.setTexture("ColorMap", manager.loadTexture("Textures/hardwood.jpg"));
		mat.setBoolean("UseMaterialColors", true);
		mat.setColor("Diffuse", ColorRGBA.Orange);
		g.setMaterial(mat);
		g.setShadowMode(RenderQueue.ShadowMode.Receive);
		room.attachChild(g);
	}
	private void buildCeil() {
		Box b = new Box(w, 0.5f, d);
		Geometry g = new Geometry("ceil", b);
		g.setLocalTranslation(new Vector3f(0, height*1.5f, 0).add(pos));
		Material mat = new Material(manager, "Common/MatDefs/Light/Lighting.j3md");
		//mat.setTexture("ColorMap", manager.loadTexture("Textures/houseTilesDoor01.png"));
		mat.setBoolean("UseMaterialColors", true);
		mat.setColor("Diffuse", ColorRGBA.Red);
		g.setMaterial(mat);
		g.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
		room.attachChild(g);
	}
	private void buildPillar(Vector3f pos) {
		Geometry geom = new Geometry("pillar", new Box(2, height, 2));
		Material mat = new Material(manager, "Common/MatDefs/Light/Lighting.j3md");
		mat.setBoolean("UseMaterialColors", true);
		mat.setColor("Diffuse", ColorRGBA.Blue);
		geom.setMaterial(mat);
		geom.setLocalTranslation(pos.x, 4, pos.z);
		geom.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
		room.attachChild(geom);
	}
	
	protected Node getNode() {
		return room;
	}
	protected ArrayList<RigidBodyControl> getPhysics() {
		return physics;
	}
	protected PointLight getLight() {
		return light;
	}
}
LightManager Class
package mygame;

import com.jme3.light.PointLight;
import com.jme3.math.FastMath;
import com.jme3.math.Vector3f;
import com.jme3.renderer.ViewPort;
import com.jme3.scene.Node;
import com.jme3.shadow.PointLightShadowRenderer;
import java.util.ArrayList;

/**
 *
 * @author codex
 */
public class LightManager {
	
	int maxLights;	
	ArrayList<Lightbulb> lights = new ArrayList<>();
	
	
	LightManager(int maxLights) {
		this.maxLights = maxLights;
	}
	
	
	protected void update(Node root, ViewPort port, Vector3f p) {
		ArrayList<Lightbulb> keep = new ArrayList<>();
		int removes = lights.size()-maxLights;
		if (removes > 0) {
			for (Lightbulb i : lights) {
				root.removeLight(i.getBulb());
				port.removeProcessor(i.getRender());
				int biggers = 0;
				float dist1 = FastMath.sqrt(sqr(i.getLocation().z-p.z)+sqr(i.getLocation().x-p.x));
				for (Lightbulb j : lights) {
					if (i == j) continue;
					float dist2 = (float)Math.sqrt(sqr(j.getLocation().z-p.z)+sqr(j.getLocation().x-p.x));
					if (dist2 > dist1) biggers++;
					if (biggers > removes) break;
				}
				if (biggers > removes) keep.add(i);
			}
			for (Lightbulb i : keep) {
				System.out.println("adding light");
				root.addLight(i.getBulb());
				port.addProcessor(i.getRender());
			}
		}
	}
	protected void add(PointLight light, PointLightShadowRenderer render) {
		lights.add(new Lightbulb(light, render));
	}
	
	private float sqr(float num) {
		return num*num;
	}
}
Lightbulb Class
package mygame;

import com.jme3.light.PointLight;
import com.jme3.math.Vector3f;
import com.jme3.shadow.PointLightShadowRenderer;

/**
 *
 * @author codex
 */
public class Lightbulb {
	
	private PointLight bulb;
	private PointLightShadowRenderer render;
	
	
	Lightbulb(PointLight bulb, PointLightShadowRenderer render) {
		this.bulb = bulb;
		this.render = render;
		this.render.setLight(this.bulb);
	}
	
	
	protected void setBulb(PointLight bulb) {
		this.bulb = bulb;
	}
	protected void setRender(PointLightShadowRenderer render) {
		this.render = render;
	}
	protected void enable(boolean on) {
		bulb.setEnabled(on);
	}
	
	protected PointLight getBulb() {
		return bulb;
	}
	protected PointLightShadowRenderer getRender() {
		return render;
	}
	
	protected Vector3f getLocation() {
		return bulb.getPosition();
	}
	
}
Wall Class

package mygame;

import com.jme3.bullet.control.RigidBodyControl;
import com.jme3.material.Material;
import com.jme3.math.Vector3f;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.shape.Box;

/**
*

  • @author codex
    */
    public class Wall {

    Vector3f pos, axis;
    float width, height, thickness;

    Node wallNode = new Node();
    RigidBodyControl phys = new RigidBodyControl(0);
    Material mat = null;

    public static final String REG = “regular”, NONE = “none”, ENTIRE = “entire”, SAME = “same”, WINDOW = “window”;
    String doorMode = REG;
    float doorWidth = 3f, doorTop = 1.5f, doorOffset = 0;

    Vector3f posiPos = new Vector3f();
    Vector3f posiSize = new Vector3f();
    Vector3f negPos = new Vector3f();
    Vector3f negSize = new Vector3f();

    Wall(Vector3f pos, Vector3f axis, float width, float height, float thickness) {
    this.pos = pos;
    this.axis = axis;
    this.width = width;
    this.height = height;
    this.thickness = thickness;
    }

    protected void setMaterial(Material m) {
    mat = m;
    }
    protected void setDoorWidth(float w) {
    if (w != 0) doorWidth = w;
    }
    protected void setDoorOffset(float off) {
    if (off != 0) doorOffset = off;
    }
    protected void setDoorMode(String mode) {
    if (!mode.equals(SAME)) doorMode = mode;
    }
    protected void setDoorTop(float top) {
    if (top != 0) doorTop = top;
    }
    protected void setDoorMat(Material m) {

    }

    private void setPosiCoord() {
    if (doorMode.equals(REG) || doorMode.equals(WINDOW)) {
    float s = Math.abs(width+doorOffset-doorWidth)/2;
    if (axis == Vector3f.UNIT_X) {
    posiPos.set(width-s, height0.5f, 0);
    posiSize.set(s, height, thickness);
    }
    else if (axis == Vector3f.UNIT_Z) {
    posiPos.set(0, height
    0.5f, width-s);
    posiSize.set(thickness, height, s);
    }
    }
    else if (doorMode.equals(NONE)) {
    if (axis == Vector3f.UNIT_X) {
    posiPos.set(0, height0.5f, 0);
    posiSize.set(width, height, thickness);
    }
    else if (axis == Vector3f.UNIT_Z) {
    posiPos.set(0, height
    0.5f, 0);
    posiSize.set(thickness, height, width);
    }
    }
    }
    private void setNegCoord() {
    float s = Math.abs(width-doorOffset-doorWidth)/2;
    if (axis == Vector3f.UNIT_X) {
    negPos.set(-width+s, height0.5f, 0);
    negSize.set(s, height, thickness);
    }
    else if (axis == Vector3f.UNIT_Z) {
    negPos.set(0, height
    0.5f, -width+s);
    negSize.set(thickness, height, s);
    }
    }
    private void initPosiWall() {
    if (!doorMode.equals(ENTIRE)) {
    Box b = new Box(posiSize.x, posiSize.y, posiSize.z);
    Geometry g = new Geometry(“pos wall”, b);
    g.setLocalTranslation(posiPos);
    g.setMaterial(mat);
    g.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
    //g.setUserData(KEY, this);
    wallNode.attachChild(g);
    }
    }
    private void initNegWall() {
    if (!doorMode.equals(ENTIRE) && !doorMode.equals(NONE)) {
    Box b = new Box(negSize.x, negSize.y, negSize.z);
    Geometry g = new Geometry(“neg wall”, b);
    g.setLocalTranslation(negPos);
    g.setMaterial(mat);
    g.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
    wallNode.attachChild(g);
    }
    }
    private void initTop() {
    if (!doorMode.equals(ENTIRE) && !doorMode.equals(NONE)) {
    Geometry g = new Geometry();
    Geometry g2 = new Geometry();
    if (axis == Vector3f.UNIT_X) {
    Box b = new Box(doorWidth, doorTop, thickness);
    g = new Geometry(“top wall”, b);
    //g.setMesh(b);
    g.setLocalTranslation(-doorOffset, height1.5f-doorTop, 0);
    g2 = new Geometry(“bottom wall”, b);
    g2.setLocalTranslation(-doorOffset, -height
    0.45f+doorTop, 0);
    }
    else if (axis == Vector3f.UNIT_Z) {
    Box b = new Box(thickness, doorTop, doorWidth);
    g = new Geometry(“top wall”, b);
    g.setLocalTranslation(0, height1.5f-doorTop, -doorOffset);
    g2 = new Geometry(“bottom wall”, b);
    g2.setLocalTranslation(0, -height
    0.45f+doorTop, -doorOffset);
    }
    g.setMaterial(mat);
    g2.setMaterial(mat);
    g.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
    g2.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
    wallNode.attachChild(g);
    if (doorMode.equals(WINDOW)) wallNode.attachChild(g2);
    }
    }
    private void initDoor() {

    }

    protected void build() {
    setPosiCoord();
    setNegCoord();
    initPosiWall();
    initNegWall();
    initTop();

     wallNode.setLocalTranslation(pos);
     //wallNode.setUserData(Interactable.KEY, this);
    

    }

    protected Node getNode() {
    return wallNode;
    }
    }

Things are pretty slow with only 5 lights showing. I couldn’t get the weird light passing through walls bug to come up. :angry:

The only differences between my original project and this are decor, textured floor and ceiling with img, and layout.

So if I’m reading this right, every frame you remove all of the lights and then add back only the best ones?

…yeah, you’re going to have a bad time with that. There is book-keeping done that is probably pretty unhappy and may explain your flickering.

Why is it that you build a keep list instead of a remove list?

Yup, that is very bad. I’m working on using the PointLight.setEnable() method, but it doesn’t seem to be working for me yet.

I doubt that is what is making the lights flicker, because they aren’t flickering in this example. I’ve been looking over my main program again, and it seems more descriptive to say that the floor and some walls flicker, because my furniture, other walls, and ceiling look fine.

The problem only seems to occur when the camera is looking in the right direction , not unlike that GPU ceiling-disappearing bug (except this one didn’t go away after rebooting).

As of now, I haven’t yet been able to recreate either the flickering lights or the light passing through walls in the above code.

Yes it sounds like it could likely be an issue with those models, if you could not reproduce it in a test case using other models.

Does the flickering stop if you remove shadows?

And if so, do your models have a low vertex count?

I recall someone once advised me that shadows will not look good if the shadow is being cast to / from a model that is large and has too few vertices for its size.

Your general approach of adding/removing lights will work if you don’t remove them unless they actually need to be removed.

Easiest way: keep the lights in a wrapper that knows ‘distance to camera’. Make those wrappers Comparable. Keep them in a list. When you want to update the lights, resort the list (there is a sort that operates better if the list is already mostly sorted which is probably best in your case). (Actually, reading the JDK javadoc the built in sort is already optimized for nearly sorted lists: “If the input array is nearly sorted, the implementation requires approximately n comparisons.”)

Add unadded lights in the beginning of the list. Remove added lights in the overflow part of the list.

@yaRnMcDonuts
I’m using Geometries w/ Box textured with an image for my floors, ceilings, and walls.
Yes, the flickering stops when shadows are disabled.
My walls are about (10, 8, 0.5f) in size.

@pspeed
What’s a wrapper? Is it some sort of Collection subclass?