DirectionalLight cast incorrectly when setParallelProjection is enabled

I have a project running in ParallelProjection mode. The camera is setup for (true) isometric view.



Here are 3 cubes lit from light coming straight down the Z axis, Vector3f(0.0f, 0.0f, -1.0f)

http://i.imgur.com/uXDqH.png

Note you can’t see any face other than the one facing the Z axis because the light is uniform and directional down Z.



HOWEVER, when I move the cube around the root node, we start getting lighting artifacts:

http://i.imgur.com/yv98t.png

http://i.imgur.com/yMn7X.png



I can confirm that the DirectionLight is the only light source, because if I don’t attach it to the root node nothing is lit.



Does anyone have any idea why the extra faces are being lit?

Hm, seems like a bug, yes, as the light is directional it should make no differnece where the cube is at.

→ do the objects have tangents?

→ do the objects have normals?

→ does this only happen with parallel projection?

The boxes do define a Specular property:



mat.setColor(“Specular”, ColorRGBA.Gray);



When removing it the coloring becomes uniform as expected.



However, why would specular lighting react to motion of the object if the light source is uniform?

Well wild guess is that specular is something like simulating a reflection, I gues it has something to do with the camera angle to surface and angle to light. But I’m not really good with that stuff ^^

EmpirePhoenix said:
Well wild guess is that specular is something like simulating a reflection, I gues it has something to do with the camera angle to surface and angle to light. But I'm not really good with that stuff ^^


That may be the bug - its reacting to motion relative to the uniform light source attached to the rootNode instead of the camera Frustum.

You can setup an isometric situation much like an MC Escher illusion - two objects in different x,y,z axis appearing on top of each other, at the same scale. However, due to this bug, they will be in the same position, of the same scale, but with different lighting.

I don’t think it’s really a bug, necessarily… just that specular in the sense that you’d like it in an orthogonal world doesn’t really make sense:

http://en.wikipedia.org/wiki/Specular_reflection



It’s view dependent lighting. It’s possible that it’s not working right in orthogonal mode but even if it is working right then you’d still get different lighting depending on where the object is in the camera view.



Is there a reason that you think you need specular highlights in this case?

pspeed said:
I don't think it's really a bug, necessarily... just that specular in the sense that you'd like it in an orthogonal world doesn't really make sense:
http://en.wikipedia.org/wiki/Specular_reflection

It's view dependent lighting. It's possible that it's not working right in orthogonal mode but even if it is working right then you'd still get different lighting depending on where the object is in the camera view.

Is there a reason that you think you need specular highlights in this case?


I would still like specular highlights for rotation around the Z axis.

Something I don’t get…Z axis is not the up axis , it’s Y.

I don’t really get how you set up your scene. Are you looking at it from the top?



Anyway a test case could help here to find the problem.

nehon,



I did more research and you are correct - the up axis for isometric should be Y.



My scene is setup by (sorry I don’t have exact code, will post later tonight):



setParallelProjection(true);

cam.setLocation(Vector3f.UNIT_XYZ); //move to 1,1,1

cam.lookAt(Vector3f.ZERO, Vector3f.UNIT_Z); //look at 0,0,0 with up axis being Z



I now think there are 2 problems with this:


  1. Move to 1,1,1 and look at 0,0,0 isn’t true isometric. The camera should probably be moved to 1,0,0 (or 0,1,0 or 0,0,1) then rotated into position. Rotation should be 45 degrees on XY and 30 degrees on Z.
  2. My lookAt() should use Vector3f.UNIT_Y as the up direction parameter.



    I tried looking to void setFrame(Vector3f location, Quaternion axes) but cannot get Quaternion to behave as expected.



    Quaternion q = Quaternion.IDENTITY;

    q.fromAngleAxis(FastMath.DEG_TO_RAD45.0f, new Vector3f(1.0f,1.0f,0.0f)); //45 degrees on XY

    q.fromAngleAxis(FastMath.DEG_TO_RAD
    30.0f, Vector3f.UNIT_Z); //30 degrees on Z

    cam.setFrame(Vector3f.UNIT_X, q);



    There are two problems with this:


  3. The camera is rotated in place, not about the rootNode. So even if it was rotated to face at the correct angle, it is not in the correct position in space.
  4. The Quaternion is not rotating as expected. It seems as if the fromAngleAxis() operation are not cumulative.



    For 1, I should probably attach the camera to a cameraNode (located at rootNode), and then rotate cameraNode, which will rotate the camera in relation to it. However, I would still like help in figuring out how to setup a Quaternion rotating 45 degrees on XY and 30 degrees on Z.

