Reputation: 7285
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)
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
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:
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
Reputation: 17586
As an option, let Three.js do the work for you, using THREE.Shape()
and THREE.ExtrudeGeometry()
.
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