three.js 截图是空白?看看这里!

395 阅读2分钟

three.js 截图是空白?看看这里!

前言

目前公司有一个需求,在 three.js 中展示模型,并截图作为模型的预览图。由于 three.js 本身是通过 canvas 画出来的,而 canvas 标签本身就可以输出成图片,本以为是一个鸽鸽下蛋这么简单的事情,但是还是有一些细节在的,于是记录下来,和大家分享一下。

环境

nodev18.16.1
Microsoft Edge116.0.1938.62
three.js0.156.1

需要实现的效果

点击页面上的按钮,将three.js当前的画面截取下来,显示在img标签中。

搭建基础框架

import * as THREE from "three";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";

const canvas = document.querySelector("canvas.webgl") as HTMLCanvasElement;

const scene = new THREE.Scene();

const renderer = new THREE.WebGLRenderer({
  canvas: canvas,
});
renderer.setSize(300, 300, false);

const camera = new THREE.PerspectiveCamera(75, 300 / 300, 0.1, 1000);
camera.position.z = 3;
scene.add(camera);

const light = new THREE.AmbientLight(0xffffff, 2);
scene.add(light);

const directionLight = new THREE.DirectionalLight(0xffffff, 2);
directionLight.position.set(0, 0, 2);
scene.add(directionLight);

const controls = new OrbitControls(camera, canvas);
controls.enableDamping = true;

const gltfLoader = new GLTFLoader();
gltfLoader.load("/DamagedHelmet.glb", (gltf) => {
  scene.add(gltf.scene);
});

const tick = () => {
  requestAnimationFrame(tick);
  controls.update();
  renderer.render(scene, camera);
};

tick();

css代码不做展示,最终实现了以下效果:

image.png

实现截图功能

这里所说的截图功能,实际是将canvas画布上的内容保存下来,可以通过官方的API获得一个包含图片展示的data URI:HTMLCanvasElement.toDataURL() - Web API 接口参考 | MDN (mozilla.org),然后将img标签的src指向它就可以了。

实现:

const elButton = document.querySelector("button") as HTMLButtonElement;
const elImg = document.querySelector("img") as HTMLImageElement;
elButton.addEventListener("click", () => {
  const url = canvas.toDataURL();
  elImg.src = url;
});

效果:

Kapture 2023-09-28 at 14.17.57.gif

可以看到,img标签的内容发生了变化,但是图片并没有正常显示。

preserveDrawingBuffer属性

通过three.js的文档WebGLRenderer – three.js docs (threejs.org)可知,在构造WebGLRenderer时,可传入的参数中又一个preserveDrawingBuffer的属性,three.js对其的解释为:

preserveDrawingBuffer -是否保留缓(存)直到手动清除或被覆盖。 默认false.

也就是说,出于性能考虑,three.js默认是会清除canvas画布上的缓存内容的,那么我们截取的图像自然就是空白的,那我们开启这个选项:

...

const renderer = new THREE.WebGLRenderer({
  canvas: canvas,
  preserveDrawingBuffer: true,
});

...

效果:

Kapture 2023-09-28 at 14.25.23.gif

可以看到,已经能正常的截取画面了。

优化

three.js 出于性能方面的考虑,默认将preserveDrawingBuffer选项设置为false,说明打开这个选项会造成较大的性能开销。有代码洁癖的诸位看官肯定会忍不住说,不利于性能的代码我们不写!因此,有什么好办法能在关闭这个选项的前提下,也能截取画面呢?

既然three.js会一直清除缓存,那我在截图前,让它重新画一次不就行了?

代码如下:

...

const renderer = new THREE.WebGLRenderer({
  canvas: canvas,
});
renderer.setSize(300, 300, false);

...

elButton.addEventListener("click", () => {

  renderer.render(scene, camera);     //关键代码
  
  const url = canvas.toDataURL();
  elImg.src = url;
});

效果:

Kapture 2023-09-28 at 14.31.03.gif

可以看到,同样实现了截图的功能。

后记

没想到three.js截图还有这么些细节在里面,笔者也只是抛砖引玉,如果各位看官有更好的解决方式,欢迎交流。