很高兴我们又见面了,这一章节很是重要,我写了好几个案例来辅助学习
什么是位置属性
位置属性告诉电脑模型的“顶点”在哪里
大胆想象一下,假设我们想在电脑里面画一个简单的三角形,这个三角形由3个点(称为顶点)构成。亦或是我们有一个正方体的盒子,它由8个点(顶点)构成,当然复杂的模型可能有成千上万个顶点,这些顶点构建了模型的基础形态,也就是是所谓的骨架点
位置属性就好比是一张“顶点坐标清单”,我们常用BufferGeometry.position来获取顶点坐标,其本质就是
一个超长的数字清单(数组)
而这个清单按顺序记录了模型每一个顶点在3D空间中的具体位置。
其记录规则为(每3个数字表示一个顶点的坐标)
- 第一个数字是
X坐标(左右方向)。 - 第二个数字是
Y坐标(上下方向)。 - 第三个数字是
Z坐标(前后方向,屏幕里外)。
了解这些基本概念即可,重要的是实战练习和应用,让我们开始吧
案例 - point点位置演示
这里使用点模型来展示,我们将坐标数据使用Float32Array进行保存,然后设置position位置,3个数字为一组形成一个顶点,代码如下
const geometry = new THREE.BufferGeometry();
const data = new Float32Array([
-1.0, 0.0, 0.0,
1.0, 0.0, 0.0,
0.0, 2.0, 0.0,
])
geometry.setAttribute('position', new THREE.BufferAttribute(data, 3));
geometry.center();
const points = new THREE.Points(
geometry,
new THREE.PointsMaterial({color: 'deepskyblue', size: 0.5})
)
scene.add(points);
案例 - mesh模型
在这个中使用了new THREE.Mesh创建了2个三角形,分别给到了2中不同的材质,左侧为标准材质,右侧为数据纹理,具体纹理实现如下
const createDataTexture = function(opt){
opt = opt || {};
opt.width = opt.width === undefined ? 16: opt.width;
opt.height = opt.height === undefined ? 16: opt.height;
opt.forPix = opt.forPix || function(color, x, y, i, opt){
let v = Math.floor( THREE.MathUtils.seededRandom() * 255 );
color.r = v;
color.g = v;
color.b = v;
return color;
};
let size = opt.width * opt.height;
let data = new Uint8Array( 4 * size );
for ( let i = 0; i < size; i ++ ) {
let stride = i * 4,
x = i % opt.width,
y = Math.floor(i / opt.width),
color = opt.forPix( new THREE.Color(), x, y, i, opt);
data[ stride ] = color.r;
data[ stride + 1 ] = color.g;
data[ stride + 2 ] = color.b;
data[ stride + 3 ] = 255;
}
let texture = new THREE.DataTexture( data, opt.width, opt.height );
texture.needsUpdate = true;
return texture;
};
const tex1 = createDataTexture();
在添加至场景之前,我们使用mesh1.position.x = -2;将模型沿x轴移动了-2,在这里是左边
案例 - vertor顶点演示
同上一个案例一样创建两个三角形,先看效果
这里你会发现两个三角形在旋转过程中,一个显示另一个不显示,是因为大家使用了同一个材质且这个材质设置了 side为THREE.FrontSide,
const material_mesh = new THREE.MeshBasicMaterial({
side: THREE.FrontSide,
color: 'deeppink'
});
const mesh_1 = new THREE.Mesh(geometry_1, material_mesh);
mesh_1.position.x = -1.25;
scene.add(mesh_1);
const mesh_2 = new THREE.Mesh(geometry_2, material_mesh);
mesh_2.position.x = 1.25;
scene.add(mesh_2);
注意,这里使用Pointsmesh显示三角形的三个顶点, 至于相机,则是围绕Y轴做圆周旋转运动
function animation() {
const a_frame = f / fm; // 0 ~ 1
const now = new Date();
const secs = (now - lt) / 1000;
if (secs > 1 / fps) {
e.y = Math.PI * 2 * a_frame;
camera.position.set(0, 1, 1).normalize().applyEuler(e).multiplyScalar(6);
camera.lookAt(0, 0, 0);
renderer.render(scene, camera);
f += 1;
f %= fm;
lt = now;
}
requestAnimationFrame( animation );
}
案例 - boxGeometry盒子开面
这是一个有意思的案例,我们在选中盒子的一个面,在一角打开,看效果
其核心实现其实就是修改某一顶点的位置
const index = geometry.getIndex();
const vertIndex = index.array[0];
position.array[vertIndex] = 0.8;
position.array[vertIndex + 1] = 0.5;
position.array[vertIndex + 2] = 0.5;
尝试修改下右边那个顶点的位置,我们将得到如下效果
案例 - 展开立方体
基于上面的案例,实现将一个立方体的每个面展开,先看效果
效果看似简单,可实现起来并不容易,首先我们要知道一个立方体由六个矩形平面组成,一个平面又是由两个三角面组成,因此我们只需要将一个矩形面的两个三角进行移动,即可实现将一个矩形面向外展开
const setVert = function(geometry, vertIndex, pos){
pos = pos || {};
const posIndex = geometry.index.array[vertIndex] * 3,
position = geometry.getAttribute('position');
position.array[posIndex] = pos.x === undefined ? position.array[posIndex]: pos.x;
position.array[posIndex + 1] = pos.y === undefined ? position.array[posIndex + 1]: pos.y;
position.array[posIndex + 2] = pos.z === undefined ? position.array[posIndex + 2]: pos.z;
};
const setTri = function(geometry, triIndex, pos){
pos = pos || {};
const vertIndex = triIndex * 3;
setVert(geometry, vertIndex, pos);
setVert(geometry, vertIndex + 1, pos);
setVert(geometry, vertIndex + 2, pos);
};
const geometry = new THREE.BoxGeometry(1, 1, 1);
const posArray = [{x: 1}, {x: 1}, {x: -1}, {x: -1}, {y: 1}, {y: 1}, {y: -1}, {y: -1}, {z: 1}, {z: 1}, {z: -1}, {z: -1}]
posArray.forEach((pos, index) => {
setTri(geometry, index, pos)
})
案例 - 根据三维坐标显示点
const v3Array = [
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(1, 0, 0),
new THREE.Vector3(0, 1, 0),
new THREE.Vector3(0, 0, 1),
new THREE.Vector3(2, 0, 0),
new THREE.Vector3(0, 2, 0),
new THREE.Vector3(0, 0, 2),
new THREE.Vector3(3, 0, 0),
new THREE.Vector3(0, 3, 0),
new THREE.Vector3(0, 0, 3)
];
scene.add(
new THREE.Points(
new THREE.BufferGeometry().setFromPoints(v3Array),
new THREE.PointsMaterial({size: 0.15, color: 'deepskyblue'})
)
)
setFromPoints用于将一个包含 THREE.Vector3 对象的数组(v3Array)转换为几何体的顶点数据
案例 - 根据几何体显示对应的点
与上个案例类似,只是这个案例中,我们是拿到geo中的三维向量生成点
这里创建了一个函数用于获取几何体上的三维向量数组,然后通过setFromPoint转换为几何体的顶点数据
const Vector3ArrayFromGeometry = geometry => {
const pos = geometry.getAttribute('position');
let i = 0;
const len = pos.count, v3Array = [];
while(i < len ) {
const v = new THREE.Vector3(pos.getX(i), pos.getY(i), pos.getZ(i));
v3Array.push(v);
i++;
}
return v3Array;
}
const geometry = new THREE.TorusGeometry( 2, 0.55, 30, 60 );
geometry.rotateX(Math.PI / 180 * 90);
const v3Array = Vector3ArrayFromGeometry(geometry);
const Vector3ArrayToGeometry = v3Array => {
return new THREE.BufferGeometry().setFromPoints(v3Array)
}
如上图,离我们的效果还差点,我们只需要修改下顶点数据即可实现
v3Array.forEach(v => {
const vd = new THREE.Vector3();
vd.copy(v).normalize().multiplyScalar(0.75* THREE.MathUtils.seededRandom() );
v.add(vd);
})
案例 - 通过向量更新
这个案例是粒子变换的雏形,明白这个后粒子模型的变化就比较好理解了,先看下面这个效果 ,它是圆环和球体之间的组合
let geo_sphere = new THREE.SphereGeometry(1.5, 30, 30); // 球体
let geo_torus = new THREE.TorusGeometry(1, 0.5, 30, 30); // 圆环
let v3array = Vector3ArrayFromGeometry(geo_torus);
let points = new THREE.Points( geo_sphere, new THREE.PointsMaterial({ size: 0.05, color: 'deepskyblue'}) );
scene.add(points);
这里我创建了一个球体和圆环,并拿到了圆环的向量数组,以及将圆球粒子化呈现
接下来我们来进行变换,拿到圆环的顶点数据,这里我创建了一个方法,将之前拿到的向量转换为顶点,当然有点脱裤子放那个的味道,可以使用position直接获取
let typed = Vector3ArrayToTyped(v3array);
let pos = geo_sphere.getAttribute('position');
let alpha = 1;
pos.array = pos.array.map( (n, i) => {
let d = typed[i] === undefined ? 0: typed[i];
return n + d * alpha;
});
pos.needsUpdate = true;
这里我将圆环的顶点数据和球体的顶点数据进行了相加,当然我们也可以进行其他运算,alpha这里我用来控制变换形态
案例 - 应用欧拉
先看实现效果
这个案例与之前的圆环粒子不同,这里采用欧拉角
const geometry = new THREE.TorusGeometry(2, 0.75, 30, 90);
geometry.rotateX(Math.PI / 180 * 90);
const v3Array = Vector3ArrayFromGeometry(geometry);
v3Array.forEach((v) => {
const v_delta = new THREE.Vector3(0, 0, 1);
const eu = new THREE.Euler();
if(v.y > 0){
eu.x = 1 * Math.random();
}
v_delta.normalize().applyEuler(eu).multiplyScalar(1);
v.add(v_delta);
});
这应该很好理解,通过使用欧拉来旋转向量从而改变点的位置,eu.x表示在x轴上进行选择
案例 - 练习setFromPoints
这个案例很简单,就是单纯的再练习下setFromPoints方法
const v_start = new THREE.Vector3(-5,2,-5); // 起始点
const v_end = new THREE.Vector3(5,0,0); // 终止点
const curve = new THREE.LineCurve3(v_start, v_end); // 创建一个线段曲线
const geometry = new THREE.BufferGeometry().setFromPoints( curve.getPoints( 50 ) ); // 根据曲线生成几何体
scene.add(
new THREE.Points(
geometry,
new THREE.PointsMaterial({size: 0.15, color: 'deepskyblue'})
)
)
案例 - 设置属性
前面实现了一条直线,这里来一条曲线吧,不知道怎么生成曲线的同志可以看我之前曲线章节 Three.js-硬要自学系列15 (圆弧顶点、几何体方法、曲线简介、圆、椭圆、样条曲线、贝塞尔曲线)
const v_start = new THREE.Vector3(-5, 0, 0);
const v_end = new THREE.Vector3(5, 0, 0);
const v_controlA = v_start.clone().lerp(v_end, 0.25).add( new THREE.Vector3(0,8,0) );
const v_controlB = v_start.clone().lerp(v_end, 0.75).add( new THREE.Vector3(0,-8,0) );
const curve = new THREE.CubicBezierCurve3(v_start, v_controlA, v_controlB, v_end);
const data = [];
let i = 0, count = 100;
while(i < count){
const a = i / ( count - 1 );
const v = curve.getPoint(a); //
data.push(v.x, v.y, v.z)
i += 1;
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute( new Float32Array( data ), 3 ));
geometry.center();
const points = new THREE.Points(
geometry,
new THREE.PointsMaterial({size: 0.15, color: 'deepskyblue'})
)
scene.add(points);
最后我们来个粒子几何体变换来结束本章的学习,先看效果
如果对上面那个圆环和球体的案例理解的话,那么对于这个案例中的核心方法lerpGeo应该很容易理解
完整代码
const lerpGeo = function (geo, geoA, geoB, alpha) {
alpha = alpha || 0;
let pos = geo.getAttribute('position');
let posA = geoA.getAttribute('position');
let posB = geoB.getAttribute('position');
let i = 0;
const len = pos.array.length;
while(i < len){
const v = new THREE.Vector3(posA.array[i], posA.array[i + 1], posA.array[i + 2]);
const v2 = new THREE.Vector3(posB.array[i], posB.array[i + 1], posB.array[i + 2]);
v.lerp(v2, alpha);
pos.array[i] = v.x;
pos.array[i + 1] = v.y;
pos.array[i + 2] = v.z;
i += 3;
}
pos.needsUpdate = true;
}
let geo_sphere = new THREE.SphereGeometry(1.5, 30, 30);
let geo_torus = new THREE.TorusGeometry(1, 0.5, 30, 30);
let points = new THREE.Points( geo_sphere.clone(), new THREE.PointsMaterial({ size: 0.05, color: 'deepskyblue' }) );
scene.add(points);
const FPS_UPDATE = 30,
FPS_MOVEMENT = 30,
FRAME_MAX = 450;
let secs = 0, frame = 0, lt = new Date();
const update = function (frame, frameMax) {
const a = frame / frameMax;
const b = 1 - Math.abs(0.5 - a) / 0.5;
lerpGeo(points.geometry, geo_sphere, geo_torus, b);
}
orbitControls.update();
function animation() {
const now = new Date();
secs = (now - lt) / 1000;
if (secs > 1 / FPS_UPDATE) {
update(Math.floor(frame), FRAME_MAX);
renderer.render(scene, camera);
frame += FPS_MOVEMENT * secs;
frame %= FRAME_MAX;
lt = now;
}
requestAnimationFrame( animation );
}
animation();