简易螺旋

305 阅读7分钟

之前用两个管道,利用螺旋曲线合成了一根绳子,流程相对来说比较复杂。车削几何体改良为螺旋修改器,虽然很方便,但是首尾的口子,我现在还没能力封住。

所以,今天来一个更简单的螺旋,这种螺旋就能封住口子。既然要简单,就先定死一些条件,比如说旋转轴就是Y轴。只要绕Y轴旋转即可。

着色器版

大家不要看到着色器就不明觉厉。这次的着色器代码非常简单。

要用着色器做形变,肯定是在顶点着色器里。

我们想绕Y轴拧转几何体,实际上就是之前说的,一边上升一边旋转,也就是把顶点的旋转量和顶点的y分量关联起来。

定义角度,关联y分量,加上一个偏移值方便调节。按下面这个公式的话,每2π整个几何体就会扭一圈。

    float theta =  position.y * spiralTheta +spiralStart;

拧转几何体,是十分简单的。

    float cosA = position.x, sinA = -position.z;
    float cosB = cos(theta), sinB = sin(theta);
    transformed = vec3(cosA * cosB - sinA * sinB, position.y, sinA * cosB + cosA * sinB);
  

稍微麻烦的是,我要在three里实现,得找到在哪里加这几行代码。

在原本的材质基础上修改

我们要改的是传入的position数据,在应用模型、视图、投影矩阵之前。

于是就找到了一个文件,begin_vertex,一看便知,我们直接在它刚刚赋值transform之后,修改transform即可,因为three后面都是用这个变量。

还得把上面的角度的倍数和偏移量做成uniform,方便外部控制,这样着色器就写完了,加上变量声明 一共就5行。

这里为了方便控制,我直接把uniforms的对象在外部声明了。

完整代码如下。

const guiConfig = {
    axis: true,
    mat: '物理',
    spiralTheta: Math.PI* .25,
    spiralOffset: 1,
    segments: 40,
    flat: false,
}

const pbrMat = new MeshPhysicalMaterial({
    color: 0xffffff,
    side: DoubleSide,
    wireframe: false,
    // ior:1.3,
    transmission: 1,
    thickness: 1,
    roughness: 0.1,
    reflectivity: .8,
});
const spiralUniform = { value: guiConfig.spiralTheta };
const spiraOffsetlUniform = { value: guiConfig.spiralOffset };
pbrMat.onBeforeCompile = (shader)=>{
    console.log(shader);
    shader.uniforms.spiralTheta = spiralUniform;
    shader.uniforms.spiralStart = spiraOffsetlUniform;
    const spiralParamsStr =   /*glsl*/`uniform float spiralTheta ;uniform float spiralStart ;
    void main() {`;
   const spiralStr =   /*glsl*/`
    #include <begin_vertex>
 
     transformed = vec3(cosA * cosB - sinA * sinB, position.y, sinA * cosB + cosA * sinB);
     `
    //  法线也得改
    const sprialNormalStr =   /*glsl*/`
    float theta =  position.y * spiralTheta +spiralStart;
  
    float cosA = position.x, sinA = -position.z;
    float cosB = cos(theta), sinB = sin(theta);
    #include <begin_vertex>
    // objectNormal = vec3(normal.x * cosB  + normal.z * sinB, normal.y, -normal.z* cosB+ normal.x * sinB ) ;
    `
    shader.vertexShader = shader.vertexShader.replace('void main() {',spiralParamsStr)  
    shader.vertexShader = shader.vertexShader.replace('#include <begin_vertex>',spiralStr)  
    shader.vertexShader = shader.vertexShader.replace('#include <begin_vertex>',sprialNormalStr)  
}

拧转立方体

材质做好了,要看看它的效果,就用一个y轴上有足够细分的立方体来吧。虽然是立方体,但是这次用圆柱来做更改合适一些。

const cubeGeo = new CylinderGeometry(1,1,20,4,40,false,Math.PI*.25) ;
cubeGeo.scale(2,.5,1);
let geo = cubeGeo;
const mesh4 = new Mesh(geo, pbrMat)

修改器版

着色器虽好,但是如果我在拧了一个麻花之后,还想对它使用其他的修改器,比如我们上次做的曲线修改器,就会有麻烦。 这是顺序问题,真正对几何体的顶点数据做修改,肯定是在着色器代码运行之前的。

