本文由 AI 生成,結合 GLSL 原理與 Three.js 實踐,旨在幫助初學者逐行理解代碼,而不是僅僅“照抄能跑”。我會用直觀類比、數值例子、代碼註解來拆解整個火焰效果。
Shadertoy 上有很多絢麗的著色器,但它們常常讓新手望而生畏:幾十行數學公式,cos/sin 嵌套,光線行進(raymarching)循環一堆看不懂的變數。
其實這些代碼是有邏輯脈絡的:
這篇文章會帶你走完整個過程,並給出 Vue + Three.js 的完整實現。
// 火焰效果片元著色器
// 輸入:時間 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);
}
公式:uv = (2.0*fragCoord - iResolution.xy) / iResolution.y
作用:把螢幕像素映射到中心為 (0,0),範圍約 [-1,1] 的坐標系。
舉例:分辨率 800×600,中點 (400,300) → uv = (0,0)。
通過 dir0
(相機主方向)、up
、right
三個正交向量,把 uv 映射到 3D 空間。
直觀理解:每個像素就是你眼睛發出的一條射線。
光線前進 50 步,每一步:
類比:就像走進一個霧氣團,每一步聞一口煙霧濃度,最後累加。
使用 5 層 cos 疊加,模擬分形噪聲:
gCol = sin(rayDepth/3.0 + vec3(7,2,3)) + 1.1
+1.1
提升基底亮度tanh(col/2000.0)
把顏色壓到合理範圍,避免過曝。
<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>
hitPoint.z += 5.0
→ 火焰遠近coneDist / 7.0
→ 控制步長大小tanh(col/2000.0)
→ 調整亮度範圍火焰效果其實就是:
一句話:每個像素是一條射線,在錐體火焰裡採樣累積顏色,得到動態火焰。
參考 歐陽大盆裁 文章而成
👉 如果你覺得還難,可以做一個“簡化版練習”:先只保留錐體形狀(不加 turbulence、不加顏色函數),跑起來後再一步步加擾動、顏色、tone mapping,這樣更直觀。