[SOLVED] Viewing frustum

Hi everyone, I would like to write a test class to debug the visual cone of the camera as shown in the example image.

I created two views on the test environment by splitting the screen in the following way.

     private void splitScreen() {
        Camera camera = cam.clone();
        camera.setViewPort(0f, 1, .5f, 1f);
        camera.setLocation(new Vector3f(-15f, 5f, -10));
        camera.lookAt(new Vector3f(1, 1, 0), Vector3f.UNIT_Y);

        ViewPort vp = renderManager.createMainView("TopCam", camera);
        vp.setClearFlags(true, true, true);
        vp.attachScene(rootNode);
    }

I created the camera cone shape with the following code.

public static Geometry createViewingFrustum(Camera cam) {
            
	Vector3f[] points = new Vector3f[8];
	for (int i = 0; i < 8; i++) {
		points[i] = new Vector3f();
	}
	
	ShadowUtil.updateFrustumPoints2(cam, points);
	WireFrustum wireFrustum = new WireFrustum(points);
	
	Geometry frustumGeo = new Geometry("Camera-Frustum", wireFrustum);
	Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
	mat.setColor("Color", ColorRGBA.White);
	frustumGeo.setMaterial(mat);
	frustumGeo.setCullHint(Spatial.CullHint.Never);
	frustumGeo.setShadowMode(RenderQueue.ShadowMode.Off);
	return frustumGeo;
}

I would like the cone to follow the position and rotation of the camera during the update cycle, but after many attempts I could not find the solution.

Here is the code and the result i get.

public class AvatarControl extends AbstractControl {

	private Camera camera;
	private Geometry viewingFrustum;
	
	...
	
	public AvatarControl(Node rootNode, Camera camera) {
		this.camera = camera;
		this.viewingFrustum = JMonkey3.Debug.createViewingFrustum(camera);
        rootNode.attachChild(viewingFrustum);
	}

	@Override
    protected void controlUpdate(float tpf) {
		
		...
		
		viewingFrustum.setLocalTranslation(camera.getLocation().clone());
		viewingFrustum.setLocalRotation(camera.getRotation().clone());
	}

}

Could someone help me please?

What way does the wirefrustum point by default? My guess is “not down the Z-axis”… in which case the easiest approach is to put a node around your geometry so that you can prerotate the geometry so that it looks the appropriate direction.

right, that’s a point of view that I hadn’t considered :slight_smile: if so this would solve the rotation problem.
Why does the position also not match?

If I read what you mean right:
What way –does the wirefrustum point– is the wirefrustum positions by default? My guess is “not with eye at the origin”… in which case the easiest approach is to put a node around your geometry so that you can pretranslate the geometry so that it’s positioned at the appropriate direction.

I fixed it, but there is probably a bug in the constructor of the WireFrustum class.
To understand where I was placing the camera’s visual cone, I attached the geometry to a node and saved it as a .j3o file. If I build the Mesh with the constructor new WireFrustum(points), when I open the .j3o file in the Editor I get the following error message:

binary-frustum

If I use the static method WireFrustum.makeFrustum(points) to build the Mesh of the cone, everything works.

This code does not work:

public static Node createCameraFrustum(Camera cam) {
            
	Vector3f[] points = new Vector3f[8];
	for (int i = 0; i < 8; i++) {
		points[i] = new Vector3f();
	}
	
	Camera frustumCam = cam.clone();
	ShadowUtil.updateFrustumPoints2(frustumCam, points);
	WireFrustum mesh = new WireFrustum(points); /** error */
	
	Geometry frustumGeo = new Geometry("Viewing.Frustum", mesh);
	Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
	mat.setColor("Color", ColorRGBA.White);
	frustumGeo.setMaterial(mat);
	frustumGeo.setCullHint(Spatial.CullHint.Never);
	frustumGeo.setShadowMode(RenderQueue.ShadowMode.Off);
	
	Node node = new Node("WireFrustum");
	node.attachChild(frustumGeo);
        
	/**
	 * Save a Node to a .j3o file.
	 */
	String userHome = System.getProperty("user.dir");
	BinaryExporter exporter = BinaryExporter.getInstance();
	File file = new File(userHome + "/assets/Models/wire-frustum.j3o");
	try {
		System.out.println(file.getAbsolutePath());
		exporter.save(node, file);
		
	} catch (IOException ex) {
		System.err.println("Failed to save node!");
		ex.printStackTrace();
	}
	
	return node;
}

This code works:

