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.

    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).

    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
    Browsers like Netscape Navigator and Microsoft Internet Explorer may 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).

    The MIME type for VRML is x-world/x-vrml and a typical file extension would be .wrl .

    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 the Common Lisp 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 the *STANDARD-OUTPUT* stream:

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

    This scene has been exported as a file "http:examples;vrml;scene1.wrl" and 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 has been exported as a file "http:examples;vrml;scene2.wrl" and 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 first argument to EXPORT-URL is the URL we want to export. The #u read macro completes the partial URL with the standard host and port information for the HTTP server. The second argument is the type of document to export: :VRML-WORLD. The next arguments are keyword and value pairs. The third argument is the pathname of the file that is being exported.

    (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))
        

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

    (defun colored-simple-scene (out-stream)
      (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))))
       

    The response function takes an URL and a stream argument. This function is responsible for generating the content. It has to use http:with-conditional-get-response or http:with-successful-response. These macros will return the correct header information to the browser. The response function calls COLORED-SIMPLE-SCENE to generate the VRML content.

    (defun colored-simple-scene-fn (url out-stream)
      (http:with-conditional-get-response
        (out-stream :vrml :expires (url:expiration-universal-time url))
          (colored-simple-scene out-stream)))

    The first argument to EXPORT-URL is the URL we want to export. The #u read macro completes the partial URL with the standard host and port information for the HTTP server. The second argument is the type of document to export: :computed. The next arguments are keyword and value pairs. The third argument is the response function for the computed URL. The response function takes an URL and a stream argument.

    (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 netscape4.0:embed-object to specify which object (denoted by an URL) should be embedded with which size. The URL can point to a computed VRML scene or to a static VRML file. Here is an example in HTML:

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

    The example for CL-HTTP:

    (netscape4.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. This is a typical example for the use of Functional Programming techniques. CUBE-ARRAY is a higher-order function since it takes a function as a parameter. Don't be puzzled by this.

    (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)))))
        

    The scene then needs to be written to a file:

    (write-vrml-file #'example3 "http:examples;vrml;scene3.wrl")

    This file has be exported and the 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))))
        

    We define a response function:

    (defun compute-example3a (url stream)
      (http:with-conditional-get-response
        (stream :vrml :expires ((url:expiration-universal-time url))
          (example3a stream)))

    This response function is being exported:

    (http:export-url #u"/cl-http/vrml/scene4.wrl"
                     :computed
                     :response-function #'compute-example3a
                     :documentation "An 5x4 array of float values as rectangular arrangement of cubes.
    Randomly colored and exported as a function."
                     :keywords '(:cl-http :demo :vrml))

    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 example4 (stream xn x0 dx yn y0 dy)
      (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)))))
                                          xn x0 dx yn y0 dy)
                            #'first
                            #'(lambda (item) (hsv->color (second item) 0.5 0.5)))))
       

    The response function:

    (defun compute-example4a (url stream)
      (http:with-conditional-get-response
        (out-stream :vrml :expires (url:expiration-universal-time url))
          (example4a stream)))

    This scene has been exported as a function and looks like this: Scene 5

    It also is possible to use form-based input to specify parameters for VRML scenes. For this purpose the following response function is exported for a fillout form URL. A response function for a form takes an additional parameter: the QUERY-ALIST. HTTP:BIND-QUERY-VALUES extracts the values as strings from the QUERY-ALIST. W3P:ACCEPT-FROM-STRING then converts the string to the desired data types and signals a condition if the string has not the specified content type. In case of an unusual condition this page is simpy being presented again. For this purpose the call to HTTP:REDIRECT-REQUEST in the HANDLER-CASE form redirects the client to this page.

    (defun compute-example4b (url stream query-alist)
      (http:bind-query-values (xn x0 x1 yn y0 y1)
                              (url query-alist)
        (handler-case (let* ((xn (w3p:accept-from-string '(integer 5 50) xn))
                             (x0 (w3p:accept-from-string '(float 0.0 5.0) x0))
                             (x1 (w3p:accept-from-string '(float 0.0 5.0) x1))
                             (yn (w3p:accept-from-string '(integer 5 50) yn))
                             (y0 (w3p:accept-from-string '(float 0.0 5.0) y0))
                             (y1 (w3p:accept-from-string '(float 0.0 5.0) y1)))
                        (assert (> x1 x0))
                        (assert (> y1 y0))
                        (let ((dx (float (/ (- x1 x0) xn)))
                              (dy (float (/ (- y1 y0) yn))))
                          (http:with-successful-response
                            (stream :vrml :expires (url:expiration-universal-time url))
                              (example4 stream xn x0 dx yn y0 dy))))
          (condition (condition)
             (declare (ignore condition))
             (http:redirect-request http:*server* #u"/cl-http/vrml/vrml.html")))))

    A form will be exported as usual with HTTP:EXPORT-URL. The type is :HTML-FORM. We just don't return the usual HTML code. CL-HTTP dynamically generates VRML in response to the fillout form.

    (export-url #u"/cl-http/vrml/scene5a.wrl"
                :html-form
                :pathname "http:www;cl-http;vrml;vrml.html"
                :response-function #'compute-example4b
                :documentation "A surface generated by a double complex sine function"
                :keywords '(:cl-http :demo :vrml))

    Try the following form:

    Parameter

    Minimum

    Maximum

    Value

    XN

    5

    50


    X0

    0.0

    5.0


    X1

    0.0

    5.0


    YN

    5

    50


    Y0

    0.0

    5.0


    Y1

    0.0

    5.0


    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@nospam.lisp.de / WWW: http://lispm.dyndns.org/