donmccurdy / glTF-Transform

glTF 2.0 SDK for JavaScript and TypeScript, on Web and Node.js.
https://gltf-transform.dev
MIT License
1.39k stars 149 forks source link

Transform entire scene #1530

Open rotu opened 2 days ago

rotu commented 2 days ago

Is your feature request related to a problem? Please describe. I have a bunch of models which may be rotated and scaled incorrectly, and gltf-transform does not provide an easy way to fix this.

Describe the solution you'd like I would like a new file function that inserts a new root node in all scenes with a given transform.

e.g. gltf-transform reroot --translate [0,0,1] --rotate [1,0,0,1] --scale 0.4 (where the argument to rotate is a non-normalized quaternion)

Describe alternatives you've considered I currently do this with a script like following:

document.getRoot().listScenes().forEach(
  s=>{
    s.listChildren().forEach(c=>{
    const newRoot = document.createNode();
    newRoot.setTranslation([0,0,1])
    newRoot.setRotation(normalize([1,0,0,1]));
    newRoot.setScale([0.4,0.4,0.4])
    newRoot.addChild(c);
    s.addChild(newRoot)
  })}
)

function normalize(v){
  const n = 1/Math.hypot(...v)
  if (!Number.isFinite(n)){ throw new Error() }
  return Array.from(v, x=>x*n)
}

The translate feature has much in common with the center function, so it may make sense to unify these.

rotu commented 2 days ago

Here's a cleaned up and parameterized implementation:

import { Document, Transform, vec3, vec4 } from "@gltf-transform/core";
import { assignDefaults, createTransform } from "@gltf-transform/functions";

const NAME = "transformWorld";

type TransformWorldOptions = {
  translation?: vec3;
  rotation?: vec4;
  scale?: number | vec3;
};
const TRANSFORM_WORLD_DEFAULTS: Required<TransformWorldOptions> = {
  translation: [0, 0, 0] as vec3,
  rotation: [0, 0, 0, 1] as vec4,
  scale: 1,
};

function normalize(v: vec4): vec4;
function normalize(v: number[]) {
  const n = 1 / Math.hypot(...v);
  return Array.from(v, (x) => x * n);
}
function uniform(s): vec3 {
  return [s, s, s];
}
function transformWorld(
  _options: TransformWorldOptions = TRANSFORM_WORLD_DEFAULTS,
): Transform {
  const options = assignDefaults(TRANSFORM_WORLD_DEFAULTS, _options);
  return createTransform(NAME, (doc: Document): void => {
    const logger = doc.getLogger();
    doc
      .getRoot()
      .listScenes()
      .forEach((s) => {
        // let vScale:vec3 =
        const newRoot = doc
          .createNode()
          .setRotation(normalize(options.rotation))
          .setTranslation(options.translation)
          .setScale(
            typeof options.scale === "number"
              ? uniform(options.scale)
              : options.scale,
          );
        s.listChildren().forEach((c) => {
          newRoot.addChild(c);
        });
        s.addChild(newRoot);
      });
    logger.debug(`${NAME}: Complete.`);
  });
}

await document.transform(transformWorld({ rotation: [1, 0, 0, 1] }));
kzhsw commented 2 days ago

What about animations? Skip, remove, or keep

rotu commented 2 days ago

I don’t see animations would need to change anything here. Keep animations and they should still work.

Did I miss something?

donmccurdy commented 1 day ago

Hi @rotu, thanks for the suggestion!

Animations should still work, if we're adding a new parent to the scene and not changing the local transform of any existing animated nodes. The new root will cause some validation warnings (skinned meshes are not supposed to be transformed, only the joint hierarchy) but that's already true for center() and doesn't need to block this proposal.

I think my preferred starting point would be a transformScene(scene, matrix) function, similar to the existing transformMesh. I'm still thinking about how/if it should be exposed as a document-level transform and/or a CLI command.

One more consideration – KHRMaterialsVolume is affected by scale. If we're changing the scene's scale, then the thickness of volumetric materials will need to be updated too, as we do in quantization:

https://github.com/donmccurdy/glTF-Transform/blob/7aa92bea25d775dea3fd067e6bdfe0eb078849ac/packages/functions/src/quantize.ts#L358-L372

rotu commented 1 day ago

skinned meshes are not supposed to be transformed, only the joint hierarchy :+1:

s.listChildren().forEach((c) => {if (c.skin!==null){newRoot.addChild(c)}});

Not sure how to handle inverseBindMatrices, though.

I think my preferred starting point would be a transformScene(scene, matrix) function, similar to the existing transformMesh. I'm still thinking about how/if it should be exposed as a document-level transform and/or a CLI command.

I definitely think it belongs as a CLI command or document-level transform for use in a transform pipeline on gltf.report. If the processing can be abstracted to useful intermediate functions that's just a bonus.

I don't like taking a matrix as an argument - especially since (1) the matrix may or may not be a legal transform (2) I'm usually only rotating the scene or scaling it.

then the thickness of volumetric materials will need to be updated too, as we do in quantization

I don't think it's appropriate to update the thickness factor, since, "Thickness is given in the coordinate space of the mesh. Any transformations applied to the mesh's node will also be applied to the thickness."^1.

Also, I think that, when you flatten a node graph, thickness should scale with the transformed length of the normal vector, not uniformly like transformMeshMaterials does: "Baking thickness into a map is similar to ambient occlusion baking, but rays are cast into the opposite direction of the surface normal."

donmccurdy commented 1 day ago

The inverse bind matrices shouldn't need to change, though I suppose we should test it.

Taking separate TRS arguments as transformScene(scene, t, r, s) would also be fine with me. But I do prefer to start with a dedicated function taking a scene. In general I do not expose everything in the CLI, which is meant to offer a subset of the library's scripting features. The cost for me to maintain additional CLI features is much higher. In https://gltf.report/, this would also work:

import { transformScene } from '@gltf-transform/functions';

for (const scene of document.getRoot().listScenes()) {
  transformScene(scene, [0, 5, 0]);
}

And I see you're correct about keeping the thickness factor, yes! We'd need to update that only if the vertex data had changed, no need here. But I don't think I follow about "thickness should scale with the transformed length of the normal vector". In that case we're uniformly scaling a node, potentially by a very large or very small factor.

rotu commented 1 day ago

In general I do not expose everything in the CLI, which is meant to offer a subset of the library's scripting features.

That's fair. Would you want to expose a limited subset like permuting the basis vectors?

this would also work:

I wasn't even sure how to get started with this. The gltf.report example script uses document.transform at the top level, but there's no analogous scene.transform.

I do think it's kinda awkward for a transformScene to mutate the existing node instead of returning a new, transformed scene but maybe that's for lack of experience with this library!

But I don't think I follow about "thickness should scale with the transformed length of the normal vector". In that case we're uniformly scaling a node, potentially by a very large or very small factor.

Ah you're right. I missed the fact that getNodeTransform function returns a uniform scale. I was thinking of the case where you squish a cube into a flat pane, in which case, the thickness should not be scaled uniformly.