Last modified: 2025-04-14 20:34:31
< 2025-04-13 2025-04-15 >The solution to the "bulging blends" problem:
is to make Box's out of Intersections of half-spaces, and then we can apply blends between individual pairs of half-spaces:
Current hacky implementation is on the "half-spaces-fiddle" branch, with issues:
radius
parameterSo I want to solve these issues.
For surface ids changing every time the scene is recompiled, perhaps the BoxNode
initialises
its 6 HalfSpaces and its Intersection when it is constructed, and always returns the same
ones in makeNormalised()
? And makeNormalised()
just needs to set the sizes etc.
I could tolerate losing the radius
parameter, as long as blending all the surfaces works.
How could hover/select on cube surfaces work? We kind of need to return 2 different ids: one for the surface you're on, and one for the object that that surface belongs to.
Hovering on a face of the cube will yield the id of that surface. So we need
document.findNodeById(...)
to come up with the BoxNode if you give it an id that belongs
to one of the BoxNode's HalfSpaces.
We could make uniqueIds()
return the ids of all the HalfSpaces? And make findNodeById()
look in uniqueIds()
for extra ids?
And to fix the distance field... how is it actually different now to what it was before?
Now it is max()
of 6 "half-spaces" which are all like:
p.x-halfSize
And previously it was:
d = abs(p)-halfSize
vlength(vmax(d, vec3(0,0,0)) + min(max(d.x,d.y,d.z), 0)
I don't immediately understand why this works off the top of my head. But thoughts are:
If the point is inside the box, vmax(d, vec3(0,0,0))
gives you vec3(0,0,0)
because d
is
negative where inside and positive outside. And then min(max(d.x,d.y,d.z), 0)
gvies you
the coordinate which is closest to the surface. Which is what you want.
If the point is outside the box, vmax(d, vec3(0,0,0))
gives you 0's for coordinates
that are within the footprint of the box, and distance to the box surface for other
coordinates. So then vlength()
of that gives you the distance to the nearest edge or
surface or corner. And min(max(d.x,d.y,d.z), 0)
will be 0 if any of the coordinates are
positive.
So the Intersection is max(d.x,d.y,d.z)
. min()
with 0 just masks that out for
points outside the shape.
So the way it works is we decide whether the point is inside or outside. If inside,
take max()
of 6 half-spaces. If outside, take the length of the axes that are outside
their respective half-space.
So how do we generalise that?
By defining:
union(a,b) = max(min(a,b),0) - length(vec2(min(a,0), min(b,0)))
intersection(a,b) = min(max(a,b), 0) + length(vec2(max(a,0), max(b,0)))
We get:
Which is obviously not quite right, but it's almost there!
Actually, it is exactly right, until we start applying blends. If I remove the blends then I get this:
So, why does adding blends affect it? Obviously it leads to rewriting the tree, but why
is that a problem? The union()
/intersection()
functions are ignoring the blend radius.
I guess they no longer distribute over each other when we're adding on the lengths?
Forget it for now, come back to it.
So we need findNodeById()
to look in uniqueIds()
(which btw should probably be called
surfaceIds()
or something). And then we also need to track which particular surface id
is hovered, so that we can highlight the right one.
We can highlight the right surface by highlighting renderer.rayMarchResult.uniqueId
.
How do we allow selection to handle both a selected surface and a selected object?
"Add blend" needs to add it to the surface, everything else needs to work with the object.
Working, super cool: https://www.youtube.com/watch?v=LOvqdlDbkBs
Kind of a bad video because it seems to have dropped some frames at crucial times so it looks like I clicked on the wrong thing.
There is obviously some issue with the chamfer. And it doesn't manage to add a blend that intersects with an existing blend.
The only remaining issues from the "half-spaces-fiddle" thing are that BoxNode no longer respects its radius, and the distance field of a Box is now broken.
Can I implement "radius" by just setting the blendRadius
on the IntersectionNode
?
Yes, looks like it, although it interacts badly with targeted blends:
The upper right one has a 1mm radius on the Box. The other one has no radius on the Box, but there is a targeted blend between the two boxes.
The bad chamfer was caused by using too much sharpness on chmax
. I've put it back down
from 0.98 to 0.90 and the problem is gone. Kind of not ideal to have rounded chamfers
though.
Maybe we don't care too much about cubes having broken distance fields? As long as they work consistently it's probably fine. And if you do an Offset and the corners are sharp and you want round ones, just fillet it afterwards?
I've merged "half-spaces-fiddle" into master because this is definitely how I want it to work.
One issue is that when you select a Box in the tree it no longer highlights it on the 3d view, because it would need to select the surface ids instead of the Box id. Also we don't support multiple select. How would you do that?
Have the SDF struct propagate up a field saying whether it is hovered and whether it is selected. And then you can hover/select either individual surfaces or compound objects, and multiple select works as well.
The field of the struct would be filled in by treenode.js
in peptide()
or whatever,
and would be a constant value either 0 or 1 based on whether the object is selected.
And combinators would propagate up the "selected" value of the chosen surface.
Actually it would be a uniform that gets set to either 0 or 1 based on whether the object
is selected.
But, kick that can down the road. I think this is an acceptable solution for "blends bulge out on parallel surfaces".
I wonder if I can look at the individual object normals to work out how much of the "length" thing to do, to fix the distance field? So instead of making them perpendicular and taking the length, I should multiply each one by its gradient, sum them, and take the length of the result?
The secondary object is no longer working, and I can't quite remember why.
It's because of the normalisation and tree rewriting thing. I just need to copy the "processedDocument" logic to "processedSecondaryNode" or something.
Hmm, still not quite working right. In the mode where it shows the field cross section it looks right, but when it is meant to show a red transparent object, it is just drawing a transparent white rectangle over the entire screen.
Ah! It was missing the lipschitz
field in the struct. Sorted now.
And now it is irksome that the secondary object doesn't show up in the right place.
How do we fix that? Collect up all of the Transform
nodes between it and the root
and apply them anyway? Or the more general thing I used to have where each node can
declare whether it needs to apply to the secondary?
Also when the mouse hover goes off the selected object it somehow gets unselected?
Ah, that's only for boxes. It's because it unselects the node if the selected node does not exist in the tree. Why doesn't the selected node exist in the tree?
Oh! Because it doesn't report its own surfaceId
in surfaceIds()
!
Good, fixed now.
< 2025-04-13 2025-04-15 >