🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付

深入理解 3D 火焰著色器:從 Shadertoy 到 Three.js 的完整實現解析

火焰著色器

本文由 AI 生成,結合 GLSL 原理與 Three.js 實踐,旨在幫助初學者逐行理解代碼,而不是僅僅“照抄能跑”。我會用直觀類比、數值例子、代碼註解來拆解整個火焰效果。

示例地址


一、前言:從 Shadertoy 到 Three.js

Shadertoy 上有很多絢麗的著色器,但它們常常讓新手望而生畏:幾十行數學公式,cos/sin 嵌套,光線行進(raymarching)循環一堆看不懂的變數。

其實這些代碼是有邏輯脈絡的:

  1. 定義相機 → 每個像素對應一條射線
  2. 沿射線逐步前進(raymarching)
  3. 在每個點做幾何變換 + 噪聲擾動 → 得到火焰體積感
  4. 根據深度和噪聲算顏色 → 累積
  5. 最後做 tone mapping → 顯示在螢幕上

這篇文章會帶你走完整個過程,並給出 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);
}

三、逐段原理解析

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 集成示例

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

七、總結

火焰效果其實就是:

  1. 定義錐體幾何(火焰形狀)
  2. 用噪聲擾動模擬跳動
  3. 用光線行進採樣累積亮度
  4. 用顏色公式生成火焰色彩
  5. 最後映射成螢幕像素

一句話:每個像素是一條射線,在錐體火焰裡採樣累積顏色,得到動態火焰。

參考 歐陽大盆裁 文章而成

👉 如果你覺得還難,可以做一個“簡化版練習”:先只保留錐體形狀(不加 turbulence、不加顏色函數),跑起來後再一步步加擾動、顏色、tone mapping,這樣更直觀。


原文出處:https://juejin.cn/post/7556384277605466147


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝20   💬9   ❤️5
708
🥈
我愛JS
📝4   💬14   ❤️7
254
🥉
御魂
💬1  
3
#5
2
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付