优化JavaScript画布以大规模绘制微小对象

如何解决优化JavaScript画布以大规模绘制微小对象

我一直在开发一款游戏,该游戏需要渲染数千个非常小的图像(20 ^ 20 px),并每帧旋转一次。提供了一个示例代码段。

我已经使用了我所知道的每一个技巧来加速它以提高帧速率,但是我怀疑我还有其他方法可以优化它。

当前的优化包括:

  • 用显式转换替换保存/恢复
  • 避免比例/大小转换
  • 明确说明目标大小,而不是让浏览器猜测
  • requestAnimationFrame而不是set-interval

已尝试但未出现在示例中:

  • 将对象批量渲染到其他屏幕外画布,然后再编译(降低性能)
  • 避免浮点位置(由于放置精度而需要)
  • 不在主画布上使用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,10);
myImageCtx.fillRect(7.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,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,1,0);
  requestAnimationFrame(frame);
}

//fastest transform draw method - no transform reset
function drawImageNoReset(myCtx,image,x,y,rotation) {
    myCtx.setTransform(1,y);
    myCtx.rotate(rotation);
    myCtx.drawImage(image,image.width,image.height,-image.width / 2,-image.height / 2,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)

  • 而不是使用许多图像,而是将所有图像放置在一个图像(子画面)中。在这种情况下不适用

  • 不要乱扔全局范围。使对象尽可能靠近函数范围,并按引用传递对象。访问全局范围的变量要比本地范围的变量慢得多。

    最好使用的模块,因为它们具有自己的本地作用域

  • 使用弧度。将角度转换为度和向度都将浪费处理时间。学习使用弧度Math.PI * 2 === 360 Math.PI === 180,依此类推

  • 对于正整数,请不要使用Math.floor,因为它们会自动将Doubles转换为Int32,因此请使用按位运算符,例如Math.floor(Math.random() * 800)的速度比Math.random() * 800 | 0({{1} }是OR)

    请注意所使用的数字类型。如果每次使用整数都将其转换回两倍,则转换为整数将需要花费周期。

  • 始终尽可能进行预计算。例如,每次渲染图像时,将否定并划分宽度和高度。这些值可以预先计算。

  • 避免数组查找(索引)。索引数组中的对象比直接引用慢。例如,主循环索引|有11次。使用myObject循环,以便每次迭代仅进行一次数组查找,并且该计数器是性能更高的内部计数器。 (请参见示例)

  • 尽管这样做会降低性能,但如果在较慢的渲染设备上分离更新和渲染循环,则可以通过为每个渲染帧更新两次游戏状态来获得性能。例如,如果您两次检测到此更新状态,则缓慢的渲染设备降至30FPS,游戏速度降至一半速度,然后渲染一次。游戏仍然会以30FPS的速度呈现,但仍会以正常的速度播放(甚至在您将渲染负载减半的情况下,甚至还可以保存偶尔的下垂帧)

    不要试图使用增量时间,这会带来一些负面的性能开销(对于许多可以为Ints的值,Force会加倍),并且实际上会降低动画质量。

  • 尽可能避免条件分支,或使用性能更高的替代方法。在您的示例中,例如,EG使用if语句跨边界循环对象。可以使用余数运算符for of(请参见示例)

    您选中%。不需要,因为旋转是循环的。360的值与44444160相同。(rotation > 360Math.PI * 2的旋转相同)

非性能点。

每个动画调用都为下一(即将发生的)显示刷新准备框架。在您的代码中,您将显示游戏状态,然后进行更新。这意味着您的游戏状态比客户看到的要早一帧。始终更新状态,然后显示。

示例

此示例为对象添加了一些额外的负担

  • 可以朝任何方向
  • 具有各自的速度和旋转度
  • 不要在边缘眨眼。

该示例包括一个实用程序,该实用程序尝试通过更改对象数量来平衡帧速率。

每180帧会更新一次加载。最终它将达到稳定的速度。

不要通过运行此代码段来评估性能,因此,这些代码段位于运行该页面的所有代码下,还对代码进行了修改和监视(以防止无限循环)。您看到的代码不是在代码段中运行的代码。仅移动鼠标会在SO代码段中导致数十帧丢失

要获得准确的结果,请复制代码并在页面上单独运行(删除测试时浏览器上的所有扩展名)

使用此代码或类似代码定期测试您的代码,并帮助您获得了解性能优缺点的经验。

价目表文字的含义。

  1. +/-为下一个周期添加或删除的对象
  2. 上一期间每帧渲染的对象总数
  3. 数字渲染时间的运行平均值(以毫秒为单位)(不是帧速率)
  4. 数字FPS是最佳的平均帧速率。
  5. 期间删除的数字帧。丢帧是所报告帧速率的长度。即Math.PI * 246912五个丢帧为30fps,丢帧的总时间为"30fps 5dropped"

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 = 180;  // rendered frames
const INIT_OBJ_COUNT = 500;
const MAX_CPU_COST = 8; // in ms
const MAX_ADD_OBJ = 50;

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,IMAGE_SIZE);
    ctx.fillRect(IMAGE_SIZE * (3/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,obj) }
}
function drawImage(ctx,image,{x,y,r}) {
    const ax = Math.cos(r),ay = Math.sin(r);
    ctx.setTransform(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];
    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) {
        str = "" + (count - objects.length);
        objects.length = count;
    } 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,1,0);
        ctx.clearRect(0,DISPLAY_WIDTH,DISPLAY_WIDTH);
        update(objects);
        render(ctx,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;
}

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


依赖报错 idea导入项目后依赖报错,解决方案:https://blog.csdn.net/weixin_42420249/article/details/81191861 依赖版本报错:更换其他版本 无法下载依赖可参考:https://blog.csdn.net/weixin_42628809/a
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下 2021-12-03 13:33:33.927 ERROR 7228 [ main] o.s.b.d.LoggingFailureAnalysisReporter : *************************** APPL
错误1:gradle项目控制台输出为乱码 # 解决方案:https://blog.csdn.net/weixin_43501566/article/details/112482302 # 在gradle-wrapper.properties 添加以下内容 org.gradle.jvmargs=-Df
错误还原:在查询的过程中,传入的workType为0时,该条件不起作用 &lt;select id=&quot;xxx&quot;&gt; SELECT di.id, di.name, di.work_type, di.updated... &lt;where&gt; &lt;if test=&qu
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct redisServer’没有名为‘server_cpulist’的成员 redisSetCpuAffinity(server.server_cpulist); ^ server.c: 在函数‘hasActiveC
解决方案1 1、改项目中.idea/workspace.xml配置文件,增加dynamic.classpath参数 2、搜索PropertiesComponent,添加如下 &lt;property name=&quot;dynamic.classpath&quot; value=&quot;tru
删除根组件app.vue中的默认代码后报错:Module Error (from ./node_modules/eslint-loader/index.js): 解决方案:关闭ESlint代码检测,在项目根目录创建vue.config.js,在文件中添加 module.exports = { lin
查看spark默认的python版本 [root@master day27]# pyspark /home/software/spark-2.3.4-bin-hadoop2.7/conf/spark-env.sh: line 2: /usr/local/hadoop/bin/hadoop: No s
使用本地python环境可以成功执行 import pandas as pd import matplotlib.pyplot as plt # 设置字体 plt.rcParams[&#39;font.sans-serif&#39;] = [&#39;SimHei&#39;] # 能正确显示负号 p
错误1:Request method ‘DELETE‘ not supported 错误还原:controller层有一个接口,访问该接口时报错:Request method ‘DELETE‘ not supported 错误原因:没有接收到前端传入的参数,修改为如下 参考 错误2:cannot r
错误1:启动docker镜像时报错:Error response from daemon: driver failed programming external connectivity on endpoint quirky_allen 解决方法:重启docker -&gt; systemctl r
错误1:private field ‘xxx‘ is never assigned 按Altʾnter快捷键,选择第2项 参考:https://blog.csdn.net/shi_hong_fei_hei/article/details/88814070 错误2:启动时报错,不能找到主启动类 #
报错如下,通过源不能下载,最后警告pip需升级版本 Requirement already satisfied: pip in c:\users\ychen\appdata\local\programs\python\python310\lib\site-packages (22.0.4) Coll
错误1:maven打包报错 错误还原:使用maven打包项目时报错如下 [ERROR] Failed to execute goal org.apache.maven.plugins:maven-resources-plugin:3.2.0:resources (default-resources)
错误1:服务调用时报错 服务消费者模块assess通过openFeign调用服务提供者模块hires 如下为服务提供者模块hires的控制层接口 @RestController @RequestMapping(&quot;/hires&quot;) public class FeignControl
错误1:运行项目后报如下错误 解决方案 报错2:Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project sb 解决方案:在pom.
参考 错误原因 过滤器或拦截器在生效时,redisTemplate还没有注入 解决方案:在注入容器时就生效 @Component //项目运行时就注入Spring容器 public class RedisBean { @Resource private RedisTemplate&lt;String
使用vite构建项目报错 C:\Users\ychen\work&gt;npm init @vitejs/app @vitejs/create-app is deprecated, use npm init vite instead C:\Users\ychen\AppData\Local\npm-