“使用 Three.js 的 WebGL 小实验。计算粒子鼠标特效。”

HTML:
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.176.0/build/three.webgpu.js",
"three/webgpu": "https://cdn.jsdelivr.net/npm/three@0.176.0/build/three.webgpu.js",
"three/tsl": "https://cdn.jsdelivr.net/npm/three@0.176.0/build/three.tsl.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.176.0/examples/jsm/"
}
}
</script>
body {
background-color: black;
margin: 0;
}
JAVASCRIPT:
import * as THREE from 'three';
import { Fn, If, uniform, float, uv, vec2, vec3, hash, instancedArray, instanceIndex, viewportSize } from 'three/tsl';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import Stats from 'three/addons/libs/stats.module.js';
const particleCount = 500_000;
const gravity = uniform( - .00098 );
const bounce = uniform( .8 );
const friction = uniform( .99 );
const size = uniform( .12 );
const clickPosition = uniform( new THREE.Vector3() );
let camera, scene, renderer;
let controls, stats;
let computeParticles;
let isOrbitControlsActive;
init();
function init() {
const { innerWidth, innerHeight } = window;
camera = new THREE.PerspectiveCamera( 50, innerWidth / innerHeight, .1, 1000 );
camera.position.set( 0, 10, 30 );
scene = new THREE.Scene();
//
const positions = instancedArray( particleCount, 'vec3' );
const velocities = instancedArray( particleCount, 'vec3' );
const colors = instancedArray( particleCount, 'vec3' );
// compute
const separation = 0.2;
const amount = Math.sqrt( particleCount );
const offset = float( amount / 2 );
const computeInit = Fn( () => {
const position = positions.element( instanceIndex );
const color = colors.element( instanceIndex );
const x = instanceIndex.mod( amount );
const z = instanceIndex.div( amount );
position.x = offset.sub( x ).mul( separation );
position.z = offset.sub( z ).mul( separation );
const randX = hash( instanceIndex );
const randY = hash( instanceIndex.add( 2 ) );
const randZ = hash( instanceIndex.add( 3 ) );
color.assign( vec3( randX, randY.mul( 0.5 ), randZ ) );
} )().compute( particleCount );
//
const computeUpdate = Fn( () => {
const position = positions.element( instanceIndex );
const velocity = velocities.element( instanceIndex );
velocity.addAssign( vec3( 0.00, gravity, 0.00 ) );
position.addAssign( velocity );
velocity.mulAssign( friction );
// floor
If( position.y.lessThan( 0 ), () => {
position.y = 0;
velocity.y = velocity.y.negate().mul( bounce );
// floor friction
velocity.x = velocity.x.mul( .9 );
velocity.z = velocity.z.mul( .9 );
} );
} );
computeParticles = computeUpdate().compute( particleCount );
// create particles
const material = new THREE.SpriteNodeMaterial();
material.colorNode = uv().mul( colors.element( instanceIndex ) );
material.positionNode = positions.toAttribute();
material.scaleNode = size;
material.alphaTestNode = uv().mul( 2 ).distance( vec2( 1 ) );
material.alphaToCoverage = true;
material.transparent = false;
const particles = new THREE.Sprite( material );
particles.count = particleCount;
particles.frustumCulled = false;
scene.add( particles );
//
const helper = new THREE.GridHelper( 142, 71, 0x303030, 0x303030 );
scene.add( helper );
const geometry = new THREE.PlaneGeometry( 1000, 1000 );
geometry.rotateX( - Math.PI / 2 );
const plane = new THREE.Mesh( geometry, new THREE.MeshBasicMaterial( { visible: false } ) );
scene.add( plane );
const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();
//
renderer = new THREE.WebGPURenderer( { antialias: false } );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.setAnimationLoop( animate );
document.body.appendChild( renderer.domElement );
stats = new Stats();
document.body.appendChild( stats.dom );
//
renderer.computeAsync( computeInit );
// click event
const computeHit = Fn( () => {
const position = positions.element( instanceIndex );
const velocity = velocities.element( instanceIndex );
const dist = position.distance( clickPosition );
const direction = position.sub( clickPosition ).normalize();
const distArea = float( 3 ).sub( dist ).max( 0 );
const power = distArea.mul( .01 );
const relativePower = power.mul( hash( instanceIndex ).mul( 1.5 ).add( .5 ) );
velocity.assign( velocity.add( direction.mul( relativePower ) ) );
} )().compute( particleCount );
//
function onMove( event ) {
if ( isOrbitControlsActive ) return;
pointer.set( ( event.clientX / window.innerWidth ) * 2 - 1, - ( event.clientY / window.innerHeight ) * 2 + 1 );
raycaster.setFromCamera( pointer, camera );
const intersects = raycaster.intersectObjects( [ plane ], false );
if ( intersects.length > 0 ) {
const { point } = intersects[ 0 ];
// move to uniform
clickPosition.value.copy( point );
clickPosition.value.y = - 1;
// compute
renderer.computeAsync( computeHit );
}
}
// events
renderer.domElement.addEventListener( 'pointermove', onMove );
//
controls = new OrbitControls( camera, renderer.domElement );
controls.enableDamping = true;
controls.minDistance = 5;
controls.maxDistance = 200;
controls.target.set( 0, -8, 0 );
controls.update();
controls.addEventListener( 'start', function () {
isOrbitControlsActive = true;
} );
controls.addEventListener( 'end', function () {
isOrbitControlsActive = false;
} );
controls.touches = {
ONE: null,
TWO: THREE.TOUCH.DOLLY_PAN
};
//
window.addEventListener( 'resize', onWindowResize );
}
function onWindowResize() {
const { innerWidth, innerHeight } = window;
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( innerWidth, innerHeight );
}
async function animate() {
controls.update();
await renderer.computeAsync( computeParticles );
await renderer.renderAsync( scene, camera );
stats.update();
}
源码:
https://codepen.io/mrdoob_/pen/myyQLPL
体验:
https://codepen.io/mrdoob_/full/myyQLPL