A more precise exception report about modifying the scene from another thread:

That’s the “cat leap” part:

You simply have to set the name of your spatials with high quality.

The name of mine contains the class name holding/connecting to it + the internal ID + the type of the spatial in my database.

With all that information, I know exactly where to find it. If for any reason it is still too complex, further breakpoints will help me detect what point of the code caused the trouble, but now, I know exactly where to place these breakpoints!

So again, this is the comparison of the previous error message: “Root Node”
with the new: “Root Node,MySpecialSpatial.java:123:Hero2”.

Before I was clueless for months, then days, then hours, then I manage to multi-thread again. Now, with this patch, I know what to do in less then a minute!

Unless, this precise spatial naming is not completely under control in the way people are implementing their projects, now that would really make this approach limited. Because of that, I asked for more tips. May be other things than the name could help in these cases.

Will not work in my game since I heavily use geometry instancing optimizations for example.

I guess they are created and destroyed dynamically, and they do not exist for a long time so are not important to be saved to reload later, so you don’t care on heavily tracking them, right?

So, could you suggest some improvement to that code? Will be greatly appreciated! :slight_smile:

almost every geometry in my space game is instanced … which means that a single geometry instance in some cases lives in hundreds of places in my scene graph at any one time. Since it’s the same geometry instance of course it will only have one name. This is a new feature in 3.1 and extremely important in scenes with thousands of independent geometries like in space games of large forests etc.

1 Like

Good to know it is on JME already!
I actually am just learning more about Geometry Instancing.

By what I read there, it seems we could modify each instance individually with potential risk of generating that exception. But… there must have something that uniquely identifies each instance. That information could be added to the default spatial name error report. Do you know what could that be? may be a geometry instance index?

EDIT: I need some sleep, gn!

Look at TestInstanceNode in the 3.1 tests

1 Like

As I saw, TestInstanceNode creates 3600 geometry objects that share only 2 meshes.
It also produces 12 Instanced Geometries.

I managed to name each geometry indifidually (based on their position).
I also saw that all InstancedGeometry where independent objects, they were named against the mesh mainly, but certainly we can make them have unique names too!

EDIT: therefore, each geometry has its userdata, that we can link to our project classes, what will help on creating a precise match to find the culprit code for the exception.

Instead of having to come up with an understandable naming convention for 10000 dynamic objects in my scene in hopes that I will use this information in a stack trace to fins where I modified the scene graph outside the JME thread, I would rather just write my code properly and just use the provided enqueue method and never have to go through all the naming and tracking issues that I must implement throughout my design in order to read that exception that should never be thrown. If you ever see that exception it means you don’t know how to properly use the enqueue method. Just my two cents worth.

3 Likes

The Queue
I actually have my own queue, and I have been using it to exactly avoid that exception.

My queue allow me to choose if my code will be run threaded or at main thread (at the update per frame method).
Actually I learned about enqueue after my queue was fully functional, so I dont really know how it works yet.

Veteran Newbied
Then, one day, I used an external library improperly, I didnt know how to use it yet, and suddenly that obnoxious error exception happened and I was clueless again, because I was implementing several different things by several hours without testing. My only hope was to focus on what was new (external) to my code, so I fixed the library usage and the problem was gone.

So I implemented this patch and simulated the error again, and the culprit spatial name poped like magic, and instantly I would have known what would be necessary to be fixed…

Lawful Newbie
Now, imagine a newbie, doing that, multithreaded and receiving that message, trying to implement several fancy ideas and test them and being prevented by that limited exception message. I was like that several years ago. I abbandoned JME, tryied UnrealEngine, tried many other engines and couldnt like them at all. So my only choice was to come back :smile:

I just believe, if some newbie give up, that it is not by that exception reason.

Newbie and multithreaded do not combine well no matter what.

4 Likes

@pspeed
good point lol