public static Node createCameraFrustum(Camera cam) {
            
	Vector3f[] points = new Vector3f[8];
	for (int i = 0; i < 8; i++) {
		points[i] = new Vector3f();
	}
	
	Camera frustumCam = cam.clone();
	ShadowUtil.updateFrustumPoints2(frustumCam, points);
	Mesh mesh = WireFrustum.makeFrustum(points); /** works */
	
	Geometry frustumGeo = new Geometry("Viewing.Frustum", mesh);
	Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
	mat.setColor("Color", ColorRGBA.White);
	frustumGeo.setMaterial(mat);
	frustumGeo.setCullHint(Spatial.CullHint.Never);
	frustumGeo.setShadowMode(RenderQueue.ShadowMode.Off);
	
	Node node = new Node("WireFrustum");
	node.attachChild(frustumGeo);
        
	/**
	 * Save a Node to a .j3o file.
	 */
	String userHome = System.getProperty("user.dir");
	BinaryExporter exporter = BinaryExporter.getInstance();
	File file = new File(userHome + "/assets/Models/wire-frustum.j3o");
	try {
		System.out.println(file.getAbsolutePath());
		exporter.save(node, file);
		
	} catch (IOException ex) {
		System.err.println("Failed to save node!");
		ex.printStackTrace();
	}
	
	return node;
}

After several attempts, I found the right code to center the origin of the visual cone in the origin of the axes and to orient it correctly with the z axis. (see figure):

camera-scenegraph

for posterity here is the final code to correctly create the camera view cone centered in the origin and oriented with the z axis:

public static Geometry createCameraFrustum(Camera cam) {
	
	Vector3f[] points = new Vector3f[8];
	for (int i = 0; i < 8; i++) {
		points[i] = new Vector3f();
	}
	
	Camera frustumCam = cam.clone();
	frustumCam.setLocation(new Vector3f(0, 0, 0));
	frustumCam.lookAt(Vector3f.UNIT_Z, Vector3f.ZERO);
	ShadowUtil.updateFrustumPoints2(frustumCam, points);
	Mesh mesh = WireFrustum.makeFrustum(points);
	
	Geometry frustumGeo = new Geometry("Viewing.Frustum", mesh);
	Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
	mat.setColor("Color", ColorRGBA.White);
	frustumGeo.setMaterial(mat);
	frustumGeo.setCullHint(Spatial.CullHint.Never);
	frustumGeo.setShadowMode(RenderQueue.ShadowMode.Off);
	
	return frustumGeo;
}
2 Likes

It’s really strange that this works at all with no up vector. You really should specify y-up or something.

Glad you figured out how to get things working, though.

2 Likes

sorry, my mistake in copying the code.

Here is the correct one:

Camera frustumCam = cam.clone();
frustumCam.setLocation(new Vector3f(0, 0, 0));
frustumCam.lookAt(Vector3f.UNIT_Z, Vector3f.UNIT_Y);
ShadowUtil.updateFrustumPoints2(frustumCam, points);

a point of attention perhaps deserves the constructor of the WireFrustum class, whose Mesh is not saved correctly in the binary file .j3o. There may be a bug in the engine.
I am using jme-3.3.2 stable

To fix an exception we need the stack trace. Since the stack trace is 99% of the useful information, I and others often just ignore mentions of them without a stack trace.

here is the example and the stacktrace.

package mygame;

import com.jme3.app.SimpleApplication;
import com.jme3.export.binary.BinaryExporter;
import com.jme3.light.AmbientLight;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Node;
import com.jme3.scene.SceneGraphVisitorAdapter;
import com.jme3.scene.Spatial;
import com.jme3.scene.debug.WireFrustum;
import com.jme3.shadow.ShadowUtil;
import java.io.File;
import java.io.IOException;

/**
 *
 * @author capdevon
 */
public class TestIssueWireFrustum extends SimpleApplication {
    
    private final String assetKey = "wire-frustum.j3o";
    
    /**
     * 
     * @param args 
     */
    public static void main(String[] args) {
        TestIssueWireFrustum app = new TestIssueWireFrustum();
        app.start();
    }

    @Override
    public void simpleInitApp() {
        
        AmbientLight ambient = new AmbientLight();
        ambient.setColor(ColorRGBA.White);
        rootNode.addLight(ambient);

        createCameraFrustum(cam);
        Node node = (Node) assetManager.loadModel("Models/" + assetKey);
        rootNode.attachChild(node);

        node.breadthFirstTraversal(new SceneGraphVisitorAdapter() {
            @Override
            public void visit(Geometry geo) {
                System.out.println(geo.getMesh());
            }
        });
    }

