Skip to content

Commit d369b82

Browse files
authored
Optimized culling solution for skinned meshes (playcanvas#2435)
* ModelComponent can specify oversize bounding box and disable expensive skinned bounds update * es5 compatibility
1 parent f1692a6 commit d369b82

File tree

5 files changed

+87
-14
lines changed

5 files changed

+87
-14
lines changed

src/framework/components/model/component.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ import { Component } from '../component.js';
4747
* @property {boolean} lightmapped If true, this model will be lightmapped after using lightmapper.bake().
4848
* @property {number} lightmapSizeMultiplier Lightmap resolution multiplier.
4949
* @property {boolean} isStatic Mark model as non-movable (optimization).
50+
* @property {pc.BoundingBox} aabb If set, the bounding box is used as a bounding box for visibility culling of attached mesh instances. This is an optimization,
51+
* allowing oversized bounding box to be specified for skinned characters in order to avoid per frame bounding box computations based on bone positions.
5052
* @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.
5153
* @property {number} batchGroupId Assign model to a specific batch group (see {@link pc.BatchGroup}). Default value is -1 (no group).
5254
* @property {number[]} layers An array of layer IDs ({@link pc.Layer#id}) to which this model should belong.
@@ -78,6 +80,9 @@ function ModelComponent(system, entity) {
7880
this._layers = [LAYERID_WORLD]; // assign to the default world layer
7981
this._batchGroupId = -1;
8082

83+
// bounding box which can be set to override bounding box based on mesh
84+
this._aabb = null;
85+
8186
this._area = null;
8287

8388
this._assetOld = 0;
@@ -494,6 +499,26 @@ Object.defineProperty(ModelComponent.prototype, "meshInstances", {
494499
}
495500
});
496501

502+
Object.defineProperties(ModelComponent.prototype, {
503+
504+
'aabb': {
505+
get: function () {
506+
return this._aabb;
507+
},
508+
set: function (value) {
509+
this._aabb = value;
510+
511+
// set it on meshInstances
512+
var mi = this._model.meshInstances;
513+
if (mi) {
514+
for (var i = 0; i < mi.length; i++) {
515+
mi[i].setOverrideAabb(this._aabb);
516+
}
517+
}
518+
}
519+
}
520+
});
521+
497522
Object.defineProperty(ModelComponent.prototype, "type", {
498523
get: function () {
499524
return this._type;
@@ -683,6 +708,7 @@ Object.defineProperty(ModelComponent.prototype, "model", {
683708
meshInstances[i].castShadow = this._castShadows;
684709
meshInstances[i].receiveShadow = this._receiveShadows;
685710
meshInstances[i].isStatic = this._isStatic;
711+
meshInstances[i].setOverrideAabb(this._aabb);
686712
}
687713

688714
this.lightmapped = this._lightmapped; // update meshInstances

src/scene/batching.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,10 @@ SkinBatchInstance.prototype = Object.create(SkinBatchInstance.prototype);
9898
SkinBatchInstance.prototype.constructor = SkinBatchInstance;
9999

100100
Object.assign(SkinBatchInstance.prototype, {
101-
updateMatrices: function (rootNode) {
101+
updateMatrices: function (rootNode, skinUpdateIndex) {
102102
},
103103

104-
updateMatrixPalette: function () {
104+
updateMatrixPalette: function (rootNode, skinUpdateIndex) {
105105
var pe;
106106
var mp = this.matrixPalette;
107107
var base;

src/scene/forward-renderer.js

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ var skipRenderCamera = null;
117117
var _skipRenderCounter = 0;
118118
var skipRenderAfter = 0;
119119

120+
var _skinUpdateIndex = 0;
121+
120122
// The 8 points of the camera frustum transformed to light space
121123
var frustumPoints = [];
122124
for (var fp = 0; fp < 8; fp++) {
@@ -1180,19 +1182,22 @@ Object.assign(ForwardRenderer.prototype, {
11801182
},
11811183

11821184
updateCpuSkinMatrices: function (drawCalls) {
1185+
1186+
_skinUpdateIndex++;
1187+
11831188
var drawCallsCount = drawCalls.length;
11841189
if (drawCallsCount === 0) return;
11851190

11861191
// #ifdef PROFILER
11871192
var skinTime = now();
11881193
// #endif
11891194

1190-
var i, skin;
1195+
var i, si;
11911196
for (i = 0; i < drawCallsCount; i++) {
1192-
skin = drawCalls[i].skinInstance;
1193-
if (skin) {
1194-
skin.updateMatrices(drawCalls[i].node);
1195-
skin._dirty = true;
1197+
si = drawCalls[i].skinInstance;
1198+
if (si) {
1199+
si.updateMatrices(drawCalls[i].node, _skinUpdateIndex);
1200+
si._dirty = true;
11961201
}
11971202
}
11981203

@@ -1213,7 +1218,7 @@ Object.assign(ForwardRenderer.prototype, {
12131218
skin = drawCalls[i].skinInstance;
12141219
if (skin) {
12151220
if (skin._dirty) {
1216-
skin.updateMatrixPalette();
1221+
skin.updateMatrixPalette(drawCalls[i].node, _skinUpdateIndex);
12171222
skin._dirty = false;
12181223
}
12191224
}

src/scene/mesh-instance.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,8 @@ Object.defineProperty(MeshInstance.prototype, 'skinInstance', {
294294
for (var i = 0; i < this._shader.length; i++) {
295295
this._shader[i] = null;
296296
}
297+
298+
this._setupSkinUpdate();
297299
}
298300
});
299301

@@ -512,6 +514,23 @@ Object.assign(MeshInstance.prototype, {
512514
parameter.scopeId.setValue(parameter.data);
513515
}
514516
}
517+
},
518+
519+
setOverrideAabb: function (aabb) {
520+
this._updateAabb = !aabb;
521+
if (aabb) {
522+
this.aabb.copy(aabb);
523+
}
524+
525+
this._setupSkinUpdate();
526+
},
527+
528+
_setupSkinUpdate: function () {
529+
530+
// set if bones need to be updated before culling
531+
if (this._skinInstance) {
532+
this._skinInstance._updateBeforeCull = this._updateAabb;
533+
}
515534
}
516535
});
517536

src/scene/skin-instance.js

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ var _invMatrix = new Mat4();
1818
function SkinInstance(skin) {
1919
this._dirty = true;
2020

21+
// sequencial index of when the bone update was performed the last time
22+
this._skinUpdateIndex = -1;
23+
24+
// true if bones need to be updated before the frustum culling (bones are needed to update bounds of the MeshInstance)
25+
this._updateBeforeCull = true;
26+
2127
if (skin) {
2228
this.initSkin(skin);
2329
}
@@ -77,16 +83,33 @@ Object.assign(SkinInstance.prototype, {
7783
}
7884
},
7985

80-
updateMatrices: function (rootNode) {
86+
_updateMatrices: function (rootNode, skinUpdateIndex) {
87+
88+
// if not already up to date
89+
if (this._skinUpdateIndex !== skinUpdateIndex) {
90+
this._skinUpdateIndex = skinUpdateIndex;
8191

82-
_invMatrix.copy(rootNode.getWorldTransform()).invert();
83-
for (var i = this.bones.length - 1; i >= 0; i--) {
84-
this.matrices[i].mulAffine2(_invMatrix, this.bones[i].getWorldTransform()); // world space -> rootNode space
85-
this.matrices[i].mulAffine2(this.matrices[i], this.skin.inverseBindPose[i]); // rootNode space -> bind space
92+
_invMatrix.copy(rootNode.getWorldTransform()).invert();
93+
for (var i = this.bones.length - 1; i >= 0; i--) {
94+
this.matrices[i].mulAffine2(_invMatrix, this.bones[i].getWorldTransform()); // world space -> rootNode space
95+
this.matrices[i].mulAffine2(this.matrices[i], this.skin.inverseBindPose[i]); // rootNode space -> bind space
96+
}
8697
}
8798
},
8899

89-
updateMatrixPalette: function () {
100+
updateMatrices: function (rootNode, skinUpdateIndex) {
101+
102+
if (this._updateBeforeCull) {
103+
this._updateMatrices(rootNode, skinUpdateIndex);
104+
}
105+
},
106+
107+
updateMatrixPalette: function (rootNode, skinUpdateIndex) {
108+
109+
// make sure matrices are up to date
110+
this._updateMatrices(rootNode, skinUpdateIndex);
111+
112+
// copy matrices to palette
90113
var pe;
91114
var mp = this.matrixPalette;
92115
var base;

0 commit comments

Comments
 (0)