Click Halo 交互光环
我们来做一个交互练习:点击屏幕,生成程序化霓虹光环。本章重点在于打通 React 事件与 TSL 逻辑。
本示例使用 uv、 vec2、 vec3、 length、 smoothstep、 mx_hsvtorgb、 uniform处理几何与渐变。
1. 核心思路
- 隐形交互层:使用 invisible plane 捕获点击,实现零延迟反馈。
- SDF 建模:零贴图。完全依靠数学(距离场)计算出光环形状和柔边。
- 独立生命周期:每个光环都是独立的 React 组件,自己管理出生、扩散和销毁。
2. 搭建隐形舞台
我们需要一个射线检测(Raycast)的靶子。创建一个 InteractPlane,设为不可见,专门用来接收点击并获取 UV 坐标。
// 底层:不可见的触摸接收层
function InteractPlane({ onPointerDown }) {
return (
<mesh onPointerDown={onPointerDown}>
<planeGeometry args={[10, 10]} />
{/* 设为不可见依然可以接收射线检测! */}
<meshBasicMaterial visible={false} />
</mesh>
)
}
export default function ClickHalo() {
// ... state management
return (
<Canvas gl={async (p) => { const r = new WebGPURenderer(p); await r.init(); return r }}>
<InteractPlane onPointerDown={handlePointerDown} />
{rings.map(r => (
<Ring key={r.id} centerUV={r.center} onDone={() => removeRing(r.id)} />
))}
</Canvas>
)
}3. TSL 几何与配色
Shader 只需要做两件事:用 smoothstep 切出圆环形状(遮罩),并生成随机的霓虹渐变(颜色)。
// <Ring> 内部:纯数学,无贴图
const radius = uniform(0.0) // 稍后由 useFrame 驱动
// 1. Geometry: Signed Distance Field
const d = length(uv().sub(center)) // 像素到点击中心的距离
const outer = smoothstep(radius.sub(halfT).sub(feather), radius.add(halfT).add(feather), d)
const inner = smoothstep(radius.sub(halfT).add(feather), radius.add(halfT).sub(feather), d)
const ringMask = outer.sub(inner) // 外圆减内圆得到环
// 2. Color: Neon & Gradient
const neon = mx_hsvtorgb(vec3(hueSeed, 0.8, 0.8)) // 固定饱和度,随机色相
const tBand = d.sub(radius.sub(halfT)).div(thickness).saturate() // 归一化环宽位置 0..1
const brightness = tBand.sub(EDGE_BIAS).div(1.0 - EDGE_BIAS).saturate() // 在内边缘增强亮度
const finalColor = neon.mul(brightness).mul(2.0)
const alpha = ringMask.mul(fade.oneMinus()) // 随动画淡出透明度4. 动画生命周期
React 负责“生”(数组增加),而 Ring 组件内部利用 useFrame 负责“动”和通知“死”。
// 动画循环与资源清理
useFrame((_, delta) => {
// 在 JS 中计算缓动
tRef.current += delta / duration
const t = easeOutCubic(tRef.current)
// 通过管道推送到 GPU Uniform
radius.value = t * maxR
fade.value = t
if (t >= 1) onDone()
})
return (
// 关键:不要挡住地板的交互!
<mesh raycast={null}>
<planeGeometry />
<meshBasicNodeMaterial ... />
</mesh>
)5. 避坑指南
- 射线遮挡陷阱:新生成的光环也是 Mesh!务必设置 raycast={null},否则它会挡住鼠标,导致点不到底下的地板。
- Uniform 数据管道:我们在 JS 里计算缓动曲线(EaseOut),每帧通过 radius.value 推送给 GPU。
- 垃圾回收:动画结束时必须调用 onDone 通知父组件清理 State,防止内存无限增长。