nick zoum
nick zoum

Reputation: 7285

Three.js Custom Hollow Cylinder Geometry

I want to create my own custom three.js geometry for hollow cylinders. I tried to combine parts from the RingGeometry and CylinderGeometry classes with some success but I still get some visual errors:

Here is an example. (image used for caps) Sample Cylinder

Here is the code

/**
 * 
 * @param {number} radius 
 * @param {number} holeRadius 
 * @param {number} height 
 * @param {number} segments 
 * @param {boolean} openEnded 
 * @param {number} thetaStart 
 * @param {number} thetaLength 
 */
function HollowCylinderGeometry(radius, holeRadius, height, segments, openEnded, thetaStart, thetaLength) {

    if (!(this instanceof HollowCylinderGeometry)) {
        throw new TypeError("HollowCylinderGeometry needs to be called using new");
    }

    THREE.Geometry.call(this);

    this.type = 'HollowCylinderGeometry';

    this.parameters = {
        radius: radius,
        holeRadius: holeRadius,
        height: height,
        segments: segments,
        openEnded: openEnded,
        thetaStart: thetaStart,
        thetaLength: thetaLength
    };

    this.fromBufferGeometry(new HollowCylinderBufferGeometry(radius, holeRadius, height, segments, openEnded, thetaStart, thetaLength));
    this.mergeVertices();

}

HollowCylinderGeometry.prototype = Object.create(THREE.Geometry.prototype);
HollowCylinderGeometry.prototype.constructor = HollowCylinderGeometry;

/**
 * 
 * @param {number} radius 
 * @param {number} holeRadius 
 * @param {number} height 
 * @param {number} segments 
 * @param {boolean} openEnded 
 * @param {number} thetaStart 
 * @param {number} thetaLength 
 */
function HollowCylinderBufferGeometry(radius, holeRadius, height, segments, openEnded, thetaStart, thetaLength) {

    if (!(this instanceof HollowCylinderBufferGeometry)) {
        throw new TypeError("HollowCylinderBufferGeometry needs to be called using new");
    }

    THREE.BufferGeometry.call(this);

    this.type = 'HollowCylinderBufferGeometry';

    this.parameters = {
        radius: radius,
        holeRadius: holeRadius,
        height: height,
        segments: segments,
        openEnded: openEnded,
        thetaStart: thetaStart,
        thetaLength: thetaLength
    };

    var scope = this;

    radius = !isNaN(radius) ? radius : 20;
    holeRadius = !isNaN(holeRadius) ? holeRadius : 20;
    height = !isNaN(height) ? height : 100;
    segments = !isNaN(segments = Math.floor(segments)) ? segments : 8;
    openEnded = !!openEnded;
    thetaStart = !isNaN(thetaStart) ? thetaStart : 0;
    thetaLength = !isNaN(thetaLength) ? thetaLength : Math.PI * 2;


    // buffers

    var indices = [];
    var vertices = [];
    var normals = [];
    var uvs = [];

    // helper variables

    var index = 0;
    var indexArray = [];
    var halfHeight = height / 2;
    var groupStart = 0;

    // generate geometry

    generateTorso(true);
    generateTorso(false);

    if (thetaLength % (Math.PI * 2) !== 0) {
        generateSide(true);
        generateSide(false);
    }

    if (!openEnded && radius > 0) {
        generateCap(true);
        generateCap(false);
    }

    // build geometry

    this.setIndex(indices);
    this.addAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
    this.addAttribute('normal', new THREE.Float32BufferAttribute(normals, 3));
    this.addAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));

    function generateTorso(isOuter) {

        var x, y;
        var normal = new THREE.Vector3();
        var vertex = new THREE.Vector3();

        var groupCount = 0;

        var sign = isOuter ? 1 : -1;

        var activeRadius = isOuter ? radius : holeRadius;

        // this will be used to calculate the normal
        // generate vertices, normals and uvs


        // calculate the radius of the current row
        for (y = 0; y < 2; y++) {
            var indexRow = [];
            for (x = 0; x <= segments; x++) {

                var u = x / segments;

                var theta = u * thetaLength + thetaStart;

                var sinTheta = Math.sin(theta);
                var cosTheta = Math.cos(theta);

                // vertex

                vertex.x = activeRadius * sinTheta;
                vertex.y = -y * height + halfHeight;
                vertex.z = activeRadius * cosTheta;
                vertices.push(vertex.x, vertex.y, vertex.z);

                // normal

                normal.set(sinTheta, 0, cosTheta).normalize();
                normals.push(normal.x * sign, normal.y, normal.z * sign);

                // uv

                uvs.push(u, 1 - y);

                // save index of vertex in respective row

                indexRow.push(index++);

            }
            indexArray.push(indexRow);

        }

        // generate indices

        for (x = 0; x < segments; x++) {

            // we use the index array to access the correct indices
            var addSign = isOuter ? 0 : 2;
            var a = indexArray[addSign][x];
            var b = indexArray[addSign + 1][x];
            var c = indexArray[addSign + 1][x + 1];
            var d = indexArray[addSign][x + 1];

            // faces
            if (isOuter) {
                indices.push(a, b, d);
                indices.push(b, c, d);
            } else {
                indices.push(a, d, b);
                indices.push(b, d, c);
            }
            // update group counter

            groupCount += 6;


        }

        // add a group to the geometry. this will ensure multi material support

        scope.addGroup(groupStart, groupCount, 0);

        // calculate new start value for groups

        groupStart += groupCount;

    }

    /**
     * @returns {void}
     */
    function generateCap(isTop) {
        var indexStart = index;
        var segment = 0;

        var uv = new THREE.Vector2();

        var vertex = new THREE.Vector3();
        var sign = isTop ? 1 : -1;
        var groupCount = 0;

        for (var heightIndex = 0; heightIndex < 2; heightIndex++) {
            var activeRadius = heightIndex == 0 ? holeRadius : radius;
            for (var segmentIndex = 0; segmentIndex <= segments; segmentIndex++) {

                segment = segmentIndex / segments * thetaLength + thetaStart;

                // vertex

                vertex.x = activeRadius * Math.sin(segment);
                vertex.y = halfHeight * sign;
                vertex.z = activeRadius * Math.cos(segment);

                vertices.push(vertex.x, vertex.y, vertex.z);

                // normal

                normals.push(0, sign, 0);

                // uv

                uvs.push((vertex.x / radius + 1) / 2, (vertex.z / radius + 1) / 2);

                index++;
            }
        }

        // Generate Indices

        for (var segmentIndex = 0; segmentIndex < segments; segmentIndex++) {

            segment = segmentIndex + indexStart;

            var a = segment;
            var b = segment + segments + 1;
            var c = segment + segments + 2;
            var d = segment + 1;

            // faces
            if (isTop) {
                indices.push(a, b, d);
                indices.push(b, c, d);
            } else {
                indices.push(a, d, b);
                indices.push(b, d, c);
            }
            groupCount += 6;
        }

        scope.addGroup(groupStart, groupCount, 1);

        // calculate new start value for groups

        groupStart += groupCount;
    }

    function generateSide(isLeft) {

        var indexStart = index;
        var normal = new THREE.Vector3();
        var vertex = new THREE.Vector3();

        var theta = thetaStart;
        if (isLeft) theta += thetaLength;
        var sinTheta = Math.sin(theta);
        var cosTheta = Math.cos(theta);
        for (var y = 0; y < 2; y++) {
            for (var x = 0; x < 2; x++) {
                var activeRadius = x == 0 ? radius : holeRadius;
                vertex.x = activeRadius * sinTheta;
                vertex.y = halfHeight * (y == 0 ? -1 : 1);
                vertex.z = activeRadius * cosTheta;

                vertices.push(vertex.x, vertex.y, vertex.z);

                normal.set(sinTheta, 0, cosTheta).normalize();
                normals.push(normal.x, normal.y, normal.z);

                // uv

                uvs.push(1 - x, 1 - y);
                index++;
            }
        }

        var a = indexStart + 0;
        var b = indexStart + 1;
        var c = indexStart + 3;
        var d = indexStart + 2;

        // faces

        if (isLeft) {
            indices.push(a, b, d);
            indices.push(b, c, d);
        } else {
            indices.push(a, d, b);
            indices.push(b, d, c);
        }

        scope.addGroup(groupStart, 6, 0);

        // calculate new start value for groups

        groupStart += 6;
    }

}