First read this https://wiki.jmonkeyengine.org/legacy/doku.php/jme3:math_for_dummies.

Rotation are quite tricky, and you need to understand the basis



about 1. (the second one) quaternions represent relative rotations, not absolute, so a quaternion always make an object rotate around it’s center.

Though, once it’s rotated, you can set the location of the cam where you see fit. if you want to compute the position of the cam transformed by the rotation, you have to apply the rotation to the translation by multipliying them rotationQuat.mult(positionvector). This will give you the new position.



Also your node approach is simpler, so i recommend using it, because all math will be done by the engine. But, one thing you have to keep in mind is that the camNode and the camera will always be at the same position, you can’t separate them.

What you have to do is to create a “pivot” node and position it at the center of the scene. create a CameraNode with the camera and attach it to the pivot node. Position it anywhere you want using it’s setLocalTranslation method. Then rotate the pivot node, and the cam node will follow.

Note that you have to set the controlDirection of the camNode to SpatialToCamera.

Then move your camera only by moving the cameraNode.



about 2 fromAngleAxis initialize the quaternion, so indeed it’s not cumulative. If you want to combine rotations, you have to multiply them, but be aware that this multiplicaiton is not reversible. in your case the tight rotation would be



Quaternion q = new Quaternion().fromAngleAxis(FastMath.DEG_TO_RAD45.0f, new Vector3f(1.0f,1.0f,0.0f)); //45 degrees on XY;

q.multLocal( new Quaternion().fromAngleAxis(FastMath.DEG_TO_RAD
30.0f, Vector3f.UNIT_Z));





Also doing Quaternion q = Quaternion.IDENTITY; and then changing q is bad, because you are modifying the IDENTITY constant.

use q=new Quaternion(Quaternion.IDENTITY); instead, it will create a new quat initialized with the IDENTITY.

I have no problems with vectors, I just don’t quite understand Quaternions. The wiki article (both here and on wikipedia) is too abstract and doesn’t provide examples of what the visual effect of modifying Quaternion coefficients is.



I will try this approach when I get home:



Create pivotNode.

Move camera to 1 unit distance away from pivot node.

Attach camera to pivot node.



Create Quaternion q1 with rotation 45 degrees on XY.

Create Quaternion q2 with rotation 30 on Z.



Rotate pivotNode by product of q1 and q2.

cam.lookAt(Vector3f.ZERO);



Am I right to assume that this is how 45 degree rotation on XY axis is done?



Quaternion q = new Quaternion().fromAngleAxis(FastMath.DEG_TO_RAD*45.0f, new Vector3f(1.0f,1.0f,0.0f));



…or would rotation in the XY axis mean its rotating around the Z axis, so Vector3f should be UNIT_Z?

I read the slides at https://wiki.jmonkeyengine.org/legacy/doku.php/jme3:math_for_dummies. and its a EXCELLENT resource, thank you.

That quaternion is something unpredictable as its generated with a non-normalized vector and not an axis (x,y,z).

Look through “rotating a vector” and “combining rotations” in the wiki slideshow @nehon posted again, there you see how combining the quaternions works.

zed03 said:
I will try this approach when I get home:

Create pivotNode.
Move camera to 1 unit distance away from pivot node.
Attach camera to pivot node.

