1. 动态扫光效果的核心原理扫光效果本质上是通过Shader对UV坐标进行动态计算实现的。想象一下用手电筒在黑暗的房间里扫过墙面的场景——光束中心最亮边缘逐渐衰减。在Shader中我们通过以下几个关键参数来模拟这个效果光束中心点(lightCenterPoint)用二维向量表示范围在[0,1]的UV空间内光束宽度(lightWidth)控制光束的照射范围光束角度(lightAngle)决定光束的倾斜方向光束颜色(lightColor)RGBA格式的光照颜色核心算法是通过计算当前像素UV到光束中心线的距离结合宽度参数生成一个0-1的衰减系数。这个系数会与原始纹理颜色进行混合实现扫光效果。具体公式如下// 计算UV到光束中心线的距离 float angleRad radians(lightAngle); float distanceToBeam abs((uv.x - centerX) * sin(angleRad) - (uv.y - centerY) * cos(angleRad)); // 生成衰减系数 float attenuation smoothstep(lightWidth/2.0, 0.0, distanceToBeam);2. 基础扫光Shader实现2.1 Effect资源配置首先创建Effect文件定义Shader需要的属性和渲染管线CCEffect %{ techniques: - passes: - vert: sprite-vs:vert frag: sprite-fs:frag properties: texture: { value: white } // 主纹理 lightColor: { value: [1.0, 1.0, 0.0, 1.0], // 默认黄色光束 editor: { type: color } } lightCenter: { value: [0.5, 0.5] } // 中心点默认居中 lightWidth: { value: 0.3 } // 默认宽度 lightAngle: { value: 45.0 } // 默认45度角 }%2.2 顶点着色器顶点着色器主要负责坐标变换和UV传递CCProgram sprite-vs %{ precision highp float; #include cc-global in vec3 a_position; in vec2 a_texCoord; out vec2 uv; void main() { gl_Position cc_matViewProj * vec4(a_position, 1); uv a_texCoord; } }%2.3 片段着色器片段着色器实现核心扫光逻辑CCProgram sprite-fs %{ precision highp float; in vec2 uv; uniform sampler2D texture; uniform vec4 lightColor; uniform vec2 lightCenter; uniform float lightWidth; uniform float lightAngle; void main() { vec4 texColor texture2D(texture, uv); // 计算角度和距离 float angleRad radians(lightAngle); vec2 dir vec2(sin(angleRad), cos(angleRad)); vec2 toCenter uv - lightCenter; float distanceToBeam abs(dot(toCenter, vec2(dir.y, -dir.x))); // 生成平滑衰减 float attenuation smoothstep(lightWidth/2.0, 0.0, distanceToBeam); // 混合颜色 gl_FragColor texColor lightColor * attenuation * texColor.a; } }%3. 动态参数控制3.1 创建材质脚本控制器通过TypeScript脚本动态控制Shader参数const { ccclass, property } _decorator; ccclass(ScanLightController) export class ScanLightController extends Component { property(Sprite) targetSprite: Sprite null; property({ type: CCFloat, tooltip: 扫描速度 }) scanSpeed: number 1.0; property({ type: CCFloat, tooltip: 扫描宽度 }) lightWidth: number 0.3; private _material: Material; private _scanPos: number 0; start() { this._material this.targetSprite.getMaterial(0); this._material.setProperty(lightWidth, this.lightWidth); } update(dt: number) { this._scanPos dt * this.scanSpeed; if (this._scanPos 1.5) this._scanPos -0.5; // 沿对角线扫描 this._material.setProperty(lightCenter, new Vec2(this._scanPos, this._scanPos)); } }3.2 参数动画过渡实现平滑的参数过渡效果// 在控制器类中添加方法 public setLightColor(targetColor: Color, duration: number 0.5) { const currentColor this._material.getProperty(lightColor); tween(currentColor) .to(duration, targetColor, { onUpdate: (color: Color) { this._material.setProperty(lightColor, color); } }) .start(); } // 调用示例 this.setLightColor(new Color(255, 100, 255, 255)); // 过渡到紫色光4. 高级功能扩展4.1 边缘裁剪与雾化效果增强扫光的视觉效果// 在片段着色器中添加 uniform float edgeSoftness; // 0-1的软边参数 uniform float fogIntensity; // 雾化强度 // 修改衰减计算 float edge smoothstep(lightWidth/2.0, lightWidth/2.0 * (1.0 - edgeSoftness), distanceToBeam); float fog exp(-distanceToBeam * fogIntensity); attenuation edge * fog;4.2 多光束混合支持多个扫光效果叠加uniform vec4 lightColors[3]; uniform vec2 lightCenters[3]; uniform float lightWidths[3]; void main() { vec4 texColor texture2D(texture, uv); vec4 finalColor texColor; for(int i0; i3; i) { // 计算每个光束的贡献 float atten calculateAttenuation(uv, lightCenters[i], lightWidths[i]); finalColor lightColors[i] * atten * texColor.a; } gl_FragColor min(finalColor, vec4(1.0)); // 防止颜色值超过1.0 }5. 性能优化技巧5.1 指令数优化减少Shader指令数以提升性能// 优化后的距离计算 vec2 beamVec vec2(sin(angleRad), cos(angleRad)); float distanceToBeam abs(beamVec.y*(uv.x-center.x) - beamVec.x*(uv.y-center.y)); // 使用step替代smoothstep获得更硬朗的边缘性能更好 float edge step(distanceToBeam, lightWidth/2.0);5.2 批处理优化确保材质实例化参数正确设置// 在脚本中设置 this._material.initialize({ defines: { USE_INSTANCING: true } }); // 对于动态参数使用setProperty的instanced版本 this._material.setProperty(lightCenter, new Vec2(x,y), true);6. 实际应用案例6.1 UI高亮提示为重要按钮添加扫光效果// 按钮提示控制器 ccclass(UIHighlight) export class UIHighlight extends Component { property(Button) targetButton: Button null; private _material: Material; start() { const sprite this.targetButton.node.getComponent(Sprite); this._material sprite.getMaterial(0); this.targetButton.node.on(Node.EventType.MOUSE_ENTER, () { this._material.setProperty(lightColor, new Color(0, 255, 255, 255)); }); this.targetButton.node.on(Node.EventType.MOUSE_LEAVE, () { this._material.setProperty(lightColor, new Color(0, 0, 0, 0)); }); } }6.2 道具稀有度表现不同稀有度道具使用不同扫光效果enum ItemRarity { Common, Rare, Epic, Legendary } ccclass(ItemRarityEffect) export class ItemRarityEffect extends Component { property({ type: Enum(ItemRarity) }) rarity: ItemRarity ItemRarity.Common; private _colors [ new Color(200, 200, 200, 100), // 普通 new Color(0, 150, 255, 150), // 稀有 new Color(180, 0, 255, 200), // 史诗 new Color(255, 100, 0, 255) // 传说 ]; start() { const mat this.node.getComponent(Sprite).getMaterial(0); mat.setProperty(lightColor, this._colors[this.rarity]); mat.setProperty(lightWidth, 0.1 this.rarity * 0.1); } }