用4种方法实现内发光的多边形区域

介绍

在前端可视化开发中,实现多边形区域的内发光效果是常见的需求。本文将介绍四种不同的实现方法,从最基础的贴图方案到高级的着色器方案,每种方案都有其特点和适用场景。

通过顶部贴图、边缘贴图、Canvas贴图和着色器编程这四种方式,我们可以根据实际项目需求选择最合适的实现方案。本文将详细介绍这四种方案的具体实现步骤,包括所需工具、代码示例以及注意事项,帮助开发者根据项目需求选择最适合的实现方案。

实现思路

在前端可视化开发中,多边形区域的内发光效果实现一直是一个有趣且具有挑战性的课题。经过深入研究和实践,我总结出了四种不同的实现方案,从简单的贴图方案到复杂的着色器编程。每种方案都各具特色,适用于不同的应用场景和技术需求。这些方案的实现难度逐步递增,但同时能够满足不同层次的开发需求。在选择具体方案时,需要权衡项目的性能要求、开发周期、维护成本等多个因素。接下来,让我们深入分析每种方案的具体实现方法、优势特点以及潜在的局限性。

方案 操作方法 优点 缺点
1.顶部贴图 制作专属贴图贴在polygon上 贴图效果可高度定制 贴图无法复用;贴图无法以动态更新;放大模型时容易看到图片模糊
2.边缘贴图 在polygon边缘生成Meshline,对Meshline进行重复贴图 操作方便,可以快速更换各种边缘线效果 对polygon形状有要求,比边线太宽或者拐点太尖锐会出现视觉效果异常
3.canvas贴图 方案1的改进版,通过polygon顶点路径数据动态生成Canvas画布内容,并贴在polygon上 视觉效果好;动态更新方便 每一个polygon都需要先占用CPU资源绘制canvas,数量多的话可能有性能风险
4.编写着色器 计算polygon上每个点的有向距离场sdf,根据每个点sdf的值填色 动态更新方便;GPU渲染性能优化空间大 上手门槛较高,需要对计算机图形学有一定的了解

准备工作

技术栈

工具 版本 说明
AMap JSAPI 2.0 高德地图JsAPI实现GIS部分工作,使用map.customCoords的lngLatsToCoords实现坐标转换工作
three.js 0.157 基础3d引擎,封装好的webGL,实现Mesh贴图靠它了
turf.js 2.0 GIS数据分析处理函数库
Cursor 代码编辑器 代码提示功能逆天,可以大大减轻编码和理解代码的工作量,花点小钱让AI给我打工就是爽

创建几何体

