Cocos Creator 节点截图

2,110 阅读4分钟

前言

最近开发的cocos项目中需要实现截图等需求,第一时间想到的就是canvas,如何将cocos的节点的色彩数据导入到canvas,并将最后的canvas重新生成的图片变成cocos的SpriteFrame就是需要解决的重点,本次项目基于版本2.4.7,2.4以内应该都支持,更高或者更低的版本没有经过验证

图像储存

数字化图像储存数据有两种方式:位图储存和矢量储存。

我们常见的图片都是属于位图储存,canvas绘制的图片也是属于位图。位图图像又称作点阵图像、位映射图像,他是🈶由一系列像素组成的可识别图像,如果把一幅位图图像看成一个数字矩形,那么矩形中的任一元素(即像素)对应于图像中的一个点,而相应的值对应于该点的颜色或者灰度。

例如,一张4px * 4px的彩色图片,未压缩的原始图像数据就是一个4 * 4的矩形网格,每个网格代表一个像素,如果是使用RGB颜色通道,每个像素都有三个通道值,每个颜色通道都是一个0~255的整数值,占用一个字节(Byte)的储存空间,那么一个4px * 4px的图片占用的空间就是4 * 4 * 3 = 48(Byte)。如果是使用RGBA通道,那么除了基础的红、绿、蓝三基色之外,还多了一个A(alpha透明度)的通道,所以占用的空间会更大一些,也就是4 * 4 * 4 = 64(Byte)。

image (1).png

截图原理

那么我们想要用canvas生成一张新的截图,我们就需要获取界面需要截图区域的像素数据。以下是操作步骤

  1. 使用摄像机将界面的图像渲染到一张RenderTexture中,
  2. RenderTexture导出像素数据绘制在Canvas上
  3. Canvas导出图像的Base64 (可供下载)
  4. 将Base64转化成Texture2D(可供游戏内展示)

首先,我们创建一个相机用来绘制想要截图的节点,并且需要将这个相机挂载到场景上

let nodeCamera = new cc.Node();
nodeCamera.parent = cc.find('Canvas');
let camera = nodeCamera.addComponent(cc.Camera);

接着我们获取想要截图节点的位置以及宽高

// 获取当前节点的位置,因为之后截图需要将其移至画面中心,提前记录以便之后挪回来
let position = node.getPosition();
let width = node.width;
let height = node.height;

然后我们来设置摄像机的基础属性:

  • 首先我们不需要把摄像机的视窗大小调整为整个屏幕的大小,只需要取我们想要截图的地方大小
  • 摄像机不需要透视显示,正交模式就可以了
  • orthoSize的值越大,摄像机的视野就会越大,而主体显示的画面就会越小,当值为height/2时可以正好显示截图区域的全部内容(动态修改orthoSize我们可以在游戏内实现缩放的效果)
// 当 alignWithScreen 为 true 的时候,摄像机会自动将视窗大小调整为整个屏幕的大小。如果想要完全自由地控制摄像机,则需要将 alignWithScreen 设置为 false。(v2.2.1 新增)
camera.alignWithScreen = false;
// 设置摄像机的投影模式是正交(true)还是透视(false)模式
camera.ortho = true;
// 摄像机在正交投影模式下的视窗大小。该属性在 alignWithScreen 设置为 false 时生效。
camera.orthoSize = height / 2;

为了避免摄像机直接渲染到屏幕上,我们需要给摄像机一个渲染对象,并且拿到想要的渲染数据,可以创建一个RenderTexture对象赋值给targetTexture

let texture = new cc.RenderTexture();
// 如果截图内容中不包含 Mask 组件,可以不用传递第三个参数
texture.initWithSize(width, height, cc.game['_renderContext'].STENCIL_INDEX8);
// 如果设置了 targetTexture,那么摄像机渲染的内容不会输出到屏幕上,而是会渲染到 targetTexture 上。
camera.targetTexture = texture;

之后我们就可以把节点调整至中央让摄像头渲染,渲染后再回归原位置,再用RenderTexture导出像素数据

node.setPosition(cc.Vec2.ZERO);
// 渲染一次摄像机,即更新一次内容到 RenderTexture 中
camera.render(node);
node.setPosition(position);
// 从 render texture 读取像素数据,数据类型为 RGBA 格式的 Uint8Array 数组。
// 默认每次调用此函数会生成一个大小为 (长 x 高 x 4) 的 Uint8Array。
let data = texture.readPixels();

导出的像素数据借助Canvas,将每个像素点的色值绘制上去,然后再通过Canvas提供的api导出成Base64进行下一步的处理

// 创建画布
let canvas =
  typeof lynx !== 'undefined' ? lynx.createOffscreenCanvas(width, height) : document.createElement('canvas');
canvas.width = width;
canvas.height = height; 
let ctx = canvas.getContext('2d')
// PNG 中 1 像素 = 32 bit(RGBA),1 byte = 8 bit,所以 1 像素 = 4 byte
// 每行 width 像素,即 width * 4 字节
const rowBytes = width * 4;
for (let row = 0; row < height; row++) {
  // RenderTexture 得到的纹理是上下翻转的,所以绘制需要翻转过来
  const srow = height - 1 - row;
  const imageData = ctx.createImageData(width, 1);
  const start = srow * width * 4;
  for (let i = 0; i < rowBytes; i++) {
    imageData.data[i] = data[start + i];
  }
  ctx.putImageData(imageData, 0, row);
}
// 得出Base64数据
const dataURL = canvas.toDataURL('image/jpeg');

下载截图

在Dom环境下,下载截图可以通过a标签进行,代码很简单,如下所示:

export const downloadImg = (node: cc.Node) => {
  // 封装的截图函数,导出成Base64
  const dataUrl = screenshot(node);
  let a = document.createElement("a");
  a.href = dataUrl;
  a.download = "Image.png";
  a.click();
};

截图再展示

cc.Texture2D支持用 HTML Image或Canvas对象初始化贴图,我们可以使用Image对象初始化成贴图,然后创建节点展示在场景中,代码如下:

export const showTexture = (node: cc.Node): Promise<cc.SpriteFrame> => {
  const dataUrl = screenshot(node);
  let img = document.createElement("img");
  img.src = dataUrl;
  img.width = node.width;
  img.height = node.height;

  return new Promise((resolve) => {
    img.onload = () => {
      let texture2 = new cc.Texture2D();
      texture2.initWithElement(img);

      let sf = new cc.SpriteFrame();
      sf.setTexture(texture2);
      resolve(sf);
    };
  });
};

项目地址

github.com/Mrlgm/cocos…