Object.assign(pc, function () {
/**
* @component
* @constructor
* @name pc.ModelComponent
* @classdesc Enables an Entity to render a model or a primitive shape. This Component attaches additional model
* geometry in to the scene graph below the Entity.
* @description Create a new ModelComponent
* @param {pc.ModelComponentSystem} system The ComponentSystem that created this Component
* @param {pc.Entity} entity The Entity that this Component is attached to.
* @extends pc.Component
* @property {String} type The type of the model, which can be one of the following values:
* <ul>
* <li>asset: The component will render a model asset</li>
* <li>box: The component will render a box (1 unit in each dimension)</li>
* <li>capsule: The component will render a capsule (radius 0.5, height 2)</li>
* <li>cone: The component will render a cone (radius 0.5, height 1)</li>
* <li>cylinder: The component will render a cylinder (radius 0.5, height 1)</li>
* <li>plane: The component will render a plane (1 unit in each dimension)</li>
* <li>sphere: The component will render a sphere (radius 0.5)</li>
* </ul>
* @property {pc.Asset} asset The asset for the model (only applies to models of type 'asset') - can also be an asset id.
* @property {Boolean} castShadows If true, this model will cast shadows for lights that have shadow casting enabled.
* @property {Boolean} receiveShadows If true, shadows will be cast on this model
* @property {Number} materialAsset The material {@link pc.Asset} that will be used to render the model (not used on models of type 'asset')
* @property {pc.Model} model The model that is added to the scene graph. It can be not set or loaded, so will return null.
* @property {Object} mapping A dictionary that holds material overrides for each mesh instance. Only applies to model components of type 'asset'. The mapping contains pairs of mesh instance index - material asset id.
* @property {Boolean} castShadowsLightmap If true, this model will cast shadows when rendering lightmaps
* @property {Boolean} lightmapped If true, this model will be lightmapped after using lightmapper.bake()
* @property {Number} lightmapSizeMultiplier Lightmap resolution multiplier
* @property {Boolean} isStatic Mark model as non-movable (optimization)
* @property {pc.MeshInstance[]} meshInstances An array of meshInstances contained in the component's model. If model is not set or loaded for component it will return null.
* @property {Number} batchGroupId Assign model to a specific batch group (see {@link pc.BatchGroup}). Default value is -1 (no group).
* @property {Array} layers An array of layer IDs ({@link pc.Layer#id}) to which this model should belong.
* Don't push/pop/splice or modify this array, if you want to change it - set a new one instead.
*/
var ModelComponent = function ModelComponent(system, entity) {
pc.Component.call(this, system, entity);
// this.enabled = true;
this._type = 'asset';
// model asset
this._asset = null;
this._model = null;
this._mapping = {};
this._castShadows = true;
this._receiveShadows = true;
this._materialAsset = null;
this._material = system.defaultMaterial;
this._castShadowsLightmap = true;
this._lightmapped = false;
this._lightmapSizeMultiplier = 1;
this._isStatic = false;
this._layers = [pc.LAYERID_WORLD]; // assign to the default world layer
this._batchGroupId = -1;
this._area = null;
this._assetOld = 0;
this._materialEvents = null;
this._dirtyModelAsset = false;
this._dirtyMaterialAsset = false;
this._clonedModel = false;
// #ifdef DEBUG
this._batchGroup = null;
// #endif
};
ModelComponent.prototype = Object.create(pc.Component.prototype);
ModelComponent.prototype.constructor = ModelComponent;
Object.assign(ModelComponent.prototype, {
setVisible: function (visible) {
console.warn("WARNING: setVisible: Function is deprecated. Set enabled property instead.");
this.enabled = visible;
},
addModelToLayers: function () {
var layer;
var layers = this.system.app.scene.layers;
for (var i = 0; i < this._layers.length; i++) {
layer = layers.getLayerById(this._layers[i]);
if (!layer) continue;
layer.addMeshInstances(this.meshInstances);
}
},
removeModelFromLayers: function (model) {
var layer;
var layers = this.system.app.scene.layers;
for (var i = 0; i < this._layers.length; i++) {
layer = layers.getLayerById(this._layers[i]);
if (!layer) continue;
layer.removeMeshInstances(model.meshInstances);
}
},
onRemove: function () {
if (this.type === 'asset') {
this.asset = null;
} else {
this.model = null;
}
this.materialAsset = null;
this._unsetMaterialEvents();
},
onLayersChanged: function (oldComp, newComp) {
this.addModelToLayers();
oldComp.off("add", this.onLayerAdded, this);
oldComp.off("remove", this.onLayerRemoved, this);
newComp.on("add", this.onLayerAdded, this);
newComp.on("remove", this.onLayerRemoved, this);
},
onLayerAdded: function (layer) {
var index = this.layers.indexOf(layer.id);
if (index < 0) return;
layer.addMeshInstances(this.meshInstances);
},
onLayerRemoved: function (layer) {
var index = this.layers.indexOf(layer.id);
if (index < 0) return;
layer.removeMeshInstances(this.meshInstances);
},
_setMaterialEvent: function (index, event, id, handler) {
var evt = event + ':' + id;
this.system.app.assets.on(evt, handler, this);
if (!this._materialEvents)
this._materialEvents = [];
if (!this._materialEvents[index])
this._materialEvents[index] = { };
this._materialEvents[index][evt] = {
id: id,
handler: handler
};
},
_unsetMaterialEvents: function () {
var assets = this.system.app.assets;
var events = this._materialEvents;
if (!events)
return;
for (var i = 0, len = events.length; i < len; i++) {
if (!events[i]) continue;
var evt = events[i];
for (var key in evt) {
assets.off(key, evt[key].handler, this);
}
}
this._materialEvents = null;
},
_getAssetByIdOrPath: function (idOrPath) {
var asset = null;
var isPath = isNaN(parseInt(idOrPath, 10));
// get asset by id or url
if (!isPath) {
asset = this.system.app.assets.get(idOrPath);
} else if (this.asset) {
var url = this._getMaterialAssetUrl(idOrPath);
if (url)
asset = this.system.app.assets.getByUrl(url);
}
return asset;
},
_getMaterialAssetUrl: function (path) {
if (!this.asset) return null;
var modelAsset = this.system.app.assets.get(this.asset);
if (!modelAsset) return null;
var fileUrl = modelAsset.getFileUrl();
var dirUrl = pc.path.getDirectory(fileUrl);
return pc.path.join(dirUrl, path);
},
_loadAndSetMeshInstanceMaterial: function (materialAsset, meshInstance, index) {
var assets = this.system.app.assets;
if (!materialAsset)
return;
if (materialAsset) {
if (materialAsset.resource) {
meshInstance.material = materialAsset.resource;
this._setMaterialEvent(index, 'remove', materialAsset.id, function () {
meshInstance.material = this.system.defaultMaterial;
});
} else {
this._setMaterialEvent(index, 'load', materialAsset.id, function (asset) {
meshInstance.material = asset.resource;
this._setMaterialEvent(index, 'remove', materialAsset.id, function () {
meshInstance.material = this.system.defaultMaterial;
});
});
if (this.enabled && this.entity.enabled)
assets.load(materialAsset);
}
}
},
onEnable: function () {
var app = this.system.app;
var scene = app.scene;
scene.on("set:layers", this.onLayersChanged, this);
if (scene.layers) {
scene.layers.on("add", this.onLayerAdded, this);
scene.layers.on("remove", this.onLayerRemoved, this);
}
var isAsset = (this._type === 'asset');
var asset;
if (this._model) {
this.addModelToLayers();
} else if (isAsset && this._asset) {
// bind and load model asset if necessary
asset = app.assets.get(this._asset);
if (asset && asset.resource !== this._model) {
this._bindModelAsset(asset);
}
}
if (this._materialAsset) {
// bind and load material asset if necessary
asset = app.assets.get(this._materialAsset);
if (asset && asset.resource !== this._material) {
this._bindMaterialAsset(asset);
}
}
if (isAsset) {
// bind mapped assets
// TODO: replace
if (this._mapping) {
for (var index in this._mapping) {
if (this._mapping[index]) {
asset = this._getAssetByIdOrPath(this._mapping[index]);
if (asset && !asset.resource) {
app.assets.load(asset);
}
}
}
}
}
if (this._batchGroupId >= 0) {
app.batcher.insert(pc.BatchGroup.MODEL, this.batchGroupId, this.entity);
}
},
onDisable: function () {
var app = this.system.app;
var scene = app.scene;
scene.off("set:layers", this.onLayersChanged, this);
if (scene.layers) {
scene.layers.off("add", this.onLayerAdded, this);
scene.layers.off("remove", this.onLayerRemoved, this);
}
if (this._batchGroupId >= 0) {
app.batcher.remove(pc.BatchGroup.MODEL, this.batchGroupId, this.entity);
}
if (this._model) {
this.removeModelFromLayers(this._model);
}
},
/**
* @function
* @name pc.ModelComponent#hide
* @description Stop rendering model without removing it from the scene hierarchy.
* This method sets the {@link pc.MeshInstance#visible} property of every MeshInstance in the model to false
* Note, this does not remove the model or mesh instances from the scene hierarchy or draw call list.
* So the model component still incurs some CPU overhead.
* @example
* this.timer = 0;
* this.visible = true;
* // ...
* // blink model every 0.1 seconds
* this.timer += dt;
* if (this.timer > 0.1) {
* if (!this.visible) {
* this.entity.model.show();
* this.visible = true;
* } else {
* this.entity.model.hide();
* this.visible = false;
* }
* this.timer = 0;
* }
*/
hide: function () {
if (this._model) {
var i, l;
var instances = this._model.meshInstances;
for (i = 0, l = instances.length; i < l; i++) {
instances[i].visible = false;
}
}
},
/**
* @function
* @name pc.ModelComponent#show
* @description Enable rendering of the model if hidden using {@link pc.ModelComponent#hide}.
* This method sets all the {@link pc.MeshInstance#visible} property on all mesh instances to true.
*/
show: function () {
if (this._model) {
var i, l;
var instances = this._model.meshInstances;
for (i = 0, l = instances.length; i < l; i++) {
instances[i].visible = true;
}
}
},
// NEW
_bindMaterialAsset: function (asset) {
asset.on('load', this._onMaterialAssetLoad, this);
asset.on('unload', this._onMaterialAssetUnload, this);
asset.on('remove', this._onMaterialAssetRemove, this);
asset.on('change', this._onMaterialAssetChange, this);
if (asset.resource) {
this._onMaterialAssetLoad(asset);
} else {
// don't trigger an asset load unless the component is enabled
if (!this.enabled || !this.entity.enabled) return;
this.system.app.assets.load(asset);
}
},
_unbindMaterialAsset: function (asset) {
asset.off('load', this._onMaterialAssetLoad, this);
asset.off('unload', this._onMaterialAssetUnload, this);
asset.off('remove', this._onMaterialAssetRemove, this);
asset.off('change', this._onMaterialAssetChange, this);
},
_onMaterialAssetAdd: function (asset) {
this.system.app.assets.off('add:' + asset.id, this._onMaterialAssetAdd, this);
if (this._materialAsset === asset.id) {
this._bindMaterialAsset(asset);
}
},
_onMaterialAssetLoad: function (asset) {
this.material = asset.resource;
},
_onMaterialAssetUnload: function (asset) {
this.material = this.system.defaultMaterial;
},
_onMaterialAssetRemove: function (asset) {
this._onMaterialAssetUnload(asset);
},
_onMaterialAssetChange: function (asset) {
},
_bindModelAsset: function (asset) {
this._unbindModelAsset(asset);
asset.on('load', this._onModelAssetLoad, this);
asset.on('unload', this._onModelAssetUnload, this);
asset.on('change', this._onModelAssetChange, this);
asset.on('remove', this._onModelAssetRemove, this);
if (asset.resource) {
this._onModelAssetLoad(asset);
} else {
// don't trigger an asset load unless the component is enabled
if (!this.enabled || !this.entity.enabled) return;
this.system.app.assets.load(asset);
}
},
_unbindModelAsset: function (asset) {
asset.off('load', this._onModelAssetLoad, this);
asset.off('unload', this._onModelAssetUnload, this);
asset.off('change', this._onModelAssetChange, this);
asset.off('remove', this._onModelAssetRemove, this);
},
_onModelAssetAdded: function (asset) {
this.system.app.assets.off('add:' + asset.id, this._onModelAssetAdd, this);
if (asset.id === this._asset) {
this._bindModelAsset(asset);
}
},
_onModelAssetLoad: function (asset) {
this.model = asset.resource.clone();
this._clonedModel = true;
},
_onModelAssetUnload: function (asset) {
this.model = null;
},
_onModelAssetChange: function (asset, attr, _new, _old) {
if (attr === 'data') {
this.mapping = this._mapping;
}
},
_onModelAssetRemove: function (asset) {
this.model = null;
}
});
Object.defineProperty(ModelComponent.prototype, "meshInstances", {
get: function () {
if (!this._model)
return null;
return this._model.meshInstances;
},
set: function (value) {
if (!this._model)
return;
this._model.meshInstances = value;
}
});
Object.defineProperty(ModelComponent.prototype, "type", {
get: function () {
return this._type;
},
set: function (value) {
if (this._type === value) return;
var mesh = null;
this._area = null;
this._type = value;
if (value === 'asset') {
if (this._asset !== null) {
this._bindModelAsset(this._asset);
} else {
this.model = null;
}
} else {
var system = this.system;
var gd = system.app.graphicsDevice;
switch (value) {
case 'box':
if (!system.box) {
system.box = pc.createBox(gd, {
halfExtents: new pc.Vec3(0.5, 0.5, 0.5)
});
}
mesh = system.box;
this._area = { x: 2, y: 2, z: 2, uv: (2.0 / 3) };
break;
case 'capsule':
if (!system.capsule) {
system.capsule = pc.createCapsule(gd, {
radius: 0.5,
height: 2
});
}
mesh = system.capsule;
this._area = { x: (Math.PI * 2), y: Math.PI, z: (Math.PI * 2), uv: (1.0 / 3 + ((1.0 / 3) / 3) * 2) };
break;
case 'cone':
if (!system.cone) {
system.cone = pc.createCone(gd, {
baseRadius: 0.5,
peakRadius: 0,
height: 1
});
}
mesh = system.cone;
this._area = { x: 2.54, y: 2.54, z: 2.54, uv: (1.0 / 3 + (1.0 / 3) / 3) };
break;
case 'cylinder':
if (!system.cylinder) {
system.cylinder = pc.createCylinder(gd, {
radius: 0.5,
height: 1
});
}
mesh = system.cylinder;
this._area = { x: Math.PI, y: (0.79 * 2), z: Math.PI, uv: (1.0 / 3 + ((1.0 / 3) / 3) * 2) };
break;
case 'plane':
if (!system.plane) {
system.plane = pc.createPlane(gd, {
halfExtents: new pc.Vec2(0.5, 0.5),
widthSegments: 1,
lengthSegments: 1
});
}
mesh = system.plane;
this._area = { x: 0, y: 1, z: 0, uv: 1 };
break;
case 'sphere':
if (!system.sphere) {
system.sphere = pc.createSphere(gd, {
radius: 0.5
});
}
mesh = system.sphere;
this._area = { x: Math.PI, y: Math.PI, z: Math.PI, uv: 1 };
break;
default:
throw new Error("Invalid model type: " + value);
}
var node = new pc.GraphNode();
var model = new pc.Model();
model.graph = node;
model.meshInstances = [new pc.MeshInstance(node, mesh, this._material)];
if (system._inTools)
model.generateWireframe();
this.model = model;
this._asset = null;
}
}
});
Object.defineProperty(ModelComponent.prototype, "asset", {
get: function () {
return this._asset;
},
set: function (value) {
var assets = this.system.app.assets;
var _id = value;
if (value instanceof pc.Asset) {
_id = value.id;
}
if (this._asset !== _id) {
if (this._asset) {
// remove previous asset
assets.off('add:' + this._asset, this._onModelAssetAdded, this);
var _prev = assets.get(this._asset);
if (_prev) {
this._unbindModelAsset(_prev);
}
}
this._asset = _id;
if (this._asset) {
var asset = assets.get(this._asset);
if (!asset) {
this.model = null;
assets.on('add:' + this._asset, this._onModelAssetAdded, this);
} else {
this._bindModelAsset(asset);
}
} else {
this.model = null;
}
}
}
});
Object.defineProperty(ModelComponent.prototype, "model", {
get: function () {
return this._model;
},
set: function (value) {
if (this._model === value) {
return;
}
if (this._model) {
this.removeModelFromLayers(this._model);
this.entity.removeChild(this._model.getGraph());
delete this._model._entity;
if (this._clonedModel) {
this._model.destroy();
this._clonedModel = false;
}
}
this._model = value;
if (this._model) {
var meshInstances = this._model.meshInstances;
for (var i = 0; i < meshInstances.length; i++) {
meshInstances[i].castShadow = this._castShadows;
meshInstances[i].receiveShadow = this._receiveShadows;
meshInstances[i].isStatic = this._isStatic;
}
this.lightmapped = this._lightmapped; // update meshInstances
this.entity.addChild(this._model.graph);
if (this.enabled && this.entity.enabled) {
this.addModelToLayers();
}
// Store the entity that owns this model
this._model._entity = this.entity;
// Update any animation component
if (this.entity.animation)
this.entity.animation.setModel(this._model);
// trigger event handler to load mapping
// for new model
if (this.type === 'asset') {
this.mapping = this._mapping;
} else {
this._unsetMaterialEvents();
}
}
}
});
Object.defineProperty(ModelComponent.prototype, "lightmapped", {
get: function () {
return this._lightmapped;
},
set: function (value) {
if (value === this._lightmapped) return;
var i, m, mask;
this._lightmapped = value;
if (this._model) {
var rcv = this._model.meshInstances;
if (value) {
for (i = 0; i < rcv.length; i++) {
m = rcv[i];
mask = m.mask;
m.mask = (mask | pc.MASK_BAKED) & ~(pc.MASK_DYNAMIC | pc.MASK_LIGHTMAP);
}
} else {
for (i = 0; i < rcv.length; i++) {
m = rcv[i];
m.deleteParameter("texture_lightMap");
m.deleteParameter("texture_dirLightMap");
m._shaderDefs &= ~pc.SHADERDEF_LM;
mask = m.mask;
m.mask = (mask | pc.MASK_DYNAMIC) & ~(pc.MASK_BAKED | pc.MASK_LIGHTMAP);
}
}
}
}
});
Object.defineProperty(ModelComponent.prototype, "castShadows", {
get: function () {
return this._castShadows;
},
set: function (value) {
if (this._castShadows === value) return;
var layer;
var i;
var model = this._model;
if (model) {
var layers = this.layers;
var scene = this.system.app.scene;
if (this._castShadows && !value) {
for (i = 0; i < layers.length; i++) {
layer = this.system.app.scene.layers.getLayerById(this.layers[i]);
if (!layer) continue;
layer.removeShadowCasters(model.meshInstances);
}
}
var meshInstances = model.meshInstances;
for (i = 0; i < meshInstances.length; i++) {
meshInstances[i].castShadow = value;
}
if (!this._castShadows && value) {
for (i = 0; i < layers.length; i++) {
layer = scene.layers.getLayerById(layers[i]);
if (!layer) continue;
layer.addShadowCasters(model.meshInstances);
}
}
}
this._castShadows = value;
}
});
Object.defineProperty(ModelComponent.prototype, 'receiveShadows', {
get: function () {
return this._receiveShadows;
},
set: function (value) {
if (this._receiveShadows === value) return;
this._receiveShadows = value;
if (this._model) {
var meshInstances = this._model.meshInstances;
for (var i = 0, len = meshInstances.length; i < len; i++) {
meshInstances[i].receiveShadow = value;
}
}
}
});
Object.defineProperty(ModelComponent.prototype, "castShadowsLightmap", {
get: function () {
return this._castShadowsLightmap;
},
set: function (value) {
this._castShadowsLightmap = value;
}
});
Object.defineProperty(ModelComponent.prototype, "lightmapSizeMultiplier", {
get: function () {
return this._lightmapSizeMultiplier;
},
set: function (value) {
this._lightmapSizeMultiplier = value;
}
});
Object.defineProperty(ModelComponent.prototype, "isStatic", {
get: function () {
return this._isStatic;
},
set: function (value) {
if (this._isStatic === value) return;
this._isStatic = value;
var i, m;
if (this._model) {
var rcv = this._model.meshInstances;
for (i = 0; i < rcv.length; i++) {
m = rcv[i];
m.isStatic = value;
}
}
}
});
Object.defineProperty(ModelComponent.prototype, "layers", {
get: function () {
return this._layers;
},
set: function (value) {
var i, layer;
var layers = this.system.app.scene.layers;
if (this.meshInstances) {
// remove all meshinstances from old layers
for (i = 0; i < this._layers.length; i++) {
layer = layers.getLayerById(this._layers[i]);
if (!layer) continue;
layer.removeMeshInstances(this.meshInstances);
}
}
// set the layer list
this._layers.length = 0;
for (i = 0; i < value.length; i++) {
this._layers[i] = value[i];
}
// don't add into layers until we're enabled
if (!this.enabled || !this.entity.enabled || !this.meshInstances) return;
// add all mesh instances to new layers
for (i = 0; i < this._layers.length; i++) {
layer = layers.getLayerById(this._layers[i]);
if (!layer) continue;
layer.addMeshInstances(this.meshInstances);
}
}
});
Object.defineProperty(ModelComponent.prototype, "batchGroupId", {
get: function () {
return this._batchGroupId;
},
set: function (value) {
if (this._batchGroupId === value) return;
var batcher = this.system.app.batcher;
if (this.entity.enabled && this._batchGroupId >= 0) {
batcher.remove(pc.BatchGroup.MODEL, this.batchGroupId, this.entity);
}
if (this.entity.enabled && value >= 0) {
batcher.insert(pc.BatchGroup.MODEL, value, this.entity);
}
if (value < 0 && this._batchGroupId >= 0 && this.enabled && this.entity.enabled) {
// re-add model to scene, in case it was removed by batching
this.addModelToLayers();
}
this._batchGroupId = value;
}
});
Object.defineProperty(ModelComponent.prototype, "materialAsset", {
get: function () {
return this._materialAsset;
},
set: function (value) {
var _id = value;
if (value instanceof pc.Asset) {
_id = value.id;
}
var assets = this.system.app.assets;
if (_id !== this._materialAsset) {
if (this._materialAsset) {
assets.off('add:' + this._materialAsset, this._onMaterialAssetAdd, this);
var _prev = assets.get(this._materialAsset);
if (_prev) {
this._unbindMaterialAsset(_prev);
}
}
this._materialAsset = _id;
if (this._materialAsset) {
var asset = assets.get(this._materialAsset);
if (!asset) {
this.material = this.system.defaultMaterial;
assets.on('add:' + this._materialAsset, this._onMaterialAssetAdd, this);
} else {
this._bindMaterialAsset(asset);
}
} else {
this.material = this.system.defaultMaterial;
}
}
}
});
Object.defineProperty(ModelComponent.prototype, "material", {
get: function () {
return this._material;
},
set: function (value) {
if (this._material !== value) {
this._material = value;
var model = this._model;
if (model && this._type !== 'asset') {
var meshInstances = model.meshInstances;
for (var i = 0, len = meshInstances.length; i < len; i++) {
meshInstances[i].material = value;
}
}
}
}
});
Object.defineProperty(ModelComponent.prototype, "mapping", {
get: function () {
return this._mapping;
},
set: function (value) {
if (this._type !== 'asset')
return;
// unsubscribe from old events
this._unsetMaterialEvents();
// can't have a null mapping
if (!value)
value = {};
this._mapping = value;
if (!this._model) return;
var meshInstances = this._model.meshInstances;
var modelAsset = this.asset ? this.system.app.assets.get(this.asset) : null;
var assetMapping = modelAsset ? modelAsset.data.mapping : null;
var asset = null;
for (var i = 0, len = meshInstances.length; i < len; i++) {
if (value[i] !== undefined) {
if (value[i]) {
asset = this.system.app.assets.get(value[i]);
this._loadAndSetMeshInstanceMaterial(asset, meshInstances[i], i);
} else {
meshInstances[i].material = this.system.defaultMaterial;
}
} else if (assetMapping) {
if (assetMapping[i] && (assetMapping[i].material || assetMapping[i].path)) {
if (assetMapping[i].material !== undefined) {
asset = this.system.app.assets.get(assetMapping[i].material);
} else if (assetMapping[i].path !== undefined) {
var url = this._getMaterialAssetUrl(assetMapping[i].path);
if (url) {
asset = this.system.app.assets.getByUrl(url);
}
}
this._loadAndSetMeshInstanceMaterial(asset, meshInstances[i], i);
} else {
meshInstances[i].material = this.system.defaultMaterial;
}
}
}
}
});
return {
ModelComponent: ModelComponent
};
}());