@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
720 lines (521 loc) • 20 kB
JavaScript
import { OctahedronBufferGeometry } from "three";
import { TransformControls } from "../../../../editor/tools/v2/TransformControls.js";
import { TransformMode } from "../../../../editor/tools/v2/TransformMode.js";
import { assert } from "../../../core/assert.js";
import { BinaryBuffer } from "../../../core/binary/BinaryBuffer.js";
import { downloadAsFile } from "../../../core/binary/downloadAsFile.js";
import { EndianType } from "../../../core/binary/EndianType.js";
import { SignalBinding } from "../../../core/events/signal/SignalBinding.js";
import { SimpleStateMachine } from "../../../core/fsm/simple/SimpleStateMachine.js";
import { SimpleStateMachineDescription } from "../../../core/fsm/simple/SimpleStateMachineDescription.js";
import { SurfacePoint3 } from "../../../core/geom/3d/SurfacePoint3.js";
import Vector3 from "../../../core/geom/Vector3.js";
import { Action } from "../../../core/process/undo/Action.js";
import { ActionProcessor } from "../../../core/process/undo/ActionProcessor.js";
import { ArrayBufferLoader } from "../../asset/loaders/ArrayBufferLoader.js";
import Tag from "../../ecs/components/Tag.js";
import Entity from "../../ecs/Entity.js";
import { Transform } from "../../ecs/transform/Transform.js";
import { EngineHarness } from "../../EngineHarness.js";
import Highlight from "../ecs/highlight/Highlight.js";
import { ShadedGeometryHighlightSystem } from "../ecs/highlight/system/ShadedGeometryHighlightSystem.js";
import { ShadedGeometry } from "../ecs/mesh-v2/ShadedGeometry.js";
import { ShadedGeometrySystem } from "../ecs/mesh-v2/ShadedGeometrySystem.js";
import { make_ray_from_viewport_position } from "../make_ray_from_viewport_position.js";
import { MaterialTransformer } from "./gi/material/MaterialTransformer.js";
import { lpv_volume_bake_via_task } from "./lpv/build_probes_for_scene.js";
import { LightProbeVolumeSerializationAdapter } from "./lpv/serialization/LightProbeVolumeSerializationAdapter.js";
import { sh3_make_shaded_geometry } from "./visualise_spherical_harmonic_sphere.js";
const DEFAULT_SH_DATA = new Float32Array(27);
DEFAULT_SH_DATA.fill(1);
class ActionProbeRemove extends Action {
entity = -1;
_index = -1;
/**
* @type {Entity}
* @private
*/
_destroyed;
static from(entity) {
const r = new ActionProbeRemove();
r.entity = entity;
return r;
}
/**
*
* @param {EditorState} context
* @return {Promise<void>}
*/
async apply(context) {
const probe_index = context.getProbeIndex(this.entity);
this._index = probe_index;
this._destroyed = context.removeProbe(probe_index);
}
/**
*
* @param {EditorState} context
* @return {Promise<void>}
*/
async revert(context) {
this.entity = this._destroyed.id;
context.createProbe(this._destroyed);
this._destroyed = null;
this._index = -1;
}
}
class EditorState {
/**
*
* @type {Entity[]}
*/
probes = [];
/**
*
* @type {LightProbeVolume|null}
*/
volume = null
/**
* @type {Engine}
*/
engine
mesh_needs_rebuilding = false;
hideProbes(){
this.probes.forEach(p => p.destroy());
}
showProbes(){
const ecd = this.ecd;
this.probes.forEach(p => p.build(ecd));
}
ensureMesh() {
if (this.mesh_needs_rebuilding) {
this.volume.build_mesh();
this.mesh_needs_rebuilding = false;
}
}
/**
*
* @return {EntityComponentDataset}
*/
get ecd() {
return this.engine.entityManager.dataset;
}
/**
*
* @param {Entity} entity
*/
createProbe(entity) {
entity.build(this.ecd);
this.probes.push(entity);
const p = entity.getComponent(Transform).position;
const index = this.volume.add_point(...p);
assert.equal(index, this.probes.length - 1);
this.mesh_needs_rebuilding = true;
}
/**
*
* @param {number} index
* @return {Entity}
*/
removeProbe(index) {
const [probe] = this.probes.splice(index, 1);
probe.destroy();
this.volume.remove_point(index);
this.mesh_needs_rebuilding = true;
return probe;
}
probeCoefficientsFromVolume(index) {
const probe = this.probes[index];
const sg = probe.getComponent(ShadedGeometry);
sg.material.fromArray(this.volume.harmonics, index * 27);
}
/**
*
* @param {number} index
*/
probeToVolume(index) {
const probe = this.probes[index];
const t = probe.getComponent(Transform);
const existing_position = new Vector3();
existing_position.fromArray(this.volume.points, index * 3);
if (existing_position.equals(t.position)) {
// all good
return;
}
t.position.toArray(this.volume.points, index * 3);
this.volume.incrementVersion();
this.mesh_needs_rebuilding = true;
}
/**
*
* @param {number} entity_id
* @return {number}
*/
getProbeIndex(entity_id) {
return this.probes.findIndex(p => p.id === entity_id);
}
}
/**
*
* @param {Engine} engine
* @param {LightProbeVolume} volume
* @param {string} [path_key]
* @param {number} [placement_surface_offset] How far to place new probes away from the surface
* @param {number} [probe_size] purely visual, how big is each probe when visualised?
*/
export async function lpv_build_editor({
engine,
volume,
path_key = "lpv",
placement_surface_offset = 1,
probe_size = 1,
}) {
engine.assetManager.registerLoader('binary', new ArrayBufferLoader());
let build_in_progress = false;
async function bake() {
if (build_in_progress) {
console.warn('build in progress');
return;
}
build_in_progress = true;
ctx.hideProbes();
volume.build_mesh();
await lpv_volume_bake_via_task(volume, ecd, engine);
for (let i = 0; i < ctx.probes.length; i++) {
ctx.probeCoefficientsFromVolume(i);
}
ctx.showProbes();
update_volume_visual();
build_in_progress = false;
}
// create drop handle
document.body.addEventListener('dragover', (ev) => {
ev.preventDefault();
});
document.body.addEventListener("drop",
/**
*
* @param {DragEvent} ev
*/
(ev) => {
ev.preventDefault();
let processed = false;
/**
*
* @param {File} file
*/
function processFile(file) {
if (processed) {
return;
}
// URL @ Mozilla, webkitURL @ Chrome
const url = (window.webkitURL ? webkitURL : URL).createObjectURL(file);
engine.assetManager.promise(url, "binary").then(asset => {
const data = asset.create();
const buffer = BinaryBuffer.fromArrayBuffer(data);
buffer.endianness = EndianType.LittleEndian;
const adapter = new LightProbeVolumeSerializationAdapter();
adapter.deserialize(buffer, volume);
// assume mesh is invalid just in case
ctx.mesh_needs_rebuilding = true;
// reinit
reinit();
update_volume_visual();
});
processed = true;
}
if (ev.dataTransfer.items) {
// Use DataTransferItemList interface to access the file(s)
for (var i = 0; i < ev.dataTransfer.items.length; i++) {
// If dropped items aren't files, reject them
if (ev.dataTransfer.items[i].kind === 'file') {
var file = ev.dataTransfer.items[i].getAsFile();
processFile(file);
}
}
} else {
// Use DataTransfer interface to access the file(s)
for (var i = 0; i < ev.dataTransfer.files.length; i++) {
const file = ev.dataTransfer.files[i];
processFile(file);
}
}
})
const camera_controller = await EngineHarness.buildOrbitalCameraController({
engine
});
camera_controller.destroy();
const em = engine.entityManager;
const ecd = em.dataset;
em.addSystem(new ShadedGeometryHighlightSystem(engine));
ecd.registerComponentType(Highlight);
const PROBE_TAG_STRING = 'PLV/probe';
const geometry = new OctahedronBufferGeometry(1, 5);
const ctx = new EditorState();
ctx.probes = [];
ctx.volume = volume;
ctx.engine = engine;
/**
*
* @type {number[]}
*/
const selected = [];
const actions = new ActionProcessor(ctx);
const controls = new TransformControls(engine.graphics.camera, engine.viewStack.el);
controls.setMode(TransformMode.Translate);
controls.build(ecd);
controls.detach();
function unselect() {
if (selected.length === 0) {
return;
}
for (let i = 0; i < selected.length; i++) {
ecd.removeComponentFromEntity(selected[i], Highlight);
}
selected.splice(0, selected.length);
}
/**
*
* @param {number[]} entities
*/
function set_selection(entities) {
unselect();
for (let i = 0; i < entities.length; i++) {
const entity = entities[i];
ecd.addComponentToEntity(entity, Highlight.fromOne(1, 1, 0));
selected.push(entity);
}
}
function create_probe_entity(volume_index = -1) {
const entity = new Entity();
const transform = new Transform();
transform.scale.setScalar(probe_size);
entity.add(Tag.fromOne(PROBE_TAG_STRING));
entity.add(transform);
let sg;
if (volume_index >= 0) {
sg = sh3_make_shaded_geometry(
ctx.volume.harmonics, 27 * volume_index
);
} else {
sg = sh3_make_shaded_geometry(DEFAULT_SH_DATA, 0);
}
entity.add(sg);
return entity;
}
const smd = new SimpleStateMachineDescription();
const STATE_NAVIGATION = smd.createState();
const STATE_TRANSFORM = smd.createState();
smd.createEdge(STATE_NAVIGATION, STATE_TRANSFORM);
smd.createEdge(STATE_TRANSFORM, STATE_NAVIGATION);
const transform_proxy = new Entity().add(new Transform());
transform_proxy.build(ecd);
/**
*
* @param {number[]} entities
*/
function attach_transform(entities) {
const entities_copy = entities.slice();
// figure out center mass
const center = new Vector3();
for (let i = 0; i < entities.length; i++) {
const entity = entities[i];
center.add(
ecd.getComponent(entity, Transform).position
);
}
center.multiplyScalar(1 / entities.length);
transform_proxy.getComponent(Transform).position.copy(center);
const last_position = new Vector3();
last_position.copy(center);
controls.attach(transform_proxy.id);
controls.entity.addEventListener("change", () => {
if (entities_copy.length === 0) {
return;
}
const new_position = transform_proxy.getComponent(Transform).position.clone();
const displacement = new_position.clone();
displacement.sub(last_position);
last_position.copy(new_position);
for (let i = 0; i < entities.length; i++) {
const entity = entities[i];
const index = ctx.getProbeIndex(entity);
const probe = ctx.probes[index];
probe.getComponent(Transform).position.add(displacement);
ctx.probeToVolume(index);
}
})
}
const sm = new SimpleStateMachine(smd);
const keys = engine.devices.keyboard.keys;
function register_shortcuts_common() {
const ctrl = keys.ctrl;
keys.l.down.add(reinit);
keys.b.down.add(bake);
keys.z.down.add(() => {
if (ctrl.is_down) {
actions.undo();
}
});
keys.y.down.add(() => {
if (ctrl.is_down) {
actions.redo();
}
});
keys.s.down.add(() => {
if (ctrl.is_down) {
ctx.ensureMesh();
const buffer = BinaryBuffer.fromEndianness(EndianType.LittleEndian);
const adapter = new LightProbeVolumeSerializationAdapter();
adapter.serialize(buffer, volume);
buffer.trim();
engine.storage.promiseStoreBinary(`lpv:${path_key}`, buffer.data);
const date = new Date();
downloadAsFile(buffer.data, `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}_${date.getHours()}-${date.getMinutes()}-${date.getSeconds()}.lpv`)
}
});
}
const state_bindings_nav = [
new SignalBinding(engine.devices.pointer.on.tap, () => {
const ray = make_ray_from_viewport_position(engine, engine.devices.pointer.position);
const sgm = em.getSystem(ShadedGeometrySystem);
const contact = new SurfacePoint3();
if (keys.ctrl.is_down) {
// create new entity
const entity = sgm.raycastNearest(
contact,
...ray.origin,
...ray.direction,
(entity) => {
return !(ecd.getComponent(entity, Tag)?.contains(PROBE_TAG_STRING));
}
);
if (entity !== undefined) {
const probe = create_probe_entity();
const p = new Vector3();
p.copy(contact.normal);
p.multiplyScalar(0.01);
p.add(contact.position);
const p2 = new SurfacePoint3();
// do raycast in opposite direction
const entity_2 = sgm.raycastNearest(
p2,
...p,
...contact.normal,
(entity) => {
return !(ecd.getComponent(entity, Tag)?.contains(PROBE_TAG_STRING));
}
);
if (entity_2 !== undefined && p2.position.distanceTo(p) < placement_surface_offset * 2) {
p.lerpVectors(p, p2.position, 0.5);
} else {
p.copy(contact.normal);
p.multiplyScalar(placement_surface_offset);
p.add(contact.position);
}
probe.getComponent(Transform).position.copy(p);
ctx.createProbe(probe);
set_selection([probe.id]);
}
} else {
// do select
const entity = sgm.raycastNearest(
contact,
...ray.origin,
...ray.direction,
(entity) => {
return ecd.getComponent(entity, Tag)?.contains(PROBE_TAG_STRING);
}
);
if (entity !== undefined) {
const entity_id = entity.entity;
if (keys.shift.is_down) {
if (selected.includes(entity_id)) {
set_selection(selected.filter(e => e !== entity_id));
} else {
set_selection(selected.slice().concat(entity_id));
}
} else {
set_selection([entity_id]);
}
}
}
}),
new SignalBinding(keys.x.down, () => {
if (selected.length === 0) {
return;
}
const entities = selected.slice();
unselect();
actions.mark('Delete');
for (let i = 0; i < entities.length; i++) {
actions.do(ActionProbeRemove.from(entities[i]));
}
}),
new SignalBinding(keys.e.down, () => {
if (selected.length === 0) {
return;
}
sm.navigateTo(STATE_TRANSFORM);
}),
new SignalBinding(keys.a.down, () => {
if (keys.ctrl.is_down) {
// select all
set_selection(ctx.probes.map(e => e.id));
}
}),
new SignalBinding(keys.h.down,()=>{
if(ctx.probes.length === 0){
return;
}
if(ctx.probes[0].isBuilt){
ctx.hideProbes();
}else{
ctx.showProbes();
}
})
];
sm.addEventHandlerStateEntry(STATE_NAVIGATION, () => {
camera_controller.build(ecd);
state_bindings_nav.forEach(b => b.link());
});
sm.addEventHandlerStateExit(STATE_NAVIGATION, () => {
camera_controller.destroy();
state_bindings_nav.forEach(b => b.unlink())
});
const state_bindings_transform = [
new SignalBinding(keys.e.down, () => {
sm.navigateTo(STATE_NAVIGATION);
}),
];
sm.addEventHandlerStateEntry(STATE_TRANSFORM, () => {
state_bindings_transform.forEach(b => b.link());
attach_transform(selected);
});
sm.addEventHandlerStateExit(STATE_TRANSFORM, () => {
state_bindings_transform.forEach(b => b.unlink());
controls.detach();
});
sm.setState(STATE_NAVIGATION);
function initialize_from_volume() {
for (let i = 0; i < volume.count; i++) {
const entity = create_probe_entity(i);
entity.getComponent(Transform).position.fromArray(volume.points, i * 3);
ctx.probes.push(entity);
entity.build(ecd);
}
}
function update_volume_visual() {
const transformers = engine.graphics.getMaterialManager().getCompilationSteps(MaterialTransformer);
ctx.ensureMesh();
transformers.forEach(t => t.update());
}
function reinit() {
unselect();
ctx.probes.forEach(p => p.destroy());
ctx.probes.splice(0, ctx.probes.length);
initialize_from_volume();
console.log(volume);
}
initialize_from_volume();
register_shortcuts_common();
}