HollowCylinderBufferGeometry.prototype = Object.create(THREE.BufferGeometry.prototype);
HollowCylinderBufferGeometry.prototype.constructor = HollowCylinderBufferGeometry;

Upvotes: 1

Views: 2947

Answers (2)

Rabbid76
Rabbid76

Reputation: 210890

Whether you can see a face or not, depends on if the primitive is draw clockwise or counterclockwise. See Face Culling.
You have to draw all your poligons in the same orientation (counterclockwise).

Change the function generateTorso:

if ( isOuter ) {
    indices.push(a, b, d);
    indices.push(b, c, d);
} else {
    indices.push(a, d, b);
    indices.push(b, d, c);
}

Change the function generateCap:

if (isTop) {
    indices.push(a, b, d);
    indices.push(b, c, d);
} else {
    indices.push(a, d, b);
    indices.push(b, d, c);
}


Preview:

enter image description here

See the Code Snippet:

var renderer, scene, camera, controls;

function HollowCylinderGeometry(radius, holeRadius, height, segments, openEnded) {

    if (!(this instanceof HollowCylinderGeometry)) {
        throw new TypeError("HollowCylinderGeometry needs to be called using new");
    }

    THREE.Geometry.call(this);

    this.type = 'HollowCylinderGeometry';

    this.parameters = {
        radius: radius,
        holeRadius: holeRadius,
        height: height,
        segments: segments,
        openEnded: openEnded
    };

    this.fromBufferGeometry(new HollowCylinderBufferGeometry(radius, holeRadius, height, segments, openEnded));
    this.mergeVertices();

}

HollowCylinderGeometry.prototype = Object.create(THREE.Geometry.prototype);
HollowCylinderGeometry.prototype.constructor = HollowCylinderGeometry;

