Computer Graphics Workshop '97 Problem Set 3

1/10/97

Note: this problem set only seems long. Actually, it requires very little code.

Problems

Problem 1 - Compute a point on a torus

This problem set will lead you through the steps to making a torus. We have chosen a torus because it has several parameters which can be modified; if you have done something like this before, you may create some other type of shape (but not a sphere, cone, cube, or cylinder!). The fourth problem set presupposes the existence of some torus code, so keep this in mind.

The first step is to write the function compute-point-on-torus. The arguments to this function are:

This function should return an SbVec3f containing the x, y, and z coordinates of a point on the surface of the torus.

You can think of this computation as the sum of two 3D vectors. The first ("major") vector goes from the center of the torus to a center point of the "tube", and iterates (using the horizontal index as its counter) along the circle which defines the center of the tube. The second ("minor") vector goes from a center point of the tube to the surface of the torus, and iterates (using the vertical index as its counter) along the circle defined by a radial cross-section of the tube.

To help you along, here are procedure skeletons for computing these major and minor vectors. Note that the indices x-index and y-index provide identical results for 0, x-res, 2*x-res, ...

(define M_PI 3.14159265358979323846)

;; META-HINT: It's not hard. Use trigonometry.

(define (compute-torus-major-vector major-radius
				    x-res
				    x-index)
  (let* ((theta (* (* 2.0 M_PI) (/ x-index x-res)))
	 ;; YOUR CODE HERE to compute x, y, and z.
	 ;; HINT: y is equal to 0.0.
	)
    (new-SbVec3f x y z)))
    
(define (compute-torus-minor-vector minor-radius
				    x-res
				    y-res
				    x-index
				    y-index)
  (let* ((theta (* (* 2.0 M_PI) (/ x-index x-res)))
	 (phi (* (* 2.0 M_PI) (/ y-index y-res)))
	 ;; YOUR CODE HERE to compute x, y, and z.
	 ;; HINT: x and z depend on both phi and theta.
	 ;; y depends only on phi.
	)
    (new-SbVec3f x y z)))
Now write compute-point-on-torus. This function simply calls compute-torus-major-vector and compute-torus-minor-vector, and returns the sum of these vectors. How do you compute the sum of two SbVec3fs? From the manual page:

SbVec3f operator +(const SbVec3f &v1, const SbVec3f &v2)

This is a method on an SbVec3f which takes the second SbVec3f as argument and returns the sum of the two vectors in a new vector. The syntax in Scheme is

