I had some to come up with different approach which produces a similar result. The original code confused me a bit so I have implemented the shape drawing differently.
If you are interested in how the circle drawing works, you can study the GLSL from this shader toy demo. https://www.shadertoy.com/view/Md23DV
The tutorial provides multiple code snippets for shape rendering.
Just for the case StackBlitz didn’t save my code, here is a copy of `Flowmap.ts`.
import { useThrelte } from '@threlte/core';
import {
Mesh,
Vector2,
NodeMaterial,
OrthographicCamera,
RenderTarget,
WebGPURenderer,
QuadMesh,
} from 'three/webgpu';
import {
float,
Fn,
min,
pow,
smoothstep,
texture,
uniform,
uv,
vec2,
vec3,
mix,
} from 'three/tsl';
import { ScreenGeometry } from './ScreenGeometry';
import { onDestroy } from 'svelte';
type FlowmapOptions = {
size?: number;
falloff?: number;
alpha?: number;
dissipation?: number;
};
export class Flowmap {
private _threlte = useThrelte<WebGPURenderer>();
private _texture = texture();
private _location = uniform(new Vector2(-1));
private _velocity = uniform(new Vector2());
private _aspect = uniform(float(1));
private _material = new NodeMaterial();
private _radius = uniform(float(0.1));
private _mesh = new QuadMesh(this._material);
private _mask: {
read: RenderTarget;
write: RenderTarget;
};
private _options: Readonly<Required<FlowmapOptions>>;
constructor(options: FlowmapOptions = {}) {
this._options = Object.freeze({
size: 128,
falloff: 0.3,
alpha: 1,
dissipation: 0.7,
...options,
});
this._mask = {
read: new RenderTarget(this._options.size, this._options.size),
write: new RenderTarget(this._options.size, this._options.size),
};
this._init();
}
private _init() {
this._material.colorNode = this._colorNode();
this._material.depthTest = false;
this._material.depthWrite = false;
// this._material.transparent = true
this._swap();
onDestroy(() => {
this._mask.read.dispose();
this._mask.write.dispose();
this._material.dispose();
});
}
update() {
const { renderer } = this._threlte;
const lastTarget = renderer.getRenderTarget();
renderer.setRenderTarget(this._mask.write);
this._mesh.render(renderer);
renderer.setRenderTarget(lastTarget);
this._swap();
}
get texture() {
return this._texture;
}
get location() {
return this._location;
}
get velocity() {
return this._velocity;
}
get aspect() {
return this._aspect;
}
get radius() {
return this._radius;
}
private _swap() {
const temp = this._mask.read;
this._mask.read = this._mask.write;
this._mask.write = temp;
this._texture.value = this._mask.read.texture;
}
private _circle = Fn(([uv, center, radius]) => {
const position = uv.sub(center);
position.x.mulAssign(this._aspect);
// depending on how you define the first two paramters, the circle gets more hard of soft edges
return smoothstep(
radius.sub(0.05),
radius.add(0.05),
position.length()
).oneMinus();
}).setLayout({
name: 'circle',
type: 'float',
inputs: [
{ name: 'uv', type: 'vec2' },
{ name: 'center', type: 'vec2' },
{ name: 'radius', type: 'float' },
],
});
private _colorNode = Fn(() => {
const color = this._texture.mul(this._options.dissipation).toVar();
// define the color of the circle based on the pointer's velocity
const circleColor = vec3(
this._velocity.mul(vec2(1, -1)),
float(1).sub(pow(float(1).sub(min(float(1), this._velocity.length())), 3))
);
// mix the base color with the circle color.
color.rgb = mix(
color.rgb,
circleColor,
this._circle(uv(), this._location, this.radius)
);
return color;
});
}