ThreeJs入门27-WebGl性能篇:高性能渲染160万个三角形

3,842 阅读5分钟

「这是我参与2022首次更文挑战的第31天,活动详情查看:2022首次更文挑战

示例代码采用three.js-r73版本: cdnjs.cloudflare.com/ajax/libs/t…

有的时候我们会问到一些问题, webgl能不能实现一些大型的展示场景? 我们webgl项目在浏览器没问题,在手机端就卡了怎么回事? 这些问题都是和webgl性能相关的问题,我们一起来看看吧。

渲染160万三角形

  • 如何渲染160万个三角形,还能保持达到35帧以上
    • 约定:限制三角形的大小,三角形都在一个立方体内
  • 我们先来看看效果,然后开始我们的绘制

image.png

初始化场景

  • 我们给场景添加雾效果
function initScene() {
    scene = new THREE.Scene();
    scene.fog = new THREE.Fog(0x050505, 2000, 3500);
}

初始化相机

  • 把相机添加到场景中
const near = 1;
const far = 3500;

function initCamera() {
    camera = new THREE.PerspectiveCamera(27, width / height, near, far);
    camera.position.z = 2750;
    scene.add(camera);
}

初始化灯光

  • 创建一个环境光,两个平行光
function initLight() {
    scene.add(new THREE.AmbientLight(0x444444));
    light = new THREE.DirectionalLight(0xffffff, 0.5);
    light.position.set(1, 1, 1);
    scene.add(light);

    var light2 = new THREE.DirectionalLight(0xffffff, 1.5);
    light2.position.set(0, -1, 0);
    scene.add(light2);
}

创建160万个三角形

初始化存储空间

var triangles = 1600000; // 三角形的数量

var geometry = new THREE.BufferGeometry();
// 生成160万个三角形所需顶点的存储空间,一个三角形有三个顶点,一个顶点有xyz三个变量
var positions = new Float32Array(triangles * 3 * 3);
// 这里是每个顶点一个法线,也可以一个面一个法线
var normals = new Float32Array(triangles * 3 * 3);
// 每个顶点一种颜色
var colors = new Float32Array(triangles * 3 * 3);

var color = new THREE.Color();

javascript分配连续的空间

  • 这里我们涉及到了存储空间的概念
  • javascript的Uint16Array、Float32Array等对象就能够分配指定数目的整形或者浮点型数组。由它们分配数组是线性的、连续的单元,所以CPU访问速度极快无比。
  • 文档:Uint16Array
  • 我们一个三角形有三个顶点,每个顶点有3个变量xyz,所以顶点需要的存储空间就是triangles * 3 * 3,法线和顶点颜色同理

初始化向量和限定三角形范围

var n = 800, n2 = n / 2; // 限定三角形出现的范围是[-400,400]这么一个立方体中,n2表示直径的一半
var d = 12, d2 = d / 2; // 限定三角形的大小为12,d2表示边长的一半(假如有半径)

var pA = new THREE.Vector3();
var pB = new THREE.Vector3();
var pC = new THREE.Vector3();

var cb = new THREE.Vector3();
var ab = new THREE.Vector3();
  • 我们限定了三角形出现的范围是-400,400],三角形的大小是12

向量的加法、减法

  • 这里涉及到了向量的加减法问题,后面会用到。
  • 三角形定则:
    • 三角形定则解决向量加法的方法:将各个向量依次首尾顺次相接,结果为第一个向量的起点指向最后一个向量的终点
  • 平面四边形定则:
    • 平行四边形定则解决向量加法的方法:将两个向量平移至公共起点,以向量的两条边作平行四边形,结果为公共起点的对角线。
    • 平行四边形定则解决向量减法的方法:将两个向量平移至公共起点,以向量的两条边作平行四边形,结果由减向量的终点指向被减向量的终点

image.png

遍历点的长度,随机生成顶点、法向量、顶点颜色

  • 随机生成一个顶点
