UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

467 lines (361 loc) • 13.1 kB
import { mat4 } from "gl-matrix"; import { assert } from "../../../../../core/assert.js"; import { BVH } from "../../../../../core/bvh2/bvh3/BVH.js"; import { BvhClient } from "../../../../../core/bvh2/bvh3/BvhClient.js"; import { bvh_query_leaves_ray } from "../../../../../core/bvh2/bvh3/query/bvh_query_leaves_ray.js"; import { bvh_query_user_data_overlaps_frustum } from "../../../../../core/bvh2/bvh3/query/bvh_query_user_data_overlaps_frustum.js"; import { LoadingCache } from "../../../../../core/cache/LoadingCache.js"; import { returnTrue } from "../../../../../core/function/returnTrue.js"; import { strictEquals } from "../../../../../core/function/strictEquals.js"; import { aabb3_matrix4_project } from "../../../../../core/geom/3d/aabb/aabb3_matrix4_project.js"; import { aabb3_raycast } from "../../../../../core/geom/3d/aabb/aabb3_raycast.js"; import { aabb3_transformed_compute_plane_side } from "../../../../../core/geom/3d/aabb/aabb3_transformed_compute_plane_side.js"; import { ray3_array_apply_matrix4 } from "../../../../../core/geom/3d/ray/ray3_array_apply_matrix4.js"; import { ray3_array_compose } from "../../../../../core/geom/3d/ray/ray3_array_compose.js"; import { SurfacePoint3 } from "../../../../../core/geom/3d/SurfacePoint3.js"; import { computeStringHash } from "../../../../../core/primitives/strings/computeStringHash.js"; import { AssetManager } from "../../../../asset/AssetManager.js"; import { GameAssetType } from "../../../../asset/GameAssetType.js"; import { AbstractContextSystem } from "../../../../ecs/system/AbstractContextSystem.js"; import { SystemEntityContext } from "../../../../ecs/system/SystemEntityContext.js"; import { Transform } from "../../../../ecs/transform/Transform.js"; import { Reference } from "../../../../reference/v2/Reference.js"; import { Decal as FPDecal } from '../../../render/forward_plus/model/Decal.js'; import { ForwardPlusRenderingPlugin } from "../../../render/forward_plus/plugin/ForwardPlusRenderingPlugin.js"; import { Sampler2D } from "../../../texture/sampler/Sampler2D.js"; import { sampler2d_ensure_uint8_RGBA } from "../../../texture/sampler/sampler2d_ensure_uint8_RGBA.js"; import { Decal } from "./Decal.js"; const placeholder_texture = Sampler2D.uint8(4, 1, 1); placeholder_texture.data.fill(255); /** * @readonly * @type {Float32Array} */ const scratch_ray_0 = new Float32Array(6); /** * * @type {Float32Array|mat4} */ const scratch_m4 = new Float32Array(16); /** * * @type {Float32Array} */ const AABB_UNIT_CUBE = new Float32Array([ -0.5, -0.5, -0.5, 0.5, 0.5, 0.5 ]); class Context extends SystemEntityContext { /** * * @type {FPDecal|null} * @private */ __fp_decal = null; #bvh_leaf = new BvhClient(); /** * @return {ForwardPlusRenderingPlugin} * @private */ getPlugin() { return this.system.__fp_plugin.getValue(); } /** * * @returns {Transform} */ getTransform() { return this.components[1]; } /** * * @returns {Decal} */ getDecalComponent() { return this.components[0]; } __update_transform() { /** * * @type {Transform} */ const transform = this.getTransform(); this.__fp_decal.setTransform(transform.matrix); aabb3_matrix4_project(this.#bvh_leaf.bounds, AABB_UNIT_CUBE, transform.matrix); this.#bvh_leaf.write_bounds(); } async __load_texture() { const decal_spec = this.getDecalComponent(); const _uri = decal_spec.uri; if (decal_spec.__cached_sampler !== null && decal_spec.__cached_uri === _uri) { // cached asset is still relevant return; } /** * * @type {FPDecalSystem} */ const system = this.system; /** * @type {Sampler2D} */ const loaded_texture = await system.__texture_cache.get(_uri); assert.defined(loaded_texture, 'loaded_texture'); assert.notNull(loaded_texture, 'loaded_texture'); assert.equal(loaded_texture.isSampler2D, true, 'texture.isSampler2D !== true'); decal_spec.__cached_sampler = loaded_texture; decal_spec.__cached_uri = _uri; } rebuild() { const plugin = this.getPlugin(); const lm = plugin.getLightManager(); if (this.__fp_decal === null || !lm.hasLight(this.__fp_decal)) { return; } lm.requestDataUpdate(); } link() { const plugin = this.getPlugin(); const lm = plugin.getLightManager(); const fpDecal = new FPDecal(); this.__fp_decal = fpDecal; this.__update_transform(); const transform = this.getTransform(); transform.subscribe(this.__update_transform, this); const decal_spec = this.getDecalComponent(); // propagate draw priority onto decal Object.defineProperties(fpDecal, { 'draw_priority': { get() { return decal_spec.priority; } }, 'color': { get() { return decal_spec.color; } } }); decal_spec.color.onChanged.add(this.rebuild, this); this.__load_texture().then(() => { if (!this.__is_linked) { // not linked anymore return; } const sampler = decal_spec.__cached_sampler; if (sampler === null) { return; } fpDecal.texture_diffuse = sampler; if (this.__fp_decal === fpDecal) { lm.addLight(fpDecal); } }); this.#bvh_leaf.link(this.system.bvh, this.entity); super.link(); } unlink() { this.getPlugin() .getLightManager() .removeLight(this.__fp_decal); this.__fp_decal = null; const transform = this.getTransform(); transform.subscribe(this.__update_transform, this); const decal_spec = this.getDecalComponent(); decal_spec.color.onChanged.remove(this.rebuild, this); this.#bvh_leaf.unlink(); super.unlink(); } } // TODO we can reduce memory usage and speed things up considerably by using a queue to wait for assets instead of using promises everywhere /** * */ export class FPDecalSystem extends AbstractContextSystem { bvh = new BVH(); /** * * @param {Engine} engine */ constructor(engine) { super(Context); this.dependencies = [Decal, Transform]; /** * * @type {LoadingCache<string, Sampler2D>} * @private */ this.__texture_cache = new LoadingCache({ load: (key) => { return this.__assets.promise(key, GameAssetType.Image).then(asset => { const asset_sampler = asset.create(); return sampler2d_ensure_uint8_RGBA(asset_sampler); }); }, keyHashFunction: computeStringHash, keyEqualityFunction: strictEquals }); /** * * @type {AssetManager} * @private */ this.__assets = engine.assetManager; /** * * @type {Engine} * @private */ this.__engine = engine; /** * * @type {Reference<ForwardPlusRenderingPlugin>|null} * @private */ this.__fp_plugin = Reference.NULL; } async startup(em) { this.__fp_plugin = await this.__engine.plugins.acquire(ForwardPlusRenderingPlugin); } async shutdown(em) { this.__fp_plugin.release(); } /** * * @param {number[]} result entity IDs * @param {number} result_offset * @param {number[]} planes * @returns {number} number of results */ queryOverlapFrustum(result, result_offset, planes) { const ecd = this.entityManager.dataset; if (ecd === null) { return 0; } const entities = []; const rough_count = bvh_query_user_data_overlaps_frustum( entities, 0, this.bvh, planes ); let result_cursor = result_offset; loop_0: for (let i = 0; i < rough_count; i++) { const entity = entities[i]; /** * * @type {Transform} */ const transform = ecd.getComponent(entity, Transform); if (transform === undefined) { // this shouldn't happen either continue; } for (let j = 0; j < 6; j++) { // test true box against each plane const plane_address = j * 4; const plane_x = planes[plane_address] const plane_y = planes[plane_address + 1] const plane_z = planes[plane_address + 2] const plane_offset = planes[plane_address + 3] const side = aabb3_transformed_compute_plane_side( -0.5, -0.5, -0.5, 0.5, 0.5, 0.5, plane_x, plane_y, plane_z, plane_offset, transform.matrix ); if (side < 0) { // completely outside continue loop_0; } } result[result_cursor++] = entity; } return result_cursor - result_offset; } /** * * @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, component:Decal):boolean} [filter_function] * @param {*} [filter_function_context] * @returns {{entity:number, component:Decal, contact: SurfacePoint3}[]} */ raycast( origin_x, origin_y, origin_z, direction_x, direction_y, direction_z, filter_function = returnTrue, filter_function_context ) { const ecd = this.entityManager.dataset; if (ecd === null) { return []; } const leaves = []; const bvh = this.bvh; const hit_count = bvh_query_leaves_ray(bvh, bvh.root, leaves, 0, origin_x, origin_y, origin_z, direction_x, direction_y, direction_z ); const result = []; const temp_hit = new Float32Array(6); for (let i = 0; i < hit_count; i++) { const node = leaves[i]; const entity = bvh.node_get_user_data(node); /** * * @type {Decal} */ const decal = ecd.getComponent(entity, Decal); if (decal === undefined) { // this shouldn't happen continue; } if (!filter_function.call(filter_function_context, entity, decal)) { continue; } /** * * @type {Transform} */ const transform = ecd.getComponent(entity, Transform); if (transform === undefined) { // this shouldn't happen either continue; } ray3_array_compose( scratch_ray_0, origin_x, origin_y, origin_z, direction_x, direction_y, direction_z ); // get transform in local space mat4.invert(scratch_m4, transform.matrix); // transform ray into decal's local space ray3_array_apply_matrix4(scratch_ray_0, 0, scratch_ray_0, 0, scratch_m4); if (aabb3_raycast( temp_hit, 0, -0.5, -0.5, -0.5, 0.5, 0.5, 0.5, scratch_ray_0[0], scratch_ray_0[1], scratch_ray_0[2], scratch_ray_0[3], scratch_ray_0[4], scratch_ray_0[5], )) { const contact = new SurfacePoint3(); contact.fromArray(temp_hit); // contact will be the center of decal, and normal will follow the decal normal contact.applyMatrix4(transform.matrix); result.push({ entity, component: decal, contact }); } } return result; } }