用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图案内阴影效果的实现

相关推荐
2013编程爱好者11 小时前
Vue工程结构分析
前端·javascript·vue.js·typescript·前端框架
小满zs12 小时前
Next.js第十一章(渲染基础概念)
前端
不羁的fang少年13 小时前
前端常见问题(vue,css,html,js等)
前端·javascript·css
change_fate13 小时前
el-menu折叠后文字下移
前端·javascript·vue.js
yivifu13 小时前
CSS Grid 布局详解(2025最新标准)
前端·css
o***Z44814 小时前
前端性能优化案例
前端
张拭心15 小时前
前端没有实际的必要了?结合今年工作内容,谈谈我的看法
前端·ai编程
姜太小白15 小时前
【前端】CSS媒体查询响应式设计详解:@media (max-width: 600px) {……}
前端·css·媒体
HIT_Weston15 小时前
39、【Ubuntu】【远程开发】拉出内网 Web 服务:构建静态网页(二)
linux·前端·ubuntu
百***060115 小时前
SpringMVC 请求参数接收
前端·javascript·算法