three.js 截图是空白?看看这里!
前言
目前公司有一个需求,在 three.js 中展示模型,并截图作为模型的预览图。由于 three.js 本身是通过 canvas 画出来的,而 canvas 标签本身就可以输出成图片,本以为是一个鸽鸽下蛋这么简单的事情,但是还是有一些细节在的,于是记录下来,和大家分享一下。
环境
|  |  | 
| node | v18.16.1 | 
| Microsoft Edge | 116.0.1938.62 | 
| three.js | 0.156.1 | 
需要实现的效果
点击页面上的按钮,将three.js当前的画面截取下来,显示在img标签中。
搭建基础框架
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 
 | 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代码不做展示,最终实现了以下效果:

实现截图功能
这里所说的截图功能,实际是将canvas画布上的内容保存下来,可以通过官方的API获得一个包含图片展示的data URI:HTMLCanvasElement.toDataURL() - Web API 接口参考 | MDN (mozilla.org),然后将img标签的src指向它就可以了。
实现:
| 12
 3
 4
 5
 6
 7
 
 | const elButton = document.querySelector("button") as HTMLButtonElement;const elImg = document.querySelector("img") as HTMLImageElement;
 elButton.addEventListener("click", () => {
 const url = canvas.toDataURL();
 elImg.src = url;
 });
 
 
 | 
效果:

可以看到,img标签的内容发生了变化,但是图片并没有正常显示。
preserveDrawingBuffer属性
通过three.js的文档WebGLRenderer – three.js docs (threejs.org)可知,在构造WebGLRenderer时,可传入的参数中又一个preserveDrawingBuffer的属性,three.js对其的解释为:
preserveDrawingBuffer -是否保留缓(存)直到手动清除或被覆盖。 默认false.
也就是说,出于性能考虑,three.js默认是会清除canvas画布上的缓存内容的,那么我们截取的图像自然就是空白的,那我们开启这个选项:
| 12
 3
 4
 5
 6
 7
 8
 
 | ...
 const renderer = new THREE.WebGLRenderer({
 canvas: canvas,
 preserveDrawingBuffer: true,
 });
 
 ...
 
 | 
效果:

可以看到,已经能正常的截取画面了。
优化
three.js 出于性能方面的考虑,默认将preserveDrawingBuffer选项设置为false,说明打开这个选项会造成较大的性能开销。有代码洁癖的诸位看官肯定会忍不住说,不利于性能的代码我们不写!因此,有什么好办法能在关闭这个选项的前提下,也能截取画面呢?
既然three.js会一直清除缓存,那我在截图前,让它重新画一次不就行了?
代码如下:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 
 | ...
 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;
 });
 
 | 
效果:

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