Public
Edited
Jan 14, 2023
6 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
class GPGPU {
constructor(renderer, textureSize) {
const gl = renderer.getContext()
// we need FLOAT Textures to store positions
//https://github.com/KhronosGroup/WebGL/blob/master/sdk/tests/conformance/extensions/oes-texture-float.html
if (!gl.getExtension('OES_texture_float')) {
alert('client support error - float textures not supported')
throw new Error('float textures not supported')
}
// we need to access textures from within the vertex shader
//https://github.com/KhronosGroup/WebGL/blob/90ceaac0c4546b1aad634a6a5c4d2dfae9f4d124/conformance-suites/1.0.0/extra/webgl-info.html
if (gl.getParameter(gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS) === 0) {
alert('client support error - vertex shader cannot read textures')
throw new Error('vertex shader cannot read textures')
}
this.renderer = renderer
// GPGPU buffer textures
// A
this.positionsA = new THREE.DataTexture(
preloadedDataBuffer(textureSize),
textureSize, textureSize,
THREE.RGBAFormat, THREE.FloatType)
// B
this.positionsB = new THREE.DataTexture(
null, // will be copied over from A
// preloadedDataBuffer(textureSize),
textureSize, textureSize,
THREE.RGBAFormat, THREE.FloatType)
this.positionsA.needsUpdate = true

// rtt setup
this.scene = new THREE.Scene()
this.orthoCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1)

// create a target texture
const options = {
wrapS: THREE.ClampToEdgeWrapping,
wrapT: THREE.ClampToEdgeWrapping,
stencilBuffer: false,
minFilter: THREE.NearestFilter, // important as we want to sample square pixels
magFilter: THREE.NearestFilter,
format: THREE.RGBAFormat,
type: (/(iPad|iPhone|iPod)/g.test(navigator.userAgent)) ? THREE.HalfFloatType : THREE.FloatType,
}

this.rtts = [
new THREE.WebGLRenderTarget(textureSize, textureSize, options),
new THREE.WebGLRenderTarget(textureSize, textureSize, options)
]
this.rttIndex = 0

// the simulation:
//create a bi-unit quadrilateral and uses the simulation material to update the Float Texture
let geom = new THREE.BufferGeometry()
geom.addAttribute('position', new THREE.BufferAttribute(new Float32Array([
-1, -1, 0,
1, -1, 0,
1, 1, 0,
-1, -1, 0,
1, 1, 0,
-1, 1, 0
]), 3))
geom.addAttribute('uv', new THREE.BufferAttribute(new Float32Array([0,1, 1,1, 1,0, 0,1, 1,0, 0,0 ]), 2))

this.simulationMaterial = new THREE.ShaderMaterial({
uniforms: {
positions: {type: 't', value: this.positionsA },
elapsedTime: {type: 'f', value: 0},
delta: {type: 'f', value: 0},
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = vec2(uv.x, uv.y);
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}
`,
fragmentShader: `

#define PI 3.1415926536
#define PI2 6.28318530718

uniform float elapsedTime;
uniform float delta;
uniform sampler2D positions; // Data Texture containing original positions
varying vec2 vUv;
void main() {
vec4 sample = texture2D(positions, vec2(vUv.x, 1.0 - vUv.y));
vec3 pos = sample.xyz;
float lifetime = sample.a;

// increase lifetime by delta, cap it at 1 and start over
lifetime = mod(lifetime + delta, 1.0);
float radius = 125.0;
float radius2 = 250.0;

float index = vUv.x * ${textureSize}.0 + vUv.y * ${textureSize}.0 * ${textureSize}.0;

/* *************************** */
pos.x = vUv.x * radius2 - radius;
pos.y = vUv.y * radius2 - radius;

//pos.x += sin(lifetime * 16.1) * 4.0;
//pos.z += sin(lifetime * 22.5) * 4.0;
//pos.y += lifetime * 25.0;

gl_FragColor = vec4(pos.rgb, lifetime);
}
`
})
this.mesh = new THREE.Mesh(geom, this.simulationMaterial)
this.scene.add(this.mesh)
}
simulate (delta, elapsedTime) {
this.rttIndex = (this.rttIndex + 1) % 2
this.simulationMaterial.uniforms.elapsedTime.value = elapsedTime
this.simulationMaterial.uniforms.delta.value = delta
this.renderer.setRenderTarget(this.rtts[this.rttIndex])
this.renderer.clear()
this.renderer.render(this.scene, this.orthoCamera)
this.renderer.setRenderTarget(null)
const texture = this.rtts[this.rttIndex].texture
this.simulationMaterial.uniforms.positions.value = texture
return texture
}
}
Insert cell
function preloadedDataBuffer (textureSize) {
let len = textureSize * textureSize * 4
const data = new Float32Array(len)
const maxLifeTime = 5
const radius = 10
const height = 50
const PI2 = Math.PI * 2
for (let i4 = 0; i4 < len; i4 += 4) {
const ratio = i4 / len
const ratioMod = (ratio * 4) % 1
data[i4] = 0 // x
data[i4 + 1] = 0 // y
data[i4 + 2] = 0 // z
data[i4 + 3] = Math.random() // lifetime
}
return data
}
Insert cell
Insert cell
function createParticles(textureSize) {
const radius = 50
// create an instanced buffer from a plane buffer geometry
const geometry = new THREE.InstancedBufferGeometry()
geometry.copy(new THREE.PlaneBufferGeometry(1, 1, 1, 1))
// Add/generate attributes buffers for the geometry
const offsets = []
const scales = []
const indexes = []
const lifetimes = []
const spacing = 2
const halfOffset = textureSize * spacing / 2
for (let i = 0; i < numberOfParticles; i += 1) {
offsets.push(
0, 0, 0
)
scales.push(3)
indexes.push(i + 1)
lifetimes.push(0, 0.5 + Math.random() / 2)
}
geometry.addAttribute('offset', new THREE.InstancedBufferAttribute(new Float32Array(offsets), 3))
geometry.addAttribute('scale', new THREE.InstancedBufferAttribute(new Float32Array(scales), 1))
geometry.addAttribute('particleIndex', new THREE.InstancedBufferAttribute(new Float32Array(indexes), 1))
geometry.addAttribute('lifetime', new THREE.InstancedBufferAttribute(new Float32Array(lifetimes), 2))
const material = createRenderMaterial(textureSize)

const mesh = new THREE.Mesh(geometry, material)
return {geometry, material, mesh}
}
Insert cell
function createRenderMaterial (textureSize) {
const textureLoader = new THREE.TextureLoader()
const material = new THREE.RawShaderMaterial( {
uniforms: {
time: { value: 1.0 },
orientation: {type: 'v4', value: new THREE.Quaternion()},
gpgpuOffsets: {type: 't', value: null },
mapper: textureLoader,
// map: new THREE.TextureLoader().load('https://i.imgur.com/h1v0P5s.jpg'),
colorBase: new THREE.Uniform(new THREE.Color(0xd9ff0e)),
colorStart: new THREE.Uniform(new THREE.Color(0x8674fd)),
colorPeak: new THREE.Uniform(new THREE.Color(0x0ed9ff)),
colorEnd: new THREE.Uniform(new THREE.Color(0x333333)),
},
vertexShader: `
precision highp float;

#define PI 3.1415926536
#define PI2 6.28318530718

attribute vec3 position;
attribute vec3 offset;
attribute float scale;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;

uniform float time;

attribute vec2 uv;
varying vec2 vUv;

varying vec4 vSample;
attribute float particleIndex;

uniform sampler2D gpgpuOffsets;
varying float vParticleIndex;

uniform vec4 orientation;
uniform sampler2D mapper;
varying vec4 vSample2;

vec3 applyQuaternionToVector( vec4 q, vec3 v ){
return v + 2.0 * cross( q.xyz, cross( q.xyz, v ) + q.w * v );
}

void main(){
vec4 positions = texture2D(gpgpuOffsets, vec2(mod(particleIndex-1.0, ${textureSize}.0)/${textureSize}.0, floor((particleIndex-1.0)/${textureSize}.0)/${textureSize}.0));

//vSample = sample;


float i = particleIndex - 1.0;

vec4 sample = texture2D(mapper, vec2(
mod(i, ${textureSize}.0) / ${textureSize}.0,
1.0 - i / ${textureSize}.0 / ${textureSize}.0
));
vSample = sample;

float lifetime = positions.a;

vec3 pos = position * vec3(scale, scale, 1.0);
pos = applyQuaternionToVector(orientation, pos);
pos = pos + offset;




pos.xyz += positions.xyz;
pos.z += length(sample.rgb) * 20.0;

// vLifetime = lifetime;
vParticleIndex = particleIndex;
vUv = vec2(uv.x, 1.0-uv.y);


gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0 );
}
`,
fragmentShader: `
precision highp float;

varying vec4 vSample;
varying vec2 vUv;
varying float vParticleIndex;
varying vec2 vLifetime;

uniform float time;

uniform vec3 colorBase;
uniform vec3 colorStart;
uniform vec3 colorPeak;
uniform vec3 colorEnd;
uniform sampler2D mapper;

#define PI 3.1415926536
#define PI2 6.28318530718


float drawBase (in vec2 uv, in float min, in float max) {
float dist = sqrt(dot(uv, uv));
if (dist >= max || dist <= min) {
return 0.0;
}
float sm = smoothstep(max, max - 0.01, dist);
float sm2 = smoothstep(min, min - 0.01, dist);
float alpha = sm * sm2;
return (1.0-alpha);
}

void main() {
float alpha = drawBase(vUv - vec2(0.5), 0.0, 0.5);
// discard if not needed
if (alpha == 0.0) {
discard;
}

vec4 color = vSample;


//color.r = .1;//alpha;
color.a = alpha;

gl_FragColor = color;

}
`,
// side: THREE.DoubleSide,
side: THREE.FrontSide, // only front since the plane will always face the camera
transparent: true,
alphaTest: 0.1,
blending: THREE.NormalBlending
// blending: THREE.MultiplyBlending
// blending: THREE.AdditiveBlending
})
textureLoader.load(imageURI, (texture) => {
material.uniforms.mapper.value = texture
})
return material
}
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more