Last modified: 2025-04-02 21:54:04
< 2025-04-01 2025-04-03 >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:
Remaining core problems:
VoxelNode
just classify voxels as "in" or "out" and then propagate distances from there - so that we can use it for renormalisationuniqueId
the distance came from)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 struct
s 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.
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:
TreeNode
's getSDF()
will wrap the Peptide expression in P.field(expr, 'distance')
so that it works like it currently doesTreeNode.min
/max
will have to choose/blend coloursTreeNode
has to return a Peptide struct instead of floatColorNode
for changing object coloursWhoa, 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:
TreeNode
types return structsThe 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.
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 TreeNode
s would be good.
So then the hard part is how to look up blend radiuses by the pair of
TreeNode
s? 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
?
uniqueId
in SDFWhoa, 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:
BoxNode
for exampleAnd 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.
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 >