(-> vec1 'operator+ vec2)
(This syntax is the same for all operators except operator [] and operator (); those become, in Scheme, operator-brackets and operator-parens, respectively.)

Problem 2 - Make a FaceSet torus

Using the function compute-point-on-torus, we can now write compute-torus-polygons. This function iterates over the "x" (major radius) and "y" (minor radius) dimensions of the torus and computes the coordinates of quadrilaterals which lie on the surface of the torus. These coordinates are packed in one large list, with the first four coordinates as the corners of the first quadrilateral, and so on. One implementation is given below; you are welcome to change it.
(define (compute-torus-polygons major-radius
				minor-radius
				x-res
				y-res
				x-index
				y-index)
  (if (not (= y-index y-res))
      (begin
	(let ((p1 (compute-point-on-torus major-radius
					  minor-radius
					  x-res
					  y-res
					  x-index
					  y-index))
	      (p2 (compute-point-on-torus major-radius
					  minor-radius
					  x-res
					  y-res
					  x-index
					  (1+ y-index)))

	      (p3 (compute-point-on-torus major-radius
					  minor-radius
					  x-res
					  y-res
					  (1+ x-index)
					  (1+ y-index)))
	      (p4 (compute-point-on-torus major-radius
					  minor-radius
					  x-res
					  y-res
					  (1+ x-index)
					  y-index)))
	  (if (= x-index (- x-res 1))
	      (cons p1 
		    (cons p2 
			  (cons p3 
				(cons p4
				      (compute-torus-polygons
				         major-radius
					 minor-radius
					 x-res
					 y-res
					 0
					 (1+ y-index))))))
	      (cons p1
		    (cons p2
			  (cons p3
				(cons p4
				      (compute-torus-polygons
				         major-radius
					 minor-radius
					 x-res
					 y-res
					 (1+ x-index)
					 y-index))))))))
      '()))
Now we can put the torus together. We will need at least three nodes to do so:

You will probably want to add more nodes to the above scene graph (such as an SoMaterial node) to be able to change more parameters of the torus (like its color).

We want the torus to be an "object" that we can interact with in the same way we interact with Open Inventor objects from within Scheme. You should use this wrapper for the "->" function to allow Scheme and C++ objects to share the same syntax, in a similar fashion to the object system used in the adventure game in 6.001.

;;; SICP-ish simple object system for dealing similarly
;;; with Scheme and C++ objects using "send"

(define (send object message . args)
  (if (C++-object? object)
      (eval `(-> ,object ',message ,@args))
      (let ((method (get-method object message)))
	(if (not (no-method? method))
	    (apply method (cons object args))
	    (error "No method named" message)))))

(define (get-method object message)
  (object message))

(define (no-method method)
  'no-method)

(define (no-method? method)
  (eq? method 'no-method))
Using this wrapper, and the structure for your Scheme objects described below, you can now use this "send" function to send messages to native Inventor objects and objects you have written in Scheme in the same way. For example:

> (define root (new-SoSeparator))
> (send root 'ref)
> (define torus (new-Torus 10 3 20 10)) ;; new-Torus 
                                        ;; defined below
> (send root 'addChild (send torus 'getGeometry))
Now define the "new-Torus" function, which is the constructor for a new Torus object:
(define (new-Torus major-radius 
		   minor-radius
		   x-res
		   y-res)
  (define root (new-SoSeparator))
  (send root 'ref)
  (define coords (new-SoCoordinate3))
  (define mat (new-SoMaterial))
  (define face-set (new-SoFaceSet))

  ;;; ... YOUR CODE HERE to build scene graph ...

  (let ((self
	 (lambda (message)
	   
           ;;; Method to return pointer to root of scene graph
	   (cond ((eq? message 'getGeometry)
		  (lambda (self)
		    root))

		 ((eq? message 'generate)
		  (lambda (self)
		    (let* ((points (compute-torus-polygons major-radius
							   minor-radius
							   x-res
							   y-res
							   0
							   0))
			   (num-points (length points)))
		      ;; set-mfield-values! sets the values of the passed
		      ;; mfield, starting at the given index, from the
		      ;; passed list of values.
		      ;; Note that the elements of this list must be valid
		      ;; objects for use in the set1Value method.
		      (set-mfield-values! (send coords 'point) 0 points)
		      ;; Truncate list if necessary (i.e., when
		      ;; reducing resolution)
		      (send (send coords 'point) 'setNum num-points)

		      ;; update the values in the face set: 
		      ;; (num-points / 4) polygons of four vertices each.
		      (let loop
			  ((i 0))
			(send (send face-set 'numVertices) 'set1Value i 4)
			(if (< i (/ num-points 4))
			    (loop (1+ i))))
		      ;; Truncate numVertices as well, if necessary
		      (send (send face-set 'numVertices) 'setNum
			    (/ num-points 4)))))

	         ;;; YOUR CODE HERE for other methods which include 
	         ;;; setColor, setMajorRadius, setMinorRadius, 
	         ;;; setHorizResolution, and setVertResolution.

	         ;;; As an example: setMajorRadius
		 ((eq? message 'setMajorRadius)
		  (lambda (self new-major-radius)
		    (set! major-radius new-major-radius)
		    (send self 'generate)))

	         ;;; After all other methods' definitions,
	         ;;; if we haven't been able to find this method,
	         ;;; signal an error

		 (else (no-method message))))))

    ;;; Initialization code goes here
    (send self 'generate) ;; generate when first constructed
    self))
Each method takes the current object as its first argument (i.e. (lambda (self arg1 arg2 ...))). Note that as in the example, all of the "set" methods end with a call to
(send self 'generate)
This allows the torus object to be operated upon in a similar fashion to other Inventor objects; when a "field" in the torus is changed (i.e. the major radius) the scene graph is automatically redrawn to reflect that change.

Problem 3 - Make the torus look better

Once you have the torus on the screen, you may notice that it looks like it is made out of tiles; it is not smoothly shaded. This happens because Inventor does not know that it should smoothly interpolate the color between adjacent polygons.

Look at the man page for SoShapeHints. It has three fields: vertexOrdering, shapeType, faceType, and creaseAngle. The creaseAngle field specifies (in radians) the maximum angle between adjacent polygons before they stop being smoothly shaded together. In Inventor 2.1 the default crease angle is 0; in previous releases it was 0.5. If your torus is not smoothly shaded, add a shape hints node to your scene graph and increase its crease angle to 0.6 or 0.7.

To make the torus render faster, you can enable backface culling; this prevents polygons not facing the camera from rendering. It is only appropriate to enable this feature for solid shapes; but since normally you can not see the "inside" of the torus, we can use it here.

Add a shape hints node to your graph and set the shapeType field to be SOLID:

(send (send shape-hints 'shapeType) 'setValue SoShapeHints::SOLID)
Now, depending on whether you ordered the vertices in your polygons clockwise or counterclockwise with respect to the outside of the torus, set the vertexOrdering field to CLOCKWISE or COUNTERCLOCKWISE:
(send (send shape-hints 'vertexOrdering) 'setValue
    SoShapeHints::CLOCKWISE)
;; or:
(send (send shape-hints 'vertexOrdering) 'setValue 
    SoShapeHints::COUNTERCLOCKWISE)
(If you have the wrong ordering, you will know: the inside of the torus will be visible on the screen, and will not look like a physically realizable object.) Add methods to your torus to be able to switch back and forth between the SOLID and UNKNOWN_SHAPE_TYPE shape types. Note the difference in rendering speed.

Problem 4 - Making a faster torus

Unfortunately, by using a FaceSet to represent the polygons in the torus, each vertex must be recomputed four times (once for each of the four polygons it's contained in). If we want to interactively modify the parameters of the torus (as we will in the next problem set), we'll want the regeneration of the torus to be as fast as possible, which means computing each point only once.

Look at the man page for SoQuadMesh. This node constructs a "sheet" of quadrilaterals out of a "grid" of coordinates, specified in rows. A torus can be constructed out of this sheet by attaching the top and bottom edges to make a cylinder and then "bending" the cylinder into a ring. In order to use an SoQuadMesh, we must change the list of coordinates we insert into the SoCoordinate3 node so they conform to this ordering. Here is a function, compute-torus-points, which returns an appropriate point list:

(define (compute-torus-points major-radius
			      minor-radius
			      x-res
			      y-res)
  (define (vert-loop vert-step horiz-step)
    (if (<= vert-step y-res)
	(cons (compute-point-on-torus major-radius 
				      minor-radius
				      x-res
				      y-res
				      horiz-step
				      vert-step)
	      (vert-loop (1+ vert-step) horiz-step))
	'()))
  (define (horiz-loop step)
    (if (<= step x-res)
	(cons (vert-loop 0 step)
	      (horiz-loop (1+ step)))
	'()))
  (let ((slices-list (horiz-loop 0)))
    (apply append slices-list)))
Note that the horizontal and vertical loops go from 0 to x-res and y-res, respectively. This means that there are duplicate points along the "seams" of the torus. Without the duplicate points, the last row and column of polygons would not appear, and the torus would have two orthogonal "strips" missing (try it -- after you've got it working!). Modify the scene graph of your torus to add an SoQuadMesh node instead of the SoFaceSet node. Now modify the generate method of your torus to set up the verticesPerColumn and verticesPerRow fields of the QuadMesh node.
	  ((eq? message 'generate)
	   (lambda (self)
	     (let* ((points (compute-torus-points major-radius
						  minor-radius
						  x-res
						  y-res))
		    (num-points (length points)))
	       ;; Set up Coordinate3 node as before
	       (set-mfield-values! (send coords 'point) 0 points)
	       (send (send coords 'point) 'setNum num-points)

	       ;; YOUR CODE HERE to set up the verticesPerRow and
	       ;; verticesPerColumn fields of the QuadMesh.
	       ;; Note that verticesPerRow is equal to y-res+1,
	       ;; and verticesPerColumn is equal to x-res+1

	       )))
After you have finished the above steps and have gotten the torus to render using the quad mesh, you may have to change the vertexOrdering field of your ShapeHints node if your torus shows up very dark on the screen.


Back to the CGW '97 home page

$Id: index.html,v 1.13 1997/01/10 18:48:25 kbrussel Exp $