Three.js扩散波纹动画实现

一、效果展示

扩散波纹动画是一种以场景中心为起点,向外不断扩散的圆形波纹效果,常用于地图中心点、雷达扫描等场景。波纹呈现出柔和的发光效果,由中心向外不断循环扩散。

二、实现原理

2.1 整体架构

扩散波纹动画的实现分为三个核心部分:

  1. 平面载体 :使用 PlaneGeometry 创建一个足够大的平面,作为波纹的渲染载体
  2. 自定义着色器 :通过 onBeforeCompile 钩子注入 GLSL 代码,实现波纹的计算与渲染
  3. 动画驱动:使用 GSAP 动画库驱动 uniform 变量,实现波纹的循环扩散

2.2 着色器实现

2.2.1 顶点着色器修改

在顶点着色器中添加 vPosition 变量,将片元的位置信息传递到片元着色器:

glsl 复制代码
// 在 void main() 之前声明 varying 变量
varying vec3 vPosition;

void main(){
    vPosition = position;  // 将顶点位置传递给片元着色器
    // ... 原有代码
}

2.2.2 片元着色器修改

片元着色器是波纹计算的核心,主要逻辑包括:

  1. 计算波纹半径r = uTime * uSpeed(当前波纹的前沿半径)
  2. 计算片元距离rDistance = distance(vPosition.xy, center)(片元到中心的距离)
  3. 判断波纹范围rDistance > r && rDistance < r + 2.0 * w
  4. 平滑过渡 :使用 smoothstep 函数实现波纹前后沿的渐隐效果
  5. 颜色混合 :使用 mix 函数在原色和波纹色之间平滑过渡
glsl 复制代码
// 声明 uniform 变量
uniform float uTime;    // 时间控制
uniform float uSpeed;   // 扩散速度
uniform float uWidth;   // 波纹宽度
uniform vec3 uColor;    // 波纹颜色
varying vec3 vPosition; // 顶点位置

void main(){
    // 计算当前波纹前沿位置
    float r = uTime * uSpeed;
    
    // 波纹半宽
    float w = uWidth;
    
    // 波纹中心坐标
    vec2 center = vec2(0.0, 0.0);
    
    // 计算片元到中心的距离
    float rDistance = distance(vPosition.xy, center);
    
    // 判断是否在波纹范围内
    if(rDistance > r && rDistance < r + 2.0 * w) {
        float per = 0.0;
        
        // 波纹前沿(从外向内过渡)
        if(rDistance < r + w) {
            float p = smoothstep(0.0, 1.0, (rDistance - r) / w);
            p *= p;  // 增强渐隐效果
            outgoingLight = mix(outgoingLight, uColor, p);
        } 
        // 波纹后沿(从内向外过渡)
        else {
            float p = smoothstep(0.0, 1.0, (rDistance - r - w) / w);
            outgoingLight = mix(uColor, outgoingLight, p);
        }
        gl_FragColor = vec4(outgoingLight, diffuseColor.a);
    } 
    // 不在波纹范围内,完全透明
    else {
        gl_FragColor = vec4(outgoingLight, 0.0);
    }
}

2.3 混合模式配置

为了实现发光叠加效果,需要配置自定义混合模式:

javascript 复制代码
material.blending = THREE.CustomBlending
material.blendEquation = THREE.AddEquation
material.blendSrc = THREE.DstColorFactor
material.blendDst = THREE.OneFactor

混合公式result = src_color * DstColorFactor + dst_color * OneFactor

这种混合模式让波纹颜色与背景颜色产生叠加发光效果。

2.4 动画驱动

使用 GSAP 驱动 uTime uniform 变量,实现波纹循环扩散:

javascript 复制代码
gsap.to(shader.uniforms.uTime, {
    value: 4,       // 从 0 动画到 4
    repeat: -1,     // 无限循环
    duration: 6,    // 每次循环 6 秒
    ease: 'power1.easeIn',  // 缓动函数
})

三、完整 HTML 实现

以下是一个可独立运行的扩散波纹动画示例:

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>扩散波纹动画</title>
    <style>
        body { margin: 0; overflow: hidden; background: #102736; }
        canvas { display: block; }
    </style>
</head>
<body>
    <script type="importmap">
    {
        "imports": {
            "three": "https://esm.sh/three@0.161.0",
            "three/addons/": "https://esm.sh/three@0.161.0/examples/jsm/"
        }
    }
    </script>
    <script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>
    <script type="module">
        import * as THREE from 'three'
        import { OrbitControls } from 'three/addons/controls/OrbitControls.js'

        // 1. 初始化场景
        const scene = new THREE.Scene()
        scene.background = new THREE.Color(0x102736)

        const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000)
        camera.position.set(0, 10, 20)
        camera.lookAt(0, 0, 0)

        const renderer = new THREE.WebGLRenderer({ antialias: true })
        renderer.setSize(window.innerWidth, window.innerHeight)
        renderer.setPixelRatio(window.devicePixelRatio)
        document.body.appendChild(renderer.domElement)

        // 控制器
        const controls = new OrbitControls(camera, renderer.domElement)
        controls.enableDamping = true
        controls.enablePan = false

        // 2. 创建扩散波纹
        function createDiffuse() {
            // 2.1 创建平面载体
            const geometry = new THREE.PlaneGeometry(200, 200)

            // 2.2 创建基础材质
            const material = new THREE.MeshBasicMaterial({
                color: 0x000000,
                depthWrite: false,
                depthTest: false,
                transparent: true,
                blending: THREE.CustomBlending,
            })

            // 2.3 配置混合模式
            material.blendEquation = THREE.AddEquation
            material.blendSrc = THREE.DstColorFactor
            material.blendDst = THREE.OneFactor

            // 2.4 注入自定义着色器
            material.onBeforeCompile = (shader) => {
                // 添加 uniform 变量
                shader.uniforms = {
                    ...shader.uniforms,
                    uTime: { value: 0 },
                    uSpeed: { value: 8.0 },
                    uWidth: { value: 2.0 },
                    uColor: { value: new THREE.Color(0x71918e) },
                }

                // 顶点着色器 - 添加 varying 变量
                shader.vertexShader = shader.vertexShader.replace(
                    'void main() {',
                    `
                    varying vec3 vPosition;
                    void main(){
                        vPosition = position;
                    `
                )

                // 片元着色器 - 声明 uniform 和 varying
                shader.fragmentShader = shader.fragmentShader.replace(
                    'void main() {',
                    `
                    uniform float uTime;
                    uniform float uSpeed;
                    uniform float uWidth;
                    uniform vec3 uColor;
                    varying vec3 vPosition;
                    void main(){
                    `
                )

                // 片元着色器 - 替换片元颜色计算逻辑
                shader.fragmentShader = shader.fragmentShader.replace(
                    '#include <opaque_fragment>',
                    `
                    #ifdef OPAQUE
                    diffuseColor.a = 1.0;
                    #endif
                    
                    #ifdef USE_TRANSMISSION
                    diffuseColor.a *= material.transmissionAlpha;
                    #endif
                    
                    // 计算波纹前沿半径
                    float r = uTime * uSpeed;
                    
                    // 波纹半宽
                    float w = uWidth;
                    
                    // 波纹中心
                    vec2 center = vec2(0.0, 0.0);
                    
                    // 计算片元到中心的距离
                    float rDistance = distance(vPosition.xy, center);
                    
                    // 判断是否在波纹范围内
                    if(rDistance > r && rDistance < r + 2.0 * w) {
                        float per = 0.0;
                        
                        // 波纹前沿(从外向内过渡)
                        if(rDistance < r + w) {
                            float p = smoothstep(0.0, 1.0, (rDistance - r) / w);
                            p *= p;
                            outgoingLight = mix(outgoingLight, uColor, p);
                        } 
                        // 波纹后沿(从内向外过渡)
                        else {
                            float p = smoothstep(0.0, 1.0, (rDistance - r - w) / w);
                            outgoingLight = mix(uColor, outgoingLight, p);
                        }
                        gl_FragColor = vec4(outgoingLight, diffuseColor.a);
                    } 
                    // 不在波纹范围内,完全透明
                    else {
                        gl_FragColor = vec4(outgoingLight, 0.0);
                    }
                    `
                )

                // 保存 shader 引用用于动画
                window.diffuseShader = shader
            }

            // 2.5 创建网格并添加到场景
            const mesh = new THREE.Mesh(geometry, material)
            mesh.rotation.x = -Math.PI / 2  // 旋转至水平
            mesh.position.set(0, 0.21, 0)   // 稍微抬高避免 z-fighting
            mesh.renderOrder = 3
            scene.add(mesh)

            // 2.6 启动动画
            setTimeout(() => {
                gsap.to(window.diffuseShader.uniforms.uTime, {
                    value: 4,
                    repeat: -1,
                    duration: 6,
                    ease: 'power1.easeIn',
                })
            }, 1000)
        }

        createDiffuse()

        // 3. 添加参考网格(可选)
        const gridHelper = new THREE.GridHelper(20, 20, 0x1b4b70, 0x1b4b70)
        gridHelper.material.opacity = 0.3
        gridHelper.material.transparent = true
        scene.add(gridHelper)

        // 4. 渲染循环
        function animate() {
            requestAnimationFrame(animate)
            controls.update()
            renderer.render(scene, camera)
        }
        animate()

        // 5. 响应窗口大小变化
        window.addEventListener('resize', () => {
            camera.aspect = window.innerWidth / window.innerHeight
            camera.updateProjectionMatrix()
            renderer.setSize(window.innerWidth, window.innerHeight)
        })
    </script>
</body>
</html>

四、参数说明

参数 类型 默认值 说明
uTime float 0 → 4 时间控制变量,通过 GSAP 驱动
uSpeed float 8.0 波纹扩散速度,值越大波纹扩散越快
uWidth float 2.0 波纹宽度,影响波纹的厚度
uColor Color 0x71918e 波纹颜色
duration float 6 单次循环时长(秒)
value float 4 uTime 动画的目标值,影响扩散范围

五、扩展应用

  1. 多波纹叠加 :可以创建多个平面,使用不同的 uTime 初始值实现多层波纹
  2. 自定义形状 :修改 distance 函数,实现矩形、三角形等形状的波纹
  3. 多颜色渐变 :在 mix 中加入更多颜色,实现彩虹波纹效果
  4. 波纹衰减 :根据 rDistance 添加衰减因子,实现远处波纹变暗的效果
相关推荐
Htr_2 小时前
Cortex 使用指南:开源 API 知识层
前端·数据库·人工智能·重构·计算机外设
计算机魔术师3 小时前
Dario Amodei 发文呼吁 AI 行业放慢前沿速度并公布三步计划
前端
EatFan3 小时前
前后端分离项目中 Token 到底应该怎么设计?Access Token + Refresh Token 实战
java·前端·后端
dsyyyyy11013 小时前
Typescript装饰器
前端·javascript·typescript
一拳不是超人4 小时前
一个没测暗色模式的 Bug,吃掉了我一半 谷歌扩展用户
前端·javascript·程序员
liangshanbo12154 小时前
React useState 函数式更新面试题整理
前端·react.js·前端框架
涛涛ing4 小时前
为什么你的页面在 Safari 上总出问题?Interop 2027 正在解决这个 20 年老毛病
前端
不一样的少年_4 小时前
WebP 压缩到底在干嘛?小白也能看懂的原理拆解
前端·后端·图片资源
问心无愧05135 小时前
ctf show web 177
前端·笔记
不一样的少年_5 小时前
PNG/JPG 如何变成 WebP?真相不是改后缀!
前端·后端·图片资源