function HollowCylinderBufferGeometry(radius, holeRadius, height, segments, openEnded) {

    if (!(this instanceof HollowCylinderBufferGeometry)) {
        throw new TypeError("HollowCylinderBufferGeometry needs to be called using new");
    }

    THREE.BufferGeometry.call(this);

    this.type = 'HollowCylinderBufferGeometry';

    this.parameters = {
        radius: radius,
        holeRadius: holeRadius,
        height: height,
        segments: segments,
        openEnded: openEnded
    };

    var scope = this;

    radius = !isNaN(radius) ? radius : 20;
    height = !isNaN(radius) ? height : 100;

    segments = Math.floor(segments) || 8;

    openEnded = !!openEnded;

    // buffers

    var indices = [];
    var vertices = [];
    var normals = [];
    var uvs = [];

    // helper variables

    var index = 0;
    var indexArray = [];
    var halfHeight = height / 2;
    var groupStart = 0;

    // generate geometry

    generateTorso(true);
    generateTorso(false);

    if (!openEnded && radius > 0) {
        generateCap(true);
        generateCap(false);
    }

    // build geometry

    this.setIndex(indices);
    this.addAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
    this.addAttribute('normal', new THREE.Float32BufferAttribute(normals, 3));
    this.addAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));

    function generateTorso(isOuter) {

        var x, y;
        var normal = new THREE.Vector3();
        var vertex = new THREE.Vector3();

        var groupCount = 0;

        var sign = isOuter ? 1 : -1;

        var activeRadius = isOuter ? radius : holeRadius;

        // this will be used to calculate the normal
        // generate vertices, normals and uvs


        // calculate the radius of the current row
        for (y = 0; y < 2; y++) {
            var indexRow = [];
            for (x = 0; x <= segments; x++) {

                var u = x / segments;

                var theta = u * Math.PI * 2;

                var sinTheta = Math.sin(theta);
                var cosTheta = Math.cos(theta);

                // vertex

                vertex.x = activeRadius * sinTheta;
                vertex.y = -y * height + halfHeight;
                vertex.z = activeRadius * cosTheta;
                vertices.push(vertex.x, vertex.y, vertex.z);

                // normal

                normal.set(sinTheta, 0, cosTheta).normalize();
                normals.push(normal.x * sign, normal.y, normal.z * sign);

                // uv

                uvs.push(u, 1 - y);

                // save index of vertex in respective row

                indexRow.push(index++);

            }
            indexArray.push(indexRow);

        }

        // generate indices

        for (x = 0; x < segments; x++) {

            // we use the index array to access the correct indices
            var addSign = isOuter ? 0 : 2;
            var a = indexArray[addSign][x];
            var b = indexArray[addSign + 1][x];
            var c = indexArray[addSign + 1][x + 1];
            var d = indexArray[addSign][x + 1];

            // faces

            if ( isOuter ) {
                indices.push(a, b, d);
                indices.push(b, c, d);
            } else {
                indices.push(a, d, b);
                indices.push(b, d, c);
            }

            // update group counter

            groupCount += 6;


        }

        // add a group to the geometry. this will ensure multi material support

        scope.addGroup(groupStart, groupCount, 0);

        // calculate new start value for groups

        groupStart += groupCount;

    }

    /**
     * @returns {void}
     */
    function generateCap(isTop) {
        var indexStart = index;
        var segment = 0;

        var uv = new THREE.Vector2();

        var vertex = new THREE.Vector3();
        var sign = isTop ? 1 : -1;
        var groupCount = 0;

        for (var heightIndex = 0; heightIndex < 2; heightIndex++) {
            var activeRadius = heightIndex == 0 ? holeRadius : radius;
            for (var segmentIndex = 0; segmentIndex <= segments; segmentIndex++) {

                segment = segmentIndex / segments * Math.PI * 2;

                // vertex

                vertex.x = activeRadius * Math.sin(segment);
                vertex.y = halfHeight * sign;
                vertex.z = activeRadius * Math.cos(segment);

                vertices.push(vertex.x, vertex.y, vertex.z);

                // normal

                normals.push(0, sign, 0);

                // uv

                uvs.push((vertex.x / radius + 1) / 2, (vertex.z / radius + 1) / 2);

                index++;
            }
        }

        // Generate Indices

        for (var segmentIndex = 0; segmentIndex < segments; segmentIndex++) {

            segment = segmentIndex + indexStart;

            var a = segment;
            var b = segment + segments + 1;
            var c = segment + segments + 2;
            var d = segment + 1;

            // faces

            if (isTop) {
                indices.push(a, b, d);
                indices.push(b, c, d);
            } else {
                indices.push(a, d, b);
                indices.push(b, d, c);
            }

            groupCount += 6;
        }

        scope.addGroup(groupStart, groupCount, 1);

        // calculate new start value for groups

        groupStart += groupCount;
    }

}

HollowCylinderBufferGeometry.prototype = Object.create(THREE.BufferGeometry.prototype);
HollowCylinderBufferGeometry.prototype.constructor = HollowCylinderBufferGeometry;

