游戏引擎详解——图片

图片

图片的格式

图片文件格式
png
jpg
纹理压缩格式
ETC1/2
PVRTC
ASTC

图片的属性

图片属性 解释
分辨率 宽高像素值(pt),如:1024*1024
位深度 用来存储像素颜色的值,如RGBA8888,红黄蓝透明度4个维度每个8bit,一共就是32位,一般使用的就是32位,也被称为真彩色
文件大小 文件所占用的存储大小

图片的优化

图片的优化分为两种:

    1. 文件大小优化:这种优化会影响到包大小,较小的图片大小对于手机存储容量和网络传输速度和时间会更友好。

一般的优化方式是使用压缩工具如pngquant、tinypng等,直接压缩文件大小。

    1. 图片纹理优化:

图片文件大小压缩,不意味着读到内存中的大小会减少。

一般情况下,以ARPG8888来说,计算一个1024*1024分辨率的图片读到内存中的大小,计算公式为:

1024 * 1024 * 4 * 8 = 33,554,432 bit = 4,194,304 byte ≈ 4 mb

理解起来就是 1024 * 1024 个像素,每个像素有 argb 4个通道,每个通道含有 8 bit数据用来存储颜色值。

png格式不能直接被GPU识别,需要在cpu把图片读进内存中解码后,再传递给gpu使用,这样做会造成一定的cpu消耗和很大的瞬时运行内存(RAM)占用。

因为大部分gpu对于压缩后的纹理有比较好的支持,无需cpu解码,占用内存小。于是我们要寻找一种合适的纹理压缩方式。

  • etc1不支持透明通道。
  • etc2效果很差,容易出现色块。
  • PVRTC仅能在ios上使用.

这时候astc格式就展露在我们眼前,IOS和安卓端都对astc有较好的支持率。

  • iPhone6及iPad mini 4以上iOS设备支持。
  • 大多数支持OpenGL ES 3.1或Vulkan的现代Android GPU也支持ASTC格式,其中包括:自Adreno 4xx / Snapdragon 415(2015年)起的高通GPU,自Mali T624(2012年)起的ARM GPU,自Tegra K1(2014年)起的NVIDIA GPU,以及自GX6250(2014年)起的PowerVR GPU。

在运行内存中来说,astc的内存使用能节省75%左右,提升巨大。

cocoscreator中源码理解

creator引擎在编译时会把选中的纹理压缩类型记录到import文件夹下的json文件中,多种图片格式以下划线'_'区分开,下图演示里选择了astc6x6格式和webp格式,所以生成的类型就是

代码中在读取文件格式时,会根据列举的文件类型判断当前机器是否支持。由下面代码图里能够看到,7@34这里的7代表的就是第7个.astc,4代表的就是第4个.webp。若当前设备不支持astc就会去寻找webp格式。

cocoscreator中源码整个下载流程的理解(建议配合cocos2.4.11源码享用)

  1. 一切的一切都要从bundle.js这个类说起,类中的load函数会调用cc.assetManager.loadAny函数来下载相关资源并获取资源数据返回。

    ,传递的参数是资源名paths,类型type,进度回调函数onProgress,完成回调函数onComplete。

  2. cc.assetManager是CCAssetManager类,这个类里有一个loadAny函数,此函数简单封装了一个Task并把task传递到pipeline中。这里的pipeline是定义在shared.js中的new Pipeline('normal load', [])一个名字叫做normal load的管线,异步执行任务,管线填充在CCAssetManager类中pipeline.append(preprocess).append(load);包含一个preprocess和load方法。

    task的状态为:
    {
    input: 资源名,
    onProgress: onProgress,
    onComplete: onComplete,
    options: {
    preset: 'default',
    requestType: 'path',
    type: type,
    bundle: bundle名,
    outputAsArray: false,
    }
    }

  1. normal load--pipeline先调用preprocess,这个函数里对Task的options属性做遍历,这里也就是preset、requestType 、type、bundle和__outputAsArray__五个key。
    requestType 、type、bundle和preset键值对保存到subOptions对象中,__outputAsArray__和preset保存到leftOptions对象中,并覆盖掉task的options属性。

新建一个subTask,input为task.input,options为subOptions对象,走transformPipeline管线流程同步执行任务,获取的值设置为task.source和task.output。最后调用done()。

