融化 + 能量涟漪
目标:让一个高细分立方体在噪声的帮助下“融化”为球体,并用一条紫色能量带勾勒前沿。整套效果只靠 TSL 节点堆出,像拼乐高。
本示例用到 add、 clamp、 color、 mix、 mul、 mx_fractal_noise_float、 normalize、 positionLocal、 smoothstep、 sub、 uniform、 varying TSL 节点。
先看成品与要点
- uProgress 是总开关:0=立方体,1=球体。
- 用分形噪声让过渡边界抖一点,像液体在爬。
- 把 warpedProgress 传到片元阶段,用它来点亮能量带。
最小可运行代码(R3F + TSL)
import { useMemo, useRef } from "react"
import { WebGPURenderer, MeshBasicNodeMaterial } from "three/webgpu"
import {
add, clamp, color, mix, mul, mx_fractal_noise_float,
normalize, positionLocal, smoothstep, sub, uniform, varying
} from "three/tsl"
import { Canvas } from "@react-three/fiber"
import { useFrame } from "@react-three/fiber"
import * as THREE from "three"
function MorphingAndMovingMesh() {
const meshRef = useRef()
const [material, uProgress] = useMemo(() => {
const u = uniform(0)
const mat = new MeshBasicNodeMaterial()
// 顶点阶段:给每个顶点一个从“立方体->球体”的滑块
const noiseScale = uniform(2.5) // 噪声密度。像把云图放大或缩小
const noiseStrength = uniform(0.5) // 噪声对过渡边界的影响力度
const boxPos = positionLocal
const sphereDir = mul(normalize(positionLocal), 1.0) // 把本地坐标归一化就得到指向球面的方向
const noise = mul(
add(mx_fractal_noise_float(mul(positionLocal, noiseScale), 3, 2.0, 0.5), 1.0),
0.5
) // 三层分形噪声,先偏移到 [0,1] 再用
const warped = clamp(
sub(mul(u, add(1.0, noiseStrength)), mul(noise, noiseStrength)),
0.0, 1.0
) // 把全局进度做一次“抖动 + 夹紧”,更像液体在爬
mat.positionNode = mix(boxPos, sphereDir, warped) // 在立方体位置和球面方向之间插值
const vWarped = varying(warped) // 把每个顶点的进度带到片元阶段
// 片元阶段:铺底色,再叠能量带
const baseColor = color("#2563eb")
const noiseColor = color("#86efac") // 底色与建模噪声的两种颜色
const surfaceNoise = mx_fractal_noise_float(mul(positionLocal, 3.0), 4, 2.0, 0.5) // 表面噪声,用于做出不均匀的受光感
const noiseFactor = smoothstep(0.2, 0.8, surfaceNoise) // 用 smoothstep 扩大对比,让纹理更清楚
const baseSurface = mix(baseColor, noiseColor, noiseFactor) // 把底色与噪声色按噪声权重混合
const glowColor = color("#818cf8")
const waveWidth = uniform(0.25) // 能量带颜色与厚度
const waveCenter = sub(1.0, mul(add(vWarped, -0.5).abs(), 1.0)) // 以 0.5 为中心的“前沿距离”指标,越近值越高
const glowFactor = smoothstep(sub(1.0, waveWidth), 1.0, waveCenter) // 根据厚度做一次软阈值,得到发光强度
const finalColor = mix(baseSurface, glowColor, glowFactor) // 把能量带加权混到表面色里
mat.colorNode = finalColor
u.value = 0.35 // 初始进度,动画会接管它
return [mat, u]
}, [])
useFrame(({ clock }) => {
const time = clock.getElapsedTime()
const MOVE = 3.5, PAUSE = 1.5
const TOTAL = (MOVE + PAUSE) * 2
const t = time % TOTAL
let p = 0
if (t < MOVE) p = t / MOVE // 0→1
else if (t < MOVE + PAUSE) p = 1 // hold
else if (t < MOVE * 2 + PAUSE) // 1→0
p = 1 - (t - (MOVE + PAUSE)) / MOVE
else p = 0
uProgress.value = THREE.MathUtils.clamp(p, 0, 1) // 更稳的值:夹到 [0,1],减少边界抖动
meshRef.current.position.x = -1.5 + p * 3.0 // 顺手做个水平位移,便于观察
})
return (
<mesh ref={meshRef}>
<boxGeometry args={[0.5, 0.5, 0.5, 128, 128, 128]} />
<primitive object={material} attach="material" />
</mesh>
)
}
export default function TSLMorphing() {
return (
<Canvas
gl={async (props) => {
const r = new WebGPURenderer(props)
await r.init?.()
return r
}}
camera={{ position: [0, 0, 2.5], fov: 50 }}
style={{ width: "100%", height: "100vh", background: "black" }}
>
<MorphingAndMovingMesh />
</Canvas>
)
}关键节点图拆解
思路很直白:顶点阶段把“立方体坐标”和“球面方向”做 mix;片元阶段在表面铺一层建模噪声,再用 warpedProgress 做一条可控厚度的发光带。
// 顶点:给每个点一只滑块
const boxPos = positionLocal
const sphereDir = normalize(positionLocal) // 从原点指向球面
const noise = mx_fractal_noise_float(positionLocal.mul(2.5), 3, 2.0, 0.5).add(1.0).mul(0.5) // 三层分形噪声,映射到 [0,1]
const warped = clamp( uProgress.mul(1.0 + 0.5).sub(noise.mul(0.5)), 0.0, 1.0 ) // 全局进度 + 噪声抖动,再夹紧
position = mix(boxPos, sphereDir, warped)// 片元:底色 + 噪声 + 能量带
const baseSurface = mix(color("#2563eb"), color("#86efac"),
smoothstep(0.2, 0.8, mx_fractal_noise_float(positionLocal.mul(3.0), 4, 2.0, 0.5))) // 先把底色与噪声混一层
const waveCenter = 1.0 - (vWarped.add(-0.5)).abs() // 越接近形变前沿值越高
const glowFactor = smoothstep(1.0 - waveWidth, 1.0, waveCenter) // 厚度控制的软阈值
color = mix(baseSurface, color("#818cf8"), glowFactor) // 把能量带混到表面色里动画时间线:一根旋钮管全局
时间被拆成“来->停->回->停”。同一个 p 同时驱动形状与颜色,减少参数心智负担。
// useFrame:往 uProgress 写值;用同一个 p 驱动形状和颜色
const MOVE = 3.5, PAUSE = 1.5
// 去毛刺:clamp 到 [0,1],画面更稳可调参数:怎么“看”数值
- noiseScale:云图缩放。小=大颗粒起伏,大=细碎皱褶。
- noiseStrength:噪声对过渡的影响强度。越大越“毛边”。
- waveWidth:能量带厚度。0.1 薄、0.35 厚。
- 细分数:128³ 看起来细腻;性能不够可降到 64³。
排错与建议
- 黑屏:WebGPU 需 init;浏览器需支持;不行先回退 WebGL 再调试。
- 动画太慢:检查除法/取余是否写错成“/”与“%”。
- 材质没生效:确保用 Node 材质,并把表达式赋给 colorNode,而不是 color。