Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transform entire scene #1530

Open
rotu opened this issue Oct 14, 2024 · 7 comments
Open

Transform entire scene #1530

rotu opened this issue Oct 14, 2024 · 7 comments
Labels

Comments

@rotu
Copy link
Contributor

rotu commented Oct 14, 2024

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 rotu added the feature New enhancement or request label Oct 14, 2024
@donmccurdy donmccurdy added this to the 🗄️ Backlog milestone Oct 14, 2024
@rotu
Copy link
Contributor Author

rotu commented Oct 14, 2024

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
Copy link
Contributor

kzhsw commented Oct 15, 2024

What about animations? Skip, remove, or keep

@rotu
Copy link
Contributor Author

rotu commented Oct 15, 2024

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

Did I miss something?

@donmccurdy
Copy link
Owner

donmccurdy commented Oct 15, 2024

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:

/** Applies corrective scale to volumetric materials, which give thickness in local units. */
function transformMeshMaterials(mesh: Mesh, scale: number) {
for (const prim of mesh.listPrimitives()) {
let material = prim.getMaterial();
if (!material) continue;
let volume = material.getExtension<Volume>('KHR_materials_volume');
if (!volume || volume.getThicknessFactor() <= 0) continue;
// quantize() does cleanup.
volume = volume.clone().setThicknessFactor(volume.getThicknessFactor() * scale);
material = material.clone().setExtension('KHR_materials_volume', volume);
prim.setMaterial(material);
}
}

@rotu
Copy link
Contributor Author

rotu commented Oct 15, 2024

skinned meshes are not supposed to be transformed, only the joint hierarchy
👍

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."

Footnotes

  1. https://github.com/KhronosGroup/glTF/blob/0251c5c0cce8daec69bd54f29f891e3d0cdb52c8/extensions/2.0/Khronos/KHR_materials_volume/README.md#thickness-texture

@donmccurdy
Copy link
Owner

donmccurdy commented Oct 15, 2024

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
Copy link
Contributor Author

rotu commented Oct 15, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants