火球效果 · Fireball · ▶ 在线运行案例

火球效果

你将学到什么

  • ShaderMaterial 自定义着色器实现核心视觉效果
  • EffectComposer 多 Pass 后期处理管线
  • UnrealBloomPass 辉光 Bloom 效果
  • OrbitControls 相机轨道交互
  • 自定义 ShaderPass 调色/特效
  • requestAnimationFrame 渲染循环与 resize 自适应

效果说明

本案例演示 火球效果 效果:原场景渲染后经 EffectComposer 叠加 Bloom/模糊等全屏后期;核心用到 ShaderMaterial、EffectComposer、UnrealBloomPass。建议先打开文首在线案例查看动态画面,再对照下方源码逐步理解。

核心概念

  • Scene / Camera / WebGLRenderer 构成最小渲染闭环;大场景可开 logarithmicDepthBuffer 缓解 Z-fighting。
  • ShaderMaterial 通过 uniforms + 自定义 GLSL 控制逐像素/逐点效果;透明粒子常配合 depthTest: false
  • EffectComposer 以多 Pass 链式渲染:RenderPass → 特效 Pass → 输出屏幕,替代直接 renderer.render
  • OrbitControls 提供轨道旋转/缩放;开启 enableDamping 后需在 animate 中 controls.update()

实现步骤

  1. 搭建灯光与环境(如有)
  2. requestAnimationFrame 循环 update + render

代码要点

