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

2025-04-02

Last modified: 2025-04-02 21:54:04

< 2025-04-01 2025-04-03 >

Clock

This has been running for over 36 hours without stopping, but I connected up the clockwatcher at some point yesterday after it had already been running overnight and part of the day.

The amplitude drops precipitously at one point, I'm guessing the remontoire didn't rewind properly.

I think the plan is:

Isoform core problems

Remaining core problems:

Peptide structs

You can get most of the way here by passing a plain JavaScript object that has multiple Peptide expressions in it.

The problem is that when you turn it in to JS/GLSL code, you get a scalar out at the end instead of a struct.

So then let's imagine P.struct({a: ..., b: ..., c: ...}) where all the "..." are Peptide expressions. In JS code we can simply return an object with fields that are the evaluations of the expressions. In GLSL we will need to declare the struct somewhere.

Probably the only struct we need to declare is the one that is finally returned. Any intermediate structs are better off just disappearing into the expression tree.

So another function P.field(struct, name) would select out the named field from the struct.

And for the SDFs we'd want fields:

TreeNode.min/max can automatically select uniqueId, and interpolate colour. But everything else that has to deal with a child node is now going to have to laboriously track the colour and uniqueId, and extract the distance separately.

So another way is we could add "metadata" to Peptide nodes. And when you construct a node it will automatically inherit metadata from its first child that has any, but let you override it if you want.

Primitives will have to explicitly set their metadata. Modifiers of 1 child can probably ignore it completely. Combinators may have to combine them sensibly, but may get it for free if TreeNode.min/max does it.

And then at code generation, sometimes we'll want the struct with all the metadata, and sometimes we'll just want the primary value. Maybe just always give the struct, and you have to get the primary value if that's what you want?

Carrying metadata around statically won't work, because it won't allow selecting between nodes at runtime.

So we basically do want the explicit P.struct thing.

And I think P.field needs to look "through" the struct, so that a tree that looks up fields in structs can still be handled properly by the simplify() code that assumes the children are all referenced by left/right/third.

And then we'll say the only things that you can do with structs is access their fields and evaluate them as a root node. And then nothing else cares about it. And since "accessing their fields" just shortcuts the struct and gives back the field directly, the only place a struct node can be is the root of the tree.

So then do we special-case structs in code generation so that it knows about all of the children? Or do we say that any node can have arbitrarily-many children, just so that code generation handles it automatically?

Maybe we make the calling code specifically know about structs, so that it can declare them etc. in the surrounding GLSL code? Or at least know what type name to use?

OK, I have P.struct/P.field working in the "interpreted" case, with a unit test checking that common subexpression elimination works.

Now how to handle structs for compiled code?

What I want to get is SSA code that computes each field individually and then return a struct of them all at the end.

So probably I "hide" the struct from the code generation, but remember which value corresponds to which field, and then if the root node was a struct, I construct it and return it right at the end, otherwise just return the last value.

Great, that's working for JS code. Now need JS intervals, GLSL, GLSL intervals.

For propagating material information (starting with just colour), I think we'll let each TreeNode optionall set a "colour" vec3, and if it is set then its peptide() will overwrite the colour generated by makePeptide(), otherwise it will leave it blank.

And maybe primitives will always have to pick the default colour if not set?

Actually to start with let's always just propagate the child node's colour, and make a ColourNode which overwrites it, similar to how TransformNode is its own node type but will eventually be common to all.

But I'm getting ahead of myself, still need GLSL structs.

GLSL has no way for me to return a populated struct without knowing the name of the struct. So I think the caller will need to tell me the name of the struct if the expression is a struct.

OK, great success, I now have working tests for all combinations of JavaScript/GLSL and normal/interval mode, with structs.

Object colours

The first useful thing to do with structs is let objects have different colours.

So that will mean changing the way Peptide is used in TreeNode so that instead of just yielding a distance, it instead yields a struct with:

We'll accept the American spelling of "color" to make it easier for Claude.

And then:

Whoa, working!

The sphere has one colour and the gyroid has the other, and it correctly colours the surfaces based on which primitive they came from.

