Skip to content

Commit 6cd198f

Browse files
authored
Add faster synchronous method for VAT baking on vertexAnimationBaker (#16749)
Rather than defer to the engine loop, we should be able to "stop the world" and render each frame of a given range set to produce a VAT as quickly as possible. Theoretically this could run on a worker with a NullEngine and isolated scene, but figure the overhead might not be worth spinning that up. Add a test to match sandbox from https://playground.babylonjs.com/?BabylonToolkit#CP2RN9#302 I don't know if @RaggarDK those assets are okay to check in and use in the test suite/be available in the playground? Added some perf logging in the tests with output on my machine console.log Synchronous bake took: 34.68687499999987 ms console.log Asynchronous bake took: 2059.416625 ms It would be nice to see something like a "ThinContainerManager" that can import .babylon,.gltf, etc. and do all the perf things under the hood like create VAT, discard unused skeleton/AG, prime itself to create instances... These benchmarks move towards being able to do something like that in runtime.
1 parent a38a5fe commit 6cd198f

File tree

3 files changed

+147
-0
lines changed

3 files changed

+147
-0
lines changed

packages/dev/core/src/BakedVertexAnimation/vertexAnimationBaker.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { Scene } from "../scene";
77
import { Constants } from "../Engines/constants";
88
import { Skeleton } from "core/Bones/skeleton";
99
import type { Nullable } from "core/types";
10+
import { ToHalfFloat } from "core/Misc/textureTools";
1011

1112
/**
1213
* Class to bake vertex animation textures.
@@ -33,6 +34,52 @@ export class VertexAnimationBaker {
3334
}
3435
}
3536

37+
/**
38+
*
39+
* @param ranges Defines the ranges in the animation that will be baked.
40+
* @param halfFloat If true, the vertex data will be returned as half-float (Uint16Array), otherwise as full float (Float32Array).
41+
* @returns The array of matrix transforms for each vertex (columns) and frame (rows), as a Float32Array or Uint16Array.
42+
*/
43+
public bakeVertexDataSync(ranges: AnimationRange[], halfFloat: boolean): Float32Array | Uint16Array {
44+
if (!this._skeleton) {
45+
throw new Error("No skeleton provided.");
46+
}
47+
const bones = this._skeleton.bones.length;
48+
const floatsPerFrame = (bones + 1) * 16;
49+
const totalFrames = ranges.reduce((sum, r) => sum + (Math.floor(r.to) - Math.floor(r.from) + 1), 0);
50+
51+
const vertexData = halfFloat ? new Uint16Array(floatsPerFrame * totalFrames) : new Float32Array(floatsPerFrame * totalFrames);
52+
53+
let offset = 0;
54+
const matrices = this._skeleton.getTransformMatrices(this._mesh);
55+
56+
this._skeleton.returnToRest();
57+
58+
if (halfFloat) {
59+
for (const { from, to } of ranges) {
60+
for (let f = Math.floor(from); f <= Math.floor(to); ++f) {
61+
this._scene.beginAnimation(this._skeleton, f, f, false, 1.0);
62+
this._skeleton.computeAbsoluteMatrices(true);
63+
for (let i = 0; i < floatsPerFrame; ++i) {
64+
vertexData[offset + i] = ToHalfFloat(matrices[i]);
65+
}
66+
offset += floatsPerFrame;
67+
}
68+
}
69+
} else {
70+
for (const { from, to } of ranges) {
71+
for (let f = Math.floor(from); f <= Math.floor(to); ++f) {
72+
this._scene.beginAnimation(this._skeleton, f, f, false, 1.0);
73+
this._skeleton.computeAbsoluteMatrices(true);
74+
vertexData.set(matrices, offset);
75+
offset += floatsPerFrame;
76+
}
77+
}
78+
}
79+
80+
return vertexData;
81+
}
82+
3683
/**
3784
* Bakes the animation into the texture. This should be called once, when the
3885
* scene starts, so the VAT is generated and associated to the mesh.
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import * as fs from "fs";
2+
import * as path from "path";
3+
import { NullEngine } from "core/Engines";
4+
import { Engine } from "core/Engines/engine";
5+
import { Scene } from "core/scene";
6+
import { VertexAnimationBaker } from "core/BakedVertexAnimation/vertexAnimationBaker";
7+
import { AnimationRange } from "core/Animations";
8+
import { ImportMeshAsync } from "core/Loading";
9+
import type { Mesh } from "core/Meshes";
10+
import "core/Animations/animatable";
11+
import { FreeCamera } from "core/Cameras";
12+
import { Vector3 } from "core/Maths";
13+
14+
/**
15+
* Describes the test suite for VertexAnimationBaker.
16+
*/
17+
describe("VertexAnimationBaker", function () {
18+
let subject: Engine;
19+
let scene: Scene;
20+
let mesh: Mesh;
21+
22+
const animationRanges = [
23+
{ from: 7, to: 31 },
24+
{ from: 33, to: 61 },
25+
{ from: 63, to: 91 },
26+
{ from: 93, to: 130 },
27+
];
28+
29+
/**
30+
* Create a new engine, scene, and skeleton before each test.
31+
*/
32+
beforeEach(async function () {
33+
subject = new NullEngine({
34+
renderHeight: 256,
35+
renderWidth: 256,
36+
textureSize: 256,
37+
deterministicLockstep: false,
38+
lockstepMaxSteps: 1,
39+
});
40+
41+
// Avoid creating normals in PBR materials.
42+
subject.getCaps().standardDerivatives = true;
43+
44+
// Create a scene
45+
scene = new Scene(subject);
46+
new FreeCamera("camera", new Vector3(0, 0, 0), scene);
47+
const meshPath = path.join(__dirname, "../../../../../../packages/tools/playground/public/scenes/arr.babylon");
48+
const meshBuffer = fs.readFileSync(meshPath);
49+
const dataUrl = `data:model/gltf-binary;base64,${meshBuffer.toString("base64")}`;
50+
51+
const result = await ImportMeshAsync(dataUrl, scene);
52+
mesh = result.meshes[0] as Mesh;
53+
54+
subject.runRenderLoop(() => {
55+
scene.render();
56+
});
57+
});
58+
59+
/**
60+
* Tests for bakeVertexDataSync.
61+
*/
62+
describe("#bakeVertexDataSync", () => {
63+
it("should bake vertex data as Float32Array for given ranges and produce data ~equal to async bake", async () => {
64+
// Arrange: Create a VertexAnimationBaker with the skeleton
65+
const baker = new VertexAnimationBaker(scene, mesh);
66+
67+
// Act: Bake vertex data with halfFloat: false
68+
let start = performance.now();
69+
const vertexData = baker.bakeVertexDataSync(animationRanges as AnimationRange[], false);
70+
let end = performance.now();
71+
console.log(`Synchronous bake took: ${end - start} ms`);
72+
const asyncVertexData = await baker.bakeVertexData(animationRanges as AnimationRange[]);
73+
end = performance.now();
74+
console.log(`Asynchronous bake took: ${end - start} ms`);
75+
// Assert: Check type and size
76+
expect(vertexData.length).toEqual(asyncVertexData.length, "Synchronous and asynchronous vertex data should match");
77+
expect(vertexData).toBeInstanceOf(Float32Array, "Vertex data should be Float32Array");
78+
});
79+
80+
it("should bake vertex data as Uint16Array for half-float", () => {
81+
const baker = new VertexAnimationBaker(scene, mesh);
82+
const vertexData = baker.bakeVertexDataSync(animationRanges as AnimationRange[], true);
83+
expect(vertexData).toBeInstanceOf(Uint16Array, "Vertex data should be Uint16Array");
84+
});
85+
86+
it("should throw an error if no skeleton is provided", () => {
87+
const mesh = { skeleton: null }; // Mock mesh with no skeleton
88+
const baker = new VertexAnimationBaker(scene, mesh as any);
89+
expect(() => baker.bakeVertexDataSync(animationRanges as AnimationRange[], false)).toThrow("No skeleton provided.");
90+
});
91+
});
92+
93+
/**
94+
* Clean up after each test.
95+
*/
96+
afterEach(function () {
97+
subject.dispose();
98+
});
99+
});

packages/tools/playground/public/scenes/arr.babylon

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)