discuss@lists.openscad.org

OpenSCAD general discussion Mailing-list

View all threads

volumetric color

JB
Jordan Brown
Tue, Dec 30, 2025 10:44 PM

On 12/30/2025 12:53 PM, Cory Cross via Discuss wrote:

Right, but that's why I want to export just the colored face and not
change the interior of model. Let slicer handle the realization and
don't make me recolor it every time I reload my model.

And so that's an argument for retaining face coloring.

But it's not an argument for not also handling volumetric coloring, so
that these various cases can be well-defined, should you choose to
specify them in the design tool.  As you note, you definitely do want to
control the interior when you're talking about multiple materials, and
that's the same as controlling filament color.

(Well, excluding dynamic-mixing print technologies, where you can do
gradients.)

[ For coincident faces ] Theoretically, I think the answer should be
the same as for volumes, which means either first-wins or last-wins,
and for me that coin came down on first-wins.

Sadly, it does not do that. I'm, uh, not sure exactly this is. F6 w/
Manifold (duh, because color).

I did say "should", and I said it in the context of volumetric color
support.  What it does today is almost certainly not what I would want
it to do.

Preview Z-fights, which is almost certainly not desirable.  Manifold, I
don't know, but whatever it does isn't old enough to be considered
legacy that we need to preserve.

Practically, however, there are cases where faces are mathematically
coincident but in actual calculation (because of floating point
error, grid snap, et cetera) are slightly off.

I don't think that's true with Manifold (and we should skate to where
the puck's going to be).

I'm pretty sure that the problem is unavoidable.  When you calculate the
location of a point in two different ways, even when those two ways are
mathematically guaranteed to produce the same result, you cannot assume
that actual floating point operations will produce exactly the same
result out to the last bit.  That alone will sometimes cause the faces
to misalign microscopically.  But then if you need to match up such
points, you have to apply a fuzzy-match algorithm.  Now, consider a
zillion points along a line, each a tiny bit further along the line. 
Considering each individual pair of adjacent points, your fuzzy match
will consider them the same.  Thus A ≅ B, and B≅ C, and C≅ D, all the
way down the line.  But it's not transitive; A does not ≅ Z.  At some
point the rubber band has to break, and you have to start a new
"cluster".  Exactly how that works depends on your algorithm, but at
some point you'll have two points where A ≅ M, and N ≅Z, but M does not ≅ N.

That's why I'm a bit lost on how to create and color a face in
OpenSCAD, which, except for polygon/polyhedron, exposes nothing about
faces.

For those, it's more or less straightforward.  For anything else, not so
much.

The very fact that unioning two different "colored" solids has an
indeterminate interior essentially proves that it's six squares in a
particular layout.

It's indeterminate in today's OpenSCAD.  It is not indeterminate in my
POC; it's well-defined.

Also, consider:  does OpenSCAD operate on a mesh, or does it operate on
voxels?  How would you tell the difference?

A voxel-based system naturally wants to color the solid shape, and
doesn't care about faces.

On 12/30/2025 12:53 PM, Cory Cross via Discuss wrote: > Right, but that's why I want to export just the colored face and not > change the interior of model. Let slicer handle the realization and > don't make me recolor it every time I reload my model. And so that's an argument for retaining face coloring. But it's not an argument for not *also* handling volumetric coloring, so that these various cases *can* be well-defined, should you choose to specify them in the design tool.  As you note, you definitely do want to control the interior when you're talking about multiple materials, and that's the same as controlling filament color. (Well, excluding dynamic-mixing print technologies, where you can do gradients.) >> [ For coincident faces ] Theoretically, I think the answer should be >> the same as for volumes, which means either first-wins or last-wins, >> and for me that coin came down on first-wins. > > Sadly, it does not do that. I'm, uh, not sure exactly this is. F6 w/ > Manifold (duh, because color). I did say "should", and I said it in the context of volumetric color support.  What it does today is almost certainly not what I would want it to do. Preview Z-fights, which is almost certainly not desirable.  Manifold, I don't know, but whatever it does isn't old enough to be considered legacy that we need to preserve. >> Practically, however, there are cases where faces are mathematically >> coincident but in actual calculation (because of floating point >> error, grid snap, et cetera) are slightly off. > > I don't think that's true with Manifold (and we should skate to where > the puck's going to be). I'm pretty sure that the problem is unavoidable.  When you calculate the location of a point in two different ways, even when those two ways are mathematically guaranteed to produce the same result, you cannot assume that actual floating point operations will produce exactly the same result out to the last bit.  That alone will sometimes cause the faces to misalign microscopically.  But then if you need to match up such points, you have to apply a fuzzy-match algorithm.  Now, consider a zillion points along a line, each a tiny bit further along the line.  Considering each individual pair of adjacent points, your fuzzy match will consider them the same.  Thus A ≅ B, and B≅ C, and C≅ D, all the way down the line.  But it's not transitive; A does not ≅ Z.  At some point the rubber band has to break, and you have to start a new "cluster".  Exactly how that works depends on your algorithm, but at some point you'll have two points where A ≅ M, and N ≅Z, but M does not ≅ N. > That's why I'm a bit lost on *how* to create and color a face in > OpenSCAD, which, except for polygon/polyhedron, exposes nothing about > faces. For those, it's more or less straightforward.  For anything else, not so much. > The very fact that unioning two different "colored" solids has an > indeterminate interior essentially proves that it's six squares in a > particular layout. It's indeterminate in today's OpenSCAD.  It is not indeterminate in my POC; it's well-defined. Also, consider:  does OpenSCAD operate on a mesh, or does it operate on voxels?  How would you tell the difference? A voxel-based system naturally wants to color the solid shape, and doesn't care about faces.
CC
Cory Cross
Tue, Dec 30, 2025 10:55 PM

