在three.js中加载字节形式的点云数据并利用Draco加快渲染速度
文章目录
基于利用Draco对点云数据进行编码解码以实现高效网络传输一文介绍个人项目中关于three.js和Draco的使用实践。
threejs渲染
在 three.js
的官方说明中对于对于点云数据的加载主要是通过PCDLoader,其官方的使用说明如下,十分简洁。
// instantiate a loader
const loader = new PCDLoader();
// load a resource
loader.load(
// resource URL
'pointcloud.pcd',
// called when the resource is loaded
function(points) {
scene.add(points);
},
// called when loading is in progress
function(xhr) {
console.log((xhr.loaded / xhr.total * 100) + '% loaded');
},
// called when loading has errors
function(error) {
console.log('An error happened');
}
);
上述API是通过直接加载pcd
点云文件的方式实现的,而我们常规的使用场景是pcd
文件在服务器端,需要在客户端读取并渲染,涉及到字节流的传输,此时客户端不存在现成的pcd
文件,需要接收字节形式的点云数据。
上述基于PCDLoader
实现的代码虽然很简洁,但其只能读取文件或URL数据流,使用范围受到一定程度的约束,个人希望在项目中能直接基于点云数据集合进行渲染。
参考网络上的相关资料,修改后的代码如下,其通过renderPcd()
函数接收点云数据集合并渲染,适用性更广。
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
export const initComponments = (width, height, parent) => {
let renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(width, height);
parent.appendChild(renderer.domElement);
let scene = new THREE.Scene();
let camera = new THREE.PerspectiveCamera(12, width / height, 0.5, 50000);
camera.position.z = 310;
scene.add(camera);
let controls = new OrbitControls(camera, renderer.domElement);
controls.addEventListener('change', () => renderer.render(scene, camera)); // use if there is no animation loop
controls.target = new THREE.Vector3(0, 0, 1);
controls.autoRotate = false;
controls.dampingFactor = 0.25;
// 控制缩放范围
//controls.minDistance = 0.1;
//controls.maxDistance = 100;
//scene.add( new THREE.AxesHelper( 1 ) );
window.addEventListener('resize', () => {
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height);
renderer.render(scene, camera);
});
return [renderer, camera, scene];
}
export const renderPcd = (name, data, renderer, camera, scene) => {
// 移除旧点云数据
for (let child of scene.children) {
if (child.type === 'Points') {
scene.remove(child);
break;
}
}
let geometry = new THREE.BufferGeometry();
let material = new THREE.PointsMaterial({ size: 0.05, vertexColors: 2 }); //vertexColors: THREE.VertexColors
let points = new THREE.Points(geometry, material);
let positions = Float32Array.from(data);
let color = []
for (let i = 0; i < data.length; i += 3) {
color[i] = 0.12;
color[i + 1] = 0.565;
color[i + 2] = 1;
}
let colors = new Float32Array(color)
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));
geometry.center();
geometry.rotateX(Math.PI);
// 沿y轴方向平移一定单位
//points.translateY(10);
//points.translateX(70);
points.name = name;
// 图像缩放
points.scale.set(1.2, 1.2, 1.2);
scene.add(points);
renderer.render(scene, camera);
}
import { PcdData } from '../proto/pcd_pb';
import { request } from '../tools/axios_tools';
import { initComponments, renderPcd } from '../tools/play_tools';
let renderer, camera, scene;
export const clearPcdData = () => {
renderer = null;
camera = null;
scene = null;
}
export const playPcd = (pId, pHeight, data, playingRef, updateState) => {
// 进行一些必要的初始化操作
let parent = document.getElementById(`${pId}`);
let width = parent.offsetWidth;
let total = data.total;
if (total === 0) {
return;
}
if (!scene) {
let [sRenderer, sCamera, sScene] = initComponments(width, pHeight, parent);
renderer = sRenderer;
camera = sCamera;
scene = sScene;
}
loadPlayPcd(data, playingRef, updateState, renderer, camera, scene);
};
let count = 0;
export const loadPlayPcd = (data, playingRef, updateState, renderer, camera, scene) => {
let file = data.file[count];
let total = data.total;
count++;
let url = `main/loadPcdBinary?pcd=${file}`;
request.get(url, { responseType: "arraybuffer" }).then(function (response) {
if (!playingRef.current) {
return;
}
let result = PcdData.deserializeBinary(response.data);
renderPcd(result.getName(), result.getPointList(), renderer, camera, scene);
if (count < total) {
let percent = (count / total * 100).toFixed(2);
updateState({ 'progress': percent, processCount: count });
loadPlayPcd(data, playingRef, updateState, renderer, camera, scene)
} else {
updateState({ 'progress': 0, processCount: 0 });
count = 0;
}
}).catch(function (error) {
console.log(error);
});
};
服务端实现
服务端采用Express实现,其主要功能如下:
- 以
JSON
格式显示pcd
、ply
、drc
等点云文件的整体信息 - 将
pcd
文件转化为ply
或drc
格式的点云文件,相关操作均基于JavaScript
实现 - 以
Protocol Buffers
编码的形式提供pcd
、ply
、drc
格式点云文件的下载,供客户端下载并渲染
相关代码位于draco-point-cloud-server,可将其下载到本地直接运行,也可基于下述相关指令构建Docker
镜像并运行,或采用个人已经提前构建好的镜像draco_server
# build image
docker build -t lucumt/draco_server:1.0 .
# run docker
docker run -d -p 8000:8000 --name draco_server lucumt/draco_server:1.0
# stop docker
docker stop draco_server && docker rm draco_server
FROM node:22.14.0-alpine
RUN mkdir /draco
COPY draco-point-cloud-server.zip /draco/draco-point-cloud-server.zip
WORKDIR /draco
RUN unzip draco-point-cloud-server.zip && rm draco-point-cloud-server.zip
RUN mv draco-point-cloud-server server
WORKDIR /draco/server
RUN npm i
ENV PORT=8000
CMD ["node", "./bin/www"]
启动该服务后在浏览器中打开,其主页面显示如下
点击相关链接即可进行相关操作,而基于JavaScript
通过Draco
将pcd
转化为drc
文件十分耗时,故推荐采用脚本进行批量转化以加快转化时间,具体请参见脚本转换。
为了方便使用,个人提前在该项目中已经准备好了若干pcd
和drc
格式的点云数据,通过源码或镜像启动服务端后可直接让客户端下载相关数据并进行渲染。
客户端实现
客户端基于React实现,其主要功能如下:
- 获取服务端
pcd
和drc
文件的总数信息,并在页面展示 - 下载
pcd
或drc
文件,基于Protocol Buffers
进行解码,并对drc
文件通过Draco
进行进一步的解码 - 针对
pcd
和drc
文件,分别基于threejs
进行点云渲染,可进行开始播放、暂停播放、切换播放源等操作
相关的代码位于draco-point-cloud-client,可将其下载到本地自行编译部署,也可基于下述相关指令构建Docker
镜像并运行,或采用个人已经提前构建好的镜像draco_client
# build image
docker build -t lucumt/draco_client:1.0 .
# run docker
docker run -d -p 3000:3000 --name draco_client lucumt/draco_client:1.0
# stop docker
docker stop draco_client && docker rm draco_client
FROM nginx
COPY dist /usr/share/nginx/html
COPY draco.conf /etc/nginx/conf.d/
EXPOSE 3000
server {
listen 3000;
server_name draco-server;
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods 'GET, POST,PUT,DELETE, OPTIONS';
add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
启动该服务后在浏览器中打开,其主页面显示如下
点击图中的播放按钮,会加载点云文件并基于threejs
进行渲染展示,或者通过顶部的tab菜单切换不同的数据格式播放
相关错误修复
Draco启动报错
基于Draco
官方的demo说明,在客户端项目中添加如下代码进行Draco
初始化
import draco3d from 'draco3d';
draco3d.createDecoderModule({}).then(function(module) {
// xxx
});
通过npm start
启动会发现其立即报错,报错信息类似如下
此问题的根源是自己使用的react-scripts
版本为5.0.1
导致的,通过将其版本降低到4.0.3
可修复,但自己并不想降低其版本,于是参考这篇文章中说的第2种方法来解决:
1.先确保项目中没有要提交的代码,git status
显示无任何修改的内容,然后执行下述指令将React
项目的webpack config从node_modules里暴露出来
npm run eject
2.安装对应依赖包
npm i stream-browserify buffer path-browserify crypto-browserify crypto-browserify
3.测试根据报错提示在config/webpack.config.js
下搜索resolve:
,并添加如下内容
fallback: {
"stream": require.resolve("stream-browserify"),
"buffer": require.resolve("buffer/"),
"path": require.resolve("path-browserify"),
"crypto": require.resolve("crypto-browserify")
}
4.之后重新执行npm start
即可正常启动。
Draco播放报错
修复完上述问题后,点击"Draco播放"以基于drc
文件播放时,页面上会提示如下错误
查看浏览器控制台有类似如下报错
基于报错信息去网络查找,大部分都说要添加draco_decoder.wasm
文件,继续在控制台查看网络请求,发现其确实在请求该文件,且加载报错
进一步查看其请求路径如下,可知其是在static/js
目录下查找该文件,其对应于项目中的public/static/js
目录,检查后发现确实没有该文件,将draco_decoder.wasm
放入该目录 即可解决此问题。
效果展示对比
本章节主要利用前述预先准备好的120个pcd
和drc
文件,在客户端对它们分别进行连续播放测试,对比其耗时以及显示效果。
在这篇文章中已经说明了利用Draco
压缩为drc
文件后可大幅减少点云文件的大小
分别基于pcd
和drc
进行播放,其耗时对比如下
从上述对比图可看出drc
文件相对于原始的pcd
文件,即使在经过Protocol Buffers
序列化,其大小是原文件的1/20,且耗时在10ms级别,基本上实现了无感知延迟的点云连续播放。
基于公司内部局域网,将前述的120帧点云文件全部播放完毕后对比效果如下:
pcd
格式的点云文件全部播放完毕耗时58s,有肉眼可见的明显延迟
drc
格式的点云文件全部播放完毕耗时7s,播放体验十分流畅,基本满足使用需求。