    private Node createCameraFrustum(Camera cam) {

        Vector3f[] points = new Vector3f[8];
        for (int i = 0; i < 8; i++) {
            points[i] = new Vector3f();
        }

        Camera frustumCam = cam.clone();
        ShadowUtil.updateFrustumPoints2(frustumCam, points);
        WireFrustum mesh = new WireFrustum(points);     // doesn't works
        //Mesh mesh = WireFrustum.makeFrustum(points);  // this works

        Geometry frustumGeo = new Geometry("Viewing.Frustum", mesh);
        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        mat.setColor("Color", ColorRGBA.White);
        frustumGeo.setMaterial(mat);
        frustumGeo.setCullHint(Spatial.CullHint.Never);
        frustumGeo.setShadowMode(RenderQueue.ShadowMode.Off);

        Node node = new Node("WireFrustum");
        node.attachChild(frustumGeo);

        /**
         * Save a Node to a .j3o file.
         */
        String userHome = System.getProperty("user.dir");
        BinaryExporter exporter = BinaryExporter.getInstance();
        File file = new File(userHome + "/assets/Models/" + assetKey);
        try {
            System.out.println(file.getAbsolutePath());
            exporter.save(node, file);

        } catch (IOException ex) {
            System.err.println("Failed to save node!");
            ex.printStackTrace();
        }

        return node;
    }

}

StackTrace:

gen 26, 2021 7:02:36 PM class com.jme3.export.binary.BinaryImporter readObject(int id)
SEVERE: Exception
java.lang.InstantiationException: Loading requires a no-arg constructor, but class com.jme3.scene.debug.WireFrustum lacks one.
null
	at com.jme3.export.SavableClassUtil.findNoArgConstructor(SavableClassUtil.java:231)
	at com.jme3.export.SavableClassUtil.fromName(SavableClassUtil.java:174)
	at com.jme3.export.SavableClassUtil.fromName(SavableClassUtil.java:215)
	at com.jme3.export.binary.BinaryImporter.readObject(BinaryImporter.java:331)
	at com.jme3.export.binary.BinaryInputCapsule.readSavable(BinaryInputCapsule.java:457)
	at com.jme3.scene.Geometry.read(Geometry.java:711)
	at com.jme3.export.binary.BinaryImporter.readObject(BinaryImporter.java:342)
	at com.jme3.export.binary.BinaryInputCapsule.resolveIDs(BinaryInputCapsule.java:483)
	at com.jme3.export.binary.BinaryInputCapsule.readSavableArray(BinaryInputCapsule.java:471)
	at com.jme3.export.binary.BinaryInputCapsule.readSavableArrayList(BinaryInputCapsule.java:587)
	at com.jme3.scene.Node.read(Node.java:748)
	at com.jme3.export.binary.BinaryImporter.readObject(BinaryImporter.java:342)
	at com.jme3.export.binary.BinaryImporter.load(BinaryImporter.java:242)
	at com.jme3.export.binary.BinaryImporter.load(BinaryImporter.java:125)
	at com.jme3.export.binary.BinaryImporter.load(BinaryImporter.java:109)
	at com.jme3.export.binary.BinaryLoader.load(BinaryLoader.java:36)
	at com.jme3.asset.DesktopAssetManager.loadLocatedAsset(DesktopAssetManager.java:260)
	at com.jme3.asset.DesktopAssetManager.loadAsset(DesktopAssetManager.java:374)
	at com.jme3.asset.DesktopAssetManager.loadModel(DesktopAssetManager.java:417)
	at com.jme3.asset.DesktopAssetManager.loadModel(DesktopAssetManager.java:421)
	at mygame.TestIssueWireFrustum.simpleInitApp(TestIssueWireFrustum.java:51)
	at com.jme3.app.SimpleApplication.initialize(SimpleApplication.java:239)
	at com.jme3.system.lwjgl.LwjglAbstractDisplay.initInThread(LwjglAbstractDisplay.java:132)
	at com.jme3.system.lwjgl.LwjglAbstractDisplay.run(LwjglAbstractDisplay.java:213)
	at java.lang.Thread.run(Thread.java:748)

gen 26, 2021 7:02:36 PM com.jme3.app.LegacyApplication handleError
SEVERE: Uncaught exception thrown in Thread[jME3 Main,5,main]
java.lang.NullPointerException: Geometry: Viewing.Frustum has null mesh
	at com.jme3.scene.Geometry.updateWorldBound(Geometry.java:307)
	at com.jme3.scene.Spatial.updateGeometricState(Spatial.java:908)
	at com.jme3.scene.Node.updateGeometricState(Node.java:272)
	at com.jme3.scene.Node.updateGeometricState(Node.java:272)
	at com.jme3.app.SimpleApplication.update(SimpleApplication.java:264)
	at com.jme3.system.lwjgl.LwjglAbstractDisplay.runLoop(LwjglAbstractDisplay.java:153)
	at com.jme3.system.lwjgl.LwjglDisplay.runLoop(LwjglDisplay.java:193)
	at com.jme3.system.lwjgl.LwjglAbstractDisplay.run(LwjglAbstractDisplay.java:234)
	at java.lang.Thread.run(Thread.java:748)
2 Likes

Ahah. That’s a pretty easy fix if someone were inclined to submit a PR for it. Just add an empty no-arg protected constructor to WireFrustum so that save/load works.

1 Like

I’ve opened issue 1462 so I won’t forget.

1 Like

Fixed in master branch.

2 Likes