本文由 AI 生成,结合 GLSL 原理与 Three.js 实践,旨在帮助初学者逐行理解代码,而不是仅仅"照抄能跑"。我会用直观类比、数值例子、代码注释来拆解整个火焰效果。
一、前言:从 Shadertoy 到 Three.js
Shadertoy 上有很多绚丽的着色器,但它们常常让新手望而生畏:几十行数学公式,cos/sin 嵌套,光线行进(raymarching)循环一堆看不懂的变量。
其实这些代码是有逻辑脉络的:
- 定义相机 → 每个像素对应一条射线
- 沿射线逐步前进(raymarching)
- 在每个点做几何变换 + 噪声扰动 → 得到火焰体积感
- 根据深度和噪声算颜色 → 累积
- 最后做 tone mapping → 显示在屏幕上
这篇文章会带你走完整个过程,并给出 Vue + Three.js 的完整实现。
二、完整片元着色器代码(带注释)
ini
// 火焰效果片元着色器
// 输入:时间 iTime(秒),画布分辨率 iResolution(x=宽,y=高,z备用)
uniform float iTime;
uniform vec3 iResolution;
void mainImage(out vec4 fragColor, vec2 fragCoord)
{
float t = iTime; // 动画时间
float rayDepth = 0.0; // 射线累计前进距离
vec3 col = vec3(0.0); // 颜色累积器
// 将像素坐标转为 [-1,1] 的标准化坐标
vec2 uv = (2.0 * fragCoord - iResolution.xy) / iResolution.y;
// 相机设置
vec3 rayOrigin = vec3(0.0, 0.0, 0.0); // 相机位置
vec3 camPos = vec3(0.0, 0.0, 1.0); // 相机朝向参考点
vec3 dir0 = normalize(-camPos); // 主视线方向(-Z)
vec3 up = vec3(0.0, 1.0, 0.0); // 世界上方向
vec3 right = normalize(cross(dir0, up)); // 相机右方向
up = cross(right, dir0); // 重算正交上方向
// 每个像素对应的射线方向
vec3 rayDirection = normalize(
dir0 + uv.x * right + uv.y * up
);
// 光线行进主循环
for (float i = 0.0; i < 50.0; i++) {
// 当前射线位置
vec3 hitPoint = rayOrigin + rayDepth * rayDirection;
// 火焰推远并随时间摆动
hitPoint.z += 5.0 + cos(t);
// 火焰随高度扭曲
float rot = hitPoint.y * 0.5;
mat2 rotMat = mat2(cos(rot), -sin(rot), sin(rot), cos(rot));
hitPoint.xz *= rotMat;
// 火焰锥体形状:上窄下宽
hitPoint.xz /= max(hitPoint.y * 0.1 + 1.0, 0.1);
// Turbulence:多层余弦扰动模拟噪声
float freq = 2.0;
for (int it = 0; it < 5; it++) {
vec3 offset = cos((hitPoint.yzx - vec3(t/0.1, t, freq)) * freq);
hitPoint += offset / freq;
freq /= 0.6;
}
// 距离场:判断火焰边界
float coneRadius = length(hitPoint.xz);
float coneDist = abs(coneRadius + hitPoint.y * 0.3 - 0.5);
// 自适应步长
float stepSize = 0.01 + coneDist / 7.0;
rayDepth += stepSize;
// 累积颜色
vec3 gCol = sin(rayDepth / 3.0 + vec3(7.0, 2.0, 3.0)) + 1.1;
col += gCol / stepSize;
}
// Tone mapping
col = tanh(col / 2000.0);
fragColor = vec4(col, 1.0);
}
void main() {
mainImage(gl_FragColor, gl_FragCoord.xy);
}
三、逐段原理解析
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 集成示例
ini
<script setup>
import * as THREE from "three";
import { onMounted, onBeforeUnmount, ref } from "vue";
const container = ref(null);
let renderer, scene, camera, mesh, material, rafId;
onMounted(() => {
scene = new THREE.Scene();
camera = new THREE.OrthographicCamera(-1,1,1,-1,0,1);
renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
container.value.appendChild(renderer.domElement);
const uniforms = {
iTime: { value: 0 },
iResolution: { value: new THREE.Vector3(window.innerWidth, window.innerHeight, 1) }
};
material = new THREE.ShaderMaterial({
uniforms,
fragmentShader: fragmentSource, // 上面着色器代码
});
mesh = new THREE.Mesh(new THREE.PlaneGeometry(2,2), material);
scene.add(mesh);
function animate(time){
uniforms.iTime.value = time * 0.001;
renderer.render(scene, camera);
rafId = requestAnimationFrame(animate);
}
animate();
window.addEventListener("resize", () => {
renderer.setSize(window.innerWidth, window.innerHeight);
uniforms.iResolution.value.set(window.innerWidth, window.innerHeight, 1);
});
});
onBeforeUnmount(() => cancelAnimationFrame(rafId));
</script>
<template>
<div ref="container" style="width:100vw;height:100vh;"></div>
</template>
五、可调参数(实验一下!)
- 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,这样更直观。