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