这个Three.js示例展示了如何通过着色器选择性绘制技术高效控制大量线条的可见性。通过自定义着色器和顶点属性,实现了在单个绘制调用中动态隐藏或显示特定线条,避免了传统方法中频繁切换绘制状态的性能开销。
核心技术包括:
DOCTYPE html>
<html lang="en">
<head>
<title>three.js webgl - buffergeometry - selective - drawtitle>
<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">
<script type="x-shader/x-vertex" id="vertexshader">
attribute float visible; // 顶点可见性属性
varying float vVisible; // 传递给片段着色器的可见性
attribute vec3 vertColor; // 顶点颜色
varying vec3 vColor; // 传递给片段着色器的颜色
void main() {
vColor = vertColor;
vVisible = visible;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}
script>
<script type="x-shader/x-fragment" id="fragmentshader">
varying float vVisible; // 从顶点着色器接收的可见性
varying vec3 vColor; // 从顶点着色器接收的颜色
void main() {
if ( vVisible > 0.0 ) {
gl_FragColor = vec4( vColor, 1.0 ); // 可见则绘制
} else {
discard; // 不可见则丢弃片段
}
}
script>
head>
<body>
<div id="info">
<a href="https://threejs.org" target="_blank" rel="noopener">three.jsa> buffergeometry - selective - draw
<div id="title">div>
<div id="ui"><a href="#" id="hideLines">CULL SOME LINESa> - <a href="#" id="showAllLines">SHOW ALL LINESa>div>
div>
<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 camera, scene, renderer, stats;
let geometry, mesh;
// 经纬度线数量
const numLat = 100;
const numLng = 200;
// 被剔除的线条数量
let numLinesCulled = 0;
init();
function init() {
// 初始化场景
scene = new THREE.Scene();
// 初始化相机
camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.01, 10 );
camera.position.z = 3.5;
// 添加性能统计
stats = new Stats();
document.body.appendChild( stats.dom );
// 窗口大小变化事件监听
window.addEventListener( 'resize', onWindowResize );
// 添加线条
addLines( 1.0 );
// 按钮事件监听
document.getElementById( 'hideLines' ).addEventListener( 'click', hideLines );
document.getElementById( 'showAllLines' ).addEventListener( 'click', showAllLines );
// 初始化渲染器
renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.setAnimationLoop( animate );
document.body.appendChild( renderer.domElement );
}
// 添加线条
function addLines( radius ) {
// 创建BufferGeometry
geometry = new THREE.BufferGeometry();
// 创建位置、颜色和可见性数组
// 每条线2个顶点,每个顶点3个坐标
const linePositions = new Float32Array( numLat * numLng * 3 * 2 );
// 每条线2个顶点,每个顶点3个颜色分量
const lineColors = new Float32Array( numLat * numLng * 3 * 2 );
// 每条线2个顶点,每个顶点1个可见性值
const visible = new Float32Array( numLat * numLng * 2 );
// 生成线条数据
for ( let i = 0; i < numLat; ++ i ) {
for ( let j = 0; j < numLng; ++ j ) {
// 随机生成经纬度
const lat = ( Math.random() * Math.PI ) / 50.0 + i / numLat * Math.PI;
const lng = ( Math.random() * Math.PI ) / 50.0 + j / numLng * 2 * Math.PI;
// 计算索引
const index = i * numLng + j;
// 设置线条起点(中心点)
linePositions[ index * 6 + 0 ] = 0;
linePositions[ index * 6 + 1 ] = 0;
linePositions[ index * 6 + 2 ] = 0;
// 设置线条终点(球面坐标)
linePositions[ index * 6 + 3 ] = radius * Math.sin( lat ) * Math.cos( lng );
linePositions[ index * 6 + 4 ] = radius * Math.cos( lat );
linePositions[ index * 6 + 5 ] = radius * Math.sin( lat ) * Math.sin( lng );
// 设置线条颜色
let color = new THREE.Color( 0xffffff );
// 起点颜色(暗)
color.setHSL( lat / Math.PI, 1.0, 0.2 );
lineColors[ index * 6 + 0 ] = color.r;
lineColors[ index * 6 + 1 ] = color.g;
lineColors[ index * 6 + 2 ] = color.b;
// 终点颜色(亮)
color.setHSL( lat / Math.PI, 1.0, 0.7 );
lineColors[ index * 6 + 3 ] = color.r;
lineColors[ index * 6 + 4 ] = color.g;
lineColors[ index * 6 + 5 ] = color.b;
// 默认都可见
visible[ index * 2 + 0 ] = 1.0;
visible[ index * 2 + 1 ] = 1.0;
}
}
// 设置几何体属性
geometry.setAttribute( 'position', new THREE.BufferAttribute( linePositions, 3 ) );
geometry.setAttribute( 'vertColor', new THREE.BufferAttribute( lineColors, 3 ) );
geometry.setAttribute( 'visible', new THREE.BufferAttribute( visible, 1 ) );
// 计算边界球体
geometry.computeBoundingSphere();
// 创建自定义着色器材质
const shaderMaterial = new THREE.ShaderMaterial( {
vertexShader: document.getElementById( 'vertexshader' ).textContent,
fragmentShader: document.getElementById( 'fragmentshader' ).textContent
} );
// 创建线条对象
mesh = new THREE.LineSegments( geometry, shaderMaterial );
scene.add( mesh );
// 更新显示计数
updateCount();
}
// 更新显示计数
function updateCount() {
// 格式化显示文本
const str = '1 draw call, ' + numLat * numLng + ' lines, ' + numLinesCulled + ' culled (author)';
document.getElementById( 'title' ).innerHTML = str.replace( /\B(?=(\d{3})+(?!\d))/g, ',' );
}
// 隐藏部分线条
function hideLines() {
for ( let i = 0; i < geometry.attributes.visible.array.length; i += 2 ) {
// 随机选择约25%的线条隐藏
if ( Math.random() > 0.75 ) {
if ( geometry.attributes.visible.array[ i + 0 ] ) {
++ numLinesCulled;
}
// 设置线条两个顶点都不可见
geometry.attributes.visible.array[ i + 0 ] = 0;
geometry.attributes.visible.array[ i + 1 ] = 0;
}
}
// 标记属性需要更新
geometry.attributes.visible.needsUpdate = true;
updateCount();
}
// 显示所有线条
function showAllLines() {
numLinesCulled = 0;
for ( let i = 0; i < geometry.attributes.visible.array.length; i += 2 ) {
// 设置所有线条为可见
geometry.attributes.visible.array[ i + 0 ] = 1;
geometry.attributes.visible.array[ i + 1 ] = 1;
}
// 标记属性需要更新
geometry.attributes.visible.needsUpdate = true;
updateCount();
}
// 窗口大小变化处理
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
}
// 动画循环
function animate() {
const time = Date.now() * 0.001;
// 旋转网格
mesh.rotation.x = time * 0.25;
mesh.rotation.y = time * 0.5;
// 渲染场景
renderer.render( scene, camera );
// 更新性能统计
stats.update();
}
script>
body>
html>
这个示例实现选择性绘制的核心原理是:
visible
属性,表示该顶点是否可见这种方法的优势在于:
顶点着色器:
attribute float visible;
varying float vVisible;
attribute vec3 vertColor;
varying vec3 vColor;
void main() {
vColor = vertColor;
vVisible = visible;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}
片段着色器:
varying float vVisible;
varying vec3 vColor;
void main() {
if ( vVisible > 0.0 ) {
gl_FragColor = vec4( vColor, 1.0 );
} else {
discard;
}
}
在JavaScript代码中,我们可以动态更新visible
属性来控制线条的可见性:
// 隐藏部分线条
function hideLines() {
for ( let i = 0; i < geometry.attributes.visible.array.length; i += 2 ) {
if ( Math.random() > 0.75 ) {
geometry.attributes.visible.array[ i + 0 ] = 0;
geometry.attributes.visible.array[ i + 1 ] = 0;
}
}
// 标记属性需要更新
geometry.attributes.visible.needsUpdate = true;
}
注意,修改顶点属性后需要设置needsUpdate = true
来通知Three.js重新上传数据到GPU。
这种选择性绘制技术特别适合以下场景:
与传统方法相比,这种技术的主要优势是:
通过合理使用这种技术,可以在保持良好视觉效果的同时显著提升大型场景的渲染性能。