文本渐变 Shader 使用说明

简介

TextGradientShader 是一个用于 2D 精灵的线性渐变着色器。支持可调节的渐变角度、双颜色混合和渐变强度控制。适用于文本标签、按钮背景、图片渐变等场景。


效果演示

在这里插入图片描述


参数说明

渐变参数

参数名类型默认值说明
u_GradientTopColorVector4[1, 0.5, 0, 1]渐变顶部颜色 (RGBA)
u_GradientBottomColorVector4[0.5, 0, 1, 1]渐变底部颜色 (RGBA)
u_GradientAngleFloat90渐变角度 (度),0=向右,90=向下,180=向左,270=向上
u_GradientIntensityFloat1渐变强度 (0=原始颜色,1=完全渐变)

快速开始

方式一:在IDE中将材质拖到Image属性Material上

// 加载材质并应用到 Image 组件
Laya.loader.load("shader/gradient/TextGradient.lmat", Laya.Handler.create(this, (mat: Laya.Material) => {
    this.image.material = mat;

    // 修改渐变颜色
    const topColorID = Laya.Shader3D.propertyNameToID("u_GradientTopColor");
    const bottomColorID = Laya.Shader3D.propertyNameToID("u_GradientBottomColor");
    mat.shaderData.setVector(topColorID, new Laya.Vector4(1, 0, 0, 1));    // 红色
    mat.shaderData.setVector(bottomColorID, new Laya.Vector4(0, 0, 1, 1));  // 蓝色
}));

方式二:运行时创建材质

const image = new Laya.Image();
image.skin = "atlas/comp/image.png";
Laya.stage.addChild(image);

// 创建材质
const mat = new Laya.Material();
mat.setShaderName("TextGradientShader");
image.material = mat;

// 设置渐变参数
const topColorID = Laya.Shader3D.propertyNameToID("u_GradientTopColor");
const bottomColorID = Laya.Shader3D.propertyNameToID("u_GradientBottomColor");
const angleID = Laya.Shader3D.propertyNameToID("u_GradientAngle");
const intensityID = Laya.Shader3D.propertyNameToID("u_GradientIntensity");

mat.shaderData.setVector(topColorID, new Laya.Vector4(1, 0.5, 0, 1));     // 橙色
mat.shaderData.setVector(bottomColorID, new Laya.Vector4(0.5, 0, 1, 1));  // 紫色
mat.shaderData.setNumber(angleID, 90);     // 从上到下
mat.shaderData.setNumber(intensityID, 1);  // 完全渐变

动画示例

颜色循环动画

const topColorID = Laya.Shader3D.propertyNameToID("u_GradientTopColor");
const bottomColorID = Laya.Shader3D.propertyNameToID("u_GradientBottomColor");

let time = 0;
Laya.timer.frameLoop(1, this, () => {
    time += 0.01;

    // 顶部颜色:在红-绿-蓝之间循环
    const r = (Math.sin(time) + 1) / 2;
    const g = (Math.sin(time + 2) + 1) / 2;
    const b = (Math.sin(time + 4) + 1) / 2;
    mat.shaderData.setVector(topColorID, new Laya.Vector4(r, g, b, 1));

    // 底部颜色:相位偏移
    const r2 = (Math.sin(time + 3.14) + 1) / 2;
    const g2 = (Math.sin(time + 5.14) + 1) / 2;
    const b2 = (Math.sin(time + 7.14) + 1) / 2;
    mat.shaderData.setVector(bottomColorID, new Laya.Vector4(r2, g2, b2, 1));
});

角度旋转动画

const angleID = Laya.Shader3D.propertyNameToID("u_GradientAngle");

let angle = 0;
Laya.timer.frameLoop(1, this, () => {
    angle = (angle + 1) % 360;  // 每帧增加1度
    mat.shaderData.setNumber(angleID, angle);
});

淡入渐变效果

const intensityID = Laya.Shader3D.propertyNameToID("u_GradientIntensity");

const target = { value: 0 };
Laya.Tween.create(target)
    .to("value", 1)      // 从0渐变到1
    .duration(1000)      // 1秒
    .onUpdate(() => {
        mat.shaderData.setNumber(intensityID, target.value);
    });

效果预设

日落渐变

const topColorID = Laya.Shader3D.propertyNameToID("u_GradientTopColor");
const bottomColorID = Laya.Shader3D.propertyNameToID("u_GradientBottomColor");
const angleID = Laya.Shader3D.propertyNameToID("u_GradientAngle");

mat.shaderData.setVector(topColorID, new Laya.Vector4(1, 0.4, 0, 1));     // 橙色
mat.shaderData.setVector(bottomColorID, new Laya.Vector4(0.5, 0, 0.3, 1)); // 深紫红
mat.shaderData.setNumber(angleID, 90);  // 从上到下