On 12/30/25 2:44 PM, Jordan Brown via Discuss wrote:

On 12/30/2025 12:53 PM, Cory Cross via Discuss wrote:

Right, but that's why I want to export just the colored face and not
change the interior of model. Let slicer handle the realization and
don't make me recolor it every time I reload my model.

And so that's an argument for retaining face coloring.

But it's not an argument for not also handling volumetric coloring,
so that these various cases can be well-defined, should you choose
to specify them in the design tool.  As you note, you definitely do
want to control the interior when you're talking about multiple
materials, and that's the same as controlling filament color.

I'm trying to make an argument to have both simultaneously, but I must
not be doing a good job.

Practically, however, there are cases where faces are mathematically
coincident but in actual calculation (because of floating point
error, grid snap, et cetera) are slightly off.

I don't think that's true with Manifold (and we should skate to where
the puck's going to be).

I'm pretty sure that the problem is unavoidable.  When you calculate
the location of a point in two different ways, even when those two
ways are mathematically guaranteed to produce the same result, you
cannot assume that actual floating point operations will produce
exactly the same result out to the last bit.  That alone will
sometimes cause the faces to misalign microscopically. But then if you
need to match up such points, you have to apply a fuzzy-match algorithm.

Right, Manifold does that.

Now, consider a zillion points along a line, each a tiny bit further
along the line.  Considering each individual pair of adjacent points,
your fuzzy match will consider them the same. Thus A ≅ B, and B≅ C,
and C≅ D, all the way down the line.  But it's not transitive; A does
not ≅ Z.  At some point the rubber band has to break, and you have to
start a new "cluster".  Exactly how that works depends on your
algorithm, but at some point you'll have two points where A ≅ M, and N
≅Z, but M does not ≅ N.

Smarter people have already solved this in Manifold. I trust they did it
correctly.

The very fact that unioning two different "colored" solids has an
indeterminate interior essentially proves that it's six squares in a
particular layout.

It's indeterminate in today's OpenSCAD.  It is not indeterminate in my
POC; it's well-defined.

Agreed, that was my example.

Also, consider:  does OpenSCAD operate on a mesh, or does it operate
on voxels?  How would you tell the difference?

Mesh. Voxels cannot not exactly represent a pyramid, but OpenSCAD (thus
meshes) can.

A voxel-based system naturally wants to color the solid shape, and
doesn't care about faces.

Indeed.

  • Cory
On 12/30/25 2:44 PM, Jordan Brown via Discuss wrote: > On 12/30/2025 12:53 PM, Cory Cross via Discuss wrote: >> Right, but that's why I want to export just the colored face and not >> change the interior of model. Let slicer handle the realization and >> don't make me recolor it every time I reload my model. > > And so that's an argument for retaining face coloring. > > But it's not an argument for not *also* handling volumetric coloring, > so that these various cases *can* be well-defined, should you choose > to specify them in the design tool.  As you note, you definitely do > want to control the interior when you're talking about multiple > materials, and that's the same as controlling filament color. I'm trying to make an argument to have both simultaneously, but I must not be doing a good job. >>> Practically, however, there are cases where faces are mathematically >>> coincident but in actual calculation (because of floating point >>> error, grid snap, et cetera) are slightly off. >> >> I don't think that's true with Manifold (and we should skate to where >> the puck's going to be). > > I'm pretty sure that the problem is unavoidable.  When you calculate > the location of a point in two different ways, even when those two > ways are mathematically guaranteed to produce the same result, you > cannot assume that actual floating point operations will produce > exactly the same result out to the last bit.  That alone will > sometimes cause the faces to misalign microscopically. But then if you > need to match up such points, you have to apply a fuzzy-match algorithm. Right, Manifold does that. > Now, consider a zillion points along a line, each a tiny bit further > along the line.  Considering each individual pair of adjacent points, > your fuzzy match will consider them the same. Thus A ≅ B, and B≅ C, > and C≅ D, all the way down the line.  But it's not transitive; A does > not ≅ Z.  At some point the rubber band has to break, and you have to > start a new "cluster".  Exactly how that works depends on your > algorithm, but at some point you'll have two points where A ≅ M, and N > ≅Z, but M does not ≅ N. Smarter people have already solved this in Manifold. I trust they did it correctly. >> The very fact that unioning two different "colored" solids has an >> indeterminate interior essentially proves that it's six squares in a >> particular layout. > > It's indeterminate in today's OpenSCAD.  It is not indeterminate in my > POC; it's well-defined. Agreed, that was my example. > Also, consider:  does OpenSCAD operate on a mesh, or does it operate > on voxels?  How would you tell the difference? Mesh. Voxels cannot not exactly represent a pyramid, but OpenSCAD (thus meshes) can. > A voxel-based system naturally wants to color the solid shape, and > doesn't care about faces. Indeed. - Cory
JB
Jordan Brown
Wed, Dec 31, 2025 1:45 AM

