CartoonEdgeFilter detects edges of meshes without normals

I’ve heard that the CartoonEdgeFilter shouldn’t detect edges on meshes that don’t have normals, but it isn’t necessarily so.

I wrote some test code. It generates a blue sky cube without normals. Yet the filter draws edges on some (but not all) of the faces.

Perhaps someone would like to investigate further and tell us what’s going on …

package mygame;

import com.jme3.app.SimpleApplication;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.post.FilterPostProcessor;
import com.jme3.post.filters.CartoonEdgeFilter;
import com.jme3.renderer.queue.RenderQueue.Bucket;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Node;
import com.jme3.scene.VertexBuffer.Type;

public class Main extends SimpleApplication {

    /**
     * Direction vector for the center of each cube face.
     */
    final private static Vector3f[] faceDirection = {
        new Vector3f(-1f, 0f, 0f),
        new Vector3f(1f, 0f, 0f),
        new Vector3f(0f, 1f, 0f),
        new Vector3f(0f, -1f, 0f),
        new Vector3f(0f, 0f, -1f),
        new Vector3f(0f, 0f, 1f)
    };
    /**
     * Direction vector for 1st (+U) texture coordinate of each cube face.
     */
    final private static Vector3f[] uDirection = {
        new Vector3f(0f, 0f, 1f),
        new Vector3f(0f, 0f, -1f),
        new Vector3f(-1f, 0f, 0f),
        new Vector3f(-1f, 0f, 0f),
        new Vector3f(-1f, 0f, 0f),
        new Vector3f(1f, 0f, 0f)
    };
    /**
     * Direction vector for 2nd (+V) texture coordinate of each cube face.
     */
    final private static Vector3f[] vDirection = {
        new Vector3f(0f, -1f, 0f),
        new Vector3f(0f, -1f, 0f),
        new Vector3f(0f, 0f, -1f),
        new Vector3f(0f, 0f, 1f),
        new Vector3f(0f, -1f, 0f),
        new Vector3f(0f, -1f, 0f)
    };

    class Square extends Mesh {

        public Square() {
            setMode(Mode.TriangleFan);
            setBuffer(Type.Position, 3, new float[]{
                -1f, -1f, 0f,
                -1f, 1f, 0f,
                1f, 1f, 0f,
                1f, -1f, 0f});
            setBuffer(Type.Index, 3, new short[]{0, 3, 2, 1});
            updateBound();
            setStatic();
        }
    }
    Square squareMesh = new Square();
    Node cubeMap = new Node("cube map");

    public static void main(String[] args) {
        Main app = new Main();
        app.start();
    }

    @Override
    public void simpleInitApp() {
        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        mat.setColor("Color", ColorRGBA.Blue);

        cubeMap.setQueueBucket(Bucket.Sky);
        for (int faceIndex = 0; faceIndex < 6; faceIndex++) {
            String faceName = String.format("face%d", faceIndex + 1);
            Geometry geometry = new Geometry(faceName, squareMesh);
            geometry.setMaterial(mat);
            cubeMap.attachChild(geometry);
            /*
             * Set the location of the face.
             */
            Vector3f offset = faceDirection[faceIndex].clone();
            geometry.setLocalTranslation(offset);
            /*
             * Orient the face.
             */
            Vector3f u = uDirection[faceIndex];
            Vector3f v = vDirection[faceIndex];
            Vector3f w = faceDirection[faceIndex].negate();
            Quaternion orientation = new Quaternion();
            orientation.fromAxes(u, v, w);
            geometry.setLocalRotation(orientation);
        }
        rootNode.attachChild(cubeMap);

        CartoonEdgeFilter cartoonEdgeFilter = new CartoonEdgeFilter();
        int numSamples = settings.getSamples();
        FilterPostProcessor fpp = new FilterPostProcessor(assetManager);
        if (numSamples > 0) {
            fpp.setNumSamples(numSamples);
        }
        viewPort.addProcessor(fpp);
        fpp.addFilter(cartoonEdgeFilter);

        cam.setRotation(new Quaternion(0.25563002f, 0.49118856f, -0.15341493f, 0.81844425f));
    }

    @Override
    public void simpleUpdate(float tpf) {
        /*
         * Center the cube map on the camera.
         */
        Vector3f cameraLocation = cam.getLocation();
        cubeMap.setLocalTranslation(cameraLocation);
        /*
         * Scale the cube map so that its geometries lie
         * between the near and far planes of the view frustum.
         */
        float far = cam.getFrustumFar();
        float near = cam.getFrustumNear();
        float cubeRadius = (near + far) / 2f;
        assert cubeRadius > near : cubeRadius;
        cubeMap.setLocalScale(cubeRadius);
    }
}