Published
Edited
Jul 23, 2019
1 star
Insert cell
Insert cell
Insert cell
funPart = `
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 x = floor(vUv.x * ${textureSize}.0);
float y = floor((1.0-vUv.y) * ${textureSize}.0);
float index = x + y * ${textureSize}.0;
float speed = 1.2;

if (x == 0.0) {
float innerX = mod(y, ${textureSize / 8}.0);
float innerY = floor(y / ${textureSize / 8}.0);
pos.x = innerX * 10.0 - 35.0;
pos.y = innerY * 10.0 - 35.0;
pos.x += cos(elapsedTime * speed) * 4.0;
pos.y += sin(elapsedTime * speed) * 4.0;
pos.z = 0.0;
} else {
vec2 neighborUv = vec2(
mod(x-1.0, ${textureSize}.0)/${textureSize}.0,
y/${textureSize}.0
);
vec4 neighbor = texture2D(positions, neighborUv);
pos.xyz = mix(sample.xyz, neighbor.xyz, 0.12);
}

// temporary
lifetime = index;

gl_FragColor = vec4(pos.rgb, lifetime);`
Insert cell
Insert cell
Insert cell
textureSize = 64
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 = uv; // vec2(uv.x, 1.0-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;

vec3 lerp (vec3 valueA, vec3 valueB, float ratio) {
return (valueA * ratio) + (valueB * (1.0 - ratio));
}
void main() {
${funPart}
}
`
})
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
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(
// Math.random() * 20.0 - 10.0,
// Math.random() * 20.0 - 10.0,
// Math.random() * 20.0 - 10.0
0, 0, 0
)
scales.push(1)
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)),
},
// transparent: true,
// alpha: true,
side: THREE.DoubleSide,
// alphaTest: 0.1,
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;
attribute float particleIndex;

uniform sampler2D gpgpuOffsets;
varying float vParticleIndex;

uniform vec4 orientation;
uniform sampler2D mapper;
varying vec4 vColor;

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)
);
float lifetime = positions.a;
float i = particleIndex - 1.0;
vec4 sample = texture2D(mapper, vec2(
mod(i, ${textureSize}.0) / ${textureSize}.0,
1.0 - i / ${textureSize}.0 / ${textureSize}.0
));
vec3 pos = position * vec3(scale, scale, 1.0);
// vec3 pos = position * vec3(scale * lifetime * 2.0, scale * lifetime * 2.0, 1.0);
pos = applyQuaternionToVector(orientation, pos);
pos = pos + offset + positions.xyz;

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

vColor.xyz = sample.xyz;
vColor.a = lifetime;
// vColor.a = sample.a;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0 );
}
`,
fragmentShader: `
precision highp float;

varying vec4 vColor;
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 = vColor;
float r = vColor.a / ${numberOfParticles}.0;
color.rgba = vec4(r, 0.0, 1.0-r, 1.0);
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
THREE = {
const THREE = window.THREE = await require('three@0.105/build/three.min.js')
await require('three@0.105/examples/js/controls/OrbitControls.js').catch(() => {})
return THREE
}
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more