jes notes Index Gallery . Signed Distance Functions Shaft passers Snap issues

2025-04-04

Last modified: 2025-04-04 22:05:30

< 2025-04-03 2025-04-05 >

Selection

I think this should default to selecting the latest ancestor that only has 1 child. So then you're still targeting at the smallest areas of surface that you can (at least until we have sub-primitive surfaces), but applying modifiers works in the order you expect.

OK, that's done now.

I also made it always send the "nodeUnderCursor" id as the preselect id, so that the surface stays highlighted. It does still have the issue that it doesn't highlight the larger object if you scroll up to the larger object though, and the whole object doesn't show up as selected once you select it.

Edge detection

We can test a fixed distance away parallel to the viewing plane by shifting the point only in 2 axes instead of 3, this should solve the problem where thin regions get detected as fatter edges.

And also we can test for "edges" by looking for places where there are 2 (or more?) surface ids within that neighbourhood.

And this lines up perfectly with the types of things we can easily apply fillets to, so is exactly what the edges we want to be able to select.

And in the raymarch on hover, if we get 2 surface ids, then we can pass them both into the shader, to highlight the edge that is preselected, the same way we currently preselect surfaces.

More than 2 surface ids means a vertex, maybe could draw that differently.

This idea does work for detecting edges of selectable surfaces, but it still actually doesn't solve the problem of not having consistent line widths:

Fillet/chamfer continuum

Instead of "chamfer" being a bool - it should be a float, saying how much we interpolate from a smooth blend to a straight chamfer.

Easy to do, no reason not to.

And let's start calling them "blends" instead of fillets/chamfers.

Targeted blends

OK so I think I'm finally ready to do the "targeted blends" tool!

For now it will be:

And then we'll create a "Blend", somewhere slightly separate from the document tree (or attached to the root node?), that will say the 2 object ids, and will let you set a radius and a chamfer ratio.

And then we make it build a Peptide expression to tell you the blend parameters, and then we make all of the combinators use it automatically (with structmin/structmax).

And then think about optimising the expression so that we only evaluate the relevant parts at each node.

Actually before doing the UI I'll set up the "Blends" structure and populate it manually just to make sure it works. I'm not 100% sure it won't create discontinuities in the distance field.

  1. First issue is that a smoothblend with 0 blendRadius gives broken normals. Deal with that later.
  2. the "blends" array will create duplicate references when serialised
  3. max() refers to app.document.blends instead of walking the tree to find it
  4. should use uniforms for the blend radii

It is working though!

Can I come up with an example where this lets us do a targeted blend that we couldn't do just with setting the blendRadius on a combinator? Or does that only come once I can target sub-primitive surfaces?

I think if I make a union(A,B) and a union(C,D) and then an intersection of the 2 unions, then this would allow me to blend the intersection of A and C for example.

OK, I have a union(Sphere,Box) intersected with a Gyroid, with a blendRadius only on the (Sphere,Gyroid) interface. And it is indeed discontinuous where the blendRadius changes:

So what can I do about that?

What if the logic is all smooth logic instead of step functions?

Well I made the step function smooth, and now the chamfer between gyroid and sphere looks like this:

It has a ridge at the transition.

But it goes away if I make the step function sharper!

I think park the smooth logic thing for now. What else could I do?

The issue is that blends are permuting the distance field, so if they don't apply everywhere then either there are discontinuities, or they need to smoothly drop off at the boundary.

How do you tell if you're close to the boundary?

Could we rewrite the tree so that each combinator always has a constant blend radius?

If we want a blend between Sphere and Gyroid, then:

Intersection(Union(Sphere,Box), Gyroid)

becomes:

Union(Intersection(Sphere, Gyroid), Intersection(Box, Gyroid))

And that actually works and gives a good distance field. But how to do that automatically in the general case? There may be modifiers on top of any of the primitives or the combinators.

ChatGPT tells me that it actually is possible to do this in the general case.

I think we build up the Peptide expression for the entire object, and somehow include specially-tagged min/max nodes where blends are required, and then then rewrite the Peptide expression so that the blends always have consistent parameters.

Hmm, can't quite see it. In particular, all the P.struct tagging is lost in the Peptide expression, it all gets short-circuited.

