本实例主要讲解内容
这个Three.js示例展示了如何使用**GPU实例化(GPU Instancing)**技术创建高性能的粒子系统。通过实例化渲染,我们可以在单次绘制调用中渲染大量相同基元的不同实例,显著提高渲染效率。
核心技术包括:
- GPU实例化渲染
- 自定义着色器编程
- 顶点属性与实例属性
- 基于时间的动态效果
- HSL颜色空间转换
完整代码注释
<!DOCTYPE html>
<html lang="en">
<head>
<title>three.js webgl - instanced particles - billboards - colors</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<link type="text/css" rel="stylesheet" href="main.css">
</head>
<body>
<div id="info">
<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - instanced circle billboards - colors
<div id="notSupported" style="display:none">Sorry, your graphics card + browser does not support hardware instancing</div>
</div>
<!-- 顶点着色器 -->
<script id="vshader" type="x-shader/x-vertex">
precision highp float;
uniform mat4 modelViewMatrix; // 模型视图矩阵
uniform mat4 projectionMatrix; // 投影矩阵
uniform float time; // 时间变量,用于动画
attribute vec3 position; // 顶点位置(基元几何体)
attribute vec2 uv; // 纹理坐标
attribute vec3 translate; // 实例平移向量
varying vec2 vUv; // 传递给片段着色器的纹理坐标
varying float vScale; // 传递给片段着色器的缩放因子
void main() {
// 计算实例的模型视图位置
vec4 mvPosition = modelViewMatrix * vec4( translate, 1.0 );
// 基于时间和位置计算缩放因子,创建波浪效果
vec3 trTime = vec3(translate.x + time,translate.y + time,translate.z + time);
float scale = sin( trTime.x * 2.1 ) + sin( trTime.y * 3.2 ) + sin( trTime.z * 4.3 );
vScale = scale;
scale = scale * 10.0 + 10.0;
// 应用缩放并计算最终裁剪空间位置
mvPosition.xyz += position * scale;
vUv = uv;
gl_Position = projectionMatrix * mvPosition;
}
</script>
<!-- 片段着色器 -->
<script id="fshader" type="x-shader/x-fragment">
precision highp float;
uniform sampler2D map; // 粒子纹理
varying vec2 vUv; // 从顶点着色器接收的纹理坐标
varying float vScale; // 从顶点着色器接收的缩放因子
// HSL到RGB的颜色转换函数
vec3 HUEtoRGB(float H){
H = mod(H,1.0);
float R = abs(H * 6.0 - 3.0) - 1.0;
float G = 2.0 - abs(H * 6.0 - 2.0);
float B = 2.0 - abs(H * 6.0 - 4.0);
return clamp(vec3(R,G,B),0.0,1.0);
}
vec3 HSLtoRGB(vec3 HSL){
vec3 RGB = HUEtoRGB(HSL.x);
float C = (1.0 - abs(2.0 * HSL.z - 1.0)) * HSL.y;
return (RGB - 0.5) * C + HSL.z;
}
void main() {
// 采样纹理颜色
vec4 diffuseColor = texture2D( map, vUv );
// 基于缩放因子计算HSL颜色并转换为RGB
gl_FragColor = vec4( diffuseColor.xyz * HSLtoRGB(vec3(vScale/5.0, 1.0, 0.5)), diffuseColor.w );
// 丢弃透明度低于0.5的片段,实现纹理的透明效果
if ( diffuseColor.w < 0.5 ) discard;
}
</script>
<script type="importmap">
{
"imports": {
"three": "../build/three.module.js",
"three/addons/": "./jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import Stats from 'three/addons/libs/stats.module.js';
let container, stats;
let camera, scene, renderer;
let geometry, material, mesh;
init();
function init() {
container = document.createElement( 'div' );
document.body.appendChild( container );
// 初始化相机
camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 1, 5000 );
camera.position.z = 1400;
// 初始化场景
scene = new THREE.Scene();
// 创建基础几何体(圆形)
const circleGeometry = new THREE.CircleGeometry( 1, 6 );
// 创建实例化几何体
geometry = new THREE.InstancedBufferGeometry();
geometry.index = circleGeometry.index;
geometry.attributes = circleGeometry.attributes;
// 设置粒子数量
const particleCount = 75000;
// 创建并填充实例位置数组
const translateArray = new Float32Array( particleCount * 3 );
for ( let i = 0, i3 = 0, l = particleCount; i < l; i ++, i3 += 3 ) {
// 随机分布在单位球内
translateArray[ i3 + 0 ] = Math.random() * 2 - 1;
translateArray[ i3 + 1 ] = Math.random() * 2 - 1;
translateArray[ i3 + 2 ] = Math.random() * 2 - 1;
}
// 将位置数组设置为实例属性
geometry.setAttribute( 'translate', new THREE.InstancedBufferAttribute( translateArray, 3 ) );
// 创建自定义着色器材质
material = new THREE.RawShaderMaterial( {
uniforms: {
'map': { value: new THREE.TextureLoader().load( 'textures/sprites/circle.png' ) },
'time': { value: 0.0 }
},
vertexShader: document.getElementById( 'vshader' ).textContent,
fragmentShader: document.getElementById( 'fshader' ).textContent,
depthTest: true,
depthWrite: true
} );
// 创建实例化网格并添加到场景
mesh = new THREE.Mesh( geometry, material );
mesh.scale.set( 500, 500, 500 ); // 放大整体场景
scene.add( mesh );
// 初始化渲染器
renderer = new THREE.WebGLRenderer();
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.setAnimationLoop( animate );
container.appendChild( renderer.domElement );
// 添加性能统计
stats = new Stats();
container.appendChild( stats.dom );
// 窗口大小变化事件监听
window.addEventListener( 'resize', onWindowResize );
return true;
}
// 窗口大小变化处理函数
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
}
// 动画循环
function animate() {
// 更新时间统一变量
const time = performance.now() * 0.0005;
material.uniforms[ 'time' ].value = time;
// 旋转整个粒子系统
mesh.rotation.x = time * 0.2;
mesh.rotation.y = time * 0.4;
// 渲染场景
renderer.render( scene, camera );
// 更新性能统计
stats.update();
}
</script>
</body>
</html>
GPU实例化技术解析
什么是GPU实例化
GPU实例化是一种渲染技术,允许在单次绘制调用中渲染同一基元的多个实例,每个实例可以有不同的属性(如位置、颜色、缩放)。与传统的逐个渲染方式相比,实例化渲染的优势在于:
- 减少CPU-GPU通信:只需一次绘制调用,而不是为每个实例单独调用
- 降低内存占用:共享相同的几何体数据
- 提高渲染效率:特别适合大量相似对象的场景,如粒子系统、植被、城市建筑等
在Three.js中,我们可以通过InstancedBufferGeometry
和InstancedBufferAttribute
来实现GPU实例化。
实例化几何体的创建
本示例中,我们创建实例化几何体的步骤如下:
- 创建基础几何体:使用
CircleGeometry
创建一个简单的圆形 - 创建实例化几何体:
new THREE.InstancedBufferGeometry()
- 复制基础几何体的属性:将圆形的索引和属性复制到实例化几何体
- 添加实例属性:创建并设置每个实例的位置属性
translate
关键代码:
// 创建基础圆形几何体
const circleGeometry = new THREE.CircleGeometry( 1, 6 );
// 创建实例化几何体并复制基础几何体的属性
const geometry = new THREE.InstancedBufferGeometry();
geometry.index = circleGeometry.index;
geometry.attributes = circleGeometry.attributes;
// 创建并设置实例位置属性
const translateArray = new Float32Array( particleCount * 3 );
// 填充位置数据...
geometry.setAttribute( 'translate', new THREE.InstancedBufferAttribute( translateArray, 3 ) );
自定义着色器编程
本示例使用了自定义着色器来实现粒子的动态效果:
- 顶点着色器:计算每个粒子的最终位置和缩放,并传递给片段着色器
- 片段着色器:采样纹理并根据顶点着色器传递的缩放因子计算颜色
特别值得注意的是HSL到RGB的颜色转换算法,它允许我们基于单一变量(缩放因子)生成丰富的颜色变化。
性能优化与应用场景
GPU实例化技术特别适合以下场景:
- 粒子系统:如本例所示,高效渲染大量粒子
- 植被模拟:渲染森林、草地等
- 城市建筑:渲染大量相似的建筑或建筑部件
- 大规模数据可视化:如点云数据、星空模拟等
使用实例化渲染时的性能优化建议:
- 批量更新数据:尽量批量更新实例属性,减少渲染状态切换
- 合理使用uniforms和attributes:将频繁变化的数据放在uniforms中,静态数据放在attributes中
- 优化着色器计算:避免在着色器中进行复杂计算,特别是在处理大量实例时
- 考虑视锥体剔除:对于大规模场景,考虑实现视锥体剔除以避免渲染不可见的实例
这种技术虽然强大,但需要注意并非所有硬件都支持,特别是较旧的移动设备。在实际应用中,建议提供回退方案或降级策略。