Click here to Skip to main content
15,896,063 members
Articles / Web Development / HTML5

Building a Molecular 3D Viewer using WebGL and HTML5

Rate me:
Please Sign up or sign in to vote.
5.00/5 (8 votes)
5 Jan 2012CPOL3 min read 56K   2.1K   29  
Use JavaScript and HTML5 to build a 3D molecule viewer.
<!DOCTYPE html>
<!--
/*
 * Copyright (c) 2012 Frederick Ackers. All rights reserved.
 * Based on code from Apple example code
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *    * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *    * Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following disclaimer
 * in the documentation and/or other materials provided with the
 * distribution.
 *    * Neither the name of Google Inc. nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
 -->
<html>
  <head>
    <title>Molecular 3D Viewer</title>
    <script type="text/javascript" src="webgl-utils.js"></script>
    <script type="text/javascript" src="webgl-debug.js"></script>
    <script src="J3DI.js"> </script>
    <script src="J3DIMath.js" type="text/javascript"> </script>
	<script type="text/javascript" src="cameracontroller.js"></script>
    <script id="vshader" type="x-shader/x-vertex">
        uniform mat4 u_modelViewProjMatrix;
        uniform mat4 u_normalMatrix;
        uniform vec3 lightDir;

        attribute vec3 vNormal;
        attribute vec4 vTexCoord;
        attribute vec4 vPosition;

        varying float v_Dot;
        varying vec2 v_texCoord;

        void main()
        {
            gl_Position = u_modelViewProjMatrix * vPosition;
            v_texCoord = vTexCoord.st;
            vec4 transNormal = u_normalMatrix * vec4(vNormal,1);
            v_Dot = max(dot(transNormal.xyz, lightDir), 0.0);
        }
    </script>

    <script id="fshader" type="x-shader/x-fragment">
        precision mediump float;

        uniform sampler2D sampler2d;

        varying float v_Dot;
        varying vec2 v_texCoord;

        void main()
        {
            vec4 color = texture2D(sampler2d,v_texCoord);
            color += vec4(0.1,0.1,0.1,1);
            gl_FragColor = vec4(color.xyz * v_Dot, color.a);
        }
    </script>

    <script>
        const minIncAngle = 0.2;
        const maxIncAngle = 2;
        var g = {};  // globals
		var controller = null;
		var g_ctx = null;
		var objects =new Array();
		var object_count = 0;
		var setcamera = false;
		
        function init()
        {
            var gl = initWebGL("molview");
            if (!gl) {
                return;
            }
			var c = document.getElementById("molview");

			//c = WebGLDebugUtils.makeLostContextSimulatingCanvas(c);
			// tell the simulator when to lose context.
			//c.loseContextInNCalls(15);

			c.addEventListener('webglcontextlost', handleContextLost, false);
			c.addEventListener('webglcontextrestored', handleContextRestored, false);
            g.program = simpleSetup(gl, "vshader", "fshader",
                                [ "vNormal", "vTexCoord", "vPosition"],
                                [ 0, 0, 0, 1 ], 10000);
            gl.uniform3f(gl.getUniformLocation(g.program, "lightDir"), 0, 0, 1);
            gl.uniform1i(gl.getUniformLocation(g.program, "sampler2d"), 0);

            if (g.program) {
                g.u_normalMatrixLoc = gl.getUniformLocation(g.program, "u_normalMatrix");
                g.u_modelViewProjMatrixLoc = gl.getUniformLocation(g.program, "u_modelViewProjMatrix");
            }

            g.sphere = makeSphere(gl, 1, 30, 30);

            // get the images
            earthTexture = loadImageTexture(gl, "./c1.jpg");
            molTexture = loadImageTexture(gl, "./h1.jpg");

			controller = new CameraController(c);
			// Try the following (and uncomment the "pointer-events: none;" in
			// the index.html) to try the more precise hit detection
			//  controller = new CameraController(document.getElementById("body"), c, gl);
			controller.onchange = function(xRot, yRot) {
				drawPicture(g_ctx);
			};
			controller.onnewposition = function(xPos, yPos, zPos) {
				drawPicture(g_ctx);
			};
            return gl;
        }

        width = -1;
        height = -1;
        var requestId;

        function reshape(ctx)
        {
            var canvas = document.getElementById('molview');
            if (canvas.width == width && canvas.height == height)
                return;

            width = canvas.width;
            height = canvas.height;

            ctx.viewport(0, 0, width, height);

            g.perspectiveMatrix = new J3DIMatrix4();
            g.perspectiveMatrix.perspective(150, 1, 1, 10000);
            g.perspectiveMatrix.lookat(0,0,20, 0, 0, 0, 0, 1, 0);
        }

        function drawOne(ctx, angle, x, y, z, scale, texture)
        {
            // setup VBOs
            ctx.enableVertexAttribArray(0);
            ctx.enableVertexAttribArray(1);
            ctx.enableVertexAttribArray(2);

            ctx.bindBuffer(ctx.ARRAY_BUFFER, g.sphere.vertexObject);
            ctx.vertexAttribPointer(2, 3, ctx.FLOAT, false, 0, 0);

            ctx.bindBuffer(ctx.ARRAY_BUFFER, g.sphere.normalObject);
            ctx.vertexAttribPointer(0, 3, ctx.FLOAT, false, 0, 0);

            ctx.bindBuffer(ctx.ARRAY_BUFFER, g.sphere.texCoordObject);
            ctx.vertexAttribPointer(1, 2, ctx.FLOAT, false, 0, 0);

            ctx.bindBuffer(ctx.ELEMENT_ARRAY_BUFFER, g.sphere.indexObject);

            // generate the model-view matrix
            var mvMatrix = new J3DIMatrix4();
			// Add in camera controller's rotation
			var pos = controller.getPosition();
			mvMatrix.translate(pos[0],pos[1],pos[2]);
			mvMatrix.rotate(controller.getRoll(), 1, 0, 0);
			mvMatrix.rotate(controller.getYaw(), 0, 1, 0);
			mvMatrix.rotate(controller.getPitch(), 0, 0, 1);
			//mvMatrix.rotate(controller.zRot, 0, 0, 1);
			mvMatrix.translate(-x,-y,-z);
			//mvMatrix.translate(x,y,z);
            mvMatrix.rotate(30, 1,0,0);
            mvMatrix.rotate(angle, 0,1,0);
            mvMatrix.scale(scale, scale, scale);

            // construct the normal matrix from the model-view matrix
            var normalMatrix = new J3DIMatrix4(mvMatrix);
            normalMatrix.invert();
            normalMatrix.transpose();
            normalMatrix.setUniform(ctx, g.u_normalMatrixLoc, false);

            // construct the model-view * projection matrix
            var mvpMatrix = new J3DIMatrix4(g.perspectiveMatrix);
            mvpMatrix.multiply(mvMatrix);
            mvpMatrix.setUniform(ctx, g.u_modelViewProjMatrixLoc, false);

            ctx.bindTexture(ctx.TEXTURE_2D, texture);
            ctx.drawElements(ctx.TRIANGLES, g.sphere.numIndices, ctx.UNSIGNED_SHORT, 0);
        }

        function drawPicture(ctx)
        {
            reshape(ctx);
	        ctx.clear(ctx.COLOR_BUFFER_BIT | ctx.DEPTH_BUFFER_BIT);
          
			for (i = 0; i < object_count; ++i) {
				if (objects[i][0] == "ATOM")
				{
					if (setcamera != true)
					{
						g.perspectiveMatrix.lookat(0, 0, objects[i][7], 0, 0, 0, 0, 1, 0);
						setcamera = true;
					}
			
					drawOne(ctx, 30,
						objects[i][5],
						objects[i][6],
						objects[i][7],
						0.75,
						molTexture);
				}

            }

            ctx.bindTexture(ctx.TEXTURE_2D, null);

            framerate.snapshot();
        }

		function readSingleFile(evt) {
			//Retrieve the first (and only!) File from the FileList object
			var f = evt[0]; 
			if (f) 
			{
				var r = new FileReader();
					r.onload = function(e) { 
					var contents = e.target.result;
					alert( "Got the file.\n" 
						+"name: " + f.name + "\n"
						+"type: " + f.type + "\n"
						+"size: " + f.size + " bytes\n"
						+ "starts with: " + contents);  
						var lines = contents.split("\n");
						object_count = 0;
						objects = [];
						for (i=0; i < lines.length; i++)
						{
							//alert(lines[i]);
							var singleLine = lines[i].split(" ");
							for (x = 0; x < singleLine.length; x++)
							{
								objects[object_count] = new Array();
								var counter = 0;
								for (y = 0; y < singleLine.length; y++)
								{
									if (singleLine[y]!="" && singleLine[y]!=",")
										objects[object_count][counter++] = singleLine[y];
								}
								x = singleLine.length;
								object_count++;
								setcamera = false;
								//controller.setZoomFactor(0.35);
								//alert("Array reads: "+objects[object_count-1]);
							}
						}
						controller.reset();
				}
				r.readAsText(f);
				drawPicture(g_ctx);
			}
			else 
			{ 
			  alert("Failed to load file");
			}
		}
        function start()
        {
            var c = document.getElementById("molview");
            var w = Math.floor(window.innerWidth * 0.7);
            var h = Math.floor(window.innerHeight * 0.9);

            //c = WebGLDebugUtils.makeLostContextSimulatingCanvas(c);
            // tell the simulator when to lose context.
            //c.loseContextInNCalls(1500);

            c.addEventListener('webglcontextlost', handleContextLost, false);
            c.addEventListener('webglcontextrestored', handleContextRestored, false);

            c.width = w;
            c.height = h;

            g_ctx = init();
            if (!g_ctx) {
                return;
            }

            framerate = new Framerate("framerate");
            var f = function() {
                drawPicture(g_ctx)
                requestId = window.requestAnimFrame(f, c);
            };
            f();

            function handleContextLost(e) {
                e.preventDefault();
                clearLoadingImages();
                if (requestId !== undefined) {
                    window.cancelRequestAnimFrame(requestId);
                    requestId = undefined;
                }
            }

            function handleContextRestored() {
                init();
                f();
            }
        }
		function handleContextLost(e) {
			log("handle context lost");
			e.preventDefault();
			clearLoadingImages();
		}

		function handleContextRestored() {
			log("handle context restored");
			init();
		}

		function keyCamera(event){//a key is released
		var cam = controller;
		if(event.shiftKey) {
			switch(event.keyCode) {//determine the key pressed
				case 65://a key
					cam.roll(-Math.PI * 0.25);//tilt to the left
					break;
				case 37://left arrow
					cam.yaw(Math.PI * 0.25);//rotate to the left
					break;
				case 68://d key
					cam.roll(Math.PI * 0.25);//tilt to the right
					break;
				case 39://right arrow
					cam.yaw(-Math.PI * 0.25);//rotate to the right
					break;
				case 83://s key
				case 40://down arrow
					cam.pitch(Math.PI * 0.25);//look down
				break;
				case 87://w key
				case 38://up arrow
					cam.pitch(-Math.PI * 0.25);//look up
				break;
			}
		}
		else {
			var pos = cam.getPosition();
			//alert(pos);
			switch(event.keyCode) {//deterime the key pressed
				case 65://a key
				case 37://left arrow
					cam.setPosition(pos[0]-0.5,pos[1],pos[2]);//move - along the X axis
				break;
				case 68://d key
				case 39://right arrow
					cam.setPosition(pos[0]+0.5,pos[1],pos[2]);//more + along the X axis
				break;
				case 83://s key
					cam.setPosition(pos[0],pos[1]-0.5,pos[2]);//move - along the Y axis (down)
				break;
				case 40://down arrow
					cam.setPosition(pos[0],pos[1],pos[2]+0.5);//move + on the Z axis
				break;
				case 87://w key
					cam.setPosition(pos[0],pos[1]+0.50,pos[2]);//move + on the Y axis (up)
				break;
				case 38://up arrow
					cam.setPosition(pos[0],pos[1],pos[2]-0.5);//move - on the Z axis
				break;
			}
		}
	} 
	function reset(){
		controller.reset();
	}
	
		//document.getElementById('fileinput').addEventListener('change', readSingleFile, false);
    </script>
    <style type="text/css">
        canvas {
            border: 2px solid black;
        }
    </style>
  </head>
  <body onload="start()" onkeydown="keyCamera(event)">
  <table><tr><td width="70%">
    <canvas id="molview">
    There is supposed to be a Molecular 3D Viewer drawing here.
    </canvas>
    <div id="framerate"></div>
	</td>
	<td width="30%">
	<p>The viewer takes a PDB file and parses it for the ATOM entries and displays them.<br />
	Note: Large or complex molecules may be slow in viewer.
	</p>
	<input type="submit" value="Reset View" onclick="reset()">
	<input type="file" id="fileinput" onchange="readSingleFile(this.files)">
	<p>
	Keys are: <br />
	A: Move Along -X Axis<br />
	W: Move Along +Y Axis<br />
	S: Move Along -Y Axis<br />
	D: Move Along +X Axis<br />
	Left Arrow: Move Along -X Axis<br />
	Right Arrow:Move Along +X Axis<br />
	Down Arrow: Move Along +Z Axis<br />
	Up Arrow:   Move Along -Z Axis<br />
	Shift+A: Tilt to the left <br />
	Shift+W: Look Up<br />
	Shift+S: Look Down<br />
	Shift+D: Tile to the right<br />
	Shift+Left Arrow: Rotate to the Left<br />
	Shift+Right Arrow:Rotate to the right<br />
	Shift+Down Arrow: Look Down<br />
	Shift+Up Arrow:   Look UP<br />
	Mouse Drag Left: Rotate X<br />
	Mouse Drag Right:Rotate X<br />
	Mouse Drag Up:   Rotate Y<br />
	Mouse Drag Down: Rotate Y<br />
	Mouse Wheel Forward: Zoom In<br />
	Mouse Wheel Back:    Zoom Out<br />
	</p>
	</td>
	</tr>
	</table>
  </body>
</html>

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Web Developer
United States United States
Programming using MFC and ATL for almost 12 years now. Currently studying Operating System implementation as well as Image processing. Previously worked on DSP and the use of FFT for audio application. Programmed using ADO, ODBC, ATL, COM, MFC for shell interfacing, databasing tasks, Internet items, and customization programs.

Comments and Discussions