Skip to content

Instantly share code, notes, and snippets.

@jcarletto27
Last active November 18, 2025 11:47
Show Gist options
  • Select an option

  • Save jcarletto27/e271bbb7639c4bed2427 to your computer and use it in GitHub Desktop.

Select an option

Save jcarletto27/e271bbb7639c4bed2427 to your computer and use it in GitHub Desktop.
updated Three.js STLExporter to export model including morphTargets
/**
* Based on https://github.com/mrdoob/three.js/blob/a72347515fa34e892f7a9bfa66a34fdc0df55954/examples/js/exporters/STLExporter.js
* Tested on r68 and r70
* @author jcarletto / https://github.com/jcarletto27
* @author kjlubick / https://github.com/kjlubick
* @author kovacsv / http://kovacsv.hu/
* @author mrdoob / http://mrdoob.com/
*/
THREE.STLExporter = function () {};
THREE.STLExporter.prototype = {
constructor : THREE.STLExporter,
parse : (function () {
var vector = new THREE.Vector3();
var normalMatrixWorld = new THREE.Matrix3();
return function (scene) {
var output = '';
output += 'solid exported\n';
scene.traverse(function (object) {
if (object instanceof THREE.Mesh) {
var geometry = object.geometry;
var matrixWorld = object.matrixWorld;
var mesh = object;
if (geometry instanceof THREE.Geometry) {
var vertices = geometry.vertices;
var faces = geometry.faces;
normalMatrixWorld.getNormalMatrix(matrixWorld);
for (var i = 0, l = faces.length; i < l; i++) {
var face = faces[i];
vector.copy(face.normal).applyMatrix3(normalMatrixWorld).normalize();
output += '\tfacet normal ' + vector.x + ' ' + vector.y + ' ' + vector.z + '\n';
output += '\t\touter loop\n';
var indices = [face.a, face.b, face.c];
for (var j = 0; j < 3; j++) {
var vertexIndex = indices[j];
if (mesh.geometry.skinIndices.length == 0) {
vector.copy(vertices[vertexIndex]).applyMatrix4(matrixWorld);
output += '\t\t\tvertex ' + vector.x + ' ' + vector.y + ' ' + vector.z + '\n';
} else {
vector.copy(vertices[vertexIndex]); //.applyMatrix4( matrixWorld );
// see https://github.com/mrdoob/three.js/issues/3187
boneIndices = [];
boneIndices[0] = mesh.geometry.skinIndices[vertexIndex].x;
boneIndices[1] = mesh.geometry.skinIndices[vertexIndex].y;
boneIndices[2] = mesh.geometry.skinIndices[vertexIndex].z;
boneIndices[3] = mesh.geometry.skinIndices[vertexIndex].w;
weights = [];
weights[0] = mesh.geometry.skinWeights[vertexIndex].x;
weights[1] = mesh.geometry.skinWeights[vertexIndex].y;
weights[2] = mesh.geometry.skinWeights[vertexIndex].z;
weights[3] = mesh.geometry.skinWeights[vertexIndex].w;
inverses = [];
inverses[0] = mesh.skeleton.boneInverses[boneIndices[0]];
inverses[1] = mesh.skeleton.boneInverses[boneIndices[1]];
inverses[2] = mesh.skeleton.boneInverses[boneIndices[2]];
inverses[3] = mesh.skeleton.boneInverses[boneIndices[3]];
skinMatrices = [];
skinMatrices[0] = mesh.skeleton.bones[boneIndices[0]].matrixWorld;
skinMatrices[1] = mesh.skeleton.bones[boneIndices[1]].matrixWorld;
skinMatrices[2] = mesh.skeleton.bones[boneIndices[2]].matrixWorld;
skinMatrices[3] = mesh.skeleton.bones[boneIndices[3]].matrixWorld;
//this checks to see if the mesh has any morphTargets - jc
if (mesh.geometry.morphTargets !== 'undefined') {
morphMatricesX = [];
morphMatricesY = [];
morphMatricesZ = [];
morphMatricesInfluence = [];
for (var mt = 0; mt < mesh.geometry.morphTargets.length; mt++) {
//collect the needed vertex info - jc
morphMatricesX[mt] = mesh.geometry.morphTargets[mt].vertices[vertexIndex].x;
morphMatricesY[mt] = mesh.geometry.morphTargets[mt].vertices[vertexIndex].y;
morphMatricesZ[mt] = mesh.geometry.morphTargets[mt].vertices[vertexIndex].z;
morphMatricesInfluence[mt] = mesh.morphTargetInfluences[mt];
}
}
var finalVector = new THREE.Vector4();
if (mesh.geometry.morphTargets !== 'undefined') {
var morphVector = new THREE.Vector4(vector.x, vector.y, vector.z);
for (var mt = 0; mt < mesh.geometry.morphTargets.length; mt++) {
//not pretty, but it gets the job done - jc
morphVector.lerp(new THREE.Vector4(morphMatricesX[mt], morphMatricesY[mt], morphMatricesZ[mt], 1), morphMatricesInfluence[mt]);
}
}
for (var k = 0; k < 4; k++) {
if (mesh.geometry.morphTargets !== 'undefined') {
var tempVector = new THREE.Vector4(morphVector.x, morphVector.y, morphVector.z);
} else {
var tempVector = new THREE.Vector4(vector.x, vector.y, vector.z);
}
tempVector.multiplyScalar(weights[k]);
//the inverse takes the vector into local bone space
//which is then transformed to the appropriate world space
tempVector.applyMatrix4(inverses[k])
.applyMatrix4(skinMatrices[k]);
finalVector.add(tempVector);
}
output += '\t\t\tvertex ' + finalVector.x + ' ' + finalVector.y + ' ' + finalVector.z + '\n';
}
}
output += '\t\tendloop\n';
output += '\tendfacet\n';
}
}
}
});
output += 'endsolid exported\n';
return output;
};
}
())
};
function saveSTL(scene, name) {
var exporter = new THREE.STLExporter();
var stlString = exporter.parse(scene);
var blob = new Blob([stlString], {
type : 'text/plain'
});
saveAs(blob, name + '.stl');
}
var exporter = new THREE.STLExporter();
var exportString = function (output, filename) {
var blob = new Blob([output], {
type : 'text/plain'
});
var objectURL = URL.createObjectURL(blob);
var link = document.createElement('a');
link.href = objectURL;
link.download = filename || 'data.json';
link.target = '_blank';
link.click();
};
@verybigelephants
Copy link
Copy Markdown

