GPGPU particles using texture caches for position and velocity.
For more of these experiments, please see: https://codepen.io/collection/XEEaEa/
GPGPU particles using texture caches for position and velocity.
For more of these experiments, please see: https://codepen.io/collection/XEEaEa/
| <script id="vertexShaderParticle" type="x-shader/x-vertex"> | |
| uniform vec2 u_resolution; | |
| uniform vec2 u_mouse; | |
| uniform float u_time; | |
| uniform sampler2D u_noise; | |
| attribute vec2 reference; | |
| uniform sampler2D texturePosition; | |
| uniform bool u_clicked; | |
| varying float v_op; | |
| float random(vec2 st) { | |
| return fract(sin(dot(st, | |
| vec2(12.9898,78.233)))* | |
| 43758.5453123); | |
| } | |
| void main() { | |
| vec3 position = texture2D(texturePosition, reference).xyz; | |
| position *= 3.; | |
| // position -= 10.; | |
| vec3 transformed = vec3( position ); | |
| vec4 mvpos = modelViewMatrix * vec4( transformed, 1.0 ); | |
| // gl_PointSize = 30.0 * (1.0 / (mvpos.z * mvpos.z)); | |
| // gl_PointSize = 1.; | |
| // gl_PointSize = clamp(2. - length(transformed) * .01, 0., 2.); | |
| gl_PointSize = random(reference) * 50. * (1. / length(mvpos.xyz) * 5.51); | |
| v_op = 1. / length(position) * 8.; | |
| // gl_PointSize = 2.; | |
| gl_Position = projectionMatrix * mvpos; | |
| } | |
| </script> | |
| <script id="fragmentShaderParticle" type="x-shader/x-fragment"> | |
| uniform vec2 u_resolution; | |
| uniform vec2 u_mouse; | |
| uniform float u_time; | |
| uniform sampler2D u_noise; | |
| uniform bool u_clicked; | |
| varying float v_op; | |
| vec2 hash2(vec2 p) | |
| { | |
| vec2 o = texture2D( u_noise, (p+0.5)/256.0, -100.0 ).xy; | |
| return o; | |
| } | |
| void main() { | |
| // vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution.xy) / min(u_resolution.x, u_resolution.y); | |
| vec2 uv = gl_PointCoord.xy - .5; | |
| vec3 particlecolour = vec3(.5, .53, .53) * 1.8; | |
| vec3 outercolour = vec3(1.); | |
| if(u_clicked) { | |
| particlecolour = vec3(.05, .15, .2) * .5; | |
| outercolour = vec3(0.); | |
| } | |
| float l = length(uv); | |
| vec3 colour = mix(outercolour, particlecolour, smoothstep(.5, -.1, l)); | |
| colour = mix(vec3(2., 0.5, 0.), colour, smoothstep(3., 0.5, v_op)); | |
| gl_FragColor = vec4(colour, 1. - l * 2.); | |
| } | |
| </script> | |
| <script id="fragmentShaderVelocity" type="x-shader/x-fragment"> | |
| uniform vec2 u_resolution; | |
| uniform vec2 u_mouse; | |
| uniform float u_time; | |
| uniform float u_mousex; | |
| varying float v_op; | |
| // otaviogood's noise from https://www.shadertoy.com/view/ld2SzK | |
| const float nudge = 0.739513; // size of perpendicular vector | |
| float normalizer = 1.0 / sqrt(1.0 + nudge*nudge); // pythagorean theorem on that perpendicular to maintain scale | |
| float SpiralNoiseC(vec3 p) | |
| { | |
| float n = 0.0; // noise amount | |
| float iter = 1.0; | |
| for (int i = 0; i < 8; i++) | |
| { | |
| // add sin and cos scaled inverse with the frequency | |
| n += -abs(sin(p.y*iter) + cos(p.x*iter)) / iter; // abs for a ridged look | |
| // rotate by adding perpendicular and scaling down | |
| p.xy += vec2(p.y, -p.x) * nudge; | |
| p.xy *= normalizer; | |
| // rotate on other axis | |
| p.xz += vec2(p.z, -p.x) * nudge; | |
| p.xz *= normalizer; | |
| // increase the frequency | |
| iter *= 1.733733; | |
| } | |
| return n; | |
| } | |
| void main() { | |
| vec2 uv = gl_FragCoord.xy / resolution.xy; | |
| vec3 position = texture2D(v_samplerPosition, uv).xyz; | |
| vec3 velocity = texture2D(v_samplerVelocity, uv).xyz; | |
| vec3 acceleration = vec3(0.); | |
| float l = length(position); | |
| vec3 spherical = vec3(1./l, atan(position.y, position.x), acos(position.z / l)); | |
| float n = SpiralNoiseC(spherical * 6. + u_time); | |
| n = SpiralNoiseC(vec3(l, spherical.y * 6. + u_time * 5., spherical.z)); | |
| // n = fract(n) * 3.; | |
| // spherical *= 1. + n; | |
| spherical.z += (1. / n-.5)*length(velocity); | |
| spherical.y += n; | |
| float a = n * .1 + smoothstep(5., 40., l) * 20.; | |
| a += smoothstep(20., 0., l) * .3; | |
| a -= smoothstep(30., 41., l) * 21.; | |
| // spherical.x +=; | |
| // spherical.x += smoothstep(20., 0., l) * .3; | |
| // spherical.x -= smoothstep(30., 41., l) * 21.; | |
| // spherical.xy += n; | |
| // spherical.z *= 1.5; | |
| // spherical.z += 1.; | |
| // spherical.x -= smoothstep(5., 1., l) * 1.; | |
| // spherical.yz += n*.5; | |
| acceleration.x = spherical.x * sin(spherical.z) * cos(spherical.y) * a; | |
| acceleration.y = spherical.x * sin(spherical.z) * sin(spherical.y) * a; | |
| acceleration.z = spherical.x * cos(spherical.z) * a; | |
| // if(acceleration.x == 0) { acceleration.x = .01 }; | |
| // acceleration *= acceleration * acceleration * 200.; | |
| // acceleration = sin(acceleration) * .5 + .5; | |
| // acceleration *= 100.; | |
| vec3 vel = velocity * .98 + acceleration * .3; | |
| if(length(vel) > 5.) { | |
| vel = normalize(vel) * 5.; | |
| } | |
| gl_FragColor = vec4(vel, 1.0); | |
| // gl_FragColor = vec4(-.1); | |
| } | |
| </script> | |
| <script id="fragmentShaderPosition" type="x-shader/x-fragment"> | |
| uniform float delta; | |
| uniform float u_time; | |
| uniform sampler2D v_samplerPosition_orig; | |
| uniform sampler2D u_noise; | |
| vec3 hash3(vec2 p) | |
| { | |
| vec3 o = texture2D( u_noise, (p+0.5)/256.0, -100.0 ).xyz; | |
| return o; | |
| } | |
| void main() { | |
| vec2 uv = gl_FragCoord.xy / resolution.xy; | |
| vec3 position_original = texture2D(v_samplerPosition_orig, uv).xyz; | |
| vec3 position = texture2D(v_samplerPosition, uv).xyz; | |
| vec3 velocity = texture2D(v_samplerVelocity, uv).xyz; | |
| // velocity -= .5; | |
| // velocity *= 3.; | |
| // velocity = velocity * 2. - 1.; | |
| vec3 pos = position + velocity * delta; | |
| // This just adds a little touch more randomness to the motion. | |
| // This is incredibly subtle but has the effect of making the particles | |
| // look more "separate" in motion | |
| vec3 hash = hash3(position_original.xy * position_original.zx * 20.); | |
| // pos *= 1. + (hash - .5) * .0005; | |
| // pos += (hash - .5) * .001; | |
| // vec2 p = vec2(atan(pos.y, pos.x), length(pos.xy)); | |
| // p.x -= velocity.x * .001 + .0001; | |
| // pos.x = cos(p.x) * p.y; | |
| // pos.y = sin(p.x) * p.y; | |
| // pos.z += .005; | |
| if(length(pos) > 40.) { | |
| pos = position_original; | |
| } | |
| gl_FragColor = vec4(pos, 1.0); | |
| } | |
| </script> | |
| <div id="container" touch-action="none"></div> |
| const texturesize = 1024; | |
| const particles = texturesize * texturesize; | |
| const GPUComputationRenderer = THREE.GPUComputationRenderer; | |
| let container; | |
| let camera, scene, renderer, controls; | |
| let cloud_obj; | |
| let uniforms; | |
| let gpuComputationRenderer, dataPos, dataVel, textureArraySize = texturesize*texturesize*4.; | |
| let textureVelocity, texturePosition; | |
| const particleVert = document.getElementById( 'vertexShaderParticle' ).textContent; | |
| const particleFrag = document.getElementById( 'fragmentShaderParticle' ).textContent; | |
| const velocityFrag = document.getElementById( 'fragmentShaderVelocity' ).textContent; | |
| const positionFrag = document.getElementById( 'fragmentShaderPosition' ).textContent; | |
| let loader=new THREE.TextureLoader(); | |
| let texture; | |
| loader.setCrossOrigin("anonymous"); | |
| loader.load( | |
| 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/982762/noise.png', | |
| function do_something_with_texture(tex) { | |
| texture = tex; | |
| texture.wrapS = THREE.RepeatWrapping; | |
| texture.wrapT = THREE.RepeatWrapping; | |
| texture.minFilter = THREE.LinearFilter; | |
| init(); | |
| animate(); | |
| } | |
| ); | |
| function init() { | |
| container = document.getElementById( 'container' ); | |
| camera = new THREE.PerspectiveCamera(65, 1, 0.001, Math.pow(2, 16)); | |
| camera.position.x = 0; | |
| camera.position.y = 0; | |
| camera.position.z = 50.; | |
| scene = new THREE.Scene(); | |
| scene.background = new THREE.Color( 0xffffff ); | |
| // create out particles | |
| // ---------------------------- | |
| let vertices = new Float32Array(particles * 3).fill(0); | |
| let references = new Float32Array(particles * 2); | |
| for (let i = 0; i < references.length; i += 2) { | |
| let index = i / 2; | |
| references[i] = (index % texturesize) / texturesize; | |
| references[i + 1] = Math.floor(index / texturesize) / texturesize; | |
| } | |
| let geometry = new THREE.BufferGeometry(); | |
| geometry.addAttribute('position', new THREE.BufferAttribute(vertices, 3)); | |
| geometry.addAttribute('reference', new THREE.BufferAttribute(references, 2)); | |
| // Create our particle material | |
| // ---------------------------- | |
| uniforms = { | |
| u_time: { type: "f", value: 1.0 }, | |
| u_resolution: { type: "v2", value: new THREE.Vector2() }, | |
| u_noise: { type: "t", value: texture }, | |
| u_mouse: { type: "v2", value: new THREE.Vector2() }, | |
| u_texturePosition: { value: null }, | |
| u_clicked: { type: 'b', value: true } | |
| }; | |
| let particleMaterial = new THREE.ShaderMaterial( { | |
| uniforms: uniforms, | |
| vertexShader: particleVert, | |
| fragmentShader: particleFrag, | |
| side: THREE.DoubleSide, | |
| transparent: true | |
| } ); | |
| particleMaterial.transparent = true; | |
| particleMaterial.blending = THREE.MultiplyBlending; | |
| particleMaterial.depthTest = false; | |
| particleMaterial.extensions.derivatives = true; | |
| // Create the particle cloud object | |
| // ---------------------------- | |
| cloud_obj = new THREE.Points(geometry, particleMaterial); | |
| scene.background = new THREE.Color( 0x111111 ); | |
| cloud_obj.material.blending = THREE.AdditiveBlending; | |
| // scene.background = new THREE.Color( 0xFFFFFF ); | |
| // cloud_obj.material.blending = THREE.SubtractiveBlending; | |
| // Create the renderer and controls and add them to the scene | |
| // ---------------------------- | |
| renderer = new THREE.WebGLRenderer(); | |
| renderer.setPixelRatio( 1 ); | |
| controls = new THREE.OrbitControls(camera, renderer.domElement); | |
| window.controls = controls; | |
| container.appendChild( renderer.domElement ); | |
| // Finally, add everything to stage | |
| // ---------------------------- | |
| scene.add( cloud_obj ); | |
| // Add the computational renderer and populate it with data | |
| // ---------------------------- | |
| gpuComputationRenderer = new GPUComputationRenderer(texturesize, texturesize, renderer); | |
| dataPos_orig = gpuComputationRenderer.createTexture(); | |
| dataPos = gpuComputationRenderer.createTexture(); | |
| dataVel = gpuComputationRenderer.createTexture(); | |
| for (let i = 0; i < textureArraySize; i += 4) { | |
| let radius = 2.; | |
| let phi = Math.random() * Math.PI * 2.; | |
| let costheta = Math.random() * 2. - 1.; | |
| let u = Math.random(); | |
| let theta = Math.acos( costheta ); | |
| let r = radius * Math.cbrt( u ); | |
| let x = r * Math.sin( theta) * Math.cos( phi ); | |
| let y = r * Math.sin( theta) * Math.sin( phi ); | |
| let z = r * Math.cos( theta ); | |
| dataPos.image.data[i] = x; | |
| dataPos.image.data[i + 1] = y; | |
| dataPos.image.data[i + 2] = z; | |
| dataPos.image.data[i + 3] = 1; | |
| dataPos_orig.image.data[i] = x; | |
| dataPos_orig.image.data[i + 1] = y; | |
| dataPos_orig.image.data[i + 2] = z; | |
| dataPos_orig.image.data[i + 3] = 1; | |
| dataVel.image.data[i] = x * 3.; | |
| dataVel.image.data[i + 1] = y * 3.; | |
| dataVel.image.data[i + 2] = z * 3.; | |
| dataVel.image.data[i + 3] = 1; | |
| } | |
| textureVelocity = gpuComputationRenderer.addVariable('v_samplerVelocity', velocityFrag, dataVel); | |
| texturePosition = gpuComputationRenderer.addVariable('v_samplerPosition', positionFrag, dataPos); | |
| texturePosition.material.uniforms.delta = { value: 0 }; | |
| texturePosition.material.uniforms.v_samplerPosition_orig = { type: "t", value: dataPos_orig }; | |
| textureVelocity.material.uniforms.u_time = { value: -1000 }; | |
| textureVelocity.material.uniforms.u_mousex = { value: 0 }; | |
| texturePosition.material.uniforms.u_time = { value: 0 }; | |
| gpuComputationRenderer | |
| .setVariableDependencies(textureVelocity, [ textureVelocity, texturePosition ]); | |
| gpuComputationRenderer | |
| .setVariableDependencies(texturePosition, [ textureVelocity, texturePosition ]); | |
| texturePosition.wrapS = THREE.RepeatWrapping; | |
| texturePosition.wrapT = THREE.RepeatWrapping; | |
| textureVelocity.wrapS = THREE.RepeatWrapping; | |
| textureVelocity.wrapT = THREE.RepeatWrapping; | |
| const gpuComputationRendererError = gpuComputationRenderer.init(); | |
| if (gpuComputationRendererError) { | |
| console.error('ERROR', gpuComputationRendererError); | |
| } | |
| // Add event listeners for resize and mouse move | |
| // ---------------------------- | |
| onWindowResize(); | |
| window.addEventListener( 'resize', onWindowResize, false ); | |
| document.addEventListener('pointermove', pointerMove); | |
| // document.addEventListener('click', onClick); | |
| // initialise the video renderer | |
| } | |
| function onWindowResize( event ) { | |
| let w = window.innerWidth; | |
| let h = window.innerHeight; | |
| camera.aspect = w / h; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize( w, h ); | |
| uniforms.u_resolution.value.x = renderer.domElement.width; | |
| uniforms.u_resolution.value.y = renderer.domElement.height; | |
| } | |
| function pointerMove( event ) { | |
| let ratio = window.innerHeight / window.innerWidth; | |
| textureVelocity.material.uniforms.u_mousex.value = event.pageX; | |
| uniforms.u_mouse.value.x = (event.pageX - window.innerWidth / 2) / window.innerWidth / ratio; | |
| uniforms.u_mouse.value.y = (event.pageY - window.innerHeight / 2) / window.innerHeight * -1; | |
| event.preventDefault(); | |
| } | |
| function onClick() { | |
| // return; | |
| let newval = !uniforms.u_clicked.value; | |
| uniforms.u_clicked.value = newval; | |
| console.log(cloud_obj.material.blending); | |
| if(newval === false) { | |
| scene.background = new THREE.Color( 0xffffff ); | |
| cloud_obj.material.blending = THREE.MultiplyBlending; | |
| } else { | |
| scene.background = new THREE.Color( 0x000000 ); | |
| cloud_obj.material.blending = THREE.AdditiveBlending; | |
| } | |
| } | |
| function animate(delta) { | |
| requestAnimationFrame( animate ); | |
| render(delta); | |
| } | |
| let capturer = new CCapture( { | |
| verbose: true, | |
| framerate: 60, | |
| // motionBlurFrames: 4, | |
| quality: 90, | |
| format: 'webm', | |
| workersPath: 'js/' | |
| } ); | |
| let capturing = false; | |
| isCapturing = function(val) { | |
| if(val === false && window.capturing === true) { | |
| capturer.stop(); | |
| capturer.save(); | |
| renderer.setPixelRatio( window.devicePixelRatio ); | |
| } else if(val === true && window.capturing === false) { | |
| capturer.start(); | |
| controls.enabled = false; | |
| renderer.setPixelRatio( 1 ); | |
| } | |
| capturing = val; | |
| } | |
| toggleCapture = function() { | |
| isCapturing(!capturing); | |
| } | |
| window.addEventListener('keyup', function(e) { if(e.keyCode == 68) toggleCapture(); }); | |
| let then = 0; | |
| function render(delta) { | |
| let now = Date.now() / 1000; | |
| let _delta = now - then; | |
| then = now; | |
| gpuComputationRenderer.compute(); | |
| texturePosition.material.uniforms.delta.value = Math.min(_delta, 0.5); | |
| textureVelocity.material.uniforms.u_time.value += .0005; | |
| texturePosition.material.uniforms.u_time.value += _delta; | |
| uniforms.u_time.value += _delta; | |
| uniforms.u_texturePosition.value = gpuComputationRenderer.getCurrentRenderTarget(texturePosition).texture; | |
| window.pos = gpuComputationRenderer.getCurrentRenderTarget(texturePosition); | |
| renderer.render( scene, camera ); | |
| if(capturing) { | |
| capturer.capture( renderer.domElement ); | |
| } | |
| } |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/107/three.min.js"></script> | |
| <script src="https://codepen.io/shubniggurath/pen/2294630344abf93923fbffbeb7916689.js"></script> | |
| <script src="https://codepen.io/shubniggurath/pen/61f7965c363fe2b4f112d0aa48494e31.js"></script> | |
| <script src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/982762/ccapture.js"></script> |
| body { | |
| margin: 0; | |
| padding: 0; | |
| } | |
| #container { | |
| position: fixed; | |
| touch-action: none; | |
| } |