使用 Three.js 实现火焰效果

大家好!我是 [数擎 AI],一位热爱探索新技术的前端开发者,在这里分享前端和 Web3D、AI 技术的干货与实战经验。如果你对技术有热情,欢迎关注我的文章,我们一起成长、进步!
开发领域:前端开发 | AI 应用 | Web3D | 元宇宙
技术栈:JavaScript、React、ThreeJs、WebGL、Go
经验经验:6 年+ 前端开发经验,专注于图形渲染和 AI 技术
演示地址演示地址
开源项目智简未来晓智元宇宙数字孪生引擎源码地址

演示地址:https://shader.shuqin.cc/mdX3zr
源码地址:https://github.com/dezhizhang/shadertoy

引言

在现代 Web 开发中,使用 WebGL 渲染图形已经变得越来越流行。Three.js 作为一个高效的 3D 图形库,简化了 WebGL 的操作,使得创建复杂的 3D 图形变得更加容易。着色器(Shader)是实现视觉效果的核心之一,它允许我们直接控制像素的颜色、位置和其他属性。

在本文中,我们将探讨如何通过自定义的 GLSL (OpenGL Shading Language) 着色器来创建火焰效果,使用 Raymarching 技术来模拟火焰的形态和光辉。

环境设置

首先,我们需要引入 Three.js 并创建一个基础的 WebGL 渲染环境。你可以通过以下代码来实现:

import * as THREE from 'three';

// 创建摄像机
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
// 创建场景
const scene = new THREE.Scene();
// 创建一个平面几何体作为着色器的应用表面
const geometry = new THREE.PlaneGeometry(2, 2);
// 使用自定义的着色器创建材质
const material = new THREE.ShaderMaterial({
  ...flameShader,  // 引用我们接下来的火焰着色器
  depthWrite: false,
  transparent: true
});
// 创建网格对象并将其添加到场景中
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

// 创建 WebGL 渲染器
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

这段代码创建了一个 2D 平面,通过自定义材质和着色器将火焰效果渲染在这个平面上。接下来,我们来详细讲解如何实现火焰效果。

火焰着色器解析

火焰效果的核心在于两个部分:噪声函数和Raymarching。

1. 噪声函数

噪声是模拟自然界中随机现象的一个重要工具。我们使用一个简单的三维噪声函数来生成火焰效果中的随机波动:

float noise(vec3 p) {
  vec3 i = floor(p);
  vec4 a = dot(i, vec3(1., 57., 21.)) + vec4(0., 57., 21., 78.);
  vec3 f = cos((p-i)*acos(-1.))*(-.5)+.5;
  a = mix(sin(cos(a)*a), sin(cos(1.+a)*(1.+a)), f.x);
  a.xy = mix(a.xz, a.yw, f.y);
  return mix(a.x, a.y, f.z);
}

这个函数使用的是 Perlin 噪声的变种,通过对每个像素点进行采样,创建出自然、随机的波动效果。在火焰的表现上,这种噪声将带来动态的变化,模拟出火焰的流动感。

2. 火焰的形态

火焰本身是由许多细小的颗粒组成的,它们随着时间变化而不断变形。我们通过球形方程来定义火焰的形态,并结合噪声来创建动态的效果:

float flame(vec3 p) {
  float d = sphere(p*vec3(1.,.5,1.), vec4(.0,-1.,.0,1.));
  return d + (noise(p+vec3(.0,iTime*2.,.0)) + noise(p*3.)*.5)*.25*(p.y);
}

这里,sphere 函数用于创建一个球体的距离场,而火焰的形态则通过 flame 函数在此基础上加入噪声,并且随着时间 (iTime) 变化,使火焰的外观不断变化。

3. Raymarching 技术

Raymarching 是一种基于逐步逼近的方法,用于在复杂的距离场中寻找表面交点。通过在场景中发射光线并不断前进,直到光线与物体表面相交,可以得到物体的轮廓。

vec4 raymarch(vec3 org, vec3 dir) {
  float d = 0.0, glow = 0.0, eps = 0.02;
  vec3 p = org;
  bool glowed = false;
  
  for(int i=0; i<64; i++) {
    d = scene(p) + eps;
    p += d * dir;
    if(d > eps) {
      if(flame(p) < 0.0) glowed = true;
      if(glowed) glow = float(i)/64.;
    }
  }
  return vec4(p, glow);
}

在这个 Raymarching 循环中,我们通过不断推进光线,并检测其是否与火焰相交,来模拟火焰的光辉效果。glow 变量则控制了火焰的亮度和衰减。

4. 渲染火焰

最后,在着色器的主函数中,我们使用上述的 Raymarching 函数来渲染火焰,并根据计算出的亮度值调整颜色:

