缘起Blender
在练习Blender的时候,某个案例里面用到了绳子。我就在想,three里面要怎么做一根绳子呢。
用Blender搓绳子
three里面怎么搓,暂且还不知道。 先来仔细研究Blender里面是怎么搓的。
- 新建一个圆环
- 使用螺旋修改器,轴向为圆环的某直径,
- 修改角度,螺旋长度,和迭代次数。
可以看到,Blender里面就是用螺旋修改器(车削),车这个圆环180度, 就出来一个球,然后,这个球沿刚才的轴向拉伸,出来一个油条麻花一样的东西,最后就是不断地迭代(重复)了。
螺旋修改器与车削几何体
threejs里面有没有类似的几何体呢?当然是有的,那就是车削几何体。 只不过,three的车削轴向是固定为Y轴而已。
不考虑轴向和螺旋(距离)、迭代次数,three的车削几何体和Blender的螺旋修改器可以说是相差无几了。
虽然有限制,前两步还是可以做到的,我拿了一个圆环车了90度,效果如下。
js
const circleCurve = new CircleCurve(1);// 简单自定义了一个圆环曲线
const circlePoints = circleCurve.getPoints(32);
const rope = new LatheGeometry(circlePoints,32 * 2,0,Math.PI *4); // x y 二维路径点 切面分段数 起始 总弧度) ;
但是,这个螺旋(长度)参数three上没有。仔细观察,不难看出。这个螺旋的意思就是,每车一定的角度,在轴向上移动一段距离。
大概试了一下,可以是可以,但是,这里还要处理三角形的绕匝,虽然也可以用双面来解决就是了,又去看了一下Blender,一样,也是有背面的问题。
再优化一下迭代,重复车削这里,理论上是可以减少计算的,直接使用第一次车削的,在轴向上偏移和旋转即可。
但是,我觉得有点儿麻烦,这个车削几何体改螺旋修改器有点儿不好搞。
算了,我决定用更简单的法子,不需要修改three的几何体。
那就是管道几何体。刚好,最近由于某些原因,对管道几何体有了一点儿深入的研究,所以也更加熟悉一些。
使用管道几何体
管道几何体可以说是特殊的挤出几何体,截面是圆。管道几何体就是让圆环在给定的路径上运动一下,线扫成面,形成的几何体。
管道几何体最重要的就是路径曲线 。 就是下面的参数1path
js
TubeGeometry(path: QuadraticBezierCurve3 , tubularSegments: number , radius: number , radialSegments: number , closed: boolean , cover: boolean
其实,这个麻绳,可以拆解为两个细的绳子扭在一起,就是两个管道。管道的路径曲线,就是一个螺旋曲线。我们只需要造出一条螺旋曲线,再绕Y轴旋转180度,就可以拿到另一条曲线。
自定义螺旋曲线
一条曲线,最重要的就是取点规则。这里的取点,用的是曲线的起点到终点的路程百分比。只要输入百分比,就输出一个曲线上的点。
自定义曲线只需要继承原本的Curve,并自定义getPoint方法即可。
我们先来一个简单的圆形曲线热热身。 其中getPoint方法的参数1就是路程百分比,参数2是用来接输出的点位的。
js
export class CircleCurve extends Curve {
/**
* 构造函数
*
* @param {number} r - 圆的半径,默认为1
* @param {Vector3} center - 圆心位置,默认为(0,0,0)
* @param {number} startAngle - 起始角度,默认为0
*/
constructor(r = 1, center = new Vector3(0,0,0), startAngle = 0){
super();
this.r = r;
this.center = center;
this.startAngle = startAngle;
}
getPoint(t, target = new Vector3()){
t *= 2*PI + this.startAngle;
let x = cos(t)*this.r, y = sin(t) *this.r;
return target.set(x,y,0).add(this.center);
}
}
螺旋曲线
什么是螺旋?就是一边儿打转儿一边儿上天,螺旋升天。 也就是说,我们只要在刚才的圆形曲线的基础上加上z轴的偏移,它就是一个螺旋曲线了。
对了,这种单函数的曲线路线,可以用这个网站直接显示出来。
js
getPoint(t, target = new Vector3()){
t *= 2*PI + this.startAngle;
let x = cos(t)*this.r, y = sin(t) *this.r;
return target.set(x,y,t).add(this.center);
}
显然,我们可以看到这个曲线初步效果出来了,但是有些参数需要调整。
- 原本的圆只有[0,2π],而螺旋应该放开这个限制
- 现在有了z方向的偏移,我们应该加一个参数控制这个偏移量
调整后如下。 depth为总拉伸量。
js
class SpiralPath extends Curve {
constructor(r = 1, theta = PI * 2, depth = 1, thetaOffset = 0) {
super();
this.r = r;
this.theta = theta;
this.thetaOffset = thetaOffset;
this.depth = depth;
}
getPoint(t) {
const z = this.depth * t;
const theta = this.theta * t + this.thetaOffset;
return new Vector3(this.r * Math.cos(theta), this.r * -Math.sin(theta),z);
}
}
这个曲线还有可以优化的地方,一个是距离的计算,这里是一个非常简单的函数曲线,其总路程我们是可以直接算出来的,路程的计算会影响到一些东西比如uv。 然后就是切线的计算,按理说是可以通过数学公式计算出来,不过我目前还没写出正确的来。
螺旋管道
螺旋曲线有了,管道也就水到渠成。 刚才的曲线命名为 SpiralCurve
。
js
const spiralCurve = new SpiralCurve(
spiralRadius,
spiralTheta,
spiralDepth,
thetaOffset
);
const spiralGeo = new TubeGeometry2(
spiralCurve,
tubeSegments,
tubeRadius,
tubeRadiusSegments,
close,
cover
);
const spiralMat = new MeshPhysicalMaterial({
color: 0xfff10f,
});
const tube1 = new Mesh(spiralGeo, spiralMat);
双螺旋
只要把上面的Mesh浅克隆一个,绕Z轴旋转180度,即可实现双螺旋,还能复用几何体,真是一举多得。
js
const tube2 = new Mesh(spiralGeo, spiralMat);
tube2.rotateZ(Math.PI);
要搓成一根绳子,这个螺旋的半径,刚好就是管道的半径。。
下面demo里,我已经把曲线的长度计算的优化做了,法线那里其实有一个不算优化的优化,就是切线,three本身是直接用两点连线作为切线的,而我们这种丝滑的函数曲线,完全可以用三个点计算切线。
上贴图
麻绳的形状有了,但是还缺少一些细节。 用Blenderkit随便找了一个绳子的材质,把材质的纹理贴图分离出来了,用在three里面,用上去有效果的就三张贴图,所以也只用了三张贴图。
js
const spiralMat = new MeshPhysicalMaterial({
color: 0xfff10f,
map: ropeMap,
normalMap: ropeNormalMap,
roughnessMap: ropeRoughMap,
});
用了贴图之后,绳子的质感一下子就出来了。
小结
搓绳子就是把两条细的绳子搓成一根不那么细的。
两条细的,就是两条管道,其路径形状是一个螺旋曲线。
螺旋就是一边转圈一边上升。
两条螺旋曲线刚好是绕轴旋转180度的对称关系。
这个绳子是搓出来了,但是还有一些地方需要优化的。 比如说,three的Tube几何体两端是没有封口的,可以加一下,不然这个绳子就只能空心的了。
至于弯曲的绳子,下一篇再说。