实现一个完整的多边形Mesh需要几何体geometry和材质Material,我们先搞定geometry部分。

  1. geometry的实现也可以有两种方式:

    (1)计算geometry的矩形范围range,创建一个简单的矩形平面,后续把png格式的贴图贴上去就可以了,polygon范围外保持透明,这样从视觉看上去就是个多边形

    (2)绘制真正的geometry,后续把贴图对齐,这样实现的Mesh具有真正的polygon边缘,如何还有其他物体存在的话,比较容易处理它与其他物体的关系,光标拾取检测也比较准确。

    这里我们采用第二种方式,因为我们最后需要放置多个polyon在地图上。以下为具体代码:

    jsx 复制代码
    /**
     * 绘制多边形
     * @private
     * @param {Array} path 路径
     * @param {Object} properties 属性
     */
    drawPolygon({ path, properties }) {
      const { altitude, opacity } = this._conf
    
      // 获取多边形的范围
      const range = this.getExtRange(path)
    
      // 创建几何体
      const geometry = this.generateOneGeometry(range, path)
    
      // 创建材质,不同的方案实现方法不同
      const material = ...
    
      const polygon = new THREE.Mesh(geometry, material)
      // 将多边形网格对象添加到场景中
      const _scene = this.group || this.scene
      _scene.add(polygon)
    
      this._meshs.push(polygon)
    }
    
    /**
     * 初始化Polygon边界范围
     * @private
     */
    getExtRange(positions) {
      let minX = positions[0][0]
      let minY = positions[0][1]
      let maxX = positions[0][0]
      let maxY = positions[0][1]
    
      for (let i = 0; i < positions.length; i += 1) {
        const [x, y] = positions[i]
        if (x < minX) {
          minX = x
        } else if (x > maxX) {
          maxX = x
        }
        if (y < minY) {
          minY = y
        } else if (y > maxY) {
          maxY = y
        }
      }
      return { minX, minY, maxX, maxY }
    }
    
    /**
     * 创建多边形几何体
     * @private
     * @param {Object} range 范围
     * @param {Array} path 路径
     * @return {THREE.BufferGeometry}
     */
    generateOneGeometry(range, path) {
      // 创建多边形
      const shape = new THREE.Shape()
      path.forEach(([x, y], index) => {
        if (index === 0) {
          shape.moveTo(x, y)
        } else {
          shape.lineTo(x, y)
        }
      })
      // 创建几何体
      let geometry = new THREE.ShapeGeometry(shape)
      // 创建 UV 属性并将其设置到几何体
      const uvArray = this.getGeometryUV(geometry, range)
      const uvAttribute = new THREE.BufferAttribute(uvArray, 2)
      geometry.setAttribute('uv', uvAttribute)
    
      return geometry
    }
  2. 这里需要注意的点是如何调整好polygon顶部面的UV,相当于告诉机器要如何把纹理图纸铺到polygon面上,如图所示我们使用平铺的方式,因此需要将UV坐标归一化。

    jsx 复制代码
    /**
     * 获取几何体归一化之后的UV坐标
     * @param geometry
     * @return {Float32Array}
     */
    getGeometryUV(geometry, range) {
      // 获取所有顶点数量
      const vertexCount = geometry.attributes.position.count
      // 创建 UV 坐标数组
      const uvArray = new Float32Array(vertexCount * 2)
      const { minX, minY, maxX, maxY } = range
      // 设置 UV 坐标
      for (let i = 0; i < vertexCount; i++) {
        const vertexIndex = i * 2
        // UV坐标归一化
        const u = (geometry.attributes.position.getX(i) - minX) / (maxX - minX)
        const v = (geometry.attributes.position.getY(i) - minY) / (maxY - minY)
        uvArray[vertexIndex] = u
        uvArray[vertexIndex + 1] = v
      }
      return uvArray
    }
  3. 创建好几何体后,我们创建一个测试的纹理覆盖上去看看是否贴图正常,就可以进行下一步工作了。

实现步骤

方案1.顶部贴图

  1. 获取多边形的顶点坐标geoJSON文件,将该文件转换为svg格式

  2. 把图片交给设计师进行二次创作了,除了绘制边缘和内发光,也可以定制一些效果,经过美术上的加工处理后转为png格式的图片

  3. 处理好的贴图直接贴到geometry上面,对齐贴图,设置好平铺、重复等参数即可

    jsx 复制代码
    /**
     * 生成纹理
     * @param {string} url - 纹理图片的URL
     * @returns {Texture} - 生成的纹理对象
     */
    async function generateTexture(name = 'glow-line1.png') {
        const texture = await new THREE.TextureLoader().load(`./demo/images/${name}`)
        texture.wrapS = THREE.RepeatWrapping
        texture.wrapT = THREE.RepeatWrapping
        return texture
    }
    
    ...
    // 创建纹理实例
    this._conf.style.polygonMap = generateTexture('textureName')
    
    /**
     * 绘制单个多边形Mesh
     * @private
     * @param {Array} path 路径
     * @param {Object} properties 属性
     */
    drawPolygon({ path, properties }) {
      const { altitude, opacity } = this._conf
      ...
      // 创建材质
      const material = new THREE.MeshBasicMaterial({
        color: properties.color || '#0674F1',
        transparent: true,
        opacity: properties.opacity || opacity
      })
      // 设置多边形纹理
      const polygonMap = properties.polygonMap || this._conf.style.polygonMap
      if (polygonMap) {
        material.map = polygonMap
      }
      ...
    }
  4. 贴图完成后如果发现纹理没对,可以调整偏移值强行对齐。

    jsx 复制代码
    // 调整参数UI
    function initGUI(material) {
      gui = new dat.GUI()
      // [AIGC] 切换材质map的偏移
      gui.add(guiControl, 'offsetX', -1, 1).step(0.01).name('offsetX')
      .onChange(async (value) => {
          await setLayerStyle('offsetX', value)
      });
      // [AIGC] 切换材质map的偏移
      gui.add(guiControl, 'offsetY', -1, 1).step(0.01).name('offsetY')
      .onChange(async (value) => {
          await setLayerStyle('offsetY', value)
      });
    }
    // 设置图层样式
    async function setLayerStyle(key, value) {
      const layer = layerManger.findLayerById('polygonGlow1');
      switch (key) {
          case 'offsetX':
            // [AIGC] 设置材质map的偏移
            layer._meshs.forEach(mesh => {
                mesh.material.map.offset.x = value
                mesh.material.needsUpdate = true
            })
            break;
          case 'offsetY':
            // [AIGC] 设置材质map的偏移
            layer._meshs.forEach(mesh => {
                mesh.material.map.offset.y = value
                mesh.material.needsUpdate = true
            })
            break;
          default:
              break;
      }
    }
  5. 这个方案的绘制范围是polgyone从边缘到内部,因此我们也可以在范围内定制各种外观。但缺陷非常明显,就是纹理在放大后容易出现锯齿感。

