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();
相关推荐
Larcher3 分钟前
新手也能学会,100行代码玩AI LOGO
前端·llm·html
徐子颐15 分钟前
从 Vibe Coding 到 Agent Coding:Cursor 2.0 开启下一代 AI 开发范式
前端
小月鸭28 分钟前
如何理解HTML语义化
前端·html
jump6801 小时前
url输入到网页展示会发生什么?
前端
诸葛韩信1 小时前
我们需要了解的Web Workers
前端
brzhang1 小时前
我觉得可以试试 TOON —— 一个为 LLM 而生的极致压缩数据格式
前端·后端·架构
yivifu1 小时前
JavaScript Selection API详解
java·前端·javascript
这儿有一堆花1 小时前
告别 Class 组件:拥抱 React Hooks 带来的函数式新范式
前端·javascript·react.js
十二春秋2 小时前
场景模拟:基础路由配置
前端
六月的可乐2 小时前
实战干货-Vue实现AI聊天助手全流程解析
前端·vue.js·ai编程