深入理解 3D 火焰着色器:从 Shadertoy 到 Three.js 的完整实现解析

作者:excel日期:2025/10/4

本文由 AI 生成,结合 GLSL 原理与 Three.js 实践,旨在帮助初学者逐行理解代码,而不是仅仅“照抄能跑”。我会用直观类比、数值例子、代码注释来拆解整个火焰效果。

示例地址


一、前言:从 Shadertoy 到 Three.js

Shadertoy 上有很多绚丽的着色器,但它们常常让新手望而生畏:几十行数学公式,cos/sin 嵌套,光线行进(raymarching)循环一堆看不懂的变量。

其实这些代码是有逻辑脉络的:

  1. 定义相机 → 每个像素对应一条射线
  2. 沿射线逐步前进(raymarching)
  3. 在每个点做几何变换 + 噪声扰动 → 得到火焰体积感
  4. 根据深度和噪声算颜色 → 累积
  5. 最后做 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(相机主方向)、upright 三个正交向量,把 uv 映射到 3D 空间。
直观理解:每个像素就是你眼睛发出的一条射线。


3. Raymarch 循环

光线前进 50 步,每一步:

  1. 计算 hitPoint = rayOrigin + rayDepth * rayDirection
  2. 加上时间和 cos(t) 让火焰晃动
  3. 用旋转矩阵让火焰扭动
  4. 用锥体缩放实现上窄下宽
  5. 加 turbulence 扰动 → 看起来不规则
  6. 算到火焰边界的“距离” → 控制步长
  7. 按颜色函数累积颜色

类比:就像走进一个雾气团,每一步闻一口烟雾浓度,最后累加。


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 是否在更新

七、总结

火焰效果其实就是:

  1. 定义锥体几何(火焰形状)
  2. 用噪声扰动模拟跳动
  3. 用光线行进采样累积亮度
  4. 用颜色公式生成火焰色彩
  5. 最后映射成屏幕像素

一句话:每个像素是一条射线,在锥体火焰里采样累积颜色,得到动态火焰。

参考 欧阳大盆裁 文章而成


👉 如果你觉得还难,可以做一个“简化版练习”:先只保留锥体形状(不加 turbulence、不加颜色函数),跑起来后再一步步加扰动、颜色、tone mapping,这样更直观。


深入理解 3D 火焰着色器:从 Shadertoy 到 Three.js 的完整实现解析》 是转载文章,点击查看原文


相关推荐


Vue3和element plus在el-table中使用el-tree-select遇到的change事件坑
fxshy2025/10/3

1. 在el-tree-select中change事件使用 如果直接传递row, 拿到的不是最新的row的数据, 使用$index结合tableData来拿row <el-table-column prop="directory" label="所属目录树"> <template #default="scope"> <el-tree-select v-model="scope.row.areaTreeNodeId" :data="dir


贪心算法 | 每周8题(一)
小邓儿◑.◑2025/10/2

