'How to create oblique cylinders/cones in three.js
I'm struggling to work out how to draw an oblique cyclinder in three.js. Essentially, I would like to produce a cylinder with a top radius of r1 and a bottom radius of r2 (ie not the same radii), with the top and bottom of the cylinder offset by a particular factor, let's say x.
I might not want to draw 360 degrees of the cylinder, maybe only a portion of it (say 90 degrees).
I'm drawn to the cylinder function already present as this ticks most of the boxes that I want, as in I can give a start and end angle, and differing top and bottom radii. The only issue is trying to achieve the offset between the top and bottom.
I'm reasonably new to three.js, so there may be some technique or way to achieve this, but I've tried searching and haven't come up with anything that helps.
Solution 1:[1]
You can implement this with a customized version of CylinderGeometry
.
The code here is mostly borrowed from CylinderGeometry, but with the change that instead of defining a start and end radius, you pass in a function that gets called for each "layer" of the cylinder, and it needs to return a 4-array of numbers: [centerX, layerY, centerZ, radius]
.
This allows for cylinder- and cone-like geometries with an uneven height and center.
For instance, in the example in the CodeSandbox where I sketched this out, the function is
const coordFunc = (i, t) => {
const j = i / t; // 0 .. 1
const x = Math.sin(j * 3) * 2;
const y = Math.cos(j * 3) * 2;
return [x, height / 2 - height * j, y, radius * (j * j) + 0.5];
};
and the result is a funky spiraling vase of sorts:
For a skewed cylinder, you'd want something simpler:
return [j * 5, height * j, 0, radius];
yields
import {
BufferGeometry,
Float32BufferAttribute,
Vector3,
Vector2
} from "three";
export default class CustomCylinderGeometry extends BufferGeometry {
constructor(
heightSegmentFunction, // (i, t) => [x, y, z, radius]
radialSegments = 8,
heightSegments = 1,
openEnded = false,
thetaStart = 0,
thetaLength = Math.PI * 2
) {
super();
this.type = "AdvancedCylinderGeometry";
const scope = this;
radialSegments = Math.floor(radialSegments);
heightSegments = Math.floor(heightSegments);
const [, , , radiusBottom] = heightSegmentFunction(0, heightSegments);
const [, height, , radiusTop] = heightSegmentFunction(
heightSegments,
heightSegments
);
// buffers
const indices = [];
const vertices = [];
const normals = [];
const uvs = [];
// helper variables
let index = 0;
const indexArray = [];
let groupStart = 0;
// generate geometry
generateTorso();
if (openEnded === false) {
if (radiusTop > 0) generateCap(true);
if (radiusBottom > 0) generateCap(false);
}
// build geometry
this.setIndex(indices);
this.setAttribute("position", new Float32BufferAttribute(vertices, 3));
this.setAttribute("normal", new Float32BufferAttribute(normals, 3));
this.setAttribute("uv", new Float32BufferAttribute(uvs, 2));
function generateTorso() {
const normal = new Vector3();
const vertex = new Vector3();
let groupCount = 0;
// this will be used to calculate the normal
const slope = (radiusBottom - radiusTop) / height;
// generate vertices, normals and uvs
for (let y = 0; y <= heightSegments; y++) {
const [cx, cy, cz, radius] = heightSegmentFunction(y, heightSegments);
const indexRow = [];
const v = y / heightSegments;
// calculate the radius of the current row
//const radius = v * (radiusBottom - radiusTop) + radiusTop;
for (let x = 0; x <= radialSegments; x++) {
const u = x / radialSegments;
const theta = u * thetaLength + thetaStart;
const sinTheta = Math.sin(theta);
const cosTheta = Math.cos(theta);
vertex.x = cx + radius * sinTheta;
vertex.y = cy;
vertex.z = cz + radius * cosTheta;
vertices.push(vertex.x, vertex.y, vertex.z);
normal.set(sinTheta, slope, cosTheta).normalize();
normals.push(normal.x, normal.y, normal.z); // TODO: probably not correct
uvs.push(u, 1 - v);
indexRow.push(index++);
}
indexArray.push(indexRow);
}
// generate indices
for (let x = 0; x < radialSegments; x++) {
for (let y = 0; y < heightSegments; y++) {
// we use the index array to access the correct indices
const a = indexArray[y][x];
const b = indexArray[y + 1][x];
const c = indexArray[y + 1][x + 1];
const d = indexArray[y][x + 1];
// faces
indices.push(a, b, d);
indices.push(b, c, d);
// 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;
}
function generateCap(top) {
// save the index of the first center vertex
const centerIndexStart = index;
const uv = new Vector2();
const vertex = new Vector3();
let groupCount = 0;
const sign = top === true ? 1 : -1;
const [cx, cy, cz, radius] = heightSegmentFunction(
top ? 0 : heightSegments,
heightSegments
);
// first we generate the center vertex data of the cap.
// because the geometry needs one set of uvs per face,
// we must generate a center vertex per face/segment
for (let x = 1; x <= radialSegments; x++) {
vertices.push(cx, cy, cz);
normals.push(0, sign, 0);
uvs.push(0.5, 0.5);
index++;
}
// save the index of the last center vertex
const centerIndexEnd = index;
// now we generate the surrounding vertices, normals and uvs
for (let x = 0; x <= radialSegments; x++) {
const u = x / radialSegments;
const theta = u * thetaLength + thetaStart;
const cosTheta = Math.cos(theta);
const sinTheta = Math.sin(theta);
vertex.x = cx + radius * sinTheta;
vertex.y = cy;
vertex.z = cz + radius * cosTheta;
vertices.push(vertex.x, vertex.y, vertex.z);
normals.push(0, sign, 0);
uv.x = cosTheta * 0.5 + 0.5;
uv.y = sinTheta * 0.5 * sign + 0.5;
uvs.push(uv.x, uv.y);
index++;
}
// generate indices
for (let x = 0; x < radialSegments; x++) {
const c = centerIndexStart + x;
const i = centerIndexEnd + x;
if (top === true) {
indices.push(i, i + 1, c);
} else {
indices.push(i + 1, i, c);
}
groupCount += 3;
}
scope.addGroup(groupStart, groupCount, top === true ? 1 : 2);
groupStart += groupCount;
}
}
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | AKX |