Reputation: 301
Having an instance of WebGLRenderer
created with antialias = true
results in noticeable performance issues as resolution grows, especially on retina displays (window.devicePixelRatio === 2
).
Since it's not possible to change the antialiasing mode on the fly, the question is: how to automatically adjust the pixel ratio trying to keep FPS higher than a certain threshold (e.g. 30)?
Upvotes: 2
Views: 981
Reputation: 301
The idea is to monitor FPS in the rendering loop (by measuring intervals between requestAnimationFrame
calls) and decreasing or increasing DPR accordingly.
"Monitoring" means recording those intervals into an array, removing min/max values (to avoid peaks), taking an average and comparing it with predefined thresholds.
const highFrequencyThreshold = 20; // ~50 FPS
const lowFrequencyThreshold = 34; // ~30 FPS
const minDpr = 0.5;
const maxDpr = window.devicePixelRatio;
const deltaDpr = 0.1;
const relaxPeriod = 4000;
const accumulatorLength = 20;
const frameTimestamp = performance.now();
const frequencyAccumulator = [];
const lastUpdatedAt = null;
const renderer = new WebGLRenderer({
antialias: true,
});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
animate();
function animate(timestamp: number = performance.now()) {
requestAnimationFrame(animate);
monitor(frameTimestamp, timestamp);
frameTimestamp = timestamp;
// animation calculations and rendering
// ...
}
function monitor(frameTimestamp: number, now: number) {
collectFrequency(now - frameTimestamp);
// accumulator is not fully filled
if (frequencyAccumulator.length < accumulatorLength) {
return;
}
// an update happened recently
if (now - lastUpdatedAt < relaxPeriod) {
return;
}
const dpr = renderer.getPixelRatio();
const frequencyMedian = median(...frequencyAccumulator);
if (frequencyMedian > lowFrequencyThreshold && dpr > minDpr) {
updateDpr(dpr, -deltaDpr, now);
} else if (frequencyMedian < highFrequencyThreshold && dpr < maxDpr) {
updateDpr(dpr, deltaDpr, now);
}
}
function collectFrequency(frequency: number) {
if (frequency > 0) {
frequencyAccumulator.push(frequency);
}
if (frequencyAccumulator.length > accumulatorLength) {
frequencyAccumulator.shift();
}
}
function updateDpr(dpr: number, delta: number, now: number) {
renderer.setPixelRatio(dpr + delta);
frequencyAccumulator = [];
lastUpdatedAt = now;
}
function median(...elements: number[]): number {
const indexOfMin = elements.indexOf(Math.min(...elements));
const indexOfMax = elements.indexOf(Math.max(...elements));
const noMinMax = elements.filter((_, index) => index !== indexOfMin && index !== indexOfMax);
return average(...noMinMax);
}
function average(...elements: number[]): number {
return elements.reduce((sum, value) => sum + value, 0) / elements.length;
}
Note that updating the DPR could result in short-time animation freezing.
Also, something more clever could be used for balancing the DPR value (rather than calling the updateDpr()
with linear step of 0.1
), e.g. bisectional search.
Upvotes: 1