在cesium中实现倾斜摄影的压平功能
1.功能分析
cesium中可以加载倾斜摄影模型,倾斜摄影是一种测绘技术,通过无人机拍摄可以快速建模。当把倾斜摄影模型添加进来后,一般会在倾斜摄影中某个位置添加一个真实模型进行融合,这个时候为了融合的好一点需要对倾斜模型的某些位置进行压平处理。
实现步骤一般为选择一块区域,设置压平高度,实现压平
2.实现思路
cesium对3Dtiles模型提供了CustomShader自定义shader的接口,所以可以在顶点着色器中对顶点进行处理,判断这个顶点的x,y是否在区域范围内,如果是则修改z的高度
3.基本架构设计
先封装实现区域选择的操作
import { PlanishOperate } from '../ScenePlanish/PlanishOperate';
import { CallbackProperty, Color, defined, Entity, PolygonHierarchy } from 'cesium';
// 这里使用了继承,因为区域选择交互与上一篇文章地形压平里是一样的,所以可以使用继承来复用代码,覆盖部分方法函数进行不同的业务处理。
// 地形压平文章地址 https://juejin.cn/post/7350624030464180234
class PlanishOsgbOperate extends PlanishOperate {
public pointArray: any;
constructor(app: any) {
super(app);
this.pointArray = [];
this.planishHeight = 0.0;
}
// 右键结束区域绘制
public innerRightClickAction(e: any, successCallback: Function) {
this.pointArray.pop();
if (successCallback) successCallback(this.pointArray, this.planishHeight);
this.cancelDrawArea();
}
// 左键开始区域绘制
public innerLeftClickAction(movement: any) {
let cartesian = this.viewerCesium.scene.pickPosition(movement.position);
// 是否获取到空间坐标
if (defined(cartesian)) {
this.drawPoint(cartesian);
this.pointArray.push(cartesian);
}
if (this.pointArray.length === 3) {
let polygonEntity = new Entity({
name: '拍平区域面对象',
polygon: {
hierarchy: new CallbackProperty(() => {
return new PolygonHierarchy(this.pointArray);
}, false),
material: Color.RED.withAlpha(0.5)
}
});
this.viewerCesium.entities.add(polygonEntity);
this.polygonEntity = polygonEntity;
}
}
// 鼠标移动绘制区域面
public innerMouseMoveAction(movement: any) {
let cartesian = this.viewerCesium.scene.pickPosition(movement.endPosition);
// 是否获取到空间坐标
if (defined(cartesian)) {
this.pointArray.pop();
this.pointArray.push(cartesian);
}
}
}
export { PlanishOsgbOperate };
封装地形压平的数据项
class PlanishOsgbArea {
// 平整区域名称
name: string;
/** uuid */
uuid: string;
// 平整区域点数组
area: Array<number>;
// 平整高度
height: number | null;
show: boolean;
constructor() {
this.name = '区域';
this.uuid = this.createUUID();
this.area = [];
this.height = null;
this.show = true;
}
private createUUID() {
function S4() {
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
}
return S4() + S4() + '-' + S4() + '-' + S4() + '-' + S4() + '-' + S4() + S4() + S4();
}
}
class PlanishOsgbOptions {
// 版本号
version: string;
// 自定义场地平整数据
customPlanishArr: PlanishOsgbArea[];
constructor() {
this.version = '1.0';
this.customPlanishArr = [];
}
public copyData(PlanishOsgbOptions: PlanishOsgbOptions) {
if (!PlanishOsgbOptions) return;
this.version = PlanishOsgbOptions.version;
this.customPlanishArr = PlanishOsgbOptions.customPlanishArr;
}
}
export { PlanishOsgbOptions, PlanishOsgbArea };
封装业务代码操作
import { PlanishOsgbOperate } from './PlanishOsgbOperate';
import { PlanishOsgbArea, PlanishOsgbOptions } from './PlanishOsgbOptions';
import { TilesetPlanish } from './TilesetPlanish';
import { ModelTypeEnum } from '../SceneModel/ModelTypeEnum';
class PlanishOsgbManager {
planishOsgbOperate: PlanishOsgbOperate;
planishOsgbOptions: PlanishOsgbOptions;
viewerCesium: any;
TilesetPlanishArr: any[] = [];
constructor(app: any) {
super(app);
this.planishOsgbOptions = new PlanishOsgbOptions();
this.planishOsgbOperate = new PlanishOsgbOperate(app);
this.viewerCesium = this.app.getViewerCesium();
}
protected doGetSceneData(): any {
return this.planishOsgbOptions;
}
protected doSetSceneData(sceneData: PlanishOsgbOptions): void {
this.planishOsgbOptions.copyData(sceneData);
this.addOsgbEditsDataArr(this.planishOsgbOptions.customPlanishArr);
}
public startDrawArea(successCallback: Function) {
this.planishOsgbOperate.startDrawArea((areaData: Array<any>, height: number) => {
let num = this.planishOsgbOptions.customPlanishArr.length + 1;
let name = '区域' + num;
let planishArea = new PlanishOsgbArea();
planishArea.name = name;
planishArea.area = areaData;
planishArea.height = height;
this.planishOsgbOptions.customPlanishArr.push(planishArea);
this.savePlanish(planishArea.uuid, areaData, height);
if (successCallback) successCallback(planishArea);
});
}
public savePlanish(uuid: string, areaData: Array<any>, height: number) {
this.TilesetPlanishArr.forEach((item) => {
item.value.addRegionEditsData(uuid, areaData, height);
});
}
public addOsgbEditsDataArr(arr: PlanishOsgbArea[]) {
this.createTilesetPlanish();
this.TilesetPlanishArr.forEach((item) => {
item.value.addRegionEditsDataArr(arr);
});
}
public createTilesetPlanish() {
// 可能有很多3dTiles模型,所以要遍历这些模型,筛选osgb模型只对倾斜摄影模型进行压平处理
const modelManager = this.app.getViewer().getSceneModelManager();
const modelList = modelManager.getModelList();
modelList.forEach((item: any) => {
if (item.modelType === ModelTypeEnum.obliquePhotograph) {
let add = true;
for (let i = 0; i < this.TilesetPlanishArr.length; i++) {
if (this.TilesetPlanishArr[i].key === item.modelId.modelKey) {
add = false;
}
}
if (add) {
const modelOp = this.app.getViewer().getSceneModelManager().getModelOperateByFileKey(item.modelId.modelKey);
this.TilesetPlanishArr.push({
key: item.modelId.modelKey,
value: new TilesetPlanish(this.app, modelOp.tilesetList[0])
});
}
}
});
}
public removePlanish(uuid: string, successFunc: Function) {
for (let i = 0; i < this.planishOsgbOptions.customPlanishArr.length; i++) {
let item = this.planishOsgbOptions.customPlanishArr[i];
if (item.uuid === uuid) {
this.planishOsgbOptions.customPlanishArr.splice(i, 1);
this.TilesetPlanishArr.forEach((item) => {
item.value.removeRegionEditsData(uuid);
});
successFunc();
}
}
}
public editPlanishHeight(uuid: string, heightValue: number, successFunc: Function) {
for (let i = 0; i < this.planishOsgbOptions.customPlanishArr.length; i++) {
let item = this.planishOsgbOptions.customPlanishArr[i];
if (item.uuid === uuid) {
item.height = heightValue;
this.TilesetPlanishArr.forEach((item) => {
item.value.setRegionEditsHeight(uuid, heightValue);
});
successFunc();
}
}
}
public editPlanishVisible(uuid: string, visible: boolean, successFunc: Function) {
for (let i = 0; i < this.planishOsgbOptions.customPlanishArr.length; i++) {
let item = this.planishOsgbOptions.customPlanishArr[i];
if (item.uuid === uuid) {
item.show = visible;
this.TilesetPlanishArr.forEach((item) => {
item.value.setRegionEditsVisible(uuid, visible);
});
successFunc();
}
}
}
public cancelPlanish() {
this.planishOsgbOperate.cancelDrawArea();
}
public setPlanishHeight(height: number) {
this.planishOsgbOperate.setPlanishHeight(height);
}
}
export { PlanishOsgbManager };
4.核心代码实现
import { Cartesian3, Cesium3DTileset, CustomShader, Matrix4, Transforms, UniformType } from 'cesium';
import { PlanishOsgbArea } from './PlanishOsgbOptions';
class TilesetPlanish {
private _app: any;
private _tileset: Cesium3DTileset;
private _matrix: Matrix4;
private _localMatrix: Matrix4;
private _polygonEdits: any[];
constructor(app: any, tileset: Cesium3DTileset) {
this._app = app;
this._tileset = tileset;
const center = tileset.boundingSphere.center.clone(); // 包围盒的中心点
this._matrix = Transforms.eastNorthUpToFixedFrame(center); // 根据向量点获取变换矩阵
this._localMatrix = Matrix4.inverse(this._matrix, new Matrix4()); // 获取逆矩阵
this._polygonEdits = [];
}
/**
* 添加压平区域数据
* @param uuid 压平区域的uuid
* @param area 压平区域数据
* @param height 压平高度
* @returns
*/
addRegionEditsData(uuid: string, area: Array<Cartesian3>, height: number = 0.0) {
for (let p = 0; p < this._polygonEdits.length; p++) {
if (this._polygonEdits[p].uuid === uuid) {
return;
}
}
if (area.length === 0) return;
// 把多边形压平区域的数据对象存储起来,用于后续处理
this._polygonEdits.push({
uuid: uuid,
show: true,
polygon: this.cartesiansToLocal(area), // 坐标转换
height: height
});
this.renderShader();
}
/**
* 业务需要封装根据压平区域数据的数组添加(内部实现与addRegionEditsData类似)
* @param {*} arr 压平区域数据的对象数组
* @returns
*/
addRegionEditsDataArr(arr: PlanishOsgbArea[]) {
arr.forEach((element) => {
const uuid = element.uuid;
const height = element.height;
const show = element.show;
let area: Cartesian3[] = [];
element.area.forEach((item: any) => {
area.push(new Cartesian3(item.x, item.y, item.z));
});
for (let p = 0; p < this._polygonEdits.length; p++) {
if (this._polygonEdits[p].uuid === uuid) {
return;
}
}
if (area.length === 0) return;
this._polygonEdits.push({
uuid: uuid,
show: show,
polygon: this.cartesiansToLocal(area),
height: height
});
});
this.renderShader();
}
// 世界坐标转模型里的局部坐标
cartesiansToLocal(positions: Array<Cartesian3>) {
let arr = [];
for (let i = 0; i < positions.length; i++) {
let position = positions[i];
let localp = Matrix4.multiplyByPoint(this._localMatrix, position.clone(), new Cartesian3()); // 将世界坐标点与之前通过包围盒中心点算得的矩阵的逆矩阵相乘,得到模型里的局部坐标
arr.push([localp.x, localp.y]);
}
return arr;
}
/**
* 生成对应的shader代码
*/
renderShader() {
const funstr = this.getPointInPolygon(this._polygonEdits);
let str = ``;
this._polygonEdits.forEach((item: any, index: number) => {
if (item.show) {
const name = index;
item.polygon.forEach((point: Array<number>, i: number) => {
str += `points_${name}[${i}] = vec2(${point[0]}, ${point[1]});`;
});
str += `
if (isPointInPolygon_${name}(position2D)) {
float ground_z = float(${item.height});
vec4 tileset_local_position_transformed = vec4(tileset_local_position.x, tileset_local_position.y, ground_z, 1.0);
vec4 model_local_position_transformed = czm_inverseModel * u_tileset_localToWorldMatrix * tileset_local_position_transformed;
vsOutput.positionMC.xyz = model_local_position_transformed.xyz;
return;
}\n
`;
}
});
this.updateShader(funstr, str);
}
/**
* 根据数组长度,构建 判断点是否在面内 的压平函数
* @param polygons 压平区域的数据数组
* @returns
*/
getPointInPolygon(polygons: any) {
let str = ``;
polygons.forEach((item: any, index: number) => {
if (item.show) {
const length = item.polygon.length;
const name = index;
str += `
vec2 points_${name}[${length}];
bool isPointInPolygon_${name} (vec2 point) {
int nCross = 0; // 交点数
const int n = ${length};
for (int i = 0; i < n; i++) {
vec2 p1 = points_${name}[i];
vec2 p2 = points_${name}[int(mod(float(i+1), float(n)))];
if (p1[1] == p2[1]) {
continue;
}
if (point[1] < min(p1[1], p2[1])) {
continue;
}
if (point[1] >= max(p1[1], p2[1])) {
continue;
}
float x = p1[0] + ((point[1] - p1[1]) * (p2[0] - p1[0])) / (p2[1] - p1[1]);
if (x > point[0]) {
nCross++;
}
}
return int(mod(float(nCross), float(2))) == 1;
}`;
}
});
return str;
}
/**
* 更新自定义shader
* @param vtx1 判断点是否在面内的压平函数 string
* @param vtx2 调用范围判断压平 string
*/
updateShader(vtx1: string, vtx2: string) {
let flatCustomShader = new CustomShader({
uniforms: {
u_tileset_localToWorldMatrix: {
type: UniformType.MAT4,
value: this._matrix
},
u_tileset_worldToLocalMatrix: {
type: UniformType.MAT4,
value: this._localMatrix
}
},
vertexShaderText: `
${vtx1}
void vertexMain (VertexInput vsInput, inout czm_modelVertexOutput vsOutput) {
vec3 modelMC = vsInput.attributes.positionMC;
vec4 model_local_position = vec4(modelMC.x, modelMC.y, modelMC.z, 1.0);
vec4 tileset_local_position = u_tileset_worldToLocalMatrix * czm_model * model_local_position;
vec2 position2D = vec2(tileset_local_position.x, tileset_local_position.y);
${vtx2}
}`
});
this._tileset.customShader = flatCustomShader;
console.log(flatCustomShader.vertexShaderText);
}
/**
* 删除压平区域
* @param {*} uuid 压平区域的uuid
*/
removeRegionEditsData(uuid: string) {
for (let i = 0; i < this._polygonEdits.length; i++) {
if (this._polygonEdits[i].uuid === uuid) {
this._polygonEdits.splice(i, 1);
}
}
this.renderShader();
}
/**
* 设置压平区域的显隐
* @param {*} uuid 压平区域的uuid
* @param {*} visible 显隐值
*/
setRegionEditsVisible(uuid: string, visible: boolean) {
for (let i = 0; i < this._polygonEdits.length; i++) {
if (this._polygonEdits[i].uuid === uuid) {
this._polygonEdits[i].show = visible;
}
}
this.renderShader();
}
/**
* 设置压平区域高度
* @param {*} uuid 压平区域的uuid
* @param {*} height 压平高度
*/
setRegionEditsHeight(uuid: string, height: number) {
for (let i = 0; i < this._polygonEdits.length; i++) {
if (this._polygonEdits[i].uuid === uuid) {
this._polygonEdits[i].height = height;
}
}
this.renderShader();
}
}
export { TilesetPlanish };