UNPKG

@absulit/points

Version:

A Generative Art library made in WebGPU

1,768 lines (1,506 loc) 153 kB
/* @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