Add shared-scene 2D authoring, sprite animation and Rapier2D physics
CI / validate (push) Canceled after 0s
CI / validate (push) Canceled after 0s
This commit is contained in:
+219
-35
@@ -313,46 +313,230 @@ test(
|
||||
},
|
||||
);
|
||||
|
||||
test('changing a parent material or transform does not repaint child entities', async () => {
|
||||
const r = runtime(), p = defaultProject(true);
|
||||
const parent = entity('Parent', {mesh:{type:'box'}, material:{color:'#ff0000',alpha:.5}}, [0,0,0], 'parent');
|
||||
const child = entity('Child', {mesh:{type:'box'}, material:{color:'#00ff00',alpha:1}}, [2,0,0], 'child');
|
||||
child.parentId = parent.id; activeScene(p).entities = [parent,child];
|
||||
test("changing a parent material or transform does not repaint child entities", async () => {
|
||||
const r = runtime(),
|
||||
p = defaultProject(true);
|
||||
const parent = entity(
|
||||
"Parent",
|
||||
{ mesh: { type: "box" }, material: { color: "#ff0000", alpha: 0.5 } },
|
||||
[0, 0, 0],
|
||||
"parent",
|
||||
);
|
||||
const child = entity(
|
||||
"Child",
|
||||
{ mesh: { type: "box" }, material: { color: "#00ff00", alpha: 1 } },
|
||||
[2, 0, 0],
|
||||
"child",
|
||||
);
|
||||
child.parentId = parent.id;
|
||||
activeScene(p).entities = [parent, child];
|
||||
try {
|
||||
await r.load(p);
|
||||
parent.transform.position = [3,0,0]; parent.components.material.roughness = .1;
|
||||
parent.transform.position = [3, 0, 0];
|
||||
parent.components.material.roughness = 0.1;
|
||||
await r.load(p);
|
||||
const mat:any = r.nodes.get('child')!.getChildMeshes()[0].material;
|
||||
assert.deepEqual(mat.albedoColor.asArray(), [0,1,0]); assert.equal(mat.alpha,1);
|
||||
assert.equal((r.nodes.get('parent')!.getChildMeshes().find(m=>m.metadata.entityId==='parent')!.material as any).roughness,.1);
|
||||
} finally { r.dispose(); }
|
||||
const mat: any = r.nodes.get("child")!.getChildMeshes()[0].material;
|
||||
assert.deepEqual(mat.albedoColor.asArray(), [0, 1, 0]);
|
||||
assert.equal(mat.alpha, 1);
|
||||
assert.equal(
|
||||
(
|
||||
r.nodes
|
||||
.get("parent")!
|
||||
.getChildMeshes()
|
||||
.find((m) => m.metadata.entityId === "parent")!.material as any
|
||||
).roughness,
|
||||
0.1,
|
||||
);
|
||||
} finally {
|
||||
r.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
import { sceneAnimationGlb } from './fixtures.ts';
|
||||
test('imported material, morph, camera and light animation targets are independent per entity', async () => {
|
||||
const r=runtime(), p=defaultProject(true), bytes=sceneAnimationGlb();
|
||||
p.assets=[{id:'scene',name:'scene.glb',kind:'model',uri:'data:model/gltf-binary;base64,'+bytes.toString('base64')}];
|
||||
activeScene(p).entities=['a','b'].map(id=>entity(id,{mesh:{type:'model',assetId:'scene'},...(id==='a'?{camera:{mode:'imported'},animator:{autoplay:'Scene'}}:{})},[0,0,0],id));
|
||||
import { sceneAnimationGlb } from "./fixtures.ts";
|
||||
test("imported material, morph, camera and light animation targets are independent per entity", async () => {
|
||||
const r = runtime(),
|
||||
p = defaultProject(true),
|
||||
bytes = sceneAnimationGlb();
|
||||
p.assets = [
|
||||
{
|
||||
id: "scene",
|
||||
name: "scene.glb",
|
||||
kind: "model",
|
||||
uri: "data:model/gltf-binary;base64," + bytes.toString("base64"),
|
||||
},
|
||||
];
|
||||
activeScene(p).entities = ["a", "b"].map((id) =>
|
||||
entity(
|
||||
id,
|
||||
{
|
||||
mesh: { type: "model", assetId: "scene" },
|
||||
...(id === "a"
|
||||
? { camera: { mode: "imported" }, animator: { autoplay: "Scene" } }
|
||||
: {}),
|
||||
},
|
||||
[0, 0, 0],
|
||||
id,
|
||||
),
|
||||
);
|
||||
try {
|
||||
await r.play(p);
|
||||
assert.ok(!r.logs.some(l=>l.level==='error'),JSON.stringify(r.logs));
|
||||
const a=r.containers.get('a')!, b=r.containers.get('b')!;
|
||||
assert.equal(r.scene.activeCamera,a.cameras[0]);
|
||||
const group=r.animations.get('a')![0];
|
||||
assert.ok(group.isStarted,'autoplay starts the imported timeline');
|
||||
group.pause(); group.goToFrame(group.to);
|
||||
const am:any=a.materials[0], bm:any=b.materials[0];
|
||||
const close=(value:number,expected:number)=>assert.ok(Math.abs(value-expected)<.0001,`${value} != ${expected}`);
|
||||
close(am.metallic,.9);close(am.roughness,.8);close(am.albedoColor.b,1);close(am.alpha,.5);
|
||||
close(bm.metallic,.1);close(bm.roughness,.2);close(bm.albedoColor.r,1);
|
||||
close(a.cameras[0].fov,1.1);close(b.cameras[0].fov,.7);
|
||||
close(a.cameras[1].orthoLeft!,-4);close(a.cameras[1].orthoRight!,4);
|
||||
close(a.cameras[1].orthoBottom!,-3);close(a.cameras[1].orthoTop!,3);
|
||||
close(a.lights[0].intensity,20);close(b.lights[0].intensity,10);
|
||||
close(a.morphTargetManagers[0].getTarget(0).influence,1);
|
||||
close(b.morphTargetManagers[0].getTarget(0).influence,0);
|
||||
activeScene(p).entities=activeScene(p).entities.filter(n=>n.id==='b');
|
||||
await r.stop(p);await r.load(p);
|
||||
assert.equal(r.containers.has('a'),false);assert.equal(r.containers.get('b')!.meshes[0].isDisposed(),false);
|
||||
} finally {r.dispose();}
|
||||
assert.ok(!r.logs.some((l) => l.level === "error"), JSON.stringify(r.logs));
|
||||
const a = r.containers.get("a")!,
|
||||
b = r.containers.get("b")!;
|
||||
assert.equal(r.scene.activeCamera, a.cameras[0]);
|
||||
const group = r.animations.get("a")![0];
|
||||
assert.ok(group.isStarted, "autoplay starts the imported timeline");
|
||||
group.pause();
|
||||
group.goToFrame(group.to);
|
||||
const am: any = a.materials[0],
|
||||
bm: any = b.materials[0];
|
||||
const close = (value: number, expected: number) =>
|
||||
assert.ok(Math.abs(value - expected) < 0.0001, `${value} != ${expected}`);
|
||||
close(am.metallic, 0.9);
|
||||
close(am.roughness, 0.8);
|
||||
close(am.albedoColor.b, 1);
|
||||
close(am.alpha, 0.5);
|
||||
close(bm.metallic, 0.1);
|
||||
close(bm.roughness, 0.2);
|
||||
close(bm.albedoColor.r, 1);
|
||||
close(a.cameras[0].fov, 1.1);
|
||||
close(b.cameras[0].fov, 0.7);
|
||||
close(a.cameras[1].orthoLeft!, -4);
|
||||
close(a.cameras[1].orthoRight!, 4);
|
||||
close(a.cameras[1].orthoBottom!, -3);
|
||||
close(a.cameras[1].orthoTop!, 3);
|
||||
close(a.lights[0].intensity, 20);
|
||||
close(b.lights[0].intensity, 10);
|
||||
close(a.morphTargetManagers[0].getTarget(0).influence, 1);
|
||||
close(b.morphTargetManagers[0].getTarget(0).influence, 0);
|
||||
activeScene(p).entities = activeScene(p).entities.filter(
|
||||
(n) => n.id === "b",
|
||||
);
|
||||
await r.stop(p);
|
||||
await r.load(p);
|
||||
assert.equal(r.containers.has("a"), false);
|
||||
assert.equal(r.containers.get("b")!.meshes[0].isDisposed(), false);
|
||||
} finally {
|
||||
r.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
test(
|
||||
"Mixed 2D/3D scene, worker physics commands, animation and Stop restoration",
|
||||
{ timeout: 15000 },
|
||||
async () => {
|
||||
const { project2D } = await import("../engine/template2d.ts");
|
||||
const p = project2D(true),
|
||||
r = runtime();
|
||||
p.scripts.push({
|
||||
id: "two_d_script",
|
||||
name: "2D test",
|
||||
fields: {},
|
||||
source:
|
||||
"({start(api){api.impulse2D([1,0]);},update(api,dt){api.state.ticks=(api.state.ticks||0)+1;api.patch(api.get().id,{components:{data:{ticks:api.state.ticks}}});},collision2d(api,event){api.patch(api.get().id,{components:{data:{collided:event.otherId}}})}})",
|
||||
});
|
||||
activeScene(p).entities.find(
|
||||
(n) => n.id === "crate_2d",
|
||||
)!.components.script = { scriptId: "two_d_script" };
|
||||
activeScene(p).entities.push(
|
||||
entity(
|
||||
"3D fall",
|
||||
{
|
||||
mesh: { type: "box", size: [1, 1, 1] },
|
||||
collider: { shape: "box", size: [1, 1, 1] },
|
||||
rigidbody: { type: "dynamic" },
|
||||
},
|
||||
[0, 3, -6],
|
||||
"three_d",
|
||||
),
|
||||
);
|
||||
try {
|
||||
await r.play(p);
|
||||
assert.ok(r.physics2d);
|
||||
assert.equal(r.scene.activeCamera!.mode, 1);
|
||||
r.setInput({ x: 1, durationMs: 5000 });
|
||||
const deadline = Date.now() + 4000;
|
||||
while (Date.now() < deadline) {
|
||||
const state = r.snapshot();
|
||||
if (
|
||||
state.entities.find((n) => n.id === "player_2d")!.transform
|
||||
.position[0] > 4 &&
|
||||
state.entities.find((n) => n.id === "three_d")!.transform
|
||||
.position[1] < -1 &&
|
||||
state.entities.find((n) => n.id === "crate_2d")!.components.data
|
||||
?.collided
|
||||
)
|
||||
break;
|
||||
await wait(50);
|
||||
}
|
||||
const snap = r.snapshot(),
|
||||
hero = snap.entities.find((n) => n.id === "player_2d")!,
|
||||
crate = snap.entities.find((n) => n.id === "crate_2d")!;
|
||||
assert.ok(hero.transform.position[0] > 4, "builtin controller moved");
|
||||
assert.equal(hero.transform.position[2], 0);
|
||||
assert.ok(crate.components.data.ticks > 5, JSON.stringify(snap.logs));
|
||||
assert.ok(
|
||||
snap.entities.find((n) => n.id === "three_d")!.transform.position[1] <
|
||||
0,
|
||||
"3D body falls through 2D ground",
|
||||
);
|
||||
assert.equal(snap.physics.player_2d.dimension, 2);
|
||||
assert.ok(
|
||||
crate.components.data.collided,
|
||||
"Worker receives 2D collision events",
|
||||
);
|
||||
assert.ok(
|
||||
!snap.logs.some((l) => l.level === "error"),
|
||||
JSON.stringify(snap.logs),
|
||||
);
|
||||
await r.stop(p);
|
||||
assert.equal(r.physics2d, undefined);
|
||||
assert.deepEqual(
|
||||
r.state.find((n) => n.id === "player_2d")!.transform.position,
|
||||
[3, 2, 0],
|
||||
);
|
||||
r.setView2D(true);
|
||||
assert.equal(r.camera.mode, 1);
|
||||
r.setView2D(false);
|
||||
assert.equal(r.camera.mode, 0);
|
||||
} finally {
|
||||
r.dispose();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
test("Spawning a 2D prefab creates its physics world when the scene initially had none", async () => {
|
||||
const p = defaultProject(),
|
||||
r = runtime();
|
||||
p.assets = [
|
||||
{
|
||||
id: "prefab2d",
|
||||
kind: "prefab",
|
||||
name: "2D body",
|
||||
entities: [
|
||||
entity(
|
||||
"Body",
|
||||
{
|
||||
sprite: {},
|
||||
collider2d: { shape: "box", size: [1, 1] },
|
||||
rigidbody2d: { type: "fixed" },
|
||||
},
|
||||
[0, 0, 0],
|
||||
"body2d",
|
||||
),
|
||||
],
|
||||
},
|
||||
];
|
||||
try {
|
||||
await r.play(p);
|
||||
assert.equal(r.physics2d, undefined);
|
||||
await (r as any).spawn("prefab2d", [4, 3, -2]);
|
||||
assert.equal(r.physics2d!.entries.size, 1);
|
||||
const n = r.state[0];
|
||||
assert.notEqual(n.id, "body2d");
|
||||
assert.deepEqual(n.transform.position, [4, 3, -2]);
|
||||
assert.ok(r.graphics2d.visuals.has(n.id));
|
||||
} finally {
|
||||
r.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user