Children inheriting scale before rotation?

I want to make a cylinder that rotates each frame, then gets scaled (smaller) along the y axis (say, small enough to make it look like a quad). The effect I’m trying to achieve is that the texture on the cylinder would appear to be moving sideways, but the cylinder would not.appear to rotate. My setup for the scene is as follows:

Scene node - the node for the scene (rootNode for example).
Intermediate node - I set the scale of this object.
Cylinder geometry object - I set the rotation of this object.

My expectation would be that the object at the lowest level (the geometry in this case) be rotated first, then the next level spatial’s scale would apply. I’m finding the opposite. It’s as if I’m applying scale and rotation to the same thing.

If I have a big scene in a single node, and I scaled it on one or two axes, my expectation would be that objects in the same level keep their relative scales on those axes, but that doesn’t happen if any of them have different rotations.

Is this intentional? And if it is, why?

I threw together some code in case anyone wanted to see what I was doing:
[java] @Override
public void simpleInitApp()
{
Cylinder test=new Cylinder(16,16,2,2);
geom=new Geometry("",test);
Node temp=new Node("");
temp.attachChild(geom);
temp.rotate(0,FastMath.HALF_PI,0);//rotate so it faces the samera
temp.setLocalScale(1,0.1f,1);
Material mat = new Material(assetManager,“Common/MatDefs/Misc/Unshaded.j3md”);
mat.setTexture(“ColorMap”,assetManager.loadTexture(“Interface/Logo/Monkey.jpg”));
geom.setMaterial(mat);
rootNode.attachChild(temp);
temp.setLocalTranslation(0,0,-5);
}

@Override
public void simpleUpdate(float tpf)
    {
    geom.rotate(0,0,0.03f);
    }[/java]

https://wiki.jmonkeyengine.org/legacy/doku.php/jme3:scenegraph_for_dummies

Transformations applied to a node are applied to all child nodes. Child transforms are applied first.

If you want to scale the geometry then scale the geometry, not the node.

Then you can do whatever you like in the node transforms and it works on the already-scaled geometry.

@zarch said: https://wiki.jmonkeyengine.org/legacy/doku.php/jme3:scenegraph_for_dummies

Transformations applied to a node are applied to all child nodes. Child transforms are applied first.

If you want to scale the geometry then scale the geometry, not the node.

Then you can do whatever you like in the node transforms and it works on the already-scaled geometry.

He’s using non-uniform scaling and I know that JME has had problems with this in the past. I actually couldn’t tell from the description whether the problem is one of expectations or a bug…

In this case, a couple pictures would be extremely helpful.

Edit: a case in point from my own experience was my cloud layer. I want it to be a squashed sphere and so I used non-uniform scaling. But no matter where I placed the scaling the hierarchy, rotating the cloud layer also rotated the scaling (which was not what I wanted). I ended up having to hack my .vert shader to get the effect I wanted. There were some later changes made to how scaling is applied and I’ve never gone back to see if the issue was fixed. It was like scale was accumulated separately and then applied to the Geometry.

A simple test case is warranted, I think.

I’m not sure what screenshots or test cases you want. What you described is exactly the problem I’m having (I searched the forum for non uniform scaling after reading your post, but none of the results had any solutions).

I essentially want a flat texture to scroll sideways as though it were an animated texture on a quad, and I didn’t want to do it by changing texture coordinates every frame (that doesn’t seem very efficient)

@Shard said: I'm not sure what screenshots or test cases you want. What you described is exactly the problem I'm having (I searched the forum for non uniform scaling after reading your post, but none of the results had any solutions).

I essentially want a flat texture to scroll sideways as though it were an animated texture on a quad, and I didn’t want to do it by changing texture coordinates every frame (that doesn’t seem very efficient)

A picture of the scaling looking right and a picture of the rotated scaling looking wonky is probably enough for others to understand the issue. Or a test case illustrating this with a rotating cube or something.

As an aside: changing the texture coordinates wouldn’t be that bad if you just plug the new coordinates into the existing buffer. Other than that, a custom shader is also an alternative.

Here’s my test texture, being applied to an open cylinder.</img>

