How to bring your 3d models from 3ds max to the browser

How to bring your 3d models from 3ds max to the browser

This tutorial shows us a simple method to export 3d meshes from 3ds max to an own Javascript file format. We save a 3d-Mesh including textures and UV mapping from 3ds max using MaxScript and load them into a lightweight WebGL-Engine.

What you need

  • 3ds max
  • A text editor
  • A WebGL compatible browser (Firefox, Chrome, …)

The MaxScript

Just open a new script file in 3ds max, paste the following code and press Strg-E or Evaluate. A small dialog appears. When you have a standard material with a diffuse texture press the “Get Diffuse Filename”-Button to get the name of your texture file. Don’t forget to copy this texture file into the same directory like the JS- and HTML-File later. To export your selected 3d model press “Export” and set your filepath and name.

	rollout JSExport "js  Export"
	(
		global jsexport_name
		global js_export_captionname
		global js_export_pathname
		global js_export_filetype
		global diffuseName

		group "Export JS Mesh" (
			button filenameBtn "Get Diffuse Filename"
			edittext imagePath ""
			button exportSelected "Export"
		)

		on filenameBtn pressed do
		(
			imagePath.text = filenameFromPath $.material.diffuseMap.filename
		)

		on exportSelected pressed do
		(
			jsexport_name = GetSaveFileName caption:"Save 3D as JS:" types:"JavaScript (*.js)|*.js"
			obj = selection
			if jsexport_name != undefined then(
				tmesh = snapshotAsMesh selection[1]
				out_file = createfile jsexport_name
				num_verts = tmesh.numverts
				num_tverts = tmesh.numtverts
				num_faces = tmesh.numfaces
				format "var triangles = [n" to:out_file
				for f = 1 to num_faces do(
					faceVertIndex = getFace tmesh f
					tVertIndex = getTVFace tmesh f
					for i = 1 to 3 do(
						vpos = getVert tmesh faceVertIndex[i]
						format "%,%,%," vpos.x vpos.y vpos.z to:out_file
						vuvpos = getTVert tmesh tVertIndex[i]
						format "%,%," vuvpos.x vuvpos.y to:out_file
						normals = getNormal tmesh faceVertIndex[i]
						nl = sqrt(normals.x^2 + normals.y^2 + normals.z^2)
						nx = (normals.x/nl) as string
						ny = (normals.y/nl) as string
						nz = (normals.z/nl) as string
						if  nl == 0  then (
							nx = 0
							ny = 0
							nz = 0
						)
						format "%,%,%," nx ny nz to:out_file
					)
					format "n" to:out_file
				)
				format "n];n" to:out_file
				format "var vertCount = %;n" num_verts to:out_file
				format "var faceCount = %;n" num_faces to:out_file
				format "var centre = %;n"  selection[1].pos to:out_file
				format "var diffuseMap = "%";n" imagePath.text  to:out_file
				close out_file
			)
		)
	)createDialog JSExport

What you get

After saving you get a JS-File similar this structure:

var triangles = [
19.6391, -20.3159, 38.8219, 1.85714, 1.98571, -0.957021, 0.218757, -0.190408, 20.2092, -15.2357, 38.8219, 2.0, 1.98571, -0.981749, 0.0, -0.190181, 20.4854, -15.2357, 38.0573, 2.0, 2.0, -0.940499, -1.31962e-007, -0.339798,
...
];
var vertCount = 1598;
var faceCount = 3180;
var centre = [0,0,0];
var diffuseMap = "teapot.jpg";

The long row in the triangles variable shows only the position, the UVs and the normalized normals of all three vertices in the first triangle of the mesh. The center point is the pivot position in 3ds max.

The HTML

For this example you have to load three script files. The first one is your exported mesh. For our WebGL-Example we need the GLMatrix-Utility as secound javascript file. The third file will be our little WebGL example

<!DOCTYPE html>
<html>
	<head>
		<title>WebGL</title>
		<script src="teapot.js" type="text/javascript">
		<script src="http://glmatrix.googlecode.com/files/glMatrix-0.9.5.min.js" type="text/javascript">
		<script src="webgl.js" type="text/javascript">
	</head>
	<body bgcolor="#000" style="margin: auto 0" onload="webGLStart();">
		<canvas id="GLWindow"></canvas>
	</body>
