优化 javascript canvas 大量绘制微小物体
Optimise javascript canvas for mass-drawing of tiny objects
我一直在开发一款游戏,该游戏需要每帧渲染和旋转数千张非常小的图像 (20^20 px)。提供了示例片段。
我已经使用了所有我知道的技巧来加速它以提高帧速率,但我怀疑我可以做其他事情来优化它。
当前的优化包括:
- 用显式转换替换 save/restore
- 避免scale/size-transformations
- 明确目标大小而不是让浏览器猜测
- requestAnimationFrame 而不是 set-interval
已尝试但未出现在示例中:
- 将对象分批渲染到其他屏幕外 canvases 然后稍后编译(降低性能)
- 避免浮点位置(放置精度要求)
- 不在主 canvas 上使用 alpha(由于 SO 片段呈现,片段中未显示)
//initial canvas and context
var canvas = document.getElementById('canvas');
canvas.width = 800;
canvas.height = 800;
var ctx = canvas.getContext('2d');
//create an image (I) to render
let myImage = new OffscreenCanvas(10,10);
let myImageCtx = myImage.getContext('2d');
myImageCtx.fillRect(0,2.5,10,5);
myImageCtx.fillRect(0,0,2.5,10);
myImageCtx.fillRect(7.5,0,2.5,10);
//animation
let animation = requestAnimationFrame(frame);
//fill an initial array of [n] object positions and angles
let myObjects = [];
for (let i = 0; i <1500; i++){
myObjects.push({
x : Math.floor(Math.random() * 800),
y : Math.floor(Math.random() * 800),
angle : Math.floor(Math.random() * 360),
});
}
//render a specific frame
function frame(){
ctx.clearRect(0,0,canvas.width, canvas.height);
//draw each object and update its position
for (let i = 0, l = myObjects.length; i<l;i++){
drawImageNoReset(ctx, myImage, myObjects[i].x, myObjects[i].y, myObjects[i].angle);
myObjects[i].x += 1; if (myObjects[i].x > 800) {myObjects[i].x = 0}
myObjects[i].y += .5; if (myObjects[i].y > 800) {myObjects[i].y = 0}
myObjects[i].angle += .01; if (myObjects[i].angle > 360) {myObjects[i].angle = 0}
}
//reset the transform and call next frame
ctx.setTransform(1, 0, 0, 1, 0, 0);
requestAnimationFrame(frame);
}
//fastest transform draw method - no transform reset
function drawImageNoReset(myCtx, image, x, y, rotation) {
myCtx.setTransform(1, 0, 0, 1, x, y);
myCtx.rotate(rotation);
myCtx.drawImage(image, 0,0,image.width, image.height,-image.width / 2, -image.height / 2, image.width, image.height);
}
<canvas name = "canvas" id = "canvas"></canvas>
您已经非常接近使用 2D API 和单线程的最大吞吐量,但是还有一些小问题可以提高性能。
WebGL2
首先,如果您希望使用 javascript 获得最佳性能,则必须使用 WebGL
与 2D API 相比,使用 WebGL2 可以绘制 8 倍或更多的 2D 精灵,并且具有更大范围的 FX(例如颜色、阴影、凹凸、单次调用智能平铺贴图... )
WebGL 非常值得努力
性能相关点
globalAlpha
应用于每个 drawImage
调用,1 以外的值不影响性能。
避免调用 rotate
这两个数学调用(包括比例)比 rotate
快一点点。例如 ax = Math..cos(rot) * scale; ay = Math.sin(rot) * scale; ctx.setTransform(ax,ay,-ay,ax,x,y)
不要使用很多图像,而是将所有图像放在一个图像中(sprite sheet)。不适用于这种情况
不要乱扔全局范围。使对象尽可能靠近函数范围并通过引用传递对象。访问全局范围变量比局部范围变量慢得多。
最好使用模块,因为它们有自己的本地作用域
使用弧度。将角度转换为度数再返回是浪费处理时间。学习使用弧度 Math.PI * 2 === 360
Math.PI === 180
等等
对于正整数,不要使用 Math.floor
使用 bit-wise 运算符,因为它们会自动将 Doubles 转换为 Int32,例如 Math.floor(Math.random() * 800)
比 [=22= 更快](|
是或)
注意正在使用的数字类型。如果每次使用它都将其转换回双精度,则转换为整数将花费周期。
尽可能Pre-calculate。例如,每次渲染图像时,您都会否定并划分宽度和高度。这些值可以预先计算。
避免数组查找(索引)。索引数组中的对象比直接引用慢。例如,主循环索引 myObject
11 次。使用 for of
循环,因此每次迭代只有一次数组查找,并且计数器是一个性能更高的内部计数器。 (参见示例)
虽然这会降低性能,但如果您在较慢的渲染设备上分离更新和渲染循环,您将获得性能提升,方法是为每个渲染帧更新游戏状态两次。例如,如果您两次检测到此更新状态并渲染一次,则慢速渲染设备会降至 30FPS 并且游戏会减慢一半速度。游戏仍将以 30FPS 的速度呈现,但仍会以正常速度播放(甚至可能会在渲染负载减半时保存偶尔掉帧的情况)
不要试图使用增量时间,这会带来一些负面的性能开销(对于许多可以是 Ints 的值,强制加倍)并且实际上会降低动画质量。
尽可能避免条件分支,或使用性能更高的替代方案。 EG 在您的示例中,您使用 if 语句跨边界循环对象。这可以使用余数运算符 %
(参见示例)
来完成
你检查rotation > 360
。这不是必需的,因为旋转是循环的。值 360 与 44444160 相同。(Math.PI * 2
与 Math.PI * 246912
的旋转相同)
非性能点。
您正在为下一次(即将到来的)显示刷新准备帧的每个动画调用。在您的代码中,您正在显示游戏状态然后更新。这意味着您的游戏状态比客户看到的早一帧。始终更新状态,然后显示。
例子
这个例子给对象增加了一些额外的负载
- 任何方向都可以
- 有独立的速度和旋转
- 不要在边缘进出眨眼。
该示例包含一个实用程序,它试图通过改变对象的数量来平衡帧速率。
(工作)负载每 15 帧更新一次。最终它会达到一个稳定的速度。
不要通过 运行 宁这个片段来衡量性能,所以片段位于 运行 页面的所有代码之下,代码也被修改和监控(以防止无限循环)。您看到的代码不是代码段中 运行 的代码。只需移动鼠标即可导致 SO 片段中出现数十帧丢帧
为了获得准确的结果,请复制代码并 运行 将其单独放在页面上(删除测试时浏览器上可能存在的任何扩展)
使用此方法或类似方法定期测试您的代码并帮助您获得经验,了解什么对性能有利和不利。
费率文本的含义。
- 1 +/- 下一个时期添加或删除的对象数
- 2前期每帧渲染的对象总数
- 3 数字 运行 渲染时间的平均值(以毫秒为单位)(这不是帧速率)
- 4 数字 FPS 是最佳平均帧率。
- 5 期间丢失的帧数。丢帧是报告的帧速率的长度。 IE。
"30fps 5dropped"
五个丢帧都是30fps,丢帧的总时间是5 * (1000 / 30)
const IMAGE_SIZE = 10;
const IMAGE_DIAGONAL = (IMAGE_SIZE ** 2 * 2) ** 0.5 / 2;
const DISPLAY_WIDTH = 800;
const DISPLAY_HEIGHT = 800;
const DISPLAY_OFFSET_WIDTH = DISPLAY_WIDTH + IMAGE_DIAGONAL * 2;
const DISPLAY_OFFSET_HEIGHT = DISPLAY_HEIGHT + IMAGE_DIAGONAL * 2;
const PERFORMANCE_SAMPLE_INTERVAL = 15; // rendered frames
const INIT_OBJ_COUNT = 500;
const MAX_CPU_COST = 8; // in ms
const MAX_ADD_OBJ = 10;
const MAX_REMOVE_OBJ = 5;
canvas.width = DISPLAY_WIDTH;
canvas.height = DISPLAY_HEIGHT;
requestAnimationFrame(start);
function createImage() {
const image = new OffscreenCanvas(IMAGE_SIZE,IMAGE_SIZE);
const ctx = image.getContext('2d');
ctx.fillRect(0, IMAGE_SIZE / 4, IMAGE_SIZE, IMAGE_SIZE / 2);
ctx.fillRect(0, 0, IMAGE_SIZE / 4, IMAGE_SIZE);
ctx.fillRect(IMAGE_SIZE * (3/4), 0, IMAGE_SIZE / 4, IMAGE_SIZE);
image.neg_half_width = -IMAGE_SIZE / 2; // snake case to ensure future proof (no name clash)
image.neg_half_height = -IMAGE_SIZE / 2; // use of Image API
return image;
}
function createObject() {
return {
x : Math.random() * DISPLAY_WIDTH,
y : Math.random() * DISPLAY_HEIGHT,
r : Math.random() * Math.PI * 2,
dx: (Math.random() - 0.5) * 2,
dy: (Math.random() - 0.5) * 2,
dr: (Math.random() - 0.5) * 0.1,
};
}
function createObjects() {
const objects = [];
var i = INIT_OBJ_COUNT;
while (i--) { objects.push(createObject()) }
return objects;
}
function update(objects){
for (const obj of objects) {
obj.x = ((obj.x + DISPLAY_OFFSET_WIDTH + obj.dx) % DISPLAY_OFFSET_WIDTH);
obj.y = ((obj.y + DISPLAY_OFFSET_HEIGHT + obj.dy) % DISPLAY_OFFSET_HEIGHT);
obj.r += obj.dr;
}
}
function render(ctx, img, objects){
for (const obj of objects) { drawImage(ctx, img, obj) }
}
function drawImage(ctx, image, {x, y, r}) {
const ax = Math.cos(r), ay = Math.sin(r);
ctx.setTransform(ax, ay, -ay, ax, x - IMAGE_DIAGONAL, y - IMAGE_DIAGONAL);
ctx.drawImage(image, image.neg_half_width, image.neg_half_height);
}
function timing(framesPerTick) { // creates a running mean frame time
const samples = [0,0,0,0,0,0,0,0,0,0];
const sCount = samples.length;
var samplePos = 0;
var now = performance.now();
const maxRate = framesPerTick * (1000 / 60);
const API = {
get FPS() {
var time = performance.now();
const FPS = 1000 / ((time - now) / framesPerTick);
const dropped = ((time - now) - maxRate) / (1000 / 60) | 0;
now = time;
if (FPS > 30) { return "60fps " + dropped + "dropped" };
if (FPS > 20) { return "30fps " + (dropped / 2 | 0) + "dropped" };
if (FPS > 15) { return "20fps " + (dropped / 3 | 0) + "dropped" };
if (FPS > 10) { return "15fps " + (dropped / 4 | 0) + "dropped" };
return "Too slow";
},
time(time) { samples[(samplePos++) % sCount] = time },
get mean() { return samples.reduce((total, val) => total += val, 0) / sCount },
};
return API;
}
function updateStats(CPUCost, objects) {
const fps = CPUCost.FPS;
const mean = CPUCost.mean;
const cost = mean / objects.length; // estimate per object CPU cost
const count = MAX_CPU_COST / cost | 0;
const objCount = objects.length;
var str = "0";
if (count < objects.length) {
var remove = Math.min(MAX_REMOVE_OBJ, objects.length - count);
str = "-" + remove;
objects.length -= remove;
} else if (count > objects.length + MAX_ADD_OBJ) {
let i = MAX_ADD_OBJ;
while (i--) {
objects.push(createObject());
}
str = "+" + MAX_ADD_OBJ;
}
info.textContent = str + ": " + objCount + " sprites " + mean.toFixed(3) + "ms " + fps;
}
function start() {
var frameCount = 0;
const CPUCost = timing(PERFORMANCE_SAMPLE_INTERVAL);
const ctx = canvas.getContext('2d');
const image = createImage();
const objects = createObjects();
function frame(time) {
frameCount ++;
const start = performance.now();
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, DISPLAY_WIDTH, DISPLAY_WIDTH);
update(objects);
render(ctx, image, objects);
requestAnimationFrame(frame);
CPUCost.time(performance.now() - start);
if (frameCount % PERFORMANCE_SAMPLE_INTERVAL === 0) {
updateStats(CPUCost, objects);
}
}
requestAnimationFrame(frame);
}
#info {
position: absolute;
top: 10px;
left: 10px;
background: #DDD;
font-family: arial;
font-size: 18px;
}
<canvas name = "canvas" id = "canvas"></canvas>
<div id="info"></div>
我一直在开发一款游戏,该游戏需要每帧渲染和旋转数千张非常小的图像 (20^20 px)。提供了示例片段。
我已经使用了所有我知道的技巧来加速它以提高帧速率,但我怀疑我可以做其他事情来优化它。
当前的优化包括:
- 用显式转换替换 save/restore
- 避免scale/size-transformations
- 明确目标大小而不是让浏览器猜测
- requestAnimationFrame 而不是 set-interval
已尝试但未出现在示例中:
- 将对象分批渲染到其他屏幕外 canvases 然后稍后编译(降低性能)
- 避免浮点位置(放置精度要求)
- 不在主 canvas 上使用 alpha(由于 SO 片段呈现,片段中未显示)
//initial canvas and context
var canvas = document.getElementById('canvas');
canvas.width = 800;
canvas.height = 800;
var ctx = canvas.getContext('2d');
//create an image (I) to render
let myImage = new OffscreenCanvas(10,10);
let myImageCtx = myImage.getContext('2d');
myImageCtx.fillRect(0,2.5,10,5);
myImageCtx.fillRect(0,0,2.5,10);
myImageCtx.fillRect(7.5,0,2.5,10);
//animation
let animation = requestAnimationFrame(frame);
//fill an initial array of [n] object positions and angles
let myObjects = [];
for (let i = 0; i <1500; i++){
myObjects.push({
x : Math.floor(Math.random() * 800),
y : Math.floor(Math.random() * 800),
angle : Math.floor(Math.random() * 360),
});
}
//render a specific frame
function frame(){
ctx.clearRect(0,0,canvas.width, canvas.height);
//draw each object and update its position
for (let i = 0, l = myObjects.length; i<l;i++){
drawImageNoReset(ctx, myImage, myObjects[i].x, myObjects[i].y, myObjects[i].angle);
myObjects[i].x += 1; if (myObjects[i].x > 800) {myObjects[i].x = 0}
myObjects[i].y += .5; if (myObjects[i].y > 800) {myObjects[i].y = 0}
myObjects[i].angle += .01; if (myObjects[i].angle > 360) {myObjects[i].angle = 0}
}
//reset the transform and call next frame
ctx.setTransform(1, 0, 0, 1, 0, 0);
requestAnimationFrame(frame);
}
//fastest transform draw method - no transform reset
function drawImageNoReset(myCtx, image, x, y, rotation) {
myCtx.setTransform(1, 0, 0, 1, x, y);
myCtx.rotate(rotation);
myCtx.drawImage(image, 0,0,image.width, image.height,-image.width / 2, -image.height / 2, image.width, image.height);
}
<canvas name = "canvas" id = "canvas"></canvas>
您已经非常接近使用 2D API 和单线程的最大吞吐量,但是还有一些小问题可以提高性能。
WebGL2
首先,如果您希望使用 javascript 获得最佳性能,则必须使用 WebGL
与 2D API 相比,使用 WebGL2 可以绘制 8 倍或更多的 2D 精灵,并且具有更大范围的 FX(例如颜色、阴影、凹凸、单次调用智能平铺贴图... )
WebGL 非常值得努力
性能相关点
globalAlpha
应用于每个drawImage
调用,1 以外的值不影响性能。避免调用
rotate
这两个数学调用(包括比例)比rotate
快一点点。例如ax = Math..cos(rot) * scale; ay = Math.sin(rot) * scale; ctx.setTransform(ax,ay,-ay,ax,x,y)
不要使用很多图像,而是将所有图像放在一个图像中(sprite sheet)。不适用于这种情况
不要乱扔全局范围。使对象尽可能靠近函数范围并通过引用传递对象。访问全局范围变量比局部范围变量慢得多。
最好使用模块,因为它们有自己的本地作用域
使用弧度。将角度转换为度数再返回是浪费处理时间。学习使用弧度
Math.PI * 2 === 360
Math.PI === 180
等等对于正整数,不要使用
Math.floor
使用 bit-wise 运算符,因为它们会自动将 Doubles 转换为 Int32,例如Math.floor(Math.random() * 800)
比 [=22= 更快](|
是或)注意正在使用的数字类型。如果每次使用它都将其转换回双精度,则转换为整数将花费周期。
尽可能Pre-calculate。例如,每次渲染图像时,您都会否定并划分宽度和高度。这些值可以预先计算。
避免数组查找(索引)。索引数组中的对象比直接引用慢。例如,主循环索引
myObject
11 次。使用for of
循环,因此每次迭代只有一次数组查找,并且计数器是一个性能更高的内部计数器。 (参见示例)虽然这会降低性能,但如果您在较慢的渲染设备上分离更新和渲染循环,您将获得性能提升,方法是为每个渲染帧更新游戏状态两次。例如,如果您两次检测到此更新状态并渲染一次,则慢速渲染设备会降至 30FPS 并且游戏会减慢一半速度。游戏仍将以 30FPS 的速度呈现,但仍会以正常速度播放(甚至可能会在渲染负载减半时保存偶尔掉帧的情况)
不要试图使用增量时间,这会带来一些负面的性能开销(对于许多可以是 Ints 的值,强制加倍)并且实际上会降低动画质量。
尽可能避免条件分支,或使用性能更高的替代方案。 EG 在您的示例中,您使用 if 语句跨边界循环对象。这可以使用余数运算符
来完成%
(参见示例)你检查
rotation > 360
。这不是必需的,因为旋转是循环的。值 360 与 44444160 相同。(Math.PI * 2
与Math.PI * 246912
的旋转相同)
非性能点。
您正在为下一次(即将到来的)显示刷新准备帧的每个动画调用。在您的代码中,您正在显示游戏状态然后更新。这意味着您的游戏状态比客户看到的早一帧。始终更新状态,然后显示。
例子
这个例子给对象增加了一些额外的负载
- 任何方向都可以
- 有独立的速度和旋转
- 不要在边缘进出眨眼。
该示例包含一个实用程序,它试图通过改变对象的数量来平衡帧速率。
(工作)负载每 15 帧更新一次。最终它会达到一个稳定的速度。
不要通过 运行 宁这个片段来衡量性能,所以片段位于 运行 页面的所有代码之下,代码也被修改和监控(以防止无限循环)。您看到的代码不是代码段中 运行 的代码。只需移动鼠标即可导致 SO 片段中出现数十帧丢帧
为了获得准确的结果,请复制代码并 运行 将其单独放在页面上(删除测试时浏览器上可能存在的任何扩展)
使用此方法或类似方法定期测试您的代码并帮助您获得经验,了解什么对性能有利和不利。
费率文本的含义。
- 1 +/- 下一个时期添加或删除的对象数
- 2前期每帧渲染的对象总数
- 3 数字 运行 渲染时间的平均值(以毫秒为单位)(这不是帧速率)
- 4 数字 FPS 是最佳平均帧率。
- 5 期间丢失的帧数。丢帧是报告的帧速率的长度。 IE。
"30fps 5dropped"
五个丢帧都是30fps,丢帧的总时间是5 * (1000 / 30)
const IMAGE_SIZE = 10;
const IMAGE_DIAGONAL = (IMAGE_SIZE ** 2 * 2) ** 0.5 / 2;
const DISPLAY_WIDTH = 800;
const DISPLAY_HEIGHT = 800;
const DISPLAY_OFFSET_WIDTH = DISPLAY_WIDTH + IMAGE_DIAGONAL * 2;
const DISPLAY_OFFSET_HEIGHT = DISPLAY_HEIGHT + IMAGE_DIAGONAL * 2;
const PERFORMANCE_SAMPLE_INTERVAL = 15; // rendered frames
const INIT_OBJ_COUNT = 500;
const MAX_CPU_COST = 8; // in ms
const MAX_ADD_OBJ = 10;
const MAX_REMOVE_OBJ = 5;
canvas.width = DISPLAY_WIDTH;
canvas.height = DISPLAY_HEIGHT;
requestAnimationFrame(start);
function createImage() {
const image = new OffscreenCanvas(IMAGE_SIZE,IMAGE_SIZE);
const ctx = image.getContext('2d');
ctx.fillRect(0, IMAGE_SIZE / 4, IMAGE_SIZE, IMAGE_SIZE / 2);
ctx.fillRect(0, 0, IMAGE_SIZE / 4, IMAGE_SIZE);
ctx.fillRect(IMAGE_SIZE * (3/4), 0, IMAGE_SIZE / 4, IMAGE_SIZE);
image.neg_half_width = -IMAGE_SIZE / 2; // snake case to ensure future proof (no name clash)
image.neg_half_height = -IMAGE_SIZE / 2; // use of Image API
return image;
}
function createObject() {
return {
x : Math.random() * DISPLAY_WIDTH,
y : Math.random() * DISPLAY_HEIGHT,
r : Math.random() * Math.PI * 2,
dx: (Math.random() - 0.5) * 2,
dy: (Math.random() - 0.5) * 2,
dr: (Math.random() - 0.5) * 0.1,
};
}
function createObjects() {
const objects = [];
var i = INIT_OBJ_COUNT;
while (i--) { objects.push(createObject()) }
return objects;
}
function update(objects){
for (const obj of objects) {
obj.x = ((obj.x + DISPLAY_OFFSET_WIDTH + obj.dx) % DISPLAY_OFFSET_WIDTH);
obj.y = ((obj.y + DISPLAY_OFFSET_HEIGHT + obj.dy) % DISPLAY_OFFSET_HEIGHT);
obj.r += obj.dr;
}
}
function render(ctx, img, objects){
for (const obj of objects) { drawImage(ctx, img, obj) }
}
function drawImage(ctx, image, {x, y, r}) {
const ax = Math.cos(r), ay = Math.sin(r);
ctx.setTransform(ax, ay, -ay, ax, x - IMAGE_DIAGONAL, y - IMAGE_DIAGONAL);
ctx.drawImage(image, image.neg_half_width, image.neg_half_height);
}
function timing(framesPerTick) { // creates a running mean frame time
const samples = [0,0,0,0,0,0,0,0,0,0];
const sCount = samples.length;
var samplePos = 0;
var now = performance.now();
const maxRate = framesPerTick * (1000 / 60);
const API = {
get FPS() {
var time = performance.now();
const FPS = 1000 / ((time - now) / framesPerTick);
const dropped = ((time - now) - maxRate) / (1000 / 60) | 0;
now = time;
if (FPS > 30) { return "60fps " + dropped + "dropped" };
if (FPS > 20) { return "30fps " + (dropped / 2 | 0) + "dropped" };
if (FPS > 15) { return "20fps " + (dropped / 3 | 0) + "dropped" };
if (FPS > 10) { return "15fps " + (dropped / 4 | 0) + "dropped" };
return "Too slow";
},
time(time) { samples[(samplePos++) % sCount] = time },
get mean() { return samples.reduce((total, val) => total += val, 0) / sCount },
};
return API;
}
function updateStats(CPUCost, objects) {
const fps = CPUCost.FPS;
const mean = CPUCost.mean;
const cost = mean / objects.length; // estimate per object CPU cost
const count = MAX_CPU_COST / cost | 0;
const objCount = objects.length;
var str = "0";
if (count < objects.length) {
var remove = Math.min(MAX_REMOVE_OBJ, objects.length - count);
str = "-" + remove;
objects.length -= remove;
} else if (count > objects.length + MAX_ADD_OBJ) {
let i = MAX_ADD_OBJ;
while (i--) {
objects.push(createObject());
}
str = "+" + MAX_ADD_OBJ;
}
info.textContent = str + ": " + objCount + " sprites " + mean.toFixed(3) + "ms " + fps;
}
function start() {
var frameCount = 0;
const CPUCost = timing(PERFORMANCE_SAMPLE_INTERVAL);
const ctx = canvas.getContext('2d');
const image = createImage();
const objects = createObjects();
function frame(time) {
frameCount ++;
const start = performance.now();
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, DISPLAY_WIDTH, DISPLAY_WIDTH);
update(objects);
render(ctx, image, objects);
requestAnimationFrame(frame);
CPUCost.time(performance.now() - start);
if (frameCount % PERFORMANCE_SAMPLE_INTERVAL === 0) {
updateStats(CPUCost, objects);
}
}
requestAnimationFrame(frame);
}
#info {
position: absolute;
top: 10px;
left: 10px;
background: #DDD;
font-family: arial;
font-size: 18px;
}
<canvas name = "canvas" id = "canvas"></canvas>
<div id="info"></div>