shisaku ~/devlog
$ auth github

Author sign in

Sign in with GitHub to comment on devlog entries.

Continue with GitHub
← all posts

Devlog #10 - Shaders and the Fine Art of Making the GPU Little Less Annoyed

**\*\*Focus:\*\*** Optimising shader work without sanding off the magic, and admitting that "just make it look better" is how you accidentally write a small weather system inside a fragment shader.

Devlog #10 - Shaders and the Fine Art of Making the GPU Little Less Annoyed

<br />

Honest status first

This is a devlog about an uncomfortable little truth:

Making shaders prettier is easy.

Making shaders prettier while they still compile everywhere, remain moddable,

fit inside a survivors-like screen full of effects, and do not slowly turn the

frame budget into wet cardboard is the actual work.

The recent shader pass is still in the working tree along with documentation

and version metadata. It has been checked, the shader loader accepted the full

set, and mod_check is happy. But it is not a magic certificate from the GPU

gods. It means the work is coherent enough to inspect, tune, and criticize.

Which is, frankly, the most useful state for shader work anyway.


The trap: “more detail” is not a plan

The user-facing request was clear: the healing ray should feel closer to a

high-end chain heal style effect. Not a lazy green line. Not a stock beam with

a halo and a hopeful name. A support spell with travel, hop intention, impact,

warmth, and the slightly smug visual confidence of something that knows it is

restoring health at exactly the dramatic moment.

The obvious move is to add layers:

And visually, that is the right instinct. The problem is that every one of those

layers wants math. Distance fields, noise, loops, curve samples, sine gates,

segment projections, bloom falloffs. The GPU will absolutely do all of this,

because the GPU is very brave and very literal. It will also do it for every

fragment of every effect quad, at the same time, while the player is moving and

fifty enemies are reconsidering their life choices.

So the job became less “add more stuff” and more:

> Which details actually sell the spell, and which details are just me paying

> rent to the instruction counter?

Deeply glamorous work. Truly, the red carpet of arithmetic.


The first useful rule: make the silhouette earn its cost

Good VFX reads from far away first.

If the only thing a shader adds is texture inside a blob, it may look nicer in

a screenshot and still fail during play. EchoWarrior needs readable chaos, which

means the big shape matters more than the microscopic wiggle.

For the healing ray, the readable shape is now:

Those are silhouette and motion layers. They communicate “healing chain” before

anyone notices the exact noise function. That is the useful kind of expensive.

The same thinking applies to the other shaders:

If a layer does not help the effect read faster, it has to justify itself very

carefully. Preferably in writing, where it can feel embarrassed.


The second useful rule: shared math is design, not tidiness

Several shaders needed the same little geometric operation: distance from a

fragment to a line segment. Forks, barbs, leap hints, lashes. Same problem, same

math, slightly different local copies.

That is not just duplication. It is future visual drift.

If every shader carries a private version of “distance to a segment,” then the

next pass changes one of them, forgets two of them, and accidentally invents

three dialects of the same spell language. Very artisanal. Very cursed.

So the shared arcane.glsl prelude now includes:


float aw_segment_distance(vec2 p, vec2 a, vec2 b) { /* ... */ }

and for traveling light packets:


float aw_packet_train(float path_t, float progress, float offset, float width, float sharp) { /* ... */ }

That second one is small, but important. Travel pulses are one of those details

that make an effect feel alive. They also invite everyone to write their own

slightly broken fract() expression. Now custom shaders get the good version

by asking for the shared prelude.

This is shader optimisation in the unromantic sense: not only fewer repeated

instructions, but fewer repeated ideas.


The Bezier problem, or, “how many samples is enough?”

The healing ray is curved, so each fragment needs to know how close it is to the

curve. There is no free lunch there. You sample points along the curve, find the

nearest, and build the beam from that distance.

The old version took a fixed set of curve samples. That is straightforward and

pleasantly blunt, like measuring a room by throwing rulers at it.

The refinement changed the search to:

  1. take a coarse pass across the whole curve

  2. find the best candidate

  3. sample again near that candidate

So instead of spending every sample evenly across places the fragment probably

isn’t near, the shader concentrates its second pass where it matters. It is not

a grand algorithmic revelation. It is just the shader equivalent of looking

where your keys actually fell instead of politely inspecting the entire house.

The win is modest but real:

There is a lesson in there, unfortunately: the fastest shader is often the one

where you stop doing confident nonsense.


Optimisation did not mean making it boring

This is the part I want to be careful about.

Optimising shaders can turn into a joyless little purge. Remove the motes.

Remove the second strand. Remove the pulse. Remove the bloom. Congratulations,

the frame is cheap and the spell looks like a spreadsheet had a medical event.

That is not the goal.

The goal is to keep the expensive details that players perceive:

and reduce the boring cost underneath:

The shader should still feel a little extravagant. It just should not be

extravagant in ways no one can see.


Vulkan, because of course the word entered the room

While doing this, we also looked at Vulkan.

The short version: under the current stack, Vulkan is not a switch.

EchoWarrior currently uses Macroquad 0.4.14, which rides on Miniquad 0.4.8.

Miniquad supports OpenGL/GLES/WebGL and Metal on Apple platforms. Its shader

path is GLSL for GL and MSL for Metal. There is no “please be Vulkan now” flag

hiding in the pantry.

So the practical Vulkan route is not “turn it on.” It is:

  egui, and shader materials

Which is a lot. Not impossible. Just not a weekend toggle, unless the weekend

is six months long and has strong opinions about WGSL.


What was verified

For the shader refinement pass:

The smoke test matters because mod_check can prove the manifest and uniform

contracts are coherent, but the graphics driver gets the final vote on whether

the GLSL is real. Drivers are not known for their interest in our feelings.


The humbling part

Shader optimisation is a special kind of humility.

You start with the heroic idea that you are painting light. Then you spend an

hour moving a fract() expression into a helper because three shaders were each

being annoying in their own bespoke way.

You want the effect to look expensive. The trick is making it look expensive in

the places humans actually see, and cheap in all the places only the GPU has to

suffer through.

That is the work now: keep the magic lush, readable, and moddable, while slowly

teaching the shader set to stop doing the same tiny stupid thing sixteen times.

Next: either tune the pixels with human eyes on the running game, or begin the

renderer-boundary work that would make a future Vulkan path less like replacing

the floor while the house is politely on fire.

$ comments

Reader notes

0 notes

No notes yet.

If you're reading this as a developer: this devlog is built in the open.

If you're reading this as a modder: the direction is source-visible, inspectable systems.