UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

523 lines (408 loc) • 14.8 kB
import { mat4 } from "gl-matrix"; import { assert } from "../../../../core/assert.js"; import { BVH } from "../../../../core/bvh2/bvh3/BVH.js"; import { bvh_query_leaves_generic } from "../../../../core/bvh2/bvh3/query/bvh_query_leaves_generic.js"; import { bvh_query_user_data_overlaps_frustum } from "../../../../core/bvh2/bvh3/query/bvh_query_user_data_overlaps_frustum.js"; import { BVHQueryIntersectsRay } from "../../../../core/bvh2/bvh3/query/BVHQueryIntersectsRay.js"; import { returnTrue } from "../../../../core/function/returnTrue.js"; import { ray3_array_compose } from "../../../../core/geom/3d/ray/ray3_array_compose.js"; import { SurfacePoint3 } from "../../../../core/geom/3d/SurfacePoint3.js"; import { v3_distance_sqr } from "../../../../core/geom/vec3/v3_distance_sqr.js"; import Task from "../../../../core/process/task/Task.js"; import { TaskSignal } from "../../../../core/process/task/TaskSignal.js"; import TaskState from "../../../../core/process/task/TaskState.js"; import { iteratorTask } from "../../../../core/process/task/util/iteratorTask.js"; import { System } from "../../../ecs/System.js"; import { Transform } from "../../../ecs/transform/Transform.js"; import { RuntimeDrawMethodOptimizer } from "./render/optimization/RuntimeDrawMethodOptimizer.js"; import { ShadedGeometryRendererContext } from "./render/ShadedGeometryRendererContext.js"; import { ShadedGeometry } from "./ShadedGeometry.js"; /** * * @type {SurfacePoint3} */ const scratch_point = new SurfacePoint3(); /** * @readonly * @type {Float32Array} */ const scratch_ray_0 = new Float32Array(6); /** * * @param {ShadedGeometrySystem} system * @return {Generator} */ function* maintenance_generator(system) { while (true) { //infinite loop const contexts = system.__view_contexts; for (const [key, value] of contexts) { if (value === undefined) { continue; } yield* value.maintain(); } // a small break between re-runs yield; } } export class ShadedGeometrySystem extends System { /** * * @param {Engine} engine */ constructor(engine) { super(); this.dependencies = [ShadedGeometry, Transform]; /** *s * @type {Engine} * @private */ this.__engine = engine; /** * * @type {RenderLayer|null} * @private */ this.__render_layer = null; /** * * @type {Map<CameraView, ShadedGeometryRendererContext>} * @private */ this.__view_contexts = new Map(); /** * * @type {Map<number, function():AbstractRenderAdapter>} * @private */ this.__adapter_suppliers = new Map(); /** * Keeps count of usage for each geometry by ID * @type {Map<number, number>} * @private */ this.__geometry_usage_counters = new Map(); /** * * @type {BVH} * @private */ this.__bvh_binary = new BVH(); const optimizer = new RuntimeDrawMethodOptimizer(); this.__optimization_task = new Task({ name: 'ShadedGeometry Draw Method optimizer', initializer: () => { optimizer.__system = this; }, cycleFunction() { if (optimizer.step()) { return TaskSignal.Continue; } else { return TaskSignal.Yield; } } }); this.__maintenance_task = iteratorTask( 'Maintenance', maintenance_generator(this), TaskSignal.Yield ); } /** * * @returns {BVH} */ get bvh() { return this.__bvh_binary; } /** * NOTE: DO NOT MODIFY RESULTS * @returns {Map<number,number>} */ getGeometryUsageCounters() { return this.__geometry_usage_counters; } /** * * @param {number} index * @param {function():AbstractRenderAdapter} supplier */ register_render_adapter(index, supplier) { assert.isNonNegativeInteger(index, 'index'); assert.isFunction(supplier, 'supplier'); const map = this.__adapter_suppliers; if (map.has(index)) { // slot is already taken console.warn(`Render adapter slot '${index}' is already taken, removing old adapter`); this.unregister_render_adapter(index); } map.set(index, supplier); this.__view_contexts.forEach((value, key) => { const adapter = supplier(); value.setAdapter(index, adapter); }); } /** * * @param {number} index * @return {boolean} */ unregister_render_adapter(index) { assert.isNonNegativeInteger(index, 'index'); const supplier = this.__adapter_suppliers.get(index); if (supplier === undefined) { // not found return false; } this.__adapter_suppliers.delete(index); this.__view_contexts.forEach((value, key) => { value.removeAdapter(index); }); return true; } /** * * @param {CameraView} view * @private */ __handle_view_added(view) { const ctx = new ShadedGeometryRendererContext(); // attach adapters this.__adapter_suppliers.forEach((value, key) => { ctx.setAdapter(key, value()); }); this.__view_contexts.set(view, ctx); } /** * * @param {CameraView} view * @private */ __handle_view_removed(view) { const ctx = this.__view_contexts.get(view); this.__view_contexts.delete(view); ctx.dispose(); } async startup(entityManager) { this.entityManager = entityManager; const engine = this.__engine; const graphics = engine.graphics; const view_list = graphics.views.elements; view_list.forEach(this.__handle_view_added, this); view_list.on.added.add(this.__handle_view_added, this); view_list.on.removed.add(this.__handle_view_removed, this); this.__render_layer = graphics.layers.create('shaded-geometry'); this.__render_layer.buildVisibleSet = ((destination, destination_offset, view) => { const ctx = this.__view_contexts.get(view); const ecd = entityManager.dataset; if (ecd === null) { return 0; } return ctx.collect(destination, destination_offset, graphics.renderer, view, this.__bvh_binary, ecd); }); this.__optimization_task.state.set(TaskState.INITIAL); engine.executor.run(this.__optimization_task); this.__maintenance_task.state.set(TaskState.INITIAL); engine.executor.run(this.__maintenance_task); } async shutdown(entityManager) { const engine = this.__engine; const graphics = engine.graphics; const view_list = graphics.views.elements; engine.executor.removeTask(this.__optimization_task); engine.executor.removeTask(this.__maintenance_task); view_list.forEach(this.__handle_view_removed, this); view_list.on.added.remove(this.__handle_view_added, this); view_list.on.removed.remove(this.__handle_view_removed, this); } /** * * @param {ShadedGeometry} sg * @param {Transform} t * @param {number} entity */ link(sg, t, entity) { sg.__c_transform = t; sg.update_bounds(); t.subscribe(sg.updateTransform, sg); // remember entity for lookups sg.__entity = entity; // insert BVH entry sg.__bvh_leaf.link(this.__bvh_binary, entity); // update usage count const geometry_id = sg.geometry.id; const count_existing = this.__geometry_usage_counters.get(geometry_id); const count_new = count_existing !== undefined ? count_existing + 1 : 1; this.__geometry_usage_counters.set(geometry_id, count_new); } /** * * @param {ShadedGeometry} sg * @param {Transform} t * @param {number} entity */ unlink(sg, t, entity) { t.unsubscribe(sg.updateTransform, sg); // disconnect BVH sg.__bvh_leaf.unlink(); const geometry_id = sg.geometry.id; const count_existing = this.__geometry_usage_counters.get(geometry_id); if (count_existing === 1) { // last instance this.__geometry_usage_counters.delete(geometry_id); // release GPU memory sg.geometry.dispose(); } else { this.__geometry_usage_counters.set(geometry_id, count_existing - 1); } } /** * * @param {number[]} result entity IDs * @param {number} result_offset offset into result array where to write first result into * @param {number[]|ArrayLike<number>} planes * @returns {number} number of results */ queryOverlapFrustum( result, result_offset, planes ) { return bvh_query_user_data_overlaps_frustum(result, result_offset, this.__bvh_binary, planes); } /** * * @param {number} origin_x * @param {number} origin_y * @param {number} origin_z * @param {number} direction_x * @param {number} direction_y * @param {number} direction_z * @param {function(entity:number, mesh:ShadedGeometry):boolean} [filter_function] * @param {*} [filter_function_context] * @returns {{entity:number, mesh:ShadedGeometry, contact: SurfacePoint3}[]} */ raycast( origin_x, origin_y, origin_z, direction_x, direction_y, direction_z, filter_function = returnTrue, filter_function_context ) { const result = []; const hits = []; const bvh = this.__bvh_binary; const hit_count = bvh_query_leaves_generic(hits, 0, bvh, bvh.root, BVHQueryIntersectsRay.from([origin_x, origin_y, origin_z, direction_x, direction_y, direction_z])); const ecd = this.entityManager.dataset; if (ecd === null) { // no ECD set return result; } const sg_component_index = ecd.computeComponentTypeIndex(ShadedGeometry); for (let i = 0; i < hit_count; i++) { const node_id = hits[i]; const entity = bvh.node_get_user_data(node_id); /** * * @type {ShadedGeometry} */ const sg = ecd.getComponentByIndex(entity, sg_component_index); if (!filter_function.call(filter_function_context, entity, sg)) { // filtered out continue; } const m4 = sg.transform; ray3_array_compose( scratch_ray_0, origin_x, origin_y, origin_z, direction_x, direction_y, direction_z ); const geometry_hit_found = sg.query_raycast_nearest(scratch_point, scratch_ray_0, m4); if (!geometry_hit_found) { continue; } result.push({ entity, mesh: sg, contact: scratch_point.clone() }); } return result; } /** * * @param {SurfacePoint3} contact * @param {number} origin_x * @param {number} origin_y * @param {number} origin_z * @param {number} direction_x * @param {number} direction_y * @param {number} direction_z * @param {function(entity:number, mesh:ShadedGeometry):boolean} [filter_function] * @param {*} [filter_function_context] * @returns {{entity:number, mesh:ShadedGeometry}|undefined} */ raycastNearest( contact, origin_x, origin_y, origin_z, direction_x, direction_y, direction_z, filter_function = returnTrue, filter_function_context ) { let best_distance = Infinity; let found_hit = false; let best_result = null; const hits = []; const bvh = this.__bvh_binary; const hit_count = bvh_query_leaves_generic(hits, 0, bvh, bvh.root, BVHQueryIntersectsRay.from([origin_x, origin_y, origin_z, direction_x, direction_y, direction_z])); const ecd = this.entityManager.dataset; if (ecd === null) { return; } const sg_component_index = ecd.computeComponentTypeIndex(ShadedGeometry); for (let i = 0; i < hit_count; i++) { const node_id = hits[i]; const entity = bvh.node_get_user_data(node_id); /** * * @type {ShadedGeometry} */ const sg = ecd.getComponentByIndex(entity, sg_component_index); if (!filter_function.call(filter_function_context, entity, sg)) { // filtered out continue; } /** * * @type {number[]|mat4|null} */ const m4 = sg.transform; ray3_array_compose( scratch_ray_0, origin_x, origin_y, origin_z, direction_x, direction_y, direction_z ); const geometry_hit_found = sg.query_raycast_nearest(scratch_point, scratch_ray_0, m4); if (!geometry_hit_found) { continue; } found_hit = true; const hit_position = scratch_point.position; const distance_sqr = v3_distance_sqr( hit_position.x, hit_position.y, hit_position.z, origin_x, origin_y, origin_z ); if (distance_sqr < best_distance) { contact.copy(scratch_point); best_result = sg; best_distance = distance_sqr; } } if (found_hit) { return { entity: best_result.__entity, mesh: best_result }; } } }