news 2026/11/2 14:43:17

Cesium三维行政区划立体贴图:UV映射、材质分层与性能优化实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Cesium三维行政区划立体贴图:UV映射、材质分层与性能优化实践

做三维地图这行,只要接到政务展示、园区汇报、城市驾驶舱类的需求,十有八九绕不开同一个画面:一片区域被整体拔起来,做成有厚度的立体块,顶面严丝合缝地贴着一张行政区划图,边线带着光,选中时还会亮一下。这个效果在 Cesium 里的叫法很朴素,就是三维立体的行政区划贴图。听起来简单,真动手做就会撞上三个拦路虎:顶面贴图被拖成橡皮筋、侧壁颜色和顶面糊成一片、区域一多帧率直接掉到个位数。

我前后做过七八个类似的需求,从单个园区展示到几十个区县同屏的指挥大屏,方案从最粗暴的 Entity 堆叠一路改到自定义 Primitive 手搓 UV。这篇把整条链路摊开讲,包括参数怎么算、坑埋在哪、哪一步可以偷懒哪一步绝对不能省。刚接触 Cesium 的前端能照着复现,写过一段时间 Entity 的老手也能从 UV 分配和材质分层这两块里拎出点新东西。

1. 先把"立体行政区划贴图"的真实诉求拆开

1.1 三种业务形态对应三套技术指标

同样是"立体贴图",不同场景的验收标准完全不一样,先分清楚再动手,能省掉一半返工。

第一种是展示型,典型画面是屏幕上就三五个区域,立体块高度统一,顶面贴一张带区域名称和边界的图,鼠标悬停变色。这种对精度要求低,用 Entity 叠层三十行代码就能出效果,性能也不是问题。

第二种是对比型,比如同一批区域按某个指标做立体高度,面积大的矮、数值高的挺拔,顶面贴图要和实际地理范围严格对齐,错一点点就会被业务方指着屏幕说"这块偏了"。这时候 UV 就绕不过去了,必须精确控制顶面纹理坐标。

第三种是大屏监控型,几十上百个区县同屏,每个都要有立体、有贴图、有边线、有选中态,还要求帧率稳在 40 以上。这种场景下 Entity 的数量就是个雷,必须走 Primitive 合并的路子。

我在第二个项目里吃过亏,一开始拿展示型的方案去套对比型,顶面贴图用默认 UV,结果一个南北狭长的区被横向拉伸得面目全非,改了三版才意识到问题不在材质而在纹理坐标本身。

1.2 为什么不能直接给 polygon 一张图了事

很多人第一反应是polygon.material = new ImageMaterialProperty({ image }),代码一跑,图是上去了,但马上发现两个问题。

第一个是顶面和侧面共用同一份材质。Cesium 的PolygonGraphics在拉伸之后,顶面、底面、侧壁是同一个几何体,只有一个 material 槽位。你想让顶面贴图、侧壁半透明纯色,单靠一个 Entity 做不到,除非在着色器里做面朝向判断——而 Entity 生成的几何默认不带法线属性,这条路基本堵死。

第二个是顶面 UV 是按包围盒归一化的。Cesium 在生成多边形几何时,会把顶点投影到一个局部平面上,算出一个轴对齐的包围矩形,然后把纹理坐标按这个矩形拉伸到 0 到 1。这意味着一个细长区域和一个方正区域,吃到的是同一张图的不同拉伸比例,视觉上就是一张图被揉变形。

注意:这两个限制不是 Cesium 的缺陷,而是通用几何管线为了兼容任意多边形做的取舍。理解这一点,后面所有的方案选择就都顺了。

2. 原理拆解:拉伸之后纹理到底怎么走

2.1 从 GeoJSON 到多边形几何的完整链路

搞明白链路,排查问题才有方向。一份行政区划数据的流转大致是这样:

  1. GeoJSON 里的坐标是经纬度数组,先转成Cesium.Cartesian3.fromDegrees的世界坐标,地球上这个坐标是带弧度的;
  2. PolygonGraphics把顶点投到以多边形中心为原点的切平面上,变成一个二维多边形;
  3. 对这个二维多边形做三角剖分,剖分用的是耳切法,遇到自相交的多边形会直接失败并返回 undefined;
  4. 计算包围矩形,把每个顶点的平面坐标映射成st,也就是 0 到 1 的纹理坐标;
  5. 如果有extrudedHeight,再沿法线方向复制一层顶点,生成侧壁三角形;
  6. 最终打包成Geometry,交给Primitive渲染。

