Got it. With Draw Material to Render Target at 512×512, you can do a very solid 2D ripple sim, and your point SDF “outward force” can plug in nicely as the injection term (or as a directional advection term if you want the ripples to “push” outward).
Here’s a concrete setup that works well in UE5 materials.
Use your ping-pong RTs like:
- R = h(t) (current height)
- G = h(t-Δt) (previous height)
Each update pass writes:
This lets you keep only the two RTs you already have.
WaveK = 0.18
Damping = 0.04
StampStrength = whatever you like (try 0.5)
TexelSize = (1/512, 1/512) = (0.001953125, 0.001953125)
In the update material:
-
Sample source RT at UV → C (float4)
-
Sample neighbors (R channel):
hL = Sample(UV - float2(du,0)).r
hR = Sample(UV + float2(du,0)).r
hD = Sample(UV - float2(0,dv)).r
hU = Sample(UV + float2(0,dv)).r
- Laplacian:
lap = (hL + hR + hD + hU - 4*h)
Compute:
next = (2 - Damping)*h - (1 - Damping)*prev + WaveK * lap
That’s your ripple propagation.
You mentioned you’re already computing an outward force via a point SDF. In practice you’ll want a scalar injection (adds displacement/energy) and optionally use the direction for advection later.
Let your stamp mask be S in 0..1 (white where you stamp). Then:
Add it:
This yields nice circular ripples.
Velocity is roughly h - prev. A quick trick:
vel = h - prev
vel += S * StampStrength
next = h + vel + WaveK*lap - Damping*vel
If you want it in the earlier form without re-deriving, the cheap approximation is:
next += S * StampStrength
prev -= S * StampStrength * 0.5 (makes the kick sharper)
This often looks more like a splash.
If your SDF gives you a gradient direction dir = normalize(∇SDF) pointing outward, you have two useful options:
Make the impulse stronger in the outward direction by biasing the height injection with the direction field you’re already computing (still scalar in the end):
inj = S * StampStrength
next += inj
…and separately compute a normal from height for visuals (below). This alone often “reads” as outward motion if the source is radial.
This is optional and more “force-like”:
UV_adv = UV - dir * AdvectStrength * S
- sample
h_adv = Sample(UV_adv).r instead of h (or blend with it)
So you do:
h_used = lerp(h, h_adv, S * AdvectAmount)
Then use h_used in the wave update.
This makes ripples get “pushed” outward by your force field, which sounds like what you’re after.
Start tiny:
AdvectStrength = 0.5 * du (so about 0.000976)
AdvectAmount = 0.25
Your current “lerp to accumulate” trail will act like heavy damping if you apply it to the wave state.
Best pattern:
- Maintain trail separately (another RT or another channel), OR
- Keep trail as a very small bias added to
h (not overwriting the state).
If you want to keep everything in one RT:
- Put trail in B channel and don’t involve it in the wave step.
- B = trail(t)
- Update:
trailNext = lerp(trail, stamp, TrailBlend) or trailNext = max(trail * fade, stamp)
- Output:
RGB = (next, h, trailNext)
Then in your display material you can combine:
- visualHeight =
next + trailScale * B
In your display material (not the update pass), compute normals from height:
dx = hR - hL
dy = hU - hD
N = normalize(float3(-dx * NormalStrength, -dy * NormalStrength, 1))
Use that for:
- refraction offset
- specular highlights
- foam mask =
saturate(length(float2(dx,dy))*FoamK)
- Use Clamp addressing on the RT sampler to avoid edge wrapping weirdness.
- Disable sRGB on the RT (it’s data).
- Use a float format RT if you can (R16f/RG16f ideal). If you’re using RGBA8, it still works but you’ll get quantization and faster damping.
Update pass output:
R = next
G = h
B = trailNext (optional)
A = 1
Where:
next = (2 - Damping)*h - (1 - Damping)*prev + WaveK*lap + inj
If you tell me how you currently compute the SDF “outward force” in the material (do you already have dir as a float2/float3?), I’ll map it directly into either the impulse or the advection variant with exact node operations.