Appearance
海量点云渲染
一、需求背景
二、竞品分析
三、学术调研与技术预研
四、海量渲染方案
4.1、云端渲染,视频流推送到前端
4.2、分层分块,根据索引按需加载
4.2.1、Potree
4.2.2、Cesium
4.2.3、3DTilesRendererJS
五、关键措施
六、4D 语义分割
一、4 个基础工具
TIP
套索工具 + 多边形工具 + 矩形形工具 + 点工具
二、4 个标注模式
TIP
覆盖 + 不覆盖 + 更新 + 删除
三、语义分割整体方案
3.1、大体思路
- 页面操作:在二维平面上绘制形状(套索、多边形、矩形和点),框选出三维点云。
- 实现原理:将三维点云坐标转换成屏幕坐标,然后在二维平面上判断该点是否在绘制的形状内部。
- 实现宗旨:将三维复杂的问题转换成二维简单的问题。
3.2、三维点云坐标转换成屏幕坐标
TIP
把大象放冰箱
总体分两步:三维点云坐标 -> NDC(归一化设备坐标)坐标 -> 二维屏幕坐标
3.2.1、三维点云坐标转换成 NDC(归一化设备坐标)坐标
js
// point 是单个点云坐标,camera 是相机
point.clone().project(camera);
// or
const matrix = new THREE.Matrix4();
matrix.copy(camera.projectionMatrix);
matrix.multiply(camera.matrixWorldInverse);
point.clone().applyMatrix4(matrix);
3.2.2、NDC(归一化设备坐标)坐标转换成二维屏幕坐标
js
const unProject = (pos: { x: number; y: number }) => {
return { x: ((pos.x + 1) / 2) * view.width, y: (-(pos.y - 1) / 2) * view.height };
};
3.3、在二维平面上判断一个点是否在绘制的形状内部
TIP
能偷懒就偷懒
先判断包围框,可以简化计算
js
pos.x >= minV2.x
pos.y >= minV2.y
pos.x <= maxV2.x
pos.y <= maxV2.y
3.3.1、方案一:向量叉乘 ×
- 遍历多边形的所有边,利用向量的叉乘计算方向,可以得出点在边的左侧还是右侧
- 每次都需要重复计算,计算量大。
3.3.2、方案二:射线交点法 ×
- 从该点向任意方向发出一条射线,统计射线与多边形的边相交的次数,奇数,则点在多边形内部;偶数,则点在多边形外部
- 只适用于凸多边形。
3.3.3、方案三:夹角求和 ×
- 计算点与多边形所有相邻顶点的夹角,并求和。如果夹角的总和等于 2π 或 -2π,则点在多边形内部;否则,点在外部。
- 只适用于凸多边形。
3.3.4、方案四:离屏 canvas 颜色判断 √
- 先根据多边形绘制离屏 canvas,将多边形内部的颜色填充为红色。只需要查询点的颜色即可:红色在多边形内部,白色多边形外部。
- 离屏 canvas 只需要绘制一次,计算量小。
四、分割标注物体显示问题
4.1 着色器字段设计
TIP
大体思路:两个数字表示一个点的语义分割信息,在着色器中控制最终的颜色显示
4.1.1、第一个数字
- 代表标签颜色
- 需要预留无标签的颜色或者直接在着色器中写死
4.1.2、第二个数字
- Default = 0, // 默认分割
- Hide = 1, // 隐藏
- Discard = 2, // 透明度 0
- Highlight = 3, // 分割批注高亮
- Fade = 4, // 透明度 0.6
4.2 顶点着色器代码(简化版)
glsl
precision mediump float;
precision mediump int;
uniform int isSegmentMode;
// 分割漏标
uniform float missedHighlight;
// color
#ifdef COLOR_N
uniform vec3 classColorTable[COLOR_N];
#endif
attribute vec2 property;
// matrix
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
// X
uniform vec2 lengthRange;
// Y
uniform vec2 widthRange;
// Z
uniform vec2 heightRange;
uniform vec2 pointHeight;
uniform float pointSize;
attribute vec3 position;
//attribute vec4 color;
attribute vec3 color;
varying vec3 vColor;
varying float vOpacity;
void main() {
vOpacity = 1.0;
float vDiscard = -1.0;
float vPointSize = pointSize;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
gl_PointSize = pointSize;
if(position.z>heightRange.y||position.z<heightRange.x||position.x>lengthRange.y||position.x<lengthRange.x||position.y>widthRange.y||position.y<widthRange.x){
vDiscard = 1.0;
}
bool isSegmentObject = property.x > 0.0;
bool isHide = property.y == 1.0;
bool isDiscard = property.y == 2.0;
bool isHighlight = property.y == 3.0;
bool isFade = property.y == 4.0;
bool isMissedLight = missedHighlight > 0.0;
if(isHighlight){
// 分割批注高亮
vColor = vec3(1.0,0.0,0.0);
vPointSize += 2.0;
} else if (isSegmentObject && !isHide && isSegmentMode == 1) {
int _id = int(abs(property.x));
vColor = vec3(1.0, 1.0, 1.0);
#ifdef COLOR_N
if (_id > 0 && _id <= COLOR_N) {
vColor = classColorTable[_id - 1];
}
#endif
} else if(!isSegmentObject && isMissedLight && isSegmentMode == 1){
isFade = false;
vColor = vec3(1.0, 0.0, 0.0);
vPointSize += 4.0;
}
if (isSegmentMode == 1) {
if(isDiscard){
vDiscard = 1.0;
}else if(isFade){
vOpacity = 0.6;
}
}
if(vDiscard > 0.0) {
gl_Position = vec4(2.0,2.0,2.0,1.0);
vOpacity = 0.0;
}
// 漏标 size 修改
gl_PointSize = vPointSize;
}
五、分割物体被选中方案
TIP
5.1、点云拾取方案一:离屏渲染拾取 ×
js
const { offsetX, offsetY } = event;
const { renderer, height, width } = view;
renderer.readRenderTargetPixels(renderTarget, offsetX, height - offsetY - 1, 1, 1, pickUnit);
const [r, g, b, a] = pickUnit;
const points = view.pointCloud.groupPoints.children[0] as Points;
const position = points?.geometry.getAttribute('position');
const idx = r + g * 255 + b * 255 * 255 - 1;
if (idx > 0 && position) {
pickPos.fromBufferAttribute(position, idx);
pickPos.idx = idx;
return pickPos;
}
5.1、点云拾取方案二:射线投射拾取 √
js
const { offsetX, offsetY } = event;
const { height, width } = view;
const points = view.pointCloud.groupPoints.children[0] as Points;
const position = points?.geometry.getAttribute('position');
let x = (offsetX / width) * 2 - 1;
let y = (-offsetY / height) * 2 + 1;
view.raycaster.setFromCamera({ x, y }, view.camera);
let intersects = view.raycaster.intersectObject(
view.pointCloud.groupPoints.children[0],
true,
);
if (intersects.length > 0) {
const idx = intersects[0].index as number;
if (idx > 0 && position) {
pickPos.fromBufferAttribute(position, idx);
pickPos.idx = idx;
return pickPos;
}
}