EDIT: But, I knew how to handle all other implementations problems as they were fairly easy to track, as what you manage to track you can fix. I do not mean that exception is untrackable, but the effort to track it may be too much depending on the developer (JME knowledge level), as it was to me at that time. So, a more precise error message would have been a lot more helpful.

Proper use of a debigger can find any issues that any exception throughs. The debugger is your friend.

@zissis that would be true if the spatial modification (like translation) was the originating point of the thrown exception.
In this case, the exception originates from an assert like verification code point. The debugger will not pin-point the code where the spatial was changed, only the point where the assertion was made. :frowning:

EDIT: anyway, this subject is of the other topic :). I want to keep this topic about improved error message :). EDIT: for lawful newbies (like I used to be) :slight_smile:

Putting a break point where the exception occurred and using the mouse to click through the node tree in the debugger will give you the same information as your code. Just saying.

Well if you want to know where it happens:
Drop in replacment for Node, with no real runtime cost additionally, as the asserts must be actvated, and are removed at class loading time if not enabled. (The subrenderer was a bitch to fix, because I got the exact same error, but it was a internal jme state problem, not a threading one apparently.) However noticing that it was actually not my error took quite some time, wich is why this class was created.

package de.visiongamestudios.client.jme.subrenderer;

import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.Queue;

import com.jme3.asset.AssetKey;
import com.jme3.bounding.BoundingVolume;
import com.jme3.collision.Collidable;
import com.jme3.collision.CollisionResults;
import com.jme3.export.JmeExporter;
import com.jme3.export.JmeImporter;
import com.jme3.light.Light;
import com.jme3.light.LightList;
import com.jme3.material.Material;
import com.jme3.math.Matrix3f;
import com.jme3.math.Matrix4f;
import com.jme3.math.Quaternion;
import com.jme3.math.Transform;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.renderer.Camera.FrustumIntersect;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.renderer.queue.RenderQueue.Bucket;
import com.jme3.renderer.queue.RenderQueue.ShadowMode;
import com.jme3.scene.Node;
import com.jme3.scene.SceneGraphVisitor;
import com.jme3.scene.Spatial;
import com.jme3.scene.control.Control;

public class ThreadSaveNode extends Node {

	private Thread thread;

	public ThreadSaveNode(final String string, final Thread openGLThread) {
		super(string);
		this.thread = openGLThread;
	}

	@Override
	public int getQuantity() {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.getQuantity();
	}

	@Override
	protected void setTransformRefresh() {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		super.setTransformRefresh();
	}

	@Override
	protected void setLightListRefresh() {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		super.setLightListRefresh();
	}

	@Override
	protected void updateWorldBound() {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		super.updateWorldBound();
	}

