Add browser-only editor export and 2.5D runtime presentation
This commit is contained in:
+129
-12
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user