第 4 步就是顶面变形的根源。第 5 步则是侧壁颜色的来源,因为侧壁顶点也要st,Cesium 只能另算一套。

2.2 顶面 UV 的归一化规则和那个隐藏参数

默认规则是包围盒映射,刚才说了。但 Cesium 留了个口子:PolygonGraphics.textureCoordinates。这个属性很少有人用,文档也写得简略,但它就是解决顶面错位的钥匙。

它的类型是PolygonHierarchy,值是一组Cartesian2,数量必须和外环顶点数一致,每个点对应一个顶点的st值。你只要自己算好每个顶点在多边形包围矩形里的相对位置,填进去,顶面贴图就能严格对齐地理范围。

// 假设 positions 是 Cartesian3 顶点数组 const rectangle = Cesium.Rectangle.fromCartesianArray(positions); const textureCoordinates = positions.map(p => { const carto = Cesium.Cartographic.fromCartesian(p); const s = (carto.longitude - rectangle.west) / (rectangle.east - rectangle.west); const t = (carto.latitude - rectangle.south) / (rectangle.north - rectangle.south); return new Cesium.Cartesian2(s, t); }); entity.polygon.textureCoordinates = new Cesium.PolygonHierarchy( textureCoordinates, [] // 内环(洞)的纹理坐标,没有洞就传空数组 );

这段代码的价值在于:不管你的多边形多细长,只要贴图的经纬度范围和数据的经纬度范围一致,贴上去就是对齐的。我一般会顺手加一句stRotation = 0,避免别处设了旋转值继承过来。

需要提醒一句,textureCoordinates的顶点顺序必须和hierarchy严格一致,包括起点的位置。数据如果是绕序反的(顺时针),这里也得跟着反,否则贴图会镜像。我遇到过一次,图整个翻了个个儿,查了半天以为是坐标系问题,结果就是绕序。

2.3 侧壁为什么要单独一套 UV

侧壁的st是 Cesium 内部另算的:横向按底边周长比例分配,纵向按墙高归一化到 0 到 1。这套规则对"沿着墙走一圈的条形纹理"是友好的,但对"竖直方向的渐变"也友好,唯独对"网格状纹理"不友好——因为横向的疏密会随周长变化,长边上的格子会被拉宽。

反过来说,如果你侧壁本来就想做纯色或者竖向渐变,这套 UV 完全够用,没必要上自定义 Primitive。真正需要手搓侧壁 UV 的情况只有两种:一是侧壁要贴特定图案并且要求等距,二是侧壁要跟顶面做视觉上的接缝对齐。

提示:判断是否需要自定义 Primitive,就问自己一句"侧壁需不需要等距纹理"。不需要,走 Entity 方案;需要,老老实实手搓几何。

3. 快速落地:两个 Entity 叠出顶面贴图加立体侧壁

3.1 数据清洗和高度参数的确定

动手前先处理数据。行政区划数据常见的毛病有三个:坐标精度过高(小数点后十几位,白占内存)、存在重复点、存在自相交。前两个用 Douglas-Peucker 抽稀就能解决,Cesium.Cartographic层面做个简单的距离阈值过滤也行,一般区域抽到 500 米精度视觉上就看不出来了。第三个要特别注意,自相交会让三角剖分直接失败,几何体根本不生成,控制台还只给你一句DeveloperError,很容易懵。

高度参数我一般不用固定值,按面积开方来算,视觉上更协调:

// areaKm2 是该区域的近似面积,单位平方公里 function calcHeight(areaKm2) { const base = Math.sqrt(areaKm2) * 120; return Math.min(Math.max(base, 1500), 9000); }

拿几个真实规模验证一下:面积 100 平方公里的小区,算出来 1200,被拉到 1500;面积 900 平方公里的中等区县,3600;面积 2500 的大区,6000。这个区间在大屏上看着比较"挺拔"但不夸张。要是所有区域都统一高度,视觉上一眼看不出主次,失去了立体化的意义。

3.2 顶面贴图 Entity 的四个关键参数

顶面那块我用一个独立的 Entity 来做,只负责贴图,不参与拉伸。核心参数就四个:

