Melt + Energy Ripple
Goal: morph a highly subdivided cube into a sphere using noise and draw a purple energy band along the front. Pure TSL nodes.
This example uses add、 clamp、 color、 mix、 mul、 mx_fractal_noise_float、 normalize、 positionLocal、 smoothstep、 sub、 uniform、 varying TSL nodes.
Result and key ideas
- uProgress as the global knob: 0=cube, 1=sphere.
- Fractal noise jitters the boundary to feel liquid.
- Pass warpedProgress to fragment to light the band.
Minimal runnable code (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()
// Vertex stage: give each vertex a cube->sphere slider
const noiseScale = uniform(2.5) // Noise density. Think zooming a cloud map
const noiseStrength = uniform(0.5) // How strongly noise affects the boundary
const boxPos = positionLocal
const sphereDir = mul(normalize(positionLocal), 1.0) // Normalize local position to get the sphere direction
const noise = mul(
add(mx_fractal_noise_float(mul(positionLocal, noiseScale), 3, 2.0, 0.5), 1.0),
0.5
) // 3-layer fractal noise, remapped to [0,1]
const warped = clamp(
sub(mul(u, add(1.0, noiseStrength)), mul(noise, noiseStrength)),
0.0, 1.0
) // Global progress with noise jitter, then clamped
mat.positionNode = mix(boxPos, sphereDir, warped) // Interpolate between cube coords and sphere direction
const vWarped = varying(warped) // Pass per-vertex progress to fragment
// Fragment stage: base surface + energy band
const baseColor = color("#2563eb")
const noiseColor = color("#86efac") // Two colors: base and modeling noise
const surfaceNoise = mx_fractal_noise_float(mul(positionLocal, 3.0), 4, 2.0, 0.5) // Surface noise for uneven highlights
const noiseFactor = smoothstep(0.2, 0.8, surfaceNoise) // smoothstep to widen contrast
const baseSurface = mix(baseColor, noiseColor, noiseFactor) // Mix base and noise by the noise factor
const glowColor = color("#818cf8")
const waveWidth = uniform(0.25) // Glow color and band thickness
const waveCenter = sub(1.0, mul(add(vWarped, -0.5).abs(), 1.0)) // Distance to the morph front around 0.5, higher = nearer
const glowFactor = smoothstep(sub(1.0, waveWidth), 1.0, waveCenter) // Soft threshold by thickness to get glow strength
const finalColor = mix(baseSurface, glowColor, glowFactor) // Blend glow into surface color
mat.colorNode = finalColor
u.value = 0.35 // Initial progress; animation will take over
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) // Stabilize: clamp to [0,1] to reduce edge jitter
meshRef.current.position.x = -1.5 + p * 3.0 // Tiny horizontal move to observe the morph
})
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>
)
}Node graph breakdown
Vertex: mix cube coords and sphere direction. Fragment: base modeling noise + a controllable glow band derived from warpedProgress.
// Vertex: one slider per point
const boxPos = positionLocal
const sphereDir = normalize(positionLocal) // Direction from origin to sphere
const noise = mx_fractal_noise_float(positionLocal.mul(2.5), 3, 2.0, 0.5).add(1.0).mul(0.5) // 3-layer fractal noise mapped to [0,1]
const warped = clamp( uProgress.mul(1.0 + 0.5).sub(noise.mul(0.5)), 0.0, 1.0 ) // Global progress + noise jitter, then clamp
position = mix(boxPos, sphereDir, warped)// Fragment: base + noise + band
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))) // Mix base and noise first
const waveCenter = 1.0 - (vWarped.add(-0.5)).abs() // Higher near the deformation front
const glowFactor = smoothstep(1.0 - waveWidth, 1.0, waveCenter) // Thickness-controlled soft threshold
color = mix(baseSurface, color("#818cf8"), glowFactor) // Blend the band into the surfaceTimeline: one knob drives all
Phases: go -> hold -> back -> hold. Use the same p for shape and color.
// useFrame: write uProgress; drive shape and color with the same p
const MOVE = 3.5, PAUSE = 1.5
// Denoise: clamp to [0,1] for steadier framesTweakables and intuition
- noiseScale (cloud zoom), noiseStrength (edge fuzziness).
- waveWidth (band thickness).
- Subdivisions: 128³ looks smooth; 64³ for performance.
Troubleshooting
- Black screen: WebGPU init + browser support; temporarily fallback to WebGL.
- Slow anim: check division vs modulo typos.
- No effect: use Node material and assign colorNode.