方案2.边缘贴图

  1. 该方案其实受到了arcGIS实现内发光方案的启发。我们获取多边形顶点坐标,使用Meshline组件生成边缘线面geometry,通过在geometry贴发光材质实现内发光。

  2. 这里需要注意线面的宽度在整个图形的占比,太宽的话会出现"边缘毛刺"的情况。

  3. 为了保证后续的贴图更均匀且減少毛刺的情况,我们可以优化一下顶点的分布,让顶点之间的距离尽量是差不多的。我们可以用turf.js 实现顶点的均匀化,代码逻辑如下:

    (1)设定顶点之间的距离spacing,获取整个polygon边缘的总长度length;

    (2)通过spacing和length生成均匀分布的点,获取点坐标;

    (3)更新polygon的顶点坐标。

    jsx 复制代码
    /**
     * [AIGC]
     * 生成均匀分布的点
     * @param {Array} coordinates - 多边形坐标数组
     * @param {number} spacing - 点之间的间距(单位:公里),默认0.5公里
     * @returns {Array} - 均匀分布的点坐标数组 [[lng, lat], ...]
     */
    function generateUniformPoints(coordinates, spacing = 0.5) {
        // 创建一个LineString几何体
        const line = turf.lineString(coordinates);
    
        // 计算线的总长度(单位:公里)
        const length = turf.length(line);
    
        // 计算需要生成的点数
        const pointCount = Math.ceil(length / spacing);
    
        // 生成均匀分布的点
        const points = [];
        for (let i = 0; i <= pointCount; i++) {
            const distance = (i / pointCount) * length;
            // 获取分段点坐标
            const point = turf.along(line, distance, { units: 'kilometers' });
            points.push(point.geometry.coordinates);
        }
        console.log(`生成均匀分布的点: ${pointCount}/${coordinates.length}`)
        return points;
    }
  4. 创建贴图实例,并设置好贴图,通过调节lineWidth就可以改变内发光的范围。

    jsx 复制代码
    // 创建贴图实例
    async function generateTexture(name = 'glow-line1.png') {
        const lineMap = await new THREE.TextureLoader().load(`./demo/images/${name}`)
        lineMap.wrapS = THREE.RepeatWrapping
        lineMap.wrapT = THREE.RepeatWrapping
        return lineMap
    }
    //...
    const lineMap = generateTexture('textureName')
    // 初始化贴图
    initLineMaterial() {
      const { sizeAttenuation, lineWidth, lineColor, lineMap } = this._conf
      let option = {
          useMap: true,
          map: lineMap,
          transparent: true,
          depthTest: false,
          alphaTest: 0.0,
          sizeAttenuation: 1,
          lineWidth,
          dashOffset: 0
        }
      this._lineMaterial = new MeshLineMaterial(option)
      return this._lineMaterial
    }
    // 给Polygon边缘线面贴图
    drawLines({ path, properties }) {
      //...
      const line = new MeshLineGeometry()
      line.setPoints(points)
      const mesh = new THREE.Mesh(line, this._lineMaterial)
      //...
    }
  5. 使用meshline实现边缘内发光的一个好处,就是我们可以利用它自带的动态调整纹理offset的功能,使用不同的纹理和偏移方式创造不同的效果。

    jsx 复制代码
    const guiControl = {
        texture: 'glow-line5.png', //纹理
        lineWidth: 500, //范围
        dashOffset: 0,
        offsetDirect: 'y', //偏移方向
        offsetSpeed: 1 //偏移速度
    }
    
    // 逐帧函数中更新纹理偏移值
    layer2.on('update', (layer) => {
        const {_lineMaterial} = layer
        const {offsetDirect, offsetSpeed} = guiControl
        // 调整偏移量
        _lineMaterial.uniforms.offset.value[offsetDirect] -= 0.01 * offsetSpeed
        _lineMaterial.uniforms.dashOffset.value -= 0.01 * offsetSpeed
    })