海洋渐变

mat.shaderData.setVector(topColorID, new Laya.Vector4(0, 0.6, 0.9, 1));   // 浅蓝
mat.shaderData.setVector(bottomColorID, new Laya.Vector4(0, 0.1, 0.3, 1)); // 深蓝
mat.shaderData.setNumber(angleID, 90);  // 从上到下

森林渐变

mat.shaderData.setVector(topColorID, new Laya.Vector4(0.4, 0.8, 0.3, 1));  // 嫩绿
mat.shaderData.setVector(bottomColorID, new Laya.Vector4(0.1, 0.3, 0.1, 1)); // 深绿
mat.shaderData.setNumber(angleID, 90);  // 从上到下

火焰渐变

mat.shaderData.setVector(topColorID, new Laya.Vector4(1, 1, 0.2, 1));   // 黄色
mat.shaderData.setVector(bottomColorID, new Laya.Vector4(1, 0.1, 0, 1)); // 红色
mat.shaderData.setNumber(angleID, 90);  // 从上到下

金属渐变

mat.shaderData.setVector(topColorID, new Laya.Vector4(0.9, 0.9, 0.9, 1)); // 银白
mat.shaderData.setVector(bottomColorID, new Laya.Vector4(0.4, 0.4, 0.5, 1)); // 灰蓝
mat.shaderData.setNumber(angleID, 90);  // 从上到下

霓虹渐变

mat.shaderData.setVector(topColorID, new Laya.Vector4(1, 0, 1, 1)); // 洋红
mat.shaderData.setVector(bottomColorID, new Laya.Vector4(0, 1, 1, 1)); // 青色
mat.shaderData.setNumber(angleID, 45);  // 对角线

方向参考

角度效果图示
0°从左到右▶▶▶
45°左上到右下↘↘↘
90°从上到下▼▼▼
135°右上到左下↙↙↙
180°从右到左◀◀◀
225°右下到左上↖↖↖
270°从下到上▲▲▲
315°左下到右上↗↗↗

Shader 源码详解

Shader 结构

Shader3D Start
{
    type:Shader3D,
    name:TextGradientShader,
    enableInstancing:true,
    supportReflectionProbe:true,
    shaderType:D2_TextureSV,  // 2D精灵纹理着色器类型

    // Uniform 变量定义(从 CPU 传入 GPU 的数据)
    uniformMap:{
        u_GradientTopColor:{ type:Vector4, default:[1, 0.5, 0, 1] },    // 顶部颜色
        u_GradientBottomColor:{ type:Vector4, default:[0.5, 0, 1, 1] }, // 底部颜色
        u_GradientAngle:{ type:Float, default:90 },                     // 渐变角度
        u_GradientIntensity:{ type:Float, default:1 }                   // 渐变强度
    },

    // 顶点属性定义(顶点数据)
    attributeMap: {
        a_posuv: Vector4,        // 位置和UV坐标
        a_attribColor: Vector4,  // 顶点颜色
        a_attribFlags: Vector4,  // 顶点标志
        a_customs:Vector4,       // 自定义数据
    },

    // Shader 宏定义
    defines: {
        GRADIENT: { type: bool, default: true }  // 启用渐变
    }

    shaderPass:[
        {
            pipeline:Forward,      // 前向渲染管线
            VS:TextGradientVS,     // 顶点着色器
            FS:TextGradientFS      // 片元着色器
        }
    ]
}

顶点着色器 (TextGradientVS)

顶点着色器负责处理顶点数据,将 2D 精灵的顶点变换到屏幕空间。

#defineGLSL TextGradientVS
    #define SHADER_NAME TextGradientShader
    #include "Sprite2DVertex.glsl";  // 引入 LayaAir 内置的 2D 精灵顶点处理

    void main() {
        vertexInfo info;
        getVertexInfo(info);  // 获取顶点信息(位置、UV、颜色等)

        // 将顶点信息传递给片元着色器
        v_cliped = info.cliped;           // 裁剪信息
        v_texcoordAlpha = info.texcoordAlpha;  // UV坐标和alpha
        v_useTex = info.useTex;           // 是否使用纹理
        v_useClip = info.useClip;         // 是否使用裁剪
        v_customs = info.customs;         // 自定义数据
        v_color = info.color;             // 顶点颜色

        // 计算最终顶点位置(屏幕坐标)
        gl_Position = getPosition(info.pos);
    }
#endGLSL

关键点说明:

  • 顶点着色器主要是一个"传递者",将原始精灵的顶点数据传递给片元着色器
  • v_texcoordAlpha 包含纹理坐标 (0~1),用于片元着色器中计算渐变位置

