About Primitives
Primitives are objects that you can render. There isn’t really much else in a fluxus scene, except lights, a camera and lots of primitives.
Primitive state
The normal way to create a primitive is to set up some state which the primitive will use, then call it’s build function and keep it’s returned ID (using with-primitive) to modify it’s state later on.
(define myobj (with-state (colour (vector 0 1 0)) (build-cube))) ; makes a green cube (with-primitive myobj (colour (vector 1 0 0))) ; changes its colour to red
So primitives contain a state which describes things like colour, texture and transform information. This state operates on the primitive as a whole – one colour for the whole thing, one texture, shader pair and one transform. To get a little deeper and do more we need to introduce primitive data.
Primitive Data Arrays [aka. Pdata]
A pdata array is a fixed size array of information contained within a primitive. Each pdata array has a name, so you can refer to it, and a primitive may contain lots of different pdata arrays (which are all the same size). Pdata arrays are typed – and can contain floats, vectors, colours or matrices. You can make your own pdata arrays, with names that you choose, or copy them in one command.
Some pdata is created when you call the build function. This automatically generated pdata is given single character names. Sometimes this automatically created pdata results in a primitive you can use straight away (in commands such as build-cube) but some primitives are only useful if pdata is setup and controlled by you.
In polygons, there is one pdata element per vertex – and a separate array for vertex positions, normals, colours and texture coordinates.
So, for example <code>(build-sphere)</code> creates a polygonal object with a spherical distribution of vertex point data, surface normals at every vertex and texture coordinates, so you can wrap a texture around the primitive. This data (primitive data, or pdata for short) can be read and written to inside a with-primitive corresponding to the current object.
(pdata-set! name vertnumber vector)
Sets the data on the current object to the input vector
(pdata-ref name vertnumber)
Returns the vector from the pdata on the current object
(pdata-size)
Returns the size of the pdata on the current object (the number of vertices).
The name describes the data we want to access, for instance “p” contains the vertex positions:
(pdata-set! “p” 0 (vector 0 0 0))
Sets the first point in the primitive to the origin (not all that useful)
(pdata-set! “p” 0 (vadd (pdata-ref “p” 0) (vector 1 0 0)))
The same, but sets it to the original position + 1 in the x offsetting the position is more useful as it constitutes a deformation of the original point. (See Deforming, for more info on deformations)
Mapping, Folding
The pdata-set!
and pdata-ref
procedures are useful, but there is a more powerful way of deforming primitives. Map and fold relate to the scheme functions for list processing, it’s probably a good idea to play with them to get a good understanding of what these are doing.
(pdata-map! Procedure read/write-pdata-name read-pdata-name ...)
Maps over pdata arrays – think of it as a for-every pdata element, and writes the result of procedure into the first pdata name array.
An example, using pdata-map to invert normals on a primitive:
(define p (build-sphere 10 10)) (with-primitive p (pdata-map! (lambda (n) (vmul n -1)) "n"))
This is more concise and less error prone than using the previous functions and setting up the loop yourself.
(pdata-index-map! Procedure read/write-pdata-name read-pdata-name ...)
Same as pdata-map!
but also supplies the current pdata index number to the procedure as the first argument.
(pdata-fold procedure start-value read-pdata-name read-pdata-name ...)
This example calculates the centre of the primitive, by averaging all it’s vertex positions together:
(define my-torus (build-torus 1 2 10 10)) (define torus-centre (with-primitive my-torus (vdiv (pdata-fold vadd (vector 0 0 0) “p”) (pdata-size)))))
(pdata-index-fold procedure start-value read-pdata-name read-pdata-name ...)
Same as pdata-fold
but also supplies the current pdata index number to the procedure as the first argument.
Instancing
Sometimes retained mode primitives can be unwieldy to deal with. For instance, if you are rendering thousands of identical objects, or doing things with recursive graphics, where you are calling the same primitive in lots of different states – keeping track of all the Ids would be annoying to say the least.
This is where instancing is helpful, all you call is:
(draw-instance myobj)
Will redraw any given object in the current state (immediate mode). An example:
(define myobj (build-nurbs-sphere 8 10)) ; make a sphere (define (render-spheres n) (cond ((not (zero? n)) (with-state (translate (vector n 0 0)) ; move in x (draw-instance myobj)) ; stamp down a copy (render-spheres (- n 1))))) ; recurse! (every-frame (render-spheres 10)) ; draw 10 copies
Built In Immediate Mode Primitives
To make life even easier than having to instance primitives, there are some built in primitives that can be rendered at any time, without being built:
(draw-cube) (draw-sphere) (draw-plane) (draw-cylinder)
For example:
(define (render-spheres n) (cond ((not (zero? n)) (with-state (translate (vector n 0 0)) ; move in x (draw-sphere)) ; render a new sphere (render-spheres (- n 1))))) ; recurse! (every-frame (render-spheres 10)) ; draw 10 copies
These built in primitives are very restricted in that you can’t edit them or change their resolution settings etc, but they are handy to use for quick scripts with simple shapes.