191 lines
6.9 KiB
TypeScript
191 lines
6.9 KiB
TypeScript
export function node(
|
|
id: string,
|
|
parentId: string | null = null,
|
|
components: Record<string, any> = {},
|
|
) {
|
|
return {
|
|
id,
|
|
name: id,
|
|
parentId,
|
|
enabled: true,
|
|
transform: { position: [0, 0, 0], rotation: [0, 0, 0], scale: [1, 1, 1] },
|
|
components,
|
|
};
|
|
}
|
|
export function project(nodes: any[] = []) {
|
|
return {
|
|
format: "forma",
|
|
version: 1,
|
|
id: "test_project",
|
|
name: "Regression fixture",
|
|
revision: 0,
|
|
activeSceneId: "scene_main",
|
|
scenes: [{ id: "scene_main", name: "Main", entities: nodes }],
|
|
assets: [],
|
|
scripts: [],
|
|
settings: {
|
|
background: "#f3f0eb",
|
|
ambient: 0.8,
|
|
shadows: false,
|
|
renderScale: 1,
|
|
},
|
|
} as any;
|
|
}
|
|
export function scene(p: any) {
|
|
return p.scenes.find((s: any) => s.id === p.activeSceneId);
|
|
}
|
|
export function findNode(p: any, id: string) {
|
|
return scene(p).entities.find((e: any) => e.id === id);
|
|
}
|
|
|
|
/** A generated triangle, optionally skinned and animated; no game assets. */
|
|
export function triangleGlb(animated = false, configure?: (document: any, add: (data: Float32Array | Uint16Array, type: string, count: number, bounds?: Record<string, any>) => number) => void): Buffer {
|
|
const chunks: Buffer[] = [];
|
|
const views: any[] = [];
|
|
const accessors: any[] = [];
|
|
let length = 0;
|
|
const add = (
|
|
data: Float32Array | Uint16Array,
|
|
type: string,
|
|
count: number,
|
|
bounds: Record<string, any> = {},
|
|
) => {
|
|
const bytes = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
views.push({ buffer: 0, byteOffset: length, byteLength: bytes.length });
|
|
chunks.push(bytes);
|
|
length += bytes.length;
|
|
const padding = (4 - (length % 4)) % 4;
|
|
if (padding) {
|
|
chunks.push(Buffer.alloc(padding));
|
|
length += padding;
|
|
}
|
|
accessors.push({
|
|
bufferView: views.length - 1,
|
|
componentType: data instanceof Float32Array ? 5126 : 5123,
|
|
count,
|
|
type,
|
|
...bounds,
|
|
});
|
|
return accessors.length - 1;
|
|
};
|
|
const position = add(
|
|
new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]),
|
|
"VEC3",
|
|
3,
|
|
{ min: [0, 0, 0], max: [1, 1, 0] },
|
|
);
|
|
const normal = add(new Float32Array([0, 0, 1, 0, 0, 1, 0, 0, 1]), "VEC3", 3);
|
|
const indices = add(new Uint16Array([0, 1, 2]), "SCALAR", 3);
|
|
const attributes: Record<string, number> = {
|
|
POSITION: position,
|
|
NORMAL: normal,
|
|
};
|
|
const document: any = {
|
|
asset: { version: "2.0", generator: "Forma test fixture" },
|
|
scene: 0,
|
|
scenes: [{ nodes: animated ? [0, 1] : [0] }],
|
|
nodes: [{ name: "Triangle", mesh: 0 }],
|
|
meshes: [{ primitives: [{ attributes, indices }] }],
|
|
};
|
|
if (animated) {
|
|
attributes.JOINTS_0 = add(new Uint16Array(12), "VEC4", 3);
|
|
attributes.WEIGHTS_0 = add(
|
|
new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0]),
|
|
"VEC4",
|
|
3,
|
|
);
|
|
const inverseBindMatrices = add(
|
|
new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]),
|
|
"MAT4",
|
|
1,
|
|
);
|
|
const input = add(new Float32Array([0, 1]), "SCALAR", 2, {
|
|
min: [0],
|
|
max: [1],
|
|
});
|
|
const output = add(new Float32Array([0, 0, 0, 0, 0.2, 0]), "VEC3", 2);
|
|
document.nodes[0].skin = 0;
|
|
document.nodes.push({ name: "Joint" });
|
|
document.skins = [{ joints: [1], inverseBindMatrices }];
|
|
document.animations = [
|
|
{
|
|
name: "Translate",
|
|
samplers: [{ input, output, interpolation: "LINEAR" }],
|
|
channels: [{ sampler: 0, target: { node: 1, path: "translation" } }],
|
|
},
|
|
];
|
|
}
|
|
configure?.(document, add);
|
|
Object.assign(document, {
|
|
buffers: [{ byteLength: length }],
|
|
bufferViews: views,
|
|
accessors,
|
|
});
|
|
const json = Buffer.from(JSON.stringify(document));
|
|
const jsonPadding = Buffer.alloc((4 - (json.length % 4)) % 4, 0x20);
|
|
const body = Buffer.concat(chunks);
|
|
const header = Buffer.alloc(12);
|
|
header.writeUInt32LE(0x46546c67, 0);
|
|
header.writeUInt32LE(2, 4);
|
|
header.writeUInt32LE(
|
|
12 + 8 + json.length + jsonPadding.length + 8 + body.length,
|
|
8,
|
|
);
|
|
const jsonHeader = Buffer.alloc(8);
|
|
jsonHeader.writeUInt32LE(json.length + jsonPadding.length, 0);
|
|
jsonHeader.writeUInt32LE(0x4e4f534a, 4);
|
|
const bodyHeader = Buffer.alloc(8);
|
|
bodyHeader.writeUInt32LE(body.length, 0);
|
|
bodyHeader.writeUInt32LE(0x004e4942, 4);
|
|
return Buffer.concat([
|
|
header,
|
|
jsonHeader,
|
|
json,
|
|
jsonPadding,
|
|
bodyHeader,
|
|
body,
|
|
]);
|
|
}
|
|
|
|
export const triangleAsset = (animated = false) => ({
|
|
id: "asset_triangle",
|
|
name: "Triangle.glb",
|
|
kind: "model" as const,
|
|
uri:
|
|
"data:model/gltf-binary;base64," + triangleGlb(animated).toString("base64"),
|
|
});
|
|
|
|
/** Material, camera, light and morph animation in one real glTF clip. */
|
|
export function sceneAnimationGlb() {
|
|
return triangleGlb(false, (doc, add) => {
|
|
doc.materials = [{ name: 'AnimatedPBR', pbrMetallicRoughness: { baseColorFactor: [1, 0, 0, 1], metallicFactor: .1, roughnessFactor: .2 } }];
|
|
doc.meshes[0].primitives[0].material = 0;
|
|
doc.meshes[0].primitives[0].targets = [{ POSITION: add(new Float32Array([0,0,0, 0,0,0, 0,1,0]), 'VEC3', 3) }];
|
|
doc.meshes[0].weights = [0];
|
|
doc.cameras = [{ name: 'ImportedCamera', type: 'perspective', perspective: { yfov: .7, znear: .1 } }];
|
|
doc.nodes.push({ name: 'ImportedCamera', camera: 0, translation: [0, 0, 5] }, { name: 'ImportedLight', extensions: { KHR_lights_punctual: { light: 0 } } });
|
|
doc.scenes[0].nodes.push(1, 2);
|
|
doc.cameras.push({ name: 'Ortho', type: 'orthographic', orthographic: { xmag: 2, ymag: 1, znear: .1, zfar: 100 } });
|
|
doc.nodes.push({ name: 'Ortho', camera: 1 });doc.scenes[0].nodes.push(3);
|
|
doc.extensions = { KHR_lights_punctual: { lights: [{ name: 'ImportedLight', type: 'point', intensity: 10 }] } };
|
|
doc.extensionsUsed = ['KHR_lights_punctual', 'KHR_animation_pointer'];
|
|
const input = add(new Float32Array([0, 1]), 'SCALAR', 2, { min: [0], max: [1] });
|
|
const animation: any = { name: 'Scene', channels: [], samplers: [] };
|
|
for (const [pointer, type, values] of [
|
|
['/materials/0/pbrMetallicRoughness/baseColorFactor', 'VEC4', [1,0,0,1, 0,0,1,.5]],
|
|
['/materials/0/pbrMetallicRoughness/metallicFactor', 'SCALAR', [.1,.9]],
|
|
['/materials/0/pbrMetallicRoughness/roughnessFactor', 'SCALAR', [.2,.8]],
|
|
['/cameras/0/perspective/yfov', 'SCALAR', [.7,1.1]],
|
|
['/cameras/1/orthographic/xmag', 'SCALAR', [2,4]],
|
|
['/cameras/1/orthographic/ymag', 'SCALAR', [1,3]],
|
|
['/extensions/KHR_lights_punctual/lights/0/intensity', 'SCALAR', [10,20]],
|
|
] as const) {
|
|
animation.channels.push({ sampler: animation.samplers.length, target: { path: 'pointer', extensions: { KHR_animation_pointer: { pointer } } } });
|
|
animation.samplers.push({ input, output: add(new Float32Array(values), type, 2), interpolation: 'LINEAR' });
|
|
}
|
|
animation.channels.push({ sampler: animation.samplers.length, target: { node: 0, path: 'weights' } });
|
|
animation.samplers.push({ input, output: add(new Float32Array([0,1]), 'SCALAR', 2), interpolation: 'LINEAR' });
|
|
doc.animations = [animation];
|
|
});
|
|
}
|