	@Override
	protected void setParent(final Node parent) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		super.setParent(parent);
	}

	@Override
	public void updateLogicalState(final float tpf) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		super.updateLogicalState(tpf);
	}

	@Override
	public void updateGeometricState() {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		super.updateGeometricState();
	}

	@Override
	public int getTriangleCount() {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.getTriangleCount();
	}

	@Override
	public int getVertexCount() {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.getVertexCount();
	}

	@Override
	public int attachChild(final Spatial child) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.attachChild(child);
	}

	@Override
	public int attachChildAt(final Spatial child, final int index) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.attachChildAt(child, index);
	}

	@Override
	public int detachChild(final Spatial child) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.detachChild(child);
	}

	@Override
	public int detachChildNamed(final String childName) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.detachChildNamed(childName);
	}

	@Override
	public Spatial detachChildAt(final int index) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.detachChildAt(index);
	}

	@Override
	public void detachAllChildren() {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		super.detachAllChildren();
	}

	@Override
	public int getChildIndex(final Spatial sp) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.getChildIndex(sp);
	}

	@Override
	public void swapChildren(final int index1, final int index2) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		super.swapChildren(index1, index2);
	}

	@Override
	public Spatial getChild(final int i) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.getChild(i);
	}

	@Override
	public Spatial getChild(final String name) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.getChild(name);
	}

	@Override
	public boolean hasChild(final Spatial spat) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.hasChild(spat);
	}

	@Override
	public List<Spatial> getChildren() {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.getChildren();
	}

	@Override
	public void setMaterial(final Material mat) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		super.setMaterial(mat);
	}

	@Override
	public void setLodLevel(final int lod) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		super.setLodLevel(lod);
	}

	@Override
	public int collideWith(final Collidable other, final CollisionResults results) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.collideWith(other, results);
	}

	@Override
	public <T extends Spatial> List<T> descendantMatches(final Class<T> spatialSubclass, final String nameRegex) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.descendantMatches(spatialSubclass, nameRegex);
	}

	@Override
	public <T extends Spatial> List<T> descendantMatches(final Class<T> spatialSubclass) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.descendantMatches(spatialSubclass);
	}

	@Override
	public <T extends Spatial> List<T> descendantMatches(final String nameRegex) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.descendantMatches(nameRegex);
	}

	@Override
	public Node clone(final boolean cloneMaterials) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.clone(cloneMaterials);
	}

	@Override
	public Spatial deepClone() {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.deepClone();
	}

	@Override
	public void write(final JmeExporter e) throws IOException {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		super.write(e);
	}

	@Override
	public void read(final JmeImporter e) throws IOException {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		super.read(e);
	}

	@Override
	public void setModelBound(final BoundingVolume modelBound) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		super.setModelBound(modelBound);
	}

	@Override
	public void updateModelBound() {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		super.updateModelBound();
	}

	@Override
	public void depthFirstTraversal(final SceneGraphVisitor visitor) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		super.depthFirstTraversal(visitor);
	}

	@Override
	protected void breadthFirstTraversal(final SceneGraphVisitor visitor, final Queue<Spatial> queue) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		super.breadthFirstTraversal(visitor, queue);
	}

	@Override
	public void setKey(final AssetKey key) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		super.setKey(key);
	}

	@Override
	public AssetKey getKey() {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.getKey();
	}

	@Override
	protected void setRequiresUpdates(final boolean f) {
		if (this.thread != null) {
			assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		}
		super.setRequiresUpdates(f);
	}

	@Override
	protected void setBoundRefresh() {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		super.setBoundRefresh();
	}

	@Override
	public void forceRefresh(final boolean transforms, final boolean bounds, final boolean lights) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		super.forceRefresh(transforms, bounds, lights);
	}

	@Override
	public boolean checkCulling(final Camera cam) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.checkCulling(cam);
	}

	@Override
	public void setName(final String name) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		super.setName(name);
	}

	@Override
	public String getName() {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.getName();
	}

	@Override
	public LightList getLocalLightList() {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.getLocalLightList();
	}

	@Override
	public LightList getWorldLightList() {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.getWorldLightList();
	}

	@Override
	public Quaternion getWorldRotation() {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.getWorldRotation();
	}

	@Override
	public Vector3f getWorldTranslation() {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.getWorldTranslation();
	}

	@Override
	public Vector3f getWorldScale() {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.getWorldScale();
	}

	@Override
	public Transform getWorldTransform() {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.getWorldTransform();
	}

	@Override
	public void rotateUpTo(final Vector3f newUp) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		super.rotateUpTo(newUp);
	}

	@Override
	public void lookAt(final Vector3f position, final Vector3f upVector) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		super.lookAt(position, upVector);
	}

	@Override
	protected void updateWorldLightList() {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		super.updateWorldLightList();
	}

	@Override
	protected void updateWorldTransforms() {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		super.updateWorldTransforms();
	}

	@Override
	public void runControlRender(final RenderManager rm, final ViewPort vp) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		super.runControlRender(rm, vp);
	}

	@Override
	public void addControl(final Control control) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		super.addControl(control);
	}

	@Override
	public void removeControl(final Class<? extends Control> controlType) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		super.removeControl(controlType);
	}

	@Override
	public boolean removeControl(final Control control) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.removeControl(control);
	}

	@Override
	public <T extends Control> T getControl(final Class<T> controlType) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.getControl(controlType);
	}

	@Override
	public Control getControl(final int index) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.getControl(index);
	}

	@Override
	public int getNumControls() {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.getNumControls();
	}

	@Override
	public Vector3f localToWorld(final Vector3f in, final Vector3f store) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.localToWorld(in, store);
	}

	@Override
	public Vector3f worldToLocal(final Vector3f in, final Vector3f store) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.worldToLocal(in, store);
	}

	@Override
	public Node getParent() {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.getParent();
	}

	@Override
	public boolean removeFromParent() {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.removeFromParent();
	}

	@Override
	public boolean hasAncestor(final Node ancestor) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.hasAncestor(ancestor);
	}

	@Override
	public Quaternion getLocalRotation() {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.getLocalRotation();
	}

	@Override
	public void setLocalRotation(final Matrix3f rotation) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		super.setLocalRotation(rotation);
	}

	@Override
	public void setLocalRotation(final Quaternion quaternion) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		super.setLocalRotation(quaternion);
	}

	@Override
	public Vector3f getLocalScale() {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.getLocalScale();
	}

	@Override
	public void setLocalScale(final float localScale) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		super.setLocalScale(localScale);
	}

	@Override
	public void setLocalScale(final float x, final float y, final float z) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		super.setLocalScale(x, y, z);
	}

	@Override
	public void setLocalScale(final Vector3f localScale) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		super.setLocalScale(localScale);
	}

	@Override
	public Vector3f getLocalTranslation() {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.getLocalTranslation();
	}

	@Override
	public void setLocalTranslation(final Vector3f localTranslation) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		super.setLocalTranslation(localTranslation);
	}

	@Override
	public void setLocalTranslation(final float x, final float y, final float z) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		super.setLocalTranslation(x, y, z);
	}

	@Override
	public void setLocalTransform(final Transform t) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		super.setLocalTransform(t);
	}

	@Override
	public Transform getLocalTransform() {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.getLocalTransform();
	}

	@Override
	public void addLight(final Light light) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		super.addLight(light);
	}

	@Override
	public void removeLight(final Light light) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		super.removeLight(light);
	}

	@Override
	public Spatial move(final float x, final float y, final float z) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.move(x, y, z);
	}

	@Override
	public Spatial move(final Vector3f offset) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.move(offset);
	}

	@Override
	public Spatial scale(final float s) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.scale(s);
	}

	@Override
	public Spatial scale(final float x, final float y, final float z) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.scale(x, y, z);
	}

	@Override
	public Spatial rotate(final Quaternion rot) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.rotate(rot);
	}

	@Override
	public Spatial rotate(final float xAngle, final float yAngle, final float zAngle) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.rotate(xAngle, yAngle, zAngle);
	}

	@Override
	public Spatial center() {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.center();
	}

	@Override
	public CullHint getCullHint() {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.getCullHint();
	}

	@Override
	public BatchHint getBatchHint() {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.getBatchHint();
	}

	@Override
	public Bucket getQueueBucket() {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.getQueueBucket();
	}

	@Override
	public ShadowMode getShadowMode() {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.getShadowMode();
	}

	@Override
	public Spatial clone() {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.clone();
	}

	@Override
	public void setUserData(final String key, final Object data) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		super.setUserData(key, data);
	}

	@Override
	public <T> T getUserData(final String key) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.getUserData(key);
	}

	@Override
	public Collection<String> getUserDataKeys() {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.getUserDataKeys();
	}

	@Override
	public boolean matches(final Class<? extends Spatial> spatialSubclass, final String nameRegex) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.matches(spatialSubclass, nameRegex);
	}

	@Override
	public BoundingVolume getWorldBound() {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.getWorldBound();
	}

	@Override
	public void setCullHint(final CullHint hint) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		super.setCullHint(hint);
	}

	@Override
	public void setBatchHint(final BatchHint hint) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		super.setBatchHint(hint);
	}

	@Override
	public CullHint getLocalCullHint() {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.getLocalCullHint();
	}

	@Override
	public BatchHint getLocalBatchHint() {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.getLocalBatchHint();
	}

	@Override
	public Bucket getLocalQueueBucket() {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.getLocalQueueBucket();
	}

	@Override
	public ShadowMode getLocalShadowMode() {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.getLocalShadowMode();
	}

	@Override
	public FrustumIntersect getLastFrustumIntersection() {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.getLastFrustumIntersection();
	}

	@Override
	public void setLastFrustumIntersection(final FrustumIntersect intersects) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		super.setLastFrustumIntersection(intersects);
	}

	@Override
	public Matrix4f getLocalToWorldMatrix(final Matrix4f store) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.getLocalToWorldMatrix(store);
	}

	@Override
	public void setQueueBucket(final Bucket queueBucket) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		super.setQueueBucket(queueBucket);
	}

	@Override
	public void setShadowMode(final ShadowMode shadowMode) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		super.setShadowMode(shadowMode);
	}

	@Override
	public String toString() {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		return super.toString();
	}

	@Override
	public void breadthFirstTraversal(final SceneGraphVisitor visitor) {
		assert Thread.currentThread() == this.thread : "Expected " + this.thread.getName() + " but got " + Thread.currentThread();
		super.breadthFirstTraversal(visitor);
	}

}
1 Like

