游戏引擎详解——图片

图片

图片的格式

图片文件格式
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函数,获取依赖,再新建一个任务,走下载管线完成实际图片加载。
相关推荐
虾球xz8 小时前
游戏引擎学习第119天
学习·游戏引擎
晴空了无痕15 小时前
现代任务调度系统架构深度解析——以TaskSchedulerController为核心的弹性任务管理方案
unity·系统架构·游戏引擎
虾球xz1 天前
游戏引擎学习第118天
学习·游戏引擎
不吃斋的和尚1 天前
Unity中一个节点实现植物动态(Shader)
unity·游戏引擎
虾球xz1 天前
游戏引擎学习第117天
学习·游戏引擎
千年奇葩1 天前
Unity shader glsl着色器特效之 模拟海面海浪效果
unity·游戏引擎·着色器
太妃糖耶1 天前
Unity摄像机与灯光相关知识
unity·游戏引擎
程序趣谈2 天前
UE5中按钮圆角,设置边框
ue5·游戏引擎
龚子亦2 天前
Unity结合Vuforia虚拟按键实现AR机械仿真动画效果
unity·游戏引擎·ar·数字孪生·虚拟仿真
虾球xz2 天前
游戏引擎学习第115天
学习·游戏引擎