UNPKG

@matematrolii/sketchbook

Version:

3D matematrolii playground built on three.js and cannon.js

564 lines (499 loc) 17.5 kB
import * as THREE from "three"; // @ts-ignore import * as CANNON from "cannon"; import { CameraOperator } from "../core/CameraOperator"; import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer"; import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass"; import { ShaderPass } from "three/examples/jsm/postprocessing/ShaderPass"; import { FXAAShader } from "three/examples/jsm/shaders/FXAAShader"; import { Detector } from "../../lib/utils/Detector"; import { Stats } from "../../lib/utils/Stats"; import * as _ from "lodash"; import { CannonDebugRenderer } from "../../lib/cannon/CannonDebugRenderer"; import { InputManager } from "../core/InputManager"; import * as Utils from "../core/FunctionLibrary"; import { LoadingManager } from "../core/LoadingManager"; import { IWorldEntity } from "../interfaces/IWorldEntity"; import { IUpdatable } from "../interfaces/IUpdatable"; import { Character } from "../characters/Character"; import { CollisionGroups } from "../enums/CollisionGroups"; import { BoxCollider } from "../physics/colliders/BoxCollider"; import { TrimeshCollider } from "../physics/colliders/TrimeshCollider"; import { Item } from "../items/Item"; import { Bullet } from "../characters/Bullet"; import { Scenario } from "./Scenario"; import { Sky } from "./Sky"; import { IEventType } from "../interfaces/IEventType"; import { EventType } from "../enums/EventType"; import { ModelProps, MapDataType, MapType, MATEMATROLII_EVENT, } from "../enums/WorldType"; import { IWorldOptions, IWorldParams } from "../interfaces/IWorldSettings"; export class World { public cannonDebugRenderer: CannonDebugRenderer; public renderer: THREE.WebGLRenderer; public camera: THREE.PerspectiveCamera; public composer: any; public stats: Stats; public graphicsWorld: THREE.Scene; public sky: Sky; public physicsWorld: CANNON.World; public parallelPairs: any[]; public physicsFrameRate: number; public physicsFrameTime: number; public physicsMaxPrediction: number; public clock: THREE.Clock; public renderDelta: number; public logicDelta: number; public requestDelta: number; public sinceLastFrame: number; public justRendered: boolean; public params: any; public inputManager: InputManager; public cameraOperator: CameraOperator; public timeScaleTarget: number = 1; public scenarios: Scenario[] = []; public characters: Character[] = []; public characterNames: { [key: string]: Character; } = {}; public items: Item[] = []; public bullets: Bullet[] = []; public updatables: IUpdatable[] = []; public worldOptions: IWorldOptions = null; public worldParams: IWorldParams = null; private lastScenarioID: string; private animationFrame = null; private pixelRatio: number = null; private fxaaPass: ShaderPass = null; constructor( worldOptions: IWorldOptions, worldParams: IWorldParams, canvas?: HTMLCanvasElement ) { const scope = this; // WebGL not supported if (!Detector.webgl) { console.log( "This browser doesn't seem to have the required WebGL capabilities" ); } this.worldOptions = worldOptions; this.worldParams = worldParams; // Renderer this.renderer = new THREE.WebGLRenderer({ powerPreference: "high-performance", antialias: false, alpha: false, stencil: false, canvas, }); this.renderer.setPixelRatio(window.devicePixelRatio); this.renderer.setSize(window.innerWidth, window.innerHeight); this.renderer.toneMapping = THREE.ACESFilmicToneMapping; this.renderer.toneMappingExposure = 1.0; this.renderer.shadowMap.enabled = true; this.renderer.shadowMap.type = THREE.PCFSoftShadowMap; this.renderer.sortObjects = false; //this.renderer.physicallyCorrectLights = true; // Canvas if (!canvas) { document.body.appendChild(this.renderer.domElement); } // Auto window resize window.addEventListener("resize", this.onWindowResize.bind(this), false); // Three.js scene this.graphicsWorld = new THREE.Scene(); this.camera = new THREE.PerspectiveCamera( 80, window.innerWidth / window.innerHeight, 0.1, 300 ); // Passes let renderPass = new RenderPass(this.graphicsWorld, this.camera); this.fxaaPass = new ShaderPass(FXAAShader); // FXAA this.pixelRatio = this.renderer.getPixelRatio(); this.fxaaPass.material["uniforms"].resolution.value.x = 1 / (window.innerWidth * this.pixelRatio); this.fxaaPass.material["uniforms"].resolution.value.y = 1 / (window.innerHeight * this.pixelRatio); // Composer this.composer = new EffectComposer(this.renderer); this.composer.addPass(renderPass); this.composer.addPass(this.fxaaPass); // Physics this.physicsWorld = new CANNON.World(); this.physicsWorld.gravity.set(0, -9.81, 0); this.physicsWorld.broadphase = new CANNON.SAPBroadphase(this.physicsWorld); this.physicsWorld.solver.iterations = 10; this.physicsWorld.allowSleep = true; this.parallelPairs = []; this.physicsFrameRate = 60; this.physicsFrameTime = 1 / this.physicsFrameRate; this.physicsMaxPrediction = this.physicsFrameRate; // RenderLoop this.clock = new THREE.Clock(); this.renderDelta = 0; this.logicDelta = 0; this.sinceLastFrame = 0; this.justRendered = false; // Stats (FPS, Frame time, Memory) this.stats = Stats(); this.params = _.merge( { Pointer_Lock: true, // true - false Mouse_Sensitivity: 0.3, // 0 - 1 Time_Scale: 1, // 0 - 1 Shadows: true, // true - false FXAA: false, // true - false Debug_Physics: false, // true - false Debug_FPS: false, // true - false Sun_Elevation: 50, // 0 - 180 Sun_Rotation: 150, // 0 - 360 }, worldParams ); // Initialization this.inputManager = new InputManager(this, this.renderer.domElement); this.cameraOperator = new CameraOperator( this, this.camera, this.params.Mouse_Sensitivity ); this.sky = new Sky(this); this.sky.phi = this.params.Sun_Elevation; this.sky.theta = this.params.Sun_Rotation; if (this.params.Shadows) { this.sky.csm.lights.forEach((light) => { light.castShadow = true; }); } else { this.sky.csm.lights.forEach((light) => { light.castShadow = false; }); } if (this.params.Debug_Physics) { this.cannonDebugRenderer = new CannonDebugRenderer( this.graphicsWorld, this.physicsWorld ); } if (this.params.Debug_FPS) { document.getElementById("statsBox").style.display = "block"; } // Load scene if path is supplied if (worldOptions?.world !== undefined) { let loadingManager = new LoadingManager(this); loadingManager.onFinishedCallback = () => { this.update(1, 1); this.setTimeScale(1); this.triggerEvent({ type: EventType.INITIALISE, targets: [], }); }; loadingManager.loadGLTF(worldOptions?.world, (gltf) => { this.loadScene(loadingManager, gltf); }); } else { this.triggerEvent({ type: EventType.MISSING_WORLD, targets: [], }); } this.render(this); } private onWindowResize(): void { this.camera.aspect = window.innerWidth / window.innerHeight; this.camera.updateProjectionMatrix(); this.renderer.setSize(window.innerWidth, window.innerHeight); this.fxaaPass.uniforms["resolution"].value.set( 1 / (window.innerWidth * this.pixelRatio), 1 / (window.innerHeight * this.pixelRatio) ); this.composer.setSize( window.innerWidth * this.pixelRatio, window.innerHeight * this.pixelRatio ); } // Update // Handles all logic updates. public update(timeStep: number, unscaledTimeStep: number): void { this.updatePhysics(timeStep); // Update registred objects this.updatables.forEach((entity) => { if (entity.update) { entity.update(timeStep, unscaledTimeStep); } }); // Lerp time scale this.params.Time_Scale = THREE.MathUtils.lerp( this.params.Time_Scale, this.timeScaleTarget, 0.2 ); if (this.params.Debug_Physics) this.cannonDebugRenderer.update(); } public updatePhysics(timeStep: number): void { // Step the physics world this.physicsWorld.step(this.physicsFrameTime, timeStep); this.characters.forEach((char) => { if (this.isOutOfBounds(char.characterCapsule.body.position)) { this.outOfBoundsRespawn(char.characterCapsule.body); } }); } public isOutOfBounds(position: CANNON.Vec3): boolean { const inside = position.x > -211.882 && position.x < 211.882 && position.z > -169.098 && position.z < 153.232 && position.y > 0; const belowSeaLevel = position.y < 0; const isOFB = !inside && belowSeaLevel; return isOFB; } public outOfBoundsRespawn(body: CANNON.Body, position?: CANNON.Vec3): void { let newPos = position || new CANNON.Vec3(0, 16, 0); let newQuat = new CANNON.Quaternion(0, 0, 0, 1); body.position.copy(newPos); body.interpolatedPosition.copy(newPos); body.quaternion.copy(newQuat); body.interpolatedQuaternion.copy(newQuat); body.velocity.setZero(); body.angularVelocity.setZero(); } public stopRender() { window.cancelAnimationFrame(this.animationFrame); this.animationFrame = null; } /** * Rendering loop. * Implements fps limiter and frame-skipping * Calls world's "update" function before rendering. * @param {World} world */ public render(world: World): void { this.requestDelta = this.clock.getDelta(); this.animationFrame = requestAnimationFrame(() => { world.render(world); }); // Getting timeStep let unscaledTimeStep = this.requestDelta + this.renderDelta + this.logicDelta; let timeStep = unscaledTimeStep * this.params.Time_Scale; timeStep = Math.min(timeStep, 1 / 30); // min 30 fps // Logic world.update(timeStep, unscaledTimeStep); // Measuring logic time this.logicDelta = this.clock.getDelta(); // Frame limiting let interval = 1 / 60; this.sinceLastFrame += this.requestDelta + this.renderDelta + this.logicDelta; this.sinceLastFrame %= interval; // Stats end this.stats.end(); this.stats.begin(); // Actual rendering with a FXAA ON/OFF switch if (this.params.FXAA) this.composer.render(); else this.renderer.render(this.graphicsWorld, this.camera); // Measuring render time this.renderDelta = this.clock.getDelta(); } public setTimeScale(value: number): void { this.params.Time_Scale = value; this.timeScaleTarget = value; } public add(worldEntity: IWorldEntity): void { worldEntity.addToWorld(this); this.registerUpdatable(worldEntity); } public registerUpdatable(registree: IUpdatable): void { this.updatables.push(registree); this.updatables.sort((a, b) => (a.updateOrder > b.updateOrder ? 1 : -1)); } public remove(worldEntity: IWorldEntity): void { worldEntity.removeFromWorld(this); this.unregisterUpdatable(worldEntity); } public unregisterUpdatable(registree: IUpdatable): void { _.pull(this.updatables, registree); } public triggerEvent(data: { type: IEventType; targets: string[] }) { const event = new CustomEvent(MATEMATROLII_EVENT, { detail: data }); window.document.dispatchEvent(event); } public loadScene(loadingManager: LoadingManager, gltf: any): void { gltf.scene.traverse((child) => { if (child.hasOwnProperty(ModelProps.USER_DATA)) { const userData = child[ModelProps.USER_DATA]; if (child.type === "Mesh") { Utils.setupMeshProperties(child, this.worldOptions); this.sky.csm.setupMaterial(child.material); } if (userData.hasOwnProperty(MapDataType.DATA_PROP)) { if (userData[MapDataType.DATA_PROP] === MapDataType.PHYSICS) { if (userData.hasOwnProperty(MapType.TYPE_PROP)) { // Convex doesn't work! Stick to boxes! if (userData.type === MapType.BOX) { let phys = new BoxCollider({ size: new THREE.Vector3( child.scale.x, child.scale.y, child.scale.z ), }); phys.body.position.copy(Utils.cannonVector(child.position)); phys.body.quaternion.copy(Utils.cannonQuat(child.quaternion)); phys.body.computeAABB(); phys.body.shapes.forEach((shape) => { shape.collisionFilterMask = ~CollisionGroups.TrimeshColliders; }); this.physicsWorld.addBody(phys.body); phys = undefined; } else if (userData.type === MapType.TRIMESH) { let phys = new TrimeshCollider(child, {}); this.physicsWorld.addBody(phys.body); phys.body.sleep(); phys = undefined; } } } if (userData[MapDataType.DATA_PROP] === MapDataType.SCENARIO) { this.scenarios.push(new Scenario(child, this)); } } } }); this.graphicsWorld.add(gltf.scene); // Launch default scenario let defaultScenarioID: string; for (const scenario of this.scenarios) { if (scenario.id === this.worldOptions.scenario) { defaultScenarioID = scenario.id; break; } } if (defaultScenarioID !== undefined) this.launchScenario(defaultScenarioID, loadingManager); } public launchScenario( scenarioID: string, loadingManager?: LoadingManager ): void { this.lastScenarioID = scenarioID; this.clearEntities(); // Launch default scenario if (!loadingManager) loadingManager = new LoadingManager(this); for (const scenario of this.scenarios) { if (scenario.id === scenarioID) { scenario.launch(loadingManager, this); } } } public restartScenario(): void { if (this.lastScenarioID !== undefined) { //document.exitPointerLock(); this.launchScenario(this.lastScenarioID); } else { console.warn("Can't restart scenario. Last scenarioID is undefined."); } } public clearEntities(): void { for (let i = 0; i < this.characters.length; i++) { this.remove(this.characters[i]); i--; } this.characters = []; for (let key in this.characterNames) { this.characterNames[key] = undefined; } this.characterNames = {}; for (let i = 0; i < this.items.length; i++) { this.remove(this.items[i]); i--; } this.items = []; for (let i = 0; i < this.bullets.length; i++) { this.remove(this.bullets[i]); i--; } this.bullets = []; } public destroy(): void { this.clearEntities(); window.cancelAnimationFrame(this.animationFrame); window.removeEventListener("resize", this.onWindowResize); if (this.graphicsWorld) { while (this.graphicsWorld.children.length > 0) { this.graphicsWorld.remove(this.graphicsWorld.children[0]); } this.graphicsWorld = undefined; } if (this.renderer) { this.renderer.dispose(); this.renderer.forceContextLoss(); this.renderer = undefined; } this.clock?.stop(); this.clock = undefined; this.camera?.remove(); this.camera = undefined; this.inputManager.destroy(); this.inputManager = undefined; this.physicsWorld = undefined; for (let i = 0; i < this.scenarios.length; i++) { this.scenarios[i].destroy(); } this.scenarios = undefined; this.cannonDebugRenderer = undefined; this.composer = undefined; this.stats = undefined; this.sky.destroy(); this.sky = undefined; this.parallelPairs = undefined; this.physicsFrameRate = undefined; this.physicsFrameTime = undefined; this.physicsMaxPrediction = undefined; this.renderDelta = undefined; this.logicDelta = undefined; this.requestDelta = undefined; this.sinceLastFrame = undefined; this.justRendered = undefined; this.params = undefined; this.cameraOperator = undefined; this.timeScaleTarget = undefined; this.characters = undefined; this.characterNames = undefined; this.items = undefined; this.bullets = undefined; this.updatables = undefined; this.worldOptions = undefined; this.worldParams = undefined; this.lastScenarioID = undefined; this.animationFrame = undefined; this.pixelRatio = undefined; this.fxaaPass = undefined; } public scrollTheTimeScale(scrollAmount: number): void { // Changing time scale with scroll wheel const timeScaleBottomLimit = 0.003; const timeScaleChangeSpeed = 1.3; if (scrollAmount > 0) { this.timeScaleTarget /= timeScaleChangeSpeed; if (this.timeScaleTarget < timeScaleBottomLimit) this.timeScaleTarget = 0; } else { this.timeScaleTarget *= timeScaleChangeSpeed; if (this.timeScaleTarget < timeScaleBottomLimit) this.timeScaleTarget = timeScaleBottomLimit; this.timeScaleTarget = Math.min(this.timeScaleTarget, 1); } } }