</html>

The WebGL-Script

The webgl.js – File is a small 3d program viewer with an ambient, a directional light and a rotation matrix.

var program;
var mvMatrix = mat4.create();
var pMatrix = mat4.create();
var textureMap;
var VertexBuffer;
var gl;
var rotationMatrix = mat4.create();
var mouseTranslation = [0.0, 0.0, 0.0];
var currentAngle = 0;

function checkGLError(func)
{
	var error = gl.getError();
	if(error != gl.NO_ERROR)
	alert("GL error: " + " " +func + " : " + error);
}

function webGLStart() {
	var canvas = document.getElementById("GLWindow");
	initGL(canvas);
	initShaders();
	initBuffers();
	initTexture();

	gl.clearColor(1.0, 1.0, 1.0, 1.0);
	gl.enable(gl.DEPTH_TEST);

	updateLoop();
}

function initGL(canvas) {
	canvas.width = window.innerWidth;
	canvas.height = window.innerHeight;
	var names = ["webgl", "experimental-webgl", "webkit-3d", "moz-webgl"];
	for (var i = 0; i < names.length; ++i)
	{
		try {
			gl = canvas.getContext(names[i]);
		} catch(e) {}
		if (gl) break;
	}
	if (!gl) alert("Kein WebGL verfuegbar!");	

	gl.viewportWidth = canvas.width;
	gl.viewportHeight = canvas.height;  

}

function initShaders() {

	var vertexShaderSource =
		"attribute vec3 aVertexPosition;n" +
		"attribute vec3 aVertexNormal;n" +
		"attribute vec2 aTextureCoord;n" +

		"uniform mat4 uMVMatrix;n" +
		"uniform mat4 uPMatrix;n" +
		"uniform mat3 uNormalMatrix;n" +
		"uniform vec3 uAmbientColor;n" +
		"uniform vec3 uDirectionalColor;n" +
		"varying vec3 vLightWeighting;n" +
		"varying vec2 vTextureCoord;n" +

		"void main(void) {n" +

			"vec3 ulightDirection = vec3(1.0,1.0,0.0);n" +
			"vec3 transNormal = uNormalMatrix * aVertexNormal;n" +

			"float greyScaleVal = max(dot(transNormal, ulightDirection), 0.0);n" +
			"vec3 uAmbientColor = vec3(0.4,0.4,0.4);n" +
			"vLightWeighting = uAmbientColor + vec3(greyScaleVal,greyScaleVal,greyScaleVal);n" +

			"gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);n" +
			"vTextureCoord = aTextureCoord;n" +
		"}";

	var fragmentShaderSource =
		"#ifdef GL_ESn" +
		"precision highp float;n" +
		"#endifn" +

		"varying vec3 vLightWeighting;n" +
		"varying vec2 vTextureCoord;n" +

		"uniform sampler2D uSampler;n" +

		"void main(void) {	n" +
			"vec4 textureColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t));" +
			"gl_FragColor = vec4(textureColor.rgb * vLightWeighting, textureColor.a);" +
		"}";

	var fragmentShader = loadShader(gl.FRAGMENT_SHADER, fragmentShaderSource);
	var vertexShader = loadShader(gl.VERTEX_SHADER, vertexShaderSource);

	program = gl.createProgram();
	gl.attachShader(program, vertexShader);
	gl.attachShader(program, fragmentShader);
	gl.linkProgram(program);

	if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
		alert("Could not initialise program");
	}

	gl.useProgram(program);
	program.vertexPositionAttribute = gl.getAttribLocation(program, "aVertexPosition");
	gl.enableVertexAttribArray(program.vertexPositionAttribute);

	program.vertexNormalAttribute = gl.getAttribLocation(program, "aVertexNormal");
	gl.enableVertexAttribArray(program.vertexNormalAttribute);

	program.textureCoordAttribute = gl.getAttribLocation(program, "aTextureCoord");
	gl.enableVertexAttribArray(program.textureCoordAttribute);

	program.pMatrixUniform = gl.getUniformLocation(program, "uPMatrix");
	program.mvMatrixUniform = gl.getUniformLocation(program, "uMVMatrix");
	program.nMatrixUniform = gl.getUniformLocation(program, "uNormalMatrix");
	program.samplerUniform = gl.getUniformLocation(program, "uSampler");
    program.useLightingUniform = gl.getUniformLocation(program, "uUseLighting");
	program.ambientColorUniform = gl.getUniformLocation(program, "uAmbientColor");
	program.lightingDirectionUniform = gl.getUniformLocation(program, "uLightingDirection");
	program.directionalColorUniform = gl.getUniformLocation(program, "uDirectionalColor");
}