On 12/30/2025 2:55 PM, Cory Cross via Discuss wrote:

I'm trying to make an argument to have both simultaneously, but I must
not be doing a good job. 

OK, I'm happy with that.  (Maybe not so much for the reasons you're
giving, but in support of non-3DP applications including legacy
visualization and gaming.)

Practically, however, there are cases where faces are
mathematically coincident but in actual calculation (because of
floating point error, grid snap, et cetera) are slightly off.

[...]

Right, Manifold does that. 

I realized, but not at a convenient time to comment, that it doesn't
matter for this purpose.

If the underlying graphics engine can cleanly do a difference with
coincident faces, great!  If it can't, the best practice would be to
avoid that.  Nothing that we do in this discussion will affect that result.

Also: A scheme using the technique I used for that POC does not depend
on multi-color support from the underlying graphics engine, and might be
better if it doesn't use that support even if it's there, because then
it can be independent of that underlying engine's behavior.

On 12/30/2025 2:55 PM, Cory Cross via Discuss wrote: > I'm trying to make an argument to have both simultaneously, but I must > not be doing a good job.  OK, I'm happy with that.  (Maybe not so much for the reasons you're giving, but in support of non-3DP applications including legacy visualization and gaming.) >>>> Practically, however, there are cases where faces are >>>> mathematically coincident but in actual calculation (because of >>>> floating point error, grid snap, et cetera) are slightly off. >>> [...] > Right, Manifold does that.  I realized, but not at a convenient time to comment, that it doesn't matter for this purpose. If the underlying graphics engine can cleanly do a difference with coincident faces, great!  If it can't, the best practice would be to avoid that.  Nothing that we do in this discussion will affect that result. Also: A scheme using the technique I used for that POC does not depend on multi-color support from the underlying graphics engine, and might be better if it doesn't use that support even if it's there, because then it can be independent of that underlying engine's behavior.
CC
Cory Cross
Wed, Dec 31, 2025 4:58 AM

On 12/30/25 5:45 PM, Jordan Brown via Discuss wrote:

Also: A scheme using the technique I used for that POC does not depend
on multi-color support from the underlying graphics engine, and might
be better if it doesn't use that support even if it's there, because
then it can be independent of that underlying engine's behavior.

Manifold lets you specify properties of vertices, but not faces. I'm not
sure how to use it to have face color, so here's some relevant links
where you'll see familiar names:

https://github.com/elalish/manifold/issues/1477
https://github.com/elalish/manifold/discussions/711
https://github.com/elalish/manifold/discussions/749
https://github.com/elalish/manifold/issues/1238

But it will be much more performant and less code to maintain if it can
be offloaded to Manifold.

  • Cory
On 12/30/25 5:45 PM, Jordan Brown via Discuss wrote: > Also: A scheme using the technique I used for that POC does not depend > on multi-color support from the underlying graphics engine, and might > be better if it doesn't use that support even if it's there, because > then it can be independent of that underlying engine's behavior. Manifold lets you specify properties of vertices, but not faces. I'm not sure how to use it to have face color, so here's some relevant links where you'll see familiar names: https://github.com/elalish/manifold/issues/1477 https://github.com/elalish/manifold/discussions/711 https://github.com/elalish/manifold/discussions/749 https://github.com/elalish/manifold/issues/1238 But it will be much more performant and less code to maintain if it can be offloaded to Manifold. - Cory
JB
Jordan Brown
Wed, Dec 31, 2025 5:08 AM

On 12/30/2025 8:58 PM, Cory Cross via Discuss wrote:

But it will be much more performant and less code to maintain if it
can be offloaded to Manifold.

Maybe.  Maybe not, if Manifold only does face coloring.  (Which is the
case now.)

On 12/30/2025 8:58 PM, Cory Cross via Discuss wrote: > But it will be much more performant and less code to maintain if it > can be offloaded to Manifold. Maybe.  Maybe not, if Manifold only does face coloring.  (Which is the case now.)
RW
Raymond West
Wed, Dec 31, 2025 5:07 PM

Manifold color is attached to vertices. it is straightforward to use it
to color objects, not so easy for faces. It seems that openscad colors
faces, which gives to my mind, odd results, due to the way it has been
designed.

It is possible to have two objects in the same place, (at least in the
cad world), without using union, but not if you use color in the
openscad world.

the following code

//difference(){
//union(){
//color("red")
cube([50,50,40],true);
//color("blue")
cube([80,30,30],true);
//}
//translate([0,0,-30])cylinder(d=20,h=60);}

Two cubes can exist overlapping in the same place. Now, add color, it
seems to be as  I would expect. But, you cannot perform a boolean on the
colored objects unless unioned. In the example, the second cube is
removed by the difference, but it appears to leave its color behind.
When unioned, the difference does not retain the colors of the
individual cubes, since it is only the existing faces that have color.
You can progressively remove the comments to see the results.

My argument is that if two or more objects can exist in the same place,
then existing functions should be able to operate on them, or prevent
objects from overlapping without union.

Manifold color is attached to vertices. it is straightforward to use it to color objects, not so easy for faces. It seems that openscad colors faces, which gives to my mind, odd results, due to the way it has been designed. It is possible to have two objects in the same place, (at least in the cad world), without using union, but not if you use color in the openscad world. the following code //difference(){ //union(){ //color("red") cube([50,50,40],true); //color("blue") cube([80,30,30],true); //} //translate([0,0,-30])cylinder(d=20,h=60);} Two cubes can exist overlapping in the same place. Now, add color, it seems to be as  I would expect. But, you cannot perform a boolean on the colored objects unless unioned. In the example, the second cube is removed by the difference, but it appears to leave its color behind. When unioned, the difference does not retain the colors of the individual cubes, since it is only the existing faces that have color. You can progressively remove the comments to see the results. My argument is that if two or more objects can exist in the same place, then existing functions should be able to operate on them, or prevent objects from overlapping without union.
JB
Jordan Brown
Wed, Dec 31, 2025 7:07 PM

On 12/31/2025 9:07 AM, Raymond West via Discuss wrote:

It seems that openscad colors faces, which gives to my mind, odd
results, due to the way it has been designed. 

I wasn't around for the history, but I don't think it was so much a
matter of design as of how the pieces being used happened to fit
together. Some years ago I started to document the color behavior, and
was told not to, because it wasn't really intentional or final.

First, remember that until recently OpenSCAD render lost all color. 
Color was a preview-only thing.

The previewer is very much face-centric.  It throws triangles at the
graphics subsystem, sometimes mixing in some black magic, and the
graphics subsystem figures out what's in front.  If there are coincident
faces with different colors, the graphics subsystem can't decide which
is in front and there's Z-fighting.  When you do a difference, it
doesn't have a clue what the color is of the object you cut into - or
even that you cut into one! - and uses the color of the negative object
so that it has some color to apply.  Similarly for intersection.

It is possible to have two objects in the same place, (at least in the
cad world), without using union, but not if you use color in the
openscad world. 

It is not possible to have two objects in the same place in OpenSCAD
(absent lazy union, which you shouldn't be using).  They will be
implicitly unioned.

the following code

//difference(){
//union(){
//color("red")
cube([50,50,40],true);
//color("blue")
cube([80,30,30],true);
//}
//translate([0,0,-30])cylinder(d=20,h=60);}

Two cubes can exist overlapping in the same place.

No.  That's one shape.  It's implicitly unioned.

Now, add color, it seems to be as  I would expect.

Yes, because the previewer draws the various triangles in the specified
colors and the graphics subsystem figures out what's in front.  (And,
these days, because Manifold tracks the colors of the faces as it
combines them.)

But, you cannot perform a boolean on the colored objects unless unioned.

I assume that you mean that you cannot perform a boolean on the two
colored objects together unless they're unioned.  Well, yeah, if you
want to operate on the combined shape, you must combine them so that
they are a single child for your boolean operator.

In the example, the second cube is removed by the difference, but it
appears to leave its color behind. 

Yes.  In the previewer, any faces created by cutting away the earlier
object get the color of the later object.  That applies to both
difference and intersection.

When unioned, the difference does not retain the colors of the
individual cubes, since it is only the existing faces that have color.
You can progressively remove the comments to see the results. 

Are you referring to the fact that the cylindrical hole is green?  Yes,
the color of the negative object - the cylinder - gets used for the new
faces.  This is the same as the "second cube is removed but leaves its
color behind" that you mention above, with a small bit of magic that the
default color for negative objects is green rather than yellow.

My argument is that if two or more objects can exist in the same place, 

They can't.

then existing functions should be able to operate on them, or prevent
objects from overlapping without union.

They are implicitly unioned.

We don't have a working wireframe mode any more, but if you look at the
results of rendering your two cubes, you'd see that the result is a
single mesh; the parts of the blue cube's X-oriented edges that were
inside the red cube are gone.  That's true at every stage of your
progression.  (Except, of course, when you are just subtracting the blue
cube from the red cube, when they are the only parts of the blue cube
that remain.)

On 12/31/2025 9:07 AM, Raymond West via Discuss wrote: > It seems that openscad colors faces, which gives to my mind, odd > results, due to the way it has been designed.  I wasn't around for the history, but I don't think it was so much a matter of design as of how the pieces being used happened to fit together. Some years ago I started to document the color behavior, and was told not to, because it wasn't really intentional or final. First, remember that until recently OpenSCAD render lost all color.  Color was a preview-only thing. The previewer is very much face-centric.  It throws triangles at the graphics subsystem, sometimes mixing in some black magic, and the graphics subsystem figures out what's in front.  If there are coincident faces with different colors, the graphics subsystem can't decide which is in front and there's Z-fighting.  When you do a difference, it doesn't have a clue what the color is of the object you cut into - or even that you cut into one! - and uses the color of the negative object so that it has *some* color to apply.  Similarly for intersection. > It is possible to have two objects in the same place, (at least in the > cad world), without using union, but not if you use color in the > openscad world.  It is not possible to have two objects in the same place in OpenSCAD (absent lazy union, which you shouldn't be using).  They will be implicitly unioned. > the following code > > //difference(){ > //union(){ > //color("red") > cube([50,50,40],true); > //color("blue") > cube([80,30,30],true); > //} > //translate([0,0,-30])cylinder(d=20,h=60);} > > Two cubes can exist overlapping in the same place. No.  That's one shape.  It's implicitly unioned. > Now, add color, it seems to be as  I would expect. Yes, because the previewer draws the various triangles in the specified colors and the graphics subsystem figures out what's in front.  (And, these days, because Manifold tracks the colors of the faces as it combines them.) > But, you cannot perform a boolean on the colored objects unless unioned. I assume that you mean that you cannot perform a boolean on the two colored objects together unless they're unioned.  Well, yeah, if you want to operate on the combined shape, you must combine them so that they are a single child for your boolean operator. > In the example, the second cube is removed by the difference, but it > appears to leave its color behind.  Yes.  In the previewer, any faces created by cutting away the earlier object get the color of the later object.  That applies to both difference and intersection. > When unioned, the difference does not retain the colors of the > individual cubes, since it is only the existing faces that have color. > You can progressively remove the comments to see the results.  Are you referring to the fact that the cylindrical hole is green?  Yes, the color of the negative object - the cylinder - gets used for the new faces.  This is the same as the "second cube is removed but leaves its color behind" that you mention above, with a small bit of magic that the default color for negative objects is green rather than yellow. > My argument is that if two or more objects can exist in the same place,  They can't. > then existing functions should be able to operate on them, or prevent > objects from overlapping without union. They are implicitly unioned. We don't have a working wireframe mode any more, but if you look at the results of rendering your two cubes, you'd see that the result is a single mesh; the parts of the blue cube's X-oriented edges that were inside the red cube are gone.  That's true at every stage of your progression.  (Except, of course, when you are just subtracting the blue cube from the red cube, when they are the only parts of the blue cube that remain.)
CC
Cory Cross
Fri, Jan 2, 2026 9:58 PM

