cesium ClippingPolygon多边形裁切

1.多边形裁切

1.1 基本流程

        cesium117版本添加了多边形裁切功能,本文分析源码,看看是如何处理的。多边形裁切的大概流程分为4部分:

  1. 通过经纬度坐标传入多个闭合的边界;
  2. 将多个边界打包成两张纹理,一张是每个多边形的坐标,另一张是每个多边形的边界;
  3. 将两张多边形纹理通过一个计算着色器(屏幕空间着色器模拟计算着色器)生成一张符号距离场纹理;
  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 latitude
  magXY = Math.sqrt(
    spherePoint.x * spherePoint.x + spherePoint.y * spherePoint.y
  );

  // Use fastApproximateAtan2 for alignment with shader
  sphereLatitude = 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 analysis
    if (
      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);
    // 计算uv
    float u = (float(pixX) + 0.5) * pixelWidth; // sample from center of pixel
    float 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,和上一个坐标b
                vec2 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.  根据光栅化插值的特点将每个像素转换到对应的范围纹理坐标中,这个坐标是一个索引;
  2. 根据上述索引,遍历多边形纹理中的数据,看看那个多边形的范围索引与1中计算出来的索引对应;
  3. 遍历这个索引下的多边形中的相邻的两个顶点坐标并计算向量,然后再计算像素坐标对应的边界插值坐标,将插值坐标投影到计算向量上,然后计算垂向量,垂向量的长度就是距离场;
  4. 当前像素点对应的坐标距离最短的那个边界的长度,然后计算符号,最后存入纹理中。

        上面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坐标对应的经纬度坐标。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值