片元着色器核心算法 (TextGradientFS)

片元着色器是渐变效果的核心,包含渐变方向计算和颜色混合逻辑。

主函数详解
void main()
{
    clip();  // 处理裁剪(LayaAir 内置功能)

    // 获取原始精灵颜色
    vec4 originalColor = getSpriteTextureColor();

    // 【透明检测】透明部分不参与渐变效果
    if(originalColor.a < 0.01) {
        setglColor(originalColor);
        return;
    }

    // 【渐变因子计算】
    vec2 uv = v_texcoordAlpha.xy;  // 获取纹理坐标 (0~1)

    // 将角度转换为弧度
    float angleRad = radians(u_GradientAngle);

    // 计算渐变方向向量
    vec2 gradientDir = vec2(cos(angleRad), sin(angleRad));

    // 投影 UV 坐标到渐变方向上
    // UV 中心点为 (0.5, 0.5),计算当前点相对于中心的偏移
    float gradientFactor = dot(uv - 0.5, gradientDir) + 0.5;

    // 将渐变因子限制在 0-1 范围内
    gradientFactor = clamp(gradientFactor, 0.0, 1.0);

    // 【颜色混合】根据渐变因子混合顶部和底部颜色
    vec4 gradientColor = mix(u_GradientTopColor, u_GradientBottomColor, gradientFactor);

    // 【应用强度】在原始颜色和渐变颜色之间插值
    vec4 finalColor = mix(originalColor, gradientColor * originalColor.a, u_GradientIntensity);

    // 【保持透明度】使用原始透明度
    finalColor.a = originalColor.a;

    setglColor(finalColor);
}

关键算法图解

渐变因子计算原理:

UV 坐标系 (0~1):

(0,0) ─────────── (1,0)
  │                  │
  │        ●         │  ● = 中心点 (0.5, 0.5)
  │                  │
(0,1) ─────────── (1,1)

1. 计算相对位置:uv - 0.5
   结果范围:(-0.5, -0.5) 到 (0.5, 0.5)

2. 投影到渐变方向:dot(uv - 0.5, gradientDir)
   - angle = 90° (向下) → gradientDir = (0, 1)
   - dot((x, y), (0, 1)) = y
   - 顶部点 y = -0.5,底部点 y = 0.5

3. 归一化:+ 0.5
   - 顶部:-0.5 + 0.5 = 0
   - 底部:0.5 + 0.5 = 1

渐变方向示意图:

angle = 0°   (→)    angle = 45°  (↘)    angle = 90°  (↓)    angle = 135° (↙)

0 ────────→ 1      0 ↘↘↘↘↘↘ 1        0 ↓↓↓↓↓↓ 1        1 ↙↙↙↙↙↙ 0
TopColor     BotColor     TopColor     BotColor     TopColor     BotColor

颜色混合公式:

// 1. 渐变颜色计算
gradientColor = mix(topColor, bottomColor, gradientFactor)

当 gradientFactor = 0:  gradientColor = topColor
当 gradientFactor = 0.5: gradientColor = 两者各50%混合
当 gradientFactor = 1:  gradientColor = bottomColor

// 2. 强度混合
finalColor = mix(originalColor, gradientColor * originalColor.a, intensity)

当 intensity = 0:  finalColor = originalColor     (无渐变)
当 intensity = 1:  finalColor = gradientColor     (完全渐变)

参数效果可视化

参数值视觉效果
u_GradientAngle0°从左到右渐变
90°从上到下渐变
180°从右到左渐变
270°从下到上渐变
u_GradientIntensity0无渐变,显示原图
0.550%渐变效果
1完全渐变

动态文本与 TextGradientShaderComponent 组件

问题:文本改变时颜色可能不一致

当使用 shader 方式实现文本渐变时,如果文本内容动态改变,会出现以下问题:

初始文本: "HELLO"     → 渐变正常
改变后:   "WORLD"     → 渐变范围可能不正确

原因:shader 基于纹理的 UV 坐标计算渐变,文本长度改变后:

  • 纹理尺寸变化 → UV 坐标范围变化 → 渐变效果不一致

解决方案:使用 TextGradientShaderComponent 组件

TextGradientShaderComponent 组件封装了文本渐变的完整逻辑,自动处理文本变化:

// 方式一:在 IDE 中添加组件
// 1. 创建一个 Text 组件
// 2. 添加 TextGradientShaderComponent 组件
// 3. 在属性面板配置渐变参数

// 方式二:代码中创建
const text = new Laya.Text();
text.text = "动态渐变文本";
text.fontSize = 48;
text.width = 400;
text.height = 60;