for (var i = 0; i < positions.length; i += 9) {
    // 通过随机数生成点的位置

    // 生成一个顶点,范围是[-400,400]
    var x = Math.random() * n - n2;
    var y = Math.random() * n - n2;
    var z = Math.random() * n - n2;
}
  • 根据随机生成的顶点,随机生成三个点,供组成三角形
// 随机产生a,b,c三个点
// 点的位置再加上三角形的大小
var ax = x + Math.random() * d - d2;
var ay = y + Math.random() * d - d2;
var az = z + Math.random() * d - d2;

var bx = x + Math.random() * d - d2;
var by = y + Math.random() * d - d2;
var bz = z + Math.random() * d - d2;

var cx = x + Math.random() * d - d2;
var cy = y + Math.random() * d - d2;
var cz = z + Math.random() * d - d2;
// a点
positions[i] = ax;
positions[i + 1] = ay;
positions[i + 2] = az;
// b点
positions[i + 3] = bx;
positions[i + 4] = by;
positions[i + 5] = bz;
// c点
positions[i + 6] = cx;
positions[i + 7] = cy;
positions[i + 8] = cz;

计算每个三角形的法线向量

// 把a,b,c点放到向量中
pA.set(ax, ay, az);
pB.set(bx, by, bz);
pC.set(cx, cy, cz);
// 向量减法计算向量,是为了计算垂直于三角形的向量
cb.subVectors(pC, pB)
ab.subVectors(pA, pB)
cb.cross(ab) // 得到正交向量,垂直于两个向量组成的平面,也就是法线
// 向量的归一化
cb.normalize()

// 存储法线坐标
var nx = cb.x
var ny = cb.y
var nz = cb.z
// a顶点法线
normals[i] = nx
normals[i + 1] = ny
normals[i + 2] = nz
// b顶点法线
normals[i + 3] = nz
normals[i + 4] = nz
normals[i + 5] = nz
// c顶点法线
normals[i + 6] = nz
normals[i + 7] = nz
normals[i + 8] = nz

向量计算

// 向量减法计算向量,是为了计算垂直于三角形的向量
cb.subVectors(pC, pB)
ab.subVectors(pA, pB)
  • 上面两个减法计算我们通过画图解释一下

image.png

向量的正交向量cross

  • 我们的法向量是垂直于平面的,所以我们需要计算正交向量
  • 如果两个或多个向量,它们的点积为0,那么它们互相称为正交向量。在二维或三维的欧几里得空间中,两个或三个向量两两成90。角时,它们互为正交向量。正交向量的集合称为正交向量组。

image.png

  • 向量的归一化:即按比例缩短到单位长度,方向不变

为每个顶点赋值颜色

// 为每个顶点赋值颜色
// x / n得到范围[-0.5,0.5],加0.5得到[0,1]
var vx = (x / n) + 0.5
var vy = (y / n) + 0.5
var vz = (z / n) + 0.5

color.setRGB(vx, vy, vz)

colors[i] = color.r
colors[i + 1] = color.g
colors[i + 2] = color.b

colors[i + 3] = color.r
colors[i + 4] = color.g
colors[i + 5] = color.b

colors[i + 6] = color.r
colors[i + 7] = color.g
colors[i + 8] = color.b

给几何体添加属性

geometry.addAttribute('position', new THREE.BufferAttribute(positions, 3))
geometry.addAttribute('normal', new THREE.BufferAttribute(normals, 3))
geometry.addAttribute('color', new THREE.BufferAttribute(colors, 3))
// 计算几何体的包围盒
geometry.computeBoundingSphere();

创建网格并添加到场景中

var material = new THREE.MeshPhongMaterial({
    color: 0xaaaaaa,
    specular: 0xffffff,
    shininess: 250,
    side: THREE.DoubleSide,
    vertexColors: THREE.VertexColors
})

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

添加旋转动画

/* 数据更新 */
function update() {
    var time = Date.now() * 0.001;
    mesh.rotation.x = time * 0.25
    mesh.rotation.y = time * 0.5
}

positions、colors、normals之间的关系

  • BufferGeometry的attributes的各个属性变量赋值后,positions、colors、normals之间的关系是什么?
  • 通过下图可以看出它们之间的关系

image.png

codepen示例代码