src/viewer/scene/core.js
import {Queue} from './utils/Queue.js';
import {Map} from './utils/Map.js';
import {stats} from './stats.js';
import {utils} from './utils.js';
const scenesRenderInfo = {}; // Used for throttling FPS for each Scene
const sceneIDMap = new Map(); // Ensures unique scene IDs
const taskQueue = new Queue(); // Task queue, which is pumped on each frame; tasks are pushed to it with calls to xeokit.schedule
const tickEvent = {sceneId: null, time: null, startTime: null, prevTime: null, deltaTime: null};
const taskBudget = 10; // Millisecs we're allowed to spend on tasks in each frame
const fpsSamples = [];
const numFPSSamples = 30;
let lastTime = 0;
let elapsedTime;
let totalFPS = 0;
/**
* @private
*/
function Core() {
/**
Semantic version number. The value for this is set by an expression that's concatenated to
the end of the built binary by the xeokit build script.
@property version
@namespace xeokit
@type {String}
*/
this.version = "1.0.0";
/**
Existing {@link Scene}s , mapped to their IDs
@property scenes
@namespace xeokit
@type {Scene}
*/
this.scenes = {};
this._superTypes = {}; // For each component type, a list of its supertypes, ordered upwards in the hierarchy.
/**
* Registers a scene on xeokit.
* This is called within the xeokit.Scene constructor.
* @private
*/
this._addScene = function (scene) {
if (scene.id) { // User-supplied ID
if (core.scenes[scene.id]) {
console.error(`[ERROR] Scene ${utils.inQuotes(scene.id)} already exists`);
return;
}
} else { // Auto-generated ID
scene.id = sceneIDMap.addItem({});
}
core.scenes[scene.id] = scene;
const ticksPerOcclusionTest = scene.ticksPerOcclusionTest;
const ticksPerRender = scene.ticksPerRender;
scenesRenderInfo[scene.id] = {
ticksPerOcclusionTest: ticksPerOcclusionTest,
ticksPerRender: ticksPerRender,
renderCountdown: ticksPerRender
};
stats.components.scenes++;
scene.once("destroyed", () => { // Unregister destroyed scenes
sceneIDMap.removeItem(scene.id);
delete core.scenes[scene.id];
delete scenesRenderInfo[scene.id];
stats.components.scenes--;
});
};
/**
* @private
*/
this.clear = function () {
let scene;
for (const id in core.scenes) {
if (core.scenes.hasOwnProperty(id)) {
scene = core.scenes[id];
// Only clear the default Scene
// but destroy all the others
if (id === "default.scene") {
scene.clear();
} else {
scene.destroy();
delete core.scenes[scene.id];
}
}
}
};
/**
* Schedule a task to run at the next frame.
*
* Internally, this pushes the task to a FIFO queue. Within each frame interval, xeokit processes the queue
* for a certain period of time, popping tasks and running them. After each frame interval, tasks that did not
* get a chance to run during the task are left in the queue to be run next time.
*
* @param {Function} callback Callback that runs the task.
* @param {Object} [scope] Scope for the callback.
*/
this.scheduleTask = function (callback, scope = null) {
taskQueue.push(callback);
taskQueue.push(scope);
};
this.runTasks = function (until = -1) { // Pops and processes tasks in the queue, until the given number of milliseconds has elapsed.
let time = (new Date()).getTime();
let callback;
let scope;
let tasksRun = 0;
while (taskQueue.length > 0 && (until < 0 || time < until)) {
callback = taskQueue.shift();
scope = taskQueue.shift();
if (scope) {
callback.call(scope);
} else {
callback();
}
time = (new Date()).getTime();
tasksRun++;
}
return tasksRun;
};
this.getNumTasks = function () {
return taskQueue.length;
};
}
/**
* @private
* @type {Core}
*/
const core = new Core();
const frame = function () {
let time = Date.now();
elapsedTime = time - lastTime;
if (lastTime > 0 && elapsedTime > 0) { // Log FPS stats
var newFPS = 1000 / elapsedTime; // Moving average of FPS
totalFPS += newFPS;
fpsSamples.push(newFPS);
if (fpsSamples.length >= numFPSSamples) {
totalFPS -= fpsSamples.shift();
}
stats.frame.fps = Math.round(totalFPS / fpsSamples.length);
}
for (let id in core.scenes) {
core.scenes[id].compile();
}
runTasks(time);
lastTime = time;
};
function customSetInterval(callback, interval) {
let expected = Date.now() + interval;
function loop() {
const elapsed = Date.now() - expected;
callback();
expected += interval;
setTimeout(loop, Math.max(0, interval - elapsed));
}
loop();
return {
cancel: function() {
// No need to do anything, setTimeout cannot be directly cancelled
}
};
}
customSetInterval(() => {
frame();
}, 100);
const renderFrame = function () {
let time = Date.now();
elapsedTime = time - lastTime;
if (lastTime > 0 && elapsedTime > 0) { // Log FPS stats
var newFPS = 1000 / elapsedTime; // Moving average of FPS
totalFPS += newFPS;
fpsSamples.push(newFPS);
if (fpsSamples.length >= numFPSSamples) {
totalFPS -= fpsSamples.shift();
}
stats.frame.fps = Math.round(totalFPS / fpsSamples.length);
}
runTasks(time);
fireTickEvents(time);
renderScenes();
(window.requestPostAnimationFrame !== undefined) ? window.requestPostAnimationFrame(frame) : requestAnimationFrame(renderFrame);
};
renderFrame();
function runTasks(time) { // Process as many enqueued tasks as we can within the per-frame task budget
const tasksRun = core.runTasks(time + taskBudget);
const tasksScheduled = core.getNumTasks();
stats.frame.tasksRun = tasksRun;
stats.frame.tasksScheduled = tasksScheduled;
stats.frame.tasksBudget = taskBudget;
}
function fireTickEvents(time) { // Fire tick event on each Scene
tickEvent.time = time;
for (var id in core.scenes) {
if (core.scenes.hasOwnProperty(id)) {
var scene = core.scenes[id];
tickEvent.sceneId = id;
tickEvent.startTime = scene.startTime;
tickEvent.deltaTime = tickEvent.prevTime != null ? tickEvent.time - tickEvent.prevTime : 0;
/**
* Fired on each game loop iteration.
*
* @event tick
* @param {String} sceneID The ID of this Scene.
* @param {Number} startTime The time in seconds since 1970 that this Scene was instantiated.
* @param {Number} time The time in seconds since 1970 of this "tick" event.
* @param {Number} prevTime The time of the previous "tick" event from this Scene.
* @param {Number} deltaTime The time in seconds since the previous "tick" event from this Scene.
*/
scene.fire("tick", tickEvent, true);
}
}
tickEvent.prevTime = time;
}
function renderScenes() {
const scenes = core.scenes;
const forceRender = false;
let scene;
let renderInfo;
let ticksPerOcclusionTest;
let ticksPerRender;
let id;
for (id in scenes) {
if (scenes.hasOwnProperty(id)) {
scene = scenes[id];
renderInfo = scenesRenderInfo[id];
if (!renderInfo) {
renderInfo = scenesRenderInfo[id] = {}; // FIXME
}
ticksPerOcclusionTest = scene.ticksPerOcclusionTest;
if (renderInfo.ticksPerOcclusionTest !== ticksPerOcclusionTest) {
renderInfo.ticksPerOcclusionTest = ticksPerOcclusionTest;
renderInfo.renderCountdown = ticksPerOcclusionTest;
}
if (--scene.occlusionTestCountdown <= 0) {
scene.doOcclusionTest();
scene.occlusionTestCountdown = ticksPerOcclusionTest;
}
ticksPerRender = scene.ticksPerRender;
if (renderInfo.ticksPerRender !== ticksPerRender) {
renderInfo.ticksPerRender = ticksPerRender;
renderInfo.renderCountdown = ticksPerRender;
}
if (--renderInfo.renderCountdown === 0) {
scene.render(forceRender);
renderInfo.renderCountdown = ticksPerRender;
}
}
}
}
export {core};