OUTDATED Technical documentation for developpers or advanced users
The objectsIn World of pycao
There is a class objectsInWorld which contains all the objects that can be embedded in 3D : camera, points, line, vectors... Formally, an instance of ObjectInWorld is an object on which we can operate using an instance of the class Map.
More informally, the class objectInWorld is created to share methods between many different kind of objects. For instance, whatever the object self is, camera or line, it is moved using the declaration self.move(map).
In practice, there are classes which derive from ObjectInWorld, for instance Line,Plane,Camera... The objects that we use are instances of these derived classes. There is no pure instance of ObjectInWorld, only instances of derived classes.
Moving the objects and the genealogy system
Every object self instance of ObjectInWorld can be nested in a genealogy system, with one parent self.parent, and a list of children self.children. The method move moves self with all its children. The method move_alone, move self without its children, and is called recursivly in move.
Move is the fundamental method to move any object. The advised procedure is the following. The user defines a map M using any of the possible primitives in the class mathutils.Map. Then applies on self with the instruction self.move(M).
For very common displacements: translation, rotation, scale, there are shortcuts. Instead of applying M=Map.translation(v), self.move(M) the user may directly type self.translate(v). This is compatible with the recommanded procedure since translate, rotate and scale are basically macros applying the procedure.
THe function move returns self, so that the operations may be nested self.move(M1).move(M2). However, Remark it is faster to compute self.move(M1*M2). With n children (n+1 operations) against 2n operations. Thus the chaining is useful only when the definition of M2 requires parameters obtained after the displacement of M1, or with few children.
Decomposable and primitive Objects: why we need them
Instances of ObjectInWorld can be divided using two classifications: An object may be : - decomposable or indecomposable - primitive/elaborate/compound.
These distinctions are motivated by performance consideration and ease of building library of objects, as we shall see.
The decomposable objects are built from several parts, for instance when a line is built from 2 points. In contrast, some objects are “indecomposable” objects, like a point or a plane, and cannot be defined by simpler parts. This vocabulary is useful to understand the following, but is not implemented formally in the language.
In contrast, the second classification primitive/elaborate/compound is implemented. Theses concepts correspond to three classes Primitive/Elaborate/Compound inheriting from ObjectInWorld. An object is primitive/elaborate/compound if it is an instance of the corresponding classes.
Before the introduction of compound objects, we focus on the difference between primitive and elaborate. So for the moment, we discuss only the distinction between primitive and elaborate objects.
Primitive objects are “simple”, in a naive sense, ie. built from few indecomposable objects, Elaborate objects are in general more complex, built from many parts. There are different possibilities to move an object, and the efficiency of the method depends on the complexity of the objects,
To understand why the way we move objects depends on the complexity of the object, consider the example of a convex envolop of n points p1,...pn that we move with maps M1,...Mk. There are 2 possibilities to make the computation. First compute the composition M=Mk...M1, then compute Mp1,...,Mpn. The other possibility is to compute M1p1,...,M1pn, M2M1p1...M2M1pn,,,,,Mk...M1p1, ...Mk..M1pn. Since in our model (see below) evaluation requires 4 multiplication and composition 16 multiplications, in the first case, one needs 16(k-1)+4n multiplications. In the second case, one nees 4nk multiplication. Thus, when n<=4 the second method is faster and when n>4, the first method is faster. We could have done more precise computations including additions, but what is important is the general picture : when the objects are simple, it is more rapid to do the computations at each step, and when the objects are complicated and carry a lot of information, like for a polygon with a lot of points, a better option is to only remember the maps used to move the objects, without moving the object effectivly, and to perform the actual computations only at the end, at the level of the raytracer. For these objects, we transmit to the raytracer the indecomposable the parts at the time the object was created along with the matrix M=Mk...M1 used to move the object.
We shall call the simple objects for which we do the computations directly “primitive objects”. The objects for which we only compute the matrix M and send it to the raytracer are called “elaborate” objects. Elaborate objects are in general assembled from many elements.
Formally, they are distinguished by the way the method self.move_alone is defined. In the class Elaborate, we implements a method move_alone which basically computes the matrix M before sending it to the raytracer. For primitive objects, move_alone is defined specifically for each object, it is not a generic method of a class.
An other element comes into the scene. Some objects are simple, but we need to declare them as “elaborate” objects since a direct calculation for them is not possible. Consider a line L defined by two points p1,p2. It is a primitive object, and M(L) is the line through M(p1),M(p2). Thus, to move L, we plainly move its parts. Suppose now that we want to apply the same strategy to a a portion of bounded cylinder, simply called “cylinder” in several raytracers. There are two faces of this cylinder which are disks and the cylinder is defined by 3 “parts” p1,p2,R: one center for each disk and one radius. One could try to move C by moving its parts and say that M(C) is defined by M(p1),M(p2) and R. However it is true only if M is an orthogonal transformation: The orthogonality between the disks and the axis of revolution is not preserved by a general map M. Thus for a cylinder, we shall transmit the initial points p1,p2, and R and M to the raytracer, even if it is defined by simple data: it is an elaborate object.
In general, the conclusion of the above discussion is that an object is necessarily elaborate if it is an instance of a class which is not stable by affine transformation. This includes cylinders, cubes... Since the primitive objects must be simple and stable by affine transformations: we finally end up with a small list: points,vectors,lines,planes,conics and quadrics...
Remark how the classifications decomposable/indecomposable and primitive/elaborate interact. Indecomposable objects, like points or planes, are always primitive. Decomposable objects can be primitive (ex: lines through two points) or elaborate (ex: cylinder).
Elaborate and primitive Objects: how they are implemented
Let’s sum up the above discussion in terms of implementation
- Primitive instances self are simple objects defined by a list of indecomposable components which are attributes of self which are computed in real time, ie. recomputed each time the object is moved. Elaborate instances have an attribute self.parts which is a list of indecomposable elements at the time of creation and a map self.mapFromParts.
- The raytracer is able to draw a picture using:
- The attributes of a Primitive self
- The lis self.parts and the map self.mapFromParts for a Elaborate self
- The primitive objects are basic mathematical objects and they are defined in the file mathutils. The elaborate objects are defined in the file elaborate.py
- Every class of a primitive object is stable by affine transformations
- The rule to move an object self with the map M depends of the type
- If self is primitive, self.move_alone(M).attribute is defined specifically for each attribute and self.move_alone(M).mapFromParts=self.mapFromParts=Identity
- If self is elaborate. self.move_alone(M).parts=self.parts self.move_alone(M).mapFromParts=M*self.mapFromParts.
It is not absolutly necessary to send the mapFromParts to the raytracer for primitives objects since it is Identity, but we keep it in the definition of the objects as it gives a unified way to access to every object, primitive or elaborate.
For elaborate objects, the attribute self.parts is a data at the time of creation and has no meaning after moving. Thus it is not intended to be accessed directy by the user. The access to different elements is via methods and/or markers (see below) which give the elements in real time. Moreover, the risk if the user access to self.parts that he breaks the object.or instance, if a single corner of a cube is adopted by a parent, moving the parent will yield a mess in the cube.
The rendering is completly defined by a camera : what are the objects seen or not seen, what lights are useful, what file are used for the rendering, the pre-hooks or post-hooks to be applied ... All these informations are defined as attributes of the camera.
This gives the possibility to describe the scene completly. Then using several cameras, we get different views from different points. Each camera may show only part of the scene to view clearly some details, with different lights if necessary.
Remark that the orientation of the world depends on the camera (camera.directFrame=False by default) because it depends on the image rendered (an symmetry in a mirror changes the orientation) whereas the conventions for orientation are governed by a global variable ( screwPositiveRotations=True by default).
The camera carries a visibilityLevel between 0 and 1. All objects whose visibility is at least the visibility of the camera are seen. Objects with visibility smaller than the visibilityLevel of the camera are not seen.
The rendering in py2pov is done in several steps:
- self.modifier() computes the modifier of the object ( no image and no shadow,no_reflection if insufficient visibility, texture)
- self.object_string_but_csg() computes the object, forgetting the csg operations that apply on it.
- self.object_string_alone computes the object, including csg
- self.object_string_recursive computes the objects, and all its child recursivly
Taking copies or not in CSG
There are some reasons to consider union on one side, and intersection and difference on the other side in very different ways, also the mathematical operations seee similar. Difference concern the problemes of copy, the problem of compatibility with the genealogy system and with the visibility constants.
First we make a hole in an object A using an intersection with B, when we move A, we expect the hole to stay in the same position relatively to A, ie the hole follow A, even when B does not move. Similarly, if we move B alone, we don’t want the hole to move in A, as it would be very different from our habits in the usual world : when we move a tool used to make a hole, the hole stays ! This means that we need to construct A-copyOfB where copyOfB is adopted by A when the input is A-B.
Taking the copy to build difference also has a virtue: we avoid circular dependencies in the boolean constructions. Blender refuses to take B a parent of A when constructing A-B. We have no problem with that because of the copy : even if B is a parent of A, the copy of B is defined to be a child of A.
For a union, this is different. A union is not really a new object, it is just some abstract logic to consider separate elements as a unique entity. If a queen is on a chess board, and if A=Q U B is the union of the queen Q and the board B, if we move the queen and ask for a picture of the union A, we expect that the queen has moved on the board. For this reason, we don’t want to take a copy of the components for a union.
CSG and visibility
For the visibility and the union it is easy : an element in the union is visible if its visibility is at least the visibility of the camera.
For an object A-B, sometimes we want to see for checking only A, or or A-B, independently of the visibility of the tool B used to cut A. To allow this level of detail in the rendering, we introduce the booleanVisibility attribute.
For a difference A-B, what is seen by the camera is - nothing is visibility(A)<camera.visibilityLevel - A if visibility(A)>camera.visibilityLevel and booleanVisibility(B)<camera.visibilityLevel - the difference A-B if visibility(A)>camera.visibilityLevel and booleanVisibility(B)>camera.visibilityLevel In this last case, a fullcopy of B is computed, adobted by A, and A is replaced by A-fullcopy(B).
CSG and slave terminology
In this context, when we define A-[B,c], we say that A is the master and that B and C are the slaves of A. For the intersection, we proceed similarly to the intersection case since, A cap B = A - (complementary of B). For a union, there are only slaves, and no master, to respect the symmetry of the union.
CSG and compatibility with the parenting system
As we say, with differences, the problem of circular dependencies have been solved thanks to copies.
With unions, there is the problem that we have already implemented a sort of union with the genealogy concepts of children/parent, also more suttle since the union goes one way : moving A moves its children but not its parents.
To implement a union, we cannot take any union of objects. There are some limitations, to insure the compatibility with the genealogy system.
If a compound is a union of objects considered as a single object: two compounds must not intersect. Moreover, if p and q are elaborate/primitive, which are in the compound with p ancestor of q, all the genealogy segment (ie. all the nodes in the segment) from q to p should be in the compound. Finally, if p and q are roots of the compound, ie nodes without parent in the compound then, if both p and q have parent outside the compound, parent(p)=parent(q).
With these limitations, everything should be implementable for compounds: the children are the children of the nodes outside the compound, the parent is the common parent to all roots. move_alone moves all the nodes, the copy of the compound is the copy of the nodes, the representation without children represents all the nodes.
To avoid all these technicalities, we shoul not implement a union in general, only in some cases where the technicalities can be avoided (see the Compound class below). In practice, the compound Class and the parenting system are sufficient to easily build the objects we need.
The Compound class: why we need it
It is not clear a priori that we need a compound class to implement unions. If an object O is a union of a,b,c,d , one could simply define an empty O and declare a,b,c,d as children of O. Then moving O would automatically the children and the camera rendering O would render a,b,c,d.
The problems with this method is that intersection with O is not possible, or more precisely requires too much work since the intersection does not intersect with the children. Moreover, all the markers that we have built in a,b,c,d are lost if we have access only to O.
Of course we could consider a,b,c,d and build a new object in the class elaborate as a workaround. However, this requires some work. We want something easier to easily build librarys of objects. We want the possibility to quickly define an object by compound: We just build the objects we are interested in, declare a compound, and that’s it. This class should automatically rebuild for us the markers that we need to manipulate the objects and be manipulable in csg operations and displacements.
As users of the modeler, we don’t want to think about the type of objects ( primitive, elaborate or compound) that we use, After a declaration myObject=MyClass(parameters), the object is moved by the same method myObject.move(map) whatever the type of myObject is. This is only when we code the object that we must take care of the class to which it belongs.
It is very important that this stuff is transparent for the user of the modeler. We need to check that every operation is accessible in a unified methodology for primitive,elaborate and compound. The technical complexity of the code should not break the ease of use for day to day work on the modeller.
Implementation of Compound objects:
Compounds objects are instances of the class Compound defined in compound.py
As a compound can be defined as a single object in povray, using the union keyword: implementation is easy, as long as the limitations explained above for the compatibility with the genealogy system are satisfied.
Remark that these limitations are automatically satisfied when the objects p_i are disconnected from the rest of the world and parented between them, and all assembled together in a compound. Then the children of the compound form an empty list, and it has no parent, the list of nodes is the p_i. This is the context in which it is implemented at the moment. In this simplifying context, the children are not computed from the tree, but the list is built as for the other objects. In other words, in this context, the compound self is just an objectInWorld with the usual attributes, and a list of nodes self.parts, and a list of markers (see below).
Move_alone move each part of the compound but we never change the matrix identifier of the compound because it would put a mess between the compound and its components which would not have the same location. The recursive move rendering does not change,Intersection, and difference need no modification I suppose.
There are no genealogy relations between the different components of the compound. Otherwise this could do a mess in moving the object.
If we want to go beyond this simple case, to produce a compound from an existing tree of parenting, one should implement many some sanity check. The price seems too high at the moment, but maybe in the future we shall need to implement it. Or maybe only “Views” or “Tags” will be necessary if we want to factorize some attributes like colors.... without moving the objects.
In principle, as users of the software, we only define compound objects, which is easy. Only developpers should construct new primitive or elaborate objects.
Implementation of CSG operations
self.csgoperations=[csgOp] csgOp.keyword=”union” | “intersection” | “difference” csgOp.slaves=[slave...] slave= Primitive | Elaborate | Compound
Recall that for a union, there are slaves, but no master ie self is empty.
Handling objects is easier with markers. For instance, if a solid of revolution keeps its axis of revolution attached as a marker, it will be easy to put a screw along this axis. The user simply defines the axis, then transplants it on the parent setting the visibility to zero. A function child.annotates(parent) is a wrapper for this step.
The definition of objects can be laborious and time consuming. As users, we want something faster. For instance, when we define a cube, we want to access to the center of the cube with self.center without defining the center itself. Besides the center of the object, we want many other markers potentially usable by the end user. Since we want to compute the coordinates of the marker, we want them to be primitive objects.
We then face the following difficulty. We want a lof of markers, but many of these markers which can possibly be used by the end-user. We put them just in case we need them. In practice many of them won’t be are not useful leading to unuseful time consuming computations. Each time we move a solid of revolution for instance, if we need to compute the new place of the axis, it will take time, whereas this axis may not appear in any geometrical construction that we perform on this object.
Our answer to have many markers without wasting time is the following.
For primitive objects, this is not a concern. Since these objects are simple, there are in general no markers needed, and if some are needed, we simply enlarge the list self.parts and/or we add a new method to compute a marker in real time.
For elaborate objects or compound objects, we build the markers of self at the time of creation of self and to save them in a name like self.markers.nameOfTheMarker. The object self.markers.nameOfTheMarker is not a child of self, so that it is not moved automatically when self moves, avoiding unuseful computations. At the end of the__init__ function of the class self, we put an instruction self.markers_as_functions(). The produces new attributes for self. Namely, for each attribute name markerName in self.markers, the instruction builds a callable attribute self.markerName such that self.markerName() returns self.markers.markerName.copy().move_alone(self.mapFromParts). In other words, the new attributes are functions which compute the markers in real time, where the name of the function is similar to the name of the marker. The decorator builds self.markersList which returns the list of Markers to have a global view of the existing markers for an object.
The recommended implementation of the above to produce a new class of elaborate objects is: - declare self.markers=Object() - populate self.markers with self.markers.markerName=theMarkerConstructeInSomeWayInFunctionOfTheParameters. - conclude with self.markers_as_functions().
Let’s be careful when defining functions using markers not to evaluated markers in the evaluation of the function. For instance, for the center of a cube with a marker box, the def
def centerCube(self): return self.box().center()
is correct whereas
is not correct : the marker box() is evaluated on the right side and centerCube will not move with the box.
Remark that all the instances of the class have more or less the same method, thus we could have built the callable as in instance of the class instead of one callable by object. This would be more memory efficient, but as users, we would need to write: MyClass.markerName(self)() instead of self.markerName() Since the callable has a very small footprint, basically one line, we have chosen the facility for the input.
Architecture in Brief
- generic.py : define the class ObjectInWorld which basically defines how objects are moved, parented, intersect,...
- mathutils.py : define some math and the “pure math objects”: plane,line,vectors..., defined below as primitive objects
- aliases.py : some global names useful to speed up the data capture.
- genericwithmaths: a continuation of generic, with some functions needing mathutils
- elaborate.py : define elaborate objects ie. complex objects built from many indecomposable ones.
With these modules, one can build a 3D-picture in some file.py. There is an other module:
Calling py2pov.py at the end of file.py, produces a povray file and calls povray for the rendering. In other words, the first three modules are a python modeler of a 3D scene. THe last module is an interpretor from the cadmodeler to povray.
Architecture in Detail
Contains the primitive objects:.... Also introduce the formalism of massic space.
I shall use mass point formalism below. This is unusual, first let me explain why and what it means.
Recall that in math, adding 2 points in an affine space makes no sense, because we find two different results if we make the computation twice with two different frames. To avoid these problems, mathematicias and physicists have introduced the concepts of vectors and point with the rules point+vector=vector and point-point=vector which are valid operations. From the informatics point of view, it is natural to use oriented object languages to declare two classes points and vector and so that the integrity of the comutations is automatically checked : the software raises an exception when the use tries the illegal operation of adding two points, even if these points are represented by a vector with 3 coordinates.
At the level of functions, maps on vectors are linear maps, and they are represented by 3x3 matrices. Maps on points are affine maps. To allow matrix computations on the affine maps, the affine maps are represented with a matrix with bottom line = 0001 and affine points with coordinates xyz are represented by the list xyz1. In theoretical terms, this comes from the fact that an affine map is the restriction of linear map of the projective Casting is necessary: when we add a point and a vector, we need 4 coordinates for the point. and when we evaluate an affine map on a point : we need 4 coordinates for the point if we want to use the matrix formalism.
Thus it is natural to make the distinction between points and vectors, both for integrity check and to allow easy matrix computations. Although this makes the code robust, this has drawbacks : vectors have 3 coordinates and points 4 coordinates and the code this makes the code longer since computing a-b needs to be implemented several times : when both a,b are vectors, when both a,b are points, or when a,b are point and vector. An other drawbak, is that simple operations like taking the middle, 0.5p1+0.5p2 which make sense and are easy in coordinates, are not allowed any more and a special function Barycentre need to be implemented. This is bad in terms of readability since the code Barycentre(p1,p2,.5,.5) is less readable than 0.5*p1+0.5*p2
- Summing up, using classes to distinguis points and vectors:
- the code is more robust We have matrix formalism to evaluate maps both for points and vectors
- Points are implemented with three of four coordinates depending on the operation the code is longer with multiple implementations of addition and difference Simple operations like barycentre need special implementation.
Our goal is to imagine an implementation with the above advantages (integrity check and matrix formalism,), without the inconvenients. We shall below introduce “mass points”. This is a context which unifies points and vectors, thus we don’t need multiple implementations of the same operations, and barycentric formalism is possible in this framework. Finally, we always take 4 coordinates for both points and vectors.
In practical terms, this means that an affine point in the affine space (x,y,z) has mass 1 and is represented by 4 coordinates (x,y,z,1) A vector (x,y,z) has mass 0 and is represented by 4 coordinates (x,y,z,0) Both vectors and affine points are special cases of mass points (x,y,z,m). The addition and external multiplication on mass points are the obvious ones. With this formalism, we recover the usual fact that point+vector=vector and point-point=vector. But we have new objects. For instance, And point of mass 1 + point of mass 1 = point of mass 2, an unusual object rarely used in maths. The middle point 0.5p1+0.5p2 has mass 1, so it is a well defined point.
In this framework, we have:
- unification of affine points and vectors : both leave in the same space which makes addition and other operations easy to implement, while keeping a different type for theses objects.
- possibility of simple notations for barycentre as a linear combination of affine points
- easy formalism for computing with affine maps. In practical term, an affine map is written as a 4x4 matrix with last line=0001 and this simple matrix formalism is good for composition, inversion and evaluation of maps. The theoretical justification is that every affine map is the restriction of a unique linear map on the space of points of mass 1 and the affine maps identify with the massic maps which stabilize the affine space.
The only difficulty is that linear maps (ie maps on the vectors) do not naturally extend to the space of mass points. A linear map f is represented by a 4x3 matrix with last line equal 0. Then to homogeneize all the computations: we represent the map by a 4x4 matrix using an arbitrarily chosen last column. Thus all the maps are “massic maps” and the operations of composition and evaluation are unified for linear and affine maps in the space of massic maps.
There are two simple choices for the last column C of a linear map. We may take C=0000 or C=0001. Both choice work well for composition and evaluation on vectors, but not for invertibility and linear combinations of maps. With the second choice, an invertible linear map is represented by a invertible matrix. With the first choice, the matrix of a linear map f depend linearly on f. The choice C=0 is also useful to debug, as long as we don’t use general massic maps but only affine and linear maps: the type of map, linear of affine, is checked by the bottom right coefficient of the matrix. ( not to be implemented: fragile since me may put general massic maps in the game one day). We take the choice C=0, and we add a function invert_as_linear_map in the class of massic functions to bypass the invertibility problem.
As for the base changes, they are unified as follows. In the massic space, We have linear maps relative to a base. Giving two bases, we may compute a change of coordinates. We identify a basis v1,v2,v3 of the vector space with the basis v1,v2,v3,v4 in the massic space, where v4=0001. Then the base change for the linear map is equal to the base change for the associated massic map. Similarly a frame in the afine space is a base in the massic space. Thus as long as the basis vector_base(v1,v2,v3) is equal to massic_base(v1,v2,v3,v4) and that frame(v1,v2,v3,v4)=massic_base(v1,v2,v3,v4), the change of coordinates for the vector space and for the affine space are performed by the same operation: a base change in the massic space.
The class creation of an elaborate object is as follows. We define the class using a only list of parts and default values in self.listOfParts. Then, using a decorators, we define the init function, and for each part partName we add an attribute self.partName() as documented before. Because the initial parts do not move, they have no genealogy connection with anything : self.parents and self.child are empty. The decorators also make the elements inherit from ObjectInWorld.
# FOR PRIMITIVE define the attributes and move_alone
# FOR ELABORATE define self.parts, self.markers and finish the init by markers_as_functions(Self)
# FOR COMPOUND define self.parts, self.markers and finish the init by self.build_from_parts(self) markers_as_functions(self)
- documented bodies and skeletons
- finir les squelettes et documenter/publier sur le newsgroups povray
- reflechir a enlever les guillemets pour selectionner une boite/un
axe + doc
copy et add_axis sont incompatibles - finir et documenter skeleton :
permettre de recuperer les angles - permettre de poser un pied sur la pedale
- resoudre le allActors si possible en utilisant __main__ more precisely, for each object constructed in mathutils.py or elaborate.py, get the grandparent frame, if this grand parent is main, add the object to the photoGroupList,
- ajouter une page pour les conventions dans la doc
- reflechir aux conventions ( radius ou diameter, interieur vers
exterieur, nom avant l’objet dans les couples)
- faire une fonction add_marker
- delete pointInABox.png
- documented point.glued_on act as a marker, and return point
- ajouter la fonction automove et flip pour positionner les cubes.
- remplacer amputed_by with drilled_by
- comprendre comment gerer les emmerdes dues a numpy
par exemple si a est un ndarray, a in [2,5,7] renvoie un tableau au lieu d’un booleen
- construire les joints de 2 courbes et (apres verif qu’on l’a pas utilise’) enlever coneOverPolygon et prismOverPolygon qui sont des cas particuliers de joints.
- faire des prismes
- documenter les textures
- pour les creations d’objets: faire systematiquement des copies des objets passes en paraemetre en enlevant les enfants
- quand de nombreuses facon de creer : utiliser from_... en methode statique
- deployer en ecrivant la doc progressivement
- penser aux box des compounds
- idee directrice a verifier: les methodes sans argument qui renvoient des non mutables sont transformees en property.
- for each class document using general description,attributes,construction
- self.drill: couper avec un cylindre infini,
- mettre un attribut draw_as pour les markers qui est une macro (bof)