这样,我们上面的写的螺旋的结果就不是我们期望的了。所以得来一个真实修改顶点的版本。

上面的逻辑很简单,我们直接搬下来就可以了。而且上面没旋转法线,这里我们加上。

/**
 *
 * @param {BufferGeometry} sourceGeo 源几何体
 * @param {BufferGeometry} targetGeo 目标几何体 不传就创建一个新的
 * @param {Vector3} axis  旋转轴
 * @param {number} offset  弧度旋转偏移
 * @param {number} theta   每单位旋转角度
 */
function twistModyfier(sourceGeo, targetGeo = new BufferGeometry(),axis,spiralStart,spiralTheta) {
    // 考虑法线 
    /** @type {Float32Array}*/
    const positionArr = sourceGeo.attributes.position.array;
    const targetPositionArr = targetGeo.attributes.position.array;
    const normalAttr = sourceGeo.attributes.normal;
    const targetNormalAttr = targetGeo.attributes.normal;
    /**@type {Float32BufferAttribute} */
    // const positionAttr = targetGeo.attributes.position;
    let theta,cosA,sinA,cosB,sinB;
    const v1 = new Vector3(), v2 = new Vector3();
    for (let index = 0; index < positionArr.length; index+=3) {
        v1.set( positionArr[index], positionArr[index + 1], positionArr[index + 2] );
         theta =  v1.y * spiralTheta +spiralStart;
         cosA = v1.x, sinA = -v1.z;
         cosB = Math.cos(theta), sinB = Math.sin(theta);

         v1.set(cosA * cosB - sinA * sinB, v1.y, sinA * cosB + cosA * sinB);
         targetPositionArr[index] = v1.x;
         targetPositionArr[index + 1] = v1.y;
         targetPositionArr[index + 2] = v1.z;
        //  法线也一起旋转
         if( normalAttr){
             v1.set(normalAttr.array[index], normalAttr.array[index + 1], normalAttr.array[index + 2] );
             v1.set(cosA * cosB - sinA * sinB, v1.y, sinA * cosB + cosA * sinB);

            //  v1.applyAxisAngle(axis,offset).applyAxisAngle(axis,theta)
             targetNormalAttr.array[index] = v1.x;
             targetNormalAttr.array[index + 1] = v1.y;
             targetNormalAttr.array[index + 2] = v1.z;
         }    
        // v2.copy(v1).applyAxisAngle(axis,offset).applyAxisAngle(axis,theta)
        
    }

    targetPositionArr.needsUpdate = true;
    targetNormalAttr && (targetNormalAttr.needsUpdate = true);
    return targetGeo;
}

再来一个锥子测试一下。还是用圆柱,把圆柱的上半径设为0 ,即是圆锥。还是给一个金属材质。

const metalMat = new MeshStandardMaterial({
    color: 0xffff00,
    side: DoubleSide,
    metalness: 1,
    roughness: 0.4,
    flatShading: true,
})
const spiralCone = new CylinderGeometry(0,1,10,5,40,false,Math.PI*.25) ;
twistModyfier(spiralCone,spiralCone,new Vector3(0,1,0),0,Math.PI*0.15)

const mesh1 = new Mesh(spiralCone, metalMat);

image.png

看上去没什么问题。

拧麻花

这次我们拧麻花,其实和绳子是差不多的。这次就解决上次说的封口的问题。 要使用上面的拧转着色器,得有一个几何体。所以一根麻花如果完全捋顺了,是什么样的。我想,应该是类似跑道的管道,而且要是一个无缝的,首尾相连的。

image.png

跑道管道

这个曲线很简单,就是两个半圆加两条线段,注意顺序即可。

还有就是,我们要用来做形变,那么线段部分须有足够的细分。

/**
 *
 * @param {number} radius 跑道半圆的半径
 * @param {number} height
 * @param {number} radialSegments
 * @param {number} heightSegments
 * @param {number} tubeRadius 管道的半径 应小于跑道半径
 * @returns
 */
