VRML Generation

Overview

  1. About
  2. Introduction
  3. Viewing VRML
  4. VRML Generation
  5. VRML Generation and CL-HTTP
  6. Examples
  7. The VRML1.0 Lisp Code
  8. Online Information about VRML


  1. About
  2. VRML is a description language for 3d scenes. Future standards will be able to describe interactive worlds.

    ANSI Common Lisp is a highly interactive and dynamic object-oriented programming language. It enables development of complex software systems.

    This generation package is especially designed to be incorporated into CL-HTTP, a web server written in Common Lisp. We do not provide a tutorial how to use VRML, but the Lisp code has extensive documentation for each construct. The software has been written by Rainer Joswig (Lavielle EDV Systemberatung GmbH & Co., Hamburg, Germany) and John C. Mallery (MIT AI Lab).

  3. Introduction
  4. With this software we currently support VRML 1.0. In future release some of the basic mechanism may change to better support newer versions of the VRML specification (VRML 2.0) and VRML implementations with extensions like Live3d from Netscape.

    The VRML1.0 Common Lisp package supports almost all VRML 1.0 features. Operators generate VRML text for output streams. There is currently no feature for representing internal scene graphs and reading VRML code. We expect that some Common Lisp users may generate VRML scenes from self written code or use already existing 3d libraries. For these existing 3d libraries you may want to use a generation package like this to produce VRML code from existing internal 3d representations.

    One of the main characteristics of CL-HTTP is its implementation and full programmability in a high level language: ANSI Common Lisp. Many different methods of high-level abstractions are supported by Common Lisp. The dynamic nature of CL-HTTP makes it a good choice for experimenting with 3d data generation and serving 3d content over the Internet. CL-HTTP easily can be used to generate WWW interfaces for already existing complex Common Lisp based software. These large software packages often have the need for multi-modal data presentation.

    There are a lot of potential visualization uses for this package. Here are some of them:

    Maths (Scientific Visualization, Statistics, Geometry, ...)
    Create 3d surfaces and 3d plots from observed or calculated data sets. You can build charting and plotting software on top of this VRML package.
    Graphs, Trees, Maps, Plants, ...
    Displaying 3d graphs and trees may create impressive looks for your documents. Let users navigate through 3d graph-like displays of data which would aid them in understanding complex contexts. These data types (which are typical for AI applications) now can be generated and served for display over the Internet.
    Interactive Games
    CL-HTTP could represent 3d scenes and generate VRML output on the fly. High level abstractions simply the implementation.
    3d User Interfaces
    Experiment with generated and adaptive user interfaces.
    Education
    Why not implement "Blocks World" with CL-HTTP and VRML? Make it possible for students to experiment with newest technology in one perfectly integrated package. CL-HTTP is self-documenting and can be used in leading Common Lisp development systems on Macintosh, Unix and Symbolics.

    But, dear Reader, be warned. The whole area of the VRML specification and viewers for VRML is still rapidly evolving. You may need some will to experiment and you will have to cope with incomplete specifications and implementations.

  5. Viewing VRML
  6. There are three types of applications you can use to view VRML data:

    3d Browsers
    Some viewers for 3d data can also read and display VRML data. These viewers typically won't support inlining VRML or embedded URLs.
    VRML Browsers
    VRML browsers support accessing VRML data on the WWW.
    WWW Browsers
    Some of the newer browsers (Netscape Navigator and Microsoft Internet Explorer) have plugins that support viewing VRML files inline. That way you can embed VRML viewers in your HTML documents. Other browsers need external helper applications (3d Browsers or VRML Browsers).

    Contact the VRML archives (see below for pointers) about getting software for displaying VRML content.

  7. VRML Generation
  8. Currently we support VRML by providing a set of macros, functions and data types.

    A Simple VRML Scene

    This is an example for an extremely simple VRML scene. The VRML operators are accessible from package VRML1.0.

    (let ((out-stream *standard-output*))
      (with-vrml-world (:stream out-stream)
        (with-separator-group (out-stream)
          (translation-node* out-stream 0.1 0.7 -8)
          (cube-node out-stream :width 1.1))))

    Here we have declared that we want to generate VRML code on stream OUT-STREAM by using the macro WITH-VRML-WORLD. WITH-SEPARATOR-GROUP is being used to group the next two constructs, because VRML1.0 accepts only one toplevel construct. Then we use two VRML node operators: TRANSLATION-NODE* and CUBE-NODE. This Common Lisp code generates the following VRML output on *STANDARD-OUTPUT*:

    #VRML V1.0 ascii
     
    Separator {
    Translation {
      translation 0.1 0.7 -8
    }
    Cube {
      width 1.1
    }
    }

    This scene looks like this: Scene 1.

    Additionally we may want to specify a color and we're using a cylinder instead of the cube. LET-FIELDS helps us allocating and deallocating resources. In this case we use a resource of type COLOR. Colors are RGB records. Each color slot is a float in the range of 0.0 upto 1.0. MATERIAL-NODE then specifies the color (by using a diffuse color) of the following node.

    (let ((out-stream *standard-output*))
      (with-vrml-world (:stream out-stream)
        (with-separator-group (out-stream)
          (let-fields ((color color :red 1.0 :green 0.5 :blue 0.1))
            (material-node out-stream :diffuse-color color))
          (translation-node* out-stream 0.1 0.7 -8)
          (cylinder-node out-stream :radius 3.1))))

    This scene looks like this: Scene 2.

  9. VRML Generation and CL-HTTP
  10. Exporting VRML Data

    CL-HTTP can export VRML files and Common Lisp functions that generate VRML on the fly. The mechanism works just like exporting HTML, but you have to specify a different data type: :VRML-WORLD for .wrl files and for computed URLs you have to specify :VRML as your generated data type. The VRML data is accessible via the URL. The browser may display the VRML scene inside one of its windows or it may use an external helper application. This depends on your browser capabilities and your browser configuration.

    The first example shows how to export a VRML file via EXPORT-URL. The type of document to export is :VRML-WORLD.

    (export-url #u"/cl-http/vrml/scene2.wrl"
                :vrml-world
                :pathname (pathname "http:www;cl-http;vrml;scene2.wrl")
                :documentation "A colored simple scene with a cylinder."
                :keywords '(:cl-http :demo :vrml))
     

    This example defines a function which generates VRML code and then we export the function.

    (defun colored-simple-scene-fn (out-stream)
      (with-conditional-get-response
        (out-stream :vrml :expires (url:expiration-universal-time url))
          (colored-simple-scene out-stream)))
     
    (export-url #u"/cl-http/vrml/scene2-fn.wrl"
                :computed
                :response-function #'colored-simple-scene-fn
                :documentation "A colored simple scene with a cylinder.
                                Exported as a function."
                :keywords '(:cl-http :demo :vrml))
     

    Using Embedded VRML

    Some newer browsers are capable of embedding display for VRML data into HTML documents. With this feature you can create some spectacular looking documents. Imagine having active 3d data displays (also using links to the Internet) embedded into your documents. CL-HTTP provides the macro NETSCAPE2.0:EMBED-OBJECT to specify which object (denoted by an URL) should be embedded with which size. Here is an example in HTML:

    <EMBED SRC="/cl-http/vrml/scene2.wrl" HEIGHT=200 WIDTH=200>

    The example for CL-HTTP:

    (netscape2.0:embed-object "/cl-http/vrml/scene2.wrl"
                              :stream *the-output-stream*
                              :width 200
                              :height 200)

    The above HTML code is being used below. You'll need a browser capable of displaying embedded data and maybe an VRML plugin.

  11. Examples
  12. Some examples have been written. We would like to hear from users, who may wish to contribute code.

    Some Cubes

    Here we use the content of a two-dimensional array to specify the height of cubes. The cubes are arranged in a rectangular fashion.

    So let us start with a function CREATE-EXAMPLE-ARRAYthat generates a two-dimensional array and fills it with a random value below MAX.

    (defun create-example-array (i-dimension j-dimension max)
      (let ((array (make-array (list i-dimension j-dimension)
                               :element-type 'float)))
        (loop for i from 0 below i-dimension
              do (loop for j from 0 below j-dimension
                       do (setf (aref array i j)
                                (* 2 (random max)))))
        array))
     

    Now that we can create such an array, we need a function to display it. We show each a value in the array as a cube, where the height depends on the value. The cubes are arranged on an rectangular grid. We also make it possible to change the attributes of each cube at a later point. For this purpose we define a parameter INSERTER, which takes a procedure as an argument. This procedure can create additional output on the VRML stream.

    (defun cube-array (stream array &key (cube-size 0.6) inserter)
      (flet ((default-inserter (fun stream)
               (funcall fun stream)))
        (unless inserter
          (setf inserter #'default-inserter))
        (with-separator-group (stream)
          (loop for i from 0 below (array-dimension array 0)
                for x from 0.0 by 1.0
                do (loop for j from 0 below (array-dimension array 1)
                         and z from 0.0 by 1.0
                         for element = (aref array i j)
                         do (with-separator-group (stream)
                              (funcall inserter
                                       #'(lambda (stream)
                                           (translation-node* stream x (/ element 2) z)
                                           (cube-node stream
                                                      :width cube-size
                                                      :depth cube-size
                                                      :height element))
                                       stream)))))))
     

    This third example is now complete and we define a function that generates the VRML world and calls our example function CUBE-ARRAY with a five by four array of random values generated by CREATE-EXAMPLE-ARRAY.

    (defun example3 (stream)
      (with-vrml-world (:stream stream)
        (cube-array stream
                    (create-example-array 5 4 (random 3.0)))))
     

    This scene looks like this: Scene 3.

    With a simple modification we also can assign each cube a random color. We define a local function RANDOM-INSERTER, which inserts the necessary color information for each cube.

    (defun example3a (stream)
      "Similar to example3. Shows the use of the inserter parameter.
    Changes the color of each cube."
      (flet ((random-inserter (fun stream)
               (let-fields ((color color
                                   :red (random 1.0)
                                   :green (random 1.0)
                                   :blue (random 1.0)))
                 (material-node stream :diffuse-color color)
                 (funcall fun stream))))
        (with-vrml-world (:stream stream)
          (cube-array stream
                      (create-example-array 5 6 (random 3.0))
                      :inserter #'random-inserter))))
     

    This scene is being exported as a function, so that each call generates a different array. It looks like this: Scene 4.

    Scientific Visualization

    This example is a lot more complicated. It shows how to generate a surface, which consist of a polygon mesh build by triangles. Each triangle can have a different color. Again the data we use is being defined by an two-dimensional array. This time we fill the contents of the array by an double complex sine function: sin(sin(c)). The absolute value gives us the height of the data point and the phase angle will be used to calculate a color. Thus every entry in the two-dimensional array will consist of two values: height and color. From this array we will generate a list of triangle faces and a list of colors.

    We define two constants: 2*pi and pi/3.

    (defconstant 2pi (* pi 2))
    (defconstant pi/3 (/ pi 3.0))

    The next function fills a 2d array with values from a function. FN takes two arguments: x and y. XN is the number of samples on the x-axis. X0 is the start point on the x-axis. DX is the step width on the x-axis. YN is the number of samples on the y-axis. Y0 is the start point on the y-axis. DY is the step width on the y-axis. ELEMENT-TYPE is the element-type of the array to create. ARRAY is a default array. If ARRAY equals NIL, than a new array is being created.

    (defun create-array (fn xn x0 dx yn y0 dy
                            &key (element-type t) array)
      (unless (arrayp array)
        (setf array (make-array (list xn yn) :element-type element-type)))
      (loop for i fixnum from 0 below xn
            for x from x0 by dx
            do (loop for j fixnum from 0 below yn
                     for y from y0 by dy
                     do (setf (aref array i j)
                              (funcall fn x y))))
      array)
     

    The next function creates a list of points. The points being the grid of the array. KEY is a function which extracts the height from the array content. It calls (ALLOCATE-RESOURCE 3D-FLOAT-VECTOR ...) to create the points.

    (defun array-to-point-list (array &key (key #'identity))
      (loop with points = nil
            for i downfrom (1- (array-dimension array 0)) downto 0 
            and x downfrom (float (1- (array-dimension array 0))) by 1.0
            do (loop for j downfrom (1- (array-dimension array 1)) downto 0
                     and z downfrom (float (1- (array-dimension array 1))) by 1.0
                     do (push (allocate-resource 3d-float-vector
                                                 (:x x
                                                  :y (funcall key (aref array i j))
                                                  :z z))
                              points))
            finally (return points)))

    We now define a function that creates a list of color values from the array. KEY is a function parameter which extracts the color from the array content.

    (defun array-to-color-list (array key)
      (loop with color-list = nil and i-dimension = (array-dimension array 0)
            and j-dimension = (array-dimension array 1)
            for i downfrom (1- i-dimension) above 0
            do (loop for j downfrom (1- j-dimension) above 0
                     for color = (funcall key (aref array i j))
                     do (push color color-list))
            finally (return color-list)))

    We also need to calculate a list of face indices from the array. Faces are triangles.

    (defun array-to-face-index-list (array)
      (loop with index-list = nil and i-dimension = (array-dimension array 0)
            and j-dimension = (array-dimension array 1)
            for i downfrom (1- i-dimension) above 0
            do (loop for j downfrom (1- j-dimension) above 0
                     for index = (+ (* i-dimension j) i)
                     do (push -1 index-list)
                     do (push index index-list)
                     do (push (1- index) index-list)
                     do (push (- index i-dimension) index-list)
                     do (push -1 index-list)
                     do (push (1- index) index-list)
                     do (push (- index i-dimension) index-list)
                     do (push (- index i-dimension 1) index-list))
            finally (return index-list)))

    The next function converts color from HSV to RGB. H is between 0.0 and 2pi. S and V are between 0.0 and 1.0. Returns values for R, G and B. Sure Genera has this already. See: Computer Graphics, Principles and Practice, Second Edition in C, by Foley/van Dam/Feiner/Hughes, Section 13.3.4, The HSV Color Model.

    (defun hsv->rgb (h s v)
      (assert (<= 0.0 h 2pi) (h))
      (assert (<= 0.0 s 1.0) (s))
      (assert (<= 0.0 v 1.0) (v))
      (if (zerop s)
        (values v v v)
        (let* ((i (truncate (setf h (/ (if (>= h 2pi) 0.0 h)
                                       pi/3))))
               (f (- h i)))
          (let ((p (* v (- 1.0 s)))
                (q (* v (- 1.0 (* s f))))
                (t1 (* v (- 1.0 (* s (- 1.0 f))))))
            (ecase i
              (0 (values v  t1 p))
              (1 (values q  v  p))
              (2 (values p  v  t1))
              (3 (values p  q  v))
              (4 (values t1 p  v))
              (5 (values v  p  q)))))))

    We need to convert color from HSV to VRML RGB color.

    (defun hsv->color (h s v)
      (multiple-value-bind (red green blue) (hsv->rgb h s v)
        (assert (and (<= 0.0 red 1.0) (<= 0.0 green 1.0) (<= 0.0 blue 1.0))
                (red green blue))
        (allocate-resource color (:red red :green green :blue blue))))

    The next function outputs a surface build by triangles to a stream. Source for the data is a 2d ARRAY. HEIGHT-KEY extracts the height from the 2d array content. COLOR-KEY extracts the color from the 2d array content.

    (defun surface-from-array (stream array height-key color-key)
      (with-separator-group (stream)
        (let ((points nil) (color-list nil))
          (unwind-protect
            (let ((index-list (array-to-face-index-list array))
                  (color-index-list (loop for i from 0
                                          below (* (1- (array-dimension array 0))
                                                   (1- (array-dimension array 1)))
                                          collect i
                                          collect i)))  ; output color for both triangles
              (setf points (array-to-point-list array :key height-key)
                    color-list (array-to-color-list array color-key))
              (material-binding-node stream :per-face-indexed)
              (material-node stream :diffuse-color color-list)
              (coordinate3-node stream points)
              (indexed-face-set-node stream
                                     :coordinate-index index-list
                                     :material-index color-index-list))
            (loop for point in points do (deallocate-resource '3d-float-vector point))
            (loop for color in color-list do (deallocate-resource 'color color))))))

    Finally an example call that generates VRML output for a surface generated by a double complex sine function.

    (defun example3 (stream)
      (with-vrml-world (:stream stream)
        (surface-from-array stream
                            (create-array #'(lambda (x y)
                                              (let ((c (sin (sin (complex x y)))))
                                                (list (min 20.0 (abs c))
    														        (+ pi (phase c)))))
                                          30 0.0 0.12 30 0.0 0.12)
                            #'first
                            #'(lambda (item) (hsv->color (second item) 0.5 0.5)))))
     
     

    This scene looks like this: Scene 5

    More Examples to come

    There are sure more examples we could provide. Do you have any specific wishes or maybe you even could contribute some code?


  13. The VRML1.0 Lisp Code

  14. Online Information about VRML


Author: Rainer Joswig / Mail:joswig@lavielle.com / WWW: http://www.lavielle.com/~joswig/