Add browser-only editor export and 2.5D runtime presentation

This commit is contained in:
emil28092005
2026-09-12 00:05:19 +03:00
parent a25ed40805
commit 1a0bf725fa
16 changed files with 883 additions and 53 deletions
+129 -12
View File
@@ -13,6 +13,7 @@ import {
import { workerSource } from "./script-host.ts";
import { inspectModel } from "./model.ts";
import { CharacterMotor } from "./character.ts";
import { RuntimePresentation } from "./presentation.ts";
export interface RuntimeCallbacks {
select?: (id: string | null) => void;
transform?: (id: string, t: Entity["transform"]) => void;
@@ -95,6 +96,13 @@ export class FormaRuntime {
{ to: B.AnimationGroup; from: B.AnimationGroup[]; time: number }
>();
private flashes = new Map<string, number>();
private presentation: RuntimePresentation | null = null;
private binding(
action: "attack" | "jump" | "dash" | "sprint" | "reset",
defaults: string[],
) {
return this.document?.settings.controls?.[action] ?? defaults;
}
constructor(
public canvas: HTMLCanvasElement,
public callbacks: RuntimeCallbacks = {},
@@ -138,9 +146,12 @@ export class FormaRuntime {
)
e.preventDefault();
if (!this.keys.has(e.code)) {
if (e.code === "Space") this.requestAction("jump");
if (e.code === "KeyE") this.requestAction("dash");
if (e.code === "KeyR") this.requestAction("reset");
if (this.binding("jump", ["Space"]).includes(e.code))
this.requestAction("jump");
if (this.binding("dash", ["KeyE"]).includes(e.code))
this.requestAction("dash");
if (this.binding("reset", ["KeyR"]).includes(e.code))
this.requestAction("reset");
}
this.keys.add(e.code);
},
@@ -335,11 +346,14 @@ export class FormaRuntime {
this.shadow.filteringQuality = B.ShadowGenerator.QUALITY_LOW;
this.shadow.bias = 0.0005;
this.shadow.normalBias = 0.04;
scene.imageProcessingConfiguration.toneMappingEnabled = true;
scene.imageProcessingConfiguration.toneMappingEnabled =
p.settings.rendering?.toneMapping !== false;
scene.imageProcessingConfiguration.toneMappingType =
B.ImageProcessingConfiguration.TONEMAPPING_ACES;
scene.imageProcessingConfiguration.exposure = 1.12;
scene.imageProcessingConfiguration.contrast = 1.05;
scene.imageProcessingConfiguration.exposure =
p.settings.rendering?.exposure ?? 1.12;
scene.imageProcessingConfiguration.contrast =
p.settings.rendering?.contrast ?? 1.05;
const lines: B.Vector3[][] = [];
for (let i = -35; i <= 35; i++) {
lines.push([new B.Vector3(i, -0.55, -35), new B.Vector3(i, -0.55, 35)]);
@@ -435,6 +449,23 @@ export class FormaRuntime {
root.scaling.copyFromFloats(...n.transform.scale);
root.setEnabled(n.enabled);
root.computeWorldMatrix(true);
const c = n.components.material;
if (c && (n.components.mesh?.type !== "model" || c.override)) {
for (const mesh of root.getChildMeshes()) {
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.alpha = c.alpha ?? 1;
mat.unlit = c.unlit === true;
mat.transparencyMode =
mat.alpha < 1
? B.PBRMaterial.PBRMATERIAL_ALPHABLEND
: B.PBRMaterial.PBRMATERIAL_OPAQUE;
mat.backFaceCulling = c.doubleSided !== true;
}
}
}
}
private removeNode(id: string) {
const node = this.nodes.get(id);
@@ -614,6 +645,13 @@ export class FormaRuntime {
mat.roughness = c.roughness ?? 0.8;
mat.metallic = c.metallic ?? 0;
mat.emissiveColor = mat.albedoColor.scale(c.emissive || 0);
mat.alpha = c.alpha ?? 1;
mat.unlit = c.unlit === true;
mat.transparencyMode =
mat.alpha < 1
? B.PBRMaterial.PBRMATERIAL_ALPHABLEND
: B.PBRMaterial.PBRMATERIAL_OPAQUE;
mat.backFaceCulling = c.doubleSided !== true;
for (const mesh of meshes) mesh.material = mat;
}
}
@@ -777,6 +815,8 @@ export class FormaRuntime {
this.runId = uid("run");
this.camera.detachControl();
this.scene.activeCamera = this.gameCamera;
this.updateFixedCamera();
this.updateProjection();
this.grid.setEnabled(false);
this.select(null);
this.keys.clear();
@@ -789,6 +829,33 @@ export class FormaRuntime {
)?.components.camera;
this.look = { yaw: fp?.yaw ?? 0, pitch: fp?.pitch ?? 0 };
this.currentAnims.clear();
if (!this.options.headless && p.settings.presentation) {
this.presentation?.dispose();
this.presentation = new RuntimePresentation(
this.canvas,
p.settings.presentation,
{
pause: (value) => {
this.paused = value;
this.releaseInput();
},
paused: () => this.paused,
restart: () => {
this.releaseInput();
this.requestAction("reset");
this.paused = false;
},
move: (x, z) => {
this.touch.x = x;
this.touch.z = z;
},
attack: (value) => {
this.touch.attack = value;
},
dash: () => this.requestAction("dash"),
},
);
}
this.startWorker();
this.callbacks.mode?.(true);
this.log("info", "Запуск " + this.runId);
@@ -797,6 +864,8 @@ export class FormaRuntime {
stop(project?: Project) {
return this.enqueue(async () => {
this.stopWorker();
this.presentation?.dispose();
this.presentation = null;
this.playing = false;
this.paused = false;
this.world?.free();
@@ -924,6 +993,7 @@ export class FormaRuntime {
this.applyTransform(n);
if (Number.isFinite(c.yaw)) this.look = { yaw: c.yaw, pitch: 0 };
} else if (c.type === "event") {
this.presentation?.event(String(c.name), c.data);
this.callbacks.event?.(String(c.name), c.data, c.id);
} else if (c.type === "move" && n) {
if (
@@ -1155,13 +1225,20 @@ export class FormaRuntime {
(this.keys.has("KeyW") || this.keys.has("ArrowUp") ? 1 : 0) -
(this.keys.has("KeyS") || this.keys.has("ArrowDown") ? 1 : 0),
attack:
this.input.attack || this.touch.attack || this.keys.has("Space"),
jump: this.touch.jump || this.keys.has("Space"),
dash: this.touch.dash || this.keys.has("KeyE"),
this.input.attack ||
this.touch.attack ||
this.binding("attack", ["Space"]).some((k) => this.keys.has(k)),
jump:
this.touch.jump ||
this.binding("jump", ["Space"]).some((k) => this.keys.has(k)),
dash:
this.touch.dash ||
this.binding("dash", ["KeyE"]).some((k) => this.keys.has(k)),
sprint:
this.touch.sprint ||
this.keys.has("ShiftLeft") ||
this.keys.has("ShiftRight"),
this.binding("sprint", ["ShiftLeft", "ShiftRight"]).some((k) =>
this.keys.has(k),
),
jumpPressed: this.actionQueue.has("jump"),
dashPressed: this.actionQueue.has("dash"),
resetPressed: this.actionQueue.has("reset"),
@@ -1192,7 +1269,9 @@ export class FormaRuntime {
target = c?.targetId
? this.nodes.get(c.targetId)?.getAbsolutePosition()
: B.Vector3.Zero();
if (target && c?.mode === "firstPerson") {
if (c?.mode === "fixed") {
this.updateFixedCamera();
} else if (target && c?.mode === "firstPerson") {
const player = this.state.find((n) => n.id === c.targetId);
const data = player?.components.data || {};
const speed = data.speed || 0;
@@ -1226,6 +1305,7 @@ export class FormaRuntime {
this.gameCamera.fov = c?.fov || 0.72;
}
}
if (this.playing) this.updateProjection();
for (const [id, b] of this.blends) {
b.time += dt;
const t = Math.min(1, b.time / 0.16);
@@ -1267,10 +1347,47 @@ export class FormaRuntime {
dispose() {
this.disposed = true;
this.stopWorker();
this.presentation?.dispose();
this.presentation = null;
this.cleanup.forEach((fn) => fn());
this.world?.free();
this.world = null;
this.scene?.dispose();
this.engine.dispose();
}
private updateFixedCamera() {
const entity = this.state.find((n) => n.enabled && n.components.camera);
const c = entity?.components.camera;
if (c?.mode !== "fixed") return;
this.gameCamera.position.copyFrom(
this.nodes.get(entity!.id)?.getAbsolutePosition() ||
B.Vector3.FromArray(entity!.transform.position),
);
this.gameCamera.setTarget(B.Vector3.FromArray(c.lookAt || [0, 0, 0]));
}
private updateProjection() {
const c = this.state.find((n) => n.enabled && n.components.camera)
?.components.camera;
const cam = this.gameCamera;
cam.mode =
c?.projection === "orthographic"
? B.Camera.ORTHOGRAPHIC_CAMERA
: B.Camera.PERSPECTIVE_CAMERA;
const screen =
this.engine.getRenderWidth() / Math.max(1, this.engine.getRenderHeight());
const aspect =
Number.isFinite(c?.aspect) && c?.aspect > 0 ? c!.aspect : screen;
cam.viewport = c?.aspect
? screen > aspect
? new B.Viewport((1 - aspect / screen) / 2, 0, aspect / screen, 1)
: new B.Viewport(0, (1 - screen / aspect) / 2, 1, screen / aspect)
: new B.Viewport(0, 0, 1, 1);
if (cam.mode === B.Camera.ORTHOGRAPHIC_CAMERA) {
const w = Math.max(0.1, c?.orthoWidth || 20);
cam.orthoLeft = -w / 2;
cam.orthoRight = w / 2;
cam.orthoTop = w / aspect / 2;
cam.orthoBottom = -w / aspect / 2;
}
}
}