v3d-core
Version:
WebGL-based humanoid model rendering engine.
369 lines • 13.7 kB
JavaScript
/** 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