方案3.Canvas贴图

  1. Canvas方案可以算是方案1.顶部贴图的升级版,为了规避方案1贴图复用性差,无法动态更新的问题,我们直接使用Canvas绘制贴图,我们依旧需要polygon的顶点坐标coordinates和矩形范围ExtRange两个数据。

    jsx 复制代码
    /**
     * 获取边界范围盒子
     * @param {Array} positions 边界坐标
     * @private
     */
    getExtRange(positions) {
        let minX = positions[0][0]
        let minY = positions[0][1]
        let maxX = positions[0][0]
        let maxY = positions[0][1]
        for (let i = 0; i < positions.length; i += 1) {
            const [x, y] = positions[i]
            if (x < minX) {
                minX = x
            } else if (x > maxX) {
                maxX = x
            }
            if (y < minY) {
                minY = y
            } else if (y > maxY) {
                maxY = y
            }
        }
        return { minX, minY, maxX, maxY }
    }
  2. 绘制贴图纹理CanvasTexture,canvas的api里其实没有内发光的方法,我们直接沿着边缘绘制一条外阴影的线,并绘制多重阴影

    a. 通过rang设置canvas画布大小,为了避免画布过大影响性能,还加了_CANVAS_MAX_LEN 限制画布的最大宽高 ```jsx generateCanvasTexture(range, path) { const polygonVertices = path.map(([x, y]) => { return new THREE.Vector3(x, y, 0); });

    arduino 复制代码
        const { minX, minY, maxX, maxY } = range;
    
        // 限制画布的最大宽和高
        const scale = this._CANVAS_MAX_LEN / this._maxDimension;
        console.log('scale', scale)
    
        // 缩放多边形顶点的坐标
        const scaledVertices = polygonVertices.map(vertex => {
            const x = (vertex.x - minX) * scale;
            const y = (vertex.y - minY) * scale;
            return new THREE.Vector3(x, y, vertex.z);
        });
    
        // 计算调整后的画布大小
        const width = Math.ceil((maxX - minX) * scale);
        const height = Math.ceil((maxY - minY) * scale);
        console.log(width,height)
    ```

    b. 根据path开始绘制多边形边线,并给边线增加多种外阴影效果,为什么要多重绘制外阴影呢?因为glowBlur控制阴影的范围越大时,阴影会变得越"稀薄",为了同时提升阴影的范围和"浓度",我们需要多重绘制。

    ini 复制代码
    ```jsx
    // 开始绘制多边形
    context.beginPath();
    context.moveTo(scaledVertices[0].x, scaledVertices[0].y);
    for (let i = 1; i < scaledVertices.length; i++) {
        context.lineTo(scaledVertices[i].x, scaledVertices[i].y);
    }
    context.closePath();
    
    // 设置线条样式
    context.lineWidth = lineWidth * 2; // 设置线条宽度
    context.strokeStyle = lineColor; // 设置线条颜色
    
    // 多重阴影的数量
    const numShadows = 5;
    // 内发光强度
    const baseIntensity = glowIntensity * 100.0;
    // 内发光颜色
    const [r,g,b] = hexToArray(glowColor)
    
    for (let i = 1; i <= numShadows; i++) {
        context.shadowColor = `rgba(${r}, ${g}, ${b}, ${baseIntensity * (numShadows - i + 1) / numShadows})`; // 内发光颜色和透明度
        context.shadowBlur = glowBlur * i; // 增加模糊程度
        context.shadowOffsetX = 0; // 内发光水平偏移
        context.shadowOffsetY = 0; // 内发光垂直偏移
        context.stroke(); // 重新填充以应用内发光效果
    }
    ```
    1. 生成CanvasTexture作为材质纹理贴图,当然每个形状的polygon需要单独渲染一块贴图
    arduino 复制代码
    ```jsx
    const texture = this.generateCanvasTexture(range, path);
        texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
    
        const shaderMaterial = new THREE.MeshBasicMaterial({
            map: texture,
            transparent: true,
            side: THREE.DoubleSide,
            opacity: 0.9
        });
    //...
    // 创建几何体
    const geometry = this.generateOneGeometry(range, path)
    // 创建材质
    const material = this.getMaterial(range, path)
    // 创建多边形的网格对象
    const polygon = new THREE.Mesh(geometry, material)
    ```
    1. 最终可以得到相对理想的PolygonMesh

    该方案的完整代码如下:

    jsx 复制代码
    /**
     * 通过边缘坐标数据获取灰度图纹理
     * @private
     */
    generateCanvasTexture(range, path) {
        const polygonVertices = path.map(([x, y]) => {
            return new THREE.Vector3(x, y, 0);
        });
    
        const { minX, minY, maxX, maxY } = range;
    
        // 限制画布的最大宽和高
        const scale = this._CANVAS_MAX_LEN / this._maxDimension;
        console.log('scale', scale)
    
        // 缩放多边形顶点的坐标
        const scaledVertices = polygonVertices.map(vertex => {
            const x = (vertex.x - minX) * scale;
            const y = (vertex.y - minY) * scale;
            return new THREE.Vector3(x, y, vertex.z);
        });
    
        // 计算调整后的画布大小
        const width = Math.ceil((maxX - minX) * scale);
        const height = Math.ceil((maxY - minY) * scale);
        console.log(width,height)
        // 读取外观配置
        const {lineWidth, lineColor, glowColor, glowIntensity, glowBlur} = this._conf.style
    
        const canvas = this.generateCanvas({ width, height });
        const context = canvas.getContext('2d');
    
        // 绘制多边形背景为黑色
        context.fillStyle = 'transparent';
        context.fillRect(0, 0, width, height);
    
        // 开始绘制多边形
        context.beginPath();
        context.moveTo(scaledVertices[0].x, scaledVertices[0].y);
        for (let i = 1; i < scaledVertices.length; i++) {
            context.lineTo(scaledVertices[i].x, scaledVertices[i].y);
        }
        context.closePath();
    
        // 设置线条样式
        context.lineWidth = lineWidth * 2; // 设置线条宽度
        context.strokeStyle = lineColor; // 设置线条颜色
    
        // 多重阴影的数量
        const numShadows = 5;
        // 内发光强度
        const baseIntensity = glowIntensity * 100.0;
        // 内发光颜色
        const [r,g,b] = hexToArray(glowColor)
    
        for (let i = 1; i <= numShadows; i++) {
            context.shadowColor = `rgba(${r}, ${g}, ${b}, ${baseIntensity * (numShadows - i + 1) / numShadows})`; // 内发光颜色和透明度
            context.shadowBlur = glowBlur * i; // 增加模糊程度
            context.shadowOffsetX = 0; // 内发光水平偏移
            context.shadowOffsetY = 0; // 内发光垂直偏移
            context.stroke(); // 重新填充以应用内发光效果
        }
    
        return new THREE.CanvasTexture(canvas, null, THREE.RepeatWrapping, THREE.RepeatWrapping);
    }
    /**
    * 获取材质
    * @private
    * @param {*} range polygon边界矩形范围
    * @param {*} path 边界路径
    * @returns
    */
    getMaterial(range, path) {
        // 通过边缘坐标数据获取灰度图纹理
        const texture = this.generateCanvasTexture(range, path);
        texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
    
        const shaderMaterial = new THREE.MeshBasicMaterial({
            map: texture,
            transparent: true,
            side: THREE.DoubleSide,
            opacity: 0.9
        });
        return shaderMaterial;
    }
    
    /**
     * 绘制多边形
     * @private
     * @param {Array} path 路径
     * @param {Object} properties 属性
     */
    drawPolygon({ path, properties }) {
        const { altitude, opacity } = this._conf
        // 获取多边形的范围
        const range = this.getExtRange(path)
    
        // 创建几何体
        const geometry = this.generateOneGeometry(range, path)
        // 创建材质
        const material = this.getMaterial(range, path)
        // 创建多边形的网格对象
        const polygon = new THREE.Mesh(geometry, material)
        polygon.position.z = altitude
        // 缓存属性
        polygon._attrs = properties
        // 将多边形网格对象添加到场景中
        const _scene = this.group || this.scene
        _scene.add(polygon)
    
        this._meshs.push(polygon)
    }
  3. 贴图到几何体上,我们可以借助dat.gui控件调整到最佳效果,从目前看,使用Canvas绘制的方法是相对省心,效果最好的

