本文主要展示“如何使用node开发一个地图集打包工具”。内容简单易懂,条理清晰,希望能帮你解开疑惑。让边肖带领大家学习《如何使用node开发地图集打包工具》一文。
比如把下面的几张图片合成一张.
这个地图集是我用本文介绍的工具打包合成的。
合成的图片质量还是很高的。
00-1010
为什么需要使用图集
在web开发中,每次在浏览器中显示图片时,都需要请求服务器资源。
例如,3次请求每次4k和一次请求12k,之间有着本质的区别,而且更多的时候,请求不是来自3 * 4k.
使用atlas可以优化资源加载,提高网站性能。
web开发
在游戏开发中,图册的使用非常重要。无论是一般的帧动画还是svga这样的动画解决方案,每一张图片都不会要求资源。
更多时候,我们把它们打包到atlas中,atlas打包工具texturepacker更受欢迎。
其次,因为游戏场景太多,我们通常需要一步一步加载资源。有时候,一个动画模型涉及的图片少则十几张,多则一百张。
图集的使用不可或缺.
让我们来看看如何编写一个地图集打包工具。
游戏开发
开发地图集打包工具脚本需要哪些技巧?
node.js的编程能力
二维矩形布局算法
然后我们考虑如何打包地图集。
我们需要找到需要打包的文件夹。可能有多个文件夹或嵌套文件夹。
这本地图集由几张零散的图片组成。
地图集的大小需要配置。
尽可能压缩地图集空间,使每张图片紧密贴合。
每个文件夹都打包成一个地图集,图片太多了,没办法考虑。
可能需要生成atlas所需的json文件来记录图片的位置信息。
工具设计
开始编写脚本
,这是我的设计。
首先,我们需要一个打包对象实例MySpritePackTool,它也支持写配置参数选项。
/* *阿特拉斯包装对象*/
constMySpritePackTool=函数(opt){ 0
this . options={ 0
//文件夹中的图片太多或太长。最大递归次数
maxCount:opt.maxCount||2,
//图册的文件路径需要打包。
assetsPath:opt.assetsPath,
//输出文件路径
outPutPath:opt.outPutPath,
//一个地图包的最大尺寸
maxSize:nb
sp;{ width: 2048, height: 2048 }
}
};
然后我们需要输出这个对象, 可以被其他项目所引用.
module.exports = MySpritePackTool;
遍历文件生成节点树
我们的输入参数尽可能的少, 这样就需要我们程序去遍历文件夹.
例如, 我们有如下的目录树:
|--assets |--index |--img-3.png |--img-4.png |--login |--img-5.png |--img-1.png |--img-2.png
我们需要每个文件夹下打包出一张图集.
思考: 需要什么样的数据结构?
首先便于js解析, 我们约定一个对象,
每一层, 需要一个图片信息容器assets
;
一个所包含的图片标识keys
;
一个文件夹名字, 也方便我们后面为图集命名name
;
然后每层文件夹前套相同对象;
结构如下:
{ assets: [ { id: 'assets/img-1.png', width: 190, height: 187 }, ... ], name: 'assets', keys: 'img-1.png,img-2.png,', index: { assets: [ { id: 'assets/index/img-3.png', width: 190, height: 187 }, ... ], name: 'index', keys: 'img-3.png,img-4.png,' }, login: { assets: [ { id: 'assets/login/img-5.png', width: 190, height: 187 } ], name: 'index', keys: 'img-5.png,' }, }
不难发现, 我们已经可以得到需要打包的所有文件和文件夹.
那么用程序如何实现呢?
主要用到nodejs的fs模块来递归操作文件夹, 并输出所需要的节点树.
注意在书写的时候需要判断是图片还是文件夹.
MySpritePackTool.prototype.findAllFiles = function (obj, rootPath) { let nodeFiles = []; if (fs.existsSync(rootPath)) { //获取所有文件名 nodeFiles = fs.readdirSync(rootPath); //组装对象 let nameArr = rootPath.split('/'); obj["assets"] = []; obj["name"] = nameArr[nameArr.length - 1]; obj["keys"] = ""; nodeFiles.forEach(item => { //判断不是图片路径 if (!/(.png)|(.jpe?g)$/.test(item)) { let newPath = path.join(rootPath, item); //判断存在文件 同时是文件夹系统 if (fs.existsSync(newPath) && fs.statSync(newPath).isDirectory()) { // console.log("获得新的地址", newPath); obj[item] = {}; this.findAllFiles(obj[item], newPath); } else { console.log(`文件路径: ${newPath}不存在!`); } } else { console.log(`图片路径: ${item}`); obj["keys"] += item + ","; let params = {}; params["id"] = path.resolve(rootPath, `./${item}`); //获得图片宽高 params["width"] = images(path.resolve(rootPath, `./${item}`)).width(); params["height"] = images(path.resolve(rootPath, `./${item}`)).height(); obj["assets"].push(params); } }) } else { console.log(`文件路径: ${rootPath}不存在!`); } }
这样子我们就可以得到我们所需要的节点树了.
获取新的图集位置信息
我们对文件夹的操作已经完成, 这时候我们就需要思考.
如何把这些零散的图片打包到一张图片上.
散图有两个信息, 一个width
和一个height
, 其实就是一个矩形.
我们现在所要做的就是把这些不同面积的矩形放到一个具有最大长宽的大矩形中.
跳开图片, 从矩形放置入手
二维矩形装箱算法有不少, 我这里选用一种比较简单的.
首先得到一个具有最大长宽的矩形盒子.
我们先放入一个矩形A, 这样子, 剩余区域就有两块: 矩形A的右边和矩形A的下边.
然后我们继续放入矩形B, 可以先右再下, 然后基于矩形B又有两块空白空间.
依次类推, 我们就可以将合适的矩形全部放入.
举个例子
把左边的散装矩形放入右边的矩形框中, 可以得到:
可以看到, 我们节省了很多空间, 矩形排列紧凑.
如果用代码实现, 是怎么样的呢?
/** * 确定宽高 w h * 空白区域先放一个, 剩下的寻找右边和下边 * 是否有满足右边的, 有则 放入 无则 继续遍历 * 是否有满足下边的, 有则 放入 无则 继续遍历 */ const Packer = function (w, h) { this.root = { x: 0, y: 0, width: w, height: h }; // /** 匹配所有的方格 */ Packer.prototype.fit = function (blocks) { let node; for (let i = 0; i < blocks.length; i++) { let block = blocks[i]; node = this.findNode(this.root, block.width, block.height); if (node) { let fit = this.findEmptyNode(node, block.width, block.height); block.x = fit.x; block.y = fit.y; block.fit = fit; } } } /** 找到可以放入的节点 */ Packer.prototype.findNode = function (node, w, h) { if (node.used) { return this.findNode(node.rightArea, w, h) || this.findNode(node.downArea, w, h); } else if (node.width >= w && node.height >= h) { return node; } else { return null; } } /** 找到空位 */ Packer.prototype.findEmptyNode = function (node, w, h) { //已经使用过的 删除 node.used = true; //右边空间 node.rightArea = { x: node.x + w, y: node.y, width: node.width - w, height: h }; //下方空位 node.downArea = { x: node.x, y: node.y + h, width: node.width, height: node.height - h } return node; } }
使用递归, 代码量很少, 但是功能强大.
但是有一个问题, 如果超出定长定宽, 或者一个矩形装不完, 我们的算法是不会放入到大矩形中的.
这样子就有点不满足我们的图集打包思路了.
所以我们还需要将这个算法改进一下;
加入两个变量, 一个记录使用的总的区域, 一个记录未被装入的矩形.
//记录使用的总的区域 this.usedArea = { width: 0, height: 0 }; //记录未被装入的矩形 this.levelBlocks = [];
详细代码可以查看源码中的packing
.
当然, 这里只是最简单的一种二维装箱算法
还有一种加强版的装箱算法, 我放在源码里了, 这里就不赘述了, 原理基本一致
现在, 我们已经可以将矩形合适的装箱了, 那怎么使用去处理成图集呢?
定义一个dealImgsPacking
方法, 继续去处理我们的节点树.
这里用到了我们的配置项maxCount
, 就是为了一张图集装不完, 多打出几张图集的作用.
然后我们打包出来的图集命名使用文件夹 + 当前是第几张的形式.
`${obj['name'] + (count ? "-" + count : '')}`
具体方法如下:
MySpritePackTool.prototype.dealImgsPacking = function (obj) { let count = 0; if (obj.hasOwnProperty("assets")) { let newBlocks = obj["assets"]; obj["assets"] = []; while (newBlocks.length > 0 && count < this.options.maxCount) { let packer1 = new Packer(this.options.maxSize.width, this.options.maxSize.height); packer1.fit(newBlocks); let sheets1 = { maxArea: packer1.usedArea, atlas: newBlocks, fileName: `${obj['name'] + (count ? "-" + count : '')}` }; newBlocks = packer1.levelBlocks; obj["assets"].push(sheets1); count++; } } for (let item in obj) { if (obj[item].hasOwnProperty("assets")) { this.dealImgsPacking(obj[item]); } } }
通过这个方法我们改造了之前的节点树;
将之前节点树中的assest
变为了一个数组, 每个数组元素代表一张图集信息.
结构如下:
assets: [ { maxArea: { width: 180,height: 340 }, atlas: [ { id: 'assets/index/img-3.png', width: 190, height: 187, x: 0, y: 0 } ], fileName: 'assets' }, ... ]
我们可以清晰的得到, 打包之后的图集, 最大宽高是maxArea
, 每张图宽高位置信息是atlas
,以及图集名称fileName
.
接下来, 就是最后一步了, 绘制新的图片, 并输出图片文件.
注意
我们在使用打包算法的时候, 可以先进行一下基于图片大小的排序
这样以来打包出来的图集会留白更小
图集打包并输出
这里图集的绘制和输出均是使用了node-images
的API;
遍历之前得到的节点树, 首先绘制一张maxArea
大小的空白图像.
images(item["maxArea"].width, item["maxArea"].height)
然后遍历一张图集所需要的图片信息, 将每一张图片绘制到空白图像上.
//绘制空白图像 let newSprites = images(item["maxArea"].width, item["maxArea"].height); //绘制图集 imgObj.forEach(it => { newSprites.draw(images(it["id"]), it["x"], it["y"]); });
然后绘制完一张图集输出一张.
newSprites.save(`${this.options.outPutPath}/${item['fileName']}.png`);
最后对节点树递归调用, 绘制出所有的图集.
具体代码如下:
MySpritePackTool.prototype.drawImages = function (obj) { let count = 0; if (obj.hasOwnProperty("assets")) { //打包出一个或者多个图集 let imgsInfo = obj["assets"]; imgsInfo.forEach(item => { if (item.hasOwnProperty("atlas")) { let imgObj = item["atlas"]; // console.log("8888",imgObj) //绘制一张透明图像 let newSprites = images(item["maxArea"].width, item["maxArea"].height); imgObj.forEach(it => { newSprites.draw(images(it["id"]), it["x"], it["y"]); }); newSprites.save(`${this.options.outPutPath}/${item['fileName']}.png`); count++; } }) } for (let item in obj) { if (obj[item].hasOwnProperty("assets")) { this.drawImages(obj[item]); } } }
这样子, 我们就大功告成了,
运行测试一下, 可以得到如下的图集:
效果还不错.
如何使用
安装
npm i sprites-pack-tool
使用
const MySpritePackTool = require("sprites-pack-tool"); const path = require("path"); /** 打包最多递归次数 */ const MAX_COUNT = 2; //需要合成的图集的路径 const assetsPath = path.resolve(__dirname, "./assets"); /** 图集打包工具配置*/ const mySpritePackTool = new MySpritePackTool({ //一个文件夹图片过多或者过长 递归最大次数 maxCount: MAX_COUNT, //需要打包图集的文件路径 assetsPath: assetsPath, //输出文件路径 outPutPath: path.resolve(__dirname, "./res"), //一张图集打包最大size maxSize: { width: 2048,height: 2048} }); /** 图集打包 */ mySpritePackTool.Pack2Sprite();
以上是“如何使用node开发一款图集打包工具”这篇文章的所有内容,感谢各位的阅读!相信大家都有了一定的了解,希望分享的内容对大家有所帮助,如果还想学习更多知识,欢迎关注行业资讯频道!
内容来源网络,如有侵权,联系删除,本文地址:https://www.230890.com/zhan/126265.html