3dtiles平移旋转工具制作
3dtiles平移旋转缩放原理及可视化工具实现
背景
平时工作中,通过cesium平台来搭建一个演示场景是很常见的事情。一般来说,演示场景不需要多完善的功能,但是需要一批三维模型搭建,如厂房、电力设备、园区等。在实际搭建过程中,就会面临一个尴尬的问题,那就是模型定位,常规操作中,我们一般用三维模型的中心点对应一个经纬度坐标,以此转换成3dtiles格式,但是给定的经纬度坐标一般是模糊的大致位置,甚至有的场景不需要准确的坐标,只需要你找个合适的场景把各个模型搭建起来,这就不得不对模型进行微调位置,以适应场景。
-
cesium中有对模型进行调整的代码,事实上,如果模型定位的位置不是我们想要的位置,可以通过修改该3dtiles的矩阵,来改变它,如平移:
/**基于本地的ENU坐标系的偏移,也就是垂直于地表向上为Z,东为X,北为Y* @param tileset Cesium3DTileset* @param dx x轴偏移量。单位:米* @param dy y轴偏移量。单位:米* @param dz z轴偏移量。单位:米*/ function translate(tileset: Cesium3DTileset, dx: number, dy: number, dz: number) {if (dx === 0 && dy === 0 && dz === 0) return// 对于3DTileset,我们需要的结果是一个模型矩阵,那么平移就是计算一个世界坐标下的平移矩阵。// 获取中心点const origin = tileset.boundingSphere.center// 以该点建立ENU坐标系const toWorldMatrix = Transforms.eastNorthUpToFixedFrame(origin)// 该坐标系下平移后的位置const translatePosition = new Cartesian3(dx, dy, dz)// 获取平移后位置的世界坐标const worldPosition = Matrix4.multiplyByPoint(toWorldMatrix, translatePosition, new Cartesian3())// 计算世界坐标下的各个平移量const offset = Cartesian3.subtract(worldPosition, origin, new Cartesian3())// 从世界坐标下的平移量计算世界坐标的平移矩阵const translateMatrix = Matrix4.fromTranslation(offset)// 应用平移矩阵。这里应该与原本的模型矩阵点乘,而不是直接赋值tileset.modelMatrix = Matrix4.multiply(translateMatrix, tileset.modelMatrix, new Matrix4()) }
-
但是实际开发中,我们把一个模型准确的放在我们想要的位置上,或者调整其与底图对齐,需要多次进行一点一点的矫正,也就是说需要多次调用该函数,调整xyz平移参数,这样做十分麻烦,因此最好能做一个可视化控件,能够直接通过拖住的形式来调整模型,最后得出模型调整后的位置矩阵,然后在新一轮代码中,直接将模型的位置改为该位置矩阵,即可完成模型的调整。
-
功能开发
可视化控件
可视化控件,即我们看到的拖拽箭头,当平移时,是一个带箭头的三维坐标系的坐标轴,当是旋转时,是三个互相垂直的圆环;而这个可视化的开发,依靠cesium完全可以实现,因为cesium本身支持绘制图形,并且可以监听到鼠标是否划过和点击图形。
平移控件的显示
-
首先控件的显示需要思考的问题是,控件需要多大,控件的位置在哪里,显然,控件的大小需要根据模型的大小来制定,而控件的位置肯定就是模型的位置,因此,很自然我们需要创建一个函数,来在创建控件前获取模型的位置和根据模型范围制定控件大小。
/*** 初始化参数和清理工具*/private initParam(): TransformOption {this.removeAllTools();const b3dm = this._b3dm;const viewer = this._viewer;const length = b3dm.boundingSphere.radius / 3;const originalMatrix = this._b3dm.root.transform.clone()const ps = new Cesium.Cartesian3();Cesium.Matrix4.getTranslation(originalMatrix,ps)let pos = CoordTransform.transformCartesianToWGS84(viewer,ps)this._params = {...this._params,tx: pos.lng,ty: pos.lat,tz: pos.alt ,};return {originDegree:pos,length,};}
_params记录了6个参数,分别是tx,ty,tz和rx,ry,rz,它们可以用来记录当前模型的位置和姿态变化
-
有了初始的位置参数和范围之后,我们就可以构建坐标系,首先是坐标轴,在构建坐标轴的时候,需要考虑到,坐标轴是三个正交的方向,但是每一个方向指向哪里?比如X轴指向哪里?
-
事实上,X轴指向任意一个方向,我们都能轻易的构建出一个三个方向正交的坐标系,但是显然,不符合我们的操作习惯。
-
首先,先做一个假设,如果是根据Cesium的世界坐标系的方向来建立坐标轴,是否可行?显然是不行的,假设一个模型处于地球表面,我们的习惯,依然是想按照朝东为X轴,朝北为Y轴,朝上为Z轴来移动物体,这种习惯和我们的经纬度的习惯是符合的,东西走向为经度走向,南北走向为维度走向,那么我们让物体按X轴移动,就是沿着经度走,按Y轴移动,就是沿着维度走
-
而我们的Cesium的世界坐标系显然不是这种情况,如图所示,绿色坐标就是我们刚才说的坐标系,而蓝色坐标系,而是Cesium的世界坐标系,显然如果我们按照世界坐标系的方向建立坐标轴,当我们移动某个方向的时候,都会偏离出地球。
-
-
-
清楚了这一点,我们就可以按照图上绿色坐标系的形式建立坐标系,我们姑且称为局部坐标系,我们已经知道这个坐标系的原点的坐标了,也知道坐标系的范围了(可以理解为每个轴的长度),那么只需要求出每个轴的终点坐标就可以了,基于此,我们再创建一个getTransPosition函数,用来求终点坐标。
const { originDegree, length } = this.initParam();const translateCartesian = new Cesium.Cartesian3(length, length, length);const ps = CoordTransform.transformWGS84ToCartesian(this._viewer,originDegree)const targetDegree = this.getTransPosition(ps, translateCartesian);
我们不需要求出每个轴的终点坐标,事实上只需要求出
translateCartesian
这个向量的坐标就行了,然后每个轴的终点坐标,相当于向量坐标的分量,基于此,我们完善getTransPosition
函数/*** 根据平移距离获取目标点* @param originPosition - 原始位置(笛卡尔坐标)* @param translateCartesian - 平移向量(笛卡尔坐标)* @return 平移后的位置(经纬度坐标)*/private getTransPosition(originPosition: Cesium.Cartesian3,translateCartesian: Cesium.Cartesian3): { lng: number; lat: number; alt: number } {// 东-北-上参考系构造出4*4的矩阵const transform = Cesium.Transforms.eastNorthUpToFixedFrame(originPosition);//构造平移矩阵const m = new Cesium.Matrix4();Cesium.Matrix4.setTranslation(Cesium.Matrix4.IDENTITY,translateCartesian,m);//将当前位置矩阵乘以平移矩阵得到平移后的位置矩阵const modelMatrix = Cesium.Matrix4.multiply(transform, m, new Cesium.Matrix4());const finalPosition = new Cesium.Cartesian3();//从位置矩阵中获取坐标信息Cesium.Matrix4.getTranslation(modelMatrix, finalPosition);//转换为地理坐标系return CoordTransform.transformCartesianToWGS84(this._viewer,finalPosition);}
这个思路很简单,首先根据原始位置创建一个局部坐标系,然后求出在局部坐标系下从
(0,0,0)
点平移到向量终点的平移矩阵,而局部坐标系下原点转到世界坐标系下也是一个矩阵,两个矩阵相乘即可求出向量终点在世界坐标系下的位置,也就是一个笛卡尔坐标,有了笛卡尔坐标,也就最终能算出向量终点的经纬度坐标 -
我们可以分析一下向量终点的经纬度坐标,这里简称终点坐标,假设为
(lng1,lat1,alt1)
,而我们原点的坐标是(lng0,lat0,alt0)
,那么很显然,我们能够得出,X轴的长度范围为(lng0,lng1 )
,其他轴也是如此。 -
因此,我们获取到了坐标系的起点,终点,长度等信息,自然而然,下一步可以建立坐标系了
this.initLineArrow(originDegree, targetDegree, length);
/*** 绘制坐标轴* @param originDegree -原始坐标(经纬度)* @param targetDegree -目标坐标(经纬度)* @param length - 坐标轴长度*/private initLineArrow(originDegree: { lng: number; lat: number; alt: number },targetDegree: { lng: number; lat: number; alt: number },length: number): void {const arrows = new Cesium.PolylineCollection();//x轴(红色)const xPos = [originDegree.lng,originDegree.lat,originDegree.alt,targetDegree.lng,originDegree.lat,originDegree.alt,];this.drawArrow(arrows, "model_edit_xArrow", xPos, Cesium.Color.RED);//y轴(绿色)const yPos = [originDegree.lng,originDegree.lat,originDegree.alt,originDegree.lng,targetDegree.lat,originDegree.alt,];this.drawArrow(arrows, "model_edit_yArrow", yPos, Cesium.Color.GREEN);//z轴(蓝色)const zPos = [originDegree.lng,originDegree.lat,originDegree.alt,originDegree.lng,originDegree.lat,targetDegree.alt,];this.drawArrow(arrows, "model_edit_zArrow", zPos, Cesium.Color.BLUE);this._coordArrows = this._viewer.scene.primitives.add(arrows);if(this._coordArrows){this._coordArrows._name = "CoordAxis";}}
-
这里创建的x,y,z轴坐标明显就是两个点的连线,X轴是横跨经线的直线,Y轴是横跨纬线的直线,Z轴则是垂直地面的直线,且三个轴长度相等。然后,我们将每个轴坐标传入
drawArrow
函数,这里是最终实现坐标轴的代码,我们这里可以做一个思考,如果给每个坐标轴的终点,加上箭头,我们需要做两个操作,首先把坐标轴画出来,其次在该轴末尾画一个箭头,这样操作显然比较麻烦,尤其是画箭头,cesium
有一个api已经实现了这个操作,只需要给出坐标,就能实现一条直线并且末尾带箭头,让我们完善这个代码/*** 绘制箭头* @param arrows - PolylineCollection集合* @param name - 箭头名称* @param positions - 箭头位置数组* @param color - 箭头颜色*/private drawArrow(arrows: Cesium.PolylineCollection,name: string,positions: number[],color: Cesium.Color): void {const arrow = arrows.add({positions: Cesium.Cartesian3.fromDegreesArrayHeights(positions),width: this._defaultWidth,material: Cesium.Material.fromType(Cesium.Material.PolylineArrowType, {color: color,}),}) as EditablePolyline;arrow._name = name;}
至此,我们成功创建了坐标系轴,并且每个坐标系轴都赋予一个name属性,整个坐标系有一个整体name属性
旋转控件的实现
旋转控件的实现就比较简单,大致思路就是首先确定圆环的圆心(原点),其次插值求出一个圆环点,其次根据圆环点借助cesium中的api创建一个圆环,最后通过旋转把其它两个圆环创建出来,我们一步步剖析这个操作
-
根据最开始求得的模型的位置坐标和范围创建圆环坐标点,这里以原始坐标为圆心,以范围为半径,进行插值求点
public editRotation(): void {const { originDegree, length } = this.initParam();this.createCircle(originDegree.lng,originDegree.lat,originDegree.alt,length);}
/*** 创建旋转圆环*/private createCircle(lon: number,lat: number,height: number,radius: number): void {const positions: Cesium.Cartesian3[] = [];//生成圆形点位for (let i = 0; i <= 360; i += 3) {const sin = Math.sin(Cesium.Math.toRadians(i));const cos = Math.cos(Cesium.Math.toRadians(i));positions.push(new Cesium.Cartesian3(radius * cos, radius * sin, 0));}const matrix = Cesium.Transforms.eastNorthUpToFixedFrame(Cesium.Cartesian3.fromDegrees(lon, lat, height));//创建三个方向的旋转圆环this.createAxisCircles(positions, matrix);}
这里同样以模型坐标为原点建立局部坐标系,然后插值计算了局部坐标系为圆心的圆的各个插值点坐标,接下来就是创建圆环
-
由于插值点是在XY平面的点,所以它构建的圆环可以刚好垂直Z轴,作为Z轴的旋转圆环,假设我们同时创建三个这样的圆环,只需要让其中一个圆环向Y轴方向旋转90度,即可得出Y轴旋转圆环,向X轴方向旋转90度,即可得出X轴旋转圆环,这里的旋转不能简单的只是旋转90度,要知道这个圆环是在局部坐标系下,要想变换成局部坐标系下的旋转90度,需要变换到世界坐标系下,再乘以旋转矩阵
/*** 创建三个方向的旋转圆环*/private createAxisCircles(positions: Cesium.Cartesian3[],matrix: Cesium.Matrix4): void {//Z轴圆环const zCircle = this.createAxisSphere("model_edit_zCircle",positions,matrix,Cesium.Color.BLUE);this._viewer.scene.primitives.add(zCircle);//X轴圆环const yCircle = this.createAxisSphere("model_edit_yCircle",positions,matrix,Cesium.Color.RED);this._viewer.scene.primitives.add(yCircle);const yRotation = Cesium.Matrix4.fromRotationTranslation(Cesium.Matrix3.fromRotationY(Cesium.Math.toRadians(90)));Cesium.Matrix4.multiply((yCircle.geometryInstances as Cesium.GeometryInstance).modelMatrix,yRotation,(yCircle.geometryInstances as Cesium.GeometryInstance).modelMatrix);//Y轴圆环const xCircle = this.createAxisSphere("model_edit_xCircle",positions,matrix,Cesium.Color.GREEN);this._viewer.scene.primitives.add(xCircle);const xRotation = Cesium.Matrix4.fromRotationTranslation(Cesium.Matrix3.fromRotationX(Cesium.Math.toRadians(90)));Cesium.Matrix4.multiply((xCircle.geometryInstances as Cesium.GeometryInstance).modelMatrix,xRotation,(xCircle.geometryInstances as Cesium.GeometryInstance).modelMatrix);}
-
而创建球体也很简单,当有了插值点后,直接借助cesium中的api进行画线即可,插值点越密,圆形越光滑
/*** 创建坐标轴球体*/private createAxisSphere(name: string,positions: Cesium.Cartesian3[],matrix: Cesium.Matrix4,color: Cesium.Color): Cesium.Primitive {const primitive = new Cesium.Primitive({geometryInstances: new Cesium.GeometryInstance({id: name,geometry: new Cesium.PolylineGeometry({positions,width: 5,}),attributes: {color: Cesium.ColorGeometryInstanceAttribute.fromColor(color),},}),releaseGeometryInstances: false,appearance: new Cesium.PolylineColorAppearance({translucent: false,}),modelMatrix: matrix,}) as EditablePrimitive;primitive._name = name;this._coordCircle.push(primitive);return primitive;}
控件的销毁
我们实现了控件的创建,很显然让切换控件时,需要销毁当前控件,我们之前已经记录了每个控件,因此当销毁时,只需要拿到该控件,在primitives集合中去除即可
/*** 移除所有工具*/private removeAllTools(): void {this.removeCoordArrows();this.removeCoordCircle();}/*** 移除坐标箭头*/private removeCoordArrows(): void {if (this._coordArrows) {this._viewer.scene.primitives.remove(this._coordArrows);this._coordArrows = undefined;}}/*** 移除坐标圆环*/private removeCoordCircle(): void {this._coordCircle.forEach((element) => {this._viewer.scene.primitives.remove(element);});this._coordCircle = [];}
}
实现控件拖动
不管是移动模型还是旋转模型,我们操作的逻辑都是需要鼠标左键按下,然后检测鼠标是否在控件上,如果是,则检测是否是否滑动,计算滑动的距离应用到模型变换上,然后检测鼠标左键抬起,整个过程结束。我们逐步分解一下
-
首先需要监听鼠标按下命令,如果按下后,再检测是否悬停在控件上,如果是,那就要锁定相机,避免鼠标移动地图拖动,然后让悬停的控件变粗,检测悬停位置的鼠标经纬度,如果在地球范围内,在继续进行后续操作
/*** 初始化鼠标事件(移动,按下,抬起)*/private initEvent(): void {const viewer = this._viewer;this._handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);this._handler.setInputAction((event:Cesium.ScreenSpaceEventHandler.PositionedEvent) => {const pick = viewer.scene.pick(event.position);if (pick?.primitive?._name &&pick.primitive._name.indexOf("model_edit") !== -1) {//锁定相机viewer.scene.screenSpaceCameraController.enableRotate = false;this._currentPick = pick.primitive as EditablePrimitive;this._currentPick.width = 25;const downPos = viewer.scene.camera.pickEllipsoid(event.position,viewer.scene.globe.ellipsoid);let _tx = 0,_ty = 0,_tz = 0;let _rx = 0,_ry = 0,_rz = 0;//防止点击到地球之外报错if (downPos && Cesium.defined(downPos)) {const downDegree = CoordTransform.transformCartesianToWGS84(viewer,downPos);//鼠标移动事件// 剩余操作}, Cesium.ScreenSpaceEventType.LEFT_DOWN);}
然后在剩余操作里完善鼠标移动事件,其实不管是平移操作还是旋转操作,本质上都是鼠标移动,当是移动操作的时候,我们可以轻易的计算X轴移动距离和Y轴移动距离,只需要鼠标移动的末尾的经纬度减去最初鼠标点击的经纬度,即可算出差来,作为X轴移动的距离和Y轴移动的距离,但是移动Z轴的时候,我们发现鼠标依然是向上拖动的,也就是鼠标在竖向操作,类似于Y轴的移动逻辑,此时只需要求出鼠标滑动的距离。乘以一个系数,就可以得出Z轴移动的距离了。
//鼠标移动事件this._handler.setInputAction((movement: Cesium.ScreenSpaceEventHandler.MotionEvent) => {if (!this._currentPick) return; // 增加空值检查const endPos = viewer.scene.camera.pickEllipsoid(movement.endPosition,viewer.scene.globe.ellipsoid);if (endPos && Cesium.defined(endPos)) {const endDegree = CoordTransform.transformCartesianToWGS84(viewer,endPos);const _yPix = movement.endPosition.y - event.position.y;const _xPix = movement.endPosition.x - event.position.x;//根据当前选中控制器更新相应变量switch (this._currentPick._name) {case "model_edit_xArrow":_tx = endDegree.lng - downDegree.lng;break;case "model_edit_yArrow":_ty = endDegree.lat - downDegree.lat;break;case "model_edit_zArrow":_tz = -this._dStep * _yPix;break;case "model_edit_xCircle":_rx = this._rStep * _yPix;break;case "model_edit_yCircle":_ry = this._rStep * _xPix;break;case "model_edit_zCircle":_rz = this._rStep * _xPix;break;}this.updateModel(this._params, _tx, _ty, _tz, _rx, _ry, _rz);}}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
这里的movement.endPosition是屏幕坐标系,他是个二维坐标,用来判断鼠标横向滑动的距离和竖向滑动的距离,然后我们再来分析旋转的逻辑,也就是
_rx,_ry,_rz
,让旋转X轴时,其实是鼠标竖向滑动,Y和Z轴则是横向滑动,我们分别计算一个滑动系数,最后更新模型/*** 更新模型位置*/private updateModel(params: EditParams,tx: number,ty: number,tz: number,rx: number,ry: number,rz: number): void {//创建旋转矩阵const rotationX = Cesium.Matrix4.fromRotationTranslation(Cesium.Matrix3.fromRotationX(Cesium.Math.toRadians(params.rx + rx)));const rotationY = Cesium.Matrix4.fromRotationTranslation(Cesium.Matrix3.fromRotationY(Cesium.Math.toRadians(params.ry + ry)));const rotationZ = Cesium.Matrix4.fromRotationTranslation(Cesium.Matrix3.fromRotationZ(Cesium.Math.toRadians(params.rz + rz)));let position = Cesium.Cartesian3.fromDegrees(params.tx + tx,params.ty + ty,params.tz + tz )let matrix = Cesium.Transforms.eastNorthUpToFixedFrame(position)//旋转、平移矩阵相乘Cesium.Matrix4.multiply(matrix,rotationX,matrix)Cesium.Matrix4.multiply(matrix,rotationY,matrix)Cesium.Matrix4.multiply(matrix,rotationZ,matrix)//更新模型变换this._b3dm.root.transform = matrix;// //更新平移指示器if (this._coordArrows) {this.updateLineArrow(this._b3dm);}}
更新模型的操作很简单,分别求出旋转矩阵和平移矩阵,最后替换掉原有的矩阵,模型就发生变换了,这里需要注意的是,当模型发生平移后,可视化控件也需要跟着发生变换,此时需要更新可视化控件的位置,原理也很简单,重新求一下模型的位置,然后创建可视化控件即可
/*** 更新箭头指示器* @param b3dm - 3DTiles模型*/private updateLineArrow(b3dm: Cesium.Cesium3DTileset): void {//移除当前的箭头指示器this.removeCoordArrows();const viewer = this._viewer;//计算长度(使用包围球半径的1/3作为指示器长度)const length = b3dm.boundingSphere.radius / 3;const originalMatrix = this._b3dm.root.transform.clone()const ps = new Cesium.Cartesian3();Cesium.Matrix4.getTranslation(originalMatrix,ps)let originDegree = CoordTransform.transformCartesianToWGS84(viewer,ps)//创建平移向量(三个方向等长)const translateCartesian = new Cesium.Cartesian3(length, length, length);//深拷贝中心点位置const originPos = JSON.parse(JSON.stringify(ps));//计算目标点位置const targetDegree = this.getTransPosition(originPos, translateCartesian);//重新初始化箭头指示器this.initLineArrow(originDegree, targetDegree, length);}
- 最后是当鼠标滑动结束后,伴随而来的是鼠标抬起事件,在这个事件中需要记录模型变换后的姿态,重新解锁相机,移除掉鼠标移动的监听
//鼠标抬起事件this._handler.setInputAction(() => {if (!this._currentPick) return; // 增加空值检查viewer.scene.screenSpaceCameraController.enableRotate = true;this._currentPick.width = this._defaultWidth;this._currentPick = undefined;//更新最新参数this._params.tx += _tx;this._params.ty += _ty;this._params.tz += _tz;this._params.rx += _rx;this._params.ry += _ry;this._params.rz += _rz;//移除事件监听this._handler.removeInputAction(Cesium.ScreenSpaceEventType.MOUSE_MOVE);this._handler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_UP);}, Cesium.ScreenSpaceEventType.LEFT_UP);}
以上就是所有流程的原理,主要是需要弄明白cesium世界坐标系和局部坐标系之间的变换以及鼠标移动对应的是哪个轴的操作。
最新参数
this._params.tx += _tx;
this._params.ty += _ty;
this._params.tz += _tz;
this._params.rx += _rx;
this._params.ry += _ry;
this._params.rz += _rz;
//移除事件监听this._handler.removeInputAction(Cesium.ScreenSpaceEventType.MOUSE_MOVE);this._handler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_UP);}, Cesium.ScreenSpaceEventType.LEFT_UP);}
> 以上就是所有流程的原理,主要是需要弄明白cesium世界坐标系和局部坐标系之间的变换以及鼠标移动对应的是哪个轴的操作。
相关文章:
3dtiles平移旋转工具制作
3dtiles平移旋转缩放原理及可视化工具实现 背景 平时工作中,通过cesium平台来搭建一个演示场景是很常见的事情。一般来说,演示场景不需要多完善的功能,但是需要一批三维模型搭建,如厂房、电力设备、园区等。在实际搭建过程中&…...
【第十节】C++设计模式(结构型模式)-Flyweight( 享元)模式
目录 一、问题背景 二、模式选择 三、代码实现 四、总结讨论 一、问题背景 享元模式(Flyweight Pattern)在对象存储优化中的应用 在面向对象系统的设计与实现中,创建对象是最常见的操作之一。然而,如果一个应用程序使用了过多…...
VScode在windows10上使用clang-format
用途:自动调整代码格式,如缩进等。 clang-format官方文档:ClangFormat — Clang 21.0.0git documentation 前提:有一个.clang-format文件 下载LLVM:https://github.com/llvm/llvm-project/releases,将可…...
青少年编程与数学 02-010 C++程序设计基础 11课题、程序结构
青少年编程与数学 02-010 C程序设计基础 11课题、程序结构 一、C程序结构二、main函数1. main 函数的基本形式1.1 无参数形式1.2 带参数形式 2. 参数解释3. 示例3.1 无参数形式3.2 带参数形式 4. 编译和运行4.1 编译4.2 运行 5. main 函数的返回值6. 总结 三、预处理指令1. #in…...
Node.js与MySQL的深入探讨
Node.js与MySQL的深入探讨 引言 Node.js,一个基于Chrome V8引擎的JavaScript运行时环境,以其非阻塞、事件驱动的方式在服务器端应用中占据了一席之地。MySQL,作为一款广泛使用的开源关系型数据库管理系统,凭借其稳定性和高效性,成为了许多应用的数据库选择。本文将深入探…...
【洛谷贪心算法题】P2240部分背包问题
【解题思路】 贪心策略选择 对于部分背包问题,关键在于如何选择物品放入背包以达到最大价值。由于物品可以分割,遍历排序后的物品数组,根据物品重量和背包剩余容量的关系,决定是将整个物品放入背包还是分割物品放入背包ÿ…...
ArcGIS Pro技巧实战:高效矢量化天地图地表覆盖图
在地理信息系统(GIS)领域,地表覆盖图的矢量化是一项至关重要的任务。天地图作为中国国家级的地理信息服务平台,提供了丰富且详尽的地表覆盖数据。然而,这些数据通常以栅格格式存在,不利于进行空间分析和数据…...
AF3 pair_sequences函数解读
AlphaFold3 msa_pairing模块的pair_sequences函数的核心目标是基于 MSA(多序列比对)中的物种信息,在多条链之间建立 MSA 配对索引,从而帮助 AlphaFold3 捕捉共进化信息,提升蛋白复合物预测的准确性。函数pair_sequences 通过调用 _make_msa_df、 _create_species_dict 以…...
在VSCode 中使用通义灵码最新版详细教程
在 VSCode 中使用通义灵码:最新版详细教程与使用场景 Visual Studio Code(简称 VSCode)是一款由微软开发的轻量级、功能强大的开源代码编辑器,支持多种编程语言,深受开发者喜爱。而通义灵码(TONGYI Lingma…...
ssh和rdp踩坑
ssh和rdp(远程桌面)踩坑 使用微软账号登录windows的话,ssh的用户名是本地用户名(就是c盘用户文件夹下的用户名),rdp的用户名是微软账号用户名,但是密码都是微软账号的密码,跟登录密…...
Linux驱动学习(四)--字符设备注册
上一节讲到的字符设备注册与销毁是通过cdev_init、cdev_add、cdev_del等函数分步执行的,本小节用一种更简单的方式,来注册字符设备 register_chrdev 如果major为0,该函数将动态的分配一个主设备号并且返回对应的值如果major > 0ÿ…...
vue3 下载文件 responseType-blob 或者 a标签
在 Vue 3 中,你可以使用 axios 或 fetch 来下载文件,并将 responseType 设置为 blob 以处理二进制数据。以下是一个使用 axios 的示例: 使用 axios 下载文件 首先,确保你已经安装了 axios: npm install axios然后在你…...
Meta最新研究:从单张照片到3D数字人的革命性突破
随着人工智能技术的发展,3D建模和虚拟人物生成逐渐变得更加普及和高效。Meta(前身为Facebook)的最新研究成果展示了如何仅通过一张普通手机拍摄的照片就能生成高质量、全方位的3D数字人。这项技术不仅适用于虚拟试衣、游戏角色建模,还能广泛应用于AR/VR内容生成等领域。本文…...
大中型虚拟化园区网络设计
《大中型虚拟化园区网络设计》属于博主的“园区网”专栏,若想成为HCIE,对于园区网相关的知识需要非常了解,更多关于园区网的内容博主会更新在“园区网”专栏里,请持续关注! 一.前言 华为云园区网络解决方案(简称Cloud…...
Vue 项目中配置代理的必要性与实现指南
Vue 项目中配置代理的必要性与实现指南 在 Vue 前端项目的开发过程中,前端与后端地址通常不同,可能引发跨域问题。为了在开发环境下顺畅地请求后端接口,常常会通过配置**代理(proxy)**来解决问题。这篇文章将详细解析…...
chromadb向量数据库使用 (1)
目录 完整代码代码解释 完整代码 import chromadb chroma_client chromadb.Client()collection chroma_client.create_collection(name"my_collection")collection.add(documents["This is a document about pineapple","This is a document about…...
玩机日记 12 fnOS使用lucky反代https转发到外网提供服务
目录 1、安装lucky 2、更新lucky 3、上传ssl证书 4、设置安全入口,替换fnOS的应用url 5、添加https反代 这一篇主要是解决一下飞牛反代https的问题。可以先看玩机日记 12.5 在PVE Windows11上部署本地AI模型,使用群晖反代https转发到外网提供服务&a…...
5分钟学会SpringAI
引言 要开发一个Spring AI的入门案例,我们可以从一个简单的Spring Boot项目开始,然后集成Spring AI的功能来实现基本的生成式AI任务。下面是一个步骤指南,帮助你快速启动并运行一个简单的Spring AI应用。 步骤 1: 准备环境 首先࿰…...
专业的UML开发工具StarUML
专业的UML开发工具StarUML 可靠的软件建模软件StarUML StarUML 是一款支持统一建模语言 (UML)框架的开源建模软件。它提供了几种类型的图表,并允许用户生成多种语言的代码。在它的帮助下,软件开发人员可以创建设计、概念和编码解决方案。但是࿰…...
go语言环境下载与配置(Windows)
下载 Go下载 - Go语言中文网 - Golang中文社区 建议在D盘中创建文件夹安装到 D 盘 ,方便进行管理,然后进行傻瓜式安装。 安装 验证安装 go version 安装成功 配置环境变量 winE --> 右击此电脑 --> 选择属性 --> 高级系统设置 --> 点击…...
矩阵系列 题解
1.洛谷 P1962 斐波那契数列 题意 大家都知道,斐波那契数列是满足如下性质的一个数列: F n { 1 ( n ≤ 2 ) F n − 1 F n − 2 ( n ≥ 3 ) F_n \left\{\begin{aligned} 1 \space (n \le 2) \\ F_{n-1}F_{n-2} \space (n\ge 3) \end{aligned}\right. …...
C++ 正则表达式分组捕获入门指南
在 C 中,正则表达式(regex)是一种用于匹配字符串模式的强大工具。正则表达式不仅能帮助你查找符合特定模式的字符,还能捕获匹配的子字符串(即分组捕获)。这篇文章将介绍 C 正则表达式中的分组捕获机制&…...
动态数据表格:基于 PrimeFaces 的运行时列选择实现
在现代的 Web 应用开发中,动态数据表格是一个非常实用的功能,它允许用户根据自己的需求选择显示哪些列。这种灵活性不仅提升了用户体验,还能适应不同的数据展示需求。今天,我们将通过一个具体的实现案例,展示如何使用 …...
Plugin ‘mysql_native_password‘ is not loaded`
Plugin ‘mysql_native_password’ is not loaded mysql_native_password介绍1. 使用默认的认证插件2. 修改 my.cnf 或 my.ini 配置文件3. 加载插件(如果确实没有加载)4. 重新安装或检查 MySQL 版本 遇到错误 ERROR 1524 (HY000): Plugin mysql_nativ…...
Kotlin 知识点二 延迟初始化和密封类
对变量延迟初始化 Kotlin 语言的许多特性,包括变量不可变,变量不可为空,等等。这些特性 都是为了尽可能地保证程序安全而设计的,但是有些时候这些特性也会在编码时给我们带来不 少的麻烦。 比如,如果你的类中存在很多…...
DeepSeek开源周第二弹:DeepEP如何用RDMA+FP8让MoE模型飞起来?
一、引言:MoE模型的通信瓶颈与DeepEP的诞生 在混合专家(MoE)模型训练中,专家间的全对全(All-to-All)通信成为性能瓶颈。传统方案在跨节点传输时带宽利用率不足50%,延迟高达300μs以上。DeepSee…...
C++ Primer 成员访问运算符
欢迎阅读我的 【CPrimer】专栏 专栏简介:本专栏主要面向C初学者,解释C的一些基本概念和基础语言特性,涉及C标准库的用法,面向对象特性,泛型特性高级用法。通过使用标准库中定义的抽象设施,使你更加适应高级…...
无人机遥控器的亮度 和 两个工作频率
工作频率 2.4000-2.4835 GHz , 5.725-5.850 GHz 1.这是一个无人机的遥控器的两个工作频率,为什么会有两个工作频率? 无人机的遥控器采用双频段设计(2.4GHz 和 5.8GHz),主要是为了解决以下问题并优化性…...
ubuntu20.04安装docker
3台主机,2台都能正确安装,第三台怎么都安装不成功; 3台主机都是一样的配置和系统; 后来看来是其外网的ip不一样,导致第三台主机可能被Qiang,不过错误只是提示签名不正确,在设置签名时好像没有…...
【含文档+PPT+源码】基于过滤协同算法的旅游推荐管理系统设计与实现
项目介绍 本课程演示的是一款基于过滤协同算法的旅游推荐管理系统设计与实现,主要针对计算机相关专业的正在做毕设的学生与需要项目实战练习的 Java 学习者。 1.包含:项目源码、项目文档、数据库脚本、软件工具等所有资料 2.带你从零开始部署运行本套系…...
深入解析Crawl4AI:为AI应用量身定制的高效开源爬虫框架
引言 在当今数据驱动的时代,人工智能(AI)和大型语言模型(LLM)的发展对高质量数据的需求日益增长。如何高效地从互联网上获取、处理和提取有价值的数据,成为了研究人员和开发者面临的关键挑战。Crawl4AI作为…...
《Effective Objective-C》阅读笔记(下)
目录 内存管理 理解引用计数 引用计数工作原理 自动释放池 保留环 以ARC简化引用计数 使用ARC时必须遵循的方法命名规则 变量的内存管理语义 ARC如何清理实例变量 在dealloc方法中只释放引用并解除监听 编写“异常安全代码”时留意内存管理问题 以弱引用避免保留环 …...
深度生成模型(二)——基本概念与数学建模
上一篇笔记中提到了端到端模型底层核心采用了深度生成模型,先简单梳理一下 生成式人工智能(Artificial Intelligence Generated Content,AIGC)经历了从早期基于概率模型和规则系统的方法到现代深度生成模型的跨越式发展 深度神经…...
4.WebSocket 配置与Nginx 的完美结合
序言 在现代 web 应用中,WebSocket 作为一种全双工通信协议,为实时数据传输提供了强大的支持。若要确保 WebSocket 在生产环境中的稳定性和性能,使用 Nginx 作为反向代理服务器是一个明智的选择。本篇文章将带你了解如何在 Nginx 中配置 Web…...
【R语言】dplyr包经典函数summarise函数
dplyr包经典函数summarise函数,后面改名乘reframe函数了,但是summarise仍然适用 这个函数的返回结果是一个新的数据框,下面讲一下几种常见用法 示例数据为R自带的数据集mtcars 1.不分组 mtcars %>%summarise(mean mean(disp), n n()…...
Cuppa CMS v1.0 任意文件读取(CVE-2022-25401)
漏洞简介: Cuppa CMS v1.0 administrator/templates/default/html/windows/right.php文件存在任意文件读取漏洞 漏洞环境: 春秋云镜中的漏洞靶标,CVE编号为CVE-2022-25401 漏洞复现 弱口令行不通 直接访问administrator/templates/defau…...
8.Dashboard的导入导出
分享自己的Dashboard 1. 在Dashboard settings中选择 JSON Model 2. 导入 后续请参考第三篇导入光放Dashboard,相近...
RabbitMQ 的介绍与使用
一. 简介 1> 什么是MQ 消息队列(Message Queue,简称MQ),从字面意思上看,本质是个队列,FIFO先入先出,只不过队列中存放的内容是message而已。 其主要用途:不同进程Process/线程T…...
unity学习58:下拉列表框 dropdown的caption和options
目录 1 下拉列表框 dropdown 1.1 创建dropdown 1.2 dropdown的子物体构成 1.3 drop的子物体构成:默认灰色的模板 template 1.3.1 默认灰色的模板 template 实际是生效的,只是不直接显示 1.3.2 其中 item下面是下拉选项的格式 1.3.3 可以修改和新增 1.4 drop…...
STM32中使用PWM对舵机控制
目录 1、硬件JIE 2、PWM口配置 3、角度转换 4、main函数中应用 5、工程下载连接 1、硬件介绍 单片机:STM32F1 舵机:MG995 2、PWM口配置 20毫秒的PWM脉冲占空比,对舵机控制效果较好 计算的公式: PSC、ARR值的选取…...
如何免费使用稳定的deepseek
0、背景: 在AI辅助工作中,除了使用cursor做编程外,使用deepseek R1进行问题分析、数据分析、代码分析效果非常好。现在我经常会去拿行业信息、遇到的问题等去咨询R1,也给了自己不少启示。但是由于官网稳定性很差,很多…...
STM32内存五区及堆栈空间大小设置(启动文件浅析)
前言 嘿,朋友们!今天咱们来聊聊STM32的内存五区和堆栈空间大小设置。这可是嵌入式开发里的“必修课”,要是没整明白,程序说不定就“翻车”了。别担心,我这就带你一步步搞懂这事儿,让你轻松上手,…...
LVS+Keepalived高可用群集配置案例
以下是一个 LVSKeepalived 高可用群集配置案例: 1、环境准备 LVS 主调度器(lvs1):IP 地址为 192.168.8.101,心跳 IP 为 192.168.4.101LVS 备调度器(lvs2):IP 地址为 192.168.8.102…...
MySQL 中如何解决深度分页的问题? MySQL中 join、inner join、left join、right join区别
MySQL 中如何解决深度分页的问题? 在 MySQL 中解决深度分页问题的核心思路是减少扫描的数据量,尤其是避免通过 LIMIT offset, size 导致的大范围数据扫描。以下是三种优化方法及其原理、适用场景和注意事项: 1. 子查询 覆盖索引(…...
本地部署DeepSeek-R1(Ollama+Docker+OpenWebUI知识库)
安装Ollama 打开 Ollama官网 https://ollama.com/下载安装 Ollama服务默认只允许本机访问,修改允许其它主机访问 OLLAMA_HOST0.0.0.0 ollama serve也可以添加系统环境变量 都知道模型体积很大,顺便也通过环境变量修改模型存放位置,我这…...
ubuntu22.04安装docker engine
在Ubuntu 22.04上安装Docker Engine可以通过以下步骤完成: 更新系统包索引: sudo apt update安装必要的依赖包: 这些包允许apt通过HTTPS使用仓库。 sudo apt install -y apt-transport-https ca-certificates curl software-properties-commo…...
Protobuf原理与序列化
本文目录 1. Protobuf介绍2. Protobuf的优势3. 编写Protobuf头部全局定义消息结构具体定义字段类型定义标签号Base128编码 4. TLVProtobuf的TLV编码如何通过Varint表示300? 5. 编译Protobuf6. 构造消息对象 前言:之前写项目的时候只是简单用了下Protobuf…...
Redis|事务
文章目录 是什么能干嘛Redis 事务 VS 数据库事务怎么玩小总结 是什么 首先回想一下什么是数据库的事务?数据库事务是指作为单个逻辑单元执行的一系列操作,具备以下四个关键特性(ACID): 原子性(Atomicity&am…...
树莓百度百科更新!宜宾园区业务再添新篇
树莓集团宜宾园区业务不断拓展,主要体现在以下几个方面: 产业布局 -聚焦数字经济核心领域:涵盖软件开发、人工智能、大数据等,吸引众多上下游企业入驻,形成从芯片研发、软件开发到系统集成的完整产业链条。 -推进“双…...
设计模式教程:模板方法模式(Template Method Pattern)
一、概述 模板方法模式(Template Method Pattern) 是一种行为型设计模式,旨在定义一个操作中的算法骨架,而将一些步骤的具体实现延迟到子类中。通过模板方法模式,父类可以不改变算法结构的情况下,让子类重…...