On 12/30/25 9:08 PM, Jordan Brown via Discuss wrote:

On 12/30/2025 8:58 PM, Cory Cross via Discuss wrote:

But it will be much more performant and less code to maintain if it
can be offloaded to Manifold.

Maybe.  Maybe not, if Manifold only does face coloring.  (Which is the
case now.)

https://github.com/elalish/manifold/discussions/1479#discussioncomment-15384231

The whole thing is great, lots of good info.

Trying to summarize:

  • Manifold does not do volumetric color.
  • Suggested method is to continue tracking origin shape for face coloring.
  • Manifold essentially works like OpenSCAD: if you color something,
    you color all its faces; after CSG operations color of a face goes
    back to the origin of the face (i.e. differences take the face color
    of the subtracting face)

So... I wonder if volumetric and face coloring is easier than I've been
thinking. We've been using examples of differing volumetric colors. If
you perform operations on two shapes of the same volumetric coloring....
then that's how it already works today, isn't it? If you assume
everything has the same volumetric color (let's call it white alpha=0
aka translucent :) ) then all our operations are already defined and
just do those operations (almost) exactly as they work today. ("almost"
because we pointed out it does some weird things with coincident faces).
Face logic doesn't have to change at all?

vol() is mesh(es) with volumetric color

face() does not have a volumetric color

