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 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
| GLSL | HLSL | Note |
|---|---|---|
mix | lerp | identical |
fract | frac | identical |
mod | fmod | NOT identical |
texture | tex2D / .Sample | sampler model differs |
dFdx | ddx | identical |
vec3 | float3 | identical |
mat3 | float3x3 | multiply order differs |
inversesqrt | rsqrt | identical |
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
fmodis notmod. DefineglslModonce and never callfmodin ported code.- The dangerous differences compile. Assume a clean build means nothing about correctness.
- Test the port on negative coordinates. Half these bugs live entirely at x below zero.
- Matrix multiply order is reversed. A wrong rotation direction is the symptom.
- Keep the shared file free of textures, uniforms and pipeline concerns. Arithmetic only.
- Force
highpin the fragment stage on the web side, or the two builds band differently.
One email when something new goes up. No newsletter, no schedule, nothing else.
Double opt-in, so watch for a confirmation email. Unsubscribe any time.