-
Notifications
You must be signed in to change notification settings - Fork 138
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Doc fixes, code changes, bugfixes, new examples #1566
Doc fixes, code changes, bugfixes, new examples #1566
Conversation
|
And it probably should. Everything in your picture would be purple if you view with "Thrown together". That would mean the values outside the shape are bigger than the values inside. If you set an isovalue range [0.9,1,1] with |
I don't understand. Everything purple would be an understandable solution, but why does that mean I get extra faces? |
Finished all items 1-14.
I think that would be confusing. It still confuses me sometimes when I look at the code. I think it's enough just to say that I have already included that example a second time, showing the additional parameters.
As above, I think that would be confusing, but it's done. |
I am not sure, and it's curious why it happens only at that voxel size. I note that you have isovalue=1, and your expression evaluates to 1 in places outside the shape but within the bounding box (like at [-1,0,0]. The voxel bounding box got expanded and centered around your original, so it's [[-1.05, -1.05, -1.05], [1.05, 1.05, 1.05]]. |
Regarding my proposed suggesting of a second copy of metalobe with a passthrough function, why would that be confusing? I think it would be LESS confusing than jumping to the noisy sphere example, which uses a passthrough function with two strikes against it: you didn't just see a simpler version of the same example and it has the additional complication of cutoff and influence in the mix. I guess I'm not sure what you did because you do say you "already included the example a second time with additional parameters". It seems to me that saying "we aren't going to show this because it's confusing" is the wrong thing if by confusing you meant "difficult" or "advanced". If it's just a convoluted example that doesn't have a point to make, then sure it should be cut. But if it's a tricky technique that might be useful to users and it's hard to understand then it's more important to include examples of how to use that technique. If you go all the way to showing how to do influence and cutoff but not how to actually return it from a function you are in some sense assuming that it's obvious to the user how to return it from a function, so that they can add seamless new metaballs if they want. The alternative position would be, "users don't need to do this" in which case maybe the noisy sphere example is not necessary at all. (But I think showing to to apply cutoff and influence is probably a good idea.) |
How is [-1,0,0] "outside the shape". The shape should have radius 1 and all six axis points are part of the shape. |
I didn't say your lobe example was confusing. I already added it, first the basic one, and then the one with more parameters. What would be confusing is an example that has the extra support functions allowing the user to avoid putting |
Isn't it a natural question, especially where noisy sphere is almost duplicating a built-in metaball, how to fully get the same effect? |
Sure it's a natural question, but not one that needs to be answered in the examples. "If you want to do it this way, here are the hoops you have to jump through" doesn't strike me as meaningful or helpful. Built-in is built-in, user-defined is user-defined, there is no problem with them being different. Nevertheless, I have made this change showing the full implementation for the noisy metaball example. I don't think it's enlightening at all, but there it is. Further looking into the but you reported above with three faces on the figure: Any other voxel size works correctly, but at voxel size 0.05, the detection of voxels coinciding with the bounding box on three faces was off by some tiny number. Clearly a precision error. I have fixed this to check whether the outer face of the voxel is within EPSILON of the bounding box, and now it works correctly. All 14 items in the list above are completed. Pushing another revision now. Non-cubical voxels isn't done yet. Stay tuned. |
I would say it's counter-intuitive that requesting "closed" adds those faces. The figure was already closed and no part of it hits the bounding box. Basically it's adding the second surface at infinity and clipping it to the cube. We did say that isovalue=c was the same as isovalue=[c,INF] but I didn't understand this meant that it would draw a shell using the surface at infinity. So I think we need to add a sentence in the docs about this---maybe where the isovalue is discussed?---and also I think we need to have an example where we show it (or something like it) showing up as a cube and then you have a following example where the isovalue range is changed to [-INF,c] and then it works. The example might work better if the bounding box goes only to 0 along one axis, so you don't just get a cube. I think that makes it more clear what's going on. |
I think part of the reason for being confused has to do with thinking that I'm computing "an isosurface" instead of an object bounded by an isosurface. So I wonder if maybe changing the first line docs to Computes a VNF structure of an object bounded by one or two isosurfaces, intersected with a specified bounding box. Then here's a stab at rewriting paragraph 2: The specified isovalue can be a range or a scalar. When it is a range, [c1,c2], the returned object is the set of points, p, satisfying c1<=f(p)<=c2. If f has values larger than c2 and values smaller than c1 then the basic result is a VNF with two bounding surfaces corresponding to the isosurfaces at c1 and c2; this is a shell object with an inside and outside surface. If f(p)<c2 everywhere, then no isosurface exists for c2, so the object has only one bounding surface---the one defined by c1. This can result in a bounded object like a sphere or it can result an an unbounded object such as all the points outside of a sphere out to infinity. A similar situation arises if f(p)>c1 everywhere. You are allowed to set c1=-INF or c2=INF, which always produces an object with only a single bounding isosurface. If you want to obtain a bounded object, think about whether the function values inside your object are smaller or larger than your isosurface value. If the values inside are smaller, you produce a bounded object using [-INF,c]. If the values inside are larger than [c,INF] will produce a bounded object. When you give a scalar isovalue, this is equivalent to [c,INF], so it will produce a bounded object when the points inside the object are larger than the points at the isovalue. The description above suggests that the computed shape is a single object, but depending on your function, the object may have many (even infinitely many) disconnected components. It is also possible to have an unbounded object combined with several bounded shell objects. It is impossible to calculate an unbounded object, so isosurface calculations are restricted to a bounding box region, but when your object is unbounded it may display as simply the specified bounding box if That hopefully transitions to the paragraph on the bounding box. Then at the end of that paragraph add: If your object is unbounded then when it is intersected with the bounding box, the result may appear like a solid cube, because the clipping faces are all you can see and the bounding surface is hidden inside. Setting Then we should have examples of multi-component isosurface, and the previously noted unbounded case showing up as a cube, or maybe half-cube. I actually was wondering if the isosurface behavior is backwards and if the default for a scalar should be [-INF,c] instead of how you did it. I feel like that's more typically how somebody would construct a (bounded) set. (Yes, I know this is backwards relative to metaballs.) It seems like reverse is only useful with closed=false when you have chosen the wrong isovalue range. Is there any other possibility? |
That's a lot of words, but they look good to me. My only concern is that the previous words made a distinction between interior and exterior, which is important for a VNF because each facet has an interior and exterior side. I thought it was important to say that by default, "exterior" is wherever f < c. That distinction got lost in the new text. The |
The gyroid example says that the vnf can be tiled or wrapped around an axis with vnf_bend. Richard pointed out that this seemed out of place, and I agree. Delete the last sense of "things I can do with a vnf". If there's something you think is particularly notable with the gyroid, add another example, e.g. with vnf_bend applied to the gyroid. |
I removed the bit about vnf_bend, and made other changes to the docs as suggested, with some copy-editing. I propose:
These rules would have the desired behavior in all cases, as far as I can tell. The examples may not even need to be modified, and the user is unlikely to need to set The gyroid/periodic surface examples with an isovalue range would still get the bounding box faces as they should, a sphere with bigger values inside than outside (scalar isovalue) would also get clipped correctly, and a sphere that has smaller values inside and bigger values outside would have -INF as the minimum isovalue and not have the bounding box faces. |
Note that I tossed out the idea about changing the The idea was that closed=true can be confusing, so therefore maybe closed=false should be the default. The problem with the above proposal is that closed=true is potentially confusing when the shape is unbounded, and that can happen with any isovalue range. It's not a special property of [-INF,c]. So having a complicated default just creates more potential confusion while only solving the problem in one situation. Actually did you change the default range? Because my example where I encountered the problem was [c,INF]. Also note that the default for a parameter is not "what it says on the function line" it's how that parameter behaves when you run the function. You can implement a default by writing Here the shape is unbounded and we get a cube in each case covering all the possible isovalue range types:
Not shown is [-INF,INF] which also makes a cube. Might be worth banning the case of After pondering the idea of defaulting closed to false I think I don't really like it. The justification is that it makes things less confusing. The cost is that we have a default which is basically wrong most of the time. I think that places the priority incorrectly. So I lean towards keeping the default closed=true and trying to strongly highlight the issue in the docs. Maybe we have a bold face sentence that reads "Why did I just get a cube?" followed by instructions that either the bounding box is too small or the range is flipped. That, combined with the example that shows the cube problem and solution seems like it should be good. And I think I lean towards the solution being correct the range, not using reverse. This is because I think that it's better to encourage people to know what they are doing---that is, people should think for a moment about the interval that they need. Making closed=true by default instead supports a trial-and-error approach to getting things right. And if you end up needing the object to be clipped...or if your bounding box gets too small...then having switched to closed=false doesn't work and you need to flip the range. This makes me wonder if the scalar default should be removed and only a range permitted. That way you're forced to think explicitly when picking the isovalue whether you want to go up to INF or down to -INF. That could decrease confusion around the behavior of scalar isovalue, which sort of promotes "do what I mean" thinking. There is also a failure mode when closed=false, which is that your bounding box is too small and you get nothing:
I wonder if we need to mention this behavior and how it differs when closed=true vs closed=false. |
|
A bounded example?
Wonder if it can be simplified. or perhaps
|
Finished all items 1,3,4,5. The last one I had already done yesterday.
There is no scalar default for isovalue in isosurface(), in fact there has never been a default at all. We already agreed that metaballs should have a default isovalue=1. I like those examples. The second one looks most interesting. Working on debugging those holes you found in the clipping surface.
Can't you adjust the bounding box to eliminate the other cut faces? Or are you suggesting to leave the side cuts open? |
By scalar default I meant that you're currently allowed to give a scalar for isovalue and it defaults to [c,INF]. The proposal is that ONLY a range is accepted (scalar is an error) and you enter [c,INF] or [-INF,c] if you want an unbounded interval. This was mentioned with more context earlier in the longer message about default for closed. Yeah, I tweaked the last example and actually posted the example on the chat with it attached to some cylinders and a cube. For some reason when I tried to enlarge the bounding box I didn't go about it right and it didn't seem to be helping. But if you just lower the box until it's below the flat face that obviously will work. I do wonder if there's a way to design forms with more intent than my approach, which is to sort of randomly stick equations together. |
I fixed the holes by changing one "<" to "<=". The holes appeared when one corner of a clip face is exactly equal to max isovalue. I am not sure of the purpose of requiring isosurface() to take a range for isovalue, but I've already done it. Would it be useful to keep the scalar, perhaps just show a range in all the examples, describe isovalue in terms of ranges in the documentation, and mention that a scalar is the same as [c,INF]? And what about metaballs? It can take either a scalar or range. A scalar makes sense probably for 99.9% of what anybody would want to do with metaballs, but now and then I'd like to make a metaball shell. Update: isosurface() now requires a range, examples are updated. metaballs() can have either a scalar or a range. Still revising the documentation to reflect the changes. |
Remember my caution about the use of The reason I proposed having isosurface require a range is that a scalar is ambiguous in that it could be [-INF,c] or it could be [c,INF]. Now sure, you had it defined to be the latter---so strictly speaking, it's not ambiguous. But by accepting a scalar you enable the user to not think about this distinction---like I did---which makes it more likely to lead to confusion where you get the shape backwards because you never had to make a choice about which way it goes. It seems likely that if I had needed to specify a range for that original test that I got backwards that I would have gotten it right. Metaball should definitely still accept a scalar, and range also is OK. |
It needs to be <=. These are comparison of function values at the corners. If the corner is equal to the max isovalue, the regular surface triangulation was picking this up but the clipping surface wasn't. If it isn't equal, even off by eps, it would still work. Also, f+eps does not work when f has a high value. |
All tasks in this PR are done, examples have been added, docs changed as discussed. This should be the "almost final draft" if all OK and docsgen doesn't abort. |
Please review and merge if it looks OK.
|
Probably not going to have time to actually review until tomorrow. One thought, it seems like grow_bounds arg should be instead exact_voxel or fixed_voxel or something like that. Nobody is going to think: I want the bounds to not be what I asked for. The reason you want the bounds to grow is because that happens when you don't allow the voxel to change and you want to prevent he voxel from changing. |
Should the discussion of isovalue ranges use [c_min,c_max] instead of c1 and c2? e result is a VNF with two bounding surfaces corresponding to the isosurfaces at This text is confusing. I can consider the "front" surface to be the one facing me and the back whatever is not facing me, which means that different parts of the surface change names depending on my viewpoint. This seems like's it's saying something self-evident---it's much harder to understand the explanation than to just recognize the fact you're trying to explain---but if you think it's important you need to call them the "outside" and "inside" surfaces....and I'm not sure how to state it clearly. A "gap" sounds like a space that is not part of the object (e.g. outside the object). I guess ultimately I'm not sure what exactly it is that you're trying to explain. That the inside is the space between the two surfaces (instead of the inside being the complement of that space)? Note that at least in my understanding of typography, em dashes don't have spaces around them, but actually that em dash should be a colon, not a dash. You added a parenthetical "which is true when c_ = infinity" but we haven't yet admitted that this is an option. When I wrote it I figured I would start with finite values and then mention the infinite case later. I thought this was a gentler way to introduce things. If you disagree then the first sentence should say "the specified isovalue must be a range [cmin,cmax], where cmin can be -INF or cmax can be INF...." and then continue from there. The sentence that starts "if your object is unbounded" I suggest a bold face preceeding sentence that says something like "Why did I get a cube?" And "may appear like a solid cube" I'd suggest "may appear to be a solid cube". The suggestion says to change the isovalue range and then says: one of |
It's probably also better to use INF instead of |
Should include an example with artifacts for isosurface and then show taking log to smooth it out? |
I made the changes you suggested. I tried to demonstrate artifacts with a tilted cube but they aren't strong artifacts. Here's an example:
What did you do to get more pronounced ridges? I can easily demonstrate smoothing with logs with the trivial example of a sphere with a high exponent, but that seems a little too trivial and too obvious that a log would linearize the function. I'd like to have an example that's simple but not obvious. The bullet isn't a good example because logs don't help with the flat base. |
The cube artifact example was something like:
The exact artifacts are pretty sensitive to the voxel size. |
I generally think it works better to use two separate examples, which makes it easier to see which code produced which result. And when the pictures are small, it can work better, especially if you can get the code to be on the right in the wiki. In this case, the difference is huge so it's not like you need to study them carefully side by side. But I'll let you be the judge on what works best. We should mention, if it wasn't already that the isosurfaces are preserved by taking the log (as long as the function is positive everywhere), so taking the log in principle gives exactly the same result. f(p)=c ===> log(f(p)) = log(c) I will look over the revisions in the morning. The one question I have about artifacts is whether they can look different under different circumstances. Like the artifacts on the mb_cuboid look different from the artifacts on the above thing. So we may want to mention that the artifacts can manifest in different ways and any case of unexpected lack of smoothness that is not improved by smaller voxels might be caused by nonlinearity of your function around the isovalue. (Maybe you did this already.) |
It's obvious in this example which line produced which result. There's a left and a right in the image as well as in the code. Putting them both in the same place makes it clear that the underlying function is identical for both cases. The artifacts all have one thing in common, as far as I've observed: They are rounded ridges. The spacing and amplitude of the ridges vary depending on voxel size, isovalue, and the underlying function. I reformatted the example code so that it should fit on the right side of the image. I'm going to wait until tomorrow to see if @RAMilewski provides any further updates in #1559 and include those with the final submission after your review. What we have works fine for typical use cases. I'm not looking forward to fixing up the clip face triangulations to handle parts or all of two different shells of min and max isovalue in the same voxel. I got started on the 81 required triangulation rules but it took me an hour to figure out the first 11 and I'm sure I made mistakes caused by needing base-3 addressing rather than base-2 for the isosurfaces. That's going to be several days of tedium to finish and test all cases. |
Comments on isosurface() description:
For metaballs docs many of the above comments also apply, so check that. Also
|
Thanks for going through this in detail. We have disagreement about a logical list of function arguments versus a good user experience in actual practice. Those two goals don't always align. Isosurface()
Metaballs() - applied relevant changes above here too.
|
Captured suggestions from chat. Still to do is add a new example of a custom function and renumber the example references in the text.
@adrianVmariano , @RAMilewski :
Please add any new suggestions to this PR.