558 lines
17 KiB
JavaScript
558 lines
17 KiB
JavaScript
|
|
import Node from './Node';
|
||
|
|
import Light from './Light';
|
||
|
|
import Camera from './Camera';
|
||
|
|
import BoundingBox from './math/BoundingBox';
|
||
|
|
import util from './core/util';
|
||
|
|
import mat4 from './glmatrix/mat4';
|
||
|
|
import LRUCache from './core/LRU';
|
||
|
|
import Matrix4 from './math/Matrix4';
|
||
|
|
|
||
|
|
var IDENTITY = mat4.create();
|
||
|
|
var WORLDVIEW = mat4.create();
|
||
|
|
|
||
|
|
var programKeyCache = {};
|
||
|
|
|
||
|
|
function getProgramKey(lightNumbers) {
|
||
|
|
var defineStr = [];
|
||
|
|
var lightTypes = Object.keys(lightNumbers);
|
||
|
|
lightTypes.sort();
|
||
|
|
for (var i = 0; i < lightTypes.length; i++) {
|
||
|
|
var lightType = lightTypes[i];
|
||
|
|
defineStr.push(lightType + ' ' + lightNumbers[lightType]);
|
||
|
|
}
|
||
|
|
var key = defineStr.join('\n');
|
||
|
|
|
||
|
|
if (programKeyCache[key]) {
|
||
|
|
return programKeyCache[key];
|
||
|
|
}
|
||
|
|
|
||
|
|
var id = util.genGUID();
|
||
|
|
programKeyCache[key] = id;
|
||
|
|
return id;
|
||
|
|
}
|
||
|
|
|
||
|
|
function RenderList() {
|
||
|
|
|
||
|
|
this.opaque = [];
|
||
|
|
this.transparent = [];
|
||
|
|
|
||
|
|
this._opaqueCount = 0;
|
||
|
|
this._transparentCount = 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
RenderList.prototype.startCount = function () {
|
||
|
|
this._opaqueCount = 0;
|
||
|
|
this._transparentCount = 0;
|
||
|
|
};
|
||
|
|
|
||
|
|
RenderList.prototype.add = function (object, isTransparent) {
|
||
|
|
if (isTransparent) {
|
||
|
|
this.transparent[this._transparentCount++] = object;
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
this.opaque[this._opaqueCount++] = object;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
RenderList.prototype.endCount = function () {
|
||
|
|
this.transparent.length = this._transparentCount;
|
||
|
|
this.opaque.length = this._opaqueCount;
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @typedef {Object} clay.Scene.RenderList
|
||
|
|
* @property {Array.<clay.Renderable>} opaque
|
||
|
|
* @property {Array.<clay.Renderable>} transparent
|
||
|
|
*/
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @constructor clay.Scene
|
||
|
|
* @extends clay.Node
|
||
|
|
*/
|
||
|
|
var Scene = Node.extend(function () {
|
||
|
|
return /** @lends clay.Scene# */ {
|
||
|
|
/**
|
||
|
|
* Global material of scene
|
||
|
|
* @type {clay.Material}
|
||
|
|
*/
|
||
|
|
material: null,
|
||
|
|
|
||
|
|
lights: [],
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Scene bounding box in view space.
|
||
|
|
* Used when camera needs to adujst the near and far plane automatically
|
||
|
|
* so that the view frustum contains the visible objects as tightly as possible.
|
||
|
|
* Notice:
|
||
|
|
* It is updated after rendering (in the step of frustum culling passingly). So may be not so accurate, but saves a lot of calculation
|
||
|
|
*
|
||
|
|
* @type {clay.BoundingBox}
|
||
|
|
*/
|
||
|
|
viewBoundingBoxLastFrame: new BoundingBox(),
|
||
|
|
|
||
|
|
// Uniforms for shadow map.
|
||
|
|
shadowUniforms: {},
|
||
|
|
|
||
|
|
_cameraList: [],
|
||
|
|
|
||
|
|
// Properties to save the light information in the scene
|
||
|
|
// Will be set in the render function
|
||
|
|
_lightUniforms: {},
|
||
|
|
|
||
|
|
_previousLightNumber: {},
|
||
|
|
|
||
|
|
_lightNumber: {
|
||
|
|
// groupId: {
|
||
|
|
// POINT_LIGHT: 0,
|
||
|
|
// DIRECTIONAL_LIGHT: 0,
|
||
|
|
// SPOT_LIGHT: 0,
|
||
|
|
// AMBIENT_LIGHT: 0,
|
||
|
|
// AMBIENT_SH_LIGHT: 0
|
||
|
|
// }
|
||
|
|
},
|
||
|
|
|
||
|
|
_lightProgramKeys: {},
|
||
|
|
|
||
|
|
_nodeRepository: {},
|
||
|
|
|
||
|
|
_renderLists: new LRUCache(20)
|
||
|
|
|
||
|
|
};
|
||
|
|
}, function () {
|
||
|
|
this._scene = this;
|
||
|
|
},
|
||
|
|
/** @lends clay.Scene.prototype. */
|
||
|
|
{
|
||
|
|
|
||
|
|
// Add node to scene
|
||
|
|
addToScene: function (node) {
|
||
|
|
if (node instanceof Camera) {
|
||
|
|
if (this._cameraList.length > 0) {
|
||
|
|
console.warn('Found multiple camera in one scene. Use the fist one.');
|
||
|
|
}
|
||
|
|
this._cameraList.push(node);
|
||
|
|
}
|
||
|
|
else if (node instanceof Light) {
|
||
|
|
this.lights.push(node);
|
||
|
|
}
|
||
|
|
if (node.name) {
|
||
|
|
this._nodeRepository[node.name] = node;
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
// Remove node from scene
|
||
|
|
removeFromScene: function (node) {
|
||
|
|
var idx;
|
||
|
|
if (node instanceof Camera) {
|
||
|
|
idx = this._cameraList.indexOf(node);
|
||
|
|
if (idx >= 0) {
|
||
|
|
this._cameraList.splice(idx, 1);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else if (node instanceof Light) {
|
||
|
|
idx = this.lights.indexOf(node);
|
||
|
|
if (idx >= 0) {
|
||
|
|
this.lights.splice(idx, 1);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (node.name) {
|
||
|
|
delete this._nodeRepository[node.name];
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get node by name
|
||
|
|
* @param {string} name
|
||
|
|
* @return {Node}
|
||
|
|
* @DEPRECATED
|
||
|
|
*/
|
||
|
|
getNode: function (name) {
|
||
|
|
return this._nodeRepository[name];
|
||
|
|
},
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Set main camera of the scene.
|
||
|
|
* @param {claygl.Camera} camera
|
||
|
|
*/
|
||
|
|
setMainCamera: function (camera) {
|
||
|
|
var idx = this._cameraList.indexOf(camera);
|
||
|
|
if (idx >= 0) {
|
||
|
|
this._cameraList.splice(idx, 1);
|
||
|
|
}
|
||
|
|
this._cameraList.unshift(camera);
|
||
|
|
},
|
||
|
|
/**
|
||
|
|
* Get main camera of the scene.
|
||
|
|
*/
|
||
|
|
getMainCamera: function () {
|
||
|
|
return this._cameraList[0];
|
||
|
|
},
|
||
|
|
|
||
|
|
getLights: function () {
|
||
|
|
return this.lights;
|
||
|
|
},
|
||
|
|
|
||
|
|
updateLights: function () {
|
||
|
|
var lights = this.lights;
|
||
|
|
this._previousLightNumber = this._lightNumber;
|
||
|
|
|
||
|
|
var lightNumber = {};
|
||
|
|
for (var i = 0; i < lights.length; i++) {
|
||
|
|
var light = lights[i];
|
||
|
|
if (light.invisible) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
var group = light.group;
|
||
|
|
if (!lightNumber[group]) {
|
||
|
|
lightNumber[group] = {};
|
||
|
|
}
|
||
|
|
// User can use any type of light
|
||
|
|
lightNumber[group][light.type] = lightNumber[group][light.type] || 0;
|
||
|
|
lightNumber[group][light.type]++;
|
||
|
|
}
|
||
|
|
this._lightNumber = lightNumber;
|
||
|
|
|
||
|
|
for (var groupId in lightNumber) {
|
||
|
|
this._lightProgramKeys[groupId] = getProgramKey(lightNumber[groupId]);
|
||
|
|
}
|
||
|
|
|
||
|
|
this._updateLightUniforms();
|
||
|
|
},
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Clone a node and it's children, including mesh, camera, light, etc.
|
||
|
|
* Unlike using `Node#clone`. It will clone skeleton and remap the joints. Material will also be cloned.
|
||
|
|
*
|
||
|
|
* @param {clay.Node} node
|
||
|
|
* @return {clay.Node}
|
||
|
|
*/
|
||
|
|
cloneNode: function (node) {
|
||
|
|
var newNode = node.clone();
|
||
|
|
var clonedNodesMap = {};
|
||
|
|
function buildNodesMap(sNode, tNode) {
|
||
|
|
clonedNodesMap[sNode.__uid__] = tNode;
|
||
|
|
|
||
|
|
for (var i = 0; i < sNode._children.length; i++) {
|
||
|
|
var sChild = sNode._children[i];
|
||
|
|
var tChild = tNode._children[i];
|
||
|
|
buildNodesMap(sChild, tChild);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
buildNodesMap(node, newNode);
|
||
|
|
|
||
|
|
newNode.traverse(function (newChild) {
|
||
|
|
if (newChild.skeleton) {
|
||
|
|
newChild.skeleton = newChild.skeleton.clone(clonedNodesMap);
|
||
|
|
}
|
||
|
|
if (newChild.material) {
|
||
|
|
newChild.material = newChild.material.clone();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
return newNode;
|
||
|
|
},
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Traverse the scene and add the renderable object to the render list.
|
||
|
|
* It needs camera for the frustum culling.
|
||
|
|
*
|
||
|
|
* @param {clay.Camera} camera
|
||
|
|
* @param {boolean} updateSceneBoundingBox
|
||
|
|
* @return {clay.Scene.RenderList}
|
||
|
|
*/
|
||
|
|
updateRenderList: function (camera, updateSceneBoundingBox) {
|
||
|
|
var id = camera.__uid__;
|
||
|
|
var renderList = this._renderLists.get(id);
|
||
|
|
if (!renderList) {
|
||
|
|
renderList = new RenderList();
|
||
|
|
this._renderLists.put(id, renderList);
|
||
|
|
}
|
||
|
|
renderList.startCount();
|
||
|
|
|
||
|
|
if (updateSceneBoundingBox) {
|
||
|
|
this.viewBoundingBoxLastFrame.min.set(Infinity, Infinity, Infinity);
|
||
|
|
this.viewBoundingBoxLastFrame.max.set(-Infinity, -Infinity, -Infinity);
|
||
|
|
}
|
||
|
|
|
||
|
|
var sceneMaterialTransparent = this.material && this.material.transparent || false;
|
||
|
|
this._doUpdateRenderList(this, camera, sceneMaterialTransparent, renderList, updateSceneBoundingBox);
|
||
|
|
|
||
|
|
renderList.endCount();
|
||
|
|
|
||
|
|
return renderList;
|
||
|
|
},
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get render list. Used after {@link clay.Scene#updateRenderList}
|
||
|
|
* @param {clay.Camera} camera
|
||
|
|
* @return {clay.Scene.RenderList}
|
||
|
|
*/
|
||
|
|
getRenderList: function (camera) {
|
||
|
|
return this._renderLists.get(camera.__uid__);
|
||
|
|
},
|
||
|
|
|
||
|
|
_doUpdateRenderList: function (parent, camera, sceneMaterialTransparent, renderList, updateSceneBoundingBox) {
|
||
|
|
if (parent.invisible) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
// TODO Optimize
|
||
|
|
for (var i = 0; i < parent._children.length; i++) {
|
||
|
|
var child = parent._children[i];
|
||
|
|
|
||
|
|
if (child.isRenderable()) {
|
||
|
|
// Frustum culling
|
||
|
|
var worldM = child.isSkinnedMesh() ? IDENTITY : child.worldTransform.array;
|
||
|
|
var geometry = child.geometry;
|
||
|
|
|
||
|
|
mat4.multiplyAffine(WORLDVIEW, camera.viewMatrix.array, worldM);
|
||
|
|
if (updateSceneBoundingBox && !geometry.boundingBox || !this.isFrustumCulled(child, camera, WORLDVIEW)) {
|
||
|
|
renderList.add(child, child.material.transparent || sceneMaterialTransparent);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (child._children.length > 0) {
|
||
|
|
this._doUpdateRenderList(child, camera, sceneMaterialTransparent, renderList, updateSceneBoundingBox);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
/**
|
||
|
|
* If an scene object is culled by camera frustum
|
||
|
|
*
|
||
|
|
* Object can be a renderable or a light
|
||
|
|
*
|
||
|
|
* @param {clay.Node} object
|
||
|
|
* @param {clay.Camera} camera
|
||
|
|
* @param {Array.<number>} worldViewMat represented with array
|
||
|
|
* @param {Array.<number>} projectionMat represented with array
|
||
|
|
*/
|
||
|
|
isFrustumCulled: (function () {
|
||
|
|
// Frustum culling
|
||
|
|
// http://www.cse.chalmers.se/~uffe/vfc_bbox.pdf
|
||
|
|
var cullingBoundingBox = new BoundingBox();
|
||
|
|
var cullingMatrix = new Matrix4();
|
||
|
|
return function(object, camera, worldViewMat) {
|
||
|
|
// Bounding box can be a property of object(like light) or renderable.geometry
|
||
|
|
// PENDING
|
||
|
|
var geoBBox = object.boundingBox;
|
||
|
|
if (!geoBBox) {
|
||
|
|
if (object.skeleton && object.skeleton.boundingBox) {
|
||
|
|
geoBBox = object.skeleton.boundingBox;
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
geoBBox = object.geometry.boundingBox;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!geoBBox) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
cullingMatrix.array = worldViewMat;
|
||
|
|
cullingBoundingBox.transformFrom(geoBBox, cullingMatrix);
|
||
|
|
|
||
|
|
// Passingly update the scene bounding box
|
||
|
|
// FIXME exclude very large mesh like ground plane or terrain ?
|
||
|
|
// FIXME Only rendererable which cast shadow ?
|
||
|
|
|
||
|
|
// FIXME boundingBox becomes much larger after transformd.
|
||
|
|
if (object.castShadow) {
|
||
|
|
this.viewBoundingBoxLastFrame.union(cullingBoundingBox);
|
||
|
|
}
|
||
|
|
// Ignore frustum culling if object is skinned mesh.
|
||
|
|
if (object.frustumCulling) {
|
||
|
|
if (!cullingBoundingBox.intersectBoundingBox(camera.frustum.boundingBox)) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
cullingMatrix.array = camera.projectionMatrix.array;
|
||
|
|
if (
|
||
|
|
cullingBoundingBox.max.array[2] > 0 &&
|
||
|
|
cullingBoundingBox.min.array[2] < 0
|
||
|
|
) {
|
||
|
|
// Clip in the near plane
|
||
|
|
cullingBoundingBox.max.array[2] = -1e-20;
|
||
|
|
}
|
||
|
|
|
||
|
|
cullingBoundingBox.applyProjection(cullingMatrix);
|
||
|
|
|
||
|
|
var min = cullingBoundingBox.min.array;
|
||
|
|
var max = cullingBoundingBox.max.array;
|
||
|
|
|
||
|
|
if (
|
||
|
|
max[0] < -1 || min[0] > 1
|
||
|
|
|| max[1] < -1 || min[1] > 1
|
||
|
|
|| max[2] < -1 || min[2] > 1
|
||
|
|
) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return false;
|
||
|
|
};
|
||
|
|
})(),
|
||
|
|
|
||
|
|
_updateLightUniforms: function () {
|
||
|
|
var lights = this.lights;
|
||
|
|
// Put the light cast shadow before the light not cast shadow
|
||
|
|
lights.sort(lightSortFunc);
|
||
|
|
|
||
|
|
var lightUniforms = this._lightUniforms;
|
||
|
|
for (var group in lightUniforms) {
|
||
|
|
for (var symbol in lightUniforms[group]) {
|
||
|
|
lightUniforms[group][symbol].value.length = 0;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for (var i = 0; i < lights.length; i++) {
|
||
|
|
|
||
|
|
var light = lights[i];
|
||
|
|
|
||
|
|
if (light.invisible) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
var group = light.group;
|
||
|
|
|
||
|
|
for (var symbol in light.uniformTemplates) {
|
||
|
|
var uniformTpl = light.uniformTemplates[symbol];
|
||
|
|
var value = uniformTpl.value(light);
|
||
|
|
if (value == null) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
if (!lightUniforms[group]) {
|
||
|
|
lightUniforms[group] = {};
|
||
|
|
}
|
||
|
|
if (!lightUniforms[group][symbol]) {
|
||
|
|
lightUniforms[group][symbol] = {
|
||
|
|
type: '',
|
||
|
|
value: []
|
||
|
|
};
|
||
|
|
}
|
||
|
|
var lu = lightUniforms[group][symbol];
|
||
|
|
lu.type = uniformTpl.type + 'v';
|
||
|
|
switch (uniformTpl.type) {
|
||
|
|
case '1i':
|
||
|
|
case '1f':
|
||
|
|
case 't':
|
||
|
|
lu.value.push(value);
|
||
|
|
break;
|
||
|
|
case '2f':
|
||
|
|
case '3f':
|
||
|
|
case '4f':
|
||
|
|
for (var j = 0; j < value.length; j++) {
|
||
|
|
lu.value.push(value[j]);
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
default:
|
||
|
|
console.error('Unkown light uniform type ' + uniformTpl.type);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
getLightGroups: function () {
|
||
|
|
var lightGroups = [];
|
||
|
|
for (var groupId in this._lightNumber) {
|
||
|
|
lightGroups.push(groupId);
|
||
|
|
}
|
||
|
|
return lightGroups;
|
||
|
|
},
|
||
|
|
|
||
|
|
getNumberChangedLightGroups: function () {
|
||
|
|
var lightGroups = [];
|
||
|
|
for (var groupId in this._lightNumber) {
|
||
|
|
if (this.isLightNumberChanged(groupId)) {
|
||
|
|
lightGroups.push(groupId);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return lightGroups;
|
||
|
|
},
|
||
|
|
|
||
|
|
// Determine if light group is different with since last frame
|
||
|
|
// Used to determine whether to update shader and scene's uniforms in Renderer.render
|
||
|
|
isLightNumberChanged: function (lightGroup) {
|
||
|
|
var prevLightNumber = this._previousLightNumber;
|
||
|
|
var currentLightNumber = this._lightNumber;
|
||
|
|
// PENDING Performance
|
||
|
|
for (var type in currentLightNumber[lightGroup]) {
|
||
|
|
if (!prevLightNumber[lightGroup]) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
if (currentLightNumber[lightGroup][type] !== prevLightNumber[lightGroup][type]) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for (var type in prevLightNumber[lightGroup]) {
|
||
|
|
if (!currentLightNumber[lightGroup]) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
if (currentLightNumber[lightGroup][type] !== prevLightNumber[lightGroup][type]) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
},
|
||
|
|
|
||
|
|
getLightsNumbers: function (lightGroup) {
|
||
|
|
return this._lightNumber[lightGroup];
|
||
|
|
},
|
||
|
|
|
||
|
|
getProgramKey: function (lightGroup) {
|
||
|
|
return this._lightProgramKeys[lightGroup];
|
||
|
|
},
|
||
|
|
|
||
|
|
setLightUniforms: (function () {
|
||
|
|
function setUniforms(uniforms, program, renderer) {
|
||
|
|
for (var symbol in uniforms) {
|
||
|
|
var lu = uniforms[symbol];
|
||
|
|
if (lu.type === 'tv') {
|
||
|
|
if (!program.hasUniform(symbol)) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
var texSlots = [];
|
||
|
|
for (var i = 0; i < lu.value.length; i++) {
|
||
|
|
var texture = lu.value[i];
|
||
|
|
var slot = program.takeCurrentTextureSlot(renderer, texture);
|
||
|
|
texSlots.push(slot);
|
||
|
|
}
|
||
|
|
program.setUniform(renderer.gl, '1iv', symbol, texSlots);
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
program.setUniform(renderer.gl, lu.type, symbol, lu.value);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return function (program, lightGroup, renderer) {
|
||
|
|
setUniforms(this._lightUniforms[lightGroup], program, renderer);
|
||
|
|
// Set shadows
|
||
|
|
setUniforms(this.shadowUniforms, program, renderer);
|
||
|
|
};
|
||
|
|
})(),
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Dispose self, clear all the scene objects
|
||
|
|
* But resources of gl like texuture, shader will not be disposed.
|
||
|
|
* Mostly you should use disposeScene method in Renderer to do dispose.
|
||
|
|
*/
|
||
|
|
dispose: function () {
|
||
|
|
this.material = null;
|
||
|
|
this._opaqueList = [];
|
||
|
|
this._transparentList = [];
|
||
|
|
|
||
|
|
this.lights = [];
|
||
|
|
|
||
|
|
this._lightUniforms = {};
|
||
|
|
|
||
|
|
this._lightNumber = {};
|
||
|
|
this._nodeRepository = {};
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
function lightSortFunc(a, b) {
|
||
|
|
if (b.castShadow && !a.castShadow) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export default Scene;
|