function getPlaygroundTubeGeo(
  radius,
  height,
  radialSegments,
  heightSegments,
  tubeRadius
) {
  /**@type {Vector3[]} */
  const points2 = [];
  let left = -radius,
    right = radius,
    halfHeight = height *.5,
    bottom = 0;
  // 上半圆
  for (let index = 0; index < radialSegments; index++) {
    const angle = (Math.PI / radialSegments) * index;
    points2.push(
      new Vector3(Math.cos(angle) * radius, Math.sin(angle) * radius + halfHeight)
    );
  }
  // 左侧跑道
  for (let index = 0; index < heightSegments; index++) {
    const y = halfHeight -(height / heightSegments) * index;
    const x = left;
    points2.push(new Vector3(x, y));
  }
  // 下半圆
  for (let index = 0; index < radialSegments; index++) {
    const angle = Math.PI *(index / radialSegments + 1);
    points2.push(
      new Vector3(Math.cos(angle) * radius, Math.sin(angle) * radius -halfHeight)
    );
  }
  // 右侧跑道
  for (let index = heightSegments - 1; index > -1; index--) {
    const y = halfHeight -(height / heightSegments) * index;
    const x = right;
    points2.push(new Vector3(x, y));
  }
  const  curve = new PolyLineCurve3(points2);
  const tubeGeo = new TubeGeometry2(curve, points2.length -1, tubeRadius, 5, true,false,true);
  return tubeGeo;
}

做出来,大概是下面这个样子,但是为了拧麻花,我们应该让跑道的半径接近管道的半径。 image.png

增加一下,直线部分的细分 ,然后这里,就直接换上我们的螺旋材质了,便宜查看效果。

如果,拧转角度不大,或者管道细些的话,还是可以糊弄一下的。

chrome_ailW39xxRJ.gif

老问题

但是一旦拧转的角度稍大,结果出来都是扁的。因为每个圆环的法向都没变,如果再去改圆环的法向,那就是和之前的曲线修改器的路子一样了,而且按这个思路也没法获取到切向。

image.png 今天,搞点儿简单的。

我们可以用上面的修改器去修改曲线,从而得到一条闭合的双螺旋曲线。之前用螺旋曲线生成管道,组合绳子的时候,我就有这个想法了,但是直接构造一条闭合的双螺旋曲线,性价比不高。

用曲线修改器去修改曲线,得到一个双螺旋曲线,是可以,但是前提就是得有一条双螺旋曲线。

所以,今天这个简易螺旋,反而很容易就能得到一条双螺旋曲线。

用修改器来修改曲线

把上面的getPlaygroundTubeGeo 简单改造一下,就不贴重复的代码了。

只需要在跑道的点生成之后,再绕Y轴旋转一下,再用这些点去构造曲线,其余逻辑不变。 下面还把这些点连了一根线,便于我们观察

function getSpialTubeGeo(
  radius,
  height,
  radialSegments,
  heightSegments,
  tubeRadius,
  spiralTheta,  spiralStart
) {
 .....
    spiralTheta ??= .1,  spiralStart ??=0;
    let theta = 0, cosA = 0, sinA = 0, cosB = 0, sinB = 0;
  for (let index = 0; index < points2.length; index++) {
     const v1 =  points2[index];
    theta = v1.y * spiralTheta + spiralStart;
    (cosA = v1.x), (sinA = -v1.z);
    (cosB = Math.cos(theta)), (sinB = Math.sin(theta));
    points2[index].set(cosA * cosB - sinA * sinB, v1.y, sinA * cosB + cosA * sinB);
  }
  const lineGeo = new BufferGeometry().setFromPoints(points2);
  const line = new Line(lineGeo);
  scene.add(line);
  const  curve = new PolyLineCurve3(points2);
  const tubeGeo = new TubeGeometry2(curve, points2.length -1, tubeRadius, radialSegments, true, false,true);
  return tubeGeo;
}

上个法线框材质,这样可以清楚滴看到几何结构。

const strokeMat2 = new MeshNormalMaterial({ wireframe: true });
const mesh2 = new Mesh(spiralGeo, strokeMat2);//麻花

然后,我们就得到了这样一条曲线生成的管道。

image.png

小结

本文介绍了一种简单的螺旋方法,可用在顶点着色器里,也可以直接修改顶点数据。

还可以用这种方法去修改曲线,从而得到一条闭合的双螺旋曲线,再构造出管道。

本来想拧麻花的,但是没找到材质,有人提供一下吗?