Three.js-硬要自学系列38之专项学习缓冲几何体

什么是缓冲几何体

假设我们要在电脑屏幕上画一个 3D 的立方体。这个立方体由很多个小的三角形面片组成(这是 WebGL 绘制的基础)。每个三角形的角叫做顶点。每个顶点至少需要知道:

  1. 位置信息:它在 3D 空间中的坐标 (x, y, z)。

  2. 朝向信息 :这个点所面对的方向(法向量),用于计算光照。

  3. 贴图坐标 :告诉电脑这个点对应到图片(纹理)上的哪个位置,以便给立方体贴上图案。

在three.js中使用BufferGeometry来构建缓冲几何体

它是高效描述 3D 形状的数据结构

它把顶点数据(位置、法线、UV等)按类型分离,各自打包成连续的大数组(缓冲区)。

我们需要通过一系列的练习来熟悉掌握缓冲几何体相关概念

案例 - 顶点构建三角形

通过设置一组顶点坐标,来构建一个三角形

js 复制代码
const geometry = new THREE.BufferGeometry();
const vertices = new Float32Array([
    -1, 0, 0,
    1, 0, 0,
    1, 1, 0
])
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
const mesh = new THREE.Mesh(geometry, new THREE.MeshBasicMaterial({color: 'deepskyblue', side: THREE.DoubleSide}));
scene.add(mesh);

案例 - 设置法线

js 复制代码
const data_normal = [
    0, -0, 1,
    0, -0, 1,
    0, -0, 1
];
geometry.setAttribute('normal', new THREE.BufferAttribute( new Float32Array(data_normal), 3 ));

// 显示法线
const helper = new VertexNormalsHelper( mesh1, 0.5, 'yellow' );
scene.add( helper );

案例 - UV贴图

这个案例在之前的章节里有写过,这里就当是复习一下,下面是其核心代码

js 复制代码
const width = 4,
height = 4;
const size = width * height;
const data = new Uint8Array( 4 * size );
for (let i = 0; i < size; i++) {
    const stride = i * 4;
    const v = Math.floor(THREE.MathUtils.seededRandom() * 255);
    data[stride] = v-15;
    data[stride + 1] = v-40;
    data[stride + 2] = v-170;
    data[stride + 3] = 155;
}

const texture = new THREE.DataTexture(data, width, height);
texture.needsUpdate = true;
const mesh1 = new THREE.Mesh(
    geometry,
    new THREE.MeshStandardMaterial({
        map: texture,
        side: THREE.FrontSide
}));

案例 - groups组合

先看效果

这个案例用到了缓冲几何体的分组功能,它可以让我们将一个几何体划分为多个部分,每个部分使用不同的材质渲染

js 复制代码
const materials = [
    new THREE.MeshBasicMaterial({color: 'deepskyblue', side: THREE.DoubleSide}),
    new THREE.MeshBasicMaterial({color: 'deeppink', side: THREE.DoubleSide}),
]

const geometry = new THREE.BufferGeometry();
const vertices = new Float32Array([
    0, 0, 0, // triangle 1
    1, 0, 0,
    1, 1, 0,
    0, 0, 0, // triangle 2
    0, 1, 0,
    1, 1, 0
])

geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
geometry.addGroup(0, 3, 1);
geometry.addGroup(3, 3, 0);
scene.add(new THREE.Mesh(geometry, materials));

代码中materials数组中定义了两种不同颜色的基础材质,用于后续分组渲染,vertices定义了6个顶点数据,需要注意的是分组规则中的addGroup,其参数分别指顶点数据开始索引,顶点数,以及材质索引

案例 - Index索引

这个案例主要是练习如何通过索引渲染,减少顶点数据的重复存储

通过索引渲染,我们只需要存储4个顶点,6个索引值便可实现该效果

js 复制代码
const geometry = new THREE.BufferGeometry();
const pos = new THREE.BufferAttribute(
    new Float32Array([
        0,-3, 0,  // 0
        0, 3, 0,  // 1
        -5, 0, 0,  // 2
        0, 0,-5   // 3 
    ]), 3
);
geometry.setAttribute('position', pos);
geometry.computeVertexNormals();  // 计算顶点法线
geometry.setIndex([0, 1, 2, 0, 1, 3]);  // 定义索引

const mesh = new THREE.Mesh(
    geometry, 
    new THREE.MeshNormalMaterial({ side: THREE.DoubleSide})
);
scene.add(mesh);

案例 - triangles三角

先看效果

