@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
523 lines (408 loc) • 14.8 kB
JavaScript
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
};
}
}
}