@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
568 lines (449 loc) • 18.6 kB
JavaScript
import { GUI } from "dat.gui";
import {
ClampToEdgeWrapping,
DataTexture,
LinearFilter,
Matrix4,
MeshBasicMaterial,
MeshStandardMaterial,
NearestFilter,
PlaneBufferGeometry,
RepeatWrapping,
RGBAFormat,
TextureLoader,
UnsignedByteType
} from "three";
import '../../../../../../../css/game.scss';
import { BinaryBuffer } from "../../../core/binary/BinaryBuffer.js";
import { array_copy } from "../../../core/collection/array/array_copy.js";
import { is_typed_array_equals } from "../../../core/collection/array/typed/is_typed_array_equals.js";
import { Color } from "../../../core/color/Color.js";
import { kelvin_to_rgb } from "../../../core/color/kelvin/kelvin_to_rgb.js";
import { AABB3 } from "../../../core/geom/3d/aabb/AABB3.js";
import { compose_matrix4_array } from "../../../core/geom/3d/mat4/compose_matrix4_array.js";
import { visualize_tetrahedral_mesh } from "../../../core/geom/3d/tetrahedra/visualize_tetrahedral_mesh.js";
import Quaternion from "../../../core/geom/Quaternion.js";
import { v3_length } from "../../../core/geom/vec3/v3_length.js";
import Vector3 from "../../../core/geom/Vector3.js";
import { max2 } from "../../../core/math/max2.js";
import { min2 } from "../../../core/math/min2.js";
import { RAD_TO_DEG } from "../../../core/math/RAD_TO_DEG.js";
import { randomFloatBetween } from "../../../core/math/random/randomFloatBetween.js";
import { seededRandom } from "../../../core/math/random/seededRandom.js";
import { delay } from "../../../core/process/delay.js";
import ButtonView from "../../../view/elements/button/ButtonView.js";
import { GLTFAssetLoader } from "../../asset/loaders/GLTFAssetLoader.js";
import Entity from "../../ecs/Entity.js";
import GUIElement from "../../ecs/gui/GUIElement.js";
import GUIElementSystem from "../../ecs/gui/GUIElementSystem.js";
import ViewportPositionSystem from "../../ecs/gui/position/ViewportPositionSystem.js";
import { TransformAttachmentSystem } from "../../ecs/transform-attachment/TransformAttachmentSystem.js";
import { Transform } from "../../ecs/transform/Transform.js";
import { EngineHarness } from "../../EngineHarness.js";
import { Camera } from "../ecs/camera/Camera.js";
import { focal_length_to_fov } from "../ecs/camera/focal_length_to_fov.js";
import TopDownCameraController from "../ecs/camera/topdown/TopDownCameraController.js";
import { Light } from "../ecs/light/Light.js";
import LightSystem from "../ecs/light/LightSystem.js";
import { LightType } from "../ecs/light/LightType.js";
import { SGMeshSystem } from "../ecs/mesh-v2/aggregate/SGMeshSystem.js";
import { ShadedGeometry } from "../ecs/mesh-v2/ShadedGeometry.js";
import { ShadedGeometryFlags } from "../ecs/mesh-v2/ShadedGeometryFlags.js";
import { ShadedGeometrySystem } from "../ecs/mesh-v2/ShadedGeometrySystem.js";
import { three_object_to_entity_composition } from "../ecs/mesh-v2/three_object_to_entity_composition.js";
import { GizmoRenderingPlugin } from "../render/gizmo/GizmoRenderingPlugin.js";
import { MaterialTransformer, ProbeResolutionStage } from "./gi/material/MaterialTransformer.js";
import { lpv_volume_bake_via_task } from "./lpv/build_probes_for_scene.js";
import { OctahedralDepthDebuggerWidget } from "./lpv/depth/octahedral/OctahedralDepthDebuggerWidget.js";
import { VLPDepthMapVisualisation } from "./lpv/depth/octahedral/v2/VLPDepthMapVisualisation.js";
import { LightProbeVolume } from "./lpv/LightProbeVolume.js";
import { lpv_obtain_storage_cached_volume } from "./lpv/lpv_obtain_storage_cached_volume.js";
import { lpv_visualize_probes } from "./lpv/util/lpv_visualise_probes.js";
import { lpv_build_editor } from "./lpv_build_editor.js";
/**
*
* @param {Storage} storage
* @param {*} component
* @param {BinaryClassSerializationAdapter} adapter
* @param {string} key
* @param {Signal[]} signals
*/
async function persist_component(storage, component, adapter, key = 'temp', signals) {
const current_state = new BinaryBuffer();
const full_key = `persisted_transform:${key}`;
if (await storage.promiseContains(full_key)) {
const loaded = await storage.promiseLoadBinary(full_key);
const buffer = BinaryBuffer.fromArrayBuffer(loaded);
adapter.deserialize(buffer, component);
}
// make a binary state copy
adapter.serialize(current_state, component);
for (let i = 0; i < signals.length; i++) {
signals[i].add(store);
}
function store() {
const buffer = new BinaryBuffer();
adapter.serialize(buffer, component);
buffer.trim();
if (!is_typed_array_equals(current_state.__data_uint8, buffer.__data_uint8)) {
// change detected
storage.promiseStoreBinary(full_key, buffer.data);
current_state.setCapacity(buffer.length);
array_copy(buffer.__data_uint8, 0, current_state.__data_uint8, 0, buffer.capacity);
}
}
}
/**
*
* @param {number|Vector3} scale
* @param {Vector3} offset
* @param {Vector3} direction
* @param up
* @param shadow
* @param material
* @param {Engine} engine
*/
function make_plane({
scale,
offset,
direction = Vector3.forward,
up = Vector3.up,
shadow = true,
material = new MeshBasicMaterial({ color: 0xFFFFFF }),
engine
}) {
const t1 = Transform.fromJSON({
position: offset,
scale: scale
});
t1.rotation.lookRotation(direction, up);
if (engine) {
material = engine.graphics.getMaterialManager().obtain(material).getValue();
}
const sg = ShadedGeometry.from(new PlaneBufferGeometry(), material);
sg.writeFlag(ShadedGeometryFlags.CastShadow, shadow);
sg.writeFlag(ShadedGeometryFlags.ReceiveShadow, shadow);
return new Entity()
.add(sg)
.add(t1);
}
function make_cornel_box(ecd) {
make_plane({
scale: 20,
offset: new Vector3(10, 10, 0),
direction: Vector3.forward,
material: new MeshBasicMaterial({ color: 0xFF0000 })
}).build(ecd);
make_plane({
scale: 20,
offset: new Vector3(20, 10, 10),
direction: Vector3.left,
material: new MeshBasicMaterial({ color: 0x00FF00 })
}).build(ecd);
make_plane({
scale: 20,
offset: new Vector3(0, 10, 10),
direction: Vector3.right,
material: new MeshBasicMaterial({ color: 0xFFFFFF })
}).build(ecd);
}
function make_test_texture(t = 1) {
const t1 = (1 - t) * 0.5;
const tex = new DataTexture(new Uint8ClampedArray([
255 * t, 255 * t1, 255 * t1, 255,
255 * t1, 255 * t, 255 * t1, 255,
255 * t1, 255 * t1, 255 * t, 255,
255 * t, 255 * t, 255 * t1, 255
]), 2, 2, RGBAFormat, UnsignedByteType);
tex.flipY = false;
tex.wrapS = ClampToEdgeWrapping;
tex.wrapT = ClampToEdgeWrapping;
tex.magFilter = NearestFilter;
tex.minFilter = LinearFilter;
tex.generateMipmaps = false;
tex.needsUpdate = true;
return tex;
}
/**
*
* @param {EntityComponentDataset} ecd
* @param {Vector3} [center]
* @param {Vector3} [size]
* @param {GraphicsEngine} graphics
* @param {number} [texture_frequency]
*/
function createFloor({
ecd,
graphics,
center = new Vector3(0, 0, 0),
size = new Vector3(1000, 1000, 1000),
texture_frequency = 1
}) {
const map = new TextureLoader().load('data/textures/utility/checkers_dark_grey_256x256.png', t => {
map.needsUpdate = true;
map.wrapS = RepeatWrapping;
map.wrapT = RepeatWrapping;
map.repeat.set(size.x / texture_frequency, size.z / texture_frequency);
map.matrixAutoUpdate = true;
});
const material = graphics.getMaterialManager().obtain(new MeshStandardMaterial({
map: map
})).getValue();
const geometry = new PlaneBufferGeometry(1, 1, 1, 1);
const q = Quaternion.fromEulerAngles(-Math.PI * 0.5, 0, 0);
const m = new Matrix4();
compose_matrix4_array(m.elements, Vector3.zero, q, Vector3.one);
geometry.applyMatrix4(m);
geometry.computeVertexNormals();
geometry.computeTangents();
new Entity()
.add(Transform.fromJSON({
position: center.toJSON(),
// rotation: ,
scale: size
}))
.add(ShadedGeometry.from(geometry, material))
.build(ecd);
}
/**
*
* @param {Engine} engine
* @return {Promise<void>}
*/
async function main(engine) {
const volume = new LightProbeVolume();
const transformer = new MaterialTransformer({
volume ,
stage: ProbeResolutionStage.Vertex
});
engine.graphics.getMaterialManager().addCompileStep(transformer);
await EngineHarness.buildBasics({
engine,
showFps: false,
enableWater: false,
enableTerrain: false,
enableLights: false,
// enableLights: true,
cameraFarDistance: 200,
cameraController: false,
focus: { x: 0, y: 0, z: 0 },
pitch: 1,
yaw: -1.54,
distance: 54,
cameraFieldOfView: focal_length_to_fov(50, 25) * RAD_TO_DEG,
// shadowmapResolution: 4096
});
const sun_color = new Color();
kelvin_to_rgb(sun_color, 0, 5000);
await EngineHarness.buildLights({
engine,
shadowmapResolution: 4096,
ambientIntensity: 0.0,
sun: sun_color,
// sunIntensity: 1.7,
sunIntensity: 3,
// sunDirection: new Vector3(0.2, -0.8, 1)
sunDirection: new Vector3(1.2, -1, 0.2)
})
engine.graphics.getRenderer().setClearColor('#1e3441', 1);
// load_and_set_cubemap_v0(engine.graphics, 'data/textures/cubemaps/hip_miramar/32/', '.png');
const ecd = engine.entityManager.dataset;
// const path = 'data/models/samples/sd_macross_city_standoff_diorama/scene.gltf';
// const path = 'data/models/sibenik/2/model.gltf';
// const path = 'data/models/vokselia_spawn/model.gltf';
// const path = 'data/models/samples/just_a_girl/scene.gltf';
// const path = 'data/models/samples/slum_house/scene.gltf';
// const path = 'data/models/samples/jack_trigger/scene.gltf';
// const path = 'data/models/samples/cornell_box/scene.gltf';
// const path = 'data/models/Slaughter Mech/Slaugter Mech.gltf';
// const path = 'data/models/samples/gi_box_01/model-thick-1.glb';
// const path = 'data/models/samples/ancient_bath_house_-_modular_set/scene.gltf';
// const path = 'data/models/samples/environment_-_library/scene.gltf';
// const path = 'data/models/Scans/green_car_wreck/scene.gltf';
// const path = 'data/models/samples/the_attic_environment/scene1.gltf';
// const path = 'data/models/LowPolyTownshipSet/Small_house/Small_house.gltf';
// const path = 'data/models/samples/cyberpunk_bike/scene.gltf';
// const path = 'data/models/LowPolyTownshipSet/Town_Hall/model.gltf';
// const path = 'data/models/sibenik/3-window-less/model.gltf';
// const path = 'data/models/sponza-pbr/gltf/sponza.glb';
// const path = 'data/models/samples/susanne.glb';
// const path = 'data/models/samples/teapot.gltf';
// const path = 'data/models/samples/salle_de_bain/model.glb';
// const path = 'data/models/samples/conference/model-no-curtains.glb';
const path = 'data/models/pica_pica/pica_pica.gltf';
// const path = 'data/models/samples/gi_box_01/model.glb';
// const path = 'data/models/samples/low_poly_classroom/no-glass/model.gltf';
// const path = 'customer_data/halon_scene.glb';
const mesh_asset = await engine.assetManager.promise(path, 'model/gltf+json');
const gltf = mesh_asset.gltf;
console.log(gltf);
// add floor
// make_cornel_box(ecd);
const composition = three_object_to_entity_composition(gltf.scene);
// composition.transform.rotation.multiply(Quaternion.fromEulerAngles(DEG_TO_RAD * 30, DEG_TO_RAD * 30, 0));
composition.traverse(n => {
/**
* @type {ShadedGeometry}
*/
const sg = n.entity.getComponent(ShadedGeometry);
if (sg === null) {
return;
}
sg.writeFlag(ShadedGeometryFlags.CastShadow | ShadedGeometryFlags.ReceiveShadow, true);
const material = sg.material;
if (material !== null) {
material.depthTest = true;
material.depthWrite = true;
}
});
composition.transform.scale.setScalar(1);
//
// composition.transform.rotation.fromEulerAngles(0, Math.PI, 0);
composition.build(ecd);
/**
* @type {AABB3}
*/
const mesh_bounds = mesh_asset.boundingBox;
// const mesh_bounds = new AABB3(0, 0, 0, 20, 20, 20);
createFloor({
center: new Vector3(mesh_bounds.getCenterX(), mesh_bounds.y0, mesh_bounds.getCenterZ()),
size: new Vector3(
max2(0.1, mesh_bounds.getExtentsX() * 2),
1,
max2(0.1, mesh_bounds.getExtentsZ() * 2)
),
ecd,
graphics: engine.graphics
});
const model_footprint = v3_length(mesh_bounds.getExtentsX(), mesh_bounds.getExtentsY(), mesh_bounds.getExtentsZ());
const camera = ecd.getAnyComponent(Camera);
camera.component.clip_far = max2(100, model_footprint * 3);
const camera_controller = ecd.getComponent(camera.entity, TopDownCameraController);
camera_controller.distanceMax = model_footprint * 2;
camera_controller.distanceMin = min2(camera_controller.distanceMin, model_footprint * 0.01);
mesh_bounds.getCenter(camera_controller.target)
const random = seededRandom();
for (let i = 0; i < 0; i++) {
new Entity()
.add(Transform.fromJSON({
position: {
x: randomFloatBetween(random, mesh_bounds.x0, mesh_bounds.x1),
y: randomFloatBetween(random, mesh_bounds.y0, mesh_bounds.y1),
z: randomFloatBetween(random, mesh_bounds.z0, mesh_bounds.z1),
}
}))
.add(Light.fromJSON({
distance: model_footprint * 0.1,
type: LightType.POINT,
intensity: 0.5,
color: `hsv(${random()},${1},1)`
}))
.build(ecd);
}
new Entity()
.add(GUIElement.fromView(new ButtonView({
name: "Toggle Model",
action() {
composition.isBuilt ? composition.destroy() : composition.build(ecd)
}
})))
.build(ecd);
await delay(200);
console.time('getVolume');
// console.profile('getVolume');
await lpv_obtain_storage_cached_volume({ engine, path, volume, density: 50 });
// console.profileEnd('getVolume');
console.timeEnd('getVolume');
console.log('LPV:', volume);
transformer.update();
const probe_viz = lpv_visualize_probes({ volume, size: model_footprint * 0.002 });
// probe_viz.build(ecd);
const mesh_viz = visualize_tetrahedral_mesh({
mesh: volume.mesh,
positions: volume.points,
opacity: 0.5
});
const depth_viz = VLPDepthMapVisualisation.from(volume).build();
// depth_viz.build(ecd);
// volume.visualize_mesh({ ecd, opacity: 0.5 });
const gui = new GUI();
gui.add(transformer, 'intensity').min(0).max(10);
gui.add({
capture_camera() {
const transform = ecd.getComponent(camera.entity, Transform);
console.log(JSON.stringify(transform.toJSON()))
}
}, "capture_camera");
gui.add({
get probe_vis() {
return probe_viz.isBuilt
},
set probe_vis(v) {
if (v) {
probe_viz.build(ecd);
} else {
probe_viz.destroy();
}
}
}, "probe_vis").name("Visualise Probes");
gui.add({
get mesh_vis() {
return mesh_viz.isBuilt
},
set mesh_vis(v) {
if (v) {
mesh_viz.build(ecd);
} else {
mesh_viz.destroy();
}
}
}, "mesh_vis").name("Visualise Mesh");
gui.add({
get depth_vis() {
return depth_viz.isBuilt
},
set depth_vis(v) {
if (v) {
depth_viz.build(ecd);
} else {
depth_viz.destroy();
}
}
}, "depth_vis").name("Visualise Depth");
gui.add({
bake(){
volume.build_mesh();
lpv_volume_bake_via_task(volume, ecd, engine);
}
},"bake").name("Bake");
// makeDebugGizmo(engine, mesh_bounds.getCenter());
// mesh_entity.addEventListener(SGMeshEvents.AssetLoaded, build);
//
// mesh_entity.build(ecd);
lpv_build_editor({ engine, volume, path_key: path });
}
/**
*
* @param {Engine} engine
* @param {Vector3} position
*/
function makeDebugGizmo(engine, position) {
const widget = new OctahedralDepthDebuggerWidget();
widget.entity.getComponentSafe(Transform).position.copy(position);
widget.build(engine.entityManager.dataset, engine);
}
new EngineHarness().initialize({
configuration(config, engine) {
config.addSystem(new ShadedGeometrySystem(engine));
config.addSystem(new LightSystem(engine, {
shadowResolution: 8192
}));
config.addSystem(new SGMeshSystem(engine));
config.addSystem(new TransformAttachmentSystem());
config.addSystem(new GUIElementSystem(engine.gui.view, engine));
config.addSystem(new ViewportPositionSystem(engine.gui.view.size));
config.addLoader('model/gltf+json', new GLTFAssetLoader());
config.addPlugin(GizmoRenderingPlugin);
// config.addPlugin(AmbientOcclusionPostProcessEffect);
}
}).then(main);