Fix editor regressions and add Blender scene and animated material import
This commit is contained in:
+67
-61
@@ -1,5 +1,5 @@
|
||||
import * as B from "@babylonjs/core";
|
||||
import "@babylonjs/loaders/glTF";
|
||||
import "./gltf-compat.ts";
|
||||
import {
|
||||
type Project,
|
||||
type Entity,
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { workerSource } from "./script-host.ts";
|
||||
import { inspectModel } from "./model.ts";
|
||||
import { CharacterMotor } from "./character.ts";
|
||||
import { bindViewInput } from "./view-input.ts";
|
||||
import { RuntimePresentation } from "./presentation.ts";
|
||||
export interface RuntimeCallbacks {
|
||||
select?: (id: string | null) => void;
|
||||
@@ -108,6 +109,7 @@ export class FormaRuntime {
|
||||
public callbacks: RuntimeCallbacks = {},
|
||||
public options: RuntimeOptions = {},
|
||||
) {
|
||||
const runtime = this;
|
||||
this.engine =
|
||||
options.engine ||
|
||||
new B.Engine(
|
||||
@@ -121,6 +123,12 @@ export class FormaRuntime {
|
||||
false,
|
||||
);
|
||||
if (!options.headless) {
|
||||
this.cleanup.push(bindViewInput(canvas, {
|
||||
get playing() { return runtime.playing; },
|
||||
get paused() { return runtime.paused; },
|
||||
firstPerson: () => this.firstPerson(),
|
||||
lookBy: (x, y) => this.lookBy(x, y),
|
||||
}));
|
||||
const resize = new ResizeObserver(() => this.engine.resize());
|
||||
resize.observe(canvas);
|
||||
this.cleanup.push(() => resize.disconnect());
|
||||
@@ -233,6 +241,7 @@ export class FormaRuntime {
|
||||
this.actionQueue.add(name);
|
||||
}
|
||||
releaseInput() {
|
||||
if (!this.options.headless && document.pointerLockElement === this.canvas) document.exitPointerLock();
|
||||
this.keys.clear();
|
||||
this.actionQueue.clear();
|
||||
this.automation = null;
|
||||
@@ -331,7 +340,7 @@ export class FormaRuntime {
|
||||
}
|
||||
scene.activeCamera = this.camera;
|
||||
const sky = new B.HemisphericLight("Sky", B.Vector3.Up(), scene);
|
||||
sky.intensity = p.settings.ambient;
|
||||
sky.intensity = p.settings.rendering?.defaultLights === false ? 0 : p.settings.ambient;
|
||||
sky.groundColor = B.Color3.FromHexString("#8a8475");
|
||||
const sun = new B.DirectionalLight(
|
||||
"Sun",
|
||||
@@ -339,7 +348,7 @@ export class FormaRuntime {
|
||||
scene,
|
||||
);
|
||||
sun.position = new B.Vector3(-12, 22, -14);
|
||||
sun.intensity = 2.5;
|
||||
sun.intensity = p.settings.rendering?.defaultLights === false ? 0 : 2.5;
|
||||
sun.diffuse = B.Color3.FromHexString("#fff4df");
|
||||
this.shadow = new B.ShadowGenerator(1024, sun);
|
||||
this.shadow.usePercentageCloserFiltering = true;
|
||||
@@ -451,11 +460,13 @@ export class FormaRuntime {
|
||||
root.computeWorldMatrix(true);
|
||||
const c = n.components.material;
|
||||
if (c && (n.components.mesh?.type !== "model" || c.override)) {
|
||||
for (const mesh of root.getChildMeshes()) {
|
||||
for (const mesh of root.getChildMeshes().filter(mesh => mesh.metadata?.entityId === n.id)) {
|
||||
const mat = mesh.material;
|
||||
if (mat instanceof B.PBRMaterial) {
|
||||
mat.albedoColor = B.Color3.FromHexString(c.color || "#91a697");
|
||||
mat.emissiveColor = mat.albedoColor.scale(c.emissive || 0);
|
||||
mat.roughness = c.roughness ?? 0.8;
|
||||
mat.metallic = c.metallic ?? 0;
|
||||
mat.alpha = c.alpha ?? 1;
|
||||
mat.unlit = c.unlit === true;
|
||||
mat.transparencyMode =
|
||||
@@ -475,6 +486,8 @@ export class FormaRuntime {
|
||||
node.dispose();
|
||||
}
|
||||
this.nodes.delete(id);
|
||||
this.containers.get(id)?.dispose();
|
||||
this.containers.delete(id);
|
||||
this.signatures.delete(id);
|
||||
this.animations.get(id)?.forEach((a) => a.dispose());
|
||||
this.animations.delete(id);
|
||||
@@ -490,7 +503,9 @@ export class FormaRuntime {
|
||||
);
|
||||
this.scene.shadowsEnabled = p.settings.shadows;
|
||||
const sky = this.scene.getLightByName("Sky");
|
||||
if (sky) sky.intensity = p.settings.ambient;
|
||||
if (sky) sky.intensity = p.settings.rendering?.defaultLights === false ? 0 : p.settings.ambient;
|
||||
const sun = this.scene.getLightByName("Sun");
|
||||
if (sun) sun.intensity = p.settings.rendering?.defaultLights === false ? 0 : 2.5;
|
||||
this.engine.setHardwareScalingLevel(1 / p.settings.renderScale);
|
||||
const live = new Set(this.state.map((n) => n.id));
|
||||
for (const id of this.nodes.keys()) if (!live.has(id)) this.removeNode(id);
|
||||
@@ -532,61 +547,40 @@ export class FormaRuntime {
|
||||
if (m.type === "model") {
|
||||
const a = p.assets.find((a) => a.id === m.assetId);
|
||||
if (!a?.uri) throw Error("У модели нет файла");
|
||||
const key = a.id + "|" + a.uri;
|
||||
let container = this.containers.get(key);
|
||||
if (!container) {
|
||||
let bytes: Uint8Array;
|
||||
if (this.options.readAsset)
|
||||
bytes = await this.options.readAsset(a.uri);
|
||||
else {
|
||||
const r = await fetch(a.uri);
|
||||
if (!r.ok) throw Error("Ошибка загрузки " + a.name);
|
||||
bytes = new Uint8Array(await r.arrayBuffer());
|
||||
}
|
||||
if (scene.isDisposed || this.disposed) return;
|
||||
inspectModel(bytes, a.name);
|
||||
container = await B.LoadAssetContainerAsync(bytes, scene, {
|
||||
pluginExtension: a.name.toLowerCase().endsWith(".gltf")
|
||||
? ".gltf"
|
||||
: ".glb",
|
||||
name: a.name,
|
||||
});
|
||||
if (scene.isDisposed || this.disposed) {
|
||||
container.dispose();
|
||||
return;
|
||||
}
|
||||
this.containers.set(key, container);
|
||||
const info = {
|
||||
clips: container.animationGroups.map((g) => g.name),
|
||||
skeletons: container.skeletons.length,
|
||||
triangles: container.meshes.reduce(
|
||||
(s, m) => s + m.getTotalIndices() / 3,
|
||||
0,
|
||||
),
|
||||
};
|
||||
this.importInfo.set(a.id, info);
|
||||
this.log(
|
||||
"info",
|
||||
"Импорт " +
|
||||
a.name +
|
||||
": " +
|
||||
info.triangles +
|
||||
" треугольников, " +
|
||||
info.clips.length +
|
||||
" анимаций",
|
||||
);
|
||||
let bytes: Uint8Array;
|
||||
if (this.options.readAsset) bytes = await this.options.readAsset(a.uri);
|
||||
else {
|
||||
const response = await fetch(a.uri);
|
||||
if (!response.ok) throw Error("Ошибка загрузки " + a.name);
|
||||
bytes = new Uint8Array(await response.arrayBuffer());
|
||||
}
|
||||
const instance = container.instantiateModelsToScene(
|
||||
(name) => n.id + "_" + name,
|
||||
true,
|
||||
{ doNotInstantiate: true },
|
||||
);
|
||||
for (const node of instance.rootNodes) node.parent = root;
|
||||
this.animations.set(n.id, instance.animationGroups);
|
||||
instance.animationGroups.forEach((g, i) => {
|
||||
g.name = container!.animationGroups[i].name;
|
||||
g.stop();
|
||||
if (scene.isDisposed || this.disposed) return;
|
||||
const metadata = inspectModel(bytes, a.name);
|
||||
// A container per entity keeps cameras, lights, morph targets, textures
|
||||
// and material animation targets independent. Mesh-only cloning can
|
||||
// retain animation targets pointing into the cached source container.
|
||||
const container = await B.LoadAssetContainerAsync(bytes, scene, {
|
||||
pluginExtension: a.name.toLowerCase().endsWith(".gltf") ? ".gltf" : ".glb",
|
||||
name: a.name,
|
||||
pluginOptions: { gltf: { animationStartMode: 0 } },
|
||||
});
|
||||
if (scene.isDisposed || this.disposed) { container.dispose(); return; }
|
||||
this.containers.set(n.id, container);
|
||||
// getNodes() also includes Bones, whose parent must remain a Bone.
|
||||
const roots = [...container.meshes, ...container.transformNodes, ...container.cameras, ...container.lights].filter(node => !node.parent);
|
||||
container.addAllToScene();
|
||||
for (const imported of roots) imported.parent = root;
|
||||
for (const camera of container.cameras) camera.detachControl();
|
||||
this.animations.set(n.id, container.animationGroups);
|
||||
container.animationGroups.forEach(group => group.stop());
|
||||
this.importInfo.set(a.id, {
|
||||
...metadata,
|
||||
clips: container.animationGroups.map(group => group.name),
|
||||
cameraNames: container.cameras.map(camera => camera.name),
|
||||
triangles: container.meshes.reduce((sum, mesh) => sum + mesh.getTotalIndices() / 3, 0),
|
||||
});
|
||||
for (const warning of metadata.warnings) this.log("warning", warning, n.id);
|
||||
this.log("info", `Импорт ${a.name}: ${metadata.nodes} узлов, ${metadata.clips.length} анимаций, ${metadata.animatedProperties.length} анимированных свойств`, n.id);
|
||||
meshes = root.getChildMeshes();
|
||||
} else if (m.type === "custom" || m.type === "geometry") {
|
||||
const g =
|
||||
@@ -712,7 +706,7 @@ export class FormaRuntime {
|
||||
sign.isPickable = false;
|
||||
}
|
||||
for (const mesh of meshes) {
|
||||
mesh.metadata = { entityId: n.id };
|
||||
mesh.metadata = { ...mesh.metadata, entityId: n.id };
|
||||
mesh.isPickable = true;
|
||||
mesh.receiveShadows = n.components.mesh?.receiveShadows !== false;
|
||||
if (n.components.mesh?.castShadows !== false)
|
||||
@@ -829,6 +823,10 @@ export class FormaRuntime {
|
||||
)?.components.camera;
|
||||
this.look = { yaw: fp?.yaw ?? 0, pitch: fp?.pitch ?? 0 };
|
||||
this.currentAnims.clear();
|
||||
for (const node of this.state) {
|
||||
const animator = node.components.animator;
|
||||
if (node.enabled && animator?.autoplay) this.animate(node.id, animator.autoplay, animator.loop !== false);
|
||||
}
|
||||
if (!this.options.headless && p.settings.presentation) {
|
||||
this.presentation?.dispose();
|
||||
this.presentation = new RuntimePresentation(
|
||||
@@ -1326,6 +1324,7 @@ export class FormaRuntime {
|
||||
else this.flashes.set(id, t - dt);
|
||||
}
|
||||
try {
|
||||
this.scene.animationsEnabled = !(this.playing && this.paused);
|
||||
this.scene.render();
|
||||
} catch (e) {
|
||||
this.log("error", "Render: " + String(e));
|
||||
@@ -1364,10 +1363,17 @@ export class FormaRuntime {
|
||||
B.Vector3.FromArray(entity!.transform.position),
|
||||
);
|
||||
this.gameCamera.setTarget(B.Vector3.FromArray(c.lookAt || [0, 0, 0]));
|
||||
this.gameCamera.fov = c.fov || 0.72;
|
||||
}
|
||||
private updateProjection() {
|
||||
const c = this.state.find((n) => n.enabled && n.components.camera)
|
||||
?.components.camera;
|
||||
const cameraEntity = this.state.find(n => n.enabled && n.components.camera);
|
||||
const c = cameraEntity?.components.camera;
|
||||
if (cameraEntity && c?.mode === "imported") {
|
||||
const cameras = this.containers.get(cameraEntity.id)?.cameras || [];
|
||||
const imported = cameras.find(camera => camera.name === c.cameraName) || cameras[0];
|
||||
if (imported) { this.scene.activeCamera = imported; return; }
|
||||
}
|
||||
this.scene.activeCamera = this.gameCamera;
|
||||
const cam = this.gameCamera;
|
||||
cam.mode =
|
||||
c?.projection === "orthographic"
|
||||
|
||||
Reference in New Issue
Block a user