very cool, would it be also possible to export colored stl?

@jcarletto27
Copy link
Copy Markdown
Author

@verybigelephants
I wasn't aware stl could store color, but three.js offers obj exporting with materials, might be your best bet. I tried modifying their stock obj exporter to include morphs and animations, but obj with material is a bit more complex than a stl export.

@markskelton
Copy link
Copy Markdown

Hey great work with this! When I open the exported STL file in Maya there seems to be double the number of vertices. Would you know the reason for this?

@sashidarsync
Copy link
Copy Markdown

when we try to export the 3D Scene loaded via threejs json loader to STL The scale/size is not outputting (we are applying blendshapes/morph targets)

@sashidarsync
Copy link
Copy Markdown

STL Export is working but only the base size is getting exported.

@atnartur
Copy link
Copy Markdown

atnartur commented Aug 3, 2016

Hi! I updated your code. I include support of BufferGeometry and support of AMD, common.js and ES6 modules. Also I published npm package and added you to contributors in package.json. https://github.com/atnartur/three-STLexporter

@tart2000
Copy link
Copy Markdown

Hi! All I get is scene.traverse is not a function...
Do you have any idea why?
(using revision 81)

@jcarletto27
Copy link
Copy Markdown
Author

I've modified the OBJ Exporter to incorporate bones, and morph data like this one, only it uses bufferGeometry instead of the standard Geometry type
https://gist.github.com/jcarletto27/35b19f811c657ec73b0238d78413f88d

atnartur's version converts the BufferGeometry to geometry which loses all the bone and morph data, this version works with them directly. Either version should work depending on your needs.

@alejandroLoz
Copy link
Copy Markdown

I manipulate the positions of my meshes before exporting, but they all appear centered in the output STL.
Any way to get the proper positions in the STL?
Thanks

@HelliceSaouli
Copy link
Copy Markdown

could some one help, i'm trying to make poser, where i have a fbx file send to three js and then via transform controller i i create poses, and i want to export the result . i tried this code and still did not work

@swebal
Copy link
Copy Markdown

swebal commented Nov 18, 2025

Noob here. How do I import this and use it instead of the regular STL exporter? Regular exporter works fine, but I can't figure out how to import this script..

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment