One SDF core, two dialects, four traps

A field is just arithmetic, so the same functions should work in GLSL for a web prototype and in HLSL for the engine build. That is almost true, and the ways it is not true share an unpleasant property: they compile.

The same domain repetition. Left: HLSL's fmod semantics. Right: GLSL's mod. The orange lines are the axes.

The top-right quadrant of each is identical. Everywhere else, the left half is empty. Three quarters of the tiling is simply gone, because fmod returns a negative remainder for negative input, so the folded coordinate lands outside the cell the shape occupies and nothing is ever drawn there.

Nothing errors. The shader compiles, the shape draws, and one quadrant is correct, which is exactly enough to convince you the port worked. If the camera happens to start at the origin looking up and right, it can survive a long way into production.

The four that actually bite

1. mod against fmod

GLSL's mod(x,y) is x − y*floor(x/y), so its result takes the sign of the divisor and is always positive for a positive cell size. HLSL's fmod is x − y*trunc(x/y), taking the sign of the dividend.

// HLSL, the correct port of GLSL mod():
float glslMod(float x, float y) { return x - y * floor(x / y); }

Every domain repetition, every tiling, every "wrap this coordinate" depends on this, so it is the one to fix first and fix globally. Define glslMod once and never call fmod in ported code.

2. Matrix multiplication order

GLSL is column-major with M * v. HLSL is row-major with mul(v, M). Get it backwards and you get the transpose, which for a rotation is a rotation the other way: entirely plausible, silently wrong, and it looks like a sign error in your angle rather than a convention mismatch.

3. Integer division and bit operations on old targets

Fine on modern shader models, and a genuine trap if the Unity target is older or the WebGL2 path is still live. Anything hashing with integer operations is where this shows up, and the failure is a different noise pattern rather than an error.

4. Precision defaults

A mobile GLSL target defaults to mediump in the fragment stage, and a distance field wants highp. HLSL has no equivalent default to trip over, so the same code is fine in the engine build and banded on the web build, which is a difference nobody looks for because the web build was the prototype.

The names, which are the easy half

GLSLHLSLNote
mixlerpidentical
fractfracidentical
modfmodNOT identical
texturetex2D / .Samplesampler model differs
dFdxddxidentical
vec3float3identical
mat3float3x3multiply order differs
inversesqrtrsqrtidentical
atan(y,x)atan2(y,x)argument order is the same

A rename table is a search and replace, and it is not where the time goes. The four above are where the time goes.

Keeping one source rather than two

The approach that survives contact: write the core in a subset both dialects accept, and put every difference behind a macro in one header.

#ifdef HLSL
  #define vec2 float2
  #define mix  lerp
  #define fract frac
  float glslMod(float x, float y){ return x - y*floor(x/y); }
  #define mod glslMod
#endif

Then the SDF primitives, the blends, the deformers and the easing curves are one file that both builds include, and only the entry points differ. The discipline that makes it work is that the shared file may not contain a texture read, a uniform declaration or anything about the pipeline. It is pure arithmetic in, arithmetic out, which is what a distance function is anyway.

The payoff is that a web prototype is not a throwaway. The shape you tuned in a browser at two in the morning is byte-for-byte the shape in the engine, and a change to either is a change to both.

Rules of thumb

  1. fmod is not mod. Define glslMod once and never call fmod in ported code.
  2. The dangerous differences compile. Assume a clean build means nothing about correctness.
  3. Test the port on negative coordinates. Half these bugs live entirely at x below zero.
  4. Matrix multiply order is reversed. A wrong rotation direction is the symptom.
  5. Keep the shared file free of textures, uniforms and pipeline concerns. Arithmetic only.
  6. Force highp in the fragment stage on the web side, or the two builds band differently.

All 61 notes How to use them Credits