So I've changed my mind, let's work on the tree of TreeNodes instead. Clone the document, label each node with a "surface id", which is the unique id of the leaf node, if it is a straight line to a leaf (i.e. either a leaf, or a stack of modifiers or no-op combinators on top of a leaf).

And then treat the "top" of that stack as if it's a leaf.

Then if the document is top-level combinators over a bunch of leafs, it's easy.

What if there are modifiers applied to combinators? Work out how distributivity would work there and somehow rearrange the tree so that each combinator only has one blend setting?

ChatGPT says Modifier(Combinator(a, b)) is equivalent to Combinator(Modifier(a), Modifier(b)). Big if true, because then we actually can turn the tree into Combinators of Modifiers of Primitives, which makes rewriting for consistent blends easy (?).

If we imagine that we want to allow a different blend for every pair of surfaces, how would we do that in the general case?

Apparently exploding the tree via distributivity is enough.

I can do some quick tests to see if Modifier(Combinator(a, b)) truly is equivalent to Combinator(Modifier(a), Modifier(b)).

Looks equivalent for Union/Twist.

Ah, OK. This is only true with no blend applied. When you apply a blend, there is a difference between blending the union of the twists, and twisting the blended union.

So what else?

What if we also propagate another value up the tree saying how far a point in space is away from the nearest 3rd surface that didn't become a child of min/max?

And then we smoothly drop off the blend parameters if the 3rd surface is too close?

That won't be a full solution, because if the blend radius with the 3rd surface should also be the same, we'd erroneously drop off to 0 blend radius at the point where the 3 surfaces intersect. But it may be a step forwards.

First impression is this actually looks to work pretty well!

We do need another parameter now, which controls "how fast do we taper off our blend near the third surface". And we may also want to know the third surface id, so that instead of tapering to 0 we can taper to some weighted average of A's blend with C and B's blend with C.

I'll just set it to smoothly blend out over 5x the blend radius for now.

Also there is a weird glitch line along the boundary of the box, suggests the function is slightly discontinuous? It's because the 1e-10 constant in smoothabs is too small. Changed to 1e-5 and looks better now.

So now I think I want some UI to let me experiment with these better?

Hmm, I actually don't like this. I feel like the artifacts you get from filleting a twist instead of twisting a fillet are not as bad as the artifacts you get from blending out the fillet just because it goes near a piece of space that some other geometry also went near.

OK, so let's say we want to do the tree rewriting thing. Can we do that in a way that does still keep all the modifiers in the right places?

Let's consider:

Intersection(Twist(Union(Sphere,Box)), Gyroid)

With fillet between Sphere and Gyroid.

That should become:

Union(Intersection(Twist(Sphere), Gyroid), Intersection(Twist(Box), Gyroid))

Twist(Sphere) keeps the surface id of Sphere intact so is fine to do.

We need to rewrite the tree so that the modifiers always take the same blend parameters no matter which of the possible surface ids come out of each child.

So what would the tree rewriting algorithm look like?

First, label all nodes with the set of surface ids they can yield.

Starting from the root, you get to the Intersection node. You notice that the surface ids of the left child are Sphere and Box, and for the right child is only Gyroid. The blend radius for (Sphere,Gyroid) is different to (Box,Gyroid), so we need to rewrite.

Go down the left branch until we find the node that takes those ids from its children. We find that we visited Twist and Union.

So move Union to the top, make 2 Intersections, and put the Twist directly on top of the 2 children of Union.

In general:

Intersection(ModifierChain(Union(A,B)), C)

becomes:

Union(Intersection(ModifierChain(A), C), Intersection(ModifierChain(B), C))

No. Wrong. Because we already agreed that Twist(Union(A,B)) is not the same as Union(Twist(A), Twist(B)) once the union has a blend applied.

Maybe the operation I'm asking for doesn't even exist.

I should work it out in 1 dimension on https://www.shadertoy.com/view/ltf3W2

I want to draw 3 curves a,b,c on the screen, and also plot min(min(a,b), c), but with a blend between a and c but not between a and b.

You can see the green line is discontinuous. That comes from picking the blend radius based solely on the line segment ids.

So we're basically wanting sharp corners, except the join between the parabola and one of the lines is rounded:

So like that, but no discontinuities.

Can't figure it out.

< 2025-04-03 2025-04-05 >