1.多边形裁切
1.1 基本流程
cesium117版本添加了多边形裁切功能,本文分析源码,看看是如何处理的。多边形裁切的大概流程分为4部分:
- 通过经纬度坐标传入多个闭合的边界;
- 将多个边界打包成两张纹理,一张是每个多边形的坐标,另一张是每个多边形的边界;
- 将两张多边形纹理通过一个计算着色器(屏幕空间着色器模拟计算着色器)生成一张符号距离场纹理;
- 将这两张图传入地球瓦片和3DTiles的着色器中进行多边形裁切.
1.2 多边形纹理打包
这是在js代码中处理的,使用了ClippingPolygon和ClippingPolygonCollection两个类,ClippingPolygon类负责每个多边形的坐标收集以及每个多边形的范围计算。
以下是ClippingPolygon类的主要代码,过程比较简单。
/*** Computes a rectangle with the spherical extents that encloses the polygon defined by the list of positions, including cases over the international date line and the poles.* 根据给定的位置列表计算球上的坐标区域(使用弧度表示),包括越过国际日期线和极点的情况* @private** @param {Rectangle} [result] An object in which to store the result.* @returns {Rectangle} The result rectangle with spherical extents.*/
ClippingPolygon.prototype.computeSphericalExtents = function (result) {if (!defined(result)) {result = new Rectangle();}// 经纬度范围const rectangle = this.computeRectangle(scratchRectangle);// 计算出球面点笛卡尔let spherePoint = Cartographic.toCartesian(Rectangle.southwest(rectangle),this.ellipsoid,spherePointScratch);// Project into plane with vertical for latitude// 投影到具有垂直纬度的平面中let magXY = Math.sqrt(spherePoint.x * spherePoint.x + spherePoint.y * spherePoint.y);// Use fastApproximateAtan2 for alignment with shader// 球面纬度let sphereLatitude = CesiumMath.fastApproximateAtan2(magXY, spherePoint.z);// 球面经度let sphereLongitude = CesiumMath.fastApproximateAtan2(spherePoint.x,spherePoint.y);// 西南的经纬度result.south = sphereLatitude;result.west = sphereLongitude;// 计算东北点位spherePoint = Cartographic.toCartesian(Rectangle.northeast(rectangle),this.ellipsoid,spherePointScratch);// Project into plane with vertical for latitudemagXY = Math.sqrt(spherePoint.x * spherePoint.x + spherePoint.y * spherePoint.y);// Use fastApproximateAtan2 for alignment with shadersphereLatitude = CesiumMath.fastApproximateAtan2(magXY, spherePoint.z);sphereLongitude = CesiumMath.fastApproximateAtan2(spherePoint.x,spherePoint.y);// 计算东北经纬度result.north = sphereLatitude;result.east = sphereLongitude;return result;
};
ClippingPolygonCollection类的过程主要在update函数中,函数过程如下
ClippingPolygonCollection.prototype.update = function (frameState) {const context = frameState.context;// 是否支持if (!ClippingPolygonCollection.isSupported(frameState)) {throw new RuntimeError("ClippingPolygonCollections are only supported for WebGL 2.");}// It'd be expensive to validate any individual position has changed. Instead verify if the list of polygon positions has had elements added or removed, which should be good enough for most cases.// 验证任何个人立场的改变都是昂贵的。相反,请验证多边形位置列表中是否添加或删除了元素,这在大多数情况下应该足够好。// 总共的顶点数量const totalPositions = this._polygons.reduce((totalPositions, polygon) => totalPositions + polygon.length,0);// 总共的顶点数量不变if (totalPositions === this.totalPositions) {return;}this._totalPositions = totalPositions;// If there are no clipping polygons, there's nothing to update.if (this.length === 0) {return;}// 符号距离计算命令,命令存在就取消if (defined(this._signedDistanceComputeCommand)) {// 如果正在计算就取消this._signedDistanceComputeCommand.canceled = true;this._signedDistanceComputeCommand = undefined;}// 多边形纹理let polygonsTexture = this._polygonsTexture;// 范围纹理let extentsTexture = this._extentsTexture;// 符号距离纹理let signedDistanceTexture = this._signedDistanceTexture;if (defined(polygonsTexture)) {// 当前像素数量const currentPixelCount = polygonsTexture.width * polygonsTexture.height;// Recreate the texture to double current requirement if it isn't big enough or is 4 times larger than it needs to be.// Optimization note: this isn't exactly the classic resizeable array algorithm// * not necessarily checking for resize after each add/remove operation// * random-access deletes instead of just pops// * alloc ops likely more expensive than demonstrable via big-O analysis/*重建2倍的当前纹理,如果不够到,或者是所需内存的4倍,优化注意:这不是经典的重新设置数组大小的算法,不一定要在每次添加/删除操作后检查是否调整大小随机访问删除而不是弹出分配操作可能比通过big-O(大O分析法)分析证明的更昂贵*/if (currentPixelCount < this.pixelsNeededForPolygonPositions || // 内存不够大this.pixelsNeededForPolygonPositions < 0.25 * currentPixelCount // 所需要的比当前四分之一还小,就需要重新分配显存) {// 销毁纹理polygonsTexture.destroy();polygonsTexture = undefined;this._polygonsTexture = undefined;}}if (!defined(polygonsTexture)) {// 获取分辨率const requiredResolution = ClippingPolygonCollection.getTextureResolution(polygonsTexture,this.pixelsNeededForPolygonPositions,textureResolutionScratch);// 创建纹理polygonsTexture = new Texture({context: context,width: requiredResolution.x,height: requiredResolution.y,pixelFormat: PixelFormat.RG,pixelDatatype: PixelDatatype.FLOAT,sampler: Sampler.NEAREST,flipY: false,});// 数据this._float32View = new Float32Array(requiredResolution.x * requiredResolution.y * 2);// 纹理this._polygonsTexture = polygonsTexture;}// 处理范围纹理if (defined(extentsTexture)) {const currentPixelCount = extentsTexture.width * extentsTexture.height;// Recreate the texture to double current requirement if it isn't big enough or is 4 times larger than it needs to be.// Optimization note: this isn't exactly the classic resizeable array algorithm// * not necessarily checking for resize after each add/remove operation// * random-access deletes instead of just pops// * alloc ops likely more expensive than demonstrable via big-O analysisif (currentPixelCount < this.pixelsNeededForExtents ||this.pixelsNeededForExtents < 0.25 * currentPixelCount) {extentsTexture.destroy();extentsTexture = undefined;this._extentsTexture = undefined;}}if (!defined(extentsTexture)) {// 获取范围纹理的分辨率const requiredResolution = ClippingPolygonCollection.getTextureResolution(extentsTexture,this.pixelsNeededForExtents,textureResolutionScratch);// 创建范围纹理extentsTexture = new Texture({context: context,width: requiredResolution.x,height: requiredResolution.y,pixelFormat: PixelFormat.RGBA,pixelDatatype: PixelDatatype.FLOAT,sampler: Sampler.NEAREST,flipY: false,});// 范围纹理依赖的数据内存this._extentsFloat32View = new Float32Array(requiredResolution.x * requiredResolution.y * 4);this._extentsTexture = extentsTexture;}// 打包多边形packPolygonsAsFloats(this);// 拷贝范围的纹理数据extentsTexture.copyFrom({source: {width: extentsTexture.width,height: extentsTexture.height,arrayBufferView: this._extentsFloat32View,},});// 拷贝多边形纹理数据polygonsTexture.copyFrom({source: {width: polygonsTexture.width,height: polygonsTexture.height,arrayBufferView: this._float32View,},});// 定义符号距离场景if (!defined(signedDistanceTexture)) {// 符号距离场纹理分辨率const textureDimensions = ClippingPolygonCollection.getClippingDistanceTextureResolution(this,textureResolutionScratch);// 符号距离纹理signedDistanceTexture = new Texture({context: context,width: textureDimensions.x,height: textureDimensions.y,pixelFormat: context.webgl2 ? PixelFormat.RED : PixelFormat.LUMINANCE, // 只有一个通道pixelDatatype: PixelDatatype.FLOAT,sampler: new Sampler({wrapS: TextureWrap.CLAMP_TO_EDGE,wrapT: TextureWrap.CLAMP_TO_EDGE,minificationFilter: TextureMinificationFilter.LINEAR,magnificationFilter: TextureMagnificationFilter.LINEAR,}),flipY: false,});this._signedDistanceTexture = signedDistanceTexture;}// 创建符号距离场命令this._signedDistanceComputeCommand = createSignedDistanceTextureCommand(this);
};
这个过程中主要是如很将多边形信息打包到两个纹理中,以及创建一张距离场纹理,用于后续将计算着色器(像素着色器模拟计算着色器)的计算结果存入距离场纹理中。
打包的两张纹理的结构图如下:
1.3 计算命令
接着就是计算命令的创建过程:
// 创建距离场纹理命令
function createSignedDistanceTextureCommand(collection) {// 多边形纹理、范围纹理const polygonTexture = collection._polygonsTexture;const extentsTexture = collection._extentsTexture;// 计算命令return new ComputeCommand({fragmentShaderSource: PolygonSignedDistanceFS, // 只有光栅化过程outputTexture: collection._signedDistanceTexture, // 输出纹理uniformMap: {u_polygonsLength: function () { // 多少个多边形return collection.length;},u_extentsLength: function () { // 多少个范围return collection.extentsCount;},u_extentsTexture: function () { // 范围纹理return extentsTexture;},u_polygonTexture: function () { // 多边形纹理return polygonTexture;},},persists: false, // 持续使用这个命令,还是使用一次就释放owner: collection, // 归属postExecute: () => { // 执行完成后collection._signedDistanceComputeCommand = undefined;},});
}
这个过程涉及到了ComputeCommand和ComputeEngine类,ComputeCommand类主要是收集信息,ComputeEngine类主要是update函数
// 执行
ComputeEngine.prototype.execute = function (computeCommand) {//>>includeStart('debug', pragmas.debug);Check.defined("computeCommand", computeCommand);//>>includeEnd('debug');// This may modify the command's resources, so do error checking afterwards// 可能会更改命令的分辨率,后续会做错误检查if (defined(computeCommand.preExecute)) {computeCommand.preExecute(computeCommand);}//>>includeStart('debug', pragmas.debug);if (!defined(computeCommand.fragmentShaderSource) &&!defined(computeCommand.shaderProgram)) {throw new DeveloperError("computeCommand.fragmentShaderSource or computeCommand.shaderProgram is required.");}Check.defined("computeCommand.outputTexture", computeCommand.outputTexture);//>>includeEnd('debug');// 输出的纹理const outputTexture = computeCommand.outputTexture;const width = outputTexture.width;const height = outputTexture.height;const context = this._context;// 定义顶点数组const vertexArray = defined(computeCommand.vertexArray)? computeCommand.vertexArray: context.getViewportQuadVertexArray(); // 获取视口四边形顶点// 着色程序const shaderProgram = defined(computeCommand.shaderProgram)? computeCommand.shaderProgram: createViewportQuadShader(context, computeCommand.fragmentShaderSource); // 创建视口着色器// 创建帧缓冲const framebuffer = createFramebuffer(context, outputTexture);// 创建渲染状态const renderState = createRenderState(width, height);const uniformMap = computeCommand.uniformMap;// 执行清空命令const clearCommand = clearCommandScratch;clearCommand.framebuffer = framebuffer;clearCommand.renderState = renderState;clearCommand.execute(context);// 执行绘制命令const drawCommand = drawCommandScratch;drawCommand.vertexArray = vertexArray;drawCommand.renderState = renderState;drawCommand.shaderProgram = shaderProgram;drawCommand.uniformMap = uniformMap;drawCommand.framebuffer = framebuffer;drawCommand.execute(context);// 执行完成销毁framebuffer.destroy();// 非持久的计算命令(一次性的)if (!computeCommand.persists) {shaderProgram.destroy();if (defined(computeCommand.vertexArray)) {vertexArray.destroy();}}// 处理完成后的回调if (defined(computeCommand.postExecute)) {computeCommand.postExecute(outputTexture);}
};
类似一个后处理过程,创建一个四边形,占满整个屏幕,然后使用像素着色器进行距离场插值计算。
1.4 生成距离场纹理
这个过程是在着色器中处理的,PolygonSignedDistanceFS.glsl文件中是计算过程
in vec2 v_textureCoordinates;uniform int u_polygonsLength;
uniform int u_extentsLength;
uniform highp sampler2D u_polygonTexture;
uniform highp sampler2D u_extentsTexture;// 获取多边形索引
int getPolygonIndex(float dimension, vec2 coord) {// 将当前的纹理坐标(按照0~1的比例)转换到(范围纹理的)整数坐标vec2 uv = coord.xy * dimension;return int(floor(uv.y) * dimension + floor(uv.x));
}// 获取范围
vec2 getLookupUv(ivec2 dimensions, int i) {//int pixY = i / dimensions.x;int pixX = i - (pixY * dimensions.x);// 获取宽度、高度步长float pixelWidth = 1.0 / float(dimensions.x);float pixelHeight = 1.0 / float(dimensions.y);// 计算uvfloat u = (float(pixX) + 0.5) * pixelWidth; // sample from center of pixelfloat v = (float(pixY) + 0.5) * pixelHeight;return vec2(u, v);
}// 获取范围
vec4 getExtents(int i) {return texture(u_extentsTexture, getLookupUv(textureSize(u_extentsTexture, 0), i));
}//
ivec2 getPositionsLengthAndExtentsIndex(int i) {//vec2 uv = getLookupUv(textureSize(u_polygonTexture, 0), i);vec4 value = texture(u_polygonTexture, uv);return ivec2(int(value.x), int(value.y));
}vec2 getPolygonPosition(int i) {vec2 uv = getLookupUv(textureSize(u_polygonTexture, 0), i);return texture(u_polygonTexture, uv).xy;
}vec2 getCoordinates(vec2 textureCoordinates, vec4 extents) {// 插值出中间坐标 extents.x:范围开始的地方,extents.x + 1.0 / extents.z:范围结束的地方float latitude = mix(extents.x, extents.x + 1.0 / extents.z, textureCoordinates.y);float longitude = mix(extents.y, extents.y + 1.0 / extents.w, textureCoordinates.x);return vec2(latitude, longitude);
}/*
具体的逻辑好像是:
如果是4个范围,则将整个4096*4096的图像分成四部分,每一个部分进行距离场计算,如果是8个范围,则就缩小每个距离范围的分辨率,
*/
void main() {int lastPolygonIndex = 0;out_FragColor = vec4(1.0);// Get the relevant region of the texture 获取纹理的相关区域// 范围个数,例如100个float dimension = float(u_extentsLength);// 多于2个范围if (u_extentsLength > 2) {//转化成一个正方形的范围dimension = ceil(log2(float(u_extentsLength)));}// 坐标转换成索引(这个像素gl_FragCoord)对应的范围索引int regionIndex = getPolygonIndex(dimension, v_textureCoordinates);// 遍历多边形for (int polygonIndex = 0; polygonIndex < u_polygonsLength; polygonIndex++) {// 获取每一个多边形的顶点个数和这个多边形的范围索引ivec2 positionsLengthAndExtents = getPositionsLengthAndExtentsIndex(lastPolygonIndex);// 长度int positionsLength = positionsLengthAndExtents.x;// 索引int polygonExtentsIndex = positionsLengthAndExtents.y;lastPolygonIndex += 1;// Only compute signed distance for the relevant part of the atlas// 仅计算图集相关部分的有符号距离// 找到对应的区域if (polygonExtentsIndex == regionIndex) {float clipAmount = czm_infinity;// 这个多边形对应的范围vec4 extents = getExtents(polygonExtentsIndex);// 偏移,将范围左边转换到一个正方形的范围内vec2 textureOffset = vec2(mod(float(polygonExtentsIndex), dimension), floor(float(polygonExtentsIndex) / dimension)) / dimension;// 插值出的坐标vec2 p = getCoordinates((v_textureCoordinates - textureOffset) * dimension, extents);float s = 1.0;// Check each edge for absolute distance 绝对距离检查每个边// 这个多边形的遍历坐标for (int i = 0, j = positionsLength - 1; i < positionsLength; j = i, i++) {// 获取多边形的坐标a,和上一个坐标bvec2 a = getPolygonPosition(lastPolygonIndex + i);vec2 b = getPolygonPosition(lastPolygonIndex + j);// 两个点(经纬度点)之间的差vec2 ab = b - a;//vec2 pa = p - a;// 直线pa在直线ab上的投影(在单位直线ab)// pa在ab单位向量上的投影,然后在除以ab,即占pa总长度的百分比float t = dot(pa, ab) / dot(ab, ab);// 百分比限制在【0.0~1.0】之间t = clamp(t, 0.0, 1.0);// 计算垂线vec2 pq = pa - t * ab;// 计算垂线距离float d = length(pq);// Inside / outside computation to determine sign// 内外计算决定符号bvec3 cond = bvec3(p.y >= a.y,p.y < b.y,ab.x * pa.y > ab.y * pa.x);if (all(cond) || all(not(cond))) s = -s;// 找到距离最小的一个if (abs(d) < abs(clipAmount)) {// 裁切数量(有向距离场的垂线)clipAmount = d;}}// Normalize the range to [0,1] 归一化范围到【0-1】// clipAmount * length(extents.zw)转换到【-1~1】,然后添加s符号,然后/2为转换到【-0.5~0.5】,然后+0.5为转换到【0~1】vec4 result = (s * vec4(clipAmount * length(extents.zw))) / 2.0 + 0.5;// In the case where we've iterated through multiple polygons, take the minimum// 在我们迭代多个多边形的情况下,取最小值out_FragColor = min(out_FragColor, result);}lastPolygonIndex += positionsLength;}
}
上述过程有点绕,主要过程如下:
- 根据光栅化插值的特点将每个像素转换到对应的范围纹理坐标中,这个坐标是一个索引;
- 根据上述索引,遍历多边形纹理中的数据,看看那个多边形的范围索引与1中计算出来的索引对应;
- 遍历这个索引下的多边形中的相邻的两个顶点坐标并计算向量,然后再计算像素坐标对应的边界插值坐标,将插值坐标投影到计算向量上,然后计算垂向量,垂向量的长度就是距离场;
- 当前像素点对应的坐标距离最短的那个边界的长度,然后计算符号,最后存入纹理中。
上面glsl的过程中,原来的范围长度是一维数组,经过如下计算会转换为边长为dimension的正方形,对应于上图四个图中的一个。
// Get the relevant region of the texture 获取纹理的相关区域// 范围个数,例如100个float dimension = float(u_extentsLength);// 多于2个范围if (u_extentsLength > 2) {//转化成一个正方形的范围dimension = ceil(log2(float(u_extentsLength)));}
由于uv坐标是【0~1】范围内的,所以需要将uv坐标 转换成externsTexture纹理的像素坐标,计算这是第几个范围,引文一个纹素对应者一个范围,第几个纹素就是第几个范围。
// 获取多边形索引
int getPolygonIndex(float dimension, vec2 coord) {// 将当前的纹理坐标(按照0~1的比例)转换到(范围纹理的)整数坐标vec2 uv = coord.xy * dimension;return int(floor(uv.y) * dimension + floor(uv.x));
}
例如:dimension是2x2的4个像素,而coord是【0~1】的范围,假设是coord=(0.6, 0.6)则计算出来就是coord*2 =(1.2, 1.2)取整数就是(1,1),就是这个像素,所以(0,0)到(0.5,0.0)范围对应第一行第一列的像素,所以(0.5,0)到(1.0,0.0)范围对应第一行第二列的像素,(0.0,0.5)到(0.0,1.0)范围对应第二行第一列的像素,所以(0.5,0.5)到(1.0,1.0)范围对应第二行第二列的像素。
将整个4096x4096的距离场纹理划分成4个部分,每个部分就是一个polygon,然后按照如下
// 这个多边形对应的范围vec4 extents = getExtents(polygonExtentsIndex);// 偏移,将范围左边转换到一个正方形的范围内vec2 textureOffset = vec2(mod(float(polygonExtentsIndex), dimension), floor(float(polygonExtentsIndex) / dimension)) / dimension;// 插值出的坐标vec2 p = getCoordinates((v_textureCoordinates - textureOffset) * dimension, extents);
代码进行计算,索引找到就能查出范围externs,textureOffset为映射出的uv坐标,p就是映射出的uv坐标对应的经纬度坐标。