本文由 AI 生成,结合 GLSL 原理与 Three.js 实践,旨在帮助初学者逐行理解代码,而不是仅仅“照抄能跑”。我会用直观类比、数值例子、代码注释来拆解整个火焰效果。
一、前言:从 Shadertoy 到 Three.js
Shadertoy 上有很多绚丽的着色器,但它们常常让新手望而生畏:几十行数学公式,cos/sin 嵌套,光线行进(raymarching)循环一堆看不懂的变量。
其实这些代码是有逻辑脉络的:
- 定义相机 → 每个像素对应一条射线
- 沿射线逐步前进(raymarching)
- 在每个点做几何变换 + 噪声扰动 → 得到火焰体积感
- 根据深度和噪声算颜色 → 累积
- 最后做 tone mapping → 显示在屏幕上
这篇文章会带你走完整个过程,并给出 Vue + Three.js 的完整实现。
二、完整片元着色器代码(带注释)
1// 火焰效果片元着色器 2// 输入:时间 iTime(秒),画布分辨率 iResolution(x=宽,y=高,z备用) 3uniform float iTime; 4uniform vec3 iResolution; 5 6void mainImage(out vec4 fragColor, vec2 fragCoord) 7{ 8 float t = iTime; // 动画时间 9 float rayDepth = 0.0; // 射线累计前进距离 10 vec3 col = vec3(0.0); // 颜色累积器 11 12 // 将像素坐标转为 [-1,1] 的标准化坐标 13 vec2 uv = (2.0 * fragCoord - iResolution.xy) / iResolution.y; 14 15 // 相机设置 16 vec3 rayOrigin = vec3(0.0, 0.0, 0.0); // 相机位置 17 vec3 camPos = vec3(0.0, 0.0, 1.0); // 相机朝向参考点 18 vec3 dir0 = normalize(-camPos); // 主视线方向(-Z) 19 vec3 up = vec3(0.0, 1.0, 0.0); // 世界上方向 20 vec3 right = normalize(cross(dir0, up)); // 相机右方向 21 up = cross(right, dir0); // 重算正交上方向 22 23 // 每个像素对应的射线方向 24 vec3 rayDirection = normalize( 25 dir0 + uv.x * right + uv.y * up 26 ); 27 28 // 光线行进主循环 29 for (float i = 0.0; i < 50.0; i++) { 30 // 当前射线位置 31 vec3 hitPoint = rayOrigin + rayDepth * rayDirection; 32 33 // 火焰推远并随时间摆动 34 hitPoint.z += 5.0 + cos(t); 35 36 // 火焰随高度扭曲 37 float rot = hitPoint.y * 0.5; 38 mat2 rotMat = mat2(cos(rot), -sin(rot), sin(rot), cos(rot)); 39 hitPoint.xz *= rotMat; 40 41 // 火焰锥体形状:上窄下宽 42 hitPoint.xz /= max(hitPoint.y * 0.1 + 1.0, 0.1); 43 44 // Turbulence:多层余弦扰动模拟噪声 45 float freq = 2.0; 46 for (int it = 0; it < 5; it++) { 47 vec3 offset = cos((hitPoint.yzx - vec3(t/0.1, t, freq)) * freq); 48 hitPoint += offset / freq; 49 freq /= 0.6; 50 } 51 52 // 距离场:判断火焰边界 53 float coneRadius = length(hitPoint.xz); 54 float coneDist = abs(coneRadius + hitPoint.y * 0.3 - 0.5); 55 56 // 自适应步长 57 float stepSize = 0.01 + coneDist / 7.0; 58 rayDepth += stepSize; 59 60 // 累积颜色 61 vec3 gCol = sin(rayDepth / 3.0 + vec3(7.0, 2.0, 3.0)) + 1.1; 62 col += gCol / stepSize; 63 } 64 65 // Tone mapping 66 col = tanh(col / 2000.0); 67 fragColor = vec4(col, 1.0); 68} 69 70void main() { 71 mainImage(gl_FragColor, gl_FragCoord.xy); 72} 73
三、逐段原理解析
1. UV 转换
公式:uv = (2.0*fragCoord - iResolution.xy) / iResolution.y
作用:把屏幕像素映射到中心为 (0,0),范围约 [-1,1] 的坐标系。
举例:分辨率 800×600,中点 (400,300) → uv = (0,0)。
2. 射线方向
通过 dir0(相机主方向)、up、right 三个正交向量,把 uv 映射到 3D 空间。
直观理解:每个像素就是你眼睛发出的一条射线。
3. Raymarch 循环
光线前进 50 步,每一步:
- 计算 hitPoint = rayOrigin + rayDepth * rayDirection
- 加上时间和 cos(t) 让火焰晃动
- 用旋转矩阵让火焰扭动
- 用锥体缩放实现上窄下宽
- 加 turbulence 扰动 → 看起来不规则
- 算到火焰边界的“距离” → 控制步长
- 按颜色函数累积颜色
类比:就像走进一个雾气团,每一步闻一口烟雾浓度,最后累加。
4. Turbulence(扰动)
使用 5 层 cos 叠加,模拟分形噪声:
- 低频控制整体摆动
- 高频增加细节
- 层层叠加后,就像火焰的跳动纹理
5. 颜色生成
gCol = sin(rayDepth/3.0 + vec3(7,2,3)) + 1.1
- 三个通道不同相位 → 颜色过渡(红→橙→黄)
+1.1提升基底亮度- 除以步长 → 靠近边界亮度增强
6. Tone mapping
tanh(col/2000.0) 把颜色压到合理范围,避免过曝。
四、Vue + Three.js 集成示例
1<script setup> 2import * as THREE from "three"; 3import { onMounted, onBeforeUnmount, ref } from "vue"; 4 5const container = ref(null); 6let renderer, scene, camera, mesh, material, rafId; 7 8onMounted(() => { 9 scene = new THREE.Scene(); 10 camera = new THREE.OrthographicCamera(-1,1,1,-1,0,1); 11 renderer = new THREE.WebGLRenderer(); 12 renderer.setSize(window.innerWidth, window.innerHeight); 13 container.value.appendChild(renderer.domElement); 14 15 const uniforms = { 16 iTime: { value: 0 }, 17 iResolution: { value: new THREE.Vector3(window.innerWidth, window.innerHeight, 1) } 18 }; 19 20 material = new THREE.ShaderMaterial({ 21 uniforms, 22 fragmentShader: fragmentSource, // 上面着色器代码 23 }); 24 25 mesh = new THREE.Mesh(new THREE.PlaneGeometry(2,2), material); 26 scene.add(mesh); 27 28 function animate(time){ 29 uniforms.iTime.value = time * 0.001; 30 renderer.render(scene, camera); 31 rafId = requestAnimationFrame(animate); 32 } 33 animate(); 34 35 window.addEventListener("resize", () => { 36 renderer.setSize(window.innerWidth, window.innerHeight); 37 uniforms.iResolution.value.set(window.innerWidth, window.innerHeight, 1); 38 }); 39}); 40 41onBeforeUnmount(() => cancelAnimationFrame(rafId)); 42</script> 43 44<template> 45 <div ref="container" style="width:100vw;height:100vh;"></div> 46</template> 47
五、可调参数(实验一下!)
- Raymarch 循环次数:20 vs 80 → 精度 vs 性能
- Turbulence 层数:3 vs 7 → 平滑 vs 细节
hitPoint.z += 5.0→ 火焰远近coneDist / 7.0→ 控制步长大小tanh(col/2000.0)→ 调整亮度范围
六、调试与常见问题
- 黑屏 → 着色器语法错误,看控制台
- 帧率低 → 减少循环步数,降分辨率
- 颜色过曝 → 调整 tone mapping 参数
- 没动画 → 检查 iTime 是否在更新
七、总结
火焰效果其实就是:
- 定义锥体几何(火焰形状)
- 用噪声扰动模拟跳动
- 用光线行进采样累积亮度
- 用颜色公式生成火焰色彩
- 最后映射成屏幕像素
一句话:每个像素是一条射线,在锥体火焰里采样累积颜色,得到动态火焰。
参考 欧阳大盆裁 文章而成
👉 如果你觉得还难,可以做一个“简化版练习”:先只保留锥体形状(不加 turbulence、不加颜色函数),跑起来后再一步步加扰动、颜色、tone mapping,这样更直观。
《深入理解 3D 火焰着色器:从 Shadertoy 到 Three.js 的完整实现解析》 是转载文章,点击查看原文。