从图中可以看到,我们创建了两个相连的三角形,形成了一个具有立体感的几何体

js 复制代码
const geometry = new THREE.BufferGeometry();
const vertices = new Float32Array([
    0.00,  0.00,  0.00,
    1.00,  0.00,  0.00,
    0.50,  1.00, -0.50,
    1.00,  0.00,  0.00,
    0.75,  0.00, -1.00,
    0.50,  1.00, -0.50
])
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
geometry.computeVertexNormals();  // 计算顶点法线

const mesh = new THREE.Mesh(geometry, materials);
scene.add(mesh);

案例 - unIndexed贴图索引

这是一个对比案例,左侧蓝色三角形设置uv属性以及进行了法线贴图使得平面看起来更具立体感,右侧红色三角形被转换为非索引几何体与蓝色三角形产生区别

js 复制代码
const texture_normal = new THREE.DataTexture( new Uint8Array( data_normalmap ), 4,  4 );
texture_normal.needsUpdate = true;
const material_nm = new THREE.MeshPhongMaterial({
    color: 'deepskyblue',
    normalMap: texture_normal,
    side: THREE.FrontSide
});
const material = new THREE.MeshPhongMaterial({
    color: 'deeppink',
    side: THREE.FrontSide
});

const geometry = new THREE.BufferGeometry();
const vertices = new Float32Array([
    0.00,  0.00,  0.00,
    2.00,  0.00,  0.00,
    0.00,  2.00,  0.00,
    0.00,  0.00, -3.50
]);
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
geometry.setIndex([0,1,2,1,3,2]);
geometry.computeVertexNormals();
const data_uv = new Float32Array([
    0.00,  1.00,
    0.00,  0.00,
    1.00,  0.00,
    1.00,  1.00,
]);
geometry.setAttribute('uv', new THREE.BufferAttribute(data_uv, 2));
geometry.computeTangents();  // 计算切线向量,用于光照计算。

const geometry_ni = geometry.toNonIndexed(); // 转换为非索引几何体,以便后续操作。
geometry_ni.computeVertexNormals();  // 计算顶点法线,用于光照计算。

案例 - rotation旋转

先看效果

如图所示,左侧椎体是以几何体级别进行旋转,而右侧椎体则是以对象级别进行旋转的,两者的旋转方式如下

js 复制代码
mesh1.geometry.copy( new THREE.ConeGeometry(0.25, 2, 32, 32) );
mesh1.geometry.rotateX( rx );
mesh1.geometry.rotateZ( rz );

mesh2.rotation.set(rx ,0, rz)

案例 - json转换

这个案例演示了Three.js中3D几何体的序列化和反序列化流程,通过创建THREE.BufferGeometryLoader实例,调佣parse方法将JSON对象解析为BufferGeometry实例, 这个案例对于需要保存、传输或动态生成3D模型数据的开发工作特别有用,感兴趣的童鞋可以尝试载入个模型转换看看

js 复制代码
const geo = new THREE.SphereGeometry(1,8,8);

const buffObj = geo.toNonIndexed().toJSON(); // 转为非索引几何体
console.log(buffObj);

const text = JSON.stringify(buffObj);

const loader = new THREE.BufferGeometryLoader();
const obj = JSON.parse(text); // 解析json

const geo2 = loader.parse(obj);
const mesh = new THREE.Mesh(geo2)
scene.add(mesh);

案例 - 四元数

这里通过四元数来对几何体进行旋转,并以点云形式展现该几何体的顶点,效果如下

js 复制代码
const geometry = new THREE.CylinderGeometry(0, 2, 6, 32, 32);
const q = new THREE.Quaternion();  // 创建一个四元数
q.setFromAxisAngle(new THREE.Vector3(0, 0, 1), Math.PI / 4);  // 将四元数设置为绕Z轴旋转45度的四元数
geometry.applyQuaternion(q);  // 应用四元数到几何体上

const material = new THREE.PointsMaterial({ color: 'deepskyblue', size: 0.1 });  // 点的材质
const points = new THREE.Points(geometry, material);  // 创建点云
scene.add(points);  // 将点云添加到场景中

案例 - 中心点

这个案例主要使用到 center方法,该方法有以下几个功能

  1. 计算几何体的边界盒(bounding box)
  2. 计算几何体的中心点坐标
  3. 将几何体平移,使其中心点位于原点(0,0,0)
