学习threejs,使用TSL计算粒子鼠标特效

发布于:2025-06-12 ⋅ 阅读:(23) ⋅ 点赞:(0)

👨‍⚕️ 主页: gis分享者
👨‍⚕️ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅!
👨‍⚕️ 收录于专栏:threejs gis工程师



一、🍀前言

本文详细介绍如何基于threejs在三维场景中使用TSL计算粒子鼠标特效,亲测可用。希望能帮助到您。一起学习,加油!加油!

1.1 ☘️Three.js Shading Language (TSL)

Three.js宣布引入了一种新的着色语言,能够生成GLSL和WGSL代码。

  • TSL 抹平了 GLSL 和 WGSL 着色器语言的差异。
  • 通过 GLSLNodeBuilder 编译成适用于 WebGL 2 的 GLSL
  • 通过 WGSLNodeBuilder 编译成适用于 WebGPU 的 WGSL
  • GLSLNodeBuilder 和 WGSLNodeBuilder 都继承于 NodeBuilder,你甚至可以基于
    NodeBuilder 扩展到任何着色器编程语言。

1.1.1 ☘️背景

3D图形在Web上正经历一场革命,从WebGL过渡到更强大的WebGPU。
WebGPU利用最新的GPU技术,提供更好的性能。

1.1.2 ☘️着色语言的转变

WebGL使用GLSL编写着色器,而WebGPU需要使用WGSL。
两种语言相似,静态类型,与C语言紧密相关,专注于3D图形的复杂向量计算。

1.1.3 ☘️特点

TSL采用了基于节点的方法,类似于Unreal Engine的Blueprints、Blender和Unity的Shader Graph。
这种方法通过将着色器分解为一系列节点来促进着色器开发,每个节点应用特定效果,可以组合生成最终着色器。

1.1.4 ☘️优势

TSL的节点本质上是函数,可以被使用、组合和链接以生成最终着色器。
TSL自动处理适应不同API的适配,无论是WebGL的GLSL还是WebGPU的WGSL。

二、🍀使用TSL计算粒子鼠标特效

1. ☘️实现思路

通过THREE.Raycaster射线拾取,以及TSL计算粒子,实现鼠标移动粒子跳动特效。具体代码参考代码样例。可以直接运行。

2. ☘️代码样例

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>tsl计算</title>
</head>
<style>
    body {
        background-color: black;
        margin: 0;
    }
</style>
<body>

<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>
<script type="module">
  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();

  }
</script>
</body>
</html>

效果如下
在这里插入图片描述
参考:Three.js TSL 计算粒子


网站公告

今日签到

点亮在社区的每一天
去签到