方案4.编写着色器

  1. 使用着色器材质的方式是最灵活且性能潜力最大的,毕竟是GPU渲染,问题就是难度太大,不过可以上shadertoy抄作业,前提需要懂GLSL和计算机图形学的基础知识

  2. 说实话写shader挺难的,但如果人人都会写,就体现不出个人能力优势了。这里我们参考距离场实现描边的方案。 a. shader核心逻辑就是针对矩形贴图中每个像素点,判断它是否在polygon内部,且跟polygon边缘的最短距离,根据这个信息确定其透明度a。如果点在polygon外部,a为0,即不显示;如果点在polygon内部,且与边缘距离d比在设定范围edgeWidth内,则根据d与edgeWidth的比例调整透明度a,如果在edgeWidth范围外,也不显示。

    b. 如何获取Polygon内1个像素点P跟Polygon边缘的最小距离呢?其实是遍历了该像素点周围一圈的所有点之后确定的,如果这个点刚好在边缘上,就测出它与P的距离distance,如果distance比minDistance还小则替换minDistance

  3. 着色器的具体代码实现可以看这里

    jsx 复制代码
    const shader = {
    
        uniforms: {
            inputImageTexture: { value: null }, // polygon灰度图
            glowColor: { value: new THREE.Color(0x00ffff) }, //发光颜色
            glowIntensity: { value: 2.0 }, //发光强度
            stepWidth: { value: 1.0 }, //固定值,控制过渡的平滑度
            resolution: { value: new THREE.Vector2(100, 100)},画布的宽高
            md: { value: 10.0 },//固定值,控制过渡的平滑度
            edgeWidth: { value: 0.08 },// 内发光范围占画布整体的百分比
            textureOffsetX: { value: 0.0 },//纹理X偏移
            textureOffsetY: { value: 0.0 }//纹理Y偏移
        },
        /**
         * [AIGC] 定义顶点着色器代码
         */
        vertexShader: `
            varying vec2 vUv;
            void main() {
                vUv = uv;
                gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
            }
      `,
        /**
         * [AIGC] 定义片元着色器代码
         */
        fragmentShader: `
            uniform sampler2D inputImageTexture;
            uniform vec3 glowColor;
            uniform float glowIntensity;
            uniform float edgeWidth;
            uniform vec2 resolution
            uniform float textureOffsetX;
            uniform float textureOffsetY;
            varying vec2 vUv;
    
            // 在for循环中需要设置为静态值,该算法复杂度太高 width*height*md*md
            // todo: 待优化为双通道有向距离场生成
            const float md = 20.0;
            const float stepWidth = 1.0;
    
            // 判断点是否在边界内
            float source(vec2 uv) {
                vec2 textureOffset = vec2(textureOffsetX, textureOffsetY);
                vec2 finalUV = uv - textureOffset;
                return texture2D(inputImageTexture, finalUV).r - 0.7;
            }
    
            // 计算当前点到边界的最短距离
            float distanceToEdge(vec2 point) {
                float minDistance = md;
    
                // 处于边界外
                if(source(point) < 0.0){
                    return minDistance;
                }
    
                for (float x = -md ; x <= md; x += stepWidth) {
                    for (float y = -md; y <= md; y += stepWidth) {
                        // 采样点坐标
                        vec2 samplePoint = point + vec2(x, y) / resolution;
                        // 判断采样点是否刚好在边界
                        if (source(samplePoint) < 0.0) {
                            float distance = length(samplePoint - point);
                            if (distance < minDistance) {
                                minDistance = distance;
                            }
                        }
                    }
                }
                return minDistance;
            }
    
            //
            void main() {
                // 计算到最近边界的距离 distance值应该是归一化后的值
                float distance = distanceToEdge(vUv);
    
                // 计算透明度渐变: 距离越远alpha越小
                float alphaFactor = smoothstep(0.0, 1.0, 1.0 - clamp( distance / edgeWidth, 0.0, 1.0));
    
                // 应用发光效果
                // float glowFactor = exp(-distance * glowIntensity);
                vec3 finalGlow = glowColor * glowIntensity;
    
                // 输出最终颜色
                gl_FragColor = vec4(finalGlow, alphaFactor);
            }
      `,
    };
  4. 在代码实现上与其他三个方案最大的区别就是getMaterial材质的生成,核心代码已经给出,后面的依葫芦画瓢即可。

    jsx 复制代码
    /**
     * 获取材质
     * @private
     * @param {*} range polygon边界矩形范围
     * @param {*} path 边界路径
     * @returns
     */
    getMaterial(range, path) {
    
        // 通过边缘坐标数据获取灰度图纹理
        const texture = this.generateAlphaMap(range, path);
        // 获取canvas的宽高
        const { width, height } = texture.source.data
        // 着色器
        const { fragmentShader, vertexShader } = PolyInnerGlowShader
        // 自定义材质
        const shaderMaterial = new THREE.ShaderMaterial({
            uniforms: {
                inputImageTexture: { value: texture }, // polygon灰度图
                glowColor: { value: new THREE.Color(0x00ffff) },//发光颜色
                glowIntensity: { value: 2.0 }, //发光强度
                stepWidth: { value: 1.0 },//固定值,控制过渡的平滑度
                md: { value: 10.0 }, //固定值, 控制每个像素点检测范围
                resolution: { value: new THREE.Vector2(width, height) },//画布尺寸
                edgeWidth: { value: 0.02 }, // 内发光范围占画布整体的百分比
                textureOffsetX: { value: 0.0 }, //纹理X偏移
                textureOffsetY: { value: 0.0 } //纹理Y偏移
            },
            vertexShader,
            fragmentShader,
            transparent: true,
            side: THREE.DoubleSide
        });
        return shaderMaterial;
    }
  5. 最后实现效果如下,shader方案的动态更新响应性能非常高,即使目前的代码仍有优化空间。