You can't, or only if pivotNode is a CameraNode, but this won't work, because as soo as you hit the first update() method, the cam location will be set to the same as the cameraNode's....and in this case it's the pivot node.
Your pivotnode has to be a standard node. then create a CameraNode attach the cam to it, attach the cameraNode to the pivotNode. THEN move the CameraNode 1 unit distance from the pivot node.

zed03 said:
Am I right to assume that this is how 45 degree rotation on XY axis is done?

Quaternion q = new Quaternion().fromAngleAxis(FastMath.DEG_TO_RAD*45.0f, new Vector3f(1.0f,1.0f,0.0f));

...or would rotation in the XY axis mean its rotating around the Z axis, so Vector3f should be UNIT_Z?

No...this means you are rotating for 45 degrees around the axis (1.0f,1.0f,0.0f). but i doubt that it's what you want.
I guess what you want id rotating 45 around the x axis....(to look a bit from above right?)

Quaternion q = new Quaternion().fromAngleAxis(FastMath.DEG_TO_RAD*45.0f, Vector3f.UNIT_X);
nehon said:
No...this means you are rotating for 45 degrees around the axis (1.0f,1.0f,0.0f). but i doubt that it's what you want.

I also doubt that works with a non-normalized random axis.

The shininess is no longer applied when the angle of the light to the surface is greater than 90 degrees. This is what caused your issue so it should be fixed now

Sorry to necro this post, but I am having a similar problem.

In my case I have also a parallel projection and a directional light. The objects render fine, but objects on one edge of the map shine differently than the ones on the other edge.

If I remove the parallel projection they both shine similar. The shadows however behave the same:

Any reason anyone can think why this is?

need more information i assume. Maybe some simple TestCase you could provide?

I’ll try to make a simple PoC. as is, my code is a little too complex to be ran alone. but basically I am making a hex tile board and putting some characters in some cells. I am using the ninja mesh for testing in the meantime. the camera setup code is this:

private var mFrustumSize = 490F
private var mCamXRot = 1.5F
private var mCamYRot = 0F
private var mCamZRot = -0.5F

private fun setCamera() {
    cam.isParallelProjection = true
    mAspect = cam.width.toFloat() / cam.height
    cam.location = Vector3f.ZERO
    updateFrustum(cam)
    updateCamRotation(cam)
    cam.update()
}

private fun updateCamRotation(cam: Camera) {
    println("Setting Rotation X: $mCamXRot, Y: $mCamYRot, Z: $mCamZRot") //NON-NLS
    cam.rotation = Quaternion(floatArrayOf(mCamXRot * FastMath.HALF_PI, mCamYRot * FastMath.HALF_PI, mCamZRot * FastMath.HALF_PI))
}

private fun updateFrustum(cam: Camera) {
    println("Setting Frustum Size to: $mFrustumSize") //NON-NLS
    cam.setFrustum(-1000F, 1000F, -mAspect * mFrustumSize, mAspect * mFrustumSize, mFrustumSize, -mFrustumSize)
}

as for the light the code is:

val sun = DirectionalLight(Vector3f(-1f, -1f, -1f).normalizeLocal())
mLocalNode.addLight(sun)
val dlsr = DirectionalLightShadowRenderer(engine.assetManager, 1024, 3)
dlsr.light = sun
engine.viewPort.addProcessor(dlsr)

I’m scaling the robots in half as they were a little too big for my board. each tile is about 40 units in the long diagonal.

Let me know if you need anything else for reproducing the issue.

Please note, you wanted me to create TestCase for you. dont do this next time please :slight_smile: Just provide it all.

Here is TestCase i made based on your Kotlin code:

package test;

import com.jme3.app.SimpleApplication;
import com.jme3.light.DirectionalLight;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.scene.Geometry;
import com.jme3.scene.shape.Box;
import com.jme3.shadow.DirectionalLightShadowRenderer;

public class Test3 extends SimpleApplication {