const gradientComp = text.addComponent(TextGradientShaderComponent);
gradientComp.topColor = { r: 1, g: 0.5, b: 0, a: 1 };  // 橙色
gradientComp.bottomColor = { r: 0.5, g: 0, b: 1, a: 1 }; // 紫色
gradientComp.angle = 90;      // 从上到下
gradientComp.intensity = 1;   // 完全渐变

// 文本改变时,渐变效果会自动更新
text.text = "新的文本内容";  // 渐变自动适应

组件特性

特性说明
自动监听文本变化检测 Text.text 属性变化
重新渲染纹理文本改变后自动重新生成 texture
保持渐变一致渐变范围始终匹配当前文本尺寸
无需手动刷新框架自动处理,调用 refresh() 可强制刷新

适用场景对比

方案优点缺点适用场景
直接使用 Shader性能更好,GPU 直接渲染文本改变需手动处理静态文本、Image 背景
ImageGradientComponent文本改变自动处理使用 canvas,性能稍低动态文本、频繁变化
TextGradientShaderComponent结合两者优点,自动刷新需要额外组件动态文本,需要稳定效果

使用场景

文本渐变

// 注意:Text 组件不支持直接使用 shader
// 需要将文本渲染为 texture,然后用 Sprite 显示

const text = new Laya.Text();
text.text = "渐变文本";
text.fontSize = 48;
text.color = "#FFFFFF";

// 将 text 的内容绘制到 canvas
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d')!;
canvas.width = 200;
canvas.height = 60;
ctx.font = "48px Arial";
ctx.fillStyle = "#FFFFFF";
ctx.fillText("渐变文本", 0, 48);

// 创建 texture 和 sprite
const tex = new Laya.Texture(canvas);
const sprite = new Laya.Sprite();
sprite.graphics.drawTexture(tex, 0, 0);

// 应用渐变 shader
const mat = new Laya.Material();
mat.setShaderName("TextGradientShader");
mat.shaderData.setVector(Laya.Shader3D.propertyNameToID("u_GradientTopColor"),
    new Laya.Vector4(1, 0.5, 0, 1));
mat.shaderData.setVector(Laya.Shader3D.propertyNameToID("u_GradientBottomColor"),
    new Laya.Vector4(0.5, 0, 1, 1));
sprite.material = mat;

Laya.stage.addChild(sprite);

按钮背景渐变

const btn = new Laya.Image();
btn.skin = "button.png";

const mat = new Laya.Material();
mat.setShaderName("TextGradientShader");

// 按钮正常状态:蓝色渐变
mat.shaderData.setVector(Laya.Shader3D.propertyNameToID("u_GradientTopColor"),
    new Laya.Vector4(0.4, 0.7, 1, 1));
mat.shaderData.setVector(Laya.Shader3D.propertyNameToID("u_GradientBottomColor"),
    new Laya.Vector4(0.2, 0.4, 0.8, 1));

btn.material = mat;

// 鼠标悬停时改变颜色
btn.on(Laya.Event.MOUSE_OVER, this, () => {
    mat.shaderData.setVector(Laya.Shader3D.propertyNameToID("u_GradientTopColor"),
        new Laya.Vector4(0.6, 0.9, 1, 1));
    mat.shaderData.setVector(Laya.Shader3D.propertyNameToID("u_GradientBottomColor"),
        new Laya.Vector4(0.3, 0.6, 1, 1));
});

注意事项

  1. 透明区域处理:透明部分(alpha < 0.01)不会参与渐变效果,保持透明
  2. Text 组件限制:LayaAir 的 Text 组件不支持直接使用 shader,需要将文本渲染为 texture 后使用 Sprite 显示
  3. 角度范围:角度为 0-360 度,超出范围会自动循环
  4. 颜色格式:颜色使用 RGBA 格式,每个分量范围 0-1

常见问题

Q: 为什么图片没有渐变效果?

A: 确保材质已正确加载,并且 u_GradientIntensity 值大于 0。

Q: 如何实现水平渐变?

A: 将 u_GradientAngle 设置为 0(从左到右)或 180(从右到左)。

Q: 如何实现垂直渐变?

A: 将 u_GradientAngle 设置为 90(从上到下)或 270(从下到上)。

Q: 渐变颜色和原图怎么混合?

A: 使用 u_GradientIntensity 参数控制:0=完全原图,1=完全渐变,中间值=两者混合。

Q: 对角线渐变怎么设置?

A: 将 u_GradientAngle 设置为 45(左上到右下)或 135(右上到左下)。

Q: Text 组件能直接使用这个 shader 吗?

A: 不能。Text 组件有特殊的渲染机制,不支持自定义 shader。需要将文本渲染到 canvas,然后创建 Sprite 来显示并应用 shader。参考 TextGradientShaderComponent.ts 组件的实现方式。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