Operations are only binary ever. op() { a; b; c; } is considered op() {
op() { a; b; } c; } as Jordan proposes. Which means we only need to
define the binary case. The number of cases is small, so this isn't that
hard.

Intersection:

Jordan effectively proposes the volumetric color of the right-hand
operand is always ignored. Thus all intersection() { vol1; vol2; } are
equivalent to intersection() { vol1; existing_openscad_union() {vol2's
meshes...} }.

intersection() { vol(); face(); } -- meshes preserve vol's volumetric
color, faces preserve existing face color whether they come from vol()
or face().

intersection() { face(); vol(); } -- This is the same thing but with a
single mesh and uncolor!

Multiple meshes for the left operand with different colors are obvious,
because they don't overlap; they're non-overlapping union of
intersections. Coincident faces of the right-hand operand never could be
used.

Difference:

difference() { vol(); face(); } -- every new face receives vol's
volumetric color, but the volumetric color of the mesh is unchanged.

difference() { vol(); other_vol(); } -- Like intersection, volumetric
color is ignored for the right-hand operand. Thus this is actually the
previous case.

difference() { face(); vol(); } -- here's the maybe-controversial one,
mesh is volumetric uncolor, but every new face receives the face color
from vol() (existing behaviour), unless it's uncolored, then it receives
the volumetric color from the mesh the face belongs to (this is the new
stuff), unless that's also uncolored, so it gets negative color
(existing behaviour). This is backward-compatible because shapes are
volumetric uncolored by default.

Union:

Jordan proposes that an overlapping union() { a; b; } is defined using a
non-overlapping union: union() { union() { difference() { a; b; }
intersection() { a; b;} } difference() { b; a; } }.

We're already defined those operations for face/vol interactions.
Non-overlapping union is completely trivial. Done.

