👨⚕️ 主页: gis分享者
👨⚕️ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅!
文章目录
- 一、🍀前言
-
- [1.1 ☘️Three.js Shading Language (TSL)](#1.1 ☘️Three.js Shading Language (TSL))
-
- [1.1.1 ☘️背景](#1.1.1 ☘️背景)
- [1.1.2 ☘️着色语言的转变](#1.1.2 ☘️着色语言的转变)
- [1.1.3 ☘️特点](#1.1.3 ☘️特点)
- [1.1.4 ☘️优势](#1.1.4 ☘️优势)
- 二、🍀使用TSL计算粒子鼠标特效
-
- [1. ☘️实现思路](#1. ☘️实现思路)
- [2. ☘️代码样例](#2. ☘️代码样例)
一、🍀前言
本文详细介绍如何基于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. ☘️代码样例
html
<!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/[email protected]/build/three.webgpu.js",
"three/webgpu": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.webgpu.js",
"three/tsl": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.tsl.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/[email protected]/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>
效果如下