One of the subjects which I posted about some time ago was, how easy it is for a Computer Algebra System – a CAS – to output a single variable, in order to colour an Iso-Surface, and how hard it is in contrast, to output a normal vector, from whatever Geometry Shader computes the Iso-Surface, such that this normal vector can be used to shade the surface, in a later Fragment Shader invocation.
What needs to be done in ‘3D Game Design’ and (other) ‘CGI’, is essentially that.
But, given that a CAS can be used both, to plot a 3D surface, as well as, to define what the colour-range of this surface is supposed to be, a bit of a trick can be used, to bypass the need actually to compute a normal vector, but to achieve an equivalent result. And this posting will begin with an example which is slightly simpler, than what my earlier posting had assumed. Iso-Surfaces tend to smack of ‘implicit’ functions, while this example is going to start with an ‘explicit’ plot, in which (X) and (Y) are parameters of the function, but where it was already easy to achieve, that a single (Z) value results, such that (X, Y, Z) are in fact the coordinates plotted.
Because we have a Computer Algebra System in the first place, for continuous functions, it’s easy to compute the derivative with respect to one of the parameters. That derivative can be used, just to modulate the brightness of the surface. The simplest example is shown blow:
(Updated 7/10/2020, 6h10… )
(As of 7/06/2020: )
Now, if the goal is to extend this sort of logic to an implicit function, consistent with Iso-Surfaces, then the following would be an example:
Just to render this one frame – not an animation – took a single core of my 3GHz desktop CPU, 30 seconds. For such reasons, rendering results with 12 frames etc., does not seem like a good idea.
One of the reasons for which this approach, for ‘pseudo-shading’ the implicit function, ‘works’, is the fact that, even though the magnitude of the (linearly added) derivative might be large for many of the voxels, its components largely cancel, where the function is also close to zero, and where the surface is being plotted for the same reason. If the function had been:
x2 + y2 + z2 – 100 ( =0 )
Then, it would have been much harder to determine the range, since the derivatives do not cancel.
To be certain that the shading effect is always in range, an approach that could be taken would be, to compute the sum of the squares of each of the derivatives, with respect to (X), (Y) and (Z), and then to compute its square root, and then to divide the linear sum of the individual derivatives by the result. In that case, the linear sum will always fall into a predictable range, for all the voxels…
However, if that approach was taken, there would be no computational simplification left, over actually computing a normal vector. At that point, the equivalent of a normal vector would have been computed.
What the software will do is, to normalize the resulting brightness values, rather than the gradient vectors for all the voxels.
(Update 7/07/2020, 9h35: )
These methods of highlighting the surface of the implicit, 3D plot, all have a flaw, which I’ve been aware of from the beginning. Because of the way the surfaces behave, the gradient vector at the surface could be facing in one out of 2 directions, 180⁰ apart, and highlight a bidirectional surface either way. To reflect that, I used the absolute function, of the dot product, with the lighting vector. Whether the spot on the surface should be lit or not, additionally depends, on whether the dot product with the lighting vector, has the same sign, as the dot product with the view vector.
(Update 7/07/2020, 15h50: )
This exercise can be extended further, to parametric surfaces, although, to do so becomes more computationally expensive.
Parametric surfaces are surfaces, which begin from arbitrary parameters, that are usually named (U) and (V), but as functions which return a set of (X), (Y) and (Z) graph coordinates.
It’s possible to state a thesis, that two vectors can be computed, from the original functions, one of which is the (X, Y, Z) derivative of the functions, with respect to (U), at a given set of (U,V) values. And the second vector can be the (X, Y, Z) derivative of the functions, with respect to (V), at the same set of (U,V) values. These derivatives should form a tangent and a bitangent vector of sorts. Therefore, the cross-product between them should form a normal vector of sorts…
(Update 7/08/2020, 17h00: )
I have just replaced the worksheet above with a worksheet, that illustrates how my shading concept is supposed to work, and that demonstrates this better, than a previous version of the worksheet did.
(Update 7/09/2020, 11h00: )
There is an assumption that should be applied, when trying to shade the surfaces as described, in all the examples above. The range of (X), (Y) and (Z) values plotted should match. What the CAS will do if the ranges do not match, is simply to rescale (Z) differently, from how (X) and (Y) are scaled, as an example. If the range of (Z) values to be plotted is [0.0 .. 9.0], but the range of (X) and (Y) values is [-1.0 .. +1.0], then the effect this will have is, that the visual angles that the surfaces can be seen with, no longer match the ‘real’ angles, according to ‘real’ (X), (Y) and (Z) values.
What the Math does which I suggested above, is to use the ‘real’ (X), (Y) and (Z) values nonetheless, in order to compute the normal vectors, with respect to an arbitrary lighting vector.
If the lighting vector ‘does not seem to make sense’ in this way, the best solution to the problem is not, just to change the lighting vector, so that one particular function is shaded correctly. And the reason why not, is the fact that a user may want to plot any function, including functions that were not foreseen when the lighting vector was set. The correct solution then is really, to choose each function to be plotted, so that the length of its interval along the three coordinates is approximately similar…
I did something different in the worksheet the filename of which is ‘slider_6.html’ above, because I needed the lighting vector to match in some way, with the view angle. And, when the view angle’s (X) component equals 45⁰, then the corresponding, 3D, lighting vectors are easy to compute.
(Update 7/09/2020, 13h00: )
When using the ‘draw()’ functions within ‘Maxima’, and, when also using the ‘enhaced3d’ parameter, there is a way to turn off the automatic normalization of the range of values output, by the first element of the 4-element list that has been given as the ‘enhanced3d’ parameter. The way to do that would be, also to set:
cbrange = [0.0, 1.0]
(As an example.) This parameter accepts a list of two values, that are output-values of the ‘enhanced3d’ function, which are expected to correspond to the endpoints, of whatever ‘palette’ has been chosen.
The naming of this parameter would suggest that all it does is to define what the properties of the ‘colorbox’ are going to be. But, contrarily to that naming, it continues to have effect, if the colorbox is not shown. What ‘cbrange’ defaults to, is ‘auto’. In fact, if the first item in the list fed to ‘enhanced3d’, is a constant and not a function, the user will obtain messages from ‘GNU-Plot’, even if he has deselected that the colorbox should be shown, because in most implementations, ‘GNU-Plot’ cannot handle a range with a difference of zero.
What the user should also notice, however, would be, that in my examples above, even though I tried to keep the range of brightness levels between (0.0) and (1.0), I did not really succeed. Some of those plots will not display correctly, if the auto-normalization is turned off.
However, in ‘Other CGI’ as well as ‘3D Game Design’, the going assumption is, that no normalization takes place in the Fragment Shader output, because each fragment output, is computed without any awareness of the other fragments output. And so, in CGI, the assumption is, that the shader code must arrive at correct brightness-values, in a constant way, or in some other way.
(Update 7/09/2020, 15h35: )
Another question which the example above poses could be, ‘Why did Dirk use such a strange method, to define the cross-product between two vectors?’
According to certain sources on the Web, a version of Maxima exists, which only requires that the user load a package named ‘vect’, and that doing so, causes the ’tilde’ operator, which is the name for the character ‘~’, to be defined as the cross-product between any two vectors.
The problem with this is the fact, that not all versions of Maxima will define that operator, in a way that generates numerical results, from vectors, all the elements of which were numeric. Thus, when I had used that method before on a Linux version, because of the way the tilde operator was defined, any attempt to compute the normal vector resulted in an expression, that still contained the tilde operator. Further, calling ‘vectorsimp()’ within the ‘unitvector()’ function call, still did not get rid of this symbol, as a definitive part of the returned expression. (:1)
And, when an expression is fed to the ‘enhanced3d’ parameter, which it cannot evaluate, its default behaviour is to proceed, as though ‘
enhanced3d=true‘ had been specified, resulting in a plot, that is coloured solely according to Z-axis altitude. This could lead to the false perception that maybe, ‘enhanced3d’ has not been implemented for parametric surfaces…
There are several ways to define one’s own cross-product function, and my example above uses one, that can be stated on one line.
Conversely, if the user simply replaces the ‘wxdraw3d()’ function with a ‘draw3d()’ function, even the last example offered above, works on ‘Maxima For Android’, as the following screen-shot shows:
The fact that this also works, serves as an indication to me, that the Dev behind Maxima For Android, actually did a good job of porting the application to Android, resulting in a version, the ‘eigen’ package of which works.
(Update 7/09/2020, 23h40: )
I have just learned, that To use the ‘express()’ function on an argument, which contains the tilde operator in this way, does cause a vector to be output, all the elements of which are numeric, if the input vectors had all-numeric elements. After loading the ‘vect’ package, the following Maxima statement gives the correct result:
Normal(u, v) := unitvector(express((vDu(u, v) ~ vDv(u, v))))$
(Update 7/10/2020, 6h10: )
The way in which the ‘vect’ package does define the tilde operator (~) is intended as a feature, because ‘Maxima’ is a Computer Algebra System, and not a numerical tool, in first place. As a CAS, the software is supposed to be able to compute complicated symbolic operations, that include vector cross-products and dot-products, resulting in expressions that are not necessarily numeric. If the situation is given where a numerical result can be computed, as opposed to a return value, that is itself a function or an expression, Maxima just doesn’t jump in to replace that expression with a vector, all three elements of which are numeric.
However, what some people regard as a valid solution to a certain function, such as, to the cross-product operator, is not itself supposed to contain the same operator. In order to obtain that, the ‘express()’ function needs to be called on expressions that contain this operator.
What I find annoying about Maxima, is its lack of consistency, in what function-name achieves this. Maxima allows the user to call the ‘float()’ function on an expression, which is supposed to compute floating-point numbers. But, when called on certain expressions that contain symbols, ‘float()’ will only replace some of those symbols with floating-point numbers. If the expression contains a multiplication between two variables, both of which are complex numbers, then ‘float()’ will work on it fine, in that applying ‘float()’ will simplify that to a single complex number, with two floating-point values, an addition operator, and the imaginary constant. However, if the expression contains a complex base, raised to a fractional exponent, then ‘float()’ will leave that expression as-is. This has as result that more complicated expressions, that contain such a term, can also not be simplified, in any way that would take it apart.
In other situations, Maxima’s ‘ev()’ function is supposed to ‘evaluate’ an expression. But, evaluate in a way designed to get rid of what, exactly?
When given differential equations, Maxima’s ‘at()’ function gives the actual value of the derivative at one combination of parameters. The fact that such a function is needed follows from Calculus, because the nth derivative of a function (with respect to one of its own variables) is itself a function (of the same parameters as the original function). It does make sense, that an explicit operation may be needed, to tell Maxima to give a numeric answer to such a derivative, ‘at’ a specific combination of parameter-values.
Why can’t Maxima just have a single function, that accomplishes this, regardless of which system of symbolic functions the user has put into his expression? My guess is, Because there are too many programmers adding packages. And each package, ultimately needs a different ‘evaluation function’.