function loadShader(shaderType, source) {
	var shader = gl.createShader(shaderType);
	if (!shader)
		return null;
	gl.shaderSource(shader, source);
	gl.compileShader(shader);

	if(!gl.getShaderParameter(shader, gl.COMPILE_STATUS)){
		alert(gl.getHeaderInfoLog(shader));
		return null;
	}
	return shader;
}

function handleLoadedTexture(texture)
{
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, texture.image);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
    gl.bindTexture(gl.TEXTURE_2D, null);
}

function initBuffers() {
	VertexBuffer = gl.createBuffer();
	gl.bindBuffer(gl.ARRAY_BUFFER, VertexBuffer);
	gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(triangles), gl.STATIC_DRAW);
	VertexBuffer.numItems = faceCount * 3;
}

function initTexture()
{
	textureMap = gl.createTexture();
	textureMap.image = new Image();
	textureMap.image.onload = function () {
		handleLoadedTexture(textureMap);
	}

	textureMap.image.src = diffuseMap;
}

function updateLoop() {
	requestAnimFrame(updateLoop);
	drawScene();
}

window.requestAnimFrame = (function() {
	return window.requestAnimationFrame ||
         window.webkitRequestAnimationFrame ||
         window.mozRequestAnimationFrame ||
         window.oRequestAnimationFrame ||
         window.msRequestAnimationFrame ||
         function( callback, element) {
           window.setTimeout(callback, 1000/60);
         };
})();

function drawScene() {
	gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
	gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

	mat4.perspective(45, gl.viewportWidth / gl.viewportHeight, 0.1, 100.0, pMatrix);

	mat4.identity(mvMatrix);
	mat4.translate(mvMatrix, [-centre[0],-centre[1],-centre[2]]);

	mat4.translate(mvMatrix, [0.0,0.0,-3.0]);
	mat4.translate(mvMatrix, mouseTranslation);

	currentAngle += 0.2;
	if (currentAngle > 360) currentAngle = 0;
	mat4.rotate(mvMatrix, degToRad(-90), [1, 0, 0]);
	mat4.rotate(mvMatrix, degToRad(currentAngle), [0, 0, 1]);
	setMatrixUniforms();

	gl.bindBuffer(gl.ARRAY_BUFFER, VertexBuffer);
	gl.vertexAttribPointer(program.vertexPositionAttribute, 3, gl.FLOAT, false, 8*4, 0); // position
	gl.vertexAttribPointer(program.textureCoordAttribute, 2, gl.FLOAT, false, 8*4, 3*4); // texcoord
	gl.vertexAttribPointer(program.vertexNormalAttribute, 3, gl.FLOAT, false, 8*4, 5*4); // normals

	gl.activeTexture(gl.TEXTURE0);
	gl.bindTexture(gl.TEXTURE_2D, textureMap);
	gl.uniform1i(program.samplerUniform, 0);
	gl.useProgram(program);

	gl.drawArrays(gl.TRIANGLES, 0, VertexBuffer.numItems);
}

function degToRad(degrees) {
    return degrees * Math.PI / 180;
}

function setMatrixUniforms() {
	gl.uniformMatrix4fv(program.pMatrixUniform, false, pMatrix);
	gl.uniformMatrix4fv(program.mvMatrixUniform, false, mvMatrix);

	var normalMatrix = mat3.create();
	mat4.toInverseMat3(mvMatrix, normalMatrix);
	mat3.transpose(normalMatrix);
	gl.uniformMatrix3fv(program.nMatrixUniform, false, normalMatrix);
}

View the 3d model

Now just open your HTML-File in your Browser. To view this 3d example online just click here. Use your browser to get the source online.

Thanks to Norman Pohl for some background information about WebGL.

Greetz
Vali

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.