Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Share textures across models in a limited but useful case #5051

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 44 additions & 3 deletions Source/Scene/Model.js
Original file line number Diff line number Diff line change
Expand Up @@ -2234,6 +2234,8 @@ define([

///////////////////////////////////////////////////////////////////////////

var resourcesCachedAcrossModels = {};
Copy link
Contributor

@lilleyse lilleyse Mar 7, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency with other cached resources, should this instead be stored somewhere like context.cache.modelTextureCache?


function createTexture(gltfTexture, model, context) {
var textures = model.gltf.textures;
var texture = textures[gltfTexture.id];
Expand All @@ -2249,7 +2251,7 @@ define([
(sampler.minificationFilter === TextureMinificationFilter.NEAREST_MIPMAP_LINEAR) ||
(sampler.minificationFilter === TextureMinificationFilter.LINEAR_MIPMAP_NEAREST) ||
(sampler.minificationFilter === TextureMinificationFilter.LINEAR_MIPMAP_LINEAR));
var requiresNpot = mipmap ||
var requiresPowerOfTwo = mipmap ||
(sampler.wrapS === TextureWrap.REPEAT) ||
(sampler.wrapS === TextureWrap.MIRRORED_REPEAT) ||
(sampler.wrapT === TextureWrap.REPEAT) ||
Expand All @@ -2270,9 +2272,31 @@ define([
sampler : sampler
});
} else if (defined(source)) {
var cacheKey;

// Only try to cache textures created from an image with a reasonably short src URL.
// i.e. not data URIs.
if (defined(source.src) && source.src.length < 1024) {
cacheKey = JSON.stringify([
context.id,
requiresPowerOfTwo,
source.src,
texture.internalFormat,
texture.type,
sampler
]);

var cachedTexture = resourcesCachedAcrossModels[cacheKey];
if (defined(cachedTexture)) {
++cachedTexture._referenceCount;
model._rendererResources.textures[gltfTexture.id] = cachedTexture;
return;
}
}

var npot = !CesiumMath.isPowerOfTwo(source.width) || !CesiumMath.isPowerOfTwo(source.height);

if (requiresNpot && npot) {
if (requiresPowerOfTwo && npot) {
// WebGL requires power-of-two texture dimensions for mipmapping and REPEAT/MIRRORED_REPEAT wrap modes.
var canvas = document.createElement('canvas');
canvas.width = CesiumMath.nextPowerOfTwo(source.width);
Expand All @@ -2297,6 +2321,12 @@ define([
if (mipmap) {
tx.generateMipmap();
}

if (defined(cacheKey)) {
tx._referenceCount = 1;
tx._cacheKey = cacheKey;
resourcesCachedAcrossModels[cacheKey] = tx;
}
}

model._rendererResources.textures[gltfTexture.id] = tx;
Expand Down Expand Up @@ -4215,7 +4245,18 @@ define([
function destroy(property) {
for (var name in property) {
if (property.hasOwnProperty(name)) {
property[name].destroy();
var resource = property[name];
if (defined(resource._referenceCount)) {
--resource._referenceCount;
if (resource._referenceCount === 0) {
if (defined(resource._cacheKey)) {
delete resourcesCachedAcrossModels[resource._cacheKey];
}
resource.destroy();
}
} else {
property[name].destroy();
}
}
}
}
Expand Down
92 changes: 92 additions & 0 deletions Specs/Scene/ModelSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -922,6 +922,98 @@ defineSuite([
});
});

it('frees a texture that is only used once', function() {
return loadModel(texturedBoxSeparateUrl, { incrementallyLoadTextures : false }).then(function(m) {
expect(m._cachedRendererResources).toBeDefined();
expect(m._cachedRendererResources.textures).toBeDefined();
expect(Object.keys(m._cachedRendererResources.textures).length).toBe(1);

var texture = m._cachedRendererResources.textures[Object.keys(m._cachedRendererResources.textures)[0]];

scene.primitives.remove(m);
expect(texture.isDestroyed()).toBe(true);
});
});

function loadTwoCopiesOfModel(url) {
return loadJson(url).then(function(modelJson) {
var model1 = primitives.add(new Model({
modelMatrix : Transforms.eastNorthUpToFixedFrame(Cartesian3.fromDegrees(0.0, 0.0, 100.0)),
gltf : modelJson,
id : '1',
show : false,
incrementallyLoadTextures : false,
basePath : './Data/Models/Box-Textured-Separate/'
}));

var model2 = primitives.add(new Model({
modelMatrix : Transforms.eastNorthUpToFixedFrame(Cartesian3.fromDegrees(1.0, 0.0, 100.0)),
gltf : modelJson,
id : '2',
show : false,
incrementallyLoadTextures : false,
basePath : './Data/Models/Box-Textured-Separate/'
}));

return pollToPromise(function() {
// Render scene to progressively load the model
scene.renderForSpecs();
return model1.ready && model2.ready;
}, { timeout: 10000 }).then(function() {
return {
model1: model1,
model2: model2
};
});
});
}

it('shares common textures between models', function() {
return loadTwoCopiesOfModel(texturedBoxSeparateUrl).then(function(models) {
var model1 = models.model1;
var model2 = models.model2;

var texture1 = model1._cachedRendererResources.textures[Object.keys(model1._cachedRendererResources.textures)[0]];
var texture2 = model2._cachedRendererResources.textures[Object.keys(model2._cachedRendererResources.textures)[0]];
expect(texture1).toBe(texture2);

scene.primitives.remove(model1);
scene.primitives.remove(model2);
});
});

it('does not destroy shared texture when the first model that uses it is destroyed', function() {
return loadTwoCopiesOfModel(texturedBoxSeparateUrl).then(function(models) {
var model1 = models.model1;
var model2 = models.model2;

var texture1 = model1._cachedRendererResources.textures[Object.keys(model1._cachedRendererResources.textures)[0]];
var texture2 = model1._cachedRendererResources.textures[Object.keys(model1._cachedRendererResources.textures)[0]];

scene.primitives.remove(model1);
expect(texture1.isDestroyed()).toBe(false);
expect(texture2.isDestroyed()).toBe(false);

scene.primitives.remove(model2);
});
});

it('destroys shared texture after all models that use it are destroyed', function() {
return loadTwoCopiesOfModel(texturedBoxSeparateUrl).then(function(models) {
var model1 = models.model1;
var model2 = models.model2;

var texture1 = model1._cachedRendererResources.textures[Object.keys(model1._cachedRendererResources.textures)[0]];
var texture2 = model1._cachedRendererResources.textures[Object.keys(model1._cachedRendererResources.textures)[0]];

scene.primitives.remove(model1);
scene.primitives.remove(model2);

expect(texture1.isDestroyed()).toBe(true);
expect(texture2.isDestroyed()).toBe(true);
});
});

///////////////////////////////////////////////////////////////////////////

it('loads cesiumAir', function() {
Expand Down