5
\$\begingroup\$

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:

  1. Typical initialization (in init) of Three.js objects such as renderer, scene, camera and controls
  2. 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'.
  3. The add method of Screw checks what type the element is (i.e. GFA or KB) and creates an instances of the relevant element object (i.e. GFAElement or KBElement using the parameters of the element and moves it to the end of the screw.
  4. 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. For GFAElements, the geometry is subsequently twisted to generate the helical shape of the screw. For KBElements, the mesh of a block is extruded to the required thickness and then cloned while rotating in discrete steps to generate a Three.Group of smaller sections offset by an angle.
  5. 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:

  1. 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 in extrudeSettings).
  2. 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.
  3. 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 method add. Perhaps it is better to have a abstract base class for an element and inherit from it for GFAElement and KBElement.

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();
\$\endgroup\$
  • \$\begingroup\$ hey Did you attempt to make any changes based on my suggestions? If so, did it help at all? \$\endgroup\$ – Sᴀᴍ Onᴇᴌᴀ Sep 9 '18 at 18:10
2
+50
\$\begingroup\$

memory usage and performance

I see that the constructor for GFAElement uses a forEach iterator. While functional programming is great, one drawback is that it is typically slower because function calls are made for each element in the array. Instead, try using for...of to reduce the computational requirements.

Should getProfile() be called by every GFA/KB element? If not, Perhaps it would be wise to cache the profile return value and invalidate that when necessary (i.e. when the view changes?). And would it be acceptable to use the same material object for each mesh element?

Improve feathering of GFAElements

I am not sure if this will help with optimization but have you considered setting the value of the step option based on the resolution (I.e an inverse relationship)? Otherwise maybe you could allow the user to specify that value (e.g. perhaps with an <input type=“range” />), allowing him/her to make the decision of optimization vs. appearance.

Class structuring

It is a shame that the two element classes don't have the same parent class - if so, an intermediary class could be created to abstract out common code like the cloning, parameter extraction, etc. However, a mixin could be used:

let ElementMixin = superclass => class extends superclass { 
    clone(){
        return new this.constructor( this._params ).copy( this );
    }
}
class GFAElement extends ElementMixin(THREE.Mesh) { ... }
class KBElement extends ElementMixin(THREE.Group) { ... }

The code in the constructor methods could perhaps be abstracted out into that mixin (e.g. getting the userdata object, updating the geometry/vertex items, etc.). For more of an explanation of mixins in ecmascript-2015, check out "Real" Mixins with JavaScript Classes.

Other suggestions

Use more EcmaScript-2015 (ES-6) features

The code already uses Classes and Object destructuring (for the return value of getFlankParams()). It is recommended that one use const for block-scoped variables that should not be re-assigned and let for block-scoped variables that should be re-assigned (typically just iterator variables and counters).

ParseInt() radix specification

This likely won't be an issue unless the parameters/attributes of the elements are specified by input from the user but it would be wise to pass the radix (typically 10) as the second parameter to calls to parseInt(). According to the MDN documentation:

"Always specify a radix" 1

Consider wrapping code in an IIFE or wait for DOM to be ready

It is a good habit to wrap the code in an IIFE or put it all in a function called when the DOM is ready, so as to avoid putting all the variables currently declared outside of a function in the global namespace (i.e window).

1https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseInt

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.