The goal of my work at Logilab is to create tools to visualize scientific 3D volumic-mesh-based data (mechanical data, electromagnetic...) in a standard web browser. It's a part of the european OpenDreamKit project. Franck Wang has been working on this subject last year. I based my work on his results and tried to improve them.
Our goal is to create widgets to be used in Jupyter Notebook (formerly IPython) for easy 3D visualization and analysis. We also want to create a graphical user interface in order to enable users to intuitively compute multiple effects on their meshes.
As a consequence, we decided to take a look at another 3D framework. Our best candidates were:
ThreeJS and BabylonJS are two well-known Open Source frameworks for 3D web visualization. They are well maintained by hundreds of contributors since several years. Even if BabylonJS was first thought for video games, these two engines are interesting for our project. Some advantages of ThreeJS are:
- wide file formats support (glTF, vtk, obj, collada...), which makes it easy to be up and running with scientific 3D-volumic-mesh-based data
- very valuable features and plugins like LookUpTable, Constructive Solid Geometry (CSG), Octree (there is also an implementation of octree in BabylonJS).
Finally, the choice of using ThreeJS was quite obvious because of its Nodes feature, contributed by Sunag Entertainment. It allows users to compose multiple effects like isocolor, threshold, clip plane, etc. As ThreeJS is an Open Source framework, it is quite easy to propose new features and contributors are very helpful.
As we want to compose multiple effects like isocolor and threshold (the pixel color correspond to a pressure but if this pressure is under a certain threshold we don't want to display it), it seems a good idea to compose shaders instead of creating a big shader with all the features we want to implement. The problem is that WebGL is still limited (as of the 1.x version) and it's not possible for shaders to exchange data with other shaders. Only the vertex shader can send data to the fragment shader through varyings.
So it's not really possible to compose shaders, but the good news is we can use the new node system of ThreeJS to easily compute and compose a complex material for a mesh.
It's the graphical view of what you can do in your code, but you can see that it's really simple to implement effects in order to visualize your data.
You can define your visualization in a .yml file containing urls to your mesh and data and a hierarchy of effects (called block structures).
See https://demo.logilab.fr/SciviJS/ for an online demo.
You can see the block structure like following:
Data blocks are instantiated to load the mesh and define basic parameters like color, position etc. Blocks are connected together to form a tree that helps building a visual analysis of your mesh data. Each block receives data (like mesh variables, color and position) from its parent and can modify them independently.
Following parameters must be set on dataBlocks:
- coordURL: URL to the binary file containing coordinate values of vertices.
- facesURL: URL to the binary file containing indices of faces defining the skin of the mesh.
- tetrasURL: URL to the binary file containing indices of tetrahedrons. Default is ''.
- dataURL: URL to the binary file containing data that you want to visualize for each vertices.
Following parameters can be set on dataBlocks or plugInBlocks:
- type: type of the block, which is dataBlock or the name of the plugInBlock that you want.
- colored: define whether or not the 3D object is colored. Default is false, object is rendered gray.
- colorMap: color map used for coloration, available values are rainbow and gray. Default is rainbow.
- colorMapMin and colorMapMax: bounds for coloration scaled in [0, 1]. Default is (0, 1).
- visualizedData: data used as input for coloration. If data are 3D vectors available values are magnitude, X, Y, Z, and default is magnitude. If data are scalar values you don't need to set this parameter.
- position, rotation, scale: 3D vectors representing position, rotation and scale of the object. Default are [0., 0., 0.], [0., 0., 0.] and [1., 1., 1.].
- visible: define whether or not the object is visible. Default is true if there's no childrenBlock, false otherwise.
- childrenBlocks: array of children blocks. Default is empty.
As of today, there are 6 types of plug-in blocks:
Threshold: hide areas of your mesh based on a variable's value and bound parameters
- lowerBound: lower bound used for threshold. Default is 0 (representing dataMin). If inputData is under lowerBound, then it's not displayed.
- upperBound: upper bound used for threshold. Default is 1 (representing dataMax). If inputData is above upperBound, then it's not displayed.
- inputData: data used for threshold effect. Default is visualizedData, but you can set it to magnitude, X, Y or Z.
ClipPlane: hide a part of the mesh by cutting it with a plane
- planeNormal: 3D array representing the normal of the plane used for section. Default is [1., 0., 0.].
- planePosition: position of the plane for the section. It's a scalar scaled bewteen -1 and 1. Default is 0.
Slice: make a slice of your mesh
Warp: deform the mesh along the direction of an input vector data
- warpFactor: deformation factor. Default is 1, can be negative.
- inputData: vector data used for warp effect. Default is data, but you can set it to X, Y or Z to use only one vector component.
VectorField: represent the input vector data with arrow glyphs
- lengthFactor: factor of length of vectors. Default is 1, can be negative.
- nbVectors: max number of vectors. Default is the number of vertices of the mesh (which is the maximum value).
- mode: mode of distribution. Default is volume, you can set it to surface.
- distribution: type of distribution. Default is regular, you can set it to random.
Points: represent the data with points
- pointsSize: size of points in pixels. Default is 3.
Using those blocks you can easily render interesting 3D scenes like this: