之前用两个管道,利用螺旋曲线合成了一根绳子,流程相对来说比较复杂。车削几何体改良为螺旋修改器,虽然很方便,但是首尾的口子,我现在还没能力封住。
所以,今天来一个更简单的螺旋,这种螺旋就能封住口子。既然要简单,就先定死一些条件,比如说旋转轴就是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);
看上去没什么问题。
拧麻花
这次我们拧麻花,其实和绳子是差不多的。这次就解决上次说的封口的问题。 要使用上面的拧转着色器,得有一个几何体。所以一根麻花如果完全捋顺了,是什么样的。我想,应该是类似跑道的管道,而且要是一个无缝的,首尾相连的。
跑道管道
这个曲线很简单,就是两个半圆加两条线段,注意顺序即可。
还有就是,我们要用来做形变,那么线段部分须有足够的细分。
/**
*
* @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;
}
做出来,大概是下面这个样子,但是为了拧麻花,我们应该让跑道的半径接近管道的半径。
增加一下,直线部分的细分 ,然后这里,就直接换上我们的螺旋材质了,便宜查看效果。
如果,拧转角度不大,或者管道细些的话,还是可以糊弄一下的。
老问题
但是一旦拧转的角度稍大,结果出来都是扁的。因为每个圆环的法向都没变,如果再去改圆环的法向,那就是和之前的曲线修改器的路子一样了,而且按这个思路也没法获取到切向。
今天,搞点儿简单的。
我们可以用上面的修改器去修改曲线,从而得到一条闭合的双螺旋曲线。之前用螺旋曲线生成管道,组合绳子的时候,我就有这个想法了,但是直接构造一条闭合的双螺旋曲线,性价比不高。
用曲线修改器去修改曲线,得到一个双螺旋曲线,是可以,但是前提就是得有一条双螺旋曲线。
所以,今天这个简易螺旋,反而很容易就能得到一条双螺旋曲线。
用修改器来修改曲线
把上面的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);//麻花
然后,我们就得到了这样一条曲线生成的管道。
小结
本文介绍了一种简单的螺旋方法,可用在顶点着色器里,也可以直接修改顶点数据。
还可以用这种方法去修改曲线,从而得到一条闭合的双螺旋曲线,再构造出管道。
本来想拧麻花的,但是没找到材质,有人提供一下吗?