    private final float mFrustumSize = 490f;
    private final float mCamXRot = 1.5f;
    private final float mCamYRot = 0f;
    private final float mCamZRot = -0.5f;
    private float mAspect;

    public static void main(String... argv) {
        Test3 app = new Test3();
        app.start();
    }

    @Override
    public void simpleInitApp() {
        setCamera();
        updateCamRotation(cam);
        updateFrustum(cam);
        DirectionalLight sun = new DirectionalLight(new Vector3f(1f, -0.2f, -1f).normalizeLocal());
        rootNode.addLight(sun);
        DirectionalLightShadowRenderer dlsr = new DirectionalLightShadowRenderer(assetManager, 1024, 3);
        dlsr.setLight(sun);
        viewPort.addProcessor(dlsr);
        createBox(new Vector3f(100, 0, 100));
        createBox(new Vector3f(200, 0, 200));
        createBox(new Vector3f(-100, 0, -100));
        createBox(new Vector3f(-200, 0, -200));
        createBox(new Vector3f(-500, 0, 0));
        inputManager.setCursorVisible(true);
        flyCam.setZoomSpeed(0);
        flyCam.setMoveSpeed(0.0f);
        flyCam.setEnabled(false);
        flyCam.setRotationSpeed(0);
    }

    private void createBox(Vector3f loc) {
        Box b = new Box(100, 100, 100); // create cube shape
        Geometry geom = new Geometry("Box", b);  // create cube geometry from the shape
        Material mat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
        mat.setBoolean("UseMaterialColors", true);
        mat.setColor("Diffuse", ColorRGBA.Gray);
        mat.setColor("Specular", ColorRGBA.White);
        mat.setFloat("Shininess", 64f);  // [0,128]
        geom.setMaterial(mat);                   // set the cube's material
        rootNode.attachChild(geom);              // make the cube appear in the scene
        geom.setLocalTranslation(loc);
    }

    private void setCamera() {
        cam.setParallelProjection(true);
        mAspect = cam.getWidth() / cam.getHeight();
        cam.setLocation(Vector3f.ZERO);
        updateFrustum(cam);
        updateCamRotation(cam);
        cam.update();
    }

    private void updateCamRotation(Camera cam) {
        System.out.println("Setting Rotation X: $mCamXRot, Y: $mCamYRot, Z: $mCamZRot"); //NON-NLS
        cam.setRotation(new Quaternion(new float[]{mCamXRot * FastMath.HALF_PI, mCamYRot * FastMath.HALF_PI, mCamZRot * FastMath.HALF_PI}));
    }

    private void updateFrustum(Camera cam) {
        System.out.println("Setting Frustum Size to: $mFrustumSize"); //NON-NLS
        cam.setFrustum(-1000F, 1000F, -mAspect * mFrustumSize, mAspect * mFrustumSize, mFrustumSize, -mFrustumSize);
    }
}

I were able to replicate some “light difference” you talk about

image:

When using different Camera location like:

cam.setLocation(new Vector3f(1000,1000,1000)); // extending frustrum far too

this box is not more “specular lit”

The “more lit area” what we see here is Specular Since:

    mat.setColor("Specular", ColorRGBA.White);

when this color is black, will not see this area anymore. So this is specular.

It is how it works, but im not sure if its intended for “parallel projection” view.

As i know specular is added by Shader in:
Lighting.frag l:207 and it is BlinnPhongLighting.glsllib#L31

Temporary solution for you @superjugy if you want have exactly same visual,
is to make Specular Color Black. (you can also setup Shininess param so it will not focus on small area)

Im also not sure if your camera rotation or frustrum params might cause exactly this specular appearance.

Anyway: How do you expect specular to work in parallel projection? Because im not sure myself.

You can ofc. use specular and make each “ninja” render in own viewport, so each of them will have own specular. But i dont think specular should affect them a way you want it, without split render into separate render of this characters.

Since im not sure if this works as expected in Parallel Projection, i cant say if its correct or not.

Need to wait for someone opinion.