function init() {

    // renderer
    renderer = new THREE.WebGLRenderer();
    renderer.setSize( window.innerWidth, window.innerHeight );
    renderer.setClearColor(0x404040, 1);
    document.body.appendChild( renderer.domElement );

    // scene
    scene = new THREE.Scene();
    
    // camera
    camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 1000 );
    camera.position.set( 3, 3, 3 );

    // controls
    controls = new THREE.OrbitControls( camera );

    var loader = new THREE.TextureLoader();
    loader.setCrossOrigin("");
    var texture1 = loader.load("https://threejs.org/examples/textures/hardwood2_diffuse.jpg");
    texture1.wrapS = texture1.wrapT = THREE.RepeatWrapping;
    texture1.repeat.set(2.0*Math.PI, 1.0);
    var texture2 = loader.load("https://threejs.org/examples/textures/crate.gif");
    texture2.wrapS = texture1.wrapT = THREE.RepeatWrapping;
    texture2.repeat.set(1.0, 1.0);
    
    // materials
    material_1 = new THREE.MeshBasicMaterial({
        map: texture1
        });
    material_2 = new THREE.MeshBasicMaterial({
        map: texture2
        });
    
    var geometry = new HollowCylinderGeometry(1.0, 0.3, 0.5, 16, false);
    var mesh = new THREE.Mesh(geometry, [material_1, material_2]);
    mesh.material.side = THREE.DoubleSide;
    
    // mesh
    scene.add( mesh );
}

function animate() {

    requestAnimationFrame( animate );
    renderer.render( scene, camera );
}

init();
animate();
body {
    margin: 0;
    overflow: hidden;
}

canvas {
    width: 100%;
    height: 100%
}
<script src="https://threejs.org/build/three.min.js"></script>
<script src="https://threejs.org/examples/js/controls/OrbitControls.js"></script>

Upvotes: 1

prisoner849
prisoner849

Reputation: 17586

As an option, let Three.js do the work for you, using THREE.Shape() and THREE.ExtrudeGeometry().

enter image description here

var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 1000);
camera.position.set(0, 10, 20);
var renderer = new THREE.WebGLRenderer({
  antialias: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0x818181);
document.body.appendChild(renderer.domElement);

var controls = new THREE.OrbitControls(camera, renderer.domElement);

var loader = new THREE.TextureLoader();
loader.setCrossOrigin("");
var texture1 = loader.load("https://threejs.org/examples/textures/crate.gif");
texture1.wrapS = texture1.wrapT = THREE.RepeatWrapping;
texture1.repeat.set(0.05, 0.05);
var texture2 = loader.load("https://threejs.org/examples/textures/hardwood2_diffuse.jpg");
texture2.wrapS = texture2.wrapT = THREE.RepeatWrapping;
texture2.repeat.set(0.1, 0.1);

var outerRadius = 10;
var innerRadius = 5;
var height = 2;

var arcShape = new THREE.Shape();
arcShape.moveTo(outerRadius * 2, outerRadius);
arcShape.absarc(outerRadius, outerRadius, outerRadius, 0, Math.PI * 2, false);
var holePath = new THREE.Path();
holePath.moveTo(outerRadius + innerRadius, outerRadius);
holePath.absarc(outerRadius, outerRadius, innerRadius, 0, Math.PI * 2, true);
arcShape.holes.push(holePath);

var geometry = new THREE.ExtrudeGeometry(arcShape, {
  amount: height,
  bevelEnabled: false,
  steps: 1,
  curveSegments: 60
});
geometry.center();
geometry.rotateX(Math.PI * -.5);
var mesh = new THREE.Mesh(geometry, [new THREE.MeshBasicMaterial({
  map: texture1
}), new THREE.MeshBasicMaterial({
  map: texture2
})]);
scene.add(mesh);

render();

function render() {
  requestAnimationFrame(render);
  renderer.render(scene, camera);
}
body {
  overflow: hidden;
  margin: 0;
}
<script src="https://threejs.org/build/three.min.js"></script>
<script src="https://threejs.org/examples/js/controls/OrbitControls.js"></script>

Upvotes: 4

Related Questions