Last modified: 2025-03-20 20:27:07
< 2025-03-18 2025-03-20 >Next thing is unit testing GLSL code.
So the plan is a new test harness that sets up Peptide expressions like before, then compiles to GLSL, writes a fragment shader that includes the generated GLSL code, sets up a WebGL context, runs the fragment shader outputting to some random texture, and then reads in the texture to check that the colours are correct.
Cursor seems to have basically got the entire thing right on the first try. What incredible technology we have. This would have taken me hours!
It didn't get it 100% right but it got all the annoying stuff done. It set up WebGL etc. for me, I just need to manually fix up the interface between my generated GLSL code and the test harness's GLSL code.
Whoa, cool, it's working! It even sets "variables" as uniforms, this is exactly what I need.
And it has a debugging feature where it can show me the generated GLSL code for failed tests.
Also it occurred to me that maybe my "register allocation" thing is
actually completely unnecessary. The GLSL compiler will do its own
version of that, giving the GLSL compiler SSA code is probably actually
more helpful overall? Can always experiment later, see if it compiles/runs
faster if I delete the greedyAllocate()
call.
The next thing I could do is use Peptide in some of the SDF
nodes. Make generateShaderImplementation()
compile GLSL from a Peptide
expression in just one or two special cases. And later on make
the whole TreeNode
structure use Peptide.
And only when the whole thing is using Peptide do I make it use interval arithmetic.
For a sphere: return length(p)-r
:
generateShaderImplementation() {
const expr = P.sub(P.vlength(P.vvar('p')), P.const(this.radius));
const ssa = new PeptideSSA(expr);
return ssa.compileToGLSL(float ${this.getFunctionName()}(vec3 p)
);
}
Seems to be working!
So how are compile times looking?
Default scene was: 297 ms.
Now is: 294 ms.
So, not difference.
Now what if I change more of the objects?
With box: 297 ms. No change.
With transform: 301 ms.
What about subtraction?
It would be easier to do combinators if I already had all the child nodes in Peptide expressions, because then we could compile them statically instead of via function calls.
With subtraction: 305 ms.
It is looking like this is slightly more costly to compile than the old version. Not by a lot though.
What about SketchNode?
This syntax is getting pretty gnarly.
s = P.mix(s, P.neg(s), P.min(P.const(1.0),
P.add(P.mul(P.step(vay, py), P.mul(P.step(py, P.sub(vby, eps)), P.step(P.const(0.0), ds.y))),
P.mul(P.step(vby, py), P.mul(P.step(py, P.sub(vay, eps)), P.step(ds.y, P.const(0.0)))))
));
I might want a way to do "chaining" instead of instantiating
everythin with P.foo(...)
.
Whoa, it's working! Best compilation time was 301 ms.
I'll try and make this actually compose Peptide expressions now.
So each TreeNode
should have a function that takes an argument that
is the expression for the point in space, and returns an
expression that calculates the distance.
And ideally we could support interaction between both types so that I don't have to convert everything all at once.
So SphereNode now is:
peptide(p) { return P.sub(P.vlength(p), P.const(this.radius)); }
generateShaderImplementation() {
// length(p) - radius
const expr = this.peptide(P.vvar('p'));
const ssa = new PeptideSSA(expr);
return ssa.compileToGLSL(float ${this.getFunctionName()}(vec3 p)
);
}
OK, and next step is don't even provide generateShaderImplementation()
etc., just make TreeNode
evaluate this.peptide(P.vvar('p'))
and then
compile to GLSL!
One thought is that when I am setting vectors etc. with uniforms, then the bounding spheres won't be static any more. So we may have to ditch bounding spheres, which also maybe means ditching domain repetition?
But it might be that short-circuiting min/max in the binary search is more valuable than domain repetition anyway? Seems unlikely, at least in some cases. So maybe domain repetition just becomes an option in the UI? And if it glitches your shape you can't use it, otherwise you can.
Turning the entire scene into a single Peptide expression is working! And I'm actually deleting more code than I'm adding, this is great.
OK, I now have the default object rendered entirely using Peptide!
The important question: how does performance compare? Best I saw for compile time was 298 ms for the whole shape, which is basically identical to before.
The code generation time is now about 40ms instead of 0ms, but I can live with that.
At least the shader compile time isn't substantially slower.
And how are frame rates? With it loaded up at the default scene, with dev tools open, it stabilises at 46 fps at resolution scale 5.972.
With the old code it was 46 fps at resolution scale 5.982. So close enough to being unchanged.
So the situation is we haven't lost any performance in terms of frame rate, we've lost a tiny bit on shader compile time, but we can now evaluate SDFs in JavaScript! So I could potentially add the coordinates of the surface underneath the cursor.
I need to convert some remaining node types to use Peptide, and then can experiment with evaluating the expressions in JavaScript.
The tricky ones are the ones that use rotateToAxis()
etc. to
transform space so that they can act in the Z axis. I need to either
write out all the matrix multiplication stuff the long way,
or else add matrices to Peptide.
Some optimisations that would be good to try out:
I tried skipping register allocation and it didn't seem to make a lot of difference either way. So, not sure.
Is there a way to compute bounding volumes with Peptide?
Or, can you work out if 2 shapes overlap by evaluating them over some large cube and see if the resulting interval crosses 0? I know that tells you that a ray intersects a shape if you only use an interval for one axis, but if you do it for all 3 axes does it tell you if they do overlap, or merely if they might overlap?
For example if I have a unit sphere at (1,1) and another at (3,3), then they don't overlap, but if I evaluate over the interval (0..4, 0..4) then there are points in that interval that are inside both shapes, so I think the resulting interval would cross 0? Have I got that right?
Is there some n-dimensional analogue of interval arithmetic? Or do you just have to recursively subdivide until you're evaluating some small-enough region?
Really all I want bounding volumes for is to work out how to do patterns. You need them even for naive domain repetition because you need to know where the centre is, to work out how to transform the coordinate to evaluate the child.
I've removed all of the shader code for drawing the secondary object.
I also made the shader leave the background transparent where there is nothing to draw.
So the plan will be to draw the secondary object with a second shader drawing on top of the first.
I may need to untangle all the main/renderer/scene interactions first though.
< 2025-03-18 2025-03-20 >