Skip to content
Open
100 changes: 85 additions & 15 deletions src/scene/skin-instance.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class SkinInstance {
* matrices to generate the final matrix palette.
*/
constructor(skin) {

this._dirty = true;

// optional root bone - used for cache lookup, not used for skinning
Expand All @@ -44,6 +45,16 @@ class SkinInstance {
// true if bones need to be updated before the frustum culling (bones are needed to update bounds of the MeshInstance)
this._updateBeforeCull = true;

// ring buffer size. Multiple slots are used instead of just one to
// avoid GPU stalls during asynchronous read/write operations.
this._numTexturesInRing = 1;

// current ring buffer texture index
this._currentRingIndex = 0;

// ring buffer
this._boneTextureRingBuffer = [];

if (skin) {
this.initSkin(skin);
}
Expand All @@ -57,33 +68,92 @@ class SkinInstance {
return this._rootBone;
}

init(device, numBones) {
set numTexturesInRing(value) {
this._numTexturesInRing = value;
this._resizeBoneTextureRingBuffer(value);
}

// texture size - roughly square that fits all bones, width is multiply of 3 to simplify shader math
const numPixels = numBones * 3;
let width = Math.ceil(Math.sqrt(numPixels));
width = math.roundUp(width, 3);
const height = Math.ceil(numPixels / width);
get numTexturesInRing() {
return this._numTexturesInRing;
}

get boneTexture() {
return this._boneTextureRingBuffer[this._currentRingIndex];
}

_nextTexture() {
this._currentRingIndex = (this._currentRingIndex + 1) % this._boneTextureRingBuffer.length;
return this._boneTextureRingBuffer[this._currentRingIndex];
}

_createBoneTexture(device, width, height, data) {

this.boneTexture = new Texture(device, {
return new Texture(device, {
width: width,
height: height,
format: PIXELFORMAT_RGBA32F,
mipmaps: false,
minFilter: FILTER_NEAREST,
magFilter: FILTER_NEAREST,
name: 'skin'
name: 'skin',
numLevels: data ? 1 : undefined,
levels: data ? [data] : undefined
});
}

_resizeBoneTextureRingBuffer(size) {

Debug.assert(size > 0, 'Bone texture ring buffer size must be more than 0');
Debug.assert(this._boneTextureRingBuffer.length > 0, 'Bone texture ring buffer is empty');

const currentLength = this._boneTextureRingBuffer.length;

if (currentLength > size) {

this.matrixPalette = this.boneTexture.lock({ mode: TEXTURELOCK_READ });
this.boneTexture.unlock();
for (let i = size; i < currentLength; i++) {
this._boneTextureRingBuffer[i]?.destroy();
}

this._boneTextureRingBuffer.length = size;

} else if (currentLength < size) {

const mainTexture = this._boneTextureRingBuffer[0];
const device = mainTexture.device;
const width = mainTexture.width;
const height = mainTexture.height;
const data = this.matrixPalette;

for (let i = currentLength; i < size; i++) {
const newTexture = this._createBoneTexture(device, width, height, data);
this._boneTextureRingBuffer.push(newTexture);
}
}
}

destroy() {
init(device, numBones) {

// texture size - roughly square that fits all bones, width is multiply of 3 to simplify shader math
const numPixels = numBones * 3;
let width = Math.ceil(Math.sqrt(numPixels));
width = math.roundUp(width, 3);
const height = Math.ceil(numPixels / width);

const mainBoneTexture = this._createBoneTexture(device, width, height);

this.matrixPalette = mainBoneTexture.lock({ mode: TEXTURELOCK_READ });

if (this.boneTexture) {
this.boneTexture.destroy();
this.boneTexture = null;
mainBoneTexture.unlock();

this._boneTextureRingBuffer.push(mainBoneTexture);

this._resizeBoneTextureRingBuffer(this._numTexturesInRing);
}

destroy() {
if (this._boneTextureRingBuffer.length) {
this._boneTextureRingBuffer.map(x => x?.destroy());
this._boneTextureRingBuffer.length = 0;
}
}

Expand Down Expand Up @@ -137,7 +207,7 @@ class SkinInstance {
}

uploadBones(device) {
this.boneTexture.upload();
this._nextTexture().upload();
}

_updateMatrices(rootNode, skinUpdateIndex) {
Expand Down