R3F 制作的3D数字人流体粒子化组件FlowFieldParticles

职场   2024-11-12 17:09   北京  

今天又发现一个很好用的3D模型流体粒子化组件FlowFieldParticles,所谓流体化,就是当鼠标放上去的时候可以像水一样流动。然后将自己的数字人放进去试试效果。

特点:
  • 可定制的粒子形状(圆盘、环、球体、正方形)。

  • 动态粒子颜色和大小控制。

  • 通过鼠标移动实时交互来实现粒子排斥。

  • 支持光源以实现真实的照明和阴影。

  • 能够使用外部网格作为粒子的基本模型。



源码:

import React, { useCallback, useEffect, useRef, useMemo, memo, useState, forwardRef } from "react";import { useFrame, useThree } from "@react-three/fiber";import { BufferGeometry, BufferAttribute, Color, Uniform, Vector3, MathUtils } from "three";import { GPUComputationRenderer } from "three/addons/misc/GPUComputationRenderer.js";
const GpgpuFragmentShader = /*glsl*/ ` uniform float uTime; uniform float uDeltaTime; uniform vec3 uMouse; uniform float uMouseDelta; uniform float uDisturbIntensity; uniform bool uInteractive; uniform sampler2D uBaseParticlesTexture; vec4 permute(vec4 x){return mod(((x*34.0)+1.0)*x, 289.0);} float permute(float x){return floor(mod(((x*34.0)+1.0)*x, 289.0));} vec4 taylorInvSqrt(vec4 r){return 1.79284291400159 - 0.85373472095314 * r;} float taylorInvSqrt(float r){return 1.79284291400159 - 0.85373472095314 * r;} vec4 grad4(float j, vec4 ip){ const vec4 ones = vec4(1.0, 1.0, 1.0, -1.0); vec4 p,s; p.xyz = floor( fract (vec3(j) * ip.xyz) * 7.0) * ip.z - 1.0; p.w = 1.5 - dot(abs(p.xyz), ones.xyz); s = vec4(lessThan(p, vec4(0.0))); p.xyz = p.xyz + (s.xyz*2.0 - 1.0) * s.www; return p; } float snoise(vec4 v){ const vec2 C = vec2( 0.138196601125010504, 0.309016994374947451); vec4 i = floor(v + dot(v, C.yyyy) ); vec4 x0 = v - i + dot(i, C.xxxx); vec4 i0; vec3 isX = step( x0.yzw, x0.xxx ); vec3 isYZ = step( x0.zww, x0.yyz ); i0.x = isX.x + isX.y + isX.z; i0.yzw = 1.0 - isX; i0.y += isYZ.x + isYZ.y; i0.zw += 1.0 - isYZ.xy; i0.z += isYZ.z; i0.w += 1.0 - isYZ.z; vec4 i3 = clamp( i0, 0.0, 1.0 ); vec4 i2 = clamp( i0-1.0, 0.0, 1.0 ); vec4 i1 = clamp( i0-2.0, 0.0, 1.0 ); vec4 x1 = x0 - i1 + 1.0 * C.xxxx; vec4 x2 = x0 - i2 + 2.0 * C.xxxx; vec4 x3 = x0 - i3 + 3.0 * C.xxxx; vec4 x4 = x0 - 1.0 + 4.0 * C.xxxx; i = mod(i, 289.0); float j0 = permute( permute( permute( permute(i.w) + i.z) + i.y) + i.x); vec4 j1 = permute( permute( permute( permute ( i.w + vec4(i1.w, i2.w, i3.w, 1.0 )) + i.z + vec4(i1.z, i2.z, i3.z, 1.0 )) + i.y + vec4(i1.y, i2.y, i3.y, 1.0 )) + i.x + vec4(i1.x, i2.x, i3.x, 1.0 )); vec4 ip = vec4(1.0/294.0, 1.0/49.0, 1.0/7.0, 0.0) ; vec4 p0 = grad4(j0, ip); vec4 p1 = grad4(j1.x, ip); vec4 p2 = grad4(j1.y, ip); vec4 p3 = grad4(j1.z, ip); vec4 p4 = grad4(j1.w, ip); vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3))); p0 *= norm.x; p1 *= norm.y; p2 *= norm.z; p3 *= norm.w; p4 *= taylorInvSqrt(dot(p4,p4)); vec3 m0 = max(0.6 - vec3(dot(x0,x0), dot(x1,x1), dot(x2,x2)), 0.0); vec2 m1 = max(0.6 - vec2(dot(x3,x3), dot(x4,x4)), 0.0); m0 = m0 * m0; m1 = m1 * m1; return 49.0 * ( dot(m0*m0, vec3( dot( p0, x0 ), dot( p1, x1 ), dot( p2, x2 ))) + dot(m1*m1, vec2( dot( p3, x3 ), dot( p4, x4 ) ) ) ) ;} void main() { // resolution + uParticles are given by the GPUComputationRenderer vec2 uv = gl_FragCoord.xy / resolution.xy; vec4 particle = texture2D(uParticles, uv); vec4 baseParticle = texture2D(uBaseParticlesTexture, uv);
float uRepelStrength = uMouseDelta; uRepelStrength = mix(uRepelStrength, 0.0, uMouseDelta); vec2 particlePos = particle.xy; vec2 mousePos = uMouse.xy; float dist = distance(mousePos, particlePos); vec2 dir = normalize(particlePos - mousePos); float repulsionForce = uRepelStrength / (dist * (dist + 1.0)); vec2 repulsion = dir * repulsionForce;
if(uInteractive){ particle.xy += repulsion * uRepelStrength; }
if (particle.a >= 1.0) { particle.a = mod(particle.a, 1.0); particle.xyz = baseParticle.xyz; } else { float disturbIntensity = (uDisturbIntensity > 0.0) ? pow(uDisturbIntensity, 4.0) : 0.0; float timer = uDeltaTime * disturbIntensity; vec3 flowField = vec3( snoise(vec4(particle.xyz + disturbIntensity, timer )), snoise(vec4(particle.yxz + disturbIntensity, timer )), snoise(vec4(particle.zxy + disturbIntensity, timer )) ); flowField = normalize(flowField); if(disturbIntensity > 0.0){ particle.xyz += flowField * disturbIntensity * uDeltaTime * particle.a; particle.a += uDeltaTime; } else { particle.a += uDeltaTime; } } gl_FragColor.rgba = particle;}
`;const ParticlesVertexShader = /*glsl*/ ` uniform float uSize; uniform vec2 uMouse; uniform vec3 uColors[2]; uniform sampler2D uParticlesTexture; uniform vec2 uResolution; attribute vec2 aParticlesUv; attribute vec2 aMeshUv; attribute float aParticlesSize; attribute vec3 aParticlesColor; attribute vec3 aNormal; varying vec3 vColor[2]; varying vec3 vPosition; varying float vParticlesAlpha; varying vec2 vMeshUv; varying vec3 vNormal;
void main() { vec4 particle = texture2D(uParticlesTexture, aParticlesUv); vec4 modelPosition = modelMatrix * vec4(particle.xyz, 1.0); vec4 viewPosition = viewMatrix * modelPosition; vec4 projectedPosition = projectionMatrix * viewPosition; gl_Position = projectedPosition;
/* Point Size */ float lifeSize = 1.0-smoothstep(0.5, 1.0, particle.a); gl_PointSize = aParticlesSize * lifeSize * uSize * uResolution.y; gl_PointSize *= (1.0 / - viewPosition.z);
/* Variables to the fragment Shader */ vColor = uColors; vPosition = position.xyz; vParticlesAlpha = particle.a; vMeshUv = aMeshUv; vNormal = aNormal; }`;const ParticlesFragmentShader = /*glsl*/ ` uniform float uTime; uniform sampler2D uMeshMap; uniform int uShape; // 1: disc | 2: ring | 3: sphere | 4: square uniform bool uHasLightSource; uniform vec3 uLightSource; uniform vec3 uLightSourceColor; uniform float uLightSourceIntensity; uniform bool uHasColors; varying vec3 vColor[2]; varying float vParticlesAlpha; varying vec2 vMeshUv; varying vec3 vPosition; varying vec3 vNormal;
void main() { vec2 uv = gl_PointCoord.xy; vec3 diffuseMap = texture2D(uMeshMap, vMeshUv).rgb; float circle = 1.0; if (uShape == 1) { circle = 1.0-length(uv - vec2(0.5)); circle = smoothstep(0.5, 0.51, circle); } if (uShape == 2) { circle = length(uv - vec2(0.5)); circle = smoothstep(0.4, 0.4+0.1, circle); circle -= smoothstep(0.6, 0.6+0.1, circle); } if (uShape == 3) { circle = length(uv - vec2(0.5)); circle = smoothstep(0.5, 0.0, circle); } if(circle < 0.01) discard;
/* Lighting */ float light = 1.0; float lightIntensity = 1.0; float specular = 0.0; vec3 lightColor = vec3(1.0); if(uHasLightSource) { vec3 lightDir = normalize(uLightSource - vPosition); vec3 normal = normalize(vNormal); vec3 reflectDir = reflect(-lightDir, normal); vec3 viewDir = normalize(cameraPosition - vPosition); light = max(dot(normal, lightDir), 0.08); lightIntensity = 0.5 + uLightSourceIntensity; specular = pow(max(dot(viewDir, reflectDir), 0.0), 1.0); specular *= 0.5; lightColor = uLightSourceColor; }
float circleSphere = length(uv - vec2(0.5)); circleSphere = smoothstep(1.0, 0.0, circleSphere); circleSphere = pow(circleSphere, 2.0);
vec3 color = vec3(1.0); if (uHasColors) { float colorType = (uShape == 3) ? circleSphere : smoothstep(0.0, 1.0, 1.0-vNormal.y); color = mix(vColor[0], vColor[1], colorType); } else { color = diffuseMap; } color *= lightColor; color *= (light * lightIntensity) + specular; gl_FragColor.rgba = vec4(color, 1.0); }`;const ParticlesMaterial = memo( forwardRef((props, ref) => { const uniforms = useMemo(() => { return { uTime: new Uniform(0), uSize: new Uniform(0.4), uColors: new Uniform([]), uHasColors: new Uniform(false), uShape: new Uniform(1), uMeshMap: new Uniform(null), uParticlesTexture: new Uniform(null), uResolution: new Uniform([0, 0]), uHasLightSource: new Uniform(false), uLightSource: new Uniform(new Vector3()), uLightSourceColor: new Uniform(new Vector3()), uLightSourceIntensity: new Uniform(1.0), }; }, []); return <shaderMaterial ref={ref} uniforms={uniforms} vertexShader={ParticlesVertexShader} fragmentShader={ParticlesFragmentShader} />; }));const ParticleShapeIntValue = shapeString => { switch (shapeString) { case "disc": return 1; case "ring": return 2; case "sphere": return 3; case "square": return 4; default: return 1; }};const InitMeshWrapper = forwardRef(({ children, visible, onUpdate, onPointerMove }, ref) => { const handleUpdate = e => onUpdate(e); const handlePointerMove = e => onPointerMove(e); const clonedChildren = React.Children.map(children, child => { return React.cloneElement(child, { ref, scale: 0.98, visible, onUpdate: handleUpdate, onPointerMove: handlePointerMove, }); });
return <>{clonedChildren}</>;});const DebugMessage = (mssg, color = "red") => { console.log(`%c ${mssg}`, `color: white; background-color: ${color}; font-size: 12px; padding: 4px; border-radius: 4px;`);};const FlowFieldParticles = ({ debug = false, name = null, interactive = true, childMeshVisible = false, colors = null, size = 0.1, disturbIntensity = 0.3, shape = "disc", lightSource = null, children,}) => { const [initMeshRef, setInitMeshRef] = useState(null); const ref = useRef(null); const meshRef = useRef(null); const particlesRef = useRef(null); const particlesMaterialRef = useRef(null); const helperRef = useRef(null); const mouseRef = useRef(new Vector3()); const mouseDeltaRef = useRef(new Vector3()); const gl = useThree(state => state.gl);
const modelMesh = useMemo(() => { if (!meshRef.current) return; return meshRef.current; }, [initMeshRef]); const modelGeometry = useMemo(() => { if (!modelMesh) return; if (debug) { DebugMessage(`${name} - modelGeometry()`, "green"); } const { geometry, material } = modelMesh; const { attributes } = geometry; const { count } = attributes.position; return { geometry, material, attributes, count }; }, [modelMesh]); const gpgpu = useMemo(() => { if (!modelGeometry) return; if (debug) { DebugMessage(`${name} - gpgpu()`, "green"); } const size = Math.ceil(Math.sqrt(modelGeometry.count)); const GCR = new GPUComputationRenderer(size, size, gl); const dataTexture = GCR.createTexture(); // RGBA DATA Texture
for (let i = 0; i < modelGeometry.count; i++) { dataTexture.image.data[i * 4 + 0] = modelGeometry.attributes.position.array[i * 3 + 0]; dataTexture.image.data[i * 4 + 1] = modelGeometry.attributes.position.array[i * 3 + 1]; dataTexture.image.data[i * 4 + 2] = modelGeometry.attributes.position.array[i * 3 + 2]; dataTexture.image.data[i * 4 + 3] = Math.random() * 2.0 - 1.0; }
const particlesVariable = GCR.addVariable("uParticles", GpgpuFragmentShader, dataTexture); GCR.setVariableDependencies(particlesVariable, [particlesVariable]);
GCR.init(); const renderTarget = GCR.getCurrentRenderTarget(particlesVariable); const renderTargetTexture = renderTarget.texture;
// Uniforms particlesVariable.material.uniforms.uTime = new Uniform(0); particlesVariable.material.uniforms.uDeltaTime = new Uniform(0); particlesVariable.material.uniforms.uBaseParticlesTexture = new Uniform(dataTexture); particlesVariable.material.uniforms.uDisturbIntensity = new Uniform(disturbIntensity); particlesVariable.material.uniforms.uMouse = new Uniform(new Vector3(0, 0, 0)); particlesVariable.material.uniforms.uMouseDelta = new Uniform(0); particlesVariable.material.uniforms.uInteractive = new Uniform(interactive);
return { ref: GCR, texture: renderTargetTexture, particlesVariable, size }; }, [modelGeometry, disturbIntensity]);
const particles = useMemo(() => { if (!modelGeometry) return; if (debug) { DebugMessage(`${name} - particles()`, "green"); } const particlesUvArray = new Float32Array(modelGeometry.count * 2); const particlesSizeArray = new Float32Array(modelGeometry.count); for (let y = 0; y < gpgpu.size; y++) { for (let x = 0; x < gpgpu.size; x++) { const i = y * gpgpu.size + x; const i2 = i * 2; const uvX = (x + 0.5) / gpgpu.size; // (x+0.5) pour centrer le px const uvY = (y + 0.5) / gpgpu.size; // Set UV Position particlesUvArray[i2 + 0] = uvX; particlesUvArray[i2 + 1] = uvY; // Random size particlesSizeArray[i] = Math.random(); } } const geometry = new BufferGeometry(); geometry.setDrawRange(0, modelGeometry.count); geometry.setAttribute("aParticlesUv", new BufferAttribute(particlesUvArray, 2)); geometry.setAttribute("aParticlesSize", new BufferAttribute(particlesSizeArray, 1)); modelGeometry.attributes.color && geometry.setAttribute("aParticlesColor", new BufferAttribute(modelGeometry.attributes.color.array, 3)); modelGeometry.attributes.position && geometry.setAttribute("position", new BufferAttribute(modelGeometry.attributes.position.array, 3)); modelGeometry.attributes.uv && geometry.setAttribute("aMeshUv", new BufferAttribute(modelGeometry.attributes.uv.array, 2)); return { geometry, material: null, uvAttribute: particlesUvArray }; }, [modelGeometry]); const handlePointerMove = useCallback(e => { const { point, object } = e; const { position } = object; if (mouseRef.current) { const { x, y, z } = point.sub(position); mouseRef.current.set(x, y, z); } }, []);
useEffect(() => { if (debug) { DebugMessage(`${name} - useEffect()`, "purple"); } if (gpgpu) { gpgpu.particlesVariable.material.uniforms.uInteractive.value = interactive; } if (particlesRef.current) { particlesRef.current.geometry.setAttribute("aNormal", modelGeometry.attributes.normal); } if (particlesMaterialRef.current) { particlesMaterialRef.current.uniforms.uHasColors.value = true; const colorsArray = colors?.map(color => new Color(color)) || [modelGeometry.material.color, modelGeometry.material.color]; particlesMaterialRef.current.uniforms.uColors.value = colorsArray; particlesMaterialRef.current.uniforms.uSize.value = size;
if (lightSource) { let light; if ("current" in lightSource) { light = lightSource.current; } else if ("position" in lightSource) { light = lightSource; }
if ("position" in light) { particlesMaterialRef.current.uniforms.uHasLightSource.value = true; particlesMaterialRef.current.uniforms.uLightSource.value.copy(light.position); } if ("color" in light) { particlesMaterialRef.current.uniforms.uLightSourceColor = new Uniform(light.color); } if ("intensity" in light) { particlesMaterialRef.current.uniforms.uLightSourceIntensity = new Uniform(light.intensity); } } else { particlesMaterialRef.current.uniforms.uHasLightSource.value = false; } particlesMaterialRef.current.uniforms.uShape.value = ParticleShapeIntValue(shape);
if (modelMesh?.material?.map) { particlesMaterialRef.current.uniforms.uHasColors.value = false; particlesMaterialRef.current.uniforms.uMeshMap.value = modelMesh.material.map; } } }, [colors, size, shape, lightSource, interactive, particles]); let lastMousePosX = 0; let mouseDeltaValue = 0;
useFrame(({ clock }, delta) => { const elapsedTime = clock.getElapsedTime(); mouseDeltaValue = MathUtils.lerp(mouseDeltaValue, Math.abs(lastMousePosX - mouseRef.current.x), 0.1); if (particlesRef.current) { particlesRef.current.position.copy(modelMesh.position); modelMesh.position.copy(modelMesh.position); } if (particlesMaterialRef.current) { mouseDeltaRef.current.sub(mouseRef.current); mouseDeltaRef.current.copy(mouseRef.current); /** Gpgpu computation */ gpgpu.ref.compute(); gpgpu.particlesVariable.material.uniforms.uTime.value = elapsedTime; gpgpu.particlesVariable.material.uniforms.uDeltaTime.value = delta; gpgpu.particlesVariable.material.uniforms.uMouse.value.copy(mouseRef.current); gpgpu.particlesVariable.material.uniforms.uMouseDelta.value = mouseDeltaValue;
/** Particles Material uniforms */ particlesMaterialRef.current.uniforms.uTime.value = elapsedTime; particlesMaterialRef.current.uniforms.uResolution.value = [gpgpu.size, gpgpu.size]; particlesMaterialRef.current.uniforms.uParticlesTexture.value = gpgpu.ref.getCurrentRenderTarget(gpgpu.particlesVariable).texture; lightSource && particlesMaterialRef.current.uniforms.uLightSource.value.copy(lightSource.current.position);
/** Helper */ helperRef.current?.position.copy(mouseRef.current).add(modelMesh.position); } /** Mouse */ lastMousePosX = MathUtils.lerp(lastMousePosX, mouseRef.current.x, 0.5); }); if (debug) { DebugMessage(`${name} - <FlowFieldParticles />`, "blue"); } return ( <group ref={ref}> {particles && ( <points visible={true} ref={particlesRef} geometry={particles?.geometry} position={modelMesh?.position}> <ParticlesMaterial ref={particlesMaterialRef} attach='material' /> </points> )} <InitMeshWrapper ref={meshRef} visible={childMeshVisible} onUpdate={e => { if (e) setInitMeshRef(e); }} onPointerMove={handlePointerMove} > {children} </InitMeshWrapper>
{debug && ( <mesh ref={helperRef} position={[0, 0, 0]} scale={0.5}> <sphereGeometry args={[0.1, 32, 32]} /> <meshBasicMaterial color='red' /> </mesh> )} </group> );};
export { FlowFieldParticles };



代码地址:

https://github.com/sebastien-lempens/r3f-flow-field-particles/tree/main



有相同兴趣爱好的可通过加星球的方式添加作者微信。加入后查看置顶评论可加微信交流。
关于作者

做一只爬的最久的乌龟,保持学习保持好奇,即使慢一点,遇到一点困难,只要最后能到达终点,又有什么关系呢。
毕竟人生没有白走的路,每一步都算数。


前端程序设计
专注前端最前沿技术,数据可视化,web3d。偶尔插播生活和艺术。
 最新文章