And here are my examples:
</img>
All three cylinders have been placed in different nodes that have been scaled to 1/10 on the Z axis. The cylinders themselves have no scaling applied to them.
The cylinder on the left has no rotations applied to it.
The cylinder in the middle is what I would expect to get when rotating the cylinder 90 degrees clock about the y axis. (I faked it by changing the texture in photoshop)
The cylinder on the right is what I actually get when rotating the cylinder 90 degrees clock about the y axis.

Yeah, looks like the same issue.

If someone is going to look into fixing this someday, then you could save them a lot of time by posting a single class test case illustrating the issue.

Edit: also be clear about what version of JME you are using just in case.

[java]import com.jme3.app.SimpleApplication;
import com.jme3.light.;
import com.jme3.material.Material;
import com.jme3.math.
;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.shape.*;

/*

  • This class shows how parent-level scaling (on nodes for example) applies to

  • child nodes before child rotations are applied. In complex scenes or models

  • this causes the unexpected result that some objects in the scene exhibit

  • scaling in the wrong dimensions.

  • testModel1 (which appears on the left) is the model with the incorrect scaling.

  • Its torso and sword have been rotated 90 degrees away from the other geoms.

  • testModel2 (which appears on the right) is the model where all of its children

  • have the same rotations.

  • The result is that when testModel1 gets scaled, every child is scaled on the same

  • dimension except the torso and sword geoms, which end up being scaled on a

  • different dimension instead.

  • This happens because all rotations seem to apply before all scalings.

  • With same-level transforms this makes sense, but the expected behavior is that

  • parent-level scaling should apply after child-level rotations.
    */
    public class NonUnifiedScalingTest extends SimpleApplication
    {
    Node testModel=new Node("");
    Node testModel2=new Node("");
    Material checkerMat,blankMat;
    float time=0.0f;

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

    @Override
    public void simpleInitApp()
    {
    checkerMat=new Material(assetManager,“Common/MatDefs/Light/Lighting.j3md”);
    checkerMat.setTexture(“DiffuseMap”, assetManager.loadTexture(“Textures/test1.jpg”));

     blankMat=new Material(assetManager,"Common/MatDefs/Light/Lighting.j3md");
     
     DirectionalLight light=new DirectionalLight();
     light.setDirection(new Vector3f(1,-1,-1));
     rootNode.addLight(light);
     
     DirectionalLight light2=new DirectionalLight();
     light2.setDirection(new Vector3f(-1,-1,-1));
     light2.setColor(new ColorRGBA(0.5f,0.5f,1,1));
     rootNode.addLight(light2);
     
     flyCam.setMoveSpeed(20.0f);
     makeRandomClutter();
     makeTestModel();
     makeExpectedModel();
     }
    

    /*

    • Make some clutter for lighting
      */
      public void makeRandomClutter()
      {
      Box box=new Box(1,1,1);
      Geometry temp=new Geometry("",box);
      temp.rotate(0,FastMath.PI/4.0f,FastMath.PI/4.0f);
      temp.setLocalTranslation(0, 0, -3);
      temp.setMaterial(blankMat);
      rootNode.attachChild(temp);
      }

    public Geometry makeCylinderGeom(float width, float height)
    {
    Cylinder cylinder=new Cylinder(16,16,width/5.0f,height/5.0f,true);
    Geometry result=new Geometry("",cylinder);
    //make it face the camera, nothing else in this class uses x rotations
    result.rotate(-FastMath.HALF_PI,0,0);
    result.setMaterial(checkerMat);
    return result;
    }

    /*

    • The model whose torso and sword geoms are rotated differently.
      */
      public void makeTestModel()
      {
      Geometry head=makeCylinderGeom( 2, 3.5f);
      Geometry torso=makeCylinderGeom( 4, 8f);
      Geometry armLeft=makeCylinderGeom( 1.2f, 6f);
      Geometry armRight=makeCylinderGeom( 1.2f, 6f);
      Geometry legLeft=makeCylinderGeom( 1.5f, 7f);
      Geometry legRight=makeCylinderGeom( 1.5f, 7f);

      head.setLocalTranslation(0,2,0);
      torso.setLocalTranslation(0,1,0);
      armLeft.setLocalTranslation(-1,1,0);
      armRight.setLocalTranslation(1,1,0);
      legLeft.setLocalTranslation(-0.3f, -.5f,0);
      legRight.setLocalTranslation(0.3f, -.5f, 0);

      Box box=new Box(1f,0.2f,0.2f);
      Geometry sword=new Geometry("",box);
      sword.rotate(0,FastMath.HALF_PI,0);
      sword.setMaterial(blankMat);
      sword.setLocalTranslation(1,0.5f,1);
      testModel.attachChild(sword);

      //rotate only the torso
      torso.rotate(0,0,FastMath.HALF_PI);

      testModel.attachChild(head);
      testModel.attachChild(torso);
      testModel.attachChild(armLeft);
      testModel.attachChild(armRight);
      testModel.attachChild(legLeft);
      testModel.attachChild(legRight);
      testModel.setLocalTranslation(-2,0,0);
      rootNode.attachChild(testModel);
      }

    /*

    • The model where all rotations are initially the same.
      */
      public void makeExpectedModel()
      {
      Geometry head=makeCylinderGeom( 2, 3.5f);
      Geometry torso=makeCylinderGeom( 4, 8f);
      Geometry armLeft=makeCylinderGeom( 1.2f, 6f);
      Geometry armRight=makeCylinderGeom( 1.2f, 6f);
      Geometry legLeft=makeCylinderGeom( 1.5f, 7f);
      Geometry legRight=makeCylinderGeom( 1.5f, 7f);

      head.setLocalTranslation(0,2,0);
      torso.setLocalTranslation(0,1,0);
      armLeft.setLocalTranslation(-1,1,0);
      armRight.setLocalTranslation(1,1,0);
      legLeft.setLocalTranslation(-0.3f, -.5f,0);
      legRight.setLocalTranslation(0.3f, -.5f, 0);

      Box box=new Box(0.2f,0.2f,1);
      Geometry sword=new Geometry("",box);
      sword.setMaterial(blankMat);
      sword.setLocalTranslation(1,0.5f,1);
      testModel2.attachChild(sword);
      //don’t apply any geom-level rotations to this model

      testModel2.attachChild(head);
      testModel2.attachChild(torso);
      testModel2.attachChild(armLeft);
      testModel2.attachChild(armRight);
      testModel2.attachChild(legLeft);
      testModel2.attachChild(legRight);
      testModel2.setLocalTranslation(2,0,0);
      rootNode.attachChild(testModel2);
      }

    /*

    • The update loop will periodically set the scale and/or rotation
    • of the entire model.
      /
      @Override
      public void simpleUpdate(float tpf)
      {
      time+=tpf;
      float adjustedTime=(time%10.0f);
      //0-2.99 seconds: rotate with no scaling
      if (adjustedTime>=0 && adjustedTime<3.0f)
      {
      float angle=2
      FastMath.PI*(adjustedTime/3.0f);
      Quaternion q=new Quaternion();
      q.fromAngles(0,angle,0);
      testModel.setLocalRotation(q);
      testModel2.setLocalRotation(q);
      }
      //3-4.99 seconds: scale down
      else if (adjustedTime>=3 && adjustedTime<5.0f)
      {
      adjustedTime-=3.0f;
      float scale=1f-(adjustedTime/3.0f);
      testModel.setLocalScale(scale,1,1);
      testModel2.setLocalScale(scale,1,1);
      }
      //5-7.99 seconds: rotate with scaling
      else if (adjustedTime>=5 && adjustedTime<8.0f)
      {
      adjustedTime-=5.0f;
      float angle=2FastMath.PI(adjustedTime/3.0f);
      Quaternion q=new Quaternion();
      q.fromAngles(0,angle,0);
      testModel.setLocalRotation(q);
      testModel2.setLocalRotation(q);
      }
      //8-9.99 seconds: unscale
      else if (adjustedTime>=8 && adjustedTime<10.0f)
      {
      adjustedTime-=8.0f;
      float scale=1f-((2-adjustedTime)/3.0f);
      testModel.setLocalScale(scale,1,1);
      testModel2.setLocalScale(scale,1,1);
      }
      }

}
[/java]
The image I used on the cylinders: (if you want you can use your own, it’s just to differentiate between which directions the geoms are facing):

3 Likes

I haven’t tried the (more complicated than necessary) test case yet but thanks for posting it. In your test case it looks like you are rotating and scaling the same node. So I’m not sure how that tests the issue.

I thought the problem was scaling an upper node and having the children rotated… in which case the bad result was that the scaling was rotated in the children when it should not be. You should be able to demonstrate that issue with one node and one or two box children, actually.

I’m rotating the node in the update to show what the scaling looks like without the user having to move the camera around. The box/cylinder rotation is done in the method that makes the model. You can comment out the rotation in update and still see the problem.

1 Like

This test case may be overly complicated for someone who doesn’t already understand what the issue is. Some time when I have time I may make a simpler one with just two boxes.

Edit: for anyone curious or ambitious, the test would be:

Node A
->Blue Box
->Node B
---->Red Box offset from blue box

Then set non-uniform scaling on Node A.
Then rotate Node B on update.

…if things were working right then the red box should look deformed and rotate through the deformation. If things are still not right then the deformation will rotate and the red box will appear to be a static shape (just rotating).

1 Like

I tested it, the red box appeared as a static rotating (but non-uniformly scaled) shape.

I did another test where I chain multiplied the transform matrices from the branch, and this time I got the result Paul described.

However I am having difficulty picturing this as a bug… as a game developer I think I would prefer the former result where the scale is separated from the rest of the transforms, rather than this one where the whole thing looks distorted.

@Momoko_Fan said: I did another test where I chain multiplied the transform matrices from the branch, and this time I got the result Paul described.

However I am having difficulty picturing this as a bug… as a game developer I think I would prefer the former result where the scale is separated from the rest of the transforms, rather than this one where the whole thing looks distorted.

No… non-uniform scale should be applied properly or not allowed. Otherwise, if you down scale (non-uniform) a model and then rotate parts of it then things won’t line up right anymore, etc… If you squish space in a particular direction then the space should be squished. This is the way every other scene graph I’ve ever used has worked.

<cite>@pspeed said:</cite> No... non-uniform scale should be applied properly or not allowed. Otherwise, if you down scale (non-uniform) a model and then rotate parts of it then things won't line up right anymore, etc.. If you squish space in a particular direction then the space should be squished. This is the way every other scene graph I've ever used has worked.
Other engines also don't allow non-uniform scale, Ogre3D for example. And non-uniform scale should work correctly when set on the leaves (geometries). The only alternative is to store world transform as a 4x4 matrix .. but then you cannot get a spatial's world rotation or scale directly, you would have to compute it on an ad-hoc basis.
@Momoko_Fan said: Other engines also don't allow non-uniform scale, Ogre3D for example. And non-uniform scale should work correctly when set on the leaves (geometries). The only alternative is to store world transform as a 4x4 matrix .. but then you cannot get a spatial's world rotation or scale directly, you would have to compute it on an ad-hoc basis.

Or keep them separately. This is also why some scene graphs accumulate the world transform on demand or during rendering… which would still duplicate the state, essentially.

If we want to fix this with a documentation change then we can. Here is what we should put:

Caveat regarding non-uniform scaling: Most scene graphs treat a transform as the definition of local space. Mostly JME does too except where non-uniform scaling is concerned. In that case, JME applies it in the leafs' local coordinate space so it's more like applying local non-uniform scale everywhere instead of defining a coordinate space. This may make it difficult to line things up properly and if transferring a set of transforms from another scene graph then the non-uniform scales cannot be duplicated properly. Non-uniform scaling should therefore be avoided unless you apply it directly to Geometry or otherwise have an expert understanding of what you are doing.

I guess another approach would be to kick in a 4x4 (even though only 3x4 is needed) when doing non-uniform scaling. At least JME has abstracted the idea of a Transform so there are options. The easiest is to just continue not supporting it, though.

Are there actually any real use-cases where non-uniform scaling of nodes other than the leaf is required?

@zarch said: Are there actually any real use-cases where non-uniform scaling of nodes other than the leaf is required?

My world of inception example relies pretty heavily on it but its also only one level basically.

@zarch said: Are there actually any real use-cases where non-uniform scaling of nodes other than the leaf is required?

Yeah, there are a few. So far I’ve hacked shaders to make it happen when I needed it.

For example, I have cloud layers that are a bunch of quads arranged spherically. I wanted to squash the sphere flat but non-uniform scaling did not work… so I flatten it in the .vert shader instead. Since mine was only a visual trick and I didn’t need the quads to be flattened in a JME sense then that was ok.

The OP’s example is definitely one I’d have done with a shader anyway… a texture offset is an easy uniform to add.