@absulit/points
Version:
A Generative Art library made in WebGPU
1,768 lines (1,506 loc) • 153 kB
JavaScript
/* @ts-self-types="./points.d.ts" */
/**
* In different calls to the main {@link Points} class, it is used to
* tell the library in what stage of the shaders the data to be sent.
* @class ShaderType
*
* @example
* // Send storage data to the Fragment Shaders only
* points.setStorage('variables', 'Variables', false, ShaderType.FRAGMENT);
* points.setStorage('objects', `array<Object, ${numObjects}>`, false, ShaderType.FRAGMENT);
*
* @example
* // Send storage data to the Compute Shaders only
* points.setStorage('variables', 'Variable', false, ShaderType.COMPUTE);
*
*/
class ShaderType {
/**
* Vertex Shader
*/
static VERTEX = 1;
/**
* Compute Shader
*/
static COMPUTE = 2;
/**
* Fragment Shader
*/
static FRAGMENT = 3;
}
/**
* A RenderPass is a way to have a block of shaders to pass to your application pipeline and
* these render passes will be executed in the order you pass them in the {@link Points#init} method.
*
* @example
* import Points, { RenderPass } from 'points';
* // vert, frag and compute are strings with the wgsl shaders.
* let renderPasses = [
* new RenderPass(vert1, frag1, compute1),
* new RenderPass(vert2, frag2, compute2)
* ];
* // we pass the array of renderPasses
* await points.init(renderPasses);
*/
class RenderPass {
#vertexShader;
#computeShader;
#fragmentShader;
#compiledShaders
#computePipeline = null;
#renderPipeline = null;
#computeBindGroup = null;
#uniformBindGroup = null;
#bindGroupLayout = null;
#bindGroupLayoutCompute = null;
#entries = null;
#internal = false;
#hasComputeShader;
#hasVertexShader;
#hasFragmentShader;
#hasVertexAndFragmentShader;
#workgroupCountX;
#workgroupCountY;
#workgroupCountZ;
/**
* A collection of Vertex, Compute and Fragment shaders that represent a RenderPass.
* This is useful for PostProcessing.
* @param {String} vertexShader WGSL Vertex Shader in a String.
* @param {String} fragmentShader WGSL Fragment Shader in a String.
* @param {String} computeShader WGSL Compute Shader in a String.
*/
constructor(vertexShader, fragmentShader, computeShader, workgroupCountX, workgroupCountY, workgroupCountZ) {
this.#vertexShader = vertexShader;
this.#computeShader = computeShader;
this.#fragmentShader = fragmentShader;
this.#compiledShaders = {
vertex: '',
compute: '',
fragment: '',
};
this.#hasComputeShader = !!this.#computeShader;
this.#hasVertexShader = !!this.#vertexShader;
this.#hasFragmentShader = !!this.#fragmentShader;
this.#hasVertexAndFragmentShader = this.#hasVertexShader && this.#hasFragmentShader;
this.#workgroupCountX = workgroupCountX || 8;
this.#workgroupCountY = workgroupCountY || 8;
this.#workgroupCountZ = workgroupCountZ || 1;
Object.seal(this);
}
/**
* To use with {link RenderPasses} so it's internal
* @ignore
*/
get internal() {
return this.#internal;
}
set internal(value) {
this.#internal = value;
}
/**
* get the vertex shader content
*/
get vertexShader() {
return this.#vertexShader;
}
/**
* get the compute shader content
*/
get computeShader() {
return this.#computeShader;
}
/**
* get the fragment shader content
*/
get fragmentShader() {
return this.#fragmentShader;
}
set computePipeline(value) {
this.#computePipeline = value;
}
get computePipeline() {
return this.#computePipeline;
}
set renderPipeline(value) {
this.#renderPipeline = value;
}
get renderPipeline() {
return this.#renderPipeline;
}
set computeBindGroup(value) {
this.#computeBindGroup = value;
}
get computeBindGroup() {
return this.#computeBindGroup;
}
set uniformBindGroup(value) {
this.#uniformBindGroup = value;
}
get uniformBindGroup() {
return this.#uniformBindGroup;
}
set bindGroupLayout(value) {
this.#bindGroupLayout = value;
}
get bindGroupLayout() {
return this.#bindGroupLayout;
}
set bindGroupLayoutCompute(value) {
this.#bindGroupLayoutCompute = value;
}
get bindGroupLayoutCompute() {
return this.#bindGroupLayoutCompute;
}
set entries(value) {
this.#entries = value;
}
get entries() {
return this.#entries;
}
get compiledShaders() {
return this.#compiledShaders;
}
get hasComputeShader() {
return this.#hasComputeShader;
}
get hasVertexShader() {
return this.#hasVertexShader;
}
get hasFragmentShader() {
return this.#hasFragmentShader;
}
get hasVertexAndFragmentShader() {
return this.#hasVertexAndFragmentShader;
}
get workgroupCountX() {
return this.#workgroupCountX;
}
get workgroupCountY() {
return this.#workgroupCountY;
}
get workgroupCountZ() {
return this.#workgroupCountZ;
}
}
const vert$8 = /*wgsl*/`
@vertex
fn main(
@location(0) position: vec4f,
@location(1) color: vec4f,
@location(2) uv: vec2f,
@builtin(vertex_index) vertexIndex: u32
) -> Fragment {
return defaultVertexBody(position, color, uv);
}
`;
/**
* These are wgsl functions, not js functions.
* The function is enclosed in a js string constant,
* to be appended into the code to reference it in the string shader.
* @module points/image
*/
/**
* Places a texture. The texture being an image loaded from the JS side.
* @type {String}
* @param {texture_2d<f32>} texture `texture_2d<f32>`
* @param {sampler} aSampler `sampler`
* @param {vec2f} uv `vec2f`
* @param {bool} crop `bool`
* @returns {vec4f}
*
* @example
*
* // js
* import { texture } from 'points/image';
*
* await points.setTextureImage('image', 'myimage.jpg');
*
* // wgsl string
* ${texture}
* let value = texture(image, imageSampler, uvr, true);
*/
const texture = /*wgsl*/`
fn texture(texture:texture_2d<f32>, aSampler:sampler, uv:vec2f, crop:bool) -> vec4f {
let flipTexture = vec2(1.,-1.);
let flipTextureCoordinates = vec2(-1.,1.);
let dims:vec2u = textureDimensions(texture, 0);
let dimsF32 = vec2f(dims);
let minScreenSize = params.screen.y;
let imageRatio = dimsF32 / minScreenSize;
let displaceImagePosition = vec2(0., 1.);
let imageUV = uv / imageRatio * flipTexture + displaceImagePosition;
var rgbaImage = textureSample(texture, aSampler, imageUV);
// e.g. if uv.x < 0. OR uv.y < 0. || uv.x > imageRatio.x OR uv.y > imageRatio.y
if (crop && (any(uv < vec2(0.0)) || any(uv > imageRatio))) {
rgbaImage = vec4(0.);
}
return rgbaImage;
}
`;
/**
* Places texture in a position
* @type {String}
* @param {texture_2d<f32>} texture `texture_2d<f32>`
* @param {sampler} aSampler `sampler`
* @param {vec2f} position `vec2f`
* @param {vec2f} uv `vec2f`
* @param {bool} crop `bool`
* @returns {vec4f}
*
* @example
* // js
* import { texturePosition } from 'points/image';
*
* await points.setTextureImage('image', 'myimage.jpg');
*
* // wgsl string
* ${texturePosition}
* let value = texturePosition(image, imageSampler, vec2f(), uvr, true);
*/
const texturePosition = /*wgsl*/`
fn texturePosition(texture:texture_2d<f32>, aSampler:sampler, position:vec2f, uv:vec2f, crop:bool) -> vec4f {
let flipTexture = vec2(1.,-1.);
let flipTextureCoordinates = vec2(-1.,1.);
let dims: vec2<u32> = textureDimensions(texture, 0);
let dimsF32 = vec2f(dims);
let minScreenSize = params.screen.y;
let imageRatio = dimsF32 / minScreenSize;
let displaceImagePosition = position * flipTextureCoordinates / imageRatio + vec2(0., 1.);
let top = position + vec2(0, imageRatio.y);
let imageUV = uv / imageRatio * flipTexture + displaceImagePosition;
var rgbaImage = textureSample(texture, aSampler, imageUV);
// e.g. if uv.x < 0. OR uv.y < 0. || uv.x > imageRatio.x OR uv.y > imageRatio.y
if (crop && (any(uv < vec2(0.0)) || any(uv > imageRatio))) {
rgbaImage = vec4(0.);
}
return rgbaImage;
}
`;
/**
* Increase the aparent pixel size of the texture image using `texturePosition`.
* This reduces the quality of the image.
* @type {String}
* @param {texture_2d<f32>} texture `texture_2d<f32>`
* @param {sampler} textureSampler `sampler`
* @param {vec2f} position `vec2f`
* @param {f32} pixelsWidth `f32`
* @param {f32} pixelsHeight `f32`
* @param {vec2f} uv `vec2f`
* @returns {vec4f}
*
* @example
* // js
* import { pixelateTexturePosition } from 'points/image';
*
* // wgsl string
* ${pixelateTexturePosition}
* let value = pixelateTexturePosition(image, imageSampler, vec2f(), 10,10, uvr);
*/
const pixelateTexturePosition = /*wgsl*/`
fn pixelateTexturePosition(texture:texture_2d<f32>, textureSampler:sampler, position:vec2f, pixelsWidth:f32, pixelsHeight:f32, uv:vec2f) -> vec4f {
let dx = pixelsWidth * (1. / params.screen.x);
let dy = pixelsHeight * (1. / params.screen.y);
let coord = vec2(dx*floor( uv.x / dx), dy * floor( uv.y / dy));
//texturePosition(texture:texture_2d<f32>, aSampler:sampler, position:vec2f, uv:vec2f, crop:bool) -> vec4f {
return texturePosition(texture, textureSampler, position, coord, true);
}
`;
const frag$8 = /*wgsl*/`
${texturePosition}
@fragment
fn main(
@location(0) color: vec4f,
@location(1) uv: vec2f,
@location(2) ratio: vec2f, // relation between params.screen.x and params.screen.y
@location(3) uvr: vec2f, // uv with aspect ratio corrected
@location(4) mouse: vec2f,
@builtin(position) position: vec4f
) -> @location(0) vec4f {
let imageColor = texturePosition(renderpass_feedbackTexture, renderpass_feedbackSampler, vec2(0., 0), uvr, true);
let colorParam = vec4(params.color_r, params.color_g, params.color_b, params.color_a);
let finalColor:vec4f = (imageColor + colorParam) * params.color_blendAmount;
return finalColor;
}
`;
const color = {
vertexShader: vert$8,
fragmentShader: frag$8,
init: async (points, params) => {
points._setInternal(true);
points.setSampler('renderpass_feedbackSampler', null);
points.setTexture2d('renderpass_feedbackTexture', true);
points.setUniform('color_blendAmount', params?.blendAmount || .5);
points.setUniform('color_r', params?.color[0] || 1);
points.setUniform('color_g', params?.color[1] || 1);
points.setUniform('color_b', params?.color[2] || 0);
points.setUniform('color_a', params?.color[3] || 1);
points._setInternal(false);
},
update: points => {
}
};
const vert$7 = /*wgsl*/`
@vertex
fn main(
@location(0) position: vec4f,
@location(1) color: vec4f,
@location(2) uv: vec2f,
@builtin(vertex_index) vertexIndex: u32
) -> Fragment {
return defaultVertexBody(position, color, uv);
}
`;
/**
* Utilities for animation.
* <br>
* Functions that use sine and `params.time` to increase and decrease a value over time.
* <br>
* <br>
* These are wgsl functions, not js functions.
* The function is enclosed in a js string constant,
* to be appended into the code to reference it in the string shader.
* @module points/animation
*/
/**
* Animates `sin()` over `params.time` and a provided `speed`.
* The value is normalized, so in the range 0..1
* @type {String}
* @param {f32} speed
* @example
* // js
* import { fnusin } from 'points/animation';
*
* // wgsl string
* ${fnusin}
* let value = fnusin(2.);
*/
const fnusin = /*wgsl*/`
fn fnusin(speed: f32) -> f32{
return (sin(params.time * speed) + 1.) * .5;
}
`;
/**
* A few color constants and wgsl methods to work with colors.
* <br>
* <br>
* These are wgsl functions, not js functions.
* The function is enclosed in a js string constant,
* to be appended into the code to reference it in the string shader.
* @module points/color
*/
/**
* WHITE color;
* @type {vec4f}
*
* @example
* // js
* import { WHITE } from 'points/color';
*
* // wgsl string
* ${WHITE}
* let value = WHITE * vec4f(.5);
*/
const WHITE = /*wgsl*/`
const WHITE = vec4(1.,1.,1.,1.);
`;
/**
* Compute the FFT (Fast Fourier Transform)
* @type {String}
* @param {f32} input `f32`
* @param {i32} iterations `i32` 2, two is good
* @param {f32} intensity `f32` 0..1 a percentage
* @returns {f32}
*
* @example
* // js
* import { bloom } from 'points/color';
*
* // wgsl string
* ${bloom}
* let value = bloom(input, iterations, intensity);
*/
const bloom$1 = /*wgsl*/`
fn bloom(input:f32, iterations:i32, intensity:f32) -> f32 {
var output = 0.;
let iterationsF32 = f32(iterations);
for (var k = 0; k < iterations; k++) {
let kf32 = f32(k);
for (var n = 0; n < iterations; n++) {
let coef = cos(2. * PI * kf32 * f32(n) / iterationsF32 );
output += input * coef * intensity;
}
}
return output;
}
`;
/**
* Returns the perceived brightness of a color by the eye.<br>
* // Standard<br>
* `LuminanceA = (0.2126*R) + (0.7152*G) + (0.0722*B)`
* @type {String}
* @param {vec4f} color
* @returns {f32}
* @example
* // js
* import { brightness } from 'points/color';
*
* // wgsl string
* ${brightness}
* let value = brightness(rgba);
*/
const brightness = /*wgsl*/`
fn brightness(color:vec4f) -> f32 {
// // Standard
// LuminanceA = (0.2126*R) + (0.7152*G) + (0.0722*B)
// // Percieved A
// LuminanceB = (0.299*R + 0.587*G + 0.114*B)
// // Perceived B, slower to calculate
// LuminanceC = sqrt(0.299*(R**2) + 0.587*(G**2) + 0.114*(B**2))
return (0.2126 * color.r) + (0.7152 * color.g) + (0.0722 * color.b);
}
`;
const frag$7 = /*wgsl*/`
${fnusin}
${texturePosition}
${brightness}
${WHITE}
@fragment
fn main(
@location(0) color: vec4f,
@location(1) uv: vec2f,
@location(2) ratio: vec2f, // relation between params.screen.x and params.screen.y
@location(3) uvr: vec2f, // uv with aspect ratio corrected
@location(4) mouse: vec2f,
@builtin(position) position: vec4f
) -> @location(0) vec4f {
let imageColor = texturePosition(renderpass_feedbackTexture, renderpass_feedbackSampler, vec2(0., 0), uvr, true);
let finalColor:vec4f = brightness(imageColor) * WHITE;
return finalColor;
}
`;
const grayscale = {
vertexShader: vert$7,
fragmentShader: frag$7,
init: async (points, params) => {
points._setInternal(true);
points.setSampler('renderpass_feedbackSampler', null);
points.setTexture2d('renderpass_feedbackTexture', true);
points._setInternal(false);
},
update: points => {
}
};
const vert$6 = /*wgsl*/`
@vertex
fn main(
@location(0) position: vec4f,
@location(1) color: vec4f,
@location(2) uv: vec2f,
@builtin(vertex_index) vertexIndex: u32
) -> Fragment {
return defaultVertexBody(position, color, uv);
}
`;
const frag$6 = /*wgsl*/`
${texturePosition}
@fragment
fn main(
@location(0) color: vec4f,
@location(1) uv: vec2f,
@location(2) ratio: vec2f, // relation between params.screen.x and params.screen.y
@location(3) uvr: vec2f, // uv with aspect ratio corrected
@location(4) mouse: vec2f,
@builtin(position) position: vec4f
) -> @location(0) vec4f {
let imageColor = texturePosition(renderpass_feedbackTexture, renderpass_feedbackSampler, vec2(0., 0), uvr, true);
// --------- chromatic displacement vector
let cdv = vec2(params.chromaticAberration_distance, 0.);
let d = distance(vec2(.5,.5), uvr);
let imageColorR = texturePosition(renderpass_feedbackTexture, renderpass_feedbackSampler, vec2(0.) * ratio, uvr + cdv * d, true).r;
let imageColorG = texturePosition(renderpass_feedbackTexture, renderpass_feedbackSampler, vec2(0.) * ratio, uvr, true).g;
let imageColorB = texturePosition(renderpass_feedbackTexture, renderpass_feedbackSampler, vec2(0.) * ratio, uvr - cdv * d, true).b;
let finalColor:vec4f = vec4(imageColorR, imageColorG, imageColorB, 1);
return finalColor;
}
`;
const chromaticAberration = {
vertexShader: vert$6,
fragmentShader: frag$6,
init: async (points, params) => {
points._setInternal(true);
points.setSampler('renderpass_feedbackSampler', null);
points.setTexture2d('renderpass_feedbackTexture', true);
points.setUniform('chromaticAberration_distance', params.distance);
points._setInternal(false);
},
update: points => {
}
};
const vert$5 = /*wgsl*/`
@vertex
fn main(
@location(0) position: vec4f,
@location(1) color: vec4f,
@location(2) uv: vec2f,
@builtin(vertex_index) vertexIndex: u32
) -> Fragment {
return defaultVertexBody(position, color, uv);
}
`;
const frag$5 = /*wgsl*/`
${texturePosition}
${pixelateTexturePosition}
@fragment
fn main(
@location(0) color: vec4f,
@location(1) uv: vec2f,
@location(2) ratio: vec2f, // relation between params.screen.x and params.screen.y
@location(3) uvr: vec2f, // uv with aspect ratio corrected
@location(4) mouse: vec2f,
@builtin(position) position: vec4f
) -> @location(0) vec4f {
let pixelatedColor = pixelateTexturePosition(
renderpass_feedbackTexture,
renderpass_feedbackSampler,
vec2(0.),
params.pixelate_pixelsWidth,
params.pixelate_pixelsHeight,
uvr
);
let finalColor:vec4f = pixelatedColor;
return finalColor;
}
`;
const pixelate = {
vertexShader: vert$5,
fragmentShader: frag$5,
init: async (points, params) => {
points._setInternal(true);
points.setSampler('renderpass_feedbackSampler', null);
points.setTexture2d('renderpass_feedbackTexture', true);
points.setUniform('pixelate_pixelsWidth', params.pixelsWidth);
points.setUniform('pixelate_pixelsHeight', params.pixelsHeight);
points._setInternal(false);
},
update: points => {
}
};
const vert$4 = /*wgsl*/`
@vertex
fn main(
@location(0) position: vec4f,
@location(1) color: vec4f,
@location(2) uv: vec2f,
@builtin(vertex_index) vertexIndex: u32
) -> Fragment {
return defaultVertexBody(position, color, uv);
}
`;
/**
* Math utils
*
* These are wgsl functions, not js functions.
* The function is enclosed in a js string constant,
* to be appended into the code to reference it in the string shader.
* @module points/math
*/
/**
* PI is the ratio of a circle's circumference to its diameter.
*
* @see https://en.wikipedia.org/wiki/Pi
*
* @example
* // js
* import { PI } from 'points/math';
*
* // wgsl string
* ${PI}
* let value = PI * 3;
*/
const PI = /*wgsl*/`const PI = 3.14159265;`;
/**
* Using polar coordinates, calculates the final point as `vec2f`
* @type {String}
* @param {f32} distance distance from origin
* @param {f32} radians Angle in radians
*
* @example
* // js
* import { polar } from 'points/math';
*
* // wgsl string
* ${polar}
* let value = polar(distance, radians);
*/
const polar = /*wgsl*/`
fn polar(distance: f32, radians: f32) -> vec2f {
return vec2f(distance * cos(radians), distance * sin(radians));
}
`;
/**
* Rotates a vector an amount of radians
* @type {String}
* @param {vec2f} p vector to rotate
* @param {f32} rads angle in radians
*
* @example
* // js
* import { rotateVector } from 'points/math';
*
* // wgsl string
* ${rotateVector}
* let value = rotateVector(position, radians);
*/
const rotateVector = /*wgsl*/`
fn rotateVector(p:vec2f, rads:f32 ) -> vec2f {
let s = sin(rads);
let c = cos(rads);
let xnew = p.x * c - p.y * s;
let ynew = p.x * s + p.y * c;
return vec2(xnew, ynew);
}
`;
/**
* original: Author : Ian McEwan, Ashima Arts.
* https://github.com/ashima/webgl-noise/blob/master/src/noise2D.glsl
*
* These are wgsl functions, not js functions.
* The function is enclosed in a js string constant,
* to be appended into the code to reference it in the string shader.
* @module points/noise2d
*/
/**
* Sinplex Noise function
* @type {String}
* @param {vec2f} v usually the uv
* @returns {f32}
*
* @example
* // js
* import { snoise } from 'points/noise2d';
*
* // wgsl string
* ${snoise}
* let value = snoise(uv);
*/
const snoise = /*wgsl*/`
fn mod289_v3(x: vec3f) -> vec3f {
return x - floor(x * (1.0 / 289.0)) * 289.0;
}
fn mod289_v2(x: vec2f) -> vec2f {
return x - floor(x * (1.0 / 289.0)) * 289.0;
}
fn permute(x: vec3f) -> vec3f {
return mod289_v3(((x*34.0)+10.0)*x);
}
fn snoise(v:vec2f) -> f32 {
let C = vec4(0.211324865405187, // (3.0-sqrt(3.0))/6.0
0.366025403784439, // 0.5*(sqrt(3.0)-1.0)
-0.577350269189626, // -1.0 + 2.0 * C.x
0.024390243902439); // 1.0 / 41.0
// First corner
var i = floor(v + dot(v, C.yy) );
var x0 = v - i + dot(i, C.xx);
// Other corners
var i1 = vec2(0.);
//i1.x = step( x0.y, x0.x ); // x0.x > x0.y ? 1.0 : 0.0
//i1.y = 1.0 - i1.x;
//i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
if(x0.x > x0.y){ i1 = vec2(1.0, 0.0); }else{ i1 = vec2(0.0, 1.0); }
//x0 = x0 - 0.0 + 0.0 * C.xx ;
// x1 = x0 - i1 + 1.0 * C.xx ;
// x2 = x0 - 1.0 + 2.0 * C.xx ;
var x12 = x0.xyxy + C.xxzz;
//x12.xy -= i1;
x12 = vec4(x12.xy - i1, x12.zw); // ?? fix
// Permutations
i = mod289_v2(i); // Avoid truncation effects in permutation
let p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 ))
+ i.x + vec3(0.0, i1.x, 1.0 ));
var m = max(vec3(0.5) - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), vec3(0.0));
m = m*m ;
m = m*m ;
// Gradients: 41 points uniformly over a line, mapped onto a diamond.
// The ring size 17*17 = 289 is close to a multiple of 41 (41*7 = 287)
let x = 2.0 * fract(p * C.www) - 1.0;
let h = abs(x) - 0.5;
let ox = floor(x + 0.5);
let a0 = x - ox;
// Normalise gradients implicitly by scaling m
// Approximation of: m *= inversesqrt( a0*a0 + h*h );
m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h );
// Compute final noise value at P
var g = vec3(0.);
g.x = a0.x * x0.x + h.x * x0.y;
//g.yz = a0.yz * x12.xz + h.yz * x12.yw;
g = vec3(g.x,a0.yz * x12.xz + h.yz * x12.yw);
return 130.0 * dot(m, g);
}
`;
// https://www.geeks3d.com/20140213/glsl-shader-library-fish-eye-and-dome-and-barrel-distortion-post-processing-filters/
const frag$4 = /*wgsl*/`
${texture}
${rotateVector}
${snoise}
${PI}
${WHITE}
${polar}
fn angle(p1:vec2f, p2:vec2f) -> f32 {
let d = p1 - p2;
return abs(atan2(d.y, d.x)) / PI;
}
@fragment
fn main(
@location(0) color: vec4f,
@location(1) uv: vec2f,
@location(2) ratio: vec2f, // relation between params.screen.x and params.screen.y
@location(3) uvr: vec2f, // uv with aspect ratio corrected
@location(4) mouse: vec2f,
@builtin(position) position: vec4f
) -> @location(0) vec4f {
let imagePosition = vec2(0.0,0.0) * ratio;
let center = vec2(.5,.5) * ratio;
let d = distance(center, uvr); // sqrt(dot(d, d));
//vector from center to current fragment
let vectorToCenter = uvr - center;
let sqrtDotCenter = sqrt(dot(center, center));
//amount of effect
let power = 2.0 * PI / (2.0 * sqrtDotCenter ) * (params.lensDistortion_amount - 0.5);
//radius of 1:1 effect
var bind = .0;
if (power > 0.0){
//stick to corners
bind = sqrtDotCenter;
} else {
//stick to borders
if (ratio.x < 1.0) {
bind = center.x;
} else {
bind = center.y;
};
}
//Weird formulas
var nuv = uvr;
if (power > 0.0){//fisheye
nuv = center + normalize(vectorToCenter) * tan(d * power) * bind / tan( bind * power);
} else if (power < 0.0){//antifisheye
nuv = center + normalize(vectorToCenter) * atan(d * -power * 10.0) * bind / atan(-power * bind * 10.0);
} else {
nuv = uvr;
}
// let imageColor = texturePosition(renderpass_feedbackTexture, renderpass_feedbackSampler, imagePosition, nuv, false);
// Chromatic Aberration --
// --------- chromatic displacement vector
let cdv = vec2(params.lensDistortion_distance, 0.);
// let dis = distance(vec2(.5,.5), uvr);
let imageColorR = texture(renderpass_feedbackTexture, renderpass_feedbackSampler, nuv + cdv * params.lensDistortion_amount , true).r;
let imageColorG = texture(renderpass_feedbackTexture, renderpass_feedbackSampler, nuv, true).g;
let imageColorB = texture(renderpass_feedbackTexture, renderpass_feedbackSampler, nuv - cdv * params.lensDistortion_amount , true).b;
let chromaticAberration:vec4f = vec4(imageColorR, imageColorG, imageColorB, 1);
// -- Chromatic Aberration
let finalColor = chromaticAberration;
// let finalColor = vec4(nuv,0,1) * WHITE;
return finalColor;
}
`;
const lensDistortion = {
vertexShader: vert$4,
fragmentShader: frag$4,
init: async (points, params) => {
points._setInternal(true);
points.setSampler('renderpass_feedbackSampler', null);
points.setTexture2d('renderpass_feedbackTexture', true);
points.setUniform('lensDistortion_amount', params?.amount || .4);
points.setUniform('lensDistortion_distance', params?.distance || .01);
points._setInternal(false);
},
update: async points => {
}
};
const vert$3 = /*wgsl*/`
@vertex
fn main(
@location(0) position: vec4f,
@location(1) color: vec4f,
@location(2) uv: vec2f,
@builtin(vertex_index) vertexIndex: u32
) -> Fragment {
return defaultVertexBody(position, color, uv);
}
`;
/**
* Various random functions.
*
* These are wgsl functions, not js functions.
* The function is enclosed in a js string constant,
* to be appended into the code to reference it in the string shader.
* @module points/random
*/
/**
* Random number that returns a `vec2f`.<br>
* You have to set the `rand_seed` before calling `rand()`.
* @type {String}
* @return {f32} equivalent to `rand_seed.y` and `rand_seed` is the result.
*
* @example
* // js
* import { rand } from 'points/random';
* rand_seed.x = .01835255;
*
* // wgsl string
* ${rand}
* let value = rand();
*/
const rand = /*wgsl*/`
var<private> rand_seed : vec2f;
fn rand() -> f32 {
rand_seed.x = fract(cos(dot(rand_seed, vec2f(23.14077926, 232.61690225))) * 136.8168);
rand_seed.y = fract(cos(dot(rand_seed, vec2f(54.47856553, 345.84153136))) * 534.7645);
return rand_seed.y;
}
`;
const frag$3 = /*wgsl*/`
${texturePosition}
${rand}
${snoise}
@fragment
fn main(
@location(0) color: vec4f,
@location(1) uv: vec2f,
@location(2) ratio: vec2f, // relation between params.screen.x and params.screen.y
@location(3) uvr: vec2f, // uv with aspect ratio corrected
@location(4) mouse: vec2f,
@builtin(position) position: vec4f
) -> @location(0) vec4f {
let imageColor = texturePosition(renderpass_feedbackTexture, renderpass_feedbackSampler, vec2(0., 0), uvr, true);
rand_seed = uvr + params.time;
var noise = rand();
noise = noise * .5 + .5;
let finalColor = (imageColor + imageColor * noise) * .5;
return finalColor;
}
`;
const filmgrain = {
vertexShader: vert$3,
fragmentShader: frag$3,
init: async (points, params) => {
points._setInternal(true);
points.setSampler('renderpass_feedbackSampler', null);
points.setTexture2d('renderpass_feedbackTexture', true);
points._setInternal(false);
},
update: points => {
}
};
const vert$2 = /*wgsl*/`
@vertex
fn main(
@location(0) position: vec4f,
@location(1) color: vec4f,
@location(2) uv: vec2f,
@builtin(vertex_index) vertexIndex: u32
) -> Fragment {
return defaultVertexBody(position, color, uv);
}
`;
const frag$2 = /*wgsl*/`
${texturePosition}
${bloom$1}
${brightness}
${PI}
@fragment
fn main(
@location(0) color: vec4f,
@location(1) uv: vec2f,
@location(2) ratio: vec2f, // relation between params.screen.x and params.screen.y
@location(3) uvr: vec2f, // uv with aspect ratio corrected
@location(4) mouse: vec2f,
@builtin(position) position: vec4f
) -> @location(0) vec4f {
let startPosition = vec2(0.,0.);
let rgbaImage = texturePosition(renderpass_feedbackTexture, renderpass_feedbackSampler, startPosition, uvr, false); //* .998046;
let input = brightness(rgbaImage);
let bloomVal = bloom(input, i32(params.bloom_iterations), params.bloom_amount);
let rgbaBloom = vec4(bloomVal);
let finalColor:vec4f = rgbaImage + rgbaBloom;
return finalColor;
}
`;
const bloom = {
vertexShader: vert$2,
fragmentShader: frag$2,
/**
*
* @param {Points} points
* @param {*} params
*/
init: async (points, params) => {
points._setInternal(true);
points.setSampler('renderpass_feedbackSampler', null);
points.setTexture2d('renderpass_feedbackTexture', true);
points.setUniform('bloom_amount', params?.amount || .5);
points.setUniform('bloom_iterations', params?.iterations || 2);
points._setInternal(false);
},
update: points => {
}
};
const vert$1 = /*wgsl*/`
@vertex
fn main(
@location(0) position: vec4f,
@location(1) color: vec4f,
@location(2) uv: vec2f,
@builtin(vertex_index) vertexIndex: u32
) -> Fragment {
return defaultVertexBody(position, color, uv);
}
`;
/**
* These are wgsl functions, not js functions.
* The function is enclosed in a js string constant,
* to be appended into the code to reference it in the string shader.
* @module points/effects
*/
/**
* Applies a blur to an image
* <br>
* based on https://github.com/Jam3/glsl-fast-gaussian-blur/blob/master/9.glsl
*
* @param {texture_2d} image
* @param {sampler} imageSampler
* @param {vec2f} position
* @param {vec2f} uv
* @param {vec2f} resolution
* @param {vec2f} direction
*
* @example
* // js
* import { blur9 } from 'points/effects';
*
* // wgsl string
* ${blur9}
* let value = blur9(image, imageSampler, position, uv, resolution, direction);
*/
const blur9 = /*wgsl*/`
fn blur9(image: texture_2d<f32>, imageSampler:sampler, position:vec2f, uv:vec2f, resolution: vec2f, direction: vec2f) -> vec4f {
var color = vec4(0.0);
let off1 = vec2(1.3846153846) * direction;
let off2 = vec2(3.2307692308) * direction;
color += texturePosition(image, imageSampler, position, uv, true) * 0.2270270270;
color += texturePosition(image, imageSampler, position, uv + (off1 / resolution), true) * 0.3162162162;
color += texturePosition(image, imageSampler, position, uv - (off1 / resolution), true) * 0.3162162162;
color += texturePosition(image, imageSampler, position, uv + (off2 / resolution), true) * 0.0702702703;
color += texturePosition(image, imageSampler, position, uv - (off2 / resolution), true) * 0.0702702703;
return color;
}
`;
/**
* WIP
*/
// export const blur8 = /*wgsl*/`
// fn blur8(color:vec4f, colorsAround:array<vec4f, 8>, amount:f32) -> {
// }
// `;
const frag$1 = /*wgsl*/`
${texturePosition}
${PI}
${rotateVector}
${blur9}
@fragment
fn main(
@location(0) color: vec4f,
@location(1) uv: vec2f,
@location(2) ratio: vec2f, // relation between params.screen.x and params.screen.y
@location(3) uvr: vec2f, // uv with aspect ratio corrected
@location(4) mouse: vec2f,
@builtin(position) position: vec4f
) -> @location(0) vec4f {
let feedbackColor = blur9(
renderpass_feedbackTexture,
renderpass_feedbackSampler,
vec2(0.,0),
uvr,
vec2(params.blur_resolution_x, params.blur_resolution_y), // resolution
rotateVector(vec2(params.blur_direction_x, params.blur_direction_y), params.blur_radians) // direction
);
let finalColor = feedbackColor;
return finalColor;
}
`;
const blur = {
vertexShader: vert$1,
fragmentShader: frag$1,
init: async (points, params) => {
points._setInternal(true);
points.setSampler('renderpass_feedbackSampler', null);
points.setTexture2d('renderpass_feedbackTexture', true);
points.setUniform('blur_resolution_x', params?.resolution[0] || 50);
points.setUniform('blur_resolution_y', params?.resolution[1] || 50);
points.setUniform('blur_direction_x', params?.direction[0] || .4);
points.setUniform('blur_direction_y', params?.direction[1] || .4);
points.setUniform('blur_radians', params?.radians || 0);
points._setInternal(false);
},
update: points => {
}
};
const vert = /*wgsl*/`
@vertex
fn main(
@location(0) position: vec4f,
@location(1) color: vec4f,
@location(2) uv: vec2f,
@builtin(vertex_index) vertexIndex: u32
) -> Fragment {
return defaultVertexBody(position, color, uv);
}
`;
const frag = /*wgsl*/`
${texturePosition}
${snoise}
@fragment
fn main(
@location(0) color: vec4f,
@location(1) uv: vec2f,
@location(2) ratio: vec2f, // relation between params.screen.x and params.screen.y
@location(3) uvr: vec2f, // uv with aspect ratio corrected
@location(4) mouse: vec2f,
@builtin(position) position: vec4f
) -> @location(0) vec4f {
let scale = params.waves_scale;
let intensity = params.waves_intensity;
let n1 = (snoise(uv / scale + vec2(.03, .4) * params.time) * .5 + .5) * intensity;
let n2 = (snoise(uv / scale + vec2(.3, .02) * params.time) * .5 + .5) * intensity;
let n = n1 + n2;
let imageColor = texturePosition(renderpass_feedbackTexture, renderpass_feedbackSampler, vec2(0., 0), uvr + n2, true);
let finalColor:vec4f = imageColor;
return finalColor;
}
`;
const waves = {
vertexShader: vert,
fragmentShader: frag,
init: async (points, params) => {
points._setInternal(true);
points.setSampler('renderpass_feedbackSampler', null);
points.setTexture2d('renderpass_feedbackTexture', true);
points.setUniform('waves_scale', params?.scale || .45);
points.setUniform('waves_intensity', params?.intensity || .03);
points._setInternal(false);
},
update: points => {
}
};
/**
* List of predefined Render Passes for Post Processing.
* @class
*
* @example
* import Points, { RenderPass, RenderPasses } from 'points';
* const points = new Points('canvas');
*
* let renderPasses = [
* new RenderPass(vert1, frag1, compute1),
* new RenderPass(vert2, frag2, compute2)
* ];
*
* RenderPasses.grayscale(points);
* RenderPasses.chromaticAberration(points, .02);
* RenderPasses.color(points, .5, 1, 0, 1, .5);
* RenderPasses.pixelate(points, 10, 10);
* RenderPasses.lensDistortion(points, .4, .01);
* RenderPasses.filmgrain(points);
* RenderPasses.bloom(points, .5);
* RenderPasses.blur(points, 100, 100, .4, 0, 0.0);
* RenderPasses.waves(points, .05, .03);
*
* await points.init(renderPasses);
*
* update();
*
* function update() {
* points.update();
* requestAnimationFrame(update);
* }
*/
class RenderPasses {
static COLOR = 1;
static GRAYSCALE = 2;
static CHROMATIC_ABERRATION = 3;
static PIXELATE = 4;
static LENS_DISTORTION = 5;
static FILM_GRAIN = 6;
static BLOOM = 7;
static BLUR = 8;
static WAVES = 9;
static #LIST = {
1: color,
2: grayscale,
3: chromaticAberration,
4: pixelate,
5: lensDistortion,
6: filmgrain,
7: bloom,
8: blur,
9: waves,
};
/**
* Adds a `RenderPass` from the `RenderPasses` list
* @param {Points} points References a `Points` instance
* @param {RenderPasses} renderPassId Select a static property from `RenderPasses`
* @param {Object} params An object with the params needed by the `RenderPass`
* @returns {Promise<void>}
*/
static async add(points, renderPassId, params) {
if (points.renderPasses?.length) {
throw '`addPostRenderPass` should be called prior `Points.init()`';
}
let shaders = this.#LIST[renderPassId];
let renderPass = new RenderPass(shaders.vertexShader, shaders.fragmentShader, shaders.computeShader);
renderPass.internal = true;
points.addRenderPass(renderPass);
await shaders.init(points, params);
}
/**
* Color postprocessing
* @param {Points} points a `Points` reference
* @param {Number} r red
* @param {Number} g green
* @param {Number} b blue
* @param {Number} a alpha
* @param {Number} blendAmount how much you want to blend it from 0..1
* @returns {Promise<void>}
*/
static async color(points, r, g, b, a, blendAmount) {
return await RenderPasses.add(points, RenderPasses.COLOR, { color: [r, g, b, a], blendAmount });
}
/**
* Grayscale postprocessing. Takes the brightness of an image and returns it; that makes the grayscale result.
* @param {Points} points a `Points` reference
* @returns {Promise<void>}
*/
static async grayscale(points) {
return await RenderPasses.add(points, RenderPasses.GRAYSCALE);
}
/**
* Chromatic Aberration postprocessing. Color bleeds simulating a lens effect without distortion.
* @param {Points} points a `Points` reference
* @param {Number} distance from 0..1 how far the channels are visually apart from each other in the screen, but the value can be greater and negative
* @returns {Promise<void>}
*/
static async chromaticAberration(points, distance) {
return await RenderPasses.add(points, RenderPasses.CHROMATIC_ABERRATION, { distance });
}
/**
* Pixelate postprocessing. It reduces the amount of pixels in the output preserving the scale.
* @param {Points} points a `Points` reference
* @param {Number} width width of the pixel in pixels
* @param {Number} height width of the pixel in pixels
* @returns {Promise<void>}
*/
static async pixelate(points, width, height) {
return await RenderPasses.add(points, RenderPasses.PIXELATE, { pixelsWidth: width, pixelsHeight: height });
}
/**
* Lens Distortion postprocessing. A fisheye distortion with chromatic aberration.
* @param {Points} points a `Points` reference
* @param {Number} amount positive or negative value on how distorted the image will be
* @param {Number} distance of chromatic aberration: from 0..1 how far the channels are visually apart from each other in the screen, but the value can be greater and negative
* @returns {Promise<void>}
*/
static async lensDistortion(points, amount, distance) {
return await RenderPasses.add(points, RenderPasses.LENS_DISTORTION, { amount, distance });
}
/**
* Film grain postprocessing. White noise added to the output to simulate film irregularities.
* @param {Points} points a `Points` reference
* @returns {Promise<void>}
*/
static async filmgrain(points) {
return await RenderPasses.add(points, RenderPasses.FILM_GRAIN);
}
/**
* Bloom postprocessing. Increases brightness of already bright areas to create a haze effect.
* @param {Points} points a `Points` reference
* @param {Number} amount how bright the effect will be
* @returns {Promise<void>}
*/
static async bloom(points, amount) {
return await RenderPasses.add(points, RenderPasses.BLOOM, { amount });
}
/**
* Blur postprocessing. Softens an image by creating multiple samples.
* @param {Points} points a `Points` reference
* @param {Number} resolutionX Samples in X
* @param {Number} resolutionY Samples in Y
* @param {Number} directionX direction in X
* @param {Number} directionY directon in Y
* @param {Number} radians rotation in radians
* @returns {Promise<void>}
*/
static async blur(points, resolutionX, resolutionY, directionX, directionY, radians) {
return await RenderPasses.add(points, RenderPasses.BLUR, { resolution: [resolutionX, resolutionY], direction: [directionX, directionY], radians });
}
/**
* Waves postprocessing. Distorts the image with noise to create a water like effect.
* @param {Points} points a `Points` reference
* @param {Number} scale how big the wave noise is
* @param {Number} intensity a soft or hard effect
* @returns {Promise<void>}
*/
static async waves(points, scale, intensity) {
return await RenderPasses.add(points, RenderPasses.WAVES, { scale, intensity });
}
}
/**
* Collection of Keys used for the default uniforms
* assigned in the {@link Points} class.
* This is mainly for internal purposes.
* @class UniformKeys
* @ignore
*/
class UniformKeys {
/**
* To set the time in milliseconds
* @type {string}
* @static
*/
static TIME = 'time';
/**
* To set the time after the last frame
* @type {string}
* @static
*/
static DELTA = 'delta';
/**
* To set the current date and time in seconds
* @type {string}
* @static
*/
static EPOCH = 'epoch';
/**
* To set screen dimensions
* @type {string}
* @static
*/
static SCREEN = 'screen';
/**
* To set mouse coordinates
* @type {string}
* @static
*/
static MOUSE = 'mouse';
/**
* To set if the mouse has been clicked.
* @type {string}
* @static
*/
static MOUSE_CLICK = 'mouseClick';
/**
* To set if the mouse is down.
* @type {string}
* @static
*/
static MOUSE_DOWN = 'mouseDown';
/**
* To set if the wheel is moving.
* @type {string}
* @static
*/
static MOUSE_WHEEL = 'mouseWheel';
/**
* To set how much the wheel has moved.
* @type {string}
* @static
*/
static MOUSE_DELTA = 'mouseDelta';
}
/**
* Along with the vertexArray it calculates some info like offsets required for the pipeline.
* Internal use.
* @ignore
*/
class VertexBufferInfo {
#vertexSize
#vertexOffset;
#colorOffset;
#uvOffset;
#vertexCount;
/**
* Along with the vertexArray it calculates some info like offsets required for the pipeline.
* @param {Float32Array} vertexArray array with vertex, color and uv data
* @param {Number} triangleDataLength how many items does a triangle row has in vertexArray
* @param {Number} vertexOffset index where the vertex data starts in a row of `triangleDataLength` items
* @param {Number} colorOffset index where the color data starts in a row of `triangleDataLength` items
* @param {Number} uvOffset index where the uv data starts in a row of `triangleDataLength` items
*/
constructor(vertexArray, triangleDataLength = 10, vertexOffset = 0, colorOffset = 4, uvOffset = 8) {
this.#vertexSize = vertexArray.BYTES_PER_ELEMENT * triangleDataLength; // Byte size of ONE triangle data (vertex, color, uv). (one row)
this.#vertexOffset = vertexArray.BYTES_PER_ELEMENT * vertexOffset;
this.#colorOffset = vertexArray.BYTES_PER_ELEMENT * colorOffset; // Byte offset of triangle vertex color attribute.
this.#uvOffset = vertexArray.BYTES_PER_ELEMENT * uvOffset;
this.#vertexCount = vertexArray.byteLength / this.#vertexSize;
}
get vertexSize() {
return this.#vertexSize;
}
get vertexOffset() {
return this.#vertexOffset;
}
get colorOffset() {
return this.#colorOffset;
}
get uvOffset() {
return this.#uvOffset;
}
get vertexCount() {
return this.#vertexCount;
}
}
class Coordinate {
#x;
#y;
#z;
#value;
constructor(x = 0, y = 0, z = 0) {
this.#x = x;
this.#y = y;
this.#z = z;
this.#value = [x, y, z];
}
set x(value) {
this.#x = value;
this.#value[0] = value;
}
set y(value) {
this.#y = value;
this.#value[1] = value;
}
set z(value) {
this.#z = value;
this.#value[2] = value;
}
get x() {
return this.#x;
}
get y() {
return this.#y;
}
get z() {
return this.#z;
}
get value() {
return this.#value;
}
set(x, y, z) {
this.#x = x;
this.#y = y;
this.#z = z;
this.#value[0] = x;
this.#value[1] = y;
this.#value[2] = z;
}
}
/**
* @class RGBAColor
* @ignore
*/
class RGBAColor {
#value;
constructor(r = 0, g = 0, b = 0, a = 1) {
if (r > 1 && g > 1 && b > 1) {
r /= 255;
g /= 255;
b /= 255;
if (a > 1) {
a /= 255;
}
}
this.#value = [r, g, b, a];
}
set r(value) {
this.#value[0] = value;
}
set g(value) {
this.#value[1] = value;
}
set b(value) {
this.#value[2] = value;
}
set a(value) {
this.#value[3] = value;
}
get r() {
return this.#value[0];
}
get g() {
return this.#value[1];
}
get b() {
return this.#value[2];
}
get a() {
return this.#value[3];
}
get value() {
return this.#value;
}
get brightness() {
// #Standard
// LuminanceA = (0.2126*R) + (0.7152*G) + (0.0722*B)
// #Percieved A
// LuminanceB = (0.299*R + 0.587*G + 0.114*B)
// #Perceived B, slower to calculate
// LuminanceC = sqrt(0.299*(R**2) + 0.587*(G**2) + 0.114*(B**2))
let [r, g, b, a] = this.#value;
return (0.2126 * r) + (0.7152 * g) + (0.0722 * b);
}
set brightness(value) {
this.#value = [value, value, value, 1];
}
set(r, g, b, a) {
this.#value = [r, g, b, a];
}
setColor(color) {
this.#value = [color.r, color.g, color.b, color.a];
}
add(color) {
let [r, g, b, a] = this.#value;
//this.#value = [(r + color.r)/2, (g + color.g)/2, (b + color.b)/2, (a + color.a)/2];
//this.#value = [(r*a + color.r*color.a), (g*a + color.g*color.a), (b*a + color.b*color.a), 1];
this.#value = [(r + color.r), (g + color.g), (b + color.b), (a + color.a)];
}
blend(color) {
let [r0, g0, b0, a0] = this.#value;
let [r1, b1, g1, a1] = color.value;
let a01 = (1 - a0) * a1 + a0;
let r01 = ((1 - a0) * a1 * r1 + a0 * r0) / a01;
let g01 = ((1 - a0) * a1 * g1 + a0 * g0) / a01;
let b01 = ((1 - a0) * a1 * b1 + a0 * b0) / a01;
this.#value = [r01, g01, b01, a01];
}
additive(color) {
// https://gist.github.com/JordanDelcros/518396da1c13f75ee057
let base = this.#value;
let added = color.value;
let mix = [];
mix[3] = 1 - (1 - added[3]) * (1 - base[3]); // alpha
mix[0] = Math.round((added[0] * added[3] / mix[3]) + (base[0] * base[3] * (1 - added[3]) / mix[3])); // red
mix[1] = Math.round((added[1] * added[3] / mix[3]) + (base[1] * base[3] * (1 - added[3]) / mix[3])); // green
mix[2] = Math.round((added[2] * added[3] / mix[3]) + (base[2] * base[3] * (1 - added[3]) / mix[3])); // blue
this.#value = mix;
}
equal(color) {
return (this.#value[0] == color.r) && (this.#value[1] == color.g) && (this.#value[2] == color.b) && (this.#value[3] == color.a);
}
static average(colors) {
// https://sighack.com/post/averaging-rgb-colors-the-right-way
let r = 0, g = 0, b = 0;
for (let index = 0; index < colors.length; index++) {
const color = colors[index];
//if (!color.isNull()) {
r += color.r * color.r;
g += color.g * color.g;
b += color.b * color.b;
//a += color.a