Now, when Jordan's thinking volumetric color, I believe he's thinking
the faces shown in the viewport should reflect the volumetric color. And
that can be the case just fine, but we can still have different face
color stored in the model. So we'll want a widget to switch between
displaying face color or displaying volumetric color. And we'll need to
reify uncolors; probably face uncolor (positive and negative) inherit
from volumetric color if its not uncolor; otherwise do the legacy
behaviour. Volumetric uncolor will need a new setting.

Finally, I thought CGAL was preserving coincident faces, but it's not.
They're completely removed. So there isn't any backward-compatibility
/with the last stable/ with the proposed over-2-children ordering of
operands. But it does differ from HEAD and we should probably get that
changed before the next stable release.

Jordan's POC makes this assumption: every operation op() { a; b; c; d; }
considered op() { op() { op() { a; b; } c; } d; } I like that.

That's not backward-compatible with rendering with Manifold, which seems
to be: op() { d; op() { c; op() { b; a; } } }

(I'm especially bothered by a & b being reversed.)

Because that's what I had to do to make these the same w/ F6 Manifold:

union() {
translate([0,0,5]) color("black") cube(10);
color("white") cube(10);
translate([3,0,9]) rotate([-90,0,0]) color("red") cylinder(d=5,h=10);
translate([7,0,9]) rotate([-90,0,0]) color("blue") cylinder(d=5,h=10);
}

translate([15,0,0]) union() {
translate([3,0,9]) rotate([-90,0,0]) color("red") cylinder(d=5,h=10);
union() {
translate([7,0,9]) rotate([-90,0,0]) color("blue") cylinder(d=5,h=10);
union() {
color("white") cube(10);
translate([0,0,5]) color("black") cube(10);
}
}
}

  • Cory
On 12/30/25 9:08 PM, Jordan Brown via Discuss wrote: > On 12/30/2025 8:58 PM, Cory Cross via Discuss wrote: >> But it will be much more performant and less code to maintain if it >> can be offloaded to Manifold. > > Maybe.  Maybe not, if Manifold only does face coloring.  (Which is the > case now.) https://github.com/elalish/manifold/discussions/1479#discussioncomment-15384231 The whole thing is great, lots of good info. Trying to summarize: * Manifold does not do volumetric color. * Suggested method is to continue tracking origin shape for face coloring. * Manifold essentially works like OpenSCAD: if you color something, you color all its faces; after CSG operations color of a face goes back to the origin of the face (i.e. differences take the face color of the subtracting face) So... I wonder if volumetric and face coloring is easier than I've been thinking. We've been using examples of differing volumetric colors. If you perform operations on two shapes of the same volumetric coloring.... then that's how it already works today, isn't it? If you assume everything has the same volumetric color (let's call it white alpha=0 aka translucent :) ) then all our operations are already defined and just do those operations (almost) exactly as they work today. ("almost" because we pointed out it does some weird things with coincident faces). Face logic doesn't have to change at all? vol() is mesh(es) with volumetric color face() does not have a volumetric color Operations are only binary ever. op() { a; b; c; } is considered op() { op() { a; b; } c; } as Jordan proposes. Which means we only need to define the binary case. The number of cases is small, so this isn't that hard. Intersection: Jordan effectively proposes the volumetric color of the right-hand operand is always ignored. Thus all intersection() { vol1; vol2; } are equivalent to intersection() { vol1; existing_openscad_union() {vol2's meshes...} }. intersection() { vol(); face(); } -- meshes preserve vol's volumetric color, faces preserve existing face color whether they come from vol() or face(). intersection() { face(); vol(); } -- This is the same thing but with a single mesh and uncolor! Multiple meshes for the left operand with different colors are obvious, because they don't overlap; they're non-overlapping union of intersections. Coincident faces of the right-hand operand never could be used. Difference: difference() { vol(); face(); } -- every new face receives vol's volumetric color, but the volumetric color of the mesh is unchanged. difference() { vol(); other_vol(); } -- Like intersection, volumetric color is ignored for the right-hand operand. Thus this is actually the previous case. difference() { face(); vol(); } -- here's the maybe-controversial one, mesh is volumetric uncolor, but every new face receives the *face color* from vol() (existing behaviour), unless it's uncolored, then it receives the volumetric color from the mesh the face belongs to (this is the new stuff), unless that's also uncolored, so it gets negative color (existing behaviour). This is backward-compatible because shapes are volumetric uncolored by default. Union: Jordan proposes that an overlapping union() { a; b; } is defined using a non-overlapping union: union() { union() { difference() { a; b; } intersection() { a; b;} } difference() { b; a; } }. We're already defined those operations for face/vol interactions. Non-overlapping union is completely trivial. Done. Now, when Jordan's thinking volumetric color, I believe he's thinking the faces shown in the viewport should reflect the volumetric color. And that can be the case just fine, but we can still have different face color stored in the model. So we'll want a widget to switch between displaying face color or displaying volumetric color. And we'll need to reify uncolors; probably face uncolor (positive and negative) inherit from volumetric color if its not uncolor; otherwise do the legacy behaviour. Volumetric uncolor will need a new setting. Finally, I thought CGAL was preserving coincident faces, but it's not. They're completely removed. So there isn't any backward-compatibility /with the last stable/ with the proposed over-2-children ordering of operands. But it does differ from HEAD and we should probably get that changed before the next stable release. Jordan's POC makes this assumption: every operation op() { a; b; c; d; } considered op() { op() { op() { a; b; } c; } d; } I like that. That's not backward-compatible with rendering with Manifold, which seems to be: op() { d; op() { c; op() { b; a; } } } (I'm especially bothered by a & b being reversed.) Because that's what I had to do to make these the same w/ F6 Manifold: union() { translate([0,0,5]) color("black") cube(10); color("white") cube(10); translate([3,0,9]) rotate([-90,0,0]) color("red") cylinder(d=5,h=10); translate([7,0,9]) rotate([-90,0,0]) color("blue") cylinder(d=5,h=10); } translate([15,0,0]) union() { translate([3,0,9]) rotate([-90,0,0]) color("red") cylinder(d=5,h=10); union() { translate([7,0,9]) rotate([-90,0,0]) color("blue") cylinder(d=5,h=10); union() { color("white") cube(10); translate([0,0,5]) color("black") cube(10); } } } - Cory
CC
Cory Cross
Fri, Jan 2, 2026 11:25 PM

On 12/23/25 5:02 PM, Jordan Brown via Discuss wrote:

Implementations of hull(), minkowski(), et cetera, and of the 2D
operations, left as an exercise for the reader.

hull()'s output should always be volumetric uncolor. There are some
cases when it could not, but I don't think that's worth the confusion
why it does sometimes and sometimes doesn't.

The extrusions could support volumetric color, but not face coloring,
if, for no other reason, than it doesn't now. The tricky thing might be
ensuring the coincident edges result in coincident faces in the face of
twist.

minkowski() looks like it could support it, until you consider the
second argument could have multiple colors. Then I don't think it'll
work, so same reasoning as hull().

  • Cory
On 12/23/25 5:02 PM, Jordan Brown via Discuss wrote: > Implementations of hull(), minkowski(), et cetera, and of the 2D > operations, left as an exercise for the reader. hull()'s output should always be volumetric uncolor. There are some cases when it could not, but I don't think that's worth the confusion why it does sometimes and sometimes doesn't. The extrusions could support volumetric color, but not face coloring, if, for no other reason, than it doesn't now. The tricky thing might be ensuring the coincident edges result in coincident faces in the face of twist. minkowski() looks like it could support it, until you consider the second argument could have multiple colors. Then I don't think it'll work, so same reasoning as hull(). - Cory
JB
Jordan Brown
Sat, Jan 3, 2026 12:31 AM

On 1/2/2026 1:58 PM, Cory Cross via Discuss wrote:

Now, when Jordan's thinking volumetric color, I believe he's thinking
the faces shown in the viewport should reflect the volumetric color.

Yes.  Let's state that as requirements:

  • You should be able to "think" in volumetric color, and have
    everything work simply; you should never have to consider face color.
  • If you're "thinking" in face color, everything should be as it is today.
  • If you're trying to mix the two, shrug, the result just needs to be
    understandable and non-stupid.

Terminological nit:  you're using "meshes" when I think you mean
"volumes".  Meshes are by definition collections of faces.

I'm having a hard time wrapping my head around the definitions you've
given.  That's not to say that they are wrong, just that they aren't
"clicking" in my head.

I wonder whether it would be helpful to take a lesson from 2D drawing
programs, and consider there to be "face color" and "fill color", with
the possibility that either of those might be "undefined".

Let me try restating the rules with that basis.  (I'm not sure this is
different from what you said.)

What is visible in the UI is the face color, if defined, else the fill
color, and if there's no fill color, the default color (yellow).

Union:

  • Face colors are unchanged.
  • Fill colors are unchanged, with A winning for overlaps.
  • Coincident faces:  A wins (because it's an overlap).

Visually, unions are the same for face coloring and fill coloring.

Difference:

  • Existing faces of A are unchanged.
  • Existing fill of A is unchanged.
  • Newly created faces get the face color from B, if any.
  • Coincident faces:  A wins (because they are existing faces of A).

Note that this means that if B has no face color, newly created faces
have no face color and so will be displayed as the fill color of A, if any.

Intersection:

  • Existing faces of A are unchanged.
  • Existing fill of A is unchanged.
  • Newly created faces get the face color from B, if any.
  • Coincident faces:  A wins (because they are existing faces of A).

Again, this means that if B has no face color, newly created faces will
have no face color and so will be displayed as the fill color of A, if any.

In this scheme, I think there's an overall rule that if the inputs have
no face colors, the outputs must not have face colors either.  (If the
operation can introduce face colors where there were none before, that
will unexpectedly influence downstream operations.)

But... are there settings for those various things that yield compatible
behavior in the various default-color cases?  I don't think so.

Consider the simple case:

difference() {
    cube(10);
    sphere(10);
}

The "outside", where the cube's original faces remain, is displayed as
yellow.  The "inside", where the sphere was scooped out, is displayed as
green.  So where did the green come from?

  • It's not the face color of the sphere moved over into the face color
    of the result, because that's yellow (if it's anything).
  • It's not face coloring of the result at all, because that would then
    change the behavior of downstream operations.  C-(A-B), where C has
    a fill color but no face color defined, should display as that fill
    color but would get some faces colored green.
  • It's not a simple "undefined", because that's the color of the
    cube's original faces, yellow.

It seems like it needs to be a "negative undefined".  When we get done
with all of the operations, if you have a face that has no color, on a
volume that has no fill color, if it's positive undefined it's displayed
as yellow and if it's negative undefined then it's displayed as green. 
(And, except for the "fill color" part, I think that's what the
previewer already does.)

So I think that expands the rules for difference to:

  • Existing faces of A are unchanged.
  • Existing fill of A is unchanged.
  • Newly created faces get the face color from B, if any, else they get
    negative undefined.

I think the rule for intersection is largely unchanged from above, but
explicitly preserving the undefined state of B's faces:

  • Existing faces of A are unchanged.
  • Existing fill of A is unchanged.
  • Newly created faces get the face color from B, including its
    undefined-ness.  That is, if B's face has a color, the result gets
    that color, else if B's face is negative-undefined, the result is
    negative-undefined, else the result is positive-undefined.

Perhaps more generally, the "default colors" are not colors at all. 
Rather, they are the presentation of the lack of color, with a hint of
how the face came to be created.  A shape to which no color has been
applied has neither face colors nor a fill color.  Notably, a "default
color" should not be included in any export.

That face/fill split seems to yield a fairly understandable set of
behaviors, and in particular I think it meets the requirement that I set
at the top:  if you're using only fill colors you don't have to think
about face colors, and if you're using only face colors you don't have
to think about fill colors.

On 1/2/2026 1:58 PM, Cory Cross via Discuss wrote: > Now, when Jordan's thinking volumetric color, I believe he's thinking > the faces shown in the viewport should reflect the volumetric color. Yes.  Let's state that as requirements: * You should be able to "think" in volumetric color, and have everything work simply; you should never have to consider face color. * If you're "thinking" in face color, everything should be as it is today. * If you're trying to mix the two, shrug, the result just needs to be understandable and non-stupid. Terminological nit:  you're using "meshes" when I think you mean "volumes".  Meshes are by definition collections of faces. I'm having a hard time wrapping my head around the definitions you've given.  That's not to say that they are wrong, just that they aren't "clicking" in my head. I wonder whether it would be helpful to take a lesson from 2D drawing programs, and consider there to be "face color" and "fill color", with the possibility that either of those might be "undefined". Let me try restating the rules with that basis.  (I'm not sure this is different from what you said.) What is visible in the UI is the face color, if defined, else the fill color, and if there's no fill color, the default color (yellow). Union: * Face colors are unchanged. * Fill colors are unchanged, with A winning for overlaps. * Coincident faces:  A wins (because it's an overlap). Visually, unions are the same for face coloring and fill coloring. Difference: * Existing faces of A are unchanged. * Existing fill of A is unchanged. * Newly created faces get the face color from B, if any. * Coincident faces:  A wins (because they are existing faces of A). Note that this means that if B has no face color, newly created faces have no face color and so will be displayed as the fill color of A, if any. Intersection: * Existing faces of A are unchanged. * Existing fill of A is unchanged. * Newly created faces get the face color from B, if any. * Coincident faces:  A wins (because they are existing faces of A). Again, this means that if B has no face color, newly created faces will have no face color and so will be displayed as the fill color of A, if any. In this scheme, I think there's an overall rule that if the inputs have no face colors, the outputs must not have face colors either.  (If the operation can introduce face colors where there were none before, that will unexpectedly influence downstream operations.) But... are there settings for those various things that yield compatible behavior in the various default-color cases?  I don't think so. Consider the simple case: difference() { cube(10); sphere(10); } The "outside", where the cube's original faces remain, is displayed as yellow.  The "inside", where the sphere was scooped out, is displayed as green.  So where did the green come from? * It's not the face color of the sphere moved over into the face color of the result, because that's yellow (if it's anything). * It's not face coloring of the result at all, because that would then change the behavior of downstream operations.  C-(A-B), where C has a fill color but no face color defined, should display as that fill color but would get some faces colored green. * It's not a simple "undefined", because that's the color of the cube's original faces, yellow. It seems like it needs to be a "negative undefined".  When we get done with all of the operations, if you have a face that has no color, on a volume that has no fill color, if it's positive undefined it's displayed as yellow and if it's negative undefined then it's displayed as green.  (And, except for the "fill color" part, I think that's what the previewer already does.) So I think that expands the rules for difference to: * Existing faces of A are unchanged. * Existing fill of A is unchanged. * Newly created faces get the face color from B, if any, else they get negative undefined. I think the rule for intersection is largely unchanged from above, but explicitly preserving the undefined state of B's faces: * Existing faces of A are unchanged. * Existing fill of A is unchanged. * Newly created faces get the face color from B, including its undefined-ness.  That is, if B's face has a color, the result gets that color, else if B's face is negative-undefined, the result is negative-undefined, else the result is positive-undefined. Perhaps more generally, the "default colors" are not colors at all.  Rather, they are the *presentation* of the lack of color, with a hint of how the face came to be created.  A shape to which no color has been applied has neither face colors nor a fill color.  Notably, a "default color" should not be included in any export. That face/fill split seems to yield a fairly understandable set of behaviors, and in particular I think it meets the requirement that I set at the top:  if you're using only fill colors you don't have to think about face colors, and if you're using only face colors you don't have to think about fill colors.