上一节中我们生成了一个简陋的场景:两个薄片。
也许你可能会说,为什么不加载建模软件生成的3D模型呢?
在以后的内容中,会有外部模型的导入,不过不想占太多的比例。写代码才有意思,你说呢。
我的想法是,海岛应该是不规则的,有突出来的部分,也有海水冲激形成的沙滩。
所以海岛边缘应该是随机生成的曲线,简单的思路如下:
将islands.mesh.rotation.x = Math.PI / 2注释掉,还原plane原始的位置。
选取网格一边最外围的两个顶点做为起始点和结束点,再随机选取两者之间的一个点,用这三个点生成一段曲线。
上下左右各画一次。再想办法把边缘上的顶点映射到曲线上,这样就可以生成一个简单的海岛形状,如上图。
让我们开始吧!
目测顶点坐标是这样的:
先画一条曲线,修改 island() 方法
function island (width, height, segments) {
const island = {
width: width,
height: height,
segments: segments,
mesh: null,
init: function() {
const geometry = new THREE.PlaneGeometry(this.width, this.height, this.segments, this.segments)
const vertices = geometry.vertices
const curve = new THREE.CatmullRomCurve3([
new THREE.Vector3(-4, 4, 0),
new THREE.Vector3(0, 4 * (1.2 + Math.random()), 0),
new THREE.Vector3(4, 4, 0)
])
const points = curve.getPoints(this.segments)
const line = new THREE.BufferGeometry().setFromPoints(points)
const lineMaterial = new THREE.LineBasicMaterial({color : 0xff0000})
const curveObject = new THREE.Line(line, lineMaterial)
scene.add(curveObject)
})
const material = new THREE.MeshPhongMaterial({
color: 0xcc6699,
side: THREE.DoubleSide,
wireframe: true
})
this.mesh = new THREE.Mesh(geometry, material)
}
}
island.init()
return island
}
可以看到我们画出了第一条曲线,还不错
接下来继续生成其他的三条。因为画法都一样,我们让它循环生成吧。
我的习惯是先构建一个画曲线顺序的数组。
图形中大部分的顺序都是顺时针的,比如css中盒模型的padding和margin,这里也按照惯例来吧。
增加如下代码
const halfX = this.width / 2
const halfY = this.height / 2
const border = [-halfX, halfY, halfX, -halfY]
const list = []
border.forEach((n, index) => {
let indexB = index + 1
if (index === border.length -1) {
indexB = 0
}
list.push([n, border[indexB]])
})
console.log(list)
打开控制台,console.log输出的结果是
[
[-4,4],
[4,4],
[4,-4],
[-4,-4]
]
回过头来观察一下图二顶点的顺序,对应上了。
将之前生成曲线的代码做一下修改
list.forEach((l, index) => {
let indexB = index + 1
if (index === list.length -1) {
indexB = 0
}
const middlePoint = (index%2 ==0) ? new THREE.Vector3(0, l[1] * (1.2 + Math.random()), 0) : new THREE.Vector3(l[0] * (1.1 + Math.random()), 0, 0)
const curve = new THREE.CatmullRomCurve3([
new THREE.Vector3(l[0], l[1], 0),
middlePoint,
new THREE.Vector3(list[indexB][0], list[indexB][1], 0)
])
const points = curve.getPoints(this.segments)
const line = new THREE.BufferGeometry().setFromPoints(points)
const lineMaterial = new THREE.LineBasicMaterial({color : 0xff0000})
const curveObject = new THREE.Line(line, lineMaterial)
scene.add(curveObject)
})
到这里四条线都画出来了,效果和想象中一样,而且每次生成的曲线也是随机的,这样意味着我们的岛屿每次都会以新的面貌出现。
接下来把海岛的外围顶点映射到曲线上。
得将曲线生成的顶点坐标保存起来,在上述代码中增加:
const newPointList = []
newPointList.push(points)
为了简单化处理代码逻辑,先映射一条曲线
let a = 0
vertices.forEach((v) => {
if (v.y === 4) {
console.log(v)
if (halfX - v.x === 0 || halfX - v.x === this.width) {
return
} else {
a++
v.y = newPointList[0][a].y
}
}
})
结果如下图所示
以上代码需要解释一下,
- 先映射最上面的曲线,所以v.y === 4;
- 输出v,可以看到,顶点的顺序是从左到右的,第一个顶点和最后一个顶点分别是-4,4和4,4;
- 过滤掉这两个点不做修改;
- 从第二个点开始,将v.y坐标修改成曲线的y坐标,因为x坐标是不变的。
如果看代码不能理解的,可以在纸上画出来看看。
好了,还有三条边需要映射,修改以上代码:
let a = 0
let b = 0
let c = 0
let d = 0
vertices.forEach((v) => {
if (v.y === 4) {
if (halfX - v.x === 0 || halfX - v.x === this.width) {
return
} else {
a++
v.y = newPointList[0][a].y
}
} else if (v.x === 4) {
if (halfY - v.y === 0 || halfY - v.y === this.height) {
return
} else {
b++
v.x = newPointList[1][b].x
}
} else if (v.y === -4) {
if (halfX - v.x === 0 || halfX - v.x === this.width) {
return
} else {
c++
v.y = newPointList[2][c].y
}
} else if (v.x === -4) {
if (halfY - v.y === 0 || halfY - v.y === this.height) {
return
} else {
d++
v.x = newPointList[3][d].x
}
} else {
v.z -= Math.random() * 0.3
}
})
以上代码顺便把平面内部没有变换的点增加一些随机的高度。
最后旋转成正确的角度。
网格的效果比之前的平面多了一些灵性
贴个图看看
这一节内容就到这里了,下一节我们聊聊贴图和一些改进。