目录 0.0引言 1.0贪心算法介绍 1.1什么是贪心算法 2.0例题详解(来源力扣) 2.1 柠檬水找零 2.2将数组和减半的最少操作次数 2.3 最大数 2.4 K 次取反后最大化的数组和 2.5最长递增子序列 2.6摆动序列 2.7递增的三元子序列 2.8最长连续递增序列 3.0小结 0.0引言 从本周起,小编儿将带大家一起进入算法(^▽^)的学习当中。废话不多说,咱们从贪心走起儿😄😄😄 1.0贪心算法介绍 1.1什么是贪心算法 贪心算法(


风力发电机输出功率模型综述
less is more_09302025/10/2

摘要:在设计最优系统时,准确的建模是十分重要的。影响风机性能的主要因素是风速分布、轮毂高度以及所选风力机的功率输出曲线,故在进行风机建模时必须适当考虑这些因素。本文对风机输出功率建模的各种方法进行了详细的介绍。基于风能基本方程的建模方法使用时较繁琐,而且无法正确地复现实际风机的特性。基于假定功率曲线的建模方法虽然使用较为简单,但也缺乏准确性,不过当年平均风速较高时,表现出较为满意的响应。采用风机实际功率曲线建立特征方程的建模方法,当风机功率曲线光滑时,最小二乘法和三次样条插值法均能得到准确的结果


分布式光纤声波振动与AI的深度融合:开启智慧感知新时代
无锡布里渊10/2/2025

未来,随着AI算法的持续创新、算力的提升以及多源数据融合技术的发展,分布式光纤传感与AI的结合必将在更广阔的领域绽放异彩,为构建更安全、高效、智能的未来社会贡献核心力量。人工智能(AI),特别是机器学习与深度学习算法的迅猛发展,为解决这些难题提供了强大的工具,二者的深度融合正催生着智慧感知的新范式,前景广阔,优势显著。AI通过对大量标注数据的学习,能够精准识别不同物理事件(如不同类型的入侵行为、设备异常振动、管道泄漏等)的细微模式差异,大幅提高事件分类和识别的准确率,降低误报率和漏报率。


Word和WPS文字中的题注没有更新?全选按F9刷新
揭老师高效办公9/30/2025

在Word文档和WPS文字中有多个图片或表格等对象时,一个好的习惯是给这些图片和表格添加标题并编号,这些标题不要手动编号,要使用程序自带的“题注”功能,以实现自动更新序号。如果插入、删除或调整图片、表格后,题注的编号没有更新,可全选整个文档,按F9刷新实现自动编号。


电子电气架构 --- 汽车智能座舱发展供应链痛点
汽车电子实验室9/30/2025

摘要 智能座舱作为汽车智能化核心载体,正经历从机械控制到数字生态的转型,硬件架构向多屏联动与感知融合升级,软件层面通过域控制器与操作系统实现功能整合。然而,技术跃迁带来成本激增,单车价值量从千元级跃升至万元级,迫使车企采取硬件预埋+软件订阅、供应链整合、场景化精简等策略平衡成本与体验。当前市场面临车机芯片供应单一问题,高通芯片凭借高性能占据主导地位。未来竞争焦点将从硬件参数转向体验深度,车企需在性价比与个性化体验间找到平衡,通过场景化设计满足差异化需求,以&quot;体验经济&quot;赢得市场。


在高并发场景下,如何平衡QPS和TPS的监控资源消耗?
IT橘子皮2025/10/5

在高并发场景下,平衡QPS(每秒查询数)和TPS(每秒事务数)的监控资源消耗,需要从监控策略、工具选型、资源分配及优化手段等多维度进行综合设计。以下是具体方案: 一、分层监控策略 ​轻量级采样与聚合​ ​高频QPS监控​:对简单请求(如API调用)采用低精度采样(如每10秒聚合一次),通过Prometheus的rate()函数计算QPS趋势,减少实时采集压力。 ​低频TPS监控​:对复杂事务(如支付流程)采用事件驱动记录,仅记录事务开始和结束时间戳,通过日志分析或消息队列(如Kafka)


从0-1建设数据仓库
DashingGuy2025/10/7

基于onedata,纯个人理解,不完善的会慢慢补充 整体流程 业务调研数据调研划分数据域构建总线矩阵数仓模型设计数仓模型开发数仓模型质量保障以及运维 一、业务调研 业务调研有几个内容要做: 确定目标和范围、收集业务需求、梳理业务流程和数据流向、输出物 1.1 确定目标和范围 明确业务目标:为什么建设数仓?数仓要解决什么问题?要实现哪些业务目标?例如提升数据分析能力、提高经营效率、支持精准营销、预测风险等。 确定数仓范围:数仓要包含哪些业务领域?哪些数据需要纳入数仓?需要支持哪些业务场景?例


如何使用 INFINI Gateway 对比 ES 索引数据
极限实验室2025/10/8

上一篇我们通过 极限网关(INFINI Gateway) 进行了索引数据迁移,对索引迁移结果进行了初步且直观的校验,既对比索引的文档数是否一致。今天介绍个实实在在的数据比对方法,通过网关对比索引文档的内容在两个集群是否一致,此方法适用于 Elasticsearch、Easysearch、Opensearch。话不多说,就拿上次迁移的两个索引开整。 测试环境 软件版本Easysearch1.12.0Elasticsearch7.17.29INFINI Ga


Python 的内建函数
hubenchang05152025/10/9

#Python 的内建函数 此文档创建于 Python 3.13,可能未及时更新,请以 Python 官方文档 为准。 虽然称为内建函数,但部分 API 并不是函数,例如 object 是类。 函数名详情简介__import__查看导入模块abs查看计算绝对值aiter查看获取异步可迭代对象的迭代器all查看判断可迭代对象内容是否全部为真值anext查看获取异步迭代器的下一数据项any查看判断可迭代对象内容是否存在真值ascii查看转换为字符串,非 ASCII 字符将被转义bin查看将一

首页编辑器站点地图

Copyright © 2025 聚合阅读

License: CC BY-SA 4.0