Here’s what I can see and what I’d fix/improve based on the compute shader you shared.
You’ve got:
RWTexture2D<float> NState;
Texture2D<float4> Sources;
Texture2D<float> Obstacles;
Conceptually this is a classic wave solver:
PState – previous timestep
State – current timestep
NState – next timestep (RW)
Sources – excitation / emitters
Obstacles – walls/geometry
dt, c, D – timestep, wave speed, damping
CSMain – presumably does the stencil / wave equation update
Combine – adds sources on top of the propagated state
That’s all reasonable. Most of the issues are in the details: types, indexing, and a couple of Unity/HLSL gotchas.
In your Combine kernel:
void Combine (uint3 id : SV_DispatchThreadID)
NState[id.xy] = State[id.xy] + Sources[id.xy];
Problem: State and Sources are Texture2D<...>, which are SRVs. In HLSL:
RWTexture2D<T> can be indexed with [] using integer coordinates.
Texture2D<T> cannot; you must call .Load() or .Sample().
So State[id.xy] and Sources[id.xy] are invalid and will not compile (or will behave incorrectly depending on compiler).
Fix options:
Option A – make them RW textures and use []:
RWTexture2D<float> PState;
RWTexture2D<float> State;
RWTexture2D<float> NState;
RWTexture2D<float4> Sources;
RWTexture2D<float> Obstacles;
Then State[id.xy] and Sources[id.xy] are fine.
Option B – keep them as Texture2D and use Load:
float stateVal = State.Load(int3(id.xy, 0));
float4 srcVal = Sources.Load(int3(id.xy, 0));
NState[id.xy] = stateVal + srcVal.r; // or some combination of (r,g,b,a)
This is usually what you want if you are binding them as SRVs from the CPU side.
Right now:
Texture2D<float4> Sources;
RWTexture2D<float> NState;
NState[id.xy] = State[id.xy] + Sources[id.xy];
Assuming you fix the indexing, you still have a type mismatch:
State is float
Sources is float4
float + float4 → float4 (scalar is promoted to vector)
- Assigning
float4 to RWTexture2D<float> is not valid.
You must decide how you want to use Sources:
- If it’s a greyscale mask: make it
Texture2D<float> and keep everything scalar.
- If you only care about one channel: explicitly pick that channel.
Suggested fix:
Texture2D<float> Sources; // Greyscale, simplest
float src = Sources.Load(int3(id.xy, 0)).r;
Then:
float stateVal = State.Load(int3(id.xy, 0));
float srcVal = Sources.Load(int3(id.xy, 0));
NState[id.xy] = stateVal + srcVal;
Unity already defines a _Time uniform (typically float4 _Time).
You’re declaring:
This can cause:
- name collisions
- confusing behavior depending on include order and compiler
Fix: rename your uniform to something else, e.g.:
Or, if you really want Unity’s _Time, do not redeclare it and instead use the built-in.
If you dispatch your compute shader with dimensions rounded up (common pattern in Unity: ceil(width / groupSize)), SV_DispatchThreadID may extend beyond the valid texture region.
Your kernels should guard against that:
void Combine (uint3 id : SV_DispatchThreadID)
if (id.x >= Width || id.y >= Height) return;
float stateVal = State.Load(int3(id.xy, 0));
float srcVal = Sources.Load(int3(id.xy, 0));
NState[id.xy] = stateVal + srcVal;
Otherwise you risk out-of-bounds reads/writes if your dispatch is not perfectly aligned.
I can’t see your CSMain body (it’s redacted/omitted as ...), but for a typical 2D wave equation with damping you’d expect something like:
// Laplacian using 4-neighbour or 8-neighbour stencil
State.Load(int3(id.x + 1, id.y, 0)) +
State.Load(int3(id.x - 1, id.y, 0)) +
State.Load(int3(id.x, id.y + 1, 0)) +
State.Load(int3(id.x, id.y - 1, 0)) -
4.0 * State.Load(int3(id.xy, 0));
// Wave update (simple form)
float uPrev = PState.Load(int3(id.xy, 0));
float u = State.Load(int3(id.xy, 0));
float dt2c2 = c * c * dt * dt;
float next = 2.0 * u - uPrev + dt2c2 * lap;
// Damping (e.g. simple linear term)
Points to watch:
-
Stability: For an undamped wave equation on a regular grid, c * dt / dx must satisfy a CFL condition (e.g. ≤ 1/√2 for 2D with central differences).
If dt and c are too large, the simulation blows up.
-
Obstacles: Obstacles is declared but not shown in use. Usually you’d:
-
Zero out next where obstacles exist, or
-
Treat them as fixed boundary conditions, e.g.:
float obs = Obstacles.Load(int3(id.xy, 0));
next = 0.0; // rigid wall
// optionally: set PState = State = 0 here via another pass
-
Boundary conditions at edges:
- You need either clamped indexing, mirrored boundaries, or explicit conditions (e.g. zero / reflective).
- Make sure your Laplacian doesn’t read outside the texture; handle
x=0, x=Width-1, etc.
Double-check that your C# code matches the shader:
-
Resource types:
- If you’re binding with
SetTexture(kernel, "State", stateRT), that RenderTexture must have enableRandomWrite set true for RWTexture2D, but not required for plain Texture2D SRVs.
- Decide which of your textures are read-only vs read-write, and ensure both the C# side and HLSL declarations line up.
-
Uniforms:
-
Thread group / dispatch:
-
With [numthreads(8,8,1)], choose dispatch counts as:
int groupsX = (width + 7) / 8;
int groupsY = (height + 7) / 8;
cs.Dispatch(kernel, groupsX, groupsY, 1);
-
That matches the bounds check pattern mentioned earlier.
-
Group configuration data:
Put all scalar params into a cbuffer for clarity and better packing.
-
Consistent naming:
PState, State, NState is fine; sometimes Prev, Curr, Next is a bit clearer.
- Avoid leading underscores in custom variables in Unity compute shaders to reduce confusion with built-ins.
-
Comment data ranges:
Note in comments whether fields are normalized (e.g. height in [-1,1] vs [0,1]), what D physically represents (damping coefficient), etc. Helps future you tune them.
- Fix texture access:
- Use
.Load(int3(id.xy, 0)) for Texture2D or convert to RWTexture2D if you want [] indexing.
- Fix
Sources type:
- Either use
Texture2D<float> or explicitly use Sources.Load(...).r etc.
- Rename
_Time:
- Avoid clashing with Unity’s built-in
_Time.
- Add bounds checks and size uniforms if you dispatch with padded group counts.
- Verify wave update uses a stable scheme (CFL condition) and that
Obstacles + edges are handled.
If you paste the full CSMain body (the part that’s currently ... in the file I see), I can walk through the numerical scheme step-by-step and check stability, artifacts, and optimization opportunities (e.g. using groupshared for neighbor fetches).