UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

720 lines (521 loc) • 20 kB
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(); }