UNPKG

v3d-core

Version:

WebGL-based humanoid model rendering engine.

369 lines 13.7 kB
/** Copyright (c) 2021 The v3d Authors. All rights reserved. * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ import { ArcRotateCamera } from "@babylonjs/core/Cameras/arcRotateCamera"; import { Scene } from "@babylonjs/core/scene"; import { SceneLoader } from "@babylonjs/core/Loading/sceneLoader"; import { Color3, Color4, Vector3 } from "@babylonjs/core/Maths/math"; import { VRMFileLoader, VRMLoaderExtension } from "./importer/babylon-vrm-loader/src"; import { GLTFLoader } from "@babylonjs/loaders/glTF/2.0"; import { HemisphericLight } from "@babylonjs/core/Lights/hemisphericLight"; import { Animation, DefaultRenderingPipeline, ShadowGenerator, DepthOfFieldEffectBlurLevel } from "@babylonjs/core"; import { getAnimationDataType, isIShadowLight } from "./utilities/types"; import { V3DSceneOptimizer } from "./scene/optimizer"; import { v3DSkyBox } from "./scene/skybox"; export class V3DCore { constructor(engine, scene, camera) { this.engine = engine; this.scene = scene; /** * GLTFFileLoader plugin factory * @private */ this._vrmFileLoader = new VRMFileLoader(); // Whether starts spring bones animation automatically this._springBonesAutoUpdate = true; /** * Shadow generators */ this._shadowGenerators = new Map(); /** * Callbacks when loading is done */ this._onLoadCompleteCallbacks = []; this._beforeRenderFunc = () => { }; this._afterRenderFunc = () => { for (const manager of this.loadedVRMManagers) { if (this._springBonesAutoUpdate) manager.update(this.engine.getDeltaTime()); } }; this._cameraOnBeforeRenderFunc = []; this.skyBox = null; /** * Loaded VRM Managers * @private */ this.loadedVRMManagers = []; // Register this.registerVrmPlugin(); this.registerVrmExtension(); if (!this.scene) this.scene = new Scene(this.engine); else this.engine = this.scene.getEngine(); this.setupObservable(); this.enableResize(); if (camera) { this._mainCamera = camera; this.scene.switchActiveCamera(camera); } else this.addCamera(); this._renderingPipeline = new DefaultRenderingPipeline("defaultPipeline", // The name of the pipeline true, // Do you want the pipeline to use HDR texture? this.scene, // The scene instance [this._mainCamera] // The list of cameras to be attached to ); this.setupRenderingPipeline(); } get springBonesAutoUpdate() { return this._springBonesAutoUpdate; } set springBonesAutoUpdate(value) { this._springBonesAutoUpdate = value; } get renderingPipeline() { return this._renderingPipeline; } addOnLoadCompleteCallbacks(callback) { this._onLoadCompleteCallbacks.push(callback); } removeOnLoadCompleteCallback(callback) { const idx = this._onLoadCompleteCallbacks.indexOf(callback); if (idx !== -1) { this._onLoadCompleteCallbacks.splice(idx, 1); } } resetOnLoadCompleteCallbacks() { this._onLoadCompleteCallbacks = []; } updateBeforeRenderFunction(func) { this._beforeRenderFunc = func; } updateAfterRenderFunction(func) { this._afterRenderFunc = func; } get mainCamera() { return this._mainCamera; } set mainCamera(value) { this._mainCamera = value; } addVRMManager(manager) { if (manager) this.loadedVRMManagers.push(manager); } /** * Get VRM Manager by index * @param idx */ getVRMManagerByIndex(idx) { return (idx >= 0 && idx < this.loadedVRMManagers.length) ? this.loadedVRMManagers[idx] : null; } /** * Get VRM Manager by URI * VRM doesn't have any UID in metadata. Title can be unfilled too. * Filename is the only reasonable ID. * @param uri */ // VRM doesn't have any UID in metadata. Title can be unfilled too. // Filename is the only reasonable ID. getVRMManagerByURI(uri) { for (const manager of this.loadedVRMManagers) { if (manager.uri === uri) return manager; } return null; } /** * Make background transparent. */ transparentBackground() { this.scene.clearColor.a = 0; } /** * Make background solid. */ solidBackground() { this.scene.clearColor.a = 1; } /** * Change background color. * @param color */ setBackgroundColor(color) { this.scene.clearColor = Color4.FromColor3(color, this.scene.clearColor.a).toLinearSpace(); } /** * Set background color from hex string. * @param hex Hex color string */ setBackgroundColorHex(hex) { this.setBackgroundColor(Color3.FromHexString(hex)); } /** * Add an ambient light. * @param color color of the light */ addAmbientLight(color) { const light = new HemisphericLight("V3DHemiLight", new Vector3(0, 1, 1), this.scene); if (color) light.diffuse = color; light.setEnabled(true); } /** * Add a basic arc rotate camera to scene. * TODO: there seems to be a bug when meshes are near the edge of camera cone * Probably has something to do with culling * @param radius rotation radius */ addCamera(radius = 3) { const camera = new ArcRotateCamera('V3DMainCamera', 0, 0, radius, new Vector3(0, 0, 0), this.scene, true); camera.lowerRadiusLimit = 0.1; camera.upperRadiusLimit = 20; camera.wheelDeltaPercentage = 0.05; camera.minZ = 0; camera.setPosition(new Vector3(0, 1.5, -5)); camera.attachControl(this.engine.getRenderingCanvas()); this._mainCamera = camera; this.scene.switchActiveCamera(this._mainCamera, true); } /** * Attach a arc rotate following camera to VRM model. * Probably has something to do with culling * @param manager VRM Manager to attach the camera to * @param radius rotation radius */ attachCameraTo(manager, radius = 3) { const camera = new ArcRotateCamera('V3DArcCamera' + manager.cameras.length, 0, 0, radius, manager.rootMesh.position, this.scene, true); camera.lowerRadiusLimit = 0.1; camera.upperRadiusLimit = 20; camera.wheelDeltaPercentage = 0.05; camera.minZ = 0; camera.setPosition(new Vector3(0, 1.5, -5)); camera.setTarget(manager.rootMesh.getAbsolutePosition()); camera.attachControl(this.engine.getRenderingCanvas()); manager.appendCamera(camera); this._cameraOnBeforeRenderFunc.push(() => { camera.setTarget(manager.rootMesh.getAbsolutePosition()); }); } /** * * Create a skybox for the scene. * @param size size of the skybox * @param textureName path to skybox texture */ createSkyBox(size, textureName) { if (!this.skyBox) { this.skyBox = new v3DSkyBox(this.scene, textureName ? textureName : "texture/skybox", size); } } /** * Enable shadow caster for light. * @param light Light to enable shadows. */ enableShabows(light) { if (light) { if (!this._shadowGenerators.has(light)) { const shadowGenerator = new ShadowGenerator(1024, light); this.setupShadowGenerator(shadowGenerator); this._shadowGenerators.set(light, shadowGenerator); } else { console.warn("Light " + light.name + " already has a shadow generator!"); } } else { for (const l of this.scene.lights) { if (isIShadowLight(l)) { const shadowGenerator = new ShadowGenerator(1024, l); this.setupShadowGenerator(shadowGenerator); this._shadowGenerators.set(l, shadowGenerator); } } } } /** * Get corresponding shadow generator for light. * @param light Light to get shadow generator */ getShadownGenerator(light) { return this._shadowGenerators.get(light); } /** * Convenience function for starting animation * @param target * @param name * @param property * @param duration * @param from * @param to * @param loopMode * @param easingFunction * @param easingMode */ startQuickAnimation(target, name, property, duration, from, to, loopMode, easingFunction, easingMode) { const anim = this.createAnimation(target, name, property, [{ frame: 0, value: from }, { frame: duration, value: to }], loopMode, easingFunction, easingMode); return this.scene.beginDirectAnimation(anim[0], [anim[1]], 0, duration, false); } /** * Convenience function for creating animation * @param target * @param name * @param property * @param keyFrames * @param loopMode * @param easingFunction * @param easingMode */ createAnimation(target, name, property, keyFrames, loopMode, easingFunction, easingMode) { // Make sure keyFrames is not empty if (keyFrames.length < 1) throw Error("Key Frames empty"); // Get data type const dataType = getAnimationDataType(keyFrames[0].value); if (dataType === null) throw Error("Cannot determine data type from keyframes!"); const animation = new Animation(name, property, V3DCore.FRAMERATE, dataType, loopMode); animation.setKeys(keyFrames); if (easingFunction) { if (easingMode) easingFunction.setEasingMode(easingMode); animation.setEasingFunction(easingFunction); } return [target, animation]; } enableOptimizer(options) { this._sceneOptimizer = new V3DSceneOptimizer(this.scene, options); } // Don't make wrappers static, so plugins will always be registered /** * Wrapper for SceneLoader.AppendAsync. * @param rootUrl a string that defines the root url for the scene and resources or the concatenation of rootURL and filename * @param sceneFilename a string that defines the name of the scene file or starts with "data:" following by the stringified version of the scene or a File object (default: empty string) */ AppendAsync(rootUrl, sceneFilename) { return SceneLoader.AppendAsync(rootUrl, sceneFilename, this.scene); } /** * Wrapper for SceneLoader.LoadAsync * @param rootUrl a string that defines the root url for the scene and resources or the concatenation of rootURL and filename * @param sceneFilename a string that defines the name of the scene file or starts with "data:" following by the stringified version of the scene or a File object (default: empty string) */ LoadAsync(rootUrl, sceneFilename) { return SceneLoader.LoadAsync(rootUrl, sceneFilename, this.engine); } // GLTFLoaderExtensionObserver onLoadReady() { for (const f of this._onLoadCompleteCallbacks) { f(); } } /** * Set up for time update. * @private */ setupObservable() { this.scene.onBeforeRenderObservable.add((eventData, eventState) => { this._beforeRenderFunc(eventData, eventState); }); // Camera this.scene.onBeforeRenderObservable.add(() => { for (const f of this._cameraOnBeforeRenderFunc) f(); }); // Update secondary animation this.scene.onAfterRenderObservable.add((eventData, eventState) => { this._afterRenderFunc(eventData, eventState); }); } enableResize() { this.engine.getRenderingCanvas().onresize = () => { this.engine.resize(); }; } setupShadowGenerator(shadowGenerator) { shadowGenerator.usePercentageCloserFiltering = true; shadowGenerator.filteringQuality = ShadowGenerator.QUALITY_HIGH; } // TODO Unregister registerVrmExtension() { // ローダーに登録する GLTFLoader.RegisterExtension(VRMLoaderExtension.NAME, (loader) => { return new VRMLoaderExtension(loader, this); }); } registerVrmPlugin() { if (SceneLoader && SceneLoader.GetPluginForExtension('.vrm').name === 'babylon.js') { SceneLoader.RegisterPlugin(this._vrmFileLoader); } } ; setupRenderingPipeline() { this._renderingPipeline.samples = 4; this._renderingPipeline.depthOfFieldEnabled = true; this._renderingPipeline.depthOfFieldBlurLevel = DepthOfFieldEffectBlurLevel.Medium; this._renderingPipeline.depthOfField.focusDistance = 2000; // distance of the current focus point from the camera in millimeters considering 1 scene unit is 1 meter this._renderingPipeline.depthOfField.focalLength = 10; // focal length of the camera in millimeters this._renderingPipeline.depthOfField.fStop = 1.4; // aka F number of the camera defined in stops as it would be on a physical device } } V3DCore.FRAMERATE = 60; export default V3DCore; //# sourceMappingURL=v3d-core.js.map