I am working on code to generate a screw made up of different elements and animating it by rotatation. The elements are so called conveying elements (denoted by GFA
) which are helical shaped screw elements and kneading blocks (denoted by KB
) which are smaller sequential sections offset by a staggering angle.
The algorithm is as follows:
- Typical initialization (in
init
) of Three.js objects such as renderer, scene, camera and controls - An instance of a custom object
Screw
is initialized and elements are added using a identifier string, e.g. 'GFA 2-40-90' or 'KB 5-2-30-90'. - The
add
method ofScrew
checks what type the element is (i.e.GFA
orKB
) and creates an instances of the relevant element object (i.e.GFAElement
orKBElement
using the parameters of the element and moves it to the end of the screw. - As an element object is instantiated, the profile shape is determined from the screw parameters and used to extrude to the required geometry using the element parameters stored in
userData
. ForGFAElements
, the geometry is subsequently twisted to generate the helical shape of the screw. ForKBElements
, the mesh of a block is extruded to the required thickness and then cloned while rotating in discrete steps to generate aThree.Group
of smaller sections offset by an angle. - After the adding of elements to the screw has finished, the screw is cloned to a mirror screw which is offset by a certain distance and angle from the original screw. During
animate
, the screw and its mirrored clone are rotated by a certain angle.
What I would like to improve:
- memory usage and performance - for GFAElements 'twisting' the vertices seems to be a big performance hit at initialization specifically for a large number of elements and high resolutions (defined by the
steps
property inextrudeSettings
). - Improve feathering of GFAElements - at low resolutions the edges of the element are feathered; this is reduced by increasing the
step
resolution but also decreases performance at initialization and increases memory usage. - Class structuring - I am unsure if I have structured my classes logically. Particularly, I am not sure about the way I decide on which type of element is added in
Screw
methodadd
. Perhaps it is better to have a abstract base class for an element and inherit from it forGFAElement
andKBElement
.
Using JavaScript with three.js.
Code (fiddle):
'use strict';
var container;
var camera, scene, renderer, controls;
var screw, mirror;
// Screw parameters
var P = 2; // number of flights
var D = 50, // outer diameter
Dr = D/1.66, // root diameter
Cl = (Dr+D)/2, // centerline distance
αi = 2*Math.acos(Cl/D),
Ih = D*Math.sin(αi/2)/2,
H = D-Cl;
var αf = αi,
αt = Math.PI/P - αf,
αr = αt;
//console.log(D, Dr, Cl, Ih, H);
//console.log(αi, αf, αt, αr);
function getFlankParams(α1, D1, α2, D2, ctr){
// flanks are arcs with origin (xc, yc) of radius Cl passing through (x1, y1) and (x2, y2):
// (x1-xc)^2 + (y1-yc)^2 = Cl^2
// (x2-xc)^2 + (y2-yc)^2 = Cl^2
var x1 = D1*Math.cos(α1),
y1 = D1*Math.sin(α1),
x2 = D2*Math.cos(α2),
y2 = D2*Math.sin(α2);
// Solving system of equations yields linear eq:
// y1-yc = beta - alpha*(x1-xc)
var alpha = (x1-x2)/(y1-y2),
beta = (y1-y2)*(1+Math.pow(alpha,2))/2;
// Substitution and applying quadratic equation:
var xc = x1 - alpha*beta/(1+Math.pow(alpha,2))*(1+Math.pow(-1,ctr)*Math.sqrt(1-(1-Math.pow(Cl/beta,2))*(1+1/Math.pow(alpha,2)))),
yc = y1 + alpha*(x1-xc) - beta;
// Following from law of consines, the angle the flank extends wrt its own origin:
var asq = Math.pow(Dr/2,2)+Math.pow(D/2,2)-2*(Dr/2)*(D/2)*Math.cos(αf),
af = Math.acos(1-asq/Math.pow(Cl, 2)/2);
return {xc, yc, af};
}
function getProfile() {
var shape = new THREE.Shape();
var angle = 0, ctr = 0;
// loop over number of flights
for (var p=0; p<P; p++){
// tip
shape.absarc(0, 0, D/2, angle, angle+αt);
angle += αt;
// flank
var params = getFlankParams(angle, D/2, angle+αf, Dr/2, ctr++);
shape.absarc(params.xc, params.yc, Cl, angle+αf-params.af, angle+αf, false);
angle += αf;
// root
shape.absarc(0, 0, Dr/2, angle, angle+αr);
angle += αr;
// flank
params = getFlankParams(angle, Dr/2, angle+αf, D/2, ctr++);
shape.absarc(params.xc, params.yc, Cl, angle, angle+αf-params.af, false);
angle += αf;
}
return shape;
}
class GFAElement extends THREE.Mesh {
constructor(params){
//
var p = params.split("-");
var userData = {
type: "GFA",
flights: parseInt(p[0]),
pitch: parseInt(p[1]),
length: parseInt(p[2]),
};
var shape = getProfile();
var extrudeSettings = {
steps: userData.length/2,
depth: userData.length,
bevelEnabled: false
};
var geometry = new THREE.ExtrudeGeometry( shape, extrudeSettings );
var material = new THREE.MeshStandardMaterial( {
color: 0xffffff,
metalness: 0.5,
roughness: 0.5,
} );
super( geometry, material );
this.geometry.vertices.forEach( vertex => {
var angle = -2*Math.PI/userData.flights*vertex.z/userData.pitch;
var updateX = vertex.x * Math.cos(angle) - vertex.y * Math.sin(angle);
var updateY = vertex.y * Math.cos(angle) + vertex.x * Math.sin(angle);
vertex.x = updateX;
vertex.y = updateY;
});
this.geometry.computeFaceNormals();
this.geometry.computeVertexNormals();
this.type = 'GFAElement';
this.userData = userData;
this._params = params;
this._name = 'GFA ' + params;
}
clone(){
return new this.constructor( this._params ).copy( this );
}
}
class KBElement extends THREE.Group {
//
constructor(params){
super();
var p = params.split("-");
var userData = {
type: "KB",
thickness: parseInt(p[0]),
flights: parseInt(p[1]),
length: parseInt(p[2]),
stagAngle: parseInt(p[3]),
};
var shape = getProfile();
var extrudeSettings = {
depth: userData.thickness,
bevelEnabled: false
};
var geometry = new THREE.ExtrudeGeometry( shape, extrudeSettings );
var material = new THREE.MeshStandardMaterial( {
color: 0xffffff,
metalness: 0.5,
roughness: 0.5,
} );
var mesh = new THREE.Mesh( geometry, material );
super.add( mesh );
for (var n=1, nt = userData.length/userData.thickness; n<nt; n++){
mesh = mesh.clone();
mesh.position.z += userData.thickness;
mesh.rotation.z += userData.stagAngle;
super.add( mesh );
}
this.type = 'KBElement';
this.userData = userData;
this._params = params;
this._name = 'KB ' + params;
}
clone(){
return new this.constructor( this._params ).copy( this );
}
}
class Screw extends THREE.Group {
//
constructor(){
super();
this.userData.length = 0; //length of screw starting at origin
}
add(desc){
var elem,
params = desc.split(" ");
if (params[0] == "GFA") {
elem = new GFAElement(params[1]);
} else
if (params[0] == "KB") {
elem = new KBElement(params[1]);
}
elem.position.z = this.userData.length;
this.userData.length += elem.userData.length;
super.add(elem);
}
clone(){
var clone = super.clone(false);
clone.userData.length = 0;
this.children.forEach(function(elem){
var e = elem.clone();
clone.add(e._name);
});
clone.position.x += -Cl;
clone.rotation.z += Math.PI/2;
return clone
}
}
function init() {
renderer = new THREE.WebGLRenderer();
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
//renderer.gammaInput = true;
//renderer.gammaOutput = true;
document.body.appendChild( renderer.domElement );
scene = new THREE.Scene();
scene.background = new THREE.Color( 0x222222 );
camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 1000 );
camera.position.set( -200, 200, -200 );
scene.add( camera );
var light = new THREE.PointLight( 0xffffff );
camera.add( light );
controls = new THREE.TrackballControls( camera, renderer.domElement );
controls.minDistance = 100;
controls.maxDistance = 500;
screw = new Screw();
screw.add('GFA 2-40-90');
screw.add('KB 5-2-30-90');
screw.add('GFA 2-40-90');
screw.add('KB 10-2-120-15');
mirror = screw.clone();
scene.add(screw, mirror);
}
function animate() {
screw.rotation.z += 2*Math.PI/100;
mirror.rotation.z += 2*Math.PI/100;
requestAnimationFrame( animate );
controls.update();
renderer.render( scene, camera );
}
init();
animate();