<!DOCTYPE html><html lang="en"><head>
    <title>three.js</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
  </head>
  <body>
    <script type="importmap">
        {
          "imports": {
            "three": "https://threejs.org/build/three.module.js",
            "three/addons/": "https://threejs.org/examples/jsm/"
          }
        }
      </script>

    <script type="module">
      import * as THREE from "three";
      import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
      import { RenderPass } from "three/addons/postprocessing/RenderPass.js";
      import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js";
      import { ShaderPass } from "three/addons/postprocessing/ShaderPass.js";
      import { OrbitControls } from "three/addons/controls/OrbitControls.js";
      import { GUI } from "three/addons/libs/lil-gui.module.min.js";

      const ENTIRE_SCENE = 0,
        BLOOM_SCENE = 1;

      const bloomLayer = new THREE.Layers();
      bloomLayer.set(BLOOM_SCENE);
      const materials = {};
      const darkMaterial = new THREE.MeshBasicMaterial({ color: "black" });

      const vert = `
    varying vec3 vNormal;
    varying vec3 camPos;
    varying vec2 vUv;

    void main() {
    vNormal = normal;
    vUv = uv;
    camPos = cameraPosition;
    gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
    }
`;

      const frag = `
#define NUM_OCTAVES 5
#define M_PI 3.1415926535897932384626433832795
uniform vec4 resolution;
varying vec3 vNormal;
uniform sampler2D perlinnoise;
uniform sampler2D sparknoise;
uniform float time;
uniform vec3 color0;
uniform vec3 color1;
uniform vec3 color2;
uniform vec3 color3;
uniform vec3 color4;
uniform vec3 color5;
varying vec3 camPos;
varying vec2 vUv;

float setOpacity(float r, float g, float b, float tonethreshold) {
  float tone = (r + g + b) / 3.0;
  float alpha = 1.0;
  if(tone<tonethreshold) {
    alpha = 0.0;
  }
  return alpha;
}

vec3 rgbcol(vec3 col) {
  return vec3(col.r/255.0,col.g/255.0,col.b/255.0);
}

vec2 rotate(vec2 v, float a) {
  float s = sin(a);
  float c = cos(a);
  mat2 m = mat2(c, -s, s, c);
  return m * v;
}

vec2 UnityPolarCoordinates (vec2 UV, vec2 Center, float RadialScale, float LengthScale){
  //https://twitter.com/Cyanilux/status/1123950519133908995/photo/1
  vec2 delta = UV - Center;
  float radius = length(delta) * 2. * RadialScale;
  float angle = atan(delta.x, delta.y) * 1.0/6.28 * LengthScale;
  return vec2(radius, angle);
}

void main() {
  vec2 olduv = gl_FragCoord.xy/resolution.xy ;
  vec2 uv = vUv ; 
  vec2 imguv = uv;
  float scale = 1.;
  olduv *= 0.5 + time; 
  olduv.y = olduv.y ;
  vec2 p = olduv*scale;
  vec4 txt = texture2D(perlinnoise, olduv);
  float gradient = dot(normalize( -camPos ), normalize( vNormal ));
  float pct = distance(vUv,vec2(0.5));

  vec3 rgbcolor0 = rgbcol(color0);
  vec3 rgbcolor1 = rgbcol(color1);
  vec3 rgbcolor2 = rgbcol(color2);
  vec3 rgbcolor5 = rgbcol(color5);

  // set solid background
  float y = smoothstep(0.16,0.525,pct);
  vec3 backcolor = mix(rgbcolor0, rgbcolor5, y);

  gl_FragColor = vec4(backcolor,1.);

  // set polar coords
  vec2 center = vec2(0.5);
  vec2 cor = UnityPolarCoordinates(vec2(vUv.x,vUv.y), center, 1., 1.);

  // set textures
  vec2 newUv = vec2(cor.x + time,cor.x*0.2+cor.y);
  vec3 noisetex = texture2D(perlinnoise,mod(newUv,1.)).rgb;    
  vec3 noisetex2 = texture2D(sparknoise,mod(newUv,1.)).rgb;    


  // set textures tones
  float tone0 =  1. - smoothstep(0.3,0.6,noisetex.r);
  float tone1 =  smoothstep(0.3,0.6,noisetex2.r);


  // set opacity for each tone
  float opacity0 = setOpacity(tone0,tone0,tone0,.29);
  float opacity1 = setOpacity(tone1,tone1,tone1,.49);

  //set final render
  if(opacity1>0.0){
    gl_FragColor = vec4(rgbcolor2,0.)*vec4(opacity1);
  } else if(opacity0>0.0){
    gl_FragColor = vec4(rgbcolor1,0.)*vec4(opacity0);
  }   
}
`;

      const vertcylinder = `
    varying vec2 vUv;

    void main() {
        vUv = uv;
        vec3 pos = vec3(position.x/1.,position.y,position.z/1.);
        if(pos.y >= 1.87){
            pos = vec3(position.x*(sin((position.y - 0.6)*1.27)-0.16),position.y,position.z*(sin((position.y - 0.6)*1.27)-0.16));
        } else{
            pos = vec3(position.x*(sin((position.y/2. -  .01)*.11)+0.75),position.y,position.z*(sin((position.y/2. -  .01)*.11)+0.75));
        }
        gl_Position = projectionMatrix * modelViewMatrix * vec4( pos, 1.0 );
    }
`;

      const fragcylinder = `
    varying vec2 vUv;
    uniform sampler2D perlinnoise;
    uniform vec3 color4;
    uniform float time;
    varying vec3 vNormal;

    vec3 rgbcol(vec3 col) {
        return vec3(col.r/255.0,col.g/255.0,col.b/255.0);
    }

    void main() {
        vec3 noisetex = texture2D(perlinnoise,mod(1.*vec2(vUv.y-time*2.,vUv.x + time*1.),1.)).rgb;    
        gl_FragColor = vec4(noisetex.r);

        if(gl_FragColor.r >= 0.5){
            gl_FragColor = vec4(rgbcol(color4),gl_FragColor.r);
        }else{
            gl_FragColor = vec4(0.);
        }
        gl_FragColor *= vec4(sin(vUv.y) - 0.1);
        gl_FragColor *= vec4(smoothstep(0.3,0.628,vUv.y));

    }

`;

      const vertflame = `
    varying vec2 vUv;
    varying vec3 camPos;
    varying vec3 vNormal;
    varying vec3 nois;
    uniform sampler2D noise;
    uniform float time;

    void main() {
        vUv = uv;
        camPos = cameraPosition;
        vNormal = normal;
        vec3 pos = vec3(position.x/1.,position.y,position.z/1.);
        vec3 noisetex = texture2D(noise,mod(1.*vec2(vUv.y-time*2.,vUv.x + time*1.),1.)).rgb;
        if(pos.y >= 1.87){
            pos = vec3(position.x*(sin((position.y - 0.64)*1.27)-0.12),position.y,position.z*(sin((position.y - 0.64)*1.27)-0.12));
        } else{
            pos = vec3(position.x*(sin((position.y/2. -  .01)*.11)+0.79),position.y,position.z*(sin((position.y/2. -  .01)*.11)+0.79));
        }
        pos.xz *= noisetex.r;
        gl_Position = projectionMatrix * modelViewMatrix * vec4( pos, 1.0 );
    }
`;

      const fragflame = `
    varying vec2 vUv;
    uniform sampler2D perlinnoise;
    uniform sampler2D noise;
    uniform vec3 color4;
    uniform float time;
    varying vec3 camPos;
    varying vec3 vNormal;
    varying vec3 nois;

    vec3 rgbcol(vec3 col) {
        return vec3(col.r/255.0,col.g/255.0,col.b/255.0);
    }

      
    void main() {
        // vec3 noisetex = texture2D(perlinnoise,mod(1.*vec2(vUv.y-time*2.,vUv.x + time*1.),1.)).rgb;    
        // gl_FragColor += vec4(sin((vUv.y - time)*(20. + vUv.y)));
        vec3 noisetex = texture2D(noise,mod(1.*vec2(vUv.y-time*2.,vUv.x + time*1.),1.)).rgb;
        // nois = noisetex;
        gl_FragColor = vec4(noisetex.r);

        if(gl_FragColor.r >= 0.44){
            gl_FragColor = vec4(rgbcol(color4),gl_FragColor.r);
        }
        // else if(gl_FragColor.r >= 0.9){
        //     // gl_FragColor = vec4(rgbcol(color4),gl_FragColor.r)*0.5;
        // }
        else{
            gl_FragColor = vec4(0.);
        }
        gl_FragColor *= vec4(smoothstep(0.2,0.628,vUv.y));
        // gl_FragColor = vec4(vUv.y - 0.3 );
        // gl_FragColor = 1. - vec4(dot(normalize(vNormal),normalize(camPos)).r);
    }

`;

      let options = {
        exposure: 2.8,
        bloomStrength: 3.5,
        bloomRadius: 0.39,
        color0: [0, 0, 0],
        color1: [81, 14, 5],
        color2: [181, 156, 24],
        color3: [66, 66, 66],
        color4: [79, 79, 79],
        color5: [64, 27, 0],
      };

      // let options = {
      //   exposure: 2.8,
      //   bloomStrength: 3.5,
      //   //   bloomStrength: 0,
      //   bloomRadius: 0.39,
      //   color0: [15, 0, 10],
      //   color1: [13, 6, 5],
      //   color2: [137, 56, 10],
      //   color3: [166, 166, 166],
      //   color4: [237, 149, 67],
      //   color5: [20, 0, 51],
      // };
      let gui = new GUI();

      let bloom = gui.addFolder("Bloom");
      bloom
        .add(options, "bloomStrength", 0.0, 5.0)
        .name("bloomStrength")
        .listen();
      bloom.add(options, "bloomRadius", 0.1, 2.0).name("bloomRadius").listen();
      bloom.open();

      let color = gui.addFolder("Colors");
      color.addColor(options, "color0").name("ball0");
      color.addColor(options, "color1").name("ball1");
      color.addColor(options, "color2").name("ball2");
      color.addColor(options, "color4").name("steam");
      color.addColor(options, "color5").name("trail");
      color.open();
      gui.close();

      let scene,
        camera,
        renderer,
        controls,
        material,
        material2,
        material3,
        bloomPass,
        bloomComposer,
        composer,
        finalPass,
        finalComposer;
      const width = window.innerWidth,
        height = window.innerHeight;

      function init() {
        createScene();
        postProc();
        mesh(); 
        animatBloom();
      }

      function createScene() {
        scene = new THREE.Scene();
        camera = new THREE.PerspectiveCamera(
          75,
          window.innerWidth / window.innerHeight,
          0.1,
          1000
        );
        camera.position.set(
          3.4369982203815655,
          3.5239085092722098,
          2.994862383531814
        );
        renderer = new THREE.WebGLRenderer();
        renderer.antialias = true;
        renderer.setClearColor(new THREE.Color("#000"));
        renderer.setPixelRatio(window.devicePixelRatio);
        renderer.setSize(width, height);
        controls = new OrbitControls(camera, renderer.domElement);
        document.body.appendChild(renderer.domElement);
      }

      function postProc() {
        const renderScene = new RenderPass(scene, camera);
        bloomPass = new UnrealBloomPass(
          new THREE.Vector2(window.innerWidth, window.innerHeight),
          1.5,
          0.4,
          0.85
        );
        bloomPass.threshold = options.bloomThreshold;
        bloomPass.strength = options.bloomStrength;
        bloomPass.radius = options.bloomRadius;

        bloomComposer = new EffectComposer(renderer);
        bloomComposer.addPass(renderScene);
        bloomComposer.addPass(bloomPass);
      }

      function mesh() {
        const geometry = new THREE.SphereGeometry(1, 30, 30);
        material = new THREE.ShaderMaterial({
          uniforms: {
            time: {
              type: "f",
              value: 0.0,
            },
            perlinnoise: {
              type: "t",
              value: new THREE.TextureLoader().load(
                FILE_HOST + "images/dissolve/dissolveRamp.png"
              ),
            },
            sparknoise: {
              type: "t",
              value: new THREE.TextureLoader().load(
                FILE_HOST + "images/dissolve/dissolveTex.png"
              ),
            },
            color5: {
              value: new THREE.Vector3(...options.color5),
            },
            color4: {
              value: new THREE.Vector3(...options.color4),
            },
            color3: {
              value: new THREE.Vector3(...options.color3),
            },
            color2: {
              value: new THREE.Vector3(...options.color2),
            },
            color1: {
              value: new THREE.Vector3(...options.color1),
            },
            color0: {
              value: new THREE.Vector3(...options.color0),
            },
            resolution: { value: new THREE.Vector2(width, height) },
          },
          vertexShader: vert,
          fragmentShader: frag,
        });

        const mesh = new THREE.Mesh(geometry, material);
        mesh.scale.set(0.78, 0.78, 0.78);
        mesh.position.set(1 + 0, 0, 0);
        scene.add(mesh);
      }

  
 
      function updateDraw(deltaTime) {
        material.uniforms.time.value = -deltaTime / (1000 * 2); 
        material.uniforms.color5.value = new THREE.Vector3(...options.color5); 
        material.uniforms.color3.value = new THREE.Vector3(...options.color3);
        material.uniforms.color2.value = new THREE.Vector3(...options.color2);
        material.uniforms.color1.value = new THREE.Vector3(...options.color1);
        material.uniforms.color0.value = new THREE.Vector3(...options.color0);
      }

      function animatBloom(deltaTime) {
        requestAnimationFrame(animatBloom);
        updateDraw(deltaTime);
        controls.update();
        bloomPass.strength = options.bloomStrength;
        bloomPass.radius = options.bloomRadius;
        bloomComposer.render();
      }

      function handleResize() {
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();
        renderer.setSize(window.innerWidth, window.innerHeight);
      }

      window.addEventListener("load", init);
      window.addEventListener("resize", handleResize, false);
    </script>
  
  </body>

</html>

完整源码:GitHub

小结