下载 App
10/16案例分享
【案例分享】改了八版都不对:nvgRadialGradient 的 inner/outer 参数根本不生效
一句话结论
本引擎的 nvgRadialGradient 会忽略你传进去的 inner / outer 参数,径向渐变的边界固定在引擎自己的默认值上(约 0.35 × min(W,H))。你改那两个参数,画面不会有任何变化 —— 而且不会有任何报错。
现象
我想做「动量驱动的氛围灯」:中心干净、越往外越有色,半径随速度变大。
写法:
local pt = nvgRadialGradient(vg, cx, cy, inner, outer,
nvgRGBA(255,200,90, 0), -- 中心:全透明
nvgRGBA(255,200,90,60)) -- 边缘:半透明
nvgFillPaint(vg, pt)
nvgFill(vg)
其中 inner = 0.70 × hd、outer = 1.00 × hd(hd = 半对角线,随动量变化)。
结果:不管怎么调,永远是一块「中心是背景色、外圈是实色」的硬边圆盘,半径看着像固定的,跟动量完全无关。前七版都在调那两个参数和颜色,全都没用。
怎么确认(关键是别靠眼睛)
用径向亮度剖面:从中心向外按半径采样,取每个半径环上像素亮度的中位数。
# 伪代码:沿半径采样中位亮度
for r in range(0, 720, 20):
px = sample_circle(img, cx, cy, r) # 该半径环上的像素
print(r, median_brightness(px))
两张不同动量的截图,剖面完全重合:
半径 0 ~ 240 中位亮度 15 <- 恰好是背景色 nvgRGBA(11,15,26) 的亮度
半径 260 ~ 720 中位亮度 63/90 <- 氛围灯色
边界固定在半径 ≈ 250,而我代码里传的 inner 换算过去是 514。
250 ≈ 0.35 × min(W,H) = 0.35 × 720 = 252 —— 这正是引擎自己的默认值。
所以传进去的两个参数被丢掉了。
修法:手绘描边同心圆环
半径完全由代码控制:
local RINGS = 40
for i = 1, RINGS do
local t = i / RINGS -- 0 .. 1
local r = maxR * t
local a = math.floor(alpha_max * t * t) -- alpha 按半径平方递增
nvgBeginPath(vg)
nvgCircle(vg, cx, cy, r)
nvgStrokeWidth(vg, maxR / RINGS + 1) -- 注意:描边,不是填充
nvgStrokeColor(vg, nvgRGBA(r_, g_, b_, a))
nvgStroke(vg)
end
为什么必须用「描边环」而不是「填充同心圆」
填充圆会累加 alpha。N 个同心填充圆叠起来,中心点被覆盖 N 次,中心最亮,与「中心干净、边缘有色」正好相反。这个坑我第一版就踩了。
描边环每个半径只描一次,alpha 才是「你写多少就是多少」。
代价
40 个描边环在 1080p 下实测无感。想更省可以降到 20 环 + 加大 stroke width,但环数太少会出现可见色带(banding)。
什么场景适合用这套手绘环
适合的:
- 半径需要由代码精确控制(跟随速度、血量、进度变化)
- 需要「中心干净、边缘有色」这种反向着色
- 层数固定、形状简单(同心圆)
不适合的:
任意形状的渐变:描边环只能画圆,要等引擎级 shader
需要真实高斯衰减:环状叠加是离散逼近,边界会有台阶感,该用后处理 blur
环数需求很大(几百环):draw call 线性增长,改用 shader
只是画个普通渐变圆、半径不需要变:引擎默认值本来就够用,直接用 nvgRadialGradient
关键判断:nvgRadialGradient 的参数被忽略这件事,只有在你需要控制半径的时候才是问题。如果你的渐变半径本来就是固定的,那用引擎自带函数就行,别绕这一圈。
另一个边界:这套方案是从外向内由亮到暗的。如果你的需求是「中心亮、边缘暗」(更常见的发光效果),那用 nvgRadialGradient 的默认行为反而更省事。
可迁移的经验
看到「改了参数画面不动」,不要继续调参数 —— 先验参数有没有被消费。
径向亮度剖面是最省事的验法:它把「我以为我改了」变成一条数轴上的读数。这个验法不只对渐变色有用,任何「按距离衰减」的效果(光照、雾、描边衰减)都可以用它来判「公式生效了没」。