总结

最后综合考虑可读性、可维护性、实现难度等综合因素,我们选择了方案3Canvas贴图方案作为产品常用方案。

在鲁迅的小说中,孔乙己虽然掌握了茴字的四种写法,但他只是把知识作为炫耀自己的工具,而没有真正将知识用于启发他人或推动社会进步。这也提醒我们知识的价值不仅在于掌握,更在于传播和应用,这是我编写这边文章的初衷。今天就分享到这里吧。

相关链接

用hlsl/glsl实现内发光效果

OpenGL之仿美图实现不规则物体加描边特效

How to add an inner glow to polygons with the ArcGIS API for JavaScript

记录Canvas图案内阴影效果的实现

相关推荐
2401_878454532 小时前
Themeleaf复用功能
前端·学习
葡萄城技术团队4 小时前
基于前端技术的QR码API开发实战:从原理到部署
前端
八了个戒5 小时前
「数据可视化 D3系列」入门第三章:深入理解 Update-Enter-Exit 模式
开发语言·前端·javascript·数据可视化
noravinsc6 小时前
html页面打开后中文乱码
前端·html
小满zs6 小时前
React-router v7 第四章(路由传参)
前端·react.js
小陈同学呦7 小时前
聊聊双列瀑布流
前端·javascript·面试
键指江湖7 小时前
React 在组件间共享状态
前端·javascript·react.js
诸葛亮的芭蕉扇8 小时前
D3路网图技术文档
前端·javascript·vue.js·microsoft
小离a_a8 小时前
小程序css实现容器内 数据滚动 无缝衔接 点击暂停
前端·css·小程序