import { StringEncoder, Callbacks, ObjectProxy } from '@uino/base-thing';
import { Utils } from '../common/Utils';
import { Object3D } from '../objects/Object3D';
import { RootObject } from '../objects/RootObject';
import { AmbientLight } from '../objects/AmbientLight';
import { DirectionalLight } from '../objects/DirectionalLight';
import { ImageTexture } from '../resources/ImageTexture';
import { ImageMappingType } from '../const';
import { CubeTexture } from '../resources/CubeTexture';
const __ = {
private: Symbol('private'),
}
const _renderOverlayFuncName = StringEncoder.toText("<@secret renderOverlay>");
const _ClearColorBuffer = StringEncoder.toText("<@secret ClearColorBuffer>");
/**
* @class Scene
* The scene object.
* @memberof THING
*/
class Scene {
/**
* The scene of object(s).
* @param {Object} param The initial parameters.
*/
constructor(param = {}) {
this[__.private] = {};
let _private = this[__.private];
_private.app = Utils.getCurrentApp();
_private.visible = true;
// Root objects
_private.rootObjects = {};
_private.scene = null;
// Lights
_private.ambientLight = null;
_private.mainLight = null;
// Environment
_private.envMap = null;
// Render options
_private.instancedOffset = null;
_private.renderOptions = null;
// Camera
_private.curCameraIndex = -1; // The current logic camera index
_private.cameras = []; // [0]: main render camera
// Callbacks
_private.beforeRenderCallbacks = new Callbacks();
_private.afterRenderCallbacks = new Callbacks();
this._initScene(param);
this._initLight(param);
this._initRenderOptions();
this._initEnvMap(param);
// Enable static mode as default
this.staticMode = true;
}
// #region Private
_onBeforeRender(deltaTime, camera) {
let _private = this[__.private];
_private.beforeRenderCallbacks.invoke(deltaTime, camera);
_private.app.delegate.onBeforeRender(camera);
_private.scene.beforeRender(deltaTime);
}
_onAfterRender(deltaTime, camera) {
let _private = this[__.private];
_private.scene.afterRender(deltaTime);
_private.app.delegate.onAfterRender(camera);
_private.afterRenderCallbacks.invoke(deltaTime, camera);
}
_initScene(param) {
let _private = this[__.private];
// Create scene
_private.scene = param['scene'];
// The scene root object
_private.rootObjects['scene'] = new RootObject({
name: 'scene-root',
loaded: true,
queryable: false
});
// Create logical root nodes
let rootNodes = {
'attachedPoint': Utils.createObject('Node'),
'debug': Utils.createObject('Node'),
'bodyBridge': Utils.createObject('Node')
};
// Create other logical root objects
for (let key in rootNodes) {
let rootName = key + '-root';
let rootObject = new Object3D({
rootNode: rootNodes[key],
name: rootName,
queryable: false,
loaded: true,
castShadow: false,
receiveShadow: false
});
_private.rootObjects[key] = rootObject;
}
// Bind root objects
let root = _private.scene.getRoot();
for (let key in _private.rootObjects) {
root.add(_private.rootObjects[key].node);
}
// Get overlay root node
let getOverlayRootFunc = StringEncoder.toText("<@secret getOverlayRoot>");
let overlayRoot = _private.scene[getOverlayRootFunc]();
overlayRoot.setVisible(true);
// Create overlay root node
_private.overlayRootNode = Utils.createObject('Node');
_private.overlayRootNode.setName(StringEncoder.toText("<@secret OverlayRoot>"));
overlayRoot.add(_private.overlayRootNode);
// Create overlay logo root node
_private.overlayLogoRootNode = Utils.createObject('Node');
_private.overlayLogoRootNode.setName(StringEncoder.toText("<@secret OverlayLogoRoot>"));
overlayRoot.add(_private.overlayLogoRootNode);
// Update accessors for internal usage
_private.accessors = {};
_private.accessors[StringEncoder.toText("<@secret overlayRootNode>")] = _private.overlayRootNode;
_private.accessors[StringEncoder.toText("<@secret overlayLogoRootNode>")] = _private.overlayLogoRootNode;
}
_initLight(param) {
let initLights = Utils.parseValue(param['initLights'], true);
if (!initLights) {
return;
}
let _private = this[__.private];
let root = _private.rootObjects['scene'];
// Create ambient light
let ambientLight = new AmbientLight({
parent: root,
name: 'mainAmbientLight',
});
// Create main light
let mainLight = new DirectionalLight({
parent: root,
name: 'mainDirectionalLight',
target: [0, 0, 0]
});
// Initialize adapater for main light as default
let adapter = mainLight.adapter;
if (_private.app.view._compatibleRender) {
adapter.horzAngle = 30;
}
else {
adapter.horzAngle = 40;
}
adapter.vertAngle = 30;
adapter.bind(root);
// Create lights finished
this.ambientLight = ambientLight;
this.mainLight = mainLight;
}
_initRenderOptions() {
let _private = this[__.private];
_private.renderOptions = new ObjectProxy({
data: {
environment: {
quaternion: null
},
},
onChange: function (ev) {
if (_private.scene) {
_private.scene.setRenderOptions(ev.data);
}
}
});
}
_initEnvMap(param) {
const envMap = Utils.parseValue(param['envMap'], null);
let _private = this[__.private];
const app = _private.app;
if (Utils.isArray(envMap)) {
const imageTexture = new CubeTexture(envMap);
this.envMap = imageTexture;
}
else if (Utils.isString(envMap)) {
if ((envMap.indexOf('.png') > -1 || envMap.indexOf('.jpg') > -1)) {
const imageTexture = new ImageTexture({
url: envMap,
mappingType: ImageMappingType.EquirectangularReflection
});
this.envMap = imageTexture;
}
else {
const urls = Utils.parseCubeTextureUrlsByPath(envMap);
const imageTexture = new ImageTexture(urls);
this.envMap = imageTexture;
}
}
else if (param.defaultSettings.envMap) {
app.view.getDefaultEnvImage().then((envMap) => {
if (envMap) {
this.envMap = new CubeTexture({ data: envMap });
}
});
}
}
// #endregion
dispose() {
let _private = this[__.private];
if (_private.overlayRootObject) {
_private.overlayRootObject.destroy();
_private.overlayRootObject = null;
}
_private.overlayRootNode.dispose();
_private.overlayLogoRootNode.dispose();
_private.overlayRootNode = null;
_private.overlayLogoRootNode = null;
_private.accessors = null;
_private.app.objectManager.clearKeepAliveObjects();
for (let key in _private.rootObjects) {
_private.rootObjects[key].destroy(true);
}
_private.rootObjects = {};
_private.renderOptions.dispose();
_private.renderOptions = null;
_private.scene.dispose();
_private.scene = null;
_private.ambientLight = null;
_private.mainLight = null;
_private.envMap = null;
_private.curCameraIndex = -1;
_private.cameras = [];
_private.beforeRenderCallbacks.clear();
_private.afterRenderCallbacks.clear();
}
resize(width, height) {
let _private = this[__.private];
_private.cameras.forEach(camera => {
camera.resize(width, height);
});
if (_private.overlayRootObject) {
_private.overlayRootObject.width = width;
_private.overlayRootObject.height = height;
}
}
render(deltaTime) {
let _private = this[__.private];
let cameras = _private.cameras;
let mainCamera = cameras[0];
if (!mainCamera) {
return;
}
let mainCameraNode = mainCamera.node;
// Prepare for rendering
this._onBeforeRender(deltaTime, mainCameraNode);
if (_private.visible) {
// Render main camera
mainCameraNode.render();
// Render scene with other cameras
for (let i = 1, l = cameras.length; i < l; i++) {
let camera = cameras[i];
if (!camera.visible) {
continue;
}
// Render to texture
if (camera.renderTexture) {
camera.node.render(camera.renderTexture.getTextureResource());
}
// Render to viewport
else if (camera.enableViewport && camera.viewport) {
camera.node.render();
}
}
// Render overlay
if (_private.overlayRootNode && _private.overlayRootNode.getVisible()) {
// We can not clear color buffer due to context use the same color buffer ...
let clearColorBuffer = mainCameraNode.getAttribute(_ClearColorBuffer);
mainCameraNode.setAttribute(_ClearColorBuffer, false);
mainCameraNode[_renderOverlayFuncName]();
mainCameraNode.setAttribute(_ClearColorBuffer, clearColorBuffer);
}
}
// Finished for rendering
this._onAfterRender(deltaTime, mainCameraNode);
}
addRenderCamera(camera) {
let _private = this[__.private];
let cameras = _private.cameras;
// For other render camera, we are not going to clear color buffer as default
let cameraNode = camera.node;
cameraNode.setAttribute(_ClearColorBuffer, false);
let mainCamera = cameras[_private.curCameraIndex];
Utils.pushToArray(cameras, camera);
_private.curCameraIndex = cameras.indexOf(mainCamera);
}
removeRenderCamera(camera) {
let _private = this[__.private];
let cameras = _private.cameras;
let mainCamera = cameras[_private.curCameraIndex];
Utils.removeFromArray(cameras, camera);
_private.curCameraIndex = cameras.indexOf(mainCamera);
}
/**
* The function to call when render.
* @callback OnRenderCallback
* @param {Number} deltaTime The delta time in seconds.
* @param {Object} camera The camera node.
* @private
*/
/**
* Add callback before render.
* @param {OnRenderCallback} callback The function.
* @private
*/
addBeforeRenderCallback(callback) {
this[__.private].beforeRenderCallbacks.add(callback);
}
/**
* Remove callback before render.
* @param {OnRenderCallback} callback The function.
* @private
*/
removeBeforeRenderCallback(callback) {
this[__.private].beforeRenderCallbacks.remove(callback);
}
/**
* Add callback after render.
* @param {OnRenderCallback} callback The function.
* @private
*/
addAfterRenderCallback(callback) {
this[__.private].afterRenderCallbacks.add(callback);
}
/**
* Remove callback after render.
* @param {OnRenderCallback} callback The function.
* @private
*/
removeAfterRenderCallback(callback) {
this[__.private].afterRenderCallbacks.remove(callback);
}
/**
* Get attribute by type.
* @param {String} type The type string.
* @returns {*}
* @private
*/
getAttribute(type) {
const overlayRootObject = StringEncoder.toText("<@secret OverlayRootObject>");
const accessors = StringEncoder.toText("<@secret Accessors>");
let _private = this[__.private];
switch (type) {
case overlayRootObject:
if (!_private.overlayRootObject) {
let app = _private.app;
_private.overlayRootObject = new Object3D({
name: StringEncoder.toText("<@secret overlayRoot>"),
rootNode: _private.overlayRootNode,
parent: null,
queryable: false,
});
_private.overlayRootObject.x = 0;
_private.overlayRootObject.y = 0;
_private.overlayRootObject.width = app.size[0];
_private.overlayRootObject.height = app.size[1];
}
return _private.overlayRootObject;
case accessors:
return _private.accessors;
default:
return null;
}
}
// #region Accessor
/**
* Get root.
* @type {Object}
* @private
*/
get root() {
return this[__.private].scene.getRoot();
}
/**
* Get root objects.
* @type {Object}
* @private
*/
get rootObjects() {
return this[__.private].rootObjects;
}
/**
* Get cameras.
* @type {Array<THING.Camera>}
* @private
*/
get cameras() {
return this[__.private].cameras;
}
/**
* Get/Set the logic camera.
* @type {THING.Camera}
* @private
*/
get camera() {
let _private = this[__.private];
return _private.cameras[_private.curCameraIndex];
}
set camera(value) {
let _private = this[__.private];
let index = _private.cameras.indexOf(value);
if (index === -1) {
_private.cameras.push(value);
index = _private.cameras.length - 1;
}
_private.curCameraIndex = index;
// Use camera as render camera
this.renderCamera = value;
// Keep it alive
_private.app.objectManager.addKeepAliveObject(value);
}
/**
* Get/Set the main render camera.
* @type {THING.Camera}
* @private
*/
get renderCamera() {
let _private = this[__.private];
return _private.cameras[0];
}
set renderCamera(value) {
let _private = this[__.private];
let cameras = _private.cameras;
let mainCamera = cameras[_private.curCameraIndex];
// Disable previous main camera render to viewport
let mainRenderCamera = cameras[0];
if (mainRenderCamera && mainRenderCamera != value) {
mainRenderCamera.enableViewport = false;
}
let index = cameras.indexOf(value);
if (index === -1) {
cameras._insert(0, value);
}
else if (index !== 0) {
cameras._swap(0, index);
}
_private.curCameraIndex = cameras.indexOf(mainCamera);
// Enable render to viewport
value.enableViewport = true;
}
/**
* Get/Set the instanced offset(It would effect relative position of all instanced drawing objects).
* @type {Array<Number>}
* @example
* app.scene.instancedOffset = [10000, 10000, 10000];
* @private
*/
get instancedOffset() {
let _private = this[__.private];
return _private.instancedOffset;
}
set instancedOffset(value) {
let _private = this[__.private];
if (value) {
_private.instancedOffset = value.slice(0);
this.root.setInstancedOffset(_private.instancedOffset);
}
else {
_private.instancedOffset = null;
this.root.setInstancedOffset([0, 0, 0]);
}
}
/**
* Enable/Disable static mode to improve matrix calculation.
* We should disable it when loading resources, then enable it when rendering in preview mode.
* @type {Boolean}
* @private
*/
get staticMode() {
let _private = this[__.private];
return _private.scene.isEnabled('TransformCache');
}
set staticMode(value) {
let _private = this[__.private];
_private.scene.enable('TransformCache', value);
}
/**
* Get/Set environment map resource.
* @type {THING.CubeTexture|THING.ImageTexture}
* @private
*/
get envMap() {
let _private = this[__.private];
return _private.envMap;
}
/**
* Set environment map resource.
* @type {THING.CubeTexture|String}
* @private
*/
set envMap(value) {
let _private = this[__.private];
let scene = _private.scene;
if (_private.envMap) {
_private.envMap.release();
}
if (value) {
if (value.isImageTexture || value.isCubeTexture) {
_private.envMap = value;
_private.envMap.addRef();
_private.envMap.waitForComplete().then((image) => {
scene.setEnvironment(image.getTextureResource());
});
}
// Image url of String type
else if (Utils.isString(value)) {
_private.envMap = new ImageTexture(value);
_private.envMap.mappingType = ImageMappingType.EquirectangularReflection;
_private.envMap.waitForComplete().then((image) => {
scene.setEnvironment(image.getTextureResource());
});
}
else {
_private.envMap = null;
scene.setEnvironment(null);
}
}
else {
_private.envMap = null;
scene.setEnvironment(null);
}
}
/**
* Get/Set the environment map light intensity between 0 and 1.
* @type {Number}
* @public
* @example
* app.scene.envMapLightIntensity = 0.5;
* // @expect(app.scene.envMapLightIntensity == 0.5);
*/
get envMapLightIntensity() {
let _private = this[__.private];
return _private.scene.getEnvironmentLightIntensity();
}
set envMapLightIntensity(value) {
let _private = this[__.private];
_private.scene.setEnvironmentLightIntensity(value);
}
/**
* Get/Set the ambient light.
* @type {THING.AmbientLight}
* @example
* let app = THING.App.current;
* app.scene.ambientLight.color = 'blue';
* app.scene.ambientLight.intensity = 0.1;
* let ret1 = app.scene.ambientLight.color == 'blue';
* let ret2 = app.scene.ambientLight.intensity == 0.1;
* // @expect(ret1 == true && ret2 == true);
*/
get ambientLight() {
return this[__.private].ambientLight;
}
set ambientLight(value) {
let _private = this[__.private];
_private.ambientLight = value;
// Keep it alive to prevent destroy action
_private.app.objectManager.addKeepAliveObject(value);
}
/**
* Get/Set the main light.
* @type {THING.DirectionalLight}
* @private
*/
get mainLight() {
return this[__.private].mainLight;
}
set mainLight(value) {
let _private = this[__.private];
_private.mainLight = value;
// Keep it alive to prevent destroy action
_private.app.objectManager.addKeepAliveObject(value);
}
/**
* @typedef {Object} SceneRenderEnvironmentOptions
* @property {Array<Number>} quaternion The quaternion.
* @private
*/
/**
* @typedef {Object} SceneRenderOptions
* @property {SceneRenderEnvironmentOptions} environment The environment.
* @private
*/
/**
* Get/Set render options.
* @type {SceneRenderOptions}
* @private
*/
get renderOptions() {
let _private = this[__.private];
return _private.renderOptions.dataProxy;
}
set renderOptions(value) {
let _private = this[__.private];
let scene = _private.scene;
let dataProxy = _private.renderOptions.dataProxy;
if (value) {
Utils.mergeObject(dataProxy, value, true);
scene.setRenderOptions(dataProxy);
}
else {
dataProxy.environment.quaternion = null;
scene.setRenderOptions(dataProxy);
}
}
/**
* Get the current render state.
* @type {RenderStateResult}
* @private
*/
get renderState() {
let _private = this[__.private];
return _private.scene.getRenderState();
}
/**
* Get/Set the output enconding type.
* @type {TexelEncodingType}
*/
get outputEncodingType() {
let _private = this[__.private];
return _private.scene.getOutputEncodingType();
}
set outputEncodingType(value) {
let _private = this[__.private];
_private.scene.setOutputEncodingType(value);
}
get visible() {
let _private = this[__.private];
return _private.visible;
}
set visible(value) {
let _private = this[__.private];
_private.visible = value;
}
// #endregion
get isScene() {
return true;
}
}
export { Scene }