js 复制代码
const geometry = new THREE.BufferGeometry();
const vertices = new Float32Array([
    0,0,0,
    2,0,0,
    0,2,0,
    2,2,0
])
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
geometry.setIndex([0, 1, 2, 1, 3, 2 ]);  // 定义索引
geometry.computeVertexNormals();  // 计算顶点法线
geometry.center();  // 计算几何体的中心

const mesh = new THREE.Mesh(
    geometry,
    new THREE.MeshNormalMaterial({ side: THREE.DoubleSide})
);
scene.add(mesh);

案例 - 平移

练习一下几何体的平移 translate很简单,效果如下

js 复制代码
[ [0,1,0], [1,0,-1], [0,1,-4], ].forEach( (pos, i, arr) => {
    const geometry = geometry_source.clone().translate( pos[0], pos[1], pos[2]);
    const mesh = new THREE.Mesh(
        geometry,
        new THREE.MeshBasicMaterial({
            color: 'deepskyblue',
            side: THREE.FrontSide,
            transparent: true,
            opacity: 0.5
        })
    );
    mesh.renderOrder = arr.length - i;  // 控制渲染顺序,后添加的在前显示
    scene.add(mesh);
});

案例 - setfrompoints

这个案例主要是练习使用setfrompoints自动设置geometryposition属性

js 复制代码
const points_array = [
    new THREE.Vector3( -1, -1,  1),
    new THREE.Vector3( -1, -1, -1),
    new THREE.Vector3(  1, -1,  1),
    new THREE.Vector3(  1, -1, -1),
    new THREE.Vector3( -1,  1,  1),
    new THREE.Vector3( -1,  1, -1),
    new THREE.Vector3(  1,  1,  1),
    new THREE.Vector3(  1,  1, -1),
];

const geometry = new THREE.BufferGeometry();
geometry.setFromPoints(points_array);
const material = new THREE.PointsMaterial( { color: 'deepskyblue', size: 0.15 } );
scene.add( new THREE.Points( geometry, material ) );

案例 - 克隆

练习克隆几何体,clone方法会创建当前几何体的完整副本,对于需要基于同一几何体创建多个不同变体的场景非常有用

js 复制代码
const geo_source = new THREE.ConeGeometry( 0.5, 1, 24, 24 );  // 圆锥体
const geo1 = geo_source.clone().rotateX(Math.PI / 180 * 45).translate(-2,0,0);  // 旋转45度,然后平移到(-2,0,0)
const geo2 = geo_source.clone();

const material = new THREE.MeshNormalMaterial();
const mesh1 = new THREE.Mesh(geo1, material);
const mesh2 = new THREE.Mesh(geo2, material);
scene.add(mesh1, mesh2);

案例 - builtinBox

这个案例在之前的章节里有介绍过了,实现方式也很简单,这里就贴代码看看就好了

js 复制代码
const w=2, h=4,d=2;
const ws=8, hs=20, ds=32;
const geometry = new THREE.BoxGeometry(w, h, d, ws, hs, ds);  // 立方体

const material = new THREE.PointsMaterial({ size: 0.05, color: 'deepskyblue' });  // 点材质
const points = new THREE.Points(geometry, material);  // 点
scene.add(points);

案例 - 胶囊

该案例演示了如何创建一个胶囊几何体

js 复制代码
const radius = 1;
const length = 2;
const capsubs = 8;
const radsegs = 16;
const geometry = new THREE.CapsuleGeometry(radius, length, capsubs, radsegs);  // 胶囊

const material = new THREE.MeshNormalMaterial({wireframe: true});   
const mesh = new THREE.Mesh(geometry, material);  // 网格
scene.add(mesh);

案例 - builtinEdges

这个案例使用到EdgesGeometry,它接收一个已有的几何体作为输入,提取该几何体的边缘线,根据角度阈值过滤边缘(只保留两个相邻面之间夹角大于指定阈值的边缘),最后生成一个质保函这些边缘线的新几何体

js 复制代码
const geo_source = new THREE.BoxGeometry();
const threshold_angle = 1;
const geometry = new THREE.EdgesGeometry(geo_source, threshold_angle);  // 边

const material = new THREE.LineBasicMaterial({color: 'deeppink', linewidth: 5});  // 线
const line = new THREE.LineSegments(geometry, material);  // 线段
scene.add(line);

案例 - 挤出

先看效果

该案例中将使用ExtrudeGeometry对2D形状进行挤出,从而实现如图效果, 该方法的配置参数说明如下

  • depth: 挤出的深度(默认为1)
  • bevelEnabled: 是否启用斜面(默认为true)
  • bevelThickness: 斜面厚度
  • bevelSize: 斜面大小
  • bevelSegments: 斜面的分段数
  • curveSegments: 曲线的分段数
  • steps: 沿挤出深度的分段数