void main() {
  vec2 uv = -1.0 + 2.0 * vUv;
  uv.x *= iResolution.x/iResolution.y;
  
  vec3 org = vec3(0., -2., 4.);
  vec3 dir = normalize(vec3(uv.x*1.6, -uv.y, -1.5));
  
  vec4 p = raymarch(org, dir);
  float glow = p.w;
  
  vec4 col = mix(vec4(1.,.5,.1,1.), vec4(0.1,.5,1.,1.), p.y*.02+.4);
  
  gl_FragColor = mix(vec4(0.), col, pow(glow*2.,4.));
}

我们根据计算出的火焰的亮度来混合颜色,使得火焰看起来既明亮又充满动感。

动画与实时更新

为了使火焰效果随时间动态变化,我们需要不断更新着色器中的 iTime 和 iResolution 参数。我们可以通过如下代码来实现动画效果:

function animate() {
  requestAnimationFrame(animate);
  
  material.uniforms.iTime.value += 0.01;
  material.uniforms.iResolution.value.set(
    renderer.domElement.width,
    renderer.domElement.height
  );
  
  renderer.render(scene, camera);
}
animate();

每一帧我们都会增加 iTime 的值,从而让火焰在屏幕上不断变化。

处理窗口大小变化

为了确保火焰效果在窗口尺寸变化时正确显示,我们需要监听窗口的 resize 事件,并更新渲染器的大小以及着色器的分辨率:

window.addEventListener('resize', () => {
  renderer.setSize(window.innerWidth, window.innerHeight);
  material.uniforms.iResolution.value.set(
    renderer.domElement.width,
    renderer.domElement.height
  );
});

源码

import * as THREE from 'three';

const flameShader = {
  uniforms: {
    iTime: { value: 0 },
    iResolution: { value: new THREE.Vector2(1, 1) }
  },

  vertexShader: `
    varying vec2 vUv;
    void main() {
      vUv = uv;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,

  fragmentShader: `
    uniform vec2 iResolution;
    uniform float iTime;
    varying vec2 vUv;

    float noise(vec3 p) {
      vec3 i = floor(p);
      vec4 a = dot(i, vec3(1., 57., 21.)) + vec4(0., 57., 21., 78.);
      vec3 f = cos((p-i)*acos(-1.))*(-.5)+.5;
      a = mix(sin(cos(a)*a), sin(cos(1.+a)*(1.+a)), f.x);
      a.xy = mix(a.xz, a.yw, f.y);
      return mix(a.x, a.y, f.z);
    }

    float sphere(vec3 p, vec4 spr) {
      return length(spr.xyz-p) - spr.w;
    }

    float flame(vec3 p) {
      float d = sphere(p*vec3(1.,.5,1.), vec4(.0,-1.,.0,1.));
      return d + (noise(p+vec3(.0,iTime*2.,.0)) + noise(p*3.)*.5)*.25*(p.y);
    }

    float scene(vec3 p) {
      return min(100.-length(p), abs(flame(p)));
    }

    vec4 raymarch(vec3 org, vec3 dir) {
      float d = 0.0, glow = 0.0, eps = 0.02;
      vec3 p = org;
      bool glowed = false;
      
      for(int i=0; i<64; i++) {
        d = scene(p) + eps;
        p += d * dir;
        if(d > eps) {
          if(flame(p) < 0.0) glowed = true;
          if(glowed) glow = float(i)/64.;
        }
      }
      return vec4(p, glow);
    }

    void main() {
      vec2 uv = -1.0 + 2.0 * vUv;
      uv.x *= iResolution.x/iResolution.y;
      
      vec3 org = vec3(0., -2., 4.);
      vec3 dir = normalize(vec3(uv.x*1.6, -uv.y, -1.5));
      
      vec4 p = raymarch(org, dir);
      float glow = p.w;
      
      vec4 col = mix(vec4(1.,.5,.1,1.), vec4(0.1,.5,1.,1.), p.y*.02+.4);
      
      gl_FragColor = mix(vec4(0.), col, pow(glow*2.,4.));
    }
  `
};

// 使用示例
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
const scene = new THREE.Scene();
const geometry = new THREE.PlaneGeometry(2, 2);
const material = new THREE.ShaderMaterial({
  ...flameShader,
  depthWrite: false,
  transparent: true
});

const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// 更新uniforms
function animate() {
  requestAnimationFrame(animate);
  
  material.uniforms.iTime.value += 0.01;
  material.uniforms.iResolution.value.set(
    renderer.domElement.width,
    renderer.domElement.height
  );
  
  renderer.render(scene, camera);
}

animate();

// 窗口大小变化处理
window.addEventListener('resize', () => {
  renderer.setSize(window.innerWidth, window.innerHeight);
  material.uniforms.iResolution.value.set(
    renderer.domElement.width,
    renderer.domElement.height
  );
});

总结

在这篇博客中,我们通过使用 Three.js 和 GLSL 编写自定义着色器,成功实现了一个动态的火焰效果。通过噪声函数、Raymarching 技术以及实时更新参数,我们能够模拟出真实的火焰效果,并将其渲染到 Web 页面上。希望本教程能为你提供一些灵感,帮助你在自己的项目中实现类似的效果!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值