The "register allocation" in SSA seems like it needs to know about structs, because I only got it to work by turning that off.

It's not blending colours yet, it just thresholds them at whichever distance is min/max:

OK, sweet, blending colours now:

In the long-term I expect not to care very much about blending colours, because I'll be using different colours for different conceptual "objects", rather than within a single object. But still cool to have and easy to do.

At some point I could probably do with more robust type checking on structs, to make sure they all have all the fields. Or alternatively make TreeNode have a struct constructor that automatically sets default values for fields you're missing?

Still need to fix:

The register allocation problem was that we were renaming the variables that were the result of the computations of the struct fields, but still using the original names when assigning to the resulting struct.

So if a field was originally called f1234, and greedyAllocate() renamed it to f0, we'd still be trying to use f1234.

The solution to the "type safety" thing is to have TreeNode.peptide() check everything, and fill in default values where applicable.

I think I've made all the other node types return structs now.

Targeted fillets

An edge is always formed by a discontinuity. (Fact).

A discontinuity is always formed by a min or max of 2 more fundamental objects. (Maybe? But maybe the idea is useful even if not).

Therefore we can target fillets by choosing a blendRadius based on the uniqueId of the 2 surfaces being blended.

Define a "surface query" as the "coordinates under cursor" thing, except it gives you the surface uniqueId instead of the coordinates.

So the UI could be that you click on the first surface, then click on the second surface, then select your blend radius. Or else you click on the edge and then it does "surface queries" in some small-ish neighbourhood (either in screen space or object space) and picks the 2 closest surfaces to the click point?

This does mean that all min/max operations that want to support targeted fillets need to have separate uniqueId for each of the child surfaces. For example a cube is the intersection of 6 half-spaces. If each half-space has a different id then we can potentially set a different blend radius on each edge.

As a first pass, just being able to set a different blend radius on each pair of TreeNodes would be good.

So then the hard part is how to look up blend radiuses by the pair of TreeNodes? Given that one or both of the arguments could vary at runtime.

Do we have to make a big "blend radius selector" expression that is like:

blendRadius = (a.id == 0 && b.id == 1 ? 0.1
            : (a.id == 1 && b.id == 2 ? 0.5
            : (a.id == 1 && b.id == 3 ? 1.0
            : (a.id == 1 && b.id == 4 ? 0.5
            : 0.0
            ))));

And we could either use the same one everywhere (although it won't prune very well because a and b are dynamic), or have a function in TreeNode that computes only the subset of the blendRadius expression that is applicable for its own subtrees.

And then a policy question, but not a "core problem", is how this interacts with nodes that already have their own blendRadius?

Click to select

  1. Propagate node uniqueId in SDF
  2. In the ray march on hover, remember the id you hit
  3. On click, select the hovered id

Whoa, done, that was actually really easy!

Right-clicking on the 3d view should pop up the same context menu as on the tree view.

Also easy! Great success, this feels like all the complexity I've been building up is finally paying off.

One issue is that you can only right click on your base primitives, but it feels like after you apply a modifier to it, right-clicking should let you stack something else on top of the modifier, rather than underneath it.

One idea is we could let you collapse nodes in the tree to make them into "false leafs", and when we find that the node under the cursor is collapsed (or inside a collapsed subtree), we walk up until we find its first parent that is actually visible in the tree view, and select that one instead?

And also, any time we add a new thing, we collapse it, so that its children become unselectable?

Another thing would be "multiple select", and then applying some combinator to the multiple selection. I think they would have to be sibling nodes.

Or maybe we only allow click-to-select of top-level objects under Document? So we'll return 2 ids:

And then for click-to-select we use the top-level id, and for targeted fillets we use the bottom one.

Also if you right-click somewhere and add a new object, it could maybe make half an effort at placing the object in the place where you clicked.

Highlight on hover

It would be cool to highlight the objects on hover. We could give the shader a uniform to say the uniqueId under the cursor, and any pixels that land on that uniqueId get coloured brighter or something?

Nice, working. I made it mix in a green colour if the hit surface is the same a the hovered surface.

< 2025-04-01 2025-04-03 >