js 复制代码
const shape = new THREE.Shape();  // 定义形状
shape.moveTo( 2,-1 );
shape.bezierCurveTo( 0.45,-0.25,    0.25,0,    1,0 );
shape.lineTo(  1,1 );
shape.lineTo( -1,2 );
shape.bezierCurveTo(-2,0,   -2,-1,   0,-1 ); 

const geometry = new THREE.ExtrudeGeometry( shape );

const material = new THREE.MeshNormalMaterial();
const mesh = new THREE.Mesh( geometry, material );
scene.add( mesh );

案例 - 旋转成型

这个案例中用到了LatheGeometry,它一系列二维点(Vector2数组)作为输入,将这些点围绕Y轴旋转一周(或指定角度),生成一个旋转体(车削体)几何形状

js 复制代码
const v1 = new THREE.Vector2( 0, 0 );
const v2 = new THREE.Vector2( 0.5, 0 );
const v3 = new THREE.Vector2( 0.5, 0.5);
const v4 = new THREE.Vector2( 0.4, 0.5);
const v5 = new THREE.Vector2( 0.2, 0.1);
const v6 = new THREE.Vector2( 0, 0.1);
const vc1 = v2.clone().lerp(v3, 0.5).add( new THREE.Vector2(0.25,-0.1) );
const vc2 = v4.clone().lerp(v5, 0.5).add( new THREE.Vector2(0.25, 0) );
const curve = new THREE.CurvePath();
curve.add( new THREE.LineCurve( v1, v2 ) );
curve.add( new THREE.QuadraticBezierCurve(v2, vc1, v3) );
curve.add( new THREE.LineCurve( v3, v4 ) );
curve.add( new THREE.QuadraticBezierCurve( v4, vc2, v5 ) );
curve.add( new THREE.LineCurve( v5, v6 ) );
const v2array = curve.getPoints(20);

const segments_lathe = 80;
const phi_start = 0;
const phi_length = 2*Math.PI;
const geometry = new THREE.LatheGeometry( v2array, segments_lathe, phi_start, phi_length );

案例 - ring环

这个案例利用了three.js 内置的RingGeometry几何体创建一个圆环

js 复制代码
const radius1 = 1.5;
const radius2 = 1;
const segments = 80;
const geometry = new THREE.RingGeometry( radius1, radius2, segments );

const material = new THREE.LineBasicMaterial({ linewidth: 3, color: 'deepskyblue'});
const line = new THREE.LineSegments( geometry, material );
scene.add( line );

案例 - shape形状

这个案例使用了three.js内置的ShapeGeometry几何体实现

js 复制代码
const heartShape = new THREE.Shape();
heartShape.moveTo( 2.5, 2.5 );
heartShape.bezierCurveTo( 2.5, 2.5, 2.0, 0, 0, 0 );
heartShape.bezierCurveTo( - 3.0, 0, - 3.0, 3.5, - 3.0, 3.5 );
heartShape.bezierCurveTo( - 3.0, 5.5, - 1.0, 7.7, 2.5, 9.5 );
heartShape.bezierCurveTo( 6.0, 7.7, 8.0, 5.5, 8.0, 3.5 );
heartShape.bezierCurveTo( 8.0, 3.5, 8.0, 0, 5.0, 0 );
heartShape.bezierCurveTo( 3.5, 0, 2.5, 2.5, 2.5, 2.5 );

const geometry = new THREE.ShapeGeometry( heartShape );
geometry.rotateX(Math.PI);
geometry.rotateY(Math.PI);
geometry.scale(0.3, 0.3, 0.3);
geometry.center();

const material = new THREE.MeshNormalMaterial({ side: THREE.DoubleSide});
const mesh = new THREE.Mesh( geometry, material );
scene.add( mesh );

案例 - 环形体

与上面那个案例不同,这里用到了内置的TorusGeometry创建了一个环形体,而不是环面,效果中是以点云形式呈现

js 复制代码
const geometry = new THREE.TorusGeometry( 1, 0.3, 26, 80 );
const material = new THREE.PointsMaterial( { color: 'cyan', size: 0.05 } );
const points = new THREE.Points( geometry, material );
scene.add( points );

案例 - loadPromise

先看效果

这个案例演示了如何处理多个模型加载的方案,首先我们创建一个能够加载多个BufferGeometry JSON

