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

Newbie and multithreaded do not combine well no matter what.

5 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