下载 App

NanoVG 动态火焰效果 — 径向渐变叠加法

精华05/0270 浏览开发交流
在给主界面制作火炉效果的时候,发现初始ai给到的效果并不理想,而提示让生成火苗就是椭圆形的阴影在飘来飘去。最后让火粒子是几百颗小圆时,感觉也差那么点。
如下图:
而迭代后,ai产出的效果如下:
--------- 以下是方法:
## 完整实现
### 函数签名
```lua
--- 在指定区域绘制动态火焰
--- @param vg    NanoVG context
--- @param cx    火焰中心 x
--- @param cy    火焰中心 y
--- @param w     区域宽度(用于计算火焰尺寸比例)
--- @param h     区域高度
--- @param t     当前时间(秒,持续递增,驱动动画)
local function drawFireEffect(vg, cx, cy, w, h, t)
```
### 第一层:全局闪烁因子
火焰不是均匀燃烧的,需要一个全局的明暗波动。用 3 个不同频率的 sin 波叠加,避免单一频率的机械感:
```lua
local R = math.max(w, h) * 0.6  -- 基准半径
local f1 = math.sin(t * 3.2)
local f2 = math.sin(t * 1.7 + 0.5)
local f3 = math.sin(t * 5.1 + 1.2)
local flicker = 0.65 + 0.22 * f1 * f2 + 0.13 * f3
-- flicker 在 ~0.3 到 ~1.0 之间波动
```
这里的关键是 **f1 * f2 的乘积** —— 两个 sin 相乘会产生拍频效果,比简单相加更像真实的火焰闪烁。
### 第二层:环境热辐射光晕
火焰周围应该有柔和的暖色光晕,用一个大椭圆径向渐变实现:
```lua
local envP = nvgRadialGradient(vg,
    cx, cy, R * 0.05, R * 2.0,
    nvgRGBAf(1.0, 0.4, 0.08, 0.18 * flicker),  -- 中心:暖橙,半透明
    nvgRGBAf(0.6, 0.1, 0.0, 0.0)               -- 边缘:全透明
)
nvgBeginPath(vg)
nvgEllipse(vg, cx, cy, R * 2.0, R * 2.5)  -- 竖向略长
nvgFillPaint(vg, envP)
nvgFill(vg)
```
再加一个小范围的热芯光晕,让火焰根部更通透:
```lua
local coreGlowP = nvgRadialGradient(vg,
    cx, cy + R * 0.2, R * 0.02, R * 0.7,
    nvgRGBAf(1.0, 0.7, 0.2, 0.3 * flicker),
    nvgRGBAf(1.0, 0.3, 0.0, 0.0)
)
nvgBeginPath(vg)
nvgCircle(vg, cx, cy + R * 0.2, R * 0.7)
nvgFillPaint(vg, coreGlowP)
nvgFill(vg)
```
### 第三层:火焰主体(核心!)
这是最关键的部分 —— 140 个径向渐变圆的叠加。
#### 3.1 伪随机分布:黄金比例 Hash
不用 `math.random()`(每帧结果不同,火焰会抖),而是用**黄金比例 Hash** 为每个粒子生成固定的伪随机参数:
```lua
local h1 = (i * 0.6180339887) % 1.0          -- 第一组随机值
local h2 = ((i * 7 + 13) * 0.6180339887) % 1.0  -- 第二组(不同种子)
local h3 = ((i * 17 + 31) * 0.6180339887) % 1.0 -- 第三组
```
`0.6180339887` 是黄金比例的小数部分 `(sqrt(5)-1)/2`。乘以不同的整数系数再取模,就得到了多组均匀分布但互不相关的伪随机序列。
#### 3.2 生命周期动画
每个粒子有一个 `p` 值(0=底部刚生成,1=顶部消亡),通过时间驱动循环:
```lua
local speed  = 0.4 + h1 * 0.7    -- 每个粒子速度不同
local period = 2.0 + h2 * 2.0    -- 每个粒子周期不同
local p = ((t * speed + h3 * period) % period) / period
```
`speed` 和 `period` 都带随机变化,这样 140 个粒子的运动周期互不相同,避免了"齐步走"的机械感。
#### 3.3 火焰轮廓:底宽顶窄
火焰的外形用一个指数衰减的包络线控制:
```lua
-- y 位置:从底部线性上升
local py = baseY - p * FIRE_H
-- x 方向的允许范围随高度衰减(指数 1.2 → 略快于线性)
local envelopeW = FIRE_HALF_W * (1.0 - p) ^ 1.2
-- 在包络线内随机偏移 + 时间驱动的摆动
local xOff = (h1 - 0.5) * 2.0 * envelopeW
local sway  = math.sin(t * (2.5 + h2 * 2.5) + i * 0.7) * envelopeW * 0.4
local sway2 = math.sin(t * (4.0 + h1 * 3.0) + i * 1.3) * envelopeW * 0.15
local px = cx + xOff + sway + sway2
```
双层 sway(主摆动 + 高频微颤)让火焰的晃动更有层次。
#### 3.4 粒子大小
底部大(叠加后产生高亮核心),顶部小(火星飘散感):
```lua
local sizeBase = R * (0.22 + h2 * 0.16)  -- 基础大小带随机
local size = sizeBase * (1.0 - p * 0.65)  -- 随高度缩小
    * (0.85 + 0.15 * math.sin(t * 5.0 + i))  -- 微弱的大小脉动
```
#### 3.5 五段色带
火焰颜色随高度(`p` 值)分 5 段渐变:
```lua
if p < 0.12 then
    -- 核心:近白色      白(255,247,204)
    r, g, b = 1.0, 0.97, 0.8
    a = 0.35 * flicker
elseif p < 0.3 then
    -- 内焰:亮黄        黄(255,217,77)
    r, g, b = 1.0, 0.85, 0.3
    a = 0.30 * flicker
elseif p < 0.55 then
    -- 中焰:橙色        橙(255,140,15)
    r, g, b = 1.0, 0.55, 0.06
    a = 0.25 * flicker
elseif p < 0.78 then
    -- 外焰:暗橙→红     红橙(242,77,8)
    r, g, b = 0.95, 0.3, 0.03
    a = 0.18 * flicker
else
    -- 烟尾:暗红         暗红(179,31,3)
    r, g, b = 0.7, 0.12, 0.01
    a = 0.10 * (1.0 - (p - 0.78) / 0.22) * flicker
end
```
每段的 alpha 也不同 —— 核心最高(0.35),叠加后趋近不透明;烟尾最低(0.10),快速消失。
#### 3.6 绘制:径向渐变圆
这是整个效果的灵魂:
```lua
local paint = nvgRadialGradient(vg,
    px, py,           -- 圆心
    size * 0.05,      -- 内圆半径(几乎为 0,亮色集中在中心点)
    size,             -- 外圆半径
    nvgRGBAf(r, g, b, a),              -- 中心色:亮,半透明
    nvgRGBAf(r*0.5, g*0.25, 0.0, 0.0) -- 边缘色:暗,全透明
)
nvgBeginPath(vg)
nvgCircle(vg, px, py, size)
nvgFillPaint(vg, paint)
nvgFill(vg)
```
**为什么 `内圆半径 = size * 0.05` 而不是 0?** 如果内圆是 0,渐变会过于集中在一个像素点上。留一点点内圆面积,让中心有一个小的实色区域,叠加后核心更饱满。
**为什么边缘色不是纯黑透明 `(0,0,0,0)` 而是 `(r*0.5, g*0.25, 0)`?** 纯黑透明在半透明叠加时会产生灰色过渡带。用偏暖的暗色作为边缘色,过渡更自然。
### 第四层:飘散火星
火焰顶部应该有小火星飘散。40 个极小的渐变圆,速度快、alpha 衰减快:
```lua
local SPARKS = 40
for i = 1, SPARKS do
    -- ... 伪随机 + 生命周期(同上)
    -- 从火焰中上部射出,快速上升
    local sy = (baseY - FIRE_H * 0.35) - p * FIRE_H * 1.0
    -- 横向漂移随高度增大
    local drift = math.sin(t * (3.5 + h2 * 2.5) + i * 1.3) * R * 0.5 * p
    local sx = cx + (h1 - 0.5) * R * 0.4 + drift
    -- 很小的尺寸 + 快速消失的 alpha
    local sparkSize = R * (0.04 + h2 * 0.025) * (1.0 - p * 0.4)
    local sa = (0.7 + h1 * 0.3) * (1.0 - p)^2 * flicker
    -- 同样用径向渐变绘制
    local sp = nvgRadialGradient(vg, sx, sy, 0, sparkSize,
        nvgRGBAf(1.0, 0.75, 0.12, sa),
        nvgRGBAf(1.0, 0.2, 0, 0)
    )
    nvgBeginPath(vg)
    nvgCircle(vg, sx, sy, sparkSize)
    nvgFillPaint(vg, sp)
    nvgFill(vg)
end
```
火星的 alpha 用 `(1-p)^2` 衰减(平方),比线性衰减更快 —— 刚射出时亮,很快就暗下去消失。
---
## 调参指南
| 参数 | 作用 | 调大 | 调小 |
|------|------|------|------|
| `COUNT` | 粒子总数 | 火焰更浓密、核心更亮 | 火焰稀疏、若隐若现 |
| `FIRE_H` | 火焰高度 | 火柱更高 | 矮火 |
| `FIRE_HALF_W` | 底部半宽 | 火焰根部更宽(篝火感) | 窄火柱(蜡烛感) |
| `sizeBase` | 粒子基础半径 | 每个光球更大、更柔和 | 颗粒感更强 |
| 各段 `a` 值 | 透明度 | 更亮更实 | 更虚更透 |
| `speed` | 粒子上升速度 | 更猛烈的涌动 | 更缓慢的燃烧 |
| `sway` 幅度 | 横向摆动 | 更狂暴 | 更稳定 |
| 轮廓指数 `^1.2` | 收窄速度 | 更尖的火焰 | 更圆的火焰 |
| `SPARKS` | 火星数量 | 更多飘散余火 | 更干净的火焰 |
| `flicker` 基准 | 全局亮度下限 | 闪烁幅度更小 | 闪烁更剧烈 |
### 几种预设风格
**蜡烛火焰**(小而稳定):
```lua
COUNT = 40, FIRE_H = R * 1.5, FIRE_HALF_W = R * 0.25
sway 幅度 * 0.15, SPARKS = 5
```
**篝火**(宽而猛烈):
```lua
COUNT = 140, FIRE_H = R * 3.0, FIRE_HALF_W = R * 0.7
sway 幅度 * 0.4, SPARKS = 40
```
**地狱烈焰**(极端):
```lua
COUNT = 250, FIRE_H = R * 4.0, FIRE_HALF_W = R * 0.9
所有 a 值 * 1.5, speed 范围 0.6~1.5, SPARKS = 80
```
---
## 踩过的坑
### 1. 纯色填充 vs 径向渐变
```lua
-- 错误:纯色圆 → 像彩色弹珠
nvgFillColor(vg, nvgRGBAf(1, 0.5, 0, 0.3))
-- 正确:径向渐变圆 → 有光晕的发光体
nvgFillPaint(vg, nvgRadialGradient(vg, x, y, 0, r, bright, transparent))
```
这是最关键的一步。没有径向渐变,再多粒子也只是一堆色块。
### 2. math.random() vs 黄金比例 Hash
```lua
-- 错误:每帧随机 → 火焰像在抖
local x = cx + math.random() * w
-- 正确:确定性伪随机 → 每帧位置平滑变化
local h = (i * 0.6180339887) % 1.0
-- 动画由 sin(t * speed + phase) 驱动,而非随机
```
粒子的"随机性"应该是**空间分布的随机**,不是**时间上的随机**。黄金比例 Hash 给每个粒子一个固定身份,动画由 `sin(time)` 平滑驱动。
### 3. 单频闪烁 vs 多频叠加
```lua
-- 错误:单频 → 像呼吸灯
local flicker = 0.5 + 0.5 * math.sin(t * 2.0)
-- 正确:多频乘积 → 不规则的自然闪烁
local flicker = 0.65
    + 0.22 * math.sin(t * 3.2) * math.sin(t * 1.7 + 0.5)
    + 0.13 * math.sin(t * 5.1 + 1.2)
```
两个 sin 相乘产生拍频,加上第三个 sin 打破周期性。
### 4. 火苗形状粒子的失败
一开始试过用贝塞尔曲线画火苗形状的粒子(底宽顶尖),但效果很不自然 —— 每个火苗都有明确的轮廓边界,看起来像一堆叶子而不是火。
正确的思路是:**不要让单个粒子看起来像火苗,让大量模糊光球的叠加效果看起来像火焰。** 单个粒子应该是看不清楚的(径向渐变到透明),只有群体叠加才产生视觉。
---
## 性能
- 140 个火焰粒子 + 40 个火星 = 每帧 ~180 次 `nvgRadialGradient` + `nvgCircle` + `nvgFill`
- NanoVG 底层走 GPU,这个量级基本无压力
- 没有用到纹理、没有创建/销毁对象、没有内存分配,全是数学计算 + 绘图调用
---
猜你想搜
taptap 制造nanovg动态火焰
别再烧积分了!从每次 2000 到每次 50 的血泪经验截图
别再烧积分了!从每次 2000 到每次 50 的血泪经验
适用场景:所有在用 TapTap Code 开发游戏的小白 难度:★☆☆☆☆ 背景最近群里总有人"炫耀"一次操作花了几千积分,说实话这不是值得高兴的事——这是在告诉所有人你的开发方式有根本性问题。我也走过这条弯路,所以来说说怎么改。 积分消耗的本质是什么积分 = AI 处理的 token 数量 = 你发给 AI 的内容 + AI 回给你的内容所以烧积分的本质只有两种: 你让 AI 做了大量无效/重
精华
82 赞
23 回复
如何提升你游戏中的UI美术水平截图
如何提升你游戏中的UI美术水平
一、请为你的游戏挑选一款合适的字体 很多开发者没有注意到其实UI界面中最重要的元素是字体,一款合适的字体能给整个游戏的界面感官带来蜕变。 请看案例 那么如何挑选适合自己的字体呢? 我总结出了一些小经验可供参考: 黑体:通用 宋体:古风、武侠、修仙 楷体:古风、武侠、修仙 圆体:卡通、动漫、Q版 科技:科幻、未来、电子 顺手分享一个免费可商用字体网站: 当然 如果你不知道自己的游戏适合什么风格
精华
126 赞
44 回复
开发日志(day2)坚持去做就是好的开始截图
开发日志(day2)坚持去做就是好的开始
今天主要是修复了一下昨天遇到的很多问题,闪帧;装备的等级没有随怪物等级参考;再次进入地下城,怪物状态竟然显示(已倒下);不会自动更换目标,导致会反复鞭尸;等很多的问题,不过通过昨天的经验,有效的向ai提出问题并且发送截图发给ai调试,也是把这些问题成功排除,甚至还有多余的free额度来生成新的内容。 新增的内容有返回城镇;返回主界面;中途退出地下城保留进入前的状态;完成地下城的面板结算;当然最有感
1 赞
1 回复
2D 等轴测视角:完整理论截图
2D 等轴测视角:完整理论
2D 等轴测视角:完整理论 一、视角的本质 什么是等轴测 人站在高处斜着往下看地面,地面上的方格子不再是正方形,而变成了菱形。这就是等轴测的视觉本质。 俯视(正上方看下去): 等轴测(斜上方看下去): ┌──┬──┬──┐ ◇ ├──┼──┼──┤ ◇ ◇ ├──┼──┼──┤ ◇ ◇ ◇ └──┴──┴──┘ ◇ ◇ ◇ 正方形网格经过旋转 + 压缩,变成了菱形网格。 为什么是 2:1
6 赞
01:29
官方阅,申请积分计划2,希望官方继续支持我完成这款游戏,截图
官方阅,申请积分计划2,希望官方继续支持我完成这款游戏,
视频内容只是一部分, 本游戏耗时3个月,目前完成进度百分之80, 是生存游戏,进入修仙界,走剧情找到宗门加入, 这时游戏才是真正开始, 提升实力境界后就可以变强,获取功法法术法宝, @视频内是游戏里挑战妖族入侵一种主流玩法,游戏就是围绕着这个来进行, 还有很多地方可以战斗,获得资源修炼变强,比如去藏书阁学习功法, 去法宝楼抽法宝,每一个宗门数十个法宝抽取 去修仙界探索,获得各自的资源,可能遇见妖兽
1 赞
3 回复
永恒虚空持续开发日记1——关于游戏中的新名词:场频截图
永恒虚空持续开发日记1——关于游戏中的新名词:场频
写在前边:首先感谢TapTap制造版主大大@汪崽 对于作品潜力的肯定,给与了我积分计划二的权限[表情_不好意思]。这边也在加紧开发改进各种功能中,期望尽快给大家带来成熟的版本。[表情_猫咪举手] 大家好,我是永恒虚空的制作人黑猫不详,大家叫我黑猫就好。从今天开始我将以后置开发日记的形式给大家分享我在TapTap制造中制作的游戏永恒虚空的开发过程,新人希望大家多多关照! 今天先给大家介绍我作品中最重
投票
3 赞
【UI教程】如何将已有的效果图应用为实际的UI界面截图
【UI教程】如何将已有的效果图应用为实际的UI界面
在经过了一些折腾和比较折磨的尝试后,我总算是摸清楚了嗒啦啦怎么把UI效果图转换为实际游戏中的UI组件,今天就给大家带来这部分的经验分享。 tips:1.本篇教程需要各位掌握最基本的ps使用,具体的操作部分我会大致讲解,但仍需各位自行学习ps的基本操作,部分可以由ai代劳的环节我会注明。 2.本篇教程需要在以我前两篇教程作为前置环节,请在阅读这两篇前置教程后再进行本篇教程内容的学习 在按照前
12 赞
5 回复
关于制作出质量很高的横版战斗角色的方法截图
关于制作出质量很高的横版战斗角色的方法
其实全篇就一句话,角色为啥做起来这么好看,因为我一帧一帧K的。 在做角色之前,我们需要一个角色的设计稿,这个东西很简单,你把人设写好剩下的交给AI就行了,所有图片,序列帧都可以让AI绘制和处理,学会绘图是你要制作高质量游戏的第一课。 这里拿我在做的角色举例,他的参考图是这样: 当你拥有了一个角色以后,就要想办法让她动起来了。 那么如何让她动起来,答案是序列帧。 在此之前你需要知道,最容易出现一个误
42 赞
12 回复
00:54
等距场景编辑器——自行编辑2D瓦片场景地图截图
等距场景编辑器——自行编辑2D瓦片场景地图
一个可嵌入任何 UrhoX 项目的完整等距瓦片地图编辑器,适用于 RPG、策略、模拟经营等类型游戏的关卡设计。 SKILL审核已通过,各位创作者可以根据自己的需要对工具进行调整修改 声明:该工具仅作为场景编辑以及序列化,交互等功能需要用户自行在项目中定义 后续会根据实际情况补充rule tile相关逻辑,实现tile map编辑自由 目录 1. 界面总览 2. 六种编辑工具 3. 双视图模式 4.
精华
21 赞
11 回复
游戏自荐《少女终焉契约》截图
游戏自荐《少女终焉契约》
之前玩放置类型的rpg游戏的时候我就在想,现在游戏的操作有的太过于繁琐了,有没有一款休闲一点的,想上线就上线,离线的时候角色能自动行动的游戏呢?感谢TapTap制造,我按照我的想法开发出了现在这款游戏。 一、故事背景和UI 游戏的故事背景是传统的异世界穿越故事,玩家被女神召唤到异世界,随后组建工会,招募美少女,打怪升级探索世界。所以我选择了更加二次元萌系画风的UI风格,主页为各个主要的功能,点开来
8 赞
3 回复