js 复制代码
const loadBufferGeometryJSON = (urls = [], w = 2, scale = 5, material = new THREE.MeshNormalMaterial()) => {
    const onBuffLoad = (geometry, i) => {
        const x = i % w;
        const z = Math.floor(i / w);
        const mesh = new THREE.Mesh(geometry, material);
        mesh.name = `mesh_${i}`;
        mesh.position.set(x,0, z).multiplyScalar(scale);
        scene.add(mesh);
    }
    const onBuffProgress = (geometry) => {};
    return new Promise((resolve, reject) => {
        const manager = new THREE.LoadingManager();
        manager.onLoad = () => {
            resolve(scene);
        };
        const onBuffError = (error) => {
            reject(error);
        }
        const loader = new THREE.BufferGeometryLoader(manager);
        urls.forEach((url,index)=> {
            loader.load(url,(geometry)=> {
                onBuffLoad(geometry, index);
            }, onBuffProgress, onBuffError);
        })
    });
}

加载多个json格式的geometry

js 复制代码
const URLS = [
    '/json/vertcolor-trees/6tri/0.json',
    '/json/vertcolor-trees/6tri/1.json',
    '/json/vertcolor-trees/6tri/2.json',
    '/json/vertcolor-trees/6tri/3.json',
    '/json/vertcolor-trees/6tri/4.json',
    '/json/vertcolor-trees/6tri/5.json'
];
const material = new THREE.MeshBasicMaterial({vertexColors: true, side: THREE.DoubleSide });
loadBufferGeometryJSON(URLS, 2, 4, material)
.then( (scene_source) => {
    console.log('JSON files are loaded!');
    scene.add( scene_source );
    renderer.render(scene, camera);
})
.catch( (e) => {
    console.warn('No Good.');
    console.warn(e);
});  

案例 - morphattributes

先看效果

这里用到了morphTargetInfluences,它用于在不同的几何形状之间实现平滑过渡,常用于角色动画、面部表情或物体变形等效果

js 复制代码
const geo = new THREE.BoxGeometry(2, 2, 2, 32, 32, 32);
geo.morphAttributes.position = []; 
const pos = geo.attributes.position;
const data_pos = [];
for ( let i = 0; i < pos.count; i ++ ) {
    const x = pos.getX( i );
    const y = pos.getY( i );
    const z = pos.getZ( i );
    data_pos.push(
        x * Math.sqrt( 1 - ( y * y / 2 ) - ( z * z / 2 ) + ( y * y * z * z / 3 ) ),
        y * Math.sqrt( 1 - ( z * z / 2 ) - ( x * x / 2 ) + ( z * z * x * x / 3 ) ),
        z * Math.sqrt( 1 - ( x * x / 2 ) - ( y * y / 2 ) + ( x * x * y * y / 3 ) )
    );
}
geo.morphAttributes.position[ 0 ] = new THREE.Float32BufferAttribute( data_pos, 3 );

const material = new THREE.MeshNormalMaterial({ side: THREE.DoubleSide });
const mesh = new THREE.Mesh(geo, material);
scene.add(mesh);

orbitControls.update();

let count = 0.01;
function animation() {
    // 渲染场景和相机
    renderer.render( scene, camera );
    requestAnimationFrame( animation );
    count += 0.01;
    if ( count > 10 ) count = 0;
    mesh.morphTargetInfluences[ 0 ] = ( Math.cos( count * Math.PI ) + 1 ) / 0.5;
}
animation();
相关推荐
zwjapple1 小时前
docker-compose一键部署全栈项目。springboot后端,react前端
前端·spring boot·docker
像风一样自由20203 小时前
HTML与JavaScript:构建动态交互式Web页面的基石
前端·javascript·html
aiprtem4 小时前
基于Flutter的web登录设计
前端·flutter
浪裡遊4 小时前
React Hooks全面解析:从基础到高级的实用指南
开发语言·前端·javascript·react.js·node.js·ecmascript·php
why技术4 小时前
Stack Overflow,轰然倒下!
前端·人工智能·后端
GISer_Jing4 小时前
0704-0706上海,又聚上了
前端·新浪微博
止观止5 小时前
深入探索 pnpm:高效磁盘利用与灵活的包管理解决方案
前端·pnpm·前端工程化·包管理器
whale fall5 小时前
npm install安装的node_modules是什么
前端·npm·node.js
烛阴5 小时前
简单入门Python装饰器
前端·python
袁煦丞6 小时前
数据库设计神器DrawDB:cpolar内网穿透实验室第595个成功挑战
前端·程序员·远程工作