如何在 Three.js 中创建倾斜的圆柱体/圆锥体

发布于 2025-01-17 02:53:49 字数 443 浏览 1 评论 0原文

我正在努力弄清楚如何在 Three.js 中绘制倾斜的圆柱体。本质上,我想生成一个顶部半径为 r1 、底部半径为 r2 (即半径不同)的圆柱体,圆柱体的顶部和底部偏移特定因子,假设为 x。

我可能不想绘制 360 度的圆柱体,可能只想绘制其中的一部分(比如 90 度)。

输入图片这里的描述

我被已经存在的圆柱体函数所吸引,因为它勾选了我想要的大多数框,例如我可以给出开始和结束角度以及不同的顶部和底部半径。唯一的问题是试图实现顶部和底部之间的偏移。

我对 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).

enter image description here

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.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

故事↓在人 2025-01-24 02:53:49

您可以使用 CylinderGeometry 的自定义版本来实现此目的。

这里的代码大部分借自 CylinderGeometry ,但变化是不再定义开始和结束radius,您传入一个函数,该函数会为圆柱体的每个“层”调用,并且它需要返回一个 4 数字数组:[centerX、layerY、centerZ、radius]

这允许具有不均匀高度和中心的圆柱体和圆锥体几何形状。

例如,在 CodeSandbox 的示例中,勾勒出这个,其功能是

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];
};

,结果是一个时髦的螺旋花瓶:

在此处输入图像描述

对于倾斜的圆柱体,您需要更简单的东西:

return [j * 5, height * j, 0, radius];

产生

在此处输入图像描述


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;
    }
  }
}

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:

enter image description here

For a skewed cylinder, you'd want something simpler:

return [j * 5, height * j, 0, radius];

yields

enter image description here


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;
    }
  }
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文