task的状态为:
{
	input: 资源名,
	onProgress: onProgress,
	onComplete: onComplete,
	options: {
		preset: 'default',
		__outputAsArray__: false,
	},
	output: ,
	source: ,
}
subTask的状态为:
{
	input: 资源名,
	options: {
		preset: 'default',
		__requestType__: 'path',
		type: type,
		bundle: bundle名,
	}
}
  1. transformPipeline里包含两个函数parse和combine。
  • parse管线任务,先判断input,若input为字符串,新建一个无原型链对象item,把item对象的【options.__requestType__值】,也就是【RequestType.PATH】,也就是【'path'】设置为input值,把options中的其他键值对都复制到item中,这里包含四个键值对,requestType 、type、bundle和path。
    再遍历item,这里只有path这个key有处理,其他key都过滤了,通过bundle的_config找item.path值的info。这里的item.path也就是我们一开始传进来的资源名。info里包含了资源的uuid。
    把config、info和uuid值填入到out对象中,并把out对象push到task.output中。
    管线会把task.output值赋值给task.input。

    subTask的状态为:
    {
    options: {
    preset: 'default',
    requestType: 'path',
    type: type,
    bundle: bundle名,
    },
    input: [{
    config: bundle所有配置,
    uuid: 资源的uuid,
    info: {path: path, uuid: uuid},
    ext: '.json',
    }]
    }

    源码展示:
    function parse (task) {

      var input = task.input, options = task.options;
      input = Array.isArray(input) ? input : [ input ];
    
      task.output = [];
      for (var i = 0; i < input.length; i ++ ) {
          var item = input[i];
          var out = RequestItem.create();
          if (typeof item === 'string') {
              item = Object.create(null);
              item[options.__requestType__ || RequestType.UUID] = input[i];
          }
          if (typeof item === 'object') {
              // local options will overlap glabal options
              cc.js.addon(item, options);
              if (item.preset) {
                  cc.js.addon(item, cc.assetManager.presets[item.preset]);
              }
              for (var key in item) {
                  switch (key) {
                      case RequestType.UUID: 
                          var uuid = out.uuid = decodeUuid(item.uuid);
                          if (bundles.has(item.bundle)) {
                              var config = bundles.get(item.bundle)._config;
                              var info = config.getAssetInfo(uuid);
                              if (info && info.redirect) {
                                  if (!bundles.has(info.redirect)) throw new Error(`Please load bundle ${info.redirect} first`);
                                  config = bundles.get(info.redirect)._config;
                                  info = config.getAssetInfo(uuid);
                              }
                              out.config = config;
                              out.info = info;
                          }
                          out.ext = item.ext || '.json';
                          break;
                      case '__requestType__':
                      case 'ext': 
                      case 'bundle':
                      case 'preset':
                      case 'type': break;
                      case RequestType.DIR: 
                          if (bundles.has(item.bundle)) {
                              var infos = [];
                              bundles.get(item.bundle)._config.getDirWithPath(item.dir, item.type, infos);
                              for (let i = 0, l = infos.length; i < l; i++) {
                                  var info = infos[i];
                                  input.push({uuid: info.uuid, __isNative__: false, ext: '.json', bundle: item.bundle});
                              }
                          }
                          out.recycle();
                          out = null;
                          break;
                      case RequestType.PATH: 
                          if (bundles.has(item.bundle)) {
                              var config = bundles.get(item.bundle)._config;
                              var info = config.getInfoWithPath(item.path, item.type);
                              
                              if (info && info.redirect) {
                                  if (!bundles.has(info.redirect)) throw new Error(`you need to load bundle ${info.redirect} first`);
                                  config = bundles.get(info.redirect)._config;
                                  info = config.getAssetInfo(info.uuid);
                              }
    
                              if (!info) {
                                  out.recycle();
                                  throw new Error(`Bundle ${item.bundle} doesn't contain ${item.path}`);
                              }
                              out.config = config; 
                              out.uuid = info.uuid;
                              out.info = info;
                          }
                          out.ext = item.ext || '.json';
                          break;
                      case RequestType.SCENE:
                          if (bundles.has(item.bundle)) {
                              var config = bundles.get(item.bundle)._config;
                              var info = config.getSceneInfo(item.scene);
                              
                              if (info && info.redirect) {
                                  if (!bundles.has(info.redirect)) throw new Error(`you need to load bundle ${info.redirect} first`);
                                  config = bundles.get(info.redirect)._config;
                                  info = config.getAssetInfo(info.uuid);
                              }
                              if (!info) {
                                  out.recycle();
                                  throw new Error(`Bundle ${config.name} doesn't contain scene ${item.scene}`);
                              }
                              out.config = config; 
                              out.uuid = info.uuid;
                              out.info = info;
                          }
                          break;
                      case '__isNative__': 
                          out.isNative = item.__isNative__;
                          break;
                      case RequestType.URL: 
                          out.url = item.url;
                          out.uuid = item.uuid || item.url;
                          out.ext = item.ext || cc.path.extname(item.url);
                          out.isNative = item.__isNative__ !== undefined ? item.__isNative__ : true;
                          break;
                      default: out.options[key] = item[key];
                  }
                  if (!out) break;
              }
          }
          if (!out) continue;
          task.output.push(out);
          if (!out.uuid && !out.url) throw new Error('Can not parse this input:' + JSON.stringify(item));
      }
      return null;
    

    }

  • combine管线任务,主要是拼出来完整的资源地址。

    subTask的状态为:
    {
    options: {
    preset: 'default',
    requestType: 'path',
    type: type,
    bundle: bundle名,
    },
    output: [{
    config: bundle所有配置,
    uuid: 资源的uuid,
    info: {path: path, uuid: uuid},
    ext: '.json',
    url: 具体地址,
    }]
    }
    task的状态为:
    {
    input: 资源名,
    onProgress: onProgress,
    onComplete: onComplete,
    options: {
    preset: 'default',
    outputAsArray: false,
    },
    output: [{
    config: bundle所有配置,
    uuid: 资源的uuid,
    info: 具体信息,
    ext: '.json',
    url: 具体地址,
    }],
    source: [{
    config: bundle所有配置,
    uuid: 资源的uuid,
    info: 具体信息,
    ext: '.json',
    url: 具体地址,
    }],
    }

    源码展示:
    function combine (task) {
    var input = task.output = task.input;
    for (var i = 0; i < input.length; i++) {
    var item = input[i];
    if (item.url) continue;

          var url = '', base = '';
          var config = item.config;
          if (item.isNative) {
              base = (config && config.nativeBase) ? (config.base + config.nativeBase) : cc.assetManager.generalNativeBase;
          } 
          else {
              base = (config && config.importBase) ? (config.base + config.importBase) : cc.assetManager.generalImportBase;
          }
    
          let uuid = item.uuid;
              
          var ver = '';
          if (item.info) {
              if (item.isNative) {
                  ver = item.info.nativeVer ? ('.' + item.info.nativeVer) : '';
              }
              else {
                  ver = item.info.ver ? ('.' + item.info.ver) : '';
              }
          }
    
          // ugly hack, WeChat does not support loading font likes 'myfont.dw213.ttf'. So append hash to directory
          if (item.ext === '.ttf') {
              url = `${base}/${uuid.slice(0, 2)}/${uuid}${ver}/${item.options.__nativeName__}`;
          }
          else {
              url = `${base}/${uuid.slice(0, 2)}/${uuid}${ver}${item.ext}`;
          }
          
          item.url = url;
      }
      return null;
    

    }

  • 管线运行完毕,返回subTask.output

  1. 回到normal load管线任务中的preprocess任务,transformPipeline返回了对象设置成task的output和source值。把output的值赋值给input,再把output设为null,继续下一个normal load管线任务load。

    task的状态为:
    {
    onProgress: onProgress,
    onComplete: onComplete,
    options: {
    preset: 'default',
    outputAsArray: false,
    },
    input: [{
    config: bundle所有配置,
    uuid: 资源的uuid,
    info: {path: path, uuid: uuid},
    ext: '.json',
    url: 具体地址,
    }],
    source: [{
    config: bundle所有配置,
    uuid: 资源的uuid,
    info: 具体信息,
    ext: '.json',
    url: 具体地址,
    }],
    }

  2. load函数里给task.options新增了progress和__exclude__字段,progress用来记录资源下载进度。然后遍历每一个task.input,对每一个需要load的资源新建了一个subTask,input值为每一个task的input值,onProgress为task.onProgress,options为task.options,progress为task.progress,onComplete为新的函数,完成时调用,更新progress中的值。再把这个subTask传递到loadOneAssetPipeline下载管道中。

    subTask的状态为:
    {
    input: {
    config: bundle所有配置,
    uuid: 资源的uuid,
    info: {path: path, uuid: uuid},
    ext: '.json',
    url: 具体地址,
    },
    onProgress: onProgress,
    onComplete: newComplete,
    options: {
    preset: 'default',
    outputAsArray: false,
    exclude: {},
    },
    }
    task的状态为:
    {
    onProgress: onProgress,
    onComplete: onComplete,
    progress : { finish: 0, total: task.input.length, canInvoke: true },
    options: {
    preset: 'default',
    outputAsArray: false,
    exclude: {},
    },
    input: [{
    config: bundle所有配置,
    uuid: 资源的uuid,
    info: 具体信息,
    ext: '.json',
    url: 具体地址,
    }],
    source: [{
    config: bundle所有配置,
    uuid: 资源的uuid,
    info: 具体信息,
    ext: '.json',
    url: 具体地址,
    }],
    }

    源码展示:
    function load (task, done) {

     let firstTask = false;
     if (!task.progress) {
         task.progress = { finish: 0, total: task.input.length, canInvoke: true };
         firstTask = true;
     }
     
     var options = task.options, progress = task.progress;
    
     options.__exclude__ = options.__exclude__ || Object.create(null);
    
     task.output = [];
     
     forEach(task.input, function (item, cb) {
         let subTask = Task.create({ 
             input: item, 
             onProgress: task.onProgress, 
             options, 
             progress, 
             onComplete: function (err, item) {
                 if (err && !task.isFinish) {
                     if (!cc.assetManager.force || firstTask) {
                         if (!CC_EDITOR) {
                             cc.error(err.message, err.stack);
                         }
                         progress.canInvoke = false;
                         done(err);
                     }
                     else {
                         progress.canInvoke && task.dispatch('progress', ++progress.finish, progress.total, item);
                     }
                 }
                 task.output.push(item);
                 subTask.recycle();
                 cb();
             }
         });
    
         loadOneAssetPipeline.async(subTask);
    
     }, function () {
    
         options.__exclude__ = null;
    
         if (task.isFinish) {
             clear(task, true);
             return task.dispatch('error');
         }
    
         gatherAsset(task);
         clear(task, true);
         done();
     });
    

    }

  3. loadOneAssetPipeline下载单资源管线包含两个管线任务,fetch和parse。

  4. fetch函数调用packManager.load获取资源数据。packManager.load先判断是否在files缓存中有无此id。由于我们在加载资源前必定已经加载了fgui的bin文件,而我们都是选择合并json类型,而bin文件下载时已经把依赖的json文件下载过了,文件值都缓存到files中,这里就直接拿到json信息了,返回值赋值给task的file变量,也就是包含资源类型的json串。进入下一个管线任务parse。parse函数中对未缓存的uuid资源调用parser.parse函数,传递file值。其中又调用parser.parseImport函数,传递file和options;继续调用deserialize函数,传递file和options;继续调用cc.deserialize也就是deserialize-compiled文件中的deserialize函数,传递file和options;继续调用parseInstances;继续调用deserializeCustomCCObject;继续调用对象的_deserialize函数;这里图片对象调用的就是CCTexture2D的_deserialize函数;继续调用Texture2D._parseExt函数;这个_parseExt函数里就做了判断,对资源类型进行解析,并判断设备是否支持纹理类型,支持就返回该后缀。假设这里返回.png后缀,设置Texture2D的_native属性为.png,再设置其他属性,如过滤方案,并返回一个CCTexture2D对象。

  • 这里会衍生到图片的其他属性值,原串是这样的,eg: "0,9729,9729,33071,33071,0,0,1"
  • 第一个0:表示图片的纹理类型,0就是第0个.png。其他类型如下['.png', '.jpg', '.jpeg', '.bmp', '.webp', '.pvr', '.pkm', '.astc'],
  • 第二个9729:表示纹理缩小时设定的纹理过滤方案。const GL_NEAREST = 9728;const GL_LINEAR = 9729; 9728是最近点过滤,采样时选择最近的点,成本小,但易产生锯齿。9729是线性过滤,由4个颜色进行加权,这种方法可以让纹理边缘看起来更平滑,但需要进行更多的计算。
  • 第三个9729:表示纹理放大时设定的纹理过滤方案。同上。
  • 第四个33071:表示横轴纹理环绕方式。指定了当纹理坐标超出0到1的标准范围时该如何处理纹理的采样。10497是重复、33071是边缘拉伸、33648是镜像重复。
  • 第五个33071:表示纵轴纹理环绕方式。同上。
  • 第六个0:表示颜色是否预乘,1表示预乘,0表示非预乘。
  • 第七个0:表示是否是否mipmaps。1表示使用,0表示不使用。
  • 第八个1:表示纹理是否参与合图。1表示参与,0表示不参与。
  1. parser.parse完成后调用loadDepends函数,先为Texture2D添加addRef,然后调用getDepends函数,获取依赖,再新建一个任务,走下载管线完成实际图片加载。
相关推荐
我要吐泡泡了哦1 小时前
GAMES104:15 游戏引擎的玩法系统基础-学习笔记
笔记·学习·游戏引擎
躺下睡觉~10 小时前
Unity-Transform类-父子关系
java·unity·游戏引擎
躺下睡觉~10 小时前
Unity-Transform类-缩放和看向
unity·游戏引擎
君莫愁。12 小时前
【Unity】检测鼠标点击位置是否有2D对象
unity·c#·游戏引擎
咩咩觉主13 小时前
Unity实战案例全解析:PVZ 植物卡片状态分析
unity·c#·游戏引擎
蓝裕安16 小时前
伪工厂模式制造敌人
开发语言·unity·游戏引擎
谢泽浩20 小时前
Unity 给模型贴上照片
unity·游戏引擎
z2014z20 小时前
Unity Resource System 优化笔记
unity·游戏引擎
王维志20 小时前
Unity 高亮插件HighlightPlus介绍
unity·游戏引擎
孑么2 天前
C# 委托与事件 观察者模式
开发语言·unity·c#·游戏引擎·游戏程序