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).
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:
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.
There are three types of applications you can use to view VRML data:
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.
Currently we support VRML by providing a set of macros, functions and data types.
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-nodeout-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-nodeout-stream :diffuse-color color)) (translation-node*out-stream 0.1 0.7 -8) (cylinder-nodeout-stream :radius 3.1))))
This scene has been exported as a file
"http:examples;vrml;scene2.wrl" and looks like this:
Scene 2.
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.
(defuncolored-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-nodeout-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.
(defuncolored-simple-scene-fn(url out-stream) (http:with-conditional-get-response(out-stream :vrml :expires (url:expiration-universal-timeurl)) (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))
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.
Some examples have been written. We would like to hear from users, who may wish to contribute code.
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.
(defuncreate-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.
(defuncube-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-nodestream :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.
(defunexample3(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.
(defunexample3a(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-nodestream :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:
(defuncompute-example3a(url stream) (http:with-conditional-get-response(stream :vrml :expires ((url:expiration-universal-timeurl)) (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.
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.
(defconstant2pi(* pi 2)) (defconstantpi/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.
(defuncreate-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.
(defunarray-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-resource3d-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.
(defunarray-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.
(defunarray-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.
(defunhsv->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.
(defunhsv->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-resourcecolor (: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.
(defunsurface-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-nodestream :per-face-indexed) (material-nodestream :diffuse-color color-list) (coordinate3-nodestream points) (indexed-face-set-nodestream :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.
(defunexample4(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:
(defuncompute-example4a(url stream) (http:with-conditional-get-response(out-stream :vrml :expires (url:expiration-universal-timeurl)) (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.
(defuncompute-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-timeurl)) (example4 stream xn x0 dx yn y0 dy)))) (condition (condition) (declare (ignore condition)) (http:redirect-requesthttp:*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:
There are sure more examples we could provide. Do you have any specific wishes or maybe you even could contribute some code?