viewer.entities.add({ polygon: { hierarchy: new Cesium.PolygonHierarchy(positions), height: baseHeight + height + 0.05, // 关键:比立体块高一点点 material: new Cesium.ImageMaterialProperty({ image: textureImage, // 复用同一个 Image 对象 repeat: new Cesium.Cartesian2(1, 1), color: Cesium.Color.WHITE.withAlpha(0.95), transparent: true }), textureCoordinates: new Cesium.PolygonHierarchy(uvs, []), stRotation: 0, classificationType: Cesium.ClassificationType.TERRAIN, zIndex: 10 } });

height那个+0.05是关键。它让顶面贴图浮在立体块上方 5 厘米,避免两个几何体在同一个高度上打架导致闪烁。5 厘米在地图尺度上肉眼看不出来,但足够骗过深度测试。这个技巧我在好几个项目里都用过,属于"看起来土但极其有效"的那一类。

image一定要传同一个Image对象或者已经加载好的HTMLCanvasElement,别每次都传 URL 字符串。ImageMaterialProperty传字符串时会走内部的图片加载,多个实体各自加载同一个 URL,浏览器缓存能救一部分,但解码和上传纹理还是各做一遍,区域一多内存蹭蹭涨。

classificationType设成TERRAIN是为了让贴图跟着地形走,如果场景是纯平地,设成CESIUM_3D_TILE或者不设都行。

3.3 立体块的材质和层级关系

立体块本身用另一个 Entity,负责体积感:

viewer.entities.add({ polygon: { hierarchy: new Cesium.PolygonHierarchy(positions), height: baseHeight, extrudedHeight: baseHeight + height, material: new Cesium.ColorMaterialProperty( Cesium.Color.fromCssColorString('#1e6fd9').withAlpha(0.72) ), outline: true, outlineColor: Cesium.Color.fromCssColorString('#7fd4ff'), outlineWidth: 2, closeTop: true, closeBottom: false } });

closeTop: true是为了让立体块有个封顶面,即使上面盖了贴图,从侧面斜视角看也不会有穿透感。closeBottom: false是因为底面永远看不见,开着纯属浪费三角形。

这里有个视觉细节值得说:立体块的材质用半透明,比不透明的观感好很多。因为半透明能看到被它挡住的地面纹理和路网,立体块和地面之间有"融合"的感觉,不像一块实心砖头砸在地图上。透明度的经验值在 0.65 到 0.8 之间,太低会显得脏,太高就没有"实心"的质感了。

3.4 描边、发光和地面贴合

描边这块,outline: true在 WebGL 实现里走的是gl.LINES,线宽在很多平台上被驱动限制为 1 像素,设outlineWidth也没用。想做出真正的发光边框,得用PolylineGraphics单独画:

viewer.entities.add({ polyline: { positions: [...positions, positions[0]], width: 3, material: new Cesium.PolylineGlowMaterialProperty({ color: Cesium.Color.fromCssColorString('#8fe3ff'), glowPower: 0.28, taperPower: 0.6 }), clampToGround: false } });

PolylineGlowMaterialProperty的glowPower越大光晕越窄越锐利,我一般用 0.2 到 0.35 之间。taperPower控制首尾收缩,设 0.6 到 1 比较自然。注意clampToGround要设成 false,因为这条线是画在立体块顶部高度的,不是地面上。

4. 精确控制:自定义 Primitive 手搓顶面和侧壁

Entity 方案在"侧壁要纯色"这个前提下够用了。一旦侧壁要求竖向渐变、流光扫描,或者区域数量上到几十个,就得换成自定义 Primitive。这条路麻烦,但可控性完全不一样。

4.1 坐标降精度和局部坐标系的建立

这一步是很多人容易漏的。Cesium 的世界坐标数值动辄几百万米,直接塞进Float32Array会有明显的精度损失,表现为模型轻微抖动、顶点接缝错位。标准做法是把顶点转到局部坐标系:

const cartesians = positions.map(p => Cesium.Cartesian3.fromDegrees(p[0], p[1], baseHeight)); const center = Cesium.BoundingSphere.fromPoints(cartesians).center; const modelMatrix = Cesium.Transforms.eastNorthUpToFixedFrame(center); const inverse = Cesium.Matrix4.inverseTransformation(modelMatrix, new Cesium.Matrix4()); const local = cartesians.map(p => Cesium.Matrix4.multiplyByPoint(inverse, p, new Cesium.Cartesian3()));

local里的坐标数值就变成了几十到几千米的量级,Float32 完全够用。渲染时把primitive.modelMatrix设成modelMatrix,位置就还原回去了。这套路子在 Cesium 内部到处都是,自己建几何的时候照抄就行。

4.2 顶面三角剖分和 UV 映射

Cesium 暴露了Cesium.PolygonPipeline.triangulate,虽然带点内部 API 的性质,但一直挺稳定:

const flat = []; local.forEach(p => flat.push(p.x, p.y)); const triangleIndices = Cesium.PolygonPipeline.triangulate(flat, []); if (!triangleIndices || triangleIndices.length === 0) { throw new Error('三角剖分失败,检查多边形是否自相交'); }

拿到索引之后,顶面的 UV 我按局部坐标的包围矩形来算,这样和 3.2 节的textureCoordinates逻辑一致,两套方案切换时贴图位置不会跳:

let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity; local.forEach(p => { minX = Math.min(minX, p.x); maxX = Math.max(maxX, p.x); minY = Math.min(minY, p.y); maxY = Math.max(maxY, p.y); }); const width = maxX - minX || 1; const height = maxY - minY || 1; const sts = []; local.forEach(p => { sts.push((p.x - minX) / width, (p.y - minY) / height); });

如果想更精确,可以把包围矩形换成地理经纬度矩形,但那需要把局部坐标反算回经纬度,多一道换算,收益不大。局部平面在几十公里的范围内误差可以接受。

4.3 侧壁条带生成和按弧长分配 U

侧壁的逻辑是:遍历相邻两个顶点,生成一个四边形(两个三角形),UV 的横向按累计弧长归一化,纵向按高度归一化。

const wallPositions = []; const wallSts = []; const wallIndices = []; let totalLength = 0; const segLengths = []; for (let i = 0; i < local.length; i++) { const a = local[i]; const b = local[(i + 1) % local.length]; const len = Math.hypot(b.x - a.x, b.y - a.y); segLengths.push(len); totalLength += len; } let accumulated = 0; for (let i = 0; i < local.length; i++) { const a = local[i]; const b = local[(i + 1) % local.length]; const u0 = accumulated / totalLength; const u1 = (accumulated + segLengths[i]) / totalLength; accumulated += segLengths[i]; const baseIdx = wallPositions.length / 3; // 底部两点、顶部两点 wallPositions.push(a.x, a.y, 0, b.x, b.y, 0, b.x, b.y, height, a.x, a.y, height); wallSts.push(u0, 0, u1, 0, u1, 1, u0, 1); wallIndices.push(baseIdx, baseIdx + 1, baseIdx + 2); wallIndices.push(baseIdx, baseIdx + 2, baseIdx + 3); }

这里的关键是u用弧长比例而不是顶点索引比例。用索引比例的话,顶点密集的地方纹理被挤压,顶点稀疏的地方被拉伸,尤其行政区划数据抽稀程度不一致时特别明显。换成弧长之后,纹理在水平方向就是等距的,条纹类的图案才看得过去。

纵向的t从 0 到 1,对应贴图的下沿到上沿。如果你希望贴图在竖直方向平铺 N 次,就在着色器里乘上 N,不用改几何。

4.4 组装 Geometry 和 Appearance

几何属性、索引、包围球都齐了,直接构造:

const geometry = new Cesium.Geometry({ attributes: { position: new Cesium.GeometryAttribute({ componentDatatype: Cesium.ComponentDatatype.FLOAT, componentsPerAttribute: 3, values: new Float32Array(wallPositions) }), st: new Cesium.GeometryAttribute({ componentDatatype: Cesium.ComponentDatatype.FLOAT, componentsPerAttribute: 2, values: new Float32Array(wallSts) }), normal: new Cesium.GeometryAttribute({ componentDatatype: Cesium.ComponentDatatype.FLOAT, componentsPerAttribute: 3, values: new Float32Array(wallPositions.length) // 朝上即可,本项目不用法线光照 }) }, indices: new Uint32Array(wallIndices), primitiveType: Cesium.PrimitiveType.TRIANGLES, boundingSphere: Cesium.BoundingSphere.fromVertices(wallPositions) });

indices用Uint32Array而不是Uint16Array,因为顶点数超过 65535 之后 16 位会溢出,剖分出来一片黑。这个坑我踩过一次,一个超复杂的边界剖出七万多个顶点,用 16 位索引直接花屏。

normal那个属性是给MaterialAppearance的MaterialSupport.TEXTURED用的,它要求 position、normal、st 三件套齐全,缺一个就抛错。我们不用光照,填全零也没事,只要长度对。

然后创建材质和 Primitive:

const wallMaterial = new Cesium.Material({ fabric: { type: 'WallGradient', uniforms: { bottomColor: new Cesium.Color(0.08, 0.35, 0.75, 0.15), topColor: new Cesium.Color(0.25, 0.72, 1.0, 0.85), image: textureImage }, source: ` uniform vec4 bottomColor; uniform vec4 topColor; uniform sampler2D image; czm_material czm_getMaterial(czm_materialInput materialInput) { czm_material material = czm_getDefaultMaterial(materialInput); vec2 st = materialInput.st; vec4 tex = texture2D(image, st); vec4 grad = mix(bottomColor, topColor, st.t); material.diffuse = mix(grad.rgb, tex.rgb, 0.35); material.alpha = mix(grad.a, tex.a, 0.3); return material; } ` } }); const wallPrimitive = new Cesium.Primitive({ geometryInstances: new Cesium.GeometryInstance({ geometry: geometry }), appearance: new Cesium.MaterialAppearance({ material: wallMaterial, materialSupport: Cesium.MaterialAppearance.MaterialSupport.TEXTURED, translucent: true, flat: true }), modelMatrix: modelMatrix }); viewer.scene.primitives.add(wallPrimitive);

顶面那一半重复同样的流程,只是 UV 用 4.2 的计算,剖分用triangulate的结果,材质换成贴图。两块 Primitive 各自拥有独立材质,这就是精确控制的全部意义。

5. 材质进阶:流光、渐变和扫描

5.1 自定义 Material 的 fabric 结构

Cesium 的Material本质是一段 GLSL 片段加一组 uniform。写自定义材质,只要实现czm_getMaterial这个函数就行,返回值是一个czm_material结构体。结构体里常用的字段有diffuse(基础色)、alpha(透明度)、emission(自发光)、specular、shininess。想让物体在暗景里发光,往emission上堆颜色就行。

czm_material czm_getMaterial(czm_materialInput materialInput) { czm_material material = czm_getDefaultMaterial(materialInput); vec2 st = materialInput.st; vec4 tex = texture2D(image, vec2(st.s * repeatU, st.t * repeatV)); material.diffuse = tex.rgb; material.alpha = tex.a * opacity; material.emission = tex.rgb * emissionStrength; return material; }

repeatU、repeatV这两个 uniform 是控制平铺次数的,比在几何上改 UV 方便得多,尤其适合那种"网格密度要能实时调"的需求。

注意:fabric 里声明为sampler2D的 uniform 必须传Cesium.Texture或者能转成纹理的对象,直接塞 Image 对象在部分版本里会报错,稳妥做法是new Cesium.Material.Texture包一层,或者干脆用viewer.scene.context手动创建。

5.2 竖向渐变和顶部高光

侧壁最常见的需求是从底部深蓝渐变到顶部亮蓝,顶部再来一道亮边。渐变用mix按st.t插值就行,顶部高光用smoothstep卡一条窄带:

vec4 grad = mix(bottomColor, topColor, pow(st.t, 1.4)); float rim = smoothstep(0.92, 1.0, st.t) * rimIntensity; vec3 finalColor = grad.rgb + vec3(rim) * rimColor;

pow(st.t, 1.4)那个指数是控制渐变曲线形状的。指数越大,颜色越集中在顶部,底部显得更暗、更有"沉稳"的感觉。我在政务类大屏上一般用 1.3 到 1.6,科技风可以拉到 2.0 以上,做出很硬的分界。

顶部高光的smoothstep(0.92, 1.0, ...)意味着最上面 8% 的高度被点亮,这个比例和立体块的实际高度有关。块高 3000 米的时候,8% 是 240 米,视觉上就是一细条;块高只有 500 米的时候,240 米就显得很宽了。所以严格来说应该用世界高度来算,但视觉上稍微调一下参数就够了,不必较真。

5.3 沿边界流动的扫描光带

流光效果是这类需求的标配。思路是让st.s随时间偏移,再用fract取小数部分制造循环:

uniform float time; uniform float speed; float flow = fract(st.s * repeatU - time * speed); float band = smoothstep(0.0, 0.08, flow) * (1.0 - smoothstep(0.08, 0.18, flow)); vec3 glow = vec3(0.6, 0.9, 1.0) * band * intensity; finalColor += glow;

speed的单位是"每秒跑过多少个完整周长",取值在 0.15 到 0.4 之间比较舒服,太快会晕。repeatU和它配合,repeatU是几,光带就有几条,各自独立流动。

时间更新不要每帧创建一个新材质,共享一个Material实例,在preUpdate里改 uniform:

const start = performance.now(); viewer.scene.preUpdate.addEventListener(() => { wallMaterial.uniforms.time = (performance.now() - start) / 1000; });

如果场景开了requestRenderMode,记得在改 uniform 之后调viewer.scene.requestRender(),否则画面不会刷新。这个细节在开发环境里往往看不出来(因为鼠标一动就重绘了),部署到自动巡航的页面上就变成"流光不动",很隐蔽。

5.4 多区域共享材质和 uniform

几十个区域的时候,每个区域一个材质实例会导致 shader 编译几十次,首次加载卡顿明显。正确的做法是所有区域共用一个 Material 实例,uniform 也共用。如果某些区域需要不同的颜色,就在几何上做文章,比如把颜色烘进顶点色,或者按区域分组,每组一个材质。

分组数量控制在 5 组以内,编译开销基本可以忽略。我一般按"高值、中值、低值、选中"分四组,每组一个材质,每个材质只改一两个 uniform,管理起来也清爽。

6. 问题排查实录

6.1 贴图变形与错位

现象可能原因处理方式
顶面贴图整体拉伸未指定 textureCoordinates,走包围盒归一化手动计算 UV 并赋给 polygon.textureCoordinates
顶面贴图镜像多边形绕序与 UV 顺序不一致反序 UV 数组,或反序 hierarchy
只有部分区域贴图正常数据含内环(洞),未给洞的 UVtextureCoordinates 的第二个参数补上内环坐标
侧壁纹理疏密不均用了顶点索引比例分配 U改用累计弧长比例
顶面贴图偏移半格贴图本身含边距或留白调整贴图内容,或在 shader 里st * scale + offset

表格里最后一条很实用。很多时候不是代码问题,是美术给的贴图四周带了一圈透明边距,按 0 到 1 映射上去自然整体偏移。这种情况我一般直接让美术重新导一版不带边距的,比在代码里凑 offset 靠谱。

6.2 渲染闪烁与深度冲突

立体块和顶面贴图贴在一起,最常见的症状是斜视角下边缘处有闪烁的锯齿带。原因是两个面几乎共面,深度值在浮点精度内分不出前后。

处理办法按优先顺序排:

  1. 给顶面加一个微小的抬高量,0.05 到 0.2 米,按场景尺度调,这是最省事的;
  2. 给两个材质分别设translucent,让渲染顺序可控,半透明物体默认后绘制并且不写深度;
  3. 极端情况下用polygonOffset,但 Cesium 的 Entity 层面不直接暴露这个,得下沉到 Primitive 改MaterialAppearance,成本较高。

还有一种是侧面和地形的冲突,立体块底部和起伏地形穿插。如果数据本身是平底,把height设成区域的最高地形高度再加一点余量就行;如果是起伏的,用perPositionHeight: true让每个顶点各自贴合,但那样挤出的侧壁高度就不统一了,视觉上会参差不齐,看需求取舍。

6.3 性能和内存

几十个 Entity 的时候性能问题还不明显,上百个就开始掉帧。几个实测有效的优化点:

  • 把顶面贴图和侧壁的 Entity 换成 Primitive,所有区域的几何合并进一个GeometryInstance数组。合并之后 draw call 从几百降到个位数,帧率提升非常明显。
  • 关闭不需要的面。closeBottom: false、不需要描边就不加outline。
  • 贴图尺寸别超过 1024×1024。行政区划贴图本质是色块和线,用 512 就够了,2048 的贴图在几十个区域上会把显存吃光。
  • 抽稀边界点。一个精度很高的区县 GeoJSON 可能有上万个点,剖分出来的三角形数量巨大。抽到 300 到 800 个点,视觉上完全看不出差别,顶点数直接砍掉一个数量级。
  • 开启scene.requestRenderMode,静态场景下不再每帧重绘。

提示:判断是不是顶点数的问题很简单,把viewer.scene.debugShowFramesPerSecond打开,然后单独关掉立体层看帧率变化。如果关掉之后立刻恢复,那就是几何数量的问题,不是材质的问题。

7. 大范围场景下的取舍记录

做到跨市、跨省这种尺度,前面那套"一个区域一个立体块"的思路就得重新审视了。我遇到过的最直接的问题是视觉比例失真:在几百公里的视野下,为了让立体块看起来有厚度,高度得设到一万米以上,结果从斜视角看,这些块像一根根针插在地上,反而更难看。

比较稳妥的处理是分区处理。视野远的时候只保留色彩区分,不拉伸;视野拉近到市级以内,再逐步显示立体块。Cesium 本身没有给 Entity 做 LOD 的现成机制,得自己在camera.changed里判断高度,动态加减实体。实现上不复杂,但要注意节流,camera.changed触发非常频繁,加个 200 毫秒的防抖,不然每帧都在增删实体,卡得比不优化还厉害。

另一个取舍是是否真的需要贴图。有时候业务方要的"贴图"其实就是一层颜色区分加边线,这种情况下用纯色材质加描边,视觉效果差不了多少,性能却能省一大截。我现在的习惯是先出一版纯色的,给业务方看,确认确实需要纹理细节再往上加。多数时候他们看完纯色版就说"这样就行"。

至于顶面贴图的对齐精度,我的底线是:在默认视角高度下,贴图上的边界线和几何边界的偏差不超过 2 个像素。超过这个数就会被看出来,尤其是那种边界线是红色、几何边是蓝色的对比场景,错位一眼就露馅。想验证的话,把贴图画成棋盘格,投上去扫一眼,哪个区域变形立刻就能定位到,比凭感觉调参数高效得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/2 14:41:55

Seaquel数据库工具:高性能跨平台开发实战解析

1. Seaquel工具核心定位解析Seaquel作为新一代数据库开发工具&#xff0c;其设计理念直击现代开发者三大痛点&#xff1a;多数据库环境切换成本高、大数据量操作卡顿、跨团队协作困难。我在实际使用中发现&#xff0c;它通过Rust构建的高性能内核能轻松处理百万级数据表的秒级响…

作者头像 李华
网站建设 2026/11/2 14:41:32

基于知识图谱与BERT+BiLSTM+CRF的医生推荐系统实现

简介&#xff1a;这套基于BERT、BiLSTM与CRF混合模型的医生推荐系统知识图谱项目&#xff0c;是一套面向计算机专业毕业设计的完整解决方案&#xff0c;也适用于课程设计、期末大作业或项目实战练习。它通过深度学习从医学文本中抽取实体与关系&#xff0c;构建包含疾病、症状、…

作者头像 李华
网站建设 2026/11/2 14:40:59

VoiceStudio:从干音到成品的语音处理流水线搭建指南

语音这行干久了&#xff0c;最让人头疼的往往不是嗓子状态&#xff0c;而是流程本身。环境底噪、设备电平、降噪参数、AI 合成、导出格式&#xff0c;任何一环掉链子&#xff0c;之前几个小时全白干。我最近把自己这整套流程收拢成了一个个人项目&#xff0c;名字就叫 VoiceStu…

作者头像 李华
网站建设 2026/11/2 14:40:45

VoiceStudio:本地语音合成工作流搭建指南,从文本转语音到音色克隆

1. 为什么我会动手做 VoiceStudio&#xff1a;自制配音工坊的起因与定位 大概在半年前&#xff0c;我开始做一档偏技术科普类的音频栏目&#xff0c;每期需要录制 10 到 15 分钟的口播内容。原以为买个麦克风就能解决一切&#xff0c;结果三周之后我发现问题根本不在收音&#…

作者头像 李华
网站建设 2026/11/2 14:40:07

SRC漏洞挖掘入门与25个主流平台详解

1. SRC漏洞挖掘入门指南对于刚接触网络安全的新手来说&#xff0c;SRC&#xff08;Security Response Center&#xff09;漏洞提交平台无疑是最佳的实战起点。我最初接触漏洞挖掘时&#xff0c;也是从这些平台开始积累经验。SRC平台为白帽黑客提供了合法测试和报告漏洞的渠道&a…

作者头像 李华
网站建设 2026/11/2 14:38:30

VOX TOKYO黑客松:语音AI技术实战与社会创新

1. 项目概述&#xff1a;VOX TOKYO黑客松的核心价值 这场名为"VOX TOKYO"的黑客松活动正在日本川崎市集结全球AI开发者&#xff0c;以语音技术为核心&#xff0c;探索声音改变社会的可能性。不同于常规编程马拉松&#xff0c;它提供了TRAE开发框架、MiniMax自然语音合…

作者头像 李华