I’m curious about that. Why don’t you leave all the rendering to clients? I’ve never heard about a server wondering about client viewing considerations. Could give some details?

Server side scene culling is a means to prevent cheaters fom knowing where nearby enemies are that the player can not see. I know some multiplayer team tactic games that use this. They also use this in combination with highlighting those targets that are potentially visible*.

(* = that’s the other side of the medal: make it as visible as possible - draw a red outline for example. The reason for this is that cheaters could remove vegetation, e.g. forrests and bushes, to have a more clear view to partially hidden enemies than the other players)

2 Likes

If your level is static, I might want to direct you to
http://commons.apache.org/proper/commons-math/apidocs/org/apache/commons/math3/geometry/partitioning/BSPTree.html

Basically create a tree with a extra tool that contains what is visible from where materialized.
Now you only drop in the location and get back a true or false.

If you have static (mostly indoor) levels this is probably the most efficient technique that exists.

There are others too (e.g. kd-tree) and cells & portals.
Problem is that many culling solutions are ‘conservative’ and not ‘exact’.
To prevent exploits by cheaters, an exact culling technique is needed.

I think the question of methusalah was more in the direction:
“Why do scene culling on server and not on clients?”

1 Like

@methusalah Very good question. I created a dynamically optimizing network protocol because my game is an opened world to scale space game with tens of thousands of individually moving spatials supporting tens of thousands of users with physics/ ai (Player and npc) / player commands (Even first person real time flight) etc all controlled by the server and running smoothly even on low bandwidth. It’s a space MMO. I explained my network optimization HERE

Since I already have the real time camera location and rotation of every user because of the network protocol needs, I also run server side culling based on either the default server seting of “Cull objects that have visible surface areas less than X”, or some other in game server AI logic. For example, objects phase in and out when inside certain nedulas, or if the objects are not in the camera frustum at all.

Why? Three reasons:

  1. If the server controls the culling it always knows which spatials to ignore and not send any updates for.
  2. EVERYTHING is controlled by the server so ABSOLUTELY no cheating.
  3. Applying an MVC (Model - View - Controller) design pattern to a networked game framework where the client is truly the View makes the code clean, efficient, and massively portable in the future.
2 Likes